Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/hippy vue next ssr for 3.0 #3539

Merged
merged 11 commits into from
Jan 19, 2024
Merged
108 changes: 106 additions & 2 deletions docs/api/hippy-vue/vue3.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,106 @@ const router: Router = createRouter({
});
```

# 服务端渲染

@hippy/vue-next 现已支持服务端渲染,具体代码可以查看[示例项目](https://github.com/Tencent/Hippy/tree/main/examples/hippy-vue-next-ssr-demo)中的 SSR
部分,关于 Vue SSR 的实现及原理,可以参考[官方文档](https://cn.vuejs.org/guide/scaling-up/ssr.html)。

## 如何使用SSR

请参考[示例项目](https://github.com/Tencent/Hippy/tree/main/examples/hippy-vue-next-ssr-demo)说明文档中的 How To Use SSR

## 实现原理

### SSR 架构图

<img src="assets/img/hippy-vue-next-ssr-arch-cn.png" alt="hippy-vue-next SSR 架构图" width="80%"/>

### 详细说明

@hippy/vue-next SSR 的实现涉及到了编译时,客户端运行时,以及服务端运行时三个运行环境。在 vue-next ssr的基础上,我们开发了 @hippy/vue-next-server-renderer
用于服务端运行时节点的渲染,开发了 @hippy/vue-next-compiler-ssr 用于编译时 vue 模版文件的编译。以及 @hippy/vue-next-style-parser 用于服务端渲染得到的
Native Node List 的样式插入。下面我们通过一个模版的编译和运行时过程来说明 @hippy/vue-next SSR 做了哪些事情

我们有形如`<div id="test" class="test-class"></div>`的一段模版

- 编译时

模版经过 @hippy/vue-next-compiler-ssr 的处理,得到了形如

```javascript
_push(`{"id":${ssrGetUniqueId()},"index":0,"name":"View","tagName":"div","props":{"class":"test-class","id": "test",},"children":[]},`)
```

的 render function

- 服务端运行时

在服务端运行时,编译时得到的 render function 执行后得到了对应节点的 json object。注意 render function 中的
ssrGetUniqueId 方法,是在 @hippy/vue-next-server-renderer 中提供的,在这里 server-renderer 还会对
节点的属性值等进行处理,最后得到 Native Node 的 json object

```javascript
{ "id":1,"index":0,"name":"View","tagName":"div","props":{"class":"test-class","id": "test",},"children":[] }
```

> 对于手写的非 sfc 模版的渲染函数,在 compiler 中无法处理,也是在 server-renderer 中执行的

- 客户端运行时

在客户端运行时,通过 @hippy/vue-next-style-parser,给服务端返回的节点插入样式,并直接调用 hippy native 提供的
native API,将返回的 Native Node 对象作为参数传入,并完成节点的渲染上屏。 完成节点上屏之后,再通过系统提供的
global.dynamicLoad 异步加载客户端异步版 jsBundle,完成客户端 Hydrate 并执行后续流程。

## 初始化差异

SSR 版本的 Demo 初始化与异步版的初始化有一些差异部分,这里对其中的差异部分做一个详细的说明

- src/main-native.ts 变更

1. 使用 createSSRApp 替换之前的 createApp,createApp 仅支持 CSR 渲染,而 createSSRApp 同时支持 CSR 和 SSR
2. 在初始化时候新增了 ssrNodeList 参数,作为 Hydrate 的初始化节点列表。这里我们服务端返回的初始化节点列表保存在了 global.hippySSRNodes 中,并将其作为参数在createSSRApp时传入
3. 将 app.mount 放到 router.isReady 完成后调用,因为如果不等待路由完成,会与服务端渲染的节点有所不同,导致 Hydrate 时报错

```javascript
- import { createApp } from '@hippy/vue-next';
+ import { createSSRApp } from '@hippy/vue-next';
- const app: HippyApp = createApp(App, {
+ const app: HippyApp = createSSRApp(App, {
// ssr rendered node list, use for hydration
+ ssrNodeList: global.hippySSRNodes,
});
+ router.isReady().then(() => {
+ // mount app
+ app.mount('#root');
+ });
```

- src/main-server.ts 新增

main-server.ts 是在服务端运行的业务 jsBundle,因此不需要做代码分割。整体构建为一个 bundle 即可。其核心功能就是在服务端完成首屏渲染逻辑,并将得到的首屏 Hippy 节点进行处理,插入节点属性和 store(如果存在)后返回,
以及返回当前已生成节点的最大 uniqueId 供客户端后续使用。

>注意,服务端代码是同步执行的,如果有数据请求走了异步方式,可能会出现还没有拿到数据,请求就已经返回了的情况。对于这个问题,Vue SSR 提供了专用 API 来处理这个问题:
>[onServerPrefetch](https://cn.vuejs.org/api/composition-api-lifecycle.html#onserverprefetch)。
>在 [Demo](https://github.com/Tencent/Hippy/blob/main/examples/hippy-vue-next-ssr-demo/src/app.vue) 的 app.vue 中也有 onServerPrefetch 的使用示例

- server.ts 新增

server.ts 是服务端执行的入口文件,其作用是提供 Web Server,接收客户端的 SSR CGI 请求,并将结果作为响应数据返回给客户端,包括了渲染节点列表,store,以及全局的样式列表。

- src/main-client.ts 新增

main-client.ts 是客户端执行的入口文件,与之前纯客户端渲染不同,SSR的客户端入口文件仅包含了获取首屏节点请求、插入首屏节点样式、以及将节点插入终端完成渲染的相关逻辑。

- src/ssr-node-ops.ts 新增

ssr-node-ops.ts 封装了不依赖 @hippy/vue-next 运行时的 SSR 节点的插入,更新,删除等操作逻辑

- src/webpack-plugin.ts 新增

webpack-plugin.ts 封装了 SSR 渲染所需 Hippy App 的初始化逻辑

# 其他差异说明

目前 `@hippy/vue-next` 与 `@hippy/vue` 功能上基本对齐,不过在 API 方面与 @hippy/vue 有一些区别,以及还有一些问题还没有解决,这里做些说明:
Expand Down Expand Up @@ -266,7 +366,7 @@ const router: Router = createRouter({
}
```

更多信息可以参考 demo 里的 [extend.ts](https://github.com/Tencent/Hippy/blob/master/examples/hippy-vue-next-demo/src/extend.ts).
更多信息可以参考 demo 里的 [extend.ts](https://github.com/Tencent/Hippy/blob/main/examples/hippy-vue-next-demo/src/extend.ts).

- whitespace 处理

Expand Down Expand Up @@ -305,6 +405,10 @@ const router: Router = createRouter({

`<dialog>` 组件的第一个子元素不能设置 `{ position: absolute }` 样式,如果想将 `<dialog>` 内容铺满全屏,可以给第一个子元素设置 `{ flex: 1 }` 样式或者显式设置 width 和 height 数值。这与 Hippy3.0 的逻辑保持一致。

- 书写 SSR 友好的代码

因 SSR 的渲染方式和生命周期等与客户端渲染方式有一些差异,因此需要在代码编写过程中注意,这里可以参考[Vue官方的SSR指引](https://cn.vuejs.org/guide/scaling-up/ssr.html#writing-ssr-friendly-code)

# 示例

更多使用请参考 [示例项目](https://github.com/Tencent/Hippy/tree/master/examples/hippy-vue-next-demo).
更多使用请参考 [示例项目](https://github.com/Tencent/Hippy/tree/main/examples/hippy-vue-next-demo).
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions driver/js/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ module.exports = {
'@typescript-eslint/consistent-type-assertions': 'off',
'@typescript-eslint/naming-convention': 'off',
'@typescript-eslint/prefer-for-of': 'off',
'@typescript-eslint/no-require-imports': 'off',
},
parserOptions: {
project: ['./**/tsconfig.json'],
Expand Down Expand Up @@ -171,6 +172,8 @@ module.exports = {
['sfc', resolveVue('sfc')],
['he', path.resolve(__dirname, './packages/hippy-vue/src/util/entity-decoder')],
['@hippy-vue-next-style-parser', resolvePackage('hippy-vue-next-style-parser')],
['@hippy-vue-next', resolvePackage('hippy-vue-next')],
['@hippy-vue-next-server-renderer', resolvePackage('hippy-vue-next-server-renderer')],
],
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const app: HippyApp = createApp(App, {
backgroundColor: 4283416717,

// 状态栏背景图,要注意这个会根据容器尺寸拉伸。
// background image of status bar, scale with wrapper size
// backgroundImage: 'https://user-images.githubusercontent.com/12878546/148737148-d0b227cb-69c8-4b21-bf92-739fb0c3f3aa.png',
},
},
Expand Down
21 changes: 21 additions & 0 deletions driver/js/examples/hippy-vue-next-ssr-demo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.DS_Store
node_modules
/dist

# local env files
.env.local
.env.*.local

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
1 change: 1 addition & 0 deletions driver/js/examples/hippy-vue-next-ssr-demo/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
46 changes: 46 additions & 0 deletions driver/js/examples/hippy-vue-next-ssr-demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# @hippy/vue-next demo


### Introduction
This package is the demo project for @hippy/vue-next. Project include most use case for
@hippy/vue-next. Just try it.

### Usage
Read the hippy framework [doc](https://github.com/Tencent/Hippy/blob/master/README.md#-getting-started) and learn
how to use.

### How To Use SSR

we were support SSR for @hippy/vue-next. here is only how to use SSR. how to use vue-next doc is [here](https://hippyjs.org/en-us/#/hippy-vue/vue3)

1. Before running vue-next-ssr-demo, you should run `npm run init` at root directory to install dependencies and build front-end sdk packages.
2. Then run `cd examples/hippy-vue-next-ssr-demo` and `npm install --legacy-peer-deps` to install demo dependencies.

Now determine which environment you want build

> Because our server listening port 8080, so if you are using android device, you should run `adb reverse tcp:8080 tcp:8080`
> to forward mobile device port to pc port, iOS simulator doesn't need this step.

ensure you were at `examples/hippy-vue-next-ssr-demo`.

#### Development

1. run `npm run ssr:dev-build` to build client entry & client bundle, then running hippy debug server
2. run `npm run ssr:dev-server` to build server bundle and start SSR web server to listen port **8080**.
3. debug your app with [reference](https://hippyjs.org/en-us/#/guide/debug)
> You can change server listen port 8080 in `server.ts` by your self, but you also need change request port 8080 in
> `src/main-client.ts` and modify the adb reverse port, ensure port is same at three place

#### Production

1. run `npm run ssr:prod-build` to build client entry, server bundle, client bundle
2. run `npm run ssr:prod-server` to start SSR web server to listen port **8080**.
3. test your app
> In production, you can use process manage tool to manage your NodeJs process, like pm2.
>
> And you should deploy you web server at real server with real domain, then you can request
> SSR cgi like https://xxx.com/getSsrFirstScreenData
>

#### Tips
> Usage of non SSR is [here](https://hippyjs.org/en-us/#/guide/integration)
10 changes: 10 additions & 0 deletions driver/js/examples/hippy-vue-next-ssr-demo/app.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
declare module '*.jpg';
declare module '*.png';
declare module '*.vue' {
import { defineComponent } from 'vue';
const Component: ReturnType<typeof defineComponent>;
export default Component;
}

type NeedToTyped = any;

83 changes: 83 additions & 0 deletions driver/js/examples/hippy-vue-next-ssr-demo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
{
"name": "hippy-vue-next-demo",
"version": "3.0.0",
"description": "A SSR Demo Example For Hippy-Vue-Next Library To Show.",
"private": true,
"webMain": "./src/main-web.ts",
"nativeMain": "./src/main-native.ts",
"serverMain": "./src/main-server.ts",
"serverEntry": "./server.ts",
"ssrMain": "./src/main.ts",
"repository": "https://github.com/Tencent/Hippy/tree/master/examples/hippy-vue-next-demo",
"license": "Apache-2.0",
"author": "OpenHippy Team",
"scripts": {
"hippy:dev": "node ./scripts/env-polyfill.js hippy-dev -c ./scripts/hippy-webpack.dev.js",
"web:dev": "npm run hippy:dev & node ./scripts/env-polyfill.js webpack serve --config ./scripts/hippy-webpack.web-renderer.dev.js",
"web:build": "node ./scripts/env-polyfill.js webpack --config ./scripts/hippy-webpack.web-renderer.js",
"ssr:dev-client": "node ./scripts/env-polyfill.js hippy-dev -c ./scripts/webpack-ssr-config/client.dev.js",
"ssr:dev-server": "node ./scripts/env-polyfill.js && node ./scripts/webpack.ssr.dev.js",
"ssr:prod-build": "node ./scripts/webpack.ssr.build.js",
"ssr:prod-server": "node ./dist/server/index.js --mode production"
},
"dependencies": {
"@hippy/vue-router-next-history": "latest",
"@hippy/web-renderer": "latest",
"@hippy/vue-next": "latest",
"@hippy/vue-next-server-renderer": "file:../../packages/hippy-vue-next-server-renderer",
"@hippy/vue-next-style-parser": "file:../../packages/hippy-vue-next-style-parser",
"@vue/runtime-core": "^3.2.46",
"@vue/shared": "^3.2.46",
"core-js": "^3.20.2",
"vue": "^3.2.46",
"vue-router": "^4.0.12",
"express": "^4.18.2",
"pinia": "2.0.30"
},
"devDependencies": {
"@babel/core": "^7.12.0",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-decorators": "^7.10.5",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4",
"@babel/plugin-proposal-object-rest-spread": "^7.5.5",
"@babel/plugin-proposal-optional-chaining": "^7.10.4",
"@babel/plugin-transform-async-to-generator": "^7.5.0",
"@babel/plugin-transform-runtime": "^7.11.0",
"@babel/polyfill": "^7.12.0",
"@babel/preset-env": "^7.12.0",
"@babel/runtime": "^7.16.0",
"@hippy/debug-server-next": "latest",
"@hippy/hippy-dynamic-import-plugin": "^2.0.0",
"@hippy/hippy-hmr-plugin": "^0.1.0",
"@hippy/rejection-tracking-polyfill": "^1.0.0",
"@hippy/vue-css-loader": "^2.0.1",
"@vitejs/plugin-vue": "^1.9.4",
"@hippy/vue-next-compiler-ssr": "file:../../packages/hippy-vue-next-compiler-ssr",
"@types/shelljs": "^0.8.5",
"@vue/cli-service": "^4.5.19",
"@vue/compiler-sfc": "^3.2.46",
"babel-loader": "^8.1.0",
"case-sensitive-paths-webpack-plugin": "^2.2.0",
"chokidar": "^3.5.3",
"clean-webpack-plugin": "^4.0.0",
"webpack-manifest-plugin": "^4.1.1",
"cross-env": "^7.0.3",
"cross-env-os": "^7.1.1",
"esbuild": "^0.13.14",
"esbuild-loader": "^2.18.0",
"file-loader": "^4.3.0",
"less": "^4.1.2",
"less-loader": "^7.1.0",
"shelljs": "^0.8.5",
"terser": "^4.8.0",
"ts-loader": "^8.4.0",
"@types/express": "^4.17.17",
"url-loader": "^4.0.0",
"vue-loader": "^17.0.0",
"webpack": "^4.46.0",
"webpack-cli": "^4.7.2"
},
"engines": {
"node": ">=15"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const { exec } = require('shelljs');

const runScript = (scriptStr) => {
console.log(`Full command execute: "${scriptStr}"`);
const result = exec(scriptStr, { stdio: 'inherit' });
if (result.code !== 0) {
console.error(`❌ Execute cmd - "${scriptStr}" error: ${result.stderr}`);
process.exit(1);
}
};

const toNum = (originalNum) => {
const num = `${originalNum}`;
const versionList = num.split('.');
const currentSplitLength = versionList.length;
if (currentSplitLength !== 4) {
let index = currentSplitLength;
while (index < 4) {
versionList.push('0');
index += 1;
}
}
const r = ['0000', '000', '00', '0', ''];
for (let i = 0; i < versionList.length; i += 1) {
let len = versionList[i].length;
if (len > 4) {
len = 4;
versionList[i] = versionList[i].slice(0, 4);
}
versionList[i] = r[len] + versionList[i];
}
return versionList.join('');
};

const versionCompare = (targetVer, currentVer) => {
if (!targetVer || !currentVer) return 1;
const numA = toNum(currentVer);
const numB = toNum(targetVer);
if (numA === numB) {
return 0;
}
return numA < numB ? -1 : 1;
};

const LEGACY_OPENSSL_VERSION = '3.0.0';
const scriptString = process.argv.slice(2).join(' ');
let envPrefixStr = '';

console.log(`Start to execute cmd: "${scriptString}"`);
console.log(`Current openssl version: ${process.versions.openssl}`);

const result = /^(\d+\.\d+\.\d+).*$/.exec(process.versions.openssl.toString().trim());
if (result && result[1]) {
const currentVersion = result[1];
const compareResult = versionCompare(LEGACY_OPENSSL_VERSION, currentVersion);
if (compareResult >= 0) {
envPrefixStr += 'NODE_OPTIONS=--openssl-legacy-provider';
}
}

runScript(`${envPrefixStr} ${scriptString}`); // start to execute cmd
Loading
Loading