Node.js
模块化
CommonJS
node实现commonjs的模块化规范
require(path)
导入 path是文件夹路径时,会找其中package.json的main
属性指向的文件作为入口,否则会找index.js注意: require的参数不能是纯变量,可以是字符开头拼接变量
module.exports/exports.xx
导出整个对象或属性
通常ESmodule可以将CommonJS作为默认导入,反之不行。因为CommonJS是同步加载(运行时),而ESmodule要先进行异步静态分析(编译时)
内置模块
path
- linux、mac等系统路径以
/
分隔,而windows有的以\
分割,直接写死发布到linux等系统下就会有问题。以下方法用于进行路径拼接path.join
纯粹拼接,不处理path.resolve
拼接前会将相对路径解析为对应绝对路径再拼接。若后面的参数为绝对路径,会忽略之前的参数;相对路径会进行解析拼接。
fs
fs.open
获取文件描述符。通过描述符也可以用内置模块操作对应文件读取写入文件时,可以传配置项
flag
设置读写方式。读取文件时若不传配置项encoding
设置编码格式,默认会返回buffer
fs.readdir
读取文件夹目录,配置区分文件类型后才可通过isDirectory/isFile
方法判断目录还是文件写入
JSON
格式文件时,所有内容挤在一行很丑,可以在序列化时:1
2//JSON.stringify第三个参数可添加缩进规则
JSON.stringify(curConfig, null, '\t')
events
事件触发器,vue等框架的事件总线思想由此而来
events
是一个类,下面称为EventEmitter
,实例称为emitter
emitter.on/addListener
监听自定义方法emitter.once
监听自定义方法,只监听一次emitter.emit
触发自定义方法emitter.off
取消监听EventEmitter.eventNames
获取当前监听的事件名EventEmitter.listeners
获取当前事件名对应的事件个数EventEmitter.removeAllListeners
移除所有事件
http
监听127.0.0.1本地回环地址时,只能捕获127.0.0.1和localhost,不会捕获向本地网络ip的请求。而监听0.0.0.0(默认)监听ipv4所以地址并根据端口找到所有应用程序,可以捕获127.0.0.1、localhost、本地ip三种。
请求参数req(http监听请求的第一个参数):
原生node中,req没有body属性。框架内部是通过监听req的data事件获取请求的body。
req.setEncoding()
可以先预设请求编码。参数:utf-8 - 字符串,binary - 二进制(文件)等。默认情况下req的data事件返回buffer请求头headers参数:
connection: keep-alive
在不同web服务会默认有不同的保持连接时间(除非主动断开)。node服务默认是5saccept-encoding
告知服务器客户端文件的压缩格式
res.satatusCode
设置响应状态码res.writeHead(statusCode: number, headers: obj)
设置响应头的同时也可设置状态码res.setHeader(key, val)
设置某一个header项
http模块也可以发送请求。在游览器中axios通过xhr封装请求,在node中axios通过http模块封装。
http模块发送请求的回调参数res不直接返回响应结果,需要监听data事件:
1
res.on('data', (data) => {...})
http.request
方法发送请求时,返回一个clientRequest实例req,需要调用req.end()
表示请求设置完毕才能发出。http.get
则不需要
url
url.parse
解析get请求
querystring
querystring.parse
解析get请求参数
promise封装
内置模块方法都可使用promise
的方式使用了,有两种用法:
模块.promises.fn
调用promise
封装的内置模块方法,require("模块/promises")
,该内置模块所有异步方法都会是promise
方式了
可能与回调方式返回的数据结构不同,需要查看文档对应类
- 基于 promise 的 API 不使用数字文件描述符。 而是,基于 promise 的 API 使用
FileHandle
类
用fileHandle
写入文件时出现了operation not permitted, write错误,用路径却成功。猜测因为此 fs.open
调用时未设置可写入的flag
参数 ,于是设置为"a+"
成功。得出结论: fs.open
需要设置flag确保可进行读写操作
还要注意,fileHandle
写入后再用fileHandle
读取不到内容。猜测由于文件被写入状态更新,导致第一次open
获取的fileHandle
失效, 于是重新fs.open
获取,再用新获取的fileHandle
尝试成功。得出结论: 写入等更新文件的操作后需要重新获取fileHandle
才能继续操作
全局类
Buffer
本质上是存放8位2进制的数组,因此可通过索引操作其中一项。默认会被解析位16进制。
字母、数字占1字节,普通中文(‘utf8’占3字节、’utf16le‘占2字节)
字符串转buffer:
1
2//new Buffer(str)已不推荐
const buffer = new Buffer.from(str)数组转buffer
1
const buffer = Buffer.concat(arr)
buffer转字符串
1
2//toString默认'utf8'编码。编码解码格式必须一致
const str = buffer.toString()Buffer.alloc(size, fill, encoding)
创建一个预期的buffer- size为buffer预期的长度,使用最多
- fill填充每一项的默认值,默认是0
- encoding每一项字符串编码,默认’ust8’
fs.readFile不设置编码格式则默认拿到文件的buffer
创建buffer时不会频繁申请内存,默认会先申请8kb内存,填充的数据大于4kb时再申请新的内存
npm
package.json属性
scripts
运行脚本,可使用npm run key
执行对应命令行指令。有几个特定的key
执行时不需要加run
:start
、restart
、test
、stop
engines
指定node和npm版本,不符合会报错。还可指定操作系统os
(很少用)browserslist
指定webpack打包后js游览器兼容情况,否则需要使用polyfills
指令
npm i --production
只安装package.json中生产环境依赖
版本规范
npm包遵从semver规范x-y-z
:
x
主版本号。不兼容的api修改y
次版本号。向下兼容的功能性新增z
修订号。向下兼容的问题修正- 先行版本号和版本编译元数据可以追到到上面三个之后
版本号前缀^
、~
^
主版本号不变,其他安装最新版~
主次版本号不变,修订号安装最新版
包上传
npm login
登录必须是npm的源,不能是淘宝镜像源,设置镜像后登录时需要切换回去:1
2npm config set registry https://registry.npmjs.org/
npm cache clean --forcepackage.json => keywords中的关键字,可以从npm搜索到这个包
npm publish
npm unpublish packageName@version --force
删除已发布的包
自实现脚手架工具
目录划分
1 | │ .gitignore |
初始化
- 使用shebang/hashbang。在入口js文件顶部加上一行
#!/usr/bin/env node
,也可以写node的绝对路径,但不同系统有兼容问题。执行文件时会在当前用户环境找node执行。 - package.json的bin属性对象添加
"key"(指令名): "入口js路径"
- 然后
npm link
将配置的指令名放入环境变量,才可全局使用此指令执行入口文件。npm发布后进行全局安装效果相同。 - 使用commander管理指令:
npm i commander
。后面统称导入的commander为prgprg.version(版本号, 指令)
参数都是字符串。版本号可以从package.json中动态获取,相对路径必须要加./
;多个指令用,
分隔,且超过两个后面的无效,设置指令后默认-V --version
会失效。prg.parse(process.argv)
通过指令解析执行上面定义的相关进程
修改文件后需要npm unlink
删除node目录下node_modules和cmd相关文件重新npm link
npm link时,由于用cnpm安装会多出许多包,并且cnpm包命名的规则可能导致link过程报错,并且node_modules出现模块丢失。
出现这个问题可以用npm安装; 或者用npm安装一次后保留package-lock.json文件(锁住版本),再用cnpm即可
commander指令
prg.options("-选项1 --选项2... <必须参数名> [可选参数名]", 描述)
设置指令选项。就可通过prg.选项2
获取到参数,新版(7.2.0)通过program.opts()
获取参数。还可通过prg.on('option:选项2', callback(参数))
监听指令。只能监听到一个参数创建指令
1
2
3
4
5
6//...表示有多个相关参数
program.command("create <单个参数名> [可选参数名...]")
.description("指令描述")
.action((arg1, arg2) => {
...
})
下载模板
使用download-git-repo从git上下载项目模板
1 | //path需要查看官方指定的格式: |
node的util模块可以导入{ promisify }
,将回调方式的方法包装成为promise
方式
- 从github拉取:
download(github:owner/project-name#branch, projectName)
- 从gitlab拉取:
download(direct:gitlab地址/owner/project-name.git#branch, projectName)
options中clone: true会拉取模板git信息,但由于权限问题非公开账号很容易failed 128;使用clone: true方式网址最后最好加上后缀
.git
。
加载动画
使用三方库ora
1 | const ora = require("ora") |
命令行指令
使用node子进程模块child_process
的spawn
或exec
方法在子进程执行npm命令安装依赖,并运行项目。
1 | const cp = spawn(cmd指令, [arg1, arg2...], {cwd: "需要cd进的目录"}) |
项目运行成功后可以使用三方插件open打开游览器,但无法灵活操作跟选择端口;建议在项目模板webpack的配置运行时自动打开游览器
命令行选择
三方库inqurer实现命令行选择
创建组件和页面
计划使用ejs动态生成页面和组件文件,指令如下:
addcpn cpnName
添加组件addpage pageName
添加页面组件和路由
需要四个ejs模板: 组件文件、router、store、types。在需要动态变化的地方使用<%= data.xx %>
ejs.renderFile(path, dataObj, options, callback)
根据ejs模板渲染对应的文件,在回调中通过fs写入文件。写入前要通过fs.exists
判断路径是否存在(path.dirname
逐级找到父级目录判断),否则需要mkdir
创建。组件默认在components目录下创建,可以增加
-d
参数选项设置存放路径添加页面时生成页面组件文件和对应的路由配置,模板项目中在对路由进行了工程化处理,汇总所有页面下的router.js生成路由配置:
1
2
3
4
5
6
7//'@/view'为项目存放页面的目录
const files = require.context('@/views', true, /router\.js$/)
const routes = files.keys().map(key => {
//注意require内可以解析代码,但不能放变量
const page = require('@/views' + key.replace('.', ''))
return page.default
})
阻塞和非阻塞
阻塞、非阻塞 => 对于被调用者
同步、异步 => 对应调用者
非阻塞IO时,libuv线程池负责进行轮询和其他方式,等待结果后将回调放入队列由事件循环接管。而非主线程轮询大大消耗性能
Stream
流,读写文件时,二进制数据(字节)源源不断被读写到程序中,这一连串字节数据就是流。
可以控制文件比较细节的操作,如读取位置、大小。而非普通的fs.readFile这样直接一次性读取完
所有流都是
EventEmitter
的实例
node有四种基本流类型:
- Writable: 可写入数据的流(
fs.createWriteStream
) - Readable: 可读取数据的流(
fs.createReadStream
) - Duplex: 可写入又可读取的流(
net.Socket
) - Transform: 写入和读取时可修改/转换数据的流(
zlib.createDeflate()
)
流方法返回流对象(reader、writer……),可以细化操作对应的文件数据。并且可以用on
方法监听事件:
open: 开始传输
data: 监听获取每次传输的数据
close: 传输结束
写入时必须手动调用end或close,不会自动关闭
- reader.pipe(writer) 可将读取流中的数据直接放到写入流写入。windows系统若写入失败,创建writer时flage设置为
r+
即可
文件上传
原生node实现文件上传处理
需要设置
req.setEncoding("binary")
,否则默认通过utf8对文件数据编码导致错误。要去除数据首尾多余信息,将剩余二进制数据写入文件。去除信息如下:
头部通过
Content-Type
分割截取去除尾部boundary要先通过
res.headers['content-type']
截取到值,并在二进制数据中去除(在二进制数据尾部,boundary首尾会多出--
)。头部也有boundary,但已经在上一步截去了不用管。
boundary后面可能还有信息需要除去,这时boundary末尾没有
--
头部的空格
\r\n
会导致文件错误,用正则\s
匹配去除1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47const http = require("http");
const fs = require("fs");
const qs = require("querystring")
http.createServer((req, res) => {
if (req.method === "POST" && req.url === "/upload") {
let data = "";
req.setEncoding("binary");
req.on("data", (fd) => {
data += fd;
})
const boundary = req.headers["content-type"].split("; ")[1].replace("boundary=", "")
req.on("end", () => {
const valCT = qs.parse(data, "\r\n", ": ")["Content-Type"];
//获取文件名和后缀
const beforeCT = data.split(valCT)[0];
const preName = qs.parse(beforeCT, "; ", "=")["filename"].split("\"")[1];
const suffix = preName.split(".")[1];
//头部的\r\n不能保留,用\s匹配去掉
const afterCT = data.split(valCT)[1].replace(/^\s\s*/, "");
const boundaryIndex = afterCT.indexOf(`--${boundary}`);
//去除文件尾部空格
const file = afterCT.substring(0, boundaryIndex).replace(/\s\s*$/, "");;
const afterFile = afterCT.substring(boundaryIndex)
//解析formdata文件外其他参数
const params = qs.parse(afterFile, "form-data; name=", "\r\n\r\n")
for(let key in params) {
const val = params[key];
const subIndex = val.indexOf("\r\n");
//去除key中的 " 和value中其他字符
params[key.replace(/\"/g, "")] = val.substring(0, subIndex);
delete params[key];
}
let setName = preName;
//参数params中有setName可以设置保存的文件名
params.setName && (setName = `${params.setName}.${suffix || "png"}`)
fs.writeFile(`./${setName}`, file, "binary", err => {
if (err) return;
res.end("上传成功")
})
})
}
}).listen("8866", () => {
console.log("server start...");
})
其他
whistle
基于node的抓包工具, 官方文档
安装 SwitchyOmega设置代理到whistle的主机,默认127.0.0.1,端口8899
手机与PC同一wifi下并设置代理: 电脑的ip地址与8899端口
w2 start
启动whistle后,新建rules:1
接口地址 127.0.0.1
w2 run
调试模式开启,可以看到插件console默认只能拦截http,拦截https请求,需要进行如下操作,参考 whistle——抓包https请求的解决办法 :
- 添加rules时使用:
接口地址 filter://intercept
- 点击最上方工具栏的https,取消 Capture TUNNEL CONNECTs 勾选后下载安装证书到PC(需要添加到受信任的机构)。然后再勾选Capture TUNNEL CONNECTs,下载到手机上安装
- 添加rules时使用:
旧版支持ESM
旧版的node并不支持esmodule,需要进行以下操作:
- 在package.json中增加
type: 'mode'
- 13.2.x之前还需要在执行node命令时加上
--experimental-modules
express
express核心是中间件,中间件的本质就是回调函数
express-generator是exporess脚手架: express [projectName]
创建项目
基本使用
中间件的回调函数有三个参数: req、res、next。回调中可以:
- 修改req和res
- 完成请求响应
- next()调用栈中的下一个中间件
如果当前中间件中没有完成响应请求(如
res.end()
),则必须执行next
,否则请求将被挂起有两种应用中间件的方式
app/router.use()
app/router.use(callback)
可以匹配任何情况
app/router.use(path, callback)
只匹配路径,不关心方法app/router.method()
method比如get
、post
。app/router.method(path, callback)
同时匹配路径和对应方法。app/router.method(path, callback1, callback2...)
注册多个中间件,但callback1后面的中间件要执行也必须next()
同时调用
res.end
和next
并不会报错。res.end
只是结束响应周期,但还是可以next
执行下一个中间件;只是后面的中间件内不能再res.end
调用响应
中间件获取body
- 根据content-type判断是否要处理请求体, 监听req的data事件获取拼接请求体。不处理则直接
next()
- 在req的end事件中解析完整的请求体并挂载到
req.body
并next()
- 数据应该挂载到
req
上,传入next
会被当做错误信息直接返回给客户端错误页面
1 | const createApp = require("express"); |
body-parser解析请求体的方法类似,并提供多种content-type的处理,更加完善。这里我只处理了自己最常用的urlencoded
工作中更多使用body-parser, 之前需要npm i body-parser
;Express 4.16.0已经内置了body-parser。路由上方添加两行代码即可:
1 | //解析json |
multer
可用于处理formData类型的数据,一般用于文件上传。前面我们用原生node实现,过程比较繁琐,开发当然用multer。
multer
本身是一个函数,返回的实例下面称作upload。该函数可传入以下参数:dest
或storage
dest:string
可指定存放文件的目录。storage:obj
可以对文件存放做更多控制,storage
可配置如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16let upload = multer({
//multer.diskStorage指定磁盘存放
storage: multer.diskStorage({
//指定存放目录
destination: function (req, file, cb) {
//cb第一个参数有值代表错误
cb(null, './uploads/');
},
//指定文件名(若未指定filename,每个文件将设置为一个随机文件名,并且没有扩展名)
filename: function (req, file, cb) {
//originalname中有扩展名
var changedName = Date.now() + '-' + file.originalname;
cb(null, changedName);
}
})
})fileFilter:fn
文件过滤器,控制哪些文件可以被接受limits:obj
: 限制上传的数据preservePath
: 保存包含文件名的完整文件路径
multer实例upload有如下方法可返回中间件回调:
single(fieldname)
单文件上传。 只接受以fieldname
为name的数据并且只能传一个文件。文件的信息保存在req.file
,文本保存在req.body
array(fieldname[, maxCount])
多文件上传。接受一个以fieldname
为name的文件数组。可以配置maxCount
来限制上传的最大数量。这些文件的信息保存在req.files
,文本保存在req.body
fields(fields)
只接收满足fieldsname
和数量的文件。接受指定fields
的混合文件。这些文件的信息保存在req.files
。fields
应该是一个对象数组,应该具有name
和可选的maxCount
属性。none()
只接受文本域。如果任何文件上传到这个模式,将发生 “LIMIT_UNEXPECTED_FILE” 错误。这和upload.fields([])
的效果一样any()
接收任何方式(文本和文件)。文件保存在req.files
,文本保存在req.body
避免app.use全局方式注册multer的中间件,这样恶意用户可以上传文件到一个你没有预料到的路由。最好只在处理文件上传的路由的中间件前注册
morgan
三方中间件,记录请求日志
需要注册morgan(type, opts)
作为中间件,参数如下:
type
: 设置日志格式"combined"
打印到文件中(一般是log文件)"dev"打印到控制台
opts
: 配置对象参数:stream
设置写入文件流如:fs.createWriteStream(path, opts)
,打印到文件中才需要此参数
开始发现日志的时间和实际的时间不对应,可能是时区问题,可以通过morgan.token
和morgan.format
自定义时区和格式
1 | // 自定义token |
url传参
除了上述通过手动解析、body-parser、multer等方式可以用req.body
获取请求体中的参数外,还可以直接在路由上获取参数:
params
类似vue-router中的动态路由,node中通过url/:id
预设params参数,客户端请求时url为url/1
,就可以拿到req.params.id
为1,而且参数不传会报错query
客户端在url末尾?
后所传的参数(url?id=1&name=qq
),req.query
接收。express已经解析为对象
响应数据
res.status()
设置响应状态码res.type
() 设置响应的header。res.end()
不能传二进制之外的数据,传统方式要通过res.type("application/json")
设置json格式的content-type,再res.end(json字符串)
res.json(obj | array)
直接返回content-type为json格式的数据,不用手动设置type和转换字符串,可直接返回数组、对象
路由
express.Router
创建路由实例router
,拥有完整的中间件和路由管理系统,专门处理路由相关逻辑,不必全部冗余在app.js中。
使用router
注册各路由相关中间件,再导出到app.js, 通过app.use(router)
统一注册到app中, 也可以app.use(path, router)
以path开始的路由才会匹配对应router
静态资源
中间件express.static(root)
可指定root作为静态资源根路径开启静态资源服务,从服务根路径即可访问对应root下的静态资源
错误处理
next
传递参数会跳到后面注册的拥有**四个参数(err, req, res, next)**的中间件,err
就是next
传来的参数没有错误的情况下,不应该向
next
传参next
不传参是不会到有四个参数的中间件的
根据上述特性,可以在最后注册四个参数的中间件集中处理错误信息。错误时只需向next传递错误信息
koa
koa导出的是一个类,可通过new
创建app。
中间件
koa中,中间件只有app.use()
注册方式,并且与express相比:
- 不能传
path
, - 一个
app.use
不可以传多个中间件。
中间件有两个参数:
ctx
上下文对象,其中有request
和response
ctx.response
没有end
方法,只需要将响应数据放到context.response.body
中。当所有中间件进行完毕,会将body的内容响应给客户端。
dispatch
类似于next
express和koa中间件在同步情况下都是洋葱圈模型,next后的逻辑在后面中间件执行后进行
但在异步情况下,express没有办法在之前的中间件调用next之后,获取后面中间件拿到的异步数据。
而koa中使用koa-compose整合中间件调用时,
dispatch
函数对中间件进行了promise等处理,异步情况下可以使用await、generator等方案(所有涉及的中间件都要处理)待后续next拿到数据再进行。在异步情况下也能保持洋葱圈模型。
1 | router.post("/test", async(ctx, next) => { |
路由
社区三方koa-router是使用最多的koa路由管理工具
koa-router导入一个构造函数,通过
new
创建router。构造函数可以传配置:prefix:string
定义路由前缀
支持通过
router.method(path, cb1, cb2...)
方法注册路由(post、get、put……)- 第一个参数
path
。如果构造函数传入了prefix会以prefix开头。完全匹配prefix则传"/"
- 后面参数传中间件。与express一样,可以传入多个
- 第一个参数
最后通过
app.use(router.routes())
添加到app还可通过
app.use(router.allowedMethods())
将路径可匹配但方法匹配不到的请求返回状态码跟Method Not Allowed,而不全是Not Found在koa中,普通
app.use
不能传path
,也就无法自动获取路径中的params
参数。使用koa-router后经过封装,可以用ctx.request.params
获取
请求参数
请求body数据(urlencoded、json)通过koa-bodyparser解析:
app.use(bodyParser())
。通过ctx.request.body
获取formData类型数据使用koa-multer处理,用法与express中的multer相似。安装时有两个依赖可以选择:
koa-multer 是在express的multer封装了一层,因此数据结果不在
ctx.request.body
,而在ctx.req.body
@koa/multer,需要先安装multer。结果用
ctx.request.body
获取,推荐这种
代理上下文
在koa中,将response
和request
对象的一些属性通过delegate
代理到上下文ctx
中更加便于操作,在koa源码context.js中:
1 | /** |
因此,通常我们可以类似下面方式: (还有其他的代理内容见上文源码)
ctx.body
相当于ctx.response.body
设置响应数据。注意与
ctx.request.body
请求体区分开ctx.status
相对于ctx.response.status
设置响应状态码ctx.query
相当于ctx.request.query
router路由中,也可用
ctx.params
代替ctx.request.params
multer中,也可用
ctx.files
代替ctx.request.files
等都是经过代理封装
静态资源
koa-static指定静态资源服务,与express.static
用法相似:
1 | const staticAssets = require("koa-static") |
错误处理
路由中抛出错误事件:
1
2//路由中可由ctx.app拿到app
ctx.app.emit("error", new Error("错误信息"), ctx)app监听错误事件:
1
2
3
4
5
6app.on("error", (err, ctx) => {
ctx.body = {
code: 0,
message: err.message
}
})
- 本文作者: MR-QXJ
- 本文链接: https://mr-qxj.github.io/2021/07/26/语言/nodejs/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!