当我们在编写DEMO的时候,如何即时预览,提升编程体验和工作效率是一个重要技能。
我可以使用nginx或apache等HTTP服务器,但是通常需要安装下载,配置复杂。
虽然功能强大,但是做为前端,我们通常只需要一个简易的静态http服务器即可,那么nodeJS将是我们的首选!
在线实时预览编辑器
首先我们可以利用在线编辑器,其中有很多优秀的前端DEMO展示,这里有以下几个优秀平台推荐
CodePen 首页有很多优秀的前端示例,支持各种JS库、CSS预编译库、HTML预编译库等等
JS Bin
RunJS
JsFiddle
HTTP简易静态服务器的基本功能点
基本流程
浏览器发送URL,服务端解析URL,对应到硬盘上的文件。
如果文件存在,返回200状态码,并发送文件到浏览器端;
如果文件不存在,返回404状态码,发送一个404的文件到浏览器端。
如果文件读取发生错误,返回500状态码,发送错误信息到浏览器端。
基本功能
- 能开启web服务,加载各种web资源
- 能自动分配可用端口,也可以配置端口
- 能加载json文件并模拟网速
- 启动时能自动打开默认浏览器浏览,方便调试
- 支持常用文件的MIME类型
- 可配置主页
- 允许跨域请求,方便其它项目测试时调用模拟接口的JSON数据
- host默认为本地IP(方便生成二维码手机扫描预览)
扩展功能
- 支持304缓存响应
- 启用GZip功能对指定文件进行压缩
- 使用快捷命令
- 支持断点续传
要解决的问题
如何获取本地的IP地址?os.networkInterfaces
通过内置的OS模块中的networkInterfaces()
方法获取本机的网络接口信息集合,从中可以得到IPv4的地址var ifaces = os.networkInterfaces();
var ip = '';
for (var dev in ifaces) {
ifaces[dev].forEach(function (details) {
if (ip === '' && details.family === 'IPv4' && !details.internal) {
ip = details.address;
return;
}
});
}
如何打开默认浏览器?
通过判断当前系统的类型,获取相应的cli命令,调用内置的进程管理模块中的exec方法打开url。var exec = require('child_process').exec;
var url= "http://127.0.0.1:8080/"
switch (process.platform) {
case "darwin":
exec('open ' + url);
break;
case "win32":
exec('start ' + url);
break;
default:
exec('xdg-open', [url]);
}
如何自动分配一个可用的端口号?
我只想启动一个可用的服务,端口号我不关注,只要不报错,不被占用就ok,当然启动完,我要知道我在哪个端口启动了。var server = http.createServer();
server.listen(0);
server.on('listening', function() {
var port = server.address().port;
console.log(port);
})
监听0.0.0.0,nodejs会自动分配给你一个可用端口,listening中获取port就ok了
基本版代码结构
var http = require('http'); // Http服务器API |
编写逻辑
新建一个http服务
var server = http.createServer(); |
注册 监听端口 和 请求响应 的事件
在成功连接到端口后,获取端口号 和 本地IP,并打开默认浏览器浏览var self = this, defaultUrl = CONFIG.homedir ? CONFIG.homedir+'/'+CONFIG.home : CONFIG.home;
// 注册监听端口启用事件
server.on('listening', function() {
var port = server.address().port;
log('Server running at '+ port);
CONFIG.browser && self._openURL('http://'+self._getIPAddress()+':'+port+'/'+defaultUrl);
})
注册请求处理事件
在收到请求的url后,解析并路由到指定文件并输出server.on('request', function(request, response) {
// 解析请求的URL
var oURL = url.parse(request.url);
var pathName = oURL.pathname.slice(1);
if(!pathName) pathName = defaultUrl;
self.route.bind(self)(pathName, request, response);
});
路由处理
判断文件是否存大,如果不存在,则返回404状态码,并提示未找到
如果存在,则判断是否为文件夹,如果是文件夹,则返回文件夹下的默认home页
如果是文件,则获取扩展名和请求的参数
根据扩展名获取文件的MIME类型
响应流中添加Access-Control-Allow-Origin=*,允许跨域请求
如果文件是json格式,则判断是否有delay延迟参数,如果有,则延迟响应时间responseFile:function(pathName, res, ext, params){ /* 读取文件流并输出 */
var self = this;
var raw = fs.createReadStream(pathName);
// 允许跨域调用
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Content-Type", self._getMIME(ext));
if(ext == 'json' && params.delay){
setTimeout(function(){
res.writeHead(200, "Ok");
raw.pipe(res);
},params.delay);
}else{
res.writeHead(200, "Ok");
raw.pipe(res);
}
},
route:function( pathName, req, res ){/* 路由到指定的文件并响应输出 */
var self = this;
fs.stat(pathName, function(err, stats){
if(err){
res.writeHead(404, "Not Found", {'Content-Type': 'text/plain'});
res.write("This request URL " + pathName + " was not found on this server.");
res.end();
}else{
if(stats.isDirectory()){
pathName = path.join(pathName, '/', CONFIG.home);
self.route(pathName, req, res);
}else{
var method = req.method,
ext = path.extname(pathName),
params='';
log(method+': '+pathName);// 打印请求日志
ext = ext ? ext.slice(1) : 'unknown';
// 如果是get请求,且url结尾为'/',那么就返回 home 页
if(method=='GET'){
pathName.slice(-1) === '/' && (pathName = path.normalize(pathName + '/' +CONFIG.home));
params = url.parse(req.url, true).query;
self.responseFile.bind(self)(pathName, res, ext, params);
}else if(method == 'POST'){
var _postData = "", _postMap = "";
req.on('data', function (chunk){
_postData += chunk;
}).on("end", function (){
params = require('querystring').parse(_postData);
responseFile.bind(self)(pathName, res, ext, params);
});
}else{
self.responseFile.bind(self)(pathName, res, ext, params);
}
}
}
});
}
扩展功能
上面的版本基本上可以满足我们常用的http功能了,但是如果要测试文件缓存,加载时间,多媒体大文件加载,gzip压缩优化响应时间,使用快捷命令启动,那么还需要对基础版本增加一些功能。
支持304缓存响应
上面的版本,用户每次请求都会从服务器硬盘中读取文件,如果并发量大,那么磁盘IO会吃不消。所以可以利用浏览器的缓存机制,减少不必要的请求,节省资源消耗。
在响应流输出之前,给响应头添加过期时间和最后修改时间
浏览器在发送请求之前由于检测到Cache-Control和Expires(Cache-Control的优先级高于Expires,但有的浏览器不支持Cache-Control,这时采用Expires)
如果没有过期,则不会发送请求,而直接从缓存中读取文件。// 添加过期时间
if(ext.match(CONFIG.fileMatch)){
var expires = new Date();
expires.setTime(expires.getTime()+CONFIG.maxAge*1000);
res.setHeader('Expires', expires.toUTCString());
res.setHeader('Cache-Control','max-age='+CONFIG.maxAge);
}
// 添加Last-Modified头
var lastModified = stat.mtime.toUTCString();
res.setHeader('Last-Modified', lastModified);
// 检测请求头是否携带 If-Modified-Since 信息,如果请求的文件的If-Modified-Since时间与最后修改时间相同,则返回304
var ifModifiedSince = "if-modified-since";
if(req.header[ifModifiedSince] && lastModified == req.header[ifModifiedSince]){
res.setHeader(304, 'Not Modified');
res.end();
return;
}
支持断点续传
大请求大文件时,如果网络中断,我们希望可以在网速恢复以后,可以继续下载
这时就必须在响应头和请求头里做相应的处理,服务端告知浏览器端支持断点续传,并响应字节长度信息,服务端在收到字节长度信息后去按字节读取文件并响应。_getRange:function(str, size){
if (str.indexOf(",") != -1) {
return;
}
var range = str.split("-"),
start = parseInt(range[0], 10),
end = parseInt(range[1], 10);
// Case: -100 返回最后的end个字节
if (isNaN(start)) {
start = size - end;
end = size - 1;
// Case: 100- 返回从start往后到end之间的字节
} else if (isNaN(end)) {
end = size - 1;
}
// Invalid
if (isNaN(start) || isNaN(end) || start > end || end > size) {
return;
}
return {start: start, end: end};
},
// 告知浏览器支持断点续传
res.setHeader('Accept-Ranges', 'bytes');
if(req.headers['range']){
var range = self._getRange(req.headers['range'], stat.size);
if(range){
res.setHeader('Content-Range', 'bytes ' + range.start + '-' +range.end +'/'+stat.size);
res.setHeader('Content-Length', range.end - range.start+1);
res.writeHeader(206, 'Partial Content');
raw = fs.createReadStream(pathName, {"start": range.start, "end": range.end});
raw.pipe(res);
}else{
res.removeHeader('Content-Length');
res.writeHeader(416, 'Request Range Not Satisfiable');
res.end();
}
}
使用curl 命令测试curl --header "Range:0-50" -i http://192.168.0.101:8089/index.html
HTTP/1.1 206 Partial Content
Server: Node/V5
Accept-Ranges: bytes
Access-Control-Allow-Origin: *
Content-Type: text/html
Last-Modified: Sat, 20 Feb 2016 06:45:26 GMT
Content-Range: bytes 0-50/7672
Content-Length: 51
Date: Tue, 15 Mar 2016 14:57:13 GMT
Connection: keep-alive
<html lang="zh-CN">
<head>
<m% ➜ ~
启用GZip功能对指定文件进行压缩
请求的资源经过GZip压缩后,文件体积变小,一方面可以加快响应速度,另一方面可以节省很多资源var zlib = require('zlib'); //引入内置的zlib模块,用于文件GZip压缩
var compressHandle = function(raw, statusCode, msg){
var stream = raw;
var acceptEncoding = req.headers['accept-encoding'] || "";
var zipMatch = ext.match(CONFIG.zipMatch);
// 判断浏览器是否支持Gzip压缩
if (zipMatch && acceptEncoding.match(/\bgzip\b/)) {
res.setHeader("Content-Encoding", "gzip");
stream = raw.pipe(zlib.createGzip());
} else if (zipMatch && acceptEncoding.match(/\bdeflate\b/)) {
res.setHeader("Content-Encoding", "deflate");
stream = raw.pipe(zlib.createDeflate());
}
res.writeHead(statusCode, msg);
stream.pipe(res);
}
使用快捷命令
经过以上的配置编写,一个基本完善的http服务器搭建完成了,我们把这段JS保存到任意文件夹下,使用node运行,就能在当前目录下建立一个http服务了。
但是当我们需要修改端口号,或者要修改主页名称时,我们就必须修改这个文件中的配置对象CONFIG。
而我们在使用其它很多nodeJS模块时,都有一些快捷命令可以使用。如何让我们的这个http.js也能像命令一样使用,并可配置参数,现在让我们也一起来实现一下
1. 安装 minimist
模块
用于解析命令参数 NPM仓库 minimistnpm install minimist --save
2. 引用 minimist
处理命令参数 var argv = require("minimist")(process.argv.slice(2), {
alias: {
'port': 'p',
'home': 'h',
'homedir': 'd'
},
string: ['port', 'home', 'homedir']
});
if (argv.help) {
log("Usage:");
log(" iter-http --help // print help information");
log(" iter-http // random a port, current folder as root");
log(" iter-http 8888 // 8888 as port");
log(" iter-http -p 8989 // 8989 as port");
log(" iter-http -h index.htm // index.htm as home page");
log(" iter-http -d dist // dist as root");
process.exit(0);
}
// 调用时, 解析出来的argv的值示例如下
iter-http
{ _: []}
iter-http 8888
{ _: [8888]}
iter-http -p 8888
{ _: [], port: 8888}
npm 模块发布
目录结构
新建一个文件夹 iterhttp ,其中的目录结构如下,并将其发布到GitHub的仓库中去.
├── LICENSE
├── README.md
├── bin
│ └── iter-http
├── http.js
├── node_modules
│ └── minimist
└── package.json
配置pagekage.json
其中的package.json配置如下, 可以使用npm init初始化后,进行修改{
"name": "iter-http",
"version": "1.0.10",
"description": "Run static file server",
"main": "./http.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"bin": {
"iter-http": "./bin/iter-http"
},
"repository": {
"type": "git",
"url": "git@github.com:git-lt/iterhttp.git"
},
"keywords": [
"Static",
"file",
"server",
"Assets"
],
"author": "ITER",
"license": "ISC",
"homepage": "http://coderlt.coding.me/coderlt",
"dependencies": {
"minimist": "^1.2.0"
},
"bugs": {
"url": "https://github.com/git-lt/iterhttp/issues"
}
}
注意
name
为我们包的名称version
为包的版本号,在每次发布包时,一定要更新版本号,否则不能发布bin
配置安装到全局时的路径和命令名称,这里我们将包安装到全局后,就可以在任意目录执行 iter-http 命令开启一个http服务了dependencies
中是我们包的依赖,在安装依赖的时候附带 –save 参数,会将依赖写入到这个配置中- bin 目录中的iter-http文件的头部必须添加
#!/usr/bin/env node
, 以告知系统使用node运行
添加npm账号
在npm官网注册一个账号
在本机 包的目录下 执行 npm adduser
添加账号
发布到npm
执行 npm publish
如果发布失败,很可能是使用了淘宝的镜像,执行 npm config set registry http://registry.npmjs.org
还原仓库地址,再次发布就可以了
注意
- 包名和文件夹的名称不能相同
- npm包package.json中registory属性一定要填写,每次
npm publish
时package.json中version
版本一定要大于上一次 npm publish failed put 500 unexpected status code 401
这样的报错信息,往往是没有登录成功,操作npm login
发布成功后,信息如下➜ iterhttp git:(master) ✗ npm publish
+ iter-http@1.0.9
安装使用
安装
npm install iter-http -g |
这里一定要安装到全局,才可以在任意目录下开启http服务
安装时也会自动安装依赖包 minimist
用于解析命令参数
显示帮助
iter-http --help |
至次一个基本完善的,基于NodeJS的http服务器脚本终于搭建好了,我们发布到了npm中,可以在任意一台设备中安装该模块,可以在任意目录下启用http静态服务器。
神器在手,如鱼得水,妈妈再也不用担心我写Demo了:)