概述

什么是模块化?

  1. 模块化指的是按照一定的规则,将大文件拆分成多个相互依赖的小模块。

  2. 模块之间都是相互隔离独立的,模块中的数据都是私有的,可以通过导出手段把模块中的数据、函数等导出供其他模块使用,通过导入实现模块间数据、函数的共享。

为什么需要模块化?

随着编写应用的复杂性越来越高,如果不进行模块化,会逐渐出现全局污染、依赖混乱、数据安全等问题:

  1. 全局污染
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// news.js
function getTodayData() {
return [
{ id: 'N001', title: '新闻01' },
{ id: 'N002', title: '新闻02' },
{ id: 'N003', title: '新闻03' }
]
}
// weather.js
function getTodayData() {
return [
{ id: 'W001', weather: '天气01' },
{ id: 'W002', weather: '天气02' },
{ id: 'W003', weather: '天气03' }
]
}
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="text/javascript" src="./news.js"></script>
<script type="text/javascript" src="./weather.js"></script>
</body>
</html>

通过在F12控制台中调用getTodayData函数我们发现最后返回的是天气的数据,因为weather.js是后引入的,会同名覆盖,这就是所谓的全局污染问题,因为通过这种引入方式我们把对应js文件的函数、数据等都放到了window这个全局对象中去了,导致全局污染问题。

  1. 依赖混乱

对于通过<script type="text/javascript" src="xxx.js"></script>这一引入方式的项目,由于可能项目本身需要很多依赖,而不同依赖可能有依赖另一个依赖,因此如果引入的顺序不同可能就导致了项目本身出现问题,即所谓的依赖混乱。

  1. 数据安全

对于通过<script type="text/javascript" src="xxx.js"></script>这一引入方式的项目,会把对应js所有数据都放在window对象中,因此可能会泄露一些敏感的用户信息,导致出现数据安全问题。

常见的模块化规范

  1. CommonJS —— 服务端应用广泛
  2. AMD
  3. CMD
  4. ES6 —— 浏览器端应用广泛

CommonJS

导出

在CommonJS规范中,在一个js文件导出数据、方法有两种方式:

  1. module.exports = { }
  2. exports.xxx = xxx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const username = '小张'
const token = '<<token>>'
const age = 18

const getAge = () => {
return age
}

// 导出方式一
exports.username = username
exports.getAge = getAge

// 导出方式二
module.exports = {
username,
getAge
}

对于上述两种方式,打印导出的内容均为{ username: '小张', getAge: [Function: getAge] }

exports和module.exports的区别:

  1. 谨记一条原则:无论使用 exports 导出成员,或是 module.exports 导出成员,最终导出的结果,都是以 module.exports 所指向的对象为准。
  2. exports是对module.exports的一个引用变量,两者在最初均指向同一个空对象,exports的写法是为了方便给导出对象添加属性,不能使用exports = value的方式导出数据和方法,因为这样直接改变了exports这个变量的指向,module.exports的内容并没有改变。
1
2
3
4
5
6
7
8
9
10
const username = '小张'
const token = '<<token>>'
const age = 18

const getAge = () => {
return age
}

exports = { a: 1 }
// 最后导出的对象为空

导入

通过require可以导入对应文件的module.exports对象,可以直接利用一个变量接收,也可以对导入结果进行解构。

1
2
3
4
5
6
// 导入方式一
const user = require('./user.js')
// 导入方式二:解构
const { username, getAge } = require('./user.js')
// 导入方式三:解构的同时重命名
const { username: name, getAge: getUserAge } = require('./user.js')

扩展理解

在CommonJS规范中,每个模块在执行时是把js文件所写的内容包裹在一个内置函数中执行的,每个模块都有自己的作用域,即所谓的数据隔离,可以通过console.log(arguments.callee.toString())来进行验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function (exports, require, module, __filename, __dirname) {
const username = '小张'
const token = '<<token>>'
const age = 18

const getAge = () => {
return age
}

module.exports = {
username,
getAge
}
console.log(arguments.callee.toString())
}

CommonJS是适合在服务端的模块化,如果想在浏览器端使用,需要使用Browserify这一工具来预编译使用CommonJS模块化规范编写的JS文件,最后即可将预编译完的JS文件引入html中使用。

ES6

ES6是支持在浏览器端和服务端的模块化规范,默认是支持浏览器端,如果想支持服务端有两种方式:

  1. 将js文件后缀改为mjs即可

  2. 在项目根目录下添加package.json文件,内容如下:

    1
    2
    3
    {
    "type": "module"
    }

导出

  1. 分别导出:在需要导出的数据或者方法前添加export关键字

    1
    2
    3
    4
    5
    6
    7
    export const username = '小张'
    const age = 18
    export const motto = '相信明天会更好'

    export function getAge() {
    return age
    }
  2. 统一导出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const username = '小张'
    const age = 18
    const motto = '相信明天会更好'

    function getAge() {
    return age
    }

    export {
    username, motto, getAge
    }
  3. 默认导出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const username = '小张'
    const age = 18
    const motto = '相信明天会更好'

    function getAge() {
    return age
    }

    export default{
    username, motto, getAge
    }

对于分别导出、统一导出,最后导出的每一个数据/方法都会作为module的一个属性,而默认导出会使得module多了一个default对象,default对象的内容就是导出的内容,如下图所示:

注意:不同的导出方式可以混合使用

导入

  1. 全部导入:

    1
    import * as user from './user.js'
  2. 命名导入:通过导入和导出一样的命名这种方式来导入,如果想重命名则需要使用as关键字(针对分别导入、统一导入)

    1
    2
    3
    4
    import { username as name, getAge } from './user.js'

    console.log(name)
    console.log(getAge())
  3. 默认导入:可以用自定义变量名接受默认导出的default对象(针对默认导出)

    1
    2
    3
    4
    import user from './user.js'

    console.log(user)
    // { username: '小张', motto: '相信明天会更好', getAge: [Function: getAge] }
  4. 动态导入:导入的结果和全部导入一致,具体的使用可以参考下述例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    </head>
    <body>
    <script type="module" src="./index.js"></script>
    <button id="btn">导入模块</button>
    </body>
    </html>
    1
    2
    3
    4
    5
    6
    7
    // index.js
    const btn = document.getElementById('btn')

    btn.onclick = async () => {
    const user = await import('./user.js') // import是异步
    console.log(user)
    }
  5. 无绑定导入:如果只是想执行模块中的一些代码,而不需要导入它的任何内容,可以使用无绑定的导入,即import可以不接受任何数据,直接导入一个文件

    1
    import './log.js'

CJS和ES6区别

  1. 加载模块方式:CommonJS模块是同步加载的,这意味着在加载模块时,会阻塞代码的执行,直到模块加载完成;ES6模块是异步加载的,这意味着在加载模块时,不会阻塞代码的执行。

  2. 数据引用:CommonJS导出的是一个值拷贝,会对加载结果进行缓存,一旦内部再修改这个值,不会同步到外部;ES6是导出数据的引用,内部修改可以同步到外部。

  3. 依赖关系处理:CommonJS对模块依赖的解决是“动态的”而ES6是“静态的”。在这里“动态的”含义是,模块依赖关系的建立发生在代码运行阶段;而“静态”则是模块依赖关系的建立发生在代码编译阶段。

ES5和ES6的一些区别可以参考:ES5与ES6语法的区别及优缺点分析_es5和es6语法差异-CSDN博客

最后

对于AMD和CMD两种方式现在貌似用的比较少了,这里就不过多叙述(狗头.jpg