整理一波前端模块化

928 阅读9分钟

模块化概述:

前端模块化是一种将前端代码分解成独立的可重用模块的方法。它可以帮助开发人员更好地组织和管理大型的复杂项目。另外,模块化在前端代码中的应用不仅仅停留在代码层面,还涉及到一些打包和构建工具,如Webpack、Rollup等。它们可以优化代码的加载和执行,减小网络请求和代码体积,提高项目的加载速度和性能。

JavaScript原本的模块化可以通过函数或对象实现:

函数: 将几个函数封装到一个文件,需要的时候记载这个文件,调用其中函数即可。 但是这样会污染全局变量,造成命名冲突。

对象: 模块写成一个对象,模块成员都封装在对象里,通过调用对象属性,访问使用模块成员。但同时也暴露了模块成员,外部可以修改模块内部状态。

而JavaScript历史上并没有模块体系,无法将一个大程序拆分,对开发大型的、复杂的项目形成了巨大障碍。所以,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 、CMD

模块化开发的前提就是所有开发者必须以同样的方式编写模块,否则你有你的写法,我有我的写法,就会乱套,所以出现了四种规范 CommonJS,AMD,CMD,ES6模块化。

  • CommonJS: 服务器端模块化,是Node.js中使用的模块规范,它允许开发人员使用require和module.exports这两个关键字来定义和导出模块。在浏览器中可以使用Browserify或Webpack等工具将CommonJS模块转换为浏览器可以识别的代码。
  • AMD: 一种异步模块定义方式,它使用require.js作为模块加载器。在使用AMD时,每个模块是在需要的时候异步加载的。
  • CMD: 浏览器模块化,国内发展出来的,CMD有个浏览器的实现SeaJSSeaJS要解决的问题和requireJS一样,只不过在模块定义方式和模块加载(可以说运行、解析)时机上有所不同
  • ES6 Module: 使用import和export关键字来定义和导出模块。ES6模块化是一种静态模块定义方式,它需要在编译时确定模块依赖关系,并且支持tree shaking等优化工具。

Node.js 中,模块化遵循 CommonJS 规范,使用 require() 函数引入模块,使用 module.exports 或 exports 导出模块。

Vue 和 react 中,模块化遵循 ES6 模块规范,使用 import 关键字引入模块,使用 export 关键字导出模块。

Node.js 的模块化方式适用于后端开发,方便导入和使用第三方模块。而 Vue.js 的模块化方式适用于前端开发,方便组织和管理大型前端项目。

此外,Node.js 还支持 AMD 和 UMD 等多种模块化规范,而 Vue.js 则只支持 ES6 模块规范。

模块化规范

CommonJS

CommonJS是一种模块化规范,用于规定JavaScript代码的模块化设施,并提供模块之间的依赖管理机制。它最初是为了在服务端使用JavaScript而设计的,但现在它在前端也得到了广泛应用。

CommonJS规范定义的模块通过module.exports或exports 导出,通过require()函数导入。例如:

定义模块

// 定义一个模块
let count = 0;

function increase() {
  count += 1;
}

module.exports = {
  count,
  increase
}

// 导入模块
const { count, increase } = require('./mymodule.js');

这里用module.exports导出了一个对象,该对象具有count和increase两个属性以及相应的方法,导入时可以使用解构赋值的方式获取对象中的属性和方法。可以在另一个模块中使用require()函数导入这个模块中定义的对象。

CommonJS模块支持循环依赖,也就是说,一个模块可以依赖于其他多个模块,而其他多个模块又可能依赖于该模块。在在运行时,CommonJS实现会对模块进行缓存,以提高性能。

Node.js是一个严格遵循CommonJS规范的JavaScript环境,但在浏览器环境中,需要通过插件或其他转换工具来支持CommonJS规范的模块化机制。

下面是一个用CommonJS规范编写的示例:

1. 新建一个名为person.js的文件, 定义一个名为Person的类
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  sayHello() {
    console.log(`Hello, my name is ${this.name}, I am ${this.age} years old.`);
  }
}

module.exports = Person;
2. 在另一个文件中导入person模块,并使用该模块
const Person = require('./person');

