当前位置: 首页 > 工具软件 > tio-webpack > 使用案例 >

Webpack--模块打包

雍光远
2023-12-01

一、CommonJS

(1)CommonJS是由Javascript社区于2009年提出的包含模块、文件、IO、控制台在内的一系列标准。

(2)在Node.js的实现中采用了CommonJS标准的一部分,并在其基础上进行了一些调整。我们所说的CommonJS模块和Node.js中的实现并不完全一致,现在一般提到CommonJS其实是Node.js中的版本,而非它的原始定义。

(3)CommonJS中规定每个文件是一个模块

  • 导出

(1)导出是一个模块向外暴露自身的唯一方式。在CommonJS中,通过module.exports可以导出模块中的内容

(2)CommonJS模块内部会有一个module对象用于存放当前模块的信息。

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

(3)简化的导出方式,直接使用exports

exports.name = 'xxx';
exports.add = function (a, b) {
  return a + b;
}

1、其内在机制是将exports指向了module.exports,而module.exports在初始化时是一个空对象。

2、注意不要直接给exports赋值,否则会导致其失效。

(4)注意导出语句不代表模块的末尾,在module.exports或exports后面的代码依旧会照常执行。

  • 导入

 (1)在CommonJS中使用require进行模块导入

const calculator = require('./calculator.js')

(2)当我们require一个模块时会有两种情况

1、require的模块是第一次被加载。这时会首先执行该模块,然后导出内容。

2、require的模块曾被加载过。这时该模块的代码不会再次行,而是直接导出上次执行后得到的结果。 (模块会有一个module对象用来存放其信息,这个对象中有一个属性loaded用于记录该模块是否被加载过。它的默认值为false,当模块第一次被加载和执行后会置为true,后面再次加载时会检查到module.loaded为true,则不会再次执行模块代码)

(3) 有时我们加载一个模块,不需要获取其导出的内容,只是想要通过执行它而产生某种作用,比如把它的接口挂在全局对象上,此时直接使用require即可。

(4)require函数可以接收表达式,借助这个特性我们可以动态地指定模块加载路径

const moduleNames = ['foo.js', 'bar.js'];
moduleNames.forEach(name => {
  require('/' + name);
})

二、ES6 Module

 (1)ES6 Module也是将每个文件作为一个模块,每个模块拥有自身的作用域。

(2)ES6 Module会自动采用严格模式。这在ES5中是一个可选项。

  • 导出

(1)使用export命令来导出模块。export有两种形式

  • 命名导出

     a、一个模块可以有多个命名导出。它有两种不同的写法

//将变量的声明和导出写在一行
export const name = 'calculator';
//先进行变量声明,然后再用同一个export语句导出
const name = 'calculator';
const add = function (a, b) {
  return a + b;
}
export { name, add }

b、可以通过as关键字对变量重命名。

export { name, add as getSum }//在导入时即为name和getSum
  • 默认导出 

a、默认导出只能有一个

//导出一个对象
export default {
  name: 'xxx',
  add: function (a, b) {
    return a + b;
  }
}
//导出字符串
export default 'This is xxx'
//导出class
export default class {}
//导出匿名函数
export default function(){}
  • 导入

(1)使用import语法导入模块。

(2)加载带有命名导出的模块的时候,import后面要跟一对大括号来将导入的变量名包裹起来,并且这些变量名需要与导出的变量名完全一致。导入变量的效果相当于在当前作用域下声明了这些变量,并且不可对其进行更改,也就是所有导入的变量都是只读的。

(3)可以通过as关键字对导入的变量重命名

import { name, add as calculateSum } from './calculator.js'

(3)整体导入

import * as calculator from './calculator.js'

 使用import * as <myModule>可以把所有导入的变量作为属性值添加到<myModule>对象中,从而减少了对当前作用域的影响。

(4)对于默认导出,import后面直接跟变量名(可自由指定)

import myCalculator from './calculator.js'

(5)两种导入方式混合

import React,{ Component } from 'react'
  •  复合写法 

导入立即导出: 

export { name, add } from './calculator.js'

这种写法目前只支持当被导入模块通过命名导出的方式暴露出来的变量,默认导出则没有对应的复合形式,职能将导入和导出拆出来写

import myCalculator from './calculator.js'
export default myCalculator

 三、CommonJS 和 ES6 Module的区别

  •  动态和静态

(1) CommonJS 和 ES6 Module 最本质的区别在于前者对模块依赖的解决是“动态的”,而后者是“静态的”。

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

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

(2)CommonJS模块被执行前,并没有办法确定明确的依赖关系,模块的导入、导出发生在代码的运行阶段。

(3)ES6 Module的导入、导出语句都是声明式的,它不支持导入的路径是一个表达式,并且导入、导出语句必须位于模块的顶层作用域(比如不能放在if语句里)。因此我们说,ES6 Module是一种静态的模块结构,在ES6代码的编译阶段就可以分析出模块的依赖关系。它想对于CommonJS来说具备以下几点优势

1、死代码检测和排除

2、模块变量类型检查

