UNPKG

8.89 kBJavaScriptView Raw
1/**
2 * Created by rodey on 2016/8/16.
3 * 一个简单的静态文件服务器
4 * 同时包含热开发(浏览器实时更新)
5 */
6
7'use strict';
8
9const http = require('http');
10const url = require('url');
11const path = require('path');
12const os = require('os');
13const pako = require('pako');
14const chalk = require('chalk');
15const execFile = require('child_process').execFile;
16const mimeTypes = require('./mime');
17const config = require('./serconf');
18const T = require('./tools');
19const LiveServer = require('./live/liveReloadServer').LiveServer;
20const liveApp = require('./live/liveApp');
21const getGupackConfig = require('./live/liveReloadConfig');
22
23//取得用户配置文件信息
24const userCustomConfig = getGupackConfig();
25const hostname = userCustomConfig['host'];
26const port = userCustomConfig['port'];
27const sport = userCustomConfig['sport'];
28const liveReloadDelay = userCustomConfig['liveDelay'];
29
30//当前项目目录,服务启动后将从该目录开始读取文件
31const basePath = userCustomConfig.buildDir && T.Path.isAbsolute(userCustomConfig.buildDir) ? userCustomConfig.buildDir : T.Path.resolve(process.cwd(), 'dist');
32let isOpenBrowser, oldRealPath;
33
34//如果项目根目录不存在
35if (!basePath) {
36 throw new Error(T.msg.red('\u672a\u8bbe\u7f6e\u6b63\u786e\u7684\u9879\u76ee\u8def\u5f84'));
37}
38
39// 创建web服务器
40function createLiveServer(gupack) {
41 isOpenBrowser = gupack.openBrowser;
42
43 const server = liveApp(doFile);
44 //启动服务
45 server.listen(port, hostname, listen);
46 return server;
47}
48
49function doFile(req, res) {
50 let headers = {
51 'Accept-Ranges': 'bytes',
52 'Content-Type': 'text/plain',
53 'Access-Control-Allow-Origin': '*',
54 'Access-Control-Allow-Methods': 'GET,PUT,POST,DELETE',
55 'Access-Control-Allow-Headers': 'Content-Type',
56 'Access-Control-Allow-Credentials': 'true',
57 'Timing-Allow-Origin': '*',
58 Server: 'Gupack, NodeJS/' + process.version
59 };
60
61 let pathname = url.parse(req.url).pathname.replace(/\.\./g, '');
62 if (pathname.slice(-1) === '/') {
63 pathname += config.indexFile.file;
64 }
65 let realPath = path.join(basePath, path.normalize(pathname));
66 if (pathname === '/favicon.ico') {
67 realPath = path.resolve(__dirname, '../', path.normalize(pathname));
68 }
69 if (/mock(Data)?.*?\.json$/gi.test(pathname)) {
70 realPath = path.resolve(basePath, '../', pathname.replace(/^\//, ''));
71 }
72 // console.log(realPath);
73 //判断路径是否存在
74 if (!T.fs.existsSync(realPath)) {
75 sendResponse(req, res, 404, 'This request URL " + pathname + " was not found on this server.', headers);
76 return;
77 }
78
79 let stats = T.fs.statSync(realPath);
80 if (!stats) {
81 sendResponse(req, res, 404, 'This request URL " + pathname + " was not found on this server.', headers);
82 return;
83 }
84
85 //如果访问的是content 目录
86 //exp: http://127.0.0.1:3000/assets/images
87 if (stats.isDirectory()) {
88 realPath = path.join(realPath, '/', config.indexFile.file);
89 }
90 //读取文件
91 T.fs.readFile(realPath, 'binary', (err, file) => {
92 if (err) {
93 //读取文件失败
94 T.log.red('--->>> \u8bfb\u53d6\u6587\u4ef6\u5931\u8d25 ');
95 sendResponse(req, res, 500, err.toLocaleString(), headers);
96 } else {
97 let extname = path.extname(realPath).replace(/^\./i, '');
98 headers['Content-Type'] = (mimeTypes[extname] || 'text/plain') + '; charset=UTF-8';
99
100 //缓存控制===========Start
101 let cacher = setHeaderCache(req, stats, extname, headers);
102 if (304 === cacher) {
103 sendResponse(req, res, 304, 'Not Modified', headers);
104 return;
105 } else {
106 headers = cacher;
107 }
108
109 //加入 实时更新===========Start
110 if (config.liveReload.match.test(extname)) {
111 file = implanteStyleCode(file);
112 file = implanteScriptCode(file);
113 oldRealPath && liveServer.unwatch(oldRealPath);
114 liveServer.watching(realPath);
115 oldRealPath = realPath;
116 T.log.yellow('.................page reload................');
117 }
118
119 //如果支持gzip压缩===========Start
120 if (extname.match(config.Gzip.match)) {
121 let gziper = setHeaderGzip(req, file, headers);
122 file = gziper.content;
123 headers = gziper.headers;
124 }
125
126 //add Content-Length===========Start
127 headers['Content-Length'] = file.length;
128 //respond content as status 200
129 sendResponse(req, res, 200, file, headers);
130 }
131 });
132}
133
134function listen() {
135 let url = `http://${hostname}:${port}/${getIndexFile()}`;
136 T.log.yellow(chalk.underline.bgBlue(`Server running at ${url}`));
137 connectSocket();
138 isOpenBrowser && openBrowse(url);
139}
140
141function getIndexFile() {
142 return userCustomConfig.indexFile;
143}
144
145function openBrowse(url) {
146 let osType = os.type();
147 let shellFile = /windows/gi.test(osType) ? 'open.cmd' : /macos/gi.test(osType) ? 'open' : 'xdg-open';
148 shellFile = T.Path.resolve(__dirname, '../shell', shellFile);
149 execFile(shellFile, [url]);
150}
151
152/**
153 * 响应客户端请求
154 * @param req request对象
155 * @param res response对象
156 * @param status 返回状态码
157 * @param body 响应数据
158 * @param headers 响应头信息对象
159 * @param charType 响应数据类型
160 */
161function sendResponse(req, res, status, body, headers, charType) {
162 res.writeHead(status, headers);
163 res.write(body, charType || 'binary');
164 res.end();
165}
166
167/**
168 * 植入javascript代码
169 * @param content 当前访问的文件内容
170 * @returns {*}
171 */
172function implanteScriptCode(content) {
173 let scriptCode = T.getFileContent(T.Path.resolve(__dirname, 'live/liveReloadBrowser.js'));
174 let tag = '<script id="' + Math.random() * 999999 + '" data-host="' + hostname + '" data-socket="true" data-port="' + sport + '">';
175 //将浏览器上的socket端口进行替换
176 scriptCode = T.replaceVar(scriptCode, null, sport);
177 tag += scriptCode + '</script></body>';
178 content = content.replace('</body>', tag);
179 return content;
180}
181
182function implanteStyleCode(content) {
183 let styleCode = T.getFileContent(T.Path.resolve(__dirname, 'live/liveReloadStyle.css'));
184 let tag = '<style id="' + Math.random() * 999999 + '" data-host="' + hostname + '" data-socket="true" data-port="' + sport + '">';
185
186 tag += styleCode + '</style></head>';
187 content = content.replace('</head>', tag);
188 return content;
189}
190
191/**
192 * 响应进行 gzip压缩
193 * @param req
194 * @param content
195 * @param headers
196 * @returns {{content: *, headers: *}}
197 */
198function setHeaderGzip(req, content, headers) {
199 //将字符串数据转成二进制数据流,(有效解决二进制存储文件显示,如:图片,字体等)
200 let bin = new Buffer(content, 'binary');
201 let acceptEncoding = req.headers['accept-encoding'] || '';
202 if (/\bgzip\b/gi.test(acceptEncoding)) {
203 headers['Content-Encoding'] = 'gzip';
204 content = pako.gzip(new Uint8Array(bin), { to: 'string' });
205 } else if (/\bdeflate\b/gi.test(acceptEncoding)) {
206 headers['Content-Encoding'] = 'deflate';
207 content = pako.gzip(new Uint8Array(bin), { to: 'string' });
208 }
209 return { content: content, headers: headers };
210}
211
212/**
213 * 响应进行 缓存设置
214 * @param req
215 * @param stats
216 * @param extname
217 * @param headers
218 * @returns {*}
219 */
220function setHeaderCache(req, stats, extname, headers) {
221 let lastModified = stats.mtime.toUTCString();
222 let ifModifiedSince = 'If-Modified-Since'.toLowerCase();
223 headers['Last-Modified'] = lastModified;
224 if (extname.match(config.Expires.fileMatch)) {
225 let expires = new Date();
226 expires.setTime(expires.getTime() + config.Expires.maxAge * 1000);
227 headers['Expires'] = expires.toUTCString();
228 headers['Cache-Control'] = 'max-age=' + config.Expires.maxAge;
229 }
230
231 if (req.headers[ifModifiedSince] && lastModified === req.headers[ifModifiedSince]) {
232 return 304;
233 }
234 return headers;
235}
236
237/**
238 * 监听文件变化,触发浏览器热更新
239 */
240let liveServer;
241
242function connectSocket() {
243 if (liveServer) return false;
244 liveServer = new LiveServer({ port: sport, liveDelay: liveReloadDelay });
245}
246
247function emitBuilding() {
248 liveServer && liveServer.send('<<<-----start building----->>>');
249}
250
251module.exports = {
252 createLiveServer,
253 emitBuilding
254};