第一章:NODE.JS基础
第一节:http
利用node.js代码创建简单的http服务:
http.createServer((re, res) => {})
第二节:express
路由
- Route methods: 支持get\post\all方法
- Route paths: 路由匹配支持正则表达式。
- Route params: 使用
/:param方式传递,使用req.params获取。 - Route handle: 中间件支持两种参数传递方式:
// 多参数方式:
app.get('/example/b', function (req, res, next) {
next()
}, function (req, res) {
res.send('Hello')
})
// array方式:
var cb0 = function (req, res, next) {
next()
}
var cb1 = function (req, res) {
res.send('Hello')
}
app.get('/example/c', [cb0, cb1])
- Response methods:
download\end\json\jsonp\redirect\render\send\sendFile\sendStatus - Express.Router Vs app.route(): 前者更方便实现模块划分,
中间件
- 中间件结构:中间件函数传入
req,res,next三个参数。 - 中间件功能:1.可以执行任何代码,2.修改req和res对象3.终结请求/响应循环4.调用
next()以结束当前中间件并执行下一个中间件, - 中间件分类:1.应用级中间件2.路由级中间件3.错误处理中间件4.内置中间件5.第三方中间件
- 中间件执行顺序类似一种洋葱结构
第三节:koa
应用
- 中间件:koa当执行到
next()会暂停当前中间件,执行下一个中间件。 不同于express,koa的中间件执行支持await方式。 - 配置:支持
app.env、app.proxy、app.subdomainOffset实例属性配置。 - app.listen(): 可以同时在http和https或者在不同端口号监听同一个应用
- app.callback(): 返回
http.createServer()的回调函数。 - app.use(): 用于为应用添加中间件。
- app.context: 是
ctx的原型,可以加入其它属性。 - 错误处理:通过
app.on('error', e =>{})捕获错误。
上下文(Context、ctx)
- 解释:将Node的request和response封装在内部,并额外提供了很多方法。
- ctx.req与ctx.res:node的request 和 response
- ctx.response与ctx.request:koa的Request与koa的Response
- ctx.state: 推荐的命名空间
- ctx.throw([status], [msg], [properties]): 抛出错误
- 其它与Node同价的属性
Request与Response
koa的Request与koa的Response是ode的request和response的进一步封装。
koa的第三方中间件
- koa-router
- koa-bodyparser
- koa-views
- koa-static
- koa-session
- koa-jwt
- koa-mount
- koa-static
- ...
第四节:RPC
一、解释
- 两台服务器进行互相调用的方式,多采用TCP协议。
- 使用特有的服务进行寻址,用ID进行寻址的
- 可以使用单工通信/双工通信|半双工通信
- 使用的是二进制协议,更小的数据包体积,更快的编解码速率
二、Node Buffer
1、创建buffer
- Buffer.from() // 创建buffer
const bf1 = Buffer.from('buffer')
const bf2 = Buffer.from([1,2,3])
- Buffer.alloc() // 指定长度创造buffer
const bf3 = Buffer.alloc(20)
2、写入/读取buffer
const bf2 = Buffer.from([1,2,3])
console.log(bf2) // <Buffer 01 02 03>
bf2.writeInt8(4, 1)
console.log(bf2) // <Buffer 01 04 03>
bf2.writeInt16BE(5, 1)
console.log(bf2) // <Buffer 01 00 05>
console.log(bf2.readInt16BE(1)) // 5
三、Protobuf
- 说明:rpc调用常使用Protobuf,是性能优异、跨语言、跨平台的序列化数据结构的协议。
- 1、定义
test.proto:
message Column{
required int32 id = 1;
required string name = 2;
required float price = 3;
}
- 2、encode
const pb = require('protocol-buffers')
const fs = require('fs')
const schema = pb(fs.readFileSync('test.proto'), 'utf-8')
const buffer = schema.Column.encode({
id: 1,
name: 'NodeJs 入门到放弃',
price: '0.11'
})
- 3、decode
console.log(schema.Column.decode(buffer))
四、protobufjs
- 使用
pbjs命令转换.proto为其他格式文件;使用pbts命令转换.js为.d.ts - 如何使用
.proto转换的.js:
// step1. 编写一个`test.proto`
package thePackage;
message TestDataType {
uint32 uid = 4;
}
// step2. 转换为`test.js`:
$root.thePackage = (function() {
thePackage.TestDataType = (function() {
// ...
TestDataType.prototype.uid = 0;
TestDataType.create = function create(properties) {
// return new TestDataType(properties);
};
// encode 方法
TestDataType.encode = function encode(message, writer) {};
// decode 方法
TestDataType.decode = function decode(reader, length) {};
// ...
})
})
module.exports = $root
// step3. 调用提供encode或decode方法即可:
// 构造请求结构
const req = thePackage.TestDataType.create(params)
// encode,得到buffer
const pbBuffer = thePackage.TestDataType.encode(req).finish()
// decode,得到数据
const res = thePackage.TestDataType.decode(new Uint8Array(theBuffer))
五、Node的net模块
利用tcp进行RPC通信,以下是个单工通信的例子:
1、创建server服务
const net = require('net')
const sever = net.createServer(socket => {
socket.on('data', buffer => {
// 收到buffer,进行解析等操作
console.log(buffer)
})
})
sever.listen(4000)
2、创建client服务
const net = require('net')
const socket = new net.Socket()
socket.connect({
host:'127.0.0.1',
port: 4000
})
// 创建16位的buffer
let buffer = Buffer.alloc(2);
buffer.writeInt16BE(233333)
// 发送buffer
socket.write(buffer)
第五节:模板引擎
一、ejs
EJS可以使用普通的js生成html页面:
<script src="ejs.js"></script>
<script>
let people = ['geddy', 'neil', 'alex']
let html = ejs.render('<%= people.join(", "); %>', {people: people});
</script>
二、ES6模板字符串
使直接用ES6模板字符串实现模板引擎,无沙箱模式
const user = {
name: 'Linda'
}
const template = `<h2>${user.name}</h2>`
使用vm模块实现模板引擎,实现沙箱模式并进行字符串过滤:
const vm = require('vm')
const user = {
name: '<script>xss</script>'
}
const template = `<h2>${user.name}</h2>`
const str = vm.runInNewContext('`<h2>${_(user.name)}</h2>`',
{
user,
_: function (markup) { // xss 过滤[不完整]
if (!markup) return ''
return String(markup).replace(/&/g, '&')
}
})
console.log(str)
简单封装子模板的使用方式,使用key-value 模式存储模板。使用自定义include方法返回子模板:
const vm = require('vm')
const templateMap = {
tA: '`<h2>Out ${include("tB")}</h2>>`',
tB: '`<p>inner</p>`'
}
const ctx = {
include: function (name) {
return templateMap[name]()
}
}
Object.keys(templateMap).forEach(key => {
const tem = templateMap[key]
// 改写模板字符串为渲染方法
templateMap[key] = vm.runInNewContext(`
(function() {
return ${tem}
})
`, ctx)
})
console.log(templateMap['tA']())
第六节:GraphQL
一、直接使用GraphQL
- 说明:一个用于API的查询语言
- garphql 查询语法:
第一个参数是一个
schema。第二个参数是查询语句。第三个参数是数据源。
function graphql(
schema: GraphQLSchema,
source: Source | string,
rootValue?: any,
contextValue?: any,
variableValues?: Maybe<{ [key: string]: any }>,
operationName?: Maybe<string>,
fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>,
typeResolver?: Maybe<GraphQLTypeResolver<any, any>>,
): Promise<ExecutionResult>;
- 使用
buildSchema构建一个GraphQLSchema,结构与数据分离:
const {graphql, buildSchema} = require('graphql')
const schema = buildSchema(`
type Query {
hello: String
}
`)
const root = {
hello: () => {
return 'Hello GraphQL'
}
}
graphql(schema, '{ hello }', root).then(res => {
console.log(res)
})
- 使用
GraphQLSchema构造器:
const {graphql,
GraphQLSchema,
GraphQLObjectType,
GraphQLString
} = require('graphql')
const schema2 = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'RootQueryType',
fields: {
hello: {
type: GraphQLString,
resolve() {
return 'world'
}
}
}
})
})
graphql(schema2, '{hello}').then(res => {
console.log('res2--->', res)
})
二、使用 koa-graphql 中间件
- 基本使用:
const Koa = require('koa');
const mount = require('koa-mount');
const graphqlHTTP = require('koa-graphql');
const app = new Koa();
app.use(mount('/graphql', graphqlHTTP({
schema: MyGraphQLSchema
})));
app.listen(4000);
— 使用 结构-数据-action 分离的方式,便于模块划分:
- 1、定义一个schema:
const schema = buildSchema(`
type Query {
message: User
}
type User {
name: String
id: Int,
like: Int
}
type Mutation {
praise(id: Int): Int
}
`)
- 2、定义数据源:
const message = {
name: 'Mary',
id: 999,
like: 2,
}
- 3、定义操作action:
// 查询,get请求即可
schema.getQueryType().getFields().message.resolve = () => {
return message
}
// 点赞,使用post请求
schema.getMutationType().getFields().praise.resolve = (_, id) => {
message.like ++
return message.like
}
- 4、使用koa-graphql中间件:
app.use(
mount('/api', graphqlHttp({
schema:schema
}))
)
第二章:SSR
第一节:VUE、REACT前后端同构
一、VueSSR与React原理
- React:
ReactDomServer.renderToString()更多 - Vue:
VueServeRenderer.renderToString()更多 - 原则:
- 避免单例,为每个请求创建一个新的Vue/React实例,避免状态共享。
- 生命周期执行环境:不同的钩子函数执行环境要判断是服务端还是客户端。
- 避免通用代码使用特定环境的API,如window不能在服务器环境执行。
- 自定义指令:操作dom的自定义指令不能在服务器环境运行。更多
二、使用Vue
调用renderToString,第一个参数是Vue实例。
const Vue = require('vue')
const app = new Vue({
data: {
text: 'Vue'
},
template: `<div>Hello {{text}}!</div>`
})
const { createRenderer } = require('vue-server-renderer')
const renderer = createRenderer({ /* 选项 */ })
renderer.renderToString(app).then(html => {
console.log('html---->', html)
})
三、使用react
由于react使用了jsx语法,需要使用babel进行编译。调用renderToString
// app.jsx
const React = require('react')
class App extends React.Component{
render() {
return<h1>REACT</h1>
}
}
module.exports = <App />
// index.js
require('@babel/register')({
presets: ['@babel/preset-react']
})
const app = require('./app.jsx')
const reactDOMServer = require('react-dom/server')
console.log(
reactDOMServer.renderToString(app)
)
四、React的SSR与CSR
- CSR:【浏览器请求HTML】——>【浏览器请求JS】——>【React初始化,更新页面内容及监听事件】——>【用户的页面】
- SSR:【浏览器请求HTML】——>【用户的页面】——>【浏览器请求JS】——>【React初始化,更新页面内容及监听事件】
通过上述SSR渲染的页面到用户可见时,页面的还是不完全可用,因为只包含了静态部分,还需要将React的打包出的js加载进静态页面,以构建出完全的页面。 下一节介绍如何同构出可以运行的项目。
第二节、使用webpack+express构建出项目
一、直接利用React进行编写
step1:编写一个React SSR项目
基本Demo: feature/1.0分支
步骤:
- 通过webpack分别打包出server、client的
bundle.js代码。 - 服务端与客户端分别应用这两个
bundle.js,服务端使用nodemon启用,客户端直接通过<script>引入
注意点:
- 使用webpack打包server端代码:可以使用
webpack-node-externals外置化应用程序依赖模块。可以使服务器构建速度更快, 并生成较小的 bundle 文件。 参考:vue_SSR_server配置 - 使用
ReactDOM.hydrate替换ReactDOM.render以修复两端不同内容,参考:hydrate使用 - 构建一个
<html>模板并手动引入client打包的bundle.js:
app.get("/", (req, res) => {
const content = renderToString(<Home />)
const html = `
<html>
<head ></head>
<body>
<div>
<div id="root">${content}</div>
<script src="bundle.js"></script>
</div>
</body>
</html>
`
res.send (html)
})
step2:引入React-Router
引入React-Router: feature/2.0分支
步骤:
- 引入
react-router-dom,server部分使用StaticRouter,client使用BrowserRouter渲染不同组件。 - 将
app.get("/", (req, res) => {})修改为app.get("*", (req, res) => {})以适配不同的路由。
注意:
- server部分的router需要使用无状态的
StaticRouter
// server.js
app.get("*", (req, res) => {
const content = renderToString(
<StaticRouter>
<Routes />
</StaticRouter>
)
const html = `
<html>
<head ></head>
<body>
<div>
<div id="root">${content}</div>
<script src="bundle.js"></script>
</div>
</body>
</html>
`
res.send (html)
})
step3:引入Redux
引入Redux: feature/3.0分支
- 使用redux需要使用
<Provider store={store}>分别包裹在服务端<StaticRouter>和客户端的<BrowserRouter>外部。 - 使用
axios可以同时在服务端和客户端允许。 - 值得注意的是
useEffect并不在服务端运行,所以如果需要初始数据,可以使用中间件提前获取初始数据。当然next.js或nuxt.js有自己的生命周期也可以解决。
二、使用Next.js进行编写
Next.js是React的一个主流SSR框架,使用Next能快速开发出一个应用。- Next支持两种预渲染模式, 静态生成是在编译环境预生成html,服务端渲染是在请求时生成html,并且可以混用两种模式。
- 其他这里有两篇我之前的文章: next.config.js重点解读 和 从Next.js到服务端渲染的学习 ,也讲到Next.js怎么使用。
第三章:性能调优
第一节:Http性能优化
一、压力测试
- 工具:ab(apache bench)、webbench
- 概念:吞吐率(qps,Requests per second)、并发连接数、并发用户数、用户平均请求等待时长(Time per request)、服务器平均请求等待时长(Time per request: across all concurrent requests)
二、通过ab进行压测
- 命令:
ab -n 200 -c 2000 http://127.0.0.1:3000,-n 表示请求数,-c 表示并发数:
Document Path: /
Document Length: 5 bytes
Concurrency Level: 200
Time taken for tests: 0.347 seconds
Complete requests: 2000
Failed requests: 0
Total transferred: 280000 bytes
HTML transferred: 10000 bytes
Requests per second: 5769.36 [#/sec] (mean)
Time per request: 34.666 [ms] (mean)
Time per request: 0.173 [ms] (mean, across all concurrent requests)
Transfer rate: 788.78 [Kbytes/sec] received
三、性能分析报告
方式一:NodeJs自带profile
- 启动时候输入:
node --prof xx.js #启动
ab -n 200 -t 5 http://127.0.0.1:3000 #压测5秒
node --prof-process ioslate-0x100000000000-0000-v8.log > profile.txt #分析数据到profile.txt
profile.txt得到类似下面的调用栈:
...
ticks parent name
1841 65.4% /usr/local/bin/node
567 30.8% /usr/local/bin/node
147 25.9% LazyCompile: *Socket net.js:268:16
126 85.7% LazyCompile: *onconnection net.js:1529:22
21 14.3% LazyCompile: ~onconnection net.js:1529:22
64 11.3% LazyCompile: *Readable _stream_readable.js:183:18
...
分析得到: net.js 占用内存较高。
方式二: Chrome devtool
- 启动的时候输入:
node --inspect-brk xx.js #启动
- 在chrome地址栏输入
chrome://inspect/#devices,进入调试,在profiler 一栏点击(CPU start profiling)。 - 开始压测
ab -n 200 -t 5 http://127.0.0.1:3000,等待5秒结束后,点击(CPU stop profiling),得到数据 - 分析结果:chrome提供3种图表分析方式:Chart、Heavy、Tree,左上角进行选择
四、代码优化
提前计算
在中间件中执行的内容,提前在启动阶段执行,如fs.readFileSync放到中间件外部,提前读取。将string提前转换为buffer。
五、内存管理
- 特点:v8分为: 新生代 和 老生代 两种内存区域。新生代:容量小,垃圾回收快。老生代:容量大,垃圾回收慢。
- 检测内存泄漏:
node --inspect-brk xx.js #启动服务
ab -n 200 -t 5 http://127.0.0.1:3000 #开始压测
在chrome://inspect/#devices的memory栏内截取内存快照,使用comparsion比较两个内存快照,可以知道哪些变量未被释放。
- 减少内存使用:使用池;
- 使用C++插件优化允许速率:编译成.node 文件和平台和版本相关,C++运算更快,但是有C++变量转换为V8变量的成本
五、使用多进行、多线程进行优化
- 进程与线程的概念,其中主线程运行v8与JavaScript,多个子线程通过事件循环被调用。
- 使用子进程或线程利用更多CPU资源。
创建子进程
通过cp.fork创建子进程,通过child_process.on('message', str => {})监听子进程信息。
通过child_process.send发送信息给子进程。子进程通过全局方法process操作。
// 父进程 ./index.js
const cp = require('child_process')
const child_process = cp.fork(__dirname + '/child.js')
child_process.send('i am father')
child_process.on('message', str => {
console.log('parent->', str) // 真正的端口监听在主进程里,主进程监听到了在分发到子进程里
})
// 子进程 ./child.js
process.on('message', str => {
console.log('child->', str)
process.send('i am sun')
})
Worker Threads 多线程
Worker Threads 在V10 版本提供,多数情况下无需使用。
六、使用Node.js 的 Cluster 模块
-
使用Cluster 进行http多进程通信,
cluster.fork()启动一个进程。cluster.isMaster判断是否为主进程。 -
端口监听其实都在主进程里进行监听的,在cluster内部主进程会与子进程进行通信,父进程onConnection的句柄(回调方法)会传给子进程, 其中父进程通过轮询找到那个空闲的子进程。拓展阅读:cluster原理简析
-
通过
process.on('uncaughtException', res => {})实现进程守护, -
下面代码演示了启动3个进程,每隔3000ms 发送一个ping包,超过3次未收到pong包杀死子进程。5000ms后重启进程。
const fs = require('fs')
const http = require('http')
const cluster = require('cluster')
const os = require('os')
console.log(os.cpus().length) // cpu核数
if (cluster.isMaster) {
for(let i =0 ; i < 3; i++) { // 开启3个进程
const worker = cluster.fork()
// 心跳检查
let missedPing = 0
let inter = setInterval(() => {
worker.send('ping')
missedPing++
if (missedPing >= 3) {
clearInterval(inter)
// 3次没收到pong包
console.log('kill')
process.kill(worker.process.pid) // 杀进程
}
}, 3000)
worker.on('message', (msg) => {
if(msg === 'pong') {
console.log('pong')
missedPing--
}
})
}
// 监听到子进程退出,即5s后重启进程
cluster.on('exit', () => {
setTimeout(() => {
cluster.fork()
}, 5000)
})
// 监控内存情况
setInterval(() => {
if (process.memoryUsage().rss > 734003200) {
// 内存占用太多,杀进程
process.exit(1)
}
}, 5000)
} else {
http.createServer((req, res) => {
res.writeHead(200, {'content-type': 'text/html'})
res.end(fs.readFileSync(__dirname + '/testCluster.html', 'utf-8'))
}).listen(3001, () => {
console.log('listened 3001')
})
// 进程守护
process.on('uncaughtException', (err) => {
console.error(err) // 上报
process.exit(1) // 进程退出
})
process.on('message', msg => {
if (msg === 'ping') {
process.send('pong')
}
})
while (true) { // 假死,用于测试
const a = 1
}
}
七、 架构优化
-
动静分离:静态内容不会变动,不会因为请求参数而改变,一般是指静态资源。动态部分会因为请求参数改变而改变,且几乎不可枚举。
-
分离原则:静态内容使用CDN,HTTP缓存;动态部分使用大量的源站承载,结合反向代理进行负载均衡
-
redis:由内存做缓存,也是一个rpc调用,下面简单展示了redis的使用
const app = new (require('koa'))
const cacheRedis = require('redis')('cache')
const backupRedis = require('redis')('backup')
app.use( async (ctx, next) => {
const result = cacheRedis(ctx.url)
if (result) {
ctx.body = result
return
}
await next()
if (ctx.status === 200) {
cacheRedis.set(ctx.url, ctx.body, {expire: 300})
backupRedis.set(ctx.url, ctx.body, {expire: 300}) // 做备份
}
if (ctx.status !== 200) {
const result = await backupRedis(ctx.url)
ctx.status = 200
ctx.body = result
}
})
第四章:框架设计和工程化
框架设计要遵循KISS原则,即在框架设计中使用渐进式
一、设计模式
设计模式6大法则:
- 单一职责原则:一个方法只做一件事,每个模块都有明确的功能
- 里氏替换原则:...
- 依赖倒转原则:...
- 接口隔离:...
- 最小知晓原则:...
- 开闭原则:对扩展开放,对修改关闭,可以理解为在代码迭代中避免修改原代码,如webpack的loader支持各种扩展文件
常见设计模式:
- 外观模式: 如JQuery把不同浏览器的兼容性操作隐藏起来
- 观测者模式: 如addEventListener
二、屏蔽细节
将通用逻辑下沉,代码配置化。如在koa服务中:
- 将逻辑配置化:如将koa的路由配置化(类似于vue/react-router)
- 屏蔽更多细节:如将http启动服务屏蔽
- 使用框架next.js/nuxt.js 屏蔽更多细节
三、Serverless
屏蔽服务器的细节,不用为运维、框架的事情操心;减少出错概率;上手难度低。 如云函数、如低代码发布平台。
完结