1
本篇文章意在用node.js简单实现https://news.ycombinator.com 的部分服务端功能,且nodejs的基本语法、html页面模板部分不再做描述。尚有不足,望指正,谢谢!

<实现效果>
index
details
submit

一、原生js实现

<目录结构>
图片描述

1、引入模块

//加载需要的核心模块
const http = require('http');
const url = require('url');
const path = require('path');
const fs = require('fs');
const querystring = require('querystring');

//加载需要的第三方模块,加载前需先在命令行通过npm下载,如npm i mime --save
const mime = require('mime');
const template = require('art-template');

2、功能实现

//创建服务器
const server = http.createServer();

//该全局常量存放数据加载地址(此例中用data.json存储数据,之后会写从mongoDB中获取数据)
const data_path = path.join(__dirname, './data.json');

//处理请求
server.on('request', (req, res) => {
    // --------[首页模块]---------
    
    //设置路由
    if (req.url == '/' || req.url == '/index') {
        //调用封装的从读取数据功能函数
        readData(data_path,  data=> {
            let file_path = path.join(__dirname, './views/index.html');
            let obj = { list: data };
            let html = template(file_path, obj);
            res.end(html);
        });
        
        //--------以下注释部分代码为未封装读取数据功能前的代码(下文不再重复此部分代码)---------
        // 读取数据,将数据渲染到页面结构,再将结果响应给客户端
        // fs.readFile(data_path, 'utf8', (err, data) => {
        //     data = JSON.parse(data);
       
        //     let file_path = path.join(__dirname, './views/index.html');
        //     let obj = { list: data };
        
        //     将数据渲染到模板中并返回响应:调用封装的response对象的成员函数
               res.render(file_path,obj);
        // });
    }

    // -------------[详情页]--------------
    else if (req.url.startsWith('/details')) {
        readData(data_path, data => {
            // 根据url中的id找到数据中的数据项,渲染到页面,将渲染结果响应给浏览器
            let id = url.parse(req.url, true).query.id;
            let item = data.find((v, i) => {
                //【这里用'=='而不用'==='的原因:v.id是number型,id是string型】
                return v.id == id;
            });
            let file_path = path.join(__dirname, './views/details.html');
            
            res.render(file_path,item);
        });
    }

    // -------------------[提交页:需针对不同请求方式进行处理]----------------
    else if (req.url == '/submit') {
        fs.readFile(path.join(__dirname, './views/submit.html'), (err, data) => {
            if (err) {
                return console.log('404 not found');
            }
            res.end(data);
        });
    }
    
    // 【若为get请求】提交后跳转到数据添加过渡页,需处理数据并重定向到首页
    else if (req.url.startsWith('/add') && req.method == 'GET') {
        // 解析url,将查询数据转换为对象,并添加id属性
        let item = url.parse(req.url, true).query;
        item.id = +new Date(); //确保id的唯一
        
        // 获取json数据,将新对象追加到转为数组的数据中
        readData(data_path, data => {
            data.unshift(item);
            
            //调用封装的将数据覆盖写入data.json的功能函数
            writeData(data,() => {
                // 重定向并返回响应:调用封装的response对象的成员函数
                res.redirect();
            });
        });
    }
    
    // 【若为post请求】特别地,上传参数以数据流(二进制)的形式传输
    else if (req.url.startsWith('/add') && req.method == 'POST') {
        // 监听查询数据上传,并将数据流拼接为字符串
        let str = '';
        req.on('data', (chunk) => {
            str += chunk;
        });
        // 监听到数据上传完成,将查询字符串解析为查询对象
        req.on('end', () => {
            let item = querystring.parse(str);
            // 给此条对象添加id属性,并追加到数据中,再将数据重新覆盖写入本地
            item.id = +new Date();
            readData(data_path, data => {
                data.unshift(item);
                writeData(data,() => {
                    res.redirect();
                });
            })
        });
    }

    // ------------[静态资源:可模拟apache,开放静态资源目录下的所有资源]------------
    else if (req.url.startsWith('/assets')) {
        let rs_path = path.join(__dirname, req.url);
        fs.readFile(path.join(__dirname, req.url), (err, data) => {
            if (err) {
                return console.log('404 not found');
            }
            res.setHeader('content-type', mime.getType(rs_path));
            res.end(data);
        });
    }
    
    // -----------非路由请求页面处理-----------
    else {
        res.end('404');
    }
    
    //-------[将重定向和渲染功能(都返回响应)封装为response的成员函数]----------
    res.redirect=function(){
        res.statusCode = 302;
        res.setHeader('location', '/index');
        res.end();
    }
    
    res.render=function(file_path,obj){
        let html = template(file_path, obj);
            res.end(html);
    }
});
//-------------功能函数封装--------------

