前端基础知识总结(四)- webpack

时倾

Webpack几个概念:

  • module: 模块,在webpack眼里,任何可以被导入导出的文件都是一个模块。
  • chunk: chunk是webpack拆分出来的:

    • entry chunk:每个入口文件都是一个chunk
    • 通过 import、require 引入的代码
    • children chunk:通过 splitChunks 拆分出来的代码
    • commons chunk: 通过CommonsChunkPlugin创建出来的文件
  • bundle: webpack打包出来的文件,也可以理解为就是对chunk编译压缩打包等处理后的产出。

webpack 优化

问题分析:

  • 核心问题:多页应用打包后代码冗余,文件体积大。
  • 根本原因:相同模块在不同入口之间没有得到复用,bundle之间比较独立。

解决思路:

  1. 解决代码冗余。把不同入口之间,共同引用的模块,抽离出来,放到一个公共模块中。这样不管这个模块被多少个入口引用,都只会在最终打包结果中出现一次。
  2. 减小文件体积。当把这些共同引用的模块都堆在一个模块中,这个文件可能异常巨大,也是不利于网络请求和页面加载的。所以我们需要把这个公共模块再按照一定规则进一步拆分成几个模块文件。

如何拆分,方式因人而异,因项目而异。拆分原则有:

  • 业务代码和第三方库分离打包,实现代码分割;
  • 业务代码中的公共业务模块提取打包到一个模块;
  • 第三方库最好也不要全部打包到一个文件中,因为第三方库加起来通常会很大。可以把特别大的库独立打包,剩下的加起来如果还很大,就把它按照一定大小切割成若干模块。

提取公共模块

通过将公共模块拆出来,最终合成的文件在最开始的时候加载一次,便存到缓存中供后续使用。这个带来速度上的提升,因为浏览器会迅速将公共的代码从缓存中取出来,而不是每次访问一个新页面时,再去加载一个更大的文件。

webpack提供了一个非常好的内置插件帮我们实现这一需求:CommonsChunkPlugin。不过在 webpack4 中CommonsChunkPlugin被删除,取而代之的是optimization.splitChunks

CommonsChunkPlugin

CommonsChunkPlugin 插件,是一个可选的用于建立一个独立文件(又称作 chunk)的功能,这个文件包括多个入口 chunk 的公共模块。

配置选项:
  • name:可以是已经存在的chunk(一般指入口文件)对应的name,那么就会把公共模块代码合并到这个chunk上;否则,会创建名字为name的commons chunk进行合并
  • filename:指定commons chunk的文件名
  • chunks:指定source chunk,即指定从哪些chunk当中去找公共模块,省略该选项的时候,默认就是entry chunks
  • children

    • 指定为true的时候,就代表source chunks是通过entry chunks(入口文件)进行code split出来的children chunks
    • children和chunks不能同时设置,因为它们都是指定source chunks的
    • children 可以用来把 entry chunk 创建的 children chunks 的共用模块合并到自身,但这会导致初始加载时间较长
  • async:即解决children:true时合并到entry chunks自身时初始加载时间过长的问题。async设为true时,commons chunk 将不会合并到自身,而是使用一个新的异步的commons chunk。当这个children chunk 被下载时,自动并行下载该commons chunk
  • minChunks:既可以是数字,也可以是函数,还可以是Infinity, 默认值是2
minChunks含义:
数字:模块被多少个chunk公共引用才被抽取出来成为commons chunk
函数:接受 (module, count) 两个参数,返回一个布尔值,你可以在函数内进行你规定好的逻辑来决定某个模块是否提取成为commons chunk
Infinity:只有当入口文件(entry chunks) >= 3 才生效,用来在第三方库中分离自定义的公共模块
基本使用

1. 分离出第三方库、自定义公共模块、webpack运行文件, 放在同一个文件中

修改webpack.config.js新增一个入口文件vendor, 使用CommonsChunkPlugin插件进行公共模块的提取:

const path = require("path");
const webpack = require("webpack");
const packageJson = require("./package.json");

module.exports = {
  entry: {
    first: './src/first.js',
    second: './src/second.js',
    // 新增一个入口文件vendor
    vendor: Object.keys(packageJson.dependencies)
  },
  output: {
    path: path.resolve(__dirname,'./dist'),
    filename: '[name].js'
  },
  plugins: [
    // 使用CommonsChunkPlugin插件进行公共模块的提取
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      filename: '[name].js'
    }),
  ]
}

