前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >前端模块化-CommonJS,AMD,CMD,ES6

前端模块化-CommonJS,AMD,CMD,ES6

作者头像
李振
发布2021-11-26 11:52:43
3790
发布2021-11-26 11:52:43
举报
文章被收录于专栏:乱码李乱码李

模块化解决什么问题

随着 JavaScript 工程越来越大,团队协作不可避免,为了更好地对代码进行管理和测试,模块化的概念逐渐引入前端。模块化可以降低协同开发的成本,减少代码量,同时也是“高内聚,低耦合”的基础。

模块化主要解决两个问题:

  1. 命名冲突
  2. 文件依赖:比如 bootstrap 需要引入 jquery,jquery 文件的位置必须要 bootstrap.js 之前引入。

远古时代的人们是怎样解决模块化的

在各种模块化规范出来之前,人们使用匿名闭包函数解决模块化的问题。

代码语言:javascript
复制
var num0 = 2; // 注意这里的分号
(function () {
  var num1 = 3
  var num2 = 5 
  var add = function () {
    return num0 + num1 + num2
  }
  console.log(add()) // 10
})()

// console.log(num1) // num1 is not defined

这样做的好处是,你可以在函数内部使用全局变量和局部变量,并且不用担心局部变量污染全局变量。这种用括号把匿名函数包起来的方式,也叫做立即执行函数(IIFE)。所有函数内部代码都在闭包(closure)内。它提供了整个应用生命周期的私有和状态。

CommonJS 规范

CommonJS 将每个文件都视为一个模块,在每个模块中变量默认都是私有变量,通过 module.exports 定义当前模块对外输出的接口,通过 require 加载模块。

(1) 使用方法:

circle.js

代码语言:javascript
复制
const { PI } = Math
exports.area = (r) => PI * r ** 2
exports.circumference = (r) => 2 * PI * r

app.js

代码语言:javascript
复制
const circle = require('./circle.js')
console.log(circle.area(4))

(2) 原理:node 在编译 js 文件的过程中,会使用一个如下的函数包装器将其包装模块包装器

代码语言:javascript
复制
(function (exports, require, module, __filename, __dirname) {
  const circle = require('./circle.js')
  console.log(circle.area(4))
})

这也是为什么在 node 环境中可以使用这几个没有显式定义的变量的原因。其中 __filename__dirname 在查找文件路径的过程中分析得到后传入的。module 变量是这个模块对象自身,exports 是在 module 的构造函数中初始化的一个空对象。

更详细的内容可以参考 node modules

关于什么时候使用 exports、什么时候使用 module.exports,可以参考 exports shutcut

(3) 优点 vs 缺点

CommonJS 能够避免全局命名空间污染,并且明确代码之间的依赖关系。但是 CommonJS 的模块加载是同步的,假如一个模块引用三个其它模块,那么这三个模块需要被完全加载后这个模块才能运行。这在服务端不是什么问题(node),但是在浏览器端就不是那么高效了,毕竟读取网络文件比本地文件要耗时的多。

AMD

AMD 全称异步模块化定义规范(Asynchronous Module Definition),采用异步加载模块的方式,模块的加载不影响后面语句的执行,并且使用 callback 回调函数的方式来运行模块加载完成后的代码。

(1) 使用方式

定义一个 myModule 的模块,它依赖 jQuery 模块:

代码语言:javascript
复制
define('myModule', ['jQuery'], function ($) {
  // $ 是 jQuery 的输出模块
  $('#app').text('Hello World')
})

第一个参数表示模块 id,为可选参数,第二个参数表示模块依赖,也是可选参数。

使用 myModule 模块:

代码语言:javascript
复制
require(['myModule', function (myModule) {}])

requirejs 是 AMD 规范的一个实现,详细的使用方法可以查看官方文档。

CMD

CMD 规范来源于 seajs,CMD 总体于 AMD 使用起来非常接近,AMD 与 CMD 的区别,可以查看 与 RequireJS 的异同](https://github.com/seajs/seajs/issues/277)

(1) 使用方式:

代码语言:javascript
复制
// CMD
define(function(require, exports, module) {
  var a = require('./a')
  a.doSomething()
  // ...
  var b = require('./b')
  // 依赖可以就近书写
  b.doSomething()
  // ...
})

CMD 推崇依赖就近,可以把依赖写进你的代码中的任意一行,AMD 是依赖前置的,在解析和执行当前模块之前,模块必须指明当前模块所依赖的模块。

UMD

UMD(Universal Module Definition)并不是一种规范,而是结合 AMD 和 CommonJS 的一种更为通用的 JS 模块解决方案。

在打包模块的时候经常会见到这样的写法:

代码语言:javascript
复制
output: {
  path: path.resolve(__dirname, '../dist'),
  filename: 'vue.js',
  library: 'Vue',
  libraryTarget: 'umd'
},

表示打包出来的模块为 umd 模块,既能在服务端(node)运行,又能在浏览器端运行。我们来看 vue 打包后的源码 vue.js

代码语言:javascript
复制
(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  typeof define === 'function' && define.amd ? define(factory) : 
  (global.Vue = factory());
}(this, (function () { 'use strict';
// ...
})))

