JS模块化,整理一波

698 阅读8分钟

工作中,随着前端项目的扩张,无论从代码精简,还是团队合作,上线部署等各个方面来说,模块化已经不可或缺。

模块化的最大作用就是提高代码的复用率,解耦,减少冲突

前端模块化的规范,比较著名的CommonJs AMD CMD ES6模块化。这也是模块化演变的进程。

1.CommonJs

CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。

CommonJs定义的模块可以分为三个部分:

  • require 用来引入模块
  • exports 对象用于导出当前模块的方法或变量,是唯一的导出口
  • module 这个对象就代表模块本身

特性

  • 同步加载模块
  • 所有要输出的对象统统挂载在 module.exports 上,然后暴露给外界
  • 设计模式是单例模式,只会生成一个实例对象
  • 代码运行在模块作用域内,不会污染全局作用域,不会产生命名空间冲突

Node应用就是采用CommonJS模块规范的典型。

我们来看一个简单的例子,定义一个methods对象,然后暴露出去

var methods = {};
<!--可以这样输出-->
module.exports.methods = methods;

<!--也可以这样输出,相当于隐藏一行 -->  
<!--var exports = module.exports; -->
exports.methods = methods;

<!--也可以包装一层再输出-->
module.exports = { methods: methods };

引用:

var obj = require('./methods.js');
console.log(obj.methods);

但是,前面我们写到CommonJS的一个重要特性是加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。
这种规范在用于服务器变成时,没什么毛病,因为模块文件一般都存于本地硬盘中,所以加载起来也比较快。
但是放在浏览器环境中呢?
这个时候的资源大都放在服务器上,这个时候再采取同步加载,很有可能因为网速等原因造成浏览器“假死”的情况,用户体验十分不好。
这就引申出了AMD规范。

2.AMD规范

AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。

比较著名的AMD规范的典型就是Require.js,我们可以通过requireJS一窥一二

Require.js

Require.js定义了两个显性API:

  • define(id, [depends], callback) 定义模块
  • require([module], callback) 加载模块

基本思想是,通过define方法,将代码定义为模块;通过require方法,实现代码的模块加载。

我们直接看代码:

define可以定义独立模块和非独立模块(也就是含有依赖)

<!--独立模块-->
define(function(){
    return {
        m: 0,
        methods: function() {}
    }
})

<!--非独立模块-->
<!--m1, m2的参数与前面的依赖数组是相对应的-->
define(['module1', 'module2'], function(m1, m2){
    return {
        m: 0,
        methods: function() {}
    }
})

require引用:

require(['module1', 'module2'], function ( m1, m2 ) {
        m1.doSomething();
        m2.doSomething();
});

require也可以实现动态加载以及第三方加载,详细可参考阮一峰老师的《RequireJS和AMD规范》,这里不详细举例了。

我们了解了AMD规范是异步加载模块,不影响页面构建进程,但是AMD规范的一个显著特性是预加载,就是遇到的引入包都要提前执行加载(2.0之后也可以改成延迟执行,此处按下不表)比如:

define(function(require, exports, module) {
    console.log('start');

    var a = require('./a');
    a.doSomething();
    var b = require('./b');
    b.doSomething();

    return {
        c: function() {
            console.log('this is c');
        }
    };
});

这个执行顺序是什么呢?

加载a =>
加载b =>
console.log('start'); =>
a.doSomething(); =>
b.doSomething(); =>
console.log('this is c');

其实就相当于:

define(['a', 'b'], function(require, exports, module) {
    console.log('start');
    a.doSomething();
    b.doSomething();
    return {
        c: function() {
            console.log('this is c');
        }
    };
});

这种就是依赖前置,也是AMD规范的一个显著特征。
那到这可能会想为什么不能用到的时候再加载呢?
这个时候就可以引申出CMD规范。

3.CMD规范

CMD规范,全称是Common Module Definition
实现CMD规范的典范是Sea.js,虽然已经过时,但大家不妨了解一下。

CMD规范是延迟执行,就是说等执行到这了,我再加载,也就是懒加载。并且推崇依赖就近。
就是用到了,你再写,再执行加载。

define(function(require, exports, module) {
    console.log('start');

    var a = require('./a');
    a.doSomething();
    var b = require('./b');
    b.doSomething();

    return {
        c: function() {
            console.log('this is c');
        }
    };
});

