next.js 源码解析 - dynamic 篇

2023-05-09 11:30:16 浏览数 (2)

上文我们一起看完了在 next.js 中如何解决 hydration fail 的错误和如何局部关闭 SSR 的几个方案,其中聊到了 next.jsdynamic API。老规矩,今天我们一起来看看 dynamic API 的源码实现。

API

因为昨天的文章中主要讲到如何使用 dynamic 关闭组件 SSR,并未讲到其它细节,所以先看下 dynamic 的具体 API 设计。dynamic 的设计很容易让人想到 React.lazy,事实上也确实差不多,不过 dynamicReact.load 多了一些功能。dynamic 除了 ssr 外,还支持 suspenseloading 参数。

suspensetrue 时类似 React.lazy 的常见写法,我们需要使用 Suspense 来包裹异步组件:

代码语言:javascript复制
const DynamicHeader = dynamic(() => import('../components/header'), {
    suspense: true
});

export default function Home() {
    return (
        <Suspense fallback={`Loading...`}>
            <DynamicHeader />
        </Suspense>
    );
}

而当使用提供的 loading 参数时,我们则可以直接将 fallback 作为 loading 参数传入:

代码语言:javascript复制
const DynamicHeader = dynamic(() => import('../components/header'), {
    loading: () => <div>Loading...</div>
});

这种情况下 next.js 会在组件加载过程中显示 loading 的内容来占位,这里其实在内部使用的是 react-loadable

源码

我们再来看下源代码,dynamic 所在的文件位置为 packages/next/shared/lib/dynamic.tsx,我们下面分块解析一下,先看下接口部分:

代码语言:javascript复制
function dynamic<P = {}>(
    dynamicOptions: DynamicOptions<P> | Loader<P>,
    options?: DynamicOptions<P>
): React.ComponentType<P>;
export type DynamicOptions<P = {}> = LoadableGeneratedOptions & {
    loading?: (loadingProps: DynamicOptionsLoadingProps) => JSX.Element | null;
    loader?: Loader<P> | LoaderMap;
    loadableGenerated?: LoadableGeneratedOptions;
    ssr?: boolean;
    suspense?: boolean;
};

看接口就可以猜到其实 dynamic 可以只接受一个参数,将 loader 放在属性中就行了:

代码语言:javascript复制
const DynamicHeader = dynamic({
    loading: () => <div>Loading...</div>,
    loader: () => import('../components/header')
});

loadingsuspensessr 参数我们上面都提到了,但是这里还有个 loadableGenerated 参数,别急我们一会就会看到。

代码语言:javascript复制
import Loadable from './loadable';

let loadableFn: LoadableFn<P> = Loadable;

let loadableOptions: LoadableOptions<P> = options?.suspense
    ? {}
    : {
          loading: ({ error, isLoading, pastDelay }) => {
              if (!pastDelay) return null;
              if (process.env.NODE_ENV === 'development') {
                  if (isLoading) {
                      return null;
                  }
                  if (error) {
                      return (
                          <p>
                              {error.message}
                              <br />
                              {error.stack}
                          </p>
                      );
                  }
              }

              return null;
          }
      };

可以看到这里用到了 Loadable,其实就是 react-loadable 这个库,只是 next.js 将源码放在了自己的仓库中,然后根据是否为 suspense 初始化 loadableOptions。这里可以看到默认的 loading 参数,在开发环境下如果异步组件加载有报错将会进行展示。

然后 next.js 将会判断接收的参数类型将 dynamicOptionsoptions 参数合并到 loadableOptions

代码语言:javascript复制
if (dynamicOptions instanceof Promise) {
    loadableOptions.loader = () => dynamicOptions;
} else if (typeof dynamicOptions === 'function') {
    loadableOptions.loader = dynamicOptions;
} else if (typeof dynamicOptions === 'object') {
    loadableOptions = { ...loadableOptions, ...dynamicOptions };
}
loadableOptions = { ...loadableOptions, ...options };

紧接着会对环境和参数进行参数检查,如 suspense 开启时不能关闭 ssrsuspense 时不能使用 loading,接着会处理我们上面看到的 loadableGenerated 参数:

代码语言:javascript复制
if (loadableOptions.loadableGenerated) {
    loadableOptions = {
        ...loadableOptions,
        ...loadableOptions.loadableGenerated
    };
    delete loadableOptions.loadableGenerated;
}

loadableGenerated 会被合并到 loadableOptions 中。然后就到了最后一段逻辑:

代码语言:javascript复制
if (typeof loadableOptions.ssr === 'boolean' && !loadableOptions.suspense) {
    if (!loadableOptions.ssr) {
        delete loadableOptions.ssr;
        return noSSR(loadableFn, loadableOptions);
    }
    delete loadableOptions.ssr;
}

return loadableFn(loadableOptions);

可以看到当 ssr 参数被设置为 false 时并且非 suspense 时,将会直接返回 noSSR,否则将会直接调用 react-loadable,将上面拼接出的 loadableOptions 进行传入,我们再看下 noSSR

代码语言:javascript复制
const isServerSide = typeof window === 'undefined';
export function noSSR<P = {}>(
    LoadableInitializer: LoadableFn<P>,
    loadableOptions: DynamicOptions<P>
): React.ComponentType<P> {
    // Removing webpack and modules means react-loadable won't try preloading
    delete loadableOptions.webpack;
    delete loadableOptions.modules;

    if (!isServerSide) {
        return LoadableInitializer(loadableOptions);
    }

    const Loading = loadableOptions.loading!;
    return () => <Loading error={null} isLoading pastDelay={false} timedOut={false} />;
}

可以看到这里一样会使用 window 来判断代码环境,如果为客户端渲染,将会直接调用 react-loadable,而服务端将会使用 loading 参数进行渲染。

到这里源码解读就结束了,可能又同学会疑惑,在 ssr 关闭的情况下,客户端依旧会使用 react-loadable 进行渲染,而服务端则会直接渲染 Loading,那为啥不会出现 hydration fail 的错误?我一开始也愣了一下,想了想 react-loadable 在客户端初始化渲染的也是 loading 的内容,所以确实没问题的。

0 人点赞