├── index.js
├── .gitignore
├── src
├── templates
│ ├── images
│ │ └── 404.png
│ ├── index.js
│ ├── 404.js
│ └── default.js
├── mime.js
└── static-server.js
├── package.json
├── bin
└── app.js
├── LICENSE
└── README.md
/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | module.exports = require('./bin/app.js');
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.swap
3 | .idea
4 | .DS_Store
5 | *.log
6 | .vscode
7 | *-lock.json
--------------------------------------------------------------------------------
/src/templates/images/404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WisestCoder/static-server/HEAD/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 | };
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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();
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/src/templates/404.js:
--------------------------------------------------------------------------------
1 | module.exports = `
2 |
3 |
4 |
5 |
6 |
7 |
8 | node静态服务器
9 |
33 |
34 |
35 |
36 |

37 |
38 |
抱歉,你访问的路径不存在
39 |
您要找的页面没有找到,请返回首页继续浏览
40 |
41 |
42 |
43 |
44 | `
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## 使用node搭建静态资源服务器
2 |
3 | [](https://npmjs.org/package/yumu)
4 | [](https://npmjs.org/package/yumu)
5 | [](https://github.com/dushao103500/static-server)
6 | [](https://github.com/dushao103500/static-server)
7 |
8 | ### Demo
9 | 
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 |
--------------------------------------------------------------------------------
/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/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;
--------------------------------------------------------------------------------