express应该算是早期最优秀的一个node框架了,刚开始学node做后端语言就是用的express,它的cli可以帮我们搭建好项目目录,就像现在的vue,react一样。express本身没有做太多的事情,它主要是实现了路由以及扩展request和response的方法,最为重要的是中间件的使用,express通过app.use(connect)来调用各种中间件以便于实现各种功能,于是乎npm上出现了大量的中间件。现在我们就根据express的用法来实现一个自己的express。

分析

对于一个框架,首先要做的不是怎么去实现它,而是设计用户该怎么用更方便,以此来实现我们的框架,这里我们以express的用法来一步一步实现。

1.基本用法

用过express的应该很容易理解,没用过的也没关系,我会有注释。

/*app.js*/
const express = require('express')
const path = require('path')
const app = express()
app.use(express.static(path.join(__dirname, 'static')))//设置静态目录
app.set('views', path.join(__dirname, 'views'));//设置模板文件目录
app.use(function(req, res, next){ //第一个参数不是字符串的时候
console.log('middle ware')
next()
}) app.use('/user', function (req, res, next){//第一个参数是字符串的时候
console.log('middle 1')
next();
}) app.use('/hello', function (req, res){//返回值只有返回没有渲染页面的
console.log('res hello:'+req.query)
res.send('hello')
}) app.use('/query', function (req, res){//渲染页面并填充值的
console.log('res query')
res.render('query', {
title: 'query-html',
name: 'KE',
content: 'this is node frame KE, like express, this page is use ejs to render.'
})
// res.send('query:'+JSON.stringify(req.body))
}) app.use(function(req, res){//错误处理
res.send(404, 'not found')
})
module.exports = app
/*bin/www 启动文件*/
#!/usr/bin/env node
const http = require('http')
const app = require('../app')
http.createServer(app).listen(8181, function(){
console.log('server listen 8181')
})

2.代码分析

(1)首先我们看一下启动文件,第一行不去管它(有兴趣可以了解下,是关于环境的问题),接下来这几行是不是很熟悉,创建httpserver,监听端口,而处理httpserver的函数我们看到是app,那么说明app是个函数,从app.js文件导入的。

回过头来看app.js,被导出的app被定义为const app = express(),express显然是个函数,app也是个函数,那么说明express这个包返回了一个函数,这个函数又返回一个函数。代码如下(我们的框架起名叫ke):

//ke.js
function ke(){
let app = function(req, res){ }
return app
}
module.exports = ke

(2)然后我们看到app有个use的方法,那我们就给app加一个方法就可以了。这个方法有两个参数,第一个是表示路由,第二个表示回调函数,当第一个参数是函数的时候我们需要调用next方法来让它继续执行,在这个函数中我们可以对req, res做一些操作,其实这个函数就相当与一个中间件。第一个参数是路由的时候,匹配到路由就停止,如果没有匹配到就执行最后的错误处理。回调函数有三个参数:req, req, next。继续上代码:

    let tasks = [];//要执行的函数队列
let app = function(req, res){
let i = 0;
function next(){
let task = tasks[i++];
if(!task){
return;
}
if(task.routePath === null || url.parse(req.url).pathname === task.routePath){
task.middleWare(req, res, next);
}else{
next()
}
}
next()
}
app.use = function(routePath, middleWare){
if(typeof routePath === 'function'){
middleWare = routePath;
routePath = null;
}
tasks.push({
routePath: routePath,
middleWare: middleWare
})
}

由于中间件每次请求都要挨个执行,所以我们把use方法拿到的中间件放到tasks队列中。

use方法负责把中间件插到队列中,没有路由的routePath设置为null。next方法负责执行这些中间件我们可以看到判断条件,如果路由没有匹配到会继续执行中间件,直到最后的错误处理。

(3)req和res的扩展

express的req.query和req.body可以获取请求参数,而我们知道原生的node并没有,所以我们需要做一个扩展,关于query处理很简单,我们使用url模块即可处理到。处理body就需要自己手动写了,这里只是做了简单的处理,对于其他类型(文件等参数)并不能用。建议使用第三方的body-parser模块。

