別記事にて簡易な記事閲覧アプリを作成しています。その一環でブラウザから今開いているタブのURLをアプリへ記事登録できるように簡単なchrome-extensionを作成しました。今回はその内容をまとめます。
アプリのソースはこちらになります。
全体像
ざっくりとした全体像を描くと上のようになります。
chrome-extension側ではバックグラウンド(background.ts
)で実際のログインや登録したいURLのFirebaseへの送信等の機能を実装します。
popup.ts
(あるいはトランスパイル後のjsファイル)chrome-extensionのmanifest.json
のdefault_popup:
経由で呼ばれるよう設定します。これより拡張機能アイコンをクリックしたときにLogin及び(URLの)Saveイベントを受け取るButton UIを表示させます。
ユーザーがログイン前の場合はLogin buttonを、ログイン後の場合はSave buttionを表示させます。
この時background.ts
とpopup.ts
間のリクエストやデータ通信はchrome-extensionのmessage
やstorage
機能を使って実装します。
Firebase側ではAuthentication機能としてsignInWithPopup()
機能を使います。background.ts
からリクエストが来た際に認証を行い、ユーザー情報を返します。
またFunctionsではbackground.ts
のSaveToDb(User)
から送られてきたURL情報をパースしFirestoreに保存します。これにより記事閲覧アプリから見れるようになります。
Chrome-extension詳細
環境
公式のチュートリアルで扱われているものは、そのままだとFirebase SDKを使えません。そのためwebpack等を用いてnodejsアプリとしてbundleできるように整備しました。
今回はchrome-extension-typescript-starterをベースとして利用させてもらい、Firebaseとのbundleを構成しました。
Firebase SDK自体のnodejs向けインストール方法は公式を参照して下さい。
一点嵌まったポイントとして、webpack
経由で生成されたjavascriptファイルをchrome extension側から読ませた場合にEncoding is not UTF-8
と表示されるエラーが発生する場合があります。これはFirebase SDKをwebpack
でbundleした際にUTF-8でない文字�
が発生するためのようです(詳細)。今回は回避手段としてsed
等を駆使して文字列検出と排除を行っています。
background.ts
このファイルでは主にFirebase Authenticationを利用したlogin
、及びFirebase Functionsを利用したsaveToDb
メソッドを実装します。
var auth = firebase.auth();
var functions = firebase.functions();
chrome.runtime.onMessage.addListener(
function (request, sender, sendResponse) {
if (request.name === "login") {
// Login request.
var provider = new firebase.auth.GoogleAuthProvider();
provider.addScope('https://www.googleapis.com/auth/contacts.readonly');
// Chrome extension only supports popup sign in.
auth.signInWithPopup(provider).then(function (result) {
var user = result.user;
var status = '200';
sendResponse({ user, status });
}).catch(function (error) {
// Handle Errors here.
var errorCode = error.code;
if (errorCode === 'auth/account-exists-with-different-credential') {
alert('You have already signed up with a different auth provider for that email.');
// If you are using multiple auth providers on your app you should handle linking
// the user's accounts here.
} else {
console.error(error);
}
var status = '400';
sendResponse({ error, status });
});
} else {
var error = "unknown request: " + request.name;
console.error("unknown request", request.name);
var status = '400';
sendResponse({ error, status });
}
return true;
});
chrome.runtime.onMessage.addListener
は他(popup.ts)からのmessageを待ち受け、リクエストの種類(login
)に応じて処理を行います。
login
リクエストに対しては、今回Googleのpopupベースのsign inスキームを利用しています。セットアップ方法は公式が参考になります。特にchrome-extensionは制約としてpopup operationのみのサポートとなっています。また利用時にはFirebase側でドメインの承認が必要です(参考)
ログイン情報の管理はbackground.ts
上でonAuthStateChanged
を利用して以下のように設定できます。
var auth = firebase.auth();
function initApp() {
// Listening for auth state changes.
auth.onAuthStateChanged(function (user: firebase.User | null) {
if (user) {
// User is signed in.
let displayName = user.displayName;
let email = user.email;
let uid = user.uid;
chrome.storage.sync.set({ displayName, email, uid }, () => {
console.log('User is signed in: ', displayName, email, uid);
});
} else {
// User is signed out.
let displayName = '';
let email = '';
let uid = '';
chrome.storage.sync.set({ displayName, email, uid }, () => {
console.log('User is signed out');
});
}
});
}
initApp();
ここで現在のログイン情報はchrome.storage
機能を使って保持し、後ほどpopup.ts
からも見えるようにしています。
続いてsaveToDb
リクエストに対する処理も同様にchrome.runtime.onMessage.addListener
へ加えていきます。
} else if (request.name === "saveToDb") {
// Save requested URL to DB.
let url = request.url;
// Request to Firebase functions.
chrome.storage.sync.get("uid", ({ uid }) => {
let saveToDb = functions.httpsCallable('saveToDb');
saveToDb({ uid, url })
.catch((error) => console.log('error saving url:', error));
});
こちらではFirebase functions側のhttpsCallable
を使った呼び出しにてfunctions側に対応する登録済saveToDb
メソッドを呼び出していきます。functions側のメソッドは今回省略しますが、現時点では単純にFirestoreへデータ追加を行うのみとなります。
popup.ts
先述した通り、manifest.json
の設定より、拡張機能のアイコンをクリックした際にこのスクリプトが呼び出されることになります。
popup.ts
ではReactを使ってlogin
やsaveToDb
に関わるViewを実装します。
const Popup = () => {
const [uid, setUid] = useState<string>("");
const [currentURL, setCurrentURL] = useState<string>();
useEffect(() => {
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
setCurrentURL(tabs[0].url);
});
}, []);
useEffect(() => {
chrome.storage.sync.get("uid", ({ uid }) => {
setUid(uid);
});
}, []);
const Login = () => {
chrome.runtime.sendMessage({ name: "login" }, response => {
if (response.status !== "200") {
console.error("Error on login request", response.error);
alert(response.error);
return;
}
console.log("Login succeeded:", response.user);
setUid(response.user.uid);
});
}
if (!uid) {
// User is signed out.
return (
<>
<ul>
<li>Current URL: {currentURL}</li>
<li>Current Time: {new Date().toLocaleTimeString()}</li>
<li>User ID: Not logged in</li>
</ul>
<button
onClick={() => Login()}
>
Log in
</button>
</>
);
}
// User is signed in.
let view: React.ReactElement;
if (!isUrlSaved) {
view = (
<button
onClick={() => {
SendUrlToDb(currentURL);
setIsUrlSaved(true);
}}
>
Save button
</button>
);
} else {
view = (<p>"URL saved!"</p>);
}
return (
<>
<ul>
<li>Current URL: {currentURL}</li>
<li>Current Time: {new Date().toLocaleTimeString()}</li>
<li>User ID: {uid}</li>
</ul>
{view}
</>
);
};
ReactDOM.render(
<React.StrictMode>
<Popup />
</React.StrictMode>,
document.getElementById("root")
);
Popup
メソッドはユーザー情報に基づいてlogin button、あるいはsave buttonを表示させます。ユーザー情報は先程background.ts
側でchrome.storage
を用いて保持してあります。
続いてログイン後に表示されるsave buttonをクリックすると、以下のようなsendToDb
メソッドが呼ばれます。
const SendToDb = (url: string | undefined) => {
if (url === undefined) {
console.error("Given URL is undefined.");
alert("Given URL is undefined.");
return;
}
console.log("Send URL to firebase DB", url);
chrome.runtime.sendMessage({ name: "sendToDb", url }, response => {
if (response.status !== "200") {
console.error("Error on sending URL", response.error);
alert(response.error);
return;
}
console.log("Send URL successfully:", url);
});
}
background.js
側でmessage requestを受ける機能を実装済ですので、このメソッドではそれをurl
と共に単純に呼び出しています。
デモ
Firebase側ではemulatorを使って実際に動かしてみました。
Webpackから出力したファイル群をchrome-extensionのパッケージ化されていない拡張機能を読み込む
から読み込みます。
テスト用にexample.comへアクセスしアイコンをクリックしてみます
Log in buttonをクリックしてGoogleからログインします。するとポップアップ表示が以下に変わりました。
Save buttonを押してみた後、Firebase Firestoreを確認します。
以上のようにFireStore側でデータが追加されている事が確認できました。
補足
Firebase emulatorでauthenticationをemulateしようとしましたが、恐らく?既知のバグがあるようで今回は断念し、実際の環境で試しています。