建立相似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 = ""}