注册

一种简单实用的 JS 动态加载方案

背景


在做 Web 应用的时候,你或许遇到过这样的场景:为了实现一个使用率很低的功能,却引入了超大的第三方库,导致项目打包后的 JS bundle 体积急剧膨胀。


我们有一些具体的案例,例如:


产品要求在项目中增加一个导出数据为 Excel 文件的功能,这个功能其实只有管理员才能看到,而且最多一周才会使用一次,绝对属于低频操作。


团队里的小伙伴为了实现这个功能,引入了 XLSX 这个库,JS bundle 体积因而增加了一倍,所有用户的体验都受到影响了。


XLSX 用来做 Excel 相关的操作是不错的选择,但因为新增低频操作影响全部用户却不值得。


除了导出 Excel 这种功能外,类似的场景还有使用 html2canvas 生成并下载海报,使用 fabric 动态生成图片等。


针对这种情况,你觉得该如何优化呢?

自动分包和动态加载


机智如你很快就想到使用 JS 动态加载,如果熟悉 React,还知道可以使用 react-loadable 来解决。


原理就是利用 React Code-Splitting,配合 Webpack 自动分包,动态加载。


这种方案可以,React 也推荐这么做,但是对于引用独立的第三方库这样的场景,还有更简单的方案。

更简单的方案


这些第三方库往往都提供了 umd 格式的 min.js,我们动态加载这些 min.js 就可以了。比如 XLSX,引入其 min.js 文件之后,就可以通过 window.XLSX 来实现 Excel 相关的操作。


此方案的优点有:



  • 与框架无关,不需要和 React 等框架或 Webpack 等工具绑定
  • 精细控制,React Code-Splitting 之类的方案只能到模块级别,想要在点击按钮后才动态加载较难实现

具体实现

我们重点需要实现一个 JS 动态加载器 AsyncLoader,代码如下:

function initLoader() {
// key 是对应 JS 执行后在 window 中添加的变量
const jsUrls = {
html2canvas: 'https://cdn.jsdelivr.net/npm/html2canvas@1.0.0-rc.7/dist/html2canvas.min.js',
XLSX: 'https://cdn.jsdelivr.net/npm/xlsx@0.16.9/dist/xlsx.min.js',
flvjs: 'https://cdn.jsdelivr.net/npm/flv.js@1.5.0/dist/flv.min.js',
domtoimage: 'https://cdn.jsdelivr.net/npm/dom-to-image@2.6.0/src/dom-to-image.min.js',
fabric: 'https://cdn.jsdelivr.net/npm/fabric@4.3.1/dist/fabric.min.js',
};

const loadScript = (src) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.type = 'text/javascript';
script.onload = resolve;
script.onerror = reject;
script.crossOrigin = 'anonymous';
script.src = src;
if (document.head.append) {
document.head.append(script);
} else {
document.getElementsByTagName('head')[0].appendChild(script);
}
});
};

const loadByKey = (key) => {
// 判断对应变量在 window 是否存在,如果存在说明已加载,直接返回,这样可以避免多次重复加载
if (window[key]) {
return Promise.resolve();
} else {
if (Array.isArray(jsUrls[key])) {
return Promise.all(jsUrls[key].map(loadScript));
}
return loadScript(jsUrls[key]);
}
};

// 定义这些方法只是为了方便使用,其实 loadByKey 就够了。
const loadHtml2Canvas = () => {
return loadByKey('html2canvas');
};

const loadXlsx = () => {
return loadByKey('XLSX');
};

const loadFlvjs = () => {
return loadByKey('flvjs');
};

window.AsyncLoader = {
loadScript,
loadByKey,
loadHtml2Canvas,
loadXlsx,
loadFlvjs,
};
}

initLoader();

使用方式


以 XLSX 为例,使用这种方式之后,我们不需要在顶部 import xlsx from 'xlsx',只有当用户点击 导出Excel 按钮的时候,才从 CDN 动态加载 xlsx.min.js,加载成功后使用 window.XLSX 即可,代码如下:

await window.AsyncLoader.loadXlsx().then(() => {
const XLSX = window.XLSX;
if (resp.data.signList && resp.data.signList.length > 0) {
const new_workbook = XLSX.utils.book_new();

resp.data.signList.map((item) => {
const header = ['班级/学校/单位', '姓名', '帐号', '签到时间'];
const { signRecords } = item;
signRecords.unshift(header);

const worksheet = XLSX.utils.aoa_to_sheet(signRecords);
XLSX.utils.book_append_sheet(new_workbook, worksheet, item.signName);
});

XLSX.writeFile(new_workbook, `${resp.data.fileName}.xlsx`);
} else {
const new_workbook = XLSX.utils.book_new();
const header = [['班级/学校/单位', '姓名', '帐号']];
const worksheet = XLSX.utils.aoa_to_sheet(header);
XLSX.utils.book_append_sheet(new_workbook, worksheet, '');
XLSX.writeFile(new_workbook, `${resp.data.fileName}.xlsx`);
}
});

另一个动态加载 domtoimage 的示例

window.CommonJsLoader.loadByKey('domtoimage').then(() => {
const scale = 2;
window.domtoimage
.toPng(poster, {
height: poster.offsetHeight * scale,
width: poster.offsetWidth * scale,
style: {
zoom: 1,
transform: `scale(${scale})`,
transformOrigin: 'top left',
width: `${poster.offsetWidth}px`,
height: `${poster.offsetHeight}px`,
},
})
.then((dataUrl) => {
copyImage(dataUrl, liveData?.planName);
message.success(`${navigator.clipboard ? '复制' : '下载'}成功`);
});
});

AsyncLoader 方案使用方便、理解简单,而且可以很好地利用 CDN 缓存,多个项目可以共用同样的 URL,进一步提高加载速度。而且这种方式使用的是原生 JS,在任何框架中都可以使用。


注意,如果你用 TypeScript 开发,这种方案或许会丢失一些智能提示,如果引入了对应的 @types/xxx 应该没影响。如果你特别在意开发时的智能提示,也可以在开发的过程中 import 对应的包,开发完成后才换成 AsyncLoader 方案。

原文:https://juejin.cn/post/6953193301289893901

0 个评论

要回复文章请先登录注册