这是我参与「第五届青训营 」伴学笔记创作活动的第 11 天
一、本堂课重点内容:
- Node.js的应用场景(why)
- Node.js运行时结构(what)
- 编写Http Server(how)
- 延伸话题
二、详细知识点介绍:
1. Node.js的应用场景
- 前端工程化
Nodejs的出现加速了这些前端工程化的工具的出现。
用于打包的工具:Bundle:webpack、vite、ebuild、parcel
代码压缩工具:Uglify:uglifyjs
语法转换工具:Transpile:bablejs,typescript
现状:尽管有其他语言的加入,Nodejs学习成本较低,地位仍处于难以替代的地位。
- Web服务端应用
Nodejs可以做到其他后端语言能做到的事情,且学习曲线平缓,开发效率高,运行效率接近常见的编译语言,社区生态丰富且工具链成熟(npm,VB inspector),对于与前端结合的场景会有优势(SSR)。
现状:多种语言竞争激烈,但Nodejs有自己独特的优势。
- Electron跨端桌面应用
有非常多应用都是基于Electron进行开发的,因为它基于跨端方面有较大的优势。
例如vscode、slack、discord、zoom等商业应用,还有许多大型公司内部的效率工具。
现状:大部分场景在选型时,都值得考虑。
2. Node.js运行时结构
-
Nodejs的组成部分:
acron
node-inspect:调试
npm
用户代码:业务代码和外部导入的包
Node.js Core(JavaScript):Node.js模块,JavaScript代码有时会调用C++代码进行一些功能实现。
N-API:通过JavaScript无法满足需求时,例如性能需求,就会用更Native的语言与JavaScript通信,这种时候就会用到N-API。
Node.js Core(C++):Node.js模块,作为底层模块存在。
V8:JavaScript Runtime,诊断调试工具(inspector)。
libuv:封装操作系统api,提供跨平台I/O操作,包括eventloop(事件循环),syscall(系统调用)。
nghttp2:http2相关模块
zlib:常见压缩和解压缩算法
c-ares:DNS查询库
llhttp:http协议解析
OpenSSL:网络层面的加密和解密
-
特点:
异步I/O
当Node.js执行I/O操作时,会在响应返回后恢复操作,而不是阻塞线程并占用额外内存等待。
例:example.ts
setTimeout({} => { console.log('B') }) console.log('A')单线程
JS单线程实际:JS线程 + uv线程池 + v8任务线程池 + v8 Inspector线程
优点:不用考虑多线程状态同步问题,也就不需要锁;同时还能比较高效地利用系统资源。
缺点:阻塞会产生更多负面影响,所以解决办法为多进程或多线程。
例:example.ts
function fibonacci(num: number): number { if(num === 1 || num === 2){ return 1; } return fibonacci(num - 1) + fibonacci(num - 2); } fibonacci(42) fibonacci(43)跨平台
Node.js跨平台 + JS无需编译环境(+ Web跨平台 + 诊断工具跨平台) = 开发成本低(大部分场景无需考虑跨平台问题),整体学习成本低。
const net = require('net') const socket = new net.Socket('/tmp/socket.sock')
3. 编写Http Server
- 安装Node.js
Mac、Linux推荐使用nvm进行多版本管理。
Windows推荐使用nvm4w或官方安装包。
安装慢或失败的情况可以设置安装源
-
编写Http Server + Client,收发GET,POST请求
server:每次接收到http请求,触发回调函数。
res:客户端传入数据。
req:服务端返回数据。
port:监听的端口号。用server.listen进行监听,监听到后就调用回调函数,打印listening on,server返回hello显示在客户端。
Server:
Hello world:
```
const http = require('http')
const server = http.createServer((req,res) => {
res.end('hello')
})
const port = 3000
server.listen(port, () => {
console.log('listening on',port)
})
```
json:
```
const http = require('http')
const server = http.createServer((req,res) => {
const bufs = []
req.on('data',(buf) => {
bufs.push(buf)
})
req.on('end',() => {
const buf = Buffer.concat(bufs).toString('utf8')
let msg = 'hello'
try{
const ret = JSON.parse(buf)
msg = ret.msg
} catch (err) {
//
}
const responseJson = {
msg:'recetive: ${msg}'
}
res.setHeader('Content-type, 'application/Json')
res.end(Json.stringfy(responeJson))
})
})
const port = 3000
server.listen(port, () => {
console.log('listening on',port)
})
```
Client:
```
const http = require('http')
const body = JSON.stringfy({
msg: 'Hello from my own client',
})
const req = http.request('http://127.0.0.1:3000', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
}, (res) => {
const bufs = []
res.on('data', (buf) => {
bufs.push(buf)
})
res.on('end',() => {
const buf = Buffer.concat(bufs)
const json = JSON.parse(buf)
console.log('json.msg is:', json.msg)
})
})
req.end(body)
```
可以用Promise和async await 重写这两个例子,把callback转换成promise。但只适用于只会被调用一次的回调函数。
json:
```
const http = require('http')
const server = http.createServer(async (req,res) => {
//receive body from client
const msg = await new Promise((resolve, reject) => {
const bufs = []
req.on('data',(buf) => {
bufs.push(buf)
})
req.on('error',(err) => {
reject(err)
})
req.on('end',() => {
const buf = Buffer.concat(bufs).toString('utf8')
let msg = 'hello'
try{
const ret = JSON.parse(buf)
msg = ret.msg
} catch (err) {
//
}
resolve(msg)
})
})
//respone
const responseJson = {
msg:'recetive: ${msg}'
}
res.setHeader('Content-type, 'application/Json')
res.end(Json.stringfy(responeJson))
})
const port = 3000
server.listen(port, () => {
console.log('listening on',port)
})
```
2. 编写静态文件服务器
编写一个服务,接收用户发送的http请求,处理http请求参数,也就是获取url,读取文件内容,返回给这个请求。
除了使用http模块,还要使用fs模块、path模块和url模块,path模块可以进行绝对和相对路径的转化。
例:使用stream风格api进行编写
```
const http = require('http')
const fs = require('fs')
const path = require('path')
const url = require('url')
const folderPath = path.resolve(_dirname,'./static')
const server = http.createServer((req,res) => {
// expected http://127.0.0.1:3000/index.html?abc=10
const info = url.parse(req.url)
// static/index.html
const filepath = path.resolve(folderPath,'./' + info.path)
//stream api
const filestream= fs.createReadStream(filepath)
filestream.pipe(res)
})
const port = 3000
server.listen(port, () => {
console.log('listening on',port)
})
```
访问localhost:3000/index.html即可获得index.html页面内容。
- 与高性能、可靠的服务相比,还需要:
CDN:缓存+加速
分布式储存,容灾
例如一些优秀的外部服务:cloudflare,七牛云,阿里云,火山云。
- 编写React SSR服务
- SSR特点:
相比传统HTML模板引擎:避免重复编写代码。
相比SPA:首屏渲染更快,SEO友好
缺点:通常qps较低,前端代码编写时需要考虑服务端渲染情况。
- HTML例子:
const React = require('react')
const ReactDOMServer = require('react-dom/server')
const http = require('http')
//require <div>Hello<div>
function App(props) {
return React.createElement('div',{}, props.children || 'Hello')
}
const server = http.createServer((req,res) => {
res.end('
<!DOCTYPE html>
<html>
<head>
<title>My Application</title>
</head>
<body>
${ReactDOMServer.renderToString(React.createElement(App,{}, 'my_content'))}
<script>
alert('yes')
</script>
</body>
</html>
')
})
const port = 3000
server.listen(port, () => {
console.log('listening on',port)
})
- SSR难点:
需要打包代码。
需要思考前端代码在服务器运行时的逻辑。
一处对服务端无意义的副作用,或重置环境。
- 适用inspector进行调试、诊断
V8 Inspector:开箱即用、特性丰富强大、与前端开发一致、跨平台。
场景:查看console.log内容、breakpoint、高CPU或死循环的场景(如cpuprofile)、高内存占用(如heapsnapshot)、性能分析等。
node --inspector 文件名 启动调试,打开http://localhost:9299/json 进行调试。
- 部署简介
- 部署要解决的问题:
守护进程:当进程退出时,重新拉起。
多线程:cluster便捷地利用多进程。
记录进程状态,用于诊断
- 容器环境:
通常有健康检查地手段,只需考虑多核cpu利用率即可。
4. 延伸话题
- 了解Node.js贡献代码
好处:
从使用者的角色逐步理解底层逻辑,可以解决更复杂的问题;
自我证明,有助于职业发展;
解决社区问题,促进社区发展
难点:
花时间
- WASM、NAPI
Node.js是执行WASM代码的天然容器,和浏览器WASM是同一个运行时,同时Node.js支持WASI。
NAPI执行C接口的代码(C/C++/Rust),同时能保留原生代码的性能。
不同编程语言间通信的一种方案
三、课后个人总结:
我学习到了关于Node.js的应用场景和运行时结构,并且通过实际编写Http Server,懂得了Node.js的实际运用方式,以及调试方法。