实现简单的 JS 模块加载器
实现简单的 JS 模块加载器
1. 背景介绍
按需加载是前端性能优化的一个重要手段,按需加载的本质是从远程服务器加载一段JS代码(这里主要讨论JS,CSS或者其他资源大同小异),该JS代码就是一个模块的定义,如果您之前有去思考过按需加载的原理,那你可能已经知道按需加载需要依赖一个模块加载器。它可以加载所有的静态资源文件,比如:
- JS 脚本
- CSS 脚本
- 图片 资源
如果你了解 webpack,那您可以发现在 webpack 内部,它实现了一个模块加载器。模块加载器本身需要遵循一个规范,当然您可以自定义规范,大部分运行在浏览器模块加载器都遵循 AMD 规范,也就是异步加载。
容易理解的是,对于某个应用使用了模块加载器,那么首先需要加载该模块加载器JS代码。然后有一个主模块,程序从主模块开始执行, requireJS 中使用main来标记,webpack 中叫 webpackBootstrap 模块。
2. 实现简单的加载器
2.1 需求整理
- 模块的定义
- 模块的加载
- 已经加载过的模块需要缓存
- 同一个模块并行加载的处理
2.2 运行流程图
2.2 功能实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
1. 相同模块的并发加载问题?
</body>
<script>
// 模块加载配置
var config = {
baseDir: window.location.origin + '/module'
}
// loader 模块加载器
var loader = {
// 配置
config: {
baseDir: window.location.origin + '/module'
},
// 缓存
modules: {},
// 注册加载后的回调
installed: {},
// 标记某个模块是否已经加载
status: {},
// 定义模块
define: function(name, fn) {
this.modules[name] = fn ? fn() : undefined
},
// 加载模块
require: function(name, fn) {
if (this.modules[name]) {
// 已经加载成功, 直接从缓存读取
callback(this.modules[name])
} else {
if (this.status[name]) {
// 加载过了, 但是还未加载成功
this.installed[name].push(fn)
} else {
// 还未加载过
this.installed[name] = []
this.installed[name].push(fn)
this.loadScript(name)
this.status[name] = true
}
}
},
// 加载JS文件
loadScript: function (name, callback) {
let _this = this
let script = document.createElement('script')
script.src = this.config.baseDir + '/' + name + '.js'
script.onload = function () {
_this.installed[name].forEach(fn => fn(_this.modules[name]))
}
setTimeout(() => {
// 模拟请求时间
document.body.append(script)
}, 200)
}
}
loader.require('lazyload', function(lazyload){
console.log(Date.now())
lazyload()
})
loader.require('lazyload', function(lazyload){
console.log(Date.now())
lazyload()
})
</script>
</html>
// module/lazyload.js
loader.define('lazyload', function(){
return function () {
console.log('I am lazyload')
}
})
这个版本已经是简单的不能再简单了,首先它没有对模块加载失败设计异常处理机制,其次真实的场景中存在一个模块的定义依赖其他模块:
// toolbar 模块依赖common模块
loader.define('toolbar', ['common'], function(){
return function () {
console.log('I am toolbar')
}
})
3. 模块的定义依赖其他模块
3.1 程序分析
假如模块A依赖模块B和C,这里有几个关键点:
- 如何判断B和C都加载完,然后再执行模块A的导出函数
- 使用 define 定义模块A时,需要先收集依赖,然后当A模块加载完成后(loadScript),再加载其依赖项,当所有依赖项都加载完成后,再获取模块A的导出值。
演示代码:
// 定义模块
loader.define('toolbar', ['common', 'lazyload'], function(){
return function () {
const { common, lazyload } = loader.modules; // 通过这样访问依赖
console.log('I am toolbar')
}
})
// 加载模块
loader.require('lazyload', function(){
console.log('require lazyload')
})
3.2 代码实现
/**
* loader 模块加载器
*/
var loader = {
config: { // 配置
baseDir: window.location.origin + '/module'
},
modules: {}, // 缓存
installed: {}, // 加载成功
status: {}, // 加载状态
deps: {}, // 模块的依赖
moduleDefined: {}, // 缓存模块的定义
/**
* @description 注册模块, 每个模块最多只会注册1次
* @example:
* define('sleep', function(){ return 5 }) 定义模块名为sleep,导出值为5
* define('sleep', function(){ return { name: 5 } }) 定义模块名为sleep,导出一个对象
* define('sleep', function(){ return function(){ console.log(5)}}) 定义模块名为sleep,导出一个函数
*/
define: function() {
let name, fn, deps, args = arguments
if (args.length === 2) {
name = args[0]
fn = args[1]
} else if (args.length === 3) {
name = args[0]
deps = (typeof args[1] === 'string') ? [args[1]] : args[1]
fn = args[2]
} else {
throw "invalid params for define function"
}
// 收集依赖
if (deps) this.deps[name] = deps;
// 缓存模块导出函数
this.moduleDefined[name] = fn
},
/**
* @description 加载模块
* @param {string} name 模块名
* @param {*} requireCb 加载完成回调函数
*/
require: function(name, requireCb) {
if (this.modules[name]) {
// 已经加载成功, 直接从缓存读取
requireCb(this.modules[name])
} else {
if (this.status[name]) {
// 加载过了, 但是还未加载成功
this.installed[name].push(requireCb)
} else {
// 还未加载过
this.installed[name] = []
this.installed[name].push(requireCb)
this.loadScript(name)
this.status[name] = true
}
}
},
/**
* @description 加载多个模块
* @param {string} names 模块名数组
* @param {*} fn 回调函数
*/
requires: function(names, fn) {
let excuted = false
names.forEach(name => {
this.require(name, () => {
if (!excuted) {
// 保证回调只执行一次
if (names.filter(v => this.modules[v] !== undefined).length === names.length) {
excuted = true
fn && fn()
}
}
})
})
},
/**
* @description 加载JS文件
* @param {string} name 模块名
*/
loadScript: function (name) {
let _this = this
let script = document.createElement('script')
script.src = this.config.baseDir + '/' + name + '.js'
script.onload = function () {
// 需要注意, 当模块的JS文件加载完成, 不能立即调用require(name, fn) 所注册的fn回调函数
// 因为它可能依赖其它模块, 需要将依赖的模块也加载完成之后, 再触发
// _this.installed[name] 为数组是因为并行加载时, 注册了多个回调
if (!_this.deps[name]) {
_this.modules[name] = _this.moduleDefined[name]();
_this.installed[name].forEach(fn => {
fn(_this.modules[name]);
})
} else {
_this.requires(_this.deps[name], () => {
// 依赖项全部加载完成
_this.modules[name] = _this.moduleDefined[name]();
_this.installed[name].forEach(fn => {
fn(_this.modules[name]);
})
});
}
}
document.body.append(script)
}
}
4. 注入依赖到导出函数
在第3步骤中,虽然实现了模块定义的依赖支持,但是没有注入到导出函数中,我们希望模块的定义改成下面的样子:
// 定义模块
loader.define('toolbar', ['common', 'lazyload'], function(common, lazyload){
return function () {
// const { common, lazyload } = loader.modules; // 不使用这种方式
console.log('I am toolbar')
}
})
这个还是比较简单,只需要修改下面标识Mark的位置:
if (!_this.deps[name]) {
_this.modules[name] = _this.moduleDefined[name]();
_this.installed[name].forEach(fn => {
fn(_this.modules[name]);
})
} else {
_this.requires(_this.deps[name], () => {
// 依赖项全部加载完成
const injector = _this.deps[name].map(v => _this.modules[v]) // Mark
_this.modules[name] = _this.moduleDefined[name](...injector); // Mark
_this.installed[name].forEach(fn => {
fn(_this.modules[name]);
})
});
}
5. require 支持列表的方式
按照之前的方式,如果先后require两个模块,代码可能是:
loader.require('common', function(common){
loader.require('lazyload', function(lazyload){
console.log('require1 toolbar', Date.now())
})
})
这种嵌套的方式看起来非常糟糕,希望把它换成下面这种方式:
loader.require(['common', 'lazyload'], function(common, lazyload){
console.log('require1 toolbar', Date.now())
})
这里主要修改 require 方法:
require: function(name, requireCb) {
if (Array.isArray(name)) {
// 加载多个
this._requires(name, () => {
const injector = name.map(v => this.modules[v])
requireCb(...injector)
})
return
}
if (this.modules[name]) {
// 已经加载成功, 直接从缓存读取
requireCb(this.modules[name])
} else {
if (this.status[name]) {
// 加载过了, 但是还未加载成功
this.installed[name].push(requireCb)
} else {
// 还未加载过
this.installed[name] = []
this.installed[name].push(requireCb)
this.loadScript(name)
this.status[name] = true
}
}
}
6. 总结
通过一步一步的功能丰富,到此一个满足大部分功能的JS模块加载器就实现了。在梳理其过程中加深了我对依赖注入的理解。下面是完整代码:
/**
* loader 模块加载器
*/
var loader = {
config: { // 配置
baseDir: window.location.origin + '/module'
},
modules: {}, // 缓存
installed: {}, // 加载成功
status: {}, // 加载状态
deps: {}, // 模块的依赖
moduleDefined: {}, // 缓存模块的定义
/**
* @description 注册模块, 每个模块最多只会注册1次
* @example:
* define('sleep', function(){ return 5 }) 定义模块名为sleep,导出值为5
* define('sleep', function(){ return { name: 5 } }) 定义模块名为sleep,导出一个对象
* define('sleep', function(){ return function(){ console.log(5)}}) 定义模块名为sleep,导出一个函数
* define('sleep', ['common'], function(common){ return function(){}}) sleep模块依赖 common模块
*/
define: function() {
let name, fn, deps, args = arguments
if (args.length === 2) {
name = args[0]
fn = args[1]
} else if (args.length === 3) {
name = args[0]
deps = (typeof args[1] === 'string') ? [args[1]] : args[1]
fn = args[2]
} else {
throw "invalid params for define function"
}
// 收集依赖
if (deps) this.deps[name] = deps;
// 缓存模块导出函数
this.moduleDefined[name] = fn
},
/**
* @description 加载模块
* @param {string} name 模块名
* @param {*} requireCb 加载完成回调函数
* @examples:
* require('common', function(common){}) 加载一个模块
* require(['common', 'toolbar], function(common, toolbar){}) 加载多个模块
*/
require: function(name, requireCb) {
if (Array.isArray(name)) {
// 加载多个
this._requires(name, () => {
const injector = name.map(v => this.modules[v])
requireCb(...injector)
})
return
}
if (this.modules[name]) {
// 已经加载成功, 直接从缓存读取
requireCb(this.modules[name])
} else {
if (this.status[name]) {
// 加载过了, 但是还未加载成功
this.installed[name].push(requireCb)
} else {
// 还未加载过
this.installed[name] = []
this.installed[name].push(requireCb)
this.loadScript(name)
this.status[name] = true
}
}
},
/**
* @description 加载多个模块
* @param {string} names 模块名数组
* @param {*} fn 回调函数
*/
_requires: function(names, fn) {
let excuted = false
names.forEach(name => {
this.require(name, () => {
if (!excuted) {
// 保证回调只执行一次
if (names.filter(v => this.modules[v] !== undefined).length === names.length) {
excuted = true
fn && fn()
}
}
})
})
},
/**
* @description 处理某个模块加载完成
* @param {string} name
*/
_onLoadScriptSuccess: function(name) {
if (!this.deps[name]) {
this.modules[name] = this.moduleDefined[name]();
this.installed[name].forEach(fn => {
fn(this.modules[name]);
})
} else {
this._requires(this.deps[name], () => {
const injector = this.deps[name].map(v => this.modules[v])
this.modules[name] = this.moduleDefined[name](...injector);
this.installed[name].forEach(fn => {
fn(this.modules[name]);
})
});
}
},
/**
* @description 加载JS文件
* @param {string} name 模块名
*/
loadScript: function (name) {
let script = document.createElement('script')
script.src = this.config.baseDir + '/' + name + '.js'
script.onload = () => {
this._onLoadScriptSuccess(name)
}
document.body.append(script)
}
}
实现简单的 JS 模块加载器的更多相关文章
- 一个简单的AMD模块加载器
一个简单的AMD模块加载器 参考 https://github.com/JsAaron/NodeJs-Demo/tree/master/require PS Aaron大大的比我的完整 PS 这不是一 ...
- js模块化/js模块加载器/js模块打包器
之前对这几个概念一直记得很模糊,也无法用自己的语言表达出来,今天看了大神的文章,尝试根据自己的理解总结一下,算是一篇读后感. 大神的文章:http://www.css88.com/archives/7 ...
- JS模块加载器加载原理是怎么样的?
路人一: 原理一:id即路径 原则.通常我们的入口是这样的: require( [ 'a', 'b' ], callback ) .这里的 'a'.'b' 都是 ModuleId.通过 id 和路径的 ...
- 关于前端JS模块加载器实现的一些细节
最近工作需要,实现一个特定环境的模块加载方案,实现过程中有一些技术细节不解,便参考 了一些项目的api设计约定与实现,记录下来备忘. 本文不探讨为什么实现模块化,以及模块化相关的规范,直接考虑一些技术 ...
- 【模块化编程】理解requireJS-实现一个简单的模块加载器
在前文中我们不止一次强调过模块化编程的重要性,以及其可以解决的问题: ① 解决单文件变量命名冲突问题 ② 解决前端多人协作问题 ③ 解决文件依赖问题 ④ 按需加载(这个说法其实很假了) ⑤ ..... ...
- sea.js模块加载工具
seajs的使用 seajs是一个jS模块加载器,由淘宝前端架构师玉伯开发,它可以解决命名空间污染,文件依赖的问题.可以在一个js文件中引入另外一个js.require('a.js') 1.安装 np ...
- js 简易模块加载器 示例分析
前端模块化 关注前端技术发展的各位亲们,肯定对模块化开发这个名词不陌生.随着前端工程越来越复杂,代码越来越多,模块化成了必不可免的趋势. 各种标准 由于javascript本身并没有制定相关标准(当然 ...
- 实现一个类 RequireJS 的模块加载器 (二)
2017 新年好 ! 新年第一天对我来说真是悲伤 ,早上兴冲冲地爬起来背着书包跑去实验室,结果今天大家都休息 .回宿舍的时候发现书包湿了,原来盒子装的牛奶盖子松了,泼了一书包,电脑风扇口和USB口都进 ...
- 使用RequireJS并实现一个自己的模块加载器 (一)
RequireJS & SeaJS 在 模块化开发 开发以前,都是直接在页面上引入 script 标签来引用脚本的,当项目变得比较复杂,就会带来很多问题. JS项目中的依赖只有通过引入JS的顺 ...
随机推荐
- [TJOI2007] 足彩投注
足彩投注 题目概述 题目背景 了解足球彩票的人可能知道,足球彩票中有一种游戏叫做"胜负彩",意为猜比赛的胜负.下面是一些与胜负彩有关的术语 注 :每一组有效组合数据. 投 注:彩民 ...
- Java使用POI读取Word中的表格
个人博客 地址:https://www.wenhaofan.com/a/20190627135921 代码 package live.autu.word; import java.io.FileInp ...
- MySQL系列(一):谈谈MySQL架构
MySQL整体架构 与所有服务端软件一样,MySQL采用的也是C/S架构,即客户端(Client)与服务端(Server)架构,我们在使用MySQL的时候,都是以客户端的身份,发送请求连接到运行服务端 ...
- JSP+Servlet+Ajax实现用户增删改查的例子
一.数据库设计 用户表User 已有的测试数据 二.Java代码编写 Java EE的架构一般分为以下五层: ①.Domain ②.DAO ③.Service ④.Controller ⑤.View ...
- centos7 下 安装GeoIP2,在nginx中根据ip地址对应的国家转发请求
最近有个需求是根据用户的地理位置,访问不同的服务器,比如国外用户访问国外的服务器,国内的用户访问国内的服务器,实现的思路主要两种: 智能dns,这个需要在阿里云中注册为企业版才有提供 nginx中使用 ...
- 2020牛客寒假算法基础集训营4-F树上博弈
链接:https://ac.nowcoder.com/acm/contest/3005/F来源:牛客网 题目描述 现有一个 n 个点,n-1条边组成的树,其中 1 号点为根节点. 牛牛和牛妹在树上玩游 ...
- Django 上下文管理器,为父模板添加动态数据
1.摘要:模板继承可以减少页面内容的重复定义,实现页面内容的重用. 但是当父模板中有动态数据的话,这些动态数据在子模版中是不会显示的.我们可以通过自定义上下文处理器来解决 2.Django上下文处理器 ...
- Leetocode7道买卖股票问题总结(121+122+123+188+309+901+714)
题目1----121. 买卖股票的最佳时机I: 链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/ 给定一个数组, ...
- deepin开机自动启动服务备忘
systemctl enable mysql.service(设置开机自启) sudo systemctl start nginx.service sudo systemctl restart ngi ...
- BZOJ2190 SDOI2008 仪仗队 gcd,欧拉函数
题意:求从左下角能看到的元素个数 引理:对点(x,y),连线(0,0)-(x,y),元素个数为gcd(x,y)-1(中间元素) 即要求gcd(x,y)=1 求gcd(x,y)=1的个数 转化为2 \s ...