hiccLoghicc log by wccHipo日志

Next.js 简明教程

最近用Next.js + Editor.js 撸了一个博客系统。开发起来甚是畅滑,如果你喜欢React,又有同构的需求,不妨由此文入手。

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

为什么需要前端同构?#

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

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

为什么需要现代的前端同构框架?#

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

原因是,一个正常的同构需求,我们需要:

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

React SSR的API只有四个函数: 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可以说是前端同构中的开山,翘楚级框架,依赖React渲染组件。当然Vue有Nuxt.js,Angular有 Angular Universal,甚至Svelte也有Sapper 。

Why Next.js

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

Next.js的官方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 
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方式。
页面间的导航#
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#
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()语法,可以拆分代码,或者有些第三方组件依赖浏览器API时候精致服务端渲染(ssr: false)

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

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获取:

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一个asyncgetServerSideProps方法,next就会在每次请求时候在服务端调用这个方法。

  • 方法只会在服务端运行,每次请求都运行一边getServerSideProps方法
  • 如果页面通过浏览器端Link组件导航而来,Next会向服务端发一个请求,然后在服务端运行getServerSideProps方法,然后返回JSON到浏览器。
👉getServerSideProps方法主要是升级了9.3之前的getInitialProps方法

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

示例:

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都是在build阶段将页面构建成静态的html文件,这样线上直接访问HTML文件,性能极高。

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

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

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

  • 使用getStaticProps方法在build阶段返回页面所需的数据。
  • 如果是动态路由的页面,使用getStaticPaths方法来返回所有的路由参数,以及是否需要回落机制。
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很有用:如果fallbackfalse,访问该方法没有返回的路由会404
  • 但是如果不想或者不方便在build阶段拿到路由参数,可以设置fallbacktrue,Next在访问build中没有的动态路由时候,先浏览器loading,然后服务端开始build该页面的信息,然后再返回浏览器渲染,再次访问该路由该缓存就会生效,很强大!!
  • 静态缓存目前没办法很灵活的更新!!,例如博客内容在build或者fallback生效之后发生更改,目前没办法很方便的替换缓存。
  • Next 在9.5.0之后getStaticProps方法可以增加revalidate的属性以此来重新生成缓存,这点就很强大:页面加载仍然很快,页面永不离线,即使重新生成失败,老的还可以访问,而且可以大幅减少数据库,server的负载。
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方法来给每个页面添加数据。

function MyApp({ Component, pageProps }) { return <Component {...pageProps} /> } export default MyApp

自定义Document#

./pages/_document.tsx来自定义页面的Document,可以配置页面html,head属性,或者使用静态getInitialProps方法中renderPage方法来包括整个react 应用。

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自然也可以自定义构建,根目录使用next.config.js来配置webpack,可以用来支持less编译,按需加载,path alias等。

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

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等。

/ 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 的某些神奇功能可能会受影响。