ふと、「OSっぽいものって、TypeScriptだけで作れたりしないかな?」と思って、実際に作ってみました。
本物のOSとは違いますし、あくまでブラウザ上で動く なんちゃってOS です。 実行環境はあくまでブラウザですし、「OSって呼ぶには甘すぎる!」というツッコミもあるかもしれません。 でもそこはひとつ、温かい目で見ていただけると嬉しいです (っ´ω`)っ
動作イメージ
本プロジェクトのソースコードは GitHubにて公開しています。 興味がある方は、ぜひそちらもご覧ください!
ローカルにクローンして index.html をブラウザで開くだけで、すぐに動作確認できます。 めんどうな環境構築は不要なので、「ちょっと遊んでみたい」くらいのテンションでぜひどうぞ!
コード解説
① HTMLファイル
ボタンが3つ並んでいるだけのファイルとなります! TSから出力されたmain.jsを最後の方で読み込んでいます。 こちらで、DOMの生成だったり削除だったりを行っているイメージです。
lang= "ja" >
charset= "UTF-8" />
なんちゃってOS
rel= "stylesheet" href= "styles.css" >
id= "desktop" >
id= "clockBtn" > 🕒 時計アプリ
id= "editorBtn" > 📝 テキストエディタ
id= "terminalBtn" > 💻 ターミナル
"module" src= "main.js" >
② ウィンドウのドラッグ・アンド・ドロップ処理
こちらでは、
ウィンドウを掴んだ時(mousedown)
掴んだまま移動している時(mousemove)
ウィンドウを離す時(mouseup)
のイベントリスナーを設定しています。 このあたりは画面の座標を基に、DOM自身の座標を変更することで移動させています。offsetだったり、clientだったり、頭がこんがらがりましたが、なんとか実装できました。
また、ドラッグ中の判定は isDragging という変数に格納していたのですが、 「Reactの useState やVueの ref のようにステート管理してくれるものってやっぱり便利なんだなー」と、つくづく感じました。
const makeDraggable = ( win : HTMLElement , titleBar : HTMLElement ) => {
let offsetX = 0
let offsetY = 0
let isDragging = false
titleBar . addEventListener ( ' mousedown ' , ( e ) => {
isDragging = true
offsetX = e . clientX - win . offsetLeft
offsetY = e . clientY - win . offsetTop
win . style . zIndex = ` ${ ++ zIndexCounter } `
})
document . addEventListener ( ' mousemove ' , ( e ) => {
if ( ! isDragging ) return
win . style . left = ` ${ e . clientX - offsetX } px`
win . style . top = ` ${ e . clientY - offsetY } px`
})
document . addEventListener ( ' mouseup ' , () => {
isDragging = false
})
}
③ 各種アプリのウィンドウの生成
こちらでは各種アプリの大枠となるウィンドウを生成しているだけです。 「タイトルバー」だったり、「削除ボタン」だったりですね。
なお、引数contentHTML にアプリのUIがテキストとして入るイメージです。
const createWindow = ( title : string , contentHTML : string ): HTMLElement => {
const win = document . createElement ( ' div ' )
win . className = ' window '
Object . assign ( win . style , {
top : ' 100px ' ,
left : ' 100px '
})
const titleBar = document . createElement ( ' div ' )
titleBar . className = ' titleBar '
titleBar . innerHTML = ` ${ title } `
const closeBtn = document . createElement ( ' button ' )
closeBtn . className = ' close-btn '
closeBtn . textContent = ' × '
closeBtn . onclick = () => win . remove ()
titleBar . appendChild ( closeBtn )
const content = document . createElement ( ' div ' )
content . className = ' content '
content . innerHTML = contentHTML
win . append ( titleBar , content )
document . getElementById ( ' desktop ' )?. appendChild ( win )
makeDraggable ( win , titleBar )
return content
}
④ 時計アプリ
③で解説したウィンドウ生成関数 createWindow で大枠を作成しています。
clockEl.textContent に現在時刻を入れることで時計としてのUIになっています!
const launchClock = () => {
const content = createWindow (
' 時計アプリ ' ,
`🕒 00:00:00
`
)
const clockEl = content . querySelector ( ' .clock ' ) as HTMLElement
clockEl . textContent = ' 🕒 ' + new Date (). toLocaleTimeString ()
setInterval (() => {
clockEl . textContent = ' 🕒 ' + new Date (). toLocaleTimeString ()
}, 1000 )
}
⑤ テキストエディタアプリ
③で解説したウィンドウ生成関数 createWindow で大枠を作成しています。
ここは横着してローカルストレージに登録するようにしています。 やったことないですが、Electronとか使えば、実際にOS上にあるファイルを使って運用することができるんですかね。。。?
const launchEditor = () => {
const content = createWindow (
' テキストエディタ ' ,
`
保存する
`
)
const editor = content . querySelector ( ' #editorArea ' ) as HTMLTextAreaElement
editor . value = localStorage . getItem ( ' editorContent ' ) || ''
const saveBtn = content . querySelector ( ' #saveBtn ' ) as HTMLButtonElement
saveBtn . onclick = () => {
localStorage . setItem ( ' editorContent ' , editor . value )
alert ( ' 保存したよー! ' )
}
}
⑥ ターミナルアプリ
③で解説したウィンドウ生成関数 createWindow で大枠を作成しています。
terminalOutput が出力結果、terminalInputが入力フォームになります。
コマンドは4種類だけあり、以下出力結果が表示されるようになっています。
help
echo
echo 何らかの文字列 で、そのまま入力した文字列が表示されます
date
clear
ターミナルっぽくするために、黒緑のいわゆるなデザインにして、enter キーでコマンド実行できるようになっています。
なお、筆者はこんな感じでターミナルを使ってます。 なにかで詰まっていても、海の生き物がいつも寄り添ってくれています萌 (右側にgitのブランチ名を表示させているのですが、これ結構便利です。作り方は調べてみてください!) ↓
⑦ イベントリスナーを追加
各種イベントを登録しています。
document . getElementById ( ' clockBtn ' )?. addEventListener ( ' click ' , launchClock )
document . getElementById ( ' editorBtn ' )?. addEventListener ( ' click ' , launchEditor )
document
. getElementById ( ' terminalBtn ' )
?. addEventListener ( ' click ' , launchTerminal )
⑧ 以上!!!
終わりに
本記事はふと「OSっぽいことをTypeScriptだけでやってみたい!」と思って始めただけです。 でもその中に、UI設計・アーキテクチャ・UXなど多くの要素が詰まっていて、振り返ってみると、意外と深い学びがあったなと感じています。
OSって銘打ってみましたが、結局のところ DOM を生成したり、消したりしているだけです。 しかし、その「だけ」をどこまで広げられるかを考えたのは、結構楽しかったです。
例えば、ウィンドウの z-index管理 。
ドラッグ処理ひとつ取っても、座標の計算、クリック位置の記憶、重なり順の制御、etc… 普段OSが勝手にやってくれている「地味だけど重要な処理 」に、真正面から向き合うことができました。
加えて、React の再レンダリングや状態管理がないので、自分で「いつ」「どこを」更新するかを全部決める必要があります。 実務では DOM をそのまま JS で生成することはあまりないので、このあたりを再認識できたのは良かったです。
P.S. ターミナルに echo と打ち込んで、画面に文字が出るだけでちょっと嬉しかった。。。
最後まで読んでくださりありがとうございます。
もし記事が参考になったら、「いいね」してもらえるとすごく励みになります! また、内容に誤りや気になる点があれば、遠慮なくご指摘していただけると嬉しいです!
他にもいろいろな記事を投稿しているので、もしよかったら見てみてください!
ではでは!