Gem 自己写的 Gem, VueRails,让 Vue 在 Rails 里服务端渲染!并支持 vue-router 和 vuex

ad583255925 · 2019年03月01日 · 最后由 cicholgricenchos 回复于 2019年05月27日 · 4294 次阅读

webpacker 已经普及在各个 rails 项目里很久了,在 rails 项目里使用 Vue 组件来强化用户体验,甚至整个项目都用一个完整的VueApp来做都是很正常的。关于他的老对头 React 有一个 Gem,ReactRails,我翻了一下有没有关于 Vue 的类似 Gem,结果都不是很完善,于是自己动手写了一个,也借鉴了里面很多思路和代码,希望大家支持,如果在使用过程中碰到什么 bug,可以到项目地址提交问题。

https://github.com/jiyarong/vue_rails

废话不多说,先上 Demo,体验一下服务端渲染优化后的首屏加载!

Demo 地址 右上角登录 admin(密码也是 admin) 可以体验完整的 CURD

这是一台带宽只有 1m 的机器,正常情况下,一个 bundle 要下载好几秒,如果不用服务端渲染,情况就很糟糕,但是现在你应该几乎看不到白屏时间,即便 js 还在下载

解决了哪些问题

1. 合理初始化 Vue 组件

之前比较普遍的做法是,在 html 里放一个 div 作为根组件,在 js 里监控 dom 事件,有 Turbolinks 的可能还要额外处理一些事件,总之就是在某个事件触发后,初始化一个Vue组件,这种做法初始化一个 Vue 组件一般的步骤是:

写一个组件 -> 在某个页面放一个 Div -> 在 application.js 引入这个组件 -> 监听 dom 事件,如果找到这个 div -> 初始化组件

这样做先不说步骤比较多,最大的问题在于,在一个页面有多个 Vue 组件后,你很难在一堆 html 里一眼看出这是不是一个 Vue 组件,装了这个 Gem 后,步骤变为

写一个组件 -> 直接在 html 里引用

<%= vue_component('hello') %>

这样看起来是不是好多了!并且也不怕在一个页面内初始化多个 Vue 组件了, 你不必考虑 Turbolinks 的兼容问题,我已经全部做掉了

2. 服务端渲染

打包后的 js 往往会很大,用户第一次访问你的网站的时候,如果机器的带宽不够,会出现白屏时间过长的问题,这对于用户的体验来说不是很友好,然而如果能让用户先看到一个画面,再继续下载 js,对于用户体验来说会好很多,同时解决了SEO的问题,但是配置服务端渲染往往非常繁琐,尤其我们的服务端还不是 nodejs,在这方面就更困难了,我在这个 GEM 里把这些事都做了,你只需要一个参数,就能打开服务端渲染,详情可以看下面的文档,原理是基于execjs + vue-server-renderer + webpacker,同是我兼容了vue-routervuex

接下来是文档部分,文档还在持续完善中,希望大家支持

项目地址 https://github.com/jiyarong/vue_rails

VueRails

基于 Webpacker

将 webpacker 和 vue_rails 加入 gemfile

gem 'webpacker'
gem 'vue_rails'

跑一些 installer

$ bundle install
$ rails webpacker:install
$ rails webpacker:install:vue
$ rails generate vue:install

确认文件夹结构

|-- app
   |-- javascript
      |-- packs
         |-- application.js
         |-- vue_server_render.js
      |-- vue_components
         |-- hello.vue
      |-- rails_vue_ujs.js

所有的 Vue 组件现在都应该放在 vue_components里面

基本用法

1. 添加 javascript_pack_tag 到 application.html

<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>

2. 建一个控制器和页面

example:

$ rails generate controller home index

别忘了路由设置

3. 打开 home/index.html,加入如下代码

<%= vue_component("hello") %>

一个页面里数个组件也是没问题,即便他们是相同的组件

<%= vue_component("hello") %>
<%= vue_component("hello") %>
<%= vue_component("hello") %>

4. 打开浏览器,现在应该已经成功了!

Vue 文件路径和组件名之间的关系

/vue_components/hello.vue -> vue_component('hello')

/vue_components/post/index.vue -> vue_component('post/index')

/vue_components/post/edit/index.vue -> vue_component('post/edit/index')

高级用法

给组件传参

<%= vue_component("hello", {foo: 'bar'}) %>

