最簡單的服務端渲染框架-Next.js快速入門
簡介
Next.js是一個用於React應用的極簡的服務端渲染框架。框架中集成了Webpack,Babel等一系列React相關的工具並進行了默認的配置。因此省去了複雜的配置過程,實現了一鍵搭建開發環境和打包構建。同時提供了自定義配置介面,可以在默認配置的基礎上對工具進行自定義配置,滿足個性化需求。
基本用法
安裝
使用npm安裝: npm install next --save
為了方便的使用next提供的命令,把命令寫在package.json文件的scripts中:
{n "scripts": {n "dev": "next", // 運行開發伺服器,並監控源代碼,具備hod reload功能n "build": "next build", // 以生產模式打包代碼n "start": "next start" // 啟動Next伺服器,可以自定義伺服器和埠n "init": "next init" // 初始化項目,創建基礎的文件夾和index頁面文件n }n}n
之後,在項目的根目錄下創建pages文件夾和static文件夾,分別用來放對應的頁面資源和靜態資源。
Note:也可以使用npm run init命令自動生成。
運行
如果使用npm run init命令的話,現在pages文件夾下已經有了index.js文件,如果是手動創建pages文件夾的話,現在在該文件下創建一個index.js文件,內容為:
export default () => <p>Hello, world</p>n
接著執行npm run dev命令並在瀏覽器中打開http://localhost:3000。
現在,就得到了一個採用服務端渲染的極簡React應用,這個應用還實現了自動代碼分割,保證每個頁面只會載入自身的依賴,不會有依賴冗餘。
Next的核心就是pages和static文件夾。其中pages文件夾用於存放每個頁面的頂層組件,static用於存放項目中的靜態資源。
Next會將pages中的文件結構自動映射為對應的路由結構,例如現在該文件夾下有兩個文件:pages/index.js和pages/about.js。則對應的路由分別為/和/about。並且支持多級目錄,例如page/foo/bar.js對應的路由為/foo/bar。
static文件夾用來存放靜態文件,例如現在有一個圖片文件static/image.png,使用的時候引用/static/image.png就可以了:
export default () => (n <img src="/static/mage.png" />n)n
打包完成後,Next會在項目根目錄生成一個.next文件夾,其中的兩個文件夾dist和bundles,dist文件夾中存放著編譯後的源代碼,用於服務端渲染。bunldes文件夾中存放著pages中每個頁面打包後的整體代碼的JSON格式。在應用的初始頁面,會使用dist文件夾中的代碼進行服務端渲染,而其他使用路由到達的頁面,則將bundles文件夾中的對應JSON格式的代碼返回客戶端執行渲染。
Next的出現大大簡化了React應用開發的配置和構建工作,使開發者能夠專註於組件的開發,而不需要在Webpack,Babel等工具上花費過多的精力。基於簡單的文件系統,就可以創建包含路由功能和服務端渲染的React應用。需要注意的是:創建的應用中只有初始頁面採用服務端渲染,其他通過路由操作到達的頁面均為客戶福渲染。
組件
Next對React組件的getInitialProps生命周期方法做了改造,傳入一個上下文對象,該對象在服務端渲染和客戶端渲染時,具有不同的屬性:
- req: HTTP請求對象(服務端渲染獨有)
- res: HTTP響應對象(服務端渲染獨有)
- pathname: URL中的路徑部分
- query:URL中的查詢字元串部分解析出的對象
- err:錯誤對象,如果在渲染時發生了錯誤
- xhr:XMLHttpRequest對象(客戶端渲染獨有)
因此,可以在組件的getInitialProps方法中處理上下文對象,控制傳入組件的props數據。例如:
import React from reactnexport default class extends React.Component {n static async getInitialProps ({ req }) {n return reqn ? { userAgent: req.headers[user-agent] }n : { userAgent: navigator.userAgent }n }n render () {n return <div>n Hello World {this.props.userAgent}n </div>n }n}n
上面的例子根據是否有req對象來判斷是服務端渲染還是客戶端渲染,然後採用對應的方式取得用戶代碼數據並傳入組件的props中。
獲取數據
組件的getInitialProps還可以用來獲取數據:
import React, { Component } from react;nimport isomorphic-fetch;nnexport default class extends Component {n static async getInitialProps() {n const res = await fetch(https://api.github.com/repos/zeit/next.js);n const json = await res.json();n return { n stars: json.stargazers_count n };n n }n n render() {n return <div>{this.props.stars}</div>n }n}n
需要注意的一點是,getInitialProps方法執行完畢之後,才會執行組件的render方法。這也就導致了如果網路狀況不佳的情況下,會出現長時間的等待。並且只有每個頁面的頂層組件的getInitialProps會被執行,所以想在子組件中獲取數據的話只能在其他生命周期函數例如componentDidMount配合組件的state實現:
export default class extends Component {n constructor(props) {n super(props);n this.state = {n stars: 0n }n }nn async componentDidMount() {n const res = await fetch(https://api.github.com/repos/zeit/next.js);n const json = await res.json();n this.setState({n stars: json.stargazers_countn });n }n n render() {n return <div>{this.state.stars}</div>n }n}n
CSS
NEXT組件中聲明CSS,目前主要有兩種方式:
- 內嵌CSS
- CSS-in-JS
內嵌(Built-in)CSS
Next採用的內嵌CSS方案是styled-jsx庫,也是Next所推薦的CSS聲明方式。優點是具有組件級的獨立作用域,避免了樣式污染問題。並且支持完整的CSS功能,如:hover等。
import React from reactnnexport default () => (n <div>n Hello worldn <p>scoped!</p>n <style jsx>{`n p {n color: blue;n }n div {n background: red;n }n div:hover {n background: blue;n }n @media (max-width: 600px) {n div {n background: blue;n }n }n `}</style>n </div>n)n
CSS-in-JS
Next支持多種CSS-in-JS方案,例如基本的在組件style屬性中寫樣式:
import React from reactnnexport default () => (n <div stylex={{color: red}}>n Hello worldn </div>n)n
還有其他的CSS-in-JS庫,可以根據自己的需要和喜好靈活選擇。
路由系統
Link組件
Next中提供了一個組件,用來實現路由功能。例如,我們的應用有兩個頁面:pages/index.js和pages/about.js,想要實現頁面跳轉,只需要:
// pages/index.jsnimport Link from next/linknexport default () => (n <div>Click <Link href="/about"><a>here</a></Link> to read more</div>n)n
// pages/about.jsnexport default () => (n <p>Welcome to About!</p>n)n
<Link>組件的工作流程和瀏覽器很相似:
- 獲取新的組件
- 如果新組件定義了getInitialProps,則獲取數據,如果發生錯誤,則渲染_error.js
- 步驟1,2完成之後,執行pushState並渲染新組件
每個頂層組件中還會傳入一個url對象,提供了幾個路由相關的方法:
- pathname:String-當前URL不包括查詢字元串的path部分
- query:Object-當前URL中查詢字元串解析成的對象
- back-後退
- push(url, as=url)-使用傳入的url(字元串)執行pushState操作
- replace(url, as=url)-使用傳入的url(字元串)執行replaceState操作 注意:push和replace方法中的第二個參數as為可選項,只有在服務端配置了自定義路由才有作用。
Router對象
除了使用<Link>組件之外,Next還提供了一個Router對象滿足命令式寫法的需要:
import Router from next/routernnexport default () => (n <div>Click <span onClick={() => Router.push(/about)}>here</span> to read more</div>n)n
與url對象相比,Router對象多了一個route屬性,值為當前的路由。 需要注意的是,Router對象中的屬性和方法僅可以在客戶端部分使用,服務端渲染的頁面無法使用,否則會報錯。
路由事件
Router對象還提供了三個路由事件方法:
- routeChangeStart(url) - 路由變化開始時觸發
- routeChangeComplete(url) - 路由變化完成時觸發
- routeChangeError(err, url) - 路由變化發生錯誤時觸發 如果使用Router.push(url, as)或相似的方法並傳入了as參數,則路由事件方法中的url參數值為as的值,否則,url參數的值是路由舔磚目標的URL
注意:與Router對象中其他的屬性和方法不同的是,這三個路由事件方法可以在服務端渲染的頁面使用。
監聽路由變化:
Router.onRouteChangeStart = (url) => {n console.log(App is changing to: , url)n}n
取消監聽:
Router.onRouteChangeStart = null;n
如果路由載入取消了(連續快速點擊兩個鏈接),就會觸發routeChangeError的回調,傳入的err參數中將包含一個cancelled屬性,值為true。
Router.onRouteChangeError = (err, url) => {n if (err.cancelled) {n console.log(`Route to ${url} was cancelled!`)n }n}n
預獲取頁面
Next提供了一個基於ServiceWorker實現的,具有預獲取頁面功能的模塊:next/prefetch。 使用預獲取功能,可以使APP預載入那些可能到達的頁面,提升網站的使用體驗和性能。當然,前提是你的瀏覽器必須支持ServiceWorker。並且預獲取功能只支持應用內的頁面,不支持外部鏈接。
<Link>組件
next/prefetch模塊也提供了一個具有預獲取功能的<Link>組件,代替路由系統中的<Link>組件,使用方法一致:
import Link from next/prefetchnnexport default () => (n <nav>n <ul>n <li><Link href=/><a>Home</a></Link></li>n <li><Link href=/about><a>About</a></Link></li>n <li><Link href=/contact><a>Contact</a></Link></li>n </ul>n </nav>n)n
此外預獲取功能可以精確控制到每個<Link>標籤,使用prefetch屬性來控制開關:
<Link href=/contact prefetch={false}><a>Home</a></Link>n
prefetch方法
和路由器一樣,預獲取模塊也提供了一個prefetch方法,用來方便命令式的寫法:
import { prefetch } from next/prefetchnexport default ({ url }) => (n <div>n <a onClick={ () => setTimeout(() => url.pushTo(/dynamic), 100) }>n 100ms後執行路由跳轉n </a>n {n 預獲取頁面n prefetch(/dynamic)n }n </div>n)n
自定義配置
如果默認的配置無法滿足需要的話,Next還提供了諸多的自定義配置介面,可以根據自己的需求靈活配置。
自定義伺服器和路由
默認的伺服器和路由系統可能無法滿足需要,比如,我需要把/a的路由解析到pages/b.js,把/b的路由解析到pages/a.js,此時,就需要通過自定義,手動控制頁面渲染來實現,在項目根目錄下創建server.js文件:
// server.jsnnconst { createServer } = require(http)nconst { parse } = require(url)nconst next = require(next)nnconst dev = process.env.NODE_ENV !== productionnconst app = next({ dev })nconst handle = app.getRequestHandler()nnapp.prepare().then(() => {n createServer((req, res) => {n const parsedUrl = parse(req.url, true)n const { pathname, query } = parsedUrlnn if (pathname === /a) {n app.render(req, res, /b, query)n } else if (pathname === /b) {n app.render(req, res, /a, query)n } else {n handle(req, res, parsedUrl)n }n })n .listen(3000, (err) => {n if (err) throw errn console.log(> Ready on http://localhost:3000)n })n})n
你可以選擇自己喜歡的服務端框架,express或者koa等,進行自定義。
自定義<head>
Next提供了<HEAD>組件,可以自定義頁面<head>標籤中的內容。每個組件都可以在內部自定義<head>的內容:
import Head from next/headnexport default () => (n <div>n <Head>n <title>My page title</title>n <meta name="viewport" content="initial-scale=1.0, width_=device-width" />n </Head>n <p>Hello world!</p>n </div>n)n
每個頁面組件只需要定義本頁面需要的<head>內容,並且對於相同的標籤,例如<title>。會按照組件渲染的順序,後定義的覆蓋先定義的內容。
自定義<Document>
在前面的例子中,服務端渲染時,所有的頁面我們只需要寫內容組件,這是因為使用了默認的<Document>模板。當然,可以自定義自己的服務端渲染模板。首先,創建pages/_document.js文件,寫上內容:
// pages/_document.jsnimport Document, { Head, Main, NextScript } from next/documentnnexport default class MyDocument extends Document {n static async getInitialProps (ctx) {n const props = await Document.getInitialProps(ctx)n return { ...props, customValue: hi there! }n }nn render () {n return (n <html>n <Head>n <style>{`body { margin: 0 } /* custom! */`}</style>n </Head>n <body className="custom_class">n {this.props.customValue}n <Main />n <NextScript />n </body>n </html>n )n }n}n
其中的ctx對象與其他組件中的getInitialProps方法中收到的參數一樣,只不過多了一個額外的方法:renderPage()。
自定義錯誤處理
Next中,有一個默認組件error.js,負責處理404或者500這種錯誤。當然,你也可以自定義一個_error.js組件覆蓋默認的錯誤處理組件:
// _error.jsnnimport React from reactnexport default class Error extends React.Component {n static getInitialProps ({ res, xhr }) {n const statusCode = res ? res.statusCode : (xhr ? xhr.status : null)n return { statusCode }n }nn render () {n return (n <p>{n this.props.statusCoden ? `An error ${this.props.statusCode} occurred on server`n : An error occurred on clientn }</p>n )n }n}n
自定義配置
相對Next進行自定義配置的話,可以在項目根目錄下創建一個next.config.js
// next.config.jsnnmodule.exports = {n /* 自定義配置 */n}n
自定義Webpack配置
在創建好的next.config.js文件中,可以擴展Webpack配置:
module.exports = {n webpack: (config, { dev }) => {n n // 修改config對象n n return confign }n}n
該函數接收默認的Webpack config對象作為參數,返回修改後的config對象。需要注意的是,next.config.js文件會被直接執行,因為只能使用本機安裝的Node.js所支持的JS語法。
警告:不建議在自定義Webpack配置中添加loader以支持新的文件類型!因為只有客戶端渲染的代碼會經過打包,而服務端執行的是源代碼,並沒有經過Webpack處理,因此新的loader對服務端渲染不起作用。所以最好是使用Babel插件來處理新的文件類型,因為無論是客戶端還是服務端渲染的代碼,都會經過Babel處理。
自定義Babel配置
自定義Babel配置,只需要在項目根目錄下創建.babelrc文件,因為自定義配置會覆蓋默認配置,而不是擴展默認配置。因此需要把next preset寫到.babelrc中。例如:
{n "presets": [n "next/babel", // Next默認配置n "stage-0"n ],n}n
部署
生產模式下,需要先使用生產模式構建代碼,再啟動伺服器。因此,需要兩條命令:
next buildnnext startn
Next官方推薦使用now作為部署工具,只要在package.json文件中寫入:
{n "name": "my-app",n "dependencies": {n "next": "latest"n },n "scripts": {n "dev": "next",n "build": "next build",n "start": "next start"n }n}n
接著運行now命令,就可以實現一鍵部署。
Reference
Next.js
README.md
推薦閱讀:
※深入Angular:組件(Component)動態載入
※Vue.js起手式+Vue小作品實戰
※Angular Articles 2017-01
※AntV - 我認為這是一個不嚴謹的錯誤