实现简单的 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的顺 ...
随机推荐
- [Blog] Part1: 技术札记-写个创站小结吧
创站绝对是一个大坑 我当初真有勇气.. 嗯 这个站主要就是 Github+Jekyll+markdown 基本上还是现在能用的比较习惯的模式 基本流程概述 域名 -> 修改DNS -> g ...
- 破解版 Teamver 安装
一 .下载安装包 百度网盘链接:https://pan.baidu.com/s/18nEKAMmHEqU66Dq_aCnEYQ 提取码:2x2q 二.解压缩后,直接运行红框内绿色文件即可
- IntelliJ IDEA搭建一个简单的springboot项目
一.IDEA 安装包 百度网盘链接:https://pan.baidu.com/s/1MYgZaBVWXgy64KxnoeJSyg 提取码:7dh2 IDEA注册码获取:http://idea.lan ...
- [PAT] A1022 Digital Library
[题目大意] 给出几本书的信息,包括编号,名字,出版社,作者,出版年份,关键字:然后给出几个请求,分别按照1->名字,2->出版社等对应信息查询符合要求的书的编号. [思路] 模拟. [坑 ...
- Codeforces Round #617 (Div. 3) 补题记录
1296A - Array with Odd Sum 题意:可以改变数组中的一个数的值成另外一个数组中的数,问能不能使数组的和是个奇数 思路:签到,如果本来数组的和就是个奇数,那就OK 如果不是,就需 ...
- Tomcat报错Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986
问题描述:后台报错 Note: further occurrences of HTTP header parsing errors will be logged at DEBUG level.java ...
- hyper-v虚拟机不能访问外网的解决方案
直接说解决方案,将虚拟机的一张网卡改为旧版网络适配器即可.具体原因还不可知. 延申一下,一般应该使用的交换机,是“外部”类型即可.
- 获取properties文件的内容
获取properties文件的内容 public void test() throws Exception{ String resource = "application.propertie ...
- 蓝桥杯第十届C组试题C
从0开始,从右到左给这些字符串的每一位字母起个名字. 比如:A(1位)A(0位) A(2位)A(1位)A(0位) AA = 27, 可以看成(26 * 1)+ A(1) 因为:字母每经过一个轮回,可就 ...
- bfs(队列模板)
[题目描述] 当你站在一个迷宫里的时候,往往会被错综复杂的道路弄得失去方向感,如果你能得到迷宫地图,事情就会变得非常简单. 假设你已经得到了一个n*m的迷宫的图纸,请你找出从起点到出口的最短路. [输 ...