Skip to content

Commit

Permalink
refactor: loadable
Browse files Browse the repository at this point in the history
  • Loading branch information
yunsii committed Apr 12, 2021
1 parent 888cf8a commit e09296d
Show file tree
Hide file tree
Showing 6 changed files with 351 additions and 72 deletions.
9 changes: 4 additions & 5 deletions packages/runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
"README.md"
],
"scripts": {
"build": "rm -rf ./dist && tsc",
"dev": "rm -rf ./dist && tsc -w"
"build": "rm -rf ./dist && tsc && node ../../scripts/copy.js",
"dev": "rm -rf ./dist && node ../../scripts/copy.js && tsc -w"
},
"repository": {
"type": "git",
Expand All @@ -29,13 +29,12 @@
"access": "public"
},
"dependencies": {
"@types/react-loadable": "^5.5.5",
"@types/react-router": "^5.1.13",
"@types/react-router-dom": "^5.1.7",
"history-with-query": "^4.10.4",
"react-loadable": "^5.5.0",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0"
"react-router-dom": "^5.2.0",
"use-subscription": "^1.5.1"
},
"gitHead": "3a9f3aab27ee09337490e3d7b3b5bfd2d81683a4"
}
4 changes: 2 additions & 2 deletions packages/runtime/src/dynamic/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import Loadable from 'react-loadable';
import React from 'react';
import Loadable from './loadable';

