女神镇楼
我们都知道在本地起个服务直接一个
http-server -p 3000
一个端口为 3000 的服务就起来了, 我们可以直接在浏览器访问 3000 端口, 就能拿到我们需要的页面, 那么如果想自己实现一个这样的工具怎么做呢? 不要急, 看我慢慢分析写出来
写之前我们先要搞清楚要做什么: 用自己写的包, 起一个服务, 访问 3000 端口回车, 应该显示出 public 下的目录列表, 后面加 / index.html, 就应该显示 index.html 的内容来
首先先 init 一个项目, 并下载一些包, mime(解析返回头类型),chalk(五颜六色的输出),debug
建立自己的目录结构:
- index.CSS
- body{
- background: red
- }
- index.html
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta http-equiv="X-UA-Compatible" content="ie=edge">
- <title>Document</title>
- </head>
- <body>
我很美
- <link rel="stylesheet" href="/index.css">
- </body>
- </html>
config.js
- let path = require('path')
- // 启动服务的配置项
- let config = {
- hostname:'localhost',
- port:3000,
- dir:path.join(__dirname,'..','public')
- }
- module.exports = config
app.js 这里用到了 debug(用法请看这里 https://www.npmjs.com/package/debug )
- // set DEBUG=static:app (win32 // export DEBUG=static:app (ios
- let config = require('./config')
- let path = require('path')
- let fs = require('fs')
- let mime = require('mime')
- let chalk = require('chalk')
- let util = require('util')
- let url = require('url')
- let http = require('http')
- let stat = util.promisify(fs.stat)
- //debug 可以后面放参数, 可以根据后面的参数决定是否打印
- let debug = require('debug')('static:app')
- //console.log(chalk.green('hello'));
- //debug('app')
- class Server { // 首先写一个 Server 类
- constructor(){
- this.config = config
- }
- handleRequest(){
- return (req,res)=>{
- }
- }
- start(){ // 实例上的 start 方法
- let {port,hostname} = this.config
- let server = http.createServer(this.handleRequest())
- // 用 http 启动一个服务, 回调里执行 handleRequest 方法
- let url = `http://${hostname}:${chalk.green(port)}`
- debug(url);
- server.listen(port, hostname);
- }
- }
- let server = new Server()
- server.start()
node 执行 app.js,(在执行之前要先执行 set DEBUG=static:app) 得到下图
如果你想实时监控项目的变化可以安装一个 supervisor(npm install supervisor -g), 直接执行 supervisor app.js 就能监控了, 不过不是很稳定...... 这个时候可以假设访问的是 http://localhost:3000/index.html, 是个文件, 我们就可以写 handleRequest 方法了
- handleRequest(){
- return async(req,res)=>{
- // 处理路径
- let {pathname} = url.parse(req.url,true)
- // 因为拿到的 pathname 会是 / index, 这样会直接指向 c 盘, 加./ 的话就变成当前
- let p = path.join(this.config.dir,'.'+pathname)
- try{
- let statObj = await stat(p)// 判断 p 路径对不对
- if(statObj.isDirectory()){
- }else{
- // 是文件就直接读了
- res.setHeader('Content-Type',mime.getType(p)+';charset=utf8')
- fs.createReadStream(p).pipe(res)
- }
- }catch(e){
- res.statusCode = 404;
- res.end()
- }
- }
- }
这时候访问 http://localhost:3000/index.html, 就能出页面信息了
架子搭出来了, 那么就开始写吧, 因为报错和展示页面信息要重复利用, 所以把他们单独提出来封成两个方法
- sendFile(req,res,p){
- // 是文件就直接读了
- res.setHeader('Content-Type',mime.getType(p)+';charset=utf8')
- fs.createReadStream(p).pipe(res)
- }
- sendError(req,res,e){
- debug(util.inspect(e).toString())
- res.statusCode = 404;
- res.end()
- }
假如访问的是个目录的话, 我们应该把目录展示出来, 这样的话最好使用模板引擎, 常见的模板引擎有: handlebar ejs 这我们用的是 ejs, 用法 render('文件内容','变量参数'), 装一下 :npm install ejs src/tmpl.ejs
- <!DOCTYPE html>
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
- <meta name="renderer" content="webkit">
- <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
- <title>staticServer</title>
- </head>
- <body>
- <!-- 碰到 js 就 <% %> 包起来, 求值用 = -->
- <%dirs.forEach(dir=>{%>
- <li><a href="<%=dir.path%>"><%=dir.name%></a></li>
- <% })%>
- </body>
- </html>
那在 app.js 中就要放进去
- let ejs = require('ejs')
- let tmpl = fs.readFileSync(path.join(__dirname,'tmpl.ejs'),'utf8')
- let readDir = util.promisify(fs.readdir)// 读取目录用的方法
git 上各种模板在这里 https://www.npmjs.com/package/consolidate
再把 tmpl 挂在 this 上 app.js 那么如果是目录的话这个代码就这么写
- if(statObj.isDirectory()){
- // 如果是目录的话就应该把目录放出去
- // 用模板引擎写 handlebal ejs underscore jade
- let dirs = await readDir(p)
- debug(dirs)// 返回的是个数组 [index.css,index.html]
- dirs = dirs.map(dir => ({
- path: path.join(pathname, dir),
- name: dir
- }))
- let content = ejs.render(this.tmpl,{dirs})
- res.setHeader('Content-Type','text/html;charset=utf8')
- res.end(content)
- }else{
- this,this.sendFile(req,res,p)
- }
下面就是细化的问题了, 总共 3 个方向:
如果文件访问过, 就应该有缓存的功能,
文件很大应该有压缩,
范围请求 缓存
- cache(req,res,statObj){
- //etag if-none-match
- //Last-Modified if-modified-since
- //Cache-Control
- //ifNoneMatch 一般是内容的 md5 戳 => ctime+size
- let ifNoneMatch = req.headers['if-none-match']
- //ifModifiedSince 文件的最新修改时间
- let ifModifiedSince = req.headers['if-modified-since']
- let since = statObj.ctime.toUTCString();// 最新修改时间
- // 代表的是服务器文件的一个描述
- let etag = new Date(since).getTime() +'-'+statObj.size
- res.setHeader('Cache-Control','max-age=10')
- //10 秒之内强制缓存
- res.setHeader('Etag',etag)
- res.setHeader('Last-Modified',since) // 请求头带着
- // 再访问的时候对比, 如果相等, 就走缓存
- if(ifNoneMatch !== etag){
- return false
- }
- if(ifModifiedSince != since){
- return false
- }
- res.statusCode = 304
- res.end()
- return true
- }
sendFile 中加这句话
- // 缓存
- if(this.cache(req,res,statObj)) return
那么访问 index.html 访问的画面是
查看他们的头
当然再刷新的话 200 就会变成 304, 走的是缓存了 压缩 因为用到了 zlib 所以要在头上加上
let zlib = require('zlib');
压缩方法
- compress(req,res,statObj){
- // 压缩 Accept-Encoding: gzip,deflate,br
- // Content-Encoding:gzip
- let header = req.headers['accept-encoding']
- if(header){
- if(header.match(/\bgzip\b/)){
- res.setHeader('Content-Encoding','gzip')
- return zlib.createGzip()
- }else if(header.match(/\bdeflate\b/)){
- res.setHeader('Content-Encoding','deflate')
- return zlib.createDeflate()
- }else{
- return false // 不支持压缩
- }
- }else{
- return false
- }
- }
- sendFile
- sendFile(req,res,p,statObj){
- // 缓存
- if(this.cache(req,res,statObj)) return
- // 压缩
- let s = this.compress(req, res, p, statObj);
- console.log(s)
- res.setHeader('Content-Type',mime.getType(p)+';charset=utf8')
- let rs = fs.createReadStream(p)
- if(s){
- // 如果支持就是返回的流
- rs.pipe(s).pipe(res)
- }else{
- rs.pipe(res)
- }
- // 是文件就直接读了
- // fs.createReadStream(p).pipe(res)
- }
查看一下是否压缩成功: 访问 http://localhost:3000/index.css
可以看到支持的是 gzip, 成功! 范围请求 方法
- range(req,res,statObj){
- // 范围请求的头 :Rang:bytes=1-100
- // 服务器 Accept-Ranges:bytes
- //Content-Ranges:1-100/total
- let header = req.headers['range']
- //header =>bytes=1-100
- let start = 0;
- let end = statObj.size;// 整个文件的大小
- if(header){
- res.setHeader('Content-Range','bytes')
- res.setHeader('Accept-Ranges',`bytes ${start}-${end}/${statObj.size}`)
- let [,s,e] = header.match(/bytes=(\d*)-(\d*)/);
- start = s?parseInt(s):start
- end = e? parseInt(e):end
- }
- return {start,end:end-1}// 因为 start 是从 0 开始
- }
sendFile 文件就是这样了
- sendFile(req, res, p, statObj) {
- // 缓存的功能 对比 强制
- if (this.cache(req, res, statObj)) return;
- // 压缩 Accept-Encoding: gzip,deflate,br
- // Content-Encoding:gzip
- res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8');
- let s = this.compress(req, res, p, statObj);
- // 范围请求
- let {start,end} = this.range(req,res,statObj);
- let rs = fs.createReadStream(p,{start,end})
- if (s) {
- rs.pipe(s).pipe(res);
- } else {
- rs.pipe(res);
- }
- }
在命令行工具下执行
curl -v --header "Range:bytes=1-3" http://localhost:3000/index.html 就可以看到效果了
如果你的 window 不能执行 curl 可以看这里 https://blog.csdn.net/wkj001/article/details/54889907
git 地址 https://github.com/w1wenya/staticServer
来源: https://juejin.im/post/5ad6e7e66fb9a028c813636c