美文网首页
Webpack 设计理念

Webpack 设计理念

作者: BingJS | 来源:发表于2023-03-22 18:38 被阅读0次

    一、前言

    Webpack 一直都是有些人的心魔,不清楚原理是什么,不知道怎么去配置,只会基本的 API 使用。它就像一个黑盒,让部分开发者对它望而生畏。
    大家之所以认为 Webpack 复杂,很大程度上是因为它依附着一套庞大的生态系统。其实 Webpack 的核心流程远没有我们想象中那么复杂,甚至只需百来行代码就能完整复刻出来。
    因此在学习过程中,我们应注重学习它本身的设计思想,不管是它的 Plugin 系统还是 Loader 系统,都是建立于这套核心思想之上。所谓万变不离其宗,一通百通。

    二、基本使用

    初始化项目:

    npm init  //初始化一个项目
    yarn add webpack //安装项目依赖
    

    安装完依赖后,根据以下目录结构来添加对应的目录和文件:

    ├── node_modules
    ├── package-lock.json
    ├── package.json
    ├── webpack.config.js #配置文件
    ├── debugger.js #测试文件
    └── src # 源码目录
         |── index.js
         |── name.js
         └── age.js
    

    webpack.config.js

    const path = require("path");
    module.exports = {
      mode: "development", //防止代码被压缩
      entry: "./src/index.js", //入口文件
      output: {
        path: path.resolve(__dirname, "dist"),
        filename: "[name].js",
      },
      devtool: "source-map", //防止干扰源文件
    }; 
    

    src/index.js

    const name = require("./name");
    const age = require("./age");
    console.log("entry文件打印作者信息", name, age);
    

    src/name.js

    module.exports = "不要秃头啊";
    

    src/age.js

    module.exports = "99";
    

    文件依赖关系:
    入口文件 src/index.js
    依赖文件 src/name.js src/age.js
    Webpack 本质上是一个函数,它接受一个配置信息作为参数,执行后返回一个 compiler 对象,调用 compiler 对象中的 run 方法就会启动编译。run 方法接受一个回调,可以用来查看编译过程中的错误信息或编译信息。
    debugger.js

    // const { webpack } = require("./webpack.js"); //后面自己手写
    const { webpack } = require("webpack");
    const webpackOptions = require("./webpack.config.js");
    const compiler = webpack(webpackOptions);
    
    //开始编译
    compiler.run((err, stats) => {
      console.log(err);
      console.log(
        stats.toJson({
          assets: true, //打印本次编译产出的资源
          chunks: true, //打印本次编译产出的代码块
          modules: true, //打印本次编译产出的模块
        })
      );
    });
    

    执行打包命令:

    node ./debugger.js
    

    得到产出文件 dist/main.js
    运行该文件,得到结果:

    entry文件打印作者信息 不要秃头啊 99
    

    三、核心思想

    我们先来分析一下源代码和构建产物之间的关系:
    入口文件(src/index.js)被包裹在最后的立即执行函数中,而它所依赖的模块(src/name.js、src/age.js)则被放进了 modules 对象中(modules 用于存放入口文件的依赖模块,key 值为依赖模块路径,value 值为依赖模块源代码)。
    require 函数是 web 环境下 加载模块的方法( require 原本是 node环境 中内置的方法,浏览器并不认识 require,所以这里需要手动实现一下),它接受模块的路径为参数,返回模块导出的内容。
    要想弄清楚 Webpack 原理,那么核心问题就变成了:如何将左边的源代码转换成 dist/main.js 文件?

    核心思想:

    • 第一步:首先,根据配置信息(webpack.config.js)找到入口文件(src/index.js)
    • 第二步:找到入口文件所依赖的模块,并收集关键信息:比如路径、源代码、它所依赖的模块等:
    var modules = [
    {
      id: "./src/name.js",//路径
      dependencies: [], //所依赖的模块
      source: 'module.exports = "不要秃头啊";', //源代码
    },
    {
      id: "./src/age.js",
      dependencies: [], 
      source: 'module.exports = "99";',
    },
    {
      id: "./src/index.js",
      dependencies: ["./src/name.js", "./src/age.js"], 
      source:
        'const name = require("./src/name.js");\n' +
        'const age = require("./src/age.js");\n' +
        'console.log("entry文件打印作者信息", name, age);',
    },
    ];
    
    • 第三步:根据上一步得到的信息,生成最终输出到硬盘中的文件(dist): 包括 modules 对象、require 模版代码、入口执行文件等

    在这过程中,由于浏览器并不认识除 html、js、css 以外的文件格式,所以我们还需要对源文件进行转换 —— Loader 系统
    Loader 系统 本质上就是接收资源文件,并对其进行转换,最终输出转换后的文件:
    除此之外,打包过程中也有一些特定的时机需要处理,比如:

    • 在打包前需要校验用户传过来的参数,判断格式是否符合要求
    • 在打包过程中,需要知道哪些模块可以忽略编译,直接引用 cdn 链接
    • 在编译完成后,需要将输出的内容插入到 html 文件中
    • 在输出到硬盘前,需要先清空 dist 文件夹

    这个时候需要一个可插拔的设计,方便给社区提供可扩展的接口 —— Plugin 系统
    Plugin 系统 本质上就是一种事件流的机制,到了固定的时间节点就广播特定的事件,用户可以在事件内执行特定的逻辑,类似于生命周期:
    打包前的生命周期 => 打包过程中的生命周期 => 打包成功的生命周期 => 打包失败的生命周期
    这些设计也都是根据使用场景来的,只有理清需求后我们才能更好的理解它的设计思想。

    四、架构设计

    在理清楚核心思想后,剩下的就是对其进行一步步拆解。
    上面提到,我们需要建立一套事件流的机制来管控整个打包过程,大致可以分为三个阶段:

    • 打包开始前的准备工作
    • 打包过程中(也就是编译阶段)
    • 打包结束后(包含打包成功和打包失败)

    这其中又以编译阶段最为复杂,另外还考虑到一个场景:watch mode(当文件变化时,将重新进行编译),因此这里最好将编译阶段(也就是下文中的compilation)单独解耦出来。

    在 Webpack 源码中,compiler 就像是一个大管家,它就代表上面说的三个阶段,在它上面挂载着各种生命周期函数,而 compilation 就像专管伙食的厨师,专门负责编译相关的工作,也就是打包过程中这个阶段。画个图帮助大家理解:

    相关文章

      网友评论

          本文标题:Webpack 设计理念

          本文链接:https://www.haomeiwen.com/subject/pmkphdtx.html