前端模块化的发展概述

前端模块化简单梳理

本篇简介

关于前端模块化的一些知识,如CMD/AMD/Webpack等,之前都进行过专门学习,但经验尚欠,无法从上层理解模块化处于前端工程的哪一层,所以此篇文章暂且抛开之前所学内容,不做细化研究,先单独对模块化大致脉络进行梳理,等待后续工程化、设计模式等学习完成后再进行整体的梳理。

整体脉络图

1. 无模块化时期

Web初期并没有模块化的工具,且前端在当时相对轻量,甚至后端通过模板就能完全胜任全栈工程师的角色,所以当时并没有前端工程的概念,也就没有模块化。为了组织代码,采用的是不同功能代码通过文件区分:

<!-- 不同功能代码通过文件区分,导入模板,但作用域并没分离 -->
<script src="layer.js"></script>
<script src="init.js"></script>
<script src="login.js"></script>

2. 【幼年期】语法层面模块化

由于无模块化会导致全局变量污染问题,不利于团队开发,因此 IIFE 自然成了语法层面模块化的唯一选择;其原理是函数作用域内变量若存在外部引用,则函数产生引用时的执行环境不会销毁,也就是此时函数作用域一直生效,且由于外部无法访问函数内部作用域,因此就形成了即封闭又长效的“模块”。IIFE只是将匿名函数立即执行,是对上述原理的一种语法简化,用它实现模块化的方式是返回一个暴露 api 的对象(模块):

// IIFE
var moduleA = (() => {
  var count = 0;
  return {
    printCount: function() {
      console.log(count); // 对函数作用域中的count进行引用
    },
    increase: function() {
      count++;
    }
  };
})();
// 调用模块
moduleA.printCount(); // 0
moduleA.increase();
moduleA.printCount(); // 1

面试题1:有额外依赖时,如何优化 IIFE?

由于 IIFE 原理是借助函数作用域,所以额外依赖可以通过参数传入函数中供模块使用:

// IIFE
var moduleA = ((dependB, dependC) => {
  var count = dependB.count || dependC.count || 0; // 使用依赖
  return {
    printCount: function() {
      console.log(count); // 对函数作用域中的count进行引用
    },
    increase: function() {
      count++;
    }
  };
})(moduleB, moduleC);

面试题2:了解 jQuery早期依赖处理及模块化加载方案吗?

使用了揭示模式(Revealing)写法,原理仍然是 IIFE 传参,匿名函数暴露 api 对象,api 写法是指针形式:

const moduleA = ((dep1, dep2) => {
  var count = dep1.count || dep2.count || 0;
  var getCount = function() {
    return count;
  };
  return {
    getCount, // 揭示模式写法,用指针代替具体函数
  };
})(moduleB, moduleC);

3. 【成熟期】CommonJS 的出现

Node 作为服务端语言出现后,自然少不了模块化的需求,因此出现了 CommonJS。其特性如下:

  • require 引入依赖模块
  • module、exports 对象暴露 api

模块组织方式如下:

/* NodeJS模块,基于CommonJS规范 */
// 引入依赖
const dep1 = require('./dep1.js');
// coding
let count = de1.count || 0; // 使用模块
const getCount = () => count;
// 暴露api
exports.getCount = getCount;
// 也可以直接重写module.exports
module.exports = {
  getCount,
}

优缺点如下:

  • 优点:从框架层面首次实现了真正意义的模块化
  • 缺点:为服务端设计,所以起初并未考虑异步依赖问题

面试题:上述CommonJS模块实际执行过程是?

由于对异步依赖支持不足,所以不难想到早期原理是 IIFE 的语法糖:

(function(thisValue, exports, require, module) {
  const dep1 = require('./dep1.js');
  // ...
}).call(thisValue, exports, require, module);

4. AMD规范

CommonJS 解决了模块化的问题,但又有了如何处理异步依赖的新问题,而 AMD规范应运而生。原理是 “异步加载后,执行回调函数”,经典框架是 require.js。示例如下:

/**
 * @function define函数能够定义模块,require函数加载模块
 * @params 模块名,依赖列表,模块工厂函数
 */
// 定义模块
define(id, [...dependList], factory);
// 引入模块
require([...moduleList], callback); // 加载模块,完成后执行callback

demo如下:

// 定义moduleA模块
define('moduleA', ['dep1', 'dep2'], (dep1, dep2) => {
  let count = dep1.count || dep2.count || 0;
  const getCount = () => count;
  return {
    getCount,
  };
});
// 引入并使用模块
require(['moduleA'], moduleA => {
  moduleA.getCount(); // 0
});

