JS 异步插件系统方案研究

广告位招租
扫码页面底部二维码联系

在我们开发设计的 js 项目中,我们经常【本文首发于唐霜的博客】本文作者:唐霜,转载请注明出处。会设计一套插件系统,用以可选的加载某些功【本文首发于唐霜的博客】【本文首发于唐霜的博客】能模块,在使用时,需要某个功能,就加载对未经授权,禁止复制转载。本文作者:唐霜,转载请注明出处。应的插件。以 jquery 举例,它的插著作权归作者所有,禁止商业用途转载。【版权所有,侵权必究】件生态极其丰富,要使用一个 jquery著作权归作者所有,禁止商业用途转载。【原创不易,请尊重版权】 插件非常简单,只需要引入 jquery【关注微信公众号:wwwtangshuangnet】【转载请注明来源】 再引入 jquery 插件,就可以利用【本文受版权保护】【访问 www.tangshuang.net 获取更多精彩内容】插件提供的功能了。插件提供的功能可能是增【原创内容,转载请注明出处】著作权归作者所有,禁止商业用途转载。加了一个事件绑定,或者提供了新的 $  【版权所有】唐霜 www.tangshuang.net【版权所有】唐霜 www.tangshuang.net方法,等等。总之,你不需要插件,你就不需原创内容,盗版必究。【原创不易,请尊重版权】要加载该插件,而你一旦加载一个插件,就必【本文受版权保护】本文版权归作者所有,未经授权不得转载。须按照插件作者提供的文档来写代码。

转载请注明出处:www.tangshua【版权所有,侵权必究】【版权所有,侵权必究】ng.net【转载请注明来源】转载请注明出处:www.tangshua本文版权归作者所有,未经授权不得转载。【访问 www.tangshuang.net 获取更多精彩内容】ng.net著作权归作者所有,禁止商业用途转载。

我们这篇文章并不详细阐述,什么样的一个插本文版权归作者所有,未经授权不得转载。本文作者:唐霜,转载请注明出处。件系统是好的插件系统(起码我认为 jqu【作者:唐霜】本文版权归作者所有,未经授权不得转载。ery 的插件系统就非常优秀)。我们这篇【作者:唐霜】原创内容,盗版必究。文章要来阐述一个插件的异步方案。

本文作者:唐霜,转载请注明出处。【版权所有,侵权必究】

什么是异步插件?

首先,我们要指明,怎样的一种行为方式是异【作者:唐霜】原创内容,盗版必究。步插件。以及,我们为什么需要异步插件。

本文作者:唐霜,转载请注明出处。【版权所有】唐霜 www.tangshu原创内容,盗版必究。未经授权,禁止复制转载。ang.net【访问 www.tangshuang.n【版权所有】唐霜 www.tangshuang.net转载请注明出处:www.tangshuang.netet 获取更多精彩内容】

像 jquery 加载插件那样未尝不可,【本文受版权保护】【本文受版权保护】但直接从 cdn 引入插件会阻塞浏览器渲【转载请注明来源】本文版权归作者所有,未经授权不得转载。染。而且 2020 年的前端,避不开构建本文版权归作者所有,未经授权不得转载。原创内容,盗版必究。、打包等等工程化过程。异步插件解决减小 【本文受版权保护】本文作者:唐霜,转载请注明出处。bundle 文件的问题,同时,由于异步著作权归作者所有,禁止商业用途转载。未经授权,禁止复制转载。加载,因此,也不会阻塞界面渲染。带来的问【版权所有】唐霜 www.tangshuang.net未经授权,禁止复制转载。题就比较复杂,如何异步渲染,并且同时能够【原创内容,转载请注明出处】转载请注明出处:www.tangshuang.net按需要的顺序执行。

【本文受版权保护】【未经授权禁止转载】

对于插件的使用者而言,更关注这个异步插件未经授权,禁止复制转载。原创内容,盗版必究。的 cdn 地址,以及插件的使用方法。我【关注微信公众号:wwwtangshuangnet】原创内容,盗版必究。们用代码来看下具体的情况是怎么样的。来看著作权归作者所有,禁止商业用途转载。著作权归作者所有,禁止商业用途转载。下一个同步插件:

【本文受版权保护】【版权所有】唐霜 www.tangshu转载请注明出处:www.tangshuang.net本文作者:唐霜,转载请注明出处。ang.net
<script src="my-lib.min.js"></script>
<script src="my-lib.some-plugin.min.js"></script>
<script>
const myLib = new MyLib()
// some 这个方法是加载了 some-plugin 之后才有的
myLib.some()
</script>

同步插件的问题在于会阻塞渲染,在加载插件转载请注明出处:www.tangshuang.net【关注微信公众号:wwwtangshuangnet】的时候,界面渲染会阻塞,而且如果插件文件【关注微信公众号:wwwtangshuangnet】【原创不易,请尊重版权】执行过程中出了问题,报错了,还会导致所有【未经授权禁止转载】【原创不易,请尊重版权】的脚本都无法执行,最终导致应用坏死。而我【转载请注明来源】本文版权归作者所有,未经授权不得转载。们来看下异步插件:

本文版权归作者所有,未经授权不得转载。【关注微信公众号:wwwtangshua【本文首发于唐霜的博客】【本文受版权保护】ngnet】
<script src="my-lib.min.js"></script>
<script>
MyLib.registerPlugin('some-plugin', 'http://cdn.xxx.com/some-plugin.min.js') // 这句话将会自动去 cdn 异步加载插件

const myLib = new MyLib()
myLib.usePlugin('some-plugin', function() {
  // 异步插件由于异步加载,所以不能立即在程序中去调用 some 方法,而是必须在插件加载完成之后才能做这件事
  this.some()
})
</script>

异步插件会有一些局限性。包括但不限于:

