webpack全面解析(1)- webpack 的主要配置

391 阅读9分钟

1.png

webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部构建一个 依赖图(dependency graph),此依赖图对应映射到项目所需的每个模块,并生成一个或多个 bundle

webpack作为现代前端打包最重要的工具之一,在前端开发应用中是必不可少的。虽然说工程化的项目中,比如 reactcraVUEcli 以及其他的一些脚手架,都会将相关的配置给我们配置好,但是在大型的应用项目中,如果想要对自己的工程项目进行充分优化,那么对 webpack 的学习就是必不可少的了。

正如 lucasHC 的观点: webpack 工程师 是大于 前端开发工程师的。

同时,前端项目的工程化实现,也离不开 webpack 的参与。

本文将会结合 webpack 的官方文档,从如下几个角度依次带大家学习 webpack

  1. webpack 的主要配置
  2. 学习 create-react-app 的 webpack 配置
  3. webpack的主要API (loader & plugin)
  4. 如果去实现一个 loader
  5. 如果去实现一个 plugin
  6. webpack 的源码解析
  7. 如何实现一个最小的 webpack 打包工具

-- 值得说明的是,本项目使用的webpack版本为 webpack ^5.x.x

WebPack 的主要配置

这一部分的内容,其实在 webpack 的官方中文文档中阐述的比较清楚,下面我们就一起看一个普通项目的 webpack 配置。

1. chunk | module | bundle?

在说配置之前,需要先讲清楚这几个概念。

先来说说chunk

chunk: 此 webpack 特定术语在内部用于管理捆绑过程。输出束(bundle)由块组成,其中有几种类型(例如 entrychild )。通常, 直接与 输出束 (bundle)相对应,但是,有些配置不会产生一对一的关系。

以上是官方给出的解释,我自认为写得太抽象了,有些人说,每有一个entry,就对应一个chunk,其实这种说法也不完全准确。

准确的说,chunkwebpack执行的中间文件。

而对应到modulebundle,可以说是 webpack输入输出

比如:

我们写了一些es6 module的模块、一些css文件,以及一些静态资源 --module

通过打包工具,从某个entry进行打包,在webpack内部就形成了一 个chunk

最后我们通过例如 MiniCssExtractPlugin等插件,生成了一些 xx.bundle.jsxx.bundle.css文件。

2. 输入(entry) | 输出(output) | loader | 插件(plugins)

2.1 entry

entry 是一个 webpack 配置的入口文件,

module.exports = {
  entry: {
    pageOne: './src/pageOne/index.js',
    pageTwo: './src/pageTwo/index.js',
    pageThree: './src/pageThree/index.js',
  },
};

这里值得说明的有几点:

  • 当我们有多个应用入口,但是使用了相同的一些第三方库,比如 echartsreact 等,我们有两种方法去对共享的代码进行单独打包。

    a. 使用 optimization.splitChunks 为页面间共享的应用程序代码创建 bundle

    b. 在entry 中使用 dependOn 来实现。

     entry: {
        shared: ['echarts','react','react-dom'],
        catalog: {
            import: './src/react/catalog.js',
            filename: './catalog.js',
            dependOn: 'shared',
        },
        personal: {
            import: './src/react/personal.js',
            filename: './personal.js',
            dependOn: 'shared',
        },
    },
    
  • 必要的时候,我们亦可以在entry中指明输出文件的名称。

  • 常见的代码分割有三种方式:

    • webpack 配置的入口点 entry 中去进行分割;
    • 使用 SplitChunkPlugin 进行分割;
    • 在工程中使用 import().then(), 或者 reactlazyVUEdynamicImport 进行分割。

2.2 output

output

默认情况下,我们导出的是支持浏览器解析的版本。

