新普金娱乐网址


你的人生自由吗

读书笔记:neuralnetworksanddeeplearning chapter1

在讲台上站了10年

  • 一月 18, 2019
  • 数学
  • 没有评论

授业是本人的事业,哦,仍然说应该算得职业呢?

 



参考连接:http://nqdeng.github.io/7-days-nodejs/\#1.1



 

NodeJS基础

什么是NodeJS

JS是脚本语言,脚本语言都需要一个解析器才能运作。对于写在HTML页面里的JS,浏览器充当领悟析器的角色。而对此急需单独运作的JS,NodeJS就是一个解析器。

每一种解析器都是一个运行条件,不但允许JS定义各样数据结构,举办各类总计,还同意JS使用运行条件提供的放到对象和艺术做一些事情。例如运行在浏览器中的JS的用途是操作DOM,浏览器就提供了document等等的放置对象。而运作在NodeJS中的JS的用途是操作磁盘文件或搭建HTTP服务器,NodeJS就相应提供了fshttp等内置对象。

有吗用处

虽然存在一听说可以间接运行JS文件就以为很酷的同班,但多数同学在接触新东西时首先关心的是有吗用处,以及能拉动什么价值。

NodeJS的撰稿人说,他创办NodeJS的目的是为着实现高性能Web服务器,他先是注重的是事件机制和异步IO模型的优越性,而不是JS。可是她需要采用一种编程语言实现他的想法,那种编程语言不可能自带IO功能,并且需要能完美援助事件机制。JS没有自带IO功效,天生就用于拍卖浏览器中的DOM事件,并且有着一大群程序员,由此就成为了天然的挑选。

如她所愿,NodeJS在服务端活跃起来,出现了千千万万基于NodeJS的Web服务。而一方面,NodeJS让前端众如获神器,终于得以让自己的力量覆盖范围跳出浏览器窗口,更大批的前端工具如雨后春笋。

之所以,对于前端而言,即使不是众人都要拿NodeJS写一个服务器程序,但概括可至拔取命令交互形式调试JS代码片段,复杂可至编写工具进步工作效能。

NodeJS生态圈正欣欣向荣。

如何设置

安装程序

NodeJS提供了一些安装程序,都足以在nodejs.org这边下载并安装。

Windows系统下,接纳和连串版本匹配的.msi后缀的安装文件。Mac OS
X系统下,采用.pkg后缀的安装文件。

编译安装

Linux系统下并未现成的安装程序可用,尽管有的发行版可以动用apt-get等等的章程安装,但不必然能设置到最新版。因此Linux系统下一般选用以下措施编译模式安装NodeJS。

  1. 管教系统下g++版本在4.6之上,python版本在2.6之上。

  2. nodejs.org下载tar.gz后缀的NodeJS最新版源代码包并解压到某个地点。

  3. 跻身解压到的目录,使用以下命令编译和装置。

     $ ./configure
     $ make
     $ sudo make install
    

如何运作

开辟终端,键入node跻身命令交互格局,可以输入一条代码语句后立时施行并展现结果,例如:

$ node
> console.log('Hello World!');
Hello World!

即便要运行一大段代码的话,可以先写一个JS文件再运行。例如有以下hello.js

function hello() {
    console.log('Hello World!');
}
hello();

写好后在终端下键入node hello.js运行,结果如下:

$ node hello.js
Hello World!

权限问题

在Linux系统下,使用NodeJS监听80或443端口提供HTTP(S)服务时索要root权限,有二种办法能够形成。

一种格局是应用sudo一声令下运行NodeJS。例如通过以下命令运行的server.js中有权力行使80和443端口。一般推荐这种艺术,可以确保仅为有需要的JS脚本提供root权限。

$ sudo node server.js

另一种方法是利用chmod +s命令让NodeJS总是以root权限运作,具体做法如下。因为这种方法让其他JS脚本都有了root权限,不太安全,因而在急需很考虑安全的系统下不推荐使用。

$ sudo chown root /usr/local/bin/node
$ sudo chmod +s /usr/local/bin/node

模块

编辑稍大一点的先后时相似都会将代码模块化。在NodeJS中,一般将代码合理拆分到不同的JS文件中,每一个文书就是一个模块,而文件路径就是模块名。

在编排每个模块时,都有requireexportsmodule两个优先定义好的变量可供使用。

require

require函数用于在脚下模块中加载和运用此外模块,传入一个模块名,重返一个模块导出对象。模块名可使用相对路径(以./伊始),或者是绝对路径(以/C:等等的盘符开端)。此外,模块名中的.js扩充名可以简简单单。以下是一个例证。

var foo1 = require('./foo');
var foo2 = require('./foo.js');
var foo3 = require('/home/user/foo');
var foo4 = require('/home/user/foo.js');

// foo1至foo4中保存的是同一个模块的导出对象。

除此以外,可以使用以下方法加载和应用一个JSON文件。

var data = require('./data.json');

exports

exports目的是眼前模块的导出对象,用于导出模块公有方法和性质。另外模块通过require函数使用当前模块时取得的就是时下模块的exports对象。以下例子中导出了一个国有方法。

exports.hello = function () {
    console.log('Hello World!');
};

module

通过module目的足以访问到目前模块的部分相关音讯,但最多的用途是替换当前模块的导出对象。例如模块导出对象默认是一个不足为奇对象,假如想改成一个函数的话,可以运用以下办法。

module.exports = function () {
    console.log('Hello World!');
};

上述代码中,模块默认导出对象被轮换为一个函数。

模块开首化

一个模块中的JS代码仅在模块第一次被使用时进行一遍,并在实施过程中最先化模块的导出对象。之后,缓存起来的导出对象被重新使用。

主模块

经过命令行参数传递给NodeJS以启动程序的模块被誉为主模块。主模块负责调度组成总体程序的其他模块形成工作。例如通过以下命令启动程序时,main.js就是主模块。

$ node main.js

总体示例

例如有以下目录。

- /home/user/hello/
    - util/
        counter.js
    main.js

其中counter.js内容如下:

var i = 0;

function count() {
return ++i;
}

exports.count = count;

该模块内部定义了一个私家变量i,并在exports对象导出了一个国有方法count

主模块main.js内容如下:

var counter1 = require('./util/counter');
var    counter2 = require('./util/counter');

console.log(counter1.count());
console.log(counter2.count());
console.log(counter2.count());

运行该程序的结果如下:

$ node main.js
1
2
3

能够观察,counter.js并不曾因为被require了一回而起始化三次。

二进制模块

虽然一般我们应用JS编写模块,但NodeJS也支撑使用C/C++编写二进制模块。编译好的二进制模块除了文件增添名是.node外,和JS模块的选拔办法一样。即便二进制模块能使用操作系统提供的有着机能,拥有无限的潜能,但对于前端同学而言编写过于艰辛,并且难以跨平台运用,由此不在本教程的覆盖范围内。

小结

本章介绍了有关NodeJS的基本概念和采取办法,总结起来有以下知识点:

  • NodeJS是一个JS脚本解析器,任何操作系统下安装NodeJS本质上做的工作都是把NodeJS执行顺序复制到一个索引,然后保证这些目录在系统PATH环境变量下,以便终端下得以应用node命令。

  • 极端下直接输入node一声令下可进入命令交互形式,很适合用来测试一些JS代码片段,比如正则表明式。

  • NodeJS使用CMD模块系统,主模块作为程序入口点,所有模块在实施过程中只开头化两遍。

  • 除非JS模块不可能满意急需,否则不要任意使用二进制模块,否则你的用户会叫苦连天。

代码的团体和布置

有经历的C程序员在编辑一个新程序时首先从make文件写起。同样的,使用NodeJS编写程序前,为了有个非凡的开首,首先需要预备好代码的目录结构和配备格局,就好似修房子要先搭脚手架。本章将介绍与之相关的各类文化。

模块路径解析规则

俺们早已了然,require函数匡助斜杠(/)或盘符(C:)开首的相对路径,也支撑./开班的相对路径。但那两种途径在模块之间建立了强耦合关系,一旦某个模块文件的存放地方需要转移,使用该模块的其它模块的代码也需要随着调整,变得牵一发动全身。由此,require函数协理第二种情势的路径,写法类似于foo/bar,并逐项依照以下规则解析路径,直到找到模块地点。

  1. 放置模块

    假如传递给require函数的是NodeJS内置模块名称,不做路径解析,直接回到内部模块的导出对象,例如require('fs')

  2. node_modules目录

    NodeJS定义了一个与众不同的node_modules目录用于存放模块。例如某个模块的相对路径是/home/user/hello.js,在该模块中选择require('foo/bar')措施加载模块时,则NodeJS依次尝试选取以下途径。

     /home/user/node_modules/foo/bar
     /home/node_modules/foo/bar
     /node_modules/foo/bar
    
  3. NODE_PATH环境变量

    与PATH环境变量类似,NodeJS允许通过NODE_PATH环境变量来指定额外的模块搜索路径。NODE_PATH环境变量中蕴含一到多少个目录路径,路径之间在Linux下利用:分隔,在Windows下使用;相隔。例如定义了以下NODE_PATH环境变量:

     NODE_PATH=/home/user/lib:/home/lib
    

    当使用require('foo/bar')的主意加载模块时,则NodeJS依次尝试以下途径。

     /home/user/lib/foo/bar
     /home/lib/foo/bar
    

包(package)

咱俩已经了解了JS模块的中坚单位是单个JS文件,但复杂些的模块往往由四个子模块组成。为了便于管理和采用,我们得以把由三个子模块组成的大模块称做,并把所有子模块放在同一个索引里。

在重组一个包的所有子模块中,需要有一个输入模块,入口模块的导出对象被看作包的导出对象。例如有以下目录结构。

- /home/user/lib/
    - cat/
        head.js
        body.js
        main.js

其中cat目录定义了一个包,其中包含了3个子模块。main.js作为入口模块,其情节如下:

var head = require('./head');
var body = require('./body');

exports.create = function (name) {
return {
name: name,
head: head.create(),
body: body.create()
};
};

在其他模块里拔取包的时候,需要加载包的输入模块。接着上例,使用require('/home/user/lib/cat/main')能达标目标,可是进口模块名称出现在途径里看起来不是个好主意。因而大家需要做点额外的劳作,让包使用起来更像是单个模块。

index.js

当模块的文本名是index.js,加载模块时方可行使模块所在目录的路线代替模块文件路径,因而接着上例,以下两条语句等价。

var cat = require('/home/user/lib/cat');
var cat = require('/home/user/lib/cat/index');

如此这般处理后,就只需要把包目录路径传递给require函数,感觉上一切目录被用作单个模块使用,更有全体感。

package.json

万一想自定义入口模块的文本名和存放地方,就需要在包目录下富含一个package.json文本,并在里边指定入口模块的路子。上例中的cat模块可以重构如下。

- /home/user/lib/
    - cat/
        + doc/
        - lib/
            head.js
            body.js
            main.js
        + tests/
        package.json

其中package.json情节如下。

{
    "name": "cat",
    "main": "./lib/main.js"
}

如此一来,就一样可以接纳require('/home/user/lib/cat')的章程加载模块。NodeJS会依据包目录下的package.json找到入口模块所在地方。

指令行程序

