webpack 的理解

模块化

  • webpack 的最初目标是实现前端项目的模块化,旨在更加高效的管理和维护项目

  • 最早的时候,我们也会通过文件划分来实现模块化开发,也就是将每个功能点一起相关的数据各自单独放到不同的 js 文件中

  • 约定么每一个文件就是单独的模块,然后将 js 文件通过 script 标签注入到 html 网页中,实现管理模块化开发的实现

  • 但是这种模块化开发的弊端就是模块全部是在全局环境中进行工作,大量模块成员会污染环境,导致模块和模块之间没有明确依赖关系存在,维护是十分的困难的,没有解决私有空间的问题

  • 以及社区内部的解决方案有

    • 命名空间解决模块问题

    • 立即执行函数解决模块问题

问题

  • 从后端渲染的 JSP PHP 到前端原生的 javascript 再到 jquery 时期,再到三大框架的开发时期

  • 开发方式从 javascript 的 es5 到后续的 es6 再到 typescript ,以及css 方案的层出不穷的方案迭代

  • 此时前端的开发也变得越来越复杂了

    • 需要同故宫模块化的方式来进行开发

    • 使用一些高级的特性来加快开发效率以及安全性等问题,比如 es6+ 、typescript 开发脚本逻辑,通过 sass less 方式来编写css

    • 监听文件的变化来相应给浏览器,提高开发效率

    • javascript 的代码需要模块化,html 和 css 这些资源文件也会面领着模块化的问题

    • 开发完成后进行代码的 splitchunk,terser 实现压缩合并以及优化,以及代码混淆

  • 此时 webpack 就可以解决这样的问题讷

是什么

  • webpack 是一个用于构建现代化的 javascript 的应用程序的静态模块工具

  • 静态模块

    • 这些静态模块指的是开发阶段,他们可以被 webpack 直接应用得资源

    • 当 webpack 处理应用程序的时候,他会在内部构建一个依赖图,此依赖图对应映射到项目的各个模块,并生成或者多个 bundle 吧

      • 编译代码的能力

      • 模块整合的能力

      • 万物皆模块的原理

webpack 的 hmr

是什么

  • HMR 全称是 Hot Module Replacement 热模块替换,旨在应用程序运行过程中,替代、添加、删除模块,无需重新刷新整个应用

  • 使用 hmr 的技术可以实现解决的是应用在运行过程中修改了某个模块,通过自动刷新会导致整个应用整体刷新,那么页面的状态信息就会丢失

  • 但是如果使用 HMR 的话,那么就可以实现的是只将修改了的模块进行实时性的替换到应用中即可吧,不必刷新整个应用讷

实现原理

  • webpack compile 将 js 源代码编译成具体的 bundle 模块

  • HMR server 用来将 热更新的模块输出给 HMR 的 runtime

  • Bundle Server 静态资源文件服务器,提供文件的访问路径

  • HMR Runtime 创建一个 socket 服务器,会被注入到浏览器,更新文件的变化吧

  • bundle 构建输出的文件

  • 在 HMR Runtime 和 HMR Server 之间建立一个 websocket

大致的阶段是

  • 启动阶段的实现

    • 在编写未经过 webpack 打包源代码后,webpack compile 将源代码和 HMR Runtime 一起编译为 bundle 文件,输出给 Bundle Server 静态资源服务器

  • 更新阶段的实现

    • 当一个文件或者模块发生变化的时候,webpack 监听到文件变化后,对文件重新编译打包,编译生成唯一的 hash 值,这个 hash 值就会被用来标记为下一次文件热更新的标识

    • 根据文件的内容变化,生成两个补丁文件 manifest (这里面包含了 hash 和 chundId,用来说明文件内容)和 chunk.js 模块吧

    • 由于 socket 服务器在 HMR Runtime 和 HMR Server 之间建立了 websocket 的连接,当文件发生变动的时候,服务端会像浏览器发送一条消息,消息包含了文件改动后的 hash 值

总结

  • 关于 webpack 的热模块更新来说的话

    • 通过 webpack-dev-server 创建两个服务器,提供静态资源的服务器Bundle-Server(express)和 socket 服务

    • express server 负责的是直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析实现吧)

    • socket server 是一个 websocket 的长连接,浏览器和本地可以实现双工通信

    • 当 socket server 监听到了文件模块内容发生了变化后,会生成两个 json 文件 (manifest文件)和 js 文件(update chunk)

    • 通过长连接,socket server 可以直接将这两个魔魁啊主动发送给客户端

    • 浏览器拿到了这两个新的文件后,通过 HMR Runtime 机制,实现加载这两个文件吧,并且针对修改了的模块进行更新实现

webpack 构建流程

  • webpack 的构建流程是一个串行的过程吧,它的工作流程就是将每一个插件串联起来

  • 在运行过程中会使用广播事件,插件只需要监听他们所关心的事件,就能加入到这条 webpack 的机制中了吧

  • 核心是三大阶段

    • 初始化阶段:从配置文件和 shell 语句开发读取和合并参数,并初始化需要使用的插件和配置插件,等执行环境所需要的参数吧

    • 编译构建阶段:从 entry 开发,针对每个 Module 串行调用对应的 loader 去编译文件内容,再找到 Module 依赖,递归的进行编译处理吧

    • 输出流程:对编译后的 Module 组合成 chunk ,把 chunk 转化为文件,输出到文件系统中

初始化阶段

  • 从配置文件和 shell 语句读取并且合并参数,最终得到参数

  • 配置文件默认下为 webpack.config.js ,也或者通过命令的形式指定配置文件,主要的作用是用户激活 webpack 架子啊项和插件

const path = require("node:path");
/**
 * resolve 实现返回路径是绝对路径吧
 */
const node_modules = path.resolve(__dirname, "node_modules");
/**
 * 获取得到react的绝对路径
 */
const pathToReact = path.resolve(node_modules, "react/dist/react.min.js");
module.exports = {
    // 指定编译的入口文件
    entry: path.resolve(__dirname, "index.js"),
    resolve: {
        alias: {
            "react": pathToReact,
            "react-dom": path.resolve(node_modules, "react-dom/dist/react-dom.min.js"),
            "lodash": path.resolve(node_modules, "lodash/lodash.min.js"),
            "@": path.resolve(__dirname, "src"),
        }
    },
    output: {
        path: path.resolve(__dirname, "dist"),
        filename: "bundle.js",
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                use: ["babel-loader"],
                exclude: /node_modules/,
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname, "index.html"),
        }),
    ],
}
  • webpack 将 webpack.config.js 中的每个配置拷贝合并到 webpack 需要的上下文的 options 对象中,并且实现加载用户配置的 plugins

  • 完成上述的描述后,就开始初始化 compiler 编译对象,该对象掌控了 webpack 的声明周期,不执行具体的任务,只是执行一些调度工作

/**
 * 实现 webpack 编译的调度器吧
 */
class Compiler extends Tapable {
    // 核心工作是:进行解析初始化对象以及进行注册具体的钩子函数的功能吧
    // 整体是基于 taptable 来实现的讷
    constructor(context) {
        super();

        // 注册具体的钩子函数吧
        this.hooks = {
            /**
             * 编译前的钩子函数
             */
            beforeCompile: new AsyncSeriesHook(["params"]),
            /**
             * 编译函数
             */
            compile: new SyncHook(["params"]),
            /**
             * 编译后的钩子函数
             */
            afterCompile: new AsyncSeriesHook(["params"]),
            /**
             * 编译函数
             */
            make: new AsyncParallelHook(["compilation"]),
            /**
             * 入口文件的选项
             */
            entryOptions: new SyncBailHook(["compilation"]),
        }
    }
}

function webpack(options) {
    const compiler = new Compiler(options);
    return compiler;
}

编译构建阶段

  • 根据配置中的 entry 开始找到编译的入口文件

  • 初始完成后会调用 Compiler 和 run 来实现真真的启动 webpack 的编译构建流程

    • compile 开始编译

    • make 从入口点分析模块以及依赖的模块,创建这些模块对象

    • build-module 进行构建模块

    • seal 封装出构建结果

    • emit 把各个模块chunk输出到结果文件吧

