まあこれは弊社(Claves)での取り組み方(別に相談してないので独断ですらある)です。
多分そのうち陳腐化するので金科玉条のごとき扱いはしない方が良いです。
書くにあたった動機
若い人間がJavaScriptを書く場合に、
- 参照しているものが古い
- 便利なライブラリとかがあるのに再発明とかしてる
- Railsで書く場合にどう書けば良いのか
などが整理されていないと感じた。
都度説明していたが三回をこえて面倒なので書き下すことにした。
JavaScript? TypeScript?
正直モダンに書くのであればJavaScriptでもTypeScriptでも良いと思っている。
構文的にはTypeScriptはモダンなJavaScriptに型、抽象クラスなどが追加されていると思って良いかと思う。
継承とかゴリゴリ書くのであればTypeScriptは便利だし、後述するReactなんかも
TypeScriptで型の恩恵を受けられるので使った方が良いとは思っている。
JavaScriptを覚えるついでに型関連を覚えると思っておけば良いし普通にTypeScriptを使わない理由がないと思う。
import / requireなど
TS/JSどっちも構文は一緒。
今はimport文を使うんじゃないかな
昔は使いたいライブラリなんかがあったときにはscriptタグでそのJSファイルを読み出して、
グローバル名前空間にそのオブジェクトなり関数なりを食わせて、
その後に書いたJavaScriptで呼び出していた。
行儀が悪いし、使用するライブラリもどこか曖昧。
今はnpm経由でインストールして、import文を書いてクラスを呼び出すっていうのがモダンな感じなのではと感じている。
class Hoge {
constructor(){
}
}
export default Hoge
import Hoge from "./Hoge"
new Hoge()
みたいに使う。
export defaultは補完が聞きづらくて批判があったりするけど僕は補完使ってないし知らない。
不便だと言う人は話をまとめて方針を持ってきてほしい。
export defaultを使わない場合はこんな感じ。
class Hoge {
constructor(){
}
}
export Hoge
class Fuga{
constructor(){
}
}
export Fuga
import {Hoge , Fuga} from "./HogeFuga"
new Hoge()
new Fuga()
各種新しめ構文
let
昔はvarで変数宣言をしていた。
letとvarの違いは、varは関数でのみスコープが生成されるが、letはlexicalなスコープになること。
要は波かっこをまたぐとスコープが切れるってこと。
{
let i = 0;
}
console.log(i) // undefined
if(true){
let i = 0;
}
console.log(i) // undefined
(()=>{
let i = 0;
})()
console.log(i) // undefined
プログラムを書くときには必要な範囲でスコープは極力小さくする方が事故が少ないので、letで宣言を行うべき。
昔は下記のようなことがよく起こっていて
for(var i = 0;i < elements.length ;i++){
elements[i].addEventListener(function(){
// クリックされたときにはiは必ずelements.length - 1なのでelements終端要素のvalueのみがalertされる
alert(elements[i].value)
})
}
こういう対応を行なっていたが、面倒でしょ?
for(var i = 0;i < elements.length ;i++){
(function(){
// elementを固定したいので関数を作ってスコープを作成する
var element = elements[i]
element.addEventListener(function(){
// elementはここでイベントを設定したi番目のelements要素になる
alert(element.value)
})
})()
}
特にvarでのみ宣言が可能なシチュエーションはなくはないが日常的にはなくひどく限定的なので、普段はvarではなくletを使うべきである。
アロー関数
JavaScriptのfunctionは複数の役割を持っていて、その中で参照されるthisが都度変わる。
<div id="hoge">Hoge</div>
<script type="text/javascript">
var hoge = document.getElementById("hoge")
hoge.addEventListener("click" , function(e){
// thisにはクリックされたHTMLElementが入る
alert(this.innerHTML)
});
(function(){
// Window Objectが入る
alert(this)
})()
function SomeClass(){
}
SomeClass.prototype.hoge = function(){
this.s = this.s || 0
this.s++;
// thisはインスタンス化された自身を参照する
alert(this.s)
}
let someClass = new SomeClass()
someClass.hoge() // 1
someClass.hoge() // 2
let someClass2 = new SomeClass()
someClass2.hoge() // 1
someClass2.hoge() // 2
</script>
このパターンだけでthisが三つの挙動をしていることになる。
とあるオブジェクト内でイベントハンドラ設定して、そのハンドラ内でthisを使用しようとした場合などは、困ったことが起きる
function SomeClass(){
}
SomeClass.prototype.getInnerHTML = function(){
return "SET STRING"
}
SomeClass.prototype.hoge = function(){
console.log(this) // この時点ではthisはSomeClassのインスタンス
var hoge = document.getElementById("hoge")
hoge.addEventListener("click" , function(e){
// thisにはHTMLElementが入っていて、thisはSomeClassのインスタンスではないため、getInnerHTMLは存在しないためにエラーになる
this.innerHTML = this.getInnerHTML()
});
}
こういう問題には
function SomeClass(){
}
SomeClass.prototype.getInnerHTML = function(){
return "SET STRING"
}
SomeClass.prototype.hoge = function(){
console.log(this) // この時点ではthisはSomeClassのインスタンス
var self = this
var hoge = document.getElementById("hoge")
hoge.addEventListener("click" , function(e){
// thisにはHTMLElementが入っている。
// thisが書き換えられるので、SomeClassのインスタンスはselfに保持してgetInnerHTMLを呼び出す
this.innerHTML = self.getInnerHTML()
});
}
こういう対応をしていたが、大変面倒だ。
対象のHTMLELementはvar hogeに参照されているのだし、それを見れば良い。
踏まえると、イベントハンドラのfunction宣言でthisが書き換えられなければうまく行くことがわかる。
アロー関数 ()=>{/* some implemetation */}
をfunctionの代わりに用いることでthisの参照を変えることなく無名関数が定義できる。
function SomeClass(){
}
SomeClass.prototype.getInnerHTML = function() {
return "SET STRING"
}
SomeClass.prototype.hoge = function() {
console.log(this) // この時点ではthisはSomeClassのインスタンス
var hoge = document.getElementById("hoge")
hoge.addEventListener("click" , (e)=>{
// hogeにはHTMLElementが入っていて、アロー関数を使用したことで、thisはSomeClassのインスタンスのままで、、getInnerHTMLは存在するため動作する
hoge.innerHTML = this.getInnerHTML()
});
}
function構文で無名関数を作り、thisの参照を変えたい、というシチュエーションはほぼない。
普段から無名関数はfunctionではなく()=>{}を使うべきである。
クラス構文
旧来のJSは適当な関数を作って、その中にあるprototypeなる特別なオブジェクトに色々メソッドを追加することで、OOPを実現していた。
継承とかもできたし概ね機能的には問題はなかったが、やっぱりわかりづらい。
function SomeClass(){
}
SomeClass.prototype.hoge = function(){
this.s = this.s || 0
this.s++;
alert(this.s)
}
普通にクラスで描こう
class SomeClass{
hoge(){
this.s = this.s || 0
this.s++;
alert(this.s)
}
}
Webpack
昔はtscコマンドとか使って、何のtsファイルを含めるかとか考えてコンパイル(トランスパイル?まあ面倒だからいいや)してた(遠い目
今はWebpackを使ってコンパイルしている。
Railsの場合にはWebpackerだったりする
Railsの僕のプロジェクトではWebpackerは細かいところまで書きづらいし、WebpackerやめてWebpackでコンパイルしてたりする。
いいところ
- JSもCSS(SASSとかも)も画像もHTMLも一括でコンパイルできる
- 出力するファイル名と、そこに含めるファイルを限定して書くことができる
全部のファイルを一括でコンパイルとかして、使うものだけ使う、っていう発想は僕は嫌い。
必要なときには必要な処理群のみ作りたい。
let entry = {
"front/index" : [
"./frontend/src/sass/front/index.css" ,
"./frontend/src/ts/front/index.ts" ,
] ,
"admin/index" : [
"./frontend/src/ts/admin/crud/index.ts" ,
"./frontend/src/ts/admin/crud/index.sass" ,
"./frontend/src/ts/admin/view_calendar.ts" ,
] ,
}
こんな感じで書いて必要なJSにはそこで必要なtsとsassの処理を含むようにしている感じ。
ディスパッチャーが増えると大変なのは認めるところ。
よくないところ
- なんか遅い。-wしてても変更時に再度コンパイルされるのが遅い。設定ファイルの書き方なんかが悪い気はする。
React
状態とか変数に応じてDOMを変えるとかってときに使いたい。
SPAはかけるけどSPAのためのものではなく、Reactが管理する状態の一部としてURLを扱わせればSPAになるイメージ。
XHRで適当にデータ取ってきて表にするみたいな処理、生のJSで書くと辛い。
そう言うのはReactに投げたい所存。
概要
言葉で説明をすると、
- Reactを継承したクラスを作成する
- Reactを継承したサブクラスはテンプレート(的なもの)を返すメソッドを実装しないといけない
- Reactはpropsとstateという変数を持っている
- propsはインスタンス化されたときに渡されるコンストラクタ引数みたいなもの
- stateは変更可能で、変更されると二番目のテンプレート的なものを再評価して描画をしなおす
コードを見よう
XHRでAPIからリストを取ってきて、Tableタグで表示をするケースを考える。
import * as React from "react"
import * as ReactDom from "react-dom"
interface Props{
}
interface State{
list
}
// Reactを継承したクラスを作成する
// Reactはpropsとstateという変数を持っている
// TypeScriptなら、PropsとStateの型を指定できる。
// Stateはあくまで中の状態でしかないのでそこまでだけど、特に再利用が行われるようなクラスならPropsは指定しておくと、あとで呼び出すときにpropsの指定忘れなどを指摘してくれて嬉しい
class ViewTable extends React.Component<Props , State> {
constructor(props){
super(props)
this.state = {
list : [] ,
}
}
regetList(){
let url = "/api/hoge"
let xhr = new XMLHttpRequest();
xhr.open("GET" , url , true)
let json = encodeURIComponent(this.getJSON());
xhr.addEventListener("readystatechange" , ()=>{
// stateは変更可能で、変更されると二番目のテンプレート的なものを再評価して描画をしなおす
// 変更されたので、もう一度、renderメソッドは実行される。
this.setState({
list : JSON.parse(xhr.responseText);
})
});
xhr.send();
}
// Reactを継承したサブクラスはテンプレート(的なもの)を返すメソッドを実装しないといけない
render() {
return (
<div>
<table className="formList">
// 最初の状態ではlistは空配列なので、mapの中身は実行されない
// buttonをクリックして、listが更新されたのちに実行された場合には、mapの中身が実行される
{this.state.list.map((row)=>{
// propsはインスタンス化されたときに渡されるコンストラクタ引数みたいなもの
// 別にReactComponentを呼び出すときには子要素のProps(この場合はRowProps)に相当する値を渡さないといけない
return <TableRow data={row} />
}}
</table>
<button onClick={this.regetList}/>
</div>
);
}
}
interface RowProps{
data
}
interface RowState{
}
class TableRow extends React.Component<Props , State> {
constructor(props){
super(props)
}
render() {
return (
<tr>
// propsはインスタンス化されたときに渡されるコンストラクタ引数みたいなもの
// 45行目で渡されてくる dataと this.props.dataは同じもの。
{this.props.data.map((dataRow)=>{
<td>{dataRow.value}</td>
}}
</tr>
);
}
}
旧来の書き方と比較して良い点を見てみたい
class ViewList {
getList(){
// Fetch使ったほうがモダンな感じする
let url = "/api/hogelist"
let xhr = new XMLHttpRequest();
xhr.open("GET" , url , true)
let json = encodeURIComponent(this.getJSON());
xhr.addEventListener("readystatechange" , ()=>{
if (xhr.readyState === 4 && xhr.status === 200){
let result = JSON.parse(xhr.responseText);
this.list = result
this.viewList()
}
});
xhr.send('json=' + json);
}
viewList(){
this.targetElement.removeAll() //まあなんか中身消えると思ってくれ
let ul = document.createElement("ul")
this.list.forEach((row)=>{
let li = document.createElement("li")
row.keys.forEach(key)=>{
let value = row[key]
let div = document.createElement("div")
div.innerHTML = value
li.appendChild(div)
})
ul.appendChild(ul)
})
this.targetElement.appendChild(ul)
}
}
こういう書き方だったものが
class ViewList extends React.Component<Props , State> {
getList(){
// Fetch使ったほうがモダンな感じする
let url = "/api/hogelist"
let xhr = new XMLHttpRequest();
xhr.open("GET" , url , true)
let json = encodeURIComponent(this.getJSON());
xhr.addEventListener("readystatechange" , ()=>{
if (xhr.readyState === 4 && xhr.status === 200){
let result = JSON.parse(xhr.responseText);
this.setState({
list : result
})
}
});
xhr.send('json=' + json);
}
render(){
return (
<ul>
{this.list.map((row)=>{
return <li>
{row.keys.forEach(key)=>{
return <div>{row[key]}</div>
}}
</li>
})}
</ul>
)
}
}
こうなる。
何が嬉しいかというと
-
煩雑なHTMLElementのメソッド、プロパティを覚えなくて良い。だいたい HTMLがわかればかける(と思う
-
階層構造とか把握しやすい。
旧来の書き方をして、appendChildとか繰り返すと何がどの要素かわからなくなるし、階層の変更も大変手間で辛い。 -
DOMと処理の一体感は嬉しい。
上記のサンプルだとわからないのがネックだけども。
処理はあくまでReactのクラス内のrender対象のみに影響をしていて、外部に影響を与えないし、renderを行うためのstateやpropsもスコープが閉じているので、書いていて考える範囲が狭いし、書いている内容が外に影響することも考えなくて良いのがとても良い。