Next.js – 再多認識你一點!

Next.js,是一個用於 React 應用的極簡的伺服器端渲染框架。

這裡就節錄 Next.js Github 官網 v4.1.4 版本了,若要看原文版,請參考這裡:官網 README.md。言歸正傳,我們開始啦!

圖畫裡 龍不吟虎不嘯

 


如何使用?

安裝

npm install --save next react react-dom

Next.js 4 只支持 React 16
由於 React 16 和 React 15 的工作方式與使用方法不同,所以 Next 不得不移除對 React 15 的支持

在你的 package.json 添加如下代碼:

"scripts": {
  "dev": "next",
  "build": "next build",
  "start": "next start"
}

接下來,大部分事情都交由文件系統來處理。每個 .js 文件都變成了一個自訂處理和渲染的路由。

在項目中新建 ./page/index.js

export default () => <div>Welcome to next.js!</div>

然後,在控制台輸入 npm run dev,打開 http://localhost:3000 即可看到程序已經運行,也可使用其他端口號: npm run dev -- -p 8080

目前為止,我們已經介紹了:

  • 自動編譯和打包 (使用 webpack 和 babel)
  • 代碼熱更新
  • ./pages 目錄作為頁面渲染目錄的 server-side render
  • 靜態文件服務 (./static/ 被自動定位到 /static/)

代碼自動分割

你所聲明的每個 import 命令所導入的文件只會與相關頁面進行綁定並提供任務,也就是說,頁面不會加載不需要的代碼

import cowsay from 'cowsay-browser'

export default () =>
  <pre>
    {cowsay.say({text: 'hi there!'})}
  </pre>

CSS

嵌入式樣式 Build-in-CSS

我們提供  style-jsx 支持局部獨立作用域的 CSS(scope CSS),目的是提供一種類似於 web 組件的 shadow CSS,不過,後者並不支持 SSR (scope CSS 是支持的)

const Index = () => (
  <div>
    Hello world
    <p>scoped!</p>
    <style jsx>{`
      p {
        color: red;
      }
      div {
        background: red;
      }
      @media (max=width: 600px) {
        div {
          background: blue;
        }
      }
    `}</style>
    <style global jsx>{`
      body {
        background: black;
      }
    `}</style>
  </div>
);

export default Index;
  • scope CSS 作用範圍,如果添加 jsx 屬性,則是不包括子組件的當前組件;如果添加了 global 和 jsx 屬性,則是包括了子組件在內的當前組件;如果沒添加任何屬性,則作用與添加了 global 和 jsx 的作用類似,只不過 next 不會對其進行額外的提取與優化打包。
  • scope CSS 實現原理,其實就是在編譯好的代碼的對應元素上,添加一個以 jsx 開頭的 class,然後將對應的樣式代碼提取到此 class 下

內聯式樣式 CSS-in-JS

幾乎可使用所有內聯樣式解決方案:

export default () => <p style={{ 'color': 'red' }}>hi there</p>

為了使用更複雜的 CSS-in-JS 內聯樣式方案,你可能不得不在 SSR 的時候強制樣式刷新。我們透過允許自定義包裏著每個頁面的 <Document> 組件的方式來解決此問題。

靜態文件服務

在你項目根目錄新建 static 文件夾,然後你就可以在你的代碼透過 /static/ 開頭的路徑來引用此文夾的文件:

export default () => <img src="/static/my-img.png">

自定義 <head> 頭部元素

import Head from 'next/head';

export default () =>
  <div>
    <Head>
      <title>My page title</title>
      <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    </Head>
    <p>Hello world</p>
  </div>

注意:當組件卸載的時候,組件內定義的 <Head> 將會被清空,所以要確保每個頁面都在各自的 <Head> 內聲明了所有需要的內容,而不是假定這些東西已經在其他頁面中添加過了。

  1. next 框架自帶 <Head> 標籤,作為當前頁面的 <head>,如果在組件內自定義 <Head>,則自定義 <Head> 內的元素將會被追加到框架自帶的 <Head> 標籤中
  2. 每個組件自定義的 <Head> 內容只會應用在各自的頁面上,子組件內定義的 <Head> 也會追加到當前頁面的 <head> 內,如有重複定義的標籤或屬性,則子組件覆蓋父組件,位於文檔更後面的組件覆蓋更前面的組件

