【JavaScript】模块化

83 阅读6分钟

JS模块化

背景

  • JS本身定位:简单的页面设计-页面动画+基本的表单提交。
  • 并无模块化或者命名空间的概念。
  • 随着JS的模块化日益增长,逐渐发展出了页面模块化。

幼年期

无模块化(委婉的辩解)

  • 开始需要在页面中增加一些不同的js:动画、表单、格式化工具
  • 多种JS文件被分在不同的文件中
  • 不同的文件又被同一个模板所引用
 // test.html
<script src="jquery.js"></script>
<script src="main.js"></script>
<script src="tool.js"></script>
  • 文件分离是最基础的模块化

出现的问题: 污染全局作用域 ,每一个模块都是暴露在全局的,协调每一个模块变量函数名称都不可以相同, 不利于大型项目的开发以及多人团队共建

script标签的另外两个参数-async & defer
-defer解析到script标签开始异步下载,解析完成后开始执行
-async会异步下载js代码并执行

总结:
   普通情况:解析到script标签,立刻pending,并且下载执行。
   defer:解析到script标签开始异步下载,解析完成后开始执行。
   async:解析到script标签开始异步下载,下载完成后立刻执行并阻塞渲染,执行完成后,  继续渲染。

成长期

模块化的雏形——IIFE(立即执行函数)

作用域的把控

  • 例子:
// 定义一个全局变量
let count = 0;

// 代码块1
const increase = () => count++;

// 代码块2
const reset = () => {
   count = 0;
}

increase();
reset();
  • 利用函数作用域限制
// 仅仅定义了一个函数,但里面的代码并没有执行,如何能够对齐原来的逻辑呢?
(() => {
  let count = 0;
  // ……
})();
  • 尝试定义一个简单的模块
 const iifeCounterModule = (() => {
      let count = 0;
      return {
          increase: () => ++count;
          reset: () => {
              count = 0;
              console.log('hahaha count is reset');
          }
      }
  })();

  iifeCounterModule.increase();
  iifeCounterModule.reset();
  //完成了一个模块的封装,实现了对外暴露功能,保留变量 + 不污染全局作用域

如果有其他的依赖,如何处理?

优化1:依赖其他模块的IIFE

 const iifeCounterModule = ((depModule1, depModule2) => {
    let count = 0;
    // dependencyModule做处理
    return {
        increase: () => ++count;
        reset: () => {
            count = 0;
            console.log('hahaha count is reset');
        }
    }
})(depModule1, depModule2);

面试题1:了解早期jquery的依赖处理以及模块加载方案吗?

答:IIFE(立即执行函数表达式)+传参调配,实际上传统框架利用一种揭示模式写法。

实际书写上,jquery等框架实际应用涉及到revealling的写法

 const revealingCounterModule = (() => {
    let count = 0;
    const increase = () => ++count;
    const reset = () => {
        count = 0;
        console.log('hahaha count is reset');
    };

    return {
        increase,
        reset
    }
})();
//本质实现与方案上并无不同,
//只是在写法思想上,更强调所有API——局部变量的形式定义在函数中,而仅仅对外暴露出可被调用的接口

成熟期

CommonJs

node.js制定

特征:通过module + exports 来对外暴露接口;require来调用其他模块

优点:CommonJs规范在服务器端率先完成了JS的模块化,解决了依赖、全局变量污染的问题,这也是JS运行在服务端运行的必要条件。

缺点:针对的是服务端,对于异步依赖没有很友好的处理和考虑。

模块组织方式

解析:
  第一:将当前模块require的部分,从全局转成局部,
  第二:执行当前模块中需要执行的主干核心部分
  第三:将核心部分暴露出去
// commonJSCounterModule.js
    const dependencyModule1 = require('./dependencyModule1');
    const dependencyModule2 = require('./dependencyModule2');

    let count = 0;
    const increase = () => ++count;
    const reset = () => {
        count = 0;
        console.log('hahaha count is reset');
    };

    exports.increase = increase;
    exports.reset = reset;

    module.exports = {
        increase,
        reset
    }

    // main.js
    const { increase, reset } = require('./commonJSCounterModule')
    increase();

    const commonJSCounterModule = require('./commonJSCounterModule')
    commonJSCounterModule.increase();

实际执行处理

(function(exports, require, module, __filename, __dirname) {
    const dependencyModule1 = require('./dependencyModule1');
    const dependencyModule2 = require('./dependencyModule2');

    let count = 0;
    const increase = () => ++count;
    const reset = () => {
        count = 0;
        console.log('hahaha count is reset');
    };
    module.exports = {
        increase,
        reset
    };

    return module.exports;
}).call(thisValue, exports, require, module, filename, dirname);

(function (exports, require, module, __filename, __dirname) {
    const commonJSCounterModule = require('./commonJSCounterModule')
    commonJSCounterModule.increase();
}).call(thisValue, exports, require, module, filename, dirname);

AMD规范(异步模块化)

对于非同步加载模块,允许制定回调函数

经典框架:require.js

优点:解决了浏览器中异步加载模块,可以并行加载多个模块

缺点:会有引入成本,缺少考虑按需加载

新增定义方式

// 通过define来定义一个模块,然后require加载
    define(id, [depends], callback)
    require([module], callback)