行使NodeJS编写的东西,要么是一个包,要么是一个限令行程序,而前者最后也会用来开发后者。因而我们在配备代码时索要有些技能,让用户觉得自己是在利用一个发令行程序。

比如说大家用NodeJS写了个程序,可以把命令行参数原样打印出来。该程序很简单,在主模块内实现了具备机能。并且写好后,我们把该程序部署在/home/user/bin/node-echo.js其一地点。为了在其他目录下都能运行该程序,我们需要利用以下终端命令。

$ node /home/user/bin/node-echo.js Hello World
Hello World

这种应用办法看起来有点像是一个下令行程序,下边的才是我们希望的点子。

$ node-echo Hello World

Linux

在Linux系统下,我们得以把JS文件作为shell脚本来运行,从而达到上述目标,具体步骤如下:

  1. 在shell脚本中,能够透过#!诠释来指定当前剧本使用的解析器。所以我们率先在node-echo.js文件顶部扩充以下一行注释,注明当前剧本使用NodeJS解析。

     #! /usr/bin/env node
    

    NodeJS会忽略掉放在JS模块首行的#!讲明,不必担心那行注释是私自语句。

  2. 然后,我们利用以下命令赋予node-echo.js文件执行权限。

     $ chmod +x /home/user/bin/node-echo.js
    
  3. 最后,咱们在PATH环境变量中指定的某部目录下,例如在/usr/local/bin下边创造一个软链文件,文件名与大家愿意利用的顶峰命令同名,命令如下:

     $ sudo ln -s /home/user/bin/node-echo.js /usr/local/bin/node-echo
    

如此处理后,我们就可以在任何目录下利用node-echo命令了。

Windows

在Windows系统下的做法完全不同,我们得靠.cmd文件来缓解问题。即便node-echo.js存放在C:\Users\user\bin目录,并且该目录已经添加到PATH环境变量里了。接下来需要在该目录下新建一个名为node-echo.cmd的文书,文件内容如下:

@node "C:\User\user\bin\node-echo.js" %*

那般处理后,我们就可以在任何目录下使用node-echo命令了。

工程目录

叩问了上述文化后,现在大家可以来完全地设计一个工程目录了。以编制一个发令行程序为例,一般我们会同时提供命令行模式和API情势二种拔取形式,并且大家会借助三方包来编排代码。除了代码外,一个总体的程序也相应有谈得来的文档和测试用例。因而,一个正规的工程目录都看起来像下面这样。

- /home/user/workspace/node-echo/   # 工程目录
    - bin/                          # 存放命令行相关代码
        node-echo
    + doc/                          # 存放文档
    - lib/                          # 存放API相关代码
        echo.js
    - node_modules/                 # 存放三方包
        + argv/
    + tests/                        # 存放测试用例
    package.json                    # 元数据文件
    README.md                       # 说明文件

里头有的文件内容如下:

/* bin/node-echo */
var argv = require('argv'),
    echo = require('../lib/echo');
console.log(echo(argv.join(' ')));

/ lib/echo.js /
module.exports = function (message) {
return message;
};

/ package.json /
{
“name”: “node-echo”,
“main”: “./lib/echo.js”
}

上述例子中分类存放了不同档次的文书,并透过node_moudles目录直接使用三方包名加载模块。此外,定义了package.json之后,node-echo目录也可被用作一个包来使用。

NPM

NPM是及其NodeJS一起安装的包管理工具,能化解NodeJS代码部署上的无数题目,常见的采取情况有以下二种:

  • 允许用户从NPM服务器下载别人编写的三方包到地头使用。

  • 允许用户从NPM服务器下载并设置别人编写的通令行程序到地面使用。

  • 允许用户将协调编辑的包或指令行程序上流传NPM服务器供别人接纳。

可以看来,NPM建立了一个NodeJS生态圈,NodeJS开发者和用户可以在里面互通有无。以下分别介绍这两种意况下怎么样使用NPM。

下载三方包

需要选择三方包时,首先得了解有什么包可用。虽然npmjs.org提供了个搜索框可以依据包名来探寻,但要是连想行使的三方包的名字都不确定的话,就请百度时而呢。知道了包名后,比如上面例子中的argv,就足以在工程目录下开辟终端,使用以下命令来下载三方包。

$ npm install argv
...
argv@0.0.2 node_modules\argv

下载好将来,argv包就放在了工程目录下的node_modules目录中,因而在代码中只需要通过require('argv')的法子就好,无需指定三方包路径。

以上命令默认下载最新版三方包,假设想要下载指定版本的话,可以在包名后边加上@<version>,例如通过以下命令可下载0.0.1版的argv

$ npm install argv@0.0.1
...
argv@0.0.1 node_modules\argv

一旦运用到的三方包相比多,在极限下一个包一条命令地安装未免太人肉了。由此NPM对package.json的字段做了扩张,允许在内部发明三方包倚重。由此,下边例子中的package.json可以改写如下:

{
    "name": "node-echo",
    "main": "./lib/echo.js",
    "dependencies": {
        "argv": "0.0.2"
    }
}

如此这般处理后,在工程目录下就可以动用npm install命令批量装置三方包了。更要紧的是,当未来node-echo也上传到了NPM服务器,外人下载这一个包时,NPM会依照包中注脚的三方包倚重自动下载进一步依靠的三方包。例如,使用npm install node-echo命令时,NPM会自动成立以下目录结构。

- project/
    - node_modules/
        - node-echo/
            - node_modules/
                + argv/
            ...
    ...

如此一来,用户只需关注自己一直选用的三方包,不需要协调去化解所有包的依赖关系。

安装命令行程序

从NPM服务上下载安装一个下令行程序的办法与三方包类似。例如上例中的node-echo提供了命令行使用方法,只要node-echo协调配置好了连带的package.json字段,对于用户而言,只需要拔取以下命令安装程序。

$ npm install node-echo -g

参数中的-g代表全局安装,由此node-echo会默认安装到以下职务,并且NPM会自动成立好Linux系统下需要的软链文件或Windows系统下需要的.cmd文件。

- /usr/local/               # Linux系统下
    - lib/node_modules/
        + node-echo/
        ...
    - bin/
        node-echo
        ...
    ...

- %APPDATA%\npm\            # Windows系统下
    - node_modules\
        + node-echo\
        ...
    node-echo.cmd
    ...

文告代码

首先次使用NPM发表代码前需要注册一个账号。终端下运作npm adduser,之后遵照指示做即可。账号搞定后,接着我们需要编制package.json文件,出席NPM必需的字段。接着上面node-echo的例子,package.json里少不了的字段如下。

{
    "name": "node-echo",           # 包名,在NPM服务器上须要保持唯一
    "version": "1.0.0",            # 当前版本号
    "dependencies": {              # 三方包依赖,需要指定包名和版本号
        "argv": "0.0.2"
      },
    "main": "./lib/echo.js",       # 入口模块位置
    "bin" : {
        "node-echo": "./bin/node-echo"      # 命令行程序名和主模块位置
    }
}

今后,我们就可以在package.json各处目录下运行npm publish发布代码了。

版本号

采用NPM下载和披露代码时都会接触到版本号。NPM使用语义版本号来保管代码,这里大概介绍一下。

语义版本号分为X.Y.Z三位,分别表示主版本号、次版本号和补丁版本号。当代码变更时,版本号按以下标准更新。

+ 如果只是修复bug,需要更新Z位。

+ 如果是新增了功能,但是向下兼容,需要更新Y位。

+ 如果有大变动,向下不兼容,需要更新X位。

本子号有了这多少个保险后,在表达三方包依赖时,除了可凭借于一个恒定版本号外,还可依靠于某个范围的版本号。例如"argv": "0.0.x"代表依赖于0.0.x文山会海的流行版argv。NPM帮助的富有版本号范围点名方式可以查阅官方文档

脑子一点

除却本章介绍的片段外,NPM还提供了不少效率,package.json里也有好多其余有效的字段。除了可以在npmjs.org/doc/查看官方文档外,这里再介绍部分NPM常用命令。

  • NPM提供了不少发令,例如installpublish,使用npm help可查阅所有命令。

  • 使用npm help <command>可查阅某条命令的详尽协助,例如npm help install

  • package.json四处目录下使用npm install . -g可先在地面安装当前命令行程序,可用于发表前的地点测试。

  • 使用npm update <package>可以把当前目录下node_modules子目录里边的照应模块更新至最新版本。

  • 使用npm update <package> -g可以把全局安装的应和命令行程序更新至最新版。

  • 使用npm cache clear可以清空NPM本地缓存,用于对付使用同样版本号发表新本子代码的人。

  • 使用npm unpublish <package>@<version>可以裁撤公布温馨发表过的某个版本代码。

小结

本章介绍了使用NodeJS编写代码前需要做的备选工作,总结起来有以下几点:

  • 编写代码前先规划好目录结构,才能完成有条不紊。

  • 稍大些的程序可以将代码拆分为五个模块管理,更大些的先后可以动用包来组织模块。

  • 合理选拔node_modulesNODE_PATH来解耦包的应用办法和大体路径。

  • 拔取NPM插足NodeJS生态圈互通有无。

  • 想到了向往的包名时请提前在NPM上抢注。

文本操作

让前者觉得如获神器的不是NodeJS能做网络编程,而是NodeJS可以操作文件。小至文件查找,大至代码编译,几乎从未一个前端工具不操作文件。换个角度讲,几乎也只需要部分多少处理逻辑,再添加有些文本操作,就可知编写出大多数前端工具。本章将介绍与之有关的NodeJS内置模块。

开门红

NodeJS提供了基本的文本操作API,不过像文件拷贝这种高档效用就没有提供,因此我们先拿文件拷贝程序练手。与copy命令类似,我们的先后需要能接受源文件路径与目的文件路径六个参数。

小文件拷贝

大家选择NodeJS内置的fs模块简单实现那多少个程序如下。

var fs = require('fs');

function copy(src, dst) {
fs.writeFileSync(dst, fs.readFileSync(src));
}

function main(argv) {
copy(argv[0], argv[1]);
}

main(process.argv.slice(2));

如上程序采用fs.readFileSync从源路径读取文件内容,并选用fs.writeFileSync将文件内容写入目的路径。

豆知识:
process是一个全局变量,可由此process.argv取得命令行参数。由于argv[0]一定等于NodeJS执行顺序的相对路径,argv[1]一直等于主模块的绝对路径,由此首先个命令行参数从argv[2]其一地方上马。

大文件拷贝

下边的先后拷贝一些小文件没啥问题,但这种一遍性把所有文件内容都读取到内存中后再五遍性写入磁盘的主意不适合拷贝大文件,内存会爆仓。对于大文件,大家只能读一些写一些,直到完成拷贝。因而上面的主次需要改造如下。

var fs = require('fs');

function copy(src, dst) {
fs.createReadStream(src).pipe(fs.createWriteStream(dst));
}

function main(argv) {
copy(argv[0], argv[1]);
}

main(process.argv.slice(2));

上述程序行使fs.createReadStream创办了一个源文件的只读数据流,并动用fs.createWriteStream始建了一个目标文件的只写数据流,并且用pipe艺术把五个数据流连接了四起。连接起来后发出的事体,说得抽象点的话,水顺着水管从一个桶流到了另一个桶。

API走马观花

咱俩先大致看看NodeJS提供了怎样和文书操作有关的API。这里并不逐一介绍每个API的行使模式,官方文档已经做得很好了。

