实现简易的 express
1. 基础
最简单的服务器
const http = require('http');
const server = http.createServer(function (request, response) {
// 在这里处理请求
// 发送 HTTP 头部
// HTTP 状态值: 200 : OK
// 内容类型: text/plain
response.writeHead(200, {'Content-Type': 'text/plain'});
// 发送响应数据 "Hello World"
response.end('Hello World\n');
});
server.listen(8888);
console.log("端口: 8888");
2. 实现
2.1 第一版,基础
首先 express 最简单的服务器代码如下:
const express = require('express');
const app = express()
const port = 3000
app.get('/', function(req, res) {
res.end('You send GET request')
})
app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))
即 express 是一个函数,函数执行能生成一个 app 对象,对象上具有 listen,于是我们可以写出以下的代码: express/index.js
const http = require("http");
const createServer = () => {
// 这里取名为 app, 只是为了方便,实际上就是个处理请求的函数
const app = function (req, res) {
res.end('Response From Server')
}
app.listen = (...args) => {
const server = http.createServer(app)
server.listen(...args)
}
return app
}
module.exports = createServer;
2.2 第二版,请求方法和路径
在 express 中,能够针对请求的方法和路径,做对应的处理,比如:
const express = require('express');
const app = express()
const port = 3000
app.get('/name', function(req, res) {
res.end('name')
})
app.get('/age', function(req, res) {
res.end('9')
})
// 统配所有方法和路径
app.all('*', function(req, res) {
res.end('all response')
})
app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))
此时访问 localhost:3000/name 返回 name,访问 localhost:3000/age 返回 9,如果没有对应的路由,则会匹配到 all 这个兜底路由: 为了实现以上效果,我们需要扩展我们的 express/index.js :
const http = require("http");
const url = require('url');
const { METHODS } = http;
// METHODS 为
// [
// 'ACL', 'BIND', 'CHECKOUT',
// 'CONNECT', 'COPY', 'DELETE',
// 'GET', 'HEAD', 'LINK',
// 'LOCK', 'M-SEARCH', 'MERGE',
// 'MKACTIVITY', 'MKCALENDAR', 'MKCOL',
// 'MOVE', 'NOTIFY', 'OPTIONS',
// 'PATCH', 'POST', 'PROPFIND',
// 'PROPPATCH', 'PURGE', 'PUT',
// 'REBIND', 'REPORT', 'SEARCH',
// 'SOURCE', 'SUBSCRIBE', 'TRACE',
// 'UNBIND', 'UNLINK', 'UNLOCK',
// 'UNSUBSCRIBE'
// ]
const createServer = () => {
// 这里取名为 app, 只是为了方便,方便在函数中使用 this,因为 listen 方法也在 app 身上,实际上就是个处理请求的函数
const app = function (req, res) {
// 当请求真正到来的时候,调用先前存在的 handles,一一进行对比参数和方法,如果匹配则执行
for (let i = 0; i < app.handles.length; i++) {
const { method, path, handler } = app.handles[i];
// 取出请求的方法
const reqMethod = req.method.toLowerCase();
// 取出请求的路径
const { pathname } = url.parse(req.url, true)
// 如果方法,路径匹配,则进行处理, 同时处理 all
if( (method === reqMethod || method === 'all') && (pathname === path || path === '*')) {
handler(req, res)
}
}
// 如果前面没有 res.end, 就会执行这个默认返回
res.end('Default Response')
}
app.handles = []; // 存放 layer: (路由和中间件)
app.listen = function (...args) {
const server = http.createServer(app)
server.listen(...args)
}
// 给 app 添加 get, post 等 http 请求方法
// 每个方法其实给 handles 推送一个 layer
METHODS.forEach(method => {
method = method.toLowerCase();
app[method] = function (path, handler) {
app.handles.push({
method,
path,
handler
})
}
})
// 处理 all
app.all = function (path, handler) {
app.handles.push({
method: 'all',
path,
handler
})
}
return app
}
module.exports = createServer;
2.3 第三版,中间件
在 express 中,可以使用中间件,中间件可以在执行路由之前,对请求进行处理
const express = require('express');
const app = express()
const port = 3000
// 中间件
const middle1 = function (req, res, next) {
console.log('middle1')
// 必须使用 next 才会进入下一个中间件
next()
}
const middle2 = function (req, res, next) {
console.log('middle2')
next()
}
// 以下两种方式是一样的,即 use 没有第一个参数,则默认是 /, 即针对某个 url 使用中间件
app.use('/', middle1); // 等价于 app.use(middle1)
app.use('/', middle2);
app.get('/name', function(req, res) {
res.end('name')
})
app.get('/age', function(req, res) {
res.end('9')
})
app.all('*', function(req, res) {
res.end('all response')
})
app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))
那么继续修改我们的代码:
const http = require("http");
const url = require('url');
const { METHODS } = http;
// METHODS 为
// [
// 'ACL', 'BIND', 'CHECKOUT',
// 'CONNECT', 'COPY', 'DELETE',
// 'GET', 'HEAD', 'LINK',
// 'LOCK', 'M-SEARCH', 'MERGE',
// 'MKACTIVITY', 'MKCALENDAR', 'MKCOL',
// 'MOVE', 'NOTIFY', 'OPTIONS',
// 'PATCH', 'POST', 'PROPFIND',
// 'PROPPATCH', 'PURGE', 'PUT',
// 'REBIND', 'REPORT', 'SEARCH',
// 'SOURCE', 'SUBSCRIBE', 'TRACE',
// 'UNBIND', 'UNLINK', 'UNLOCK',
// 'UNSUBSCRIBE'
// ]
const createServer = () => {
// 这里取名为 app, 只是为了方便,实际上就是个处理请求的函数
const app = function (req, res) {
// 取出请求的方法
const reqMethod = req.method.toLowerCase();
// 取出请求的路径
const { pathname } = url.parse(req.url, true)
// 中间件中的 next 函数,迭代 handles
let index = 0;
function next() {
if(index === app.handles.length) {
// 如果最终没有匹配到任何路由或中间件,那么返回 not found
res.end('not found');
return;
}
// 每次调用 next,就取下一个 layer
const { method, path, handler } = app.handles[index++];
if (method === 'middleware') {
// 如果是中间件,且匹配到了
if(path === '/' || path === pathname || pathname.startsWidth(path + '/')) {
// 传入 next 函数
console.log('匹配到了');
handler(req, res, next);
} else {
// 没有匹配到,则调用下一个 layer
next()
}
} else {
// 如果方法,路径匹配,则进行处理, 同时处理 all
if( (method === reqMethod || method === 'all') && (pathname === path || path === '*')) {
handler(req, res)
} else {
// 如果没有找到,则调用下一个 layer
next();
}
}
}
next();
}
app.handles = []; // 存放 layer: (路由和中间件)
app.listen = function (...args) {
const server = http.createServer(app)
server.listen(...args)
}
// 给 app 添加 get, post 等 http 请求方法
// 每个方法其实给 handles 推送一个 layer
METHODS.forEach(method => {
method = method.toLowerCase();
app[method] = function (path, handler) {
app.handles.push({
method,
path,
handler
})
}
})
app.all = function (path, handler) {
app.handles.push({
method: 'all',
path,
handler
})
}
// use 方法
app.use = function(path, handler) {
// 如果不传 path,那么默认 path 为 '/'
if(typeof handler !== 'function') {
handler = path
path = '/'
}
app.handles.push({
method: 'middleware', // 表示这是一个中间件
path,
handler
})
}
return app
}
module.exports = createServer;
2.4 第四版,错误中间件
错误中间件,即当中间件使用参数调用 next() 函数时,该参数会被当做是 error,此时后续的中间件和路由都不会执行,而是直接执行错误中间件
const express = require('express');
const app = express()
const port = 3000
// 中间件
const middle1 = function (req, res, next) {
console.log('middle1')
// 必须使用 next 才会进入下一个中间件
next('12312')
}
const middle2 = function (req, res, next) {
console.log('middle2')
next()
}
// 以下两种方式是一样的,即 use 没有第一个参数,则默认是 /, 即针对某个 url 使用中间件
app.use('/', middle1); // 等价于 app.use(middle1)
app.use('/', middle2);
app.get('/name', function(req, res) {
res.end('name')
})
app.get('/age', function(req, res) {
res.end('9')
})
app.all('*', function(req, res) {
res.end('all response')
})
// 错误中间件, 约定该中间件参数为 4 个参数
app.use(function(err, req, res, next) {
console.log('error', err); // 打印 error 12312
})
app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))
那么继续改造我们的代码:
const http = require("http");
const url = require('url');
const { METHODS } = http;
// METHODS 为
// [
// 'ACL', 'BIND', 'CHECKOUT',
// 'CONNECT', 'COPY', 'DELETE',
// 'GET', 'HEAD', 'LINK',
// 'LOCK', 'M-SEARCH', 'MERGE',
// 'MKACTIVITY', 'MKCALENDAR', 'MKCOL',
// 'MOVE', 'NOTIFY', 'OPTIONS',
// 'PATCH', 'POST', 'PROPFIND',
// 'PROPPATCH', 'PURGE', 'PUT',
// 'REBIND', 'REPORT', 'SEARCH',
// 'SOURCE', 'SUBSCRIBE', 'TRACE',
// 'UNBIND', 'UNLINK', 'UNLOCK',
// 'UNSUBSCRIBE'
// ]
const createServer = () => {
// 这里取名为 app, 只是为了方便,实际上就是个处理请求的函数
const app = function (req, res) {
// 取出请求的方法
const reqMethod = req.method.toLowerCase();
// 取出请求的路径
const { pathname } = url.parse(req.url, true)
// 中间件中的 next 函数,迭代 handles
let index = 0;
function next(err) {
if(index === app.handles.length) {
// 如果最终没有匹配到任何路由或中间件,那么返回 not found
res.end('not found');
return;
}
// 每次调用 next,就取下一个 layer
const { method, path, handler } = app.handles[index++];
if(err) {
// next 有参数,则认为是错误, 那么跳过中间的中间件和路由,去到错误中间件(函数参数为4)
if(handler.length === 4) {
handler(err, req, res, next);
} else {
// 如果没有匹配到,则继续抛到下一个中间件或路由
next(err);
}
} else {
if (method === 'middleware') {
// 如果是中间件,且匹配到了
if(path === '/' || path === pathname || pathname.startsWidth(path + '/')) {
// 传入 next 函数
console.log('匹配到了');
handler(req, res, next);
} else {
// 没有匹配到,则调用下一个 layer
next()
}
} else {
// 如果方法,路径匹配,则进行处理, 同时处理 all
if( (method === reqMethod || method === 'all') && (pathname === path || path === '*')) {
handler(req, res)
} else {
// 如果没有找到,则调用下一个 layer
next();
}
}
}
}
next();
}
app.handles = []; // 存放 layer: (路由和中间件)
app.listen = function (...args) {
const server = http.createServer(app)
server.listen(...args)
}
// 给 app 添加 get, post 等 http 请求方法
// 每个方法其实给 handles 推送一个 layer
METHODS.forEach(method => {
method = method.toLowerCase();
app[method] = function (path, handler) {
app.handles.push({
method,
path,
handler
})
}
})
app.all = function (path, handler) {
app.handles.push({
method: 'all',
path,
handler
})
}
// 中间件
app.use = function(path, handler) {
// 如果不传 path,那么默认 path 为 '/'
if(typeof handler !== 'function') {
handler = path
path = '/'
}
app.handles.push({
method: 'middleware', // 表示这是一个中间件
path,
handler
})
}
return app
}
module.exports = createServer;
2.5 第五版,处理 req 的参数
express 中,对 req, res 对象进行了封装,可以便捷的得到一些参数:
const express = require('express');
const app = express()
const port = 3000
// 中间件
const middle1 = function (req, res, next) {
console.log(req.path) // 请求 http://localhost:3000/name 时,打印 /name
console.log(req.hostname) // 打印 localhost
console.log(req.query) // 打印 {}
next()
}
const middle2 = function (req, res, next) {
console.log('middle2')
next()
}
app.use('/', middle1);
app.use('/', middle2);
app.get('/name', function(req, res) {
res.end('name')
})
app.get('/age', function(req, res) {
res.end('9')
})
app.all('*', function(req, res) {
res.end('all response')
})
// 错误中间件, 约定该中间件参数为 4 个参数
app.use(function(err, req, res, next) {
console.log('error', err); // 打印 error 12312
res.end('error')
})
app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))
这是因为 express 内部有内置的中间件,通过该中间件给 req 设置了属性, 代码如下:
const http = require("http");
const url = require('url');
const { METHODS } = http;
const createServer = () => {
// 这里取名为 app, 只是为了方便,实际上就是个处理请求的函数
const app = function (req, res) {
// 取出请求的方法
const reqMethod = req.method.toLowerCase();
// 取出请求的路径
const { pathname } = url.parse(req.url, true)
// 中间件中的 next 函数,迭代 handles
let index = 0;
function next(err) {
if(index === app.handles.length) {
// 如果最终没有匹配到任何路由或中间件,那么返回 not found
res.end('not found');
return;
}
// 每次调用 next,就取下一个 layer
const { method, path, handler } = app.handles[index++];
if(err) {
// next 有参数,则认为是错误, 那么跳过中间的中间件和路由,去到错误中间件(函数参数为4)
if(handler.length === 4) {
handler(err, req, res, next);
} else {
// 如果没有匹配到,则继续抛到下一个中间件或路由
next(err);
}
} else {
if (method === 'middleware') {
// 如果是中间件,且匹配到了
if(path === '/' || path === pathname || pathname.startsWidth(path + '/')) {
// 传入 next 函数
handler(req, res, next);
} else {
// 没有匹配到,则调用下一个 layer
next()
}
} else {
// 如果方法,路径匹配,则进行处理, 同时处理 all
if( (method === reqMethod || method === 'all') && (pathname === path || path === '*')) {
handler(req, res)
} else {
// 如果没有找到,则调用下一个 layer
next();
}
}
}
}
next();
}
app.handles = []; // 存放 layer: (路由和中间件)
app.listen = function (...args) {
const server = http.createServer(app)
server.listen(...args)
}
// 给 app 添加 get, post 等 http 请求方法
// 每个方法其实给 handles 推送一个 layer
METHODS.forEach(method => {
method = method.toLowerCase();
app[method] = function (path, handler) {
app.handles.push({
method,
path,
handler
})
}
})
app.all = function (path, handler) {
app.handles.push({
method: 'all',
path,
handler
})
}
// 中间件
app.use = function(path, handler) {
// 如果不传 path,那么默认 path 为 '/'
if(typeof handler !== 'function') {
handler = path
path = '/'
}
app.handles.push({
method: 'middleware', // 表示这是一个中间件
path,
handler
})
}
// 内置中间件,设置 req
app.use(function(req, res, next) {
let { pathname, query } = url.parse(req.url, true);
let hostname = req.headers.host.split(":")[0];
req.hostname = hostname;
req.path = pathname;
req.query = query;
next();
})
return app
}
module.exports = createServer;
3. TODO
路径参数 /user/:id
子路由
res 的封装
模板的渲染
参考资料
Last updated