// 功能:从data.json中读取数据并转换数据格式
function readData(data_path, callback) {
    fs.readFile(data_path, 'utf8', (err, data) => {
        if (err) {
            return console.log(err);
        }
        data = JSON.parse(data || '[]');
        callback(data);
    });
}

// 功能:转换数据格式并将数据覆盖写入data.json
function writeData(data, callback) {
    data = JSON.stringify(data, null, 2);
    fs.writeFile(data_path, data, err => {
        if (err) {
            return console.log(err);
        }
        // 若写入成功则执行回调
        callback();
    });
}

3、开启服务器

server.listen(8001, () => {
    console.log('server is started ,pls visit http://localhost:8001');
})

二、模块化

在原结构基础上,将主功能页面hn.js进行自定义模块划分,即

  • hn.js的功能:创建服务器、整体结构;
  • router.js的功能:路由规则结构;
  • handle.js的功能:每个路由规则的具体处理;
  • extend.js的功能:封装为request对象的成员函数。

另外,针对每个模块,要考虑其依赖模块、依赖参数以及依赖的其他函数。

<hn.js>

// 内置模块
const http = require('http');

// 引入自定义模块
const router = require('./router.js');
const extend = require('./extend.js');

const server = http.createServer();

server.on('request', (req, res) => {
     //调用res拓展模块功能
     extend(res);
     //调用路由规则模块功能
     router(req,res);  
});

server.listen(8001, () => {
    console.log('server is started ,pls visit http://localhost:8001');
})

<router.js>

//依赖模块
const handle=require('./handle.js');
//导出功能函数
module.exports = function (req, res) {
    // 首页
    if (req.url == '/' || req.url == '/index') {
        handle.index(req,res);
    }
    // 详情页
    else if (req.url.startsWith('/details')) {
        handle.details(req,res);
    }
    // 提交页
    else if (req.url == '/submit') {
       handle.submit(req,res);
    }
    // 若添加操作为get请求
    else if (req.url.startsWith('/add') && req.method == 'GET') {
        handle.addGet(req,res);
    }
    // 若添加操作为post请求
    else if (req.url.startsWith('/add') && req.method == 'POST') {
       handle.addPost(req,res);
    }
    // 静态资源
    else if (req.url.startsWith('/assets')) {
        handle.assets(req,res);
    }
    // 非指定页面处理
    else {
        handle.others(req,res);
    }
}

<handle.js>

// 1、依赖模块
const url = require('url');
const querystring = require('querystring');
const mime = require('mime');
const path = require('path');
const fs = require('fs');

// 2、依赖参数:req,res,data_path
const data_path = path.join(__dirname, './data.json');

//导出功能函数
module.exports.index = function (req, res) {
    readData(data_path, function (data) {
        let file_path = path.join(__dirname, './views/index.html');
        let obj = { list: data };
        res.render(file_path, obj);
    });
}

module.exports.details = function (req, res) {
    readData(data_path, function (data) {
        let id = url.parse(req.url, true).query.id;
        let item = data.find((v, i) => {
            return v.id == id;
        });
        let file_path = path.join(__dirname, './views/details.html');
        res.render(file_path, item);
    });
}

module.exports.submit = function (req, res) {
    fs.readFile(path.join(__dirname, './views/submit.html'), (err, data) => {
        if (err) {
            return console.log('404 not found');
        }
        res.end(data);
    });
}

module.exports.addGet = function (req, res) {
    let item = url.parse(req.url, true).query;
    item.id = +new Date();
    readData(data_path, function (data) {
        data.unshift(item);
        writeData(data, function () {
            res.redirect();
        });
    });
}

