vue ssr服務端渲染小白解惑

vue ssr服務端渲染小白解惑

>初學ssr入坑

初學vue服務端渲染疑惑很是多,咱們大部分前端都是半路出家,上手都是先後端分離,對服務端並不瞭解,不說java、php語言了,連node服務都還沒搞明白,理解服務端渲染仍是有些困難的;php

網上有很是多的vue服務渲染的入門案例,但看了好久,不少,仍是一頭霧水,搞不明白這些文件和關鍵字的聯繫和意思:
server.js
entrt-client.js
server-js
built-server-bundle.js
vue-ssr-server-bundle.json
vue-ssrclientmanifest.json
createBundleRenderer
clientManifesthtml

這篇內容會按照 基礎服務端渲染--vue實例渲染--加入vueRouter--加入vueX的順序入坑,後續應該還有--開發模式--seo優化--部分渲染,這裏先不挖那麼多坑了;前端

>基礎服務端渲染

顧名思義,得啓個服務:(建個新項目,不要用vue-cli)
image.pngvue

//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.jsjava

image.png

再看頁面 正常,這就是最基礎的服務端渲染
image.png
其實就是一個get請求,返回一個字符串,瀏覽器默認展現返回結果;
然而對於這個字符串的解析還不明確,什麼意思,好比:
image.pngnode

去掉這句話,頁面就成了這樣,緣由不深究,本身百度
image.pngwebpack

>加入vue實例

跳過官網說的built-server-bundle.js應用,意思就是不用管這個文件了,只是一個過渡文件,項目中也不會用到。直接使用createBundleRenderer方法,直接用vue-ssr-server-bundle.json;web

看下如今的目錄結構:
image.png
新增了5個文件;有關客戶端的配置entry-client.js不是必須的,這裏先無論;vue-router

app.js是用來建立vue實例的;vuex

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

到目前爲止安裝的插件有:
image.png

本身手動一個一個裝就好了。

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

一切都手動,熟悉webpack;

image.png

修改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
image.png

正常,箭頭指的地方官網有解釋。別忘了inde.html中加入一行註釋:image.png

後續修改title,meta頭部都是經過相似的註釋方式,原理就是正則匹配替換字符串-。-;

>加入路由vue-router

新增幾個文件
image.png
須要修改的文件有:

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

image.png

//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

image.png

>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,然而結果是這樣的:
image.png
很好解釋,服務端對於鉤子函數的理解也是很正確的,created會在頁面返回以前執行,而mounted是在vue實例成型以後執行,就是頁面渲染後,這個是要在客戶端纔會執行,但是爲何頁面出來了沒有執行mounted,並且run的點擊事件沒有生效;
看看頁面:
image.png
一個js文件都沒加載,怎麼執行邏輯,就是個靜態頁面0.0;
這時候entry-client.js就出場了
image.png
新增兩個文件

//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
image.png

node server 一下,看看頁面
image.png
js有了,但是爲何還不行,不能點0.0;
看看。奧報錯了
image.png
讀取不到靜態文件;
修改server.js加個靜態文件託管:

image.png
再看看
image.png
事件也有了,頁面沒變化,console一下,發現值其實已經變了,
看看代碼,是這裏忘加return了;
image.png

>加入vuex

image.png
加個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 一下

image.png
這回總算完成了;

>總結

服務端渲染東西仍是挺多的,涉及領域也很是廣,好比vue,webpack,node,它們的生態圈都大的可怕,須要學習東西很是多,
坑又多,又大,又深,後面還有不少問題要解決:

異步數據加載;//html返回前先渲染一部分接口拿到的數據
怎麼作seo優化;//作服務端渲染的重要緣由,處理異步數據加載問題也是爲了這個
緩存怎麼加;
開發環境搭建;//你並不但願每改一行代碼就從新手動打個包,重啓下服務吧0.0
還有怎麼實現部分頁面ssr;//一個項目不可能全部頁面都服務端渲染,太耗性能,服務器壓力大呀;
相關文章
相關標籤/搜索