模块化方案及CommonJS规范和ES6规范

原文链接:软件架构之前后端分离与前端模块化发展史

传统的模块化

随着 Ajax 的流行,前端工程师能做的事情就不只是“切图” 这么简单,现在前端工程师能做的越来越多,开始出现了明确的分工,并且能够与服务端工程师进行数据联调。这里说的传统模块化还不是后现代的模块化,早期的模块化是不借助任何工具的,纯属由 JavaScript 完成代码的结构化。在传统的模块化中我们主要是将一些能够复用的代码抽成公共方法,以便统一维护和管理,比如下面代码。

function show(id) {
  document.getElementById(id).setAttribute('style', "display: block")
}
function hide(id) {
  document.getElementById(id).setAttribute('style', "display: none")
}

然后,我们将这些工具函数封装到一个 JS 脚本文件里,在需要使用它们的地方进行引入。

但是,这种做法会衍生出两个很大的问题,一个是全局变量的污染,另一个是人工维护模块之间的依赖关系会造成代码的混乱。

例如,当我们的项目有十几个甚至几十个人维护的时候,难免会有人在公用组件中添加新的方法,比如 show 这个方法一旦被覆盖了,使用它的人会得到和预期不同的结果,这样就造成的全局变量的污染。另一个问题,因为真实项目中的公用脚本之间的依赖关系是比较复杂的,比如 c 脚本依赖 b 脚本,a 脚本依赖 b 脚本,那么我们在引入的时候就要注意必须要这样引入。

<script scr="c.js"></script>
<script scr="b.js"></script>
<script scr="a.js"></script>

要这样引入才能保证 a 脚本的正常运行,否则就会报错。对于这类问题,我们该如何解决这样的问题呢?

全局变量的污染

解决这个问题有两种,先说说治标不治本的方法,我们通过团队规范开发文档,比如说我有个方法,是在购物车模块中使用的,可以如下书写。

var shop.cart.utils = {
  show: function(id) {
    document.getElementById(id).setAttribute('style', "display: block")
  },
  hide: function(id) {
    document.getElementById(id).setAttribute('style', "display: none")
  }
}

这样就能比较有效的避开全局变量的污染,把方法写到对象里,再通过对象去调用。专业术语上这叫命名空间的规范,但是这样模块多了变量名会比较累赘,一写就是一长串,所以我叫它治标不治本。

还有一种比较专业的方法技术通过立即执行函数完成闭包封装,为了解决封装内变量的问题,立即执行函数是个很好的办法,这也是早期很多开发正在使用的方式,如下所示。

(function() { 
   var Cart = Cart || {};
   function show (id) {
     document.getElementById(id).setAttribute('style', "display: block")
   }
   function hide (id) {
     document.getElementById(id).setAttribute('style', "display: none")
   }
   Cart.Util = {
     show: show,
     hide: hide
   }
})();

上述代码,通过一个立即执行函数,给予了模块的独立作用域,同时通过全局变量配置了我们的模块,达到了模块化的目的。

当前的模块化方案

先来说说 CommonJS 规范,在 Node.JS 发布之后,CommonJS 模块化规范就被用在了项目开发中,它有几个概念给大家解释一下。

  • 每个文件都是一个模块,它都有属于自己的作用域,内部定义的变量、函数都是私有的,对外是不可见的;
  • 每个模块内部的 module 变量代表当前模块,这个变量是一个对象;
    module 的 exports 属性是对外的接口,加载某个模块其实就是在加载模块的 module.exports 属性;
  • 使用 require 关键字加载对应的模块,require 的基本功能就是读入并执行一个 JavaScript 文件,然后返回改模块的 exports 对象,如果没有的话会报错的;

下面来看一下示例,我们就将上面提到过的代码通过 CommonJS 模块化。

module.exports = {
  show: function (id) {
    document.getElementById(id).setAttribute('style', "display: block")
  },
  hide: function (id) {
    document.getElementById(id).setAttribute('style', "display: none")
  }
}
// 也可以输出单个方法
module.exports.show = function (id) {
  document.getElementById(id).setAttribute('style', "display: block")
}

// 引入的方式
var utils = require('./utils')
// 使用它
utils.show("body")

除了 CommonJS 规范外,还有几个现在只能在老项目里才能看到的模块化模式,比如以 require.js 为代表的 AMD(Asynchronous Module Definition) 规范 和 玉伯团队写的 sea.js 为代表的 CMD(Common Module Definition) 规范。
AMD 的特点:是一步加载模块,但是前提是一开始就要将所有的依赖项加载完全。CMD 的特点是:依赖延迟,在需要的时候才去加载。

AMD

首先,我们来看一下如何通过 AMD 规范的 require.js 书写上述模块化代码。

define(['home'], function(){
  function show(id) {
    document.getElementById(id).setAttribute('style', "display: block")
  }
    function hide(id) {
    document.getElementById(id).setAttribute('style', "display: none")
  }
  return {
    show: show,
    hide: hide
  };
});

// 加载模块
require(['utils'], function (cart){
  cart.show('body');
});

require.js 定义了一个函数 define,它是全局变量,用来定义模块,它的语法规范如下:

define(id, dependencies, factory)

  • id:它是可选参数,用于标识模块;
  • dependencies:当前模块所依赖的模块名称数组,如上述模块依赖 home 模块,这就解决了之前说的模块之间依赖关系换乱的问题,通过这个参数可以将前置依赖模块加载进来;
  • factory:模块初始化要执行的函数或对象。

require([dependencies], function(){})

然后,在其他文件中使用 require 进行引入,第一个参数为需要依赖的模块数组,第二个参数为一个回调函数,当前面的依赖模块被加载成功之后,回调函数会被执行,加载进来的模块将会以参数的形式传入函数内,以便进行其他操作。

CMD

sea.js 和 require.js 解决的问题其实是一样的,只是运行的机制不同,遵循的是就近依赖,来看下使用CMD方式实现的模块化代码。

define(function(require, exports, module) {
  function show(id) {
    document.getElementById(id).setAttribute('style', "display: block")
  }
  exports.show = show
});

<script type="text/javascript" src="sea.js"></script>
<script type="text/javascript">
  // 引入模块通过seajs.use,然后可以在回调函数内使用上面模块导出的方法
  seajs.use('./utils.js',function (show) {
        show('#box');
  }); 
</script>

首先是引入 sea.js 库,定义和导出模块分别是 define() 和 exports,可以在定义模块的时候通过 require 参数手动引入需要依赖的模块,使用模块通过 seajs.use。

ES6

ES6 提出了最新的模块化方案,并且引入了类的机制,让 JavaScript 从早期的表单验证脚本语言摇身一变成了一个面向对象的语言了。ES6 的模块化使用的是 import/export 关键字来实现导入和导出,并且自动采用的是严格模式(use strict),考虑到都是运行在模块之中,所以 ES6 实际上把整个语言都升到了严格模式。

在 ES6 中每一个模块即是一个文件,在文件中定义变量、函数、对象在外部是无法获取的。如果想要获取模块内的内容,就必须使用 export 关键字来对其进行暴露。我们把之前的公用脚本用 ES6 的形式再重构一遍。

// utils.js
const show = () => {
  document.getElementById(id).setAttribute('style', 'display: block');
}
const hide = () => {
  document.getElementById(id).setAttribute('style', 'display: none');
}

export {
    show,
  hide
}
// 或者直接抛出方法
export const show = (id) => {
  document.getElementById(id).setAttribute('style', 'display: block');
}
export const hide = (id) => {
  document.getElementById(id).setAttribute('style', 'display: none');
}

// 外部引入模块
import { show, hide } from './utils'

可以发现,ES6 的写法更加清晰。具备了面向对象和面向函数的特征,可读性更强。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值