在框架里面都讲模块化、组件化,其实发展到今天,模块化是走了很长的一条路的,现在ES6终于标准化了,不过也可以看看以前的历史。
1、模块化发展历史
1.1 模块化的目的
2009年以前,前端还属于手动操作html/css/js之间的关系,js代码还不够庞大到需要工程化的处理。
随着js的不断发展,在项目中越来越重要,承担的功能越来越多,js代码的体量和复杂度上升,就需要进行规范的管理。
因为有了运行大量 Javascript 脚本的复杂程序 ,所以 将 JavaScript 程序拆分为可按需导入的单独模块的机制就非常重要,一个js文件代码多了之后,就需要拆分成多个模块的文件,这样就形成了模块化的概念。
在今天看来, 模块化应该具有以下价值:
可维护性
减少全局污染
可复用性
版本管理
方便管理依赖关系
分治思想的实践
但是,随着js的复杂攀升,ECMAScript却没有提出一套规范化的模块化语法,所以,js的模块化走了一条十来年不断演化的道路。
1.2 前端模块化的雏形
1.2.1 第一阶段 文件划分
具体做法就是将每个功能及其相关状态数据各自单独放到不同的文件中, 约定每个文件就是一个独立的模块, 使用某个模块就是将这个模块引入到页面中,然后直接调用模块中的成员(变量 / 函数)
//module-1.js var module_name = 'module1'; function fn1() { console.log(module_name + 'fn1'); } function fn2() { console.log(module_name + 'fn2'); } $('html').on('click', function() { console.log(module_name+'被执行了') })
//module-2.js var module_name = 'module2'; function fn1() { console.log(module_name + 'fn1'); } function fn2() { console.log(module_name + 'fn2'); }
<!--index.html --> <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> <script src="js/module-1.js"></script> <script src="js/module-2.js"></script> <script> // 调用函数的时候,两个模块有相同的函数名,导致后面的模块覆盖了前面的 // 命名冲突 fn1(); // 模块内部的成员可以在外面被修改 module_name = '我的模块名'; console.log(module_name) </script>
这种缺点非常明显:
模块变量相当于在全局声明,没有私有空间,所有成员都可以在模块外部被访问或者修改
模块多了之后,会有命名冲突的问题。
变量都在全局定义,导致难以调试,我们很难知道某个变量到底属于哪些模块。
无法清晰地管理模块之间的依赖关系和加载顺序。假如 a.js 依赖 b.js,那么 HTML 中的 script 执行顺序需要手动调整,不然可能会产生运行时错误。
1.2.2 第二阶段 命名空间
在第一阶段的基础上,通过将每个模块「包裹」为一个全局对象的形式实现,有点类似于为模块内的成员添加了「命名空间」的感觉。
// module-1.js var module1 = { module_name: 'module1', fn1: function () { console.log(this.module_name + 'fn1'); }, fn2: function () { console.log(this.module_name + 'fn2'); } } $('html').on('click', function () { console.log(module2.module_name + '被执行了') })
// module-2.js var module2 = { module_name: 'module2', fn1: function () { console.log(this.module_name + 'fn1'); }, fn2: function () { console.log(this.module_name + 'fn2'); } } // 无法管理模块之间的依赖关系。 module1.fn2();
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> <script src="js/module-1.js"></script> <script src="js/module-2.js"></script> <script> // 通过「命名空间」减小了命名冲突的可能 module1.fn1(); module2.fn1(); // 但是同样没有私有空间,所有模块成员也可以在模块外部被访问或者修改, module1.module_name = '改了之后的模块名'; console.log(module1.module_name); </script>
虽然每个变量都有自己专属的命名空间,我们可以清楚地知道某个变量到底属于哪个模块
,同时也避免全局变量命名的问题。 但是依然没有私有空间,外部可以修改模块内部变量,也无法管理模块之间的依赖关系。
1.2.3 IIFE立即执行的函数表达式
使用立即执行函数表达式(IIFE:Immediately-Invoked Function Expression)为模块提供私有空间 。
具体做法就是将每个模块成员都放在一个函数提供的私有作用域中,对于需要暴露给外部的成员,通过挂在到全局对象上的方式实现。
//module-1.js ; (function () { var module_name = 'module1'; function fn1() { console.log(module_name + 'fn1'); } function fn2() { console.log(module_name + 'fn2'); } // 不能直接访问模块内部的私有成员 $('html').on('click', function () { console.log(module1.module_name + '被执行了') }) window.module1 = { fn1: fn1, fn2: fn2 } })();
//module-2.js ; (function () { var module_name = 'module2'; function fn1() { console.log(module_name + 'fn1'); module1.fn1(); } function fn2() { console.log(module_name + 'fn2'); } window.module2 = { fn1: fn1, fn2: fn2 } })();
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> <script src="js/module-1.js"></script> <script src="js/module-2.js"></script> <script> module1.fn1(); module2.fn1(); // 外部不能直接访问模块内部的私有成员 console.log(module1.module_name); // undefined // 为模块添加了一个新的属性,虽然名字和模块内部的变量名同名,但是它们在不同的作用域,互相不受影响。 module1.module_name = '改了模块名字'; console.log(module1.module_name); //改了模块名字 module1.fn1(); // module1fn1 console.log(module1);//{module_name: '改了模块名字', fn1: ƒ, fn2: ƒ} </script>
每个IIFE
即立即执行函数
都会创建一个私有的作用域,在私有作用域中的变量外界是无法访问的,只有模块内部的方法才能访问。相比于命名空间的模块化手段,IIFE
实现的模块化安全性要更高,对于模块作用域的区分更加彻底。
对于模块内部的module_name变量,我们只能在模块内部的fn函数中通过闭包访问,而在其它模块中无法直接访问。这就是模块私有化功能,避免模块私有成员被其他模块非法篡改,相比于命名空间的实现方式更加安全。
1.2.4 利用传参解决依赖问题
只用IIFE虽然解决了私有化的问题,但是没有解决模块之间的依赖关系。
后来通过利用立即执行函数的参数传递模块依赖项。 这使得每一个模块之间的关系变得更加明显。
//module-1.js ; (function ($) { var module_name = 'module1'; function fn1() { console.log(module_name + 'fn1'); $('body').css('backgroundColor', '#ddd'); } function fn2() { console.log(module_name + 'fn2'); } window.module1 = { fn1: fn1, fn2: fn2 } })(jQuery);
//module-2.js ; (function () { var module_name = 'module2'; function fn1() { console.log(module_name + 'fn1'); } function fn2() { console.log(module_name + 'fn2'); } module1.fn2(); window.module2 = { fn1: fn1, fn2: fn2 } })();
<script src="http://unpkg.com/jquery"></script> <script src="js/module-1.js"></script> <script src="js/module-2.js"></script> <script> module1.fn1(); module2.fn1(); </script>
2、模块化的规范
十年之前,模块化还主要使用闭包简单的实现一个命名空间。使用这种解决方式可以简单粗暴的处理全局变量和依赖关系等问题。
转眼间模块化已经发展了有十余年了,不同的工具和轮子层出不穷,下面是最各大工具或框架的诞生时间:
生态 | 诞生时间 |
CommonJS | 2009年 |
Node.js | 2009年 |
NPM | 2010年 |
requireJS(AMD) | 2010年 |
seaJS(CMD) | 2011年 |
broswerify | 2011年 |
webpack | 2012年 |
grunt | 2012年 |
gulp | 2013年 |
react | 2013年 |
vue | 2014年 |
ES6(Module) | 2015年 |
angular | 2016年 |
redux | 2015年 |
vite | 2020年 |
snowpack | 2020年 |
随着前端工程的日益庞大,前端的模块化规范统一也经历了漫长的发展阶段,现在业界主流的三大模块规范是:CommonJS
、AMD
和ES Modules
。对于模块规范而言,一般会包含两方面内容:
统一的模块化代码规范
实现自动加载模块的加载器(也称
loader
)
2.1 CommonJS规范
CommonJS 是业界最早正式提出的 JavaScript 模块规范,主要用于服务端 Node.js 。
//module-a.js var data = "hello world"; function getData() { return data; } module.exports = { getData, };
//index.js const { getData } = require("./module-a.js"); console.log(getData());
CommonJS 中使用require
来导入一个模块,用module.exports
来导出一个模块。CommonJS 定义了一套完整的模块化代码规范,不过仍然存在一些问题:
它的模块加载器由 Node.js 提供,依赖了 Node.js 本身的功能实现。如果 CommonJS 模块直接放到浏览器中无法执行。
CommonJS 约定以同步的方式进行模块加载,这种加载机制放到浏览器端,会带来明显的性能问题。它会产生大量同步的模块请求,浏览器要等待响应返回后才能继续解析模块。即,模块请求会造成浏览器 JS 解析过程的阻塞,导致页面加载速度缓慢。
CommonJS 的这种加载机制放在服务端是没问题的,一来模块都在本地,不需要进行网络 IO,二来只有服务启动时才会加载模块,而服务通常启动后会一直运行,所以对服务的性能并没有太大的影响。
但是在浏览器端却会造成阻塞,白屏时间过长,用户体验不够友好。
总之,CommonJS 是一个不太适合在浏览器中运行的模块规范。因此,业界也设计出了全新的规范来作为浏览器端的模块标准,最知名的要数AMD
和CMD
了。
2.2 AMD规范和CMD规范
2.2.1 RequireJS和SeaJS
AMD
全称为Asynchronous Module Definition
,即异步模块定义规范。模块根据这个规范,在浏览器环境中会被异步加载,而不会像 CommonJS 规范进行同步加载,也就不会产生同步请求导致的浏览器解析过程阻塞的问题了。
RequireJS 遵循的是 AMD(异步模块定义)规范。
SeaJS 遵循的是 CMD (通用模块定义)规范(阿里的玉伯主导的)
随着 2015 年 6 月,ECMAScript 对 ES6 Modules 的正式发布,浏览器厂商和 Node.js 随之纷纷跟进实现,市面上的模块化加载库随之暗淡失色,间接给 CommonJS 社区判了死刑。在浏览器端取而代之流行的做法的是大家都使用 ES6 Modules 写法,然后使用 Babel 等的 transpiler 来应对不同浏览器版本的支持程度和在浏览器端异步特性产生的一些待解决的问题。Node.js 的模块还是大量的采用 CommonJS 模式,随着对 ES6 Modules 的支持力度的提高和可以兼容之前 CommonJS 模块,CommonJS 写法过渡到 ES6 Modules 只是时间的问题。
3、 ES6 Module
ES Module
(或ESM
), 是由 ECMAScript 官方提出的模块化规范,它已经得到了现代浏览器的内置支持。
不仅在浏览器端,一直以 CommonJS 作为模块标准的 Node.js 也从12.20
版本开始正式支持原生 ES Module。
3.1 ESM特性
3.1.1 在HTML中声明脚本模块
<!-- 引入外部模块 --> <script type="module" src="main.js"></script> <!-- 把模块导入内部脚本--> <script type="module"> console.log('this is es module'); </script>
3.1.2 默认是严格模式
<!-- ESM 默认采用严格模式 use strict --> <script type="module"> console.log(this); //undefined </script>
3.1.3 每个模块都是运行在单独的私有作用域中
3.1.4 通过CORS访问外部资源
<!-- 通过CORS 跨域资源共享方式访问外部的模块, 有的外部地址不支持CORS,会报错 --> <script type="module" src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> <!-- 支持CORS跨域访问资源 --> <script type="module" src="unpkg.com/jquery"></script>
CORS 全称为 Cross-Origin Resource Sharing,被译为跨域资源共享,简称跨域访问,是 W3C 制定的标准协议。它由一系列传输的 HTTP 标头(首部字段)组成,浏览器会根据这些 HTTP 标头决定着是否阻止前端 JS 代码获取跨域请求的资源。CORS 主要作用是消除各种 API 的同源限制,以便在不同源(服务器)之间共享资源,且确保跨域数据传输的安全性。
3.1.5 模块会自动延迟加载
<!-- ESM 的script标签会延迟执行脚本,不影响页面的渲染 --> <script type="module"> alert('Hello'); </script> <p>页面内容</p>
3.2 导入和导出
模块都在单独的私有作用域中,所以外部不能直接访问,必须通过导出到外部才能访问。
为了获得模块的功能要做的第一件事是把它们导出来。使用 export
语句来完成。
导出有三种方式。
3.2.1 变量函数声明导出
//module.js // 导出变量username export var username = '诸葛' // 导出函数sayName export function sayName() { console.log(username); } // 导出类Person export class Person { constructor(username) { this.username = username; } }
//app.js //导入,这里的.js不能省略。import {} 是一种语法,不是解构赋值 import { username, sayName, Person } from "../modules/module.js"; console.log(username); sayName() console.log(new Person('孔明'))
<!--index.html--> <script type="module" src="js/app.js"></script>
3.2.2 命名导出
//module.js // 命名导出方式 var username = '诸葛'; function sayName() { console.log(username); } class Person { constructor(username) { this.username = username; } } // 集中命名导出 export {username, sayName, Person}; // 通过as重命名,导入的时候,用重命名的名字导入 export {username, sayName as sayUserName, Person};
3.2.3 默认导出
//module.js //默认导出 export default ,后面没有{}大括号 export default sayName; // 或者默认导出一个匿名函数 export default function(){ console.log(username); }
//app.js //导入默认的导出时,直接写上导出的名字,不需要大括号。因为每个模块只允许有一个默认导出 import sayName from "../modules/module.js"; //是这种的缩写形式 //import {default as sayName} from '../modules/module.js'
导入有四种方式。
3.2.4 按需导入
导入的变量名字必须和导出的变量名一致。后面的路径是相对根目录的相对路径,必须以 . 或者 / 或者http路径开头,不能直接用字母开头。
import { sayName } from "../modules/module.js";
3.2.5 命名空间导入
如果需要导入的模块很多,有可能需要重命名导出的变量, 导致有点冗余。
import { sayName as sayName1 } from "../modules/module.js"; import { sayName as sayName2} from "../modules/module1.js";
一个更好的解决方是,导入每一个模块功能到一个模块功能对象上。可以使用以下语法形式:
import * as Module from '/modules/module.js';
这将获取 module.js
中所有可用的导出,并使它们可以作为对象模块的成员使用,从而有效地为其提供自己的命名空间。
import * as Module1 from '../modules/module.js'; import * as Module2 from '../modules/module1.js'; console.log(Module1.username); Module1.sayName(); console.log(new Module1.Person('孔明')) console.log(Module2.username) Module2.sayName();
3.2.6 默认导入
import _ from 'module.js'
导入export default后面的值,可以取任意名字,因为一个模块只有一个export default,可以省略大括号。
3.2.7 默认和按需同时导入
//module.js export { username, count } export default sayName
//app.js //两种方式导入 // import {username, count, default as sayName} from '../module/module.js' import sayName, {username, count} from '../module/module.js'
3.2.8 只运行不导入
import 'module.js'
只运行模块而不引入模块中的任何方法或变量。
3.2.9 动态导入
import 只能放在最外层作用域中,不能嵌套在if这种块作用域中。
from 后面的路径也不能是变量。
所以,当不知道模块加载的路径,或者在刚开始还不需要加载模块的时候,可以使用动态加载。
这时用import()函数来实现动态加载。
import('../module/module.js').then(function(module) { console.log(module) })
这是异步加载,当加载完模块后,才执行后面的回调函数,module形参获得模块返回的导出对象。
3.2.10 注意事项
export {} 和 import {}都是一种语法,不是对象字面量,也不是解构赋值
只有export default 后面可以跟一个值,可以是变量,函数,对象,字符串等值。export default {}这才是默认导出一个对象。
导入和导出之间的成员的值是一种引用关系,不是赋值关系。导出的成员是只读的。
//module.js var count = 0; var username = 'Tom'; setTimeout(function () { username = 'Jenn'; }, 1000) setInterval(function () { count++; }, 1000) export { username, count }
//app.js import {username, count} from '../module/module.js' console.log(count, username); setInterval(function() { console.log(count, username); }, 1500)
关于原生js的教案差不多就整理完了,算是一个总结吧,自动化构建和框架的教案就不放出来了。如果我以后转行不干了,再说!
发布于 2023-09-07 21:44:52 回复该评论
发布于 2023-09-07 21:51:31 回复该评论
发表评论:
◎请发表你卖萌撒娇或一针见血的评论,严禁小广告。