美文网首页笔试面试面试
JavaScript模块化(ES Module/CommonJS

JavaScript模块化(ES Module/CommonJS

作者: nymlc | 来源:发表于2018-03-22 08:53 被阅读0次

    1模块化历史

    1.1前言

    参照前端模块化开发的价值

    1.2无模块化

    每次说到JavaScript都会想到Brendan Eich花了十来天就发明了它,那就是JS的鸿蒙时期,混沌初开。
    就像当年在校初学前端时写的代码,没有那么多的套路,就是从上往下码代码,没有想着去声明函数神马的,甚至多少代码都写在一个JS里。现在想来真是惨不忍睹。虽然本人入坑前端距那个鸿蒙时代实在久远,但是据各种典籍记载,那时候的代码风格就差不多这样子,从上往下一直堆着就好了。

    var a = 0;
    if (xxx) {
      // 省略100L
    }
    document.getElementById('id').onclick = function(event) {
      // 省略若干行
    }
    ......
    

    1.3模块化冒泡

    每个行业都有梗,现在和同事聊天有时候还会吐槽十几年前的老网站,真是有幸见过。前辈的聊天更有意思了,当年的登录居然是写死在前端代码里,就像这样子

    if (username === 'xxxxx' && password === 'xxxxxx') {
      // 登录成功
    }
    

    是不是觉得很无语。当年的前端都是静态页面,没有现在这样子通过ajax和后端交互神马的,内容更是丰富多彩,更新及时。
    前端代码愈发庞大,那么自然而然会暴露很多问题。
    无非就俩个:

    • 命名冲突
    • 文件依赖

    1.3.1命名冲突解决

    1. java风格的namespace,这个很好理解,在此不赘诉,缺点的话,自行想象,不堪
    2. 自执行函数(内部变量不可见不被污染)
    // jQuery式的匿名自执行函数
    // 缺点是增添了全局变量、依赖需要提前提供
    (function(root) {
        root.jQuery = window.$ = jQuery; // 挂载到window之上
    })(window)
    
    // 普通自执行函数
    // 缺点就是暴露了全局变量,而且随着模块的增加,全局变量会很多
    module = function() {
        function module() {
    
        }
        return module;
    }()
    
    1. YUI3的沙箱机制,这个表示不晓得。

    1.3.2文件依赖解决

    这个没有解决方法,乖乖自行保证顺序和不缺漏吧

    <script src="https://cdn.bootcss.com/underscore.js/1.8.3/underscore-min.js"></script>
    <script src="https://cdn.bootcss.com/backbone.js/1.3.3/backbone-min.js"></script>
    

    1.4CommonJS

    1.4.1前言

    随着前端的发展,到node.js被创,js可以用来写server端代码。
    做过后端的同学肯定知道没有模块化怎么能忍呢?
    我们通过上节所得,我们可以得出以下几点需待解决

    • 模块代码安全,不可被污染也不可污染别人,沙箱呀
    • 把模块接口暴露出去(得优雅呀,不能增添全局变量)
    • 这个依赖顺序管理

    1.4.2发展

    这个还真没有经历了解过。度娘一番,幸好看到seajs下的一个issues
    大致就是大牛很牛,推出了Modules/1.0规范
    之后为了推广到浏览器端,大牛产生分歧,分为三大流派:

    • Modules/1.x 流派(Modules/Transport
      通过工具转换现有的CommonJS)
    • Modules/Async 流派(自立门派)
    • Modules/2.0 流(Modules/Wrappings
      对1.0的升级)

    这里说下为什么不能用在浏览器

    • 服务端代码在硬盘,加载模块时间几乎忽略不计。浏览器端就不成了。
    • 模块引用未被function,所以暴露在了全局之下。

    1.4.3番外(AMD、CMD)

    AMD是 RequireJS 在推广过程中对模块定义的规范化产出。
    CMD是 SeaJS 在推广过程中对模块定义的规范化产出。

    1.5ES Module

    这个是ECMA搞得一套。和之前的区别在于人家是官方的,根正苗红,上文的是社区搞得,野生。

    2 ES Module/CommonJS/AMD/CMD差异

    2.1 ES Module与CommonJS的差异

    编译时和运行时

    首先说下编译时和运行时。JavaScript有俩种声明方法(声明变量和声明方法)。var/const/let和function
    编译时,对于声明变量会在内存中开辟一块内存空间并指向变量名,且指向变量名,赋值为undefined。对于函数声明会一样的开启空间。不过赋值为声明的函数体。PS:无论顺序如何,都会先声明变量
    运行时,执行变量初始化之类的。

    // 源码
    var a = 3;
    function f() {
        return 'f';
    }
    
    // 编译时
    var a = undefined;
    var f = function() {
        return 'f';
    }
    // 运行时
    a = 3;
    

    CommonJS模块是对象,是运行时加载,运行时才把模块挂载在exports之上(加载整个模块的所有),加载模块其实就是查找对象属性。
    ES Module不是对象,是使用export显示指定输出,再通过import输入。此法为编译时加载,编译时遇到import就会生成一个只读引用。等到运行时就会根据此引用去被加载的模块取值。所以不会加载模块所有方法,仅取所需。

    • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
    • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
      详情参见

    2.2CommonJS与AMD/CMD的差异

    AMD/CMD是CommonJS在浏览器端的解决方案。CommonJS是同步加载(代码在本地,加载时间基本等于硬盘读取时间)。AMD/CMD是异步加载(浏览器必须这么干,代码在服务端)

    2.3AMD与CMD的差异

    • AMD是提前执行(RequireJS2.0开始支持延迟执行,不过只是支持写法,实际上还是会提前执行),CMD是延迟执行
    • AMD推荐依赖前置,CMD推荐依赖就近

    3 用法

    3.1 CommonJS

    // 导出使用module.exports,也可以exports。exports指向module.exports;即exports = module.exports
    // 就是在此对象上挂属性
    // commonjs
    module.exports.add = function add(params) {
        return ++params;
    }
    exports.sub = function sub(params) {
        return --params;
    }
    
    // 加载模块使用require('xxx')。相对、绝对路径均可。默认引用js,可以不写.js后缀
    // index.js
    var common = require('./commonjs');
    console.log(common.sub(1));
    console.log(common.add(1));
    

    3.2 AMD/RequireJS

    • 定义模块:define(id?, dependencies?, factory)
    • 加载模块:require([module], factory)
    // a.js
    // 依赖有三个默认的,即"require", "exports", "module"。顺序个数均可视情况
    // 如果忽略则factory默认此三个传入参数
    // id一般是不传的,默认是文件名
    define(["b", "require", "exports"], function(b, require, exports) {
        console.log("a.js执行");
        console.log(b);
    // 暴露api可以使用exports、module.exports、return
        exports.a = function() {
            return require("b");
        }
    })
    // b.js
    define(function() {
        console.log('b.js执行');
        console.log(require);
        console.log(exports);
        console.log(module);
        return 'b';
    })
    // index.js
    // 支持Modules/Wrappings写法,注意dependencies得是空的,且factory参数不可空
    define(function(require, exports, module) {
        console.log('index.js执行');
        var a = require('a');
        var b = require('b');
    })
    // index.js
    require(['a', 'b'], function(a, b) {
        console.log('index.js执行');
    })
    

    3.3 CMD/SeaJS

    SeaJS平时没有到,不过了解了下,丰富用法看CMD定义规范

    • 定义模块:define(factory)
    // a.js
    // require, exports, module参数顺序不可乱
    // 暴露api方法可以使用exports、module.exports、return
    // 与requirejs不同的是,若是未暴露,则返回{},requirejs返回undefined
    define(function(require, exports, module) {
        console.log('a.js执行');
        console.log(require);
        console.log(exports);
        console.log(module);
    })
    // b.js
    // 
    define(function(require, module, exports) {
        console.log('b.js执行');
        console.log(require);
        console.log(exports);
        console.log(module);
    })
    // index.js
    define(function(require) {
        var a = require('a');
        var b = require('b');
        console.log(a);
        console.log(b);
    })
    

    定义模块无需列依赖,它会调用factory的toString方法对其进行正则匹配以此分析依赖。预先下载,延迟执行

    3.4 ES Module

    输出/export

    // 报错1
    export 1;
    // 报错2
    const m = 1;
    export m;
    
    // 接口名与模块内部变量之间,建立了一一对应的关系
    // 写法1
    export const m = 1;
    // 写法2
    const m = 1;
    export { m };
    // 写法3
    const m = 1;
    export { m as module };
    

    PS:这个有点不是很明白,大致理解就是不能直接导出变量,但是可以导出声明(函数、变量声明)。这里的接口理解是export之后的变量,它和变量建立了映射关系。总的而言,export之后只能接声明或者语句

    输入/import

    基本用法

    // 类似于对象解构
    // module.js
    export const m = 1;
    // index.js
    // 注意,这里的m得和被加载的模块输出的接口名对应
    import { m } from './module';
    // 若是想为输入的变量取名
    import { m as m1 }  './module';
    // 值得注意的是,import是编译阶段,所以不能动态加载,比如下面写法是错误的。因为'a' + 'b'在运行阶段才能取到值,运行阶段在编译阶段之后
    import { 'a' + 'b' } from './module';
    // 若是只是想运行被加载的模块,如下
    // 值得注意的是,即使加载两次也只是运行一次
    import './module';
    // 整体加载
    import * as module from './module';
    

    PS:CommonJS和ES Module是可以写一起的,但是最好不要。毕竟一个是编译阶段一个是运行阶段。就在项目中入过坑,自行体会。

    赋值

    首先输入的模块变量是不可重新赋值的,它只是个可读引用,不过却可以改写属性

    // 单例
    // module.js
    export const a = {};
    // module2.js
    export { a } from './module';
    import { a as a1 } from './module';
    import { a } from './module2';
    a1.e = 3;
    console.log(a1) // { e: 3 }
    console.log(a) // { e: 3 }
    

    输出/export default

    // module.js
    // 其实export default就是export { xxx as default }
    const m = 1;
    export default m;
    ===
    export { m as default }
    // index.js
    // 对应的输入也得相应变化
    import module from './module';
    ===
    import { default as module } from './module';
    

    还记得之前export小结处的俩报错么?如下写法正确,因为提供了default接口

    // 写法1
    export default 1;
    // 写法2
    const m = 1;
    export default m;
    // 错误写法
    export default const m = 1;
    

    PS:export default只能一次

    复合写法

    可用于模块间继承。比如在a模块写下如下,那么a模块不就有了./module的方法了

    export { a } from './module';
    export { a as a1 } from './module';
    export * from './module';
    

    动态加载/import()

    因为编译时加载,所以不能动态加载模块。不过幸好有import()方法。
    这家伙返回值是一个Promise对象,所以then、catch你开心就好

    // 普通写法
    import('./module').then(({ a }) => {})
    // async、await
    const { a } = await import('./module');
    

    4 番外自实现

    var MyModules = (function(){
        var modules = [];
        function define(name, deps, cb) {
            deps.forEach(function(dep, i) {
                deps[i] = modules[dep];
            });
            modules[name] = cb.apply(cb, deps);
        }
        function get(name) {
            return modules[name];
        }
        return {
            define: define,
            get: get
        };
    })();
    MyModules.define('add', [], function() {
        return function(a, b) {
                return a + b;
            };
    })
    MyModules.define('foo', ['add'], function(add) {
        var a = 3;
        var b = 4;
        return {
            doSomething: function() {
                return add(a, b) + a;;
            }
        };
    })
    var add = MyModules.get('add');
    var foo = MyModules.get('foo');
    console.log(add(1, 2));
    console.log(foo.doSomething());
    

    相关文章

      网友评论

        本文标题:JavaScript模块化(ES Module/CommonJS

        本文链接:https://www.haomeiwen.com/subject/lrjjqftx.html