module.exports.addPost = function (req, res) {
    let str = '';
    req.on('data', (chunk) => {
        str += chunk;
    });
    req.on('end', () => {
        let item = querystring.parse(str);
        item.id = +new Date();
        readData(data_path, function (data) {
            data.unshift(item);
            writeData(data, function () {
                res.redirect();
            });
        })
    });
}

module.exports.assets = function (req,res) {
    let rs_path = path.join(__dirname, req.url);
    fs.readFile(path.join(__dirname, req.url), (err, data) => {
        if (err)  return console.log('404 not found');
        res.setHeader('content-type', mime.getType(rs_path));
        res.end(data);
    });
}

module.exports.others = function (req,res) {
    res.end('404 not found');
}

// 3、依赖的其他函数
function readData(data_path, callback) {
    fs.readFile(data_path, 'utf8', (err, data) => {
        if (err) {
            return console.log(err);
        }
        data = JSON.parse(data || '[]');
        callback(data);
    });
}

function writeData(data, callback) {
    data = JSON.stringify(data, null, 2);
    fs.writeFile(data_path, data, (err) => {
        if (err) {
            return console.log(err);
        }
        callback();
    });
}

<extend.js>

//依赖模块
const template = require('art-template');
//导出功能
module.exports = function (res) {
    res.redirect = function () {
        res.statusCode = 302;
        res.setHeader('location', '/index');
        res.end();
    }
    
    res.render = function (file_path, obj) {
        let html = template(file_path, obj);
        res.end(html);
    }
}

三、express实现

<npm安装>

npm init
npm i express --save
npm i art-template express-art-template --save
npm i body-parser --save

<main.js>

// 引入express框架
const express = require('express');

// 实例化express对象
const app = express();

// 托管静态资源
app.use('/assets',express.static('./assets'));

// 配置模板引擎
app.engine('html',require('express-art-template'));

// 引入并配置body-parser
const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({extended:true}));

// 引入外置路由模块,并挂载到app上
const router = require('./router.js');
app.use(router);

//启动服务器
app.listen(8001,()=>{
    console.log('server is started' );
});

<router.js>

//引入依赖模块
const express = require('express');
const path = require('path');
const fs = require('fs');

// 实例化外置路由
const router = express.Router();

// 注册路由

//-----------------首页------------------
router.get('/', (req, res) => {
    res.redirect('/index');
});
router.get('/index', (req, res) => {
    read_data(function (data) {
        // 渲染文件并将渲染结果响应给浏览器
        res.render('index.html', { list: data });
    })
});

// --------------详情页-------------------
router.get('/details', (req, res) => {
    // 获取查询对象
    let obj = req.query;
    
    read_data(function (data) {
        // 通过id找到所有数据中的匹配条目
        let item = data.find(v => {
            console.log(typeof v.id, typeof obj.id);
            return v.id == obj.id;
        });
        res.render('details.html', item);
    });
});

// -------------提交页-----------------
router.all('/submit', (req, res) => {
    //将文件响应给浏览器
    res.sendFile(path.join(__dirname, './views/submit.html'))
});

// 若通过get请求添加数据
router.get('/add', (req, res) => {
    let item = req.query;
    item.id = +new Date();

    read_data(function (data) {
        data.unshift(item);

        write_data(data, function () {
            res.redirect('/index');
        });
    })
});

// 若通过post请求添加数据
router.post('/add', (req, res) => {
    let item = req.body;
    item.id = +new Date();
    
    read_data(function (data) {
        data.unshift(item);
        
        write_data(data, function () {
            res.redirect('/index');
        });
    })
});

// 导出router对象
module.exports = router;

// ------封装读取数据和写入数据的功能函数--------
const data_path = path.join(__dirname, './data.json');

function read_data(callback) {
    fs.readFile(data_path, 'utf8', (err, data) => {
        if (err) {
            return console.log('404 not found');
        }
        data = JSON.parse(data || '[]');
        callback(data);
    });
}

function write_data(data, callback) {
    data = JSON.stringify(data, null, 2);
    fs.writeFile(data_path, data, err => {
        if (err) {
            return console.log('fail to write');
        }
        callback();
    });
}

形式流亡
67 声望10 粉丝