在 Typescript 中防止不适当的导入和执行项目层次结构

Posted

技术标签:

【中文标题】在 Typescript 中防止不适当的导入和执行项目层次结构【英文标题】:Preventing inappropriate imports and enforcing project hierarchy in Typescript 【发布时间】:2020-07-29 04:45:24 【问题描述】:

在 TS 项目中,我希望阻止以下内容:

来自文件夹 common 的文件正在从文件夹 projectA 导入 来自文件夹projectB 的文件正在从文件夹projectA 导入

我希望允许以下内容:

文件夹 projectA 中的文件正在从文件夹 common 导入。

我知道参考。但是,据我了解,它们需要构建用于类型检查(如果进行了这种分离,则必须先构建以创建 d.ts 文件),我宁愿避免这样做。

我有什么选择?是否可以简单地通过每个项目/文件夹的单独 tsconfig 文件来实现?

【问题讨论】:

由于构建依赖关系,您避免使用项目引用的理由有多大?查看my answer中的对位点 【参考方案1】:

我建议使用 linter 来完成这项工作,无需调整构建步骤或使用项目参考。

eslint-plugin-import 是一个非常流行的 ESLint 插件,兼容 TS 并且可以为所欲为。配置 typescript-eslint 后(如果尚未完成),您可以使用以下规则:

import/no-restricted-paths - 限制可以在给定文件夹中导入哪些文件 import/no-relative-parent-imports - 防止导入到相对父路径中的文件夹 import/no-internal-modules - 防止导入其他模块的子模块

让我们尝试以下项目结构:

|   .eslintrc.js
|   package.json
|   tsconfig.json
\---src
    +---common
    |       common.ts
    |       
    +---projectA
    |       a.ts
    |       
    \---projectB
            b.ts

.eslintrc.js:

module.exports = 
  extends: ["plugin:import/typescript"],
  parser: "@typescript-eslint/parser",
  parserOptions: 
    sourceType: "module",
    project: "./tsconfig.json",
  ,
  plugins: ["@typescript-eslint", "import"],
  rules: 
    "import/no-restricted-paths": [
      "error",
      
        basePath: "./src",
        zones: [
          // disallow import from projectB in common
           target: "./common", from: "./projectB" , 
          // disallow import from projectB in projectA
           target: "./projectA", from: "./projectB" ,     
        ],
      ,
    ],
    "import/no-relative-parent-imports": "error",
  ,
;

每个 zone 都由 target 路径和 from 路径组成。目标是应用受限导入的路径。 from 路径定义了不允许在导入中使用的文件夹。

查看文件./src/common/common.ts

import  a  from "../projectA/a"; // works 
// Error: Unexpected path "../projectB/b" imported in restricted zone.
import  b  from "../projectB/b";

import/no-relative-parent-imports 规则也抱怨这两种导入,比如a.ts

不允许从父目录进行相对导入。请在运行时传递您要导入的内容(依赖注入),将common.ts 移动到与../projectA/a 相同的目录或考虑将../projectA/a 制作成一个包。

没有使用第三条规则import/no-internal-modules,但我也在此处列出,因为它可以非常有用地限制对子文件夹/模块的访问,并在 TS 中模拟(至少)某种package internal modifier。

【讨论】:

这个很有意思,我明天试试。但是,Typescript 解决方案(如果有这样的解决方案没有额外的构建)应该更好,因为它只会显示可用/允许的自动完成导入。 >"不允许从父目录进行相对导入。"如果我有一个文件ProjectA/innerFolder/innerFolder/file.ts 尝试从projectA/consts.ts 导入,它会被阻止吗? 嗯,我怀疑,有一个直接的 TS 功能,它不允许某些模块导入模式(或者至少我不知道)。如果您需要更强的封装,通常会创建另一个 npm 包。对于自动完成:可能有多个配置,这会限制输入,但这将有其自身的组织成本,并且不会阻止模块解析/导入它们。 是的,eslint 会在导入路径中包含../ 的所有内容上出错 @Inigo,我相信你写的是正确的。但是,如果您确实将 linter 视为工具链的重要组成部分,那么 1)所有错误都显示在编辑器中(取决于插件) 2)linter 在每次构建之前运行(取决于脚本)【参考方案2】:

TLDR;你真的应该只使用参考。这正是它们的用途。

但让我们先谈谈你的一些具体想法:

    是否可以通过单独的 tsconfig 文件为每个项目/文件夹简单地实现?

    是的,但它不是很灵活。您可以通过将其rootDir 设置为. 来隔离common。如果您尝试将 projectA 导入 common,则会收到 '/path/to/projectA' is not under 'rootDir' 错误。但是为了能够将 common 导入到 projectA,它的 rootDir 必须更加全局,但这样您就可以导入 projectB.

    不仅如此,根据Project References 文档:

    以前,如果您使用单个 tsconfig 文件,则使用这种结构相当笨拙:

    实现文件可以导入测试文件 如果没有src 出现在输出文件夹名称中,则无法同时构建testsrc,您可能不希望这样 仅更改实现文件中的 internals 需要再次类型检查测试,即使这不会导致新的错误 仅更改测试需要再次对实现进行类型检查,即使没有任何更改

    您可以使用多个 tsconfig 文件来解决 一些这些问题,但会出现新问题

    没有内置的最新检查,因此您最终总是运行 tsc 两次 两次调用 tsc 会导致更多的启动时间开销 tsc -w 不能同时在多个配置文件上运行

    我知道参考资料。但是,据我了解,它们需要构建用于类型检查(如果进行了这种分离,则必须先构建以创建 d.ts 文件),而我宁愿避免这样做。

    这种厌恶的原因是什么?

    如果是构建一个新的项目克隆的前期成本,那将远远超过构建时间的改进(参见下面的参数)。后者对开发人员生产力的好处将远远超过前者的成本。

    具有讽刺意味的是,您对前期成本的关注越大,缩短构建时间所带来的好处就越大!

    如果您希望能够在 VS Code 或 WebStorm 等类型和链接感知编辑器中导航新克隆而无需构建,您可以通过将 .d.ts 文件签入源代码控制来实现此目的。


    以下是文档的具体说明:

    由于依赖项目使用从其依赖项构建的 .d.ts 文件,因此您必须检查某些构建输出在克隆项目后构建项目,然后才能导航在编辑器中进行项目,而不会看到虚假错误。我们正在开发一个应该能够缓解这种情况的幕后 .d.ts 生成过程,但目前我们建议通知开发人员他们应该在克隆后构建。

项目引用的论据

来自文档:

您可以大大缩短构建时间

期待已久的功能是 TypeScript 项目的智能增量构建。在 3.0 中,您可以将 --buildflag 与 tsc 一起使用。这实际上是tsc 的一个新入口点,它的行为更像是一个构建协调器,而不是一个简单的编译器。

运行tsc --build(简称tsc -b)将执行以下操作:

查找所有引用的项目 检测它们是否是最新的 以正确的顺序构建过时的项目

不用担心对您在命令行中传递的文件进行排序 - tsc 会在需要时重新排序,以便始终首先构建依赖项。

在组件之间实施逻辑分离

以新的更好的方式组织您的代码。

Project References 文档中有一些更有用的好处/功能。

示例设置

src/tsconfig.json

即使根目录下没有代码,这个 tsconfig 也可以在 所有常见的设置都去了(其他的将从它继承),并且 它将启用一个简单的tsc --build src 来构建整个 项目(并使用 --force 从头开始​​构建它)。


  "compilerOptions": 
    "rootDir": ".",
    "outDir": "../build",
    "composite": true
  ,
  // this root project has no source of its own
  "files": [],
  // but building this project will build all of the following:
  "references": [  
     "path": "./common" 
     "path": "./projectA" 
     "path": "./projectB" 
  ]

src/common/tsconfig.json

因为 common 没有引用,所以导入仅限于其目录中的目标和npm_modules。我相信,你甚至可以通过给它自己的package.json 来限制后者。

    
     "compilerOptions": 
        "rootDir": ".",
        "outDir": "../../build/common",
        "composite": true
      
    

src/projectA/tsconfig.json

projectA 可以导入 common 因为声明的引用。

    
      "compilerOptions": 
        "rootDir": ".",
        "outDir": "../../build/projectA",
        "composite": true
      ,
      "references": [
         "path": "../common" 
      ]
    

src/projectB/tsconfig.json

由于声明了引用,

projectB 可以导入 common AND projectA

    
      "compilerOptions": 
        "rootDir": ".",
        "outDir": "../../build/projectB",
        "composite": true
      ,
      "references": [
         "path": "../common" 
         "path": "../projectA" 
      ]
    

构建

这些只是一些例子。我使用下面的tsc 开关的缩写形式,例如-b 而不是 --build。从 repo 根目录执行的所有命令。

tsc -b src - 构建整个树。

tsc -p src/projectA/ 只编译 projectA。

tsc -b src/projectA/ 构建 projectA 和任何过期的依赖项。

tsc -b -w src - 构建并观察整个树。

tsc -b --clean src - 删除整个树的输出。

tsc -b -f src- 强制重建整个树。

使用-d-dry 开关可以预览tsc -b 的功能。

【讨论】:

谢谢!如果我更改导出参数的类型,我需要运行“tsc -p src/common”来生成 d.ts 文件,并且输入来自 common 的导入正确吗? @BenCarp 是的,完全正确。或者,如果您在处理 projectA 时更改 common,tsc -b src/projectA/ 将重新编译更改的 common 项以及 projectA 中受影响的项。在提交您的更改之前,您可以tsc -b src 以确保一切都已构建,或者将其留给您的持续集成系统。我在答案中添加了示例构建命令并修复了示例 tsconfigs。 Inigo,这个具体的项目其实是用Webpack搭建,编译过babel。所以我猜它使构建命令与这个项目无关,我看到有一个 watch 命令。它是否允许对 common 的类型更改自动编译,并对项目产生影响? tsc --build 命令与您给出的确切原因无关,并且正如我在上面已经解释的“任何过时的依赖项”。设置一个简单的项目来解决您的所有问题很容易。我不知道它如何与 Webpack 和 Babel 一起工作。

以上是关于在 Typescript 中防止不适当的导入和执行项目层次结构的主要内容,如果未能解决你的问题,请参考以下文章

如何防止我的 Vue 构建由于 Typescript 错误而失败

如何使用jsdom和typescript防止“属性'...'在'Global'类型上不存在”?

TypeScript(19): 模块

TypeScript 阻止文件成为模块?

如何防止 Vetur 和 TypeScript 在 VSCode 中同时显示打字稿警告?

TypeScript学习笔记 - 模块