React 服務端渲染實現 Gank 移動端

Github: https://github.com/OrangeXC/gank 連接: https://gank-xovwcisocl.now.sh/html

請使用手機或開發者工具手機模擬器打開node

接上一篇內容:React 服務端渲染框架 Next.js 基於 Gank api 實戰webpack

在上一篇結尾說到要實現移動端,不僅僅是響應式佈局,而是採用移動端組件庫進行開發。git

本文重點介紹如何在一個項目裏面實現兩類端的服務端渲染。github

前提

  1. 明確的 router 分割規格
  2. 判斷設備跳轉對應端的 router
  3. 兩套 UI 組件庫

根據三個前提條件逐一給出解決方案。下面首先說下路由分割。web

路由分割

路由分割規則大體上分爲兩種:json

  • 子域名形式(m.xxx.xxx)
  • 相同域名形式(xxx.xxx/m)

這裏強調是一個項目不必部署到兩個域名下,故排除子域名的形式。api

做爲區分移動端在全部的域名前加了 /m,進而實現 page 級別的組件區分antd

映射到 next.js 裏面就是在 pages 目錄下新增一個名爲 m 的文件夾,裏面的每一個文件都對應着移動端的路由app

例如: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 時,直接把請求回來的數據 concatpush 到列表尾部,後果就是頁面逐漸變卡,知道你滑不動列表,甚至網頁卡死。

慶幸 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 改變屏幕總體展現高度,滾動區域高度也可計算得到。

總結

因爲本文是基於前一篇寫的,踩坑的點數明顯減小,行文的目的也是但願看到本文的人遇到相同問題時能夠少踩坑,多一個解決問題的思路。

相關文章
相關標籤/搜索