玩转Express(三)实现你的Express

简介: 玩转Express(三)实现你的Express

前言

在第一第二节中,我们讲述了不少有关于 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 ,在这个监听函数中会有两个参数 reqres ,这两个参数都是 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文档。

安装 EJSnpm install ejs --save

接下来直接使用 EJSrender 方法把我们写的模板字符串转换为 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哟~


相关文章
|
4月前
|
Web App开发 JSON 中间件
express学习 - (3)express 路由
express学习 - (3)express 路由
74 1
|
9月前
|
开发框架 JavaScript 前端开发
koa和express有哪些不同?
koa和express有哪些不同?
67 1
|
5月前
|
存储 JavaScript 中间件
|
7月前
|
前端开发 中间件
86 # express 基本实现
86 # express 基本实现
24 0
|
9月前
|
开发框架 JavaScript 前端开发
Express
Express 是一个基于 Node.js 的快速、简洁、灵活的 Web 应用开发框架。它提供了一系列强大的功能,帮助开发者快速构建各种 Web 应用。Express 的原理是利用 Node.js 内置的 http 模块,通过中间件和路由等功能,实现Web应用的开发。
103 1
|
9月前
|
开发框架 JavaScript 前端开发
|
9月前
|
JSON JavaScript 前端开发
使用express-generator生成express应用
使用express-generator生成express应用
36 0
|
前端开发 JavaScript 中间件
express VS koa
express 出来的时候,js 还是处于混沌期间,es6的标准没有出来。而node的事件处理的方式都是基于 cb(callback) 的这种形式,在当时来看,这种方式是比较好的一种方式。所以express 里面的中间件处理的方式基本上都是回调。es6 的出现,带来了许多新的标准。使得express 不得不考虑需要兼容es6中的语法, 而 es6中处理异步的方式是promise,还有后面陆续的 async 和 await 等语法糖。(express 不是说不能使用这些语法糖,他使用第三方的库是可以解决的,只是没有koa那么优雅)
express VS koa
|
JavaScript 前端开发
Express基础(中)
Express基础(中)
98 0
Express基础(中)