前端性能优化进阶篇——动态加载模块基础补遗

13,420 阅读5分钟

背景介绍

这是设计模式系列的第四节,学习的是patterns.dev里设计模式中模块模式内容,由于是资料是英文版,所以我的学习笔记就带有翻译的性质,但并不是翻译,记录的是自己的学习过程和理解

第一节:高并发造成的数据统计困难?看我单例模式一招制敌

第二节:JS和迪丽热巴一样有专业替身?没听过的快来补补课...

第三节:还在层层传递props?来学学非常实用的供应商模式吧

第四节:都知道JavaScript原型,但设计模式里的原型模式你会用吗?

第五节:React Hooks时代,怎么实现视图与逻辑分离呢?

第六节:是时候拿出高级的技术了————观察者模式

写在前面

现在相信大家对于模块化应该会有很直观的感受,比如我们最常用的node_modules就是模块化最成功的应用了,而且这种应用是工程架构级别的,对所有前端应用都可以使用。当然我们今天说的模块化模式则是主要针对具体代码的组织形式上来说的。

极简释义

把代码拆分成更小的模块,降低耦合性,提高复用性。

正文

随着项目发展,代码库也越来越大,保持代码的可维护性解耦合越来越重要。模块化这种设计模式就可以把代码分成若干个复用性强的小模块。

模块化模式还有很多其他好处,比如说:在module中保护私有变量,降低命名冲突全局污染,在复杂场景还可以实现动态加载

内部的声明都是默认被封闭在模块内的, 不直接export的话,在模块外是不可用的。

ES2015 Modules

ES2015新增了JS语言内置的modules。一个模块是一个JS代码组成的文件,和普通的脚本在使用时有一些区别。

下面我们来看一个例子:math.js中包含一些常用的数学计算:

function add(x, y) {
  return x + y;
}
function multiply(x) {
  return x * 2;
}
function subtract(x, y) {
  return x - y;
}
function square(x) {
  return x * x;
}

当前JS文件中的方法,只能在当前文件math.js中使用,不能在其他文件中使用,如果要在其他js文件中使用,需要用到export关键字导出,我们可以在每个方法前添加export关键字:

export function add(x, y) {
  return x + y;
}


export function multiply(x) {
  return x * 2;
}


export function subtract(x, y) {
  return x - y;
}


export function square(x) {
  return x * x;
}

当然仅仅export导出, 并不能在其他js文件中直接使用,还需要在要使用的js文件中import关键字引入即可,相信大家都很熟悉:

import { add, multiply, subtract, square } from "./math.js";

这样我们就能在module外使用export导出的方法了,将代码模块化有两个好处:

一是代码分割成独立的小模块,降低耦合,提高复用性;

二就是模块内部未export的变量只对模块内部私有,不会污染全局作用域,避免命名冲突,也可以提高模块内私有变量的安全性

使用技巧

  1. 模块化导出的属性名可能和本地的属性名冲突
import { add, multiply, subtract, square } from "./math.js";


function add(...args) {
  return args.reduce((acc, cur) => cur + acc);
} /* Error: add has  already been declared */


function multiply(...args) {
  return args.reduce((acc, cur) => cur * acc);
}
/* Error: multiply has already been declared */

可以通过as关键字重命名导出的变量;

import {
  add as addValues,
  multiply as multiplyValues,
  subtract,
  square
} from "./math.js";
  1. 模块化导出时,export 和 export default的区别:
  • export 导出时,import引入属性要在大括号{}内:
// math.js
export function add(x, y) {
  return x + y;
}
// index.js
import { add } from "./math.js";
  • export default 导出,只能导出一个属性,意思是默认导出,import引入时直接引入,并且可以直接重新命名:
// math.js
export default function add(x, y) {
  return x + y;
}
// index.js
import addValues from "./math.js";

export 和 export default 可以同时使用;

  1. import引入时,可以使用import * 引入所有export和export default导出的属性, 通过as关键字赋值并重命名:
import * as math from "./math.js";

math.default(7, 8);
math.multiply(8, 9);
math.subtract(10, 3);
math.square(3);

通过*导入的export default属性,只能通过math.default访问,不能使用math.add。

动态引入模块——import()

随着项目变得越来越大,在一个复杂组件中可能需要引入很多模块,这可能会消耗一些性能,影响加载时间使用体验。仔细分析这些模块我们发现,一些模块的使用需要特定的条件,这时可以考虑使用动态引入这些模块:

import("module").then(module => {
  module.default();
  module.namedExport();
});

// Or with async/await
(async () => {
  const module = await import("module");
  module.default();
  module.namedExport();
})();

比如说上面示例中math模块,当用户点击按钮才需要调用math模块的方法,我们可以这样实现:

const button = document.getElementById("btn");

button.addEventListener("click", () => {
  import("./math.js").then((module) => {
    console.log("Add: ", module.add(1, 2));
    console.log("Multiply: ", module.multiply(3, 2));

****
    const button = document.getElementById("btn");
    button.innerHTML = "Check the console";
  });
});


/*************************** */
/**** Or with async/await ****/
/*************************** */
// button.addEventListener("click", async () => {
//   const module = await import("./math.js");
//   console.log("Add: ", module.add(1, 2));
//   console.log("Multiply: ", module.multiply(3, 2));
// });

在线示例

通过动态加载可以直接优化页面加载时间,提升用户体验

同时,import方法可以接受字面量表达式:

const res = await import(`../assets/dog${num}.png`);

比如上面的图片示例,可以弹性的加载指定路径的的图片,而不需要一次性把所有图片全部引入到组件中。

总结

使用模块化这种设计模式,可以封装非全局的代码块到某个模块之中,实现低风险地使用多级模块依赖命名空间,能有效避免命名冲突和全局污染

为了在所有js运行时使用ES2015的module,需要使用到类似babel的转译器