技术创新
首个基于 ESM 构建的 SSR 多服务模块链接。
diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..5008ddfc Binary files /dev/null and b/.DS_Store differ diff --git a/404.html b/404.html new file mode 100644 index 00000000..2ea12aa8 --- /dev/null +++ b/404.html @@ -0,0 +1,17 @@ + + + +
+ + + + +在 Gez 中,默认使用服务名来作为别名,这样有两个好处。
+程序会读取这里的 name
配置,设置别名为 ssr-module-auth
。
同时还需要在 tsconfig.json
配置别名。
如果你想了解多服务类型如何工作的,可以了解一下 gez release 命令的说明。 +
一个典型的命令配置。
+本地开发时启动。
+如果链接的服务是一个本地的目录,你也可以把该服务跑起来快速的开发调试。
构建生产代码
+有三个产物,分别是 client、server、node。
当前服务如果有对外导出模块时使用。
+gez build
命令,构建生产产物。npm run build:dts
命令,将类型输出到 dist/server/src
目录,本地开发时,可以得到类型提示。gez release
命令,将 dist/client
和 dist/server
目录生成 zip 压缩文件,放到 dist/client/versions
目录中。dist
目录的代码,部署到生产环境中。npm install
命令,触发 postinstall
钩子,再执行 gez install
命令下载可以封装一个 build
命令,将多个命令封装到一起。
+
等同于执行 gez build && gez start
运行生产环境代码
+开发环境中,所依赖的外部服务代码变更,总是会获得热更新,但是在生产环境中是没有热更新的。
如果依赖的服务发布更新了,你需要手动重启一下服务,或者编写一个脚本,监听版本发布来重启服务。
安装远程依赖到本地
+在 postinstall
钩子中,执行 npm install --production
安装生产依赖无效。
+
一个大型项目,总是会拆分成组件库、工具库、业务模块等。它们总是会写在不同的地方,以独立的仓库、monorepo 包等形式存在,但是最终都需要系统的主程序链接这些模块。Gez 的核心功能就是帮你把这些不同地方的模块,快速的链接到一起。实现一个服务发布,其它服务同时更新。
+Gez 默认是支持 SSR 的,你也可以把它当成 CSR 来使用。
由于第三方依赖,都被指向到了基础服务,不再需要重复打包,这会让 Rspack 的编译速度,再提升一个台阶。
传统的 SSR 程序在构建目标为 node
时,会将 node_modules
的模块设置为外部依赖,但是 Gez 会把全部代码都打包成 ESM 模块来进行链接。所以在使用一些第三方依赖的时候,尽可能的选择支持 ESM 的包,否则你可能会遇到一些问题。
+构建完成后,通常你可以看到这样的目录结构。
使用 [contenthash]
可以让我们生成基于内容哈希的文件名,这样我们的静态资产文件就可以放心的设置为强缓存了。
+
在服务渲染时注入所有服务的 /[服务名]/importmap.[contenthash].js
文件,将模块的哈希映射信息写入到 globalThis.__importmap__
对象中,最终将该变量值写入到 <script type="importmap"></script>
标签中。
package.json
中有一个 hash
字段,等同于 importmap.[contenthash].js
文件的哈希值。dist/client/package.json
来生成一个静态的 index.html
。可以参考一下 ServerContext 的实现。在开发阶段时,我们可以设置一个远程的依赖地址。
+程序会根据你配置的本地路径,计算出一个所有服务可以共同访问的 node_modules
路径,并自动创建软链接。
在生产环境中,你应该使用本地链接,而不是远程链接,这样能提高应用程序的启动速度。如果你使用 Docker,可以通过使用持久卷,将不同服务的产物组织到一个目录中。 +
配置 postinstall
钩子执行 gez install
命令。在安装开发依赖时,就会将远程依赖下载到你配置的 'root:../ssr-base/dist'
目录中。
这个需要在构建时,提供对应的版本才能下载。更多请查看 gez release 命令说明。 +
基础服务,提供了所有的第三方依赖和基础组件。
+如果一个依赖包,只在 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
中配置别名。
在 src/entry.node.ts
文件中配置对外导出。
在对应的服务将 query-string
模块链接到你需要的版本。
将登录、注册、验证码、修改密码、找回密码等用户信息认证的业务单独在一个服务中实现,对外导出相关的页面路由地址。
+ssr-base
服务时,项目构建速度总是会非常快,基本上都能在瞬间完成。系统的主程序,负责将不同的业务服务,链接到一起。
+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'
Gez 是 Genesis 迭代的第三个大版本,v1.0 是通过 HTTP 请求来实现的远程组件,v2.0 是通过 Module Federation v1.0
+实现的远程组件。随着主流浏览器都已经支持 ESM,这使得设计一款基于 ESM 的模块链接变成了可能。随着 Rspack v1.0 的发布,提供了对 ESM 更加友好的支持,这使得我们可以将可能变成了现实。于是,我们将 v3.0 版本重命名为 Gez
。
目前社区类微服务的解决方案基本可以分为 iframe、micro-app、module federation 三种代表。其中 iframe 和 micro-app 这种模式只适合对已有的老项目进行缝合,是以降低程序运行效率所做的一种妥协,而 module federation 的接入成本较高,里面又是一个黑盒子,一旦出了问题,都十分难以排查。
+Gez 完全是基于 ESM 模块系统进行设计,默认支持 SSR,每个服务都可以对外导出模块,也可以使用外部模块,整个过程简单透明,能够精准的控制依赖管理。通过 importmap 将多服务的模块映射到具有强缓存,基于内容哈希的 URL 中
+import vue from 'vue'
替换成 ssr-npm/npm/vue.[contenthash].js
,这样静态文件就可以设置为强缓存了。对于不支持 importmap 的浏览器,es-module-shims 提供了降级的方案node --experimental-vm-modules --experimental-import-meta-resolve
来实现它。v3.0
版本。从最早的构思,到开始调研 Vite、Rspack,中间经历了一年多的时间,庆幸的是这条路最终走通了,并且达到了生产可用。
Gez 的定位并不是成为 Next.js 或 Nuxt.js 那样大而全的框架,而是成为一个具有 Typescript、ESM、SSR、模块链接等特性的基础设施,你可以在这个基础上来构建属于你自己的 Next.js
。如果你需要定制化实现,它会很适合你。
所有的主流浏览器都已经支持,针对一些低版本的浏览器,可以提供一个升级的页面来引导用户升级它的浏览器。
+从 v1.0
、v2.0
到现在的 v3.0
,已经走过了将近 5 年的时光,支持起了公司内部数十个业务的项目,并且不断地推动业务项目的升级。
这是一个与框架无关的例子,采用原生的 HTML 来开发项目
+在 package.json 文件添加
+总是应该将生产依赖和开发依赖区分,会使 node_modules
在生产环境中更小。
+
安装生产依赖
+安装开发依赖
+在 package.json 文件添加
+基本结构
+模拟水合,更新当前时间
+模拟框架的 SSR API,渲染出 HTML 内容返回
+创建一个 web 服务器,来处理客户请求
++浏览器打开:http://localhost:3005
+
Write something to build your own docs! 🎁
URL: ${params.url}
\n \n ${script}\n\n\n`;\n};\n\n'})}),"\n",(0,s.jsxs)(n.h3,{id:"srcentrynodets",children:["src/entry.node.ts",(0,s.jsx)(n.a,{className:"header-anchor","aria-hidden":"true",href:"#srcentrynodets",children:"#"})]}),"\n",(0,s.jsx)(n.p,{children:"创建一个 web 服务器,来处理客户请求"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-ts",children:"import http from 'node:http';\nimport type { GezOptions } from '@gez/core';\n\nexport default {\n // 设置应用的唯一名字,如果有多个项目,则名字不能重复\n name: 'ssr-html',\n // 本地执行 dev 和 build 时会使用\n async createDevApp(gez) {\n return import('@gez/rspack').then((m) =>\n m.createApp(gez, (buildContext) => {\n // 可以在这里修改 Rspack 编译的配置\n })\n );\n },\n async createServer(gez) {\n const server = http.createServer((req, res) => {\n // 静态文件处理\n gez.middleware(req, res, async () => {\n // 传入渲染的参数\n const ctx = await gez.render({\n url: req.url\n });\n // 响应 HTML 内容\n res.end(ctx.html);\n });\n });\n // 监听端口\n server.listen(3005, () => {\n console.log('http://localhost:3005');\n });\n }\n} satisfies GezOptions;\n\n"})}),"\n",(0,s.jsxs)(n.h3,{id:"启动项目",children:["启动项目",(0,s.jsx)(n.a,{className:"header-anchor","aria-hidden":"true",href:"#启动项目",children:"#"})]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"npm run dev\n"})}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsxs)(n.p,{children:["浏览器打开:",(0,s.jsx)(n.a,{href:"http://localhost:3005",target:"_blank",rel:"noopener noreferrer",children:"http://localhost:3005"})]}),"\n"]})]})}function d(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},{wrapper:n}=Object.assign({},(0,t.ah)(),e.components);return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(a,{...e})}):a(e)}let i=d;d.__RSPRESS_PAGE_META={},d.__RSPRESS_PAGE_META["guide%2Fstart%2Fgetting-started.mdx"]={toc:[{text:"创建项目",id:"创建项目",depth:2},{text:"将项目设置为 module",id:"将项目设置为-module",depth:2},{text:"安装依赖",id:"安装依赖",depth:2},{text:"添加脚本",id:"添加脚本",depth:2},{text:"入口文件",id:"入口文件",depth:2},{text:"src/entry.client.ts",id:"srcentryclientts",depth:3},{text:"src/entry.server.ts",id:"srcentryserverts",depth:3},{text:"src/entry.node.ts",id:"srcentrynodets",depth:3},{text:"启动项目",id:"启动项目",depth:3}],title:"快速开始",frontmatter:{}}}}]); \ No newline at end of file diff --git a/static/js/async/512.8135c617.js b/static/js/async/512.8135c617.js new file mode 100644 index 00000000..453f876e --- /dev/null +++ b/static/js/async/512.8135c617.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkssr_docs=self.webpackChunkssr_docs||[]).push([["512"],{9790:function(t,e,i){i.r(e),i.d(e,{default:function(){return c}});var n=i(1549),s=i(6603);function a(t){return(0,n.jsx)(n.Fragment,{})}function o(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},{wrapper:e}=Object.assign({},(0,s.ah)(),t.components);return e?(0,n.jsx)(e,{...t,children:(0,n.jsx)(a,{...t})}):a(t)}let c=o;o.__RSPRESS_PAGE_META={},o.__RSPRESS_PAGE_META["index.md"]={toc:[],title:"",frontmatter:{pageType:"home",hero:{name:"Gez",text:"基于 ESM 的模块链接。",actions:[{theme:"brand",text:"快速开始",link:"/guide/"},{theme:"alt",text:"GitHub",link:"https://github.com/dp-os/gez"}]},features:[{title:"技术创新",details:"首个基于 ESM 构建的 SSR 多服务模块链接。",icon:"\uD83D\uDC4D"},{title:"项目构建",details:"基于 Rspack 实现,构建速度极快,带给你极致的开发体验。",icon:"\uD83D\uDE80"},{title:"依赖管理",details:"一次构建,一次发布,多服务生效。",icon:"\uD83C\uDFAF"},{title:"同构渲染",details:"支持 Vue2、Vue3、React 等不同框架实现 SSR。",icon:"☁️"},{title:"基准支持",details:"Node20 和支持 ESM dynamic import 和 import.meta 的浏览器。",icon:"\uD83D\uDE0E"},{title:"长久维护",details:"Genesis 从 2020 年迭代至今,现更名为 Gez。",icon:"\uD83D\uDC4F"}]}}}}]); \ No newline at end of file diff --git a/static/js/async/52.3753b1a1.js b/static/js/async/52.3753b1a1.js new file mode 100644 index 00000000..cded2a3a --- /dev/null +++ b/static/js/async/52.3753b1a1.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkssr_docs=self.webpackChunkssr_docs||[]).push([["52"],{5:function(e,s,n){n.r(s),n.d(s,{default:function(){return t}});var r=n(1549),i=n(6603);function d(e){let s=Object.assign({h1:"h1",a:"a",p:"p",div:"div",h2:"h2",ul:"ul",li:"li",code:"code",pre:"pre",br:"br",h3:"h3",h4:"h4"},(0,i.ah)(),e.components);return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsxs)(s.h1,{id:"模块链接",children:["模块链接",(0,r.jsx)(s.a,{className:"header-anchor","aria-hidden":"true",href:"#模块链接",children:"#"})]}),"\n",(0,r.jsx)(s.p,{children:"一个大型项目,总是会拆分成组件库、工具库、业务模块等。它们总是会写在不同的地方,以独立的仓库、monorepo 包等形式存在,但是最终都需要系统的主程序链接这些模块。Gez 的核心功能就是帮你把这些不同地方的模块,快速的链接到一起。实现一个服务发布,其它服务同时更新。"}),"\n",(0,r.jsxs)(s.div,{className:"rspress-directive tip",children:[(0,r.jsx)(s.div,{className:"rspress-directive-title",children:"TIP"}),(0,r.jsx)(s.div,{className:"rspress-directive-content",children:(0,r.jsx)(s.p,{children:"Gez 默认是支持 SSR 的,你也可以把它当成 CSR 来使用。"})})]}),"\n",(0,r.jsxs)(s.h2,{id:"设计理念",children:["设计理念",(0,r.jsx)(s.a,{className:"header-anchor","aria-hidden":"true",href:"#设计理念",children:"#"})]}),"\n",(0,r.jsxs)(s.ul,{children:["\n",(0,r.jsx)(s.li,{children:"我们应该设计一个基础服务,由基础服务提供所有的第三方依赖。"}),"\n",(0,r.jsx)(s.li,{children:"基础服务统一维护第三方依赖更新,一次发布,所有业务系统生效。"}),"\n",(0,r.jsx)(s.li,{children:"业务服务仅构建业务代码,所有的第三方依赖,应指向到基础服务中。"}),"\n"]}),"\n",(0,r.jsxs)(s.div,{className:"rspress-directive tip",children:[(0,r.jsx)(s.div,{className:"rspress-directive-title",children:"TIP"}),(0,r.jsx)(s.div,{className:"rspress-directive-content",children:(0,r.jsx)(s.p,{children:"由于第三方依赖,都被指向到了基础服务,不再需要重复打包,这会让 Rspack 的编译速度,再提升一个台阶。"})})]}),"\n",(0,r.jsxs)(s.h2,{id:"构建",children:["构建",(0,r.jsx)(s.a,{className:"header-anchor","aria-hidden":"true",href:"#构建",children:"#"})]}),"\n",(0,r.jsxs)(s.p,{children:["传统的 SSR 程序在构建目标为 ",(0,r.jsx)(s.code,{children:"node"})," 时,会将 ",(0,r.jsx)(s.code,{children:"node_modules"})," 的模块设置为外部依赖,但是 Gez 会把全部代码都打包成 ESM 模块来进行链接。所以在使用一些第三方依赖的时候,尽可能的选择支持 ESM 的包,否则你可能会遇到一些问题。\n构建完成后,通常你可以看到这样的目录结构。"]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{children:"- dist/ # 构建输出目录\n - client/ # 客户端构建输出\n - chunks/ # 当前服务抽离的公共代码\n - [name].[contenthash].js\n - npm/ # 对外导出的 node_modules 包\n - [name].[contenthash].js\n - src/ # 对外导出的 src 目录下的文件\n - [name].[contenthash].js\n - versions/ # 执行 gez release 命令,会将 client 和 server 的代码打包到这里\n - [contenthash].zip # 压缩文件\n - [contenthash].json # 当前压缩的版本号\n - latest.json # 最新的版本号\n - entry.[contenthash].js # 入口文件\n - importmap.js # 不可缓存文件,执行后往 globalThis 注入 __importmap__ \n - importmap.[contenthash].js # 可缓存文件,执行后往 globalThis 注入 __importmap__ \n - package.json # 声明模块的基本导出信息\n - server/ # 服务端构建输出\n - ... # 除了缺少 versions 目录,其它和 client 目录一致\n"})}),"\n",(0,r.jsxs)(s.div,{className:"rspress-directive tip",children:[(0,r.jsx)(s.div,{className:"rspress-directive-title",children:"TIP"}),(0,r.jsx)(s.div,{className:"rspress-directive-content",children:(0,r.jsxs)(s.p,{children:["使用 ",(0,r.jsx)(s.code,{children:"[contenthash]"})," 可以让我们生成基于内容哈希的文件名,这样我们的静态资产文件就可以放心的设置为强缓存了。",(0,r.jsx)(s.br,{}),"\n"]})})]}),"\n",(0,r.jsxs)(s.h2,{id:"客户端链接",children:["客户端链接",(0,r.jsx)(s.a,{className:"header-anchor","aria-hidden":"true",href:"#客户端链接",children:"#"})]}),"\n",(0,r.jsxs)(s.p,{children:["在服务渲染时注入所有服务的 ",(0,r.jsx)(s.code,{children:"/[服务名]/importmap.[contenthash].js"})," 文件,将模块的哈希映射信息写入到 ",(0,r.jsx)(s.code,{children:"globalThis.__importmap__"})," 对象中,最终将该变量值写入到 ",(0,r.jsx)(s.code,{children:'