初识模块化开发 | 前端工程化

2,850 阅读8分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

关于整理的一些笔记,分别记录在专栏👉 【函数式编程】【前端工程化】【JS 基础】


模块化概念

随着业务的复杂,开发中的代码到了需要管理的程度。

  • 模块化 是一种主流的代码组成方式,将业务代码按照模块划分成不同的文件,从而提高开发效率,降低维护成本。
  • 模块化 是一种思想、理论,并不包含具体的实现。

早期的模块化方式

	graph LR
    D(早期的模块化方式) --> A(文件划分)
    D(早期的模块化方式) --> B(命名空间)
    D(早期的模块化方式) --> C(立即执行函数)

1. 文件划分的方式

文件划分的方式,主要依赖于约定。

将每个功能和数据存储在不同的文件之中,约定每一个文件就是一个独立的模块。 在需要使用的地方引入js,一个script标签就是一个模块,在引用的地方,调取全局的方法即可使用。

🎈例子:

001.png

会暴露出如下缺点:

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

2. 命名空间方式

以文件划分的方式 的基础之上,将模块包裹成 全局对象 的方式实现。

🎈例子:

002.png 优缺点:

  • 减少命名冲突的可能,但是没有私有空间,模块内的成员外部依然可以访问和修改。
  • 模块之间的依赖关系,依然没有解决。

3. IIFE(立即执行函数)

每一个模块都放到函数私有作用域当中,对于需要暴露的成员,可以通过挂载到全局对象的方式去实现。

🎈例子:

;(function () {
  var name = 'module1'
  function moduleFn() {
    console.log(name + '---> moduleFn');
  }
  window.module1 = {
    moduleFn: moduleFn
  }
})()

优缺点:

  • 实现了私有空间:引入模块后,自由成员只能在模块内部通过闭包的方式去访问,外部无法使用。
  • 使用自执行函数的参数来当做依赖声明来使用 ,使模块之间的依赖关系变得更加明显。
  • 自己动手写模块系统确实非常有意思,但实际开发中并不建议这么做,因为不够可靠。
    • 没有其他更好的动态加载依赖的方法,因此必须手动管理依赖和排序。
    • 要添加异步加载和循环依赖非常困难
    • 进行静态分析困难

补充: IIFE

IIFE,代表立即执行函数表达式(Immediately Invoked Function Expression)。

由于函数被包含在一对()括号内部,因此成为了一个表达式,通过在末尾加上另外一个()可以立即执行这个函数,比如(function foo(){ .. })()。第一个()将函数变成表达式,第二个()执行了这个函数。

函数名对IIFE当然不是必须的,IIFE最常见的用法是使用一个匿名函数表达式。虽然使用具名函数的IIFE并不常见,但它具有匿名函数表达式的所有优势,因此也是一个值得推广的实践。


模块化规范的出现

  • 为了统一不同的开发者和不同的项目之间的差异,需要一个标准来规范模块化实现的方式。
  • 针对模块化加载的问题,之前都是通过 script 标签引入的方式加载模块,这会导致模块不受代码的控制,时间久了,维护麻烦。

所以, 需要一些基础的公共代码,自动的帮加载模块。于是,就出现了模块化规范。

	graph
    D(模块化规范) --> A(CommonJS)
    D(模块化规范) --> B(AMD)
    D(模块化规范) --> C(Sea.js + CMD)
    D(模块化规范) --> E(UMD)
    D(模块化规范) --> F(ES Modules)

近年来常见的几种模块化规范

CommonJS规范

CommonJS 规范概述了同步声明依赖的模块定义。 这个规范主要用于在服务器端实现模块化代码组织,但也可用于定义在浏览器中使用的模块依赖。CommonJS模块语法不能在浏览器中直接运行。

概述:
  1. 一个文件就是一个模块
  2. 每个模块都有单独的作用域
  3. 通过 module.exports 导出成员
  4. 通过 require 函数载入模块
var moduleA = require('./moduleA');

moduleA.exports = {
  stuff: moduleA.doStuff()
}
问题

CommonJs 是以同步的模式加载模块,在 node 环境中,启动时加载模块,执行过程中不需要加载,只会使用到模块。 如果是 浏览器端 ,使用该规范会使效率低下,因为 每一次页面加载都会导致大量的同步模式请求出现 ,所以在早期前端模块开发中并不会选择 CommonJS 规范。

AMD(Asynchronous Module Definition)

概述

为了解决浏览器端出现的 CommonJS 规范的同步问题,出现了 异步模块定义(AMD, Asynchronous ModuleDefinition)。

CommonJS 以服务器端为目标环境,能够一次性把所有模块都加载到内存,而的模块定义系统则以浏览器为目标执行环境,这需要考虑网络延迟的问题。

