IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    弄懂这几个概念后,我对webpack有了新的理解

    limingcan发表于 2023-06-26 18:58:34
    love 0

    前言

    随着vite的诞生,webpack似乎渐渐的被大家抛弃。前阵子我也用vue@3.x + vite@4.x开发了一个后台管理系统,体验了一把,确实有被vite飞快的启动速度给惊艳到。

    但是毕竟webpack已经诞生了许久,也经过市场的一些考验,并且它有着丰富的插件,丰富的功能,一些大型的项目也使用过它,目前来说,它是一个相对于vite来说更稳定的打包工具。

    基于以下原因:

    • 有些公司由于历史原因,项目构建也是基于webpack,所以如果去到一些公司要做脚手架迁移升级什么的,会使用webpack就显得十分有必要了。
    • 前端构建工具,使用步骤,思想基本差不多,因此学会webpack,对其他构建工具的学习也有帮助

    所以,思来想去,还是决定开个webpack的专栏,跟大家一起学习webpack怎么配置(虽然专栏开的有点晚了)。

    专栏会带着大家循序渐进的学习webpack,从比较实际的例子出发,让大家更好的理解webpack,到最后自己配出一个完整的webpack脚手架。当我们学会了自己配webpack后,可以根据自己不同的需求,高度定制属于自己的脚手架,这还是十分实用的。

    本篇文章主要核心,是跟大家一起搞懂使用webpack时,涉及到的几个概念,让我们对webpack编译流程有个更好的理解。

    webpack是什么

    它就是一个打包工具:

    • 它可以把我们源码中的es6+ js、less、sass、img、font等多个模块资源文件,经过一系列处理,编译出我们浏览器能识别并运行css、js、html等前端资源
    • 同时,它还提供很多可高度配置的功能(tree shaking、代码分割、模块热替换)等,帮助我们更好的优化管理我们的代码和资源

    webpack.config.js

    webpack在v4.0.0以后拥有开箱即用的功能,即我们安装了webpack、webpack-cli两个依赖以后,无需任何配置文件,就可以直接进行打包。这是因为:

    • webpack默认入口为:根目录/src/index.js
    • webpack默认输出编译后的文件及文件名为:根目录/dist/main.js

    但我们一般会在我们的项目根目录新建一个名为webpack.config.js的配置文件。这样可以指示webpack如何处理我们项目中的文件、资源等;通过更高度的自定义配置来满足我们项目的需求。

    我们看看这个配置文件大概得框架是什么样,心里有个印象:

    // webpack.config.js
    module.exports = {
        // 入口
        entry: {},
    
        // 打包输出
        output: {},
    
        // 配置模块如何解析
        resolve: {},
    
        // 配置各种loader
        module: {},
    
        // 配置插件
        plugins: [],
    
        // 优化(可以进行代码分割)
        optimization: {},
    
        // webpack-dev-server 开发时的配置,一般用于development模式
        devServer: {}
    };

    webapck当然还支持很多配置项,只是我们平时用到的一般就这几个,其他配置项可以看文档

    注意

    webpack配置文件,里面的代码随便大家怎么玩,配置文件名字也不一定一定要为webpack.config.js,只要保证最后输出的是一个webpack配置的对象就可以了:

    // myconfig.js
    
    // 各种逻辑代码
    ...
    ...
    
    // 保证最后导出一个`webpack`配置的对象就可以
    module.exports = {
        // 各种webpack配置
        entry: {},
        output: {},
        ....
    }
    "build": "webpack --config myconfig.js"

    优化

    所有配置都写在一个文件,这不利于我们维护与开发;而且我们开发项目时,会有多个环境(本地开发环境,测试环境,生产环境),所以我们一般会有不同环境的配置文件,跟所有环境都通用的配置文件。
    我们一般会将"通用的配置文件"与“不同环境的配置文件”合并,最后再由webpack运行这个合并后的这个配置文件。

    配置文件一般有:

    • 开发与生产环境通用的配置(webpack.common.js)
    • 开发环境配置(webpack.dev.js)
    • 生产环境配置(webpack.pro.js)
    后续文章会教大家如何进行这种配置方法

    entry

    它指的是webpack开始解析,构建依赖图的起点。我们一般用{key: value}对象的形式,来配置entry,例如:

    module.exports = {
        entry: {
            index: './src/index.js',
            share: './src/share.js',
        },
    }

    这表示:

    • 我们项目中有两个入口index、share
    • webpack会从index、share两个入口开始构建它们相关依赖的模块,从而形成一个依赖图(后面会有更详细的解释)
    • 打包后会以key为打包后的文件名字

    output

    它十分好理解,它可以设置经过webpack打包后的编译文件的名称,及应该输出到哪个位置。

    需要注意的是,即使我们设置了多个entry的入口,但是只能指定一个 output 配置。

    Module

    我们通过import或require进来的资源文件,或我们项目中的每个文件,都可以看作为一个独立的模块。因此,它可以是js文件、css文件、也可以是图片等任何一种资源。所以我们开发中经常会看到以下语句:

    • import 'index.css'
    • import A from './x.js'
    • import pic from './x.png'
    • ...

    模块之间的组合又会形成一个Chunk。Chunk是一个很重要的概念,后文会详细讲。

    参考文章:Modules

    Loader

    通过上文我们知道,项目中每个一个文件都可以看作是一个模块,又由于我们的文件类型多种多样,因此我们需要某个东西,它可以把这些模块解析成webpack能够识别的有效模块,并将他们添加到依赖图中,这个东西就是Loader。

    webpack也很贴心的给我们提供了一个module的配置项,它专门用来配置不同的loader以至于解析不同的文件类型(模块)。这是因为webpack默认只能解析js和json文件,所以如果要解析不同的类型的文件(模块),我们就要安装相应的loader:

    // webpack.config.js
    module.exports = {
        ...,
        modules: {
            rules: [
                // 解析.txt文件,使用raw-loader
                { test: /.txt$/, use: 'raw-loader' },
            ],
        }
    }

    loader有两个属性:

    • test 属性,识别出哪些文件会被转换。
    • use 属性,定义出在进行转换时,应该使用哪个loader。

    webpack中,还有哪些loader可以看这里

    Plugin

    Loader用于转化模块,Plugin则用来加强webpack打包编译时的功能。所以它一般有打包优化,资源管理,注入环境变量等功能。

    因为webpack也很贴心的给我们提供了一个plugin的配置项,我们想增强什么功能,去plugin里面配置就好了,例如我们在我们项目中直接定义一些全局变量:

    // webpack.config.js
    module.exports = {
        ...,
        plugins: [
             new webpack.DefinePlugin({
                AUTHOR_NAME: JSON.stringify('Lee'),
            })
        ]
    }
    
    // index.js
    console.log(AUTHOR_NAME); // Lee

    webpack中,还有哪些plugin可以看这里

    Dependency graph(依赖图)

    当我们一个文件依赖另一个文件时,webpack都会将文件视为直接存在“依赖关系”。

    举个简单的例子,假设我们有三个文件,它们的关系如下:

    // main.js
    import './index.js';
    
    // index.js
    import 'plugin.js'
    console.log('I am from index.js');
    
    // plugin.js
    console.log('I am plugin.js');

    我们来分析一下:

    1. main.js、index.js、plugin.js相当于三个模块;
    2. 然后main.js引入了index.js,index.js又引入了plugin.js;
    3. 因此这三个文件都存在相互引用关系;
    4. 这时形成了这样一个有引用关系的图谱:main.js→index.js→plugin.js

    总结:

    webpack会以entry为起点,并把它作为依赖图的起始点;然后分析处理entry里面内部的import,不断递归查询类似上述示例的依赖关系,这个过程最后会形成一个具有依赖关系的图谱,这个图谱就是“依赖图”。

    webpack会根据这个依赖图,再进一步操作。

    ⭐️ Chunk

    在我们查阅webpack中文文档时,我们经常会看到Chunk这个单词,它并没有被翻译成中文。这是因为Chunk是webpack打包过程中产生的一个逻辑概念,需要结合上下文才能理解出它的意思。

    Chunk是webapck里面比较重要的概念,如果我们弄懂它,对webpack打包整个流程,代码分割也会有很好的理解。

    解释

    在webpack打包过程中,会将一个或一组模块(我们上面说到的webpack中的任何一个文件,都可以看作是一个模块)组合成一个整体,那么这个整体就可以当做一个Chunk。一般来说,webpack打包过程中,有几个Chunk,最后就会输出几个js文件。

    我们通过 learn-01 这个案例,里面是最简单的配置,最简单的代码,这样更好理解Chunk是什么。

    我们有三个js文件:

    • index.js:入口文件
    • async.js:用来异步引入的文件
    • vendors.js:可以把它当做某个第三方依赖文件

    文件代码及webpack配置如下:

    // webpack.config.js
    module.exports = {
        entry: {
            index: './src/index.js'
        },
        output: {
            filename: '[name]-bundle.js'
        }
    }
    
    // index.js
    import './vendors';
    import(/* webpackChunkName: "async" */ './async');
    console.log('I am from index.js');
    
    // async.js
    console.log('I am from async.js');
    
    // vendors.js
    console.log('I am from vendors.js');

    看到这,我们可以先猜一下打包出来的文件有几个。如果猜对了,说明大家对Chunk也有一定了解了。

    分析

    我们先分析一下打包后的文件及结构:

    dist
    ├── async-bundle.js
    └── index-bundle.js

    一共输出了两个js文件。

    通过上文,我们知道:

    • 每个文件都可以看成一个module(模块)
    • 在webpack打包过程中,会将一个或一组模块组合成一个整体,那么这个整体就可以当做一个Chunk

    我们接着分析:

    • 通过查看dist/index-bundle.js文件,我们会发现里面包含了非异步引入的js文件:入口文件index.js、vendors.js(还有一些webpack打包时,自己加入的代码 runtime)。这说明index.js、vendors.js这两个模块组成了一个Chunk。这里我们称它为chunk[initial];
    • 通过查看dist/async-bundle.js文件,里面只包含了async.js的代码。这说明异步引入的模块,会被单独分成一个Chunk。这里我们把这个Chunk称为chunk[no-initial]
    • chunk[initial]与chunk[no-initial]都来自于同一个入口index.js,所以这两个Chunk组合起来,可以看成一个Chunk组,或者说入口文件会组成一个整体的Chunk组。我们称它为Chunk[index]

    好,相信到这里,大家应该对Chunk形成有个大致印象了,我们再来捋一下这个过程。

    webpack编译时,通过我们的配置:

    1. 会先找到enrty,有几个entry,就会以这些entry组成一个Chunk组(示例中的Chunk[index])
    2. 再分析这些Chunk组,将入口js及这个入口所有相关的依赖模块,组成一个chunk(示例中的chunk[initial])
    3. 如果有异步引入的模块,则这个模块单独再组成一个Chunk(示例中的chunk[no-initial])
    4. 最后打包输出chunk[initial](index-bundle.js)、chunk[no-initial](async-bundle.js)

    上述示例,chunk、module(模块)的关系如下:

    chunk_pic

    形式

    通过上述分析,我们可以知道,Chunk有两种形式:

    • initial:初始的。我们的入口js及这个入口所有相关的依赖模块,组合成的一个集合,可以看成一个Chunk。(即上文index.js、vendors.js组成的chunk[initial])
    • non-initial:非初始的。说明它是异步加载的模块,如果我们在代码中用到了类似import('./A.js')的语句,这这个js会被单独分成一个异步的Chunk。(即上文async.js组成的chunk[no-initial])

    如何产生Chunk

    • 通过entry配置,产生一个以入口为整体的Chunk组(这个组不会被打包出来,只是会形成这个Chunk组)
    • 我们的入口js及这个入口所有相关的依赖模块,产生initial Chunk
    • 通过异步import(),产生non-initial Chunk
    • 通过webpack强大的代码分割,产生其他chunk

    通过上面的解释与分析,希望大家以后用webpack时,可以在脑海中有一个Chunk形成的大概过程,这对我们使用代码分割是十分有帮助的。

    参考文章:揭示内部原理

    Bundle

    Bundle指的是webpack打包后的所有产物。

    如果我们output配置打包后输出的文件目录是dist,我们的Bundle就是dist文件夹里面的所有产物。

    一般来说有几个Chunk,就会打包出多少个js bundle文件。

    打包过程

    浅析

    为了大家更好的理解上文解析的概念,我们浅析webpack的打包流程,看看上文的概念体现在哪些流程中。

    我们在终端运行webpack后,它会经历以下过程:

    1. 读取我们指定的配置文件(webpack.config.js)
    2. 从入口entry开始,分析我们的Module(模块)并递归我们整个项目模块间的依赖关系
    3. 加载相应的Loader,将这些Module(模块)解析成webpack能够识别的有效模块,并它们加入到依赖图(Dependency graph)
    4. 编译过程会触发多个事件,执行配置的Plugin(插件)
    5. 将分析好的模块进行分组,形成Chunk
    6. 根据配置文件(output),输出最后的Bundle

    上述过程可以看作三个阶段:

    • 初始化阶段(过程1)
    • 编译阶段(过程2-过程5)
    • 输出阶段(过程6)

    可总结为下图:

    webpack_process

    webpack实际打包的过程当然复杂得多,这里为了更好的理解,简化了

    体验

    我们从实际出发,通过 learn-02 这个案例,用实际代码再深入体验理解一下webpack打包过程,跟涉及到的概念。

    我们来看看 learn-02 项目结构:

    learn-02
    ├── index.html
    ├── package-lock.json
    ├── package.json
    ├── project.config.js
    └── src
        ├── assets
        │   └── style.less
        ├── index.js
        ├── plugin
        │   ├── common.js
        │   ├── index-vendors.js
        │   └── share-vendors.js
        └── share.js

    我们来介绍一下相应的文件:

    • index.html:用来运行我们打包后的文件,查看效果
    • project.config.js:webpack配置文件(为了区别webpack.config.js,专门另起一个名字)
    • style.less:样式
    • index.js:入口文件
    • share.js:入口文件
    • common.js:存放公用方法,分别会被两个入口文件,引用两次
    • index-vendors.js:可以当做index.js的一些依赖
    • share-vendors.js:可以当做share.js的一些依赖

    以下项目中的相关代码:

    directory

    我们看到project.config.js的配置后,以后一样可以猜猜打包后会输出几个文件。

    好,现在我们开始分析:

    1️⃣ 当我们在终端运行npm run build后,webpack会读取我们指定的文件project.config.js

    2️⃣ 从entry开始,分析我们的模块。我们的入口有两个index、share,所以这时会形成两个Chunk组:Chunk[index]、Chunk[share],并且递归我们模块相应的依赖关系。

    3️⃣ index.js引入了style.less,所以会加载相应的Loader,将它解析成webpack能识别的有效模块,并将其加入到依赖图中。这时会形成两个依赖图:

    • 一个由入口index.js及其依赖组成的依赖图(index.js -> style.less,common.js、index-vendors.js)
    • 一个由入口share.js及其依赖组成的依赖图(share.js -> common.js、share-vendors.js)

    4️⃣ 然后将这些模块进行分组:

    • 入口index.js及其依赖组成的一个chunk[initial-index];
    • 入口share.js及其依赖组成的chunk[initial-share]

    5️⃣ 发现我们的配置中,还利用代码分割把commonjs也独立分割出来,因此它独立组成了一个chunk[initial-common]

    6️⃣ 至此,webpack已经分出了三个chunk:

    • chunk[initial-index]
    • chunk[initial-share]
    • chunk[initial-common]

    7️⃣ 根据output最后输出Bundle

    同样,实际打包过程肯定要复杂得多

    最后

    • 这篇文章分析讲解了webpack里面涉及到的一些概念,尤其是Chunk的知识比较重要。理解了Chunk,大家一定会对webpack有一个更好的理解。希望读完这篇文章后,我们在使用webpack时,脑海会有一个大致的过程,跟分辨出大概有几个Chunk
    • 后续的文章会开始教大家怎么配置webpack,如果感兴趣的话可以关注一下这个👉🏻专栏
    • 文章涉及到的案例已经上传到 github,非常欢迎star、fork学习

    最后的最后,如果大家觉得文章有帮助到,创作不易,还请大家多点赞转发,如果有异同点,欢迎评论讨论。



沪ICP备19023445号-2号
友情链接