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

orangexc發表於2017-11-15

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

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

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

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

本文重點介紹如何在一個專案裡面實現兩類端的服務端渲染。

前提

  1. 明確的 router 分割規格
  2. 判斷裝置跳轉對應端的 router
  3. 兩套 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 時,直接把請求回來的資料 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 改變螢幕整體展示高度,滾動區域高度也可計算獲得。

總結

由於本文是基於前一篇寫的,踩坑的點數明顯減少,行文的目的也是希望看到本文的人遇到相同問題時可以少踩坑,多一個解決問題的思路。

相關文章