Jamey's Jamey's
首页
导航站
  • 学习专栏

    • 《HTML》笔记
    • 《CSS》笔记
    • 《JavaScript》笔记
    • 《Vue》笔记
    • 《Git》笔记
    • 《规范》笔记
    • 《软技能》笔记
    • 《面试》笔记
    • 《持续集成&交付&部署》笔记
  • 踩坑专栏

    • 《Element-UI 实践系列》笔记
    • 《移动端 实践系列》笔记
    • 《综合》笔记
  • 配置专栏

    • 《环境系列》笔记
  • 极空间

    • Docker
  • 影视

    • movie
  • 编辑器笔记

    • 开发编辑器
  • 浏览器笔记

    • Chrome
  • Mac笔记

    • Mac
  • 跨界学习

    • 运营
  • 破解合集

    • 破解
  • 本站

    • 分类
    • 标签
    • 归档
  • 我的

    • 收藏
    • 书单
    • 关于

Jamey

首页
导航站
  • 学习专栏

    • 《HTML》笔记
    • 《CSS》笔记
    • 《JavaScript》笔记
    • 《Vue》笔记
    • 《Git》笔记
    • 《规范》笔记
    • 《软技能》笔记
    • 《面试》笔记
    • 《持续集成&交付&部署》笔记
  • 踩坑专栏

    • 《Element-UI 实践系列》笔记
    • 《移动端 实践系列》笔记
    • 《综合》笔记
  • 配置专栏

    • 《环境系列》笔记
  • 极空间

    • Docker
  • 影视

    • movie
  • 编辑器笔记

    • 开发编辑器
  • 浏览器笔记

    • Chrome
  • Mac笔记

    • Mac
  • 跨界学习

    • 运营
  • 破解合集

    • 破解
  • 本站

    • 分类
    • 标签
    • 归档
  • 我的

    • 收藏
    • 书单
    • 关于
  • 深入系列

  • 专题系列

  • underscore系列

  • ES6系列

  • 模块化

    • 模块化
      • 一. 模块化趋势
        • 1. 痛点
        • 2. 优势
      • 二. 模块化进化史
        • 1. 全局模式
        • 2. 单例模式
        • 3. IIFE 模式
        • 4. IIFE 模式增强
      • 三. 模块化方案
        • 1. CommonJS
        • 2. AMD
        • 3. CMD
        • 4. ES6 Module
      • 四. 严格模式
      • 五. 模块化与组合化
    • 模块导入 import
    • 模块导出 export
    • 模块导入/导出的复合写法
    • 模块继承
    • 跨模块常量
    • 动态加载
  • 正则表达式

  • 单元测试

  • 微前端

  • 实用函数

  • Rollup

  • 解决方案

  • 《JavaScript》笔记
  • 模块化
Jamey
2022-03-04
目录

模块化

# 模块化

🌽 模块化:把复杂的系统分解到多个模块以方便编码

在 模块化编程 中,开发者将程序分解成离散功能块(discrete chunks of functionality),并称之为 模块。

  • 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件),并运行组合在一起。
  • 块的内部数据相对而言是私有的,只是向外部暴露一些接口与外部其他模块通信。

每个模块具有比较完整,程序更小的接触面,使得校验、调试、测试轻而易举。精心编写的模块提供了可靠的抽象和封装界限,使得应用程序中每个模块都具有条理清楚的设计和明确的目的。

# 一. 模块化趋势

# 1. 痛点

过去代码组织方式,会出现的问题:

  • 命名空间冲突。
  • 无法合理的管理项目依赖和版本。
  • 无法方便控制依赖的加载顺序。
  • 项目体积变大后难易维护。

# 2. 优势

实现模块化能实现的优势:

  • 方便代码维护。
  • 提高代码复用性。
  • 降低代码耦合度(解耦)。
  • 分治思想。

# 二. 模块化进化史

# 1. 全局模式

  • module1.js
