前端模块化

155 阅读4分钟

前言

前端模块化主要是为了解决变量作用域和文件之间的相互引用和依赖关系。

一、作用域问题解决方案的演进过程

  • 传统编写模式
    • 会造成全局对象的污染,命名冲突。
function a() {
    console.log(b)
}
var b = '1'
  • 命名空间模式
    • 使用对象进行包裹,减少全局污染,但是外部仍然可以通过这个对象对属性进行操作,不安全。这里可以借助proxy来进行规避。
var pageA = {
    inputVal: 'this is a template.',
    getVal: function () {
        return this.inputVal
    }
}
  • 匿名闭包
    • 通过匿名闭包的方式向外暴漏一个对象,达到对属性的保护。
var fn = (function(){
    var _val = 'private value';
    var getVal = function () {
        return _val;  
    };
    return {
        getVal
    }
})()
fn.getVal(); // private value
fn._val; // undefined

// 也可以传递值到闭包中,这是现在模块化的基础。
var fn = (function(window){
    var _val = 'private value';
    var getVal = function () {
        return _val;  
    };
    return {
        getVal
    }
})(window)
fn.getVal(); // private value
fn._val; // undefined

文件引用及相互依赖

  • 传统模式
    • 引入的文件过多,难以维护
    • 文件之间的依赖关系模糊
    • 每一个引入文件都会发送一个请求,导致请求过多:合并、压缩、混淆
    <script src="https://cdn.bootcss.com/jquery/3.4.1/core.js"></script>
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.slim.js"></script>
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.slim.min.js"></script>
    <script src="https://cdn.bootcss.com/jquery/3.4.0/core.js"></script>
    <script src="https://cdn.bootcss.com/jquery/3.4.0/jquery.js"></script>
    <script src="https://cdn.bootcss.com/jquery/3.4.0/jquery.slim.js"></script>
    
  • CommonJS应用与服务器端,导出模块的方式有module.exports、exports,引入的方式是require。require是同步阻塞加载进行加载模块,所以能在require之后直接使用require模块,也是因为同步阻塞加载的缘故,一般用在服务器端。当然浏览器也可以使用。
    // moduleA.js
    var val = 'this is module A'
    var getVal = function() {
        return val;
    }
    module.exports = {
        getVal
    }
    // main.js
    var moduleA = require('./moduleA.js');
    console.log(moduleA.getVal());
    
  • AMD:浏览器端的模块化解决方案,因为CommonJS是同步阻塞加载,在浏览器进行请求的时候无法异步并发加载模块,不适用。requireJS是AMD规范的一种实现。
    // moduleA.js、定义
    define(function() {
        var val = 'this is module A'
        var getVal = function() {
            return val;
        }
        return {
            getVal
        }
    )
    // moduleB.js 定义
    define(['jQuery'], function($) {
        return {
            el: $('#input')
        }
    })
    // 引入依赖,AMD对于所有的依赖都会提前下载、提前执行,即使模块暂时不需要用到
    define(['./moduleA.js', 'moduleB.js'], function(moduleA, moduleB){ // 模块加载之后会作为入参
        if (moduleB.el) { // moduleA已经执行加载完毕,而不是懒加载
            console.log(moduleA.getVal());
        }
    })
    
  • CMD:在AMD基础上,引入了延迟加载机制。sea.js是CMD规范的实现。CMD利用类似require的同步加载机制,实现了在需要的时候才加载模块。(Early Download, Lazy Executing)
    // moduleA.js 定义
    define(function(require, exports, module) {
        var $ = require('jQuery');
        module.exports = {
            el: $('#input')
        }
    })
    // moduleB 定义
    define(function(require, exports, module) {
        var val = 'this is module A'
        var getVal = function() {
            return val;
        }
        module.exports = {
            getVal
        }
    })
    // main.js
    define(function(require, exports, module) {
        var moduleA = require('./moduleA.js');
        if (moduleA.el) {
            var moduleB = require('./moduleB.js');
            console.log(moduleB.getVal());
        }
    })
    
  • es6 module:ES6的模块不是对象,import命令会被 JavaScript 引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。也正因为这个,使得静态分析成为可能。

ES6 模块与 CommonJS 模块的差异

一、CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
  • ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
二、CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  • 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
  • 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。

CommonJS暴漏的是对象,所以需要加载完模块之后才能生成对象,ES6暴漏的是接口,在import的时候只需要暴漏了什么接口即可,不需要关心模块中具体的实现,实现在加载模块的时候才去获取相应的实现。ES6能在编译阶段就识别出模块是否有暴漏出相应的接口。CommonJS必须在运行时才能去判断是否有暴漏相应的对象。