9
6

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.

Qiitaのタイトルの最大文字数が気になって調べていたら、デベロッパーツールがクラッシュした件

Last updated at Posted at 2019-05-20

いつものようにQiitaを眺めてたら すごい長いタイトルの記事 を見かけました。

「ラノベでも長いタイトルで売れてるものもあるし、記事もバズるだろ」みたいな不純な理由で、 Qiita記事のタイトルの最大文字数を調べてみると、事件は起こりました。

とりあえず1000文字試してみた

さすがに1000文字は無理だろ? と思い、タイトル欄に1000文字の文字列をコピペ。

公開したくないから『下書き保存』でポチッとな。

……。

…………?

あら? 押せてなかったかな。じゃあもう一回、ポチッとな!

……。

…………。

………………!?

エラーが出たり、保存が完了するならまだしも、Qiitaの出した答えは『無言』。

qiita-error-1.png

どうやらオートセーブでサーバーリクエストしてエラーが返ってきている様子。

レスポンスは、

{"errors":{"title":["は255文字以内で入力してください"]}}

ふむふむ。最大文字数は255文字とな。

当初の目的は達成できたし、そっとブラウザを閉じ……………… れるわけ無いでしょうがぁぁぁ!!

エラーが出るのは仕様どおりとして、『下書き保存』クリックした時にエラーメッセージが出ないのはどうなの? とソースコードを確認してみました。

デベロッパーツールが音を上げる

ずっと一緒に戦ってきたデベロッパーツール……。

qiita-error-2.png

お亡くなりになりました。

ということでローカルにコードをコピーしてきて調査開始!

ちなみに、『限定共有投稿』や『Qiitaに投稿』だとエラーメッセージがツールチップで表示されます。

そのあたりと、『下書き保存』で処理が違うんだろなーと、ぼんやり考えながら調査をはじめました。

まずはJS

JSファイルはMinifyされてて、mapファイルも用意されていなかったので、読むのがしんどかったです。

とりあえず https://beautifier.io/ とかで整形してみました。

行数なんと 62135行!!

「よし! 調査諦めるか!!」

(うそです。もう少しがんばります。)

とりあえずエラーを吐いている部分。

"undefined" != typeof console && void 0 !== console.warn && (a = function(t) {
    console.warn(t)
}, c.isNode && e.stderr.isTTY ? a = function(t, e) {
    var n = e ? "[33m" : "[31m";
    console.warn(n + t + "[0m\n")
} : c.isNode || "string" != typeof(new Error).stack || (a = function(t, e) {
    console.warn("%c" + t, e ? "color: darkorange" : "color: red")
}));

まあ、世の中そんなに甘くはないですね……。

JSから調べるのは一旦置いておいて、他のコード見てみましょう。

エラーのツールチップの要素

『限定共有投稿』と『Qiita に投稿』では正しくエラーが表示されています。

エラー時に表示されるツールチップのHTML要素を見てみます。

<div class="popover editorValidationError fade top in" role="tooltip" id="popover25140" style="top: -60px; left: 805px; display: block;">
    <div class="arrow" style="left: 50%;"></div>
    <h3 class="popover-title" style="display: none;"></h3>
    <div class="editorValidationError_label">
        <i class="fa fa-exclamation-triangle"></i>
    </div>
    <div class="popover-content">タイトルは 255 文字以内で入力してください。</div>
</div>

何個かヒントになりそうなものがありますね。

  • popover25140 というid属性

  • editorValidationError とか fade とかのclass属性

とりあえずid属性で、JSを検索してみる

まあ、だめでした。

サフィックスが数字な時点でお察しでしたが、動的にid振り付けられているようです。

各種class属性で、JSを検索してみる

ヒットするのはツールチップ要素をメイクするコードばかり。

エラーハンドリング系の処理は見つかりませんでした。

保存ボタンの要素

まあ、先にこっち見ろって話なんですけど……。

(実際、なぜかツールチップから見てしまっていたので、記事もそれに従っています)

エラーを表示するための『下書き保存』『限定共有投稿』『Qiita に投稿』ボタンのHTML要素を見てみます。

<div class="editorFooter">
  <div class="editorFooter_save editorSave">
    <div class="editorSave_submit">
      <div class="editorSubmit">
        <div class="btn-group editorSubmit_submitBtnGroup">
          <button class="btn btn-primary editorSubmit_submitBtn" tabindex="12" type="button">
            <span class="editorSubmit_submitBtnLabel active">
              <i class="fa fa-upload"></i>
              <!-- react-text: 138 -->Qiita に投稿
              <!-- /react-text -->
            </span>
          </button>
          <a class="btn btn-primary dropdown-toggle editorSubmit_dropdownToggle" tabindex="13">
            <i class="fa fa-caret-up"></i>
          </a>
          <ul class="list-unstyled editorSubmit_dropdownMenu ">
            <li>
              <a class="editorSubmit_dropdownItem " data-state="save" href="#" tabindex="14">
                <i class="fa fa-check"></i>
                <i class="fa fa-save"></i>
                <!-- react-text: 146 -->下書き保存
                <!-- /react-text -->
              </a>
            </li>
            <li>
              <a class="editorSubmit_dropdownItem " data-state="limited_post" href="#" tabindex="15">
                <i class="fa fa-check"></i>
                <i class="fa fa-lock"></i>
                <!-- react-text: 151 -->限定共有投稿
                <!-- /react-text -->
              </a>
            </li>
            <li>
              <a class="editorSubmit_dropdownItem active" data-state="post" href="#" tabindex="16">
                <i class="fa fa-check"></i>
                <i class="fa fa-upload"></i>
                <!-- react-text: 156 -->Qiita に投稿
                <!-- /react-text -->
              </a>
            </li>
          </ul>
        </div>
      </div>
    </div>
  </div>
  <!-- react-text: 157 -->
  <!-- /react-text -->
  <div class="editorFooter_save editorSave">
    <div class="editorSave_autoSaveNotification">
      <i class="fa fa-check-circle"></i>
      <span class="hidden-xs">自動保存しました</span>
    </div>
  </div>
