feat(core): rework client modules lifecycles, officially make API public (#6732)

This commit is contained in:
Joshua Chen 2022-04-29 21:11:20 +08:00 committed by GitHub
parent 2429bfbd59
commit ae788c536f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 259 additions and 126 deletions

View file

@ -5,21 +5,19 @@
* LICENSE file in the root directory of this source tree.
*/
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import type {ClientModule} from '@docusaurus/types';
export default (function analyticsModule() {
if (!ExecutionEnvironment.canUseDOM) {
return null;
}
return {
onRouteUpdate({location}: {location: Location}) {
const clientModule: ClientModule = {
onRouteDidUpdate({location, previousLocation}) {
if (previousLocation && location.pathname !== previousLocation.pathname) {
// Set page so that subsequent hits on this page are attributed
// to this page. This is recommended for Single-page Applications.
window.ga('set', 'page', location.pathname);
// Always refer to the variable on window in-case it gets
// overridden elsewhere.
window.ga('send', 'pageview');
},
};
})();
}
},
};
export default clientModule;

View file

@ -5,20 +5,16 @@
* LICENSE file in the root directory of this source tree.
*/
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import globalData from '@generated/globalData';
import type {PluginOptions} from '@docusaurus/plugin-google-gtag';
import type {ClientModule} from '@docusaurus/types';
export default (function gtagModule() {
if (!ExecutionEnvironment.canUseDOM) {
return null;
}
const {trackingID} = globalData['docusaurus-plugin-google-gtag']!
.default as PluginOptions;
const {trackingID} = globalData['docusaurus-plugin-google-gtag']!
.default as PluginOptions;
return {
onRouteUpdate({location}: {location: Location}) {
const clientModule: ClientModule = {
onRouteDidUpdate({location, previousLocation}) {
if (previousLocation && location.pathname !== previousLocation.pathname) {
// Always refer to the variable on window in case it gets overridden
// elsewhere.
window.gtag('config', trackingID, {
@ -27,9 +23,11 @@ export default (function gtagModule() {
});
window.gtag('event', 'page_view', {
page_title: document.title,
page_location: location.href,
page_location: window.location.href,
page_path: location.pathname,
});
},
};
})();
}
},
};
export default clientModule;

View file

@ -35,6 +35,7 @@
"copy-text-to-clipboard": "^3.0.1",
"infima": "0.2.0-alpha.38",
"lodash": "^4.17.21",
"nprogress": "^0.2.0",
"postcss": "^8.4.12",
"prism-react-renderer": "^1.3.1",
"prismjs": "^1.28.0",
@ -48,6 +49,7 @@
"@docusaurus/module-type-aliases": "2.0.0-beta.18",
"@docusaurus/types": "2.0.0-beta.18",
"@types/mdx-js__react": "^1.5.5",
"@types/nprogress": "^0.2.0",
"@types/prismjs": "^1.26.0",
"@types/rtlcss": "^3.1.4",
"cross-env": "^7.0.3",

View file

@ -138,6 +138,7 @@ export default function docusaurusThemeClassic(
require.resolve(getInfimaCSSFile(direction)),
'./prism-include-languages',
'./admonitions.css',
'./nprogress',
];
if (customCss) {

View file

@ -11,12 +11,16 @@
* https://github.com/rstacruz/nprogress/blob/master/nprogress.css
*/
:root {
--docusaurus-progress-bar-color: var(--ifm-color-primary);
}
#nprogress {
pointer-events: none;
}
#nprogress .bar {
background: #29d;
background: var(--docusaurus-progress-bar-color);
position: fixed;
z-index: 1031;
top: 0;
@ -30,7 +34,8 @@
right: 0;
width: 100px;
height: 100%;
box-shadow: 0 0 10px #29d, 0 0 5px #29d;
box-shadow: 0 0 10px var(--docusaurus-progress-bar-color),
0 0 5px var(--docusaurus-progress-bar-color);
opacity: 1;
transform: rotate(3deg) translate(0, -4px);
}

View file

@ -0,0 +1,31 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import nprogress from 'nprogress';
import './nprogress.css';
import type {ClientModule} from '@docusaurus/types';
nprogress.configure({showSpinner: false});
const delay = 200;
const clientModule: ClientModule = {
onRouteUpdate({location, previousLocation}) {
if (previousLocation && location.pathname !== previousLocation.pathname) {
const progressBarTimeout = window.setTimeout(() => {
nprogress.start();
}, delay);
return () => window.clearTimeout(progressBarTimeout);
}
return undefined;
},
onRouteDidUpdate() {
nprogress.done();
},
};
export default clientModule;

View file

@ -601,11 +601,14 @@ export type TOCItem = {
};
export type ClientModule = {
onRouteDidUpdate?: (args: {
previousLocation: Location | null;
location: Location;
}) => (() => void) | void;
onRouteUpdate?: (args: {
previousLocation: Location | null;
location: Location;
}) => void;
onRouteUpdateDelayed?: (args: {location: Location}) => void;
}) => (() => void) | void;
};
/** What the user configures. */

