將同一個組件渲染爲服務器端的 HTML 字符串,將它們直接發送到瀏覽器,最後將靜態標記"混合"爲客戶端上徹底交互的應用程序。
To solvecss
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遍了,每一個人理解不一樣,我發表一下本身我的的理解,若是有什麼理解錯誤請原諒我。html
先看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 + Vuexvue
先來配置vue-router, 生成router.jsnode
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路由的新實例。webpack
配置Vuex, 生成store.jsgit
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實例並暴露該方法github
建立Vue實例,生成app.jsweb
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傳入服務端渲染對應的數據上下文。vue-router
到此咱們已經基本完成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。
直接上手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端配置參考了官網的註釋。
const { 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}`) })
最後效果固然是這樣的了:
參考文檔:
代碼倉庫: