webpack4之原理分析

1,247 阅读7分钟

webpack本质:理解为是一种基于事件流的编程范例,一系列的插件运行

往期文章:

命令行

  • 通过 npm scripts 运行 webpack
    • 开发环境 npm run dev
    • 生产环境 npm run build
  • 通过 wepback直接运行
    • webpack entry.js bundle.js

这个过程发生了什么

运行命令后 npm让命令行工具进入node_modules/.bin目录查找是否存在webpack.sh或者webpack.cmd文件 如果存在,则执行,不存在,抛出错误(node_modules/wepback/bin/wepback.js)

启动后的结果:wepback最终找到wepback-cli(webpack-command)包,并且执行cli

// 正常执行返回
process.exitCode = 0; 
// 运行某个命令
const runCommand = (command, args) => {
  const cp = require("child_process");
  return new Promise((resolve, reject) => {
        const executedCommand = cp.spawn(command, args, {});
        executedCommand.on("error", error => reject(error););
    // code 为 0 则说明成功,resolve,否则reject
        executedCommand.on("exit", code => {})
    });
} 
// 判断某个包是否安装
onst isInstalled = packageName => {
    try {
        require.resolve(packageName);
        return true;
    } catch (err) {
        return false;
    }
};
// wepback 可用的 cli:webpck-cli和webpack-command
const installedClis = CLIs.filter(cli => cli.installed)
// 判断两个cli是否安装,根据安装数量处理
if (installedClis.length === 0) {} 
else if (installedClis.length === 1) {}
else {}

webpack-cli

  • 引入 yargs,对命令行进行定制
  • 分析命令行参数,对各个参数进行转换,组成编译配置项
  • 引用webpack,根据配置项进行编译和构建
