前端指南 前端指南
指南
资源
  • 刷力扣 (opens new window)
  • 手写题 (opens new window)
  • 归档
  • 分类
  • 标签
  • 关于我
  • 关于本站
GitHub (opens new window)

Seognil LC

略懂点前端
指南
资源
  • 刷力扣 (opens new window)
  • 手写题 (opens new window)
  • 归档
  • 分类
  • 标签
  • 关于我
  • 关于本站
GitHub (opens new window)
  • 前端模块化

    • 关于模块化设计
      • 什么是模块化设计
      • 为什么要用模块化
    • 学习 使用模块化
      • 概览
      • 学习路线
    • 参考资料
      • JS 中的模块化方案
        • 代码分发
      • Learn By Doing
        • ESM (ES Module)
        • CJS (CommonJS)
        • IIFE
        • UMD
        • AMD
        • system
    • note
    • frontend
    • javascript
    Seognil LC
    2019-11-10
    目录

    前端模块化

    前端模块化

    # 关于模块化设计

    # 什么是模块化设计

    模块化设计(Modular design),是一种将系统分解为更小的“模块”的生产方式。
    这一思想广泛运用于机械制造、电子和软件工业中。

    # 代码的模块化

    常见的生产级编程语言都支持模块化,
    如 C++、Java、Python、PHP、JS 中都有 import 或 include 保留字。
    通常以单个文件作为模块的最小单元。

    代码的模块化设计一般可抽象为三个部分:

    • 输入(import)
    • 计算(业务代码)
    • 输出(export)

    # 为什么要用模块化

    • 把复杂问题分解成多个子问题
      • 关注点分离
    • 大型软件开发的技术基础
      • 可扩展
      • 可替换
      • 代码重用
    • 使多人并行开发成为可能
      • 面向接口开发(而不是面向实现开发)

    # 学习 使用模块化

    上文中提到,模块化是大型软件开发的基础,
    那么模块化的运用是必须掌握的。

    # 概览

    • 耗时:从入门到熟悉大约需要 1~4 小时(个人经验)
    • 难度:低-中
    • 难点:记住 ES6 的不同语法,CommonJS 调用原理
    • 准备:本地,npm,webpack/rollup

    # 学习路线

    • 基础
      • 理解和尝试后文中所有的模块化方案
      • 熟悉 ES6 模块化的所有用法
    • 进阶
      • 懒加载和 Dynamic Imports
      • 理解 Node.js 中的模块化机制
      • 理解 Webpack 中的模块化机制

    # 参考资料

    • 视频
      • JavaScript Modules in 100 Seconds (opens new window)
        100 秒:ESM 和 CJS 简介
      • How javascript modules work - from past to present (opens new window)
        10 分钟:JS 模块化历史,script 标签 -> AMD -> CJS -> ESM
      • Modules, import and export / Intro to JavaScript ES6 programming, lesson 13 (opens new window)
        5 分钟:模块化的起源和本质,ESM 语法简介
      • How Require and Exports work in NodeJS (opens new window)
        9 分钟:Node.js 模块化原理分析
    • 文档
      • import - MDN (opens new window):ES Module 文档
    • 文章
      • ‪Module Cheatsheet (opens new window):ESM 语法速查表
      • 前端模块化:CommonJS,AMD,CMD,ES6 (opens new window)
      • JavaScript 模块化七日谈 (opens new window)
      • 深入理解 ES6 模块机制 (opens new window)
    • 工具
      • Browserify and the Universal Module Definition (opens new window):UMD
      • 常见问题 - Rollup.js 中文网 (opens new window)
      • Webpack
        • webpack 源码学习系列之一:如何实现一个简单的 webpack (opens new window)
        • [webpack]源码解读:命令行输入 webpack 的时候都发生了什么? (opens new window)
        • 输出文件分析 - 深入浅出 Webpack (opens new window)

    # JS 中的模块化方案

    JS 的模块化经历了各种历史时期,在不同时期产生了不同的模块化方案。
    到目前为止,对于编写源码来说,主流的方案只剩下两种。

    • esm: 从 ES6 起官方规范自带的方案
    • cjs: Node.js 使用的方案

    但是为了支持不同的目标运行环境,需要编译成不同的输出格式(方案),
    了解不同的模块化方案是很有必要的。

    流行打包工具 Rollup.js (opens new window) 和 Webpack (opens new window) 都支持导出格式功能。

    以 Rollup 文档为例子,一共有以下几种:

    • cjs: CommonJS, 支援 Node.js
    • esm: 作为 ES module 文件,现代浏览器中用 <script type=module> 标签可直接支持
    • iife: 立即执行函数,可直接使用 <script> 标签。
      (如果你想打包你的前端应用,也可以用这种方式)
    • umd: Universal Module Definition,通用模块定义,
      直接封装 amd、cjs、iife 三种方式并根据环境自动切换
    • amd: Asynchronous Module Definition,异步模块定义,以 RequireJS (opens new window) 为代表
    • system: SystemJS (opens new window) 的方式

    # 代码分发

    上文提到,模块化的好处之一是具有可重用性,
    那么重用就会涉及到代码分发。

    自己写的业务代码的本地源码中,模块关系很容易理解,
    但是注意也有其他的调用方式,比如 npm 和 CDN 分发。

    相关的有一些工具和平台:

    • npm (opens new window)
    • jsDelivr (opens new window)
    • UNPKG (opens new window)
    • BootCDN (opens new window)

    # Learn By Doing

    可以直接使用 Rollup (opens new window) 来理解不同模块化方案在用法上的异同

    // ES6 源码
    import { every } from 'lodash';
    
    const result = every([true, 1, null, 'yes'], Boolean);
    
    export default result;
    
    1
    2
    3
    4
    5
    6

    # ESM (ES Module)

    rollup --format=esm --file=output.js -- index.js

    输出:

    import { every } from 'lodash';
    
    const result = every([true, 1, null, 'yes'], Boolean);
    
    export default result;
    
    1
    2
    3
    4
    5

    在模块化的语法上和源码没有区别(因为源码就是 ES6)

    完整的语法规则可以查看 MDN 文档:

    • import (opens new window)
    • export (opens new window)
    import defaultExport from "module-name";
    import * as name from "module-name";
    import { export1 , export2 } from "module-name";
    import { export1 , export2 as alias2 , [...] } from "module-name";
    import defaultExport, { export1 [ , [...] ] } from "module-name";
    // 等 ...
    
    export let name1, name2, …, nameN; // also var, const
    export { name1, name2, …, nameN };
    export default function (…) { … } // also class, function*
    export { name1, name2, …, nameN } from …;
    // 等 ...
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    # CJS (CommonJS)

    rollup --format=cjs --file=output.js -- index.js

    输出:

    'use strict';
    
    var lodash = require('lodash');
    
    const result = lodash.every(
      [true, 1, null, 'yes'],
      Boolean,
    );
    
    module.exports = result;
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    相比 esm 主要差别在语法上:

    • esm
      • import a from 'b'
      • export default c
    • cjs
      • const a = require('b')
      • module.exports = c

    在写 Node 应用时是常用的方案,
    很多优秀工具的源码中也很常见。

    需要理解的是 module.exports 的机制:

    module 和 exports 是 cjs 模块加载器设计的两个变量
    exports 是 module.exports 的简写形式

    初始状态下:

    exports === module.exports;
    module.exports === {};
    
    1
    2

    最终读取的是 module.exports
    所以需要注意进行正确地输出。
    (值传递 vs 引用传递之类的问题)

    Node.js 文档:

    • require(id) (opens new window)
    • exports (opens new window)
    • module.exports (opens new window)

    # IIFE

    rollup --format=iife --name='result' --file=output.js -- index.js

    输出:

    var result = (function (lodash) {
      'use strict';
    
      const result = lodash.every(
        [true, 1, null, 'yes'],
        Boolean,
      );
    
      return result;
    })(lodash);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    IIFE 就是 Immediately Invoked Function Expression,立即执行函数表达式,是一个常见的代码技巧。

    IIFE 原本的作用是将计算过程闭包化,防止变量污染。

    对于导出包来说,依赖视为已经准备好,直接从 window/global 取值并传入。
    以及,根据同样的方式输出,
    所以还需要指定一个变量名用于输出。
    一般用于直接从 HTML 的 script 标签加载,以便无需打包也可以运行。

    IIFE 的典型写法:

    const result = (
      (形参, ...) => {
        // ...
        return result
      }
    )(实参, ...);
    
    1
    2
    3
    4
    5
    6

    # UMD

    rollup --format=umd --name='result' --file=output.js -- index.js

    输出:

    (function (global, factory) {
      typeof exports === 'object' &&
      typeof module !== 'undefined'
        ? (module.exports = factory(require('lodash')))
        : typeof define === 'function' && define.amd
        ? define(['lodash'], factory)
        : ((global = global || self),
          (global.result = factory(global.lodash)));
    })(this, function (lodash) {
      'use strict';
    
      const result = lodash.every(
        [true, 1, null, 'yes'],
        Boolean,
      );
    
      return result;
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    由于 esm、cjs、iife 具有不同的用途,以及都有一定的使用量,
    那么对于通用代码功能来说,最好能够一次编译到处使用。
    这是 UMD 的初衷。

    UMD 支持 IIFE,所以也需要指定输出名。

    典型的 UMD 分为两个部分:

    • 模块化封装:检索当前存在的变量,判断并自动采用多种模块化方案的其中一种
      • 用 typeof 做判断
      • 三元 或 if else 做切换
      • 这个部分一般位于文件的头部或尾部
    • 业务封装:将剩余的业务逻辑代码以类似 IIFE 的方式封装调用

    不同生成工具生成的 UMD 封装在实现上可能有差异,但效果都是相似的。

    参考:

    • returnExports - UMD (opens new window)
    • jquery@3.4.1/dist/jquery.js (opens new window)
    • lodash@4.17.15/lodash.js (opens new window)
    • redux@4.0.4/dist/redux.js (opens new window)
    • vuex@3.1.1/dist/vuex.js (opens new window)

    # AMD

    rollup --format=amd --file=output.js -- index.js

    输出:

    define(['lodash'], function (lodash) {
      'use strict';
    
      const result = lodash.every(
        [true, 1, null, 'yes'],
        Boolean,
      );
    
      return result;
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    AMD 是一个早期的方案,现已式微
    (或在老项目中可见)
    (因为有了上文中其他更好的方案)

    AMD 的好处是支持异步加载,
    但是 CommonJS 也支持异步加载,
    ES6 的 Dynamic Imports (opens new window) 目前(2019 年)也在草案中(Webpack 已经通过插件支持)
    那么 AMD 已经基本没有使用的理由了。

    AMD 的典型实现是 RequireJS
    原理和语法上类似 iife + cjs 的混合

    • cjs
      • const a = require('b')
      • module.exports = c
    • amd
      • const a = require('b')
      • define( ... () => { return c })

    有两种主要写法,参考文档:Define a Module - RequireJS (opens new window)

    // * -------- case 1
    define(['./a', './b'], function (exportOfA, exportOfB) {
      // ...
    
      return c;
    });
    
    // * -------- case 2
    define(function (require) {
      const exportOfA = require('./a');
      const exportOfB = require('./b');
    
      // ...
    
      return c;
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    # system

    rollup --format=system --file=output.js -- index.js

    输出:

    System.register(['lodash'], function (exports) {
      'use strict';
      var every;
      return {
        setters: [
          function (module) {
            every = module.every;
          },
        ],
        execute: function () {
          const result = every([true, 1, null, 'yes'], Boolean);
          exports('default', result);
        },
      };
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    SystemJS (opens new window) 也是一个早期的方案

    说实话我没有用过…
    在能选择其他方案时,好像也没有使用的意义。
    (npm 下载量还没有 RequireJS 多…)
    仅作为参考吧…

    #编程语言#模块化
    上次更新: Jan 29, 2022 6:01 PM
    最近更新
    01
    Linux Shell 快速入门笔记
    11-18
    02
    我的 Web 前端开发知识体系 (2022)
    01-29
    03
    游戏环境研究笔记(2022-01)
    01-16
    更多文章>
    Theme by Vdoing | Copyright © 2019-2022 Seognil LC | MIT License
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式