什么是模块?如何结构化管理代码?
模块是比对象和函数更大的代码单元,使用模块可以将程序进行归类。创建模块的时候,我们应该努力形成一致的抽象和封装。
自然的优化程序的结构和组织的方式,就是把它们分成下的,耦合相对松散的片段,这些片段就是模块,我们也完成了结构化管理我们的代码。
代码实现模块化的优点
使用模块意味着可以在应用程序的不同地方更容易复用模块的功能,甚至可以跨应用来复用模块,极大地提高了应用程序的开发效率。并且模块的作用域的封闭,会形成变量名的保护,有自己的命名空间,合作开发的时候效率更高,会避免很多的麻烦。
原生实现模块化技术
利用现有的一些特性,如对象,立即执行函数,闭包等。
实现模块化前提
当利用现有技术创造,模块化代码的时候,每个模块系统至少应该能执行以下操作:
- 定义模块接口,通过接口可以调用模块的功能。
- 隐藏模块的内部实现,使模块的使用者无需关注模块内部的实现细节。同时,隐藏模块的内部实现,避免有可能产生的副作用和对bug的不必要的修改。
模块化的了解顺序:
(1) 如何使用如对象,闭包和立即执行函数来创建模块。
(2)研究广泛使用的模块化标准AMD(Asynchronous Module Definition) 和CommonJS
利用原生技术实现模块化
利用模块模式 实现返回对象接口(对象接口通常包含一组数量和函数)
使用自执行函数模块模式:
//创建一个全局模块变量,赋值立即执行的函数 为返回的对象名
const MouseCounterModule = function(){
//创建模块私有变量
let numClicks = 0;
//创建模块私有函数
const handleClick = ()=>{
alert(++numClicks);
};
return {
//返回一个对象,代表模块的接口. 通过闭包可以访问模块的私有变量和方法 但是无法访问模块的内部的实现
countClicks : ()=>{
//方法用来注册事件处理器 然后获取次数
document.addEventListener("click",handleClick);
}
};
}();
立即执行函数内部,定义模块内部的实现细节: 一个函数变量numClicks 一个局部函数handleClick,都只能在模块内部访问。我们创建并返回一个对象作为模块的“公共接口”(MouseCounterModule)。此处由于暴露了接口(没有失去引用)所以可以保持活跃。不停调用countClicks 所以闭包内部的numClicks和handleClick保持活跃。
这种在JS 中使用立即执行函数,对象和闭包来创建模块的方式我们称之为模块模式
调用函数的自执行 进行模块扩展:
//在不修改原有代码的前提下就可以定义更多的功能: 模块扩展
// 传入参数为模块化
(function(module){
//定义新的私有变量和函数
let numScroll = 0;
const handleScroll = ()=>{
alert(++numScrolls);
}
module.conuntScrolls =()=>{
document.addEventListener("wheel",handleScroll);
}
return module;
})(MouseCounterModule);
注意 通过独立的立即执行函数扩展模块,无法共享模块私有变量,因为每个函数都分别创建了新的作用域,虽然这是一个缺点,但并不致命,我们仍然可以使用模块模式保持JavaScript应用模块化
//可以给模块增加新属性
MouseCounterModule.newMethod = ()=>{}
//也可以轻松的创建一个子模块
MouseCounterModule.newSubmodule = ()=>{
//1 函数自执行 定义作用域 2 返回一个对象接口 一个小的模块
return {...};
}()
糟糕的问题是:当我们开始创建模块化应用的时候,模块本身常常依赖其他模块的功能。比如在第二个模块中像用第一个模块的alert的方法计算出点击次数的函数。然而,模块模式无法实现。
这些依赖关系。我们应该考虑正确的依赖顺序,这样我们的模块化才具有执行时所需的完整的依赖。
在使用 大量 内部模块 依赖的大型应用中则是非常严重的问题。
总而言之,就是模块模式适应不了一些私有变量共享的问题,所以为了解决这个问题就出现了 AMD 和CommonsJS
使用AMD和CommonsJS模块化JavaScript应用
两者主要的区别在于: AMD的设计理念是明确基于浏览器的,而CommonsJS的设计是面向JavaScript环境的(如Node.js服务端)而不局限于浏览器
AMD
AMD可以自动解决依赖,异步加载模块,避免阻塞。(因为是浏览器,所以也能想到为什么是异步加载模块)
AMD可以很容易指定模块及依赖关系。目前,基于AMD规范的是RequireJS依赖加载。
案例: 利用AMD 进行模块化
//配置path
requirejs.config({
paths: {
"jquery": './libs/jquery-3.4.0',
"CounterModule" : './libs/CounterModule'
}
})
//定义模块函数
define('CounterModule',['jquery'],()=>{
let numClicks =0;
let $ = jQuery;
console.log(jQuery);
const handleClick = ()=>{
alert(++numClicks);
};
return{
countClicks : ()=>{
//jquery 引入成功
console.log($)
$(document).click(handleClick);
}
};
});
//业务函数 html 调用
require(['./libs/require.config'],()=>{
require(['CounterModule'],obj=>obj.countClicks());
});
注意在项目定义模块函数的时候要进行分情况判断
1 在AMD 标准下调用 2 在CommonsJS 标准下调用 3 全局对象上调用
;(function(global,factory){
//AMD
if(type define ==='function' && define.amd){
define(['jquery'],factory);
}else if(type module ==='object' && typeof module.exports ==='object'){
//commonsJS
var jquery = require('jquery');
module.exports = factory('jquery');
}else{
//啥都没有
global.PopUp = factory(jQuery);
}
})(window,function($){
//这里的$ 是依赖的jQuery传入 这里定义的$是形参 接受,jQuery这个实参 这个函数PopUp对于 factory来说就是一个实参函数 要注意这里的知识点
function PopUp(){
this.init.apply(this,arguments);
}
//用后面的对象来覆盖前面的PopUp的原型对象
$.extend(PopUp.prototype,{
init(btn_selector,model_selector){
this.btn = $(btn_selector);
this.model = $(model_selector);
this.bindEvent();
},
bindEvent(){
this.btn.om('click',$.proxy(this.toggle,this));
this.model.on('click',$.proxy(this.toggle,this));
},
toggle(){
this.model.toggle();
}
})
//插件更新了 PopUp的方法 jQuery继承了PopUp的方法
$.extend({PopUp});
return PopUp;
})
ES6模块
- 于CommonsJS类似,ES6模块语法相对简单,并且基于文件(每个文件就是一个模块)
- 于AMD类似,ES6模块支持异步模块加载。
为了提供了这个模块功能 ES6引入了两个关键字:
export ----- 从模块外部指定标识符
import -----导入模块标识符
它的主要思想就是: 必须显示地使用标识符导出模块,才能从外部访问的模式。
其他未标识的标识符,甚至在最顶级作用域中定义的(可以是标准JavaScript中的全局作用域)标识符,只能在模块内使用。这一点是受到CommonsJS启发的。
引入部分
- import 核心关键字表示引入内容
- as 别名关键字
- from 路径名称关键字
- * 表示所有模块
- 加载 exprot default 返回的模块 (此格式返回的就是你写的对象)
import obj from "path"
tip : path 是根据当前文件的路径作为起始开始找寻的。
- 加载 exprot 返回的模块 (此格式返回的是 {你的对象} 所以导入的时候需要解构)
import { A , B , C ...} from "path"
tip: 用export返回的对象其实是被Module包裹的一个对象,所以在使用对象中内容的时候必须使用解构赋值
- 别名操作及*操作
import { a as b } from "path" // 别名
import * as ModuleName from "path" // 这是加载了整个Module对象,在这个对象之中取值要用解构赋值;
定义模块部分
- export 定义模块的功能 ,export 直接定义的模块,在Module (ES6内置的一个容器)之中被包裹
- default 和export连用,表示原样返回定义的模块内容,是什么就返回什么
- from 和import表示的相同 都是路径
- const | let | class | .... 所有的声明类关键字都可以用。
- 定义es6模块
export {
xxx1,
xxx2,
xxx3
}
定义出来的结果是被Module(对象)包裹的,大概长成这样的对象 :
Module {
xxx1 ,
xxx1 :getter(){}
xxx1 :setter(){}
}
tip : 这个对象想要使用请务必使用解构赋值。
- 原样返回
export default xxx //导入的时候直接自己定义变量名就可以
- 返回各种类型的数据;
export const FOO = "FOO";
export let FOO = "FOO";
...
- 返回加载来的数据;
export { Foo } from "./test2.js";
上述就是模块化的发展历程和使用的方法。