聊聊关于js模块化规范的那些事儿~

285 阅读4分钟

这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

上一篇文章中,我们聊到了"史前时代"js是如何"模拟"模块化的,但本质上依旧是用“旁门左道”来实现的,并不是js语言层面的规范,要想真正实现模块化,肯定需要js自己本身去推出模块化相关的规范,因此这篇文章就准备聊聊这些规范,废话不多说,开搞!

ppx2.jpg

Commonjs

Nodejs 使用的就是Commonjs规范,主要通过提供module,exports,require来实现模块化,require方能看到的只有module.exports这个对象,它是看不到exports对象的,而我们在编写模块时用到的exports对象实际上只是对module.exports的引用,通常情况下更建议使用 module.exports 而不是 exports,该规范的用法如下:

    //a.js
    var name = 'test'
    
    module.exports = {
        name
    }

    //b.js
    var moduleA = require('./a.js')
    console.log(moduleA.name)

这种规范具有如下几个特点

  • 一个模块就是一个对象,是在 运行时 加载的,因此无法进行 静态分析
  • 由于无法进行静态分析,因此导出的内容是 全量 的,无法进行 tree-shaking
  • 加载模块是 同步的,因此只适合 服务端。因为服务端的资源都在本地,因此读取模块资源的速度很快,但是浏览器环境下资源都是 远程获取 的,因此不适合浏览器环境
  • 模块的导出是值拷贝的方式,意思就是变量的变化无法影响外部模块

AMD

AMD规范的主要践行者是 Requirejs,它主要通过 define,require来实现模块化,具体用法如下

    /** 网页中引入require.js及main.js **/
    <script src="js/require.js" data-main="js/main"></script>

    /** main.js 入口文件/主模块 **/
    // 首先用config()指定各模块路径和引用名
    require.config({
        baseUrl: "js/lib",
        paths: {
            "jquery": "jquery.min",  //实际路径为js/lib/jquery.min.js
            "test1": "test1",
            "test2": "test2"
        }
    });

    // 定义test1.js模块
    define(function () {
        return {
            name: 'test1'
        };
    });
    // 定义一个依赖test1.js的test2模块
    define(['test1'],function(test1){
        return {
            name: test1.name + 'test2'
        };
    })

    // 引用模块
    require(["jquery","test1","test2"],function($,test1,test2){
        // some code here
    });

该模块规范的特点如下:

  • 模块的导入是 异步的,因此适用于浏览器环境
  • 依赖的模块无法按需导入,只能一次性全部导入,也就是所谓的“依赖前置”

CMD

CMD吸取了AMD和cjs的优点,因此是一种相对前两者更好的模块化方案,Seajs 是该规范的主要践行者,具体用法如下

    //定义没有依赖的模块
    define(function(require, exports, module){
        exports.xxx = value
        module.exports = value
    })

    //定义有依赖的模块
    define(function(require, exports, module){
        //引入依赖模块(同步)
        var module2 = require('./module2')
        //引入依赖模块(异步)
        require.async('./module3', function (m3) {
        })
        if(false) {
            var moduleFalse = require('./moduleFalse') //该模块不会被导入
        }
        //暴露模块
        exports.xxx = value
    })





    // main.js文件
    define(function (require) {
        var m1 = require('./module1')
        var m4 = require('./module4')
        m1.show()
        m4.show()
    })
    //在index.html中引入
    <script type="text/javascript" src="js/libs/sea.js"></script>
    <script type="text/javascript">
        seajs.use('./js/modules/main')
    </script>

该模块规范有如下特点:

  • 由于模块的导入同时支持同步和异步两种方式,因此服务端和浏览器都适用
  • 支持模块的 按需导入,只有在真正需要使用依赖模块时才会去导入,也就是所谓的“就近依赖”,我认为这也是更合理的方案

ES module

ES module是 ES6 中提出来的模块化方案,旨在统一浏览器和服务器的模块化规范,也是直接从语言层面来规范js的模块化,所以未来ES module肯定是最终发展的方向,它的具体用法如下

    /*a.js*/
    export let a = 1
    export default {
        name:'yy'
    }


    /*b.js*/
    import {a},aModule from './a.js'
    console.log(a,aModule.name)

该规范有如下特点

  • 模块的导出只是一个 静态的接口定义,因此在静态分析阶段就能确定模块之间的依赖关系
  • 正是由于可以在静态分析阶段就能确定模块之间的依赖关系,所以天然支持tree-shaking
  • 因为导出的是静态接口的定义,因此看作导出的是变量的 引用,因此模块内部变量的变化会影响外部的引用,这也是跟cjs存在差异的地方
  • 由于在编译期就会导入模块的代码,因此不存在同步或者异步导入模块的说法
  • 未来会成为浏览器和服务器的通用模块化方案

结语

未来是 ES module 的天下,只不过现在还处在一个过渡的时期,因此有必要去了解除ES module外的规范,这样我们才能对js这门语言有更加深刻的认识,从而成为一个专业的前端工程师,所以,加油吧,骚年!