const tom = new Person('Tom', 18);
tom.sayHello(); //hello, my name is Tom, I am 18 years old.

在上述代码中,首先在person.js模块中定义了一个Person类,然后通过module.exports导出。在另一个模块中,使用require()函数导入persion模块,并使用Person类创建一个tom对象,并使用tom的sayHello()方法输出一段文本。

由于CommonJS规范的存在,开发者可以将JavaScript的代码组织在多个模块中,通过导出和导入不同模块的对象,实现代码的复用和管理。需要注意的是,在浏览器环境中,需要通过webpack等工具来支持CommonJS规范的模块化机制。

AMD

AMD模块化规范(Asynchronous Module Definition)是一种JavaScript模块化规范,它允许开发者在需要的时候按需加载代码模块以实现更好的代码管理和性能优化

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

AMD有几个重要概念:

  1. define函数:用于定义一个模块。它接收三个参数:模块名、依赖数组和模块函数。其中模块名是可选的,依赖数组中指定了该模块所依赖的其他模块,而模块函数则定义了该模块的功能。
  2. require函数:用于引入模块或依赖。它接收一个依赖数组和一个回调函数,当所有依赖加载完毕后,回调函数将被执行。

1. 引入require.js文件

<script src="https://cdn.bootcss.com/require.js/2.3.6/require.min.js"></script>

2. 模块编写 (define)

模块可以使用define()函数定义,模块依赖也可以通过define()函数的参数列表来声明。每当一个模块定义完毕后,它就可以通过模块的名称(ID)来异步地加载它所依赖的模块。当所有依赖加载完毕后,回调函数将会被执行,它会接收所有依赖作为参数,从而实现模块的加载和初始化。

define(id?, dependencies?, factory);

// 定义一个模块
define('myModule', ['jquery'], function ($) {
  // 模块逻辑
  function myModuleMethod() {
    $('body').append('<p>Hello World!</p>');
  }
  
  // 返回公共API
  return {
    myMethod: myModuleMethod
  };
});

// 加载并初始化模块
require(['myModule'], function (myModule) {
  // 调用模块方法
  myModule.myMethod();
});

参数说明:

参数说明
idstring(可选)模块名称
dependenciesarray(可选)依赖数组,表示该模块所依赖的模块,模块名不需要加js后缀
factoryarray/object模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值
导出模块

require.js加载的模块,采用AMD规范。也就是说,模块必须按照AMD的规定来写。

假定现在顶一个模块math.js,如果不需要依赖其他模块,则:

define(function(){
    var num = 10;
    function add(a, b){ return a + b };

    // 声明了两个属性进行导出
    return {
        num,
        add
    }
})  
导入模块

例子:main.js文件加载两个模块(jquery, math)并在回调函数中使用:

// main.js 文件
require(['jquery', 'math'], function ($, math){
    // 回调函数中的math即为刚才导出的{ num, add }
  alert( math.add(1,2) )   // 3
}); 

模块加载的配置

上面案例中,引入三个模块(jquery, math),默认情况下,require.js假定这两个模块与main.js在同一个目录,文件名分别为jquery.jsmath.js,然后自动加载。

使用require.config()方法,可以对模块的加载行为进行自定义:

require.config({
    baseUrl: "./js",
  paths: {
    "jquery": "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min",
    "math": "lib/math.min",    // 省略js后缀
  }
});  
配置项说明
baseUrlstring定义读取模块的根目录
pathsobject定义模块路径与别名,如上例模块路径lib/math.min,用math指代

CMD

CMD规范是国内发展出来的,CMD的浏览器的实现是sea.js,SeaJS与requireJS类似,只不过在模块定义方式和模块加载(运行、解析)时机上有所不同。

导入:

<script src="https://cdn.bootcss.com/seajs/3.0.3/sea.js"></script>

CMD 模块编写

在 CMD 规范中,一个模块就是一个文件。代码的书写格式如下:

define(function(require, exports, module) {
  // 模块功能
  var moduleFunc = function() {
    // ...
  };
  // 导出模块接口
  exports.moduleFunc = moduleFunc;
});

