前端模块化小记

221 阅读8分钟

模块

什么是模块

  • 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
  • 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信

模块化的好处

  1. 避免命名冲突(减少命名空间污染)
  2. 更好的分离, 按需加载
  3. 更高复用性
  4. 高可维护性

最初的模块化

对象封装

将整个模块放在一个对象里面,外部访问时直接调用对象的属性或者方法

var utils = {
  request() {
    console.log(window.utils);
  }
}

utils.request();

匿名立即执行函数

匿名函数里的变量因为在不同的作用域下,不会互相干扰,外部只能通过暴露的方法操作。匿名函数方式基本上解决了函数污染及变量随意被修改问题

// 单例设计模式
var Singleton = (function () {
    var instantiated;
    function init() {
        /*这里定义单例代码*/
        return {
            publicMethod: function () {
                console.log('hello world');
            },
            publicProperty: 'test'
        };
    }

    return {
        getInstance: function () {
            if (!instantiated) {
                instantiated = init();
            }
            return instantiated;
        }
    };
})();

/*调用公有的方法来获取实例:*/
Singleton.getInstance().publicMethod();

CommonJS(CJS)

出发点

js没有完善的模块系统,标准库较少,缺少包管理工具。伴随着NodeJS的兴起,能让js在任何地方运行,特别是服务端,也达到了具备开发大型项目的能力,所以CommonJS应运而生

规定

一个文件就是一个模块,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性

require命令用于加载模块文件。它的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错

实践

NodeJSCommonJS规范的主要实践者,有四个重要的环境变量为模块化的实现提供支持:moduleexportsrequireglobal。实际上使用时,用module.exports定义当前模块对外输出的接口,用require加载模块

commonJS用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,由于网络原因限制,更合理的方法是使用异步加载

// 暴露模块
module.exports = value
exports.xxx = value

// 引入模块
require(xxx)

注意事项

  • exportsmodule.exports同时存在,module.exports会覆盖exports
  • 当模块内全是exports时,就等同于module.exports
  • 所有代码都运行在模块作用域,不会污染全局作用域
  • 模块可以多次加载,但只会在第一次加载时运行时结果就会被缓存,以后再加载的时候就直接读取缓存结果
  • 模块加载顺序,按照代码出现的顺序同步加载

AMD

Asynchronous Module Definition,异步加载模块。它是一个在浏览器端模块化开发的规范,不是原生js的规范,使用AMD规范进行页面开发需要用到对应的函数库,RequireJS

AMD规范采用异步方式加载模块,模块的加载不影响后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行

实践

使用require.js实现AMD规范的模块化:用require.config()指定引用路径,用define()定义模块,用require()加载模块

语法 define(id, dependencies, factory)

  • id:可选参数,用来定义模块的标识,如果没有提供该参数,脚本文件名(去掉扩展名)
  • dependencies:是一个当前模块用来的模块名称数组
  • factory:工厂方法,模块初始化要执行的函数或对象,如果为函数,应该只被执行一次,如果是对象,此对象应该为模块的输出值
// 定义模块
define('moduleName', ['a', 'b'], function(a, b){
    return someExportValue
})

//  引入模块
require(['a', 'b'], function(a, b){
    // 执行代码逻辑
})
require.config({
    baseUrl: 'js/', //基本路径 出发点在根目录下
    paths: {
      //自定义模块
      alerter: './modules/alerter', //此处不能写成alerter.js,会报错
      dataService: './modules/dataService',
      // 第三方库模块
      jquery: './libs/jquery-1.10.1' //注意:写成jQuery会报错
    }
})
require(['alerter'], function(alerter) {
    alerter.showMsg()
})

CMD

CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD推崇依赖前置,提前执行,CMD推祟依赖就近,延迟执行。此规范其实是在sea.js推广过程中产生的

因为CMD推崇一个文件一个模块,所以经常就用文件名作为模块id; CMD推崇依赖就近,所以一般不在define的参数中写依赖,而是在factory中写

实践

define(id, deps, factory)

