Imperceptibly already in July 2021, beautiful boy still useless rise, still continue to lie flat. There have been a lot of articles written about VUE3, but I haven’t used it in any of my projects.

Idle, suddenly want to get a VUE3 + SSR + Vite demo, to do a simple project combat. The server intends to use egg(not started, updated in a later section).

Vue3 +vite+ SSR, nuxT does not support vue3+ Vite + SSR. Fortunately, I found vue3+ Vite + SSR demo in the Github repository of Vite.

Open the demo, the general logic is almost written, but there is no important router+vuex, then we fill up. First of all, let’s take a look at the project catalog after MY transformation

SRC | - pages | -- - About. Vue | -- Home. Vue | - utils | -- - index. Js | -- App. Vue | - entry - client. Js | - entry - server. Js | - main. Js | - router. Js | -- store. Js index. The HTML pageage. Json for server js vite. Config. JsCopy the code

server.js

const express = require('express')
const fs = require('fs')
const path = require('path')
const isTest = process.env.NODE_ENV === 'test'| |!!!!! process.env.VITE_TEST_BUILDconst serialize = require('serialize-javascript');
async function createServer(
  root = process.cwd(),
  isProd = process.env.NODE_ENV === 'production'
) {
  const resolve = (p) = > path.resolve(__dirname, p)

  const indexProd = isProd
    ? fs.readFileSync(resolve('dist/client/index.html'), 'utf-8')
    : ' '

  const manifest = isProd
    ? // @ts-ignore
    require('./dist/client/ssr-manifest.json')
    : {}

  const app = express()

  / * * *@type {import('vite').ViteDevServer}* /
  let vite
  if(! isProd) { vite =await require('vite').createServer({
      root,
      logLevel: isTest ? 'error' : 'info'.server: {
        middlewareMode: 'ssr'.watch: {
          // During tests we edit the files too fast and sometimes chokidar
          // misses change events, so enforce polling for consistency
          usePolling: true.interval: 100}}})// use vite's connect instance as middleware
    app.use(vite.middlewares)
  } else {
    app.use(require('compression')())
    app.use(
      require('serve-static')(resolve('dist/client'), {
        index: false
      })
    )
  }

  app.use(The '*'.async (req, res) => {
    try {
      const url = req.originalUrl
      // Read the index.html template file
      let template, render
      if(! isProd) {// always read fresh template in dev
        template = fs.readFileSync(resolve('index.html'), 'utf-8')
        template = await vite.transformIndexHtml(url, template)
        render = (await vite.ssrLoadModule('/src/entry-server.js')).render
      } else {
        template = indexProd
        render = require('./dist/server/entry-server.js').render
      }
      // Call the server-side rendering method to render the Vue component into a DOM structure, and analyze the js, CSS and other files that need to be preloaded.
      const [appHtml, preloadLinks, store] = await render(url, manifest)
      // Add + to the server prefetch data store, insert HTML template file
      const state = ("<script>window.__INIT_STATE__" + "=" + serialize(store, { isJSON: true }) + "</script>");
      // Replace the stand character in the HTML with the corresponding resource file
      const html = template
        .replace(` <! --preload-links-->`, preloadLinks)
        .replace(` <! --app-html-->`, appHtml)
        .replace(` <! --app-store-->`, state)

      res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
    } catch (e) {
      vite && vite.ssrFixStacktrace(e)
      console.log(e.stack)
      res.status(500).end(e.stack)
    }
  })

  return { app, vite }
}
// Create a node server for SSR
if(! isTest) { createServer().then(({ app }) = >
    app.listen(3000.() = > {
      console.log('http://localhost:3000')}}))// for test use
exports.createServer = createServer
Copy the code

index.html

<! DOCTYPEhtml>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <! --preload-links-->
    <div id="app"><! --app-html--></div>
    <script type="module" src="/src/entry-client.js"></script>
    <! --app-store--><! -- This is used to preload store -->
  </body>
</html>
Copy the code

src/main.ts

Since each request will arrive at the server, in order for the data not to be contaminated with each other, we need to use the factory function to create a new instance for each request, returning a new Vue, Router, Store, etc

import { createSSRApp } from 'vue'
import { createStore } from './store'
import App from './App.vue'
import { createRouter } from './router'

export function createApp() {
  const app = createSSRApp(App)
  const router = createRouter()
  const store = createStore()
  app.use(router)
  app.use(store)
  return { app, router, store }
}

Copy the code

router

import {
  createMemoryHistory,
  createRouter as _createRouter,
  createWebHistory
} from 'vue-router'

// Auto generates routes from vue files under ./pages
// https://vitejs.dev/guide/features.html#glob-import
const pages = import.meta.glob('./pages/*.vue')

