表單 Practice

Form Validation

這邊是我最近對於表單的一些作法,因為內化還不夠,每次都會漏掉一些,所以花了些時間整理整理,適合的情境不是 single page application 就是了,比較偏傳統形式網頁的表單,然後可能也包括不少大家早就知道(?)的細節就是了。

首先,我現在偏好不用 JavaScript 做表單檢查,而是先做好最基本的 server side 檢查,然後加上HTML5 的表單檢查,會這樣決定的主因是:

  1. JavaScript 的表單檢查 library 用起來都不太順手,而且不想花時間處理串接,且能少點 library 總是好的;
  2. 幾個常用的 type,像是 email, url 比較不需要擔心檢查的 pattern 有不周全的地方,我想很多人都有上網搜尋過這些欄位的 regular expression pettern 的經驗;
  3. 支援度已經不是大問題了,事實上我的工作上還需要支援 IE 10, 9 之類的,其實這些非 modern browser 的量都已經非常少了,所以就靠個 server side 檢查對付他們就好,使用體驗稍微差一點也還可以接受,這也是種 graceful degradation(優雅降級);
  4. HTML5 的表單檢查可以說是 web developer 當年對抗網路標準發展遲緩一大勝利指標,當然應該要好好用一下。

而用 HTML5 表單檢查還有個意外的好處是基本的錯誤訊息自動有翻譯(看使用者瀏覽器的語言),另外如果有自製的輸入元件,也有API可以串接,當然訊息就要自己提供就是了。

用 HTML5 表單檢查當然也不是完全沒有問題,例如目前 email 欄位還沒有瀏覽器支援IDN domain 的信箱;另外就是上傳檔案的 file input 的值不能從 server 端直接給,這限制是因為會有安全性問題,而這限制所衍生的問題是:表單送到 server side 檢查後發現有錯誤時(例如 captcha 錯),使用者就一定要重新選取上傳的檔案,對於使用者體驗算是個扣分(而且上傳檔案大的話很花時間,然後另外還有個上傳檔案大小限制、就又是另外一個議題了),要解決這問題一般來說就是靠 JavaScript 做些加強,例如針對 captcha 可以先用 ajax call 檢查 captcha ,正確的話就換個 session token 之類的回來,不過即使這樣,還是逃不了完整的 server side 表單檢查,所以也還要處理 ajax submit 後的表單錯誤訊息顯示。

不管是 server side 檢查後產生的錯誤訊息,還是 ajax call 之前檢查產生的錯誤訊息,理所當然都會放在欄位附近,不過還要讓訊息和欄位之間建立關聯,才好進一步做一些處理,例如使用者有更新欄位值之後會把錯誤訊息隱藏之類的,或許很多人會用父層 DOM 節點加上特殊的 class 包起來找,不過我比較偏好用aria-describedby,大概會看起像是:

<input id="mail" name="mail" type="email" aria-describedby="mail-field-info" />
<span id="mail-field-info">Required field!</span>

這樣只要找的到#mail欄位,就可以透過他的aria-describedby屬性找到該欄位的相關訊息的 DOM 節點,另外值得注意的是,aria-describedby 值的格式是IDRefList,不是單一個 ID,而是一個用空白切分的 ID 指標們,所以如果有這種情形,還可以在錯誤訊息的那個 DOM 節點加上role="alert"給它用來辨識,其實就算只有一個 ID 也還是可以加上 role 屬性啦。如果真的需要用透過父層 DOM 節點來找的話,之前研究的結論是可以在預期的父層標籤用role="section"來方便定位,用 jQuery 大概會像是:

$fieldSection = $field.closest('[role="section"]');

這個標籤下應該會包括欄位的標籤(label)、欄位的 input element 以及相關的資訊(說明、錯誤訊息)等。

另外還有一點,就是要用 ajax 上傳檔案的話,需要有支援FormData的瀏覽器,並且如果用 jQuery 送 FormData 的話記得要加點設定:

contentType: false,
processData: false

