Next.js 踩坑
几乎一整年没咋写文章,主要是懒,加上工作也挺忙。但是想趁着年底发一篇,希望明年更勤奋一点。其实不是没东西写,就是想深入一个东西还是很困难的,要查各种资料,最终还是懒就是了。
next.js 是 react 的同构库,很多文章里把他当作一个脚手架,也不是不行,但是个人认为 next.js 比一般的脚手架能做的更多,但也有局限性。这两天下班回去实践了一下 nextjs 的开发。遇到一些坑,也有一些收获这里记录一下。
请求数据
nextjs 没有客户端的生命周期,只有一个静态方法 getInitialProps,所以获取接口数据也只能在这个方法里了。getInitialProps 的返回数据就作为该组件的 props。getInitialProps 有两个参数:req 和 res,也就是我们非常熟悉的 http 参数。说句题外话,现有的 node web 框架都是 req 作为输入,res 作为输出,增加各种中间件。
所以个人觉得 nextjs 的组件形式太适合无状态组件了。下面是一个简单的例子代码:
// 获取电影列表并渲染
class MovieList extends Component {
static async getInitialProps(){
const data = await getMovieList()
return {
list: data
}
}
render () {
return (
<div>
{this.props.list.map(movie => {
<MovieCard key={movie.id} movie={movie}>
})}
</div>
)
}
}
当然这里最终仅仅是服务端输出的列表,我们可能还会有其他操作,比如删除加载下一页之类的,但是这些操作都没必要在服务端操作的。添加几个相应的方法即可。
路由管理
nextjs 的路由是基于文件系统的,相当清晰和简单,比如在 pages 文件夹下面增加一个 movie-detail 组件,并写上相应的代码,我们就可以访问 /movie-detail 这个路由了。起初觉得这样的路由形式实在太优雅了,但是用久了就会发现很多问题。
路由嵌套
首先是嵌套路由,比如我想建立 /user/profile 这个路由,这个其实很好解决,就是在 pages 文件夹下面依次嵌套就行了:
具名路由
其次是没有官方实现具名路径,什么是具名路径呢?就是 /movie/:id 这里这种形式,个人感觉 nextjs 在这方面是追随 react-router4 的。vuejs 的同构框架 nuxtjs 则不存在这个问题,因为 vue-router 本身也是统一管理路由的。先不说这种情况的好坏,还是找找解决方案吧。
根据我找到的实例和文档,目前有两种解决方案:
使用 query 代替具名路
下图可以看到其实在 nextjs router 里 query 是存在的。
那我们需要访问具名路由页面的时候可以这么写, 将 id 用 query 传过去 /movie-detail?id=xxx:
// 电影详情页面
class MovieDetail extends Component {
static async getInitialProps({req}) {
const {id} = req.query
const detail = await getDetail(id)
return {
detail
}
}
render () {
return (
// do anything you want
)
}
}
custom server 解决
使用 query 传参数过去确实可以解决问题,但是太不优雅,与 rest 的思想也不太符合。所以 next 社区找到了另一个解决方案,使用 custom server。
在说具体方案之前我们我们可以了解一下,说到底 nextjs 并不是一个生成静态资源的脚手架,next 最终还是要单独部署 node 服务的。也就是 nextjs 其实内置了一个 http 服务,如果我们不使用 custom sever 的话,内置服务还是可以很好的帮我们完成渲染页面的任务。
但是如果我们的 node 不仅仅是渲染页面,还需要写接口。那么这时候的情况就很类似传统后端的开发模式了:不仅仅需要写接口还需要渲染页面。
很显然 nextjs 的内置 http 服务是无法完成这个任务的,我们需要更加完善的 web 框架。毕竟专业的事还是交给专业的。这时候就是 custom server 大显身手的时候了。nextjs 里也有一系列的例子:
那么 custom server 是如何解决具名路径的问题的呢?我们是借用 nextjs 的渲染能力。这里以 express 为例,具体代码如下:
// server.js
const express = require(‘express’)
const next = require(‘next’)
const dev = process.env.NODE_ENV !== ‘production’
const app = next({dev, quiet: false})
const handle = app.getRequestHandler()
const SERVE_PORT = process.env.SERVE_PORT || 8001
app.prepare().then(() => {
const server = express()
server.get(‘/movie-detail/:id’, async (req, res) => {
// 渲染 movie-detail 这个组件
const html = await app.renderToHTML(req, res, ‘/movie-detail’, req.query)
res.send(html)
})
server.get(‘*’, (req, res) => handle(req, res))
server.listen(SERVE_PORT, err => {
if (err) throw err
console.log(`> Ready on http://localhost:${SERVE_PORT}`)
})
})
上面是 server.js 的简略代码,当然在组件里我们也要做相应处理,代码如下
// /pages/movie-detail.jsx
// 电影详情页面
class MovieDetail extends Component {
static async getInitialProps({req}) {
const {id} = req.params
const detail = await getDetail(id)
return {
detail,
id
}
}
render () {
return (
// do anything you want
)
}
}
页面缓存
对于 csr 的的 react 应用来说,渲染耗时 100ms 并不是什么太大问题,但是到了服务端,100ms 很明显是没法忍受的。首先客户端渲染并不会造成服务器资源的浪费,其实也不会对服务器造成太大鸭梨。但是服务端就不一样了。一旦用户量大了,势必会引起各种问题,所以页面缓存还是很有必要的。
具体页面缓存在哪里并不是我们考量的范围,同样页面缓存也需要用到 custom server,具体服务端框架自定吧。这里以 lru-cache 为例做一个简单的页面缓存,其实换成其他的诸如 redis 也是没有任何问题的。
const dev = process.env.NODE_ENV !== ‘production’
const next = require(‘next’)
const express = require(‘express’)
const LRUCache = require(‘lru-cache’)
const ssrCache = new LRUCache({
max: 1000, // cache item count
maxAge: 1000 * 60 * 60, // 1 hour
})
const app = next({dev, quiet: false})
const handle = app.getRequestHandler()
const SERVE_PORT = process.env.SERVE_PORT || 8001
app.prepare().then(() => {
const server = express()
server.get(‘/’, async (req, res) => {
renderAndCache(req, res, ‘/’, { …req.query})
})
server.get(‘/movie-detail/:id’, async (req, res) => {
renderAndCache(req, res, ‘/movie-detail’, { …req.query})
})
server.get(‘*’, (req, res) => handle(req, res))
server.listen(SERVE_PORT, err => {
if (err) throw err
console.log(`> Ready on http://localhost:${SERVE_PORT}`)
})
})
const getCacheKey = req => `${req.url}`
// 缓存并渲染页面,具体是重新渲染还是使用缓存
async function renderAndCache(req, res, pagePath, queryParams) {
const key = getCacheKey(req)
if (ssrCache.has(key)) {
res.setHeader(‘x-cache’, ‘HIT’)
res.send(ssrCache.get(key))
return
}
try {
const html = await app.renderToHTML(req, res, pagePath, queryParams)
// Something is wrong with the request, let’s skip the cache
if (res.statusCode !== 200) {
res.send(html)
return
}
// Let’s cache this page
ssrCache.set(key, html)
res.setHeader(‘x-cache’, ‘MISS’)
res.send(html)
} catch (err) {
app.renderError(err, req, res, pagePath, queryParams)
}
}
其中 renderAndCache 是关键。这里判断页面是否有缓存,如果有的话则直出缓存内容。否则的话就重新渲染。至于缓存时间还有缓存大小看个人设置了,这里不赘述了。
部署上线
部署上线这一块实在没什么好说的,简单的话直接起一个 node 服务的就可以,复杂一点就要包括报警重启等等,都是看个人情况的。
个人习惯使用 supervisor 启动 node 服务。
总结
说了上面那么多,其实官方文档里都有相关例子,就当我的个人踩坑记录吧。
对于 nextjs 来说,我认为如果是展示型的应用,就应该放心大胆的用起来。不光开发快还爽,同时屏蔽 webpack 配置,有什么理由不用?
如果是功能性的,比如一系列的绘图组件则完成没必要使用了,对于 canvas 之类的还是必须用客户端渲染,然而 nextjs 又没有生命周期,用 nextjs 可能会相当坑。
对于个人开发这我则是相当推荐。何必去配置 webpack 浪费生命啊。
如果是完全静态的应用,我推荐 gatsbyjs。具体怎么使用则是另外一个话题了。
如有谬误,轻点喷。over