【AICon】探索八个行业创新案例,教你在教育、金融、医疗、法律等领域实践大模型技术! >>> 了解详情
写点什么

不影响开发体验,如何将单体 Node.js 变成 Monorepo

作者:Adrien Joly

  • 2022-12-08
    北京
  • 本文字数:9046 字

    阅读完需:约 30 分钟

不影响开发体验,如何将单体Node.js变成Monorepo

将单体拆分成服务会带来维护多个存储库(每个服务一个存储库)的复杂性,每个存储库都有独立(但相互依赖)的构建流程和版本控制历史。Monorepo 已经成为一种降低复杂性的流行解决方案。


尽管 Monorepo 工具开发商有时会提供建议,但在现有代码库中配置 Monorepo 并不容易,尤其是单体代码库。更重要的是,迁移到 Monorepo 可能会给代码库开发团队带来巨大影响。例如,需要将大多数文件移动到子目录中,这会与团队当前正在进行的其他更改产生冲突。


本文将探讨如何平滑地将单体 Node.js 代码库变成 Monorepo,并将可能带来的影响和风险降到最低。


简介:单体代码库


假如存储库包含两个 Node.js API 服务器:api-server 和 back-for-front-server。它们是用 TypeScript 编写的,并转译为 JavaScript 在生产环境中运行。这两个服务器共用一套开发工具(用于检查、测试、构建和部署服务器)和 npm 依赖。它们还共用 Dockerfile 打成一个包,运行哪个 API 服务器要通过指定不同的入口点来选择。


迁移之前的文件结构:

├─ .github│  └─ workflows│     └─ ci.yml├─ .yarn│  └─ ...├─ node_modules│  └─ ...├─ scripts│  ├─ e2e-tests│  │  └─ e2e-test-setup.sh│  └─ ...├─ src│  ├─ api-server│  │  └─ ...│  ├─ back-for-front-server│  │  └─ ...│  └─ common-utils│     └─ ...├─ .dockerignore├─ .eslintrc.js├─ .prettierrc.js├─ .yarnrc.yml├─ docker-compose.yml├─ Dockerfile├─ package.json├─ README.md├─ tsconfig.json└─ yarn.lock
复制代码


迁移之前的 Dockerfile(经过简化):

FROM node:16.16-alpineWORKDIR /backendCOPY . .COPY .yarnrc.yml .COPY .yarn/releases/ .yarn/releases/RUN yarn installRUN yarn buildRUN chown node /backendUSER nodeCMD exec node dist/api-server/start.js
复制代码


在共享存储库中维护多个服务器有以下好处。


  • 开发工具(TypeScript、ESLint、Prettier……)的配置和部署过程是共享的,这减少了维护工作,而且可以保证所有贡献团队的做法一致。

  • 方便开发人员跨服务器重用模块,例如日志模块、数据库客户端、外部 API 封装器等。

  • 版本控制简单,因为所有服务器共用版本,任何服务器的任何更新都会产生新版本的 Docker 镜像,其中包含所有服务器。

  • 也很容易编写覆盖多个服务器的端到端测试,并将它们包含在存储库中,因为所有东西都在一个地方。遗憾的是,这些服务器的源代码是单体的。我的意思是,各服务器的代码是分不开的。为其中一个服务器编写的代码(例如 SQL 适配器)最终也会被其他服务器导入。因此,要防止服务器 A 的代码更改也影响到服务器 B,这非常复杂,可能会导致意想不到的回归。而且,随着时间的推移,代码的耦合度会变得越来越高,代码会越来越脆弱,越来越难维护。


“Monorepo 结构”是一个有趣的折衷方案:在共享存储库的同时将代码库分割成包。这种划分使得接口更加清晰,因此,可以有意识的选择包之间的依赖关系。它还实现了一些工作流优化,例如,只在更改过的包上构建和运行测试。