數據獲取及組件生命週期

你可透過導出一個基於 React.Component 的組件來獲取狀態 (state)、生命週期或者初始數據 (而不是無狀態函數 stateless)

import Head from 'next/head';
import React from "react";

export default class extends React.Component {

  static async getInitialProps({req}) {
    const userAgent = req ? req.headers['user-agent'] : navigator.userAgent;
    return {userAgent};
  }

  render() {
    return (
      <div>
        <Head>
          <title>My page title</title>
          <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
        </Head>
        <p>Hello world {this.props.userAgent}</p>
      </div>
    )
  }
};

當頁面加載數據時,我們使用一個異步 (async) 的靜態方法 getInitialProps。此靜態方法能夠獲取所有的數據,並將其解析成一個 JavaScript 對象,然後將其作為屬性附加到 props 對象上。

當初始化頁面的時候,getInitialProps 只會在 server-side 執行,而當透過 Link 組件或使用命令路由 API 來將頁面導航到另外一個路由的時候,此方法就只會在客戶端執行。

注意: getInitialProps 不能 在子組件上使用,只能應用於當前頁面的頂層組件。

如果你在 getInitialProps 中引入了一些只能在 server-side 使用的模組 (例如 node.js 的核心模組),請確保透過正確的方式來導入它們 import them properly ,否則的話,那很可能會拖慢應用的速度。

你也可以為無狀態組件自定義 getInitialProps 生命週期方法:

import fetch from 'isomorphic-unfetch';

const Page = ({stars}) =>
  <div>
    Next stars: {stars}
  </div>;

Page.getInitialProps = async ({req}) => {
  const res = await fetch('https://api.github.com/repos/zeit/next.js');
  const json = await res.json();
  return {stars: json.stargazers_count};
};

export default Page;

getInitialProps 接收的上下文對象包含以下屬性:

  • pathname – URL 的 path 部分
  • query – URL 的 query string 部分,已被解析成一個 object
  • asPath – 在瀏覽器上展示的實際路徑 (包括 query 字符串)
  • req – HTTP request object (只存在 server-side)
  • res – HTTP response object (只存在 server-side)
  • jsonPageRes – 獲取的響應數據對象 Fetch Response (只存在於 client-side)
  • err – 渲染時發生錯誤拋出的錯誤對象

基於 getInitialProps 在 server-side 和 client-side 的不同表現,例如 req 的存在與否,
可以透過此來區分 server-side 和 client-side。

路由 <Link>

可以透過 <Link> 組件來實現 client-side 在兩個路由間的切換功能,例如下面兩個頁面:

//pages/index.js
import Link from 'next/link';

export default () =>
  <div>
    Click{' '}
    <Link href="/about">
      <a>here</a>
    </Link>{' '}
    to read more
  </div>
export default () =>
  <p>Welcome to About!</p>

注意,可使用 <Link prefetch> 來讓頁面在後台同時獲取和預加載,以獲得更佳的頁面加載性能

客戶端路由行為與瀏覽器完全相同:

  1. 獲取組件
  2. 如果組件定義了 getInitialProps,那麼進行數據的獲取,如果拋出異常,則將渲染 _error.js
  3. 步驟 1 和步驟 2 完成後,pushState 開始執行,接著新組件將會被渲染

每一個頂層組件都會接收到一個 url 屬性,其包括了以下 API:

  • pathname – 不包括 query 字符串在內的當前鏈接地址的 path 字符串 (即 pathname)
  • query – 當前鏈接地址的 query 字符串,已經被解析為對象,默認為 {}
  • asPath – 在瀏覽器地址欄顯示的當前頁面的實際地址 (包括 query 字符串)
  • push(url, as=url) – 透過 pushState 來跳轉路由到給定的 url
  • replace(url, as=url) – 透過 replaceState 來將當前路由替換到給定的路由地址 url 上