Buffer(数据块)

官方文档: http://nodejs.org/api/buffer.html

JS语言自身唯有字符串数据类型,没有二进制数据类型,由此NodeJS提供了一个与String对等的全局构造函数Buffer来提供对二进制数据的操作。除了能够读取文件得到Buffer的实例外,还是可以一贯社团,例如:

var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);

Buffer与字符串类似,除了可以用.length属性拿到字节长度外,还足以用[index]措施读取指定位置的字节,例如:

bin[0]; // => 0x68;

Buffer与字符串可以互相转化,例如可以运用指定编码将二进制数据转发为字符串:

var str = bin.toString('utf-8'); // => "hello"

要么反过来,将字符串转换为指定编码下的二进制数据:

var bin = new Buffer('hello', 'utf-8'); // => <Buffer 68 65 6c 6c 6f>

Buffer与字符串有一个根本区别。字符串是只读的,并且对字符串的别样改动得到的都是一个新字符串,原字符串保持不变。至于Buffer,更像是可以做指针操作的C语言数组。例如,可以用[index]艺术直接修改某个地方的字节。

bin[0] = 0x48;

.slice方法也不是回去一个新的Buffer,而更像是重临了指向原Buffer中档的某个地方的指针,如下所示。

[ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]
    ^           ^
    |           |
   bin     bin.slice(2)

因此对.slice方法再次来到的Buffer的修改会效率于原Buffer,例如:

var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);
var sub = bin.slice(2);

sub[0] = 0x65;
console.log(bin); // =>

也就此,尽管想要拷贝一份Buffer,得首先创立一个新的Buffer,并通过.copy办法把原Buffer中的数据复制过去。这些类似于申请一块新的内存,并把已有内存中的数量复制过去。以下是一个事例。

var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);
var dup = new Buffer(bin.length);

bin.copy(dup);
dup[0] = 0x48;
console.log(bin); // =>
console.log(dup); // =>

总之,Buffer将JS的数额处理能力从字符串扩大到了随机二进制数据。

Stream(数据流)

官方文档: http://nodejs.org/api/stream.html

当内存中不可能几次装下需要处理的数额时,或者一边读取一边处理更加连忙时,我们就需要用到数据流。NodeJS中通过各类Stream来提供对数据流的操作。

如上面的大文件拷贝程序为例,我们可以为数量出自创设一个只读数据流,示例如下:

var rs = fs.createReadStream(pathname);

rs.on(‘data’, function (chunk) {
doSomething(chunk);
});

rs.on(‘end’, function () {
cleanUp();
});

豆知识:
Stream遵照事件机制工作,所有Stream的实例都连续于NodeJS提供的EventEmitter

下边的代码中data事件会源源不断地被触发,不管doSomething函数是否处理得过来。代码可以连续做如下改造,以缓解这些问题。

var rs = fs.createReadStream(src);

rs.on(‘data’, function (chunk) {
rs.pause();
doSomething(chunk, function () {
rs.resume();
});
});

rs.on(‘end’, function () {
cleanUp();
});

如上代码给doSomething函数加上了回调,由此我们得以在处理多少前戛然则止数据读取,并在拍卖数据后持续读取数据。

此外,我们也足以为数据目的成立一个只写数据流,示例如下:

var rs = fs.createReadStream(src);
var ws = fs.createWriteStream(dst);

rs.on(‘data’, function (chunk) {
ws.write(chunk);
});

rs.on(‘end’, function () {
ws.end();
});

我们把doSomething换成了往只写多少流里写入数据后,以上代码看起来就像是一个文本拷贝程序了。可是上述代码存在下边提到的题材,如果写入速度跟不上读取速度的话,只写多少流内部的缓存会爆仓。大家可以依照.write格局的重返值来判定传入的数码是写入目的了,仍旧临时放在了缓存了,并依照drain事件来判断啥时候只写数据流已经将缓存中的数据写入目的,可以流传下一个待写多少了。因而代码可以改造如下:

var rs = fs.createReadStream(src);
var ws = fs.createWriteStream(dst);

rs.on(‘data’, function (chunk) {
if (ws.write(chunk) === false) {
rs.pause();
}
});

rs.on(‘end’, function () {
ws.end();
});

ws.on(‘drain’, function () {
rs.resume();
});

如上代码实现了数量从只读数据流到只写数据流的搬运,并包括了防爆仓控制。因为这种应用情况很多,例如下边的大文件拷贝程序,NodeJS直接提供了.pipe形式来做这件事情,其内部贯彻格局与上方的代码类似。

File System(文件系统)

合法文档: http://nodejs.org/api/fs.html

NodeJS通过fs松开模块提供对文本的操作。fs模块提供的API基本上可以分为以下三类:

  • 文件属性读写。

    其间常用的有fs.statfs.chmodfs.chown等等。

  • 文件内容读写。

    个中常用的有fs.readFilefs.readdirfs.writeFilefs.mkdir等等。

  • 底层文件操作。

    里面常用的有fs.openfs.readfs.writefs.close等等。

NodeJS最精华的异步IO模型在fs模块里装有充裕的显示,例如下面提到的这一个API都经过回调函数传递结果。以fs.readFile为例:

fs.readFile(pathname, function (err, data) {
    if (err) {
        // Deal with error.
    } else {
        // Deal with data.
    }
});

如下边代码所示,基本上所有fs模块API的回调参数都有多少个。第一个参数在有不当爆发时非凡异常对象,第二个参数始终用来重返API方法执行结果。

此外,fs模块的持有异步API都有照应的联手版本,用于不能使用异步操作时,或者同步操作更利于时的事态。同步API除了艺术名的终极多了一个Sync之外,非凡对象与执行结果的传递模式也有对应变更。同样以fs.readFileSync为例:

try {
    var data = fs.readFileSync(pathname);
    // Deal with data.
} catch (err) {
    // Deal with error.
}

fs模块提供的API很多,这里不一一介绍,需要时请自行查阅官方文档。

Path(路径)

法定文档: http://nodejs.org/api/path.html

操作文件时不免不与公事路径打交道。NodeJS提供了path松开模块来简化路径相关操作,并升级代码可读性。以下分别介绍多少个常用的API。

  • path.normalize

    将盛传的途径转换为业内路径,具体讲的话,除精晓析路径中的...外,仍可以去掉多余的斜杠。假如有程序需要动用路径作为少数数据的目录,但又同意用户自由输入路径时,就需要使用该办法保证路径的唯一性。以下是一个例证:

      var cache = {};
    

      function store(key, value) {
          cache[path.normalize(key)] = value;
      }
    

      store('foo/bar', 1);
      store('foo//baz//../bar', 2);
      console.log(cache);  // => { "foo/bar": 2 }
    

    坑出没在意:
    标准化之后的路线里的斜杠在Windows系统下是\,而在Linux系统下是/。如若想保证其他系统下都采纳/用作路径分隔符的话,需要用.replace(/\\/g, '/')再交替一下正式路径。

  • path.join

    将盛传的三个路子拼接为业内路径。该方法可避免手工拼接路径字符串的累赘,并且能在不同类别下正确行使相应的门道分隔符。以下是一个事例:

      path.join('foo/', 'baz/', '../bar'); // => "foo/bar"
    
  • path.extname

    当我们需要按照不同文件扩张名做不同操作时,该办法就显得很好用。以下是一个例证:

      path.extname('foo/bar.js'); // => ".js"
    

path模块提供的其它方法也不多,稍微看一下官方文档就能全体控制。

遍历目录

遍历目录是操作文件时的一个大面积需求。比如写一个先后,需要找到并处理指定目录下的持有JS文件时,就需要遍历整个目录。

递归算法

遍历目录时一般拔取递归算法,否则就难以编写出简洁的代码。递归算法与数学归咎法类似,通过不断压缩问题的局面来化解问题。以下示例表达了这种办法。

function factorial(n) {
    if (n === 1) {
        return 1;
    } else {
        return n * factorial(n - 1);
    }
}

上面的函数用于总计N的阶乘(N!)。可以看看,当N大于1时,问题简化为统计N乘以N-1的阶乘。当N等于1时,问题达成最小规模,不需要再简化,由此一直重临1。

陷阱:
使用递归算法编写的代码即使简单,但鉴于每递归一遍就暴发两回函数调用,在需要事先考虑性能时,需要把递归算法转换为循环算法,以减小函数调用次数。

遍历算法

目录是一个树状结构,在遍历时一般拔取深度优先+先序遍历算法。深度优先,意味着到达一个节点后,首先随着遍历子节点而不是邻里节点。先序遍历,意味着第一次到达了某节点尽管遍历完成,而不是最后两次回到某节点才算数。由此拔取这种遍历格局时,下面这棵树的遍历顺序是A > B > D > E > C > F

          A
         / \
        B   C
       / \   \
      D   E   F

协办遍历

打听了必需的算法后,我们得以概括地贯彻以下目录遍历函数。

function travel(dir, callback) {
    fs.readdirSync(dir).forEach(function (file) {
        var pathname = path.join(dir, file);

if (fs.statSync(pathname).isDirectory()) {
travel(pathname, callback);
} else {
callback(pathname);
}
});
}

可以见见,该函数以某个目录作为遍历的起源。遭逢一个子目录时,就先跟着遍历子目录。碰到一个文书时,就把公文的相对路径传给回调函数。回调函数得到文件路径后,就可以做各个判断和处理。因而一旦有以下目录:

- /home/user/
    - foo/
        x.js
    - bar/
        y.js
    z.css

应用以下代码遍历该目录时,得到的输入如下。

travel('/home/user', function (pathname) {
    console.log(pathname);
});

/home/user/foo/x.js
/home/user/bar/y.js
/home/user/z.css

异步遍历

假如读取目录或读取文件状态时拔取的是异步API,目录遍历函数实现起来会稍微复杂,但原理完全相同。travel函数的异步版本如下。

function travel(dir, callback, finish) {
    fs.readdir(dir, function (err, files) {
        (function next(i) {
            if (i < files.length) {
                var pathname = path.join(dir, files[i]);

fs.stat(pathname, function (err, stats) {
if (stats.isDirectory()) {
travel(pathname, callback, function () {
next(i + 1);
});
} else {
callback(pathname, function () {
next(i + 1);
});
}
});
} else {
finish && finish();
}
}(0));
});
}

这里不详细介绍异步遍历函数的编制技巧,在延续章节中会详细介绍这些。不言而喻大家可以看到异步编程依旧蛮复杂的。

文本编码

行使NodeJS编写前端工具时,操作得最多的是文本文件,由此也就关系到了文件编码的拍卖问题。我们常用的文书编码有UTF8GBK两种,并且UTF8文件还可能包含BOM。在读取不同编码的文书文件时,需要将文件内容转换为JS使用的UTF8编码字符串后才能正常处理。

BOM的移除

BOM用于标记一个文书文件使用Unicode编码,其本身是一个Unicode字符(”\uFEFF”),位于文本文件头部。在不同的Unicode编码下,BOM字符对应的二进制字节如下:

    Bytes      Encoding
----------------------------
    FE FF       UTF16BE
    FF FE       UTF16LE
    EF BB BF    UTF8