面试题1:若希望AMD兼容之前CommonJS代码,怎么办?

AMD引入依赖的方式除了传入列表,还给工厂函数提供了 require方法,能够兼容CommonJS的写法:

define('moduleA', [], require => {
  let dep1 = require('./dep1.js');
  let dep2 = require('./dep2.js');
  let count = dep1.count || dep2.count || 0;
  const getCount = () => count;
  return {
    getCount,
  };
});

面试题2:AMD使用 revealing 写法?

除了返回对象,也可以使用工厂函数的 export 对象挂载指针,虽然是一种设计模式,但其实写法没太大区别:

define('moduleA', [], (require, exports, module) => {
  let dep1 = require('./dep1.js');
  let dep2 = require('./dep2.js');
  let count = dep1.count || dep2.count || 0;
  const getCount = () => count;
  exports.getCount = getCount; // 直接挂载export对象
});

AMD的优缺点:

  • 优点:可在浏览器中加载模块,且支持异步、并行得加载多个模块
  • 缺点:无法按需加载

还有一种能够兼容 AMD 和 CommonJS 的规范叫 UMD,原理就是在工厂函数外包裹一层兼容函数,通过不同传参实现兼容,在这里不过多展开。

5. CMD规范

由于AMD无法按需加载,国内团队做了CMD规范进行优化,主流框架是 seaJS,示例如下:

/**
 * @function 省去依赖列表参数,依赖动态引入,与AMD区分是能按需加载
 */
define('moduleA', (require, exports, module) => {
  let $ = require('jquery');
  //...jquery相关逻辑
  let dep1 = require('./dep1');
  // ...dep1相关逻辑
});

优缺点:

  • 优点:在打包时能够实现按需加载,且依赖就近,方便维护
  • 缺点:按需加载会使打包过程变慢,且按需加载逻辑放入每个模块,模块体积反而会增大

面试题:AMD & CMD 的区别?

CMD能够按需加载

6. 【新时代】ES模块化

从 ES6 开始,实现了JS模块化的标准语法,且能实现上述旧工具的所有功能,并得到了良好支持。示例如下:

// 引入依赖
import dep1 from './dep1'

let count = 0;
function getCount() {
  return count;
}
// 导出接口
export default {
  getCount,
}

浏览器中引入模块的方法是 type=module 的script标签:

<script type="module" src="./moduleA.js"></script>

新版 Node中直接使用 ES6语法引入即可:

// 模块文件一般用 mjs 后缀
import moduleA from './moduleA.mjs'
// 使用模块
moduleA.getCount();

面试题:ES6 如何实现动态加载模块?

Webpack 支持使用 CMD写法或 import('./moduleA'),不过 ES11 已原生支持该特性:

// 原理就是包一层promise,等模块加载完就引入
import('./moduleA').then(dynamicModule => {
  dynamicModule.getCount();
});

ES6模块化的优缺点:

  • 优点:通过统一且标准的方式实现了模块化
  • 缺点:若不使用Webpack 等工程化工具,其本质仍然是运行时依赖分析

7. 【完备方法】工程化

为了解决 ES6 是运行时依赖分析的痛点,使用工程化构建工具,使依赖能够在代码构建阶段就完成。典型工具有 gruntgulpwebpack,大致原理如下:

<script>
  // 构建工具的占位符
  // require.config(__FRAME_CONFIG__);
</script>
<script>
  import A from './modA'
  define('B', () => {
    let c = require('C');
    // 业务逻辑
  });
</script>

编译时会扫描依赖关系并生成map:

{
  a: [],
  b: ['c'],
}

然后会根据依赖关系替换占位符:

<script>
  require.config({
    a: [],
    b: ['c'],
  })
</script>

接着会根据模块化工具的配置处理依赖并生成符合兼容要求的代码:

// 同步方案,加载进C
define('b', ['c'], () => {
  // ...
})

完成。

这种方案的优点:

  • 构建时分析依赖
  • 方便拓展,比如同时使用3种不同的模块化方案

总结

大概梳理一下模块化的脉络,有助于理解现在为何Webpack、Vite 等工具流行的原因,因为能解决足够多的问题,将多规范进行统一并且可以拓展。若想研究具体的规范使用,可以参考对应热门框架的实现,每一个都可以讲很多东西,在这里不过多阐述。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容