[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 將預取頁面,並且導航將在不重新整理頁面的情況下進行.
使用 Link API
修改 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
- 一箇中文文件