仿貝殼app全景看房(React+Mobx+Egg+Three.js入門級全棧實戰專案)
背景
前段時間學習了一下React,感覺學起來還是比較愉快的,遂迫不及待的寫了個專案牛刀小試了一下;學的時候感覺很多東西就是這麼回事,但是真正實踐起來確實也碰到了很多意想不到的問題,但是通過強大的搜尋引擎還是把這些問題解決掉了。實踐出真理,現在就讓我介紹一下這個專案。
介紹
模仿貝殼看房
app,簡單的實現了 app 首頁和我的頁面,使用 three.js 做了一個全景看房模組。
- 技術棧:React+Mobx+Egg+Three.js+Echarts
- 使用 React 編寫前端頁面,Mobx 進行資料的管理, 遵循元件模組化的思想,將可複用的、獨立的功能方法封裝到一個模組當中,使用 Egg 進行後端搭建,同時進行了路由集中式的管理。
- 功能實現:使用 Three.js 簡單實現全景看房的功能
專案思路
首先就是對貝殼看房
app進行分析。進來 app 首頁點選下方的 Tabber 標籤欄就能實現頁面的切換;於是就決定將這五個頁面寫成五個不同的路由,將這些路由進行集中的管理。每個頁面裡面的內容進行元件式的封裝用不同的路由地址展示元件的內容,再引用到該頁面上,大致思路就是這樣。
專案預處理
用Vite
將專案搭建起來之後,在src
資料夾裡面建立components(存放元件)、pages(專案所有的頁面)、routes(路由管理)、store(倉庫資料的管理)以及utils(公共工具包)資料夾。看到貝殼看房
app裡面涉及到很多Icon的使用,故先在components資料夾裡面使用react-vant
的自定義圖示進行Iconfont
圖示的引入處理,為了在引入檔案時方便點在 vite.config.js 裡面配置路徑的別名。
部分程式碼如下: ``` // vite.config.js 配置路徑別名 resolve: { alias: { '@': path.resolve(__dirname, 'src'), 'utils': path.resolve(__dirname, 'src/utils') } },
// src/components/myIcon/index.jsx 引入外部圖示 import { createFromIconfontCN } from '@react-vant/icons' export default createFromIconfontCN('//at.alicdn.com/t/c/font_3847257_b9j1g2f7n06.js')
```
成果
前端
- 元件庫:React-vant
- Icon: Iconfont
- 腳手架:Vite
- 地圖api:百度地圖api
路由配置
這裡我把專案的所有路由都配置到這個routes/index.jsx裡面,這樣集中的配置好路由再將這個路由丟擲使用。 所以先維護出一個路由陣列 routes=[],再配置對應的路由以及路由頁面。 src/routes/index.jsx 部分程式碼:完整程式碼點選這裡
``` import { useRoutes } from 'react-router-dom' import { lazy, Suspense } from 'react' import { Loading } from 'react-vant';
// 頁面元件的懶載入 const Home = lazy(() => import('../pages/Home')) const Meaasge = lazy(() => import('../pages/Message')) const Info = lazy(() => import('../pages/Info'))
.......
const routes = [
{
path: '/',
element:
function RouterList() { let element = useRoutes(routes) // 讀取路由陣列 return element }
function Router() {
let style = { // 菊花圖的位置
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
marginTop: '300px'
}
return (
// 載入菊花圖
export default Router ```
store倉庫配置
我們都知道倉庫就是用來儲存資料的一些狀態,這裡使用了MobX狀態管理庫將專案響應式的資料與頁面繫結起來,進而實現資料驅動檢視的效果,本專案的store
倉庫同樣也沿用了模組化的思想,將不同頁面的資料使用不同的倉庫儲存起來,最後彙總到RootStore
裡面,要使用的時候只需要引用RootStore
,再使用某個具體的倉庫。
部分程式碼額如下:完整程式碼點選這裡
```
// src/store/index.js
import { createContext, useContext } from "react";
import { observer } from 'mobx-react-lite';
import homeStore from './homeStore'
import mainStore from "./mainStore";
import userStore from "./userStore"
class RootStore { constructor() { this.homeStore = homeStore this.mainStore = mainStore this.userStore = userStore } }
const rootStore = new RootStore()
const context = createContext(rootStore) const useStore = () => { return useContext(context) }
export { useStore, observer }
// src/store/homeStore.js import { makeAutoObservable, runInAction } from "mobx"; // 建立一個響應式倉庫
class HomeStore {
historyArray = [] // 搜尋歷史記錄
recommendItem = [] // 首頁推薦列表
......
constructor() {
makeAutoObservable(this); // 將this放進去將倉庫變成響應式的
}
addHistory(e) {
this.historyArray.unshift(e)
}
....... }
export default new HomeStore() ```
首頁頁面
首頁功能預覽
首頁內容
首頁頁面被分成了五個模組,從上往下依次是:Header(頂部搜尋欄與右側地圖)、House(二手房、新房、租房)、More(輪播圖)、Deal(貝殼指數與我的房子模組)、Botton(下方推薦模組)。
- Header:實現的效果是點選搜尋欄直接跳轉搜尋頁面,點選地圖跳轉到地圖頁面 src/pages/Home/components/Header/index.jsx 部分程式碼:完整程式碼點選這裡 ``` import MyIcon from '@/components/myIcon' import React, { useState } from 'react'; import { Search, Toast } from 'react-vant'; import s from './style.module.less' import { useNavigate } from 'react-router-dom'
export default () => {
const navigate = useNavigate()
const goToMap = () => { // 跳轉地圖頁面 navigate('/map') } const goToSearch = () => { // 跳轉搜尋頁面 navigate('/search') console.log(1); } return (
- House:這個模組分為左中右三部分對應著 二手房、新房、租房三個頁面,但是進入這些頁面的時候也出現了首頁下面的
Tabbar
標籤欄,於是在App.jsx
裡面對底部標籤欄的出現進行了處理:維護出一個數組用於儲存出現Tabbar
頁面的路徑,當切換頁面的時候獲取到當前頁面的路徑,如果不在此陣列中就將Tabbar
隱藏掉。 - src/pages/Home/components/House/index.jsx 部分程式碼:完整程式碼點選這裡 ``` // App.jsx 部分程式碼 function App() { const [showNav, setShowNav] = useState(false) const needNav = ['/', '/message', '/info', '/recommand', '/user'] // 需要被載入Tabbar的路由 const { pathname } = useLocation()
useEffect(() => { setShowNav(needNav.includes(pathname)) console.log(showNav); }, [pathname])
return (
</ConfigProvider>
) } ``` - More:輪播圖模組並沒有做相應的功能,僅僅實現輪播的效果。完整程式碼點選這裡 - Deal:這個模組如下:
實現了點選上方 貝殼指數 與 我的房子 實現下面內容切換的效果,同時點選貝殼指數
以及立即新增
按鈕實現相應頁面的跳轉;在貝殼指數裡面使用了Echarts實現視覺化折線圖的效果,在新增頁面將資料儲存到Mobx中實現內容資料的持久化,同時將資料在我的頁面進行展示。
src/pages/Home/components/Deal/index.jsx 部分程式碼:完整程式碼點選這裡 ```
... ....
// 點選判斷隱藏模組實現內容的切換
<div className={cx({ [s.hidden]: show !== 'beike' })}>
<div className={s.botton2}>
<div className={s.zhishu}>
<div className={s.topWords} onClick={()=>changePage('/cityprice')}>貝殼指數</div>
<div className={s.cityBox}>
<div className={s.city}>南昌房價</div>
<div className={s.arrow}></div>
</div>
</div>
<div className={s.line}></div>
<div className={s.info}>
<div className={s.leftBox}>
<div className={s.topPrice}>
<div className={s.num}>12078</div>
<div className={s.dollor}>元/平</div>
</div>
<div className={s.oldHouse}>
<div className={s.name}>二手房</div>
<div className={s.bigArrow}></div>
<div className={s.percent}>0.2%</div>
</div>
</div>
<div className={s.rightBox}>
<div className={s.num}>19套</div>
<div className={s.yesterday}>昨日成交</div>
</div>
</div>
</div>
</div>
</div>
``
**Deal模組內部:**
點選
貝殼指數`:
則路由跳轉到貝克指數頁面,如下:
這個頁面引用了echarts
的折線圖,實現資料的視覺化效果。
src/pages/other/CityPrice 部分程式碼:完整程式碼點選這裡
```
useEffect(() => {
getData()
var chartDom1 = document.getElementById('echarts1');
var myChart1 = echarts.init(chartDom1);
var option1;
option1={...} // echarts 圖配置
option1 && myChart1.setOption(option1);
}
// echarts 圖的容器
``
點選
立即新增`:
同樣也是將其做成了一個頁面,其功能就是進行個人房屋的新增,在輸入內容後將內容儲存到倉庫中,並且可以在我的頁面進行展示,如果未登入就不進行展示。
- Botton:從後端獲取資料展示,以及實現點選上方推薦、二手房、新房、租房實現頁面內容的切換。頁面內容封裝成一個元件,獲取資料的時候將資料儲存到倉庫內,在元件頁面引入倉庫獲取資料,渲染到元件上。
src/pages/Home/components/Botton/index.jsx 部分程式碼 :完整程式碼點選這裡
我的介面
功能預覽
我的
介面實現登入時向後端請求賬號資訊資料,匹配成功則跳轉我的
頁面,成功登入則從倉庫獲取使用者的頭像與暱稱(這裡後端只返回了賬號與密碼 [賬號:123456 密碼:111111],賬號的暱稱與頭像都是在倉庫裡面寫死的,但是暱稱可以修改),右上角的設定有修改暱稱(已實現)與頭像(未實現)的功能,同時還可以退出當前賬號登入。下方我的房子則與首頁的新增房子功能一樣,新增成功則房產在下面展示。
這個頁面的效果如下:
點選暱稱
位置跳轉登入頁面,如果未登入就不允許修改暱稱
與頭像
(未實現);將暱稱資料儲存在倉庫中,若修改則修改倉庫中的暱稱就實現了暱稱的修改功能。
部分程式碼:完整程式碼點選這裡
```
// src/pages/User/index.jsx
const User = () => {
// 解構出userStore倉庫
const { userStore } = useStore()
const navigate = useNavigate()
const showSetting = () => { navigate('/setting') }
const goToLogin = () => { if(userStore.isLogin){ Toast.info('已登入') }else{ navigate('/login') } } ..... ..... } // src/pages/User/components/Setting/index.jsx 右上角設定功能 const Setting = () => { const navigate = useNavigate() const { userStore } = useStore()
const changeLoginState = () => { // 點選退出登入或者點選登入
if (userStore.isLogin) {
userStore.changeLogin(false)
Toast.info('登出成功!')
navigate('/user')
} else {
navigate('/login')
}
}
const changeInfo = () => {
if (userStore.isLogin) {
navigate('/userInfo')
} else {
Toast.info('請登入')
}
}
const changeName = () => {
if (userStore.isLogin) {
navigate('/userName')
} else {
Toast.info('請登入')
}
}
} ```
Three.js 實現簡單的全景看房
效果如下:
我將這個寫成了一個元件,實現的原理也是比較好理解的:用Three.js
先生成一個立方體,將照片資源分別放在立方體的六個面上,然後將立方體的外表面往內部翻轉,將camera
放到立方體裡面通過鏡頭的旋轉實現全景看房效果。
部分程式碼如下: 完整程式碼點選這裡
// 獲取掛載房子容器的dom結構
const container1 = useRef(null)
// 建立渲染器
const renderer1 = new THREE.WebGLRenderer({
//增加下面兩個屬性,可以抗鋸齒
antialias: true,
alpha: true
})
renderer1.setSize = (window.innerWidth, window.innerHeight) // 設定渲染的頁面大小
// 建立鏡頭
const camera1 = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
camera1.position.z = 0.1
// 建立場景
const scene1 = new THREE.Scene()
// 立方體
const geometry1 = new THREE.BoxGeometry(1, 1, 1)
let livingRoom = ['4_l', '4_r', '4_u', '4_d', '4_b', '4_f'] // 圖片資源
let boxMaterials1 = []
livingRoom.forEach((item, index) => {
// 紋理載入
let texture = new THREE.TextureLoader().load(`./imgs/living/${item}.jpg`)
// if (item === '4_u' || item === '4_d') { // 天花板與地面要設定旋轉中心
if (index === 2 || index === 3) { // 天花板與地面要設定旋轉中心
texture.rotation = Math.PI
texture.center = new THREE.Vector2(0.5, 0.5)
boxMaterials1.push(new THREE.MeshBasicMaterial({ map: texture }))
} else {
boxMaterials1.push(new THREE.MeshBasicMaterial({ map: texture }))
}
})
const cube1 = new THREE.Mesh(geometry1, boxMaterials1)
cube1.geometry.scale(1, 1, -1) // 將幾何體的面往內部翻轉
scene1.add(cube1)
// 渲染場景函式
const render1 = () => {
// console.log(container);
requestAnimationFrame(render1) // 讓瀏覽器遞迴的渲染模型
renderer1.render(scene1, camera1)
}
useEffect(() => {
const controls1 = new OrbitControls(camera1, container1.current)
controls1.enableDamping = true // 增加控制器的阻尼感
container1.current.appendChild(renderer1.domElement) // 往dom結構上掛載這個模型
render1()
}, [])
後端
基於 Egg.js 搭建
總的來說個人感覺 Egg.js 封裝的非常簡便。當然我也只寫了一點點的資料模擬了一下介面請求。具體程式碼看這裡
寫這個專案的時候碰到的問題
- 重新整理後底部Tabbar標籤欄上面的選中顏色會重新匹配到首頁的icon上面:
解決方法:在載入當前頁面的時候獲取當前頁面的路由儲存下來。 ``` const { pathname } = useLocation()
useEffect(() => { setShowNav(needNav.includes(pathname)) }, [pathname]) // 監聽路由變化,設定Tabbar標籤欄的選中 ``` 2. 跳轉不需要底部Tabbar標籤欄的頁面時也會展示標籤欄:
解決方法:維護一個數組用來儲存需要展示標籤欄的頁面的路由地址,在路由地址改變的時候判斷標籤欄是否顯示。
```
const needNav = ['/', '/message', '/info', '/recommand', '/user']
const { pathname } = useLocation()
useEffect(() => {
setShowNav(needNav.includes(pathname))
console.log(showNav);
}, [pathname])
return (
<ConfigProvider>
<>
<Router />
{/* 讓導航欄在底部五個路徑的頁面顯示,其他頁面則不顯示 */}
<div className={cs({ [s.hidden]: showNav == false })}>
<NavBar />
</div>
</ConfigProvider>
)
```
- 在渲染首頁資料的時候發現重複渲染了很多條,後來發現是自己重複的迴圈了儲存資料的陣列,蠢的我!
總結: 經過了一個多星期的 coding ,雖然功能寫的比較拉跨,但是通過這段時間的努力還是有不小的收穫,對於寫專案前的規劃以及在專案中碰到問題如何解決的能力還是有提升的。這個專案讓我在學習 React 的道路上邁出了第一步,雖然中間碰到很多意外bug,但是寫完還是很成就感的。歡迎大佬們點個贊哦,同時也希望大佬們可以提點建議。感激不盡!😎😎😎
附
專案原始碼:點選此處