Next.js 是一个轻量级的 React 服务端渲染应用框架。
渲染方式
服务端渲染指的是页面的渲染和生成由服务器来完成,并将渲染好的页面返回客户端进行展示,例如: ASP、JSP、Smarty。
客户端渲染指的是页面的生成和数据的渲染过程是在客户端(浏览器或APP)执行JavaScript代码完成展示。例如: Angular、React、Vue。
Web页面的渲染方式从之前的服务端渲染到后续的客户端渲染SPA应用,解耦了前后端开发,使得前端专注于用户UI层,提升用户体验,让后端专注于业务逻辑处理,分离成微服务等。然而SPA对于SEO的支持非常之不友好,对于需要SEO的网站,SPA就不是一个很好的选择。所幸的是 React、Vue 都开始支持服务端渲染(同构)。
同构
首屏的渲染交给服务端渲染,提升渲染速度。首屏加载之后,进入页面内部的路由交给客户端控制,不再经过服务端,降低服务器压力。目前基于React的NextJS以及基于Vue的NuxtJs都是采用这样的方式。(本文只介绍 NextJS )
NextJS
特性:
默认服务端渲染模式,以文件系统为基础的客户端路由
代码自动分隔使页面加载更快
以webpack的热替换为基础的开发环境
使用React的JSX和ES6的module,模块化和维护更方便
可以运行在Koa和其他Node.js的HTTP 服务器上
可以定制化专属的babel和webpack配置
预置 CSS-in-JS 方案
构建项目
推荐使用官方(非fb)提供的脚手架工具 create-next-app,直接构建。当然可以手动配置 create-react-app构建出来的项目。详见
结构目录:
|- components/* // 组件文件夹
|- pages/* // 页面文件夹
|- static/* // 静态资源文件夹
|- .gitignore
|- package.json
开发
NextJS是根据pages目录下的文件名来确定页面的路由(支持文件夹嵌套),因此pages目录是只能放置页面代码,不能放置其他代码的。而static目录是项目的静态文件目录,可以放置图片资源,并且Next也帮助我们配置 /static
作为引入资源的根路径。(emmmmm,限制有点多,所以初始化的目录,需要增添点~)
|- components/*
|- lib/* // 资源库文件夹(自写或者不能npm install的包)
|- pages/*
|- server/* // 自定义服务端代码
|- static/*
|- store/* // redux
|- style/* // 如果不需要官方提供的css-in-js方案,可自定义其他方案(css-module或者styled-components),放置样式
|- .gitignore
|- package.json
|- next.config.js // next主配置文件
配置好目录之后,就是和一般的react项目一样进行开发就完事了,推荐使用 hooks
一些坑
- css-in-js 不支持除官方已经使用的
style-jsx
之外的多种css-in-js方案合集
比如不支持同时使用css-module
和styled-components
,否则可能会在页面中使用Link前端跳转页面失败。当然如果不需要保存页面状态,可以直接使用 a 标签跳转,没有此问题。详情可见
- 异步组件无法使用 ref 获取组件实例
因为NextJS是每个页面都可能在服务端渲染的,所以对于一些使用了浏览器对象(如: window、document等)的JavaScript库,不能直接引入。幸好的是NextJS提供了dynamic
引入,选择No SSR
引入使用此类库的子组件,可以解决此类的问题。当然万事有利有弊,使用此类方式引入的子组件就不能被父组件通过 ref 获取到子组件的实例,从而不能在父组件调用子组件的任何方法,因为异步组件是一个LoadableComponent
详见 react 源码
- 自定义node服务,如果使用
Koa-router
不支持使用,router.get/post
此类方式编写接口,需使用中间件,传递ctx
,进而判断path
// api.js
const api = (server) => {
server.use(async (ctx, next) => {
const { path, method } = ctx;
if (path.startWith('xxxx')) { // 满足接口的path条件
// do something
} else {
await next();
}
})
}
// server.js
const server = new Koa();
api(server)
当然如果不想在同一个项目里面写接口和渲染的话,可以另起Api接口项目,使用node服务转发。主要通过http-proxy-middleware
和koa-connect
。需要注意的是http-proxy-middleware
转发接口,如果是上传资源之类的接口,当接口持续时间过长没有返回接口,代理会被断开,导致接收不到响应。但是线上推荐使用nginx
。
const Koa = require('koa');
const next = require('next');
const c2k = require('koa-connect');
const proxyMiddleware = require('http-proxy-middleware');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
const devProxy = {
target: '转发接口地址',
changeOrigin: true,
secure: false,
onError(err, req, res) {
res.writeHead(500, {
'Content-Type': 'text/plain',
});
res.end(
'Something went wrong. And we are reporting a custom error message.',
);
},
};
app.prepare().then(() => {
const server = new Koa();
// 开发环境代理接口,线上推荐使用nginx
if (dev && devProxy) {
server.use(async (ctx, nextKoa) => {
if (ctx.url.startsWith('/api')) { // 以api开头的请求全部转发
// ctx.respond = false;
await c2k(proxyMiddleware(devProxy))(ctx, next);
} else {
await nextKoa();
}
});
}
// 中间件
server.use(async (ctx) => {
// 传入的是nodeJS原生的req、res达到兼容更多的node框架
const { req, res } = ctx;
await handle(req, res);
ctx.respond = false;
});
server.listen(8888, () => {
console.log('koa server listen on port 8888'); // eslint-disable-line
});
});
const mapStateToProps = (state) => {
return {
reduxProp: state['模块名']['需要获取redux的该模块下的某一字段的key'],
};
};
- static 文件夹存放的图片资源压缩问题
在开发SPA应用的时候,经常会使用 webpack 进行打包编译,NextJS也是。但是不同的是: NextJS规定了静态文件路由 static,打包编译过程中不会处理这些文件,且打包之后启动服务同样使用的 static 文件。所以在图片压缩这个上面, NextJS 可能无法做到像一般的SPA一样通过 loader 处理。笔者使用的方式是通过编写一个 node 压缩图片脚本,把图片输出到 static 目录使用。主要使用imagemin
、imagemin-jpegtran
、imagemin-pngquant
三个来自于image-webpack-loader
的依赖。
- getInitialProps 生命周期钩子
NextJS提供了一个很强大的生命周期钩子: getInitialProps
,会在服务端渲染和客户端渲染的时候都执行,但是只要服务端执行了客户端就不会执行。所以一般用来当做请求数据的钩子使用。
有些时候我们可能需要对一些数据随机的进行展示,两种可能采用的方式:
- 在
getInitialProps
中返回数据到组件的 props 中,然后再随机取值,渲染 - 在
getInitialProps
中返回随机去的数据到组件的 props 中,然后再渲染
需要注意的是一旦采用了函数式组件的编写模式,方式1是不可取的,因为一旦当前页面被当做首屏,进行服务端渲染,服务端是会执行getInitialProps
以及组件函数,浏览器渲染的时候不会执行getInitialProps
,但是同样会执行组件函数,导致前后端数据存在不一致的可能就会发生。因此推荐第二种方式,数据的处理统一放置于getInitialProps
。
- getInitialProps 钩子发起请求
使用 SSR 的时候,一般希望进入页面就有数据,那么请求数据一定就要在进入页面之前。所以 getInitialProps
钩子就必不可少,但是需要注意的是 getInitialProps
的两端执行特性,在服务端执行的时候,如果直接使用接口路由地址,则会出现找不到地址的情况,必须使用完整的接口地址: 域名 / ip + 端口 + 路由 的方式,然而客户端请求的时候是不需要这样的,并且一旦客户端使用这样完整接口地址方式,如果后端没有针对跨域进行配置,则会出现跨域请求。因此需要针对此种情况进行兼容处理。
// pages 文件夹下 页面 Jsx
Component.getInitialProps = () => {
let requestURL = '/api/xxx';
if (typeof window === 'undefined') { // 判断是否是服务端渲染
requestURL = `http://127.0.0.1:3000${requestURL}`
}
// 发起请求。当然推荐使用 axios 使用baseURL处理
// ...
}
文章所用NextJS版本为9.1.1~
# 框架