Next.js 踩坑入门系列(七) —— 其他相关知识

10,843 阅读11分钟

Next.js踩坑入门系列

获取数据&&getInitialProps

获取数据,依然是Next与普通的React SPA应用不同的地方,React应用基本都有自己的路由组件(当然大部分是react-router),我们可以通过路由组件为我们提供的方法,比如react-router的onEnter()方法或者universal-router的beforeEnter()方法。

这里给大家推荐一个区别于react-router的路由组件universal-router

而Next.js没有路由组件,所以具体方式肯定不同于路由组件的方式,具体不同就体现在Next.js为我们提供了一个区别于React的新生命周期——getIntialProps(),下面来说说这个API的牛X之处。

使用方法

  • 在React.Component使用
   import React from 'react'

   export default class extends React.Component {
     static async getInitialProps({ req }) {
       const userAgent = req ? req.headers['user-agent'] : navigator.userAgent
       return { userAgent }
     }
   
     render() {
       return (
         <div>
           Hello World {this.props.userAgent}
         </div>
       )
     }
   }

  • 在stateless组件内使用
   const Page = ({ stars }) =>
     <div>
       Next stars: {stars}
     </div>
   
   Page.getInitialProps = async ({ req }) => {
     const res = await fetch('https://api.github.com/repos/zeit/next.js');
     const json = await res.json();
     return { stars: json.stargazers_count };
   }
   
   export default Page;

这个生命周期是脱离于React的正常生命周期的,不过我们依然可以在组件里正常使用react组件的各种生命周期函数。

服务端可用

这真是getInitialProps这个生命周期的过人之处了,他可以在服务端运行,这样做有什么好处呢?说实话,我真不太清楚,我只知道一点,下面会讲,哈哈。如果有大牛知道的话,可以在留言给我讲讲~话不多说,上图:

可以看到,这个生命周期我触发了action获取数据,而这个action在控制台被打印出来了,说明可以运行在服务端~

减少抓取数据的次数

  • React老生命周期内获取数据

    以抓取用户列表为例,我们可以在组件里的componentDidMount生命周期内获取

 // /components/user/userList.js
 ...
 componentDidMount() {
    this.props.fetchUserList();
 }

从上图我们可以看出来,每次进入用户列表页,都会重新抓取用户数据。有人可能会说,这不废话吗,react不就这样吗,路由都切换了啊。没错,正常就是应该这样,所以才说Next.js的这个新生命周期牛逼啊。

  • 使用getInitialProps生命周期
// /pages/user/userList.js
import UserList from '../../containers/user/UserList';
import { fetchUserListData } from '../../redux/actions/user';

UserList.getInitialProps = async (props) => {
  const { store, isServer } = props.ctx;
  if (store.getState().user.list.list.length === 0) {
    store.dispatch(fetchUserListData());
  }
  return { isServer };
};

export default UserList;


兄弟们,看看上图,发现没,进入系统后只会在第一次进入路由的时候获取数据,之后再进入因为服务端缓存过数据,所以不需要重新获取,减少了获取次数~

具体原因就是因为static getInitialProps()这个生命周期是可以在服务端运行的,当页面第一次加载时,服务器收到请求,getInitialProps()会执行,getInitialProps()返回的数据,会序列化后添加到 window.__NEXT_DATA__.props上,写入HTML源码里,类似于={props:{xxx}}。这样服务端的getInitialProps()就实现了把数据传送给了客户端。当我们通过Next.js的路由Link来进行页面跳转的时候,客户端就会从window.__NEXT_DATA__里获取数据渲染页面,就无需重新获取数据,算是提升性能的话一种方式吧~如下图所示:

存在问题——踩坑

这里其实还真遇到一个坑,可能有很多人遇到过了,也可能没人遇到过。具体问题描述起来大概是这个样子,我们在getInitialProps里面预获取数据,以用户列表为例,在首次加载的时候都是没有问题的包括各种客户端跳转。不过当我们在用户列表页面进行刷新的时候,其实他就没有再走getInitialProps这个生命周期了,因此页面会没有可以渲染的数据,就会出现空页面,因为他认为这个应该从window.__Next_DATA__里面获取,而不是重新获取数据~那么为什么刷新页面之后没有走这个getIntialProps,讲道理,我还真没太弄清楚,不过确实刷新页面next.js会给我们在props里返回一个isServer:true,但是控制台并没有获取数据。具体问题见下面截图:

从截图我们可以很清楚地看到,页面数据通过redux-saga获取,在pages的getIntialProps()里面,代码如下:

import { fetchUserListData } from '../../redux/actions/user';

UserList.getInitialProps = async (props) => {
  const { store, isServer } = props.ctx;
  if (store.getState().user.list.list.length === 0) {
    store.dispatch(fetchUserListData());
  }
  return { isServer };
};

上面fetchUserListData()就是抓取数据的action,返回值就会存入state,渲染数据列表。很明显,在第一次加载的时候是抓取成功的。但是刷新页面后,没有dispatch这个action,也就是表明,刷新页面没有走这个getIntialProps这个生命周期!!!

上面才是关键问题所在,不刷新页面的情况下是正常的,刷新页面没有走这个生命周期,而我们很多数据都是需要预获取的,所以说还挺坑的,事实上,很多人遇到这个问题,而且我在next官方给出的reudx-demo里面也发现这个问题,也就是说他们官方的demo刷新也会出现这个问题。

解决办法

既然是踩坑,当然有解决办法啦~而且还是两种:

  • 第一种:在组件生命周期里判断isServer

    刚刚问题描述过了,也就是正常加载和通过路由跳转页面,数据会正常渲染且会从浏览器的window.__NEXT_DATA__获取来减少不必要的网络请求~,而在页面进行刷新的时候不会重新请求数据并且window.__NEXT_DATA__里也找不到我们想要的数据。不过通过控制台信息我们可以发现问题所在以及解决办法。那就是,第一次启动系统的时候返回的isServer是false,而浏览器刷新页面的时候isServer返回的是true,我们可以在组件里进行这个变量的判断,如果是true,就重新进行一次数据抓取。

// /components/user/UserList.js
...
componentDidMount() {
  if(this.props.isServer) {
  // 需要重新抓取数据
    this.props.fetchUserListData();
  }
}
...

从上图可以看到,刷新页面的时候,我们会重新获取数据渲染页面,如果不刷新就不会重新获取。还是可行的这个方法~

  • 第二种:换一种方式预获取数据

    另一种方法就比较高级了,原理我依然不知道,但是就是好用,哈哈,这东西真是邪门,为什么这么说呢,其实本质没改变什么,就是换了种写法就可以。具体就是,上面的写法我在getInitalProps里面写了dispatch了一个获取数据的action,从上一节或者代码里你们可以看到,其实这个action就是fetch一个api获取数据返回state。这就是redux一个获取数据的基本过程,这种方法在刷新时行不通,而行得通的方法是:不通过dispatch action的方式获取数据,而是直接在getIntialProps里面通过fetch api的方式获取数据,这样每次刷新页面也都可以获取到数据了。。。就是这么神奇,我也真不知道为啥。

// /pages/user/userList
import fetch from 'isomorphic-unfetch';
import UserList from '../../containers/user/UserList';
import { fetchUserListDataSuccess } from '../../redux/actions/user';

UserList.getInitialProps = async (props) => {
  const { store, isServer } = props.ctx;
  let userData;
  if (store.getState().user.list.list.length === 0) {
    const res = await fetch('https://jsonplaceholder.typicode.com/users');
    userData = await res.json();
    store.dispatch(fetchUserListDataSuccess(userData));
  }
  return { isServer };
};

export default UserList;

就是很神奇有木有,说实话我是真不知道为啥,有大牛的话真心给我讲讲万分感谢了~ 不过这两种写法我还是比较喜欢上面第一种的,因为觉得第一种在自己可控范围内,因为以前写react项目也是在生命周期里控制一些数据的获取。可能更习惯吧,不过我承认第二种更牛逼一些,性能也可能更好吧~各取所需吧。

Document

这个组件从我使用的角度来看,作用跟我前几章有个地方的目的是一样的,就是我们在Next.js里没有类似create-react-app里面的index.html。因此我们没有办法定义最后渲染的html的结构,比如title,meta等标签。我最开始是通过next/head的Head组件来实现的,但是head组件其实最后生成的就是html的head标签。而Document组件是完全帮助我们构造html结构。

 // 除去Layout的Head结构
 // pages文件夹新增_document.js文件

  // ./pages/_document.js
  import Document, { Head, Main, NextScript } from 'next/document';
  
  export default class MyDocument extends Document {
    static async getInitialProps(ctx) {
      const initialProps = await Document.getInitialProps(ctx);
      return { ...initialProps };
    }
  
    render() {
      return (
        <html>
          <Head>
            <meta name='viewport' content='width=device-width, initial-scale=1' />
            <meta charSet='utf-8' />
            <title>Next-Antd-Scafflod</title>
            <link rel='shortcut icon' href='/static/favicon.ico' type='image/ico'/>
            <link rel='stylesheet' href='/_next/static/style.css' />
          </Head>
          <body>
            <Main />
            <NextScript />
          </body>
        </html>
      );
    }
  }

_document.js是只在Next.js的服务端来进行渲染的,客户端只是拿到服务端渲染过后的html字符串渲染前端页面,上面提到的window.__NEXT_DATA__就是存放在NextScript里的。

Dynamic Import

其实以前在写服务端渲染项目的时候会遇到很多坑,最常见的就是比如我想引入一些外部组件,这些组件里有window,document等这种客户端变量,而这些变量在服务端是不存在的,因此在服务端渲染的时候就会报错,所以就很麻烦,需要webpack各种配置然后在异步引入。比如:富文本编辑器。而next直接为我们封装了动态引入的import,不出意外用的应该就是webpack的import方法,管他呢,好用就行。下面就给大家简单是演示一下其中一个功能,就是动态引入一个富文本编辑器,然后空白期loading另一个组件~用法非常简单,就是下面这样:

import dynamic from 'next/dynamic';

const DynamicComponent = dynamic(import('braft-editor'), {
  loading: () => <p>正在加载组件...</p>
});

render() {
    return (
      <Fragment>
        <h1>用户信息:{this.state.username}</h1>
        <div style={{ width: '50%', height: '400px', }}>
          <DynamicComponent />
        </div>
      </Fragment>
    );
  }

详细的Next为我们提供了更多的方法,感兴趣的可以去官网看文档,有四种异步引入的方法,其中还包含只在服务端引入~文档地址

error handling

错误处理,目前很多优秀的脚手架都为我们提供了错误处理,比如404和500的时候的页面渲染,Next.js同样,内部自动为我们封装了errorPage。也就是我们其实什么都不用干,就可以享受这个服务。比如我在系统里随便输入一个网址,会出现下面的结果:

然后你还可以自己定义你的errorPage页面,方法非常的简单,就是在pages文件夹下面新建一个_error.js的文件,里面写上你的errorPage代码就可以了,下面就简单写一个,其实就是从官网扒下来的~

// /pages/_error.js
import React from 'react'

export default class Error extends React.Component {
  static getInitialProps({ res, err }) {
    const statusCode = res ? res.statusCode : err ? err.statusCode : null;
    return { statusCode }
  }

  render() {
    return (
      <p>
        {this.props.statusCode
          ? `An error ${this.props.statusCode} occurred on server`
          : 'An error occurred on client'}
      </p>
    )
  }
}

ok,可以看到,很明显的生效了。虽然效果差不多,但是你如果按照自己的来写,肯定是没问题的。哈哈~

Static HTML export

又一个高级功能,它支持我们把各种路由导出成静态页面,不过你细想其实也没啥大用,毕竟我们项目都是有逻辑的,导出静态页面也不能操作,哈哈。不过既然是挺牛逼的一个功能,就拿来试试。

  • 第一步,在config文件夹里配置一下页面和路由
exportPathMap: async (defaultPathMap) => {
    return {
      '/home': { page: '/' },
      '/userList': { page: '/user/userList' },
    }
  },
  • 第二步,package.json添加export命令
"scripts": {
    ...
    // 新增导出命令
    "export": "yarn build && next export"
  },
  • 第三步,运行yarn export命令

    运行完命令之后,根目录下会出现一个out文件夹,真的是非常神奇,里面有页面文件夹和必要的静态资源。

然后我们打开index.html访问一下应该就是我们的首页了,首页就是下面这个样子。 emm...这个首页有点奇怪,静态资源和css都不太对劲儿,至于为什么我就不去追究了,肯定有办法的。不过我只是试试功能,时间有限准备休息了,哈哈。感兴趣的大家自己研究研究。

这里还有一个高级的Next.js项目推送到github page的功能,依赖的也是这个export,不过时间问题我就没写,大家感兴趣的去看看官方demo,应该可以解决的~

总结

写到这里,Next.js踩坑入门系列就写完了。非常感谢有很多小伙伴一直在看,还有一些可爱的小伙伴催更,水平有限,完全是踩坑集锦,如果能帮助到大家真的很开心。谢谢大家的阅读。接下来准备用Next.js搭一个网站。完成后可能会再写一篇Next.js的建站文章,其他的就不写了,再写就是其他内容啦~
本章节代码地址

项目代码地址,喜欢的给个Star,谢谢米娜桑