JS中模块(Module)的前生今世

461 阅读5分钟

前言

什么是一个模块,js中的模块又是什么?为什么有AMD,CMD,UMD以及现在的CommonJS和ES6 他们有啥区别呢?究竟都是要解决什么问题?

远古时代

在ES6出来之前,JS中是没有严格意义的模块的。在ES6之前代码作用域只有全局作用域 和函数作用域。所有代码最终是在一个上下文,共享整个全局变量。JS的引用通常是使用script标签,然后共享全局变量。这就会产生非常多共享全局变量的问题。由于JS的函数有函数作用域,所以就有了这个IIFE设计模式。

   let people =  (function(){
        
        var  name = 'ada';
        
        var getName = function (){
            return name
        }
        var setName = function(n){
            name = name;
        }
        return {
            getName:getName,
            setName:setName
        }
    })()

这就是最一个典型的模块。这个模块返回一个对象,这个对象里有一些函数或者变量。可以提供外界调用。内部通过闭包封装了一些私有变量,如图所示name 就无法被外界访问。这就是最初级的一个模块的实现。

AMD,CMD时代

上面这个模式解决了模块设计的基本问题,就是封装性。但是还欠缺一些通用的对模块的管理。怎么引用一个模块?怎么管理全局的某块。于是业界就进化出了AMD和CMD。他们实质上都是通过一个配置为每个模块分配了一个名字,对应到这个模块的导出对象。

比如我们看一下 CMD的seajs

seajs.config({
  base: "../sea-modules/",
  alias: {
    "jquery": "jquery/jquery/1.10.1/jquery.js"
  }
})
//使用一个模块
seajs.use(['jquery'],function($,hm){  
   //引用一个库
});  

//定义一个模块
define(function(require, exports, module) {
  // 通过 require 引入依赖
  var $ = require('jquery');

  // 或者通过 module.exports 提供整个接口
  module.exports = ...

});

如代码所示首先通过config方法为每个模块定义一个名字和文件的映射。然后在使用中就可以用名字来获取代码的导出对象。定义模块时候实际上就是封装了一个函数,然后内部有一个导出对象。 requireJs中的也类似。

CommonJS

CommonJS是在Nodejs中使用的,但是他也不是JS原生的模块。commonjs里每一个文件都是一个模块。因为在服务端我们有一个文件路径作为文件的名字。于是commonjs中变成了这样。 定义一个模块 a.js

var x = 5;
let setX = (a){
    x = a;
}

module.exports={
    x,
    setX
};

b.js中引用a.js

let b = require("a.js")
b.setX(100);
console.log(b.x);

commonjs 里有两个问题。1个就是浏览器无法理解。2 是因为他是同步加载代码的,不太适合浏览器(此处有疑问,为何浏览器不支持同步,如果每次require就添加一个script标签来执行,这是一个异步的过程)。

于是webpack 就有了一个解决方案叫做bundle.js

webpack

bundle.js就是webpack将所有js文件打包到一个文件里。webpack同时实现了require。和模块。究竟是什么让我们看下代码。原始的代码就是entry.js.只有一句话,然后我们看看webpack打包之后是什么样的代码。

entry.js

console.log("a");

下面看看打包之后的代码

/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId]) {
/******/ 			return installedModules[moduleId].exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			i: moduleId,
/******/ 			l: false,
/******/ 			exports: {},
/******/ 			hot: hotCreateModule(moduleId),
/******/ 			parents: (hotCurrentParentsTemp = hotCurrentParents, hotCurrentParents = [], hotCurrentParentsTemp),
/******/ 			children: []
/******/ 		};
/******/
/******/ 		// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));
/******/
/******/ 		// Flag the module as loaded
/******/ 		module.l = true;
/******/
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
...
...//这里是模块的代码,被一个函数包装起来。
                    /***/ (function(module, exports) {
                    
                    console.log("a");
                    
                    /***/ })

可以看到webpack 的实现也是将模块的代码用IIFE来封装,require的实现其实 也就是根据moduleId来找到对应函数,执行并返回模块的导出对象。这也没有什么稀奇本质上和我们最开始的IIFE没有区别。

ES6 Module

从ES6 开始 ES真正有了Module,他和以往标准的有本质不同,他才是真正的模块。在浏览器中我们可以通过添加属性type='module'引用一个模块。

<html>
    <body>
     <script type=module src='dom.js'></script>
    </body>
</html>

dom.js

import getUsers from "./users.js"
console.log(getUsers());

users.js

// users.js

var users = ["Tyler", "Sarah", "Dan"];
export default function getUsers() {
  return users;
}

我们页面中引入dom.js,dom.js中import user.js。如果我们用浏览器打开最初的html,随着浏览器执行代码,会自动请求users.js。并且每个模块是独立隔离的,并不需要IIFE模式。

介绍一个优化办法

对于现代的浏览器很多已经支持了ES6了,如果你还用babel转化为es5代码,那么会添加很多webpack相关的代码,而且执行效率相对较低。可以通过type='module'来直接执行代码。这就需要你在webpack的时候打两份代码。一份没有转化,一份转化了。对于非现代浏览器则继续用老的降级方案。新浏览器直接执行es6。

<script type="module" src="runs-if-module-supported.js"></script>
<script nomodule src="runs-if-module-not-supported.js"></script>

TreeShaking

commonjs的 require 和 import 另外一个区别就是import只能在模块头部引用,不能在条件分支中引用。而require中并没有这个限制。所以要求我们在配置webpack的treeshaking的时候,需要使用es6的模块,不能使用commonjs。 .babelrc文件

{
    "presets": [
        [
            "env",
            {
                "modules": false,//特别注意只有这个为false ,webpack才能treeshaking
                "targets": {
                    "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
                }
            }
        ],
        "stage-1"
    ],
    "plugins": ["transform-runtime"

错误示例,条件语句中不能使用import

if (!user) {
  import * as api from './api' // 只能在页面头部使用
}

require中可以使用变量,而import 中就不行

let b = "er";
let fileName = "./us" + b + ".js";
let user = require(fileName)

这个例子在require中正确。而在import中就错误

    let b = "er";
    let fileName = "./us" + b + ".js";
    import a from fileName;//报错!error!

import()

动态和异步对于动态引入只能用import().import()可以使用变量 也可以在分支中使用。import()返回的事一个promise,所以可以使用await来等待。

 const name = await import(`./util/${name}`);

说明

由于本人水平所限,难免有所疏漏。如果有错误之处,请评论,我会及时回复并修改。