看這 Vue SSR 指南css
webpack4-ssr-config ├── client # 項目代碼目錄 │ ├── assets # css、images等靜態資源目錄 │ ├── components # 項目自定義組件目錄 │ ├── plugins # 第三方插件(只能在客戶端運行)目錄,好比 編輯器 │ ├── store # vuex數據存儲目錄 │ ├── utils # 通用Mixins目錄 │ ├── views # 業務視圖.vue和route路由目錄 │ ├── app.vue # │ ├── config.js # vue組件、mixins註冊,http攔截器配置等等 │ ├── entry-client.js # 僅運行於瀏覽器 │ ├── entry-server.js # 僅運行於服務器 │ ├── index.js # 通用 entry │ ├── router.js # 路由配置和相關鉤子配置 │ └── routes.js # 匯聚業務模塊全部路由route配置 ├── config # 配置文件目錄 │ ├── http # axios封裝的http請求 │ ├── logger # .vue裏this.[log,warn,info,error]和koa2裏 logger日誌輸出 │ ├── middle # koa2中間件目錄 │ │ ├── errorMiddleWare.js # 錯誤處理中間件 │ │ ├── proxyMiddleWare.js # 接口代理中間件 │ │ └── staticMiddleWare.js # 靜態資源中間件 │ ├── eslintrc.conf.js # eslint詳細配置 │ ├── index.js # server入口 │ ├── koa.server.js # koa2服務詳細配置 │ ├── setup.dev.server.js # koa2開發模式實現hot熱更新 │ ├── vue.koa.ssr.js # vue ssr的koa2中間件。匹配路由、請求接口生成dom,實現SSR │ ├── webpack.base.config.js # 基本配置 (base config) │ ├── webpack.client.config.js # 客戶端配置 (client config) │ └── webpack.server.config.js # 服務器配置 (server config) ├── dist # 代碼打包目錄 ├── log # pm2日誌輸出目錄 ├── node_modules # node包 ├── .babelrc # babel配置 ├── .eslintrc.js # eslint配置 ├── .gitignore # git配置 ├── app.config.js # 端口、代理配置、webpack配置等等 ├── constants.js # 存放常量 ├── favicon.ico # ico圖標 ├── index.template.ejs # index模板 ├── package.json # ├── package-lock.json # ├── pm2.config.js # 項目pm2配置 ├── pm2.md # pm2的api文檔 ├── postcss.config.js # postcss配置文件 └── README.md # 文檔
使用 webpack 來打包咱們的 Vue 應用程序,參考官方分紅3個配置,這裏使用的webpack4和官方的略有區別。html
├── webpack.base.config.js # 基本配置 (base config) ├── webpack.client.config.js # 客戶端配置 (client config) ├── webpack.server.config.js # 服務器配置 (server config)
具體webpack配置代碼這裏省略...vue
對於客戶端應用程序和服務器應用程序,咱們都要使用 webpack 打包 - 服務器須要「服務器 bundle」而後用於服務器端渲染(SSR),
而「客戶端 bundle」會發送給瀏覽器,用於混合靜態標記。基本流程以下圖:node
├── entry-client.js # 僅運行於瀏覽器 ├── entry-server.js # 僅運行於服務器 ├── index.js # 通用 entry ├── router.js # 路由配置 ├── routes.js # 匯聚業務模塊全部路由route配置
index.js
index.js
是咱們應用程序的「通用 entry」,對外導出一個 createApp 函數。這裏使用工廠模式爲爲每一個請求建立一個新的根 Vue 實例,
從而避免server端單例模式,若是咱們在多個請求之間使用一個共享的實例,很容易致使交叉請求狀態污染。webpack
entry-client.js
:客戶端 entry 只需建立應用程序,而且將其掛載到 DOM 中:ios
import Vue from 'vue' import { createApp } from './index' // 引入http請求 import http from './../config/http/http' ...... const { app, router, store } = createApp() if (window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__) // 客戶端和服務端保持一致 store.state.$http = http } router.onReady(() => { ...... Promise.all(asyncDataHooks.map(hook => hook({ store, router, route: to }))) .then(() => { bar.finish() next() }) .catch(next) }) // 掛載 app.$mount('#app') })
entry-server.js
:服務器 entry 使用 default export 導出函數,並在每次渲染中重複調用此函數。此時,除了建立和返回應用程序實例以外,
還在此執行服務器端路由匹配和數據預取邏輯。nginx
import { createApp } from './index' // 引入http請求 import http from './../config/http/http' // 處理ssr期間cookies穿透 import { setCookies } from './../config/http/http' // 客戶端特定引導邏輯…… const { app } = createApp() // 這裏假定 App.vue 模板中根元素具備 `id="app"` app.$mount('#app') export default context => { return new Promise((resolve, reject) => { const { app, router, store } = createApp() const { url } = context ...... // 設置服務器端 router 的位置,路由配置裏若是設置過base,url須要把url.replace(base,'')掉,否則會404 router.push(url) // 等到 router 將可能的異步組件和鉤子函數解析完 router.onReady(() => { ...... // SSR期間同步cookies setCookies(context.cookies || {}) // http注入到rootState上,方便store裏調用 store.state.$http = http // 使用Promise.all執行匹配到的Component的asyncData方法,即預取數據 Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({ store, router, route: router.currentRoute, }))).then(() => { // 在全部預取鉤子(preFetch hook) resolve 後, // 咱們的 store 如今已經填充入渲染應用程序所需的狀態。 // 當咱們將狀態附加到上下文, // 而且 `template` 選項用於 renderer 時, // 狀態將自動序列化爲 `window.__INITIAL_STATE__`,並注入 HTML。 context.state = store.state resolve(app) }).catch(reject) }, reject) }) }
router.js
、routes.js
、store.js
router和store也都是工廠模式,routes是業務模塊路由配置的集合。git
routergithub
import Vue from 'vue' import Router from 'vue-router' import routes from './routes' Vue.use(Router) export function createRouter() { const router = new Router({ mode: 'history', fallback: false, // base: '/ssr', routes }) router.beforeEach((to, from, next) => { /*todo * 作權限驗證的時候,服務端和客戶端狀態同步的時候會執行一次 * 建議vuex裏用一個狀態值控制,默認false,同步時直接next,由於服務端已經執行過。 * */ next() }) router.afterEach((route) => { /*todo*/ }) return router }
routeweb
import testRoutes from './views/test/routes' import entry from './app.vue' const home = () => import('./views/home.vue') const routes = [ { path: '/', component: home }, { path: '/test', component: entry, children: testRoutes }, ] export default routes
store
import Vue from 'vue' import Vuex from 'vuex' import test from './modules/test' Vue.use(Vuex) export function createStore() { return new Vuex.Store({ modules: { test } }) }
http使用Axios庫封裝
/** * Created by zdliuccit on 2019/1/14. * @file axios封裝 * export default http 接口請求 * export addRequestInterceptor 請求前攔截器 * export addResponseInterceptor 請求後攔截器 * export setCookies 同步cookie */ import axios from 'axios' const currentIP = require('ip').address() const appConfig = require('./../../app.config') const defaultHeaders = { Accept: 'application/json, text/plain, */*; charset=utf-8', 'Content-Type': 'application/json; charset=utf-8', Pragma: 'no-cache', 'Cache-Control': 'no-cache', } Object.assign(axios.defaults.headers.common, defaultHeaders) if (!process.browser) { axios.defaults.baseURL = `http://${currentIP}:${appConfig.appPort}` } const methods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'request', 'head'] const http = {} methods.forEach(method => { http[method] = axios[method].bind(axios) }) export const addRequestInterceptor = (resolve, reject) => { if (axios.interceptors.request.handlers.length === 0) axios.interceptors.request.use(resolve, reject) } export const addResponseInterceptor = (resolve, reject) => { if (axios.interceptors.response.handlers.length === 0) axios.interceptors.response.use(resolve, reject) } export const setCookies = Cookies => axios.defaults.headers.cookie = Cookies export default http
store中已經注入到rootState,使用以下:
loading({ commit, rootState: { $http } }) { return $http.get('path').then(res => { ... }) }
在config.js
中,把http註冊到vue的原型鏈和配置request、response的攔截器
import Vue from 'vue' // 引入http請求插件 import http from './../config/http' // 引入log日誌插件 import { addRequestInterceptor, addResponseInterceptor } from './../config/http/http' import titleMixin from './utils/title' // 引入log日誌插件 import vueLogger from './../config/logger/vue-logger' // 註冊插件 Vue.use(http) Vue.use(vueLogger) Vue.mixin(titleMixin) // request前自動添加api配置 addRequestInterceptor( (config) => { /*統一加/api前綴*/ config.url = `/api${config.url}` return config }, (error) => { return Promise.reject(error) } ) // http 返回response前處理 addResponseInterceptor( (response) => { /*todo 在這裏統一前置處理請求響應 */ return Promise.resolve(response.data) }, (error) => { /* * todo 統一處理500、400等錯誤狀態 * 這裏reject下,交給entry-server.js的處理 */ const { response, request } = error return Promise.reject({ code: response.status, data: response.data, method: request.method, path: request.path }) } )
這樣,.vue中間中直接調用this.$http.get()、this.$http.post()...
在ssr期間咱們須要截取客戶端的cookie,保持用戶會話惟一性。
在entry-server.js
中使用setCookies方法,傳入的參數是從context上獲取。
...... // SSR期間同步cookies setCookies(context.cookies || {}) ......
在vue.koa.ssr.js
代碼中往context注入cookie
...... const context = { url: ctx.url, title: 'Vue Koa2 SSR', cookies: ctx.request.headers.cookie } ......
還有不少優化、深坑,看看官方文檔、踩踩就知道了
官方使用express框架。express雖然如今也支持async、await,不過獨愛koa。
// 引入相關包和中間件等等 const Koa = require('koa') ... const appConfig = require('./../app.config') const uri = `http://${currentIP}:${appConfig.appPort}` // koa server const app = new Koa() // 定義中間件, const middleWares = [ ...... ] middleWares.forEach((middleware) => { if (!middleware) { return } app.use(middleware) }) // vue ssr處理 vueKoaSSR(app, uri) // http代理中間件 app.use(proxyMiddleWare()) console.log(`\n> Starting server... ${uri} \n`) // 錯誤處理 app.on('error', (err) => { // console.error('Server error: \n%s\n%s ', err.stack || '') }) app.listen(appConfig.appPort)
vue.koa.ssr.js
vue koa2 ssr中間件
setup.dev.server.js
webpack hot熱更新dist
目錄的文件路由匹配
proxyMiddleWare.js
接口代理中間件const fs = require('fs') const path = require('path') const LRU = require('lru-cache') const { createBundleRenderer } = require('vue-server-renderer') const isProd = process.env.NODE_ENV === 'production' const proxyConfig = require('./../app.config').proxy const setUpDevServer = require('./setup.dev.server') module.exports = function (app, uri) { const renderData = (ctx, renderer) => { const context = { url: ctx.url, title: 'Vue Koa2 SSR', cookies: ctx.request.headers.cookie } return new Promise((resolve, reject) => { renderer.renderToString(context, (err, html) => { if (err) { return reject(err) } resolve(html) }) }) } function createRenderer(bundle, options) { return createBundleRenderer(bundle, Object.assign(options, { cache: LRU({ max: 1000, maxAge: 1000 * 60 * 15 }), runInNewContext: false })) } function resolve(dir) { return path.resolve(process.cwd(), dir) } let renderer if (isProd) { // prod mode const template = fs.readFileSync(resolve('dist/index.html'), 'utf-8') const bundle = require(resolve('dist/vue-ssr-server-bundle.json')) const clientManifest = require(resolve('dist/vue-ssr-client-manifest.json')) renderer = createRenderer(bundle, { template, clientManifest }) } else { // dev mode setUpDevServer(app, uri, (bundle, options) => { try { renderer = createRenderer(bundle, options) } catch (e) { console.log('\nbundle error', e) } } ) } app.use(async (ctx, next) => { if (!renderer) { ctx.type = 'html' return ctx.body = 'waiting for compilation... refresh in a moment.'; } if (Object.keys(proxyConfig).findIndex(vl => ctx.url.startsWith(vl)) > -1) { return next() } let html, status try { status = 200 html = await renderData(ctx, renderer) } catch (e) { console.log('\ne', e) if (e.code === 404) { status = 404 html = '404 | Not Found' } else { status = 500 html = '500 | Internal Server Error' } } ctx.type = 'html' ctx.status = status ? status : ctx.status ctx.body = html }) }
setup.dev.server.js
koa2的webpack熱更新配置和相關中間件的代碼,這裏就不貼出來了,和express略有區別。
簡介
PM2是node進程管理工具,能夠利用它來簡化不少node應用管理的繁瑣任務,如性能監控、自動重啓、負載均衡等,並且使用很是簡單。
pm2.config.js
配置以下
module.exports = { apps: [{ name: 'ml-app', // app名稱 script: 'config/index.js', // 要運行的腳本的路徑。 args: '', // 由傳遞給腳本的參數組成的字符串或字符串數組。 output: './log/out.log', error: './log/error.log', log: './log/combined.outerr.log', merge_logs: true, // 集羣的全部實例的日誌文件合併 log_date_format: "DD-MM-YYYY", instances: 4, // 進程數 一、數字 二、'max'根據cpu內核數 max_memory_restart: '1G', // 當內存超過1024M時自動重啓 watching: true, env_test: { NODE_ENV: 'production' }, env_production: { NODE_ENV: 'production' } }], }
構建生產代碼
npm run build 構建生產代碼
pm2啓動服務
初次啓動 pm2 start pm2.config.js --env production # production 對應 env_production or pm2 start ml-app
pm2的用法和參數說明能夠參考pm2.md,也可參考PM2實用入門指南
在pm2基礎上,Nginx配置upstream實現負載均衡
upstream server_name { server 172.16.119.198:8018 max_fails=2 fail_timeout=30s; server 172.16.119.198:8019 max_fails=2 fail_timeout=30s; server 172.16.119.198:8020 max_fails=2 fail_timeout=30s; ..... }
location / { proxy_pass http://server_name; proxy_set_header Host localhost; proxy_set_header X-Forwarded-For $remote_addr }
詳細配置參考文檔
若是應用服務是域名子路徑ssr
的話,須要注意以下
/ssr
規則以外,還需設置接口、資源的前綴好比(/api,/dist) location ~ /(ssr|api|dist) {...}
base:'/ssr'
entry-server.js
裏router.push(url)
這裏,url應該把/ssr
去掉,即router.push(url.replace('/ssr','''))
Demo地址 服務器帶寬垃圾,將就看看。
git倉庫地址 喜歡的點個star Thanks!
還有不少不足,後續慢慢折騰....
結束語:生命的價值在於瞎折騰