没有CommonJS和ESModule之前,我们是如何做前端模块化的

我们都知道当今前端世界里,前端工程化,模块化都已经是常态化了。模块化的诞生给我们带来了极大的便利,它可以将复杂的程序规范拆分成若干模块,一个模块包括输入和输出;同时模块的内部实现是私有的,对外暴露接口与其他模块通信,还有很多好处这里就不细说了。

那在没有CommonJSAMDCMDUMDEsModule这些模块化规范之前,我们都是怎么做前端模块化的呢?下面笔者将分成四个阶段阐述。通过这四个阶段可以看出前端模块化的发展,以及没有以上模块化规范之前,我们是多么的不便利(难用!!)

一、第一阶段,全局函数

早期,我们都是将不同的功能模块拆分成不同的全局函数

//function.js
​
//前端调用后端接口
function api() {
  return {
    code: 0,
    data: {
      a: 1,
      b: 2
    }
  }
}
​
//处理后端接口
function handle(data, key) {
  return data.data[key]
}
​
//数据操作
function sum(a, b) {
  return a + b;
}
​
​
const data = api();
const a = handle(data, 'a');
const b = handle(data, 'b');
const c = sum(a, b);
​
console.log(c);
复制代码

html文件引入funciton.js

image.png

浏览器查看

image.png

这样做有一个较大的缺陷就是容易引发全局命名空间冲突,因为全局定义的函数是挂在到window的全局属性上的
image.png

如果一不小心起了一个名字和Window上属性一样的方法,就会被覆盖掉了,所以很快就引发了下面第二个阶段

二、第二阶段,namespace命名空间

既然名字容易冲突,那可以通过对象封装模块的方式,把所以函数都放在对象里面

//namespace.js
​
window.__Module = {
    //前端调用后端接口
    api() {
        return {
            code: 0,
            data: {
                a: 1,
            b: 2
            }
        } 
    },
​
    //处理后端接口
    handle(data, key) {
        return data.data[key]
    },
​
    //数据操作
    sum(a, b) {
        return a + b;
    }
}
​
const m = window.__Module;
const data = m.api();
const a = m.handle(data, 'a');
const b = m.handle(data, 'b');
const c = m.sum(a, b); 
​
console.log(c);
​
console.log(window)
复制代码

html文件引入namespace.js

image.png

浏览器查看

image.png

输出结果还是3,我们再看下window里面的属性

image.png

通过这种方式,我们起了一个很难被覆盖的名字(__Module),很大程度上降低了第一阶段说的问题,但是这种方式没有弊端吗?当然有!!!

我们可以通过外部能够修改模块内部的数据

假设__Module里面有个变量x

window.__Module = {
  x: 1,
  api() {
    //....
  },
  ....
}
​
const m = window.__Module;
console.log(m.x); //1
m.x = 2;
console.log(m.x); //2
复制代码

如果是这样,就失去了模块化私有的特征了,所以又引发了第三阶段

三、第三阶段,函数作用域 + 闭包

首先说第三阶段之前,先看看函数作用域

funciton test() {
  var a = 1;
}
​
console.log({
  test
});
复制代码

image.png

我们是根本拿不到里面的变量a的值的,因为外界无法访问,如果想要访问/修改,可以通过闭包的方式

function test() {
    var a = 1;
    return {
        set(val) {
            a = val
        },
        get() {
            return a;
        }
    }
}
​
const t = test();
console.log(t.get()); //1
t.set(2);
console.log(t.get()); //2
复制代码

所以第三阶段,是通过IIFE,就是我们常说的立即执行函数创建闭包

//iife.js
​
(function (global) {
  var x = 1;
​
  //前端调用后端接口
  function api() {
    return {
      code: 0,
      data: {
        a: 1,
        b: 2,
      },
    };
  }
​
  //处理后端接口
  function handle(data, key) {
    return data.data[key];
  }
​
  //数据操作
  function sum(a, b) {
    return a + b;
  }
​
  //获取x
  function getX() {
    return x;
  }
​
  //设置x
  function setX(val) {
    x = val;
  }
​
  global.__Module = {
    x,
    api,
    handle,
    sum,
    getX,
    setX,
  };
})(window);
​
const m = window.__Module;
const data = m.api();
const a = m.handle(data, "a");
const b = m.handle(data, "b");
const c = m.sum(a, b);
​
console.log(c);
console.log(m.x);
m.x = 2;
console.log(m.x);
复制代码

html文件引入iife.js

image.png

浏览器查看

image.png

有些同学肯定发现了,刚才不是说通过函数作用域,是无法直接通过外部修改函数内部的值吗?

为什么这里m.x = 2会生效呢?不是应该通过闭包,调用m.setX(2)才会修改x的值吗??

细心的同学应该知道了,在函数内部我把x也放到了__Module的对象中

global.__Module = {
    x,  //把x放在这里,相当于是浅拷贝,
    api,
    handle,
    sum,
    getX,
    setX,
};
复制代码

这里就关系到了函数作用域和对象属性的区别了,我们调用的m.x = 2实际修改的是对象属性,并非真正的函数内部的var x = 1的值,想要修改函数内部的值,唯一的方法就是使用闭包!!

console.log(c); //3
console.log(m.x); //1 对象属性
m.setX(2);
console.log(m.getX()); //2 函数内部的x
console.log(m.x); //1 对象属性
复制代码

虽然这个阶段看上去已经近乎完美了,但是仍然是有缺陷的,就是无法解决模块间相互依赖的问题,从而引发了阶段四

四、第四阶段,IIFE模式增强,支持传入自定义依赖

什么是模块依赖呢?简单来说就是一个模块需要用到另外一个模块的属性或者方法。还是上面的例子,我们将原来的模块进行拆分

//iffe.js
​
(function (global, api) {
  var x = 1;
​
  global.__Module = {
    x,
    api
  };
})(window, window.__Module_API);
​
const m = window.__Module;
const data = m.api.api();
const a = m.api.handle(data, "a");
const b = m.api.handle(data, "b");
const c = m.api.sum(a, b);
​
console.log(c); //3
console.log(m.x); //1
m.api.setX(2);
console.log(m.api.getX()); //2
复制代码
//iife_module.js
​
(function (global) {
  //前端调用后端接口
  function api() {
    return {
      code: 0,
      data: {
        a: 1,
        b: 2,
      },
    };
  }
​
  //处理后端接口
  function handle(data, key) {
    return data.data[key];
  }
​
  //数据操作
  function sum(a, b) {
    return a + b;
  }
​
  //获取x
  function getX() {
    return x;
  }
​
  //设置x
  function setX(val) {
    x = val;
  }
​
  global.__Module_API = {
    api,
    handle,
    sum,
    getX,
    setX,
  };
})(window);
复制代码

html文件需要引入iife.jsiife_module.js,并且需要注意引入顺序

image.png

这种方式,一定程度上支持模块的依赖,但是也存在其他的问题:

  1. 多依赖传入时,代码阅读困难
  2. 无法支持大规模的模块化开发
  3. 模块之间的引入顺序需要固定
  4. 无特定语法支持,代码简陋

五、总结

模块化的发展大致是经历了这四个阶段,在没有标准的模块化规范之前,前端想要实现模块化,可谓是举步维艰。直到它们的出现,前端工程化,打包工具,MVVM框架等才逐渐发展壮大,才有百花齐放的前端世界。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享