模块化简介
为什么模块化
随着前端代码越来越复杂,我们迫切希望解决以下几个问题
- 全局变量污染(多人合作)
- 抽出公共代码(封装)
- 减少请求次数(减少script标签)
一个好的模块化方案,必须要能解决依赖
问题以及加载顺序
问题
模块式历史
IIFE: 使用自执行函数来编写模块化,特点:在一个单独的函数作用域中执行代码,避免变量冲突
如何解决依赖,可以让一个依赖暴露到windows上,然后当做参数传入到另外一个需要此依赖的匿名函数里
如何解决加载顺序呢,这个有两个方案,要么用defer,等全部加载完再运行,要么顺序的摆放script标签,
(function(){
return {
data:[]
}
})()
AMD: 使用requireJS 来编写模块化,特点:提前执行,前置依赖。
define('./index.js',function(code){
// code 就是index.js 返回的内容
})
CMD: 使用seaJS 来编写模块化,特点:延迟执行,就近依赖。
define(function(require, exports, module) {
var indexCode = require('./index.js');
});
CommonJS: nodejs 中自带的模块化。
var fs = require('fs');
UMD:兼容AMD,CommonJS 模块化语法。
UMD的实现很简单:
- 先判断是否支持Node.js模块格式(exports是否存在),存在则使用Node.js模块格式。
- 再判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。
- 前两个都不存在,则将模块公开到全局(window或global
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery', 'underscore'], factory);
} else if (typeof exports === 'object') {
// Node, CommonJS-like
module.exports = factory(require('jquery'), require('underscore'));
} else {
// Browser globals (root is window)
root.returnExports = factory(root.jQuery, root._);
}
}(this, function ($, _) {
// methods
function a(){}; // private because it's not returned (see below)
function b(){}; // public because it's returned
function c(){}; // public because it's returned
// exposed public methods
return {
b: b,
c: c
}
}));
ES Modules: ES6 引入的模块化,支持import 来引入另一个 js 。
import a from 'a';
commonjs
用法
Node 应用由模块组成,采用 CommonJS 模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见
暴露模块:module.exports=value 或者 exports.xxx = value
引入模块 require(xxx)
特点
- 所有代码都运行在模块作用域,不会污染全局作用域。
- 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
- 模块加载的顺序,按照其在代码中出现的顺序。
commonjs的简易实现
commonjs实质就是一个立即执行函数,包裹住被执行的文件,让文件里面的内容通过module的exports属性暴露出来
let path = require('path');
let fs = require('fs');
let vm = require('vm')
function Module(id){
this.id = id;
this.exports = {};
}
Module.wrapper = [
"(function(exports,module,req,__firname,__dirname){", // 为什么在文件里可以用module require 的原因
"})"
]
Module._extensions = {
'.js'(module){ // 如何处理模块
let fileContent = fs.readFileSync(module.id);
// 给读取出来的文件内容 添加自执行函数
let script = Module.wrapper[0] + fileContent + Module.wrapper[1];
let fn = vm.runInThisContext(script);
fn.call(module.exports,module.exports,module,req)
},
'.json'(module){
// 在json中只需要将 结果赋予给exports 对象上即可
let fileContent = fs.readFileSync(module.id);
module.exports = JSON.parse(fileContent)
}
}
// 解析文件的绝对路径 可以尝试添加后缀
Module.resolveFilename = function(filePath){
... 解析绝对路径
}
Module.prototype.load = function(){ // 是真正加载模块的方法
//this指代的就是当前的模块
let extension = path.extname(this.id);
Module._extensions[extension](this);
return this.exports; // 特别注意这里返回的是module.exports
}
Module._cache = {};
Module.load = function(filePath){ // ./a
let absPath = this.resolveFilename(filePath);
// 如果缓存有这个模块,直接把这个模块的module.exoports;
if(Module._cache[absPath]) return Module._cache[absPath].exports;
let module = new Module(absPath); // module.id module.exports;
Module._cache[absPath] = module; // 把文件和模块对应上
return module.load();
}
function req(filePath){
return Module.load(filePath)
}
从实现还是可以看出一些东西的
1、模块里面的this就是module.exports
2、require返回的是module的exports属性(一个对象)
,这个属性在执行立即执行函数(模块里代码)的时候会被赋值,所以我们可以有这么几种写法 module.exports = value(这里不要理解为改变返回值地址,指向一个新对象,而应该理解为改变module的属性,具体原因看错误的写法)
module.exports.a = value(不改变原返回对象,只是加个属性)
exports.a = value (因为exports也作为参数传到立即执行函数里了,exports是完全等于module.exports的)
但是这种写法是错的
exports = value 因为这是直接改变传入函数的参数对象(不是添加属性),外面的对象不会受影响(函数参数值传递)
3、缓存指的是在同一个js里require多次只采用第一次结果
commonjs与ems(es6 module)区别
两个差异
- CommonJS 模块输出的是一个值的拷贝,一旦输出一个值,模块内部的变化就影响不到这个值(引用类型还是能改的),ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口,有read-only特性
浏览器中执行commonjs
首先,commonjs是同步加载的,浏览器里面加载会比较慢,js会阻塞dom,导致白屏
所以浏览器里面可以用esm,script标签里面加type=module,这个会默认开启defer,在dom加载完且script下载完以后才会执行
如果非要在浏览器里面加载commonjs模块呢,因为浏览器没有node环境,也就是没有module,require,exprots这些变量的实现,所以如果能有工具添加了这些实现,是可以让浏览器运行commonjs模块规范的代码的
browserify就是这么一个工具
原理很简单,获取到每个模块的依赖关系,生成一个以 id 为键的模块依赖字典,模块3依赖模块1,模块1依赖模块2,然后包装每个模块(传入依赖字典以及自己实现的 export 和 require 函数),生成用于执行的 js
{
1:[
function(require,module,exports){
var t = require("./mo2.js");
exports.write = function(){
document.write("test1");
t.write2();
}
},
{"./mo2.js":2}
],
2:[
function(require,module,exports){
exports.write2 = function(){
document.write("=2=");
}
},
{}
],
3:[
function(require,module,exports){
var mo = require("./mo.js");
mo.write();
},
{"./mo.js":1}
]}
参考
1.浏览器加载 CommonJS 模块的原理与实现
2.commonjs与ems的差异
3.commonjs与ems差异
4.前端模块化详解
5.UMD
6.script标签 defer async type=module
7.browserify 运行原理分析