push 和 replace 的第二個參數 as 提供額外的配置項,當你在 server 上配置自定義路由的話,那麼此參數就會發揮作用。

as 可根據 server-side 路由的配置做出相對應的路由改變,例如,在 server-side,你自定義規定當獲取 /a 的 path 請求的時候,返回一個位於 /b 目錄下的頁面,則為了配合 server-side 的這種指定,你可以這麼定義 <Link> 組件:<Link href=”/a” as=”/b”><a>a</a></Link>

<Link> 組件主要用於路由跳轉功能,其可以接收一個必須的子元素 (DOM 標籤或純文字等)

  1. 如果添加的子元素是 DOM 元素,則 Link 會為此子元素賦予路由跳轉功能
  2. 如果添加的元素是純文字,則 <Link> 默認轉化為 a 標籤,包裏在此文字外部,如果當前組件有 jsx 屬性的 scope CSS,這個 a 標籤是不會受此 scope CSS 影響的,也就是說,不會加上以 jsx 開頭的類名

需要注意的是,直接添加純文字作為子元素的做法如今已經不被贊成 (deprecated)。

URL 對象

<Link> 組件可以接收一個 URL 對象,此 URL 將會被自動格式化為 URL 字符串。

import Link from 'next/link';

export default () =>
  <div>
    Click{' '}
    <Link href={{pathname: '/about', query: {name: 'Zeit'}}} prefetch>
      <a>here</a>
    </Link>{' '}
    to read more
  </div>

上述代碼 <Link> 組件將會根據 href 屬性對象生成:/about?name=Zeit 的 URL 字符串,你也可以在此 URL 對象中使用 Node.js URL module document 中定義好的屬性來配置路由。

替換 (replace) 而非追加 (push) 路由 url

<Link> 組件默認將新的 URL 追加 (push) 到路由棧中,但你可使用 replace 屬性來避免此追加動作 (直接替換掉當前路由)。

import Link from 'next/link';

export default () =>
  <div>
    Click{' '}
    <Link href="/about" replace>
      <a>here</a>
    </Link>{' '}
    to read more
  </div>

讓組件支持 onClick 事件

<Link> 標籤支持所有支持 onClick 事件的組件 (即只要某組件或元素標籤支持 onClick 事件,則 <Link> 就能夠為其提供跳轉路由的功能)。如果你沒給 <Link> 標籤添加一個 <a> 標籤的子元素的話,那麼它只會執行給定的 onClick 事件,而不是執行跳轉路由的動作。

import Link from 'next/link';

export default () =>
  <div>
    Click{' '}
    <Link href="/about" replace>
      <img src="/static/image.png"/>
    </Link>
  </div>

將 <Link> 的 href 暴露給子元素 (child)

如果 <Link> 的子元素是一個 <a> 標籤並且沒有指定 href 屬性的話,那麼我們會自動指定此屬性 (與 <Link> 的 href 相同) 以避免重複工作,然而有時候,你可能想要透過一個被包裏在某個容器 (例如組件) 內的 <a> 標籤來實現跳轉功能,但是 <Link> 並不認為那是一個超連接,因此,就不會把它的 href 屬性傳遞給子元素,為避免此問題,你應該給 Link 附加一個 passHref 屬性,強制讓 Link 將其 href 屬性傳遞給它的子元素。

import Link from 'next/link'
import Unexpected_A from 'third-library'

export default ({ href, name }) =>
  <Link href={href} passHref>
    <Unexpected_A>
      {name}
    </Unexpected_A>
  </Link>

命令式路由

你可使用 next/router 來實現客戶端側的頁面切換

import Router from 'next/router';

export default () =>
  <div>
    Click <span onClick={() => Router.push('/about')}>here</span> to read more
  </div>

