前端模块化

前端模块化

背景

模块化是大前端发展的必然趋势,模块化的好处是能把多个复杂的抽象的功能模块依据一定的规范和规则封装成单独块并进行组合。模块内的数据和实现都是私有的,因此不必担心污染的问题,它们只是对外提供一些接口方法和其他模块进行通信。目前比较流行的就是 AMD, CMD, CommonJS, ES6。

  • 优点:

    1. 降低命名空间污染

    2. 各自独立,方便按需加载

    3. 提高复用性

    4. 易于维护和扩展

模块化进化史

全局 function 函数

  • 描述:不同功能封装成不同的函数

  • 优点:抽象了公共功能

  • 缺点:污染全局命名空间,所以的 function 都是挂在 window 下的

function a() {}
function b() {}
function c() {}
//...

namespace 空间模式

  • 描述:使用对象进行封装方法属性

  • 优点:减少了全局的变量,降低命名污染的几率

  • 缺点:方法直接暴露到外部,并且外部可以直接对内部属性进行修改,安全系数低

let myModule = {
    data: 'www.baidu.com',
    foo() {
        console.log(`foo() ${this.data}`);
    },
    bar() {
        console.log(`bar() ${this.data}`);
    }
};
myModule.data = 'other data'; //能直接修改模块内部的数据
myModule.foo(); // foo() other data

IIFE 立即执行函数表达式

  • 描述:通过创建 IIFE(Immediately Invoked Function Expression)配合闭包创建一个私有作用域的空间并把部分方法暴露出去

  • 优点:数据是私有的,作用域独立互不干扰,外部只能通过暴露出去的方法进行调用

  • 缺点:模块之间互相不能依赖,各自独立

// module.js文件
(function(window) {
    let data = 'www.baidu.com';
    //操作数据的函数
    function foo() {
        //用于暴露有函数
        console.log(`foo() ${data}`);
    }
    function bar() {
        //用于暴露有函数
        console.log(`bar() ${data}`);
        otherFun(); //内部调用
    }
    function otherFun() {
        //内部私有的函数
        console.log('otherFun()');
    }
    //暴露行为
    window.myModule = { foo, bar }; //ES6写法
})(window);

使用:

<!-- index.html文件 -->
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
    myModule.foo();
    myModule.bar();
    console.log(myModule.data); //undefined 不能访问模块内部数据
    myModule.data = 'xxxx'; //不是修改的模块内部的data
    myModule.foo(); //没有改变
</script>

IIFE 引入依赖(经典)

  • 描述:和上一条用的相同的实现方式,此项是奠定当前前端模块化的基石,通过参数引入其他模块创建了模块和模块之间的依赖关系链条

  • 优点:既保证了模块的独立性,也让模块之间能够互相依赖

  • 缺点:

    • 请求多

    • 依赖模糊(不清楚依赖关系导致加载顺序错误)

    • 难维护

// module.js文件
(function(window, $) {
    let data = 'www.baidu.com';
    //操作数据的函数
    function foo() {
        //用于暴露有函数
        console.log(`foo() ${data}`);
        $('body').css('background', 'red');
    }
    function bar() {
        //用于暴露有函数
        console.log(`bar() ${data}`);
        otherFun(); //内部调用
    }
    function otherFun() {
        //内部私有的函数
        console.log('otherFun()');
    }
    //暴露行为
    window.myModule = { foo, bar };
})(window, jQuery);

使用:

// index.html文件
<!-- 引入的js必须有一定顺序 -->
<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
    myModule.foo();
</script>

CommonJS

CommonJS 特点

  • 第一次运行,后面缓存(如果需要再次运行需要清除缓存)

  • 同步执行

    ,和代码顺序有关系

  • CommonJS 模块的加载机制是,输入的是被输出的值的拷贝。

