2

原文

懒加载是一种将资源初始化推迟到需要时再加载的一种设计模式。本文展示了如何进行ES6模块的懒加载来提神页面的性能。

过去几年来,开发者逐渐开始把代码从服务端向移动端迁移并达到性能上的提升。

然而,这可能还不够。你有想过页面也许会加载许多实际不会用到的资源?通过懒加载,一种设计模式,来实现推迟初始化(加载/获取/分配)资源(代码/数据/静态资源)直到你需要使用的时候再加载。

与此同时,通过Babel这样的编译器,ES6已经可以在实际产品中运行。现在无需care AMD与CommonJS之争,本文中将可以直接写ES6模块,并将其转译运行在浏览器,同时还支持已存在的CommonJS/AMD模块。

在本文中,我将会演示如何同步加载ES6模块(在页面加载时)以及如何使用System.js异步加载ES6模块(执行懒加载)。

页面加载VS懒加载

开发需要在浏览器环境中执行的JavaScript代码时,你总需要决定何时执行这段代码。

总有一些代码块需要在页面加载时就执行,例如构建SPA时需要的结构性框架如Angular,Ember,Backbone或React。在页面请求发起时,这些代码必须被包含在返回的主题HTML文件中,通常由一个或多个<script>标签包裹。

另一方面,你也许有更多的代码块需要在促发条件达成时再执行。包括:

  • 被折叠的内容:例如评论板,在用户下滑时才显示。

  • 在触发事件后响应的内容:例如用户点击放大后呈现的高清图。

  • 不使用/频率不高的内容, 例如,"免费送货"这类应用面较窄的部件。

  • 指定情况下呈现的内容, 例如,客服聊天窗口。

对于上述情形,如果促发条件不成立,代码块就不会执行。因此,代码块在页面加载是不需要的,可以被延迟。

为了实现懒加载,你只需要将这部分代码从页面加载的代码块中分离出来。直到促发条件达成后,再下载并执行代码。

这种异步加载延迟代码或懒加载的方式,在缩短页面加载时间与页面指数上对页面性能提升大有裨益。

AMD的坑

AMD标准被提出用来在浏览器端加载图片,它第一个解决了js全局文件杂乱地散落在页面中的窘境。以下引用Require.js文档。

相较于当下使用的多个script标签隐式并依赖于你手动排列的顺序,AMD格式希望能自由地使用模版样式,并使之可以直接在浏览器环境中简便地被使用。

其基于一套拥有模块加载,依赖注入,别名解析,异步能力的模块设计模式。其中一个主要的用处就是执行页面懒加载。

尽管这是一个绝妙的点子,但也带来了内在的复杂:即需要理解运行时的时间线。这使得开发者需要了解每个模块在何时完成工作。

如果无法理解这些,在竞争条件下开发者就会陷入时而成功时而bug的抓狂中,这是很难调试的。因此,AMD不幸地失去了相当大的势头与推动力。

要了解更多坑,可阅读本文

ES6模块 101

首先,让我们先来温习一下ES6的模版。如果你对其足够熟悉,就当作是一个快速复习。

模块功能最终被作为JS语言2015版的一部分。站在了CommonJS巨人的肩膀上,其强大且易于使用。

作用域scope

基本上,ES6的模块存在于它自生的文件里。所有的“全局”变量的作用域仅仅在文件中。模块可以暴露(export)出数据也可以引用(import)其他模块。

暴露(export)与引用(import)

通过export 关键字,ES6模块接口暴露出你想要暴露的数据(变量,函数或类)。在下面的例子中我们暴露出狗(Dog)狼(Wolf)

// zoo.js
var getBarkStyle = function(isHowler) {  
  return isHowler? 'woooooow!': 'woof, woof!';
};
export class Dog {  
  constructor(name, breed) {
    this.name = name;
    this.breed = breed;
  }
  bark() {
    return `${this.name}: ${getBarkStyle(this.breed === 'husky')}`;
  };
}
export class Wolf {  
  constructor(name) {
    this.name = name;
  }
  bark() {
    return `${this.name}: ${getBarkStyle(true)}`;
  };
}

让我们看一下在Mocha/Chai的单元测试中是如果引用模块,使用import <object> from <path>语法。对于<object>可以按需引用,这被称作“具名引用”。我们可以从chai中引用expect同样也可以从Zoo中引用DogCat。另外,这种具名引用的语法类似于ES6中一个好用的特性:对象解构

