北鱼扶摇

V1

2022/06/19阅读:11主题:自定义主题1

前端 JS 模块化及其工具演变

CommonJS
CommonJS

关注公众号 逻魔代码 ,及时获取更多技术干货!

作为一个脚本语言,js设计之初仅仅是为了解决基础的表单验证、页面交互功能,所有的路由系统、存储系统,硬件交互接口基本上都交由其他语言/编程接口实现, 没有命名空间的内置处理,更不存在模块化的概念。 2005年随着ajax被广泛的应用,前端开发(更准确说js)进入了一个飞速发展的阶段,展现出了成为一门非常有潜力的语言; 伴随着应用规模、业务复杂度、代码量的增长,js作为一门嵌入型语言的劣势也非常明显;

想一下这两个问题。存在即合理,先抛开这两个问题?我们来回顾一下js模块化的发展历史。

进入主题之前我们再来想一个问题,js开发的基本元素是什么? 数据类型?函数?事件循环?可能都不是,诚然对于一个JS程序处理,离开了这些都不能正常运行,但是如果我们进一步拆分,发现还可以进一步拆分为一个一个的变量, 我们进行编程开发,从根本上来说都只做了三件事:变量声名、变量赋值、变量运算。软件应用无非是对更多的变量进行组合、处理、展示。

1、混沌初开的世界

在js开发之初的年代,我们可能是这样书写代码



let count = 0;
const increase = () => ++count;
const reset = () => {
    count = 0;
    console.log("a.js Count is reset.");
};

// Use global variables.
increase();
reset();

let count = 1000;
const decrease = (count)=> --count;
const reset = () => {
    count = 1000;
    console.log('b.js count is reset.')
}

...

var module1 = {
    count: 0,
    increase: function(count){
        count = count || this.count;
        console.log("module1 Count is reset.");
    }
}
var module2 = {
    count: 1000,
    decrease: function(count){
        count = count || this.count;
        console.log("module2 Count is reset.");
    }
}

这样我们就采用命名空间的方式初步解决了变量冲突的问题,为什么是初步解决呢,因为在外部的js还是可以改变模块内部的状态

  • 优点: ① 代码有一定的逻辑组织方式;②采用命名空间避免了变量/方法命名冲突
  • 缺点: ① 外部代码仍然可以改变模块颞部的变量,存在隐患

2.2 IIFE / 闭包

2.2.1  Javascript的模块模式

既然初步聚合之后,外部调用还是可以访问修改模块内部的变量,那么那么是不是隐藏内部的局部变量就可以了? 要在函数内执行代码方法fn, 语法是fn(),要在匿名函数中执行代码(()=>{}),可以使用相同的函数调用语法(()=>{})()



const iifeCounterModule1 = (()=>{
    let count = 0;
    const increase = () => ++count;
    const reset = () => {
        count = 0;
        console.log("iifeCounterModule Count is reset.");
    };
})()
const iifeCounterModule2 = (()=>{
    let count = 1000;
    const decrease = () => --count
    const reset = ()=> {
        count = 1000;
        console.log('iifeCounterModule Count is reset.')
    }
})()
// use IIFE module
iifeCounterModule1.increase();  // 2
iifeCounterModule2.reset();     // 999

((globalVariable)=>{})(globalVariable)
  • 优点:① 代码块封装,暴露对外接口;② 导出API占位符(命名空间); ③ 模块内部变量不会被外部污染
2.2.2 导入混合

使用IIFE解决了变量污染的问题,每个模块都是一个全局变量,是不是就万事大吉了呢,假如我们其中的一个模块也需要引用第三个模块,这时又改怎么处理呢? 是的,我们需要引入一个新的概念依赖。作为依赖的模块既可以在匿名函数内部访问,同时也可以作为匿名函数参数传递。流行的前端库,基本都遵循这种模式,如大名鼎鼎的JQuery(最新版本的JQuery已经升级为UMD模式)



const iifeCounterModule = ((iifeCounterModule1,iifeCounterModule2) => {
    let count = 0 ;
    return {
        update: () => {},
        reset : ()=>{
            count = 100;
            console.log('iifeCounterModuleMix Counter is reset.')
        }
    }
})(iifeCounterModule1,iifeCounterModule2)
  • 优点:① 解决了变量命名冲突;② 模块拥有私有变量;③ 支持模块间的相互依赖
2.2.3 Reveaaling module- js显式模块模式

揭示模块本质上也是IIFE的一种,但更强调将所有API作为匿名函数内的局部变量;使用时相比普通的IIFE,其API更新加显式,调用更容易,这种模式是对IIFE、对象模式的一种组合。



