前端的部分使用native javascript開發,並且用了一些功能讓網頁更方便操作,當然UI還有待改善,畢竟我不是學美術的XD

還沒看過該作品的同學,可以從這邊前往,一鍵公車,眼尖的朋友可能會發現畫面和當初的版本似乎不同了,畢竟也花了不少時間持續優化,所以跟初版相比算是華麗了不少吧,以下是前端的一些特點
洋洋灑灑也有好幾項,雖然有些只是使用到而已,但是適當的使用,也可以讓你的應用加分,而不是把所有功能都放進來就好,有些是使用別人的成果,雖然有經過一些調整,還是會將出處附在參考,以示尊重。另外因為是設定給手機使用,就不考慮相容老爺機或者桌機的部分,盡量使用新的API
資料取得與展示
使用fetch來取得資料,為了避免使用者習慣性重新整理,使用了快取機制,因為資料端更新頻率約1分鐘,太頻繁沒有幫助反而造成後端壓力,所以1分鐘內直接使用快取。
另外自動1分鐘重新整理一次,省去使用者手動操作,考量到使用者會關螢幕,一段時間再打開來看,接收visibilitychange事件,讓使用者開螢幕時,馬上重新整理,並在讀取時,右下角顯示圖示讓使用者知道
const DATA_CACHE_TIME = 'data_cache_time'; const DATA_CACHE = 'data_cache'; const REFRESH_PERIOD = 60*1000; const DATA_URL = 'https://script.google.com/macros/s/AKfycbxVH6wsfiRs_u-GSLVKVoENVX_SfJ7wUSFEqTB_hDFleA4NzEY/exec?key=7dKw1ubRxz4EtL2EuN5ZWKZPT9GBeL6T'; let loadTimeout; //檢測頁面不可見時,停止重整,在頁面恢復時,馬上重新整理 document.addEventListener('visibilitychange', function(){ clearTimeout(loadTimeout); if(document['hidden']){ }else{ loadData() } }, false); function loadData(){ //檢查和上次快取的時間間隔,低於間隔則直接使用快取,減少要求次數 var now = new Date(); //if less than 60s use cache if(localStorage[DATA_CACHE] && localStorage[DATA_CACHE_TIME] && (now.getTime() - parseInt(localStorage[DATA_CACHE_TIME], 10) < 60000)){ parseData(JSON.parse(localStorage[DATA_CACHE])); return; } document.querySelector('.loading').classList.toggle('hidden'); fetch(DATA_URL,{ method:'GET', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' } }) .then(res => { return res.json(); }).then(result => { parseData(result); localStorage[DATA_CACHE] = JSON.stringify(result); localStorage[DATA_CACHE_TIME] = now.getTime(); document.querySelector('.loading').classList.toggle('hidden'); loadTimeout = setTimeout(function(){loadData()}, REFRESH_PERIOD); }) .catch(function(err) { alert(err); document.querySelector('.loading').classList.toggle('hidden'); loadTimeout = setTimeout(function(){loadData()}, REFRESH_PERIOD); }); } function parseData(result){ let estimate = []; for(let i=0;i<result.length;i++){ let text = ''; if(result[i]['PlateNumb'] == '-1' || result[i]['PlateNumb'] == ''){ text = getStatus(result[i]['StopStatus'], result[i]['NextBusTime']); }else{ if(result[i]['EstimateTime'] > 60){ text = Math.ceil(result[i]['EstimateTime']/60)+'分後'; }else{ text = '1分鐘內'; } } estimate.push({ route: result[i]['SubRouteName']['Zh_tw'], no: result[i]['PlateNumb'], text: text, stop: result[i]['StopName']['Zh_tw'] }); } showResult(estimate); } function showResult(estimate){ let content = ''; for(let i=0;i<estimate.length;i++){ content += '<div class="info_row">'+ '<div class="info_route">'+estimate[i]['route'] + '</div>'+ '<div class="info_no">'+estimate[i]['no']+'</div>'+ '<div class="info_text">' + estimate[i]['text']+'</div>'+ '<div class="info_stop">' + estimate[i]['stop']+'</div>'+ '</div>'; } document.getElementById('estimate_info').innerHTML = content; } function getStatus(status, next_time){ if(status === 1){ if(next_time == undefined){ return '尚未發車';} var time = new Date(next_time); var hour = time.getHours() < 10 ? '0'+time.getHours() : time.getHours(); var minute = time.getMinutes() < 10 ? '0'+time.getMinutes() : time.getMinutes(); return hour + ':' + minute + '發車'; }else if(status === 2){ return '交管不停靠'; }else if(status === 3){ return '末班車已過'; }else if(status === 4){ return '今日未營運'; } }
CSS部分,簡單定義了每一欄的寬度,讀取圖示等
.info_row{ padding: .5em 0; } .info_route{ display: inline-block; width: 15%; } .info_no{ display: inline-block; width: 25%; } .info_text{ display: inline-block; width: 30%; } .info_stop{ display: inline-block; width: 30%; } .svg-icon { fill: #fff; width: .8em; height: .8em; margin: .2em; } .loading{ width: 1.5em; height: 1.5em; position: absolute; bottom: .3em; right: .3em; fill: black; animation: spin 1.4s infinite linear; } .hidden { display: none; } @keyframes spin{ 0% { transform: rotate(0deg); } 100% { transform: rotate(-360deg); } }
加到桌面,如APP一般

Add to home screen,這個功能可以將捷徑建立在手機桌面,執行時可隱藏網址列,獲得類似原生APP的操作體驗,需要建立manifest.json,定義捷徑的外觀和作用,還有記得製作圖示檔案

{ "short_name": "一鍵公車", "name": "一鍵公車", "icons": [ { "src": "icon-192.png", "type": "image/png", "sizes": "192x192" }, { "src": "icon-512.png", "type": "image/png", "sizes": "512x512" } ], "start_url": "./index.html?source=app", "scope": "./", "background_color": "#3367D6", "display": "standalone", "theme_color": "#3367D6" }
SVG Sprite
考量到SVG的支援度已相當普及,用起來也比CSS Sprite簡單,因此把圖示的部分全部集中到一個SVG檔案中,使用方法如下,只要指定symbol的id就可以囉,是不是很方便~
<svg class="svg-icon loading hidden" xmlns="http://www.w3.org/2000/svg"> <use xlink:href="icon.svg#loading"></use> </svg>
SVG的格式如下,svg節點中可放置多個symbol,此格式的圖可以由CSS來定義大小和顏色,非常靈活
<svg xmlns="http://www.w3.org/2000/svg"> <symbol id="loading" viewBox="0 0 24 24"><path d="M23 12c0 1.042-.154 2.045-.425 3h-2.101c.335-.94.526-1.947.526-3 0-4.962-4.037-9-9-9-1.706 0-3.296.484-4.655 1.314l1.858 2.686h-6.994l2.152-7 1.849 2.673c1.684-1.049 3.659-1.673 5.79-1.673 6.074 0 11 4.925 11 11zm-6.354 7.692c-1.357.826-2.944 1.308-4.646 1.308-4.962 0-9-4.038-9-9 0-1.053.191-2.06.525-3h-2.1c-.271.955-.425 1.958-.425 3 0 6.075 4.925 11 11 11 2.127 0 4.099-.621 5.78-1.667l1.853 2.667 2.152-6.989h-6.994l1.855 2.681z"/></symbol> </svg>
可單手操作的選單
多數手機的APP和網頁選單都是從上面操作,不是左上就是右上,問題是這樣很不利於單手使用,考慮到這點,我花了一些時間找是否有單手可操作的選單,終於讓我找到了,居然連文章都沒有,只有一個範例,根據我的需求調整了一些屬性,單位也都改成em
不得不佩服作者,因為他把大部分的參數都使用CSS Variables,很輕易地可以調整,HTML的部分也不會太複雜,還保留了不少彈性,篇幅太大,詳細的代碼還是要參考github原始碼~
:root{ --colorWhite: #fff; --colorMain: #4557bb; --rLinkTextColor: var(--colorWhite); --rlinkActiveColor: #d0e6ff; --menuCircleSize: 6em; --menuCircleBgColor: var(--colorMain); --menuHamburgerWidth: 1.68em; --menuHamburgerHeight: 1.2em; --menuHamburgerBgColor: var(--colorWhite); }
<div class="menu"> <nav class="menu__nav"> <ul class="r-list menu__list"> <li class="menu__group"> <a href="https://ptx.transportdata.tw/PTX" class="r-link menu__link"> <svg class="svg-icon" xmlns="http://www.w3.org/2000/svg" data-svg-id="bus" onload="assignSvgUrl(this);"> <use xlink:href=""></use> </svg>交通部PTX </a> </li> <li class="menu__group"> <a href="https://woodloch.blog" class="r-link menu__link"> <svg class="svg-icon" xmlns="http://www.w3.org/2000/svg" data-svg-id="user" onload="assignSvgUrl(this);"> <use xlink:href=""></use> </svg>木澤 </a> </li> <li class="menu__group"> <a href="https://woodlochhome.wordpress.com/2020/05/30/oneclickbus/" class="r-link menu__link"> <svg class="svg-icon" xmlns="http://www.w3.org/2000/svg" data-svg-id="link" onload="assignSvgUrl(this);"> <use xlink:href=""></use> </svg>部落格 </a> </li> <li class="menu__group"> <a href="mailto:admin@woodloch.blog" class="r-link menu__link"> <svg class="svg-icon" xmlns="http://www.w3.org/2000/svg" data-svg-id="mail" onload="assignSvgUrl(this);"> <use xlink:href=""></use> </svg>聯繫我 </a> </li> <li class="menu__group"> <a href="javascript:void(0);" onclick="shareUrl();" class="r-link menu__link"> <svg class="svg-icon" xmlns="http://www.w3.org/2000/svg" data-svg-id="share" onload="assignSvgUrl(this);"> <use xlink:href=""></use> </svg>分享給朋友 </a> </li> </ul> </nav> <div class="menu__toggle"> <button class="r-button menu__hamburger"> <span class="m-hamburger"> <span class="m-hamburger__label">Open menu</span> </span> </button> </div> </div>
簡潔的Toast訊息
考量訊息提示時,不希望使用者一直去點確定,那就學原生APP的Toast方式好了,許多套件都很花俏,但容量不小,後來找到一個簡易輕量的範例,套用了以後發現了一個bug,因為它使用js的setTimeout來隱藏,要配合CSS動畫的時間,有時候兩者時間因為延遲不一致,會看到不正常的閃爍。這部份好解決,不要使用animation,只用transition就可以了
#toast { min-width: 250px; background-color: #333; color: #fff; text-align: center; border-radius: 2px; padding: 16px; position: fixed; z-index: 1000; /* need to higher than .menu in nav.css */ left: 50%; /* Center the toast */ transform: translatex(-50%); opacity: 0; bottom: calc(var(--toastBottom) * -1 ); transition: bottom .5s, opacity .5s;/* move with animation */ } #toast.show { opacity: 1; bottom: var(--toastBottom); transition: bottom .5s, opacity .5s;/* move with animation */ } :root{ --toastBottom: 3.2em; }
<div id="toast">Some text some message..</div>
function showToast(text) { var x = document.getElementById("toast"); x.innerText = text; x.classList.toggle('show'); setTimeout(function(){ x.classList.toggle('show'); }, 3000); }
分享功能
Web應用畢竟還是靠網址傳遞,因此方便分享,所以使用了Share API來呼叫原生的分享功能,假設不支援,也可以把網址複製到剪貼簿中,再到其他的社群貼上
//for share API const shareData = { url: window.location.href }; function shareUrl(){ //確認支援度 if (navigator.share) { try { navigator.share(shareData); } catch (err) { const { name, message } = err; if (name === 'AbortError') { console.log('您已取消分享此訊息'); } else { console.log('發生錯誤', err); } } } else { //不支援則將網址複製到剪貼簿 copy(window.location.href); showToast('網址複製完成'); } //隱藏選單 document.querySelector('.menu__hamburger').dispatchEvent(new Event('click')); } function copy(text){ const input = document.createElement('input'); input.value = text; document.body.appendChild(input); input.select(); if (document.execCommand('Copy')) { document.execCommand('Copy'); } document.body.removeChild(input); }
避免外部資源被快取而不更新
在初版開發完畢後,後續改版一直有遇到一個困擾,就是外部引用的資源會因為瀏覽器快取,顯示舊版本的內容,像是SVG明明已經新增圖了,可是快取的圖沒有,就空一塊,又或者是選單的nav.css已經修改樣式,但同樣因為快取問題,顯示舊的樣式。
通常處理這個問題,就是從引用的url下手,尾部加上版號之類的,只要url不同,瀏覽器就會重新取得最新版,問題是CSS和SVG都是hardcode上去的,這是前端,沒有後端幫我加字串,因此不得已的情況下,只好採用動態載入,採用半自動的方式,定義好一個值,每次有更新就跟著改變,配合動態載入外部資源url變更達到更新的效果
const LAST_MODIFIED_DATE = '2020060121'; //prevent css cache not update document.getElementsByTagName("head")[0].insertAdjacentHTML( 'afterBegin', '<link rel="stylesheet" href="nav.css?v='+LAST_MODIFIED_DATE+'" />');
SVG則是定義在onload事件,動態改變href值
function assignSvgUrl(svg){ var use = svg.getElementsByTagName("use")[0]; use.href.baseVal = 'icon.svg?v='+LAST_MODIFIED_DATE+'#' + svg.dataset.svgId; }
<svg class="svg-icon" xmlns="http://www.w3.org/2000/svg" data-svg-id="user" onload="assignSvgUrl(this);"> <use xlink:href=""></use> </svg>
感覺篇幅有點長,可以看到這裡,耐心令人佩服,Web前端博大精深,UI、UX更是一門學問,如果你有更好的想法,歡迎多多指教~
一拖拉庫的參考