使用 require.resolve() 来计算 Node.js 相对于模块的文件路径

原文 by Ben Nadel

这个周末,我开始看 Express.js - 这个(应该是)Node.js 最老且最流行的网络框架了。Node.js 我已经用了几年了,但我从未真正构建过一个完整的网络应用。当我阅读的时候,遇到了从未见过的 – 一篇 StrongLoop 的文章 使用 require.resolve 来计算相对于模块的文件路径。以前我只用过模块的本地变量 __dirname 来计算相对于模块的文件路径,所以现在我想花点实践看看 require.resolve() 是如何工作的。

Node.js 里,模块加载器 - require() - 可以使用相对于模块的路径。例如,在文件系统的任意目录,有一个模块需要加载自己的库,可以安全地使用相对于模块的文件路径来引用这个库:

1
require("../lib/thinger")

不使用模块加载器的时候,文件路径一般要求是绝对或者相对于应用执行环境的根目录的。例如,读取文件内容时,需要给 fs.readFileSync 方法提供一个非本地的路径。我常使用 __dirname 变量作为计算非本地路径的方法:

1
fs.readFileSync(path.join(__dirname, "local-file.txt"))

这会产生一个由模块目录和提供的文件名拼接而成的路径。这种方法没毛病,但是,如果能更自然地计算相对于模块的文件路径就更好了。这也是 require.ensure() 吸引我眼球的原因。require.ensure() 使用和 require 相同的机制来加载模块 – 也就是说它可以使用相对于模块的路径;但是,它并不加载模块,而只是返回表明模块的文件路径。这个相对于模块的文件绝对路径就可以用来,比如,读取所代表模块的内容。

实践一下,创建一个简单的模块,来定位和读取同目录下的一个文件的内容。但是,为了确保不会创建相对于根目录的文件路径,本模块从子目录加载 demo。

1
2
3
// We want to request a module in a sub-directory so that module-relative paths aren't
// accidentally ALSO RELATIVE to the root of the application.
require( "./sub/test" );

然后,从子目录的 demo 模块,我使用 __dirnamerequire.resolve() 来生成相对于模块的文件路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Require the core node modules.
var chalk = require( "chalk" );
var fileSystem = require( "fs" );
var path = require( "path" );
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// Traditionally, I have used the "__dirname" as a way to calculate module-relative
// paths since "__dirname" provides the absolute path to the current module directory.
// That way, any relative-path appended to the "__dirname" generates a module-relative
// path to the given location.
console.log( chalk.red.bold( "Using __dirname:" ) );
console.log( path.join( __dirname, "data.txt" ) );
console.log( chalk.dim( fileSystem.readFileSync( path.join( __dirname, "data.txt" ) ) ) );
console.log( "" );
// The require.resolve() method uses the same mechanics for locating a module (ie, the
// thing you normally "require"); but, instead of loading it, .resolve() returns the
// calculated path to the given module. And, since require() can use module-relative
// paths, so can require.resolve().
console.log( chalk.red.bold( "Using require.resolve():" ) );
console.log( require.resolve( "./data.txt" ) );
console.log( chalk.dim( fileSystem.readFileSync( require.resolve( "./data.txt" ) ) ) );

可以看到,我使用这两种不同的方式只是定位并读入文件。然后,当我们运行根目录的 node 应用时,得到下图的输出:

<>

可以看到,两种方法使用相对于模块的路径计算出了相同的绝对路径。而且两种方法都可以读取文件的内容。

起初,看上去此两种都可以生成相对于模块的文件路径。但 require.resolve() 有一些很重要的注意点。一开始,它就会和文件系统交互。因为 require.resolve() 使用和 require() 一样的模块解析算法,它实际上会定位模块。也就是说,它会查找文件系统来确认文件是否存在。这会增加处理负担(大概会比 __dirname 方法要多)。还有随之而来的第二个注意点:它只能用于存在的文件。这意味着,不能用 require.resolve() 来产生相对于模块的文件路径以保存新文件。

为了讲清我的意思,我将更新上例,加入一个多余的调用来解析一个并不存在的相对于模块的文件路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Require the core node modules.
var chalk = require( "chalk" );
var fileSystem = require( "fs" );
var path = require( "path" );
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// Executing a no-op call to resolve a module-local file path to a non-existent file
// in order to demonstrate that this is doing more than just calculating file paths.
require.resolve( "./my-new-file.txt" );
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// Traditionally, I have used the "__dirname" as a way to calculate module-relative
// paths since "__dirname" provides the absolute path to the current module directory.
// That way, any relative-path appended to the "__dirname" generates a module-relative
// path to the given location.
console.log( chalk.red.bold( "Using __dirname:" ) );
console.log( path.join( __dirname, "data.txt" ) );
console.log( chalk.dim( fileSystem.readFileSync( path.join( __dirname, "data.txt" ) ) ) );
console.log( "" );
// The require.resolve() method uses the same mechanics for locating a module (ie, the
// thing you normally "require"); but, instead of loading it, .resolve() returns the
// calculated path to the given module. And, since require() can use module-relative
// paths, so can require.resolve().
console.log( chalk.red.bold( "Using require.resolve():" ) );
console.log( require.resolve( "./data.txt" ) );
console.log( chalk.dim( fileSystem.readFileSync( require.resolve( "./data.txt" ) ) ) );

如你所见,我试图解析一个并不存在相对于模块的文件路径, 即 my-new-file.txt。运行结果如下:

<>

如你所见,整个 demo 崩溃就是因为我们试图解析一个不存在的文件的路径。模块解析算法冒泡似地展示出来了。

很明显,require.resolve() 所做的远不止生成文件路径那么简单–它实际上在和文件系统交互。正因如此,它会带来更大的处理负担,在使用方式上也有一些限制。但是,它能提供一种更自然的计算相对于模块的文件路径的方式。所以,可能在大部分情况下我想我不会用这种方法,但用来读取像设置文件那样的,只会读一次的文件,还是不错的。

简化事件监听函数写法

相比jQuery或者zepto, 原生js的写法有时太过繁琐。下面是一个简化监听函数的写法的小技巧。

从简单的开始,比如console.log, 可以写成const log = console.log

用这个思路简化事件监听函数:

1
const listen = EventTarget.addEventListener

这里EventTarget需要修改成目标元素,想到使用call/apply

1
2
3
listen.call(targetElement, "click", event => {
// ...
})

call/apply可以用来改变执行时this的指向。

这样还是要每次都写call, 这种简化还是不够理想, 是时候祭出bind – 它可以绑定this:

1
2
3
4
5
let listen = Function.prototype.call.bind(document.addEventListener)
listen(targetElement, "click", event => {
// ...
})

简化至此完成,效果还可以😊。

Proudly powered by Hexo and Theme by Hacker
© 2017 XGZ