interface LoadableOptions {
loading?: Function;
Expand Down
5 changes: 5 additions & 0 deletions packages/runtime/src/dynamic/loadable-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as React from 'react';

type CaptureFn = (moduleName: string) => void;

export const LoadableContext = React.createContext<CaptureFn | null>(null);
13 changes: 13 additions & 0 deletions packages/runtime/src/dynamic/loadable.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';

declare namespace LoadableExport {
interface ILoadable {
<P = {}>(opts: any): React.ComponentClass<P>;
Map<P = {}>(opts: any): React.ComponentType<P>;
preloadAll(): Promise<any>;
}
}

declare const LoadableExport: LoadableExport.ILoadable;

export = LoadableExport;
318 changes: 318 additions & 0 deletions packages/runtime/src/dynamic/loadable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
// ref: https://github.com/umijs/umi/blob/master/packages/runtime/src/dynamic/loadable.js

import { createElement, useContext, useImperativeHandle, forwardRef } from 'react';
import { useSubscription } from 'use-subscription';
import { LoadableContext } from './loadable-context';

const ALL_INITIALIZERS = [];
const READY_INITIALIZERS = [];
let initialized = false;

function load(loader) {
let promise = loader();

let state = {
loading: true,
loaded: null,
error: null,
};

state.promise = promise
.then((loaded) => {
state.loading = false;
state.loaded = loaded;
return loaded;
})
.catch((err) => {
state.loading = false;
state.error = err;
throw err;
});

return state;
}

function loadMap(obj) {
let state = {
loading: false,
loaded: {},
error: null,
};

let promises = [];

try {
Object.keys(obj).forEach((key) => {
let result = load(obj[key]);

if (!result.loading) {
state.loaded[key] = result.loaded;
state.error = result.error;
} else {
state.loading = true;
}

promises.push(result.promise);

result.promise
.then((res) => {
state.loaded[key] = res;
})
.catch((err) => {
state.error = err;
});
});
} catch (err) {
state.error = err;
}

state.promise = Promise.all(promises)
.then((res) => {
state.loading = false;
return res;
})
.catch((err) => {
state.loading = false;
throw err;
});

return state;
}

function resolve(obj) {
return obj && obj.__esModule ? obj.default : obj;
}

function render(loaded, props) {
return createElement(resolve(loaded), props);
}

function createLoadableComponent(loadFn, options) {
let opts = Object.assign(
{
loader: null,
loading: null,
delay: 200,
timeout: null,
render: render,
webpack: null,
modules: null,
},
options
);

let subscription = null;

function init() {
if (!subscription) {
const sub = new LoadableSubscription(loadFn, opts);
subscription = {
getCurrentValue: sub.getCurrentValue.bind(sub),
subscribe: sub.subscribe.bind(sub),
retry: sub.retry.bind(sub),
promise: sub.promise.bind(sub),
};
}
return subscription.promise();
}

// Server only
if (typeof window === 'undefined') {
ALL_INITIALIZERS.push(init);
}

// Client only
if (!initialized && typeof window !== 'undefined' && typeof opts.webpack === 'function') {
const moduleIds = opts.webpack();
READY_INITIALIZERS.push((ids) => {
for (const moduleId of moduleIds) {
if (ids.indexOf(moduleId) !== -1) {
return init();
}
}
});
}

const LoadableComponent = (props, ref) => {
init();

const context = useContext(LoadableContext);
const state = useSubscription(subscription);

useImperativeHandle(ref, () => ({
retry: subscription.retry,
}));

if (context && Array.isArray(opts.modules)) {
opts.modules.forEach((moduleName) => {
context(moduleName);
});
}

if (state.loading || state.error) {
if (process.env.NODE_ENV === 'development' && state.error) {
console.error(`[@umijs/runtime] load component failed`, state.error);
}
return createElement(opts.loading, {
isLoading: state.loading,
pastDelay: state.pastDelay,
timedOut: state.timedOut,
error: state.error,
retry: subscription.retry,
});
} else if (state.loaded) {
return opts.render(state.loaded, props);
} else {
return null;
}
};

const LoadableComponentWithRef = forwardRef(LoadableComponent);
// add static method in React.forwardRef
// https://github.com/facebook/react/issues/17830
LoadableComponentWithRef.preload = () => init();
LoadableComponentWithRef.displayName = 'LoadableComponent';

return LoadableComponentWithRef;
}

class LoadableSubscription {
constructor(loadFn, opts) {
this._loadFn = loadFn;
this._opts = opts;
this._callbacks = new Set();
this._delay = null;
this._timeout = null;

this.retry();
}

promise() {
return this._res.promise;
}

retry() {
this._clearTimeouts();
this._res = this._loadFn(this._opts.loader);

this._state = {
pastDelay: false,
timedOut: false,
};

const { _res: res, _opts: opts } = this;

if (res.loading) {
if (typeof opts.delay === 'number') {
if (opts.delay === 0) {
this._state.pastDelay = true;
} else {
this._delay = setTimeout(() => {
this._update({
pastDelay: true,
});
}, opts.delay);
}
}

if (typeof opts.timeout === 'number') {
this._timeout = setTimeout(() => {
this._update({ timedOut: true });
}, opts.timeout);
}
}

this._res.promise
.then(() => {
this._update();
this._clearTimeouts();
})
// eslint-disable-next-line handle-callback-err
.catch((err) => {
this._update();
this._clearTimeouts();
});
this._update({});
}

_update(partial) {
this._state = {
...this._state,
...partial,
};
this._callbacks.forEach((callback) => callback());
}

_clearTimeouts() {
clearTimeout(this._delay);
clearTimeout(this._timeout);
}

getCurrentValue() {
return {
...this._state,
error: this._res.error,
loaded: this._res.loaded,
loading: this._res.loading,
};
}

subscribe(callback) {
this._callbacks.add(callback);
return () => {
this._callbacks.delete(callback);
};
}
}

function Loadable(opts) {
return createLoadableComponent(load, opts);
}

function LoadableMap(opts) {
if (typeof opts.render !== 'function') {
throw new Error('LoadableMap requires a `render(loaded, props)` function');
}

return createLoadableComponent(loadMap, opts);
}

Loadable.Map = LoadableMap;

function flushInitializers(initializers, ids) {
let promises = [];

while (initializers.length) {
let init = initializers.pop();
promises.push(init(ids));
}

return Promise.all(promises).then(() => {
if (initializers.length) {
return flushInitializers(initializers, ids);
}
});
}

Loadable.preloadAll = () => {
return new Promise((resolve, reject) => {
flushInitializers(ALL_INITIALIZERS).then(resolve, reject);
});
};

Loadable.preloadReady = (ids = []) => {
return new Promise((resolve) => {
const res = () => {
initialized = true;
return resolve();
};
// We always will resolve, errors should be handled within loading UIs.
flushInitializers(READY_INITIALIZERS, ids).then(res, res);
});
};

if (typeof window !== 'undefined') {
window.__NEXT_PRELOADREADY = Loadable.preloadReady;
}

export default Loadable;
Loading

0 comments on commit e09296d

Please sign in to comment.