由此,咱们得以遵照文件文件头多少个字节等于啥来判断文件是否包含BOM,以及利用哪个种类Unicode编码。但是,BOM字符即使起到了标记文件编码的效应,其本人却不属于文件内容的一部分,假诺读取文本文件时不去掉BOM,在好几使用状况下就会有问题。例如我们把多少个JS文件合并成一个文书后,假若文件中间含有BOM字符,就会招致浏览器JS语法错误。因而,使用NodeJS读取文本文件时,一般需要去掉BOM。例如,以下代码实现了识别和去除UTF8
BOM的效用。

function readText(pathname) {
    var bin = fs.readFileSync(pathname);

if (bin[0] === 0xEF && bin[1] === 0xBB && bin[2] === 0xBF) {
bin = bin.slice(3);
}

return bin.toString(‘utf-8’);
}

GBK转UTF8

NodeJS协理在读取文本文件时,或者在Buffer转移为字符串时指定文本编码,但遗憾的是,GBK编码不在NodeJS自身补助范围内。由此,一般大家依靠iconv-lite其一三方包来转换编码。使用NPM下载该包后,大家可以按下边情势编写一个读取GBK文本文件的函数。

var iconv = require('iconv-lite');

function readGBKText(pathname) {
var bin = fs.readFileSync(pathname);

return iconv.decode(bin, ‘gbk’);
}

单字节编码

偶然,大家不可能预知需要读取的公文拔取哪个种类编码,由此也就不可能指定正确的编码。比如大家要处理的一点CSS文件中,有的用GBK编码,有的用UTF8编码。尽管可以毫无疑问程度足以依据文件的字节内容估摸出文本编码,但这边要介绍的是有些局限,不过要简单得多的一种技术。

率先大家领略,即使一个文件文件只包含英文字符,比如Hello World,这无论是用GBK编码或是UTF8编码读取那么些文件都是没问题的。这是因为在那一个编码下,ASCII0~128范围内字符都利用相同的单字节编码。

反过来讲,即便一个文本文件中有中文等字符,如若我们需要处理的字符仅在ASCII0~128限量内,比如除了注释和字符串以外的JS代码,大家就足以统一行使单字节编码来读取文件,不用关心文件的实在编码是GBK仍旧UTF8。以下示例表明了这种办法。

1. GBK编码源文件内容:
    var foo = '中文';
2. 对应字节:
    76 61 72 20 66 6F 6F 20 3D 20 27 D6 D0 CE C4 27 3B
3. 使用单字节编码读取后得到的内容:
    var foo = '{乱码}{乱码}{乱码}{乱码}';
4. 替换内容:
    var bar = '{乱码}{乱码}{乱码}{乱码}';
5. 使用单字节编码保存后对应字节:
    76 61 72 20 62 61 72 20 3D 20 27 D6 D0 CE C4 27 3B
6. 使用GBK编码读取后得到内容:
    var bar = '中文';

此间的妙方在于,不管大于0xEF的单个字节在单字节编码下被分析成如何乱码字符,使用相同的单字节编码保留这么些乱码字符时,背后对应的字节保持不变。

NodeJS中自带了一种binary编码可以用来贯彻这些方法,因而在下例中,大家利用这种编码来演示上例对应的代码该怎么写。

function replace(pathname) {
    var str = fs.readFileSync(pathname, 'binary');
    str = str.replace('foo', 'bar');
    fs.writeFileSync(pathname, str, 'binary');
}

小结

本章介绍了拔取NodeJS操作文件时索要的API以及一些技巧,总括起来有以下几点:

  • 学好文件操作,编写各类程序都虽然。

  • 万一不是很在意性能,fs模块的同步API能让生活进一步美好。

  • 内需对文本读写做到字节级另外精美控制时,请使用fs模块的公文底层操作API。

  • 决不选拔拼接字符串的模式来拍卖途径,使用path模块。

  • 支配好目录遍历和文件编码处理技术,很实用。

网络操作

不打听网络编程的程序员不是好前端,而NodeJS恰好提供了一扇掌握网络编程的窗口。通过NodeJS,除了可以编制一些服务端程序来协助前端开发和测试外,还是能够学习一些HTTP协议与Socket协议的连带知识,这多少个知识在优化前端性能和排查前端故障时或许能派上用场。本章将介绍与之有关的NodeJS内置模块。

开门红

NodeJS本来的用处是编制高性能Web服务器。大家先是在那边再一次一下法定文档里的例子,使用NodeJS内置的http模块简单实现一个HTTP服务器。

var http = require('http');

http.createServer(function (request, response) {
response.writeHead(200, { ‘Content-Type’: ‘text-plain’ });
response.end(‘Hello World\n’);
}).listen(8124);

以上程序创造了一个HTTP服务器并监听8124端口,打开浏览器访问该端口http://127.0.0.1:8124/就可知看出功效。

豆知识:
在Linux系统下,监听1024以下端口需要root权限。因而,假设想监听80或443端口的话,需要使用sudo一声令下启动程序。

API走马观花

咱俩先大致看看NodeJS提供了怎么着和网络操作有关的API。这里并不逐一介绍每个API的利用形式,官方文档已经做得很好了。

HTTP

法定文档: http://nodejs.org/api/http.html

‘http’模块提供二种拔取方法:

  • 用作服务端使用时,创设一个HTTP服务器,监听HTTP客户端请求并赶回响应。

  • 用作客户端采取时,发起一个HTTP客户端请求,获取服务端响应。

首先我们来看望服务端形式下怎样工作。如开门红中的例子所示,首先需要利用.createServer主意创制一个服务器,然后调用.listen方法监听端口。之后,每当来了一个客户端请求,成立服务器时传入的回调函数就被调用一遍。可以看看,这是一种事件机制。

HTTP请求精神上是一个数据流,由请求头(headers)和请求体(body)组成。例如以下是一个完好无缺的HTTP请求数据内容。

POST / HTTP/1.1
User-Agent: curl/7.26.0
Host: localhost
Accept: */*
Content-Length: 11
Content-Type: application/x-www-form-urlencoded

Hello World

可以见到,空行之上是请求头,之下是请求体。HTTP请求在发送给服务器时,可以认为是比照从头到尾的各类一个字节一个字节地以数量流情势发送的。而http模块创造的HTTP服务器在接受到一体化的请求头后,就会调用回调函数。在回调函数中,除了可以行使request目标访问请求头数据外,仍是可以把request目的当作一个只读数据流来访问请求体数据。以下是一个事例。

http.createServer(function (request, response) {
    var body = [];

console.log(request.method);
console.log(request.headers);

request.on(‘data’, function (chunk) {
body.push(chunk);
});

request.on(‘end’, function () {
body = Buffer.concat(body);
console.log(body.toString());
});
}).listen(80);

POST
{ 'user-agent': 'curl/7.26.0',
  host: 'localhost',
  accept: '*/*',
  'content-length': '11',
  'content-type': 'application/x-www-form-urlencoded' }
Hello World

HTTP响应本质上也是一个数据流,同样由响应头(headers)和响应体(body)组成。例如以下是一个完完全全的HTTP请求数据内容。

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 11
Date: Tue, 05 Nov 2013 05:31:38 GMT
Connection: keep-alive

Hello World

在回调函数中,除了能够拔取response对象来写入响应头数据外,还可以把response目的当作一个只写多少流来写入响应体数据。例如在以下例子中,服务端原样将客户端请求的请求体数据再次来到给客户端。

http.createServer(function (request, response) {
    response.writeHead(200, { 'Content-Type': 'text/plain' });

request.on(‘data’, function (chunk) {
response.write(chunk);
});

request.on(‘end’, function () {
response.end();
});
}).listen(80);

接下去我们看看客户端模式下如何做事。为了倡导一个客户端HTTP请求,大家需要指定目的服务器的职位并发送请求头和请求体,以下示例演示了具体做法。

var options = {
        hostname: 'www.example.com',
        port: 80,
        path: '/upload',
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
        }
    };

var request = http.request(options, function (response) {});

request.write(‘Hello World’);
request.end();

可以见见,.request方法创制了一个客户端,并点名请求目的和请求头数据。之后,就足以把request对象当作一个只写多少流来写入请求体数据和得了请求。此外,由于HTTP请求中GET呼吁是最普遍的一种,并且不需要请求体,因而http模块也提供了以下便捷API。

http.get('http://www.example.com/', function (response) {});

当客户端发送请求并采取到完全的服务端响应头时,就会调用回调函数。在回调函数中,除了可以动用response目的访问响应头数据外,仍能把response目的当作一个只读数据流来访问响应体数据。以下是一个例子。

http.get('http://www.example.com/', function (response) {
    var body = [];

console.log(response.statusCode);
console.log(response.headers);

response.on(‘data’, function (chunk) {
body.push(chunk);
});

response.on(‘end’, function () {
body = Buffer.concat(body);
console.log(body.toString());
});
});

200
{ 'content-type': 'text/html',
  server: 'Apache',
  'content-length': '801',
  date: 'Tue, 05 Nov 2013 06:08:41 GMT',
  connection: 'keep-alive' }
<!DOCTYPE html>
...

HTTPS

合法文档: http://nodejs.org/api/https.html

https模块与http模块极为类似,区别在于https模块需要出色处理SSL证书。

在服务端格局下,创设一个HTTPS服务器的以身作则如下。

var options = {
        key: fs.readFileSync('./ssl/default.key'),
        cert: fs.readFileSync('./ssl/default.cer')
    };

var server = https.createServer(options, function (request, response) {
// …
});

可以见见,与创立HTTP服务器相相比较,多了一个options对象,通过keycert字段指定了HTTPS服务器使用的私钥和公钥。

除此以外,NodeJS辅助SNI技术,可以按照HTTPS客户端请求使用的域名动态使用不同的讲明,因而同一个HTTPS服务器能够应用七个域名提供劳动。接着上例,可以利用以下情势为HTTPS服务器添加多组证书。

server.addContext('foo.com', {
    key: fs.readFileSync('./ssl/foo.com.key'),
    cert: fs.readFileSync('./ssl/foo.com.cer')
});

server.addContext(‘bar.com’, {
key: fs.readFileSync(‘./ssl/bar.com.key’),
cert: fs.readFileSync(‘./ssl/bar.com.cer’)
});

在客户端形式下,发起一个HTTPS客户端请求与http模块几乎相同,示例如下。

var options = {
        hostname: 'www.example.com',
        port: 443,
        path: '/',
        method: 'GET'
    };

var request = https.request(options, function (response) {});

request.end();

但假若目的服务器使用的SSL证书是自制的,不是从颁发机构采购的,默认情状下https模块会拒绝连接,指示说有证书安全问题。在options里加入rejectUnauthorized: false字段可以禁用对证件有效性的自我批评,从而允许https模块请求支付环境下行使自制证书的HTTPS服务器。

URL

法定文档: http://nodejs.org/api/url.html

处理HTTP请求时url模块使用率超高,因为该模块允许解析URL、生成URL,以及拼接URL。首先大家来探望一个完好的URL的各组成部分。

                           href
 -----------------------------------------------------------------
                            host              path
                      --------------- ----------------------------
 http: // user:pass @ host.com : 8080 /p/a/t/h ?query=string #hash
 -----    ---------   --------   ---- -------- ------------- -----
protocol     auth     hostname   port pathname     search     hash
                                                ------------
                                                   query

俺们可以动用.parse方法来将一个URL字符串转换为URL对象,示例如下。

