webpack5系列2--拆分js的几种方式

webpack非常受欢迎的一个特性之一就是代码分离,能把一个js按照需求拆分到不通的js中,按需加载或者并行加载这些文件,可以用来控制资源加载优先级,降低首屏加载时间等等,今天就来总结下,webpack中拆分js的几种方式,以及他们的特点和使用场景。

一、Entry point

webpack配置对象中的entry是最简单的、最直观的代码拆分方式,但是这种适用场景也比较有限,主要是用来配置多入口页面。

a.js

import com from './common'
console.log(`a中引入的${com}`)

console.log('a.js')

b.js

import com from './common'

console.log(`b中引入的${com}`)

console.log('b.js');

common.js

import _ from 'lodash';

console.log(
  _.join(['hello', 'common', 'loaded!'], ' ')
);
export default 1

webpack.config.js

const path = require('path')

module.exports = {
    devtool: 'none',
    mode: 'development',
    entry: {
        index: './a.js',
        another: './b.js'
    },
    output: {
        filename: '[name].bundle.js',
        path: path.resolve('dist')
    }
}

编译产出

image.png

可以看到共生成了2个js,能够实现入口的js拆分,这里有个缺陷就是,他们共同依赖的common.js 在2个入口文件中都被打入进去,编译了2次,这个就是入口拆分的局限,再webpack4以前,我们可以通过如下配置解决重复依赖

    plugins: [
      new webpack.optimize.CommonsChunkPlugin({
         name: 'common' // 指定公共 bundle 的名称。
      })
    ],

但是在webpack4以后,CommonsChunkPlugin被移除,取而代之的是 optimization.splitChunks(后面会做详细介绍)

const path = require('path')

module.exports = {
    devtool: 'none',
    mode: 'development',
    entry: {
        a: './a.js',
        b: './b.js'
    },
    output: {
        filename: '[name].bundle.js',
        path: path.resolve('dist')
    },
    optimization: {
        splitChunks: {
          chunks: 'initial', // 同步的js切割,默认是异步
          cacheGroups: {
            defaultVendors: {
              test: /[\\/]node_modules[\\/]/,  // 正则匹配文件,这里匹配的是node_modules
            },
          },
        },
    },
}

这样将入口文件的重复js打包成一个,我们看下编译结果


image.png

共产出了3个js,除了2个入口文件,针对共同依赖的common.js中的lodash,做了单独的打包。

总结

  • entry可以实现代码分隔
  • entry 更适合多入口应用的代码分隔,默认情况下,多入口的共同依赖会分别打包到各入口文件中,造成重复引用
  • 多入口分隔的重复引用模块,可以用optimization.splitChunks作拆分

二、optimization.runtimeChunk

将 optimization.runtimeChunk 设置为 true 或 'multiple',会为每个入口添加一个只含有 runtime 的额外 chunk。

runtime 的 chunk 内容主要是指:在浏览器运行过程中,webpack 用来连接模块化应用程序所需的所有运行时代码。包括已经加载到浏览器中的连接模块逻辑,以及尚未加载模块的延迟加载逻辑。

optimization: {
    // runtimeChunk : true, // 默认名称 runtime~entrypoint.name
    runtimeChunk: {
        // 根据入口,自定义runtime名称
        name: (entrypoint) => `myruntime~${entrypoint.name}`,
    }
},
image.png

如图所示,就多了2个根据入口文件单独创建的2个webpack连接所有模块化应用程序的运行时js。

总结

  • optimization.runtimeChunk 会为每个入口添加一个只含有 runtime 的额外js

import()

ES2020提案 引入的import()函数,支持动态加载模块。

import()返回一个 Promise 对象,调用 import() 之处,被作为分离的模块起点。
import()类似于 Node 的require方法,区别主要是前者是异步加载,后者是同步加载。
如果您在旧版浏览器(例如 IE 11)中使用 import(),请记住使用诸如 es6-promise 或 promise-polyfill 之类的 polyfill 填充 Promise。

多种使用形式

  • 因为返回promise实例,可以搭配Promise.all使用
Promise.all([
  import('./module1.js'),
  import('./module2.js'),
  import('./module3.js'),
])
.then(([module1, module2, module3]) => {
   ···
});

import()也可以用在 async 函数之中。

async function main() {
  const myModule = await import('./myModule.js');
  const {export1, export2} = await import('./myModule.js');
  const [module1, module2, module3] =
    await Promise.all([
      import('./module1.js'),
      import('./module2.js'),
      import('./module3.js'),
    ]);
}
main();

总结

import()主要适用于以下3种情况
(1)按需加载

button.addEventListener('click', event => {
  import('./dialogBox.js')
  .then(dialogBox => {
    dialogBox.open();
  })
  .catch(error => {
    /* Error handling */
  })
});

(2)条件加载

if (condition) {
  import('moduleA').then(...);
} else {
  import('moduleB').then(...);
}

(3)动态的模块路径,根据函数f的返回结果,加载不同的模块。
import()允许模块路径动态生成。

import(f())
.then(...);

require.ensure

require.ensure() 是 webpack 特有的,已经被 import() 取代。可以作为了解看看,

给定 dependencies 参数,将其对应的文件拆分到一个单独的 bundle 中,此 bundle 会被异步加载。当使用 CommonJS 模块语法时,这是动态加载依赖的唯一方法。意味着,可以在模块执行时才运行代码,只有在满足某些条件时才加载依赖项。

var a = require('normal-dep');

if ( module.hot ) {
  require.ensure(['b'], function(require) {
    var c = require('c');

    // Do something special...
  });
}

总结

  • require.ensure() 是 webpack 特有的,已经被 import() 取代,作为了解即可。

optimization.splitChunks

webpack4以后,将所有优化编译的配置整合到了optimization选项中,webpack会根据用户配置整合默认配置,调用内部不同的优化插件,进行编译优化。

代码拆分的选项集合到了 optimization.splitChunks 选项中,内部使用的 SplitChunksPlugin 插件。

默认情况下,SplitChunksPlugin 只会影响按需加载的 chunks,即 chunks: "async"

默认配置

optimization:{  
    splitChunks: {
        // 表示分割chunk的类型 可选值有:async,initial和all
        chunks: "async",
        
        // 表示新分离出的chunk必须大于等于minSize,默认为30000,约30kb。
        minSize: 30000

        // 表示一个模块至少应被minChunks个chunk所包含才能分割。默认为1。
        minChunks: 1,
        
        // 表示按需加载文件时,并行请求的最大数目。默认为30。
        maxAsyncRequests: 30,

        // 表示加载入口文件时,并行请求的最大数目。默认为30。
        maxInitialRequests: 30,

        // 表示拆分出的chunk的名称连接符。默认为~。如chunk~vendors.js
        automaticNameDelimiter: '~',

        // boolean = false  string function (module, chunks, cacheGroupKey) => string,也可用于每个cacheGroup: splitChunks.cacheGroups.{cacheGroup}.name。

        //拆分块的名称。提供false将保持块的相同名称,因此不会不必要地更改名称。这是生产构建的建议值。
        //提供字符串或函数使您可以使用自定义名称。指定字符串或始终返回相同字符串的函数会将所有通用模块和供应商合并为一个块。这可能会导致更大的初始下载量并减慢页面加载速度。
        name: false,
        
        // cacheGroups 下可以配置多个组,每个组根据test设置条件,符合test条件的模块,就分配到该组。
        // 一个 module 可能会满足多个 cacheGroups 的正则匹配,最终根据priority来决定打包到哪个组中,数字越大表示优先级越高。
        // 默认将所有来自 node_modules目录的模块打包至vendors组,将两个以上的chunk所共享的模块打包至default组。
        cacheGroups: {
                svgGroup: {
                    test(module) {
                        // test 也可以为一个函数,接受一个module参数,module.resource是资源的绝对路径
                        const path = require('path');
                        return (
                            module.resource &&
                            module.resource.endsWith('.svg') &&
                            module.resource.includes(`${path.sep}cacheable_svgs${path.sep}`)
                        );
                    },
                },
                vendors: {
                    test: /[\\/]node_modules[\\/]/,
                    priority: -10
                },
                default: {
                    minChunks: 2,
                    priority: -20,
                    reuseExistingChunk: true
                }
            }
        }
  },

总结

  • splitChunks为webpack提供了更多的代码拆分控制在选项。
  • splitChunks配置选项中,我们可以配置更贴合自己项目的性能策略。

建议

关于拆分的一点小意见:

  • 路由代码做动态加载:不多说,这个我们一般都是这么做的。
  • 分离出的代码太小:如果分离出的代码大小只有几kb,比起多发一个网络请求,倒不如索性打包到一起了。
  • 业务和依赖库尽量拆分开:业务代码可能经常上线,每次更改都会改变文件hash,依赖库一般不会变,可以走浏览器缓存。
  • 可以按照功能拆分:对于用户经常访问的页面,可以尽可能小的切割,那么多数都都会缓存,不经常访问的,可以采用动态加载。

参考
webpack的optimization.SplitChunks
阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容