模块打包中CommonJS与ES6 Module的导入与导出问题详解

简介: 文章全面解析了CommonJS模块系统的模块定义、导出、导入的操作和注意事项。同时,它也简要地提到了ES6 Module的相关概念,包括命名导出、默认导出、命名导入、默认导入、混合导入和复合写法。

CommonJS

CommonJS模块

CommonJS中规定每个文件是一个模块。每个模块是拥有各自的作用域的,各自作用域的变量互不影响。

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

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

这里可以看到,导入calculator.js并不会覆盖index.js中的name字段
这样做区别于直接用<script>标签插入页面中的好处在于
插入<script>标签后顶层作用域是全局作用域,在进行变量及函数声明时会污染全局环境;而封装成CommonJS模块会形成一个属于模块自身的作用域,所有的变量及函数只有自己能访问,对外是不可见的。

CommonJS模块导出

下面两种写法实质上是一样的

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

等同于

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

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

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

因此,为exports.add赋值相当于在module.exports对象上添加了一个add属性。
注意点一:不要直接给exports赋值,否则会导致其失效。 如:

exports = {
   
    name: 'calculater'
};

上面代码中,由于对exports进行了赋值操作,使其指向了新的对象{name: 'calculater'}module.exports却仍然是原来的空对象,因此name属性并不会被导出。

注意点二:不要把module.exportsexports混用。

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

上面的代码先通过exports导出了add属性,相当于module.exports = { add: function(){...}}然后将module.exports重新赋值为另外一个对象。这会导致原本拥有add属性的对象丢失了,最后导出的只有name

注意点三:导出语句不代表模块的末尾

module.exports = {
   
    name: 'lcylcy'
};
console.log('end');

module.exportsexports后面的代码依旧会照常执行。比如上面的console会在控制台上打出“end”,但在实际使用中,为了提高可读性,不建议采取上面的写法,而是应该将module.exportsexports语句放在模块的末尾。

CommonJS模块导入

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

// 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

我们在index.js中导入了calculator模块,并调用了它的add函数。当我们require一个模块时会有两种情况:

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

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

请看下面的例子:

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

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

控制台的输出结果如下:

running calculator.js
sum: 5
end

从结果可以看到,两次requirecalculator.js,但console.log('running calculator.js');只执行了一遍。模块会有一个module对象用来存放其信息,这个对象中有一个属性loaded用于记录该模块是否被加载过。它的值默认为false,当模块第一次被加载和执行过后会置为true,后面再次加载时检查到module.loadedtrue,则不会再次执行模块代码。有时我们加载一个模块,不需要获取其导出的内容,只是想要通过执行它而产生某种作用,比如把它的接口挂在全局对象上,此时直接使用require即可。

require('./test.js');

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

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

ES6 Module

ES6 模块

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

请看下面的例子,我们将前面的calculator.jsindex.js使用ES6的方式进行了改写。

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

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

ES6 Module会自动采用严格模式,这在ES5ECMAScript 5.0)中是一个可选项。以前我们可以通过选择是否在文件开始时加上“use strict”来控制严格模式,在ES6 Module中不管开头是否有“use strict”,都会采用严格模式。如果将原本是CommonJS的模块或任何未开启严格模式的代码改写为ES6 Module要注意这点。

ES6 Module导出

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

1.命名导出
2.默认导出

命名导出

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

// 写法1
export const name = 'calculator';
export const add = function(a, b) {
    return a + b; };

// 写法2
const name = 'calculator';
const add = function(a, b) {
    return a + b; };
export {
    name, add };

第1种写法是将变量的声明和导出写在一行;
第2种则是先进行变量声明,然后再用同一个export语句导出。
两种写法的效果是一样的。

在使用命名导出时,可以通过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导入

ES6 Module中使用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);

加载带有命名导出的模块时,那就要对应命名导入。import后面要跟{ }来将导入的变量名包裹起来,并且这些变量名需要与导出的变量名完全一致。

导入变量的效果相当于在当前作用域下声明了这些变量(nameadd),并且不可对其进行更改,也就是所有导入的变量都是只读的。

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

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

在导入多个变量时,我们还可以采用整体导入的方式。如:

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

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

默认导入

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

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

对于默认导出来说,那就要默认导入,import后面直接跟变量名,并且这个名字可以自由指定(比如这里是myCalculator),它指代了calculator.js中默认导出的值。
从原理上可以这样去理解:

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