上面代碼中的 Router 對象擁有以下 API:

  • router – 當前路由字符串
  • pathname – 不包括 query string 在內的當前路由的 path (也就是 pathname)
  • query – Object with parsed query string. Defaults to {}
  • asPath – 在瀏覽器地址欄顯示的當前頁面的實際地址 (包括 query 字符串)
  • push(url, as=url) – 透過 pushState 來跳轉路由到給定的 url
  • replace(url, as=url) – 透過 replaceState 來將當前路由替換到給定的路由地址 url 上

push 和 replace 的第二個參數 as 提供額外的配置項,當你在 server 上配置自定義路由的話,那麼此參數就會發揮作用。

為了使編程的方式而不是觸發導航和組件獲取方式來切換路由,可在組件內部使用 props.url.push 和 props.url.replace

除非特殊需要,否則在組件不贊成 deprecated 使用 props.url.push 和 props.url.replace,而是建議使用 next/router 的相關 API。

URL 對象

命令式路由 (next/router) 所接收的 URL 對象與 <Link> 的 URL 對象很類似,你可以使用相同的方式來 push 和 replace 路由 URL:

import Router from 'next/router';

const handler = () =>
  Router.push({
    pathname: '/about',
    query: {name: 'Zeit'}
  });

export default () =>
  <div>
    Click <span onClick={handler}>here</span> to read more
  </div>

命令式路由 (next/router) 的 URL 對象的屬性及其參數的使用方法和 <Link> 組件完全一樣。

路由事件

你還可以監聽到與 Router 相關的一些事件。

以下是你所能夠監聽的 Router 事件:

  • routeChangeStart(url) – 當路由剛開始切換的時候觸發
  • routeChangeComplete(url) – 當路由切換完成時觸發
  • routeChangeError(err, url) – 當路由切換發生錯誤時觸發
  • beforeHistoryChange(url) – 在改變瀏覽器 history 之前觸發
  • appUpdated(nextRoute) – 當切換頁面的時候,應用版本剛好更新的時觸發 (例如在部署期間切換路由)

上面 API 中的 url 參數指的是瀏覽器地址欄顯示的鏈接地址,如果你使用 Router.push(url, as) (或者類似的方法) 來改變路由,則此值就將是 as 的值

Router.onRouteChangeStart = url => {
  console.log('App is changing to: ', url);
}

如果你不想繼續監聽,也可很輕鬆卸載掉:

Router.onRouteChangeStart = null;

如果某個路由加載被取消掉了 (例如連續快速單擊兩個鏈接),routeChangeError 將會被執行。此方法的第一個參數 err 對象中將包括一個值為 true 的 cancelled 屬性。

Router.onRouteChangeError = (err, url) => {
  if (err.cancelled) {
    console.log(`Route to ${url} was cancelled!`);
  }
}

如果你在一次項目新部署的過程中改變了路由,那們我們就無法在客戶端對應用進行導航,必須要進行一次完整的導航動作 (意思是無法像正常那樣透過 PWA 方式進行導航),我們已經自動幫你做了這些事。

不過,你也可以透過 Router.onAppUpdated 事件對此進行自定義操作:

Router.onAppUpdated = nextUrl => {
  // persist the local state
  location.href = nextUrl;
}

一般情況下,上述路由事件發生順序:

  1. routeChangeStart
  2. beforeHistoryChange
  3. routeChangeComplete

淺層路由

淺層路由 (Shallow routing) 允許你在不觸發 getInitialProps 的情況下改變路由(URL),你可透過要加載頁面的 url 來獲取更新後的 pathname 和 query,這樣就不會丟失路由狀態 (state) 了。

你可以透過調用 Router.push 或 Router.replace,並給它們加上 shallow: true 的配置參數來實現此功能:

// Current URL is "/"
const href = '/?counter=10';
const as = href;
Router.push(href, as, {shallow: true});

現在,URL 已經被更新到 /?counter=10,你可以在組件內部透過 this.props.url 來獲取此 URL

你可以在 componentWillReceiveProps 鉤子函數中獲取到 URL 的變化,就像這樣:

componentWillReceiveProps(nextProps) {
  const { pathname, query } = nextProps.url;
  // fetch data based on the new query
}