生成dist文件夹下文件有:first.js, second.js, vendor.js。

通过查看vendor.js文件,发现first.js和second.js文件中依赖的第三方库和自定义公共模块都被打包进vendor.js中,同时还有webpack的运行文件。

2. 单独分离出第三方库、自定义公共模块、webpack运行文件

plugins: [
  // 抽离第三方库与webpack运行文件
  new webpack.optimize.CommonsChunkPlugin({
    name: ['vendor','runtime'], 
    // 创建runtime.js进行webpack运行文件的抽离,其中source chunks是vendor.js
    filename: '[name].js',
    minChunks: Infinity
  }),
  // 抽离自定义公共模块
  new webpack.optimize.CommonsChunkPlugin({
    name: 'common',
    filename: '[name].js',
    chunks: ['first','second'],// 从first.js和second.js中抽取commons chunk
  }),
]

生成dist文件夹下文件有:first.js, second.js, vendor.js, runtime.js, common.js

splitChunks

属性
  • cacheGroups: cacheGroupssplitChunks配置的核心,在cacheGroups缓存组里配置代码的拆分规则。缓存组的每一个属性都是一个配置规则, 例如配置default属性,属性名可以不叫default可以自己定。属性的值是一个对象。
  • name: 提取出来的公共模块将会以这个来命名,可以不配置,如果不配置,就会生成默认的文件名,大致格式是index/a.js这样的。
  • chunks: 指定哪些类型的chunk参与拆分,值可以是string可以是函数。如果是string,可以是这个三个值之一:all,async,initialall代表所有模块,async代表异步加载的模块, initial代表初始化时就能获取的模块。如果是函数,则可以根据chunk参数的name等属性进行更细致的筛选。
  • minChunks:splitChunks是自带默认配置的,而缓存组默认会继承这些配置,其中有个minChunks属性:

    1. 它控制的是每个模块什么时候被抽离出去:当模块被不同entry引用的次数大于等于这个配置值时,才会被抽离出去。
    2. 它的默认值是1。也就是任何模块都会被抽离出去(入口模块其实也会被webpack引入一次)。
  • minSize

    minSize设置生成文件的最小大小,单位是字节。如果一个模块符合之前所说的拆分规则,但是如果提取出来最后生成文件大小比minSize要小,那它不会被提取出来。这个属性可以在每个缓存组属性中设置,也可以在splitChunks属性中设置,在每个缓存组都会继承这个配置。

  • priority

    priority设置拆分规则的优先级,属性值为数字,可以为负数。当某个模块同时符合一个以上的规则时,通过优先级属性priority来决定使用哪个拆分规则。优先级高者执行。

  • test

    test设置缓存组选择的模块,与chunks属性的作用有一点像,但是维度不一样。test的值可以是一个正则表达式,也可以是一个函数。它可以匹配模块的绝对资源路径或chunk名称,匹配chunk名称时,将选择chunk中的所有模块。

实例

1. 实现代码分离:

//webpack.config.js
optimization: {
  splitChunks: {
    cacheGroups: {
      default: {
        name: 'common',
        chunks: 'initial',
        minChunks: 2, //模块被引用2次以上的才抽离
      }
    }
  }
}

进入dist目录查看:
common.js: 包含引用2次以上的所有模块

2. 分离第三方库与自定义组件库

//webpack.config.js
optimization: {
  splitChunks: {
    minSize: 300,  //提取出的chunk的最小大小
    cacheGroups: {
      default: {
        name: 'common',
        chunks: 'initial',
        minChunks: 2, //模块被引用2次以上的才抽离
        priority: -20,
      },
      // 拆分第三方库(通过npm|yarn安装的库)
      vendors: {
        test: /[\\/]node_modules[\\/]/,
        name: 'vendors',
        chunks: 'initial',
        priority: -10,
      },
      // 拆分指定文件
      locallib: {
         test: /(src\/locallib\.js)$/,
         name: 'locallib',
         chunks: 'initial',
         priority: -9
       }
    }
  }
}

进入dist目录查看:
common.jsvendor.js包含第三方库代码,locallib.js包含locallib模块的代码。

