vue服务端渲染实践
首先什么是ssr?不是玩游戲抽的ssr卡牌,而是server side render 服務端渲染
什么是客戶端渲染?就是在瀏覽器渲染dom結構和數據
什么是服務端渲染?就是在服務端把dom結構渲染好,把想要展示的數據都插入想展示的地方,將資源一次性梭哈給瀏覽器
下面用兩個圖說明一下傳統的vue spa客戶端渲染和vue服務端渲染的區別
spa:
spa的優點:
開發效率高
服務端壓力小
spa的缺點:
seo效果差(因為dom結構里的文字,圖片都是vue異步渲染出來的,首屏加載并沒有這些東西,搜索引擎的爬蟲往往根據文字內容,圖片的title等信息抓取頁面信息)
首屏加載速度慢(因為服務器首次只給前端返回了一個index.html,里面只包含vue的根組件例如#app,頁面的內容加載之前還需要去加載vue.js等其他的資源,然后通過異步ajax請求得到頁面數據,再通過vue的數據更新機制重新渲染頁面)
ssr:
ssr的優點:
首屏渲染速度快
seo比較友好
ssr的缺點:
開發體驗不如spa,需要借助nodejs
服務端壓力大
目前vue常用的ssr模式有兩種開發方式,第一種是使用一些ssr框架,例如nuxt.js,第二種是在服務端單獨用vue實現ssr,nuxt的開發大家可以參照nuxt的官網https://www.nuxtjs.cn/
這里面給大家介紹一下不使用框架,直接使用nodejs+vue實現ssr
敲黑板!!正文開始
首先用一個小例子實現vue的ssr
1在本地使用vue-cli新建工程 ,這里使用的vue-cli3,對腳手架不了解的同學需要去官網了解一下
vue create ssr-app
2在根目錄下新建server文件夾,創建一個01-vue-ssr-demo.js文件,在里面編寫node代碼
在這之前需要安裝下面幾個文件
npm install vue-server-renderer -s //服務端創建dom節點用 npm install vue-router -s //路由 npm install express -s //express框架 npm install nodemon -s //熱更新node服務 啟動項目時使用nodemon 代替 node
在01-vue-ssr-demo.js中插入下面代碼
// 創建一個express實例
const express = require('express')
const app = express()
// 導入vue
const Vue = require('vue')
// 創建渲染器
const { createRenderer } = require('vue-server-renderer')
const renderer = createRenderer()
// 導入路由
const Router = require('vue-router')
Vue.use(Router)
// 路由:問題2:由express在管理
app.get('*', async (req, res) => {
// 創建一個路由器實例
const router = new Router({
routes: [
{ path: '/', component: { template: '<div>Index</div>' } },
{ path: '/detail', component: { template: '<div>detail</div>' } },
]
})
// 構建渲染頁面內容
// 問題1:沒辦法交互
// 問題3:同構開發問題
const vm = new Vue({
router,
data() {
return {
name: 'ssr-simple-demo'
}
},
template: `
<div>
<router-link to="/">index</router-link>
<router-link to="/detail">detail</router-link>
<div>{{name}}</div>
<router-view></router-view>
</div>
`
})
try {
// 路由跳轉
router.push(req.url)
// 渲染: 得到html字符串
const html = await renderer.renderToString(vm)
// 發送回前端
res.send(html)
} catch (error) {
res.status(500).send('服務器內部錯誤')
}
})
// 監聽端口
app.listen(3000)
使用nodemon命令運行服務
nodemon server/01-vue-ssr-demo.js
在瀏覽器輸入localhost:3000打開,可以看到下面的頁面
并且index detail點擊之后可以切換內容
上面我們實現了一個簡單的vue ssr應用,但是,如果你在dom上通過vue命令@click綁定了事件,在頁面上點擊是不會觸發的,原因就是后臺返回到前端的所有都是字符串,在前端接收到的也是一段普普通通的html代碼,在頁面上選擇查看網頁源代碼可以看到如下內容
并沒有事件的綁定,點擊之后更不會觸發事件,那么我們怎么能讓返回到前端的頁面做一次激活?下面給大家講vue ssr的折中方案:原理是node服務根據用戶請求的地址,給用戶返回對應路由的首屏的資源,之后用戶的一切操作都交由vue去管理,在前端執行掛載,初始化,這個過程我們一般叫做zhushui。通過zhushui之后,我們的頁面就可以像正常的vue頁面一樣執行點擊事件了。
代碼結構如下:
標注顏色的就是核心代碼部分,我們要對之前的代碼做一些修改
具體代碼如下:
1 router.js-管理路由的邏輯
和原來路由不同的地方是這里采用的工廠模式,每一次請求都返回一個router實例,后面要說的new Vue和Vuex的創建都要用這種返回實例的方法。
原因是現在我們編寫的代碼是在服務端,每一個人請求的地址都不一樣,如果同時有3個人請求了三個頁面,但是我只創建一個router對象返回的話肯定有2個人接收到的router是錯誤的,所以這里面用了工廠模式返回了一個路由實例,保證每次請求得到的router是不受污染的
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
Vue.use(Router)
// 工廠函數,每次請求返回一個Router實例
export function createRouter() {
return new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
}
]
})
}
2 store.js - 全局狀態管理,和router.js一樣,這里每次請求都會返回一個vuex的實例,原因同上
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// 工廠函數
export function createStore() {
return new Vuex.Store({
state: {
count: 108
},
mutations: {
add(state) {
state.count += 1;
},
// 加一個初始化
init(state, count) {
state.count = count;
},
},
actions: {
// 加一個異步請求count的action
getCount({ commit }) {
return new Promise(resolve => {
setTimeout(() => {
commit("init", Math.random() * 100);
resolve();
}, 1000);
});
},
},
})
}
3 main.js - 創建router實例/vuex實例/vue實例的方法 (并不是在這里直接就調用了,后面會在入口文件調用這里的方法創建實例)
import Vue from "vue";
import App from "./App.vue";
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,
});
}
},
});
// 需要每個請求返回一個Vue實例
export function createApp(context) {
const router = createRouter()
const store = createStore()
const app = new Vue({
router,
store,
context, // 用于和外面renderer交互
render: h => h(App)
})
return {app,router,store}
}
4 entry-server.js -服務端入口文件,作用:創建vue實例,創建router實例,創建store實例,返回 vue實例 (app),將來和后端渲染器 vue renderer打交道
import { createApp } from "./main"
// 首屏渲染
// 將來和渲染器打交道
// 創建vue實例
export default context => {
const {app, router, store} = createApp(context)
return new Promise((resolve, reject) => {
// 跳轉首屏地址去
router.push(context.url)
// 等待路由就緒
router.onReady(() => {
// 判斷是否存在asyncData選項
// 獲取匹配路由相關組件
const comps = router.getMatchedComponents()
// 遍歷它們,并執行可能存在的asyncData
Promise.all(comps.map(comp => {
if (comp.asyncData) {
return comp.asyncData({
store,
route: router.currentRoute
})
}
})).then(() => {
// 數據已經存入store,只需要序列化它,傳到前端在復原
// 設置到上下文中的state,renderer將來會轉換它
context.state = store.state
// 返回實例
resolve(app)
})
.catch(reject)
}, reject)
})
}
5 entry-client.js - 客戶端入口文件 作用:通過app.$mount 激活頁面vue
import { createApp } from "./main";
// 激活
const { app, router, store } = createApp()
// 還原store.state
// renderer會把它放到window.__INITIAL_STATE__
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
router.onReady(() => {
app.$mount('#app')
})
6 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">
<!-- 1.刪掉之前動態title -->
<title>vue-study</title>
</head>
<body>
<!-- 2.把宿主元素變成一個注釋 -->
<!--vue-ssr-outlet-->
</body>
</html>
7 app.vue和添加兩個測試的頁面vue文件
app.vue:
<template>
<div id="app">
<p>{{$store.state.count}}</p>
<nav>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</nav>
<router-view></router-view>
</div>
</template>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
view/About.vue
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
view/Home.vue
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>
<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
export default {
name: 'home',
components: {
HelloWorld
},
asyncData({ store, route }) { // 約定預取邏輯編寫在預取鉤子asyncData中
// 觸發 action 后,返回 Promise 以便確定請求結果
return store.dispatch("getCount");
}
}
</script>
8 ssr.js服務端的啟動文件 server/ssr.js 服務端代碼
// 創建一個express實例
const express = require('express')
const app = express()
// 獲取絕對地址
const resolve = dir => require('path').resolve(__dirname, dir)
// 靜態文件服務
// 開發dist/client目錄,關閉默認的index頁面打開功能
app.use(express.static(resolve('../dist/client'), {index: false}))
// 創建渲染器
const { createBundleRenderer } = require('vue-server-renderer')
// 參數1:服務端bundle
const bundle = resolve('../dist/server/vue-ssr-server-bundle.json')
const renderer = createBundleRenderer(bundle, {
runInNewContext: false, // https://ssr.vuejs.org/zh/api/#runinnewcontext
template: require('fs').readFileSync(resolve("../public/index.html"), "utf-8"), // 宿主文件
clientManifest: require(resolve("../dist/client/vue-ssr-client-manifest.json")) // 客戶端清單
})
// 只做一個件事,渲染
app.get('*', async (req, res) => {
try {
const context = {
url: req.url
}
// 渲染: 得到html字符串
const html = await renderer.renderToString(context)
// 發送回前端
res.send(html)
} catch (error) {
res.status(500).send('服務器內部錯誤')
}
})
// 監聽端口
app.listen(3000)
9 webpack的配置
首先需要安裝webpack插件
npm install webpack-node-externals lodash.merge -D
根目錄新增vue.confg.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,
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
});
});
}
};
10 在package 里增加打包命令,打包的時候使用npm run build,會自動執行build:client和build:server
安裝依賴
npm i cross-env -D
代碼:
"scripts": {
"serve": "vue-cli-service serve",
"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"
},
打包完之后會在dist下面生成client文件夾和server文件夾
client下面的vue-ssr-client-manifest.json的作用是:描述了一些客戶端的信息,all數組里面的是將來要返回給前端要加載的一些資源
server下面的vue-ssr-server-bundle.json的作用是:將來'vue-server-renderer'會以這個json為工作目錄去創建dom
注意,因為是服務端渲染的,每一次修改都需要執行build命令以更新代碼
編寫完上面的代碼,執行node命令起服務 nodemon/server/ssr.js
然后在瀏覽器打開localhost:3000,會看到如下頁面,右面加載的資源就是vue-ssr-client-manifest.json 里面的all的文件
回顧整個編碼過程我們發現我們實際編寫vue代碼的部分并沒有改變什么,和spa開發有著一樣的 同構體驗,主要的改變有下面幾點:
1 vue-router/vuex/vue實例的創建都是使用的工廠函數,每一次請求服務端都會創建一個實例,防止數據污染
2 增加了兩個入口文件 entry-client.js/entry-server.js
3 增加了webpack的配置,產出dist/server 和dist/client相關的文件,供renderer插件使用
4 增加了服務端代碼-核心內容就是使用renderer函數渲染dom,并返回給客戶端,其中包含激活前端頁面的js代碼
最后總結一下對服務端渲染的理解以及使用場景
服務端渲染相比于spa應用,主要為我們提供了2點優勢,一個是快速的首屏加載,一個是seo引擎搜索
一般來說,對于首屏加載速度要求較高的場景是移動端的頁面,特別是hybrid混合開發的應用,在app里嵌入webview的方式,如果首屏加載時間過長的話會出現一個白屏,會讓用戶從比較流暢的原生頁面切換到了一個白屏頁面,如果是弱網情況白屏時間過長的話極大的降低了用戶體驗;另一種是微信公眾號/企業微信的第三方應用開發,點擊微信里的鏈接跳轉到我們自己服務器的url的過程中,除了我們自己的請求還有很多微信重定向等需要耗時的操作,白屏的時間更加的長。我們無法通過前端代碼控制客戶端加載頁面的白屏期間的操作,也就是無法加載骨架屏或者加上loading提高用戶體驗。因此移動端的hybrid開發和微信開發在技術選型的時候最好使用ssr的開發模式。
除了移動端之外,一些大型官網開發要求首屏渲染速度和搜索引擎seo抓取的時候也需要使用服務端渲染,搜索引擎爬蟲爬取網站并排名的最重要因素是網站首屏的加載速度,其次是里面的關鍵字,圖片的title,meta里的keysords等,而ssr模式剛好解決了這兩個問題。對于一些對首屏速度沒有要求的網站,且數據交互比較多的網站,例如后臺管理系統,就完全不需要ssr的開發模式。
當然,采用ssr模式的架構開發也有缺點,一是增加了代碼的復雜度,新手開發起來成本還是比較高的。二是在服務端渲染增加了服務端的壓力,如果服務器的配置比較低,而用戶數量很大的時候,服務器的cpu很容易滿載,如果公司有增加服務器的預算可以采用負載均衡,或者采用node server / nginx做一些緩存,如果登錄用戶有效的話將緩存的頁面直接返回給前端而不做服務端渲染。如果開發周期比較充裕,也可以做一些兼容性的判斷,用node監聽當前cpu的使用量,如果達到自己設定的閾值,那么就不用服務端渲染,返回給用戶spa的應用,如果cpu占用的比較少,正常返回服務端渲染的頁面。
上面的例子只是給大家提供vue ssr開發的模式的一個思路,如果公司新開發的項目,如果需要采用ssr,還是建議大家采用nuxt.js。
總結
以上是生活随笔為你收集整理的vue服务端渲染实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 钢件攻丝用什么丝锥?
- 下一篇: 向SqlServer数据库插入数据