我們在上篇介紹了 @track / @api的區別。在父子 component中,針對api類型的變量,如果聲明以后就只允許在parent修改,son component修改便會導致報錯。
sonItem.html
1 <template> 2 <lightning-input value={itemName} onchange={changeItemName} label="item name"></lightning-input> 3 </template>
sonItem.js
1 import { LightningElement, api } from 'lwc'; 2 3 export default class SonItem extends LightningElement { 4 @api itemName = 'test'; 5 6 changeItemName() { 7 this.itemName = 'change item name'; 8 } 9 }
parentForSonItem.html
1 <template> 2 <c-son-item item-name="test parent for son item"></c-son-item> 3 </template>
運行結果:默認顯示一個輸入框,當修改了里面內容,打開console以后會顯示報錯,原因為sonItem這個 component嵌在了parentForSonItem這個父component中聲明了api注解,所以針對itemName只能允許parentForSonItem去更新,然后傳遞到子component。如果我們想子修改后影響api聲明的public變量,就只能通過事件處理方式傳播給父component,讓父去做修改的事情。這個后續會有類似的demo。
如果我們單獨把sonItem放在布局中,改變item name便可以正常的觸發事件並且沒有報錯信息。
一. 父子component交互
在項目中我們針對一個大的component/app設計時,可能有多個component組合在一起,比如我們在salesforce lightning零基礎學習(十一) Aura框架下APP構造實現 這篇中,針對一個最終的功能頁面可能有N個component進行組合從而實現,這種設計的好處是很多component都是可重用的。針對LWC中針對這種component組合有幾個概念。下面是例舉的一個官方的demo。根據層次結構,在LWC中有幾個概念:
Owner:Owner代表當前Own這個template的component,我們可以理解成當前的最高級別的component。當前的component中,todoItem嵌入在了todoWrapper中,todoWrapper嵌在了todoApp中,所以針對當前的component,todoApp是這幾個component的owner。針對owner的 component有以下的功能:
- 針對他包含的component可以設置public變量,這里我們可以設置todoItem的item-name這個public變量(itemName在todoItem中聲明為api類型);
- 可以調用包含的component中的方法;
- 當包含的component設置了事件情況下,owner的component可以監聽到。
Container:Container代表當前這個component包含了其他的component,但是當前的component還在其他的component中。下面的demo中我們可以看到todoWrapper包含了todoItem,但是todoWrapper還被todoApp包含着,所以針對這個demo中,todoWrapper是container。針對container的component有以下的功能:
- 可以讀到包含的component中的public的變量,但是是只讀的,沒法編輯;
- 可以調用包含的component中的方法;
- 可以監聽到bubble類型的對應的所包含的component事件。(事件可以分為bubble/capture)
1 <!-- todoApp.html --> 2 <template> 3 <c-todo-wrapper> 4 <c-todo-item item-name="Milk"></c-todo-item> 5 <c-todo-item item-name="Bread"></c-todo-item> 6 </c-todowrapper> 7 <template>
Parent and Child:當一個component只包含一個子component時,形成了父子模型,todoApp為父,todoItem為子。按照上面的模型,todoApp為owner,具有owner的功能。
1 <!-- todoApp.html --> 2 <template> 3 <c-todo-item item-name="Milk"></c-todo-item> 4 <c-todo-item item-name="Bread"></c-todo-item> 5 </template>
我們在上一篇和這一篇demo中已經介紹過如何去針對Owner設置child component的public變量,此篇中講parent/owner component如何調用child component的方法。這里先介紹一下html中的selector的概念。
Selector:下面的這張截圖大家很常見也很好懂,我們在聲明一個css時經常會寫成類似這種,左側代表一組選擇器,右側代表聲明的css規則塊。css selector可以理解成CSS rule中左側的Group of selectors.
Selector可以分成不同的類型:
- Simple selectors: 基於element type來匹配一個或者多個元素,比如使用class或者id;
- Attribute selectors: 基於attribute或者attribute value來匹配一個或者多個元素;
- Pseudo-classes:匹配一個或者多個處於特定狀態的元素,例如鼠標指針懸停在其上的元素、當前被禁用或選中的復選框或是DOM樹中其父級的第一個子級元素等等;
- Pseudo-elements: 匹配一個元素的某個位置的一個或者多個內容。比如每個段落的第一個字等。
Simple selectors我們在項目中經常用到的就是標簽選擇器,class選擇器,id選擇器。
1 <style type="text/css"> 2 p { 3 background:green; 4 } 5 6 .spanClass { 7 background:red; 8 } 9 10 #spanId { 11 background:yellow; 12 } 13 </style> 14 15 <p>標簽選擇器</p> 16 <span class="spanClass">class選擇器</span> 17 <span id="spanId">id選擇器</span>
Attribute Selector我們在項目中常用的就是基於屬性精確或者模糊設置CSS樣式。
1 <style type="text/css"> 2 [data-vegetable] { 3 color: green; 4 } 5 6 [data-vegetable="liquid"] { 7 background-color: goldenrod; 8 } 9 10 [data-vegetable~="spicy"] { 11 color: red; 12 } 13 </style> 14 15 <ul> 16 <li data-quantity="700g" data-vegetable="not spicy like chili">Red pepper</li> 17 <li data-quantity="2kg" data-meat>Chicken</li> 18 <li data-quantity="optional 10ml" data-vegetable="liquid">Olive oil</li> 19 </ul>
Pseudo-classes我們經常會在項目中處理一些偽類的處理,比如針對超鏈接的懸停,active等的處理。針對此種類型,我們通常在一個selector后面使用' : '關鍵字。
1 <style type="text/css"> 2 a { 3 color: blue; 4 font-weight: bold; 5 } 6 7 a:visited { 8 color: blue; 9 } 10 </style> 11 12 <a href="https://trailhead.salesforce.com" target="_blank">trailhead</a>
Pseudo-elements和Pseudo-classes用法很像,區別是關鍵字是' :: '.用來獲取一個元素的一部分內容。demo中展示的是如果href后面中以https起始,則添加⤴。
1 <style type="text/css"> 2 [href^=https]::after { 3 content: '⤴'; 4 } 5 </style> 6 7 <ul> 8 <li><a href="https://test.com">HTTPS</a> demo will show ⤴</li> 9 <li><a href="http://test.com">HTTP</a> demo will not show ⤴</li> 10 </ul>
四種常用的css selector介紹完了,下面就引出querySelector以及querySelectorAll的概念。
querySelector方法是一個標准的DOM API,作用為針對匹配的selector返回第一個元素。salesforce建議我們盡量不要使用ID作為selector,因為當template中使用ID的時候,瀏覽器渲染以后ID將會變成一個global 的唯一的key。如果我們使用ID作為selector,將可能無法正常匹配上。
querySelector詳情可以查看:https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelector
我們針對一個數組的迭代,比如我們針對復選框使用class作為selector可能需要返回多個元素,這個時候我們就要使用querySelectorAll。此方法用來返回的是所有的匹配的selector的元素。querySelector我們可以查看:https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelectorAll。
有人會提出疑問,繞了這么半天,我可以使用document或者window的global方法啊,比如document.getElementById或者document.querySelector?原因為因為locker原因,LWC不允許使用window或者document這個global 變量,所以替代方案為使用this.template.querySelector()替代document.querySelector()。
使用querySelector/querySelectorAll有幾點注意事項:
- 針對返回的多個數據,元素的順序無法保證;
- 使用querySelector時,如果元素沒有在DOM中渲染的無法搜索出來,我們在后面會有component生命周期管理的內容,當子component沒有渲染加載或者當前在構造函數沒有渲染出來的時候,使用querySelector是無法查詢出來的;
- querySelector不要使用ID作為selector。
下面就通過一個例子來了解針對parent/owner如何去訪問son component的方法。
currentTime.html: 用來將傳進來Date變量format成指定格式的日期,默認獲取的是當前的時間;
1 <template> 2 當前時間: 3 <lightning-formatted-date-time 4 value={currentTime} 5 year="numeric" 6 month="numeric" 7 day="numeric" 8 hour="2-digit" 9 minute="2-digit" 10 second="2-digit" 11 time-zone-name="short" 12 > 13 </lightning-formatted-date-time> 14 </template>
currentTime.js:聲明一個變量用於前台展示,聲明的方法必須要使用@api標簽才可以供owner/parent component進行方法調用。
1 import { LightningElement,track,api } from 'lwc'; 2 3 export default class CurrentTime extends LightningElement { 4 @track currentTime = new Date(); 5 6 @api refreshTime() { 7 this.currentTime = new Date(); 8 } 9 }
showCurrentTime.html:引入current-time,並且放一個按鈕
1 <template> 2 <c-current-time></c-current-time> 3 <lightning-button 4 label="Refresh Time" 5 onclick={handleRefresh} 6 ></lightning-button> 7 </template>
showCurrentTime.js:使用querySelector獲取到current-time這個元素,然后調用其方法。這里的template變量用於在javascript中訪問組件中渲染的元素。
1 import { LightningElement } from 'lwc'; 2 3 export default class ShowCurrentTime extends LightningElement { 4 handleRefresh() { 5 this.template.querySelector('c-current-time').refreshTime(); 6 } 7 }
上述的demo中實現的就是最基本的使用querySelector實現獲取子component並且調用子component方法的例子。其他更多細節歡迎自行查看文檔。
二. LWC針對component的生命周期管理
LWC針對component加載以及移除有一套生命周期管理機制,針對不同生命周期的節點我們可以做不同的事情,也有不同的限制。
針對component加載渲染的生命周期管理圖如下所示:
1. 針對有父子關系嵌套的component,先執行parent component的constructor()方法,針對constructor方法,有幾點需要注意:
- 第一個語句必須是super()並且不帶參數,聲明以后便可以使用了this關鍵字;
- 在constructor方法里面不要使用return語句去return什么返回值,除非是針對某些邏輯下直接返回不執行下面可以使用return 或者return this,其他不允許;
- 和上面的querySelector相同,不允許使用document以及window;
- 不要檢查元素的attribute以及他們的子元素,因為這個階段他們還不存在;
- 不要檢查元素中的使用@api聲明的public 變量,因為他們在component創建以后才可以引用;
2. 查看public變量是否有等待被更新的,如果有,更新public 變量的值;
3. Parent Component插入進DOM中,當插入完會觸發parent component的connectedCallback()方法,這個時候因為parent component已經插入完畢,所以此方法中可以調用parent component中對應的element等信息,我們可以使用this.template去訪問相關的element。通過方法描述可以看出來,此方法可能不止調用一次,當DOM中有新插入的component便會觸發此方法。比如我們動態搜索數據,list數據可能會變化或者reorder,會調用此方法多次;
4. 當connectedCallbacl()方法執行完以后,parent component渲染完成;
5. 此時子component會自動觸發構造函數constructor()方法;
6.查看子component中的變量是否有被等待更新的,如果有,更新public 變量的值;
7. 子component插入進DOM中,插入完成后會調用connectedCallback()方法;
8.子component渲染完成;
9. 當父子component都渲染完成以后,父component調用renderedCallback()方法。
針對component移除的生命周期管理圖如下所示:
當parent component從DOM移除時,會觸發parent component的disconnectedCallback方法;
當son component從DOM移除時,會觸發son component的disconnectedCallback方法。
當我們了解了LWC針對component的生命周期管理,我們便可以更好的針對每個做不同的處理,當然,我們很多時候會將生命周期管理和事件管理一起使用。接下來的內容為LWC的事件管理。
三. LWC 事件管理
對Aura事件不了解或者對web標准的事件管理不了解的可以先看一下salesforce lightning零基礎學習(五) 事件階段(component events phase),LWC和他們有很多相似之處。最開始的demo中我們演示了針對@api的public變量,子component不能修改其變量值,如果子真的有必要修改如何做呢?那就創建一個事件並且去通知其父組件。父組件對這個事件進行監聽,然后父組件去更改這個值並且重新渲染會子組件從而實現了子組件修改變量值的訴求。
在LWC中,Event基於DOM Event,感興趣的小伙伴可以讀一下https://dom.spec.whatwg.org/#events,里面包括了很多的object以及相對應的API方法。當創建Event的時候,官方推薦使用customEvent,因為其擁有更好的兼容性以及更多的功能,同時他封裝了detail變量,我們在事件處理中可以使用此變量去傳遞任意類型的數據。Event事件管理可以進行以下的幾步走。
1. 創建事件
我們使用CustomEvent()去新建一個自定義的事件,此構造函數由兩個參數,第一個參數傳遞的是事件名稱,第二個參數是CustomEventInit,是一個可選的設置項,此參數可以設置好幾個字段。比如detail字段用來在事件中傳遞處理中可以作為參數作為傳遞,bubbles來決定當前的事件處理是bubble還是capture,cancelable來決定當前事件觸發以后是否可以取消,composed來確定是否會觸發shadow DOM 根節點以外的事件監聽器。當然我們在使用中可能常用的就是設置detail用來傳遞參數以及bubble來設置傳播方式。
2. 調度事件
當我們自定義完事件以后,我們需要調度此事件才可以正常的進行事件監聽。使用this.dispatchEvent(eventName)即可調度,調度以后,會根據custom event設置的傳播方式對父子component進行調度。調度順序不懂的可以查看上面的事件階段的博客。
3. 事件監聽處理
當事件創建並且在子component調度完成后,父component便需要進行事件監聽處理。LWC提供了兩種方式進行事件監聽。一種是在父component引入子component時直接在其template上添加監聽器的標簽,另外一種是通過js方式設置監聽器,很像我們的瀏覽器標准事件監聽處理方式。
component標簽方式:比如我們創建了一個自定義的事件名稱為notification在child的component,我們在parent component引入並且想要設置此事件的監聽處理方法為handleNotification方法,我們只需要使用on + 自定義事件名稱即可實現事件監聽處理,這也就是上一篇中介紹為什么不能以on作為變量開頭的原因。
1 <template> 2 <c-child onnotification={handleNotification}></c-child> 3 </template>
js方式:我們在父componet的初始化的方法中,使用addEventListener方法去實現事件監聽,第一個參數是自定義的事件名稱,第二個是要事件處理的方法。
1 import { LightningElement } from 'lwc'; 2 export default class Parent extends LightningElement { 3 constructor() { 4 super(); 5 this.template.addEventListener('notification', this.handleNotification.bind(this)); 6 } 7 }
通過上面的三步走,我們便完成了針對事件處理的基本了解。但是我們疑問還是特別多,比如針對事件處理的方法,我能做什么?針對Event是否有什么封裝好的方法可以讓我更好的去運用? 大家在aura學習事件處理的時候應該很有了解,salesforce lightning零基礎學習(九) Aura Js 淺談二: Event篇 aura提供了我們針對事件處理的一系列的方法。LWC的custom event大部分使用的是DOM原生的,所以DOM 原生Event也封裝好了很多的變量以及方法,想要詳細了解的小伙伴可以查看:https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent,下面只例舉部分常用變量。
detail:detail變量可以獲取到事件聲明的時候傳遞的參數信息,傳遞的參數類型可以為任何的類型。下面的demo中傳遞了一個簡單的object包含了isSelected,在事件處理中我們便可以使用event.detail獲取到當前傳遞的參數。
1 const selectEvent = new CustomEvent('select', { 2 detail: {isSelected:true}3 }); 4 this.dispatchEvent(selectEvent); 5 6 const isSelected = selectEvent.detail.isSelected;
bubbles:返回的是一個布爾類型的變量,判斷當前的事件聲明的是bubble還是capture。如果是bubble則返回true,capture則返回false。
currentTarget:我們事件調度以后,會根據bubble/capture順序去執行相關的handler,currentTarget指定了當前正在處理該事件的元素。
target:獲取我們當前正在執行的事件最開始調度的元素。他和currentTarget是有區別的,currentTarget永遠指定的是當前正在處理該事件的元素。target指定的是最開始調度的元素。
紙上學來終覺淺,絕知此事要躬行。下面以一個官方提供的簡單的demo去更好的了解事件處理。
ContactController.cls:此方法封裝了一個簡單的查詢語句然后返回數據列表
1 public with sharing class ContactController { 2 3 @AuraEnabled(cacheable=true) 4 public static List<Contact> getContactList() { 5 return [SELECT Id, Name, Title, Phone, Email FROM Contact LIMIT 10]; 6 } 7 }
contactListItem.html:作為item,顯示contact name,點擊以后調用handleClick方法。
1 <template> 2 <a href="#" onclick={handleClick}> 3 {contact.Name} 4 </a> 5 </template>
contactListItem.js:在handleClick方法中聲明了自定義事件並且對事件進行了調度。
1 import { LightningElement, api } from 'lwc'; 2 3 export default class ContactListItem extends LightningElement { 4 @api contact; 5 6 handleClick(event) { 7 // 1. Prevent default behavior of anchor tag click which is to navigate to the href url 8 event.preventDefault(); 9 // 2. Read about event best practices at http://developer.salesforce.com/docs/component-library/documentation/lwc/lwc.events_best_practices 10 const selectEvent = new CustomEvent('select', { 11 detail: this.contact.Id 12 }); 13 // 3. Fire the custom event 14 this.dispatchEvent(selectEvent); 15 } 16 }
eventWithData.html:迭代顯示數據,並且針對引入的子component設置了監聽處理的方法。當子component點擊觸發事件,執行handleSelect方法獲取選中的contact然后渲染出來隱藏的詳情區域。
1 <template> 2 <lightning-card title="EventWithData" icon-name="standard:logging"> 3 <template if:true={contacts.data}> 4 <lightning-layout class="slds-m-around_medium"> 5 <lightning-layout-item> 6 <template for:each={contacts.data} for:item="contact"> 7 <c-contact-list-item 8 key={contact.Id} 9 contact={contact} 10 onselect={handleSelect} 11 ></c-contact-list-item> 12 </template> 13 </lightning-layout-item> 14 <lightning-layout-item class="slds-m-left_medium"> 15 <template if:true={selectedContact}> 16 17 <p>{selectedContact.Name}</p> 18 <p>{selectedContact.Title}</p> 19 <p> 20 <lightning-formatted-phone 21 value={selectedContact.Phone} 22 ></lightning-formatted-phone> 23 </p> 24 <p> 25 <lightning-formatted-email 26 value={selectedContact.Email} 27 ></lightning-formatted-email> 28 </p> 29 </template> 30 </lightning-layout-item> 31 </lightning-layout> 32 </template> 33 34 </lightning-card> 35 </template>
eventWithData.js:此方法封裝了一個變量,使用wire Service調用后台的controller去獲取數據展示,當選中子以后調用handleSelect方法執行事件監聽處理。wire service后期會有單獨篇去講。
1 import { LightningElement, wire, track } from 'lwc'; 2 import getContactList from '@salesforce/apex/ContactController.getContactList'; 3 4 export default class EventWithData extends LightningElement { 5 @track selectedContact; 6 7 @wire(getContactList) contacts; 8 9 handleSelect(event) { 10 const contactId = event.detail; 11 this.selectedContact = this.contacts.data.find( 12 contact => contact.Id === contactId 13 ); 14 } 15 }
顯示效果:
默認顯示列表數據,當點擊一個contact name以后,會創建並且調度事件,eventWithData監聽到事件以后,執行監聽處理方法,設置selectedContact。此變量使用track標簽聲明,所以改變以后會重新渲染eventWithData component實現細節的展示。
官方在 github.com/trailheadapps/lwc-recipes提供了很多的學習的demo,感興趣的可以查看。更多生命周期的demo可以查看git demo中的pubsubContactList。
總結:篇中只是介紹了父子通過querySelector獲取相關的element實現交互以及component生命周期管理和事件的簡單處理。篇中有錯誤的地方歡迎指出,有問題歡迎留言。更多學習內容還要自行參看官方文檔。