Next.js中文说明

Next.js 是一个轻量级的 React 服务端渲染应用框架。

可访问 nextjs.org/learn 开始学习 Next.js.

怎么使用

安装

在项目文件夹中运行:

1
npm install --save next react react-dom

将下面脚本添加到 package.json 中:

1
2
3
4
5
6
7
{
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
}
}
本文同时提供以下语言的翻译: English.

下面, 文件系统是主要的 API. 每个.js 文件将变成一个路由,自动处理和渲染。

新建 ./pages/index.js 到你的项目中:

1
export default () => <div>Welcome to next.js!</div>;

运行 npm run dev 命令并打开 http://localhost:3000。 要使用其他端口,你可以运行 npm run dev -- -p <your port here>.

到目前为止,我们做到:

  • 自动打包编译 (使用 webpack 和 babel)
  • 热加载
  • ./pages作为服务的渲染和索引
  • 静态文件服务. ./static/ 映射到 /static/ (可以 创建一个静态目录 在你的项目中)

这里有个简单的案例,可以下载看看 sample app - nextgram

代码自动分割

每个页面只会导入import中绑定以及被用到的代码. 这意味着页面不会加载不必要的代码

1
2
3
import cowsay from "cowsay-browser";

export default () => <pre>{cowsay.say({ text: "hi there!" })}</pre>;

CSS

支持嵌入样式

案例

我们绑定 styled-jsx 来生成独立作用域的 CSS. 目标是支持 “shadow CSS”,但是 不支持独立模块作用域的 JS.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default () => (
<div>
Hello world
<p>scoped!</p>
<style jsx>{`
p {
color: blue;
}
div {
background: red;
}
@media (max-width: 600px) {
div {
background: blue;
}
}
`}</style>
<style global jsx>{`
body {
background: black;
}
`}</style>
</div>
);

想查看更多案例可以点击 styled-jsx documentation.

内嵌样式

Examples

有些情况可以使用 CSS 内嵌 JS 写法。如下所示:

1
export default () => <p style={{ color: "red" }}>hi there</p>;

更复杂的内嵌样式解决方案,特别是服务端渲染时的样式更改。我们可以通过包裹自定义 Document,来添加样式,案例如下:custom <Document>

使用 CSS / Sass / Less / Stylus files

支持用.css, .scss, .less or .styl,需要配置默认文件 next.config.js,具体可查看下面链接

静态文件服务(如图像)

在根目录下新建文件夹叫static。代码可以通过/static/来引入相关的静态资源。

1
export default () => <img src="/static/my-image.png" alt="my image" />;

注意:不要自定义静态文件夹的名字,只能叫static ,因为只有这个名字 Next.js 才会把它当作静态资源。

生成<head>

<head>

Examples

我们设置一个内置组件来装载<head>到页面中。

1
2
3
4
5
6
7
8
9
10
11
import Head from "next/head";

export default () => (
<div>
<Head>
<title>My page title</title>
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
</Head>
<p>Hello world!</p>
</div>
);

我们定义key属性来避免重复的<head>标签,保证<head>只渲染一次,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Head from "next/head";
export default () => (
<div>
<Head>
<title>My page title</title>
<meta
name="viewport"
content="initial-scale=1.0, width=device-width"
key="viewport"
/>
</Head>
<Head>
<meta
name="viewport"
content="initial-scale=1.2, width=device-width"
key="viewport"
/>
</Head>
<p>Hello world!</p>
</div>
);

只有第二个<meta name="viewport" />才被渲染。

注意:在卸载组件时,<head>的内容将被清除。请确保每个页面都在其<head>定义了所需要的内容,而不是假设其他页面已经加过了

获取数据以及组件生命周期

Examples

当你需要状态,生命周期钩子或初始数据填充时,你可以导出React.Component(而不是上面的无状态函数),如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
import React from "react";

export default class extends React.Component {
static async getInitialProps({ req }) {
const userAgent = req ? req.headers["user-agent"] : navigator.userAgent;
return { userAgent };
}

render() {
return <div>Hello World {this.props.userAgent}</div>;
}
}

请注意,当页面渲染时加载数据,我们使用了一个异步静态方法getInitialProps。它能异步获取 JS 普通对象,并绑定在props上。

当服务渲染时,getInitialProps将会把数据序列化,就像JSON.stringify。所以确保getInitialProps返回的是一个普通 JS 对象,而不是Date, MapSet类型。

当页面初次加载时,getInitialProps只会在服务端执行一次。getInitialProps只有在路由切换的时候(如Link组件跳转或路由自定义跳转)时,客户端的才会被执行。

当页面初始化加载时,getInitialProps仅在服务端上执行。只有当路由跳转(Link组件跳转或 API 方法跳转)时,客户端才会执行getInitialProps

注意:getInitialProps将不能在子组件中使用。只能在pages页面中使用。


只有服务端用到的模块放在getInitialProps里,请确保正确的导入了它们,可参考import them properly
否则会拖慢你的应用速度。


你也可以给无状态组件定义getInitialProps

1
2
3
4
5
6
7
8
9
const Page = ({ stars }) => <div>Next stars: {stars}</div>;

Page.getInitialProps = async ({ req }) => {
const res = await fetch("https://api.github.com/repos/zeit/next.js");
const json = await res.json();
return { stars: json.stargazers_count };
};

export default Page;

getInitialProps入参对象的属性如下:

  • pathname - URL 的 path 部分
  • query - URL 的 query 部分,并被解析成对象
  • asPath - 显示在浏览器中的实际路径(包含查询部分),为String类型
  • req - HTTP 请求对象 (仅限服务器端)
  • res - HTTP 返回对象 (仅限服务器端)
  • jsonPageRes - 获取响应对象(仅限客户端)
  • err - 渲染过程中的任何错误

路由

