JS 模块化

151 阅读4分钟

本文根据JS的发展历程做记录

背景

JS定位: 简单的页面设计 = 页面动画 + 基本的表单提交, 没有模块化和空间命名概念。

幼年期-无模块化(类模块化)

场景1:开始需要在页面加载不同的js,动画库,表单库,格式化工具。 场景2:多种JS文件被分在不同的文件中。 场景3:不同的文件又被同一个模块引用。

<script src="jquery.js"></script>
<script src="tools.js"></script>
<script src="main.js"></script>
上面的加载顺序是 同步加载
问题: 如果上面的JS文件,每个都有全家变量,而且有些全局变量名一样,就会冲突,然后被覆盖。

文件分离是最基础的模块化,类模块化的转变。 问题: script标签两个参数 async & defer

mean.png

<script src="jquery.js" type="text/javascript" async></script>

总结: 普通(normal) - 解析到标签,立刻pending,并且下载且执行 defer - 解析到标签开始异步下载,继续解析(渲染的意思)完成后开始执行 async - 解析到标签开始异步下载,下载完成之后立刻执行并且阻塞解析,执行完成后,再继续解析

  1. 兼容性 => IE9 + 兼容程度不同
  2. 问题方向 => 浏览器渲染原理(界面渲染线程,调用线程。。。)、同步异步原理(宏微任务,事件队列...)、模块化加载原理。
  3. 问题出现:
  • 污染全局作用域 => 不利于大型项目的开发以及多人团队共建

成长期:模块化的雏形 - 语法基础 IIFE(立即执行函数)

使用立即执行函数做作用域

<script src="jquery.js"></script>
<script src="tools.js"></script>
//  const increase = () => ++count;
<script src="main.js"></script>
//  const reset = () => { count = 0;}
increase函数是tools.js里的函数,reset是main.js的函数。

<script>
 // 全局变量
    let count = 0;
    // 代码块1
    const increase = () => ++count;
    // 代码块2
    const reset = () => {
        count = 0;
    }
    increase();
    reset();
</script>


问题: 两个函数共用一个count,count是全局变量了。
解决:利用函数块级作用域
定义函数 + 立即执行 => 独立的空间
初步实现了一个最最最最最最最简单的模块
    (() => {
        let count = 0;
        //……
    })();

尝试定义一个最简单的模块 - 模块 and 外部(解决模块和外部的联系)

要实现内部的功能,又要杜绝外部的干扰 : IIFEmodule
 // iifemodule
    const module = (() => {
        let count = 0;
        return {
            increase: () => ++count;
            reset: () => {
                count = 0;
            }
        }
    })();
用module.xxx 使用函数
    module.increase();
    module.reset();

** 追问:有额外依赖的时候,如何优化处理IIFE **

优化1:依赖其他的IIFE

    const iifeModule = ((dependecyModule1, dependecyModule2) => {
        let count = 0;
        // dependecyModule……
        return {
            increase: () => ++count;
            reset: () => {
                count = 0;
            }
        }
    })(dependecyModule1, dependecyModule2);

追问2:了解早期jQuery依赖处理/模块加载的方案么? / 了解传统IIFE是如何解决多方依赖的问题么? IIFE + 传参调配

实际上,传统框架应用了一种revealing的写法 揭示模式

    const iifeModule = ((dependencyModule1, dependencyModule2) => {
        let count = 0;
        const increase = () => ++count;
        const reset = val => {
            count = 0;
            // dependencyModule
        }

        return {
            increase,
            reset
        }
    })(dependencyModule1, dependencyModule2);
    iifeModule.increse(1);
    iifeModule.reset();
    // 返回的是一种能力 = 使用方传参 + 本身逻辑能力 + 依赖能力
    // $('').attr('title', 'new');
  • 追问: 面试中后续引导的方向:
  1. 深入模块化实现 2. 转向框架:jquery、vue、react的模块化实现的细节,以及框架的特征原理 3. 转向设计模式 - 注重模块化的设计模式

成熟期 (这一块要理解,比如amd和cmd他们是怎么加载的...)

CJS - Commonjs

node.js指定的标准 特征:

  • 通过module + export 去对外暴露接口
  • 通过require去调用其他模块