View file

@ -77,7 +77,6 @@
"leven": "^3.1.0",
"lodash": "^4.17.21",
"mini-css-extract-plugin": "^2.6.0",
"nprogress": "^0.2.0",
"postcss": "^8.4.12",
"postcss-loader": "^6.2.1",
"prompts": "^2.4.2",
@ -108,7 +107,6 @@
"@docusaurus/module-type-aliases": "2.0.0-beta.18",
"@docusaurus/types": "2.0.0-beta.18",
"@types/detect-port": "^1.3.2",
"@types/nprogress": "^0.2.0",
"@types/react-dom": "^18.0.2",
"@types/react-router-config": "^5.0.6",
"@types/rtl-detect": "^1.0.0",

View file

@ -6,6 +6,7 @@
*/
import React from 'react';
import '@generated/client-modules';
import routes from '@generated/routes';
import {useLocation} from '@docusaurus/router';
@ -19,8 +20,6 @@ import SiteMetadataDefaults from './SiteMetadataDefaults';
import Root from '@theme/Root';
import SiteMetadata from '@theme/SiteMetadata';
import './clientLifecyclesDispatcher';
// TODO, quick fix for CSS insertion order
import ErrorBoundary from '@docusaurus/ErrorBoundary';
import Error from '@theme/Error';
@ -36,9 +35,7 @@ export default function App(): JSX.Element {
<SiteMetadataDefaults />
<SiteMetadata />
<BaseUrlIssueBanner />
<PendingNavigation
location={normalizeLocation(location)}
delay={200}>
<PendingNavigation location={normalizeLocation(location)}>
{routeElement}
</PendingNavigation>
</Root>

View file

@ -0,0 +1,55 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {useLayoutEffect, type ReactElement} from 'react';
import clientModules from '@generated/client-modules';
import type {ClientModule} from '@docusaurus/types';
import type {Location} from 'history';
export function dispatchLifecycleAction<K extends keyof ClientModule>(
lifecycleAction: K,
...args: Parameters<NonNullable<ClientModule[K]>>
): () => void {
const callbacks = clientModules.map((clientModule) => {
const lifecycleFunction = (clientModule?.default?.[lifecycleAction] ??
clientModule[lifecycleAction]) as
| ((
...a: Parameters<NonNullable<ClientModule[K]>>
) => (() => void) | void)
| undefined;
return lifecycleFunction?.(...args);
});
return () => callbacks.forEach((cb) => cb?.());
}
function ClientLifecyclesDispatcher({
children,
location,
previousLocation,
}: {
children: ReactElement;
location: Location;
previousLocation: Location | null;
}): JSX.Element {
useLayoutEffect(() => {
if (previousLocation !== location) {
const {hash} = location;
if (!hash) {
window.scrollTo(0, 0);
} else {
const id = decodeURIComponent(hash.substring(1));
const element = document.getElementById(id);
element?.scrollIntoView();
}
dispatchLifecycleAction('onRouteDidUpdate', {previousLocation, location});
}
}, [previousLocation, location]);
return children;
}
export default ClientLifecyclesDispatcher;

View file

@ -7,18 +7,14 @@
import React from 'react';
import {Route} from 'react-router-dom';
import nprogress from 'nprogress';
import clientLifecyclesDispatcher from './clientLifecyclesDispatcher';
import ClientLifecyclesDispatcher, {
dispatchLifecycleAction,
} from './ClientLifecyclesDispatcher';
import ExecutionEnvironment from './exports/ExecutionEnvironment';
import preload from './preload';
import type {Location} from 'history';
import './nprogress.css';
nprogress.configure({showSpinner: false});
type Props = {
readonly delay: number;
readonly location: Location;
readonly children: JSX.Element;
};
@ -28,14 +24,19 @@ type State = {
class PendingNavigation extends React.Component<Props, State> {
private previousLocation: Location | null;
private progressBarTimeout: number | null;
private routeUpdateCleanupCb: () => void;
constructor(props: Props) {
super(props);
// previousLocation doesn't affect rendering, hence not stored in state.
this.previousLocation = null;
this.progressBarTimeout = null;
this.routeUpdateCleanupCb = ExecutionEnvironment.canUseDOM
? dispatchLifecycleAction('onRouteUpdate', {
previousLocation: null,
location: this.props.location,
})!
: () => {};
this.state = {
nextRouteHasLoaded: true,
};
@ -56,56 +57,32 @@ class PendingNavigation extends React.Component<Props, State> {
// Save the location first.
this.previousLocation = this.props.location;
this.setState({nextRouteHasLoaded: false});
this.startProgressBar();
this.routeUpdateCleanupCb = dispatchLifecycleAction('onRouteUpdate', {
previousLocation: this.previousLocation,
location: nextLocation,
})!;
// Load data while the old screen remains.
preload(nextLocation.pathname)
.then(() => {
clientLifecyclesDispatcher.onRouteUpdate({
previousLocation: this.previousLocation,
location: nextLocation,
});
this.setState({nextRouteHasLoaded: true}, this.stopProgressBar);
const {hash} = nextLocation;
if (!hash) {
window.scrollTo(0, 0);
} else {
const id = decodeURIComponent(hash.substring(1));
const element = document.getElementById(id);
element?.scrollIntoView();
}
this.routeUpdateCleanupCb?.();
this.setState({nextRouteHasLoaded: true});
})
.catch((e) => console.warn(e));
return false;
}
private clearProgressBarTimeout() {
if (this.progressBarTimeout) {
window.clearTimeout(this.progressBarTimeout);
this.progressBarTimeout = null;
}
}
private startProgressBar() {
this.clearProgressBarTimeout();
this.progressBarTimeout = window.setTimeout(() => {
clientLifecyclesDispatcher.onRouteUpdateDelayed({
location: this.props.location,
});
nprogress.start();
}, this.props.delay);
}
private stopProgressBar() {
this.clearProgressBarTimeout();
nprogress.done();
}
override render(): JSX.Element {
const {children, location} = this.props;
// Use a controlled <Route> to trick all descendants into rendering the old
// location.
return <Route location={location} render={() => children} />;
return (
<ClientLifecyclesDispatcher
previousLocation={this.previousLocation}
location={location}>
<Route location={location} render={() => children} />
</ClientLifecyclesDispatcher>
);
}
}

View file

@ -1,34 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import clientModules from '@generated/client-modules';
import type {ClientModule} from '@docusaurus/types';
function dispatchLifecycleAction<K extends keyof ClientModule>(
lifecycleAction: K,
args: Parameters<NonNullable<ClientModule[K]>>,
) {
clientModules.forEach((clientModule) => {
const lifecycleFunction = (clientModule?.default?.[lifecycleAction] ??
clientModule[lifecycleAction]) as
| ((...a: Parameters<NonNullable<ClientModule[K]>>) => void)
| undefined;
lifecycleFunction?.(...args);
});
}
const clientLifecyclesDispatchers: Required<ClientModule> = {
onRouteUpdate(...args) {
dispatchLifecycleAction('onRouteUpdate', args);
},
onRouteUpdateDelayed(...args) {
dispatchLifecycleAction('onRouteUpdateDelayed', args);
},
};
export default clientLifecyclesDispatchers;