分离动态库

什么是DLL

DLL(Dynamic Link Library)文件为动态链接库文件。在Windows中,许多应用程序并不是一个完整的可执行文件,它们被分割成一些相对独立的动态链接库,即DLL文件,放置于系统中。当我们执行某一个程序时,相应的DLL文件就会被调用。

为什么使用DLL

通常来说,我们的代码都可以至少简单区分成业务代码和第三方库。如果不做处理,每次构建时都需要把所有的代码重新构建一次,耗费大量的时间。然后大部分情况下,很多第三方库的代码并不会发生变更(除非是版本升级),这时就可以用到dll:把复用性较高的第三方模块打包到动态链接库中,在不升级这些库的情况下,动态库不需要重新打包,每次构建只重新打包业务代码。

如何使用

DllPluginDllReferencePlugin 用某种方法实现了拆分 bundles,大幅度提升了构建的速度。使用DLL时,可以把构建过程分成dll构建过程和主构建过程,所以需要两个构建配置文件,例如叫做webpack.config.jswebpack.dll.config.js

1. 使用DLLPlugin打包需要分离到动态库的模块

DllPluginwebpack内置的插件,不需要额外安装,直接配置webpack.dll.config.js文件。此插件用于在单独的 webpack 配置中创建一个 dll-only-bundle,会生成一个名为 manifest.json 的文件,这个文件是用于让 DllReferencePlugin 能够映射到相应的依赖上。

  • context(可选): manifest 文件中请求的 context (默认值为 webpack 的 context)
  • format (boolean = false):如果为 true,则 manifest json 文件 (输出文件) 将被格式化。
  • name:暴露出的 DLL 的函数名(TemplatePaths[fullhash] & [name]
  • path:manifest.json 文件的 绝对路径(输出文件)
  • entryOnly (boolean = true):如果为 true,则仅暴露入口
  • type:dll bundle 的类型
我们建议 DllPlugin 只在 entryOnly: true 时使用,否则 DLL 中的 tree shaking 将无法工作,因为所有 exports 均可使用。
// webpack.dll.config.js

module.exports = {
  entry: {
    // 第三方库
    react: ['react', 'react-dom', 'react-redux']
  },
  output: {
    // 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称,
    filename: '[name].dll.js',
    path: resolve('dist/dll'),
    // library必须和后面dllplugin中的name一致
    library: '[name]_dll_[hash]'
  },
  plugins: [
    new webpack.DllPlugin({
      // 动态链接库的全局变量名称,需要和 output.library 中保持一致
      // 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
      name: '[name]_dll_[hash]',
      // 描述动态链接库的 manifest.json 文件输出时的文件名称
      path: path.join(__dirname, 'dist/dll', '[name].manifest.json')
    }),
  ]
}

2. 在主构建配置文件使用DllReferencePlugin引用动态库文件

webpack.config.js中使用dll要用到DllReferencePlugin, 此插件会把 dll-only-bundles 引用到需要的预编译的依赖中。

  • context:(绝对路径) manifest (或者是内容属性)中请求的上下文
  • extensions:用于解析 dll bundle 中模块的扩展名 (仅在使用 'scope' 时使用)。
  • manifest :包含 contentname 的对象,或者是一个字符串 —— 编译时用于加载 JSON manifest 的绝对路径
  • content (可选): 请求到模块 id 的映射(默认值为 manifest.content
  • name (可选):dll 暴露地方的名称(默认值为 manifest.name)(可参考externals
  • scope (可选):dll 中内容的前缀
  • sourceType (可选):dll 是如何暴露的 (libraryTarget)

通过引用 dll 的 manifest 文件来把依赖的名称映射到模块的 id 上,之后再在需要的时候通过内置的 __webpack_require__ 函数来 require 对应的模块。

new webpack.DllReferencePlugin({
  context: __dirname,
  manifest: require('./dist/dll/react.manifest.json')
}),

第一步产出的manifest文件就用在这里,给主构建流程作为查找dll的依据:DllReferencePlugin去 manifest.json 文件读取 name 字段的值,把值的内容作为在从全局变量中获取动态链接库中内容时的全局变量名,因此:在 webpack.dll.config.js 文件中,DllPlugin 中的 name 参数必须和 output.library 中保持一致。

3. 在入口文件引入dll文件

生成的dll暴露出的是全局函数,因此还需要在入口文件里面引入对应的dll文件。

<body>
  <div id="app"></div>
  <!--引用dll文件-->
  <script src="../../dist/dll/react.dll.js"></script>
</body>

使用DLL作用

1.分离代码,业务代码和第三方模块可以被打包到不同的文件里,这个有几个好处:

  • 避免打包出单个文件的大小太大,不利于调试
  • 将单个大文件拆成多个小文件之后,一定情况下有利于加载(不超出浏览器一次性请求的文件数情况下,并行下载肯定比串行快)

2.提升构建速度。第三方库没有变更时,由于我们只构建业务相关代码,相比全部重新构建自然要快的多。

移除不必要的文件

moment.js日期处理库,占用很大的体积, 因为所有的locale文件都被引入,而这些文件在整个库的体积中占了大部分,因此当webpack打包时移除这部分内容会让打包文件的体积有所减小。

webpack自带的两个库可以实现这个功能:

  • IgnorePlugin
  • ContextReplacementPlugin

IgnorePlugin的使用方法如下:

// 插件配置
plugins: [
  // 忽略moment.js中所有的locale文件
  new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
],
  
// 使用方式
const moment = require('moment');
// 引入zh-cn locale文件
require('moment/locale/zh-cn');
moment.locale('zh-cn');

ContextReplacementPlugin的使用方法如下:

// 插件配置
plugins: [
  // 只加载locale zh-cn文件
  new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /zh-cn/),
],
  
// 使用方式
const moment = require('moment');
moment.locale('zh-cn');

模块化引入

在项目中使用了lodash这个很常用的工具库,然而在使用这类工具库的时候往往只使用到了其中的很少的一部分功能,但却把整个库都引入了。因此这里也可以进一步优化,只引用需要的部分。

import { chain, cloneDeep } from 'lodash';
// 或者
import chain from 'lodash/chain';
import cloneDeep from 'lodash/cloneDeep';

压缩混淆代码

我们平常也会对代码进行压缩混淆,可以通过UglifyJS等工具来对js代码进行压缩,同时可以去掉不必要的空格、注释、console信息等,也可以有效的减小代码体积。

webpack 基本功能

webpack hash区别

hash一般是结合CDN缓存来使用,通过webpack构建之后,生成对应文件名自动带上对应的MD5值。如果文件内容改变的话,那么对应文件哈希值也会改变,对应的HTML引用的URL地址也会改变,触发CDN服务器从源服务器上拉取对应数据,进而更新本地缓存。

  • hash

hash是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改。同一次构建过程中生成的哈希都是一样的。

output:{
  path:path.join(__dirname, '/dist'),
  filename: 'bundle.[name].[hash].js',
}
  • chunkhash

根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值。把一些公共库和程序入口文件区分开,单独打包构建,接着我们采用chunkhash的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响。

output:{
  path:path.join(__dirname, '/dist/js'),
  filename: 'bundle.[name].[chunkhash].js',
}

采用chunkhash,项目主入口文件Index.js及其对应的依赖文件Index.css由于被打包在同一个模块,共用相同的chunkhash。由于公共库是不同的模块,有单独的chunkhash。所以Index文件的更改不会影响公共库。如果index.js更改了代码,css未改变,由于该模块发生了改变,导致css文件会重复构建。

  • contenthash

根据文件内容创建出唯一 hash。当文件内容发生变化时,[contenthash] 才会发生变化。

output: {
  filename: '[name].[contenthash].js',
  chunkFilename: '[name].[contenthash].js',
  path: path.resolve(__dirname, '../dist'),
}

模块热更新

模块热替换(hot module replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新所有类型的模块,而无需完全刷新。

实现

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');

{
  devServer: {
    contentBase: './dist',
    hot: true, // DevServer开启模块热替换模式
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Hot Module Replacement',
    }),
  ],
}

filename 和 chunkFilename 的区别

filename列在 entry 中,打包后输出的文件的名称。

chunkFilename未列在 entry 中,却又需要被打包出来的文件的名称。默认使用 [id].js 或从 output.filename 中推断出的值([name] 会被预先替换为 [id] 或 [id].)。

// webpack.config.js

module.exports =  {
  entry: './src/index.js',
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    chunkFileName: '[name].bundle.js'
  }
}

CSS Modules 模块化

现状

因为 css 不是编程语言,所以不能声明变量、函数,不能做判断、循环和计算,也不能嵌套。为了解决这个问题,衍生了两种拓展语言 lesssass,它们兼容 css,并且拓展了编程的功能,主要是带来了以下的特性:

  • 可以声明变量、函数,可以进行一些简单的计算、判断、循环;
  • 可以嵌套选择器,这样节省了书写的内容,也更具阅读性;
  • @import 避免重复导入问题,因此可以放心大胆的导入其他文件。

从模块化的角度来讲,lesssass 只是扩充了 css 的功能,但并没有在语言的层面做模块化,因为全局命名冲突的问题依然还在。

实现模块化

想要让 css 具备模块化功能,暂时还不能从语言的层面来考虑,所以只能从工具的角度来实现。目前比较好的方式是使用 js 来加载 css 文件,并将 css 的内容导出为一个对象,使用 js 来渲染整个 dom 树和匹配相应的样式到对应的元素。

css文件建议遵循如下原则

  • 不使用选择器与id,只使用 class 名来定义样式(因为只有 .class 才能导出为对象的属性)
  • 不层叠多个 class,只使用一个 class 把所有样式定义好
  • 所有样式通过 composes 组合来实现复用
  • 不嵌套
  • 推荐用 .className 书写,而非 .class-name(前者可以通过 styles.className 访问,后者需要通过 styles['class-name'] 才能访问)。

实例

/* dialog.css */
.root {}
.confirm {}
.disabledConfirm {}

js文件引入dialog.css, 使用 classnames 库来操作 class 名:

/* dialog.jsx */
import classNames from 'classnames';
import styles from './dialog.css';

export default class Dialog extends React.Component {
  render() {
    const cx = classNames({
      [styles.confirm]: !this.state.disabled,
      [styles.disabledConfirm]: this.state.disabled
    });

    return <div className={styles.root}>
      <a className={cx}>Confirm</a>
      ...
    </div>
  }
}
如果你不想频繁的输入 styles.**,可以试一下 react-css-modules,它通过高阶函数的形式来避免重复输入 styles.**

依赖webpack: css-loader

这个功能需要构建工具的支持,如果使用 webpack ,可以使用 css-loader,并设置 options.modulestrue, 便可使用模块化的功能了。
css-loader 解析@import和 url() ,会 import/require() 后再解析(resolve)它们。
css-loader配置项:

名称类型默认值描述
rootString root值将被添加到 URL 前面,然后再进行转译。因为对于以 / 开头的 URL,默认行为是不转译。
urlBooleantrue启用/禁用解析 url()
aliasObject{}给url创建别名。用别名重写你的 URL,在难以改变输入文件的url 路径时,这会很有帮助。
importBooleantrue启用/禁用 @import 处理
minimizeBoolean\Objectfalse启用/禁用 压缩
sourceMapBooleanfalse启用/禁用 Sourcemap
importLoadersNumber0在 css-loader 前应用的 loader 的数量
modulesBooleanfalse启用/禁用 CSS 模块
camelCaseBoolean\Stringfalse是否以驼峰化式命名导出类名
localIdentNameString[hash:base64]配置生成的类名标识符(ident)
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/i,
        loader: "css-loader",
        options: {
          modules: true,
        },
      },
    ],
  },
};

loader 和 plugin 的区别

loader

是一个转换器,将A文件进行编译成B文件,比如:将A.less转换为A.css,单纯的文件转换过程。
是一个导出为function的node模块。可以将匹配到的文件进行一次转换,同时loader可以链式传递。

plugin

是一个扩展器,通过钩子可以涉及整个构建流程,可以做一些在构建范围内的事情。
它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务。

常用loader

  • 样式:style-loader、css-loader、less-loader、sass-loader等
  • 文件:raw-loader、file-loader 、url-loader等
  • 编译:babel-loader、coffee-loader 、ts-loader等
  • 校验测试:mocha-loader、jshint-loader 、eslint-loader等

sass-loader转化sass为css文件,并且包一层module.exports成为一个js module。

css-loader解析@import和 url() 。

style-loader将创建一个style标签将css文件嵌入到html中。

vue-loader、coffee-loader、babel-loader等可以将特定文件格式转成js模块、将其他语言转化为js语言和编译下一代js语言。

file-loader可以处理资源,file-loader可以复制和放置资源位置,并可以指定文件名模板,用hash命名更好利用缓存。

url-loader可以处理资源, 将小于配置limit大小的文件转换成内敛Data Url的方式,减少请求。

raw-loader可以将文件以字符串的形式返回

imports-loader、exports-loader可以向模块注入变量或者提供导出模块功能。

常用Plugin

  • webpack内置UglifyJsPlugin,压缩和混淆代码。
  • webpack内置CommonsChunkPlugin,将指定的模块或公用模块打包出来,减少主bundle文件的体积,配合缓存策略,加快应用访问速度。
  • webpack 内置DllPluginDllReferencePlugin相互配合,前置第三方包的构建,只构建业务代码,同时能解决Externals多次引用问题。DllReferencePlugin引用DllPlugin配置生成的manifest.json文件,manifest.json包含了依赖模块和module id的映射关系
  • html-webpack-plugin可以根据模板自动生成html代码,并自动引用css和js文件
  • extract-text-webpack-plugin 将js文件中引用的样式单独抽离成css文件
  • HotModuleReplacementPlugin 热更新
  • optimize-css-assets-webpack-plugin 不同组件中重复的css可以快速去重
  • webpack-bundle-analyzer 一个webpack的bundle文件分析工具,将bundle文件以可交互缩放的treemap的形式展示。
  • compression-webpack-plugin 生产环境可采用gzip压缩JS和CSS
  • happypack:通过多进程模型,来加速代码构建
  • clean-wenpack-plugin 清理每次打包后没有使用的文件

webpack文件分离思想

现状

为什么要分离第三方库?

第三方库是比较稳定,不会轻易改变的,利用浏览器缓存后,用户再次加载页面会减少服务器请求,提高速度优化体验。提取多个应用(入口)公共模块的作用和他类似,公共部分会被缓存,所有应用都可以利用缓存内容从而提高性能。

分离第三方库就能利用浏览器换缓存了么?

答案是否定的。导致无法利用缓存的因素有很多,比如每次分离的库文件重新打包都会得到不同的名称,后台的同事给js文件设置的缓存过期时间为0,只要文件是完全不变的,包括修改时间,文件内容等,依然会利用缓存。

浏览器缓存机制是什么样的?

HTTP1.1给的策略是使用Cache-control配合Etag。
Apache中,ETag的值默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的。如果Etag相同,依然不会请求新资源,而会使用以前的文件。

文件分离插件

CommonsChunkPlugin与SplitChunksPlugin

作用

将公共模块抽离。每次打包的时候都会重新打包,还是会去处理一些第三方依赖库,只是它能把第三方库文件和我们的代码分开掉,生成一个独立的 js 文件。但是它还是不能提高打包的速度

自 webpack 4.0 上线之后,CommonsChunkPlugin 已被替换成 SplitChunksPlugin,旨在优化 chunk 的拆分。

CommonsChunkPlugin

设计思路:满足 minChunks 的引用次数时,都会将对应的模块抽离如一个新的 chunk 文件中,这个文件为所有的业务文件的父级。

这种设计思路带来了会造成模块打包冗余。总的来说会造成这么几个问题:

  • 产出的 chunk 在引入时,会包含重复的代码;
  • 无法优化异步 chunk;
  • 高优的 chunk 产出需要的 minchunks 配置比较复杂。

SplitChunksPlugin

SplitChunksPlugin 优化了 webpack 的打包策略,使用自动重复算法,会自动计算出各页面公共的包引用以及部分页面公共的包引用,当然,对于那些部分共有但是阈值过小的文件其不会创建单独的输出文件,因为其大小不值得去新开一个请求。(缓存策略配置在 cacheGroup 中)

SplitChunksPlugin 默认的分包策略基于以下 4 个条件:

  1. 新代码块可以被共享引用,或这些模块都是来自 node_modules;
  2. 新产出的 vendor-chunk 的大小得大于 30kb;
  3. 按需加载的代码块(vendor-chunk)并行请求的数量不多于 5 次;
  4. 初始加载的代码块,并行请求的数量不多于 3 次。
  • SplitChunksPlugin 配合使用 RuntimeChunk 对运行时的 hash 变动做优化(相当于 CommonsChunkPlugin 的两次使用)
  • 减少 maxInitial/AsyncRequest 会加大 module 的冗余,但是会进一步的减少请求。
DllPlugin与DllReferencePlugin

使用

DLLPlugin 这个插件是在一个额外独立的 webpack 设置中创建一个只有 dll 的 bundle,也就是说,除了 webpack.config.js,项目中还会新建一个 webpack.dll.config.js 文件来配置 dll 的打包。webpack.dll.config.js 作用是把所有的第三方库依赖打包到一个 bundle 的 dll 文件里面,还会生成一个名为 manifest.json 文件。该 manifest.json 的作用是用来让 DllReferencePlugin 映射到相关的依赖上去的。(可类比 CommonsChunkPlugin 的两次打包或者 RuntimeChunk 的运行包配置)

设计思路

DLLPlugin 是提前将公共的包构建出来,使得在 build 时过滤掉这些构建过的包,使得在正是构建时的速度缩短。所以其相对来说打包速度会更快

推荐使用策略

  • 如果是单页应用,只用DllPlugin打包库文件即可,业务代码一个包搞定。
  • 如果是多页应用,DllPlugin打包库文件,如果有很多公共的业务代码而且可能随时变动,就需要使用CommonsChunkPlugin提取公共业务代码。在页面间切换时,公共部分还是会被缓存的。

参考:
webpack 文件分离思想

webpack运行机制

webpack的运行过程可以简单概述为如下流程:

初始化配置参数 -> 绑定事件钩子回调 -> 确定Entry逐一遍历 -> 使用loader编译文件 -> 输出文件

webpack事件流

什么是webpack事件流?

Webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。Webpack 通过 Tapable 来组织这条复杂的生产线。 Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 Webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。 -- 吴浩麟《深入浅出webpack》

我们将webpack事件流理解为webpack构建过程中的一系列事件,他们分别表示着不同的构建周期和状态,我们可以像在浏览器上监听click事件一样监听事件流上的事件,并且为它们挂载事件回调。我们也可以自定义事件并在合适时机进行广播,这一切都是使用了webpack自带的模块 Tapable 进行管理的。我们不需要自行安装 Tapable ,在webpack被安装的同时它也会一并被安装,如需使用,我们只需要在文件里直接 require 即可。

Tapable的原理

Tapable的原理其实就是我们在前端进阶过程中都会经历的EventEmit,通过发布者-订阅者模式实现,它的部分核心代码可以概括成下面这样:

class SyncHook{
    constructor(){
        this.hooks = [];
    }

    // 订阅事件
    tap(name, fn){
        this.hooks.push(fn);
    }

    // 发布
    call(){
        this.hooks.forEach(hook => hook(...arguments));
    }
}

webpack运行流程详解

  • 首先,webpack会读取你在命令行传入的配置以及项目里的 webpack.config.js 文件,初始化本次构建的配置参数,并且执行配置文件中的插件实例化语句,生成Compiler传入plugin的apply方法,为webpack事件流挂上自定义钩子。
  • 接下来到了entryOption阶段,webpack开始读取配置的Entries,递归遍历所有的入口文件
  • Webpack进入其中一个入口文件,开始compilation过程。先使用用户配置好的loader对文件内容进行编译(buildModule),我们可以从传入事件回调的compilation上拿到module的resource(资源路径)、loaders(经过的loaders)等信息;之后,再将编译好的文件内容使用acorn解析生成AST静态语法树(normalModuleLoader),分析文件的依赖关系逐个拉取依赖模块并重复上述过程,最后将所有模块中的require语法替换成__webpack_require__来模拟模块化操作。
  • emit阶段,所有文件的编译及转化都已经完成,包含了最终输出的资源,我们可以在传入事件回调的compilation.assets 上拿到所需数据,其中包括即将输出的资源、代码块Chunk等等信息。

参考:
Webpack揭秘——走向高阶前端的必经之路

阅读 517

把梦想放在心中

353 声望
2.3k 粉丝
0 条评论
你知道吗?

把梦想放在心中

353 声望
2.3k 粉丝
宣传栏