frandiox / vite-ssr

Use Vite for server side rendering in Node
MIT License
823 stars 91 forks source link

How to use NaiveUI with vite-ssr? #168

Open yuriifabirovskyi opened 2 years ago

yuriifabirovskyi commented 2 years ago

NaiveUI provide a good example of how to use it with ssr: https://github.com/07akioni/naive-ui-vite-ssr . The algorithm is as follows: first you need to collect all css from project and then inject its in the index.html file on the server. But I can't figure out how to do it with vite-ssr.

SkyleLai commented 2 years ago

This is my workaround:

I modify entry-server.js and html.js in vite-ssr (commented // changed), and use postinstall script to replace it after yarn/npm install

package.json

"scripts": {
  "postinstall": "cp ./src/utils/entry-server.js ./node_modules/vite-ssr/vue/entry-server.js && cp ./src/utils/html.js ./node_modules/vite-ssr/utils/html.js"
}

./src/utils/entry-server.js

import { createApp } from 'vue'
import { renderToString } from '@vue/server-renderer'
import { setup } from '@css-render/vue3-ssr' // changed
import { createRouter, createMemoryHistory } from 'vue-router'
import { getFullPath, withoutSuffix } from '../utils/route'
import { addPagePropsGetterToRoutes } from './utils'
import { renderHeadToString } from '@vueuse/head'
import coreViteSSR from '../core/entry-server.js'
import { provideContext } from './components.js'
export { ClientOnly, useContext } from './components.js'
export const viteSSR = function viteSSR(
  App,
  {
    routes,
    base,
    routerOptions = {},
    pageProps = { passToPage: true },
    ...options
  },
  hook
) {
  if (pageProps && pageProps.passToPage) {
    addPagePropsGetterToRoutes(routes)
  }
  return coreViteSSR(options, async (context, { isRedirect, ...extra }) => {
    const app = createApp(App)
    const { collect } = setup(app) // changed
    const routeBase = base && withoutSuffix(base(context), '/')
    const router = createRouter({
      ...routerOptions,
      history: createMemoryHistory(routeBase),
      routes: routes,
    })
    router.beforeEach((to) => {
      to.meta.state = extra.initialState || null
    })
    provideContext(app, context)
    const fullPath = getFullPath(context.url, routeBase)
    const { head } =
      (hook &&
        (await hook({
          app,
          router,
          initialRoute: router.resolve(fullPath),
          ...context,
        }))) ||
      {}
    app.use(router)
    router.push(fullPath)
    await router.isReady()
    if (isRedirect()) return {}
    Object.assign(
      context.initialState || {},
      (router.currentRoute.value.meta || {}).state || {}
    )
    const body = await renderToString(app, context)
    const cssHtml = collect() // changed
    if (isRedirect()) return {}
    const {
      headTags = '',
      htmlAttrs = '',
      bodyAttrs = '',
    } = head ? renderHeadToString(head) : {}
    return { body, cssHtml, headTags, htmlAttrs, bodyAttrs } // changed
  })
}
export default viteSSR

./src/utils/html.js

export function findDependencies(modules, manifest) {
  const files = new Set()
  for (const id of modules || []) {
    for (const file of manifest[id] || []) {
      files.add(file)
    }
  }
  return [...files]
}
export function renderPreloadLinks(files) {
  let link = ''
  for (const file of files || []) {
    if (file.endsWith('.js')) {
      link += `<link rel="modulepreload" crossorigin href="${file}">`
    } else if (file.endsWith('.css')) {
      link += `<link rel="stylesheet" href="${file}">`
    }
  }
  return link
}
// @ts-ignore
const containerId = __CONTAINER_ID__
const containerRE = new RegExp(
  `<div id="${containerId}"([\\s\\w\\-"'=[\\]]*)><\\/div>`
)
export function buildHtmlDocument(
  template,
  { cssHtml, htmlAttrs, bodyAttrs, headTags, body, initialState } // changed
) {
  // @ts-ignore
  if (__DEV__) {
    if (template.indexOf(`id="${containerId}"`) === -1) {
      console.warn(
        `[SSR] Container with id "${containerId}" was not found in index.html`
      )
    }
  }
  if (htmlAttrs) {
    template = template.replace('<html', `<html ${htmlAttrs} `)
  }
  // changed
  if (cssHtml) {
    template = template.replace('<meta name="naive-ui-style-ssr" />', cssHtml)
  }
  if (bodyAttrs) {
    template = template.replace('<body', `<body ${bodyAttrs} `)
  }
  if (headTags) {
    template = template.replace('</head>', `\n${headTags}\n</head>`)
  }
  return template.replace(
    containerRE,
    // Use function parameter here to avoid replacing `$1` in body or initialState.
    // https://github.com/frandiox/vite-ssr/issues/123
    (_, d1) =>
      `<div id="${containerId}" data-server-rendered="true"${d1 || ''}>${
        body || ''
      }</div>\n\n  <script>window.__INITIAL_STATE__=${
        initialState || "'{}'"
      }</script>`
  )
}

./index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta name="naive-ui-style-ssr" />
    <meta name="naive-ui-style" />
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0, maximum-scale=1.0"
    />
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

./src/main.js

import { setup } from '@css-render/vue3-ssr' // unused but needs to import (I don't know why either)

./vite.config.js

import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'

export default {
  plugins: [Components({ resolvers: [NaiveUiResolver()] })],
}