【访问 www.tangshuang.n【本文受版权保护】未经授权,禁止复制转载。et 获取更多精彩内容】【本文首发于唐霜的博客】【作者:唐霜】
  • 由于是异步加载的,所以实例化的时候,插件转载请注明出处:www.tangshuang.net未经授权,禁止复制转载。可能还没有加载呢,所以插件所提供的功能都转载请注明出处:www.tangshuang.net【作者:唐霜】还没有加载完成,因此,在编程上和同步调用转载请注明出处:www.tangshuang.net【关注微信公众号:wwwtangshuangnet】会有一些不同
  • 【未经授权禁止转载】【关注微信公众号:wwwtangshua【版权所有】唐霜 www.tangshuang.net【版权所有,侵权必究】ngnet】【版权所有】唐霜 www.tangshu转载请注明出处:www.tangshuang.net【作者:唐霜】ang.net
  • 如果一个项目,需要通过异步的方式加载插件未经授权,禁止复制转载。【版权所有】唐霜 www.tangshuang.net,那么项目本身的代码会变得更复杂
  • 【作者:唐霜】著作权归作者所有,禁止商业用途转载。【原创内容,转载请注明出处】
  • 异步插件存在依赖关系怎么办?例如 A 插【版权所有】唐霜 www.tangshuang.net本文版权归作者所有,未经授权不得转载。件依赖 B 插件,怎么解决执行顺序问题?
  • 【本文受版权保护】本文作者:唐霜,转载请注明出处。本文作者:唐霜,转载请注明出处。未经授权,禁止复制转载。

对于插件开发者的心智是一个挑战,我们需要【版权所有,侵权必究】本文版权归作者所有,未经授权不得转载。找到更好的异步加载方案,和写代码的正确方本文版权归作者所有,未经授权不得转载。【版权所有】唐霜 www.tangshuang.net式,这样才能保证开发者用最简单的方式开发【访问 www.tangshuang.net 获取更多精彩内容】【作者:唐霜】插件。所以本文接下来,就要探讨一下,怎样【版权所有,侵权必究】【原创内容,转载请注明出处】的 js 插件系统的异步加载方案更优秀,【作者:唐霜】【关注微信公众号:wwwtangshuangnet】更值得我们推崇。

【访问 www.tangshuang.n未经授权,禁止复制转载。原创内容,盗版必究。et 获取更多精彩内容】【作者:唐霜】

异步插件加载方式

我们将异步加载插件分解为两个问题:1)如【作者:唐霜】本文作者:唐霜,转载请注明出处。何异步加载 js 脚本?2)如何在该脚本著作权归作者所有,禁止商业用途转载。【原创内容,转载请注明出处】中定义插件?

本文作者:唐霜,转载请注明出处。【未经授权禁止转载】

异步加载脚本

异步加载脚本的方式有很多。总结起来主要分未经授权,禁止复制转载。本文作者:唐霜,转载请注明出处。为 3 大类:一类是通过创建 scrip本文版权归作者所有,未经授权不得转载。未经授权,禁止复制转载。t DOM 实例的 src 实现脚本加载【版权所有】唐霜 www.tangshuang.net【转载请注明来源】;二类是通过 xhr 获取脚本内容,再在未经授权,禁止复制转载。本文作者:唐霜,转载请注明出处。当前环境中运行脚本内容;三类是通过创建 转载请注明出处:www.tangshuang.net【版权所有,侵权必究】iframe 来实现同于脚本运行。这些知著作权归作者所有,禁止商业用途转载。【关注微信公众号:wwwtangshuangnet】识可以通过搜索引擎来学习。

【转载请注明来源】【原创不易,请尊重版权】【本文首发于唐霜的博客】

我们应该如何选择呢?我们想到的原则是,既【转载请注明来源】【转载请注明来源】要保证插件系统本身的代码量不要太大,也要转载请注明出处:www.tangshuang.net原创内容,盗版必究。保证最终插件的使用者代码写起来不别扭(学【关注微信公众号:wwwtangshuangnet】未经授权,禁止复制转载。习成本低)。之所以带来学习成本,最大的问【原创不易,请尊重版权】【本文受版权保护】题在于“异步”所带来的副作用。插件脚本在【未经授权禁止转载】【作者:唐霜】加载完时,我们很有可能已经完成自己类库的【作者:唐霜】【版权所有】唐霜 www.tangshuang.net实例化,甚至有些情况下已经做了很多事。然著作权归作者所有,禁止商业用途转载。【作者:唐霜】后,在做这些事情的时候,我们可能会忽略插转载请注明出处:www.tangshuang.net著作权归作者所有,禁止商业用途转载。件还没有加载完。而另一方面,如果等到插件【本文受版权保护】【版权所有】唐霜 www.tangshuang.net加载完再执行所有代码,那会使得实例化过程【原创内容,转载请注明出处】原创内容,盗版必究。变得非常漫长,导致错过一些重要时间节点上原创内容,盗版必究。【原创内容,转载请注明出处】的操作。

著作权归作者所有,禁止商业用途转载。本文版权归作者所有,未经授权不得转载。【访问 www.tangshuang.n未经授权,禁止复制转载。未经授权,禁止复制转载。et 获取更多精彩内容】本文版权归作者所有,未经授权不得转载。

在我自己的实践中,我选择了通过 scri【访问 www.tangshuang.net 获取更多精彩内容】【未经授权禁止转载】pt.src 来加载脚本的方式,这种方式未经授权,禁止复制转载。【未经授权禁止转载】中,需要设置 script.async 【未经授权禁止转载】【版权所有,侵权必究】属性为 true,甚至为了兼容,同时设置【作者:唐霜】【关注微信公众号:wwwtangshuangnet】 script.defer 属性为 tr【关注微信公众号:wwwtangshuangnet】【版权所有,侵权必究】ue。在 onload 中,通知插件系统原创内容,盗版必究。【原创内容,转载请注明出处】,该插件加载好了,需要做一些处理。

原创内容,盗版必究。【访问 www.tangshuang.n著作权归作者所有,禁止商业用途转载。【版权所有】唐霜 www.tangshuang.netet 获取更多精彩内容】

插件的定义

作为插件,而非类库的主要部分,它只是将自【作者:唐霜】【版权所有】唐霜 www.tangshuang.net己的功能附加到主库中去。如何将插件插入主【未经授权禁止转载】【版权所有】唐霜 www.tangshuang.net库的功能列表中呢?这需要一个合理的设计。

