JS模块化
背景
- JS本身定位:简单的页面设计-页面动画+基本的表单提交。
- 并无模块化或者命名空间的概念。
- 随着JS的模块化日益增长,逐渐发展出了页面模块化。
幼年期
无模块化(委婉的辩解)
- 开始需要在页面中增加一些不同的js:动画、表单、格式化工具
- 多种JS文件被分在不同的文件中
- 不同的文件又被同一个模板所引用
// test.html
<script src="jquery.js"></script>
<script src="main.js"></script>
<script src="tool.js"></script>
- 文件分离是最基础的模块化
出现的问题: 污染全局作用域 ,每一个模块都是暴露在全局的,协调每一个模块变量函数名称都不可以相同, 不利于大型项目的开发以及多人团队共建
script标签的另外两个参数-async & defer
-defer解析到script标签开始异步下载,解析完成后开始执行
-async会异步下载js代码并执行
总结:
普通情况:解析到script标签,立刻pending,并且下载执行。
defer:解析到script标签开始异步下载,解析完成后开始执行。
async:解析到script标签开始异步下载,下载完成后立刻执行并阻塞渲染,执行完成后, 继续渲染。
成长期
模块化的雏形——IIFE(立即执行函数)
作用域的把控
- 例子:
// 定义一个全局变量
let count = 0;
// 代码块1
const increase = () => count++;
// 代码块2
const reset = () => {
count = 0;
}
increase();
reset();
- 利用函数作用域限制
// 仅仅定义了一个函数,但里面的代码并没有执行,如何能够对齐原来的逻辑呢?
(() => {
let count = 0;
// ……
})();
- 尝试定义一个简单的模块
const iifeCounterModule = (() => {
let count = 0;
return {
increase: () => ++count;
reset: () => {
count = 0;
console.log('hahaha count is reset');
}
}
})();
iifeCounterModule.increase();
iifeCounterModule.reset();
//完成了一个模块的封装,实现了对外暴露功能,保留变量 + 不污染全局作用域
如果有其他的依赖,如何处理?
优化1:依赖其他模块的IIFE
const iifeCounterModule = ((depModule1, depModule2) => {
let count = 0;
// dependencyModule做处理
return {
increase: () => ++count;
reset: () => {
count = 0;
console.log('hahaha count is reset');
}
}
})(depModule1, depModule2);
面试题1:了解早期jquery的依赖处理以及模块加载方案吗?
答:IIFE(立即执行函数表达式)+传参调配,实际上传统框架利用一种揭示模式写法。
实际书写上,jquery等框架实际应用涉及到revealling的写法
const revealingCounterModule = (() => {
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log('hahaha count is reset');
};
return {
increase,
reset
}
})();
//本质实现与方案上并无不同,
//只是在写法思想上,更强调所有API——局部变量的形式定义在函数中,而仅仅对外暴露出可被调用的接口
成熟期
CommonJs
node.js制定
特征:通过module + exports 来对外暴露接口;require来调用其他模块
优点:CommonJs规范在服务器端率先完成了JS的模块化,解决了依赖、全局变量污染的问题,这也是JS运行在服务端运行的必要条件。
缺点:针对的是服务端,对于异步依赖没有很友好的处理和考虑。
模块组织方式
解析:
第一:将当前模块require的部分,从全局转成局部,
第二:执行当前模块中需要执行的主干核心部分
第三:将核心部分暴露出去
// commonJSCounterModule.js
const dependencyModule1 = require('./dependencyModule1');
const dependencyModule2 = require('./dependencyModule2');
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log('hahaha count is reset');
};
exports.increase = increase;
exports.reset = reset;
module.exports = {
increase,
reset
}
// main.js
const { increase, reset } = require('./commonJSCounterModule')
increase();
const commonJSCounterModule = require('./commonJSCounterModule')
commonJSCounterModule.increase();
实际执行处理
(function(exports, require, module, __filename, __dirname) {
const dependencyModule1 = require('./dependencyModule1');
const dependencyModule2 = require('./dependencyModule2');
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log('hahaha count is reset');
};
module.exports = {
increase,
reset
};
return module.exports;
}).call(thisValue, exports, require, module, filename, dirname);
(function (exports, require, module, __filename, __dirname) {
const commonJSCounterModule = require('./commonJSCounterModule')
commonJSCounterModule.increase();
}).call(thisValue, exports, require, module, filename, dirname);
AMD规范(异步模块化)
对于非同步加载模块,允许制定回调函数
经典框架:require.js
优点:解决了浏览器中异步加载模块,可以并行加载多个模块
缺点:会有引入成本,缺少考虑按需加载
新增定义方式
// 通过define来定义一个模块,然后require加载
define(id, [depends], callback)
require([module], callback)
模块的定义方式
define(
'amdCounterModule',
['dependencyModule1', 'dependencyModule2'],
(dependencyModule1, dependencyModule2) => {
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log('hahaha count is reset');
};
return {
increase,
reset
}
}
)
require(
['amdCounterModule'],
amdCounterModule => {
amdCounterModule.increase();
amdCounterModule.reset();
}
)
面试题2:如果想在AMD中使用require方式加载同步模块可以吗?
答:AMD支持向前兼容,以提供回调的形式来做require方法动态加载模块
define(require => {
const dependecyModule1 = require('dependecyModule1');
const dependecyModule2 = require('dependecyModule2');
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log('hahaha count is reset');
};
// return {
// increase,
// reset
// }
// revealing
exports.increase = increase;
exports.reset = reset;
})
面试题3:有没有什么方式可以统一兼容AMD和common
答:UMD的出现
优点:适合在浏览器环境中异步加载模块,同时又采用common模块
缺点:提高了开发成本,并且不能按需加载,必须提前加载所有依赖
(define => define((require, exports, module) => {
const dependecyModule1 = require('dependecyModule1');
const dependecyModule2 = require('dependecyModule2');
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
console.log('hahaha count is reset');
};
module.export = {
increase,
reset
}
}))
(
// 判断区分是否为AMD or common
typeof module === 'object' && module.exports && typeof define !== 'function'
? // CommonJS
factory => module.exports = factory(require, exports, module)
: // AMD
define
);
CMD规范(sea.js)
按需加载
优点:按需加载,依赖就近
缺点:依赖打包,加载逻辑存在于每个模块中,扩大了模块的体积
define(function(require, exports, module) {
var $ = require('jquery');
var dependencyModule1 = require('./dependencyModule1');
// ……
// exports.increase = ...
// module.exports = ...
})
面试题4:CMD 和 AMD 区别
// AMD
define([
'./dependencyModule1',
'./dependencyModule2'
], function(dependencyModule1, dependencyModule2) {
dependencyModule1.increase();
dependencyModule2.reset();
})
// CMD - 依赖就近
define(
function(require, exports, module) {
let dependencyModule1 = require('./dependencyModule1');
dependencyModule1.increase();
// if () {
// let dependencyModule2 = require('./dependencyModule2');
// dependencyModule2.reset();
// }
}
)
ES6模块化-ESM
走向新时代
新增定义方式:引入:
import;导出:export
- 模块引入、导出和定义
import depModule1 from './dependecncyModule1';
import depModule2 from './dependecncyModule2';
let count = 0;
const obj = {
increase: () => ++count;
reset: () => {
count = 0;
// fn(depModule1);
// depModule1, depModule2
}
}
export default {
increase,
reset
}
// 异步加载
import('xxx').then(a => {
// ../
})
- 动态模块的加载
import ('./esModule').then(({ increase, reset }) => {
increase();
reset();
});
import ('./esModule').then((dynamicESModule) => {
dynamicESModule.increase();
dynamicESModule.reset();
});
解决模块化的新思路
- 根本问题:运行时分析依赖
前端的模块化处理方案依赖于运行时进行分析,并且同时进行依赖加载处理以及实际的逻辑执行 解决方案:线下执行
project
| - lib
| |- xmd.js
| - mods
| - a.js
| - b.js
| - c.js
| - d.js
| - e.js
| - f.js
| - index.html
// index.html
<!doctype html>
<script src="lib/xmd.js"></script>
<script>
{/* 等待构建工具生成数据替换`__FRAMEWORK_CONFIG__` */}
require.config(__FRAMEWORK_CONFIG__);
</script>
<script>
{/* 业务代码 */}
require.async(['a', 'e'], function(a, e) {
// ……
})
</script>
// mods/a.js
define('a', function(require, exports, module) {
let b = require('b');
let c = require('c');
exports.run = function() {
//……
}
})
// 工程化模块的构建
// 1. 扫描生成依赖关系表
{
"a": ["b", "c"],
"b": ["d"]
}
// 2. 生成构建模版
<!doctype html>
<script src="lib/xmd.js"></script>
<script>
{/* 等待构建工具生成数据替换`__FRAMEWORK_CONFIG__` */}
require.config({
"deps": {
"a": ["b", "c"],
"b": ["d"]
}
});
</script>
<script>
{/* 业务代码 */}
require.async(['a', 'e'], function(a, e) {
// ……
})
</script>
// 3. 转化配置为依赖加载代码
define('a', ["b", "c"], function(require, exports, module) {
let name = _check ? 'b' : 'c';
let mod = require(name);
exports.run = function() {
//……
}
})