const iifeCounterModule1 = (()=>{
    let count = 0;
    const increase = () => ++count;
    const reset = () => {
        count = 0;
        console.log("iifeCounterModule Count is reset.");
    };
    return {
        increse,
        reset,
    }
})()
const iifeCounterModule2 = (()=>{
    let count = 1000;
    const decrease = () => --count
    const reset = ()=> {
        count = 1000;
        console.log('iifeCounterModule Count is reset.')
    }
    return {
        decrease,
        reset,
    }
})()

// 引用 IIFE module
iifeCounterModule1.increase()  // 2
iifeCounterModule2.reset()     // 999
  • 优点:① 显式暴露对外API; ② 隐藏内部私有变量;

3、变革的年代

  • 至此解决了全局变量污染、数据变量保护的问题,js的模块化已经具备了基本的功能,可以应对一些中小规模的应用开发场景;
  • 此外有一点需要特别之处的是,作为一名开发者,必须清楚的了解依赖文件的正确顺序,而前面的方案都把依赖对外隐藏,这些依赖管理在日常开发中足以让人头疼不已;
  • 最主要的是前面js模块化的这么多探索,仍然也仅仅是局限于语言层面的,并没有形成规范,最多也只能成为一个个最佳实践,也没有从根本上解决模块间的相互依赖问题。

3.1 走出浏览器,征服世界第一步: CommonJS/CJS

随着前端模块化概念的深入普及以及js语言开发进一步发展,从语言规范化的角度,2009年Commonjs的横空出世,第一次脱离了语法层面从规范上解决了模块化的问题。那么什么是规范呢,通俗来讲,规范就是规范,是不可改变的。

按照commonjs规范,每个文件就是一个模块,有自己的作用域。换句话也就是说,在一个文件中定义的变量、函数、类、都是私有的,对其他文件是不可见的。每个模块内部会有requiremodule用于加载和暴露模块。


// Define Module counter1.js
const counter1 = {
    count: 0,
    increase: ()=> ++count,
    reset: ()=>{
        count = 0;
        console.log('counter1 module count is reset :' + count)
    } 
}
module.exports = counter1
// Define Module counter2.js
const counter2 = {
    count: 1000,
    decrease: ()=> --count,
    reset: ()=>{
        count = 1000;
        console.log('counter2 module count is reset :' + count)
    } 
}
module.exports = counter2
//  module.export
// use Module index.js
const counter1 = require('./counter1');
const counter2 = require('./counter2');
counter1.reset() // counter1 module count is reset : 0
counter2.reset() // counter2 module count is reset : 1000

在运行时,node.js通过将文件内的代码包装到一个函数中来实现模块的提供和消费功能,表现在形式上就是通过传递exports、 module变量和require函数,即:


// Define Module module3
(function(exports,require,module,__filename,__dirname)=>{
    const counter1 = require('./counter1');
    const counter2 = require('./counter2');
    let count = 0;
    const increase = ()=> ++count;
    const reset = ()=>{
        count = 300;
        console.log('commonjs module3 count is reset :' + count);
    }
    module.exports={reset, increse}
})(exports,require,module,__filename, __dirname)

// use module3 
(function(exports,require,module,__filename, __dirname)=>{
    const counter3 = require('./counter3');
    counter3.increase(); // 1
    counter3.reset() // 300
}).call(thisValue,exports,require,module,filename, dirname)

需要注意的是:

  • CommonJs属于服务端的js模块化方案,是同步加载,如nodeJs这也是为什么最初命名为Server.js的原因,
  • CommonJs模块导出的是值的拷贝,第一次加载时便执行运行的结果,此后的引用都是值的拷贝;
  • CommonJs模块定义的exports仅仅是对于module.exports变量的指向引用,不可改变其指向;

3.2 来自底层人民的呼唤:AMD -> CMD -> UMD

nodeJs因为规范化的模块方案commonjs的出现,开始了井喷式的快速增长,基于commonjs形成了稳定而又庞大的nodejs、npm社区;服务端加载文件无延时,但在浏览器端却大不相同,作为js开发的广大开发者,急切需求一种与cmmonjs类似的浏览器端模块化方案,AMD与CMD此时就应运而生。

  • AMD 即 Asynchronous Module Definition 异步模块化规范
  • CMD 即 Common Module Definition       普通模块化规范
  • UMD 即Universal Module definition     通用模块化规范
3.2.1 AMD 异步模块化规范