Next.js不会随应用程序中每个可能的路由一起发布路由清单,因此当前页面不知道客户端上的任何其他页面。出于可扩展性考虑,所有后续路由都会惰性加载。

<Link>用法

Examples

可以用 <Link> 组件实现客户端的路由切换。

基本例子

参考下面的两个页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// pages/index.js
import Link from 'next/link'

function Home() {
return (
<div>
Click{' '}
<Link href="/about">
<a>here</a>
</Link>{' '}
to read more
</div>
)
}

export default Home
1
2
3
4
5
6
// pages/about.js
function About() {
return <p>Welcome to About!</p>
}

export default About

自定义路由 (使用URL中的props)

<Link> 组件有两个主要属性:

  • href: pages目录内的路径+查询字符串.
  • as: 将在浏览器URL栏中呈现的路径.

例子:

  1. 假设你有个这样的路由 /post/:slug.

  2. 你可以创建文件 pages/post.js

1
2
3
4
5
6
7
8
9
10
11
class Post extends React.Component {
static async getInitialProps({ query }) {
console.log('SLUG', query.slug)
return {}
}
render() {
return <h1>My blog post</h1>
}
}

export default Post
  1. 将路由添加到 express (或者其他服务端) 的 server.js 文件 (这仅适用于SSR). 这将解析/post/:slugpages/post.js并在getInitialProps中提供slug作为查询的一部分。
1
2
3
server.get('/post/:slug', (req, res) => {
return app.render(req, res, '/post', { slug: req.params.slug })
})
  1. 对于客户端路由,使用 next/link:
    1
    <Link href="/post?slug=something" as="/post/something">

注意:可以使用<Link prefetch>使链接和预加载在后台同时进行,来达到页面的最佳性能。

客户端路由行为与浏览器很相似:

  1. 获取组件
  2. 如果组件定义了getInitialProps,则获取数据。如果有错误情况将会渲染 _error.js
  3. 1 和 2 都完成了,pushState执行,新组件被渲染。

如果需要注入pathname, queryasPath到你组件中,你可以使用withRouter

URL 对象

Examples

组件<Link>接收 URL 对象,而且它会自动格式化生成 URL 字符串

1
2
3
4
5
6
7
8
9
10
11
12
// pages/index.js
import Link from "next/link";

export default () => (
<div>
Click{" "}
<Link href={{ pathname: "/about", query: { name: "Zeit" } }}>
<a>here</a>
</Link>{" "}
to read more
</div>
);

将生成 URL 字符串/about?name=Zeit,你可以使用任何在Node.js URL module documentation定义过的属性。

替换路由

<Link>组件默认将新 url 推入路由栈中。你可以使用replace属性来防止添加新输入。

1
2
3
4
5
6
7
8
9
10
11
12
// pages/index.js
import Link from "next/link";

export default () => (
<div>
Click{" "}
<Link href="/about" replace>
<a>here</a>
</Link>{" "}
to read more
</div>
);
组件支持点击事件 onClick

<Link>支持每个组件所支持的onClick事件。如果你不提供<a>标签,只会处理onClick事件而href将不起作用。

1
2
3
4
5
6
7
8
9
10
11
// pages/index.js
import Link from "next/link";

export default () => (
<div>
Click{" "}
<Link href="/about">
<img src="/static/image.png" alt="image" />
</Link>
</div>
);
暴露 href 给子元素

如子元素是一个没有 href 属性的<a>标签,我们将会指定它以免用户重复操作。然而有些时候,我们需要里面有<a>标签,但是Link组件不会被识别成超链接,结果不能将href传递给子元素。在这种场景下,你可以定义一个Link组件中的布尔属性passHref,强制将href传递给子元素。

注意: 使用a之外的标签而且没有通过passHref的链接可能会使导航看上去正确,但是当搜索引擎爬行检测时,将不会识别成链接(由于缺乏 href 属性),这会对你网站的 SEO 产生负面影响。

1
2
3
4
5
6
7
8
import Link from "next/link";
import Unexpected_A from "third-library";

export default ({ href, name }) => (
<Link href={href} passHref>
<Unexpected_A>{name}</Unexpected_A>
</Link>
);
禁止滚动到页面顶部

