背景
在日語學習初期階段,我發現日語五十音的記憶並不是很容易的,片假名的記憶尤其令人費神。這時我想如果有一個應用可以充分利用碎片時間,在午休或地鐵上隨時可以練習五十音該多好。於是搜尋 App Store
,確實有很多五十音學習的小軟體,但是商店的軟體不是含有內購、夾帶廣告、就是動輒 40M
以上,沒找到一個自己滿意的應用。於是打算自己寫一個,主要介紹自己在開發設計該應用過程中的一些收穫。
實現
實現效果如下,該應用主要分為三個頁面:
- 首頁:包括選單選項(平假名練習、片假名練習、混合練習)、深色模式切換按鈕。
- 答題頁:包括剩餘機會和分數顯示區、中間出題區、底部答題按鈕。
- 結果頁:結果分數顯示和返回首頁按鈕。
答題邏輯規則是從給出的 4
個答案按鈕中選出題目展示區的那個單詞對應正確的那個選項,應用根據點選給出錯對反饋並進行記分,錯誤 10
次後遊戲結束,載入結果頁。遊戲邏輯實現不是本文的主要內容,因此後面不再贅述。本文後續主要內容是此次小遊戲開發流程涉及到的前端知識的介紹。
深色模式 ⚪⚫
隨著 Windows 10
、 MacOs
、 Android
等系統陸續推出深色模式,瀏覽器也開始支援檢測系統主題色配置,越來越多的網頁應用都配置了深色模式切換功能。為了優化 50音小遊戲
的視覺體驗,我也配置了深色樣式,實現效果如下:
CSS
媒體查詢判斷深色模式
prefers-color-scheme
媒體特性用於檢測使用者是否有將系統的主題色設定為亮色或者暗色。使用語法如下所示:
@media (prefers-color-scheme: value) {}
其中 value
有以下 3
種值,其中:
light
:表示使用者系統支援深色模式,並且已設定為淺色主題(預設值)。dark
:表示使用者系統支援深色模式,並且已設定為深色主題。no-preference
:表示使用者系統不支援深色模式或無法得知是否設定為深色模式(已廢棄)。
若結果為
no-preference
,無法通過此媒體特性獲知宿主系統是否支援設定主題色,或者使用者是否主動將其設定為無偏好。出於隱私保護等方面的考慮,使用者或使用者代理也可能在一些情況下在瀏覽器內部將其設定為no-preference
。
下面例子中,當系統主題色為深色時 .demo
元素的背景色為 #FFFFFF
;當系統主題色為淺色時,.demo
元素的背景色為 #000000
。
@media (prefers-color-scheme: dark) {
.demo { background: #FFFFFF; }
}
@media (prefers-color-scheme: light) {
.demo { background: #000000; }
}
JavaScript
判斷深色模式
window.matchMedia()
方法返回一個新的 MediaQueryList
物件,表示指定的媒體查詢 (en-US)字串
解析後的結果。返回的 MediaQueryList
可被用於判定 Document
是否匹配媒體查詢,或者監控一個 document
來判定它匹配了或者停止匹配了此媒體查詢。其中 MediaQueryList
物件具有屬性 matches
和 media
,方法 addListener
和 removeListener
。
使用 matchMedia
作為判斷媒介,也可以識別系統是否支援主題色:
if (window.matchMedia('(prefers-color-scheme)').media === 'not all') {
// 瀏覽器不支援主題色設定
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches){
// 深色模式
} else {
// 淺色模式
}
另外還可以動態監聽系統深色模式的狀態,根據系統深色模式的切換做出實時響應:
window.matchMedia('(prefers-color-scheme: dark)').addListener(e => {
if (e.matches) {
// 開啟深色模式
} else {
// 關閉深色模式
}
});
或者單獨檢測深色或淺色模式:
const listeners = {
dark: (mediaQueryList) => {
if (mediaQueryList.matches) {
// 開啟深色模式
}
},
light: (mediaQueryList) => {
if (mediaQueryList.matches) {
// 開啟淺色模式
}
}
};
window.matchMedia('(prefers-color-scheme: dark)').addListener(listeners.dark);
window.matchMedia('(prefers-color-scheme: light)').addListener(listeners.light);
在50音小遊戲中,就是使用 JavaScript
檢測系統是否開啟深色模式,動態新增 css
類名來自動載入深色模式,同時也提供深淺色切換按鈕,可以手動切換主題。
HTML
元素中判斷深色模式
頁面使用圖片元素時,可以直接在 HTML
中判斷系統是否開啟深色模式。如:
<picture>
<source srcset="dark.png" media="(prefers-color-scheme: dark)">
<img src="light.png">
</picture>
picture
元素允許我們在不同的裝置上顯示不同的圖片,一般用於響應式。HTML5
引入了 <picture>
元素,該元素可以讓圖片資源的調整更加靈活。<picture>
元素零或多個 <source>
元素和一個 <img>
元素,每個 <source>
元素匹配不同的裝置並引用不同的影像源,如果沒有匹配的,就選擇 <img>
元素的 src
屬性中的 url
。
注意:
<img>
元素是放在最後一個<picture>
元素之後,如果瀏覽器不支援該屬性則顯示<img>
元素的的圖片。
離線快取
為了能夠像原生應用一樣可以在桌面生成快捷方式快速訪問,隨時隨地離線使用,50音小遊戲
使用了離線快取技術,它是一個 PWA應用
。下面內容是 PWA離線應用
實現技術的簡要描述。
PWA (progressing web app)
,漸進式網頁應用程式,是下一代WEB應用模型
。一個PWA
應用首先是一個網頁, 並藉助於App Manifest
和Service Worker
來實現安裝和離線等功能。
特點:
- 漸進式:適用於選用任何瀏覽器的所有使用者,因為它是以漸進式增強作為核心宗旨來開發的。
- 自適應:適合任何機型:桌面裝置、移動裝置、平板電腦或任何未來裝置。
- 連線無關性:能夠藉助於服務工作執行緒在離線或低質量網路狀況下工作。
- 離線推送:使用推送訊息通知,能夠讓我們的應用像
Native App
一樣,提升使用者體驗。 - 及時更新:在服務工作執行緒更新程式的作用下時刻保持最新狀態。
- 安全性:通過
HTTPS
提供,以防止窺探和確保內容不被篡改。
配置頁面引數
在專案根目錄新增檔案 manifest.webmanifest
或 manifest.json
檔案,並在檔案內寫入如下配置資訊,本例中 50音小遊戲
的頁面引數資訊配置如下:
// manifest.webmainifest
{
"name": "かなゲーム",
"short_name": "かなゲーム",
"start_url": "index.html",
"display": "standalone",
"background_color": "#fff",
"description": "かなゲーム",
"icons": [
{
"src": "assets/images/icon-64x64.png",
"sizes": "64x64",
"type": "image/png"
},
{
"src": "assets/images/icon-256x256.png",
"sizes": "256x256",
"type": "image/png"
}
]
}
引數說明:
name
:Web App
的名稱,也是儲存到桌面上時應用圖示的名稱。short_name
:name
過長時,將會使用short_name
代替name
顯示,是Web App
的簡稱。start_url
:指定了使用者開啟該Web App
時載入URL
。URL
會相對於manifest
檔案所在路徑。display
:指定了應用的顯示模式,它有四個值可以選擇:fullscreen
:全屏顯示,會盡可能將所有的顯示區域都佔滿。standalone
:瀏覽器相關UI
(如導航欄、工具欄等)將被隱藏,看起來更像一個Native App
。minimal-ui
:顯示形式與standalone
類似,瀏覽器相關UI
會最小化為一個按鈕,不同瀏覽器在實現上略有不同。browser
:一般來說,會和正常使用瀏覽器開啟樣式一致。- 需要說明的是,當一些系統的瀏覽器不支援
fullscreen
時將會顯示成standalone
效果,當不支援standalone
時,將會顯示成minimal-ui
的效果,以此類推。
description
:應用描述。icons
:指定了應用的桌面圖示和啟動頁影像,用陣列表示:- sizes:圖示大小。通過指定大小,系統會選取最合適的圖示展示在相應位置上。
- src:圖示路徑。相對路徑是相對於
manifest
檔案,也可以使用絕對路徑。 - type:圖示圖片型別。 瀏覽器會從
icons
中選擇最接近128dp(px = dp * (dpi / 160))
的圖片作為啟動畫面影像。
background_color
:指定啟動畫面的背景顏色,採用相同顏色可以實現從啟動畫面到首頁的平穩過渡,也可以用來改善頁面資源正在載入時的使用者體驗。theme_color
:指定了Web App
的主題顏色。可以通過該屬性來控制瀏覽器UI
的顏色。比如狀態列、內容頁中狀態列、位址列的顏色。
配置資訊自動生成工具:https://tomitm.github.io/appmanifest/
配置 HTML
檔案
在 index.html
中引入 manifest
配置檔案,並在 head
中新增以下配置資訊以相容 iOS系統
<meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no,viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="かなゲーム">
<link rel="stylesheet" type="text/css" href="./assets/css/main.css">
<link rel="stylesheet" type="text/css" href="./assets/css/dark.css">
<link rel="stylesheet" type="text/css" href="./assets/css/petals.css">
<link rel="shortcut icon" href="./assets/images/icon-256x256.png">
<link rel="apple-touch-icon" href="./assets/images/icon-256x256.png"/>
<link rel="apple-touch-icon-precomposed" href="./assets/images/icon-256x256.png">
<link rel="Bookmark" href="./assets/images/icon-256x256.png" />
<link rel="manifest" href="./manifest.webmanifest">
<title>かなゲーム</title>
apple-touch-icon
: 指定應用圖示,類似與manifest.json
檔案的icons
配置,也是支援sizes
屬性,來供不同場景的選擇。apple-mobile-web-app-capable
:類似於manifest.json
中的display
的功能,通過設定為yes
可以進入standalone
模式。apple-mobile-web-app-title
:指定應用的名稱。apple-mobile-web-app-status-bar-style
:指定iOS移動裝置的狀態列status bar
的樣式,有Default
,Black
,Black-translucent
可以設定。
註冊使用 Service Worker
在 index.html
中新增如下程式碼進行server-worker註冊:
window.addEventListener('load', () => {
registerSW();
});
async function registerSW() {
if ('serviceWorker' in navigator) {
try {
await navigator.serviceWorker.register('./sw.js');
} catch (e) {
console.log(`SW registration failed`);
}
}
}
使用 serviceWorkerContainer.register()
進行 Service worker
註冊,同時新增 try...catch...
容錯判斷,以保證在不支援 Service worker
的情況下正常執行。另外需要注意的是隻有在 https
下,navigator
裡才會有 serviceWorker
物件。
Service workers
本質上充當Web
應用程式、瀏覽器與網路(可用時)之間的代理伺服器。旨在建立有效的離線體驗,它會攔截網路請求並根據網路是否可用採取來適當的動作、更新來自伺服器的的資源。它還提供入口以推送通知和訪問後臺同步API
。瞭解更多Service workder
知識可以訪問文章末尾連結?
。
在根目錄新增 sw.js
,定義快取資訊和方法
// 定義快取的key值
const cacheName = 'kana-v1';
// 定義需要快取的檔案
const staticAssets = [
'./',
'./index.html',
'./assets/css/main.css',
'./assets/js/main.js',
'./assets/images/bg.png'
// ...
];
// 監聽install事件,安裝完成後,進行檔案快取
self.addEventListener('install', async e => {
// 找到key對應的快取並且獲得可以操作的cache物件
const cache = await caches.open(cacheName);
// 將需要快取的檔案加進來
await cache.addAll(staticAssets);
return self.skipWaiting();
});
// 監聽activate事件來更新快取資料
self.addEventListener('activate', e => {
// 保證第一次載入fetch觸發
self.clients.claim();
});
// 監聽fetch事件來使用快取資料:
self.addEventListener('fetch', async e => {
const req = e.request;
const url = new URL(req.url);
if (url.origin === location.origin) {
e.respondWith(cacheFirst(req));
} else {
e.respondWith(networkAndCache(req));
}
});
async function cacheFirst(req) {
// 判斷當前請求是否需要快取
const cache = await caches.open(cacheName);
const cached = await cache.match(req);
// 有快取就用快取,沒有就從新發請求獲取
return cached || fetch(req);
}
async function networkAndCache(req) {
const cache = await caches.open(cacheName);
try {
// 快取報錯還直接從新發請求獲取
const fresh = await fetch(req);
await cache.put(req, fresh.clone());
return fresh;
} catch (e) {
const cached = await cache.match(req);
return cached;
}
}
在 sw.js
中採用的標準的 web worker
的程式設計方式,由於執行在另一個全域性上下文中 (self)
,這個全域性上下文不同於 window
,所以採用 self.addEventListener()
。
Cache API
是 Service Worker
提供用來操作快取的的介面,這些介面基於 Promise
實現,包括 Cache
和 Cache Storage
,Cache
直接和請求打交道,為快取的 Request / Response
物件對提供儲存機制,CacheStorage
表示 Cache
物件的儲存例項,我們可以直接使用全域性的 caches
屬性訪問 Cache API
。
** Cache
相關 API
說明:
Cache.match(request, options)
:返回一個Promise
物件,resolve
的結果是跟Cache
物件匹配的第一個已經快取的請求。Cache.matchAll(request, options)
:返回一個Promise
物件,resolve
的結果是跟Cache
物件匹配的所有請求組成的陣列。Cache.addAll(requests)
:接收一個URL
陣列,檢索並把返回的response
物件新增到給定的Cache
物件。Cache.delete(request, options)
:搜尋key
值為request
的Cache
條目。如果找到,則刪除該Cache
條目,並且返回一個resolve
為true
的Promise
物件;如果未找到,則返回一個resolve
為false
的Promise
物件。Cache.keys(request, options)
:返回一個Promise
物件,resolve
的結果是Cache
物件key
值組成的陣列。
注:使用
request.clone()
和response.clone()
是因為request
和response
是一個流,只能消耗一次。快取時已經消耗一次,再發起HTTP
請求還要消耗一次,此時使用clone
方法克隆請求。
至此,當已安裝的 Service Worker
頁面被開啟時,便會觸發 Service Worker
指令碼更新。當上次指令碼更新寫入 Service Worker
資料庫的時間戳與本次更新超過 24小時
,便會觸發 Service Worker
指令碼更新。當 sw.js
檔案改變時,便會觸發 Service Worker
指令碼更新。更新流程與安裝類似,只是在更新安裝成功後不會立即進入 active
狀態,更新後的 Service Worker
會和原始的 Service Worker
共同存在,並執行它的 install
,一旦新 Service Worker
安裝成功,它會進入 wait
狀態,需要等待舊版本的 Service Worker
進/執行緒終止。
更多
Server Worker
進階知識可以檢視文章末尾連結?
實現效果:
PC端 ?️:Windows
上,在瀏覽器中初次開啟應用後會有安裝提示,點選安裝圖示之後進行安裝,桌面和開始選單中會生成應用快捷方式,點選快捷方式就可以開啟應用。
Mac ?
: 上面 chromiumn核心
的瀏覽器(chrome
、opera
、新版edge
)也是類似的。安裝之後會在 launchpad
中生成快捷方式。
移動端 ?:iPhone
。瀏覽器中選擇儲存到桌面,就可生成桌面圖示,點選圖示開啟離線應用。
櫻花飄落動畫 ?
為增強視覺效果和趣味性,於是在頁面增加了櫻花 ?
飄落的效果。飄落效果動畫主要使用到 Element.animate()
方法。
Element
的 animate()
方法是一個建立新 Animation
的便捷方法,將它應用於元素,然後執行動畫。它將返回一個新建的 Animation
物件例項。一個元素上可以應用多個動畫效果。你可以通過呼叫此函式獲得這些動畫效果的一個列表: Element.getAnimations()
。
基本語法:
var animation = element.animate(keyframes, options);
引數:
keyframes
:關鍵幀。一個物件,代表關鍵幀的一個集合。options
:可選項代表動畫持續時間的整形數字 (以毫秒為單位), 或者一個包含一個或多個時間屬性的物件:id
: 可選,在animate()
裡可作為唯一標識的屬性: 一個用來引用動畫的字串(DOMString
)delay
:可選,開始時間的延遲毫秒數,預設值為0
。direction
:可選,動畫的運動方向。向前執行normal
、向後執行reverse
、每次迭代後切換方向alternate
,向後執行並在每次迭代後切換方向alternate-reverse
。預設為normal
。duration
:可選,動畫完成每次迭代的毫秒數,預設值為0
。easing
:可選,動畫隨時間變化的頻率。接受預設的值包括linear
、ease
、ease-in
、ease-out
、ease-in-out
及一個自定義值cubic-bezier
, 如cubic-bezier(0.42, 0, 0.58, 1)
。預設值為linear
。endDelay
:可選,一個動畫結束後的延遲,預設值為0
。fill
:可選,定義動畫效果對元素的影響時機,backwards
動畫開始前影響到元素上、forwards
動畫完成後影響到元素上、both
兩者兼具。預設值為none
。iterationStart
:可選,描述動畫應該在迭代中的哪個點開始。例如,0.5
表示在第一次迭代中途開始,並且設定此值後,具有2
次迭代的動畫將在第三次迭代中途結束。預設為0.0
。iterations
:可選,動畫應該重複的次數。預設為1
,也可以取infinity
的值,使其在元素存在時重複。
以下程式碼為本例中的具體實現,HTML
中有若干個 .petal
元素,然後 JavaScript
中獲取到所有 .petal
元素新增隨機動畫,css中新增兩種旋轉和變形兩種動畫,實現櫻花花瓣飄落的效果。
<div id="petals_container">
<div class="petal"></div>
<!-- ... -->
<div class="petal"></div>
</div>
var petalPlayers = [];
function animatePetals() {
var petals = document.querySelectorAll('.petal');
if (!petals[0].animate) {
var petalsContainer = document.getElementById('petals_container');
return false;
}
for (var i = 0, len = petals.length; i < len; ++i) {
var petal = petals[i];
petal.innerHTML = '<div class="rotate"><img src="petal.png" class="askew"></div>';
var scale = Math.random() * .6 + .2;
var player = petal.animate([{
transform: 'translate3d(' + (i / len * 100) + 'vw,0,0) scale(' + scale + ')',
opacity: scale
},
{
transform: 'translate3d(' + (i / len * 100 + 10) + 'vw,150vh,0) scale(' + scale + ')',
opacity: 1
}
], {
duration: Math.random() * 90000 + 8000,
iterations: Infinity,
delay: -(Math.random() * 5000)
});
petalPlayers.push(player);
}
}
animatePetals();
.petal .rotate {
animation: driftyRotate 1s infinite both ease-in-out;
perspective: 1000;
}
.petal .askew {
transform: skewY(10deg);
display: block;
animation: drifty 1s infinite alternate both ease-in-out;
perspective: 1000;
}
.petal:nth-of-type(7n) .askew {
animation-delay: -.6s;
animation-duration: 2.25s;
}
.petal:nth-of-type(7n + 1) .askew {
animation-delay: -.879s;
animation-duration: 3.5s;
}
/* ... */
.petal:nth-of-type(9n) .rotate {
animation-duration: 2s;
}
.petal:nth-of-type(9n + 1) .rotate {
animation-duration: 2.3s;
}
/* ... */
@keyframes drifty {
0% {
transform: skewY(10deg) translate3d(-250%, 0, 0);
display: block;
}
100% {
transform: skewY(-12deg) translate3d(250%, 0, 0);
display: block;
}
}
@keyframes driftyRotate {
0% {
transform: rotateX(0);
display: block;
}
100% {
transform: rotateX(359deg);
display: block;
}
}
完整程式碼可檢視文後連結
?
。
CSS
判斷手機橫屏
本例 50音小遊戲
應用是針對移動端開發,未作pc端的樣式適配,所以可以新增一個橫屏引導頁面提示使用者使用豎屏。在 CSS
中判斷移動裝置是否處於橫屏狀態,需要用到 aspect-ratio
進行媒體查詢,通過測試 viewport
的寬高比來進行判斷。
aspect-ratio
寬高比屬性被指定為 <ratio>
值來代表 viewport
的寬高比。其為一個範圍,可以使用 min-aspect-ratio
和 max-aspect-ratio
分別查詢最小和最大值。基本語法如下:
/* 最小寬高比 */
@media (min-aspect-ratio: 8/5) {
// ...
}
/* 最大寬高比 */
@media (max-aspect-ratio: 3/2) {
// ...
}
/* 明確的寬高比, 放在最下部防止同時滿足條件時的覆蓋 */
@media (aspect-ratio: 1/1) {
// ...
}
在應用中的具體實現方式是新增一個 .mod_orient_layer
引導層並隱藏,當達到最小寬高比時將其顯示:
<div class="mod_orient_layer"></div>
.mod_orient_layer {
display: none;
position: fixed;
height: 100%;
width: 100%;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 999;
background: #FFFFFF url('landscape.png') no-repeat center;
background-size: auto 100%;
}
@media screen and (min-aspect-ratio: 13/8) {
.mod_orient_layer {
display: block;
}
}
實現效果:
相容性
下面是本文中涉及的幾個屬性的相容性檢視,在實際生產專案中需要注意相容性適配。
Photoshop 技能
logo設計
logo
主要由兩個元素構成由一個 ⛩️
圖示和日語平假名 あ
構成,都是經典的日系元素,同時對 あ
進行拉伸漸變,形成類似 ⛩️
的陰影,使字母和圖形巧妙連線在一起,使畫面和諧。logo背景色使用應用主題背景色,與頁面在無形之中建立聯絡,形成 全鏈路
統一風格標準。(編不下去了。。。?
⛩
鳥居原始模型來源於dribbble
: https://dribbble.com
外部連結及參考資料
- 櫻花散落動畫完整版 https://codepen.io/dragonir/full/WNjEPwW
- Dark Mode Support in WebKit https://webkit.org/blog/8840/dark-mode-support-in-webkit
- PWA技術理論+實戰全解析 https://www.cnblogs.com/yangyangxxb/p/9964959.html
- H5 PWA技術 https://zhuanlan.zhihu.com/p/144512343
- aspect-ratio https://developer.mozilla.org/zh-CN/docs/Web/CSS/@media/aspect-ratio
- Service Worker https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API
- Element.animate() https://developer.mozilla.org/zh-CN/docs/Web/API/Element/animate
作者:dragonir 部落格地址:https://www.cnblogs.com/dragonir/p/15041977.html