1.为什么需要模块化

随着前端应用的日益复杂,我们的项目代码已经逐渐膨胀到了不得不花大量时间去管理的程度了。而模块化就是一种最主流的代码组织方式,它通过把复杂的代码按照功能的不同划分为不同的模块单独维护,从而提高开发效率、降低维护成本。模块化可以使你能够更容易地重用代码。你可以创建一个模块来完成一个特定的功能,然后在多个地方重用这个模块,而不是复制和粘贴代码。

2.没有工具和规范时模块化的演进历史

2.1文件划分

最早期的模块化是通过文件划分的方式,将不同的文件划分为不同的模块,一个文件就对应一个模块,如下图就有2个模块a和b。

想要使用模块的时候就用script标签引入该模块

<script src="module-a.js"></script>
<script src="module-b.js"></script>

这种方式存在的问题

  1. 难以管理模块之间的依赖关系
  2. 多个模块的变量名会出现冲突
  3. 外部可以修改模块的内容

2.2命名空间

为了解决以上出现的问题,又有了一种新的模块化方式,便是命名空间,通过将每个模块包裹成一个全局对象来实现,这样的确解决了命名冲突问题,但是仍然存在外部可以修改模块内部内容的问题

使用模块

<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
moduleA.method1()
moduleB.method1()
//模块成员可以被修改
moduleA.name = 'foo‘
</script>

2.3立即执行函数

用立即执行函数实现了私有成员的方式,外部无法修改内部的变量,通过挂载到window对象上来完成模块化的暴露

 

3.模块化规范

前面所提到的几种早期模块化方式都有一个问题,就是必须通过script脚本标签来使用模块,但是如果随着项目规模的增大,忘记加入script标签或者引入了已经删除的模块,就会出现一些问题。也就是说,最好要把引入模块化这个工作放到js代码中去完成,而不只是在html中引入

3.1 CommonJS

NodeJS里的CommonJS规范是一个很好的模块化方式,CommonJS包含以下几个特征

  1. 一个文件就是一个模块
  2. 每个模块都有单独的作用域
  3. 通过 module.exports 导出成员
  4. 通过 require 函数载入模块

特征中的第4个,require是同步的加载,在Node中只会在启动的时候加载,执行的时候只是去使用,而到了浏览器端,每一次刷新页面都会导致大量的同步模式请求出现,这就无法使用了。

3.2 AMD(Asynchronous Module Definition)

AMD(Asynchronous Module Definition)是 RequireJS 在推广过程中对模块定义的规范化产出,。由于不是JavaScript原生支持,使用AMD规范进行页面开发需要用到对应的库函数,也就是require.js。

AMD这个规范约定每一个模块都必须通过 define 这个函数定义,默认可以接收两个参数,也可以传递三个参数:

  1. 第一个参数是模块的名字;
  2. 第二个参数是一个数组,用于声明模块依赖项;
  3. 第三个参数是一个函数,函数的参数与前面的依赖项一一对应,每一项分别为依赖项这个模块导出的成员,这个函数的作用可以以理解为为当前的这个模块提供一个私有的空间。如果需要在这个模块当中向外部导出一些成员,可以通过 return 实现

 AMD也可以通过require方法来加载对应的模块,require与define的区别是,require只是用来加载,而define是定义一个模块

 

 

 案例

src
├── index.html
├── index.js
├── lib
│   └── require.js // 使用require.js 库
└── modules
    ├── dataServe.js
    └── example.js
  • dataServe
// 导入example
define(['example'], function (example) {
    let msg = "data"
    function showMsg () {
        console.log(msg, example.getName());
    }
    return { showMsg }
})
  • example.js
define(function () {
    let name = "w"
    function getName () { return name }
    return { getName }
})
  • index.js
(function () {
    requirejs.config({
        paths: {
            example: './modules/example',
            dataServe: './modules/dataServe'
        }
    })

    requirejs(['dataServe'], function (d) {
        d.showMsg()
    })

})()
  • index.html
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    </head>
    <body>
        <script data-main="./index.js" src="lib/require.js"></script>
    </body>
</html>

问题:

  1. 使代码复杂度提高
  2. 如果模块划分的过于细致,同一个页面的请求会过多,页面效率低下

3.3 CMD+Sea.js

与CommonJS基本保持一致,但是后来也被require.js兼容了

3.4 ES Module

ES Module是现在最常用的模块化解决方案,仍然采用了与CommonJS相似的import和export来完成模块的导入和导出

在html中,只需要在script标签里加入type="module"就可以导入模块

<script type="module"> console.log('this is es module') </script>

与普通script标签不同的地方是:

  1. es模块会自动开启严格模式,忽略掉use strict。
  2. es模块都有单独的作用域
  3. es模块通过CORS方式请求,如果请求的资源不支持CORS会报跨域错误
  4. es模块等于在脚本上加入defer属性,让脚本等同步内容加载完后异步按顺序执行
<script type="module">
    //es模块会自动开启严格模式
    console.log(this); //undefined
</script>
<script type="module">
    //es模块都有单独的作用域
    let a = 1
    console.log(a); //1
</script>
<script type="module">
    //es模块都有单独的作用域
    let a = 2
    console.log(a); //2
</script>

 

导出:使用export关键词来完成导出

普通导出:

方式一 用{}包裹需要导出的变量,函数或者类,如果想要改名,可以在导出时用as来改

const name = "why";
const age = 18;

function sum(a, b) {
  return a + b;
}

class Person {
  constructor(name) {
    this.name = name;
  }
}

//3.统一导出时使用as关键字给变量起别名
export { name as bName, age, sum as bSum, Person };

方式二 export直接放在变量,函数,类声明之前

export const name = "why";
export const age = 18;

export function sum(a, b) {
  return a + b;
}

export class Person {
  constructor(name) {
    this.name = name;
  }
}

默认导出

方式一:不使用{}包裹变量,函数,类

 

const height = 1.88;

export default height;

 

方式二:使用{}包裹变量,函数,类,但必须通过as改变名字为default

 

const height = 1.88;

export {
  height as default
};

 

 

导入:使用import关键词来完成导入

方式一:分别导入,可以通过as来起别名

import {
  name as barName,
  age,
  sum,
  Person as BarPerson,
} from "./bar.js";

方式二:整体导入,通过as来起别名,然后分别使用

import * as baz from "./baz.js";
console.log(baz.name, baz.age);

baz.sum(1, 2);

const person2 = new baz.Person("lily");
console.log(person2);

方式三:导入默认导出的变量,不加{}包裹

import height from "./demo.js";
console.log(height);

 

导入导出注意点

  1. ES Module导出的变量并非变量的值本身,而是一个引用,所以导入的变量的值会受原模块的影响
  2. 导入的变量是只读的,不能进行赋值更改
  3. import异步实现,会有一个独立的模块依赖的解析阶段
  4. 不能与CommonJS相似地在导入路径中省略.js(可通过打包配置改善)

举例:导入的变量的值会受原模块的影响

 在导入中使用导出:把import from改成export from

常用于集中导出,方便后续导入资源