通过手写源码可以加深对express
的理解,例如路由调用、中间件的运行机制、错误捕获,功能扩展等。express官方源码
express使用
下面是初始化express
最简单的示例:
const Express = require("./express")
const app = new Express()
app.use(function(req,res,next){
req.str = "use";
next();
});
app.get("/",(req,res,next)=>{
req.str = "-get1";
next()
},(req,res,next)=>{
req.str+="-get2"
next();
});
// get 请求
app.get("/",(req,res)=>res.end(req.str))
// post 请求
app.post("/post",(req,res)=>res.end("post"))
app.listen(3000);
运行上面的示例,打开地址localhost:3000
可以看到use-get1-get2
的结果,上面的示例主要包含这几个步骤:
- 实例化一个
app
应用 app
实例包含中间件方法use
、路由get
、post
方法- 路由方法包含
路径
和回调函数
两个参数 request
和response
对象封装到回调函数里面next
方法可以决定是否向下执行app
实例包含启动和监听服务的方法listen
通过上面步骤的分析,有几点是比较重要的:
1、可以利用数组stack
保存用户设置的路由函数和中间件函数。
2、保存在stack
里面的函数应该有对应的名称或者路径方便匹配,可以封装成layer
对象(名称,路径,方法)。
3、逐一取出stack
里面的函数和请求request
匹配 (路径和方法) ,匹配成功则执行,并把下一次的执行函数next
作为参数传递。
项目结构
根据express
先创建出对应的项目结构:
/- lib
/- index.js
/- application.js
/- router
/- index.js
/- route.js
/- layer.js
初始化
/lib/index.js
初始化方法:
const Application = require("./application");
function createApplication(){
return new Application()
}
module.exports = createApplication;
/lib/application.js
内容如下:
const http = require("http")
function Application() {}
Application.prototype.listen = function () {
// 执行监听函数后,开始创建Http服务
const requestHandler = (req,res)=>{};
http.createServer(requestHandler).listen(...arguments);
}
module.exports = Application
路由
主要的方法集中在router
文件夹里面,里面包含index.js
、route.js
、layer.js
。
分析:
- 封装请求方法例如
get
、post
、delete
等,通过methods
包获取 router/index
处理app
下的路由和中间件router/route
处理路由method
下的函数router/layer
保存路径
、方法
和函数
- 每个
layer
先保存在数组stack
里面 handleRequest
取出layer
进行匹配,匹配成功则执行,否则下一个layer
匹配(封装成next
方法)
router/layer.js
// 保存路径和方法
function Layer(path, handler) {
this.path = path
this.method = null;
this.handler = handler
}
// 匹配路径和请求方法
Layer.prototype.match = function (pathname, method) {
if (!this.method) {
let path = this.path === "/" ? "/" : this.path + "/"
return pathname.startsWith(path) || this.path === pathname;
} else if (pathname === this.path && method === this.method) {
return true
}
}
module.exports = Layer
router/route.js
const methods = require("methods");
const Layer = require("./layer");
function Route(){
this.stack = [];
}
// 封装请求方法、get、post、delete、put等
methods.forEach(method=>{
Route.prototype[method] = function(handlers){
handlers.forEach(handler=>{
const layer = new Layer("/",handler);
this.stack.push(layer);
})
}
})
// 取出stack保存的方法执行
Route.prototype.handler = function (req, res, next) {
const dispatch = (index)=>{
const layer = this.stack[index++];
if(!layer) return next();
layer.handler(req,res,next);
}
dispatch(0);
}
module.exports = Route;
router/index.js
这里有一点不容易理解,Router
里面stack数组保存的layer
,包含中间件函数use
和路由route
实例,route
实例里面也有自己的stack
数组,都需要取出来匹配看是否执行:
// route 里面保存的layer
let route1 = [layer,layer,layer]
let route2 = [layer,layer,layer]
let route3 = [layer,layer,layer]
// Router里面中间件就是一个layer
let use = layer;
// Router里面的layer包含中间件和route实例
let Router = [use,route1,route2,route3]
决定是否往下执行的方法dispatch
:
const dispatch = (index)=>{
const layer = this.stack[index++];
// 不存在layer说明已经完成
if(!layer) return done();
// next 函数执行下一次的dispatch
const next = ()=>dispatch(index);
layer.handler(req,res,next);
}
dispatch(0);
router/index
内容如下:
const url = require("url");
const methods = require("methods");
const Layer = require("./layer");
const Route = require("./route");
function Router() {
this.stack = [];
}
Router.prototype.use = function(path,handler){
// 中间件函数,如果只有一个参数,重置path参数
if (typeof path === "function") {
handler = path
path = "/"
}
const layer = new Layer(path,handler);
this.stack.push(layer);
}
// 路由方法
Router.prototype.route = function (path, method) {
const route = new Route()
// layer里面的handler方法其实是route实例的handler
const layer = new Layer(path, route.handler.bind(route))
// 保存请求方法
layer.method = method
this.stack.push(layer)
return route
}
methods.forEach(method=>{
Router.prototype[method]=function(path,handlers){
const route = this.route(path,method);
// 执行route的请求方法,handlers会被保存到route实例的stack数组里面
route[method](handlers);
}
})
Router.prototype.handler=function(req,res){
// 所有layer都没有被匹配到执行done
const done = ()=>res.end(`Not Found ${req.method} ${req.url}`);
// 逐一取出layer匹配,看是否执行
const dispatch=(index)=>{
const layer = this.stack[index++];
if(!layer) return done();
const method = req.method.toLowerCase();
const {pathname} = url.parse(req.url,true);
const next = ()=>dispatch(index);
layer.match(pathname,method) ? layer.handler(req,res,next) : next();
}
dispatch(0);
}
module.exports = Router;
application.js
const http = require("http")
const methods = require("methods")
const Router = require("./router")
function Application() {}
// 路由懒加载
Application.prototype.lazy_router = function () {
if (!this.router) {
this.router = new Router()
}
}
// 绑定中间件
Application.prototype.use = function (path, handler) {
this.lazy_router()
this.router.use(path, handler)
}
// 绑定路由
methods.forEach((method) => {
Application.prototype[method] = function (path, ...handlers) {
this.lazy_router()
this.router[method](path, handlers)
}
})
Application.prototype.listen = function () {
this.lazy_router()
// 重新绑定this
const handleRequest = this.router.handler.bind(this.router)
http.createServer(handleRequest).listen(...arguments)
}
module.exports = Application
错误处理
express
可以给next
方法传参,参数代表出现错误信息,在以四个参数的中间件中,第一个参数为错误参数:
app.use(function(req,res,next){
try {
// 这里没有test方法,会被catch捕获
req.test();
} catch (error) {
next(error)
}
})
app.use(function(error,req,res,next)=>{
// 第一个参数为next传递的错误信息
// 显示结果:TypeError: req.test is not a function
res.end(error);
});
上面的示例中,req并没有test方法,直接执行会被tryCatch捕获,将错误信息传递给next,将在中间件中出现四个参数的方法里面,以第一个参数的方式被取到。
接下来就需要对中间件的处理函数dispatch
进行修改了,让它能够支持传参,并能够传递到出现四个参数的中间件方法中。
修改router/route.js
如下:
Route.prototype.handler = function (req, res, next) {
// 将累加的指针提取出来
let index = 0
// 支持传参
const dispatch = (error) => {
const layer = this.stack[index++];
// 如果中间件取完或者出现错误参数,直接跳出
if (!layer || error) return next(error)
layer.handler(req, res, (error) => dispatch(error));
}
dispatch()
}
修改router/index.js
如下:
Router.prototype.handler = function (req, res) {
let index = 0
// 保存错误信息
let errorMsg = ""
const done = () => {
if (errorMsg) {
// 如果有错误信息,并且没有中间件处理,在页面显示出来
res.statusCode = 500
res.end(`handle Error ${errorMsg}`)
} else {
res.statusCode = 404
res.end(`Not Found ${req.method} ${req.url}`)
}
}
// 支持传参
const dispatch = (error) => {
errorMsg = error
const layer = this.stack[index++]
if (!layer) return done()
const { pathname } = url.parse(req.url, true)
const method = req.method.toLowerCase()
const next = (error) => dispatch(error)
if (error) {
// 如果有错误参数,交给handleError处理
layer.handleError(error, req, res, next)
} else if (layer.match(pathname, method)) {
// 正常匹配
layer.handleRequest(req, res, next)
}else{
next(error);
}
}
dispatch()
}
同时给layer
添加上两种响应处理,修改router/layer.js
如下:
Layer.prototype.handleError = function (error, req, res, next) {
// 没有method方法,代表是中间件,有四个参数代表是错误处理中间件
if (!this.method && this.handler.length === 4) {
return this.handler(error, req, res, next)
} else {
return next(error)
}
}
Layer.prototype.handleRequest = function (req, res, next) {
// 没有四个参数的方法才处理
return this.handler.length !== 4 ? this.handler(req, res, next) : next()
}
带参数路由
express
带参数的路由可以通过request.params
获取,如下:
app.get("/article/:sort/:id",(req,res)=>{
// 访问路径 /article/nodejs/123
// req.params 可以得到结果:{"sort":"nodejs","id":"123"}
res.send(req.params)
})
先分析下如何获取路由的路径参数,最终得到我们想要的key
和value
:
- 利用正则替换匹配的路径参数,匹配结果保存到
keys
数组 - 替换后的正则匹配访问路径,获取匹配结果
values
数组 - 组合
keys
数组和valuse
数组
let path = "/get/:name/:id";
let url = "/get/chenwl/123";
let keys = [];
let regExpUrl = path.replace(/:([^\/]+)/g,function(){
keys.push(arguments[1])
return "([^/]+)"
})
let [,...values]= url.match(regExpUrl);
// keys = ["name","id"]
// values = ["chenwl","123"]
let result = keys.reduce(
(memo, current, index) => ((memo[current] = values[index]), memo),
{}
)
console.log(result) // {name:"chenwl",id:"123"}
这里使用更方便的路径正则匹配包path-to-regexp,使用起来更加方便:
const pathToRegExp = require("path-to-regexp")
let path = "/get/:name/:id"
let url = "/get/chenwl/123"
let keys = [];
let regExpUrl = pathToRegExp(path, keys);
let [,...values]=url.match(regExpUrl);
console.log(keys); // [{ name: 'name' },{ name: 'id'}]
console.log(values); // [ 'chenwl', '123' ]
这里将路径的匹配和参数的绑定放到Layer
类中,修改router/layer.js
如下:
const pathToRegexp = require("path-to-regexp")
function Layer(path, handler) {
this.path = path
this.handler = handler
// 生成正则路径,同时将参数赋值到keys
this.regexpUrl = pathToRegexp(this.path,this.keys =[]);
}
Layer.prototype.match = function (pathname, method) {
if (!this.method) {
let path = this.path === "/" ? "/" : this.path + "/"
return pathname.startsWith(path) || this.path === pathname;
} else if(method === this.method){
// 匹配正则路径
let [, ...values] = pathname.match(this.regexpUrl) || [];
// 如果有值,生成key-value的对象,绑定到this.params中
if(values.length){
let keys = this.keys.map(k=>k.name);
let params = keys.reduce((memo, current, i) => ((memo[current] = values[i]), memo),{})
this.params = params
return true;
}
// 没有匹配成功,判断路径是否相同
return pathname === this.path
}
}
Layer.prototype.handleError = function (error, req, res, next) {
if (!this.method && this.handler.length === 4) {
return this.handler(error, req, res, next)
} else {
return next(error)
}
}
Layer.prototype.handleRequest = function (req, res, next) {
// 如果有params,添加到request中
if(this.params) req.params = this.params;
return this.handler.length !== 4 ? this.handler(req, res, next) : next()
}
module.exports = Layer
app.param
利用app.param可以对路由参数重新赋值,比如下面的操作,判断用户等级,提前显示对应标识:
app.param("level", (req, res, next, value, key) => {
req.params.level = parseInt(value)
next()
})
app.param("name", (req, res, next, value, key) => {
if (req.params.level <= 2) {
req.params.name = " " + value
}
next()
})
app.get("/admin/:name/:level", (req, res) => {
const { name, level } = req.params;
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" })
res.end(`${name} level:${level}`)
})
这里需要给application.js
添加上param
方法,再交由Router
去处理:
//application.js
Application.prototype.param = function (key, handler) {
this.lazy_router()
this.router.param(key, handler)
}
router/index.js
给原型添加paramsFns
对象,保存app.params
的定义的方法:
function Router() {
this.stack = []
// {key:[handler]}
this.paramsFns = {};
}
当router.handler
里面中间件进行时,判断layer.params
是否有数据,有则先处理Router.paramsFns
里面保存的app.param
方法:
if (error) {
layer.handleError(error, req, res, next)
} else if (layer.match(pathname, method)) {
// 判断是否二级路由
if (layer.matchMiddleRouter(pathname)) {
// 截取中间件前置路由
middleRouter = layer.path
req.url = req.url.slice(middleRouter.length)
}
// layer.match 已经将匹配到的路由params绑定
if(layer.params){
// 这里再重新赋值给request对象
req.params = layer.params;
// 用handleParams先处理router.paramsFns里面的方法
this.handleParams(layer, req, res, next)
}else{
layer.handleRequest(req, res, next)
}
} else {
next(error)
}
原本Layer
里面的params
也可以去除了:
Layer.prototype.handleRequest = function (req, res, next) {
- if(this.params) req.params = this.params;
return this.handler.length !== 4 ? this.handler(req, res, next) : next()
}
接下来就是编写router.handleParams
方法:
Router.prototype.handleParams = function (layer, req, res, next) {
let stack = [];
let keys = layer.keys.map(k=>k.name);
// 取出当前layer下的所有params.key
keys.forEach(key=>{
let fns = this.paramsFns[key];
if (fns && fns.length) {
// 先保存到stack里面
fns.forEach((fn) => stack.push({ key, fn }))
}
});
let index = 0;
let done = ()=>layer.handleRequest(req, res, next);
let dispatch = ()=>{
let paramFn = stack[index++];
// 当前保存的params栈里面的方法都执行完毕再跳出继续执行后面的中间件
if(!paramFn) return done();
let {fn,key} = paramFn;
// app.param 回调函数参数:req,res,next,value,key
fn.call(this, req, res, () => dispatch(), req.params[key],key);
}
dispatch(0);
}
二级路由
express
通过Express.Router()
创建二级路由:
const user = Express.Router();
app.use("/user", user)
user.use((req, res, next) => {
req.router = "/user";
next();
});
user.get("/name", (req, res)=>{
res.end(req.router + req.url) // /user/name
});
这里可以看到,创建二级路由并没有通过new
生成router实例,而是在Router
类上直接调用原型方法。
分析:
Router
类既能实例化获取相关属性和方法,同时也是一个中间件函数- 二级路由的匹配,在中间件函数中判断
req.url
返回的路径是二级路由路径,需要截取req.url
路径- 二级路由
router.stack
里面的中间件执行完成后,再拼接回正常的req.url
路径
接下来改造router/index.js
这个构造函数,绑定新的原型链:
function Router() {
// 返回的是一个中间件函数
const router = (req, res, next) => {
// router作为中间件函数,也通过绑定原型链proto也有了Router所有的方法和属性
// 当中间件路径匹配成功,取出二级路由里面保存的stack逐一匹配,也就是执行它的handler方法
router.handler(req, res, next)
}
router.stack = []
router.paramsFns = {}
// 重新赋值新的原型链
router.__proto__ = proto
return router
}
const proto = {}
// 原型方法绑定到proto上
// proto.param
// proto.handleParams
// proto.use
// proto.route
// proto[method]
// proto.handler
修改lib/application.js
给Express
构造函数添加静态方法Router
:
const Application = require("./application");
+ const Router = require("./router");
function createApplication(){
return new Application()
}
+ createApplication.Router = Router;
module.exports = createApplication;
修改router.handler
方法:
proto.handler = function (req, res, done) {
let index = 0
let errorMsg = ""
// 二级路由的done方法其实是中间件next,如果不存在则都没有匹配到
done = done || (() => {
if (errorMsg) {
res.statusCode = 500
res.end(`handle Error ${errorMsg}`)
} else {
res.statusCode = 404
res.end(`Not Found ${req.method} ${req.url}`)
}
})
// 二级路由根路径
let middleRouter = ""
const dispatch = (error) => {
errorMsg = error
const layer = this.stack[index++]
if (!layer) return done()
const { pathname } = url.parse(req.url, true)
const method = req.method.toLowerCase()
const next = (error) => dispatch(error)
// 执行完二级路由的中间件函数后,拼接回请求的req.url路径
if (middleRouter) {
req.url = middleRouter + req.url
middleRouter = ""
}
if (error) {
layer.handleError(error, req, res, next)
} else if (layer.match(pathname, method)) {
// 判断是否二级路由
if (layer.matchMiddleRouter(pathname)) {
// 截取中间件前置路由
middleRouter = layer.path
req.url = req.url.slice(middleRouter.length)
}
layer.handleRequest(req, res, next)
} else {
next(error)
}
}
dispatch()
}
同时给Layer
类添加matchMiddleRouter
方法,判断是否二级路由:
// 判断是否二级路由
Layer.prototype.matchMiddleRouter = function(pathname){
// 中间件 && 当前路径不等于"/" && 请求路径跟当前路径不同
return !this.method && this.path !== "/" && this.path !== pathname
}
扩充方法
express
给request
和response
绑定了一些常用的熟悉和方法,这里主要实现最常用的几种:
- req.path
- req.query
- res.send
- res.sendFile
修改application.js
,添加中间价方法绑定:
Application.prototype.init = function(){
this.use((req,res)=>{
const {pathname,query}= url.parse(req.url,true);
req.path = pathname;
req.query = query;
// 发送值
res.send = function(value){
if (typeof value === "string" || Buffer.isBuffer(value)) {
res.end(value)
} else if (typeof value === "object") {
res.end(JSON.stringify(value))
}
}
// 发送文件
res.sendFile = (filename, { root }) => {
res.setHeader("Content-Type", mime.lookup(filename) + ";charset=utf-8")
fs.createReadStream(path.join(root, filename)).pipe(res)
}
next();
})
}
静态文件处理
Express
通过静态方法static
可以生成静态文件服务,给createApplication
添加静态方法:
createApplication.static = function (dirname) {
return function (req, res, next) {
let { pathname } = url.parse(req.url, true)
pathname = path.join(dirname, pathname)
fs.stat(pathname, (err, statObj) => {
if (err) return next();
if (statObj.isFile()) {
fs.createReadStream(pathname).pipe(res)
return
} else {
return next()
}
})
}
}
模板引擎和配置
在使用express
的时候,通常会先使用app.set
方法配置环境变量,如下:
// 设置环境比变量
app.set("env",process.env.NODE_ENV || "development");
//错误中间件判断当前环境,决定是否显示错误信息给用户
app.use((error,req,res,next)=>{
req.get("env")==="development" ? res.send(error) : next();
})
修改application.js
,添加配置对象和方法:
function Application() {
+ this.setting = {}
}
+ Application.prototype.set = function(key,value){
+ // 如果只有一个参数,返回value值,避免跟get方法冲突
+ if(arguments.length === 1) return this.setting[key];
+ this.setting[key] = value;
+ }
Application.prototype.init = function(){
this.use((req, res, next) => {
...
// 中间件给req添加上get方法
+ req.get = this.get.bind(this)
}
}
methods.forEach((method) => {
Application.prototype[method] = function (path, ...handlers) {
// 如果是get方法,并且只有一个参数,通过set方法可以获得对应的value值
+ if(method === "get" && arguments.length===1){
+ return this.set(path)
+ }
this.lazy_router()
this.router[method](path, handlers)
}
})
添加模板引擎,这里以ejs
为例:
function Application() {
this.setting = {
"views": "views", // 模板文件夹
"view engine": "ejs", // 渲染模板后缀
}
this.engines = {
".ejs": require("ejs").__express, // 渲染方法
}
}
// 设置渲染模板函数,例如:app.engine(".html", require("ejs").__express)
Application.prototype.engine = function (ext, rednerFn) {
this.engines[ext] = rednerFn
}
给中间价添加render
函数:
res.render = (filename, obj = {}) => {
try{
// 获取模板后缀
let extension = this.get("view engine")
// 模板文件夹
let dir = this.get("views")
// 后缀前面加上".",例如.html或者.ejs
extension = extension.includes(".") ? extension : "." + extension
// 拼接文件
let filepath = path.resolve(dir, filename + extension)
// 获取渲染函数
let renderFn = this.engines[extension]
renderFn(filepath, obj, (err, html) => {
// 渲染成功后返回
res.end(html)
})
}catch(error){
next(error);
}
}
使用方法:
app.set("view engine","html")
app.engine(".html", require("ejs").__express)
app.get("/index",(req,res)=>{
res.render("index")
})