打開github,在github上建立新項目:javascript
Repository name: anydoor
Descripotion: Tiny NodeJS Static Web servercss
選擇:public
選擇:Initialize this repository with a README
添加gitignore文件:Add .gitignore:Node
添加License文件:Add a license: MIT Licensehtml
git clone 該項目地址到本地文件夾java
https://git-scm.com/docs/gitignorenode
https://docs.npmjs.com/misc/developersgit
https://editorconfig.org/github
安裝一個顏色插件chalknpm
npm init //初始化項目
npm -i chalk緩存
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 }`); } }
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.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-aliveMIT Licens
緩存原理圖
緩存header
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;