實現ssr服務端渲染demo

来源:https://www.cnblogs.com/yanxiafei/archive/2019/08/13/11346829.html
-Advertisement-
Play Games

最近在研究SSR伺服器端渲染,自己寫了的小demo。 項目佈局 註:以防版本不對應產生的問題。package.json我也把放出來了,不過在文章的最後面 上圖是Vue官方的SSR原理介紹圖片。從這張圖片,我們可以知道:我們需要通過Webpack打包生成兩份bundle文件: Client Bundl ...


最近在研究SSR伺服器端渲染,自己寫了的小demo。

項目佈局

├── build                                       // 配置文件
│   │── webpack.base                            // 公共配置
│   │── webpack.client                          // 生成Client Bundle的配置
│   │── webpack.server                          // 生成Server Bundle的配置
├── dist                                        // 項目打包路徑
├── public                                      // 模板文件
│   │── index.html                              // Client模板html文件
│   │── index.ssr.html                          // Server模板html文件
├── src                                         // 源碼目錄
│   ├── assets                                  // 圖片目錄
│   ├── components                              // 組件
│   │   ├── Bar.vue                             // Bar測試組件
│   │   ├── Foo.vue                             // Foo測試組件
│   │── App.vue                                 // Vue應用的根組件
│   │── main.js                                 //  入口基礎文件
│   ├── client-entry.js                         // 瀏覽器環境入口
│   ├── server-entry.js                         // 伺服器環境入口
│   │   ├── router.js                           // 路由配置
│   │   ├── store.js                            // vuex的狀態管理
├── favicon.ico                                 // 圖標

註:以防版本不對應產生的問題。package.json我也把放出來了,不過在文章的最後面

上圖是Vue官方的SSR原理介紹圖片。從這張圖片,我們可以知道:我們需要通過Webpack打包生成兩份bundle文件:

  • Client Bundle,給瀏覽器用。和純Vue前端項目Bundle類似

  • Server Bundle,供服務端SSR使用,一個json文件

技術棧

vue + vuex + vue-router + webpack +ES6/7 + less + koa

拆分 Webpack 打包配置

構建文件目錄

webpack.base.js 是公共配置,配置如下:

// 基礎的webpack配置
// webpack專用配置
const path = require('path')
const VueLoader = require('vue-loader/lib/plugin')
const resolve = dir => {
    return path.resolve(__dirname, dir)
}
module.exports = {
    output: {
        filename: '[name].bundle.js',
        path: resolve('../dist')
    },
    resolve: {
        extensions: ['.js', '.vue']
    },
    module: {
        rules: [{
                test: /\.js$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env']
                    }
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: ['vue-style-loader', 'css-loader']
            },
            {
                test: /\.vue$/,
                use: 'vue-loader'
            },
            {
                test: /\.less$/,
                loader: 'vue-style-loader!css-loader!less-loader'
            },
            {
                test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        limit: 300000,
                        name: '[name].[ext]?[hash]'
                    }
                }
            }
        ]
    },
    plugins: [
        new VueLoader()
    ]
}

webpack.client.js 是生成Client Bundle的配置,配置如下:

const merge = require('webpack-merge')
const base = require('./webpack.base')
const path = require('path')
const resolve = dir => {
    return path.resolve(__dirname, dir)
}
const ClientRenderPlugin = require('vue-server-renderer/client-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = merge(base, {
    entry: {
        client: resolve('../src/client-entry.js')
    },
    plugins: [
        new ClientRenderPlugin(),
        new HtmlWebpackPlugin({
            filename: 'index.html',
            template: resolve('../public/index.html')
        })
    ]
})

webpack.server.js是生成Server Bundle的配置,配置如下:

const merge = require('webpack-merge')
const base = require('./webpack.base')
const path = require('path')
const resolve = dir => {
    return path.resolve(__dirname, dir)
}
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = merge(base, {
    entry: {
        server: resolve('../src/server-entry.js')
    },
    target: 'node', // 用給node來使用
    // devtool: 'source-map',
    output: {
        libraryTarget: 'commonjs2'
    },
    plugins: [
        new VueSSRServerPlugin(),
        new HtmlWebpackPlugin({
            filename: 'index.ssr.html',
            template: resolve('../public/index.ssr.html'),
            excludeChunks: ['server'] // 排查某個模塊
        }),

    ]
})

下圖是我的項目文件目錄

components 目錄下是組件
App.vue Vue應用的根組件
client-entry.js 瀏覽器環境入口
server-entry.js 伺服器環境入口
main.js 入口基礎文件
router.js 路由配置文件
store.js vuex狀態管理文件

前端渲染 Demo

前端渲染demo部分比較簡單,就包含兩個組件:Foo 和 Ba

Foo.vue

<template>
    <div >
        <p @click="handleClick">Foo--{{num}}-點擊測試js是否正常</p>
        <p>{{this.$store.state.name}}</p>     
        <p>-----圖片分割線----</p>
        <img :src="logo" alt="">
        <img src="../assets/images/kfbg.png" alt="">
    </div>
</template>
<script>
export default {
    data(){
        return {
            num:0,
            logo: require('../assets/images/kfbg.png')
        }
    },
    asyncData(store) {
        // asyncData 方法只在服務端執行,並且只在頁面組件中執行
        return store.dispatch('changeName')
    },
    mounted: function() {
        this.$store.dispatch('changeName')
    },
    methods: {
        handleClick() {
           this.num ++;
        }
    }
}
 // vue 優化 pwa+ ssr 實現預緩存效果 //vue多頁面一般都用ssr寫 //學而思、掘金、新聞類網站用的的ssr
</script>

Bar.vue

<template>
    <div>
       bar
       <p>Vue.js 是構建客戶端應用程式的框架。預設情況下,可以在瀏覽器中輸出 Vue 組件,進行生成 DOM 和操作 DOM。然而,也可以將同一個組件渲染為伺服器端的 HTML 字元串,將它們直接發送到瀏覽器,最後將這些靜態標記"激活"為客戶端上完全可交互的應用程式。
    </div>
</template>

App.vue

<template>
    <div id="app">
        <p class="nav">
            <router-link :class="{'currentClass':path=='/'}" to="/">foo</router-link> 
            <router-link :class="{'currentClass':path=='/Bar'}" to="/Bar">Bar</router-link>
        </p>
       <router-view></router-view>
    </div>
</template>
<script>
export default {
    // data(){
    //     return {
    //         path: this.$store.state.route.path
    //     }
    // },
    computed: {
        path(){
            return this.$store.state.route.path
        }
    }
}
</script>
<style lang="less" scope>
    .nav{
        text-align: center;
        display: flex;
        align-items: center;
      a{
          flex: 2;
          background: #f5f5f5;
           text-decoration:none; 
           color: #333;
           &.currentClass{
               background:#f43553;
               color: #fff;
           }
      }
    }
</style>

router.js

import Vue from 'vue'
import Foo from './components/Foo.vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

export default () => {
    const router = new VueRouter({
        mode: 'history',
        routes: [
            {
                path: '/',
                component: Foo
            },
            {
                path: '/bar',
                component: () => import('./components/Bar.vue')
            },
        ]
    })
    return router
}

store.js

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

export default () => {
    const store = new Vuex.Store({
        state: {
            name: ''
        },
        mutations: {
            changeName(state) {
                state.name = 'yxf'
            }
        },
        actions: {
            changeName({ commit }) {
                return new Promise((resolve, reject) => {
                    setTimeout(() => {
                        commit('changeName')
                        resolve()
                    })
                })
            }
        }
    })
    if(typeof window !== 'undefined' && window.__INITIAL_STATE__) {
        store.replaceState(window.__INITIAL_STATE__)
    }
    return store
}

拆分 JS 入口

在前端渲染的時候,只需要一個入口 main.js。現在要做後端渲染,就得有兩個 JS 文件:client-entry.js 和 server-entry.js 分別作為瀏覽器和伺服器的入口。

