嵌套的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>
## 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。
- axios执行到context.commit,
- 执行mutations中的SET_LOADING_STATUS方法,
- 然后再对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
附加:
如果要使用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.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) } }
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
ActionController::InvalidAuthenticityToken in Api::V1::CommentsController#create
加上sikp_before_action :verify_authenticity_token即可。
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: [], #错误的记录
}
选择1:这里使用vue-map-fields组件中的2个功能:
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
选择2:如果不用vue-map-fields组件可以自己写:
主要是因为v-model在严格模式下,会有可能抛出❌。
用Vuex思想解决这个问题:
<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参数选项,这样可以进行服务器端的验证了!
//添加 errors, 显示的格式根据代码自己调整 {{ errors }}
另外,添加comments成功后,需要清除原来的内容,添加clearComment方法
#修改createComment方法 + commit('clearComment')
#在mutations中:
clearComment(state) {
state.name = ''
state.email = ""
state.body = ""
}