为什么从“..”父索引文件的相对导入会导致异步模块解析行为?
Why do relative imports from a '..' parent index file cause asynchronous module resolution behaviour?
我有一个结构如下的回购协议:
src/
index.ts
main.ts
bar/
file.ts
index.ts
foo/
fooFile.ts
src/index
只是一个顶级索引文件,可以导出我包中的所有内容。
但是,我要追踪的行为实际上需要我调用此文件中的某些功能,稍后我会详细介绍。
src/bar/file.ts
导出纯字符串。
src/foo/fooFile.ts
从父 ..
索引导入该字符串。
// comments relate to what happens if you run `node lib/index.js`
import { fileName } from "..";
import {fileName as fileName2} from "../bar";
export const test = "test";
const myData = {
data: fileName, // This resolves to undefined,
data2: fileName2 //This resolves to "bar"
};
export function main() {
console.log(myData);
console.log(fileName); // This resolves to "bar"
}
如果我的 src/index.ts
看起来像:
import {main} from "./foo/fooFile";
export * from "./bar";
export * from "./foo/fooFile";
main();
然后我们得到这种异步行为 - 其中从 ..
导入 fileName
在声明 myData
const 时解析为未定义,但在运行时解析字符串。
而如果我从 ../bar
导入,那么我在两个实例中都得到了字符串。
即。输出:
{ data: undefined, data2: 'bar' }
bar
但是,这种行为似乎只在我从索引文件调用 main()
函数时才会发生。如果我在 main.ts
中做同样的事情
import {main} from "./index";
main();
我不明白这种行为。
{ data: 'bar', data2: 'bar' }
bar
我想这种行为的原因与节点模块解析和循环依赖有关 - 谁能准确解释为什么会出现这种行为?
此处的回购:https://github.com/dwjohnston/import-from-parent-issue
请注意,我使用 TypeScript 创建了此重现 - 我想这不是问题的原因 - 但这是重现我实际面临的问题的最佳方式。
你说得对,这是因为循环依赖。
那么第一种情况会发生什么:
- 开始处理
index.ts
index.ts
进口 foo/fooFile.ts
foo/fooFile.ts
的处理已开始
foo/fooFile.ts
进口 index.ts
index.ts
已经被处理,在 nodejs 而不是 export blabla
中,您将使用 exports
对象并为其分配属性,然后该对象将从 require
当你 require
这个文件在某处时的功能。如果存在循环依赖,则返回“未完成”exports
对象,因此它会获得已经分配给它的属性,但是 nodejs 不会尝试继续执行 exports
这个对象到的文件获取由于循环依赖而丢失的属性。所以本例中的“未完成”exports
对象只是一个空对象,因为我们没有导出任何东西。这意味着如果您在 foo/fooFile.ts
中执行 import * as obj from '..'
,则此 obj
将只是一个空对象。由于您导入 { fooFile }
,fooFile
变为未定义
剩下的过程应该很清楚了,当main
函数触发时,index.ts
已经导出了一些变量,包括file
变量,所以main
功能可以使用它。
现在是第二种情况。我想看到这个问题我应该删除 index.ts
中的 main()
行,因为如果我不这样做 - 我没有观察到你在说什么,它仍然显示 { data: undefined, data2: 'bar' }
.
所以我删除了 index.ts
中的 main()
行。看起来一切都应该是一样的,因为无论如何你导入相同的 index.ts
文件,如果不是因为一个小问题,它实际上会是这样:
Note that I've created this repro using TypeScript - I imagine that this isn't the cause of the issue
实际上有点像。如果你在这两种情况下编译这个项目并打开 index.ts
文件,你会看到这个。第一种情况:
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
var fooFile_1 = require("./foo/fooFile");
__exportStar(require("./bar"), exports);
__exportStar(require("./foo/fooFile"), exports);
fooFile_1.main();
第二种情况,当我注释掉main()
:
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./bar"), exports);
__exportStar(require("./foo/fooFile"), exports);
// main()
注意到区别了吗?如果删除 main()
,那么 index.ts
中的第一个导入,即 import { main } from './foo/fooFile'
就没有用了。通常打字稿不会删除它,因为即使 import 没用,它也可能会产生一些副作用,但在这里你无论如何都要从 fooFile
稍后导入一些东西:export * from './foo/fooFile'
。好吧,不是导入而是再导出,但是在 nodejs 的上下文中没有任何区别。
所以现在一切都开始变得有意义了:
- 处理
main.ts
已启动,它导入 index.ts
- 处理
index.ts
已开始
index.ts
导入 ./bar
,获取 fileName
并导出它。现在这个“未完成的”exports
对象不是空的,它包含 { fileName: 'bar' }
index.ts
进口 ./foo/fooFile
./foo/fooFile
导入 index.ts
,但它已经导出 fileName
因此可以使用
剩下的应该清楚了
您可以向自己保证这就是实际发生的情况,只是改变了导入的顺序。要么在开头手动添加一行 require('./foo/fooFile')
到已编译的 index.js
文件,这样您将像第一种情况一样保留导入顺序,并且什么都不会改变。或者在 index.ts
中交换 ./bar
和 ./foo/fooFile
导入,那将是相同的。
这部分是由 typescript 引起的,因为如果你只是 运行 使用 nodejs 的这些文件,它不会删除顶部未使用的导入并且不会再次发生任何变化
我有一个结构如下的回购协议:
src/
index.ts
main.ts
bar/
file.ts
index.ts
foo/
fooFile.ts
src/index
只是一个顶级索引文件,可以导出我包中的所有内容。
但是,我要追踪的行为实际上需要我调用此文件中的某些功能,稍后我会详细介绍。
src/bar/file.ts
导出纯字符串。
src/foo/fooFile.ts
从父 ..
索引导入该字符串。
// comments relate to what happens if you run `node lib/index.js`
import { fileName } from "..";
import {fileName as fileName2} from "../bar";
export const test = "test";
const myData = {
data: fileName, // This resolves to undefined,
data2: fileName2 //This resolves to "bar"
};
export function main() {
console.log(myData);
console.log(fileName); // This resolves to "bar"
}
如果我的 src/index.ts
看起来像:
import {main} from "./foo/fooFile";
export * from "./bar";
export * from "./foo/fooFile";
main();
然后我们得到这种异步行为 - 其中从 ..
导入 fileName
在声明 myData
const 时解析为未定义,但在运行时解析字符串。
而如果我从 ../bar
导入,那么我在两个实例中都得到了字符串。
即。输出:
{ data: undefined, data2: 'bar' }
bar
但是,这种行为似乎只在我从索引文件调用 main()
函数时才会发生。如果我在 main.ts
import {main} from "./index";
main();
我不明白这种行为。
{ data: 'bar', data2: 'bar' }
bar
我想这种行为的原因与节点模块解析和循环依赖有关 - 谁能准确解释为什么会出现这种行为?
此处的回购:https://github.com/dwjohnston/import-from-parent-issue
请注意,我使用 TypeScript 创建了此重现 - 我想这不是问题的原因 - 但这是重现我实际面临的问题的最佳方式。
你说得对,这是因为循环依赖。
那么第一种情况会发生什么:
- 开始处理
index.ts
index.ts
进口foo/fooFile.ts
foo/fooFile.ts
的处理已开始foo/fooFile.ts
进口index.ts
index.ts
已经被处理,在 nodejs 而不是export blabla
中,您将使用exports
对象并为其分配属性,然后该对象将从require
当你require
这个文件在某处时的功能。如果存在循环依赖,则返回“未完成”exports
对象,因此它会获得已经分配给它的属性,但是 nodejs 不会尝试继续执行exports
这个对象到的文件获取由于循环依赖而丢失的属性。所以本例中的“未完成”exports
对象只是一个空对象,因为我们没有导出任何东西。这意味着如果您在foo/fooFile.ts
中执行import * as obj from '..'
,则此obj
将只是一个空对象。由于您导入{ fooFile }
,fooFile
变为未定义
剩下的过程应该很清楚了,当main
函数触发时,index.ts
已经导出了一些变量,包括file
变量,所以main
功能可以使用它。
现在是第二种情况。我想看到这个问题我应该删除 index.ts
中的 main()
行,因为如果我不这样做 - 我没有观察到你在说什么,它仍然显示 { data: undefined, data2: 'bar' }
.
所以我删除了 index.ts
中的 main()
行。看起来一切都应该是一样的,因为无论如何你导入相同的 index.ts
文件,如果不是因为一个小问题,它实际上会是这样:
Note that I've created this repro using TypeScript - I imagine that this isn't the cause of the issue
实际上有点像。如果你在这两种情况下编译这个项目并打开 index.ts
文件,你会看到这个。第一种情况:
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
var fooFile_1 = require("./foo/fooFile");
__exportStar(require("./bar"), exports);
__exportStar(require("./foo/fooFile"), exports);
fooFile_1.main();
第二种情况,当我注释掉main()
:
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./bar"), exports);
__exportStar(require("./foo/fooFile"), exports);
// main()
注意到区别了吗?如果删除 main()
,那么 index.ts
中的第一个导入,即 import { main } from './foo/fooFile'
就没有用了。通常打字稿不会删除它,因为即使 import 没用,它也可能会产生一些副作用,但在这里你无论如何都要从 fooFile
稍后导入一些东西:export * from './foo/fooFile'
。好吧,不是导入而是再导出,但是在 nodejs 的上下文中没有任何区别。
所以现在一切都开始变得有意义了:
- 处理
main.ts
已启动,它导入index.ts
- 处理
index.ts
已开始 index.ts
导入./bar
,获取fileName
并导出它。现在这个“未完成的”exports
对象不是空的,它包含{ fileName: 'bar' }
index.ts
进口./foo/fooFile
./foo/fooFile
导入index.ts
,但它已经导出fileName
因此可以使用
剩下的应该清楚了
您可以向自己保证这就是实际发生的情况,只是改变了导入的顺序。要么在开头手动添加一行 require('./foo/fooFile')
到已编译的 index.js
文件,这样您将像第一种情况一样保留导入顺序,并且什么都不会改变。或者在 index.ts
中交换 ./bar
和 ./foo/fooFile
导入,那将是相同的。
这部分是由 typescript 引起的,因为如果你只是 运行 使用 nodejs 的这些文件,它不会删除顶部未使用的导入并且不会再次发生任何变化