本文作者:唐霜,转载请注明出处。【作者:唐霜】【版权所有】唐霜 www.tangshu本文作者:唐霜,转载请注明出处。转载请注明出处:www.tangshuang.netang.net【原创内容,转载请注明出处】

实际上,我们需要很简单的一个操作。例如 【版权所有】唐霜 www.tangshuang.net【访问 www.tangshuang.net 获取更多精彩内容】MyLib.addPlugin(plug本文作者:唐霜,转载请注明出处。【转载请注明来源】in) 即可,其中的操作由开发者自己去想【本文受版权保护】转载请注明出处:www.tangshuang.net象。现在的问题是,作为插件的开发者,我们【本文首发于唐霜的博客】转载请注明出处:www.tangshuang.net应该怎么去定义一个插件?在异步加载过程中未经授权,禁止复制转载。【访问 www.tangshuang.net 获取更多精彩内容】,我们可能会有一些需求,例如,我们当前这【关注微信公众号:wwwtangshuangnet】【版权所有,侵权必究】个插件需要依赖另外一个插件,但是由于插件原创内容,盗版必究。【原创内容,转载请注明出处】是异步加载的,所以对于当前这个插件而言,原创内容,盗版必究。未经授权,禁止复制转载。并不能保证被依赖的插件已经加载好了。所以著作权归作者所有,禁止商业用途转载。本文作者:唐霜,转载请注明出处。,我们需要有一个依赖列表,用来表示自己所本文作者:唐霜,转载请注明出处。原创内容,盗版必究。依赖的其他插件。在真正运行的时候,插件系【本文首发于唐霜的博客】【本文首发于唐霜的博客】统,应该保证,该插件的依赖被运行之后,才【未经授权禁止转载】【未经授权禁止转载】运行当前这个插件。

【转载请注明来源】转载请注明出处:www.tangshua【本文受版权保护】【原创内容,转载请注明出处】ng.net【作者:唐霜】著作权归作者所有,禁止商业用途转载。

既然提到了“依赖”,那就不得不说新晋的前【本文首发于唐霜的博客】本文作者:唐霜,转载请注明出处。端开发者不知道的 AMD 模块加载规范。【原创不易,请尊重版权】【原创内容,转载请注明出处】遵循 AMD 的 require.js 【本文首发于唐霜的博客】【版权所有,侵权必究】曾经流行一时,它通过约定模块加载路径,从著作权归作者所有,禁止商业用途转载。本文作者:唐霜,转载请注明出处。而使得加载的内容可以根据需要实现 laz转载请注明出处:www.tangshuang.net【版权所有,侵权必究】yload(这个词大家应该都懂)。而 A本文作者:唐霜,转载请注明出处。【版权所有,侵权必究】MD 规范中,规定了一个模块定义的方法,【原创内容,转载请注明出处】本文版权归作者所有,未经授权不得转载。最主要的用法是

【原创不易,请尊重版权】【关注微信公众号:wwwtangshua【本文受版权保护】著作权归作者所有,禁止商业用途转载。ngnet】【作者:唐霜】本文版权归作者所有,未经授权不得转载。
define(name: string, dependencies: string[], function(...deps: any[]): any {
  // ...
  return exports
})

通过这样一个 define 函数来定义一【作者:唐霜】转载请注明出处:www.tangshuang.net个模块,其中 define 的第二个参数未经授权,禁止复制转载。【转载请注明来源】,就是告诉 require.js 当前这【版权所有,侵权必究】【版权所有】唐霜 www.tangshuang.net个模块需要依赖哪些模块,在运行当前这个模转载请注明出处:www.tangshuang.net【转载请注明来源】块之前,要确保 dependencies本文作者:唐霜,转载请注明出处。【原创不易,请尊重版权】 这些模块已经运行过了。对于 requi【未经授权禁止转载】未经授权,禁止复制转载。re.js 而言,在运行当前这个模块时,【作者:唐霜】【转载请注明来源】会自动检查依赖,如果依赖没有准备好,re【关注微信公众号:wwwtangshuangnet】【原创不易,请尊重版权】quired.js 会按照自己的模块加载未经授权,禁止复制转载。【版权所有,侵权必究】机制,先加载被依赖的模块(加载模块脚本,转载请注明出处:www.tangshuang.net转载请注明出处:www.tangshuang.net同时运行模块函数),然后才运行当前这个模【版权所有】唐霜 www.tangshuang.net转载请注明出处:www.tangshuang.net块。而在加载依赖时,模块会并行加载和运行本文版权归作者所有,未经授权不得转载。【关注微信公众号:wwwtangshuangnet】(当然,如果依赖内部还有依赖,它也会安装【原创不易,请尊重版权】【本文首发于唐霜的博客】依赖顺序进行执行,但可以肯定的是,模块脚本文版权归作者所有,未经授权不得转载。【作者:唐霜】本的加载是并行的)。

本文作者:唐霜,转载请注明出处。【版权所有,侵权必究】

AMD 方案不能说最好,但是却非常适用我【未经授权禁止转载】原创内容,盗版必究。所面临的问题——如何高效合理的加载异步插【未经授权禁止转载】本文版权归作者所有,未经授权不得转载。件。

【本文受版权保护】【版权所有,侵权必究】著作权归作者所有,禁止商业用途转载。

因此,最终,我选择了类似 AMD 这样的未经授权,禁止复制转载。本文作者:唐霜,转载请注明出处。模块定义方案来定义我们的插件:

【访问 www.tangshuang.n本文版权归作者所有,未经授权不得转载。本文版权归作者所有,未经授权不得转载。et 获取更多精彩内容】【版权所有】唐霜 www.tangshu本文作者:唐霜,转载请注明出处。原创内容,盗版必究。ang.net【关注微信公众号:wwwtangshua原创内容,盗版必究。本文版权归作者所有,未经授权不得转载。ngnet】【本文受版权保护】
MyLib.defineAsyncPlugin('some', [], function() {
  // 和 AMD 不同,插件的模块函数并非全局执行,而是针对每一个 MyLib 实例运行,所以,在本函数内,插件作者可以调用 this 来指向 MyLib 的实例
  this.some = function() {
    console.log('some is invoked')
  }
})

这样,对于一个 MyLib 的实例而言,本文版权归作者所有,未经授权不得转载。未经授权,禁止复制转载。就拥有了一个 some 方法。

