[Next] 一.初見next.js


next 簡介

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

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 //創建pages

mkdir pages 這一步是必須創建一個叫 pages 的文件夾,因為 next 是根據 pages 下面的 js jsx tsx 文件來進行路由生成,且文件夾名字必須是pages

然后打開 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.

Link組件默認是將路由push進入瀏覽器記錄,所以點擊后退按鈕是返回上一頁.這一默認形式可以替換為replace,更改為<Link href="/about" replace>

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

如果將功能組件作為子組件進行傳遞,則需要將功能組件包裝React.forwardRef才能在<Link>使用

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

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

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

另外的客戶端導航是Router

import Router from 'next/router'

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

export default Index

Next.js更多關於路由route的內容

盡管實現代碼的過程中以及官方案例中在Link組件里將a標簽作為子元素傳進去,但是實際使用中,a標簽會造成路由切換失效的情況,酌情使用其他標簽代替.

組件

目前 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 Show() {
  return (
    <div>
      <Header />
      <p>Hello Next.js</p>
    </div>
  );
}

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

當前所使用的components這個名字並不是必須的,你可以將這個文件夾命名為任何名稱.next中固定且不能改變的文件夾只有兩個'pages'和'static'.next也並不限制將公共組件存放在pages里面,但最好不要在 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 Show() {
  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>
  );
}

同樣的效果Link使用對象形式

<Link href={{ pathname: '/post', query: { title: 'this is title' } }}>

依然同樣的效果使用Router

import Router from 'next/router'

const handler = () => {
  Router.push({
    pathname: '/about',
    query: { name: 'this is title },
  })
}

Router.push(url, as)與Link組件使用了相同的參數,第一個參數就是url,如果有第二參數,就是對應as

創建 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 看看不同,現在的頁面也具有一套完整的布局.

路由事件

可以通過Router監聽路由器內部發生的不同事件

// 監聽
Router.events.on('routeChangeStart', handleRouteChange)
// 關閉
Router.events.off('routeChangeStart', handleRouteChange)
  • routeChangeStart(url) 當路由開始改變時觸發
  • routeChangeComplete(url) 路由完全改變時觸發
  • routeChangeError(err, url) 更改路由時發生錯誤或取消路由負載時觸發
  • beforeHistoryChange(url) 在更改瀏覽器的歷史記錄之前觸發
  • hashChangeStart(url) 當哈希值改變但頁面不改變時觸發
  • hashChangeComplete(url) 哈希值更改但頁面未更改時觸發

官方表示getInitialProps情況下不建議使用路由器事件.如果需要最好是在組件加載后或者某些事件后進行監聽.

動態路由

當前我們的路由是這樣的 http://localhost:6688/post?title=Hello Next.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()進行訪問.

useRouter是一個React鈎子函數,它不能與類一起使用.類組件可以使用withRouter高階組件或將類包裝在功能組件中.同時withRouter也可以直接用於功能組件中.

在鏈接多個頁面,新建 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>
  );
}

<Link href={/p?id=${item.id}} as={/p/${item.id}}>,訪問路徑就是http://localhost:6688/p/975

<Link href={/p?id=${item.id}}>,訪問路徑就是http://localhost:6688/p?id=975

<Link href={/p?id=${item.id}} as={/post/${item.id}}>,起別名http://localhost:6688/post/975

在該頁面中我們看一下 元素,其中 href 屬性 p 文件夾中頁面的路徑, as 是要在瀏覽器的 URL 欄中顯示的 URL,這是next官方提供的一個路由遮擋功能,用來隱藏原本復雜的路由,顯示出來簡潔的路由.使網站路徑更簡潔.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();

  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,會發現這次是在瀏覽器端進行接口請求.

getInitialProps 上下文對象context具有以下屬性

  • pathname -URL的路徑部分
  • query -URL的查詢字符串部分被解析為對象
  • asPath- String實際路徑(包括查詢)的-在瀏覽器中顯示
  • req -HTTP請求對象(僅服務器)
  • res -HTTP響應對象(僅服務器)
  • err -渲染期間遇到任何錯誤的錯誤對象

給組件添加樣式

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 頁面點擊查看樣式效果

其他解決方案

引入 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://react-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

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM