咱们先来体验下npm包http-server的功能 html
下面咱们来整一个吧node
咱们先来整理下步骤:webpack
@babel/core 主要是babel的核心模块
@babel/preset-env env这个预设是es6转es5的插件集合
babel-loader 是webpack和loader的桥梁
说到用到babel,那确定少不了配置文件.babelrc文件啦,配置以下es6
{
"presets": [
["@babel/preset-env", {
"targets":{
"node": "current"
}
}]
]
}
复制代码
例如咱们使用es6编写的代码是放在src目录下,咱们能够写一个npm scripts 经过babel转成commonjsweb
"scripts": {
"babel:dev": "babel ./src -d ./dist --watch",
"babel": "babel ./src -d ./dist"
},
复制代码
这边是将咱们的源码babel转移后到dist目录,用户使用到的其实就是咱们dist目录内容了算法
正常咱们开发的npm包怎么调试呢
答案可能有不少哈,我这边的话主要是推荐使用npm link 或者是sync-files(只是同步文件,须要配置node_modules对应的文件目录)
npm link是一种软链的方式去把当前目录 作一个软链到node全局安装的目录下,这样咱们不管在哪里均可以使用了,其实真正访问的仍是本地npm包的目录 npm
//scripts指令:
sync-files --verbose ./lib $npm_config_demo_path
// 这边用到了npm的变量$npm_config_demo_path,须要配置.npmrc的文件
// demo_path = 实际要替换的依赖包地址
复制代码
上面的npm link 还没说完,npm包要告诉我当前的包要从哪里开始执行怎么整呢
配置bin或者main方法
promise
"bin": {
"server": "./bin/www"
}, // 这边的指令名称能够随便起哈
复制代码
第一步咱们都介绍完了,咱们要真正开始来实现了浏览器
模板文件template.html缓存
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>模板数据</title>
</head>
<body>
<ul>
<% dirs.forEach(dir => {%>
<li><a href="<%=pathname%>/<%=dir%>"><%=dir%></a></li>
<% }) %>
</ul>
</body>
</html>
复制代码
main.js文件主要是让咱们作一个相似于http-server脚手架的一个展现信息,咱们接收参数,能够引用commander这个包
import commander from "commander";
import Server from './server';
commander.option('-p, --port <val>', "please input server port").parse(process.argv);
let config = {
port: 3000
}
Object.assign(config, commander);
const server = new Server(config);
server.start();
复制代码
这个文件里面咱们只是监听了命令的入参,可使用-p 或者--p来传一个参数
若是不传,我这边会有个初始的端口
而后咱们传给了server.js
import fs from "fs";
import http from 'http';
import mime from 'mime';
import path from 'path';
import chalk from "chalk";
import url from 'url';
import ejs from 'ejs';
const { readdir, stat } = fs.promises;
// 同步读取下template.html文件内容
const template = fs.readFileSync(path.join(process.cwd(), 'template.html'), 'utf8');
class Server {
constructor(config){
this.port = config.port;
this.template = template;
}
/** * 处理请求响应 */
async handleRequest(req, res){
// 获取请求的路径和传参
let {pathname, query} = url.parse(req.url, true);
// 转义中文文件名处理(解决中文字符转义问题)
pathname = decodeURIComponent(pathname);
// 下面是解决 // 访问根目录的问题
let pathName = pathname === '/' ? '': pathname;
let filePath = path.join(process.cwd(), pathname);
try {
// 获取路径的信息
const statObj = await stat(filePath);
// 判断是不是目录
if(statObj.isDirectory()) {
// 先遍历出全部的目录节点
const dirs = await readdir(filePath);
// 若是当前是目录则经过模板来解析出来
const content = ejs.render(this.template, {
dirs,
pathname:pathName
});
res.setHeader('Content-Type', 'text/html;charset=utf-8');
res.statusCode = 200;
res.end(content);
} else {
// 若是是文件的话要先读取出来而后显示出来
this.sendFile(filePath, req, res, statObj);
}
}catch(e) {
// 出错了则抛出404
this.handleErr(e, res);
}
}
/** * 处理异常逻辑 * @param {*} e * @param {*} res */
handleErr(e, res){
console.log(e);
res.setHeader('Content-Type', 'text/plain;charset=utf-8');
res.statusCode = 404;
res.end('资源未找到')
}
/** * 处理文件模块 */
sendFile(filePath, req, res, statObj){
console.log(chalk.cyan(filePath));
res.statusCode = 200;
let type = mime.getType(filePath);
// 当前不支持压缩的处理方式
res.setHeader('Content-Type', `${type};charset=utf-8`);
fs.createReadStream(filePath).pipe(res);
}
start(){
// 建立http服务
let server = http.createServer(this.handleRequest.bind(this));
server.listen(this.port, () => {
console.log(`${chalk.yellow('Starting up http-server, serving')} ${chalk.cyan('./')} ${chalk.yellow('Available on:')} http://127.0.0.1:${chalk.green(this.port)} Hit CTRL-C to stop the server`)
})
}
}
export default Server;
复制代码
总计下:
上面其实就实现了个简单的http-server
那么咱们想下咱们能作些什么优化呢???
怎么压缩呢,咱们来看下http请求内容吧,里面可能会注意到
Accept-Encoding: gzip, deflate, br
复制代码
浏览器支持什么方式咱们就是什么方式
咱们使用zlib包来作压缩操做吧
代码走一波
import fs from "fs";
import http from 'http';
import mime from 'mime';
import crypto from 'crypto';
import path from 'path';
import chalk from "chalk";
import url from 'url';
import ejs from 'ejs';
import zlib from 'zlib';
const { readdir, stat } = fs.promises;
const template = fs.readFileSync(path.join(process.cwd(), 'template.html'), 'utf8');
class Server {
constructor(config){
this.port = config.port;
this.template = template;
}
/** * 压缩文件处理 */
zipFile(filePath, req, res){
// 使用zlib库去压缩对应的文件
// 获取请求头数据Accept-Encoding来识别当前浏览器支持哪些压缩方式
const encoding = req.headers['accept-encoding'];
console.log('encoding',encoding);
// 若是当前有accept-encoding 属性则按照匹配到的压缩模式去压缩,不然不压缩 gzip, deflate, br 正常几种压缩模式有这么几种
if(encoding) {
// 匹配到gzip了,就使用gzip去压缩
if(/gzip/.test(encoding)) {
res.setHeader('Content-Encoding', 'gzip');
return zlib.createGzip();
} else if (/deflate/.test(encoding)) { // 匹配到deflate了,就使用deflate去压缩
res.setHeader('Content-Encoding', 'deflate');
return zlib.createDeflate();
}
return false;
} else {
return false;
}
}
/** * 处理请求响应 */
async handleRequest(req, res){
let {pathname, query} = url.parse(req.url, true);
// 转义中文文件名处理
pathname = decodeURIComponent(pathname);
let pathName = pathname === '/' ? '': pathname;
let filePath = path.join(process.cwd(), pathname);
try {
const statObj = await stat(filePath);
if(statObj.isDirectory()) {
// 先遍历出全部的目录节点
const dirs = await readdir(filePath);
// 若是当前是目录则经过模板来解析出来
const content = ejs.render(this.template, {
dirs,
pathname:pathName
});
res.setHeader('Content-Type', 'text/html;charset=utf-8');
res.statusCode = 200;
res.end(content);
} else {
// 若是是文件的话要先读取出来而后显示出来
this.sendFile(filePath, req, res, statObj);
}
}catch(e) {
// 出错了则抛出404
this.handleErr(e, res);
}
}
/** * 处理异常逻辑 * @param {*} e * @param {*} res */
handleErr(e, res){
console.log(e);
res.setHeader('Content-Type', 'text/plain;charset=utf-8');
res.statusCode = 404;
res.end('资源未找到')
}
/** * 处理文件模块 */
sendFile(filePath, req, res, statObj){
let zip = this.zipFile(filePath, req, res);
res.statusCode = 200;
let type = mime.getType(filePath);
if(!zip) {
// 当前不支持压缩的处理方式
res.setHeader('Content-Type', `${type};charset=utf-8`);
fs.createReadStream(filePath).pipe(res);
} else {
fs.createReadStream(filePath).pipe(zip).pipe(res);
}
}
start(){
let server = http.createServer(this.handleRequest.bind(this));
server.listen(this.port, () => {
console.log(`${chalk.yellow('Starting up http-server, serving')} ${chalk.cyan('./')} ${chalk.yellow('Available on:')} http://127.0.0.1:${chalk.green(this.port)} Hit CTRL-C to stop the server`)
})
}
}
export default Server;
复制代码
这边加个方法zipFile,来匹配浏览器支持的类型来作压缩,压缩也要给res告诉浏览器我服务端是根据什么来压缩的res.setHeader('Content-Encoding', '***');
压缩以前:
咱们整理下
强缓存是http 200
cache-control 能够设置max-age 相对时间 几秒
no-cache 是请求下来的内容仍是会保存到缓存里,每次都仍是要请求数据
no-store 表明不会将数据缓存下来
复制代码
Expires 绝对时间,须要给出一个特定的值 协商缓存是http 304
Etag 判断文件内容是否有修改
Last-Modified 文件上一次修改时间 根据这个方案咱们来作优化
import fs from "fs";
import http from 'http';
import mime from 'mime';
import crypto from 'crypto';
import path from 'path';
import chalk from "chalk";
import url from 'url';
import ejs from 'ejs';
import zlib from 'zlib';
const { readdir, stat } = fs.promises;
const template = fs.readFileSync(path.join(process.cwd(), 'template.html'), 'utf8');
class Server {
constructor(config){
this.port = config.port;
this.template = template;
}
/** * 压缩文件处理 */
zipFile(filePath, req, res){
// 使用zlib库去压缩对应的文件
// 获取请求头数据Accept-Encoding来识别当前浏览器支持哪些压缩方式
const encoding = req.headers['accept-encoding'];
console.log('encoding',encoding);
// 若是当前有accept-encoding 属性则按照匹配到的压缩模式去压缩,不然不压缩 gzip, deflate, br 正常几种压缩模式有这么几种
if(encoding) {
// 匹配到gzip了,就使用gzip去压缩
if(/gzip/.test(encoding)) {
res.setHeader('Content-Encoding', 'gzip');
return zlib.createGzip();
} else if (/deflate/.test(encoding)) { // 匹配到deflate了,就使用deflate去压缩
res.setHeader('Content-Encoding', 'deflate');
return zlib.createDeflate();
}
return false;
} else {
return false;
}
}
/** * 处理请求响应 */
async handleRequest(req, res){
let {pathname, query} = url.parse(req.url, true);
// 转义中文文件名处理
pathname = decodeURIComponent(pathname);
let pathName = pathname === '/' ? '': pathname;
let filePath = path.join(process.cwd(), pathname);
try {
const statObj = await stat(filePath);
if(statObj.isDirectory()) {
// 先遍历出全部的目录节点
const dirs = await readdir(filePath);
// 若是当前是目录则经过模板来解析出来
const content = ejs.render(this.template, {
dirs,
pathname:pathName
});
res.setHeader('Content-Type', 'text/html;charset=utf-8');
res.statusCode = 200;
res.end(content);
} else {
// 若是是文件的话要先读取出来而后显示出来
this.sendFile(filePath, req, res, statObj);
}
}catch(e) {
// 出错了则抛出404
this.handleErr(e, res);
}
}
/** * 处理异常逻辑 * @param {*} e * @param {*} res */
handleErr(e, res){
console.log(e);
res.setHeader('Content-Type', 'text/plain;charset=utf-8');
res.statusCode = 404;
res.end('资源未找到')
}
/** * 缓存文件 * @param {*} filePath * @param {*} req * @param {*} res */
cacheFile(filePath, statObj, req, res) {
// 读出上一次文件中的变动时间
const lastModified = statObj.ctime.toGMTString();
const content = fs.readFileSync(filePath);
// 读取出当前文件的数据进行md5加密获得一个加密串
const etag = crypto.createHash('md5').update(content).digest('base64');
res.setHeader('Last-Modified', lastModified);
res.setHeader('Etag', etag);
// 获取请求头的数据 If-Modified-Since 对应上面res返回的Last-Modified
const ifLastModified = req.headers['if-modified-since'];
// 获取请求头的数据 If-None-Match 对应上面res返回的Etag
const ifNoneMatch = req.headers['if-none-match'];
console.log(ifLastModified,lastModified);
console.log(ifNoneMatch,etag);
if(ifLastModified && ifNoneMatch) {
if(ifLastModified === lastModified || ifNoneMatch === etag) {
return true;
}
return false;
}
return false;
}
/** * 处理文件模块 */
sendFile(filePath, req, res, statObj){
console.log(chalk.cyan(filePath));
// 设置cache的时间间隔,表示**s内不要在访问服务器
res.setHeader('Cache-Control', 'max-age=3');
// 若是强制缓存,首页是不会缓存的 访问的页面若是在强制缓存,则会直接从缓存里面读取,不会再请求了
// res.setHeader('Expires', new Date(Date.now()+ 3*1000).toGMTString())
// res.setHeader('Cache-Control', 'no-cache'); // no-cache 是请求下来的内容仍是会保存到缓存里,每次都仍是要请求数据
// res.setHeader('Cache-Control', 'no-store'); // no-store 表明不会将数据缓存下来
// 在文件压缩以前能够先走缓存,查看当前的文件是不是走的缓存出来的数据
const isCache = this.cacheFile(filePath, statObj, req, res);
if(isCache) {
res.statusCode = 304;
return res.end();
}
let zip = this.zipFile(filePath, req, res);
res.statusCode = 200;
let type = mime.getType(filePath);
if(!zip) {
// 当前不支持压缩的处理方式
res.setHeader('Content-Type', `${type};charset=utf-8`);
fs.createReadStream(filePath).pipe(res);
} else {
fs.createReadStream(filePath).pipe(zip).pipe(res);
}
}
start(){
let server = http.createServer(this.handleRequest.bind(this));
server.listen(this.port, () => {
console.log(`${chalk.yellow('Starting up http-server, serving')} ${chalk.cyan('./')} ${chalk.yellow('Available on:')} http://127.0.0.1:${chalk.green(this.port)} Hit CTRL-C to stop the server`)
})
}
}
export default Server;
复制代码
总结下:
正常来讲强缓存和协商缓存是一块儿用的
强缓存设置cache-control 咱们设置下缓存时间是3S,这边设置的是相对时间3S,不加协商缓存,咱们试下看看
下面咱们加下协商缓存试下吧
Last-Modified 这个正常来讲这个值是放的文件的更新时间,咱们这边使用stat获取到文件的ctime
Etag 官方说这个是一个新鲜复杂度的算法,这边为了方便处理,个人Etag没作什么算法处理,只是用文件内容md5加密成base64,内容长度固定,不会太大
咱们第一次访问会将这2个值塞入到res响应头里面去
咱们看来看下请求的内容