(GoRails)使用vue和Vuex管理嵌套的JavaScript評論, 使用組件vue-map-field

嵌套的JavaScript評論 Widget Models

建立相似https://disqus.com/ 的插件javascript

交互插件:html

  • Real time comments:
  • Adapts your site's lokk and feel,能夠自定義的調整界面外觀
  • Rich media commenting讀者能夠增長圖片和視頻。
  • Works everywhere.支持各類設備,語言。

 

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>

 
生成User表格時沒有使用Trackable下的屬性:
      ## 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個方法解決:

  • 添加上須要的屬性,migration
  • 或者從Devise model中去掉:trackable.

 

視頻2

使用Vuex創建Vue前端和Rails後端的關聯

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來觸發。

 


 

幾張截圖回顧一下Vuex和Vue

1.  比較vue, Vuex實例中的特性:

  • data - state
  • methods - actions/mutations
  • computed - getters

2.Vuex的motion。

  1. axios執行到context.commit,
  2. 執行mutations中的SET_LOADING_STATUS方法,
  3. 而後再對state中的特性進行修改。

 

3. 執行fetchTodos方法的過程圖

  • 首先,執行:commit("SET_LOADING_STATUS", status),  最後State上更新loadingStatus: 'loading'
  • 而後:取數據:axios.get('/api/todos'),
  • 當數據被取回後,執行後面的context.commit。
    • commit('SET_LOADING_STATUS', status),  最後更新loadingStatus: 'notLoading'
    • 最後commit('SET_TODOS', todos), 最後更新State中的todos屬性。
  • 最後, 執行this.$store.getters.doneTodo

 

 


 

 新建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

附加:

Promise.resolve(value)

 

若是要使用async function必須引進babel-polyfill

import "babel-polyfill" 

不然:

 

注意⚠️: 一個store.dispatch在不一樣模塊能夠觸發多個action函數。當全部函數完成後,返回的Promise纔會執行。

 

報錯❌

[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.dispatch放回document.addEventListener。

增長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'
      }
    })
  ]
)

 

視頻3

嵌套的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) } }

 

而後在store.js增長一個createComment action
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)
  }
}

 

 此時刷新網頁會出錯❌,看log或者網頁consoles,沒有定義/comments的路徑:
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

 

再次刷新網頁報告❌:

ActionController::InvalidAuthenticityToken in Api::V1::CommentsController#create

加上sikp_before_action :verify_authenticity_token便可。

 
再次刷新網頁報告❌:
看log:傳入by_url的參數是nil, 即params[:id]不存在。改成 params[:discussion_id],由於使用了嵌套的路由, nested resources url.

 

解釋:
remote_ip方法是ActionDispatch::Request中的方法。返回client的IP地址。 
 
測試網頁成功添加一個comment!

 

下一步:

在form的input中綁定event.
<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: [], #錯誤的記錄
}

 

選擇1:這裏使用vue-map-fields組件中的2個功能:

import { getField, updateField } from "vue-map-fields" 

 

yarn add vue-map-fields
 
在store.js:
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

 

選擇2:若是不用vue-map-fields組件能夠本身寫:

主要是由於v-model在嚴格模式下,會有可能拋出❌。

用Vuex思想解決這個問題:

不用v-model改用v-bind和v-on:input,綁定事件,調用methods, 而後methods在commit Vuex中的mutation。
<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
  }
}

另一種方法:使用帶有setter的雙向綁定計算屬性

<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
    },
    ...

 

回到store.js

//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參數選項,這樣能夠進行服務器端的驗證了!
修改app.vue中模版的代碼:
//添加 errors, 顯示的格式根據代碼本身調整
{{ errors }}

 

另外,添加comments成功後,須要清除原來的內容,添加clearComment方法

#修改createComment方法
+  commit('clearComment')#在mutations中:clearComment(state) {  state.name = ''  state.email = ""  state.body = ""}
相關文章
相關標籤/搜索