乐趣区

vue-ssr服务端渲染小白解惑

vue ssr 服务端渲染小白解惑

> 初学 ssr 入坑

初学 vue 服务端渲染疑惑非常多,我们大部分前端都是半路出家,上手都是前后端分离,对服务端并不了解,不说 java、php 语言了,连 node 服务都还没搞明白,理解服务端渲染还是有些困难的;

网上有非常多的 vue 服务渲染的入门案例,但看了很久,很多,还是一头雾水,搞不明白这些文件和关键字的联系和意思:
server.js
entrt-client.js
server-js
built-server-bundle.js
vue-ssr-server-bundle.json
vue-ssrclientmanifest.json
createBundleRenderer
clientManifest

这篇内容会按照 基础服务端渲染 –vue 实例渲染 – 加入 vueRouter– 加入 vueX 的顺序入坑,后续应该还有 – 开发模式 –seo 优化 – 部分渲染,这里先不挖那么多坑了;

> 基础服务端渲染

顾名思义,得启个服务:(建个新项目,不要用 vue-cli)

//server.js
const express = require('express');
const chalk = require('chalk');// 加个 chalk 就是 console 好看点。。const server = express();

server.get('*', (req, res) => {res.set('content-type', "text/html");
res.end(`
<!DOCTYPE html>
<html lang="en">
    <head><title>Hello</title></head>
    <body> 你好 </body>
</html>
`)
})

server.listen(8080,function(){let ip = getIPAdress();
console.log(` 服务器开在:http://${chalk.green(ip)}:${chalk.yellow(8080)}`)
})

function getIPAdress(){//node 下的 os 模块可以拿到启动该文件的服务端的部分信息
var interfaces = require('os').networkInterfaces();
for (var devName in interfaces) {var iface = interfaces[devName];
    for (var i = 0; i < iface.length; i++) {var alias = iface[i];
        if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {return alias.address;}
    }
}
}

启动 node server.js

再看页面 正常,这就是最基础的服务端渲染

其实就是一个 get 请求,返回一个字符串,浏览器默认展示返回结果;
然而对于这个字符串的解析还不明确,什么意思,比如:

去掉这句话,页面就成了这样,原因不深究,自己百度

> 加入 vue 实例

跳过官网说的 built-server-bundle.js 应用,意思就是不用管这个文件了,只是一个过渡文件,项目中也不会用到。直接使用 createBundleRenderer 方法,直接用 vue-ssr-server-bundle.json;

看下现在的目录结构:

新增了 5 个文件;有关客户端的配置 entry-client.js 不是必须的,这里先不管;

app.js 是用来创建 vue 实例的;

entry-server.js 是用来创建生成 vue-ssr-server-bundle.json(需要用到 app.js)所需的配置配件;是给 webpack.server.config.js 用的;

webpack.server.config.js 是用来生成 vue-ssr-server-bundle.json 的;

vue-ssr-server-bundle.json 是给 server.js 中的 createBundleRenderer 用的。

//app.js 
import Vue from 'vue'
import Vue from './App.vue'// 这里一定要写上.vue, 不然会匹配到 app.js,require 不区分大小写 0.0
export default createApp=function(){
return new Vue({render:h => h(App)
})
}

一个 createApp 生成一个 vue 实例;

//App.vue
<template>
<div id='app'>
    这是个 app
</div>
</template>
<script>
export default {}
</script>

还没用到 <router-view>

//weback-base.config.js
const path = require('path')
const VueLoaderPlugin = require('vue-loader/lib/plugin')

module.exports = {
output:{path:path.resolve(__dirname,'./dist'),
    filename:'build.js',
},
module: {
    rules: [
        {
            test:/\.js$/,
            use: {
                loader: 'babel-loader',
                options: {presets: ['@babel/preset-env']
                }
            },
            exclude:[/node_modules/,/assets/]
        },
        {
            test:/\.vue$/,
            use:['vue-loader']
        }
    ]
},
resolve: {
    alias:{'@':path.resolve(__dirname,'../')
    },
    extensions:['.js','.vue','.json']
},
plugins:[new VueLoaderPlugin()
]
}

有关 webpack 配置不啰嗦

//webpack.server.config.js 用来生成 vue-ssr-server-bundle.json
const merge = require('webpack-merge')
const baseConfig = require('./webpack.base.js')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')

module.exports = merge(baseConfig, {
  entry: './entry-server.js',

  // 这允许 webpack 以 Node 适用方式 (Node-appropriate fashion) 处理动态导入(dynamic import),// 并且还会在编译 Vue 组件时,// 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。target: 'node',

  // 对 bundle renderer 提供 source map 支持
  devtool: 'source-map',

  // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)
  output: {libraryTarget: 'commonjs2'},


  // 这是将服务器的整个输出
  // 构建为单个 JSON 文件的插件。// 默认文件名为 `vue-ssr-server-bundle.json`
  plugins: [new VueSSRServerPlugin()
  ]
})