如果代码库很大,集成了很多工具(例如代码分析、转译、打包、自动化测试、持续集成、基于 Docker 的部署……),那么将单体代码库迁移到 Monorepo 很快就会变得困难和反复。此外,由于存储库做了结构更改,所以在迁移期间,操作任何 Git 分支都会导致冲突。让我们看下将代码库转换为 Monorepo 的必要步骤,最大限度减少迁移问题。


所需的更改


将代码库迁移到 Monorepo 需要遵循以下步骤。


  1. 文件结构:一开始,创建包含所有源代码的惟一包,这样,所有文件都将被移动。

  2. Node.js 模块解析的配置:使用 Yarn 工作空间来实现包之间的相互导入。

  3. Node.js 项目和依赖的配置:package.json (包括 npm/yarn 脚本)将被拆分:主脚本在根目录,然后每个包里有一个。

  4. 开发工具的配置:tsconfig.json、.eslintrc.js、 .prettierrc.jsjest.config.js 也将拆分成两部分:一个“基础”部分,然后每个包里有一个对它的扩展。

  5. 持续集成工作流的配置:.github/workflows/ci.yml 需要做多处调整,例如,确保其中的步骤会针对每个包运行,多个包的指标(如测试覆盖率)会合并成一个。

  6. 构建和部署流程的配置:优化 Dockerfile,使其只包含要构建的服务器所需的文件和依赖。

  7. 跨包脚本的配置:使用 Turborepo 编排影响多个包的 npm 脚本的执行(如构建、测试、分析)。迁移之后的文件结构:

├─ .github│  └─ workflows│     └─ ci.yml├─ .yarn│  └─ ...├─ node_modules│  └─ ...├─ packages│  └─ common-utils│     └─ src│        └─ ...├─ servers│  └─ monolith│     ├─ src│     │  ├─ api-server│     │  │  └─ ...│     │  └─ back-for-front-server│     │     └─ ...│     ├─ scripts│     │  ├─ e2e-tests│     │  │  └─ e2e-test-setup.sh│     │  └─ ...│     ├─ .eslintrc.js│     ├─ .prettierrc.js│     ├─ package.json│     └─ tsconfig.json├─ .dockerignore├─ .yarnrc.yml├─ docker-compose.yml├─ Dockerfile├─ package.json├─ README.md├─ turbo.json└─ yarn.lock
复制代码


由于 Node.js 及其工具生态系统非常灵活,所以共享一个通用的方法会很复杂,因此请记住,为了让开发人员的体验至少与迁移前一样好,将需要进行大量的优化迭代。


如何将影响降至最低


所幸,虽然迭代优化可能需要几周的时间,但影响最大的是第一步:更改文件结构。


如果你的团队借助 Git 分支并行开发,那么这一步骤将导致这些分支发生冲突,在合并到存储库的主分支时解决冲突就会非常麻烦。


因此,我们有三方面的建议,特别是当需要就迁移到 Monorepo 说服整个团队时。


  • 提前计划(短时间的)代码冻结:为了避免迁移时发生冲突,定义一个日期和时间,到时所有分支都必须合并。提前计划,以便开发人员可以做出适当的调整。但在可行的迁移计划确认前,不要选定日期。

  • 将迁移计划中最关键的部分编写 bash 脚本,这样就可以确保开发工具在迁移前后都能工作,包括在持续集成管道上。这样应该可以打消怀疑者的疑虑,在代码冻结的实际日期和时间上获得更大的灵活性。

  • 在团队的帮助下,列出他们日常工作所需的所有工具、命令和工作流(包括 IDE 的特性,如代码导航、代码分析和自动补全)。这个需求列表(或验收标准)将帮助我们检查将开发体验迁移到 Monorepo 设置的步骤。这有助于确保在迁移时不会忘掉重要事项。以下是我们决定满足的需求列表:

  • yarn install 仍然安装依赖;

  • 所有自动化测试仍能运行并通过;

  • yarn lint 仍然能够发现代码风格违规的情况(如果有的话);

  • eslint 错误(如果有的话)仍然会在 IDE 中报告;

  • prettier 仍然会在 IDE 保存文件对其进行格式化;

  • IDE 仍然会发现错误的导入和 / 或违反tsconfig.json 文件中定义的 TypeScript 规则的情况(如果有的话);

  • 在使用外部包暴露的符号时,如果它被声明为依赖,那么 IDE 仍然能够提出导入正确模块的建议;

  • 生成的 Docker 镜像在部署后仍然能够启动且和预期一样正常运行;

  • 生成的 Docker 镜像大小仍然(大致)一样;

  • 整个 CI 工作流都可以通过,而且不会消耗更多的时间;

  • 集成的第三方代码分析器(SonarCloud)仍然能够和预期一样工作。下面是迁移脚本示例:

# 这个脚本使用 Yarn 工作空间和 Turborepo 将存储库转换为 Monorepo
set -e -o pipefail # stop in case of error, including for piped commands
NEW_MONOLITH_DIR="servers/monolith" # 第一个工作空间的路径:"monolith"
# 清理临时目录,即没有存储在 Git 中的那些rm -rf ${NEW_MONOLITH_DIR} dist
# 创建目标目录mkdir -p ${NEW_MONOLITH_DIR}
# 将文件和目录从 root 移动到 ${NEW_MONOLITH_DIR}目录# ……除了那些绑定到 Yarn 和 Docker 的(目前)mv -f \ .eslintrc.js \ .prettierrc.js\ README.md \ package.json \ src \ scripts \ tsconfig.json \ ${NEW_MONOLITH_DIR}
# 将新文件复制到 root 目录cp -a migration-files/. . # 包括 turbo.json, package.json, Dockerfile, # 和 servers/monolith/tsconfig.json
# 更新路径sed -i.bak 's,docker\-compose\.yml,\.\./\.\./docker\-compose\.yml,g' \ ${NEW_MONOLITH_DIR}/scripts/e2e-tests/e2e-test-setup.shfind . -name "*.bak" -type f -delete # delete .bak files created by sed
unset CI # to let yarn modify the yarn.lock file, when script is run on CIyarn add --dev turbo # 安装 Turboreporm -rf migration-files/echo "✅ You can now delete this script"
复制代码


我们在持续集成工作流中添加了一个作业(GitHub Actions),用于检查测试和其他常规 Yarn 脚本在迁移之后是否仍然可以正常工作:


jobs:  monorepo-migration:    timeout-minutes: 15    name: Test Monorepo migration    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v2      - run: ./migrate-to-monorepo.sh        env:          YARN_ENABLE_IMMUTABLE_INSTALLS: "false" # 允许 yarn.lock 变化      - run: yarn lint      - run: yarn test:unit      - run: docker build --tag "backend"      - run: yarn test:e2e
复制代码


从单体的源代码转换生成第一个包


看看迁移之前我们唯一的package.json 文件是什么样子:


{  "name": "backend",  "version": "0.0.0",  "private": true,  "scripts": {    /* 所有 npm/yarn 脚本... */  },  "dependencies": {    /* 所有运行时依赖 ... */  },  "devDependencies": {    /* 所有开发依赖 ... */  }}
复制代码


以下片段摘自迁移之前 TypeScript 配置文件tsconfig.json


