nodejs实现一个http-server静态文件服务器
目录结构
- bin 存放配置信息和启动程序
- node_modules
- src 存放主体资源
- package.json
- package-lock.json
bin > www.js 启动文件
#! /usr/bin/env node
const program = require('commander');
const options = require('./config');
const examples = new Set();
const defaultMapping = {};
program.name('fs');
program.usage('[options]');
Object.entries(options).forEach(([key,value]) => {
defaultMapping[key] = value;
examples.add(value.usage);
program.option(value.option,value.usage,value.description);
})
program.on('--help',() => {
console.log('\r\nExamples: \r');
examples.forEach(item => {
console.log(` ${item}`);
})
})
program.parse(process.argv);
let userArgs = program.opts();
let serverOptions;
if (userArgs.port > 0 && userArgs.port < 65535) {
defaultMapping.port.default = userArgs.port;
serverOptions = defaultMapping;
} else {
serverOptions = defaultMapping;
}
const Server = require('../src/index');
let server = new Server(serverOptions);
server.start()
bin > config.js
const options = {
'port': {
option: '-p --port <n>',
default: 8080,
usage: 'fs --port 3000',
description: 'set http-server port'
},
'gzip': {
option: '-g --gzip <n>',
default: 1,
usage: 'fs --gzip 0',
description: 'set http-server gzip'
},
'cache': {
option: '-c --cache <n>',
default: 1,
usage: 'fs --cache 0',
description: 'set http-server cache'
},
'directory': {
option: '-d --directory <d>',
default: process.cwd(),
usage: 'fs --directory d:',
description: 'set http-server work directory'
}
}
module.exports = options;
src > utils.js
const os = require('os');
const interfaces = os.networkInterfaces();
function getIps() {
const ips = [];
for (let ip in interfaces) {
interfaces[ip].find(item => {
if (item.family === 'IPv4') {
ips.push(item.address);
}
})
}
return ips;
}
module.exports = {
getIps
}
src > index.js 主入口文件
const http = require('http');
const chalk = require('chalk');
const url = require('url');
const path = require('path');
const crypto = require('crypto');
const zlib = require('zlib');
const mime = require('mime');
const { createReadStream, readFileSync } = require('fs');
const fs = require('fs').promises;
const { getIps } = require('./utils');
const ejs = require('ejs');
const template = readFileSync(path.resolve(__dirname, 'directory.html'), 'utf-8');
class Server {
constructor(options) {
this.port = options.port.default;
this.cache = options.cache;
this.gzip = options.gzip;
this.directory = options.directory.default;
this.template = template;
this.handleRequest = this.handleRequest.bind(this);
}
async handleRequest(req, res) {
const { pathname } = url.parse(req.url, true);
const requestFile = path.join(this.directory, decodeURIComponent(pathname));
try {
let fileStat = await fs.stat(requestFile);
if (fileStat.isDirectory()) {
let dirs = await fs.readdir(requestFile);
dirs = dirs.map((dir) => {
return {
name: dir,
href: path.join(pathname, dir)
}
})
dirs.push({
name: '返回上一级目录',
href: './'
})
res.setHeader('Content-Type', 'text/html;charset=utf-8');
let fileContent = await ejs.render("" + this.template, { dirs });
res.end(fileContent);
} else {
this.sendFile(req, res, requestFile, fileStat);
}
} catch (e) {
this.sendError(req, res, e);
}
}
sendFile(req, res, filePath, fileStat) {
if (this.cacheFile(req, res, filePath, fileStat)) {
res.statusCode = 304;
res.end();
} else {
res.setHeader('Content-Type', mime.getType(filePath) + ';charset=utf-8');
let createCompress;
if (createCompress = this.gzipFile(req, res)) {
createReadStream(filePath).pipe(createCompress).pipe(res);
} else {
createReadStream(filePath).pipe(res);
}
}
}
gzipFile(req, res) {
const acceptEncoding = req.headers['accept-encoding'];
if (acceptEncoding) {
if (acceptEncoding.includes('gzip')) {
res.setHeader('Content-Encoding', 'gzip');
return zlib.createGzip();
} else if (acceptEncoding.includes('deflate')) {
res.setHeader('Content-Encoding', 'deflate');
return zlib.createDeflate();
}
}
return false;
}
cacheFile(req, res, filePath, fileStat) {
res.setHeader('Cache-Control', 'max-age=10');
const lastModifyed = fileStat.ctime.toGMTString();
const etag = crypto.createHash('md5').update(readFileSync(filePath)).digest('base64');
res.setHeader('Last-Modified', lastModifyed);
res.setHeader('Etag', etag);
const ifModifiedSince = req.headers['if-modified-since'];
const ifNoneMatch = req.headers['if-none-match'];
if (ifModifiedSince !== lastModifyed) {
return false;
}
if (etag !== ifNoneMatch) {
return false;
}
return true;
}
sendError(req, res, err) {
res.end(err);
}
start() {
const server = http.createServer((req, res) => {
if (req.url === '/favicon.ico') {
console.log('快滚!')
}
this.handleRequest(req, res);
})
server.listen(this.port, () => {
console.log(` Starting up http - server, serving./
Available on:`);
const ips = getIps();
ips.forEach(ip => {
console.log(' http://' + chalk.blue(ip) + ':' + chalk.green(this.port));
})
})
server.on('error', err => {
if (err.code === 'EADDRINUSE') {
server.listen(++this.port);
}
})
}
}
module.exports = Server;
src > directory.html 网页列表
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Directory</title>
<style>
.list-group-flush {
border-radius: 0;
}
.list-group {
margin-left: 50px;
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;
flex-direction: column;
padding-left: 0;
margin-bottom: 0;
border-radius: .25rem;
width: 200px;
}
.list-group-item+.list-group-item {
border-top-width: 0;
}
.list-group-item-light.list-group-item-action:focus,
.list-group-item-light.list-group-item-action:hover {
background-color: #ececf6;
}
.list-group-item-light {
background-color: #fdfdfe;
}
.list-group-item {
position: relative;
display: block;
padding: .75rem 1.25rem;
background-color: #fff;
border-bottom: 1px solid rgba(0, 0, 0, .125);
}
.list-group-item-action {
width: 100%;
color: #007bff;
text-align: inherit;
}
h2 {
margin-left: 50px;
}
</style>
</head>
<body>
<h2>目录列表</h2>
<div class="list-group list-group-flush">
<% dirs.forEach(function(dir) { %>
<a href="<%=dir.href%>" class="list-group-item list-group-item-action list-group-item-light">
<%=dir.name%>
</a>
<% }) %>
</div>
<script>
const as = document.getElementsByClassName('list-group-item');
as[as.length - 1].style.color = '#ff6700';
</script>
</body>
</html>