【访问 www.tangshuang.n【原创内容,转载请注明出处】【原创不易,请尊重版权】et 获取更多精彩内容】【关注微信公众号:wwwtangshua【版权所有】唐霜 www.tangshuang.net原创内容,盗版必究。ngnet】原创内容,盗版必究。

这里需要有一个更深入的理解。我们的插件虽著作权归作者所有,禁止商业用途转载。【访问 www.tangshuang.net 获取更多精彩内容】然在 MyLib 上定义,但是定义的时候转载请注明出处:www.tangshuang.net【关注微信公众号:wwwtangshuangnet】,并不执行插件函数,而是被记录在 MyL【关注微信公众号:wwwtangshuangnet】本文作者:唐霜,转载请注明出处。ib 内部,真正拥有插件能力的,是 My【版权所有】唐霜 www.tangshuang.net【关注微信公众号:wwwtangshuangnet】Lib 的实例 myLib。这样设计和 著作权归作者所有,禁止商业用途转载。【原创内容,转载请注明出处】require.js 的巨大差别在于,r【本文首发于唐霜的博客】【原创内容,转载请注明出处】equire.js 是模块系统,本身就是【原创内容,转载请注明出处】本文作者:唐霜,转载请注明出处。在全局执行,每一个模块在加载的时候,就需【转载请注明来源】转载请注明出处:www.tangshuang.net要执行模块函数,而插件则不同,插件是要提转载请注明出处:www.tangshuang.net转载请注明出处:www.tangshuang.net供功能,而这些功能,往往都是针对实例,因【转载请注明来源】【本文首发于唐霜的博客】此,它不是全局执行,而是在实例化时执行。

【访问 www.tangshuang.n转载请注明出处:www.tangshuang.net【原创不易,请尊重版权】et 获取更多精彩内容】【版权所有,侵权必究】【作者:唐霜】【转载请注明来源】

定义一个插件只是提供了它的定义,并不立即本文版权归作者所有,未经授权不得转载。【关注微信公众号:wwwtangshuangnet】运行它。真正的运行是在实例中,这样才能保本文作者:唐霜,转载请注明出处。原创内容,盗版必究。证实例拥有插件提供的方法。

著作权归作者所有,禁止商业用途转载。著作权归作者所有,禁止商业用途转载。本文版权归作者所有,未经授权不得转载。【版权所有】唐霜 www.tangshu【未经授权禁止转载】本文版权归作者所有,未经授权不得转载。ang.net

异步插件的注册和使用

和 jquery 插件一样,我们要先注册著作权归作者所有,禁止商业用途转载。【本文受版权保护】插件,然后才能使用它。但是和同步加载插件【关注微信公众号:wwwtangshuangnet】原创内容,盗版必究。不同,我们不能马上加载和运行异步插件文件【本文受版权保护】【原创不易,请尊重版权】,这样会阻塞进程,达不到我们要的效果。我【原创不易,请尊重版权】【原创内容,转载请注明出处】们得使用一种合理的方法来注册插件,这样技未经授权,禁止复制转载。【本文受版权保护】能不阻塞界面渲染,同时又能得到想要的效果未经授权,禁止复制转载。本文版权归作者所有,未经授权不得转载。

【本文受版权保护】未经授权,禁止复制转载。

插件的注册

为什么要先注册再使用呢?为什么不直接在使【未经授权禁止转载】【作者:唐霜】用的时候自动注册呢?就像 require转载请注明出处:www.tangshuang.net本文作者:唐霜,转载请注明出处。.js 一样,使用的时候自己去加载,加载【转载请注明来源】【版权所有,侵权必究】完马上使用。这样做未尝不可以,例如,我们【未经授权禁止转载】本文版权归作者所有,未经授权不得转载。用一段伪代码来表示:

未经授权,禁止复制转载。【版权所有】唐霜 www.tangshu【关注微信公众号:wwwtangshuangnet】【版权所有,侵权必究】ang.net转载请注明出处:www.tangshua【关注微信公众号:wwwtangshuangnet】著作权归作者所有,禁止商业用途转载。ng.net【未经授权禁止转载】
const myLib = new MyLib()
myLib.useAsyncPlugin(url).then(function() {
  this.some()
})

这样的代码非常简洁,甚至你会觉得这才是正未经授权,禁止复制转载。转载请注明出处:www.tangshuang.net解。

本文版权归作者所有,未经授权不得转载。未经授权,禁止复制转载。【关注微信公众号:wwwtangshua本文版权归作者所有,未经授权不得转载。转载请注明出处:www.tangshuang.netngnet】本文版权归作者所有,未经授权不得转载。

但是,想象一下,你的系统中可能存在多个 【本文受版权保护】【原创不易,请尊重版权】MyLib 的实例。那是不是意味着每一个【作者:唐霜】转载请注明出处:www.tangshuang.net实例都要传入 url 进行加载?其实这个【版权所有】唐霜 www.tangshuang.net【原创不易,请尊重版权】加载是没有必要的,因为同一个 scrip【原创不易,请尊重版权】【本文首发于唐霜的博客】t 脚本被加载之后,没有必要再去运行第二【关注微信公众号:wwwtangshuangnet】转载请注明出处:www.tangshuang.net次,否则每次使用一个插件都要走加载+执行未经授权,禁止复制转载。【版权所有】唐霜 www.tangshuang.net两个步骤,严重拖长了执行时间。

【本文受版权保护】著作权归作者所有,禁止商业用途转载。未经授权,禁止复制转载。【版权所有,侵权必究】

我最终选择的方式是,你需要在全局注册需要【访问 www.tangshuang.net 获取更多精彩内容】【版权所有】唐霜 www.tangshuang.net的插件,在实例上,使用这些插件即可。

【版权所有】唐霜 www.tangshu未经授权,禁止复制转载。【本文首发于唐霜的博客】ang.net【未经授权禁止转载】
MyLib.registerPlugin(url)

const myLib = new MyLib()
myLib.usePlugin('some', options)

