美文网首页
NodeJS(五):静态资源服务器

NodeJS(五):静态资源服务器

作者: 林ze宏 | 来源:发表于2020-09-15 21:49 被阅读0次

    1 前言

    通过 http 模块,快速创建一个 http server demo,为了在控制台打印出有颜色的信息,可以安装 chalk 库:

    npm i chalk

    const http = require('http');
    const chalk = require('chalk');
    
    const hostname = '127.0.0.1';
    const port = 3000;
    
    const server = http.createServer((req, res) => {
      res.statusCode = 200;
      // res.end('Hello, World!\n');
      // res.setHeader('Content-Type', 'text/plain');
      res.setHeader('Content-Type', 'text/html;charset=utf-8');
      res.write('<http>')
      res.write('<body>')
      res.write('<h1>')
      res.write('测试')
      res.write('</h1>')
      res.write('</body>')
      res.write('</http>')
      res.end();
    });
    
    server.listen(port, hostname, () => {
      console.log(`服务器运行在 http://${chalk.green(hostname)}:${chalk.blue(port)}/`);
    });
    
    

    在更改 node 代码的时候,需要不断的重启 node 服务,解决方法,可以全局安装 supervisor ,通过 supervisor 监听文件的变化,自动重启:

    npm install supervisor -g

    Examples:
    supervisor myapp.js
    supervisor myapp.coffee
    supervisor -w scripts -e myext -x myrunner myapp
    supervisor -w lib,server.js,config.js server.js
    supervisor -- server.js -h host -p port
    
    

    2 实例

    根据访问的 url,如果 url 为文件,则读取文件的内容,如果为文件夹,则展示文件夹中的所有文件。

    const http = require('http');
    const path = require('path');
    const fs = require('fs');
    const chalk = require('chalk');
    const { hostname, port, root } = require('./config');
    
    const server = http.createServer((req, res) => {
    
      const url = req.url;
      const filePath = path.join(root, url);
    
      fs.stat(filePath, (err, status) => {
        if (err) {
          res.statusCode = 404;
          res.setHeader('Content-Type', 'text/plain;charset=utf-8');
          res.end(`${filePath} 不存在!`);
          return;
        }
        if (status.isFile()) {
          res.statusCode = 404;
          res.setHeader('Content-Type', 'text/plain;charset=utf-8');
          fs.createReadStream(filePath).pipe(res);
          return;
        }
        if (status.isDirectory()) {
          res.statusCode = 404;
          res.setHeader('Content-Type', 'text/plain;charset=utf-8');
          fs.readdir(filePath, (err, files) => {
            if (err) throw err;
            res.end(files.join(', '));
            return;
          })
        }
      });
    });
    
    server.listen(port, hostname, () => {
      console.log(`服务器运行在 http://${chalk.green(hostname)}:${chalk.blue(port)}/`);
    });
    
    
    config / index.js:
    module.exports = {
      root: process.cwd(),  // 查看应用程序当前目录
      hostname: '127.0.0.1',
      port: 3000,
    }
    
    

    针对上面的代码进行异步优化:

    const http = require('http');
    const path = require('path');
    const fs = require('fs');
    const chalk = require('chalk');
    const { hostname, port, root } = require('./config');
    
    const fsPromises = fs.promises;
    
    const server = http.createServer((req, res) => {
      const url = req.url;
      const filePath = path.join(root, url);
      stat(filePath, req, res);
    });
    
    server.listen(port, hostname, () => {
      console.log(`服务器运行在 http://${chalk.green(hostname)}:${chalk.blue(port)}/`);
    });
    
    async function stat(filePath, req, res) {
      try {
        const status = await fsPromises.stat(filePath);
        if (status.isFile()) {
          res.statusCode = 200;
          res.setHeader('Content-Type', 'text/plain;charset=utf-8');
          fs.createReadStream(filePath).pipe(res);
          return;
        }
        if (status.isDirectory()) {
          res.statusCode = 200;
          res.setHeader('Content-Type', 'text/plain;charset=utf-8');
          const files = await fsPromises.readdir(filePath);
          res.end(files.join(', '));
          return;
        }
      } catch (error) {
        res.statusCode = 404;
        res.setHeader('Content-Type', 'text/plain;charset=utf-8');
        res.end(`${filePath} 不存在!`);
        return;
      }
    }
    
    

    进一步扩展功能,目录和文件可以进行点击操作,引入 Handlebars 模板引擎。

    安装 handlebars

    npm install handlebars

    const http = require('http');
    const path = require('path');
    const Handlebars = require("handlebars");
    const fs = require('fs');
    const chalk = require('chalk');
    const { hostname, port, root } = require('./config');
    
    const fsPromises = fs.promises;
    
    // __dirname test.js 所在的目录 -> C:\E\教程\node\node\src
    const templatePath = path.join(__dirname, './template/tpl.html'); // 绝对路径
    const source = fs.readFileSync(templatePath);
    const template = Handlebars.compile(source.toString());
    
    const server = http.createServer((req, res) => {
      const url = req.url;
      const filePath = path.join(root, url);
      stat(filePath, req, res);
    });
    
    server.listen(port, hostname, () => {
      console.log(`服务器运行在 http://${chalk.green(hostname)}:${chalk.blue(port)}/`);
    });
    
    async function stat(filePath, req, res) {
      try {
        const status = await fsPromises.stat(filePath);
        if (status.isFile()) {
          res.statusCode = 200;
          res.setHeader('Content-Type', 'text/plain;charset=utf-8');
          fs.createReadStream(filePath).pipe(res);
          return;
        }
        if (status.isDirectory()) {
          res.statusCode = 200;
          res.setHeader('Content-Type', 'text/html;charset=utf-8');
          const files = await fsPromises.readdir(filePath); // 所有文件和目录
    
          // Solve the relative path from {from} to {to}. At times we have two absolute paths.
          // 当访问 http://127.0.0.1:3000/src 时
          // root -> C:\E\教程\node\node\ 
          // filePath -> C:\E\教程\node\node\src
          // path.relative(root, filePath) -> src
          const dir = path.relative(root, filePath);
    
          const data = {
            title: filePath,
            dir: dir ? `/${dir}` : "",
            files,
          }
          res.end(template(data));
          return;
        }
      } catch (error) {
        res.statusCode = 404;
        res.setHeader('Content-Type', 'text/plain;charset=utf-8');
        res.end(`${filePath} 不存在!`);
        return;
      }
    }
    
    

    模板:

    <!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}}</title>
    </head>
    
    <body>
      <!-- files 为 传递进来对象的数组 -->
      {{#each files}}
      <a href="{{../dir}}/{{this}}">{{this}}</a>
      {{/each}}
    
    </body>
    
    </html>
    
    
    项目图示

    对上面的代码的各种文件请求类型,新增对应的 mimeType 类型

    const http = require('http');
    const path = require('path');
    const Handlebars = require("handlebars");
    const fs = require('fs');
    const chalk = require('chalk');
    const mime = require('./config/mime');
    const { hostname, port, root } = require('./config');
    
    const fsPromises = fs.promises;
    
    // __dirname test.js 所在的目录 -> C:\E\教程\node\node\src
    const templatePath = path.join(__dirname, './template/tpl.html'); // 绝对路径
    const source = fs.readFileSync(templatePath);
    const template = Handlebars.compile(source.toString());
    
    const server = http.createServer((req, res) => {
      const url = req.url;
      const filePath = path.join(root, url);
      stat(filePath, req, res);
    });
    
    server.listen(port, hostname, () => {
      console.log(`服务器运行在 http://${chalk.green(hostname)}:${chalk.blue(port)}/`);
    });
    
    async function stat(filePath, req, res) {
      try {
        const status = await fsPromises.stat(filePath);
        if (status.isFile()) {
          const contentType = mime(filePath);
    
          res.statusCode = 200;
          res.setHeader('Content-Type', `${contentType};charset=utf-8`);
          // res.setHeader('Content-Type', 'text/plain;charset=utf-8');
          fs.createReadStream(filePath).pipe(res);
          return;
        }
        if (status.isDirectory()) {
          res.statusCode = 200;
          res.setHeader('Content-Type', 'text/html;charset=utf-8');
          const files = await fsPromises.readdir(filePath); // 所有文件和目录
    
          // Solve the relative path from {from} to {to}. At times we have two absolute paths.
          // 当访问 http://127.0.0.1:3000/src 时
          // root -> C:\E\教程\node\node\ 
          // filePath -> C:\E\教程\node\node\src
          // path.relative(root, filePath) -> src
          const dir = path.relative(root, filePath);
    
          const data = {
            title: filePath,
            dir: dir ? `/${dir}` : "",
            files,
          }
          res.end(template(data));
          return;
        }
      } catch (error) {
        res.statusCode = 404;
        res.setHeader('Content-Type', 'text/plain;charset=utf-8');
        res.end(`${filePath} 不存在!`);
        return;
      }
    }
    
    
    const path = require('path');
    
    // https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Complete_list_of_MIME_types
    const mimeType = {
      "aac": "audio/aac",
      "abw": "application/x-abiword",
      "arc": "application/x-freearc",
      "avi": "video/x-msvideo",
      "azw": "application/vnd.amazon.ebook",
      "bin": "application/octet-stream",
      "bmp": "image/bmp",
      "bz": "application/x-bzip",
      "bz2": "application/x-bzip2",
      "csh": "application/x-csh",
      "css": "text/css",
      "csv": "text/csv",
      "doc": "application/msword",
      "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      "eot": "application/vnd.ms-fontobject",
      "epub": "application/epub+zip",
      "gif": "image/gif",
      "htm": "text/html",
      "html": "text/html",
      "ico": "image/vnd.microsoft.icon",
      "ics": "text/calendar",
      "jar": "application/java-archive",
      "jpeg": "image/jpeg",
      "jpg": "image/jpeg",
      "js": "text/javascript",
      "json": "application/json",
      "jsonld": "application/ld+json",
      "mid": "audio/midi audio/x-midi",
      "midi": "audio/midi audio/x-midi",
      "mjs": "text/javascript",
      "mp3": "audio/mpeg",
      "mpeg": "video/mpeg",
      "mpkg": "application/vnd.apple.installer+xml",
      "odp": "application/vnd.oasis.opendocument.presentation",
      "ods": "application/vnd.oasis.opendocument.spreadsheet",
      "odt": "application/vnd.oasis.opendocument.text",
      "oga": "audio/ogg",
      "ogv": "video/ogg",
      "ogx": "application/ogg",
      "otf": "font/otf",
      "png": "image/png",
      "pdf": "application/pdf",
      "ppt": "application/vnd.ms-powerpoint",
      "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
      "rar": "application/x-rar-compressed",
      "rtf": "application/rtf",
      "sh": "application/x-sh",
      "svg": "image/svg+xml",
      "swf": "application/x-shockwave-flash",
      "tar": "application/x-tar",
      "tif": "image/tiff",
      "tiff": "image/tiff",
      "ttf": "font/ttf",
      "txt": "text/plain",
      "vsd": "application/vnd.visio",
      "wav": "audio/wav",
      "weba": "audio/webm",
      "webm": "video/webm",
      "webp": "image/webp",
      "woff": "font/woff",
      "woff2": "font/woff2",
      "xhtml": "application/xhtml+xml",
      "xls": "application/vnd.ms-excel",
      "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
      "xml": "application/xml",
      "xul": "application/vnd.mozilla.xul+xml",
      "zip": "application/zip",
      "3gp": "video/3gpp",
      "3g2": "video/3gpp2",
      "7z": "application/x-7z-compressed",
    }
    
    module.exports = (filePath) => {
      const ext = path.extname(filePath)
        .split(".")
        .pop()
        .toLowerCase();
    
      return mimeType[ext] || mimeType['txt'];
    }
    
    

    接着对文件进行压缩处理,利用 zlib 进行 gzip 或者 deflate 压缩:

    主要代码:
    
    const compress = require('./helper/compress');
    
    if (stats.isFile()) {
      const contentType = mime(filePath);
      res.setHeader('Content-Type', `${contentType};charset=utf-8`);
      res.statusCode = 200;
    
      // fs.createReadStream(filePath).pipe(res);
      // 创建可读流
      let rs = fs.createReadStream(filePath);
      if (filePath.match(config.compress)) {
        rs = compress(rs, req, res);
      }
      rs.pipe(res);
      return;
    }
    

    compress.js

    const zlib = require("zlib");
    
    module.exports = (rs, req, res) => {
      // 获取浏览器支持的压缩格式
      let encoding = req.headers["accept-encoding"];
    
      // 支持 gzip 使用 gzip 压缩,支持 deflate 使用 deflate 压缩
      let compress = "";
      let compressType = "";
      if (!encoding || !encoding.match(/\b(gzip|bdeflate)\b/)) {
        return rs;
      } else if (encoding && encoding.match(/\bgzip\b/)) {
        compress = zlib.createGzip();
        compressType = "gzip";
      } else if (encoding && encoding.match(/\bdeflate\b/)) {
        compress = zlib.createDeflate();
        compressType = "deflate";
      }
      // 将压缩流返回并设置响应头
      res.setHeader("Content-Encoding", compressType);
      return rs.pipe(compress);
    }
    
    

    配置文件新增 compress 对特定类型进行压缩

    module.exports = {
      root: process.cwd(), // 查看应用程序当前目录
      hostname: '127.0.0.1',
      port: 3000,
      compress: /\.(html|js|css)/ // 对特定类型进行压缩
    }
    
    

    可以参考:https://blog.csdn.net/github_38140984/article/details/83011150

    接下来对文件进行缓存处理,首先要知道缓存相关的 http 请求字段为:

    • Expires、Cache-Control
    • If-Modified-Since / Last-Modified
    • If-None-Match / ETag
    主要代码:
    
    const isFresh = require('./helper/cache');
    
    if (stats.isFile()) {
      const contentType = mime(filePath);
      res.setHeader('Content-Type', `${contentType};charset=utf-8`);
    
      // 是否可以使用缓存
      if (isFresh(stats, req, res)) {
        res.statusCode = 304;
        res.end();
        return;
      }
    
      res.statusCode = 200;
    
      // fs.createReadStream(filePath).pipe(res);
      // 创建可读流
      let rs = fs.createReadStream(filePath);
      if (filePath.match(config.compress)) {
        rs = compress(rs, req, res);
      }
      rs.pipe(res);
      return;
    }
    

    cache.js

    const { cache } = require('../config');
    
    function refreshRes(stats, res) {
      const { maxAge, expires, cacheControl, lastModified, etag } = cache;
      if (expires) {
        res.setHeader('Expires', (new Date(Date.now() + maxAge * 1000)).toUTCString());
      }
      if (cacheControl) {
        res.setHeader('Cache-Control', `public, max-age=${maxAge}`);
      }
      if (lastModified) {
        res.setHeader('Last-Modified', stats.mtime.toUTCString());
      }
      if (etag) {
        res.setHeader('ETag', `${stats.size}-${stats.mtime}`);
      }
    }
    
    module.exports = function isFresh(stats, req, res) {
      refreshRes(stats, res); // 初始化
    
      const lastModified = req.headers['if-modified-since'];
      const etag = req.headers['if-none-match'];
    
      if (!lastModified && !etag) {
        return false;
      }
    
      if (lastModified && lastModified !== res.getHeader('Last-Modified')) {
        return false;
      }
    
      if (etag && etag !== res.getHeader('ETag')) {
        return false;
      }
    
      return true;
    }
    
    
    module.exports = {
      root: process.cwd(), // 查看应用程序当前目录
      hostname: '127.0.0.1',
      port: 3000,
      compress: /\.(html|js|css)/, // 对特定类型进行压缩
      cache: {
        maxAge: 10 * 60, // 秒
        expires: true,
        cacheControl: true,
        lastModified: true,
        etag: true,
      }
    }
    

    yargs 是 nodejs 环境下的命令行参数解析工具。

    参数
    -p 或者 -p 8080 或者 --port=8080 或者 -p=8080

    可以通过 process.anyv 读取命令行的参数列表

    参考:https://www.jianshu.com/p/7851001dd93b

    扩展:npm 版本号的理解:

    版本号大概可以理解为 x.y.z,x 表示大版本,可以不兼容上一版本,y 表示有新增的功能,必须兼容同一版本,z 表示 fix 一些 bug 等,常见有如下几种格式:

    • 1.2.* :表示 1.2 这个版本,z 是最新的版本号;
    • ~1.2.0 :跟 1.2.* 同样意思;
    • 2.x :表示 y、z 使用最新的版本号
    • ^2.0.0 :跟 2.x 同样意思;

    相关文章

      网友评论

          本文标题:NodeJS(五):静态资源服务器

          本文链接:https://www.haomeiwen.com/subject/ogljfhtx.html