注意:

淺層路由只會在某些頁面上起作用,例如,我們可假定存在另外一個名為 about 的頁面,然後執行下面這行代碼:

Router.push(‘/about?counter=10’, ‘/about?counter=10’, {shallow: true})

因為這是一個新的頁面 (/about?counter=10),所以即使我們已經聲明只執行淺層路由,但當前頁面仍然會被卸載掉 (unload),然後加載新的頁面並調用 getInitialProps 方法

使用高階函數 HOC

如果你想在應用的任何組件都能獲取到 router 對象,那麼你可使用 withRouter  高階函數,下面是一個使用此高階函數的示例:

import {withRouter} from 'next/router';

const ActiveLink = ({children, router, href}) => {
  const style = {
    marginRight: 10,
    color: router.pathname === href ? 'red' : 'black'
  };

  const handleClick = e => {
    e.preventDefault();
    router.push(href);
  };

  return (
    <a href={href} onClick={handleClick} style={style}>
      {children}
    </a>
  );
};

export default withRouter(ActiveLink);

上述代碼中的 router 對象擁有和 next/router 相同的 API。

預獲取頁面 Prefetching Pages

Next.js 自帶允許你預獲取 (prefetch) 頁面的 API

因為 Next.js 在 Server-side 渲染頁面,所以應用的所有將來可能發生交互的相關鏈接可以在瞬間完成交互,事實上 Next.js 可以透過預下載功能來達到一個絕佳的加載性能

由於 Next.js 只會預加載 JS 代碼,所以在頁面加載的時候,你可能還需要花點時間來等待數據的獲取。

透過 <Link> 組件

你可為任何一個 <Link> 組件添加 prefetch 屬性,Next.js 將會在後台預加載這些頁面。

import Link from 'next/link'

// example header component
export default () => 
    <nav>
      <ul>
        <li>
          <Link prefetch href="/">
            <a>Home</a>
          </Link>
        </li>
        <li>
          <Link prefetch href="/about">
            <a>About</a>
          </Link>
        </li>
        <li>
          <Link prefetch href="/contact">
            <a>Contact</a>
          </Link>
        </li>
      </ul>
    </nav>

透過命令的方式

大部分預獲取功能都要透過 <Link> 組件來指定鏈接地址,但是我們還曝露了一個命令式的 API 以方便更加複雜的場景:

import Router from 'next/router';

export default ({url}) =>
    <div>
      <a onClick={() => setTimeout(() => url.pushTo('/dynamic'), 100)}>
        A router transition will happen after 100ms
      </a>
      {
        // but we can prefetch it!
        Router.prefetch('/dynamic')
      }
    </div>

自定義 Server 和路由

一般來說,你可以使用 next start  命令啟動 next 服務,但是,你也完全可以使用編程 (programmatically) 的方式,例如路由匹配等,來定製化路由。

下面是一個將 /a 匹配到 ./page/b,以及將 /b 匹配到 ./page/a 的例子:

const {createServer} = require('http');
const {parse} = require('url');
const next = require('next');

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

app.prepare().then(() => {
  createServer((req, res) => {
    const parsedUrl = parse(req.url, true);
    const {pathname, query} = parsedUrl;

    if (pathname === '/a') {
      app.render(req, res, '/b', query);
    } else if (pathname === '/b') {
      app.render(req, res, '/a', query);
    } else {
      handle(req, res, parsedUrl);
    }
  }).listen(3000, err => {
    if (err) throw err;
    console.log('> Ready on http://localhost:3000');
  });
});

next API 如下所示:

  • next(path: string, opts: object) – path 是 Next 應用當前的路由位置
  • next(opts: object)

上述 API 中的 opt 對象存在如下屬性:

  • dev (bool) 是否使用開發模式 (dev) 來啟動 Next.js – 默認為 false
  • dir (string) 當前 Next 應用的路由位置 – 默認為 ‘.’
  • quiet (bool) 隱藏包括 server-side 消息在內的錯誤消息 – 默認為 false
  • conf (object) 和 next.config.js 中的對象是同一個 – 默認為 false

