前端模块化规范进化史

127 阅读5分钟

最开始的模块化

目录如下: image.png

// => A.js
let name = "HH";

const sum = (...params) => {
  let len = params.length;
  if (len === 0) return 0;
  if(len === 1) return params[0];
  params.reduce((x, item) => (+x) + (+item));
}
// => B.js
// B需要依赖A中的sum方法
let name = "RR";

// 求平均数
const average = (...params) => {
  let total = sum(...params);
  let len = params.length;
  if ( len === 0) return 0;
  return (total/len).toFixed(2);
}
// => main.js入口模块
// 调用AB模块的中的方法
console.log(sum(10,20,30));
console.log(average(10,20,30));
//=> index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <!-- 
    最开始的模块化开发: 
      - 把每个模块代码写在不同的文件中,最后在页面导入
      - 缺点: 
        + 需要分析依赖导入的先后顺序(麻烦)
        + 如果不基于闭包私有化处理,相同的变量容易引发"全局变量污染"
  -->
  <script src="./A.js"></script>
  <!-- B依赖A所以后导入 -->
  <script src="./B.js"></script>
  <!-- main.js依赖A和B -->
  <script src="./main.js"></script>
</body>
</html>

单例设计模式早期处理"全局变量污染"方式,模块之间相互访问

  • 解决私有化:自执行函数执行产生闭包即可
  • 解决模块之间的相互访问, 把需要访问的内容暴露到全局上, window.xxx = xxx
    • window.xxx === xxx也不是很好的方式,如果方法多了就很麻烦,模块的名称一样也可能导致全局变量污染
    • 把模块中暴露的东西放在对象管理,最后基于模块名存储这个对象即可(模块名必须唯一)
  • 但是还是不能解决顺序问题
// => A.js
const AModule = (function () {
  let name = "HH";

  const sum = (...params) => {
    let len = params.length;
    if (len === 0) return 0;
    if (len === 1) return params[0];
    params.reduce((x, item) => (+x) + (+item));
  }
  return {
    sum
  }
})();
// => B.js
// B需要依赖A中的sum方法
const BModule = (function () {
  let name = "RR";
  // 求平均数
  const average = (...params) => {
    let total = AModule.sum(...params);//调用AModule下的sum方法
    let len = params.length;
    if (len === 0) return 0;
    return (total / len).toFixed(2);
  }

  return {
    average
  }
})();
//=> main.js
console.log(AModule.sum(10,20,30));
console.log(BModule.average(10,20,30));
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script src="./A.js"></script>
  <!-- B依赖A所以后导入 -->
  <script src="./B.js"></script>
  <!-- main.js依赖A和B -->
  <script src="./main.js"></script>
</body>
</html>
  • 结果如下: image.png
  • 总结:
  1. 这种处理方式即保证代码之间的私有化,也支持模块之间的相互访问,避免的全局变量污染
  2. 我们把这种代码设计方案称之为单例设计模式
  3. 所有的设计模式都是一种思想,这种思想解决的某一类问题
  4. 还是需要手动分析各个模块之间的依赖关系,还有模块的命名空间的问题

AMD模块化思想按需引入思想

  • 金典requre.js
    • 解决的顺序所有问题,不过写法太麻烦了哈哈
    • 定义模块用defined
    • 定义模块的时候可以把依赖的模块"前置导入"
// => A.js
defined(function() {
    'use strict';
    let name = "HH";
    
    const sum = (...params) => {
      let len = params.length;
      if (len === 0) return 0;
      if(len === 1) return params[0];
      params.reduce((x, item) => (+x) + (+item));
    }
    
    return {
        sum
    }
})
// => B.js
// B需要依赖A中的sum方法
// => 定义模块的时候可以把依赖的模块"前置导入"
defined(['A'],function(AModule) {
    let name = "RR";

    // 求平均数
    const average = (...params) => {
      let total = AModule.sum(...params);//调用A模块的sum
      let len = params.length;
      if ( len === 0) return 0;
      return (total/len).toFixed(2);
    }
    return {
       average 
    }
})

// => main.js
require.config({
    baseUrl: './lib',// => 配置找模块的文件夹路径
})
// => 导入相关模块,处理内容
require(['A', 'B'], function(A, B) {
   // 调用AB模块的中的方法
    console.log(A.sum(10,20,30));
    console.log(B.average(10,20,30));
});
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <!-- 导入requre.js下载好的包 -->
  <script src="require.min.js"></script>
  <script src="./main.js"></script>
