初識Vue_ssr 基礎的服務端渲染

資源

https://ssr.vuejs.orgcss

當前開發版本

"vue": "^2.6.11",
    "vue-router": "^3.3.4",
    "vue-server-renderer": "^2.6.11",
    "vuex": "^3.4.0"

概念

服務端渲染:將vue實例渲染爲HTML字符串直接返回,在前端激活爲交互程序,老得;SSH只能返回HTML字符串,並沒有法激活。html

接下來 採用同構(同vue)的方式來進行開發,實現服務端渲染。前端

目前服務端渲染的方式

傳統web開發:
傳統web開發,網⻚內容直接在服務端渲染完成,一次性傳輸到瀏覽器。從數據庫直接拿到html代碼;

缺點是:服務器響應時間長;帶寬消耗,負荷比較大。這種就是所見即所得。vue

單⻚應用 Single Page App:(spa)
單⻚應用優秀的用戶體驗,使其逐漸成爲主流,⻚面內容由JS渲染出來,這種方式稱爲客戶端渲染。
給前端返回的是html的結構,可是沒有內容。內容由前端的庫,vue or react渲染html;再發送ajax請求,請求數據獲取data中的數據來渲染。

缺點:不利於SEO,不利於搜索引擎。若是數據沒有返回,首屏加載速度會慢node

服務端渲染 Server Side Render
SSR解決方案,後端渲染出完整的首屏的dom結構返回,仍是用vue or react模板去開發的。前端拿到的內容包括首屏(html結構)及完整spa結構(路由....),在前端作路由的跳轉;應用激活後依然按照spa方式運行,這種⻚面渲染方式被稱爲服務端渲染 (server side render)

使用SSR的優勢

  • seo:搜索引擎優化
  • 首屏渲染內容到達時間快一次請求一次響應;
若是是一個spa 一個請求 一次響應,打開請求以後還須要ajax請求數據,速度就沒有那麼快。

express

npm i express -S
基礎http服務 代碼演示:
// nodejs 代碼
const express = require('express')

// 這裏是獲取express的實例,
// 能夠從源碼中看到:
//源碼路徑:/node_modules/@types/express/index.d.ts
// declare function e(): core.Express; 最後導出了 export = e;  全部執行這個函數就能夠獲得實例
const server = express()

// 須要作路由處理,不然打開http://localhost:3000/ 端口會報錯。
// 編寫路由作不一樣的url處理
// req 請求
// res 響應
server.get('/', (req,res)=>{
  res.send('3000') //瀏覽器會認爲返回的是 html
})
// 監聽端口
server.listen(3000, ()=>{
  console.log('執行了')
})
進入到當前目錄下,執行文件,如 node 1-express-start.js;
顯示 執行了 就說明 代碼沒有問題
直接訪問端口也是同樣的, localhost:300

node 文件進行修改,每次都須要node運行一下,能夠安裝nodemon;就能夠實時更新 npm install -g nodemon; 在啓動node 的時候能夠用 nodemon 1-express-start.jsreact

基礎的服務端渲染

使用服務器將vue實例成HTMLHTML字符串並返回webpack

vue服務器渲染器: vue-server-renderer**

npm i vue-server-renderer -S 或者 同時安裝vue npm i vue vue-server-renderer -S;確保版本相同
WX20200615-142832@2x.pnggit

  • 安裝好以後 ,測試下
首先建立文件
分爲三個步驟
1. 建立vue實例
 2. 獲取渲染器實例
 3. 用渲染器來渲染vue實例
// 建立vue實例
   const Vue = require ('vue')
   const app = new Vue({
      template:'<div>Hello</div>'
   })

 // 獲取渲染器實例
   const {createRenderer} = require('vue-server-renderer') // 獲取到工廠函數
   const renderer =createRenderer() // 就能夠獲得一個渲染器

 // 用渲染器來渲染vue實例
  // 返回的是promise,須要.then
 renderer.renderToString(app)
 .then((html)=>{
  console.log(html)
 })
 .catch((err)=>{
  console.log(err);
  
})
結果展現:
WX20200614-230858@2x.png

data-server-rendered 服務端渲染的github

如今須要作的是若是用戶刷新展現出來,結合express
在剛剛的express的get中,將上面vue編寫的代碼進行返回便可
// nodejs 代碼
const express = require('express')

const server = express()
// 建立vue實例
const Vue = require ('vue')
// 獲取渲染器實例
const {createRenderer} = require('vue-server-renderer') // 獲取到工廠函數
// 用渲染器來渲染vue實例
const renderer =createRenderer() // 就能夠獲得一個渲染器
server.get('/', (req,res)=>{
  // 每次用戶刷新 都渲染出一個全新的vue出來
  const app = new Vue({
    template:'<div>Hello~~~~哇哦~~~</div>'
  })
  // 返回的是promise,須要.then
  renderer.renderToString(app)
  .then((html)=>{
    // 直接把結果返回給瀏覽器
    res.send(html)
  })
  .catch(()=>{
    // 錯誤時 返回狀態嗎500
    res.status(500)
    res.send('Internal Server Error, 500')
  })
})
// 監聽端口
server.listen(3000, ()=>{
  console.log('執行了')
})

WX20200614-232519@2x.png

WX20200614-232747@2x.png

修改一下,使用數據展現
const app = new Vue({
    template:'<div>{{context}}</div>',
    data(){
      return {
        context:'vvvvvue'
      }
    }

WX20200614-233104@2x.png

如何實現交互呢?
若是直接在服務端寫,是否能實現呢?
template:'<div @click="onClick">{{context}}</div>',
    data(){
      return {
        context:'vue-ssr'
      }
    },
    methods: {
      onClick(){
        console.log('能夠點擊嗎')
      }
    }
頁面:
WX20200614-233747@2x.png
沒有綁定成功,緣由是:
已經轉化成字符串再發送到前端,是不可能的。因此須要 激活過程

解決路由、同構問題

路由

npm i vue-router -s
在沒有SSR的狀況下,是返回一個單例的Router實例,
WX20200615-150049@2x.pngweb

在服務端渲染的狀況下,爲了不Router污染的問題,每次請求都返回一個全新的Router。

//做爲一個工廠函數,每次用戶請求返回一個新的router實例
export default function createRouter(){
 return new VueRouter({
   mode: 'history',
   base: process.env.BASE_URL,
   routes
 })
}

如何讓服務端渲染的路由,拿到前端來用?
須要先理解構建流程:
WX20200615-151509@2x.png

仍是須要用webpack進行打包;
服務端的入口有兩個:Server entryClient enrty
——————————會生成兩個包——————————
生成文件: Server Bundle 「服務器 bundle」用於服務端首屏(不是首頁,請求的是什麼頁什麼就是首屏)渲染、Client Bundle「客戶端bundle」用於客戶端激活(生成的js代碼附加到html,新建一個vue實例,好比上面測試的點擊事件的實現)由於服務端傳過來的是字符串。前端須要激活。

代碼結構

src  
├── router  
├────── index.js # 路由
├── store  
├────── index.js # 全局狀態  
├── main.js # 建立vue實例  
├── entry-client.js # 客戶端入口,靜態內容「激活」 
└── entry-server.js # 服務端入口,首屏內容渲染
  • main.js 的調整

src/main.js

import Vue from 'vue'
import App from './App.vue'
import createRouter from './router'

Vue.config.productionTip = false

// 每一個請求獲取一個單獨的vue實例
// `調用者是entry-server(首屏渲染) 會傳遞參數是上下文對象`
export function createApp(context){
  const router = createRouter()
  const app =new Vue({
    router,
    context, // 利用context能夠拿到一些參數
    render: h => h(App)
  }).$mount('#app')
  // 導出app實例以及router實例
  return {app, router}
}
  • entry-server.js 首屏渲染

src/entry-server.js

`// 首屏渲染`
`// 在服務端執行的代碼`
import {createApp} from './main'

// 建立vue的實例
`// 調用者是renderer`
export default context =>{
  // 爲了讓renderer 能夠等待處理最後的結果,return的應該是一個promiss
  return new Promise((resolve, reject)=>{
    // 建立vue實例和路由實例
    const {app, router} =createApp(context)

    // 須要渲染首屏 就要拿到當前的url 渲染器會拿到當前的url
    // 跳轉首屏。
    // url的來源。是從請求中能夠拿到。傳遞給renderer
    router.push(context.url) // 考慮到當前頁面會存在ajax請求等異步任務處理。要等異步任務處理完在跳轉頁面

    // 監聽路由器的ready,確異步任務都完成
    router.onReady(()=>{
      //該方法把一個回調排隊,在路由完成初始導航時調用,這意味着它能夠解析全部的異步進入鉤子和路由初始化相關聯的異步組件。
      //這能夠有效確保服務端渲染時服務端和客戶端輸出的一致。
      resolve(app)
    }, reject) // 做爲onReady事件的失敗函數處理
  })
}
  • entry-client.js 客戶端激活

src/entry-client.js

`// 客戶端激活 就是用戶端的交互 好比click等
// 在瀏覽器執行的代碼`
import {createApp}~~~~ from './main'
// 建立vue實例
const {app, router}  =createApp()

//等待router就緒
router.onReady(()=>{
  //掛載激活
  app.$mount('#app')
})
entry-server 和 entry-client都用到了 main.js中的createApp 渲染實例必需要獲得vue的實例;
webpack打包
  • 安裝依賴
npm install webpack-node-externals lodash.merge \-D
  • 建立一個vue.config.js在根目錄下
// 兩個插件分別負責打包客戶端和服務端
const VueSSRServerPlugin = require("vue-server-renderer/server-plugin");
const VueSSRClientPlugin = require("vue-server-renderer/client-plugin");
const nodeExternals = require("webpack-node-externals");
const merge = require("lodash.merge");
// 根據傳入環境變量決定入口文件和相應配置項
const TARGET_NODE = process.env.WEBPACK_TARGET === "node";
const target = TARGET_NODE ? "server" : "client";

module.exports = {
  css: {
    extract: false
  },
  outputDir: './dist/'+target, // 輸出路徑  target看上面判斷
  configureWebpack: () => ({ // 輸入路徑
    // 將 entry 指向應用程序的 server / client 文件
    entry: `./src/entry-${target}.js`, // 入口
    // 對 bundle renderer 提供 source map 支持
    devtool: 'source-map',
    // target設置爲node使webpack以Node適用的方式處理動態導入,
    // 而且還會在編譯Vue組件時告知`vue-loader`輸出面向服務器代碼。
    target: TARGET_NODE ? "node" : "web",
    // 是否模擬node全局變量
    node: TARGET_NODE ? undefined : false,
    output: {
      // 此處使用Node風格導出模塊
      libraryTarget: TARGET_NODE ? "commonjs2" : undefined
    },
    // https://webpack.js.org/configuration/externals/#function
    // https://github.com/liady/webpack-node-externals
    // 外置化應用程序依賴模塊。可使服務器構建速度更快,並生成較小的打包文件。
    externals: TARGET_NODE
      ? nodeExternals({
          // 不要外置化webpack須要處理的依賴模塊。
          // 能夠在這裏添加更多的文件類型。例如,未處理 *.vue 原始文件,
          // 還應該將修改`global`(例如polyfill)的依賴模塊列入白名單
          whitelist: [/\.css$/]
        })
      : undefined,
    optimization: {
      splitChunks: undefined
    },
    // 這是將服務器的整個輸出構建爲單個 JSON 文件的插件。
    // 服務端默認文件名爲 `vue-ssr-server-bundle.json`
    // 客戶端默認文件名爲 `vue-ssr-client-manifest.json`。
    plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
  }),
  chainWebpack: config => {
    // cli4項目添加
    if (TARGET_NODE) {
        config.optimization.delete('splitChunks')
    }
      
    config.module
      .rule("vue")
      .use("vue-loader")
      .tap(options => {
        merge(options, {
          optimizeSSR: false
        });
      });
  }
};
package.json 進行調整
  • 安裝依賴:
npm i cross-env \-D
"scripts": {
    "build": "npm run build:server & npm run build:client",
    "build:client": "vue-cli-service build",
    "build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build"
  },
執行 npm run build 進行打包

WX20200615-173939@2x.png

  • dist文件

WX20200615-174131@2x.png

  • index.html 宿主文件

public/index.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">
  </head>
  <body>
      <!--vue-ssr-outlet-->
  </body>
</html>
  • 將server中的node文件進行調整

server/4-ssr.js

const express = require('express')
const app = express()

// 靜態資源服務
// 把這個路徑打開(../dist/client),讓用戶能夠下載文件
const path = require('path')
const resolve = dir => path.resolve(__dirname, dir)
// 相對路徑不可靠(../dist/client),須要用絕對路徑
app.use(express.static(resolve('../dist/client'),{index: false}))//指定根目錄,將根目錄開發給用戶看
//{index: false} 設置的目的是,由於在client 裏面有index.html; 因此就不會走下面的代碼,就會直接返回client裏面的index.html

// 渲染器: bundleRenderer, 它能夠獲取前面生成的兩個json文件
const { createBundleRenderer } = require('vue-server-renderer')
//指向絕對路徑
const bundle = resolve('../dist/server/vue-ssr-server-bundle.json')
//獲得渲染器能夠直接渲染 vue實例
const renderer = createBundleRenderer(bundle, {
  // 選項
  runInNewContext: false, // https://ssr.vuejs.org/zh/api/#runinnewcontext 文檔地址
  // 宿主文件
  template: require('fs').readFileSync(resolve("../public/index.html"), "utf-8"), // 宿主文件  utf-8的方式轉化成字符串
  clientManifest: require(resolve("../dist/client/vue-ssr-client-manifest.json")) // 客戶端清單   優化內容
})

app.get('*',async(req,res)=>{
   const context = {
     url: req.url
    }
  try{
    // 渲染獲取html
    // 建立vue實例 建立首屏 渲染出來 如今是個靜態的不能交互
    const html = await renderer.renderToString(context)
    res.send(html)//發送到 前端交互  entry-client 一掛在就能夠渲染出來
  }catch(error){
    res.status(500).send('Internal Server Error')
  }
})

app.listen(3001)
  • 代碼測試結果

WX20200616-172617@2x.png
同構成功!!!!


(注意)到當前開發進度,:每一次的項目修改,都須要從新 npm run build 和 啓動node文件

整合vuex

安裝vuex
`npm install -S vuex`  
若是是用vue-cli建立的項目,能夠用 
vue add vuex 安裝,結果同樣
  • 生成store/index.js

WX20200616-174902@2x.png

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

Vue.use(Vuex)
// `須要獨立出來`
export default function createStore(){
  return new Vuex.Store({
    state: {
      count:100
    },
    mutations: {
      add(state){
        state.count+=1
      }
    },
    actions: {
    },
    modules: {
    }
  })
}
  • 在main.js中掛在 sotre
import Vue from 'vue'
import App from './App.vue'
import createRouter from './router'
`import createStore from './store'`

Vue.config.productionTip = false

// 每一個請求獲取一個單獨的vue實例
// 調用者是entry-server(首屏渲染) 會傳遞參數是上下文對象
export function createApp(context){
  const router = createRouter()
  const store = createStore()
  const app =new Vue({
    router,
     // 利用context能夠拿到一些參數
    context,
    store, `// 掛載`
    render: h => h(App)
  }).$mount('#app')
  // 導出app實例以及router實例
  return {app, router, store}
}
  • 在Hello.vue 中添加

測試vuex的引入

<p @click="$store.commit('add')">{{$store.state.count}}</p>
數據預取

服務器端渲染的是應用程序的"快照",若是應用依賴於一些異步數據,那麼在開始渲染以前,須要先預取和解析好這些數據。
在服務端把數據準備好,帶着數據的把頁面渲染完成。

  • 首先在store/index.js 中添加一個數據的的初始化
mutations: {
  // 加初始化數據
  init(state,count){
    state.count =count
  }
},
  • 以後在sotre/index.js 利用actions 發起一個異步請求,模仿接口的請求
actions: {
      `// 一個異步請求數據 觸發init 模仿一個接口`
      getCount({commit}){
        return new Promise((reslove)=>{
          setTimeout(() => {
            commit('init', Math.random()*100) // 生成隨機數做爲初始值
            reslove()
          }, 1000);
        })
      }
    },
  • 接下來在須要調用數據的路由對應頁面來作數據預取邏輯 Hello.vue
export default {
  name: 'Hello',
  asyncData({store, router}) {
  console.log(router, 'asyncDtata-router')
    return store.dispatch('getCount')
  }
}

asyncData它使得你可以在渲染組件以前異步獲取數據。 asyncData方法會在組件(限於頁面組件)每次加載以前被調用。它能夠在服務端或路由更新以前被調用。

  • 首屏處理數據在entry-server.js
// 首屏渲染
//在服務端執行的代碼
import {createApp} from './main'

//建立vue的實例
// 調用者是renderer
export default context =>{
  // 爲了讓renderer 能夠等待處理最後的結果,return的應該是一個promiss
  return new Promise((resolve, reject)=>{
    // 建立vue實例和路由實例
    `const {app, router,store} =createApp(context)`

    // 須要渲染首屏 就要拿到當前的url 渲染器會拿到當前的url
    // 跳轉首屏。
    // url的來源。是從請求中能夠拿到。傳遞給renderer
    router.push(context.url) // 考慮到當前頁面會存在ajax請求等異步任務處理。要等異步任務處理完在跳轉頁面
    // 監聽路由器的ready,確異步任務都完成
    router.onReady(()=>{
      //該方法把一個回調排隊,在路由完成初始導航時調用,這意味着它能夠解析全部的異步進入鉤子和路由初始化相關聯的異步組件。
      //這能夠有效確保服務端渲染時服務端和客戶端輸出的一致。

      `// 首先處理異步的數據,以後在存放到渲染中
      // 因此須要匹配組建中是否存在asyncData選項
      const matchedComponents =router.getMatchedComponents() // 獲取url匹配 到全部匹配的組建數組`

      //用戶是瞎輸入的地址,可能會致使matchedComponents獲取錯誤 404
      if(!matchedComponents.length){
        return reject({code:404})
      }

      `//須要遍歷一下數組 看組建是否有匹配asyncData`
      Promise.all(
        matchedComponents.map(comp =>{
          // 看組建是否有匹配到asyncData
          if(comp.asyncData){  
            //  傳 store 爲了找到dispatch 對應的 actions
            // 傳 router 是爲了若是url後面帶參數 &wd=vue
            return comp.asyncData({store,route:router.currentRoute})// 異步調用 因此返回的是一個promise,每次都return,就會返回一個promise數組
          }
        })).then(()=>{
          // 數據放在store 前端不知道這一步,因此須要通知前端
          //接下來作一個約定
          // 全部的預取數據resolve以後
          //  store已進填充了當前數據狀態
          //數據須要同步到前端
          // 序列化操做,轉化成字符串 前端使用window.__INITIAL_STATE__獲取
          // 賦值給 context.state; 是一個約定,
          context.state = store.state
          resolve(app)
        }).catch(reject) // 捕獲異常
    }, reject) // 做爲onReady事件的失敗函數處理
  })
}

在entry-client.js
恢復store

const {app, router, store}  =createApp()

if(window.__INITIAL_STATE__){
  console.log(window.__INITIAL_STATE__, 'window.__INITIAL_STATE__');
  // 恢復state
  store.replaceState(window.__INITIAL_STATE__)
}

WX20200616-222307@2x.png
WX20200616-223053@2x.png
服務端渲染的時候直接生成的,反序列化以後直接生成一個字符串插入這裏,在前端一執行就變成對象了。


  • About做爲首頁,刷新後返回home頁面,asyncData沒有執行

若是,路由切換到http://localhost:3001/about在About刷新再切回首頁,就不會走服務器的數據,而是本地的state數據。
About刷新結果:
WX20200616-222622@2x.png

切回來會變成這樣:
WX20200616-222721@2x.png

問題

只是解決首屏加載數據的問題,沒有解決在客戶端路由切換的問題。
若是在客戶端的組建裏面也發現asyncData這個配置項,也須要執行

思路

加入全局混入 mixin
在main.js 中 混合式mixin

// 添加全局混入 mixin
// 混入到 beforeMount鉤子中
// 服務端中不會被觸發beforeMount,由於對服務端來講直接渲染頁面,不存在Dom掛在,因此不會觸發這個鉤子
// 因此只會在客戶端執行 beforMount
Vue.mixin({
  beforeMount() { // 這個鉤子執行的時候vue實例已經存在了,在前端已經掛載過了。因此能夠從this中去獲取sotre
    const {asyncData} =this.$options
    if(asyncData){
      //存在就調用
      asyncData(
        {
          store: this.$store, // this 指的是vue實例
          route: this.$route
        }
      )//這裏須要傳參數
    }
  },
})

OK~關於 初識vue 的ssr服務端渲染就到這裏了。
可能會存在錯別字,可是不影響知識的傳遞,哈哈哈哈哈哈!
有問題隨時留言,咱們一塊兒探討和進步,謝謝

一塊兒加油把 Yes ok!

學習資源

原生:vue ssr https://ssr.vuejs.org/zh/
框架:nuxt.js https://nuxtjs.org/
github: https://github.com/speak44/ss...

相關文章
相關標籤/搜索