用Next.js快速上手React服務器渲染

新前端時代的服務端渲染也提了好久了,各類技術演進層出不窮。css

帶你領略一下基於React和Nodejs架構的服務端渲染技術,讓你快速上手,開發Next服務端渲染的項目。前端

本文參考官方文檔,作精簡總結,用一些小例子,讓你快速理解Next.js項目的開發。node

全文3800字左右,閱讀全文約15分鐘。react

 

本文同步發表於知乎專欄:前端微志。歡迎關注。webpack

什麼是服務端渲染?

 

服務端渲染,是指頁面的渲染和生成由服務器來完成,並將渲染好的頁面返回客戶端。而客戶端渲染是頁面的生成和數據的渲染過程是在客戶端(瀏覽器或APP)完成。ios

隨着先後端分離模式的興起,從服務端渲染到服務端渲染的演進,前端Web App給前端開發帶來了很大的便利,並減輕後端服務器壓力。web

先後端解耦,讓前端專一作好用戶UI層,專一於提高用戶體驗,讓後端專一於業務邏輯處理,分離成微服務,專心作好一件事。ajax

先後端分離前的服務端渲染技術有:PHP,ASP,JSP等方式,分離後的前端SPA(單頁面應用)渲染擁有獨立的路由和頁面渲染(React,Vue和Angular等),而SPA的最大問題是對SEO不友好,當項目對SEO有需求時,SPA就不是一個好的選擇。express

近兩年來,React和Vue也開始支持服務端渲染(Server Side Render ),行業內也有這方面的實踐,其中掘金就使用Vue的SSR功能作了全站服務端渲染,且效果良好。npm

咱們如今介紹一個React生態中表現突出的服務端渲染框架:Next.js。

Next.js是什麼?它有什麼優勢?

 

 

 

 

Next.js是一個基於React的一個服務端渲染簡約框架。它使用React語法,能夠很好的實現代碼的模塊化,有利於代碼的開發和維護。

Next.js帶來了不少好的特性:

  • 默認服務端渲染模式,以文件系統爲基礎的客戶端路由

  • 代碼自動分隔使頁面加載更快

  • (以頁面爲基礎的)簡潔的客戶端路由

  • 以webpack的熱替換爲基礎的開發環境

  • 使用React的JSX和ES6的module,模塊化和維護更方便

  • 能夠運行在Express和其餘Node.js的HTTP 服務器上

  • 能夠定製化專屬的babel和webpack配置

怎麼開始構建一個Next.js項目?

 

 

在開始構建Next.js項目以前,須要作好一些準備:

  1. 首先,無論你使用哪一個操做系統,你須要一個趁手的命令行工具,在Mac系統和Linux下自帶的命令行工具比較好用,在Windows系統下,我推薦一個命令行工具:Cmder;

  2. 已經在本地安裝好Nodejs和Npm;

  3. 熟悉React技術棧開發及ES6語法;

  4. 熟悉Express架構的Nodejs開發。

下面開始構建一個Next.js項目。

建立項目

在命令行工具中依次執行下面語句:

// 在本地建立一個項目跟目錄
$ mkdir hello-next 

// 切換到項目根目錄
$ cd hello-next

// 用npm初始化項目
$ npm init -y

// 將react和next安裝到本地依賴
$ npm install --save react react-dom next

// 建立文件夾 pages
$ mkdir pages

建立完文件夾以後,打開hello-next文件下的package.json文件,在 scripts 下添加一個script,以下:

{
  "scripts": {
    "dev": "next"
  }
}

至此,項目準備工做完成,運行下面這條命令開啓項目服務器:

$ npm run dev

命令執行完畢後,在瀏覽器中打開 http://localhost:3000,你會看到這樣的頁面:

這個頁面是Next.js默認生成的404頁面,而開啓服務後訪問的之因此是404頁面,是由於咱們尚未設置項目主頁。

建立頁面

Next.js是從服務器生成頁面,再返回給前端展現。Next.js默認從 pages 目錄下取頁面進行渲染返回給前端展現,並默認取 pages/index.js 做爲系統的首頁進行展現。注意,pages 是默認存放頁面的目錄,路由根路徑也是pages目錄。

在 pages/index.js 中建立一個React函數式組件:

