React服务端渲染-next.js
前端项目大方向上可以分为两种模式:前台渲染和服务端渲染。
前台渲染-SPA应用是一个主要阵营,如果说有什么缺点,那就是SEO不好。因为默认的HTML文档只包含一个根节点,实质内容由JS渲染。并且,首屏渲染时间受JS大小和网络延迟的影响较大,因此,某些强SEO的项目,或者首屏渲染要求较高的项目,会采用服务端渲染SSR。
Next.js 是一个轻量级的 React 服务端渲染应用框架。
熟悉React框架的同学,如果有服务端渲染的需求,选择Next.js是最佳的决定。
- 默认情况下由服务器呈现
- 自动代码拆分可加快页面加载速度
- 客户端路由(基于页面)
- 基于 Webpack 的开发环境,支持热模块替换(HMR)
初始化项目
方式1:手动撸一个
mkdir next-demo //创建项目
cd next-demo //进入项目
npm init -y // 快速创建package.json而不用进行一些选择
npm install --save react react-dom next // 安装依赖
mkdir pages //创建pages,一定要做,否则后期运行会报错
然后打开 next-demo
目录下的 package.json
文件并用以下内容替换 scripts
配置段:
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
}
运行以下命令启动开发(dev)服务器:
npm run dev // 默认端口为3000
npm run dev -p 6688 // 可以用你喜欢的端口
服务器启动成功,但是打开localhost:3000,会报404错误。
那是因为pages目录下无文件夹,因而,无可用页面展示。
利用脚手架:create-next-app
npm init next-app
# or
yarn create next-app
如果想用官网模板,可以在 https://github.com/zeit/next.js/tree/canary/examples 里面选个中意的,比如hello-world
,然后运行如下脚本:
npm init next-app --example hello-world hello-world-app
# or
yarn create next-app --example hello-world hello-world-app
下面,我们来看看Next有哪些与众不同的地方。
Next.js特点
特点1:文件即路由
在pages目录下,如果有a.js,b.js,c.js三个文件,那么,会生成三个路由:
http://localhost:3000/a
http://localhost:3000/b
http://localhost:3000/c
如果有动态路由的需求,比如http://localhost:3000/list/:id
,那么,可以有两种方式:
方式一:利用文件目录
需要在/list
目录下添加一个动态目录即可,如下图:
方式二:自定义server.js
修改启动脚本使用server.js:
"scripts": {
"dev": "node server.js"
},
自定义server.js:
下面这个例子使 /a
路由解析为./pages/b
,以及/b
路由解析为./pages/a
// This file doesn't go through babel or webpack transformation.
// Make sure the syntax and sources this file requires are compatible with the current node version you are running
// See https://github.com/zeit/next.js/issues/1245 for discussions on Universal Webpack or universal Babel
const { createServer } = require('http')
const { parse } = require('url')
const next = require('next')
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
app.prepare().then(() => {
createServer((req, res) => {
// Be sure to pass `true` as the second argument to `url.parse`.
// This tells it to parse the query portion of the URL.
const parsedUrl = parse(req.url, true)
const { pathname, query } = parsedUrl
if (pathname === '/a') {
app.render(req, res, '/b', query)
} else if (pathname === '/b') {
app.render(req, res, '/a', query)
} else {
handle(req, res, parsedUrl)
}
}).listen(3000, err => {
if (err) throw err
console.log('> Ready on http://localhost:3000')
})
})
特点2:getInitialProps中初始化数据
不同于前端渲染(componentDidMount
),Next.js有特定的钩子函数初始化数据,如下:
import React, { Component } from 'react'
import Comp from '@components/pages/index'
import { AppModal, CommonModel } from '@models/combine'
interface IProps {
router: any
}
class Index extends Component<IProps> {
static async getInitialProps(ctx) {
const { req } = ctx
try {
await AppModal.effects.getAppList(req)
} catch (e) {
CommonModel.actions.setError(e, req)
}
}
public render() {
return <Comp />
}
}
export default Index
如果项目中用到了Redux,那么,接口获得的初始化数据需要传递给ctx.req,从而在前台初始化Redux时,才能够将初始数据带过来!!!
特点3:_app.js和_document.js
_app.js可以认为是页面的父组件,可以做一些统一布局,错误处理之类的事情,比如:
- 页面布局
- 当路由变化时保持页面状态
- 使用componentDidCatch自定义处理错误
import React from 'react'
import App, { Container } from 'next/app'
import Layout from '../components/Layout'
import '../styles/index.css'
export default class MyApp extends App {
componentDidCatch(error, errorInfo) {
console.log('CUSTOM ERROR HANDLING', error)
super.componentDidCatch(error, errorInfo)
}
render() {
const { Component, pageProps } = this.props
return (
<Container>
<Layout>
<Component {...pageProps} />
</Layout>
</Container>)
}
}
_document.js 用于初始化服务端时添加文档标记元素,比如自定义meta标签。
import Document, {
Head,
Main,
NextScript,
} from 'next/document'
import * as React from 'react'
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx)
return { ...initialProps }
}
props
render() {
return (
<html>
<Head>
<meta charSet="utf-8" />
<meta httpEquiv="x-ua-compatible" content="ie=edge, chrome=1" />
<meta name="renderer" content="webkit|ie-comp|ie-stand" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no,viewport-fit=cover"
/>
<meta name="keywords" content="Next.js demo" />
<meta name="description" content={'This is a next.js demo'} />
</Head>
<body>
<Main />
<NextScript />
</body>
</html>
)
}
}
特点4:浅路由
如果通过<Link href={href}></Link>
或者<a href={href}></a>
做路由跳转,那么,目标页面一定是全渲染,执行getInitialProps
钩子函数。
浅层路由允许改变 URL但是不执行getInitialProps
生命周期。可以加载相同页面的 URL,得到更新后的路由属性pathname
和query
,并不失去 state 状态。
因为浅路由不会执行服务端初始化数据函数,所以服务端返回HTML的速度加快,但是,返回的为空内容,不适合SEO。并且,你需要在浏览器钩子函数componentDidMount
中重新调用接口获得数据再次渲染内容区。
浅路由模式比较适合搜索页面,比如,每次的搜索接口都是按照keyword参数发生变化:
/search?keyword=a
到/search?keyword=b
使用方式如下:
const href = '/search?keyword=abc'
const as = href
Router.push(href, as, { shallow: true })
然后可以在componentdidupdate钩子函数中监听 URL 的变化。
componentDidUpdate(prevProps) {
const { pathname, query } = this.props.router
const { keyword } = router.query
if (keyword) {
this.setState({ value: keyword })
...
}
}
注意:
浅层路由只作用于相同 URL 的参数改变,比如我们假定有个其他路由about,而你向下面代码样运行:
Router.push('/?counter=10', '/about?counter=10', { shallow: true })
那么这将会出现新页面,即使我们加了浅层路由,但是它还是会卸载当前页,会加载新的页面并触发新页面的getInitialProps
。
Next.js踩坑记录
踩坑1:访问window和document对象时要小心!
window和document对象只有在浏览器环境中才存在。所以,如果直接在render函数或者getInitialProps函数中访问它们,会报错。
如果需要使用这些对象,在React的生命周期函数里调用,比如componentDidMount
componentDidMount() {
document.getElementById('body').addEventListener('scroll', function () {
...
})
}
踩坑2:集成antd
集成antd主要是加载CSS样式这块比较坑,还好官方已经给出解决方案,参考:https://github.com/zeit/next.js/tree/7.0.0-canary.8/examples/with-ant-design
多安装4个npm包:
"dependencies": {
"@zeit/next-css": "^1.0.1",
"antd": "^4.0.4",
"babel-plugin-import": "^1.13.0",
"null-loader": "^3.0.0",
},
然后,添加next.config.js
和 .babelrc
加载antd样式。具体配置参考上面官网给的例子。
踩坑3:接口鉴权
SPA项目中,接口一般都是在componentDidMount
中调用,然后根据数据渲染页面。而componentDidMount
是浏览器端可用的钩子函数。
到了SSR项目中,componentDidMount
不会被调用,这个点在踩坑1中已经提到。
SSR中,数据是提前获取,渲染HTML,然后将整个渲染好的HTML发送给浏览器,一次性渲染好。所以,当你在Next的钩子函数getInitialProps
中调用接口时,用户信息是不可知的!不可知!
- 如果用户已经登录,
getInitialProps
中调用接口时,会带上cookie信息 - 如果用户未登录,自然不会携带cookie
- 但是,用户到底有没有登录呢???
getInitialProps
中,你无法通过接口(比如getSession
之类的API)得知
要知道,用户是否登录,登录用户是否有权限,那必须在浏览器端有了用户操作之后才会发生变化。
这时,你只能在特定页面(如果只有某个页面的某个接口需要鉴权),或者在_app.js
这个全局组件上添加登录态判断:componentDidMount
中调用登录态接口,并根据当前用户状态做是否重定向到登录页的操作。
踩坑4:集成 typescript, sass, less 等等
都可以参考官网给出的Demo,例子十分丰富:https://github.com/zeit/next.js/tree/7.0.0-canary.8/examples
小结
Next.js的其他用法和React一样,比如组件封装,高阶函数等。
demo code: https://github.com/etianqq/next-app
网友评论