3、编译器优化。(CommonJS本质上导入的都是一个对象,而ES6 Module支持直接导入变量,减少了引用层级,程序效率更高)

  • 值拷贝与动态映射 

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

  • 循环依赖

 ES6 Module的特性(导出是动态映射)使其可以更好地支持循环依赖,只是需要由开发者来保证当导入的值被使用时已经设置好正确的导出值。

三、非模块化文件

(1)非模块化文件指的是并不遵循任何一种模块标准的文件(如JQuery)

(2)webpack如何打包?只要直接引入即可

import './jquery.min.js'

四、AMD(异步模块定义)

(1)它是由Javascript社区提出的专注于支持浏览器端模块化的标准。

(2)它与CommonJS 和 ES6 Module最大的区别在于它加载模块的方式是异步的。

(3)定义一个AMD模块 

define('getSum', ['calculator'], function (math) {
  return function (a, b) {
    console.log('sum' + calculator.add(a, b));

  }
})

define函数的三个参数:

第一个:当前模块的id,相当于模块名

第二个: 当前模块的依赖

第三个:描述模块的导出值,可以是函数或对象。如果是函数则导出的是函数的返回值;如果是对象则直接导出对象本身

(4)使用require函数加载模块,采用异步的形式

require(['getSum'], function (getSum) {
  getSum(2, 3);
})

二个参数:

第一个:指定了加载的模块。

第二个:当加载完成后执行的回调函数。

(5)缺点

1、语法冗长。

2、异步加载的方式并不如同步显得清晰。

3、容易造成回调地狱 

五、UMD

(1)目标是使一个模块能运行在各种环境下,不论是CommonJS、AMD,还是非模块化的环境。

(2)UMD其实就是根据当前全局对象中的值判断目前处于那种模块环境。

(3)UMD模块一般都最先判断AMD环境,也就是全局环境下是否由define函数,而通过AMD定义的模块是无法使用CommonJS或ES6 Module的形式正确引入的。

  (function (global, main) {
    //根据当前环境采取不同的导出方式
    if (typeof define === 'function' && define.amd) {
      //AMD
      defined(...);
    } else if (typeof exports === 'object') {
      //CommonJS
      module.exports = ...;
    }else {
  //非模块化环境
  globalThis.add = ...;
}
} (this, function () {
  //定义模块主体
  return { ...}
}));

六、加载npm模块

import _ from 'loadsh

(1)每一个npm模块都有一个入口。当我们加载一个模块时,实际上就是加载该模块的入口文件。这个入口被维护在模块内部的package.json文件的main字段中。

七、模块打包原理

(1)模块代码

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

(2)经过Webpack打包结果(bundle)(大致结构)

  (function (modules) {
    //模块缓存
    var installedModules = {};
    //实现require
    function _webpack_require_(moduleId) {
      //...
    }
    //执行入口模块的加载
    return _webpack_require_(_webpack_require_.s = 0)
  })({
    //modules:以key-value的形式存储所有被打包的模块
    0: function (module, exports, _webpack_require_) {
      //打包入口
      module.exports = _webpack_require_('3qiv');
    },
    '3qiv': function (module, exports, _webpack_require_) {
      //index.js内容
    }
    'jkzz': function (module, exports) {
      //calculator.js内容
    }
  });

bundle分为以下几个部分

1、最外层立即执行的匿名函数:它用来包裹整个bundle,并构成自身的作用域。

2、installedModules对象:用于实现模块缓存,第一次被加载执行后存储到这个对象里,后续加载直接从这个对象里取,不会重新执行。

3、_webpack_require_函数:对模块加载的实现,在浏览器中可以通过调用_webpack_require_(module_Id)来完成模块导入。

4、modules对象:工程中所有产生了依赖关系的模块都会以key-value的形式放在这里。key可以理解为一个模块的id,由数字或者一个很短的hash字符串构成;value则是由一个匿名函数包裹的模块实体,匿名函数的参数则赋予了每个模块导出和导入的能力。

(3)一个bundle如何在浏览器中执行

1、在最外层的匿名函数中会初始化浏览器执行环境,包括定义 installedModules对象、_webpack_require_函数等,为模块的加载执行一些准备工作。

2、加载入口模块。每个bundle都有且只有一个入口模块。

3、执行模块代码。如果执行到了module.exports则记录下模块的导出值;如果中间遇到require函数(_webpack_require_),则会暂时交出执行权,进入_webpack_require_体内进行加载其他模块的逻辑。(递归)

4、在_webpack_require_中会判断即将加载的模块是否存在于installedModules对象中,如果存在则直接取值,否则回到第三步,执行该模块的代码来获取导出值。(递归)

5、所有依赖的模块都已经执行完毕,最后执行权又回到入口模块。当入口模块的代码执行到结尾,也就意味着整个bundle运行结束。

(4)Webpack为每个打包模块创造了一个可以导出和导入模块的环境,但本质上并没有修改代码的执行逻辑,因此代码执行的顺序与模块加载的顺序是完全一致的,这就是Webpack模块打包的奥秘。 

 类似资料: