1. Webpack 模块与解析原理

在讲webpack模块解析之前,我们先了解下webpack模块的概念,以及简单探究下webpack的具体实现。

① webpack 模块

① 何为 webpack 模块
能在 webpack 工程化环境里成功导入的模块,都可以视作webpack模块。 与 Node.js 模块相比,webpack 模块 能以各种方式表达它们的依赖关系。下面是一些示例:

  • ES2015 import 语句
  • CommonJS require() 语句
  • AMD define 和 require 语句
  • css/sass/less 文件中的 @import 语句
  • stylesheet url(…) 或者 HTML第十一章  模块与依赖 - 图1 文件中的图片链接

② 支持的模块类型 Webpack 天生支持如下模块类型:

  • ECMAScript 模块
  • CommonJS 模块
  • AMD 模块
  • Assets
  • WebAssembly 模块

而我们早就发现——通过 loader 可以使 webpack 支持多种语言和预处理器语法编写的模块。loader 向 webpack 描述了如何处理非原生模块,并将相关依赖引入到你的 bundles 中。包括且不限于:

  • TypeScript
  • Sass
  • Less
  • JSON
  • YAML

总的来讲,这些都可以被认为是 webpack 模块。

② compiler与Resolvers

在我们运行webpack的时候(就是我们执行webpack命令进行打包时),其实就是相当于执行了下面的代码:

  1. const webpack = require('webpack');
  2. const compiler = webpack({
  3. // ...这是我们配置的webpackconfig对象
  4. })

webpack的执行会返回一个描述webpack打包编译整个流程的对象,我们将其称之为 compiler 。compiler对象描述整个 webpack 打包流程———它内置了一个打包状态,随着打包过程的进行,状态也会实时变更,同时触发对应的 webpack 生命周期钩子。 (简单点讲,我们可以将其类比为一个Promise对象,状态从打包前,打包中到打包完成或者打包失败。) 每一次 webpack 打包,就是创建一个 compiler 对象,走完整个生命周期的过程。

而 webpack 中所有关于模块的解析,都是 compiler 对象里的内置模块解析器去工作的————简单点讲,你可以理解为这个对象上的一个属性,我们称之为 Resolvers。 webpack 的 Resolvers 解析器的主体功能就是模块解析,它是基于 enhanced-resolve 这个包实现的。换句话讲,在webpack中,无论你使用怎样的模块引入语句,本质其实都是在调用这个包的 api 进行模块路径解析。

image.png

2. 模块解析

webpack 通过Resolvers实现了模块之间的依赖和引用。举个例子:

  1. // 引入第三方库
  2. import _ from 'lodash';
  3. // 引入本地模块
  4. const add = require('./utils/add');

所引用的模块可以是来自应用程序的代码,也可以是第三方库。 resolver帮助 webpack 从每个 require/import 语句中,找到需要引入到 bundle 中的模块代码。 当打包模块时,webpack 使用enhanced-resolve来解析文件路径。

① webpack中的模块路径解析规则

通过内置的enhanced-resolve,webpack 能解析三种文件路径:

  • 绝对路径

    1. import '/home/me/file';
    2. import 'C:\\Users\\me\\file';

    由于已经获得文件的绝对路径,因此不需要再做进一步解析。

  • 相对路径

    1. import '../utils/reqFetch';
    2. import './styles.css';

    这种情况下,使用 import 或 require 的资源文件所处的目录,被认为是上下文目录。 在import/require中给定的相对路径,enhanced-resolve会拼接此上下文路径,来生成模块的绝对路径 (path.resolve(__dirname, RelativePath) 。 这也是我们在写代码时最常用的方式之一

  • 模块路径

    1. import _ from 'lodash'
    2. import 'module/lib/file';

    也就是在resolve.modules中指定的所有目录检索模块(node_modules里的模块已经被默认配置了)。 你可以通过配置别名的方式来替换初始模块路径, 具体请参照下面resolve.alias配置选项。

② resolve

  • alias

通过配置resolve.alias为路径起别名:

  1. const path = require('path');
  2. module.exports = {
  3. //...
  4. resolve: {
  5. alias: {
  6. "@": path.resolve(__dirname, './')
  7. },
  8. },
  9. };
  1. // 原来引入路径
  2. import './src/utils/add.js'
  3. // 现在通过别名引入
  4. import '@/utils/add.js'
  • extentions

当所引入的文件在原来的文件夹中没有同名文件时,我们可以省略文件后缀:

  1. import './src/utils/add'

但当所引入的文件在原来的文件夹中有同名文件时(如:utils 文件夹下有两个 add 文件,一个是 js 文件,一个是 json 文件),它会按照 webpack 默认配置的优先级,优先引入优先级高的文件格式的文件(add.js),那么如何自定义引入文件格式的优先级呢?可以通过配置resolve.extentions来实现

  1. module.exports = {
  2. //...
  3. resolve: {
  4. extensions: ['.js', '.json', '.vue'],
  5. },
  6. };

webpack 会按照数组顺序去解析这些后缀名,对于同名的文件,webpack 总是会先解析列在数组首位的后缀名的文件。

3. 外部扩展

有时候我们为了减小 bundle 的体积,从而把一些不变的第三方库用cdn的形式引入进来,比如:jQuery。在我们 webpack 项目中不安装 jQuery 的情况下,通过 cdn 的方式使用 jQuery ,则可以通过 webpack 的内置属性externals来配置外部扩展模块。

index.js

  1. import $ from 'jquery'
  2. console.log($)
  1. module.exports = {
  2. //...
  3. externals: {
  4. // key值(jquery)与引入的包名要相同
  5. // 第一个参数表示cdn链接
  6. // 第二个参数表示script标签在浏览器上暴露的对象
  7. jquery: ['https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js', 'jQuery']
  8. },
  9. // 指定cdn链接是以什么标签插入index.html中的
  10. externalsType: 'script'
  11. };

然后我们就实现了以 cdn 的方式使用 jQuery 了