Skip to content
一、express

官网参考:https://expressjs.com/zh-cn/

  • express 是 nodejs 最常用的 web server 框架
  • express 下载、安装、使用、express 中间件机制
  • 开发接口、连接数据库、实现登录、日志记录
  • 分析 express 中间件原理
1. 介绍 express
  • 安装(使用脚手架 express-generator)

    shell
    npm install express-generator -g
    express express-test
    npm i & npm start
  • 初始化代码,处理路由

    • 新建项目
      shell
      express express-blog
      npm i nodemon cross-env -D  
      # 配置 启动脚本
      "dev": "cross-env NODE_ENV=dev nodemon ./bin/www"
    • 介绍 app.js
      • 各个插件的作用
        js
        // 处理未定义的路由
        const createError = require('http-errors');
        const express = require('express');
        const path = require('path');
        // 解析 cookie
        const cookieParser = require('cookie-parser');
        // 日志
        const logger = require('morgan');
        
        // 定义的路由接口
        const indexRouter = require('./routes/index');
        const usersRouter = require('./routes/users');
        
        const app = express();
        
        // view engine setup
        app.set('views', path.join(__dirname, 'views'));
        app.set('view engine', 'jade');
        
        // 注册
        app.use(logger('dev'));
        app.use(express.json());
        app.use(express.urlencoded({ extended: false }));
        app.use(cookieParser());
        app.use(express.static(path.join(__dirname, 'public')));
        
        app.use('/', indexRouter);
        app.use('/users', usersRouter);
        
        // catch 404 and forward to error handler
        app.use(function(req, res, next) {
          next(createError(404));
        });
        
        // error handler
        app.use(function(err, req, res, next) {
          // set locals, only providing error in development
          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;
  • 中间件的使用和执行过程

    shell
    npm init -y;
    npm i express -D;
    js
    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) => {
      req.cookie = {
        userId: '123'
      }
      next()
    })
    
    app.use((req, res, next) => {
      // 假设处理 post data
      // 异步
      setTimeout(() => {
        req.body = {
          a: 100,
          b: 200
        }
        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()
      })
    }
    
    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('get /api/get-post-data 路由');
      res.json({
        errno: 0,
        data: req.body
      })
    })
    
    app.use('/api', (req, res, next) => {
      console.log('处理 404');
      res.json({
        errno: -1,
        msg: '404 not fount'
      })
    })
    
    app.listen(3000, () => {
      console.log('server is running on port 3000!')
    })
  • 总结

    • 初始化代码中,各个插件的作用
    • express 如何处理路由
    • express 中间件
2. express 开发blog接口
  • 插件安装
    shell
    npm i mysql xss -S;
  • 登录
    • 使用 express-session 和 connect-redis,简单方便

      shell
      npm i express-session -S
    • req.session 保存登录信息,登录校验做成 express 中间件

      • app.js 中注册
        javascript
        const session = require('express-session');
        app.use(session({
          secret: 'ASDASD_12',
          cookie: {
            path: '/', // 默认配置
            httpOnly: true, // 默认配置
            maxAge: 24 * 60 * 60 * 1000
          }
        }));
    • ioredis connect-redis

      • 安装命令
        shell
         npm i ioredis connect-redis -S
      • 搭配 session 使用,在 app.js 中
        javascript
        // redis
        const RedisStore = require('connect-redis')(session);
        
        const redisClient = require('./db/redis');
        const sessionStore = new RedisStore({
          client: redisClient
        })
        
        app.use(session({
          secret: 'ASDASD_12',
          cookie: {
            path: '/', // 默认配置
            httpOnly: true, // 默认配置
            maxAge: 24 * 60 * 60 * 1000,
          },
          store: sessionStore
        }));
        js
        const Redis = require('ioredis');
        const { REDIS_CONF } = require('../config/db');
        
        // 创建客户端
        const redisClient = new Redis(REDIS_CONF.port, REDIS_CONF.host);
        redisClient.on('error', err => {
          console.error(err)
        });
        
        module.exports = redisClient
    • 中间件 middleware/loginCheck.js

      • loginCheck.js
        js
        const { ErrorModel } = require('../model/resModel');
        
        module.exports = (req, res, next) => {
          if (req.session.username) {
            next()
            return;
          }
          res.json(new ErrorModel('未登录'))
        }
      • 业务中应用
        js
        const express = require('express');
        const router = express.Router();
        const { getList, getDetail, newBlog, updateBlog, deleteBlog } = require('../controller/blog');
        const { SuccessModel, ErrorModel } = require('../model/resModel');
        const loginCheck = require('../middleware/loginCheck')
        
        router.get('/list', (req, res, next) => {
          const author = req.query.author || '';
          const keyword = req.query.keyword || '';
          const result = getList(author, keyword);
          return result.then(listData => {
            res.json(new SuccessModel(listData));
          })
        });
        
        router.get('/detail', (req, res, next) =>  {
          const result = getDetail(req.query.id);
          return result.then(data => {
            return new SuccessModel(data);
            res.json(new SuccessModel(data))
          })
        });
        
        router.post('/new', loginCheck, (req, res, next) => {
          req.body.author = req.session.username; // 假数据
          const result = newBlog(req.body);
          return result.then(data => {
            res.json(new SuccessModel(data))
          })
        })
        
        router.post('update', loginCheck, (req, res, next) => {
          const result = updateBlog(req.query.id, req.body);
          return result.then(data => {
            if (data) {
              res.json(new SuccessModel())
            } else {
              res.json(new ErrorModel('更新博客失败'))
            }
          })
        })
        
        router.post('/del', loginCheck, (req, res, next) => {
          const author = req.session.username; // 假数据
          const result = deleteBlog(req.query.id, author);
          return result.then(data => {
            if (data) {
              res.json(new SuccessModel())
            } else {
              res.json(new ErrorModel('删除博客失败'))
            }
          })
        })
        
        module.exports = router;
3. 日志
  • access log 记录,直接使用脚手架推荐的 morgan
  • 自定义日志使用 console.log 和 console.error 即可
  • 日志文件拆分、日志内容分析
  • express 日志拆分
    • package.json
      shell
      "scripts": {
        "start": "node ./bin/www",
        "dev": "cross-env NODE_ENV=dev nodemon ./bin/www",
        "production": "cross-env NODE_ENV=production nodemon ./bin/www"
      }
    • app.js
      javascript
      const ENV = process.env.NODE_ENV;
      if (ENV !== 'production') {
        app.use(logger('dev') );
      } else {
        app.use(logger('combined', {
          stream: process.stdout
        }) );
      }
    • 新建 logs/access.log
    • 配置格式 参考:https://github.com/expressjs/morgan
      • app.js
        javascript
        const path = require('path');
        const fs = require('fs')
        const ENV = process.env.NODE_ENV;
        if (ENV !== 'production') {
          app.use(logger('dev') );
        } else {
          const logFileName = path.resolve(__dirname, 'logs', 'access.log')
          const writeStream = fs.createWriteStream(logFileName, {
            flags: 'a'
          })
          app.use(logger('combined', {
            stream: writeStream
          }) );
        }
4. express 中间件原理
  • 分析
    • app.use 用来注册中间件,先收集起来
    • 遇到 http 请求,根据 path 和 method 判断触发哪些
    • 实现 next 机制,即上一个通过 next 触发下一个
  • 实现
    • 模拟实现
      js
      const http = require('http');
      
      const slice = Array.prototype.slice;
      
      class LikeExpress {
        constructor() {
          // 存放中间件的列表
          this.routes = {
            all: [], // app.use
            get: [], // app.get
            post: [], // app.post
          }
        }
        register(path) {
          const info = {};
          if (typeof path === 'string') {
            info.path = path;
            // 从第二个参数开始 转换为数组,存入stack
            console.log('arguments--->', JSON.stringify(arguments))
            info.stack = slice.call(arguments, 1);
          } else {
            info.path = '/';
            // 从第一个参数开始 转换为数组,存入stack
            info.stack = slice.call(arguments, 0);
          }
          return info;
        }
        use() {
          console.log('use info--->', arguments)
          const info = this.register.apply(this, arguments);
          this.routes.all.push(info)
        }
        get() {
          const info = this.register.apply(this, arguments);
          this.routes.get.push(info);
        }
        post() {
          const info = this.register.apply(this, arguments);
          this.routes.post.push(info)
        }
      
        match(method, url) {
          let stack = [];
          if (url === '/favicon.ico') {
            return stack;
          }
          // 获取 routes
          let curRoutes = [];
          curRoutes = curRoutes.concat(this.routes.all);
          curRoutes = curRoutes.concat(this.routes[method]);
      
          curRoutes.forEach(routeInfo => {
            console.log('routeInfo--->', JSON.stringify(routeInfo.path))
            console.log('url--->', url)
            if (url.trim().indexOf(routeInfo.path.trim()) === 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(routeInfo.stack)
            }
          })
          console.log('stack--->', JSON.stringify(stack))
          return stack;
        }
      
        // 核心的 next 机制
        handler(req, res, stack) {
          const next = () => {
            // 拿到第一个匹配的中间件
            const middleware = stack.shift();
            if (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.handler(req, res, resultList)
          }
        }
        listen(...args) {
          const server = http.createServer(this.callBack());
          server.listen(...args)
        }
      }
      
      module.exports = () => {
        return new LikeExpress()
      }
    • 测试
      js
      const express = require('./like-express');
      
      // 本次 http 请求的实例
      const app = express();
      
      app.use((req, res, next) =>{
        console.log('请求开始',req.method, req.url);
        next();
      })
      
      app.use(function(req, res, next) {
        req.cookie = {
          userId: '123'
        }
        next()
      })
      
      app.use(function(req, res, next) {
        // 假设处理 post data
        // 异步
        setTimeout(() => {
          req.body = {
            a: 100,
            b: 200
          }
          next();
        })
      })
      
      app.use('/api', function(req, res, next) {
        console.log('处理 /api 路由');
        next()
      })
      
      app.get('/api', function(req, res, next) {
        console.log('get /api 路由');
        next()
      })
      
      app.post('/api',function (req, res, next) {
        console.log('post /api 路由');
        next()
      })
      
      // 模拟登陆验证
      function loginCheck(req, res, next) {
        console.log('模拟登陆成功');
        setTimeout(() => {
          next()
        })
      }
      
      app.get('/api/get-cookie', loginCheck, function(req, res, next)  {
        console.log('get /api/get-cookie 路由');
        res.json({
          errno: 0,
          data: req.cookie
        })
      })
      
      app.listen(8002, function() {
        console.log('server is running on port 8002!')
      })

Released under the MIT License.