還有就是 ajax 送表單的目標 URL,我目前比較喜歡的作法是讀<form>action屬性,也就是和瀏覽器自己送的 URL 一樣,然後透過 HTTP content negotiation 機制來決定回傳的格式,比較正確的作法是看Accept,以 jQuery 來說,如果要 server 回 JSON 格式的話,可以加上:

dataType: 'json'

這樣送出的 request 就會帶上正確的Acceptheader,向 server 端要求application/json,不過Accept的值解析起來比較麻煩些,其實是可以送出說 client 端可以接受多種格式,然後還加上個優先度的,也因此也有很多人是看X-Requested-With,一般 library 如果是透過 XHR 發的 request 都會有這個 header;還有就是送出的資料格式(Content-Type),即使是 ajax call,我目前也都不用 JSON 了,還是用application/x-www-form-urlencoded為主,另外要上傳檔案的話當然一定要用multipart/form-data,主要是因為:

  1. 送 JSON 的話就不會是simple request了,有些時候會比較麻煩,例如 Cross Origin 時會需要發 preflight,然後就可能遇到 AWS 以前不支援 preflight request 的 bug;
  2. 用這幾個老的 Content-Type 支援度還是比較高,對於 server 端實做和 client 端實做其實都相對友善一點,例如 jQuery 預設就依然是 form-urlencoded,没特别需求還是用標準一點的格式,特殊需求是例如 GraphQL,不過一般表單發送應該不會走 GraphQL 吧。

其實 JSON 雖然已經有 RFC 規範了,不過在 Web 標準的世界還沒相當深入內化,不知道以後有沒有機會更加的內化整合進去。

前面有提到 ajax call 送出的目標 URL 我會偏好從,<form>裡面讀,不過或許有的情境會讓 ajax call 必須要自己用不一樣的 API URL,這時候我建議還是把 API URL 寫在<form>的屬性裡面,這樣可以讓 JavaScript 的邏輯比較乾淨,也不用作什麼 mapping 或是常數來儲存 API 的 URL,維護修改時也不用兩邊檢查,屬性名稱可以用例如:data-action之類的屬性,data-*屬性正好適合來做這些事情,不但有 DOM API 支援,jQuery 也可以用.data()method 來讀取,命名上,如果覺得有個標準參考比較好,可以看看jQuery-ujs的設計,雖然比較長一點,它用的是:data-ujs:submit-button-formaction,我是覺得有些不正確啦,畢竟要送出表單不一定是點擊 submit button。

其實假設送出表單的動作都是滑鼠點擊 submit button 這是個親和力問題,如果只把 ajax call 送的函式 bind 在 submit button 的 click 事件上,這其實是不太好的,因為其實瀏覽器預設的行為是可以在很多地方用鍵盤送出表單,例如在 text input 上按下 Enter 鍵,或是在 submit button 上按下空白鍵之類的,所以針對表單還是要去 bind form submit 事件才是正解,至於 jQuery-ujs,其實也是這樣做的,它是用 delegate event 的形式去監聽傳遞到 document 上的 submit 事件,然後才去做後續的處理,只是命名上讓人覺得不太正確。

最後一項,前面說不用 JavaScript 做表單檢查(不看自訂輸入元件的話),其實有一個例外,就是上傳檔案的大小檢查,因為沒做對使用體驗的影響比較大,然後就是要還要記得針對 ajax call 送表單加上HTTP 413Status Code 的錯誤訊息處理。


a11y

Pa11y Dashboard,

標題的 a11y 其實是 accessibility (親和力)的縮寫,現在英文世界似乎很大量的使用這個簡稱,今年的 JSConf EU 前陣子放出演講錄影,其中有一場是在講網路親和力的議題「YES, your site too can (and should) be accessible.」:

講者是 Laura Carvajal,在Financial Times工作,而這場演講就是他們改進 ft.com 網站親和力的過程和一些想法,我覺得精華在後半,前面是介紹自動化工具Pa11y,a11y 是 accessibility,至於首字母的 P,看 README 應該是 pal 的意思;他們把這個自動化工具整合進他們的開發流程,然後慢慢的修改,直到把回報的問題都修完,其實現在自動化工具已經很強了,連顏色對比度夠不夠都能算出來(瀏覽器的開發工具以後也會有相關資訊),不過要驗證親和力做的如何,還是有很大量的驗證其實是需要手動測試。

手動測試的部分他們是請了DAC(Digital Accessibility Centre) 來做,演講中還有一些測試者的測試影片,每位測試者都會先說他身體有怎樣的障礙,然後他邊測試會邊口述他在做什麼,遇到怎樣的問題,建議可以怎麼處理,感覺就是很專業的測試員。總之,在他們處理完所有 Pa11y 檢測到的問題後,才請 DAC 做親和力評估驗證,結果還是收到了一份兩百多頁的測試報告,回報了各種 ft.com 網站上的親和力問題,之後又花了幾個月的時間來處理這些問題,最後終於得到 DAC 的認證,這份認證資訊還蠻完整的,還說明了他們認證時網站的狀況,還有哪些問題待解決,甚至連可能會使用到但是還沒處理過親和力問題的同組織的網站(服務)都有列出來,另外在 ft.com 的親和力聲明也可以看到 DAC 的認證。

接著 Laura Carvajal 介紹到如何實際體會(參與)這些親和力問題,其中一個很經典的狀態就是只用鍵盤做所有的控制,他提供了一些強迫自己只能用鍵盤操控的方法,並且在這種狀態下工作,其它還有像是使用 Mac 的 VoiceOver 做為 ScreenReader、使用 Windows 的高對比模式等等,他建議可以實際自己去體驗看看的,甚至強迫自己使用一陣子,會對這些問題更有體會,除此之外,他還透過一些活動來讓其他公司內的人也來參與,像是模擬一個障礙者會面臨的環境,以他的例子來說是把網頁模糊化,模擬視力障礙的使用者狀態,然後在這個狀態下請人去完成一些任務,像是填一個表單,並且有提供些獎勵增加參與人數,記得他們是提供 Amazon Credit,這樣可以讓更多人體會到需要依靠輔具來上網的不便,長久下來也可以讓這些工作的推動更加順利。

這幾天我也試著裝起了Pa11y Dashboard開始做些檢測,看到的 Error 加上 Warning 數量真是有點驚人,再來慢慢處理吧...


Graceful Degradation Background

Rockman

昨天解了一個其實困擾蠻久的問題,就是在蘋果開始支援 Retina Display(HiDPI)之後,網站上使用的圖片也跟著要提升解析度,一般網頁會使用的圖片基本上就是<img />標籤的圖片和用 CSS 設定的背景圖,對於<img />標籤來說,提供高解析度的圖片並不會有向下相容的問題,當然瀏覽器和作業系統不支援的,抓了比較大的圖下來是比較浪費,也為此有<picture>標籤、srcset屬性和Client Hints等標準來處理這個問題,不過我的需求只要圖片都能正常且正確的顯示。

<img />標籤只要有設定寬高,圖片顯示出來就會是預期的大小,但是background-image就不是了,在background-size出來前,背景圖多大,它在網頁上就會照那樣的尺寸下去畫,所以在不支援background-size的瀏覽器(IE6-8)上用 HiDPI 的背景圖就會很悲劇,雖然可以用 media query 來處理,不過解析度相關的 media query,如果要考慮到以前的瀏覽器,變化有點多,從早期的 device pixel ratio 到 dppx 到現在 resoulution 用的 dpi,還要加上 vendor prefix,自己寫起來有點痛苦,而且原始碼變醜很多,當然,如果有用 PostCSS 就可以用 autoprefixer 解決,不過其實我不太喜歡寫 Media Query,所以都是能少則少(針對 CSS 的 polyfill 也是不太喜歡用),並且有些舊的專案沒有 PostCSS,所以就一直有這個問題,以前的處理方法就是幾種:

  • 真的寫 Media Query
  • <img />標籤模擬背景圖,也很麻煩(也可以用 polyfill,不過個人不想用)
  • 大家一起用標準解析度的圖

