〇、关于Code Splitting
考虑一个场景,如果应用中有个地图组件,有可能用户永远不会切换到这个组件,我们就不必要为整个路由加载一个庞大的地图库,这个就是为什么我们要进行代码拆分的原因。
react-loadable与React.lazy+React.Suspense方案都支持基于组件的拆分方式。
一、Webpack 2+对import()的处理
基于动态导入提案,我们可以修改一个组件,使它异步加载Bar模块
class MyComponent extends React.Component {
state = {
Bar: null
};
componentWillMount() {
import('./components/Bar').then(Bar => {
this.setState({ Bar: Bar.default });
});
}
render() {
let {Bar} = this.state;
if (!Bar) {
return <div>Loading...</div>;
} else {
return <Bar/>;
};
}
}
webpack有三种通用的code splitting方法
- 入口点: 使用配置手动拆分代码
- 防止重复: 使用依赖项或者SplitChunksPlugin
- 动态导入:使用模块内的内联函数拆分代码
对于匹配动态导入模式的文件,webpack会将对应的模块单独分离到一个包中去。 下面代码提供了类似import()的功能
function importModule(url) {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
const tempGlobal = "__tempModuleLoadingVariable" + Math.random().toString(32).substring(2);
script.type = "module";
script.textContent = `import * as m from "${url}"; window.${tempGlobal} = m;`;
script.onload = () => {
resolve(window[tempGlobal]);
delete window[tempGlobal];
script.remove();
};
script.onerror = () => {
reject(new Error("Failed to load module script with URL " + url));
delete window[tempGlobal];
script.remove();
};
document.documentElement.appendChild(script);
});
}
二、react-loadable实现原理
react-loadable实际上只是将部分功能进行了封装,解决了几个场景的问题。
- import失败
- 服务端渲染
- loading过程显示其它组件 import()方法是用户调用的时候传入的,所以在react-loadable源码中甚至不存在import。
核心代码
function load(loader) {
var promise = loader();
var state = {
loading: true,
loaded: null,
error: null
};
state.promise = promise.then(function (loaded) {
state.loading = false;
state.loaded = loaded;
return loaded;
}).catch(function (err) {
state.loading = false;
state.error = err;
throw err;
});
return state;
}
核心代码实际上并不复杂, 先执行loader,初始化state, promise返回的组件传给state.loaded
LoadableComponent.prototype.render = function render() {
if (this.state.loading || this.state.error) {
return React.createElement(opts.loading, {
isLoading: this.state.loading,
pastDelay: this.state.pastDelay,
timedOut: this.state.timedOut,
error: this.state.error,
retry: this.retry
});
} else if (this.state.loaded) {
return opts.render(this.state.loaded, this.props);
} else {
return null;
}
};
然后就是浅显易懂的render方法,loading或者error则render loading组件并传入对应参数,加载完成则render上面的state.loaded.
三、React.lazy实现原理
项目中使用的是react 16.14.0
function lazy(ctor) {
var lazyType = {
$$typeof: REACT_LAZY_TYPE,
_ctor: ctor,
// React uses these fields to store the result.
_status: -1,
_result: null
};
{
// In production, this would just set it on the object.
var defaultProps;
var propTypes;
Object.defineProperties(lazyType, {
defaultProps: {
configurable: true,
get: function () {
return defaultProps;
},
set: function (newDefaultProps) {
error('React.lazy(...): It is not supported to assign `defaultProps` to ' + 'a lazy component import. Either specify them where the component ' + 'is defined, or create a wrapping component around it.');
defaultProps = newDefaultProps; // Match production behavior more closely:
Object.defineProperty(lazyType, 'defaultProps', {
enumerable: true
});
}
},
propTypes: {
configurable: true,
get: function () {
return propTypes;
},
set: function (newPropTypes) {
error('React.lazy(...): It is not supported to assign `propTypes` to ' + 'a lazy component import. Either specify them where the component ' + 'is defined, or create a wrapping component around it.');
propTypes = newPropTypes; // Match production behavior more closely:
Object.defineProperty(lazyType, 'propTypes', {
enumerable: true
});
}
}
});
}
return lazyType;
}
case LazyComponent:
{
var elementType = workInProgress.elementType;
return mountLazyComponent(current, workInProgress, elementType, updateExpirationTime, renderExpirationTime);
}
function mountLazyComponent(_current, workInProgress, elementType, updateExpirationTime, renderExpirationTime) {
if (_current !== null) {
// A lazy component only mounts if it suspended inside a non-
// concurrent tree, in an inconsistent state. We want to treat it like
// a new mount, even though an empty version of it already committed.
// Disconnect the alternate pointers.
_current.alternate = null;
workInProgress.alternate = null; // Since this is conceptually a new fiber, schedule a Placement effect
workInProgress.effectTag |= Placement;
}
var props = workInProgress.pendingProps; // We can't start a User Timing measurement with correct label yet.
// Cancel and resume right after we know the tag.
cancelWorkTimer(workInProgress);
var Component = readLazyComponentType(elementType); // Store the unwrapped component in the type.
workInProgress.type = Component;
var resolvedTag = workInProgress.tag = resolveLazyComponentTag(Component);
function readLazyComponentType(lazyComponent) {
initializeLazyComponentType(lazyComponent);
if (lazyComponent._status !== Resolved) {
throw lazyComponent._result;
}
return lazyComponent._result;
}
var Uninitialized = -1;
var Pending = 0;
var Resolved = 1;
var Rejected = 2;
function refineResolvedLazyComponent(lazyComponent) {
return lazyComponent._status === Resolved ? lazyComponent._result : null;
}
function initializeLazyComponentType(lazyComponent) {
if (lazyComponent._status === Uninitialized) {
lazyComponent._status = Pending;
var ctor = lazyComponent._ctor;
var thenable = ctor();
lazyComponent._result = thenable;
thenable.then(function (moduleObject) {
if (lazyComponent._status === Pending) {
var defaultExport = moduleObject.default;
{
if (defaultExport === undefined) {
error('lazy: Expected the result of a dynamic import() call. ' + 'Instead received: %s\n\nYour code should look like: \n ' + "const MyComponent = lazy(() => import('./MyComponent'))", moduleObject);
}
}
lazyComponent._status = Resolved;
lazyComponent._result = defaultExport;
}
}, function (error) {
if (lazyComponent._status === Pending) {
lazyComponent._status = Rejected;
lazyComponent._result = error;
}
});
}
}
按照lazy函数的定义,_ctor赋值为传入的参数,_status初始化值就是Uninitialized(-1)。所以在initializeLazyComponentType中直接进入第一个if,执行var thenable = ctor()
, 当thenable这个promise转为fulfilled状态时,status转为resolved状态,将_result赋值为moduleObject.default,并在上层函数(readLazyComponentType)中返回。如果thenable转为rejected状态,则将_result赋值为error,并在上层函数(readLazyComponentType)中throw出来。
由React.Suspense组件对thenable的pending, fulfilled, rejected状态进行处理。
四、为什么我们选择React.lazy+ React.Suspense方案
(前提:项目不支持SSR,并且项目当前的react版本已经支持React.lazy及React.Suspense, 否则就要考虑更新代价)
- react-loadable已经不再维护,对出现的安全问题以及未来react的新feature也不会再做处理。并且在有可替代方案的情况下,我们应该减少对第三方库的依赖,避免维护以及后续更新问题。
- 减少打包大小, 提升一点点点点点点性能。那么删除react-loadable到底会提升多少性能,可以使用source-map-explorer对打包后react-loadable的大小做一下分析
在我们项目中,如果删除react-loadable理论上足足可以减少惊人的4.27kb.......
五、替换前与替换后的代码
react-loadable
export default function AsyncLoad(importFn) {
return Loadable({
loader : importFn,
loading : Loading
});
}
React.lazy + React.Suspense
export default function AsyncLoad(importFn) {
const AsyncLoadComponent = React.lazy(importFn);
return (props) => (
<React.Suspense fallback={<Loading />}>
<AsyncLoadComponent {...props} />
</React.Suspense>
)
六、使用React.lazy与React.Suspense后出现的问题
当前项目使用的仍然是React 16.14.0, 而React 18的Suspense支持SSR, 应该不会出现下面的问题 项目使用Jest+enzyme进行测试,在使用新写法后单元测试报错
ReactDOMServer does not yet support Suspense.
ReactDOMServer does not yet support lazy-loaded components
在这种情况下之前react-loadable始终会返回Loading组件,所以这里我简单做了一下处理
jest.mock('react', () => {
return {
...jest.requireActual('react'),
lazy: () => () => null,
Suspense: ({ children }) => <div>{children}</div>,
}
})
mock掉lazy与Suspense,update对应的snapshot。