Javascript模块化发展,前端的血泪史。

2023-05-21,,

前言

 
Javascript 模块的演化历史一定程度上代表了前端的发展史。从早期的 对象字面量IIFE 到后来的 commonjs, AMD 等, 再到如今的 ES Module。这些模块化方案在互联网技术发展需求下不断革新,演进。

本文从四个阶段来讲述 JS 模块化的发展历程,旨在让大家了解 JS 模块化是如何发展到今天,每个模块方案在当时又解决了什么问题。

认知革命

 

Javascript 早期诞生的目的用于客户端验证表单,提高用户体验。站在今天的解决方案角度去回顾,在那个无样式,无交互的,简单的不能再简单的 Web 页面,很难想 JS 的模块化意义在哪里。

如果非要达到一定程度代码复用,对象字面量完全可以满足 Web 互联网早期的需求。
 

//Person.js
var Person = {
    say: function(words) {
        //code
    },
    run: function() {
        //code
    }
};

Person.say('say somthing');
Person.run();

历史总会进步,互联网上 Web 页面越来越多样化,好在人们会不断的根据变化的需求调整模块化的方式。
 
当团队相互合作,去完成某一个项目时,对象字面量缺点就一览无遗。命名冲突、作用域隔离等问题就不可避免的会发生,只是一个时间早与晚的问题。
 
javascript 函数,拥有者天然的局部作用域,外界是访问不到函数内部的作用域。自然而然过渡到IIFE模块化。
 


(function(global){
    var Person = global.Person || {};
    var pritiveFn = function(){
        //other code
    };
    var pritiveName = 'Tom';
    Person.say = function(words) {
        pritiveFn();
        console.log( pritiveName + 'say: ' + words);
        //other code
    }
    Person.run = function() {
        pritiveFn();
        //other code
    }
})(window);

Person.say();
Person.run();

这种模式,能任意定义不会被外界访问到局部变量,也不会污染全局作用域,同时还能访问全局中的一些变量。通过传参命名空间,可将模块挂在到全局 Person 命名空间上。

IIEF的模块化方式,早已***到前端开发的基因。直到今天,在我们的日常开发中,都能见到或用到这种方式。

农业革命

Web2.0时代的到来,网站应用更加注重用户与服务的双向交互,前端开发也逐渐承担更多的责任。一个网站,可能有成百上千的页面,而且,javascrpt 不局限于客户端。

commonjs

推崇 commonjs 模块化规范的 Nodejs ,将模块化推向了一个新的高度。

// path/ModuleA.js
var ModuleA = function(){
    //code
}
module.exports = ModuleA;

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

// path/ModuleB.js
var ModuleB = function(){
   //code
}

module.exports = ModuleB;

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

// path/index.js
var ModuleA = require('./path/ModuleA');
var ModuleB = require('./path/ModuleB');

ModuleA();
ModuleB();

commonjs规范提供 module.exports(或者 exports)接口用于对外暴露模块。require加载模块。
仔细想想,日常开发中我们理所应当只关心模块的自由导出和加载。而加载速度、依赖顺序、作用域隔离等问题应该交给框架或者其他科学技术来系统解决,让我们无感知。
但,nodejs 毕竟是运行在服务端的 javascript。
nodejs 中每个文件具有独立的作用域,所以每个文件可认为是一个模块。除非你显示的定义在全局 global 对象上,否则其他文件是访问不到该作用域的定义的任何数据。
在 nodejs 中,一个 js 文件拥有访问其他模块(文件)能力,这就很好的解决模块间相互依赖的问题。并且所有文件都是在服务器本地加载,速度极快。
但浏览器客户端的现状是残酷的。看下面例子,如果某个页面依赖Slider, Dialog, Tab模块,而这三个模块又有一些自身的依赖。

<!-- 模块自身的依赖 -->
<script src="./util/Animation.js"></script>
<script src="./util/Mask.js"></script>

<!-- 模块依赖 -->
<script src="./Slider/index.js"></script>
<script src="./Dialog/index.js"></script>
<script src="./Tab/index.js"></script>

<script>
    Slider();
    Dialog();
    Tab();
</script>

上面的例子可以看出:

  1. 全局作用域被污染
  2. 开发人员必须手动解决模块依赖关系(顺序)。
  3. 同步远程加载过多的文件,也会造成严重的页面性能问题。
  4. 在大型,多人合作项目中,会导致整体架构混乱。
    而通过工具browserify,可将commonjs规范移植到浏览器端,本质上。browserify 是将所有被依赖commonjs的模块,打包到当前业务代码中。
    AMD

    浏览器中的 js,本身并无加载其他文件(模块)的接口。聪明的人们用动态创建 script 节点实现了动态加载模块。
    AMD, 异步模块定义,采用的是异步加载模块方式。依赖模块是异步加载,不会阻塞页面的渲染。
    AMD规范中最核心的接口是definerequire,顾名思义:定义和加载模块。
    其中以requirejs代表,是AMD规范的实现。

// 定义模块
define(['path/util/Animation'], function(Animation){
    // Slider code
    return Slider;
});

// 加载执行模块
require(['path/Slider'], function(Slider){
    Slider();
})

可以看出,接口的第一个参数,代表模块的依赖路径。模块或业务的代码,放在 callback 中,其中 callback 参数提供暴露出了各依赖模块的接口。

