vue-ssr

Umajs-vue-ssr是由@umajs/plugin-vue-ssr在新窗口中打开 搭配Srejs在新窗口中打开构建的轻量级,使用简单,灵活的vue服务端渲染解决方案;可以在controllermiddleware中灵活使用,通过模板引擎式的语法对vue页面组件进行服务端渲染。

1、插件介绍

plugin-vue-ssr插件扩展了Umajs中提供的统一返回处理Result对象,新增了Result.vue页面组件渲染方法,可在controller自由调用,使用类似传统模板引擎;也同时将方法挂载到了 koa 中间件中的ctx对象上;当一些公关的页面组件,比如 404、异常提示页面、登录或者需要在中间件中拦截跳转时可以在middleware中调用。

2、特性

  • 不默认路由,不需区分前端路由和后端路由概念,且支持页面级组件 AB 测;灵活
  • 页面组件中没有__isBrowser__之类变量对ssrcsr模式进行特殊区分处理;统一
  • 自定义HTML采用htmlWebpackPlugin,没有runtime,页面响应速度更高;高性能
  • 支持html中使用nunjucks类模板引擎语法实现SEO易上手
  • 页面开发不依赖框架包装的任何模块,保持原生的vue开发体验;友好,易升级
  • 数据获取提供服务端注入方式,页面视图渲染和数据加工分开处理;逻辑更清晰
  • 支持SSRCSR动态调整,支持SSR缓存,降级。高可用
  • 支持其他koa开发框架使用。可扩展
  • 支持 MPA,各页面组件可单独构建,可页面级更新

3、脚手架快速初始化项目[推荐]

在 cli 中支持快速创建umajs-vue-ssr模板工程。

$ npm i @umajs/cli -g  // 安装cli工具
$ uma project umajs-vue-demo  //通过uma初始化工程,选择vue2.0模板工程
✔ loading template
? please choise a template to create project (Use arrow keys)
  all,全套模板,包含部分代码示例
  mini,极简模板,仅有核心代码
  standard,标准模板,不包含示例代码
  react,umajs-react-ssr模板,包含示例代码
❯ vue2.0,umajs-vue-ssr模板,包含示例代码

1
2
3
4
5
6
7
8
9
10

依赖安装和启动

cd umajs-vue-demo
yarn install
yarn start

1
2
3
4

4、手动安装集成插件

插件安装

 yarn add @umajs/plugin-vue-ssr --save
1

打开 package.json 文件并添加 scripts 配置段:

"scripts": {
    "start":'node app.js',
    "build":"npx ssr build",
    "analyzer": "npx ssr analyzer",
},
1
2
3
4
5
  • start 启动你的 node 项目
  • build 运行npx ssr build构建用于生产环境的应用程序,Srejs 为多项目工程目录结构,可通过指定页面标识单独构建或者启动特定页面,命令为:npm run build xxx
  • analyzer 运行npx ssr analyzer 用于分析页面组件打包依赖分析 可通过 npm run analyzer xxx xxx 为页面组件标识,可指定分析某个页面组件打包结果

插件配置

