mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-03 12:17:20 +02:00
feat(core): rework client modules lifecycles, officially make API public (#6732)
This commit is contained in:
parent
2429bfbd59
commit
ae788c536f
16 changed files with 259 additions and 126 deletions
|
@ -5,21 +5,19 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* 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() {
|
const clientModule: ClientModule = {
|
||||||
if (!ExecutionEnvironment.canUseDOM) {
|
onRouteDidUpdate({location, previousLocation}) {
|
||||||
return null;
|
if (previousLocation && location.pathname !== previousLocation.pathname) {
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
onRouteUpdate({location}: {location: Location}) {
|
|
||||||
// Set page so that subsequent hits on this page are attributed
|
// Set page so that subsequent hits on this page are attributed
|
||||||
// to this page. This is recommended for Single-page Applications.
|
// to this page. This is recommended for Single-page Applications.
|
||||||
window.ga('set', 'page', location.pathname);
|
window.ga('set', 'page', location.pathname);
|
||||||
// Always refer to the variable on window in-case it gets
|
// Always refer to the variable on window in-case it gets
|
||||||
// overridden elsewhere.
|
// overridden elsewhere.
|
||||||
window.ga('send', 'pageview');
|
window.ga('send', 'pageview');
|
||||||
},
|
}
|
||||||
};
|
},
|
||||||
})();
|
};
|
||||||
|
|
||||||
|
export default clientModule;
|
||||||
|
|
|
@ -5,20 +5,16 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
|
||||||
import globalData from '@generated/globalData';
|
import globalData from '@generated/globalData';
|
||||||
import type {PluginOptions} from '@docusaurus/plugin-google-gtag';
|
import type {PluginOptions} from '@docusaurus/plugin-google-gtag';
|
||||||
|
import type {ClientModule} from '@docusaurus/types';
|
||||||
|
|
||||||
export default (function gtagModule() {
|
const {trackingID} = globalData['docusaurus-plugin-google-gtag']!
|
||||||
if (!ExecutionEnvironment.canUseDOM) {
|
.default as PluginOptions;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {trackingID} = globalData['docusaurus-plugin-google-gtag']!
|
const clientModule: ClientModule = {
|
||||||
.default as PluginOptions;
|
onRouteDidUpdate({location, previousLocation}) {
|
||||||
|
if (previousLocation && location.pathname !== previousLocation.pathname) {
|
||||||
return {
|
|
||||||
onRouteUpdate({location}: {location: Location}) {
|
|
||||||
// Always refer to the variable on window in case it gets overridden
|
// Always refer to the variable on window in case it gets overridden
|
||||||
// elsewhere.
|
// elsewhere.
|
||||||
window.gtag('config', trackingID, {
|
window.gtag('config', trackingID, {
|
||||||
|
@ -27,9 +23,11 @@ export default (function gtagModule() {
|
||||||
});
|
});
|
||||||
window.gtag('event', 'page_view', {
|
window.gtag('event', 'page_view', {
|
||||||
page_title: document.title,
|
page_title: document.title,
|
||||||
page_location: location.href,
|
page_location: window.location.href,
|
||||||
page_path: location.pathname,
|
page_path: location.pathname,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
};
|
},
|
||||||
})();
|
};
|
||||||
|
|
||||||
|
export default clientModule;
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
"copy-text-to-clipboard": "^3.0.1",
|
"copy-text-to-clipboard": "^3.0.1",
|
||||||
"infima": "0.2.0-alpha.38",
|
"infima": "0.2.0-alpha.38",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
"postcss": "^8.4.12",
|
"postcss": "^8.4.12",
|
||||||
"prism-react-renderer": "^1.3.1",
|
"prism-react-renderer": "^1.3.1",
|
||||||
"prismjs": "^1.28.0",
|
"prismjs": "^1.28.0",
|
||||||
|
@ -48,6 +49,7 @@
|
||||||
"@docusaurus/module-type-aliases": "2.0.0-beta.18",
|
"@docusaurus/module-type-aliases": "2.0.0-beta.18",
|
||||||
"@docusaurus/types": "2.0.0-beta.18",
|
"@docusaurus/types": "2.0.0-beta.18",
|
||||||
"@types/mdx-js__react": "^1.5.5",
|
"@types/mdx-js__react": "^1.5.5",
|
||||||
|
"@types/nprogress": "^0.2.0",
|
||||||
"@types/prismjs": "^1.26.0",
|
"@types/prismjs": "^1.26.0",
|
||||||
"@types/rtlcss": "^3.1.4",
|
"@types/rtlcss": "^3.1.4",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
|
|
|
@ -138,6 +138,7 @@ export default function docusaurusThemeClassic(
|
||||||
require.resolve(getInfimaCSSFile(direction)),
|
require.resolve(getInfimaCSSFile(direction)),
|
||||||
'./prism-include-languages',
|
'./prism-include-languages',
|
||||||
'./admonitions.css',
|
'./admonitions.css',
|
||||||
|
'./nprogress',
|
||||||
];
|
];
|
||||||
|
|
||||||
if (customCss) {
|
if (customCss) {
|
||||||
|
|
|
@ -11,12 +11,16 @@
|
||||||
* https://github.com/rstacruz/nprogress/blob/master/nprogress.css
|
* https://github.com/rstacruz/nprogress/blob/master/nprogress.css
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--docusaurus-progress-bar-color: var(--ifm-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
#nprogress {
|
#nprogress {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#nprogress .bar {
|
#nprogress .bar {
|
||||||
background: #29d;
|
background: var(--docusaurus-progress-bar-color);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1031;
|
z-index: 1031;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -30,7 +34,8 @@
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
height: 100%;
|
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;
|
opacity: 1;
|
||||||
transform: rotate(3deg) translate(0, -4px);
|
transform: rotate(3deg) translate(0, -4px);
|
||||||
}
|
}
|
31
packages/docusaurus-theme-classic/src/nprogress.ts
Normal file
31
packages/docusaurus-theme-classic/src/nprogress.ts
Normal 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;
|
7
packages/docusaurus-types/src/index.d.ts
vendored
7
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -601,11 +601,14 @@ export type TOCItem = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ClientModule = {
|
export type ClientModule = {
|
||||||
|
onRouteDidUpdate?: (args: {
|
||||||
|
previousLocation: Location | null;
|
||||||
|
location: Location;
|
||||||
|
}) => (() => void) | void;
|
||||||
onRouteUpdate?: (args: {
|
onRouteUpdate?: (args: {
|
||||||
previousLocation: Location | null;
|
previousLocation: Location | null;
|
||||||
location: Location;
|
location: Location;
|
||||||
}) => void;
|
}) => (() => void) | void;
|
||||||
onRouteUpdateDelayed?: (args: {location: Location}) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** What the user configures. */
|
/** What the user configures. */
|
||||||
|
|
|
@ -77,7 +77,6 @@
|
||||||
"leven": "^3.1.0",
|
"leven": "^3.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mini-css-extract-plugin": "^2.6.0",
|
"mini-css-extract-plugin": "^2.6.0",
|
||||||
"nprogress": "^0.2.0",
|
|
||||||
"postcss": "^8.4.12",
|
"postcss": "^8.4.12",
|
||||||
"postcss-loader": "^6.2.1",
|
"postcss-loader": "^6.2.1",
|
||||||
"prompts": "^2.4.2",
|
"prompts": "^2.4.2",
|
||||||
|
@ -108,7 +107,6 @@
|
||||||
"@docusaurus/module-type-aliases": "2.0.0-beta.18",
|
"@docusaurus/module-type-aliases": "2.0.0-beta.18",
|
||||||
"@docusaurus/types": "2.0.0-beta.18",
|
"@docusaurus/types": "2.0.0-beta.18",
|
||||||
"@types/detect-port": "^1.3.2",
|
"@types/detect-port": "^1.3.2",
|
||||||
"@types/nprogress": "^0.2.0",
|
|
||||||
"@types/react-dom": "^18.0.2",
|
"@types/react-dom": "^18.0.2",
|
||||||
"@types/react-router-config": "^5.0.6",
|
"@types/react-router-config": "^5.0.6",
|
||||||
"@types/rtl-detect": "^1.0.0",
|
"@types/rtl-detect": "^1.0.0",
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import '@generated/client-modules';
|
||||||
|
|
||||||
import routes from '@generated/routes';
|
import routes from '@generated/routes';
|
||||||
import {useLocation} from '@docusaurus/router';
|
import {useLocation} from '@docusaurus/router';
|
||||||
|
@ -19,8 +20,6 @@ import SiteMetadataDefaults from './SiteMetadataDefaults';
|
||||||
import Root from '@theme/Root';
|
import Root from '@theme/Root';
|
||||||
import SiteMetadata from '@theme/SiteMetadata';
|
import SiteMetadata from '@theme/SiteMetadata';
|
||||||
|
|
||||||
import './clientLifecyclesDispatcher';
|
|
||||||
|
|
||||||
// TODO, quick fix for CSS insertion order
|
// TODO, quick fix for CSS insertion order
|
||||||
import ErrorBoundary from '@docusaurus/ErrorBoundary';
|
import ErrorBoundary from '@docusaurus/ErrorBoundary';
|
||||||
import Error from '@theme/Error';
|
import Error from '@theme/Error';
|
||||||
|
@ -36,9 +35,7 @@ export default function App(): JSX.Element {
|
||||||
<SiteMetadataDefaults />
|
<SiteMetadataDefaults />
|
||||||
<SiteMetadata />
|
<SiteMetadata />
|
||||||
<BaseUrlIssueBanner />
|
<BaseUrlIssueBanner />
|
||||||
<PendingNavigation
|
<PendingNavigation location={normalizeLocation(location)}>
|
||||||
location={normalizeLocation(location)}
|
|
||||||
delay={200}>
|
|
||||||
{routeElement}
|
{routeElement}
|
||||||
</PendingNavigation>
|
</PendingNavigation>
|
||||||
</Root>
|
</Root>
|
||||||
|
|
|
@ -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;
|
|
@ -7,18 +7,14 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {Route} from 'react-router-dom';
|
import {Route} from 'react-router-dom';
|
||||||
import nprogress from 'nprogress';
|
import ClientLifecyclesDispatcher, {
|
||||||
|
dispatchLifecycleAction,
|
||||||
import clientLifecyclesDispatcher from './clientLifecyclesDispatcher';
|
} from './ClientLifecyclesDispatcher';
|
||||||
|
import ExecutionEnvironment from './exports/ExecutionEnvironment';
|
||||||
import preload from './preload';
|
import preload from './preload';
|
||||||
import type {Location} from 'history';
|
import type {Location} from 'history';
|
||||||
|
|
||||||
import './nprogress.css';
|
|
||||||
|
|
||||||
nprogress.configure({showSpinner: false});
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
readonly delay: number;
|
|
||||||
readonly location: Location;
|
readonly location: Location;
|
||||||
readonly children: JSX.Element;
|
readonly children: JSX.Element;
|
||||||
};
|
};
|
||||||
|
@ -28,14 +24,19 @@ type State = {
|
||||||
|
|
||||||
class PendingNavigation extends React.Component<Props, State> {
|
class PendingNavigation extends React.Component<Props, State> {
|
||||||
private previousLocation: Location | null;
|
private previousLocation: Location | null;
|
||||||
private progressBarTimeout: number | null;
|
private routeUpdateCleanupCb: () => void;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
// previousLocation doesn't affect rendering, hence not stored in state.
|
// previousLocation doesn't affect rendering, hence not stored in state.
|
||||||
this.previousLocation = null;
|
this.previousLocation = null;
|
||||||
this.progressBarTimeout = null;
|
this.routeUpdateCleanupCb = ExecutionEnvironment.canUseDOM
|
||||||
|
? dispatchLifecycleAction('onRouteUpdate', {
|
||||||
|
previousLocation: null,
|
||||||
|
location: this.props.location,
|
||||||
|
})!
|
||||||
|
: () => {};
|
||||||
this.state = {
|
this.state = {
|
||||||
nextRouteHasLoaded: true,
|
nextRouteHasLoaded: true,
|
||||||
};
|
};
|
||||||
|
@ -56,56 +57,32 @@ class PendingNavigation extends React.Component<Props, State> {
|
||||||
// Save the location first.
|
// Save the location first.
|
||||||
this.previousLocation = this.props.location;
|
this.previousLocation = this.props.location;
|
||||||
this.setState({nextRouteHasLoaded: false});
|
this.setState({nextRouteHasLoaded: false});
|
||||||
this.startProgressBar();
|
this.routeUpdateCleanupCb = dispatchLifecycleAction('onRouteUpdate', {
|
||||||
|
previousLocation: this.previousLocation,
|
||||||
|
location: nextLocation,
|
||||||
|
})!;
|
||||||
|
|
||||||
// Load data while the old screen remains.
|
// Load data while the old screen remains.
|
||||||
preload(nextLocation.pathname)
|
preload(nextLocation.pathname)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
clientLifecyclesDispatcher.onRouteUpdate({
|
this.routeUpdateCleanupCb?.();
|
||||||
previousLocation: this.previousLocation,
|
this.setState({nextRouteHasLoaded: true});
|
||||||
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();
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch((e) => console.warn(e));
|
.catch((e) => console.warn(e));
|
||||||
return false;
|
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 {
|
override render(): JSX.Element {
|
||||||
const {children, location} = this.props;
|
const {children, location} = this.props;
|
||||||
// Use a controlled <Route> to trick all descendants into rendering the old
|
// Use a controlled <Route> to trick all descendants into rendering the old
|
||||||
// location.
|
// location.
|
||||||
return <Route location={location} render={() => children} />;
|
return (
|
||||||
|
<ClientLifecyclesDispatcher
|
||||||
|
previousLocation={this.previousLocation}
|
||||||
|
location={location}>
|
||||||
|
<Route location={location} render={() => children} />
|
||||||
|
</ClientLifecyclesDispatcher>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
|
@ -215,6 +215,7 @@ preconfigured
|
||||||
preconnect
|
preconnect
|
||||||
prefetch
|
prefetch
|
||||||
prefetching
|
prefetching
|
||||||
|
preloads
|
||||||
prepended
|
prepended
|
||||||
preprocessors
|
preprocessors
|
||||||
prerendered
|
prerendered
|
||||||
|
|
|
@ -5,13 +5,48 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
import siteConfig from '@generated/docusaurus.config';
|
||||||
|
import type {Location} from 'history';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
function logPage(
|
||||||
export function onRouteUpdate({location}: {location: Location}): void {
|
event: string,
|
||||||
// console.log('onRouteUpdate', {location});
|
location: Location,
|
||||||
|
previousLocation: Location | null,
|
||||||
|
): void {
|
||||||
|
console.log(`${event}
|
||||||
|
Previous location: ${previousLocation?.pathname}
|
||||||
|
Current location: ${location.pathname}
|
||||||
|
Current heading: ${document.getElementsByTagName('h1')[0]?.innerText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ExecutionEnvironment.canUseDOM) {
|
export function onRouteUpdate({
|
||||||
// console.log('client module example log');
|
location,
|
||||||
|
previousLocation,
|
||||||
|
}: {
|
||||||
|
location: Location;
|
||||||
|
previousLocation: Location | null;
|
||||||
|
}): (() => void) | void {
|
||||||
|
if (
|
||||||
|
process.env.NODE_ENV === 'development' ||
|
||||||
|
siteConfig.customFields!.isDeployPreview
|
||||||
|
) {
|
||||||
|
logPage('onRouteUpdate', location, previousLocation);
|
||||||
|
return () => logPage('onRouteUpdate cleanup', location, previousLocation);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onRouteDidUpdate({
|
||||||
|
location,
|
||||||
|
previousLocation,
|
||||||
|
}: {
|
||||||
|
location: Location;
|
||||||
|
previousLocation: Location | null;
|
||||||
|
}): void {
|
||||||
|
if (
|
||||||
|
process.env.NODE_ENV === 'development' ||
|
||||||
|
siteConfig.customFields!.isDeployPreview
|
||||||
|
) {
|
||||||
|
logPage('onRouteDidUpdate', location, previousLocation);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,7 +86,7 @@ Client modules are part of your site's bundle, just like theme components. Howev
|
||||||
|
|
||||||
These modules are imported globally before React even renders the initial UI.
|
These modules are imported globally before React even renders the initial UI.
|
||||||
|
|
||||||
```js title="App.tsx"
|
```js title="@docusaurus/core/App.tsx"
|
||||||
// How it works under the hood
|
// How it works under the hood
|
||||||
import '@generated/client-modules';
|
import '@generated/client-modules';
|
||||||
```
|
```
|
||||||
|
@ -117,5 +117,70 @@ CSS stylesheets imported as client modules are [global](../styling-layout.md#glo
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
<!-- TODO client module lifecycles -->
|
### Client module lifecycles {#client-module-lifecycles}
|
||||||
<!-- https://github.com/facebook/docusaurus/issues/3399 -->
|
|
||||||
|
Besides introducing side-effects, client modules can optionally export two lifecycle functions: `onRouteUpdate` and `onRouteDidUpdate`.
|
||||||
|
|
||||||
|
Because Docusaurus builds a single-page application, `script` tags will only be executed the first time the page loads, but will not re-execute on page transitions. These lifecycles are useful if you have some imperative JS logic that should execute every time a new page has loaded, e.g., to manipulate DOM elements, to send analytics data, etc.
|
||||||
|
|
||||||
|
For every route transition, there will be several important timings:
|
||||||
|
|
||||||
|
1. The user clicks a link, which causes the router to change its current location.
|
||||||
|
2. Docusaurus preloads the next route's assets, while keeping displaying the current page's content.
|
||||||
|
3. The next route's assets have loaded.
|
||||||
|
4. The new location's route component gets rendered to DOM.
|
||||||
|
|
||||||
|
`onRouteUpdate` will be called at event (2), and `onRouteDidUpdate` will be called at (4). They both receive the current location and the previous location (which can be `null`, if this is the first screen).
|
||||||
|
|
||||||
|
`onRouteUpdate` can optionally return a "cleanup" callback, which will be called at (3). For example, if you want to display a progress bar, you can start a timeout in `onRouteUpdate`, and clear the timeout in the callback. (The classic theme already provides an `nprogress` integration this way.)
|
||||||
|
|
||||||
|
Note that the new page's DOM is only available during event (4). If you need to manipulate the new page's DOM, you'll likely want to use `onRouteDidUpdate`, which will be fired as soon as the DOM on the new page has mounted.
|
||||||
|
|
||||||
|
```js title="myClientModule.js"
|
||||||
|
import type {Location} from 'history';
|
||||||
|
|
||||||
|
export function onRouteDidUpdate({location, previousLocation}) {
|
||||||
|
// Don't execute if we are still on the same page; the lifecycle may be fired
|
||||||
|
// because the hash changes (e.g. when navigating between headings)
|
||||||
|
if (location.pathname !== previousLocation?.pathname) {
|
||||||
|
const title = document.getElementsByTagName('h1')[0];
|
||||||
|
if (title) {
|
||||||
|
title.innerText += '❤️';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onRouteUpdate({location, previousLocation}) {
|
||||||
|
if (location.pathname !== previousLocation?.pathname) {
|
||||||
|
const progressBarTimeout = window.setTimeout(() => {
|
||||||
|
nprogress.start();
|
||||||
|
}, delay);
|
||||||
|
return () => window.clearTimeout(progressBarTimeout);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, if you are using TypeScript and you want to leverage contextual typing:
|
||||||
|
|
||||||
|
```ts title="myClientModule.js"
|
||||||
|
import type {ClientModule} from '@docusaurus/types';
|
||||||
|
|
||||||
|
const module: ClientModule = {
|
||||||
|
onRouteUpdate({location, previousLocation}) {
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
onRouteDidUpdate({location, previousLocation}) {
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default module;
|
||||||
|
```
|
||||||
|
|
||||||
|
Both lifecycles will fire on first render, but they will not fire on server-side, so you can safely access browser globals in them.
|
||||||
|
|
||||||
|
:::tip Prefer using React
|
||||||
|
|
||||||
|
Client module lifecycles are purely imperative, and you can't use React hooks or access React contexts within them. If your operations are state-driven or involve complicated DOM manipulations, you should consider [swizzling components](../swizzling.md) instead.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
|
@ -113,6 +113,7 @@ const config = {
|
||||||
onBrokenMarkdownLinks: 'warn',
|
onBrokenMarkdownLinks: 'warn',
|
||||||
favicon: 'img/docusaurus.ico',
|
favicon: 'img/docusaurus.ico',
|
||||||
customFields: {
|
customFields: {
|
||||||
|
isDeployPreview,
|
||||||
description:
|
description:
|
||||||
'An optimized site generator in React. Docusaurus helps you to move fast and write content. Build documentation websites, blogs, marketing pages, and more.',
|
'An optimized site generator in React. Docusaurus helps you to move fast and write content. Build documentation websites, blogs, marketing pages, and more.',
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Reference in a new issue