<Link>的默认行为就是滚到页面顶部。当有 hash 定义时(#),页面将会滚动到对应的 id 上,就像<a>标签一样。为了预防滚动到顶部,可以给<Link>
scroll={false}属性:

1
2
<Link scroll={false} href="/?counter=10"><a>Disables scrolling</a></Link>
<Link href="/?counter=10"><a>Changes with scrolling to top</a></Link>

命令式

Examples

你也可以用next/router实现客户端路由切换

1
2
3
4
5
6
7
import Router from "next/router";

export default () => (
<div>
Click <span onClick={() => Router.push("/about")}>here</span> to read more
</div>
);

拦截器 popstate

有些情况(比如使用custom router),你可能想监听popstate,在路由跳转前做一些动作。
比如,你可以操作 request 或强制 SSR 刷新

1
2
3
4
5
6
7
8
9
10
11
12
import Router from "next/router";

Router.beforePopState(({ url, as, options }) => {
// I only want to allow these two routes!
if (as !== "/" || as !== "/other") {
// Have SSR render bad routes as a 404.
window.location.href = as;
return false;
}

return true;
});

如果你在beforePopState中返回 false,Router将不会执行popstate事件。
例如Disabling File-System Routing

以上Router对象的 API 如下:

  • route - 当前路由,为String类型
  • pathname - 不包含查询内容的当前路径,为String类型
  • query - 查询内容,被解析成Object类型. 默认为{}
  • asPath - 展现在浏览器上的实际路径,包含查询内容,为String类型
  • push(url, as=url) - 用给定的url调用pushState
  • replace(url, as=url) - 用给定的url调用replaceState
  • beforePopState(cb=function) - 在路由器处理事件之前拦截.

pushreplace 函数的第二个参数as,是为了装饰 URL 作用。如果你在服务器端设置了自定义路由将会起作用。

URL 对象用法

pushreplace可接收的 URL 对象(<Link>组件的 URL 对象一样)来生成 URL。

1
2
3
4
5
6
7
8
9
10
11
12
13
import Router from "next/router";

const handler = () =>
Router.push({
pathname: "/about",
query: { name: "Zeit" }
});

export default () => (
<div>
Click <span onClick={handler}>here</span> to read more
</div>
);

也可以像<Link>组件一样添加额外的参数。

路由事件

你可以监听路由相关事件。
下面是支持的事件列表:

  • routeChangeStart(url) - 路由开始切换时触发
  • routeChangeComplete(url) - 完成路由切换时触发
  • routeChangeError(err, url) - 路由切换报错时触发
  • beforeHistoryChange(url) - 浏览器 history 模式开始切换时触发
  • hashChangeStart(url) - 开始切换 hash 值但是没有切换页面路由时触发
  • hashChangeComplete(url) - 完成切换 hash 值但是没有切换页面路由时触发

这里的url是指显示在浏览器中的 url。如果你用了Router.push(url, as)(或类似的方法),那浏览器中的 url 将会显示 as 的值。

下面是如何正确使用路由事件routeChangeStart的例子:

1
2
3
4
5
const handleRouteChange = url => {
console.log("App is changing to: ", url);
};

Router.events.on("routeChangeStart", handleRouteChange);

如果你不想再监听该事件,你可以用off事件去取消监听:

1
Router.events.off("routeChangeStart", handleRouteChange);

如果路由加载被取消(比如快速连续双击链接),routeChangeError将触发。传递err,并且属性cancelled的值为true。

1
2
3
4
5
Router.events.on("routeChangeError", (err, url) => {
if (err.cancelled) {
console.log(`Route to ${url} was cancelled!`);
}
});
浅层路由

Examples

浅层路由允许你改变 URL 但是不执行getInitialProps生命周期。你可以加载相同页面的 URL,得到更新后的路由属性pathnamequery,并不失去 state 状态。

你可以给Router.pushRouter.replace方法加shallow: true参数。如下面的例子所示:

1
2
3
4
// Current URL is "/"
const href = "/?counter=10";
const as = href;
Router.push(href, as, { shallow: true });

现在 URL 更新为/?counter=10。在组件里查看this.props.router.query你将会看到更新的 URL。

你可以在componentdidupdate钩子函数中监听 URL 的变化。

1
2
3
4
5
6
7
componentDidUpdate(prevProps) {
const { pathname, query } = this.props.router
// verify props have changed to avoid an infinite loop
if (query.id !== prevProps.router.query.id) {
// fetch data based on the new query
}
}

注意:

浅层路由只作用于相同 URL 的参数改变,比如我们假定有个其他路由about,而你向下面代码样运行:

1
Router.push("/?counter=10", "/about?counter=10", { shallow: true });

那么这将会出现新页面,即使我们加了浅层路由,但是它还是会卸载当前页,会加载新的页面并触发新页面的getInitialProps

高阶组件

Examples

如果你想应用里每个组件都处理路由对象,你可以使用withRouter高阶组件。下面是如何使用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { withRouter } from "next/router";

const ActiveLink = ({ children, router, href }) => {
const style = {
marginRight: 10,
color: router.pathname === href ? "red" : "black"
};

const handleClick = e => {
e.preventDefault();
router.push(href);
};

return (
<a href={href} onClick={handleClick} style={style}>
{children}
</a>
);
};

export default withRouter(ActiveLink);

上面路由对象的 API 可以参考next/router.

预加载页面

⚠️ 只有生产环境才有此功能 ⚠️

Examples

Next.js 有允许你预加载页面的 API。

用 Next.js 服务端渲染你的页面,可以达到所有你应用里所有未来会跳转的路径即时响应,有效的应用 Next.js,可以通过预加载应用程序的功能,最大程度的初始化网站性能。查看更多.

Next.js 的预加载功能只预加载 JS 代码。当页面渲染时,你可能需要等待数据请求。

<Link>用法

你可以给添加 prefetch 属性,Next.js 将会在后台预加载这些页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import Link from "next/link";

// example header component
export default () => (
<nav>
<ul>
<li>
<Link prefetch href="/">
<a>Home</a>
</Link>
</li>
<li>
<Link prefetch href="/about">
<a>About</a>
</Link>
</li>
<li>
<Link prefetch href="/contact">
<a>Contact</a>
</Link>
</li>
</ul>
</nav>
);

命令式 prefetch 写法

大多数预加载是通过处理的,但是我们还提供了命令式 API 用于更复杂的场景。

1
2
3
4
5
6
7
8
9
10
11
import { withRouter } from "next/router";

export default withRouter(({ router }) => (
<div>
<a onClick={() => setTimeout(() => router.push("/dynamic"), 100)}>
A route transition will happen after 100ms
</a>
{// but we can prefetch it!
router.prefetch("/dynamic")}
</div>
));

路由实例只允许在应用程序的客户端。以防服务端渲染发生错误,建议 prefetch 事件写在componentDidMount()生命周期里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from "react";
import { withRouter } from "next/router";

class MyLink extends React.Component {
componentDidMount() {
const { router } = this.props;
router.prefetch("/dynamic");
}

render() {
const { router } = this.props;
return (
<div>
<a onClick={() => setTimeout(() => router.push("/dynamic"), 100)}>
A route transition will happen after 100ms
</a>
</div>
);
}
}

export default withRouter(MyLink);

自定义服务端路由

Examples

一般你使用next start命令来启动 next 服务,你还可以编写代码来自定义路由,如使用路由正则等。

当使用自定义服务文件,如下面例子所示叫 server.js 时,确保你更新了 package.json 中的脚本。

1
2
3
4
5
6
7
{
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "NODE_ENV=production node server.js"
}
}

