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 服务前,此测量范围内会经历:

  1. 客户端下载入口文件、js、css等资源

  2. 客户端跳转微信授权服务,获取授权 code

  3. 客户端跳回源地址,进行授权登录(客户可看到页面)

接入 ssr 服务后,此测量范围内会经历:

  1. 服务器跳转微信授权服务,获取授权 code

  2. 客户端下载入口文件、js、css等资源(客户可看到页面)

  3. 客户端授权登录

我们可以看到,接入 ssr 服务后,客户理论上能更早得看到页面了

607b008b2ac9470d75bc761ff526e145.png

根据上图可以看到,在接入 ssr 服务后,客户能更早得看到页面内容,客户感知到的性能提高了。

SSR 有什么风险?

  • 加重服务器负载

  • 通常用于 SSR 的服务都是基于 NodeJS 环境,需要额外的研发成本(例如:日志、监控、追踪)

  • SSR 的服务通常都由前端工程师研发和维护,增加了更多的心智负担

  • 基于同构代码的 SSR 应用是同一份代码,既在浏览器运行,也在服务器运行,代码层面的问题造成的影响更大

今天,我们使用新版的 cli 工具(v3.x),搭建一个基于 vue 同构代码的 ssr 工程项目。

我们的目标:使用 @vue/cli v3.x 与 koa v2.x 创建一个 ssr 工程

我们的步骤如下:

  1. 安装 @vue/cli

  2. 使用 @vue/cli 创建 spa 工程

  3. 将 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)。

我们的步骤如下:

  1. 改造状态存储 src/store.js

  2. 改造路由 src/router.js

  3. 改造应用入口 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 的构建过程。

借用官方文档的示意图:

2fcfbad9020706f48c8158c711804f94.png

我们看到:

源代码分别为客户端和服务器提供了独立的入口文件: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

3bc089fdea603ba295b4e23416436d77.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/527991.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

读保护_混合ASIL系统中不同安全等级模块间的边界保护

01功能安全组件的软件开发针对ISO 26262对功能安全软件研发的要求&#xff0c;AUTOSAR将功能安全需求进行了具体拆分。当前的AUTOSAR规范囊括了诸多功能安全组件软件开发的需求概念。在AUTOSAR系统设计的架构中&#xff0c; 从系统功能设计到ECU功能实现&#xff0c;每个功能特…

android studio javah不是内部或外部命令,Android Studio 环境搭建参考,jdk10javac命令提示不是内部或外部命令...

https://blog.csdn.net/qq_33658730/article/details/78547789 win10下Android Studio和SDK下载、安装和环境变量配置http://yanmin99.com/ android-0基础从入门到精通环境变量ANDROID_HOMEE:\Program Files (x86)\Android\SdkJAVA_HOMEE:\Program Files\Java\jdk1.8.0_161TOMC…

starops 云效运维 文档_阿里云 SAE 携手云效助力「石家庄掌讯」持续交付、降本提效...

背景石家庄掌讯信息技术有限公司创立于2009年&#xff0c;是一家提供企业信息化咨询、创新型软件产品、电商代运营服务&#xff0c;标准化管理、快速发展的高新技术企业。当前公司正处于企业互联网市场突破转型重要阶段&#xff0c;希望将更多精力转移到业务创新&#xff0c;提…

setdata改数组里的值 微信小程序_今日艰难笔记redux小程序随记

redux的前身Flux https://zhuanlan.zhihu.com/p/38050036Flux的一整套抽象(action&#xff0c;dispatcher&#xff0c;store)&#xff0c;在单向数据流的基础上可以提高应用的可维护性和代码的可预测性。然而&#xff0c;全局action多store的架构面对复杂的应用依然不能很好地解…

竞赛数据清洗缺失值_Kaggle 数据清洗挑战 Day 1 - 手把手教你五步处理缺失值

前些天报名参加了 Kaggle 的 Data Cleaning 5天挑战&#xff0c;5天的任务如下&#xff1a;Day 1: Handling missing valuesDay 2: Data scaling and normalizationDay 3: Cleaning and parsing datesDay 4: Fixing encoding errors (no more messed up text fields!)Day 5: Fi…

html5中行内样式写法,react怎么写行内样式?

react怎么写行内样式&#xff1f;下面本篇文章给大家介绍一下react 行内样式几种写法。有一定的参考价值&#xff0c;有需要的朋友可以参考一下&#xff0c;希望对大家有所帮助。react 行内样式几种写法法一这是评论列表组件1法二const title{color:"red",fontSize:3…

程序左上角的字_微信内测7.0.7新版本,小程序迎来大改动!

最近几个月来&#xff0c;微信就跟打了鸡血一样不断更新了多个版本上线多个新功能。此前7月末&#xff0c;微信安卓7.0.6带来了将收藏笔记&#xff0c;文件预览等页面设为浮窗的功能&#xff0c;目前最多支持5个文档或笔记设为浮窗。这不&#xff0c;距离微信7.0.6 for Android…

html 跑步比赛小游戏,疯狂趣味跑步竞赛3D

疯狂趣味跑步竞赛3D是一款跑酷类型的休闲游戏&#xff0c;游戏在整体画面上搭配了最新的技术&#xff0c;使得游戏画面看起来非常华丽&#xff0c;人物的建模也很可爱。玩家在游戏中可以在各种风格的跑道上进行比赛&#xff0c;还可以在线跟别的玩家进行匹配。喜欢这款游戏的快…

得到进程id_搞懂进程组、会话、控制终端关系,才能明白守护进程干嘛的?

守护进程概念&#xff1a;守护进程&#xff0c;也就是通常所说的Daemon进程&#xff0c;是Linux中的后台服务进程。周期性的执行某种任务或等待处理某些发生的事件。Linux系统有很多守护进程&#xff0c;大多数服务都是用守护进程实现的。比如&#xff1a;像我们的tftp&#xf…

w7计算机的收藏夹里弄出桌面,Win7电脑桌面的便签怎么弄出来?

原标题&#xff1a;Win7电脑桌面的便签怎么弄出来&#xff1f;可能很多人都不知道&#xff0c;作为常用的一款电脑桌面操作系统&#xff0c;Windows 7上除了有Txt记事本和Office办公文档外&#xff0c;还有便笺小工具呢&#xff01;这款便笺小工具虽然很不起眼&#xff0c;但是…

native html5 区别,H5与Native优劣对比

即将启动爱奇艺商城(mall.iqiyi.com)移动端的native化。项目开始之前当然要梳理下为什么做native&#xff0c;究竟H5跟native比有什么劣势&#xff0c;而H5又为什么占比那么高并且有“Html5才是未来”的说法呢。下面简单理下对比。H5&#xff0c;即Html5&#xff0c;指第5代HTM…

html制作圆盘时钟,jquery+html5制作超酷的圆盘时钟表

自己封装的一个用HTML5jQuery写的时钟表代码&#xff1a;超酷数码钟表//引用的是在线jquery地址&#xff0c;如果不行请自行下载切换(function($){$.fn.drawClock function(options){var mainId $(this);//设置默认参数var defaultOptions {width: 300px,height: 300px,marg…

调用外部程序处理文件_Python使用内置方法、模块调用外部命令

导读Python内置调用外部命令&#xff1a;os.systemos.popenos.popen2os.popen3os.popen4commands模块subprocess模块在Python3中&#xff0c;将os.popen2、os.popen3、os.popen4、commands等模块方法移除所以不用花精力了解&#xff0c;可以绕过了os.system方法os.system("…

计算机软件类ui工资多少,ui设计师工资一般多少

ui设计师月薪大概多少&#xff0c;UI设计师的收入现处于中等水平&#xff0c;一般月薪5000~7000元&#xff0c;资深设计师的收入可上升至7000~10000元。ui设计师月薪大概多少&#xff0c;ui设计师工资有多少&#xff1f;&#xff0c;不清楚ui设计师工资有多少的伙伴可以看看。U…

大学计算机课第二章内容总结,第四周市政系《大学计算机基础》课程总结

本周大一的新生终于在漫长的等待、报到、军训之后开始了大学课程的学习&#xff0c;在《大学计算机基础》课程的学习中&#xff0c;其实只是需要大学确立一种新的学习思想&#xff1a;计算机是人类智慧的结晶&#xff0c;它给予我们这样一种工具&#xff0c;通过对它的使用&…

在maven项目中打开jsp_零基础在intellij中打开一个项目复制粘贴内容即可运行的java拼图...

我刚学java语言&#xff0c;在学习java简单的拼图游戏时发现网上有些居然要导入调试&#xff0c;而且网上有些说的调试方法不明不白&#xff0c;所以我就分享了我的可直接复制在新项目中的源码&#xff0c;如果要直接要搞好的话就可以用结尾的分享提取链接进行百度网盘获取(该项…

计算机考试报名无法弹出支付界面,教资报名支付页面不弹出怎么办 2021教师资格证报名入口网址...

2021教师资格证报名入口是中小学教师资格考试网站&#xff0c;教师资格证报名时间是1月14-17日&#xff0c;请大家请及时登陆教师资格证报名官网报名以防错过考试。点击进入&#xff1a;2021年教师资格考试报名入口教师资格证报名缴费进不了支付页面的话原因可能有以下几点&…

gcn在图像上的应用_GCN总结 - nxf_rabbit75 - 博客园

一、GCN简介GNN模型主要研究图节点的表示(Graph Embedding)&#xff0c;图边结构预测任务和图的分类问题&#xff0c;后两个任务也是基于Graph Embedding展开的。目前论文重点研究网络的可扩展性、动态性、加深网络。谱卷积有理论支持&#xff0c;但有时候会受到拉普拉斯算子的…

mysql源码_MySql轻松入门系列——第一站 从源码角度轻松认识mysql整体框架图

一&#xff1a;背景1. 讲故事最近看各大技术社区&#xff0c;不管是知乎&#xff0c;掘金&#xff0c;博客园&#xff0c;csdn基本上看不到有小伙伴分享sqlserver类的文章&#xff0c;看来在国内大环境下是不怎么流行了&#xff0c;看样子我再写sqlserver是不可能再写了&#x…

测试图片色域软件,显示器色域检测

色域是颜色的一种编码&#xff0c;也是某一个规定的色彩空间或者输出装置呈现出来的一个颜色范围&#xff0c;展示给用户看到的颜色&#xff0c;使用显示器色域检测工具&#xff0c;可以检测到电脑显示器的色域情况&#xff0c;并且提供具体的参数&#xff0c;这款软件对于需要…