githtml
const express = require('express') // 導出的是一個函數
const app = express()
app.use('/', (req, res, next) => {
// 在中間件中處理公共路徑
next()
})
// 內部不會將回調函數包裝成promise,內部集成了路由
app.get('/', (req, res)=> {
res.end('ok')
})
app.listen(3000, () => {
console.log('start')
})
複製代碼
// 主要代碼
// 'application.js'
const http = require('http')
const Router = require('./router')
/*
@ 在初始化application的時候,須要建立一個router實例,中間的請求處理都讓router去完成
@*/
function Application() {
// 在建立應用的時候,初始化一個router
this._router = new Router()
}
Application.prototype.get = function (path, ...handlers) {
this._router.get(path, handlers) //
},
Application.prototype.listen = function (...arg) {
const server = http.createServer((req, res) => {
function done() {
// 若是router處理不了,直接執行done
res.end('not xx `Found')
}
this._router.handle(req, res, done)
})
server.listen(...arg)
}
module.exports = Application
// 'layer.js'
function Layer(path, handle) {
this.path = path
this.handle = handle
}
module.exports = Layer
// 'route.js'
const Layer = require("./layer");
function Route() {
// route也有一個stack,存放內層的layer(方法類型與用戶傳入函數的對應)
this.stack = []
}
/*
@ route有一個diapatch屬性,調用一次執行內層的stack
@ 還有一個get方法,用來標記內層layer函數與方法類型的對應關係
*/
Route.prototype.dispatch = function(req, res, out) {
let idx = 0
const next = () => {
if (idx >= this.stack.length) return out()
const routeLayer = this.stack[idx++]
// 比較方法
if (routeLayer.method === req.method.toLowerCase()) {
routeLayer.handle(req, res, next)
} else {
next()
}
}
next()
}
Route.prototype.get = function(handles) {
handles.forEach(handle => {
const layer = new Layer('', handle)
layer.method = 'get'
this.stack.push(layer)
});
}
module.exports = Route
// 'index.js'
/*
@router有route屬性和layer屬性,layer存路徑和route.dispatch的對應關係,route存放用戶真實的回調
@ router是真個路由系統,route是一條條的路由
@ 在express的路由系統中,又一個外層的棧,存放着路徑與回調函數的對應關係,這個回調函數是route的diapatch,
@ 當調用外層的handle時,會讓route.dispatch執行,dispatch找到route中存放的layer,根據方法進行匹配,一次執行
@ 內層的layer存放的是方法類型與真實回調的對應關係
*/
const Layer = require('./layer')
const Route = require('./route')
const url = require('url')
function Router() {
this.stack = []
}
Router.prototype.route = function (path) {
const route = new Route() // 建立route
const layer = new Layer(path, route.dispatch.bind(route)) // 建立外層layer
layer.route = route // 爲layer設置route屬性
// 將layer存放到路由系統的stack中
this.stack.push(layer)
return route
}
// router中有get方法與handle放法
Router.prototype.get = function (path, handlers) {
// 當調用get時咱們建立一個layer,給他添加對應關係
// 建立對應關係
const route = this.route(path)
// 讓route調用get方法標記route中的每一個layer是何種方法
route.get(handlers)
}
Router.prototype.handle = function (req, res, done) {
/*
@ 當有請求過來時,咱們先拿到path找到外層的layer,而後執行他的handle,讓內層的route,根據method依次執行內層的layer的handle
@
*/
let { pathname } = url.parse(req.url)
let idx = 0
let next = () => {
if (idx >= this.stack.length) return done() // 遍歷完後仍是沒找到,那就直接走出路由系統便可
let layer = this.stack[idx++]
// 須要查看 layer上的path 和 當前請求的路徑是否一致,若是一致調用dispatch方法
if (layer.path === pathname) {
// 路徑匹配到了 須要讓layer上對應的dispatch執行
layer.handle(req, res, next) // 將遍歷路由系統中下一層的方法傳入
} else {
next()
}
}
next()
}
module.exports = Router
複製代碼
function Application() {
// 在建立應用的時候,初始化一個router
}
Application.prototype.lazy_router = function() {
if (!this._router) {
this._router = new Router() // 在調用了對應的方法或者listen的建立路由
}
}
methods.forEach(method => {
Application.prototype[method] = function (path, ...handlers) {
this.lazy_router()
...
}
})
Application.prototype.listen = function (...arg) {
const server = http.createServer((req, res) => {
this.lazy_router() // 實現路由的懶加載
...
}
module.exports = Application
複製代碼
咱們在拿到一個請求的路徑和方法後,先去外層layer匹配path,path匹配以後,在去內層匹配methods,這就存在內層匹配不到對應的方法,形成浪費,所以咱們在外層layer的route屬性中加一個methods屬性,這個屬性包括route中全部存在的方法,在當匹配到外層,先去methods映射去找是否存在,若是存在就觸發route.dispatch,不存在則直接跳過,匹配下一個layergit
// index.js
Router.prototype.handle = function (req, res, done) {
...
let next = () => {
...
// 須要查看 layer上的path 和 當前請求的路徑是否一致,若是一致調用dispatch方法
if (layer.match(pathname)) {
// 路徑匹配到了 須要讓layer上對應的dispatch執行
if (layer.route.methods[req.method.toLowerCase()]) {
layer.handle(req, res, next) // 將遍歷路由系統中下一層的方法傳入
} else {
next()
}
} else {
next()
}
}
next()
}
module.exports = Router
// route.js
function Route() {
...
this.methods = {}
}
...無用的代碼省略
methods.forEach(method => {
Route.prototype[method] = function(handles) {
handles.forEach(handle => {
const layer = new Layer('', handle)
this.methods[method] = true
...
});
}
})
module.exports = Route
複製代碼
...
app.get('/', function(req, res, next) {
console.log(1)
setTimeout(() => {
next();
console.log('xxx')
}, 6000);
}, function(req, res, next) {
next();
console.log(11)
}, function(req, res, next) {
next();
console.log(111);
})
app.get('/', function(req, res, next) {
console.log('2');
res.end('end')
})
app.post('/', function(req, res, next) {
res.end('post ok')
})
app.listen(3001, () => {
console.log('server start 3000');
})
// 1, 2, 111, 11,xxx
解析: 洋蔥模型,其實就是從外面一層層的深刻,再一層層的穿出來,中間件的執行順序嚴格遵循洋蔥模型,next能夠理解爲先執行下一個中間件,next以後的代碼會在執行完下一個中間件再回來執行
上述代碼的輸出:
+ 1輸出毫無疑問,
+ next在setTimeOut中等待,到時間後先進入下一個中間件,在下一個中間件中next在輸出之前,繼續先執行下一個回調,輸出2
+ 再回到上一層,輸出111
+ 回到上一層輸出11
+ 回到最開始的那一層,輸出xxx
複製代碼
// + 第一個參數,匹配以path開頭的路徑 ,第二個參數是回調
+ 中間件不匹配方法,只匹配路徑
app.use('/', (req, res, next) => {
// todo
})
複製代碼
// application.js
Application.prototype.use = function() {
// 中間件的實現邏輯,只要用到路由就要判斷一次路由是否加載
this.lazy_router()
// 將邏輯交給路由系統中實現
this._router.use(...arguments)
}
// index.js
...
Router.prototype.use = function(path, ...handles) {
if(typeof path == 'function') {
// 只傳遞了一種參數
handles.unshift(path)
path = '/'
}
// 遍歷handles建立layer
handles.forEach(handle => {
let layer = new Layer(path, handle)
// 對於中間件,沒有route屬性,爲了區分,設置route屬性爲undeinfed,而且把這個layer放入到路由系統中
layer.route = undefined
this.stack.push(layer)
})
}
Router.prototype.handle = function (req, res, done) {
let { pathname } = url.parse(req.url)
let idx = 0
let next = () => {
...
if (layer.match(pathname)) {
// 當匹配到路徑的後,可能爲中間件,可能爲路由
if (!layer.route) { // 判斷是不是中間件
// 中間件的話,直接去執行
layer.handle_request(req, res, next)
} else {
// 路由
if (layer.route.methods[req.method.toLowerCase()]) {
layer.handle(req, res, next) // 將遍歷路由系統中下一層的方法傳入
} else {
next()
}
}
// 路徑匹配到了 須要讓layer上對應的dispatch執行
} else {
next()
}
}
next()
}
// layer.js
function Layer(path, handle) {
this.path = path
this.handle = handle
}
Layer.prototype.match = function (pathname) {
// 路由和中間件的匹配規則不一樣
if (this.path === pathname) return true
if (!this.route) {
// 中間件
if (this.path === '/') return true
return pathname.startsWith(this.path + '/')
}
return false
}
Layer.prototype.handle_request = function(req, res, next) {
this.handle(req, res, next)
}
module.exports = Layer
複製代碼
const express = require('express');
const app = express();
app.use('/', (req, res, next) => {
// todo
console.log(1)
next('出錯了')
})
app.use('/', (req, res, next) => {
// todo
console.log(2)
next()
})
app.get('/', function(req, res, next) {
console.log(1)
setTimeout(() => {
next();
console.log('xxx')
}, 10000);
}, function(req, res, next) {
next();
console.log(11)
}, function(req, res, next) {
next();
console.log(111);
})
複製代碼
// route.js
...
Route.prototype.dispatch = function(req, res, out) {
let idx = 0
const next = (err) => {
if(err) { return out(err)} // out就是外層的next,若是內層有錯誤,直接跳出
if (idx >= this.stack.length) return out()
const routeLayer = this.stack[idx++]
// 比較方法
if (routeLayer.method === req.method.toLowerCase()) {
routeLayer.handle(req, res, next)
} else {
next()
}
}
next()
}
...
// index.js
Router.prototype.handle = function (req, res, done) {
/*
@ 當有請求過來時,咱們先拿到path找到外層的layer,而後執行他的handle,讓內層的route,根據method依次執行內層的layer的handle
@
*/
let { pathname } = url.parse(req.url)
let idx = 0
let next = (err) => {
// 在這裏進行統一的監聽
if (idx >= this.stack.length) return done() // 遍歷完後仍是沒找到,那就直接走出路由系統便可
let layer = this.stack[idx++]
if (err) { //錯誤處理。通常放在最後
// 進入到錯誤能夠是中間件也能夠是路由
if (!layer.route) {
// 找中間件
layer.handle_error(err, req, res, next)
} else {
// 路由,繼續攜帶錯誤信息匹配中間件
next(err)
}
} else {
if (layer.match(pathname)) {
// 當匹配到路徑的後,可能爲中間件,可能爲路由
if (!layer.route) {
// 中間件的化,直接去執行
// 排除錯誤中間件
if (layer.handle.length !== 4) {
layer.handle_request(req, res, next) // 普通中間件
} else {
// 是錯誤中間件,跳出
next()
}
} else {
...
}
} else {
next()
}
}
}
next()
}
// layer.js
...
Layer.prototype.handle_error = function(err, req, res, next) {
// 找到錯誤處理中間件所在的layer,若是不是帶着錯誤信息繼續next
if(this.handle.length === 4) {
// 錯誤處理中間件,讓handle執行
return this.handle(err, req, res, next)
}
next(err) // 普通的中間件
}
...
測試代碼
server.js
app.use('/', (req, res, next) => {
// todo
console.log(1)
next()
})
app.use('/', (req, res, next) => {
// todo
console.log(2)
next()
})
app.use('/', (req, res, next) => {
// todo
console.log(3)
next()
})
// 路由的中間件 將處理邏輯 拆分紅一個個的模塊
app.get('/', function(req, res, next) {
console.log(1)
next()
}, function(req, res, next) {
next();
console.log(11)
}, function(req, res, next) {
next('出錯了');
console.log('出錯了');
})
app.get('/', function(req, res, next) {
console.log('2');
res.end('end')
})
app.use((err,req,res,next)=>{
next();
})
複製代碼
// server.js
const express = require('express');
const LoginRouter =require('./routes/loginRouter');
const app = express();
app.use('/login',LoginRouter);
app.listen(3000);
// loginRouter.js
const express = require('express');
let router = express.Router(); // 是個構造函數
router.get('/add',function (req,res) {
res.end('/login-add')
})
router.get('/out',function (req,res) {
res.end('/login-out')
})
module.exports = router;
複製代碼
// router/index.js
...
// 提供一個Router類,既能夠new也能夠執行
createApplication.Router = require('./router')
...
複製代碼
function Router() {
this.stack = []
const router = (req, res, next) => {
router.handle(req, res, next)
}
router.stack = []
router.__proto__ = proto
return router
}
複製代碼
這樣改造後,經過new以後,this均指向router這個函數,在這個函數上沒有以前的各類放法,所以以前寫的邏輯均失效, 須要把這些屬性放到原型鏈上es6
// index.js
proto.handle = function (req, res, done) {
/*
@ 當有請求過來時,咱們先拿到path找到外層的layer,而後執行他的handle,讓內層的route,根據method依次執行內層的layer的handle
@
*/
let { pathname } = url.parse(req.url)
let idx = 0
let removed = '';
let next = (err) => {
// 在這裏進行統一的監聽
if (idx >= this.stack.length) return done() // 遍歷完後仍是沒找到,那就直接走出路由系統便可
let layer = this.stack[idx++]
// 出來中間件的時候將路徑補氣
if (removed) {
req.url = removed + pathname;// 增長路徑 方便出來時匹配其餘的中間件
removed = '';
}
if (err) { //錯誤處理。通常放在最後
...
} else {
if (layer.match(pathname)) {
if (!layer.route) {
if (layer.handle.length !== 4) {
if (layer.path !== '/') {
// 進入中間件的時候剪切路徑
removed = layer.path // 中間件的路徑
req.url = pathname.slice(removed.length);
}
layer.handle_request(req, res, next) // 普通中間件
} else {
// 是錯誤中間件,跳出
next()
}
} else {
...
} else {
next()
}
}
}
next()
}
複製代碼
// applicatin
const EventEmitter = require('events')
const http = require('http')
const context = require('./context.js')
const request = require('./request.js')
const response = require('./response.js')
const Stream = require('stream')
class Application extends EventEmitter {
constructor() {
super()
// 爲了實現每new一次有個全新的額context,所以須要使用object.create()賦值一份
this.context = Object.create(context)
this.response = Object.create(response)
this.request = Object.create(request)
this.middlewares = []
}
use(middleware) {
this.middlewares.push(middleware)
}
generateContext(req, res) {
....
}
compose(ctx) {
....
}
handleRequest(req, res) {
// 根據req,res以及新的context生成一個ctx
const ctx = this.generateContext(req, res)
// 執行中間件
this.compose(ctx).then(() => {
// 對返回的處理
let body = ctx.body; //當組合後的promise完成後,拿到最終的結果 響應回去
if(typeof body == 'string' || Buffer.isBuffer(body)){
res.end(body);
}else if(body instanceof Stream){
res.setHeader('Content-Disposition',`attachement;filename=${encodeURIComponent('下載1111')}`)
body.pipe(res);
}else if(typeof body == 'object'){
res.end(JSON.stringify(body));
}
})
}
listen(...args) {
const server = http.createServer(this.handleRequest.bind(this))
server.listen(...args)
}
}
module.exports = Application
複製代碼
generateContext(req, res)
// 保證每次use都建立新的上下文
let context = Object.create(this.context);
let request = Object.create(this.request);
let response = Object.create(this.response);
// 上下文中有一個request對象 是本身封裝的的對象
context.request = request;
context.response = response;
// 上下文中還有一個 req屬性 指代的是原生的req
// 本身封裝的request對象上有req屬性
context.request.req = context.req = req;
context.response.res = context.res = res;
return context;
}
複製代碼
異步回調使用next類型的
compose(ctx) {
const dispatch = i => {
try {
if(i >= this.middlewares.length) return Promise.reslove()
const currentMiddle = this.middlewares[i]
return Promise.reslove(currentMiddle(ctx, () => dispatch(i+1))
} catch(e) {
}
}
dispatch(0)
}
複製代碼
const context = {
}
// 代理方法
function defineGetter(target, key) {
// 定義一個getter, context和this不是一個,this.__proto__.__proto__ = context
context.__defineGetter__(key, () => {
return this[target][key]
})
}
function defineSetter(target, key) {
context.__defineSetter__(key, (newValue) => {
this[target][key] = newValue
})
}
defineGetter('request', 'url')
defineGetter('request', 'path')
defineGetter('request', 'query') // ctx.query = ctx.requesrt.query
defineGetter('response','body');// ctx.body => ctx.response.body
defineSetter('response','body');// ctx.body => ctx.response.body
module.exports = context
複製代碼
const url = require('url')
const request = {
// 屬性訪問器,相似Object.definedProperty()
get url() {
// 這裏的this指的是ctx.request
return this.req.url
},
get path() {
return url.parse(this.req.url).pathname
},
// 但願有什麼屬性就擴展
get query() {
return url.parse(this.req.url, true).query
}
}
module.exports = request
複製代碼
const response = {
_body:'',
get body(){
return this._body;
},
set body(val){
this._body = val;
}
}
module.exports = response;
複製代碼
const querystring = require('querystring');
const uuid = require('uuid');
const path = require('path');
const fs = require('fs');
// 中間件的功能能夠擴展屬性 / 方法
module.exports = function(uploadDir) {
return async (ctx, next) => {
await new Promise((resolve,reject)=>{
const arr = [];
ctx.req.on('data',function (chunk) {
arr.push(chunk);
})
ctx.req.on('end',function () {
if(ctx.get('content-type') === 'application/x-www-form-urlencoded'){
let result = Buffer.concat(arr).toString();
ctx.request.body = querystring.parse(result);
}
if(ctx.get('content-type').includes('multipart/form-data')){ // 二進制不能直接toString 可能會亂碼
let result = Buffer.concat(arr); // buffer
let boundary = '--'+ ctx.get('Content-Type').split('=')[1];
let lines = result.split(boundary).slice(1,-1);
let obj = {}; // 服務器收到的結果所有放在這個對象中
lines.forEach(line=>{
let [head,body] = line.split('\r\n\r\n');
head = head.toString();
let key = head.match(/name="(.+?)"/)[1]
if(!head.includes('filename')){
obj[key] = body.toString().slice(0,-2);
}else{
// 是文件 文件上傳 名字須要是隨機的
let content = line.slice(head.length + 4,-2);
let filePath = path.join(uploadDir,uuid.v4());
obj[key] = {
filePath,
size:content.length
}
fs.writeFileSync(filePath,content);
}
});
ctx.request.body = obj;
}
resolve();
})
});
await next(); // 完成後須要繼續向下執行
}
}
Buffer.prototype.split = function (sep) { // 分隔符多是中文的,我但願將他轉化成buffer來計數
let sepLen = Buffer.from(sep).length;
let arr = [];
let offset = 0;
let currentIndex = 0;
while((currentIndex = this.indexOf(sep,offset)) !== -1){
arr.push(this.slice(offset,currentIndex));
offset = currentIndex + sepLen;
}
arr.push(this.slice(offset));
return arr;
}
複製代碼
項目目錄 github
const Router = require('koa-router')
const routerA = new Router({prefix: '/login'}) // 劃分接口以loagin開頭的
複製代碼
const combineRouters = require('koa-combine-routers')
combineRouters(A, B)// 將A和B路由合併,返回一箇中間件
複製代碼
const views = require('koa-views')// 在ctx加一個render方法,基於Promise
app.use(views(__dirname + './views', {
map: {
html: 'ejs' //使用ejs模版
}
}))
// 使用
class Controller {
async add(ctx, next) {
await ctc.render('a.html', { age: 11, name: 'xx'})
}
}
// 模版
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<%=name%> <%=age%>
</body>
</html>
複製代碼