上一篇文章我们讲了怎么用Node.js原生API来写一个web服务器,虽然代码比较丑,但是基本功能还是有的。但是一般我们不会直接用原生API来写,而是借助框架来做,比如本文要讲的Express。通过上一篇文章的铺垫,我们可以猜测,Express其实也没有什么黑魔法,也仅仅是原生API的封装,主要是用来提供更好的扩展性,使用起来更方便,代码更优雅。本文照例会从Express的基本使用入手,然后自己手写一个Express来替代他,也就是源码解析。
本文可运行代码已经上传GitHub,拿下来一边玩代码,一边看文章效果更佳:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Node.js/Express
简单示例使用Express搭建一个最简单的Hello World也是几行代码就可以搞定,下面这个例子来源官方文档:
const express = require('express'); const app = express(); const port = 3000; app.get('http://www.likecs.com/', (req, res) => { res.send('Hello World!'); }); app.listen(port, () => { console.log(`Example app listening at :${port}`); });可以看到Express的路由可以直接用app.get这种方法来处理,比我们之前在http.createServer里面写一堆if优雅多了。我们用这种方式来改写下上一篇文章的代码:
const path = require("path"); const express = require("express"); const fs = require("fs"); const url = require("url"); const app = express(); const port = 3000; app.get("http://www.likecs.com/", (req, res) => { res.end("Hello World"); }); app.get("/api/users", (req, res) => { const resData = [ { id: 1, name: "小明", age: 18, }, { id: 2, name: "小红", age: 19, }, ]; res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify(resData)); }); app.post("/api/users", (req, res) => { let postData = ""; req.on("data", (chunk) => { postData = postData + chunk; }); req.on("end", () => { // 数据传完后往db.txt插入内容 fs.appendFile(path.join(__dirname, "db.txt"), postData, () => { res.end(postData); // 数据写完后将数据再次返回 }); }); }); app.listen(port, () => { console.log(`Server is running on :${port}/`); });Express还支持中间件,我们写个中间件来打印出每次请求的路径:
app.use((req, res, next) => { const urlObject = url.parse(req.url); const { pathname } = urlObject; console.log(`request path: ${pathname}`); next(); });Express也支持静态资源托管,不过他的API是需要指定一个文件夹来单独存放静态资源的,比如我们新建一个public文件夹来存放静态资源,使用express.static中间件配置一下就行:
app.use(express.static(path.join(__dirname, 'public')));然后就可以拿到静态资源了:
手写源码手写源码才是本文的重点,前面的不过是铺垫,本文手写的目标就是自己写一个express来替换前面用到的express api,其实就是源码解析。在开始之前,我们先来看看用到了哪些API:
express(),第一个肯定是express函数,这个运行后会返回一个app的实例,后面用的很多方法都是这个app上的。
app.listen,这个方法类似于原生的server.listen,用来启动服务器。
app.get,这是处理路由的API,类似的还有app.post等。
app.use,这是中间件的调用入口,所有中间件都要通过这个方法来调用。
express.static,这个中间件帮助我们做静态资源托管,其实是另外一个库了,叫serve-static,因为跟Express架构关系不大,本文就先不讲他的源码了。
本文所有手写代码全部参照官方源码写成,方法名和变量名尽量与官方保持一致,大家可以对照着看,写到具体的方法时我也会贴出官方源码的地址。
express()首先需要写的肯定是express(),这个方法是一切的开始,他会创建并返回一个app,这个app就是我们的web服务器。
// express.js var mixin = require('merge-descriptors'); var proto = require('./application'); // 创建web服务器的方法 function createApplication() { // 这个app方法其实就是传给http.createServer的回调函数 var app = function (req, res) { }; mixin(app, proto, false); return app; } exports = module.exports = createApplication;上述代码就是我们在运行express()的时候执行的代码,其实就是个空壳,返回的app暂时是个空函数,真正的app并没在这里,而是在proto上,从上述代码可以看出proto其实就是application.js,然后通过下面这行代码将proto上的东西都赋值给了app:
mixin(app, proto, false);这行代码用到了一个第三方库merge-descriptors,这个库总共没有几行代码,做的事情也很简单,就是将proto上面的属性挨个赋值给app,对merge-descriptors源码感兴趣的可以看这里:https://github.com/component/merge-descriptors/blob/master/index.js。