Github: https://github.com/OrangeXC/gank 連接: https://gank-xovwcisocl.now.sh/html
請使用手機或開發者工具手機模擬器打開node
接上一篇內容:React 服務端渲染框架 Next.js 基於 Gank api 實戰webpack
在上一篇結尾說到要實現移動端,不僅僅是響應式佈局,而是採用移動端組件庫進行開發。git
本文重點介紹如何在一個項目裏面實現兩類端的服務端渲染。github
根據三個前提條件逐一給出解決方案。下面首先說下路由分割。web
路由分割規則大體上分爲兩種:json
(m.xxx.xxx)
(xxx.xxx/m)
這裏強調是一個項目不必部署到兩個域名下,故排除子域名的形式。api
做爲區分移動端在全部的域名前加了 /m
,進而實現 page 級別的組件區分markdown
映射到 next.js 裏面就是在 pages
目錄下新增一個名爲 m
的文件夾,裏面的每一個文件都對應着移動端的路由antd
例如: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 裏面進行中間件的掛載,以及服務端層面的路由控制,具體的實現官網和本項目均可查看。
對於我的或者小項目沒那麼大精力開發組件庫,也沒有精力設計樣式。
前面的 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 的文章,解釋的夠詳細,這裏單說下實現時可能遇到的問題
上面介紹了自定義圖標的配置,在組件裏面具體怎麼實現呢,首先要寫一個渲染函數
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 了。
衆所周知移動端的長列表性能堪憂,若是採用前文每次 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 }) 複製代碼
到這爲止,整個列表請求就實現了
至於展現上的配置項仍是蠻多的,官網寫的十分詳細,配置的優劣也會影響性能。
因爲咱們須要全屏高度的展現效果,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
改變屏幕總體展現高度,滾動區域高度也可計算得到。
因爲本文是基於前一篇寫的,踩坑的點數明顯減小,行文的目的也是但願看到本文的人遇到相同問題時能夠少踩坑,多一個解決問題的思路。