// plugin.config.ts
export default <{ [key: string]: TPluginConfig }>{
  vue: {
    enable: true,
    options: {
      rootDir: 'web', // 客户端页面组件根文件夹
      rootNode: 'app', // 客户端页面挂载根元素ID
      ssr: true, // 全局开启服务端渲染
      cache: false, // 全局使用服务端渲染缓存 开发环境设置true无效
      prefixCDN: '/', // 客户端代码部署CDN前缀
    },
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
13

目录结构

框架默认配置属性rootDir默认为根目录下web,pages 下是页面组件入口,比如list页面,vue 主入口文件为list/index.js,页面组件为list/App.vue

└── web
    └── pages
        └── list
            ├── App.vue
            ├── index.js
1
2
3
4
5

页面组件(App.vue)

<template>
  <!--vue2.0版本APP.vue必须要设置根元素-->
  <div id="app">
    <h1>{{ msg }}</h1>
  </div>
</template>

<script>
export default {
  name: 'app',
  data() {
    return {
      msg: 'hi vue ssr!',
    }
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

页面主入口文件(index.js)

import App from './App.vue'
export default {
  App, // 必须导出App
  Router, //如使用vue-router 导出路由配置对象
  Store, //如使用vuex 导出store对象
}
1
2
3
4
5
6

Pages下按照文件夹名称定义 vue 页面组件,每一个页面组件必须包含inde.js主入口文件,文件必须导出组件App。如果使用vue-router,则将路由配置导出为Router对象;当使用vuex时,则将初始化配置导出为Store

5、插件使用

import { BaseController, Path } from '@umajs/core'
import { Result } from '../plugins/vue-ssr'

export default class Index extends BaseController {
  @Path('/')
  index() {
    return Result.vue('index', { title: 'umajs-vue-ssr' })
  }
}
1
2
3
4
5
6
7
8
9

6、插件 API

插件扩展了Umajs中提供的统一返回处理Result方法,新增了vue页面组件可在controller自由调用,方式类似传统模板引擎使用方法;也同时将方法挂载到了 koa 中间件中的ctx对象上;当一些公关的页面组件,比如 404、异常提示页面、登录或者需要在中间件中拦截跳转时可以在middleware中调用。

interface TviewOptions{
    ssr?: boolean, // 全局开启服务端渲染
    cache?: boolean, // 全局使用服务端渲染缓存
}
Result.vue(viewName:string,initProps?:object,options?:TviewOptions);
ctx.vue(viewName:string,initProps?:object,options?:TviewOptions);
1
2
3
4
5
6

如果 options 参数传递为空 则默认会使用全局配置属性,全局配置采用插件集成时传递的 options 参数

注意 cache只在生产环境开启有效。

7、数据获取

Pages页面中,vue 页面组件获取数据有两种形式;我们分为服务端直出数据(Props 和 State)和 vue 组件静态方法asyncData获取两种形式。我们可以通过这两种方式对服务端渲染时首次页面渲染进行数据填充,使得服务端渲染时能返回完整的 DOM 结构,提高用户体验和更利于SEO.

服务端直出 Props

框架在服务端提供了页面组件的渲染函数Result.vue,在调用函数渲染时我们可以在 initProps 参数中传递初始化的数据对象;这些数据可以在创建 vue 实例时会注册为组件实例的 Props 参数。在页面组件中我们可以将其定义为 Props。 了解 vue 组件 Props在新窗口中打开

Result.vue('list', {
  title: 'xxx',
  keywords: 'xxx',
  description: 'xxxx',
  say: 'hi!',
})
1
2
3
4
5
6

在页面组件中我们可以直接通过 Props 将服务端直出的数据在 vue 模板中使用。

<template>
  <!--vue2.0版本APP.vue必须要设置根元素-->
  <div id="app">
    <h1>{{ title }}</h1>
    <p>{{ say }}</p>
  </div>
</template>

<script>
export default {
  name: 'app',
  props: ['say', 'title'],
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

**特别说明:**在initProps参数中,title,keywords,description还会默认被解析为 web 网页头中的 标题,关键字,描述填充。

服务端初直出 State

在 initProps 对象中,框架会默认将 State 属性初始化为 store 实例。从根组件“注入”到每一个子组件中。

Result.vue('list', {
  title: 'xxx',
  keywords: 'xxx',
  description: 'xxxx',
  state: {
    say: 'hi!',
  },
})
1
2
3
4
5
6
7
8

我们可以直接在 Vue 任意组件中直接获得 Vuex 状态。获取方式为this.$store.state

<template>
  <!--vue2.0版本APP.vue必须要设置根元素-->
  <div id="app">
    <h1>{{ title }}</h1>
    <p>{{ say }}</p>
  </div>
</template>

<script>
export default {
  name: 'app',
  props: ['title'],
  data: () => {
    return {
      say: this.$store.state.say,
    }
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

由于 Vuex 的状态存储是响应式的,从 store 实例中可以在计算属性中返回某个状态。

<script>
export default {
  name: 'app',
  props: ['title'],
  computed: {
    say() {
      return this.$store.state.say
    },
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11

更多获取 Vuex 状态的使用方法可查看官方文档Vuex在新窗口中打开

asyncData 静态方法

asyncData是页面组件数据获取的钩子,只能作用于页面。其接收对象参数默认是vuexstore和当前router,通过router可以获取到当前路由的参数等数据,然后调用异步请求获取 http 类型的数据,然后通过store触发状态管理的更新,也可直接改写操作store.state属性。框架会合并到store数据上下文state中。

<script>
export default {
  name: 'app',
  data() {
    return {
      msg: this.$store.state.msg,
    }
  },
  async asyncData({ store, route }) {
    // store.state.msg = 'about来自asyncData的数据'
    // 触发 action 后,会返回 Promise
    return store.dispatch('fetchItem', route.params.id)
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

asyncData灵感来自官方vue-ssr 示例在新窗口中打开nuxtjs在新窗口中打开

8、SEO 和 HTML

initProps中通过,默认接收title,keywords,description作为页面标题,关键字,网页描述填充字段。

Result.vue('list', { title: 'xxx', keywords: 'xxx', description: 'xxxx' })
1

自定义 html

框架内置HTMLWebpackPlugin插件,开发者在页面组件同级目录下可以覆盖默认 html 模板自定义引入第三方资源和脚本。自定义 html 文件名为页面下的index.html

<!-- web/pages/index/index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,user-scalable=0,width=device-width"
    />
    <meta name="format-detection" content="telephone=no" />
    <meta name="format-detection" content="email=no" />
    <meta name="format-detection" content="address=no;" />
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <meta name="apple-mobile-web-app-status-bar-style" content="default" />
    <title>vue ssr</title>
    <!-- 引入第三方组件库样式 -->
  </head>
  <body>
    <!--vue-ssr-outlet-->
  </body>
  <!-- 引入第三方sdk脚本 -->
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

模板插值

在 html 模板中,还可以使用模板插值。服务端渲染初始化时传递的initProps数据在服务端渲染时会被注入为渲染上下文对象

<html>
  <head>
    <!-- 使用双花括号(double-mustache)进行 HTML 转义插值(HTML-escaped interpolation) -->
    <title>{{ title }}</title>

    <!-- 使用三花括号(triple-mustache)进行 HTML 不转义插值(non-HTML-escaped interpolation) -->
    {{{ meta }}}
  </head>
  <body>
    <!--vue-ssr-outlet-->
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12

支持全局 HTML

为满足业务引入第三方脚本也提供了以下方式自定义 html 模板。

  • web/pages/xxx/index.html(局部页面生效)
  • web/index.html(全局生效)

优先级web/pages/xxx/index.html > web/index.html

9、vuex

当项目比较复杂时,我们可以在页面组件入口文件中导出 Vuex store 对象。框架会自动从根页面组件注入 vue 子组件中,通过$store 访问到实例化后的 store。

import App from './App.vue'
export default {
  App,
  Store: {
    state: {
      count: 100,
    },
    mutations: {
      increment: (state) => state.count++,
      decrement: (state) => state.count--,
    },
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
13

框架接收到 Store 对象后,会实例化 vuex。并且初始化到 vue 实例中,通过 vue.use 注入到全局。对于初始化的 state 数据,如果在 initProps 中也传入同名的属性,则 initProps.state 将会覆盖主入口文件传入 store.state 中的属性值。

服务端初始化 state

Result.vue(ctx, 'vuex', { state: { count: 200 } })
1

页面组件获取状态

<template>
  <div id="app">
    <p>{{ count }}</p>
    <p>
      <button @click="increment">+</button>
      <button @click="decrement">-</button>
    </p>
  </div>
</template>

<script>
export default {
  name: 'vuex',
  computed: {
    count() {
      return this.$store.state.count
    },
  },
  methods: {
    increment() {
      this.$store.commit('increment')
    },
    decrement() {
      this.$store.commit('decrement')
    },
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

最后展示到页面上的count初始值为:200

10、vue-router

当 vue 页面组件需要和 vue-router 结合开发单页面应用时,在入口文件中我们也可以通过 Router 属性导出路由配置。

  • 客户端页面组件入口文件路由配置
import App from './App.vue'
import About from './about.vue'
import Home from './home.vue'

export default {
  App,
  Router: {
    mode: 'history',
    fallback: false,
    base: '/index/',
    scrollBehavior: () => ({ y: 0 }),
    routes: [
      { path: '/about', props: true, component: About },
      { path: '/home', props: true, component: Home },
      { path: '/', redirect: '/home' },
    ],
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

注意:服务端渲染模式下不支持使用 import 动态导入组件方式进行路由懒加载

  • 服务端路由

对于服务端我们需要保持客户端和服务端路由一致,否则会出现子路由页面刷新404现象。

  • 页面路由获取数据

在页面组件中页面数据获取方式和普通页面组件保持一致,我们可以使用服务端渲染 props 直出和 asyncData 钩子函数两种方式获取,具体使用参考数据获取章节。

11、内置 css 支持

框架内置了vue-style-loader以及 css 预处理器 loader,支持*.vue单个文件组件内的 <style>提取为单独 css 样式文件。也支持 CSS Modules。以下示例均开箱即用,无需额外配置。

Scoped CSS

<template>
  <p class="scoped">style scoped</p>
</template>

<style scoped>
.scoped {
  color: red;
  font-size: 18px;
}
</style>
1
2
3
4
5
6
7
8
9
10

预处理器 less/scss

<template>
  <p class="scoped">style scoped</p>
</template>

<style lang="less" scoped>
.scoped {
  color: red;
  font-size: 18px;
}
</style>

<style lang="scss" scoped>
.scoped_scss {
  color: red;
  font-size: 18px;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

当我们采用.less 或者.scss 文件编写样式时,也可以从 JavaScript 中导入 CSS,例如,import './foo.css'**

12、CSS Modules

  • 使用 style module

在你的 <style>上添加 module 特性,这个 module 特性指引 Vue Loader 作为名为 $style 的计算属性,向组件注入 CSS Modules 局部对象。然后你就可以在模板中通过一个动态类绑定来使用它了。

<template>
  <p :class="$style.red">This should be red</p>
</template>

<style module>
.red {
  color: red;
}
.bold {
  font-weight: bold;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12

详细原理查看官方文档vue-loader在新窗口中打开

  • 使用预处理器样

如果你的样式是从 JavaScript 中导入的,那么你只需要将把文件命名为*.module.(less|scss|css)

<template>
  <p :class="styles.module">
    对于外部样式文件,框架默认支持以文件命名: xxx.module.(less|scss|css)
    开启使用css module
  </p>
</template>

<script>
import styles from './index.module.less'
export default {
  data() {
    styles
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

13、案例

14、源码

如果你觉得有用,请 star 支持下我们~谢谢!