这样的好处是,同一个插件,一旦完成加载和原创内容,盗版必究。著作权归作者所有,禁止商业用途转载。注册,那么就不需要再进行第二次加载。对于【关注微信公众号:wwwtangshuangnet】【转载请注明来源】实例而言,可以在最快的时间,进入插件的执【原创内容,转载请注明出处】未经授权,禁止复制转载。行阶段,从而拥有该插件提供的功能。

本文版权归作者所有,未经授权不得转载。原创内容,盗版必究。【访问 www.tangshuang.n【本文受版权保护】原创内容,盗版必究。et 获取更多精彩内容】

插件的使用

但是,一定要记住,插件的加载和执行都是异著作权归作者所有,禁止商业用途转载。转载请注明出处:www.tangshuang.net步的。为什么执行也是异步的呢?比如上面的【作者:唐霜】【关注微信公众号:wwwtangshuangnet】代码中 myLib.usePlugin('some', options) 并不能在执行当前这一句代码的时候,就真本文作者:唐霜,转载请注明出处。【原创内容,转载请注明出处】正执行插件的定义函数,而是要等待插件加载【本文首发于唐霜的博客】【版权所有,侵权必究】完之后才会执行。为什么?因为我们在调用 【关注微信公众号:wwwtangshuangnet】本文作者:唐霜,转载请注明出处。usePlugin 的时候,这个插件可能【原创内容,转载请注明出处】【版权所有】唐霜 www.tangshuang.net还没有加载完呢。

未经授权,禁止复制转载。【未经授权禁止转载】

而且,我们的一些操作,必须等到插件加载执【原创内容,转载请注明出处】【本文受版权保护】行完之后才去运行,因此,我们需要将我们的【原创不易,请尊重版权】转载请注明出处:www.tangshuang.net程序分段执行,例如:

本文作者:唐霜,转载请注明出处。【访问 www.tangshuang.n【原创不易,请尊重版权】【原创不易,请尊重版权】et 获取更多精彩内容】
myLib.usePlugin('some', options, function() {
  this.some()
})

这段代码的意思是,当插件 some 已经准备好(加载完,并执行完)之后,执【关注微信公众号:wwwtangshuangnet】【作者:唐霜】行回调函数中的内容。通过传入一个回调函数著作权归作者所有,禁止商业用途转载。【本文首发于唐霜的博客】,表示当插件真正加载完之后再运行这个函数未经授权,禁止复制转载。【未经授权禁止转载】,而在这个函数内部,可以使用 this 【本文受版权保护】【访问 www.tangshuang.net 获取更多精彩内容】表示当前这个实例。但是,在一些情况下,我未经授权,禁止复制转载。【原创不易,请尊重版权】们可能需要同时等待多个插件加载完之后,才本文版权归作者所有,未经授权不得转载。未经授权,禁止复制转载。运行某个回调。我们可以这样子:

本文版权归作者所有,未经授权不得转载。【作者:唐霜】
myLib.onPlugin(['a', 'b'], function() {
  this.a()
  this.b()
})

由于异步的加载和执行,给我们的插件使用带著作权归作者所有,禁止商业用途转载。本文版权归作者所有,未经授权不得转载。来了麻烦,但是这就是代价,异步编程让我们【版权所有,侵权必究】未经授权,禁止复制转载。没有那么方便的使用按照同步思维进行。不过本文版权归作者所有,未经授权不得转载。【作者:唐霜】,为了更好的符合我们的编程习惯,我们可以原创内容,盗版必究。【版权所有,侵权必究】设计成 async/await 的模式。

本文作者:唐霜,转载请注明出处。【原创内容,转载请注明出处】【原创内容,转载请注明出处】本文版权归作者所有,未经授权不得转载。
;(async function() {
  const myLib = new MyLib()

  await myLib.usePlugin('some', options) // 使用 await 等待插件加载和执行完
  await myLib.usePlugin('plugin2', options)

  myLib.some()
  myLib.b()
})()

我们将异步插件系统设计为返回一个 Pro原创内容,盗版必究。本文版权归作者所有,未经授权不得转载。mise,这样就可以使用 async/a【作者:唐霜】【本文受版权保护】wait 进行编程,当然,这也不是必须的【本文首发于唐霜的博客】未经授权,禁止复制转载。选择。

【关注微信公众号:wwwtangshua【版权所有,侵权必究】【未经授权禁止转载】ngnet】转载请注明出处:www.tangshua本文版权归作者所有,未经授权不得转载。【访问 www.tangshuang.net 获取更多精彩内容】ng.net

插件的命名

一个问题是,我们让插件开发者命名自己的插【未经授权禁止转载】【关注微信公众号:wwwtangshuangnet】件,还是让插件的使用者在注册插件时命名插【未经授权禁止转载】【作者:唐霜】件?这里需要关心的一个问题是,如果两个插【版权所有,侵权必究】【版权所有,侵权必究】件的开发者使用了相同的插件名,而插件的使【本文首发于唐霜的博客】【版权所有】唐霜 www.tangshuang.net用者可能同时需要这两个插件。因此,我更倾【本文首发于唐霜的博客】著作权归作者所有,禁止商业用途转载。向让插件的使用者给插件命名。

【版权所有,侵权必究】【访问 www.tangshuang.n著作权归作者所有,禁止商业用途转载。【关注微信公众号:wwwtangshuangnet】et 获取更多精彩内容】原创内容,盗版必究。

然而,这也会带来一些副作用。实际上,我们【本文受版权保护】本文作者:唐霜,转载请注明出处。使用异步插件的目的,是希望减小项目本身的本文作者:唐霜,转载请注明出处。转载请注明出处:www.tangshuang.net打包代码,加快代码初始化速度。但是,倘若【本文受版权保护】【未经授权禁止转载】我们给了插件使用者更多选择,那么势必我们【作者:唐霜】本文版权归作者所有,未经授权不得转载。的目的会遭受调整,项目的代码可能会由于这【关注微信公众号:wwwtangshuangnet】【版权所有,侵权必究】些逻辑,多出一些原本可能不必要的逻辑代码本文作者:唐霜,转载请注明出处。【版权所有,侵权必究】。这是取舍两难的话题。

【本文受版权保护】著作权归作者所有,禁止商业用途转载。【原创内容,转载请注明出处】