compile 阶段

  • 执行 run 方法,首次会触发 compile ,主要是用于构建一个 compilation 对象吧

  • 该对象是编译阶段的主要执行者,核心和一次执行:执行模块创建、依赖收集、分块、打包等主要工作对象

make 阶段

  • 当完成了上面的 compilation 对象后,就开始从 entry 开始读取配置,主要执行的是 _addModuleChain 函数来实现吧

build-module 阶段完成编译

主要是调用配置的 loaders 来实现对应的功能吧,将我们的模块转化为标准的 js 进行输出讷

内部核心使用的 babel 或者 acorn 等这样的 AST 抽象语法树来实现的讷

输出流程

seal 输出资源
  • 主要是生成的是一个一个的 chunk 吧,对 chunks 进行优化操作,并且生成要输出的代码

  • webpack 中的 chunk 可以理解为配置在 entry 中的模块吧,或者是动态引入的模块

  • 根据入口和模块之间的依赖关系,组装成一个包含了多个模块的 chunk ,再把每个模块 chunk 转化为单独的文件加载到输出列表中吧

emit 阶段
  • 就是根据具体的 options 配置实现对应的输出特定格式的资源的阶段吧

webpack proxy 原理

是什么讷

  • 是 webpack 在本地开启的一个代理服务器吧

  • 基本的实现思路就是将从客户端接受到的请求转发给其他服务器的功能

  • 核心解决的痛点就是解决开发阶段出现浏览器的同源策略和跨域问题的讷

  • 想要实现代理首先是需要一个中间服务器,webpack 提供了服务器工具 webpack-dev-server

webpack-dev-server

  • 核心实现的是资源的自动编译和自动刷新浏览器功能的实现讷

  • 目的是为了提高开发效率,在生产环境下我们是需要进行结合代理服务器或者请求配置来实现的讷:nginx 来做请求的转化,负载均衡的实现吧

底层原理

  • 基于 http-proxy-middleware 来实现的是这个代理服务器的讷,这是一个基于 http 模块封装的一个 middleware 吧,实现的是请求的转化给其他的服务器的实现讷

const express = require("express");
const { createProxyMiddleware } = require('http-proxy-middleware');

const app = express();
const port = 3001;

app.use('/api', createProxyMiddleware({
    target: 'http://localhost:3000',
    changeOrigin: true,
}))

app.listen(port, () => {
    console.log(`Example app listening at http://localhost:${port}`);
})

webpack 提高构建速度

优化手段

  • 优化 loader 配置:babel --> swc

  • 合理使用 resolve.extension

  • 优化 resolve.modules

  • 优化 resolve.alias

  • 使用 DLLPlugin 动态链接库 dynamic link library

  • 使用 cache-loader 缓存

  • terser 多线程

  • 合理使用开发阶段的 sourceMap 特性

webpack 前端优化

优化手段

  • JS代码压缩

    • 通过 terser 来实现 javascript 的压缩

    • 核心插件是 TerserWebpackPlugin + 自定义 optimization 字段实现吧

  • CSS 代码压缩

    • css-minimizer-webpack-plugin 实现压缩实现吧

    • 也是需要在生产环境下和 optimization 结合使用的讷

  • HTML 文件代码压缩

    • HtmlWebpackPlugin 插件来实现 html 的模板压缩实现吧

  • 文件大小压缩

    • compress-webpack-plugin 来实现吧

  • 图片压缩

    • image-webpack-loader 来实现吧

  • Tree Shaking

    • 代码摇树,删除死代码的方案吧,基于 EesModule 的静态此法分析来实现的呢,可以实现在不执行任何代码的情况下明确知道依赖的关系

    • 也是自定义配置 optimization 字段来实现的讷

      • usedExports

      • sideEffects

      • css-tree-shaking: purgecss-plugin-webpack

  • 代码分离

    • splitchunk 来实现吧

  • 内联 chunk 实现

webpack 类似的生态

模块化工具

  • rollup

  • tsup

  • esbuild

  • vite

    • 核心构成

      • 一个开发服务器 nodejs:基于原生的 es 模块提供丰富的内建功能,速度十分惊人

      • 一套构建指令

    • 优势

      • 快速启动

      • 即时的模块热更新

      • 按需编译实现

  • rspack