前端模块化CommonJS/AMD/CMD/ESM(完整版)

前言

模块化是前端开发和工程化的重要组成部分,也是前端的基石。在早期没有前端的概念,更多是叫做 UED(用户体验设计师)。鉴于前端功能比较简单,只需要完成与用户的基础交互即可。甚至很多站点基本都是静态页面,基本无需依赖数据完成交互,很多情况下基本由后端同学顺便写了
但随着时间的推移个人电脑的普及,底层计算机处理能力的提升,同时浏览器能力也日益强大,迫使 JavaScript 这门语言必须承担更多能力。
Web1.0 向着 Web2.0 大步迈进的时代,前端代码日趋膨胀,对于模块化的需求呼声越来越高。
所以我们也就能看到在 ESM 横空出世之前,出现了 CommonJS、AMD、CMD、UMD 这些主流的模块化设计思想,这是一个野蛮发展的时代。

温馨提示:码字不易,先赞后看,养成习惯!!!

1:什么是模块化

模块化是将代码解耦并且分割成一个个文件进行管理,将变量私有化,每个模块只对外暴露指定的 API 接口。

2:什么是模块

模块就是完成特定功能的单元。在开发场景中,一个模块就是实现特定功能的文件。

3:模块化的远古时期

3.1:函数时代

早期我们为了实现最简单的模块化,开发中大量使用到函数来实现。毕竟函数是 JavaScript 唯一的 Local Scope

function bar() { ... } 
function foo() { ... }

实际开发中被很快被过渡,但是大量的 函数 变量 被挂载在 window 上,造成命名冲突。

3.2:命名空间时代

为了解决上面的问题,出现了命名空间这样的模块化设计

var obj = {
    price1: 1,
    price2: 2,
    sum: function c() {
      return this.price1 + this.price1
    }
  }



这种做法缓解了命名冲突的问题,但是随之而来的是安全问题。既然是一个对象,那么对象里的所有属性值本质上都是对外暴露,那么就有可能被意外更改。

3.3:IIFE时代(立即执行函数)

<!DOCTYPE html>


<html>


  <head>


    <title>IIFE</title>

  </head>


  <body>


    <div class="font-show">IIFE</div>

  </body>


  // 引入自定义模块
  <script type="text/javascript" src="module.js"></script>
  // 使用
  <script type="text/javascript">
    akubelaModule.bar()
  </script>
</html>
// module.js文件

(function (w) {
  var msg1 = 'msg1'

  var msg2 = 'msg2'

  function foo() {

    console.log('foo:', msg1)

  }



  function bar() {

    foo()

    console.log('bar:', msg2)

  }
  // 对外只暴露foo与bar这两个方法
  w.akubelaModule = { foo, bar }
})(window)

利用 IIFE 基本上可以认为解决了命名冲突及安全问题,但是如果该模块需要依赖第三方模块或者自定义模块该如何处理?

3.4:IIFE增强时代

只需要将以上函数做简易的修改就可满足要求,如下:

<!DOCTYPE html>


<html>


  <head>


    <title>IIFE</title>

  </head>


  <body>


    <div class="font-show">IIFE</div>

  </body>


+ // 引入百度jQuery库
+ <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
  // 引入自定义模块
  <script type="text/javascript" src="module.js"></script>
  // 使用
  <script type="text/javascript">
    akubelaModule.bar()
  </script>
</html>
// module.js文件

(function (w, $) {
  var msg1 = 'msg1'

  var msg2 = 'msg2'

  function foo() {

    console.log('foo:', msg1)

  }



  function bar() {

    foo()

    console.log('bar:', msg2)

+   $('body').css('background', 'lightgreen')
  }
  // 对外只暴露foo与bar这两个方法
  w.akubelaModule = { foo, bar }
})(window, jQuery)

运行:

企业微信截图_16868945839918.png

上图我们能看见 msg1 与 msg2 都正常输出页面也正常渲染了,但是如果访问其他变量或函数就出现了报错或者 undefined。这就很好的解决了我们上面提到了问题。

IIFE 增强也是现代模块化的基石,缘起于此思想。

3.5:IIFE增强带来的问题

<!DOCTYPE html>


