16. 模块与依赖

143 阅读5分钟

相关代码

在模块化编程中,开发者将程序分解为功能离散的文件,并称之为模块。

Node.js 从一开始就支持模块化编程,目前大多数主流浏览器已支持 ESM 模块化。

一、Webpack 模块与解析原理

1. Webpack 模块

能在 Webpack 工程化环境里成功导入的模块,都可以视作 Webpack 模块。与 Node.js 模块相比,Webpack 模块能以各种方式表达它们的依赖关系。如:

  • ES6 的 import 语句
    import a from './a'
    
  • CommonJS 的 require 语句
    const a = require('./a')
    
  • AMD 的 definerequire 语句
    define([], function () { return () => {} })
    require(['./a.js', './b.js'], function (a, b) {})
    
  • css/sass/less 的 @import 语句
    @import './a.less'
    
  • stylesheet 的 url(...) 或者 HTML 的 <img src=...>
    .box-bg {
        background-image: url("./a.png");
    }
    

Webpack 默认支持以下模块类型:

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

通过 loader 可以使 Webpack 支持其他语言和预处理器语法编写的模块,loader 向 Webpack 描述了如何处理非原生模块,并将相关依赖引入到你的 bundles 中。如:

  • TypeScript
  • Sass、Less
  • JSON、YAML

2. compiler 与 Resolvers

  • compiler

    在使用 webpack 命令打包的时候,相当于执行了以下代码:

    const webpack = require('webpack')
    const compiler = webpack({
        // ... 这是配置的 webpackconfig 对象
    })
    

    webpack 命令的执行会返回一个 compiler 对象,它描述了 Webpack 打包编译的整个流程。它内置了一个打包状态,随着打包过程的进行,状态会实时变更,同时触发对应的 Webpack 生命周期钩子。

    每一次 webpack 打包,就是创建一个 compiler 对象,走完整个生命周期的过程。

  • Resolvers

    Webpack 中所有关于模块的解析,都是依靠 compiler 对象里的 Resolvers (内置模块解析器)去工作的。

    Resolvers 的主要功能就是解析模块,它是基于 enhanced-resolve 这个包实现的。无论使用怎样的模块引入语句,本质其实都是在调用这个包的 api 进行模块路径解析。

Webpack 模块解析简易原理图示:

image.png

二、模块解析(resolve)

Webpack 通过 Resolvers 实现了模块之间的依赖和引用。如:

import _ from 'lodash'
// 或
const add = require('./utils/add')

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

1. Webpack 中的模块路径解析规则

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

  • 绝对路径

    import '/home/me/file'
    import 'C:\\Users\\me\\file'
    

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

  • 相对路径

    import '../utils/reqFetch'
    import './styles.css'
    

    使用 importrequire 引入的资源文件所处的目录,会被认为是上下文目录。在 importrequire 中给定的相对路径,enhanced-resolve 会拼接此上下文路径,来生成模块的绝对路径:path.resolve(__dirname, RelativePath)

  • 模块路径

    import 'module'
    import 'module/lib/file'
    

    resolve.modules 中指定的所有目录检索模块(node_modules 里的模块已经被默认配置了),也可以通过配置别名(resolve.alias)的方式来替换初始模块路径。

2. resolve.alias

通过 resolve.alias 可以自定义配置模块路径。

假设有文件 src/index.js、src/utils/add.js,在 index.js 中使用以下方式引入 add.js 会报错:

import add from '@/utils/add'

因为 Webpack 会将其当做一个模块路径来识别,所以无法找到 @ 这个模块。这时,我们配置一下 resolve

// webpack.config.js

const path = require('path')
module.exports = {
    resolve: {
        alias: {
            "@": path.resolve(__dirname, 'src')    // 将 src 配置为一个模块路径,并起别名为 @
        },
    },
}

这样做的好处是,在项目任何文件中引入 src 目录中的资源,不用再通过相对路径去引入,换句话说不用写一堆 ../,而是直接通过 @ 就可以访问到 src 目录。

3. resolve.extentions

以上代码,通过 import add from '@/utils/add',就可以引入 add.js 文件。如果 utils 目录中同时存在 add.js、add.json、add.wasm 文件,那么引入的会是哪个文件呢?答案是仍然会引入 add.js。

如果把 add.js 文件删掉,又会引入什么文件呢?这就要看 resolve.extentions 是如何配置的了:

module.exports = {
    resolve: {
        extensions: ['.js', '.json', '.wasm'],
    },
}

Webpack 会按照数组顺序去解析这些后缀名。也就是说,如果把 add.js 删掉,默认就会引入 add.json 文件。

三、外部扩展(Externals)

有时我们为了减小 bundle 的体积,把一些不变的第三方库用 cdn 的形式引入进来,比如 jQuery:

<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>

此时我们想在代码里使用引入的 jQuery,但似乎三种模块引入方式都不行,这时候怎么办呢?Webpack 给我们提供了 Externals 的配置属性,让我们可以配置外部扩展模块:

module.exports = {
    externals: {
        jquery: 'jQuery',
        // 或者
        // jquery: '$',
    },
}

此时就可以在代码中引入 jQuery 并使用:

import $ from 'jquery'
console.log($)

如果不想在页面通过 script 标签手动引入,还可以通过以下方式配置外部扩展模块:

module.exports = {
    externalsType: 'script',
    externals: {
        jquery: ['https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js', 'jQuery']
    },
}

四、依赖图(dependency graph)

当一个文件依赖另一个文件时,Webpack 会将文件视为存在依赖关系。这使得 Webpack 可以获取非代码资源,如图片或字体等,并把它们作为依赖提供给应用程序。

当 Webpack 开始工作时,它会根据我们写好的配置,从入口(entry)开始递归地构建一个依赖关系图,这个依赖关系图包含着应用程序中所需要的每个模块,然后将所有模块打包为 bundle(output)。

这里有一些 bundle 分析工具,可以帮助你查看打包产物的依赖关系:

  • webpack-chart:webpack stats 可交互饼图;
  • webpack-visualizer:可视化并分析你的 bundle,检查哪些模块占用空间,哪些可能是重复使用的;
  • webpack-bundle-analyzer:一个 plugin 和 CLI 工具,它将 bundle 内容展示为一个便捷的、交互式、可缩放的树状图形式;
  • Bundle optimize helper:这个工具会分析你的 bundle,并提供可操作的改进措施,以减少 bundle 的大小;
  • bundle-stats:生成一个 bundle 报告(bundle 大小、资源、模块),并比较不同构建之间的结果。

使用 webpack-bundle-analyzer:

npm install --save-dev webpack-bundle-analyzer
// webpack.config.js

const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')

module.exports = {
    plugins: [
        new BundleAnalyzerPlugin()
    ]
}

当执行 npx webpack,会自动在浏览器中打开下面的链接,可以查看每个模块的依赖关系及其他信息。

image.png