憑空來寫一個(gè)框架是不現(xiàn)實(shí)的,所以在這篇文章中我們會嘗試寫一些簡單的應(yīng)用代碼十艾。
我們一開始先寫基礎(chǔ)的 JavaScript,然后將它重構(gòu)成基于 MVVM 的項(xiàng)目查剖。
我在這篇文章中所有代碼都是基于 JSbin 用 babel/ES-6 語法寫的飒泻。如果你對哪一行代碼有疑惑鞭光,可以在那里嘗試一下。
用 MVVM 方式編寫你的代碼
基礎(chǔ) js 方式寫的學(xué)生信息
我們繼續(xù)上一篇文章中提到的學(xué)生信息應(yīng)用泞遗。
如果我們想要完成這樣一個(gè)應(yīng)用惰许,我們可能從下面的代碼開始:
const student = {
'first-name': 'Tracy',
'last-name': 'Kent',
'height': 170,
'weight': 50,
}
const root = document.createElement('ul')
const nameLi = document.createElement('li')
const nameLabel = document.createElement('span')
nameLabel.textContent = 'Name: '
const name_ = document.createElement('span')
name_.textContent = student['first-name'] + ' ' + student['last-name']
nameLi.appendChild(nameLabel)
nameLi.appendChild(name_)
const heightLi = document.createElement('li')
const heightLabel = document.createElement('span')
heightLabel.textContent = 'Height: '
const height = document.createElement('span')
height.textContent = '' + student['height'] / 100 + 'm'
heightLi.appendChild(heightLabel)
heightLi.appendChild(height)
const weightLi = document.createElement('li')
const weightLabel = document.createElement('span')
weightLabel.textContent = 'Weight: '
const weight = document.createElement('span')
weight.textContent = '' + student['weight'] + 'kg'
weightLi.appendChild(weightLabel)
weightLi.appendChild(weight)
root.appendChild(nameLi)
root.appendChild(heightLi)
root.appendChild(weightLi)
document.body.appendChild(root)
輸出內(nèi)容就是一個(gè)像下面的列表:
- Name: Tracy Kent
- Height: 1.7m
- Weight: 50kg
一個(gè)三行的列表,卻花費(fèi)了這么多的代碼刹孔,有點(diǎn)恐怖吧啡省?
為了復(fù)用而重構(gòu)
為啥說有的程序員都沉迷于各種各樣的最佳實(shí)踐娜睛?
那是因?yàn)樗麄兊膽卸琛?br>
懶惰于程序員們而言,卻是一種美德卦睹。
復(fù)用畦戒,是這個(gè)行業(yè)中最棒的想法之一。我們現(xiàn)在的應(yīng)用中重復(fù)了很多行代碼结序,然而程序設(shè)計(jì)中一個(gè)為人們所普遍接受的觀點(diǎn)是 “DRY”:
別重復(fù)你自己的工作(Do not Repeat Yourself).
現(xiàn)在障斋,讓我們減少這個(gè)應(yīng)用重復(fù)的代碼。
我們可以發(fā)現(xiàn)徐鹤,我們執(zhí)行了很多次 document.createElement
來為列表創(chuàng)建 HTML 節(jié)點(diǎn)垃环。事實(shí)上,我們不需要這樣做返敬,因?yàn)樗械牧斜碓毓蚕硗瑯拥慕Y(jié)構(gòu)遂庄。
所以,那應(yīng)該是一個(gè)共享函數(shù)劲赠。
首先涛目,我們先為函數(shù)復(fù)制行的部分:
const createListItem = function (label, content) {
const nameLi = document.createElement('li')
const nameLabel = document.createElement('span')
nameLabel.textContent = 'Name: '
const name_ = document.createElement('span')
name_.textContent = student['first-name'] + ' ' + student['last-name']
nameLi.appendChild(nameLabel)
nameLi.appendChild(name_)
}
光是這樣好像并不正常運(yùn)行,于是我們再修復(fù)一下:
const createListItem = function (label, content) {
const li = document.createElement('li')
const labelSpan = document.createElement('span')
labelSpan.textContent = label
const contentSpan = document.createElement('span')
contentSpan.textContent = content
li.appendChild(labelSpan)
li.appendChild(contentSpan)
return li
}
于是凛澎,整個(gè)應(yīng)用就變成了:
const student = {
'first-name': 'Tracy',
'last-name': 'Kent',
'height': 170,
'weight': 50,
}
const createListItem = function (label, content) {
const li = document.createElement('li')
const labelSpan = document.createElement('span')
labelSpan.textContent = label
const contentSpan = document.createElement('span')
contentSpan.textContent = content
li.appendChild(labelSpan)
li.appendChild(contentSpan)
return li
}
const root = document.createElement('ul')
const nameLi = createListItem('Name: ', student['first-name'] + ' ' + student['last-name'])
const heightLi = createListItem('Height: ', student['height'] / 100 + 'm')
const weightLi = createListItem('Weight: ', student['weight'] + 'kg')
root.appendChild(nameLi)
root.appendChild(heightLi)
root.appendChild(weightLi)
document.body.appendChild(root)
變得更簡短且易讀了吧霹肝。
(老版本里,)你一眼看不出在在一堆節(jié)點(diǎn)創(chuàng)建的代碼里我到底在干什么塑煎,但是新版本里沫换,很明顯我在創(chuàng)建一個(gè)列表和它的元素。
對于閱讀里代碼的人最铁,可能他們并不在意你如何創(chuàng)建列表元素讯赏,他們知道你在創(chuàng)建一個(gè)列表元素就夠了;對于那些在意列表元素的人炭晒,他們可以只查閱 createListItem
函數(shù)待逞,而不考慮你如何創(chuàng)建你的列表。于是网严,應(yīng)用就變成了:
const student = {
'first-name': 'Tracy',
'last-name': 'Kent',
'height': 170,
'weight': 50,
}
// The list creation util
const createList = function(kvPairs){
const createListItem = function (label, content) {
const li = document.createElement('li')
const labelSpan = document.createElement('span')
labelSpan.textContent = label
const contentSpan = document.createElement('span')
contentSpan.textContent = content
li.appendChild(labelSpan)
li.appendChild(contentSpan)
return li
}
const root = document.createElement('ul')
kvPairs.forEach(function (x) {
root.appendChild(createListItem(x.key, x.value))
})
return root
}
//The business logic
const ul = createList([
{
key: 'Name: ',
value: student['first-name'] + ' ' + student['last-name']
},
{
key: 'Height: ',
value: student['height'] / 100 + 'm'
},
{
key: 'Weight: ',
value: student['weight'] + 'kg'
}])
document.body.appendChild(ul)
朝 MVVM 更進(jìn)一步
說真的识樱,現(xiàn)在我們的應(yīng)用已經(jīng)多少有了點(diǎn) MVVM 的風(fēng)格。
student
對象是我們的原始數(shù)據(jù)震束,在我們的重構(gòu)中怜庸,它永遠(yuǎn)不曾變動,我們可以稱之為“模型”垢村。createList
函數(shù)返回了我們需要展示的 DOM 樹割疾,我認(rèn)為它理論上可以被稱為“視圖”。而 “View-Model” 呢嘉栓?不幸的是宏榕,到目前為止拓诸,我們還有沒獨(dú)立出一個(gè)“View-Model”。是的麻昼,我是說奠支,“View-Model”沒有被獨(dú)立出來,但事實(shí)上抚芦,它是存在的倍谜。我們傳遞到 createList
中的參數(shù)是“模型”轉(zhuǎn)換之后的結(jié)果,換句話說叉抡,我們通過手動創(chuàng)建的數(shù)組來使得“模型”和“視圖”相適配尔崔。
下面來將其獨(dú)立出來:
//Model
const tk = {
'first-name': 'Tracy',
'last-name': 'Kent',
'height': 170,
'weight': 50,
}
//View
const createList = function(kvPairs){
const createListItem = function (label, content) {
const li = document.createElement('li')
const labelSpan = document.createElement('span')
labelSpan.textContent = label
const contentSpan = document.createElement('span')
contentSpan.textContent = content
li.appendChild(labelSpan)
li.appendChild(contentSpan)
return li
}
const root = document.createElement('ul')
kvPairs.forEach(function (x) {
root.appendChild(createListItem(x.key, x.value))
})
return root
}
//View-Model
const formatStudent = function (student) {
return [
{
key: 'Name: ',
value: student['first-name'] + ' ' + student['last-name']
},
{
key: 'Height: ',
value: student['height'] / 100 + 'm'
},
{
key: 'Weight: ',
value: student['weight'] + 'kg'
}]
}
const ul = createList(formatStudent(tk))
document.body.appendChild(ul)
這看起來好多了,除了最后兩行……
好吧褥民,再把它們封裝一下:
const run = function (root, {model, view, vm}) {
const rendered = view(vm(model))
root.appendChild(rendered)
}
run(document.body, {
model: tk,
view: createList,
vm: formatStudent
})
需求變更:BMI(Body Mass Index季春,身體質(zhì)量指數(shù))
比如說,我們的產(chǎn)品經(jīng)理要求我們要為學(xué)生信息添加一個(gè)新的字段 BMI轴捎。在原來的代碼基礎(chǔ)之上要實(shí)現(xiàn)這樣的功能缺失讓人惱火鹤盒。至少我不會那樣做,我討厭頻繁的復(fù)制和粘貼 document.createElement
侦副。
相較而言,在 MVVM 版本中驼鞭,它卻變得輕松了:我們只需要修改 “View-Model”秦驯,因?yàn)?BMI 可以通過 height
和 weight
字段計(jì)算得出。
const formatStudent = function (student) {
return [
{
key: 'Name: ',
value: student['first-name'] + ' ' + student['last-name']
},
{
key: 'Height: ',
value: student['height'] / 100 + 'm'
},
{
key: 'Weight: ',
value: student['weight'] + 'kg'
},
{
key: 'BMI: ',
value: student['weight'] / (student['height'] * student['height'] / 10000)
}]
}
我們可以選擇像這樣簡單的完成它挣棕,又或者在函數(shù)內(nèi)部做一些優(yōu)化译隘,但這不是我們這里要討論的問題。
我想表達(dá)的是:
為啥我們要選擇修改 “View-Model”洛心?
在 MVVM 模式中固耘,當(dāng)需要作出修改的時(shí)候,我們的首選總是傾向于修改“View-Model”词身。我認(rèn)為這不難理解:
視圖可能被用于展示其它的數(shù)據(jù)集厅目;它只關(guān)注數(shù)據(jù)該被如何展示。
模型可能被展示成其它模式法严;它只關(guān)注業(yè)務(wù)中發(fā)生了什么损敷。
它們都有被復(fù)用的潛力。所以我們最好要保持它們的通用性深啤。
“View-Model”你是基本上沒有辦法復(fù)用的拗馒;它相當(dāng)于是一個(gè)專門的針對于一個(gè)特定視圖和一個(gè)特定模型的適配器。
因?yàn)樗菍iT的溯街,修改它將不會讓你面臨程序其它地方崩潰的風(fēng)險(xiǎn)诱桂。但是如果你想修改視圖或者模型洋丐,你需要檢查所有它們被使用過的地方。
轉(zhuǎn)換 height
的度量單位
在中國挥等,有一個(gè)笑話是友绝,一個(gè)程序員可以和除了產(chǎn)品經(jīng)理之外的任何人稱為朋友,因?yàn)楫a(chǎn)品經(jīng)理總是變更他們的需求:)触菜。
假設(shè)產(chǎn)品經(jīng)理告訴你九榔,要添加一個(gè)轉(zhuǎn)化 height
字段單位的功能……
事實(shí)上,
我并不想解釋大量關(guān)于如何管理用戶輸入的東西涡相,這會很復(fù)雜哲泊,所以我計(jì)劃在后面的文章中討論它。但是在用戶界面開發(fā)的時(shí)候催蝗,用戶輸入又是如此的重要切威,所以我覺得還是有必要在這個(gè)問題上多說兩句。
為了添加一個(gè)按鈕丙号,我們需要修改我們的視圖先朦,而我們的視圖又可能在其它地方被復(fù)用犬缨,所以我們不應(yīng)該草率地修改我們當(dāng)前的視圖。
這里我們將通過我們將舊的視圖與一些新代碼相結(jié)合來復(fù)用它怀薛。
首先,我們需要一些新的代碼來負(fù)責(zé)現(xiàn)在的量度枝恋,所以我們引入了一個(gè)新的模型创倔。
const tk = {
'first-name': 'Tracy',
'last-name': 'Kent',
'height': 170,
'weight': 50
}
const measurement = 'cm'
我們添加了一條測量數(shù)據(jù)畦攘,而非修改 tk
:所以 tk
仍舊能被其它模塊所復(fù)用。
對于視圖部分十电,我們可以將之前的列表視圖作為我們新視圖的一個(gè)部分:
const createList = function(kvPairs){
const createListItem = function (label, content) {
const li = document.createElement('li')
const labelSpan = document.createElement('span')
labelSpan.textContent = label
const contentSpan = document.createElement('span')
contentSpan.textContent = content
li.appendChild(labelSpan)
li.appendChild(contentSpan)
return li
}
const root = document.createElement('ul')
kvPairs.forEach(function (x) {
root.appendChild(createListItem(x.key, x.value))
})
return root
}
const createToggle = function (options) {
const createRadio = function (name, opt){
const radio = document.createElement('input')
radio.name = name
radio.value = opt.value
radio.type = 'radio'
radio.textContent = opt.value
radio.addEventListener('click', opt.onclick)
radio.checked = opt.checked
return radio
}
const root = document.createElement('form')
options.opts.forEach(function (x) {
root.appendChild(createRadio(options.name, x))
root.appendChild(document.createTextNode(x.value))
})
return root
}
const createToggleableList = function(vm){
const listView = createList(vm.kvPairs)
const toggle = createToggle(vm.options)
const root = document.createElement('div')
root.appendChild(toggle)
root.appendChild(listView)
return root
}
我們的 createToggle
函數(shù)返回一個(gè)帶有一系列單選按鈕的表單。但是從目前的代碼來看摆出,我們無法得知它在我們的應(yīng)用中將扮演的角色。換句話說偎漫,它是同業(yè)務(wù)解耦的爷恳。
最后象踊,View-Model 部分:
如你所見温亲,createToggleableList
函數(shù)需要一個(gè)和我們之前的 createList
函數(shù)不同的參數(shù)棚壁。
所以在 View-Model 上重構(gòu)是必須的。
const createVm = function (model) {
const calcHeight = function (measurement, cms) {
if (measurement === 'm'){
return cms / 100 + 'm'
}else{
return cms + 'cm'
}
}
const options = {
name: 'measurement',
opts: [
{
value: 'cm',
checked: model.measurement === 'cm',
onclick: () => model.measurement = 'cm'
},
{
value: 'm',
checked: model.measurement === 'm',
onclick: () => model.measurement = 'm'
}
]
}
const kvPairs = [
{
key: 'Name: ',
value: model.student['first-name'] + ' ' + model.student['last-name']
},
{
key: 'Height: ',
value: calcHeight(model.measurement, model.student['height'])
},
{
key: 'Weight: ',
value: model.student['weight'] + 'kg'
},
{
key: 'BMI: ',
value: model.student['weight'] / (model.student['height'] * model.student['height'] / 10000)
}]
return {kvPairs, options}
}
我們?yōu)? createToggle
添加了 opt
參數(shù)栈虚,我們使用不同的公式來計(jì)算 height
袖外;當(dāng)仍和一個(gè)單選按鈕被點(diǎn)擊的時(shí)候,這個(gè)模型的度量單位將發(fā)生變化魂务。
看起來很完美曼验,但實(shí)際上當(dāng)你點(diǎn)擊單選按鈕的時(shí)候,它并不會生效粘姜。因?yàn)槲覀儾]有數(shù)據(jù)變化時(shí)的更新機(jī)制鬓照。
這一部分,關(guān)于一個(gè) MVVM 框架如何處理模型更新孤紧,有一點(diǎn)糾結(jié)(思路并不難)豺裆。我將把它留到后面的文章中。
這里号显,我們將使用一種幾乎最簡單的方式實(shí)現(xiàn)它臭猜。
const run = function (root, {model, view, vm}) {
let m = {...model}
let m_old = {}
setInterval( function (){
if(!_.isEqual(m, m_old)){
const rendered = view(vm(m))
root.innerHTML = ''
root.appendChild(rendered)
m_old = {...m}
}
},1000)
}
run(document.body, {
model: {student:tk, measurement},
view: createToggleableList,
vm: createVm
})
這種機(jī)制在計(jì)算機(jī)領(lǐng)域被稱為“輪詢”。在你的應(yīng)用運(yùn)行在瀏覽器中時(shí)押蚤,使用它并不是一個(gè)好的想法蔑歌。雖然它被瀏覽器廣泛地使用:)。
這里揽碘,我們引入一個(gè)國外的庫丐膝。我懶得去自己實(shí)現(xiàn)一個(gè) isEqual
函數(shù)。所以我使用 lodash 來檢查模型的更新钾菊。
每秒鐘,run
函數(shù)都將檢查是否有模型更新:如果更新了偎肃,我們將重新渲染整個(gè)視圖(當(dāng)你有大量的 DOM
節(jié)點(diǎn)的時(shí)候煞烫,這將導(dǎo)致性能問題);否則累颂,我們什么也不做滞详,靜候下一秒。
這是一個(gè)簡單的 MVVM 風(fēng)格的應(yīng)用的示例紊馏,下一篇文章料饥,我們將基于它創(chuàng)建一個(gè) MVVM 框架的示例。
< 上一篇?????????????????????????????????????????????????????????????????????????????????????????????????????????下一篇 >