🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
#### 1. 走进模块 ``` 模块在其他语言非常常见 代码量越来越大,所有代码都挤在一个文件里,很难维护,太可怕了, 如果分拆代码在不同的文件,文件多请求数就多,不说性能越来越差,就单单管理文件之间的依赖就非常痛苦 什么是依赖? 如果代码使用了《》 ,代码就依赖了《》,那么《》必须在代码之前加载,文件多,依赖就多,此时就变得更加 复杂了 所以需要一个体系来拆解代码,将代码变为小模块,来维护小模块之间的依赖,这就是模块化 ``` #### 2. 模块引言 ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/%E6%A8%A1%E5%9D%97%E5%BC%95%E8%A8%80.png) #### 3. 命名冲突和变量污染 命名冲突 ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/%E5%91%BD%E5%90%8D%E5%86%B2%E7%AA%811.png) 修改命名冲突 ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/%E5%91%BD%E5%90%8D%E5%86%B2%E7%AA%812.png) 变量污染 ![]( https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/%E5%91%BD%E5%90%8D%E5%86%B2%E7%AA%813.png) #### 4. 命名空间 避免命名冲突和变量污染 命名空间可使用以下方法: 1.前缀命名空间 ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/%E5%91%BD%E5%90%8D%E7%A9%BA%E9%97%B4%201.png) 2.对象命名空间 ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/%E5%91%BD%E5%90%8D%E7%A9%BA%E9%97%B4%202.png) 3.IIFE 立即执行函数表达式 ``` JavaScript 语言本身并没有命名空间这一个概念,为了避免变量冲突,需要通过各种手段来模拟实现命名空间的功能。 如使用前缀命名空间,前缀命名空间能够有效解决命名冲突的问题,但是还是存在着定义大量全局变量的问题。 另外我们也可以使用对象来创建命名空间,这能够减少全局变量污染,并且由于对象的 key-value 聚合的、排列整齐的结构, 大大提高代码可读性。 ``` #### 5. 自执行函数实现模块 ``` IIFE 立即执行函数表达式:Immediately Invoked Function Expression 使用立即执行函数表达式,可以较少全局变量造成的空间污染,也可以使用它创造更多的私有变量 ``` ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/%E8%87%AA%E6%89%A7%E8%A1%8C%E5%87%BD%E6%95%B0%E5%AE%9E%E7%8E%B0%E6%A8%A1%E5%9D%97%201.png) ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/%E8%87%AA%E6%89%A7%E8%A1%8C%E5%87%BD%E6%95%B0%E5%AE%9E%E7%8E%B0%E6%A8%A1%E5%9D%97%202.png) #### 6. [资料] 立即执行函数表达式 IIFE 详解 ``` 立即调用的函数表达式(IIFE) 大家在学习 JavaScript 的过程中,可能会看到许多下面的或者类似的代码: (function(){ //do something here; })(); 对于上面的代码,有两种说法,一部分人称之为 自执行的匿名函数(self-executing anonymous function),另外一部分 则更为推崇 Ben Alman 的叫法:立即调用的函数表达式(IIFE,Immediately-Invoked Function Expression )。事实上, 大家可以按照自己的理解来选择符合的说法。 立即调用函数会报错 在Javascript中,一对圆括号()是一种运算符,我们可以使用圆括号来调用一个函数,如调用 sayHello 方法则可以这样做。 var sayHello = function() { // some code } // 使用圆括号表达式调用 sayHello(); 有时候,我们可能会和上面的代码一样,在定义一个函数后,立即且只调用一次该函数。 然而当我们按照下面这样写的时候,发现会报错。 // 定义了一个匿名函数 function () { // some code }(); // SyntaxError: Unexpected token ( 之所以产生这个错误的原因是,JavaScript引擎在解析发现 function 关键字出现在行首,认为这一段代码是函数申明语句, 因此在 function 关键字后面需要的是一个函数标识符名称,而对于标识符来说是不能以 ( 来命名的。 因此,我们给该函数加上命名。但是发现还是会报一个不一样的错误: // 定义了一个匿名函数 function sayHello() { // some code }(); // SyntaxError: Unexpected token ) 原因其实前面也提及到了,JavaScript引擎在解析发现 function 关键字出现在行首,认为这一段代码是函数申明语句。 而函数申明语句并不能通过圆括号 () 来结尾,因此就报错了。 立即调用的函数表达式 上面的问题的解决方法就是,让我们函数申明语句转换成函数表达式,即使 function 关键字不出现在行首,让 JavaScript 解析引擎将其理解为一个表达式,避免错误。要使其成为一个表达式,有很多种方法。其中最简单且最容易阅读的方式, 便是使用圆括号来实现。 (function(){ /* some code */ }()); // 或者可以这样写 (function(){ /* some code */ })(); 因此我们把上面的表达式叫做 立即调用的函数表达式(Immediately-Invoked Function Expression) 也就是 IIFE。 其他的写法 除了上面的写法外,还有许多能够让 JavaScript 解析器解析成表达式的写法,如下面的写法: true && function(){ /* some code */ }(); // 或者使用操作符 !function(){ /* some code */ }(); ~function(){ /* some code */ }(); -function(){ /* some code */ }(); +function(){ /* some code */ }(); IIFE 的好处 大多数情况,我们使用到 IIFE ,并不会被再次调用,因此我们通常会使用匿名函数,即忽略立即执行的函数的函数名称。 我们可以使用 IIFE 来形成了一个私有的作用域(模拟块级作用域), 因此我们可以封装想要的私有变量,以及避免变量出现冲突的情况。 // 可被访问 canRead var canRead = 2; var same = 1; // 私有变量 noRead (function (){ var noRead = 3; var same = 2; })(); console.log(canRead); // 2 console.log(noRead); // noRead is not defined // IIFE 定义变量不会污染外部的变量 console.log(same); // 1 IIFE 创建 Module 我们可以利用 IIFE 来创建私有的函数作用域,不仅可以避免变量冲突还可以创建私有变量和方法, 十分适合用来创建一个模块,如下所示: // 创建一个立即调用的匿名函数表达式 // return一个变量,其中这个变量里包含你要暴露的东西 // 返回的这个变量将赋值给 module var module = (function() { var num = 1; return { get: function () { return num; }, set: function (value) { num = value; } } })(); 更多阅读 IIFE by Ben Alman:http://benalman.com/news/2010/11/immediately-invoked-function-expression/#iife 立即调用的函数表达式(IIFE)- 阮一峰:http://javascript.ruanyifeng.com/grammar/function.html#toc24 ``` ``` 练习题 /** * 修改下面的代码,完成以下要求: * 1、使用立即执行函数表达式包裹下面的模块代码,并使用全局变量 myModule 来表示模块对象 * 2、改造后的模块只暴露接口 getPrivateNum 和 属性 publicName */ var myModule = (function() { var privateNum = 20; var publicName = 'module'; function getPrivateNum() { return privateNum; } return { publicName: publicName, getPrivateNum: getPrivateNum } })(); ``` #### 7. 模块依赖 ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/%E6%A8%A1%E5%9D%97%E4%BE%9D%E8%B5%96%201.png) ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/%E6%A8%A1%E5%9D%97%E4%BE%9D%E8%B5%96%202.png) #### 8. 模块化规范 ``` 1.CommonJS 2.AMD 异步模块定义 3.CMD 通用模块定义 ``` ``` 1.模块化规范主要是为了解决命名冲突和模块间依赖管理的问题而诞生的。 2.模块化规范使开发人员能够以同样的方式来编写模块。 3.使我们能够不用通过手动的方式来维护依赖,让模块化开发变得更加简单和自然。 4.目前通用的模块化规范有CommonJS、AMD(异步模块定义)、CMD(通用模块定义)等。 ``` ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/%E6%A8%A1%E5%9D%97%E5%8C%96%E8%A7%84%E8%8C%83%201.png) #### 9. 一切源自 CommonJS ``` node :2009 年node.js诞生==》服务端编写复杂需要模块化==》参照 CommonJS 规范 ``` ``` CommonJS: 1.文件即模块: 一个文件即一个模块每个模块都有自己的作用域,在一个文件里定义的变量及函数都是私有的,对其他文件时不可见的 2.使用 module.exports 或使用简单引用(exports) 暴露对外的接口: 规定每个模块的内部 module 变量,代表当前模块,module 是一个对象,当加载某个模块时,其实就是加载该module.exports 属性。 3.使用 require 同步加载依赖模块 ``` ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/%E4%B8%80%E5%88%87%E6%BA%90%E8%87%AA%20CommonJS%20.png) #### 10. AMD & RequireJs ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/AMD%20%26%20RequireJs%201.png) ``` 异步模块定义:Asynchronous Module Defintion 采用异步模式加载模块,模块加载不会影响后面语句的运行,十分适用于浏览器的场景 1.使用 define 函数定义模块 define(id?,dependencies?,factory); id?,------> 模块标识 dependencies?,----->依赖的模块数组,默认为:['require','exports','module'] factory ----> 模块初始化要执行的函数或者对象 ``` ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/AMD%20%26%20RequireJs%202.png) ``` 2.使用 require 函数调用模块 require(modules,callback); modules, ---加载的模块ID数组 callback ---加载后的回调 ``` ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/AMD%20%26%20RequireJs%203.png) ``` JS没有原生支持AMD 规范,使用 AMD 进行页面开始的时候,需要使用到模块加载器 ``` ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/AMD%20%26%20RequireJs%204.png) ``` 模块加载器:require.js 下载地址:requirejs.org/docs/download.html Mininfied: 压缩版本 With Comments:是注释版本 ``` #### 11. [资料] CMD 和 SeaJs ##### CMD & SeaJS ``` CMD(Common Module Definition) 通用模块定义。其是国内前端大牛玉伯的杰作,而 SeaJS 则是 CMD 规范的模块加载器 即具体实现。然而随着前端的发展,SeaJS 已经逐渐退出了前端的舞台了。因此这里只会稍微给大家讲解下 CMD 和 SeaJS 的 一些知识点。大家了解下就好。 ``` ##### CMD 和 AMD 通常我们会拿 CMD 规范来和 AMD 规范进行对比。那么 AMD 和 CMD 有什么区别和对比的呢? ##### 申明依赖模块不同 对于依赖的模块,AMD 和 CMD 的处理方式是不一样的。 * AMD 推崇依赖前置,在定义模块的时候就要声明其依赖的模块。 * CMD 推崇依赖就近,只有在用到某个模块的时候再去 require 。 ``` // CMD define(function(require, exports, module) { var a = require('./a') a.doSomething() // 依赖可以就近书写 var b = require('./b') b.doSomething() }) // AMD 默认推荐的是 依赖必须一开始就写好 define(['./a', './b'], function(a, b) { a.doSomething() b.doSomething() }) ``` ##### 执行依赖模块时机 * AMD 提前执行依赖(异步加载:依赖先执行)+延迟执行 * CMD 延迟执行依赖(运行到需加载,根据顺序执行) ##### 加载器 * AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。 * CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。 ##### 使用SeaJS SeaJS 是 CMD 规范的具体实现。使用 SeaJS 和使用 requireJS 十分类似,只是写法上稍微有所不同,具体如下: * 引入 SeaJS 的库 * 定义模块(define) * 暴露模块接口(exports) * 加载依赖模块(require) 如下面的代码: ``` <script src="./sea.js"></script> <script> define(function (require,exports,module) { // exports : 对外的接口 // requires : 依赖的模块 require('./a.js');//如果地址是一个模块的话,那么require的返回值就是模块中的exports }); </script> ``` ##### SeaJS(CMD)为什么被淘汰 ``` 事实上,在过去 SeaJS(CMD) 曾经颇具影响力,为什么要逐渐被淘汰了呢? 感兴趣的同学可以带着这样的问题,阅读下面的帖子。 Sea.js作者发布微博: 应该给 Sea.js 和 KISSY 也树一块墓碑了。 :https://www.zhihu.com/question/34756861 ``` ##### 更多阅读 SeaJS 规范文档:https://github.com/seajs/seajs/issues/242 从 CommonJS 到 Sea.js :https://github.com/seajs/seajs/issues/269 #### 12. [资料] ES6 模块标准 ##### 面向未来的 ES6 模块标准 ``` 既然模块化已经越来越重要,那么从语言层面上直接去解决这个问题就显得很有必要(况且其他语言早就有了)。于是 ES6 直接在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和 服务器通用的模块解决方案。 ``` ##### 设计思想 ``` 简单来说,ES6 模块的设计思想就是:一个 JS 文件就代表一个 JS 模块。在模块中你可以使用 import 和 export 关键字来 导入或导出模块中的东西。 ES6 模块主要具备以下几个基本特点: 自动开启严格模式,即使你没有写 use strict 每个模块都有自己的上下文,每一个模块内声明的变量都是局部变量,不会污染全局作用域 模块中可以导入和导出各种类型的变量,如函数,对象,字符串,数字,布尔值,类等 每一个模块只加载一次,每一个 JS 只执行一次, 如果下次再去加载同目录下同文件,直接从内存中读取。 一个模块就是一个单例, 或者说就是一个对象 ``` ##### export ``` export 命令用于规定模块的对外接口。如果你希望外部能够读取模块内部的变量,函数或类等,就必须使用 export 关键字 输出该内容。 下面我们以个人所得税计算为一个模块,来具体使用下 export: // 个人所得税计算模块 // 在线参考站点:[个人所得税](http://www.gerensuodeshui.cn/) // personal-income-tax.js // 个税起征点 export const taxBasicNum = 3500; // 税率等级 export const taxRatioLevel = [ { num: 1500, // 小于1500 ratio: '3%', subtract: 0, // 速算扣除数 }, { num: 4500, // 大于1500,小于4500 ratio: '10%', subtract: 105, }, { num: 9000, // 大于4500,小于9000 ratio: '20%', subtract: 555, }, { num: 35000, // 大于9000,小于35000 ratio: '25%', subtract: 1005, }, { num: 55000, // 大于35000,小于55000 ratio: '30%', subtract: 2755, }, { num: 80000, // 大于55000,小于80000 ratio: '35%', subtract: 5505, }, { num: 80000, // 大于80000 ratio: '45%', subtract: 13505, }]; // 所缴税收 // 应纳税所得额 = 应发工资 - 五险一金 - 个税起征点 // 所缴税收 = 应纳税所得额 * 税率 - 速算扣除数 export function calTax (num, insurance) { let taxShouldNum = num - insurance - taxBasicNum; let tax; switch (true) { case taxShouldNum < taxRatioLevel[0].num: tax = taxShouldNum * taxRatioLevel[0].ratio - taxRatioLevel[0].subtract; break; case taxShouldNum < taxRatioLevel[1].num: tax = taxShouldNum * taxRatioLevel[1].ratio - taxRatioLevel[1].subtract; break; case taxShouldNum < taxRatioLevel[2].num: tax = taxShouldNum * taxRatioLevel[2].ratio - taxRatioLevel[2].subtract; break; case taxShouldNum < taxRatioLevel[3].num: tax = taxShouldNum * taxRatioLevel[3].ratio - taxRatioLevel[3].subtract; break; case taxShouldNum < taxRatioLevel[4].num: tax = taxShouldNum * taxRatioLevel[4].ratio - taxRatioLevel[4].subtract; break; case taxShouldNum < taxRatioLevel[5].num: tax = taxShouldNum * taxRatioLevel[5].ratio - taxRatioLevel[5].subtract; break; case taxShouldNum > taxRatioLevel[6].num: tax = taxShouldNum * taxRatioLevel[6].ratio - taxRatioLevel[6].subtract; break; default: tax = 0; } return tax; } // 实发工资 export function calWages(num, insurance) { let tax = calTax(num, insurance); let wages = num - insurance - tax; return wages; } ``` ##### import ``` 使用 export 命令定义了模块的对外接口以后,其他 JS 文件就可以通过 import 命令加载这个模块。现在我们导入前面设计 的个人所得税计算模块。 // main.js import {taxBasicNum, taxRatioLevel, calTax, calWages} from './personal-income-tax'; // 可以使用 taxBasicNum 输出一段话,说明个税的起征点是多少 console.log(`个税起征点为:taxBasicNum`); // 还可以使用 taxRatioLevel 数据输出一个表格,对应各个等级的税率,这里就不演示了 // 计算20000元缴纳了五险一金3000后,应该缴纳多少税收及实际税后工资为多少 let tax = calTax(20000, 3000); let wages = calWages(20000, 3000); ``` ##### 更多 ES6 模块知识 ``` 通过上面的 export 和 import 的使用,是不是觉得 ES6 的模块很方便,也很简单。当然上面都是些基础的运用,而关于 ES6 模块网上也已经有了很多详细的文档,我们在这就不一一介绍了,这里推荐两篇详细文档以供参考学习: [译]JavaScript ES6模块指南 :https://segmentfault.com/a/1190000004100661 ECMAScript 6 入门之 Module 的语法 :http://es6.ruanyifeng.com/#docs/module 注:目前现代浏览器对 ES6 模块支持程度不同,所以一般都是使用 babel 把 ES6 代码转化为兼容的 ES5 版本的代码。 ``` #### 13. r.js ``` r.js RequireJS 的优化(Optimizer)工具:前端文件的压缩与合并 在我们使用 RequireJS通常是:在页面的 HTML 中,我们只需要加载两个文件,分别是RequireJS,页面主逻辑文件 main.js, ``` ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/r.js%201.png) ``` 通过使用 RequireJS ,他会帮我们在运行的时递归地去分析模块的依赖==》会帮我们去查找依赖模块的路径,==》会通过动态插入 script 的方式,异步加载我们依赖的模块,并且监听script 的加载方式,通过回调的方式去执行接下来的操作, ``` ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/r.js%202png.png) ``` 比如我们运行我们 main.js 的时候,由于发现其依赖 common 的模块,于是RequireJS 会通过插入script 标签的方式,帮我去 异步加载我们 common.js文件,同理,由于其要依赖 moduleD 模块,于是我们又会异步加载我们的 d.js 文件,由于 moduleD 依赖于 moduleA 文件,于是它会帮我们去异步加载我们的 a.js 文件,通过使用 RequireJS 能够使我们更好的去管理模块间的依赖, 但是也暴露出这样一个问题 ``` ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/r.js%203.png) 我发现当我们使用模块的开发的时候,会导致页面的请求数过多,是否有一种方式可以让我们去解决模块化导致请求数过多的这个问题吗? ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/r.js%204.png) ``` 有的,就是 r.js,它是 RequireJS 的优化(Optimizer)工具 ;其目的是通过使用这个工具,把我们对前端的文件进行压缩以及合并, 我们的开发就变成这样子,我们会使用 RequireJS + r.js,这样一种组合的方式来开发我的页面,同样是这样的一个页面,此时的我们 不再是在运行 main.js 的时候去分析模块间的依赖,而是通过使用 r.js 线下去分析依赖和查找模块间的路径,通过 r.js 分析依赖, 我们大概会得到一个这样的一个对象, ``` ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/r.js%205.png) ``` 分析完模块依赖和模块的路径之后呢, r.js 会把我多个模块的文件进行合并,比如我把我们的这些 模块文件合并到我们的 bundle.js 文件里, ``` ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/r.js%206.png) 合并文件之后呢,会对我的文件进行压缩,来减少我们文件的请求体积。 ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/r.js%207.png) ``` 只要修改我们的主逻辑入口就行了,我把我们这里面的 main.js 修改成压缩之后的 bundle.js 文件即可了,通过使用 r.js 能够让我们在 RequireJS 按需加载的基础上,进一步的提供前端的优化,来减少我们前端文件的大小,减少我们的 HTTP 的请求数,使我们能够, 模块化与性能兼得。 ``` ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/r.js%208.png) r.js 下载地址:http://requirejs.org/docs/download.html #### 14. 新时代模块化管理 前三个冗余 ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/%E6%96%B0%E6%97%B6%E4%BB%A3%E6%A8%A1%E5%9D%97%E5%8C%96%E7%AE%A1%E7%90%86%201.png) 建议使用 ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/%E6%96%B0%E6%97%B6%E4%BB%A3%E6%A8%A1%E5%9D%97%E5%8C%96%E7%AE%A1%E7%90%86%202.png) 为什么淘汰 r.js ? ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/%E6%96%B0%E6%97%B6%E4%BB%A3%E6%A8%A1%E5%9D%97%E5%8C%96%E7%AE%A1%E7%90%86%203.png) 工程化的模块化管理 ![](https://raw.githubusercontent.com/lz109896/Web-datum/d01817a4407f77622ab2cb8f4286bb3fec003a80/%E6%96%B0%E6%97%B6%E4%BB%A3%E6%A8%A1%E5%9D%97%E5%8C%96%E7%AE%A1%E7%90%86%204.png)