不少同学在前端开发过程中,应该都会用到webpack打包编译,一点都不会陌生,但一遇到问题,往往不少同学都又两眼一黑,归根结底还是应该存在理解不够透彻的原因把。所以我希望通过这篇文章来分享,让更多同学更加透彻额理解。

目录

内容

为什么会需要webpack

简而言之就是解决模块之间的依赖,把各个模块按照特定的规则和顺序组织在一起,最终合并为一个JS文件。你可以把Webpack理解为一个模块处理工厂。我们把源代码交给Webpack,由它去进行加工、拼装处理,产出最终的资源文件,等待送往用户。

  • 使浏览器代码模块化(像nodejs那样)
  • 对生产环境中各类资源的加载进行优化,如合并代码请求,压缩代码,去重代码。
  • 对代码进行split chunk,实现按需加载。

webpack的demo

起步做什么?当然是合并代码呀。

  • 初始化一个helloworld项目, 把webpack装好
  • 加入必要文件,webpack.config.js, /src/*.js
  • 你会得到如下,这样一个最简单的目录结构
    /src
        app.js
    webpack.config.js
    package.json
  • webpack.config.js的内容
    module.exports = {
        entry: './src/app.js',
        output: {
            filename: '[name].js'
        }
    };
  • package.json中script定义个命令方便执行
    "scripts": {
        "build": "webpack"
    },
  • 执行 npm run build. 则会生产/dist/main.js

webpack配置概览

我们写一个最粗略,最全的webpack配置内容

module.exports = {
    mode:"production", // "production" | "development" | "none"
    //模式配置
    entry: "./app/entry", // string | object | array
    //输入配置
    output: {},
    //输出配置
    module: {},
    //模块配置
    plugins: [],
    //插件
    resolve: {},
    // 解析模块请求的选项(不适用于对 loader 解析)
    performance: {}
    //性能消耗设定
    devtool: "source-map", // enum
    // 通过在浏览器调试工具(browser devtools)中添加元信息(meta info)增强调试
    // 牺牲了构建速度的 `source-map' 是最详细的。
    context: __dirname, // string(绝对路径!)
    //webpack的主目录
    target: "web", // 枚举 
    // 包(bundle)应该运行的环境
    externals: ["react", /^@angular\//],
    // 不要遵循/打包这些模块,而是在运行时从环境中请求他们
    stats: "errors-only",
    //精确控制要显示的 bundle 信息
    devServer: {}
    //开发服务模块
}

js从webpack输入到输出

我们先尽可能的简化配置,简化流程。怎么理解这句呢?因为webpack其实包含很多内容,例如entry, output,loader, plugin等多块内容。

假设我们就只从js的入口输入到出口输出。则我们只能处理js文件的打包。让我们围绕着这个内容先弄清楚,再考虑其他内容。

那让我们先从输入到输出把。

entry即定义代码分为哪些入口进入。通常我们可以指定它为sting或object,如下:

//单入口
entry: "./src/app.js" 
//多入口, key值则为对应的打包出口文件的的命名, 如app.js和index.js
entry: {
    app:'./src/app.js', 
    index: './src/index.js'
}
//或者数组
entry: ['./src/app.js','./src/index.js']
//传入一个数组的作用是将多个资源预先合并,这样Webpack在打包时会将数组中的最后一个元素作为实际的入口路径

output则是输出的相关配置

output: {
    path: path.resolve(__dirname, "dist"), // string
    // 所有输出文件的目标路径, 必须是绝对路径(使用 Node.js 的 path 模块)
    filename: "bundle.js", // string
    // filename: "[name].js", // 用于多个入口点(entry point)(出口点?)
    // filename: "[chunkhash].js", // 用于长效缓存
    // 入口分块(entry chunk)」的文件名模板(出口分块?)
    chunkFilename: 'bundle.js', // bundle.js
    //指未被列在 entry 中,却又需要被打包出来的 chunk 文件的名称。
    //一般来说,这个 chunk 文件指的就是要懒加载的代码。
    publicPath: "/assets/", // string
    // 输出解析文件的目录,url 相对于 HTML 页面
}

output有很多配置,我们着重看几个把。

  • filename 输出资源的文件名称。 在多入口的场景中,我们需要为对应产生的每个bundle指定不同的名字。我们可以通过[name]来匹配entry的key名。除了[name]之外,我们还可以用到以下几种:
    • [name] 当前入口的name
    • [id] 当前入口的id
    • [hash] 是对webpack整个一次构建而言
    • [chunkhash] 当前chunk单一内容的hash, 对某个模块而言,它会从入口出发,对依赖文件进行解析,构建对应的chunk和hash值
    • [contenthash] 当前chunk内容的hash,范围更具体和更小,即对某一个文件而言
  • path 可以指定资源输出的位置,要求值必须为绝对路径
  • publicPath(path用来指定资源的输出位置,publicPath则用来指定资源的请求位置。)。webpack-dev-server的配置中也有一个publicPath,但是这个publicPath与Webpack中的配置项含义不同,它的作用是指定webpack-dev-server的静态资源服务路径。

表中的[contenthash]和[chunkhash]都与chunk内容直接相关,在filename中使用了这些变量后,当chunk的内容改变时,资源文件名也会随之更改,从而使用户在下一次请求资源文件时会立即下载新的版本,而不会使用本地缓存。
在实际工程中,我们使用比较多的是[name],它与chunk是一一对应的关系,并且可读性较高。
如果要控制客户端缓存,最好还要加上[chunkhash],因为每个chunk所产生的[chunkhash]只与自身内容有关,单个chunk内容的改变不会影响其他资源,可以最精确地让客户端缓存得到更新。

所以entry定义入口,output定义出口。我们其实是借助webpack的语法,告诉webpack我们的项目如何进入,从这个入口进入后,就像是一个树有其子孙并不断生长展开。然后又如何最终打包输出。

伴随着上面我们描述的过程,我们需要开始了解下面四个概念: entry、module、chunk、bundle。他们四者的关系我们可以简单通过图示来理解:
四者关系

  • entry是入口
  • module为其子孙模块,
  • chunk是分块
  • bundle是打包
    通常情况下,一个entry,对应打包编译过程中的恶一个chunk(但也可能是多个),对应一个输出阶段的bundle。
    如图:

从单页面应用开始提取vendor

假设我们定义1个入口,1个出口,所有内容就包含在1条打包文件里面,这就是一个单页面应用的典型场景了。这样则所有的框架,库,页面模块都是单一的入口进入,只会有一条JS文件,依赖关系很清晰。但问题是当应用规模上升后,产生的资源体积会过大,降低用户访问的页面渲染速度。
在webpack中,默认是当一个bundle的体积大于250KB时(压缩前),webpack会发出warning, 会打印出一个[big]的警告,如下:
体积过大
如何规避这个问题呢?首先想到的就是提取vendor。vendor的字面意思是“供应商”,在Webpack中则一般指工程所使用的库、框架等第三方模块集中打包而产生的bundle。

module.exports = {
    entry:{
        app: "./src/app.js",
        vendor: ["react", "react-dom", "react-router"]
    }
}

我们只是增加了一个vendor的定义,但它还需要基于splitChunks(旧版时是CommonsChunkPlugin)为vendor设置入口路径。
通过这样的配置,app.js产生的bundle将只包含业务模块,其依赖的第三方模块将会被抽取出来生成一个新的bundle,从而达到我们提取vendor的目标。由于vendor仅仅包含第三方模块,这部分不会经常变动,因此可以有效地利用客户端缓存,在用户后续请求页面时加快整体的渲染速度。

多页面应用的场景

对于多页应用的场景,为了尽可能减小资源的体积,会每个页面都只加载各自必要的逻辑,而不是将所有页面打包到同一个bundle中,也就是说,每个页面都需要有一个独立的bundle。则我们可以定义三个入口, 如下:

module.exports = {
    entry:{
        pageA:"./src/pageA.js",
        pageB:"./src/pageB.js",
        pageC:"./src/pageC.js",
        vendor: ["react", "react-dom", "react-router"]
    }
}

入口与页面是一一对应的关系,这样每个HTML只要引入各自的JS就可以加载其所需要的模块。
同样可以使用提取vendor的方法,将各个页面之间的公共模块进行打包。将react和react-dom打包进了vendor,之后再配置optimization.splitChunks,将它们从各个页面中提取出来,生成单独的bundle即可。

一切皆模块

一个Web工程通常会包含HTML、JS、CSS、模板、图片、字体等多种类型的静态资源,且这些资源之间都存在着某种联系。对于Webpack来说,所有这些静态资源都是模块,我们可以像加载一个JS文件一样去加载它们。方式如图:

聊聊loader

我们可以理解loader就是个转换函数,它有各种不同类型的转换函数。它有几点我们需要大致了解

  • loader它可以是链式的。怎么理解? 如: toA(toB(toC(source)))
  • loader的转换结果包含转化后的代码、source-map和AST对象
  • 源码结构:传入的代码,source-map和AST会被处理,且传入回调(为后续链式调用准备)
    module.exports = function loader (content, map, meta) {
        var callback = this.async();
        var result = handler(content, map, meta);
        callback(
            null,           // error
            result.content, // 转换后的内容
            result.map,     // 转换后的 source-map
            result.meta,    // 转换后的 AST
        );
    };
  • loader的配置
    • 引入, 以css-loader为例( npm i css-loader -D )
      module.exports = {
          module: {
              rules: [{
                  test: /\.css$/,
                  use: ['style-loader','css-loader'],
              }],
          },
      };
    • 两个重要参数: test和use
      • test可接收一个正则表达式或者一个元素为正则表达式的数组,只有正则匹配上的模块才会使用这条规则。
      • use可接收一个数组,数组包含该规则所使用的loader。
        • 如果只有一个,其实字符串也行。
        • 如果是数组,则它们是有执行顺序的,从后往前的顺序将资源交给loader处理的,因此要把最后生效的放在最前面
        • 关于loader的自定义配置就通过option来定义, 如:
          rules: [
              {
                  test: /\.css$/,
                  use: [
                      'style-loader',
                      {
                          loader: 'css-loader',
                          options: {
                              // css-loader 配置项
                          },
                      }
                  ],
              },
          ],
    • 其他配置, 如
      • exclude与include, 排除和包含
      • resource与issuer, 加载模块是resource,而加载者是issuer
      • enforce。执行顺序。只接收pre或post两种字符串类型的值。

常用loader

  • babel-loader
  • ts-loader
  • file-loader
  • url-loader(url-loader允许用户设置一个文件大小的阈值,当大于该阈值时它会与file-loader一样返回publicPath,而小于该阈值时则返回base64形式的编码)
    rules: [
        {
            test: /\.(png|jpg|gif)$/,
            use: {
                loader: 'url-loader',
                options: {
                    limit: 10240, //阈值
                    name: '[name].[ext]',
                    publicPath: './assets-path/',
                },
            },
        }
    ],

如何实现一个loader

  • 定义主体,内容就是采用严格模式
    module.exports = function(content) {
        var useStrictPrefix = '\'use strict\';\n\n';
        return useStrictPrefix + content;
    }
  • 开启缓存, webpack中使用this.cacheable控制
    module.exports = function(content) {
        if (this.cacheable) {
            this.cacheable();
        }
        var useStrictPrefix = '\'use strict\';\n\n';
        return useStrictPrefix + content;
    }
  • 获取options

    // 前文我们提过loader的配置项通过use.options传进来
    如下:
    rules: [
        {
            test: /\.js$/,
            use: {
                loader: 'force-strict-loader',
                options: {
                    sourceMap: true,
                },
            },
        }
    ],
    
    //通过安装库loader-utils: npm install loader-utils -D
    //代码中即可以通过它获取: loaderUtils.getOptions(this)
    var loaderUtils = require("loader-utils");
    module.exports = function(content) {
        if (this.cacheable) {
            this.cacheable();
        }
        // 获取和打印 options
        var options = loaderUtils.getOptions(this) || {};
        console.log('options', options);
        // 处理 content
        var useStrictPrefix = '\'use strict\';\n\n';
        return useStrictPrefix + content;
    }

前言-代码分片

实现高性能应用的重要的一点就是尽可能地让用户每次只加载必要的资源,对于优先级不太高的资源则采用延迟加载等技术渐进式获取,这样可以保证页面的首屏速度。代码分片是Webpack作为打包工具所特有的一项技术,通过这项技术,我们可以把代码按照特定的形式进行拆分,使用户不必一次加载全部代码,而是按需加载。

手动配置代码分片

我们肯定会遇到一个页面(多页面多入口的情况也是类似的)中会用到的代码逻辑可以区分为业务自身逻辑代码和公共第三方的模块代码,很显然公共第三方的模块代码是基本不变的,无论哪个页面引用都一样。

参考列表

分类: 互联网技术

0 条评论

发表回复

Avatar placeholder

您的邮箱地址不会被公开。 必填项已用 * 标注

粤ICP备2023023347号-1
error: Content is protected !!