url.parse('http://user:pass@host.com:8080/p/a/t/h?query=string#hash');
/* =>
{ protocol: 'http:',
  auth: 'user:pass',
  host: 'host.com:8080',
  port: '8080',
  hostname: 'host.com',
  hash: '#hash',
  search: '?query=string',
  query: 'query=string',
  pathname: '/p/a/t/h',
  path: '/p/a/t/h?query=string',
  href: 'http://user:pass@host.com:8080/p/a/t/h?query=string#hash' }
*/

传给.parse措施的不必然如若一个完整的URL,例如在HTTP服务器回调函数中,request.url不分包协议头和域名,但一样可以用.parse措施分析。

http.createServer(function (request, response) {
    var tmp = request.url; // => "/foo/bar?a=b"
    url.parse(tmp);
    /* =>
    { protocol: null,
      slashes: null,
      auth: null,
      host: null,
      port: null,
      hostname: null,
      hash: null,
      search: '?a=b',
      query: 'a=b',
      pathname: '/foo/bar',
      path: '/foo/bar?a=b',
      href: '/foo/bar?a=b' }
    */
}).listen(80);

.parse形式还帮忙第二个和第六个布尔类型可选参数。第二个参数等于true时,该格局再次回到的URL对象中,query字段不再是一个字符串,而是一个透过querystring模块转换后的参数对象。第多少个参数等于true时,该形式能够正确分析不带协议头的URL,例如//www.example.com/foo/bar

反过来,format主意允许将一个URL对象转换为URL字符串,示例如下。

url.format({
    protocol: 'http:',
    host: 'www.example.com',
    pathname: '/p/a/t/h',
    search: 'query=string'
});
/* =>
'http://www.example.com/p/a/t/h?query=string'
*/

另外,.resolve形式可以用来拼接URL,示例如下。

url.resolve('http://www.example.com/foo/bar', '../baz');
/* =>
http://www.example.com/baz
*/

Query String

合法文档: http://nodejs.org/api/querystring.html

querystring模块用于落实URL参数字符串与参数对象的交互转换,示例如下。

querystring.parse('foo=bar&baz=qux&baz=quux&corge');
/* =>
{ foo: 'bar', baz: ['qux', 'quux'], corge: '' }
*/

querystring.stringify({ foo: ‘bar’, baz: [‘qux’, ‘quux’], corge: ” });
/ =>
‘foo=bar&baz=qux&baz=quux&corge=’
/

Zlib

法定文档: http://nodejs.org/api/zlib.html

zlib模块提供了数据压缩和解压的意义。当我们处理HTTP请求和响应时,可能需要用到那个模块。

率先我们看一个利用zlib模块压缩HTTP响应体数据的例证。这么些事例中,判断了客户端是不是补助gzip,并在补助的情景下行使zlib模块重临gzip之后的响应体数据。

http.createServer(function (request, response) {
    var i = 1024,
        data = '';

while (i–) {
data += ‘.’;
}

if ((request.headers[‘accept-encoding’] || ”).indexOf(‘gzip’) !== -1) {
zlib.gzip(data, function (err, data) {
response.writeHead(200, {
‘Content-Type’: ‘text/plain’,
‘Content-Encoding’: ‘gzip’
});
response.end(data);
});
} else {
response.writeHead(200, {
‘Content-Type’: ‘text/plain’
});
response.end(data);
}
}).listen(80);

进而我们看一个采取zlib模块解压HTTP响应体数据的事例。这多少个例子中,判断了服务端响应是否使用gzip压缩,并在减小的境况下行使zlib模块解压响应体数据。

var options = {
        hostname: 'www.example.com',
        port: 80,
        path: '/',
        method: 'GET',
        headers: {
            'Accept-Encoding': 'gzip, deflate'
        }
    };

http.request(options, function (response) {
var body = [];

response.on(‘data’, function (chunk) {
body.push(chunk);
});

response.on(‘end’, function () {
body = Buffer.concat(body);

if (response.headers[‘content-encoding’] === ‘gzip’) {
zlib.gunzip(body, function (err, data) {
console.log(data.toString());
});
} else {
console.log(data.toString());
}
});
}).end();

Net

法定文档: http://nodejs.org/api/net.html

net模块可用来创建Socket服务器或Socket客户端。由于Socket在前端领域的施用限制还不是很广,这里先不关乎到WebSocket的牵线,仅仅简单演示一下咋样从Socket层面来贯彻HTTP请求和响应。

第一我们来看一个使用Socket搭建一个很不谨言慎行的HTTP服务器的例子。这么些HTTP服务器不管收到什么请求,都稳定再次回到相同的响应。

net.createServer(function (conn) {
    conn.on('data', function (data) {
        conn.write([
            'HTTP/1.1 200 OK',
            'Content-Type: text/plain',
            'Content-Length: 11',
            '',
            'Hello World'
        ].join('\n'));
    });
}).listen(80);

随之大家来看一个用到Socket发起HTTP客户端请求的事例。这一个例子中,Socket客户端在创立连接后发送了一个HTTP
GET请求,并透过data事件监听函数来拿到服务器响应。

var options = {
        port: 80,
        host: 'www.example.com'
    };

var client = net.connect(options, function () {
client.write([
‘GET / HTTP/1.1’,
‘User-Agent: curl/7.26.0’,
‘Host: www.baidu.com’,
‘Accept: /‘,
”,

].join(‘\n’));
});

client.on(‘data’, function (data) {
console.log(data.toString());
client.end();
});

脑子一点

运用NodeJS操作网络,特别是操作HTTP请求和响应时会遭逢有些惊喜,这里对部分广泛问题做解答。

  • 问:
    为何通过headers目的访问到的HTTP请求头或响应头字段不是驼峰的?

    答:
    从正式上讲,HTTP请求头和响应头字段都应有是驼峰的。但实际是残忍的,不是各种HTTP服务端或客户端程序都严峻依照规范,所以NodeJS在处理从此外客户端或服务端收到的头字段时,都合并地变换为了小写字母格式,以便开发者能利用统一的点子来访问头字段,例如headers['content-length']

  • 问:
    为什么http模块成立的HTTP服务器重返的响应是chunked传输模式的?

    答:
    因为默认情状下,使用.writeHead艺术写入响应头后,允许选择.write方法写入随便长度的响应体数据,并应用.end措施截止一个响应。由于响应体数据长度不确定,因此NodeJS自动在响应头里添加了Transfer-Encoding: chunked字段,并采用chunked传输情势。可是当响应体数据长度确定时,可采纳.writeHead措施在响应头里加上Content-Length字段,这样做之后NodeJS就不会自动抬高Transfer-Encoding字段和行使chunked传输模式。

  • 问:
    为啥使用http模块发起HTTP客户端请求时,有时候会生出socket hang up错误?

    答:
    发起客户端HTTP请求前需要先创制一个客户端。http模块提供了一个大局客户端http.globalAgent,可以让我们采用.request.get办法时毫不手动创造客户端。但是全局客户端默认只允许5个并发Socket连接,当某一个每一天HTTP客户端请求创制过多,超越这些数字时,就会暴发socket hang up不当。解决情势也很简短,通过http.globalAgent.maxSockets特性把这多少个数字改大些即可。此外,https模块遭受这一个问题时也如出一辙通过https.globalAgent.maxSockets属性来处理。

小结

本章介绍了利用NodeJS操作网络时需要的API以及部分坑回避技巧,总计起来有以下几点:

  • httphttps模块援助服务端情势和客户端形式二种接纳方法。

  • requestresponse对象除了用于读写头数据外,都足以视作数据流来操作。

  • url.parse办法加上request.url性能是拍卖HTTP请求时的定点搭配。

  • 使用zlib模块可以减掉使用HTTP协议时的数码传输量。

  • 通过net模块的Socket服务器与客户端可对HTTP协议做底层操作。

  • 小心踩坑。

进程管理

NodeJS可以感知和操纵自己进程的周转条件和情景,也得以创建子进程并与其协同工作,这使得NodeJS可以把两个程序组合在协同共同完成某项工作,并在中间担任胶水和调度器的功用。本章除了介绍与之有关的NodeJS内置模块外,还会重要介绍典型的运用情形。

开门红

咱俩早就知晓了NodeJS自带的fs模块相比较基础,把一个目录里的具有文件和子目录都拷贝到另一个索引里需要写过多代码。另外大家也精晓,终端下的cp指令相比较好用,一条cp -r source/* target命令就能搞定目录拷贝。这我们第一看望咋样采用NodeJS调用极端命令来简化目录拷贝,示例代码如下:

var child_process = require('child_process');
var util = require('util');

function copy(source, target, callback) {
child_process.exec(
util.format(‘cp -r %s/* %s’, source, target), callback);
}

copy(‘a’, ‘b’, function (err) {
// …
});

从以上代码中可以观望,子进程是异步运行的,通过回调函数重临执行结果。

API走马观花

咱俩先大致看看NodeJS提供了什么样和经过管理有关的API。这里并不逐一介绍每个API的行使办法,官方文档已经做得很好了。

Process

合法文档: http://nodejs.org/api/process.html

任何一个历程都有启动进程时使用的命令行参数,有正规输入标准输出,有运行权限,有运行条件和运作情状。在NodeJS中,可以透过process对象感知和操纵NodeJS自身进程的全部。此外索要留意的是,process不是放网店模特块,而是一个大局对象,因而在其他地点都得以一向利用。

Child Process

合法文档: http://nodejs.org/api/child_process.html

使用child_process模块可以创制和控制子进程。该模块提供的API中最基本的是.spawn,此外API都是本着特定使用处境对它的更为封装,算是一种语法糖。

Cluster

法定文档: http://nodejs.org/api/cluster.html

cluster模块是对child_process模块的尤其封装,专用于解决单进程NodeJS
Web服务器不可以充裕利用多核CPU的题目。使用该模块可以简化多进程服务器程序的开支,让每个核上运行一个做事经过,并统一通过主进程监听端口和散发请求。

使用场景

和进程管理有关的API单独介绍起来比较干燥,由此这里从部分天下无双的运用场景出发,分别介绍部分重点API的行使情势。

如何获取命令行参数

在NodeJS中可以透过process.argv得到命令行参数。但是比较奇怪的是,node举办顺序路径和主模块文件路径固定占据了argv[0]argv[1]六个职位,而首先个命令行参数从argv[2]开始。为了让argv利用起来更为自然,可以依照以下情势处理。

function main(argv) {
    // ...
}

main(process.argv.slice(2));

何以退出程序

平凡一个主次做完所有工作后就层出不穷退出了,这时程序的退出状态码为0。或者一个程序运行时发出了充足后就挂了,这时程序的退出状态码不等于0。假若我们在代码中捕获了某个万分,可是觉得程序不应有继承运行下去,需要即刻退出,并且需要把退出状态码设置为指定数字,比如1,就足以遵照以下措施:

try {
    // ...
} catch (err) {
    // ...
    process.exit(1);
}

如何支配输入输出

NodeJS程序的正儿八经输入流(stdin)、一个正经输出流(stdout)、一个正经错误流(stderr)分别对应process.stdinprocess.stdoutprocess.stderr,第一个是只读数据流,后面多少个是只写数据流,对它们的操作遵照对数据流的操作办法即可。例如,console.log可以依据以下方法贯彻。

function log() {
    process.stdout.write(
        util.format.apply(util, arguments) + '\n');
}

哪些降权

在Linux系统下,我们领略需要使用root权限才能监听1024以下端口。可是假使形成端口监听后,继续让程序运行在root权限下存在安全隐患,因而最好能把权力降下来。以下是这般一个例证。

http.createServer(callback).listen(80, function () {
    var env = process.env,
        uid = parseInt(env['SUDO_UID'] || process.getuid(), 10),
        gid = parseInt(env['SUDO_GID'] || process.getgid(), 10);

process.setgid(gid);
process.setuid(uid);
});

上例中有几点需要留意:

  1. 要是是因此sudo获取root权限的,运行程序的用户的UID和GID保存在环境变量SUDO_UIDSUDO_GID内部。倘诺是因而chmod +s主意取得root权限的,运行程序的用户的UID和GID可径直通过process.getuidprocess.getgid格局赢得。

  2. process.setuidprocess.setgid方法只接受number品类的参数。

  3. 降权时务必先降GID再降UID,否则顺序反过来的话就没权力更改程序的GID了。

什么创造子进程

以下是一个创建NodeJS子进程的事例。

var child = child_process.spawn('node', [ 'xxx.js' ]);

child.stdout.on(‘data’, function (data) {
console.log(‘stdout: ‘ + data);
});

child.stderr.on(‘data’, function (data) {
console.log(‘stderr: ‘ + data);
});

child.on(‘close’, function (code) {
console.log(‘child process exited with code ‘ + code);
});

上例中动用了.spawn(exec, args, options)主意,该措施补助两个参数。第一个参数是实施文书路径,可以是实施文书的争持或相对路径,也足以是基于PATH环境变量能找到的进行文书名。第二个参数中,数组中的每个成员都按梯次对应一个命令行参数。第多少个参数可选,用于配置子进程的执行环境与作为。

另外,上例中即使通过子进程对象的.stdout.stderr访问子进程的输出,但因而options.stdio字段的不比安排,可以将子进程的输入输出重定向到此外数据流上,或者让子进程共享父进程的专业输入输出流,或者直接忽略子进程的输入输出。

经过间如何报道

在Linux系统下,进程之间可以经过信号相互通信。以下是一个例证。

/* parent.js */
var child = child_process.spawn('node', [ 'child.js' ]);

