美文网首页
前端模块化的发展概述

前端模块化的发展概述

作者: 进击的阿群 | 来源:发表于2022-01-21 18:41 被阅读0次

    前端模块化简单梳理

    本篇简介

    关于前端模块化的一些知识,如CMD/AMD/Webpack等,之前都进行过专门学习,但经验尚欠,无法从上层理解模块化处于前端工程的哪一层,所以此篇文章暂且抛开之前所学内容,不做细化研究,先单独对模块化大致脉络进行梳理,等待后续工程化、设计模式等学习完成后再进行整体的梳理。

    整体脉络图

    1. 无模块化时期

    Web初期并没有模块化的工具,且前端在当时相对轻量,甚至后端通过模板就能完全胜任全栈工程师的角色,所以当时并没有前端工程的概念,也就没有模块化。为了组织代码,采用的是不同功能代码通过文件区分:

    <!-- 不同功能代码通过文件区分,导入模板,但作用域并没分离 -->
    <script src="layer.js"></script>
    <script src="init.js"></script>
    <script src="login.js"></script>
    

    2. 【幼年期】语法层面模块化

    由于无模块化会导致全局变量污染问题,不利于团队开发,因此 IIFE 自然成了语法层面模块化的唯一选择;其原理是函数作用域内变量若存在外部引用,则函数产生引用时的执行环境不会销毁,也就是此时函数作用域一直生效,且由于外部无法访问函数内部作用域,因此就形成了即封闭又长效的“模块”。IIFE只是将匿名函数立即执行,是对上述原理的一种语法简化,用它实现模块化的方式是返回一个暴露 api 的对象(模块):

    // IIFE
    var moduleA = (() => {
      var count = 0;
      return {
        printCount: function() {
          console.log(count); // 对函数作用域中的count进行引用
        },
        increase: function() {
          count++;
        }
      };
    })();
    // 调用模块
    moduleA.printCount(); // 0
    moduleA.increase();
    moduleA.printCount(); // 1
    

    面试题1:有额外依赖时,如何优化 IIFE?

    由于 IIFE 原理是借助函数作用域,所以额外依赖可以通过参数传入函数中供模块使用:

    // IIFE
    var moduleA = ((dependB, dependC) => {
      var count = dependB.count || dependC.count || 0; // 使用依赖
      return {
        printCount: function() {
          console.log(count); // 对函数作用域中的count进行引用
        },
        increase: function() {
          count++;
        }
      };
    })(moduleB, moduleC);
    

    面试题2:了解 jQuery早期依赖处理及模块化加载方案吗?

    使用了揭示模式(Revealing)写法,原理仍然是 IIFE 传参,匿名函数暴露 api 对象,api 写法是指针形式:

    const moduleA = ((dep1, dep2) => {
      var count = dep1.count || dep2.count || 0;
      var getCount = function() {
        return count;
      };
      return {
        getCount, // 揭示模式写法,用指针代替具体函数
      };
    })(moduleB, moduleC);
    

    3. 【成熟期】CommonJS 的出现

    Node 作为服务端语言出现后,自然少不了模块化的需求,因此出现了 CommonJS。其特性如下:

    • require 引入依赖模块
    • module、exports 对象暴露 api

    模块组织方式如下:

    /* NodeJS模块,基于CommonJS规范 */
    // 引入依赖
    const dep1 = require('./dep1.js');
    // coding
    let count = de1.count || 0; // 使用模块
    const getCount = () => count;
    // 暴露api
    exports.getCount = getCount;
    // 也可以直接重写module.exports
    module.exports = {
      getCount,
    }
    

    优缺点如下:

    • 优点:从框架层面首次实现了真正意义的模块化
    • 缺点:为服务端设计,所以起初并未考虑异步依赖问题

    面试题:上述CommonJS模块实际执行过程是?

    由于对异步依赖支持不足,所以不难想到早期原理是 IIFE 的语法糖:

    (function(thisValue, exports, require, module) {
      const dep1 = require('./dep1.js');
      // ...
    }).call(thisValue, exports, require, module);
    

    4. AMD规范

    CommonJS 解决了模块化的问题,但又有了如何处理异步依赖的新问题,而 AMD规范应运而生。原理是 “异步加载后,执行回调函数”,经典框架是 require.js。示例如下:

    /**
     * @function define函数能够定义模块,require函数加载模块
     * @params 模块名,依赖列表,模块工厂函数
     */
    // 定义模块
    define(id, [...dependList], factory);
    // 引入模块
    require([...moduleList], callback); // 加载模块,完成后执行callback
    

    demo如下:

    // 定义moduleA模块
    define('moduleA', ['dep1', 'dep2'], (dep1, dep2) => {
      let count = dep1.count || dep2.count || 0;
      const getCount = () => count;
      return {
        getCount,
      };
    });
    // 引入并使用模块
    require(['moduleA'], moduleA => {
      moduleA.getCount(); // 0
    });
    

    面试题1:若希望AMD兼容之前CommonJS代码,怎么办?

    AMD引入依赖的方式除了传入列表,还给工厂函数提供了 require方法,能够兼容CommonJS的写法:

    define('moduleA', [], require => {
      let dep1 = require('./dep1.js');
      let dep2 = require('./dep2.js');
      let count = dep1.count || dep2.count || 0;
      const getCount = () => count;
      return {
        getCount,
      };
    });
    

    面试题2:AMD使用 revealing 写法?

    除了返回对象,也可以使用工厂函数的 export 对象挂载指针,虽然是一种设计模式,但其实写法没太大区别:

    define('moduleA', [], (require, exports, module) => {
      let dep1 = require('./dep1.js');
      let dep2 = require('./dep2.js');
      let count = dep1.count || dep2.count || 0;
      const getCount = () => count;
      exports.getCount = getCount; // 直接挂载export对象
    });
    

    AMD的优缺点:

    • 优点:可在浏览器中加载模块,且支持异步、并行得加载多个模块
    • 缺点:无法按需加载

    还有一种能够兼容 AMD 和 CommonJS 的规范叫 UMD,原理就是在工厂函数外包裹一层兼容函数,通过不同传参实现兼容,在这里不过多展开。

    5. CMD规范

    由于AMD无法按需加载,国内团队做了CMD规范进行优化,主流框架是 seaJS,示例如下:

    /**
     * @function 省去依赖列表参数,依赖动态引入,与AMD区分是能按需加载
     */
    define('moduleA', (require, exports, module) => {
      let $ = require('jquery');
      //...jquery相关逻辑
      let dep1 = require('./dep1');
      // ...dep1相关逻辑
    });
    

    优缺点:

    • 优点:在打包时能够实现按需加载,且依赖就近,方便维护
    • 缺点:按需加载会使打包过程变慢,且按需加载逻辑放入每个模块,模块体积反而会增大

    面试题:AMD & CMD 的区别?

    CMD能够按需加载

    6. 【新时代】ES模块化

    从 ES6 开始,实现了JS模块化的标准语法,且能实现上述旧工具的所有功能,并得到了良好支持。示例如下:

    // 引入依赖
    import dep1 from './dep1'
    
    let count = 0;
    function getCount() {
      return count;
    }
    // 导出接口
    export default {
      getCount,
    }
    

    浏览器中引入模块的方法是 type=module 的script标签:

    <script type="module" src="./moduleA.js"></script>
    

    新版 Node中直接使用 ES6语法引入即可:

    // 模块文件一般用 mjs 后缀
    import moduleA from './moduleA.mjs'
    // 使用模块
    moduleA.getCount();
    

    面试题:ES6 如何实现动态加载模块?

    Webpack 支持使用 CMD写法或 import('./moduleA'),不过 ES11 已原生支持该特性:

    // 原理就是包一层promise,等模块加载完就引入
    import('./moduleA').then(dynamicModule => {
      dynamicModule.getCount();
    });
    

    ES6模块化的优缺点:

    • 优点:通过统一且标准的方式实现了模块化
    • 缺点:若不使用Webpack 等工程化工具,其本质仍然是运行时依赖分析

    7. 【完备方法】工程化

    为了解决 ES6 是运行时依赖分析的痛点,使用工程化构建工具,使依赖能够在代码构建阶段就完成。典型工具有 gruntgulpwebpack,大致原理如下:

    <script>
      // 构建工具的占位符
      // require.config(__FRAME_CONFIG__);
    </script>
    <script>
      import A from './modA'
      define('B', () => {
        let c = require('C');
        // 业务逻辑
      });
    </script>
    

    编译时会扫描依赖关系并生成map:

    {
      a: [],
      b: ['c'],
    }
    

    然后会根据依赖关系替换占位符:

    <script>
      require.config({
        a: [],
        b: ['c'],
      })
    </script>
    

    接着会根据模块化工具的配置处理依赖并生成符合兼容要求的代码:

    // 同步方案,加载进C
    define('b', ['c'], () => {
      // ...
    })
    

    完成。

    这种方案的优点:

    • 构建时分析依赖
    • 方便拓展,比如同时使用3种不同的模块化方案

    总结

    大概梳理一下模块化的脉络,有助于理解现在为何Webpack、Vite 等工具流行的原因,因为能解决足够多的问题,将多规范进行统一并且可以拓展。若想研究具体的规范使用,可以参考对应热门框架的实现,每一个都可以讲很多东西,在这里不过多阐述。

    相关文章

      网友评论

          本文标题:前端模块化的发展概述

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