{    "compilerOptions": {        "target": "es2020",        "module": "commonjs",        "lib": ["es2020"],        "moduleResolution": "node",        "esModuleInterop": true,        /* ... 多条让 TypeScript 更严谨的规则 */    },    "include": ["src/**/*.ts"],    "exclude": ["node_modules", "dist", "migration-files"]
复制代码


在将单体拆分成包时,我们必须:


  • 告诉包管理器(这里是 Yarn)代码库包含多个包;

  • 更明确地指出可以在哪里找到这些包。为了使包可以作为其他包的依赖项导入(也就是workspaces),我们建议使用 Yarn 3 或其他支持工作空间的包管理器。


所以我们在package.json中添加了"packageManager": "yarn@3.2.0" ,并在其旁边创建了一个.yarnrc.yml 文件:


nodeLinker: node-modulesyarnPath: .yarn/releases/yarn-3.2.0.cjs
复制代码


根据 Yarn 迁移路径 的建议:


  • 提交.yarn/releases/yarn-3.2.0.cjs 文件;

  • 我们还是坚持使用node_modules目录,至少目前如此。在将单体代码库(包括package.jsontsconfig.json)移动到 servers/monolith/之后,在项目的根目录下新建一个package.json 文件,其中 workspaces 属性列出了工作空间的位置:


{  "name": "@myorg/backend",  "version": "0.0.0",  "private": true,  "packageManager": "yarn@3.2.0",  "workspaces": [    "servers/*"  ]}
复制代码


从现在开始,每个工作空间必须有自己的package.json 文件,用于指定其包名和依赖。截至目前,我们只有一个工作空间“monolith”。在servers/monolith/package.json文件中使用组织名作为其名称的前缀,明确标明它现在是一个 Yarn 工作空间:


{  "name": "@myorg/monolith",  /* ... */}
复制代码


在运行完yarn install 之后,我们又修复了一些路径:


  • yarn build 及其他 npm 脚本(从 servers/monolith/运行时)应用仍然有效;

  • Dockerfile 应该仍然可以生成一个有效的构建;

  • 所有的 CI 检查应该仍然可以通过。

提取第一个包:common-utils


到目前为止,我们的 Monorepo 只定义了一个“monolith”工作空间。它在servers目录下,这表明它无意让其他工作空间导入其模块。


让我们定义一个可以被这些服务器导入的包。为了更好地传达这种差异,我们在servers目录旁增加了一个packages目录。要提取一个包的话,目录common-utils(来自servers/monolith/common-utils)是首选,因为“monolith”工作空间的多个服务器都使用了它的模块。当每个服务器都在自己的工作空间中定义时,common-utils包将被声明为两个服务器的依赖项。


现在,我们将common-utils 目录从servers/monolith/ 移动到新建的目录packages/


为了将其转换成一个包,创建packages/common-utils/package.json 文件,其中包含所需的依赖和构建脚本:


{  "name": "@myorg/common-utils",  "version": "0.0.0",  "private": true,  "scripts": {    "build": "swc src --out-dir dist --config module.type=commonjs --config env.targets.node=16",    /* 其他脚本 ... */  },  "dependencies": {    /* common-utils 的依赖 ... */  },}
复制代码


注意:我们使用swc 将 TypeScript 转译为 JavaScript,但使用tsc 应该也可以获得类似的效果。此外,我们尽力让它的配置(使用命令行参数)与servers/monolith/package.json 中的配置一致。确保包会按预期构建:


$ cd packages/common-utils/$ yarn$ yarn build$ ls dist/ # 应该包含 src/ 中所有文件的.js 构建
复制代码


接下来,更新根package.json 文件,将packages/ 的所有子目录(包括common-utils)也声明为工作空间:


{  "name": "@myorg/backend",  "version": "0.0.0",  "private": true,  "packageManager": "yarn@3.2.0",  "workspaces": [    "packages/*",    "servers/*"  ],  /* ... */}
复制代码


common-utils 添加为服务器包monolith 的依赖:$ yarn workspace @myorg/monolith add @myorg/common-utils


你可能已经注意到,Yarn 创建了一个到packages/common-utils/ (源代码就在这里)的符号链接node_modules/@myorg/common-utils


完成此操作后,我们必须修复所有有问题的common-utils 导入。实现这一目标的一种低成本方法是在servers/monolith/中重新引入common-utils目录,并使用一个从新生成的包@myorg/common-utils导出函数的文件:


export { hasOwnProperty } from "@myorg/common-utils/src/index"


更新服务器的Dockerfile ,以便构建包并包含在镜像中:


# 使用以下命令从项目根目录构建:# $ docker build -t backend -f servers/monolith/Dockerfile .
FROM node:16.16-alpine
WORKDIR /backendCOPY . .COPY .yarnrc.yml .COPY .yarn/releases/ .yarn/releases/RUN yarn install
WORKDIR /backend/packages/common-utilsRUN yarn build
WORKDIR /backend/servers/monolithRUN yarn build
WORKDIR /backendRUN chown node /backendUSER nodeCMD exec node servers/monolith/dist/api-server/start.js
复制代码


这个Dockerfile 必须从根目录构建,那样它才能访问yarn 环境和那里的文件。注意:可以通过在Dockerfile 中将yarn install 替换为yarn workspaces focus --production来从 Docker 镜像中除去开发依赖,这要感谢 plugin-workspace-tools 插件,参考“使用 Yarn 3 和 Turborepo 编排和 Docker 化 Monorepo”一文中的介绍。


至此,我们已经成功地从单体中提取出了一个可导入的包,但是:


  • 生产构建因为Cannot find module 错误运行失败;

  • common-utils 的导入路径过于冗长。

修复开发和生产环境的模块解析


我们从@myorg/types-helpers导入函数的方法是有问题的,因为 Node.js 从子目录src/中查找模块,即使它们被转译到子目录dist/中。


我们宁愿采用一种子目录无关的方式导入函数:


import { hasOwnProperty } from "@myorg/common-utils"


即使我们在包的package.json 文件里指定"main": "src/index.ts" ,在运行转译构建时路径仍然会被破坏。


作为补救使用 Node 的 条件导入,以使包的入口点可以适配运行时上下文:


 {    "name": "@myorg/common-utils",    "main": "src/index.ts",+   "exports": {+     ".": {+       "transpiled": "./dist/index.js",+       "default": "./src/index.ts"+     }+   },    /* ... */  }
复制代码


简而言之,增加一个exports配置项,关联包根目录的两个入口点:


  • default条件指定 ./src/index.ts 为包的入口点;

  • transpiled条件指定./dist/index.js 为包的入口点。根据 Node 的文档,default 条件应该始终放在最后。transpiled条件是自定义的,所以你可以随意指定其名称。


为了让这个包在转译后的运行时上下文中运行,需要修改相应的 node 命令,指定自定义条件。例如,在Dockerfile中:


- CMD exec node servers/monolith/dist/api-server/start.js+ CMD exec node --conditions=transpiled servers/monolith/dist/api-server/start.js
复制代码


确保开发工作流和以前一样


现在,我们有了一个 Monorepo。它包含两个工作空间,每一个都可以从另一个导入模块、构建并运行。


但是,每增加一个工作空间,就需要更新Dockerfile ,因为必须针对每个工作空间手动运行yarn build 命令。


此时,像 Turborepo 这样的 Monorepo 编排器就派上用场了:我们可以让它根据声明好的依赖关系递归地构建包。


在将 Turborepo 作为 Monorepo 的开发依赖项添加以后(命令:$ yarn add turbo --dev ),可以在turbo.json中定义一个构建管道:


{    "pipeline": {        "build": {            "dependsOn": ["^build"]        }    }}
复制代码


这个管道定义的意思是,对于任何包,$ yarn turbo build 会从它依赖的包开始构建,以此类推。这样就可以简化Dockerfile


# 使用以下命令从项目根目录构建:# $ docker build -t backend -f servers/monolith/Dockerfile .
FROM node:16.16-alpineWORKDIR /backendCOPY . .COPY .yarnrc.yml .COPY .yarn/releases/ .yarn/releases/RUN yarn installRUN yarn turbo build # builds packages recursivelyRUN chown node /backendUSER nodeCMD exec node --conditions=transpiled servers/monolith/dist/api-server/start.js
复制代码


注意:可以利用 Docker 多阶段构建和turbo prune 来优化构建时间和镜像大小,但在本文写作时,生成的yarn.lock 文件与 Yarn 3 还不兼容。(关于这个问题,可以查看 这个 pull 请求 了解最新进展。)借助 Turborepo,在定义好管道后(和构建时类似),只需一条命令(yarn turbo test:unit )就可以运行所有包的单元测试。


也就是说,大多数开发工作流的依赖项和所依赖的配置文件都移到了servers/monolith/目录下,因此,它们大部分都无法正常工作了。


我们可以把这些依赖项和文件留在根目录一级,那样所有包都可以共用。或者在每个包中复制一份。当然,还有更好的方法。


将通用配置提取到包中并扩展它


现在,最关键的构建和开发工作流已经可以正常工作了,接下来,要让测试执行器、代码分析器和格式化器在针对不同的包执行时行为一致,同时还要留出定制空间。


一种方法是创建保存基础配置的包,然后让其他包扩展它。


就像我们对common-tools所做的那样,创建以下包:


├─ packages│  ├─ config-eslint│  │  ├─ .eslintrc.js│  │  └─ package.json│  ├─ config-jest│  │  ├─ jest.config.js│  │  └─ package.json│  ├─ config-prettier│  │  ├─ .prettierrc.js│  │  └─ package.json│  └─ config-typescript│     ├─ package.json│     └─ tsconfig.json├─ ...
复制代码


然后,把它们作为依赖项添加到每个包含源代码的包中,并创建配置文件扩展它们:


packages/*/.eslintrc.js:
module.exports = { extends: ["@myorg/config-eslint/.eslintrc"], /* ... */}
packages/*/jest.config.js:
module.exports = { ...require("@myorg/config-jest/jest.config"), /* ... */}
packages/*/.prettierrc.js:
module.exports = { ...require("@myorg/config-prettier/.prettierrc.js"), /* ... */}
packages/*/tsconfig.json:
{ "extends": "@myorg/config-typescript/tsconfig.json", "compilerOptions": { "baseUrl": ".", "outDir": "dist", "rootDir": "." }, "include": ["src/**/*.ts"], /* ... */}
复制代码