为了和 AMD 契合,我最终选择了让插件转载请注明出处:www.tangshuang.net本文版权归作者所有,未经授权不得转载。的开发者来定义自己的插件名字。

本文作者:唐霜,转载请注明出处。本文作者:唐霜,转载请注明出处。

让插件开发者定义插件的名字,这样,一个插【原创不易,请尊重版权】【版权所有】唐霜 www.tangshuang.net件拥有什么名字,就是固定的了。这样的好处本文版权归作者所有,未经授权不得转载。【作者:唐霜】非常多,除了可能的命名冲突之外,全是好处【访问 www.tangshuang.net 获取更多精彩内容】【作者:唐霜】。当然,作为插件系统,最好对该命名有一个【版权所有,侵权必究】【版权所有】唐霜 www.tangshuang.net规范要求,例如要求插件名有一个 scop著作权归作者所有,禁止商业用途转载。本文版权归作者所有,未经授权不得转载。e,这样就可以通过 scope 来区分不转载请注明出处:www.tangshuang.net【关注微信公众号:wwwtangshuangnet】同开发者提供的不同插件。

【访问 www.tangshuang.n【版权所有,侵权必究】未经授权,禁止复制转载。et 获取更多精彩内容】【原创不易,请尊重版权】本文版权归作者所有,未经授权不得转载。转载请注明出处:www.tangshua原创内容,盗版必究。【版权所有,侵权必究】ng.net

而且,这样的更大的好处在于,对依赖的确定本文作者:唐霜,转载请注明出处。本文作者:唐霜,转载请注明出处。更加有利。插件的开发者可以非常明确,自己转载请注明出处:www.tangshuang.net著作权归作者所有,禁止商业用途转载。的插件是依赖哪一个插件的,这个插件在当前【关注微信公众号:wwwtangshuangnet】原创内容,盗版必究。插件中运行起来没有任何问题。但假如插件名【本文受版权保护】本文作者:唐霜,转载请注明出处。不是有插件开发者自己规定,而是插件使用者原创内容,盗版必究。本文作者:唐霜,转载请注明出处。在自己系统中动态规定,那么就会导致当前这【版权所有,侵权必究】【访问 www.tangshuang.net 获取更多精彩内容】个插件完全不知道自己该使用什么样的依赖,【作者:唐霜】【未经授权禁止转载】以及是否正确加载了依赖的插件。

转载请注明出处:www.tangshua原创内容,盗版必究。【本文首发于唐霜的博客】ng.net【未经授权禁止转载】转载请注明出处:www.tangshua著作权归作者所有,禁止商业用途转载。【转载请注明来源】ng.net【本文首发于唐霜的博客】

而且,这样的好处还在于,可以更大限度的节著作权归作者所有,禁止商业用途转载。著作权归作者所有,禁止商业用途转载。省项目初始化脚本文件的大小,让插件可以容【本文首发于唐霜的博客】【版权所有】唐霜 www.tangshuang.net纳更多代码,从而达到我们异步加载的目的。

著作权归作者所有,禁止商业用途转载。本文版权归作者所有,未经授权不得转载。

插件预处理

在一些使用场景中,插件还会尝试在加载之前未经授权,禁止复制转载。【原创不易,请尊重版权】收集一些信息,加载结束的时候利用这些信息转载请注明出处:www.tangshuang.net【未经授权禁止转载】进行下一步操作。因此,插件的注册,还需要【关注微信公众号:wwwtangshuangnet】【原创不易,请尊重版权】安装一定的方式进行重新设计。在同步任务中本文作者:唐霜,转载请注明出处。转载请注明出处:www.tangshuang.net,插件需要收集数据,如下:

【未经授权禁止转载】转载请注明出处:www.tangshua转载请注明出处:www.tangshuang.net未经授权,禁止复制转载。ng.net未经授权,禁止复制转载。【版权所有】唐霜 www.tangshu未经授权,禁止复制转载。【访问 www.tangshuang.net 获取更多精彩内容】ang.net
MyLib.registerPlugin({
  url, // 插件的 url
  options, // 插件的基础参数,后续启用插件时所传入的参数会和这个 options 进行合并
  onReady, // 插件注册完成之后立即运行的函数
  onInit, // 实例化时运行的函数,支持 this
  onLoad, // 插件加载完成时运行的函数
  onPlugin, // 插件函数被调用后运行的函数,支持 this
  onDestroy, // 实例销毁时运行的函数
})

通过这样的配置,可以更全面的控制插件。在【关注微信公众号:wwwtangshuangnet】【版权所有,侵权必究】 onReady 和 onInit 中,转载请注明出处:www.tangshuang.net本文版权归作者所有,未经授权不得转载。可以进行一些数据收集。对于插件的作者而言【本文首发于唐霜的博客】转载请注明出处:www.tangshuang.net,不需要再暴露 url 给插件的使用者,【版权所有,侵权必究】著作权归作者所有,禁止商业用途转载。而是直接提供一个封装好的 js 模块给使【关注微信公众号:wwwtangshuangnet】【本文首发于唐霜的博客】用者调用。例如:

【本文首发于唐霜的博客】未经授权,禁止复制转载。
import SomePlugin from './some-plugin.js'
MyLib.registerPlugin(SomePlugin)

而这个 some-plugin.js 的【访问 www.tangshuang.net 获取更多精彩内容】【关注微信公众号:wwwtangshuangnet】实际内容可能是:

【版权所有,侵权必究】本文作者:唐霜,转载请注明出处。
let count = 0
const SomePlugin = {
  url,
  onInit() {
    this.count = count
    count ++
  }
}

这个插件的作用,就是在实例上面添加了一个原创内容,盗版必究。【本文首发于唐霜的博客】 count 属性,而这个 count 未经授权,禁止复制转载。【作者:唐霜】属性,正好在我们的插件的定义函数中使用。本文版权归作者所有,未经授权不得转载。著作权归作者所有,禁止商业用途转载。总之,这种方式帮助插件的开发者更好的做一【未经授权禁止转载】原创内容,盗版必究。些处理。

