previously:html
Node和http:一本通node
当你输入一个url时,这个url可能对应服务器上的一个资源(文件)也可能对应一个目录。 So服务器会对这个url进行分析,针对不一样的状况作不一样的事。 若是这个url对应的是一个文件,那么服务器就会返回这个文件。 若是这个url对应的是一个文件夹,那么服务器会返回这个文件夹下包含的全部子文件/子文件夹的列表。 以上,就是一个静态服务器所主要干的事。git
但真实的状况不会像这么简单, 咱们所拿到的url多是错误的,它所对应的文件或则文件夹或许根本不存在, 又或则有些文件和文件夹是被系统保护起来的是隐藏的,咱们并不想让客户端知道。 所以,咱们就要针对这些特殊状况进行一些不一样的返回和提示。github
再者,当咱们真正返回一个文件前,咱们须要和客户端进行一些协商。 咱们须要知道客户端可以接受的语言类型、编码方式等等以便针对不一样浏览器进行不一样的返回处理。 咱们须要告诉客户端一些关于返回文件的额外信息,以便客户端能更好的接收数据: 文件是否须要缓存,该怎样缓存? 文件是否进行了压缩处理,该以怎样的方式解压? 等等...npm
至此,咱们已经初步了解了一个静态服务器所主要作的几乎全部事情, let's go!json
static-server/
|
| - bin/
| | - www # 批处理文件
|
|
| - src/
| | - App.js # main文件
| | - Config.js # 默认配置
|
|
·- package.json
复制代码
要启动一个服务器,咱们须要知道这个服务器的启动时的端口号数组
而在拿到用户的请求后咱们须要在咱们本身的服务器上去查找资源,so咱们须要配置一个工做目录。浏览器
let config = {
host:'localhost' //提示用
,port:8080 //服务器启动时候的默认端口号
,path:path.resolve(__dirname,'..','test-dir') //静态服务器启动时默认的工做目录
}
复制代码
注意缓存
class Server(){
constructor(options){
/* === 合并配置参数 === */
this.config = Object.assign({},config,options)
}
start(){
/* === 启动http服务 === */
let server = http.createServer();
server.on('request',this.request.bind(this));
server.listen(this.config.port,()=>{
let url = `${this.config.host}:${this.config.port}`;
console.log(`server started at ${chalk.green(url)}`)
})
}
async request(req,res){
/* === 处理客户端请求,决定响应信息 === */
// try
//若是是文件夹 -> 显示子文件、文件夹列表
//若是是文件 -> sendFile()
// catch
//出错 -> sendError()
}
sendFile(){
//对要返回的文件进行预处理并发送文件
}
handleCache(){
//获取和设置缓存相关信息
}
getEncoding(){
//获取和设置编码相关信息
}
getStream(){
//获取和设置分块传输相关信息
}
sendError(){
//错误提示
}
}
module.exports = Server;
复制代码
获取url的pathname
,和服务器本地的工做根目录地址进行拼接,返回一个filename
利用filename和stat方法
检测是文件仍是文件夹bash
若是是文件夹, 利用readdir方法
返回该文件夹下的列表,将列表包装成一个对象组成的数组 而后结合handlebar将数组数据编译到模板中,最后返回这个模板给客户端
若是是文件, 将req、res、statObj、filepath传递给sendFile
,交由sendFile处理
async request(req,res){
let pathname = url.parse(req.url);
if(pathname == '/favicon.ico') return; //浏览器会自动向咱们索取网站图标,这里没有准备,为了防止报错,返回便可
let filepath = path.join(this.config.root,pathname);
try{
let statObj = await stat(filepath);
if(statObj.isDirectory()){
let files = awaity readdir(filepath);
files.map(file=>{
name:file
,path:path.join(pathname,file)
});
// 让handlebar 拿着数去编译模板
let html = this.list({
title:pathname
,files
})
res.setHeader('Content-Type','text/html');
res.end(html);
}else{
this.sendFile(req,res,filepath,statObj);
}
}catch(e){
this.sendError(e,req,res);
}
}
复制代码
[tip] 咱们将
request
方法async
化,这样咱们就能像写同步代码同样写异步
涉及缓存、编码、分段传输等功能
sendFile(){
if(this.handleCache(req,res,filepath,statObj)) return; //若是走缓存,则直接返回。
res.setHeader('Content-type',mime.getType(filepath)+';charset=utf-8');
let encoding = this.getEncoding(req,res); //获取浏览器能接收的编码并选择一种
let rs = this.getStream(req,res,filepath,statObj); //支持断点续传
if(encoding){
rs.pipe(encoding).pipe(res);
}else{
rs.pipe(res);
}
}
复制代码
缓存处理时要注意的是,缓存分为强制缓存和对比缓存,且强制缓存的优先级是高于相对缓存的。
也就是说,当强制缓存生效的时候并不会走相对缓存,不会像服务器发起请求。
但一旦强制缓存失效,就会走相对缓存,若是文件标识
没有改变,则相对缓存生效,
客户端仍然会去缓存数据拿取数据,因此强制缓存和相对缓存并不冲突。
强制缓存和相对缓存一块儿使用时,能在减小服务器的压力的同时又保持请求数据的及时更新。
另外须要注意的是,若是同时设置了两种相对缓存的文件标识,必需要两种都没有改变时,缓存才生效。
handleCache(req,res,filepath,statObj){
let ifModifiedSince = req.headers['if-modified-since']; //第一次请求是不会有的
let isNoneMatch = req.headers['is-none-match'];
res.setHeader('Cache-Control','private,max-age=30');
res.setHeader('Expires',new Date(Date.now()+30*1000).toGMTString()); //此时间必须为GMT
let etag = statObj.size;
let lastModified = statObj.ctime.toGMTString(); //此时间格式可配置
res.setHeader('Etag',etag);
res.setHeader('Last-Modified',lastModified);
if(isNoneMatch && isNoneMatch != etag) return false; //如果第一次请求已经返回false
if(ifModifiedSince && ifModifiedSince != lastModified) return false;
if(isNoneMatch || ifModifiedSince){
// 说明设置了isNoneMatch或则isModifiedSince且文件没有改变
res.writeHead(304);
res.end();
return true;
}esle{
return false;
}
}
复制代码
若想更详细的了解缓存相关的内容,能够阅读个人这篇文章
从请求头中拿取到浏览器能接收的编码类型,利用正则匹配匹配出最前面那个, 建立出对应的zlib实例返回给sendFile方法,以便在返回文件时进行编码。
getEncoding(req,res){
let acceptEncoding = req.headers['accept-encoding'];
if(/\bgzip\b/.test(acceptEncoding)){
res.setHeader('Content-Encoding','gzip');
return zlib.createGzip();
}else if(/\bdeflate\b/.test(acceptEncoding)){
res.setHeader('Content-Encoding','deflate');
return zlib.createDeflate();
}else{
return null;
}
}
复制代码
分段传输,主要利用的是请求头中的req.headers['range']
来确认要接收的文件是从哪里开始到哪里结束,然而真正拿到这部分数据是经过fs.createReadStream
来读取到的。
getStream(req,res,filepath,statObj){
let start = 0;
// let end = statObj.size - 1;
let end = statObj.size;
let range = req.headers['range'];
if(range){
let result = range.match(/bytes=(\d*)-(\d*)/); //不可能有小数,网络传输的最小单位为一个字节
if(result){
start = isNaN(result[1])?0:parseInt(result[1]);
// end = isNaN(result[2])?end:parseInt(result[2]) - 1;
end = isNaN(result[2])?end:parseInt(result[2]);
}
res.setHeader('Accept-Range','bytes');
res.setHeader('Content-Range',`bytes ${start}-${end}/${statObj.size}`)
res.statusCode = 206; //返回整个数据的一块
}
return fs.createReadStream(filepath,{
start:start-1,end:end-1
});
}
复制代码
咱们能够像在命令行中输入npm start
启动一个dev-server同样自定义一个启动命令来启动咱们的静态服务器。
#! /usr/bin/env node
// -d 静态文件根目录
// -o --host 主机
// -p --port 端口号
let yargs = require('yargs');
let Server = require('../src/app.js');
let argv = yargs.option('d',{
alias:'root'
,demand:'false' //是否必填
,default:process.cwd()
,type:'string'
,description:'静态文件根目录'
}).option('o',{
alias:'host'
,demand:'false' //是否必填
,default:'localhost'
,type:'string'
,description:'请配置监听的主机'
}).option('p',{
alias:'port'
,demand:'false' //是否必填
,default:8080
,type:'number'
,description:'请配置端口号'
})
//usage 命令格式
.usage('static-server [options]')
// example 用法实例
.example(
'static-server -d / -p 9090 -o localhost'
,'在本机9090的端口上监听客户端的请求'
)
.help('h').argv;
//argv = {d,root,o,host,p,port}
let server = new Server(argv);
server.start();
let os = require('os').platform();
let {exec} = require('child_process');
let url = `http://${argv.hostname}:${argv.port}`
if(argv.open){
if(os === 'win32'){
exec(`start ${url}`);
}else{
exec(`open ${url}`);
}
}
复制代码
至于原理,限于篇幅,更多详细信息请关注这篇个人这篇文章process.argv与命令行工具
npm i static-server-study
复制代码
let static = require('static-server-study');
let server = new static({
port:9999
,root:process.cwd()
});
server.start();
复制代码
clone后,执行如下命令
npm init
npm link
复制代码
而后咱们就能将任意目录当作一个静态服务器的工做目录, 只需在那个目录下打开命令行窗口输入static-server