UNPKG

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