CommonJS 基本语法

  • 暴露模块:module.exports = value / exports.* = value

  • 引入模块:require(xxx),如果是第三方模块,xxx 为模块名;如果是自定义模块,xxx 为模块文件路径 加载某个模块其实就是加载对应的 module.exports 属性

CommonJS 实例

// lib.js
var counter = 3;
function incCounter() {
    counter++;
}
module.exports = {
    counter: counter,
    incCounter: incCounter
};
// main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;

console.log(counter); // 3
incCounter();
console.log(counter); // 3

AMD(Asynchronous Module Definition,异步模块定义)

AMD 特点

  • 非同步模式

  • 允许依赖

AMD 基本语法

  • 定义模块

//定义没有依赖的模块
define(function() {
    return 模块;
});
//定义有依赖的模块
define(['module1', 'module2'], function(m1, m2) {
    return 模块;
});
  • 引入模块

require(['module1', 'module2'], function(m1, m2) {
    使用m1 / m2;
});

AMD 实例

使用基于 AMD 规范的

RequireJS

库进行模块管理:

// moduleA.js
// 定义无依赖的模块
define(function(){
    function getData(){
        return { data: /*data*/ }
    }
    return { getData }
})
// moduleB.js
// 定义有依赖的模块
define(['module_a', 'jquery'], function(ma, $) {
    function formatData() {
        return format(ma.data);
    }
    return { formatData };
});
// main.js
(function() {
    require.config({
        baseUrl: 'js/', //基本路径 出发点在根目录下
        paths: {
            // 注意:此处路径不能写成moduleA.js,会报错
            module_a: './modules/moduleA',
            module_b: './modules/moduleB',
            // 第三方库模块
            //注意:写成jQuery会报错(内部导出为'jquery')
            // ref: https://www.tuicool.com/articles/jam2Anv
            jquery: './libs/jquery-1.10.1'
        }
    });
    require('module_b', function(mb) {
        alert(mb.formatData);
    });
})();

使用:

<!DOCTYPE html>
<html>
    <head>
        <title>Modular Demo</title>
    </head>
    <body>
        <!-- 引入require.js并指定js主文件的入口 -->
        <script data-main="js/main" src="js/libs/require.js"></script>
    </body>
</html>

CMD(Common Module Definition,通用模块定义)

CMD 特点

  • 模块的加载是异步的,模块使用时才会加载执行

  • 允许依赖

CMD 基本语法

// 定义无依赖模块
define(function(require, exports, module){
    exports.*** = value
    module.exports = value
})
// 定义有依赖模块
define(function(require, exports, module) {
    //引入依赖模块(同步)
    var module2 = require('./module2');
    //引入依赖模块(异步)
    require.async('./module3', function(m3) {});
    //暴露模块
    exports.xxx = value;
    module.exports = value;
});
// 引用模块
define(function(require) {
    var m1 = require('./module1');
    var m4 = require('./module4');
    m1.show();
    m4.show();
});

CMD 实例

在 Sea.js 中,所有 JavaScript 模块都遵循 CMD 模块定义规范。

// module1.js文件
define(function(require, exports, module) {
    //内部变量数据
    var data = 'atguigu.com';
    //内部函数
    function show() {
        console.log('module1 show() ' + data);
    }
    //向外暴露
    exports.show = show;
});
// module2.js文件
define(function(require, exports, module) {
    module.exports = {
        msg: 'I Will Back'
    };
});
// module3.js文件
define(function(require, exports, module) {
    const API_KEY = 'abc123';
    exports.API_KEY = API_KEY;
});
// module4.js文件
define(function(require, exports, module) {
    //引入依赖模块(同步)
    var module2 = require('./module2');
    function show() {
        console.log('module4 show() ' + module2.msg);
    }
    exports.show = show;
    //引入依赖模块(异步)
    require.async('./module3', function(m3) {
        console.log('异步引入依赖模块3  ' + m3.API_KEY);
    });
});

使用:

