# express

# 安装

使用脚手架 express-generator 来生成 express 框架。

npm install express-generator -g
express express-test
npm install & npm start

# 使用

app.js:

// 处理路由请求出错
var createError = require('http-errors');
var express = require('express');
var path = require('path');
const fs = require('fs')
// 解析cookie  可以通过 req.cookie 访问 cookie 内容
var cookieParser = require('cookie-parser');
// 记录日志的插件
var logger = require('morgan');
// 使用 express-session 处理session
const session = require('express-session')
// 将 session 存储到 redis 中
const RedisStore = require('connect-redis')(session)
// 引入两个路由
const blogRouter = require('./routes/blog')
const userRouter = require('./routes/user')
var app = express();
const ENV = process.env.NODE_ENV

// https://github.com/expressjs/morgan 查看打印日志的格式
if(ENV != 'production') {
  // 对日志进行配置
  app.use(logger('dev', {
    // 标准输出,直接输出到控制台
    stream: process.stdout
  }));
} else {
  // 线上环境使用,打印的日志比较详细--将日志写到access.log文件当中
  const logFileName = path.join(__dirname, 'logs', 'access.log')
  const writeStream = fs.createWriteStream(logFileName, {
    flags: 'a'
  })
  app.use(logger('combined', {
    stream: writeStream
  }))
}
// 线上环境使用,打印的日志比较详细
// app.use(logger('combined', {
//   stream: process.stdout
// }))


// 处理 post 请求的数据,获取 content-type=applica/json 格式的数据,可以通过 req.body 获取数据
app.use(express.json());
// 处理 post 数据,兼容处理其他数据
app.use(express.urlencoded({ extended: false }));
// 解析cookie
app.use(cookieParser());

const redisClient = require('./db/redis')
const sessionStore = new RedisStore({
  client: redisClient
})
// 解析 session
app.use(session({
  // 密匙
  secret: 'jiegiser_95#',
  cookie: {
    // path: '/', // 模式配置
    // httpOnly: true, // 默认配置
    maxAge: 24 * 60 * 60 * 1000
  },
  // session 存储到 redis 中
  store: sessionStore
}));
// public文件夹
// app.use(express.static(path.join(__dirname, 'public')));

// 注册路由
// app.use('/', indexRouter);
// 访问/users/
// app.use('/users', usersRouter);
app.use('/api/blog', blogRouter);
app.use('/api/user', userRouter);

// 检测 404
app.use(function(req, res, next) {
  next(createError(404));
});

