react服务端渲染框架nextjs使用记录,踩坑记录,nginx配置,webpack配置
刚刚用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
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 } })
在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
- 渲染过程中的任何错误
当用户访问一个网址时,服务端执行顺序为
执行app中的getInitialProps
执行页面中的getInitialProps
执行app中的construct、componentWillMount、render
执行页面中的construct、componentWillMount、render
在服务端是不执行componentDidMount的,以上几个在服务端执行的函数中,要注意不要调用浏览器才有的对象,比如window,否则会报错。
第一次访问之后,如果通过nextjs的router切换页面,getInitialProps等将不会在服务端执行了,而是直接在浏览器端执行,这和单页面应用又是相似的了。
在getInitialProps中进行网络请求,就不能使用ftech了,因为这是浏览器端特有的,我们可以使用isomorphic-unfetch代替fetch,isomorphic-unfetch既可以在浏览器端执行,也可以在node环境中执行。
我们可以通过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 值但是没有切换页面路
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文件达到了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)