main.js基礎文件

//入口文件
import Vue from 'vue'
import createRouter from './router'
import App from './App.vue'
import createStore from './store'
import { sync } from 'vuex-router-sync' // 把當前VueRouter狀態同步到Vuex中
export default () => {
    const router = createRouter()
    const store = createStore()
    sync(store, router)
    const app = new Vue({
        router,
        store,
        render: h => h(App)
    })
    return { app, router, store }
}

client-entry.js 瀏覽器入口

import createApp from './main'
const { app,  router } = createApp()
router.onReady(() => {
    app.$mount('#app')
})

server-entry.js 伺服器入口

import createApp from './main'
// 伺服器需要調用當前這個文件產生一個vue實例

export default context => {
    // 涉及到非同步組件的問題
    return new Promise((resolve, reject) => {
        const { app, router, store } = createApp()
        // 設置路由
        router.push(context.url)
        // 返回的實例應跳轉到 /     如/bar
        router.onReady(() => {
            const matchs = router.getMatchedComponents()
            console.log(matchs.length)
            if(matchs.length === 0) {
                reject({ code: 404 })
            }
            // matchs匹配到所有的組件,整個都在服務端執行的
            Promise.all(
                matchs.map(component => {
                    if(component.asyncData) {
                        // asyncData 是在服務端調用的
                        return component.asyncData(store)
                    }
                })
            ).then(() => {
                // 以上all中的方法,會改變store中的state
                context.state = store.state;// 把vuex的狀態掛載到上下文中,會將狀態掛到window上
                resolve(app)
            }).catch(reject)
        },reject)
    })
}
// 伺服器端配置好後,需要導出給node使用

模板文件

index.html client模板html文件

<!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>
    <div id="app"></div>
    <!--vue-ssr-outlet-->
</body>
</html>

index.ssr.html server模板html文件

<!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>
    <!--vue-ssr-outlet-->
</body>
</html>

編寫服務端渲染主體邏輯

Vue SSR 依賴於包 vue-server-render,它的調用支持兩種入口格式:createRenderer 和 createBundleRenderer,前者以 Vue 組件為入口,後者以打包後的 JS 文件為入口,本文采取後者。

server.js

const Koa = require('koa')
const Router = require('koa-router')
const server = new Koa()
const router = new Router()
const path = require('path')
const static = require('koa-static')
const fs = require('fs')
const { createBundleRenderer } = require('vue-server-renderer')

const serverBundle = require('./dist/vue-ssr-server-bundle.json')
//渲染打包後的結果
const template = fs.readFileSync(path.resolve(__dirname, './dist/index.ssr.html'), 'utf8')
//客戶端manifest.json
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
const render = createBundleRenderer(serverBundle, {
    template, // 模板里必須要有 vue-ssr-outlet
    clientManifest
})
router.get('/',async ctx => {
    ctx.body = await new Promise((resolve, reject) => {
        render.renderToString({url: '/'}, (err, data) => {
            if(err) reject(err);
            resolve(data);
        })  
    }) 
   })
server.use(router.routes())


// koa 靜態服務中間件
server.use(static(path.resolve(__dirname,'./dist')))

server.use( async ctx => {
    try{
        ctx.body = await new Promise((resolve, reject) => {
            render.renderToString({ url: ctx.url }, (err, data) => {
                if(err) reject(err)
                resolve(data)
            })
        })
    }catch (e) {
        ctx.body = '404'
    }
}) 


server.listen(3002, () => {
    console.log('伺服器已啟動!')
  })

項目地址:https://github.com/xiaonizi66/vue-ssr-demo

package.json

{
  "name": "ssr",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "client:dev": "webpack-dev-server --config ./build/webpack.client.js --mode development",
    "client:build": "webpack --config ./build/webpack.client.js --mode production",
    "server:build": "webpack --config ./build/webpack.server.js --mode production"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "koa": "^2.7.0",
    "koa-router": "^7.4.0",
    "koa-static": "^5.0.0",
    "vue": "^2.6.10",
    "vue-loader": "^15.7.1",
    "vue-router": "^3.1.2",
    "vue-server-renderer": "^2.6.10",
    "vuex": "^3.1.1",
    "vuex-router-sync": "^5.0.0"
  },
  "devDependencies": {
    "@babel/core": "^7.5.5",
    "@babel/preset-env": "^7.5.5",
    "babel-loader": "^8.0.6",
    "css-loader": "^3.2.0",
    "html-webpack-plugin": "^3.2.0",
    "less": "^3.9.0",
    "less-loader": "^5.0.0",
    "url-loader": "^2.1.0",
    "vue-style-loader": "^4.1.2",
    "vue-template-compiler": "^2.6.10",
    "webpack": "^4.39.1",
    "webpack-cli": "^3.3.6",
    "webpack-dev-server": "^3.8.0",
    "webpack-merge": "^4.2.1"
  }
}

最終渲染效果:

項目運行

git clone https://github.com/xiaonizi66/vue-ssr-demo
npm install
npm run server:build
npm run cilent:build
nodemon server.js

也可在build後面加上 -- --watch 如:npm run server:build -- --watch 用來監聽


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、swich case判斷語句eg // 只有exp和值1或值2類型相同時,才能執行,否則會跳到default關鍵字處,執行對應代碼段; 註:default關鍵字:規定不存在 case 匹配時所運行的代碼。 二、if else if 註:if中條件可以是有多個,用&&或||隔開; 三、if els ...
  • 迴圈語句分類{ for while do ( ) while } 一、for迴圈語句和for迴圈的嵌套 for迴圈格式eg: 表達式“i=1”共運行1次,在迴圈之前運行; 表達式“i<=100”是判斷能否滿足執行迴圈體的條件,如果滿足,迴圈多少次就執行多少次,不滿足時跳出迴圈體; 表達式“i++”進 ...
  • 問題描述: 使用vue-cli創建的項目,開發地址是localhost:8080,由於後臺開發不同的模塊,導致每個模塊請求的ip和埠號不一致 例如:http://192.168.10.22:8081 或者 http://192.168.10.30:9999等 解決問題: 在vue.config.j ...
  • 一、for迴圈 1.單個for迴圈: for(初始值;條件;增量){ 語句 } 初始值:無條件的執行第一個表達式 條件:是判斷是否能執行迴圈體的條件 增量:做增量的操作 //迴圈輸出1~100之間數字的和 var sum=0; for(var i=1;i<=100;i++){ sum= sum+i; ...
  • github 獲取更多資源 https://github.com/ChenMingK/WebKnowledges Notes 線上閱讀:https://www.kancloud.cn/chenmk/web knowledges/1080520 垃圾回收機制 對垃圾回收演算法而言,其核心思想就是如何判斷 ...
  • Introduction 技術棧:react + redux + react router + express + Nginx 練習點: redux 連接 react router 路由跳轉 scss 樣式書寫 容器組件與展示組件的設計 express 腳手架項目結構設計 用戶信息持久化(cooki ...
  • 上面這張圖出自 "冴羽的博客" ,這張圖已經能很好地解釋原型與原型鏈了,其涉及到的屬性如下: : 每個函數都有一個 prototype(原型)屬性,這個屬性是一個指針,指向一個對象,而這個對象的用途是包含可以由特定類型的所有實例共用的屬性和方法,如果使用這個函數生成了實例,那麼稱這個對象為所有實例的 ...
  • @[toc] 推薦閱讀 "掘金 前端模塊化" "模塊化七日談" 部分內容摘自《移動 Web 前端高效開發實踐》 iKcamp 著 為什麼需要模塊化? JavaScript 發展初期,代碼簡單地堆積在一起,只要能順利地從上往下一次執行即可。但隨著網站越來越複雜,實現網站功能的 JavaScript 代 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...