const Index = () => (
  <div>
    <p>Hello Next.js</p>
  </div>
)

export default Index

Next.js默認使用Webpack構建項目,webpack的熱部署功能同樣能提高開發效率。建立完 pages/index.js 後,再訪問 http://localhost:3000 便可看到設置好的頁面。

多頁面構建

使用Next.js的目的就是構建非SPA的多頁面項目。下面開始構建多頁面應用,建立第二個頁面。

在 pages 目錄下建立文件 pages/about.js :

export default () => (
  <div>
    <p>This is the about page</p>
  </div>
)

建立完以後,能夠經過 http://localhost:3000/about 訪問該頁面。至此,全部的頁面的路由都是經過後端服務器來控制的,要想實現客戶端路由,須要藉助Next.js的Link API

Link API

從 next/link中能夠引用到 Link 組件。在pages/index.js文件中引用Link,修改以下:

// This is the Link API
import Link from 'next/link'

const Index = () => (
  <div>
    <Link href="/about">
      <a>About Page</a>
    </Link>
    <p>Hello Next.js</p>
  </div>
)

export default Index

在瀏覽器中訪問首頁 http://localhost:3000 時,點擊 About Page便可跳轉到about頁面

Link組件是經過location.history的瀏覽器API保存歷史路由,因此,你能夠經過瀏覽器左上角的前進後退按鈕來切換歷史路由。而在開發過程當中,你不須要再單獨寫客戶端路由的配置。

Link組件是React的高階組件的實現,不能對它進行樣式的設置,它只是起到路由的跳轉功能,可是它的複用性強,只要包含一個能觸發onClick事件的組件便可。

組件複用

Next.js是以多頁面爲中心,只要將頁面文件放在pages目錄下,就能夠在瀏覽器上以文件名爲路由名來訪問到。

組件的設置跟React同樣,經過export導出,經過import導入。通常,只要不想讓用戶經過頁面直接訪問的組件,都不放在pages目錄下。對除了pages目錄,組件放在哪一個目錄下沒有要求,開發者能夠自定義設置。

下面再 components 目錄下,建立一個公用組件 Header,用於各個文件的頭部導航,經過導航能夠在頁面見切換。

import Link from 'next/link'

const linkStyle = {
  marginRight: 15
}

const Header = () => (
  <div>
    <Link href="/">
      <a style={linkStyle}>Home</a>
    </Link>
    <Link href="/about">
      <a style={linkStyle}>About</a>
    </Link>
  </div>
)

export default Header

在 pages/index.js 中引入Header:

import Header from '../components/Header'

export default () => (
  <div>
    <Header />
    <p>Hello Next.js</p>
  </div>
)

在 pages/about.js 中一樣引入 Header 組件,在瀏覽器上經過點擊導航切換頁面。

 

進一步封裝Header組件,建立一個自動包含Header和Content的組件 components/MyLayout.js:

import Header from './Header'

const layoutStyle = {
  margin: 20,
  padding: 20,
  border: '1px solid #DDD'
}

const Layout = (props) => (
  <div style={layoutStyle}>
    <Header />
    {props.children}
  </div>
)

export default Layout

建立動態頁面

使用Next.js建立動態頁面,與使用React或Vue建立一個SPA頁面大致相同,惟一的區別就是頁面的渲染主體不一樣,前者是Nodejs服務器獲取到後端數據渲染完頁面後再返回給前端展現,後者是前端先獲取頁面主體架構,再經過ajax的方式請求後端的數據,在前端渲染展現。

以一個簡易的博客頁面爲例,建立博客列表頁,修改 pages/index.js:

import Layout from '../components/MyLayout.js'

import Link from 'next/link'

const PostLink = (props) => (
  <li>
    <Link href={`/post?title=${props.title}`}>
      <a>{props.title}</a>
    </Link>
  </li>
)

export default () => (
  <Layout>
    <h1>My Blog</h1>
    <ul>
      <PostLink title="Hello Next.js"/>
      <PostLink title="Learn Next.js is awesome"/>
      <PostLink title="Deploy apps with Zeit"/>
    </ul>
  </Layout>
)

訪問 http://localhost:3000/ 訪問,頁面以下:

