1. 程式人生 > >[Next] 初見next.js

[Next] 初見next.js

next 簡介

Next.js 是一個輕量級的 React 服務端渲染應用框架

next 特點

  • 預設情況下由伺服器呈現
  • 自動程式碼拆分可加快頁面載入速度
  • 簡單的客戶端路由(基於頁面)
  • 基於 Webpack 的開發環境,支援熱模組替換(HMR)
  • 能夠與 Express 或任何其他 Node.js HTTP 伺服器一起實現
  • 可使用您自己的 Babel 和 Webpack 配置進行自定義

系統需求

Next.js 可與 Windows,Mac 和 Linux 一起使用.您只需要在系統上安裝 Node.js 即可開始構建 Next.js 應用程式.如果有個編輯器就更好了

初始化專案

mkdir next-demo
cd next-demo
npm init -y // 快速建立package.json而不用進行一些選擇
npm install --save react react-dom next
mkdir pages

mkdir pages 這一步是必須建立一個叫 pages 的資料夾,因為 next 是根據 pages 下面的 js jsx tsx 檔案來進行路由生成

然後開啟 package.json 目錄中的 next-demo 檔案並替換 scripts 為以下內容:

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

執行以下命令以啟動開發伺服器:

npm run dev

現在可以開啟 localhost:3000 來檢視頁面效果,如果不喜歡 3000 或者埠衝突,執行下面命令

npm run dev -p 6688(你喜歡的埠)

這時候就可以在 localhost:6688 上看到頁面效果了

hello world

此時我們在 pages 資料夾下建立一個 index.js 作為首頁

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

export default Index;

再次檢視 localhost:6688 就可以看到當前頁面顯示出 hello world

頁面間導航

next 中實現路由非常的簡便,新建 pages/about.js

export default function About() {
  return (
    <div>
      <p>This is the about page</p>
    </div>
  );
}

此時訪問 localhost:6688/about,就可以看到頁面相應的效果(路由與 pages 下的檔名稱完全匹配)

頁面間的導航,我們可以 a 標籤來進行導航.但是,它不會執行客戶端導航.並且,每次點選瀏覽器將向伺服器請求下一頁,同時重新整理頁面.因此,為了支援客戶端導航,,我們需要使用 Next.js 的 Link API,該 API 通過匯出 next/link. Link 將預取頁面,並且導航將在不重新整理頁面的情況下進行.

修改 pages/index.js

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;

再次訪問 localhost:6688,然後點選 About Page 跳轉到 about 頁面.之後點選瀏覽器的後退按鈕,頁面能夠回到 index.

因為 next/link 只是一個更高階的元件(高階元件) , next/link 元件上的設定 props 無效.只接受 href 和類似的 props.如果需要向其新增 props,則需要對下級元件進行新增. next/link 元件不會將那些 props 傳遞給子元件,並且還會給你一個錯誤警告.在這種情況下,next/link 元件的子元件/元素是接受樣式和其他 props 最好承載體.它可以是任何元件或標籤,唯一要求是它們能夠接受 onClick 事件.

   <Link href="/about">
      <a className="redLink">About Page</a>
    </Link>

    <Link href="/show">
      <div>Show Page</div>
    </Link>

這是客戶端導航;該操作在瀏覽器中進行,而無需向伺服器發出請求.開啟開發者工具 networks 進行檢視

更多 routing 內容

元件

目前 Next.js 程式碼都是關於頁面的.我們可以通過匯出 React 元件並將該元件放入 pages 目錄來建立頁面.然後,它將具有基於檔名的固定 URL. 但同時一些共享元件也是專案中必須的,我們將建立一個公共的 Header 元件並將其用於多個頁面.

新建 components/Header.js

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>
    <Link href="/show">
      <a style={linkStyle}>Show</a>
    </Link>
  </div>
);

export default Header;

然後修改 pages 目錄下的 index.js / about.js / show.js

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

export default function Index() {
  return (
    <div>
      <Header />
      <p>Hello Next.js</p>
    </div>
  );
}

開啟 localhost:6688 點選 3 個 link 按鈕就可以進行頁面間的來回跳轉了

我們不需要將我們的元件放在一個名叫 components 的目錄中.該目錄可以命名為任何名稱.只有/pages 和/static 是特殊的.但也不要在 pages 裡面建立共享元件,會生成許多無效的路由導航.

layout 元件