const routes = Object.keys(pages).map((path) = > {
  const name = path.match(/\.\/pages(.*)\.vue$/) [1].toLowerCase()
  return {
    path: name === '/home' ? '/' : name,
    component: pages[path] // () => import('./pages/*.vue')}})export function createRouter() {
  return _createRouter({
    // use appropriate history implementation for server/client
    // import.meta.env.SSR is injected by Vite.
    history: import.meta.env.SSR ? createMemoryHistory() : createWebHistory(),
    routes
  })
}
Copy the code

Data prefetching

Server-side rendering is a “snapshot” of the application, and if the application relies on asynchronous data, this data needs to be prefetched and parsed before rendering can begin.

Asynchronously retrieving data

store

import { createStore as _createStore } from 'vuex'

export function createStore() {
  return _createStore({
    state() {
      return {
        count: 0}},mutations: {
      increment(state) {
        state.count++
      },
      init(state, count) {
        state.count = count
      }
    },
    actions: {
      getCount({ commit }) {
        return new Promise(resolve= > {
          setTimeout(() = > {
            console.log('run here');
            commit('init'.Math.random() * 100)
            resolve()
          }, 1000)})}}})}Copy the code

SRC /entry-server.js Server rendering entry function.

import { createApp } from './main'
import { renderToString } from '@vue/server-renderer'

import { getAsyncData } from './utils/';  // when processing data asynchronously
export async function render(url, manifest) {
  const { app, router, store } = createApp()

  // set the router to the desired URL before rendering
  router.push(url)
  // store.$setSsrPath(url);
  await router.isReady()
  await getAsyncData(router, store, true);

  // passing SSR context object which will be available via useSSRContext()
  // @vitejs/plugin-vue injects code into a component's setup() that registers
  // itself on ctx.modules. After the render, ctx.modules would contain all the
  // components that have been instantiated during this render call.
  const ctx = {}
  const html = await renderToString(app, ctx)
  ctx.state = store.state
  // the SSR manifest generated by Vite contains module -> chunk/asset mapping
  // which we can then use to determine what files need to be preloaded for this
  // request.
  const preloadLinks = ctx.modules
  ? renderPreloadLinks(ctx.modules, manifest)
  : [];
  return [html, preloadLinks, store]
}

function renderPreloadLinks(modules, manifest) {
  let links = ' '
  const seen = new Set()
  modules.forEach((id) = > {
    const files = manifest[id]
    if (files) {
      files.forEach((file) = > {
        if(! seen.has(file)) { seen.add(file) links += renderPreloadLink(file) } }) } })return links
}

function renderPreloadLink(file) {
  if (file.endsWith('.js')) {
    return `<link rel="modulepreload" crossorigin href="${file}"> `
  } else if (file.endsWith('.css')) {
    return `<link rel="stylesheet" href="${file}"> `
  } else {
    // TODO
    return ' '}}Copy the code

src/utils/index.js

Execute the register Store hook
export const registerModules = (components, router, store) = > {
  return components
    .filter((i) = > typeof i.registerModule === "function")
    .forEach((component) = > {
      component.registerModule({ router: router.currentRoute, store });
    });
};

// Call the asyncData hook of the currently matched component to prefetch data
export const prefetchData = (components, router, store) = > {
  const asyncDatas = components.filter(
    (i) = > typeof i.asyncData === "function"
  );
  return Promise.all(
    asyncDatas.map((i) = > {
      return i.asyncData({ router: router.currentRoute.value, store }); })); };// SSR custom hooks
export const getAsyncData = (router, store, isServer) = > {
  return new Promise(async (resolve) => {
    const { matched } = router.currentRoute.value;

    // The component to which the current route matches
    const components = matched.map((i) = > {
      return i.components.default;
    });
    // Dynamically register store
    registerModules(components, router, store);

    if (isServer) {
      // Prefetch data
      await prefetchData(components, router, store);
    }

    resolve();
  });
};
Copy the code

Data prefetch logic in the component,/src/page/Home.vueServer data prefetch,

export default {
  asyncData({store}) {
    return store.dispatch('getCount')}})Copy the code

src/client.js

Client entry functions that store should get state before the client is mounted to the application

import { createApp } from './main'

const { app, router, store } = createApp()
if(window.__INIT_STATE__) {
  // When template is used, context.state is automatically embedded in the final HTML as the window.__init_state__ state
  // Store should get the state before the client is mounted to the application:

  store.replaceState(window.__INIT_STATE__._state.data)
}
router.isReady().then(() = > {
  app.mount('#app')})Copy the code

Finally, attach package.json

  "scripts": {
    "dev": "node server"."build": "npm run build:client && npm run build:server"."build:client": "vite build --ssrManifest --outDir dist/client"."build:server": "vite build --ssr src/entry-server.js --outDir dist/server"."generate": "vite build --ssrManifest --outDir dist/static && yarn build:server && node prerender"."serve": "cross-env NODE_ENV=production node server"."debug": "node --inspect-brk server"
  },
  "dependencies": {
    "vue": "^ 3.1.2." "."vue-router": "^ 4.0.10"."vuex": "^ 4.0.2." "
  },
  "devDependencies": {
    "@vitejs/plugin-vue": ^ "1.2.3"."@vitejs/plugin-vue-jsx": "^ 1.1.6." "."@vue/compiler-sfc": "^ 3.0.5"."@vue/server-renderer": "^ 3.1.2." "."express": "^ 4.17.1"."sass": "^ 1.35.1"."sass-loader": "^ 12.1.0"."serialize-javascript": "^ 6.0.0"."vite": "^ 2.3.8." "
  }
Copy the code

When you are done, run

npm run dev
Copy the code

Open http://localhhost:3000 and you should see the following screen

Some potholes I’ve stepped on

  1. Pageage. json must be the same as @vue/server-renderer
  2. @vitejs/plugin-vue-jsx remember to install this, otherwise an error will be reported
  3. [email protected] is norouter.getMatchedComponents()This method, however, can be usedrouter.currentRoute.valueTo take the place of
  4. I haven’t found anything yet.vueThe file with<script setup>Let me write it like thisasyncData, or only
export default defineComponent({
  setup() {},
  asyncData({store}) {
    return store.dispatch('getCount')}Copy the code

The resources

Play, use vite, do vue3.0 server render (SSR)