目录

环境配置

  • next.js: 13.1.6
  • node.js: v16.14.0
  • mongoDB: v4.4.3
  • mongoose: v5.13.8
  • 操作系统:
    • 生产环境: centos v7
    • 开发环境:macos v12.3

路由问题


Route did not complete loading

点击<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版本仍然出现了该问题。

路由导航时,router.asPath的值不一致

代码

// _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

在使用路由导航事件添加导航间的动画时,发生了以下的问题:

  • 在登录态时,handleComplete中的url与router.asPath的值是一样的,因此NProgress.done()会被调用,页面表现上就是页面的加载条加载完成后消失;
  • 在未登录态时,handleComplete中的url与router.asPath的值是不一样的,因此NProgress.done()不会被调用,页面表现上就是页面的加载条一直在动画加载,并且不会消失;

登录态应该不至于影响路由的路径,而且测试的路由都是不需要进行用户身份验证的。

解决

目前暂时不去验证url === router.asPath来解决这个问题,next.js官网的page transition example同样没有去验证。 在本地调试时,发现,未登录态,动态路由的页面,会有这个问题。如跳转到文章详情页面时,就会这样。也就是说,在调用handleComplete方法时,router的asPath还未更改。router事件回调中,访问router的asPath属性比较混乱,不建议使用该属性

路由、page transition与CSS

问题描述

使用framer-motion为页面过渡添加动画,当路由变化时,页面的样式会瞬间失效,然后开始动画,经排查,发现样式文件会在路由变化开始时,卸载当前的样式,见下图,注意看路由变化时,右侧的style标签变化。同时,该问题早在2020年就有人提出。在生产环境下会出现,在开发环境下不会出现,原因在于开发环境下没有对应的style标签,也就是说,开发环境下的样式添加与生产环境下的样式添加不一样。 如果使用style属性来进行样式添加,则没有该问题,例如styled-component的方式进行样式添加(但不知道stled-component与framer-motion协作效果如何)。

路由变化css样式失效.gif

解决

  • 方案1: GitHub上有人提出了一个hacking的方法如下, 因为受影响的style标签里有media属性,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)
  • 方案2:使用如下方案,在_app.js中调用该Hook即可。经验证,可以解决该bug,首次切换页面也不会出现bug了。
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();
        };
    }, []);
};

同构应用的渲染问题

style内部文本样式由于转义,SSR和CSR不一致导致hydration报错

问题描述

使用react-markdown渲染Markdown文本为HTML时,在使用SSR渲染的情况下,如果有数学公式插件,会导致SSR和CSR渲染的结果不一致,hydration失败的问题。经过排查,发现rehypeMathJaxSvg插件会生成style标签,将文本样式放在style标签里。react-markdown组件会调用React.createElement生成虚拟DOM节点,此时的style标签内部children文本仍然没有问题。但是在服务端使用ReactDOM.renderToString函数时,会将style标签内的文本进行转义"转义为&quot;,而客户端在使用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=&quot;SVG&quot;] {
  direction: ltr;
}

mjx-container[jax=&quot;SVG&quot;] &gt; svg {
  overflow: visible;
  min-height: 1px;
  min-width: 1px;
}

mjx-container[jax=&quot;SVG&quot;] &gt; svg a {
  fill: blue;
  stroke: blue;
}

mjx-container[jax=&quot;SVG&quot;][display=&quot;true&quot;] {
  display: block;
  text-align: center;
  margin: 1em 0;
}

mjx-container[jax=&quot;SVG&quot;][display=&quot;true&quot;][width=&quot;full&quot;] {
  display: flex;
}

mjx-container[jax=&quot;SVG&quot;][justify=&quot;left&quot;] {
  text-align: left;
}

mjx-container[jax=&quot;SVG&quot;][justify=&quot;right&quot;] {
  text-align: right;
}

g[data-mml-node=&quot;merror&quot;] &gt; g {
  fill: red;
  stroke: red;
}

g[data-mml-node=&quot;merror&quot;] &gt; rect[data-background] {
  fill: yellow;
  stroke: none;
}

g[data-mml-node=&quot;mtable&quot;] &gt; line[data-line], svg[data-table] &gt; g &gt; line[data-line] {
  stroke-width: 70px;
  fill: none;
}

g[data-mml-node=&quot;mtable&quot;] &gt; rect[data-frame], svg[data-table] &gt; g &gt; rect[data-frame] {
  stroke-width: 70px;
  fill: none;
}

g[data-mml-node=&quot;mtable&quot;] &gt; .mjx-dashed, svg[data-table] &gt; g &gt; .mjx-dashed {
  stroke-dasharray: 140;
}

g[data-mml-node=&quot;mtable&quot;] &gt; .mjx-dotted, svg[data-table] &gt; g &gt; .mjx-dotted {
  stroke-linecap: round;
  stroke-dasharray: 0,140;
}

g[data-mml-node=&quot;mtable&quot;] &gt; g &gt; svg {
  overflow: visible;
}

[jax=&quot;SVG&quot;] mjx-tool {
  display: inline-block;
  position: relative;
  width: 0;
  height: 0;
}

[jax=&quot;SVG&quot;] mjx-tool &gt; mjx-tip {
  position: absolute;
  top: 0;
  left: 0;
}

mjx-tool &gt; 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=&quot;maction&quot;][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=&quot;SVG&quot;] path[data-c], mjx-container[jax=&quot;SVG&quot;] 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=&quot;SVG&quot;] {
            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'] }] ]}
/>
留言板
0