入门NODE.JS 这一篇就够了

·  阅读 469

第一章: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.envapp.proxyapp.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, '&amp;')
        }
    })

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 中间件

npm: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分支

步骤:

  1. 通过webpack分别打包出server、client的bundle.js代码。
  2. 服务端与客户端分别应用这两个bundle.js,服务端使用nodemon启用,客户端直接通过<script>引入

注意点:

  1. 使用webpack打包server端代码:可以使用webpack-node-externals外置化应用程序依赖模块。可以使服务器构建速度更快,

并生成较小的 bundle 文件。 参考:vue_SSR_server配置 2. 使用ReactDOM.hydrate替换ReactDOM.render以修复两端不同内容,参考:hydrate使用 3. 构建一个<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分支

步骤:

  1. 引入react-router-dom,server部分使用StaticRouter,client使用BrowserRouter渲染不同组件。
  2. app.get("/", (req, res) => {})修改为app.get("*", (req, res) => {})以适配不同的路由。

注意:

  1. 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分支

  1. 使用redux需要使用<Provider store={store}>分别包裹在服务端<StaticRouter>和客户端的<BrowserRouter>外部。
  2. 使用axios可以同时在服务端和客户端允许。
  3. 值得注意的是useEffect并不在服务端运行,所以如果需要初始数据,可以使用中间件提前获取初始数据。当然next.js或nuxt.js有自己的生命周期也可以解决。

二、使用Next.js进行编写

  • Next.js是React的一个主流SSR框架,使用Next能快速开发出一个应用。
  • Next支持两种预渲染模式,

静态生成是在编译环境预生成html,服务端渲染是在请求时生成html,并且可以混用两种模式。

](juejin.cn/post/684490…

第三章:性能调优

第一节: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大法则:

  1. 单一职责原则:一个方法只做一件事,每个模块都有明确的功能
  2. 里氏替换原则:...
  3. 依赖倒转原则:...
  4. 接口隔离:...
  5. 最小知晓原则:...
  6. 开闭原则:对扩展开放,对修改关闭,可以理解为在代码迭代中避免修改原代码,如webpack的loader支持各种扩展文件

常见设计模式:

  • 外观模式: 如JQuery把不同浏览器的兼容性操作隐藏起来
  • 观测者模式: 如addEventListener

二、屏蔽细节

将通用逻辑下沉,代码配置化。如在koa服务中:

  • 将逻辑配置化:如将koa的路由配置化(类似于vue/react-router)
  • 屏蔽更多细节:如将http启动服务屏蔽
  • 使用框架next.js/nuxt.js 屏蔽更多细节

三、Serverless

屏蔽服务器的细节,不用为运维、框架的事情操心;减少出错概率;上手难度低。 如云函数、如低代码发布平台。

完结

分类:
前端
标签: