这是我参加[第五届青训营]伴学笔记创作活动的第7天
本堂课重点知识
- 了解Node.js的应用场景
- 了解Node.js运行时结构
- 了解如何编写Http Server以及延伸话题
应用场景
1.前端工程化(现状:nodejs在这方面是难以替代)
- 打包工具:webpack、vite、esbuild(由go实现)、parcel(由Rust实现)、prisma
- 压缩工具:uglifyjs
- 语法转换:bablejs、typescript()
2.web服务端应用(现状:语言竞争激烈,Node.js有自己独特的优势)
- 学习曲线平缓,开发效率较高
- 运行效率接近常见的编译语言
- 社区生态丰富及工具链成熟(npm,V8 inspector)
- 与前端结合的场景会有优势(SSR)
3.Electron跨端桌面应用(现状:大部分场景在选型时,都值得考虑)
- 商业应用:vscode,slack,discord,zoom
- 大型公司内部桌面的效率工具
4.Node.js在字节中的应用
- BFF应用、SSR应用,举例:Modern.js--接口拼接和裁剪
- 服务端应用,举例:头条搜索,西瓜视频,懂车帝
- Electron应用:飞连,飞书
- 每年新增1000+Node.js应用
Node.js运行时结构
V8:JavaScript Runtime,诊断调试工具(inspector)
libuv:eventloop(事件循环),syscall(系统调用)
举例:用node-fetch发起请求时
首先用
npm安装node-fetch模块后会到用户代码,然后在代码中调用node-fetch模块,因为这些代码为JavaScript代码,因此会找到V8去执行,因为node-fetch是含http模块,所以调用的是Node.js Core(JavaScript)模块,然后这个模块再调用底层的Node.js Core(C++)的API,那么就可能调用到llhttp协助完成http的序列化和反序列化工作,然后将得到的数据通过libuv创建TCP连接发送给远端,同理,远端传送过来的数据通过libuv的事件循环得到消息获取数据,然后再给llhttp解析,再将数据给Node.js Core(JavaScript)中的js代码,最后传给用户代码就会收到整个数据。
特点
- 异步I/O
- 单线程
- 跨平台
nodejs12开始可以使用worker_thread
异步I/O
当Nodejs执行I/O操作时,会在响应返回后恢复操作,而不是阻塞线程并占用额外内存等待(异步调用,可先处理其他调用,异步处理请求返回数据后执行回调)
单线程
- JS单线程
- 实际:JS线程+uv线程池+V8任务线程池+V8 Inspector线程
- 优点:不用考虑多线程状态同步问题,也就不需要锁;同时还能比较高效地利用系统资源;
- 缺点:阻塞会产生更多负面影响
- 解决办法:多进程或多线程
跨平台
- 跨平台(大部分功能、api需要该平台特有的或底层的来提升性能)
- Node.js跨平台+JS无需编译环境(+Web跨平台+诊断工具跨平台)=开发成本低(大部分场景无需担心跨平台问题),整体学习成本低
编写Http Server
0.安装Node.js
考虑多个nodejs版本问题,采用nvm进行管理
mac和linux
1.编写Http Server+Client,收发GET, POST请求
Http Server
1.Hello World
测试 在终端文件对应目录输入node http_server.js,等"listening on: 3000"出现后打开浏览器输入localhost:3000后页面显示“hello”。
2.返回JSON数据
const http = require('http')
const port =3000
//回调函数
const server = http.createServer((req, res) => {
const bufs = []
req.on('data',(buf)=>{
//获取数据后将数据存储在bufs中
bufs.push(buf)
})
//当end事件触发后表明数据传输完成
req.on('end',()=>{
//连接每一段bufs
const buf = Buffer.concat(bufs).toString('utf8')
console.log("1"+buf)
let msg = 'Hello'
console.log("2"+msg)
try {
const ret = JSON.parse(buf)
console.log("3"+ret)
msg = ret.msg
console.log("4"+msg)
} catch (err) {
// res.end('invalid json')
}
const responseJson = {
msg:`receive: ${msg}`
}
res.setHeader('Content-Type','application/json')
res.end(JSON.stringify(responseJson))
})
})
server.listen(port, ()=>{
console.log('listening on:',port)
})
测试 在终端文件对应目录输入node http_server_json.js,"listening on: 3000"出现后打开浏览器输入localhost:3000后页面显示{"msg":"receive: Hello"}。
注意 responseJson中的msg一定要用“`”这个引号包含起来,否则${msg}接收不到。
Http Client
发送一个post请求
const http = require('http')
//创建body
const body = JSON.stringify({
msg: 'Hello from my own client',
})
//创建请求
const req=http.request('http://127.0.0.1:3000',{
method: 'POST',
headers:{
'Content-Type':'applocation/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)
})
})
//发送body
req.end(body)
测试 在服务端3000端口还在运行的时候,新建终端将文件对应目录输入node http_client.js,在终端上显示json.msg is receive: Hello from my own client。
Promisify
用Promise+async await重写这个例子 原因:回调函数虽然在很多场景可以直接用,但不太好维护和管理,无法确定触发回调函数的时机,而有些需要在回调函数中实现就会写的深,并且函数之间的关联难以通过代码察觉
技巧 将callback转换成promise
并非所有回调函数都能改成promise,比如http_server_json,但可以改成http.createServer(async(req, res)...)
修改后
const http = require('http')
const port =3000
//回调函数
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中
bufs.push(buf)
})
req.on("error",(err)=>{
reject(err)
})
//当end事件触发后表明数据传输完成
req.on('end',()=>{
//连接每一段bufs
const buf = Buffer.concat(bufs).toString('utf8')
let msg = 'Hello'
try {
const ret = JSON.parse(buf)
msg = ret.msg
} catch (err) {
// res.end('invalid json')
}
resolve(msg)
})
})
//response
const responseJson = {
msg:`receive: ${msg}`
}
res.setHeader('Content-Type','application/json')
res.end(JSON.stringify(responseJson))
})
server.listen(port, ()=>{
console.log('listening on:',port)
})
2.编写静态文件服务器
首先在同一目录下创建一个static的文件夹并且添加一个index.html的文件
测试 用node启动后会发现直接输是无法访问,输入localhost:3000/index.html,才能从static文件中访问到index页面。
与高性能、可靠的服务相比,还差什么?
1.CDN:缓存+加速
2.分布式储存,容灾
外部服务:cloudflare,七牛云,阿里云,火山云...
3.编写ReactSSR服务以及渲染原理
SSR (server side rendering)有什么特点?
- 相比传统HTML模版引擎:避免重复编写代码
- 相比SPA(single page application):首屏渲染更快,SEO 友好
- 缺点:
- 通常qps较低,前端代码编写时需要考虑服务端渲染情况
- 通常qps较低,前端代码编写时需要考虑服务端渲染情况
const React =require('react')
const ReactDOMServer = require('react-dom/server')
const http = require('http')
function App(props){
// return <div>hello</div>
return React.createElement('div',{},props.children || 'Hello')
}
const server = http.createServer((req, res) => {
res.end(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Application</title>
</head>
<body>
${ReactDOMServer.renderToString(React.createElement(App,{},'my_content'))}
<script>
//不会渲染,此处可以初始化react应用
</script>
</body>
</html>
`)
})
server.listen(port, ()=>{
console.log('listening on:',port)
})
SSR难点:
1.需要处理打包代码
tmp.js
require('./static/style.css')
2.需要思考前端代码在服务端运行时的逻辑
async componentDidMount(){
const res = await fetch('http://my.server.domain')
3.移除对服务端无意义的副作用,或重置环境
4.适用inspector进行调试、诊断
V8 Inspector:开箱即用、特性丰富强大、与前端开发一致、跨平台
- node --inspect 文件名
- open http://localhost:9229/json
场景:
- 查看console.log内容
- breakpoint
- 高CPU、死循环:cpuprofile
- 高内存占用:heapsnapshot
- 性能分析
5.部署简介
- 部署要解决的问题
- 守护进程:当进程退出时,重新拉起
- 多进程:cluster便捷地利用多进程
- 记录进程状态,用于诊断
- 容器环境
- 通常有健康检查的手段,只需考虑多核cpu利用率即可
延伸话题
贡献代码
快速了解Node.js代码 Node.js Core贡献入门
- 好处:
- 从使用者的角色逐步理解底层细节,可以解决更复杂的问题;
- 自我证明,有助于职业发展;
- 解决社区问题,促进社区发展;
- 难点:
- 花时间
编译
为什么要学习编译Node.js
- 认知:黑盒到白盒,发生问题时能有迹可循
- 贡献代码的第一步
如何编译
- 参考:Maintaining the build files
- ./configure && make install
- 演示:给net模块添加自定义属性
诊断与追踪
- 诊断是一个低频、重要同时也相当有挑战的方向。是企业衡量自己能否依赖一门语言的重要参考。
- 技术咨询行业中的热门角色。
- 难点:
- 需要了解Node.js底层,需要了解操作系统以及各种工具
- 需要经验
WASM,NAPI
- Node.js (因为V8)是执行WASM代码的天然容器,和浏览器器WASM是同一运行时,同时 Node.js支持 WASI。
- NAPI执行C接口的代码(C/C++/Rust...),同时能保留原生代码的性能。
- 不同编程语言间通信的一种方案。
课后
- 了解并尝试使用更多 Node.js 的原生模块 nodejs.org/dist/latest…
- 学习在 npm 上搜索并安装模块 www.npmjs.com/