这个配置哪都能找到,重点是 VueSSRServerPlugin 这个插件,生成 vue-ssr-server-bundle.json 全靠它,去掉的话生成的是 built-server-bundle.js;关于 merge 插件,libraryTarget,target 配置问题自己百度 webpack 去 0.0;

//entry-server.js
import {createApp} from './src/app'

export default context => {return createApp()
}

固定写法,返回一个函数供 createBundleRenderer 使用;

生成 vue-ssr-server-bundle.json

到目前为止安装的插件有:

自己手动一个一个装就行了。

生成 vue-ssr-server-bundle.json,使用 webpack 命令

一切都手动,熟悉 webpack;

修改 server.js

const express = require('express');
const chalk = require('chalk');

const server = express();
const serverBundle = require('./dist/vue-ssr-server-bundle.json')//** 新增 **//
const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle,{
    runInNewContext: false, // 看名字也知道是生成某个新的 Context 对象, 默认是 true, 改成 false 理解为某种缓存机制,提高服务器效率
    template: require('fs').readFileSync('./index.html', 'utf-8'),
  })//** 新增 **//
server.get('*', (req, res) => {//res.set('content-type', "text/html");
    //res.end(`
    //<!DOCTYPE html>
    //<html lang="en">
    //    <head><title>Hello</title></head>
    //    <body >
    //    <div style='color:red'> 你好 </div>
    //    </body>
   // </html>
   // 改成下面这样
   const context = {// 这里的参数现在还没用,但这个对象还是得用,要做 renderToString 的参数
    url:req.url
  }
    renderer.renderToString(context, (err, html) => {if (err) {res.status(500).end('Internal Server Error')
        return
      } else {res.end(html)
      }
    })
    `)
  })

server.listen(8080,function(){let ip = getIPAdress();
    console.log(` 服务器开在:http://${chalk.green(ip)}:${chalk.yellow(8080)}`)
})

function getIPAdress(){//node 下的 os 模块可以拿到启动该文件的服务端的部分信息,细节自己去 node 上面查
    var interfaces = require('os').networkInterfaces();
    for (var devName in interfaces) {var iface = interfaces[devName];
        for (var i = 0; i < iface.length; i++) {var alias = iface[i];
            if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {return alias.address;}
        }
    }
}

试一蛤:node server.js

正常,箭头指的地方官网有解释。别忘了 inde.html 中加入一行注释:

后续修改 title,meta 头部都是通过类似的注释方式,原理就是正则匹配替换字符串 -。-;

> 加入路由 vue-router

新增几个文件

需要修改的文件有:

App.vue// 加个 router-view 就行

//app.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
export  function createApp(){
    const app = new Vue({
        router,
        render:h => h(App)
    })
    return {app,router}
}

把 app 实例和 router 都抛出去,给 entry-server.js 用

// entry-server.js
import {createApp} from './src/app'

export default context => {
   // 这里用 promise 的原因有很多,其中有一个就是下面这个 onReady 方法是异步的。createBundleRenderer 支持 promise
  return new Promise((resolve, reject) => {const { app, router} = createApp()

    router.push(context.url)

    router.onReady(() => {//onReady 方法还有 getMatchedComponents 方法还是需要了解一下
      const matchedComponents = router.getMatchedComponents()
      if (!matchedComponents.length) {return reject({ code: 404})
      }
      resolve(app)
    }, reject)
  })
}

最后看一下 router.js

//router.js
 import Vue from 'vue'
 import VueRouter from 'vue-router'
// 页面要先声明后使用,不要问为什么
import home from './pages/home'
import store from './pages/store'

Vue.use(VueRouter)
export default new VueRouter({
    mode: 'history',
    routes:[{path:'/',name:'home',component:home},
        {path:'/store',name:'store',component:store},
    ]
})

再看一下两个页面的代码;

    //store.vue 
    <template>
    <div>this is store</div>
    </template>
    <script>
         export default {}
    </script>

改的差不多了,试一哈:

重新打个包 webpack --config webpack.server.js

启动 node server

>entry-client.js 是干啥的

到目前为止还没用到 entry-client.js 叫客户端配置,不着急使用,先做个测试,写点逻辑试试:
修改下 store.vue

//store.vue
<template>
<div @click='run'>{{msg}}</div>
</template>
<script>
    export default {data(){msg:'this is store'},
        created(){this.msg = 'this is created'},
        mounted(){this.msg = 'this is mounted'},
        methods: {run(){alert('this is methods')
            }
        }
    }
</script>

看这个样子页面最终展示的结果应该是 this is mounted,然而结果是这样的:

很好解释,服务端对于钩子函数的理解也是很正确的,created 会在页面返回之前执行,而 mounted 是在 vue 实例成型之后执行,就是页面渲染后,这个是要在客户端才会执行,可是为什么页面出来了没有执行 mounted,而且 run 的点击事件没有生效;
看看页面:

一个 js 文件都没加载,怎么执行逻辑,就是个静态页面 0.0;
这时候 entry-client.js 就出场了

新增两个文件

//entry-client.js 
import {createApp} from './src/app.js';

const {app} = createApp();

app.$mount('#app');

基本配置;

//webpack.client.config.js

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: './entry-client.js',
  optimization:{runtimeChunk:true},
  plugins: [
    // 此插件在输出目录中
    // 生成 `vue-ssr-client-manifest.json`。new VueSSRClientPlugin(),]
})

这个地方重点除了 VueSSRClientPlugin 生成 vue-ssr-client-manifest.json 外,optimization 是 webpack4 产物,用来分离生成共公 chunk, 配置还算复杂,可以看下这里 webpack4 optimization 总结

修改下 server.js
//server.js

   const express = require('express');
    const chalk = require('chalk');

    const server = express();

    const serverBundle = require('./dist/vue-ssr-server-bundle.json')
    const clientManifest = require('./dist/vue-ssr-client-manifest.json')// 新增
    const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle,{
        runInNewContext: false, // 推荐
        template: require('fs').readFileSync('./index.html', 'utf-8'),
        clientManifest // // 新增
      })
    server.get('*', (req, res) => {res.set('content-type', "text/html");
        const context = {url:req.url}

            renderer.renderToString(context, (err, html) => {if (err) {res.status(500).end('Internal Server Error')
                return
              } else {res.end(html)
              }
            })

      })

    server.listen(8080,function(){let ip = getIPAdress();
        console.log(` 服务器开在:http://${chalk.green(ip)}:${chalk.yellow(8080)}`)
    })

    function getIPAdress(){//node 下的 os 模块可以拿到启动该文件的服务端的部分信息,细节自己去 node 上面查
        var interfaces = require('os').networkInterfaces();
        for (var devName in interfaces) {var iface = interfaces[devName];
            for (var i = 0; i < iface.length; i++) {var alias = iface[i];
                if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {return alias.address;}
            }
        }
    }

打包下:webpack –config webpack.client.config.js

node server 一下,看看页面

js 有了,可是为什么还不行,不能点 0.0;
看看。奥报错了

读取不到静态文件;
修改 server.js 加个静态文件托管:


再看看

事件也有了,页面没变化,console 一下,发现值其实已经变了,只是失去了响应式;这就是为什么要用 vuex 的缘故;

> 加入 vuex

开始想在页面中用 this.$set 方法,然而行不通,而且不可能给每个值都重新写一个这个方法;

加个 sotre.js

// store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

  export default new Vuex.Store({
    state: {msg: ''},
    actions: {setMsg ({ commit}, val) {commit('setMsg', val)
      }
    },
    mutations: {setMsg (state, val) {Vue.set(state, 'msg', val)// 关键
      }
    }
  })

很基础的逻辑,关键在 Vue.set 这个方法,重新增加了响应式;
修改下 app.js

//app.js
    import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'// 加个 store 就行了
export  function createApp(){
    const app = new Vue({
        router,
        store,
        render:h => h(App)
    })
    return {app,router}
}

store.vue 改成这样

<template>
    <div @click='run'>{{msg}}</div>
</template>
<script>
    export default {data(){},
        created(){this.$store.dispatch('setMsg','this is created')
        },
        computed:{msg(){return this.$store.state.msg;}
        },
        mounted(){this.$store.dispatch('setMsg','this is mounted')
        },
        methods: {run(){alert('this is methods')
            }
        }
    }
</script>

重新打个包,想一下,修改页面的话只需要重新打包 client, 如果修改了 app.js 两个就要都重新打包了;

node server 一下


这回总算完成了;

> 总结

服务端渲染东西还是挺多的,涉及领域也非常广,比如 vue,webpack,node,它们的生态圈都大的可怕,需要学习东西非常多,
坑又多,又大,又深,后面还有很多问题要解决:

异步数据加载;//html 返回前先渲染一部分接口拿到的数据
怎么做 seo 优化;// 做服务端渲染的重要原因,处理异步数据加载问题也是为了这个
缓存怎么加;开发环境搭建;// 你并不希望每改一行代码就重新手动打个包,重启下服务吧 0.0
还有怎么实现部分页面 ssr;// 一个项目不可能所有页面都服务端渲染,太耗性能,服务器压力大呀;

还有很多疑惑:

比如为什么会失去响应式,webpack 到底该怎么配置。。
退出移动版