可以使用像 plop 这样的样板文件生成器来简化使用这些配置文件设置新包的过程,加快设置速度。


下一步:每个服务器一个包


我们已经逐项核对了“如何将影响降至最低”一节所列出的所有需求,现在可以冻结代码贡献、运行迁移脚本、并将更改提交到源代码存储库了。


从现在起,该存储库可以正式称为“Monorepo”了!所有开发人员都应该能够创建自己的包,并在单体中导入它们,而不是直接向其中新增代码。基础已经打好,可以开始将单体拆分成多个包了,就像我们对common-tools 所做的那样。


我们不打算讨论实现这一目标的详细步骤,但这里有一些关于如何做好拆分准备的建议:


  • 从提取小的实用程序包开始,例如类型库、日志记录、错误报告、API 封装器等;

  • 然后,提取计划跨所有服务器共享的代码的其他部分;

  • 最后,复制不计划共享但不只一个服务器依赖的部分。这些建议的目标是逐步解耦各服务器。以此为基础将每个服务器提取成一个包应该和提取common-utils 一样简单。


此外,在这个过程中,你应该可以利用以下几项特性优化构建、开发和部署工作流的持续时间:


  • Docker 多阶段构建(参见 Dockerfile 文件编制最佳实践) ;

  • 重用主机的 Yarn 缓存(参见 Docker Build Mounts);

  • Turborepo 的 远程缓存。

