もっと身近にやってたPWA
2020/05/20
PWA night #16
jiyuujin
- 複雑なUIを作るのが好きです
- Vue/Nuxt/React/Next/Scala/Swift
- v-kansai/kansai.ts/..
- Profile site by Gatsby.js since 2020/03
- https://yuukit.me
- Tech blog by Nuxt.js since 2018/10
- https://webneko.dev
PWA、やってますか?
結果的にPWAを意識してない🤭
色々やった中でも具体例を3つ程
技術スタック
- Vue.jsを使うことを前提に、細かなデザインの微調整を除いてフル自作したコンポーネント
- ネイティブのDateライブラリに加え、dayjsも使用
- rangeのみならず、single / timeにも対応
力を入れた箇所は?
入りきらない程の props
import Vue from 'vue'
export default Vue.extend({
props: {
id: {
type: String,
default: ''
},
minDate: {
type: Date,
default() {
return null
}
},
maxDate: {
type: Date,
default() {
return null
}
},
linkedCalendars: {
type: Boolean,
default: true
},
singleDatePicker: {
type: Boolean,
default: false
},
showDropdown: {
type: Boolean,
default: false
},
autoApply: {
type: Boolean,
default: false
},
dateRange: {
type: Object,
required: true
},
ranges: {
type: Object,
default() {
let today = new Date()
today.setHours(0, 0, 0, 0)
let yesterday = new Date()
yesterday.setDate(today.getDate() - 1)
yesterday.setHours(0, 0, 0, 0)
let thisMonthStart = new Date(today.getFullYear(), today.getMonth(), 1)
let thisMonthEnd = new Date(
today.getFullYear(),
today.getMonth() + 1,
0
)
let result = {}
result['今日'] = [today, today]
result['昨日'] = [yesterday, yesterday]
result['今月'] = [thisMonthStart, thisMonthEnd]
result['先月'] = [
new Date(today.getFullYear(), today.getMonth() - 1, 1),
new Date(today.getFullYear(), today.getMonth(), 0)
]
result['今年'] = [
new Date(today.getFullYear(), 0, 1),
new Date(today.getFullYear(), 11, 31)
]
result['昨年'] = [
new Date(today.getFullYear() - 1, 0, 1),
new Date(today.getFullYear() - 1, 11, 31)
]
return result
}
},
localeData: {
type: Object,
default() {
return {}
}
},
opens: {
type: String,
default: 'center'
},
dateFormat: {
type: Function,
required: true
},
clear: {
type: Boolean,
default: false
},
timePicker: {
type: Boolean,
default: false
},
timePickerIncrement: {
type: Number,
default: 5
},
timePicker24Hour: {
type: Boolean,
default: true
},
timePickerSeconds: {
type: Boolean,
default: false
}
},
})
全てを紹介すると時間が足らない😅
- ranges (Object)
- singleDatePicker (Boolean)
- timePicker (Boolean)
適宜変数など準備いただければ
<v-range-picker
ref="range-picker"
:single-date-picker="singleDatePicker"
:auto-apply="autoApply"
:linked-calendars="linkedCalendars"
:date-range="dateRange"
opens="right"
:date-format="dateFormat"
@update="updateValues"
@toggle="checkOpen"
>
<div slot="input" slot-scope="picker" style="min-width: 350px;">
{{ picker.startDate }} - {{ picker.endDate }}
</div>
</v-range-picker>
npm i v-range-picker
お知らせ通知機能
こういう経緯から、
- リリースのタイミングで、ユーザに対して知らせる情報を、HTMLで静的に書く
- けどその通知が無く、それを解決しよう
互いに全く違うプロジェクトでしたが
- 新規案件 (Laravel/SPA, Play/SPA) のアプリ内ブラウザでリリースするタイミングを通知する機能を実装
- 既存案件 (CakePHP/SPA) の会員制向け管理画面で、工数をかけずリリースするタイミングを通知する機能を実装
前者の例
もともと既に、簡単なPWAを入れてた
const PRECACHE = 'precache-v1';
const RUNTIME = 'runtime';
// キャッシュのバージョン管理の自動化
const VERSION = '<%= hash %>';
const STATIC_CACHE_KEY = 'static-' + VERSION;
const PRECACHE_URLS = [
'index.php',
'./', // Alias for index.html
'./css/app.css',
'./js/app.js',
'./js/manifest.js',
'./js/vendor.js'
];
const CACHE_VERSION = 1;
let CURRENT_CACHES = {
offline: 'offline-v' + CACHE_VERSION
};
const OFFLINE_URL = 'offline.html';
self.addEventListener('install', event => {
//console.log('Installing...');
if (typeof self.skipWaiting === 'function') {
//console.log('self.skipWaiting() is supported.');
event.waitUntil(
caches.open(PRECACHE)
.then(cache => cache.addAll(PRECACHE_URLS))
.then(self.skipWaiting())
);
} else {
console.log('self.skipWaiting() is not supported');
}
});
ガチャ画像など適切に指定してはキャッシュ
さらに serviceWorker を利用しよう💡
push通知対応しているか否か
isSupported() {
return ('serviceWorker' in navigator
&& 'PushManager' in window)
}
SWインストール時に、pushする情報をLambdaにPOSTする
AWS SNSにアクセスする
const AWS = require('aws-sdk')
exports.handler = async (event) => {
const params = {
Message: 'ここに入力します。',
TopicArn: ''
}
await new AWS.SNS().publish(params).promise().then((data) => {
console.log('MessageID is ' + data.MessageId)
}).catch((err) => {
console.error(err, err.stack)
})
}
serviceWorker.js で受け取る準備をして、
// 通知内容を表示する
self.addEventListener('push', function (event) {
let name = ((((event || {}).data || {}).json() || {}).data || {}).name;
event.waitUntil(
self.registration.showNotification(`${name}さんから`, {
'body': 'v1.0.0 がリリースされました。',
})
);
});
// クリックした時に、次にとるアクションを表示する
self.addEventListener('notificationClick', function (event) {
event.notification.close();
focusWindow(event);
});
得られた成果
- 決められた時間に設定できる
- Aurora、もしくはRDBと連携して、柔軟にゲームステータスに応じてターゲットや時間を設定できる
一方、後者の例
工数を取れない制約が..🙅♀️
せめてログイン直後には通知する💡
特定のDOMに対して、クッキーを設定
const version = '';
const newestTopicId = `Topic_${version}`;
// クッキーを設定する
docCookies.setItem(
`COOKIE_TOPIC_${version}`,
newEstTopicId,
new Date()
);
if (docCookies.getItem(`COOKIE_TOPIC_${version}`)) {
// 存在した場合、ポップアップを表示する
}
得られる成果
- 初回ログイン時にこのクッキーが存在した場合、新たにポップアップを表示する
- 一方で存在しなかった場合、ポップアップを表示しない
微々たる方策でしたが、
やらないよりは少しの解決を✍️
思い返すと色々やっていた
現在のお仕事は
- Next.js + AppSync のサーバレス環境で、新しいしごと情報を管理する両サイトの制作を進めてます
- 実際のおしごとで使う用の本番アプリ
- 本番で使うため、事前に作ったデモアプリ (趣味用)
- Vue CLI ベースのMPA構成を構築、コンポーネント単位でリプレースを進めてます
最近やっていることでPWA..🤔
Auth0を使ったマルチデバイス化
Next.jsを使った会員限定サイト
@auth0/auth0-nextjs
Auth0を楽に始める
[Single Page Application] からプロジェクトを作る
consoleで設定
- Domain
- Client ID
- Client Secret
- Callback URL
- Logout URLs
- Web Origins
configを読み込む
import { initAuth0 } from '@auth0/nextjs-auth0';
export default initAuth0({
clientId: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
scope: process.env.AUTH0_SCOPE,
domain: process.env.AUTH0_DOMAIN,
redirectUri: process.env.REDIRECT_URI,
postLogoutRedirectUri: process.env.POST_LOGOUT_REDIRECT_URI,
session: {
cookieSecret: process.env.SESSION_COOKIE_SECRET,
cookieLifetime: process.env.SESSION_COOKIE_LIFETIME,
storeIdToken: false,
storeRefreshToken: false,
storeAccessToken: false
}
});
1個でも欠けると認証に失敗..😇
ログイン周りの導線を作る
@auth0/nextjs-auth0
export interface ISignInWithAuth0 {
handleLogin: (
req: IncomingMessage,
res: ServerResponse,
options?: LoginOptions
) => Promise<void>;
}
pages/api/login.tsx
import auth0 from '../../lib/auth0';
export default async function login(req, res) {
try {
await auth0.handleLogin(req, res);
} catch (error) {
console.error(error);
res.status(error.status || 500).end(error.message);
}
}
@auth0/nextjs-auth0
export interface ISignInWithAuth0 {
handleLogout: (
req: IncomingMessage,
res: ServerResponse
) => Promise<void>;
}
pages/api/logout.tsx
import auth0 from '../../lib/auth0';
export default async function logout(req, res) {
try {
await auth0.handleLogout(req, res);
} catch (error) {
console.error(error);
res.status(error.status || 500).end(error.message);
}
}
これでログイン、ログアウト完
認証するためのフックを作る
let userState;
export const fetchUser = async () => {
if (userState !== undefined) {
return userState;
}
const res = await fetch('/api/me');
userState = res.ok ? await res.json() : null;
return userState;
};
使いたい場所で呼び出す
const IndexPage: NextPage<IndexPageProps> = () => {
const { user, loading } = useFetchUser()
return (
<>
{!loading && !user && <SWHeader isAuth={false} />}
{user && (
<>
<SWHeader isAuth={true} />
<!--
ログインに成功したら表示するページ
-->
</>
)}
</>
)
}
さらにこれをやった認証は、
- 全画面同じ処理を書くのは格好悪いので、middlewareとしてラッパーを持たせた
- ログインした際にlocalStorageで保持させた
ルールを設定する
今後(も)進めたいこと
解決したい課題など
- もっとUX (ユーザ・エクスペリエンス) 向上させられるよう、フロントエンド・インフラを理解したい
- 登壇駆動開発として今回登壇させていただきましたが、黄金比率、のような一体何が正解かを知りたい
最後に自分なりのPWAを簡単にまとめ
- 求められる仕様に、自然と入ってくることが多い
- Range Pickerを始めとしたSP対応
- ユーザを逃さない施策をきっかけに始めたpush通知
- Auth0を使って簡単にマルチデバイス対応
- これまで(これからも?)意識しないことは変わらない(だろうと私感を最後にぶっ込んで)
Thank you..🙇♀️
もっと身近にやってたPWA
By jiyuujin
もっと身近にやってたPWA
PWA night #16 ではAuth0を中心に
- 1,111