库的打包优化 - childlabor/blog GitHub Wiki
前言
通常情况下组件库会有其他的第三方依赖,比如通用的dayjs、基于vue开发的会依赖vue、element-ui的二次封装库会依赖element-ui。
然而,如果将第三方依赖全部打包到组件库的bundle文件中,会使组件库的体积变得非常臃肿。做为被项目引用的组件库,缩小体积是非常重要的。
优化一
webpack为我们提供了一个很好的解决方案:
外部扩展(externals)
externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法。相反,所创建的 bundle 依赖于那些存在于用户环境(consumer's environment)中的依赖。此功能通常对 __library 开发人员__来说是最有用的,然而也会有各种各样的应用程序用到它。
先来看下未配置externals情况下的bundle文件:
// 截取片段
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
...
/***/ "23e7":
/***/ (function(module, exports, __webpack_require__) {
var global = __webpack_require__("da84");
var getOwnPropertyDescriptor = __webpack_require__("06cf").f;
var createNonEnumerableProperty = __webpack_require__("9112");
var redefine = __webpack_require__("6eeb");
var setGlobal = __webpack_require__("ce4e");
var copyConstructorProperties = __webpack_require__("e893");
var isForced = __webpack_require__("94ca");
/*
options.target - name of the target object
options.global - target is the global object
options.stat - export as static methods of target
options.proto - export as prototype methods of target
options.real - real prototype method for the `pure` version
options.forced - export even if the native feature is available
options.bind - bind methods to the target, required for the `pure` version
options.wrap - wrap constructors to preventing global pollution, required for the `pure` version
options.unsafe - use the simple assignment of property instead of delete + defineProperty
options.sham - add a flag to not completely full polyfills
options.enumerable - export as enumerable property
options.noTargetGet - prevent calling a getter on target
*/
module.exports = function (options, source) {
var TARGET = options.target;
var GLOBAL = options.global;
var STATIC = options.stat;
var FORCED, target, key, targetProperty, sourceProperty, descriptor;
if (GLOBAL) {
target = global;
} else if (STATIC) {
target = global[TARGET] || setGlobal(TARGET, {});
} else {
target = (global[TARGET] || {}).prototype;
}
if (target) for (key in source) {
sourceProperty = source[key];
if (options.noTargetGet) {
descriptor = getOwnPropertyDescriptor(target, key);
targetProperty = descriptor && descriptor.value;
} else targetProperty = target[key];
FORCED = isForced(GLOBAL ? key : TARGET + (STATIC ? '.' : '#') + key, options.forced);
// contained in target
if (!FORCED && targetProperty !== undefined) {
if (typeof sourceProperty === typeof targetProperty) continue;
copyConstructorProperties(sourceProperty, targetProperty);
}
// add a flag to not completely full polyfills
if (options.sham || (targetProperty && targetProperty.sham)) {
createNonEnumerableProperty(sourceProperty, 'sham', true);
}
// extend global
redefine(target, key, sourceProperty, options);
}
};
/***/ }),
...
/***/ "4160":
/***/ (function(module, exports, __webpack_require__) {
"use strict";
var $ = __webpack_require__("23e7");
var forEach = __webpack_require__("17c2");
// `Array.prototype.forEach` method
// https://tc39.github.io/ecma262/#sec-array.prototype.foreach
$({ target: 'Array', proto: true, forced: [].forEach != forEach }, {
forEach: forEach
});
/***/ }),
...
// EXTERNAL MODULE: ./node_modules/core-js/modules/es.array.for-each.js
var es_array_for_each = __webpack_require__("4160");
以上为webpack打包按需引入core-js
库的es.array.for-each.js
特性的部分代码。
我们可以看到webpack的__webpack_require__
函数方法,通过接收由moduleId
键对应的模块最终return了一个module.exports
。并且里面串联的大量的moduleId
,代码量巨大。
再来看下同一项目在配置了externals后,bundle文件的代码:
/***/ "139f":
/***/ (function(module, exports) {
module.exports = require("core-js/modules/es.array.for-each");
/***/ }),
// EXTERNAL MODULE: external "core-js/modules/es.array.for-each"
var es_array_for_each_ = __webpack_require__("139f");
同样是按需引入core-js
库的es.array.for-each.js
。也是__webpack_require__
,但这里只用了一个moduleId
,并且最终返回的是一个require运行时:require("core-js/modules/es.array.for-each")。也就是说,它会通过外部项目(引入了该库)的node_module来获取需要的依赖,这样可以避免重复的依赖。
当然还是需要在package.json
里配置必要的peerDependencies
,当外部项目没有install库所需的依赖时进行安装提示。
优化二
很多时候,组件库的组件可能会需要引入该组件库的其他基础组件。
写法如下:
// table-head-filter.vue
import FilterInput from '../../filter-input';
export default {
name: 'TableHeadFilter',
components: {
FilterInput
},
...
}
先来分析下此种写法下,独立模块打包出来的TableHeadFilter.js:
/* harmony default export */ var filtervue_type_script_lang_js_ = ({
name: 'FilterInput',
components: {
filterInput: input,
filterSelect: src_select,
filterDate: date
},
model: {
prop: 'value',
event: 'changeValue'
},
props: {}
...
isObjEmpty: function isObjEmpty(obj) {
var empty = true;
var targetList = this.filterListAll.length ? this.filterListAll : this.filterList;
var _loop3 = function _loop3(key) {
var current = targetList.find(function (i) {
return i.keyword === key;
});
if (current && (obj[key] && obj[key] !== '' || obj[key] === 0)) empty = false;
};
for (var key in obj) {
_loop3(key);
}
return JSON.stringify(obj) === '{}' || empty;
}
}
});
// CONCATENATED MODULE: ./packages/filter-input/src/filter.vue?vue&type=script&lang=js&
/* harmony default export */ var src_filtervue_type_script_lang_js_ = (filtervue_type_script_lang_js_);
// CONCATENATED MODULE: ./packages/filter-input/src/filter.vue
可以明显的看出,webpack将该组件内引入的其他基础组件的所有源码都打包在一起了。如果引入多个基础组件或引入的基础组件较大,那么该组件==单独打包==的文件体积将大大增加。当然,因为webpack的打包优化策略,整体打包的bundle文件并没有太大影响,依旧会合并提取。
因此,如果库有按需加载需求,需要组件独立打包的,需要注意进行此项优化。
优化方式依旧是通过externals
。以下为具体实现片段节选:
// config.js
const path = require('path');
const nodeExternals = require('webpack-node-externals');
const Components = require('../components.map.json');
let externals = {};
// 关键
Object.keys(Components).forEach(function(key) {
externals[`fb-ui-component/packages/${key}`] = `fb-ui-component/lib/${key}`;
});
externals = [Object.assign({}, externals), nodeExternals()];
exports.externals = externals;
exports.alias = {
'@': path.resolve(__dirname, '../src'),
// 关键
'fb-ui-component': path.resolve(__dirname, '../')
};
// webpack.components.js
const config = require('./config');
...
externals: config.externals,
resolve: {
alias: config.alias
},
...
// table-head-filter.vue
// 关键
import FilterInput from 'fb-ui-component/packages/filter-input';
export default {
name: 'TableHeadFilter',
components: {
FilterInput
},
...
}
可以看到在import
组件时写法有所不同。前缀取库项目名称,本地运行时,由于配置了alias
,'fb-ui-component'即根目录,依旧会去匹配对应的组件。同时,config里通过components.map
遍历了所有组件并加到externals配置。构建时,webpack会将fb-ui-component
当做第三方的依赖(依赖自己),并依照“优化一”的方法进行打包。
来看下打包源码TableHeadFilter.js:
// EXTERNAL MODULE: external "fb-ui-component/lib/filter-input"
var filter_input_ = __webpack_require__(7);
var filter_input_default = /*#__PURE__*/__webpack_require__.n(filter_input_);
...
/***/ 7:
/***/ (function(module, exports) {
module.exports = require("fb-ui-component/lib/filter-input");
/***/ })
不再将整个基础组件打包入文件,而是使用了require。
效果
通过以上两点优化,构建后的库体积极大减小了,看以下对比:
ui-component.umd.min.js 48kb -> 26kb
table-head-filter.js 106kb -> 7kb
效果显著。