<html>


  <head>


    <title>IIFE增强</title>
  </head>


  <body>


    <div class="font-show">IIFE增强</div>
  </body>


  // 引入百度jQuery库
  <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
  <script src="http://xxx1.baidu.com/n.js"></script>
  <script src="http://xxx2.baidu.com/in.js"></script>
  <script src="http://xxx3.baidu.com/min.js"></script>
  <script src="http://xxx4.baidu.com/lnx.js"></script>
  // 引入自定义模块
  <script type="text/javascript" src="module1.js"></script>
  <script type="text/javascript" src="module2.js"></script>
  <script type="text/javascript" src="module3.js"></script>
  <script type="text/javascript" src="module4.js"></script>
  <script type="text/javascript" src="module5.js"></script>
  <script type="text/javascript" src="module6.js"></script>
  <script type="text/javascript" src="module7.js"></script>
  <script type="text/javascript" src="module8.js"></script>
  <script type="text/javascript" src="module9.js"></script>
  <script type="text/javascript" src="module10.js"></script>
  // 使用
  <script type="text/javascript">
    akubelaModule.bar()
    akubelaModule.foo()
    akubelaModule.bar1()
    akubelaModule.bar2()
    akubelaModule.bar3()
  </script>
</html>
  1. 请问这些脚本文件的顺序能调换吗?
  2. 这些脚本之间依赖关系明确吗?
  3. 脚本这么多首屏不要了?
  4. 请你重构你敢吗?

带着这些问题我们看看前人是如何解决的

4:野蛮生长时代

4.1:CommonJS

1)历史

CommonJS 项目由 Mozilla 工程师 Kevin Dangoor2009年1月 发发布了一篇 《What Server Side JavaScript needs》 ,最初名为 ServerJS
2009年8月,这个项目被改名为 CommonJS 来展示其 API 的广泛的应用性。后由 nodejs 之父 Ryan Dahl2009年11月  实现了 CommonJS。
之后 node 的应用越来越广。在 npm + node 的组合下,一统了前端的包管理模块的天下。

2)理念

每一个文件就是一个模块,有自己的作用域,文件内的所有对象都可以私有化,对外只暴露指定的 API

3)特点

1:同步加载

2:一次加载后后期再访问直接读取缓存

3:每个模块都有自己的作用域,不污染全局作用域

4)语法

// 引入
const module = require(path/moduleName)
// 暴露
module.exports = value
exports.xxx= value

5)基础实现

let fs = require('fs')
let path = require('path')
// v8虚拟机
let vm = require('vm')
function Module(p){
 // 当前模块的标识(绝对路径作为模块的key)
 this.id = p
 // 模块将挂载到exports属性上
 this.exports = {}
}


// js文件自加载的包装类,类似eval或者是settimeout
Module._wrapper = ['(function(exports,require,module){', '})']

//所有的加载策略
Module._extensions = {
 '.js': function(module){
  // 将模块用字符串拼接的形式包入一个函数中
  let fn = Module._wrapper[0] + fs.readFileSync(module.id,'utf8') + Module._wrapper[1]
  // 将fn挂载在module.exports上,同时传入参数
  vm.runInThisContext(fn).call(module.exports, module.exports, req, module)
  // 返回
  return module.exports
 },
 '.json': function(module){
  // 如果匹配到的扩展是json,直接返回该模块
  return JSON.parse(fs.readFileSync(module.id,'utf8'))
 },
 '.node': 'xxx',
}


// 缓存对象
Module._cacheModule = {}

// 输入相对路径或模块名称,返回一个绝对路径
Module._resolveFileName = function(moduleId){
 let p = path.resolve(moduleId)
 // 通过路径直接去找该文件
 try{
  // 验证权限,有就返回
  fs.accessSync(p)
  // 找到直接返回  
  return p
 }catch(e){
  console.log(e)
 }
 //对象中所有的key做成一个数组[]
 let arr = Object.keys(Module._extensions)
 // 以上找不到的时候,加后缀再找
 for(let i=0; i<arr.length; i++){
  // 拼接完整路径
  let file = p+arr[i]
  try{
   fs.accessSync(file)   
   return file
  }catch(e){
   console.log(e)
  }
 }
}

// load方法
Module.prototype.load = function (abPath) {
 // 获取扩展名
 let ext = path.extname(abPath);
 // 找对应的扩展方法,并注入该模块对象
 let content = Module._extensions[ext](this);
 return content;
}

// require方法
function req(moduleId){
 // 得到绝对路径
 let p = Module._resolveFileName(moduleId)
 // 查缓存
 if(Module._cacheModule[p]){
  // 存在返回
  return Module._cacheModule[p].exports
 }
 // 没有缓存创建一个
 let module = new Module(p)
 Module._cacheModule[p] = module
 //加载模块
 module.exports = module.load(p)
 return module.exports
}

