关于前端开发中的模块化

2019-09-02 15:38:15 浏览数 (1)

前端开发离不开模块化,与模块化有关的关键字有以下几个:

  • require/module.exports
  • export/import
  • define/require/exprots
  • define/seajs.use

这涉及到4种模块化规范:AMD、CMD、CommonJS、ES6。

AngularJS

AngularJS模块化使用的并不是标准的AMD规范,AngularJS的风格风格大致是这样的:

不仅模块的定义和引入麻烦,与Html标签耦合也较严重,阅读吃力,扩展与复用麻烦。AngularJS的模块化风格还停留在代码层面。

后来出现了AMD规范。

AMD

AMD规范使用统一的define、require做为伪关键字。模块化定义是这样的:

代码语言:javascript复制
// a.js
define(["b", "require", "exports"], function(b, require, exports) {
    console.log("a.js执行");
    console.log(b);
    // 暴露api可以使用exports、module.exports、return
    exports.a = function() {
        return require("b");
    }
})
// b.js
define(function() {
    console.log('b.js执行');
    console.log(require);
    console.log(exports);
    console.log(module);
    return 'b';
})

引用是这样的:

代码语言:javascript复制
// index.js
require(['a', 'b'], function(a, b) {
    console.log('index.js执行');
})

define与require虽然不是语法关键字,但已经在努力让自己看起来是关键字了,它们依赖于requireJS才能工作。AMD规范仍然是在代码层面折腾。

再后来出现了CMD规范。

CMD

CMD(Common Module Definition)表示通用模块定义,该规范是国内发展出来的,由阿里的玉伯提出。就像AMD有个requireJS,CMD有个浏览器的实现SeaJS。

定义模块:

代码语言:javascript复制
// mymodule.js
define(function(require, exports, module) {
  var $ = require('jquery.js')
  $('div').addClass('active');
});

加载模块:

代码语言:javascript复制
// 加载与使用模块
seajs.use(['mymodule.js'], function(my){
});

无论是AMD、还是CMD都是在文件内的代码层面折腾。后来出现了CommonJS,从CommonJS开始,默认一个文件是一个模块。

CommonJS

定义模块:

代码语言:javascript复制
// foo.js
function foo(){
    lib.log('hello world!');
}
 
// 导出给其它模块
module.exports.foo = foo;

使用模块:

代码语言:javascript复制
const foo = require('foo');

CommonJS规范已经相当完善了。微信小程序、小游戏默认推荐的模块化规范即是CommonJS。

模块的动态加载与静态加载

使用CommonJS,可以在运行时动态加载模块:

代码语言:javascript复制
if (condition) {
  let my = require('./mymodual')
}

但是动态加载不便于引擎静态分析,不能在开发阶段就确认程序的依赖关系。此外,像这样的引入:

代码语言:javascript复制
let { stat, exists, readFile } = require('fs');

这是整体引入了fs,然后分别赋值给了三个变量。等价于:

代码语言:javascript复制
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

为了实现按需引入及在开发阶段尽早确认类库的依赖关系,尽量减少运行时异常。ES6模块规范应运而生。

ES2015(ES6)

2015年6月,ES2015正式发布,即ES6。

声明模块:

代码语言:javascript复制
export function area(radius) {
  return Math.PI * radius * radius;
}

引用与使用模块:

代码语言:javascript复制
import {area} from './circle'
console.log('圆面积:'   area(10))

如果想在引入时不使用析造语法,声明模块时可以使用default关键字:

代码语言:javascript复制
export default function area(radius) {
  return Math.PI * radius * radius;
}

引用就这样了,不再需要花括号:

代码语言:javascript复制
import area from './circle'

统一的导出格式

一个类库,有没有可能同时遵从多个模块化规范,支持多种方式的导入?

答案是可以的:

代码语言:javascript复制
;(function (name, definition) {
  // 检测上下文环境是否为AMD或CMD
  var hasDefine = typeof define === 'function',
      // 检查上下文环境是否为Node
      hasExports = typeof module !== 'undefined' && module.exports;

  if (hasDefine) {
    // AMD环境或CMD环境
    define(definition);
  } else if (hasExports) {
    // 定义为普通Node模块,使用CommonJS规范导出
    module.exports = definition();
  } else {
    // 将模块的执行结果挂在window变量中,在浏览器中this指向window对象
    // 使用script标签在html中引入
    this[name] = definition();
  }
})('hello', function () {
  // 真正的模块代码放在这里
  'use strict';
  /*eslint-disable */
  var hello = function () {};
  return hello;
});

这里导出方式,不但支持以上4种模块化方案,还支持传统的script标签引入。当以script标签在html页面中引入时,访问window.xxx可以调取类库。例如上面的示例,直接以script引入,可以访问window.hello。

use strict是开启严格模式。开启后,对代码有如下约束:

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用with语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  • eval不会在它的外层作用域引入变量
  • eval和arguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局对象
  • 不能使用fn.caller和fn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protected、static和interface)

2019年5月10日 石桥码农

0 人点赞