Javascript模块化


TL:DR;

  • 前端模块化主要是解决依赖管理,模块加载的问题
  • 立即调用函数的匿名闭包是模块化实现的基石
  • 在客户端和服务端都需要做模块化
  • CommonJS是服务端的模块化解决方案,模块同步加载
  • AMD是客户端(浏览器)的模块化方案,模块异步加载
  • UMD统一了客户端和服务端,兼容AMD和CommonJS
  • ES6提供了官方的模块化(import/export)

在写Javascript的时候,会经常会听到模块化这个概念,模块化在编程是一个很重要的设计,将可复用的、独立的模块抽出来,一是避免重复造轮子;二是减小维护和使用成本,虽然现在ES6提供了标准的模块化方案,但是在Javascript的发展历程中还是经历了一段从黑暗到光明的阶段。

模块的加载和封装

使用<script>来做模块化最常见的方式是使用script标签将你需要的模块在网页中加载,在用户请求网页的时候,会加载前置的依赖,例如这样:

  <html>
    <head>
      <script type="text/javascript" src="./module1.js"></script>
      <script type="text/javascript" src="./main.js"></script>
    </head>
    <body></body>
  </html>

就像上面一样,这里先加载module1.js,在main.js中会使用module1暴露出的方法,但是这样的模块化方案如果面对大量的依赖管理的话,就会面临很多问题,就像下面的代码:

  <html>
    <head>
      <script type="text/javascript" src="./framework.js"></script>
      <script type="text/javascript" src="./frameworkPlugin.js"></script>
      <script type="text/javascript" src="./frameworkPlugin1.js"></script>
      <script type="text/javascript" src="./frameworkPlugin2.js"></script>
      <script type="text/javascript" src="./frameworkPlugin3.js"></script>
      ...
      <script type="text/javascript" src="./main.js"></script>
    </head>
    <body></body>
  </html>

但是如果依赖多了的话,带来的问题就是,第一依赖模糊,模块与模块之间的依赖并不清楚;第二就是会给代码维护带来麻烦,因为在编写代码的时候,依赖的注入,是依赖于html中前置的script标签做的,在代码中并没有显示的声明依赖;第三就是每个<script>加载都需要发网络请求,这里网络请求过多。

如何构建Javascript模块也是一个值得探讨的问题,常见的是这样:

function foo() {
	......
}

function bar() {
	......
}

如果以为上面的方式暴露接口的话,存在的问题就是,很容易污染全局环境,造成命名冲突,为了解决命名冲突的问题,随之而来,我们可以增加命名空间:

const myModule = {
	foo: function () { ... },
	bar: function () { ... },
}

myModule.foo()

像上面这样加命名空间的方式减少了全局环境被污染的情况,并且这样的封装并不安全,本质上是对象,外部可以访问到不想暴露给外界的东西,这种方式并没有解决根本矛盾—>保证封装性的同时减少全局变量的数量,在这里我们要保证模块只暴露想暴露的东西,一些私有属性外界无法访问,这里使用闭包就可以解决这些问题:

const myModule = (function(){
	const _private = 1;
	const foo = () => {
		// use _private
	}
	return {
		foo,
	}
})();
myModule.foo()
myModule._private

这里使用立即调用(IIFE)的模式,将私有属性和外部隔离起来,保证了封装性,如果我们需要注入其他的依赖可以这样:

const myModule = (function($){
	const _$ = $;
	const _private = 1;
	const foo = (selector) => {
		return _$(selector);
	}
	return {
		foo,
	}
})($)

就像上面的代码,如果可以向模块引入外部依赖,这就是现代模块系统的基石。

上面讲了如何封装一个模块和加载模块,在实际的开发过程中,我们仍然需要考虑几个问题,比如跳出浏览器环境,在Node.js下如何做模块化,如何对打包之后的模块进行压缩、合并、优化。

CommonJS

CommonJSNode.js的模块化规范,Common.js对外暴露四个环境变量moduleexportsglobalrequireCommon.js以文件作为独立模块来管理,Common.js以同步的方式加载模块。

//  引入模块
// a.js
const foo = () => {
	...
}
const bar = () => {
	...
}

// 导出模块
module.exports = {
	foo,
	bar
}

// main.js
// 声明依赖模块
const { foo, bar } = reauire(./a.js);

foo();
bar();

因为在服务端,文件存在磁盘上,读取速度很快,同步的加载方式不存在问题,但是在浏览器上,因为网络的原因,同步加载的方式并不是一个好的方式,这就引入了异步加载模块的方式AMD

优缺点:

  • 服务端的模块化方案实现
  • 模块的输出是Object,无法做静态分析
  • 每个模块输出都是一个copy,无法做到热加载
  • 循环依赖的管理做的不好

AMD(Async Module Definition )

AMD规范制定了一套异步加载module的机制,define(id?, dependencies?, factory)define函数的前两个参数是可选的,如果提供了一个id,这个id就代表该模块,如果没有给这个参数,某块的名字就是模块加载器请求脚本的名字,dependencies是一个模块id的数组,声明当前模块的依赖模块,factory函数就是在所有依赖模块加载好之后的会调函数,如果dependencies中没有提供任何依赖,模块加载器会扫描factory函数中所有的require,同步加载依赖,factory函数只执行一次,如果传入的参数是一个Object的话,会将模块输出到这个对象中,如果函数的返回值是一个对象,模块输出到返回值中。

// 将alpha模块挂到exports上
  define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {
       exports.verb = function() {
           return beta.verb();
           //Or:
           return require("beta").verb();
       }
   });
// 返回输出模块
   define(["alpha"], function (alpha) {
       return {
         verb: function(){
           return alpha.verb() + 2;
         }
       };
   });
// 一个没有依赖的模块
   define({
     add: function(x, y){
       return x + y;
     }
   });
// 使用commonJS包裹的模块
   define(function (require, exports, module) {
     var a = require('a'),
         b = require('b');

     exports.action = function () {};
   });

优缺点:

  • 主要用于客户端(浏览器)
  • 语法复杂

UMD(Universal Module Definition)

AMDCommonJS的模块化方案提供两套APIUMD将这两套API统一了起来,UMD使用commonJS的语法,但是提供异步加载模块的能力。

	// File log.js
(function (global, factory) {
  if (typeof define === "function" && define.amd) {
    define(["exports"], factory);
  } else if (typeof exports !== "undefined") {
    factory(exports);
  } else {
    var mod = {
      exports: {}
    };
    factory(mod.exports);
    global.log = mod.exports;
  }
})(this, function (exports) {
  "use strict";

  function log() {
    console.log("Example of UMD module system");
  }
  // expose log to other modules
  exports.log = log;
});

如上其实UMD就是对宿主环境做了兼容性处理,在不同的宿主环境下输出不同模块。

优缺点: - 同时适用于客户端和服务端,兼容性好 - 兼容AMDCommonJS

ES6的Import和Export

ES6中有了官方的模块化解决方案,将AMDCommonJSUMD统一了起来,并且在打包工具打包的时候可以做静态分析,可以做tree shaking

// a.js
export const foo = () => {}

// main.js
import { foo } from './a';
foo();

优缺点:

  • 服务端和客户端都可以使用
  • import的时候拿到的是实际值不是拷贝,可以做热更新
  • 支持静态分析(可以用tree shaking
  • 相比于CommonJS循环依赖的管理做的更好

Webpack 、Babel

因为并不是所有的宿主环境都支持ES6,我们需要使用Babel来将ES6的代码转移成es5的代码,同时我们需要对现有的工程代码做合并、压缩和优化,这主要是通过Webpack实现的,也可以使用Webpack做按需加载,划分不同的chunk,减少http请求。

Reference


兴趣遍地都是,坚持和持之以恒才是稀缺的


Written by@wang yao
I explain with words and code.

GitHub