参数说明
require可以用来导入其他模块
exports可以把该模块内容属性方法导出
module一个对象,存储了与当前模块相关联的一些属性和方法

在上面的 define 函数中,require 参数用于加载模块依赖,exports 参数用于导出模块接口,module 参数用于指代当前模块自身。

在另一个模块中使用当前模块时,我们可以使用以下代码:

var myModule = require('myModule');
myModule.moduleFunc();

在上面的代码中,require 函数用于加载模块,myModule 即为当前模块的公共接口,可以调用其中的 moduleFunc 方法。

需要注意的是,在 CMD 规范中,模块依赖是在模块内部通过 require 函数加载的,而不是通过 define 函数传递依赖列表。这样可以让模块定义和依赖关系更加清晰易懂,也更方便对模块进行组合和拆分。

CMD 模块定义规范 (opens new window)

定义模块 (define)
// math.js文件
define(function(require, exports, module) {
    require('./jquery.min.js')     // require 导入jquery
    $.ajax()
    exports.add = function(a, b){ return a+b };        // export导出函数add
});
导入模块 (seajs.use)
seajs.use(['./math.js'], function (math) {
   math.add(1, 2);    // 3
});
配置Sea.js ( seajs.config() )
配置项说明
basestring定义读取模块的根目录
aliasobject定义模块路径与别名,如模块路径../lib/jquery.min.js,用jquery指代
seajs.config({
    base:"./js"  
    alias:{
        'jquery':'../lib/jquery.min.js'
    }
});  

AMD 与 CMD 的区别

  • AMD是依赖关系前置,在定义模块的时候就要声明其依赖的模块;
  • CMD是按需加载依赖就近,只有在用到某个模块的时候再去require:
// CMD
define(function(require, exports, module) {
  var a = require('./a')
  a.doSomething()
  // 此处略去 100 行
  var b = require('./b') // 依赖可以就近书写
  b.doSomething()
  // ...
})

// AMD 默认推荐的是
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
  a.doSomething()
  // 此处略去 100 行
  b.doSomething()
  ...
})

ES6中的模块化

ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";。

注意:ES6模块中,顶层的this指向 undefined,即不应该在顶层代码使用this

模块功能主要由两个命令构成:exportimportexport命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

1. export命令

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。export可以让我们把变量,函数,对象进行模块化,提供外部调用接口,让外部进行引用。

export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;

或在文件末尾多变量输出:

var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};

export命令除了输出变量,还可以输出函数或类(class)。

exprot function fun(){ }

function fun() {}

exprot {fun};

有些时候并不想暴露模块里边的变量名称,还可使用as关键字对变量进行重命名

export {
    v1 as firstName,
    v2 as lastName,
    v3 as lastName
};

2. export default 命令

  1. exportexport default均可用于导出常量、函数、文件、模块等
  2. 你可以在其它文件或模块中通过import+(常量 | 函数 | 文件 | 模块)名的方式,将其导入,以便能够对其进行使用
  3. 在一个文件或模块中,exportimport可以有多个,export default仅有一个
  4. 通过export方式导出,在导入时要加{ }export default则不需要
var name="李四";
export { name }
//import { name } from "./a.js"
可以写成:
var name="李四";
export default name
//import name from "./a.js" 这里name不需要大括号

3. import 命令

使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。

export对应的导入方式

export var a ='js';
export function add(a,b){
    return a+b;
}

import {a,add} from './temp';

//也可以分开写
import a from './temp';
import add from './temp';    

export defalut对应的导入方式

var a ='js';
function add(a,b){
    return a+b;
}
export defalut {a, add}
import obj from './temp';
  • import用as方式引入 (多个变量用一个空对象来代理,你所有的方法和属性都是在types命名空间)
const LOGIN = 'login';
const LOGOUT = 'logout';
const TITLE = 'title'
export {LOGIN,LOGOUT,TITLE}
import * as types from './temp.js' //你所有的方法和属性都是在types命名空间
// 调用里面里面的值可以 这样做
types.LOGIN
types.LOGOUT
types.TITLE

由于ES6的模块化在浏览器端兼容性较差,不能直接在浏览器中预览,必须要使用Babel进行编译之后正常看到结果。