模块化发展都经历了什么

949 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第21天,点击查看活动详情

  • Hello,这里是mouche,当然你也可以叫我某车,反正大家都爱这么叫😁
  • 要了解vite的必须先了解ESM,要了解ESM就可以顺便来了解一下模块化的前世今生,于是就有了这一篇

一、无模块化标准的阶段

  • 在模块化标准还没有产生之前,已经有一些模块化的开发手段了

1、文件划分

  • 简单来说就是将应用的状态和逻辑分散到不同的文件中,然后通过 HTML 中的 script 来一一引入
  • 最简单的当然也风险最多
    • 变量冲突:首先模块中的变量变量相当于在全局声明和定义,那么就会有变量冲突问题,比如我先在module-a定义变量c,再在module-b定义变量c,那么就会报错 image.png
    • 依赖管理:无法清晰地管理模块之间的依赖关系和加载顺序,如果有文件依赖于另外的文件的话,那么就需要手动调整script引入的顺序(因为执行的时候是自上而下执行),比如我在module-b定义变量b,在module-a打印b,就相等于module-a是依赖于module-b
      • 如果引入顺序如下,那么就报错b没有定义 image.png image.png
      • 但是我们引入顺序给他换个位置,就能够成功打印啦。但是在比较大的项目中,每次有依赖就去调整位置无疑太繁琐了而且难以管理
        image.png
    • 调试困难: 因为变量都在全局定义的,所以就很难知道哪个变量是属于哪些模块的,因此调试是比较困难的

2、命名空间

  • 可以解决上述文件划分方式中全局变量定义所带来的一系列问题,其实就是将变量隔离开来
  • 像上述一样,在module-a文件和module-b文件同时定义了c,但是我使用了window.awindow.b将他们包裹起来,c就并非全局变量了,自然也不会冲突;同时我们也能够清楚地知道变量是来自哪个模块的
  • 但是内部属性全部会暴露出来,内部状态可以被外部更改,所以它的数据是不安全
//module-a
window.a = {
  c:'I am module-a',
  method: function () {
    console.log(this.c)
  }
}
//module-b
window.b = {
  c:'I am module-b',
  method: function() {
    console.log(this.c)
  }
}
//index.html
<script src="./module-a.js"></script>
<script src="./module-b.js"></script>
  <script>
    window.a.method();
    window.b.method();
  </script>

3、IIFE

  • IIFEImmediately Invoked Function Expression,翻译过来就是立即执行的函数表示式
  • 尽管命名空间模式在一定程度上解决了全局命名变量污染带来的问题,但是没有办法解决代码和数据隔离的问题,所以IIFE又出现了,利用函数闭包的特性来实现私有数据共享方法,是相对于命名空间来说更安全的方法
  • 每个IIFE 即立即执行函数都会创建一个私有的作用域,在私有作用域中的变量外界是无法访问的,只有模块内部的方法才能访问,避免模块私有成员被其他模块非法篡改
//module-a.js
const moduleA = (function(){
  let name = 'module-a';
  function getName() {
    return name
  }
  return { getName }
})()
//index.html
<script src="./module-a.js"></script>
<script>
    console.log(moduleA.getName()); //可以通过共享方法获取到数据
    console.log(moduleA.name);//但是不能直接获取私有数据
  </script>
  • 说了这么多私有私有私有,那么如果模块之间有依赖关系如何实现,这个时候别忘了IIFE本质上是一个函数,既然是函数,那么就有传参功能,我们只需要把需要使用的变量传入即可
    • 这里我们在module-b需要用的moduleAname,那么将moduleA作为参数传入即可
    //module-b.js
    const moduleB = (function(moduleA){
      let  name = 'module-b';
      function getName() {
        console.log('我先获取我自己的name:',name);
        console.log('还不够,我还要获取moduleA的:', moduleA.getName());
      }
      return {getName }
    })(moduleA)
    //index.html
      <script src="./module-a.js"></script>
      <script src="./module-b.js"></script>
      <script>
        moduleB.getName();
      </script>
    
    • 打印结果 image.png

一共讲了三种,但是你会发现后面两种方法虽然在一定程度上解决了全局变量污染问题,但是并没有我们解决我们在文件划分时所说的依赖管理问题,当文件有依赖关系时,依旧需要严格地去控制模块加载顺序,否则会报错,同时也很容易出现牵一发而动全身的情况

随着前端工程的日益庞大,社区中逐渐出现了一些优秀且被大多数人认同的模块化解决方案,如CommonJS,AMD,CMD,ESM,一起来了解了解叭

二、CommonJS

CommonJS 是业界最早正式提出的 JavaScript 模块规范,主要用于服务端,随着 Node.js 越来越普及,这个规范也被业界广泛应用

  • CommonJS每一个 js 文件都是一个单独的模块,我们可以称之为 module
  • 该模块中,包含CommonJS规范的核心变量: exportsmodule.exportsrequire
    • exportsmodule.exports 可以负责对模块中的内容进行导出(不推荐直接用exports)
    • require 函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容
