转载地址
- 模块作用域
- 模块之间不需要考虑全局命名空间冲突的问题
- 模块之间的通讯规则
- 首先各个模块之间是相互依赖,相互关联的,例如cpu需要读取内存中的数据来进行计算,然后把计算结果交给了我们的操作系统
- 既然相互关联,那么模块之间肯定可以相互通讯
- 模块之间的相互通讯,也就意味着存在输入和输出
模块通讯规则
ES6出现之前就已经出现了js的模块加载规则,最主要的是AMD、CMD、commonjs;而commonjs主要应用于服务器端(nodejs)实现同步加载 AMD主要应用于浏览器端(requirejs)为异步加载 ,同事还有CMD规范,为同步加载规范seajs
commonjs规范
commonjs规定每个文件就是一个模块,有自己的作用域,在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
require模块导出
commonjs是个同步加载 (所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间)
模块的加载过程
- 假设a文件夹下有个a.js我们要解析出一个绝对路径来
- 我们写的路径可以没有后缀名
- 得到一个加载路径,先看一下缓存中是否有文件存在存在就返回,不存在就创建一个
- 得到对应的文件内容,加一个闭包,把内容塞进去,之后执行即可
简单实现commonjs
- 想要实现就要了解都做了什么,首先来分析一下require的逻辑
- 首先想要从a模块引入b模块一定要拿到b模块,拿到b模块要先拿到b模块的绝对路径
- 因为require方法中的参数可能为不带扩展名的文件,所以要根据匹配不同的文件后缀名来匹配文件,不同的文件的处理方式不一样
1.首先拿到文件(用到了fs),最后正常情况下要返回这个模块
function require(filename){
// 1.解析文件路径
filename = Module._resolveFilename(filename)
// 返回这个模块
return module.exports
}
1.1实现路径的获取方法
Module._resolveFilename = function (filename){
let filePath = path.resolve(__dirname,filename)
// 看一下这个filePath对应的文件是否存在
let isExists = fs.existsSync(filePath)
if(isExists) return filePath;
// 通过添加后缀名来拼接新路径(先假设有个后缀名的数组对象,因为每个后缀名会对应一个处理这种后缀的方法,所以为一个对象)
let keys = Reflect.ownKeys(Module._extensions)
keys.forEach(key=>{
let newPath = key + filePath
if(fs.existsSync(newPath)) return newPath;// 匹配路径存在就返回
})
throw new Error(Module not found)
}
拿到路径后根据路径看看这个模块是否被缓存,如果没有缓存就创建一个新的模块,如果缓存了就直接返回缓存中的内容。这样是为了防止重复执行模块中的代码
// 先定义个一个缓存对象
let Module._cache = {}
function require(filename){
// 1.解析文件路径
filename = Module._resolveFilename(filename)
if(Module._cache[filename]){// 如果缓存中存在这个模块直接把这个模块的exports返回
return Module._cache[filename].exports;
}
// 如果不存在就创建一个模块,并将这个模块塞到缓存中
let module = new Module(filename);
Module._cache[filename] = module
// 返回这个模块
return module.exports
}
现在模块已经有了,需要一个创建模块的构造函数,来实现这个构造函数
function Module(filename){
this.id = filename//文件名称
this.exports = {} // 输出对象
this.path = path.dirname(filename);//父路径
}
接下来加载这个模块
let Module._cache = {}
function require(filename){
// 1.解析文件路径
filename = Module._resolveFilename(filename)
if(Module._cache[filename]){// 如果缓存中存在这个模块直接把这个模块的exports返回
return Module._cache[filename].exports;
}
// 如果不存在就创建一个模块,并将这个模块塞到缓存中
let module = new Module(filename);
Module._cache[filename] = module
// 加载模块
module.load()
// 返回这个模块
return module.exports
}
// load 是一个原型上的方法
Module.prototype.load = function(){
// 通过文件名来找模块的后缀名匹配对应的策略
let extension = path.extname(this.id)
Module._extensions[extension](this)
}
接下来编写不同文件的策略
// 先定义一个对象用来放策略
Module._extensions = {}
// 当文件为json文件的时候读取文件内容直接返回
Module._extensions['.json] = function(module){
let content = fs.readFileSync(module.id,'utf8')
module.exports = JSON.parse(content)
}
// 写一个wrapper方法
Module.wrapper = function(content){
return `(function(exports,module,require,__dirname,__filename){${content}})`
}
// 当文件为js文件的时候读取文件内容直接返回
Module._extensions['.js] = function(module){
let content = fs.readFileSync(module.id,'utf8')
// js文件不同于json不能直接传输,所以用个闭包包一下(jsonp)
let str = Module.wrapper(content)
// 将这个字符串转成函数
let fn = vm.runInThisContext(str)
// 在新的文件中释放出这个content
fn.call(exports,exports,module,require,module.id,module.path)
}
结束
AMD 规范
- AMD规范不同于commonjs,AMD跑在浏览器端,加载模块需要依赖网络请求,所以不能同步执行,只能异步加载模块
模块的定义语法
define([id], [dependencies], factory)
- id:可选,字符串类型,定义模块标识,如果没有提供参数,默认为文件名
- dependencies:可选,字符串数组,AMD 推崇依赖前置,即当前模块依赖的其他模块,模块依赖必须在真正执行具体的factory方法前解决
- factory:必需,工厂方法,初始化模块需要执行的函数或对象。如果为函数,它只被执行一次。如果是对象,此对象会作为模块的输出值
模块的加载
require([dependencies], function(){})
-
dependencies:字符串数组,该模块的依赖
-
function:Function类型,所依赖的模块都加载成功之后,回调,依赖的模块会以参数的形式传入该函数,从而在回调函数内部就可以使用这些模块
-
模块的定义种类
- 无依赖模块
// base.js define(function () { return { bas: function (sou, tar) { // ... } } })
- 有依赖模块
// ui.js 数组中可放多个模块 define(['base'], function (base) { return { initPage: function () { // ... } } })
- 数据对象模块
// data.js define({ users: [], numbers: [] })
- 具名模块
define('index', ['data','base'], function(data, base) { // ... })
- 包装模块
define(function(require, exports, module) { var base = require('base'); exports.show = function() { // todo with module base } })