https://nqdeng.github.io/7-days-nodejs/
-
cmd模块系统
使用require、exports、module三个预先定义好的变量。⚠️和ES6的import、export分开。
require用于传入一个模块名,返回一个模块导出对象。可以./开头引入相对路径,也可以/或盘符:开头引入绝对路径,还可以直接foo/bar,类似关键词搜索。文件扩展名(后缀)可省略。例:var foo1 = require('./foo');
exports导出当前模块里的对象 例:exports.hello = function () { console.log('hello world!'); }
module.exports对象是由模块系统创建的,可以通过改变它来改变当前模块的默认导出对象。例如:
模块默认导出对象变成一个函数。module.exports = function () { console.log('Hello World!'); };
⚠️module.exports的赋值必须立即执行。不能再任何回调中执行。
模块初始化:一个模块中的JS代码仅在模块第一次被使用时执行一次,并在执行过程中初始化模块的导出对象。之后,缓存起来的导出对象被重复利用。也就是说第一次require后导出对象被缓存,第二次require该对象无用无影响.
-
模块路径解析规则
- 内置模块
如果给require传入的是NodeJS内置模块名称,不做路径解析,直接返回内部模块的导出对象,例require('fs'); - node_modules目录:用于存放模块。查找规则:基于本目录进行由下向上查找。
- NODE_PATH环境变量:可通过其指定额外的模块搜索路径。可包含多个路径。linux 用:分隔。windows用;分隔。加载模块时会依次尝试。
-
包
很多子模块组成的大模块称作包。物理上就是文件夹下面有很多js文件,这个文件夹就可以被称作包。每个包都需要一个入口模块。入口模块的导出对象被作为包的导出对象。
可以在包下的pakage.json中定义包名和入口文件:
{ "name": "cat", "main": "./lib/main.js" } 这样就可以在外面用require('/home/user/lib/cat')的方式引用cat包。主要目的是不出现入口文件main.js的引用。
-
命令行程序:自制脚本
- 在脚本第一行: #! /usr/bin/env node
- 给文件执行权:chmod +x /home/user/bin/node-echo.js
- 在PATH环境变量中指定的某个目录下,建立软链接文件,文件名与我们希望使用的终端命令同名: sudo ln -s /home/user/bin/node-echo.js /usr/local/bin/node-echo
-
工程目录
main可能写错了,指向编译后的最上层的入口文件。
-
发布代码
第一次使用NPM发布代码前需要注册一个账号。终端下运行npm adduser
,之后按照提示做即可。
常用npm命令-
NPM提供了很多命令,例如
install
和publish
,使用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>
可以撤销发布自己发布过的某个版本代码。
-
-
文件
- 文件拷贝:
- 小文件:
- 大文件:
- 小文件:
- API简介
- Buffer(数据块):二进制
- NodeJS提供了一个与
String
对等的全局构造函数Buffer
来提供对二进制数据的操作。 - 除了可以读取文件得到
Buffer
的实例外,还能够直接构造:var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]); //可用.length 和 bin[0] - 可与字符串转化:var str = bin.toString('utf-8'); var bin = new Buffer( 'hello', 'utf-8');
- 与string的区别:
- 字符串只读,并且对字符串的任意修改得到的都是新字符串,原字符串保持不变。而buffer类似指针数组。
- .slice也不是返回新buffer,而是指向某位置。因此对.slice返回的buffer做修改会影响到原buffer
- 因此,要想不对原buffer有影响,要先申请一块内存,再将原有拷贝过去: var dup = new Buffer(bin.length); bin.copy(dup); //感觉和c的指针数组一毛一样
- NodeJS提供了一个与
- Stream(数据流):例如大文件拷贝,当内存中无法一次装下需要处理当数据时,或者一边读一边处理更高效时,就需要用到数据流。
- 只读流
- 只写流
- .pipe也是做类似的防爆仓控制,它的内部实现方式与上边的代码类似
- 只读流
- File System(文件系统):对文件进行操作。fs模块提供的API基本上可以分为一下三类:
- 文件属性读写: fs.stat、fs.chmod、fs.chown等
- 文件内容读写:fs.readFile、fs.readdir、fs.writeFile、fs.mkdir等
- 底层文件操作:fs.open、fs.read、fs.write、fs.close等
- 上边提到的这些api都是通过回调函数传递结果。以fs.readFile为例:
- 每个异步api都对应一个同步版本。末尾+Sync,异常对象和执行结果的传递方式也不同,如上。
- Path(路径):
- path.normalize: 将传入的路径转换为标准路径,能解析.和..还能去掉多余的斜杠。如path.normalize( 'foo//baz//../bar' ) => 'foo/bar'
⚠️⚠️⚠️ windows是\,linux是/ 。如果想保证任何系统下都是用/作为路径分隔符的话,需要用.replace(/\\/g, '/') 在替换下标准路径。 - path.join: 将传入的多个路径拼接为标准路径,并且能在不同系统下正确使用相应分隔符。例:path.join( 'foo/', 'baz/', '../bar' ); => 'foo/bar'
- path.extname:获得文件的扩展名。例: path.extname(' foo/bar.js '); => '.js'
- path.normalize: 将传入的路径转换为标准路径,能解析.和..还能去掉多余的斜杠。如path.normalize( 'foo//baz//../bar' ) => 'foo/bar'
- Buffer(数据块):二进制
- 遍历算法
- 递归 :最好转化成循环
- 目录遍历:树的先序遍历:根左右
- 同步遍历:
- 异步遍历:
- 文本编码
- 要把文件内容换为js使用的UTF-8,可用文本文件头的几个字节来判断文件是否包含BOM,以及使用的是哪种编码。一般要去掉BOM
- 识别和除去UTF8 BOM:
- GBK转UTF8: 借助第三方包iconv-lite来转换编码
- 单字节编码:中文→ 乱码 → 中文 ,只要按照相同规则中文前后不会变
- 文件拷贝:
-
网络操作
- http模块提供两种使用方式:
- 作为服务端使用:创建一个http服务器,监听http客户端请求并返回响应。
- http请求和响应数据内容
- 处理请求示例
- 编辑发送响应示例
- http请求和响应数据内容
- 作为客户端使用:发起一个http客户端请求,获取服务端响应。
- 编辑发起请求示例
- 通过回调函数处理响应示例(简化get/post等方法,提供了更简单的api)
- 编辑发起请求示例
- 作为服务端使用:创建一个http服务器,监听http客户端请求并返回响应。
- https:
https
模块与http
模块极为类似,区别在于https
模块需要额外处理SSL证书。- 创建https服务器
- 客户端编辑发起请求:
- 创建https服务器
- url
- 组成:
- parse方法可将一个url字符串转换为url对象。(参数可以不完整)
url.parse('http://user:pass@host.com:8080/p/a/t/h?query=string#hash'); - format方法允许将一个url对象转换为url字符串
url.format({ protocol: 'http:', host: 'www.example.com', pathname: '/p/a/t/h', search: 'query=string' }); - resolve 拼接url
url.resolve('http://www.example.com/foo/bar', '../baz');
- 组成:
- query string
- 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 :数据压缩和解压的功能
- net:
net
模块可用于创建Socket服务器或Socket客户端 -
使用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
属性来处理。
-
- http模块提供两种使用方式:
- 进程
- API
- process: 可以感知和控制nodejs自身进程的方方面面。不是内置模块,而是一个全局对象,因此在任何地方都可以直接用。
- child_process: 创建和控制子进程,该模块最核心的api是.spawn。其余api都是针对特定使用场景对他的进一步封装,算是一种语法糖。
- cluster:对child_process的进一步封装,专用于解决单进程nodejs web服务器无法充分利用多核cpu的问题。使用该模块可以简化多进程服务器程序的开发,让每个核上运行一个工作进程,并统一通过主进程监听端口和分发请求。
- 获取命令行参数: process.argv.slice(2);
- 退出程序: try{ //... } catch (err) { process.exit(1) }; 正常退出: process.exit(0);
- 控制输入输出:
- 标准输入流: process.stdin 只读数据流
- 标准输出流:process.stdout 只写数据流
- 标准错误流:process.stderr 只写数据流
- 实现console.log :
- 降权:在Linux系统下,我们知道需要使用root权限才能监听1024以下端口。但是一旦完成端口监听后,继续让程序运行在root权限下存在安全隐患,因此最好能把权限降下来。
- ⚠️
- 如果是通过sudo获取root权限的,运行程序的用户UID和GID保存在环境变量SUDO_UID和SUDO_GID里边。如果是通过chmod+s方式获取root权限的,运行程序的用户UID和GID可直接通过process.getuid和process.getgid方法获取。
- process.setuid和process.setgid方法只接受number类型的参数。
- 降权必须先绛gid再绛uid
- ⚠️
- 创建子进程
- child_process.spawn(command[, args][, options]) 第一个参数可以是路径也可以是path环境变量能找到的执行文件名(命令名) 第二个是命令行参数数组。第三个可选用于配置子进程的执行环境与行为
- 另外,上例中虽然通过子进程对象的
.stdout
和.stderr
访问子进程的输出,但通过options.stdio
字段的不同配置,可以将子进程的输入输出重定向到任何数据流上,或者让子进程共享父进程的标准输入输出流,或者直接忽略子进程的输入输出。
- 进程间通讯
-
//父进程在创建子进程时,在options.stdio字段中通过ipc开启了一条ipc通道
//之后就可以监听子进程对象的message事件接受来自子进程的消息,并通过.send方法给子进程发送消息。
//在子进程这边,可以在process对象上监听message事件接收来自父进程的消息,并通过.send方法向父进程发送消息。
//数据在传递过程中,会先在发送端使用JSON.stringify方法序列化,再在接收端使用JSON.parse反序列化
- 守护子进程
- 监控工作进程的运行状态,在工作进程不正常退出时重启工作进程,保障工作进程不间断运行。
- 监控工作进程的运行状态,在工作进程不正常退出时重启工作进程,保障工作进程不间断运行。
- API
- 异步:
- 串行异步: 数组成员一个接一个执行:
- 并行异步: 数组成员可以并行,但后续代码需要所以数组成员处理完毕后才能执行。
- 异常:
- 普通的try catch只能捕获同步代码中的异常。异步函数会打断代码执行路径,异步函数执行过程中以及执行之后产生的异常冒泡到执行路径被打断的位置时,如果一直没有遇到try语句,就会作为一个全剧异常抛出。
- 异步异常的正确捕获方式
- 对于接连有多个异步的异常捕获,如果按上述写法,回调嵌套使得代码层级会很深,因此node推出以下解决方案。
- 可以通过process对象捕获全局异常
process.on('uncaughtException', function (err) { console.log('Error: %s', err.message); });
- 如果想尽早捕获,可以使用域(domain)。
- 用域:
- ⚠️无论是通过
process
对象的uncaughtException
事件捕获到全局异常,还是通过子域对象的error
事件捕获到了子域异常(回调),捕获异常后强烈简易立即重启程序,try一般不用。
- 可以通过process对象捕获全局异常
- 普通的try catch只能捕获同步代码中的异常。异步函数会打断代码执行路径,异步函数执行过程中以及执行之后产生的异常冒泡到执行路径被打断的位置时,如果一直没有遇到try语句,就会作为一个全剧异常抛出。
- 串行异步: 数组成员一个接一个执行:
- 大示例
-
静态文件合并服务器,该服务器需要支持类似以下格式的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
以上就是整个需求。
working/node/example_server.js
-