下面这个例子使 /a 路由解析为./pages/b,以及/b 路由解析为./pages/a;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// This file doesn't go through babel or webpack transformation.
// Make sure the syntax and sources this file requires are compatible with the current node version you are running
// See https://github.com/zeit/next.js/issues/1245 for discussions on Universal Webpack or universal Babel
const { createServer } = require("http");
const { parse } = require("url");
const next = require("next");

const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();

app.prepare().then(() => {
createServer((req, res) => {
// Be sure to pass `true` as the second argument to `url.parse`.
// This tells it to parse the query portion of the URL.
const parsedUrl = parse(req.url, true);
const { pathname, query } = parsedUrl;

if (pathname === "/a") {
app.render(req, res, "/b", query);
} else if (pathname === "/b") {
app.render(req, res, "/a", query);
} else {
handle(req, res, parsedUrl);
}
}).listen(3000, err => {
if (err) throw err;
console.log("> Ready on http://localhost:3000");
});
});

next的 API 如下所示

  • next(opts: object)

opts 的属性如下:

  • dev (boolean) 判断 Next.js 应用是否在开发环境 - 默认false
  • dir (string) Next 项目路径 - 默认'.'
  • quiet (boolean) 是否隐藏包含服务端消息在内的错误信息 - 默认false
  • conf (object) 与next.config.js的对象相同 - 默认{}

生产环境的话,可以更改 package.json 里的start脚本为NODE_ENV=production node server.js

禁止文件路由

默认情况,Next将会把/pages下的所有文件匹配路由(如/pages/some-file.js 渲染为 site.com/some-file

如果你的项目使用自定义路由,那么有可能不同的路由会得到相同的内容,可以优化 SEO 和用户体验。

禁止路由链接到/pages下的文件,只需设置next.config.js文件如下所示:

1
2
3
4
// next.config.js
module.exports = {
useFileSystemPublicRoutes: false
};

注意useFileSystemPublicRoutes只禁止服务端的文件路由;但是客户端的还是禁止不了。

你如果想配置客户端路由不能跳转文件路由,可以参考Intercepting popstate

动态前缀

有时你需要设置动态前缀,可以在请求时设置assetPrefix改变前缀。

使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const next = require("next");
const micro = require("micro");

const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handleNextRequests = app.getRequestHandler();

app.prepare().then(() => {
const server = micro((req, res) => {
// Add assetPrefix support based on the hostname
if (req.headers.host === "my-app.com") {
app.setAssetPrefix("http://cdn.com/myapp");
} else {
app.setAssetPrefix("");
}

handleNextRequests(req, res);
});

server.listen(port, err => {
if (err) {
throw err;
}

console.log(`> Ready on http://localhost:${port}`);
});
});

动态导入

Examples

ext.js 支持 JavaScript 的 TC39 提议dynamic import proposal。你可以动态导入 JavaScript 模块(如 React 组件)。

动态导入相当于把代码分成各个块管理。Next.js 服务端动态导入功能,你可以做很多炫酷事情。

下面介绍一些动态导入方式:

1. 基础用法 (也就是SSR)

1
2
3
4
5
6
7
8
9
10
11
import dynamic from "next/dynamic";

const DynamicComponent = dynamic(import("../components/hello"));

export default () => (
<div>
<Header />
<DynamicComponent />
<p>HOME PAGE is here!</p>
</div>
);

2. 自定义加载组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import dynamic from "next/dynamic";

const DynamicComponentWithCustomLoading = dynamic(
import("../components/hello2"),
{
loading: () => <p>...</p>
}
);

export default () => (
<div>
<Header />
<DynamicComponentWithCustomLoading />
<p>HOME PAGE is here!</p>
</div>
);

3. 禁止使用 SSR

1
2
3
4
5
6
7
8
9
10
11
12
13
import dynamic from "next/dynamic";

const DynamicComponentWithNoSSR = dynamic(import("../components/hello3"), {
ssr: false
});

export default () => (
<div>
<Header />
<DynamicComponentWithNoSSR />
<p>HOME PAGE is here!</p>
</div>
);

4. 同时加载多个模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import dynamic from "next/dynamic";

const HelloBundle = dynamic({
modules: () => {
const components = {
Hello1: import("../components/hello1"),
Hello2: import("../components/hello2")
};

return components;
},
render: (props, { Hello1, Hello2 }) => (
<div>
<h1>{props.title}</h1>
<Hello1 />
<Hello2 />
</div>
)
});

export default () => <HelloBundle title="Dynamic Bundle" />;

自定义 <App>

Examples

组件来初始化页面。你可以重写它来控制页面初始化,如下面的事:

  • 当页面变化时保持页面布局
  • 当路由变化时保持页面状态
  • 使用componentDidCatch自定义处理错误
  • 注入额外数据到页面里 (如 GraphQL 查询)

重写的话,新建./pages/_app.js文件,重写 App 模块如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import App, { Container } from "next/app";
import React from "react";

export default class MyApp extends App {
static async getInitialProps({ Component, router, ctx }) {
let pageProps = {};

if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}

return { pageProps };
}

render() {
const { Component, pageProps } = this.props;
return (
<Container>
<Component {...pageProps} />
</Container>
);
}
}

自定义 <Document>

Examples

  • 在服务端呈现
  • 初始化服务端时添加文档标记元素
  • 通常实现服务端渲染会使用一些 css-in-js 库,如styled-components, glamorousemotionstyled-jsx是 Next.js 自带默认使用的 css-in-js 库

