React服务端渲染-next.js

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目录下添加一个动态目录即可,如下图: image 方式二:自定义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 { static async getInitialProps(ctx) { const { req } = ctx try { await AppModal.effects.getAppList(req) } catch (e) { CommonModel.actions.setError(e, req) } } public render() { return } } 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 ( ) } } _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 (
) } } 特点4:浅路由 如果通过或者做路由跳转,那么,目标页面一定是全渲染,执行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

本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):