preface
Vue2 SSR has been a lot of big guys talk bad, vue3 SSR in addition to the official website almost did not search to other articles, and the official website did not speak vuEX, so I will come to a number, I believe we all know what SSR is, past life is not much to say, Here mainly combined with the official website vue3 SSR configuration and use of the process and pit to exclude you.
Rely on
First install the dependencies by executing the following command
NPM install webpack-manifest-plugin webpack-node-externals webpack NPM install webpack-manifest-plugin webpack-node-externals webpack lru-cache -DCopy the code
NPM install Express // Starts the Node service packageCopy the code
NPM install cross-env -d // Install cross-env -d // build:server "Set SSR=1 && vue-cli-service build --dest dist/server" Build :client: vue-cli-service build --dest dist/client", build:server": "cross-env SSR=1 vue-cli-service build --dest dist/server", "build:ssr": "npm run build:client && npm run build:server",Copy the code
routing
Each request requires a clean routing instance, so provide a routing factory function. For example the following code
import { createRouter, RouteRecordRaw } from 'vue-router'; import Home from '.. /views/Home/index.vue'; export const routes: Array<RouteRecordRaw> = [ { path: '/', name: 'Home', component: Home, meta: { // keepAlive: true, depth: 1 } }, { path: '/popular-now/:link', name: 'PopularNow', component: () => import(/* webpackChunkName: "About" * / '.. / views/PopularNow/index. The vue '), props: true, meta: {title: 'I have been don't understand why the earth to', / / keepAlive: true, the depth: 1 } }, { path: '/subscription', name: 'Subscription', component: () => import(/* webpackChunkName: "About" * / '.. / views/Subscription/index. The vue '), meta: {title: 'moves around the sun, / / keepAlive: true, the depth: 1}}, {path: '/mediaLibrary', name: 'MediaLibrary', component: () => import(/* webpackChunkName: "About" * / '.. / views/MediaLibrary/index. The vue '), meta: {title: 'until I met you, the depth: 1}}, {path:'/history, name: 'History', component: () => import(/* webpackChunkName: "about" */ '../views/History/index.vue'), meta: { title: }}, {path: '/search', name: 'search', Component: () => import(/* webpackChunkName: "about" */ '../views/Search/index.vue'), props: route => ({ keywords: route.query.q }), meta: { title: 'Maybe it's because of you ', // keepAlive: true, depth: 1}}, {path: '/watch/:mvid', name:' watch ', Component: () => import(/* webpackChunkName: "about" */ '../views/Watch/index.vue'), props: true, meta: {title: 'This is the world ', depth: 2 } }, { path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import(/* webpackChunkName: "About" * / '.. / views/Notfound/index. The vue '), meta: {title: spring, summer, autumn and winter, the depth: 2}}] export default function (history: Return createRouter({history, routes})}Copy the code
// keepAlive: True keepAlive is commented out. SSR does not support <keep-alive> component caching. If you had <router-view V-slot ="{Component}"> <keep-alive> < Component :class="[{'router-paper-full': ! isShowSidebar }, 'router-paper']" :is="Component" v-if="$route.meta.keepAlive" :key="$route.name" /> </keep-alive> <component :class="[{ 'router-paper-full': !isShowSidebar }, 'router-paper']" :is="Component" v-if="! $route.meta. KeepAlive "/> </router-view> To support SSR, disable <keep-alive> code blockCopy the code
SSR also does not support instructions, so if you use instructions, please remove them and use util mode, for example:
// util. Ts /** */ const loadScrollEvent = function (el: HTMLElement, binding: any) { window.addEventListener( "scroll", function () { const temp: any = el.lastElementChild? .previousElementSibling; if (temp) { const a = temp.offsetTop + temp.offsetHeight; / / the current element the second child (pour first RotateLoading) from top of the parent element + const element height b = document. The documentElement. ClientHeight + (el) parentElement? .parentElement? .scrollTop || 0); / / visual area window height + rolling distance if (binding.value.com mandAutoLoading. IsCommand && a - 100 < = b) {binding. Value. AutoLoading (); } } }, true ); } loadScrollEvent(this. limit refs.commentList as any, {value: {commandAutoLoading:) this.commentListAutoLoading, autoLoading: this.commentListLoading, }, });Copy the code
vuex
Still provide a factory function, for example:
import { createStore } from 'vuex'; import HistoryMv from '.. /store/modules/HistoryMv'; import Subscription from '.. /store/modules/Subscription'; import LikedMv from '.. /store/modules/LikedMv'; import TopProgressBar from '.. /store/modules/TopProgressBar'; import HomeMv from '.. /store/modules/HomeMv'; import PopularNowMv from '.. /store/modules/PopularNowMv'; import CloudSearchMv from '.. /store/modules/CloudSearchMv'; import WatchMv from '.. /store/modules/WatchMv'; Export the default function () {/ / must be reset all the data, or to share these namespace state under HistoryMv. State. HistoryMvList = []; Subscription.state.artistList = []; LikedMv.state.likedMvList = []; TopProgressBar.state.start = false; HomeMv.state.mvList = []; PopularNowMv.state.mvList = []; CloudSearchMv.state.mvList = []; WatchMv.state = { simiMvList: [], artistMvList: [], likedCount: 0, mv: null, artistDetail: null, commentMv: { hotComments: [], comments: [], total: 0, more: false, }, mvUrl: '', mvUrlErrorMsg: '' } const store = createStore({ modules: { HistoryMv, Subscription, LikedMv, TopProgressBar, HomeMv, PopularNowMv, CloudSearchMv, WatchMv } }) try { if (window && (window as any).__INITIAL_STATE__) { store.replaceState((window as any).__INITIAL_STATE__); }} Catch (error) {console.log(' there is an error ', error.message); } return store; }Copy the code
Note that ~ ~ ~
If you use vuex namespaces, be sure to reset all data, otherwise the state in those namespaces will be shared!
The factory function retrieves the store data from the node server (Window as any).initial_state.
entry-client.js
Client entry, do what you need to do, for example:
import { createSSRApp } from 'vue' import { createWebHistory } from 'vue-router' import App from './App.vue' import createRouter from './router-ssr' import createStore from './store-ssr' import { filters } from './share/filters'; import './assets/fonts/iconfont/iconfont.css'; import './assets/fonts/iconfont/iconfont.js'; // The client startup logic...... Const app = createSSRApp(app) const Router = createRouter(createWebHistory('loutube')) // createStore() app.config.globalProperties.$filters = filters; app.use(router).use(store) router.isReady().then(() => { app.mount('#app') })Copy the code
entry-server.js
Server entry, for example:
import { createSSRApp } from 'vue' import { createMemoryHistory } from 'vue-router' import App from './App.vue' import createRouter from './router-ssr' import createStore from './store-ssr' import { filters } from './share/filters'; import './assets/fonts/iconfont/iconfont.css'; // import './assets/fonts/iconfont/iconfont.js'; Make sure to comment it out, the server does not need the font js file, importing it will cause an error such as window or document cannot be found. export default function () { const app = createSSRApp(App) const router = createRouter(createMemoryHistory('loutube')) / / said to the history source of const store = createStore () app. Config. GlobalProperties. $filters = filters; app.use(router).use(store) return { app, router, store } }Copy the code
vue.config.js
Package configuration files, for example:
const { WebpackManifestPlugin } = require('webpack-manifest-plugin') const nodeExternals = require('webpack-node-externals') const webpack = require('webpack') // const emptyWebpackBuildDetailPlugin = Require ("empty-webpack-build-detail-plugin") const path = require('path') if (process.env.COM) {// if not SSR run mode Return console.log(' Exit vue.config.js -----'); } module.exports = { chainWebpack: WebpackConfig => {// We need to disable cache loader, Otherwise the client will build from the server build using a cache components of webpackConfig. The module. The rule (' vue). USES. Delete (' cache - loader) webpackConfig.module.rule('js').uses.delete('cache-loader') webpackConfig.module.rule('ts').uses.delete('cache-loader') webpackConfig.module.rule('tsx').uses.delete('cache-loader') if (! Process.env.ssr) {// Point the entry to the application's client entry file webPackConfig.entry ('app').clear().add('./ SRC /entry-client.js') return} // Point the entry to the application's server entry file webPackConfig.entry ('app').clear().add('./ SRC /entry-server.js') // This allows WebPack to handle dynamic imports in a way appropriate for Node, // It also tells the vue-loader to throw server-side code when compiling the VUE component. WebpackConfig. Target (' node ') / / this tells packages from the server using the style of export webpackConfig. Output. LibraryTarget webpackConfig (' commonjs2 ') .plugin('manifest') .use(new WebpackManifestPlugin({ fileName: 'ssr-manifest.json' })) // https://webpack.js.org/configuration/externals/#function // HTTP: / / https://github.com/liady/webpack-node-externals / / application will depend on external expansion. // This makes server builds faster and generates smaller package files. // Do not make dependencies that need to be handled by Webpack external extensions // Also whitelist dependencies that modify 'global' (e.g., various polyfills) webPackconfig. externals(nodeExternals({ allowlist: /\.(css|vue)$/ })) webpackConfig.optimization.splitChunks(false).minimize(false) webpackConfig.plugins.delete('preload') webpackConfig.plugins.delete('prefetch') webpackConfig.plugins.delete('progress') webpackConfig.plugins.delete('friendly-errors') webpackConfig.plugin('limit').use( new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }) ) // webpackConfig.plugin('emptyWebpackBuildDetailPlugin').use( // emptyWebpackBuildDetailPlugin, [{ // path: path.join(process.cwd(), 'log'), // filename: 'compile-log.md' // }] // ) } }Copy the code
Note that ~ ~ ~
The empty-webpack-build-detail-plugin can be used to log errors if they are not displayed during the packaging process. For example, if you insist on using the instructions in an SSR project, it will give you no error messages and no feedback at all. ╮ (╯ del ╰) ╭
homogeneous
// server.js const path = require('path') const express = require('express') const fs = require('fs') const { renderToString } = require('@vue/server-renderer') const manifest = require('.. /dist/server/ssr-manifest.json') const server = express() const appPath = path.join(__dirname, '.. /dist', 'server', manifest['app.js']) const createApp = require(appPath).default server.use('/img', express.static(path.join(__dirname, '.. /dist/client', 'img'))) server.use('/js', express.static(path.join(__dirname, '.. /dist/client', 'js'))) server.use('/css', express.static(path.join(__dirname, '.. /dist/client', 'css'))) server.use( '/favicon.ico', express.static(path.join(__dirname, '.. /dist/client', 'favicon. Ico '))) // const LRU = require('lru-cache') const microCache = new LRU({Max: 100, maxAge: 1000 * 60 * 60 }) server.get('*', async (req, res) => { const hit = microCache.get(req.url) if (hit) { return res.send(hit) } const { app, router, store } = createApp() await router.push(req.url.replace('/loutube', '')) await router.isReady() const matchedComponents = router.currentRoute.value.matched; Promise.all(matchedComponents. Map (Component => {if) (Component.components.default.methods.asyncData) { return Component.components.default.methods.asyncData( store, router.currentRoute ); } })).then(async () => { const appContent = await renderToString(app); fs.readFile(path.join(__dirname, '.. /dist/client/index.html'), (err, html) => { if (err) { throw err } html = html .toString() .replace('<div id="app">', `<div id="app">${appContent}`) .replace( '</script>', `</script><script type="application/javascript">window.__INITIAL_STATE__=${JSON.stringify(store.state)}</script>` ) res.setHeader('Content-Type', 'text/html') res.send(html) microCache.set(req.url, html) }) }).catch(error => { console.error(error); res.sendStatus(500) }); }) server.listen(8080)Copy the code
Note that ~ ~ ~
I’m going to take the initiative to put the store in the HTML
.replace(
'</script>',
`</script><script type="application/javascript">window.__INITIAL_STATE__=${JSON.stringify(store.state)}</script>`
)
Copy the code
Component asyncData ()
AsyncData (store: any, route: any) { return store.dispatch("HomeMv/getMvListRequest", { limit: 24, loadMoreCount: 0, }); } // If the business is more complex, you might want to write asyncData(store: any, route: any) { return Promise.all([ this.getMvUrl(store, route), this.getMvLikedCount(store, route), this.getMvDetail(store, route).then(async () => { await this.getArtistDetail(store, route); await this.getArtistMvList(store, route); await store.dispatch("HistoryMv/addHistoryMv", store.state.WatchMv.mv); }), this.getCommentMvList(store, route), ]).then(); } // List one of the functions used above async getMvUrl(store? : any, route? : any) { store = store || this.$store; GetMvUrl const mvid = this.mvUrlState; this.mvUrlState.queryParams.mvid : route.value.params.mvid; await store.dispatch("WatchMv/getMvUrlRequest", { id: mvid, }); }Copy the code
You need to study the part of promise by yourself, which is not yet mastered, and the qualification of the inner volume is not… ╮ (╯ del ╰) ╭
The last
Maybe your file path is not the same as the example code I show, please check carefully… Yeah, I’m listing my script
"scripts": {
"serve": "cross-env COMMON=1 vue-cli-service serve",
"build": "cross-env COMMON=1 vue-cli-service build",
"lint": "vue-cli-service lint",
"build:client": "vue-cli-service build --dest dist/client",
"build:server": "cross-env SSR=1 vue-cli-service build --dest dist/server",
"build:ssr": "npm run build:client && npm run build:server",
"dev:serve": "cross-env WEBPACK_TARGET=node node ./server/server.js",
"dev": "concurrently \"npm run serve\" \"npm run dev:serve\" "
},
Copy the code
So let’s use the command package
npm run build:ssr
Copy the code
Run the packaged file through Node
node server.js
Copy the code