Vue2之SSR

概念:放在浏览器进行就是浏览器渲染,放在服务器进行就是服务器渲染。

  • 客户端渲染不利于 SEO 搜索引擎优化
  • 服务端渲染是可以被爬虫抓取到的,客户端异步渲染是很难被爬虫抓取到的
  • SSR直接将HTML字符串传递给浏览器。大大加快了首屏加载时间。
  • SSR占用更多的CPU和内存资源
  • 一些常用的浏览器API可能无法正常使用
  • 在vue中只支持beforeCreate和created两个生命周期

img

Vue2之SSR

  • https://v2.ssr.vuejs.org/zh/

ssr运行过程

  • 只是首屏做ssr,服务端渲染
  • 后续的切换逻辑,执行的都是客户端渲染(前端路由来切换)

安装

yarn add vue-server-renderer vue
yarn add koa koa-router
  • createRenderer,创建一个渲染函数 renderToString, 渲染出一个字符串

  • server.js

// server.js
const Vue = require('vue');
const render = require('vue-server-renderer');
const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();
const vm = new Vue({
    data(){
        return {msg:"hello world"}
    },
    template:`<div>{{msg}}</div>`
});
router.get('/',async (ctx)=>{
  	// 把vue实例渲染为html字符串
    let r = await render.createRenderer().renderToString(vm);
    ctx.body = `
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Document</title>
    </head>
    <body>
        ${r}
    </body>
    </html>
    `
});
app.use(router.routes());
app.listen(4000);

采用模板渲染

  • 在index.html中添加一个<!--vue-ssr-outlet-->

    <!DOCTYPE html>
    <html lang="en">
      <head><title>Hello</title></head>
      <body>
        <!--vue-ssr-outlet-->
      </body>
    </html>
    
  • 在server.js中使用模板,进行渲染

    • 传入template 替换掉注释标签
    // server.js
    const Vue = require('vue');
    const render = require('vue-server-renderer');
    const Koa = require('koa');
    const Router = require('koa-router');
    const app = new Koa();
    const router = new Router();
    const vm = new Vue({
        data(){
            return {msg:"hello world"}
        },
        template:`<div>{{msg}}</div>`
    });
    // 读取模板
    const template = require('fs').readFileSync('./index.html','utf8');
    router.get('/',async (ctx)=>{
        // vm中的内容插入模板中
        let r = await render.createRenderer({
            template
        }).renderToString(vm);
        ctx.body = r;
    });
    app.use(router.routes());
    app.listen(4000);
    

通过webpack实现编译vue项目

安装插件

vue-style-loader基于style-loader,支持服务端渲染

yarn add webpack webpack-cli webpack-dev-server vue-loader vue-style-loader css-loader html-webpack-plugin @babel/core @babel/preset-env babel-loader vue-template-compiler webpack-merge

app.js

// app.js
app.js
import Vue from "vue";
import App from "./App.vue";
export default () => { // 为了保证实例的唯一性所以导出一个创建实例的函数
  const app = new Vue({
    render: h => h(App)
  });
  return { app };
};

client-entry.js

// client-entry.js

import createApp from "./app";
const { app } = createApp();
app.$mount("#app"); // 客户端渲染手动挂载到dom元素上

server-entry.js

// server-entry.js
import createApp from "./app";
export default () => {
  const { app } = createApp();
  return app; // 服务端渲染只需将渲染的实例导出即可
};

webpack.config.js

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const resolve = (dir)=>{
    return path.resolve(__dirname,dir)
}
module.exports = {
    entry: resolve('./src/client-entry.js'),
    output:{
        filename:'[name].bundle.js',
        path:resolve('dist')
    },
    module:{
        rules:[
            {
                test:/\.css$/,
                use:['vue-style-loader','css-loader']
            },
            {
                test:/\.js$/,
                use:{
                    loader:'babel-loader',
                    options:{
                        presets:['@babel/preset-env']
                    }
                },
                exclude:/node_modules/
            },
            {
                test:/.vue$/,
                use:'vue-loader'
            }
        ]
    },
    plugins:[
        new VueLoaderPlugin(),
        new HtmlWebpackPlugin({
            template:'./index.html'
        })
    ]
}

配置客户端打包和服务端打包

webpack.base.js

  • webpack.base.js

    let path = require('path');
    let VueLoaderPlugin = require('vue-loader/lib/plugin')
    module.exports = {
        output:{
            filename:'[name].bundle.js',
            path:path.resolve(__dirname,'../dist')
        },
        module:{
            rules:[
                {test:/\.css/,use:['vue-style-loader','css-loader']},
                {
                    test:/\.js/,
                    use:{
                        loader:'babel-loader',
                        options:{
                            presets:['@babel/preset-env']
                         },
                    },
                    exclude:/node_modules/,
                },
                {test:/\.vue/,use:'vue-loader'}
            ]
        },
        plugins:[
            new VueLoaderPlugin()
        ]
    }
    

webpack.client.js(客户端)

  • webpack.client.js(客户端)

    const {merge} = require("webpack-merge");
    const path = require("path");
    const HtmlWebpackPlugin = require("html-webpack-plugin");
    const base = require("./webpack.base");
    const resolve = filepath => {
      return path.resolve(__dirname, filepath);
    };
    module.exports = merge(base, {
      entry: {
        client: resolve("../src/client-entry.js")
      },
      plugins: [
        new HtmlWebpackPlugin({
          template: resolve("../template/index.client.html")
        })
      ]
    });
    

webpack.server.js(服务端)

webpack打包服务端代码,是不需要引入打包后的js的,只是引入前端的打包的结果

  • webpack.server.js(服务端)

    const {merge} = require("webpack-merge");
    const path = require("path");
    const HtmlWebpackPlugin = require("html-webpack-plugin");
    const base = require("./webpack.base");
    const resolve = filepath => {
      return path.resolve(__dirname, filepath);
    };
    module.exports = merge(base, {
      entry: {
        server: resolve("../src/server-entry.js")
      },
      target: "node",
      output: {
        libraryTarget: "commonjs2" // 导出供服务端渲染来使用
      },
      plugins: [
        new HtmlWebpackPlugin({
          filename: "index.ssr.html",
          // 不压缩 
          minify:false,
          template: resolve("../template/index.ssr.html"),
          // 排除引入文件
          excludeChunks: ["server"]
        })
      ]
    });
    
  • index.ssr.html

    <!DOCTYPE html>
    <html lang="en">
      <head><title>Hello</title></head>
      <body>
        <!--vue-ssr-outlet-->
      </body>
    </html>
    

配置运行脚本

"scripts": {
    "client:dev": "webpack-dev-server --config ./build/webpack.client.js", // 客户端开发环境
    "client:build": "webpack --config ./build/webpack.client.js", // 客户端打包环境
    "server:build": "webpack --config ./build/webpack.server.js" // 服务端打包环境
 },

并行执行多个命令

使用concurrently

  • https://www.npmjs.com/package/concurrently
  • 安装 npm i concurrently
"scripts": {
    "client:dev": "webpack-dev-server --config ./build/webpack.client.js", // 客户端开发环境
    "client:build": "webpack --config ./build/webpack.client.js", // 客户端打包环境
    "server:build": "webpack --config ./build/webpack.server.js", // 服务端打包环境
    "run:all": "concurrently \"npm run client:build\" \"npm run server:build\"" // 并行打包环境
 },

服务端配置

  • 在App.vue上增加id="app"可以保证元素被正常激活

server.js

// server.js

const Koa = require("koa");
const Router = require("koa-router");
const static = require("koa-static");
const path = require("path");
const app = new Koa();
const router = new Router();
const VueServerRenderer = require("vue-server-renderer");
const fs = require("fs");
// 服务端打包的结果
const serverBundle = fs.readFileSync("./dist/server.bundle.js", "utf8");
const template = fs.readFileSync("./dist/index.ssr.html", "utf8");
const render = VueServerRenderer.createBundleRenderer(serverBundle, {
  template
});
router.get("/", async ctx => {
  ctx.body = await new Promise((resolve, reject) => {
    render.renderToString((err, html) => {
      // 必须写成回调函数的方式否则样式不生效
      resolve(html);
    });
  });
});
app.use(router.routes());
app.use(static(path.resolve(__dirname, "dist")));
app.listen(3000);

在index.ssr.html中需要手动引入客户端打包后的结果,也就是引入dist/client.bundle.js

通过json配置createBundleRenderer方法

  • 实现热更新,自动增加preload和prefetch,以及可以使用sourceMap

  • webpack.client.js中引入 vue-server-renderer/client-plugin 并在plugins 中使用

  • webpack.server.js中引入 vue-server-renderer/server-plugin 并在plugins 中使用

  • 配置之后打会产生对应的json文件

