1. 介绍

构建过程


如上图所示,应用webpack利用咱们配置不同的入口生成服务端和客户端的bundle,服务端的bundle是用来生成html字符串,客户端bundle是用来注入到服务端生成的html字符串中的,因为服务端返回的是字符串,一系列的事件须要依赖客户端打包的js代码(客户端的js + 服务端渲染的字符串)由浏览器渲染这样就实现了一个ssr的构建

长处

1.利于seo优化
在浏览器渲染的时候当咱们查看源代码只能看到一个<div id='app'></div> 内容是由js生成,这样不利于爬虫所爬取到,服务端渲染是将解析过程放到了服务端来做,服务端将解析好的字符串传给前端,当查看源代码时就会显示解析后的元素,爬虫更容易被检索

2.解决首页白屏的成果
如果数据量比拟大那么浏览器会卡顿处于白屏状态,应用服务端渲染间接将解析好的HTML字符串传递给浏览器,大大放慢了首屏加载工夫

毛病

1.占用内存
所有的渲染逻辑都在服务端进行的,那么会占用更多的CPU和内存资源,当申请过多时不停的解析页面返回给客户端,会导致卡顿成果

2.浏览器Api不能应用
因为页面在服务端渲染那么服务端是不能调用浏览器的api的

3.生命周期
因为服务器端不晓得什么时候挂载实现,在vue中只反对beforeCreated和created两个生命周期

2. 开发前配置

1.装置依赖包
cnpm i  webpack webpack-cli webpack-dev-server koa koa-router koa-static vue vue-router vuex vue-server-renderer   vue-loader vue-style-loader css-loader html-webpack-plugin @babel/core @babel/preset-env babel-loader vue-template-compiler webpack-merge url-loader
2.意识目录

3.根底代码

App.vue
<template>  <!-- id="app" 客户端激活,服务端解析成字符串返回给客户端,使其变为由 Vue 治理的动静 DOM 的过程 -->  <div id="app">    <Bar></Bar>    <Foo></Foo>  </div></template><script>import Bar from "./components/Bar";import Foo from "./components/Foo";export default {  components: {    Bar,    Foo,  },};</script>
Bar.vue
<template>  <div id="bar">    Bar  </div></template><style scoped>#bar {  background: red;}</style>
Foo.vue
<template>  <div>    Foo    <button @click="clickMe">点击</button>  </div></template><script>export default {  methods: {    clickMe() {      alert("点我");    },  },};</script>
public/server.html
<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta http-equiv="X-UA-Compatible" content="IE=edge" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <title>Document</title>  </head>  <body>    <!--vue-ssr-outlet-->  </body></html>
main.js

Node.js 服务器是一个长期运行的过程。当咱们的代码进入该过程时,它将进行一次取值并留存在内存中。这意味着如果创立一个单例对象,它将在每个传入的申请之间共享。所以咱们须要保障每次拜访都会产生一个新的Vue实例,裸露一个函数每次调用都保障是新的根实例

import Vue from 'vue'import App from './App'export default () => {    let app = new Vue({        el: '#app',        render: h => h(App)    })    return { app }}
client-entry.js

客户端失常挂载

import createApp from './main'let { app } = createApp()app.$mount('#app')
server-entry.js
import createApp from './main'export default () => {    let { app } = createApp();    return app}

集成路由

减少router.js文件
import Vue from 'vue'import VueRouter from 'vue-router'import Foo from './components/Foo.vue'Vue.use(VueRouter)export default () => {    const router = new VueRouter({        mode: "history",        routes: [            { path: "/", component: Foo },            { path: "/bar", component: () => import("./components/Bar.vue") }        ]    });    return router;}
main.js
import Vue from 'vue'import App from './App'import createRouter from './router'export default () => {    let router = createRouter()    let app = new Vue({        el: '#app',        router,        render: h => h(App)    })    return { app, router }}
App.vue
<template>  <div id="app">    <router-link to="/">foo</router-link>    <router-link to="/bar">bar</router-link>    <router-view></router-view>  </div></template>
server-entry.js
import createApp from './main'// 服务端须要调用以后这个文件 产生一个vue的实例export default (context) => {     // 因为有可能会是异步路由钩子函数或组件,所以咱们将返回一个 Promise,     //以便服务器可能期待所有的内容在渲染前,就曾经准备就绪。    return new Promise((resolve, reject) => {        let { app, router } = createApp();        //返回的实例应该跳转到/ 或者/bar  context.url是服务端跳转的默认门路        router.push(context.url)        // 波及到异步组件的问题        router.onReady(() => {            //获取以后跳转到的匹配组件            let matchs = router.getMatchedComponents()            //matchs匹配到的所有的组件,整个都在服务端执行            if (matchs.length == 0) {                reject({ code: 404 })            }            resolve(app)        }, reject)    })}

集成vuex