child.kill(‘SIGTERM’);

/ child.js /
process.on(‘SIGTERM’, function () {
cleanUp();
process.exit(0);
});

在上例中,父进程经过.kill方法向子进程发送SIGTERM信号,子进程监听process对象的SIGTERM事件响应信号。不要被.kill办法的名目迷惑了,该办法本质上是用来给进程发送信号的,进程收到信号后实际要做吗,完全在于信号的门类和进程本身的代码。

其它,假设父子进程都是NodeJS进程,就可以经过IPC(进程间通讯)双向传递数据。以下是一个例子。

/* parent.js */
var child = child_process.spawn('node', [ 'child.js' ], {
        stdio: [ 0, 1, 2, 'ipc' ]
    });

child.on(‘message’, function (msg) {
console.log(msg);
});

child.send({ hello: ‘hello’ });

/ child.js /
process.on(‘message’, function (msg) {
msg.hello = msg.hello.toUpperCase();
process.send(msg);
});

可以观察,父进程在成立子进程时,在options.stdio字段中通过ipc敞开了一条IPC通道,之后就可以监听子进程对象的message事件接受来自子进程的音讯,并因此.send主意给子进程发送音信。在子进程这边,可以在process目的上监听message事件接受来自父进程的音信,并经过.send措施向父进程发送消息。数据在传递过程中,会先在殡葬端拔取JSON.stringify方法系列化,再在接收端使用JSON.parse办法反体系化。

怎样守护子进程

医护进程一般用来监控工作过程的运转意况,在办事进程不健康退出时重启工作进程,保障工作经过不间断运行。以下是一种实现情势。

/* daemon.js */
function spawn(mainModule) {
    var worker = child_process.spawn('node', [ mainModule ]);

worker.on(‘exit’, function (code) {
if (code !== 0) {
spawn(mainModule);
}
});
}

spawn(‘worker.js’);

可以见到,工作经过非正常退出时,守护进程立时重启工作历程。

小结

本章介绍了动用NodeJS管理过程时需要的API以及重大的利用场景,总括起来有以下几点:

  • 使用process对象管理我。

  • 使用child_process模块创设和管理子进程。

异步编程

NodeJS最大的卖点——事件机制和异步IO,对开发者并不是透明的。开发者需要按异步模式编写代码才用得上这些卖点,而这或多或少也饱受了部分NodeJS反对者的攻击。但好歹,异步编程确实是NodeJS最大的特征,没有了然异步编程就无法说是真正学会了NodeJS。本章将介绍与异步编程相关的各个知识。

回调

在代码中,异步编程的从来反映就是回调。异步编程依托于回调来兑现,但不可能说采用了回调后先后就异步化了。我们首先可以看看以下代码。

function heavyCompute(n, callback) {
    var count = 0,
        i, j;

for (i = n; i > 0; –i) {
for (j = n; j > 0; –j) {
count += 1;
}
}

callback(count);
}

heavyCompute(10000, function (count) {
console.log(count);
});

console.log(‘hello’);

— Console ——————————
100000000
hello

可以看出,以上代码中的回调函数依旧先于后续代码执行。JS本身是单线程运行的,无法在一段代码还未终止运行时去运转其它代码,由此也就不设有异步执行的概念。

但是,假设某个函数做的政工是成立一个另外线程或进程,并与JS主线程并行地做一些工作,并在业务做完后通报JS主线程,这景观又不相同了。大家随后看看以下代码。

setTimeout(function () {
    console.log('world');
}, 1000);

console.log(‘hello’);

— Console ——————————
hello
world

本次可以观察,回调函数后于继续代码执行了。如同下面所说,JS本身是单线程的,无法异步执行,由此我们得以认为setTimeout这类JS规范之外的由运行环境提供的相当函数做的业务是开创一个平行线程后旋即赶回,让JS主进程可以跟着执行后续代码,并在接到平行进程的通报后再实践回调函数。除了setTimeoutsetInterval这个科普的,这类函数还包括NodeJS提供的诸如fs.readFile等等的异步API。

其余,大家照例回到JS是单线程运行的这多少个实在,这决定了JS在举办完一段代码从前无法执行包括回调函数在内的其余代码。也就是说,即便平行线程完成工作了,通知JS主线程执行回调函数了,回调函数也要等到JS主线程空闲时才能起头实践。以下就是如此一个例证。

function heavyCompute(n) {
    var count = 0,
        i, j;

for (i = n; i > 0; –i) {
for (j = n; j > 0; –j) {
count += 1;
}
}
}

var t = new Date();

setTimeout(function () {
console.log(new Date() – t);
}, 1000);

heavyCompute(50000);

— Console ——————————
8520

可以看到,本来应该在1秒后被调用的回调函数因为JS主线程忙于运行此外代码,实际执行时间被大幅延迟。

代码设计情势

异步编程有好多蓄意的代码设计情势,为了贯彻平等的效用,使用同步情势和异步形式编写的代码会有很大距离。以下分别介绍部分广泛的格局。

函数重返值

使用一个函数的出口作为另一个函数的输入是很广泛的要求,在联合情势下一般按以下形式编写代码:

var output = fn1(fn2('input'));
// Do something.

而在异步情势下,由于函数执行结果不是经过重临值,而是通过回调函数传递,由此一般按以下情势编写代码:

fn2('input', function (output2) {
    fn1(output2, function (output1) {
        // Do something.
    });
});

可以看到,这种艺术就是一个回调函数套一个回调函多,套得太多了很容易写出>形态的代码。

遍历数组

在遍历数组时,使用某个函数依次对数据成员做一些处理也是大规模的要求。如若函数是一起执行的,一般就会写出以下代码:

var len = arr.length,
    i = 0;

for (; i < len; ++i) { arr[i] = sync(arr[i]); }
// All array items have processed.

假诺函数是异步执行的,以上代码就无法担保循环截止后具备数组成员都处理完毕了。假诺数组成员必须一个接一个串行处理,则一般遵从以下情势编写异步代码:

(function next(i, len, callback) {
    if (i < len) {
        async(arr[i], function (value) {
            arr[i] = value;
            next(i + 1, len, callback);
        });
    } else {
        callback();
    }
}(0, arr.length, function () {
    // All array items have processed.
}));

可以看到,以上代码在异步函数执行五遍并赶回执行结果后才传入下一个数组成员并起始下一轮执行,直到所有数组成员处理完毕后,通过回调的主意触发后续代码的推行。

倘诺数组成员能够并行处理,但持续代码如故需要拥有数组成员处理完毕后才能举办的话,则异步代码会调整成以下形式:

(function (i, len, count, callback) {
    for (; i < len; ++i) {
        (function (i) {
            async(arr[i], function (value) {
                arr[i] = value;
                if (++count === len) {
                    callback();
                }
            });
        }(i));
    }
}(0, arr.length, 0, function () {
    // All array items have processed.
}));

可以看出,与异步串行遍历的本子对照,以上代码并行处理所有数组成员,并透过计数器变量来判断几时所有数组成员都处理完毕了。

老大处理

JS自身提供的老大捕获和拍卖体制——try..catch..,只可以用于共同执行的代码。以下是一个例子。

function sync(fn) {
    return fn();
}

try {
sync(null);
// Do something.
} catch (err) {
console.log(‘Error: %s’, err.message);
}

— Console ——————————
Error: object is not a function

可以见见,万分会沿着代码执行路径一贯冒泡,直到曰镪第一个try说话时被抓获住。但鉴于异步函数会打断代码执行路径,异步函数执行过程中以及执行之后发生的不胜冒泡到执行路径被打断的岗位时,假诺直白尚未境遇try讲话,就当作一个大局分外抛出。以下是一个例证。

function async(fn, callback) {
    // Code execution path breaks here.
    setTimeout(function () {
        callback(fn());
    }, 0);
}

try {
async(null, function (data) {
// Do something.
});
} catch (err) {
console.log(‘Error: %s’, err.message);
}

— Console ——————————
/home/user/test.js:4
callback(fn());
^
TypeError: object is not a function
at null._onTimeout (/home/user/test.js:4:13)
at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)

因为代码执行路径被打断了,我们就需要在那多少个冒泡到断点往日用try话语把非常捕获住,并通过回调函数传递被破获的卓殊。于是咱们得以像下边这样改造上面的例子。

function async(fn, callback) {
    // Code execution path breaks here.
    setTimeout(function () {
        try {
            callback(null, fn());
        } catch (err) {
            callback(err);
        }
    }, 0);
}

async(null, function (err, data) {
if (err) {
console.log(‘Error: %s’, err.message);
} else {
// Do something.
}
});

— Console ——————————
Error: object is not a function

可以观看,十分再一次被捕获住了。在NodeJS中,几乎所有异步API都遵循上述措施设计,回调函数中率先个参数都是err。因而我们在编排自己的异步函数时,也足以坚守这种措施来拍卖万分,与NodeJS的筹划风格保持一致。

有了老大处理模式后,我们随后可以想一想一般我们是怎么写代码的。基本上,大家的代码都是做一些事情,然后调用一个函数,然后再做一些工作,然后再调用一个函数,如此循环往复。假设我们写的是一块代码,只需要在代码入口点写一个try说话就能捕获所有冒泡上来的特别,示例如下。

