为公共类使用共享节点模块
Posted
技术标签:
【中文标题】为公共类使用共享节点模块【英文标题】:Using a shared node module for common classes 【发布时间】:2020-02-26 03:38:32 【问题描述】:目标
所以我有一个具有这种结构的项目:
离子应用 firebase 功能 共享目标是在shared
模块中定义通用接口和类。
限制
我不想将我的代码上传到 npm 以在本地使用它,并且根本不打算上传代码。它应该 100% 离线工作。
虽然开发过程应该离线工作,但 ionic-app
和 firebase-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
中通过所需服务上的属性predeploy
和postdeploy
完成的。这些属性包含一系列顺序命令,分别在部署代码之前和之后运行。此外,这些命令使用环境变量RESOURCE_DIR
(./functions
或./ionic-app
的目录路径,以适用者为准)和PROJECT_DIR
(包含firebase.json
的目录路径)调用。
在firebase.json
中使用functions
的predeploy
数组,我们可以将共享库的代码复制到部署到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
条目中指定正确的访问级别(private
和config/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/shared
和 firebase-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-functions
和ionic-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 <shared-git-repository-link>
链接共享存储库。
这里是文档的链接:https://git-scm.com/docs/git-submodule
【讨论】:
其实也不错,但是本地开发和测试基本不用这种方式了。【参考方案4】:如果我正确理解您的问题,解决方案比单一答案更复杂,部分取决于您的偏好。
方法 1:本地副本
您可以使用Gulp 来自动化您已经描述的工作解决方案,但 IMO 维护起来并不容易,并且如果在某个时候另一个开发人员进来,它会大大增加复杂性。
方法 2:Monorepo
您可以创建一个包含所有三个文件夹的存储库并将它们连接起来,以便它们作为一个项目运行。正如上面已经回答的,您可以使用Lerna。它需要一些配置,但一旦完成,这些文件夹将作为一个项目运行。
方法 3:组件
将这些文件夹中的每一个都视为一个独立的组件。看看Bit。它将允许您将文件夹设置为更大项目的较小部分,并且您可以创建一个私人帐户,将这些组件的范围仅限于您。 初始设置后,您甚至可以将更新应用到单独的文件夹,使用它们的父文件夹将自动获取更新。
方法 4:包
您明确表示您不想使用 npm,但我想分享它,因为我目前正在使用如下所述的设置并且对我来说做得非常好:
-
使用
npm
或yarn
为每个文件夹创建一个包(您可以为这两个文件夹创建范围包,这样代码将只对您可用,如果您担心的话)。
在父文件夹(使用所有这些文件夹)中,创建的包作为依赖项连接。
我使用 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 link
。 npm link
提供指向本地 npm 包的符号链接。这样您就可以链接一个包并在您的主项目中使用它,而无需将其发布到 npm 包库。
应用于您的用例:
-
在您的
shared
包中使用npm link
。这将为将来的安装设置符号链接目标。
导航到您的主要项目。在您的 functions
包内并使用 npm link shared
链接共享包并将其添加到 node_modules
目录。
这里是文档的链接:https://docs.npmjs.com/cli/link.html
【讨论】:
据我所知,npm link 仅用于测试,如果您想部署生成的代码(例如我的函数)则不起作用。 我明白了,您可能应该将此要求添加到您的问题中。 问题中已经提到过,但我会澄清一下。以上是关于为公共类使用共享节点模块的主要内容,如果未能解决你的问题,请参考以下文章