【关注微信公众号:wwwtangshua【访问 www.tangshuang.net 获取更多精彩内容】未经授权,禁止复制转载。ngnet】转载请注明出处:www.tangshua著作权归作者所有,禁止商业用途转载。【原创内容,转载请注明出处】ng.net未经授权,禁止复制转载。转载请注明出处:www.tangshua转载请注明出处:www.tangshuang.net【版权所有】唐霜 www.tangshuang.netng.net

插件注册的实现

这一节,我们用实际的代码来实现异步插件的【访问 www.tangshuang.net 获取更多精彩内容】著作权归作者所有,禁止商业用途转载。加载。前文实际上已经分析过了,我们可以使转载请注明出处:www.tangshuang.net著作权归作者所有,禁止商业用途转载。用 script 标签来做异步加载,只需【本文首发于唐霜的博客】原创内容,盗版必究。要开启 script.async 属性即未经授权,禁止复制转载。本文作者:唐霜,转载请注明出处。可,这也是为什么我们不使用 xhr + 【关注微信公众号:wwwtangshuangnet】【未经授权禁止转载】script.text 的原因,因为使用【版权所有】唐霜 www.tangshuang.net原创内容,盗版必究。 script.text 是同步执行的,【原创内容,转载请注明出处】【关注微信公众号:wwwtangshuangnet】也就是说,插件的内容会在当前线程环境中立【版权所有】唐霜 www.tangshuang.net【版权所有】唐霜 www.tangshuang.net即执行。因此,我们使用 src 来实现更【版权所有】唐霜 www.tangshuang.net著作权归作者所有,禁止商业用途转载。好。

【版权所有】唐霜 www.tangshu原创内容,盗版必究。本文作者:唐霜,转载请注明出处。ang.net转载请注明出处:www.tangshua【本文受版权保护】【版权所有】唐霜 www.tangshuang.netng.net本文作者:唐霜,转载请注明出处。【版权所有,侵权必究】

让我们回到真实的场景中,对于用户而言,可著作权归作者所有,禁止商业用途转载。原创内容,盗版必究。能是这样使用插件:

【访问 www.tangshuang.n本文版权归作者所有,未经授权不得转载。【作者:唐霜】et 获取更多精彩内容】本文版权归作者所有,未经授权不得转载。【本文受版权保护】【访问 www.tangshuang.n【本文首发于唐霜的博客】【原创内容,转载请注明出处】et 获取更多精彩内容】
<!DOCTYPE html>
<head>
  <script src="http://cdn.xxx.com/my-lib.min.js"></script>
  <script>
    MyLib.registerPlugin('http://cdn.xxx.com/plugins/@tomy/touch.min.js')
    MyLib.registerPlugin('http://cdn.xxx.com/plugins/@pony/touch.min.js') // 虽然插件名相同,但是 scope 不同
  </script>
</head>

我们来实现这个 registerPlug【作者:唐霜】原创内容,盗版必究。in 方法:

【本文首发于唐霜的博客】【原创内容,转载请注明出处】
MyLib._instances = []
MyLib._plugins = []
MyLib.registerPlugin = function(def) {
  // 如果是直接传入 url
  if (typeof def === 'string') {
    def = { url: def }
  }
  
  const { url, required, options, onReady, onInit, onPlugin, onLoaded, onDestroy } = def
  const id = 'plugin' + Math.random()
  
  const plugin = {
    id,
    required,
    options,
    onInit,
    onPlugin,
    onDestroy
  }

  if (typeof onReady === 'function') {
    onReady()
  }

  const script = document.createElement('script')
  script.async = true
  script.defer = true
  script.id = id
  script.src = url
  script.onload = () => {
    if (typeof onLoad === 'function') {
      onLoad()
    }

    // 决定依赖关系,并按依赖关系排序
    this._plugins.sort((a, b) => {
      const aName = a.name
      const aDeps = a.deps
      const bName = b.name
      const bDeps = b.deps
      
      if (aDeps.includes(bName)) {
        return -1
      }
      else if (bDeps.includes(aName)) {
        return 1
      }
      else {
        return 0
      }
    })

    MyLib._instances.forEach((mylib) => {
      // TODO: 当一个插件加载之后,要去检查所有实例是否已经使用了插件,如果使用了插件,就直接启用插件
      mylib._runPlugins()
    })
  }

  this._plugins.push(plugin)
}

对于插件的开发者而言,要在异步脚本中使用【关注微信公众号:wwwtangshuangnet】【关注微信公众号:wwwtangshuangnet】 MyLib.defineAsyncPl【本文受版权保护】未经授权,禁止复制转载。ugin 来定义插件,他可能这么写:

【版权所有,侵权必究】未经授权,禁止复制转载。
MyLib.defineAsyncPlugin('@someone/count', [], function() {
  console.log(this.count)
})

那么,这个 defineAsyncPlu本文作者:唐霜,转载请注明出处。原创内容,盗版必究。gin 怎么实现呢?

【原创不易,请尊重版权】【未经授权禁止转载】【访问 www.tangshuang.n【原创内容,转载请注明出处】【转载请注明来源】et 获取更多精彩内容】【关注微信公众号:wwwtangshua本文作者:唐霜,转载请注明出处。转载请注明出处:www.tangshuang.netngnet】
MyLib.defineAsyncPlugin = function(name, deps, define) {
  const id = document.currentScript.id
  const plugin = this._plugins.find(item => item.id === id)
  if (!plugin) {
    return
  }

  Object.assign(plugin, {
    name,
    deps,
    define,
  })
}

这样,当开发者在自己的项目中注册了这个插原创内容,盗版必究。本文作者:唐霜,转载请注明出处。件,就可以正常的加载到项目系统中了。而且本文作者:唐霜,转载请注明出处。【版权所有,侵权必究】,我们还有一个 runPlugins 的【版权所有】唐霜 www.tangshuang.net未经授权,禁止复制转载。方法,这个方法会去检查所有实例上应该被执【版权所有】唐霜 www.tangshuang.net【未经授权禁止转载】行的插件。当然,如果这些插件的依赖没有执【版权所有,侵权必究】原创内容,盗版必究。行完,那么它们自己本身也不会被执行。

