理解Node.js中的module.exports和exports

Node.js编程中,module是可共享和重复使用的自包含功能单元。它们使我们作为开发人员的生活更轻松,因为我们可以使用它们来增强我们的应用程序,而无需自己编写功能。它们还允许我们组织和解耦我们的代码,从而使应用程序更易于理解、调试和维护。

不同的Node.js模块格式

由于JavaScript最初没有模块的概念,随着时间的推移出现了各种竞争的格式。以下是主要的几种格式:

  • 异步模块定义(AMD)格式在浏览器中使用,并使用define函数来定义模块。
  • CommonJSCJS)格式在Node.js中使用,并使用requiremodule.exports来定义依赖关系和模块。npm生态系统是建立在这种格式之上的。
  • ES模块(ESM)格式。从ES6(ES2015)开始,JavaScript支持原生模块格式。它使用export关键字来导出模块的公共API,使用import关键字来导入它。
    System.register格式设计用于支持ES5中的ES6模块。
  • 通用模块定义(UMD)格式可以在浏览器和Node.js中使用。当模块需要被多个不同的模块加载器导入时,这是非常有用的。

    导入模块

    Node.js自带一组内置模块,我们可以在代码中使用它们而无需安装。为了做到这一点,我们需要使用require关键字来导入模块,并将结果赋值给一个变量。然后可以使用该变量来调用模块公开的任何方法。

例如,要列出目录的内容,可以使用文件系统模块及其readdir方法:

1
2
3
4
5
6
7
8
const fs = require('fs');
const folderPath = '/home/jim/Desktop/';

fs.readdir(folderPath, (err, files) => {
files.forEach(file => {
console.log(file);
});
});

请注意,在CommonJS中,模块是同步加载的,并按照它们出现的顺序进行处理。

创建并导出模块

现在让我们看看如何创建自己的模块并将其导出以供在程序的其他地方使用。首先创建一个名为user.js的文件,并添加以下内容:

1
2
3
4
5
const getName = () => {
return 'Jim';
};

exports.getName = getName;

现在在同一文件夹中创建一个名为index.js的文件,并添加以下内容:

1
2
const user = require('./user');
console.log(`User: ${user.getName()}`);

使用node index.js运行程序,你应该在终端看到以下输出:

1
User: Jim

那么这里发生了什么呢?如果你看一下user.js文件,你会注意到我们定义了一个getName函数,然后使用exports关键字使其可以在其他地方导入。然后在index.js文件中,我们导入了这个函数并执行它。还要注意,在require语句中,模块名以./为前缀,因为它是一个本地文件。还要注意,不需要添加文件扩展名。

导出多个方法和值

我们可以以相同的方式导出多个方法和值:

1
2
3
4
5
6
7
8
9
10
11
12
13
const getName = () => {
return 'Jim';
};

const getLocation = () => {
return 'Munich';
};

const dateOfBirth = '12.01.1982';

exports.getName = getName;
exports.getLocation = getLocation;
exports.dob = dateOfBirth;

然后在index.js中:

1
2
3
4
const user = require('./user');
console.log(
`${user.getName()} lives in ${user.getLocation()} and was born on ${user.dob}.`
);

以上代码产生了如下输出:

1
Jim lives in Munich and was born on 12.01.1982.

请注意,我们给导出的dateOfBirth变量起的名称可以是任意的(在这种情况下是dob)。它不必与原始变量名相同。

语法的变化

我还应该提一下,可以在文件中逐步导出方法和值,而不仅仅是在文件末尾。

例如:

1
2
3
4
5
6
7
8
9
exports.getName = () => {
return 'Jim';
};

exports.getLocation = () => {
return 'Munich';
};

exports.dob = '12.01.1982';

由于解构赋值的存在,我们可以挑选我们想要导入的内容:

1
2
3
4
const { getName, dob } = require('./user');
console.log(
`${getName()} was born on ${dob}.`
);

正如你所期望的那样,这会输出:

1
Jim was born on 12.01.1982.

导出默认值

在上面的例子中,我们单独导出函数和值。这对于可能在整个应用程序中需要的辅助函数来说非常方便,但是当您有一个仅导出一个东西的模块时,更常见的是使用module.exports

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class User {
constructor(name, age, email) {
this.name = name;
this.age = age;
this.email = email;
}

getUserStats() {
return `
Name: ${this.name}
Age: ${this.age}
Email: ${this.email}
`;
}
}

module.exports = User;

然后在index.js中:

1
2
3
4
const User = require('./user');
const jim = new User('Jim', 37, 'jim@example.com');

console.log(jim.getUserStats());

以上代码输出:

1
2
3
Name: Jim
Age: 37
Email: jim@example.com

module.exports和exports之间有什么区别?

在网上您可能会遇到以下语法:

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
getName: () => {
return 'Jim';
},

getLocation: () => {
return 'Munich';
},

dob: '12.01.1982',
};

在这里,我们将要导出的函数和值分配给了module上的一个exports属性 - 当然,这也完全可行:

1
2
3
4
const { getName, dob } = require('./user');
console.log(
`${getName()} was born on ${dob}.`
);

这将输出:

1
Jim was born on 12.01.1982.

那么module.exportsexports之间有什么区别呢?一个只是另一个的便捷别名吗?

嗯,有点,但并非完全如此…

为了说明我的意思,让我们将index.js中的代码更改为打印module的值:

1
console.log(module);

这会产生:

1
2
3
4
5
6
7
8
9
10
11
12
Module {
id: '.',
exports: {},
parent: null,
filename: '/home/jim/Desktop/index.js',
loaded: false,
children: [],
paths:
[ '/home/jim/Desktop/node_modules',
'/home/jim/node_modules',
'/home/node_modules',
'/node_modules' ] }

您可以看到,module有一个exports属性。让我们向其中添加一些内容:

1
2
3
4
5
6
7
8
9
10
11
// index.js
exports.foo = 'foo';
console.log(module);
这会输出:

javascript
Copy code
Module {
id: '.',
exports: { foo: 'foo' },
...

exports分配属性也会将它们添加到module.exports中。这是因为(至少在开始时)exports是对module.exports的引用。

应该使用哪一个呢?

由于module.exportsexports都指向同一个对象,通常使用哪一个都无所谓。例如:

1
2
exports.foo = 'foo';
module.exports.bar = 'bar';

这段代码会导出模块的对象为{ foo: 'foo', bar: 'bar' }

然而,需要注意一点。无论您将module.exports分配给什么,最终导出的都是您的模块。

所以,看下面的例子:

1
2
exports.foo = 'foo';
module.exports = () => { console.log('bar'); };

这只会导出一个匿名函数。foo变量会被忽略。

其他module.exportsexports的常见问题

  • module.exportsexportsNode.js中有什么区别?

    module.exports和exports都用于从模块中导出值。exports本质上是对module.exports的引用,所以您可以任选其一使用。然而,通常建议使用module.exports以避免潜在问题。

  • 我可以在同一个模块文件中同时使用module.exportsexports吗?

    是的,您可以在同一个模块中同时使用两者。但在这样做时要小心,最好保持一致性,坚持使用一种惯例。

  • 在使用module.exportsexports时有哪些常见问题?

    一个常见的是直接重新赋值exports,这可能导致意想不到的结果。最好使用module.exports以获得更好的一致性,并避免潜在的问题。

  • module.exportsexports之间是否有性能差异?

    两者之间没有明显的性能差异。选择它们更多地取决于编码风格和约定。

  • 我可以在浏览器中使用module.exportsexports吗,还是它们只适用于Node.js

    module.exportsexports是特定于Node.js的,在浏览器的JavaScript中不可用。在浏览器中,通常使用其他机制,如ES6模块进行导出和导入。

    总结

    module已经成为JavaScript生态系统的一个重要组成部分,使我们能够将大型程序组合成较小的部分。