然後,將你的 start 命令改寫成: NODE_ENV=production node server.js

異步導入 Dynamic Import

Next.js 支持 JavaScript TC39 的 dynamic import proposal 規範,所以你可以動態載入 (import) JavaScript 模組 (例如 React Component)

你可以將動態導入理解為一種將代碼分割為更易管理和理解的方式。
由於 Next.js 支持 server-side render 的動態導入,所以你可以用它來做一些酷炫的東西。

  1. 基本用法 (同樣支持 SSR)
    import dynamic from 'next/dynamic';
    
    const DynamicComponent = dynamic(import('../components/hello'));
    
    export default () =>
        <div>
          <Header/>
          <DynamicComponent/>
          <p>HOME PAGE is here!</p>
        </div>
    
  2. 自定義加載組件
    import dynamic from 'next/dynamic';
    
    const DynamicComponentWithCustomLoading = dynamic(
        import('../components/hello2'),
        {
          loading: () => <p>...</p>
        }
    );
    
    export default () =>
        <div>
          <Header />
          <DynamicComponentWithCustomLoading />
          <p>HOME PAGE is here!</p>
        </div>
    
  3. 禁止 SSR
    import dynamic from 'next/dynamic';
    
    const DynamicComponentWithNoSSR = dynamic(
        import('../components/hello2'),
        {
          ssr: false
        }
    );
    
    export default () =>
        <div>
          <Header />
          <DynamicComponentWithNoSSR />
          <p>HOME PAGE is here!</p>
        </div>
    
  4. 一次性加載多個模組
    import dynamic from 'next/dynamic';
    
    const HelloBundle = dynamic({
      modules: props => {
        const components = {
          Hello1: import('../components/hello1'),
          hello2: import('../components/hello2')
        }
    
        // Add remove components based on props
    
        return components;
      },
      render: (props, {Hello1, Hello2}) =>
          <div>
            <h1>
              {props.title}
            </h1>
            <Hello1 />
            <Hello2 />
          </div>
    });
    
    export default () => <HelloBundle title="Dynamic Bundle"/>
    

自定義 <Document>

Next.js 幫你自動跳過為頁面添加文檔標記元素的操作,例如,你從來不需要主動添加 <html>、<body> 這些文檔元素。如果想重定義這些默認操作的話,那麼你可以創建 (或覆寫) ./page/_document.js 文件,在此文件中,對 Document 進行擴展:

// ./pages/_document.js
import Document, { Head, Main, NextScript } from 'next/document'
import flush from 'styled-jsx/server'

export default class MyDocument extends Document {
  static getInitialProps({ renderPage }) {
    const { html, head, errorHtml, chunks } = renderPage()
    const styles = flush()
    return { html, head, errorHtml, chunks, styles }
  }

  render() {
    return (
      <html>
        <Head>
          <style>{`body { margin: 0 } /* custom! */`}</style>
        </Head>
        <body className="custom_class">
          {this.props.customValue}
          <Main />
          <NextScript />
        </body>
      </html>
    )
  }
}

在以下前提下,所有的 getInitialProps 鉤子函數接收到的 ctx 都指的是同一個對象:

  • 回調函數 renderPage (Function) 是真正執行 React 渲染邏輯的函數 (同步地),這種作法有助於此函數支持一些類似於 Aphrodite’s 的 renderStatic 等一些 Server-side Render 容器

注意:<Main /> 之外的 React 組件都不會被瀏覽器初始化,如果你想在所有頁面中使用某些組件 (例如菜單欄或工具欄),首先保證不要在其中添加有關應用邏輯的內容,可看這個例子

不會被初始化和執行的邏輯代碼包括除了 render 之外的所有生命週期鉤子函數,例如:componentDidMount、componentWillUpdate,以及一些監聽函數,例如 onClick、onMouseOver 等,所以如果你要在 _document.js 添加額外的組件,請確保這些組件中除了 render 之外沒有其他的邏輯。

