技术创新
首个基于 ESM 构建的 SSR 多服务模块链接。
diff --git a/404.html b/404.html new file mode 100644 index 00000000..a1e8b5bc --- /dev/null +++ b/404.html @@ -0,0 +1,17 @@ + + + +
+ + + + +new Gez(options?
)
Name | +Type | +
---|---|
options |
+GezOptions |
+
Private
_app: null
| App
= null
Private
_command: null
| COMMAND
= null
Private
Readonly
_options: GezOptions
Readonly
moduleConfig: ParsedModuleConfig
根据传入的 modules 选项解析出来的对象。
+Readonly
packConfig: ParsedPackConfig
get
COMMAND(): typeof COMMAND
全部命令的枚举对象。
+typeof COMMAND
Private
get
app(): App
get
basePath(): string
根据服务名称生成的静态资源基本路径。
+string
get
basePathPlaceholder(): string
动态的 base 地址占位符。
+string
get
command(): COMMAND
当前执行的命令。
+COMMAND
get
isProd(): boolean
是否是生产环境。
+boolean
get
middleware(): Middleware
中间件。
+get
name(): string
服务名称,来源于 package.json 文件的 name 字段。
+string
get
render(): (options?
: RenderContextOptions
) => Promise
<RenderContext
>
调用 entry.server.ts 导出的渲染函数。
+fn
(options?
): Promise
<RenderContext
>
渲染函数
+Name | +Type | +Description | +
---|---|---|
options? |
+RenderContextOptions |
+透传给 RenderContextOptions | +
Promise
<RenderContext
>
get
root(): string
项目根目录。
+string
get
varName(): string
根据 name 生成的 JS 变量名称。
+string
build(): Promise
<boolean
>
构建生产代码。
+Promise
<boolean
>
createServer(): Promise
<void
>
执行下面的命令,会创建服务器。
+Promise
<void
>
destroy(): Promise
<boolean
>
销毁实例,释放内存。
+Promise
<boolean
>
getManifestList(target
): ManifestJson
[]
获取全部服务的清单文件。
+Name | +Type | +
---|---|
target |
+"client" | "server" |
+
getServerImportMap(): ImportMap
获取服务端的 importmap 映射文件。
+ImportMap
init(command
): Promise
<boolean
>
初始化实例。
+Name | +Type | +
---|---|
command |
+COMMAND |
+
Promise
<boolean
>
postCompileProdHook(): Promise
<boolean
>
执行 gez build 命令回调。
+Promise
<boolean
>
readJsonSync(filename
): any
异步的读取一个 JSON 文件。
+Name | +Type | +
---|---|
filename |
+string |
+
any
resolvePath(projectPath
, ...args
): string
解析项目路径。
+Name | +Type | +
---|---|
projectPath |
+ProjectPath |
+
...args |
+string [] |
+
string
writeSync(filepath
, data
): void
同步写入一个文件。
+Name | +Type | +
---|---|
filepath |
+string |
+
data |
+any |
+
void
Static
getDistOptions(): Promise
<GezOptions
>
获取 dist/node/src/entry.node.js 文件导出的选项
+Promise
<GezOptions
>
Static
getSrcOptions(): Promise
<GezOptions
>
获取 src/entry.node.ts 文件导出的选项
+Promise
<GezOptions
>
渲染上下文
+new RenderContext(gez
, options?
)
Name | +Type | +
---|---|
gez |
+Gez |
+
options |
+RenderContextOptions |
+
Private
_html: string
= ''
Readonly
base: string
参数传入的 base。
+Readonly
entryName: string
参数传入的 entryName。
+files: RenderFiles
importMetaSet 收集完成后,调用 rc.commit() 函数时,会更新这个对象的信息。
+gez: Gez
Gez 的实例。
+importMetaSet: Set
<ImportMeta
>
服务端渲染过程中,收集模块执行过程中的 import.meta 对象。
+Readonly
params: Record
<string
, any
>
参数传入的 params。
+redirect: null
| string
= null
重定向地址。
+status: null
| number
= null
响应的状态码。
+get
html(): string
响应的 html 内容。
+string
set
html(html
): void
Name | +Type | +
---|---|
html |
+string |
+
void
commit(): Promise
<void
>
同构应用渲染完成后,提交模块依赖更新 files 对象。
+Promise
<void
>
css(): string
根据 files 生成服务端首屏加载的 CSS。
+string
importmap(): string
根据 files 生成 importmap 相关代码。
+string
moduleEntry(): string
根据 files 生成模块入口执行代码。
+string
modulePreload(): string
根据 files 生成 ESM 模块预加载代码。
+string
preload(): string
根据 files 生成 JS 和 CSS 文件的预加载代码。
+string
serialize(input
, options?
): string
透传 https://github.com/yahoo/serialize-javascript
+Name | +Type | +
---|---|
input |
+any |
+
options? |
+SerializeJSOptions |
+
string
state(varName
, data
): string
在 window 对象,注入一个 JS 变量对象,data 必须是可以被序列化的。
+Name | +Type | +
---|---|
varName |
+string |
+
data |
+Record <string , any > |
+
string
createMiddleware(gez
): Middleware
Name | +Type | +
---|---|
gez |
+Gez |
+
mergeMiddlewares(middlewares
): Middleware
将多个中间件,合并成一个中间件执行
+Name | +Type | +Description | +
---|---|---|
middlewares |
+Middleware [] |
+中间件列表 | +
parseModuleConfig(name
, root
, config?
): ParsedModuleConfig
解析模块配置
+Name | +Type | +Description | +
---|---|---|
name |
+string |
+当前运行服务的名字 | +
root |
+string |
+当前运行服务的根路径 | +
config |
+ModuleConfig |
+模块的配置 | +
parsePackConfig(config?
): ParsedPackConfig
Name | +Type | +
---|---|
config |
+PackConfig |
+
Optional
build: () => Promise
<boolean
>
(): Promise
<boolean
>
执行构建
+Promise
<boolean
>
Optional
destroy: () => Promise
<boolean
>
(): Promise
<boolean
>
销毁实例,释放内存
+Promise
<boolean
>
middleware: Middleware
中间件列表
+render: (options?
: RenderContextOptions
) => Promise
<RenderContext
>
(options?
): Promise
<RenderContext
>
渲染函数
+Name | +Type | +Description | +
---|---|---|
options? |
+RenderContextOptions |
+透传给 RenderContextOptions | +
Promise
<RenderContext
>
详细说明,请看文档:https://dp-os.github.io/gez/api/gez.html
+Optional
basePathPlaceholder: string
| false
动态路径的变量占位符。
+Optional
createDevApp: (gez
: Gez
) => Promise
<App
>
(gez
): Promise
<App
>
创建开发应用,在执行 dev、build、preview 命令时调用。
+Name | +Type | +
---|---|
gez |
+Gez |
+
Promise
<App
>
Optional
createServer: (gez
: Gez
) => Promise
<void
>
(gez
): Promise
<void
>
创建服务器,执行 dev、build、preview 命令时调用。
+Name | +Type | +
---|---|
gez |
+Gez |
+
Promise
<void
>
Optional
isProd: boolean
是否是生产环境。
+Optional
modules: ModuleConfig
模块链接配置。
+Optional
packs: PackConfig
是否启用归档,等同于 npm pack。
+Optional
postCompileProdHook: (gez
: Gez
) => Promise
<void
>
(gez
): Promise
<void
>
gez build 构建完成后,以生产模式执行的钩子。
+Name | +Type | +
---|---|
gez |
+Gez |
+
Promise
<void
>
Optional
root: string
项目根目录,默认为当前执行命令的目录。
buildFiles: string
[]
构建的全部文件清单
+chunks: Record
<string
, ManifestJsonChunks
>
编译的文件信息 +类型:Record<源文件, 编译信息>
+exports: Record
<string
, string
>
对外导出的文件
+hash: string
构建的版本号
+name: string
服务名字,来自于:GezOptions.name
+type: "module"
模块系统
+version: string
版本号,默认为 1.0.0
css: string
[]
当前编译的 CSS 文件。
+js: string
当前编译的 JS 文件。
+resources: string
[]
其它的资源文件。
+sizes: ManifestJsonChunkSizes
构建产物的大小。
Optional
exports: string
[]
对外导出的文件 +必须以 npm: 或 root: 开头 +npm:开头代表 node_modules 的依赖 +root:开头代表项目内root目录下的文件 +例如: +npm:vue +root:src/routes +root:src/[filename]
+Optional
externals: Record
<string
, string
>
设置项目的外部依赖 +例如: +{ +"vue": "ssr-npm/vue" +}
+Optional
imports: Record
<string
, string
>
导入的模块基本配置
Optional
enable: boolean
是否启用归档
+Optional
onAfter: (gez
: Gez
, pkgJson
: Record
<string
, any
>, file
: Buffer
) => Promise
<void
>
(gez
, pkgJson
, file
): Promise
<void
>
Name | +Type | +
---|---|
gez |
+Gez |
+
pkgJson |
+Record <string , any > |
+
file |
+Buffer |
+
Promise
<void
>
Optional
onBefore: (gez
: Gez
, pkgJson
: Record
<string
, any
>) => Promise
<void
>
(gez
, pkgJson
): Promise
<void
>
Name | +Type | +
---|---|
gez |
+Gez |
+
pkgJson |
+Record <string , any > |
+
Promise
<void
>
Optional
outputs: string
| boolean
| string
[]
输出的文件
+Optional
packageJson: (gez
: Gez
, pkgJson
: Record
<string
, any
>) => Promise
<Record
<string
, any
>>
(gez
, pkgJson
): Promise
<Record
<string
, any
>>
Name | +Type | +
---|---|
gez |
+Gez |
+
pkgJson |
+Record <string , any > |
+
Promise
<Record
<string
, any
>>
Optional
releaseType: "major"
| "premajor"
| "minor"
| "preminor"
| "patch"
| "prepatch"
| "prerelease"
发布的类型 +环境变量设置:process.env.RELEASE_TYPE
exports: { exportName
: string
; exportPath
: string
; externalName
: string
; importName
: string
; name
: string
; type
: PathType
}[]
对外导出的文件
+externals: Record
<string
, { import?
: string
; match
: RegExp
}>
外部依赖
+imports: { localPath
: string
; name
: string
}[]
导入的外部服务
+name: string
当前的服务名字
+root: string
当前服务运行的根目录
enable: boolean
onAfter: (gez
: Gez
, pkgJson
: Record
<string
, any
>, file
: Buffer
) => Promise
<void
>
(gez
, pkgJson
, file
): Promise
<void
>
Name | +Type | +
---|---|
gez |
+Gez |
+
pkgJson |
+Record <string , any > |
+
file |
+Buffer |
+
Promise
<void
>
onBefore: (gez
: Gez
, pkgJson
: Record
<string
, any
>) => Promise
<void
>
(gez
, pkgJson
): Promise
<void
>
Name | +Type | +
---|---|
gez |
+Gez |
+
pkgJson |
+Record <string , any > |
+
Promise
<void
>
outputs: string
[]
packageJson: (gez
: Gez
, pkgJson
: Record
<string
, any
>) => Promise
<Record
<string
, any
>>
(gez
, pkgJson
): Promise
<Record
<string
, any
>>
Name | +Type | +
---|---|
gez |
+Gez |
+
pkgJson |
+Record <string , any > |
+
Promise
<Record
<string
, any
>>
渲染的参数
+Optional
base: string
静态资产的公共路径,可以根据业务的上下文来动态设置不同的路径。
+Optional
entryName: string
gez.render() 函数执行时,会调用 entry.server.ts 文件导出的名称。
+Optional
params: Record
<string
, any
>
传递给 RenderContext 对象的 params 字段。
当前页面渲染的文件
+css: string
[]
CSS 文件列表。
+importmap: string
[]
importmap.js 文件列表。
+js: string
[]
全部的 JS 文件列表,包含 modulepreload 和 importmap。
+modulepreload: string
[]
ESM 模块列表。
+resources: string
[]
除了 JS 和 CSS 之外的其它文件列表。
Middleware: (req
: IncomingMessage
, res
: ServerResponse
, next
: Function
) => void
(req
, res
, next
): void
Name | +Type | +
---|---|
req |
+IncomingMessage |
+
res |
+ServerResponse |
+
next |
+Function |
+
void
ServerRenderHandle: (render
: RenderContext
) => Promise
<void
>
(render
): Promise
<void
>
服务端渲染处理函数。
+Name | +Type | +
---|---|
render |
+RenderContext |
+
Promise
<void
>
路径别名允许开发者为模块定义别名,以便于在代码中更方便的引用它们。当你想要使用一个简短、易于记忆的名称来代替冗长复杂的路径时,这将非常有用。
+在 Gez 中,默认使用服务名来作为别名,这样有两个好处。
+程序会读取 package.json
的 name
字段,设置别名为 ssr-module-auth
。
同时还需要在 tsconfig.json
配置别名。
业务模块,你应该总是使用默认的别名,但是一些第三方包有时需要设置别名,你可以这样做。
+业务模块对外导出时,程序会做一些打包的优化。如果你自定义了业务模块的别名,可能会导致打包出来的内容不正确。
静态资产的文件路径,总是会读取 package.json
的 name
来生成固定的路径:/${name}/
。
有时,我们将一套代码部署在不同的国家或地区的集群中,允许独立域名访问和二级目录访问。
+你可以根据请求上下文,在给渲染函数传入不同的基本 URL。
+在服务端,静态资产文件的编译路径为 [[[___GEZ_DYNAMIC_BASE___]]]/${name}/
,程序会将你返回的 html
中的 [[[___GEZ_DYNAMIC_BASE___]]]
占位符替换成你传入的 base
。
+
一个典型的命令配置。
+你需要手动配置 tsconfig.json 文件,否则执行 build:dts
命令会报错。
+
本地开发时启动。
+如果链接的服务是一个本地的目录,你也可以把该服务跑起来快速的开发调试。
构建生产代码
+有三个产物,分别是 client、server、node。
等同于执行 gez build && gez start
运行生产环境代码。
+开发环境中,所依赖的外部服务代码变更,总是会获得热更新,但是在生产环境中是没有热更新的。
如果依赖的服务发布更新了,你需要手动重启一下服务,或者编写一个脚本,监听其它服务版本发布来重启服务。
Gez 作为基础设施,它的配置总是非常简单的。
+string
gez
如果你的网站,同一个域名下,使用 Gez 打包了多个项目,那么你需要配置一个 name
来区分不同的项目。
+
类型:string
默认值:cwd()
描述: 项目根目录,默认为当前执行命令的目录。
+如果你没有充足的理由,你都不应该配置它。
+boolean
process.env.NODE_ENV === 'production'
如果你没有充足的理由,你都不应该配置它。
boolean
process.env.npm_config_production !== 'true'
如果你没有充足的理由,你都不应该配置它。
string | false
[[[___GEZ_DYNAMIC_BASE___]]]
如果你的业务上,没有出现用户的内容被误替换,你都不应该配置它。
这是 Gez 的核心功能,点击这里深入了解。
+string[]
[]
你可以将当前项目的模块或者当前项目的第三方依赖,对外导出,这样其它服务就可以使用了。
Record<string, string | [string, string]>
{}
gez install
命令可以下载远程依赖到本地的地址。你也可以直接配置本地地址。
+Record<string, string>
{}
需要先配置对应服务的 modules.imports,否则运行起来会报错,提示找不到模块。 +
你也可以使用其它的框架来创建服务器,例如:Express。 +
(gez: Gez) => Promise<void>
undefined
你可以使用这个钩子来生成静态网站。
如果在生产环境,无法部署一个 Node 实例,可以在构建阶段就生成客户端渲染的 index.html
文件。
在服务渲染时,返回一个通用的模板即可。
+postCompileProdHook
钩子中,手动执行一次 SSR 渲染,将生成的 HTML 写入到 dist/client/index.html
文件中。dist/client/
目录复制到你的服务器上。postCompileProdHook
钩子会在构建完成后,以生产模式执行构建出来的代码。如果你要生成一个完全静态的网站,也可以在这里实现。
我们假设有三个服务,分别是 ssr-base
、ssr-module-auth
、ssr-main
,其中
ssr-base 基础服务,负责第三方依赖的管理,以及提供基础的业务组件、工具函数。
+ssr-module-auth 业务服务,按照业务模块来拆分服务,这里是负责用户认证相关,包含登录、注册、找回密码、验证码相关,最终对外会导出一个路由的配置文件。
+ssr-main 聚合服务,将不同业务服务导出的路由配置注册进路由总线中,实现应用程序的聚合。
+每一个服务,既可以是 Host,也可以是 Remote。下面将会以 ssr-base
和 ssr-main
作为例子,分别扮演 Remote 和 Host,你将会了解到它是如何工作的。
+
在执行 tsc --declaration --emitDeclarationOnly --outDir dist/src
命令时,由于找不到 npm/axios.ts
这个文件,所以不会生成类型文件。
直接将 axios
模块导出。
ssr-main
此时作为 Host,需要将 axios
链接到 ssr-base/npm/axios
,并且需要在项目安装 axios
模块来获得类型提示。
源码:
+将会被替换成:
+在执行 tsc --declaration --emitDeclarationOnly --outDir dist/src
命令时,由于存在 src/axios.ts
这个文件,就会生成相关的类型文件。
创建文件,并导出 axios
。
导出 axios
软件包。
ssr-main
此时作为 Host,需要导入 ssr-base
服务。
此时,你在业务代码中将会获得类型提示。
+如果要考虑到老系统需要迁移,你还是可以选择将 axios
替换成 ssr-base/src/axios
,并且需要安装 axios
模块到当前项目下才能获得类型提示。
+
packs.enable
配置为 true
时,在编译完成后,会将 dist
目录进行归档,写入到 dist/client/versions/latest.tgz
未完待续!
我们有 entry.node.ts
和 entry.server.ts
两个入口文件,entry.node.ts
负责创建服务器,来调用 entry.server.ts
生成 HTML。为了简化 CSS 和 JS 的注入,于是提供了一个 RenderContext
对象。
在 entry.node.ts
通常可以看到这样的代码,调用一个渲染函数,然后服务响应 HTML。
在 entry.server.ts
接收到传入的参数,并且根据传入的参数来响应内容。
在 SSR 应用程序中,要处理注入渲染页面的 CSS 和 JS 文件,并不是一件简单的事情,当需要考虑多服务提供的模块时,这个问题将会变得更难。庆幸的是,Gez 提供了一个标准的实现方案,并且在 Vue 中提供了完整的实现。
+在构建阶段,Gez 会给服务端生成的每一个 JS 文件头部注入一个 import.meta.chunkName
字段,该字段提供了一个 chunk 文件打包的入口源文件。
ssr-vue2-remote
是我们的服务名,src/entry.ts
是这个文件打包的第一个文件。
例如:
+上述代码,就会生成
+将渲染上下文的 importMetaSet
对象传递给 Vue SSR 渲染的上下文对象中。
在 Vue 组件中,收集上下文依赖。
+在实际的操作中,@gez/rspack-vue
已经在编译 .vue
组件时,已经将这段代码注入,你不需要手动调用。
+
等 Vue 组件渲染完成后,调用 await rc.commit()
函数来提交模块上下文的依赖收集, rc.preload()
、rc.css()
、rc.importmap()
、rc.modulePreload()
才能正确的注入客户端所需的依赖。
如果你想更加深入了解模块依赖收集,可以看下 ssr-html 这个例子,它是通过编码的形式来实现模块的依赖收集。 +
Gez 是基于 Rspack 构建应用程序,同时也就继承了 Rspack 的全部优势。Gez 提供了一些 Rspack 的配置,你可以根据自己的情况来选择使用哪个。
+ +提供了 Rspack 的基本配置。
+如果你没有 @gez/rspack
的依赖,那么你可以通过如下命令安装。
createRspackApp
提供了 Gez 的必要配置,默认情况下不提供任何 loader 相关的配置,你还需要配置一些 loader 才能将项目跑起来。
Gez 的默认配置不可修改,否则 Gez 无法正常工作,点击这里 了解默认配置。 +
config?: (context: RspackAppConfigContext) => void;
undefined
createRspackHtmlApp
提供了一些开箱即用的配置,支持 Typescript、Worker、JSON、CSS、Less、Video、Image、Font 的相关文件。
boolean
true
如果你要自定义 CSS loader,可以设置为 false
。
+
SwcLoaderOptions
undefined
Record<string, any>;
undefined
Record<string, any>;
undefined
Record<string, any>;
undefined
string[]
['chrome>=87', 'firefox>=78', 'safari>=14', 'edge>=88']
构建目标小于默认配置,Gez 可能无法正常工作。
string[]
['node>=20']
构建目标小于默认配置,无法保证兼容性,请认真考虑后设置。
提供了 Rspack vue 的基本配置。
+如果你没有 @gez/rspack-vue
的依赖,那么你可以通过如下命令安装。
选项继承于 createRspackHtmlApp。
+Record<string, any>
undefined
experimentalInlineMatchResource
和 optimizeSSR
由程序自动设置,你传入也无效。
+
createRspackVue2App
提供了 Vue2 的 .vue
文件支持。
createRspackVue3App
提供了 Vue3 的 .vue
文件的支持
目前暂不支持 JSX/TSX ,如果你想要支持它。需要自行添加相关的 Rspack 配置。点击这里了解 Vue 的相关配置。
这是一个约定,无法通过程序配置来修改。
+gez.name
来源于 package.json
的 name
字段。dist/package.json
来源于根目录的 package.json
。packs.enable
为 true
时,才会对 dist
目录进行归档。在构建生产代码时,可以设置强缓存部分的资源,总是以 .final[ext]
作为文件名生成规则。也就是说符合这个规则的文件,可以设置强缓存,否则应设置协商缓存。
final
文件使用了 gez.middleware
中间件,就会默认帮你处理这个逻辑。在生产环境时,你可以自己来实现静态服务器来控制不同的缓存策略,对于你来说 gez.middleware
是可选的。点击这里 可以参考实现。
+
在开发时,gez
会启用一些 Node 实验性质的功能,来获得开发环境支持 ESM 热更新和 TypeScript 的原生支持。
但是在生产环境中,我们完全不需要这些,你应该使用构建后的产物来运行程序。
+如果你在生产环境中使用 gez start
来启动你的应用程序,由于启用了 Node 实验性功能的原因,可能会给你的程序带来未知的风险,请始终使用 NODE_ENV=production node dist/index.js
来启动。
+
这是一个与框架无关的例子,采用原生的 HTML 来开发项目
+Gez 默认支持 SSR,但是你可以当成 CSR 来使用。 +
安装生产依赖
+安装开发依赖
+总是应该将生产依赖和开发依赖区分,会使 node_modules
在生产环境中更小。
+
你需要手动配置 tsconfig.json 文件,否则执行 build:dts
命令会报错。
+
你需要将上面的 ssr-html
,替换成 package.json
的 name
字段的值。
+
创建一个 web 服务器,来处理客户请求
+模拟框架的 SSR API,渲染出 HTML 内容返回
+更新当前时间
+++浏览器打开:http://localhost:3000
+
如果你使用了 Gez,欢迎提交 PR,在这里提供更多的例子。
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(ECMAScript Modules)模块系统设计,不仅默认支持服务器端渲染(SSR),还允许每个服务灵活地导出或使用外部模块。这一过程中,Gez保持了简单透明的特性,使得依赖管理变得精准可控。更值得一提的是,通过 importmap 技术,Gez能够将多个服务的模块映射到具有强缓存、基于内容哈希的URL上,从而确保了应用的高效与稳定。
+Gez的初衷在于打造一个支持服务器端渲染(SSR)的微服务架构,旨在助力构建高性能且规模庞大的 Web 应用程序。
现代JavaScript支持:参考了Vite的定义,基准为浏览器对 ESM dynamic import 和 import.meta 的支持。
+内容哈希与importmap:构建产物具备内容哈希,利用 importmap 技术将 import vue from 'vue'
转换为 ssr-npm/npm/vue.[contenthash].final.js
,确保静态文件的强缓存。对于不支持importmap的浏览器,采用 es-module-shims 进行降级处理。
Rspack与ESM外部依赖:Rspack 的 externalsType 支持 module-import,便于设置 ESM 模块的外部依赖。
+Node.js上的ESM热更新:尽管在 Node.js 上实现 ESM 模块的热更新具有挑战性,但可通过启用 node --experimental-vm-modules --experimental-import-meta-resolve
来实现。
Node.js原生支持TypeScript:自 Node.js 22.6 版本起,支持 --experimental-strip-types
,从而原生支持运行TypeScript代码。
经过一年多的构思与对Vite、farmfe、Rspack的深入调研,我们成功打通了这条路径,并确保其生产环境可用性。
Gez 的定位并非旨在成为一个如同 Next.js 或 Nuxt.js 那样功能全面的大型框架。相反,它致力于成为一个具备 Typescript、ESM、SSR(服务器端渲染)以及模块链接等核心特性的基础设施。基于这样的基础,开发者可以自由地构建出属于自己的 Next.js
。对于那些追求高度定制化的实现,Gez 将是一个理想的选择。
在大型项目开发过程中,为了提高代码的可维护性和复用性,通常会将项目拆分为多个组件库、工具库和业务模块。这些部分往往分散在不同的位置,可能以 multirepo 或 monorepo 包的形式进行管理和存储。然而,这些分散的模块最终需要通过系统的主程序进行有效的整合和链接,以确保整个系统的协同工作。
+在这个过程中,Gez发挥了至关重要的作用。其核心功能在于能够快速地将这些分布在不同地方的模块进行链接,从而形成一个完整、统一的系统。通过Gez,开发者可以轻松地实现一个服务的发布,并确保其他相关服务能够同步更新,大大提高了开发效率和系统的一致性。
+简而言之,Gez为大型项目的模块整合提供了便捷、高效的解决方案,使得分散的模块能够迅速聚合,共同构建出稳定、可靠的大型应用系统。
+在构建大型软件项目时,我们遵循以下核心理念,以确保系统的稳定性、可维护性和高效性:
+我们倡导设计一个集中的基础服务,该服务将作为所有第三方依赖的单一来源。这种集中化的管理方式能够简化依赖关系,降低系统的复杂性。
+基础服务不仅提供第三方依赖,还负责这些依赖的统一维护和更新。通过集中管理依赖的生命周期,我们可以确保整个系统中使用的依赖版本是一致的,从而避免版本冲突和不一致性问题。
+当基础服务中的第三方依赖发生更新时,我们采用“一次发布,所有业务系统生效”的策略。这意味着一旦基础服务发布了新的依赖版本,所有依赖该服务的业务系统都将立即使用这些更新,无需在每个业务系统中单独进行更新操作。这种机制大大提高了系统的更新效率和一致性。
+我们鼓励将业务服务与第三方依赖进行解耦。业务服务应专注于构建和处理业务逻辑,而不直接管理第三方依赖。所有对第三方依赖的引用都应通过基础服务进行间接访问。这种设计使得业务服务更加轻量级、灵活,且易于维护和扩展。
+综上所述,我们的设计理念旨在通过基础服务的集中化管理和统一维护,简化大型项目中的依赖关系,提高系统的整体稳定性和开发效率。同时,通过解耦业务服务与第三方依赖,我们为系统的未来扩展和维护奠定了坚实的基础。
+所有的主流浏览器都已经支持,针对一些低版本的浏览器,可以提供一个升级的页面来引导用户升级它的浏览器。
+从 v1.0
、v2.0
到现在的 v3.0
,已经走过了将近 5 年的时光,支持起了公司内部数十个业务的项目,并且不断地推动业务项目的升级。
>16&255,c[h++]=i>>8&255,c[h++]=255&i;return 2===l&&(i=n[t.charCodeAt(s)]<<2|n[t.charCodeAt(s+1)]>>4,c[h++]=255&i),1===l&&(i=n[t.charCodeAt(s)]<<10|n[t.charCodeAt(s+1)]<<4|n[t.charCodeAt(s+2)]>>2,c[h++]=i>>8&255,c[h++]=255&i),c},e.fromByteArray=function(t){for(var e,n=t.length,o=n%3,i=[],s=0,a=n-o;s>18&63]+r[o>>12&63]+r[o>>6&63]+r[63&o])}return s.join("")}(t,s,s+16383>a?a:s+16383));return 1===o?(e=t[n-1],i.push(r[e>>2]+r[e<<4&63]+"==")):2===o&&(e=(t[n-2]<<8)+t[n-1],i.push(r[e>>10]+r[e>>4&63]+r[e<<2&63]+"=")),i.join("")};for(var r=[],n=[],o="undefined"!=typeof Uint8Array?Uint8Array:Array,i="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",s=0,a=i.length;s0)throw Error("Invalid string. Length must be a multiple of 4");var r=t.indexOf("=");-1===r&&(r=e);var n=r===e?0:4-r%4;return[r,n]}n["-".charCodeAt(0)]=62,n["_".charCodeAt(0)]=63},313:function(t,e,r){let n=r(446),o=r(164),i="function"==typeof Symbol&&"function"==typeof Symbol.for?Symbol.for("nodejs.util.inspect.custom"):null;e.Buffer=a,e.INSPECT_MAX_BYTES=50;a.TYPED_ARRAY_SUPPORT=function(){try{let t=new Uint8Array(1),e={foo:function(){return 42}};return Object.setPrototypeOf(e,Uint8Array.prototype),Object.setPrototypeOf(t,e),42===t.foo()}catch(t){return!1}}(),!a.TYPED_ARRAY_SUPPORT&&"undefined"!=typeof console&&"function"==typeof console.error&&console.error("This browser lacks typed array (Uint8Array) support which is required by `buffer` v5.x. Use `buffer` v4.x if you require old browser support.");function s(t){if(t>0x7fffffff)throw RangeError('The value "'+t+'" is invalid for option "size"');let e=new Uint8Array(t);return Object.setPrototypeOf(e,a.prototype),e}function a(t,e,r){if("number"==typeof t){if("string"==typeof e)throw TypeError('The "string" argument must be of type string. Received type number');return l(t)}return f(t,e,r)}function f(t,e,r){if("string"==typeof t)return function(t,e){if(("string"!=typeof e||""===e)&&(e="utf8"),!a.isEncoding(e))throw TypeError("Unknown encoding: "+e);let r=0|d(t,e),n=s(r),o=n.write(t,e);return o!==r&&(n=n.slice(0,o)),n}(t,e);if(ArrayBuffer.isView(t))return function(t){if(F(t,Uint8Array)){let e=new Uint8Array(t);return h(e.buffer,e.byteOffset,e.byteLength)}return c(t)}(t);if(null==t)throw TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof t);if(F(t,ArrayBuffer)||t&&F(t.buffer,ArrayBuffer)||"undefined"!=typeof SharedArrayBuffer&&(F(t,SharedArrayBuffer)||t&&F(t.buffer,SharedArrayBuffer)))return h(t,e,r);if("number"==typeof t)throw TypeError('The "value" argument must not be of type number. Received type number');let n=t.valueOf&&t.valueOf();if(null!=n&&n!==t)return a.from(n,e,r);let o=function(t){if(a.isBuffer(t)){let e=0|p(t.length),r=s(e);return 0===r.length?r:(t.copy(r,0,0,e),r)}if(void 0!==t.length)return"number"!=typeof t.length||function(t){return t!=t}(t.length)?s(0):c(t);if("Buffer"===t.type&&Array.isArray(t.data))return c(t.data)}(t);if(o)return o;if("undefined"!=typeof Symbol&&null!=Symbol.toPrimitive&&"function"==typeof t[Symbol.toPrimitive])return a.from(t[Symbol.toPrimitive]("string"),e,r);throw TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof t)}function u(t){if("number"!=typeof t)throw TypeError('"size" argument must be of type number');if(t<0)throw RangeError('The value "'+t+'" is invalid for option "size"')}Object.defineProperty(a.prototype,"parent",{enumerable:!0,get:function(){if(a.isBuffer(this))return this.buffer}}),Object.defineProperty(a.prototype,"offset",{enumerable:!0,get:function(){if(a.isBuffer(this))return this.byteOffset}}),a.poolSize=8192,a.from=function(t,e,r){return f(t,e,r)},Object.setPrototypeOf(a.prototype,Uint8Array.prototype),Object.setPrototypeOf(a,Uint8Array);function l(t){return u(t),s(t<0?0:0|p(t))}a.alloc=function(t,e,r){var n,o,i;return n=t,o=e,i=r,(u(n),n<=0)?s(n):void 0!==o?"string"==typeof i?s(n).fill(o,i):s(n).fill(o):s(n)},a.allocUnsafe=function(t){return l(t)},a.allocUnsafeSlow=function(t){return l(t)};function c(t){let e=t.length<0?0:0|p(t.length),r=s(e);for(let n=0;n 你好,世界! 你好,世界!{!r&&"object"==typeof e&&(r=o.bind(null,e));let t=u(e,r);(!n||nGez
+
+ Gez
+
+ 计数器
+ 请求地址
+ ${t}
+ 图片
+
+
+`)}onClient(){setInterval(()=>{this.state.count++;let t=document.querySelector("#count");t instanceof HTMLDivElement&&(t.innerText=String(this.state.count))},1e3)}async onServer(){this.importMetaSet.add(import.meta),super.onServer(),this.state.count=1}constructor(...t){super(...t),h(this,"state",{count:0}),h(this,"title",c.title.home)}}}};
\ No newline at end of file
diff --git a/ssr-html/chunks/473.2f582836.final.js b/ssr-html/chunks/473.2f582836.final.js
new file mode 100644
index 00000000..fee3ed53
--- /dev/null
+++ b/ssr-html/chunks/473.2f582836.final.js
@@ -0,0 +1,12 @@
+export const ids=["473"];export const modules={878:function(t,e,r){function n(t){return`
+
+
+
+
+
+
+
+
+
+
+
Gez
+
+ Gez
+
+ Not Found
")}async onServer(){this.importMetaSet.add(import.meta),super.onServer()}constructor(...t){var e,r,n;super(...t),e=this,r="title",n=i.title.notFound,r in e?Object.defineProperty(e,r,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[r]=n}}}};
\ No newline at end of file
diff --git a/ssr-html/chunks/830.584a2be3.final.css b/ssr-html/chunks/830.584a2be3.final.css
new file mode 100644
index 00000000..99934719
--- /dev/null
+++ b/ssr-html/chunks/830.584a2be3.final.css
@@ -0,0 +1 @@
+.layout{padding:10px}head{--webpack-__ssr_html__-830:&_992}
\ No newline at end of file
diff --git a/ssr-html/images/cat.ed79ef6b.final.jpeg b/ssr-html/images/cat.ed79ef6b.final.jpeg
new file mode 100644
index 00000000..29e4dab9
Binary files /dev/null and b/ssr-html/images/cat.ed79ef6b.final.jpeg differ
diff --git a/ssr-html/images/loading.6e6b1b2e.final.gif b/ssr-html/images/loading.6e6b1b2e.final.gif
new file mode 100644
index 00000000..0613ccb0
Binary files /dev/null and b/ssr-html/images/loading.6e6b1b2e.final.gif differ
diff --git a/ssr-html/images/logo.310683d2.final.svg b/ssr-html/images/logo.310683d2.final.svg
new file mode 100644
index 00000000..fef098a6
--- /dev/null
+++ b/ssr-html/images/logo.310683d2.final.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/ssr-html/images/starry.d914a632.final.jpg b/ssr-html/images/starry.d914a632.final.jpg
new file mode 100644
index 00000000..1d0c3b0a
Binary files /dev/null and b/ssr-html/images/starry.d914a632.final.jpg differ
diff --git a/ssr-html/images/sun.429a7bc5.final.png b/ssr-html/images/sun.429a7bc5.final.png
new file mode 100644
index 00000000..fedb581d
Binary files /dev/null and b/ssr-html/images/sun.429a7bc5.final.png differ
diff --git a/ssr-html/importmap.2490f8b7f6308ae1.final.js b/ssr-html/importmap.2490f8b7f6308ae1.final.js
new file mode 100644
index 00000000..f80c84f1
--- /dev/null
+++ b/ssr-html/importmap.2490f8b7f6308ae1.final.js
@@ -0,0 +1 @@
+(t=>{let r="ssr-html",s="__importmap__",e=t[s]=t[s]||{},i=e.imports=e.imports||{},c=new URL(document.currentScript.src).pathname.split("/"+r+"/"),n=t=>r+t.substring(1);Object.entries({"./src/entry.client":"./src/entry.client.f8425e4a.final.js","./src/title":"./src/title.23ced5f2.final.js"}).forEach(([t,r])=>{i[n(t)]=c[0]+"/"+n(r)})})(globalThis);
\ No newline at end of file
diff --git a/ssr-html/importmap.js b/ssr-html/importmap.js
new file mode 100644
index 00000000..f80c84f1
--- /dev/null
+++ b/ssr-html/importmap.js
@@ -0,0 +1 @@
+(t=>{let r="ssr-html",s="__importmap__",e=t[s]=t[s]||{},i=e.imports=e.imports||{},c=new URL(document.currentScript.src).pathname.split("/"+r+"/"),n=t=>r+t.substring(1);Object.entries({"./src/entry.client":"./src/entry.client.f8425e4a.final.js","./src/title":"./src/title.23ced5f2.final.js"}).forEach(([t,r])=>{i[n(t)]=c[0]+"/"+n(r)})})(globalThis);
\ No newline at end of file
diff --git a/ssr-html/index.html b/ssr-html/index.html
new file mode 100644
index 00000000..83bbee39
--- /dev/null
+++ b/ssr-html/index.html
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+ Gez
+
+ 计数器
+ 请求地址
+ /
+ 图片
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Preact
0?P(r.type,r.props,r.key,r.ref?r.ref:null,r.__v):r).__=e,r.__b=e.__b+1,o=null,-1!==(u=r.__i=function(e,t,n,_){var r=e.key,o=e.type,l=n-1,u=n+1,i=t[n];if(null===i||i&&r==i.key&&o===i.type&&0==(131072&i.__u))return n;if(_>(null!=i&&0==(131072&i.__u)?1:0))for(;l>=0||uPreact
+ rspack + vue3
Count value: 1