factory有三个参数:function (require, exports, module) {)

  • requirefactory函数的第一个参数,require是一个方法,接受模块标识作为唯一参数,用来获取其他模块提供的接口
  • exports是一个对象,用来向外提供模块接口
  • module是一个对象,上面存储了与当前模块相关联的一些属性和方法
// 定义没有依赖的模块
define(function(require, exports, module){
    exports.xxx = val
    module.exports = val
})

// 定义有依赖的模块
define(function(require, exports, module){

    // 同步引入模块
    const modulel = reuqire('./module1.js')

    // 异步引入模块
    require.async('./module2.js',function(val){ 
        // 代码逻辑
    })
    
    exports.xxx = value
})

// 引入模块
define(function(require){
    const vall = require('./module.js')
    vall.show()
})

UMD通用规范

一种整合CommonJSAMD规范的方法,希望能解决跨平台模块方案

运行原理

  • UMD先判断是否支持NodeJS的模块(exports)是否存在,存在就使用nodejs模块模式
  • 再判断是否支持AMD(define是否存在),存在则使用AMD方式加载
(function(root, factory) {
    if (typeof module === 'object' && typeof module.exports === 'object') {
        console.log('是commonjs模块规范,nodejs环境')
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
        console.log('是AMD模块规范,如require.js')
        define(factory)
    } else if (typeof define === 'function' && define.cmd) {
        console.log('是CMD模块规范,如sea.js')
        define(function(require, exports, module) {
            module.exports = factory()
        })
    } else {
        console.log('没有模块环境,直接挂载在全局对象上')
        root.umdModule = factory();
    }
}(this, function() {
    return {
        name: '我是一个umd模块'
    }
}))

ES6模块化(ESM)

ES6在语言标准的层面上,实现了模块功能,为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:exportimportexport命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能

ES6还提供了export default命令,为模块指定默认输出,对应的import语句不需要使用大括号,这类似AMD的引用写法

ES6的模块不是对象,import命令会被JS引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。也正因此,静态分析成为可能

实践

  • export可以导出的是一个对象中包含的多个属性、方法。export default只能导出一可以不具名的函数,我们可以通过import进行引用
// 直接导出
export const hello = '';
export const api = '';

// 集中导出
const hello = '';
const api = '';
export {
  hello,
  api,
}
// foo.js
export default function foo() {}

// 等同于:
function foo() {}
export {
  foo as default
}
  • import
import { api, hello } from './config.js';

// 配合 import 使用的 as 关键字用来为导入的接口重命名
import { api as myApi } from './config.js';
// 整体导入
import * as config from './config.js';
const api = config.api;
// 对于 export default 导出的模块

import foo from './foo.js';
// 等同于:
import { default as foo } from './foo.js';

ESM和CommonJS差异

CommonJS模块输出的是一个值的拷贝,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

ES6模块的运行机制与CommonJS不一样。JS引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6import有点像Unix系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块

// 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
CommonJS模块是运行时加载,ES6模块是编译时输出接口

CommonJS加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成

CommonJS模块的require()是同步加载模块,ES6模块的import命令是异步加载,有一个独立的模块依赖的解析阶段

总结

CommonJS是同步加载的。主要是在nodejs也就是服务端应用的模块化机制,通过module.export导出声明,通过require加载。每一个文件都是一个模块。它有自己的作用域,文件内的变量,属性函数等不能被外界访问。node会将模块缓存,第二次加载会直接在缓存中获取

AMD是异步加载的。主要应用在浏览器环境下,requireJS遵循AMD规范化的模块化工具。它是通过define定义声明,通过require(['a', 'b'],function(a,b){})加载

ES6的模块的运行机制与CommonJS不一样,js引擎对脚本静态分析的时候,遇到模块加载指令后会生成一个只读引用, 等到脚本真正执行的时候才会通过去模块中获取值,在引用到执行的过程中模块中的值发生了变化,导入的这里也会跟着变化,ES6模块是动态引用,并不会缓存值,模块里总是绑定其所在的模块