自定義錯誤處理

客戶端和服務器端都會捕捉並使用默認組件 error.js 來處理 404 和 500 錯誤。如果你希望自定義錯誤處理,可以對其進行覆寫:

import React from 'react';

export default class Error extends React.Component {
  static getInitialProps({res, jsonPageRes}) {
    const statusCode = res
        ? res.statusCode
        : jsonPageRes ? jsonPageRes.status : null;
    return {statusCode};
  }

  render() {
    return (
        <p>
          {
            this.props.statusCode
                ? `An error ${this.props.statusCode} occurred on server`
                : 'An error occurred on client'
          }
        </p>
    );
  }
}

使用內置的錯誤頁面

如果你想使用內置的錯誤頁面,那麼你可透過 next/error 來實現:

import React from 'react';
import Error from 'next/error';
import fetch from 'isomorphic-fetch';

export default class Page extends React.Component {
  static async getInitialProps() {
    const res = await fetch('https://api.github.com/repos/zeit/next.js');
    const statusCode = res.statusCode > 200 ? res.statusCode : false;
    const json = await res.json();

    return {statusCode, stars: json.stargazers_count}
  }

  render() {
    if (this.props.statusCode) {
      return <Error statusCode={this.props.statusCode}/>;
    }

    return (
        <div>
          Next stars: {this.props.stars}
        </div>
    );
  }
}

如果你想使用自定義的錯誤頁面,那麼你可導入你自己的錯誤 (_error) 頁面組件而非內置的 next/error

自定義配置

為了對 Next.js 進行更複雜的自定義操作,你可在項目的根目錄下新建一個 next.config.js 文件

注意:next.config.js 是一個標準的 Node.js 模組,而不是一個 JSON 文件,此文件在 Next 項目的 Server 端以及 build 階段會被調用,但是在瀏覽器端構建時是不會起作用的。

module.exports = {
  /* config options here */
}

設置一個自定義的構建 (build) 目錄

你可以自行指定構建打包的輸出目錄,例如下面範例將默認打包輸出目錄 .next 指定為 build 目錄:

module.exports = {
  distDir: 'build'
}

Configuring the onDemandEntries

Next 暴露了一些能夠讓你自己控制如何部署服務或者緩存頁面的配置:

module.exports = {
  onDemandEntries: {
    // 控制頁面內存 `buffer` 中緩存的時間,單位是 ms
    maxInactiveAge: 25 * 1000,
    // number of pages that should be kept simultaneously without being disposed
    pagesBufferLength: 2,
  }
};

自定義 webpack 配置

你可以透過 next.conf.js 中的函數來擴展 webpack 的配置

// This file is not going through babel transformation.
// So, we write it in vanilla JS
// (But you could use ES2015 features supported by your Node.js version)

module.exports = {
  webpack: (config, {buildId, dev}) => {
    // Perform customizations to webpack config

    // Important: return the modified config
    return config;
  },
  webpackDevMiddleware: config => {
    // Perform customizations to webpack dev middleware config

    // Important: return the modified config
    return config;
  }
};

警告:不推薦在 webpack 的配置中添加一個支持新文件類型 (css less svg 等) 的 loader,因為 webpack 只會打包客戶端代碼,所以 (loader) 不會在服務器端的初始化渲染中起作用。Babel 是一個很好的替代品,因為其給服務端和客戶端提供一致的功能效果 (例如:babel-plugin-inline-react-svg)

自定義 Babel 配置

為了擴展對 Babel 的使用,你可以在應用的根目錄下新建 .babelrc 文件,此文件是非必需的

如果此文件存在,那麼我們就認為這個才是真正的 Babel 配置文件,因此也就需要為其定義一些 next 項目需要的東西,並將之當作是 next/babel 的預設配置 (preset) ,這種設計是為了避免你有可能對我們定制 babel 配置而感到訝異。

下面是一個 .babelrc 文件的示例:

{
  "presets": ["next/babel", "stage-0"]
}

CDN 支持

