【问题标题】:Using a shared node module for common classes为公共类使用共享节点模块
【发布时间】:2020-02-26 03:38:32
【问题描述】:

目标

所以我有一个具有这种结构的项目:

  • 离子应用
  • firebase 功能
  • 共享

目标是在shared 模块中定义通用接口和类。

限制

我不想将我的代码上传到 npm 以在本地使用它,并且根本不打算上传代码。它应该 100% 离线工作。

虽然开发过程应该离线工作,但 ionic-appfirebase-functions 模块将被部署到 firebase(托管和功能)。因此,shared 模块中的代码应该在那里可用。

到目前为止我已经尝试过什么

  • 我曾尝试在打字稿中使用Project References,但我还没有接近工作
  • 我尝试将它安装为 npm 模块,就像在 this question 的第二个答案中一样
    • 一开始似乎工作正常,但是在构建过程中,运行firebase deploy时出现这样的错误:
Function failed on loading user code. Error message: Code in file lib/index.js can't be loaded.
Did you list all required modules in the package.json dependencies?
Detailed stack trace: Error: Cannot find module 'shared'
    at Function.Module._resolveFilename (module.js:548:15)
    at Function.Module._load (module.js:475:25)
    at Module.require (module.js:597:17)
    at require (internal/module.js:11:18)
    at Object.<anonymous> (/srv/lib/index.js:5:18)

问题

您有使用 typescripts config 或 NPM 制作共享模块的解决方案吗?

请不要将此标记为重复 → 我已经尝试了我在 StackOverflow 上找到的任何解决方案。

其他信息

共享配置:

// package.json
{
  "name": "shared",
  "version": "1.0.0",
  "description": "",
  "main": "dist/src/index.js",
  "types": "dist/src/index.d.ts",
  "files": [
    "dist/src/**/*"
  ],
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "publishConfig": {
    "access": "private"
  }
}

// tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "rootDir": ".",
    "sourceRoot": "src",
    "outDir": "dist",
    "sourceMap": true,
    "declaration": true,
    "target": "es2017"
  }
}

功能配置:

// package.json
{
  "name": "functions",
  "scripts": {
    "lint": "tslint --project tsconfig.json",
    "build": "tsc",
    "serve": "npm run build && firebase serve --only functions",
    "shell": "npm run build && firebase functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },
  "engines": {
    "node": "8"
  },
  "main": "lib/index.js",
  "dependencies": {
    "firebase-admin": "^8.0.0",
    "firebase-functions": "^3.1.0",
    "shared": "file:../../shared"
  },
  "devDependencies": {
    "@types/braintree": "^2.20.0",
    "tslint": "^5.12.0",
    "typescript": "^3.2.2"
  },
  "private": true
}


// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "./",
    "module": "commonjs",
    "noImplicitReturns": true,
    "noUnusedLocals": false,
    "rootDir": "src",
    "outDir": "lib",
    "sourceMap": true,
    "strict": true,
    "target": "es2017"
  }
}

当前解决方案

我在共享模块中添加了一个 npm 脚本,它将所有文件(不包括 index.js)复制到其他模块。这有一个问题,我将重复的代码签入到 SCM 中,并且我需要在每次更改时运行该命令。此外,IDE 只是将其视为不同的文件。

