重学Node.js及其框架(Express, Koa, egg.js) 之 Express框架

644 阅读9分钟

总结自:Coderwhy老师的nodejs课程。

官网 expressjs.com/zh-cn/4x/ap…

express项目创建有两种方式:

方式一:通过express提供的脚手架,直接创建一个应用的骨架;

    // 安装express-generator
    // 安装脚手架
    npm install -g express-generator
    // 创建项目
    express express项目名
    // 安装依赖
    npm install
    // 启动项目
    node bin/www

生成的目录

.
├── app.js
├── bin
│   └── www
├── package.json
├── public
│   ├── images
│   ├── javascripts
│   └── stylesheets
│       └── style.css
├── routes
│   ├── index.js
│   └── users.js
└── views
    ├── error.pug
    ├── index.pug
    └── layout.pug

方式二:从零搭建自己的express应用结构;

npm init -y

这里我还是建议第二种方式来配置我们的express。

中间件

其实学习express就是学习中间件。

什么是中间件

中间件的本质是传递给express的一个回调函数; 这个回调函数接受三个参数:

  • 请求对象(request对象);
  • 响应对象(response对象);
  • next函数(在express中定义的用于执行下一个中间件的函数);
  • 并且中间件不调用next(),那么它将只会调用第一个匹配的中间件。

中间件可以做哪些事情

  • 执行任何代码;
  • 更改请求(request)和响应(response)对象;
  • 结束请求-响应周期(返回数据);
  • 调用栈中的下一个中间件;

如果当前中间件功能没有结束请求-响应周期,则必须调用next()将控制权传递给下一个中间件功能,否则,请求将被挂起。 如果next方法传递了参数,那么它将不会调用下一个匹配到的中间件,而是直接匹配到传递了接受错误的中间件。如果没有找到接受错误的中间件,那么它将直接终止程序。

中间件这么牛逼,那如何使用它呢?

中间件的使用方式

  • 最普通的中间件 通过app.use((req, res, next) => {})来使用。
app.use((req, res, next) => {
  console.log("注册了第01个普通的中间件~");
  next();
});
  • path匹配中间件 通过app.use(路径, (req, res, next) => {})来使用。
    // 路径匹配的中间件
    app.use('/index', (req, res, next) => {
      console.log("index middleware 01");
    });
  • path和method匹配中间件 通过app.[mothod](路径, (req, res, next) => {})来使用。
    app.get('/index', (req, res, next) => {
      console.log("index path and method middleware01");
    });

    app.post('/login', (req, res, next) => {
      console.log("login path and method middleware01");
    })
  • 注册多个中间件 每个注册中间件的方式都可以注册多个中间件。
    app.get("/index", (req, res, next) => {
      console.log("index path and method middleware 02");
      next();
    }, (req, res, next) => {
      console.log("index path and method middleware 03");
      next();
    }, (req, res, next) => {
      console.log("index path and method middleware 04");
      res.end("index page");
    });

不管中间件是什么类型的,他都是从前往后匹配的,没有调用next方法,即使匹配到后面的中间件也不会执行。如果一个匹配到的中间件结束了响应,调用了next他会继续执行下一个中间件,没有调用next他将结束响应。

特别需要注意的是,当多个匹配的中间件同时设置了响应和调用了next,该请求响应的结果为最先发出的响应。并且终端会报错。一般我们也不会这样干,只会在最后一个被匹配到的中间件设置响应。

    app.use((req, res, next) => {
      console.log("common middleware01");
      next();
    })

    // 路径匹配的中间件
    app.use('/home', (req, res, next) => {
      console.log("home middleware 01");
      next()
      res.send("=====")
    });

    // 中间插入了一个普通的中间件
    app.use((req, res, next) => {
      console.log("common middleware02");
      res.send("+++++")
      next();
    })

    app.use('/home', (req, res, next) => {
      console.log("home middleware 02");
      res.send("----------")
    });

上面事例,最终响应的结果是+++++。 image.png

解析请求传递的参数

解析params参数

我们直接通过req.params就可以获取到。

    app.get('/index/:id/:name', (req, res, next) => {
      console.log(req.params);
      res.end("参数获取成功~");
    })

解析query参数

