一、模块化历程
模块就是小而精且利于维护的低耦合代码片段。模块化是前端走向工程化最重要的一环,早期js语言层面没有模块化规范更多的是程序员利用函数、对象、自执行函数去实现分块,后来es6中将模块化规范纳入标准范围,当下常用规范是Commonjs与Esm。
前端开发为什么需要模块化
- 命名冲突和污染
- 代码冗余,无效请求多
- 文件间的依赖关系复杂
- 项目难以维护不方便使用
常见模块化规范
- Common js规范
- AMD规范
- CMD规范
- ES modules规范
二、CommonJs规范
CommonJs规范主要应用于Nodejs,由于浏览器所具备的一些特点,例如数据一般通过网路传输、而且还存在单线程阻塞的加载方式而CommonJS规范定义模块的加载是
同步
完成,因此这就让CommonJs规范不能适用于浏览器平台。CommonJs是一个超级,它是语言层面上的规范类似于ECMAScript,而模块化只是这个规范中的一个部分。
CommonJs的规范主要有以下三点
- 模块的引用
- 模块的定义
- 模块的标识
Nodejs针对以上三点的语法实现
- 任意一个文件就是一个模块,具有独立作用域
- 使用require导入其它模块
- 将模块ID传入require实现目标模块定位
- 使用module.exports与require实现模块导入与导出
- module属性及其常见信息获取
- exports导出数据及其与module.exports区别
- CommonJS规范下的模块同步加载
模块的导入导出
// 一、模块的导出
const age = 18
const addFn = (x, y) => {
return x + y
}
module.exports = {
age: age,
addFn: addFn
}
// 一、模块的导入
let obj = require('./m')
console.log(obj)
如何证明common是同步的?
let name = 'lg'
let iTime = new Date()
// 延时4s
while(new Date() -iTime < 4000) {}
module.exports = name
console.log('m.js被加载导入了')
let obj = require('./m')
console.log('01.js代码执行了') // 4秒之后陆续打印 m.js被加载导入了 01.js代码执行了 说明是同步的
Nodejs中的module属性
- 任意js文件就是一个模块,可以直接使用module属性
- id
- 返回模块标识符,一般是一个绝对路径
- filename
- 返回文件模块的绝对路径
- loaded
- 返回布尔值,表示模块是否完成加载
- parent
- 返回对象存放调用当前模块的模块
- children
- 返回数组,存放房钱模块调用的其它模块
- exports
- 返回当前模块需要暴露的内容
- paths
- 返回数组,存放不容目录下的node_modules位置
module属性的打印
module.exports = 1111
console.log(module)
let obj = require('./m')
console.log(obj); // 1111
module.exports与exports有何区别?
Commonjs中只有一个导出的api就是module.exports,而exports是nodejs为了自己方便操作给每个模块提供的一个变量,这个变量指向module.exports。
所以我们不能直接给exports赋值,这样会切断和module.exports的联系。
node中的exports使用案例1
exports.name = 'abc'
let obj = require('./m')
console.log(obj) // 和common效果一样,打印结果为 { name: 'zce' }
node中的exports使用案例2
exports = {
name: 'syy',
age: 18
}
let obj = require('./m')
console.log(obj) // 打印为空对象 {}
require属性
- 基本功能是读入并且执行一个模块文件
- resolve: 返回模块文件的绝对路径
- exrensions: 依据不同后缀名执行解析操作
- main: 返回主模块对象,也就是入口文件模块
console.log(require.main == module) // false require.main是这个模块的父级
module.exports = 'lg'
let obj = require('./m')
console.log(require.main == module) // true require.main是当前这个模块
小结
- CommonJS规范起初是为了弥补JS语言模块化缺陷
- CommonJS是语言层面的规范,当前主要用于Node.js
- CommonJS规定模块化分为引入、定义、表示符三个部分
- Moudle在任意模块中可直接使用包含模块信息
- Require 接收标识符,加载目标模块
- Exports与module.exports都能导出模块数据
- CommonJS规范定义模块的加载是
同步
完成,也是这个特点让他不适用于浏览器平台.
Node中的模块分类及加载流程
模块分类
- 内置模块
- 文件模块(第三方包、自定义文件等)
模块加载速度
- 核心模块
- Node源码编译时写入到二进制文件中
- 文件模块
- 代码运行时,动态加载,加载速度慢一些
所以加载速度跟加载流程是有关系的
- 代码运行时,动态加载,加载速度慢一些
模块的加载流程
-
模块路径分析
- 依据标识符确定模块位置
- 文件标识符
- 路径标识符
- 非路径标识符(fs、http、path等导入时能直接写名字的模块)
// 路径标识符 const a = require('./m')
// 非路径标识符 const fs = require('fs')
-
模块文件定位
- 确定目标模块中具体的文件及文件类型(.js、.json等)
- 项目下存在m1.js模块,导入时使用require(m1)语法,这个时候node是不知道后缀名的
- node会以一定的顺序补足扩展名依次去查找:m1.js->m1.json->m1.node,
- 如果以上都没有找到,node就会判定为这是一个目录,将目录当成一个包来处理,就会去查找package.json文件,使用JSON.parse()解析,从而取出描述信息
- 首先是找到描述文件中的mian属性值,那如果main属性值也是没有扩展名的那么同样也会去进行后缀的补全:main.js->main.json->main.node
- 如果以上又没找到,那么node会默认将index作为目标模块中具体文件名称,依次去查找index.js->index.json->index.node
- node首先会在当前层级查找,如果没查找到会按照文件路径一层一层向上查找,如果还没查找到就会抛出错误信息
以上会打印一个向上查找文件的路径数组,根据这个路径如果没找到目标文件就会报错 not find **console.log(module.paths);
- 确定目标模块中具体的文件及文件类型(.js、.json等)
-
编译执行
- 采用对应的方式完成文件的编译执行,最终返回一个可用的exports对象供外部使用
- js文件编译执行
- 使用fs模块同步读入目标文件的内容
- 对内容进行语法包装,生成可执行js函数
- 调用函数时传入exports、module、require等属性值
- JSON文件编译执行
- 将读取到的文件内容通过JSON.parse()进行解析
缓存优化
- 提高模块的加载速度,首先回去缓存中查找对应模块
- 如果当前模块不存在,则经历一次完整流程
- 模块加载完成之后,使用路径作为索引进行缓存
vm模块的使用
vm模块为node底层创建独立运行的沙箱环境,在require首次加载模块时,获取到的文件内容是一个字符串,那么如何安全的运行这些字符串代码就是vm的作用了
执行字符串的几种方式
test.txt文件内容为
var age = 18
eval实现
const fs = require('fs')
let age = 33
let content = fs.readFileSync('test.txt', 'utf-8')
console.log(content); // var age = 18
console.log(age); // 报错
用eval的话不符合模块化的标准,比如当前模块和eval中同时声明了同一个变量时,就会报错。而模块化中每一个模块的作用域都要求是独立的。
new Function
const fs = require('fs')
let age = 33
let content = fs.readFileSync('test.txt', 'utf-8')
console.log(content); // var age = 18
console.log(age) // 33
let fn = new Function('age', content)
console.log(fn(age)) // undefined
console.log(age) // 33
用new Function虽然能解决定义相同变量报错的问题,但是如果当前模块没有定义age而引入的模块定义了age,在当前模块任然取不到age,这同样不符合模块化标准,而且定义函数和传参会比较麻烦,在使用上也没有eval方便
vm.runInThisContext()
const fs = require('fs')
const vm = require('vm')
let age = 33
let content = fs.readFileSync('test.txt', 'utf-8')
console.log(content);// var age = 18
vm.runInThisContext(content)
console.log(age) // 33
用vm.runInThisContext() 就完美解决。如果当前模块没定义age, 那么age打印就是18
手写require模块加载
- 先根据filename按顺序找资源,找到之后拿到文件的绝对路径
- 绝对路径就是模块的id,根据这个id去缓存中找,如果找的到直接返回module.exports。否则实例化一个新的Module实例
- 实例化之后将该模块缓存起来以便下次使用
- 编译加载模块内容,不同的文件不同操作
- 返回数据module.exports
# v.json文件内容
{
"age": "18"
}
const fs = require('fs')
const path = require('path')
const vm = require('vm')
// 3 创建空对象加载目标模块
function Module (id) {
this.id = id
this.exports = {}
console.log(1111)
}
// 1 绝对路径
Module._resolveFilename = function (filename) {
// 利用 Path 将 filename 转为绝对路径
let absPath = path.resolve(__dirname, filename)
// 判断当前路径对应的内容是否存在()
if (fs.existsSync(absPath)) {
// 如果条件成立则说明 absPath 对应的内容是存在的
return absPath
} else {
// 文件定位
let suffix = Object.keys(Module._extensions)
for(var i=0; i<suffix.length; i++) {
let newPath = absPath + suffix[i]
if (fs.existsSync(newPath)) {
return newPath
}
}
}
throw new Error(`${filename} is not exists`)
}
// 文件定位与加载
Module._extensions = {
'.js'(module) {
// 读取
let content = fs.readFileSync(module.id, 'utf-8')
// 包装
content = Module.wrapper[0] + content + Module.wrapper[1]
// VM
let compileFn = vm.runInThisContext(content)
// 准备参数的值
let exports = module.exports
let dirname = path.dirname(module.id)
let filename = module.id
// 调用
compileFn.call(exports, exports, myRequire, module, filename, dirname)
},
'.json'(module) {
let content = JSON.parse(fs.readFileSync(module.id, 'utf-8'))
module.exports = content
}
}
// 包装函数
Module.wrapper = [
"(function (exports, require, module, __filename, __dirname) {",
"})"
]
// 4 缓存优先,缓存已加载过的模块
Module._cache = {}
// 5 执行加载(编译执行)
Module.prototype.load = function () {
let extname = path.extname(this.id)
Module._extensions[extname](this)
}
function myRequire (filename) {
// 1 绝对路径
let mPath = Module._resolveFilename(filename)
// 2 缓存优先
let cacheModule = Module._cache[mPath]
if (cacheModule) return cacheModule.exports
// 3 创建空对象加载目标模块
let module = new Module(mPath)
// 4 缓存已加载过的模块
Module._cache[mPath] = module
// 5 执行加载(编译执行)
module.load()
// 6 返回数据
return module.exports
}
// 初始化模块导入
let obj = myRequire('./v')
// 取缓存
let obj2 = myRequire('./v')
console.log(obj.age)