使用
webpack.config.js:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');
module.exports = {
mode:'development',
devtool:false,
entry:'./src/index.js',
output:{
filename:'[name].[hash].js',
path:path.resolve(__dirname,'dist'),
hotUpdateGlobal:'webpackHotUpdate'
},
devServer:{
hot:true,//支持热更新
port:8080,
contentBase:path.resolve(__dirname,'static')
},
plugins:[
new HtmlWebpackPlugin({
template:'./public/index.html'
}),
new HotModuleReplacementPlugin()//此处可写可不写,因为如果devServer.hot==true的话,webpack会自动帮你添加此插件
]
}
index.js:
let render = ()=>{
let title = require('./title.js');
document.getElementById('root').innerText = title;
}
render();
if(module.hot){
//当title.js模块发生修改的时候,执行render方法这个回调函数
module.hot.accept(["./title.js"],render);
}
title.js:
module.exports="title8";
注:index.js中的module.hot的module就是commonjs规范里定义的module,webpack会在这个对象上面挂hot这个属性
监听文件变化实际是调用了node的一个原生api:fs.watch
node会在内部不停的询问文件的变化
打包后的dist目录,是静态文件服务器
实现
服务端
测试脚本:node startDevServer.js
startDevServer.js:
//1.准备创建开发服务器
const webpack = require('webpack');
const config = require('./webpack.config');
const Server = require('./webpack-dev-server/lib/Server');
function startDevServer(compiler, config) {
const devServerArgs = config.devServer ||{};
//3.启动HTTP服务器,里面还会负责打包我们的项目并提供预览服务,通过它访问打包后的文件
const server = new Server(compiler,devServerArgs);
const {port=8080,host="localhost"}= devServerArgs;
server.listen(port,host,(err)=>{
console.log(`Project is running at http://${host}:${port}/`);
});
}
//2.创建complier实例
const compiler = webpack(config);
//3.启动服务HTTP服务器
startDevServer(compiler,config);
module.exports = startDevServer;
Server:
const express = require('express');
const http = require('http');
const updateCompiler = require('./utils/updateCompiler');
const webpackDevMiddleware = require('../../webpack-dev-middleware');
const io = require('socket.io');
class Server{
constructor(compiler,devServerArgs){
this.sockets = [];
this.compiler = compiler;
this.devServerArgs = devServerArgs;
updateCompiler(compiler);
this.setupHooks();//开始启动webpack的编译
this.setupApp();
this.routes();
this.setupDevMiddleware();
this.createServer();
this.createSocketServer();
}
setupDevMiddleware(){
this.middleware = webpackDevMiddleware(this.compiler);
this.app.use(this.middleware);
}
setupHooks(){
//当webpack完成一次完整的编译之后,会触发的done这个钩子的回调函数执行
//编译成功后的成果描述(modules,chunks,files,assets,entries)会放在stats里
this.compiler.hooks.done.tap('webpack-dev-server',(stats)=>{
console.log('新的一编译已经完成,新的hash值为',stats.hash);
//以后每一次新的编译成功后,都要向客户端发送最新的hash值和ok
this.sockets.forEach(socket=>{
socket.emit('hash',stats.hash);
socket.emit('ok');
});
this._stats=stats;//保存一次的stats信息
});
}
routes(){
if(this.devServerArgs.contentBase){
//此目录将会成为静态文件根目录
this.app.use(express.static(this.devServerArgs.contentBase));
}
}
setupApp(){
//this.app并不是一个http服务,它本身其实只是一个路由中间件
this.app = express();
}
createServer(){
this.server = http.createServer(this.app);
}
createSocketServer(){
//websocket通信之前要握手,握手的时候用的HTTP协议
const websocketServer = io(this.server);
//监听客户端的连接
websocketServer.on('connection',(socket)=>{
console.log('一个新的websocket客户端已经连接上来了');
//把新的客户端添加到数组里,为了以后编译成功之后广播做准备
this.sockets.push(socket);
//监控客户端断开事件
socket.on('disconnect',()=>{
let index = this.sockets.indexOf(socket);
this.sockets.splice(index,1);
});
//如果以前已经编译过了,就把上一次的hash值和ok发给客户端
if(this._stats){
socket.emit('hash',this._stats.hash);
socket.emit('ok');
}
});
}
listen(port,host,callback){
this.server.listen(port,host,callback);
}
}
module.exports = Server;
添加的中间件webpackDevMiddleware的作用就是拦截静态请求,然后去memory-fs里找对应的文件,如果找到了就返回
../../webpack-dev-middleware:
/**
* webpack开发中间件
* 1.真正的以监听模式启动webpack的编译
* 2.返回一个express中间件,用来预览我们产出的资源文件
* @param {*} compiler
*/
const MemoryFileSystem = require('memory-fs');
const fs = require('fs');
const memoryFileSystem = new MemoryFileSystem();
const middleware = require('./middleware');
function webpackDevMiddleware(compiler){
//1.真正的以监听模式启动webpack的编译
compiler.watch({},()=>{
console.log('监听到文件变化,webpack重新开始编译');
});
//产出的文件并不是写在硬盘上了,为提供性能,产出的文件是放在内存里,所以你在硬盘上看不见
//当webpack准备写入文件的时候,是用的compiler.outputFileSystem来写入
//let fs = compiler.outputFileSystem = memoryFileSystem;
return middleware({
fs,
outputPath:compiler.options.output.path//写入到哪个目录里去
});
}
module.exports = webpackDevMiddleware;
./middleware:
/**
* 这个express中间件负责提供产出文件的预览
* 拦截HTTP请求,看看请求的文件是不是webpack打包出来的文件。
* 如果是的话,从硬盘上读出来,返回给客户端
*/
let mime = require('mime')
let path = require('path')
function wrapper({fs,outputPath}){
return (req,res,next)=>{
let url = req.url;//http://localhost:9000/main.js
if(req.url === '/favicon.ico') return res.sendStatus(404);
if(url === '/') url = "/index.html";
//outputPath = path.resolve(__dirname,'dist')
//url =/main.js
//filename = C:\aproject\zhufengwebpack202011\10.hmr9000\dist/main.js
let filename = path.join(outputPath,url);
console.log(filename);
try{
let stat = fs.statSync(filename);
if(stat.isFile()){
let content = fs.readFileSync(filename);
//main.js=>application/javascript main.jpe=>image/jpeg
res.setHeader('Content-Type',mime.getType(filename));
return res.send(content);
}else{
return res.sendStatus(404);
}
}catch(error){
console.log(error);
return next(error);
}
}
}
module.exports = wrapper;
开启热更新的情况下,服务端还会给webpack.config文件中的entry里面添加两个入口:
const path = require('path');
function updateCompiler(compiler){
const options = compiler.options;
//1来自于webpack-dev-server/client/index.js 它就是浏览里的websocket客户端
options.entry.main.import.unshift(
require.resolve('../../client/index.js')
);
//2.webpack/hot/dev-server.js 它用来在浏览器里监听发身的事件,进行后续热更新逻辑
options.entry.main.import.unshift(
require.resolve('../../../webpack/hot/dev-server.js')
);
console.log(compiler.entry);
//把入口变更之后,你得通知webpack按新的入口进行编译
compiler.hooks.entryOption.call(options.context,options.entry);
}
module.exports = updateCompiler;
/**
* webpack4
* entry:{
* main:['./src/index.js']
* }
* webpack5
* entry:{
* main:{
* import:['webpack/hot/dev-server.js','webpack-dev-server/client/index.js','./src/index.js']
* }
* }
*
*/
客户端
HotModuleReplacementPlugin为每个模块添加了hot属性
hotCreateRequire为每个模块添加了parents和children属性、check和accept方法
客户端打包时有以下几个入口:
webpack-dev-server/client/index.js
webpack/hot/dev-server.js
./src/index.js
这几个入口最终都会打包到main.js中去
我们以hmr.html和hmr.js两个文件为例,来说明客户端的运行原理
实际环境中,hmr.js就相当于打包出来的合并了3个入口的main.js
hmr.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HMR</title>
</head>
<body>
<input/>
<div id="root"></div>
<script src="/socket.io/socket.io.js"></script>
<script src="hmr.js"></script></body>
</html>
引入/socket.io/socket.io.js之后,就会在window上添加了io方法,在js中就可以使用了
webpack-dev-server/client/index.js:
var hotEmitter = require('../../webpack/hot/emitter');
//通过websocket客户端连接服务器端
var socket = io();
//当前最新的hash值
var currentHash;
socket.on('hash',(hash)=>{
console.log('客户端据此到hash消息');
currentHash = hash;
});
socket.on('ok',()=>{
console.log('客户端据此到ok消息');
reloadApp();
});
function reloadApp(){
hotEmitter.emit('webpackHotUpdate',currentHash);
}
webpackHotUpdate事件在webpack/hot/dev-server.js文件中监听:
var hotEmitter = require('../../webpack/hot/emitter');
hotEmitter.on('webpackHotUpdate',(currentHash)=>{
console.log('dev-server收到了最新的hash值',currentHash);
//进行真正的热更新检查
hotCheck();
});
HotModuleReplacementPlugin插件给每个模块添加hot属性、
hotCreateRequire为每个模块添加了parents和children属性、check和accept方法,都体现在打包后的main.js里面,我们此处用hmr.js来模拟一下打包后的main.js
hotCheck是HotModuleReplacementPlugin里定义的吗?(待确认)
function hotCheck(){
console.log('开始进行热更新的检查!');
hotDownloadManifest().then(update=>{
update.c.forEach(chunkId=>{
hotDownloadUpdateChunk(chunkId);
});
lastHash = currentHash;
}).catch(()=>{
window.location.reload();
});
}
function hotDownloadManifest(){
//webpack4 ajax webpack5 fetch
return fetch(`main.${lastHash}.hot-update.json`).then(res=>res.json())
}
function hotDownloadUpdateChunk(chunkId){
let script = document.createElement('script');
script.src = `${chunkId}.${lastHash}.hot-update.js`;
document.head.appendChild(script);
}
下载下来的.hot-update.js文件大概是这样:
self["webpackHotUpdate"]("main",{
/***/ "./src/title.js":
/*!**********************!*\
!*** ./src/title.js ***!
**********************/
/***/ ((module) => {
module.exports="title5";
/***/ })
},
/******/ function(__webpack_require__) { // webpackRuntimeModules
/******/ "use strict";
/******/
/******/ /* webpack/runtime/getFullHash */
/******/ (() => {
/******/ __webpack_require__.h = () => "0805f33576e78cb08802"
/******/ })();
/******/
/******/ }
);
self["webpackHotUpdate"]方法定义如下:
self["webpackHotUpdate"]= function(chunkId,moreModules){
hotAddUpdateChunk(chunkId,moreModules);
}
let hotUpdate = {};
function hotAddUpdateChunk(chunkId,moreModules){
for(var moduleId in moreModules){
//合并到模块定义对象里
hotUpdate[moduleId]= modules[moduleId]=moreModules[moduleId];
}
hotApply();
}
function hotApply(){
for(let moduleId in hotUpdate){
let oldModule = cache[moduleId];//获取到老的模块 module parents children
delete cache[moduleId]//得把老的缓存删除,不然再加载还会读到老模块
//会不会有模块没有parents,入口模块就没有父亲. webpack5模块联邦
if(oldModule.parents && oldModule.parents.size>0){
let parents = oldModule.parents;
parents.forEach(father=>{
father.hot.check(moduleId);
});
}
}
}
function hotCreateModule(){
let hot = {
_acceptedDependencies:{},//接收的依赖对象
accept(deps,callback){//接收依赖的变化 注册各模块的回调函数
for(let i=0;i<deps.length;i++){
hot._acceptedDependencies[deps[i]] = callback;
}
},
check(moduleId){
let callback = hot._acceptedDependencies[moduleId];
callback&&callback();
}
}
return hot;
}
\