Next.js会自动定义文档标记,比如,你从来不需要添加<html>, <body>等。如果想自定义文档标记,你可以新建./pages/_document.js,然后扩展Document类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// _document is only rendered on the server side and not on the client side
// Event handlers like onClick can't be added to this file

// ./pages/_document.js
import Document, { Head, Main, NextScript } from "next/document";

export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx);
return { ...initialProps };
}

render() {
return (
<html>
<Head>
<style>{`body { margin: 0 } /* custom! */`}</style>
</Head>
<body className="custom_class">
<Main />
<NextScript />
</body>
</html>
);
}
}

钩子getInitialProps接收到的参数ctx对象都是一样的

  • 回调函数renderPage是会执行 React 渲染逻辑的函数(同步),这种做法有助于此函数支持一些类似于 Aphrodite 的 renderStatic 等一些服务器端渲染容器。

注意:<Main />外的 React 组件将不会渲染到浏览器中,所以那添加应用逻辑代码。如果你页面需要公共组件(菜单或工具栏),可以参照上面说的App组件代替。

自定义 renderPage

🚧 应该注意的是,您应该定制“renderPage”的唯一原因是使用css-in-js库,需要将应用程序包装起来以正确使用服务端渲染。 🚧

  • 它将一个选项对象作为参数进行进一步的自定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import Document from 'next/document'

class MyDocument extends Document {
static async getInitialProps(ctx) {
const originalRenderPage = ctx.renderPage

ctx.renderPage = () =>
originalRenderPage({
// useful for wrapping the whole react tree
enhanceApp: App => App,
// useful for wrapping in a per-page basis
enhanceComponent: Component => Component
})

// Run the parent `getInitialProps` using `ctx` that now includes our custom `renderPage`
const initialProps = await Document.getInitialProps(ctx)

return initialProps
}
}

export default MyDocument

自定义错误处理

404 和 500 错误客户端和服务端都会通过error.js组件处理。如果你想改写它,则新建_error.js在文件夹中:

⚠️ 该pages/_error.js组件仅用于生产。在开发过程中,您会收到调用堆栈错误,以了解错误源自何处。 ⚠️

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from "react";

export default class Error extends React.Component {
static getInitialProps({ res, err }) {
const statusCode = res ? res.statusCode : err ? err.statusCode : null;
return { statusCode };
}

render() {
return (
<p>
{this.props.statusCode
? `An error ${this.props.statusCode} occurred on server`
: "An error occurred on client"}
</p>
);
}
}

渲染内置错误页面

如果你想渲染内置错误页面,你可以使用next/error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from "react";
import Error from "next/error";
import fetch from "isomorphic-unfetch";

export default class Page extends React.Component {
static async getInitialProps() {
const res = await fetch("https://api.github.com/repos/zeit/next.js");
const statusCode = res.statusCode > 200 ? res.statusCode : false;
const json = await res.json();

return { statusCode, stars: json.stargazers_count };
}

render() {
if (this.props.statusCode) {
return <Error statusCode={this.props.statusCode} />;
}

return <div>Next stars: {this.props.stars}</div>;
}
}

如果你自定义了个错误页面,你可以引入自己的错误页面来代替next/error

自定义配置

如果你想自定义 Next.js 的高级配置,可以在根目录下新建next.config.js文件(与pages/package.json一起)

注意:next.config.js是一个 Node.js 模块,不是一个 JSON 文件,可以用于 Next 启动服务已经构建阶段,但是不作用于浏览器端。

1
2
3
4
// next.config.js
module.exports = {
/* config options here */
};

或使用一个函数:

1
2
3
4
5
6
7
module.exports = (phase, { defaultConfig }) => {
//
// https://github.com/zeit/
return {
/* config options here */
};
};

phase是配置文件被加载时的当前内容。你可看到所有的 phases 常量:constants
这些常量可以通过next/constants引入:

1
2
3
4
5
6
7
8
9
10
11
12
const { PHASE_DEVELOPMENT_SERVER } = require("next/constants");
module.exports = (phase, { defaultConfig }) => {
if (phase === PHASE_DEVELOPMENT_SERVER) {
return {
/* development only config options here */
};
}

return {
/* config options for all phases except development here */
};
};

设置自定义构建目录

你可以自定义一个构建目录,如新建build文件夹来代替.next 文件夹成为构建目录。如果没有配置构建目录,构建时将会自动新建.next文件夹

1
2
3
4
// next.config.js
module.exports = {
distDir: "build"
};

禁止 etag 生成

你可以禁止 etag 生成根据你的缓存策略。如果没有配置,Next 将会生成 etags 到每个页面中。

1
2
3
4
// next.config.js
module.exports = {
generateEtags: false
};

配置 onDemandEntries

Next 暴露一些选项来给你控制服务器部署以及缓存页面:

1
2
3
4
5
6
7
8
module.exports = {
onDemandEntries: {
// period (in ms) where the server will keep pages in the buffer
maxInactiveAge: 25 * 1000,
// number of pages that should be kept simultaneously without being disposed
pagesBufferLength: 2
}
};

这个只是在开发环境才有的功能。如果你在生成环境中想缓存 SSR 页面,请查看SSR-caching

配置解析路由时的页面文件后缀名

如 typescript 模块@zeit/next-typescript,需要支持解析后缀名为.ts的文件。pageExtensions 允许你扩展后缀名来解析各种 pages 下的文件。

1
2
3
4
// next.config.js
module.exports = {
pageExtensions: ["jsx", "js"]
};

配置构建 ID

Next.js 使用构建时生成的常量来标识你的应用服务是哪个版本。在每台服务器上运行构建命令时,可能会导致多服务器部署出现问题。为了保持同一个构建 ID,可以配置generateBuildId函数:

1
2
3
4
5
6
7
// next.config.js
module.exports = {
generateBuildId: async () => {
// For example get the latest git commit hash here
return "my-build-id";
}
};

