模块打包中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================
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页