Node实现cli(类似http-server)

本文详细介绍如何使用Node.js从零开始搭建一个自定义服务器,包括处理HTTP请求、响应、文件读取、模板渲染及缓存策略。通过实战,读者将掌握服务器基本原理及优化技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录结构

  • bin
    • www.js
  • src
    • template.html
    • server.js
  • package.json
package.json 结构
    "bin": {
        "jw-server": "./bin/www.js"
    },
复制代码
www.js
    #! /usr/bin/env node

    // commander 命令行提示工具 可以帮你处理参数
    
    // 知道启动服务的端口号 --port -p
    //        改执行的目录 --dir  -d
    //        改主机名     --address -a
    let commander = require('commander');
    // vue init xxx => 生成项目  jw-server log
    let version = require('../package.json').version
    let args = commander
      .version(version, '-v, --version') // 设置版本号
      .option('-p, --port <n>', 'server port')
      .option('-a, --address <n>', 'server address')
      .option('-d, --dir <n>', 'server show list')
      // 组成一个动作
      .usage('[options] <jw-server --port 3000>');
    
    // commander
    //   .command('log', 'print log')
    //   .action(function () {
    //     console.log('log')
    //   });
    // commander
    //   .command('init', 'init project')
    //   .action(function () {
    //     console.log('init');
    //   });
    commander.on('--help', function () {
      console.log('');
      console.log('jw-server:');
      console.log('  $ jw-server --port');
      console.log('  $ jw-server --address');
    });
    
    commander.parse(process.argv);
    
    // 默认配置
    let config =Object.assign({
      dir:process.cwd(),
      address:'localhost',
      port:8080
    },args);
    
    
    // 启动一个服务
    // 写一个创建server的功能
    let Server = require('../src/server.js')
    let server = new Server(config);
    server.start();
复制代码
template.html
    <!DOCTYPE html>
    <html lang="en">
    <head>
     <meta charset="UTF-8"/>
     <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
     <meta http-equiv="X-UA-Compatible" content="ie = edge"/>
     <title>珠峰架构师</title>
    </head>
    <body>
      
      <%dirs.forEach(f=>{%>
        <li><a href="<%=f.url%>"><%=f.dir%></a></li>
      <%})%>
    </body>
    </html>
复制代码
server.js(支持压缩,缓存)
    let http = require('http');
    let chalk = require('chalk');
    let url = require('url');
    let fs = require('mz/fs');
    let mime = require('mime');
    let path = require('path');
    let ejs = require('ejs');
    let zlib = require('zlib');
    
    let tmplStr = fs.readFileSync(path.join(__dirname,'template.html'),
    'utf8');
    // koa
    class Server {
      constructor(config) {
        this.port = config.port;
        this.address = config.address;
        this.dir = config.dir; // dir表示当前启动的目录 / 手动指定的
        this.tmpl = tmplStr;
      }
      handleRequest() { // 处理请求的方法
        return async (req, res) => {
          // 需要判断当前请求的内容是文件还是文件
          let { pathname } = url.parse(req.url);
          // 处理文件名为中文
          pathname = decodeURI(pathname);
          if(pathname === '/favicon.ico') return this.sendError('找不到');
          let realPath = path.join(this.dir, pathname);
          try {
            let statObj = await fs.stat(realPath);
            if (statObj.isFile()) { // 文件操作
              this.sendFile(req, res, realPath, statObj);
            } else { // 目录操作
              let dirs = await fs.readdir(realPath);// ['www.js']
              // 渲染出一个渲染后的字符串
              // dirs应该包含 当前点击的链接 和显示的路径
              dirs = dirs.map((dir)=>{
                return {url:path.join(pathname,dir),dir}
              })
              let renderStr = ejs.render(this.tmpl, { dirs});
              res.setHeader('Content-Type','text/html;charset=utf8');
              res.end(renderStr);
            }
          } catch (e) {this.sendError(e,res);}
        }
      }
      // gzip()
      gzip(req,res){
        let encoding = req.headers['accept-encoding'];
        if(encoding.includes('gzip')){
          res.setHeader('Content-Encoding','gzip')
          return zlib.createGzip();
        }
        if(encoding.includes('deflate')){
          res.setHeader('Content-Encoding', 'deflate')
          return zlib.createDeflate();
        }
        return false;
      }
      // 缓存 304 
      cache(req, res, path, statObj){ // 设置强制缓存  对比缓存
        res.setHeader('Cache-Control','max-age=30');
        res.setHeader('Expires',new Date(Date.now()+5*1000).toGMTString());
        let ctime = statObj.ctime.toLocaleString();
        let etag = ctime + '_' + statObj.size
        res.setHeader('Last-Modified', ctime);
        // 最好能不读取文件 就不要读取文件
        res.setHeader('Etag', etag )
        // last-modified
        let ifModifiedSince = req.headers['if-modified-since']
        // etag
        let ifNoneMatch = req.headers['if-none-match'];
        // 对比缓存
        console.log(ifModifiedSince , ifNoneMatch)
        if (ifModifiedSince && ifNoneMatch){
          if (ifModifiedSince === ctime && ifNoneMatch === etag  ){
            return true;
          }else{
            return false;
          }
        }else{
          return false;
        }
    
      }
    
      sendFile(req,res,path,statObj){
        // 范围请求 start ,end
    
    
        // 先判断有没有缓存 有缓存 走缓存  boolean
        if (this.cache(req, res, path, statObj)){
          return res.statusCode = 304, res.end();
        }
    
        // gzip压缩 转化流
        // Accept-Encoding: gzip, deflate, br
        res.setHeader('Content-Type', mime.getType(path) + ';charset=utf-8');
        // 返回的文件需要压缩
        let zip;
        if (zip = this.gzip(req,res)){ // 调用方法后返回的是一个转化流
          return fs.createReadStream(path).pipe(zip).pipe(res);
        }
        fs.createReadStream(path).pipe(res);
      }
      sendError(e,res) {
        console.log(e);
        res.statusCode = 404;
        res.end(`Not found`);
      }
      start() {
        let server = http.createServer(this.handleRequest());
        server.listen(this.port, this.address, () => {
          console.log(chalk.yellow(`Starting up http-server, serving ./
          Available on:`
          ));
          console.log(`  http://${this.address}:${chalk.green(this.port)}`)
        });
      }
    }
    
    module.exports = Server;
复制代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值