JS模块化

68 阅读6分钟

理解模块化

什么叫模块化?

JavaScript开发会遇到代码量大广泛使用第三方库的问题。解决这个问题的方案通常把代码拆分成很多部分,然后再通过某种方式把它们连接起来。所以,简单理解,模块化就是把逻辑代码分块,各自封装,相互独立,每个代码块自行决定对外暴露什么,同时自行决定引入执行哪些外部模块。

模块标识符

模块标识符是所有模块系统通用的概念。模块系统本质上是键/值实体,例如,每个模块都有一个用于引用它的标识符,这个标识符可能是字符串,可能是模块文件的路径。

完善的模块系统一定不会存在模块标识冲突的问题,且系统中的任何模块都应该能够无歧义地引用其它模块。

模块依赖

模块系统的核心是管理依赖。本地模块向模块系统声明一组外部模块(依赖),这些外部模块对于当前模块正常运行是必需的。模块系统检视这些依赖,进而保证这些外部模块能够被加载在本地模块运行时初始化所有依赖。

模块加载

当一个外部模块被指定为依赖时,本地模块期望在执行它时,依赖已准备好并初始化。
浏览器加载模块的步骤:

  1. 加载模块涉及其中的代码,但必须是在所有依赖都加载并执行之后。
  2. 如果浏览器没有收到依赖模块的代码,则必须发送请求并等待网络返回。
  3. 收到模块代码后,浏览器必须确定刚收到的模块是否也有依赖。
  4. 递归评估并加载所有依赖,直到所有依赖模块全部加载完成。
  5. 整个依赖图都加载完成,才可以执行入口文件。

入口

相互依赖的模块必须指定一个模块作为入口,即代码执行的起点。

异步依赖

即让JavaScript通知模块系统在必要时加载新模块,并在模块加载完成后提供回调。

动态依赖

动态依赖,满足某个条件后再去加载依赖。 支持复杂的依赖关系。

静态分析

模块中包含的发送到浏览器的JavaScript代码经常会被静态分析,分析工具会检查代码结构并在不实际执行代码的情况下推断其行为。对静态分析友好的模块一同可以让模块打包系统更容易将代码处理为较少的文件。
较复杂的模块行为,例如动态依赖,会导致静态分析更困难。

循环依赖

要构建一个没有循环依赖的JavaScript应用程序几乎是不可能的,因此包括CommonJS、AMD和ES6在内的所有模块系统都支持循环依赖。

立即执行函数(IIFE)实现模块化

第一种:

var Foo = (function() {
  return {
    bar: 'baz',
    baz: function () {
      console.log(this.bar);
    }
  }
})();

第二种:泄露模块模式
只返回一个对象,其属性是对私有数据和成员的引用。

var globalBar = 'bar';

var Foo = (function (bar) {
  var baz = 'baz';
  return {
    bar: bar,
    baz: function () {
      console.log(baz);
    }
  };
})(globalBar);

Foo.baz();

一些补充

  • 浏览器不支持模块的行为,即模块化的语法无法在浏览器中运行
  • ES6模块规范出现之前,ECMAScript不支持模块,往往需要伪造出类似模块的行为(使用对象、IIFE等)
  • 模块的加载是阻塞的(即按顺序来)

CommonJS 模块化规范

主要用于在服务器端实现模块化代码组织,也可以用于定义在浏览器中使用的模块依赖。CommonJS模块语法不能在浏览器中直接运行。

说明:

  • 每个文件都可以当作一个模块
  • 在服务器端:模块的加载是运行时同步加载的
  • 在浏览器端:模块需要提前编译打包处理

语法:

暴露模块:

  • module.exports = {}
  • exports = {}

引入模块:

  • 第三方模块:require('包名')
  • 自定义模块:require('模块文件路径')

ES6 模块化规范

参考链接:前端模块化详解(完整版)

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

语法

export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
    return a + b;
};
export { basicNum, add };
/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {
    ele.textContent = add(99 + basicNum);
}

如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

// export-default.js
export default function () {
  console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo'

模块默认输出, 其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

(2)ES6 模块与 CommonJS 模块的差异

它们有两个重大差异:

① CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用

② CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

下面重点解释第一个差异,我们还是举上面那个CommonJS模块的加载机制例子:

// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

ES6 模块的运行机制与 CommonJS 不一样。ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块

(3) ES6-Babel-Browserify使用教程

简单来说就一句话:使用Babel将ES6编译为ES5代码,使用Browserify编译打包js

①定义package.json文件

 {
   "name" : "es6-babel-browserify",
   "version" : "1.0.0"
 }

②安装babel-cli, babel-preset-es2015和browserify

  • npm install babel-cli browserify -g
  • npm install babel-preset-es2015 --save-dev
  • preset 预设(将es6转换成es5的所有插件打包)

③定义.babelrc文件

  {
    "presets": ["es2015"]
  }

④定义模块代码

//module1.js文件
// 分别暴露
export function foo() {
  console.log('foo() module1')
}
export function bar() {
  console.log('bar() module1')
}
//module2.js文件
// 统一暴露
function fun1() {
  console.log('fun1() module2')
}
function fun2() {
  console.log('fun2() module2')
}
export { fun1, fun2 }
//module3.js文件
// 默认暴露 可以暴露任意数据类项,暴露什么数据,接收到就是什么数据
export default () => {
  console.log('默认暴露')
}
// app.js文件
import { foo, bar } from './module1'
import { fun1, fun2 } from './module2'
import module3 from './module3'
foo()
bar()
fun1()
fun2()
module3()

⑤ 编译并在index.html中引入

  • 使用Babel将ES6编译为ES5代码(但包含CommonJS语法) : babel js/src -d js/lib
  • 使用Browserify编译js : browserify js/lib/app.js -o js/lib/bundle.js

然后在index.html文件中引入

 <script type="text/javascript" src="js/lib/bundle.js"></script>

最后得到如下结果:

此外第三方库(以jQuery为例)如何引入呢? 首先安装依赖npm install jquery@1 然后在app.js文件中引入

//app.js文件
import { foo, bar } from './module1'
import { fun1, fun2 } from './module2'
import module3 from './module3'
import $ from 'jquery'

foo()
bar()
fun1()
fun2()
module3()
$('body').css('background', 'green')