// 处理错误信息
app.use(function(err, req, res, next) {
  // dev环境错误会直接抛出
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'dev' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

处理路由:

// get 请求
router.get('/login-test', (req, res, next) => {
  // req.query 中存放 get 请求中的 queryString
  if(req.session.username) {
    // 可以直接返回 json 数据
    res.json({
      errno: 0,
      msg: '测试成功'
    })
    return
  }
  res.json({
    errno: -1,
    msg: '测试失败'  
  })
})
// post 请求
router.post('/login', function(req, res, next) {
  // express.json() 解析得到的数据存放在 req.body
  const { username, password } = req.body
  // 返回json格式的数据。相当于之前的res.end(JSON.stringify)
  res.json({
    errmo: 0,
    data: {
      username,
      password
    }
  })
});

# 中间件

可以单独安装 express:npm install express;

next 函数执行下一个 app.use() 方法;

const express = require('express')
// 本次 http 请求的示例
const app = express()

app.use((req, res, next)  => {
  console.log('请求开始...', req.method, req.url)
  next()
})

app.use((req, res, next) => {
  // 处理 cookie
  req.cookie = {
    userId: 'jiegiser'
  }
  next()
})

app.use((req, res, next) => {
  // 处理 post data
  // 异步 
  setTimeout(() => {
    req.body = {
      a: 100,
      b: 2100
    }
    next()
  })
})

app.use('/api', (req, res, next) => {
  console.log('处理 /api 路由')
  next()
})
app.get('/api', (req, res, next) => {
  console.log('get /api 路由')
  next()
})
app.post('/api', (req, res, next) => {
  console.log('post /api 路由')
  next()
})
// 模拟登陆验证
// function loginCheck(req, res, next) {
//   console.log('模拟登陆成功')
//   setTimeout(() => {
//     next()
//   })
// }
// 登陆失败
function loginCheck(req, res, next) {
  console.log('模拟登陆成功')
  setTimeout(() => {
    res.json({
      errno: -1,
      msg: '登陆失败'
    })
    // 不执行 next()
  })
}
// 可以是多个函数 --可以是登陆验证,权限验证等等。前面的方法进行验证
app.get('/api/get-cookie', loginCheck, (req, res, next) => {
  console.log('get /api/get-cookie')
  res.json({
    errno: 0,
    data: req.cookie
  })
})

app.post('/api/get-post-data', (req, res, next) => {
  console.log('post apo/post/data')
  res.json({
    errno: 0,
    data: req.body
  })
})

app.use((req, res, next) => {
  console.log('处理 404')
  res.json({
    errno: -1,
    msg: '404 not found'
  })
})

app.listen(5000, () => {
  console.log('server is running on port 5000')
})

# express-session 处理 session

安装 express-session 插件:npm install express-session --save;他可以进行处理 session 相关:

// 使用 express-session 处理 session
const session = require('express-session')
// 设置 session
app.use(session({
  // 密匙
  secret: 'jiegiser_95#',
  cookie: {
    path: '/', // 模式配置
    httpOnly: true, // 默认配置
    maxAge: 24 * 60 * 60 * 1000 // 过期时间间隔 expires 是需要设置到期的具体日期;
  }
}));

// 设置 用户 session
router.post('/login', function(req, res, next) {
  const { username, password } = req.body
  const result = login(username, password)
  return result.then(data => {
    if(data.username) {
      req.session.username = data.username
      req.session.realname = data.realname
      res.json(new SuccessModel())
      return
    }
    res.json(new ErrorModel('登录失败'))
  })
});

// 读取 session
router.get('/login-test', (req, res, next) => {
  if(req.session.username) {
    res.json({
      errno: 0,
      msg: '测试成功'
    })
    return
  }
  res.json({
    errno: -1,
    msg: '测试失败'  
  })
})

# express-session 和 connect-redis 存储 session

安装 redis 以及 connect-redis 插件,npm i redis connect-redis --save;

进行配置 redis

const redis = require('redis')
const { REDIS_CONF } = require('../config/db')
// 创建客户端
const redisClient = redis.createClient(REDIS_CONF.port, REDIS_CONF.host)
redisClient.on('error', err => {
  console.error(err)
})
module.exports = redisClient

使用 connect-redis 进行存储 session:

// 使用 express-session 处理 session
const session = require('express-session')
// 将 session 存储到 redis 中
const RedisStore = require('connect-redis')(session)

// ./db/redis 是上面创建 redis 的配置文件
const redisClient = require('./db/redis')
const sessionStore = new RedisStore({
  client: redisClient
})
// 解析session
app.use(session({
  // 密匙
  secret: 'jiegiser_95#',
  cookie: {
    // path: '/', // 模式配置
    // httpOnly: true, // 默认配置
    maxAge: 24 * 60 * 60 * 1000
  },
  // session存储到redis中
  store: sessionStore
}));

从 redis 中读取 session:

const { ErrorModel } = require('../model/resModel')
// 写一个中间件
module.exports = (req, res, next) => {
  if(req.session.username) {
    // 已经登录了
    next()
    return
  }
  res.json(
    new ErrorModel('未登录')
  )
}

# 使用 morgan 记录日志

// 记录日志的插件
var logger = require('morgan');
const ENV = process.env.NODE_ENV

// https://github.com/expressjs/morgan 查看打印日志的格式
if(ENV != 'production') {
  // 对日志进行配置 -下面的写法相当于 app.use(logger('dev'))
  app.use(logger('dev', {
    // 标准输出,直接输出到控制台
    // 输出内容::method :url :status :response-time ms - :res[content-length]
    stream: process.stdout
  }));
} else {
  // 线上环境使用,打印的日志比较详细--将日志写到 access.log 文件当中
  const logFileName = path.join(__dirname, 'logs', 'access.log')
  const writeStream = fs.createWriteStream(logFileName, {
    flags: 'a'
  })
  app.use(logger('combined', {
    // 输出内容::remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"
    stream: writeStream
  }))
}

express 与 node 之间的主要区别,就是他对我们的操作进行了很多封装:

  • 写法上:如 req.query、res.json ,以及 router.get 跟 router.post
  • 使用 express-session,connect-redis 等中间件更加方便
  • 使用 morgan 操作日志

# 中间件原理

一个简单的中间件模式:使用 app.use 注册路由,app.get 、app.post 等方法

app.use((req, res, next)  => {
  console.log('请求开始...', req.method, req.url)
  next()
})
// 使用多个中间件
// 可以是多个函数 --可以是登陆验证,权限验证等等。router 也就是这样的原理
app.get('/api/get-cookie', loginCheck, (req, res, next) => {
  console.log('get /api/get-cookie')
  res.json({
    errno: 0,
    data: req.cookie
  })
})

分析:

  • app.use 用来注册中间件,先收集
  • 遇到 http 请求,根据 path 和 method 判断触发哪些方法
  • 实现 next 机制,即上一个通过 next 触发下一个
  • 可以通过调用 use() 函数来注册新的中间件,通常,新的中间件只能被添加到高压包带的末端,但不是严格要求这么做;
  • 当接收到需要处理的新数据时,注册的中间件在意不执行流程中被依次调用。每个中间件都接受上一个中间件的执行结果作为输入值;
  • 每个中间件都可以停止数据的进一步处理,只需要简单地不调用它的毁掉函数或者将错误传递给回调函数。当发生错误时,通常会触发执行另一个专门处理错误的中间件。

实现一个简单的 express:

const http = require('http')
const slice = Array.prototype.slice

class LikeExpress {
  constructor() {
    // 存放中间件的列表
    this.routers = {
      all: [], // app.use(...)
      get: [], // app.get(...)
      post: [] // app.post(...)
    }
  }
  // 处理注册
  register(path) {
    const info = {}
    if(typeof path === 'string') {
      info.path = path
      // 从第二个参数开始转换为数组,最终获取到的是一个数组
      info.stack = slice.call(arguments, 1)
    } else {
      info.path = '/'
      // 从第一个参数开始,转换为数组,存入 stack
      info.stack = slice.call(arguments, 0)
    }
    return info
  }
  use() {
    const info = this.register.apply(this, arguments)
    this.routers.all.push(info)
  }

  get() {
    const info = this.register.apply(this, arguments)
    this.routers.get.push(info)    
  }
  post() {
    const info = this.register.apply(this, arguments)
    this.routers.post.push(info)       
  }
  match(method, url) {
    let stack = []
    if(url === '/favicon.ico') {
      return stack
    }

    // 获取 routes
    let curRoutes = []
    // 通过 use 注册的路由不管是 get 还是 post 请求都要执行
    curRoutes = curRoutes.concat(this.routers.all)
    // 然后根据请求的类型获取到对应的路由
    curRoutes = curRoutes.concat(this.routers[method])

    curRoutes.forEach(routerInfo => {
      if (url.indexOf(routerInfo.path) === 0) {
        // url === '/api/get-cookie' 且 routeInfo.path === '/'
        // url === '/api/get-cookie' 且 routeInfo.path === '/api'
        // url === '/api/get-cookie' 且 routeInfo.path === '/api/get-cookie'
        stack = stack.concat(routerInfo.stack)
      }
    })
    return stack
  }

  // 核心的 next 机制
  handle(req, res, stack) {
    const next = () => {
      // 拿到第一个匹配的中间件
      const middleware = stack.shift()
      if(middleware) {
        console.log(middleware)
        // 执行中间件函数
        middleware(req, res, next)
      }
    }
    next()
  }

  callback() {
    return (req, res) => {
      res.json = (data) => {
        res.setHeader('Content-type', 'application/json')
        res.end(
          JSON.stringify(data)
        )
      }
      const url = req.url
      const method = req.method.toLowerCase()
      // 
      const resultList = this.match(method, url)
      this.handle(req, res, resultList)
    }
  }
  listen(...args) {
    const server = http.createServer(this.callback())
    server.listen(...args)
  }
}

// 工厂函数
module.exports = () => {
  return new LikeExpress()
}

使用上面的代码:

const express = require('./like-express')
// 本次 http 请求的实例
const app = express()
console.log(app.use)
app.use((req, res, next) => {
  console.log('请求开始', req.method, req.url)
  next()
})

app.use((req, res, next) => {
  // 假设在处理 cookie、
  req.cookie = {
    userId: 'abc123'
  }
  next()
})

app.use('/api', (req, res, next) => {
  console.log('处理 /api 路由')
  next()
})

app.get('/api', (req, res, next) => {
  console.log('get /api 路由')
  next()
})

// 模拟登陆验证
function loginCheck(req, res, next) {
  setTimeout(() => {
    console.log('模拟登陆成功')
    next()
  })
}
app.get('/api/get-cookie', loginCheck, (req, res, next) => {
  console.log('get /api/get-cookoe')
  res.json({
    errno: 0,
    data: req.cookie
  })
})

app.listen(8888, () => {
  console.log('server is running on port 8888')
})

评 论:

更新: 11/21/2020, 7:00:56 PM