跳到主要内容

第三方库

bluebird

可以将回调函数实现的异步改写成Promise的方式来写的第三方库。

bluebird + fs

回调

const fs = require('fs')
fs.readFile('index.html', (err, data) => {
response.end(data)
})

Promise

const bluebird = require('bluebird')
const fs = bluebird.promisifyAll(require('fs'))

fs.readFileAsync('index.html')
.then(data => {
response.end(data)
})

bluebird + mysql

回调

const mysql = require('mysql')
// mysql配置文件
let config = require('./config')
conn.connect()

// 使用
conn.query(`sql code here...`, (err, data) => {

})

Promise

const bluebird = require('bluebird')
const mysql = require('mysql')
// mysql配置文件
let config = require('./config')
const conn = bluebird.promisifyAll(mysql.createConnection(config))
conn.connect()

// 使用
let data = await conn.queryAsync(`sql code here...`)

PM2

除了常见的 pm2 start index.js,我们也可以使用配置文件。

// 比如取名为 ecosystem.config.js
module.exports = {
apps: [{
script: './server/app.js',
watch: '.',
env_development: {
"REACT_APP_NODE_ENV": "development"
},
env_production: {
"REACT_APP_NODE_ENV": "production"
}
}]
}

之后通过以下命令来启动服务

pm2 start ecosystem.config.js --env development
// or
pm2 start ecosystem.config.js --env production

常用命令

pm2 start app.js
pm2 list
pm2 delete [app-id]
pm2 logs
pm2 logs [app-name]
pm2 monit
// ...

命令行工具

介绍常用的命令行工具

chalk

给日志输出加上颜色。

import chalk from 'chalk'
console.log(chalk.blue('akara')) // 蓝色字体
console.log(chalk.blue.bgRed('akara')) // 蓝色字体,红色背景

yargs

提供了对命令行参数的解析功能,并且默认提供了 --help--version选项。

#!/usr/bin/env node
const yargs = require("yargs/yargs");
const { hideBin } = require("yargs/helpers");
const http = require("http");

yargs(hideBin(process.argv)) // hideBin(process.argv) 相当于 process.argv.slice(2)
.command(
"serve [port]", // [port]为可选参数
"启动服务器",
{ // 设置命令参数的别名、默认值等信息
port: {
alias: "p",
default: 3000,
},
},
(argv) => {
http.createServer((req, res) => {}).listen(argv.port, () => {
console.log(`服务器运行在${argv.port}端口`);
});
}
)
.command("curl <url>", "发送请求", {}, (argv) => { // <url>为必须参数
if (argv.verbose) console.log('已经开启verbose')
console.log(argv.url);
})
.option('verbose', {
alias: 'v',
type: 'boolean',
description: 'Run with verbose logging'
})
.argv;
cli --help
cli --version
cli serve 8000 # cli serve -p 8000 | cli serve --port=8000
cli curl 'google.com' -v

commander

yargs作用差不多,可以选择其中一个来开发自己的命令行工具。

#!/usr/bin/env node
const { program } = require('commander')

program
.version('1.0.0')
.description('cli tool')
.option('--verbose', 'use verbose') // 布尔值
.option('-u, --url <url>', 'url参数') // 必须参数
.option('-p, --port [port]', 'port参数', 3000) // 可选参数,可设置默认值
.parse(process.argv)

console.log(program.opts());

inquirer

非常有用的命令行工具,常见于各种脚手架中。

#!/usr/bin/env node
const inquirer = require('inquirer')
const questions = [
{
type: 'confirm',
name: 'isPeople',
message: '你是人吗?',
default: false
},
{
type: 'input',
name: 'name',
message: '请输入你的名字',
},
{
type: 'input',
name: 'phone',
message: '请输入你的电话号码',
validate(value) {
const pass = value.match(/^1[34578]\d{9}$/g)
if (pass) return true
return '请输入正确的电话号码'
}
},
{
type: 'list',
name: 'sex',
message: '请选择你的性别',
choices: ['Male', 'Female', 'None'],
filter(val) {
return val.toLowerCase();
},

}
]
inquirer
.prompt(questions)
.then(answers => {
console.log(JSON.stringify(answers, null, ' '));
})

readline

// 官网代码
const readline = require('readline')

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})

rl.question('你好', (answer) => {
console.log('666');
rl.close()
})
// 官网代码
const fs = require('fs');
const readline = require('readline');

async function processLineByLine() {
const fileStream = fs.createReadStream('log.txt');

const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
// 注意:我们使用 crlfDelay 选项将 input.txt 中的所有 CR LF 实例('\r\n')识别为单个换行符。

for await (const line of rl) {
// input.txt 中的每一行在这里将会被连续地用作 `line`。
console.log(`Line from file: ${line}`);
}
}

