深入 webpack 核心原理

名词简介

  • Entry:编译入口,webpack 编译的起点;
  • Compiler:编译管理器,webpack 启动后会创建 compiler 对象,该对象一直存活知道结束退出;
  • Compilation:单次编辑过程的管理器,比如 watch = true 时,运行过程中只有一个 compiler 但每次文件变更触发重新编译时,都会创建一个新的 compilation 对象;
  • Dependence:依赖对象,webpack 基于该类型记录模块间依赖关系;
  • Module:webpack 内部所有资源都会以“module”对象形式存在,所有关于资源的操作、转译、合并都是以 “module” 为基本单位进行的;
  • Chunk:编译完成准备输出时,webpack 会将 module 按特定的规则组织成一个一个的 chunk,这些 chunk 某种程度上跟最终输出一一对应;
  • Loader:资源内容转换器,其实就是实现从内容 A 转换 B 的转换器;
  • Plugin:webpack构建过程中,会在特定的时机广播对应的事件,插件监听这些事件,在特定时间点介入编译过程。

核心流程

webpack 体系知识可以被抽象为:

  • 构建的核心流程;
  • loader 的作用;
  • plugin 架构与常用套路。

Webpack 最核心的功能是用于现代 JavaScript 应用程序的静态模块打包工具。简单来说,就是把包括img、css、js 等资源模块依赖,转译、组合、拼接、生成一个或多个 bundles

包括 内容转换 + 资源合并两种核心功能实现上包含三个阶段:初始化、构建、生成。

初始化

  1. 初始化参数:从配置文件、 配置对象、Shell 参数中读取,与默认配置结合得出最终的参数;
  2. 创建编译器对象:用上一步得到的参数创建 Compiler 对象;
  3. 初始化编译环境:包括注入内置插件、注册各种模块工厂、初始化 RuleSet 集合、加载配置的插件等;
  4. 开始编译:执行 compiler 对象的 run 方法;
  5. 确定入口:根据配置中的 entry 找出所有的入口文件,调用 compilition.addEntry 将入口文件转换为 dependence 对象。

具体基本流程:

  • 启动 webpack ,触发 lib/webpack.js 文件中 createCompiler 方法;
  • createCompiler 方法内部调用 WebpackOptionsApply 插件;
  • WebpackOptionsApply 定义在 lib/WebpackOptionsApply.js 文件,内部根据 entry 配置决定注入 entry 相关的插件,包括:DllEntryPluginDynamicEntryPluginEntryPluginPrefetchPluginProgressPluginContainerPlugin;
  • Entry 相关插件,如 lib/EntryPlugin.jsEntryPlugin 监听 compiler.make 钩子;
  • lib/compiler.jscompile 函数内调用 this.hooks.make.callAsync;
  • 触发 EntryPluginmake 回调,在回调中执行 compilation.addEntry 函数;
  • compilation.addEntry 函数内部经过一系列与主流程无关的 hook 之后,再调用 handleModuleCreate 函数,正式开始构建内容。

构建

  1. 编译模块(make):根据 entry 对应的 dependence 创建 module 对象,调用 loader 将模块转译为标准 JS 内容,调用 JS 解释器将内容转换为 AST 对象,从中找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  2. 完成模块编译:上一步递归处理所有能触达到的模块后,得到了每个模块被翻译后的内容以及它们之间的 依赖关系图。

具体基本流程:

  • 调用 handleModuleCreate ,根据文件类型构建 module 子类;
  • 调用 loader-runner 仓库的 runLoaders 根据用户所配置的 loader 集合读取、转译 module 内容,通常是将各类资源类型转译为 JavaScript 文本;
  • 调用 acorn 将 JS 文本解析为AST;
  • 遍历 AST,触发各种钩子
    1. HarmonyExportDependencyParserPlugin 插件监听 exportImportSpecifier 钩子,解读 JS 文本对应的资源依赖;
    2. 调用 module 对象的 addDependency 将依赖对象加入到 module 依赖列表中。
  • AST 遍历完毕后,调用 module.handleParseResult 处理模块依赖;
  • 对于 module 新增的依赖,调用 handleModuleCreate ,控制流回到第一步;
  • 所有依赖都解析完毕后,构建阶段结束。

生成

  1. 输出资源(seal):根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  2. 写入文件系统(emitAssets):在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

