重新看前端模块化

836 阅读5分钟

萌芽于面试题:importrequire 的区别?

雏形

入门学习的时候,知道了 function 的语法,声明一块代码片段(code snippet),简单来说就是代码复用

// index.js
function a() {
    // 100 行逻辑
}
function b() {
    // 100 行逻辑
}

a、b 可以视为 2 个模块。如果模块越来越多的话,直接约定一些 js 文件即可。目录结构类似这样:

|-- modules
|   |-- a.js
|   |-- b.js
|   |-- ... // 更多的模块

使用时 script 标签引入。模块日益增多,假设有这样的问题:

  • 文件依赖。一个模块依赖另一个,使用者并不知道或者忘记引入
  • 命名冲突。创建新模块时,使用的变量名覆盖了另一个

一些解决办法

只列举百度到的、稍简易的两个办法:命名空间、IIFE(自执行函数)

命名空间

参考其它语言的成熟方案是种很好的办法,毕竟 js 就是直接借鉴 JAVA。很多语言都有 namespace 的概念:

// namespace
var org = {},
    org.meta = {},
    org.meta.com = {},
    org.meta.com.utils = {};

org.meta.com.utils.a = function a() {
    // 100 行代码
};

根据资料显示,命名空间被当时的前端业界标杆 YUI 采用。讲道理,这框架都没用过。。。

IIFE

IIFE 是 js 中函数立即执行的写法,模块化的关键在于其内部作用域:

(function (global) {
    function a() {
        // 100 行代码
    }

    global.a = a;
})(window); // 传入可以是任一对象

写 jQuery 的插件就类似这样,熟悉的代码:

/**
 * 这个模块有这样的用法
 * ...
 * create by xxx
 * create at xxxx
 */
(function ($) {
    function A() {
        // 100 行代码
    }

    $.fn.A = A;
})(jQuery);

成熟的方案

模块化的规范当属 AMD 与 CMD,对应的实现方式分别是 require.jssea.js。国内用的较多的是 sea.js

还记得实习的时候,老一辈的前端(10年开始从业)一直在唏嘘 jQuerysea.js 有多火,现在都是三大框架和 es6 了。

AMD - require.js

require.js 核心 API 有两个,定义模块与引入模块:

// module/a.js
// 定义模块
define('module/a', function () {
    var name = 'tao';
    function say() {
        alert(name);
    }

    // 暴露出去的模块
    return {
        name,
        say,
    };
});

还需要一些约定配置来指明模块路径等:

// require.config.js
require.config({
    baseUrl: './',
    paths: {
        'module/a': './module/a.js',
    },
});

使用时 require.js 会自动引入需要的模块,通过 script 标签加载,这个过程是异步的。

<script data-main="require.config.js" src="require.js"></script>
<script>
    require(['module/a'], function (moduleA) {
        // 这里被执行时会看到多了个 script 标签
        moduleA.say(); // alert('tao')
    });
</script>

CMD - sea.js

万变不离其宗,sea.js 的作者玉伯写的 issue 文章可以说的上是很清楚了,中文对于模块化的诠释基本无出其右。

sea.js 推崇单文件模块:

// module/a.js
// 定义模块
define(function (require, exports, module) {
    // 依赖
    var moduleDep = require('./dep');

    // 100 行代码

    // 两种暴露模块的方式
    exports.a = a;
    module.exports = { a };
});

它同样也有配置可以简化路径、方便记忆:

<script src="sea.js"></script>
<script>
    seajs.config({
        base: './module/',
        alias: {
            'module/a': 'a.js',
            'main': '../../src/index.js',
        },
        // debug: true,
    });

    // 使用时需要借助 `seajs.use`
    seajs.use('main');
</script>

官方示例中,将入口文件也定义成了个模块,seajs.use 使用。

其实 requireseajs.use 的过程也是异步的,实在不懂为啥两个规范叫法不一样 😄

约定成俗到标准~终焉

当服务端 nodejs 使用的人越来越多,其模块化方案 CommonJS 逐渐约定成俗:

var path = require('path'); // 内部模块
// 引入
var moduleA = require('a');

// 导出
module.exports = {
    a: xxx,
};

node 应用因为跑在服务器上,加载模块都是同步的,模块文件都在磁盘上,读取时间忽略不计。但是浏览器里完全不一样,网站应用不可能等模块都下载完后再展示给用户,异步貌似是必然的,AMD/CMD 应运而生。

ECMAScript 6 标准的发布,终于给模块化划上了一个句号:

// 引入
import moduleA from '../module/a.js';

// 导出
export a;
export default b;

真正的浏览器用法,需要使用 module 类型的 script:

<script type="module">
    import moduleA from './module/a.js';

    if (true) {
        // dynamic import
        import('./module/b.js').then(({ default: moduleB }) => {
            console.log(moduleB);
        });
    }

    // a.js 和 b.js 会被浏览器自动加载,而不是通过 script 标签
</script>

说到最后,importrequire 的区别,其实就是 ES6 import 和 CommonJS 的区别。

FAQ

importrequire 的区别

  1. import 引入的是值的引用;而 require 引入的是值的拷贝
  2. import 是静态的、语言层面上的,在编译时就能确定依赖关系;而 require 只有在运行时才能

循环引用

某天,你看到了这样的代码:

// getApp.js
import fetch from './utils/request';

const app =  {
    /** 全局常量配置 */
    CONSTATNS: {
        method: 'GET',
    },
    fetch,
};

export default function getApp() {
    return app;
}
// utils/request.js
import getApp from '../getApp';

// 因为循环引用,这里拿到的全局对象 app 是个 undefined
const app = getApp();
// 解构赋值报错了
const { method } = app.CONSTANTS; // TypeError undefined

export default function request() {
    fetch(url, {
        method,
    });
};

想要解决上面报错的问题不难,只要延迟调用 getApp 获取配置就行了,比如:

setTimeout(() => {
    console.log(getApp()); // app 对象
});

But 循环引用还存在,并木有从根本上解决,以后仍会导致某些隐性问题(假想😂)。遂百度一波流,get 到面向对象设计原则之一 Dependency Inversion Principle:

  • High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g. interfaces).
  • Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

大意貌似是指各模块都应该依赖于抽象,这很面向对象。

在 js 的世界中,抽象太抽象了,也不知道怎么说,或者更应该理解为抽象模块,它不会具体做什么,只为降低高低模块耦合性而生。回到上面的问题,尝试写一个抽象模块:

// abstraction.js
/**
 * 耦合点就是请求方法,因此,把 `method` 放到
 * 抽象模块中。
 *
 * 实际场景更加复杂。。。
 */
let fetchMethod;

export default function getFetchMethod() {
    return fetchMethod;
}
export function setFetchMethod(method) {
    fetchMethod = method;
}

接着在两个模块中引入此抽象模块:

// getApp.js
import { setFetchMethod } from './abstraction';

setFetchMethod(app.CONSTATNS.method);

// utils/request.js
import getFetchMethod from './abstraction';

const method = getFetchMethod();
fetch(url, {
    method,
});

感觉有点麻烦,干嘛不把 CONSTANTS 抽成个模块呢。。。The end~

参考链接

  1. 前端模块化开发那点历史
  2. 前端模块化开发的价值
  3. 前端模块化详解(完整版)
  4. require 和 import 区别
  5. Dependency inversion principle