in your component hello.vue:

<template>
  <div id="app">
    <p>{{ outside.foo }}</p>
  </div>
</template>

<script>
  export default {
    props: ['outside'],
    data: function () {
      return {
        message: "Hello Vue!"
      }
    }
  }
</script>

数组或者 hash 都没问题

<%= vue_component("hello", {foo: {name: 'Peter'}}) %>
<template>
  <div id="app">
    <p>{{ outside.foo.name }}</p>
  </div>
</template>

<script>
  export default {
    props: ['outside'],
    data: function () {
      return {
        message: "Hello Vue!"
      }
    }
  }
</script>
<%= vue_component("hello", {foo: [1,2,3]}) %>
<template>
  <div id="app">
    <template v-for="i in outside.foo">
      <div>{{i}}</div>
    </template> 
    <p>{{ outside.foo.name }}</p>
  </div>
</template>

<script>
  export default {
    props: ['outside'],
    data: function () {
      return {
        message: "Hello Vue!"
      }
    }
  }
</script>

outside 里面自带 csrf_token,你做表单类的组件一定会用得到的 (outside.csrf_token)

服务端渲染

如果不是打算整个项目都用 VueApp 来做,不要使用服务端渲染!

使用参数 prerender, 将会自动打开服务端渲染

<%= vue_component("hello", {foo: [1,2,3]}, {prerender: true}) %>

除了 outside, 还有一个 props 用来区别环境是否是服务端渲染,env_ssr,true 为服务端,false 则为客户端

有一些组件会在服务端渲染时出错,使用该值来区分一下

<some-component v-if="!env_ssr" />

调整 js 标签的位置

<!DOCTYPE html>
<html>
  <head>
    <title>RailsVueSsr</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_pack_tag 'application' %>
  </head>

  <body>
    <%= yield %>
  </body>
  <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>

</html>

javascript_pack_tag 必须在 body 下面,不然会卡主主线程从而使服务端渲染无效

和 vue-router 一起使用

$ yarn add vue-router

在 application.js 和 vue_server_render.js 里添加如下代码

import VueRouter from 'vue-router';

RailsVueUJS.use(VueRouter);

在 vue 组件里

<template>
    <div class="container">
      <div class="content">
        <router-view :outside="outside" :env_ssr="env_ssr"></router-view>
      </div>
    </div>
</template>

<script>
  import VueRouter from 'vue-router';
  import PostList from './posts/index';
  import PostDetail from './posts/show';
  import newPost from './posts/new';
  import EditPost from './posts/edit';

  const routes = [
    {
      path: '/',
      component: PostList,
      name: 'post_index',
      props: true
    },
    {
      path: '/posts/new',
      component: newPost,
      name: 'new_post'
    },
    {
      path: '/posts/edit/:id',
      component: EditPost,
      name: 'edit_post'
    },
    {
      path: '/posts/:id',
      component: PostDetail,
      name: 'post_detail'
    }
  ];

  const router = new VueRouter({
    mode: 'history',
    routes
  });

  export default {
    props: ['outside', 'env_ssr'],
    router
  };
</script>

参数 prerender 还可以 接受一个路由传给 vue-router

<%= vue_component("hello", {foo: [1,2,3]}, {prerender: request.path}) %>

in your routes.rb

get '*path', to: 'home#index'

和 vuex 一起使用

$ yarn add vuex

初始化一个 store

import Vue from 'vue'
import Vuex from 'vuex';

Vue.use(Vuex);
const store = new Vuex.Store({
  state: {
    currentUser: {},
    hasLogin: false
  }
});

export default store;

在 application.js 和 vue_server_render.js 里添加如下代码

import VueRouter from 'vue-router';
import Vuex from 'vuex';
import store from "../vue_components/store";

RailsVueUJS.use(VueRouter, Vuex);
RailsVueUJS.initializeVuexStore(store);
...

in your html file

<%= vue_component("hello", {foo: [1,2,3]}, {prerender: true, state: {
  hasLogin: true
}}) %>

使用参数 state,这会覆盖你的初始 store, 服务端渲染和客户端渲染同样都会生效

组件内

...
<div v-if="$store.state.hasLogin">
  <a href="/users/logout">logout</a>
</div>
...
ad583255925 SimpleAPM - Rails 慢事务追踪 中提及了此贴 03月01日 17:17
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册