webpack必知必会源码篇(HMR热更新)(二)

78 阅读4分钟

webpack配置

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    mode:'development',
    devtool:false,
    //entry里可以配置多个入门,每个入口有一个名称 默认就是main
    //从入门文件出现进行编译,找到它依赖的模块,打包在一起.就会成一个chunk 代块码
    entry:'./src/index.js',
    output:{
        filename:'main.js',
        path:path.resolve(__dirname,'dist')
    },
    devServer:{
        hot:true,
        contentBase:path.resolve(__dirname,'dist')
    },
    plugins:[
        new HtmlWebpackPlugin(),//用来产出一个html文件,往里面自动插件生成的脚本
        new webpack.HotModuleReplacementPlugin()
    ]
}

hmr热更新原理

const webpack = require('webpack');
//配置对象
const config = require('../webpack.config');
const Server = require('./lib/server/Server');
//编译器对象
const compiler = webpack(config);
const server = new Server(compiler);
server.listen(9090,'localhost',()=>{
    console.log('服务已经在9090端口上使用!');
});

utils/updateCompiler


/**
 * 为了实现客户端跟服务器端通信,需要往入口里多注入两个文件
 * (webpack)-dev-server/client/index.js
 * (webpack)/hot/dev-server.js webpack源码
 * ./src/index.js 
 * @param {*} compiler 
 */
const path = require('path');
function updateCompiler(compiler){
  const config = compiler.options;
  config.entry = {
      main:[
        //4.修改了这二个脚本路径
        path.resolve(__dirname,'../../client/index.js'),
        path.resolve(__dirname,'../../../webpack/hot/dev-server.js'),
        config.entry  //./src/index.js
      ]
  }
}
module.exports = updateCompiler;

dev-serve.js

/**
 * 存二个hash值,一个是上一个hash,一个是当前的hash
 * 
 */
let lastHash;
hotEmitter.on('webpackHotUpdate',()=>{
    console.log('hotCheck');
});

client


//1.连接websocket服务器
///socket.io/socket.io.js window.io赋值
let currentHash ;
class EventEmitter{//events
    constructor(){
        this.events = {};
    }
    on(eventName,fn){
        this.events[eventName]=fn;
    }
    emit(eventName,...args){
        this.events[eventName](...args);
    }
}
let hotEmitter = new EventEmitter();
const socket = window.io('/');
//监听hash事件,保存此hash值
socket.on('hash',(hash)=>{
    currentHash=hash;
});
socket.on('ok',()=>{
    console.log('ok');
    reloadApp();
});

function reloadApp(){
    hotEmitter.emit('webpackHotUpdate');
}
const express = require('express');
const http = require('http');
const path = require('path');
//2.原来用的是内存文件系统,暂时先改成硬盘文件系统fs
const fs = require('fs-extra');
fs.join = path.join;
const mime  = require('mime');
//1.修改用socket.io替换掉socket-io
const socketIO = require('socket.io');
//3.修改了的updateCompiler位置
const updateCompiler = require('../utils/updateCompiler');
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');//给客户发一个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();//返回一个express中间件
  }
  webpackDevMiddleware(){
    let {compiler} = this;
    //以监听模式启动编译,如果以后文件发生变更了,会重装编译
    compiler.watch({},()=>{
        console.log('监听模式编译成功');//重新编译 执行compiler.run方法
    });
    //以后打包后文件写入内存文件系统,读的时候也要从内存文件系统里读
    this.fs = compiler.outputFileSystem = fs;
    //返回一个中间件,用来响应客户端对于产出文件的请求 index.html main.js .json .js
    return (staticDir)=>{//静态文件根目录,它其实就是输出目录 dist目录
        return (req,res,next)=>{
            let {url} = req;//得到请求路径
            if(url === '/favicon.ico'){
                return res.sendStatus(404);
            }
            url === '/'?url = '/index.html':null;
            //得到要访问的静态路径  /index.html /main.js
            let filePath = path.join(staticDir,url);
            //filePath C:\aproject\zhufeng_webpack_hmr_2020-ok\dist\index.html
            //C:\aproject\zhufeng_webpack_hmr_2020-ok\dist\main.js
            try{
                //返回此路径上的文件的描述对象,如果此文件不存在,会抛异常
                let statObj = this.fs.statSync(filePath);
                console.log('statObj',statObj);
                if(statObj.isFile()){
                    let content = this.fs.readFileSync(filePath);//读取文件内容
                    res.setHeader('Content-Type',mime.getType(filePath));//设置响应头告诉 此浏览器此文件内容是什么?
                    res.send(content);//把内容发送给浏览器
                }else{
                    return res.sendStatus(404);
                }
            }catch(error){
                console.log(error);
                return res.sendStatus(404);
            }
        }
    }
  }
  setupHooks(){
    let {compiler} = this;
    //监听编译完成事件,当编译完成之后会调用此钩子函数
    compiler.hooks.done.tap('webpack-dev-server',(stats)=>{
        //stats是一个描述对象,里面放着打包后的结果hash chunkHash contentHash 产生了哪些代码块 产出哪些模块
        console.log('hash',stats.hash);
        this.currentHash = stats.hash;
        //会向所有的客户进行广播 ,告诉客户我已经编译成功了,新的模 块代码已经生成,快来拉我的新代码啊
        this.clientSocketList.forEach(socket=>{
            socket.emit('hash',this.currentHash);//把最新的hash值 发给客户端
            socket.emit('ok');//给客户发一个ok
        });
    });
  }
  setupApp(){
      this.app = express();//执行express函数得到this.app  代表http应用对象
  }
  createServer(){
      //通过http模 块创建一个普通的http服务器
      //this.app是一个路由中间件
      this.server = http.createServer(this.app);
  }
  listen(port,host,callback){
    this.server.listen(port,host,callback);
  }
}
module.exports = Server;