我们直接通过req.query就可以获取到。

    app.get('/login', (req, res, next) => {
      console.log(req.query);
      res.end("用户登录成功~");
    })

解析请求中的json格式数据

调用express中内置的方法express.json()。该方法将返回一个函数。并将解析后的参数放在req.body上。

    app.use(express.json())
    app.post("/login", (req, res, next) => {
      console.log("login-body", req.body)
      // 注意end方法只能返回字符串类型或者是buffer类型
      res.end(JSON.stringify(req.body))
      next()
    })

解析请求中的x-www-from-urlencoded格式数据

调用express中内置的方法express.urlencoded({ extended: true })。该方法将返回一个函数。并将解析后的参数放在req.body

  • 如果extended为true,表示它将采取第三方的qs库解析请求的body
  • 如果extended是false,表示他将采取node的querystring模块解析请求的body
    app.use(express.urlencoded({
      extended: true
    }))
    app.post("/register", (req, res, next) => {
      console.log("register-body", req.body)
      res.end(JSON.stringify(req.body))
      next()
    })

解析请求中的form-data格式数据

这个express没有提供内置的中间件。我们需要安装multer库来帮助我们解析。不要将它作为全局中间件来使用,上传文件和费文件的form-data格式的数据,可能会产生冲突。

github.com/expressjs/m…

解析非文件的表单数据

    const multer = require("multer");
    const upload = multer();
    // 将它作为全局中间件时,它将会产生冲突,不能正确的解析文件上传。所以不要将他作为全局中间件使用
    // app.use(upload.any())// 然后他就会将form表单非文件的数据解析成对象,绑定到req.body上
    app.post('/login', upload.any(), (req, res, next) => {
      console.log("login", req.body);
      res.end("用户登录成功~")
    });

解析文件的表单数据

我们可以指定上传的文件名和后缀。也可以让系统自动分配文件名。

  • any方法不能作为全局的中间件,他会和single,array等方法冲突。
  • array方法第二个参数可以指定上传文件的最大数量。
  • fields方法表示传递不同字段的多文件。
    [
      { name: 'avatar', maxCount: 1 },
      { name: 'gallery', maxCount: 8 }
    ]
    // 方式一
    // 自定义上传文件的文件名和格式
    const storage = multer.diskStorage({
      destination: (req, file, cb) => {
        cb(null, './uploads/');
      },
      filename: (req, file, cb) => {
        cb(null, Date.now() + path.extname(file.originalname));
      }
    })
    
    const upload = multer({
      storage
    });

    // 方式二
    // 只提供文件上传的输出位置,文件名随机。
    const upload = multer({
       dest: './uploads/'
    });
    
    // 上传多个文件。注意文件上传的name都必须为file
    app.post('/uploadmore', upload.array('file'), (req, res, next) => {
      console.log(req.files);
      res.end("文件上传成功~");
    });
    
    // 上传单个文件。注意文件上传的name都必须为file
    app.post('/uploadsingle', upload.single('file'), (req, res, next) => {
      console.log(req.file);
      res.end("文件上传成功~");
    });
    // 上传多个文件。并且可以指定不同的字段名。
    app.post('/upload', upload.fields([
      {
      name: "file"
    },{
      name: "avatar"
    }
    ]), (req, res, next) => {
      console.log(req.files);
      res.end("文件上传成功~");
    });

image.png image.png

image.png

响应请求

详细请求,请访问 expressjs.com/zh-cn/api.h…

  • end方法。类似于http中的response.end方法,用法是一致的。
    res.end(返回的数据); // 只能是字符串或者buffr类型
  • json方法。json方法中可以传入很多的类型:object、array、string、boolean、number、null等,它们会被转换成json格式返回;
res.json(返回的数据)
  • status方法。用于设置状态码:
    res.status(200);
  • get方法。获取请求头中指定的字段
    res.get("Content-Type");
  • type方法。用来设置返回值mime类型的。
    res.type("application/json");

express路由

为什么要使用路由?

如果我们将所有的代码逻辑都写在app中,那么app会变得越来越复杂:

  • 一方面完整的Web服务器包含非常多的处理逻辑;

  • 另一方面有些处理逻辑其实是一个整体,我们应该将它们放在一起:比如对users相关的处理

    • 获取用户列表;get

    • 获取某一个用户信息;get/:id

    • 创建一个新的用户;post (body携带参数)

    • 删除一个用户;delete/:id

    • 更新一个用户;patch/:id