processLineByLine();

puppeteer

使用 puppeteer.connect来复用已启动的浏览器进程。

  1. 启动Chrome的时候加上 --remote-debugging-port=9222 ,重启浏览器
  2. 访问 http://127.0.0.1:9222/json/version拿到 webSocketDebuggerUrl字段
  3. const url = 'ws://127.0.0.1:9222/devtools/browser/81daad69-fb53-49ea-9f97-3683b73afea0'
    const browser = await puppeteer.connect({
    browserWSEndpoint: url,
    });

参考:https://medium.com/@jaredpotter1/connecting-puppeteer-to-existing-chrome-window-8a10828149e0

Koa

基础

const Koa = require('koa');
const app = new Koa();

app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});


// response
app.use(ctx => {
ctx.status = 200
ctx.set('Content-type', 'text/plain; charset=utf-8')
ctx.body = 'Hello Koa'
});

app.listen(3000);

// 一些其他的方法
ctx.redirect('/home')
// 相当于
// res.status = 302
// res.setHeader('Location', '/home')

核心实现

const Emitter = require('events')
// 三个对象,提前定义好原型的方法
const context = require('./context')
const request = require('./request')
const response = require('./response')
class Koa extends Emitter {
constructor() {
super()
this.middleware = []
this.context = Object.create(context)
this.request = Object.create(request)
this.response = Object.create(response)
}

callback() {
const fn = compose(this.middleware)
return (req, res) => {
const ctx = this.createContext(req, res)
return this.handlerRequest(ctx, fn)
}
}

use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
this.middleware.push(fn)
return this
}

listen(...args) {
const server = http.createServer(this.callback())
return server.listen(...args)
}

createContext(req, res) {
// 其实就是根据已有的req和res创建上下文context
const context = Object.create(this.context);
const request = Object.create(this.request);
const response = Object.create(this.response);
context.request = request
context.response = response
context.app = request.app = response.app = this;
// 重点,挂载req和res
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
// 互相引用
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
return context
}

handlerRequest(ctx, fn) {
const res = ctx.res
res.statusCode = 404
fn(ctx).catch(reason => {
console.log(reason)
})
}
}

Koa的实例app有三个公共的API

  • use

    app.use((ctx, next) => {

    })

    use方法用于将参数中间件放进app的middleware数组里

  • listen

    app.listen(3000)

    等价于

     const server = http.createServer(this.callback())
    server.listen(3000)
  • callback

    该函数内部实现三个功能

    1. 使用koa-compose函数将middleware中间件数组转化为中间件fn

    2. 调用app.createContext函数。创建context,request,response对象;将request和response挂载在context上;把req和res挂载在三个对象上。

      例如:request的原型对象上部分代码如下

      get header() {
      return this.req.headers;
      },
      set header(val) {
      this.req.headers = val;
      },

      我们现在就可以根据 ctx.request.header获取req的headers了

    3. 执行handleRequest函数,本质是把组装好的context传入中间件fn执行

Koa源码中使用到了Koa-compose, 用于将多个中间件函数组合为一个中间件函数

koa-compose

const compose = (middleware) => {
if (!Array.isArray(middleware)) throw new TypeError("Middleware stack must be an array!")
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError("Middleware must be composed of functions!")
}
let length = middleware.length
return function (ctx, next) {
let index = -1
return dispatch(0)
function dispatch(i) {
// 一个中间件内部多次调用next时,index大于等于i
if ( index >= i) {
return Promise.reject(new Error('next() called multiple times'))
}
let fn
index = i
if (i < length) {
fn = middleware[i]
}
else if (i === length) {
// 重点, 外部compose的next传进内部compose
fn = next
}
// 最后一个中间件调用next时,什么也不做
if (!fn) return
// 官方源码使用Promise是为了使用async中间件,不过这里没有怎么实现这个功能,就一个样子
return Promise.resolve(fn(ctx, dispatch.bind(null, (i + 1))))
}
}
}

koa-router

const Router = require('koa-router')
const router = new Router()
router
.get('/', (ctx, next) => {
ctx.body = 'Hello World!';
})
.post('/users', (ctx, next) => {
// ...
})
.put('/users/:id', (ctx, next) => {
// ...
})
.del('/users/:id', (ctx, next) => {
// ...
})
.all('/users/:id', (ctx, next) => {
// ...
});
app.use(router.routes())
app.use(router.allowedMethods()) // 此处例子没有实现该方法

简易实现

简易实现,只实现一个get方法,实际上要更复杂的多。