【问题讨论】:

    标签: node.js typescript firebase npm node-modules


    【解决方案1】:

    前言:我不太熟悉 Typescript 编译的工作原理以及应该如何在这样的模块中定义 package.json。这个解决方案虽然有效,但可以被认为是完成手头任务的一种 hacky 方式。

    假设如下目录结构:

    project/
      ionic-app/
        package.json
      functions/
        src/
          index.ts
        lib/
          index.js
        package.json
      shared/
        src/
          shared.ts
        lib/
          shared.js
        package.json
    

    部署 Firebase 服务时,您可以将命令附加到 predeploy and postdeploy hooks。这是在firebase.json 中通过所需服务上的属性predeploypostdeploy 完成的。这些属性包含一系列顺序命令,分别在部署代码之前和之后运行。此外,这些命令使用环境变量RESOURCE_DIR./functions./ionic-app的目录路径,以适用者为准)和PROJECT_DIR(包含firebase.json的目录路径)调用。

    firebase.json 中使用functionspredeploy 数组,我们可以将共享库的代码复制到部署到Cloud Functions 实例的文件夹中。通过这样做,您可以简单地包含共享代码,就像它是位于子文件夹中的库一样,或者您可以使用tsconfig.json 中的Typescript's path mapping 将其名称映射到命名模块(因此您可以使用import { hiThere } from 'shared';)。

    predeploy 挂钩定义(使用全局安装 shx 以实现 Windows 兼容性):

    // firebase.json
    {
      "functions": {
        "predeploy": [
          "shx rm -rf \"$RESOURCE_DIR/src/shared\"", // delete existing files
          "shx cp -R \"$PROJECT_DIR/shared/.\" \"$RESOURCE_DIR/src/shared\"", // copy latest version
          "npm --prefix \"$RESOURCE_DIR\" run lint", // lint & compile
          "npm --prefix \"$RESOURCE_DIR\" run build"
        ]
      },
      "hosting": {
        "public": "ionic-app",
        ...
      }
    }
    

    将复制的库的 typescript 源链接到函数 typescript 编译器配置:

    // functions/tsconfig.json
    {
      "compilerOptions": {
        ...,
        "baseUrl": "./src",
        "paths": {
          "shared": ["shared/src"]
        }
      },
      "include": [
        "src"
      ],
      ...
    }
    

    将模块名称“shared”与复制的库的包文件夹相关联。

    // functions/package.json
    {
      "name": "functions",
      "scripts": {
        ...
      },
      "engines": {
        "node": "8"
      },
      "main": "lib/index.js",
      "dependencies": {
        "firebase-admin": "^8.6.0",
        "firebase-functions": "^3.3.0",
        "shared": "file:./src/shared",
        ...
      },
      "devDependencies": {
        "tslint": "^5.12.0",
        "typescript": "^3.2.2",
        "firebase-functions-test": "^0.1.6"
      },
      "private": true
    }
    

    托管文件夹可以使用相同的方法。


    希望这能激发那些更熟悉 Typescript 编译的人想出一个使用这些钩子的更干净的解决方案。

    【讨论】:

      【解决方案2】:

      您可能想尝试Lerna,这是一个用于管理具有多个包的 JavaScript(和 TypeScript)项目的工具。

      设置

      假设你的项目有如下目录结构:

      packages
        ionic-app
          package.json
        firebase-functions
          package.json
        shared
          package.json
      

      确保在您不想发布的所有模块以及shared 模块中的typings 条目中指定正确的访问级别(privateconfig/access 键):

      共享:

      {
        "name": "shared",
        "version": "1.0.0",
        "private": true,
        "config": {
          "access": "private"
        },
        "main": "lib/index.js",
        "typings": "lib/index.d.ts",
        "scripts": {
          "compile": "tsc --project tsconfig.json"
        }
      }
      

      离子应用:

      {
        "name": "ionic-app",
        "version": "1.0.0",
        "private": true,
        "config": {
          "access": "private"
        },
        "main": "lib/index.js",
        "scripts": {
          "compile": "tsc --project tsconfig.json"
        },
        "dependencies": {
          "shared": "1.0.0"
        }
      }
      

      完成上述更改后,您可以创建一个根级别的package.json,您可以在其中指定您希望所有项目模块都可以访问的任何devDependencies,例如您的单元测试框架、tslint 等.

      packages
        ionic-app
          package.json
        firebase-functions
          package.json
        shared
          package.json
      package.json         // root-level, same as the `packages` dir
      

      您还可以使用这个根级别的package.json 来定义将调用项目模块中相应脚本的 npm 脚本(通过 lerna):

      {
        "name": "my-project",
        "version": "1.0.0",
        "private": true,
        "scripts": {
          "compile": "lerna run compile --stream",
          "postinstall": "lerna bootstrap",
        },
        "devDependencies": {
          "lerna": "^3.18.4",
          "tslint": "^5.20.1",
          "typescript": "^3.7.2"
        },
      }
      

      完成后,在根目录中添加 lerna 配置文件:

      packages
        ionic-app
          package.json
        firebase-functions
          package.json
        shared
          package.json
      package.json
      lerna.json
      

      内容如下:

      {
        "lerna": "3.18.4",
        "loglevel": "info",
        "packages": [
          "packages/*"
        ],
        "version": "1.0.0"
      }
      

      现在,当您在根目录中运行 npm install 时,在您的根级别 package.json 中定义的 postinstall 脚本将调用 lerna bootstrap

      lerna bootstrap 所做的是将您的 shared 模块符号链接到 ionic-app/node_modules/sharedfirebase-functions/node_modules/shared,因此从这两个模块的角度来看,shared 看起来就像任何其他 npm 模块。

      编译

      当然,对模块进行符号链接是不够的,因为您仍然需要将它们从 TypeScript 编译为 JavaScript。

      这就是根级 package.json compile 脚本发挥作用的地方。

      当您在项目根目录中运行 npm run compile 时,npm 将调用 lerna run compile --stream,而 lerna run compile --stream 在每个模块的 package.json 文件中调用名为 compile 的脚本。

      由于您的每个模块现在都有自己的compile 脚本,因此每个模块应该有一个tsonfig.json 文件。如果您不喜欢这种重复,您可以使用根级别的 tsconfig,或者根级别 tsconfig 和从根文件继承的模块级别 tsconfig 文件的组合。

      如果您想了解此设置如何在实际项目中发挥作用,请查看Serenity/JS,我已在其中广泛使用它。

      部署

      shared 模块符号链接在firebase-functionsionic-app 下的node_modules 下,以及在项目根目录下node_modules 下的devDepedencies 符号链接的好处是,如果您需要在任何地方部署使用者模块(例如ionic-app),您可以将其与其node_modules 一起压缩,而不必担心在部署之前必须删除开发依赖项。

      希望这会有所帮助!

      一月

      【讨论】:

      【解决方案3】:

      如果您使用 git 来管理您的代码,另一种可能的解决方案是使用 git submodule。使用git submodule,您可以将另一个 git 存储库包含到您的项目中。

      应用于您的用例:

      1. 推送您的 shared-git-repository 的当前版本
      2. 在主项目中使用 git submodule add &lt;shared-git-repository-link&gt; 链接共享存储库。

      这里是文档的链接:https://git-scm.com/docs/git-submodule

      【讨论】:

      • 其实也不错,但是本地开发和测试基本不用这种方式了。
      【解决方案4】:

      如果我正确理解您的问题,解决方案比单一答案更复杂,部分取决于您的偏好。

      方法 1:本地副本

      您可以使用Gulp 来自动化您已经描述的工作解决方案,但 IMO 维护起来并不容易,并且如果在某个时候另一个开发人员进来,它会大大增加复杂性。

      方法 2:Monorepo

      您可以创建一个包含所有三个文件夹的存储库并将它们连接起来,以便它们作为一个项目运行。正如上面已经回答的,您可以使用Lerna。它需要一些配置,但一旦完成,这些文件夹将作为一个项目运行。

      方法 3:组件

      将这些文件夹中的每一个都视为一个独立的组件。看看Bit。它将允许您将文件夹设置为更大项目的较小部分,并且您可以创建一个私人帐户,将这些组件的范围仅限于您。 初始设置后,您甚至可以将更新应用到单独的文件夹,使用它们的父文件夹将自动获取更新。

      方法 4:包

      您明确表示您不想使用 npm,但我想分享它,因为我目前正在使用如下所述的设置并且对我来说做得非常好:

      1. 使用npmyarn 为每个文件夹创建一个包(您可以为这两个文件夹创建范围包,这样代码将只对您可用,如果您担心的话)。
      2. 在父文件夹(使用所有这些文件夹)中,创建的包作为依赖项连接。
      3. 我使用 webpack 打包所有代码,使用 webpack 路径别名和 typescript 路径。

      像魅力一样工作,当包被符号链接以进行本地开发时,它完全脱机工作,根据我的经验 - 每个文件夹都可以单独扩展并且非常易于维护。

      注意

      在我的情况下,“子”包已经预编译,因为它们非常大,并且我为每个包创建了单独的 tsconfig,但美妙的是您可以轻松更改它。过去我在模块和编译文件中都使用过 typescript,还有原始 js 文件,所以整个东西非常非常通用。

      希望对你有帮助

      *****更新**** 继续第 4 点: 我道歉,我的错。也许我弄错了,因为据我所知,如果未上传模块,您将无法对其进行符号链接。不过,这里是:

      1. 你有一个单独的 npm 模块,让我们使用firebase-functions。您可以编译它,或者使用原始 ts,这取决于您的偏好。
      2. 在您的父项目中添加 firebase-functions 作为依赖项。
      3. tsconfig.json 中添加"paths": {"firebase-functions: ['node_modules/firebase-functions']"}
      4. 在 webpack 中 - resolve: {extensions: ['ts', 'js'], alias: 'firebase-functions': }

      这样,您只需使用import { Something } from 'firebase-functions' 即可引用来自firebase-functions 模块的所有导出函数。 Webpack 和 TypeScript 会将其链接到节点模块文件夹。使用此配置,父项目将不关心 firebase-functions 模块是用 TypeScript 还是 vanilla javascript 编写的。

      设置完成后,它将完美地用于生产。然后,链接和离线工作:

      1. 导航到firebase-functions 项目并写入npm link。它将创建一个符号链接,在您的计算机本地,并将链接映射到您在 package.json 中设置的名称。
      2. 导航到父项目并写入npm link firebase-functions,这将创建符号链接并将firebase-functions的依赖关系映射到您创建它的文件夹。

      【讨论】:

      • 我认为你误解了一些东西。我从来没有说过我不想使用 npm。事实上,这三个模块都是节点模块。我刚刚说过,我不想将我的模块上传到 npm。您能否再详细说明一下第四部分 - 这听起来很有趣?也许提供一个代码示例?
      • 我将添加另一个答案,因为它会很长且无法作为评论阅读
      【解决方案5】:

      我不想将我的代码上传到 npm 以在本地使用它,并且根本不打算上传代码。它应该 100% 离线工作。

      所有 npm 模块都安装在本地并始终离线工作,但如果您不想公开发布您的包以便人们看到它,您可以安装私有 npm 注册表。

      ProGet 是适用于 Windows 的 NuGet/Npm 私有存储库服务器,您可以在私有开​​发/生产环境中使用它来托管、访问和发布您的私有包。虽然它在 Windows 上,但我确信在 linux 上有各种可用的替代方案。

      1. Git 子模块是个坏主意,它确实是一种旧式共享代码的方式,它不像包那样进行版本控制,更改和提交子模块真的很痛苦。
      2. 源导入文件夹也是个坏主意,版本控制又是个问题,因为如果有人修改了依赖存储库中的依赖文件夹,再次跟踪它就是一场噩梦。
      3. 任何模拟包分离的第三方脚本工具都是浪费时间,因为 npm 已经提供了一系列工具来很好地管理包。

      这是我们的构建/部署方案。

      1. 每个私有包都有.npmrc,其中包含registry=https://private-npm-repository
      2. 我们将所有私有包发布到我们私有托管的 ProGet 存储库中。
      3. 每个私有包都包含依赖于 ProGet 的私有包。
      4. 我们的构建服务器通过我们设置的 npm 身份验证访问 ProGet。我们网络之外的任何人都无法访问此存储库。
      5. 我们的构建服务器使用 bundled dependencies 创建 npm 包,其中包含 node_modules 中的所有包,生产服务器无需访问 NPM 或私有 NPM 包,因为所有必要的包都已捆绑。

      使用私有 npm 存储库有多种优势,

      1. 无需自定义脚本
      2. 适合节点构建/发布管道
      3. 每个私有 npm 包都将包含指向您私有 git 源代码控制的直接链接,便于将来调试和调查错误
      4. 每个包都是只读快照,因此一旦发布就无法修改,并且在您制作新功能时,包含旧版本依赖包的现有代码库不会受到影响。
      5. 您可以轻松地将一些包公开并在将来移动到其他存储库
      6. 如果您的私有 npm 提供程序软件发生更改,例如您决定将代码移动到节点的私有 npm 包注册表云中,则无需对您的代码进行任何更改。

      【讨论】:

      • 这可能是一个解决方案,但不幸的是它不适合我。不过,感谢您的宝贵时间!
      • 还有一个本地 npm 存储库,作为小型节点服务器安装,verdaccio.org
      【解决方案6】:

      您正在寻找的工具是npm linknpm link 提供指向本地 npm 包的符号链接。这样您就可以链接一个包并在您的主项目中使用它,而无需将其发布到 npm 包库。

      应用于您的用例:

      1. 在您的shared 包中使用npm link。这将为将来的安装设置符号链接目标。
      2. 导航到您的主要项目。在您的 functions 包内并使用 npm link shared 链接共享包并将其添加到 node_modules 目录。

      这里是文档的链接:https://docs.npmjs.com/cli/link.html

      【讨论】:

      • 据我所知,npm link 仅用于测试,如果您想部署生成的代码(例如我的函数)则不起作用。
      • 我明白了,您可能应该将此要求添加到您的问题中。
      • 问题中已经提到过,但我会澄清一下。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-07-01
      • 2019-06-20
      • 1970-01-01
      • 2017-09-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多