在我們的應用中,我們將在各個頁面上使用通用樣式.為此,我們可以建立一個通用的 Layout 元件並將其用於我們的每個頁面.

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;

然後修改 pages 目錄下的 index.js / about.js / show.js

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

export default function Index() {
  return (
    <Layout>
      <p>Hello Next.js</p>
    </Layout>
  );
}

此外還可以使用 hoc 元件進行內容傳遞獲取使用 props 屬性進行傳遞

動態頁面

在實際應用中,我們需要建立動態頁面來顯示動態內容.

首先修改 pages/about.js 檔案

import Layout from "../components/MyLayout";
import Link from "next/link";

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

export default function About() {
  return (
    <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>
  );
}

建立 pages/post.js

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

const Page = () => {
  const router = useRouter();

  return (
    <Layout>
      <h1>{router.query.title}</h1>
      <p>This is the blog post content.</p>
    </Layout>
  );
};

export default Page;

開啟 localhost:6688 檢視頁面效果,點選 about 下面的 3 個帖子,會出現對應的 title 頁面

  • 們通過查詢字串引數(查詢引數)傳遞資料,通過查詢字串傳遞任何型別的資料.
  • 我們匯入並使用 useRouter 函式,next/router 函式將返回 Next.js router 物件.
  • 我們使用 query 獲取查詢字串引數
  • 獲得標題需要的引數 router.query.title.

post 頁面也可以新增通用 header

import { useRouter } from "next/router";
import Layout from "../components/MyLayout";

const Content = () => {
  const router = useRouter();

  return (
    <Layout>
      <h1>{router.query.title}</h1>
      <p>This is the blog post content.</p>
    </Layout>
  );
};

const Page = () => (
  <Layout>
    <Content />
  </Layout>
);

export default Page;

再次檢視 localhost:6688 看看不同

動態路由

當前我們的路由是這樣的 http://localhost:6688/post?title=Hello%20Next.js , 現在需要更乾淨的路由 http://localhost:6688/p/10. 新增新頁面來建立我們的第一個動態路由 p/[id].js

新建 pages/p/[id].js

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

export default function Post() {
  const router = useRouter();

  return (
    <Layout>
      <h1>{router.query.id}</h1>
      <p>This is the blog post content.</p>
    </Layout>
  );
}
  • next 會處理後面的路由/p/.例如,/p/hello-nextjs 將由此頁面處理.雖然,/p/post-1/another 不會.
  • 方括號使其成為動態路由.而且在匹配動態路由的時候必須使用全名.例如,/pages/p/[id].js 受支援,但/pages/p/post-[id].js 不受支援.
  • 建立動態路由時,我們 id 放在方括號之間.這是頁面接收到的查詢引數的名稱,因此/p/hello-nextjs 在 query 物件就是{ id: 'hello-nextjs'},我們可以使用 useRouter()進行訪問.

在連結多個頁面,新建 pages/page.js

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

const PostLink = props => (
  <li>
    <Link href="/p/[id]" as={`/p/${props.id}`}>
      <a>{props.id}</a>
    </Link>
  </li>
);

export default function Blog() {
  return (
    <Layout>
      <h1>My Blog</h1>
      <ul>
        <PostLink id="hello-nextjs" />
        <PostLink id="learn-nextjs" />
        <PostLink id="deploy-nextjs" />
      </ul>
    </Layout>
  );
}

在該頁面中我們看一下元素,其中 href 屬性 p 資料夾中頁面的路徑, as 是要在瀏覽器的 URL 欄中顯示的 URL.as 是用來與瀏覽器歷史記錄配合使用.

獲取遠端資料

實際上,我們通常需要從遠端資料來源獲取資料.Next.js 自己有標準 API 來獲取頁面資料.我們通常使用非同步函式 getInitialProps 來完成此操作 .這樣,我們可以通過遠端資料來源獲取資料到頁面上,並將其作為 props 傳遞給我們的頁面.getInitialProps 在伺服器和客戶端上均可使用.

首先需要一個獲取資料的庫

npm install --save isomorphic-unfetch

然後修改 pages/index.js