function main() {
    // Do something.
    syncA();
    // Do something.
    syncB();
    // Do something.
    syncC();
}

try {
main();
} catch (err) {
// Deal with exception.
}

而是,假如我们写的是异步代码,就只有呵呵了。由于每一回异步函数调用都会堵塞代码执行路径,只好通过回调函数来传递非凡,于是我们就需要在每个回调函数里判断是否有特别暴发,于是只用五回异步函数调用,就会时有发生下面这种代码。

function main(callback) {
    // Do something.
    asyncA(function (err, data) {
        if (err) {
            callback(err);
        } else {
            // Do something
            asyncB(function (err, data) {
                if (err) {
                    callback(err);
                } else {
                    // Do something
                    asyncC(function (err, data) {
                        if (err) {
                            callback(err);
                        } else {
                            // Do something
                            callback(null);
                        }
                    });
                }
            });
        }
    });
}

main(function (err) {
if (err) {
// Deal with exception.
}
});

能够看到,回调函数已经让代码变得复杂了,而异步模式下对特其余处理更加深了代码的复杂度。假设NodeJS的最大卖点最终成为那个样子,这就没人愿意用NodeJS了,因而接下去会介绍NodeJS提供的片段化解方案。

域(Domain)

合法文档: http://nodejs.org/api/domain.html

NodeJS提供了domain模块,可以简化异步代码的分外处理。在介绍该模块从前,我们需要首先知道“域”的概念。简单来讲,一个域就是一个JS运行环境,在一个运作条件中,如果一个异常没有被抓获,将作为一个大局异常被抛出。NodeJS通过process目的提供了捕获全局非常的点子,示例代码如下

process.on('uncaughtException', function (err) {
    console.log('Error: %s', err.message);
});

setTimeout(function (fn) {
fn();
});

— Console ——————————
Error: undefined is not a function

固然全局分外有个地点可以捕获了,可是对于多数分外,我们希望赶紧捕获,并依据结果决定代码的履行路径。大家用来下HTTP服务器代码作为例子:

function async(request, callback) {
    // Do something.
    asyncA(request, function (err, data) {
        if (err) {
            callback(err);
        } else {
            // Do something
            asyncB(request, function (err, data) {
                if (err) {
                    callback(err);
                } else {
                    // Do something
                    asyncC(request, function (err, data) {
                        if (err) {
                            callback(err);
                        } else {
                            // Do something
                            callback(null, data);
                        }
                    });
                }
            });
        }
    });
}

http.createServer(function (request, response) {
async(request, function (err, data) {
if (err) {
response.writeHead(500);
response.end();
} else {
response.writeHead(200);
response.end(data);
}
});
});

以上代码将呼吁对象交给异步函数处理后,再遵照处理结果重返响应。这里运用了应用回调函数传递相当的方案,由此async函数内部假诺再多几个异步函数调用的话,代码就变成下边这副鬼样子了。为了让代码美观点,我们可以在每处理一个请求时,使用domain模块成立一个子域(JS子运行环境)。在子域内运行的代码可以肆意抛出卓殊,而这多少个特别能够通过子域对象的error事件联合捕获。于是以上代码能够做如下改造:

function async(request, callback) {
    // Do something.
    asyncA(request, function (data) {
        // Do something
        asyncB(request, function (data) {
            // Do something
            asyncC(request, function (data) {
                // Do something
                callback(data);
            });
        });
    });
}

http.createServer(function (request, response) {
var d = domain.create();

d.on(‘error’, function () {
response.writeHead(500);
response.end();
});

d.run(function () {
async(request, function (data) {
response.writeHead(200);
response.end(data);
});
});
});

能够看来,大家应用.create主意创立了一个子域对象,并由此.run格局进入需要在子域中运行的代码的入口点。而位于子域中的异步函数回调函数由于不再需要捕获非凡,代码一下子瘦身很多。

陷阱

甭管通过process对象的uncaughtException事件捕获到全局分外,如故通过子域对象的error事件捕获到了子域异常,在NodeJS官方文档里都强烈提议处理完非凡后随即重启程序,而不是让程序继续运行。遵照法定文档的说教,暴发特别后的次第处于一个不确定的运转状态,假如不及时退出的话,程序可能会发生严重内存泄漏,也恐怕显现得很想拿到。

但这边需要澄清一些实际。JS本身的throw..try..catch卓殊处理体制并不会造成内存泄漏,也不会让程序的举行结果不料,但NodeJS并不是存粹的JS。NodeJS里大量的API内部是用C/C++实现的,因而NodeJS程序的运作过程中,代码执行路径穿梭于JS引擎内部和外部,而JS的异常抛出机制可能会阻塞正常的代码执行流程,导致C/C++部分的代码表现特别,进而导致内存泄漏等题材。

因此,使用uncaughtExceptiondomain破获非常,代码执行路径里关系到了C/C++部分的代码时,假若不可以确定是不是会导致内存泄漏等题材,最好在处理完相当后重启程序相比妥善。而采取try语句捕获相当时相似捕获到的都是JS本身的十分,不用操心上诉问题。

小结

本章介绍了JS异步编程相关的知识,总计起来有以下几点:

  • 不控制异步编程就不算学会NodeJS。

  • 异步编程依托于回调来兑现,而利用回调不必然就是异步编程。

  • 异步编程下的函数间数据传递、数组遍历和相当处理与协同编程有很大差距。

  • 使用domain模块简化异步代码的异常处理,并小心骗局。

大示例

上学讲究的是学以致用和贯通。至此我们早就各自介绍了NodeJS的很多知识点,本章作为最终一章,将总体地介绍一个施用NodeJS开发Web服务器的言传身教。

需求

咱们要开发的是一个大概的静态文件合并服务器,该服务器需要扶助类似以下格式的JS或CSS文件合并请求。

http://assets.example.com/foo/??bar.js,baz.js

在以上URL中,??是一个分隔符,在此以前是亟需统一的多少个文件的URL的集体部分,之后是行使,相隔的距离部分。因而服务器处理那么些URL时,重返的是以下五个公文按顺序合并后的内容。

/foo/bar.js
/foo/baz.js

除此以外,服务器也急需能支撑类似以下格式的通常的JS或CSS文件请求。

http://assets.example.com/foo/bar.js

上述就是全部需求。

第一次迭代

快快迭代是一种科学的开发格局,因而我们在首先次迭代时先实现服务器的基本效能。

设计

简单分析了需要之后,我们大约会获取以下的设计方案。

           +---------+   +-----------+   +----------+
request -->|  parse  |-->|  combine  |-->|  output  |--> response
           +---------+   +-----------+   +----------+

也就是说,服务器会首先分析URL,得到请求的文书的路径和连串(MIME)。然后,服务器会读取请求的公文,并按梯次合并文件内容。最后,服务器重回响应,完成对两回呼吁的处理。

其余,服务器在读取文件时需要有个根目录,并且服务器监听的HTTP端口最好也不要写死在代码里,由此服务器需假使可安排的。

实现

遵照上述设计,我们写出了第一版代码如下。

var fs = require('fs'),
    path = require('path'),
    http = require('http');

var MIME = {
‘.css’: ‘text/css’,
‘.js’: ‘application/javascript’
};

function combineFiles(pathnames, callback) {
var output = [];

(function next(i, len) {
if (i < len) { fs.readFile(pathnames[i], function (err, data) { if (err) { callback(err); } else { output.push(data); next(i + 1, len); } }); } else { callback(null, Buffer.concat(output)); } }(0, pathnames.length)); }
function main(argv) {
var config = JSON.parse(fs.readFileSync(argv[0], ‘utf-8’)),
root = config.root || ‘.’,
port = config.port || 80;

http.createServer(function (request, response) {
var urlInfo = parseURL(root, request.url);

combineFiles(urlInfo.pathnames, function (err, data) {
if (err) {
response.writeHead(404);
response.end(err.message);
} else {
response.writeHead(200, {
‘Content-Type’: urlInfo.mime
});
response.end(data);
}
});
}).listen(port);
}

function parseURL(root, url) {
var base, pathnames, parts;

if (url.indexOf(‘??’) === -1) {
url = url.replace(‘/’, ‘/??’);
}

parts = url.split(‘??’);
base = parts[0];
pathnames = parts[1].split(‘,’).map(function (value) {
return path.join(root, base, value);
});

return {
mime: MIME[path.extname(pathnames[0])] || ‘text/plain’,
pathnames: pathnames
};
}

main(process.argv.slice(2));

如上代码完整兑现了服务器所需的听从,并且有以下几点值得注意:

  1. 利用命令行参数传递JSON配置文件路径,入口函数负责读取配置并创造服务器。

  2. 入口函数完整描述了程序的运作逻辑,其中解析URL和统一文件的求实实现封装在另外六个函数里。

  3. 解析URL时先将普通URL转换为了文件合并URL,使得三种URL的处理格局可以同样。

  4. 统一文件时采取异步API读取文件,制止服务器因等待磁盘IO而发出围堵。

大家可以把上述代码保存为server.js,之后就可以透过node server.js config.json指令启动程序,于是我们的首先版静态文件合并服务器就顺风完工了。

另外,以上代码存在一个不那么彰着的逻辑缺陷。例如,使用以下URL请求服务器时会有喜怒哀乐。

    http://assets.example.com/foo/bar.js,foo/baz.js

经过分析之后咱们会发现题目出在/被活动替换/??这么些行为上,而以此问题我们得以到第二次迭代时再解决。

其次次迭代

在首先次迭代过后,我们早已有了一个可工作的本子,满足了意义要求。接下来大家需要从性质的角度出发,看看代码还有怎么着改正余地。

设计

map措施换成for巡回或许会更快一些,但首先版代码最大的习性问题存在于从读取文件到输出响应的历程当中。我们以拍卖/??a.js,b.js,c.js以此请求为例,看看整个处理过程中耗时在何方。

 发送请求       等待服务端响应         接收响应
---------+----------------------+------------->
         --                                        解析请求
           ------                                  读取a.js
                 ------                            读取b.js
                       ------                      读取c.js
                             --                    合并数据
                               --                  输出响应

可以看来,第一版代码依次把请求的文本读取到内存中之后,再统一数据和出口响应。这会造成以下五个问题:

  1. 当呼吁的文本比较多相比较大时,串行读取文件会相比较耗时,从而拉开了服务端响应等待时间。

  2. 出于每一遍响应输出的数量都亟待先全体地缓存在内存里,当服务器请求并发数较大时,会有较大的内存开销。

对于第一个问题,很容易想到把读取文件的法门从串行改为互相。不过别这么做,因为对此机械磁盘而言,因为只有一个磁头,尝试并行读取文件只会招致磁头频繁抖动,反而下降IO效能。而对于机械硬盘,即便真正存在五个互相IO通道,可是对于服务器并行处理的四个请求而言,硬盘已经在做并行IO了,对单个请求采纳互动IO无异于拆东墙补西墙。由此,正确的做法不是改用并行IO,而是一边读取文件一边输出响应,把响应输出时机提前至读取第一个文本的天天。这样调整后,整个请求处理过程变成上边那样。

