前言
在第一第二节中,我们讲述了不少有关于 Express 的用法,但是对它的细节或原理避而不谈。那么今天我们就来实现自己的一个 Express ,加深对 Express 的理解。不力求完全实现 Express 的所有代码,但求实现它的核心逻辑。
准备工作
那我们先来创建一个 my-express 文件夹,npm init添入一些基本信息。再来捋一捋我们的需求,一步一步地去实现。
- 可以解析不同路径的路由
- 中间件逻辑处理
- 模板文件处理,手写模板引擎
- 处理静态文件访问逻辑
有了这几点之后,我们就可以分步去解决这个问题了。
路由处理
首先抛开 Express 不谈,我们先来看看 Node.js 是如何创建一个服务器的,毕竟我们是基于 Node.js 来做一些上层的封装。
http.createServer()
Node.js 中创建一个服务器十分简单,只有下面寥寥数行代码:
var http = require('http');
http.createServer(function (request, response) {
response.end('Hello World');
}).listen(3000);
创建完一个服务器后,我们应该处理不同路由的逻辑。新建一个express.js文件,主要编写我们的源码逻辑,再新建一个app.js,主要编写业务测试逻辑。
- 创建一个监听函数
app,在这个监听函数中会有两个参数req和res,这两个参数都是createServer提供给我们的,用于处理请求的逻辑参数和返回的逻辑参数 app.js中调用一下listen方法,咱们的服务器就跑起来了
//express.js
const http = require('http')
const url = require('url')
function express() {
function app(req, res) {
}
app.listen = function () {
let server = http.createServer(app)
server.listen(...arguments)
}
return app
}
module.exports = express
//app.js
var express = require('./express')
var app = express()
app.listen(3000)
解析请求方法和路径
我们可以通过如下方法来获取请求的方法和路径
let _method = req.method.toLowerCase()
let {
pathname
} = url.parse(req.url, true)
中间件
在没有路由分区之前, Express 可以通过如下方式来注册一个路由,即用中间件方法use。
app.use('/user/get',function(req,res,next){
})
实现这种方式之前,思考一下我们在开发的时候,所有的路由都是已经写好的,然后程序跑起来的时候再根据方法与路径来匹配对应的路由。所以app.use应当是一个注册的方法,它可以注册路由,也可以注册中间件,而广义来说 Express 中的路由就是中间件的一种。
app.routes = [];
app.use = function (path, handler) {
if (typeof handler !== 'function') {
//这是一个中间件函数,所有的路由都应该匹配到
handler = path
path = '/'
}
let layer = {
//这是一个普通的路由中间件
method: 'middleware',//method中间件
path,
handler,
}
app.routes.push(layer)
}
将路由存起来之后,应该有一个调度方法。也就是我们在 Express 开发的时候经常看到的 next 参数,这个调度方法笔者在面试字节的时候也被让手写过,我们一起来看看它的实现:
- 遍历
routes数组 - 如果是中间件,则判断路径是否匹配,匹配则执行,不匹配则继续往下遍历
- 从这里也可以看出,中间件调用时必须调用
next方法,不然的话下面的逻辑就不会继续走了
let index = 0
function next() {
let index = 0
function next() {
if (index === app.routes.length) {
//匹配完了没有匹配到
res.end(`Cannot ${_method} ${pathname}`)
}
let {
method,
path,
handler
} = app.routes[index++] // 每次调用next就应该取出下一个layer
//如果是中间件,则判断是否使用走这个中间件的逻辑
if (method === 'middleware') {
if (path === '/' || path === pathname || pathname.startsWith(path + '/')) {
handler(req, res, next)
} else {
next() //没有匹配到当前中间件 继续往下迭代
}
} else {
//处理路由
if ((_method === method || method === 'all') && (path === pathname || path === '*')) {
handler(req, res)
} else {
next()
}
}
}
next()
}
next()
路由
在实现了中间件的访问逻辑后,接下来就要使用路由方法了。 Express 中如下使用一个路由
//app.js
var userRouter = require('./routes/userRouter')
app.use('/users',userRouter)
// /routes/userRouter.js
var express = require('express');
var router = express.Router();
/* GET users listing. */
router.get('/', function (req, res, next) {
res.send('respond with a resource');
});
也就是我们先实现一个 Router 方法,实现路由分区
- 把用户定义的路由都收集进
routes数组 - 然后给
router对象加入所有http请求的method app.use方法中注册路由,把所有的路由最终都是推进app.routes数组中遍历
//express.js
express.Router = function () {
let router = {
routes: []
}
http.METHODS.forEach(method => {
method = method.toLowerCase()
router[method] = function (path, handler) {
let layer = {
method,
path,
handler
}
router.routes.push(layer)
}
})
router.all = function (path, handler) {
let layer = {
method: 'all', //method是all 全部匹配
path,
handler
}
app.routes.push(layer)
}
return router
}
//......
app.use = function (path, handler) {
if (path && Object.prototype.toString.call(handler) == '[object Object]') {
let basePath = path,
routes = handler.routes
routes.forEach(item => {
let {
method,
path,
handler
} = item
let layer = {
method,
path: basePath + path,
handler
}
app.routes.push(layer)
})
} else {
//......
}
}
处理参数
在编写好路由之后,咱们还得处理一下用户传进来的参数。这里统一封装一下,加入 req 对象中。
await getRequest(req)
function getRequest(req) {
return new Promise((resolve, reject) => {
//get参数
req.query = url.parse(req.url, true).query
//post参数
let data = ''
req.on('data', function (chunk) {
data += chunk;
});
req.on('end', function () {
data = decodeURI(data);
var dataObject = querystring.parse(data);
req.body = dataObject
resolve()
});
})
}
至此,我们完成了中间件和路由的编写。
模板文件处理
接下来咱们处理 MVC 框架中的视图层,即 Views 模板文件。还记得第一节中第一次打开 Express 示例代码时的那个页面吗?那就是渲染的一个视图文件。根目录下新建一个 views 文件夹。然后新建一个 index.tpl 文件,这就是我们专属的文件名后缀hh。先写入一些示例代码:
// routes/users.js
router.get('/my', (req, res, next) => {
res.render('index', {
title: 'my',
user: {
name: 'my-express'
}
})
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title%></title>
</head>
<body>
<% if (user) { %>
<h2><%= user.name %></h2>
<% } %>
</body>
</html>
我们希望:用户用浏览器访问 localhost:3000/users/my 时,渲染 index.tpl 这个模板,并把 render 方法里面的参数都展示出来。同时约定:表达式写法为 <% %>。让我们先来实现 render 方法
render
render方法主要思路如下:
- 在
res参数中注入render方法 - 调用了
render方法,则读取静态文件,最后吐出一个页面
function render(filePath,title, options) {
if (filePath[0] == '/') filePath.splice(0, 1)
fs.readFile(`./views/${filePath}.tpl`, (err, data) => {
if (err) {
throw new Error(err)
} else {
let buffer = compile(data, options)//编译模板
res.write(data)
res.end()
}
})
}
这样吐出来的页面时没有经过编译的,也就是我们还需要一个编译模板的方法,将传入的值替换到html中。这里直接取一个业界比较出名的模板引擎来处理-EJS。EJS的文档请参阅ejs文档。
安装 EJS :npm install ejs --save。
接下来直接使用 EJS 的 render 方法把我们写的模板字符串转换为 HTML 字符串即可。
function compile(tpl, options) {
tpl = tpl.toString();
let str = ejs.render(tpl, options)
return Buffer.from(str)
}
到这里,咱们的模块解析也完成了。
静态文件处理
最后。咱们的框架还差一个静态资源访问的逻辑。咱们来处理一下
- 为了避免一些冲突,简约编码,咱们就不像
Express内部那么处理了,规定路由为localhost:3000/static/xxx - 获取到文件后吐出即可,像模板读取那样
- 特殊处理
static的逻辑
function getStatic(pathname, res) {
let _path = pathname.slice(1).split('/')
if (_path[0] == 'static') {
let filePath = './' + _path.join('/')
fs.readFile(filePath, (err, data) => {
res.write(data)
res.end()
})
}
}
最后
至此,玩转Express系列就到处结束了。在实现自己的一个 Express 的时候,可能还是有些差强人意,比如说代码的封装性、异常的处理等等。但是我们一开始的目的可能不是去造一个一模一样的生成环境能用的轮子,而是希望能够去手写一些核心的逻辑,以小见大地去学习一个 MVC 框架的核心思想。
行文至此,感谢阅读,如果您喜欢的话,可以帮忙点个like哟~