为公共类使用共享节点模块

Posted

技术标签:

【中文标题】为公共类使用共享节点模块【英文标题】: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 制作共享模块的解决方案吗?

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

其他信息

共享配置:

// 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 只是将其视为不同的文件。

【问题讨论】:

【参考方案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 一起压缩,而不必担心在部署之前必须删除开发依赖项。

希望这会有所帮助!

一月

【讨论】:

有趣!我一定会检查一下,看看这是否合适。 这不适用于部署 firebase 功能。获取registry.npmjs.org/@appName%!f(MISSING)shared - 未找到【参考方案3】:

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

应用于您的用例:

    推送您的 shared-git-repository 的当前版本 在主项目中使用 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,但我想分享它,因为我目前正在使用如下所述的设置并且对我来说做得非常好:

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

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

注意

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

希望对你有帮助

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

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

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

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

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

【讨论】:

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

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

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

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

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

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

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

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

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

【讨论】:

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

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

应用于您的用例:

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

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

【讨论】:

据我所知,npm link 仅用于测试,如果您想部署生成的代码(例如我的函数)则不起作用。 我明白了,您可能应该将此要求添加到您的问题中。 问题中已经提到过,但我会澄清一下。

以上是关于为公共类使用共享节点模块的主要内容,如果未能解决你的问题,请参考以下文章

在 C++ 中的派生类之间共享公共代码

试图将类成员公开为只读或公共常量

Angular:导出类和公共类之间的区别?

iOS:我可以创建一个所有类(.h 和 .m)为公共的静态库吗?

对非共享成员的引用需要在调用公共子时发生对象引用

TPThinkPHP5公共模块的设置与使用