// wepback-cli处理不需要经过编译的命令
const NON_COMPILATION_ARGS = [
    "init", // 创建一份webpack配置文件
    "migrate", // 进行webpack版本迁移
    "add", // 往webpack配置文件中增加属性
    "remove", // 从webpack配置文件中删除属性
    "serve", // 运行webpack-serve
    "generate-loader", // 生成webpack loader 代码
    "generate-plugin", //  生成webpack plugins 代码
    "info" // 返回与本地环境相关的一些信息
];
const NON_COMPILATION_CMD = process.argv.find(arg => {
    if (arg === "serve") {
        global.process.argv = global.process.argv.filter(a => a !== "serve");
        process.argv = global.process.argv;
    }
    return NON_COMPILATION_ARGS.find(a => a === arg);
});
if (NON_COMPILATION_CMD) {
    return require("./prompt-command")(NON_COMPILATION_CMD, ...process.argv);
}
// 通过yargs,提供命令和分组参数,动态生成help帮助信息
const yargs = require("yargs").usage(`webpack-cli ${
    require("../package.json").version
}
// 将输入的命令传递给config-yargs
require("./config-yargs")(yargs);
// 对命令行参数进行解析
yargs.parse(process.argv.slice(2), (err, argv, output) => {}
// 生成 options webpack参数配置对象
let options = require("./convert-argv")(argv);
// 将参数设置对象交给webpack执行
let compiler = webpack(options);
  • webpack-cli 使用 args 分析,参数分组,将命令划分为9类:

    • Config options: 配置相关参数(文件名称、运行环境)
    • Basic options: 基础参数(entry、debug、watch、devtool)
    • Module options: 模块参数,给loader设置扩展
    • Output options: 输出参数(输出路径、输出文件名称)
    • Advanced options: 高级用法(记录设置、缓存设置、监听频率、bail等)
    • Resolving options: 解析参数(alias和解析的文件后缀设置)
    • Optimizing options: 优化参数
    • Stats options: 统计参数
    • options: 通用参数(帮助命令、版本信息)
  • webpack-cli执行结果

    • webpack-cli对配置文件和命令行参数进行转换最终生成配置选项参数options,最终会根据配置参数实例花webpack对象,然后交给webpack执行构建流程(complier)

Tapable插件架构和Hooks设计

  • compiler extends Tapable -> compilation extends Tapable
  • Tapable 是一个类似Nodejs的EventEmitter的事件库,主要控制钩子函数的发布与订阅,控制着webpack插件系统,Tapable暴露了很多Hook(钩子)类,为插件提供挂载的钩子
    • SyncHook: 同步钩子
    • SyncBailHook: 同步熔断钩子
    • SyncWaterfallHook: 同步流水钩子
    • SyncLoopHook: 同步循环钩子
    • AsyncParallelHook: 异步并发钩子
    • AsyncParallelBailHook: 异步并发熔断钩子
    • AsyncSeriesHook: 异步串行钩子
    • AsyncSeriesBailHook: 异步串行熔断钩子
    • AsyncSeriesWaterfallHokk: 异步穿行流水钩子
  • Tapable Hooks 类型
    • Hook:所有钩子的后缀
    • Waterfall:同步方法,但是它会传值给下一个汉顺
    • Bail:熔断:当函数有任何返回值,就会在当前执行函数停止
    • Loop:监听函数返回true表示继续循环,返回undefined表示结束循环
    • Sync:同步方案
    • AsyncSeries:异步串行钩子
    • AsyncParallel:异步并发执行钩子
  • Tapable暴露出来的都是类方法,new一个类方法获得我们需要的钩子
    • class接受数组参数options,非必传,类方法会根据传参,接受同样数量的参数
    • 绑定/订阅:
      • 异步:tapAsync/tabPromise/tap
      • 同步:tap
    • 执行/发布:
      • 异步:callAsync/promise
      • 同步:call
// 创建钩子
const hook = new SyncHook(['arg1', 'arg2', 'arg3'])
// 绑定事件到webpack事件流
hook.tap('hook1', (arg1, arg2, arg3) => {console.log(arg1, arg2, arg3)})
// 执行
hook.call(1, 2, 3);// 1, 2, 3

具体Tapable使用请查看 轻松搞定Tapable

Tapable与webpack联系起来

if (Array.isArray(options)) {
        compiler = new MultiCompiler(options.map(options => webpack(options)));
    } else if (typeof options === "object") {
        options = new WebpackOptionsDefaulter().process(options);
        compiler = new Compiler(options.context);
        compiler.options = options;
        new NodeEnvironmentPlugin().apply(compiler);
        if (options.plugins && Array.isArray(options.plugins)) {
            for (const plugin of options.plugins) {
                plugin.apply(compiler);
            }
        }
        compiler.hooks.environment.call(); // hook了
        compiler.hooks.afterEnvironment.call();
        compiler.options = new WebpackOptionsApply().process(options, compiler);
    } else {
        throw new Error("Invalid argument: options");
    }
    if (callback) {
        // ....
        compiler.run(callback);
    }
    return compiler;
};

webpack整体流程

流程图

过程分析

  • webpack编译按照钩子调用顺序执行
  • webbpack 本质上就是一个 JS Module Bundler,用于将多个代码模块进行打包。bundler 从一个构建入口出发,解析代码,分析出代码模块依赖关系,然后将依赖的代码模块组合在一起,在JavaScriptbundler中,还需要提供一些胶水代码让多个代码模块可以协同工作,相互引用
  • 分析出依赖关系后,webpack 会利用JavaScript Function的特性提供一些代码来将各个模块整合到一起,即是将每一个模块包装成一个JS Function,提供一个引用依赖模块的方法,如下面例子中的__webpack__require__,这样做,既可以避免变量相互干扰,又能够有效控制执行顺序
// 分别将各个依赖模块的代码⽤ modules 的⽅式组织起来打包成⼀个⽂件
================================entry======================================
// entry.js 
import { bar } from './bar.js'; // 依赖 ./bar.js 模块 
// bar.js 
const foo = require('./foo.js'); // 依赖 ./foo.js 模块
递归下去,直至没有更多的依赖模块,最终形成一颗模块依赖树
 ================================moudles======================================
 // entry.js
 modules['./entry.js'] = function() {
    const { bar } = __webpack__require__('./bar.js')
 }
 // bar.js
 modules['./bar.js'] = function() {
    const foo = __webpack__require__('./foo.js')
 };
 // foo.js
 modules['./foo.js'] = function() {
    // ... 
 }
================================output===========================
// 已经执⾏的代码模块结果会保存在这⾥
(function(modules){
    const installedModules = {}
    function __webpack__require__(id) {
        // 如果 installedModules 中有就直接获取
        // 没有的话从 modules 中获取 function 然后执⾏,
        //将结果缓存在 installedModules 中然后返回结果
    }
})({
    "./entry.js": (function(__webpack_require__){
        var bar = __webpack_require__(/*code内容*/)
    }),
    "./bar.js": (function(){}),
    "./foo.js": (function(){}),
})

其实webpack就是把AST分析树 转化成 链表

  • webpackOptionsApply

    • 将素有配置options参数转换成webpack内部插件
    • 使用默认列表,例如
      • output.library -> LibraryTemplatePlugin
      • externals -> ExternalsPlugin
  • 模块构建和chunk生成阶段 compiler hooks

    • 流程相关
      • (before-)run
      • (before-/after-)compiler
      • make
      • (after-)emit
      • done
    • 监听相关
      • watch-run
      • watch-close

compilation

  • compiler 调用 compilation 生命周期方法
    • addEntry -> addModuleChain
    • finish(上报模块错误)
    • seal

ModuleFactory

  • NormalModuleFactory
  • ContextModuleFactory

Module

  • NormalModule: 普通模块
  • ContextModule: ./src/a ./src/b
  • ExternalModule: module.exports = jQuery
  • DelegatedModule: manifest
  • MultiModule: entry: ['a', 'b']

build

  • 使用 loader-runner 运行loaders
  • 通过 Parser 解析(内部是acron)
  • ParserPlugins 添加依赖

Compilation hooks

  • 模块相关
    • build-module
    • failed-module
    • succeed-module
  • 资源生成相关
    • module-asset
    • chunck-asset

优化和seal相关

  • (after-)seal
  • optimize
  • optimize-modules(-basic/advanced)
  • after-optimize-modules
  • after-optimize-chunks
  • after-optimize-tree
  • optimize-chunk-modules(-basic/advanced)

chunk生成算法

  • 1.webpack先将entry中对应的module都生成一个新的chunk
  • 2.遍历module的依赖列表,将依赖的module也加入到chunk
  • 3.如果一个依赖module是动态引入的模块,那么就会根据这个module创建一个新的chunk,继续遍历依赖
  • 4.重复上面过程,直到得到所有的chunks

文件生成

全剧终

经过一周的时间,重新对这几年使用webpack4的感悟进行整理,是时候和 webpack4 说再见了,希望以后不要再见了...

webpack5 向我们招手,读了webpack5源码,得出一个结论:v5之前所有的webpack技巧和插件、loader...,全被遗弃了,我特么心态崩了,已无力吐槽了...

敬请期待 webpack5

他来了,他来了,他带着绿帽走来了(webpack5之风起云涌

❤️ 加入我们

字节跳动 · 幸福里团队

Nice Leader:高级技术专家、掘金知名专栏作者、Flutter中文网社区创办者、Flutter中文社区开源项目发起人、Github社区知名开发者,是dio、fly、dsBridge等多个知名开源项目作者

期待您的加入,一起用技术改变生活!!!

招聘链接: job.toutiao.com/s/JHjRX8B