网易首页 > 网易号 > 正文 申请入驻

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

0
分享至

作者 | Adrien Joly

译者 | 平川

策划 | 丁晓昀

将单体拆分成服务会带来维护多个存储库(每个服务一个存储库)的复杂性,每个存储库都有独立(但相互依赖)的构建流程和版本控制历史。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.js 和 jest.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: 15name: Test Monorepo migrationruns-on: ubuntu-lateststeps:- uses: actions/checkout@v2- run: ./migrate-to-monorepo.shenv: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.json 和 tsconfig.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 基于区块链的游戏应用的首选(https://xie.infoq.cn/article/3554ecc815a0c7d5e8f429c62)

【异常】window 10 安装 node.js 时遇到 2502 2503 错误解决方法(https://xie.infoq.cn/article/8eb3e25e164a3077482b2c53f)

JXcore 打包在企业级项目里的合理运用和模块系统以及网络的配置详解【node.js】(https://xie.infoq.cn/article/27f21a5f5903c489e172b9430)

声明:本文为 InfoQ 翻译,未经许可禁止转载。

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
原董事长退休三年突遭调查,恒邦财险战略陷两难

原董事长退休三年突遭调查,恒邦财险战略陷两难

华夏时报
2026-01-24 21:49:03
Lisa去车公庙上香,穿lululemon瑜伽裤臀很翘,她素颜长相很普通

Lisa去车公庙上香,穿lululemon瑜伽裤臀很翘,她素颜长相很普通

有范又有料
2026-01-25 19:23:45
母亲用3个虎鞭泡酒,九九八十一天后拿给儿子喝,当天就后悔了

母亲用3个虎鞭泡酒,九九八十一天后拿给儿子喝,当天就后悔了

古怪奇谈录
2025-05-15 13:49:36
基恩:库尼亚的进球太精彩了,我反复观看一整晚都不腻

基恩:库尼亚的进球太精彩了,我反复观看一整晚都不腻

懂球帝
2026-01-26 03:52:10
大家坐稳扶好了,下周周一周二周三三天,牛市或将再次主升浪!

大家坐稳扶好了,下周周一周二周三三天,牛市或将再次主升浪!

夜深爱杂谈
2026-01-25 18:36:13
整天开会有啥必要啊?

整天开会有啥必要啊?

北京老付
2026-01-20 10:59:33
40岁左右得女性这样打扮,既优雅又有成熟女人的魅力

40岁左右得女性这样打扮,既优雅又有成熟女人的魅力

牛弹琴123456
2025-12-28 16:35:58
若连碗面都要靠官媒压阵才能卖出,那不吃也罢——横竖都是预制的

若连碗面都要靠官媒压阵才能卖出,那不吃也罢——横竖都是预制的

阿天爱旅行
2026-01-22 13:14:02
美国运动员霍诺德成功徒手攀爬508米高台北101,耗时约1小时31分钟

美国运动员霍诺德成功徒手攀爬508米高台北101,耗时约1小时31分钟

潇湘晨报
2026-01-25 12:00:17
你是我此生最美的遇见,也是一生最深的眷恋

你是我此生最美的遇见,也是一生最深的眷恋

青苹果sht
2026-01-26 05:04:27
岛国暗黑界凡尔赛之最 —— 通野未帆

岛国暗黑界凡尔赛之最 —— 通野未帆

碧波万览
2026-01-26 01:00:03
天助曼城:2-3,英超领头羊遭曼联逆转,3轮不胜,仅领先曼城4分

天助曼城:2-3,英超领头羊遭曼联逆转,3轮不胜,仅领先曼城4分

侧身凌空斩
2026-01-26 03:36:53
我和一名海员相亲,1年只回1次家,他提出3个条件,我当场答应了

我和一名海员相亲,1年只回1次家,他提出3个条件,我当场答应了

星宇共鸣
2026-01-20 09:23:56
奥尔波抵达北京!

奥尔波抵达北京!

占豪
2026-01-25 22:19:14
梅洛尼回应特朗普:震惊,不可接受

梅洛尼回应特朗普:震惊,不可接受

环球时报国际
2026-01-25 10:13:23
80岁大爷哭诉:我这辈子做过最后悔的事情,就是存钱为自己养老

80岁大爷哭诉:我这辈子做过最后悔的事情,就是存钱为自己养老

惟来
2026-01-23 13:27:01
浙江知名主持嫁富商赴美定居,5年生3娃住豪宅

浙江知名主持嫁富商赴美定居,5年生3娃住豪宅

让心灵得以栖息
2026-01-25 19:51:57
善恶终有报!靠星光大道成名的“盲人”杨光,终要为自己荒唐买单

善恶终有报!靠星光大道成名的“盲人”杨光,终要为自己荒唐买单

小熊侃史
2026-01-23 11:01:14
存在分歧,安东尼·戴维斯与里奇·保罗在交易问题上意见相左

存在分歧,安东尼·戴维斯与里奇·保罗在交易问题上意见相左

好火子
2026-01-26 01:07:32
嫣然医院房东确为医美机构思妍丽创始人张毅;李亚鹏时隔一周开播,直播间瞬间拥入超10万人,多款产品刚上线就被秒光,销售额超1683万

嫣然医院房东确为医美机构思妍丽创始人张毅;李亚鹏时隔一周开播,直播间瞬间拥入超10万人,多款产品刚上线就被秒光,销售额超1683万

极目新闻
2026-01-23 21:08:36
2026-01-26 06:03:00
InfoQ incentive-icons
InfoQ
有内容的技术社区媒体
11983文章数 51713关注度
往期回顾 全部

科技要闻

黄仁勋在上海逛菜市场,可能惦记着三件事

头条要闻

委代总统控诉遭美国威胁:不配合就杀了你们

头条要闻

委代总统控诉遭美国威胁:不配合就杀了你们

体育要闻

中国足球不会一夜变强,但他们已经创造历史

娱乐要闻

央八开播 杨紫胡歌主演的40集大剧来了

财经要闻

隋广义等80人被公诉 千亿骗局进入末路

汽车要闻

别克至境E7内饰图曝光 新车将于一季度正式发布

态度原创

旅游
亲子
游戏
本地
军事航空

旅游要闻

构建世界级长城大景区 加强文物资源活化利用

亲子要闻

惊!老爸越老,孩子得病几率直线上升!真相揭秘!

LCK春季赛:道心没有破碎,KT找回状态,三局战胜BRO

本地新闻

云游中国|格尔木的四季朋友圈,张张值得你点赞

军事要闻

俄美乌三方首轮会谈细节披露

无障碍浏览 进入关怀版