# Next.js 简明教程



最近用[Next.js](https://nextjs.org) + [Editor.js](https://editorjs.io) 撸了一个[博客系统](https://hicc.pro)。开发起来甚是畅滑，如果你喜欢React，又有同构的需求，不妨由此文入手。

---

如果你的应用需要兼容IE 9，10等浏览器，请出门左拐找找ejs等“传统”方式～～～

## 为什么需要前端同构？

- 搜索引擎SEO以及首屏体验的，需要服务端渲染页面
- 日益丰富的前端交互，需要更强大前端框架来满足。

前端同构，就是一站式解决上述问题的方案：**让一套的JavaScript代码，同时跑在服务端和客户端。**

## 为什么需要现代的前端同构框架？
现代点的前端框架都有服务端渲染API，为什么我们还需要一个同构框架？

原因是，一个正常的同构需求，我们需要：


1. 前端组件渲染为HTML字符串，流
2. 服务端，客户端资源的加载不同处理，(首屏不一定全部加载完所有js……)
3. 服务端，客户端的状态数据的传递
4. 打包工具链
5. 性能优化
6. ……

而[React SSR的API](https://reactjs.org/docs/react-dom-server.html)只有四个函数： `renderToString()`， `renderToStaticMarkup()`，`renderToNodeStream()`，`renderToStaticNodeStream()`（Vue也类似），只能满足第一个需求，我们需要更多，而以Next.js为代表前端同构框架除了能满足上述基本的要求外，还能为我们带来：


1. **极佳的开发体验，做到和开发SPA一样**，(是的这个第一重要，不然不如选用传统模版渲染方案)
2. 初次server渲染及其高效，所需JS也越小越好。
3. 再之后的客户端渲染能够尽可能利用服务端带下来的数据。
4. 便利的SSG(Static Site Generation)支持。
5. 支持TypeScript
6. ……

换句话说，**让开发越发动态灵活，让渲染越发静态高效**。

举个例子：


1. Wordpress等cms系统，动态需求容易满足，但是静态缓存的优化就较难实现。
2. Hexo等方案，页面渲染完全静态化(落地为文件)，但是但凡有点动态化的需求，基本无法实现。

其中[Next.js](https://nextjs.org/)可以说是前端同构中的开山，翘楚级框架，依赖[React](https://reactjs.org/)渲染组件。当然Vue有[Nuxt.js](https://nuxtjs.org/)，Angular有 [Angular Universal](https://github.com/angular/universal)，甚至[Svelte](https://svelte.dev/)也有[Sapper](https://sapper.svelte.dev/) 。


![Why Next.js](https://cdn.hashnode.com/res/hashnode/image/upload/v1618074137131/drceXe2wZ.png)

正式开始之前，强烈推荐Next.js的官方文档，挺清晰易懂。

[Getting Started | Next.js](https://nextjs.org/docs/getting-started)

Next.js的官方Blog，也十分推荐，各个版本的更新详尽及时，堪称模范。

[Blog | Next.js](https://nextjs.org/blog)

---

## Next.js简明教程
本文基于Next.js 9.3，这里不涉及原理，只是做个入门指导。

### 基于文件路径的路由
#### 页面
一般前端web应用都可以简化为，基于路由的页面和API两部分。Next的路由系统基于文件路径自动映射，不需要做中性化的配置。

一般都约定在根目录`pages`文件夹内：


- `./pages/index.tsx` --> 首页 `/`
- `./pages/admin/index.tsx` --> `/admin`
- `./pages/admin/post.tsx` \--> `/admin/post`


```javascript
import styles from './style.module.css'
function About() {
  return <div>About</div>
}

export default About
```
默认导出一个React的组件，Next就会帮你默认生成对应路由的页面。


- 你不用关心head里面资源如何配置加载
- 可以像SPA应用一样，使用css-in-js，css module，less，sass等样式`import`方式。

##### 页面间的导航

```javascript
import Link from 'next/link'
function Home() {
  return (
    <ul>
      <li>
        <Link href="/about">
          <a>About Us</a>
        </Link>
      </li>
    </ul>
  )
}

export default Home
```
注意，Link中最好独立包裹a元素。

##### 增加Head

```jsx
import Head from 'next/head'
function About() {
  return (
    <div>
      <Head>
        <title> Hipo Log - {props.post?.name ?? ''}</title>
      </Head>
      content
    </div>
  );
}
export default About
  
```
##### Dynamic import 代码拆分
Next也支持ES2020的[dynamic import()语法](https://github.com/tc39/proposal-dynamic-import)，可以拆分代码，或者有些第三方组件依赖浏览器API时候精致服务端渲染(`ssr: false`)


```jsx
import dynamic from 'next/dynamic'

const DynamicComponentWithCustomLoading = dynamic(
  () => import('../components/hello'),
  { 
    loading: () => <p>...</p>,
  	ssr: false
  }
)

function Home() {
  return (
    <div>
      <Header />
      <DynamicComponentWithCustomLoading />
      <p>HOME PAGE is here!</p>
    </div>
  )
}

export default Home
```
**👉注意：在页面代码要谨慎import代码！！**

越多引入，上线访问后加载的js就越多，特别是下面钩子函数要注意，不要引入多余代码
### API
API类型的路由约定在`./pages/api` 文件夹内，next会自动映射为`/api/*`路径的API


```typescript
import { NextApiRequest, NextApiResponse } from 'next'

export default (req: NextApiRequest, res: NextApiResponse) => {
  res.status(200).json({ name: 'John Doe' })
}
```
_请求方法通过req中取到。_

如此你就可以很轻松的生成一个API。

#### 动态路由
正常的应用，都有动态路由，next中讨巧使用文件命名的方式来支持。


- `` `./pages/post/create.js` --> `/post/create` ``
- ``` `` `./pages/post/[pid].js` -->`` `` `/post/1` ``, `` `/post/abc` ``等，但是不会匹配 `` `/post/create` `` ```
- ```` ``` `` `./pages/post/[...slug].js` `` --> `` `/post/1/2` ``, `` `/post/a/b/c` ``等，但是不会匹配 `` `/post/create` ``, `` `/post/abc` `` ``` ````

动态参数可以通过`req.query`对象中获取（`{ pid }`， `{ slug: [ 'a', 'b' ] }`），在页面中可以通过router hook获取：


```javascript
import { useRouter } from 'next/router';

function About() {
  const router = useRouter();
  const { bID, pID } = router.query
  return <div>About</div>
}
```
### 页面SSR 钩子以及SSG
大部分的应用内容，都不是纯静态的，我们需要数据查询才能渲染那个页面，而这些就需要同构钩子函数来满足，有了这些钩子函数，我们才可以在不同需求下作出极佳体验的web应用。

#### `getServerSideProps`（SSR）每次访问时请求数据
页面中`export`一个`async`的`getServerSideProps`方法，next就会在每次请求时候在服务端调用这个方法。


- 方法只会在服务端运行，每次请求都运行一边`getServerSideProps`方法
- 如果页面通过浏览器端`Link`组件导航而来，Next会向服务端发一个请求，然后在服务端运行`getServerSideProps`方法，然后返回JSON到浏览器。

**👉`getServerSideProps`方法主要是升级了9.3之前的`getInitialProps`方法**

9.3之前的`getInitialProps`方法有一个很大的缺陷是在浏览器中`req`和`res`对象会是`undefined`。也就是使用它的页面，如果是浏览器渲染你需要在组件内再显示地请求一次。开发体验不太好。 如果没有特殊问题，建议使用`getServerSideProps`替代`getInitialProps`方法。
示例：


```tsx
import { GetServerSideProps, NextPage } from 'next'

interface PostProps {
  list: Post[]
}

const App: NextPage<PostProps> = props => {
  return <div></div>
}

export const getServerSideProps: GetServerSideProps<PostProps> = async context => {
  const list = await context.req.service.post.getPost(context.params.postID)
  return {
    props: {
      list
    }
  }
}
export default App
```
#### `getStaticProps`和`getStaticPaths`（SSG）构建时请求数据
所谓的SSG也就是静态站点生成，类似像hexo或者[gatsbyjs](https://www.gatsbyjs.org/)都是在build阶段将页面构建成静态的html文件，这样线上直接访问HTML文件，性能极高。

Next.js 再9.0的时候引入了自动静态优化的功能，也就是如果页面没有使用`getServerSideProps`和`getInitialProps`方法，Next在build阶段会生成html，以此来提升性能。

但是正如上文说的，一般应用页面都会需要动态的内容，因此自动静态优化局限性很大。

Next 在9.3中更近了一步，引入了`getStaticProps`和`getStaticPaths`方法来让开发者指定哪些页面可以做SSG优化。


- 使用`getStaticProps`方法在build阶段返回页面所需的数据。
- 如果是动态路由的页面，使用`getStaticPaths`方法来返回所有的路由参数，以及是否需要回落机制。


```tsx
export async function getStaticPaths() {
  // Call an external API endpoint to get posts
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  // Get the paths we want to pre-render based on posts
  const paths = posts.map(post => ({
    params: { id: post.id },
  }))

  // We'll pre-render only these paths at build time.
  // { fallback: false } means other routes should 404.
  return { paths, fallback: true };
}
export const getStaticProps: GetStaticProps<InitProps> = async ({ params }) => {
  const data = await fetch(
    `http://.../api/p/${params.bookUUID }/${
      params.postUUID }`
  );
  return {
    props: {
      post: data,
    },
  };
};
```
使用非常的简单，需要注意的是：


- `getStaticPaths`方法返回的`fallback`很有用：如果`fallback`是`false`，访问该方法没有返回的路由会404
- 但是如果不想或者不方便在build阶段拿到路由参数，可以设置`fallback`为`true`，Next在访问build中没有的动态路由时候，先浏览器loading，然后服务端开始build该页面的信息，然后再返回浏览器渲染，再次访问该路由该缓存就会生效，很强大！！
- ~**静态缓存目前没办法很灵活的更新！！**，例如博客内容在build或者fallback生效之后发生更改，目前没办法很方便的替换缓存。~


- Next 在9.5.0之后`getStaticProps`方法可以[增加`revalidate`的属性](https://nextjs.org/docs/basic-features/data-fetching#incremental-static-regeneration)以此来重新生成缓存，这点就很强大：页面加载仍然很快，页面永不离线，即使重新生成失败，老的还可以访问，而且可以大幅减少数据库，server的负载。


```jsx
function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li>{post.title}</li>
      ))}
    </ul>
  )
}

// This function gets called at build time on server-side.
// It may be called again, on a serverless function, if
// revalidation is enabled and a new request comes in
export async function getStaticProps() {
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  return {
    props: {
      posts,
    },
    // Next.js will attempt to re-generate the page:
    // - When a request comes in
    // - At most once every second
    revalidate: 1, // In seconds
  }
}

export default Blog
```
#### 如何选择SSR还是SSG？

1. 如果页面内容真动态(例如，来源数据库，且经常变化)， 使用`getServerSideProps`方法的SSR。
2. 如果是静态页面或者伪动态(例如，来源数据库，但是不变化)，可以酌情使用SSG。

---

上面就是Next.js中主要的部分了，下面是一些可能用到的自定义配置。

### 自定义App
用`./pages/_app.tsx`来自定义应用App，可以配置全局的css，或者`getServerSideProps`方法来给每个页面添加数据。


```javascript
function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

export default MyApp
```
### 自定义Document
用`./pages/_document.tsx`来自定义页面的Document，可以配置页面html，head属性，或者使用[静态getInitialProps方法中renderPage方法](https://nextjs.org/docs/advanced-features/custom-document#customizing-renderpage)来包括整个react 应用。


```javascript
import Document, { Html, Head, Main, NextScript } from 'next/document'

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

  render() {
    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

export default MyDocument
```
`` `<Html>` ``, `` `<Head />` ``, `` `<Main />` `` 和 `` `<NextScript />`都是必须的。``


- 上述app和document中使用`getServerSideProps`或者`getInitialProps`方法让整个应用都无法自动静态优化
- 上述app和document中在浏览器中不执行，包括react的hooks或者生命周期函数。

### 自定义构建
Next自然也可以[自定义构建](https://nextjs.org/docs/api-reference/next.config.js/custom-webpack-config)，根目录使用`next.config.js`来配置webpack，可以用来支持less编译，按需加载，path alias等。

下面是Hipo Log中的配置，支持了Antd design的按需加载。


```javascript
const withLess = require('@zeit/next-less');
const withCss = require('@zeit/next-css');
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const fixMiniCss = (nextConfig = {}) => {
  return Object.assign({}, nextConfig, {
    webpack(_config, options) {
      // force react-scripts to use newer version of `mini-css-extract-plugin` and ignore css ordering warnings
      // (related to issue: https://github.com/facok/create-react-app/issues/5372)
      let config = _config;
      if (typeof nextConfig.webpack === 'function') {
        config = nextConfig.webpack(_config, options);
      }
      let miniCssExtractOptions = {};
      for (let i = 0; i < config.plugins.length; i++) {
        const p = config.plugins[i];
        if (
          !!p.constructor &&
          p.constructor.name === MiniCssExtractPlugin.name
        ) {
          miniCssExtractOptions = { ...p.options, ignoreOrder: true };
          // config.plugins[i] = new MiniCssExtractPlugin(miniCssExtractOptions);
          break;
        }
      }
      // config.plugins.push(new MiniCssExtractPlugin(miniCssExtractOptions));
      config.resolve.alias = {
        ...config.resolve.alias,
        '@pages': path.join(__dirname, './pages'),
        '@components': path.join(__dirname, './components'),
        '@entity': path.join(__dirname, './dbEntity'),
        '@services': path.join(__dirname, './services'),
        '@lib': path.join(__dirname, './lib'),
        '@util': path.join(__dirname, './services/util'),
        '@type': path.join(__dirname, './app.type'),
      };
      return config;
    },
  });
};
module.exports = fixMiniCss(
  withLess({
    cssModules: true,
    ...withCss({
      webpack(config, options) {
        const { dev, isServer } = options;
        if (isServer) {
          const antStyles = /antd\/.*?\/style\/css.*?/;
          const origExternals = [...config.externals];
          config.externals = [
            (context, request, callback) => {
              if (request.match(antStyles)) return callback();
              if (typeof origExternals[0] === 'function') {
                origExternals[0](context, request, callback);
              } else {
                callback();
              }
            },
            ...(typeof origExternals[0] === 'function' ? [] : origExternals),
          ];

          config.module.rules.unshift({
            test: antStyles,
            use: 'null-loader',
          });
        }

        return config;
      },
    }),
  })
);
```
### 自定义服务
Next也支持node启动，以此来和其他框架配合实现更复杂的服务端功能，譬如Hipo Log 使用它来绑定数据库typeorm等。


```javascript
/ server.js
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.js 的某些神奇功能可能会受影响。**

---
