├── .gitignore ├── LICENSE ├── README.md ├── bin └── app.js ├── index.js ├── package.json └── src ├── mime.js ├── static-server.js └── templates ├── 404.js ├── default.js ├── images └── 404.png └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.swap 3 | .idea 4 | .DS_Store 5 | *.log 6 | .vscode 7 | *-lock.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 WisestCoder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 使用node搭建静态资源服务器 2 | 3 | [![NPM version](https://img.shields.io/npm/v/yumu-static-server.svg?style=flat)](https://npmjs.org/package/yumu) 4 | [![npm](https://img.shields.io/npm/dt/yumu-static-server.svg)](https://npmjs.org/package/yumu) 5 | [![GitHub stars](https://img.shields.io/github/stars/dushao103500/static-server.svg?style=social&label=Star)](https://github.com/dushao103500/static-server) 6 | [![GitHub forks](https://img.shields.io/github/forks/dushao103500/static-server.svg?style=social&label=Fork)](https://github.com/dushao103500/static-server) 7 | 8 | ### Demo 9 | ![](https://github.com/wisestcoder/assert/blob/master/static_server_demo.gif) 10 | 11 | ### 安装 12 | 13 | ```bash 14 | npm install @wisestcoder/static-server -g 15 | ``` 16 | 17 | ### 使用 18 | 19 | ```bash 20 | server # 会在当前目录下启动一个静态资源服务器,默认端口为8080 21 | 22 | server -p[port] 3000 # 会在当前目录下启动一个静态资源服务器,端口为3000 23 | 24 | server -i[index] index.html # 设置文件夹在默认加载的文件 25 | 26 | server -c[charset] UTF-8 # 设置文件默认加载的字符编码 27 | 28 | server -cors # 开启文件跨域 29 | 30 | server -h[https] # 开启https服务 31 | 32 | server --openindex # 是否打开默认页面 33 | 34 | server --no-openbrowser # 关闭自动打开浏览器 35 | ``` 36 | 37 | ### 基本功能 38 | 39 | 1. 启动静态资源服务器 40 | 2. 端口可配置 41 | 3. 字符编码可配置 42 | 4. 文件夹下默认加载文件可配置 43 | 5. 是否跨域可配置 44 | 6. 开启https服务 45 | 46 | ### TODO 47 | 48 | - [x] 引入handlerbars编译模板 49 | - [x] 支持文件是否跨域 50 | - [x] 支持https服务 51 | -------------------------------------------------------------------------------- /bin/app.js: -------------------------------------------------------------------------------- 1 | const StaticServer = require('../src/static-server'); 2 | 3 | const options = require('yargs') 4 | .option('p', { alias: 'port', describe: '设置服务启动的端口号', type: 'number' }) 5 | .option('i', { alias: 'index', describe: '设置默认打开的主页', type: 'string' }) 6 | .option('c', { alias: 'charset', describe: '设置文件的默认字符集', type: 'string' }) 7 | .option('o', { alias: 'openindex', describe: '是否打开默认页面', type: 'boolean' }) 8 | .option('h', { alias: 'https', describe: '是否启用https服务', type: 'boolean' }) 9 | .option('cors', { describe: '是否开启文件跨域', type: 'boolean' }) 10 | .option('openbrowser', { describe: '是否默认打开浏览器', type: 'boolean' }) 11 | 12 | // 默认参数 13 | .default('openbrowser', true) 14 | // .default('https', true) 15 | .default('port', 8080) 16 | .default('index', 'index.html') 17 | .default('openindex', 'index.html') 18 | .default('charset', 'UTF-8') 19 | 20 | .help() 21 | .alias('?', 'help') 22 | 23 | .argv; 24 | 25 | const server = new StaticServer(options); 26 | server.start(); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | module.exports = require('./bin/app.js'); 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wisestcoder/static-server", 3 | "version": "0.0.1", 4 | "description": "使用node搭建静态资源服务器", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "supervisor bin/app.js", 8 | "start": "npm run dev" 9 | }, 10 | "bin": { 11 | "server": "index.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/WisestCoder/static-server" 16 | }, 17 | "author": "", 18 | "license": "ISC", 19 | "homepage": "https://github.com/WisestCoder/static-server#readme", 20 | "keywords": [ 21 | "static-server", 22 | "server" 23 | ], 24 | "dependencies": { 25 | "chalk": "^2.3.2", 26 | "handlebars": "^4.0.11", 27 | "mime": "^2.2.0", 28 | "open": "^7.1.0", 29 | "pem": "^1.12.5", 30 | "yargs": "^6.6.0" 31 | }, 32 | "devDependencies": { 33 | "supervisor": "^0.12.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/mime.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const mime = require('mime'); 3 | 4 | const lookup = (pathName) => { 5 | let ext = path.extname(pathName); 6 | ext = ext.split('.').pop(); 7 | return mime.getType(ext) || mime.getType('txt'); 8 | } 9 | 10 | module.exports = { 11 | lookup 12 | }; 13 | -------------------------------------------------------------------------------- /src/static-server.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const https = require('https'); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | const zlib = require('zlib'); 7 | const chalk = require('chalk'); 8 | const os = require('os'); 9 | const open = require("open"); 10 | const Handlebars = require('handlebars'); 11 | const pem = require('pem'); 12 | const mime = require('./mime'); 13 | const Template = require('./templates'); 14 | 15 | const _defaultTemplate = Handlebars.compile(Template.page_dafault); 16 | const _404TempLate = Handlebars.compile(Template.page_404); 17 | 18 | const hasTrailingSlash = url => url[url.length - 1] === '/'; 19 | 20 | const ifaces = os.networkInterfaces(); 21 | 22 | class StaticServer { 23 | constructor(options) { 24 | this.port = options.port; 25 | this.indexPage = options.index; 26 | this.openIndexPage = options.openindex; 27 | this.openBrowser = options.openbrowser; 28 | this.charset = options.charset; 29 | this.cors = options.cors; 30 | this.protocal = options.https ? 'https' : 'http'; 31 | this.zipMatch = '^\\.(css|js|html)$'; 32 | } 33 | 34 | /** 35 | * 响应错误 36 | * 37 | * @param {*} err 38 | * @param {*} res 39 | * @returns 40 | * @memberof StaticServer 41 | */ 42 | respondError(err, res) { 43 | res.writeHead(500); 44 | return res.end(err); 45 | } 46 | 47 | /** 48 | * 响应404 49 | * 50 | * @param {*} req 51 | * @param {*} res 52 | * @memberof StaticServer 53 | */ 54 | respondNotFound(req, res) { 55 | res.writeHead(404, { 56 | 'Content-Type': 'text/html' 57 | }); 58 | const html = _404TempLate(); 59 | res.end(html); 60 | } 61 | 62 | respond(pathName, req, res) { 63 | fs.stat(pathName, (err, stat) => { 64 | if (err) return respondError(err, res); 65 | this.responseFile(stat, pathName, req, res); 66 | }); 67 | } 68 | 69 | /** 70 | * 判断是否需要解压 71 | * 72 | * @param {*} pathName 73 | * @returns 74 | * @memberof StaticServer 75 | */ 76 | shouldCompress(pathName) { 77 | return path.extname(pathName).match(this.zipMatch); 78 | } 79 | 80 | /** 81 | * 解压文件 82 | * 83 | * @param {*} readStream 84 | * @param {*} req 85 | * @param {*} res 86 | * @returns 87 | * @memberof StaticServer 88 | */ 89 | compressHandler(readStream, req, res) { 90 | const acceptEncoding = req.headers['accept-encoding']; 91 | if (!acceptEncoding || !acceptEncoding.match(/\b(gzip|deflate)\b/)) { 92 | return readStream; 93 | } else if (acceptEncoding.match(/\bgzip\b/)) { 94 | res.setHeader('Content-Encoding', 'gzip'); 95 | return readStream.pipe(zlib.createGzip()); 96 | } 97 | } 98 | 99 | /** 100 | * 响应文件路径 101 | * 102 | * @param {*} stat 103 | * @param {*} pathName 104 | * @param {*} req 105 | * @param {*} res 106 | * @memberof StaticServer 107 | */ 108 | responseFile(stat, pathName, req, res) { 109 | // 设置响应头 110 | res.setHeader('Content-Type', `${mime.lookup(pathName)}; charset=${this.charset}`); 111 | res.setHeader('Accept-Ranges', 'bytes'); 112 | 113 | // 添加跨域 114 | if (this.cors) res.setHeader('Access-Control-Allow-Origin', '*'); 115 | 116 | let readStream; 117 | readStream = fs.createReadStream(pathName); 118 | if (this.shouldCompress(pathName)) { // 判断是否需要解压 119 | readStream = this.compressHandler(readStream, req, res); 120 | } 121 | readStream.pipe(res); 122 | } 123 | 124 | /** 125 | * 响应重定向 126 | * 127 | * @param {*} req 128 | * @param {*} res 129 | * @memberof StaticServer 130 | */ 131 | respondRedirect(req, res) { 132 | const location = req.url + '/'; 133 | res.writeHead(301, { 134 | 'Location': location, 135 | 'Content-Type': 'text/html' 136 | }); 137 | const html = _defaultTemplate({ 138 | htmlStr: `Redirecting to ${location}`, 139 | showFileList: false 140 | }) 141 | res.end(html); 142 | } 143 | 144 | /** 145 | * 响应文件夹路径 146 | * 147 | * @param {*} pathName 148 | * @param {*} req 149 | * @param {*} res 150 | * @memberof StaticServer 151 | */ 152 | respondDirectory(pathName, req, res) { 153 | const indexPagePath = path.join(pathName, this.indexPage); 154 | // 如果文件夹下存在index.html,则默认打开 155 | if (this.openIndexPage && fs.existsSync(indexPagePath)) { 156 | this.respond(indexPagePath, req, res); 157 | } else { 158 | fs.readdir(pathName, (err, files) => { 159 | if (err) { 160 | respondError(err, res); 161 | } 162 | const requestPath = url.parse(req.url).pathname; 163 | const fileList = []; 164 | files.forEach(fileName => { 165 | let itemLink = path.join(requestPath, fileName); 166 | let isDirectory = false; 167 | const stat = fs.statSync(path.join(pathName, fileName)); 168 | if (stat && stat.isDirectory()) { 169 | itemLink = path.join(itemLink, '/'); 170 | isDirectory = true; 171 | } 172 | fileList.push({ 173 | link: itemLink, 174 | name: fileName, 175 | isDirectory 176 | }); 177 | }); 178 | // 排序,目录在前,文件在后 179 | fileList.sort((prev, next) => { 180 | if (prev.isDirectory && !next.isDirectory) { 181 | return -1; 182 | } 183 | return 1; 184 | }); 185 | res.writeHead(200, { 186 | 'Content-Type': 'text/html' 187 | }); 188 | const html = _defaultTemplate({ 189 | requestPath, 190 | fileList, 191 | showFileList: true 192 | }) 193 | res.end(html); 194 | }); 195 | } 196 | } 197 | 198 | /** 199 | * 路由处理 200 | * 201 | * @param {*} pathName 202 | * @param {*} req 203 | * @param {*} res 204 | * @memberof StaticServer 205 | */ 206 | routeHandler(pathName, req, res) { 207 | const realPathName = pathName.split('?')[0]; 208 | fs.stat(realPathName, (err, stat) => { 209 | this.logGetInfo(err, pathName); 210 | if (!err) { 211 | const requestedPath = url.parse(req.url).pathname; 212 | // 检查url 213 | // 如果末尾有'/',且是文件夹,则读取文件夹 214 | // 如果是文件夹,但末尾没'/',则重定向至'xxx/' 215 | // 如果是文件,则判断是否是压缩文件,是则解压,不是则读取文件 216 | if (hasTrailingSlash(requestedPath) && stat.isDirectory()) { 217 | this.respondDirectory(realPathName, req, res); 218 | } else if (stat.isDirectory()) { 219 | this.respondRedirect(req, res); 220 | } else { 221 | this.respond(realPathName, req, res); 222 | } 223 | } else { 224 | this.respondNotFound(req, res); 225 | } 226 | }); 227 | } 228 | 229 | /** 230 | * 打印ip地址 231 | * 232 | * @memberof StaticServer 233 | */ 234 | logUsingPort() { 235 | const me = this; 236 | console.log(`${chalk.yellow(`Starting up your server\nAvailable on:`)}`); 237 | Object.keys(ifaces).forEach(function (dev) { 238 | ifaces[dev].forEach(function (details) { 239 | if (details.family === 'IPv4') { 240 | console.log(` ${me.protocal}://${details.address}:${chalk.green(me.port)}`); 241 | } 242 | }); 243 | }); 244 | console.log(`${chalk.cyan(Array(50).fill('-').join(''))}`); 245 | } 246 | 247 | /** 248 | * 打印占用端口 249 | * 250 | * @param {*} oldPort 251 | * @param {*} port 252 | * @memberof StaticServer 253 | */ 254 | logUsedPort(oldPort, port) { 255 | const me = this; 256 | console.log(`${chalk.red(`The port ${oldPort} is being used, change to port `)}${chalk.green(me.port)} `); 257 | } 258 | 259 | /** 260 | * 打印https证书友好提示 261 | * 262 | * @memberof StaticServer 263 | */ 264 | logHttpsTrusted() { 265 | console.log(chalk.green('Currently is using HTTPS certificate (Manually trust it if necessary)')); 266 | } 267 | 268 | 269 | /** 270 | * 打印路由路径输出 271 | * 272 | * @param {*} isError 273 | * @param {*} pathName 274 | * @memberof StaticServer 275 | */ 276 | logGetInfo(isError, pathName) { 277 | if (isError) { 278 | console.log(chalk.red(`404 ${pathName}`)); 279 | } else { 280 | console.log(chalk.cyan(`200 ${pathName}`)); 281 | } 282 | } 283 | 284 | startServer(keys) { 285 | const me = this; 286 | let isPostBeUsed = false; 287 | const oldPort = me.port; 288 | const protocal = me.protocal === 'https' ? https : http; 289 | const options = me.protocal === 'https' ? { key: keys.serviceKey, cert: keys.certificate } : null; 290 | const callback = (req, res) => { 291 | const pathName = path.join(process.cwd(), path.normalize(decodeURI(req.url))); 292 | me.routeHandler(pathName, req, res); 293 | }; 294 | const params = [callback]; 295 | if (me.protocal === 'https') params.unshift(options); 296 | const server = protocal.createServer(...params).listen(me.port); 297 | server.on('listening', function () { // 执行这块代码说明端口未被占用 298 | if (isPostBeUsed) { 299 | me.logUsedPort(oldPort, me.port); 300 | } 301 | me.logUsingPort(); 302 | if (me.openBrowser) { 303 | open(`${me.protocal}://127.0.0.1:${me.port}`); 304 | } 305 | }); 306 | 307 | server.on('error', function (err) { 308 | if (err.code === 'EADDRINUSE') { // 端口已经被使用 309 | isPostBeUsed = true; 310 | me.port = parseInt(me.port) + 1; 311 | server.listen(me.port); 312 | } else { 313 | console.log(err); 314 | } 315 | }) 316 | } 317 | 318 | start() { 319 | const me = this; 320 | if (this.protocal === 'https') { 321 | pem.createCertificate({ days: 1, selfSigned: true }, function (err, keys) { 322 | if (err) { 323 | throw err 324 | } 325 | me.logHttpsTrusted(); 326 | me.startServer(keys); 327 | }) 328 | } else { 329 | me.startServer(); 330 | } 331 | } 332 | } 333 | 334 | module.exports = StaticServer; -------------------------------------------------------------------------------- /src/templates/404.js: -------------------------------------------------------------------------------- 1 | module.exports = ` 2 | 3 | 4 | 5 | 6 | 7 | 8 | node静态服务器 9 | 33 | 34 | 35 |
36 | not found 37 |
38 |

抱歉,你访问的路径不存在

39 |

您要找的页面没有找到,请返回首页继续浏览

40 |
41 |
42 | 43 | 44 | ` -------------------------------------------------------------------------------- /src/templates/default.js: -------------------------------------------------------------------------------- 1 | module.exports = ` 2 | 3 | 4 | 5 | 6 | 7 | 8 | node静态服务器 9 | 64 | 65 | 66 |
67 |

当前目录:{{requestPath}}

68 | {{#if showFileList}} 69 | 83 | {{else}} 84 | {{htmlStr}} 85 | {{/if}} 86 |
87 | 91 | 92 | 93 | `; -------------------------------------------------------------------------------- /src/templates/images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WisestCoder/static-server/f2e8dd92bb6e54c2f600ff98b61c060adcc8ec47/src/templates/images/404.png -------------------------------------------------------------------------------- /src/templates/index.js: -------------------------------------------------------------------------------- 1 | const page_dafault = require('./default'); 2 | const page_404 = require('./404'); 3 | 4 | module.exports = { 5 | page_dafault, 6 | page_404 7 | }; --------------------------------------------------------------------------------