起步
在 React 框架上通过 ClassComponent + HOC 模式集成 Five 开发,完整源码示例:JavaScript | TypeScript。
- 准备开发环境。
- 如何引入 Five SDK。
- 在屏幕中渲染一个三维空间。
准备工作
开发环境
- 你需要准备一个现代浏览器。
Five 的浏览器支持如下,请自行选择一个你熟悉的:
Safari | Safari on iOS | Chrome | Chrome for Android | Edge | Firefox |
---|---|---|---|---|---|
>= 9 | >= 9 | >= 49 | >= 93 | >= 13 | >= 45 |
- 你需要安装 Node.js,并且
Node.js
版本尽量 >= 12.x,以规避历史遗留的兼容问题。
使用开发构建工具
本示例通过 Vite 初始化开发环境。你可以通过下方代码自行初始化。
- JavaScript
- TypeScript
# npm 6.x
npm create vite@latest my-vue-app --template react
# npm 7+, extra double-dash is needed:
npm create vite@latest my-vue-app -- --template react
# npm 6.x
npm create vite@latest my-vue-app --template react-ts
# npm 7+, extra double-dash is needed:
npm create vite@latest my-vue-app -- --template react-ts
在 src
下建立本次教程的目录 src/0.getting-started
。
每一个教程均会创建一个新目录作为记录,便于总结和查找。当课程结束你会得到:
src
├── 0.getting-started
├── 1.displaying-work
├── 2.knowing-state
...
这样的目录结构。完整代码样例也是这样的目录结构,你可以随时参考。
如果你熟悉其他的开发构建工具如 Webpack
Snowpack
parcel
你也可以使用他们。
创建 HTML 文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>起步 | Getting started</title>
<!-- highlight-start -->
<style>
* { margin: 0; padding: 0; }
html, body, #app { width: 100%; height: 100%; overflow: hidden; }
</style>
<!-- highlight-end -->
</head>
<body>
<!-- highlight-start -->
<div id="app"></div>
<script type="module" src="./index"></script>
<!-- highlight-end -->
</body>
</html>
使用直接引入 <script type="module" src="./index"></script>
是 Vite 的特征,
如果你使用其他开发构建工具请自行处理代码引入和入口文件。 比如 webpack 下使用 HtmlWebpackPlugin 等。
书写测试逻辑代码
我们先创建一个简单的
Hello World
来确保整体代码能够跑通。
- JavaScript
- TypeScript
const app = document.querySelector("#app");
// highlight-start
app.innerHTML = "Hello World.";
// highlight-end
export {};
const app = document.querySelector("#app")!;
// highlight-start
app.innerHTML = "Hello World.";
// highlight-end
export {};
最后的 export {};
是因为 Vite 使用 type="module"
引入的关系,每个文件都需要是一个 module
, 需要 export
。
如果你使用其他开发构建工具,请按照自己的开发构建工具的要求编写。
启动服务 npm run dev
。 并跳转到当前页面 "http://localhost:3000/src/0.getting-started/index.html"。
请查看你的控制台,端口号会因为你的配置以及当前端口占用情况变更,请已控制台输出的为准。 如果你使用其他开发构建工具,请按照自己的开发构建工具的要求启动服务。
然后你会在页面看到
Hello World.
这样的输出,说明开发构建工具的已经完成。
后面几个章节将不再详细叙述上述步骤,请一并完成即可。
从 npm
安装依赖包
在你的工程目录安装依赖
- JavaScript
- TypeScript
npm install @realsee/five three@0.117 react react-dom @wordpress/compose
依赖需要
- @realsee/five Five
- three three.js 是 Five 的依赖图形/数学库。 目前请使用 0.117.x 的版本
- react React 框架
- react-dom React 的浏览器端渲染器
- @wordpress/compose React HOC 的 HOC 包装器
npm install @realsee/five three@0.117 react react-dom @types/react @types/react-dom @wordpress/compose
依赖需要
- @realsee/five Five
- three three.js 是 Five 的依赖图形/数学库。 目前请使用 0.117.x 的版本
- react React 框架
- react-dom React 的浏览器端渲染器
- @types/react React 框架的 typescript 类型声明
- @types/react-dom React 的浏览器端渲染器的 typescript 类型声明
- @wordpress/compose React HOC 的 HOC 包装器
渲染一个三维空间
是时候渲染一个VR场景看看了。
加载三维空间
删除你之前的 Hello World
代码。我们来重新编写。
你可以先不清楚接下来编写的代码的含义,你在下一章会学习到。
- JavaScript
- TypeScript
import React, { Component, ComponentClass } from "react";
import ReactDOM from "react-dom";
import { Work, parseWork } from "@realsee/five";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { compose } from "@wordpress/compose";
/** work.json 的数据 URL */
const workURL = "https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
/**
* React HOC 获取 work
* @param url work.json 的地址
*/
function withFetchWork(url) {
return function(Compnent) {
return class extends Component {
state = { work: null };
componentDidMount() {
fetch(url).then(res => res.json()).then(json => {
this.setState({ work: parseWork(json) });
});
}
render() {
if (this.state.work === null) return null;
return <Compnent work={this.state.work} {...this.props}/>;
}
}
}
}
const FiveProvider = createFiveProvider();
const App = compose(
withFetchWork(workURL)
)(class extends Component {
render() {
const { work } = this.props;
return <FiveProvider initialWork={work}>
<FiveCanvas width={512} height={512}/>
</FiveProvider>;
}
});
ReactDOM.render(<App/>, document.querySelector("#app"));
export {};
import React, { Component, ComponentClass } from "react";
import ReactDOM from "react-dom";
import { Work, parseWork } from "@realsee/five";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { compose } from "@wordpress/compose";
/** work.json 的数据 URL */
const workURL = "https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
/**
* React HOC 获取 work
* @param url work.json 的地址
*/
function withFetchWork<P extends Record<string, any>>(url: string) {
return function(Compnent: ComponentClass<P & { work: Work }>): ComponentClass<P> {
return class extends Component<P, {work: Work | null}> {
state = { work: null };
componentDidMount() {
fetch(url).then(res => res.json()).then(json => {
this.setState({ work: parseWork(json) });
});
}
render() {
if (this.state.work === null) return null;
return <Compnent work={this.state.work} {...this.props}/>;
}
}
}
}
const FiveProvider = createFiveProvider();
const App = compose(
withFetchWork(workURL)
)(class extends Component<{work: Work}> {
render() {
const { work } = this.props;
return <FiveProvider initialWork={work}>
<FiveCanvas width={512} height={512}/>
</FiveProvider>;
}
})
ReactDOM.render(<App/>, document.querySelector("#app"));
export {};
回到你的浏览器,看看是不是已经有一个三维空间已经展示在左上的位置。 你可以用鼠标或者触摸手势操作画面,基本的浏览功能都已经附带了。
让画面铺满整个屏幕
虽然可能并不能完全理解上述代码的工作原理,但是我们可以看到 FiveCanvas
上有 width
和 height
属性,非常像画面尺寸的样子。并且在浏览器中查看,画面在左上角的位置,也印证了这个猜想。对每次,他们就是用于设置画面尺寸的。
那我们尝试用我们熟悉的 React Hooks 方式,将它铺满屏幕。
- JavaScript
- TypeScript
import React, { Component } from "react";
import ReactDOM from "react-dom";
import { parseWork } from "@realsee/five";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { compose } from "@wordpress/compose";
/** work.json 的数据 URL */
const workURL = "https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
/**
* React HOC 获取 work
* @param url work.json 的地址
*/
function withFetchWork(url) {
return function(Compnent) {
return class extends Component {
state = { work: null };
componentDidMount() {
fetch(url).then(res => res.json()).then(json => {
this.setState({ work: parseWork(json) });
});
}
render() {
if (this.state.work === null) return null;
return <Compnent work={this.state.work} {...this.props}/>;
}
}
}
}
/**
* React HOC: 获取当前窗口的尺寸
*/
function withWindowDimensions() {
return function(Compnent) {
return class extends Component {
state = this.getWindowDimensions();
resizeListener = () => {
this.setState(this.getWindowDimensions());
};
getWindowDimensions() {
return { width: window.innerWidth, height: window.innerHeight };
}
componentDidMount() {
window.addEventListener("resize", this.resizeListener, false);
}
componentWillUnmount() {
window.removeEventListener("resize", this.resizeListener, false);
}
render() {
const dimensions = { width: this.state.width, height: this.state.height };
return <Compnent windowDimensions={dimensions} {...this.props}/>;
}
}
}
}
const FiveProvider = createFiveProvider();
const App = compose(
withFetchWork(workURL),
withWindowDimensions()
)(
class extends Component {
render() {
const { work, windowDimensions } = this.props;
return <FiveProvider initialWork={work}>
<FiveCanvas width={windowDimensions.width} height={windowDimensions.height}/>
</FiveProvider>;
}
})
ReactDOM.render(<App/>, document.querySelector("#app"));
export {};
import React, { Component, ComponentClass } from "react";
import ReactDOM from "react-dom";
import { Work, parseWork } from "@realsee/five";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { compose } from "@wordpress/compose";
/** work.json 的数据 URL */
const workURL = "https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
/**
* React HOC 获取 work
* @param url work.json 的地址
*/
function withFetchWork<P extends Record<string, any>>(url: string) {
return function(Compnent: ComponentClass<P & { work: Work }>): ComponentClass<P> {
return class extends Component<P, {work: Work | null}> {
state = { work: null };
componentDidMount() {
fetch(url).then(res => res.json()).then(json => {
this.setState({ work: parseWork(json) });
});
}
render() {
if (this.state.work === null) return null;
return <Compnent work={this.state.work} {...this.props}/>;
}
}
}
}
/**
* React HOC: 获取当前窗口的尺寸
*/
function withWindowDimensions<P extends Record<string, any>>() {
return function(Compnent: ComponentClass<P & { windowDimensions: { width: number, height: number} }>): ComponentClass<P> {
return class extends Component<P, {width: number, height: number}> {
state = this.getWindowDimensions();
resizeListener = () => {
this.setState(this.getWindowDimensions());
};
getWindowDimensions() {
return { width: window.innerWidth, height: window.innerHeight };
}
componentDidMount() {
window.addEventListener("resize", this.resizeListener, false);
}
componentWillUnmount() {
window.removeEventListener("resize", this.resizeListener, false);
}
render() {
const dimensions = { width: this.state.width, height: this.state.height };
return <Compnent windowDimensions={dimensions} {...this.props}/>;
}
}
}
}
const FiveProvider = createFiveProvider();
const App = compose(
withFetchWork(workURL),
withWindowDimensions()
)(class extends Component<{work: Work, windowDimensions: { width: number, height: number }}> {
render() {
const { work, windowDimensions } = this.props;
return <FiveProvider initialWork={work}>
<FiveCanvas width={windowDimensions.width} height={windowDimensions.height}/>
</FiveProvider>;
}
})
ReactDOM.render(<App/>, document.querySelector("#app"));
export {};
回到你的浏览器,看看是不是符合预期。
你真棒 🥳 !
代码整理拆分
目前我们所有的代码都写在的了一个 js / ts 文件里。虽然一眼就能看到所有的逻辑,但是比较杂乱。我们将内容拆分到多个文件将会改善这一点。
- 将
App
拆分到单独文件。 - 将
withFetchWork
方法拆分到单独文件。 - 将
withWindowDimensions
方法拆分到单独文件。
- JavaScript
- TypeScript
src/0.getting-started/withFetchWork.jsx
import React, { Component } from "react";
import { parseWork } from "@realsee/five";
/**
* React HOC 获取 work
* @param url work.json 的地址
*/
function withFetchWork(url) {
return function(Compnent) {
return class extends Component {
state = { work: null };
componentDidMount() {
fetch(url).then(res => res.json()).then(json => {
this.setState({ work: parseWork(json) });
});
}
render() {
if (this.state.work === null) return null;
return <Compnent work={this.state.work} {...this.props}/>;
}
}
}
}
export { withFetchWork };
src/0.getting-started/withWindowDimensions.jsx
import React, { Component } from "react";
/**
* React HOC: 获取当前窗口的尺寸
*/
function withWindowDimensions() {
return function(Compnent) {
return class extends Component {
state = this.getWindowDimensions();
resizeListener = () => {
this.setState(this.getWindowDimensions());
};
getWindowDimensions() {
return { width: window.innerWidth, height: window.innerHeight };
}
componentDidMount() {
window.addEventListener("resize", this.resizeListener, false);
}
componentWillUnmount() {
window.removeEventListener("resize", this.resizeListener, false);
}
render() {
const dimensions = { width: this.state.width, height: this.state.height };
return <Compnent windowDimensions={dimensions} {...this.props}/>;
}
}
}
}
export { withWindowDimensions };
src/0.getting-started/App.jsx
import React, { Component } from "react";
import { compose } from "@wordpress/compose";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { withFetchWork } from "./withFetchWork";
import { withWindowDimensions } from "./withWindowDimensions";
/** work.json 的数据 URL */
const workURL = "https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
const FiveProvider = createFiveProvider();
const App = compose(
withFetchWork(workURL),
withWindowDimensions()
)(class extends Component {
render() {
const { work, windowDimensions } = this.props;
return <FiveProvider initialWork={work}>
<FiveCanvas width={windowDimensions.width} height={windowDimensions.height}/>
</FiveProvider>;
}
});
export { App };
src/0.getting-started/index.jsx
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";
ReactDOM.render(<App/>, document.querySelector("#app"));
export {};
src/0.getting-started/withFetchWork.tsx
import React, { Component, ComponentClass } from "react";
import { Work, parseWork } from "@realsee/five";
/**
* React HOC 获取 work
* @param url work.json 的地址
*/
function withFetchWork<P extends Record<string, any>>(url: string) {
return function(Compnent: ComponentClass<P & { work: Work }>): ComponentClass<P> {
return class extends Component<P, {work: Work | null}> {
state = { work: null };
componentDidMount() {
fetch(url).then(res => res.json()).then(json => {
this.setState({ work: parseWork(json) });
});
}
render() {
if (this.state.work === null) return null;
return <Compnent work={this.state.work} {...this.props}/>;
}
}
}
}
export { withFetchWork };
src/0.getting-started/withWindowDimensions.tsx
import React, { Component, ComponentClass } from "react";
/**
* React HOC: 获取当前窗口的尺寸
*/
function withWindowDimensions<P extends Record<string, any>>() {
return function(Compnent: ComponentClass<P & { windowDimensions: { width: number, height: number} }>): ComponentClass<P> {
return class extends Component<P, {width: number, height: number}> {
state = this.getWindowDimensions();
resizeListener = () => {
this.setState(this.getWindowDimensions());
};
getWindowDimensions() {
return { width: window.innerWidth, height: window.innerHeight };
}
componentDidMount() {
window.addEventListener("resize", this.resizeListener, false);
}
componentWillUnmount() {
window.removeEventListener("resize", this.resizeListener, false);
}
render() {
const dimensions = { width: this.state.width, height: this.state.height };
return <Compnent windowDimensions={dimensions} {...this.props}/>;
}
}
}
}
export { withWindowDimensions };
src/0.getting-started/App.tsx
import React, { Component } from "react";
import { Work } from "@realsee/five";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { compose } from "@wordpress/compose";
import { withFetchWork } from "./withFetchWork";
import { withWindowDimensions } from "./withWindowDimensions";
/** work.json 的数据 URL */
const workURL = "https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
const FiveProvider = createFiveProvider();
const App = compose(
withFetchWork(workURL),
withWindowDimensions()
)(class extends Component<{work: Work, windowDimensions: { width: number, height: number }}> {
render() {
const { work, windowDimensions } = this.props;
return <FiveProvider initialWork={work}>
<FiveCanvas width={windowDimensions.width} height={windowDimensions.height}/>
</FiveProvider>;
}
});
export { App };
src/0.getting-started/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";
ReactDOM.render(<App/>, document.querySelector("#app"));
export {};
是不是舒服多了。每个文件都非常简洁,并且很容易看明白他们分别的作用。
下一章节你会学到
- 了解什么是 Work。
- 了解刚才的代码,比如
FiveProvider
/FiveCanvas
组件是如何工作的。