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;

转载于:https://www.cnblogs.com/carious/p/10096160.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值