seal 函数主要完成从 module 到 chunks 的转化,具体流程:

  • 构建本次编译的 ChunkGraph 对象;
  • 遍历 compilation.modules 集合,将 moduleentry/动态引入 的规则分配给不同的 Chunk 对象;
  • compilation.modules 集合遍历完毕后,得到完整的 chunks 集合对象,调用 createXxxAssets 方法;
  • createXxxAssets 遍历 module/chunk ,调用 compilation.emitAssets 方法将资 assets 信息记录到 compilation.assets 对象中;
  • 触发 seal 回调,控制流回到 compiler 对象。

webpack 内置的 chunk 封装规则为:

  • entry 及 entry 触达到的模块,组合成一个 chunk
  • 使用动态引入语句引入的模块,各自组合成一个 chunk

entry / 动态引入 两种情况组织 chunks ,必然引发不必要的重复打包。webpack 提供了一些插件如 CommonsChunkPlugin 、SplitChunksPlugin,在基本规则之外进一步优化 chunks 结构。

compilation.seal 函数逻辑:

  1. 遍历 compilation.modules ,记录下模块与 chunk 关系;
  2. 触发各种模块优化钩子,这一步优化的主要是模块依赖关系;
  3. 遍历 module 构建 chunk 集合;
  4. 触发各种优化钩子。

重点关注得到 chunks 集合后的步骤 4 触发的 optimizeChunks 钩子,SplitChunksPlugin 正是使用这个钩子,分析 chunks 集合的内容,按配置规则增加一些通用的 chunk : 

module.exports = class SplitChunksPlugin {
  constructor(options = {}) {
    // ...
  }

  _getCacheGroup(cacheGroupSource) {
    // ...
  }

  apply(compiler) {
    // ...
    compiler.hooks.thisCompilation.tap("SplitChunksPlugin", (compilation) => {
      // ...
      compilation.hooks.optimizeChunks.tap(
        {
          name: "SplitChunksPlugin",
          stage: STAGE_ADVANCED,
        },
        (chunks) => {
          // ...
        }
      );
    });
  }
};

小结

从资源形态扭转的角度看整个流程:

  • compiler.make 阶段:
    • entry 文件以 dependence 对象形式加入 compilation 的依赖列表,dependence 对象记录有 entry 的类型、路径等信息
    • 根据 dependence 调用对应的工厂函数创建 module 对象,之后读入 module 对应的文件内容,调用 loader-runner 对内容做转化,转化结果若有其它依赖则继续读入依赖资源,重复此过程直到所有依赖均被转化为 module
  • compilation.seal 阶段:
    • 遍历 module 集合,根据 entry 配置及引入资源的方式,将 module 分配到不同的 chunk
    • 遍历 chunk 集合,调用 compilation.emitAsset 方法标记 chunk 的输出规则,即转化为 assets 集合
  • compiler.emitAssets 阶段:
    • assets 写入文件系统

Plugins

webpack 的钩子体系是一种强耦合架构,它在特定时机触发钩子时会附带上足够的上下文信息,插件定义的钩子回调中,能也只能与这些上下文背后的数据结构、接口交互产生 side effect,进而影响到编译状态和后续流程。

插件通常是一个带有 apply 函数的类,apply 函数运行时会得到参数 compiler ,以此为起点可以调用 hook 对象注册各种钩子回调:

class SomePlugin {
    apply(compiler) {
        compiler.hooks.thisCompilation.tap('SomePlugin', (compilation) => {
            compilation.hooks.optimizeChunkAssets.tapAsync('SomePlugin', ()=>{});
        })
    }
}

而传递参数与具体的钩子强相关。 

compiler 对象触发时机如下: 

具体触发时机示例:

  • compiler.hooks.compilation
    • 时机:启动编译创建出 compilation 对象后触发
    • 参数:当前编译的 compilation 对象
    • 示例:很多插件基于此事件获取 compilation 实例
  • compiler.hooks.make
    • 时机:正式开始编译时触发
    • 参数:同样是当前编译的 compilation 对象
    • 示例:webpack 内置的 EntryPlugin 基于此钩子实现 entry 模块的初始化
  • compilation.hooks.optimizeChunks
    • 时机: seal 函数中,chunk 集合构建完毕后触发
    • 参数:chunks 集合与 chunkGroups 集合
    • 示例: SplitChunksPlugin 插件基于此钩子实现 chunk 拆分优化
  • compiler.hooks.done
    • 时机:编译完成后触发
    • 参数: stats 对象,包含编译过程中的各类统计信息
    • 示例: webpack-bundle-analyzer 插件基于此钩子实现打包分析

