开发史上最强模块加载工具

haha好吧,我承认这篇文章有点标题党了。

这次要记录的是一个很简单的但是基本符合AMD规范的浏览器端模块加载工具的开发流程。因为自从使用过require.js、webpack等模块化加载工具之后就一直对它的实现原理很好奇,于是稍微研究了一下。

实现的方法有许多,但简单实现的话大致都会有这样几个过程:

1 实现模块的加载。从主模块说起,我们需要通过一个入口来加载我们的主模块的依赖模块,同时在加载完主模块依赖之后,能够取得所各依赖模块的返回值,并将它们传入主模块代码中,再去执行我们的主模块代码。函数入口类似于这样的形式:

require([ dependents ], function( ){
    // 主模块代码
})    

至于如何去加载我们的依赖模块,这里一般可以有两种处理方式,一种是通过Ajax请求依赖模块,一种是为依赖模块动态创建 script 标签加载依赖模块,在这里我选择第二种方式,不过如果你需要加载文本文件或者JSON文件的话,建议你还是采用Ajax加载的方式,至于为什么,接下来会说明,但这里为了简单处理我们不考虑这种情况。

所以我们会遍历主模块的依赖数组,对依赖模块的路径做简单的处理之后,动态创建 script 标签加载每一个依赖模块。所谓的加载模块,其本质便是通过网络请求将模块 Fetch 到本地。同时我们还需要知道通过 script 标签加载资源的两个特点:

1.1  script 标签加载到JS代码之后会立即执行这一段代码。JSONP也利用了 script 标签的这个特性。

1.2 可以通过 script.onload 和 script.onerror 监听模块的加载状况。

2 实现模块的定义。在AMD规范中,每一个模块的编写我们需要遵循类似于这样的形式:

define([ dependents ], factory)

上面也说到,script 标签会立即执行所加载成功的模块,所以如果在此之前我们的 define 函数被挂载到全局的话,define 函数会被传入  ([ dependents ], factory)  等参数后执行,以完成模块的定义工作。

关于模块定义的概念这里需要说一下,我们的模块定义,是指成功将模块的返回值(或者该模块的全部代码) cache 到我们的本地缓存当中,我们会使用一个变量负责去缓存所有的依赖模块以及这些依赖模块所对应的模块ID,所以每次在执行 require 方法或者 define 方法之前我们都会去检查一下所依赖的模块在缓存中是否存在(根据模块ID查找),即是否已经成功定义。如果已经成功定义过了,我们便会忽略对此模块的处理,否则就会去调用 require 方法加载并定义它。待所依赖模块数组对应全部模块都检查已经成功定义过之后,我们再从缓存中取出这些依赖模块的返回值传入 factory 方法当中执行主模块或者 cache 我们当前定义的模块。

以上就是一个简单的模块加载器的一般原理了,具体细节再在下面具体说明。

所以我们的关键是实现 require 和 define 方法。不过在这里有一个重要的细节需要我们处理,前面有提到过,我们的每一次 require 或者 define 之前会去检查所依赖模块是否都已经完全定义,再去定义未定义的依赖模块,那如果所有的依赖模块都已经全部完成定义,我们的 require 或者 define 怎么样才能即时的知晓到这一情报呢?

我们可以借助于实现一个类似于 Nodejs 当中 EventEmiter 模块的事件发射器去完成我们的需求。

这个事件发射器有两个主要的方法 watch 和 emit。

watch :我们在加载依赖模块的同时,将我们的依赖模块数组和回调函数传入事件发射器的 watch 方法,watch 方法会创建一个任务,监听所传入依赖模块数组的加载状况,并且当检测到所依赖模块数组模块全部定义成功之后主动触发之前传入的回调函数,执行接下来的逻辑。

emit :每次有模块被定义成功,便会调用事件发射器的 emit 方法发送一个模块定义成功的信号,同时事件发射器会调用 deal_loaded 方法检查一遍当前定义成功的依赖模块所在依赖模块数组是否全部定义成功,如果是的话,再去调用 excute 方法执行当前完全定义成功的依赖模块数组所对应的回调函数。

事件发射器的代码如下:

var utils = {

        ......
      
        proxy : (function(  ){

            var tasks = {  }

            var task_id = 0

            var excute = function( task ){
                console.log( "excute task" )
                var urls = task.urls
                var callback = task.callback
                var results = [  ]
                for( var i = 0; i < urls.length; i ++ ){
                    results.push( modules[ urls[ i ] ] )
                }
                callback( results )
            }

            var deal_loaded = function( url ){
                console.log( "deal_loaded " + url )
                var i, k, sum = 0
                for( k in tasks ){
                    if( tasks[ k ].urls.indexOf( url ) > -1 ){
                        for( i = 0; i < tasks[ k ].urls.length; i ++ ){
                            if( m_methods.isModuleCached( tasks[ k ].urls[ i ] ) ){
                                sum ++
                            }
                        }
                        if( sum == tasks[ k ].urls.length ){
                            excute( tasks[ k ] )
                            delete( tasks[ k ] )
                        }
                    }
                }
            }

            var emit = function( m_id ){
                console.log( m_id + " was loaded !" )
                deal_loaded( m_id )
            }

            var watch = function( urls, callback ){
                console.log( "watch : " + urls )
                var sum
                for( var i = 0; i < urls.length; i ++ ){
                    if( m_methods.isModuleCached( urls[ i ] ) ){
                        sum ++
                    }
                }
                if( sum == urls.length ){
                    excute({ urls : urls, callback : callback })
                } else {
                    console.log( "创建监听任务 : " )
                    var task = { urls : urls, callback : callback }
                    tasks[ task_id ] = task
                    task_id ++
                    console.log( task )
                }
            }

            return {
                emit : emit, watch : watch
            }
        })(  )
}

define方法实现:

var define = function(deps, factory) {
    console.log("define...")
    var _deps = factory ? deps : [],
          _factory = factory ? factory : deps
    new Module(_deps, _factory)
}
function Module(deps, factory) {
        var _this = this
        _this.m_id = doc.currentScript.src
     // 判断模块是否定义成功
        if (m_methods.isModuleCached(_this.m_id)) {
            return
        }
        if (arguments[0].length == 0) {
            // 没有依赖模块
            _this.factory = arguments[1]
            // 模糊定义成功,取返回值添加到缓存中
            m_methods.cacheModule(_this.m_id, _this.factory())
            utils.proxy.emit(_this.m_id)
        } else {
            // 有依赖模块
            _this.factory = arguments[1]
       // 加载依赖模块 
            require(arguments[0], function(results) {
                m_methods.cacheModule(_this.m_id, _this.factory(results))
                utils.proxy.emit(_this.m_id)
            })
        }
}

require方法:

var require = function(deps, callback) {
        console.log("require " + deps)
        if (!Array.isArray(deps)) {
            deps = [deps]
        }
        var urls = []
        for (var i = 0; i < deps.length; i++) {
            // 处理模块路径
            urls.push(utils.resolveUrl(deps[i]))
        }
        utils.proxy.watch(urls, callback)
     // 加载依赖模块
        m_methods.fetchModules(urls)
}

这里有一个小细节,在处理依赖模块路径的时候,我们借助 a 标签去获取到我们需要的绝对路径,a 标签有一个特点,当我们通过 JS 去获取它的 href 值时,它始终会给我们返回相对应的绝对路径,哪怕我们之前给它的 href 值赋予的是相对路径。

所以我们的路径处理可以这么实现:

......

var _script = document.getElementsByTagName("script")[0]
    var _a = document.createElement("a")
    _a.style.visibility = "hidden"
    document.body.insertBefore(_a, _script)

......

var utils = {

        resolveUrl: function(url) {
            _a.href = url
            var absolute_url = _a.href
            _a.href = ""
            return absolute_url
        },

        ......
}

至此我们的模块加载工具的主要功能都已大致实现。完整代码在 https://github.com/KellyLy/loader.js

现在可以测试一下。我们现在有a、b、c、d四个模块,分别是:

以及主模块:

一切就绪,我们在关键区域都以打印 log 的方式做出标记,现在我们打开页面观察控制台:

模块加载工具的整个加载流程在控制台里我们都可以观察得到,清晰明了。至此,这篇文章就结束啦,最后祝大家新年快乐!

时间: 2024-10-18 17:19:50

开发史上最强模块加载工具的相关文章

JavaScript AMD 模块加载器原理与实现

