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 將預取頁面,並且導航將在不刷新頁面的情況下進行.
使用 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.
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
盡管實現代碼的過程中以及官方案例中在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
`}
一般不使用全局樣式來解決
使用全局樣式
有時,我們確實需要更改子組件內部的樣式.尤其是使用一些第三方庫樣式又有些不滿意的時候.
安裝 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 查看頁面出現相應的樣式
引入 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
打包生產環境代碼
查看 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