webpacker 已经普及在各个 rails 项目里很久了,在 rails 项目里使用 Vue 组件来强化用户体验,甚至整个项目都用一个完整的VueApp
来做都是很正常的。关于他的老对头 React 有一个 Gem,ReactRails,我翻了一下有没有关于 Vue 的类似 Gem,结果都不是很完善,于是自己动手写了一个,也借鉴了里面很多思路和代码,希望大家支持,如果在使用过程中碰到什么 bug,可以到项目地址提交问题。
https://github.com/jiyarong/vue_rails
Demo 地址 右上角登录 admin(密码也是 admin) 可以体验完整的 CURD
这是一台带宽只有 1m 的机器,正常情况下,一个 bundle 要下载好几秒,如果不用服务端渲染,情况就很糟糕,但是现在你应该几乎看不到白屏时间,即便 js 还在下载
之前比较普遍的做法是,在 html 里放一个 div 作为根组件,在 js 里监控 dom 事件,有 Turbolinks 的可能还要额外处理一些事件,总之就是在某个事件触发后,初始化一个Vue
组件,这种做法初始化一个 Vue 组件一般的步骤是:
写一个组件 -> 在某个页面放一个 Div -> 在 application.js 引入这个组件 -> 监听 dom 事件,如果找到这个 div -> 初始化组件。
这样做先不说步骤比较多,最大的问题在于,在一个页面有多个 Vue 组件后,你很难在一堆 html 里一眼看出这是不是一个 Vue 组件,装了这个 Gem 后,步骤变为
写一个组件 -> 直接在 html 里引用
<%= vue_component('hello') %>
这样看起来是不是好多了!并且也不怕在一个页面内初始化多个 Vue 组件了,你不必考虑 Turbolinks 的兼容问题,我已经全部做掉了
打包后的 js 往往会很大,用户第一次访问你的网站的时候,如果机器的带宽不够,会出现白屏时间过长的问题,这对于用户的体验来说不是很友好,然而如果能让用户先看到一个画面,再继续下载 js,对于用户体验来说会好很多,同时解决了SEO
的问题,但是配置服务端渲染往往非常繁琐,尤其我们的服务端还不是 nodejs,在这方面就更困难了,我在这个 GEM 里把这些事都做了,你只需要一个参数,就能打开服务端渲染,详情可以看下面的文档,原理是基于execjs
+ vue-server-renderer
+ webpacker
,同是我兼容了vue-router
和vuex
项目地址 https://github.com/jiyarong/vue_rails
gem 'webpacker'
gem 'vue_rails'
$ 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_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_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 下面,不然会卡主主线程从而使服务端渲染无效
$ 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'
$ 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>
...