上文我们一起看完了在 next.js
中如何解决 hydration fail
的错误和如何局部关闭 SSR
的几个方案,其中聊到了 next.js
的 dynamic API
。老规矩,今天我们一起来看看 dynamic API
的源码实现。
API
因为昨天的文章中主要讲到如何使用 dynamic
关闭组件 SSR
,并未讲到其它细节,所以先看下 dynamic
的具体 API
设计。dynamic
的设计很容易让人想到 React.lazy
,事实上也确实差不多,不过 dynamic
比 React.load
多了一些功能。dynamic
除了 ssr
外,还支持 suspense
和 loading
参数。
当 suspense
为 true
时类似 React.lazy
的常见写法,我们需要使用 Suspense
来包裹异步组件:
const DynamicHeader = dynamic(() => import('../components/header'), {
suspense: true
});
export default function Home() {
return (
<Suspense fallback={`Loading...`}>
<DynamicHeader />
</Suspense>
);
}
而当使用提供的 loading
参数时,我们则可以直接将 fallback
作为 loading
参数传入:
const DynamicHeader = dynamic(() => import('../components/header'), {
loading: () => <div>Loading...</div>
});
这种情况下 next.js
会在组件加载过程中显示 loading
的内容来占位,这里其实在内部使用的是 react-loadable
。
源码
我们再来看下源代码,dynamic
所在的文件位置为 packages/next/shared/lib/dynamic.tsx
,我们下面分块解析一下,先看下接口部分:
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
放在属性中就行了:
const DynamicHeader = dynamic({
loading: () => <div>Loading...</div>,
loader: () => import('../components/header')
});
loading
、suspense
和 ssr
参数我们上面都提到了,但是这里还有个 loadableGenerated
参数,别急我们一会就会看到。
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
将会判断接收的参数类型将 dynamicOptions
和 options
参数合并到 loadableOptions
:
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
开启时不能关闭 ssr
,suspense
时不能使用 loading
,接着会处理我们上面看到的 loadableGenerated
参数:
if (loadableOptions.loadableGenerated) {
loadableOptions = {
...loadableOptions,
...loadableOptions.loadableGenerated
};
delete loadableOptions.loadableGenerated;
}
loadableGenerated
会被合并到 loadableOptions
中。然后就到了最后一段逻辑:
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
:
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
的内容,所以确实没问题的。