UMD

此时,模块规范分成了commonjsAMD两大阵营。天下大势分久必合,需要一种解决方案同时兼容这两种规范。而UMD规范的诞生就是解决该问题。

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD 规范
        define([], factory);
    } else if (typeof exports === 'object') {
        // commonjs 规范
        module.exports = factory();
    } else {
        // 挂载到全局
        root.globalVar = factory();
  }
}(this, function () {
    return {};
}));

从上面可以看出 UMD 是通过判断运行环境中是否存在各模块化的接口来实现的。

ES2015 Module

不管是 commonjs, AMD, UMD,都是毕竟是为了弥补 javascript 模块缺陷而衍生出的民间解决方案。2015年es6的发布,让javascript终于在语言层面上实现了模块化。
 

// path/ModuleA.js
var ModuleA = function(){
    //code
}
exports default ModuleA;

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

// path/ModuleB.js
var ModuleB = function(){
   //code
}

exports default ModuleB;

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

// path/index.js
import ModuleA from './path/ModuleA';
import ModuleB from './path/ModuleB';

ModuleA();
ModuleB();

commonjs 已经发展很成熟,也能满足日常需求。初略看,es module “就像是语法糖”,我们为何还要去使用它呢,换句话说,我们是用它能为我们带来哪些收益?
不管是 commonjs, AMD,他们的模块架构是 “动态架构”,换句话说,模块依赖是在程序运行时才能确定。而es module“静态架构”,也就是模块依赖在代码编译时就获取到。所以在 commonjs 里能进行 “动态引入” 模块。

if ( Math.random() > 0.5 ) {
    require('./ModuleA');
} else {
    require('./ModuleB');
}

而在 es module 中是无法进行类似操作的。从这个角度来看,es6 module 灵活性还不如 commonjs。但事物具有两面性。es6 module 其实能为我们带来以下几个收益。
tree shaking
在我们部署项目时,常常需要将各个模块文件打包成单个文件,以便浏览器一次性加载所有模块,减少 reqeust 数量。因为在 HTTP/1 中,浏览器 request 并发数量有限制。不过随之带来的问题是,多个模块打包成单文件,会造成文件 size 过大。
如果我们能在编译期时确定好模块依赖,就可以消除没有用到的模块,以便达到一定程度的优化,来看看下面例子。

// moduleA.js
export function moduleX(){
    //some code
}

export function moduleY(){
     //some code
}

// index.js
import { moduleX,  moduleY } from './moduleA';

moduleX();

通过工具 Rollup, 可将 index.js 打包成如下代码:

'use strict';

function moduleX(){
    //some code
}

moduleX();

可以看出,打包的代码只包含 moduleX,最大限度的减少了打包文件 size,这就是所谓的 'tree shaking', 读者可以好好品味下这个词,很传神。
模块变量静态检查
es6 module由于是“静态架构”,在编译时就能确定模块的依赖树以及确保模块一定是被正确的 import/export ,这就为项目质量带来很大的保障。看下面例子:

// module1.mjs
export function moduleX(){
    console.log(1);
}

// index.mjs
// 注意:module1.mjs 中并没有 export 出 moduleY
import { moduleX, moduleY } from './module1.mjs';

moduleX();

//注意 
let randomNum = Math.random();
if (randomNum) > 0.3 && randomNum < 0.4 ) {
    moduleY();
}

 

如果没有静态检查,在上面代码中的条件判断得出,代码运行期间,执行 moduleY() 函数报错的概率是10%,这种风险在线上环境就是一个非常大的隐患,一旦命中条件判断,你一整年的绩效可能就都没了。
那如果有编译期间静态检查,会是怎样的结果?
运行 node --experimental-modules index.mjs 命令时,控制台会报错:

import { moduleX, moduleY } from './module1.mjs';
                  ^^^^^^^
SyntaxError: The requested module does not provide an export named 'moduleY'
    at ModuleJob._instantiate (internal/loader/ModuleJob.js:88:21)
    at <anonymous>

这种编译时静态检查对项目的质量把控非常有用。
但 es6 module 有时候也让我很忧伤。因为它很“灵活”,所以给我带来了困扰。
来看看 import 语法:

再来看看 export 语法

额,其实我就想简单的 import/export 而已,“少即使多”
农业革命是前端史的重大进步,社区各种模块化解决方案以及事实上的标准,从另一方面也推动着 Javascript 从语言层面对模块化进行支持。这为我们架构大型项目,保证项目质量提供了机会。

三:工业革命

模块的兼容问题以及重复劳动应该交给工具去做,我们应该留出更多的时间享受”美好生活“。所以,涌现了一大批模块化工具以及周边的模块管理工具。如Browserifyr.jsWebpackRollupjspmnpmyarn等等。
各种工具极大的提高了我们的工作效率,也我们对模块化有了更多的选择。
快乐同时也带来很多的痛,就是因为可选择工具太多,配置太多,让你深陷其中无法自拔。要么忙着写bug, 要么忙着写配置。

结语

科学革命的时代,还未到来。也许到那时候,模块化的使用就像var m = 1;语法一样,它在我们脑海里本应该就是理所当然的存在,而不需借助其他编译、运行等工具来实现。

《Javascript模块化发展,前端的血泪史。.doc》

下载本文的Word格式文档,以方便收藏与打印。