webpack-dev-serve 开启本地服务器
现状:打包后将index.html
文件通过open in liveServe
打开,我们继续修改js、html
文件内容,页面没有变化。
解决方法:
方法一:
在打包命令后面加上--watch
,重新打包后我们的终端不会终止,我们打包打开项目后,再代码中继续修改,不需要重新打包打开,页面会自动更新
"scripts": {
"build": "webpack --mode=development --watch"
},
方法二:
// webpack.config.js
module.exports = {
watch:true,
以上方案不足:
- 所有源代码都会重新执行编译
- 每次编译成功都会删除dist目录,然后进行文件读写,与磁盘进行交互
liveServe
是vscode
生态下的,webpack
也有这样的类似功能实现- 不能实现局部刷新:页面包含多个组件,每次不希望整个页面进行刷新,而是单个组件的更新,但是当前是整体刷新
方法三:
npm i webpack-dev-server
// package.json
"scripts": {
"build": "webpack --mode=development",
"serve": "webpack serve"
},
这样是把数据存储在内存里面,提高效率
webpack-dev-middleware 中间件
文档地址
我们追求操作性更高更灵活,vue和react内部是用的webpack-dev-server
npm install --save-dev express webpack-dev-middleware
步骤:
- 开启服务
- 将webpack打包后的工具交给服务
// Server.js
const express = require('express')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpack = require('webpack')
const app = express()
// 继续打包
// 获取配置文件
const config = require('./webpack.config')
const compiler = webpack(config)
app.use(webpackDevMiddleware(compiler))
// 开启端口服务
app.listen(3000, () => {
console.log('服务运行在3000端口上')
})
运行:node ./Server.js
热更新定义
Hot Module Replacement
是指当我们对代码修改并保存后,webpack
将会对代码进行重新打包,并将新的模块发送到浏览器端,浏览器用新的模块替换掉旧的模块 ,以实现在不刷新浏览器的前提下更新页面。
例如,我们在应用运行过程中修改了某个模块,通过自动刷新会导致整个应用的整体刷新,那页面中的状态信息都会丢失
如果使用的是HMR
,就可以实现只将修改的模块实时替换至应用中,不必完全刷新整个应用。
开启热更新:
// webpack.config.js
module.export = {
...,
devServer:{
hot:true
}
}
配置完成之后我们修改css
文件,确实能够以不刷新的形式更新到页面中,但是当我们修改保存js
文件时,页面仍然自动刷新了,需要进行配置指定哪些模块发生更新时进行HRM
// 如果当前的模块支持热更新,注册回调
if(module.hot){
// index.js模块可以接受title.js的变更,变更后,调用render方法
module.hot.accept('./util.js',()=>{
console.log("util.js更新了")
})
}
可以在回调函数中拿到最新的util
文件,进行操作
热更新流程
初始时:在编写未经webpack
打包的源码后,Webpack Compile
将源代码和 HMR Runtime
一起编译成 bundle
文件,传输给Bundle Server
静态资源服务器
更新时:当某一个模块或者文件发生变化时,webpack
监听到文件变化对文件重新编译打包,编译生成唯一的hash
值,这个hash
值用来作为下一次热更新的标识
根据变化的内容生成两个补丁文件:manifest
(包含了hash
和chunkId
,用来说明变化的内容)和chunk.js
模块
由于socket
服务器在HMR Runtime
和 HMR Server
之间建立 websocket
链接,当文件发生改动的时候,服务端会向浏览器推送一条消息,消息包含文件改动后生成的hash
值,作为下一次热更细的标识
浏览器接受到这条消息之前,浏览器已经在上一次socket
消息中已经记住了此时的hash
标识,这时候我们会创建一个 ajax
去服务端请求获取到变化内容的 manifest
文件
mainfest
文件包含重新build
生成的hash值,以及变化的模块
浏览器根据manifest
文件获取模块变化的内容,从而触发render
流程,实现局部模块更新
实现webpack-dev-server
初始搭建
const webpack = require("webpack");
const config = require("../webpack.config.js");
const Server = require("./lib/server/server.js");
// 编译器对象
const compiler = webpack(config);
const server = new Server(compiler);
server.listen(9090, "localhost", () => {
console.log("服务已经在9090端口使用");
});
启动一个服务器
// server.js
const express = require("express");
const http = require("http");
const path = require("path");
const MemoryFs = require("memory-fs"); //内存级文件系统 因为内存比硬盘操作更换
const mime = require("mime");
const updateCompiler = require("./updateCompiler");
const socketIo = require("socket.io");
/**
* webpack.config执行得到一个compile文件,compile是一个大管家,代表本次查询的对象,传递给server
* server中,updateCompiler 向入口注入代码,实现热更新的
* setupHooks设置钩子 每当新的编译成功后,会触发回调,向客户端发送hash和ok(告诉客户端来拉取我得代码)
* setupDevMiddleware 设置中间件 以监听模式启动编译 文件发生改变时会饶工webpack重新编译,编译后的文件写到内存文件系统中,触发done回调
* createServer 客户端想访问文件 启动一个静态服务中间件 app.use使用这个中间件,读取到文件返回给客户端
* createSocketServer 启动服务,双向通信服务器,编译成功后给所有客户端发送消息,
* 服务器端要主动往客户端推送代码,所以需要socket服务
*/
// webpack编译时会把文件写到文件系统去 客户端启动服务器通过路由访问中间件 中间件读取文件系统
class Server {
constructor(compiler) {
this.compiler = compiler;
updateCompiler(compiler);
this.setupAPP(); //创建app
this.currentHash; //当前的hash值,每次编译都会产生一个hash值
this.clientSocketList = []; // 存放所有通过websocket链接到服务器的客户端
this.setupHooks(); // 建立钩子
this.setupDevMiddleware(); //建立开发中间件
this.routes(); //配置路由
this.createServer(); //创建http服务器,以app作为路由
this.createSocketServer(); //创建socket服务器
}
createSocketServer() {
// websocket 协议握手需要依赖http服务器的
const io = socketIo(this.server);
// 服务器要监听客户端的连接,当客户端链接上来后,socket代表跟这个客户端连接对象
io.on("connection", (socket) => {
console.log("一个新的客户端已经连接上了");
this.clientSocketList.push(socket); //把新的socket放到数组
socket.emit("hash", this.currentHash); //发送hash值
socket.emit("ok");
// 监听客户端断开连接,把客户端从数组中删除
socket.on("disconnect", () => {
let index = this.clientSocketList.indexOf(socket);
this.clientSocketList.splice(index, 1);
});
});
}
routes() {
let { compiler } = this;
let config = compiler.options;
this.app.use(this.middleware(config.output.path));
}
setupDevMiddleware() {
this.middleware = this.webpackDevMiddleware();
}
webpackDevMiddleware() {
let { compiler } = this;
//以监听的模式启动编译,如果以后文件发生变化了 会重新编译
compiler.watch({}, () => {
console.log("监听模式编译成功");
});
let fs = new MemoryFs(); //内存文件系统实例
this.fs = compiler.outputFileSystem = fs;
// 返回一个中间件,用来响应客户端对于产出文件的请求
return (staticDir) => {
return (req, res, next) => {
let { url } = req; //得到请求路径
if (url === "/favicon.ico") {
return res.sendStatus(404);
}
url === "/" ? (url = "/index.html") : null;
let filePath = path.join(staticDir, url); //得到要访问的静态路径 绝对路径
try {
// 返回此路径上的描述对象,如果此文件不存在会抛出异常
let stateObj = this.fs.statSync(filePath);
if (stateObj.isFile()) {
let content = this.fs.readFileSync(filePath); //读取文件内容
res.setHeader("Content-Type", mime.getType(filePath)); //设置响应头 告诉浏览器此文件内容是什么
res.send(content); //把文件内容发送给浏览器
} else {
return res.sendStatus(404);
}
} catch (error) {
return res.sendStatus(404);
}
};
};
}
setupHooks() {
let { compiler } = this;
// 监听编译完成事件 当编译完成之后会调用此钩子函数
compiler.hooks.done.tap("webpack-dev-server", (stats) => {
// stats:描述对象 里卖放着打包后的结果
console.log("hash", stats.hash);
this.currentHash = stats.hash;
// 向所有的客户端广播 告诉客户端我已经编译成功了 新的模块代码已经生成了
this.clientSocketList.forEach((socket) => {
socket.emit("hash", this.currentHash);
socket.emit("ok");
});
});
}
setupAPP() {
this.app = express(); //代表http应用对象
}
createServer() {
// 通过app创建一个普通的服务器
this.server = http.createServer(this.app);
}
listen(port, host, callback) {
this.server.listen(port, host, callback);
}
}
module.exports = Server;
// updateCompiler.js
/**
* 实现客户端和服务器通信 需要往入口里面多注入两个文件
* (webpack)-dev-server/client/index.js
* (webpack)/hot/der-server.js
* */
const path = require("path");
function updateCompiler(compiler) {
const config = compiler.options;
config.entry = {
main: [
path.resolve("../client/index.js"),
path.resolve("../client/hot/der-server.js"),
config.entry,
],
};
}
module.exports = updateCompiler;