21
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Elm 2Advent Calendar 2017

Day 8

[Elm] 危険🐐スクリプト芸

Last updated at Posted at 2017-12-07

本記事では、Elm においてデメリットが大きくふつうは使うべきでない「スクリプト芸」をご紹介します。
原則として「スクリプト芸」は使わないに越したことはないですが、そのデメリットを深く認識し、非常に注意深く使う限りにおいて、ごくまれにメリットがデメリットを上回る場合があります。

基本的に飛び道具的な内容のため、乱用は絶対にさけましょう。
何が起きても知りません。
以下の内容はバージョン0.18 を想定しています。
(Elm 0.19 では予定通りスクリプト芸が禁止されました。 Elm0.19で生HTMLを埋め込む場合は別の方法が必要です。)

スクリプト芸とは

Elm で script タグを動的に生成し、その中でJSを呼び出す手法を本記事では「スクリプト芸」と呼ぶことにします。

例えば、以下の View は、実行時 Web コンソール上に Hi! という文字列を表示します。

view : Model -> Html Msg
view model =
    div
        []
        [ node "script"
            []
            [ text """
console.log('Hi!');
            """
            ]
        ]

副作用起こしまくりでヤバイ臭いしかしません。
Elm の html ライブラリは気軽にスクリプト芸が乱用されないように、script : List (Attribute msg) -> Html msg という関数を用意していません。
その親切心を裏切って node 関数で script タグを生成しています。

この場合は下記のタグが生成されます。

<script>
console.log('Hi!');
</script>

生成された script タグが実行されることで、Web コンソールに Hi! という文字列が表示されます。
当然、Elm は script タグ内で何が起きたか知りませんから、たとえば Elm に知られずにこっそりどこかのタグを細工したりすることもできます。
どう考えてもリスクが大きすぎます。 使わないでください。

応用例

もう1度言いますが、ふつうは使うべきではありません。
ですが、ごくごくたまに、デメリットを認識したうえで使うと便利な場合があります。

スクリプト芸の応用例の1つとして、さっき作った elm-raw-htmlパッケージをご紹介します。
elm-raw-html を使うと、HTML文字列をElmプログラム内でそのままHTMLとして描画できます。

div
    [ class "wrapper" ]
    [ RawHtml.render <|
        RawHtml.iAmSureThatThisRawHtmlStringDoesNotCauseXSS """
        <div class="child">
            <input type='text' class="foo">
        </div>
        """
    ]

たとえば、こんなViewを定義すると、以下のHTMLが描画されます。

<div class="wrapper">
    <div class="child">
        <input class="foo" type="text">
    </div>
</div>

仕組みは簡単で、以下のように script タグが自分自身の親の innerHTML を書き換えているだけです。

render : RawHtml -> Html msg
render (RawHtml str) =
    Html.node "script"
        []
        [ Html.text <| """
var scripts = document.getElementsByTagName("script");
var thisTag = scripts[scripts.length - 1];
thisTag.parentNode.innerHTML=\"""" ++ escapeQuot str ++ "\";"
        ]

こんなのどう考えてもXSSの温床になります。
また、詳細は説明を省きますが
「あれ? モデルの状態が変わって class="wrapper" のタグが消えて別のViewを描画したのになぜか class="child" が画面に残ってるよ?」
みたいなことも容易に起きます。
ふつうは使ったらダメです。

そもそも、たいていはElmを使っている限り、HTMLを文字列として保持する必要がないでしょう。

  • Html msg の型の値として保持するか
  • 内容を表現できる型 a とそのレンダリング関数 a -> Html msg を定義して、aとしてデータを保持するか

でふつうは対応できるはずです。

危険を十分に理解したうえで使えばまれに便利な時がある

僕は1度だけこの手法で生のHTMLをレンダリングしたことがありますが、それは以下の状況でした。

  • HTMLメールでシステムが送信する内容をプレビューとして画面上で確認させたい
  • HTMLメールはバックエンドが送るので、バックエンド側に文字列の生HTMLを保持されている

この場合もあえてスクリプト芸に頼らないでも解決する方法はあります。

  1. バックエンド側がHTMLメールの内容をモデル化して保持しておき、そのモデル構造を JSON で Elm に渡して Elm がレンダリングする
    • もちろん、実際にHTMLメールを送る際に、同等の処理をバックエンド側でも実行する必要があります
  2. バックエンド側は文字列でHTMLを保持しているが、Elmがその文字列をパースして Html msg に変換する
  3. バックエンド側が別途独立したHTMLページを返すAPIを用意しておき、 Elm が iframe 内にそのページを表示する

確かにどちらも危険なスクリプト芸に頼らずに実現できますが、あまりにも工数がかかりすぎます。
そこで、最大限の注意をはらったうえで、工数が少ないスクリプト芸を使った方法を採用しました。

まとめ

スクリプト芸は基本的に気軽な気持ちで手を出してはダメです。
でも、ごくまれに役に立つこともあります。
必要な場面で注意深く使う限りにおいては、Elm の良さと JS の良さを両立できてハッピーになれます。
でも、やっぱり怖いから使わないに越したことはないテクニックです。

P_20170816_152145_vHDR_On.jpg

21
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?