<script type="text/javascript" src="js/libs/sea.js"></script>
<script type="text/javascript">
    seajs.use(['module4.js'], function(data) {
        // ...
    });
</script>

ES6

ES6 特点

  • 编译时就能确定依赖关系(CommonJS, AMD, CMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性)

  • 静态化(import()语句可以在代码块中实现异步动态按需动态加载)

  • 只能在模块的顶层,不能在代码块之中(如:if 语句中)

  • 使用 import 导入的变量是只读的,无法被赋值,而且是引用传递

ES6 基本语法

export 只支持对象形式导出,不支持值的导出,export default 命令用于指定模块的默认输出,只支持值导出,但是只能指定一个,本质上它就是输出一个叫做 default 的变量或方法。

// 定义模块
/*错误的写法*/
// 写法一
export 1;
// 写法二
var m = 1;
export m;
// 写法三
if (x === 2) {
  import MyModual from './myModual';
}

/*正确的三种写法*/
// 写法一
export var m = 1;
// 写法二
var m = 1;
export { m };
// 写法三
var n = 1;
export { n as m };
// 写法四
var n = 1;
export default n;
// 引入模块(必须在最开始引入)
// 默认模块
import Fun from './module';
// 指定模块
import { setData, KEY, VALUE } from './module';
// 引入第三方库
import $ from 'jquery';

Fun(); // 'default'

使用: 在 html 中通过 babel(包含 COMMONJS 的语法)& browserify(编译 COMMONJS 语法)编译后引入执行。e.g.

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

ES6 import 动态引入

  • import()语句可以在代码块中实现异步动态按需动态加载(import('模块路径').then()):

if (true) {
    import('./myModule.js')
    .then(({export1, export2}) => {
      // ...·
    });
}

Promise.all([
  import('./module1.js'),
  import('./module2.js'),
  import('./module3.js'),
])
.then(([module1, module2, module3]) => {
   ···
});

兼容性: image

ES6 和 CommonJS 的区别

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

// 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
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

    • 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为"运行时加载"。

    • 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import 时采用静态命令的形式。即在 import 时可以指定加载某个输出值,而不是加载整个模块,这种加载称为"编译时加载"

对比

方案

特点

场景

实例

CommonJS

同步执行;输出拷贝;运行时确定依赖

(主要)服务端开发;打包构建

服务端:node;浏览器:browserify 转码

ES6

值的引用;编译时确定依赖

浏览器&服务端

browserify&babel 转码后再 html 中引入

AMD

异步执行;依赖前置,提前执行(RequireJS 2.0 开始也可以延迟执行);运行时确定依赖

浏览器端

RequireJS

CMD

异步执行;就近依赖,延迟执行;运行时确定依赖

浏览器端

浏览器:Sea.js;服务端:SPM 打包(node.js)

UMD(Universal Module Definition)

是 AMD+CommonJS+全局变量的结合。

UMD 先判断是否支持 Node.js 的模块(exports)是否存在,存在则使用 Node.js 模块模式。再判断是否支持 AMD(define 是否存在),存在则使用 AMD 方式加载模块。有了 UMD 后我们的代码和同时运行在 Node 和 浏览器上,所以现在前端大多数的库最后打包都使用的是 UMD 规范。

(function(window, factory) {
    if (typeof exports === 'object') {
        //Node, CommonJS之类的
        module.exports = factiory(require('jquery'));
    } else if (typeof define === 'function' && define.amd) {
        //AMD
        define(['jquery'], factory);
    } else {
        //浏览器全局变量(root 即 window)
        window.eventUtil = factory(window.jQuery);
    }
})(this, function($) {
    // module ...
    // 方法模块
    function module() {}
    // 暴露公共模块
    return module;
});

模块引用动态路径

  • CommonJS:

    • node 环境支持require('./'+path) / require(`./${path}`)

    • 浏览器端不支持

  • ES6:

    • 支持动态import(`./${path}`).then

ref

Last updated

Was this helpful?