webpack 会将上下文信息以参数或 this (compiler 对象) 形式传递给钩子回调,在回调中可以调用上下文对象的方法或者直接修改上下文对象属性的方式,对原定的流程产生 side effect。 

比如,EntryPlugin 插件:

class EntryPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap(
      "EntryPlugin",
      (compilation, { normalModuleFactory }) => {
        compilation.dependencyFactories.set(
          EntryDependency,
          normalModuleFactory
        );
      }
    );

    compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
      const { entry, options, context } = this;

      const dep = EntryPlugin.createDependency(entry, options);
      compilation.addEntry(context, dep, options, (err) => {
        callback(err);
      });
    });
  }
}

基于 webpack 的移动端尺寸适配方案

目前比较常见的移动端适配不同的屏幕宽度方案是采用css中的相对单位,有 rem 和 vw两种。

rem

rem 的定义是 font-size of the root element,是只相对于浏览器的根元素(HTML元素)的 font-size的来确定的单位,即只需要计算出对应的根元素的字体大小,用同样的 css 代码可以实现等比适配:

1 rem = (1 root element font-size) px

一般在 webpack 构建的时候使用插件来实现rem适配:postcss-pxtorem 和 lib-flexible。 

首先,在webpack.config.js中配置 postcss-loader:

module.exports = {
    entry: "./src/index.js",
    output: {
        path: path.join(__dirname,"/dist"),
        filename: "bundle.js"
    },
    module:{
        rules:[
            {
                test: /\.css$/,
                use:  ['style-loader','css-loader','postcss-loader']  //配置postcss-loader
            }
        ]
    },  
}

然后,安装:

npm i postcss-pxtorem --D 和 npm i amfe-flexible --S

在项目根目录新建.postcssrc.js文件,在其中写入 postcss-pxtorem 插件配置:

module.exports = {
  "plugins": {
    "postcss-pxtorem": {
        rootValue: 75,                   // 750的设计稿
        propList: ['*']
     }
   }
}

在entry指定的入口js文件("./src/index.js")中引入lib-flexible:

import 'amfe-flexible'

最后,使用是就可以直接用设计稿尺寸:

.button {
  width: 47px;
  height: 47px;
}

vw

vw 是 1% of viewport’s width,是相对浏览器可视区域宽度的单位。不需要再像 rem 那样,在 js 去动态设置根元素的 font-size,而是直接相对于屏幕宽度。

比如 750px 的设计稿中,一个元素的宽度是300 px,即:

x / 300px = 100 vw / 750px;

x = 100 vw  * (300 px / 750 px);

同样,使用 webpack可以用 postcss-px-to-viewport 插件来实现。

首先,像 rem 一样在webpack.config.js配置 postcss-loader。

然后,安装:

npm i postcss-px-to-viewport --D

在项目根目录新建.postcssrc.js文件,在其中写入 postcss-px-to-viewport 插件配置:

module.exports = {
  "plugins": {
    'postcss-px-to-viewport': {
        viewportWidth: 750                 //750的设计稿
     }
   }
}

 最后,使用是同样就可以直接用设计稿尺寸:

.button {
  width: 47px;
  height: 47px;
}

小结

首先是在兼容性上,rem 可以兼容更老的浏览器版本;其次是 rem 需要通过 js 计算根元素的字体大小,vm是纯 css 实现。

如果使用的是第三方框架其自身用的是 px 单位且基于 375px 的设计稿,.postcssrc.js 需要做适配:

const path = require('path')
module.exports = ({file}) => {
 
  /** 比如使用 vant UI 框架 */
  const width = file.dirname.includes(path.join('node_modules', 'vant')) ? 375 : 750; 
  
  return {
    "plugins": {
      "postcss-px-to-viewport": {
        viewportWidth: width,
      }
    }
  }
}
const path = require('path')
module.exports = ({file}) => {

  /** 比如使用 vant UI 框架 */
  const rootValue = file.dirname.includes(path.join('node_modules', 'vant')) ? 37.5 : 75;
  
  return {
    "plugins": {
      "postcss-pxtorem": {
        rootValue,
        propList: ['*']
      }
    }
  }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

薛定谔的猫96

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值