鉴于CommonJs的成功,js开发者(通常为客户端应用开发者)社区提出了一种适用于浏览器的异步模块化规范AMD规范,最出名的AMD实现是基于RequireJS库。提供了definerequire


// AMD module Define :
define(id, dependences,factory)
//define a module counter3
define('counter3',['counterModule1','counterModule2'],(counter1,counter2)=>{
    let count = 300;
    const increase = () => ++count;
    const reset = () => {
        count = 300;
        console.log('counterModule3 count is reset :' + count)
    }
    return {
        increase,
        reset,
    }
})
// AMD module useage: 
 require([dependence1,denpendece2,...],callback)
require(['counter3'],counter3=>{
    counter3.increase();
    counter3.reset();
})

① AMD也是用了require全局方法来加载模块,但和CommonJs完全不同,AMD需要两个参数:

  • dependences 要使用的模块名称数组;
  • callback   加载依赖模块之后的回调函数,其入参为加载的模块

② AMD提倡依赖前置,只有所需依赖全部加载之后,才会触发回调函数

③ dependence的加载通过创建script和事件监听的方式来异步加载模块具体实现可参考RequireJS 其加载部分代码如下

//  create dynamic script tag 
const loadModule = (name, url)=>{
    const head = document,getElementsByTagName('head')[0];
    const node = document.createElement('script);
    node.type = "text/javascript";
    node.async = true;
    node.setAttribute('
data-module',name);
    node.addEventListener('load', onScript,false);
    node.src = url;
    head.appendChild(node);
    retuen node
}

// bind onload envent to node
const onScript = evt => {
    const node = evt.currentTarget;
    node.removeEventListener('load',onScriptLoad, false);
    const name  = node.getAttribute('data-module'); // get Module name 
    const mod = new Module(name); // instance Module class
    const def = defMap[name]; // get cached Module and cb
    mode.init(def.deps,def.callback)
}
define(require=>{})
// CMD module define:  main.js 
define((require,exports,module)=>{
    const counter1 = require('./counterModule1');
    // do Something use counter1
    const counter2 = require('./counterModule2s');
    counter2.reset()
})

CMD use

Sea.js底层加载模块时,会将扫描被加载模块代码,匹配require关键字,提取所有的依赖项并进行加载;等到依赖的所有模块加载完成后,执行回调函数,此时再执行require代码片段时,直接提取内存中的依赖模块。 CMD规范主要从工程实践角度提出,不同于AMD的异步书写方案,其写法更接近于CommonJS的同步执行机制,在国外影响有限。

  • 优点:① 支持浏览器端环境  ② 按需加载 ③ 推崇依赖就近
  • 缺点:① 需要在index.html额外引入工具库sea.js  ② 需要手动引入模块入口文件
3.2.3 UMD 通用模块定义

现在服务端的模块化方案已经有了,浏览器的模块化方案也有了,那么是不是两者可以直接打通呢? 凭直觉,我们猜一下,肯定是不能直接使用的。那么问题又来了,如何将CommonJs与AMD/CMD规范统一成一套既可以使用服务端,又可以在浏览器端适用的规范呢?这下UMD规范就登上了历史的舞台。

UMD 模块定义

  • a、判断是否支持CommonJS: 全局变量module是否为一个对象;
  • b、判断是否支持AMD: 全局变量define 是否为对象
  • c、返回值: ① 如果支持Commonjs 采用module.exports 定义模块; ② 如果支持AMD 采用AMD规范define定义模块 ③ 如果CommonJS、AMD都不支持,采用原始全局变量方式定义模块
(function(global,factory)=>{
    typeof exports === 'object' && typeof module !== 'undefined' ?
      module.exports = factory() : 
        typeof define === 'function' && define.amd ?define(factory) : (global = global || self, global.myBunble = factory());
}(this,(function(){
    const counter1 = {
        count0,
        increase()=> ++count,
        reset()=>{
            count = 0;
            console.log('counter1 module count is reset :' + count)
        } 
    }
    return counter1
})))

优缺点: 苹果、梨 和 (苹果和梨)有什么区别? 这个问题你们还是自己想吧!

4 工业技术革命的风口  ES Module

4.1 ES6 模块化

什么?ES Module?我都天天在用了,那你倒是用一用看。

各种各样的JavaScript规范百花齐放,堪比文艺复兴、百家争鸣;但这都还没有得到来自浏览器厂商的全面统一支持;经过各式花样的模块化规范之后,2015年6月17日TC39(全称:Ecma International, Technical Committee 39 - ECMAScript)发布了ECMAScript6 正式提出了ES Module规范,及ES Module。正式从语言层面实现了JavsScript的模块化,目前各个浏览器厂商都在逐步支持。 ES Module采用import、export关键字进行模块的导入、导出。与CommonJS模块化之只拷贝引用不同的是,ES Module采用编译时加载,运行之前便确定了依赖关系,这使得ES Module可以解决循环引用问题;

  • 思考题1 输出?为什么?
//a.js
console.log('a.js')
import { msg } from './b.js';
// b.js
export let msg = 'hello'
console.log('b.js')
  • 思考题2  输出?为什么?
// a.js
import { foo } from './b.js';
console.log('a.js');
export const bar = 1;
export const bar2 = () => {
  console.log('bar2');
}
export function bar3({
  console.log('bar3');
}

export let foo = 1;
import * as a from './a.js';
console.log(a);

ES Module的加载过程分为下面三个步骤:

  • 构造(Construction):查找、下载所有文件,并解析成模块记录(module record)
  • 实例化/链接(Instantiation):把所有export的变量放入到内存中(暂时不求值),然后把相关export跟import都指向同一个内存区域
  • 求值(Evaluation):运行代码,把得到值放到指向的内存区域
ES Module Load Processing
ES Module Load Processing

// ES Module define
// counter1.js
let count = 0;
const increase = ()=> ++count;
const reset = ()=>{
    count = 0;
    console.log('ES Module Counter1 reset variable count:' + count);
}
export default {
    increase,
    reset

// ES useage1  index.js
import counter1 from './counter1.js'
let count = 300;
const increase = ()=> ++count;
const reset = ()=>{
    count = 300;
    console.log('ES Module Counter3 reset variable count:' + count);
}
increase() // 301
counter1.reset() // ES Module Counter1 reset variable count: 0

//ES usage index.html
  • ① 浏览器、服务器环境都支持;
  • ② 编译时确定模块间依赖,其他模块规范都是运行时确定的;
  • ③ 支持treeShaking、按需加载

4.2 ES动态模块

2020年,最新的Javascript规范ES11 新增加了支持动态加载模块的import关键字,返回一个Promise。

// ES Module Dynamic import
import('./counter1.js').then((counter1)=>{
    counter1.increase();
    counter1.reset()
})
// async/await 
(async ()=>{
    const {increase,reset} = await import('./counter1.js');
    incease();
    reset()
})()
  • ES6 import 支持情况
ES6-import-support
ES6-import-support
  • ES6 export 支持情况
ES6-export-support
ES6-export-support

4.3 SystemJs

截至目前一切都在朝着最好的方向发展,各种模块化规范越来越成熟,便捷、实用,但现实总是合乎情理的出人意料之外,假如我们遵循标准的ES6规范开发了,浏览器不支持怎么办???

就像买了一张高铁票,武汉到北京1200公里的距离,4个半小时就到站了,还安排晚上了和朋友的聚餐,第二天去看演唱会,上车了才被告知高铁取消了,这是一趟普快,但是给你安排了高级软卧,你心里还憧憬着明天的演唱会呢,怎么办,还是坐吧,起码还在路上,车速降到100公里/小时,起码还能到达目的地,赶上明天的演唱会。

是的,SystemJS就是为了解决这种场景,为一个老版本的ES提供一个触及ES6(ES6降级到ES3/5)的库。其本质就是根据ES Module的规范模拟一套ES降级规范,这样导入导出的ES6就消失了,旧的API调用语法肯定有效,这种转义也是现在构建工具的起源,webpack、typescript等都会自动完成。

// SystemJs Module
System.register(['counterModule1','conterModule2'], function(exports1,context1){
    'use strict';
    var counter1, counter2,count, increase,reset;
    var __moduleName = context1 && context1.id;
    return {
        setters: [
            function(counter1Dependeces){
                counter1 = counter1Dependeces;
            },
            function(counter2Dependeces){
                counter2 = counter2Dependeces;
            }
        ],
        excute:function(){
            counter1.default.api1();
            counter2.default.api2();
            count = 300 ;
            
            export1('increase', increase = function(){
                return ++count
            });
            export1('reset', reset = function(){
                count = 300;
                console.log('SystemJs module3 count is reset : ' + count)
            })
            export1('default',{increase,reset})
        }
    }
})

鉴于当前并非所有浏览器都支持ES Module因此需要构建工具转换成ES5后才能正常运行,System是其中一种方案,但更多的是采用构建工具这就引出了另外一个话题:模块打包器,且留待后续有机会再讲了。

关注公众号 逻魔代码 ,及时获取技术干货!

参考:

分类:

前端

标签:

JavaScript

作者介绍

北鱼扶摇
V1