React 服务端渲染框架 Next.js 基于 Gank api 实战

最开始先摆出地址,有在线 demo,目前只支持 pc:https://github.com/OrangeXC/gank

鉴于最近 vue 相关的文章写的比较多,抽出时间写点 react 的项目,当时用 react 还是 v15 现在都 v16 了,感慨跟不上所有框架的节奏(玩笑话),框架的本质都是大同小异的,每次高 star 框架更新看一下 change 是个好习惯。

之前用 Nuxt 写了个简单的 v2ex,今天的主角依然是 SSR 服务端渲染

Nuxt 文档里有写到灵感源于 Next.js,那么就是说 Next.js 算是 SSR 框架中的元老级别的了。

为什么选择 SSR 框架

前面的文章总是在官方文档上小费功夫说明下,这里对于不熟悉 Next.js 的读者建议直接转到 Github,别犹豫,当然熟悉 Nuxt 也可以无障碍阅读本文

不论是 Next.js 或 Nuxt,服务端渲染框架主要两个重要功能

  • 首屏 node.js 服务端渲染
  • 生成纯静态的 web 站

至于它们是基于哪个前端库封装的,还要看库本身是否支持 SSR,然后就是对外提供 render 函数

用此类库的原因也不必多说,节省开发成本,不再纠结于环境搭建以及渲染细节。

直接开工

本次要实现的是基于 gank api 的项目,还是看人家支持什么 api,点前面链接查看详细 api

大体总结为 => 列表,搜索,提叫到审核

列表分为了许多类型,主要的 menu 也是针对不同类型的列表展开

路由

通过已知的 api 可以轻松的定义路由

  • / (主页,最近的全部类型干货列表)
  • /fe (前端干货列表)
  • /android (安卓干货列表)
  • /ios (iOS干货列表)
  • /app (App干货列表)
  • /expand (拓展资源干货列表)
  • /videos (休息视频干货列表)
  • /welfare (福利列表,前方高能,全是干货。。。)
  • /timelien (时间轴,记录历史所有更新过干货的日期)
  • /day (某天详情,分为以上几种类型的 tab 列表)
  • /uplaod (发送干货到审核)
  • /search (搜索页)

同 Nuxt 路由配置文件不需要手动创建,/pages 下认会渲染为页面文件名自然就是路由名

路由文件都创建完了,下一步思考如抽离出公共模板 Layout 代码,Next.js 提供了 layout-component example

我们可以在里面定义 Head,Header,Footer,当然要留出一个内容区域的插槽 { children }

引用于 example 的 layout.js 代码

import Link from 'next/link'
import Head from 'next/head'

export default ({ children,title = 'This is the default title' }) => (
  <div>
    <Head>
      <title>{ title }</title>
      <Meta charSet='utf-8' />
      <Meta name='viewport' content='initial-scale=1.0,width=device-width' />
    </Head>
    <header>
      <nav>
        <Link href='/'><a>Home</a></Link> |
        <Link href='/about'><a>About</a></Link> |
        <Link href='/contact'><a>Contact</a></Link>
      </nav>
    </header>

    { children }

    <footer>
      {'I`m here to stay'}
    </footer>
  </div>
)

因为本次使用的是 antd 做 ui,固实现动态的导航展示上要注意些小问题,我们需要根据 path 动态的给 menu 激活状态。

两个解决方案:

1.在 pages 里面的每一个路由页面获取 pathname,初始化方法 getinitialProps 里可以拿到 pathname,全部列表如下

  • pathname - path section of URL
  • query - query string section of URL parsed as an object
  • asPath - String of the actual path (including the query) shows in the browser
  • req - HTTP request object (server only)
  • res - HTTP response object (server only)
  • jsonPageRes - Fetch Response object (client only)
  • err - Error object if any error is encountered during the rendering

调用方法也简单

static async getinitialProps({ pathname }) {
  return { pathname }
}

这样一来可以通过传参到 layout 组件的方式 <Layout pathname={this.props.pathname}></Layout>

在 Layout 里面改变 Meun 的 active

2.写一个 ActiveLink 组件,再封装一层原有的 Menu

在选择方案前还是要看官方有没有 example,于是找到了 using-with-router

引用于 example 的 ActiveLink.js 代码

import { withRouter } from 'next/router'

// typically you want to use `next/link` for this usecase
// but this example shows how you can also access the router
// using the withRouter utility.