模块组织方式: dep.js

    // 引入部分
    const dependecyModule1 = require(./dependecyModule1);
    const dependecyModule2 = require(./dependecyModule2);

    // 核心逻辑
    let count = 0;
    const increase = () => ++count;
    const reset = val => {
        // …… 
    }

    // 暴露接口部分
    export.increase = increase;
    export.reset = reset;

    module.exports = {
        increase,
        reset
    }
    const { increase, reset } = require('dep.js');

    increase();
    reset();

** 可能会被问到的问题 ** 实际执行处理

    (function(thisValue, export, module) {
        const dependecyModule1 = require(./dependecyModule1);
        const dependecyModule2 = require(./dependecyModule2); 
        // 业务逻辑
    }).call(thisValue, exports, require, module)

    // 部分开源项目分别传入全局、指针、框架做参数
    (function(window, $, undefined) {
        const _show = function () {}
    })(window, jQuery);
    // window - 1. 全局作用域改成了局部作用域,执行的时候不用全局调用(作为模块文件,不要用全局的变量,因为不知道会在哪个环境下运行),提升效率 (能传进来,参数肯定是这个文件需要的参数,校验过了)
    2. 编译时候优化压缩 (function(c){}(window))
    // jQuery - 1. 独立进行改动挂载,保障稳定 2. 防止全局污染
    // undefined - 1. 防止重写
  • 优点: CJS服务侧角度解决了依赖全局污染的问题
  • 缺憾: 针对服务端

新的问题 - 异步依赖

AMD

通过异步加载 + 允许定制回调函数 经典实现框架: require.js

新增定义方式:

    // 通过define来定义一个模块,然后用require去加载
    define(id, [depends], callback);
    require([module], callback);
    define('amdModule', ['dependecyModule1', 'dependecyModule2'], (dependecyModule1,
    dependecyModule2) => {
        // 业务逻辑
        let count = 0;
        const increase = () => ++count;
        const reset = val => {
            count = 0;
            // dependecyModule1……
        }
    })
    require(['amdModule'], amdModule => {
        amdModule.increase();
    })

** 面试题:如何对已有代码做兼容

  1. 增加定义阶段
  2. 返回作为回调内部的return
    define('amdModule', [], require => {
        // 引入部分
        const dependecyModule1 = require(./dependecyModule1);
        const dependecyModule2 = require(./dependecyModule2);

        // 核心逻辑
        let count = 0;
        const increase = () => ++count;
        const reset = val => {
            // …… 
        }

        // 暴露接口部分
        export.increase = increase;
        export.reset = reset;

        return {
            increase,
            reset
        }
    })

** 面试题3: 兼容判断AMD&CJS - 区分 UMD的出现

    (define('amdModule', ['dependencyModule1'], require => {
        // 核心逻辑
        let count = 0;
        const increase = () => ++count;
        const reset = val => {
            // …… 
        }

        // 暴露接口部分
        export.increase = increase;
        export.reset = reset;

        return {
            increase,
            reset
        }
    }))(
        // 目标: 一次性去区分CJS和AMD
        // 1. CJS factory
        // 2. module module.exports
        // 3. define
        typeof module === 'Object'
        && module.exports
        && typeof define !== "function"
            ? // 是CJS
                factory => module.exports => factory(require, exports, module)
            : // 是AMD
                define
    )
  • 优点: 解决了浏览器的异步动态依赖
  • 缺点:会有引入成本,没有考虑按需加载
CMD规范

按需加载 主要应用框架 sea.js

    // 依赖就近
    define('module', (require, exports, module) => {
        let $ = require('jquery');
        // ……………………

        let depends1 = require('./dependencyModule1');
    })
  • 优点:按需加载,依赖就近
  • 缺憾:1. 依赖于打包 2. 扩大了模块内的体积

区别:按需加载、依赖就近

ESM

走进新时代

新增定义: 引入关键字 - import 导出关键字 - export

    // 引入区域
    import dependencyModule1 from './dependencyModule1.js';

    // 逻辑处理区域
    // …… 

    export default {
        increase, reset
    }

** 追问: ESM动态模块 ** 考察:export promise

ES11原生地解决方案

    import('./esModule.js').then(dynamicEsModule => {
        dynamicEsModule.increase();
    })
  • 优点 通过一种最统一的形态整合了所有js的模块化