JavaScriptのFile System Access APIをご存知でしょうか。このAPIはPCのローカルのファイルを操作することができるAPIです。
File System Access APIを使いこなすと、インターネットにつながってなくても便利なブラウザツールを作ることができます。
この記事ではFile System Access APIの使い方とこれを利用したちょっとした便利なブラウザツールを紹介します。
目次
前提知識
この記事は日常的にJavaScriptで開発を行うエンジニアよりは、PC事務作業の業務効率改善に困っている方向けに書きました。
もっともFile System Access APIを利用するにはHTMLやJavaScriptといったWEBページ作成の基本的な知識やJSONといったデータフォーマットの最低限の知識が必要です。
他のサイトにも入門向けのページはありますが、このサイトでも以下のような記事がありますのでよろしければ合わせてご覧ください。
FileSystemAccessAPIとは?使い方
冒頭でも記載の通り、File System Access APIとはブラウザ上でPC上に保存されたファイルを操作できるAPIです。
これを使うことで、ブラウザベースのメモ帳などを作ったりすることができます。
早速以下のような簡単なメモ帳アプリで使い方をみてみましょう。
ソースコードの全体図はこちらです。
<!DOCTYPE html>
<html>
<head>
<title>File System Access APIサンプル</title>
</head>
<body>
<button onclick="readFile()">ファイルを読み込む</button>
<button onclick="writeFile()">ファイルに書き込む</button>
<br>
<textarea id="fileContent" style="width: 100%; height: 200px;"></textarea>
<script>
//ベースとなるファイルハンドラ
let fileHandle = null;
/**
* ファイルを読み込む
*/
async function readFile() {
try {
//ファイルを開くプロンプトを開きファイルハンドラを取得
[fileHandle] = await window.showOpenFilePicker();
//ファイルハンドラからファイルの読み込みを行う
const file = await fileHandle.getFile();
//ファイルの中身のテキストを取得
const text = await file.text();
//取得したテキストをtextareaに出力
const fileContentTextArea = document.getElementById("fileContent");
fileContentTextArea.value = text;
} catch (error) {
console.error("ファイルを読み込む際にエラーが発生しました:", error);
}
}
async function writeFile() {
//ファイルハンドラの保有を確認
if (!fileHandle) {
alert("ファイルを読み込んでから保存してください。");
return;
}
//新たに保存するテキストをtextareaから取得
const fileContentTextArea = document.getElementById("fileContent");
const text = fileContentTextArea.value;
try {
//ファイルハンドラから出力処理オブジェクトwriteableを取得
const writable = await fileHandle.createWritable();
//保存するテキストを出力
await writable.write(text);
//終了したのでテキストを閉じる
await writable.close();
alert("ファイルに書き込みました。");
} catch (error) {
console.error("ファイルに書き込む際にエラーが発生しました:", error);
}
}
</script>
</body>
</html>
大きなポイントはreadFile関数、writeFile関数で共通で利用するfileHandleというファイル操作ハンドラオブジェクトの取り扱いです。よりこの一連の動きだけにフォーカスすると以下のようになっています。
//ファイルを開くプロンプトを開きファイルハンドラを取得
//取得するときに配列になっているので注意
const [fileHandle] = await window.showOpenFilePicker();
//ファイルハンドラからファイルの読み込みを行う
const file = await fileHandle.getFile();
//ファイルの中身のテキストを取得
const text = await file.text();
//ファイルハンドラから出力処理オブジェクトwriteableを取得
const writable = await fileHandle.createWritable();
//保存するテキストを出力
await writable.write(text);
//保存終了これで保存が完了する
await writable.close();
この記事では最低限ファイルの読み取りと保存だけの紹介にとどめますが、この他保存先の指定やファイル名の取得などさまざまなことが可能です。より詳細な理解をしたい方は、GoogleやMDNの公式ドキュメントを一読することをお勧めします。
FileSystemAccessAPIの活用でできること
File System Access APIの最大の強みは、以下の構成のようなNASやファイルサーバーさえ用意してあれば、簡単にPC間のデータ共有ツールが作れる点にあると考えています。
- セキュリティの懸念があり、インターネット接続ができず、SlackやMicrosoft365のような外部クラウドの導入ができない。
- お情け的に一応、業務担当者内でのデータ共有のために、NASやファイルサーバは用意されている。もっとも内部ネットワーク限定の独自の管理システムの導入はコスト上不可能である。
例えば上記のようなPC作業の業務環境で困っている方はいないでしょうか。File System Access APIを利用してツールを作るスキルさえあれば、このような業務環境の方でも生産性の改善が期待できるツールの導入が自力でできます。
実例:チャットツールを作ってみる
この記事では、実例としてチャットツールの開発例をご紹介します。
ここで紹介するチャットツールは、チャットデータをJSONデータであるチャットルームファイルとして保存して管理しています。何か投稿するたびにチャットルームファイルの中身を更新させるとともに、5秒おきにチャットルームファイルを読み取ることで、他のユーザーが投稿したチャットを表示させてPC間のチャットを成立させております。
ソースコードは以下です。最低限の処理ではあるもののとても長いためボタンを押すと展開できるようにしてあります。また、全体ファイルはGitHubにも公開してありますので、必要に応じてGitHubでの閲覧もご検討ください。
<!DOCTYPE html>
<html>
<head>
<title>File System Access API チャット</title>
</head>
<body>
<button onclick="createChatRoomFile()">チャットルーム作成</button>
<button onclick="openChatRoomFile()">チャットルームに入室</button>
<br>
<h1 id="room-name"></h1>
<p>ユーザー名:<input id="user-name" type="text" /></p>
<p><textarea id="comment-body" style="width: 80%; height: 80px;"></textarea></p>
<p><button onClick="post()">投稿</button></p>
<div id="comment-list"></div>
<script>
//ベースとなるチャットルームのオブジェクト
let chatRoomObject = {};
//チャットルームファイルのロードのインターバルを管理するオブジェクト
let fileLoadInterval = null;
//入室中のチャットルームファイルのハンドラ
let roomFileHandler = null;
/**
* チャットルーム作成処理
*/
async function createChatRoomFile() {
//ファイル保存プロンプトの呼び出し
const option = {
startIn : "desktop",
types: [
{
description: "チャットルームファイル",
accept: { "application/json": [".room"] },
},
],
};
const handler = await window.showSaveFilePicker(option);
//ベースとなるチャットルームオブジェクトの作成
const baseChatRoomObject = {
commentList : []
}
//JSONフォーマットに変換
const json = JSON.stringify(baseChatRoomObject);
//ファイル保存開始
const writable = await handler.createWritable();
await writable.write(json);
//終了したのでファイルを閉じる
await writable.close();
}
/**
* チャットルーム入室のためのチャットルーム読み取り
*/
async function openChatRoomFile() {
//もしも他のチャットルームに入室中であれば更新処理を終了させる。
if (fileLoadInterval !== null) {
clearInterval(fileLoadInterval);
}
//ファイルプロンプトの呼び出し
const option = {
startIn : "desktop",
types: [
{
description: "チャットルームファイル",
accept: { "application/json": [".room"] },
},
],
};
//ファイルの取得
const [handler] = await window.showOpenFilePicker(option);
const file = await handler.getFile();
const json = await file.text();
//ファイル名となっている部屋名を表示
document.getElementById("room-name").innerText = file.name.replace(".room", "");
//取得したチャットルームファイルのjsonを読み取ってチャットルームオブジェクトに登録して表示させる
chatRoomObject = JSON.parse(json)
render()
//投稿を行うために取得したファイルハンドラをグローバル変数に登録
roomFileHandler = handler
//投稿のたびにファイルを保存するが、最初の1回目だけ保存許可の確認がでる。
//タイミングを固定するためにここで空保存しておく。
saveRoomFile()
//以降5秒おきにファイルの再読み取りを行い他ユーザーの投稿した投稿の反映ができるようにする。
fileLoadInterval = setInterval(function(){
(async function(){
const file = await handler.getFile();
const json = await file.text();
chatRoomObject = JSON.parse(json)
render();
})();
}, 5000);
}
/**
* ファイルの書き込み処理
*/
async function saveRoomFile() {
//チャットルームオブジェクトをJSONに変換
const json = JSON.stringify(chatRoomObject);
//ファイル保存開始
const writable = await roomFileHandler.createWritable();
await writable.write(json);
//終了したのでファイルを閉じる
await writable.close();
}
/**
* コメントごとのDOMを作成
*/
function createCommentDom(commentObject) {
//ベースのDOM作成
const commentDom = document.createElement("div");
//ユーザー情報のDOM作成
const userDom = document.createElement("p");
userDom.innerText = commentObject.user;
//投稿日時のDOM作成
const dateTimeDom = document.createElement("p");
dateTimeDom.innerText = commentObject.dateTime
//投稿内容のDOM作成
const bodyDom = document.createElement("p");
bodyDom.innerText = commentObject.body;
//作成したDOMをベースに入れていく
commentDom.appendChild(document.createElement("hr"))
commentDom.appendChild(userDom);
commentDom.appendChild(dateTimeDom);
commentDom.appendChild(bodyDom);
return commentDom;
}
/**
* 現在保持しているチャットルームオブジェクトのコメントを表示
*/
function render() {
//コメント一覧の親DOMの取得
const commentListDom = document.getElementById("comment-list");
//初期化
commentListDom.innerHTML = "";
//チャットルームオブジェクトのコメント一覧を一つずつDOMにあてはめていく。
for (let i = 0; i < chatRoomObject.commentList.length; i++) {
commentListDom.prepend(createCommentDom(chatRoomObject.commentList[i]))
}
}
/**
* 投稿処理
*/
async function post() {
//テキストフォームの投稿内容とユーザー名を取得
const commentBody = document.getElementById("comment-body").value;
const userName = document.getElementById("user-name").value;
//もしも空欄の時は投稿しない
if(commentBody === "" || userName === "") {
return;
}
//現在時刻を取得
const nowDateTime = new Date(Date.now());
const postDateTime = nowDateTime.getFullYear() + "-" + (nowDateTime.getMonth() + 1) + "-" + nowDateTime.getDate() + " " + nowDateTime.getHours() + ":" + nowDateTime.getMinutes() + ":" + nowDateTime.getSeconds();
//チャットルームオブジェクトに上書き
chatRoomObject.commentList.push({
user : userName,
dateTime : postDateTime,
body : commentBody
});
//上書きしたオブジェクトを表示させる
render(chatRoomObject);
//チャットルームファイルも上書きして他のユーザーにも投稿を反映させる
saveRoomFile();
//処理が終了したのでコメントフォームを空にしておく
document.getElementById("comment-body").value = "";
}
</script>
</body>
</html>
終わりに
最後まで読んでいただきありがとうございました。
今回はチャットツールのみご紹介しましたが、そのほかにも共有の連絡ボードなど様々な応用が可能です。
この記事を通じて日々のPC業務の生産性向上の手がかりになれば幸いです。
Photo by Matthew Henry on burst