Vue基于ssr渲染

vue ssr指南

CSR(传统的浏览器端渲染)

Client Side Render
通常,我们的Vue项目是在npm run build打包之后,直接放到服务器端。浏览器去请求相应的html,加载对应的js文件,生成DOM。
在这里插入图片描述

路由改变,局部刷新,浏览器不会刷新
缺点
需要js全部加载完,页面才能出来,加载较慢(懒加载)
js改变dom,生成页面,不利于SEO

SSR(服务器端渲染是什么)

Server Side Render
SSR的原理是将打包后的文件,先在服务器端处理,生成一个个的HTML字符串,当浏览器请求的时候直接发过去。
在这里插入图片描述

每一个路由请求到的都是一个html串,路由改变,浏览器刷新
优点
利于SEO,搜索引擎爬虫抓取工具可以直接查看完全渲染的页面
更快的内容到达时间 (time-to-content),更好的用户体验,特别是对于缓慢的网络情况或运行缓慢的设备。(压力来到了服务器,所以需要权衡哪些用服务端渲染,哪些交给客户端)

缺点

  • 复杂度
  • 库的支持性,代码兼容
  • 性能问题,每个请求都是n个实例的创建,不然会污染,消耗会变得很大
    缓存 node serve 、 nginx判断当前用户有没有过期,如果没过期的话就缓存,用刚刚的结果。
    降级:监控cpu、内存占用过多,就spa,返回单个的壳
  • 服务器负载变大,相对于前后端分离服务器只需要提供静态资源来说,服务器负载更大,所以要慎重使用(比如一整套图表页面,相对于服务端渲染,可能用户不会在乎初始加载的前几秒,可以交由客户端使用类似于骨架屏,或者懒加载之类的提升用户体验)

预渲染

只是用来改善少数营销页面的 SEO,可以使用预渲染(例如:pre-renderer),在构建时 (build time) 简单地生成针对特定路由的静态 HTML 文件,而无需使用 web 服务器实时动态编译 HTML。
也可以考虑用爬虫工具,比如puppeteer,让它直接从spa项目中爬出结果

nuxt配置SSR

nuxt中文网
全新项目,可以考虑用nuxt来做

vue项目中手动加入SSR

在这里插入图片描述
1、改造router、store和main文件,将其中的new Router new Store和new Vue转化为方法,便于后面使用
router:

import Vue from 'vue'
import Router from 'vue-router'
import page1 from '@/components/page1'
import page2 from '@/components/page2'

Vue.use(Router)

export function createRouter(){
  return new Router({
    mode: "history",
    routes: [
      {
        path: '/',
        component: page1
      },
      {
        path: '/page2',
        component: page2
      }
    ]
  })
}

store:

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export function createStore(){
    return new Vuex.Store({
        state:{
            count:0
        },
        mutations:{
           //初始化
           init(state,count){
               state.count = count
           },
           add(state){
                state.count++
           }
        },
        actions:{
            //异步请求count
            getCount({commit}){
                return new Promise(resolve=>{
                    setTimeout(()=>{
                        commit("init",Math.random()*100)
                        resolve()
                    },1000)
                })
            }
        }
    })
}

main:

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import {createRouter} from './router'
import {createStore} from './store'
Vue.config.productionTip = false
//客户端数据预取处理
Vue.mixin({
  beforeMount(){
    const {asyncData} = this.$options
    if(asyncData){
      // 将获取数据操作分配给 promise
      // 以便在组件中,我们可以在数据准备就绪后
      // 通过运行 this.dataPromise.then(...)来执行其他任务
      this.dataPromise = asyncData({
        store: this.$store,
        route: this.$route
      })
    }
  }
})

/* eslint-disable no-new */
//这儿采用工厂函数导出vue实例,确保每次请求都为独立实例
export function createApp(context){
  let router = createRouter();
  let store = createStore();
  //context用于给上下文传递参数
  const app = new Vue({
    router,
    store,
    context,
    render:h => h(App)
  })
  return {app,router,store};
}

2、创建两个js文件,server.js和client.js
将之前的一次打包,转化为两次打包,一个用于服务端打包入口js文件,一个用于客户端打包入口js文件。
server.js

//服务器端打包入口文件
import {createApp} from './main'
//返回一个函数,接收请求上下文,返回创建的vue实例
//根据返回的内容,拿到指定的路由节点
export default context => {
	//这里返回一个Promise,确保路由会组件准备就绪
    return new Promise((resolve, reject)=>{
        const {app, router, store} = createApp(context);
        //跳转到首屏地址
        router.push(context.url);
        //路由就绪,返回结果
        router.onReady(()=>{
        	//获取匹配的路由组件数组
            const mathComponents = router.getMatchedComponents();
            //若无匹配则抛出异常
            if(!mathComponents){
                return reject({code: 404});
            }
            //对所有匹配的路由组件调用可能存在的asyncData()
            Promise.all(
                mathComponents.map(Component=>{
                    if(Component.asyncData){
                        return Component.asyncData({
                            store,
                            route: router.currentRoute
                        })
                    }
                })
            ).then(()=>{
                // 所有预取钩子 resolve 后
                // store 已经填充入渲染应用所需状态
                // 将状态附加到上下文,且 template 选项用于 renderer 时
                // 状态将自动序列化为 window.__INITIAL_STATE__,并注入 HTML
                context.state = store.state;
                resolve(app)
            }).catch(reject)
        },reject);
    })
}

client.js

//客户端打包入口文件
import {createApp} from './main'
const {app,router,store} = createApp();
//当使用template时,context.state将作为window.__INITIAL_STATE__状态自动嵌入到最终的HTML
//在客户端挂载到应用程序之前,store就应该获取到状态
if(window.__INITIAL_STATE__){
    store.replaceState(window.__INITIAL_STATE__)
}
//路由完成之后,再去进行挂载,以防有异步路由的情况
router.onReady(()=>{
    app.$mount("#app");
})

3、新建存放服务器端生成的html模板
index.ssr.html用于保存服务器端生成的html(宿主文件),其中<!–vue-ssr-outlet–>为注入标记,即在这儿进行添加服务端返回内容

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>ssr-test</title>
  </head>
  <body>
    <!--vue-ssr-outlet-->
    <script type="text/javascript" src="<%=htmlWebpackPlugin.options.files.js%>"></script>
  </body>
</html>

vue文件

<template>
    <div>
        <h2>其他页</h2>
        <p @click="$store.commit('add')">{{$store.state.count}}</p>
    </div>
</template>

<script>
    export default {
         asyncData({store,route}){//约定预取逻辑编写在预取钩子asyncData中
            //触发action后,返回Promise以便确定请求结果
            return store.dispatch("getCount")
        }
    }
</script>

<style scoped>

</style>

4、创建客户端和服务器端打包配置文件
安装:

npm install vue vue-server-renderer --save

webpack.build.server.js拷贝webpack.prod.conf.js生产打包配置文件进行修改

添加入口文件
打包后的服务端文件需要在node下面执行,所以将target指定为node
nodejs的规范为commonjs,所以需要将output中的libraryTarget指定为commonjs。新版本node使用commonjs2
修改HtmlWebpackPlugin下面的模板文件和目标文件为我们创建的index.ssr.html
删除HtmlWebpackPlugin下面minify中的removeComments(删除注释),因为我们上面说的服务器端html模板中的注释有特定作用,不能被删除
引入vue-server-renderer下面的server-plugin,并进行注册
const VueSSRServePlugin = require('vue-server-renderer/server-plugin')
entry:{
  app:'./src/server.js'
},
target: 'node',
output:{
  libraryTarget:'commonjs2'  
},
plugins: [
    new VueSSRServePlugin(),
    new HtmlWebpackPlugin({
      filename: 'index.ssr.html',
      template: 'index.ssr.html',
      inject: true,
      files:{
        js:'app.js'
      },
      minify: {
        collapseWhitespace: true,
        removeAttributeQuotes: true
        // more options:
        // https://github.com/kangax/html-minifier#options-quick-reference
      },
      // necessary to consistently work with multiple chunks via CommonsChunkPlugin
      chunksSortMode: 'dependency'
    })
]

webpack.build.client.js拷贝webpack.prod.conf.js生产打包配置文件进行修改

添加入口文件
引入vue-server-renderer下面的client-plugin,并进行注册
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
entry:{
  app:'./src/ client.js'
},

plugins: [
    new VueSSRClientPlugin(),
]

5、package.json中新加两条打包命令

"build:client": "webpack --config build/webpack.build.client.js",
"build:server": "webpack --config build/webpack.build.server.js"
"build": "npm run build:server && npm run build:client",
npm run build

打包完成之后的目录
在这里插入图片描述
6、配置node服务
根目录创建server.js

const express = require('express');
const server = express();
const { createBundleRenderer } = require('vue-server-renderer');
const path = require('path');
const fs = require('fs');
const { target } = require('./config/dev.env');
//服务端打包文件地址
const serverBuild = require(path.resolve(__dirname, "./dist/vue-ssr-server-bundle.json"));
//客户端清单
const clientManifest = require(path.resolve(__dirname, "./dist/vue-ssr-client-manifest.json"));
//宿主文件
const template = fs.readFileSync(path.resolve(__dirname, "./dist/index.ssr.html"), 'utf-8');
//创建渲染器
const renderer = createBundleRenderer(serverBuild, {
    runInNewContext: false,
    template: template,
    clientManifest: clientManifest
});
server.get("*", (req, res) => {
    if(req.url != "/favicon.ico"){
        const context = { url: req.url };
        //以流的形式分片读取大文件
        const ssrStream = renderer.renderToStream(context);
        ssrStream.on('error',(err)=>{
            res.status(500).end('Internal Server Error')
            console.log(err);
            return;
        });
        let buffers = [];
        ssrStream.on('data',(data) => buffers.push(data));
        ssrStream.on('end', () => {res.end(Buffer.concat(buffers))});
    }
})
server.listen(3000);

7、启动服务,查看效果

node server.js

在这里插入图片描述

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值