output的 chunkFormat 、chunkLoading 等参数,是和 target 深度绑定的。(详细内容可以参考 webpack/lib/defaults.js中的设置。

当然,这里也可以通过 chunkFormat 、chunkLoading 等设置,导出 node 等相关的版本。

关于 output 的配置还有很多,一般情况下我们在使用框架开发时,不太能用得到,这里只给出来一些比较简单的。

这里值得说明的有几点:

  • hash | contenthash | chunkhash 这几者的区别

    • hash: 针对每一次单独打包时进行生成,所以不同的打包环境,生成的不同,不利于缓存。同时,对于多入口的文件,每一次打包的 hash 值是相同的。
    • contenthash: 根据文件内容生成不同的 hash 值,改变文件内容时, hash 值会发生变化。
    • chunkhash: 顾名思义,不同的 chunk , 生成的 hash 值不同。

2.3 loader

loader 的配置在 module 选项中,这里关于 module 的其他选项我们暂不介绍,直接说说loader,即 module.rules

一个典型的 rule 的配置包括了 test 和 use 的配置。

{
    module:{
        rules:[{
            test: /\.js/i,
            use:[
                {loader :"babel-loader"}
            ]
        }]
    }
}

loader 在使用时,我需要注意的是:

  • 必要时请声明 include 和 exclude,这样会减少 loader 的编译内容,从而提高打包的性能;
  • 老生长谈的是,loader 是自右向左进行加载的,如对less文件的解析,可能的配置如下:use: ["style-loader", "css-loader", "less-loader"],

2.4 plugins

整个的webpack是基于插件化实现的,如果我们尝试读过源码,可以发现,很多配置内的参数,在内部是通过一个个插件,在打包的不同阶段进行处理的。

关于插件的解释,我们在后面会详细给出。

3. resolve | library | optimization

3.1 resolve

resolve 作为解析模块,更多的是在我们进行工程项目开发时,可能会遇到的一些问题的处理。

如 alias 的使用:

resolve: {
    alias: {
      Utilities: path.resolve(__dirname, 'src/utilities/'),
      Templates: path.resolve(__dirname, 'src/templates/'),
    },
  },
// 在代码里,我们可以直接使用
// import Utilities/index.js 取代:
// import ../../src/utiluties/index.js

如 extensions 的使用:

可以指定默认扩展名称的加载类型;

如 modules 的使用: 告诉 webpack 解析模块时应该搜索的目录,一般为 node_modules

值得说明的是:webpack 5 不再自动 polyfill Node.js 的核心模块,这意味着如果你在浏览器或类似的环境中运行的代码中使用它们,你必须从 NPM 中安装兼容的模块,并自己包含它们。

3.2 library

library的配置应用于,当你想要打包一个库的时候。可以选择在 output 中去使用它。

这里指的说明的是umd规范。

当我们用这种规范去打包一个库的时候,可以通过多种方式去引用它。

比如:

  • CommonJS module require:

    const webpackNumbers = require('webpack-numbers');
    // ...
    webpackNumbers.wordToNum('Two');
    
  • AMD module require:

    require(['webpackNumbers'], function (webpackNumbers) {
      // ...
      webpackNumbers.wordToNum('Two');
    });
    
  • script tag:

    <!DOCTYPE html>
    <html>
      ...
      <script src="https://example.org/webpack-numbers.js"></script>
      <script>
        // ...
        // Global variable
        webpackNumbers.wordToNum('Five');
        // Property in the window object
        window.webpackNumbers.wordToNum('Five');
        // ...
      </script>
    </html>
    

3.3 optimization

从名称上我们也能看得出来,这一部分的配置主要用于优化的工作。

我们最常用的几个地方如下:

minimize / minimizer

这里主要是用于设置一些对代码压缩的工具和插件。

splitChunk 和 runtimeChunk

这两个都是对一些代码做chunk上的拆分,笔者在学习的时候也常常搞混,这里我们举个例子,来详细地说明一下。

我们创建一个项目,在 ./src下增加一个react的文件夹,并建立如下几个文件:

// ./src/react/app.js
import React, { Component } from 'react';
export default class app extends Component {
    render() {
        return (
            <div>
                this is a React app
            </div>
        )
    }
}

// ./src/react/index.js

import ReactDOM from 'react-dom';
import App from './app';
import React from 'react';
ReactDOM.render(<App />, document.getElementById('root'))

我们使用如下的配置进行打包,因为我的测试程序的webpack.config.js是自行编辑的,所以声明了执行目录:

module.exports = {
    mode:"production",
    context: path.resolve(process.cwd()),
    entry: { app: './src/react/home.js' },
    output: {
        clean:true,
        path:path.resolve(process.cwd(),'./dist04'),
        filename:'[name].[chunkhash].js'
    },
    module:{
        rules: [{
            test: /\.(js|jsx)$/,
            use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env', '@babel/preset-react']
                    }
                },

            include: path.resolve(process.cwd(), './src/react'),
            exclude: path.resolve(process.cwd(), './node_modules/')
        }]
    },
}