有了列表頁,須要再寫一個博客的詳情頁,從上面的代碼中也可看到,咱們須要建立一個 pages/post.js 文件:

import Layout from '../components/MyLayout.js'

export default (props) => (
  <Layout>
    <h1>{props.url.query.title}</h1>
    <p>This is the blog post content.</p>
  </Layout>
)

在博文列表頁,點擊博文名可跳轉到對應博文的詳情頁。

 

用路由遮蓋(Route Masking)的乾淨的URL

Next.js上提供了一個獨特的特性:路由遮蓋(Route Masking)。它可使得在瀏覽器上顯示的是路由A,而App內部真正的路由是B。這個特性可讓咱們來設置一些比較簡潔的路由顯示在頁面,而系統背後是使用一個帶參數的路由。好比上面的例子中,地址欄中顯示的是 http://localhost:3000/post?title=Hello%20Next.js ,這個地址含有一個title參數,看着很不整潔。下面咱們就用Next.js來改造路由,使用路由遮蓋來建立一個更加簡潔的路由地址。好比咱們將該地址改形成 http://localhost:3000/p/hello-nextjs。

首先咱們要修改 pages/index.js 下的PostLink組件,會使用到 next/link 組件的 as 屬性,並給組件添加一個屬性 id:

import Layout from '../components/MyLayout.js'

import Link from 'next/link'

const PostLink = (props) => (
  <li>
    <Link as={`/p/${props.id}`} 
      href={`/post?title=${props.title}`}>
      <a>{props.title}</a>
    </Link>
  </li>
)

export default () => (
  <Layout>
    <h1>My Blog</h1>
    <ul>
      <PostLink id="hello-nextjs" 
        title="Hello Next.js"/>
      <PostLink id="learn-nextjs" 
        title="Learn Next.js is awesome"/>
      <PostLink id="deploy-nextjs" 
        title="Deploy apps with Zeit"/>
    </ul>
  </Layout>
)

當在 Link 組件上使用 as 屬性時,瀏覽器上顯示的是 as 屬性的值,走的是客戶端路由,而服務器真正映射的是 href 屬性的值,走的是服務端路由。

這樣就會有一個問題,若是在前端路由間切換不會有問題,能夠正常顯示,可是在頁面 http://localhost:3000/p/hello-nextjs 時刷新頁面,會顯示 404頁面。這是由於路由遮蓋默認只在客戶端路由中有效,要想在服務端也支持路由遮蓋,須要在服務端單獨設置路由解析的方法。

服務端支持路由遮蓋

上面說到,服務器默認不支持路由遮蓋,要讓服務器支持它,須要單獨對路由進行設置。下面以 Express (你也可使用Koa等其餘Nodejs的Web服務器框架)建立後端服務器講解如何設置服務器來支持路由遮蓋

首先須要將 express 安裝到項目依賴中:

$ npm install --save express

在項目目錄下建立 server.js ,添加內容以下:

const express = require('express')
const next = require('next')
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare().then(() => {
  const server = express()

  server.get('/p/:id', (req, res) => {
    const actualPage = '/post'
    const queryParams = { 
        title: req.params.id 
    } 
    app.render(req, res, actualPage, queryParams)
  })

  server.get('*', (req, res) => {
    return handle(req, res)
  })

  server.listen(3000, (err) => {
    if (err) throw err
    console.log('> Ready on http://localhost:3000')
  })
}).catch((ex) => {
  console.error(ex.stack)
  process.exit(1)
})

並更新 package.json 文件中的 scripts :

{
  "scripts": {
    "dev": "node server.js"
  }
}

