组件化与模块化
9 min read

组件化与模块化

我们的目标就是不断提高我们的开发效率和质量、降低维护成本、提高性能。
提高收益 -> 提高开发效率,降低研发成本 -> 加快开发速度,减少变更代价。

传统的前端开发方式

例如,在一个HTML代码中,我们需要操作DOM,
为了方便,我们引入了jQuery
处理页面中,一些公共的动画等,我们引入了js/base.js
为了处理登录逻辑,我们引入了js/login.js
为了处理注册逻辑,我们引入了js/register.js
为了页面中有轮播,我们又写了一段 JS,引入了js/slide.js
等等。

此时,我们的 HTML 代码,基本有服务器端生成输出,路由也在服务器端控制。
而 CSS 部分,一般会引入通用的样式库,然后在每个页面中,在引入和本页面相关的样式文件。

缺点

在这种模式下,如果只是处理简单的页面没有什么大问题;
但如果处理类似淘宝这样大型的网站可能面临着许多问题,如果每部分是不同的团队开发的,如何协作又是一大问题。
这种情况下可能面临哪些问题呢?

  • 全局函数,可能会引起命名冲突;
  • 安全问题,比如修改了String的某个方法,导致别人使用时不符合预期;
  • 依赖关系模糊,不能很直接的看出;
  • 请求过多,引入多个script脚本;

后来引入了namespace的概念,这样可以减少:

  • 命名冲突问题;

后来又引入了匿名函数自调用 (闭包,IIFE 模式),这样:

  • 加强了安全,数据是私有的,外部只能通过暴露的方法操作;
  • 显式的传入需要的依赖;
(function(window, $) {
  let data = 'www.baidu.com';
  function foo() {
    // 用于暴露有函数
    console.log(`foo() ${data}`);
  }
  function otherFun() {
    // 内部私有的函数
    console.log('otherFun()');
  }
  // 暴露行为
  window.myModule = { foo };
})(window, jQuery);

模块化

总结:我们在讨论模块化时,更多的针对对象是JS,是为了解决在JS方面我们遇到的问题。

通过模块化,我们可以:

  • 避免命名冲突;
  • 按需加载;
  • 提高复用性和可维护性;

目前模块化的规范,有:

  • CommonJS:实现有服务端的 Node,浏览器端的 browserify;
  • AMD:实现有 require.js;
  • CMD:实现有 seajs:
  • ES6;

CommonJS

  • 不会污染全局:每个文件是一个模块,有自己的作用域,变量、函数等都是私有的;
  • 按序加载:按照在代码中出现的顺序加载;
  • Node 中的模块是同步加载的,只会在第一次加载时运行一次,结果缓存,之后读取缓存;(输入的是被输出的值的拷贝,一旦输出某个值,模块内部变化,也不会影响到这个值 [看代码] )
  • 浏览器端需要提前编译打包;
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;
console.log(counter); // 3
incCounter();
console.log(counter); // 3

AMD

CommonJS 中模块加载是同步的,这对于浏览器环境是不适合的。AMD 规范是非同步加载模块,允许指定回调函数。
AMD 规范早于 CommonJS 规范浏览器端的实现。

RequireJS 是一个工具库,主要用于客户端的模块管理。它的模块管理遵守 AMD 规范,RequireJS 的基本思想是,通过 define 方法,将代码定义为模块;通过 require 方法,实现代码的模块加载。

AMD 模式可以用于浏览器环境,并且允许非同步加载模块,也可以根据需要动态加载模块。

CMD

CMD 规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。
CMD 规范整合了 CommonJS 和 AMD 规范的特点。在 Sea.js 中,所有 JavaScript 模块都遵循 CMD 模块定义规范。

define(function (require, exports, module) {
  // 引入依赖模块 (同步)
  var module2 = require('./module2');
  function show() {
    console.log('module4 show() ' + module2.msg);
  }
  exports.show = show;
  // 引入依赖模块 (异步)
  require.async('./module3', function (m3) {
    console.log('异步引入依赖模块 3  ' + m3.API_KEY);
  });
});

ES6

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

与 CommonJS 的差异:

    1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用;
    1. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口;
  1. 可与上面的代码进行对比;
// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
  1. 第二个差异是因为 CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

组件化

如何才能提高我们的研发速度呢?
如果每次不是重复造轮子,而是利用已有的东西;
怎样才能减少变更代价呢?
如果能够理清模块之间的关系,合理分层,每次变更只需要修改其中的某个部分,或这个仅仅改变配置,那就更好了。

服务器端的组件化

引用:在服务端,我们有很多组件化的途径,像J2EE的Beans就是一种。组件建造完成之后,需要引入一些机制来让它们可配置,比如说,工作流引擎,规则引擎,这些引擎用配置的方式组织最基础的组件,把它们串联为业务流程。不管使用什么技术、什么语言,服务端的组件化思路基本没有本质差别,大家是有共识的,具体会有服务、流程、规则、模型等几个层次。

早期前端的组件化

早期,前端对界面的形态和业务逻辑,基本都没有控制台,属于别人给什么就展示什么。
这一时期的组件化,就是把一块界面一起打包成一个组件,从界面到逻辑都有。通过获取数据,整合成HTML,渲染到页面上。
HTML可以是服务器直接输出,也可以是通过模板+数据生成,然后把CSS和JS应用到HTML上。

A

时代的发展

自从Web2.0流行后,前端不只是展示了,承接了复杂度高的Web应用,比如WebQQ、在线Office。
这时,如何更高效的开发呢?

引用:在引入了SPA后,前端所处理的不仅仅是展现层,还有一部分的业务逻辑,这样就变成了前端有展现和逻辑,后端也有逻辑,这就比较乱了。而且前端有展现和逻辑两部分,如果混起来,就比较麻烦了。我们需要分层,把展现层单独拿出来。
SPA 比较类似 C/S 程序,交互过程比较复杂。但大家为什么愿意做呢?因为高效。具体体现在:1) 用用户来说,体验好;2) 运行效率比较高。

SPA的整体思路是比较好的,比如一块界面,就可以是HTML片段,用AJAX加载后就可以放在界面上;如果是JAVASCRIPT,可以用异步加载机制运行时加载。但SPA也因为复杂度的提升引来了一些问题,除了界面本身的复杂度提升之外,也有其他的一些问题,比如多个页面上状态的同步更新问题。

对于这样的问题,需要做一些架构方面的提升。

架构提升

浏览器端形成了自己的 MVC 等层次。有很多这个层次的框架:BackBone、Knockout、Avalon、Angular等。
框架的本质在于它的逻辑处理方式。
HTML和CSS等同于Spring中XML配置文件,都是可以写成配置化的。

A

新时代的组件化

在组件化时,要在框架基础之上,而框架最重要的是它的逻辑处理方式。
框架需要做的事情有:

  • 初始化自身(bootstrap)
  • 异步加载可能尚未引入的JavaScript代码(require)
  • 解析定义在HTML上的规则(template parser)
  • 实例化模型(scope)
  • 创建模型和DOM的关联关系(binding, injection)

在组件化的时候,有HTML、JAVASCRIPT、CSS的组件化。
而最重要的 JavaScript 的组件化,在设计时要是清晰的职责,松耦合。这里的松耦合不仅体现在js代码之间,也体现在js跟DOM之间的关系,所以像Angular这样的框架会有directive的概念,把DOM操作限制到这类代码中,其他任何js代码不操作DOM。

A

参考资料