《二》Webpack模块打包

CommonJS:

模块:

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

将一个JS文件直接通过scripts标签插入页面中与封装成CommonJS模块最大的不同在于,前者的顶层作用域是全局作用域,在进行变量及函数声明时会污染全局环境;而后者会形成一个属于模块自身的作用域,所有的变量及函数只有自己能访问,对外是不可见的。

//calculator.js
var name = 'calculator.js;

//index.js
require('./calculator.js');
var name = 'index.js';
console.log(name); // index.js

在index.js中通过CommonJS的require函数加载calculator.js,运行之后控制台打印的结果是"index.js"。这说明calculator.js中的变量声明并不会影响index.js。

导出:

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

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

CommonJS模块内部会有一个module对象用于存放当前模块的信息,可以理解成在每个模块的最开始定义了以下对象;module.exports用来指定该模块要对外暴露哪些内容。

var module = {...};
module.exports  ={...};

为了书写方便,CommonJS也支持另一种简化的导出方式,直接使用exports。

但不要直接给exports赋值,否则会导致失败。
因为直接对exports进行了赋值操作,使其指向了新的对象,module.exports却仍然是原来的空对象,因此添加的属性并不会被导出。

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

//失败
exports = {
	name:'calculator',
	add: function(a,b){
		return a+b;
	}
}

其内在机制是将exports指向了module.exports,而module.exports在初始化时是一个空对象,可以简单地理解为,CommonJS在每个模块的首部默认添加了以下代码:

var module = {
	exports:{}
}
var exports = module.exports;

导出语句不代表模块的末尾,在module.exports或exports后面的代码依旧会照常执行。在实际使用中,为了提高可读性,应该将module.exports及exports语句放在模块的末尾。

导入:

在CommonJS中使用require进行模块导入。

当require一个模块时会有两种情况:

  1. require的模块是第一次被加载,这时会首先执行该模块,然后导出内容。
  2. require的模块曾经被加载过,这时该模块的代码不会再次执行,而是直接导出上次执行后得到的结果。

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

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

//index.js
const calculator = require('./calculator.js');
const sum = calculator.add(2,3);
console.log(sum);//5

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

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

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

require('./task.js');

ES6 Module:

模块:

ES6 Module也是将每个文件作为一个模块,每个模块拥有自身的作用域,不同的是导入、导出语句。import和export也作为保留关键字在ES6版本中加入了进来。

ES6 Module会自动采用严格模式,这在ES5中是一个可选项。以前可以通过选择是否在文件开始时加上"use strict"来控制严格模式,在ES6 Module中不管开头是否有"use strict"都会采用严格模式。

导出:

在ES6 Module中使用export命令来导出模块。export有两种形式:命名导出、默认导出。

一个模块可以有多个命令导出,它有两种不同的写法:

//写法1是将变量的声明和导出写在一行
export const name = 'calculator';
export const add = function(a,b){
	return a+b;
}

//写法2则是先进性变量声明,然后再用同一个export语句导出
const name = 'calculator';
const add = function(a,b){
	return a+b;
}
export {name,add};

在使用命令导出时,可以通过as关键字对变量重命名。

const name = 'calculator';
const add = function(a,b){
	return a+b;
}
export {name,add as getSum}; //在导入时即为name和getSum

与命名导出不同,模块的默认导出只能有一个。

export default {
	name:'calculator',
	add: function(a,b){
		return a+b;
	}
}

可以将export default理解为对外输出了一个名为default的变量,因此不需要像命名导出一样进行变量声明,直接导出值即可。

//导出字符串
export default 'This is calculator.js';
//导出class
export default class{...}
//导出匿名函数
export default function(){...}
导入:

ES6 Module中使用import语法导入模块。

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

//calculator.js
const name = 'calculator';
const add = function(a,b){
	return a+b;
}
export {name,add};

//index.js
import {name,add} from './calculator.js';
add(2,3);

与命名导出类似,可以通过as关键字对导入的变量命名。

import {name,add as calculateSum} from './calculator.js';
calculateSum(2,3);

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

import * as calculator from 'calculator .js';
console.log(calculator.add(2,3));
console.log(calculator.name);

对于默认导出来说,import后面直接跟变量名,并且这个名字可以自由指定,它指代了默认导出的值。

//calculator.js
export default {
	name:'calculator',
	add: function(a,b){
		return a+b;
	}
}

//index.js
import myCalculator from 'calculator.js';
myCalculator .add(2,3);

从原理上可以这样去理解:

import {default as myCalculator }  from 'calculator.js';

两种导入方式混合时,默认导出的变量名必须写在大括号前面,而不能顺序颠倒,否则会提示语法错误。

import React , {Companent} from 'react';

CommonJS与ES6 Module的区别:

动态与静态:

CommonJS与ES6 Module最本质的区别是前者对模块依赖的解决是动态的,而后者是静态的。动态的含义是,模块依赖关系的建立发生在代码运行阶段;而静态则是模块依赖的建立发生在代码编译阶段。

