使用Next.js構建React服務端渲染應用

next.js簡介

最近在學React.js,React官方推薦使用next.js框架做爲構建服務端渲染的網站,因此今天來研究一下next.js的使用。css

next.js做爲一款輕量級的應用框架,主要用於構建靜態網站和後端渲染網站。前端

框架特色

  • 使用後端渲染
  • 自動進行代碼分割(code splitting),以得到更快的網頁加載速度
  • 簡潔的前端路由實現
  • 使用webpack進行構建,支持模塊熱更新(Hot Module Replacement)
  • 可與主流Node服務器進行對接(如express)
  • 可自定義babel和webpack的配置

使用方法

建立項目並初始化

mkdir server-rendered-website
cd server-rendered-website
npm init -y

安裝next.js

使用npm或者yarn安裝,由於是建立React應用,因此同時安裝react和react-domnode

npm:
npm install --save react react-dom next
yarn:
yarn add react react-dom next

在項目根目錄下添加文件夾pages(必定要命名爲pages,這是next的強制約定,否則會致使找不到頁面),而後在package.json文件裏面添加script用於啓動項目:react

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

以下圖imagewebpack

建立視圖

在pages文件夾下建立index.js文件,文件內容:git

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

export default Index

運行

npm run next

在瀏覽器中打開http://localhost:3000/,網頁顯示以下:
imagegithub

這樣就完成了一個最簡單的next網站。web

前端路由

next.js前端路由的使用方式很是簡單,咱們先增長一個page,叫about,內容以下:express

const About = () => (
    <div>
        <p>This is About page</p>
    </div>
)

export default About;

當咱們在瀏覽器中請求https://localhost:3000/about時,能夠看到頁面展現對應內容。(==這裏須要注意:請求url的path必須和page的文件名大小寫一致才能訪問,若是訪問localhost:3000/About的話是找不到about頁面的。==)npm

咱們可使用傳統的a標籤在頁面之間進行跳轉,但每跳轉一次,都須要去服務端請求一次。爲了增長頁面的訪問速度,推薦使用next.js的前端路由機制進行跳轉。

next.js使用next/link實現頁面之間的跳轉,用法以下:

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

這樣點擊index頁面的AboutPage連接就能跳轉到about頁面,而點擊瀏覽器的返回按鈕也是經過前端路由進行跳轉的。 官方文檔說用前端路由跳轉是不會有網絡請求的,實際會有一個對about.js文件的請求,而這個請求來自於頁面內動態插入的script標籤。可是about.js只會請求一次,以後再訪問是不會請求的,畢竟相同的script標籤是不會重複插入的。 可是想比於後端路由仍是大大節省了請求次數和網絡流量。前端路由和後端路由的請求對好比下:

前端路由:

image

後端路由:

image

Link標籤支持任意react組件做爲其子元素,不必定要用a標籤,只要該子元素能響應onClick事件,就像下面這樣:

<Link href="/about">
    <div>Go about page</div>
</Link>

Link標籤不支持添加style和className等屬性,若是要給連接增長樣式,須要在子元素上添加

<Link href="/about">
    <a className="about-link" style={{color:'#ff0000'}}>Go about page</a>
</Link>

Layout

所謂的layout就是就是給不一樣的頁面添加相同的header,footer,navbar等通用的部分,同時又不須要寫重複的代碼。在next.js中能夠經過共享某些組件實現layout。

咱們先增長一個公共的header組件,放在根目錄的components文件夾下面(頁面級的組件放pages中,公共組件放components中):

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;

而後在index和about頁面中引入header組件,這樣就實現了公共的layout的header:

import Header from '../components/Header';

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

export default Index;

若是要增長footer也能夠按照header的方法實現。
除了引入多個header、footer組件,咱們能夠實現一個總體的Layout組件,避免引入多個組件的麻煩,一樣在components中添加一個Layout.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

這樣咱們只須要在頁面中引入Layout組件就能夠達到佈局的目的:

import Layout from '../components/Layout';

const Index = () => (
    <Layout>
        <p>Hello next.js</p>
    </Layout>
)

export default Index;

頁面間傳值

經過url參數(query string)

next中的頁面間傳值方式和傳統網頁同樣也能夠用url參數實現,咱們來作一個簡單的博客應用:

首先將index.js的內容替換成以下來展現博客列表:

import Link from 'next/link';
import Layout from '../components/Layout';

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="next.js is awesome" />
            <PostLink title="Deploy apps with Zeit" />
        </ul>
    </Layout>
);

經過在Link的href中添加title參數就能夠實現傳值。

如今咱們再添加博客的詳情頁post.js

import { withRouter } from 'next/router';
import Layout from '../components/Layout';

const Post = withRouter((props) => (
    <Layout>
        <h1>{props.router.query.title}</h1>
        <p>This is the blog post content.</p>
    </Layout>
));