</div>

ボタンと、(『下書き保存』とか選択するための)ドロップダウンを1つの大きな要素として持っていて、ドロップダウンでアクティブになっているものがボタンとして活性化されているみたいです。

気になるのはボタン要素なのですが、『下書き保存』と、その他では特に属性などは変わりませんでした。

<!-- 『Qiita に投稿』ボタン -->
<button class="btn btn-primary editorSubmit_submitBtn" tabindex="12" type="button">
  <span class="editorSubmit_submitBtnLabel active">
    <i class="fa fa-upload"></i>
    <!-- react-text: 138 -->Qiita に投稿
    <!-- /react-text -->
  </span>
</button>

<!-- 『下書き保存』ボタン -->
<button class="btn btn-primary editorSubmit_submitBtn" tabindex="12" type="button">
  <span class="editorSubmit_submitBtnLabel active">
    <i class="fa fa-save"></i>
    <!-- react-text: 138 -->下書き保存
    <!-- /react-text -->
  </span>
</button>

つまり、HTMLの属性はそのままにJS側で処理を分けているということです。

再びJS

ボタンに対するイベントを確認するべく、JSファイルに戻りました。

とりあえず editorSubmit_submitBtn というclass属性で検索。

 o.default.createElement("button", {
  className: "btn btn-primary editorSubmit_submitBtn",
  tabIndex: "12",
  type: "button",
  onClick: this.props.handleSubmit
})

ありました。 handleSubmit 関数みたいです。

今度は handleSubmit で検索。

key: "handleSubmit",
  value: function() {
    var t = x(regeneratorRuntime.mark(function t(e) {
      return regeneratorRuntime.wrap(function(t) {
        for (;;) switch (t.prev = t.next) {
          case 0:
            if (e.preventDefault(), "save" !== this.state.submitButtonState) {
              t.next = 7;
              break
            }
            return t.next = 4, this.save();
          case 4:
            location.href = this.props.itemPath || "/drafts/".concat(this.props.draftItem.item_uuid), t.next = 8;
            break;
          case 7:
            "creating" !== this.props.draftItem.type || "post" !== this.state.submitButtonState && "limited_post" !== this.state.submitButtonState ? this.validate().length || this.publish() : this.validate().length || this.setState({
              showModal: !0
            });
          case 8:
          case "end":
            return t.stop()
        }
      }, t, this)
    }));
    return function(e) {
      return t.apply(this, arguments)
    }
  }()

ドロップダウンの時に見かけた savelimited_post といったカスタムデータ属性も見えますし、ビンゴのようです。

ただ……、なにやら難解なコード。

case 0:

case 0:
  if (e.preventDefault(), "save" !== this.state.submitButtonState) {
    t.next = 7;
    break
  }
  return t.next = 4, this.save();

これですね。

submitButtonState"save" ではない、つまり『限定共有投稿』や『Qiita に投稿』の場合は『7』に進むそうです。

それ以外、つまり『下書き保存』の場合は『4』に進むそう。

case 7:

先に『限定共有投稿』や『Qiita に投稿』の場合を見てしまいます。

case 7:
  "creating" !== this.props.draftItem.type || "post" !== this.state.submitButtonState && "limited_post" !== this.state.submitButtonState ? this.validate().length || this.publish() : this.validate().length || this.setState({
    showModal: !0
  });

色々と処理があるようですが、入力エラーがある場合にツールチップを出力するような処理になっています。

showModal てなっているのは、昔のUIの名残かな?)

case 4:

では『下書き保存』の続きを。

case 4:
  location.href = this.props.itemPath || "/drafts/".concat(this.props.draftItem.item_uuid), t.next = 8;
  break;

すでに保存済みの場合(URLに item_uuid がある)と初回記事の場合で処理を分けているようですが、処理は『8』に進むようです。

case 8:

case 8:
case "end":
  return t.stop()

とくにエラーハンドリングなく終了ですね。

まとめ

休憩時間を有意義に過ごせた気がする本件。

『下書き保存』の場合にエラーを表示しないのが仕様なのかはわからないですが、ボタンを押して無反応なのは不親切な気がするので、お時間があるときにでも改善をいただけると嬉しいです。

ところで、タイトルがめっちゃ長い記事はバズるんでしょうか?

9
6
2

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
9
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?