Site Overlay

前端模块化

什么是模块化?

将一个复杂的程序依据功能拆分成几个独立的块(文件),每个块都包含执行预期功能一方面的内容,其内部的数据或方法都是私有的。它们之间可以通过一些规范向外暴露一些接口(方法)与其他文件进行通讯或组合。

为什么前端需要模块化?

随着js能做的事越来越多,前端项目代码日益膨胀,主要体现在复杂的依赖关系、难以维护、代码不可复用和全局污染的问题。这些问题需要花费大量时间去管理, 而前端模块化能很好地解决这些问题。

发展演变

无模块化

就是把各种js文件一股脑放在script标签中。

<body>
  <script src="module1.js"></script>
  <script src="module2.js"></script>
  <script src="module3.js"></script>
</body>

问题:
– 引入的文件都在全局作用域中,污染全局环境,可能导致命名冲突
– 引入的顺序很有讲究,先后顺序错了,会导致报错,依赖关系不明确。

命名空间

每个模块只暴露一个全局对象,我们所有的模块成员(数据/方法)都挂载在这个对象下面

// module.js
const module = {
  name: 'jennifer',
  age: 18,
};
<script src="module.js"></script>
<script>
  console.log(module.name); // 'jennifer'
  module.name = 'jie'; // module.name --> jie
</script>

解决:减少全局变量,解决命名冲突
问题:
– 模块间的依赖关系还是没解决
– 数据不安全,外部可以随意修改内部变量

IIFE(立即执行函数)

// module.js
(function(window) {
  const name = 'jennifer';
  function showName() {
    console.log(`name: ${name}`);
  }
  window.module1 = { showName } ;
})(window);
<script src="module.js"></script>
<script>
  module1.showName(); // 'jennifer'
  console.log(module1.name); // 'undefined' 模块未导出该变量,无法访问,也无法修改
</script>

解决:数据是私有的, 外部只能通过暴露的方法进行操作,安全
问题:如果需要依赖另一个模块怎么处理?? 通过传参,把另一个模块传入进去~

// module.js
(function(window, ) {
  const name = 'jennifer';
  function showName() {
    console.log(`name:{name}`);
    $('body').css('background', 'cyan');
  }
  window.module1 = { showName } ;
})(window, jQuery);

html引入的js顺序要注意!jQuery要在module.js前

模块化规范

CommonJs

该规范最初是用在服务器端的node的。node应用由模块组成,每个文件都是一个模块,有自己的作用域。它有四个重要的环境变量为模块化的实现提供支持:moduleexportsrequireglobal。实际使用时,用module.exports定义当前模块对外输出的接口(不推荐直接用exportsexports 是对 module.exports 的引用,不能直接去赋值),用require加载模块。

// module.js
const x = 5;
const add = function (value) {
  return value + x;
};
module.exports.x = x;
module.exports.add = add;
const module = require('./module.js'); // 自定义模块require的为路径,第三方模块为包名
console.log(module.x); // 5
console.log(module.add(1)); // 6

解决:全局污染,依赖关系不明确
问题:在浏览器端使用CommonJS规范会导致效率低下,在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。所以每次页面加载都会导致大量同步请求出现,造成页面阻塞,用户体验很不好。之后CommonJS中逐渐产生了一些分支,也就是AMDCMD等。

AMD

James Burke提出了 AMD 规范,RequireJS 也是他的代表作。AMD规范是非同步加载模块,允许指定回调函数。

主要的三个API:
1. require([module], callback)
2. define(id, [depends], callback)
3. require.config()
通过define来定义一个模块,然后使用require来加载一个模块, 使用require.config()指定引用路径。

<!DOCTYPE html>
<html>
  <head>
    <title>Modular Demo</title>
  </head>
  <body>
    <!-- 引入require.js, data-main不能省略,是用来指定js主文件的入口 -->
    <script data-main="js/main" src="./require.js"></script>
    <script>
      require(['main'], function(main) {
        console.log(main.getName); // jennifer
      })
    </script>
  </body>
</html>
// main.js
define(function() {
  let name = 'jennifer';
  function getName() {
    return name.toUpperCase()
  };
  return { getName }; // 暴露模块
})

如果有其他依赖,可以传一个数组作为define的第一个参数

在使用require.js的时候,我们必须要提前加载所有的依赖,然后才可以使用,而不是需要使用时再加载。

解决:CommonJS的问题):同步导致的阻塞
问题:require.js2.0以前依赖不能延迟执行,在加载模块完成后就会执行改模块。

CMD(Sea.js的推出)

CMD规范整合了CommonJS和AMD规范的特点。CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。

// AMD,require.js 2.0以前
define(['a', 'b'], function(a, b) {
  a.doSomething();
  if (false) {
    // 即使没有用到模块b,他还是被提前加载了
    b.doSomething();
  }
})

// AMD,require.js 2.0之后可以按需加载
define(['require', 'a'], function(require, a) {
  a.doSomething();
  if (false) {
    const b = require('./b');
    b.doSomething();
  }
})

// CMD
define(function(require, exports, module) {
  const a = require('./a'); // 在需要是加载模块
  a.doSomething();
  if (false) {
    const b = require('./b');
    b.doSomething();
  }
})

AMD和CMD总结

AMD和CMD对依赖模块的执行时机处理不同
– AMD推崇依赖前置,在定义模块的时候就需声明其依赖的模块,并且加载完直接执行(requireJs 2.0后可以实现延迟执行
– CMD推崇就近依赖,只有在用到某个模块的时候再去require,是延迟执行

ESM

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。统一了浏览器和服务端。但是由于ES6目前无法在浏览器中执行,所以只能通过babel将不被支持的import编译为当前受到广泛支持的 require。

// math.js
const num = 0;
const add = function(a, b) {
  return a + b;
};

export { num, add }; // 或者默认导出:export default { num, add }
import { num, add } from './math'; // 默认导入 import math from './math'
// 默认导入引用 math.add(99, math.num);
console.log(add(99, num)); // 99

总结CJS和ESM

  • CJS 模块输出的是一个值的拷贝,ESM模块输出的是值的引用
  • CJS 模块是运行时加载(加载的是一个对象,只会在脚本运行完才生成),ESM 模块是编译时输出接口(一种静态定义,代码解析阶段就会生成)
  • CJS 加载模块是同步的,主要用于服务端编程,而ESM 有异步的特性,能够tree shaking,成为了浏览器和服务端通用的模块解决方案