打造史上最强模块加载工具

AI权益加码!Claude Code、Cursor等20+工具免费用! 购周边限时加赠Coding Plan Lite,畅享主流AI工具!学习进阶更高效! 阅读详情

这次要记录的是一个很简单的但是基本符合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 监听模块的加载状况。我们只需要缓存对应模块的返回值即可,所以可以监听 script 标签的 onload 事件,在模块缓存成功之后删除对应的 script 标签。

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

define([ dependents ], factory( results ))

上面也说到,script 标签会立即执行加载成功的模块,所以如果在此之前我们的 define 函数已经被挂载到全局的话,define 函数会被立即执行,完成模块的定义工作。

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

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

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

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

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

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

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

事件发射器的代码如下:

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 的方式做出标记,现在我们打开页面观察控制台:

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

打造最强终端之一:Fish shell简明教程 本系列文章包括3篇,主要分享我在后端开发过程中使用终端的一些经验和心得,分享fish shell使用使用tmux终端复用管理会话、窗口,以及vim插件使用,让vim变成强大的IDE。通过阅读本系列文章,你也可以打造一个高效易用的终端,能很大的提高工作的效率,让手指飞起来,让命令行飞起来。 本系列的3篇文章包括: 打造最强终端之一:Fish shell简明教程 打造最强终端之二:Tmux简明教程 ... 阅读详情

相关推荐

JavaScript 模块化 - AMD 规范与 RequireJS 前端开发

模块加载是非阻塞的,可以并行加载多个模块,提高页面加载性能。RequireJS 是一个符合 AMD 规范的模块加载器,它提供了一种简洁的方式来定义和加载模块。RequireJS 提供了一个强大的模块加载器,使得模块的定义和加载变得简单而灵活。在实际开发中,我们可以把每个模块定义在独立的文件中,并使用文件路径作为模块标识符。当我们使用 RequireJS 加载模块时,它会自动根据模块标识符解析并加载相应的文件。我们可以在回调函数中执行一些与模块无关的逻辑。接下来,我们可以定义一个模块,并指定其依赖关系。

CflfEditor的博客 313

很不错的一些delphi程序模块

作者: 姚乔锋 ComputerInfo,CryptoAPI,AES,RegExpr,SHA……

