All Articles

Next.js Notes

0. 前言

最近在尝试使用Next.js,虽说比直接裸用react简单不少,但还是有不少自有的特殊概念,因此这里开篇笔记,做下记录。

入门Next.js可以阅读官方的教程:Create a Next.js App。关于Next.js的优势,为什么要使用Next.js以及很多关于Next.js的细节等,可以查看这篇博文:The Next.js Handbook

version9.4.4已知问题(会列一些对使用有影响的):

1. 文件结构

默认被占用的路径只有两个:

  • project_root/pages:用来作为页面路由使用
  • project_root/public:用来进行静态文件输出

2. 路由

路由部分Next.js做了简化,默认是使用文件夹的形式,在project_root/pages/name.js|tsx下的文件,会映射为your_site/name这个路径,见官方文档:Pages。这样使用上非常简便,正常使用的话,就完全不需要引入第三方的类库(类似于react-router),也不需要使用编程的方式在代码中定义路由。更多细节:Routing

但上述的路由不能很好支持your_site/user/:user_id这样的需求。类似这样的需求在Next.js中也有解决方案,被称为dynamic route。官方文档在:Dynamic Routes。只需要把page文件的名字定义为:pages/post/[pid].js就OK。多参数的路径,类似:pages/post/[pid]/[comment].js,也是一样处理,这种路径在query参数中获取到的object就会含有多个键。

如果需要编程的方式来进行route操作的话,见文档:next/router。如果官方以文件夹形式管理的自动化route无法满足需求的话,还有功能强大的插件形式的route可以使用:Dynamic Routes for Next.js

4. SSR CSR Static

Next.js默认支持了服务端渲染等提升前端获取速度和渲染性能的功能,但这也要求开发者必须谨慎对待组件的初始化和生命周期,因为显然运行在服务端和客户端的代码在获取组件初始化需要的数据时的方法是不一样的。

这部分的文档在:Data fetching,以及Automatic Static Optimization

如果一个Page里getServerSidePropsgetInitialProps存在的话,该Page就会被识别为服务端渲染(SSR),会在每次请求的时候进行渲染。而如果这两者皆不存在的话,该页面就会在服务器启动并构建的时候生成静态页面,后续每次请求的时候都会直接返回该静态页面。此外,在DEV模式的情况下,即便是静态渲染的Page也会在每次请求的时候触发渲染,需要注意。

主要方法有以下几个:

getStaticPaths

这个方法一般是配合dynamic route进行使用,会在Next.js服务器启动,并进行服务器静态构建的时候运行。其作用是告诉后续的getStaticProps某个动态路径的可能项。如果这个page你不需要静态构建,或者这个page也不是dynamic route,那就不需要实现这个函数。

举例来说pages/user/[id].js代码中的getStaticProps就需要返回:

export async function getStaticPaths() {
  return {
    paths: [
      { params: { id: '1' } },
      { params: { id: '2' } }
    ],
    fallback: true or false
  };
}

paths和fallback都是必须的键。fallback的意思是,当遇到客户端访问到一个并不存在于启动构建时制作的paths列表里的路径,Next.js应该允许客户端继续访问,还是直接返回一个404。这里要注意,如果fallback为true,也就是允许客户端继续访问的话,page代码一定要做好容错性,否则很容易导致页面报错。

getStaticProps

这个方法也是在Next.js服务器启动的时候,进行服务器静态构建的时候运行。其作用是用来给静态生成的页面提供props。同样的,如果这个page你不需要静态构建,那就不需要实现这个函数。

export async function getStaticProps(context) {
  return {
    props: {}, // will be passed to the page component as props
  }
}

如果是dynamic route的情况,该函数的context会包含getStaticPaths提供的路径信息:context.params = { id: '1' }

getServerSideProps

同样是运行在服务器端,但并不是在Next.js构建时候运行,而是在每一次单独的请求到达的时候触发。

export async function getServerSideProps(context) {
  return {
    props: {}, // will be passed to the page component as props
  }
}

getInitialProps

基本上已弃用,如果是静态生成页面的话,使用getStaticProps;如果是服务端渲染的话,使用getServerSideProps。如果仅只是客户端渲染的页面,则使用React的effect就可以了。

5. Next.js的其他知识点

5.1 CSS相关

官方文档:Built-In CSS Support

Next.js的CSS有很多细节,最主要的是命名后缀的问题,使用*.module.css的话,生成出来的页面上的CSS是会附带随机后缀的,就不会造成冲突。而如果需要引入global的css的话,则需要创建pages/_app.js文件,在这里修改。

jsx支持相关可以查阅这篇文档:styled-jsx

5.2 变量配置

环境变量的官方文档:Environment Variables

Next.js默认直接支持环境变量配置。基本上有配置相关需求就直接使用这个解决方案就OK了。

5.3 API后端

之前提到的route都是pages下的页面,一般来说还需要前后端交互使用的/api/*,这个在Next.js中被称为:API Routes

5.4 自定义App

创建pages/_app.js代码文件,就可以在里面进行一些应用级别的初始化工作。官方文档:Custom App。自定义App的可能需求为:

  • Persisting layout between page changes
  • Keeping state when navigating pages
  • Custom error handling using componentDidCatch
  • Inject additional data into pages
  • Add global CSS

可能的应用场景:准备全局变量。在一款WEB应用中,某些数据是所有的页面都会有需求的,比如说当前session的最基本用户信息和权限信息等。而_app是所有的页面的基本入口,每个页面的渲染都会先经过它,那么其实就可以在这个代码中做这些事情。

页面渲染的先后顺序:

// Resolution order
//
// On the server:
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. document.getInitialProps
// 4. app.render
// 5. page.render
// 6. document.render
//
// On the server with error:
// 1. document.getInitialProps
// 2. app.render
// 3. page.render
// 4. document.render
//
// On the client
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. app.render
// 4. page.render
// _app.tsx
import App, {AppContext} from "next/app";

export type AppPageProps = AppProps & {
  your_global_page_props: any;
};

MyApp.getInitialProps = async (appCtx: AppContext) => {
    const appProps = await App.getInitialProps(appCtx);
    appProps.pageProps = {
        your_global_page_props: "",
    } as AppPageProps;

    return {...appProps};
};

// yourpage.tsx
import {AppPageProps} from "../_app";

export type YourPageProps = AppPageProps & {};

export default function YourPage(props: YourPageProps) {
    console.log(props); // { your_global_page_props: "" }
}

5.5 自定义Document

创建pages/_document.js代码文件,就可以在里面改动全局的<html><body>。官方文档:Custom Document

Document is only rendered in the server.

6. React相关概念

Next.js和React的关系还是比较简单的。React本身的功能其实很简单,它就只是一个渲染引擎,而用来做应用的时候,你除了渲染之外还需要很多东西,而这些,React是给不了你的,必须你自己去组织。比如说最基本的请求路由,比如说打包用的webpack,比如说js transformation的babel,以及提升整体性能的服务端渲染和静态生成等等等等。

Next.js实际上就是在React的基础上,提供这些做应用必须的组件和功能之后的框架库。所以在使用Next.js进行开发的时候,React的知识也是必须的,否则就无法正确处理渲染相关的问题了。作为基本的使用者,React其实只有3大组的概念是必须要厘清楚的。

6.1 state

React的官方文档在:Using the State Hook。主要代码其实就只有一句:

const [count, setCount] = useState(0);

useState函数接受一个状态的默认值,然后返回一个数组,数组的0位是state变量,1位是改变这个state变量的方法,名字可以自己命名。

State的值调整需要注意内存的问题,有的时候不是仅仅只是修改对象的值就可以了。特别是遇到复杂的state,一个object或者数组里有嵌套的object和数组的时候,需要非常注意内存拷贝的问题。具体的可以看这个帖子:React: how to update state.item[1] in state using setState?

6.2 effect

React的官方文档在:Using the Effect Hook - React。只要是涉及到状态变化的,都属于effect的范畴。一个React组件里可以使用useEffect函数注册多个effect事件。第二个参数的指定可以决定该effect事件应该在什么时候触发。

[],效果同componentDidMount,仅只在组件mount的时候触发一次。同时,在这种effect中提供一个回调函数返回,则等同于componentWillUnmount,这个回调函数会在组件被unmount的时候被触发,用来做析构。

useEffect(() => {
  // your effect here
  return () => {}; // unmount
}, []);

无参数,效果同componentDidUpdate,每次组件re-render都会触发。

useEffect(() => {
  // your effect here
}); // no optional argument

[foo],在数组中放入prop或state,则该effect只会在这个prop或state发生变化的时候触发。这个数组里可以放入复数的变量,表示监听多个变量的变化,任何一个发生变化都会触发该effect。

useEffect(() => {
  // your effect here
}, [foo]);

7. 自定义服务器

在大部分情况下,使用next [start]命令启动的服务器就已经足够满足需求了,但某些时候,我们仍旧有需求需要自定义一些服务端的功能,这时候就需要改造Next.js自带的服务端功能了。

官方文档在:Custom Server。koa的例子可以在官方范例代码库中进行查看:Custom Koa Server example。此外,还有一篇博客讲得不错:NextJs + KoaJs Create custom NextJs server with KoaJs。以及,另一个例子可以参考:fridays/next-routes#on-the-server

不过官方的文档给出的信息其实也相当有限,没有任何对于next命令的细致解释,如果想要知道官方启动命令实际上做了什么,就只能去阅读源码了。简单看了下,除了启动服务器之外,next命令还是做了不少事情的,如果想要在保证这些功能的情况下兼容koa的话,是相当困难的。

这里更建议的做法是启一个纯粹的koa后端作为API的服务器,而将基本的前端页面和pages交给Next.js来进行服务。这样就可以绕开必须改动或者无法使用next命令的情况。

8. 问题及解决

8.1 TypeORM

初始Versions

next: 9.4.4
typeorm: 0.0.25

在添加一系列typeorm相关代码之后,next.js的编译会遇到报错(无法正常build):

Syntax error: Support for the experimental syntax 'decorators-legacy' isn't currently enabled

解决方法见:Syntax error - Support for the experimental syntax ‘decorators-legacy’ isn’t currently enabled,需要在项目根目录添加文件.babelrc,并添加内容(关于babel的客制化,官方文档见:Customizing Babel Config):

{
  "plugins": [
    [
      "@babel/plugin-proposal-decorators",
      {
        "legacy": true
      }
    ]
  ],
  "presets": [
    "next/babel"
  ]
}

当然还需要安装扩展:yarn add @babel/plugin-proposal-decorators -D

接下来会遇到第二个报错:

error - ./node_modules/typeorm/browser/driver/DriverFactory.js
Attempted import error: 'AuroraDataApiPostgresDriver' is not exported from './postgres/PostgresDriver'.

解决方法见:Typeorm/browser cannot be compiled since version 0.2.25 #6110,解决方案简单粗暴,将typeorm的版本降到0.2.24即可。

然后会遇到第三个报错:

warn  - ./node_modules/typeorm/browser/driver/react-native/ReactNativeDriver.js
Module not found: Can't resolve 'react-native-sqlite-storage' in './node_modules/typeorm/browser/driver/react-native'

解决方法见:browser: Can’t resolve ‘react-native-sqlite-storage’ #2158。创建一个假的stub package即可。

然后就可以在_app.tsx:MyApp.getInitialProps进行数据库连接的初始化。

try {
  getConnection();
} catch (err) {
  if (err instanceof ConnectionNotFoundError) {
    await createConnection(Database.getConnectionOptions());
  } else {
    throw err;
  }
}

到实际使用的时候最后仍旧会有问题(参见:Use TypeORM with Next.js #12254):

RepositoryNotFoundError: No repository for "..." was found. Looks like this entity is not registered in current "default" connection?
...

简单来说就是和数据库的连接正常建立了,但本地的entities文件找不到。我尝试过很多方法来解决这个问题,但最终都不能很好解决。猜想可能是next会创建build文件,而build文件中找不到entities(虽然我也尝试过给予entities绝对路径,但最后也不行)。最后还是放弃了在SSR中使用typeorm,所有的next.js中的数据访问全部都通过API来访问后端的koa服务器来解决,不再进行服务器代码直接处理。

这样当然会有不小的性能损耗,其实绕开typeorm直接在服务器自己建立连接来访问数据库也是可行的,不过小项目就算了,代码量多了维护起来比较麻烦。

资料

EOF

Published 2020/6/9

Some tech & personal blog posts