// 数据
let data1 = 'module one data';

// 操作数据的函数
function foo() {
  console.log(`foo() ${data1}`);
}

function bar() {
  console.log(`bar() ${data1}`)
}
1
2
3
4
5
6
7
8
9
10
11
  • module2.js
let data2 = 'module two data';

function foo() {
  // 与模块1中的函数冲突了
  console.log(`foo() ${data2}`);
}
1
2
3
4
5
6
  • test.html
// 同步引入,若函数冲突,则后面覆盖前面
<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript" src="module2.js"></script>

<script type="text/javascript">
  foo(); // foo() module two data
  bar(); // bar() module one data
</script>
1
2
3
4
5
6
7
8

说明:

  • 全局模式:将不同的功能封装成不同的全局函数。
  • 问题:全局变量被污染了,很容易引起命名冲突。

# 2. 单例模式

  • module1.js
let moduleOne = {
  data: 'module one data',
  foo() {
    console.log(`foo() ${data}`);
  },
  bar() {
    console.log(`bar() ${data}`);
  }
}
1
2
3
4
5
6
7
8
9
  • module2.js
let moduleTwo = {
  data: 'module two data',
  foo() {
    console.log(`foo() ${data}`);
  },
  bar() {
    console.log(`bar() ${data}`);
  }
}
1
2
3
4
5
6
7
8
9
  • test.html
<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript" src="module2.js"></script>

<script type="text/javascript">
  moduleOne.foo(); // foo() module one data
  moduleOne.bar(); // bar() module one data

  moduleTwo.foo(); // foo() module two data
  moduleTwo.bar(); // bar() module two data

  moduleOne.data = 'update data'; // 能直接修改模块内部的数据
  moduleOne.foo(); // foo() update data
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13

说明:

  • 单例模式:简单对象封装。
  • 作用:减少了全局变量(如两个模块的 data 都不是全局变量了,而是对象的某一个属性)。
  • 问题:不安全,可以直接修改模块内部的数据。

# 3. IIFE 模式

  • module1.js
(function(window) {
  // 数据
  let data = 'IIFE module data';
  
  // 操作数据的函数
  function foo() {
    // 用于暴露的函数
    console.log(`foo() ${data}`);
  }
  
  function bar() {
    // 用于暴露的函数
    console.log(`bar() ${data}`);
    otherFun(); // 内部调用
  }
  
  function otherFun() {
    // 内部私有函数
    console.log(`privateFunction go otherFun()`);
  }
  
  // 暴露 foo 函数和 bar 函数
  window.module = { foo, bar };
})(window);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  • test.html
<script type="text/javascript"></script>

<script type="text/javascript">
  module.foo(); // foo() IIFE module data
  module.bar(); // bar() IIFE module data    privateFunction go otherFun()
  
  // module.otherFun(); // 报错,module.otherFun is not a function
  
  console.log(module.data); // undefined 因为我暴露的 module 对象中五 data
  module.data = 'xxx'; // 不是修改的模块内部的 data,而是在 module 新增 data 属性
  module.foo(); // 验证内部的 data 没有改变,还是会输出 foo() IIFE module data
</script>
1
2
3
4
5
6
7
8
9
10
11
12

说明:

  • IIFE 模式:匿名函数自调用(闭包)。
  • IIFE:Immediately-Invoked Function Expression(立即调用函数表达式)。
  • 作用:数据是私有的,外部只能通过暴露的方法操作。
  • 问题:如果当这个模块依赖另一个模块怎么办?见下面 IIFE 增强版的(模块依赖于 jQuery)。

# 4. IIFE 模式增强

