React 服务端渲染实现 Gank 移动端

综合技术 2017-11-15

请使用手机或开发者工具手机模拟器打开

接上一篇内容: 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-sprite-loader@0.3.x */}
    {/*  */} {/* svg-sprite-loader@lastest */}
  
)

代码里面注释掉的有 svg-sprite-loader@lastest
版本的写法,亲测无效,也不建议尝试。

在 render 里面就可以这样调用


到这里可以展示任意自定义 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
改变屏幕整体展示高度,滚动区域高度也可计算获得。

总结

由于本文是基于前一篇写的,踩坑的点数明显减少,行文的目的也是希望看到本文的人遇到相同问题时可以少踩坑,多一个解决问题的思路。

稀土掘金

责编内容by:稀土掘金 (源链)。感谢您的支持!

您可能感兴趣的

Build a Reusable Design System With React React has done a lot to simplify web development. React's component-based arc...
How to Build a Realtime Chat App with React and Gr... GraphQL and React are a match made in heaven. Their ecosystem provides a lot of ...
Creating a Blogging App Using React, Part 6: Tags In thepreviouspart of this tutorial series, you saw how to implement the profil...
How to build, test and deploy React Applications i... How to build, test and deploy React Applications in 2017 February 2, 2017...
#Angular or #React – Try Both It’s Worth It #angul... Most of the things that I’m going to describe in this post are well know fo...