建立相似https://disqus.com/ 的插件javascript
交互插件:html
https://gorails.com/系列视频:Embeddable JS widgets.前端
1.下载模版,按照vue.jsvue
rails new embeded_comment -m template.rb rails webpacker:install:vue
2. 建立数据库表格Discussion和Comment.java
rails g scaffold Discussion url title comments_count:integer
rails g scaffold Comment discussion:references name email body:text ip_address user_agent
rails db:migrate
解释:webpack
url属性,存储当前的讨论版的网址。ios
而后修改hello_vue为embedgit
mv app/javascript/packs/{hello_vue,embed}.js
添加代码:es6
let url = window.location.href #encodeURIComponent()用于对输入的URl部分进行转义 fetch(`http://localhost:3000/api/v1/discussions/${encodeURIComponent(url)}`, { headers: { accept: 'application/json' } }) .then(response => response.json()) .then(data => console.log(data))
3. 增长路径routes.rb,而后建立一个controller.github
mkdir -p app/controllers/api/v1 touch app/controllers/api/v1/disscussions_controller.rb
改成:
namespace :api do
namespace :v1 do
resources :discussions
end
end
resources :discussions do
resources :comments
end
增长一个controller的show方法:
任何如http://localhost/?a=11之类的网址,会启用emben.js中的代码,而后执行show action行为,并转到对应的网页
class Api::V1::DiscussionsController < ApplicationController def show @discussion = Discussion.by_url(params[:id]) render "discussions/show" end end
在model,增长一个类方法by_url
#model, 增长by_url类方法。一个sanitize URL的方法,只要"/?"或者“/#”前面的URL部分
#http://localhost/?a=11
#http://localhost:3000/disscussions/#a=shanghai
class Discussion < ApplicationRecord has_many :comments def self.by_url(url) uri = url.split("?").first uri = url.split("#").first uri.sub!(/\/$/, '') # 若是comments中存在这个uri则选择它,不存在则建立它。 where(url: uri).first_or_create end end
改动:app/views/discussions/index.html.erb
在最后一行添加:
javascript_pack_tag "embed"
遇到一个问题:
NoMethodError in Devise::SessionsController#create
undefined method `current_sign_in_at' for #<User:0x00007fcad84de6f8>
## Trackable # t.integer :sign_in_count, default: 0, null: false # t.datetime :current_sign_in_at # t.datetime :last_sign_in_at # t.string :current_sign_in_ip # t.string :last_sign_in_ip
2个方法解决:
视频2
1. 安装Vuex
Vuex是a state management pattern + library。用于Vue.js app。
yarn add vuex
2. 前端vue.js
事件监听:
# embed.js const event = (typeof Turbolinks == "object" && Turbolinks.supported) ? "turbolinks:load" : "DOMContentLoaded" document.addEventListener(event, () => { const el = document.querySelector("#comment") const app = new Vue({ el, render: h => h(App) }) console.log(app) })
修改app.js
<template>
<div id="comments">
<p>{{ message }}</p>
</div>
</template>
把上一视频的代码移动到store.js中
embed.js载入它。
import store from '../store' // 使用Vuex关联store.调用store的action中的方法 store.dispatch("loadComments")
解释:Action经过store.dispatch来触发。
1. 比较vue, Vuex实例中的特性:
2.Vuex的motion。
3. 执行fetchTodos方法的过程图
新建store.js文件:
import Vue from 'vue' import Vuex from "vuex" Vue.use(Vuex) const store = new Vuex.Store({ state: { comments: [] }, mutations: { load(state, comments) { state.comments = comments } }, action: { // 使用了参数解构。用commit来代替context.commit。 // context实际上是一个store实例。 //async异步函数的普通写法:解释见👇👇👇 // 在embed.js,进口babel-polyfill async loadComments({ commit }) { let url = window.location.href // encodeURIComponent()用于对输入的URl部分进行转义 fetch(`http://localhost:3000/api/v1/discussions/${encodeURIComponent(url)}`, { headers: { accept: 'application/json' } }) .then(response => response.json()) .then(data => commit('load', data.comments)) #见_comment.json.jbuilder. } } }) window.store = store export default store
解释:
.then(data => commit('load', data.comments)) //等同 .then(function(data) { console.log("1", data) return commit('load', data.comments) }) ractr @discussion对象会找它的comments. 格式是_comment.json.jbuilder中的: json.extract! comment, :id, :discussion_id, :name, :email, :body, :ip_address, :user_agent, :created_at, :updated_at
解释:
Action主要用于异步操做,它提交的是mutation,不是直接变动state。
例子,异步:
actions: { incrementAsync ({ commit }) { setTimeout(() => { commit('increment') }, 1000) } }
实践中,咱们会常常用到 ES2015 的 参数解构 来简化代码(特别是咱们须要调用 commit
不少次的时候):
actions: {
increment ( context ) {
context.commit('increment')
}
#改成 increment ({ commit }) { commit('increment') } }
注意:
store.dispatch能够处理被触发的action的处理函数返回的Promises, 而且store.dispatch仍旧返回Promise;
actions: { actionA ({ commit }) { return new Promise((resolve, reject) => { setTimeout(() => { commit('someMutation') resolve() }, 1000) }) } }
如今你能够:
store.dispatch('actionA').then(() => { // ... })
在另一个action中也也可:
actions: { //... actionB ({ dispatch, commit}) { return dispatch('actionA').then(() => { commit('someOtherMutation') }) } }
最后,若是使用async/await,能够这么组合action:
actions: { async actionA ({ commit }) { commit('gotData', await getData()) }, async actionB({ commit }) { await dispatch('actionA') //等actionA完成 commit('gotOtherData', await getOtherData()) } }
解释:
async function声明定义了一个异步函数,它返回一个AsyncFunction对象。
一个asynchronous function是一个函数经过事件循环同步地执行,并使用一个暗含的Promise对象来返回它的结果。
不过它的语法和代码结构看起来就像使用标准的同步函数同样。(方便的写法)
例子:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
附加:
若是要使用async function必须引进babel-polyfill
import "babel-polyfill"
不然:
[vuex] unknown action type: loadComments
在js console上:
store.state.comments.length 仍是0.
解决:
在store.js中:
const store = new Vuex.Store({ //... action: { //❌,应该是actions
ActionView::Template::Error (undefined method `comment_url' for #<#<Class:0x00007fc465b2e680>:0x00007fc4644382c8> Did you mean? font_url): 1: json.extract! comment, :id, :discussion_id, :name, :email, :body, :ip_address, :user_agent, :created_at, :updated_at 2: json.url comment_url(comment, format: :json)
解决:
注释掉_comment.jbuilder.json中的 json.url comment_url(comment, format: :json)
增长store特性。
document.addEventListener(event, () => { const el = document.querySelector("#comment") store.dispatch('loadComments') const app =new Vue({ el, store, render: h => h(App) }) })
修改comments的模版。
<template> <div id="comments"> <h3><span v-if="count > 0 ">{{ count }}</span>Comments</h3> <div v-for="comment in comments" class="mb-1"> <div><span class="font-weight-bold">{{ comment.name }}</span> comment:</div> <div>{{ comment.body }}</div> </div> </div> </template> <script> export default { data: function () { return {} }, computed: { comments() { return this.$store.state.comments }, count() { return this.$store.state.comments.length } } } </script>
最后在application.html.erb中加上模版:
<div class="container"> <%= yield %> + <div id="comments"></div> </div>
render: h => h(App)是什么意思?
它是渲染函数。
vue2.0的写法,替代了vue1.0的components: {App} 。比template更接近编译器。
#等同于 render : function(h){ return h(App) } #等同于 render : function(createElement){ return createElement(App) }
具体见文档:渲染函数(文档说明)
1. ES6的写法,表示Vue实例选项对象的render方法做为一个函数,接受传入的参数h函数,返回h(app)的函数的调用结果。
2.Vue在建立Vue实例时,经过调用render方法来渲染实例的DOM树
3.Vue在调用render方法是,会传入一个createElement函数做为参数,而后createElement以App为参数进行调用。
createElement()会返回一个虚拟节点virtural Node。它所包含的信息会告诉 Vue 页面上须要渲染什么样的节点,及其子节点。
createElement()参数:
// @returns {VNode} createElement( // {String | Object | Function} // 一个 HTML 标签字符串,组件选项对象,或者 // 解析上述任何一种的一个 async 异步函数。必需参数。 'div', // {Object} // 一个包含模板相关属性的数据对象 // 你能够在 template 中使用这些特性。可选参数。 { //具体见教程渲染函数:https://cn.vuejs.org/v2/guide/render-function.html }, // {String | Array} // 子虚拟节点 (VNodes),由 `createElement()` 构建而成, // 也可使用字符串来生成“文本虚拟节点”。可选参数。 [ '先写一些文字', createElement('h1', '一则头条'), createElement(MyComponent, { props: { someProp: 'foobar' } }) ] )
嵌套的JS小部件经常包括forms。咱们使用Vuex来创建评论表格部件。而且咱们将使用vue-map-fields简化这个过程。
修改app.vue模版,创建提交form,能够提交评论,并显示最新的评论!
<template> <div id="comments"> //... // references给form存取的权利。即经过ref特性给这个子组件一个id. // 而后就可使用this.$ref.form来访问这个form了。 <form @submit.prevent="submit" ref="form"> </form>
form内部:
//v-on的修饰符.prevent用于调用event.preventDefault() <form @submit.prevent="submit" ref="form"> <div class="form-group"> <input type="text" name="comment[name]" required placeholder="Full name" class="form-control" /> </div> <div class="form-group"> <input type="text" name="comment[email]" required placeholder="Email address" class="form-control" /> </div> <div class="form-group"> <textarea name="comment[body]" required placeholder="Add a comment" class="form-control full-width"></textarea> </div> <div class="form-group text-right"> <button class="btn btn-primary">Post comment</button> </div> </form>
所以,添加submit方法:
//this.$refs.form是一个对象,持有注册过ref特性的全部DOM特性和组件实例 //使用this.$store.dispatch来执行createComment action。并传参数formData给这个action <script> //... methods: { submit() { //console.log(typeof this.$refs.form) 获得object。 // new FormData(form)生成一个formData对象,这里form参数是一个form元素对象。
let formData = new FormData(this.$refs.form) this.$store.dispatch("createComment", formData) } }
async createComment({ commit }, formData) { let url = window.location.href fetch(`.../comments`, { headers: { accept: 'application/json'}, method: 'post', body: formData, }) .then(response => response.json()) .then(comment => commit('addComment', comment)) }
增长对应的addComment mutation
mutations: { //... //push方法把新增的comment附加在comments数组最后。 addComment(state, comment) { state.comments.push(comment) } }
namespace :api do namespace :v1 do resources :discussions do resources :comments end end end
添加对应的controller
class Api::V1::CommentsController < ApplicationController #忽略验证token: sikp_before_action :verify_authenticity_token # 获得@discussion before_action :set_discussion def create @comment = @discussion.comments.new(comment_params) #给@comment对象的2个属性赋值 @comment.user_agent = request.user_agent @comment.ip_address = request.remote_ip if @comment.save render "comments/show"
else
render json: { errors: @comment.errors.full_messsages } end end private
def comment_params
params.require(:comment).permit(:name, :email, :body)
end
def set_discussion
@discussion = Disscussion.by_url(params[:id]) end end
remote_ip方法是ActionDispatch::Request中的方法。返回client的IP地址。
<input v-model="name">
等同于
<input v-bind="name" v-on:input="$emit('input', $event.target.value)"
由于咱们使用Vuex关联state,因此这里无需在app.vue中的data函数上添加对应的name。改为store.js中的state上添加:
const store= new Vuex.Store({ state: { comments: [], name: '', email: '', body: ''
errors: [], #错误的记录
}
import { getField, updateField } from "vue-map-fields"
getters: {
getField,
}
mutations: {
updateField,
在app.vue:
<script> import { mapFields } from 'vue-map-fields' export default { computed: { ...mapFields([ 'name', 'email', 'body', 'errors' ]), } // 给form的input和textarea添加v-model
主要是由于v-model在严格模式下,会有可能抛出❌。
<input :value='name' @input="updateName"> // ... methods: { updateName (e) { this.$store.commit('updateName', e.target.value) } } //在store.js中添加mutations mutations: { updateName (state, value) { state.name = value } }
<template> <input v-model="name"> <input v-model="email"> <input v-model="body"> </template> <script> export default { computed: { name: { get() { return this.$store.state.name; }, set(value) { this.$store.commit('updateName', value) } }, //还有errors.... } };
在store.js添加对应的mutations:
updateName(state, value) { state.name = value }, ...
//createComment方法修改:
//若是产生任何错误,则调用setErrors并变动state.errors的值。 .then(comment => { if (comment.errors) { commit('setErrors', comment.errors) } else { commit('setErrors', []) commit('addComment', comment) } }) //mutations中添加: setErrors(state, errors) { state.errors = errors }
在comment.rb中添加
validates :name, :email, :body, presence: true
而后移除input ,textarea中的required参数选项,这样能够进行服务器端的验证了!
//添加 errors, 显示的格式根据代码本身调整
{{ errors }}
另外,添加comments成功后,须要清除原来的内容,添加clearComment方法
#修改createComment方法 + commit('clearComment')#在mutations中:clearComment(state) { state.name = '' state.email = "" state.body = ""}