webpack.client.js

// webpack.client.js

const {merge} = require("webpack-merge");
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const base = require("./webpack.base");
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')

const resolve = filepath => {
  return path.resolve(__dirname, filepath);
};
module.exports = merge(base, {
  entry: {
    client: resolve("../src/client-entry.js")
  },
  plugins: [
    new VueSSRClientPlugin(),
    new HtmlWebpackPlugin({
      template: resolve("../template/index.client.html")
    })
  ]
});

webpack.server.js

// webpack.server.js

const {merge} = require("webpack-merge");
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const base = require("./webpack.base");
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')


const resolve = filepath => {
  return path.resolve(__dirname, filepath);
};
module.exports = merge(base, {
  entry: {
    server: resolve("../src/server-entry.js")
  },
  target: "node",
  output: {
    libraryTarget: "commonjs2" // 导出供服务端渲染来使用
  },
  plugins: [
    new VueSSRServerPlugin(),
    new HtmlWebpackPlugin({
      filename: "index.ssr.html",
      // 不压缩 
      minify:false,
      template: resolve("../template/index.ssr.html"),
      // 排除引入文件
      excludeChunks: ["server"]
    })
  ]
});

server.js

// server.js

const Vue = require('vue');
const VueServerRenderer = require('vue-server-renderer');
const Koa = require('koa');
const Router = require('@koa/router');
const fs = require('fs');
const static = require('koa-static');
const path = require('path')

const app = new Koa();
const router = new Router();
// const serverBundle = fs.readFileSync(path.resolve(__dirname, 'dist/server.bundle.js'), 'utf8');
// const template = fs.readFileSync(path.resolve(__dirname, 'dist/index.ssr.html'), 'utf8');
// const render = VueServerRenderer.createBundleRenderer(serverBundle, {
//     template
// })

const serverBundle = require('./dist/vue-ssr-server-bundle.json');
const template = fs.readFileSync(path.resolve(__dirname, 'dist/index.ssr.html'), 'utf8');
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
const render = VueServerRenderer.createBundleRenderer(serverBundle,{
    template,
    clientManifest // 通过后端注入前端的js脚本
})

router.get("/(.*)", async (ctx) => {
    //    客户端 =  template + 编译的结果 = 组成的html 
    // 在我们渲染页面的时候 需要让服务器根据当前路径渲染对应的路由
    try{
        ctx.body = await render.renderToString({url:ctx.url});
    }catch(e){
        console.log(e);
        if(e.code == 404){
            ctx.body = 'page not found'
        }
    }
});

// 先匹配静态资源 资源找不到在找对应的api
// 数据获取可以使用axios

app.use(static(path.resolve(__dirname,'dist'))); // 使用静态服务插件
app.use(router.routes());


app.listen(4000);

集成VueRouter

history api 默认刷新会变成404

  • 安装

    yarn add vue-router
    
  • create-router.js

    // create-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;
    };
    

    导出路由配置

配置入口文件app.js

app.js