// zoo_spec.js
import { expect } from 'chai';  
import { Dog, Wolf } from '../src/zoo';
describe('the zoo module', () => {  
  it('should instantiate a regular dog', () => {
    var dog = new Dog('Sherlock', 'beagle');
    expect(dog.bark()).to.equal('Sherlock: woof, woof!');
  });
  it('should instantiate a husky dog', () => {
    var dog = new Dog('Whisky', 'husky');
    expect(dog.bark()).to.equal('Whisky: woooooow!');
  });
  it('should instantiate a wolf', () => {
    var wolf = new Wolf('Direwolf');
    expect(wolf.bark()).to.equal('Direwolf: woooooow!');
  });
});

default

如果需要暴露的数量仅有一个,你可以使用export default,这将会直接暴露该对象,而非包含该对象的容器对象。

// cat.js
export default class Cat {  
  constructor(name) {
    this.name = name;
  }
  meow() {
    return `${this.name}: You gotta be kidding that I'll obey you, right?`;
  }
}  

与对象的结构相比,引用默认模块更加简单,你只需直接从模块中引用即可。

// cat_spec.js
import { expect } from 'chai';  
import Cat from '../src/cat';
describe('the cat module', () => {  
  it('should instantiate a cat', () => {
    var cat = new Cat('Bugsy');
    expect(cat.meow()).to.equal('Bugsy: You gotta be kidding that I\'ll obey you, right?');
  });
});

ES6 explore可以让你了解到更多关于ES6模块方面的知识。

ES6模块加载器与System.js

令人意外的是,ES6并没有模块加载器的规范。System.js的灵感源于一个受欢迎的动态模块加载器提案es6-module-loader。虽然该体案被撤回,但还有由WhatWG提出的新的加载器体案以及由Domenic Denicola提出的动态import体案。

然而,System.js是当下使用频率最高的支持ES6的模块加载器中之一。在浏览器及NodeJS上支持ES2015,AMD,CommonJS以及全局脚本。其提供了一步模块加载器(与Require.js匹配)以及通过BabelTraceurTypescript的ES6编译。

System.js通过基于Promises的API实现异步模块加载。由于promises既能链式调用又可以捆绑使用,使其成为一种即强大又灵活的方法:例如,你可以使用Promises.all来平行加载多个模块,仅需要监听所有promises是否都被执行。

最近,动态引用规则正逐渐受到关注并被引入Webpack 2。你可以查阅Webpack 2文档的指南中ES6代码分割一节。其也受到System.js的启发,因此转换速度很快。

同步与异步方式引入模块

为了更通俗地阐述以同步/异步方式加载模块,我创建了案例项目,在页面加载时同步引用Cat模块,在用户点击按钮后异步引用Zoo模块。可以通过lazy-load-es2015-systemjs查看这个项目的代码。

看一看在页面加载时引入的主代码块main.js

首先,通过import执行Cat的同步加载。其后创建一个Cat实例,调用meow()方法,添加DOM:

// main.js
// Importing Cat module synchronously
import Cat from 'cat';
// DOM content node
let contentNode = document.getElementById('content');
// Rendering cat
let myCat = new Cat('Bugsy');  
contentNode.innerHTML += myCat.meow();

最后,让我们看看通过System.import('zoo')异步加载Zoo,最终,DogWolf的实例调用bark()方法添加DOM:

// Button to lazy load Zoo
contentNode.innerHTML += `<p><button id='loadZoo'>Lazy load <b>Zoo</b></button></p>`;
// Listener to lazy load Zoo
document.getElementById('loadZoo').addEventListener('click', e => {
  // Importing Zoo module asynchronously
  System.import('zoo').then(Zoo => {
    // Rendering dog
    let myDog = new Zoo.Dog('Sherlock', 'beagle');
    contentNode.innerHTML += `${myDog.bark()}`;
    // Rendering wolf
    let myWolf = new Zoo.Wolf('Direwolf');
    contentNode.innerHTML += `<br/>${myWolf.bark()}`;
  });
});

结论

掌握将同步代码控制在最小并异步加载代码可以极大提升网站的性能。AMD 与 CommonJS为ES6模块铺平了道路,现在你可以通过编译器进行使用。开始加载你的ES6模块通过System.js或在官方还未给出解决方案时,通过Webpack 2实现动态加载。


这是上帝的杰作
2.2k 声望164 粉丝

//loading...