1. 第一次迭代

第一次迭代,主要实现的目标包括两个部分:

  • 实现基本的 HTTP 服务器。
  • 实现基本的 GET 方法。

HTTP 服务器的实现比较简单,可以参考 nodejs 官网:

const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
	res.statusCode = 200;
	res.setHeader('Content-Type', 'text/plain');
	res.end('Hello World\n');
});

server.listen(port, hostname, () => {
	console.log(`Server running at http://${hostname}:${port}/`);
});

实现 expross 的 listen 函数:

listen: function(port, cb) {
	var server = http.createServer(function(req, res) {
		console.log('http.createserver...');
	});

	return server.listen(port, cb);
}

如果你查看 nodejs 的官方文档,就会发现 http.createServer 拥有很多重载函数,而 Express 框架同样支持这些参数形式,为了充分满足参数的不同需求,这里可以使用 javascript 脚本语言常用的 “代理” 手法:applycall ,因为参数个数未知,推荐使用 apply 函数:

listen: function() {
	var server = http.createServer(function requestListener(req, res) {
		console.log('http.createserver...');
	});

	// 使用 apply 代理手法
	return server.listen.apply(server, arguments);
}

上面代码中的 requestListener 回调函数可以拦截(接收)一切 HTTP 请求。通过该函数,可以实现框架的路由功能,做到根据请求参数的不同,执行不同的业务逻辑。

对于 HTTP 传输层来说,一个 HTTP 请求主要包括请求行、请求头和消息体,nodejs 将常用的数据方法封装为 http.IncomingMessage 类,代码中的 req 变量就是该类的对象。

每个 HTTP 请求都会对应一个 HTTP 响应。一个 HTTP 响应主要包括状态行、响应头和消息体。nodejs 将其封装为 http.ServerResponse 类,代码中的 res 变量就是该类的对象。

其实不仅仅 nodejs 这样做,基本上所有的 HTTP 服务器框架都会抽象出 Request 和 Response 这两个对象,它们分别代表着 HTTP 传输的两端,也肩负着服务端和客户端(浏览器)交互的任务。

一个成熟的 HTTP 框架最基本的需求就是区分不同的 HTTP 请求,根据请求的不同来执行不同的业务逻辑,这在 web 服务器中有一个专有名词叫做 “路由管理”。每个请求默认为一个路由,常见的路由分类策略主要包括请求路径和请求方法。但是不仅仅限定这些,任何 HTTP 请求包含的参数都可以作为路由策略,例如可以使用 user-agent 字段判断是否为移动端等等。

不同的框架路由管理的方法略有不同,但整个流程是基本一致的,主要包括两个部分:

  • 绑定部分,用于将路由策略和执行逻辑绑定。
  • 执行部分,根据请求的不同执行前期绑定部分指定的业务逻辑。

既然知道路由系统的重要性,接下来我们就开始实现我们自己的路由系统。Express 框架的路由系统是由 Router 来负责的,它本身是一个中间件。咱们这里先实现一个简单的路由器,而非最终的路由中间件,不过随着代码的迭代,最终我们会实现和 Express 类似的东西。

首先抽象路由的基本属性:

  • path 请求路径,例如:/books、/books/1 等。
  • method 请求方法,例如:GET、POST、PUT、DELETE 等。
  • handle 处理函数。

接着定义一个路由变量 router 来保存当前的路由表:

var router = [{
	path: '*',
	method: '*',
	handle: function(req, res) {
		res.writeHead(200, {'Content-Type': 'text/plain'});
		res.end('404');
	}
}];

最后修改 requestListener 函数的逻辑,用来匹配 router 表中的项,如果匹配成功,则执行绑定的 handle 函数,否则执行 router[0].handle 函数,返回 404 ,代表未找到相关路由。

listen: function() {
	var server = http.createServer(function requestListener(req, res) {

		for(var i=1,len=router.length; i<len; i++) {
			if((req.url === router[i].path || router[i].path === '*') &&
				(req.method === router[i].method || router[i].method === '*')) {
				return router[i].handle && router[i].handle(req, res);
			}
		}
		
		return router[0].handle && router[0].handle(req, res);
	});

	return server.listen.apply(server, arguments);
}

实现路由管理的执行部分,还需要实现路由管理的绑定部分,这里根据官方测试用例,先实现 GET 请求的路由项添加:

get: function(path, fn) {
	router.push({
		path: path,
		method: 'GET',
		handle: fn
	});
}

执行 node test/index.js,访问 http://127.0.0.1:3000/ 会提示 res.send 不存在:

app.get('/', (req, res) => res.send('Hello World!'));
                               ^
TypeError: res.send is not a function

该函数并不是 nodejs 原生自带的,而是 Express 框架附加的。为了使用这个函数,我们这里临时在 res 对象上附加上该函数的实现:

listen: function() {
	var server = http.createServer(function(req, res) {
		if(!res.send) {
			res.send = function(body) {
				res.writeHead(200, {
					'Content-Type': 'text/plain'
				});
				res.end(body);
			};
		}

		......
	});

	return server.listen.apply(server, arguments);
}

到此为止,已经基本实现了 Express 官方的自带案例功能,在结束这一节内容之前,我们调整一下目前的代码结构。

创建 expross/lib/application.js 文件,将 createApplication 函数中的代码转移到该文件, expross.js 文件只保留引用。

var app = require('./application');

function createApplication() {
	return app;
}

exports = module.exports = createApplication;

整个目录结构如下:

expross
  |
  |-- lib
  |    | 
  |    |-- expross.js
  |    |-- application.js
  |
  |-- test
  |    |
  |    |-- index.js
  |
  |-- index.js

最后,运行 node test/index.js,打开浏览器访问 http://127.0.0.1:3000/