新着記事

Viewing posts for the category Javascript

Svelte で作ったWebコンポーネントを接続国によって出し分ける

概要

11月28日、当社 TORICO の初の海外店舗、マンガ展 台湾 がオープンしました。

漫畫展 台灣

オープンする直前で、「漫画全巻ドットコム、スキマ、マンガ展、ホーリンラブブックスのサイトに、台湾からアクセスされた方に告知を出したい。告知を1回クリックしたらもう出さないようにしたい。」という開発要件が出たので、作ってみました。

接続元の判定と出し分け

今回は、 AWS の CloudFront で、接続元の国名のヘッダを付与する機能を使うことにしました。

CloudFront の入口でリクエストヘッダが付与されますので、CloudFront Functions でそのヘッダを見て動的な処理が行えます。
ヘッダに応じて、対象国であれば、バックエンドのパスを変化させることで、接続国に応じた処理を行えるように設計しました。

サーバの管理はなるべく減らしたいため、今回は完全にサーバレスでサービスを作っています。動的な処理は、CloudFront Function を使いました。

コンポーネントの開発

全サイトに共通のモーダルポップアップ表示を行う必要があります。当社エンジニアのまるさんから、Svelteを進められたので、Svelte で Web コンポーネントを書きました。

完成物

これらのサイトに、台湾からアクセスすると、モーダルポップアップが表示されます。

仕組みとしては、リクエストするスクリプトの内容が、接続国に応じて変化するようになっています。

Svelte で Web コンポーネントを作る

Svelte の概要

Svelte は、Vue のシングルファイルコンポーネントによく似た形で、1ファイルで1コンポーネントをちょうどよく書けます。

CSS はスコープドなものになり、変数は特に意識せずともリアクティブに扱うことができます。

Web コンポーネントとは、JS によって作られた独自の HTML タグをどんなサイトでも使えるようにする技術です。
コンポーネントを定義した JS ファイルをロードすることで、そのサイトで <my-component></my-component> のような独自のタグを使えるようになります。
そのサイトが 素の HTML + jQuery で作られているサービスでも、React や Vue で作られているサイトでも、Google Tag Manager からでも同じようにコンポーネントが使えます。

複数サービスで共通のチャットウィジェットや、広告ウィジェットの開発に適しています。

プロジェクトの開始

Svelte のプロジェクトの開始方法は、チュートリアルがあるのでこの手順で作成できます。

https://svelte.jp/docs#getting-started

npm create vite@latest myapp

対話的に質問されますので、適切に答えます。

Select a framework: Svelte
Select a variant: TypeScript

※ SvelteKit は今回使いませんでした。
単純な Web コンポーネントであれば、特に使わなくても良いと思います。

起動

npm run dev

カウンターが起動します。

Webコンポーネント化する

このカウンターを、Webコンポーネントとしてビルドをできるようにします。

実際の商用コードはカウンターとは違うものですが、今回は例としてカウンターのコンポーネントを使います。

Svelte コンポーネントを Web コンポーネントとして書き出すには、既に良い記事がいくつかあります。

SvelteではじめるWeb Components開発 - Qiita

src/main.ts の内容を下記の1行に修正

export * from './lib/Counter.svelte'

src/lib/Counter.svelte の最上部に1行追加

<svelte:options tag="my-counter" />

ルートディレクトリの index.html の内容を書きに修正

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Svelte + TS</title>
</head>
<body>
<my-counter></my-counter>
<script type="module" src= "/src/main.ts"></script>
</body>
</html>

vite.config.ts を以下のように修正します。

import {defineConfig} from 'vite'
import {svelte} from '@sveltejs/vite-plugin-svelte'

// https://vitejs.dev/config/
export default defineConfig({
build: {
lib: {
entry: './src/main.ts',
name: 'my-counter',
fileName: (format) => `my-counter.${format}.js`,
formats: ['es'],
},
},
plugins: [svelte({
compilerOptions: {
customElement: true,
},
}),]
})

formats については、 ブラウザ用の JS を作るか、サーバ用の JS を作るかでいくつかの形式を指定できます。
デフォルトでは esumd で、umd はサーバサイドでの実行に使うものです。今回は、ブラウザ JS だけあれば良いので、es のみにしています。

独自のコンポーネントを開発する

今回は、開発したプロダクションコードの具体的な内容は割愛します。

開発サーバを実行

npm run dev

ビルド

動作が確認できたら、JS ファイルをビルドします。

npm run build

dist/my-counter.es.js が作成されます。

成果物のテスト

ビルド成果物を確認するには、プロジェクトのルートディレクトリに

<my-counter></my-counter>
<script type="module" src= "dist/my-counter.es.js"></script>

の内容を含む HTML を作り、ダブルクリックで起動すると良いと思います。

npm run preview というコマンドもありますが、dist ディレクトリ以下に index.html が必要で、
dist ディレクトリはビルドするたびに消えてしまうのでコンポーネント開発では使いづらそうです。