</body>
</html>
  • 总结
  1. 在单例设计模式的基础上有效的解决了模块之间的依赖问题,告别导入顺序问题
  2. 可以结合gulp/grount等,最后把各个模块代码合并压缩打包到一起
  3. 依赖的模块需要前置导入,不过写法太麻烦了(爷懒)哈哈

CommonJS规范

  • 随着nodeJS的发展,node里面有这commonJS思想,随导随用
  • CommonJS模块规范[模块的导入和导出]:Node自带的模块化规范(浏览器端不支持)
  • 比AMD用起来简单,从导入机制等原理也比AMD性能高
// => A.js
let name = "HH";

const sum = (...params) => {
  let len = params.length;
  if (len === 0) return 0;
  if (len === 1) return params[0];
  return params.reduce((x, item) => (+x) + (+item));
}

module.exports = {
  sum
}
// => B.js
const { sum } = require('./A.js');
let name = "RR";

// 求平均数
const average = (...params) => {
  let total = sum(...params);
  let len = params.length;
  if (len === 0) return 0;
  return (total / len).toFixed(2);
}

module.exports = {
  average
}
//=> main.js
/*
CommonJS模块规范[模块的导入和导出]:Node自带的模块化规范(浏览器端不支持)
  CommonJS模块的导入是按需的
  定义模块: 创建一个JS文件,就是定义一个模块;每个模块之间的代码本身就是私有的了
  导出模块中的方法: 
  module.exports = {
    sum//包含外界需要的属性和方法
  }
  导入指定模块
    const XModule = require('./A.js');
    使用 XModule.sum()
    or
    const { sum } = require('./A.js');
    sum();
*/
const { sum } = require('./A.js');
const { average } = require('./B.js');
console.log(sum(10,20,30));
console.log(average(10,20,30));
  • 再node的环境中执行 node .\CommonJS\main.js,结果如下: image.png
  • 补充
  1. CommonJS不支持浏览器端,所以淘宝玉伯 写了一个插件 sea.js[把其定义为CMD模块规范]
  2. CMD模块规范,本质把CommonJS规范搬到浏览器端运行(不在维护已过时)

ES6Module

  • 后来的ES6本身提供了更好的模块化规范,sea.js代表的CMD规范被pass掉了
  • vscode 记得安装live server插件
    • type = "module"
    • 基于标准的http/https协议的web服务预览页面
  • 创建一个js就是一个模块
// => A.js
let name = "HH";

export const sum = (...params) => {
  let len = params.length;
  if (len === 0) return 0;
  if (len === 1) return params[0];
  return params.reduce((x, item) => (+x) + (+item));
}
//=> B.js
import { sum } from './A.js';
let name = "RR";

// 求平均数
export const average = (...params) => {
  let total = sum(...params);
  let len = params.length;
  if (len === 0) return 0;
  return (total / len).toFixed(2);
}
// => main.js
import { sum } from './A.js';
import { average } from './B.js';
console.log(sum(10,20,30));
console.log(average(10,20,30));
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script src="main.js" type="module"></script>
</body>
</html>
  • 运行结果如下: image.png

ES6Module总结:

  • 问价你的后缀名不能省略.js
  • export default 一个模块中只能用一次
    • export default sum -> import sum from './文件路径' -> sum()
    • export default { sum } -> import A from './文件路径' -> A.sum()
  • export,可以使用多次,声明 + 赋值的形式
    • export const sum = () =>{...} -> import { sum } from './文件路径' -> sum()
    • export const sum = () =>{...} -> import A from './文件路径' -> A.sum()
  • 如果2个都有如下的常见情况
    • import A from './文件路径' (只有它的导出生效)
    • 可以 import * as A from './文件路径',就可以拿到2者导出的了
      • 把当前模块导出的所有内容获取到,后期基于A.xxx访问即可
  • import要放在文件的最上面导入需要模块的内容

当代前端开发都是基于模块化进行的,而模块化方案已CommonJS 和 ESModule为主

我们编写的JS代码,可以运行的环境

  1. 浏览器 <script src="...">,类似还有 webview环境
    • 1.1 直接支持ES6Module,不支持CommonJS
    • 1.2 全局对象 window
  2. Node
    • 2.1 支持CommonJS,但不支持ES6Module
    • 2.2 全局对象 global
  3. webpack [基于node实现代码代码的合并压缩打包;最后把打包的结果导入浏览器运行]
    • 3.1 直接支持ES6Module,支持CommonJS,而且相互之间的"混用"
    • 3.2 原理: webpack把2中模块化规范都实现了一遍
    • 3.3 支持window&global
  4. vite [新的打包工具]
    • 4.1 不像webpack的打包模式,它的本质[基于ES6Module规范,实现模块之间的相互引用]