上文我们一起看完了在 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 的内容,所以确实没问题的。😂
存疑
在 noSSR 中使用到两个参数 webpack 和 modules,看注释此处表示如果使用了 webpack 和 modules 参数,react-loadable 将会进行预加载,不过我目前没找到这两个参数是什么时候注入的,dynamic 中打断点确实存在,猜测为打包时注入的,先记录下。
总结
综上可以看出 next.js 的 dynamic 其实是将 React.lazy 和 react-loadable 两个方法进行了组合,本身代码量也并不算多,一定程度上对异步组件的使用进行了收口,有利于项目中的代码规范和代码的一致性。