模块链接

一个大型项目,总是会拆分成组件库、工具库、业务模块等。它们总是会写在不同的地方,以独立的仓库、monorepo 包等形式存在,但是最终都需要系统的主程序链接这些模块。Gez 的核心功能就是帮你把这些不同地方的模块,快速的链接到一起。实现一个服务发布,其它服务同时更新。

TIP

Gez 默认支持 SSR,你也可以把它当成 CSR 来使用。

设计理念

  • 我们应该设计一个基础服务,由基础服务提供所有的第三方依赖。
  • 基础服务统一维护第三方依赖更新,一次发布,所有业务系统生效。
  • 业务服务仅构建业务代码,所有的第三方依赖,应指向到基础服务中。
TIP

由于第三方依赖,都被指向到了基础服务,不再需要重复打包,这会让 Rspack 的编译速度,再提升一个台阶。

构建

传统的 SSR 程序在构建目标为 node 时,会将 node_modules 的模块设置为外部依赖,但是 Gez 会把全部代码都打包成 ESM 模块来进行链接。所以在使用一些第三方依赖的时候,尽可能的选择支持 ESM 的包,否则你可能会遇到一些问题。 构建完成后,通常你可以看到这样的目录结构。

- dist/ # 构建输出目录 - client/ # 客户端构建输出 - chunks/ # 当前服务抽离的公共代码 - [name].[contenthash].js - npm/ # 对外导出的 node_modules 包 - [name].[contenthash].js - src/ # 对外导出的 src 目录下的文件 - [name].[contenthash].js - versions/ # 执行 gez release 命令,会将 client 和 server 的代码打包到这里 - [contenthash].zip # 压缩文件 - [contenthash].json # 当前压缩的版本号 - latest.json # 最新的版本号 - entry.[contenthash].js # 入口文件 - importmap.js # 不可缓存文件,执行后往 globalThis 注入 __importmap__ - importmap.[contenthash].js # 可缓存文件,执行后往 globalThis 注入 __importmap__ - package.json # 声明模块的基本导出信息 - server/ # 服务端构建输出 - ... # 除了缺少 versions 目录,其它和 client 目录一致
TIP

使用 [contenthash] 可以让我们生成基于内容哈希的文件名,这样我们的静态资产文件就可以放心的设置为强缓存了。

客户端链接

在服务渲染时注入所有服务的 /[服务名]/importmap.[contenthash].js 文件,将模块的哈希映射信息写入到 globalThis.__importmap__ 对象中,最终将该变量值写入到 <script type="importmap"></script> 标签中。

src/entry.server.ts

import type { RenderContext } from '@gez/core';

export default async (rc: RenderContext) => {
    const script = await rc.script();
    rc.html = `
<!DOCTYPE html>
<html>
<head>
    <title>Gez</title>
</head>
<body>
    ${script}
</body>
</html>
`;
};

客户端渲染

TIP
  • package.json 中有一个 hash 字段,等同于 importmap.[contenthash].js 文件的哈希值。
  • 如果你只想做客户端渲染,可以编写一个脚本,读取每个服务的 dist/client/package.json 来生成一个静态的 index.html。可以参考一下 RenderContext 的实现。

服务端链接

src/entry.node.ts

  • 在开发阶段时,我们可以设置一个远程的依赖地址。

  • 程序会根据你配置的本地路径,计算出一个所有服务可以共同访问的 node_modules 路径,并自动创建软链接。

export default {
    name: 'ssr-module-auth',
    modules: {
        imports: {
            'ssr-base': ['root:../ssr-base/dist', 'https://<hostname>/ssr-base/versions/latest.json']
        }
    }
} satisfies GezOptions;
WARNING

在生产环境中,你应该使用本地链接,而不是远程链接,这样能提高应用程序的启动速度。如果你使用 Docker,可以通过使用持久卷,将不同服务的产物组织到一个目录中。

package.json