自定义 webpack 配置

Examples

可以使用些一些常见的模块

注意: webpack方法将被执行两次,一次在服务端一次在客户端。你可以用isServer属性区分客户端和服务端来配置

多配置可以组合在一起,如:

1
2
3
4
5
6
7
8
9
10
11
const withTypescript = require("@zeit/next-typescript");
const withSass = require("@zeit/next-sass");

module.exports = withTypescript(
withSass({
webpack(config, options) {
// Further custom configuration here
return config;
}
})
);

为了扩展webpack使用,可以在next.config.js定义函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// next.config.js is not transformed by Babel. So you can only use javascript features supported by your version of Node.js.

module.exports = {
webpack: (config, { buildId, dev, isServer, defaultLoaders }) => {
// Perform customizations to webpack config
// Important: return the modified config
return config;
},
webpackDevMiddleware: config => {
// Perform customizations to webpack dev middleware config
// Important: return the modified config
return config;
}
};

webpack的第二个参数是个对象,你可以自定义配置它,对象属性如下所示:

  • buildId - 字符串类型,构建的唯一标示
  • dev - Boolean型,判断你是否在开发环境下
  • isServer - Boolean 型,为true使用在服务端, 为false使用在客户端.
  • defaultLoaders - 对象型 ,内部加载器, 你可以如下配置
    • babel - 对象型,配置babel-loader.
    • hotSelfAccept - 对象型, hot-self-accept-loader配置选项.这个加载器只能用于高阶案例。如 @zeit/next-typescript添加顶层 typescript 页面。

defaultLoaders.babel使用案例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Example next.config.js for adding a loader that depends on babel-loader
// This source was taken from the @zeit/next-mdx plugin source:
// https://github.com/zeit/next-plugins/blob/master/packages/next-mdx
module.exports = {
webpack: (config, {}) => {
config.module.rules.push({
test: /\.mdx/,
use: [
options.defaultLoaders.babel,
{
loader: "@mdx-js/loader",
options: pluginOptions.options
}
]
});

return config;
}
};

自定义 babel 配置

Examples

为了扩展方便我们使用babel,可以在应用根目录新建.babelrc文件,该文件可配置。

如果有该文件,我们将会考虑数据源,因此也需要定义 next 项目需要的东西,也就是 next/babel预设。

这种设计方案将会使你不诧异于我们可以定制 babel 配置。

下面是.babelrc文件案例:

1
2
3
4
{
"presets": ["next/babel"],
"plugins": []
}

next/babel预设可处理各种 React 应用所需要的情况。包括:

  • preset-env
  • preset-react
  • plugin-proposal-class-properties
  • plugin-proposal-object-rest-spread
  • plugin-transform-runtime
  • styled-jsx

presets / plugins 不允许添加到.babelrc中,然而你可以配置next/babel预设:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"presets": [
[
"next/babel",
{
"preset-env": {},
"transform-runtime": {},
"styled-jsx": {},
"class-properties": {}
}
]
],
"plugins": []
}

"preset-env"模块选项应该保持为 false,否则 webpack 代码分割将被禁用。

暴露配置到服务端和客户端

在应用程序中通常需要提供配置值

Next.js支持2种提供配置的方式:

  • 构建时配置
  • 运行时配置

构建时配置

构建时配置的工作方式是将提供的值内联到Javascript包中。

你可以在next.config.js设置env:

1
2
3
4
5
6
// next.config.js
module.exports = {
env: {
customKey: 'value'
}
}

这将允许你在代码中使用process.env.customKey,例如:

1
2
3
4
5
6
// pages/index.js
function Index() {
return <h1>The value of customKey is: {process.env.customKey}</h1>
}

export default Index

运行时配置

⚠️ 请注意,使用此选项时不可用 target: 'serverless'

⚠️ 通常,您希望使用构建时配置来提供配置。原因是运行时配置增加了一个小的rendering/initialization开销。

next/config模块使你应用运行时可以读取些存储在next.config.js的配置项。serverRuntimeConfig属性只在服务器端可用,publicRuntimeConfig属性在服务端和客户端可用。

