Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Next.js 多页应用的设计与实现 #44

Open
xiaoxiaojx opened this issue Oct 9, 2022 · 0 comments
Open

Next.js 多页应用的设计与实现 #44

xiaoxiaojx opened this issue Oct 9, 2022 · 0 comments
Labels
SSR Server-Side Rendering

Comments

@xiaoxiaojx
Copy link
Owner

xiaoxiaojx commented Oct 9, 2022

image

Next 多页介绍

Next.js 是约定式路由, 如果你的 pages 目录是下面这样

├── pages 
│   ├── index.tsx
│   ├── blog
│   │   └── first-post.tsx
│   │   └── index.tsx

那么将得到 3 个页面, 开发环境可通过如下链接去访问

Next 多页实现

实际上 Next.js 生成的 webpackConfig.entry 并不是如下这样简单

// webpackConfig.entry

{
  "index": "./pages/index.tsx",
  "blogIndex": "./pages/blog/index.tsx",
  "blogFirstPost": "./pages/blog/first-post.tsx"
}

而是具有一定的依赖关系 depenOn 属性的对象, 并且入口文件 pages/** 的内容还会被 next-client-pages-loader 代理修改
image

webpackConfig.entry.{xxx}.dependOn 表示当前入口文件依赖的入口文件, 必须在加载此入口文件之前加载它们。

根据 depenOn 的解释, 我们知道了 3 个页面实际都是依赖于 next/dist/client/next.js 先行执行, 然后再执行 pages/_app.tsx, 最后才是执行页面的 pages/**/.tsx

如页面 "./pages/blog/index.tsx" 被 next-client-pages-loader 代理修改后, 打包的入口内容变成了如下。所做的工作只是在 window.__NEXT_P 中 push 了一个数组, 相当于只进行了一个模块的注册

(window.__NEXT_P = window.__NEXT_P || []).push([
      "/blog",
      function () {
        return require("private-next-pages/blog/index.tsx");
      }
    ]);
    if(module.hot) {
      module.hot.dispose(function () {
        window.__NEXT_P.push(["/blog"])
      });
    }

到这里知道了 Next.js 多页应用的入口文件并非是最先执行的 js 代码, 而 next/dist/client/next.js 才是客户端执行的入口, 一切运行逻辑由它进行调度与初始化操作, Next.js 相当于很好的为多页应用制造了一个 CommonEntry 来存放公共逻辑。

Next SPA ?

既然 Next.js 是多页应用, 那么为什么通过 Link 组件能在页面不刷新的情况下从 Home 页面跳转到 Blog list 页面了, 完全类似于 SPA 的用户体验?

import Link from 'next/link'

function Home() {
  return (
    <ul>
      <li>
        <Link href="/">
          <a>Home</a>
        </Link>
      </li>
      <li>
        <Link href="/blog">
          <a>Blog list</a>
        </Link>
      </li>
      <li>
        <Link href="/blog/first-post">
          <a>Blog first-post</a>
        </Link>
      </li>
    </ul>
  )
}

export default Home

其实 Next.js 这里是类似于微前端的实现, 把多个页面给聚合在了一个容器 AppContainer 里面。比如在 Blog list 页面点击 Blog first-post, 此时会去通过动态创建一个 script 加载 pages/blog/first-post.js, script load 后拿到 js 文件的导出的 App 组件再渲染到页面

// next/client/index.tsx

function AppContainer({
  children,
}: React.PropsWithChildren<{}>): React.ReactElement {
  return (
    <Container
      fn={(error) =>
        renderError({ App: CachedApp, err: error }).catch((err) =>
          console.error('Error rendering page: ', err)
        )
      }
    >
      <RouterContext.Provider value={makePublicRouterInstance(router)}>
        <HeadManagerContext.Provider value={headManager}>
          <ImageConfigContext.Provider
            value={process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete}
          >
            {children}
          </ImageConfigContext.Provider>
        </HeadManagerContext.Provider>
      </RouterContext.Provider>
    </Container>
  )
}

如下的调用栈可以看到 Next.js 的渲染过程。
image

那么 Next.js 如何通过 /blog/first-post 这个页面地址就能准确找到对应的 first-post.js 的链接地址, 如果是生产环境则是带了 hash 的 first-post.{hash}.js 又该如何找到了?

真相是在构建时 Next.js 生成一份 _buildManifest.js, 里面携带了本次构建产物的信息, 功能类似于我们常见的 asset-manifest.json

image

不得不说 Next.js 在一个独立的应用中都能想到实现成微前端的模样 ~

Next 为何这样设计

SSR 中对于 SPA 应用, 多个子路由均不使用动态 import 而采用 require 或者静态 import 倒是无需额外兼容, 但为了性能考虑, 作为有追求的技术人显然要保留动态 import 来进行懒加载。

import Loadable from 'react-loadable'

const load = (loader: any) =>
  Loadable({
    loader,
    loading: Loading
  })

const routes = [
  {
    path: '/list',
    component: load(() => import('./list'))
  },
  {
    path: '/detail',
    component: load(() => import('./detail'))
  },
  ...
]

那么你将面临如下的问题

  • Node 直出时子路由如 /list 只会渲染出 Loading 组件
  • 即使 Node 端顺利直出了, 到了客户端渲染又会发现 list 组件缺少了样式, 因为动态 import 本身就是非首屏的异步加载。

所以 Next.js 未支持 SPA 多路由应用也算规避了这个问题, 亦或许是 Next.js 认为 SSR 场景下干脆就不需要 SPA 的概念了, 一切皆独立的页面。

那么有不修改源码的情况下解决这个问题么?

当然可以, 写一个 webpack plugin 对动态 import 语法的行为进行控制, 对于首屏的 cssChunk 进行重新分配即可, 这部分内容比较多以后有机会再介绍吧 ~

@xiaoxiaojx xiaoxiaojx added the SSR Server-Side Rendering label Oct 9, 2022
@xiaoxiaojx xiaoxiaojx changed the title Next.js 多页的设计与实现 Next.js 多页应用的设计与实现 Oct 9, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
SSR Server-Side Rendering
Projects
None yet
Development

No branches or pull requests

1 participant