配置 postinstall 钩子执行 gez install 命令。在安装开发依赖时,就会将远程依赖下载到你配置的 'root:../ssr-base/dist' 目录中。

"scripts": {
    "postinstall": "gez install"
}
WARNING

这个需要在构建时,提供对应的版本才能下载。更多请查看 gez release 命令说明。

示例

ssr-base

基础服务,提供了所有的第三方依赖和基础组件。

src/entry.node.ts

export default {
    name: 'ssr-base',
    modules: {
        exports: [
            'root:src/components/layout.vue',
            'root:src/utils/index.ts',
            'npm:vue',
            'npm:vue-router'
        ]
    }
} satisfies GezOptions;
WARNING

如果一个依赖包,只在 SSR 中使用,那么你应该总是将它放到开发依赖中,这样能显著减少安装生产依赖的大小。

导出类型

  • 使用方式
    • 在其它的服务的 src/entry.node.ts 文件中的 modules.imports 配置添加 ssr-base
  • root:
    • 配置项目文件路径,通常是 src 目录的。
    • 例如:
      • import Layout from 'ssr-base/src/components/layout.vue'
      • import utils from 'ssr-base/utils/index'
  • npm:
    • 指向的是当前项目的依赖包,通常是 package.json 字段中 devDependencies 配置的依赖名。
    • 需要配置 modules.externals,将对应的依赖名,指向到 ssr-base/npm/包名

多版本依赖共存

package.json 中配置别名。

{
    "dependencies": {
        "query-string5": "npm:query-string@^5.1.1",
        "query-string6": "npm:query-string@^6.11.1"
    }
}

src/entry.node.ts 文件中配置对外导出。

export default {
    name: 'ssr-base',
    modules: {
        exports: [
            // ...
            'npm:query-string5', // 模块链接:ssr-base/npm/query-string5
            'npm:query-string6'  // 模块链接:ssr-base/npm/query-string6
        ]
    }
} satisfies GezOptions;

在对应的服务将 query-string 模块链接到你需要的版本。

ssr-module-auth

将登录、注册、验证码、修改密码、找回密码等用户信息认证的业务单独在一个服务中实现,对外导出相关的页面路由地址。

src/entry.node.ts

export default {
    name: 'ssr-module-auth',
    modules: {
        // 其它服务使用:import routes from 'ssr-module-auth/src/routes
        exports: ['root:src/routes.ts'],
        // 总是需要配置依赖服务的地址
        imports: {
            'ssr-base': 'root:../ssr-base/dist'
        },
        // 这里只配置链接的第三方依赖,实现依赖共享
        externals: {
            vue: 'ssr-base/npm/vue',
            'vue-router': 'ssr-base/npm/vue-router'
        }
    }
} satisfies GezOptions;
TIP
  • 当我们开发一个很大的系统时,可以按照业务模块来划分不同的服务,明确每个服务的职责。
  • 当你将第三方依赖全部指向到 ssr-base 服务时,项目构建速度总是会非常快,基本上都能在瞬间完成。

ssr-main

系统的主程序,负责将不同的业务服务,链接到一起。

src/entry.node.ts

export default {
    name: 'ssr-main',
    modules: {
        imports: {
            'ssr-base': 'root:../ssr-base/dist',
            'ssr-module-auth': 'root:../ssr-module-auth/dist'
        },
        externals: {
            vue: 'ssr-base/npm/vue',
            'vue-router': 'ssr-base/npm/vue-router'
        }
    }
} satisfies GezOptions;

使用方式

  • import Layout from 'ssr-base/src/components/layout.vue'
    • 调用 ssr-base 服务的路由配置文件
  • import routes from 'ssr-module-auth/src/routes
    • 调用 ssr-module-auth 服务的路由配置文件
  • import Vue from 'vue'
    • 构建工具会替换为 import Vue from 'ssr-base/npm/vue'
    • 其它依赖举一反三