Github: https://github.com/OrangeXC/gank 連結: https://gank-xovwcisocl.now.sh/
請使用手機或開發者工具手機模擬器開啟
接上一篇內容:React 服務端渲染框架 Next.js 基於 Gank api 實戰
在上一篇結尾說到要實現移動端,不單單是響應式佈局,而是採用移動端元件庫進行開發。
本文重點介紹如何在一個專案裡面實現兩類端的服務端渲染。
前提
- 明確的 router 分割規格
- 判斷裝置跳轉對應端的 router
- 兩套 UI 元件庫
根據三個前提條件逐一給出解決方案。下面首先說下路由分割。
路由分割
路由分割規則大致上分為兩種:
- 子域名形式
(m.xxx.xxx)
- 相同域名形式
(xxx.xxx/m)
這裡強調是一個專案沒必要部署到兩個域名下,故排除子域名的形式。
作為區分移動端在所有的域名前加了 /m
,進而實現 page 級別的元件區分
對映到 next.js 裡面就是在 pages
目錄下新增一個名為 m
的資料夾,裡面的每個檔案都對應著移動端的路由
例如:xxx.com/fe
移動端對應著 xxx.com/m/fe
判斷裝置跳轉路由
這裡直接上程式碼比口述來的痛快
if (/Mobile/i.test(ua) && pathname.indexOf('/m') === -1) {
app.render(req, res, `/m${pathname}`, query)
} else if (!/Mobile/i.test(ua) && pathname.indexOf('/m') > -1) {
app.render(req, res, pathname.slice(2), query)
} else {
handle(req, res, parsedUrl)
}
複製程式碼
邏輯十分簡單,疑問點是此段程式碼應該放在什麼地方,next.js 既然是服務端渲染,判斷理應在服務端進行。
next.js 允許我們自定義入口 server.js 檔案,啟動時直接執行 node server.js
命令。
在這個 server 裡面進行中介軟體的掛載,以及服務端層面的路由控制,具體的實現官網和本專案都可檢視。
兩套 UI 元件庫
對於個人或者小專案沒那麼大精力開發元件庫,也沒有精力設計樣式。
前面的 pc 端用的是 antd,這裡為了保持風格一致使用了 antd-mobile
當然引入 antd-mobile 時 iocn 是個問題,想使用自定義的 icon 需要自己配置 webpack
新建 next.config.js
,重要程式碼如下
config.module.rules.push(
{
test: /\.(svg)$/i,
loader: 'emit-file-loader',
options: {
name: 'dist/[path][name].[ext]'
},
include: [
moduleDir('antd-mobile'),
__dirname
]
},
{
test: /\.(svg)$/i,
loader: 'svg-sprite-loader',
include: [
moduleDir('antd-mobile'),
__dirname
]
}
)
複製程式碼
這裡重點說下 svg-sprite-loader
這個庫的坑,版本最好控制在 0.3.x
,如果升級到最新版會有意外的 bug 驚喜等著你
實現
前提環境搞定了剩下的就是動手開幹了。
這裡不逐一展開解釋,可以看前面 pc 的文章,解釋的夠詳細,這裡單說下實現時可能遇到的問題
問題 1 - 自定義圖示
上面介紹了自定義圖示的配置,在元件裡面具體怎麼實現呢,首先要寫一個渲染函式
const CustomIcon = ({ type, className = '', size = 'md', ...restProps }) => (
<svg
className={`am-icon am-icon-${type.substr(1)} am-icon-${size} ${className}`}
{...restProps}
>
<use xlinkHref={type} /> {/* svg-sprite-loader@0.3.x */}
{/* <use xlinkHref={#${type.default.id}} /> */} {/* svg-sprite-loader@lastest */}
</svg>
)
複製程式碼
程式碼裡面註釋掉的有 svg-sprite-loader@lastest
版本的寫法,親測無效,也不建議嘗試。
在 render 裡面就可以這樣呼叫
<CustomIcon type={require('../../static/icon/github.svg')} />
複製程式碼
到這裡可以展示任意自定義 icon 了。
問題 2 - 長列表
眾所周知移動端的長列表效能堪憂,如果採用前文每次 load more 時,直接把請求回來的資料 concat
或 push
到列表尾部,後果就是頁面逐漸變卡,知道你滑不動列表,甚至網頁卡死。
慶幸 antd-mobile 為我們提供了 ListView
元件,讓我們輕鬆實現長列表渲染
那麼問題來了,antd-mobile 官網為我們提供的例子都是完全基於客戶端的實現,在預渲染階段,我們需要渲染首屏資料,而不是在頁面載入完成後在 componentDidMount
鉤子裡初始化首屏資料。
為了使頁面更快速的渲染首屏列表內容,首次請求需要在服務端獲取資料後立即初始化 ListView
元件。
本專案的做法是,在 page 元件中
static async getInitialProps ({ req }) {
const language = req ? req.headers['accept-language'] : navigator.language
const res = await fetch('https://gank.io/api/data/all/20/1')
const json = await res.json()
return { list: json.results, language }
}
複製程式碼
然後進一步封裝 ListView
元件成一個公用元件,每個頁面都可呼叫
關鍵程式碼是在構造器裡面初始化 ListView
資料來源例項
constructor (props) {
super(props)
const dataSource = new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2,
}).cloneWithRows(props.initList)
this.state = {
rData: [],
dataSource,
isLoading: false
}
}
複製程式碼
在載入更多的時候進行資料的拼接。
注意的是判斷下當前頁數把 props 裡面傳進來的初始化資料拼接進去
this.setState({ isLoading: true })
this.setState((prevState) => ({
rData: pIndex === 2
? this.props.initList.concat(prevState.rData).concat(json.results)
: prevState.rData.concat(json.results)
}))
複製程式碼
在請求完成後不要忘記重新整理 dataSource
,使得 ListView
可以相應資料變化
this.setState({
dataSource: this.state.dataSource.cloneWithRows(this.state.rData),
isLoading: false
})
複製程式碼
到這為止,整個列表請求就實現了
至於展示上的配置項還是蠻多的,官網寫的十分詳細,配置的優劣也會影響效能。
問題 3 - MenuBar 高度問題
由於我們需要全屏高度的展示效果,NavBar 與 Menubar 分別吸附在上下,不隨內容滾動。
尷尬的點是 NavBar 被包在 Menubar 中,而 Menubar 使用了 transform,如果內容區長度超過螢幕高度,會導致 NavBar 的 position: fixed
失效,NavBar 會隨著內容區域一同滾動上去。
嘗試了幾個解決辦法,就算解決了這個問題,還存在 iphone safari 上的滑動導致的視窗高度拉長,進而影響定位不準確的問題。
這裡直接摒棄 body 層面的滾動,所有的滾動區域通過 螢幕高度 - NavBar - Menubar底部 - 其它垂直佔位空間
計算得出。
既保證了滾動區域的高度恰好填充剩餘垂直空間,又保證了 Safari 不觸發視窗的高度拉長
因為高度需要計算獲得,本專案裡面初始化給的是 height: 100vh
(iphone safari 會把下面的選單欄算到 100vh
裡面,導致 MenuBar 定位不準確)
頁面載入後計算一次屏高 document.documentElement.clientHeight
改變螢幕整體展示高度,滾動區域高度也可計算獲得。
總結
由於本文是基於前一篇寫的,踩坑的點數明顯減少,行文的目的也是希望看到本文的人遇到相同問題時可以少踩坑,多一個解決問題的思路。