混合导入

// index.js
import React, {
    Component } from 'react';

这里的React对应的是该模块的默认导出,而Component则是其命名导出中的一个变量。

注意:这里的React必须写在大括号前面,而不能顺序颠倒,否则会提示语法错误。

复合写法

复合写法在工程中,有时需要把某一个模块导入之后立即导出,比如专门用来集合所有页面或组件的入口文件。此时可以采用复合形式的写法:

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

等同于

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

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

import calculator from "./calculator.js ";
export default calculator;

不能写成export default from './calculator.js'
除非写为

export {
    default } from calculator;

但是这种方式依然还是命名导出而不是默认导出,命名的变量为default而已。

下篇:CommonJS与ES6 Module的本质区别



参考资料:

Webpack实战:入门、进阶与调优


扩展阅读:
阮一峰:ECMAScript 6 入门----Module 的语法





关注、留言,我们一起学习。





===============Talk is cheap, show me the code================

目录
相关文章
|
2月前
|
JavaScript 前端开发 编译器
将 CommonJS 模块转换为 ES6 模块
【10月更文挑战第11天】 将 CommonJS 模块转换为 ES6 模块有三种主要方法:手动修改代码、使用工具(如 Babel)自动转换和逐步迁移。手动修改涉及导出和导入方式的转换,确保名称和结构一致;使用工具可自动化这一过程;逐步迁移适用于大型项目,先在新模块中使用 ES6 语法,再逐步替换旧模块。转换过程中需注意兼容性、代码逻辑调整和充分测试。
227 58
|
3月前
|
JavaScript
es6模块中使用commonjs定义的库
es6模块中使用commonjs定义的库
|
4月前
|
JavaScript
使用 nuxi build-module 命令构建 Nuxt 模块
【8月更文挑战第29天】以下是使用 `nuxi build-module` 构建 Nuxt 模块的步骤:1. 确保已安装 Node.js 和 npm;2. 创建新目录并初始化 npm 项目;3. 安装 Nuxt 相关依赖;4. 创建模块结构,包括 `index.ts` 入口文件;5. 运行 `nuxi build-module` 构建模块;6. 在 Nuxt 项目中安装并配置该模块。确保遵循 Nuxt 最佳实践以保证稳定性和兼容性。
|
7月前
|
JavaScript 前端开发
CMD和UMD,ES Module的差别
CMD和UMD,ES Module的差别
|
JavaScript
面试题-TS(五):TypeScript 中的模块是什么?如何导入和导出模块?
在TypeScript中,模块(Modules)是一种用于组织和管理代码的概念。模块提供了一种封装代码的方式,允许我们将相关的功能和数据组织在一起,实现代码的可重用和可维护。
|
JavaScript 前端开发
模块化开发:CommonJS、AMD 和 ES6 Modules 的区别与使用方式
在前端开发中,模块化开发是一种重要的编程方法,它可以帮助我们更好地组织和管理代码,提高代码的可维护性和复用性。在JavaScript中,有多种模块化开发的标准,包括CommonJS、AMD和ES6 Modules。让我们逐一了解它们的区别和使用方式:
213 0
VSCode找不到自定义模块ModuleNotFoundError
VSCode找不到自定义模块ModuleNotFoundError
500 0
【vue2小知识】实现store中modules模块的封装与自动导入
store仓库中分模块时的需要每次导入index的问题
【vue2小知识】实现store中modules模块的封装与自动导入
5.ES6模块导出导入
5.ES6模块导出导入
83 0
|
JavaScript 前端开发 开发者
ES6模块化与导出(export)导入(import)的用法
1.ES6模块化的介绍 在 ES6 模块化规范诞生之前,JavaScript 社区已经尝试并提出了 AMD、CMD、CommonJS 等模块化规范。 但是,这些由社区提出的模块化标准,还是存在一定的差异性与局限性、并不是浏览器与服务器通用的模块化 标准,例如: ⚫ AMD 和 CMD 适用于浏览器端的 Javascript 模块化 ⚫ CommonJS 适用于服务器端的 Javascript 模块化 太多的模块化规范给开发者增加了学习的难度与开发的成本。因此,大一统的 ES6 模块化规范诞生了!
273 1
ES6模块化与导出(export)导入(import)的用法