欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

模块化之CommonJs、AMD、CMD和ES6模块化

程序员文章站 2022-06-13 22:16:00
...

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

模块化有两个重要的概念:模块的导出和模块的导入

  • 模块的导出:暴露接口的过程即模块的导出

  • 模块的导入:当通过某种语法或API去使用一个模块时,这个过程叫做模块的导入

1、CommonJS

因为CommonJs是node服务提出的模块化规范,所以我们首先需要安装node环境

安装nodeJs

官网地址:https://nodejs.org/zh-cn/

安装完成后在dos窗口中输入node命令验证

使用nodeJs

在node环境中运行index.js文件

node index.js

nodejs直接运行某个js文件,该文件被称之为入口文件

为什么要有CommonJS?

在nodejs中,由于有且仅有一个入口文件(启动文件),而开发一个应用肯定会涉及到多个文件配合,因此,nodejs对模块化的需求比浏览器端要大的多。由于nodejs刚刚发布的时候,前端没有统一的、官方的模块化规范,因此,它选择使用社区提供的CommonJS作为模块化规范。

CommonJS使用

CommonJS使用exports导出模块,导出的是一个对象,require导入模块

具体规范如下:

  1. 如果一个JS文件中存在exportsrequire,该JS文件是一个模块
  2. 模块内的所有代码均为隐藏代码,包括全局变量、全局函数,这些全局的内容均不应该对全局变量造成任何污染
  3. 如果一个模块需要暴露一些API提供给外部使用,需要通过exports导出,exports是一个空的对象,你可以为该对象添加任何需要导出的内容
//utils.js
function getNumber(){
    return 1
}
exports = {
    getNumber:getNumber  //导出getNumber方法
}
//等同于
exports.getNumber = function(){
    return 1; //导出一个方法叫getNumber
 }
//等同于
function getNumber(){
    return 1
}
module.exports = {getNUmber}
  1. 如果一个模块需要导入其他模块,通过require实现,require是一个函数,传入模块的路径即可返回该模块导出的整个内容
//index.js
const util = require("./utils.js")
//等同于
const {getNumber} = require("./utils.js")
//等同于
console.log(require("./utils.js").getNumber())

注意:必须加路径./../不可省略,否则会报错!

nodejs对CommonJS的实现

为了实现CommonJS规范,nodejs对模块做出了以下处理

  1. 为了保证高效的执行,仅加载必要的模块。nodejs只有执行到require函数时才会加载并执行模块

  2. 为了隐藏模块中的代码,nodejs执行模块时,会将模块中的所有代码放置到一个函数中执行,以保证不污染全局变量。(node笔记中解释)

     (function(){
         //模块中的代码
     })()
    
  3. 为了保证顺利的导出模块内容,nodejs做了以下处理

    1. 在模块开始执行前,初始化一个值module.exports = {}
    2. module.exports即模块的导出值
    3. 为了方便开发者便捷的导出,nodejs在初始化完module.exports后,又声明了一个变量exports = module.exports
     (function(module){
         module.exports = {};
         var exports = module.exports;
         //模块中的代码
         return module.exports;
     })()
    
  4. 为了避免反复加载同一个模块,nodejs默认开启了模块缓存,如果加载的模块已经被加载过了,则会自动使用之前的导出结果

CommonJS的工作原理

当使用require(模块路径)导入一个模块时,node会做以下两件事情(不考虑模块缓存):

  1. 通过模块路径找到本机文件,并读取文件内容
  2. 将文件中的代码放入到一个函数环境中执行,并将执行后module.exports的值作为require函数的返回结果

正是这两个步骤,使得CommonJS在node端可以良好的被支持

可以认为,CommonJS是同步的,必须要等到加载完文件并执行完代码后才能继续向后执行

2、AMD

当浏览器遇到CommonJS

当想要把CommonJS放到浏览器端时,就遇到了一些挑战

  1. 浏览器要加载JS文件,需要远程从服务器读取,而网络传输的效率远远低于node环境中读取本地文件的效率。由于CommonJS是同步的,这会极大的降低运行性能
  2. 如果需要读取JS文件内容并把它放入到一个环境中执行,需要浏览器厂商的支持,可是浏览器厂商不愿意提供支持,最大的原因是CommonJS属于社区标准,并非官方标准

因为浏览器无法支持模块化。出现了AMD和CMD规范,有效的解决了浏览器模块化的问题。

AMD(Asynchronous Module Definition):异步模块加载机制

require.js实现了AMD规范

在AMD中,导入和导出模块的代码,都必须放置在define函数中

define([依赖的模块列表], function(模块名称列表){
    //模块内部的代码
    return 导出的内容
})

在AMD中,入口文件需要特殊指明,用data-amin属性标记

<body>
    <script data-main="./index.js" src="./require"></script>
</body>
AMD使用
//utils.js
define(function(){
    let data = []
    //...模块内部代码 进行一系列
    return {
        name:"导出的内容",
        data:data
    }
})
//index.js
define(['utils'], function(utils) {
    //等utils模块加载完成后执行自己的模块,参数utils是utils模块导出的内容
    return {
        name:"index模块的内容"
    }
});

如果多个文件都用到了同一个模块,模块只会读取一次,执行一次,会加入缓存中

后来,require也实现了CMD规范

//utils2.js
define(function(require, exprots,module) {
    const utils = require("./utils.js")
    console.log("utils模块代码",utils)
    module.exprots = "导出utils2模块的内容"    
});

注意:可以省略./,但最好不要省略

3、CMD

CMD(Common Module Definition):公共模块定义规范

sea.js实现了CMD规范

在CMD中,导入和导出模块的代码,都必须放置在define函数中,内部实现类似CommonJs

define(function(require, exports, module){
    //模块内部的代码
})
CMD简单使用
define(function(require, exprots,module) {
    const utils1 = require("./utils1.js")
    const utils2 = require("./utils2.js")
    module.exprots = "导出该模块的内容"    
});

通过异步方式导入模块

define(function(require, exprots,module) {
    require.async("./utils1.js",function(utils1){
        console.log(utils1)
    })
    require.async("./utils2.js",function(utils2){
        console.log(utils2)
    })
    module.exprots = "导出a模块的内容"    
});

4、ES6模块化

ECMA组织参考了众多社区模块化标准,终于在2015年,随着ES6发布了官方的模块化标准,后成为ES6模块化

模块化声明方式有两种

  1. 依赖延迟声明 ,如:commonjs
    1. 优点:某些时候可以提高效率
    2. 缺点:无法在一开始确定模块依赖关系(比较模糊)
  2. 依赖预声明 ,如: cmd,amd,es6模块
    1. 优点:在一开始可以确定模块依赖关系
    2. 缺点:某些时候效率较低

ES6模块化具有以下的特点

  1. 使用依赖预声明的方式导入模块
  2. 灵活的多种导入导出方式
  3. 规范的路径表示法:所有路径必须以./或…/开头

模块的引入

注意:这一部分非模块化标准

目前,浏览器使用以下方式引入一个ES6模块文件

<script src="入口文件" type="module">

ES6导入和导出

1、基本导入和导出

  • 基本导出:

基本导出可以有多个,每个必须有名称。export 声明表达式

// 基本导出,声明语句
export const name = "lkx" //导出name = "llx"
export function test(){
    console.log("导出函数test")
}

其他写法export {具名符号}

const age = 10
const sex = "男"
export {
    age,
    sex
}
//将age变量的名称作为导出的名称,将age的值,作为导出的值
// 等同于
export const age = 10
export const sex = "男"

也可以给导出变量起别名

const name = 10
export {
    name as name1
}

由于基本导出必须具有名称,所以要求导出内容必须跟上声明表达式具名符号

  • 基本导入:

由于使用的是依赖预加载,因此,导入任何其他模块,导入代码必须放置到所有代码之前

(不在开头也可以,浏览器预编译时进行import导入提升)不是解构赋值

对于基本导出,如果要进行导入,使用下面的代码

import {导入的符号列表} from "模块路径" 
import {name,test} from "./until.js"
console.log(name)
console.log(test())

注意以下细节:

  1. 导入时使用的符号是常量,不可修改

  2. 导入时,可以通过关键字as对导入的符号进行重命名

import {name,test as t} from "./until.js"
console.log(t())
  1. 可以使用*号导入所有的基本导出,形成一个对象
import * as obj from "./b.js"
//会把所有导出的内容形成一个对象,必须使用as重命名
  1. 多个文件导入同一个模块,会加入缓存,不会重复运行
import "./init.js"
//这个导入语句,仅运行文件,不会导入模块,如果有缓存,不运行

2、默认导入和导出

  • 默认导出

每个模块,除了允许有多个基本导出之外,还允许有一个默认导出

默认导出类似于CommonJS中的module.exports,由于只有一个,因此无需具名

具体的语法是

export default 默认导出的数据
//或
export {默认导出的数据 as default}

基本使用

export default {
    name:name,
    age:18
}
  • 默认导入

需要想要导入一个模块的默认导出,需要使用下面的语法

import 接收变量名 from "模块路径"

由于默认导入时变量名是自行定义的,因此没有别名一说

import data from "./utils.js"
console.log(data)

基本导入和默认导入混合使用:

如果希望同时导入某个模块的默认导出和基本导出,可以使用下面的语法:

import 接收默认导出的变量, {接收基本导出的变量} from "模块路径"


export const sex = "fale"
export default {
    name:name,
    age:18
}

import data,{sex} from "./utils.js"
console.log(data,sex)

注:如果使用*号,会将所有基本导出和默认导出聚合到一个对象中,默认导出会作为属性default存在

//b.js
export const sex = "fale"
export default {
    name:name,
    age:18
}

import * as data from "./b.js"
console.log(data)

注意:

  1. 尽量导出不可变值

当导出一个内容时,尽量保证该内容是不可变的(大部分情况都是如此)。因为,虽然导入后,无法更改导入内容,但是在导入的模块内部却有可能发生更改,这将导致一些无法预料的事情发生

导入的模块是常量

import { name } from "./b.js"
name = 10 //报错
  1. 可以使用无绑定的导入用于执行一些初始化代码

如果我们只是想执行模块中的一些代码,而不需要导入它的任何内容,可以使用无绑定的导入:

import "模块路径"
  1. 可以使用绑定再导出,来重新导出来自另一个模块的内容

有的时候,我们可能需要用一个模块封装多个模块,然后有选择的将多个模块的内容分别导出,可以使用下面的语法轻松完成

export {绑定的标识符} from "模块路径"