浅谈前端模块化规范

比眉伴天荒 2021-11-02 10:58 509阅读 0赞

文章目录

  • 推荐阅读
  • 为什么需要模块化?
    • 1.原始的模块化写法
    • 2.添加命名空间
    • 3.立即执行函数表达式
  • CommonJS、AMD 和 CMD 规范
    • CommonJS 规范
    • AMD 规范与 RequireJS
    • CMD 规范与 Sea.js
  • ECMAScript6 标准的模块支持
    • export
    • import
    • export default 命令

推荐阅读

掘金-前端模块化
模块化七日谈
部分内容摘自《移动 Web 前端高效开发实践》- iKcamp 著

为什么需要模块化?

JavaScript 发展初期,代码简单地堆积在一起,只要能顺利地从上往下一次执行即可。但随着网站越来越复杂,实现网站功能的 JavaScript 代码也越来越庞大,网页越来越像桌面程序,很多问题开始暴露出来,比如全局变量冲突、函数命名冲突、依赖关系处理等。

1.原始的模块化写法

既然模块是要实现某个功能,那么可以把实现功能的一组函数放在同一文件中,像下面这样

  1. function a1() {
  2. // ...
  3. }
  4. function b2() {
  5. // ...
  6. }

函数 a1 和 b2 组成一个模块,其他文件先加载该模块,再对函数进行调用。
缺点:容易发生变量命名冲突,“污染”全局变量,模块成员之间没有太多必然的联系。

2.添加命名空间

使用命名空间来管理模块,即使用单全局变量的模式。

  1. var module_special = {
  2. _index: 0,
  3. a1: function () {
  4. // ......
  5. },
  6. b2: function () {
  7. // ......
  8. }
  9. }
  10. // 调用
  11. module_special.a1()
  12. module_special.b2()

通常在属性名前加下划线表示该属性为私有属性,不过这只是一种开发规范上的约定,这里实际上该属性仍然向外暴露。那么怎样让私有属性不被暴露呢?那就需要下面的模块化方式。

3.立即执行函数表达式

立即执行函数表达式简称 “IIFE”(Immediately-Invoked Function Expression)

其能够形成一个独立的作用域,用 IIFE 作为一个 “容器”,“容器” 内部可以访问外部的变量,而外部环境不能访问 “容器” 内部的变量,所以 IIFE 内部定义的变量不会与外部的变量发生冲突。

  1. var module_special = (function () {
  2. var _index = 0
  3. var a1 = function () {
  4. // ......
  5. }
  6. var b2 = function () {
  7. // ......
  8. }
  9. return {
  10. a1: a1,
  11. b2: b2
  12. }
  13. })()
  14. // 调用
  15. module_special.a1()
  16. module_special.b2()

这种方式既避免了命名冲突,又使得私有变量 _index 不能被外部访问和修改。jQuery 源码大量采用了这种方式。

CommonJS、AMD 和 CMD 规范

CommonJS 规范

node.js 应用由模块组成,采用 CommonJS 规范,通过全局方法 require 来加载模块

  1. var http = require('http') // 引入http模块
  2. var server = http.createServer(function (req, res) {
  3. // 用http模块提供的方法创建一个服务
  4. res.statusCode = 200 // 返回状态码为200
  5. res.setHeader('Content-Type', 'text/plain') // 指定请求和响应的HTTP内容类型
  6. res.end('Hello World\n') // 返回的数据
  7. })
  8. server.listen(3000, '127.0.0.1', function () {
  9. // 监听的端口和主机名
  10. console.log('Server running at http://127.0.0.1:3000') // 服务启动成功后控制台打印信息
  11. })

如何编写一个 CommonJS 规范的模块?这就需要 Module 对象。
node.js 内部提供一个 Module 构建函数,所有模块都是 Module 的实例。每个模块内部,都有一个 Module 对象,代表当前模块,包含如下属性:

  • id:模块的识别符,通常是带有绝对路径的模块文件名
  • filename:模块的文件名,带有绝对路径
  • loaded:返回一个布尔值,表示模块是否已经完成加载
  • parent:返回一个对象,表示调用该模块
  • children:返回一个数组,表示该模块要用到的其他模块
  • exports:表示模块对外输出的值

其中 exports 是编写模块的关键,其表示当前模块对外输出的接口。其他文件加载该模块,实际读取的是 module.exports。

  1. // moduleA.js
  2. module.exports = function (params) {
  3. console.log(params)
  4. }
  5. // 假设两个文件在同一目录下
  6. var moduleA = require('./moduleA')
  7. moduleA()
  8. // 为了方便,node.js为每个模块提供一个exports变量指向module.exports
  9. // 那么moduleA也可以这样编写
  10. exports.moduleA = function (params) {
  11. console.log(params)
  12. }

注意:不能把值直接赋给 exports,因为这样等于切断了 exports 与 module.exports 的联系

总结 CommonJS 模块的特点如下:

  1. 所有模块都有单独作用域,不会污染全局作用域
  2. 重复加载模块只会加载一次,后面都从缓存读取
  3. 模块加载的顺序按照代码中出现的顺序
  4. 模块加载是同步的

AMD 规范与 RequireJS

AMD 和 CMD 规范因为现在用的比较少了(反正我是没看见过),就简单介绍下
CommonJS 模块采用同步加载,适合服务端却不适合浏览器。AMD 规范支持异步加载模块,规范中定义了一个全局变量 define 函数,描述如下:
define(id?, dependencies?, factory)
第一个参数 id,为字符串类型,表示模块标识,为可选参数。若不存在则模块标识默认定义为在加载器中被请求脚本的标识。如果存在,那么模块标识必须为顶层的或者一个绝对的标识。

第二个参数 dependencies,定义当前所依赖模块的数组。依赖模块必须根据模块的工厂方法优先级执行,并且执行的结果按照依赖数组中的位置顺序以参数的形式传入(定义中模块的)工厂方法中。

第三个参数 factory,为模块初始化时要执行的函数或对象。如果为函数,只被执行一次。如果是对象,此对象应该为模块的输出值。如果工厂方法返回一个值(对象、函数或任意强制类型转换为 true 的值),应该设置为该模块的输出值。
创建一个标准 AMD 模块

  1. define('alpha', ['require', 'exports', 'beta'], function (require, exports, beta) {
  2. exports.berb = function () {
  3. return beta.verb()
  4. // 或者 return require('beta').verb()
  5. }
  6. })

创建模块标识为 “alpha” 的模块,依赖于内置的 “require” 和 “exports” 模块和外部标识为 “beta” 的模块。require 函数取得模块的引用,从而即使模块没有作为参数定义,也能够被使用。exports 是定义的 alpha 模块的实体,在其上定义的任何属性和方法也就是 alpha 模块的属性和方法。

RequireJS 库能够把 AMD 规范应用到实际浏览器 Web 端的开发中,其主要解决了两个问题:实现 JavaScript 文件的异步加载,避免网页失去响应;管理模块之间的依赖性,便于代码的编写和维护。

  1. // AMD Wrapper
  2. define(
  3. ['types/Employee'], // 依赖
  4. function(Employee) {
  5. // 这个回调会在所有依赖都被加载后才执行
  6. function Programmer() {
  7. // do something
  8. }
  9. Programmer.prototype = new Employee()
  10. return Programmer // return Constructor
  11. }
  12. )

我们来比较下 CommonJS 和 AMD 的书写风格:

  1. // CommonJS
  2. var a = require('./a') // 依赖就近
  3. a.doSomething()
  4. var b = require('./b')
  5. b.doSomething()
  6. // AMD
  7. define(['a', 'b'], function (a, b) {
  8. // 依赖前置
  9. a.doSomething()
  10. b.doSomething()
  11. })

CMD 规范与 Sea.js

CMD 规范全称为 Common Module Definition
CMD 是另一种 js 模块化方案,它与 AMD 很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD 推崇依赖就近、延迟执行。此规范其实是在 sea.js 推广过程中产生的。

  1. /** AMD写法 **/
  2. define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
  3. // 等于在最前面声明并初始化了要用到的所有模块
  4. a.doSomething();
  5. if (false) {
  6. // 即便没用到某个模块 b,但 b 还是提前执行了
  7. b.doSomething()
  8. }
  9. });
  10. /** CMD写法 **/
  11. define(function(require, exports, module) {
  12. var a = require('./a'); //在需要时申明
  13. a.doSomething();
  14. if (false) {
  15. var b = require('./b');
  16. b.doSomething();
  17. }
  18. });
  19. /** sea.js **/
  20. // 定义模块 math.js
  21. define(function(require, exports, module) {
  22. var $ = require('jquery.js');
  23. var add = function(a,b){
  24. return a+b;
  25. }
  26. exports.add = add;
  27. });
  28. // 加载模块
  29. seajs.use(['math.js'], function(math){
  30. var sum = math.add(1+2);
  31. });

ECMAScript6 标准的模块支持

ECMAScript5 及之前的版本不支持原生模块化,需要引入 AMD 规范的 RequireJS 或者 AMD 规范的 Seajs 等第三方库来实现。

直到 ECMAScript6 才支持原生模块化,其不但具有 CommonJS 规范和 AMD 规范的优点,而且实现得更加友好,语法较之 CommonJS 更简洁、支持编译时加载(静态加载),循环依赖处理得更好。

ES6 模块功能主要由两个命令构成:export 和 import,export 命令用于规定模块的对外接口,import 命令用于输入其他模块提供的功能。

export

在 ES6 中,一个模块也是一个独立的文件,具有独立的作用域,通过 export 命令输出内部变量

  1. let name = 'bus'
  2. let color = 'green'
  3. let weight = '20吨吨吨'
  4. export {
  5. name, color, weight}
  6. // export命令除了输出变量,还可以输出函数或类
  7. export function run() {
  8. console.log('Bus is running')
  9. }
  10. // 可以使用 as 关键字对输出的变量、函数、类重命名
  11. let name = 'bus'
  12. let color = 'green'
  13. let weight = '20吨吨吨'
  14. function run() {
  15. console.log('Bus is running') }
  16. export {
  17. name as busName,
  18. color as busColor,
  19. weight as busWeight,
  20. run as busRun
  21. }

import

import 命令用于导入模块

  1. import {
  2. name, color, weight, run } from './car'
  3. // 导入一个模块的时候也可以用 as 关键字对模块进行重命名
  4. import {
  5. name as busName } from './car'
  6. // 通过星号 '*' 整体加载某个文件
  7. import * as car from './car'
  8. console.log(car.name) // bus
  9. console.log(car.color) // green

export default 命令

从前面的例子可以看出,使用 import 命令加载模块时需要知道变量名或者函数名,或者整个文件,否则无法加载。为了方便,可以使用 export default 命令为模块指定默认输出,加载该模块时,可以使用 import 命令为其指定任意名字。

  1. // 定义模块 math.js
  2. let basicNum = 0
  3. let add = function(a, b) {
  4. return a+b
  5. }
  6. export default {
  7. basicNum, add }
  8. // 引入
  9. import math from './math'
  10. function test() {
  11. console.log(math.add(99 + math.basicNum))
  12. }

附:阮一峰《ES6标准入门》
import 命令是静态加载而不是动态加载的,如果 import 命令要取代 require 方法,就要能实现动态加载。
有一个提案:建议引入 import() 函数,完成动态加载,import 命令能够接收什么参数,import() 函数命令就能接受什么参数。

关于上面所说的提案,现在配置 webpack 使用 babel 转译应该能实现了(Vue 的路由懒加载,Webpack 的 splitChunk 都有用到)。
现在前端框架基本上使用 ES6 的模块化语法,node.js 仍然保持 require 导入,两者最主要的区别是:

  • require 是运行时加载,import 是编译时加载

即下面的条件加载时不可能实现的

  1. if (x === 2) {
  2. import MyModual from './myModual'
  3. }

发表评论

表情:
评论列表 (有 0 条评论,509人围观)

还没有评论,来说两句吧...

相关阅读

    相关 CommonJs规范

    CommonJS标准规定,一个单独的文件就是一个模块,模块内将需要对外暴露的变量放到exports对象里,可以是任意对象,函数,数组等,未放到exports对象里的都是私有的。

    相关 ES6模块

    浅谈模块化(完整版) =========== 原生的JS没有提供模块化,于是有人写了require.js,来帮助JS模块化,有人又不想用第三方提供的JS模块载入框架,所以原