升級 Livewire V3 的踩坑心得
萬眾期待的 Livewire V3 終於在前陣子推出了正式版本 (8/25),這個版本做了相當多更動,也加入了許多新功能。看了 Livewire 作者在 Laracon US 的介紹後,就非常期待正式版本的到來。
Livewire V3 剛推出 beta 的時候其實有著不少 🐛🐛🐛,幸好在作者與眾多 Contributor 的努力下,目前 V3 已經相當穩定。
Livewire V3 最讓我期待的新功能就是 SPA Mode,只要在連結上加上 wire:navigate
。點擊連結後不會重新整理頁面,而是直接更新現有的網頁內容,大大的增加了使用者體驗。
但在享受 V3 的 SPA Mode 之前,按照慣例,升級還是有一些坑得踩,下面就簡單分享一下我的踩坑心得 😆。
詳細的升級流程請參考官方文件。
在 SPA Mode 中,事件一註冊就會一直存在
在 V3 的 SPA Mode 中,換頁並不會讓瀏覽器重新整理頁面,因此要注意的是 ...
在 SPA Mode 中,事件一註冊就會一直存在。
假設我用 V3 提供的事件 Livewire.hook("commit")
來做一些事情。
// 當 livewire 更新 DOM 之後就會觸發,等同於 v2 的 'message.processed' 事件
Livewire.hook('morph.updated', ({ component }) => {
console.log("hello");
})
接下來打開瀏覽器的 Dev Tools,你會發現每次 Livewire 一更新完 DOM,Console 都會跟你 Say Hello。
如果你只想要讓事件只在特定頁面觸發,就要特別處理,例如我只想要在 say-hello-page
這個 component 中觸發事件。
Livewire.hook('morph.updated', ({ component }) => {
// 只在 'say-hello-page' 頁面觸發
if (component.name === 'say-hello-page') {
console.log("hello");
}
})
還有一點需要注意,因為事件一註冊就會一直存在,為了避免用戶在重複訪問頁面後導致一直註冊事件造成效能問題,會建議在離開頁面或是在 Component 銷毀後,將事件移除。
官方文件有示範如何移除已經綁定的事件,可以使用 Alpine.js 的 destroy()
方法。
Alpine.data('MyComponent', () => ({
listeners: [],
init() {
this.listeners.push(
Livewire.on('post-created', (options) => {
// Do something...
})
);
},
destroy() {
this.listeners.forEach((listener) => {
// livewire 的 hook 會回傳一個 function
// 只要手動執行這個 function,事件就會取消註冊
listener();
});
}
});
在 SPA Mode 中,JavaScript 的載入與執行
一般來說在 V2 ,我們可能會這樣使用第三方的前端套件。
以 Tagify 為例,首先建立一個 resources/ts/tagify.ts
檔案。
// 載入 tagify
import Tagify from "@yaireo/tagify";
// 尋找要套用 tagify 的 input element
let tagsInput: InputElement = document.setElementById("tags");
// 如果 input element 存在,就套用 tagify
if (tagsInput) {
new Tagify(tagsInput);
}
然後在 Livewire 的 Component 中使用 @vite('resources/ts/tagify.ts')
載入 tagify.ts
。
<div>
{{-- tagify 會在 input 的前方加上一個新的元素 --}}
{{-- 為了避免 component 更新後刪除新的元素,可以使用 wire:ignore --}}
<div wire:ignore>
<input type="text" id="tags" />
</div>
{{-- 會轉換成 <script> 標籤載入 tagify --}}
@vite('resources/ts/tagify.ts')
</div>
很常見的載入方式,但如果換到 V3 的 SPA mode 就會有問題。
因為在 V3 的機制中,<body>
內的 <script>
是會重複執行的。
假設你離開頁面後再重新進入頁面,tagify.ts
就會再執行一次。
重新載入一大包 Tagify,這聽起來就不是效能很好的做法 😂。
因此我們應該這麼做,不在 tagify.ts
執行套用的動作,單純只做載入
// 載入 tagify
import Tagify from "@yaireo/tagify";
// typescript 比較囉唆點,所以要先宣告一下我們想在 window 物件中放的東西
declare global {
interface Window {
Tagify: typeof Tagify;
}
}
// 因為 @vite 是使用模組化的方式載入,任何變數都無法在外部使用。
// 所以我們要將 tagify 放到 window 物件中
window.Tagify = Tagify;
然後將 @vite('resources/ts/tagify.ts')
放在 <head>
中載入。
在 V3 的機制中,<head>
中的 <script>
只會執行一次,除非你用瀏覽器重新整理頁面。
<head>
{{-- ... --}}
@vite('resources/ts/tagify.ts')
</head>
然後在 component 中進行 Tagify 套用的動作,這樣就不會重複載入整個 Tagify 了。
<div>
<div wire:ignore>
<input type="text" id="tags" />
</div>
<script>
let tagsInput: InputElement = document.setElementById("tags");
if (tagsInput) {
new Tagify(tagsInput);
}
</script>
</div>
因為 V3 底層改為使用 Alpine.js,你也可以考慮使用 Alpine.js 的語法
<div
x-data
x-init="
new Tagify($refs.tags);
"
>
<div wire:ignore>
<input type="text" x-ref="tags" />
</div>
</div>
離開頁面後再按上一頁。怎麼有兩個 Tagify 元素 !?
如剛剛提到的,Tagify 會在你的 <input>
前面加上一個新的元素。
<div wire:ignore>
{{-- tagify.ts 會新增一個新的元素 --}}
<tags class="tagify tagify--noTags tagify--empty" tabindex="-1"> ... </tags>
<input type="text" x-ref="tags" />
</div>
V3 預設會 Cache 你訪問過的頁面。假設你離開頁面後再按上一頁,V3 會直接使用 Cache 的頁面, 而且 <body>
內的 <script>
會再執行一次。
注意!Cache 是儲存經過 JavaScript 渲染後的頁面。
也就是說,如果你離開了有 Tagify 頁面,然後在按上一頁返回。這時候如果 tagify.ts
再執行一次會發生什麼事情呢?
沒錯...,你會發現畫面上有兩個 Tagify 元素 😂。
<div wire:ignore>
{{-- 剛剛 tagify.ts 新增的元素 --}}
<tags class="tagify tagify--noTags tagify--empty" tabindex="-1"> ... </tags>
{{-- 離開頁面後,點選上一頁重新回來,tagify.ts 又會新增一個新的元素 --}}
<tags class="tagify tagify--noTags tagify--empty" tabindex="-1"> ... </tags>
<input type="text" x-ref="tags" />
</div>
如果是重新點擊含有
wire:navigate
的連結進入頁面,就不會有這個問題。
這個問題在 Beta 版本就有人提出來了。而作者也很快的新增了一個 livewire:navigating
Event 來做處理。
livewire:navigating
讓你可以在離開頁面時,對即將要被 cache 的頁面做一些處理。
document.addEventListener("livewire:navigating", () => {
// Mutate the HTML before the page is navigated away...
});
因此我們要做的,就是在離開頁面前,移除掉 tagify.ts
新增的元素。
let tagify = new Tagify(tagsInput);
// 註冊 livewire:navigating 事件監聽,一離開頁面就把 Tafigy 元素移除
// 如剛剛所述,為了避免事件一直執行
// 這裡可以使用 { once: true },讓這個事件只執行一次
document.addEventListener("livewire:navigating", () => {
// 移除 Tafigy 元素
tagify.destroy();
}, { once: true });
當你使用 JavaScirpt 或者是 TypeScript 等前端語言在頁面上新增一個新的元素時,Livewire 都會將其快取下來。
如果不想在返回上一頁時重複新增一個同樣的元素,可以有以下幾種做法:
- 離開頁面前將新增的元素移除。
- 返回頁面後先判斷要新增的元素是否存在,在決定要不要新增元素。
小結
可以發現在 SPA 模式底下,JavaScript 的使用方式明顯有別於 SSR 模式。本次升級我花了不少時間釐清 JavaScript 的特性,希望這篇能幫助到同樣想升級 Livewire V3 的你 😁。