发送请求 等待服务端响应 接收响应
---------+----+------------------------------->
         --                                        解析请求
           --                                      检查文件是否存在
             --                                    输出响应头
               ------                              读取和输出a.js
                     ------                        读取和输出b.js
                           ------                  读取和输出c.js

按上述模式化解第一个问题后,因为服务器不需要完整地缓存每个请求的出口数据了,第二个问题也解决。

实现

遵照以上设计,第二版代码按以下办法调整了有的函数。

function main(argv) {
    var config = JSON.parse(fs.readFileSync(argv[0], 'utf-8')),
        root = config.root || '.',
        port = config.port || 80;

http.createServer(function (request, response) {
var urlInfo = parseURL(root, request.url);

validateFiles(urlInfo.pathnames, function (err, pathnames) {
if (err) {
response.writeHead(404);
response.end(err.message);
} else {
response.writeHead(200, {
‘Content-Type’: urlInfo.mime
});
outputFiles(pathnames, response);
}
});
}).listen(port);
}

function outputFiles(pathnames, writer) {
(function next(i, len) {
if (i < len) { var reader = fs.createReadStream(pathnames[i]);
reader.pipe(writer, { end: false });
reader.on(‘end’, function() {
next(i + 1, len);
});
} else {
writer.end();
}
}(0, pathnames.length));
}

function validateFiles(pathnames, callback) {
(function next(i, len) {
if (i < len) { fs.stat(pathnames[i], function (err, stats) { if (err) { callback(err); } else if (!stats.isFile()) { callback(new Error()); } else { next(i + 1, len); } }); } else { callback(null, pathnames); } }(0, pathnames.length)); }

可以看看,第二版代码在自我批评了请求的有着文件是否可行之后,顿时就输出了响应头,并随之一边按顺序读取文件一边输出响应内容。并且,在读取文件时,第二版代码直接利用了只读数据流来简化代码。

其三回迭代

第二次迭代从此,服务器本身的效应和性质已经拿到了始于满意。接下来大家需要从平静的角度再一次审视一下代码,看看还索要做些什么。

设计

从工程角度上讲,没有相对可靠的体系。尽管第二次迭代的代码通过多次检查后能保证没有bug,也很难说是否会因为NodeJS本身,或者是操作系统本身,甚至是硬件本身造成我们的服务器程序在某一天挂掉。因而一般生产条件下的服务器程序都配有一个守护进程,在劳务挂掉的时候顿时重启服务。一般守护进程的代码会远比服务过程的代码简单,从概率上得以确保医护进程更难挂掉。要是再做得严酷一些,甚至守护进程本身可以在友好挂掉时重启自己,从而实现双确保。

就此在此次迭代时,大家先选择NodeJS的过程管理机制,将守护进程作为父进程,将服务器程序作为子进程,并让父进程监控子进程的运行状态,在其分外退出时重启子进程。

实现

依照以上设计,大家编辑了医护进程需要的代码。

var cp = require('child_process');

var worker;

function spawn(server, config) {
worker = cp.spawn(‘node’, [ server, config ]);
worker.on(‘exit’, function (code) {
if (code !== 0) {
spawn(server, config);
}
});
}

function main(argv) {
spawn(‘server.js’, argv[0]);
process.on(‘SIGTERM’, function () {
worker.kill();
process.exit(0);
});
}

main(process.argv.slice(2));

数学,另外,服务器代码本身的入口函数也要做以下调整。

function main(argv) {
    var config = JSON.parse(fs.readFileSync(argv[0], 'utf-8')),
        root = config.root || '.',
        port = config.port || 80,
        server;

server = http.createServer(function (request, response) {

}).listen(port);

process.on(‘SIGTERM’, function () {
server.close(function () {
process.exit(0);
});
});
}

俺们得以把守护进程的代码保存为daemon.js,之后我们得以经过node daemon.js config.json起先服务,而护理进程会愈发启动和督查服务器进程。此外,为了可以健康终止服务,我们让医护进程在收到到SIGTERM信号时停下服务器进程。而在服务器进程这一端,同样在收取SIGTERM信号时先停掉HTTP服务再正常退出。至此,我们的服务器程序就靠谱很多了。

第五遍迭代

在我们解决了服务器本身的效果、性能和可靠性的题材后,接着我们需要考虑一下代码部署的题目,以及服务器控制的问题。

设计

诚如而言,程序在服务器上有一个恒定的配备目录,每一遍程序有改进后,都再也宣布到布置目录里。而假诺完成布局后,一般也得以因而一定的服务控制脚本启动和平息服务。因而我们的服务器程序部署目录可以做如下设计。

- deploy/
    - bin/
        startws.sh
        killws.sh
    + conf/
        config.json
    + lib/
        daemon.js
        server.js

在以上目录结构中,我们分类存放了服务控制脚本、配置文件和服务器代码。

实现

按以上目录结构分别寄存对应的文书从此,接下去我们看看控制脚本怎么写。首先是start.sh

#!/bin/sh
if [ ! -f "pid" ]
then
    node ../lib/daemon.js ../conf/config.json &
    echo $! > pid
fi

然后是killws.sh

#!/bin/sh
if [ -f "pid" ]
then
    kill $(tr -d '\r\n' < pid)
    rm pid
fi

于是乎这样我们就有了一个概括的代码部署目录和劳务控制脚本,大家的服务器程序就可以上线工作了。

后续迭代

我们的服务器程序正式上线工作后,我们接下去或者会发觉还有那几个足以改良的点。比如服务器程序在集合JS文件时方可自行在JS文件之间插入一个;来防止有些语法问题,比如服务器程序需要提供日志来总计访问量,比如服务器程序需要能充裕利用多核CPU,等等。而此时的您,在读书了这般久NodeJS之后,应该已经了然该肿么办了。

小结

本章将事先零散介绍的知识点串了四起,完整地示范了一个行使NodeJS开发顺序的例子,至此我们的教程就所有扫尾了。以下是对新出生的NodeJSer的片段指出。

  • 要熟知官方API文档。并不是说要熟习到能记住每个API的称谓和用法,而是要熟知NodeJS提供了什么职能,一旦需要时知道查询API文档的哪块地点。

  • 要先规划再落实。在开发一个主次前先是要有一个大局的规划,不必然要很系数,但要丰硕能写出有些代码。

  • 要落实后再规划。在写了一些代码,有了部分实际的事物后,一定会发现一些此前忽视掉的细节。这时再反过来立异此前的设计,为第二轮迭代做准备。

  • 要丰富利用三方包。NodeJS有一个特大的生态圈,在写代码在此之前先看看有没有现成的三方包能省去成千上万时间。

  • 决不迷信三方包。任何工作做过度了就不好了,三方包也是均等。三方包是一个黑盒,每多接纳一个三方包,就为顺序增添了一份机密风险。并且三方包很难恰好只提供程序需要的效率,每多应用一个三方包,就让程序更加臃肿一些。由此在决定拔取某个三方包在此以前,最好三思而后行。

问问我的心底,嗯,是事业,没错。

自我依旧挺喜欢教书的,喜欢教书的这种感觉,喜欢学生的问候。有一个女孩子很让我激动,每便见到本人,无论是在旅途,依然在楼梯口,都是90°鞠躬,然后说:老师好!非常可爱的一个女童。尽管现在,她并不曾在自家班上。

本人是一个中将,我太太也是教授。

俺们俩同一年插足入编考试,分配到均等所中学教学。几个人都是教高中,一个教化学,一个教数学,都是理科。相比幸运,这两年我们俩先后评上了一流职称。

二〇〇八年到前日,讲台上足足站了10年。实际上我还在小学的讲坛上站了1年,那是在陕西支教的一年,这一年在自家的前半生中占据举足轻重职位。犹记得二〇〇七年六月高校毕业,四月下旬回去新加坡插手志愿者培训,和一群来自天菲律宾海北的兄弟姐妹们,等待着无处希望小学的号召。很幸运地去了青海,去了山谷沟里的一所希望小学。没去以前心境澎湃,去了后头渐渐冷静下来,像个当地人一样,给男女们带去知识与喜悦。同时,也取得自己心灵上的清洗,接受云贵高原上的紫外线照射,奔跑时感受着淡淡的的气氛,贪婪地呼吸着特有的泥土气息。这一年时光里,吃住都在全校,只有寒假回了一趟家。爸妈看自己瘦的黑的不善样子,眼泪就簌簌落了下去。我的自觉,四姨一向不会干涉,只会默默补助着自己。

归来后,在小镇里落了户。迄今当过5年的班首席执行官,带过4届的高三毕业班,二〇一九年是第5届。和率先届的学习者最亲,当时的班长还一贯有挂钩,现在的她也已经大学毕业多年,有回家时必定来我这里坐一坐,聊一聊过去的年华、现在的生活、以及美好的前途。有生之年,可以完成“朱八届”的壮举。呵呵,实际上这个名头在不远的年月里就会到来,想想到时他们扣在自家头上,喊着猪八戒时,会是哪些的一种好玩?

光明的活着都是非凡单调的。现实的活着或者充满了激励。

要买车了,要买房了,要二胎了,要评职称了,要列席比赛了,要开公开课了。

要钱,真的没有多少。我不是贪心的人,我容易满足。所以现在的生存,我倒也踌躇满志。然则,没有相比,就从未危害。生活中怎么会并未对待。高校里停满了大大小小的小汽车;办公室里我们谈论的都是什么人家的房子地点好、装修很气派;什么人家的二娃生了,凑了一个“好”字;二零一九年哪位导师又在场全国的多媒体课堂大赛取得了一等奖。我没办法参与互换,有时候倒是想把团结藏起来,藏在地下室中,发酵个三年五年,再拿出去会不会像美酒一样,甘醇如饴?

我羡慕的是该校里教书教的好的这位塞尔维亚语老师,面对这群不爱学习的学童,照样能教出好成绩。我羡慕的是该校里教书教的好的这位化学老师,永远声音洪亮底气十足,充满情绪。我羡慕的是该校里教书教的好的这位音乐老师,沉醉于音乐中,如痴如醉。呵呵,实际上他们也是成功人员,该有的车啊、房呀、钱呀都不缺。

自我和太太是只拿工资的人,其他外快都不理解去赚
。于是,在该校的助教宿舍里住了10年,儿童也随之我们住在这里。旦旦还把这里当做自己的家,点头的房子是太婆的家,潋城的屋宇是祖母的家。回点头是去姑婆家作客,回潋城是去外婆家作客。既然是作客,肯定是不可能呆太久。于是,到了夜间,旦旦就吵着回前岐,回学校的家。

旦旦虽小,也是很羡慕其他的幼儿有一个福鼎的家。看着她的好对象一天坐着他老爹的反动的轿车回福鼎,他是爱戴的:“阿姨,大家什么时候有一个福鼎的家啊?”

我会伤心的,也不是就买不起福鼎的房子,也不是就买不起小车,公积金和借款完全可以化解这一个问题的。可自己连续觉得,过不了几年,等福鼎到点头的滨海大道修好了;过不了几年,等福鼎到前岐的隧道顺利通车了,来来回回都是1个时辰内的题材。没必要乘着高房价去买一幢还有点住的福鼎房子。

俺们俩,扎根于全校,可能还会有很长一段时间吧。日子仍然安逸一点,有如何不好吧?

相关文章

No Comments, Be The First!
近期评论
    分类目录
    功能
    网站地图xml地图