日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > linux >内容正文

linux

linux 使用ssr客户端_【第一期】基于 @vue/cli3 与 koa 创建 ssr 工程

發布時間:2024/9/27 linux 41 豆豆
生活随笔 收集整理的這篇文章主要介紹了 linux 使用ssr客户端_【第一期】基于 @vue/cli3 与 koa 创建 ssr 工程 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

什么是基于同構代碼的 SSR 服務(Server-side rendering based on isomorphic code)

首先,我們需要先明白什么是 spa (single page application),以及基于 vue 的 spa 是如何工作的,這里不展開,請參考:

單頁應用:

https://zh.wikipedia.org/wiki/%E5%8D%95%E9%A1%B5%E5%BA%94%E7%94%A8

vue 實例:

https://cn.vuejs.org/v2/guide/instance.html#%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA-Vue-%E5%AE%9E%E4%BE%8B

基于同構代碼的 SSR 指的是同一份代碼(spa代碼),既能在客戶端運行,并渲染出頁面,也可以在服務器端渲染為 html 字符串,并響應給客戶端。

它與傳統的服務器直出不同,傳統的服務器直出指的是路由系統只存在于服務器端,在服務器端,任何一個頁面都需要服務器響應內容。

SSR 有什么好處?

  • 相比 spa 應用,ssr 應用對搜索引擎更友好

  • 理論上,TTI 更短(TTI ,time to interactive,指用戶從第一個請求發出,到能夠與頁面交互,這之間的時間差)

下圖是一個實際項目中,在弱網環境(3g)中接入?ssr?服務之前和之后的請求耗時對比:

工程背景:實際項目在微信環境內提供h5頁面,為提高用戶體驗,我們將其接入?ssr?服務,并代理微信 OAuth 的部分過程

測量范圍:新客戶從第一個http請求發出,到入口頁面的內容下載完畢為止

