下拉重新整理

2026 鐵人賽的 SMART on FHIR 範例程式:每一天都是一份點下去就跑得起來的專案

2026 iThome 鐵人賽 SMART on FHIR 系列的範例程式已整理成 GitHub Pages 靜態站,每一天一個資料夾、點下去就在瀏覽器裡跑起來。用瀏覽器原生 JavaScript 寫成,沒有打包工具、沒有框架、不需要 Node.js。這頁記錄它的組成、每一天做什麼、實作時撞到的坑(CORS 讀不到 Location、401 回純文字、分頁只能照抄 next、拿錯 token 回 200 加 SUBSETTED),以及公開 sandbox 的安全界線。

| 1,912 字 | 5 分鐘閱讀 | 21 次閱讀 |
字級
行距

我把今年鐵人賽系列的程式碼整理成一個可以直接在瀏覽器裡跑的站:

https://losehrt.github.io/ithome-2026-smart-app/

原始碼在 GitHub,MIT 授權。

2026-08-22-ithome-2026-smart-app-day17.png

上面是 day17 跑起來的樣子。走完 SMART 授權,讀出病人基本資料、生命徵象趨勢,還有病況與用藥兩張表。資料來自公開測試 sandbox 的合成病人。

為什麼要換一種語言再寫一次

這些程式碼重走了一次火線超人的路。火線超人是已經在跑的東西,用 Rails 寫的,接的是真實的醫院 FHIR 伺服器。這裡把同一套協定用最多人會的技術再實作一次。

換語言不是因為 Rails 做不到。是想證明一件事:要參與醫療資料互通,不必先成為醫療資訊工程師。你會 Web、懂 HTTP、寫得動 JavaScript,就足以做出一個符合國際標準的 SMART on FHIR app。

這個領域長期被當成醫院資訊室的專業。門檻其實沒有那麼高,缺的是有人把路走一遍給你看。

沒有打包工具,沒有框架,不需要 Node.js

全部是瀏覽器原生 JavaScript 加 ES modules。第三方的東西只有三個,而且都下載進版控放在 vendor/,clone 下來不用網路就能跑,執行期也不連 CDN。

檔案 版本 用途 授權
fhir-client.pure.min.js 2.6.3 day12 之後改用它處理授權 Apache-2.0
chart.umd.js 4.5.1 day16 開始畫趨勢圖,208518 bytes MIT
tailwind-browser.js 4.3.3 day17 開始排版,282289 bytes MIT

Tailwind 那支是跑在瀏覽器裡的 JIT 編譯器,掃 DOM 上的 class 即時產生 CSS,所以不需要 npm 也不需要建置步驟。官方講得很明白,這個版本只適合開發,不要用在正式環境,因為每個使用者的瀏覽器都要重跑一次編譯。正式做法還是裝 npm 加一個建置步驟,事先產出只含用到的 class 的靜態 CSS。

免建置是有代價的,代價就寫在上面那張表的 bytes 欄位。

目前有哪些

每一天一個資料夾,各自是一份完整可跑的專案。讀到哪一天就進那個資料夾,不用回頭拼湊前幾天的檔案。每個資料夾也各自有一個同名的 git tag。

資料夾 內容
day04-sandbox-setup 連上公開 FHIR server,畫面顯示連線成功與拿到的病人參照。不需授權
day06-smart-discovery .well-known/smart-configuration 問出授權端點與 token 端點。不需授權
day09-first-authorization 自己寫 PKCE 與授權碼流程,走完一趟 standalone 授權
day12-launch-context 換用 fhirclient,同一件事從一百多行變成三十行不到,畫面從 patient id 變成病人姓名與生日
day14-token-lifecycle 多一顆按鈕手動換 token,再把用過的那張 refresh token 送一次,看伺服器收不收
day15-first-smart-app 把 Patient 整理成姓名、性別、生日、病歷號四個欄位。取姓名走四層 fallback,因為 Patient 上幾乎所有欄位都是選填
day16-clinical-data 血壓與體重從 Observation 挖出來畫成雙 y 軸趨勢圖
day17-clinical-data 病況與用藥列成兩張表
day18-write-back 把自己量的血壓存回伺服器。會寫入
day19-error-handling 六顆按鈕各觸發一種失敗,看伺服器實際回什麼。會寫入
day20-search-and-write 一路照抄 next 跟完 10 頁 94 筆。會寫入
day22-multi-server 兩家醫院各自一組 clientIdscope。會寫入

