[Next] 一.初見next.js

next 簡介

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

next 特色

  • 默認狀況下由服務器呈現
  • 自動代碼拆分可加快頁面加載速度
  • 簡單的客戶端路由(基於頁面)
  • 基於 Webpack 的開發環境,支持熱模塊替換(HMR)
  • 可以與 Express 或任何其餘 Node.js HTTP 服務器一塊兒實現
  • 可以使用您本身的 Babel 和 Webpack 配置進行自定義

系統需求

Next.js 可與 Windows,Mac 和 Linux 一塊兒使用.您只須要在系統上安裝 Node.js 便可開始構建 Next.js 應用程序.若是有個編輯器就更好了css

初始化項目

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 文件來進行路由生成,且文件夾名字必須是pagesjava

而後打開 package.json 目錄中的 next-demo 文件並替換 scripts 爲如下內容:react

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

運行如下命令以啓動開發服務器:webpack

npm run dev

如今能夠打開 localhost:3000 來查看頁面效果,若是不喜歡 3000 或者端口衝突,執行下面命令git

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

這時候就能夠在 localhost:6688 上看到頁面效果了github

hello world

此時咱們在 pages 文件夾下建立一個 index.js 做爲首頁web

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

export default Index;

再次查看 localhost:6688 就能夠看到當前頁面顯示出 hello worldnpm

頁面間導航

next 中實現路由很是的簡便,新建 pages/about.jsjson

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%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()進行訪問.

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

參考

相關文章
相關標籤/搜索