手撸一个Web Server【基于Node.js原生API】

839 阅读4分钟

官方文档讲到Node.js® 是一个基于 Chrome V8 引擎 的 JavaScript 运行时环境,所以严格意义上来讲,Node.js不能算是一门语言,本质上还是基于JavaScript以及Chrome V8 引擎的。在日益工程化的前端世界里,前端开发者或多或少都会涉及node的使用,比如我们常用到的CLI工具,当然它还可以做Web服务器,比如ExpressKoa,它们是对Node.js原生API进行了封装。通过文档,我们其实也可以不借助这些比较成熟的框架,只用原生API也能编写一个Web服务器出来。

Hello World

搭建一个简单的Web服务器,其实引用Node原生的http模块就够了,下面我们编写一个简单的Hello World程序:

//引进http模块
const Http = require('http');

//自定义端口
const port = 3000

// 创建请求句柄
const requestHandle = (req, res) => {
  res.statusCode = 200
  res.setHeader('Content-Type', 'text/plain')
  res.end('Hello World')
}

//创建请求监听者对象(Server)
const Server = Http.createServer(requestHandle);

Server.listen(port, () => {
  console.log(`Server is running on http://127.0.0.1:${port}/`)
})

以上我们利用Node的http模块,创建了一个监听者对象,在服务创建后通过Server.listen监听相应的port,这样我们通过127.0.0.1:3000去访问就可以返回我们编写的Hello World 程序了!!!

但是离我们的Web服务器还相差甚远呢,哈哈,主要是还差对Web服务器基础功能的处理:

1.支持`HTTP`动词,比如`GET``POST`2.支持路由

所以我们要实现一个真正能用的Web服务器,我们需要逐一实现这些基础功能

处理HTTP动词

我们将requestHandle这个句柄改造一下,添加我们的Http请求方式

//引进url模块
const url = require('url'); 
 
const requestHandle = (req, res) => {
    // url.parse可以将req.url解析成一个对象,里面包含有pathname和querystring等参数
    const urlObject = url.parse(req.url);
    
    switch (req.method) {
        case "GET":
            getGetParam(urlObject, res);
            break;
        case "POST":
            getPostParam(urlObject, res);
            break;
    }
}

//Get请求
const getGetParam = (urlObject, res ) =>{
    //我们定义以'/api/get'的为Get请求 
    if (urlObject.pathname.startsWith('/api/get')) {
          res.statusCode = 200
          res.setHeader('Content-Type', 'application/json;charset=UTF-8')
          res.end('这是Get请求')
    }
}

//POST请求
const getPostParam = (urlObject, res) =>{
    //我们定义以'/api/post'的为post请求 
    if (urlObject.pathname.startsWith('/api/post')) {
          res.statusCode = 200
          res.setHeader('Content-Type', 'application/json;charset=UTF-8')
          res.end('这是post请求')
    }
}

01.png

02.png

当然,在实际开发中的,我们的post请求并不是这样玩的,这里仅是为了区分请求方式

处理路由

我们将getGetParamgetPostParam这两个函数再修改下

03.png

//Get请求
const getGetParam = (urlObject, res) => {
    const resData = {
        code: 0,
        msg: 'success',
        data: [
                { id: 0, name: '张三' },
                { id: 1, name: '李四' },
        ],
    }
    //我们定义以'/api/get'的为Get请求
    if (urlObject.pathname.startsWith('/api/get')) {
        // 再判断路由
        if (urlObject.pathname === '/api/get/users') {
            res.setHeader('Content-Type', 'application/json;charset=UTF-8')
            res.end(JSON.stringify(resData))
        }
    }
}


04.png

//POST请求
const getPostParam = (urlObject, req, res) => {
    const resData = {
        code: 0,
        msg: 'success',
        data: [
                { id: 0, name: '王五', age: 22, sex: '男' },
                { id: 1, name: '赵六', age: 24, sex: '男' },
        ],
    }
//我们定义一个formData变量,用于暂存请求体信息
let formData = ''

//我们定义以'/api/post'的为post请求
if (urlObject.pathname.startsWith('/api/post')) {
    // 再判断路由
    if (urlObject.pathname === '/api/post/users') {
        //通过req的data事件监听函数,每当接受到请求体的数据,就累加到post变量
        //post请求经常会很长,此时会分段传入
        req.on('data', (chunk) => {
                //将段落合并
                formData += chunk
        })
        //当所有数据发送完毕之后,此时将会触发end事件
        req.on('end', () => {
            // 此时可以做一些逻辑处理,如文件写入等等
            let result = JSON.parse(JSON.stringify(formData))
            console.log('result:', result)

            // 打印如下
            // result: ----------------------------360048846796916393599860
            // Content-Disposition: form-data; name="name"

            // 王五
            // ----------------------------360048846796916393599860        
            // Content-Disposition: form-data; name="age"

            // 23
            // ----------------------------360048846796916393599860        
            // Content-Disposition: form-data; name="sex"

            // 男
            // ----------------------------360048846796916393599860--      

            res.setHeader('Content-Type', 'application/json;charset=UTF-8')
            // 这里我们就先不处理,将定义的数据返回
            res.end(JSON.stringify(resData))
        })
     }
   }
}

此时,我们的服务器已经具备路由请求能力了

写在最后

在实际开发过程中,还有许多需要考虑和权衡的地方,这里仅展示了一个server的基础能力,还有很多未实现的特性呢,如文件写入、长连接实现全双工通讯呀,这里就不赘述了,后面会继续进行探究。

  • 文中实例代码已在github上 webServer
写这篇文章,希望对初学者有所帮助。