什么是服务器端渲染 (SSR)?为什么使用服务器端渲染 (SSR)?看这 Vue SSR 指南技术栈vue、vue-router、vuexkoa2webpack4axiosbabel、eslintcss、stylus、postcsspm2目录层次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和官方的略有区别。├── webpack.base.config.js # 基本配置 (base config) ├── webpack.client.config.js # 客户端配置 (client config)├── webpack.server.config.js # 服务器配置 (server config)具体webpack配置代码这里省略…对于客户端应用程序和服务器应用程序,我们都要使用 webpack 打包 - 服务器需要「服务器 bundle」然后用于服务器端渲染(SSR),而「客户端 bundle」会发送给浏览器,用于混合静态标记。基本流程如下图:项目代码├── entry-client.js # 仅运行于浏览器├── entry-server.js # 仅运行于服务器├── index.js # 通用 entry├── router.js # 路由配置├── routes.js # 汇聚业务模块所有路由route配置index.jsindex.js 是我们应用程序的「通用 entry」,对外导出一个 createApp 函数。这里使用工厂模式为为每个请求创建一个新的根 Vue 实例,从而避免server端单例模式,如果我们在多个请求之间使用一个共享的实例,很容易导致交叉请求状态污染。entry-client.js:客户端 entry 只需创建应用程序,并且将其挂载到 DOM 中: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 导出函数,并在每次渲染中重复调用此函数。此时,除了创建和返回应用程序实例之外,还在此执行服务器端路由匹配和数据预取逻辑。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.jsrouter和store也都是工厂模式,routes是业务模块路由配置的集合。routerimport 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}routeimport 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 routesstoreimport 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请求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 = Cookiesexport default httpstore中已经注入到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()…cookies穿透在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 }……其他title处理参考官方用到全局变量的第三方插件、组件如何处理等等流式渲染预渲染……还有很多优化、深坑,看看官方文档、踩踩就知道了Koa官方使用express框架。express虽然现在也支持async、await,不过独爱koa。koa主文件// 引入相关包和中间件等等const Koa = require(‘koa’)…const appConfig = require(’./../app.config’)const uri = http://${currentIP}:${appConfig.appPort}// koa serverconst 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.jsvue koa2 ssr中间件开发模式直接使用setup.dev.server.jswebpack hot热更新生产模块直接读取dist目录的文件路由匹配匹配proxy代理配置,接口请求进入proxyMiddleWare.js接口代理中间件非接口进入render(),返回htmlconst 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’).proxyconst 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.jskoa2的webpack热更新配置和相关中间件的代码,这里就不贴出来了,和express略有区别。部署Pm2简介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, // 进程数 1、数字 2、‘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_productionorpm2 start ml-apppm2的用法和参数说明可以参考pm2.md,也可参考PM2实用入门指南Nginx在pm2基础上,Nginx配置upstream实现负载均衡在http节点下,加入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; …..}将server节点下的location节点中的proxy_pass配置为:http:// + server_name,即“ http://server_name”.location / { proxy_pass http://server_name; proxy_set_header Host localhost; proxy_set_header X-Forwarded-For $remote_addr}详细配置参考文档如果应用服务是域名子路径ssr的话,需要注意如下location除了需要设置匹配/ssr规则之外,还需设置接口、资源的前缀比如(/api,/dist) location ~ /(ssr|api|dist) {…}vue的路由也该设置base:’/ssr’entry-server.js里router.push(url)这里,url应该把/ssr去掉,即router.push(url.replace(’/ssr’,’’’))参考文档vue官方文档koanginxpm2Demo地址 服务器带宽垃圾,将就看看。 git仓库地址还有很多不足,后续慢慢折腾….结束语:生命的价值在于瞎折腾
...