AMD 的一般策略是让模块声明自己的依赖,而运行在浏览器中的模块系统会按需获取依赖,并在依赖加载完成后立即执行依赖它们的模块。

核心

AMD 模块实现的核心是用 函数包装模块 定义。在 AMD 中约定,使用 define() 函数定义一个模块, require() 函数加载一个模块, 内部会创建一个 script标签 ,并且执行响应的模块代码。

// ID 为 'moduleA' 的模块定义。 'moduleA' 依赖于 'moduleB'
// 'moduleB' 会异步加载
define('moduleA',['moduleB'], function(moduleB) {
  return {
    stuff: moduleB.doStuff()
  }
});
问题
  • 使用起来相对复杂
  • 模块JS文件请求频繁

Sea.js + CMD

淘宝系推出的模块化标准CMD

  • CMD 规范类似于 CommonJs 规范
  • 通过 require 引入依赖
  • 通过 exports 或者 module.exports 对外暴露成员

UMD(Universal Module Definition)

为了统一CommonJS和AMD生态系统,通用模块定义(UMD,Universal Module Definition)规范应运而生。

概述

UMD 定义的模块会在启动时检测要使用哪个模块系统,然后进行适当配置,并把所有逻辑包装在一个立即调用的函数表达式(IIFE)中。 虽然这种组合并不完美,但在很多场景下足以实现两个生态的共存。

(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD。注册为匿名模块
    define(['moduleB'], factory);
  } else if (typeof module === 'object' && module.exports) {
    // Node。不支持严格CommonJS
    // 但可以在Node这样支持module.exports的
    // 类CommonJS环境下使用
    module.exports = factory(require(' moduleB '));
  } else {
    // 浏览器全局上下文(root是window)
    root.returnExports = factory(root.moduleB);
  }
}(this, function (moduleB) {
  // 以某种方式使用moduleB
  // 将返回值作为模块的导出
  // 这个例子返回了一个对象
  // 但是模块也可以返回函数作为导出值
  return {};
}));

不应该期望手写这个包装函数,它应该由构建工具自动生成。开发者只需专注于模块的内由容,而不必关心这些样板代码。

ES Modules

ECMAScript2015(ES6) 定义了 JavaScript 中的模块规范,成为浏览器和服务端的通用解决方案

特性
  • 模块代码只在加载后执行
  • 模块只能加载一次
  • 模块是单例
  • 模块可以定义公共接口,其他模块可以基于这个公共接口观察和交互
  • 模块可以请求加载其他模块
  • 支持循环依赖
  • 模块是单例
    • 可以不用加注释声明'use strict'
  • 每个ES Module 都是运行在单独的私有作用域中,避免全局污染。
  • 模块顶级 this 的值是 undefined(常规脚本中是window)
  • 模块中的 var 声明不会添加到 window 对象
  • ES6模块是异步加载和执行的
    • ES Module 通过 CORS 的方式请求外部 JS模块,请求的地址需要支持CORS.
    • 完全支持ECMAScript 6模块的浏览器可以从顶级模块加载整个依赖图,且是异步完成的。
    • 浏览器会解析入口模块,确定依赖,并发送对依赖模块的请求。
    • 这些文件通过网络返回后,浏览器就会解析它们的内容,确定它们的依赖,如果这些二级依赖还没有加载,则会发送更多请求。
    • 这个异步递归加载过程会持续到整个应用程序的依赖图都解析完成。解析完依赖图,应用程序就可以正式加载模块了。
  • ES Modulescript标签,会延迟执行脚本, 不会阻碍页面元素的展示。

🌰 嵌入模块代码的执行顺序

  <!--  第二个执行-->
  <script type="module"></script>
  <!-- - 第三个执行-->
  <script type="module"></script>
  <!-- -- 第一个执行 -->
  <script></script>

🌰 模块导出

// 模块导出
const foo = 'foo'
export { foo }

// 命名导出 -方式1
export const foo = 'foo'

// 命名导出 -方式2
// 重复的默认导出会导致SyntaxError。
const foo = 'foo'
export { foo as myFoo }

// 默认导出
// 重复的默认导出会导致SyntaxError。
const foo = 'foo'
export default foo

🌰 模块导入

// 模块导入
import { foo }  from './fooModule.js'

// 使用 * 执行批量导入
import * as Foo  from './fooModule.js'

// 通过路径加载
// 如果不需要模块的特定导出,但仍想加载和执行模块以利用其副作用,可以只通过路径加载它
import './foo.js'

从很多方面看,ES6模块系统是集AMD和CommonJS之大成者。随着打包工具(webpack...)的流行,逐渐成为前端主流的模块化规范。


003.png

在努力看书学习中,参考如下,仰望大佬们 ~ 🙆‍♂️

参考