摘要:二服務(wù)端渲染初體驗(yàn)使用的服務(wù)端渲染功能,需要引入提供的服務(wù)端渲染模塊,其作用是創(chuàng)建一個(gè)渲染器,該渲染器可以將實(shí)例渲染成字符串。
詳解Vue服務(wù)端渲染 一、服務(wù)端渲染 - 簡(jiǎn)介
所謂服務(wù)端渲染就是將代碼的渲染交給服務(wù)器,服務(wù)器將渲染好的html字符串返回給客戶端,再由客戶端進(jìn)行顯示。
服務(wù)器端渲染的優(yōu)點(diǎn)有利于SEO搜索引擎優(yōu)化,因?yàn)榉?wù)端渲染是將渲染好的html字符串返回給了客戶端,所以其可以被爬蟲爬取到;
加快首屏渲染時(shí)間,不會(huì)出現(xiàn)白屏;
服務(wù)器端渲染的缺點(diǎn)SSR會(huì)占用更多的CPU和內(nèi)存資源
Vue中一些常用的瀏覽器API可能無法使用,比如Vue的生命周期在服務(wù)器端渲染只能使用beforeCreate()和created(),因?yàn)榉?wù)端呈現(xiàn)的僅僅是html字符串是沒有所謂的mount的。
二、服務(wù)端渲染 - 初體驗(yàn)使用Vue的服務(wù)端渲染功能,需要引入Vue提供的服務(wù)端渲染模塊vue-server-renderer,其作用是創(chuàng)建一個(gè)渲染器,該渲染器可以將Vue實(shí)例渲染成html字符串。
用Koa來搭建一個(gè)web服務(wù)器來實(shí)現(xiàn):
① 目錄結(jié)構(gòu)
② 創(chuàng)建一個(gè)server.js 文件
const Koa = require("koa"); const Router = require("koa-router"); const fs = require("fs"); const app = new Koa(); // 創(chuàng)建服務(wù)器端app實(shí)例 const router = new Router(); // 創(chuàng)建服務(wù)器端路由 const Vue = require("vue"); const VueServerRender = require("vue-server-renderer"); // 引入服務(wù)端渲染模塊 const vm = new Vue({ // 創(chuàng)建Vue實(shí)例 data() { return {msg: "hello vm"} }, template: `{{msg}}` // 渲染器會(huì)將vue實(shí)例中的數(shù)據(jù)填入模板中并渲染成對(duì)應(yīng)的html字符串 }); const template = fs.readFileSync("./server.template.html", "utf8"); // 讀取基本的html結(jié)構(gòu) const render = VueServerRender.createRenderer({ template }); // 創(chuàng)建渲染器并以server.template.html作為html頁面的基本結(jié)構(gòu) router.get("/", async ctx => { // ctx.body = await render.renderToString(vm); ctx.body = await new Promise((resolve, reject) => { render.renderToString(vm, (err, html) => { // 將vm實(shí)例渲染成html并插入到server.template.html模板中 console.log(`${html}`); }); ); }); app.use(router.routes()); // 添加路由中間件 app.listen(3000, () => { console.log("node server listening on port 3000."); }); // 監(jiān)聽3000端口
注意:
server.template.html文件中必須有 占位符,即將Vue實(shí)例vm渲染成的html字符串插入到占位符所在的位置;
render.renderToString(vm)方法不傳回調(diào)函數(shù)的時(shí)候返回的是Promise對(duì)象,但是如果傳入了回調(diào)函數(shù),那么就返回void了, 推薦自己創(chuàng)建一個(gè)Promise函數(shù);
Vue服務(wù)端渲染出來的字符串中會(huì)包含data-server-rendered="true"這樣一個(gè)標(biāo)識(shí),標(biāo)識(shí)這是由Vue服務(wù)端渲染的結(jié)果字符
三、服務(wù)端渲染 - 引入Vue項(xiàng)目hello vm
上面初體驗(yàn)中,我們已經(jīng)實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的Vue服務(wù)端渲染,但是我們實(shí)際中Vue是一個(gè)很大的項(xiàng)目,里面是包含了很多組件的大型應(yīng)用,而不是像初體驗(yàn)中的一個(gè)簡(jiǎn)單的Vue實(shí)例,所以我們必須引入一個(gè)Vue項(xiàng)目,包括Vue的入口文件main.js、App.vue、components、public/index.html等,如:
通過webpack來打包我們的整個(gè)Vue項(xiàng)目,webpack將以Vue的根實(shí)例main.js作為入口文件,打包出一個(gè)合并的最終的bundle.js和一個(gè)頁面入口index.html文件,該index.html文件引入bundle.js后就能加載整個(gè)Vue項(xiàng)目中的頁面以及頁面中的事件等等,這里我們的Vue項(xiàng)目是一個(gè)很簡(jiǎn)單的模板項(xiàng)目,關(guān)鍵在于webpack的配置
// webpack.config.js
const path = require("path"); const resolve = (dir) => { return path.resolve(__dirname, dir); } const VueLoader = require("vue-loader/lib/plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = { entry: resolve("./src/main.js"), // webpack 入口, 即Vue的入口文件main.js output: { filename: "bundle.js", // 打包后輸出的結(jié)果文件名 path: resolve("./dist") // 打包后輸出結(jié)果存放目錄 }, resolve: { extensions: [".js", ".vue"] // 沒有寫擴(kuò)展名的時(shí)候,解析順序 }, module: { rules: [ { test: /.js$/, use: { loader: "babel-loader", // 將所有的js文件通過babel-loader轉(zhuǎn)換為ES5代碼 options: { presets: ["@babel/preset-env"] } }, exclude: /node_modules/ }, { test: /.css$/, // 解析.vue文件中的css use: [ "vue-style-loader", "css-loader" ] }, { test: /.vue$/, // 解析.vue文件,需要配合其中的插件進(jìn)行使用 use: "vue-loader" } ] }, plugins: [ new VueLoader(), // 解析.vue文件的插件 new HtmlWebpackPlugin({ filename: "index.html", // 打包后輸出的html文件名 template: resolve("./public/index.html") // 該模板文件在哪 }) ] }
打包輸出后的dist目錄中會(huì)出現(xiàn)兩個(gè)文件: bundle.js和index.html, 直接在本地點(diǎn)擊index.html文件即可執(zhí)行并呈現(xiàn)整個(gè)Vue項(xiàng)目四、服務(wù)端渲染 - 將Vue項(xiàng)目分割為客戶端和服務(wù)端
① 在非服務(wù)端渲染的時(shí)候,我們使用的打包入口文件是main.js,其主要就是創(chuàng)建了一個(gè)Vue實(shí)例,并且渲染App.vue,然后將渲染好的App.vue掛載到index.html文件#app元素中,但是我們的服務(wù)端渲染是無法mount的,也就是說無法將渲染結(jié)果渲染到#app元素上,所以需要改造main.js文件
// 改造后的main.js文件
import Vue from "vue"; import App from "./App"; /** 1. main.js在服務(wù)端渲染中的作用就是提供一個(gè)Vue項(xiàng)目的根實(shí)例,所以導(dǎo)出一個(gè)函數(shù) 2. 讓客戶端和服務(wù)端都能獲取到Vue項(xiàng)目的根實(shí)例,然后根據(jù)需要, 3. 客戶端通過手動(dòng)調(diào)用$mount()進(jìn)行掛載 4. */ export default () => { const app = new Vue({ render: h => h(App) }); return {app}; // 返回整個(gè)Vue根實(shí)例 }
② 新建兩個(gè)入口文件: client-entry.js 和 server-entry.js
// client-entry.js
import createApp from "./main"; const {app} = createApp(); // 獲取到Vue項(xiàng)目根實(shí)例 app.$mount("#app"); // 將根實(shí)例掛載到#app上
此時(shí)將webpack.config.js的入口文件改成client-entry.js應(yīng)該和之前是一樣的
// server-entry.js
import createApp from "./main"; /** * 服務(wù)端需要調(diào)用當(dāng)前這個(gè)文件產(chǎn)生一個(gè)Vue項(xiàng)目的根實(shí)例 * 由于服務(wù)端與客戶端是1對(duì)多的關(guān)系,所以不能每個(gè)客戶端訪問都返回同一個(gè)Vue項(xiàng)目根實(shí)例 * 所以需要返回一個(gè)函數(shù),該函數(shù)返回一個(gè)新的Vue項(xiàng)目根實(shí)例 * */ export default () => { const {app} = createApp(); // 獲取到Vue項(xiàng)目根實(shí)例 return app; }
為什么客戶端入口文件就不需要暴露一個(gè)一個(gè)函數(shù)?因?yàn)榭蛻舳丝梢员辉L問多次,即多次執(zhí)行,每次執(zhí)行返回的都是一個(gè)新的Vue項(xiàng)目實(shí)例了。而服務(wù)器只會(huì)啟動(dòng)一次,但是卻需要每次客戶端訪問都返回一個(gè)新的Vue項(xiàng)目實(shí)例,所以必須放到函數(shù)中
③ 拆分webapck.config.js, 將其分成兩個(gè)配置文件,同樣一個(gè)用于客戶端,一個(gè)用于服務(wù)端打包
由于客戶端和服務(wù)端的webpack配置文件有很多是相同的,所以可以抽取出一個(gè)webpack.base.js
// webpack.base.js
const path = require("path"); const resolve = (dir) => { return path.resolve(__dirname, dir); } const VueLoader = require("vue-loader/lib/plugin"); module.exports = { output: { filename: "[name].bundle.js", // 打包后輸出的結(jié)果文件名 path: resolve("./../dist/") // 打包后輸出結(jié)果存放目錄 }, resolve: { extensions: [".js", ".vue"] // 沒有寫擴(kuò)展名的時(shí)候,解析順序 }, module: { rules: [ { test: /.js$/, use: { loader: "babel-loader", // 將所有的js文件通過babel-loader轉(zhuǎn)換為ES5代碼 options: { presets: ["@babel/preset-env"] } }, exclude: /node_modules/ }, { test: /.css$/, // 解析.vue文件中的css use: [ "vue-style-loader", "css-loader" ] }, { test: /.vue$/, // 解析.vue文件,需要配合其中的插件進(jìn)行使用 use: "vue-loader" } ] }, plugins: [ new VueLoader(), // 解析.vue文件的插件 ] }
// webpack-client.js
const merge = require("webpack-merge"); const base = require("./webpack.base"); const path = require("path"); const resolve = (dir) => { return path.resolve(__dirname, dir); } const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = merge(base, { entry: { client: resolve("./../src/client-entry.js"), // 給客戶端入口文件取名client,output的時(shí)候可以獲取到該名字動(dòng)態(tài)輸出 }, plugins: [ new HtmlWebpackPlugin({ filename: "index.html", // 打包后輸出的html文件名 template: resolve("./../public/index.html") // 該模板文件在哪 }) ] });
// webpack-server.js
const merge = require("webpack-merge"); const base = require("./webpack.base"); const path = require("path"); const resolve = (dir) => { return path.resolve(__dirname, dir); } const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = merge(base, { entry: { server: resolve("./../src/server-entry.js"), // 給客戶端入口文件取名client,output的時(shí)候可以獲取到該名字動(dòng)態(tài)輸出 }, target: "node", // 給node使用 output: { libraryTarget: "commonjs2" // 把最終這個(gè)文件導(dǎo)出的結(jié)果放到module.exports上 }, plugins: [ new HtmlWebpackPlugin({ filename: "index.server.html", // 打包后輸出的html文件名 template: resolve("./../public/index.server.html"), // 該模板文件在哪 excludeChunks: ["server"] // 排除某個(gè)模塊, 不讓打包輸出后的server.bundle.js文件引入到index.server.html文件中 }) ] });
服務(wù)端webpack配置文件比較特殊,在output的時(shí)候需要配置一個(gè)libraryTarget,因?yàn)槟J(rèn)webpack輸出的時(shí)候是將打包輸出結(jié)果放到一個(gè)匿名自執(zhí)行函數(shù)中的,通過將libraryTarget設(shè)置為commonjs2,就會(huì)將整個(gè)打包結(jié)果放到module.exports上;
服務(wù)端webpack打包后輸出的server.bundle.js文件不是直接引入到index.server.html文件中使用的,還需要經(jīng)過處理渲染成html字符串才能插入到index.server.html文件中,所以打包輸出后,要在html-webpack-plugin中排除對(duì)該模塊的引用
由于webpack配置文件被分割,所以啟動(dòng)webapck-dev-server的時(shí)候需要指定配置文件,在package.json文件中添加腳本
"scripts": { "client:dev": "webpack-dev-server --config ./build/webpack.client.js --mode development", "client:build": "webpack --config ./build/webpack.client.js --mode development", "server:build": "webpack --config ./build/webpack.server.js --mode development" },
此時(shí)分別指向npm run client:build 和 npm run server:build即可在dist目錄下生成index.html、client.bundle.js, index.server.html、server.bundle.js,其中client.bundel.js被index.html引用,server.bundle.js沒有被index.server.html引入,index.server.html僅僅是拷貝到了dist目錄下,同時(shí)server.bundle.js的整個(gè)輸出結(jié)果是掛在module.exports下的
④ 將打包好的server.bundle.js交給服務(wù)器進(jìn)行渲染并生成html字符串返回給客戶端,和之前初體驗(yàn)一樣,創(chuàng)建一個(gè)web服務(wù)器,只不過,這次不是渲染一個(gè)簡(jiǎn)單的Vue實(shí)例,而是渲染整個(gè)打包好的server.bundle.js
vue-server-renderer提供了兩種渲染方式:
和初體驗(yàn)中的一樣,把server.bundle.js當(dāng)作簡(jiǎn)單Vue實(shí)例進(jìn)行渲染,我們打包后server.bundle.js的內(nèi)容都是掛到了module.exports上,所以我們可以直接require,require返回的結(jié)果是一個(gè)對(duì)象,該對(duì)象上只有一個(gè)屬性即default,屬性值為一個(gè)函數(shù),執(zhí)行該函數(shù)即可獲取整個(gè)Vue項(xiàng)目對(duì)應(yīng)的Vue實(shí)例。
// 獲取server.bundle.js中的Vue實(shí)例進(jìn)行渲染 const VueServerRender = require("vue-server-renderer"); // 引入服務(wù)端渲染模塊 const template = fs.readFileSync("./server.template.html", "utf8"); // 讀取基本的html結(jié)構(gòu) const render = VueServerRender.createRenderer({ template }); // 創(chuàng)建渲染器并以server.template.html作為html頁面的基本結(jié)構(gòu) router.get("/", async ctx => { const vm = require("./dist/server.bundle").default(); // 執(zhí)行server.budle的default方法獲取Vue實(shí)例,每次請(qǐng)求獲取一個(gè)新的Vue實(shí)例 ctx.body = await new Promise((resolve, reject) => { render.renderToString(vm, (err, html) => { // 將vm實(shí)例渲染成html并插入到server.template.html模板中 if (err) reject(err); console.log(`${html}`); resolve(html); }); }); });
require server.bunlde.js之后調(diào)用default屬性獲取的方法,其實(shí)就是server.entry.js中導(dǎo)出的方法,這個(gè)方法可以接收路由參數(shù),后面集成路由的時(shí)候會(huì)用到
通過vue-server-renderer提供的createBundleRenderer()方法進(jìn)行渲染,該方法需要傳入server.bundle.js中的文件內(nèi)容字符串, 再傳入模板html即可,所以需要讀取server.bundle.js中的內(nèi)容:
// 直接渲染server.bundle.js const VueServerRender = require("vue-server-renderer"); // 引入服務(wù)端渲染模塊 // 讀取server.bundle.js中的內(nèi)容,即文件中的字符串 const ServerBundle = fs.readFileSync("./dist/server.bundle.js", "utf8"); const template = fs.readFileSync("./dist/index.server.html", "utf8"); // 讀取基本的html結(jié)構(gòu) const render = VueServerRender.createBundleRenderer(ServerBundle, { // 傳入server.bundle.js字符串創(chuàng)建渲染器 template }); router.get("/", async ctx => { ctx.body = await new Promise((resolve, reject) => { render.renderToString((err, html) => { // 將server.bundle.js渲染成html字符串 if (err) reject(err); resolve(html); }); }); });
render.renderToString()執(zhí)行的時(shí)候內(nèi)部也是要通過ServerBundle獲取到server.entry.js中導(dǎo)出的default()方法獲取到Vue項(xiàng)目實(shí)例進(jìn)行渲染的,總之就是要獲取到Vue項(xiàng)目的實(shí)例進(jìn)行渲染
重啟服務(wù)器,再次訪問,查看源碼,可以看到頁面已經(jīng)不是一個(gè)空的基礎(chǔ)頁面了,而是真實(shí)包含html內(nèi)容的頁面,但是仍然存在一個(gè)問題,那就是之前的事件并不起作用了,因?yàn)榉?wù)器將sever.bundle.js渲染成的是html字符串返回給客戶端的,是不包含事件的,其中的事件執(zhí)行函數(shù)在client.bundle.js中,所以我們可以在index.server.html文件中通過script標(biāo)簽顯式地引入client.bundle.js,如:
注意: 當(dāng)訪問頁面的時(shí)候,就會(huì)向服務(wù)器請(qǐng)求client.bundle.js文件,所以服務(wù)器需要將client.bundle.js以靜態(tài)資源的方式發(fā)布出去。
剛才我們是手動(dòng)在index.server.html中通過script標(biāo)簽引入client.bundle.js, 非常的不方便,vue-server-renderer給我們提供了兩個(gè)插件,vue-server-renderer/client-plugin和vue-server-renderer/server-plugin,可以在webpack配置文件中引入,那么打包的時(shí)候,會(huì)分別生成兩個(gè)json文件,vue-ssr-client-manifest.json和vue-ssr-server-bundle.json,這兩個(gè)文件主要是生成客戶端和服務(wù)端bundle的對(duì)應(yīng)關(guān)系,這樣就不需要我們收到引入client.bundle.js了。
之前是通過讀取server.bundle.js的內(nèi)容來渲染的,現(xiàn)在可以直接requirevue-ssr-server-bundle.json文件即可,同時(shí)在渲染的時(shí)候再添加vue-ssr-client-manifest.json即可,如:
// 直接渲染server.bundle.js const VueServerRender = require("vue-server-renderer"); // 引入服務(wù)端渲染模塊 // 讀取server.bundle.js中的內(nèi)容,即文件中的字符串 // const ServerBundle = fs.readFileSync("./dist/server.bundle.js", "utf8"); const ServerBundle = require("./dist/vue-ssr-server-bundle.json"); const clientManifest = require("./dist/vue-ssr-client-manifest.json"); const template = fs.readFileSync("./dist/index.server.html", "utf8"); // 讀取基本的html結(jié)構(gòu) const render = VueServerRender.createBundleRenderer(ServerBundle, { // 傳入server.bundle.js字符串創(chuàng)建渲染器 template, clientManifest });
使用者兩個(gè)插件之后,就不會(huì)生成server.bundle.js文件了五、服務(wù)端渲染 - 集成路由
要集成路由,那么需要在Vue項(xiàng)目中加入路由功能,和客戶端路由配置一樣,只不過不是直接導(dǎo)出路由實(shí)例,而是和main.js一樣導(dǎo)出一個(gè)方法返回一個(gè)新的路由實(shí)例,如:
import Vue from "vue"; import VueRouter from "vue-router"; import Foo from "./components/Foo"; Vue.use(VueRouter); export default () => { // 導(dǎo)出函數(shù)返回路由實(shí)例 const router = new VueRouter({ mode: "history", routes: [ { path: "/", component: Foo }, { path: "/bar", component: () => import("./components/Bar.vue") } ] }); return router; }
然后在main.js中調(diào)用路由方法獲取路由實(shí)例并掛到Vue實(shí)例上,同時(shí)對(duì)外暴露,如:
export default () => { const router = createRouter(); const app = new Vue({ router, // 掛在路由實(shí)例到Vue實(shí)例上 render: h => h(App) }); return {app, router}; // 對(duì)外暴露路由實(shí)例 }
此時(shí)Vue項(xiàng)目已經(jīng)實(shí)現(xiàn)路由功能,但是訪問的時(shí)候卻會(huì)報(bào)錯(cuò),The client-side rendered virtual DOM tree is not matching server-rendered content,即客戶端和服務(wù)端渲染的頁面不一致,之所以出現(xiàn)這種情況是因?yàn)椋?strong>客戶端加了路由功能進(jìn)行了相應(yīng)的路由跳轉(zhuǎn),但是服務(wù)端沒有進(jìn)行路由跳轉(zhuǎn),所以頁面會(huì)不一致,解決方法就是,服務(wù)器也要進(jìn)行相應(yīng)的路由跳轉(zhuǎn)
前面提到過createBundleRenderer()方法創(chuàng)建的渲染器在執(zhí)行renderToString()方法的時(shí)候,可以傳遞一個(gè)context上下文對(duì)象,可以將客戶端的訪問url保存到context對(duì)象上,而這個(gè)context對(duì)象會(huì)傳到server.entry.js對(duì)外暴露函數(shù)中,然后在該函數(shù)中獲取路由進(jìn)行相應(yīng)跳轉(zhuǎn)即可,如:
// server.entry.js
export default (context) => { const {app, router} = createApp(); // 獲取到Vue項(xiàng)目根實(shí)例server console.log("相當(dāng)于新創(chuàng)建了一個(gè)服務(wù)端"); router.push(context.url); // 在服務(wù)端進(jìn)行路由跳轉(zhuǎn) return app; }
此時(shí)再訪問頁面,就不會(huì)出現(xiàn)上述客戶端和服務(wù)端渲染頁面不一致的情況了,但是還有一個(gè)問題,那就是我們?cè)跒g覽器中直接訪問路由路徑的時(shí)候,會(huì)提示404,因?yàn)槲覀兎?wù)器并沒有配置相應(yīng)的路由,所以客戶端定義的路由路徑,需要在服務(wù)器端進(jìn)行相應(yīng)的配置
還有就是異步組件渲染的問題,我們現(xiàn)在的server.entry.js中是直接返回Vue實(shí)例的,同時(shí)在其中進(jìn)行router跳轉(zhuǎn),如果路由跳轉(zhuǎn)的那個(gè)是異步組件,可能還沒跳轉(zhuǎn)完成,就返回了Vue實(shí)例,而出現(xiàn)渲染異常的情況,所以我們要返回一個(gè)Promise對(duì)象,等路由跳轉(zhuǎn)完成后再返回Vue實(shí)例,如:
// 改造后的sever.entry.js
export default (context) => { return new Promise((resolve, reject) => { const {app, router} = createApp(); // 獲取到Vue項(xiàng)目根實(shí)例server router.push(context.url); router.onReady(() => { // 等路由跳轉(zhuǎn)完成 let matchs = router.getMatchedComponents(); if (matchs.length === 0) { reject({code: 404}); } resolve(app); }, reject); }); }
404頁面的處理,我們可以在router.onReady回調(diào)中進(jìn)行處理,可以根據(jù)路由匹配結(jié)果進(jìn)行提示,如果路由匹配結(jié)果為0,那么就是沒有匹配成功則reject一個(gè)錯(cuò)誤,服務(wù)器捕獲到錯(cuò)誤后進(jìn)行404提示即可六、服務(wù)端渲染 - 集成Vuex
同樣,要集成Vuex,首先和客戶端渲染一樣,引入Vuex并創(chuàng)建store,只不過是對(duì)外暴露一個(gè)函數(shù),然后在函數(shù)中返回新的store對(duì)象,如:
// store.js
import Vue from "vue"; import Vuex from "vuex"; Vue.use(Vuex); export default () => { const store = new Vuex.Store({ state: { name: "even" }, mutations: { changeName(state) { state.name = "lhb"; } }, actions: { changeName({commit}) { console.log("changeName action"); return new Promise((resolve, reject) => { setTimeout(() => { commit("changeName"); resolve(); }, 3000); }); } } }); return store; }
然后在main.js中引入并注入到Vue實(shí)例中,跟Vue根實(shí)例和路由一樣對(duì)外暴露。服務(wù)端渲染集成Vuex關(guān)鍵在于服務(wù)端渲染的時(shí)候執(zhí)行mutaion或者action后,Vuex中數(shù)據(jù)僅在服務(wù)器端改變,所以需要將服務(wù)器端的狀態(tài)數(shù)據(jù)保存起來,實(shí)際上會(huì)保存到window對(duì)象的__INITIAL_STATE__屬性上,客戶端渲染的時(shí)候只需要從window.__INITIAL_STATE__數(shù)據(jù)中獲取到服務(wù)端Vuex的狀態(tài)然后進(jìn)行替換即可。
① 在Foo.vue組件中添加一個(gè)asyncData()方法,用于派發(fā)action,如:
// Foo.vue
export default { asyncData(store) { // asyncData只在服務(wù)端執(zhí)行 console.log("asyncData"); return store.dispatch("changeName"); } }
② 在server-entry.js中,如果匹配到了Foo.Vue組件,那么執(zhí)行該組件的asyncData()方法,此時(shí)服務(wù)器端的Vuex的狀態(tài)就會(huì)發(fā)生改變,如:
// server-entry.js
export default (context) => { return new Promise((resolve, reject) => { console.log(context.url); const {app, router, store} = createApp(); // 獲取到Vue項(xiàng)目根實(shí)例server router.push(context.url); router.onReady(() => { // 等路由跳轉(zhuǎn)完成 let matchs = router.getMatchedComponents(); Promise.all(matchs.map((component) => { if (component.asyncData) { // 如果匹配的組件中含有asyncData方法則執(zhí)行 return component.asyncData(store); // 服務(wù)器端Vuex狀態(tài)會(huì)發(fā)生改變 } })).then(() => { console.log("success"); context.state = store.state; // 服務(wù)器端store狀態(tài)改變后將其掛載到context上,然后會(huì)掛載到window的__INITIAL_STATE__上 resolve(app); }); if (matchs.length === 0) { reject({code: 404}); } }, reject); }); }
將服務(wù)器Vuex狀態(tài)保存的時(shí)候,必須是保存到context的state屬性上,服務(wù)器端渲染完成后,會(huì)添加一個(gè)script標(biāo)簽其中只有一行代碼,就是將服務(wù)器端Vuex狀態(tài)保存到window.__INITIAL_STATE__上
③ 接下來就是需要客戶端去取出window.__INITIAL_STATE__中的狀態(tài)數(shù)據(jù)并替換,在store.js中返回store對(duì)象前進(jìn)行判斷,如果是客戶端執(zhí)行Vuex,那么取出window.__INITIAL_STATE__中的狀態(tài)數(shù)據(jù)并替換,如:
if(typeof window !== "undefined" && window.__INITIAL_STATE__) { // 如果是客戶端執(zhí)行 store.replaceState(window.__INITIAL_STATE__); // 將服務(wù)器端store狀態(tài)替換掉客戶端狀態(tài) } return store;
將Vuex中的數(shù)據(jù)顯示出來,此時(shí)再訪問Foo.vue就可以看到name數(shù)據(jù)的變化了,我們現(xiàn)在只有在進(jìn)行服務(wù)器端渲染Foo.vue的時(shí)候才會(huì)執(zhí)行asyncData()方法,數(shù)據(jù)才會(huì)發(fā)生變化,如果在客戶端進(jìn)行渲染Foo.vue組,那么不會(huì)執(zhí)行asyncData(),所以可以在Foo.vue組件mounted的時(shí)候派發(fā)一個(gè)相同的action進(jìn)行數(shù)據(jù)改變即可
// Foo.vue
export default { mounted () { this.$store.dispatch("changeName"); } }
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/106368.html
摘要:依舊采取傳統(tǒng)的開發(fā)技術(shù)棧進(jìn)行開發(fā),同時(shí)在終端的運(yùn)行體驗(yàn)不輸。首先來看下前端開發(fā)框架目前與構(gòu)成了三大最流行的前端開發(fā)框架,具有組件化以及三大特性,還學(xué)習(xí)的,引入了狀態(tài)管理模塊。 摘要: WEEX依舊采取傳統(tǒng)的web開發(fā)技術(shù)棧進(jìn)行開發(fā),同時(shí)app在終端的運(yùn)行體驗(yàn)不輸native app。其同時(shí)解決了開發(fā)效率、發(fā)版速度以及用戶體驗(yàn)三個(gè)核心問題。那么WEEX是如何實(shí)現(xiàn)的?目前WEEX已經(jīng)完全開...
摘要:后端主要使用的框架,數(shù)據(jù)庫采用。后臺(tái)管理登錄采用與后端進(jìn)行登陸狀態(tài)的確認(rèn)。本文首發(fā)于小站,這是一個(gè)積累和分享知識(shí)的個(gè)人博客 這篇文章擱置了很長(zhǎng)時(shí)間,最終決定還是把它寫出來,給剛開始學(xué)習(xí)vue并且想用vue寫個(gè)人博客的同學(xué)一個(gè)參考。因?yàn)楫?dāng)初我也是參考了其他人分享的知識(shí),從一個(gè)vue小白變成了一個(gè)入門級(jí)選手,并最終完成了這個(gè)個(gè)人博客的搭建工作,代碼已托管在Github-justJokee。...
摘要:無需使用服務(wù)器實(shí)時(shí)動(dòng)態(tài)編譯,而是使用預(yù)渲染方式,在構(gòu)建時(shí)簡(jiǎn)單地生成針對(duì)特定路由的靜態(tài)文件。與可以部署在任何靜態(tài)文件服務(wù)器上的完全靜態(tài)單頁面應(yīng)用程序不同,服務(wù)器渲染應(yīng)用程序,需要處于運(yùn)行環(huán)境。更多的服務(wù)器端負(fù)載。 目錄結(jié)構(gòu) -no-ssr-demo 未做ssr之前的項(xiàng)目代碼用于對(duì)比 -vuecli2ssr 將vuecli生成的項(xiàng)目轉(zhuǎn)為ssr -prerender-demo 使用prer...
閱讀 2247·2019-08-30 15:53
閱讀 2477·2019-08-30 12:54
閱讀 1257·2019-08-29 16:09
閱讀 748·2019-08-29 12:14
閱讀 778·2019-08-26 10:33
閱讀 2513·2019-08-23 18:36
閱讀 2982·2019-08-23 18:30
閱讀 2142·2019-08-22 17:09