nodejs篇-手写express

码农天地 -
nodejs篇-手写express

通过手写源码可以加深对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、路由getpost方法路由方法包含路径回调函数两个参数requestresponse对象封装到回调函数里面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.jsroute.jslayer.js

分析:

封装请求方法例如getpostdelete等,通过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)
})

先分析下如何获取路由的路径参数,最终得到我们想要的keyvalue

利用正则替换匹配的路径参数,匹配结果保存到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.jsExpress构造函数添加静态方法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
}
扩充方法

expressrequestresponse绑定了一些常用的熟悉和方法,这里主要实现最常用的几种:

req.pathreq.queryres.sendres.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")
})
特别申明:本文内容来源网络,版权归原作者所有,如有侵权请立即与我们联系(cy198701067573@163.com),我们将及时处理。

Tags 标签

加个好友,技术交流

1628738909466805.jpg