打开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;

Node项目实战-静态资源服务器的更多相关文章

  1. 使用Node.js搭建静态资源服务器

    对于Node.js新手,搭建一个静态资源服务器是个不错的锻炼,从最简单的返回文件或错误开始,渐进增强,还可以逐步加深对http的理解.那就开始吧,让我们的双手沾满网络请求! Note: 当然在项目中如 ...

  2. 原生node写一个静态资源服务器

    myanywhere 用原生node做一个简易阉割版的anywhere静态资源服务器,以提升对node与http的理解. 相关知识 es6及es7语法 http的相关网络知识 响应头 缓存相关 压缩相 ...

  3. NodeJS4-8静态资源服务器实战_构建cli工具

    Cli(command-line interface),中文是 命令行界面,简单来说就是可以通过命令行快速生成自己的项目模板等功能(比较熟悉的是vue-cli脚手架这些),把上述写的包做成Cli工具. ...

  4. [Node]创建静态资源服务器

    项目初始化 .gitignore cnpm i eslint -D eslint --init得到.eslintrc.js .eslintrc.js module.exports = { 'env': ...

  5. 极简 Node.js 入门 - 5.3 静态资源服务器

    极简 Node.js 入门系列教程:https://www.yuque.com/sunluyong/node 本文更佳阅读体验:https://www.yuque.com/sunluyong/node ...

  6. node静态资源服务器的搭建----访问本地文件夹(搭建可访问静态文件的服务器)

    我们的目标是实现一个可访问静态文件的服务器,即可以在浏览器访问文件夹和文件,通过点击来查看文件. 1.先创建一个文件夹anydoor,然后在该文件夹里npm init一个package.json文件, ...

  7. 使用node搭建静态资源服务器

    安装 npm install yumu-static-server -g 使用 shift+鼠标右键  在此处打开Powershell 窗口 server # 会在当前目录下启动一个静态资源服务器,默 ...

  8. 使用 Express 实现一个简单的 SPA 静态资源服务器

    背景 限制 SPA 应用已经成为主流,在项目开发阶段产品经理和后端开发同学经常要查看前端页面,下面就是我们团队常用的使用 express 搭建的 SPA 静态资源服务器方案. 为 SPA 应用添加入口 ...

  9. Nginx——静态资源服务器(一)

    java web的项目中,我们经常将项目部署到Tomcat或者jetty上,可以通过Tomcat或者jetty启动的服务来访问静态资源.但是随着Nginx的普及,用Nginx来作为静态资源服务器,似乎 ...

随机推荐

  1. 常用的js工具函数

    JS选取DOM元素的方法注意:原生JS选取DOM元素比使用jQuery类库选取要快很多1.通过ID选取元素document.getElementById('myid');2.通过CLASS选取元素do ...

  2. Luogu P1462 通往奥格瑞玛的道路 二分答案+最短路

    先二分答案,再跑最短路,跑的时候遇到 过路费超过二分的答案的 就不拿他更新最短路 #include<cstdio> #include<iostream> #include< ...

  3. Hive进阶_Hive的表连接

    等值连接 select e.empno, d.deptno from emp e, dept d where e.deptno=d.deptno; 不等值连接 select e.empno, e.en ...

  4. Vue2之页面 、js 、css分离

    在编写vue的时候,页面数据少的时候,可以将所有的js和css都可以直接写在页面上,但是页面数据多,js.css的方法和样式多的时候,都放在一个页面的时候,就显得页面vue十分的臃肿. 所以写项目的时 ...

  5. .net笔试题二(填空题、选择题)

    1.面向对象的语言具有_______性.________性._______性答:封装.继承.多态. 2.能用foreach遍历访问的对象需要实现 ____________接口或声明__________ ...

  6. JS的使用

    Javascript代码在浏览器中运行,做出更流畅.优美的页面效果,增强用户体验与java是完全不同的东西,只是名称类似而已写在<script></script>标签中 大小写 ...

  7. .net memcache

    非常感谢csdn及冷月宫主让我很快学会了.net操作 memcache 文章转自:http://download.csdn.net/detail/e_wsq/4358982 C#存取Memcache的 ...

  8. hadoop完全分布式模式搭建和hive安装

    简介 Hadoop是用来处理大数据集合的分布式存储计算基础架构.可以使用一种简单的编程模式,通过多台计算机构成的集群,分布式处理大数据集.hadoop作为底层,其生态环境很丰富. hadoop基础包括 ...

  9. The Mythical Man-Month

    大家所熟知的Windows XP操作系统,源代码行数已经达到40百万行.为了连接用户和计算机底层硬件,庞大操作系统这一层太过于复杂,没有一个人能完全理解它如此数量的所有代码,而多人的合作开发又需要它被 ...

  10. IOS 屏幕尺寸、分辨率、点之间的相互关系

    iOS 设备现有的分辨率如下:iPhone/iPod Touch普通屏                         320像素 x 480像素       iPhone 1.3G.3GS,iPod ...