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

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

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>
...
共收到 1 条回复
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册