(1) Vue3 + SSR + Vite

(1) Vue3 + SSR + Vite

Unknowingly, it's already July 2021, and pretty boys are still useless to rise, and they continue to lie down. However, there are endless articles about vue3 from the big guys, and I have not used it in the project.

Having nothing to do, I suddenly wanted to get a demo of vue3+ssr+vite to make a simple actual project. The server intends to use egg (not started, the subsequent chapters will be updated).

1. of course, I looked at the documentation, but I didn't find the vue3+vite+ssr documentation. All I found were vue2. Nuxt doesn't seem to support the vue3+vite+ssr approach. Fortunately, there is no unparalleled road, I found the demo of vue3+vite+ssr in the github repository of vite .

Open the demo, the general logic is basically written, but I don't see the important router+vuex , then we will add them one by one. First of all, let s take a look at the project directory 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.html pageage.json server.js vite.config.js Copy 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_BUILD const 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} */ the 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( '*' , async (req, res) => { try { const url = req.originalUrl //read 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, 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) //New + add the store where the server-side prefetch data is inserted into the html template file const state = ( "<script>window.__INIT_STATE__" + "=" + serialize(store, { isJSON : true }) + "</script>" ); //Replace the booth symbol in 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 as ssr if (!isTest) { createServer().then( ( {app} ) => app.listen( 3000 , () => { console .log( 'http://localhost:3000' ) }) ) } Use for the Test// Exports .createServer = createServer copy the code

index.html

<!DOCTYPE html > < 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 store the preloaded -> </body > </HTML > copy the code

src/main.ts

Because every request will reach the server, in order to prevent the data from being contaminated with each other, we need to use the factory function to create a new instance every time we request, and every time we return a brand new vue, router, store, etc.

Import {createSSRApp} from 'VUE' Import {createstore} from './store' Import the 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 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 code

Data prefetching

The server-side rendering is a "snapshot" of the application. If the application relies on some asynchronous data, the data needs to be prefetched and parsed before starting to render .

Get data asynchronously

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 code

src/entry-server.js server-side rendering entry function.

import {createApp} from './main' import {renderToString} from '@vue/server-renderer' import {getAsyncData} from './utils/' ; //Use export async function render ( url, manifest ) { const {app, router, store} = createApp() when processing data asynchronously //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 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 in the currently matched component to prefetch the 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 hook export const getAsyncData = ( router, store, isServer ) => { return new Promise ( async (resolve) => { const {matched} = router.currentRoute.value; //Components matched by the current route 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 code

Data prefetching logic in the component, /src/page/Home.vue server data prefetching,

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

src/client.js

Client entry function, the store should get the state before the client is mounted to the application

import {createApp} from './main' const {app, router, store} = createApp() if ( window .__INIT_STATE__) { //When using template, context.state will be automatically embedded into the final HTML as window.__INIT_STATE__ state //Hang on the client Before loading into the application, the store should get the status: store.replaceState( window .__INIT_STATE__._state.data) } router.isReady().then( () => { app.mount( '#app' ) }) Copy 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 code

When you have done the above steps, run

npm run dev copy the code

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

Some pits stepped on

  1. The version of pageage.json and @vue/server-renderer must be the same
  2. @vitejs/plugin-vue-jsx Remember to install this, otherwise an error will be reported
  3. Not in vue-router@4.xx
    router.getMatchedComponents()
    This method, but you can use
    router.currentRoute.value
    Instead of
  4. Not found in
    .vue
    File under use
    <script setup>
    To write
    asyncData
    , Still can only be used
export default defineComponent({ setup () {}, asyncData ( {store} ) { return store.dispatch( 'getCount' ) } Copy code

Reference

Play, use vite, do vue3.0 server-side rendering (ssr)