你可以設定 assetPrefix 項來配置 CDN 源,以便能夠與 Next.js 項目的 host 保持對應。

const isProd = process.env.NODE_ENV === 'production'
module.exports = {
  // You may only need to add assetPrefix in the production.
  assetPrefix: isProd ? 'https://cdn.mydomain.com' : ''
}

注意:Next.js 將會自動使用所加載腳本的 CDN 域 (作為項目的 CDN 域),但是對 /static 目錄下的靜態文件就無能為力了。如果你想讓那些靜態文件也能用上 CDN,那你就不得不要自己指定 CDN 域,有種方法可讓你的項目自動根據運行環境來確定 CDN 域,可看看這個例子

項目部署

構建打包和啟動項目被分成了以下兩條命令:

next build
next start

例如,你可像下面這樣為 now 項目配置 package.json 文件:

{
  "name": "my-app",
  "dependencies": {
    "next": "latest"
  },
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start"
  }
}

然後就可直接啟動 now 項目了!

Next.js 也可使用其他的託管方案,更多詳細可看下這部分內容 Deployment

注意:我們推薦你推送 .next,或者你自定義的打包輸出目錄 (到託管方案上) (Custom Config,可自定義一個專門用於放置配置文件(例如 .npmignore or .gitignore) 的文件夾。否則的話,使用 files or now.files 來選擇要部署的白名單(很明顯要排除掉 .next 或你自定義的打包輸出目錄))

導出靜態 HTML 頁面

你可將你的 Next.js 應用當成一個不依賴於 Node.js 服務的靜態應用。此靜態應用支持幾乎所有的 Next.js 特性,包括異步導航、預獲取、預加載和異步導入等。

使用

創建一個 Next.js 的配置文件 config:

// next.config.js
module.exports = {
  exportPathMap: function() {
    return {
      '/': { page: '/' },
      '/about': { page: '/about' },
      '/readme.md': { page: '/readme' },
      '/p/hello-nextjs': { page: '/post', query: { title: 'hello-nextjs' } },
      '/p/learn-nextjs': { page: '/post', query: { title: 'learn-nextjs' } },
      '/p/deploy-nextjs': { page: '/post', query: { title: 'deploy-nextjs' } }
    }
  }
}

需要注意的是,如果聲明的路徑表示的是一個文件夾,那麼最終將會導出一份類似於 /dir-name/index.html 的文件,如果聲明的路徑是一個文件的話,那麼最終將會以指定的文件名導出,例如上面代碼中,就會導出一個 readme.md 的文件。如果你使用了一個不是以 .html 結尾的文件,那麼在解析此文件的時候,你需要給 text/html 設置一個 Content-Type 頭 (header)

透過上述類似代碼,你可以指定你想要導出的靜態頁面。

接著,輸入以下命令:

next build
next export

接著,你還可以在 package.json 文件中多添加一條命令:

{
  "scripts": {
    "build": "next build &amp;&amp; next export"
  }
}

現在就只需要輸入這一條命令就行了:

npm run build

這樣,你在 out 目錄下就有了一個當前應用的靜態網站了。

你也可以自定義輸出目錄,更多幫助可在命令行中輸入 next export -h

現在,你就可以把輸出目錄 (例如 /out) 部署到靜態文件服務器了,需要注意的是,如果你想要部署到 Github 上的話,那麼需要增加一個步驟

例如,只需要進入 out 目錄,然後輸入以下命令,就可以把你的應用部署到 ZEIT now

now

局限性

當你輸入 next export 命令時,我們幫你構建了應用的 HTML 靜態版本,在此階段,我們將會執行頁面中的 getInitialProps 函數

所以,你只能使用 context 對象傳遞給 getInitialProps 的 pathname、query 和 asPath 字段,而 req 或 res 則是不可用的 (req 和 res 只在服務器端可用)

基於此,你也無法在我們預先構建 HTML 文件的時候,動態的呈現 HTML 頁面,如果你真的想要這麼做,請使用 next start

相關技巧

One comment Add yours

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料