前端模块化规范的前世今生…

为什么要需要前端模块化

随着W3C的规范、浏览器的飞速发展,前端模块化会逐步成为开发的基础措施。通过模块化规范,我们可以开心的造轮子无缝使用轮子。

也正是基于此,衍生出一些库、工具或者实现特殊的功能 譬如:基于ESModule模块化规范的Vite、构建工具的路由懒加载、模块懒加载、code-split等等。

模块化开发的优势

  • 避免因作用域污染出现的变量命名冲突等问题。

    • 假设老李跟老王的开发任务在一个作用域下

      老李:我要声明一个pageData用哈

      老王:真巧我也想声明一个pageData

      老李:emmmm 老王你小子不讲武德怎么把我声明的变量覆盖了!!!

  • 模块分离实现模块的按需加载,提升项目性能。

    • 假设你的项目有首屏加载的性能问题

      老板:小王,咱的页面为啥加载时间那么长,有什么优化方案么?

      小王:老板,我试下吧。我先实现下路由页面的懒加载,然后用code-split把不经常更新的工具库单独打包成一个bundle.js并做Http缓存处理 这样不用后面不用重复请求资源了还有…

      老板:不错小王,速度确实提升了不少。年底奖金大大滴(你他喵,有这些大招不早点放)

  • 对业务功能进行模块封装,提高代码的可复用性。

  • 业务维护不需要在整个业务代码进行,只在某个具体功能模块进行维护,提高项目的可维护性。

前端模块化成长之路

JavaScript被设计出来时 只是为了进行页面交互、表单处理等简单逻辑。但是随着互联网技术的发展,前端开发也变得越来越复杂。开始出现命名冲突繁琐的依赖文件等与模块化规范相关的问题,为了解决这些问题,前端模块化开始踏上探索之路。

IIFE模式

IIFE(Immediately Invoked Function Expression)立即调用函数表达式,由于JS的scope方面的缺陷。JS只有全局作用域和块级作用域,到了ES6才有块级作用域的概念。在此之前,开始尝试用IIFE的方式弥补这种缺陷,实现作用域的隔离。

// moduleA.js  
(function(window) {
 // 私有变量  
 var minAge = 18
 // 私有函数
 function goInternetBar(person) {
   if(person.age<minAge){
     console.wran("未成年人禁止去网吧")
   }else {
     // 去网吧玩,还想学习成绩好 天底下哪有那么多好事 5_5
     person.grade--
   } 
 }
 // 暴露模块私有函数
 window.plugTool = {goInternetBar}
})(window);

// moduleB.js  
(function(window,tool) {
  var student = {
    name:"小明",
    age:19,
    grade:99
  }
  // 使用moduleB暴露的goInternetBar方法
  tool.goInternetBar(student)
  console.log("小明去过网吧后,成绩:"+student.grade)
})(window,plugTool)
复制代码

优雅标准的模块化规范

IIFE 的出现解决了作用域混淆污染的问题,但是它仍然有很多不足同时写法也不够优雅

  • 如何安全的包装一个模块的代码(实现作用域隔离,不污染模块外的任何代码)
  • 如何唯一的标记一个模块(便于模块的缓存的等操作)
  • 如何优雅的暴露模块的API而非全局变量的形式
  • 如何清晰展示模块之间的依赖关系

CommonJS

CommonJS最初是服务端应用在Node环境中的模块化规范,它规定在Node中,每个文件都是一个模块都拥有各自的作用域。

语法

  • global:对象,挂载全局变量
  • module:对象,当前模块对象
  • require:函数,输入依赖
  • module.exports:对象,模块向外暴露的接口
  • exports:对象,默认指向module.exports,不可以覆盖该属性的值,只能在该属性上挂载变量或者方法
// moduleA.js  
function eat(weight, food) {
  if (food === '米饭') {
    weight++
  } else if (food === '馒头') {
    weight += 2
  }
  return weight
}
// 暴露方式1:  
module.exports = { eat }
// 暴露方式2:
// exports.eat = eat

// 错误的暴露方式:
// exports = { eat }

// moduleB.js 
const tool = require("./moduleA.js")
const person = {
  name: "张三",
  weight: 100
}
const weight = tool.eat(person.weight, "馒头")
console.log("小明吃过馒头后,体重为:" + weight)
复制代码

image.png

特性

  • 模块同步加载
    CommonJS模块化规范模块是同步加载机制,加载的资源都在本地服务器上,时间和速度都没问题。但在浏览器端,受限于网络及性能的原因CommonJS显然不适合。

  • 模块暴露出去的值为浅拷贝
    当暴露的值为简单数据类型时,模块内外的数据是独立的,即使模块内部数据改变 暴露的值也不变
    当暴露的值为复杂数据类型时,模块内外引用的为同一个对象,在模块内部修改该对象属性会影响到外部。

    // moduleA.js  
    // 复杂数据
    var person = { name: "张三", age: 18 }
    // 简单数据
    var maxAge = 10
    function ageAdd() { person.age++ }
    function maxAgeAdd() {  maxAge++  }
    
    console.log("moduleA  is  loading");
    module.exports = {
      person, maxAge, ageAdd, maxAgeAdd
    }
    
    // main.js  
    var { person, maxAge, ageAdd, maxAgeAdd } = require("./moduleA.js")
    console.log(`============= init ============`);
    console.log("person.age:" + person.age + ",maxAge:" + maxAge);
    // 修改模块内部的简单、复杂数据
    ageAdd()
    maxAgeAdd()
    console.log(`============= age add ============`);
    console.log("person.age:" + person.age + ",maxAge:" + maxAge);
    复制代码

    image.png

AMD

AMD(Asynchronous Module Definition):异步加载模块定义。
鉴于浏览器环境的特性,我们需要一个异步加载机制的模块化规范。AMD则是最早实践者的其中之一。

AMD语法规范

声明模块

define([依赖模块],function(依赖模块暴露对象) {...})

// 声明无依赖的AMD模块
define(function(){
   return 向外暴露的对象
})
// 声明有依赖的AMD模块
define(["jquery"],function($){
   return 向外暴露的对象
})
复制代码

导入依赖

require([依赖模块],function(依赖模块暴露对象) {...})

// 引用模块,将模块放在[]内
require(['jquery', 'math'],function($, math){
  var sum = math.add(10,20);
  $("#app").css("background","pink")
});
复制代码

require.js

require.js是AMD规范的最佳实践,它主要解决了在使用AMD规范中触发以下几个问题:

  • 实现js文件的异步加载,避免网页失去响应
  • 管理模块之间的依赖性,便于代码的编写和维护。

AMD规范特性

  • 依赖前置,提前执行
    在AMD中,输入依赖的语句都会提升到最前面。并且立即执行所有输入依赖的模块,然后再执行其他内容

    // moduleA.js  
    define(function () {
      console.log('modelA is loading');
      return {
        getName: function () {
          return 'modelA';
        }
      };
    });  
    
    // moduleB.js  
    define(function () {
      console.log('modelB is loading');
      return {
        getName: function () {
          return 'modelB';
        }
      };
    });
    
    // main.js  
    define(function (require) {
      var modelA = require('./a');
      console.log(modelA.getName());
      var modelB = require('./b');
      console.log(modelB.getName());
    });
    
    // index.html  
    <body>
      <!-- 引入require.js -->
      <script src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.min.js"></script>
      <script>
        // 加载main.js
        requirejs(['main']);
      </script>
    </body>
    复制代码

    image.png

CMD

CMD(Common Module Definition):通用模块定义。
它是Sea.js推广中形成的,一个文件就是一个模块,可以像Node.js一般书写模块代码。主要在浏览器中运行,当然也可以在Node.js中运行。

CMD语法规范

声明模块

// 声明没有依赖的模块
define(function(require, exports, module){
  // 暴露方式1:
  exports.xxx = value
  // 暴露方式2:
  module.exports = value
})
复制代码
// 声明有依赖的模块  
define(function(require, exports, module) {
  //引入依赖模块(同步)
  var module2 = require('./module2')
  //引入依赖模块(异步)
  require.async('./module3', function (m3) {...})
  //暴露模块
  exports.xxx = value
})
复制代码