import Layout from '../components/MyLayout';
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 href="/detail/[id]" as={`/detail/${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.map(entry => entry.show)
  };
};

export default Index;

現在這種情況下,我們只會在伺服器上獲取資料,因為我們是在服務端進行渲染.

再建立一個詳情頁,這裡用到了動態路由

新建 pages/detail/[id].js

import Layout from "../../components/MyLayout";
import fetch from "isomorphic-unfetch";
import Markdown from "react-markdown";

const Post = props => (
  <Layout>
    <h1>{props.show.name}</h1>
    <div className="markdown">
      <Markdown source={props.show.summary.replace(/<[/]?p>/g, "")} />
    </div>
    <img src={props.show.image.medium} />
    <style jsx global>{`
     .markdown {
        font-family: "Arial";
      }

     .markdown a {
        text-decoration: none;
        color: blue;
      }

     .markdown a:hover {
        opacity: 0.6;
      }

     .markdown h3 {
        margin: 0;
        padding: 0;
        text-transform: uppercase;
      }
    `}</style>
  </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;

點選 list 中的隨便一個,然後開啟控制檯和瀏覽器的 networks,會發現這次是在瀏覽器端進行介面請求.

樣式元件

Next.js 在 JS 框架中預載入了一個稱為 styled-jsx 的 CSS,該 CSS 使你的程式碼編寫更輕鬆.它允許您為元件編寫熟悉的 CSS 規則.規則對元件(甚至子元件)以外的任何東西都沒有影響.簡單來說就是帶有作用域的 css.

修改 pages/page.js

import Layout from "../components/MyLayout";
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 function Blog() {
  return (
    <Layout>
      <h1>My Blog</h1>
      <ul>
        {getPosts().map(post => (
          <li key={post.id}>
            <Link href="/p/[id]" as={`/p/${post.id}`}>
              <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: red;
        }

        a:hover {
          opacity: 0.6;
        }
      `}</style>
    </Layout>
  );
}

在上面的程式碼中,我們直接寫在模板字串中,而且必須使用模板字串({``})編寫 CSS .

此時修改一下程式碼

import Layout from "../components/MyLayout";
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" }
  ];
}

const PostLink = ({ post }) => (
  <li>
    <Link href="/p/[id]" as={`/p/${post.id}`}>
      <a>{post.title}</a>
    </Link>
  </li>
);

