摘要:三個配置文件以此如下改配置只是簡單的配置等的使用,接著提取資源文件,指定輸出的目錄,而入口文件則分別在和的中配置。將指向應用程序的文件允許適用方式處理動態導入,提供支持使用風格導出模塊不要外置化需要處理的依賴模塊。
將同一個組件渲染為服務器端的 HTML 字符串,將它們直接發送到瀏覽器,最后將靜態標記"混合"為客戶端上完全交互的應用程序。SSR的目的
To solve
首屏渲染問題
SEO問題
項目結構vue-ssr ├── build (webapck編譯配置) ├── components (vue 頁面) ├── dist (編譯后的靜態資源目錄) ├── api.js (請求接口,模擬異步請求) ├── app.js (創建Vue實例入口) ├── App.vue (Vue頁面入口) ├── entry-client.js (前端執行入口) ├── entry-server.js (后端執行入口) ├── index.template.html (前端渲染模板) ├── router.js (Vue路由配置) ├── server.js (Koa服務) ├── store.js (Vuex數據狀態中心配置)原理概覽
這張圖相信很多大佬們都看過N遍了,每個人理解不同,我發表一下自己個人的理解,如果有什么理解錯誤請原諒我。
先看Source部分,Source部分先由app.js引入Vue全家桶,至于Vue全家桶如何配置后面會說明。app.js其實就是創建一個注冊好各種依賴的Vue對象實例,在SPA單頁環境下,我們只需要拿到這個Vue實例,然后指定掛載到模板特定的dom結點,然后丟給webpack處理就完事了。但是SSR在此分為兩部分,一部分是前端單頁,一部分是后端直出。于是,Client entry的作用是掛載Vue對象實例,并由webpack進行編譯打包,最后在瀏覽器渲染。Server entry的作用是拿到Vue對象實例,并處理收集頁面中的asynData,獲取對應的數據上下文,然后再由webpack解析處理。最后Node Server端中使用weback編譯好的兩個bundle文件( 服務器需要「服務器 bundle」然后用于服務器端渲染(SSR),而「客戶端 bundle」會發送給瀏覽器,用于混合靜態標記。),當用戶請求頁面時候,這時候服務端會先使用SSR來生成對應的頁面文檔結構,而在用戶切換路由則是使用了SPA的模式。
搭建環境 項目依賴說明Koa2 + Vue2 + Vue-router + Vuex
一切都從路由開始先來配置vue-router, 生成router.js
import Vue from "vue" import Router from "vue-router" import Bar from "./components/Bar.vue" import Baz from "./components/Baz.vue" import Foo from "./components/Foo.vue" import Item from "./components/Item.vue" Vue.use(Router) export const createRouter = () => { return new Router({ mode: "history", routes: [ { path: "/item/:id", component: Item }, { path: "/bar", component: Bar }, { path: "/baz", component: Baz }, { path: "/foo", component: Foo } ] }) }
為每個請求創建一個新的Vue實例,路由也是如此,通過一個工廠函數來保證每次都是新創建一個Vue路由的新實例。
Vuex 配置配置Vuex, 生成store.js
import Vue from "vue" import Vuex from "vuex" import { fetchItem } from "./api" Vue.use(Vuex) export const createStore = () => { return new Vuex.Store({ state: { items: {} }, actions: { fetchItem ({ commit }, id) { return fetchItem(id).then(item => { commit("setItem", { id, item }) }) } }, mutations: { setItem (state, { id, item }) { Vue.set(state.items, id, item) } } }) }
同樣也是通過一個工廠函數,來創建一個新的Vuex實例并暴露該方法
生成一個Vue的根實例創建Vue實例,生成app.js
import Vue from "vue" import App from "./App.vue" import { createRouter } from "./router" import { createStore } from "./store" import { sync } from "vuex-router-sync" export const createApp = ssrContext => { const router = createRouter() const store = createStore() sync(store, router) const app = new Vue({ router, store, ssrContext, render: h => h(App) }) return { app, store, router } }
通過使用我們編寫的createRouter, createStore來每次都創建新的Vue-router和Vuex實例,保證和Vue的實例一樣都是重新創建過的,接著掛載注冊router和store到Vue的實例中,提供createApp傳入服務端渲染對應的數據上下文。
到此我們已經基本完成source部分的工作了。接著就要考慮如何去編譯打包這些文件,讓瀏覽器和Node服務端去運行解析。
先從前端入口文件開始前端打包入口文件: entry-client.js
import { createApp } from "./app" const { app, store, router } = createApp() if (window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__) } router.onReady(() => { router.beforeResolve((to, from, next) => { const matched = router.getMatchedComponents(to) const prevMatched = router.getMatchedComponents(from) let diffed = false const activated = matched.filter((c, i) => { return diffed || (diffed = (prevMatched[i] !== c)) }) if (!activated.length) { return next() } Promise.all(activated.map(c => { if (c.asyncData) { return c.asyncData({ store, route: to }) } })).then(() => { next() }).catch(next) }) app.$mount("#app") })
客戶端的entry只需創建應用程序,并且將其掛載到 DOM 中, 需要注意的是,任然需要在掛載 app 之前調用 router.onReady,因為路由器必須要提前解析路由配置中的異步組件,(如果你有使用異步組件的話,本項目沒有使用到異步組件,但后續考慮加入) 才能正確地調用組件中可能存在的路由鉤子。通過添加路由鉤子函數,用于處理 asyncData,在初始路由 resolve 后執行,以便我們不會二次預取(double-fetch)已有的數據。使用 router.beforeResolve(),以便確保所有異步組件都 resolve,并對比之前沒有渲染的組件找出兩個匹配列表的差異組件,如果沒有差異表示無需處理直接next輸出。
再看服務端渲染解析入口文件服務端渲染的執行入口文件: entry-server.js
import { createApp } from "./app" export default context => { return new Promise((resolve, reject) => { const { app, store, router } = createApp(context) router.push(context.url) router.onReady(() => { const matchedComponents = router.getMatchedComponents() if (!matchedComponents.length) { return reject({ code: 404 }) } Promise.all(matchedComponents.map(Component => { if (Component.asyncData) { return Component.asyncData({ store, route: router.currentRoute }) } })).then(() => { context.state = store.state resolve(app) }).catch(reject) }, reject) }) }
服務器 entry 使用 default export 導出函數,并在每次渲染中重復調用此函數。此時,創建和返回應用程序實例之外,還在此執行服務器端路由匹配(server-side route matching)和數據預取邏輯(data pre-fetching logic)。在所有預取鉤子(preFetch hook) resolve 后,我們的 store 現在已經填充入渲染應用程序所需的狀態。當我們將狀態附加到上下文,并且 template 選項用于 renderer 時,狀態將自動序列化為 window.__INITIAL_STATE__,并注入 HTML。
激動人心的來寫webpack直接上手weback4.x版本
webpack配置分為3個配置,公用配置,客戶端配置,服務端配置。
三個配置文件以此如下:
base config:
const path = require("path") const webpack = require("webpack") const ExtractTextPlugin = require("extract-text-webpack-plugin") module.exports = { devtool: "#cheap-module-source-map", output: { path: path.resolve(__dirname, "../dist"), publicPath: "/", filename: "[name]-[chunkhash].js" }, resolve: { alias: { "public": path.resolve(__dirname, "../public"), "components": path.resolve(__dirname, "../components") }, extensions: [".js", ".vue"] }, module: { rules: [ { test: /.vue$/, use: { loader: "vue-loader" } }, { test: /.js$/, use: "babel-loader", exclude: /node_modules/ }, { test: /.css$/, use: "css-loader" } ] }, performance: { maxEntrypointSize: 300000, hints: "warning" }, plugins: [ new ExtractTextPlugin({ filename: "common.[chunkhash].css" }) ] }
改配置只是簡單的配置vue, css, babel等loader的使用,接著ExtractTextPlugin提取css資源文件,指定輸出的目錄,而入口文件則分別在client和server的config中配置。
client config
const webpack = require("webpack") const merge = require("webpack-merge") const path = require("path") const baseConfig = require("./webpack.base.config.js") const VueSSRClientPlugin = require("vue-server-renderer/client-plugin") module.exports = merge(baseConfig, { entry: path.resolve(__dirname, "../entry-client.js"), plugins: [ new VueSSRClientPlugin() ], optimization: { splitChunks: { cacheGroups: { commons: { chunks: "initial", minChunks: 2, maxInitialRequests: 5, minSize: 0 }, vendor: { test: /node_modules/, chunks: "initial", name: "vendor", priority: 10, enforce: true } } }, runtimeChunk: true } })
客戶端的入口文件,使用VueSSRClientPlugin生成對應的vue-ssr-client-manifest.json的映射文件,然后添加vendor的chunk分離。
server config
const merge = require("webpack-merge") const path = require("path") const nodeExternals = require("webpack-node-externals") const baseConfig = require("./webpack.base.config.js") const VueSSRServerPlugin = require("vue-server-renderer/server-plugin") module.exports = merge(baseConfig, { // 將 entry 指向應用程序的 server entry 文件 entry: path.resolve(__dirname, "../entry-server.js"), // 允許 webpack Node 適用方式(Node-appropriate fashion)處理動態導入(dynamic import), target: "node", // 提供 source map 支持 devtool: "source-map", // 使用 Node 風格導出模塊(Node-style exports) output: { filename: "server-bundle.js", libraryTarget: "commonjs2" }, externals: nodeExternals({ // 不要外置化 webpack 需要處理的依賴模塊。 // 你可以在這里添加更多的文件類型。例如,未處理 *.vue 原始文件, // 你還應該將修改 `global`(例如 polyfill)的依賴模塊列入白名單 whitelist: /.css$/ }), // 這是將服務器的整個輸出 // 構建為單個 JSON 文件的插件。 // 默認文件名為 `vue-ssr-server-bundle.json` plugins: [ new VueSSRServerPlugin() ] })
到此打包的流程已經結束了,server端配置參考了官網的注釋。
使用Koa2const { createBundleRenderer } = require("vue-server-renderer") const serverBundle = require("./dist/vue-ssr-server-bundle.json") const clientManifest = require("./dist/vue-ssr-client-manifest.json") const fs = require("fs") const path = require("path") const Koa = require("koa") const KoaRuoter = require("koa-router") const serve = require("koa-static") const app = new Koa() const router = new KoaRuoter() const template = fs.readFileSync(path.resolve("./index.template.html"), "utf-8") const renderer = createBundleRenderer(serverBundle, { // 推薦 runInNewContext: false, // (可選)頁面模板 template, // (可選)客戶端構建 manifest clientManifest }) app.use(serve(path.resolve(__dirname, "./dist"))) router.get("*", (ctx, next) => { ctx.set("Content-Type", "text/html") return new Promise((resolve, reject) => { const handleError = err => { if (err && err.code === 404) { ctx.status = 404 ctx.body = "404 | Page Not Found" } else { ctx.status = 500 ctx.body = "500 | Internal Server Error" console.error(`error during render : ${ctx.url}`) console.error(err.stack) } resolve() } console.log(ctx.url) const context = { url: ctx.url, title: "Vue SSR" } // 這里無需傳入一個應用程序,因為在執行 bundle 時已經自動創建過。 // 現在我們的服務器與應用程序已經解耦! renderer.renderToString(context, (err, html) => { // 處理異常…… if (err) { handleError(err) } ctx.body = html resolve() }) }) }) app.use(router.routes()).use(router.allowedMethods()) const port = 3000 app.listen(port, "127.0.0.1", () => { console.log(`server running at localhost:${port}`) })
最后效果當然是這樣的了:
參考文檔:
vue-ssr官方文檔
代碼倉庫:
github鏈接
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/93856.html
摘要:開始改建補充安裝依賴與上一次不同,這次我們基于進行改建,已經有了很多依賴庫了,但我們任需要補充一個核心修改客戶端的配置修改文件,添加插件添加了這個配置以后,重新啟動項目通過地址就可以訪問到,頁面中出現的內容就是所需要的。 從零開始搭建一個vue-ssr 前言 上次我們已經實現了從零開始,搭建一個簡單的vue-ssr的demo:從零開始搭建一個vue-ssr(上)。那么這次呢,我們基于v...
摘要:靜態頁面的或者明顯最短,原因是模板幾乎沒什么內容。靜態頁面生成的白屏時間中,大部分是首屏數據請求消耗的時間,,同時也可以對比出,服務器渲染的對首屏時間的確有很明顯的效果。 歡迎大家前往騰訊云+社區,獲取更多騰訊海量技術實踐干貨哦~ 本文由shirishiyue發表于云+社區專欄 目前我這邊的web頁面,都是采用php+smarty模板生成的,是一種比較早期的開發模式。好處是沒有現階段...
摘要:靜態頁面的或者明顯最短,原因是模板幾乎沒什么內容。靜態頁面生成的白屏時間中,大部分是首屏數據請求消耗的時間,,同時也可以對比出,服務器渲染的對首屏時間的確有很明顯的效果。歡迎大家前往騰訊云+社區,獲取更多騰訊海量技術實踐干貨哦~ 本文由shirishiyue發表于云+社區專欄 目前我這邊的web頁面,都是采用php+smarty模板生成的,是一種比較早期的開發模式。好處是沒有現階段常用的...
摘要:從零開始搭建一個背景是什么全拼是,服務端渲染。大家不妨可以打開一些頁面或者一些公司的網站,查看源代碼,你會發現,也是有這個標記。這時候,我們發現頁面的路由切換生效了,并且不同頁面的源代碼也不一樣了。從零開始搭建一個下項目源碼 從零開始搭建一個vue-ssr 背景 What?SSR是什么? SSR全拼是Server-Side Rendering,服務端渲染。 所謂服務端渲染,指的是把...
摘要:后端主要使用的框架,數據庫采用。后臺管理登錄采用與后端進行登陸狀態的確認。本文首發于小站,這是一個積累和分享知識的個人博客 這篇文章擱置了很長時間,最終決定還是把它寫出來,給剛開始學習vue并且想用vue寫個人博客的同學一個參考。因為當初我也是參考了其他人分享的知識,從一個vue小白變成了一個入門級選手,并最終完成了這個個人博客的搭建工作,代碼已托管在Github-justJokee。...
閱讀 2222·2021-09-24 10:31
閱讀 3875·2021-09-22 15:16
閱讀 3395·2021-09-22 10:02
閱讀 1010·2021-09-22 10:02
閱讀 1822·2021-09-08 09:36
閱讀 1974·2019-08-30 14:18
閱讀 609·2019-08-30 10:51
閱讀 1863·2019-08-29 11:08