CMD + webpack 实战-减少包大小

1,269 阅读3分钟

commonjs 组件 cmd 化

commonjs 组件 cmd 化只需要加个 define 的头部。

define(function(require, exports, module) {
  const result =   // code here ...
  module.exports = result;
});

一般操作是在组件内部重新打包,并配合插件 wrapper-webpack-plugin 添加 define 头部。webpack 配置如下:

const webpackConfig = {
  ....
  plugins: [
     test: /\.(js|jsx)/,
     header: function header(fileName){
        return `define("componentName", function(require, exports, module){ \n var a =`
     },
     footer: '\n module.exports = a \n});'
  ]
}

组件外部 umd

因为我们是优化需求,得在组件外部,实现 cmd 化。 这个解决方法并不麻烦,创建一个入口文件 xxx-umd.js 将需要打包的组件引入即可。

const xxx = require('xxx')
module.exports = xxxx

此时单纯的我并没有意识到,即将进入一个大坑。

image

问题初现

某个组件升级为 umd 包之后,页面渲染非常慢。原本 1s 即触发完成 FP,升级后时间延长到了 8s。严重影响用户体验。

通过 performance 面板的分析,发现大部分时间在解析 cmd 加载的包。定睛一看包的体积。。。 同样依赖包的情况下,cmd 方式下,组件包的总体积比之前大了 20 倍。

包体积太大了

看了webpack 的配置,以及 webkpack-bundle-analyze 主要是两个问题:

  1. 没有设置 mode:'production'
  2. 公共组件被反复打包。比如 antd,在多个包里出现,还占据了多个包的大部分体积。

问题二的解法很简单,设置 webpack.externals ,使其不再打包 antd.

externals: {
   antd: 'var window.antd'
}

出人意料的 externals

配置之后,antd 依旧被打包... 使用 webkpack-bundle-analyze,可以看到 antd 的压缩包,以及它的大量依赖 rc-xxx

image

externals 原理

externals 允许用户所创建的 bundle 依赖于那些存在于用户环境(consumer's environment)中的依赖。打包文件中少了对应的包,相匹配地 html 也需要引入这些包。externals 具体做的是就是 改写依赖

webpack 文件打包后的文件,由源文件与依赖集合组成,使用 externals 之前,呈现如下:

(function(modules) {
  // webpackBootstrap  源文件具体内容
})({
 "antd": (function(module, exports) {
      // antd 文件内容
   }),
});

使用了 externals 把依赖改写了,变成了 externals 里配置的外部引用。 externals 配置:

{  antd:  'var window.antd', }

重新打包后的文件,antd 模块的引入变成了 window.antd.

(function(modules) {
  // webpackBootstrap  源文件具体内容
})({
 "antd": (function(module, exports) {
       eval("module.exports = window.antd;")
   }
})

只为成功找方法,不为失败找理由

重新翻阅 webapck externals 的文档,一个包引起了我的兴趣——Webpack node modules externals 。这个包可以将所有来自 node_modules 的包都不做打包处理,与我的需求非常契合。

翻看它的源码,发现它使用了 externals 的函数方法:

module.exports = {
  //...
  externals: [
    function(context, request, callback) {
      if (/^yourregex$/.test(request)){
        return callback(null, 'commonjs ' + request);
      }
      callback();
    }
  ]
};

于是我把 externals 改写,当 request 包含了 antd 就依赖外部的 window.antd.

module.exports = {
  //...
  externals: [
    function(context, request, callback) {
      if (/^antd$/.test(request)){
        return callback(null, 'var window.antd');
      }
      callback();
    }
  ]
};

我内心窃喜,运行了打包程序。结果现实又是一记重拳。运行时报错:

cannot find _button

重新翻看打包后的源码,之前的 externals 设置做了错误的改写

// externals 设置前
var _button = __webpack_require__(/*! antd/lib/button */ \"./node_modules/_antd@3.26.15@antd/lib/button/index.js\");

// externals 设置后,引用错误
var _button = __webpack_require__(\"antd\");

剩下的尽管难以置信,但那就是真相

喝了一杯茶,冷静了一下。发现压缩后的源码中,依赖包的 antd 的引用比我想象地复杂 antd/lib/xx, antd

当前的配置强制改写了 antd/lib/xxxxantd, 而我们要做的就是保留 antd/lib/xxxx 的依赖,使其从 window.antd.xxx 中获取 修改了配置为:

externals: [
    function(context, request, callback) {
         if(request === 'antd'){
            return callback(null, 'var window.antd')
          }
          // 保留 antd/lib/xxxx 的依赖, 改写来源
          const [,component] = request.match( /\/lib\/([^\/]+)/) || []
          if(component){
            return callback(null, 'var window.antd.' + getModuleName(component))
          } else {
            return callback();
          }
}]

结果

处理之后效果惊人,包体积缩减回了原来的体积!

招人时间

这波操作有意思吗?支付宝体验技术部招人啦!校招、社招均有,岗位涉及中后台、to c、小程序、cloud IDE ,有意者投简历 anqi.anqifeng@antfin.com.

资料