Day 25 [模块化] 前端模块化:CommonJS,AMD,CMD,ES6

文章参考自

https://juejin.im/post/6844903744518389768

https://juejin.im/post/6844903576309858318

https://juejin.im/post/6844904003722018830

甚麽是模块化开发

模块

通常一个模块会有各自的作用域,会向外界暴露特定的变量或者函数

将一个复杂的程序,依据规范封装成一块块的文件,即是模块

好处

  1. 代码更加方便管理
  2. 提升代码复用性
  3. 避免命名冲突(减少全局污染)
  4. 更符合按须加载

最原始的模块化开发

极糟糕模块

模块就是一组特定功能的文件,以下foo 与bar组合成的module1.js 也可被称为模块

// module1.js
function foo() {
  ...
}
function bar() {
  ...
}

产生问题:

假设今天我们有好几个模块需要引入到同一个地方会发现,假设不同的模块都有一个foo函数不就出事情(这个情况其实就是污染了全局变量)

创建一个obj吧

// module1.js
let module1 = new Object({
  _count: 0,
  foo: function () {
    ...
  },
  bar: function () {
    ...
  }
})

这样只要我们调用 foo函数 就可以利用 module1.foo ,如果有其他模块也有foo就不会发生冲突。

产生问题:

可是到这里我们又发现,_count可以被任意访问,他明明就是计数器怎麽可以任意被外部改变。

立即执行函数登场

有私人的变量,外界只能透过暴露的方法获取变量

// module1.js
(function (window) {
  let _count = 0
  function foo() {
    _count += 1
  }
  
  function getCount() {
    foo()
    console.log(_count);
  }
  // 暴露给全局
  window.module1 = {
    // ES6 增强语法
    getCount,
  }
})(window)

https://ithelp.ithome.com.tw/upload/images/20210201/20124350VxQt4QgI2D.png

引入依赖

到目前为止都还不错,不过我们还忘了引入依赖(这里拿JQuery当例子)

// body部分
<body>
  <script
    src="https://code.jquery.com/jquery-3.5.1.js"
    integrity="sha256-QWo7LDvxbWT2tbbQ97B53yJnYU3WhH/C8ycbRAkjPDc="
    crossorigin="anonymous"
  ></script>
  <script src="./module1.js"></script>
</body>

js部分

// module1.js
(function (window, $) {
  let _count = 0
  function foo() {
    _count += 1
  }
  
  function getCount() {
    foo()
    console.log(_count);
  }
  function changeColor() {
    console.log(++_count);
    $('body').css('background', 'red')
  }
  window.module1 = {
    // ES6 增强语法
    getCount,
    changeColor
  }
})(window, jQuery)

https://ithelp.ithome.com.tw/upload/images/20210201/201243504KIXGXSYI0.png

看起来都不错啊为何还需要其他规范

原因有两个,导致难以维护

  1. 请求过多

    有一堆 ,而且依赖过多需要发发送过多请求

  2. 依赖模糊

    从HTML来看根本看不出来谁依赖谁

CommonJS

Node.js 为主要实践者,每个模块有各自的作用域,变量或函数都是私有,外界不可视

服务器端:模块的加载是运行时同步加载的

浏览器端:模块需要提前编译打包处理

特点:

  1. 模块可以多次加载,但只会在第一次加载运行一次,会将运行结果缓存,下次从其他地方导入会从缓存读取,如果想再一次运行模块需要清除缓存
  2. 加载的顺序,按照其在代码中出现的顺序。

基本语法

  1. 暴露:

module.exports 或是 exports

  1. 引入:

require(xxx)

第三方模块(比方说npm install xxx): xxx即是模块名子

自定义模块: xxx是路径

  1. 例子:

    CommonJS模块的加载机制是,输入的是被输出的值的浅拷贝。

    注意跟ES6 差很多

    // module1.js
    let counter = 5;
    let objCounter = {
      value: 5
    }
    
    function addCounter() {
      ++counter;
    }
    function addObjCounter(params) {
      ++objCounter.value 
    }
    module.exports = {
      counter,
      objCounter,
      addCounter,
      addObjCounter,
    };
    
    // module2.js
    // 注意这里要用node
    // 所以要再控制台输入 node module2.js (注意先cd到放module2.js的资料夹)
    
    const module1 = require('./module1');
    
    console.log(module1.counter); // 5
    console.log(module1.objCounter.value); // 5
    
    
    module1.addCounter()
    module1.addObjCounter()
    
    console.log(module1.counter); // 5
    console.log(module1.objCounter.value); // 6(因为是浅拷贝,所以会增加)
    
    

