黑马程序员技术交流社区
标题:
【上海校区】你真的懂模块化吗?教你CommonJS实现
[打印本页]
作者:
不二晨
时间:
2018-8-7 16:33
标题:
【上海校区】你真的懂模块化吗?教你CommonJS实现
模块简史
早期的 JavaScript 往往作为嵌入到 HTML 页面中的用于控制动画与简单的用户交互的脚本语言,我们习惯这样写。
<!--html-->
<script type="application/javascript">
// module1 code
// module2 code
</script>
复制代码
所有的嵌入到网页内的 JavaScript 对象都会使用全局的 window 对象来存放未使用 var 定义的变量。这就会导致一个问题,那就是,最后调用的函数或变量取决于我们引入的先后顺序。
模块化时代。随着单页应用与富客户端的流行,不断增长的代码库也急需合理的代码分割与依赖管理的解决方案,这也就是我们在软件工程领域所熟悉的模块化(Modularity)。
直接声明依赖(Directly Defined Dependences)、命名空间(Namespace Pattern)、模块模式(Module Pattern)、依赖分离定义(Detached Dependency Definitions)、沙盒(Sandbox)、依赖注入(Dependency Injection)、CommonJS、AMD、UMD、标签化模块(Labeled Modules)、YModules、ES 2015 Modules。这些都是模块化时代的产物。
问题来了,过度碎片化的模块同样会带来性能的损耗与包体尺寸的增大,这包括了模块加载、模块解析、因为 Webpack 等打包工具包裹模块时封装的过多IIFE 函数导致的 JavaScript 引擎优化失败等。
那么到底什么是模块化?
简而言之,模块化就是将一个大的功能拆分为多个块,每一个块都是独立的,你不需要去担心污染全局变量,命名冲突什么的。
好处
封装功能
封闭作用域
可能解决依赖问题
工作效率更高,重构方便
解决命名冲突
...
js有模块化吗?
JS没有模块系统,不支持封闭的作用域和依赖管理
没有标准库,没有文件系统和IO流API
也没有包管理系统
那怎么实现js的模块化?
CommonJS规范,node是在v8引擎上的javascript运行时,作为服务端的,不能没有模块化的功能,于是就创造CommonJS规范,现在的node用的是CommonJS2。CommonJS2和CommonJS1的区别也在下面。属于动态同步加载。
// CommonJS2也可以通过这种方式导出
module.exports = {
a: 1
}
// CommonJS1只能通过这种方式
exports.a = 1
// b.js
var module = require('./a.js')
module.a // -> log 1
复制代码
AMD && CMD。AMD是RequireJS提出的,主要是依赖前置。CMD是SeaJS提出的,主要是就近依赖(只要用到才会导入),两者用法接近。属于异步加载。
// file lib/greeting.js
define(function() {
var helloInLang = {
en: 'Hello world!',
es: '¡Hola mundo!',
ru: 'Привет мир!'
};
return {
sayHello: function (lang) {
return helloInLang[lang];
}
};
});
// file hello.js
define(['./lib/greeting'], function(greeting) {
var phrase = greeting.sayHello('en');
document.write(phrase);
});
复制代码
UMD。因为AMD中无法使用CommonJS,所以出来了一个UMD,可在UMD中同时使用AMD和CommonJS。
(function(define) {
define(function () {
var helloInLang = 'hello';
return {
sayHello: function (lang) {
return helloInLang[lang];
}
};
});
}(
typeof module === 'object' && module.exports && typeof define !== 'function' ?
function (factory) { module.exports = factory(); } :
define
));
复制代码CommonJS实现
首先我们这里说的CommonJS是CommonJS2,我们需要了解到它的特性。
模块引用时会找到绝对路径
模块加载过会有缓存,把文件名作为key,module作为value
node实现模块化就是增加了一个闭包,并且自执行这个闭包(runInThisContext)
模块加载时是同步操作
默认会加后缀js,json,...
不同模块下的变量不会相互冲突
闭包实现(其实CommonJS中每个模块都是一个闭包,所以里面的变量互不影响)
我们可以在vscode中创建一个arguments.js项目
//arguments就是参数列表
console.log(arguments)
复制代码
此时在node环境下执行该文件,就会输出如下
{ '0': {},
'1':
{ [Function: require]
resolve: { [Function: resolve] paths: [Function: paths] },
main:
Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/chenxufeng/Desktop/笔记/node/arguments.js',
loaded: false,
children: [],
paths: [Array] },
extensions: { '.js': [Function], '.json': [Function], '.node': [Function] },
cache: { '/Users/chenxufeng/Desktop/笔记/node/arguments.js': [Object] } },
'2':
Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/chenxufeng/Desktop/笔记/node/arguments.js',
loaded: false,
children: [],
paths:
[ '/Users/chenxufeng/Desktop/笔记/node/node_modules',
'/Users/chenxufeng/Desktop/笔记/node_modules',
'/Users/chenxufeng/Desktop/node_modules',
'/Users/chenxufeng/node_modules',
'/Users/node_modules',
'/node_modules' ] },
'3': '/Users/chenxufeng/Desktop/笔记/node/arguments.js',
'4': '/Users/chenxufeng/Desktop/笔记/node' }
复制代码
其实每个模块外面都包了这么一层闭包,所以外面的require才能获取到module.exports的值
//exports内存中指向的就是module.exports指向的那块空间
//require一个方法
//Module模块类
//__filename该文件绝对路径
//__dirname该文件父文件夹的绝对路径
(function(exports,require,Module,__filename,__dirname){
module.exports = exports = this = {}
//文件中的所有代码
//不能改变exports指向,因为返回的是module.exports,所以是个{}
return module.exports
})
复制代码所以我们require的时候其实就相当于执行了这么一个闭包,然后返回的就是我们的module.exports
require是怎么样的?
每个模块都会带一个require方法
动态加载(v8执行到这一步才会去加载此模块)
不同模块的类别,有不同的加载方式,一般有三种常用后缀
后缀名为.js的JavaScript脚本文件,需要先读入内存再运行
后缀名为.json的JSON文件,fs 读入内存 转化成JSON对象
后缀名为.node的经过编译后的二进制C/C++扩展模块文件,可以直接使用
查找第三方模块
如果require函数只指定名称则视为从node_modules下面加载文件,这样的话你可以移动模块而不需要修改引用的模块路径。
第三方模块的查询路径包括module.paths和全局目录。
流程图
代码实现
下面我通过步骤讲解require整个的一个实现
根据路径找是否有缓存
//require方法
function req(moduleId){
//解析绝对路径的方法,返回一个绝对路径
let p = Module._resolveFileName(moduleId)
//查看是否有缓存
if(Module._catcheModule[p]){
//有缓存直接返回对应模块的exports
return Module._catcheModule[p].exports
}
//没有缓存就生成一个
let module = new Module(p)
//把他放入缓存中
Module._catcheModule[p] = module
//加载模块
module.exports = module.load(p)
return module.exports
}
复制代码
上面有很多方法都还没有,不急,我们慢慢实现
创建Module类,并添加_resolveFileName和_catcheModule
//node原生的模块,用来读写文件(fileSystem)
let fs = require('fs')
//node原生的模块,用来解析文件路径
let path = require('path')
//Module类,就相当于我们的模块(因为node环境不支持es6的class,这里用function)
function Module(p){
//当前模块的标识
this.id = p
//没个模块都有一个exports属性
this.exports = {}
//这个模块默认没有加载完
this.loaded = false
//模块加载方法(这个我们到时候再实现)
this.load = function(filepath){
//判断文件是json还是 node还是js
let ext = path.extname(filepath)
//返回一个exports
return Module._extensions[ext](this)
}
}
//以绝对路径为key存储一个module
Module._catcheModule = {}
// 解析绝对路径的方法,返回一个绝对路径
Module._resolveFileName = function(moduleId){
//获取moduleId的绝对路径
let p = path.resolve(moduleId)
try{
//同步地测试 path 指定的文件或目录的用户权限
fs.accessSync(p)
return p
}catch(e){
console.log(e)
}
}
复制代码
此时会有一个问题,如果我们没有传文件后缀,就会读取不到
给Module添加一个加载策略,并且在_resolveFileName中再加点东西
//所有的加载策略
Module._extensions = {
'.js': function(module){
//每个文件的加载逻辑不一样,这个我们后面再写
},
'.json': function(module){
},
'.node': 'xxx',
}
Module._resolveFileName = function(moduleId){
//对象中所有的key做成一个数组[]
let arr = Object.keys(Module._extensions)
for(let i=0;i<arr.length;i++){
let file = p+arr
//因为整个模块读取是个同步过程,所以得用sync,这里判断有没有这个文件存在
try{
fs.accessSync(file)
return p
}catch(e){
console.log(e)
}
}
}
复制代码
此时,我们能够找到文件的绝对路径,并把他丢给Module实例上的load方法
load方法实现
//node原生的模块,用来读写文件(fileSystem)
let fs = require('fs')
//node原生的模块,用来解析文件路径
let path = require('path')
//提供了一系列 API 用于在 V8 虚拟机环境中编译和运行代码。
let vm = require('vm')
//Module类,就相当于我们的模块(因为node环境不支持es6的class,这里用function)
function Module(p){
//当前模块的标识
this.id = p
//没个模块都有一个exports属性
this.exports = {}
//这个模块默认没有加载完
this.loaded = false
//模块加载方法
this.load = function(filepath){
//判断文件后缀是json还是 node还是js
let ext = path.extname(filepath)
return Module._extensions[ext](this)
}
}
//js文件加载的包装类
Module._wrapper = ['(function(exports,require,module,__dirname,__filename){','\n})']
//所有的加载策略
Module._extensions = {
//这里的module参数是就是Module的实例
'.js': function(module){
let fn = Module._wrapper[0] + fs.readFileSync(module.id,'utf8') + Module._wrapper[1]
//执行包装后的方法 把js文件中的导出引入module的exports中
//模块中的this === module.exports === {} exports也只是module.exports的别名
//runInThisContext:虚拟机会产生一个干净的作用域来跑其中的代码,类似于沙箱sandbox
vm.runInThisContext(fn).call(module.exports,module.exports,req,module)
return module.exports
},
'.json': function(module){
//同步读取文件中的内容并把它转为JSON对象
return JSON.parse(fs.readFileSync(module.id,'utf8'))
},
'.node': 'xxx',
}
复制代码
此时我们的代码已经全部完成
我们随便找个文件试一下,当然如果是vscode下的话,req的路径参数需要在根目录下,这是一个坑。
如果是vscode,就可以下一个插件Code Runner,可在vscode右键直接运行js文件,在node环境中。
我们拿之前的arguments.js来实验
成功输出!!
完整代码
//node原生的模块,用来读写文件(fileSystem)
let fs = require('fs')
//node原生的模块,用来解析文件路径
let path = require('path')
//提供了一系列 API 用于在 V8 虚拟机环境中编译和运行代码。
let vm = require('vm')
//Module类,就相当于我们的模块(因为node环境不支持es6的class,这里用function)
function Module(p){
//当前模块的标识
this.id = p
//没个模块都有一个exports属性
this.exports = {}
//这个模块默认没有加载完
this.loaded = false
//模块加载方法
this.load = function(filepath){
//判断文件是json还是 node还是js
let ext = path.extname(filepath)
return Module._extensions[ext](this)
}
}
//js文件加载的包装类
Module._wrapper = ['(function(exports,require,module,__dirname,__filename){','\n})']
//所有的加载策略
Module._extensions = {
'.js': function(module){
let fn = Module._wrapper[0] + fs.readFileSync(module.id,'utf8') + Module._wrapper[1]
//执行包装后的方法 把js文件中的导出引入module的exports中
//模块中的this === module.exports === {} exports也只是module.exports的别名
vm.runInThisContext(fn).call(module.exports,module.exports,req,module)
return module.exports
},
'.json': function(module){
return JSON.parse(fs.readFileSync(module.id,'utf8'))
},
'.node': 'xxx',
}
//以绝对路径为key存储一个module
Module._catcheModule = {}
// 解析绝对路径的方法,返回一个绝对路径
Module._resolveFileName = function(moduleId){
let p = path.resolve(moduleId)
try{
fs.accessSync(p)
return p
}catch(e){
console.log(e)
}
//对象中所有的key做成一个数组[]
let arr = Object.keys(Module._extensions)
for(let i=0;i<arr.length;i++){
let file = p+arr
//因为整个模块读取是个同步过程,所以得用sync,这里判断有没有这个文件存在
try{
fs.accessSync(file)
return file
}catch(e){
console.log(e)
}
}
}
//require方法
function req(moduleId){
let p = Module._resolveFileName(moduleId)
if(Module._catcheModule[p]){
//模块已存在
return Module._catcheModule[p].exports
}
//没有缓存就生成一个
let module = new Module(p)
Module._catcheModule[p] = module
//加载模块
module.exports = module.load(p)
return module.exports
}
【转载】
链接:
https://juejin.im/post/5b67c342e51d45172832123d
作者:
不二晨
时间:
2018-8-16 17:22
奈斯
欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/)
黑马程序员IT技术论坛 X3.2