代码翻译过来就是:

  1. 首先判断是否为 node 环境:exports 为一个对象,并且 module 存在。
  2. 如果是 node 环境就用 module.exports = factory() 把 vue 导出 (通过 require(‘vue’) 进行引用)。
  3. 如果不是 node 环境判断是否支持 AMD:define 为 function 并且 define.amd 存在。
  4. 如果支持 AMD 就使用 define 定义模块,(通过 require([‘vue’]) 引用)。
  5. 否则的话直接将 vue 绑定在全局变量上(通过 window.vue 引用)。

ES6

终于到了 ES6 的时代,JS 开始从语言层面支持模块化,从 node8.5 版本开始支持原生 ES 模块。不过有两点限制:

  1. 模块名(文件名)必须为 mjs
  2. 启动参数要加上 --experimental-modules

假如有 a.mjs 如下:

代码语言:javascript
复制
export default {
  name: 'Jack'	
}

在 b.mjs 中可以引用:

代码语言:javascript
复制
import a from './a.mjs'
console.log(a) // { name: 'Jack' }

chrome61 开始也支持 JS module,只需要在 script 属性中添加 type="module" 即可。

代码语言:javascript
复制
<script type="module" src="module.js"></script>
<script type="module">
  import { sayHello } from './main.js'
  sayHello()
</script>

// main.js
export function sayHello () {
  console.info('Hello World')	
}

ES6 module 详解

ES6 module 主要由两个命令组成:export 和 import。

(1) export 命令

代码语言:javascript
复制
// 输出变量
export let num = 123
export const name = 'Leo'

// 输出一组变量
let num = 123
let name = 'Leo'
export { num, name }

// 输出函数
export function foo (x, y) { return x ** y }

// 使用别名
function a () {}
function b () {}
export {
  a as name,
  b as value
}
// 引用的时候按照别名引用
import { name, value } from '..'

需要注意的是,export 命令只能对外输出接口,以下的输出方式均为错误的:

代码语言:javascript
复制
// 报错
export 1
var m = 1
export m
function f () {}
export f

// 正确的写法
export var m = 1
var m = 1
export { m }
export { m as n}
export function f () {}
function f () {}
export { f }

export 输出的值是动态绑定的,这点与 CommonJS 不同,CommonJS 输出的是值的缓存,不存在动态更新。

如何删除 node 缓存?

代码语言:javascript
复制
let config
setInterval(() => {
  delete require.cache[require.resolve('./config')]
  config = require('./config')
  console.log(config)
}, 3000)

export 命令必须处于模块顶层,如果处于块级作用域内,就会报错。

(2) import 命令

代码语言:javascript
复制
// import 的变量是只读的
import { a } from './a.js'
a = 33 // Syntax Error : 'a' is read-only

// 但是可以修改其属性值
a.name = 'Jack'

// import 命令可以提升变量
foo()
import { foo } from './a.js'

// import 是静态执行,不能使用表达式和变量
import { a + b } from './a.ls' // 报错

// 报错
let module = 'my_module'
import { foo } from module

// 报错
if (x === 1) {
  import { foo } from 'module1'
} else {
  import { foo } from 'module2'
}

// 加载整个模块
import * as utils from './utils.js'

(3) export default

代码语言:javascript
复制
// a.js
export default function () {
  console.log('Hello World')	
}

// 引用 a
import a from 'a.js'
a()

// b.js
export default funtion foo () {
  console.log('Hello World')	
}

// 引用 b
import foo from 'b.js'
foo()

export default 与 export 输出的模块在引用的时候,差别仅仅是是否用 {} 将变量包起来。

代码语言:javascript
复制
function add (x, y) {
  return x + y	
}
export { add as default }

// 引用
import { default as foo } from 'a.js'

// 正确
export var a = 1

// 正确
var a = 1;
export default a

// 错误
export default var a = 1

(4) export 与 import 复合写法

代码语言:javascript
复制
export { foo, bar } from 'a.js'
// 相当于
import { foo, bar } from 'a.js'
export { foo, bar }

参考资料

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018-04-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 模块化解决什么问题
    • 远古时代的人们是怎样解决模块化的
      • CommonJS 规范
        • AMD
          • CMD
            • UMD
              • ES6
                • ES6 module 详解
                • 参考资料
                相关产品与服务
                命令行工具
                腾讯云命令行工具 TCCLI 是管理腾讯云资源的统一工具。使用腾讯云命令行工具,您可以快速调用腾讯云 API 来管理您的腾讯云资源。此外,您还可以基于腾讯云的命令行工具来做自动化和脚本处理,以更多样的方式进行组合和重用。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档