乐趣区

关于ssr:vuessr-手写服务端渲染集成路由vuex

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']
        }),
    ]
})
退出移动版