小结


我们已经把一个单体 Node.js 后端变成了 Monorepo,同时将对团队的影响和风险降到最低:


  • 将单体拆分为多个相互依赖的、解耦的包;

  • 跨包共享通用 TypeScript、ESLint、Prettier 和 Jest 配置;

  • 安装 Turborepo 优化开发和构建工作流。使用迁移脚本让我们可以在准备和测试迁移时避免代码冻结和 Git 冲突,确保构建和开发工具不会因为迁移脚本添加 CI 作业而遭到破坏。


感谢 Renaud Chaput (Notos 联合创始人、CTO)、Vivien Nolot(Choose 软件工程师)和 Alexis Le Texier (Choose 软件工程师)在这次迁移中的通力合作。

原文链接:https://www.infoq.com/articles/nodejs-monorepo/
相关阅读:

Node.js 基于区块链的游戏应用的首选

【异常】window 10 安装 node.js 时遇到 2502 2503 错误解决方法

JXcore 打包在企业级项目里的合理运用和模块系统以及网络的配置详解【node.js】

2022-12-08 16:567150

评论

发布
暂无评论
发现更多内容

基于香港服务器的应用开发中测试数据管理的 3 个最佳实践

九河云安全

这本“算法宝典”讲得透彻,完全掌握后,我竟拿到字节跳动offer

