webpack实战——模块打包

作者: 流眸Tel | 来源:发表于2020-07-07 16:51 被阅读0次

    写在前面

    这是webpack实战系列的第二篇:模块和模块打包。上一篇:webpack实战——打包第一个应用 记录了webpack的一些基础内容与一个简单地小例子,开启了webpack的实战之路,这一篇记录一下关于模块和模块打包。

    模块

    先看一下模块的定义:

    模块,是能够单独命名并独立地完成一定功能的程序语句的集合(即程序代码和数据结构的集合体)。它具有两个基本的特征:外部特征和内部特征。外部特征是指模块跟外部环境联系的接口(即其他模块或程序调用该模块的方式,包括有输入输出参数、引用的全局变量)和模块的功能;内部特征是指模块的内部环境具有的特点(即该模块的局部数据和程序代码)。

    可以从定义中看出,每个独立的模块负责不同工作,彼此之间又可以联系在一起共同保证整体系统运行。那么在webpack中,如何将其打包成一个(或多个)文件呢?

    想了解这些,我们还是先要熟悉在 Javascript 中的模块。在 Javascript 模块化中比较常见的有:

    • CommonJS
    • ES6 module
    • AMD
    • CMD
    • UMD(AMD和CommonJS)
    • ...

    但由于在目前的使用场景中 CommonJSES6 module 居多,因此暂时就这两者进行讨论。

    1. CommonJS

    1.1 模块

    在 CommonJS 中规定每个文件都是一个模块。

    在 CommonJS 中,变量及函数的声明不会造成全局污染。如:

    // add.js
    var name = 'name: add.js';
    
    // index.js
    var name = 'name: index.js';
    reuqire('./add.js');
    console.log(name);  // name: index.js
    

    在上面 index.js 中通过 require 函数来加载 add.js ,输出的结果是 name: index.js ,说明在 add 中定义的变量并不会影响 index ,可以得出使用 CommonJs 模块,作用域只针对于该模块,而不会造成全局污染,对外不可见

    1.2 导出

    前面说过模块拥有自己的作用域,那么模块是需要向外传递的,怎么办呢?
    导出是一个模块向外暴露自身的唯一方式。在 CommonJS 中,我们通过 module.exports 来导出模块中的内容。如:

    // add.js
    module.exports = {
        name: 'add',
        add: function(a, b) {
            return a + b;
        }
    }
    

    在 CommonJS 内部会有一个 module 对象用于存放当前模块的信息。而 module.exports 则指定向外暴露的内容。

    1.3 导入

    导出自然是为了另外一个模块来使用,这时便使用到了导入功能。在 CommonJS 中,使用 require 来进行模块导入:

    // add.js
    module.exports = {
        name: 'add',
        add: function(a, b) {
            return a + b;
        }
    }
    
    // index.js
    const add = require('./add.js');
    const sum = add.add(1, 2);
    console.log(sum);   // 3
    

    上面这个例子,便是在 index.js 中通过 require 导入了 add.js ,并且调用了其中的 add() 方法。

    而我们在 reuqire 一个模块的时候,会分两种情况:

    1. 如果 require 的模块第一次被加载,那么会执行该模块然后导出内容;
    2. 如果非首次加载,那么该模块代码不会再次执行,而是直接导出上次代码执行后所得到的结果。

    有时候我们只想通过加载执行某个模块让它产生某种作用而不需要获取它所导出的内容,则可以直接通过 require 来导入而不需要定义:

    require('./task.js');
    ...
    

    而通过这个特性,加上 require 函数可以接收表达式,那么我们则可以动态指定模块加载路径:

    const moduleList = ['add.js', 'subtract.js'];
    moduleList.forEach(item => {
        require(`./${item}`);
    });
    

    2. ES6 Module

    熟悉 JavaScript 语言的小伙伴知道,其实在 JavaScript 设计之初,并没有模块化这个概念。而伴随 JavaScript 不断的壮大发展,社区中也涌现出了不少模块化概念。一直到 ES6 , JavaScript 终于正式的有了模块化这一特性。

    2.1 模块

    在前面我们使用 CommonJS 实现了一个例子来展示 CommonJS 的模块、导出与导入,同样在此处也先来一个例子,只需将上面例子稍微改写即可:

    // add.js
    export default {
        name: 'add',
        add: function(a, b) {
            return a + b;
        }
    }
    
    // index.js
    import add from './add.js';
    const sum = add.add(-1, 1);
    console.log(sum);   // 0
    

    ES6 Module 也是将每一个文件都作为一个模块,并且每个模块拥有自身的作用域,但是与 CommonJS 相比, 不同的是导入、导出语句。在 ES6 Module 中,
    import 和 export 也作为关键字被保留。

    2.2 导出

    在 ES6 Module 中,使用 export 来对模块进行导出。

    export 导出的两种方式:

    • 命名导出
    • 默认导出

    2.2.1 命名导出

    以下有两种写法,但其效果并无区别:

    /**
    * 命名导出: 两种写法
    **/
    
    // 1. 声明和导出写在一起
    export const name = 'add';
    export const add = function(a, b) {
        return a + b;
    }
    
    // 2. 先声明,再统一导出
    const name = 'add';
    const add = function(a, b) {
        return a + b;
    }
    export { name, add }
    

    as关键字

    在使用命名导出时,如果用写法2(先声明再统一导出),可以使用 as 关键字 来对导出的变量进行重命名。如:

    const name = 'add';
    const add = function(a, b) {
        return a + b;
    }
    // add as sum : 在导入时,使用 name 和 sum 即可
    export { name, add as sum }
    

    2.2.2 默认导出

    说完了命名导出,来到默认导出:模块的默认导出只能导出一个。举例:

    // 默认导出
    export defailt {
        name: 'add',
        add: function(a, b) {
            return a + b;
        }
    }
    

    由上可见,我们可以将默认导出理解为向外输出了一个命名为 default 的变量。

    2.3 导入

    ES6 Module 中使用 import 进行模块导入。由于在 ES6 Module 的导出中,分为 命名导出默认导出 ,因此在导入的时候也有对应的两种方式进行导入。

    2.3.1 命名

    // add.js
    const name = 'add';
    const add = function(a, b) {
        return a + b;
    }
    export { name, add }
    
    // index.js
    import { name, add } from './add.js';
    console.log(name, add(1, 2));   // add 3
    

    可以看到,在使用 import 对命名导出模块进行引入的时候, import 后面跟了一对花括号 { } 将导入的变量名包裹起来,并且变量名需要与导出时的变量命名一样。同样,我们还是可以使用 as 关键字 来对变量进行重命名:

    // index.js
    import { name, add as sum } from './add.js'
    sum(2, 2);  // 4
    

    值得注意的是,导入变量的效果相当于在当前作用域下声明了变量(如 name 和 add),但不可对这些变量不能修改,只可当成只读的来使用。

    当然,我们还可以使用 * 来进行整体导入:

    // index.js
    import * as add from './add.js';
    console.log(add.name);  // add
    console.log(add.add(1, 1)); // 2
    

    2.3.2 默认

    对于默认导出的导入处理如下:

    // add.js
    export default {
        name: 'add',
        add: function(a, b) {
            return a + b;
        }
    }
    
    // index.js
    import addMe from './add.js';
    console.log(addMe.name, addMe.add(2, 5));   // add 7
    

    可以看到,如果是导入默认导出的模块,那么在 import 后面直接跟变量名即可,并且这个变量名无需与导出模块的变量名重复,可以自己指定新的变量名。

    3. CommonJS 与 ES6 Module 的区别

    介绍了 CommonJS 与 ES6 Module 的基础应用之后,我们也要了解到在实际的开发过程中我们经常将这两者在同一个项目中混用。为了避免不必要的麻烦,还是要说一下两者的异同。

    3.1 动态与静态

    CommonJS 对模块依赖的解决是动态的,而 ES6 Module 对模块依赖的解决是静态的。

    首先要了解这里说的动态静态是什么:

    • 动态: 模块依赖关系的建立发生在代码运行阶段
    • 静态: 模块依赖关系的建立发生在代码编译阶段

    由于 ES6 Module 中导入导出语句都是声明式的,不支持导入表达式类路径,并且导入导出语句必须位于模块的顶层作用域。相比 CommonJS ,具备优势如下:

    • 死代码检测和排除: 可以使用静态分析工具检测出没有被调用的模块,减小打包资源体积;
    • 模块变量类型检查: 有助于确保模块之间传递的值或者接口类型的正确性;
    • 编译器优化: ES6 Module 直接导入变量,减少层级引用,程序效率更高。

    3.2 值拷贝和动态映射

    在导入一个模块时,对于 CommonJS 来说获取的是一份导出值的拷贝,而在 ES6 Module 中则是值的动态映射,这个映射是只读的。例如:

    // add.js
    var count = 0;
    module.exports = {
        count: count,
        add: function(a, b) {
            count += 1;
            return a + b;
        }
    }
    
    // index.js
    var count = require('./add.js').count;
    var add = require('./add/js').add;
    
    console.log(count); // 0 (这里的count是对add.js中couunt的拷贝)
    add(2, 3);
    console.log(count); // 0 (add.js中变量值的改变不会对这里的拷贝值造成影响)
    
    count += 1;
    console.log(count); // 1 (拷贝的值 0 + 1 = 1,表示拷贝的值可以更改)
    

    可以看出,index.js 中的 count 是 add.js 中 count 的一份值拷贝,因此在调用 add 函数时,即便更改了 add 中的 count,但不会对 index.js 中的值拷贝造成影响。

    但在 ES6 Module 中,却不一样:

    // add.js
    let count = 0;
    const add = function(a, b) {
        count += 1;
        return a + b;
    };
    export { count, add };
    
    // index.js
    import { count, add } from './add.js';
    console.log(count); // 0 (对add.js中count值的映射)
    add(1, 1);
    console.log(count); // 1 (对add.js中count值的映射,会映射值的变化)
    
    count += 1; // 报错,该count值不可更改
    

    4. 模块打包原理

    前面描述了一些基础的 CommonJS 与 ES6 Module 模块化的一些知识,那么回到 webpack 中来:webpack是如何将各种模块有序的组织在一起的呢?

    // add.js
    module.exports = {
        add: function(a, b) {
            return a + b;
        }
    }
    
    // index.js
    const add = require('./add.js');
    const sum = add(1, 2);
    console.log(`sum: ${sum}`);
    

    还是之前的例子,但现在经过 webpack 打包之后,它会变成什么样子呢?

    bundle.js

    如图所示,这便是一个简单地打包结果。我们可以观察自己的 bundle.js 文件,从中看打包逻辑关系:

    • 首先一个立即执行匿名函数,包裹所有内容,构成资深作用域;
    • installedModule对象(模块缓存),每个模块在第一次被加载的时候执行,到处结果存储到其中,以后再次调用模块直接取值即可,不会再次执行模块;
    • webpack_require函数: 对模块加载的实现,在浏览器中可以通过调用此函数加模块id来进行模块导入;
    • modules对象:工程中所有产生依赖关系的模块都会以 key-value 形式放在此对象中, key 作为模块 id,由数字或者 hash 字符串构成,value 则由一个匿名函数包裹的模块构成,匿名函数的参数则赋予了每个模块导出和导入能力。

    小结

    本篇记录了关于 JavaScript 的模块化与 webpack 的模块打包原理简介。

    首先,介绍了关于模块的概念,然后依次介绍了两种模块化:CommonJS 和 ES6 Module ,以及他们分别的模块概念、导出和导入,接着介绍了他们之间的两个差异:动态与静态、值拷贝和映射。最后,提及了一下模块化打包的简单原理,对webpack打包工作有一个大概认知。

    下一篇将会介绍在webpack中资源的输入与输出。敬请期待。


    学习推荐: 本系列学习资源多来自于 《webpack实战 入门、进阶与调优》 ,作者 居玉皓 , 感兴趣的朋友可以购买实体书支持学习~

    相关文章

      网友评论

        本文标题:webpack实战——模块打包

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