系列還在進行中,後面的資料夾會隨文章發布陸續加進來。

幾件實際打下去才知道的事

這些是寫的過程中撞到的,不是規格書上讀得到的。

同一種資源可以有兩種結構。血壓的值裝在 component 裡,體重直接掛在資源上。都是 Observation,取值的程式要分開寫。

CodeableConcept 取顯示文字要走三層優先序。textdisplaycode。狀態欄位另走一個只取 code 的函式,再自己對照成中文。

POST 回 201,但 LocationETag 在瀏覽器裡讀不到。CORS 擋的。新資源的 id 只能從回應 body 取。

401 回的是純文字,不是 OperationOutcome所以要先看 Content-Type 再決定怎麼解析,無條件呼叫 response.json() 會在那裡丟例外。

分頁不要自己算 offset。這台 sandbox 的 next 換成了一串 _getpages 的暫存 id,自己算 offset 行不通,只能照抄它給的 next。另外 _include 帶回來的資源要看 entry.search.mode 才分得出哪幾筆是你查的。

拿錯 token 打另一家不會回 403。day22 兩家醫院那邊,拿 B 的 token 去打 A,它回 200 加一份標著 SUBSETTED 的殘缺資料。這個行為比直接被拒絕危險得多,因為程式不會炸,只會安靜地少東西。

scope 設錯在這台 sandbox 看不出來。它在資源端不檢查 scope,換到真實 EHR 才會變成 403。

有幾天沒有資料夾

不是漏掉,是那幾篇本來就沒有可跑的專案。

day01 到 day03 與 day05 是 FHIR 資源與 HTTP 請求的範例片段,直接讀文章。day07 全程在瀏覽器網址列上操作,一個檔案都不用動。day08 新增的 pkce.js 跑起來畫面跟 day06 一樣,那篇的驗證是在 console 裡做的。

day10 與 day11 的跟著做都是改 auth.jsSCOPE 那個常數再重跑,看同意畫面多幾行、看 token response 少哪些欄位。開 day09-first-authorization/SCOPE 就能重現。

day13 比較特別。那篇的第四步是換成醫護身分再跑一次,收穫是兩種身分之下 fhirUser 指向的資源型別不同。任何一份靜態資料夾都只能凍結其中一次,所以乾脆不做。

連著跑好幾天要清 session

授權結果存在 sessionStorage,同一個網域底下共用,所以後面那天會直接沿用前一天那張 token。而每一天要的 scope 並不相同。

最容易看出來的是 day14:沿用 day12 的 token 就沒有 refresh token,那一天的手動換 token 會換不成。本機把每個資料夾都跑在同一個 port 也是一樣的情形。

換一天之前先重新整理並清掉分頁的 session。

不必改任何一行就能跑

檔案裡填好的 sandbox 設定是一串無狀態的編碼,整組設定就編在那串字裡,不綁任何人的 session,所以誰拿去用都成立。解碼出來是 Patient Standalone Launch 加自動選病人。

想換成自己的病人或情境,到 SMART Health IT Launcher 產生一組,把 Server's FHIR Base URL 那一整串貼回 app.jsFHIR_BASE_URL 即可。

本機跑就是起一個靜態伺服器:

cd day06-smart-discovery
python3 -m http.server 5173

不能用 file:// 直接開 index.htmltype="module" 的檔案會被 CORS 擋掉。

資料都是假的,但寫進去是真的

範例連的是 SMART Health IT 的公開 sandbox,病人由 Synthea 合成,不是去識別化的真人資料。

那是公開的測試環境,資料是共用的,任何人寫進去的東西大家都看得到。

絕對不要放入真實病人資料,一筆都不要,包括拿真人的姓名或生日去測試。

從 day18 開始,範例會真的寫資料進去。寫進去的東西會留在上面不會自動清掉,測完想清就自己送一個 DELETE /Observation/{id},回 200 就是刪掉了。

相關

tech 公開 smart-on-fhir fhir ithome鐵人賽 鐵人賽 2026鐵人賽 範例程式 教學範例 vanilla-javascript 原生javascript es-modules 免建置 no-build github-pages 靜態網站 fhirclient pkce oauth2 launch-context sessionstorage refresh-token offline-access observation condition medicationrequest codeableconcept chart-js tailwindcss cors operationoutcome 分頁 _getpages subsetted multi-server smart-health-it synthea sandbox 合成資料 火線超人 mit授權 vendor 開源 一個人做得動