背景
为了加深对node中的http模块的理解,自己尝试写了一本地的静态服务。相信大部分人都用过http-server。会想起我自己第一次接触http-server是这样的,我要传一本电子书给同事,同事说你直接在你本地起一个server,这样我通过你本机的ip就可以下载了不用u盘,当时我就觉得很神奇。我们要实现的效果大致如下:
- 打开localhost:3000会展示当前服务所在的目录
- 可以点开当前的目录查看里面的文件
- 对于html文件可以展示内容,执行嵌套在里面的js文件
目录结构
配置文件
const config = {
port: 3000,
host: 'localhost',
dir: process.cwd() // 获取当前文件所在的目录
}
复制代码
核心模块
这小节先给出核心骨架,然后下面再慢慢填坑。在开撸之前我们先捋捋思路。要在浏览器打开,那我们得起一个服务吧,起了服务之后,我们在url后面输入地址例如:localhost:3000/src得匹配到本地的真实路径吧,匹配到真实路径后,我们需要判断该路径是一个文件夹还是文件,如果是文件那么好办,就直接渲染就行,如果是文件夹,那么我们就循环出目录结构进行展示。所以总共需要一下四步:
- 起一个服务
- 根据url链接匹配出本地的目录
- 读取本地目录
- 判断是否为文件和文件夹分类处理
接下来我们就一步一步填坑,首先我们来起一个服务
const config = require('../config') // 引入我们的配置文件
const http = require('http')
class Server {
constructor () {
this.config = config
}
start () {
const { host, port } = config
const server = http.createServer((req, res) => {
res.end('hello static server')
})
server.listen(port, () => {
console.log(`Server is running http://${host}:${port}`)
})
}
}
new Server().start()
module.exports = Server
复制代码
简单的服务构建完啦,这个也很容易理解,就是利用node的http模块起一个服务 接下来就是匹配出本地目录
const config = require('../config') // 引入我们的配置文件
const http = require('http')
const url = require('url')
const path = require('path')
class Server {
constructor () {
this.config = config
}
start () {
const { host, port } = config
// 如果接着在回调里面处理显得比较臃肿
const server = http.createServer(this.handlerRequest.bind(this))
server.listen(port, () => {
console.log(`Server is running http://${host}:${port}`)
})
}
handlerRequest (req, res) {
// 我们引入url模块获取url上的路径
const { dir } = this.config // 获取启动文件的根目录
const { pathname } = url.parse(req.url) // localhost:3000/src ==> /src
// 拼接出真实的本地路径 => 引入path模块
const realPath = path.join(dir, pathname)
}
}
new Server().start()
module.exports = Server
复制代码
这一步就是读取本地目录啦。
const config = require('../config') // 引入我们的配置文件
const http = require('http')
const url = require('url')
const path = require('path')
const util = require('util')
const fs = require('fs')
const stat = util.promisify(stat) // 对读取目录进行promisify
const readdir = util.promisify(readdir)
const temple = fs.readFileSync(path.join(__dirname, 'index.html'), 'utf-8')
class Server {
constructor () {
this.config = config
this.template = template
}
start () {
const { host, port } = config
// 如果接着在回调里面处理显得比较臃肿
const server = http.createServer(this.handlerRequest.bind(this))
server.listen(port, () => {
console.log(`Server is running http://${host}:${port}`)
})
}
async handlerRequest (req, res) {
// 我们引入url模块获取url上的路径
const { dir } = this.config // 获取启动文件的根目录
const { pathname } = url.parse(req.url) // localhost:3000/src ==> /src
// 拼接出真实的本地路径 => 引入path模块
const realPath = path.join(dir, pathname)
try {
let statObj = await stat(realPath)
if (statObj.isDirectory()) {
res.setHeader('Content-Type', 'text/html;charset=utf-8')
let dirs = await readdir(realPath)
dirs = dirs.map(p => {
return {
name: p,
path: path.join(pathname, p)
}
})
let tempStr = ejs.render(this.template, {
dirs
})
res.end(tempStr)
} else {
res.setHeader('Content-Type', mime.getType(realPath) + ';charset=utf-8')
res.createReadStream(realPah).pipe(res)
}
}catch (e) {
console.log(e)
}
}
}
new Server().start()
module.exports = Server
复制代码
添加缓存功能
http协议缓存有两种方式:
- 强制缓存
- 协商缓存
强制缓存vs协商缓存
cache (req, res, statObj, realPath) {
console.log('cache')
/*
* 缓存类型 服务端 客户端
* 强制缓存 Expires Expires
* Cache-Control Cache-Control
* 协商缓存 Last-Modified if-modified-since
* Etag if-none-match
* */
let etag = statObj.ctime.toGMTString() + statObj.size // 文件修改时间和文件大小
let lastModified = statObj.ctime.toGMTString() // 文件的修改的时间
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toGMTString()) // 毫秒为单位
res.setHeader('Etag', etag)
res.setHeader('Last-Modified', lastModified)
let ifNoneMath = req.headers['if-none-match']
let ifModifinedSince = req.headers['if-modified-since']
if (etag != ifNoneMath) {
return false
}
if (lastModified != ifModifinedSince) {
console.log('Last-Modified')
return false
}
return true
}
复制代码