class Router {
constructor() {
this.stack = []
}

get(url, fn) {
function middleware(ctx, next) {
if (ctx.req.method.toLowerCase() === 'get' && ctx.req.url === url) {
console.log('路由匹配成功');
fn(ctx, next)
}
else {
console.log('路由匹配失败');
next()
}
}
this.stack.push(middleware)
return this
}

routes() {
return (ctx, next) => {
let fn = compose(this.stack)
// 必须加上next参数
// koa本身有一个compose, 这里也有一个,所以要把外部的next传给内部
fn(ctx, next)
}
}
}

koa-static

用于处理静态资源的koa中间件

const static = require('koa-static')
app.use(static('public'))

koa-body

处理请求的中间件,可以轻松获得请求的内容

const body = require('koa-body')
app.use(body({multipart: true}))
app.use((ctx) => {
console.log(ctx.request.body)
})

koa-logger

const logger = require('koa-logger')
app.use(logger())

koa-views

通常用于搭配模板引擎进行服务端渲染,不过似乎现在不怎么用了。

另外使用的场合要额外去安装对应的模板引擎,比如想用 ejs记得先 npm i ejs

const views = require('koa-views')
const render = views('./views', { extension: 'ejs'})

app.use(render)
app.use(async ctx => {
await ctx.render('template', {
content: 'hello'
})
})
<!-- template.ejs -->
<!DOCTYPE html>
<html>
<head></head>
<body>
<div><%= content %></div>
</body>
</html>

NestJS

NextJS使用装饰器模式(风格类似前端的Angular)、依赖注入模式、对TypeScript支持友好,是一门广泛被应用的Node后端Web框架。

在NextJS中通过Module进行功能模块的划分,每个Module通常包括Controller和Service,Controller用于提供后端接口,Service则用于提供各种服务,一个Module内定义的Service可以通过放在exports中向外暴露,再另一个模块中可以通过imports来引入该模块,从而使用该模块暴露出的Service服务。

npm i -g @nestjs/cli
nest new my-project
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
// app.module.ts
import { Module } from '@nestjs/common';
import { AppController, MyController } from './app.controller';
import { AppService } from './app.service';

@Module({
imports: [],
controllers: [AppController, MyController],
providers: [AppService],
exports: [],
})
export class AppModule {}

Controller

我们可以通过编写 controller来实现后端路由。

// app.controller.ts
import { Controller, Get, Post, Req, Res, Body, Param, Query, Headers, Header, HttpCode } from '@nestjs/common';
import { Request } from 'express'

export class DTO { // 数据传输对象
value: string
}

@Controller()
export class AppController {
@Get() // 匹配/路径
getText(): string {
return 'hello'
}

@Get('admin') // 匹配/admin路径
getAdmin(): string {
return 'admin'
}
}

@Controller('/api')
export class MyController {
@Get('fetchAllInfo') // 匹配/api/fetchAllInfo
fetchInfo(@Req() req: Request, @Query() query): string[] { // 拿到Req、Query
console.log(req.url)
console.log(query)
return ['a', 'b', 'c']
}

@Get('/fetchOneInfo/:id')
fetchOneInfo(@Param() params, @Headers() headers): string { // 拿到Params、响应头Headers
console.log(params.id)
console.log(headers)
return 'a'
}

@Post('/updateOneInfo/:id')
updateOneInfo(@Param('id') id: number, @Body() body: DTO) { // 通过@Param('id')可以直接拿到具体的Param。拿到Body
console.log(id)
console.log(body)
return { // 自动序列化为JSON并设置对应Content-Type
code: 200,
msg: 'success'
}
}

@Get('html')
@Header('Cache-Control', 'none') // 设置响应头部
getHtml(): string { // 自动设置Content-Type
return `
<html>
<body>
<h1>hello nest</hi>
</body>
</html>
`
}

@HttpCode(404) // 设置响应状态码
@Get('404')
four0four() {
return '404 not Found'
}

@Get('async')
async testAsync(): Promise<string[]> {
return ['aa', 'bb', 'cc']
}

@Get('res')
async testRes(@Res() res) {
res.send('hello nest')
}
}

Service

我们使用 Controller来进行路由控制,具体的数据操作或逻辑操作由 Service负责(Service是一种 Provider

首先创建 Service类,并在 app.module.ts中声明该 ServiceProvider,然后 Controller的构造函数添加一个入参(为 Service类的实例)。

// app.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
private name: string = 'akara'

getName(): string {
return this.name
}

setName(name: string): void {
this.name = name
}
}
// app.controller.ts
import { Controller, Get} from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}

@Get('/get/service')
async testGetService() {
return this.appService.getName()
}

@Get('/set/service')
async testSetService() {
return this.appService.setName('bkb')
}
}