导入依赖

define(function (require) {
  var mA = require('./moduleA')
  var mB = require('./moduleB')
  mA.sayHello()
  mB.sayHello()
})
复制代码

CMD规范特性

  • 依赖就近、延迟执行

在当前模块中,需要依赖的时候才进行声明导入。等导入成功后当前模块代码才会继续向下执行。

// moduleA.js
define(function (require, exports, module) {
  console.log("moduleA is loading");
  module.exports = {
    getName: function () {
      return "moduleA"
    }
  }
});

// moduleB.js
define(function (require, exports, module) {
  console.log("moduleB is loading");
  module.exports = {
    getName: function () {
      return "moduleB"
    }
  }
});

// main.js  
define(function (require, exports, module) {
  var moduleA = require("./moduleA")
  console.log(moduleA.getName());
  var moduleB = require("./moduleB")
  console.log(moduleB.getName());
});

// index.html
<body>
  <script src="https://cdn.bootcss.com/seajs/3.0.3/sea.js"></script>
  <script>
    seajs.use('./main.js')
  </script>
</body>
复制代码

image.png

前端模块化旧时代落幕

前端模块化由IIFEAMDCMDUMD(他的出现为了解决前面几种规范的兼容性问题,本文不对其进行详解介绍),这些模块化规范是前端开发者自己不断探索演变的,直到ES6推出新的模块化标准 ES Modules,老的模块化规范开始过时走向衰弱。

有两个有意思的文章可以看下:

前端模块化开发那点历史

CMD规范的陨落

ES Modules

ES Modules(ESM)是 JavaScript 官方的标准化模块系统。 作为JS ESMA标准它会逐步被全部浏览器支持。它可以在NodeJS中运行。

语法

模块暴露

export default:默认导出
export:按需导出

var num= 1
function numAdd() {
 num++
}
var count = 10
var name = "张三"
export default {num,numAdd}
export count
export name
或者  
export {name,count}
复制代码

导入依赖

// 默认导入
import tool from './moduleA.js';
// 按需导入
import { count } from './moduleA.js';
// 复合用法
import tool,{ count } from './moduleA.js';
复制代码

特性

  • import 命令会被 JavaScript 引擎静态分析,优先于模块内的其他内容执行。

    // moduleB.js
    console.log("moduleB is loading");
    import * as tool from './moduleA.js';
    console.log("tool", tool);
    
    // moduleA.js  
    console.log("moduleA is loading");
    export var num = 1
    
    // index.html  
    <body>
      <script type="module" src="./moduleB.js"></script>
    </body>
    复制代码

    image.png

  • 导出值为模块中值的引用

// moduleA.js  
// 复杂数据
var person = { name: "张三", age: 18 }
// 简单数据
var maxAge = 10
function ageAdd() { person.age++ }
function maxAgeAdd() { maxAge++ }

console.log("moduleA  is  loading");
export {
  person, maxAge, ageAdd, maxAgeAdd
}

// main.js  
import { person, maxAge, ageAdd, maxAgeAdd } from './moduleA.js';
console.log(`============= init ============`);
console.log("person.age:" + person.age + ",maxAge:" + maxAge);

ageAdd()
maxAgeAdd()
console.log(`============= age add ============`);
console.log("person.age:" + person.age + ",maxAge:" + maxAge);  

// index.html  
<body>
  <script type="module" src="./main.js"></script>
</body>
复制代码

image.png

ES Modules 与CommonJS的区别

暴露值的区别

ES Modules暴露的为一个值的引用,在模块外调用模块内的方法修改模块内的值 模块暴露的值也会改变
CommonJS暴露的为一个值的浅拷贝,当模块内这个值发生变化,如果这个值为简单数据则暴露的值不会发生变化,如果这个值为复杂数据则暴露的值也会跟着改变。

加载机制不同

CommonJS的模块为动态导入,运行时加载。会先将整个模块加载进来,然后生成一个对象 从这个对象上取暴露的数据。
ES Modules是JS静态编译时加载,import语句优先执行。在编译时import去指定加载暴露的数据

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