Node项目实战-静态资源服务器

打开github,在github上创建新项目:

Repository name: anydoor
Descripotion: Tiny NodeJS Static Web server

选择:public
选择:Initialize this repository with a README
添加gitignore文件:Add .gitignore:Node
添加License文件:Add a license: MIT License

git clone 该项目地址到本地文件夹

.gitignore

https://git-scm.com/docs/gitignore

.npmignore

https://docs.npmjs.com/misc/developers

代码一致性

https://editorconfig.org/

ESLint

https://editorconfig.org/

安装一个颜色插件chalk

npm init //初始化项目
npm -i chalk

NodeJS在服务器上构建web server

const http = require('http');
const chalk = require('chalk');
const conf = require('./config/defaultConf')

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type','text/plain');    // 输出是文本
  res.end('Hello My Friends!');
});

server.listen(conf.port, conf.hostname, () => {
  const addr = `http://${conf.hostname}:${conf.port}`;
  console.info(`Server started at ${chalk.green(addr)}`)
});

输入 node app.js:

Server started at http://127.0.0.1:9000
在网页可以输出结果:
Hello My Friends!

可以改为html代码显示效果,改变'Content-Type'为'text/html':

const http = require('http');
const chalk = require('chalk');
const conf = require('./config/defaultConf')

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type','text/html');  // 可以改为html输出效果
  res.write('<html>')
  res.write('<body>')
  res.write('Hello My Friends!');
  res.write('</body>')
  res.write('</html>')
  res.end();
});

server.listen(conf.port, conf.hostname, () => {
  const addr = `http://${conf.hostname}:${conf.port}`;
  console.info(`Server started at ${chalk.green(addr)}`)
});

为了调试方便,安装supervisor
sudo npm -g install supervisor
输入命令supervisor app.js

Running node-supervisor with
program 'app.js'
--watch '.'
--extensions 'node,js'
--exec 'node'
Starting child process with 'node app.js'

实现效果:如何是目录,输出目录下所有文件,如何是文件,输出文件内容:

const http = require('http');
const chalk = require('chalk');
const path = require('path');
const fs = require('fs');
const conf = require('./config/defaultConf')

const server = http.createServer((req, res) => {
  const filePath = path.join(conf.root, req.url);
  fs.stat(filePath, (err, stats) => {
    if (err) {
      res.statusCode = 404;
      res.setHeader('Content-Type', 'text/plain');
      res.end(`${filePath} is not a directory or file`);
      return;
    }

    if (stats.isFile()) {
      res.statusCode = 200;
      res.setHeader('Content-Type', 'text/plain');
      // fs.readFile(filePath, (err, data) => {
      //   res.end(data);
      // }); //读完才开始,响应速度慢,不推荐
      fs.createReadStream(filePath).pipe(res);
    } else if (stats.isDirectory()) {
      fs.readdir(filePath, (err, files) => {
        res.statusCode = 200;
        res.setHeader('Content-Type', 'text/plain');
        res.end(files.join(','))
      });
    }
  });
});

server.listen(conf.port, conf.hostname, () => {
  const addr = `http://${conf.hostname}:${conf.port}`;
  console.info(`Server started at ${chalk.green(addr)}`)
});

需要解决回调地狱的问题:
修改为两个文件,app.js 和route.js
app.js:

const http = require('http');
const chalk = require('chalk');
const path = require('path');
const conf = require('./config/defaultConf')
const route = require('./helper/route')

const server = http.createServer((req, res) => {
  const filePath = path.join(conf.root, req.url);
  route(req, res, filePath);
});

server.listen(conf.port, conf.hostname, () => {
  const addr = `http://${conf.hostname}:${conf.port}`;
  console.info(`Server started at ${chalk.green(addr)}`)
});

使用了promisify函数,并用同步解决异步问题: asyc和await两个都不能少!
route.js

const fs = require('fs');
const promisify = require('util').promisify;  // 去回调
const stat = promisify(fs.stat);
const readdir = promisify(fs.readdir);

module.exports = async function (req, res, filePath) {
  try {
    const stats = await stat(filePath);
    if (stats.isFile()) {
      res.statusCode = 200;
      res.setHeader('Content-Type', 'text/plain');
      fs.createReadStream(filePath).pipe(res);
    } else if (stats.isDirectory()) {
      const files = readdir(filePath);
      res.statusCode = 200;
      res.setHeader('Content-Type', 'text/plain');
      res.end(files.join(','))
    }
  } catch(ex) {
    res.statusCode = 404;
    res.setHeader('Content-Type', 'text/plain');
    res.end(`${filePath} is not a directory or file`);
  }
}

上面出现错误:修改代码如下,readdir前面漏了await

const fs = require('fs');
const promisify = require('util').promisify;  // 去回调
const stat = promisify(fs.stat);
const readdir = promisify(fs.readdir);

module.exports = async function (req, res, filePath) {
  try {
    const stats = await stat(filePath); //不加await会出现不把当成异步
    if (stats.isFile()) {
      res.statusCode = 200;
      res.setHeader('Content-Type', 'text/plain');
      fs.createReadStream(filePath).pipe(res);
    } else if (stats.isDirectory()) {
      const files = await readdir(filePath);
      res.statusCode = 200;
      res.setHeader('Content-Type', 'text/plain');
      res.end(files.join(','))
    }
  } catch(ex) {
    res.statusCode = 404;
    res.setHeader('Content-Type', 'text/plain');
    res.end(`${filePath} is not a directory or file\n }`);
  }
}

安装并使用handlebars

npm i handlebars

模板文件dir.tpl:

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>{{title}}</title>
    <style media="screen">
      body {
        margin: 30px;
      }
      a {
        display: block;
        font-size: 30px;
      }
    </style>
  </head>
  <body>
    {{#each files}}
      <a href="{{../dir}}/{{file}}">[{{icon}}] - {{file}}</a>
    {{/each}}
  </body>
</html>

配置文件:

module.exports = {
  root: process.cwd(),
  hostname: '127.0.0.1',
  port:9000,
  compress: /\.(html|js|css|md)/
};

压缩文件,可以使用js内置的压缩方法,可以大大节省带宽和下载速度:

const {createGzip, createDeflate} = require('zlib');

module.exports = (rs, req, res) => {
  const acceptEncoding = req.headers['accept-encoding'];
  if(!acceptEncoding || !acceptEncoding.match(/\b(gzip|deflate)\b/)) {
    return rs;
  }else if(acceptEncoding.match(/\bgzip\b/)) {
    res.setHeader('Content-Encoding', 'gzip');
    return rs.pipe(createGzip());
  }else if(acceptEncoding.match(/\bdeflate\b/)) {
    res.setHeader('Content-Encoding', 'defalate');
    return rs.pipe(createDeflate());
  }
};

核心处理代码route.js:

const fs = require('fs');
const path = require('path');
const Handlebars = require('handlebars');
const promisify = require('util').promisify;  // 去回调
const stat = promisify(fs.stat);
const readdir = promisify(fs.readdir);
const config = require('../config/defaultConf');   //require可以放心使用相对路径
const mime = require('./mime');
const compress = require('./compress');

const tplPath = path.join(__dirname, '../template/dir.tpl');
const source = fs.readFileSync(tplPath);  //只执行一次,下面内容之前必须提前加载好,所以用同步
const template = Handlebars.compile(source.toString());

module.exports = async function (req, res, filePath) {
  try {
    const stats = await stat(filePath); //不加await会出现不把当成异步
    if (stats.isFile()) {
      const contentType = mime(filePath);
      res.statusCode = 200;
      res.setHeader('Content-Type', contentType);
      let rs = fs.createReadStream(filePath);
      if (filePath.match(config.compress)) {
        rs = compress(rs, req, res);
      }
      rs.pipe(res);
    } else if (stats.isDirectory()) {
      const files = await readdir(filePath);
      res.statusCode = 200;
      res.setHeader('Content-Type', 'text/html');
      const dir = path.relative(config.root, filePath);
      const data = {
        title: path.basename(filePath),
        dir: dir?`/${dir}`:'',
        // files // ES6语法简写 files:files
        files: files.map(file => {
          return {
            file,
            icon: mime(file)
          }
        })
      };
      res.end(template(data));
    }
  } catch(ex) {
    res.statusCode = 404;
    res.setHeader('Content-Type', 'text/plain');
    res.end(`${filePath} is not a directory or file\n }`);
  }
}

服务器相关代码app.js:

const http = require('http');
const chalk = require('chalk');
const path = require('path');
const conf = require('./config/defaultConf')
const route = require('./helper/route')

const server = http.createServer((req, res) => {
  const filePath = path.join(conf.root, req.url);
  route(req, res, filePath);
});

server.listen(conf.port, conf.hostname, () => {
  const addr = `http://${conf.hostname}:${conf.port}`;
  console.info(`Server started at ${chalk.green(addr)}`)
});

文件传输类型mime.js:

const path = require('path');

const mimeTypes = {
  '323':	'text/h323',
  'acx':	'application/internet-property-stream',
  'ai':	'application/postscript',
  'aif':	'audio/x-aiff',
  'aifc':	'audio/x-aiff',
  'aiff':	'audio/x-aiff',
  'asf':	'video/x-ms-asf',
  'asr':	'video/x-ms-asf',
  'asx':	'video/x-ms-asf',
  'au':	'audio/basic',
  'avi':	'video/x-msvideo',
  'axs':	'application/olescript',
  'bas':	'text/plain',
  'bcpio':	'application/x-bcpio',
  'bin':	'application/octet-stream',
  'bmp':	'image/bmp',
  'c':	'text/plain',
  'cat':	'application/vnd.ms-pkiseccat',
  'cdf':	'application/x-cdf',
  'cer':	'application/x-x509-ca-cert',
  'class':	'application/octet-stream',
  'clp':	'application/x-msclip',
  'cmx':	'image/x-cmx',
  'cod':	'image/cis-cod',
  'cpio':	'application/x-cpio',
  'crd':	'application/x-mscardfile',
  'crl':	'application/pkix-crl',
  'crt':	'application/x-x509-ca-cert',
  'csh':	'application/x-csh',
  'css':	'text/css',
  'dcr':	'application/x-director',
  'der':	'application/x-x509-ca-cert',
  'dir':	'application/x-director',
  'dll':	'application/x-msdownload',
  'dms':	'application/octet-stream',
  'doc':	'application/msword',
  'dot':	'application/msword',
  'dvi':	'application/x-dvi',
  'dxr':	'application/x-director',
  'eps':	'application/postscript',
  'etx':	'text/x-setext',
  'evy':	'application/envoy',
  'exe':	'application/octet-stream',
  'fif':	'application/fractals',
  'flr':	'x-world/x-vrml',
  'gif':	'image/gif',
  'gtar':	'application/x-gtar',
  'gz':	'application/x-gzip',
  'h':	'text/plain',
  'hdf':	'application/x-hdf',
  'hlp':	'application/winhlp',
  'hqx':	'application/mac-binhex40',
  'hta':	'application/hta',
  'htc':	'text/x-component',
  'htm':	'text/html',
  'html':	'text/html',
  'htt':	'text/webviewhtml',
  'ico':	'image/x-icon',
  'ief':	'image/ief',
  'iii':	'application/x-iphone',
  'ins':	'application/x-internet-signup',
  'isp':	'application/x-internet-signup',
  'jfif':	'image/pipeg',
  'jpe':	'image/jpeg',
  'jpeg':	'image/jpeg',
  'jpg':	'image/jpeg',
  'js':	'application/x-javascript',
  'latex':	'application/x-latex',
  'lha':	'application/octet-stream',
  'lsf':	'video/x-la-asf',
  'lsx':	'video/x-la-asf',
  'lzh':	'application/octet-stream',
  'm13':	'application/x-msmediaview',
  'm14':	'application/x-msmediaview',
  'm3u':	'audio/x-mpegurl',
  'man':	'application/x-troff-man',
  'mdb':	'application/x-msaccess',
  'me':	'application/x-troff-me',
  'mht':	'message/rfc822',
  'mhtml':	'message/rfc822',
  'mid':	'audio/mid',
  'mny':	'application/x-msmoney',
  'mov':	'video/quicktime',
  'movie':	'video/x-sgi-movie',
  'mp2':	'video/mpeg',
  'mp3':	'audio/mpeg',
  'mpa':	'video/mpeg',
  'mpe':	'video/mpeg',
  'mpeg':	'video/mpeg',
  'mpg':	'video/mpeg',
  'mpp':	'application/vnd.ms-project',
  'mpv2':	'video/mpeg',
  'ms':	'application/x-troff-ms',
  'mvb':	'application/x-msmediaview',
  'nws':	'message/rfc822',
  'oda':	'application/oda',
  'p10':	'application/pkcs10',
  'p12':	'application/x-pkcs12',
  'p7b':	'application/x-pkcs7-certificates',
  'p7c':	'application/x-pkcs7-mime',
  'p7m':	'application/x-pkcs7-mime',
  'p7r':	'application/x-pkcs7-certreqresp',
  'p7s':	'application/x-pkcs7-signature',
  'pbm':	'image/x-portable-bitmap',
  'pdf':	'application/pdf',
  'pfx':	'application/x-pkcs12',
  'pgm':	'image/x-portable-graymap',
  'pko':	'application/ynd.ms-pkipko',
  'pma':	'application/x-perfmon',
  'pmc':	'application/x-perfmon',
  'pml':	'application/x-perfmon',
  'pmr':	'application/x-perfmon',
  'pmw':	'application/x-perfmon',
  'pnm':	'image/x-portable-anymap',
  'pot,':	'application/vnd.ms-powerpoint',
  'ppm':	'image/x-portable-pixmap',
  'pps':	'application/vnd.ms-powerpoint',
  'ppt':	'application/vnd.ms-powerpoint',
  'prf':	'application/pics-rules',
  'ps':	'application/postscript',
  'pub':	'application/x-mspublisher',
  'qt':	'video/quicktime',
  'ra':	'audio/x-pn-realaudio',
  'ram':	'audio/x-pn-realaudio',
  'ras':	'image/x-cmu-raster',
  'rgb':	'image/x-rgb',
  'rmi':	'audio/mid',
  'roff':	'application/x-troff',
  'rtf':	'application/rtf',
  'rtx':	'text/richtext',
  'scd':	'application/x-msschedule',
  'sct':	'text/scriptlet',
  'setpay':	'application/set-payment-initiation',
  'setreg':	'application/set-registration-initiation',
  'sh':	'application/x-sh',
  'shar':	'application/x-shar',
  'sit':	'application/x-stuffit',
  'snd':	'audio/basic',
  'spc':	'application/x-pkcs7-certificates',
  'spl':	'application/futuresplash',
  'src':	'application/x-wais-source',
  'sst':	'application/vnd.ms-pkicertstore',
  'stl':	'application/vnd.ms-pkistl',
  'stm':	'text/html',
  'svg':	'image/svg+xml',
  'sv4cpio':	'application/x-sv4cpio',
  'sv4crc':	'application/x-sv4crc',
  'swf':	'application/x-shockwave-flash',
  't':	'application/x-troff',
  'tar':	'application/x-tar',
  'tcl':	'application/x-tcl',
  'tex':	'application/x-tex',
  'texi':	'application/x-texinfo',
  'texinfo':	'application/x-texinfo',
  'tgz':	'application/x-compressed',
  'tif':	'image/tiff',
  'tiff':	'image/tiff',
  'tr':	'application/x-troff',
  'trm':	'application/x-msterminal',
  'tsv':	'text/tab-separated-values',
  'txt':	'text/plain',
  'uls':	'text/iuls',
  'ustar':	'application/x-ustar',
  'vcf':	'text/x-vcard',
  'vrml':	'x-world/x-vrml',
  'wav':	'audio/x-wav',
  'wcm':	'application/vnd.ms-works',
  'wdb':	'application/vnd.ms-works',
  'wks':	'application/vnd.ms-works',
  'wmf':	'application/x-msmetafile',
  'wps':	'application/vnd.ms-works',
  'wri':	'application/x-mswrite',
  'wrl':	'x-world/x-vrml',
  'wrz':	'x-world/x-vrml',
  'xaf':	'x-world/x-vrml',
  'xbm':	'image/x-xbitmap',
  'xla':	'application/vnd.ms-excel',
  'xlc':	'application/vnd.ms-excel',
  'xlm':	'application/vnd.ms-excel',
  'xls':	'application/vnd.ms-excel',
  'xlt':	'application/vnd.ms-excel',
  'xlw':	'application/vnd.ms-excel',
  'xof':	'x-world/x-vrml',
  'xpm':	'image/x-xpixmap',
  'xwd': 	'image/x-xwindowdump',
  'z': 	'application/x-compress',
  'zip': 	'application/zip'
}

module.exports = (filePath) => {
  let ext = path.extname(filePath).split('.').pop().toLowerCase();

  if (!ext) {
    ext = filePath;
  }
  return mimeTypes[ext]||mimeTypes['txt'];
};

range

  • range:bytes = [start]-[end]
  • Accept-Range:bytes
  • Content-Range:bytes start-end/total

增加range.js

module.exports = (totalSize, req, res) => {
  const range = req.headers['range'];
  if(!range) {
    return {code:200};
  }

  const sizes = range.match(/bytes=(\d*)-(\d*)/);
  const end = sizes[2] || totalSize - 1;
  const start = sizes[1] || totalSize - end;

  if(start > end || start < 0 || end > totalSize) {
    return {code:200};
  }

  res.setHeader('Accept-Ranges', 'bytes');
  res.setHeader('Content-Range', `bytes ${start}-${end}/${totalSize}`);
  res.setHeader('Content-Length', end - start);
  return {
    code: 206,
    start: parseInt(start),
    end: parseInt(end)
  }
};

修改了route.js部分代码:

let rs;
      const {code, start, end} = range(stats.size, req, res);
      if(code === 200) {
        rs = fs.createReadStream(filePath);
      }else{
        rs = fs.createReadStream(filePath, {start, end});
      }

用curl可以查看内容:

curl -r 0-10 -i http://127.0.0.1:9000/LICENSE

显示结果,使用range拿到了文件的部分内容:

HTTP/1.1 200 OK
Content-Type: text/plain
Accept-Ranges: bytes
Content-Range: bytes 0-10/1065
Content-Length: 10
Date: Wed, 12 Dec 2018 05:10:45 GMT
Connection: keep-alive

MIT Licens

缓存

缓存原理图
缓存原理图

缓存header

  • Expires, Cache-Control
  • If-Modified-Since / Last-Modified
  • If-None-Match/ETag文件改变就变化的值

cache.js

const {cache} = require('../config/defaultConf');

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;  //缓存可用
};

在加载资源之前,可以添加:

if(isFresh(stats, req, res)) {
        res.statusCode = 304;
        res.end();
        return;
      }

安装命令行工具:npm i yargs
index.js命令行代码:

// process.argv  -p --port=8080
// 现有工具 commander yargs

const yargs = require('yargs');
const Server = require('./app');

const argv = yargs
      .usage('anywhere [options]')
      .option('p', {
        alias: 'port',
        describe: '端口号',
        default: 9000
      })
      .option('h', {
        alias: 'hostname',
        describe: 'host',
        default: '127.0.0.1'
      })
      .option('d', {
        alias: 'root',
        describe: 'root path',
        default: process.cwd()
      })
      .version()
      .alias('v', 'version')
      .help()
      .argv;

const server = new Server(argv);
server.start();

app.js

const http = require('http');
const chalk = require('chalk');
const path = require('path');
const conf = require('./config/defaultConf');
const route = require('./helper/route');
const openUrl = require('./helper/openUrl');

class Server {

  constructor (config) {
    this.conf = Object.assign({}, conf, config);
  }

  start() {
    const server = http.createServer((req, res) => {
      const filePath = path.join(this.conf.root, req.url);
      route(req, res, filePath, this.conf);
    });

    server.listen(this.conf.port, this.conf.hostname, () => {
      const addr = `http://${this.conf.hostname}:${this.conf.port}`;
      console.info(`Server started at ${chalk.green(addr)}`)
      openUrl(addr);
    });
  }
}

module.exports = Server;

posted @ 2018-12-10 14:07  cicarius  阅读(936)  评论(0编辑  收藏  举报