Skip to content

Commit

Permalink
feature(vue-next): hippy vue next ssr for 3.0 (#3539)
Browse files Browse the repository at this point in the history
* feat(vue-next): commit ssr init version

* feat(vue-next): remove unused method & fix ts type issue

* feat(vue-next): optimize build file types & update doc

* feat(vue-next): ssr support commit for library & demo

* fix(vue-next): fix eslint issue

* fix(vue-next): compatible hippy 3.x node operate method & ui

* fix(vue-next): remove unused code

* fix(vue-next): fix ssr demo issue

* fix(vue-next): fix div demo issue & native background android issue

* docs(vue-next): add new vue-next ssr docs
  • Loading branch information
gguoyu authored Jan 19, 2024
1 parent 9e150d6 commit df6c5bf
Show file tree
Hide file tree
Showing 187 changed files with 17,828 additions and 583 deletions.
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).
Binary file added docs/assets/img/hippy-vue-next-ssr-arch-cn.png
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
1 change: 1 addition & 0 deletions driver/js/examples/hippy-vue-next-demo/src/main-native.ts
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"
}
}
61 changes: 61 additions & 0 deletions driver/js/examples/hippy-vue-next-ssr-demo/scripts/env-polyfill.js
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

0 comments on commit df6c5bf

Please sign in to comment.