実際に他のサービスからコンポーネントを使う際も、上記のようなスクリプトタグで使うことができます。その際、 script タグに type="module" を忘れずに指定します。ビルドしたコードには、ファイル直下に $ といった短い名前の関数が作られます。そのため、type="module" なしでグローバル領域に読み込んだ場合、既存のスクリプトで jQuery 等を使っている場合に誤動作につながります。

なお、type="module" をつけた場合、そのスクリプトは CORS 判定の対象となりますので、サーバ側は access-control-allow-origin の HTTP ヘッダを必ず返す必要があります。

S3の設定

本番環境で使えるようにするにするため、ビルド成果物の JS を、S3 にデプロイして静的サイトホスティングの機能でホスティングします。

バケットの命名の注意

S3 のバケットを作る際は、バケット名をサービスを提供する URL のホスト名と完全に同じにする必要があります

CloudFront で、CloudFront Functions を使ってリクエストヘッダーを操作する場合、S3に伝わる Host ヘッダーは必ずリクエストしている Host ヘッダーとなり、 Functions 内で変更することは制限されておりできないためです。

静的ウェブサイトホスティングの設定

Hosting a static website using Amazon S3 - Amazon Simple Storage Service

S3 の「プロパティ」タブの一番下から設定します。

バケットポリシーの設定

「アクセス許可」タブ内にバケットポリシーの設定欄があります。

パブリックでの読み取りを許可するポリシーにします。

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicRead",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:GetObject",
"s3:GetObjectVersion"
],
"Resource": "arn:aws:s3:::world-visitor.torico-corp.com/*"
}
]
}

CORSヘッダーの設定

「アクセス許可」タブ内の一番下に Cross-Origin Resource Sharing (CORS) の設定欄があります。

[
{
"AllowedHeaders": [
"Authorization"
],
"AllowedMethods": [
"GET",
"HEAD"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": []
}
]

ファイルのデプロイ

ビルド成果物を S3 にアップロードします。

接続国により2つのファイルを出し分ける設計とするため、今回のビルド成果物の他に、空の JS も別のファイル名でデプロイしておきます。

CloudFront の設定

オリジン

先程作成した、S3 の WebホスティングのURLをオリジンとします。

接続国に応じてリクエストヘッダーを付与する設定

CloudFront の「オリジンリクエストポリシー」という機能で、リクエスト国別のヘッダを付与することができます。

CloudFront の ポリシーページの、オリジンリクエストのカスタムポリシーで、「オリジンリクエストポリシーの作成」を選択し、追加するヘッダーを選べます。

Adding the CloudFront HTTP headers - Amazon CloudFront

CloudFront-Viewer-Country を追加すると、ヘッダーの値として2文字の国名コードが追加されます。「JP」等。大文字になります。

リクエストヘッダによってオリジンに要求するファイルのパスを変えるファンクション

付与したリクエストヘッダーを CloudFront Functions で判定し、リクエストパスを変化させます。
今回はこのようなスクリプトになりました。

なお、handle の引数の event の構造はこのページで紹介されています。

CloudFront Functions event structure - Amazon CloudFront

/*
接続元の国によってリクエストパスにプレフィックスをつけるファンクション
*/

/// 国コードを取得
function getCountryCode(event) {
return event.request.headers['cloudfront-viewer-country']
? event.request.headers['cloudfront-viewer-country'].value
: ''
}

/// 国コード(大文字)から判断するディレクトリプレフィックス
/// SG は初回は必要無いが、検証のために追加
function getDirectoryPrefix(countryCode) {
if(['TW', 'SG'].includes(countryCode)) {
return '/tw';
}
return ''
}

/// uri (パス) はルーティング対象か
// 検証用の index.html はルーティングしたくない。
function uriRootingRequired(uri) {
return ['/my-component.es.js'].includes(uri);
}

function handler(event) {
var countryCode = getCountryCode(event);
var directoryPrefix = getDirectoryPrefix(countryCode);
if (directoryPrefix && uriRootingRequired(event.request.uri)) {
event.request.uri = directoryPrefix + event.request.uri;
}
return event.request;
}

Google Tag Manager でロードする

今回作った Svelte のコンポーネントは、 Google Tag Manager からでも使うことができます。

ただし、Google Tag Manager で独自の HTML タグを書くとエラーとなって公開できないため、
document.createElement でタグを作成しています。

また、script タグに type="module" 忘れずに指定します。これをつけないとグローバル変数領域にロードされてしまうため、既存のスクリプトに悪影響があります。

そして、type="module" を入れた場合、なぜか「document.writeをサポートする」にチェックが入ってないとスクリプトが動作しないため、チェックを入れます。

新卒エンジニアが今になって就職前にやっておけば良かったと思うこと3選



就社してからは初めてのブログ投稿となります。
お久しぶりです。開発部の鈴木海人です。

株式会社TORICOにエンジニアとして入社して、半年が経ちました。
今回のブログでは、過去の内定をいただいてから入社までの間に対して何もしなかった自分に対して
入社までにやっておいたほうがいいことについてまとめました。
過去の自分のような過ちを他の人が犯さないようにまとめましたのでエンジニア内定をもらって何をすればいいかわからない人はぜひ参考にしてみてください。



私の簡単な経歴はこちら↓
都内私立文系大学卒業
大学3年時の夏に某大手プログラミングスクールに通い、プログラミングの基礎について学ぶ
スクール卒業後、都内のスタートアップの会社で2ヶ月ほどインターン(作業内容は主にLPの作成を行っていました)
大学4年時は就活を行い、株式会社TORICOに内定をいただき、今に至ります。



注意事項
簡単なコーディング知識があることを前提にお話しします。もしプログラミングが全くわからないという人はprogateなどのプログラミングを簡単に学べるサイトでまずは学びましょう。



それでは本題に戻ります。
まず結論からお話しします。以下の3つになります。
  1. タイピング強化

  2. 会社で使用する言語の参考書を1冊読んでおく

  3. ドキュメントで調べる癖をつける





それでは1つずつ説明していきます。

1.タイピング


これはどんな人でも絶対にやっておきましょう。
目安としましてはe-typingで安定してA以上や寿司打で1万円コースクリアでしょうか。
上記のサイトですと日本語入力ですので英単語を打つようなサイトを探してみてもいいかもしれません。
僕は入社してから、過去タイピング練習をしてこなかったことを最も後悔しています。

タイピングを強化しておくメリットには下記が挙げられます。
  1. 仕事スピードが上がる
  2. 成長スピードが上がる
  3. 教えていただいている時の時間を少なくできる


考えてみれば当たり前なのですが、タイピングスピードが2倍になればかけるコードも2倍になり、そのため成長スピードも2倍になります。
逆にタイピングスピードが1/2倍になればかけるコードも1/2倍になり、そのため成長スピードも1/2倍になります。
もはやエンジニアにとって一番重要なのではと思っています。
もちろん最初はコードを書くことよりも調べたり読んだりする時間の方が長いので、タイピングスピードの恩恵をあまり受けられないかもしれません
しかし後々大きく影響してくるので鍛えておきましょう。
後、単純にタイピングで遅くてミスりまくると恥ずかしいです。
1日10分とかでもいいので毎日タイピングの練習をするのがおすすめです。私も練習中です
タイピングに慣れてきたら数字や記号などもしっかりと打ち込めつように練習しましょう。

また少し話はタイピングから話がずれてしまうのですが、
よく使用するショートカットキーの暗記やカーソルの移動スピードmaxなどの使いやすいPC設定も行っておきましょう
こちらもPCを使う上での基礎スキルとなり、使っているか使っていいないかで作業効率が大幅に変わるので意識してみてください。






2.会社で使用する言語の参考書を1冊読んでおく


参考書を読むというのに抵抗感がある人は多いのではないでしょうか?
実際僕もそうでした。ネットなどで調べてみると「ネットに全部載っているのに本を買う必要はない」、「わからないことはその都度ググって調べればいい」など
本に対しては比較的、良い情報が流れていないようなイメージが僕にはあります。
これは私の上司から教えていただいて、確かにとなったのですが、本は体系的(一つ一つのものがある系統に従ってまとまっているさまのことという意味みたい)になっているため正確な情報をしっかりとインプットできるのです。
今までとりあえずわからなくなったらググってを繰り返していたのですが1通り本を読むことにより、もちろん完全暗記はできませんがコードを書いているとき、あれが使えるかなとか、それが出てこなくても
調べて出てきたメソッドなど、そういえばこんなのあったなと思い出せます。
また、おすすめの参考書なのですが
私の上司のおすすめの参考書はとりあえず分厚い本みたいです。。。
残念ながら優しくて短い本では情報量が少なすぎたりであまりお勧めをしていないようです。
参考書を買うときは分厚くて情報量の多い参考書を選びましょう。
こちらもタイピングと同様少ない時間でもいいので移動時間などを活用して少しずつ読み進めましょう。






3.ドキュメントで調べる癖をつける


皆さんはドキュメントで調べ物をしていますか?
僕は基本的にQiitaだったり個人ブログなどを参考にすることが多いです。。。
わかりやすいですよね。。。
なるべく意識はしていますが今でもあまりできていないのが現状です。
ドキュメントで調べる癖をつけた方がいい理由は、正確な情報が手に入れられるからです。
調べ物をしているとき正確な情報じゃないことや、記事が古く参考にならなかったり、バージョン違いで動作しなかったりと
結構クソみたいな記事が上に表示されることはあるあるではないでしょうか
ドキュメントで調べる癖をつけておくと正確な情報を手に入れることができるのはもちろんなのですが
英語で文を読む癖がついたり、英語で調べたりする癖がつくのでためになると思います。
日本語検索とは比べ物にならないほど英語の情報は出てくるので、英語めっちゃできるぜ!って感じを目指さなくてもいいですが
グーグル翻訳を使いながらでも少しずつ調べ物ができるようになると良いです。
プログラミングをしている人にとって英語は切っても切り離せない関係なので
早い段階で慣れておきましょう。





以上3つが私が入社前にやっておけば良かったことになります。
最後にもう1度
  1. タイピング強化

  2. 会社で使用する言語の参考書を1冊読んでおく

  3. ドキュメントで調べる癖をつける



重要順は上から1.2.3となります。
ぜひエンジニアに、これからなる人なりたい人は参考にしてみてください。




サーバレスアプリケーション (HTML/JSのみ) で、Google Analytics API を使ってアクティブユーザー数を表示するダッシュボードを作る

Google Analytics は、API経由で様々な数値を取得することができます。
今回は、API経由でサイトのアクティブユーザー数を取得して、Nuxtで作ったダッシュボード風JSアプリに表示してみます。

Google Analytics の APIから値を表示してウェブページに表示する場合、APIとの通信をサーバサイドで PHP や Python のプログラムで取得してウェブブラウザに表示時する方法と、ウェブブラウザ自体が直接 Google Analytics API にリクエストして表示する方法と、どちらの方法でも実現できます。

サーバサイドで行う場合、認証情報やシステムを隠蔽でき、またHTTPリクエストが無くともバックグラウンドで値を取得し続けることができるなど、大きな利点がありますが、常時起動するサーバを用意しておかないといけないため構築が若干手間です。

今回は、手間をかけずに実現したかったため、ブラウザと静的リソースのみで動作するものを作ります。
APIへの認証は、ブラウザにログインしている Google ユーザーが行います。

認証のしくみ

Googleの提供している APIは、OAuth2 の認証・認可が必要となります。

アプリのページを開くと、Google のログインページを表示して認証を求め、認証されるとダッシュボードアプリの指定したコールバックURL にリダイレクトされるようにします。

リダイレクトされると、コールバックURL末尾の # (ハッシュ, フラグメント) の後にアクセストークンが含まれた状態になりますので、それを nuxt 内の JSでパースし、ブラウザのメモリ(変数)に格納します。

注意点として、認証情報(アクセストークン)がブラウザの変数に格納されるため、XSSが発生した際にアクセストークンが漏洩するリスクがあります。これはJSで認証情報を扱う以上避けられません。

XSSによる認証情報漏洩を防ぐには、GoogleとのAPIリクエストをサーバサイドで行い、サーバとの認証は httpOnly属性のついたセッションクッキーで行うべきですが、今回は、静的リソースなのでXSSは通常発生しないという前提の元、リスクを受容した上でクライアントサイドJSで機能を提供するものとします。

コールバックURLから取得したアクセストークンを認証ヘッダに含めて、ブラウザから Google Analytics にAPIリクエストをすることで、アクティブユーザー数などを取得できます。

アクセストークンは、localStorage に保存すると、セッション終了後も保持されるため漏洩の危険性が上がります。そのため変数として保存するだけにとどめます。ただし、OAuth2の state パラメータは、ブラウザセッションが変更されても維持する必要があるため、localStorage に一時的に保存します。

アプリのページを開くたびに、OAuth2のアクセストークンをリクエストし続けるとすると、毎回 Google のアカウント選択ページが出て面倒なように感じられるかもしれませんが、実際に使ってみるとそんなことはなく、一度ブラウザと Google の認証セッションクッキーが作られればあとはリダイレクトしか発生せず、1秒程度で自動的に認証が終わりアクセストークンが取得済みの状態になりますので、使用感として悪くはありません。

 試しにAPIにリクエストする

https://developers.google.com/analytics/devguides/reporting/realtime/v3/reference/data/realtime/get?#try-it

こちらの Google アナリティクスのページを見ると、APIの説明と共に、中央もしくは右側にAPIのテストリクエストができるフォームが表示されます。

ids に、 ga: を先頭につけた GoogleAnalytics のID番号を書き、metrics は rt:activeUsers を入れ、

下部 「Google OAuth 2.0」と「API key」にチェックを入れ、「Execute」 ボタンを押します。

すると、さらに下部にレスポンスデータが表示されます。
この情報を、nuxt 等ウェブアプリケーションで取得できます。

作り方

Google Cloud Platform のプロジェクトを作る

まず、Google APIを使うため、GCPのプロジェクトを作ります。

GCPのダッシュボードの上部ヘッダから、プロジェクトを新しく作ります。
https://console.cloud.google.com/home/dashboard

ライブラリの追加

プロジェクトを作ったら、APIとサービスページを開きます。
https://console.cloud.google.com/apis/dashboard

左メニューの「ライブラリ」をクリックし、ライブラリの追加ページで、Google Analytics API と Google Analytics Reporting API を有効にします。

認証情報の追加

次は、認証情報ページを開きます。
https://console.cloud.google.com/apis/credentials

上部「認証情報を作成」リンクから、「API キー」を選択し、APIキーを一つ作ります。

続いて、「認証情報を作成」リンクから、「OAuthクライアント ID」を選択します。

アプリケーションの種類 は、今回は「ウェブアプリケーション」で、名前は、適当に「nuxt client」とかにします。

「承認済みの JavaScript 生成元」は、クロスオリジンリクエストを許可するオリジン名です。
平文 http でもOKなので、

http://localhost:3000

などを追加しておきます。

「承認済みのリダイレクト URI」は、OAuth2 の認証フローのリダイレクトURL(コールバックURL)です。
認証完了時、このURLに、アクセストークンが URLに含まれた形でリダイレクトされます。
今回は、

http://localhost:3000/code

としました。
本番用のURLが決まっていたら、その分も追加します。後からでも追加できます。

スクリーンショットの例は、ローカルPCで docker + Nginx でポート80 でサービスする場合を考慮し、
http://localhost も追加してあります。

ちなみに、クライアントID とクライアントシークレット が作成されますが、ブラウザからのリクエストの場合はクライアントシークレットは使いません。

OAuth 同意画面は、今回は社内用途のみ考慮しているので「内部」を選択し、作成します。

プロジェクトの状態によっては、この選択肢は無いかもしれません。

これで、GCP のプロジェクトの設定は終了です。

アプリを書く

今回は、nuxt + TypeScript で書きます。すべてを載せると冗長なので、要点のみ記載します。

流れとしては、

1.
まずブラウザで / ページ表示時。
最初はアクセストークンが無いため、
store/auth.ts の navigateTokenRequestUrl がコールされる。

2.
navigateTokenRequestUrl の中では、ステートパラメータを作ってローカルストレージに保存し、
Google の認証URLに遷移。

3.
認証が完了すると、リダイレクトURL である /code に着地します。

4.
/code では、URLのハッシュ(フラグメント) から、アクセストークンとステートを取得し、
ステートの一致を検証後、アクセストークンを保存し、/ に遷移します。

5.
/ では、今回はアクセストークンがあるため、
コンポーネント内でそのアクセストークンを使って Google API でリアルタイムユーザー数を取得して表示します。

6.
このアクセストークンの寿命は1時間のため、1時間するとGoogle API は http 401 を返すようになります。
その場合、もう一度アクセストークン取得URLに遷移することで、アクセストークンの取り直しをします。

この時、Google とのセッションクッキーが認証済みであれば、ユーザー操作は不要で、一瞬画面がちらつくだけでアクセストークンの更新が完了します。

設定ファイル settings.ts

export const GOOGLE_API = {
authAPIEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
analyticsAPIEndpoint: 'https://www.googleapis.com/analytics/v3/data/realtime',
clientId: '<GoogleAPIの認証情報で作成されてたクライアントID>.apps.googleusercontent.com',
apyKey: 'AIz<GoogleAPIの認証情報で作成されてたAPIキー>o04',
scope: 'https://www.googleapis.com/auth/analytics.readonly'
}

export const GOOGLE_ANALYTICS_ACCOUNTS = [
{
id: '157xxxxx',
title: 'サイト1'
},
{
id: '872xxxxx',
title: 'サイト2'
},
]

型ファイル types/tasks.d.ts

interface TaskResult {
success: boolean;
message: string;
}

Vuex Store モジュール store/auth.ts

import { Action, Module, Mutation, VuexModule } from 'vuex-module-decorators'
import { GOOGLE_API } from '~/settings'

// OAuth2 state パラメータの localStorage一時保存用
const LOCAL_STORAGE_AUTH_STATE_KEY = 'authState'

@Module({
name: 'auth',
stateFactory: true,
namespaced: true
})
export default class extends VuexModule {
accessToken: string = ''

/**
* 認証済みか?
*/
get authorized () {
return this.accessToken !== ''
}

/**
* アクセストークンを保存
*/
@Mutation
setAccessToken (token: string) {
this.accessToken = token
}

/**
* OAuth2 認証URLに移動する
*/
@Action
navigateTokenRequestUrl () {
// 乱数で state パラメータを作成
const authState = [...Array(30)].map(
() => Math.random().toString(36)[2]).join('')
// state パラメータをローカルストレージに保存する
window.localStorage.setItem(LOCAL_STORAGE_AUTH_STATE_KEY, authState)
const params = [
['scope', GOOGLE_API.scope],
['include_granted_scopes', 'true'],
['response_type', 'token'],
['state', authState],
['redirect_uri', `${window.location.origin}/code`],
['client_id', GOOGLE_API.clientId]
]

window.location.href = `${GOOGLE_API.authAPIEndpoint}?` +
params.map(i => `${i[0]}=${encodeURIComponent(i[1])}`).join('&')
}

/**
* OAuth2認証完了後のURLのハッシュ(フラグメント)を解析して、アクセストークンを保存
*/
@Action
parseResponseParamsString (paramsString: string) : TaskResult {
// #key=value&key2=value2 形式の文字列を URLSearchParams にする
const usp = new URLSearchParams(
paramsString.replace(/^#/, '')) as any
// URLSearchParams を辞書型(マップ型)変数に変換
const paramsDict = [...usp.entries()].reduce(
(dict, e) => ({ ...dict, [e[0]]: e[1] }), {})
// localStorage に保存されている state と一致しているか検証
if (paramsDict.state !== window.localStorage.getItem(
LOCAL_STORAGE_AUTH_STATE_KEY)) {
return {
success: false,
message: 'stateが一致していません'
}
}
// state はもう使わないので消す
window.localStorage.removeItem(LOCAL_STORAGE_AUTH_STATE_KEY)
// アクセストークンがあるか検証
if (!paramsDict.access_token) {
return {
success: false,
message: 'アクセストークンが取得できません'
}
}
// アクセストークンがあったので変数に保存する。認証成功。
this.setAccessToken((paramsDict.access_token))
return {
success: true,
message: ''
}
}
}

pages/index.vue

ユーザーが最初に表示するページ

<template>
<div>
<header class="d-flex text-white p-2">
<div class="flex-grow-1 py-1">
Developer Dashboard
</div>
</header>
<div v-if="authorized">
<div class="container-fluid">
<div class="row">
<div
v-for="account in googleAnalyticsAccounts"
:key="account.id"
class="col-6 col-md-4 col-xl-3 my-3"
>
<RealtimePanel
:analytics-id="account.id"
:title="account.title"
/>
</div>
</div>
</div>
</div>
</div>
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
import { authStore } from '~/store'
import RealtimePanel from '~/components/RealtimePanel.vue'
import { GOOGLE_ANALYTICS_ACCOUNTS } from '~/settings'
@Component({
components: {
RealtimePanel
}
})
export default class Index extends Vue {
get accessToken () {
return authStore.accessToken
}

get authorized () {
return authStore.authorized
}

get googleAnalyticsAccounts () {
return GOOGLE_ANALYTICS_ACCOUNTS
}

requestToken () {
authStore.navigateTokenRequestUrl()
}

mounted () {
// 認証済みでなければトークン取得URLへ遷移
if (!authStore.authorized) {
this.requestToken()
}
}
}
</script>

pages/code/index.vue

OAuth API 認証後のリダイレクトURL(コールバックURL)

<!--
OAuth2 認証後にリダイレクトされるURL
URLに含まれるハッシュ(フラグメント)から、アクセストークンを取得して変数に格納する。
-->
<template>
<div>
code received
</div>
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
import { authStore } from '~/store'
@Component({
components: {
}
})
export default class Index extends Vue {
getRequestToken () {
authStore.navigateTokenRequestUrl()
}

async mounted () {
const taskResult = await authStore.parseResponseParamsString(
window.location.hash.replace(/^#/, ''))
if (!taskResult.success) {
throw new Error(taskResult.message)
}
this.$router.push('/')
}
}
</script>

components/RealtimePanel.vue

<!--
GAのアカウント1つに対応
一定時間ごとに、リアルタイムユーザー数を更新し続けるコンポーネント
-->
<template>
<div class="card">
<div class="card-header h2 py-3 text-truncate">
{{ title }}
</div>
<div class="card-body">
<div v-if="errorMessage" class="my-4">
{{ errorMessage }}
</div>
<div v-if="responseSuccess" class="text-center my-4">
<div class="display-1 fw-bold">
{{ activeUsers|addComma }}
</div>
<div class="text-muted small">
Active User
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'nuxt-property-decorator'
import { authStore } from '~/store'
import { GOOGLE_API } from '~/settings'

@Component({
components: {
}
})
export default class extends Vue {
activeUsers: number | null = 0
responseSuccess: boolean = false
errorMessage: string = ''

@Prop({
type: String,
required: true
})
title!: string

@Prop({
type: String,
required: true
})
analyticsId!: string

mounted () {
this.reloadPolling()
}

reloadPolling () {
this.reload()
setTimeout(() => {
this.reloadPolling()
}, 20000)
}

async reload () {
const response = await this.$axios.get(
GOOGLE_API.analyticsAPIEndpoint, {
params: {
key: GOOGLE_API.apyKey,
ids: `ga:${this.analyticsId}`,
metrics: 'rt:activeUsers'
},
headers: {
Accept: 'application/json',
Authorization: `Bearer ${authStore.accessToken}`
},
validateStatus: _ => true
}
)
// トークン期限切れ
if (response.status === 401) {
authStore.navigateTokenRequestUrl()
return
}
// 403: アクセス過多など
if (response.status !== 200) {
this.responseSuccess = false
this.errorMessage = `ERROR: ${response.status}`
return
}
if (response.data) {
this.activeUsers = parseInt(response.data.totalsForAllResults['rt:activeUsers'])
this.responseSuccess = true
this.errorMessage = ''
}
}
}
</script>

ブラウザ上で簡単なスクリプトをブックマークから動かす方法(ブックマークレット)

この記事は、非プログラマ向けの技術記事です。非プログラマが、プログラミングを始める足がかりとなることを目的としています。

WEBブラウザと日常業務

日常業務でウェブブラウザを使う機会はけっこうあると思います。いろいろなツールが Webアプリ化されるにつれ、ブラウザを業務で使う機会は昔と比べて増えました。

ただし、多くの項目があるフォームを毎日書く業務があったり、あるページを開いて項目を1つ1つスプレッドシートや他のWebアプリ、もしくはメールにコピペしたりといった業務があったりもします。もし、その業務が面倒だと感じているならば、自動化できる余地は十分あります。

プログラマは、面倒だと思った作業はすぐに自動化できますが、プログラミングに明るくない方でも、範囲は限定的ですが Webアプリであれば面倒が改善できるかもしれません。

WEBブラウザでプログラムを動かせる

お使いのブラウザで、今すぐに自分でプログラムを書いて実行できます。そういう機能がブラウザに入っています。

Windows の Chrome, Firefox であれば、Ctrl + Shift + i を押してみてください。Edge をお使いであれば F12 です。Mac であれば Command + Option + i です。

すると、画面が分割され別のペインが出てきます。もし、警告文が出てきたら読み、同意/認可ができるようであればブラウザの案内にそって対応してください。

おそらく最初は、Firefoxの場合は「インスペクタ」、Chromeの場合は「Elements」タブが有効になっていると思います。その隣に、「コンソール」「Console」タブがありますので、それをクリックするとコンソールモードになります。

↑ Firefox

↑ Google Chrome

Firefox では、一番下に >> となっています。ここをクリックするとプログラムが入力できます。Chrome の場合は一番上に > があり、ここにプログラムが入力できます。

試しに、簡単な計算式を書くと、計算ができます。

↑消費税計算と81の平方根の算出をした例

Math.sqrt は、ブラウザの Javascript エンジンに最初から組み込まれている平方根計算の関数です。どんなことができるかはJavascript の入門書を読むと良いと思いますが、「何かをウェブページに表示する」であれば大概できます。(ただし、表示しているサイトとは別のサイトから持ってきた情報を表示するのは少し難しい場合があります)

別の例を試します。コンソールに alert('今日は' + new Date()) と入力します。そうすると、現在の日時がポップアップで表示されます。

また、history.back() を入力すれば、ブラウザの「戻る」ボタンを押した時と同じように、前のページへ戻ることができます。

このように、ウェブブラウザには非常に簡単にプログラムを動かす仕組みが入っています。

アドレスバーからでもプログラムを実行できる

先程は開発コンソールからプログラムを実行しましたが、ブラウザ上部にあるアドレスバーからでもプログラムの実行ができます。

…できたはずなんですが、最近のブラウザは、アドレスバーに入力した文字列が URL ではないと判断した場合、検索エンジンで検索するようになっているので、アドレスバーにスクリプトを入力しての実行する方法は最近のブラウザでは難しくなっています。

昔のブラウザでは、アドレスバーに

javascript:alert('今日は' + new Date());void(0);

と入力すると、先程コンソールでプログラムを動かしたのと同様に alert を動作させることができました。

アドレスバーに直接入力しての動作はできなくなりましたが、リンクのタグとして Webページに記録すれば動作させることができます。

プログラムを動作させるリンクタグ(クリックで上記プログラムを実行)

ブックマークからでもプログラムを実行できる

アドレスバーに入力したプログラムが動作するということは、そのプログラムをブックマークとして保存しておけば、そのブックマークを呼び出した時にプログラムが動作することになります。

試しに、上記「プログラムを動作させるリンクタグ」をブックマークしてください。リンクをブックマークツールバーにドラッグアンドドロップすると良いでしょう。

ブラウザによっては、「ブックマークの新規作成」のようなメニューから、さきほどの

javascript:alert('今日は' + new Date());void(0);

このスクリプトをURLとして登録すればブックマークできます。

ブックマークができたら、クリックしてください。同様にプログラムが動いて、ポップアップでメッセージが表示されたと思います。

このように、ブックマークの中にプログラム(Javascript)を記録して、クリックして実行させる方法を「ブックマークレット」といいます。

ブックマークレットのサンプル

試しに作ってみました。

ページの表示を崩すブックマークレット

ページの表示を崩す

これをブックマークして、好きなページで実行することでページの表示を崩すことができます。

元に戻すにはページをリロードしてください。

スクリプトの内容を書いておきます

var divs = document.getElementsByTagName('div');
var mode = Math.floor(Math.random() * 2);
for (var i = 0; i < divs.length; i++){
  var delay = Math.random() * 1000;
  var d = divs[i];
  setTimeout(function(d) {
   if (d){
    var t = Math.floor(Math.random() * 2000 + 1000);
    d.style.transition = "all " + t +"ms ease-in";
    var deg = Math.random() * 30 - 15;
    d.style.transform = "translate(0, 800px) rotate(" + deg + "deg)";
  }
  }, delay, d);
}

フォームの全 Input の value に name を入れるブックマークレット

inputのvalueにnameを入れる

function d(inputs){
  for(var i =0; i<inputs.length; i++){
    inputs[i].value =inputs[i].name;
  }
}
d(document.getElementsByTagName("input"));
d(document.getElementsByTagName("textarea"));

「フォーム内容を復元するブックマークレット」を作るブックマークレット

フォーム内容を保存

実行すると、ページ上にリンクが1つできます。そのリンクがブックマークレットになっているので、ブックマークしてください。

次回、同じフォームを表示した時にそのブックマークレットを実行することで内容を再度入力することができます。

var out = "javascript:function sC(n,v,c){var elms=document.getElementsByTagName('input');for(ind in elms){o=elms[ind];if(o.name==n && o.value==v){o.checked=c;}}}function sI(n,v,c){var elms=document.getElementsByTagName('input');for(ind in elms){o=elms[ind];if(o.name==n){o.value=v;o.checked=c;}}}function sO(v){var elms=document.getElementsByTagName('option');for(ind in elms){o=elms[ind];if(o.value==v){o.selected='selected'}}}function sT(n,v){var elms=document.getElementsByTagName('textarea');for(ind in elms){o=elms[ind];if(o.name==n){o.value=v;}}}";

var elms, ind, o;
elms = document.getElementsByTagName('input');
for (ind in elms) {
    o = elms[ind];
    if (o.type == "hidden") {
        continue;
    } else if ((o.type == "checkbox" || o.type == "radio") && o.name && o.value) {
        out += "sC('" + o.name + "','" + encodeURIComponent(o.value) + "'," + o.checked + ");";
    } else if (o.name && o.value) {
        out += "sI('" + o.name + "','" + encodeURIComponent(o.value) + "');"
    }
}
elms = document.getElementsByTagName('option');
for (ind in elms) {
    o = elms[ind];
    if (o.selected) {
        out += "sO('" + o.value + "');"
    }
}
elms = document.getElementsByTagName('textarea');
for (ind in elms) {
    o = elms[ind];
    if (o.value) {
        out += "sT('" + o.name + "','" + encodeURIComponent(o.value.replace(/\s+/g, ' ')) + "');"
    }
}
out += "void(0);";
console.log(out);
var a = document.createElement('a');
a.href = out;
a.textContent = 'フォーム自動入力';
a.style.display = 'block';
a.style.position = 'fixed';
a.style.backgroundColor = '#eee';
a.style.padding = "40px";
a.style.top = "100px";
a.style.left = "100px";
a.style.zIndex = 1000;
a.style.border = "1px solid black";
document.body.appendChild(a);

ブックマークレットでできること

表示しているページの改変や操作が得意です。

ブラウザで表示しているページは HTML で出来ていて、ブックマークレットではその HTML のかなり多くの箇所を操作できるため、「表示しているページに対して何かを行う」ことに関しては得意です。

例えば、入力フォームが表示されている時、その入力フォームに対して何かを行う(自動入力など)ことについては他にないほど便利です。

複数ページにまたがった処理や、別のサイトのページに対しての処理は苦手です。

ページの遷移を行うと処理が止まってしまうため、複数ページの処理はあまり得意ではありません。特に、複数のサイトを同時に操る操作はブラウザの制約があるため難しいです。がんばればなんとかなりますが、他の手段を用いたほうが良いでしょう。Selenium とか Windows Scripting Host とか。

Javascript をブックマークレットに変換する方法

ページに対して何かを行う Javascript が書けた場合(思い浮かんだ場合)、それをブックマークレットの形式にするには少しの変換が必要です。

  • 改行や余計なスペースを除去する
  • 記号を URL エンコードする
  • 先頭に「javascript:」をつける
  • 最後に void(0) をつける、もしくは全体を void() で囲む、など

(一番最後のは、処理が undefined で評価されないとページ遷移が発生して、真っ白いページが表示されるなど変な結果になってしまうため入れてます)。

短いスクリプトであれば手で修正できますが、長いスクリプト、特にインデントが入っているものなんかは手で変換するのは大変です。

変換スクリプトを書いて github pages で上げておきましたので、よかったら使ってください。

Bookmarklet スクリプト変換

Search