exports跟module.exports 的差别

建议使用module.exports进行导出,因为最终导出的绝对是module.exports 指向的内存地址的对象

epxorts比较像是node给你的语法糖

https://ithelp.ithome.com.tw/upload/images/20210201/20124350tsVY1gAmug.png

如果今天exports 指向新的对象

https://ithelp.ithome.com.tw/upload/images/20210201/20124350UugnLm56x8.png

导出是module.exports 指向的内存地址的对象,所以当然还是

{name: 'Mike', age: 15}

AMD

AMD (Asynchronous Module Definition),如同他的名子,处理异步加载模块

如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范

顺带一提Commonjs处理同步,因为Node.js主要用於服务端编成,模块文件储存在本地硬碟,加载快速。所以通常不会造成阻塞

基本语法

导出模块:

// 不依赖其他模块
define(function(){
   return 模块
})

// 依赖其他模块
define(['module1', 'module2'], function(m1, m2){
   return 模块
})

导入模块:

require(['module1', 'module2'], function(m1, m2){
   // 使用m1和m2
})

原始模块开发与AMD比较

  • 未使用AMD规范

目录结构
├─alert.js
├─index.html
├─main.js
└store.js
// store.js文件
(function (window) {
  let msg = '我在store.js里'

  function getMsg() {
    return msg
  }
  window.store = {
    getMsg,
  }
})(window)
// alerter.js文件
(function (window, store) {
  let addMsg = '我被alert添加了'

  function showMsg() {
    alert(store.getMsg() + ', ' + addMsg)
  }
  window.alerter = {
    showMsg
  }
})(window, store)
// main.js文件
(function (alerter) {
  alerter.showMsg()
})(alerter)

<!-- index.html -->
<body>
  <script src="./store.js"></script>
  <script src="./alert.js"></script>
  <script src="./main.js"></script>
</body>

https://ithelp.ithome.com.tw/upload/images/20210201/20124350vQ25QD90Jf.png

缺点:

  1. 会发送多个请求(一堆)
  2. 只看index.html根本看不出依赖谁
  3. 引入顺序完全不能有错
  • 使用AMD规范(这里透过require.js)

require载点

https://github.com/requirejs/requirejs/blob/master/require.js

目录结构
├─index.html
├─main.js
├─lib
|  └require.js
├─js
| ├─alerter.js
| └store.js
// store.js文件
// 定义无依赖模块
define(function () {
  let msg = '我在store.js里'

  function getMsg() {
    return msg
  }
  // 暴露模块
  return {
    getMsg
  } 
})

// alerter.js文件
// 定义有依赖模块
define([
  'store',
], function(store) {
  let addMsg = '我被alert添加了'

  function showMsg() {
    alert(store.getMsg() + ', ' + addMsg)
  }
  return {
    showMsg
  }
});

// main.js文件
(function () {
  // 配置require
  require.config({
    baseUrl: 'js/', 
    paths: {
      // 映射,标注模块名子(依赖时的数组里的名子就是这个)
      alerter: './alerter', // 不能写成alter.js会报错误
      store: './store'
        
      // 导入第三方库
      // jquery: './libs/jquery-1.10.1' //注意:写成jQuery会报错
    }
  })
  require(['alerter'], function (alerter) {
    alerter.showMsg()
  })
})()

<!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>
  <!-- 引入require.js并指定js主文件的入口 -->
  <script data-main="main" src="lib/require.js"></script>
</body>
</html>

注意事项

  1. 入口文件(main)定义在index.html
  2. 入口文件配置所有导出模块的名称(映射关系)

https://ithelp.ithome.com.tw/upload/images/20210201/20124350pKP1f9JQX3.png

CMD

整合AMD以及CommonJS

基本语法

导出模块:

// 不依赖其他模块
define(function(require, exports, module){
  exports.xxx = value
  module.exports = value
})

// 依赖其他模块
define(function(require, exports, module){
  //引入依赖模块(同步)
  var module2 = require('./module2')
  //引入依赖模块(异步)
    require.async('./module3', function (m3) {
    })
  //暴露模块
  exports.xxx = value
})

导入模块:

define(function (require) {
  var m1 = require('./module1')
  var m4 = require('./module4')
  m1.show()
  m4.show()
})

CMD例子(透过sea.js)

sea载点

https://github.com/seajs/seajs/blob/master/dist/sea.js

目录结构
├─index.html
├─result.txt
├─lib
|  └sea.js
├─js
| ├─main.js
| ├─moduleA.js
| ├─moduleB.js
| ├─moduleC.js
| └moduleD.js
// moduleA.js文件
define(function (require, exports, module) {
  console.log('我(A)被加载了')
  //内部数据
  var data = ' 我在ModuleA里面'
  //内部函数
  function show() {
    console.log('moduleA show() ' + data)
  }
  //向外暴露
  exports.show = show
  // 上面那样用跟module.exports.show依样意思
})

// moduleB.js文件
define(function (require, exports, module) {
  //内部数据
  var data = ' 我在ModuleB里面'
  //向外暴露
  console.log('我(B)被加载了')
  exports.data = data
})

// moduleC.js文件
// 这个会被异步加载
define(function (require, exports, module) {
  const TOKEN = 'abc123'
  exports.TOKEN = TOKEN
})
// moduleD.js文件
define(function (require, exports, module) {
  //引入依赖模块(同步)
  var moduleB = require('./moduleB')

  function show() {
    console.log('moduleD show() ' + moduleB.data)
  }
  exports.show = show
  // 异步引入依赖模块
  require.async('./moduleC', function (moduleC) {
    console.log('异步引入moduleC  ' + moduleC.TOKEN)
  })
})

// main.js文件
define(function (require) {
  var moduleA = require('./moduleA')
  var moduleD = require('./moduleD')
  moduleA.show()
  moduleD.show() // moduleD引入
})

<!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="lib/sea.js"></script>
  <script type="text/javascript">
    seajs.use('./js/main')
  </script>

</body>
</html>

https://ithelp.ithome.com.tw/upload/images/20210201/20124350viNtoRRqlT.png

注意事项

  1. 入口文件(main)也是定义在index.html
  2. 在各个模块透过require导入,不像AMD集体定义在main.js
  3. 跟CommonJS一样有exports跟module.exports

ES6 模块化

export

  • 第一种:
export var a = 125
export const _b = 'Mike'
export let c = 2222
  • 第二种(推荐使用这种)
var a = 125
const _b = 'Mike'
let c = 2222

export {a, _b, c}; 
  • 第三种
var a = 125
const _b = 'Mike'
let c = 2222
export {
  a as var1,
  _b as var2,
  c as var3 };

​ 注意:

export命令规定要处於模块顶层, 假如出现在块级作用域( { } ) 就会报错,import同理

  • 默认输出:
// module2.js
export default function(){
  console.log('foo')
}
// 相当于
function a(){
  console.log('foo')
}
export {a as default}; 

import可以指定任意名字

import Foo from './module2'
// 相当于
import {default as Foo} from './module2'

import

  • 第一种:
import {a, _b ,c} from './profile'
  • 第二种
import {stream1 as firstVal} from './profile'

  • 加载
import { foo } from './module1'
import { bar } from './module1'

// 相当于
import {foo,bar} from './module1'

  • 全部导入
import * as circle from './module1'
circle.foo();
circle.bar();

与CommonJS差异

  1. CommonJS模块输出的是一个值的浅拷贝,ES6模块输出的是值的引用(赋值)
  2. CommonJS模块是运行时加载,ES6模块是编译时加载

第一个差异:

// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4 (会指向lib模块的counter(内存地址))

第二个差异:

  • 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然後再从这个对像上面读取方法,这种加载称为“运行时加载”。

  • 编译时加载: ES6模块不是对象,而是通过export命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值(可能会导致变量提升),而不是加载整个模块,这种加载称为“编译时加载”。

延伸阅读

Module 的语法

Module 的加载实现


<<:  Day 24 [编程03] [译文] 如何在JavaScript 中更好地使用数组

>>:  Day 26 [其他04] ES6的Symbol竟然那么强大,面试中的加分点啊

DAY5 - Side Project 主题:90天原子习惯挑战

荀子劝学篇中有一段是这样的: 「积土成山,风雨兴焉;积水成渊,蛟龙生焉;.....。故不积蹞步,无以...

Python for回圈

今天要来教大家for回圈,for回圈在Python也是常常会用到的一种语法,有时候我们会希望让程序中...

CDB(集中式) 是什麽? DDB(分散式)是什麽?

分散资料库(Distributed Database, DDB) VS 集中式资料库(Central...

Day 14 照片铅笔素描效果

照片铅笔素描效果 教学原文参考:照片铅笔素描效果 这篇文章会介绍使用 GIMP 图层的混合模式,搭配...

Day2 理解 golang slice 用法及原理 II

续上篇 Day1 理解 golang slice 用法及原理 I 什麽是 slice 是的容量 (c...