咱們先來體驗下npm包http-server的功能 html
訪問下試試,有點牛皮的樣子 訪問下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響應頭裏面去
咱們看來看下請求的內容