const ActiveLink = ({ children,router,href }) => {
  const style = {
    marginRight: 10,color: router.pathname === href ? 'red' : 'black'
  }

  const handleClick = (e) => {
    e.preventDefault()
    router.push(href)
  }

  return (
    <a href={href} onClick={handleClick} style={style}>
      {children}
    </a>
  )
}

export default withRouter(ActiveLink)

简单易懂,在 withRouter 方法里可以取到 router 实例,这样可以取到 pathname,query 等等。

这里只需要稍稍修改下 style,变成 antd 的 className,如下

const ActiveLink = ({ children,href }) => {
  const active = router.pathname === href
  const className = active ? 'ant-menu-item-selected ant-menu-item' : 'ant-menu-item'
  return (
    <li href='#' onClick={onClickHandler(href)} className={className} role="menuitem" aria-selected="false">
      {children}
    </li>
  )
}

在 Layout 组件的 Menu 里直接使用 ActiveLink 组件即可,到这为止解决了全部路由相关问题和 Layout 组件问题

数据流

解决了路由问题下一步就是每个页面的 content 的数据填充

我们依旧是在 getinitialProps 里面获取数据,相当于 prefatch 方法,服务端渲染会提前执行这个方法获取数据渲染到模板

这里涉及到一个 node 和 browserify 同构的 fetch 库 isomorphic-fetch,cli 工具应该会自带这个库,没有的话提前安装下。

到这里就不用担心 fetch api 在服务端的问题了,这里获取的列表数据走的接口基本一致 https://gank.io/api/data/{type}/{perPage}/{page}

三个变量 type-类型、perPage-每页数量、page-页数

接下来可以把 List 和 ListItem 抽象出来,成为共用的组件,每个页面都可以调用,这里不详细展开说明,简单的使用 antd 的 Card 组件,没有特殊功能

每个页面的请求数据部分也基本一致,将数据存到 props 里,传入 List 组件中去

形成了简单的单向数据流动

列表页

page组件(fetch data) -> List组件(继承自 Layout) -> ListItem组件

时间轴页面

page组件(fetch data) -> Timeline组件(继承自 Layout)

提交干货页面

page组件 -> Form组件(继承自 Layout) -> post请求(发送formData)

搜索页面

page组件 -> Input组件+空ListItem组件(继承自 Layout) -> get请求(获取关键词对应query的列表数据) -> ListItem组件

Mobx

既然前面说清楚了数据流都十分简单,那么为什么要引入全局状态管理徒增烦恼呢?

有一点无奈的地方是 getinitialProps 本身 return 的就是 props,在 react 里面 props 是单向的,只能向下传递,且不能修改

这里我们要分页功能,但是首屏数据是 props 的,我们换页之后没办法更新 props 的值,也就是没办法再次执行 getinitialProps

最简单粗暴的方式就是放弃 spa 的动态切换数据,我们每次 Router.push({some page}/{per page}/{current page}),一朝回到解放前的 MVC 版路由切换。

能不能解决问题,答案是能解决问题,那么既然是分页组件,人家 antd 也提供了 Pagination 组件,问题一个接着一个,人家返回的列表并没有告诉你 totalCount,没有 totalCount 就没办法知道有多少页。。。

好尴尬的问题,这个分页没法做,怒脸~~~

也不是没办法做,这个问题变向思考下可以做 loadMore,没错加载更多,当加载到最后一页(即的列表长度小于 perPage)或是此页恰巧等于 perPage 但下一页为空数组时,我们给一个提示,没有更多内容了。

涉及到向 props 的 list 里 concat 数组,我们不得不引入全局状态来解决这个问题,不论是 redux 还是 mobx 都可以解决问题,需要注意的是,next.js 中的用法和普通 spa 的 react 应用有所差别。

还是去找 example,with mobx

引用于 example 的 store.js 代码

export function initStore (isServer,lastUpdate = Date.Now()) {
  if (isServer) {
    return new Store(isServer,lastUpdate)
  } else {
    if (store === null) {
      store = new Store(isServer,lastUpdate)
    }
    return store
  }
}

这段代码太简单,没必要解释了,总之我们在初始化页面调用 initStore 就好了,isServer 通过 getinitialProps 的 req 参数 !!req 判断

然后在 loadMore 时出发一个 action

