有关于webpack模块化打包工具
- seajs
- requirejs
- Conmon.js
- esmoudle=>浏览器
- umd=> 兼容处理
流程基本是定义模块,导出模块 ,使用模块,
我们首先要了解一下模块化
当我们项目特别大的时候,我们会将js代码模块化,
比如同时引进jquery和zepto ,这两者同时引用了 '$' ,此时就会产生命名冲突,这样会造成我们没有办法管理依赖,也没有办法控制依赖加载的顺序,那么我们有以下几种解决方案
- CMD (这是一种广泛使用的js规范 他的核心思想是通过
require方法‘同步’加载依赖其他模块,通过moudle.exports 导出需要暴露的接口 =>CommonJS 是AMD最具代表性的实现, node 内部实现了Common.js
a.js文件
import.exports = 'Hello world';
b.js文件
let str = require(./a.js);
如果说require方法在node中是同步的,那么他是如何实现的呢?
let fs = require('fs');//读取文件
function require(moduleName){ //moduleName路径
let content = fs.readFileSync(moduleName,'utf8');
//最后一个参数是函数的内容体
let fn = new function(‘exports’,'moudle','require','_dirname','_filename',content + '\n return moudle.response')
let moudle = {
exports:{}
}
return fn(moudle.exports,moudle,require,_dirname,_dirname)
}
/*
function(‘exports’,'moudle','require','_dirname','filename',content + '\n return moudle.response'){
import.exports = 'Hello world';
return import.exports
}
*/
- 另一种方式是AMD require.js AMD也是一种javascript模块化规范,与Common.js最大的不同在于它采用的异步的方式,,,Common.js仅适用于node 此方法可以在浏览器中使用,,最具代表性的实现是
AMD优点
- 可在不转码的情况下直接在浏览器中运行
- 可加载多个依赖
- 可在node 和 浏览器中运行
通过require声明模块
define('name',[],function(){
})
define('age',[],function(){
})
require(['name','age'],function(name,age){
console.log(name,age)
})
let factories = {};//将name和依赖绑定
function define(moudlName,dependencies,factory){//模块名字,依赖,工厂函数
factories[moudlName] = factory;
}
let function require(mods,callback){
let result = mods.map(function(mod){
let factory = factory[mod];//取出来每个相对应的函数
let exports;
exports = factory();
return exports;//执行结果是一个新的数组
})
calback.apllay(null,result)
}
这样我们就实现了简单的AMD的实现,那么还有更复杂的情况 假设我们加依赖
通过require声明模块
define('name',[],function(){
})
define('age',['name'],function(name){
return name + 9
})
require(['name','age'],function(name,age){
console.log(name,age)
})
##那么现在如何管理依赖呢?
let factories = {};//将name和依赖绑定
function define(moudlName,dependencies,factory){
//模块名字,依赖,工厂函数
factory.dependencies = dependencies;//将依赖系在函数上
factories[moudlName] = factory;
require(dependencies,fucntion(...args)){
factory.applay(null,arguments);
exports = factory.apply(null ,arguments)
};
return exports;
}
let function require(mods,callback){
let result = mods.map(function(mod){
let factory = factory[mod];//取出来每个相对应的函数
let exports;
exports = factory();
return exports;//执行结果是一个新的数组
})
calback.apllay(null,result)
}
require(['age'],function(name,age){
console.log(name,age)
})
我们重点说CommonJS
Conmon.js 实现
ConmonJS规范
- 定义了如何导入模块 require
- 还定义了如何导出模块 module.exports 导出xxx
- 还定义了一个js就是一个模块
- 如果第一次加载完成,则会存到缓存里,第二次从缓存中读取
下面我们根据以上三个特点,考虑各种情况,一步步实现一个简单的CommenJS引入功能
-
首先我们要引入所用到的模块
a. 读文件
b. 获取文件路径
c. 运行环境
d. 加载策略=>针对js/json/node文件
e. Module
f. 缓存
g. requier方法
let fs = require('fs');
let path = require('path');
let vm = require('vm');
function Module(p) {
this.id = p; // 当前模块的标识
this.exports = {}; // 每个模块都有一个exports属性
this.loaded = false; // 这个模块默认没有加载完
}
Module._extensions = {
//js优先级高于json,和node
'.js': function (Module) {},
'.json': function (Module) {},
'.node': 'xxx'
}
Module._cacheModule = {}// 根据的是绝对路径进行缓存的
function require(moduleId){//我们将加载模块以参数形式传递过来
}
以上是我们读取模块必备的条件,下面我们挨个增加内容,
2. 在require接收到路径的时候,我们首先要对此路径做解析,假设我们给个方法_resolveFileName(moduleId)对路径作出解析
// 解析绝对路径的方法 返回一个绝对路径
Module._resolveFileName = function (moduleId) {
let p = path.resolve(moduleId);
// 没有后缀在加上后缀 如果传过来的有后缀就不用加了
if (!path.extname(moduleId)) {//extname是path内部方法,在这里用到的类似用法,清自行查阅,笔者就先不多做解释了
//keys将一个对象转成数组
let arr = Object.keys(Module._extensions);
//如果没有后蕞名称,因为只有三种情况,我们挨个对比,在这里是有先后顺序的,假设传过来a,我们会现识别a.js
for (let i = 0; i < arr.length; i++) {
let file = p + arr[i];
try {
fs.accessSync(file);//accessSync 同步判定分拣是否存在
return file;
} catch (e) {
console.log(e)
}
}
} else {
return p;//如果有后坠就直接查找此文件,无需匹配
}
}
function req(moduleId) {
let p = Module._resolveFileName(moduleId);// p是一个绝对路径
}
- 当我们解析完绝对路径之后,就需要去查找要加载的文件了,我们之前说如果是第二次加载,就从缓存中查找,所以这里我们首先需要判断有没有缓存
Module._extensions = {
//js优先级高于json,和node
'.js': function (Module) {},//这里的function指的是加载该类型文件的方法
'.json': function (Module) {},
'.node': 'xxx'
}
function req(moduleId) {
let p = Module._resolveFileName(moduleId);// p是一个绝对路径
if (Module._cacheModule[p]) {
// 模块存在,如果有直接把对象返回即可稍后补充
}
// 表示没有缓存就生成一个模块
let module = new Module(p);
// 加载模块
let content = module.load(p); // 加载模块
Module._cacheModule[p] = module;
module.exports = content; //最终以module.export导出
return module.exports
}
在此过程中 ,我们生成一个模块,new了一个moudle, 将路径传过去,还记得上面的代码,在new的过程中,我们给模块加了id,exports,load,然后我加载此模块,并且将它添加到缓存中
load方法是在实例上调用的,我们将吧这个方法写在Module的原型上
//在new之后就有了这些标识
function Module(p) {
this.id = p; // 当前模块的标识
this.exports = {}; // 每个模块都有一个exports属性
this.loaded = false; // 这个模块默认没有加载完
}
Module.prototype.load = function (filepath) {
//判断加载的文件是json还是node,还是js
let ext = path.extname(filepath);
//根据文件类型添加方法
let content = Module._extensions[ext](this); //成功读取文件内容
//this指当前模块的实例 有id exports loaded
return content;
}
- 最后我们来补充下加载文件的方法,这里只介绍json和js的 如果是json我们直接parse,如果是js,我们说一个js是一个模块,那说明每读取一个js相当于读取一个闭包文件,我们会在js文件内容外包一个闭包,然后导出用moudul.export
//exports,require,module
Module.warpper = ['(function(exports,require,module){', '\n})'];
Module._extensions = {
//js优先级高于json,和node
'.js': function (Module) {
let script = fs.readFileSync(module.id, 'utf8');
let fn = Module.warpper[0] + script + Module.warpper[1];
//我们需要执行此文件,但是eval会在当前作用域向上查找,我们只想在require之后执行此文件,因此这里使用沙漏限制环境
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'
}