export default function Blog() {
  return (
    <Layout>
      <h1>My Blog</h1>
      <ul>
        {getPosts().map(post => (
          <PostLink key={post.id} post={post} />
        ))}
      </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 jsx 這種寫法樣式是有作用域,css 只能在當前作用域下生效.

解決 1 , 給子元件新增上子元件的樣式

const PostLink = ({ post }) => (
  <li>
    <Link href="/p/[id]" as={`/p/${post.id}`}>
      <a>{post.title}</a>
    </Link>
    <style jsx>{`
      li {
        list-style: none;
        margin: 5px 0;
      }

      a {
        text-decoration: none;
        color: blue;
        font-family: 'Arial';
      }

      a:hover {
        opacity: 0.6;
      }
    `}</style>
  </li>
);

解決 2 , 全域性樣式

 <style jsx global>{`
......css
 `}

一般不使用全域性樣式來解決

styled-jsx 文件

使用全域性樣式

有時,我們確實需要更改子元件內部的樣式.尤其是使用一些第三方庫樣式又有些不滿意的時候.

安裝 react-markdown

npm install --save react-markdown

修改 pages/post.js

import { useRouter } from "next/router";
import Layout from "../components/MyLayout";
import Markdown from "react-markdown";

const Content = () => {
  const router = useRouter();

  return (
    <Layout>
      <h1>{router.query.title}</h1>
      <div className="markdown">
        <Markdown
          source={`  # Live demo

Changes are automatically rendered as you type.

## Table of Contents

* Implements [GitHub Flavored Markdown](https://github.github.com/gfm/)
* Renders actual, "native" React DOM elements
* Allows you to escape or skip HTML (try toggling the checkboxes above)
* If you escape or skip the HTML, no dangerouslySetInnerHTML is used! Yay!

## HTML block below

<blockquote>
  This blockquote will change based on the HTML settings above.
</blockquote>`}
        />
      </div>
      <style jsx global>{`
       .markdown {
          font-family: "Arial";
        }

       .markdown a {
          text-decoration: none;
          color: blue;
        }

       .markdown a:hover {
          opacity: 0.6;
        }

       .markdown h3 {
          margin: 0;
          padding: 0;
          text-transform: uppercase;
        }
      `}</style>
    </Layout>
  );
};

const Page = () => (
  <Layout>
    <Content />
  </Layout>
);

export default Page;

開啟 localhost:6688 的 about 頁面點選檢視樣式效果

[其他解決方案]](https://github.com/zeit/next.js#css-in-js)

引入 ui 庫

目前程式碼在頁面中呈現的樣式是比較隨意的,秉承著能開啟就行的原則開發到這一步,是否應該稍微美化一下下.

引入 less

首先安裝需要的庫

npm install --save @zeit/next-less less

然後把 mylayout 和 header 裡面的行內樣式去掉

新建 assets/css/styles.less

.header {
  display: block;
  z-index: 500;
  width: 100%;
  height: 60px;
  font-size: 14px;
  background: #fff;
  color: rgba(0, 0, 0, 0.44);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
    Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
  letter-spacing: 0;
  font-weight: 400;
  font-style: normal;
  box-sizing: border-box;
  top: 0;

  &:after {
    box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.07);
    display: block;
    position: absolute;
    top: 60px;
    color: rgba(0, 0, 0, 0.07);
    content: "";
    width: 100%;
    height: 2px;
  }

 .header-inner {
    width: 1000px;
    margin: 0 auto;

    a {
      height: 60px;
      line-height: 60px;
      font-size: 18px;
      color: #c7c7c7;
      cursor: pointer;
      margin-right: 25px;
      &:hover {
        font-size: 18px;
        color: #2d2d2f;
      }
    }
  }
}

.content {
  width: 1000px;
  margin: 0 auto;
  padding-top: 30px;
}

修改 next.config.js

// next.config.js
const withLess = require('@zeit/next-less')
module.exports = withLess({
  /* config options here */
})

在 MyLayout 裡面引入 less

import "../assets/css/styles.less";

在 localhost:6688 檢視頁面出現相應的樣式

next-less 文件

引入 antd

npm install antd --save
npm install babel-plugin-import --save-dev

touch.babelrc

.babelrc

{
  "presets": ["next/babel"],
  "plugins": [
    [
      "import",
      {
        "libraryName": "antd",
        "style": "less"
      }
    ]
  ]
}

之後引入 antd 的樣式

assets/css/styles.less

@import "~antd/dist/antd.less";

這時候就是正常引入 antd 的元件進行使用就可以了

import { Typography, Card, Avatar } from "antd";
const { Title, Paragraph, Text } = Typography;

錯誤解決

ValidationError: Invalid options object. CSS Loader has been initialised using an options object that does not match the API schema. - options has an unknown property 'minimize'. These properties are valid: #541

新版中 css-loader 和 webpack 會出現這樣一個錯誤,這是升級過程中程式碼變更導致了,css-loader 已經沒有 minimize 這一選項.

解決方法,在 next.config.js 新增去除程式碼

const withLess = require("@zeit/next-less");

if (typeof require !== "undefined") {
  require.extensions[".less"] = file => {};
}

function HACK_removeMinimizeOptionFromCssLoaders(config) {
  console.warn(
    "HACK: Removing `minimize` option from `css-loader` entries in Webpack config"
  );
  config.module.rules.forEach(rule => {
    if (Array.isArray(rule.use)) {
      rule.use.forEach(u => {
        if (u.loader === "css-loader" && u.options) {
          delete u.options.minimize;
        }
      });
    }
  });
}

module.exports = withLess({
  lessLoaderOptions: {
    javascriptEnabled: true
  },
  webpack(config) {
    HACK_removeMinimizeOptionFromCssLoaders(config);
    return config;
  }
});

部署 Next.js 應用

先安裝 now,一個靜態資源託管伺服器

npm i -g now

now

等待一段時間之後會生成一個靜態連結,點選開啟就可以看到自己網頁的樣子了https://next-demo.fuhuodemao.now.sh/

zeit now 文件

打包生產環境程式碼

檢視 package.json 的 script

    "dev": "next -p 6688",
    "build": "next build",
    "start": "next start -p 6688",

現在執行命令來生成程式碼並預覽

npm run build // 構建用於生產的Next.js應用程式
npm start // 在6688埠上啟動Next.js應用程式.該伺服器將進行伺服器端渲染並提供靜態頁面

在 localhost:6688 上我們可以看到同樣的效果

開啟多個埠

修改 script 命令

 "start": "next start -p 6688",

然後執行npm start,我們可以在 localhost:8866 上再次開啟一個應用

在 window 下需要額外的工具 cross-env

npm install cross-env --save-dev

參考

  • 官方文件
  • learn next
  • 一箇中文文件