Build your own SSR, Static Site Generation (SSG) and encapsulate the Vue.js component library. Build your own SSR

mkdir vue-ssr cd vue-ssr npm init -y npm i vue vue-server-renderder server.js const Vue = require('vue') const renderer = require('vue-server-renderer').createRenderer() const app = new Vue({ template: ` <div id="app"> <h1>{{message}}</h1> </div> `, data: { message: }) renderer.renderToString(app, (err, HTML) => {if (err) throw err console.log(HTML)}) node server.js <div id="app" data-server-Rendered ="true">< /h1></div> data-server-Rendered ="true" This attribute is for the future client-rendered interface activation

Two, combined to the Web server

server.js const Vue = require('vue') const express = require('express') const renderer = require('vue-server-renderer').createRenderer() const server = express() server.get('/', (req, res) => { const app = new Vue({ template: ` <div id="app"> <h1>{{message}}</h1> </div> `, data: { message: Renderer. rendertoString (app, (err,)) html) => { if (err) { return res.status(500).end('Internal Server Error.') } res.setHeader('Content-Type', 'text/html; Charset =utf8') // Set the encoding to prevent messy res.end(' <! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, Initial -scale=1.0"> <title>Document</title> </head> <body> ${HTML} </body> </ HTML > ')})}) server.listen(3000, () => { console.log('server running at port 3000... ')})

Use HTML templates

  1. Create an HTML template file
<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, Initial-scale =1.0"> </title> </head> <body> <! --vue-ssr-outlet--> </body> </html> <! --vue-ssr-outlet--> is a placeholder for receiving variables that will be rendered in the future. 2. The createRenderer method in the js code specifies that the template file server.js const Vue = require(' Vue ') const express = require('express') const Fs = require('fs') const renderer = require('vue-server-renderer'). CreateRenderer ({// specify the template file template here: fs.readFileSync('./index.template.html', 'utf-8') }) const server = express() server.get('/', (req, res) => { const app = new Vue({ template: ` <div id="app"> <h1>{{message}}</h1> </div> `, data: { message: }}) renderer.renderToString(app, (err, HTML) => {// The HTML parameter here is processed by the template file, If (err) {return res.status(500).end('Internal Server Error.')} res.setHeader(' Content-type '); 'text/html; })}) server.listen(3000, () => {console.log(' Server running at port 3000... ')})

Use external data in templates

Index.template.html <! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, Initial - scale = 1.0 "> {{{meta}}} < title > {{title}} < / title > < / head > < body > <! --vue-ssr-outlet-- --> </body> </ HTML > uses two curly braces to create external data variables, and the tags are also exported to the page. Renderer. rendertoString (app, {title: string); renderer. rendertoString (app, {title: string); renderer.rendertoString (app, {title: string); 'Pull education ', META: '<meta name="description" content=" pull education "}, (err, html) => { if (err) { return res.status(500).end('Internal Server Error.') } res.setHeader('Content-Type', 'text/html; Charset =utf8') // Set the encoding to prevent messy res.end(HTML)})

5. Build configuration

  1. Basic idea 1. PNG
  2. SRC – Components │ ├─ Bass Exercises ─ Foo. Vue │ ├─ Bass Exercises ─ App.Vue Exercises ─ App.js # Universal Entry (Universal Entry) Exercises ─ Entry-Client.js # Runs only in browser, sigma ── Entry-Server.js # Runs only in server app.vue
<template> <div id="app"> <h1>{{message}}</ h2> <div> <input v-model="message"> </div> <div> <button @click="onClick"> </button> </div> </div> </div> </div> export default {name: 'App', data: Function () {return {message: 'click'}}, methods: {onClick () {console.log('Hello World! ') } } } </script>

App.js is the “Universal Entry” for our application. In a pure client-side application, we would create a root Vue instance in this file and mount it directly into the DOM. However, for server-side rendering (SSR), responsibility is transferred to the pure client-side entry file. App.js simply uses export to export a createApp function:

Import Vue from 'Vue' import App from './ app. Vue 'import Vue from '. Export function createApp () {const app = new Vue({// The root instance simply renders the application component. render: H => h(App)}) return {App}} entry-client.js Client Entry simply creates the application and loads it into the DOM: Import {createApp} from './app' // client specific boot logic... Const {app} = createApp() // This assumes that the root element in the app. vue template has' id="app" 'app.$mount('#app') entry-server.js server entry in default Export exports the function and calls it repeatedly on each render. At this point, it doesn't do much more than create and return an application instance -- but this is where we'll perform server-side route matching and data pre-fetching logic later. import { createApp } from './app' export default context => { const { app } = createApp() return app }
  1. Installation dependencies (1) Installation production dependencies

Npm I Vue Vue.js core library Vue-server-Renderer Vue server-side rendering tool Express The Node-based Webpack service framework cross-env uses NPM scripts to set the cross-platform environment variable (2) to install development dependencies

npm i -D webpack webpack-cli webpack-merge webpack-node-externals @babel/core @babel/plugin-transform-runtime @babel/preset-env babel-loader css-loader url-loader file-loader rimraf vue-loader vue-template-compiler The webpack core package webpack-cli webpack command line tool webpack-merge Webpack configuration information merge tool The node module rimraf is based on a cross-platform RM-RF tool that Node encapsulates with friendly-errors-webpack-plugin. The node module rimraf is a node-packaged cross-platform RM-RF tool with friendly-errors-webpack-plugin @babel/ core-@babel /plugin-transform-runtime @babel/preset-env babel-loader vue-loader vue-template-compiler The.vue resource file-loader processes the font resource css-loader processes the CSS resource url-loader processes the image resource

  1. Webpack configuration files and packaging commands (1) Initializes the Webpack packaging configuration file

Build | – webpack. Base. Config. # js common configuration | – webpack. Client. Config. Js pack # client configuration file | – webpack. The server config. Js # The server-side packaging configuration file webpack.base.config.js

/** * public configuration */ const vueLoaderPlugin = require('vue-loader/lib/plugin') const path = require('path') const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin') const resolve = file => path.resolve(__dirname, file) const isProd = process.env.NODE_ENV === 'production' module.exports = { mode: isProd ? 'production' : 'development', output: { path: resolve('.. [name].[chunkhash].js'}, resolve: {alias: {// Path alias, @point SRC '@': resolve('.. ['.js', '.vue', '.json']}, devtool: isProd? 'the source - the map' : 'being - the module - the eval - source - the map', the module: {{rules: [/ / processing image resources test: / \. (PNG | JPG | GIF) $/ I, use: [{loader: 'url - loader, the options: {limit: 8192,},},] and {}, / / handle font resources test: / \. (woff | woff2 | eot | the vera.ttf | otf) $/, use: [' file - loader '],}, {/ / processing. Vue resources test: / \. Vue $/, loader: 'vue-loader'}, // handles CSS resources // It applies to normal '.css 'files // and the' <style> 'block {test: /\.css$/, use: ['vue-style-loader', 'css-loader']}, // CSS preprocessor, reference: https://vue-loader.vuejs.org/zh/guide/pre-processors.html / / / / such as dealing with Less resources {/ / test: / \. Less $/, / / use: [ // 'vue-style-loader', // 'css-loader', // 'less-loader' // ] // }, ] }, plugins: [ new VueLoaderPlugin(), New FriendlyErrorsWebpackPlugin ()]} webpack. Client. Config. Js/client packaging configuration * * * * / const = {merge} require('webpack-merge') const baseConfig = require('./webpack.base.config.js') const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') module.exports = merge(baseConfig, { entry: { app: './ SRC /entry-client.js'}, module: {rules: [// ES6 to ES5 {test: /\.m? Js $/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'], cacheDirectory: true, plugins: ['@babel/ plugin-form-runtime ']}}},]}, // Important: This separates the Webpack runtime into a boot chunk, // so that asynchronous chunk can be injected correctly later. optimization: { splitChunks: { name: "manifest", minChunks: Infinity } }, plugins: [// This plugin generates' vue-ssr-client-manifest.json '.new VuessrClientPlugin ()]}) webpack.server.config.js /** * Server packaging configuration */  const { merge } = require('webpack-merge') const nodeExternals = require('webpack-node-externals') const baseConfig = require('./webpack.base.config.js') const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') Module. exports = merge(BaseConfig, {// Point entry to the application's server entry file entry: './ SRC /entry-server.js', // This allows Webpack to handle module loading in a node-friendly manner // and also when compiling Vue components, // Tell 'vue-loader' to send Server-Oriented code. target: 'node', output: { filename: 'server-bundle.js', // This tells the server bundle to export the Node-style module (Node-style exports) LibraryTarget: 'commonjs2'}, // do not package node_modules, instead keep the require mode and load externals directly: [nodeExternals({// the whitelisted resource is still normally packed with allowList: [/\.css$/] })], plugins: [// This is a plugin that builds the entire output of the server into a single JSON file. // Default file name 'vue-ssr-server-bundle. JSON' new VuessrServerPlugin ()]}) 5. Configure build command "scripts": {"build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js", "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js", "build": "rimraf dist && npm run build:client && npm run build:server" }
  1. Start the application
erver.js

const Vue = require('vue')
const express = require('express')
const fs = require('fs')

const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
const { static } = require('express')
const template = fs.readFileSync('./index.template.html', 'utf-8')
const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle, {
  template,
  clientManifest 
})

const server = express()

// 请求前缀,使用express中间件的static处理
server.use('/dist', express.static('./dist'))

server.get('/', (req, res) => {
  
  renderer.renderToString({
    title: '拉勾教育',
    meta: `
      <meta name="description" content="拉勾教育" >
    `
  }, (err, html) => {
    if (err) {
      return res.status(500).end('Internal Server Error.')
    }
    res.setHeader('Content-Type', 'text/html; charset=utf8') // 设置编码,防止乱码
    res.end(html)
  })
})

server.listen(3001, () => {
  console.log('server running at port 3001...')
})
7. 解析渲染流程
六、构建配置开发模式
1. 基本思路
生产模式直接渲染,开发模式监视打包构建,重新生成Renderer渲染器

2. 提取处理模块
server.js

const Vue = require('vue')
const express = require('express')
const fs = require('fs')
const createBundleRenderer = require('vue-server-renderer')
const setupDevServer = require('./build/setup-dev-server')

const server = express()

// 请求前缀,使用express中间件的static处理
server.use('/dist', express.static('./dist'))

const isProd = process.env.NODE_ENV === 'production'

let renderer
let onReady
if (isProd) {
  const serverBundle = require('./dist/vue-ssr-server-bundle.json')
  const clientManifest = require('./dist/vue-ssr-client-manifest.json')
  const { static } = require('express')
  const template = fs.readFileSync('./index.template.html', 'utf-8')
  renderer = createBundleRenderer(serverBundle, {
    template,
    clientManifest 
  })
} else {
  // 开发模式 -> 监视打包构建 -> 重新生成Renderer渲染器
  onReady = setupDevServer(server, (serverBundle, template, clientManifest) => {
    renderer = createBundleRenderer(serverBundle, {
      template,
      clientManifest 
    })
  })
}

// render 是路由函数
const render = (req, res) => {
  // renderer是Vue SSR的渲染器
  renderer.renderToString({
    title: '拉勾教育',
    meta: `
      <meta name="description" content="拉勾教育" >
    `
  }, (err, html) => {
    if (err) {
      return res.status(500).end('Internal Server Error.')
    }
    res.setHeader('Content-Type', 'text/html; charset=utf8') // 设置编码,防止乱码
    res.end(html)
  })
}

server.get('/', isProd ? render : async (req, res) => {
  // 等待有了Renderer渲染器以后,调用render进行渲染
  await onReady
  render()
})

server.listen(3001, () => {
  console.log('server running at port 3001...')
})
build/setup-dev-server.js

module.exports = (server, callback) => {
  let ready // ready就是promise中的resolve
  const onReady = new Promise(r => ready = r)

  // 监视构建 -> 更新 Renderer

  let template
  let serverBundle
  let clientManifest
  
  return onReady
  
}
3. update更新函数
const update = () => {
  if (template && serverBundle && clientManifest) {
    ready()
    callback(serverBundle, template, clientManifest)
  }
}
4. 处理模板文件
// 监视构建 template -> 调用 update -> 更新 Renderer 渲染器
const templatePath = path.resolve(__dirname, '../index.template.html')
template = fs.readFileSync(templatePath, 'utf-8')
update()
// fs.watch、fs.watchFile
chokidar.watch(templatePath).on('change', () => {
  template = fs.readFileSync(templatePath, 'utf-8')
  update()
})
5. 服务端监视打包
// 监视构建 serverBundle -> 调用 update -> 更新 Renderer 渲染器
const serverConfig = require('./webpack.server.config')
// serverCompiler是一个webpack编译器,直接监听资源改变,进行打包构建
const serverCompiler = webpack(serverConfig)
serverCompiler.watch({}, (err, stats) => {
  if (err) throw err
  if (stats.hasErrors()) return
  serverBundle = JSON.parse(
    fs.readFileSync(resolve('../dist/vue-ssr-server-bundle.json'), 'utf-8')
  )
  console.log(serverBundle)
  update()
})
6. 把数据写到内存中
// 监视构建 serverBundle -> 调用 update -> 更新 Renderer 渲染器
const serverConfig = require('./webpack.server.config')
const serverCompiler = webpack(serverConfig)
const serverDevMiddleware = devMiddleware(serverCompiler, {
  logLevel: 'silent' // 关闭日志输出,由 FriendlyErrorsWebpackPlugin 处理
})
serverCompiler.hooks.done.tap('server', () => {
  serverBundle = JSON.parse(
    serverDevMiddleware.fileSystem.readFileSync(resolve('../dist/vue-ssr-server-bundle.json'), 'utf-8')
  )
  update()
})
7. 客户端构建
// 监视构建 clientManifest -> 调用 update -> 更新 Renderer 渲染器
const clientConfig = require('./webpack.client.config')
clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin())
clientConfig.entry.app = [
  'webpack-hot-middleware/client?quiet=true&reload=true', // 和服务端交互处理热更新一个客户端脚本
  clientConfig.entry.app
]
clientConfig.output.filename = '[name].js' // 热更新模式下确保一致的 hash
const clientCompiler = webpack(clientConfig)
const clientDevMiddleware = devMiddleware(clientCompiler, {
  publicPath: clientConfig.output.publicPath,
  logLevel: 'silent' // 关闭日志输出,由 FriendlyErrorsWebpackPlugin 处理
})
clientCompiler.hooks.done.tap('client', () => {
  clientManifest = JSON.parse(
    clientDevMiddleware.fileSystem.readFileSync(resolve('../dist/vue-ssr-client-manifest.json'), 'utf-8')
  )
  update()
})
8. 热更新
clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin())

clientConfig.entry.app = [
  'webpack-hot-middleware/client?quiet=true&reload=true', // 和服务端交互处理热更新一个客户端脚本
  clientConfig.entry.app
]
clientConfig.output.filename = '[name].js' // 热更新模式下确保一致的 hash

const hotMiddleware = require('webpack-hot-middleware')

server.use(hotMiddleware(clientCompiler, {
  log: false // 关闭它本身的日志输出
}))

Seven, write general application note eight, routing processing

  1. Configure the Vue – the Router Router/index. Js
import Vue from 'vue' import VueRouter from 'vue-router' import Home from '@/src/pages/Home' Vue.use(VueRouter) export Const CreateRouter = () => {const Router = new VuerRouter ({mode: 'history', // Routes: [{path: '/', name: 'home', component: Home }, { path: '/about', name: 'about', component: () => import('@/src/pages/About') }, { path: '*', name: 'error404', Component: () => import('@/ SRC /pages/404')}]}) return router
  1. Register the route to the root instance
App.js /** */ import Vue from 'Vue' import app from './ app.vue 'import {createrOuter} from './router/' // Exports a factory function, Export function createApp () {const router = createRouter() const app = new Vue({ // mount the router to the root Vue instance. // The root instance simply renders the application component. render: h => h(App) }) return { app, router } }
  1. Copy the Entry-Server.js provided on the official website for the server entry
// Entry-Server.js import {createApp} from './app' export default context => {// Because it is possible to route an asynchronous hook function or component, So we'll return a Promise, // so that the server can wait for everything to be ready before rendering. return new Promise((resolve, reject) => { const { app, Router} = createApp() // Set the location of the server-side router router.push(context.url) // Wait until the router has resolved the possible asynchronous components and hook functions router.onReady(() => {const matchedComponents = router. GetMatchedComponents () / / can't match routing, perform reject function, and returns the if 404 (! matchedComponents.length) { return reject({ code: 404})} // Promise should resolve the application instance so that it can render resolve(app)}, reject)})})}. Then change the Promise to async/await, resulting in the following: // Entry-Server.js import {createApp} from './app' export default async context => {// So we'll return a Promise, // so that the server can wait for everything to be ready before rendering. const { app, Router} = createApp() // Set the location of the server-side router router.push(context.url) // Wait until the router has processed "await new" from the possible asynchronous components and hook functions Promise(router.onReady.bind(router)) return app }
  1. Our server code uses a * handler that accepts arbitrary URLs. This allows us to pass the URL we access to our Vue application, and then reuse the same routing configuration for both the client and the server!

Server. Js processing

/ /... // render is the routing function const render =async (req) Res) => {// renderer is the Vue SSR try {const HTML = await renderer. rendertoString ({title: 'pull education ', meta: '<meta name="description" content=" click "> ', url: req.url}) res.setHeader(' content-type ', 'text/ HTML; End (HTML)}catch(err) {res.status(500).end('Internal Server Error.')}} Get ('*', isProd? Render: async (req, res) => {// await await onReady render(req, res)}) //...
  1. Note that you still need to call Router. OnReady before you mount the app, as the router must resolve the asynchronous component in the routing configuration ahead of time in order to properly invoke any routing hooks that might exist in the component. We’ve already done this with our Server Entry, now we just need to update the Client Entry:

// entry-client.js

import { createApp } from './app'

const { app, router } = createApp()

router.onReady(() => {
  app.$mount('#app')
})
  1. Process the route exit:
App.vue <div id="app"> <ul> <li> <router-link to="/">Home</router-link> </li> <li> <router-link to="/about">About</router-link> </li> </ul> <! -- router exit --> <router-view/> </div>

Eight, management page

  1. NPM install vue-meta in SRC /app.js
import VueMeta from 'vue-meta' Vue.use(VueMeta) Vue.mixin({ metaInfo: { titleTemplate: '%s - pull education '}}) Add code to the Entry-Server.js export function: Const meta = app.$meta() context.meta = meta DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, Initial - scale = 1.0 "> {{{meta. Inject (). The title. The text ()}}} {{{meta. Inject (). The meta. The text ()}}} < / head > < body > <! --vue-ssr-outlet-- --> </body> </ HTML > in the vue page: export default {name: 'Home', metaInfo: {title: Export default {name: 'About', metaInfo: {title: 'About'}}

Data prefetching and state management

  1. During server-side rendering (SSR), we are essentially rendering “snapshots” of our application, so if the application relies on some asynchronous data, then this data needs to be prefetched and parsed before we can start the rendering process.

Another concern is that on the client side, the data needs to be exactly the same as that of the server-side application before it can be mounted to the client-side application – otherwise, the client application will fail to mix because it uses a different state than the server-side application.

To solve this problem, the retrieved data needs to be outside of the view component, that is, in a dedicated data prefetch store or “state container”. First, on the server side, we can prefetch data before rendering and populate the store with data. In addition, we will serialize and inline state in the HTML. This allows you to get the inline state directly from the Store before you mount it to the client application.

  1. Data prefetching
NPM install vuex: SRC /store/index.js import Vue from 'Vue' import vuex from 'vuex' import axios from 'axios' Vue.use(Vuex) export const createStore = () => { return new Vuex.Store({ state: () => ({ posts: [] }), mutations: { setPosts (state, data) { state.posts = data } }, actions: {// During server-side rendering, Async getPosts ({commit}) {// async returns Promise // return new Promise() const {data} = await axios.get('https://cnodejs.org/api/v1/topics') commit('setPosts', data.data) } } }) }

Inject the container into the entry file SRC /app.js

/** * import Vue from 'Vue' import App from './ app. Vue 'import {createrOuter} from './router/' import VueMeat from 'vue-meta' import { createStore } from './store' Vue.use(VueMeta) Vue.mixin({ metaInfo: { titleTemplate: }}) // Exports a factory function, Export function createApp () {const router = createRouter() const store = CreateStore () const app = new Vue({router, // mount the router to the store in the root Vue instance, // mount the container to the store in the root Vue instance. // The root instance simply renders the application component. Render: h => h(App)}) return {App, router, store}} page pages/ posts.vue, use the ServerPrefetch method to make an asynchronous request in the server. <template> <div> <h1>Post List</h1> <ul> <li v-for="post in posts" :key="post.id">{{ post.title }}</li> </ul> </div> </template> <script> // import axios from 'axios' import { mapState, mapActions } from 'vuex' export default { name: 'PostList', metaInfo: { title: 'Posts' }, data () { return { // posts: [] } }, computed: { ... }, // ServerPreFetch () {// start the action, // this.$store.dispatch('getPosts') return this.getPosts()}, methods: {... MapActions (['getPosts'])} // server-side rendering // only supports beforereate and created // does not wait for asynchronous actions in beforereate and created // does not support responsive data // All this will not work in server-side rendering!! // async created () { // console.log('Posts Created Start') // const { data } = await axios({ // method: 'GET', // url: 'https://cnodejs.org/api/v1/topics' // }) // this.posts = data.data // console.log('Posts Created End') // } } </script>  <style> </style>
  1. Synchronize data prefetching to the client
entry-server.js // entry-server.js import { createApp } from './app' export default async context => { // Since there is a possibility of asynchronous routing hook functions or components, we will return a Promise of // so that the server can wait for everything to be ready before rendering. const { app, router, Store} = createApp() const meta = app.$meta() // Router. Router Router will finish parsing await new Promise(router.onReady.bind(router)) // This function will be called Context.Rendered = after the server finishes rendering () => {// The Renderer will inline the Context. state data object into the page template // The final page sent to the client will contain a script: Window. __INITIAL_STATE__ = context.state // The client will take window.__INITIAL_STATE__ from the page and populate it with context.state = in the client store container store.state } return app } entry-client.js // entry-client.js import { createApp } from './app' const { app, router, store } = createApp() if (window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__) } router.onReady(() => { app.$mount('#app') })