function makeQuery(req){
req.query = url.parse(req.url).query;
req.on('data', function(chunck){
body+=chunck;
}).on('end', function(){
req.body = parseBody(body);
})
}
function parseBody(bodyData){
let param = {};
bodyData.split('&').forEach(item => {
let temp = item.split('=');
param[temp[0]] = temp[1];
})
return param;
}

对于res原生的返回数据只有write和end方法,而且参数必须为字符串,这里我们按照express的样子给它扩展两个方法send和render。

send方法,可以直接发送字符串,对象,也可以设置状态码,所以简单实现如下

res.send = function(data){
switch(typeof data){
case 'string': res.end(data);
break;
case 'object': res.end(JSON.stringify(data));
break;
case 'number': res.writeHead(data), res.end(arguments[1]);
break;
default: res.setHeader(500, 'server error'), res.end('server error');
}
}

那么render方法就更厉害了,可以渲染模板。比如ejs、jade(pug)。这里我们以ejs为准,首先我们要知道模板名称,然后是要渲染的数据,然后把对应的模板和数据编译之后返回到页面。

res.render = function(templatename, data){
ejs.renderFile(resolve(app.get('views'), templatename+'.html'), data, (err, content) => {
if(err){
res.writeHead(404, 'not found')
res.end('cant found this page')
}
res.writeHead(200, {'Content-Type': 'text/html'})
res.end(content);
})
}

当然这里的app.get('views')是获取模板路径,所以我们一开始需要用set方法来设置模板路径

app.set('views', path.join(__dirname, 'views'));

这个路径我们存储在app.data上,并使用set和get来存取

    app.data = {};
app.set = function(key, value){
app.data[key] = value;
}
app.get = function(key){
return app.data[key]
}

到这里,我们的框架就可以实现基本的功能了,但是我们还没有处理静态文件,express是这么用的

app.use(express.static(path.join(__dirname, 'static')))

有个static方法,那我们就加一个:

ke.static = function(rootpath){
return function(req, res, next) {
const filePath = path.resolve(rootpath, url.parse(req.url, true).pathname.substr(1))
let mimeType = mime.getType(url.parse(req.url).pathname.substr(1))
fs.readFile(filePath, 'binary', (err, data) => {
if(err) {
next();
}else{
res.writeHead(200, 'ok', {
'Content-Type': mimeType
})
res.write(data)
res.end();
}
})
}
}

主要完成的功能就是如果找到对应的文件就返回,如果没有就执行next,继续执行后面的中间件。具体实现也很简单,别忘了设置content-type就行。

到这里我们的框架就基本完成了,其实大家会发现最核心的就是中间件的实现以及使用。这也是express的核心,各种功能你都可以以中间件的方式来使用,可以自己开发也可以使用第三方包。

最后奉上我的代码https://github.com/Stevenzwzhai/KE