這時候,服務器已經能夠支持路由遮蓋了,在顯示遮蓋路由的頁面,刷新頁面也能夠正常顯示內容。具體的實現是在服務器中對 /p/* 開頭的路由進行重寫,而後重定向到 /post 開頭的路由上,最後將內容返回給前端。具體代碼是這一段:

server.get('/p/:id', (req, res) => {
  const actualPage = '/post'
  const queryParams = { 
      title: req.params.id 
  } 
  app.render(req, res, actualPage, queryParams)
})

請求接口,獲取數據

Next.js 在 React 的基礎上爲組件添加了一個新的特性: getInitialProps (有點像是getInitialState),它用於獲取並處理組件的屬性,返回組件的默認屬性。咱們能夠在改方法中請求數據,獲取頁面須要的數據並渲染返回給前端頁面。

引入一個支持在客戶端和服務器端發送 fetch 請求的插件 isomorphic-unfetch,固然你也可使用 axios 等其餘工具。

$ npm install --save isomorphic-unfetch

而後修改 pages/index.js 裏的內容,換成下面這樣:

import Layout from '../components/MyLayout.js'
import Link from 'next/link'
import fetch from 'isomorphic-unfetch'

const Index = (props) => (
  <Layout>
    <h1>Batman TV Shows</h1>
    <ul>
      {props.shows.map(({show}) => (
        <li key={show.id}>
          <Link as={`/p/${show.id}`} 
            href={`/post?id=${show.id}`}>
            <a>{show.name}</a>
          </Link>
        </li>
      ))}
    </ul>
  </Layout>
)

Index.getInitialProps = async function() {
  const res = await fetch('https://api.tvmaze.com/search/shows?q=batman')
  const data = await res.json()

  console.log(`Show data fetched. Count: ${data.length}`)

  return {
    shows: data
  }
}

export default Index

上述代碼中,在 getInitialProps 中使用了 async 和 await 來處理異步請求,並將取到的數據當作一個屬性賦給頁面,頁面拿到這個屬性的值後會用於頁面的初始化渲染。頁面展現效果以下圖:

 

樣式化組件

Next.js 提供了一個 css-in-js 的特性,它容許你在組件內部寫一些樣式,你只須要在組件內使用 <style jsx> 標籤來寫 css 便可。舉個例子,好比咱們在 pages/index.js 裏添加樣式:

import Layout from '../components/MyLayout.js'

import Link from 'next/link'

function getPosts () {
  return [
    { 
      id: 'hello-nextjs', 
      title: 'Hello Next.js'
    },
    { 
      id: 'learn-nextjs', 
      title: 'Learn Next.js is awesome'
    },
    { 
      id: 'deploy-nextjs', 
      title: 'Deploy apps with ZEIT'
    }
  ]
}

export default () => (
  <Layout>
    <h1>My Blog</h1>
    <ul>
      {getPosts().map((post) => (
        <li key={post.id}>
          <Link as={`/p/${post.id}`} 
            href={`/post?title=${post.title}`}>
            <a>{post.title}</a>
          </Link>
        </li>
      ))}
    </ul>
    <style jsx>{`
      h1, a {
        font-family: "Arial";
      }
      ul {
        padding: 0;
      }
      li {
        list-style: none;
        margin: 5px 0;
      }
      a {
        text-decoration: none;
        color: blue;
      }
      a:hover {
        opacity: 0.6;
      }
    `}</style>
  </Layout>
)

在上述代碼中,咱們沒有直接使用 <style> 標籤來書寫樣式代碼,而是寫在一個模板字符串({``})裏面。Next.js 使用 babel插件來解析 styled-jsx ,它支持樣式命名空間,將來還將支持變量賦值。

須要注意的是:styled-jsx 的樣式不會應用到子組件,若是想要該樣式適用於子組件,能夠在 styled-jsx 標籤添加屬性 global:<style jsx global>。

怎麼部署一個next.js項目

Next.js 項目的部署,須要一個 Node.js的服務器,能夠選擇 Express, Koa 或其餘 Nodejs 的Web服務器。本文中以 Express 爲例來部署 Next 項目。

服務器的入口文件就使用上文中提到的 server.js,在 server.js 裏添加了針對部署環境的選擇,代碼以下:

const dev = process.env.NODE_ENV !== 'production'

爲了區分部署環境,咱們須要在 package.json 中修改 script 屬性以下:

 

"scripts": {
  "build": "next build",
  "start": "NODE_ENV=production node server.js -",
  "dev": "NODE_ENV=dev node server.js"
}

其中,build 命令是用於打包項目,start 命令是用於生產環境部署,dev 命令是用於本地開發。

執行以下命令便可將 Next項目 部署到服務器:

$ npm run build
$ npm run start

執行完命令後,可在 http://host:3000 訪問。其中,host 是指服務器的IP地址。

相關文章
相關標籤/搜索