一、模块化概述

模块化是一种思想。模块化是主流的代码组织方式,通过把复杂代码按照功能的不同划分为不同的模块,单独进行维护的方式提高开发效率,降低维护成本。

二、模块化演变过程

Stage1-文件划分方式

早期将每个功能及状态数据单独存放到不同的文件中,约定每个文件都是独立的模块,使用的时候就是将模块引入到页面中,一个script标签对应一个模块,然后再代码中直接调用全局成员(变量或函数)

产品架构中模块化架构的特点 模块化开发的应用架构_加载


引用

产品架构中模块化架构的特点 模块化开发的应用架构_加载_02

缺点:

  • 污染全局作用域
  • 命名冲突问题
  • 无法管理模块依赖关系

Stage2-命名空间方式

每个模块只暴露一个全局对象,所有的模块成员都挂载到对象下

产品架构中模块化架构的特点 模块化开发的应用架构_作用域_03

产品架构中模块化架构的特点 模块化开发的应用架构_加载_04


缺点:

  • 无私有空间
  • 模块内部成员可以被访问修改
  • 模块之间依赖关系没有被解决

Stage3-IIFE立即执行函数

产品架构中模块化架构的特点 模块化开发的应用架构_作用域_05


产品架构中模块化架构的特点 模块化开发的应用架构_加载_06

  • 使用立即执行函数包裹代码,要输出的遍历挂载到一个全局对象上
  • 变量拥有了私有空间,只有通过闭包修改和访问变量
  • 参数作为依赖声明去使用,使得每个模块的依赖关系变得明显

三、模块化规范的出现

1、CommentJs规范

NodeJs提出的一种标准,以同步模式加载模块

如果浏览器端使用commonJS,会导致效率低下,我们每次页面加载,都会导致大量请求出现,所以早期浏览器没有采用commonJS规范,而是采用AMD

  • 一个文件就是一个模块
  • 每个模块都有单独的作用域
  • 使用module.export导出成员
  • 通过require函数载入模块

CommonJS规范—是通过module.exports定义的,在前端浏览器里面并不支持module.exports,通过node.js后端使用的。Nodejs端是使用CommonJS规范的,前端浏览器一般使用AMD、CMD、ES6等定义模块化开发的

require引入

产品架构中模块化架构的特点 模块化开发的应用架构_javascript_07

2、AMD(Asynchronous Module Definition)异步模块定义规范

AMD -> Require.js
实现了这个规范,是模块加载器
定义一个模块

// 定义一个模块
define('module1', ['jquery', './module2'], function ($, module2) {
  return {
    start: function () {
      $('body').animate({ margin: '200px' })
      module2()
    }
  }
})

载入模块

require(['./module1'], function (module1) {
  module1.start()
})

目前绝大多数第三方库都支持AMD规范,但是

  • 但是AMD使用起来相对复杂
  • 模块JS文件请求频繁,会消耗性能

3、Sea.js + CMD(Common Module Definition)通用模块规范

sea.js库是淘宝推出来的

产品架构中模块化架构的特点 模块化开发的应用架构_加载_08

四、模块化标准规范

node.js -> commonjs

我们主要研究ES Modules

产品架构中模块化架构的特点 模块化开发的应用架构_加载_09

五、ES Modules特性

// 通过给script添加type=module属性,就可以使用ES Module的标准执行其中的JS代码
<script type="module">
    console.log(123)
</script>

// ./modulejs
const foo = 'es modules'
export { foo }

// ./app.js
import { foo } from './module.js'
console.log(foo) // => es modules

// export defaults
export default var name = 'es module'
import name form './module.js'
  • 自动采用严格模式,忽略’use strict’
  • 每个ESM模块都是单独的私有作用域
  • ESM是通过CORS去请求外部JS模块的
  • ESM的script标签会延迟脚本执行

ES6在语言标准的层面上,实现了模块功能,成为浏览器和服务器通用的模块解决方案

  • 每一个模块只加载一次, 每一个JS只执行一次, 如果下次再去加载同目录下同文件,直接从内存中读取;
  • 每一个模块内声明的变量都是局部变量, 不会污染全局作用域;
  • 模块内部的变量或者函数可以通过export导出;
  • 一个模块可以导入别的模块

模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能

一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取,只可以通过export关键字导出。

var name = 'jack'
var age = 18
var obj = { name, age } 
export { name, age } // export后面的双括号是固定写法

如果我们想导出一个对象 用export default,后面可以接一个变量或者一个值

var name = 'jack'
var age = 18
var obj = { name, age } 
export default { name, age }

是将值的引用关系给了外部,并不是将值拷贝了一份给外部
module.js

var name = 'jack'
var age = 18
export { name, age }
setTimeout(() => {
	name = 'ben'
},1000)

app.js

import { name, age } from './module.js' // 这里的import是固定用法,也不是结构赋值

console.log(name, age) // jack 18

name = 'tom' // 这里会报错,说明暴露出来的成员是只读的

setTimeout(() => { 
	console.log(name, age) // ben 18
},1500)

一些注意事项:

// import { name } from './module'
 import { name } from './module.js' // 不可以省略.js扩展名 不然报错
// console.log(name)

// import { lowercase } from './utils'
 import { lowercase } from './utils/index.js' // 在原生ES Module环境 index.js也不能省略
// console.log(lowercase('HHH'))

// import { name } from 'module.js'
 import { name } from './module.js'  // ./不能省略
// import { name } from '/04-import/module.js'
// import { name } from 'http://localhost:3000/04-import/module.js'
// console.log(name)

// --------------

// import {} from './module.js'
// import './module.js'

// ---------------

 import * as mod from './module.js' //如果导出的成员比较多,可以采用 *的形式
// console.log(mod)

// ---------------

// var modulePath = './module.js'
// import { name } from modulePath // 报错
// console.log(name)

// if (true) { // 不能嵌套在if中  报错
//   import { name } from './module.js'
// }

// import('./module.js').then(function (module) { // 动态导入的形式
//   console.log(module)
// })

// ----------------

// import { name, age, default as title } from './module.js' // 导出默认成员
import title, { name, age } from './module.js'// 导出默认成员
console.log(name, age, title)
//变量
let moduleName = 'hello'
//函数
let f1 = function (msg) {
    console.log(msg)
}
export var name2 = 3
export { moduleName, f1 }
import {moduleName as a,f1,name2} from './module.js'
console.log(a) //可以使用別名
f1("hello function")
setTimeout(()=>{
   console.log(a) //输出的是hi,是修改过的值
},1300)
a = '234'  //导出之后不可进行修改

使用别名

//module.js
let moduleName = 'hello'
export { moduleName }

//app.js
import {moduleName as a } from './module.js'
console.log(a) // hello

类的导出

//module.js
export class Point{
    constructor(x, y) {
        this.x = x;
        this.y = y;
      }
    
      toString() {
        return '(' + this.x + ', ' + this.y + ')';
      }
}

//app.js
import {Point} from './module.js'
let points = new Point(1,3)
console.log(points) // Point {x: 1, y: 3}

export default { }

//module.js
let num1 = 1
let num2 = 2
// default后是一个对象
export default {num1,num2}

//app.js
import module from './module.js'
console.log(module.num1)

注意事项

  1. export { } 是一个固定用法,并非导出了一个对象
  2. 导出的值是引用关系,是只读成员,不可进行修改
  3. 导入的时候必须使用完整的名称,不可省略扩展名

导入用法

  1. 导入时,可以使用绝对路径,相对路径如果是同目录必须使用‘./’,不可省略
  2. 只加载不提取,可以使用:import 'module.js'或者 import {} from 'module.js'
  3. 全部导入,使用:import * as mod from 'module.js'
  4. import 关键词只能出现在最外层的作用域
  5. 动态加载
import('./module.js').then(function(module){
    console.log(module)
})

六、ES Modules 浏览器环境Polyfill

针对IE不兼容情况

script标签使用nomodule属性,作用是兼容ES Modules的浏览器不再做加载

产品架构中模块化架构的特点 模块化开发的应用架构_作用域_10

七、ES Modules in Node.js - 与 Common.js

运行node --experimental-modules es-module.mjs

  • ESModules中可以导入CommentJs模块

产品架构中模块化架构的特点 模块化开发的应用架构_产品架构中模块化架构的特点_11

  • commentJs不可以导入ESModule模块
  • 产品架构中模块化架构的特点 模块化开发的应用架构_加载_12

  • CommentJs始终只导出一个默认成员
  • 注意 import 不是结构导出对象

新版本node 对ES Modules支持
在package.json里面增加 属性

{
	"type":"module"
}

产品架构中模块化架构的特点 模块化开发的应用架构_加载_13


这时就可以以正常的js文件去运行ES Module

产品架构中模块化架构的特点 模块化开发的应用架构_产品架构中模块化架构的特点_14


如果想使用commonJS把文件名改成.cjs

产品架构中模块化架构的特点 模块化开发的应用架构_javascript_15


ES Module中使用commonJS

产品架构中模块化架构的特点 模块化开发的应用架构_作用域_16