function wrapper (script) {
    return '(function (exports, require, module, __filename, __dirname) {' + 
        script +
     '\n})'
}

对于CommonJS而言,它定义了一套完整的模块化代码规范,所有代码都运行在模块作用域,不会污染全局变量,同时有缓存机制,看上去是一个很不错的模块规范,但是他的模块加载由Node.js提供,这就会导致如果CommonJS直接放到浏览器是执行不了的,当然社区也产生了rowserify这种打包工具来打包CommonJS模块,相当于是一个第三方的loader

但是最凸显的问题就是,CommonJS他是同步进行模块加载,这种机制放在服务器没有问题,因为模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题;但是放在浏览器会产生大量的同步的模块请求,大量请求一听就是很可怕的事哈哈,浏览器需要等到响应后才能进行解析,所以这种情况下, 模块加载一定程度上会造成浏览器解析的阻塞,用户体验降低

总而言之,CommonJS是同步加载的,我们更希望有一个异步加载方案的规范,所以AMD和CMD就来了

三、AMD

  • 刚说到CommonJS同步问题,那么AMD(异步模块定义规范)就是专门为浏览器设置的一套异步加载方案,是 RequireJS 在推广过程中对模块定义的规范化产出
  • AMD也仍需第三方的loader来实现
  • AMD有两个API,define用于定义模块,require用于调用模块
  • 对于define而言

    define(id?: String,dependencies?: String[], factory: Function|Object

    • id: 默认是文件名,一般不传
    • dependencies: 指定了要依赖的模块列表,默认为['require','exports','module'],是一个数组,每个依赖的模块的输出将作为参数传入facotry,在模块的代码执行之前会浏览器会先加载依赖模块
    • factory:模块的具体实现
    • 可以分成两种情况
      • 独立的模块
      //show.js
      define(function() {
          return {
           showMsg: function(msg) {
           console.log('我打印出来了', msg) 
           }
          }
      })
      
      • 定义的模块需要依赖其他模块
      //依赖了`show`文件的`showMsg`方法
      define(['./show'], function(showModule){
        showModule.showMsg('我传入了这个作为msg');
      }) 
      
  • 对于require而言require只能加载模块,不能定义模块

    require([module],factory)

    • module就是需要加载的模块,
    require(['./show.js'], function(showModule) {
      showModule.show('我传入了这个作为msg');
    }) 
    
  • 看上述使用,你能感受得到它的使用其实是会比较麻烦的,同时阅读起来体验感也不是很好

这里跳过了CMD,CMD是由淘宝出品的SeaJS实现的,和AMD是同期产物,我觉得区别不大,都是专门用于浏览器的异步模块加载,不过AMD 推崇依赖前置CMD推崇依赖就近

四、UMD

  • UMD(Universal Module Definition),即通用模块定义
  • 看名字就知道,它就展示出一种我啥都要的姿态,它是兼容 AMD, CMDCommonJS 的一个模块化方案,可以同时运行在浏览器和 Node.js 环境, 更像是一种配置多个模块系统的模式

五、ES Moudle

  • 可以说是最重点的来了,ESM是由ECMAScript推出来的官方的模块化规范,看到是官方应该就能知道它有多重要了,至少在目前来说是最好的选择,也是未来的发展趋势
  • 在现代浏览器中,如果在 HTML 中加入含有type="module"属性的 script 标签,那么浏览器会按照 ES Module 规范来进行依赖加载和模块解析
  • Node.js也紧跟ES Module 发展的步伐,从12.20版本开始支持原生ES Module,也就是ESM还具有跨平台能力
  • ESM也是Vite在开发阶段实现 no-bundle 的原因,由于模块加载的任务交给了浏览器,即使不打包也可以顺利运行模块代码,
  • ESM提供了两个API,就是importexport,一看就能看出分别的作用是什么,使用起来也是很清晰明了
    • export命令用于规定模块的对外接口,同时还定义了default export,,可以为模块指定默认导出,一个模块只能有一个默认导出,在导入的时候不需要使用{},同时导出和导入的名称可以不一样
    • import命令用于输入其他模块提供的功能
//index.html
<script  type="module" src="./main.js"></script>

//module-a.js
//这个是普通导出
export const  methodA = () => {
  console.log('这里是导出的的函数')
}
//这个是默认导出
const dataA = '这个是默认导出'  
export default  dataA  

//main.js
import { methodA } from './module-a.js'//普通导出的需要加上{}进行导入,导入导出需要一直
import ShowDataA from './module-a.js' //默认导出的不需要加上{},且导出导入名称可以不一致
methodA();
console.log(ShowDataA)

image.png

参考了