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
代码一致性
ESLint
安装一个颜色插件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-aliveMIT 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;
Node项目实战-静态资源服务器的更多相关文章
- 使用Node.js搭建静态资源服务器
对于Node.js新手,搭建一个静态资源服务器是个不错的锻炼,从最简单的返回文件或错误开始,渐进增强,还可以逐步加深对http的理解.那就开始吧,让我们的双手沾满网络请求! Note: 当然在项目中如 ...
- 原生node写一个静态资源服务器
myanywhere 用原生node做一个简易阉割版的anywhere静态资源服务器,以提升对node与http的理解. 相关知识 es6及es7语法 http的相关网络知识 响应头 缓存相关 压缩相 ...
- NodeJS4-8静态资源服务器实战_构建cli工具
Cli(command-line interface),中文是 命令行界面,简单来说就是可以通过命令行快速生成自己的项目模板等功能(比较熟悉的是vue-cli脚手架这些),把上述写的包做成Cli工具. ...
- [Node]创建静态资源服务器
项目初始化 .gitignore cnpm i eslint -D eslint --init得到.eslintrc.js .eslintrc.js module.exports = { 'env': ...
- 极简 Node.js 入门 - 5.3 静态资源服务器
极简 Node.js 入门系列教程:https://www.yuque.com/sunluyong/node 本文更佳阅读体验:https://www.yuque.com/sunluyong/node ...
- node静态资源服务器的搭建----访问本地文件夹(搭建可访问静态文件的服务器)
我们的目标是实现一个可访问静态文件的服务器,即可以在浏览器访问文件夹和文件,通过点击来查看文件. 1.先创建一个文件夹anydoor,然后在该文件夹里npm init一个package.json文件, ...
- 使用node搭建静态资源服务器
安装 npm install yumu-static-server -g 使用 shift+鼠标右键 在此处打开Powershell 窗口 server # 会在当前目录下启动一个静态资源服务器,默 ...
- 使用 Express 实现一个简单的 SPA 静态资源服务器
背景 限制 SPA 应用已经成为主流,在项目开发阶段产品经理和后端开发同学经常要查看前端页面,下面就是我们团队常用的使用 express 搭建的 SPA 静态资源服务器方案. 为 SPA 应用添加入口 ...
- Nginx——静态资源服务器(一)
java web的项目中,我们经常将项目部署到Tomcat或者jetty上,可以通过Tomcat或者jetty启动的服务来访问静态资源.但是随着Nginx的普及,用Nginx来作为静态资源服务器,似乎 ...
随机推荐
- 常用的js工具函数
JS选取DOM元素的方法注意:原生JS选取DOM元素比使用jQuery类库选取要快很多1.通过ID选取元素document.getElementById('myid');2.通过CLASS选取元素do ...
- Luogu P1462 通往奥格瑞玛的道路 二分答案+最短路
先二分答案,再跑最短路,跑的时候遇到 过路费超过二分的答案的 就不拿他更新最短路 #include<cstdio> #include<iostream> #include< ...
- Hive进阶_Hive的表连接
等值连接 select e.empno, d.deptno from emp e, dept d where e.deptno=d.deptno; 不等值连接 select e.empno, e.en ...
- Vue2之页面 、js 、css分离
在编写vue的时候,页面数据少的时候,可以将所有的js和css都可以直接写在页面上,但是页面数据多,js.css的方法和样式多的时候,都放在一个页面的时候,就显得页面vue十分的臃肿. 所以写项目的时 ...
- .net笔试题二(填空题、选择题)
1.面向对象的语言具有_______性.________性._______性答:封装.继承.多态. 2.能用foreach遍历访问的对象需要实现 ____________接口或声明__________ ...
- JS的使用
Javascript代码在浏览器中运行,做出更流畅.优美的页面效果,增强用户体验与java是完全不同的东西,只是名称类似而已写在<script></script>标签中 大小写 ...
- .net memcache
非常感谢csdn及冷月宫主让我很快学会了.net操作 memcache 文章转自:http://download.csdn.net/detail/e_wsq/4358982 C#存取Memcache的 ...
- hadoop完全分布式模式搭建和hive安装
简介 Hadoop是用来处理大数据集合的分布式存储计算基础架构.可以使用一种简单的编程模式,通过多台计算机构成的集群,分布式处理大数据集.hadoop作为底层,其生态环境很丰富. hadoop基础包括 ...
- The Mythical Man-Month
大家所熟知的Windows XP操作系统,源代码行数已经达到40百万行.为了连接用户和计算机底层硬件,庞大操作系统这一层太过于复杂,没有一个人能完全理解它如此数量的所有代码,而多人的合作开发又需要它被 ...
- IOS 屏幕尺寸、分辨率、点之间的相互关系
iOS 设备现有的分辨率如下:iPhone/iPod Touch普通屏 320像素 x 480像素 iPhone 1.3G.3GS,iPod ...