表單開發是 Web 開發中最多見的需求之一,表單自己的複雜度也在日益增長。咱們如何藉助技術手段,更好地實現表單結構、組織業務代碼?本文介紹了使用 Vue.js 構造可配置化表單的一些經驗。html
做爲現代網頁中最先具備邏輯的部分,表單至今仍在博客類、分類信息以及論壇等以用戶發佈的信息爲核心的網站中,扮演着重要的角色。對這些網站來講,表單意味着信息的初始來源,所以它實際上承載了對於信息處理的第一手邏輯。對於不一樣的類目,表單的內容顯然在業務上須要進行區分,因此,如何實現表單內容的區別化和可配置化就成爲了這一類 Web 應用的一大重點。前端
傳統的 Web 應用使用服務端直接輸出表單的方式,來針對不一樣的頁面邏輯輸出不一樣的表單內容。一些相對完備的框架會提供服務端經過一些簡單的配置輸出表單的功能。例如,PHP 框架 Laravel 提供了經過 Form::textarea('content', null, ['class' => 'form-control'])
這樣的方式來容許在視圖的模板層渲染一個表單控件。然而,在交互邏輯日益複雜的今天,許多需求,例如:字段的實時校驗、控件之間的聯動,在這種模式下的實現是很是困難的,簡單的服務端渲染已經遠遠不能知足業務的發展需求。vue
微軟的 WPF 最先向咱們展現了應用的 MVVM 模式,而 Knockout 則將它帶入了前端的世界。到目前,以 React 和 Vue 爲表明的視圖層框架已經很好地將這種模式投入了生產中。而本文將要介紹的,則正是經過 Vue.js 框架來優化咱們的表單開發能力和體驗。laravel
拋開技術領域的探索,對於表單,咱們要達成的目標是什麼呢?json
試想,有這樣的一些需求:後端
你們看,即便是內容如此簡單的表單,也會有這樣的需求。有一些功能,例如:必填、格式校驗,咱們能夠經過 HTML5 中的 required
或者 pattern
這樣的字段來實現原生的約束,而更多複雜的功能則必須交由 JavaScript。拋開這一部分不談,在純頁面結構上,咱們想要的大概是這樣:瀏覽器
<form class="form">
<div class="form-line">
<div class="form-control">
<textarea name="content"></textarea>
</div>
</div>
<div class="form-line">
<div class="form-control">
<input type="hidden" name="address">
<!-- 具體的控件實現 -->
</div>
</div>
<div class="form-line">
<div class="form-control">
<input type="text" name="contact">
</div>
</div>
<input type="hidden" name="_token" value="1wev5wreb8hi1mn=">
<button type="submit">提交</button>
</form>複製代碼
而咱們指望能有這樣的配置直接配置上述的頁面結構,以及其部分的邏輯:安全
[
{
"type": "textarea",
"name": "content",
"validators": [
"minlength": 8
]
},
{
"type": "tree",
"name": "address",
"datasrc": "areaTree",
"level": 3
},
{
"type": "text",
"name": "contact",
"required": true,
"validators": [
"regexp": "<mobile>",
]
}
]複製代碼
再加上一點簡單的業務邏輯代碼,就構成了咱們對於表單的所有配置,而剩下的工做都由表單框架來生成。微信
關於如何使用 Vue.js 搭建一個簡單的 Web 應用,在不少地方已經有很是優秀的介紹,例如 Vue.js 的官網 [1] 就提供了不少實例,所以咱們也再也不贅述。在這裏我將只介紹一些核心的實現,以供你們參考。前端工程師
基本的實現邏輯以下圖所示:
整個流程能夠分爲:後端數據傳遞(品紅)和外部擴展(藍色)兩部分,接下來會對各個部分的核心流程詳細介紹。
Vue.js 面向的運行環境在絕大多數的手機瀏覽器上是能夠良好支持的 [2] 。所以咱們能夠直接在 HTML 或者對應的模板文件中寫以下的代碼:
<div id="my-form">
<my-form :schema="schema" :context="context"></my-form>
<script type="text/json" ref="schema">{!! json_encode($schema) !!}</script>
<script type="text/json" ref="context">{!! json_encode($context) !!}</script>
</div>複製代碼
(注:這裏使用的語言是 Blade [3])
#my-form
這個元素做爲咱們交由 Vue 控制的根容器聲明,而 <my-form>
則是咱們爲表單建立的控件。這裏值得注意的是,咱們經過一個帶有 ref
的 script
標籤來使得咱們能夠從後端傳遞數據給 Vue 組件。
在這裏,我使用了兩個來自於後端的數據對象。schema
是相似於上一節中我提到的配置內容,它將經過 Vue 的根容器傳遞給對應的表單控件;而 context
則用於處理其餘須要後端讀取的數據,例如一些代碼中可能會根據不一樣的用戶角色進行處理,則咱們能夠把這部分信息也傳遞給 JS 便於控制。
在 JS 文件中,咱們可使用以下的方式來處理上述的數據:
new Vue({
// ...
mounted() {
this.schema = JSON.parse(this.$refs.schema.innerText)
this.context = JSON.parse(this.$refs.context.innerText)
}
})複製代碼
這樣,咱們就能夠經過實現 form.vue
來實現咱們的表單構造。
附註
在 my-form
組件中,咱們能夠經過後端傳遞的 Schema 配置,來生成對應的控件
<template>
<form :class="form" method="post">
<my-line v-for="(item, index) in schema" :schema="item"></my-line>
</form>
</template>複製代碼
my-line
這個元素,在這裏被咱們用於構造統一的表單模板,例如,全部的控件都會被 <div class="form-line"></div>
這樣的容器包裹,那麼咱們能夠將這部份內容做爲 my-line
元素的模板聲明。使用這種方法咱們能夠構造相同的 Label 元素、錯誤提示等。
在 my-line
組件中,咱們能夠經過這樣的方式來聲明實際的表單控件:
<div class="form-ctrl">
<my-input :schema="schema" v-if="schema.type === 'input'"></my-input>
<my-textarea :schema="schema" v-else-if="schema.type === 'textarea'"></my-textarea>
</div>複製代碼
這種方式看起來簡單直接,但它會使 my-line
組件變得異常複雜。爲了解決這個問題,咱們能夠引入一個虛擬組件 my-control
,由它本身根據不一樣的 schema.type
渲染出不一樣的表單元素。
Vue.js 中使用函數式組件能夠聲明一個自己不渲染,但能夠調用子組件的組件。咱們只須要這樣聲明:
<div class="form-ctrl">
<my-control :schema="schema"></my-control>
</div>複製代碼
// my-control.js
function getControl(context) {
const type = context.props.schema.type
// 在這裏分發組件
}
export default {
functional: true,
props: {
schema: Object
},
render(h, context) {
return h(getControl(context), context)
}
}複製代碼
這樣,能夠將控件的複雜度從 my-line
這個組件中抽離出來,更有利於各組件的獨立維護。
如上所述,咱們已經能夠將各類控件,例如 my-input
、my-textarea
獨立進行實現。可是,這些組件中可能會有一些通用的邏輯。好比,控件對應的表單字段顯示的名稱,咱們實際上須要這樣的屬性:
export default {
// ...
computed: {
displayName() {
// 若是有獨立配置就使用配置的名稱,而默認使用表單項的 name 屬性做爲名稱
return this.schema.displayName || this.schema.name
}
}
}複製代碼
再好比,咱們對於全部的控件,都會有對應數據的 data
屬性;或者對於各個組件,咱們須要統一執行生命週期方法對應的操做。這種狀況下,咱們能夠將統一的實現抽象爲一個獨立的類:
// contract.js
export default {
// 一些公用的方法
}
// input.vue
import Contract from './contract'
export default {
mixins: [Contract]
// ...
}複製代碼
而且,因爲 Vue 的 mixin 機制,咱們能夠在 contract.js
中聲明統一的生命週期函數,而在控件對應的組件中,再次聲明生命週期函數不會覆蓋統一的處理,而是會在統一函數以後執行。這保證了咱們能夠安全聲明獨立的生命週期而無需再次添加統一邏輯。
有一些比較特別的元素,例如:提交按鈕、及有些網站發佈表單可能會出現的協議勾選,這些東西顯然不能做爲表單控件注入。但咱們可使用其餘方式來簡單實現:
<!-- template -->
<div id="my-form">
<my-form :schema="schema" :context="context"></my-form>
<div class="action" slot="action">
<button class="form-submit" type="submit">{{ $btnText }}</button>
</div>
</div>
<!-- my-form -->
<template>
<form :class="form" method="post">
<my-line v-for="(item, index) in schema" :schema="item"></my-line>
<slot name="action"></slot>
</form>
</template>複製代碼
經過 Slot 機制,咱們能夠從外部向 Form 內注入一個不屬於表單控件的元素。同理,若是咱們須要加入一些 CSRF 元素等隱藏的表單項,也能夠經過這種方式進行。
在完成了基礎組件以後,咱們還有一些基本的交互功能,以及業務邏輯可能會考慮的功能。例如上文中提到的必填等。這時候,咱們須要從 JavaScript 角度對咱們的表單進行擴展。
爲了防止業務邏輯擴散到控件邏輯中,咱們須要提供一套機制來使得業務邏輯能夠在對應的時刻執行。例如,必填的真實含義實際上是當控件數據改變時,觀察是否爲空。若是存在必填項數據爲空,禁用提交按鈕。顯然,控件數據改變時是生命週期的一個過程(updated,或者是自定義的 @change 事件),因此咱們能夠經過事件傳遞的機制來實現一套業務邏輯處理的框架。
表單的核心是 Form(表單元素)和 Control(控件),因此,咱們須要經過一個獨立的 Event Emitter 將對應的核心控件的事件代理出來。
const storage = {
proxy: {
form: null,
control: {}
}
}
class Core {
constructor(target) {
this.target = target
}
static control(name) {
return storage.proxy.control[name] ||
(storage.proxy.control[name] = new CoreProxy(`control.${name}`))
}
static form() {
return storage.proxy.form ||
(storage.proxy.form = new CoreProxy('form'))
}
mount(target) {
// ...
}
on(events, handler) {
// ...
}
emit(events, ...args) {
// ...
}
}複製代碼
經過這種方式,咱們能夠經過 Core.form()
或者諸如 Core.control('content')
的方式來得到一個在當前頁面持久有效的 Emitter。而後咱們只須要在對應的 Vue 文件中代理生命週期事件:
import Core from './core.js'
export default {
// ...
beforeUpdate() {
// 避免初始化以前產生事件
if (!this.schema.length) return
Core.form().mount(this).emit('create', this)
},
}複製代碼
爲了不全局引入 CoreProxy
,能夠把這個類暴露在 Vue.prototype
上。經過 Vue Plugin,能夠實現下面的效果:
// contract.js
export default {
// ...
updated() {
this.$core.control(this.schema.name).emit('update', this)
// propagation
this.$core.form().emit('update', this)
}
}複製代碼
經過這種方式,咱們能夠將對應的 Vue 對象傳遞給 Core 來代理,但同時不把它直接暴露給外部。好比咱們的代碼多是這樣:
// 這個文件用來實現「必填」功能
Core.form().on('update', function(control) {
if (!control.schema.required) return
if (control.model) {
// error對應的事件由其餘文件來處理
Core.form().emit('resolve-error', control, 'required')
} else {
Core.form().emit('reject-error', control, 'required', '此項必填')
}
})複製代碼
同理,咱們也能夠將事件在不一樣的組件中傳遞。例如,咱們須要在「類型」選擇爲「手機號碼」的狀況下校驗「聯繫方式」字段:
Core.control('contact-type').on('change', function(control) {
// 這裏咱們不能直接讀取到「聯繫方式」,應該經過其餘的方式來處理
const proxy = Core.control('contact')
const contact = proxy.read()
// ...
})複製代碼
由於在 Core 的內部,咱們能夠獲取到對應的 Vue 對象,因此咱們徹底能夠暴露出一些相似於 read
這樣的只讀方法供外部調用;對於數據修改,例如外部也可能須要修改其餘控件的數據,咱們一樣能夠提供一些內置的事件,例如 Core.control('contact').emit('write', newValue)
來使得外部有能力修改這些數據,同時能夠獲得統一的控制。
以終爲始,咱們最後來聊一聊,爲何咱們的表單在 Vue 這樣的框架中能夠被更好地表達:
.lazy
、.trim
等修飾符,可讓咱們將精力集中在對於邏輯自己的處理上Vue.js 自己是一個很是優秀的框架,一方面,它能夠經過最精簡的方式讓咱們以 Vue 組件的形式描述出咱們的控件;同時,咱們可使用 Vue 提供的一系列其餘功能,來實現諸如控件抽象、控件分發、事件傳遞模塊的共用、外部內容注入等更加複雜的功能。若是你們在平時也有相似的表單開發的需求,不妨嘗試使用 Vue 來構建。
做者:孫翛然
簡介:前端工程師、Web 開發工程師,致力於基於不一樣框架和語言的業務架構設計和開發。本文僅爲做者我的觀點,不表明百姓網立場。
本文在 「百姓網技術團隊」 微信公衆號首發,掃碼當即訂閱: