模块化开发是 JS 项目开发中的必备技能,它如同面向对象、设计模式一样,可以兼顾提升软件项目的可维护性和开发效率。

模块之间通常以全局对象维系通讯。在小游戏中,GameGlobal 是全局对象。在小程序中,App 是全局对象,任何页面都可以使用 getApp() 获取这个全局对象。在 NodeJS 中,global 是全局对象。在传统浏览器宿主中,window 是全局对象。

以下是作者总结的模块化实践经验。简言之,除了在浏览器项目中使用 sea.js,其它类型项目均建议直接使用原生的 ES6 模块规范。

目录

  1. CommonJS 规范
  2. AMD 规范
  3. CMD 规范
  4. ES6 模块规范
  5. 结论

CommonJS 规范

CommonJS 规范最早在 NodeJS 中实践并被推广开来。它使用 module.exports 输出模块,一个模块写在一个独立的文件内,一个文件即是一个模块。在另一个JS文件中,使用 require 导入模块。各个模块相互隔离,模块之间的通讯,通过全局对象 global 完成。

值得特别注意的是,CommonJS 这种规范天生是为 NodeJS 服务的。NodeJS 是一种服务器端编程语言,源码文件都在硬盘上,读起来很方便。CommonJS 规范作为一种同步方案,后续代码必须等待前面的require指令加载模块完成。

使用 CommonJS 规范的代码示例如下:

// 定义模块math.js
var basicNum = 0;
function add(a, b) {return a + b;
}
module.exports = { //在这里写上需要向外暴露的函数、变量add: add,basicNum: basicNum
}
// 在另一个文件中,引用自定义的模块时,参数包含路径,可省略后缀.js
var math = require('./math');
math.add(2, 5);

在小程序与小游戏的官方文档中,提到模块化时官方建议的规范即是 CommonJS 规范。但其实在作者看来,更适合小游戏/小程序开发的规范是 ES6 模块规范,原因稍后便会讲到。

AMD 规范

CommonJS 规范主要是为服务器端的 NodeJS 服务,服务器端加载模块文件无延时,但是在浏览器上就大不相同了。AMD 即是为了在浏览器宿主环境中实现模块化方案的规范之一。

AMD是一种使用JS语言自实现的模块化规范方案,主要由require.config()、define()、require 三个函数实现。require.config() 用于声明基本路径和模块名称;define() 用于定义模块对象;require() 则用于加载模块并使用。

与 CommonJS 规范不同,AMD 规范身处浏览器环境之中,是一种异步模块加载规范。在使用时,首先要加载模块化规范实现文件 require.js 及 JS 主文件,示例如下:

/** 网页中引入require.js及main.js **/
<script src="js/require.js" data-main="js/main"></script>

在上面的 Html 代码中,"js/require.js" 是实现 AMD 规范的类库文件,是任何使用 AMD 规范的网页都需要加载的;"js/main" 是开发者的代码主文件,在这个文件中加载并使用自定义模块,示例代码如下:

/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({baseUrl: "js/lib",paths: {"jquery": "jquery.min",  //实际路径为js/lib/jquery.min.js"underscore": "underscore.min",}
});
// 执行基本操作
require(["jquery","underscore","math"],function($,_,math){//在这里$代表jqurey、_代表underscorevar sum = math.add(10,20);$("#sum").html(sum);
});

而用于模块的定义,在其它 JS 文件中是这样声明的:

// 定义math.js模块
define(function () {var basicNum = 0;var add = function (x, y) {return x + y;};return {add: add,basicNum :basicNum};
});

如果在一个模块定义中依赖另一个模块对象,可以这样声明:

// 定义一个依赖underscore模块的模块
define(['underscore'],function(_){var classify = function(list){_.countBy(list,function(num){return num > 30 ? 'old' : 'young';})};return {classify :classify};
})

AMD 规范看起来完美解决了浏览器模块化开发的难题。但是它有一个天生的缺陷,对于依赖的模块无论实际需要与否,都会先加载并执行。如下所示:

define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { // 等于在最前面声明并初始化了要用到的所有模块if (false) {// 即便没用到某个模块 b,但 b 还是提前执行了b.foo()}
});

在上面的代码中,模块 a、b、c、d、e、f 都会加载并执行,即使它们在实际的模块代码中没有被用到。为了解决这个“浪费”的问题,CMD 规范应运而生。

CMD 规范

CMD 规范单从名字来看,它也与 AMD 规范很像。CMD 与 AMD 规范一样,同样是一种 JS 语言自实现的模块化方案。不同之处在于,AMD 规范是依赖前置、模块提前加载并执行;CMD 是依赖后置、模块懒惰加载再执行。示例代码如下:

/** CMD写法 **/
define(function(require, exports, module) {var a = require('./a'),b = require('./b'),c = require('./c'); //在需要时申明、加载和使用a.doSomething();if (false) {var b = require('./b');b.doSomething();}
});

在上面的代码中,模块 a 在使用时才被声明并加载。sea.js 是一个模块加载器,是 AMD 规范的主要实现者之一。使用 sea.js 定义和使用模块的示例如下所示:

/** sea.js **/
// 定义模块 math.js
define(function(require, exports, module) {var $ = require('jquery.js');var add = function(a,b){return a+b;}exports.add = add;
});
// 加载模块
seajs.use(['math.js'], function(math){var sum = math.add(1+2);
});

与 AMD 相比,CMD 貌似确实节省了无谓的模块加载。但是 AMD 规范本身就是一种异步模块加载方案,是只有在运行时才被加载并运行的,用则加载,不用不加载,有何浪费可言?况且,比起在代码中分别以 require 函数加载模块,直接在 define 方法的第一个参数中声明,似乎还更简洁与潇洒些。

sea.js 作为 AMD 规范的升级版,简化了使用方法,在使用上更加方便,值得推崇。但是 sea.js 便是浏览器开发中最佳的模块化解决方案吗?未必,还要看是什么类型的项目,后面会讲到。

ES6 模块规范

在讲 ES6 模块规范之前,我们先看一下规范前驱 CommonJS 的一个缺陷。如下所示:

// 模块定义代码:lib.js
var counter = 3;
function incCounter() {counter++;
}
module.exports = {counter: counter,incCounter: incCounter,
};
// 模块使用代码:main.js
var mod = require('./lib');
console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3
var mod2 = require('./lib');
console.log(mod2.counter);  // 3

在上面的代码中,为什么三个 mod.counter 的输出均是3?

CommonJS 规范是一种动态加载、拷贝值对象执行的模块规范。每个模块在被使用时,都是在运行时被动态拉取并被拷贝使用的,模块定义是唯一的,但有几处引用便有几处拷贝。所以,对于不同的 require 调用,生成的是不同的运行时对象。

即使如此,在上面的代码中,mod 只有一个,为什么 mod.incCounter() 对这个模块对象——即 mod 中的 counter 变量改变无效?相反,对于以下的代码:

// lib.js
var counter = 3;
function incCounter() {counter++;
}
module.exports = {get counter() {return counter},incCounter: incCounter,
};
// main.js
var mod = require('./lib');
console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 4

第二个输出是4。只是将 counter 声明为一个 getter 存取器属性,调用便正常了,为什么?

这是由于 CommonJS 的拷贝机制造成的。由于 CommonJS 规范的拷贝运行机制,在 lib.js 中使用 module.exports 输出的对象,是从 lib 模块内拷贝而得,当时 counter 的值是几,便拷贝了几。无论执行 incCounter 多少次,改变的都不是输出对象的 counter 变量。

而当定义了 getter 属性之后,该属性指向了模块定义对象中的 counter 变量了吗?不,是指向了被 incCounter 方法以闭包形式囊括的 counter 变量,这个变量是输出的模块对象的一部分。

CommonJS 规范的这个缺陷,有时候让程序很无奈,一不小心就写出了错误的代码。这个缺陷在 ES6 中得到了很好的解决。

在 ES6 模块规范中,只有 export 与 import 两个关键字。示例如下:

/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {return a + b;
};
export { basicNum, add };
/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {ele.textContent = add(99 + basicNum);
}

在上面的代码中,使用 export 关键字在 math.js 文件中输出模块,这里使用了对象字面量的属性名称简写与方法名称简写。在另一个文件中引用模块,在 import 关键字后面,{basicNum, add} 这是对象变量析构的写法。

如果在 export 模块时,使用了 default 限定词,如下所示:

//定义输出
export default { basicNum, add };
//引入
import math from './math';
function test(ele) {ele.textContent = math.add(99 + math.basicNum);
}

在 import 引入时,便可以省去花括号。这样看起来代码更清爽简洁。

ES6 模块规范与 CommonJS 规范相比,有以下不同:

(1)ES6 模块规范是解析(是解析不是编译)时静态加载、运行时动态引用,所有引用出去的模块对象均指向同一个模块对象。在上面使用 CommonJS 规范声明的 lib 模块,如果使用 ES6 模块规范声明,根本不会出现 counter 变量含糊不清的问题。

(2)CommonJS 规范是运行时动态加载、拷贝值对象使用。每一个引用出去的模块对象,都是一个独立的对象。

结论

所以综上所述,在模块化方案上最佳选择是什么?

在小程序(包括小游戏)开发项目中,由于支持 ES6,所以小程序最好的模块化方案便是使用ES6模块规范。虽然官方文档中提到的模块化规范是 CommonJS,但最佳方案作者认为却应该是 ES6。

小程序在手机端(无论 iOS 还是 Android)的底层渲染内核都是类 Chrome v8 引擎。v8 引擎在执行JS代码时,是将代码先以 MacroAssembler 汇编库在内存中先编译成机器码再送往 CPU 执行的,并不是像其它 JS 引擎那样解析一行执行一行。所以,静态加载的 ES6 模块规范,更有助于 v8 引擎发挥价值。而运行时加载的 CommonJS 规范、AMD 规范、CMD 规范等,均不利于 v8 引擎施展拳脚。遇到 CommonJS 代码,v8 可能会怒骂:“有什么话能不能一次讲完,你这样猫拉屎式的做法只能让我更慢!”

在 NodeJS 开发项目中,Node9 已经支持 ES6 语法,完全可以使用 ES6 模块规范。NodeJS 的诞生,本身就基于 Google 的 v8 引擎,没有理由不考虑发挥 v8 的最大潜能。

在浏览器 JS 开发项目中,因为从服务器加载文件需要时间,使用 CommonJS 规范肯定是不合适了。至于是使用原生的 ES 模块规范,还是使用sea.js,要看具体场景。如果想页面尽快加载,sea.js 适合;如果是单页面网站,适合使用原生的 ES6 模块规范。还有一点,浏览器并非只有 Chrome 一家,对于没有使用 v8 引擎的浏览器,使用 ES6 原生规范的优势就又减少了一点。

2019年1月21日于北京


参考资料

  • 浏览器已原生支持 ES 模块,这对前端开发来说意味着什么?
  • Node 9下import/export的丝般顺滑使用
  • Sea.js 是什么?
  • 前端模块化:CommonJS,AMD,CMD,ES6
  • Module 的加载实现

本文首先于微信公众号「艺述思维」:关于 JS 模块化的最佳实践总结

转载于:https://www.cnblogs.com/sban/p/10301143.html

关于 JS 模块化的最佳实践总结相关推荐

  1. Node.js CLI 工具最佳实践

    为什么写这篇文章? 一个糟糕的 CLI 工具会让用户觉得难用,而构建一个成功的 CLI 需要密切关注很多细节,同时需要站在用户的角度,创造良好的用户体验.要做到这些特别不容易. 在这个指南中,我列出了 ...

  2. 使用 Node.js Express 的最佳实践

    Production best practices: performance and reliability 本文讨论部署到生产的 Express 应用程序的性能和可靠性最佳实践. 这个话题显然属于& ...

  3. js函数式编程最佳实践 - 持续更新

    函数式编程最佳实践 学习文档 函数式编程术语 数组字串处理 function addString(el){return el + "0"; } var newArr = arr.m ...

  4. js条件控制语句最佳实践

    if else.switch case 是日常开发中最常见的条件判断语句,这种看似简单的语句,当遇到复杂的业务场景时,如果处理不善,就会出现大量的逻辑嵌套,可读性差并且难以扩展. 编写高质量可维护的代 ...

  5. ABP vnext模块化架构的最佳实践的实现

    在上一篇文章<手把手教你用Abp vnext构建API接口服务>中,我们用ABP vnext实现了WebAPI接口服务,但是并非ABP模块化架构的最佳实践.我本身也在学习ABP,我认为AB ...

  6. Vue中CSS模块化最佳实践

    Vue风格指南中介绍了单文件组件中的Style是必须要有作用域的,否则组件之间可能相互影响,造成难以调试. 在Vue Loader Scope CSS和Vue Loader CSS Modules两节 ...

  7. React.js 2016 最佳实践 徬梓阅读 1584收藏 71

    为什么80%的码农都做不了架构师?>>>    译者按:近几个月React相关话题依旧火热,相信越来越多的开发者在尝试这样一项技术,我们团队也在PC和移动端不断总结经验.2016来了 ...

  8. vue main.js中引入js_web前端的同学不容错过,大厂Vue最佳实践总结,提高竞争力...

    随着这几年前端技术的快速发展,Vue框架在国内普及率极高,人人都会用.那么,如何才能写得比别人优雅?比别人漂亮? 鉴于一线互联网大厂在前沿技术领域的持续研究和大规模投入,直接向他们取经,是最便捷也是最 ...

  9. 十个书写Node.js REST API的最佳实践(上)

    收录待用,修改转载已取得腾讯云授权 原文:10 Best Practices for Writing Node.js REST APIs 我们会通过本文介绍下书写Node.js REST API的最佳 ...

最新文章

  1. 怎样Selenium IDE选择combo box中值
  2. latex 伪代码_Latex-算法伪代码
  3. 第一次写购物车,甚是简陋。
  4. 一个祸害我很久的东东——事件过滤器之按键触发
  5. Ukiyo-e faces dataset 浮世绘面孔数据集
  6. 爬格子呀9.17(图论)
  7. 前端学习(3133):react-hello-react之高阶函数
  8. VS编辑代码时使用正则表达式去掉行号
  9. 启牛商学院计算机,启牛商学院达摩老师教你三招成为跨界理财达人
  10. inheritConstructorStealing.js
  11. 两篇一区SCI可认定A类博士!享​200平住房+40万科启!硕士也入编!
  12. Ajax : load()
  13. Android PDF文件阅读方案
  14. 计算机页面的工具,网页智能填写工具
  15. 开源生产排程aps(Advanced Planning and Scheduling)软件介绍
  16. 可以写一个表白代码吗
  17. 程序员该如何定位?看这四大方向
  18. 小程序未来发展趋势怎样?2020最新趋势分析
  19. 怎么正确理解「辩证法」
  20. openssl 的安装

热门文章

  1. Java实现MD5加密和文件校验
  2. 利用银行家算法避免死锁(C++实现)
  3. Activiti工作流之网关
  4. php reset input,PHP reset() 函数
  5. 怎么把一台华为路由器配置为FTP服务器?
  6. 【分享】SMTP工作原理大剖析
  7. 企业微信 添加白名单_企业微信群为什么只能加200人?企业微信群怎么申请扩容?...
  8. spark匹配html字段,Apache Spark中的高效字符串匹配
  9. xml相关php函数,PHP利用xml常用函数的详细集合示例
  10. Jmeter之断言操作