接入?ssr?服務前,此測量范圍內會經歷:

  • 客戶端下載入口文件、js、css等資源

  • 客戶端跳轉微信授權服務,獲取授權 code

  • 客戶端跳回源地址,進行授權登錄(客戶可看到頁面)

  • 接入?ssr?服務后,此測量范圍內會經歷:

  • 服務器跳轉微信授權服務,獲取授權 code

  • 客戶端下載入口文件、js、css等資源(客戶可看到頁面)

  • 客戶端授權登錄

  • 我們可以看到,接入?ssr?服務后,客戶理論上能更早得看到頁面了

    根據上圖可以看到,在接入?ssr?服務后,客戶能更早得看到頁面內容,客戶感知到的性能提高了。

    SSR 有什么風險?

    • 加重服務器負載

    • 通常用于 SSR 的服務都是基于 NodeJS 環境,需要額外的研發成本(例如:日志、監控、追蹤)

    • SSR 的服務通常都由前端工程師研發和維護,增加了更多的心智負擔

    • 基于同構代碼的 SSR 應用是同一份代碼,既在瀏覽器運行,也在服務器運行,代碼層面的問題造成的影響更大

    今天,我們使用新版的 cli 工具(v3.x),搭建一個基于 vue 同構代碼的 ssr 工程項目。

    我們的目標:使用 @vue/cli v3.x 與 koa v2.x 創建一個 ssr 工程

    我們的步驟如下:

  • 安裝 @vue/cli

  • 使用 @vue/cli 創建 spa 工程

  • 將 spa 工程逐步改造成 ssr 工程

  • 我們需要的工具如下:

    • @vue/cli v3.x

    • koa v2.x

    • koa-send v5.x

    • vue-server-renderer v2.x

    • memory-fs v0.x

    • lodash.get v4.x

    • lodash.merge v4.x

    • axios v0.x

    • ejs v2.x

    第一步:安裝 @vue/cli v3.x

    yarn global add @vue/cli

    筆者安裝的 @vue/cli 的版本為:?v3.6.2

    第二步:使用 @vue/cli 創建一個 spa 應用

    vue create ssr-demo

    創建完畢之后, ssr-demo 的目錄結構如下:

    ./ssr-demo├── README.md├── babel.config.js├── package.json├── public│ ├── favicon.ico│ └── index.html├── src│ ├── App.vue│ ├── assets│ │ └── logo.png│ ├── components│ │ └── HelloWorld.vue│ ├── main.js│ ├── router.js│ ├── store.js│ └── views│ ├── About.vue│ └── Home.vue└── yarn.lock

    進入 srr-demo ,安裝?vue-server-renderer

    yarn add vue-server-renderer

    筆者創建的 ssr-demo 中,各主要工具庫的版本如下:

    • vue?v2.6.10

    • vue-router?v3.0.3

    • vuex?v3.0.1

    • vue-template-compiler?v2.5.21

    • vue-server-renderer?v2.6.10

    執行 yarn serve ,在瀏覽器上看一下效果。

    至此,spa 工程就創建完畢了,接下來我們在此基礎上,將此 spa 工程逐步轉換為 ssr 工程模式。

    第三步:單例模式改造

    在 spa 工程中,每個客戶端都會擁有一個新的 vue 實例。

    因此,在 ssr 工程中,我們也需要為每個客戶端請求分配一個新的 vue 實例(包括 router 和 store)。

    我們的步驟如下:

  • 改造狀態存儲?src/store.js

  • 改造路由?src/router.js

  • 改造應用入口?src/main.js

  • 改造步驟一:改造狀態存儲

    改造前,我們看下?src/store.js?的內容:

    import Vue from 'vue'import Vuex from 'vuex'Vue.use(Vuex)export default new Vuex.Store({ state: { }, mutations: { }, actions: { }})

    src/store.js?的內部只返回了一個 store 實例。

    如果這份代碼在服務器端運行,那么這個 store 實例會在服務進程的整個生命周期中存在。

    這會導致所有的客戶端請求都共享了一個 store 實例,這顯然不是我們的目的,因此我們需要將狀態存儲文件改造成工廠函數,代碼如下:

    import Vue from 'vue'import Vuex from 'vuex'Vue.use(Vuex)export function createStore () { return new Vuex.Store({ state: { }, mutations: { }, actions: { } })}

    目錄結構同樣有變化:

    # 改造前./src├── ...├── store.js├── ...# 改造后./src├── ...├── store│ └── index.js├── ...

    改造步驟二:改造路由

    改造前,我們看下?src/router.js?的內容:

    import Vue from 'vue'import Router from 'vue-router'import Home from './views/Home.vue'Vue.use(Router)export default new Router({ mode: 'history', base: process.env.BASE_URL, routes: [ { path: '/', name: 'home', component: Home }, { path: '/about', name: 'about', // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () => import(/* webpackChunkName: "about" */ './views/About.vue') } ]})

    類似?src/store.js, 路由文件:src/router.js?的內部也只是返回了一個 router 實例。

    如果這份代碼在服務器端運行,那么這個 router 實例會在服務進程的整個生命周期中存在。

    這會導致所有的客戶端請求都共享了一個 router 實例,這顯然不是我們的目的,因此我們需要將路由改造成工廠函數,代碼如下:

    import Vue from 'vue'import Router from 'vue-router'import Home from '../views/Home.vue'Vue.use(Router)export function createRouter () { return new Router({ mode: 'history', base: process.env.BASE_URL, routes: [ { path: '/', name: 'home', component: Home }, { path: '/about', name: 'about', // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') } ] })}

    目錄結構也有變化:

    # 改造前./src├── ...├── router.js├── ...# 改造后./src├── ...├── router│ └── index.js├── ...

    改造步驟三:改造應用入口

    因為我們需要在服務器端運行與客戶端相同的代碼,所以免不了需要讓服務器端也依賴 webpack 的構建過程。

    借用官方文檔的示意圖:

    我們看到:

    源代碼分別為客戶端和服務器提供了獨立的入口文件:server entry 和 client entry

    通過 webpack 的構建過程,構建完成后,也對應得輸出了兩份 bundle 文件,分別為客戶端和服務器提供了:

    • chunk 文件映射路徑

    • 源代碼定位

    • 源代碼打包(服務器端的 bundle 文件包含了所有打包后的客戶端代碼)

      等功能。

    因此,我們接下來先改造?src/main.js,然后再創建?entry-client.js?和?entry-server.js

    改造?src/main.js?前,我們先來看看?src/main.js?的內容:

    import Vue from 'vue'import App from './App.vue'import router from './router'import store from './store'Vue.config.productionTip = falsenew Vue({ router, store, render: h => h(App)}).$mount('#app')

    與?src/store.js?和?src/router.js?類似,src/main.js?同樣也是單例模式,因此我們將它改造為工廠函數:

    import Vue from 'vue'import App from './App'import { createRouter } from './router'import { createStore } from './store'export function createApp () { const router = createRouter() const store = createStore() const app = new Vue({ router, store, render: h => h(App) }) return { app, router, store }}

    將?src/main.js?改造完畢后,我們來分別創建?entry-client.js?和?entry-server.js

    我們先來看?entry-client.js:

    import { createApp } from './main.js'const { app, router, store } = createApp()if (window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__)}router.onReady(() => { app.$mount('#app')})

    在服務器端渲染路由組件樹,所產生的?context.state?將作為脫水數據掛載到?window.__INITIAL_STATE__

    在客戶端,只需要將?window.__INITIAL_STATE__?重新注入到 store 中即可(通過?store.replaceState?函數)

    最后,我們需要將 mount 的邏輯放到客戶端入口文件內。

    創建完畢客戶端入口文件后,讓我們來看服務端的入口文件?entry-server.js:

    import { createApp } from './main.js'export default context => { return new Promise((resolve, reject) => { const { app, router, store } = createApp() router.push(context.url) router.onReady(() => { context.rendered = () => { context.state = store.state } resolve(app) }, reject) })}

    上面的?context.rendered?函數會在應用完成渲染的時候調用

    在服務器端,應用渲染完畢后,此時 store 可能已經從路由組件樹中填充進來一些數據。

    當我們將 state 掛載到 context ,并在使用 renderer 的時候傳遞了?template?選項,

    那么 state 會自動序列化并注入到 HTML 中,作為?window.__INITIAL_STATE__?存在。

    接下來,我們來給 store 添加獲取數據的邏輯,并在首頁調用其邏輯,方便后面觀察服務器端渲染后的?window.__INITIAL_STATE__

    改造 store: 添加獲取數據邏輯

    改造后的目錄結構:

    src/store├── index.js└── modules └── book.js

    src/store/index.js

    import Vue from 'vue'import Vuex from 'vuex'import { Book } from './modules/book.js'Vue.use(Vuex)export function createStore () { return new Vuex.Store({ modules: { book: Book }, state: { }, mutations: { }, actions: { } })}

    src/store/modules/book.js

    import Vue from 'vue'const getBookFromBackendApi = id => new Promise((resolve, reject) => { setTimeout(() => { resolve({ name: '《地球往事》', price: 100 }) }, 300)})export const Book = { namespaced: true, state: { items: {} }, actions: { fetchItem ({ commit }, id) { return getBookFromBackendApi(id).then(item => { commit('setItem', { id, item }) }) } }, mutations: { setItem (state, { id, item }) { Vue.set(state.items, id, item) } }}

    改造首頁:預取數據

    改造前,我們先看一下?src/views/Home.vue?的代碼

    <template> <div class="home"> <img alt="Vue logo" src="../assets/logo.png"> <HelloWorld msg="Welcome to Your Vue.js App"/> div>template><script>// @ is an alias to /srcimport HelloWorld from '@/components/HelloWorld.vue'export default { name: 'home', components: { HelloWorld }}script>

    改造后的代碼如下:

    <template> <div class="home"> <img alt="Vue logo" src="../assets/logo.png"> <HelloWorld msg="Welcome to Your Vue.js App"/> <div v-if="book">{{ book.name }}div> <div v-else>nothingdiv> div>template><script>// @ is an alias to /srcimport HelloWorld from '@/components/HelloWorld.vue'export default { name: 'home', computed: { book () { return this.$store.state.book.items[this.$route.params.id || 1] } }, // 此函數只會在服務器端調用,注意,只有 vue v2.6.0+ 才支持此函數 serverPrefetch () { return this.fetchBookItem() }, // 此生命周期函數只會在客戶端調用 // 客戶端需要判斷在 item 不存在的場景再去調用 fetchBookItem 方法獲取數據 mounted () { if (!this.item) { this.fetchBookItem() } }, methods: { fetchBookItem () { // 這里要求 book 的 fetchItem 返回一個 Promise return this.$store.dispatch('book/fetchItem', this.$route.params.id || 1) } }, components: { HelloWorld }}script>

    至此,客戶端源代碼的改造告一段落,我們接下來配置構建過程

    配置 vue.config.js

    基于?@vue/cli v3.x?創建的客戶端工程項目中不再有?webpack.xxx.conf.js?這類文件了。

    取而代之的是?vue.config.js?文件,它是一個可選的配置文件,默認在工程的根目錄下,由?@vue/cli-service?自動加載并解析。

    我們對于?webpack?的所有配置,都通過?vue.config.js?來實現。

    關于?vue.config.js?內部配置的詳細信息,請參考官方文檔:https://cli.vuejs.org/zh/config/#vue-config-js

    const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')const nodeExternals = require('webpack-node-externals')const merge = require('lodash.merge')const TARGET_NODE = process.env.TARGET_NODE === 'node'const DEV_MODE = process.env.NODE_ENV === 'development'const config = { publicPath: process.env.NODE_ENV === 'production' // 在這里定義產品環境和其它環境的 publicPath // 關于 publicPath 請參考: // https://webpack.docschina.org/configuration/output/#output-publicpath ? '/' : '/', chainWebpack: config => { if (DEV_MODE) { config.devServer.headers({ 'Access-Control-Allow-Origin': '*' }) } config .entry('app') .clear() .add('./src/entry-client.js') .end() // 為了讓服務器端和客戶端能夠共享同一份入口模板文件 // 需要讓入口模板文件支持動態模板語法(這里選了 ejs) .plugin('html') .tap(args => { return [{ template: './public/index.ejs', minify: { collapseWhitespace: true }, templateParameters: { title: 'spa', mode: 'client' } }] }) .end() // webpack 的 copy 插件默認會將 public 文件夾中所有的文件拷貝到輸出目錄 dist 中 // 這里我們需要將 index.ejs 文件排除 .when(config.plugins.has('copy'), config => { config.plugin('copy').tap(([[config]]) => [ [ { ...config, ignore: [...config.ignore, 'index.ejs'] } ] ]) }) .end() // 默認值: 當 webpack 配置中包含 target: 'node' 且 vue-template-compiler 版本號大于等于 2.4.0 時為 true。 // 開啟 Vue 2.4 服務端渲染的編譯優化之后,渲染函數將會把返回的 vdom 樹的一部分編譯為字符串,以提升服務端渲染的性能。 // 在一些情況下,你可能想要明確的將其關掉,因為該渲染函數只能用于服務端渲染,而不能用于客戶端渲染或測試環境。 config.module .rule('vue') .use('vue-loader') .tap(options => { merge(options, { optimizeSSR: false }) }) config.plugins // Delete plugins that are unnecessary/broken in SSR & add Vue SSR plugin .delete('pwa') .end() .plugin('vue-ssr') .use(TARGET_NODE // 這是將服務器的整個輸出構建為單個 JSON 文件的插件。 // 默認文件名為 `vue-ssr-server-bundle.json` ? VueSSRServerPlugin // 此插件在輸出目錄中生成 `vue-ssr-client-manifest.json` : VueSSRClientPlugin) .end() if (!TARGET_NODE) return config .entry('app') .clear() .add('./src/entry-server.js') .end() .target('node') .devtool('source-map') .externals(nodeExternals({ whitelist: /\.css$/ })) .output.filename('server-bundle.js') .libraryTarget('commonjs2') .end() .optimization.splitChunks({}) .end() .plugins.delete('named-chunks') .delete('hmr') .delete('workbox') }}module.exports = config

    至此,客戶端部分的改造告一段落,當前?ssr-demo?的目錄如下:

    ./ssr-demo├── README.md├── babel.config.js├── package.json├── public│ ├── favicon.ico│ └── index.ejs├── src│ ├── App.vue│ ├── assets│ │ └── logo.png│ ├── components│ │ └── HelloWorld.vue│ ├── entry-client.js│ ├── entry-server.js│ ├── main.js│ ├── router│ │ └── index.js│ ├── store│ │ ├── index.js│ │ └── modules│ │ └── book.js│ └── views│ ├── About.vue│ └── Home.vue├── vue.config.js└── yarn.lock

    接下來,讓我們來搭建 NodeJS 服務端部分。

    第四步:NodeJS 服務端搭建

    在搭建服務端之前,我們先安裝服務端需要的依賴:

    yarn add koa koa-send memory-fs lodash.get axios ejs

    安裝完畢后,對應的版本如下:

    • koa?v2.7.0

    • koa-send?v5.0.0

    • memory-fs?v0.4.1

    • lodash.get?v4.4.2

    • axios?v0.18.0

    • ejs?v2.6.1

    生產環境服務搭建

    在?ssr-demo?跟目錄下創建文件夾?app,然后創建文件?server.js,內容如下:

    const Koa = require('koa')const app = new Koa()const host = '127.0.0.1'const port = process.env.PORTconst productionEnv = ['production', 'test']const isProd = productionEnv.includes(process.env.NODE_ENV)const fs = require('fs')const PWD = process.env.PWD// 產品環境:我們在服務端進程啟動時,將客戶端入口文件讀取到內存中,當 發生異常 或 需要返回客戶端入口文件時響應給客戶端。const getClientEntryFile = isProd => isProd ? fs.readFileSync(PWD + '/dist/index.html') : ''const clientEntryFile = getClientEntryFile(isProd)app.use(async (ctx, next) => { if (ctx.method !== 'GET') return try { await next() } catch (err) { ctx.set('content-type', 'text/html') if (err.code === 404) { ctx.body = clientEntryFile return } console.error(' [SERVER ERROR] ', err.toString()) ctx.body = clientEntryFile }})app.use(require('./middlewares/prod.ssr.js'))app.listen(port, host, () => { console.log(`[${process.pid}]server started at ${host}:${port}`)})

    其中,需要注意的是:應該捕獲服務端拋出的任何異常,并將客戶端入口文件響應給客戶端。

    在?app?內創建文件夾?middlewares,并創建文件?prod.ssr.js:

    const path = require('path')const fs = require('fs')const ejs = require('ejs')const get = require('lodash.get')const resolve = file => path.resolve(__dirname, file)const PWD = process.env.PWDconst enableStream = +process.env.ENABLESTREAMconst { createBundleRenderer } = require('vue-server-renderer')const bundle = require(PWD + '/dist/vue-ssr-server-bundle.json')const clientManifest = require(PWD + '/dist/vue-ssr-client-manifest.json')const tempStr = fs.readFileSync(resolve(PWD + '/public/index.ejs'), 'utf-8')const template = ejs.render(tempStr, { title: '{{title}}', mode: 'server' })const renderer = createBundleRenderer(bundle, { runInNewContext: false, template: template, clientManifest: clientManifest, basedir: PWD})const renderToString = context => new Promise((resolve, reject) => { renderer.renderToString(context, (err, html) => err ? reject(err) : resolve(html))})const renderToStream = context => renderer.renderToStream(context)const main = async (ctx, next) => { ctx.set('content-type', 'text/html') const context = { title: get(ctx, 'currentRouter.meta.title', 'ssr mode'), url: ctx.url } ctx.body = await renderToString(context)}module.exports = main

    然后,我們為 package.json 配置新的打包命令和啟動?ssr?服務的命令:

    ... "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build && TARGET_NODE=node vue-cli-service build --no-clean", "start": "NODE_ENV=production TARGET_NODE=node PORT=3000 node ./app/server.js" },...

    這里需要注意一下:

    在?build?命令中,先執行客戶端的構建命令,然后再執行服務端的構建命令。

    服務端的構建命令與客戶端的區別只有一個環境變量:TARGET_NODE,當將此變量設置值為?node,則會按照服務端配置進行構建。

    另外,在服務端構建命令中有一個參數:--no-clean,這個參數代表不要清除 dist 文件夾,保留其中的文件。

    之所以需要?--no-clean?這個參數,是因為服務端構建不應該影響到客戶端的構建文件。

    這樣能保證客戶端即使脫離了服務端,也能通過?nginx?提供的靜態服務向用戶提供完整的功能(也就是 spa 模式)。

    至此,生產環境已經搭建完畢。接下來,讓我們來搭建開發環境的服務端。

    開發環境服務搭建

    開發環境的服務功能實際上是生產環境的超集。

    除了生產環境提供的服務之外,開發環境還需要提供:

    • 靜態資源服務

    • hot reload

    搭建靜態資源服務

    生產環境中的靜態資源因為都會放置到 CDN 上,因此并不需要 NodeJS 服務來實現靜態資源服務器,一般都由 nginx 靜態服務提供 CDN 的回源支持。

    但生產環境如果依賴獨立的靜態服務器,可能導致環境搭建成本過高,因此我們創建一個開發環境的靜態資源服務中間件來實現此功能。

    我們的 spa 模式在開發環境通過命令?serve?啟動后,就是一個自帶 hot reload 功能的服務。

    因此,服務端在開發環境中提供的靜態資源服務,可以通過將靜態資源請求路由到 spa 服務,來提供靜態服務功能。

    需要注意的是:開發環境中,服務端在啟動之前,需要先啟動好 spa 服務。

    稍后我們會在?package.js?中創建?dev?命令來方便啟動開發環境的 spa 與 ssr 服務。

    在?./ssr-demo/app/middlewares/?中創建文件?dev.static.js,內容如下:

    const path = require('path')const get = require('lodash.get')const send = require('koa-send')const axios = require('axios')const PWD = process.env.PWDconst clientPort = process.env.CLIENT_PORT || 8080const devHost = `http://localhost:${clientPort}`const resolve = file => path.resolve(__dirname, file)const staticSuffixList = ['js', 'css', 'jpg', 'jpeg', 'png', 'gif', 'map', 'json']const main = async (ctx, next) => { const url = ctx.path if (url.includes('favicon.ico')) { return send(ctx, url, { root: resolve(PWD + '/public') }) } // In the development environment, you need to support every static file without CDN if (staticSuffixList.includes(url.split('.').pop())) { return ctx.redirect(devHost + url) } const clientEntryFile = await axios.get(devHost + '/index.html') ctx.set('content-type', 'text/html') ctx.set('x-powered-by', 'koa/development') ctx.body = clientEntryFile.data}module.exports = main

    然后將中間件?dev.static.js?注冊到服務端入口文件?app/server.js?中:

    ...if (process.env.NODE_ENV === 'production') { app.use(require('./middlewares/prod.ssr.js'))}else{ app.use(require('./middlewares/dev.static.js')) // TODO:在這里引入開發環境請求處理中間件}app.listen(port, host, () => { console.log(`[${process.pid}]server started at ${host}:${port}`)})

    因為我們需要在開發環境同時啟動 spa 服務和 ssr 服務,因此需要一個工具輔助我們同時執行兩個命令。

    我們選擇?concurrently,關于此工具的具體細節請參照:https://github.com/kimmobrunfeldt/concurrently

    安裝?concurrently:

    yarn add concurrently -D

    然后改造?package.json?中的?serve?命令:

    ... "scripts": { "serve": "vue-cli-service serve", "ssr:serve": "NODE_ENV=development PORT=3000 CLIENT_PORT=8080 node ./app/server.js", "dev": "concurrently 'npm run serve' 'npm run ssr:serve'",...

    其中:

    • serve?開發環境啟動 spa 服務

    • ssr:serve?開發環境啟動 ssr 服務

    • dev?開發環境同時啟動 spa 服務于 ssr 服務

    啟動 ssr 服務的命令中:

    • NODE_ENV?是環境變量

    • PORT?是 ssr 服務監聽的端口

    • CLIENT_PORT?是 spa 服務監聽的端口

    因為靜態資源需要從 spa 服務中獲取,所以 ssr 服務需要知道 spa 服務的 host 、端口 和 靜態資源路徑

    至此,靜態服務器搭建完畢,接下來我們來搭建開發環境的請求處理中間件。(此中間件包含 hot reload 功能)

    實現 hot reload

    在?./ssr-demo/app/middlewares/?中創建文件?dev.ssr.js,內容如下:

    const path = require('path')const fs = require('fs')const ejs = require('ejs')const PWD = process.env.PWDconst webpack = require('webpack')const axios = require('axios')// memory-fs is a simple in-memory filesystem.// Holds data in a javascript object// See: https://github.com/webpack/memory-fsconst MemoryFS = require('memory-fs')// Use parsed configuration as a file of webpack config// See: https://cli.vuejs.org/zh/guide/webpack.html#%E5%AE%A1%E6%9F%A5%E9%A1%B9%E7%9B%AE%E7%9A%84-webpack-%E9%85%8D%E7%BD%AEconst webpackConfig = require(PWD + '/node_modules/@vue/cli-service/webpack.config')// create a compiler of webpack configconst serverCompiler = webpack(webpackConfig)// create the memory instanceconst mfs = new MemoryFS()// set the compiler output to memory// See: https://webpack.docschina.org/api/node/#%E8%87%AA%E5%AE%9A%E4%B9%89%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F-custom-file-systems-serverCompiler.outputFileSystem = mfslet serverBundle// Monitor webpack changes because server bundles need to be dynamically updatedserverCompiler.watch({}, (err, stats) => { if (err) throw err stats = stats.toJson() stats.errors.forEach(error => console.error('ERROR:', error)) stats.warnings.forEach(warn => console.warn('WARN:', warn)) const bundlePath = path.join(webpackConfig.output.path, 'vue-ssr-server-bundle.json') serverBundle = JSON.parse(mfs.readFileSync(bundlePath, 'utf-8')) console.log('vue-ssr-server-bundle.json updated')})const resolve = file => path.resolve(__dirname, file)const { createBundleRenderer } = require('vue-server-renderer')const renderToString = (renderer, context) => new Promise((resolve, reject) => { renderer.renderToString(context, (err, html) => err ? reject(err) : resolve(html))})const tempStr = fs.readFileSync(resolve(PWD + '/index.ejs'), 'utf-8')const template = ejs.render(tempStr, { title: '{{title}}', mode: 'server' })const clientHost = process.env.CLIENT_PORT || 'localhost'const clientPort = process.env.CLIENT_PORT || 8080const clientPublicPath = process.env.CLIENT_PUBLIC_PATH || '/'const main = async (ctx, next) => { if (!serverBundle) { ctx.body = 'Wait Compiling...' return } ctx.set('content-type', 'text/html') ctx.set('x-powered-by', 'koa/development') const clientManifest = await axios.get(`http://${clientHost}:${clientPort}${clientPublicPath}vue-ssr-client-manifest.json`) const renderer = createBundleRenderer(serverBundle, { runInNewContext: false, template: template, clientManifest: clientManifest.data, basedir: process.env.PWD }) const context = { title: 'ssr mode', url: ctx.url } const html = await renderToString(renderer, context) ctx.body = html}module.exports = main

    在開發環境,我們通過?npm run dev?命令,啟動一個 webpack-dev-server 和一個 ssr 服務

    通過官方文檔可知,我們可以通過一個文件訪問解析好的 webpack 配置,這個文件路徑為:

    node_modules/@vue/cli-service/webpack.config.js

    使用 webpack 編譯此文件,并將其輸出接入到內存文件系統(memory-fs)中

    監聽 webpack,當 webpack 重新構建時,我們在監聽器內部獲取最新的 server bundle 文件

    并從 webpack-dev-server 獲取 client bundle 文件

    在每次處理 ssr 請求的中間件邏輯中,使用最新的 server bundle 文件和 client bundle 文件進行渲染

    最后,將中間件?dev.ssr.js?注冊到服務端入口文件?app/server.js?中

    ...if (process.env.NODE_ENV === 'production') { app.use(require('./middlewares/prod.ssr.js'))}else{ app.use(require('./middlewares/dev.static.js')) app.use(require('./middlewares/dev.ssr.js'))}app.listen(port, host, () => { console.log(`[${process.pid}]server started at ${host}:${port}`)})

    至此,我們基于?@vue/cli v3?完成了一個簡易的 ssr 工程項目,目錄結構如下:

    ./ssr-demo├── README.md├── app│ ├── middlewares│ │ ├── dev.ssr.js│ │ ├── dev.static.js│ │ └── prod.ssr.js│ └── server.js├──?babel.config.js├── package.json├── public│???└──?index.ejs├── src│ ├── App.vue│ ├── assets│ │ └── logo.png│ ├── components│ │ └── HelloWorld.vue│ ├── entry-client.js│ ├── entry-server.js│ ├── main.js│ ├── router│ │ └── index.js│ ├── store│ │ ├── index.js│ │ └── modules│ │ └── book.js│ └── views│ ├── About.vue│ └── Home.vue├── vue.config.js└── yarn.lock

    以上,是我們基于?@vue/cli v3?構建?ssr?工程的全部過程。

    雖然我們已經有了一個基礎的?ssr?工程,但這個工程項目還有以下缺失的地方:

    • 沒有降級策略,如果?ssr?服務出現異常,整個服務就會受到影響,我們需要考慮在?ssr?服務出現問題時,如何將其降級為?spa?服務

    • 沒有日志系統,ssr?服務內部接收到的請求信息、出現的異常信息、關鍵業務的信息,這些都需要記錄日志,方便維護與追蹤定位錯誤。

    • 沒有緩存策略,我們搭建的?ssr?服務對于每一次的請求,都會耗費服務器資源去渲染,這對于那些一段時間內容不會變化的頁面來說,浪費了資源。

    • 沒有監控系統,ssr?服務是常駐內存的,我們需要盡可能實時得知道它當前的健康狀況,力求在出現問題之前,得到通知,并快速做出調整。

    • 沒有弱網支持,對于弱網用戶,我們需要給出功能完備,但更加輕盈的頁面,以便讓弱網環境下的用戶也能正常使用服務。

    因此,將此工程應用到產品項目中之前,還需要對?ssr?工程再做一些改進,未來,我們會逐步為?ssr?服務提供以下配套設施:

    • 降級

    • 日志

    • 緩存

    • 監控

    • 弱網

    下一篇文章,我們講解如何研發一個基于?@vue/cli v3?的插件,并將?ssr?工程項目中服務器端的功能整合進插件中。


    水滴前端團隊招募伙伴,歡迎投遞簡歷到郵箱:fed@shuidihuzhu.com

    總結

    以上是生活随笔為你收集整理的linux 使用ssr客户端_【第一期】基于 @vue/cli3 与 koa 创建 ssr 工程的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。