目标
- commonJS模块化规范介绍及使用
- AMD,CMD,SystemJS模块化规范介绍及使用
- ESModule规范及与其他规范的区别
- 函数式编程
知识要点
幼年期:无模块化
js最初设计目的
- 页面动画
- 表单处理,提交,数据的验证
- 与浏览器的交互
- 无模块化,命名空间的概念
- 多种js文件分在不同的文件中
- 不同的文件又被同一个模板引用
<script src='jquery.js'></script>
<script src='main.js'></script>
<script src='dep1.js'></script>
优点
- 文件分离是最基础的模块化的第一步
问题
- 污染全局作用域,函数名冲突,全局变量灾难
- 不利于大型项目的开发以及多人团队的共建
成长期:模块化的开始-IIFE(语法侧的优化)
1.利用块级作用域
(()=>{
let count=0;
//...
})
//立即执行
(()=>{
let count =0
})();
定义简单的模块
const iifeMoudle = (() => {
const helloWorld = 'helloWorld';
const syaHi = () => {
console.log(helloWorld)
}
return {
syaHi: syaHi,
}
})();
iifeMoudle.syaHi();
问题:有额外的依赖时,如何优化IIFE相关代码
传参
const iifeMoudle1 = (() => {
const w = 'world';
const world = () => {
return w;
}
return {
world: world,
}
})();
const iifeMoudle2 = ((dependency1) => {
const helloWorld = 'hello ';
const syaHi = () => {
console.log(helloWorld + dependency1.world())
}
return {
syaHi: syaHi,
}
})(iifeMoudle1);
iifeMoudle2.syaHi();
问题:早期jquery的依赖处理以及模块加载方案?/ 传统IIFE是如何解决多方依赖的问题 答:IIFE加传参调配
实际上,jquery等框架其实应用了revealing的写法: 揭示模式
const iifeModule = ((dependencyModule1, dependencyModule2) => {
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
}
return {
increase, reset
}
})(dependencyModule1, dependencyModule2);
iifeModule.increase();
iifeModule.reset();
成熟期
CJS-commonjs
node.js制定
- module.exports/ exports对外暴露接口
- 通过require来调用其他模块
例子
// 引入部分
const dependencyModule1 = require(./dependencyModule1);
const dependencyModule2 = require(./dependencyModule2);
// 处理部分
let count = 0;
const increase = () => ++count;
const reset = () => {
count = 0;
}
// 做一些跟引入依赖相关事宜……
// 暴露接口部分
exports.increase = increase;
exports.reset = reset;
module.exports = {
increase, reset
}
使用:
const { increase, reset } = require('./main.js');
increase();
reset();
问题: nodejs 模块实际执行是怎么处理
(function (thisValue, exports, require, module) {
const dependencyModule1 = require(./dependencyModule1);
const dependencyModule2 = require(./dependencyModule2);
// 业务逻辑……
}).call(thisValue, exports, require, module);
优点
- Commonjs 率先在服务端实现了,从框架层面解决依赖,全局变量污染的问题
缺点
- 主要针对服务端的解决方案,对于异步拉取依赖的处理整合不是那么的友好
- 无法直接用于浏览器端 ,外层没有function包裹,变量全暴漏在全局
新的问题,异步依赖
AMD规范 浏览器端的js模块化规范AMD(Asynchronous Module Definition)
- 异步加载+允许指定回调函数
- 经典实现框架:require.js
定义方式:
- define(id,[depends],callback)
应用方式
- require([module],callback);
模块定义例子
define('amdModule',['dep1','dep2'],(dep1,dep2)=>{
//业务逻辑
//处理部分
let count=0
const inscrease =()=>{
count=0;
}
return {
increase,
reset
}
})
引入模块
require(['amdModule'],[],requre=>{
//引入部分
const dep1 = require('./dep1')
let count=0
const inscrease =()=>{
count=0;
}
return {
increase,
reset
}
})
问题:AMD中使用revealing
define('amdModule',[],(require,export,module)=>{
const dep1 = require('./dep1')
let count=0
const inscrease =()=>{
count=0;
}
export.increase = increase();
})
define('amdModule',[],require=>{
const otherModule = require('amdModule');
otherModule.increase();
})
问题: 兼容AMD& CJS如何判断CJS和AMD
(
define('amdModule',[],(require,export,module)=>{
const dep1 = require('./dep1')
let count=0
const inscrease =()=>{
count=0;
}
export.increase = increase();
})
)(
//区分commonjs , amd
typeof module ==='object'
&& module.exports
&& typeof define !=='function'?
factory => module.exports = factory(requre,exports,module):define
)
优点
- 适合在浏览器中加载异步模块,可以并行加载多个模块
缺点
- 会有引入成本,不能按需加载
CMD规范
- 按需加载
define('module', (require, exports, module) => {
let $ = require('jquery');
// jquery相关逻辑
let dependencyModule1 = require('./dependecyModule1');
// dependencyModule1相关逻辑
})
优点
- 按需加载,依赖就近
缺点
- 依赖于打包,加载逻辑存在于每个模块中,扩大模块的体积
问题:AMD&CMD区别
- 依赖就近,按需加载
ES6模块
引入关键字 —— import 导出关键字 —— export
模块引入、导出和定义的地方:
// 引入区域
import dependencyModule1 from './dependencyModule1.js';
import dependencyModule2 from './dependencyModule2.js';
// 实现代码逻辑
let count = 0;
export const increase = () => ++count;
export const reset = () => {
count = 0;
}
// 导出区域
export default {
increase, reset
}
模板引入的地方
<script type="module" src="esModule.js"></script>
node中:
import { increase, reset } from './esModule.mjs';
increase();
reset();
import esModule from './esModule.mjs';
esModule.increase();
esModule.reset();
问题:动态模块
问题:动态模块 考察:export promise
ES11原生解决方案:
import('./esModule.js').then(dynamicEsModule => {
dynamicEsModule.increase();
})
优点(重要性)
- 通过一种最统一的形态整合了js的模块化
缺点(局限性):
- 本质上还是运行时的依赖分析
解决模块化的新思路 - 前端工程化
背景
根本问题 - 运行时进行依赖分析
前端的模块化处理方案依赖于运行时分析
解决方案:线下执行 grunt gulp webpack
<!doctype html>
<script src="main.js"></script>
<script>
// 给构建工具一个标识位
require.config(__FRAME_CONFIG__);
</script>
<script>
require(['a', 'e'], () => {
// 业务处理
})
</script>
</html>
define('a', () => {
let b = require('b');
let c = require('c');
export.run = () {
// run
}
})
工程化实现
step1: 扫描依赖关系表:
{
a: ['b', 'c'],
b: ['d'],
e: []
}
step2: 重新生成依赖数据模板
<!doctype html>
<script src="main.js"></script>
<script>
// 构建工具生成数据
require.config({
"deps": {
a: ['b', 'c'],
b: ['d'],
e: []
}
})
</script>
<script>
require(['a', 'e'], () => {
// 业务处理
})
</script>
</html>
step3: 执行工具,采用模块化方案解决模块化处理依赖
define('a', ['b', 'c'], () => {
// 执行代码
export.run = () => {}
})
优点:
- 构建时生成配置,运行时执行
- 最终转化成执行处理依赖
- 可以拓展
完全体 webpack为核心的工程化 + mvvm框架组件化 + 设计模式
补充知识点
无模块
缺点
- 多个文件不能有同名的函数,变量
- 依赖关系不好管理
//b.js需要依赖a.js,的书写顺序
<script type='text/javascript' src='a.js' />
<script type='text/javascript' src='b.js' />
萌芽时代
java风格的命名空间
app.util.modA =xxx;
app.tools.modA.format();
jQuery风格的匿名自执行函数
(function(window){
//代码
window.jQuery = window.$ = jQuery;//通过给window添加属性而暴漏到全局
})(window);
未解决根本问题,依赖还是需要外部提供,增加了全局变量
模块化面临的问题
- 如何安全的包装一个模块的代码?(不污染模块外的任何代码)
- 如何唯一标识一个模块?
- 如何优雅的把模块的API暴漏出去?(不能增加全局变量)
- 如何方便的使用所依赖的模块?
nodejs的规范CommonJs
Modules/1.0规范
- 模块的标识应遵循的规则(书写规范)
- 定义全局函数require,通过传入模块标识来引入其他模块,执行的结果即为别的模块暴漏出来的API
- 如果被require函数引入的模块中也包含依赖,那么依次加载这些依赖
- 如果引入模块失败,那么require函数应该报一个异常
- 模块通过变量exports来向往暴漏API,exports只能是一个对象,暴漏的API须作为此对象的属性。
缺点
- 无法直接用于浏览器端,外层没有function包裹,变量全暴漏在全局
AMD规范
- 用全局函数define来定义模块,用法为:define(id?, dependencies?, factory);
- id为模块标识,遵从CommonJS Module Identifiers规范
- dependencies为依赖的模块数组,在factory中需传入形参与之一一对应
- 如果dependencies的值中有"require"、"exports"或"module",则与commonjs中的实现保持一致
- 如果dependencies省略不写,则默认为["require", "exports", "module"],factory中也会默认传入require,exports,module
- 如果factory为函数,模块对外暴漏API的方法有三种:return任意类型的数据、exports.xxx=xxx、module.exports=xxx
- 如果factory为对象,则该对象即为模块的返回值
//a.js
define(function(){
console.log('a.js执行');
return {
hello: function(){
console.log('hello, a.js');
}
}
});
//b.js
define(function(){
console.log('b.js执行');
return {
hello: function(){
console.log('hello, b.js');
}
}
});
//main.js
require(['a', 'b'], function(a, b){
console.log('main.js执行');
a.hello();
$('#b').click(function(){
b.hello();
});
})
缺点
-
如果一个模块依赖了十个其他模块,那么在本模块的代码执行之前,要先把其他十个模块的代码都执行一遍,不管这些模块是不是马上会被用到。这个性能消耗是不容忽视的。
-
在定义模块的时候,要把所有依赖模块都罗列一遍,而且还要在factory中作为形参传进去,要写两遍很大一串模块名称
解决
define(function(){ console.log('main2.js执行'); require(['a'], function(a){ a.hello(); }); $('#b').click(function(){ require(['b'], function(b){ b.hello(); }); }); }); //d.js define(function(require, exports, module){ console.log('d.js执行'); return { helloA: function(){ var a = require('a'); a.hello(); }, run: function(){ $('#b').click(function(){ var b = require('b'); b.hello(); }); } } });
兼容并包的CMD/seajs
后起之秀seajs,seajs的作者是国内大牛淘宝前端布道者玉伯。seajs全面拥抱Modules/Wrappings规范
Modules/Wrappings规范
资源的下载阶段还是预先进行,资源执行阶段后置,等到需要的时候再执行。这样一种折衷的方式,能够融合前面两种方式的优点,而又回避了缺点。
这就是Modules/Wrappings规范
//a.js
define(function(require, exports, module){
console.log('a.js执行');
return {
hello: function(){
console.log('hello, a.js');
}
}
});
//b.js
define(function(require, exports, module){
console.log('b.js执行');
return {
hello: function(){
console.log('hello, b.js');
}
}
});
//main.js
define(function(require, exports, module){
console.log('main.js执行');
var a = require('a');
a.hello();
$('#b').click(function(){
var b = require('b');
b.hello();
});
});
main.js执行 a.js执行 hello, a.js
a.js和b.js都会预先下载,但是b.js中的代码却没有执行,因为还没有点击按钮。当点击按钮的时候,会输出如下:
b.js执行 hello, b.js
可以看到b.js中的代码此时才执行。这样就真正实现了“就近书写,延迟执行“,不可谓不优雅