import Vue from "vue";
import App from "./App.vue";
import createRouter from "./router";
export default () => {
  const router = createRouter();
  const app = new Vue({
    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>

防止刷新页面不存在

配置 router.get(“/(.*)”

server.js

const Vue = require('vue');
const VueServerRenderer = require('vue-server-renderer');
const Koa = require('koa');
const Router = require('@koa/router');
const fs = require('fs');
const static = require('koa-static');
const path = require('path')

const app = new Koa();
const router = new Router();
// const serverBundle = fs.readFileSync(path.resolve(__dirname, 'dist/server.bundle.js'), 'utf8');
// const template = fs.readFileSync(path.resolve(__dirname, 'dist/index.ssr.html'), 'utf8');
// const render = VueServerRenderer.createBundleRenderer(serverBundle, {
//     template
// })

const serverBundle = require('./dist/vue-ssr-server-bundle.json');
const template = fs.readFileSync(path.resolve(__dirname, 'dist/index.ssr.html'), 'utf8');
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
const render = VueServerRenderer.createBundleRenderer(serverBundle,{
    template,
    clientManifest // 通过后端注入前端的js脚本
})

router.get("/(.*)", async (ctx) => {
    //    客户端 =  template + 编译的结果 = 组成的html 
    // 在我们渲染页面的时候 需要让服务器根据当前路径渲染对应的路由
    try{
        ctx.body = await render.renderToString({url:ctx.url});
    }catch(e){
        console.log(e);
        if(e.code == 404){
            ctx.body = 'page not found'
        }
    }
});

// 先匹配静态资源 资源找不到在找对应的api
// 数据获取可以使用axios
// 静态资源要放在路由之前
app.use(static(path.resolve(__dirname,'dist'))); // 使用静态服务插件
app.use(router.routes());


app.listen(4000);

保证异步路由加载完成

ssr切换流程:服务端会先拿到路径,先在服务端渲染好对应的页面,把结果返回。当客户端切换的时候,就由客户端路由接管了

server-entry.js

// server-entry.js

import createApp from './app';

// 此方法是服务端运行的
export default (context)=>{ // context.url 这里包含着当前访问服务端的路径
    return new Promise((resolve,reject)=>{
        const {app,router,store} = createApp();

        router.push(context.url); // 默认跳转到路径里,有异步组件

        router.onReady(()=>{

            const matchComponents = router.getMatchedComponents(); // 获取匹配到的组件
            if(matchComponents.length > 0){ // 匹配到路由了
                // 调用组件对应的asyncData
                Promise.all(matchComponents.map(component=>{
                    // 需要所有的asyncdata方法执行完毕后 才会响应结果
                    if(component.asyncData){
                        // 返回的是promise
                        return component.asyncData(store);
                    }
                })).then(()=>{
                    context.state = store.state;// 将状态放到上下文中

                    resolve(app)// 每次都是一个新的  只是产生一个实例 服务端根据实例 创建字符串 
                },reject)
                
            }else{
                reject({code:404});  // 没有匹配到路由
            }

            
        },reject)
    
        // return app; 
    })
} 

集成vuex配置

  • 安装

    yarn add vuex
    
  • create-store.js

    import Vue from 'vue';
    import Vuex from 'vuex';
    Vue.use(Vuex);
    
    export default ()=>{
        let store = new Vuex.Store({
            state:{
                username:'song'
            },
            mutations:{
                changeName(state){
                    state.username = 'hello';
                }
            },
            actions:{
                changeName({commit}){
                    return new Promise((resolve,reject)=>{
                        setTimeout(() => {
                            commit('changeName');
                            resolve();
                        }, 1000);
                    })
                }
            }
        });
        return store
    }
    
  • app.js

    // app.js
    
    // 引用vuex
    import createRouter from './router';
    import createStore from './store'
    export default ()=>{
        let router = createRouter();
        let store = createStore();
        let app = new Vue({
            router,
            store,
            render:(h)=>h(App)
        })
        return {app,router,store}
    }
    

在后端更新vuex

server-entry.js

// server-entry.js

import createApp from './app';
export default (context)=>{
    return new Promise((resolve)=>{
        let {app,router,store} = createApp();
        router.push(context.url); // 默认访问到/a就跳转到/a
        router.onReady(()=>{
            let matchComponents = router.getMatchedComponents(); // 获取路由匹配到的组件
            
            Promise.all(matchComponents.map(component=>{
                if(component.asyncData){
                    return component.asyncData(store);
                }
            })).then(()=>{
                context.state = store.state; // 将store挂载在window.__INITIAL_STATE__
                resolve(app);

            });
        })
    })
}

在浏览器运行时替换store

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default () => {
    let store = new Vuex.Store({
        state: {
            name: 'jw'
        },
        mutations: {
            changeName(state, payload) {
                state.name = payload;
            }
        },
        actions: {
            changeName({ commit }, payload) {
                return new Promise((resolve,reject)=>{
                    setTimeout(() => {
                        commit('changeName', payload);
                        resolve();
                    }, 1000);
                })
            }
        }
    });

    // 前端运行时会执行此方法 ,用服务端的状态替换掉前端的状态
    if(typeof window !=='undefined' && window.__INITIAL_STATE__){
        store.replaceState(window.__INITIAL_STATE__)
    }


    return store; // 导出容器
}

需要执行的钩子函数

export default {
 mounted() {
  return this.$store.dispatch("changeName");
 },
// 只有页面级组件才有这个方法
 asyncData(store) {
  return store.dispatch("changeName");
 }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值