過去有好一陣子我沒有在這邊新增新的文章,主要是因為我自己在工作之餘陸續開發了兩年多的應用程式 Box Pleating Studio(下簡稱 BPS)發展得如火如荼、且我要趕著在去年底前正式發表上線而無暇兼顧其它事情的緣故。而現在 BPS 已經正式發表並且漸趨穩定,也該是時候來陸續寫一些相關的開發心得了。

先簡單介紹一下 BPS;基本上它是為了輔助超複雜系摺紙設計而開發的應用程式,幫助摺紙家利用一種稱為「箱形褶(box pleating)」的設計框架來創造在「正方一枚不切」的規範下達到任意程度複雜的作品。單純的箱形褶在紙張面積運用上的效率常常並不是很好,但是從 2000 年代開始,陸續有一些摺紙家發現了一些摺痕結構可以改進箱形褶的效率,而我則和一位世界級摺紙大師 Robert J. Lang 在 2017 年間共同將那些結構集大成並加以推廣成我們稱之為 GOPS 的一套系統,並且共同在 2018 年的第七屆世界摺紙研討會(7OSME)上面發表相關論文,而 BPS 也是從那段時期開始開發的,因為我希望能有一個應用程式能夠自動幫人計算出 GOPS 的組合,並且方便使用者快速地實驗各種可能的配置以找出理想的設計。BPS 是完全免費而且開源的 app,它的原始碼可以在我的 GitHub 上面找到。

從軟體工程的角度來看,我相信 BPS 對於想要開發 web app 的人來說會是一個有意思且完整的範本;它整合了許多技術來達到理想的使用者體驗和開發體驗。而在本篇中我先從 PWA 來談起。

BPS 是以 PWA(Progressive Web App,漸進式網路應用程式)的形式打造的 app,簡單來說,PWA 就是一種用網頁(亦即 HTML + CSS + JavaScript 的組合)來產生近似於原生應用程式外觀和體驗的技術。PWA 本身雖然只是網頁,但是可以達到:

  1. 可以安裝成一個具有桌面圖示的獨立應用程式,外觀上與一般應用程式無異。
  2. 打開的時候會有 splash screen(手機限定)。
  3. 可以接收推播訊息。
  4. 可以啟用背景同步功能(這點是一般的 WebView 無法做到的)。
  5. 利用 Service Worker 來完全控制檔案的快取機制並達到自動更新和離線使用。

此外,由於它本身確實只是網頁,所以一樣可以在一般的瀏覽器中開啟、而且可以完全跨平台。當初我會選擇讓 BPS 走上 PWA 的路線,主要就是因為我想達到完全的跨平台。如果我的目標只是桌機,我可能會改選用 Electron,因為暫時 Electron 在一些小方面上的使用者體驗可以比 PWA 更好 1,只不過 Electron 並不支援手機,而我從一開始就很希望可以讓 BPS 在手機上跑。就結果來說,出乎我原本意料地,BPS 其實有 2/3 的使用者都是用手機在執行 BPS,不枉費我對於把手機版給做好的堅持。

目前嚴格來說,只有三個瀏覽器支援 PWA 的獨立安裝功能,就是 Chrome(除了 iPhone 以外的版本)、桌機版的 Edge(因為同為 Chromium 核心)和 iOS 版的 Safari。用其它的瀏覽器打開 PWA 網站的時候只能在瀏覽器內部操作而不能安裝。不過,有一派的人預測 PWA 會是未來的一大趨勢,所以或許未來會有更完整的全面支援。

只是在那之前,目前 PWA 的規格暫時是分成兩大派各行其道;Chromium 這一派的設定是寫在 manifest.json 檔案裡面,而 iOS 這一派的設定是寫在 <meta><link> 標籤之上的。現階段除了兩種都寫上去之外沒有什麼別的辦法,而這真的有點麻煩,尤其 iOS 的 splash screen 竟然還要每一種解析度都給它出一張圖片這一點實在是……有人說那叫自訂度高但我叫它白痴到家(幸好網路上有自動產生器可以用)。

關於 PWA 的設定方式,網路上已經有很多教學文章了(例如 PWA 實戰經驗分享),所以這邊我不會重複同樣的內容。我這篇作為 BPS 系列的開頭,我只談幾個我實際上開使用 PWA 之後注意到的幾件事。