减少store文件
import Vue from 'vue'import Vuex from 'vuex'Vue.use(Vuex)export default () => {    let store = new Vuex.Store({        state: {            name: ''        },        mutations: {            changeName(state) {                state.name = 'myh'            }        },        actions: {            changeName({ commit }) {                return new Promise((resolve, reject) => {                    setTimeout(() => {                        commit('changeName');                        resolve()                    }, 1000)                })            }        }    })    // 如果浏览器执行时 我须要将服务器设置的最新状态替换成客户端的状态,设置到window上的操作是server-entry.js下的操作    if (typeof window !== 'undefined' && window.__INITIAL_STATE__) {        store.replaceState(window.__INITIAL_STATE__)    }    return store}
main.js
import Vue from 'vue'import App from './App'import createRouter from './router'import createStore from './store'export default () => {    let router = createRouter()    let store = createStore()    let app = new Vue({        el: '#app',        router,        store,        render: h => h(App)    })    return { app, router, store }}
Foo.vue
<template>  <div>    Foo    <button @click="clickMe">点击</button>    {{ $store.state.name }}  </div></template><script>export default {  asyncData(store) {    //asyncData只在服务端执行 只在页面组件中执行    return store.dispatch("changeName");  },  methods: {    clickMe() {      alert("点我");    },  },};</script>
server-entry.js
import createApp from './main'export default (context) => {    return new Promise((resolve, reject) => {        let { app, router, store } = createApp();        router.push(context.url)        router.onReady(() => {//获取到匹配到的所有门路            let matchs = router.getMatchedComponents()            if (matchs.length == 0) {                reject({ code: 404 })            }//如果匹配到的组件中有asyncData 默认执行            Promise.all(matchs.map(v => {                if (v.asyncData) {                    // asyncData是在服务端调用的                    return v.asyncData(store)                }            })).then(() => {                // 以上all中的办法会扭转store中的state                context.state = store.state;//把vuex的状态挂载到上下文中 会将状态挂载window上                resolve(app)            })        }, reject)    })}

服务端代码 server.js

let Koa = require('koa')let Router = require('koa-router')let Static = require('koa-static')let fs = require('fs')let path = require('path');let app = new Koa()let router = new Router()let VueServerRender = require('vue-server-renderer')let ServerBundle = require('./dist/vue-ssr-server-bundle.json')// 渲染打包后的后果let template = fs.readFileSync('./dist/server.html', 'utf8')let clientManifest = require('./dist/vue-ssr-client-manifest.json')//createBundleRenderer 找到webpack打包后的函数 外部会调用这个函数获取到vue的实例let render = VueServerRender.createBundleRenderer(ServerBundle, {    template,    clientManifest})router.get('/(.*)', async ctx => {    try {        ctx.body = await new Promise((resolve, reject) => {//renderToString=>依据实例生成一个字符串返回给浏览器            render.renderToString({ url: ctx.url }, (err, data) => {                if (err) reject(err)                resolve(data);            });        });    } catch (e) {        ctx.body = '404'    }})app.use(Static(path.resolve(__dirname, 'dist')))app.use(router.routes())app.listen(3002)

webpack配置

webpack.base.js
let path = require('path')let VueLoader = require('vue-loader/lib/plugin')let resolve = dir => {    return path.resolve(__dirname, dir)}module.exports = {    output: {        filename: '[name].bundle.js',        path: resolve('../dist')    },    resolve: {        extensions: ['.js', '.vue']    },    module: {        rules: [            {                test: /\.js$/,                 use: {                    loader: 'babel-loader',                    options: {                        presets: ['@babel/preset-env']                    }                },                exclude: /node_modules/            },//vue-style-loader基于style-loader实现的 反对服务端渲染            {                test: /\.css$/,                use: ['vue-style-loader', 'css-loader']            },            {                test: /\.vue$/,                use: 'vue-loader'            },            {                test: /\.(eot|svg|ttf|woff|woff2)(\?\S*)?$/,                loader: 'url-loader'            },            {              test: /\.(png|jpg|gif|svg)$/,              loader: 'url-loader'            },        ]    },    plugins: [        new VueLoader(),    ]}
webpack.client.js
let {merge} = require('webpack-merge')let base = require('./webpack.base')let path = require('path')let ClientRenderPlugin  = require('vue-server-renderer/client-plugin')module.exports = merge(base, {    entry: {        client: path.resolve(__dirname,'../src/client-entry.js')    },    output:{//不设置这个的话 打包进去的vue-ssr-client-manifest.json中的publicPath为'auto',默认申请动态资源http://localhost:3002/auto/client.bundle.js        publicPath:'/',    },    plugins: [ // 此插件在输入目录中,生成 `vue-ssr-client-manifest.json`。        new ClientRenderPlugin()    ]})
webpack.server.js
let { merge } = require('webpack-merge')let base = require('./webpack.base')let path = require('path')let ServerRenderPlugin = require('vue-server-renderer/server-plugin')let HtmlWebpackPlugin = require('html-webpack-plugin')let resolve = dir => {    return path.resolve(__dirname, dir)}module.exports = merge(base, {    entry: {        server: resolve('../src/server-entry.js')    },    target: 'node',//要给node来应用    output: {        libraryTarget: 'commonjs2'    },    devtool: 'source-map',    plugins: [ // 这是将服务器的整个输入 构建为单个 JSON 文件的插件。 默认文件名为 `vue-ssr-server-bundle.json`        new ServerRenderPlugin(),        new HtmlWebpackPlugin({            filename: 'server.html',            template: resolve('../public/server.html'),            minify: false,//不压缩            excludeChunks: ['server']        }),    ]})