企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[TOC] ## 阅前须知 此文正在不断完善,阅读请谨慎 ## 设计思路 当你输入一个url时,这个url可能对应服务器上的一个资源(文件)也可能对应一个目录。 So服务器会对这个url进行分析,针对不同的情况做不同的事。 如果这个url对应的是一个文件,那么服务器就会返回这个文件。 如果这个url对应的是一个文件夹,那么服务器会返回这个文件夹下包含的所有子文件/子文件夹的列表。 以上,就是一个静态服务器所主要干的事。 但真实的情况不会像这么简单, 我们所拿到的url可能是错误的,它所对应的文件或则文件夹或许根本不存在, 又或则有些文件和文件夹是被系统保护起来的是隐藏的,我们并不想让客户端知道。 因此,我们就要针对这些特殊情况进行一些不同的返回和提示。 再者,当我们真正返回一个文件前,我们需要和客户端进行一些协商。 我们需要知道客户端能够接受的语言类型、编码方式等等以便针对不同浏览器进行不同的返回处理。 我们需要告诉客户端一些关于返回文件的额外信息,以便客户端能更好的接收数据: 文件是否需要缓存,该怎样缓存? 文件是否进行了压缩处理,该以怎样的方式解压? 等等... 至此,我们已经初步了解了一个静态服务器所主要做的几乎所有事情, let's go! ## 实现 ### 项目目录 ``` static-server/ | | - bin/ | | - start # 批处理文件 | | | - src/ | | - App.js # main文件 | | - Config.js # 默认配置 | | ·- package.json ``` ### 配置文件 要启动一个服务器,我们需要知道这个服务器的启动时的端口号和静态服务器的工作目录 ``` let config = { host:'localhost' //提升用 ,port:8080 //服务器启动时候的默认端口号 ,path:path.resolve(__dirname,'..','test-dir') //静态服务器启动时默认的工作目录 } ``` ### 整体框架 **注意** - 事件函数中的this默认指向绑定的对象(这里是小server),这里修改成了Server这个大对象,以便调用在回调函数中调用Server下的方法。 ``` 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; ``` ### request请求处理 获取url的`pathname`,和**服务器本地的工作根目录地址**进行拼接,返回一个`filename` 利用filename和`stat方法`检测是文件还是文件夹 是文件夹, 利用`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 涉及缓存、编码、分段传输等功能 ``` 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 缓存处理时要注意的是,缓存分为强制缓存和对比缓存,且强制缓存的优先级是高于相对缓存的。 也就是说,当强制缓存生效的时候并不会走相对缓存,不会像服务器发起请求。 但一旦强制缓存失效,就会走相对缓存,如果`文件标识`没有改变,则相对缓存生效, 客户端仍然会去缓存数据拿取数据,所以强制缓存和相对缓存并不冲突。 **强制缓存和相对缓存一起使用时,能在减少服务器的压力的同事又保持请求数据的及时更新。** 另外需要注意的是,如果同时设置了两种相对缓存的文件标识,必须要两种都没有改变时,缓存才生效。 ``` 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; } } ``` #### getEncoding 从请求头中拿取到浏览器能接收的编码类型,利用正则匹配匹配出最前面那个, 创建出对应的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; } } ``` #### getStream 分段传输,主要利用的是请求头中的`req.headers['range']`来确认要接收的文件是从哪里开始到哪里结束,然而真正拿到这部分数据是通过`fs.createReadStream`来读取到的。 ``` getStream(req,res,filepath,statObj){ let start = 0; let end = startObj.size - 1; let range = req.headers['range']; if(range){ res.setHeader('Accept-Range','bytes'); res.statusCode = 206; //返回整个数据的一块 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; //因为readstream的索引是包前又包后故要减去1 } } return fs.createReadStream(filepath,{ start,end }); } ``` ## 包装成命令行工具 我们可以像在命令行中输入`npm start`启动一个dev-server一样自定义一个启动命令来启动我们的静态服务器。 大体实现的思路是: 在`packge.json`中的`bin`属性下配置一个启动命令和这个执行这个命令的文件的路径。 然后我们需要准备一个批处理文件,在文件中引入我们的静态服务器文件,让我们的服务器跑起来 然后将这个文件`node link`即可。