首先,關於 iOS 上面的 splash screen 圖檔,即使檔案或 HTML 設定有所更新,iOS 也似乎並不會自動跟著更新,而是要等使用者重新安裝 PWA 到桌面上才會生效;這點我並不是很確定,因為我也沒有試著一直等看看會不會過久一點它會去自動更新(就好像我知道 Chromium 是每隔一陣子才會檢查 manifest.json 有沒有更新),但是總之我建議各位最好一開始就把 splash screen 的圖檔畫到定位然後盡量就不要去動比較保險。

再來就是 splash screen 到底會持續開啟多久。其實不管是 Chromium 還是 Safari,這個 splash screen 都不是單純開著殺時間用的,而是真的在 splash screen 開啟的同時背後的特製 WebView 2 就已經有在載入網頁了。但是網路上常常有人疑惑「怎麼 splash screen 跑了那麼久還沒切換到畫面」,基本上這通常是因為網頁在啟動的時候載入了太多資源、或跑了太久的 JavaScript 的緣故。雖然至今我沒有找到正式的文件說明到底 splash screen 什麼時候會消失,但是根據我自己的實驗,答案似乎是「至少等到 DOMContentLoaded 事件觸發、然後 JavaScript 的執行堆疊完全清空」。所以,為了不要讓 splash screen 維持太久,一個辦法是在 DOMContentLoaded 事件裡面設置一個 setTimeout,然後才是在裡面執行剩下的、需要跑比較久的 JavaScript 或資源載入(當然,為了較好的 UX,此時應該在畫面上顯示簡單的「載入中」動畫)。這個 setTimeout 會製造出執行堆疊的斷點,讓 PWA 知道 splash screen 可以消失了。

最後是一個為了達到與原生 app 同樣的使用者體驗一定要做的設置:關掉 pinch-zoom(兩指縮放)。原本一般的網頁都是允許這個動作的,但是基本上沒有任何原生的 app 會讓使用者這麼做,所以這個一定要關掉。很多文件會提到類似這一行的設置:

<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, width=device-width, user-scalable=no">

但是相信我這一行是沒有用的,現在的瀏覽器根本不會理會它。真正有用的應該是 CSS 的:

* {
    touch-action: pan-x pan-y;
}

這樣寫表示使用者可以做橫向(pan-x)或縱向(pan-y)的捲動手勢,但是不能使用縮放(pinch-zoom)。如果你跟我一樣連捲動也要關掉,那就直接寫 touch-action: none; 就好了。

本來應該是這樣就好了,偏偏事情沒有那麼簡單。一直到 Safari 13 版之前,Safari 的 touch-action 只支援兩種設定:auto 或是 manipulation,也就是說上面這樣做偏偏對舊版的 Safari 是無效的。或問:為什麼會有舊版的 Safari?使用者不會更新嗎?答案很簡單,因為他或許拿著的是一支根本無法繼續更新 Safari 的手機(例如 iPhone 6),而相信我,暫時這樣的人口還是無法完全忽視的;我本來也以為不會遇到,但是我的 app 才上線沒幾天就有人跟我反應他希望也能夠在他的 iPhone 6 上使用。

那怎麼辦呢?只好用老套的辦法,捕捉事件並且阻止預設行為了:

// 由於我在 CSS 檔案裡面把 touch-action 設定成 none,
// 如果最後實際上的樣式並不是 none 就知道瀏覽器並不接受該值,
// 亦即我遇到舊版 Safari 了。新的瀏覽器則不受這段程式影響。
if(getComputedStyle(document.body).touchAction != "none") {
    document.body.addEventListener("touchmove", e => {
        // 不允許任何兩指以上的操作
        if(e.touches.length > 1) e.preventDefault();
    });
}

另一個可以使得 PWA 更接近原生行為的 CSS 設置有:

/* 只要不是輸入框,都不允許長按選取文字 */
*:not(input):not(textarea) {
    user-select: none;
    -moz-user-select: none;
    -webkit-user-select: none;
    -ms-user-select: none;
}

大概就是這樣;下一篇我再來談比較大的主題,Service Worker。


  1. 例如 Electron 可以呼叫原生的「存檔」對話方塊,但是 PWA 暫時仍然只能把存檔的行為用「下載」的方式來呈現。目前已經有相關的討論打算要增加新的 API 來做到這部份,不過暫時沒有結果。 

  2. 如同我稍早提到的,這個 WebView 跟一般的 WebView 不太一樣,裡面的 Service Worker 可以有更多的權限。 

分享此頁至:
最後修改日期: 2021/02/07

留言

撰寫回覆或留言

發佈留言必須填寫的電子郵件地址不會公開。