使用CommonJS,当模块A加载模块B时,会执行B中的代码,并将其module.exports对象作为require函数的返回值进行返回。并且require的模块路径可以动态指定,支持传入一个表达式,甚至可以通过if语句判断是否加载某个模块。因此,在CommonJS模块被执行前,并没有办法确定明确的依赖关系,模块的导入、导出发生在代码的运行阶段。

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

  1. 死代码检测和排除。可以用静态分析工具检测出哪些模块没有被调用过。比如,在引入工具类库时,工程中往往只用到其中一部分组件或接口,但有可能会将其代码完整地加载进来,未被调用到的模块代码永远不会被执行,也就成为了死代码,通过静态分析可以在打包时去掉这些未曾使用过的代码,以减少打包资源体积。
  2. 模块变量类型检查。JS属于动态类型语言,不会在代码执行前检查类型错误,ES6 Module的静态模块结构有助于确保模块之前传递的值或接口类型是正确的。
  3. 编译器优化。在CommonJS等动态模块系统中,无论采用哪种方式,本质上导入的都是一个对象,而ES6 Module支持直接导入变量,减少了引入层级,程序效率更高。
值拷贝与动态映射:

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

CommonJS中的值拷贝:

//calculator.js
var count = 0;
module.exports = {
    count:count,
    add:function(a,b){
        count += 1;
        return a+b;
    }
}

//index.js
var count = require('./calculator.js').count;
var add = require('./calculator.js').add;
console.log(count); //0。这里的count是对calculator.js中count值的拷贝
add(2,3);
console.log(count); //0。calculator.js中变量值的改变不会对这里拷贝值造成影响
count += 1;
add(2,3);
console.log(count); //1。拷贝的值可以更改

index.js中的count是对calculator.js中count的一份值拷贝,因此在调用add函数时,虽然更改了原本calculator.js中count的值,但是并不会对index.js中导入时创建的副本造成影响;另一方面,在CommonJS中允许对导入的值进行更改,在index.js中更改count将其赋于新值,由于是值得拷贝,这些操作不会影响calculator.js本身。

ES6 Module中值的动态映射:

//calculator.js
let count = 0;
const add = function(a,b){
    count += 1;
    return a+b;
}
export {count ,add};

//index.js
import {count,add} from './add-content.js';
console.log(count); //0。对calculator.js中count值的映射
add(2,3);
console.log(count); //1。实时反映calculator.js中count值的变化
count += 1;
add(2,3);
console.log(count); //不可更改,会抛出错误

ES6 Module中导入的变量其实是对原有值的动态映射,index.js中的count是对calculator.js中的count值的实时反映,当通过调用add函数更改了calculator.js中count值时,index.js中count的值也随之变化;不可以对ES6 Module导入的变量进行更改,可以将这种映射关系理解为一面镜子,从镜子里可以实时观察到原有的事物,但是并不可以操纵镜子里的镜像。

模块打包原理:

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

//index.js
const calculator= require('./calculator.js');
const sum = calculator.add(2,3);
console.log(sum);

经过Webpack打包后将会成为如下形式(为了易读性只展示代码的大体结构):
在这里插入图片描述
上面的bundle分别以下几个部分:

  1. 最外层立即执行匿名函数。它用来包裹整个bundle,并构成自身的作用域。
  2. installedModules对象。每个模块只在第一次被加载的时候,之后其导出值就会被存储到这个对象里面,当再次被加载的时候直接从这里取值,而不会重新执行。
  3. __webpack__require函数。对模块加载的实现,在浏览器中可以通过调用__webpack__require__(module_id)来完成模块导入。
  4. modules对象。工程中所有产生依赖关系的模块都会以key-value的形式放在这里。key可以理解为一个模块的id,由数字或者一个很短的hash字符串构成;value则是由一个匿名函数包裹的模块实体,匿名函数的参数则赋予了每个模块导出和导入的能力。

啊啊

bundle是如何在浏览器中执行的呢?

  1. 在最外层的匿名函数中会初始化浏览器执行环境,包括定义installedModules对象、 __webpack__require__函数等,为模块的加载和执行做一些准备工作。
  2. 加载模块入口文件。每个bundle都有且只有一个入口文件,在浏览器中会从它开始执行。
  3. 执行代码模块,如果执行到了module.exports,则记录下模块的导出值;如果中间遇到require函数(准确地说是__webpack__require__),则会暂时交出执行权,进入__webpack__require__函数体内进行加载其他模块的逻辑。
  4. __webpack__require__中会判断即将加载的模块是否存在于installedModules中,如果存在则直接取值,否则会到第三步,执行该模块的代码来获取导出值。
  5. 所以依赖的模块都已执行完毕,最后执行权又回到入口模块。当入口模块的代码执行到结尾,也就意味着整个bundle运行结束

猜你喜欢

转载自blog.csdn.net/wsln_123456/article/details/107056474