6)基础实现

6.1)建立如下目录结构:

企业微信截图_16868965468664.png

6.2)modules.js
var aaa = 10
function fff() {
  aaa = aaa + 5
}

module.exports = {
  aaa,
  a:1,
  b:2,
  c(){
    return this.a + this.b + 3
  },
  d: {
    e:1
  },
  fff,
  f() {
    this.aaa = this.aaa + 10
  }
}
6.3)index.js
let part = req('./modules.js')
console.log('a:', part.aaa)
console.log('f:', part.fff())
console.log('a:', part.aaa)
console.log('a:', part.a)
console.log('f:', part.f())
console.log('aaa:', part.aaa)
console.log('f:', part.fff())
console.log('aaa:', part.aaa)
6.4)node执行

先用原生 require 执行,再用自己写的 req 执行对比如下:

commonjs.gif

4.2:AMD(异步模块定义)

由于 CommonJS 在服务端的出色表现,开发者就萌生了将他移植到浏览器端的想法。
然而 CommonJS 的模块是同步加载的,要在浏览器端实现势必造成严重的体验问题。所以 AMD 规范应运而生。而大名鼎鼎的 RequireJS 就是 AMD 规范的实现。我们一般说的 RequireJS 也指 AMD 规范。

AMD 推崇的是依赖前置,提前执行

1)语法

define(id?, dependencies?, factory)

  • id:指定义中模块的名字(可选)。如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字。如果提供了该参数,模块名必须是“顶级”的和绝对的(不允许相同名字)。
  • dependencies:当前模块依赖的,已被模块定义的模块标识的数组字面量(可选)。
  • factory:一个需要进行实例化的函数或者一个对象。
1.1)暴露模块
// 定义没有依赖的模块
define(function(){
 return ...
})


// 定义有依赖的模块
define(['module1', 'module2'], function(module1, module2){
 return ...
})
1.2)引用模块
require(['module1', 'module2'], function(module1, module2){
  ...
})

2)引入RequireJS

AMD 是为了在浏览器宿主环境中实现异步加载模块化方案的规范之一,由于不是 JavaScript 原生支持,使用 AMD 规范进行页面开发需要用到对应的库函数。

github下载地址

2.1)创建目录结构

image.png

2.2)module1.js
define(function () {
  var msg = 'module1'
  function show() {
    console.log('module1:', msg)
  }
  return { show }
})
2.3)module2.js
define(['module1', 'jquery'], function (module1, $) {
  module1.show()
  var msg = 'module2'
  // 暴露对象
  function showModule2() {
    console.log('module2:', msg)
    $('body').css('background', 'lightgreen')
  }
  return { showModule2 }
})

2.4)main.js
// main.js文件
(function() {
  // 配置
  require.config({
    // baseUrl: 'modules/', // 基础地址,如果配置该参数路径指向就会从根目录开始
    paths: {
      // 映射: 模块标识名: 路径
      module1: './module1',
      module2: './module2',
      jquery: './libs/jquery'
    }
  })
  require(['module2'], function(module2) {
    module2.showModule2()
  })
})()
2.5)运行

image.png

4.3:CMD(常规模块定义)

CMD 规范是由 SeaJS 实现的,同时又是 SeaJS 在推广的过程中产生 CMD 的规范。
为什么会出现 CMD 这种规范呢?主要在于设计理念与实际开发中的不同需求导致。
AMD 推崇依赖前置,提前执行,CMD 遵循的是就近依赖,延迟执行。
SeaJS 的作者是玉伯。

1)语法

1.1)定义模块
// modules1.js文件
// 无依赖定义
define(function (require, exports, module) {
  var msg = 'module1'
  function show() {
    console.log('module1:', msg)
  }



  // 单一暴露
  exports.show = show
})



// modules2.js文件
// 有依赖定义
define(function (require, exports, module) {
  var module1 = require('./module1')
  module1.show()
  // 暴露对象
  module.exports = {
    msg: 'module2'
  }
})

// modules3.js文件
// 有依赖定义
define(function (require, exports, module) {
  var module2 = require('./module2')
  console.log('module2:', module2.msg)
  var msg = 'module3'
  module.exports = msg
})


// modules4.js文件
// 有依赖定义
define(function (require, exports, module) {
  // 异步挂起(事件循环)
  require.async('./module3', function (module3) {
    console.log('异步模块3:', module3)
  })
  var module4 = 'module4'
  function show() {
    console.log('module4:', module4)
  }
  exports.show = show
})

// main.js文件
define(function (require) {
  var m4 = require('./module4')
  m4.show()
})
// html文件
<!DOCTYPE html>
<html>
  <head>
    <title>CMD</title>
  </head>
  <body>
    <div>CMD</div>
  </body>
  <script type="text/javascript" src="./modules/libs/sea.js"></script>
  <script type="text/javascript">
    seajs.use('./modules/main')
  </script>
</html>
1.2)下载引入SeaJS

目录结构

image.png

1.3)执行

image.png

4.4:ESM(ES6规范)

2015年6月TC39 发布了 ES6规范 也就是 ESM 规范,ESMJavaScript 官方的标准化模块系统,在 2017年 得到了大多数主流浏览器的支持。在 2018年5月 Firefox 60 发布之后,所有的主流浏览器就都已开始广泛支持原生 ESM,这是 ES当前提案NodeJs的 8.9 之后的版本就开始支持 ES6了,在 13.2 版本之后才开启默认支持运行 ES Modules

可见 ECMA 有着极强的号召力,在规范发布后,以上提及的各种模块化的的规范开始步入生命的倒计时。前端生态开始全面拥抱 ESM

该规范有什么优点呢?

1:官方规范、先天权威性

2:语言级别的支持

3:模块静态化

4:自带树摇buff

5:默认严格模式

6:语法简洁易理解

1)语法

1.1)export 导出
// 命名导出
export const dataType = (v) => {
  return Object.prototype.toString.call(v).slice(8, -1).toLocaleLowerCase()
}



// 导出所有
export * form './module.js'

// 默认导出
export default xxx
1.2)import 导入
// 命名导入
import { dataType } from '@/utils/base'

// 导入模块中的所有变量
import * as module from './module.js'

// 默认导入
import xxx from './zhTw.js'

// 别名导入
import { foo as bar } from './math'

// 执行模块中的代码
import './module.js'

// 动态导入
xxx: () => import('@/views/login/indexTest.vue')

5:各规范对比

CommonJS AMD CMD ESM
发布时间 2009 2011 2011 2015
作者 Mozilla-Kevin Dangoor James Burke 阿里-玉伯 ECMA
引入方式 同步 同步 同步/异步 同步/异步
使用场景 服务端 客户端/服务端 客户端/服务端 客户端/服务端
输出变量拷贝方式 值拷贝 值拷贝 值拷贝 引用拷贝
加载时机 运行时加载 运行时加载 运行时加载 编译时确定依赖,输出接口(静态化)
特点 1:同步加载
2:首次次加载后缓存,后期引入直接读取缓存
依赖前置,提前执行 就近依赖,延迟执行 1:官方规范、先天权威性
2:语言级别的支持
3:模块静态化
4:自带树摇buff
5:默认严格模式
6:语法简洁易理解

6:总结

模块化到现在前前后后经历了将近15个年头,从远古时代的 全局函数 → 命名空间 → IIFE → IIFE增强
再到野蛮生长时代, CommonJS 主攻服务端。 AMD,CMD 主攻浏览器,是开发者自己定义的一种开发模式,因为设计的好用,逐渐被推广开最终形成规范。
ESM规范没有发布之前他们承担了前端世界的绝大部分实际开发需求,但是在 ESM 规范发布后他们的历史使命也正式宣告结束。

最后历史的车轮碾碎了那个野蛮的时代,前端模块的世界也就此拉开了新的序幕。

6:推荐好文

  1. 最佳实践 monorepo + pnpm + vue3 + element-plus 0-1 完整教程
  2. Vite+rollup项目如何大幅提升性能体验
  3. 面试官系列:请说说你对深拷贝、浅拷贝的理解
  4. 面试官系列:请你说说原型、原型链相关
  5. 面试官系列:请手写防抖或节流函数debounce/throttle
  6. 面试官系类:请手写instanceof
  7. 10分钟快速手写实现:call/apply
  8. 5分钟快速手写实现:bind
  9. 5分钟快速手写实现:new
  10. 10分钟入门SVG:SVG如何画出单身狗?来本文告诉你

7:参考

  1. huangxuan.me/js-module-7…
  2. juejin.cn/post/684490…
  3. juejin.cn/post/684490…

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYSwZfYj' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片