react服务端渲染框架nextjs使用记录,踩坑记录,nginx配置,webpack配置

蛰伏已久 2019-04-25

刚刚用react的服务端渲染框架nextjs完成一个项目,期间遇到很多问题,好在都一一解决了,很想把整个过程记录下来以帮助更多的人,本文将主要从这几个方面的内容进行分享

  • 服务端渲染和单页面应用主要区别在哪

  • nextjs的安装使用及webpack配置

  • 整个项目的结构如何规划,重写app.js

  • 服务器端如何请求数据

  • 服务端渲染的过程,哪些生命周期函数运行的服务端,哪些运行在浏览器端

  • 如何设置页面head中的seo等信息

  • 如何监听router变化

  • nginx服务器如何部署

在阅读本文前,你需要先对nextjs有一个初步的了解,见官方中文文档地址:http://nextjs.frontendx.cn/

为什么要用服务端渲染,和单页面应用有哪些区别

单页面应用开发起来比较简单,但是对于SEO不够友好,对于一些以提供服务为主的网站来说还无所谓,但是如果是资讯类,或者其他对SEO比较看重的应用,则必须要进行服务端渲染了,这也是用nextjs开发本次项目的唯一目的。

非服务端渲染的应用,当我们输入域名,返回的html一般是这样的,内容很简单,一个固定的title,几个css文件和几个js文件,基本没有dom内容,这样的html对搜索引擎爬虫是不友好的,爬虫获取不到页面的内容。

<!DOCTYPE html>
<html>
   <head>
       <title>**后台管理系统</title>
       <link href=/static/css/app.443bfa9a4abe8267606ac383b1f76d2c.css rel=stylesheet>
   </head>
   <body>
       <div id=app></div>
       <script type=text/javascript src=/static/js/manifest.152a5d31ac489dd0c7f6.js></script>
       <script type=text/javascript src=/static/js/vendor.1c7843af57642f4d4195.js></script>
      <script type=text/javascript src=/static/js/app.021280dc8c19b11c4c56.js></script>
   </body>
</html>

而服务端渲染的应用,js代码运行在服务器的nodejs环境中,当用户访问某个网址时,运行在node中的js先去拉取数据,然后根据拉取的数据渲染出html代码(根据拉取的数据设置不同的SEO信息,展示首屏的内容等),返回给浏览器,相比单页面应用,多了一步在服务端渲染初始html代码。

有一个点还是需要注意的,比如在单页面应用中,我们可以通过在localstorage中保存一个token、username等来标识用户的登录信息,每次进行api数据请求的时候,把token加到api地址中或者加到header中就可以了,而面对服务端渲染,这种方法就行不通了,为什么呢?当用户在浏览器中输入网址回车的时候,我们服务器端就要判断用户是否登录了,是否应该返回一些信息,而此时你根本没有办法去用户输入的网址中添加token,我们可能只能采取cookie的形式来保存用户登录信息了。


nextjs的安装及webpack中less、图片、antd配置

安装nextjs

npm install --save next react react-dom

在package.js中添加如下代码,开发模式我们运行 npm run dev,开发完成先进行npm run build,然后再进行npm run start即可以服务端渲染的模式运行代码

{
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start"
  }
}

在nextjs中,我们不需要配置路由,路由根据pages中的文件结构自动生成,这点还是很方便的

pages
   ---index.js   //路由为/index
   ---my
      --index.js      //路由为/my/index
      -- news-detail  //路由为/my/news-detail

nextjs默认是不支持less的,需要我们自己配置,首先安装依赖

npm install --save @zeit/next-less less

在项目根目录下添加next.config.js文件,并添加如下代码

const withLess = require('@zeit/next-less')
module.exports = withLess()

使用中发现在css中引用背景图片很不方便,我一般习惯在jsx中和css中这样引用图片,可惜不支持,需要更改webpack中关于图片的引用

//jsx中引用图片
<img src={require('../../assets/img/a.png')}/>

//css中引用图片
.avatar{
    ...
    background-image:url("../../assets/img/a.png") 
}

经查找资料,发现可以在withLess()中传入webpack的配置,如下,可通过config.module.rules.push添加新的loader

const withLess = require('@zeit/next-less')
module.exports = withLess({
    webpack(config, options) {
        config.module.rules.push({
            test:  [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
            use: [
                {
                    loader: 'url-loader',
                    options: {
                        limit: 10000,
                        name: 'static/media/[name].[hash:8].[ext]',
                        publicPath:"/_next/"
                    }
                }
            ]
        })

        return config
    }
})

如果需要在项目中引入antd ui组件的,还可以添加如下代码来更改ui主题,完整的next.config.js如下

const withLess = require('@zeit/next-less')
module.exports = withLess({
    lessLoaderOptions: {
        modifyVars:{
            'primary-color':'#0AA867',
            'link-color':'#0AA867',
        },
        javascriptEnabled: true
    },
    webpack(config, options) {
        config.module.rules.push({
            test:  [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
            use: [
                {
                    loader: 'url-loader',
                    options: {
                        limit: 10000,
                        name: 'static/media/[name].[hash:8].[ext]',
                        publicPath:"/_next/"
                    }
                }
            ]
        })

        return config
    }
})


重写<App>

在vue和react中都有一般都有个<App>组件,对于本次项目,顶部导航,底部,侧边栏,登录框等我们都可以进行多页面复用,可以考虑提取出来放到<App>中,除此之外,<App>组件还有如下作用

  • 当页面变化时保持页面布局

  • 当路由变化时保持页面状态

  • 使用componentDidCatch自定义处理错误

  • 注入额外数据到页面里 (如 GraphQL 查询)

这样每个路由页面只展示每个页面特有的内容,在nextjs中重写app.js是这样的。首先在pages目录下添加 _app.js文件,然后添加如下代码。

import App, {Container} from 'next/app'
import React from 'react'

export default class MyApp extends App {

  //如果页面有getInitialProps函数,要先执行页面的getInitialProps,然后当做pageProps传递给页面
  static async getInitialProps ({ Component, router, ctx }) {
    let pageProps = {}

    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps(ctx)
    }

    return {pageProps}
  }

  render () {
    const {Component, pageProps} = this.props
    return <Container>
      <Page>  //Page是自定义的组件
          <Component {...pageProps} />   //Component代指页面组件,就是pages下面的页面组件,将getInitialProps中pageProps传递给每个页面
      </Page>
    </Container>
  }
}

在nextjs中通过getInitialProps异步获取渲染页面所需的数据,然后将获取的数据当做属性props传递给页面组件

服务器端如何请求数据

在上面的 _app.js中,我们看到了一个特殊的函数 getInitialProps,这个函数只有app.js和pages下的页面组件有,普通组件没有,当用户访问某个网址时,先执行这个getInitialProps函数,getInitialProps返回一个对象,这个对象会作为属性传递给页面组件。

export default class extends Component {

    static async getInitialProps({ req }) {
        let news = await network.get("/api/news/get")
        return {news}
    }
    
    render(){
        const {news} = this.props
        return(
        <div>
            ....
        </div>)
    
    }
}

getInitialProps中有这几个参数

  • pathname - URL 的 path 部分

  • query - URL 的 query 部分,并被解析成对象

  • asPath - 显示在浏览器中的实际路径(包含查询部分),为String类型

  • req - HTTP 请求对象 (只有服务器端有)

  • res - HTTP 返回对象 (只有服务器端有)

  • jsonPageRes - 获取数据响应对象 (只有客户端有)

  • err - 渲染过程中的任何错误

当用户访问一个网址时,服务端执行顺序为

  1. 执行app中的getInitialProps

  2. 执行页面中的getInitialProps

  3. 执行app中的construct、componentWillMount、render

  4. 执行页面中的construct、componentWillMount、render

在服务端是不执行componentDidMount的,以上几个在服务端执行的函数中,要注意不要调用浏览器才有的对象,比如window,否则会报错。

第一次访问之后,如果通过nextjs的router切换页面,getInitialProps等将不会在服务端执行了,而是直接在浏览器端执行,这和单页面应用又是相似的了。

在getInitialProps中进行网络请求,就不能使用ftech了,因为这是浏览器端特有的,我们可以使用isomorphic-unfetch代替fetch,isomorphic-unfetch既可以在浏览器端执行,也可以在node环境中执行。


如何设置head

我们可以通过next/head来设置head信息,官方示例

import Head from 'next/head'

export default () =>
  <div>
    <Head>
      <title>My page title</title>
      <meta name="viewport" content="initial-scale=1.0, width=device-width" />
    </Head>
    <p>Hello world!</p>
  </div>

我们可以封装一个CommonHeader组件,然后每个页面将seo所需的title、keywords等传递进来即可

import React,{Component} from 'react'
import Head from 'next/head'

class CommonHead extends Component{

    render(){
        let {title,keywords,description,children} = this.props
       
        return(
            <Head>
                <title>{title || "默认页面名称"}</title>
                <meta name='keywords' content={keywords || '默认关键词'}/>
                <meta name='description' content={description || '默认描述'}/>
                
                //引入其他共用的css或者js文件
                <link rel='stylesheet' type='text/css' href='/static/css/common.css'/>
                <link rel="shortcut icon" href="/static/favicon.ico" />
                <script src="https://cdn.bootcss.com/babel-polyfill/7.4.3/polyfill.min.js"></script>
            

                {children}
            </Head>
        )
    }
}

export default CommonHead

然后可以在每个页面调用这个组件,传入标题、关键词和描述。


如何监听路由变化

在页面切换时,如果不添加一些loading动画效果,一旦响应慢了,体验非常不好,因此需要监听路由的切换事件。我们可以在_app.js componentDidMount中统一添加路由切换的处理事件,比如开始切换时显示loading,完成切换时隐藏loading,在开始切换至完成切换这段时间内,在进行getInitialProps中的网络请求。

import Router from 'next/router'
componentDidMount(){
    Router.events.on('routeChangeStart', ()=>{
       showLoading()
   })

   Router.events.on('routeChangeComplete',  ()=>{
       hideLoading()
   })
}

next路由提供了一些事件,如routerChangeStart、routerChangeComplete,除此之外还有

  • routeChangeStart(url) - 路由开始切换时触发

  • routeChangeComplete(url) - 完成路由切换时触发

  • routeChangeError(err, url) - 路由切换报错时触发

  • beforeHistoryChange(url) - 浏览器 history 模式开始切换时触发

  • hashChangeStart(url) - 开始切换 hash 值但是没有切换页面路由时触发

  • hashChangeComplete(url) - 完成切换 hash 值但是没有切换页面路


nginx服务器如何部署

nextjs打包后的文件存储在.next文件夹,但是只有这个文件夹下的内容是不够的,因为要在node端运行,还需要next以及react及reactDom等,简单起见,我们可以把整个项目都传到服务器。这样的缺点就是node_modules比较大,不过这样最简单。

在服务器端,进入项目路径 依次执行 npm run build ,npm run  start即可将项目运行在服务器端,默认是localhost:3000,我们肯定是要通过域名访问项目,因此还需要进行域名配置,可通过nginx反向代理来实现。


我的域名配置如下:

server {
        listen  80;
        server_name  yourdomain.com;

        error_log  /var/log/nginx/yourdomain_error.log;

        location / {
           proxy_pass     
           proxy_http_version 1.1;
           proxy_set_header Upgrade $http_upgrade;
           proxy_set_header Connection 'upgrade';
           proxy_set_header Host $host;
           proxy_cache_bypass $http_upgrade;
        }

}

现在可以通过域名访问了,但是还是存在一个问题,就是我们刚才执行的npm run  start命令,必须打开命令行才有效,一旦关闭命令行,进程也终止了,简单的做法就是通过nohup在后台执行 npm run start ,这样关闭命令行仍然有效。

nohup   npm run start &

除了nohup,我们可以看到很多文章推荐使用pm2进行管理。

PM2是node进程管理工具,可以利用它来简化很多node应用管理的繁琐任务,如性能监控、自动重启、负载均衡等,而且使用非常简单。

全局安装PM2

npm install -g pm2

为了简单的使用pm2,我们先把项目下的package.json中添加一个server指令,server指令依次执行next build和next start

"scripts": {
  "dev": "next",
  "build": "next build",
  "start": "next start",
  "server":"next build && next start", //添加server指令
},

进入到项目所在目录

执行如下命令,将yourName换成你的项目名,这个是给这个进程起的名称,可以随意  --watch代表监听项目文件,当文件发生变化是,自动重新加载如下指令,这样就很方便了,当我们更改代码之后,只需要传到服务器即可,pm2会自动监听,重新执行 npm run server

 pm2 start npm --name yourName -- run server --watch

这样就完成服务器部署了,可能有的同学还想服务器重启之后自动运行这个命令,使用pm2也很简单只需执行如下命令即可

pm2 startup

PM2其他指令

  • 查看pm2进程:pm2 list

  • 停止某个进程:pm2 stop app_name|app_id

  • 停止所有进程:pm2 stop all

  • 监听文件变化自动重启:pm2 start app.js --watch

  • 开启3个进程:pm2 start app.js -i 3 # 开启三个进程

  • 重启进程:pm2 restart app_name|app_id  、    pm2 restart all 

部署一段时间.next文件超大

距离上次部署过去了一两个月,有一天突然发现服务器挂了,最后检查发现,.next文件达到了15G,把系统占满了,原因是每次build都会在.next/static 和 .next/server中添加一个以hash命名的文件夹,内部是编译后的pages,如果我们执行的build过多,则.next会变得超大,解决办法也很简单,就是在每次build的时候,清空.next文件夹。

在next.config.js中可以修改webpack配置,我们利用clean-webpack-plugin来清空文件夹,主要代码片段为 config.plugins.push(new CleanWebpackPlugin())

const withLess = require('@zeit/next-less')
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = withLess({
    lessLoaderOptions: {
        modifyVars:{
            'primary-color':'#0AA867',
            'link-color':'#0AA867',
        },
        javascriptEnabled: true
    },
    webpack(config, options) {
        config.module.rules.push({
            test:  [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
            use: [
                {
                    loader: 'url-loader',
                    options: {
                        limit: 10000,
                        name: 'static/media/[name].[hash:8].[ext]',
                        publicPath:"/_next/"
                    }
                }
            ]
        })
        
        config.plugins.push(new CleanWebpackPlugin())

        return config
    }

})

-END-

点赞(9)