1
2
3
4
5
6
7
8
9
10
11
12
// next.config.js
module.exports = {
serverRuntimeConfig: {
// Will only be available on the server side
mySecret: 'secret',
secondSecret: process.env.SECOND_SECRET // Pass through env variables
},
publicRuntimeConfig: {
// Will be available on both server and client
staticFolder: '/static'
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// pages/index.js
import getConfig from 'next/config'
// Only holds serverRuntimeConfig and publicRuntimeConfig from next.config.js nothing else.
const { serverRuntimeConfig, publicRuntimeConfig } = getConfig()

console.log(serverRuntimeConfig.mySecret) // Will only be available on the server side
console.log(publicRuntimeConfig.staticFolder) // Will be available on both server and client

function MyImage() {
return (
<div>
<img src={`${publicRuntimeConfig.staticFolder}/logo.png`} alt="logo" />
</div>
)
}

export default MyImage

启动服务选择 hostname

启动开发环境服务可以设置不同的 hostname,你可以在启动命令后面加上--hostname 主机名-H 主机名。它将会启动一个 TCP 服务器来监听连接所提供的主机。

CDN 支持前缀

建立一个 CDN,你能配置assetPrefix选项,去配置你的 CDN 源。

1
2
3
4
5
const isProd = process.env.NODE_ENV === "production";
module.exports = {
// You may only need to add assetPrefix in the production.
assetPrefix: isProd ? "https://cdn.mydomain.com" : ""
};

注意:Next.js 运行时将会自动添加前缀,但是对于/static是没有效果的,如果你想这些静态资源也能使用 CDN,你需要自己添加前缀。有一个方法可以判断你的环境来加前缀,如 in this example

项目部署

部署中,你可以先构建打包生成环境代码,再启动服务。因此,构建和启动分为下面两条命令:

1
2
next build
next start

例如,使用now去部署package.json配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
{
"name": "my-app",
"dependencies": {
"next": "latest"
},
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
}
}

然后就可以直接运行now了。

Next.js 也有其他托管解决方案。请查考 wiki 章节‘Deployment’

注意:NODE_ENV可以通过next命令配置,如果没有配置,会最大渲染,如果你使用编程式写法的话programmatically,你需要手动设置NODE_ENV=production

注意:推荐将.next或自定义打包文件夹custom dist folder放入.gitignore.npmignore中。否则,使用filesnow.files
添加部署白名单,并排除.next或自定义打包文件夹。

无服务器部署

例子

无服务器部署通过将应用程序拆分为更小的部分(也称为lambdas)来显着提高可靠性和可伸缩性。在Next.js中,pages目录中的每个页面都变成了无服务器的lambda。
对于无服务器的人来说,有许多好处。引用的链接在Express的上下文中讨论了其中的一些,但这些原则普遍适用:无服务器允许分布式故障点,无限的可扩展性,并且通过“为您使用的内容付费”的模式来提供难以置信的价格。

要在Next.js中启用无服务器模式,可在Next.config.js中配置target值为serverless:

1
2
3
4
// next.config.js
module.exports = {
target: 'serverless'
}

serverless将每页输出一个lambda。此文件是完全独立的,不需要运行任何依赖项:

  • pages/index.js => .next/serverless/pages/index.js
  • pages/about.js => .next/serverless/pages/about.js

Next.js无服务器功能的签名类似于Node.js HTTP服务器回调:

1
export function render(req: http.IncomingMessage, res: http.ServerResponse) => void

使用无服务配置, 你可以讲Next.js部署到ZEIT Now 并提供所有的好处和易于控制; custom routes 缓存头. 要了解更多信息,请参阅 ZEIT Guide for Deploying Next.js with Now

降级部署

Next.js为无服务器部署提供低级API,因为托管平台具有不同的功能签名。通常,您需要使用兼容性层包装Next.js无服务器构建的输出。

例如,如果平台支持Node.jshttp.Server类:

1
2
3
4
const http = require('http')
const page = require('./.next/serverless/pages/about.js')
const server = new http.Server((req, res) => page.render(req, res))
server.listen(3000, () => console.log('Listening on http://localhost:3000'))

有关特定平台示例,请参阅the examples section above.

摘要

  • 用于实现无服务器部署的Low-level API
  • pages目录中的每个页面都成为无服务器功能(lambda)
  • 创建最小的无服务器功能 (50Kb base zip size)
  • 针对功能的快速cold start 进行了优化
  • 无服务器函数有0个依赖项 (依赖项包含在函数包中)
  • 使用Node.js中的http.IncomingMessagehttp.ServerResponse
  • 选择使用target: 'serverless' in next.config.js
  • 在执行函数时不要加载next.config.js,请注意这意味着publicRuntimeConfig / serverRuntimeConfig不支持。

浏览器支持

Next.js 支持 IE11 和所有的现代浏览器使用了@babel/preset-env。为了支持 IE11,Next.js 需要全局添加Promise的 polyfill。有时你的代码或引入的其他 NPM 包的部分功能现代浏览器不支持,则需要用 polyfills 去实现。

ployflls 实现案例为polyfills

导出静态页面

Examples

next export可以输出一个 Next.js 应用作为静态资源应用而不依靠 Node.js 服务。
这个输出的应用几乎支持 Next.js 的所有功能,包括动态路由,预获取,预加载以及动态导入。

next export将把所有有可能渲染出的 HTML 都生成。这是基于映射对象的pathname关键字关联到页面对象。这个映射叫做exportPathMap

页面对象有 2 个属性:

  • page - 字符串类型,页面生成目录
  • query - 对象类型,当预渲染时,query对象将会传入页面的生命周期getInitialProps中。默认为{}

使用

通常开发 Next.js 应用你将会运行:

1
2
next build
next export

next export命令默认不需要任何配置,将会自动生成默认exportPathMap生成pages目录下的路由你页面。

如果你想动态配置路由,可以在next.config.js中添加异步函数exportPathMap

1
2
3
4
5
6
7
8
9
10
11
12
13
// next.config.js
module.exports = {
exportPathMap: async function(defaultPathMap) {
return {
"/": { page: "/" },
"/about": { page: "/about" },
"/readme.md": { page: "/readme" },
"/p/hello-nextjs": { page: "/post", query: { title: "hello-nextjs" } },
"/p/learn-nextjs": { page: "/post", query: { title: "learn-nextjs" } },
"/p/deploy-nextjs": { page: "/post", query: { title: "deploy-nextjs" } }
};
}
};

注意:如果 path 的结尾是目录名,则将导出/dir-name/index.html,但是如果结尾有扩展名,将会导出对应的文件,如上/readme.md。如果你使用.html以外的扩展名解析文件时,你需要设置 header 的Content-Type头为”text/html”.

输入下面命令:

1
2
next build
next export

你可以在package.json添加一个 NPM 脚本,如下所示:

1
2
3
4
5
6
{
"scripts": {
"build": "next build",
"export": "npm run build && next export"
}
}

接着只用执行一次下面命令:

1
npm run export

然后你将会有一个静态页面应用在out 目录下。

你也可以自定义输出目录。可以运行next export -h命令查看帮助。

现在你可以部署out目录到任意静态资源服务器上。注意如果部署 GitHub Pages 需要加个额外的步骤,文档如下

例如,访问out目录并用下面命令部署应用ZEIT Now.

1
now

复制自定义文件

如果您必须复制robots.txt等自定义文件或生成sitemap.xml,您可以在其中执行此操作exportPathMapexportPathMap获取一些上下文参数来帮助您创建/复制文件:

  • dev - true表示在开发环境下使用exportPathMap. false表示运行于next export. 在开发中,“exportpathmap”用于定义路由,不需要复制文件等行为。
  • dir - 项目目录的绝对路径
  • outDir - 指向out目录的绝对路径(可配置为-o--outdir)。当devtrue时,outdir的值将为null
  • distDir - .next目录的绝对路径(可使用distDir配置键配置)
  • buildId - 导出正在运行的buildId
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// next.config.js
const fs = require('fs')
const { join } = require('path')
const { promisify } = require('util')
const copyFile = promisify(fs.copyFile)

module.exports = {
exportPathMap: async function(
defaultPathMap,
{ dev, dir, outDir, distDir, buildId }
) {
if (dev) {
return defaultPathMap
}
// This will copy robots.txt from your project root into the out directory
await copyFile(join(dir, 'robots.txt'), join(outDir, 'robots.txt'))
return defaultPathMap
}
}

限制

使用next export,我们创建了个静态 HTML 应用。构建时将会运行页面里生命周期getInitialProps 函数。

reqres只在服务端可用,不能通过getInitialProps

所以你不能预构建 HTML 文件时动态渲染 HTML 页面。如果你想动态渲染可以运行next start或其他自定义服务端 API。

多 zone

Examples

一个 zone 时一个单独的 Next.js 应用。如果你有很多 zone,你可以合并成一个应用。

例如,你如下有两个 zone:

有多 zone 应用技术支持,你可以将几个应用合并到一个,而且可以自定义 URL 路径,使你能同时单独开发各个应用。

与 microservices 观念类似, 只是应用于前端应用.

怎么定义一个 zone

zone 没有单独的 API 文档。你需要做下面事即可:

怎么合并他们

你能使用 HTTP 代理合并 zone

你能使用代理micro proxy来作为你的本地代理服务。它允许你定义路由规则如下:

1
2
3
4
5
6
7
8
9
10
{
"rules": [
{
"pathname": "/docs**",
"method": ["GET", "POST", "OPTIONS"],
"dest": "https://docs.my-app.com"
},
{ "pathname": "/**", "dest": "https://ui.my-app.com" }
]
}

生产环境部署,如果你使用了ZEIT now,可以它的使用path alias 功能。否则,你可以设置你已使用的代理服务编写上面规则来路由 HTML 页面

技巧

问答

这个产品可以用于生产环境吗? https://zeit.co 都是一直用 Next.js 写的。

它的开发体验和终端用户体验都很好,所以我们决定开源出来给大家共享。

体积多大?

客户端大小根据应用需求不一样大小也不一样。

一个最简单 Next 应该用 gzip 压缩后大约 65kb

这个像 `create-react-app`?

是或不是.

是,因为它让你的 SSR 开发更简单。

不是,因为它规定了一定的目录结构,使我们能做以下更高级的事:

  • 服务端渲染
  • 自动代码分割

此外,Next.js 还提供两个内置特性:

  • 路由与懒加载组件: <Link> (通过引入 next/link)
  • 修改<head>的组件: <Head> (通过引入 next/head)

如果你想写共用组件,可以嵌入 Next.js 应用和 React 应用中,推荐使用create-react-app。你可以更改import保持代码清晰。

怎么解决 CSS 嵌入 JS 问题?

Next.js 自带styled-jsx库支持 CSS 嵌入 JS。而且你可以选择其他嵌入方法到你的项目中,可参考文档as mentioned before

哪些语法会被转换?怎么转换它们?

我们遵循 V8 引擎的,如今 V8 引擎广泛支持 ES6 语法以及asyncawait语法,所以我们支持转换它们。但是 V8 引擎不支持修饰器语法,所以我们也不支持转换这语法。

可以参照这些 以及 这些

为什么使用新路由?

Next.js 的特别之处如下所示:

  • 路由不需要被提前知道
  • 路由总是被懒加载
  • 顶层组件可以定义生命周期getInitialProps来阻止路由加载(当服务端渲染或路由懒加载时)

因此,我们可以介绍一个非常简单的路由方法,它由下面两部分组成:

  • 每个顶层组件都将会收到一个url对象,来检查 url 或修改历史记录
  • <Link />组件用于包装如(<a/>)标签的元素容器,来执行客户端转换。

我们使用了些有趣的场景来测试路由的灵活性,例如,可查看nextgram

我怎么定义自定义路由?

我们通过请求处理来添加任意 URL 与任意组件之前的映射关系。

在客户端,我们<Link>组件有个属性as,可以装饰改变获取到的 URL。

怎么获取数据?

这由你决定。getInitialProps是一个异步函数async(也就是函数将会返回个Promise)。你可以在任意位置获取数据。

我可以使用 GraphQL 吗?

是的! 这里有个例子Apollo.

我可以使用 Redux 吗?

是的! 这里有个例子

我可以在 Next 应用中使用我喜欢的 Javascript 库或工具包吗?

从我们第一次发版就已经提供很多例子,你可以查看这些例子

什么启发我们做这个?

我们实现的大部分目标都是通过 Guillermo Rauch 的Web 应用的 7 原则来启发出的。

PHP 的易用性也是个很好的灵感来源,我们觉得 Next.js 可以替代很多需要用 PHP 输出 HTML 的场景。

与 PHP 不同的是,我们得利于 ES6 模块系统,每个文件会输出一个组件或方法,以便可以轻松的导入用于懒加载和测试

我们研究 React 的服务器渲染时并没有花费很大的步骤,因为我们发现一个类似于 Next.js 的产品,React 作者 Jordan Walke 写的react-page (现在已经废弃)

贡献

可查看 contributing.md

作者

评论