Next.js踩坑記錄

_xiadd_發表於2019-02-02

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

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資料夾下面依次巢狀就行了:

image.png

具名路由

其次是沒有官方實現具名路徑,什麼是具名路徑呢?就是/movie/:id這裡這種形式,個人感覺nextjs在這方面是追隨react-router4的。vuejs的同構框架nuxtjs則不存在這個問題,因為vue-router本身也是統一管理路由的。先不說這種情況的好壞,還是找找解決方案吧。

根據我找到的例項和文件,目前有兩種解決方案:

使用query代替具名路

下圖可以看到其實在nextjs router裡query是存在的。

image.png

那我們需要訪問具名路由頁面的時候可以這麼寫, 將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裡也有一系列的例子:

image.png

那麼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

相關文章