公众号_愿天堂没有BUG

Java 编程 程序员 架构 面试

这几个棘手的面试常见问题,如何高情商的回答?

架构精进之路

面试 情商 8月日更

服务器的升级,不可避免的安全问题

九河云安全

Serverless 全能选手,再添一“金”

Serverless Devs

Serverless 互联网 云原生

Mysql读写锁保姆级图文教程

华为云开发者联盟

MySQL 数据 读写锁 读锁 MyLSAM

2021第二届云原生编程挑战赛正式启动,抢先报名!

阿里巴巴云原生

阿里云 Serverless RocketMQ 云原生 dubbo

iOS开发-为 iOS 编写 Kotlin Parcelize 编译器插件

iOSer

ios 编译器 编译器原理 iOS 知识体系 Kotlin Parcelize

巨头纷纷布局分布式云,一场新的云战争即将打响

浪潮云

云计算

CC挖矿系统源码开发

获客I3O6O643Z97

挖矿 挖矿矿池系统开发案例 fil矿机

你的工作有弹性么?

escray

学习 极客时间 朱赟的技术管理课 8月日更

阿里云-云开发平台入门篇——静态网站的全生命周期实战

若尘

阿里云 8月日更

专业好用的数据恢复软件推荐

淋雨

EasyRecovery 文件恢复 硬盘数据恢复

【Vue2.x 源码学习】第二十七篇 - Vue 生命周期的实现

Brave

源码 vue2 8月日更

带你读AI论文:SDMG-R结构化提取—无限版式小票场景应用

华为云开发者联盟

语义 多模态 推理模型 SDMG-R 检测文本

2021全球开源技术峰会|IoT 时代的开源数据基础设施

EMQ映云科技

开源 IOT Platform IoT emq 开源技术

限12小时删!白嫖对标阿里P5—P8的Java学习路线+大厂刷题秘籍

Java架构追梦

Java 阿里巴巴 架构 面试

防止数据丢失和减轻勒索软件攻击的 5 种方法

九河云安全

快来看,大数据两地三中心的容灾也可以如此省心!

华为云开发者联盟

大数据 数据湖 容灾 华为云MRS 两地三中心

赛迪发布《2020-2021年中国IT服务市场研究年度报告》,联想位居第一梯队

科技大数据

科技互联网

只需6步,教你从零开发一个签到小程序

华为云开发者联盟

小程序 App 移动 智慧校园 FunctionGraph

TrafficStatsRunnable 实用封装

Changing Lin

8月日更

推动数据中心行业的“水电煤”,可视化如何用数据改变传统产业?

一只数据鲸鱼

机房 数据可视化 数字孪生 智能IDC

Python代码阅读(第4篇):过滤掉列表中的唯一值

Felix

Python 编程 Code Programing 阅读代码

如何保存数据并更快地从勒索软件攻击中恢复

九河云安全

「免费开源」基于Vue和Quasar的前端SPA项目crudapi后台管理系统实战之数据库逆向(十二)

crudapi

Vue crud crudapi quasar 数据库逆向

Pangaea AI 智能机器人炒币系统开发

获客I3O6O643Z97

量化策略 量化跟单 量化机器人

如何实时打通数据孤岛?Tapdata 创始人唐建法受邀于GOTC深度分享

tapdata

数据库 打通数据孤岛 数据同步 Real Time DaaS GOTC

九大核心专题,630页内容,熬夜23天吃透,我收割了3个大厂offer

公众号_愿天堂没有BUG

Java 编程 程序员 架构 面试

Swift 实现获取、展示 Mac 的 WiFi 密码

fuyoufang

ios swift SwiftUI Mac 软件 8月日更

论 Erda 的安全之道

尔达Erda

云原生 安全 企业数字化转型 云平台 开发平台

不影响开发体验,如何将单体Node.js变成Monorepo_大前端_InfoQ精选文章