前端模块化那点事-个人总结
模块化
其实我理解的模块化就是封装代码,将一个功能的代码封装到一个模块,然后需要使用时就加载这个模块
模块化开发的好处:
-
避免变量污染,命名冲突
-
提高代码复用率
-
提高维护性
-
依赖关系的管理
原生模块化
所谓的“原生模块化”,是指模块化规范没有诞生之前的所使用的一种代码封装方式
-
函数封装
函数都是挂载在window对象上的,也叫全局函数封装(全局function),污染了全局空间
function m1(){ //... } function m2(){ //... }
-
namespace模式
解决了全局空间污染问题,但是内部模块的数据全部暴露,可以直接修改数据
let myModule = { data: 'www.baidu.com', foo() { console.log(`foo() ${this.data}`) }, bar() { console.log(`bar() ${this.data}`) } } myModule.data = 'other data' //能直接修改模块内部的数据 myModule.foo() // foo() other data
-
IIFE函数匿名自调用(闭包)
作用:数据是私有的,外部只能操作暴露的方法
将暴露的模块挂载到window对象,引用js模块文件后可直接访问模块方法
但是,如果存在模块依赖怎么办?
// module.js文件 (function(window) { let data = 'www.baidu.com' //操作数据的函数 function foo() { //用于暴露有函数 console.log(`foo() ${data}`) } function bar() { //用于暴露有函数 console.log(`bar() ${data}`) otherFun() //内部调用 } function otherFun() { //内部私有的函数 console.log('otherFun()') } //暴露行为 window.myModule = { foo, bar } //ES6写法 })(window)
// index.html文件 <script type="text/javascript" src="module.js"></script> <script type="text/javascript"> myModule.foo() myModule.bar() console.log(myModule.data) //undefined 不能访问模块内部数据 myModule.data = 'xxxx' //不是修改的模块内部的data myModule.foo() //没有改变 </script>
-
IIFE模块增强:引入依赖(这就是现代模块实现的基石)
如果模块有依赖,先调用依赖的模块在调用使用依赖的模块文件,通过(function(对应依赖名的形参){})(依赖名)
// module.js文件 (function(window, $) { let data = 'www.baidu.com' //操作数据的函数 function foo() { //用于暴露有函数 console.log(`foo() ${data}`) $('body').css('background', 'red') } function bar() { //用于暴露有函数 console.log(`bar() ${data}`) otherFun() //内部调用 } function otherFun() { //内部私有的函数 console.log('otherFun()') } //暴露行为 window.myModule = { foo, bar } })(window, jQuery)
// index.html文件 <!-- 引入的js必须有一定顺序 --> <script type="text/javascript" src="jquery-1.10.1.js"></script> <script type="text/javascript" src="module.js"></script> <script type="text/javascript"> myModule.foo() </script>
CommonJs
commonJs标志着“JavaScript模块化编程”诞生了。
node.js的模块系统,就是参照CommonJS规范实现的。
因为CommonJs是同步加载模块,对于浏览器而言是不适用的。因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。这就是AMD规范诞生的背景。
-
通过module.exports暴露模块
//module1.js // module.exports = value 暴露一个对象 module.exports = { msg:'module1', foo(){ console.log("foo()",this.msg); } }
-
通过require()导入模块
let uniq=require('uniq') let module1 = require('./modules/module'); module1.foo();
AMD(Asynchronous Module Definition)
异步模块,是为了解决同步加载问题。
AMD对应的就是很有名的RequireJS。
-
requireJS主要解决两个问题
多个js文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器
js加载的时候浏览器会停止页面渲染,加载文件越多,页面失去响应时间越长
-
定义暴露模块
requireJS定义了一个函数 define,它是全局变量,用来定义模块
define(id?, dependencies?, factory);
id:可选参数,用来定义模块的标识,如果没有提供该参数,脚本文件名(去掉拓展名)
dependencies:是一个当前模块依赖的模块名称数组
factory:工厂方法,模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值- 定义没有依赖的模块
// md1.js define(function() { var name = 'Ink-模块2'; function getMinNumber(arr) { return Math.min.apply(null, arr) } return { 'name': name, 'getMinNumber': getMinNumber } });
- 定义有依赖的模块
// md2.js define(['md1'], function(md1) { function getMinNumber(arr) { console.log('此时调用md1的name:' + md1.name); return Math.min.apply(null, arr) } return { 'getMinNumber': getMinNumber } });
2.require模块requireJs文档
require([dependencies], function(){});
第一个参数是一个数组,表示所依赖的模块
第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块
require()函数在加载依赖的函数的时候是异步加载的,这样浏览器不会失去响应,它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。// 1.requireJs模块配置 requirejs.config({ // baseUrl: 'js/lib', paths: { md1: './module/md1', md2: './module/md2', // 引用第三方模块 jquery: './libs/jquery' } }); // 2.引入模块 requirejs(['md1', 'md2', 'jquery'], function(ModuleA, ModuleB, $) { "use strict"; console.log('初始化模块A', ModuleA); let arr = [1, 432, 4, 56, 99]; console.log('数组:' + arr + '\n取最大值:\t' + ModuleA.getMaxNumber(arr)); console.log('初始化模块B', ModuleB); console.log('数组:' + arr + '\n取最小值:\t' + ModuleB.getMinNumber(arr)); // 3.引用第三方模块(先在config中配置) $('body').css({ 'background': 'darkblue' }) });
- HTML页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>AMD规范</title> </head> <body> <!-- data-main是主js模块入口,就是说requireJs加载处理后会加载data-main中的main.js --> <script src="./require.js" data-main='main.js'></script> </body> </html>
CMD(Common Module Definition)
即通用模块定义,对应SeaJS,是阿里玉伯团队首先提出的概念和设计。跟requireJS解决同样问题,只是运行机制不同:是通过按需加载的方式,而不是必须在模块开始就加载所有的依赖
-
模块定义
define(id?, deps?, factory)
因为CMD推崇一个文件一个模块,所以经常就用文件名作为模块id
CMD推崇依赖就近,所以一般不在define的参数中写依赖,在factory中写
factory有三个参数function(require, exports, module)
require代表依赖其他的模块
exports代表导出给别人的模块
module代表一个模块define(function(require, exports, module) { 'use strict'; function getMaxNumber(arr) { return Math.max.apply(null, arr) } let name = 'm1'; let version = 'v1.0'; exports.name = name; exports.version = version; exports.getMaxNumber = getMaxN umber; });
-
seajs.use('./module/m1', function(m1) { console.log(m1); })
-
HTML页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>CMD规范</title> </head> <body> <script src="./libs/sea.js"></script> <script src="./main.js"></script> </body> </html>
ES6模块
使用export指令导出了模块对外提供的接口,下面我们就可以通过 import
命令来加载对应的这个模块了
-
export模块
export指令用于导出变量
// info.js let author = 'Ink-kai'; let version = 'v1.1'; // 某些情况下一个模块中包含某个功能,我们并不希望给这个功能命名,而是让导入者可以自己来命名 // default是export对象自带的一个默认对象 export default { author, version } // export function可以直接通过模块名.getMinNum(arr)调用 export function getMinNum(arr) { return Math.min.apply(null, arr) }
-
import模块
用于导入模块中的内容
// main.js 主模块 // *表示导出模块所有对象(export模块的内容) as 别名 import * as md1 from './module/info.js' let arr = [11, 323, 4, 5]; console.log('info模块:', md1); console.log(arr + '\n最小值:' + md1.getMinNum(arr)); console.log('author:' + md1.author); console.log('info模块版本:' + md1.version);
-
HTML页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>ES6模块</title> </head> <body> <!-- 类型需要设置为module --> <script type="module" src="./main.js"></script> </body> </html>
AMD与CMD区别
最明显的区别就是在模块定义时对依赖的处理不同
AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块
CMD推崇就近依赖,只有在用到某个模块的时候再去require
看到很多网站说AMD是异步加载,CMD是同步加载,肯定是不准确的,他们的都是异步加载模块。
对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.
CMD 推崇依赖就近,AMD 推崇依赖前置。
通俗来说:
AMD在加载完成定义(define)好的模块就会立即执行,所有执行完成后,遇到require才会执行主逻辑。(提前加载)
CMD在加载完成定义(define)好的模块,仅仅是下载不执行,在遇到require才会执行对应的模块。(按需加载)
AMD用户体验好,因为没有延迟,CMD性能好,因为只有用户需要的时候才执行。
CMD为什么会出现,因为对node.js的书写者友好,因为符合写法习惯,就像为何vue会受人欢迎的一个道理。
官方阐述SeaJS与RequireJS异同
两者的主要区别如下:
- 定位有差异。RequireJS 想成为浏览器端的模块加载器,同时也想成为 Rhino / Node 等环境的模块加载器。Sea.js 则专注于 Web 浏览器端,同时通过 Node 扩展的方式可以很方便跑在 Node 环境中。
- 遵循的规范不同。RequireJS 遵循 AMD(异步模块定义)规范,Sea.js 遵循 CMD (通用模块定义)规范。规范的不同,导致了两者 API 不同。Sea.js 更贴近 CommonJS Modules/1.1 和 Node Modules 规范。
- 推广理念有差异。RequireJS 在尝试让第三方类库修改自身来支持 RequireJS,目前只有少数社区采纳。Sea.js 不强推,采用自主封装的方式来“海纳百川”,目前已有较成熟的封装策略。
- 对开发调试的支持有差异。Sea.js 非常关注代码的开发调试,有 nocache、debug 等用于调试的插件。RequireJS 无这方面的明显支持。
- 插件机制不同。RequireJS 采取的是在源码中预留接口的形式,插件类型比较单一。Sea.js 采取的是通用事件机制,插件类型更丰富。