Vue?服務(wù)端渲染SSR示例詳解
手寫Vue服務(wù)端渲染
概念:放在瀏覽器進(jìn)行就是瀏覽器渲染,放在服務(wù)器進(jìn)行就是服務(wù)器渲染。
- 客戶端渲染不利于 SEO 搜索引擎優(yōu)化
- 服務(wù)端渲染是可以被爬蟲抓取到的,客戶端異步渲染是很難被爬蟲抓取到的
- SSR直接將HTML字符串傳遞給瀏覽器。大大加快了首屏加載時(shí)間。
- SSR占用更多的CPU和內(nèi)存資源
- 一些常用的瀏覽器API可能無法正常使用
- 在vue中只支持beforeCreate和created兩個(gè)生命周期
一.開始vue-ssr之旅
yarn add vue-server-renderer vue yarn add koa koa-router
createRenderer,創(chuàng)建一個(gè)渲染函數(shù) renderToString, 渲染出一個(gè)字符串
const Vue = require('vue'); const render = require('vue-server-renderer'); const Koa = require('koa'); const Router = require('koa-router'); const app = new Koa(); const router = new Router(); const vm = new Vue({ data(){ return {msg:"hello world"} }, template:`<div>{{msg}}</div>` }); router.get('/',async (ctx)=>{ let r = await render.createRenderer().renderToString(vm); ctx.body = ` <!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> ${r} </body> </html> ` }); app.use(router.routes()); app.listen(4000);
二.采用模板渲染
<!DOCTYPE html> <html lang="en"> <head><title>Hello</title></head> <body> <!--vue-ssr-outlet--> </body> </html>
傳入template 替換掉注釋標(biāo)簽
const Vue = require('vue'); const render = require('vue-server-renderer'); const Koa = require('koa'); const Router = require('koa-router'); const app = new Koa(); const router = new Router(); const vm = new Vue({ data(){ return {msg:"hello world"} }, template:`<div>{{msg}}</div>` }); const template = require('fs').readFileSync('./index.html','utf8'); router.get('/',async (ctx)=>{ let r = await render.createRenderer({ template }).renderToString(vm); ctx.body = r; }); app.use(router.routes()); app.listen(4000);
三.ssr目錄創(chuàng)建
├── config │ ├── webpack.base.js │ ├── webpack.client.js │ └── webpack.server.js ├── dist │ ├── client.bundle.js │ ├── index.html │ ├── index.ssr.html │ ├── server.bundle.js │ ├── vue-ssr-client-manifest.json │ └── vue-ssr-server-bundle.json ├── package.json ├── public │ ├── index.html │ └── index.ssr.html ├── server.js ├── src │ ├── App.vue │ ├── components │ │ ├── Bar.vue │ │ └── Foo.vue │ ├── entry-client.js │ ├── entry-server.js │ ├── app.js │ ├── router.js │ └── store.js ├── webpack.config.js
四.通過webpack實(shí)現(xiàn)編譯vue項(xiàng)目
安裝插件
yarn add webpack webpack-cli webpack-dev-server vue-loader vue-style-loader css-loader html-webpack-plugin @babel/core @babel/preset-env babel-loader vue-template-compiler webpack-merge
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const VueLoaderPlugin = require('vue-loader/lib/plugin') const resolve = (dir)=>{ return path.resolve(__dirname,dir) } module.exports = { entry: resolve('./src/client-entry.js'), output:{ filename:'[name].bundle.js', path:resolve('dist') }, module:{ rules:[ { test:/.css$/, use:['vue-style-loader','css-loader'] }, { test:/.js$/, use:{ loader:'babel-loader', options:{ presets:['@babel/preset-env'] } }, exclude:/node_modules/ }, { test:/.vue$/, use:'vue-loader' } ] }, plugins:[ new VueLoaderPlugin(), new HtmlWebpackPlugin({ template:'./index.html' }) ] }
app.js
import Vue from "vue"; import App from "./App.vue"; export default () => { // 為了保證實(shí)例的唯一性所以導(dǎo)出一個(gè)創(chuàng)建實(shí)例的函數(shù) const app = new Vue({ render: h => h(App) }); return { app }; };
client-entry.js
import createApp from "./app"; const { app } = createApp(); app.$mount("#app"); // 客戶端渲染手動(dòng)掛載到dom元素上
server-entry.js
import createApp from "./app"; export default () => { const { app } = createApp(); return app; // 服務(wù)端渲染只需將渲染的實(shí)例導(dǎo)出即可 };
五.配置客戶端打包和服務(wù)端打包
- webpack.base.js
let path = require('path'); let VueLoaderPlugin = require('vue-loader/lib/plugin') module.exports = { output:{ filename:'[name].bundle.js', path:path.resolve(__dirname,'../dist') }, module:{ rules:[ {test:/.css/,use:['vue-style-loader','css-loader']}, { test:/.js/, use:{ loader:'babel-loader', options:{ presets:['@babel/preset-env'] }, }, exclude:/node_modules/, }, {test:/.vue/,use:'vue-loader'} ] }, plugins:[ new VueLoaderPlugin() ] }
- webpack.client.js
const merge = require("webpack-merge"); const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const base = require("./webpack.base"); const resolve = filepath => { return path.resolve(__dirname, filepath); }; module.exports = merge(base, { entry: { client: resolve("../src/client-entry.js") }, plugins: [ new HtmlWebpackPlugin({ template: resolve("../template/index.client.html") }) ] });
- webpack.server.js
const merge = require("webpack-merge"); const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const base = require("./webpack.base"); const resolve = filepath => { return path.resolve(__dirname, filepath); }; module.exports = merge(base, { entry: { server: resolve("../src/server-entry.js") }, target: "node", output: { libraryTarget: "commonjs2" // 導(dǎo)出供服務(wù)端渲染來使用 }, plugins: [ new HtmlWebpackPlugin({ filename: "index.ssr.html", template: resolve("../template/index.ssr.html"), excludeChunks: ["server"] }) ] });
六.配置運(yùn)行腳本
"scripts": { "client:dev": "webpack-dev-server --config ./build/webpack.client.js", // 客戶端開發(fā)環(huán)境 "client:build": "webpack --config ./build/webpack.client.js", // 客戶端打包環(huán)境 "server:build": "webpack --config ./build/webpack.server.js" // 服務(wù)端打包環(huán)境 },
七.服務(wù)端配置
在App.vue上增加id="app"可以保證元素被正常激活
const Koa = require("koa"); const Router = require("koa-router"); const static = require("koa-static"); const path = require("path"); const app = new Koa(); const router = new Router(); const VueServerRenderer = require("vue-server-renderer"); const fs = require("fs"); // 服務(wù)端打包的結(jié)果 const serverBundle = fs.readFileSync("./dist/server.bundle.js", "utf8"); const template = fs.readFileSync("./dist/index.ssr.html", "utf8"); const render = VueServerRenderer.createBundleRenderer(serverBundle, { template }); router.get("/", async ctx => { ctx.body = await new Promise((resolve, reject) => { render.renderToString((err, html) => { // 必須寫成回調(diào)函數(shù)的方式否則樣式不生效 resolve(html); }); }); }); app.use(router.routes()); app.use(static(path.resolve(__dirname, "dist"))); app.listen(3000);
在index.ssr.html中需要手動(dòng)引入客戶端打包后的結(jié)果
七.通過json配置createBundleRenderer方法
實(shí)現(xiàn)熱更新,自動(dòng)增加preload和prefetch,以及可以使用sourceMap
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin'); // 在客戶端打包時(shí)增加插件 const VueSSRServerPlugin = require('vue-server-renderer/server-plugin'); // 在服務(wù)端打包時(shí)增加插件 const Koa = require("koa"); const Router = require("koa-router"); const static = require("koa-static"); const path = require("path"); const app = new Koa(); const router = new Router(); const VueServerRenderer = require("vue-server-renderer"); const fs = require("fs"); // 服務(wù)端打包的結(jié)果 // const serverBundle = fs.readFileSync("./dist/server.bundle.js", "utf8"); const template = fs.readFileSync("./dist/index.ssr.html", "utf8"); const serverBundle = require("./dist/vue-ssr-server-bundle.json"); const clientManifest = require("./dist/vue-ssr-client-manifest.json"); const render = VueServerRenderer.createBundleRenderer(serverBundle, { template, clientManifest // 自動(dòng)注入客戶端打包后的文件 }); router.get("/", async ctx => { ctx.body = await new Promise((resolve, reject) => { render.renderToString((err, html) => { // 必須寫成回調(diào)函數(shù)的方式否則樣式不生效 resolve(html); }); }); }); app.use(router.routes()); app.use(static(path.resolve(__dirname, "dist"))); app.listen(3000);
八.集成VueRouter
yarn add vue-router
import Vue from "vue"; import VueRouter from "vue-router"; import Foo from "./components/Foo.vue"; Vue.use(VueRouter); export default () => { const router = new VueRouter({ mode: "history", routes: [ { path: "/", component: Foo }, { path: "/bar", component: () => import("./components/Bar.vue") } ] }); return router; };
導(dǎo)出路由配置
配置入口文件
import Vue from "vue"; import App from "./App.vue"; import createRouter from "./router"; export default () => { const router = createRouter(); const app = new Vue({ router, render: h => h(App) }); return { app, router }; };
配置組件信息
<template> <div id="app"> <router-link to="/"> foo</router-link> <router-link to="/bar"> bar</router-link> <router-view></router-view> </div> </template>
防止刷新頁面不存在
router.get("*", async ctx => { ctx.body = await new Promise((resolve, reject) => { render.renderToString({ url: ctx.url }, (err, html) => { // 必須寫成回調(diào)函數(shù)的方式否則樣式不生效 resolve(html); }); }); });
保證異步路由加載完成
export default ({ url }) => { return new Promise((resolve, reject) => { const { app, router } = createApp(); router.push(url); router.onReady(() => { const matchComponents = router.getMatchedComponents(); if (!matchComponents.length) { return reject({ code: 404 }); } resolve(app); }, reject); }); }; // 服務(wù)器可以監(jiān)控到錯(cuò)誤信息,返回404 render.renderToString({ url: ctx.url }, (err, html) => { // 必須寫成回調(diào)函數(shù)的方式否則樣式不生效 if (err && err.code == 404) { resolve("404 Not Found"); } resolve(html); });
十.集成vuex配置
yarn add vuex
import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); export default ()=>{ let store = new Vuex.Store({ state:{ username:'song' }, mutations:{ changeName(state){ state.username = 'hello'; } }, actions:{ changeName({commit}){ return new Promise((resolve,reject)=>{ setTimeout(() => { commit('changeName'); resolve(); }, 1000); }) } } }); return store }
// 引用vuex import createRouter from './router'; import createStore from './store' export default ()=>{ let router = createRouter(); let store = createStore(); let app = new Vue({ router, store, render:(h)=>h(App) }) return {app,router,store} }
在后端更新vuex
import createApp from './main'; export default (context)=>{ return new Promise((resolve)=>{ let {app,router,store} = createApp(); router.push(context.url); // 默認(rèn)訪問到/a就跳轉(zhuǎn)到/a router.onReady(()=>{ let matchComponents = router.getMatchedComponents(); // 獲取路由匹配到的組件 Promise.all(matchComponents.map(component=>{ if(component.asyncData){ return component.asyncData(store); } })).then(()=>{ context.state = store.state; // 將store掛載在window.__INITIAL_STATE__ resolve(app); }); }) }) }
在瀏覽器運(yùn)行時(shí)替換store
// 在瀏覽器運(yùn)行代碼 if(typeof window !== 'undefined' && window.__INITIAL_STATE__){ store.replaceState(window.__INITIAL_STATE__); }
需要執(zhí)行的鉤子函數(shù)
export default { mounted() { return this.$store.dispatch("changeName"); }, asyncData(store) { return store.dispatch("changeName"); } };
以上就是Vue 服務(wù)端渲染SSR示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Vue 服務(wù)端渲染SSR的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Cookbook組件形式:優(yōu)化 Vue 組件的運(yùn)行時(shí)性能
本文仿照Vue Cookbook 組織形式,對(duì)優(yōu)化 Vue 組件的運(yùn)行時(shí)性能進(jìn)行闡述。通過基本的示例代碼給大家講解,需要的朋友參考下2018-11-11快速解決Vue、element-ui的resetFields()方法重置表單無效的問題
這篇文章主要介紹了快速解決Vue、element-ui的resetFields()方法重置表單無效的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-08-08vue3實(shí)現(xiàn)表格編輯和刪除功能的示例代碼
這篇文章主要為大家詳細(xì)介紹了vue3實(shí)現(xiàn)表格編輯和刪除功能的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-01-01vue3二次封裝element-ui中的table組件的過程詳解
這篇文章主要給大家介紹了vue3二次封裝element-ui中的table組件的過程,文中通過代碼示例給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友跟著小編一起來學(xué)習(xí)吧2024-01-01vue項(xiàng)目中請(qǐng)求數(shù)據(jù)特別多導(dǎo)致頁面卡死的解決
這篇文章主要介紹了vue項(xiàng)目中請(qǐng)求數(shù)據(jù)特別多導(dǎo)致頁面卡死的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09vue中的addEventListener和removeEventListener用法說明
這篇文章主要介紹了vue中的addEventListener和removeEventListener用法說明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06過濾器vue.filters的使用方法實(shí)現(xiàn)
這篇文章主要介紹了過濾器vue.filters的使用方法實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09