vue服务端渲染实践
首先什么是ssr?不是玩游戲抽的ssr卡牌,而是server side render 服務(wù)端渲染
什么是客戶端渲染?就是在瀏覽器渲染dom結(jié)構(gòu)和數(shù)據(jù)
什么是服務(wù)端渲染?就是在服務(wù)端把dom結(jié)構(gòu)渲染好,把想要展示的數(shù)據(jù)都插入想展示的地方,將資源一次性梭哈給瀏覽器
下面用兩個圖說明一下傳統(tǒng)的vue spa客戶端渲染和vue服務(wù)端渲染的區(qū)別
spa:
spa的優(yōu)點(diǎn):
開發(fā)效率高
服務(wù)端壓力小
spa的缺點(diǎn):
seo效果差(因?yàn)閐om結(jié)構(gòu)里的文字,圖片都是vue異步渲染出來的,首屏加載并沒有這些東西,搜索引擎的爬蟲往往根據(jù)文字內(nèi)容,圖片的title等信息抓取頁面信息)
首屏加載速度慢(因?yàn)榉?wù)器首次只給前端返回了一個index.html,里面只包含vue的根組件例如#app,頁面的內(nèi)容加載之前還需要去加載vue.js等其他的資源,然后通過異步ajax請求得到頁面數(shù)據(jù),再通過vue的數(shù)據(jù)更新機(jī)制重新渲染頁面)
ssr:
ssr的優(yōu)點(diǎn):
首屏渲染速度快
seo比較友好
ssr的缺點(diǎn):
開發(fā)體驗(yàn)不如spa,需要借助nodejs
服務(wù)端壓力大
目前vue常用的ssr模式有兩種開發(fā)方式,第一種是使用一些ssr框架,例如nuxt.js,第二種是在服務(wù)端單獨(dú)用vue實(shí)現(xiàn)ssr,nuxt的開發(fā)大家可以參照nuxt的官網(wǎng)https://www.nuxtjs.cn/
這里面給大家介紹一下不使用框架,直接使用nodejs+vue實(shí)現(xiàn)ssr
敲黑板!!正文開始
首先用一個小例子實(shí)現(xiàn)vue的ssr
1在本地使用vue-cli新建工程 ,這里使用的vue-cli3,對腳手架不了解的同學(xué)需要去官網(wǎng)了解一下
vue create ssr-app
2在根目錄下新建server文件夾,創(chuàng)建一個01-vue-ssr-demo.js文件,在里面編寫node代碼
在這之前需要安裝下面幾個文件
npm install vue-server-renderer -s //服務(wù)端創(chuàng)建dom節(jié)點(diǎn)用 npm install vue-router -s //路由 npm install express -s //express框架 npm install nodemon -s //熱更新node服務(wù) 啟動項(xiàng)目時使用nodemon 代替 node
在01-vue-ssr-demo.js中插入下面代碼
// 創(chuàng)建一個express實(shí)例
const express = require('express')
const app = express()
// 導(dǎo)入vue
const Vue = require('vue')
// 創(chuàng)建渲染器
const { createRenderer } = require('vue-server-renderer')
const renderer = createRenderer()
// 導(dǎo)入路由
const Router = require('vue-router')
Vue.use(Router)
// 路由:問題2:由express在管理
app.get('*', async (req, res) => {
// 創(chuàng)建一個路由器實(shí)例
const router = new Router({
routes: [
{ path: '/', component: { template: '<div>Index</div>' } },
{ path: '/detail', component: { template: '<div>detail</div>' } },
]
})
// 構(gòu)建渲染頁面內(nèi)容
// 問題1:沒辦法交互
// 問題3:同構(gòu)開發(fā)問題
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 {
// 路由跳轉(zhuǎn)
router.push(req.url)
// 渲染: 得到html字符串
const html = await renderer.renderToString(vm)
// 發(fā)送回前端
res.send(html)
} catch (error) {
res.status(500).send('服務(wù)器內(nèi)部錯誤')
}
})
// 監(jiān)聽端口
app.listen(3000)
使用nodemon命令運(yùn)行服務(wù)
nodemon server/01-vue-ssr-demo.js
在瀏覽器輸入localhost:3000打開,可以看到下面的頁面
并且index detail點(diǎn)擊之后可以切換內(nèi)容
上面我們實(shí)現(xiàn)了一個簡單的vue ssr應(yīng)用,但是,如果你在dom上通過vue命令@click綁定了事件,在頁面上點(diǎn)擊是不會觸發(fā)的,原因就是后臺返回到前端的所有都是字符串,在前端接收到的也是一段普普通通的html代碼,在頁面上選擇查看網(wǎng)頁源代碼可以看到如下內(nèi)容
并沒有事件的綁定,點(diǎn)擊之后更不會觸發(fā)事件,那么我們怎么能讓返回到前端的頁面做一次激活?下面給大家講vue ssr的折中方案:原理是node服務(wù)根據(jù)用戶請求的地址,給用戶返回對應(yīng)路由的首屏的資源,之后用戶的一切操作都交由vue去管理,在前端執(zhí)行掛載,初始化,這個過程我們一般叫做zhushui。通過zhushui之后,我們的頁面就可以像正常的vue頁面一樣執(zhí)行點(diǎn)擊事件了。
代碼結(jié)構(gòu)如下:
標(biāo)注顏色的就是核心代碼部分,我們要對之前的代碼做一些修改
具體代碼如下:
1 router.js-管理路由的邏輯
和原來路由不同的地方是這里采用的工廠模式,每一次請求都返回一個router實(shí)例,后面要說的new Vue和Vuex的創(chuàng)建都要用這種返回實(shí)例的方法。
原因是現(xiàn)在我們編寫的代碼是在服務(wù)端,每一個人請求的地址都不一樣,如果同時有3個人請求了三個頁面,但是我只創(chuàng)建一個router對象返回的話肯定有2個人接收到的router是錯誤的,所以這里面用了工廠模式返回了一個路由實(shí)例,保證每次請求得到的router是不受污染的
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
Vue.use(Router)
// 工廠函數(shù),每次請求返回一個Router實(shí)例
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 - 全局狀態(tài)管理,和router.js一樣,這里每次請求都會返回一個vuex的實(shí)例,原因同上
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// 工廠函數(shù)
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 - 創(chuàng)建router實(shí)例/vuex實(shí)例/vue實(shí)例的方法 (并不是在這里直接就調(diào)用了,后面會在入口文件調(diào)用這里的方法創(chuàng)建實(shí)例)
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) {
// 將獲取數(shù)據(jù)操作分配給 promise
// 以便在組件中,我們可以在數(shù)據(jù)準(zhǔn)備就緒后
// 通過運(yùn)行 `this.dataPromise.then(...)` 來執(zhí)行其他任務(wù)
this.dataPromise = asyncData({
store: this.$store,
route: this.$route,
});
}
},
});
// 需要每個請求返回一個Vue實(shí)例
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 -服務(wù)端入口文件,作用:創(chuàng)建vue實(shí)例,創(chuàng)建router實(shí)例,創(chuàng)建store實(shí)例,返回 vue實(shí)例 (app),將來和后端渲染器 vue renderer打交道
import { createApp } from "./main"
// 首屏渲染
// 將來和渲染器打交道
// 創(chuàng)建vue實(shí)例
export default context => {
const {app, router, store} = createApp(context)
return new Promise((resolve, reject) => {
// 跳轉(zhuǎn)首屏地址去
router.push(context.url)
// 等待路由就緒
router.onReady(() => {
// 判斷是否存在asyncData選項(xiàng)
// 獲取匹配路由相關(guān)組件
const comps = router.getMatchedComponents()
// 遍歷它們,并執(zhí)行可能存在的asyncData
Promise.all(comps.map(comp => {
if (comp.asyncData) {
return comp.asyncData({
store,
route: router.currentRoute
})
}
})).then(() => {
// 數(shù)據(jù)已經(jīng)存入store,只需要序列化它,傳到前端在復(fù)原
// 設(shè)置到上下文中的state,renderer將來會轉(zhuǎn)換它
context.state = store.state
// 返回實(shí)例
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.刪掉之前動態(tài)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 }) { // 約定預(yù)取邏輯編寫在預(yù)取鉤子asyncData中
// 觸發(fā) action 后,返回 Promise 以便確定請求結(jié)果
return store.dispatch("getCount");
}
}
</script>
8 ssr.js服務(wù)端的啟動文件 server/ssr.js 服務(wù)端代碼
// 創(chuàng)建一個express實(shí)例
const express = require('express')
const app = express()
// 獲取絕對地址
const resolve = dir => require('path').resolve(__dirname, dir)
// 靜態(tài)文件服務(wù)
// 開發(fā)dist/client目錄,關(guān)閉默認(rèn)的index頁面打開功能
app.use(express.static(resolve('../dist/client'), {index: false}))
// 創(chuàng)建渲染器
const { createBundleRenderer } = require('vue-server-renderer')
// 參數(shù)1:服務(wù)端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)
// 發(fā)送回前端
res.send(html)
} catch (error) {
res.status(500).send('服務(wù)器內(nèi)部錯誤')
}
})
// 監(jiān)聽端口
app.listen(3000)
9 webpack的配置
首先需要安裝webpack插件
npm install webpack-node-externals lodash.merge -D
根目錄新增vue.confg.js 代碼如下
// 兩個插件分別負(fù)責(zé)打包客戶端和服務(wù)端
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");
// 根據(jù)傳入環(huán)境變量決定入口文件和相應(yīng)配置項(xiàng)
const TARGET_NODE = process.env.WEBPACK_TARGET === "node";
const target = TARGET_NODE ? "server" : "client";
module.exports = {
css: {
extract: false
},
outputDir: './dist/'+target,
configureWebpack: () => ({
// 將 entry 指向應(yīng)用程序的 server / client 文件
entry: `./src/entry-${target}.js`,
// 對 bundle renderer 提供 source map 支持
devtool: 'source-map',
// target設(shè)置為node使webpack以Node適用的方式處理動態(tài)導(dǎo)入,
// 并且還會在編譯Vue組件時告知`vue-loader`輸出面向服務(wù)器代碼。
target: TARGET_NODE ? "node" : "web",
// 是否模擬node全局變量
node: TARGET_NODE ? undefined : false,
output: {
// 此處使用Node風(fēng)格導(dǎo)出模塊
libraryTarget: TARGET_NODE ? "commonjs2" : undefined
},
// https://webpack.js.org/configuration/externals/#function
// https://github.com/liady/webpack-node-externals
// 外置化應(yīng)用程序依賴模塊。可以使服務(wù)器構(gòu)建速度更快,并生成較小的打包文件。
externals: TARGET_NODE
? nodeExternals({
// 不要外置化webpack需要處理的依賴模塊。
// 可以在這里添加更多的文件類型。例如,未處理 *.vue 原始文件,
// 還應(yīng)該將修改`global`(例如polyfill)的依賴模塊列入白名單
whitelist: [/.css$/]
})
: undefined,
optimization: {
splitChunks: undefined
},
// 這是將服務(wù)器的整個輸出構(gòu)建為單個 JSON 文件的插件。
// 服務(wù)端默認(rèn)文件名為 `vue-ssr-server-bundle.json`
// 客戶端默認(rèn)文件名為 `vue-ssr-client-manifest.json`。
plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
}),
chainWebpack: config => {
// cli4項(xiàng)目添加
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,會自動執(zhí)行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數(shù)組里面的是將來要返回給前端要加載的一些資源
server下面的vue-ssr-server-bundle.json的作用是:將來'vue-server-renderer'會以這個json為工作目錄去創(chuàng)建dom
注意,因?yàn)槭欠?wù)端渲染的,每一次修改都需要執(zhí)行build命令以更新代碼
編寫完上面的代碼,執(zhí)行node命令起服務(wù) nodemon/server/ssr.js
然后在瀏覽器打開localhost:3000,會看到如下頁面,右面加載的資源就是vue-ssr-client-manifest.json 里面的all的文件
回顧整個編碼過程我們發(fā)現(xiàn)我們實(shí)際編寫vue代碼的部分并沒有改變什么,和spa開發(fā)有著一樣的 同構(gòu)體驗(yàn),主要的改變有下面幾點(diǎn):
1 vue-router/vuex/vue實(shí)例的創(chuàng)建都是使用的工廠函數(shù),每一次請求服務(wù)端都會創(chuàng)建一個實(shí)例,防止數(shù)據(jù)污染
2 增加了兩個入口文件 entry-client.js/entry-server.js
3 增加了webpack的配置,產(chǎn)出dist/server 和dist/client相關(guān)的文件,供renderer插件使用
4 增加了服務(wù)端代碼-核心內(nèi)容就是使用renderer函數(shù)渲染dom,并返回給客戶端,其中包含激活前端頁面的js代碼
最后總結(jié)一下對服務(wù)端渲染的理解以及使用場景
服務(wù)端渲染相比于spa應(yīng)用,主要為我們提供了2點(diǎn)優(yōu)勢,一個是快速的首屏加載,一個是seo引擎搜索
一般來說,對于首屏加載速度要求較高的場景是移動端的頁面,特別是hybrid混合開發(fā)的應(yīng)用,在app里嵌入webview的方式,如果首屏加載時間過長的話會出現(xiàn)一個白屏,會讓用戶從比較流暢的原生頁面切換到了一個白屏頁面,如果是弱網(wǎng)情況白屏?xí)r間過長的話極大的降低了用戶體驗(yàn);另一種是微信公眾號/企業(yè)微信的第三方應(yīng)用開發(fā),點(diǎn)擊微信里的鏈接跳轉(zhuǎn)到我們自己服務(wù)器的url的過程中,除了我們自己的請求還有很多微信重定向等需要耗時的操作,白屏的時間更加的長。我們無法通過前端代碼控制客戶端加載頁面的白屏期間的操作,也就是無法加載骨架屏或者加上loading提高用戶體驗(yàn)。因此移動端的hybrid開發(fā)和微信開發(fā)在技術(shù)選型的時候最好使用ssr的開發(fā)模式。
除了移動端之外,一些大型官網(wǎng)開發(fā)要求首屏渲染速度和搜索引擎seo抓取的時候也需要使用服務(wù)端渲染,搜索引擎爬蟲爬取網(wǎng)站并排名的最重要因素是網(wǎng)站首屏的加載速度,其次是里面的關(guān)鍵字,圖片的title,meta里的keysords等,而ssr模式剛好解決了這兩個問題。對于一些對首屏速度沒有要求的網(wǎng)站,且數(shù)據(jù)交互比較多的網(wǎng)站,例如后臺管理系統(tǒng),就完全不需要ssr的開發(fā)模式。
當(dāng)然,采用ssr模式的架構(gòu)開發(fā)也有缺點(diǎn),一是增加了代碼的復(fù)雜度,新手開發(fā)起來成本還是比較高的。二是在服務(wù)端渲染增加了服務(wù)端的壓力,如果服務(wù)器的配置比較低,而用戶數(shù)量很大的時候,服務(wù)器的cpu很容易滿載,如果公司有增加服務(wù)器的預(yù)算可以采用負(fù)載均衡,或者采用node server / nginx做一些緩存,如果登錄用戶有效的話將緩存的頁面直接返回給前端而不做服務(wù)端渲染。如果開發(fā)周期比較充裕,也可以做一些兼容性的判斷,用node監(jiān)聽當(dāng)前cpu的使用量,如果達(dá)到自己設(shè)定的閾值,那么就不用服務(wù)端渲染,返回給用戶spa的應(yīng)用,如果cpu占用的比較少,正常返回服務(wù)端渲染的頁面。
上面的例子只是給大家提供vue ssr開發(fā)的模式的一個思路,如果公司新開發(fā)的項(xiàng)目,如果需要采用ssr,還是建議大家采用nuxt.js。
總結(jié)
以上是生活随笔為你收集整理的vue服务端渲染实践的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 钢件攻丝用什么丝锥?
- 下一篇: 向SqlServer数据库插入数据