(GoRails)使用vue和Vuex管理嵌套的JavaScript评论, 使用组件vue-map-field

嵌套的JavaScript评论 Widget Models

创建类似https://disqus.com/ 的插件

交互插件:

  • Real time comments:
  • Adapts your site's lokk and feel,可以自定义的调整界面外观
  • Rich media commenting读者可以增加图片和视频。
  • Works everywhere.支持各种设备,语言。

 

https://gorails.com/系列视频:Embeddable JS widgets.


 

1.下载模版,按照vue.js

rails new embeded_comment -m template.rb

rails webpacker:install:vue

 

2. 创建数据库表格Discussion和Comment.

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

解释:

url属性,存储当前的讨论版的网址。

 

然后修改hello_vue为embed

mv app/javascript/packs/{hello_vue,embed}.js  

 

添加代码:

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.

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 = ""
}
 
 
 

 

posted @ 2018-11-20 10:07  Mr-chen  阅读(463)  评论(0编辑  收藏  举报