Webpack入门,模块打包之CommonJS与ES6 Module的区别

前面几篇文章分别介绍了CommonJS和ES6 Module两种形式的模块定义,这篇将介绍下两者各自的特性。

动态与静态

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

// calculator.js
module.exprots = {name: 'calculator'};
// index.js
const name = require('./calculator.js').name;
console.log(name);

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

// calculator.js
export const name = 'calculator';

// index.js
import { name } from './calculator.js';

ES6 Module的导入、导出语句都是声明式的,它不支持将表达式作为导入路径,并且导入、导出语句必须位于模块的顶层作用域,也不能放在if语句中。因此说,ES6 Module是一种静态的模块结构,在ES6代码的编译阶段就可以分析出模块的依赖关系。

相比CommonJS,ES6 Module具备以下优势:

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

值复制与动态映射

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

可以通过示例来了解一下什么是CommonJS中的值复制。

// calculator.js
var count = 0;
module.exoports = {
  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(5,6);
console.log(count); // 输出 0 (calculator.js中变量值得改变不会对这里的副本造成影响)
count += 1;
console.log(count); // 输出 1 (副本的值可以更改)

index.js中的count是calculator.js中count的一份副本,因此在调用add函数时,虽然改变了原本calculator.js中count值,但是并不会对index.js中导入时创建的副本造成影响。另一方面,在CommonJS中允许导入的值进行更改。开发者可以在index.js中更改count和add,将其赋予新值。同样,由于是值得副本,这些操作不会影响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 './calculator.js';
console.log(count); // 输出 0 (对calculator.js中count值得映射)

add(5,6);
console.log(count); // 输出 1 (实时反映calculator.js中count值得变化)

count += 1; // 不可更改,会抛出SyntaxError:"count" is readonly

上面的额示例展示了ES6 Module中导入的变量其实是对原有值得动态映射。index.js中count是对calculator.js中count值得实时反映,当通过调用add函数更改了calculatot.js中的count值时,index.js中count的值也随之变化。并且ES6 Module规定不能对导入的变量进行修改。

循环依赖

循环依赖是指模块A依赖于模块B,同时模块B依赖于模块A。

一般来说工程中应该避免循环依赖的产生,因为从软件设计的角度来说,单向的依赖关系更加清晰,循环依赖则会带来一定的复杂度。但是在实际开发中,循环依赖有时会在不经意间产生,因为当工程的复杂度上升到足够大时,就容易出现隐藏的循环依赖关系。

首先通过示例来了解一下循环依赖在CommonJS中的表现。

// foo.js
const bar = require('./bar.js');
console.log('value of bar:',bar);
module.exports = "This is foo.js";
// bar.js
const foo = require('./foo.js');
console.log("value of foo:", foo);
module.exoports = "This is bar.js";
// index.js
require('./foo.js');

在示例中,index.js是执行入口,它加载了foo.js,foo.js和bar.js之间存在循环依赖。

开发者的本意是想在控制台输出

value of foo: This is foo.js
value of bar: This is bar.js

而实际运行上面的示例,输出的却是

value of foo: {}
value of bar: This is bar.js

结果为什么会与预期的不一样呢?梳理下代码的实际的执行顺序就会明白了。

  1. index.js导入了foo.js,此时开始执行foo.js中的代码。
  2. foo.js的第一句导入了bar.js,这时foo.js不会继续向下执行,而是会进入bar.js内部。代码的执行权就到了bar.js。
  3. 在bar.js中又对foo.js进行了导入,这里产生了循环依赖。需要注意的是,代码的执行权并不会再交回给foo.js,而是直接取其导出值,也就是module.exports。但是由于foo.js中的代码未执行完毕,导出值在这时为默认的空对象,因此当bar.js执行到打印语句时,控制台就会输出一个空对象,即 value of foo: {}
  4. 当bar.js中的代码执行完毕,代码的执行权就会交回给foo.js,foo.js中的代码从require语句的下一句开始执行,也就是foo.js中的输出语句开始执行,由于bar.js中的代码已经执行完毕,bar.js中的module.exports已经被赋值,此时控制台输出value of bar: This is bar.js是正确的。foo.js中的代码执行完毕,整个流程结束。

通过代码的执行顺序可以看出,尽管循环依赖的模块被执行了,但是模块导入的值并不是开发者想要的。因此如果再CommonJS中遇到循环依赖,将没有办法得到预想中的结果。

提示

为什么在步骤3中,直到bar.js中代码执行完毕才会将代码的执行权交回给foo.js呢?这就跟webpack模块加载机制有关了。Webpack加载模块时,会首先判断模块是否已被加载,如果该模块事先没有被加载过,就加载该模块,执行该模块中的代码;如果该模块事先已被加载,则直接返回该模块的导出值,即module.exports;

接下来通过ES6 Module的方式改写上面的示例。

// foo.js
import bar from "./bar.js";
console.log("value of bar:",bar);
export default "This is foo.js";
// bar.js
import foo from "./foo.js";
console.log("value of foo:",foo);
export default "This is bar.js";
// index.js
import foo from ""./foo.js;

理想状态下,示例的实际运行结果:

value of foo: undefined
value of bar: This is bar.js

代码的执行顺序与之前的并无太大的区别。区别在于在ES6 Module中,只是生成一个指向被加载模块的引用,代码未执行时,这个引用的值就是undefined。所以当代码执行到bar.js中的console.log("value of boo:",boo)时,boo的值就是undefined。

提示

示例代码如果运行在Webpack环境下,会出现异常报错:Cannot access '__WEBPACK_DEFAULT_EXPORT__' before initialization。这时因为在webpack编译时,会把 es6 降级为 es5,降级处理方式上可能会有一定的差异。并且,在ES6 Module中处理引用的方式与Webpack也不相同。在ES6 Module的处理方式是,先静态分析import,然后动态export导出(导出引用),webapck的处理方式是,先将所有export提到了模块的开始,然后import提升。

示例代码仓库

https://gitee.com/zero_79152105/webpack-vblog

原创文章,作者:ZERO,如若转载,请注明出处:https://www.edu24.cn/course/webpack-commonjs-esm.html

Like (0)
Donate 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
ZEROZERO
Previous 2022年11月11日
Next 2022年11月14日

相关推荐

  • js数组去重(区分object、“NaN”、NaN)

    数组去重在前端面试中比较常见,今天来复习复习。 对这样一个数组进行去重,我尝试了几种方法,大多数不能对对象去重,或者不能区分true和”true”、NaN和…

    2021年2月23日
    1.2K
  • 一维数组结构数据转换树形结构数据JS方法

    在写小程序项目时,自定义了一个组织机构树形展示组件,后端接口返回的组织机构数据是一维数组。需要在前端转换成树形结构的数据,并且添加一些节点的树形,比如是否为叶子节点,节点是否展开等…

    2022年11月10日
    382
  • 网页布局之三栏网页宽度自适应布局

    在工作中经常遇到网页布局错乱的问题,往往引发的这种问题都是因为不同设备不同分辨率而导致。归根结底,是因为前端工程师经验不足,代码写得不够完好。以下是我总结及从网络搜集的一些网页布局…

    2018年10月8日
    2.9K
  • CSS让内容居中的方法总结

    内容水平居中  分行内元素与块级元素两种情况;其中块级元素又分定宽与不定宽两种情况; 行内元素 首先看它的父元素是不是块级元素,如果是,则直接给父元素设置 te…

    2019年6月28日
    3.2K
  • 从零开始开发vue组件库

    前言 很早之前,就有开发一套vue组件库的想法,直到现在想法依旧只是想法。汗颜啊……此篇文章将讲述如何开发vue组件库,虽然文章标题为《从零开始开发vue组件库》,实际上是从搭建v…

    2024年6月23日
    493
  • Webpack入门,资源处理流程

    在前几篇的文章中,梳理了Webpack的模块打包功能。可以把Webpack打包模块理解成类似于工厂中的产品组装,也就是把一个个零部件拼起来得到最终的成品。接下来的几篇,则主要要关注…

    2022年11月18日
    528
  • JavaScript基础知识八问

    JavaScript是前端开发中非常重要的一门语言,浏览器是他主要运行的地方。JavaScript是一个非常有意思的语言,但是他有很多一些概念,大家经常都会忽略。比如说,原型,闭包…

    2020年12月30日
    928
  • 如何搭建MyBatis开发环境

    进入一段时间的学习及温习,已经可以说是初步掌握了Javaweb入门开发,由于我的中心思想是抛弃JSP,做纯粹的前后端分离项目,所以接下来计划学习持久层开发,现在主流的持久层开发工具…

    2022年4月6日
    692
  • Vue项目中实现用户登录及token验证

    在前后端完全分离的情况下,Vue项目中实现token验证大致思路如下: 第一次登录的时候,前端调后端的登陆接口,发送用户名和密码 。 后端收到请求,验证用户名和密码,验证成功,就给…

    2019年8月8日
    4.9K
  • Webpack入门,预处理器

    一个Web工程通常会包含HTML、JS、CSS、图片、字体等多种类型的静态资源,且这些资源之间都存在着某种联系。对于Webpack来说,所有这些静态资源都是模块,开发者可以像加载一…

    2022年11月21日
    569