NodeJS
约 1032 个字 403 行代码 预计阅读时间 8 分钟
在 NodeJS 里,你搞不了 DOM/BOM,但可以使劲嚯嚯本地文件。
模块化:你不是单文件战神
CommonJS 规范
- CommonJS 将每个 JS 文件视为单独的模块
- 默认情况下,外界无法访问模块内的属性和方法、必须进行暴露
- 不导出的 方法/属性 都是 私有的 => 无论命名格式是不是
_funcName()
。
// @ file_1.js
const name = 'file1';
const printName = () => {
console.log(name);
}
console.log('@file1'); // 导入时执行
// 暴露方法 1
module.exports = {
name, // 不进行重命名
say: printName
}
// 暴露方法 2
exports.say = printName
好的,现在我们在另一个文件里进行引入
以及 JS 并不会出现循环引用的问题:假如存在
C->B->A
的依赖关系,同时在C & B
中引入module A
并不会报错。反正哪里要用就在哪里引用一下(多次引用不会多次触发)。
ES 规范
NodeJS 默认支持的是 CommonJS,如果要用 ES 的话得配置一下:
在模块暴露和引入部分就很传统浏览器了:
// @ module A
const name = 'mod A';
const moduleA = {
sayMyName(){ console.log(name); }
}
export default moduleA; // 这边导出的是一个整体
// @ module B
const moduleB = {
sayMyName() { console.log('mod B'); }
}
export {
moduleB // 导出指定接口
}
// @ index
import moduleA from './moduleA.js';
import {moduleB} from './moduleB.js'; // 需要解构
moduleA.sayMyName();
moduleB.sayMyName();
包管理:我补药造轮子口牙
NPM
总之列一些常用的命令吧:
npm init # 初始化当前目录,生成 package.json
# 以下命令中的 install 均可替换为 uninstall, update
npm install [packageName]@[version] # 安装指定版本
npm install [packageName] -g # 全局安装
npm install [packageName] --save # 固定依赖版本,生成 package-lock.json
npm install [packageName] --save-dev # 开发环境依赖,不参与打包
npm list # 列举当前目录下的包
npm list -g # 列举全局环境下的包
npm info [packageName] # 获取指定包的详细信息
npm info [packageName] version # 获取指定包的最新版本
npm outdated # 列举过时依赖
下面是 package.json
中版本号的一些说明:
// @ package.json
{
"dependencies": {
"md5": "2.1.0", // 安装 2.1.0 版本
"md5": "^2.1.0", // 安装 2.*.* 中的最新版本
"md5": "~2.1.0", // 安装 2.1.* 中的最新版本
"md5": "*", // 安装全局最新版本
}
}
换源:
- 我们必定能够手动换源,belike:
- 我们也可以通过 NPM 镜像管理工具(NRM)进行管理,并在多个 NPM 源之间进行灵活切换
npm install -g nrm # 全局安装 NRM
nrm ls # 查看可选源头,带 * 的是当前使用源
nrm use [repoName] # 切换到指定源
nrm test # 测试响应时间
- 其实也可以使用 CNPM
Yarn
没想到吧,这东西还是得通过 NPM 安装
相比于 NPM,我们 Yarn さん:
- 速度超快:Yarn 缓存了所有下载过的包(无需重复下载),且支持并行下载(NPM 只能串行)
- 更安全:Yarn 会校验包的完整性
好的,下面又是一些常见命令:
# 初始化项目(好熟悉)
yarn init
# 安装全部依赖
yarn install
# 添加依赖
yarn add [packageName]
yarn add [packageName]@[version] # 会生成 yarn.lock 锁定版本
yarn add [packageName] --dev
# 升级依赖
yarn upgrade [packageName]@[version]
# 移除依赖
yarn remove [packageName]
内置模块:朕的后端框架何在
HTTP 模块
// 引入内置依赖
const http = require('http');
// 请求的 URL 是否合法
function renderStatus(url) {
// 处理的相当 naiive => 没办法处理带参数的
const valid_url = ['/home', '/list'];
return valid_url.includes(url)?200:404;
}
// 创建本地服务器
const server = http.createServer((req, res) => {
// req: 浏览器传入的参数,res: 服务端返回的数据
// req.url 给的是不带 域名&端口号 的后缀
if(req.url == '/favicon.ico') {return;} // 不给图标
res.writeHead(
renderStatus(req.url),
{'Content-Type': 'application/json;charset=utf-8',}
// 不加 UTF-8 可能导致中文乱码
// 当传输由 `` 包裹的 HTML 内容时,需要标注 contentType = text/html
);
// 最后必须有一个 res.end(),否则会报超时
res.end(JSON.stringify({
data: 'Hello World!'
}))
// 你也可以直接用 res.end(params) 来写普通数据(字符串,数组...)
});
server.listen(
8080, // 监听 8080 端口
()=>{ console.log("server on")} // 跑起来之后马上调用
);
啊其实有个等效写法(说来 createServer()
里的函数是每次请求就会回调的)
const server = createServer(); // 这里啥也不用配
server.on('request', (req, res) => {
// 每次收到 request 时执行的回调
})
server.listen(8080); // 开始监听
跨域
JSONP
JSON with Padding(JSONP)是一种用于解决浏览器同源限制的跨域数据请求技术,通过动态创建 <script>
标签以加载外部资源、并通过回调函数处理返回的数据。
其一般流程长下面这样:
- 客户端发送请求:客户端需要首先定义一个回调函数,并通过
<script>
标签的 src 属性向后端请求数据(并附上指定的回调函数名称)
<script>
// 定义回调函数
function handleResponse(data) { console.log(data); }
</script>
<!-- 允许执行通过 src 加载的跨域 JS 文件 -->
<script src="https://server.com:442/api?cb=handleResponse"></script>
- 服务器响应:返回一个以指定数据为回调函数实参的 JS 文件
- 客户端处理:加载并执行返回的 JS 文件
这个 Step 2 用 NodeJS 写就是下面这个感觉:
var JSON_data = {name: 'John Doe'};
server.on('request', (req, res) => {
const url = URL.parse(req.url, true);
// 前端的请求是 /api?cb=handleResponse
switch(url.pathname) {
case '/api':
res.end(
// 第一个 ${} 支持前端自行执行 callback 函数名
`${url.query.cb}(${JSON.stringify(JSON_data)})`
);
break;
}
});
// 返回的是 handleResponse({"name": "John Doe"});
CORS
// 总之要写一下包头
server.on('request', (req, res) => {
res.writeHead(200, {
'Content-Type': 'applications/json;charset=utf-8',
'access-control-allow-origin': '*', // CORS 部分:允许所有域通过
})
res.end(...); // 写点数据
});
中间件:反正后端之间不跨域
如果 API 分别布在不同服务器上,前端就得一个个配置跨域(累的要死)。我们可以通过 Node 服务器向其他服务器发送 GET/POST 请求(这样就只用在当前的 Node 中间件上配置一次跨域了)。
const https = require('https');
httpGET('https://api_url', (data) => {
res.end(data); // 把数据塞回前端给的请求里
})
function httpGET(API, callback) {
var data = "";
// 如果对面的 API 是 http 协议的话,可以直接使用 http.get() 进行请求
// 如果对面用的 https 的话,就要引入 https 模块了
https.get(API, (res) => {
res.on('data', (chunk) => {
// 这边返回的是数据流,需要自己用 buffer 装一下
data += chunk;
})
res.on('end', () => {
// 所有数据传输完毕
callback(data);
})
})
}
// 模拟前端填完表单找后端要数据 => 所以是 POST 哇
function httpPOST(API, callback) {
var data = "";
var options = { // 这个是 post 接口要求的
hostname: 'server.com',
port: 443, // 用的 HTTPS
path: '/api/getcustomer',
method: 'POST',
headers: { // 模拟表单
'Content-Type': 'application/json'
/* 也可能是这个格式: 'x-www-form-urlencoded'
对应下面的 write 内容: 'id=1&age=100' */
}
}
// 离谱的是不叫 POST,叫 request
var req = https.request(options, (res) => {
res.on('data', (chunk) => { data += chunk; })
res.on('end', () => { callback(data);})
});
// 这边是为了 req 能真正发出去(必须有 end )
req.write( // 把要求的表单数据搓进包里
JSON.stringify([{},{'baseParam': {'ypClient':1}}])
);
req.end();
}
爬虫
解析 HTML,提取需要的数据
var cheerio = require('cheerio'); // 这是第三方模块
var html_content = httpGET(URL, (data) => spider(data));
function spider(data) { // 拿正则搞一些需要的标签
let $ = cheerio.load(data); // 这下像是 jQuery 了
let movies = [];
let $movieList = $('.col.content'); // 反正是选择器
$movieList.forEach((idx, item) => {
// 把 item 变成 jQ 对象 => 找到里面的 .title 元素 => 获取文本
movies.push({
title: $(item).find('.title').text(),
actpr: $(item).find('.actor').text()
});
})
return JSON.stringify(movies);
}
URL 模块
旧版(已弃用)
来来来,解析前端请求路由:
const url = require('url');
// req.url = /api/list?a=1&b=2
var url_obj = url.parse(req.url);
url_obj.pathname = '/api/list'; // base URL
url_obj.query = 'a=1&b=2'; // GET 参数
// 如果需要进一步解析 query 参数的话,需要设置第二个实参为 true
var url_obj = url.parse(req.url, true);
url_obj.query = { a: '1', b: '2' }; // 好用的 JSON !
那么后端要怎么把 URL 拼回去呢(但我觉得挺鸡肋的):
const urlObjcet = {
protocol: 'https',
slashes: true,
auth: null,
host: 'www.host.com:443',
port: 443,
hostname: 'www.host.com',
hash: '#tag=110',
search: '?a=1',
query: { a: '1' },
pathname: '/ad/index.html',
path: '/ad/index.html?a=1'
}
const parsedURL = url.format(urlObject);
// res = https://www.host.com:443/ad/index.html?a=1
地址拼接(更抽象了):
url.resolve('/one/two/', 'three'); // /one/two/three(末尾追加)
url.resolve('one/two/', 'three'); // one/three (替换首个斜杠后所有)
// 替换首个斜杠后所有
url.resolve('http://www.sample/com/', '/two');
url.resolve('http://www.sample/com/one', '/two');
// 上面俩都输出 http://www.sample/com/two
新版
// 创建 URL 对象,req.url = /api?a=1
const URL_obj = new URL(req.url, 'https://server.com:port');
// Parse
URL_obj.pathname = '/api';
// 是迭代器(类似于 Py 里的 map)
URL_obj.searchParams = URLSearchParams { 'a' => '1' };
for (let [key, val] of URL_obj.searchParams) {} // ... 做一些遍历操作
URL_obj.searchParams.get('a'); // 获取指定属性值
// Format
url.format(URL_obj, {
unicode: true, // 中文是否用 unicode 编码
auth: false, // 去掉 server:port 部分
fragment:false, // 是否保留 # 后的内容
search: true, // 是否保留 ? 后的内容
})
// Resolve
URL_obj.href = "https://server.com:port/api?a=1"; // 拼接结果
QueryString
query 字符串 x 结构体之间的互相转换(不过是在切字符串罢了)
MD,这和 JSON 模块有什么区别吗?
const querystring = require('querystring');
var query_obj = { x: 3, y: 4 };
querystring.stringify = 'x=3&y=4';
var query_str = 'name=aaa&age=15';
querystring.parse(query_str) = { name: 'aaa', age: '15' s};
// 转义字符(防注入):总之斜杠、中文什么的都被创飞了
escaped = querystring.escape(query_str);
original = quertstring.unescape(escaped); // 还原一下
Events 模块
我们可以把项目解耦成“订阅-发布”模式
const EventsEmitter = require('events');
const event = new EventsEmitter();
var eventName = 'play';
event.on(eventName, (data) => {
// do sth
})
// 上面的回调会在每次 event.emit(eventName, data) 时被触发
看起来没啥大用不是吗?但是这东西支持异步处理诶
/* 每当收到前端请求,我们新建一个 event
不然每次触发分支都会 新建 一个对 ok 的监听(类似于 timer 叠加)*/
var event = new EventsEmitter();
// 开始监听 'ok' 事件
event.on('ok', data=>res.end(data)); // 给前端发回数据
httpGET(API); // 这边的 end() 需要调用 event.emit('ok', data)
FS 文件操作
因为底层是用 C++ 写的,所以可以嗯造本地文件
目录处理,喜闻乐见了:
const fs = require('fs');
// 经典新建目录(不阻塞)
// mkdirSync() 方法会阻塞,需要加 try-catch 处理
fs.mkdir('./avatar', (err) => {
// 如果成功的话会是 null
if (err) { console.log(err); }
})
// 同理,我们有熟悉的删除目录(不能删非空目录)
fs.rmdir('./avatar', (err) => {...})
// 删除文件(不阻塞),可以用 unlinkSync()
fs.unlink('path/to/file', (err) => {...})
// 罗列指定目录下的文件(返回 文件名 arr)
fs.readdir('targetDir', (err, data) => {...})
然后我们可以搓出一个递归删除目录的函(我自己搓的,不保证对):
// forEach 方法不会阻塞,我们得想一个异步调用的办法
function rrmdir(baseDir) {
fs.promise.readdir(baseDir).then(async (data) => {
await Promise.all(data.map(item => {
let cur = path.join(baseDir, item);
fs.stat(cur, (err, res) => {
if (res.isFile()) {
fs.unlink(cur, err=>{...});
} else {
rrmdir(cur);
}
})
}));
await fs.rmdir(baseDir);
})
}
文件处理:
// 经典重命名(不知道能不能当 mv 用)
fs.rename('./oldName', './neoNAme', (err) => {...})
// 写文件(会覆盖哦)
fs.writeFile('path/to/file', content, (err) => {...})
// 追加(不覆盖)
fs.appendFile('path/to/file', content, (err) => {...})
// 读文件(异步)
fs.readFile('path/to/file', (err, data) => {
// 因为这边默认读出来的是二进制
if (!err) { console.log(data.toString('utf-8')) }
})
// 也可以提前指定用 utf-8 格式读取
fs.readFile('path/to/file', 'utf-8', (err, data) => {...})
// 我们也可以实现 Promise 风格的异步读取
fs.promise.readFile('path/to/file', 'utf-8').then(res=>{...})
Stream 输入输出流
因为文件大的时候不方便直接异步嘛,所以我们再来看看流式输入输出模块 Stream
var rs = fs.createReadStream('targetFile', 'utf-8');
rs.on('data', (chunk) => {})
rs.on('end', () => console.log('finished'))
rs.on('error', (err) => {...})
var ws = fs.createWriteStream('targetFile', 'utf-8');
ws.write(part1);
ws.write(part2);
ws.end();
但是在复制较大文件时,读写两边的流速可能对不齐(让我们用 pipe 连接一下)
// copy f1 -> f2
var rs = fs.createReadStream('f1', 'utf-8');
var ws = fs.createWriteStream('f2', 'utf-8');
// 用管道连一下
rs.pipe(ws); // 搞完他会自己把流关掉的
Zlib 压缩与解压缩
const zlib = require('zlib');
const gzip = zlib.createGzip();
// 其实 readStream 也可以串到服务器返回上
server.on('request', (req, res) => {
const rs = fs.createReadStream('targetFile', 'utf-8');
res.writeHead(200, {
'Content-Type': 'pliantext',
'Content-Encoding': 'gzip' // 不然前端没法正常解压缩
});
rs.pipe(gzip).pipe(res); // 先压缩,再传送
});
Crypto 加密好啊
用 C++ 实现了通用的哈希和加密算法,总之效率大提升。
首先来看一些用于签名的方法:
const crypto = require('crypto');
// 把参数改成 sha1 就可以换另一种了
const MD5 = crypto.createHash('md5');
MD5.update(plain_text); // 默认 utf-8,也可以用 buffer
console.log(MD5.digest('hex')); // 以十六进制显示
// 还支持 Hmac,但需要提供密钥(有种加盐的美感)
const HMAC = crypto.createHmac('sha256', secret_key);
// 下面也是 update + digest
Crypto 也支持对称加密算法(AES),总之加解密用一样的密钥
const MOD = 'aes-128-cbc';
function encrypt(key, iv, data) {
// iv 是初始向量(问题不大)
// 用的 AES 128,所以 len(key) = 8
let dep = crypto.createCipheriv(MOD, key, iv);
return dep.update(data, 'binary', 'hex') + dep.final('hex');
}
function decrypt(key, iv, encrypted) {
crypted = Buffer.from(crypted, 'hex').toString('binary');
let dep = crypto.createDecipheriv(MOD, key, iv);
return dep.update(crypted, 'binary', 'utf8') + dep.infal('utf8');
}