');}.css-15xuo19 .MuiSwitch-switchBase.Mui-checked+.MuiSwitch-track{opacity:1;background-color:#aab4be;}.css-15xuo19 .MuiSwitch-thumb{background-color:#001e3c;width:32px;height:32px;}.css-15xuo19 .MuiSwitch-thumb:before{content:'';position:absolute;width:100%;height:100%;left:0;top:0;background-repeat:no-repeat;-webkit-background-position:center;background-position:center;background-image:url('data:image/svg+xml;utf8,');}.css-15xuo19 .MuiSwitch-track{opacity:1;background-color:#aab4be;border-radius:10px;}
点击<Link>
(next/link
)或者在当前页面刷新,等待加载的过程中,会报错如下,同时加载失败,非必现,偶发;
Error: Route did not complete loading
暂时没有解决,查看GitHub上next.js官网同样有此issue(Error: Route did not complete loading: /projects #21543),并且在2021年3月6日解决(Fix idleTimeout error being thrown in route loader #22775),但目前使用的13版本仍然出现了该问题。
代码
// _app.js
import '@/styles/globals.css'
import Head from 'next/head'
import Layout from '@/components/Layout';
import { useEffect } from 'react';
import { Provider } from 'react-redux';
import { wrapper } from '@/store/index';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import { useRouter } from 'next/router';
NProgress.configure({ ease:'ease-in', speed: 300, showSpinner: false });
function App({ Component, ...rest }) {
const {store, props} = wrapper.useWrappedStore(rest);
const router = useRouter();
useEffect(() => {
const handleStart = (url) => {
console.log('page transfer start',url, router.asPath);
(url !== router.asPath && url !== '/login' && url !== '/signup') && NProgress.start();
}
const handleComplete = (url) => {
console.log('page transfer end', url, router.asPath, router.isReady);
if (url === router.asPath && url !== '/login' && url !== '/signup') {
NProgress.done();
}
}
router.events.on('routeChangeStart', handleStart)
router.events.on('routeChangeComplete', handleComplete)
router.events.on('routeChangeError', handleComplete)
return () => {
router.events.off('routeChangeStart', handleStart)
router.events.off('routeChangeComplete', handleComplete)
router.events.off('routeChangeError', handleComplete)
}
})
return (
<>
<Head>
<title>SHARE JS</title>
<meta name="description" content="js技术分享" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
</Head>
<Provider store={store}>
<Layout isMobile={props.pageProps.isMobile}>
<Component {...props.pageProps} />
</Layout>
</Provider>
</>
)
}
export async function getServerSideProps({req}) {
const userAgent = req ? req.headers['user-agent'] : navigator.userAgent;
const isMobile = /Mobile|android|iPhone/.test(userAgent);
// Pass data to the page via props
return { props: { isMobile } }
}
// export default wrapper.withRedux(App)
export default App
在使用路由导航事件添加导航间的动画时,发生了以下的问题:
NProgress.done()
会被调用,页面表现上就是页面的加载条加载完成后消失;NProgress.done()
不会被调用,页面表现上就是页面的加载条一直在动画加载,并且不会消失;登录态应该不至于影响路由的路径,而且测试的路由都是不需要进行用户身份验证的。
目前暂时不去验证url === router.asPath
来解决这个问题,next.js官网的page transition example同样没有去验证。
在本地调试时,发现,未登录态,动态路由的页面,会有这个问题。如跳转到文章详情页面时,就会这样。也就是说,在调用handleComplete方法时,router的asPath还未更改。router事件回调中,访问router的asPath属性比较混乱,不建议使用该属性;
使用framer-motion为页面过渡添加动画,当路由变化时,页面的样式会瞬间失效,然后开始动画,经排查,发现样式文件会在路由变化开始时,卸载当前的样式,见下图,注意看路由变化时,右侧的style标签变化。同时,该问题早在2020年就有人提出。在生产环境下会出现,在开发环境下不会出现,原因在于开发环境下没有对应的style标签,也就是说,开发环境下的样式添加与生产环境下的样式添加不一样。
如果使用style属性来进行样式添加,则没有该问题,例如styled-component
的方式进行样式添加(但不知道stled-component与framer-motion协作效果如何)。
media="x"
导致这一个bug的出现,所以路由变化要清除这个属性;经过尝试,在第一次切换页面时,仍然存在上述问题,但之后切换页面时,上述问题消失;const handleStart = (url) => {
const tempFix = () => {
const allStyleElems = document.querySelectorAll('style[media="x"]');
allStyleElems.forEach((elem) => {
elem.removeAttribute("media");
});
};
tempFix();
(url !== router.asPath) && NProgress.start();
}
const handleComplete = () => {
const tempFix = () => {
const allStyleElems = document.querySelectorAll('style[media="x"]');
allStyleElems.forEach((elem) => {
elem.removeAttribute("media");
});
};
tempFix();
NProgress.done();
}
router.events.on('routeChangeStart', handleStart)
router.events.on('routeChangeComplete', handleComplete)
router.events.on('routeChangeError', handleComplete)
import * as React from 'react';
export const useNextCssRemovalPrevention = () => {
React.useEffect(() => {
// Remove data-n-p attribute from all link nodes.
// This prevents Next.js from removing server rendered stylesheets.
document.querySelectorAll('head > link[data-n-p]').forEach(linkNode => {
linkNode.removeAttribute('data-n-p');
});
const mutationHandler = (mutations: MutationRecord[]) => {
mutations.forEach(({ target, addedNodes }: MutationRecord) => {
if (target.nodeName === 'HEAD') {
// Add data-n-href-perm attribute to all style nodes with attribute data-n-href,
// and remove data-n-href and media attributes from those nodes.
// This prevents Next.js from removing or disabling dynamic stylesheets.
addedNodes.forEach(node => {
const el = node as Element;
if (el.nodeName === 'STYLE' && el.hasAttribute('data-n-href')) {
const href = el.getAttribute('data-n-href');
if (href) {
el.setAttribute('data-n-href-perm', href);
el.removeAttribute('data-n-href');
el.removeAttribute('media');
}
}
});
// Remove all stylesheets that we don't need anymore
// (all except the two that were most recently added).
const styleNodes = document.querySelectorAll('head > style[data-n-href-perm]');
const requiredHrefs = new Set<string>();
for (let i = styleNodes.length - 1; i >= 0; i--) {
const el = styleNodes[i];
if (requiredHrefs.size < 2) {
const href = el.getAttribute('data-n-href-perm');
if (href) {
if (requiredHrefs.has(href)) {
el.parentNode!.removeChild(el);
} else {
requiredHrefs.add(href);
}
}
} else {
el.parentNode!.removeChild(el);
}
}
}
});
};
// Observe changes to the head element and its descendents.
const observer = new MutationObserver(mutationHandler);
observer.observe(document.head, { childList: true, subtree: true });
return () => {
// Disconnect the observer when the component unmounts.
observer.disconnect();
};
}, []);
};
使用react-markdown渲染Markdown文本为HTML时,在使用SSR渲染的情况下,如果有数学公式插件,会导致SSR和CSR渲染的结果不一致,hydration失败的问题。经过排查,发现rehypeMathJaxSvg插件会生成style标签,将文本样式放在style标签里。react-markdown组件会调用React.createElement
生成虚拟DOM节点,此时的style标签内部children文本仍然没有问题。但是在服务端使用ReactDOM.renderToString
函数时,会将style标签内的文本进行转义,"
转义为"
,而客户端在使用hydrate
方法时,不会进行转义,导致SSR和CSR结果不一致,报错。所以如果react-markdown组件只要使用SSR渲染,就会报错,使用CSR渲染则没问题。但是为了SEO,SSR渲染是很有必要的。
import ReactMarkdown from 'react-markdown';
import rehypeMathJaxSvg from 'rehype-mathjax';
<ReactMarkdown
children={content}
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeMathJaxSvg, rehypeSlug, [rehypeHighlight, { ignoreMissing: true, plainText: ['txt', 'text'] }]]}
/>
其中,SSR的数学公式插件给出的样式如下:
mjx-container[jax="SVG"] {
direction: ltr;
}
mjx-container[jax="SVG"] > svg {
overflow: visible;
min-height: 1px;
min-width: 1px;
}
mjx-container[jax="SVG"] > svg a {
fill: blue;
stroke: blue;
}
mjx-container[jax="SVG"][display="true"] {
display: block;
text-align: center;
margin: 1em 0;
}
mjx-container[jax="SVG"][display="true"][width="full"] {
display: flex;
}
mjx-container[jax="SVG"][justify="left"] {
text-align: left;
}
mjx-container[jax="SVG"][justify="right"] {
text-align: right;
}
g[data-mml-node="merror"] > g {
fill: red;
stroke: red;
}
g[data-mml-node="merror"] > rect[data-background] {
fill: yellow;
stroke: none;
}
g[data-mml-node="mtable"] > line[data-line], svg[data-table] > g > line[data-line] {
stroke-width: 70px;
fill: none;
}
g[data-mml-node="mtable"] > rect[data-frame], svg[data-table] > g > rect[data-frame] {
stroke-width: 70px;
fill: none;
}
g[data-mml-node="mtable"] > .mjx-dashed, svg[data-table] > g > .mjx-dashed {
stroke-dasharray: 140;
}
g[data-mml-node="mtable"] > .mjx-dotted, svg[data-table] > g > .mjx-dotted {
stroke-linecap: round;
stroke-dasharray: 0,140;
}
g[data-mml-node="mtable"] > g > svg {
overflow: visible;
}
[jax="SVG"] mjx-tool {
display: inline-block;
position: relative;
width: 0;
height: 0;
}
[jax="SVG"] mjx-tool > mjx-tip {
position: absolute;
top: 0;
left: 0;
}
mjx-tool > mjx-tip {
display: inline-block;
padding: .2em;
border: 1px solid #888;
font-size: 70%;
background-color: #F8F8F8;
color: black;
box-shadow: 2px 2px 5px #AAAAAA;
}
g[data-mml-node="maction"][data-toggle] {
cursor: pointer;
}
mjx-status {
display: block;
position: fixed;
left: 1em;
bottom: 1em;
min-width: 25%;
padding: .2em .4em;
border: 1px solid #888;
font-size: 90%;
background-color: #F8F8F8;
color: black;
}
foreignObject[data-mjx-xml] {
font-family: initial;
line-height: normal;
overflow: visible;
}
mjx-container[jax="SVG"] path[data-c], mjx-container[jax="SVG"] use[data-c] {
stroke-width: 3;
}
而CSR给出的数学公式插件样式如下:
mjx-container[jax="SVG"] {
direction: ltr;
}
mjx-container[jax="SVG"] > svg {
overflow: visible;
min-height: 1px;
min-width: 1px;
}
mjx-container[jax="SVG"] > svg a {
fill: blue;
stroke: blue;
}
mjx-container[jax="SVG"][display="true"] {
display: block;
text-align: center;
margin: 1em 0;
}
mjx-container[jax="SVG"][display="true"][width="full"] {
display: flex;
}
mjx-container[jax="SVG"][justify="left"] {
text-align: left;
}
mjx-container[jax="SVG"][justify="right"] {
text-align: right;
}
g[data-mml-node="merror"] > g {
fill: red;
stroke: red;
}
g[data-mml-node="merror"] > rect[data-background] {
fill: yellow;
stroke: none;
}
g[data-mml-node="mtable"] > line[data-line], svg[data-table] > g > line[data-line] {
stroke-width: 70px;
fill: none;
}
g[data-mml-node="mtable"] > rect[data-frame], svg[data-table] > g > rect[data-frame] {
stroke-width: 70px;
fill: none;
}
g[data-mml-node="mtable"] > .mjx-dashed, svg[data-table] > g > .mjx-dashed {
stroke-dasharray: 140;
}
g[data-mml-node="mtable"] > .mjx-dotted, svg[data-table] > g > .mjx-dotted {
stroke-linecap: round;
stroke-dasharray: 0,140;
}
g[data-mml-node="mtable"] > g > svg {
overflow: visible;
}
[jax="SVG"] mjx-tool {
display: inline-block;
position: relative;
width: 0;
height: 0;
}
[jax="SVG"] mjx-tool > mjx-tip {
position: absolute;
top: 0;
left: 0;
}
mjx-tool > mjx-tip {
display: inline-block;
padding: .2em;
border: 1px solid #888;
font-size: 70%;
background-color: #F8F8F8;
color: black;
box-shadow: 2px 2px 5px #AAAAAA;
}
g[data-mml-node="maction"][data-toggle] {
cursor: pointer;
}
mjx-status {
display: block;
position: fixed;
left: 1em;
bottom: 1em;
min-width: 25%;
padding: .2em .4em;
border: 1px solid #888;
font-size: 90%;
background-color: #F8F8F8;
color: black;
}
foreignObject[data-mjx-xml] {
font-family: initial;
line-height: normal;
overflow: visible;
}
mjx-container[jax="SVG"] path[data-c], mjx-container[jax="SVG"] use[data-c] {
stroke-width: 3;
}
使用create-react-app生成react项目,在App.js中加入style标签相关代码
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
<style>
{
`mjx-container[jax="SVG"] {
direction: ltr;
}`
}
</style>
</div>
);
}
在index.js中使用renderToString
方法,打印渲染结果
import React from 'react';
import ReactDOM from 'react-dom/client';
import { renderToString } from 'react-dom/server';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
const res = renderToString(<App />);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
console.log('res: ', res)
// res的打印结果,很明显转义了
res: <div class="App"><header class="App-header"><img src="/static/media/logo.6ce24c58023cc2f8fd88fe9d219db6c6.svg" class="App-logo" alt="logo"/><p>Edit <code>src/App.js</code> and save to reload.</p><a class="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer">Learn React</a></header>
<style>mjx-container[jax="SVG"] {
direction: ltr;
}</style></div>
使用rehype-katex
代替rehype-mathjax
,但要记得引入katex的样式文件import 'katex/dist/katex.min.css'
import rehypeKatex from 'rehype-katex';
import 'katex/dist/katex.min.css';
const ReactMarkdown = dynamic(() => import('react-markdown'), { ssr: true });
<ReactMarkdown
children={content}
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex, rehypeSlug, [rehypeHighlight, { ignoreMissing: true, plainText: ['txt', 'text'] }] ]}
/>