這些方法對我來說都算是 workaround 的方式,一直想找個漂亮乾淨一點的解法,直到昨天終於想到了,首先整理一下我的需求:

  • 可以有兩張圖不同解析度的圖
  • 不要用 Media Query
  • 有機會支援 HiDPI 的環境就用 HiDPI 的圖
  • 舊瀏覽器顯示正確

其實這些需求可以簡化成:**不支援background-size的就乖乖看低解析度的圖片吧**,其中隱含著一個現實狀況是,不支援background-size的瀏覽器都只能在不支援 HiDPI 的環境下跑,實做上基本的構想是先寫好一個基本背景的定義,然後再加上一個只有新瀏覽器看的懂得定義,這構想有了很久,卻到昨天才想到怎麼寫,結果如下:

background: #123456 url('the-bg.png') center center;
background: #123456 url('the-bg@2x.png') center center/32px 32px;

用了background可以把全部背景相關屬性寫在一起,而且如果解析失敗就整條失效的特性,以前因為我都會background-size單獨寫(有另外的原因),這種寫法就落入我的盲區,所以過了這麼久才想到,不過這方法不是萬能,還有些限制和使用時機:

  • 有需要支援 IE6-8 才需要,IE9 以上就有支援background-size
  • 不考慮 HiDPI 時其實不需要background-size,例如固定大小區塊的背景、用背景的 icon、有重複排列的背景等
  • background-size: cover;依然需要靠 polyfill 之類的來處理

想出來之後,有開了 IE8 來測試,結果一如預期,這樣應該是 IE6-7 也會正確吧。


Telegram Instant View

Telegram Instant View,

Telegram 前幾天發佈了 4.0,有幾個比較大的功能,包括了Video MessagePayment for Bots還有就是Instant View準備要開放給所有網頁使用了,Instant View 目的和 Facebook 的 Instant Article 以及 Google 的 AMP 一樣,都是為了提升使用者體驗,讓使用者能夠快的看到文章的內容,不過之前沒有開放,所以一直不知道背後的運作原理是怎樣,直到這次 4.0 發佈才得以一窺其原理,和 Instant Article 與 AMP 不一樣,不再是提供另一個新的版本,而是透過一種新的 template 語言來協助 Telegram service 把自己的網頁內容轉譯成 Instant Article 的內容(Instant View page object),不完全算是程式語言,裡面比較像是一些定義,加上用XPath來做文件內容的選取,蠻意外會用 XPath 的,還好我對 XPath 有點經驗,就花了一點時間研究了一下,也把自己 blog 的 tempalte 基本版做出來了:

?exists:  //article/div[@id="comments"]

author:  "othree"
channel: "@othree"

body:     //article
title:    $body//h3[1]

cover: $body/section[@itemprop="articleBody"]/p[1]/a[@itemprop="image"]/img

published_date: $body/header/time[@itemprop="datePublished dateModified"]/@datetime

@remove: //article/header
@remove: //article/footer
@remove: //article/div[@id="comments"]
@remove: //noscript
@remove: //a[has-class("dsq-brlink")]

語法還算蠻好理解的,官方也提供了幾個有完整註解的範例,仔細一看似乎之前其實也只有 medium 是非官方有支援的網站,也因為這個實做方式,對不同的網站就要有不同的 template 來處理,所以官方辦了個競賽,搶先替清單上的網站做出可用的 template 就會有獎金,目前個人網站雖然已經可以在官方的 editor 做 template、驗證並發測試連結,不過還要等 domain 被加進白名單後才會真的啟用,目前這個關卡還沒開放就是。

其實我是比較喜歡這種實做方式的,不用為了增加支援一個新的網路服務就多做一個版本,不會影響原本的網頁原始碼,不會讓<head>越來越肥大,當然缺點就是網站改版,HTML 結構有變化的話就要跟著修改 template,不過我是認為這個實做方法對於網路生態是比較好一些的。


MovableType and CommonMark

Typora

我這邊用的 blog 系統是MovableType不是新聞了,然後也因為用 MovableType 我一直都只能用最初版的 markdown 引擎,沒錯,就是 Daring Fireball 作者 John Gruber 的那一版,這個版本其實已經可以滿足我大部分的需求了,不過當我想要用Typora寫文章的時候,就遇到問題了,Typora 在輸出成 Markdown 文件時,code block 只支援三個 ``` 包起來的Fenced Code Block,而不支援Indented Code Block,剛好初版的 Markdown 格式只有 Indented Code Block,兩者其實要比的話,我是比較喜歡 Indented Code Block 的,比較符合 Markdown 的 sense,不過用 Fenced Code Block 有個優點是可以指定程式碼的語言,也因此才能夠有 syntax highlight 的效果。

總之,因為這個原因,用 Typora 寫技術文章對我來說就很不方便,一直以來都有想解決這個問題,前兩天還去 Typora 發 issue 說希望他們可以支援 Indented Code Block,結果發完沒多久,躺在床上快要睡覺的時候突然想到,CommonMark 這麼多語言有實做,搞不好有 Perl 的啊,結果快速搜尋了一下,還真的有一個perl-commonmark,橋接 Perl 和cmark,也有發佈到 CPAN 上,當下心裡就盤算著,隔天起來要來把它接到 MovableType 去看看。

結果隔天實做起來是沒花我太多時間,雖然對 Perl 不熟,但是可以直接拿初版 Markdown.pl 來修改,原本的 Markdown.pl 這個檔案裡面實際上自己是一個 Markdown 的 Perl Package,同時也可以作為 MovableType 的 plugin script,我只需要把 plugin script 的部分留下,然後把最後做轉換的 function 換掉就好了,當然系統要裝好 cmark 和 Perl 的 CommonMark,cmark 應該很多環境都有了,我在 archlinux 上是直接用 pacman 裝:

pacman -S cmark

然後 CommonMark 是用 CPAN 裝,本來要用 cpanminus 的不知道為何用它會抓不到 package:

sudo CPAN CommonMark

我的 nginx 跑 CGI 時用的 perl 不是系統預設位置的,所以 CPAN 執行檔的路徑我是特別指定給他的,這樣 MovableType 執行的時候才找的到 CommonMark Module,實際上沒花多少功夫,我就把 MovableType 和 CommonMark 串起來了,當下心情真的是非常難以言喻,一來是這個問題其實存在已經許久了,二是我竟然把第一個支援 Markdown 的部落格系統接上 2017 年最新的 CommonMark 實做,雖然現在應該是也幫不到什麼人了。不過沒高興多久,就發現在 UTF-8 字元似乎有些狀況,有中文的文章會爛掉,或是 Dashboard 那頁的文字會變亂碼,後來為了這個問題又弄了好幾個小時,推測問題應該是因為 cmark 那邊回來的字串已經失去編碼的 metadata,所以在做 summary 切文字的時候,就會出現切錯地方的狀況,花了很多時間交叉比對和測試,最後的結果只是用 Perl 的 Encoding 把 cmark 傳回來的字串重新 encode 過而已,其實很簡單。除此之外,我其實還有試著想接看看cmark-gfm,因為它還多支援了 Table,不過幾次測試都不太順利,就沒繼續嘗試下去了。

目前的成果放在 GitHub 上,取名叫MT-CommonMark,附上簡單的安裝說明,暫時是沒打算發去 movabletype.org 那邊。

做好之後 MT-CommonMark 之後,我就開始在部落格上測試程式碼的 syntax highlight了,研究一陣子之後選擇的是prismjs,選擇它的原因很多,不過有兩個是比較主要的:

  • 作者有Lea Verou
  • 支援的 class name 格式剛好和 cmark 輸出的一樣

結果兩者也很順利的搭配起來,中間就沒有再遇到什麼問題了。


➡ 看看其它文章