前端基础打卡5-模块化

110 阅读5分钟

记录一下前端学习笔记,坚持打卡记录!!

开始第五篇

什么是模块化?

  • 内部、外部模块的合理使用和管理
  • 模块源码到目标模板代码的编译

前端模块化发展史

  • node.js 09
  • npm 10
  • AMD requirejs 10
  • CMD seajs 11
  • webpack 12
  • grunt 12
  • gulp 13
  • react 13
  • vue 14
  • angular2 16
  • vite 20
  • snowpack 工程化 20

从以上可以知道先出现打包工具, 再出现前端框架。

前端模块化大致经历了以下几个阶段:

graph LR
无模块化 --> IIFE -->CommonJs --> AMD --> CMD -->UMD --> ESM

下面依次来介绍一下这几个阶段

无模块化

  • 多种js为了维护和可读性,不同类型和功能的js被分配在不同文件中
  • 不同文件在同一个模板中被使用
    <script src="jquery.js"></script>
    <script src="main.js"></script>
    <script src="deps1.js"></script>
    <script src="deps2.js"></script>

优点:比起之前所有的功能模块放在一个文件中,这种方式是最简单初步的模块化,相对是进步的。

缺点:各个js中定义的变量依然是全局的,存在全局变量为污染,且每个模块之前的变量名不能重复,不利于项目的开发与维护。

IIFE(Immediately-invoked function expression ):立即执行函数

利用函数的作用域限制定义模块

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

    iifeCounterModule.increase();
    iifeCounterModule.reset();

如果函数依赖于其它模块呢?

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

问题:了解jquery的依赖处理以及模块加载方案吗?

答:IIFE 加 传参调配。通过将整个jQuery库包裹在一个自执行的函数中,可以避免全局命名空间的污染,同时确保模块之间的依赖关系正确加载。当jQuery库被加载到页面中时,这个自执行的函数会立即执行,从而完成模块的加载和初始化。

成熟期

CommonJs:CJS module

这个是node.js制定,主要使用特征:

  • 通过module + exports来对外暴露接口
  • 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);

由实际执行处理的代码可以看出,require,module这些js本身是这些方法的,是需要作为立即执行的参数依赖,由node.js传入的。 Commonjs总结: 优点: Commonjs规范在服务端完成了javascript的模块化,解决了依赖、全局变量污染的问题,这是js运行在服务端运行的必要条件。 缺点: 由于服务端以及commonjs是同步加载模块的,但是对于浏览器而言,异步加载是很重要的,所以需要可以支持异步的解决方案。

在看异步解决方案之前,先来看两个关于cjs的两个问题。

  1. module.exports和exports的关系

Commonjs是没有module.exports概念的,但是为了实现模块的导出,node使用了Module类,每一个模块就是Moudule的一个实例,所以真正导出的不是exports而是moudule.exports。 因为module对象的exports属性是exports对象的一个引用;就是说 module.exports=exports=require(xx)

注意: exports使用的时候如果是exports= xx,则导出是无效的,因为exports参数实际上传入的是module.exports,直接覆盖是不会改变module.exports的

  1. 如果出现循环引用会怎样?

即使出现循环引用,也不会出现无限循环的问题。 因为在使用require()加载一个模块时,由于存在缓存机制,加载模块时会先判断是否存在该模块的缓存,若存在,则会直接返回缓存中该模块的module.exports,而不会在重复执行。就是说只有在第一次加载时,加载的模块代码才会执行。但是如果如果某些代码块想要执行多次怎么办?可以导出一个函数,然后多次调用函数即可。

接下来看看异步加载的规范

AMD:Asynchronous Module Definition

(经典的实现框架: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();
    }
)

那如果想在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;
    })

Amd总结:

优点:可以在浏览器中异步加载模块,同时又采用了commonm模块

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

那有没有什么方式可以统一AMD和Commonjs呢?

UMD:Universal Module Definition

其实UMD就是判断了是amd、node还是浏览器环境,根据不同规范按照不同方式加载,进行兼容,所以现在很多包打出来的都是umd格式。

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD
        define(['jquery', 'underscore'], factory);
    } else if (typeof exports === 'object') {
        // Node, CommonJS-like
        module.exports = factory(require('jquery'), require('underscore'));
    } else {
        // Browser globals (root is window)
        root.returnExports = factory(root.jQuery, root._);
    }
}(this, function ($, _) {
    //    methods
    function a(){};    //    private because it's not returned (see below)
    function b(){};    //    public because it's returned
    function c(){};    //    public because it's returned

    //    exposed public methods
    return {
        b: b,
        c: c
    }
}));

前面的这些规范,都不是js官方提出的,直到ES6模块化出现ESM,解决了上述的一系列问题。

ES6模块化 - ESM

定义方式:import引入;export导出 举个常用例子:

    import dependencyModule1 from './dependencyModule1';
    import dependencyModule2 from './dependencyModule2';

    let count = 0;

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

    export default {
        increasem
        reset
    }

es6异步加载方式:

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

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

总结

现在模块化大多使用最新的esm的方式,但是如果要考虑兼容性的话,直接打包出umd的格式基本可以满足大部分情况。