引入 jQuery 到项目中

  • `module1.js
(function(window, $) {
  // 数据
  let  data = 'IIFE Strong module data';
  
  // 操作数据的函数
  function foo() {
    // 用于暴露的函数
    console.log(`foo() ${data}`);
    $('body').css('background', 'red');
  }
  
  function bar() {
    // 用于暴露的函数
    console.log(`bar() ${data}`);
    otherFun(); // 内部调用
  }
  
  function otherFun() {
    // 内部私有函数
    console.log('privateFunction go otherFun()');
  }
  
  // 暴露 foo 函数和 bar 函数
  window.moduleOne = { foo, bar };
})(window, jQuery);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  • test.html
<!--引入的js必须有一定顺序-->
<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="module1.js"></script>

<script type="text/javascript">
  moduleOne.foo(); //foo() IIFE Strong module data  而且页面背景会变色
</script>
1
2
3
4
5
6
7

说明:

  • IIFE 模式增强:引入依赖。
  • 这就是现代模块实现的基石。其实很像了,有引入和暴露两个方面。
  • 存在的问题:一个页面需要引入多个 JS 文件的问题。
<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>
1
2
3
4
  • 请求过多:一个 <script> 标签就是一次请求。
  • 依赖模糊:看不出来谁依赖着谁?哪些模块是有依赖关系的,很难看出来。
  • 难易维护:内部依赖关系混乱也就难以维护拉。

# 三. 模块化方案

# 1. CommonJS

CommonJS 是服务器端模块的规范,Node.js 就是采用了这个规范。但目前也可用于浏览器端,需要使用 Browserify 进行提前编译打包。

CommonJS 模块化的引入方式使用 require,暴露的方式使用 module.exports 或 exports。

javascript_03-04_01

特点:

  • 同步加载依赖的模块。
  • 可复用于 Node 环境。
  • 成熟的第三方模块社区。

彻底说明白 module.exports 和 exports 的区别:

在 Node.js 中,module 是一个全局变量,类似于在浏览器端的 window 也是一个全局变量一样的道理。

module.exports 初始化的时候置为空对象,exports 也指向这个空对象。

内部代码实现:

var module = {
  id: 'xxx',
  exports: {}
};

var exports = module.exports;
// exports 是对 module.exports 的引用。
// 也就是 exports 现在指向的内存地址和 module.exports 指向的内存地址是一样的。
1
2
3
4
5
6
7
8

上面的代码可以看出我们平常使用的 exports 是对 module.exports 的一个引用,两者都是指向同一个对象。

用一句话来说明就是,模块的 require(引入)能看到的只有 module.exports 这个对象,它是看不到 exports 对象的,而我们在编写模块时用到的 exports 对象实际上只是对 module.exports 的引用。

exports = module.exports;
1

我们可以使用 exports.a = 'xxx' 或 exports.b = function(){} 添加方法或属性,本质上它也添加在 module.exports 所指向的对象身上。

但是你不能直接 exports = { a: 'xxx };,这就将 exports 重新指向新的对象,它和 module.exports 就不是指向同一个对象,也就让两者失去了关系,而 Node.js 中,require 能看到的是 module.exports 指向的对象。

因此,我们一般都会直接使用:

module.exports;
1

再举例说明两者区别:

function foo() {
  console.log('foo');
}

function bar() {
  console.log('bar');
}
1
2
3
4
5
6
7

想要将这两个函数暴露出去,可以直接使用 exports。

exports.foo = foo;
exports.bar = bar;
1
2

也可以对 module.exports 赋值

module.exports = {
  foo: foo,
  bar: bar,
};
1
2
3
4

但是不能直接对 exports 赋值

// 错误
exports = {
  foo: foo,
  bar: bar,
};
1
2
3
4
5

因为这样做仅仅改变了 exports 的引用,而不改变 module.exports。

总结:

特点: 同步加载,有缓存

用法: 关键在于引入和暴露

  • 引入模块
    • require(url)(url` 为路径参数)
    • 路径:自定义模块路径必须以 ./ 或者 ../ 开头
    • 第三方模块 / 内置模块 / 核心模块:路径直接使用模块名称
  • 暴露模块
    • exports
    • module.exports

主要是在服务器端使用的,但是也能在浏览器端运行,需要借助 Browserify (opens new window) 进行编译。

# 2. AMD

CommonJS 规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。由于 NodeJS 主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,所以同步加载没有问题。
但是如果是浏览器端,同步加载很容易阻塞,这时候 AMD 规范就出来了。AMD 规范则是非同步加载模块,允许指定回调函数。故浏览器端一般会使用 AMD 规范。

AMD (opens new window) 是 require.js (opens new window) 在推广过程中对模块定义的规范化产出。

javascript_03-04_02

图3.3. AMD ▲

特点:

  • 异步加载依赖的模块
  • 可在不转换代码的情况下直接在浏览器运行
  • 并行加载多个模块
  • 可运行在浏览器和 Node 环境

用法:

  • 暴露模块
    • 在模块内部使用 return
  • 定义模块
    • define(['模块名'], function (模块暴露内容) {})
    • require(['模块名'], function (模块暴露内容) {})
  • 主模块
    • requirejs.config({})` 配置使用的模块路径
    • requirejs(['模块名'], function (模块暴露内容) {})
  • HTML 文件引入 <script> 标签
    • <script data-main='app.js' src='require.js'></script>

AMD(通用模块定义)主要是在浏览器使用的。

# 3. CMD

CMD 是根据 CommonJS 和 AMD 基础上提出的。

CMD(通用模块定义)是和 AMD(异步模块定义)是比较相似的。

require.js 遵循的是 AMD(异步模块定义)规范,sea.js (opens new window) 遵循的是 CMD(通用模块定义)规范。

javascript_03-05_01

图3.3. CMD ▲

特点:

  • 异步加载,有缓存。

用法:

  • 定义模块
    • define(function (require, exports, module) {})
  • 引入模块
    • 同步加载 require()
    • 异步加载 require.async(['模块名'], function(模块暴露内容) {})
  • 暴露模块
    • exports
    • module.exports
  • HTML 文件引入 <script> 标签
    • <script src='sea.js'></script>
    • <script>seajs.use('app.js')</script>

sea.js 和 require.js 一样主要在浏览器中使用。其实这两个一般都很少使用。用的比较多的是 commonjs 和马上要介绍的 ES6 模块化。

# 4. ES6 Module

特点:

  • 动态引入(按需加载),没有缓存。

用法:

  • 引入模块使用 import
    • 统一暴露:import {模块暴露的内容} from '模块路径'
    • 分别暴露:import * as m1 from './module1'
      • 这两者暴露的本质是对象,接收的时候只能以对象的解构赋值的方式来接收值。
    • 默认暴露:直接使用 import 模块暴露的内容 from '模块路径'。默认暴露,暴露任意数据类型,暴露什么数据类型,接收什么数据类型。
  • 暴露模块使用 export
    • 分别暴露(基本不用)
    • 统一暴露(暴露多个内容)
    • 默认暴露(暴露单个内容)

主要是在浏览器,服务器端也使用。但是现在浏览器和服务器均不支持 ES6 的模块化语法,所以要借助工具来编译运行。

  • Babel (opens new window) 将 ES6+ 转换为 ES5-(ES6 的模块化语法 编译成 commonjs)。
  • Browserify 将 CommonJS 语法编译成能让浏览器识别的语法。

# 四. 严格模式

ES6 的模块自动采用严格模式,不管你是否有在模块头部加上 use strict。

严格模式主要有以下限制:

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用 with 语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量 delete prop,会报错,只能删除属性 delete global[prop]
  • eval 不会在它的外层作用域引入变量
  • eval 和 arguments 不能被重新赋值
  • arguments 不会自动反应函数参数的变化
  • 不能使用 arguments.callee
  • 不能使用 arguments.caller
  • 禁止 this 指向全局对象
  • 不能使用 fn.caller 和 fn.arguments 获取函数调用的堆栈
  • 增加了保留字(比如 protected、static 和 interface)

其中,尤其需要注意 this 的限制。ES6 模块之中,顶层的 this 指向 undefined,即不应该在顶层代码使用 this。

# 五. 模块化与组合化

既然说到模块化,其实我更想说说模块化与组件化。这两个概念在前端设计领域已经十分普遍。

先有模块化后有组件化。组件化是建立在模块化思想上的一次演进,一个变种。所以,我们会在软件工程体系中看到过一句话:模块化是组件化的基石。

组件化和模块化的思想都是 分而治之 的思想。但还是有细小的区分,他们的侧重点有所不同。

组件化更加倾向于 UI 层面上,是一个可以独立展示内容的「积木」,比如一个页面的头部组件,包含结构 HTML、样式 CSS、逻辑 JS、以及静态资源图片组合成一个集合体。 一个页面是由众多组件组成的,就像由众多「积木」搭成的「城堡」一样;模块化更加倾向于功能或者数据的封装,一般由几个组件或单个组件构成的带有一定功能的集合体;

引用一下 @张云龙 (opens new window) 对组件化的理解:

javascript_03-05_02

图5.0. 张云龙组件化理解 ▲

就如上图的这个 title 组件,包含了结构 HTML、样式 CSS、逻辑 JavaScript、以及静态资源图片,往往组件的组成就是以上四个方面。 这个 header 文件夹我们可以拿到其他项目中使用,它具有可以独立展示内容的特点。

结合前面提到的模块化开发,整个前端项目可以划分为这么几种开发概念:

名称 说明 举例
JS 模块 单独算法和数据单元 浏览器环境监测(detect)、网络请求(ajax)、应用配置(config)、DOM 操作(dom)、工具函数(utils)、以及组件里的 JS 单元
CSS 模块 独立的功能性样式单元 栅格系统(grid)、字体图标(icon-fonts)、动画样式(animate)、以及组件里的 CSS 单元
页面 前端这种 GUI 软件的界面状态,是 UI 组件的容器 首页(index)、列表页(list)、用户管理(user)
应用 整个项目或整个站点被称之为应用,由多个页面组成

那么它们之间的关系如下图所示,一个应用由多个下图的页面组成。一个页面由多个组件组合。组件中可依赖 JS 模块。

所以,前端开发现在不仅仅是别人说的「画画页面实现点效果」的职位,它是实现软件的图形用户界面(Graphical User Interface,简称 GUI),是一名软件工程师。 现在前端开发都是基于模块化和组件化的开发,可以说算是工程化的项目了。从单页面(SPA)的应用就可以看出 JavaScript 大大改善了 Web 应用的用户体验。 从谷歌提出 PWA(Progressive Web Apps)就可以看出前端在领域的成长。

但是,如果从整个软件工程来看,我们就会意识到一个惨痛的事实:前端工程师在整个系统工程中的地位太低了。前端是处于系统软件的上游(用户入口),因此没有其他系统会来调取前端系统的服务。 而后端它在软件开发中处于下游,后端一方面要为前端提供接口服务,一方面要向中后台以及数据层索取服务,对接层次会更多,地位也就更高了。由此导致,感觉每次需要评估前端往往是最后一道坎, 因为上游依托下游,就只能是下游先行了,整体上就会感觉前端对业务的参与度太低了。

参考资料:

  • 📝 从前端模块化编程切入想聊聊前端的未来 (opens new window)
  • 📝 前端模块化开发那点历史 (opens new window)
  • 📝 JavaScript 模块化编程@阮一峰 (opens new window)
  • 📝 知乎专栏 | AMD 和 CMD 的区别 (opens new window)
#JavaScript 模块化
上次更新: 2022/07/08, 18:18:02
ES6 完全使用手册
模块导入 import

← ES6 完全使用手册 模块导入 import→

Theme by Vdoing | Copyright © 2017-2023 Jamey | blog 闽ICP备19022664号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式