我们生成的文件如下:

app.bdeb74ac3557b6d6c47a.js ( 129KB )

很显然,这个129KB 的文件里,包含了 react 的相关的源码,那么我们先加一个 runtimecHUNK 的配置:

optimization:{ runtimeChunk:'single'}

我们发现,打包完成了的内容中,多了一个:

runtime.ecfe325a56eb578bdcaf.js (1KB)

这里其实是把 app 中的运行时函数给切分了出来。这里由于文件太单一,看不出来,我们再在 src/react下增加一个文件:

// ./src/react/home.js

import ReactDOM from 'react-dom';
import App from './app';
import React from 'react';
ReactDOM.render(<App />, document.getElementById('root'))

然后再在配置中增加:

entry: {
    home:'./src/react/index.js',
        app: './src/react/home.js'
},
optimization:{
    splitChunks :{
        chunks:'all'
    },
    runtimeChunk:'multiple'
}

我们再看看打包后的结果:

3.png

我们发现,reactreact-dom等相关的第三方库,被打到了单独的文件中。

我们来总结一下:

  • runtimeChunk会根据配置内容是multiple还是single,对runtime相关的文件,打包单独的或共享的chunk

  • splitChunks中,会对多入口上共用的文件,打成单独的包。当然,在单一入口下,也会生成相应的内容。

    当然,splitChunks作为一个老版本的独立插件,还是有很多强大的功能,如cacheGroups中的各种配置,对缓存的处理,都是值得我们去学习的。

4. watch | devServer | middleware

最后,我们来说说自动编译。

我们知道,在每次编译代码时,手动运行 npm run build 会显得很麻烦。

webpack 提供几种可选方式,帮助你在代码发生变化后自动编译代码:

  1. webpack's Watch Mode
  2. webpack-dev-server
  3. webpack-dev-middleware

多数场景中,你可能需要使用 webpack-dev-server,但是不妨探讨一下以上的所有选项。

4.1 watch 模式

我们可以通过在配置中增加{ watch: true }, 或者直接在脚本的运行中添加 webpack -- watch的方法启动 watch 模式。

值得注意的是,在这种模式下如果我们想要提高 watch 的效率,最好的办法是添加一些配置,让 watch 的内容缩减到最小:

{
    watchOptions: {
        ignored: ['**/node_modules', "**/files/**/*.js"]
    }
}

4.2 webpack-dev-server

第二种方式是使用 webpack-dev-server,也是目前 React 的脚手架中,使用的方法。

使用 webpack-dev-server 至少要满足两个条件:

  • 第一:添加一个 html-webpack-plugin
  • 第二:在webpack的命令中增加:serve --open

同时,你可以修改相关的配置,增加 devServer: { contentBase: './dist' , port:'8080'}等配置,但这不是必须的。

值得说明的是,我们也可以使用 node.js的方式进行执行:

var compiler = webpack(config);
const devServer = new WebpackDevServer(compiler, serverConfig);
devServer.listen(port, () => {
    
})

这种方式在 reactcra 中有详细的时候,后面我们可以进行讨论。

4.3 webpack-dev-middleware

关于 webpack-dev-middleware也很简单,我们只需要启动一个node-server,即可以实现,这里不作详细讨论。

总结

到了这里,关于 webpack 常用的一些配置就说完了。

当然还有很多更复杂的内容我们没有讲解,比如一些关于微前端、优化、缓存、lib库的内容,其实都是很重要的。

在后续的文章中,我们还会针对某一块的内容,做更详细的探讨。