关于前端模块化,玉伯在其博文 前端模块化开发的价值 中有论述,有兴趣的同学可以去阅读一下. 1. 模块加载器 模块加载器目前比较流行的有 Requirejs 和 Seajs.前者遵循 AMD规范,后者遵循 CMD规范.前者的规范产出比较适合于浏览器异步环境的习惯,后者的规范产出对于写过 nodejs 的同学来说是比较爽的.关于两者的比较,有兴趣的同学请参看玉伯在知乎的回答 AMD和CMD的区别有哪些.本文希望能按照 AMD 规范来简单实现自己的一个模块加载器,以此来搞清楚模块加载器的工作原理.

JavaScript之:模块加载程序的历史与背景

原文:History and Background of JavaScript Module Loaders 作者:Elias Carlston 翻译:leotso 介绍 Web 应用程序的应用程序逻辑不断从后端移到浏览器端.但是,由于富客户端 JavaScript 应用程序的规模变得更大,它们遇到了类似于多年来传统应用所面临的挑战:共享代码以便重用,同时保持架构的隔离分层,并且足够灵活以便于轻松扩展. 这些挑战的一个解决方案是开发 JavaScript 模块和模块加载系统.这篇文章将着重于比较

关于前端JS模块加载器实现的一些细节

最近工作需要,实现一个特定环境的模块加载方案,实现过程中有一些技术细节不解,便参考 了一些项目的api设计约定与实现,记录下来备忘. 本文不探讨为什么实现模块化,以及模块化相关的规范,直接考虑一些技术实现原理. 1.简单实现模块化 一开始我想如果我的代码只有一个文件,那几行不就实现了吗 main.js var modules = {} var define = function(id,factory){ moudles[id] = factory } var require = function

js模块化/js模块加载器/js模块打包器

之前对这几个概念一直记得很模糊,也无法用自己的语言表达出来,今天看了大神的文章,尝试根据自己的理解总结一下,算是一篇读后感. 大神的文章:http://www.css88.com/archives/7628(大神的文章写的很详细,建议先看完大神的文章) 一.js模块化 什么是js模块化,我们从历史说起. 1.一开始我们怎么写脚本?就是在html文件中用<script></script>写代码 这种方式的缺点:代码复用靠复制,基本是全局变量. 2.后来我们用js文件写代码,用<

Node.js【6】Web开发、进阶(模块加载、控制流、部署、弊端)

笔记来自<Node.js开发指南>BYVoid编著 实现过程:https://github.com/ichenxiaodao/express-example 第5章 使用Node.js进行Web开发 从零开始用Node.js实现一个微博系统,功能包括路由控制.页面模板.数据库访问.用户注册.登录.用户会话等内容. 会介绍Express框架.MVC设计模式.ejs模板引擎以及MongoDB数据库的操作. 5.1.准备工作 Express(http://expressjs.com/)除了为http

第三课:模块加载系统

模块加载,其实就是把js分成很多个模块,便于开发和维护.因此加载很多js模块的时候,需要动态的加载,以便提高用户体验. 在介绍模块加载库之前,先介绍一个方法. 动态加载js方法: function loadJs(url , callback){ var node = document.createElement("script"); node[window.addEventListener ? "onload":"onreadystatechange&qu

自研模块加载器(三) module模块构造器设计-模块数据初始化

依赖加载策略 模块数据初始化 status状态生命周期 代码展示 demo包括4个文件, index.html , a.js , b.js , startUp.js index.html <!DOCTYPE html> <html> <head> <title>自研模块加载器</title> </head> <body> <script src="./startUp.js"></scr

自研模块加载器(四) 模块资源定位-异步加载

资源定位-动态加载 通过resolve方法进行异步解析,完整解析如下图所示: 根据上篇文章startUp.js代码,我们继续完善本章动态加载资源的代码. (function(global) { var startUp = global.startUp = { version: '1.0.1' } var data = {}; // 获取当前模块加载器配置信息 var cache = {}; // 缓存 //模块的生命周期 var status = { FETCHED: 1, SAVED: 2,

Vs2013在Linux开发中的应用(22):模块加载

快乐虾 http://blog.csdn.net/lights_joy/ 欢迎转载,但请保留作者信息 当gdb检测到模块的加载时会输出: =library-loaded,id="/lib/ld-linux.so.2",target-name="/lib/ld-linux.so.2",host-name="/lib/ld-linux.so.2",symbols-loaded="0",thread-group="i1&q