@action loadMoreList = (more) => {
  this.list = this.list.concat(more)
}

到这加载更多的功能也就实现了,不足的一点是 List 组件里的 handleScroll 方法写的有点简陋,虽说能用,但存在问题,如多次触发、未写兼容代码(后续会改进),放出代码供大家一笑

handleScroll () {
  if (document.documentElement.offsetHeight + document.documentElement.scrollTop > document.documentElement.scrollHeight - 50) {
    this.handleLoadMore()
  }
}

其它代码感兴趣可以直接取仓库看,没有阅读难度。

表单提交

说到其它页的 fetch list 没什么可将全都是 get 请求,fetch 发一个 get 请求十分简单,不用声明请求类型。

fetch 操作 post 也仅仅在于设置 method 为 POST

之所以单独一章说表单提交,因为在提交表单时遇到了一些问题,由于要 fetch 模拟 form 的 post 请求

看了这个 issue:https://github.com/matthew-an...

开始怀疑人生,试了所有方法 POST,也走的通,但是接口返回的 msg 就是没接收到参数。

想了想还是回归到笨方法一个一个将参数拼接进去,没想到较优雅的方式,给出代码,同时欢迎讨论

handleSubmit = (e) => {
  e.preventDefault()

  this.props.form.validateFieldsAndScroll(async (err,values) => {
    if (!err) {
      this.setState({ submitLoading: true })

      let strList = []

      Object.keys(values).forEach(item => {
        strList.push(`${item}=${values[item]}`)
      })

      const res = await fetch("https://gank.io/api/add2gank",{
        method: "POST",headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        },body: strList.join('&')
      })

      const json = await res.json()

      if (json.error) {
        message.error(json.msg)
      } else {
        message.success(json.msg)
      }

      this.setState({ submitLoading: false })
    }
  })
}

看过网站的读者也发现提交表单页面上方有提示语,让大家文明使用三方 api 提供者 gank 的发表干货接口,把真正的好内容提交上去,想测试接口的请走认的 debug 模式,这里再次强调下,感谢配合

微交互

既然功能差不多了,再微交互上再加把劲,用过 NUXT 的知道 NUXT 内置了 Loading bar,切换路由时在页面顶端会有 loading 条,体验较好。

next.js 并没有内置这个功能页面看起来会显得十分怪异,点击切换路由没有反应,顿一下再跳转,顿的时候在获取初始化数据。

官方推荐使用 nprogress

关键代码如下,写在了 Layout.js 组件里

Router.onRouteChangeStart = (url) => {
  console.log(`Loading: ${url}`)
  NProgress.start()
}
Router.onRouteChangeComplete = () => NProgress.done()
Router.onRouteChangeError = () => NProgress.done()

这样整个网站看起来洋气多了,切换 router 页面顶端有 loading bar,右上角还有 loading icon

上线

开发 next.js 的组织叫 zeit,在官网他们的得意作品是 now一个快速部署的工具,同时为免费用户提供三个免费的服务,支持 docker,node 等

看 5 分钟文档就能上手部署 node 项目,比 Heroku 简单的多

这里使用的就是 now,首先安装 now-cli

在项目根路径下一句命令部署

Now

线上的路径就不贴出来了,时刻关注 Github 上方的 website 地址,因为每次部署不绑定域名的情况下是 项目名+随机哈希 的域名,绑定域名需要 money。

至于上线就讲这么多,有疑问欢迎交流。

未来

下一步要解决几个问题

  • 加载更多时的 bug
  • 支持移动端
  • 福利页面直接展示图片(点击可以全屏大轮播)
  • 美化时间轴样式

说到福利页面本想着不加来着,因为个别写 demo 的人专门把福利列表拎出来做成妹子 App,既然是干货集中营,就应该多些技术元素,福利都是次要的。

总结

到这为止一个 next.js 版本的 gank(干货集中营)完成了,感慨现在开发工具越来越好用,还是之前的想法把好用的工具分享给大家,给一个完整的例子供学习者参考,不再每次都看各个版本的 Hacker News,而是给国内的学习者一个中文版的例子,同时文中也会将实现的时候遇到的问题。

本人 orange 也是再不断的学习当中,本文也是第一次接触学习 next.js 写的项目,文章或项目有不足之处欢迎指正,感谢阅读!

文章出自 orange 的 个人博客 http://orangexc.xyz/

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom