mirror of
https://github.com/facebook/docusaurus.git
synced 2025-08-02 00:09:48 +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.
|
||||
*/
|
||||
|
||||
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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -138,6 +138,7 @@ export default function docusaurusThemeClassic(
|
|||
require.resolve(getInfimaCSSFile(direction)),
|
||||
'./prism-include-languages',
|
||||
'./admonitions.css',
|
||||
'./nprogress',
|
||||
];
|
||||
|
||||
if (customCss) {
|
||||
|
|
|
@ -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);
|
||||
}
|
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 = {
|
||||
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. */
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
Loading…
Add table
Add a link
Reference in a new issue