那么上面那段代码拿到这里,执行顺序就会改变:

console.log('start'); =>
加载a =>
a.doSomething(); =>
加载b =>
b.doSomething(); =>
console.log('this is c');

这个可以说是两种规范最直观的差别了,但是两种规范的差别不仅仅只是这些。

4. AMDCMD的不同之处

  1. 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible。
  2. CMD 推崇依赖就近,AMD 推崇依赖前置
  3. AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。

5. ES6模块化

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

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
这和CommonJS AMD 规范是有根本区别的,因为CommonJS AMD 两者都是运行时加载

模块功能主要由两个命令构成:exportimport

5.1 export

export命令用于规定模块的对外接口。
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。

exports的使用:

<!--暴露变量-->
const a = 1;
const b = 2;
export {a, b};

<!--输出函数-->
function a() {...}
export { a }
<!--也可以设置别名-->
export { a as b }

5.2 import

import命令用于输入其他模块提供的功能。

特性:

  • import命令输入的变量都是只读,不可修改,因为它的本质是输入接口。但是如果是对象,可以修改对象的属性。
  • import会提升到头部,在编译时执行。
  • 由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
  • import语句是 Singleton 模式,只有一个实例,所以多次调用同一个包的不同方法,只会生成一个实例对象。

import命令的使用:

import { a, b } from './main'

<!--取别名-->
import { a as b } from './main'

<!--也可以直接加载模块-->
import 'lodash';

<!--模块整体加载-->
<!--此时的挂在对象a,应该是可以静态分析的,不允许运行时改变其任何属性-->
import * as a from 'lodash'

5.3 export default

从前面的例子可以看出,使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。

为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

特性

  • 一个模块只能有一个export default默认输出,输出变量名或方法为default
  • export default加载时,无论是匿名函数还是有名函数,统一默认为匿名函数,需要在引用的时候重命名。
  • 使用export default的输出,在import的时候,不需要使用{}
  • export default可以输出变量,函数,还有类

代码展示一下:

<!--export-default.js-->
export default function () {
  console.log('foo');
}
<!--也可以写成有名函数-->
export default function foo() {
  console.log('foo');
}


<!--import-default.js-->
import customName from './export-default';
customName(); // 'foo'

也可以自定义default

<!--modules.js-->
function add(x, y) {
  return x * y;
}
export {add as default};
<!--等同于-->
export default add;


<!--app.js-->
import { default as foo } from 'modules';
// 等同于
import foo from 'modules

输出类:

<!--MyClass.js-->
export default class { ... }
<!--main.js-->
import MyClass from 'MyClass';
let o = new MyClass();

5.4 exportimport 的复合写法

如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。

export { a, b } from 'xxx';

<!--等同于-->
import { a, b } from 'xxx';
export { a, b }

<!--默认接口改为具名接口-->
export { default as es6 } from './someModule';

这里相当于一个中转,实际上并没有导入到当前模块,所以当前模块不能直接使用转发的接口。

5.5 import()

import()函数可以说是import的补全,因为import是静态加载,会先于模块内其他语句先执行,这样的设计固然有利于编译器提高效率,但也导致无法在运行时加载模块。
因此出现了提案,引入import()函数,实现动态加载。例如条件加载:

if (condition) {
  import('moduleA').then(...);
} else {
  import('moduleB').then(...);
}

目前import()函数提案已到stage4阶段,大家其实可以放心用了,兼容性很乐观。

import()返回一个Promise对象,下面是一个例子:

const main = document.querySelector('main');

import(`./section-modules/${someVariable}.js`)
  .then(module => {
    module.loadPageInto(main);
  })
  .catch(err => {
    main.textContent = err.message;
  });

特性:

  • import()函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。
  • import()函数与所加载的模块没有静态连接关系,这点也是与import语句不相同。
  • import()类似于 Noderequire方法,区别主要是前者是异步加载,后者是同步加载。

6.requireimport的区别总结

  • require是同步导入,import是异步导入
  • require是值拷贝,导出值变化不会影响导入值;import指向 内存地址,导入值会随导出值而变化
  • require是运行时加载,import是编译时加载(静态加载)
  • require可以实现第三方加载,import只能导入本地存在文件

I am moving forward.