【关注微信公众号:wwwtangshua【版权所有】唐霜 www.tangshuang.net【版权所有,侵权必究】ngnet】本文作者:唐霜,转载请注明出处。【转载请注明来源】本文版权归作者所有,未经授权不得转载。

插件启用的实现

我们已经完成了插件的注册,那么,接下来,【作者:唐霜】未经授权,禁止复制转载。我们在实例中启用这个插件,会遇到哪些需要著作权归作者所有,禁止商业用途转载。著作权归作者所有,禁止商业用途转载。解决的问题呢?启用插件因为是在实例上进行【本文首发于唐霜的博客】转载请注明出处:www.tangshuang.net,因此它有两种选择,一种是为实例的初始化【原创内容,转载请注明出处】【本文受版权保护】参数设计一个配置选项,另一种是使用方法发【版权所有,侵权必究】【原创内容,转载请注明出处】起命令。而在使用配置参数时,实际上也可以本文作者:唐霜,转载请注明出处。【作者:唐霜】是调用发起命令完成的。因此,我们需要定义【访问 www.tangshuang.net 获取更多精彩内容】【作者:唐霜】发起调用插件的方法:

转载请注明出处:www.tangshua【原创内容,转载请注明出处】【访问 www.tangshuang.net 获取更多精彩内容】ng.net【未经授权禁止转载】原创内容,盗版必究。
class MyLib {
  _pluginQueue = []
  _pluginModules = {}

  constructor(options = {}) {
    const { plugins } = options
    if (plugins) {
      const names = Object.keys(plugins)
      names.forEach((name) => {
        const options = plugins[name]
        this.usePlugin(name, options)
      })
    }

    MyLib._instances.push(this)
  }
  
  usePlugin(name, options = {}) {
    // 实际上,代码非常非常简单,就是将该插件的启用信息,加入到一个队列中
    this._pluginQueue.push({ name, options })
    this._runPlugins()
    return this
  }

  onPlugin(names, fn) {
    if (typeof names === 'string') {
      names = [names]
    }

    this._pluginQueue.push({ names, fn })
    this._emitPlugins()
  }

  // 这个方法用于检查全部的插件是否已经被运行,如果没有被运行,那么根据其依赖,执行它
  _runPlugins() {
    const queue = this._pluginQueue
    const modules = this._pluginModules
    const plugins = MyLib._plugins
    
    if (!Object.keys(queue).length) {
      return
    }

    plugins.forEach((plugin) => {
      const { name, deps, define, onPlugin, options: basicOptions } = plugin
      if (name in modules) {
        return
      }

      const request = queue.find(item => item.name === name)
      if (!request) {
        return
      }

      // 检查所有依赖是否已经加载好了
      if (deps.find(item => !(item in modules)) {
        return
      }

      // 记录真正的 options
      const requestOptions = request.options
      const options = { ...basicOptions, ...requestOptions }

      const injects = deps.map(item => item === '$options' ? options : modules[item])
      const module = define.apply(this, injects)
      modules[name] = module
      
      if (typeof onPlugin === 'function') {
        onPlugin.call(this, module)
      }

      this._emitPlugins()
    })
  }

  _emitPlugins() {
    const queue = this._pluginQueue
    const modules = this._pluginModules
    queue.forEach((item) => {
      if (item.done || !item.names) {
        return
      }
      if (item.names.find(name => !modules[name])) {
        return
      }
      item.fn.call(this)
      item.done = true
    })
  }
}

这样,插件的使用者,就可以使用我们提供的【版权所有,侵权必究】【作者:唐霜】这些方法,启动插件,使得当前这个实例,拥本文作者:唐霜,转载请注明出处。未经授权,禁止复制转载。有插件提供的功能。

【作者:唐霜】著作权归作者所有,禁止商业用途转载。【本文首发于唐霜的博客】【访问 www.tangshuang.n【本文首发于唐霜的博客】【原创内容,转载请注明出处】et 获取更多精彩内容】
<script>
const myLib = new MyLib()
myLib.usePlugin('@tomy/touch')
  .usePlugin('@pony/touch', { debounce: 2000 })
  .onPlugin(['@tomy/touch', '@pony/touch'], function() {
    this.on('touch', e => console.log(e))
    this.touch()
  })
</script>

我们没有实现 async/await 方【原创内容,转载请注明出处】原创内容,盗版必究。案,因为这样又会多出一些代码,我们尽可能原创内容,盗版必究。未经授权,禁止复制转载。的减少代码量。

【本文首发于唐霜的博客】未经授权,禁止复制转载。

结束语

本文分析并实现了一个异步插件系统的骨架方本文版权归作者所有,未经授权不得转载。【版权所有】唐霜 www.tangshuang.net案,这个异步插件系统立足于“类-实例”的原创内容,盗版必究。本文作者:唐霜,转载请注明出处。结构方式,而非单例模式,因此设计上会比单【转载请注明来源】原创内容,盗版必究。例模式的系统更麻烦一些。回到文章开头,我原创内容,盗版必究。【转载请注明来源】们这样做的目的,是为了将一些插件拆分出来【版权所有】唐霜 www.tangshuang.net著作权归作者所有,禁止商业用途转载。,根据实际的需要来定制需要加载的脚本,从【版权所有】唐霜 www.tangshuang.net未经授权,禁止复制转载。而减少页面初始脚本的大小,以及初始执行所【未经授权禁止转载】原创内容,盗版必究。占用的时间。但这也不可避免的带来了编程上【关注微信公众号:wwwtangshuangnet】【作者:唐霜】的一些麻烦。有舍才有得,这不一定是最好的原创内容,盗版必究。【版权所有】唐霜 www.tangshuang.net方案,更不是放之四海而皆准的方案,只有在本文作者:唐霜,转载请注明出处。【版权所有】唐霜 www.tangshuang.net你的项目需要通过这种方式加载更有利的情况【原创不易,请尊重版权】本文版权归作者所有,未经授权不得转载。下,才使用这种方案,才是正确的选择。

【访问 www.tangshuang.n【原创内容,转载请注明出处】【版权所有】唐霜 www.tangshuang.netet 获取更多精彩内容】本文版权归作者所有,未经授权不得转载。

2020-01-12 4783 ,

为价值买单,打赏一杯咖啡

本文价值47.83RMB