实现一个简单版的express的更多相关文章

  1. 动手写一个简单版的谷歌TPU-矩阵乘法和卷积

    谷歌TPU是一个设计良好的矩阵计算加速单元,可以很好的加速神经网络的计算.本系列文章将利用公开的TPU V1相关资料,对其进行一定的简化.推测和修改,来实际编写一个简单版本的谷歌TPU.计划实现到行为 ...

  2. 动手写一个简单版的谷歌TPU-指令集

    系列目录 谷歌TPU概述和简化 基本单元-矩阵乘法阵列 基本单元-归一化和池化(待发布) TPU中的指令集 SimpleTPU实例: (计划中) 拓展 TPU的边界(规划中) 重新审视深度神经网络中的 ...

  3. 动手写一个简单版的谷歌TPU

    谷歌TPU是一个设计良好的矩阵计算加速单元,可以很好的加速神经网络的计算.本系列文章将利用公开的TPU V1(后简称TPU)相关资料,对其进行一定的简化.推测和修改,来实际编写一个简单版本的谷歌TPU ...

  4. 手写一个简单版的SpringMVC

    一 写在前面 这是自己实现一个简单的具有SpringMVC功能的小Demo,主要实现效果是; 自己定义的实现效果是通过浏览器地址传一个name参数,打印“my name is”+name参数.不使用S ...

  5. MySQL 全文索引实现一个简单版搜索引擎

    前言 只有Innodb和myisam存储引擎能用全文索引(innodb支持全文索引是从mysql5.6开始的) char.varchar.text类型字段能创建全文索引(fulltext index ...

  6. 一个简单版的波纹css3动画

    ul{width: 300px;border: red;}ul li{width: 300px;height: 70px;line-height: 70px;background: #fff;text ...

  7. Java实现简单版SVM

    Java实现简单版SVM 近期的图像分类工作要用到latent svm,为了更加深入了解svm,自己动手实现一个简单版的.         之所以说是简单版,由于没实用到拉格朗日,对偶,核函数等等.而 ...

  8. 【MEF】构建一个WPF版的ERP系统

    原文:[MEF]构建一个WPF版的ERP系统 引言 MEF是微软的一个扩展性框架,遵循某种约定将各个部件组合起来.而ERP系统的一大特点是模块化,它们两者的相性很好,用MEF构建一个ERP系统是相当合 ...

  9. 手把手教你用redis实现一个简单的mq消息队列(java)

    众所周知,消息队列是应用系统中重要的组件,主要解决应用解耦,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构.目前使用较多的消息队列有 ActiveMQ,RabbitMQ,Zero ...

随机推荐

  1. javascript操作Date对象

    Date 对象用于处理日期和时间. 创建 Date 对象的语法: var myDate=new Date() Date 对象会自动把当前日期和时间保存为其初始值. 参数形式有以下5种: new Dat ...

  2. GYM 101550 G.Game Rank(模拟)

    The gaming company Sandstorm is developing an online two player game. You have been asked to impleme ...

  3. JavaScript 基础(二)数组

    字符串, JavaScript 字符串就是用'' 和""括起来的字符表示. 字符字面量, \n 换行, \t 制表, \b 退格, \r 回车, \f 进纸, \\ 斜杠,\' 单 ...

  4. 于是他错误的点名开始了(trie树)

    题目背景 XS中学化学竞赛组教练是一个酷爱炉石的人. 他会一边搓炉石一边点名以至于有一天他连续点到了某个同学两次,然后正好被路过的校长发现了然后就是一顿欧拉欧拉欧拉(详情请见已结束比赛CON900). ...

  5. 范围for语句的整理

    1.如何处理stirng中的每个字符?(来自C++Primer中文版5th中P83) 使用基于范围的for语句,比如下面的例子,输出每个字符 #include<iostream> #inc ...

  6. Java分享笔记:Java网络编程--TCP程序设计

    [1] TCP编程的主要步骤 客户端(client): 1.创建Socket对象,构造方法的形参列表中需要InetAddress类对象和int型值,用来指明对方的IP地址和端口号. 2.通过Socke ...

  7. 小程序swiper不显示图片

    按照文档上的代码运行后,发现图片不显示 解决办法: app.wxss文件 align-items: center;这句话删除了,运行 OK!

  8. LeetCode-环形链表II

    LeetCode-环形链表II 为找到入口点可以用以下方法 使用快慢指针法直到两个指针相遇 头节点处创建一个新的指针,并且向前移动,两个指针相遇处创建一个新的指针,并且向前移动,直到两个指针相遇为入口 ...

  9. Vue+webpack构建一个项目

    1.安装CLI命令的工具  推荐用淘宝的镜像 npm install -g @vue/cli @vue/cli-init 2.使用命令构建一个名为myapp的项目 vue init webpack m ...

  10. ECSHOP和SHOPEX快递单号查询韵达插件V8.6专版

    发布ECSHOP说明: ECSHOP快递物流单号查询插件特色 本ECSHOP快递物流单号跟踪插件提供国内外近2000家快递物流订单单号查询服务例如申通快递.顺丰快递.圆通快递.EMS快递.汇通快递.宅 ...