NodeJS
快速上手
浏览器内核
常说的浏览器内核指的是浏览器排版引擎 layout engine,也可称为浏览器引擎、页面渲染引擎或样板引擎。
主流浏览器内核组成:
- IE/Edge browser:Trident 转向 Blink
- Chrome browser:Webkit 转向 Blink -> 统称为 Chromium
- Firefox browser:Gecko
- Safari browser:Webkit
- Opera browser:Presto 变更为 Webkit,现在转向 Blink
浏览器内核负责对网页语法的解释以及渲染网页,通常由渲染引擎和 JS 引擎两部分组成。
浏览器渲染过程
在浏览网页过程中,所有资源并非一捆下载,而是解析时到具体的标签时,再去相应的定位处下载资源。
- 进入页面 HTML 被首先下载,HTML Parser 开始解析标签并生成 DOM Tree
- DOM Tree 生成时会有 JS 引擎帮助解析 JavaScript 代码对 DOM 进行操作
- 因 HTML 的解析过程是自上而下的,所以在遇见 <link> 标签表示的 CSS 样式时,会根据 CSS Paser 生成对应的 CSS Rules 并与 DOM Tree 结合产生 Render Tree
- Render Tree 在生成时会根据 Layout Engine 进行针对浏览器状态的适配调整,并在完成后进行 Painting 与 Display
- HTML 解析过程中遇见 JavaScript 会停止解析,不同于 CSS 异步加载执行,因为 JavaScript 代码可以操作 DOM
JavaScript Engine
高级的编程语言最终都是需要转成机器指令来执行的。JavaScript 无论是交给浏览器还是 Node.JS,最后都是需要只认识机器语言指令集的 CPU 执行。所以需要 JS 引擎帮助将 JavaScript 代码翻译成 CPU 指令进行执行。
=> 常见 JavaScript Engine
SpiderMonkey => The first JavaScript engine & Developed by Brendan Eich
Chakra => 微软开发并用于 IE 浏览器
JavaScriptCore => Apple 开发并用于 WebKit
v8 => Google 开发用于 Chromium
V8 引擎原理
C++ 编写的 V8 engine 由 Google 开源,是高性能 JS 和 WebAssembly 引擎,用于 Chrome 和 Node 等。V8 可以独立运行,也可以嵌入到任何 C++ 应用程序中。
V8 engine implements ECMAScript and WebAssembly, and runs on Windows 7 or later, macOS 10.12+, and Linux systems that use x64, IA-32, ARM, or MIPS processors.
- ECMAScript is a Standard for scripting languages such as JavaScript, JScript, etc. It is a trademark scripting language specification. JavaScript is a language based on ECMAScript. A standard for scripting languages like JavaScript, JScript is ECMAScript.
- WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications.
JS 经过词法分析生成由 type 与 value 组成的对象并存储在 tokens 数组。随后的语法分析生成 AST 抽象语法树。
类似 Babel 将 TS 转为 JS,也是先生成 Abstract Syntax Tree 并作出相应修改,产生 new Abstract Syntax Tree,再 generate code 为 JS。
同理 Vue 中 Template 也是先转换成 Abstract Syntax Tree 再 CreateVNode 虚拟节点,最后产生 JS。
const name = 'zs'
/* 词法分析 */
[
{ type: 'Keyword', value: 'const' },
{ type: 'Identifier', value: 'name' },
{ type: 'Punctuator', value: '=' },
{ type: 'String', value: "'zs'" }
]
/* 语法分析 */
{
"type": "Program",
"start": 0,
"end": 17,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 17,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 17,
"id": {
"type": "Identifier",
"start": 6,
"end": 10,
"name": "name"
},
"init": {
"type": "Literal",
"start": 13,
"end": 17,
"value": "zs",
"raw": "'zs'"
}
}
],
"kind": "const"
}
],
"sourceType": "module"
}
官方 V8 引擎原理中是通过 Blink 将源码交给 V8 engine,以 Stream 获取到源码并且进行编码转换;Scanner 进行词法分析生成 tokens,再经过 Parser 和 PreParser 转换成 AST。不同于前者的直接解析转换,后者是预解析,因为并不是所有的 JS 代码在一开始时就会被执行,所以通过延迟解析将不必要的函数进行预解析(函数的全量解析是在函数被调用时才进行 => 函数 outer 内部定义函数 inner,那 inner 函数就会进行预解析)。生成的抽象语法树会被 Ignition 转成字节码后执行。
IO 密集
非阻塞 IO 模型 => IO 即计算机输入输出,常见有外接硬件、磁盘读写、网络传输和数据库操作。阻塞 IO 即进行 IO 操作时,进程处于休眠状态,等待 IO 操作完成再通知主进程进行后续处理(触发事件函数通知主进程)。
事件驱动 => 区别于 Nginx 多进程单线程,NodeJS 通过事件驱动的方式处理请求时无需为每一个请求创建额外的线程。每一个 IO 操作都会被添加到事件队列中,线程循环地处理队列上的工作任务,当执行过程中遇到阻塞,线程不会停下来等待结果,而是留下一个处理结果的回调函数,转而继续执行队列中的下一个任务。这个传递到队列中的回调函数在阻塞任务运行结束后才被线程调用。
- IO 密集与 CPU 密集
CPU 密集 => 程序大部分时间用于处理逻辑运算、文件压缩解压与数据加密解密
IO 密集 => 程序大部分时间用来做数据存储以及网络读取的操作
- WEB 开发的 IO 密集
当浏览器中的请求到达服务器时,除了使用 cpu 进行计算 uri 路径的文件位置,剩下的都是文件读取以及数据库操作,由此可见 http 请求大部分还是 IO 操作。此外在页面渲染时,除开使用 cpu 计算,其他涉及读取模板文件或根据数据生成 Html 都算做 IO 操作。所以说 WEB 开发是典型的 IO 密集的场景。
- 进程与线程
进程 Process 通俗来说就是正在内存中运行的程序,多进程是通过 cpu 调度算法在纳秒单位切换执行多个进程。线程是进程内一个相对独立的可调度的执行单元,也就是进程中单一顺序的控制流。多任务就是说在一个进程可以并发多个线程,而每条线程并行执行不同的任务。一个进程开启一项任务,就是打开了一个线程,类似于 Downie4 开始了一个下载任务。
- REPL Read-Eval-Print Loop => 交互式编程环境。
$ node
Welcome to Node.js v14.17.1.
Type ".help" for more information.
$ process
process { version: 'v14.17.1', versions: { node: '14.17.1', v8: '8.4.371.23-...
node index.js zszszs age=22 // 给 node 传参使用 process 内置对象的 argv(argument vector) 接收
console.trace(); // 打印函数调用栈
Global object & Module
- 全局对象 => 在程序的任何位置可以直接访问的对象
在 JS 源代码通过 Parse 转换为 AST 的阶段,会创建全局对象 GlobalObject,并放入 window 属性(即 this 指向当前 GlobalObject)、setTimeout...。存在部分特殊的全局变量,这些全局变量实际上是模块中的变量,只不过每个模块都有,所以看起来好像是全局变量,但在命令行交互中不可直接使用。
// 常见的全局变量
process // 提供Node进程相关信息
console // 控制台
// 定时器全局函数
setTimeout(() => {},time)
setInterval(() => {},time)
setImmediate(() => {})
process.nextTick(() => {})
// 全局对象
global // global. 按两下 Tab
// 特殊的全局变量
// 需要进入文件所在文件夹再 node index.js
__dirname // 打印当前文件夹所在目录绝对路径
__filename // 打印当前文件所在目录绝对路径
exports、module、require() // 模块化相关
V8 在创建 GO 后,为执行代码还需要 ECStack Execution Context Stack 执行上下文栈作为容器去接收 GEC Global Execution Context 全局执行上下文。
VO Variable Object 变量对象在编译时,其内每一项只是对应 GO 的初始未赋值属性,只有在执行期间才会对相应属性更改。
/* --- 伪代码 --- */
/*
GO =>
var globalObject = {
String:"Class", Date:"Class",setTimeout:"Func",window:globalObject,name:undefined,age:undefined
}
*/
// 解释变量提升为什么出现 undefined
var name = "zs"
console.log(age) // 编译完成但未执行 => GO 中还是 undefined
var age = 22
- CJS 与 ESM 模块的导入与导出
Node CJS 不同于 ES6 ESM。后者使用 export 与 export default 导出,在需要模块的文件中以 import from "模块名" 导入。
export 导出需使用 {} 配合导入;export default 导出的模块可直接导入,无需明确所要加载模块的变量名(默认名)。模块中 export、import 不做限制,而 export default 最多只能有一个。
// ES6 —— export
// a.js
export const str = "zairesinatra";
export function zs (sth) {
return sth;
}
// 对应的导入方式:
// b.js
import { str, zs } from 'a'; // 也可以分开写两次,导入的时候带花括号
// ES6 —— export default
// a.js
const str = "zairesinatra";
export default str;
// 对应的导入方式:
//b.js
import str from 'a'; //导入的时候没有花括号
// ES6 —— 自由命名
// a.js
let name = "xzy";
export default name // name不能加大括号
// 原本直接export name外部是无法识别的,加上default就可以了.但是一个文件内最多只能有一个export default。
// 其实此处相当于为name变量值"xzy"起了一个系统默认的变量名default,自然default只能有一个值,所以一个文件内不能有多个export default。
// b.js
// 本质上,a.js文件的export default输出一个叫做default的变量,然后系统允许你为它取任意名字。所以可以为import的模块起任何变量名,且不需要用大括号包含
import zs from "./a.js"
import zy from "./a.js"
console.log(zs,zy) // xzy,xzy
CommonJS 使用 require 引入后的返回只有 module.exports 对象。exports 对象实际上只是对 module.exports 的引用。module.exports 初始值为一个空对象 {}。
// 区分 exports 与 module.exports
// 如果覆盖 exports 的值,那么将丢失对 module.exports 的引用,而 module.exports 就是作为公共接口公开的内容
var module = new Module(...);
var exports = module.exports;
// CommonJS —— 导出单个模块
// formatTime
function formatTime (){
// 需要的格式 yyyy-MM-dd hh:mm:ss
var date = new Date(); // 或者传入一个时间戳
Y = date.getFullYear() + '-';
M = (date.getMonth()+1 < 10 ? '0'+(date.getMonth()+1) : date.getMonth()+1) + '-';
D = date.getDate() + ' ';
h = date.getHours() + ':';
m = date.getMinutes() + ':';
s = date.getSeconds();
console.log(Y+M+D+h+m+s); // 当前时间格式化输出
}
module.exports = formatTime
// requireFT
var requireFT = require('./formatTime') // 当然 .js 可以省略
requireFT() // 2019-08-29 20:33:48
// CommonJS —— 导出多个模块
// func.js
var func1 = () =>{console.log('I am func1')}
var func2 = function(){console.log('I am func2')}
module.exports.func1 = func1;
module.exports.func2 = func2;
// 可简写
module.exports = { // 键值同名可以只写一个
func1: func1, // func1,
func2: func2 // func2
}
// requireFunc.js
var requireObj = require('./formatTime')
requireObj.func1(); // I am func1
requireObj.func2(); // I am func2
// mod1.js
let name = "ok"; // 字符串是值引用,已经指定了地址
setTimeout(() => { name = "okk" }, 1000)
module.exports = { name: name }
// exec1.js
const mod1 = require('./mod1')
setTimeout(() => { console.log(mod1.name); }, 2000) // ok
// mod2.js
let info = { name: "ok"}; // 引用对象在堆内存开辟空间
setTimeout(() => { info.name = "okk" }, 1000) // 改变了指针
module.exports = { info } // info 赋值的是内存地址
// exec2.js
const mod2 = require('./mod2')
setTimeout(() => { console.log(mod2.info.name); }, 2000) // okk
require 是帮助引入模块导出对象的同步函数。require 查找模块首先会判断是否为核心模块,是则直接返回并停止查找;不是则分类讨论。
当 ./
、../
或 /
开头的文件有后缀名时,先按指定后缀查找,没有则按 js 文件、json 文件、node 文件的顺序查找。若仍无相应文件,则按目录名查找。
目录名查找 => 从最近的 node_modules 文件夹查到根目录的 node_modules 文件夹。
- 模块的加载过程
模块首次引入时会直接运行一次;模块被多次引入也只会加载一次。因每个模块对象都有一个属性 loaded 表示加载完成的状态。
/* module 对象的属性 */
module.id 模块的识别符,通常是带有绝对路径的模块文件名。
module.filename 模块的文件名,带有绝对路径。
module.loaded 返回一个布尔值,表示模块是否已经完成加载。
module.parent 返回一个对象,表示调用该模块的模块(程序入口文件的module.parent为null)
module.children 返回一个数组,表示该模块要用到的其他模块。
module.exports 表示模块对外输出的值。
算法中图结构在遍历时有深度优先搜索和广度优先搜索,模块在引入时采用深度优先搜索。下图加载顺序为 main、a、c、d、e、b。
- 其他模块化 => AMD 和 CMD
AMD 是 Asynchronous Module Definition 移步模块化的缩写,采用异步加载模块。常用的库是 require.js 和 curl.js。
<script src="./lib/require.js" data-main="./index.js"></script>
// index.js
(function(){
require.config({
baseUrl: '',
path: {
"bar": "./modules/bar", // 对应模块的映射关系
"foo": "./modules/foo"
}
})
require(['foo'], function(foo){})
})()
// bar.js
define(function() {
const name = "zszs";
const age = 21;
const sayHello = function(){
console.log("hi" + name);
}
return {
name: name,
age: age,
sayHello: sayHello
}
})
// foo.js
define(['bar'], function(bar){
console.log(bar.name);
console.log(bar.age);
bar.sayHello("zy");
})
CMD 是 Common Module Definition 通用模块定义的缩写,也采用异步加载模块。实现方案是 SeaJS。
<script src="./lib.sea.js"></script>
<script>
seajs.use('./index.js')
</script>
// index.js
define(function(require, exports, module){
const foo = require('./modules/foo');
console.log(foo.name);
console.log(foo.age)
console.log(foo.sayHello('zy'))
})
// foo.js
define(function(require, exports, module){
const name = "zs";
const age = 21;
const sayHello = function(name){
console.log("hi" + name);
}
module.exports = {
name,
age,
sayHello
}
})
内置模块
File system
任何为服务端服务的语言或框架通常会有各自的文件系统。fs 模块用于文件系统交互。
- fs.existsSync(path) => 检查传参 path 是否存在;异步 fs.exists 已废弃
Node 大部分异步 API 会有回调函数 callback 作为参数,而大部分同步 API 不会。
// 创建文件夹
const fs = require('fs')
const dirname = './testDirectory'
if(!fs.existsSync(dirname)){
fs.mkdir(dirname, err => { console.log(err); });
}
- fs.readdir(path[, options], callback) => 读取目录的内容;回调函数的参数 files 是目录中文件名的数组,不递归向下
// 读取文件夹所有文件
const fs = require('fs')
fs.readdir("./testDirectory", (err, files) => {
console.log(files)
})
- fs.rename(oldPath, newPath, callback) => 对文件异步重命名;newPath 已经存在的情况下,文件会被覆盖,目录则会抛出错误
// 重命名
const fs = require('fs');
fs.rename("./files/dirtest", "./files/dirtestandrename", err => {
console.log(err)
})
- fs.readFile(path[, options], callback) => 异步读取文件的全部内容
fs.readFile() 是对 fs.read() 的封装,后者使用流程应先用 fs.stat() or fs.fstat() 获取对象文件信息,再通过 fs.open() 创建文件描述符 fd,最后才能以 fs.read() 读取内容。
// readFile() 读取文件
const fs = require("fs");
fs.readFile("./files/1.txt", {encoding: 'utf-8'}, function (err, dataStr) {
if (err) { return console.log("读取文件失败!" + err.message); } // 读取成功则 err 的结果为 null, 失败则 dataStr 的结果为 undefine
return console.log("读取文件成功!" + dataStr);
});
- fs.writeFile(file, data[, options], callback) => 当 file 是文件名时,异步将数据写入文件,如果文件已存在则替换文件;追加内容
// 写入文件内容
const fs = require("fs");
fs.writeFile("./files/toBeWrite.txt", "Hello node.js!", function (err) {
if (err) { return console.log("文件写入失败" + err.message); } // 文件写入成功 err 值等于 null, 文件写入失败,则 err 的值为错误对象
console.log("文件写入成功");
});
// 追加文件内容
const fs = require("fs");
fs.writeFile("./files/toBeWrite.txt", "\r\nHello node.js!", {flag: "a"}, function (err) {
if (err) { return console.log("文件写入失败" + err.message); } // 文件追加成功 err 值等于 null, 文件追加失败,则 err 的值为错误对象
console.log("文件追加成功");
});
- fs.appendFile(path, data[, options], callback) => 将数据异步附加到文件,如果文件尚不存在则创建文件。data可以是字符串或<Buffer>。
// 追加文件内容
const fs = require("fs");
fs.appendFile("./files/toBeWrite.txt", "\r\n'data to append'", function (err) {
if (err) { return console.log("文件追加失败" + err.message); } // 文件写入成功 err 值等于 null, 文件写入失败,则 err 的值为错误对象
console.log("文件追加成功");
});
文件描述符 file descriptors fd => POSIX 系统对于每个进程,内核都维护着一张当前打开的文件和资源表格。每个打开的文件都分配了一个称为文件描述符的简单数字标识符。在系统层,所有文件系统操作都使用文件描述符标识和跟踪特点的文件。为简化用户工作,Node 抽象出操作系统之间差异,并为所有打开文件分配一个数字型的文件描述符。
const fs = require("fs");
const path = require("path");
const fdfilepath = path.resolve(__dirname + "/files/fd.txt");
fs.open(fdfilepath, (err, fd) => {
if (err) { return; }
fs.fstat(fd, (err, info) => {});
});
const content = "hello fd";
fs.writeFile("./files/fd.txt", content, { flag: "a" }, (err) => { console.log(err); });
fs.readFile("./files/fd.txt", null, (err, data) => { console.log(data.toString()); }); // <Buffer> => 16进制是二进制的一种表现形式
Path
path 模块提供用于处理文件和目录路径的方法,屏蔽不同环境下分隔符的差异。
- path.basename(path[, ext]) => 返回最末尾分隔符的后续部分;ext -> 可选的文件扩展名,区分大小写
var path = require("path");
console.log(path.extname("/a/b/c.java"), path.basename("/a/b/c.java"), path.basename("/a/b/c.java", '.java')) // .java c.java c
- path.delimiter => 提供特定于平台的路径分隔符
var path = require("path");
console.log(process.env.PATH.split(path.delimiter)) // [...]
- path.dirname(path) => 返回路径目录名称
var path = require("path");
console.log(path.dirname('/a/b/c')) // /a/b
- path.extname(path) => 返回路径拓展名
var path = require("path");
console.log(path.extname('index.coffee.md')) // .md
- path.normalize(path) => 规范化给定的路径 -> 多个连续的路径段分隔符会被特定于平台的路径段分隔符的单个实例替换
var path = require("path");
var pathStr = path.normalize("a/b//c///d\\\e");
console.log(pathStr); // a/b/c/d\e
- path.parse(path) => 返回对象,其属性表示路径有效元素
var {parse} = require("path");
console.log(parse("/a/b/c.java")) // { root: '/', dir: '/a/b', base: 'c.java', ext: '.java', name: 'c' }
- path.format(pathObject) => 把对象转化为一个路径字符串
var {format} = require("path");
console.log(format({ root: '/', dir: '/a/b', base: 'c.java', ext: '.java', name: 'c' })) // /a/b/c.java
- path.resolve([...paths]) => 将一系列路径或路径段解析为绝对路径
var path = require('path') // 若不传入 path 则返回当前工作目录的绝对路径
console.log(path.resolve('a/b/c')) // 拼接当前的目录 => /Users/xieziyi/a/b/c
- path.join([...paths]) => 使用特定于平台的分隔符作为分隔符将所有给定的段连接
var {join} = require('path')
console.log(join('a','/b','../c','/d')) // a/c/d
两者都有连接字符串的效果,区别在于前者会判断拼接字符串路径是否包含以 / 或 ./ 或 ../ 开头的路径分隔符,并以路径分隔符拼接当前文件所在的路径。
以 / 开头 resolve 和 join 效果一致,./ 或 ../ 则会拼接上当前文件所在位置路径。
- path.sep => 提供特定于平台的路径段分隔符
var path = require("path");
console.log('a/b/c'.split(path.sep)); // [ 'a', 'b', 'c' ]
Http
不同于 Tomcat 或 Apache 等容器,http 模块具有创建服务器的功能。
- http.createServer([options][, requestListener]) => 创建 Web 服务器
const http = require('node:http');
const server = http.createServer((req, res) => { // Create a local server
// 指定编码对于 JSON 来说有些多余 => 因为 JSON 的默认编码是 UTF-8
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ data: 'Hello World!' }));
});
server.listen(8000);
- request.setHeader(name, value) => 为标头对象设置单个标头值
const http = require('node:http');
const server = http.createServer((req, res) => {
const body = 'hello world';
res.setHeader("Content-Length", body.length);
res.setHeader("Content-Type", "text/plain");
res.setHeader("Set-Cookie", "type=ninja");
res.statusCode = 200; // response.statusCode => 使用隐式标头时将发送给客户端的状态代码
res.statusMessage = 'ok'
res.end(body);
});
server.listen(8000);
- response.writeHead(statusCode[, statusMessage][, headers]) => 向请求发送响应标头
const http = require('node:http');
const server = http.createServer((req, res) => {
const body = 'hello world';
res.writeHead(200, {
'Content-Length': Buffer.byteLength(body),
'Content-Type': 'text/plain'
})
.end(body);
});
server.listen(8000);
- url.parse(urlString[, parseQueryString[, slashesDenoteHost]]) => 解析 URL 字符串 -> 路径参数是字符串类型,需要解析、分隔转成对象
var http = require('http');
var url = require('url');
http.createServer(function (req, res) {
var urlObj = url.parse(req.url, true);
var queryObj = urlObj.query;
res.writeHead(200, {'Content-Type': 'text/html'});
res.write(queryObj.name);
res.end();
}).listen(8080);
-
处理 chunk 的 server
const http = require("http");
const port = 8089;
http
.createServer((req, res) => {
let data = "";
req.on("data", (chunk) => {
data += chunk;
});
req.on("end", function () {
let method = req.method,
headers = JSON.stringify(req.headers),
httpVersion = req.httpVersion,
requestUrl = req.url;
res.writeHead(200, { "content-type": "text/plain;charset=utf-8" });
let responseData =
method + "-" + headers + "-" + httpVersion + "-" + requestUrl;
res.end(responseData);
});
})
.listen(port, () => {
console.log(`server is running at http://127.0.0.1:${port}`);
});
- 原生 Node 托管静态资源 => index.html
const fs = require("fs");
const http = require("http");
const path = require("path");
const server = http.createServer();
server.on("request", (req, res) => {
let fpath = "";
if (req.url === "/") {
fpath = path.join(__dirname, "./clock/index.html");
} else {
fpath = path.join(__dirname, "/clock", req.url);
}
fs.readFile(fpath, "utf8", (err, dataStr) => {
if (err) return res.end("404 Not found");
res.end(dataStr);
});
});
server.listen(80, () => {
console.log("server running at http://127.0.0.1");
});
Stream
A stream is an abstract interface for working with streaming data in Node.js. The node:stream module provides an API for implementing the stream interface.
There are many stream objects provided by Node.js. For instance, a request to an HTTP server and process.stdout are both stream instances.
Streams can be readable, writable, or both. All streams are instances of EventEmitter.
通常开发中不会直接使用偏底层的流模块,而是选择其的二次封装。http 中的 res 和 req 流对象。文件转化成流对象 => fs 模块的 createReadStream 和 createWriteStream;zlib 和 crypto 模块是对转化流的应用。
stram 主要用于 IO 操作 -> 网络请求、文件处理,处理端到端数据交换
Node 中处理数据的传统模式是缓冲模式,即程序将所需处理得资源从磁盘全部加载入内存缓冲区,待所有数据全部加载后,再进行后续处理。在流模式下,程序只要加载到数据就会立即进行处理,资源将会被切块传递给调用方。后者占用内存更小,且调用端可能快得到相应。
Node 中的流模块主要用作向其他模块提供流接口的 API。流在结构上可分为可读流 Writable、可写流 Readabale、(可读可写)双工流 Duplex 和转换流 Transform。
Readable 实现 readable、resume、error、data、end、close 事件;Writable 实现 close、finish、drain、error 事件;Duplex 派生 transform 和 passThrough 流。
流之间的交互上实现了 pipe 管道
/* Readable 可读流 */
const { Readable } = require("stream");
new Readable({
highWaterMark: 16 * 1024, // 可读流缓冲区最大容量
encoding: null,
objectMode: false, // JS 对象解析为可读流 => 一般情况下流对象只处理字符串和 buffer
read: function () {}, // 将数据推送至缓冲区 => 内部调用
});
class myReadableStream extends Readable {
constructor(options, data) {
super(options); // options 用于初始化父类的构造函数
this.data = data;
}
_read() {
// 往缓冲区推数据 => 此私有方法不可 static
this.push(this.data);
this.push(null); // null => 信号 -> 可读流结束
}
}
const r = new myReadableStream(
{ encoding: "utf-8" },
"Readable Stream Test Ok"
);
// r.on("readable", () => { console.log(r.read()); }); // 此 read 是向缓冲区读取数据
// r.on("readable", () => { // 非流动模式 => 调用 read() => 自由度高
// let chunk;
// while ((chunk = r.read(1))) {
// console.log(chunk);
// }
// });
r.on("data", (chunk) => { console.log(chunk)}); // 流动模式
r.on("end", () => { console.log("Read Data End")}); // end 事件 => 监听可读流完全消费后执行回调
r.on("error", () => { console.log("Read Data Error")}); // error 事件 => 捕获可读流出现的错误
/* Writable 可写流 */
const { Writable } = require("stream");
const fs = require("fs");
const writableTestObj = {
path: "./writableTestObj.txt",
content: "writableTestObj",
};
class myWritableStream extends Writable {
constructor(options) {
super({ ...options, objectMode: true });
}
_write(chunk, encoding, cb) {
fs.writeFile(chunk.path, chunk.content, { encoding }, (err) => {
cb();
});
}
}
const w = new myWritableStream();
w.on("finish", () => {
console.log("DONE");
});
w.on("error", () => {
console.log(err);
});
w.write(writableTestObj);
w.end();
/**
* 前端资源保存服务器
* http module => req 可读流、res 可写流
*/
const http = require("http");
const fs = require("fs");
const server = http.createServer((req, res) => {
const w = fs.createWriteStream("./w.jpeg");
req.on("data", (chunk) => { // 可写流 end 事件不能写在 data 事件中 => 文件会多次触发 data 事件
w.write(chunk);
});
req.on("end", () => {
w.end();
res.end("SAVE SUCCESS");
});
w.on("error", (err) => {
console.log(err);
res.end("SAVE FAILED");
});
});
server.listen(3000);
pipe 是 Node 为 Stream 实现的接口,连接可读流与可写流 => 可读流.pipe(可写流),可读流数据自动进入可写流。数据流动由 pipe 管理,无需手动调用 read 和 write 方法。
/**
* 前端资源保存服务器
* http module => req 可读流、res 可写流
*/
const http = require("http");
const fs = require("fs");
const { pipeline } = require("stream");
const server = http.createServer((req, res) => {
const w = fs.createWriteStream("./w.jpeg");
pipeline(req, w, (err) => {
console.log(err);
res.end("SAVE FAILED");
});
res.end("SAVE SUCCESS");
});
server.listen(3000);
转换流内部分别实现可写缓冲区和可读缓冲区,可写缓冲区对应外部的可读流,通过 wirte 方法转换流,将可读流数据写入内部的可写缓冲区;转换流的可写缓冲区对应外部的可写流,通过监听 data 事件,可写流可以获取到可读缓冲区的内容。在转换流内部通过私有的 _transform
方法传递数据。转换流的核心就是 _transform
方法的实现。在这个函数中可对数据进行操作,并将转换后的数据通过 push 方法推送到可读缓冲区。
/* 文件拷贝且字母大写 */
const {Transform, pipeline} = require("stream");
const fs = require("fs");
class myStream extends Transform{
constructor(options){
super(options)
}
_transform(chunk, encoding,cb){
this.push(chunk.toString().toUpperCase());
cb();
}
}
const r = fs.createReadStream("./r.txt");
const w = fs.createWriteStream("./w.txt");
const t = new myStream();
// r.pipe(t).pipe(w)
pipeline(r, t, w, (err) => {console.log(err);})
转换流除 transform 方法还有 flush 方法,其会在整个数据流结束之前被调用。这就提供了接口,可以在数据流的尾部向可写流推送额外数据。
/* 过滤数据 => 将符合条件的数据写入指定文件且计算通过率 */
const { Transform, pipeline, Readable } = require("stream");
const fs = require("fs");
const testArr = [
{ name: "A", id: 1 },
{ name: "B", id: 2 },
{ name: "C", id: 3 },
];
class myStream extends Transform {
constructor(options) {
super({ ...options, objectMode: true });
this.counter = 0;
this.total = 0;
}
_transform(chunk, encoding, cb) {
this.total += chunk.length; // 遍历数组的长度
for (let item of chunk) {
if (item.id > 1) {
this.push(JSON.stringify(item));
this.push("\n");
this.counter++;
}
}
cb();
}
_flush(cb) {
this.push("---by ok---");
this.push("\n");
this.push(`---${this.counter}/${this.total}---`);
cb();
}
}
const r = new Readable({
objectMode: true,
read() {
this.push(testArr);
this.push(null);
},
});
const t = new myStream();
const w = fs.createWriteStream("./w.txt");
pipeline(r, t, w, (err) => {
console.log(err);
});
Buffer
缓冲区类似一个整数数组,其元素为十六进制的两位数。在计算机中的二进制都会以十六进制显示,所以尽管存储的是二进制,显示的却还是十六进制。
Buffer 创建内存中空间的元素范围是 00 - ff,所以实际上一个元素就表示内存中的一个字节。
00-ff => 0-255 => 00000000-11111111 => 8bit => 1byte
Buffer 的内存不是通过 JS 分配,而是在底层通过 C++ 申请的,是对应 V8 堆内存之外的一块原始内存。
缓冲区用于操作二进制数据。后端通常需要读取和操作长文本,处理前端传递的图片和大文件,而这些对象都是二进制的。前端对二进制数据的操作需求不多,可以选择字符串来进行处理,但字符串是不可变的,所有对字符串的操作都会生成新字符串,而这种结果在耗时的同时还会占用内存。缓冲区的二进制数据可以像操作数组般的直接操作数据源。
- Buffer.alloc(size[, fill[, encoding]]) => 返回 Buffer 实例
缓冲区的大小一旦确定则不能修改,实际上是对内存的直接操作,在内存中分配出连续的长度作为确定的空间。与数组不同,但内存空间不连续会导致性能较差。
const buf1 = Buffer.alloc(10); // 创建一个长度为 10、且用 0 填充的 Buffer
const buf2 = Buffer.alloc(10, 1); // 创建一个长度为 10、且用 0x1 填充的 Buffer
- Buffer.from(?) => 返回 Buffer 实例
Buffer.from(?) 和 Buffer.alloc(size) 都可新建 Buffer 实例,前者是静态初始化,后者是动态初始化。
console.log(Buffer.from('Hello ok').length) // 占用内存大小;一个英文占1字节 - 8
console.log(Buffer.from('Hello 紫').length) // 占用内存大小;一个汉字占3字节 - 9
const buf4 = Buffer.from([1, 2, 3]); // 创建包含 [0x1, 0x2, 0x3] 的 Buffer
console.log(Buffer.from(Buffer.from('Hello zs')), Buffer.compare(Buffer.from('Hello zs'), Buffer.from(Buffer.from('Hello zs')))) // <Buffer 48 65 6c 6c 6f 20 7a 73> 0 => 拷贝传入的 Buffer 实例数据
const buf5 = Buffer.from('tést'); // 创建包含 UTF-8 字节 [0x74, 0xc3, 0xa9, 0x73, 0x74] 的 Buffer
const buf6 = Buffer.from('tést', 'latin1'); // 创建包含 Latin-1 字节 [0x74, 0xe9, 0x73, 0x74] 的 Buffer
动态初始化 => 数组的定义和分配空间赋值的操作分开进行;静态初始化 => 数组在初始化时显式指定每个数组元素的初始值。
- Buffer.allocUnsafe(size) => 返回 Buffer 实例
Buffer.allocUnsafe(size) 返回一个指定大小的 Buffer 实例,此方法比调用既分配空间又清空数据的 Buffer.alloc() 更快。但是 Buffer 实例不会被初始化则可能包含敏感的旧数据,需要使用 fill() 或 write() 重写。
const buf3 = Buffer.allocUnsafe(10); // 创建一个长度为 10 且未初始化的 Buffer
Buffer.allocUnsafeSlow(size) 适用情况 => 不确定的时刻从池中保留一小块内存
-
buf.write(string[, offset[, length]][, encoding]) => 将字符串写入 buf
-
buf.toString([encoding[, start[, end]]]) => 将 Buffer 实例解码成字符串
Events
所有流都是 EventEmitter 的实例。EventEmitter 主要作用是在 Node 中提供一种函数调用的模式,即观察者模式,给事件注册一个或多个监听器。EventEmitter 核心 API => on 注册监听器、once 注册一次性监听器、emit 触发事件,同步调用监听器、removeListener 移除某事件监听器。
const {EventEmitter} = require("events");
const ee = new EventEmitter();
ee.on("eventTest",()=>{console.log("ok");});
ee.on("eventTest",()=>{console.log("okk");});
ee.prependListener("eventTest",function yes(){console.log("yes");}) // prependListener => 监听器插入数组开头
ee.emit("eventTest"); // 同步调用 => 监听器注册顺序就是执行顺序 => 要求在触发前注册好监听器
console.log(ee.listeners("eventTest"));
const {EventEmitter} = require("events");
const ee = new EventEmitter();
process.nextTick(()=>{ // nextTick 执行会延迟到同步代码之后
ee.emit("eventTest")
})
ee.on("eventTest",()=>{console.log("ok");});
const { EventEmitter } = require("events");
const fs = require("fs");
function mySearch(param) {
const ee = new EventEmitter();
const r = fs.createReadStream("./r.txt", { encoding: "utf-8" });
r.on("data", (chunk) => {
if (chunk.match(new RegExp(param))) {
ee.emit("found");
} else {
ee.emit("unfound");
}
}).on("error", () => {
ee.emit("error", error);
});
return ee;
}
module.exports = mySearch;
const ms = require("./utils");
ms("okk").on("found",()=>{console.log("found");}).on("unfound",()=>{console.log("unfound");}).on("error",(err)=>{console.log(err);})
Crypto
The crypto module provides cryptographic functionality that includes a set of wrappers for OpenSSL's hash, HMAC, cipher, decipher, sign, and verify funcs.
message digest algorithm 消息摘要算法或 hash function 散列函数是将任意长度的输入数据映射到固定长度的输出的过程。输出通常被称为散列值、散列码或消息摘要。
- crypto.createHash(algorithm[, options]) => 创建并返回 Hash 对象
该 Hash 对象可用于以给定算法生成哈希摘要。可选选项参数控制流行为。
algorithm 取决于平台上 OpenSSL 版本所支持的算法。
$ openssl list-message-digest-algorithms
$ openssl list -digest-algorithms
$ openssl version
- hash.update(data[, inputEncoding]) => 用给定的数据更新哈希
当 data 传入的是字符串,且未设置输入编码 inputEncoding 时,默认使用 utf-8。
当 data 传入的是 Buffer、TypedArray 或 DataView 时,inputEncoding 可忽略。
update 方法可以多次调用,以更新摄取流数据,例如来自文件读取流的缓冲区。
- hash.digest([encoding]) => 计算数据的摘要
encoding 可以是 hex、latin1 或 base64。v6.4.0 中将 latin1 作为 binary 的别名。
提供 encoding 会返回字符串,否则返回 Buffer 实例。
调用完 digest 方法后的 Hash 对象不可再使用,否则会抛出异常。
const crypto = require('crypto');
const md5 = crypto.createHash('md5');
const message = 'hello crypto';
const digest = md5.update(message, 'utf8').digest('hex');
console.log(digest, digest.length); // 2384190895f6fa3de5b7c458532c8d75 32
HMAC 有时扩展为 keyed-hash message authentication code 密钥散列消息认证码,或 hash-based message authentication code 散列消息认证码,是一种通过特别计算方式之后产生的消息认证码 MAC。使用密码散列函数,同时结合一个加密密钥。可以用来保证资料的完整性,同时可以用来作某个消息的身份验证。
- crypto.createHmac(algorithm, key[, options]) => 返回特定的 Hmac 对象
创建并返回使用给定算法和密钥的 Hmac 对象。可选选项参数控制流行为。
const crypto = require("crypto");
const hmac = crypto.createHmac("sha256", "secret key");
const res = hmac.update("hello hmac").digest("hex");
console.log(res);
key 用于生成加密的 HMAC 哈希。若 key 是 KeyObject,其 type 必须为 secret。
// hmac.js
const { createReadStream } = require("fs");
const { createHmac } = require("crypto");
const { argv } = require("process");
const filename = argv[2];
const hmac = createHmac("sha256", "hello hmac");
console.log(hmac);
const input = createReadStream(filename);
input.on("readable", () => {
const data = input.read();
if (data) hmac.update(data);
else {
console.log(`${hmac.digest("hex")} ${filename}`);
}
});
$ node "/Users/.../hmac.js" hmac.js
Hmac {
_options: undefined,
[Symbol(kHandle)]: Hmac {},
[Symbol(kState)]: { [Symbol(kFinalized)]: false }
}
961f862e87d5ef0bf2b4d0d826157473484b20caa7dca746e2764624dee9b00d hmac.js
- crypto.randomBytes(size[, callback]) => 生成加密的强伪随机数据
size 为生成的字节数,不得大于 2 ** 31 - 1。
回调函数未提供时,同步生成随机字节并以缓冲区返回;回调函数提供时,会被异步调用生成字节。该回调函数包含 err 和 buf 两个参数。
err 为错误发生时的 Error 对象,buf 为生成字节的缓冲区。
const { randomBytes } = require('crypto');
randomBytes(256, (err, buf) => {
if (err) throw err;
console.log(`${buf.length} bytes of random data: ${buf.toString('hex')} => Async`);
});
const buf = randomBytes(256);
console.log(`${buf.length} bytes of random data: ${buf.toString('hex')} => Sync`);
- Public-key cryptography & Symmetric-key algorithm
在明文和密文之间通过指定算法互相转换时,可通过引入密钥来增强安全性。根据加密和解密时所用的秘钥是否相同,可以将加密算法分为对称加密与非对称加密。
公开密钥加密也称为非对称式加密,其公钥用作加密,私钥则用作解密。公钥可任意向外公开或发布;私钥不可以公开,必须严格保管。RSA、ElGamal、DSA...
对称密钥加密要求在加密和解密时使用相同的密钥。对称加密的速度比公钥加密快很多。AES、DES、Blowfish、IDEA...
- crypto.createCipheriv(algorithm, key, iv[, options]) => 返回 Cipher 对象
使用给定的算法、密钥和初始化向量创建并返回一个 Cipher 对象。
The key is the raw key used by the algorithm and iv is an initialization vector. Both arguments must be 'utf8' encoded strings, Buffers, TypedArray, or DataViews. The key may optionally be a KeyObject of type secret. If the cipher does not need an initialization vector, iv may be null.
初始化向量应该是不可预测且唯一的,在理想情况下甚至是加密随机的。初始化向量不必严格保管,通常会添加到未加密的明文。
AES 的区块长度固定为 128 比特,密钥长度则可以是 128,192 或 256 比特。
- crypto.createDecipheriv(algorithm, key, iv[, options])
通过指定条件创建并返回一个 Decipher 对象。可选参数 options 控制流的行为。
- cipher.update(data[, inputEncoding][, outputEncoding])
通过 data 更新 Cipher 对象。inputEncoding 指定时,data 是字符串类型。未提供 inputEncoding 时,data 必须时 Buffer、TypedArray 或者 DataView。若 data 是 Buffer、TypedArray 或者 DataView,输入编码格式可忽略。
输出编码格式指定时返回字符串;未提供输出编码格式则会返回一个缓冲区。
update 方法可在 final 方法前调用多次并传入新的 data。final 方法调用后再执行 update 会抛出异常。
const crypto = require("crypto");
const key = crypto.randomBytes(192 / 8);
const iv = crypto.randomBytes(128 / 8);
const algorithm = "aes192";
function encrypt(text) {
const cipher = crypto.createCipheriv(algorithm, key, iv);
cipher.update(text);
return cipher.final("hex");
}
function decrypt(encrypted) {
const decipher = crypto.createDecipheriv(algorithm, key, iv);
decipher.update(encrypted, "hex");
return decipher.final("utf8");
}
const content = "hello";
const crypted = encrypt(content);
const decrypted = decrypt(crypted);
console.log(crypted, decrypted);
- cipher.final([outputEncoding])
返回 Cipher 对象的值。指定 outputEncoding 时返回字符串,否则返回缓冲区。
final 方法被调用后,Cipher 对象不再用作加密数据,重复调用多次 final 方法会抛出异常。
- crypto.scryptSync(password, salt, keylen[, options])
返回缓冲区。提供同步的 scrypt 实现。scrypt 是一个基于密码的密钥派生函数。
In cryptography, a salt is random data that is used as an additional input to a one-way function that hashes data, a password or passphrase. Salts are used to safeguard passwords in storage. wiki
尽可能的选择具有唯一性的 salt,推荐 salt 是至少 16 bytes 的随机数。
keylen 表示密钥的长度,必须是数字。
const { scryptSync } = require("crypto");
const key1 = scryptSync("hello password", "hello salt", 64);
const key2 = scryptSync("hello password", "hello salt", 64, { N: 256 });
console.log(key1.toString("hex"), key2.toString("hex"));
Process
- process.argv => 返回命令行启动 Node 进程时所传递的参数数组
数组元素依次为 process.execPath,正在执行的文件路径和附加的命令行参数。
const { argv } = require('process');
argv.forEach((val, index) => {
console.log(`${index}: ${val}`);
});
process.argv0 属性存储 Node 启动时传递 argv[0] 的原始值的只读副本。
process.execPath.split("/").pop() == process.argv0
Configuration Template
Routing and Forwarding
在使用 Vue 的 History 路由模式时,前端路由会负责处理所有路由的匹配和渲染。当用户输入一个地址或点击链接进行页面跳转时,如果后端服务器没有对应的资源,就会将请求转发到前端入口页面(通常是 index.html
)。前端路由会根据用户输入的地址进行匹配,并根据匹配结果渲染相应的组件。如果没有找到匹配的路由,就会渲染 404 页面,从而实现前端路由处理 404 错误的功能。此外,也可以使用 connect-history-api-fallback 中间件来实现相同的功能。
const express = require('express');
const path = require('path');
const app = express();
const port = 3000;
// 静态文件服务
app.use(express.static(path.join(__dirname, 'public')));
// 所有路由请求返回Vue应用的入口页面
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// 启动服务器
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
Framework
Koa2
Express 内置捆绑许多函数,而 Koa 轻量到只剩基本功能,当需要的时候再通过适合的中间件 Middleware 来搭配组合。可通过 koa-generator 脚手架快速搭建。
- Hello World
$ npm install --save koa
引入 Koa 模块并创建 Koa 实例 app。Web 请求会经过 app.use()
函数的处理。
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => { ctx.body = 'Hello World'; });
app.listen(3000);
- Koa 洋葱模型 onion model
await next() divides each middleware into pre-operation, other middleware operations and post-operation.
代码运行到 next()
会暂停当前,执行后续中间件。next()
返回 Promise 的实例,使用 await 是欲以同步的方式等待 Promise 实例的执行完成。嵌套的 Promise 就像洋葱的模型,直到 await 返回最内层 Promise 的 resolve 值。
Promise.resolve(middleware1(context, async() => {
return Promise.resolve(middleware2(context, async() => {
return Promise.resolve(middleware3(context, async() => {
return Promise.resolve();
}));
}));
}))
.then(() => { console.log('end'); });
const Koa = require('koa');
let app = new Koa();
const middleware1 = async (ctx, next) => { console.log(1); await next(); console.log(6); }
const middleware2 = async (ctx, next) => { console.log(2); await next(); console.log(5); }
const middleware3 = async (ctx, next) => { console.log(3); await next(); console.log(4); }
app.use(middleware1).use(middleware2).use(middleware3);
app.use(async(ctx, next) => { ctx.body = 'hello world' })
app.listen(3000)
// Output 1, 2, 3, 4, 5, 6
const Koa = require('koa');
let app = new Koa();
const p = function(args) {
return new Promise(resolve => { setTimeout(() => { console.log(args); resolve(); }, 100); });
};
const middleware1 = async (ctx, next) => {
await p(1);
// await next();
next();
console.log(6);
};
const middleware2 = async (ctx, next) => {
await p(2);
// await next();
next();
console.log(5);
};
const middleware3 = async (ctx, next) => {
await p(3);
// await next();
next();
console.log(4);
};
app.use(middleware1).use(middleware2).use(middleware3);
app.use(async(ctx, next) => { ctx.body = 'hello world' })
app.listen(3000)
// Output: 1, 6, 2, 5, 3, 4
koajs/compose 模块实现中间件的执行。dispatch(0)
的执行表示首个中间件函数 fn 取 middleware[0]。middleware 数组维护的是通过 app.use()
压入的中间件。中间件执行时的传参分别是上下文和 next。
next()
执行即是调用 dispatch(i)
函数,所以遇见函数 next 的逻辑是执行下一个中间件。
- Middleware => koa-router
$ npm install @koa/router
const Koa = require('koa');
const Router = require('@koa/router');
const app = new Koa();
const router = new Router();
router.get('/', async (ctx, next) => {
console.log("@koa/router get request => ",`${ctx.method} ${ctx.url}`)
ctx.body = 'Hello World';
await next();
});
app.use(router.routes()).use(router.allowedMethods());
app.listen(3000);
- Middleware => koa-bodyparser
Koa 是先经过业务路由,再处理中间件;而 Express 是先经过中间件,如果中间件验证不通过就不会处理业务。app.use(bodyParser());
放在路由处理之前。
$ npm install koa-bodyparser
- Middleware => @koa/cors
@koa/cors 根据简单请求、预检请求分别对 CORS 头进行不同的处理。
$ npm install @koa/cors --save
// 结合 @koa/cors 与 koa-bodyParser
var Koa = require('koa');
const cors = require('koa-cors');
var bodyParser = require('koa-bodyparser'); // 获取post请求的参数
var app = new Koa();
app.use(bodyParser()).use(cors());
app.use(async ctx => {
// the parsed body will store in ctx.request.body
// if nothing was parsed, body will be an empty object {}
ctx.body = ctx.request.body;
});
app.listen(3000);
- Middleware => koa-static & koa-mount
$ npm install koa-static
$ npm install koa-mount
const Koa = require("koa");
const path = require("path");
const server = require("koa-static"); // 搭建静态服务
const mount = require("koa-mount"); // 指定静态服务的请求前缀
let app = new Koa();
const staticPath = path.resolve(__dirname, "static");
const staticServer = server(staticPath, {
setHeaders: (res, path, stats) => {
if (path.indexOf(/[jpg|png|gif|jpeg]/) !== -1) {
console.log(stats);
res.setHeader("Cache-Control", ["private", "max-age=60"]);
res.setHeader("Test-Static-Stats-Birthtime", stats.birthtime); // 自定义响应头
}
},
});
app.use(mount("/supdir", staticServer));
app.listen(3000);
Third Party Modules
project development
analysis test
- esprima => 对 JavaScript 做词法或语法分析的工具 -> online site
结束
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议,转载请注明出处!