前端的部分使用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更是一門學問,如果你有更好的想法,歡迎多多指教~
一拖拉庫的參考