幾乎一整年沒咋寫文章,主要是懶,加上工作也挺忙。但是想趁著年底發一篇,希望明年更勤奮一點。其實不是沒東西寫,就是想深入一個東西還是很困難的,要查各種資料,最終還是懶就是了。
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