在es6之前,js不像其他语言自带成熟的模块化功能,页面只能靠插入一个个script标签来引入自己的或第三方的脚本,并且容易带来命名冲突的问题。js社区做了很多努力,在当时的运行环境中,实现"模块"的效果。
通用的js模块化标准有CommonJS与AMD,前者运用于node环境,后者在浏览器环境中由Require.js等实现。此外还有国内的开源项目Sea.js,遵循CMD规范。(目前随着es6的普及已经停止维护,不论是AMD还是CMD,都将是一段历史了)
浏览器端js加载器
实现一个简单的js加载器并不复杂,主要可以分为解析路径、下载模块、解析模块依赖、解析模块四个步骤。
首先定义一下模块。在各种规范中,通常一个js文件即表示一个模块。那么,我们可以在模块文件中,构造一个闭包,并传出一个对象,作为模块的导出:
define(factory() { var x = { a: 1 }; return x;});
define函数接收一个工厂函数参数,浏览器执行该脚本时,define函数执行factory,并把它的return值存储在加载器的模块对象modules里。
如何标识一个模块呢?可以用文件的uri,它是唯一标识,是天然的id。
文件路径path有几种形式:
绝对路径:http://xxx, file://xxx 相对路径:./xxx , ../xxx , xxx(相对当前页面的文件路径) 虚拟绝对路径:/xxx /表示网站根目录因此,需要一个resolvePath函数来将不同形式的path解析成uri,参照当前页面的文件路径来解析。
接着,假设我们需要引用a.js与b.js两个模块,并设置了需要a与b才能执行的回调函数f。我们希望加载器去拉取a与b,当a与b都加载完成后,从modules里取出a与b作为参数传给f,执行下一步操作。这里可以用观察者模式(即订阅/发布模式)实现,创建一个eventProxy,订阅加载a与加载b事件;define函数执行到最后,已经把导出挂载modules里之后,emit一个本模块加载完成的事件,eventProxy收到后检查a与b是否都加载完成,如果完成,就传参给f执行回调。
同理,eventProxy也可以实现模块依赖加载
// a.jsdefine([ 'c.js', 'd.js' ], factory (c, d) { var x = c + d; return x;});
define函数的第一个参数可以传入一个依赖数组,表示a模块依赖c与d。define执行时,告诉eventProxy订阅c与d加载事件,加载好了就执行回调函数f存储a的导出,并emit事件a已加载。
浏览器端加载脚本的原始方法是插入一个script标签,指定src之后,浏览器开始下载该脚本。
那么加载器中的模块加载可以用dom操作实现,插入一个script标签并指定src,此时该模块为下载中状态。
PS:浏览器中,动态插入script标签与初次加载页面dom时的script加载方式不同:
初次加载页面,浏览器会从上到下顺序解析dom,碰到script标签时,下载脚本并阻塞dom解析,等到该脚本下载、执行完毕后再继续解析之后的dom(现代浏览器做了preload优化,会预先下载好多个脚本,但执行顺序与它们在dom中顺序一致,执行时阻塞其他dom解析)
新闻热点
疑难解答
图片精选