模块的定义方式

define(
    'amdCounterModule', 
    ['dependencyModule1', 'dependencyModule2'], 
    (dependencyModule1, dependencyModule2) => {
        let count = 0;
        const increase = () => ++count;
        const reset = () => {
            count = 0;
            console.log('hahaha count is reset');
        };

        return {
            increase,
            reset
        }
    }
)

require(
    ['amdCounterModule'],
    amdCounterModule => {
        amdCounterModule.increase();
        amdCounterModule.reset();
    }
)

面试题2:如果想在AMD中使用require方式加载同步模块可以吗?

答:AMD支持向前兼容,以提供回调的形式来做require方法动态加载模块

 define(require => {
    const dependecyModule1 = require('dependecyModule1');
    const dependecyModule2 = require('dependecyModule2');

    let count = 0;
    const increase = () => ++count;
    const reset = () => {
        count = 0;
        console.log('hahaha count is reset');
    };

    // return {
    //     increase,
    //     reset
    // }
    // revealing
    exports.increase = increase;
    exports.reset = reset;
})

面试题3:有没有什么方式可以统一兼容AMD和common

答:UMD的出现

优点:适合在浏览器环境中异步加载模块,同时又采用common模块

缺点:提高了开发成本,并且不能按需加载,必须提前加载所有依赖

(define => define((require, exports, module) => {
    const dependecyModule1 = require('dependecyModule1');
    const dependecyModule2 = require('dependecyModule2');

    let count = 0;
    const increase = () => ++count;
    const reset = () => {
        count = 0;
        console.log('hahaha count is reset');
    };

    module.export = {
        increase,
        reset
    }
}))
(
    // 判断区分是否为AMD or common
    typeof module === 'object' && module.exports && typeof define !== 'function'
         ? // CommonJS
           factory => module.exports = factory(require, exports, module)
         : // AMD
           define
);

CMD规范(sea.js)

按需加载

优点:按需加载,依赖就近

缺点:依赖打包,加载逻辑存在于每个模块中,扩大了模块的体积

define(function(require, exports, module) {
    var $ = require('jquery');
    var dependencyModule1 = require('./dependencyModule1');

    // ……
    // exports.increase = ...
    // module.exports = ...
})

面试题4:CMD 和 AMD 区别

// AMD
define([
    './dependencyModule1',
    './dependencyModule2'
], function(dependencyModule1, dependencyModule2) {
    dependencyModule1.increase();
    dependencyModule2.reset();
})

// CMD - 依赖就近
define(
    function(require, exports, module) {
        let dependencyModule1 = require('./dependencyModule1');
        dependencyModule1.increase();

        // if () {
        //     let dependencyModule2 = require('./dependencyModule2');
        //     dependencyModule2.reset();
        // }
    }
)

ES6模块化-ESM

走向新时代

新增定义方式:引入:import;导出:export

  • 模块引入、导出和定义
import depModule1 from './dependecncyModule1';
import depModule2 from './dependecncyModule2';
   let count = 0;
    const obj = {
        increase: () => ++count;
        reset: () => {
            count = 0;
            // fn(depModule1);
            // depModule1, depModule2
        }
    }


    export default {
        increase,
        reset
    }

    // 异步加载
    import('xxx').then(a => {
        // ../
    })
  • 动态模块的加载
import ('./esModule').then(({ increase, reset }) => {
    increase();
    reset();
});

import ('./esModule').then((dynamicESModule) => {
    dynamicESModule.increase();
    dynamicESModule.reset();
});

解决模块化的新思路

  • 根本问题:运行时分析依赖

前端的模块化处理方案依赖于运行时进行分析,并且同时进行依赖加载处理以及实际的逻辑执行 解决方案:线下执行

 project
        | - lib
        |   |- xmd.js
        | - mods
            | - a.js
            | - b.js
            | - c.js
            | - d.js
            | - e.js
            | - f.js
        | - index.html

    // index.html
    <!doctype html>
    <script src="lib/xmd.js"></script>
    <script>
        {/* 等待构建工具生成数据替换`__FRAMEWORK_CONFIG__` */}
        require.config(__FRAMEWORK_CONFIG__);
    </script>
    <script>
        {/* 业务代码 */}
        require.async(['a', 'e'], function(a, e) {
            // ……
        })
    </script>

    // mods/a.js
    define('a', function(require, exports, module) {
        let b = require('b');
        let c = require('c');
        exports.run = function() {
            //……
        }
    })

    // 工程化模块的构建
    // 1. 扫描生成依赖关系表
    {
        "a": ["b", "c"],
        "b": ["d"]
    }
    // 2. 生成构建模版
    <!doctype html>
    <script src="lib/xmd.js"></script>
    <script>
        {/* 等待构建工具生成数据替换`__FRAMEWORK_CONFIG__` */}
        require.config({
            "deps": {
                "a": ["b", "c"],
                "b": ["d"]
            }
        });
    </script>
    <script>
        {/* 业务代码 */}
        require.async(['a', 'e'], function(a, e) {
            // ……
        })
    </script>
    // 3. 转化配置为依赖加载代码
    define('a', ["b", "c"], function(require, exports, module) {
        let name = _check ? 'b' : 'c';
        let mod = require(name);
        exports.run = function() {
            //……
        }
    })