关键词分词工具(宏命令) 改进版(.rar

竞价员关键词分词工具史上最强大的,足够一个新手变成牛叉

vue3+vite+vant 按需导入报Internal server error: __require.resolve is not a function

vue3+vite+vant 按需导入报Internal server error: __require.resolve is not a function,原因在于vite-plugin-style-import版本引入太低

lch1399038292的博客 2673

vite构建项目按需引入vant时vite-plugin-style-import 报错 require.resolve is not a function

最近开始使用vite去替代了传统的脚手架vue- cli ,为什么好好的脚手架不用,开始追求了vite,时过境迁,我们见证了诸如webpack、Rollup和Parcel等工具的变迁,它们极大地改善了前端开发者的开发体验。在将来 一定是vue3 + vite + ts时代。 下面说一下用vite简单构建项引入vant一些流程 和 报错解决 1.搭建vite项目 使用npm npm init vite@latest 使用 Yarn yarn create vite 使用 PNPM ...

m0_57874230的博客 6488

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

这次要记录的是一个很简单的但是基本符合AMD规范的浏览器端模块加载工具的开发流程。因为自从使用过require.js、webpack等模块加载工具之后就一直对它的实现原理很好奇,于是稍微研究了一下。 实现的方法有许多,但简单实现的话大致都会实现出以下的两个方法: 1 实现模块加载。从主模块说起,我们需要通过一个入口来加载我们的主模块的依赖模块,同时在加载完依赖之后,能够取得所依赖模块的返回...

weixin_30511039的博客 179

揭秘Nyx GUI:CTCaer如何打造任天堂Switch最强启动加载

想要解锁你的Switch全部潜力吗?Nyx GUI正是你需要的终极解决方案!作为CTCaer团队开发的革命性图形界面,Nyx彻底改变了Switch启动加载器的使用体验,让普通用户也能轻松驾驭高级功能。🚀 ## 什么是Nyx GUI? Nyx GUI是hekate启动加载器的图形用户界面,专为任天堂Switch设备设计。这个强大的工具不仅支持触摸屏和Joycon输入,还提供了完整的配置选项和主

gitblog_00853的博客 1029

csrutil是什么_Mac用户如何打造史上最强终端

简介捣鼓一下,Mac下的终端主题,最开始我选择的是通过oh-my-zsh提供的我选择的是powerlevel9k,安装还挺复杂,接下来我介绍一下。安装powerlevel9k安装powerlevel9kgit clone https://github.com/bhilburn/powerlevel9k.git ~/.oh-my-zsh/custom/themes/powerlevel9k你需要修改...

weixin_39658726的博客 3043

史上最强鸿蒙教程来袭!看了不怕你学不懂

随着科技的飞速发展,鸿蒙系统作为华为自主研发的操作系统,受到了越来越多人的关注。据了解,华为HarmonyOS NEXT开发者预览版将于今年一季度面向所有开发者正式开放。该系统底座全线自研,去掉了传统的Linux内核以及AOSP(安卓开放源代码项目)等代码,仅支持鸿蒙内核和鸿蒙系统的应用。根据此前数码博主体验,HarmonyOS NEXT不再兼容安卓应用,如果打开安卓APK文件,会提示“无法打开此文件”,因此该系统也被称为“纯血鸿蒙”。这意味着华为正逐步摆脱对安卓的依赖,并打造自己的生态系统。

bugyinyin的博客 1311

PoeCharm:终极流放之路中文角色构建工具,轻松打造最强BD

还在为《流放之路》复杂的角色构建而头疼吗?面对全英文的Path of Building,你是否感到无从下手?PoeCharm作为专业的流放之路中文角色构建工具,为你提供完整的解决方案,让天赋规划、装备搭配、伤害计算变得简单直观! PoeCharm是专为中文玩家打造的Path of Building中文版本,集成了完整的中文翻译和优化界面,让你能够轻松规划流放之路角色构建,优化装备搭配,计算实时伤

gitblog_00981的博客 173

go2rtc 视频流转发实操笔记:从接入一路 RTSP 摄像头到浏览器观看

摄像头的 RTSP 流在浏览器里基本没法直接看,手机上更不行。你需要中间加一层视频流转发:把原始流接进来,再以客户端能理解的格式输出。go2rtc 是干这件事的单二进制选项,Windows、macOS、Linux 开箱即用。 ## 三分钟把它跑起来:Docker、二进制或源码编译 Docker 是决策最少的一条路,镜像里预装了 FFmpeg 和 Python,一条命令启动服务: ```bas

gitblog_01195的博客 161

全栈工程师-史上最强VSCODE插件-提高开发效率

当你点进来的时候 ,你可能是被标题吸引进来的,也有可能是 偶然间,看到的,首先恭喜你,已经准备好向全栈开发工程师靠近 ,那我们不说废话,直接开始,咱们先从安装步骤开始讲起 ,因为有些人连插件在哪都不知道 记得点关注哦 !防止迷路 VSCODE 插件安装步骤 : 这里我们开始以 VSCode Icons.为例,有点基础的,麻烦跳过。。。。。 一、安装拓展 拓展插件如上图所示。 首先VSco...

程序员小刘 5113

EmbeddingGemma:最强开源轻量嵌入模型,专为端侧AI打造

文章优先发布在,有些文章未来得及同步,可以直接关注公众号查看。

熵增定律28的博客 1541

sentinel 史上最全

基本概念 开发的原因,需要对吞吐量(TPS)、QPS、并发数、响应时间(RT)几个概念做下了解,查自百度百科,记录如下: 响应时间(RT)   响应时间是指系统对请求作出响应的时间。直观上看,这个指标与人对软件性能的主观感受是非常一致的,因为它完整地记录了整个计算机系统处理请求的时间。由于一个系统通常会提供许多功能,而不同功能的处理逻辑也千差万别,因而不同功能的响应时间也不尽相同,甚至同一功能在不同输入数据的情况下响应时间也不相同。所以,在讨论一个系统的响应时间时,人们通常是指该系统所有功能的平均时间或

架构师尼恩 2723

5步掌握Path of Building:打造流放之路最强角色的完整指南

Path of Building是一款专为《流放之路》玩家设计的离线角色构建规划工具,能够帮助你精准计算伤害输出、优化天赋树配置、管理装备方案,让你的角色构建过程更加科学高效。无论你是初入Wraeclast大陆的新手,还是追求极致伤害的资深玩家,这款强大的角色构建工具都能为你的游戏体验带来质的飞跃。通过Path of Building离线构建工具,你可以实现流放之路角色规划、天赋树优化和伤害计算,

gitblog_00872的博客 909

终极指南:3步快速安装BetterRenderDragon,打造最强Minecraft渲染龙增强工具

BetterRenderDragon(简称BRD)是一款专为Minecraft Bedrock Edition设计的渲染增强工具,通过优化渲染管线和材质处理,让玩家体验更细腻的光影效果与流畅画面。这款强大的渲染龙增强工具能够显著提升Minecraft的画质表现,为你的游戏世界带来全新的视觉体验。 ## 🎯 项目概述与核心价值 BetterRenderDragon是一个专注于提升Minecra

gitblog_00372的博客 112

Path of Building终极指南:如何用离线构建工具打造《流放之路》最强角色

还在为《流放之路》复杂的角色构建而头疼吗?面对海量天赋节点、装备搭配和技能组合,你是否感到无从下手?别担心,今天我将为你介绍一款改变游戏体验的神器——Path of Building离线构建工具。这个强大的角色构建规划器能够帮助你精准计算伤害输出、优化天赋树配置、管理装备方案,让你在Wraeclast大陆的冒险更加科学高效。 ## 为什么你需要Path of Building?解决三大构建难题

gitblog_00507的博客 387
上一篇: leetcode算法题(JavaScript实现)
下一篇: gulp源码解析(二)—— vinyl-fs
aabv54321
博客等级 码龄10年 25粉丝 35原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值