博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
node中的http会了吗? 来手写一个属于自己的'cgp-server'静态服务
阅读量:6802 次
发布时间:2019-06-26

本文共 9685 字,大约阅读时间需要 32 分钟。

序言

手写一个静态服务可以对node中http模块有更深的理解,这是我们的初衷。http-server相信大家都用过,这里我们要实现类似个功能。功能如下

  • 启动我们写好的模块后,输入localhost:3000打开我们public目录下的文件(默认打开index.html)
  • 用到debug插件,主要用于在命令行输出一些日志,我们只用基本的功能,所以没有难点。
  • 可能用到chalk插件,就是把命令行输出的日志五颜六色,变得好看,没什么太大作用,

准备工作

我们的目录解构如下

  • 大家应该一看就懂啦,启动我们服务,自动打开public/index.html
  • bin/www.js 是我们后面用命令行启动服务的配置
  • public是我们的静态目录
  • app.js 主文件
  • config.js 配置文件
  • tmpl.html是我们用ejs编译的模板,后面讲到

先写最简单的config.js

let path = require('path');let config = {    hostname:'127.0.0.1', //默认主机    port:3000,  //默认端口    dir:path.join(__dirname,'','public') //默认打开的目录(绝对路径)};module.exports = config;复制代码

以上代码都能看得懂,下面开始写我们主文件

核心代码 app.js

1、引入所需的依赖包
let http = require('http');let url = require('url');let path = require('path');let util = require('util');let fs = require('fs');let zlib = require('zlib');let mime = require('mime'); // 得到内容类型let debug = require('debug')('*'); // 打印输出 会根据环境变量控制输出let chalk = require('chalk'); // 粉笔let ejs = require('ejs'); // 模板引擎//先声明好,下面解释let config = require('./config');let stat = util.promisify(fs.stat);//promise化 fs.stat方法let readdir = util.promisify(fs.readdir);let template = fs.readFileSync(path.join(__dirname,'tmpl.html'),'utf8'); //读取ejs的模板文件复制代码
  • mime解析文件给你内容类型,
  • ejs渲染引擎,我们用最简单功能,不会也能看懂
2、http模块开启服务
/*运行的条件 指定主机名* 指定启动的端口号* 指定运行的目录 */let config = require('./config'); //引入配置文件class Server { //声明类    constructor() {        this.config = config; //讲配置挂载再我们的实例上    }    handleRequest(req,res){ //确保这里的this都是实例    }    start(){//服务开始的方法        let server =http.createServer(this.handleRequest.bind(this));        let {hostname,port} = this.config; //解构主机名和端口        server.listen(port,hostname);        debug(`http://${hostname}:${port} start`) //命令行中打印    }}//开启一个服务let server = new Server();server.start(); //调用start方法复制代码

截至到目前位置,简单的服务已经开启了,先来测试下效果吧

完美,控制台打印出了内容,

3、实现handleRequest方法,即处理请求逻辑

列出我们要做什么

  • 解析url的路径名
  • 与默认配置中路径(G://cgp-server/public)拼接
  • 判断是文件还是文件夹还是404
let stat = util.promisify(fs.stat);//promise化 fs.stat方法 async  handleRequest(req,res){ //确保这里的this都是实例        let {pathname} = url.parse(req.url,true); //获取url的路径        let p = path.join(this.config.dir,pathname); // 可能是G:/cgp-server/public 可能是G://cgp-server/public/index.html        //1、根据路径 返回不同结果 如果是文件夹 显示文件夹里的内容        //2、如果是文件 显示文件的内容        try{            let statObj=await stat(p);        }catch (e) {            //文件不存在情况            this.sendError(req,res,e)        }    }复制代码

try catch用于捕获错误,当文件不存在,调用sendError方法,先来实现这个错误的处理方法

4、文件不存在的逻辑,sendError()
sendError(req,res,e){        debug(util.inspect(e)); //输出错误,util模块提供方法        res.statusCode = 404;        res.end('Not Found');    }复制代码

写了这么多了,测试下错误文件能否打印错误

测试完美,此时我们应该判断打开的是文件还是目录,并给对应的方法,下面我们开始目录的渲染方法

5、ejs渲染目录列表

  • 先声明一个template模板,挂载到实例上
let template = fs.readFileSync(path.join(__dirname,'tmpl.html'),'utf8'); //读取ejs的模板文件class Server{    constructor(){        this.template = template //挂载到实例上    }}复制代码
  • 如果是目录,渲染出一个html页展示目录结构
if(statObj.isDirectory()){                //如果是目录 列出目录内容可以点击                let dirs = await readdir(p); //public下面的目录结构=>[index.html,style.css]                dirs =dirs.map(dir=>{                    return {                        filename:dir,                        path:path.join(pathname,dir)                    }                });                //dirs就是要渲染的数据                 //格式如下[{filename:index.html,path:'/index.html'},{
{filename:style.css,path:''/style.css}}] let str =ejs.render(this.template,{
dirs}); //ejs渲染方法 // console.log(str); res.setHeader('Content-Type', 'text/html;charset=utf-8'); res.end(str); }复制代码
  • 我们来看下tmpl.html模板是怎么写的,,我们只用最简单的,所以应该能看懂
    
Title//循环dirs中的内容到页面中<% dirs.map(item=>{%>
  • <%=item.filename%>
  • <%})%>复制代码

    渲染目录结构,我们已经写完了,测试下看能不能运行

    目前来看,无bug,接下来实现如果是文件的话,直接把文件内容渲染出来

    6、文件的渲染方法,即this.sendFile()方法

    sendFile(req,res,p,statObj){     res.setHeader('Content-Type', mime.getType(p) + ';charset=utf-8');        fs.createReadStream(p).pipe(res);//可读流pipe到可写流}复制代码

    功能已经实现拉,不信我们测下

    • 功能已经实现,但我们要求再增加三个功能
    • 1、检测是否支持缓存
    • 2、检测是否支持压缩
    • 3、检测是否支持范围请求
    6.1增加缓存功能
    • 修改下sendFile()方法添加三个功能
    sendFile(req,res,p,statObj){        // 1、检测是否有缓存        if(this.cache(req,res,p,statObj)){ //如果有缓存            res.statusCode = 304;            res.end();            return        }        //2、检测是否支持压缩            ....        //3、检测是否有范围请求            ....            }复制代码
    • cache()缓存方法

    缓存有两种方式,强制缓存和协商缓存

    • 强制缓存 服务端Catch-Control 、 Expires
    • 协商缓存 服务端Last-Modified 、Etag
    • 协商缓存 客户端if-modified-since if-none-match 与服务端对应
    • 贴下百度缓存的解构给大家看下

    看完这个图,相信大家应该懂啦。下面开始写缓存方法

    cache(req,res,p,statObj){  //实现缓存        /* 强制缓存 服务端 Cache-Control Expires            协商缓存  服务端 Last-Modified Etag            协商缓存  客户端 if-modified-since  if-none-match            etag ctime + 文件的大小            Last-modified ctime            强制缓存            */        res.setHeader('Cache-Control', 'no-cache');        res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toGMTString());//10秒后重新发请求        let etag = statObj.ctime.toGMTString() + statObj.size; //文件修改时间和文件大小        let lastModified = statObj.ctime.toGMTString(); //文件的修改时间        res.setHeader('Etag', etag);        res.setHeader('Last-Modified', lastModified);        let ifNoneMatch = req.headers['if-none-match'];         let ifModifiedSince = req.headers['if-modified-since'];         if (etag != ifNoneMatch) { //不相等,不走缓存            return false;        }        if (lastModified != ifModifiedSince) { //同理            return false;        }        return true; //否则走缓存    }复制代码

    缓存功能写完了,我们测试下设置的头有没有添加上

    缓存我们就已经实现啦

    6.2 实现压缩功能
    • node中zlib提供压缩功能,这里就不讲怎么用啦,
    gzip(req,res,p,statObj){        // 客户端 Accept-Encoding: gzip, deflate, br        // 服务端 Content-Encoding: gzip        let encoding = req.headers['accept-encoding']; //获取请求头的接收的压缩格式        if (encoding) {            if (encoding.match(/\bgzip\b/)) {                res.setHeader('Content-Encoding', 'gzip')                return zlib.createGzip();//返回一个gzip的压缩流            } else if (encoding.match(/\bdeflate\b/)) {                res.setHeader('content-encoding', 'deflate');                return zlib.createDeflate(); //返回createDeflate的压缩流            } else {                return false; //否则不支持压缩            }        } else {            return false;//否则不支持压缩        }    }复制代码
    • 修改下sendFile()方法
    sendFile(req,res,p,statObj){        // 1、检测是否有缓存        if(this.cache(req,res,p,statObj)){ //如果有缓存            res.statusCode = 304;            res.end();            return        }        //2、检测是否支持压缩        res.setHeader("Content-Type",mime.getType(p)+";charset=utf8");        let compress =this.gzip(req,res,p,statObj);        if(compress){ //检测是否压缩。返回的是压缩流           return fs.createReadStream(p).pipe(compress).pipe(res);        }else{ //不支持压缩直接把文件读出来即可           return fs.createReadStream(p).pipe(res)        }        //3、检测是否有范围请求            ....            }复制代码

    用1.txt文件测试下

    目前来看都还ok,还剩最后一个功能,实现范围请求

    6.3 实现范围请求功能
    • 客户端发送Range:bytes=0-3
    • 服务端对应Accept-Range:bytes Content-Range:bytes 0-3/xxx Content-Length:xxx

    由于可能同时会有压缩和范围请求,我们稍微改下前面的代码

    sendFile(req,res,p,statObj){        // 1、检测是否有缓存            ....        //2、检测是否支持压缩同时加上范围请求        res.setHeader("Content-Type",mime.getType(p)+";charset=utf8");        let compress =this.gzip(req,res,p,statObj);        let {start,end} = this.range(req,res,p,statObj); //解构开始和结束的位置        if(compress){ //检测是否压缩。返回的是压缩流           return fs.createReadStream(p,{start,end}).pipe(compress).pipe(res);        }else{            // res.setHeader("Content-Type",mime.getType(p)+";charset=utf8");           return fs.createReadStream(p,{start,end}).pipe(res)        }    }复制代码
    • range()范围请求的方法
    range(req, res, statObj, p) {        //客户端 Range:bytes=0-3        //服务端 Accept-Range:bytes Content-Range:bytes 0-3/8777        let range = req.headers['range']; //如果有范围请求        if (range) {            let [, start, end] = range.match(/(\d*)-(\d*)/); //解构出开始和结束的位置            start = start ? Number(start) : 0; //start设置默认值            end = end ? Number(end) : statObj.size - 1; //end设置默认值            res.statusCode = 206; //状态码 206范围请求            res.setHeader('Accept-Ranges',"bytes");            res.setHeader('Content-Length',end-start+1);            res.setHeader('Content-Range',`bytes ${start}-${end}/${statObj.size}`);            return {start,end};        }else {            return {start:0, end:statObj.size};        }    }复制代码

    基本功能已经实现,测试下代码,我们用curl工具发送请求,

    • 1.txt的内容123456789。我们只想要前4个字符

    测试完美,接下来我们还想实现输入cgp-server ,自动开启浏览器,打开目录。我们需要引用一个模块 yargs

    7、yargs模块配置命令行的输入

    • `
    • 这里我们只用最基本用法,一看就懂,详细了解请看官网
    7.1 npm link作用
    • npm link命令可以将一个任意位置的npm包链接到全局执行环境,从而在任意位置使用命令行都可以直接运行该npm包。

    7.2 修改下package.json文件

    7.3 www.js的配置
    7.3.1我们把app.js主文件导出给www.js使用

    7.3.2修改www.js文件
    #! /usr/bin/env node   //执行命令后会执行 bin/www.jsconst yargs = require('yargs');let argv = yargs.option('port',{ //yargs的基础用法    alias: 'p', //别名    default: 3000, //默认值    description:'this is port',  //描述    demand:false // 是否必须}).option('hostname',{    alias: 'h',    default: 'localhost',    description:'this is hostname',    demand:false}).option('dir',{    alias: 'd',    default: process.cwd(),    description:'this is cwd',    demand:false}).usage('cgp-server  [options]' ).argv;//开启服务let Server = require('../src/app.js');new Server(argv).start();// 判断是win还是mac平台let platform = require('os').platform();//开启子进程let {
    exec} = require('child_process');//win系统 win32if(platform==="win32"){ exec(`start http://${argv.hostname}:${argv.port}`)}else { exec(`open http://${argv.hostname}:${argv.port}`)}复制代码
    • 简单介绍yargs用法。然后我们输入 cgp-server --help看效果
    yargs.option('port',{ //yargs的基础用法    alias: 'p', //别名    default: 3000, //默认值    description:'this is port',  //描述    demand:false // 是否必须})复制代码

    • 解释下流程,先开启服务,然后判断系统,再然后根据不同的平台执行自动打开浏览器

    测试下看能不能启动

    结尾

    如果你能看到这里,真的不容易,点个赞再走吧,

    转载地址:http://cdywl.baihongyu.com/

    你可能感兴趣的文章
    Java虚拟机参数配置
    查看>>
    RHCE 学习笔记(31) - 防火墙 (中)
    查看>>
    XSS研究4-来自外部的XSS攻击的防范
    查看>>
    Spring知识点总结-1
    查看>>
    微软私有云分享(R2)21 BMC提升B格
    查看>>
    MDSF:如何使用GMF来做TOGAF建模工具
    查看>>
    Spring Security简介
    查看>>
    打造一流的研发中心
    查看>>
    MCollective架构篇3-Puppet插件的部署及测试
    查看>>
    配置GNS使用CRT连接
    查看>>
    Java:集合类性能分析
    查看>>
    创建Server 2012 VHDX虚拟磁盘模板
    查看>>
    IE调试网页之五:使用 F12 开发人员工具调试 JavaScript 错误 (Windows)
    查看>>
    《kali linux 渗透测试初级教程》免费下载
    查看>>
    [Oracle]PDB Clone 方法
    查看>>
    JavaScript词法作用域与调用对象
    查看>>
    Python天天美味(10) - 除法小技巧
    查看>>
    模板方法在Spring事务中的应用
    查看>>
    Ext.LoadMask遮罩的效果几种实现方式
    查看>>
    理解SQL SERVER中非聚集索引的覆盖,连接,交叉和过滤
    查看>>