export default Post;

上面代碼經過withRouter將next的router做爲一個prop注入到component中,實現對url參數的訪問。

運行後顯示如圖:

列表頁

image

點擊進入詳情頁:

image

使用query string能夠實現頁面間的傳值,可是會致使頁面的url不太簡潔美觀,尤爲當要傳輸的值多了以後。因此next.js提供了Route Masking這個特性用於路由的美化。

路由假裝(Route Masking)

這項特性的官方名字叫Route Masking,沒有找到官方的中文名,因此就根據字面意思暫且翻譯成路由假裝。所謂的路由假裝即讓瀏覽器地址欄顯示的url和頁面實際訪問的url不同。實現路由假裝的方法也很簡單,經過Link組件的as屬性告訴瀏覽器href對應顯示爲何url就能夠了,index.js代碼修改以下:

import Link from 'next/link';
import Layout from '../components/Layout';

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="next.js is awesome" />
            <PostLink id="deploy-nextjs" title="Deploy apps with Zeit" />
        </ul>
    </Layout>
);

運行結果:

image

瀏覽器的url已經被如期修改了,這樣看起來舒服多了。並且路由假裝對history也很友好,點擊返回再前進仍是可以正常打開詳情頁面。可是若是你刷新詳情頁,確報404的錯誤,如圖:

image

這是由於刷新頁面會直接向服務器請求這個url,而服務端並無該url對應的頁面,因此報錯。爲了解決這個問題,須要用到next.js提供的自定義服務接口(custom server API)。

自定義服務接口

自定義服務接口前咱們須要建立服務器,安裝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('*', (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裏面的dev script改成:

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

運行npm run dev後項目和以前同樣能夠運行,接下來咱們須要添加路由將被假裝過的url和真實的url匹配起來,在server.js中添加:

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

這樣咱們就把被假裝過的url和真實的url映射起來,而且query參數也進行了映射。重啓項目以後就能夠刷新詳情頁而不會報錯了。可是有一個小問題,前端路由打開的頁面和後端路由打開的頁面title不同,這是由於後端路由傳過去的是id,而前端路由頁面顯示的是title。這個問題在實際項目中能夠避免,由於在實際項目中咱們通常會經過id獲取到title,而後再展現。做爲Demo咱們偷個小懶,直接將id做爲後端路由頁面的title。

以前咱們的展現數據都是靜態的,接下來咱們實現從遠程服務獲取數據並展現。

遠程數據獲取

next.js提供了一個標準的獲取遠程數據的接口:getInitialProps,經過getInitialProps咱們能夠獲取到遠程數據並賦值給頁面的props。getInitialProps便可以用在服務端也能夠用在前端。接下來咱們寫個小Demo展現它的用法。咱們打算從TVMaze API 獲取到一些電視節目的信息並展現到個人網站上。首先,咱們安裝isomorphic-unfetch,它是基於fetch實現的一個網絡請求庫:

npm install --save isomorphic-unfetch

而後咱們修改index.js以下:

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

const Index = (props) => (
    <Layout>
        <h1>Marvel TV Shows</h1>
        <ul>
            {props.shows.map(({ show }) => {
                return (
                    <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=marvel');
    const data = await res.json();
    return {
        shows: data
    }
}

export default Index;

以上代碼的邏輯應該很清晰了,咱們在getInitialProps中獲取到電視節目的數據並返回,這樣在Index的props就能夠獲取到節目數據,再遍歷渲染成節目列表。

運行項目以後,頁面完美展現:

image

接下來咱們來實現詳情頁,首先咱們把/p/:id的路由修改成:

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

咱們經過將id做爲參數去獲取電視節目的詳細內容,接下來修改post.js的內容爲:

import fetch from 'isomorphic-unfetch';
import Layout from '../components/Layout';

const Post = (props) => (
    <Layout>
        <h1>{props.show.name}</h1>
        <p>{props.show.summary.replace(/<[/]?p>/g, '')}</p>
        <img src={props.show.image.medium} />
    </Layout>
);

Post.getInitialProps = async function (context) {
    const { id } = context.query;
    const res = await fetch(`https://api.tvmaze.com/shows/${id}`);
    const show = await res.json();
    return { show };
}

export default Post;

重啓項目(修改了server.js的內容須要重啓),從列表頁進入詳情頁,已經成功的獲取到電視節目的詳情並展現出來:

image

增長樣式

到目前爲止,我們作的網頁都太平淡了,因此接下來我們給網站增長一些樣式,讓它變得漂亮。

對於React應用,有多種方式能夠增長樣式。主要分爲兩種:

  1. 使用傳統CSS文件(包括SASS,PostCSS等)
  2. 在JS文件中插入CSS

使用傳統CSS文件在實際使用中會用到挺多的問題,因此next.js推薦使用第二種方式。next.js內部默認使用styled-jsx框架向js文件中插入CSS。這種方式引入的樣式在不一樣組件之間不會相互影響,甚至父子組件之間都不會相互影響。

styled-jsx

接下來,咱們看一下如何使用styled-jsx。將index.js的內容替換以下:

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

const Index = (props) => (
    <Layout>
        <h1>Marvel TV Shows</h1>
        <ul>
            {props.shows.map(({ show }) => {
                return (
                    <li key={show.id}>
                        <Link as={`/p/${show.id}`} href={`/post?id=${show.id}`}>
                            <a className="show-link">{show.name}</a>
                        </Link>
                    </li>
                );
            })}
        </ul>
        <style jsx>
        {`
            *{
                margin:0;
                padding:0;
            }
            h1,a{
                font-family:'Arial';
            }
            h1{
                margin-top:20px;
                background-color:#EF141F;
                color:#fff;
                font-size:50px;
                line-height:66px;
                text-transform: uppercase;
                text-align:center;
            }    
            ul{
                margin-top:20px;
                padding:20px;
                background-color:#000;
            }
            li{
                list-style:none;
                margin:5px 0;
            }
            a{
                text-decoration:none;
                color:#B4B5B4;
                font-size:24px;
            }
            a:hover{
                opacity:0.6;
            }
        `}
        </style>
    </Layout>
);

Index.getInitialProps = async function () {
    const res = await fetch('https://api.tvmaze.com/search/shows?q=marvel');
    const data = await res.json();
    console.log(`Show data fetched. Count: ${data.length}`);
    return {
        shows: data
    }
}

export default Index;

運行項目,首頁變成:
image

增長了一點樣式以後比以前好看了一點點。咱們發現導航欄的樣式並無變。由於Header是一個獨立的的component,component之間的樣式不會相互影響。若是須要爲導航增長樣式,須要修改Header.js:

import Link from 'next/link';

const Header = () => (
    <div>
        <Link href="/">
            <a>Home</a>
        </Link>
        <Link href="/about">
            <a>About</a>
        </Link>
        <style jsx>
            {`
                a{
                    color:#EF141F;
                    font-size:26px;
                    line-height:40px;
                    text-decoration:none;
                    padding:0 10px;
                    text-transform:uppercase;
                }
                a:hover{
                    opacity:0.8;
                }
            `}
        </style>
    </div>
)

export default Header;

效果以下:
image

全局樣式

當咱們須要添加一些全局的樣式,好比rest.css或者鼠標懸浮在a標籤上時出現下劃線,這時候咱們只須要在style-jsx標籤上增長global關鍵詞就好了,咱們修改Layout.js以下:

import Header from './Header';

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

const Layout = (props) => (
    <div style={layoutStyle}>
        <Header />
        {props.children}
        <style jsx global>
            {`
                a:hover{
                    text-decoration:underline;
                }
            `}
        </style>
    </div>
)

export default Layout

這樣鼠標懸浮在全部的a標籤上時會出現下劃線。

部署next.js應用

Build

部署以前咱們首先須要能爲生產環境build項目,在package.json中添加script:

"build": "next build"

接下來咱們須要能啓動項目來serve咱們build的內容,在package.json中添加script:

"start": "next start"

而後依次執行:

npm run build
npm run start

build完成的內容會生成到.next文件夾內,npm run start以後,咱們訪問的實際上就是.next文件夾的內容。

運行多個實例

若是咱們須要進行橫向擴展( Horizontal Scale)以提升網站的訪問速度,咱們須要運行多個網站的實例。首先,咱們修改package.json的start script:

"start": "next start -p $PORT"

若是是windows系統:

"start": "next start -p %PORT%"

而後運行build: npm run build,而後打開兩個命令行並定位到項目根目錄,分別運行:

PORT=8000 npm start
PORT=9000 npm start

運行完成後打開localhost:8000localhost:9000均可以正常訪問:

image

經過以上方法雖然可以打包並部署,可是有個問題,咱們的自定義服務server.js並無運行,致使在詳情頁刷新的時候依然會出現404的錯誤,因此咱們須要把自定義服務加入app的邏輯中。

部署並使用自定義服務

咱們將start script修改成:

"start": "NODE_ENV=production node server.js"

這樣咱們就解決了自定義服務的部署。重啓項目後刷新詳情頁也可以正常訪問了。

到此爲止,咱們已經瞭解了next.js的大部分使用方法,若是有疑問能夠查看next.js官方文檔,也能夠給我留言討論。

感謝你們閱讀,另外,在這邊幫朋友推一個愛心衆籌,但願你們可以奉獻點愛心,朋友母親,身患直腸癌,目前在北京武警總醫院接收治療,可留言留下您的聯繫方式,往後感激你們!

clipboard.png

相關文章
相關標籤/搜索