创建路由

通过express.Router()来创建一个路由实例。

   // userRouter.js。用户相关的路由
   const express = require('express');

   const userRouter = express.Router();
   
   userRouter.get('/', (req, res, next) => {
     res.json(["zh", "llm"]);
   });

   userRouter.get('/:id', (req, res, next) => {
     res.json(`${req.params.id}用户的信息`);
   });

   userRouter.post('/', (req, res, next) => {
     res.json("create user success~");
   });
   
   module.exports = userRouter;

   // index.js
   const express = require('express');
   const userRouter = require('./router/users.js');

   const app = express();
   // 使用路由,并且统一路由路径。
   app.use("/users", userRouter);

   app.listen(8000, () => {
     console.log("路由服务器启动成功~");
   });

部署静态资源

Node也可以作为静态资源服务器,并且express给我们提供了方便部署静态资源的方法;

通过express.static()返回一个中间件。

  • 向 express.static 函数提供的路径相对于您在其中启动 node 进程的目录。如果从另一个目录运行 Express 应用程序,那么对于提供资源的目录使用绝对路径会更安全。
  • 可以为静态文件创建虚拟路径。路径并不实际存在于文件系统中。
    app.use("/static", express.static(path.resolve(__dirname, 'uploads')));

上面表示可以访问具有 /static 路径前缀的 uploads 目录中的文件。访问的路径不需要带uploads路径,直接写uploads文件夹下的文件和文件夹即可。

    const express = require('express');
    const path = require("path")
    const app = express();
    
    // 部署静态文件,我们就可以访问项目下的static中的任何文件了
    app.use(express.static(path.resolve(__dirname, "static")));

    app.listen(8000, () => {
      console.log("路由服务器启动成功~");
    });

错误处理

错误处理中间件始终采用四个自变量。必须提供四个自变量,以将函数标识为错误处理中间件函数。即使无需使用 next 对象,也必须指定该对象以保持特征符的有效性。否则,next 对象将被解释为常规中间件,从而无法处理错误。

调用next(new Error("传递错误信息"))如果next中传入参数,那么将是错误参数,通过app.use((err, req, res, next) => {})来对错误做统一处理。 我们会将错误信息定义成常量,然后做处理。

    app.use((err, req, res, next) => {
      let status = 400;
      let message = "";

      switch(err.message) {
        case 错误信息对应的常量:
          message = "错误信息对应的常量";
          break;
        case 错误信息对应的常量:
          message = "错误信息对应的常量"
          break;
        default: 
          message = "NOT FOUND~"
      }

      res.status(status);
      res.json({
        errCode: status,
        errMessage: message
      })
    })

需要注意的是,这个中间件主要是为了处理错误,所以需要获取到错误信息,所以应该充当最后一个中间件函数。

express源码分析

image.png

调用express(),他其实就是调用createApplication(),这个函数返回一个app。并且这个app就是一个中间件调用函数。

var app = function(req, res, next) {
 app.handle(req, res, next);
};

app.listen()函数调用了一下原生的createServer().listen()方法,并传入参数。

app.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};

调用app.use()。通过判断,传入的中间件是否为一个函数,如果是,则会将所有中间件传入到一个fns数组中。不是则将报错。

var fns = flatten(slice.call(arguments, offset));
​
if (fns.length === 0) {
 throw new TypeError('app.use() requires a middleware function')
}

然后遍历这个fns数组,调用router.use()。然后再其中创建一个layer对象,并且将fn传递进去。然后将layer对象加入到一个stack数组中。

var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);
​
layer.route = undefined;
​
this.stack.push(layer);

当用户发送请求,我们可以通过app.listen()方法可以看出底层调用app.handle(),其中又调用router.handle()。其中内部不断判断匹配到的中间件函数,然后调用layer.handle_request。

if (route) {
return layer.handle_request(req, res, next);
}

然后内部调用中间件函数。并且传入req, res, next。然后在router.handle()中调用next方法。继续判断下一个中间件。

Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle;
​
if (fn.length > 3) {
 // not a standard request handler
 return next();
}
​
try {
 fn(req, res, next);
} catch (err) {
 next(err);
}
};