20191202のHTMLに関する記事は9件です。

webでバーコードリーダーを使用時の操作性を向上させるためHTM

使用時の操作性を向上させるためHTMです

webサイトを作成するときに本を登録するためにバーコードリーダで読み込んだものを自動的に送信するため必要なことを調べたときにibisBrowserDXRのページを参考にしました
http://ibis.ne.jp/browser/dxr/pc/exp_html.jsp?z=440&zp=

<form  method="post" action="">
        <label for="isbn">ISBNコード</label>
    //autofocusでページを開いたときにウェブページが表示された際に、指定した入力欄にカーソルが当たって自動的にフォーカスされます。
        <input type="text" name="isbn" placeholder="バーコード読み取り" autofocus >
    //読み込み後ボタンを押す。
        <input type="submit" value="送信" readerPress="on">
    </form>

これでバーコードリーダーで読み込んだ時とキーボードで入力したときの2パターンで使用できるようになりました

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

webでバーコードリーダーを使用時の操作性を向上させるためHTML

使用時の操作性を向上させるためHTMLです

webサイトを作成するときに本を登録するためにバーコードリーダで読み込んだものを自動的に送信するため必要なことを調べたときにibisBrowserDXRのページを参考にしました
http://ibis.ne.jp/browser/dxr/pc/exp_html.jsp?z=440&zp=

<form  method="post" action="">
        <label for="isbn">ISBNコード</label>
    //autofocusでページを開いたときにウェブページが表示された際に、指定した入力欄にカーソルが当たって自動的にフォーカスされます。
        <input type="text" name="isbn" placeholder="バーコード読み取り" autofocus >
    //読み込み後ボタンを押す。
        <input type="submit" value="送信" readerPress="on">
    </form>

これでバーコードリーダーで読み込んだ時とキーボードで入力したときの2パターンで使用できるようになりました

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者によるプログラミング学習ログ 174日目

100日チャレンジの174日目

twitterの100日チャレンジ#タグ、#100DaysOfCode実施中です。
すでに100日超えましたが、継続。

100日チャレンジは、ぱぺまぺの中ではプログラミングに限らず継続学習のために使っています。

174日目は

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

大きく表示するタイマーが欲しかったからdivのborderで数字を描いてみた

はじめに

divのborderで数字を描いてみた話です。

やりたかったこと

小学校の教室でタイマーを大きく表示したい。

背景

月に数回程度、小学校にお邪魔してK3Tunnelを使ったプログラミング授業をお届け しています。

普通の授業時間にお邪魔するので、時間厳守。
時間厳守しないと給食時間や休み時間が短くなってしまいます。
出張授業なので、45分×2コマの1本勝負。
続きはまた今度ね。は許されません。

私たちがやっているプログラミング授業は、シナリオが用意された体験モノです。そのため、ここまでは到達してほしい、ここの面白さを感じてほしいというポイントがあります。

なので、時には、作業途中の子どもたちの手を止めて「次」に移ってもらったり。
ゆっくりゆっくりやっている子のペースアップを促したりすることもあります。

が。こんな感じのこどもたちを目にしたら。
kids_doro_asobi_s.png

それがなかなか難しい。

言いにくいことはAIに言わせる。

これ。エンジニアの常識です。

時間が来たよと機械的に伝える。

終了時間を指定して時計を見てもらうだけでは弱い。
タイマーをセットして突然音を鳴らすのは心臓に悪い。
子どもたち自身のペースメイクを促したい。

タイマーを大きく誰もが見える場所に表示するのがベスト。

というのが私の結論でした。

対応方針

物理的な時計やタイマーアプリなどには目もくれず、つくる。いいから作る。
nichiyoudaiku_s.png

作成方針

実装に使うモノについて学習しない。
なぜならJavaScript/html/cssで何か作りたい熱が高まったのがトリガーで、「今日は時間が取れる」休日がぽっと生まれたときに作ることにしたから。

1日でぱぱっとつくれることが重要。
慣れない言語に手を出すなど論外。
初めてのライブラリやフレームワークに挑戦するとかもなし。

だがしかし。つくる。

つくったもの

できあがりはこちらで確認できます。
https://cocoakamen.github.io/cocoa-timer/

構想する

学習せずに作るといっても、特大フォントで時間表示するだけでは、いくらなんでも芸がない。
少しくらいはチャレンジ要素を入れたい。

と。

しばし考えて、思いついた。

ひょっとして。

divのborderを太くしたら、デジタル時計っぽくなるのでは?

とりあえず、このHTMLで、上下と左右で色を変えたdiv(縦横100pxで太さ20px)を表示してみた。

<html>
  <div style="border-color:black gray; height:100px; width:100px; border-style:solid; border-width:20px;"></div>
</html>

結果。
timer-try.jpg

デジタル数字ができそうな気がする見た目をしている。

作る

divを並べる

こんな感じにdivを並べることに。太枠のボックスが数字を描こうとしているdiv。
timer-div.jpg
各数字、二つのdivを使うイメージ。
箱の中に書いてあるのはid名。
id名が書いていないのは、スペース調整用だったりなんだり。
classがいっぱいあるのは、スタイル設定するための色々。
もしかしたら使っていないclassもあるかもしれない。。

数字を表示しているところのHTMLはこちら。
上下それぞれ横に並べるのが少しややこしい。

index.html
<div class="timer-container">
  <div class="number-row">
      <div class="digital-number number-left number-up" id="number-minute-left-up"></div>
      <div class="digital-blank-separater"></div>
      <div class="digital-number number-right number-up" id="number-minute-right-up"></div>
      <div class="digital-separater" id="separater-up"></div>
      <div class="digital-number number-left number-up" id="number-second-left-up"></div>
      <div class="digital-blank-separater"></div>
      <div class="digital-number number-right number-up" id="number-second-right-up"></div>    
  </div>
  <div class="number-row">
      <div class="digital-number number-left number-down" id="number-minute-left-down"></div>
      <div class="digital-blank-separater"></div>
      <div class="digital-number number-right number-down" id="number-minute-right-down"></div>
      <div class="digital-separater" id="separater-down"></div>  
      <div class="digital-number number-left number-down" id="number-second-left-down"></div>
      <div class="digital-blank-separater"></div>
      <div class="digital-number number-right number-down" id="number-second-right-down"></div>    
  </div>  
</div>

borderのスタイルを決めていく

文字の太さ

cssの:root 疑似クラスカスタムプロパティで定義。

timer.css
:root {
 --number-line-width: 30px;
}

digital-numberクラスにベースの設定をカキカキ。(数字を描くdiv要素は全部digital-number)

timer.css
main .digital-number{
  border: solid;
  width:200px; 
  height:170px;
  border-width: var(--number-line-width); /* :root要素で設定しているカスタムプロパティ参照 */
  box-sizing: border-box; /* パディングとボーダーを幅と高さに含める */
}
/* 全体的にmain要素で囲っているのでmainがついてます */

上と下が重なり合うところは、それぞれ幅を2分の1にします。
こういうとき、カスタムプロパティを定義しておくと便利です。

timer.css
main .number-up {
  border-bottom-width: calc( var(--number-line-width)/2);
}

main .number-down {
  border-top-width: calc( var(--number-line-width)/2);
}

文字の色

文字の色は、JavaScriptの中で指定。
コンストラクタ定義でベタ打ち定義。

timer.js
function CocoaTimer() {
  this.numberColor = 'black'; // 文字の色
  this.GRAY_OUT_COLOR = '#EFEFEF'; // 線がない部分の色
  this.remainingTime = 0;
  this.timer = {};
};

数字をかく

表示する数字ごとに色を指定します。
こんなイメージ。
number_color.jpg

あとは地道に定義していきました。引数は数字を描くdivの上と下の要素。

timer.js
CocoaTimer.prototype.drawNumber = function(upElement, downElement, num) {
  // 中略
  // 数字
  switch(num) {
    case 0:
      upElement.style.borderColor = this.numberColor;
      upElement.style.borderBottomColor = this.GRAY_OUT_COLOR;
      downElement.style.borderColor = this.numberColor;
      downElement.style.borderTopColor = this.GRAY_OUT_COLOR;
      break;
    case 1:
      upElement.style.borderColor = this.GRAY_OUT_COLOR;
      upElement.style.borderRightColor = this.numberColor;
      downElement.style.borderColor = this.GRAY_OUT_COLOR;
      downElement.style.borderRightColor = this.numberColor;
      break;
    // 中略 以下9まで地道に定義
      break;
    default:
      break;
  }
};

時間は普通に計算

時間はミリ秒で計算。
残り時間のミリ秒から、分、秒それぞれ1の位と10の位の数字を求める計算をsetInterval使って実行して再描画。詳細は省略。
時間になったら点滅。今のところ音はなりません。

ソースはこちらにも

https://github.com/cocoakamen/cocoa-timer

子どもたちの反応

授業で実際に使った時の反応はおおむね「やばい爆弾だ。爆弾。」
jigenbakudan_s.png
なんですが、残念ながら、爆弾イフェクトは実装されておりません。

将来ゲームクリエイターになりたいんだ♪という子に、あのタイマー、私がつくったんだよと自慢したところ、尊敬のまなざしを得られた気がします。

もちろん、狙い通り、子どもたち自身が残り時間を意識している様子も見受けられ、ほくほくです。

まとめ

タイマーを導入したら、子どもたちのフォローをするときに、時間を気にする脳ミソ使用量が減った気がします。ワークショップの進行がとてもラクになるのでお勧めです。

ちなみに、自分の子の小学校授業参観にいったら、物理タイマーを実物投影機で大きいディスプレイに映してました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【WEB版 WEAR】position: sticky で似ているアイテム検索の使いやすい UI/UX を実現する

この記事はZOZOテクノロジーズ #2 Advent Calendar 2019 19日目の記事になります。
昨日は、@saitoryuji さんによる「新卒2年目のエンジニアが単体テストをやってみる」でした。

また、今年は全部で5つのAdvent Calendarが公開されています。

はじめに

最近、スマートフォンのWEB版 WEARで、コーデ画像をもとにAIがアイテムを検出し、さらにそのアイテムに似ているアイテムを検出して一覧で表示する機能をリリースしました:tada:
真似したいコーデの服が簡単に見つかる!

この機能をWEBで実装するにあたり、使いやすいUI/UXを目指した奮闘記を紹介します!

こんなのをやりたい

まず、大まかな要件は以下の通りです。

  1. コーデ画像の左下の検索アイコンをタッチすると検索スクリーンが下から上がってくる
  2. コーデ画像からアイテムが検出された箇所を「◯」で表示する
  3. スクリーン下部に、選択されたアイテムの「似ているアイテム」の一覧を表示する
  4. スクロールすると「似ているアイテム」の一覧が上がってくる
  5. 「似ているアイテム」の一覧はヘッダーが固定され、一覧内でスクロールできる

コーデ詳細.png → デフォルト.png → スクロール.png → 一覧表示.png

最初にこの要件を聞いたとき、経験上難しそうだなーと感じたのは、
windowのスクロールとは別に、一覧内もスクロールできるようにしないとけないところでした。
実装自体は可能なのですが、スクロールできる要素の中にスクロールできる要素を配置すると、スクロールしたい方じゃない方がスクロールされるなど、とても使いにくいイメージがありました。。。:sweat:

今回は上記の要件のうち、1、4、5の部分について説明します。
※以下に出てくるソースコードは、説明用のサンプルです。

ボツになった案

それでも、まずは思いついたままに、以下のような構成で実装してみた。
(最初に言っておきますが、これはボツです笑)

実装

HTML
<div id="searchScreen" style="position: relative; transform: translateY(0px);">
    <div id="image" style="position: absolute;">...</div>
    <div id="list">
        <div id="listHead"></div>
        <div id="listBody" style="height: 812px; overflow-y: scroll;">
            <ul>
                <li></li>
                :
            </ul>
        </div>
    </div>
</div>

See the Pen Sample1 by mitanih (@mitanih) on CodePen.

#searchScreen
コーデ画像の検索アイコンボタンのクリックイベントで、下から上がってくる検索スクリーン
下から上げる処理として、CSSアニメーションでtransformtranslateY(100%)からtranslateY(0px)に切り替えて表現する
※marginの切り替えでも表現できるが、残像ができたり動きがカクつくなどの問題があった

#image
検索スクリーン内上部のイメージエリア
windowがスクロールされても固定したいので、position: absoluteを指定して、常に画面の同じ位置にいるようJavaScriptで制御する
※親要素にtranslateを指定しているのでposition: fixedは使えない

#list
検索スクリーン内下部の一覧

#listHead
一覧のヘッダー

#listBody
一覧のボディ
一覧内をスクロールさせるためheightを設定し、overflow-y: scrollを指定

問題点

触ってみて、やっぱり使いにくかった。。。:sweat_smile:
iOSのSafariで動作確認した範囲ですが、windowをスクロールさせて一覧を上に持っていきたいのに、一覧内がスクロールされてしまったり、慣性スクロールの影響なのか、windowがスクロールして完全に止まらないと、一覧内がスクロールできないなどの現象も起きた。
スクロールしやすいように工夫して、一覧が一番上まで来ていなければ一覧内はスクロールできないようJavaScriptで制御も入れてみたが、スムーズな動きとはいかなかった。
あと、#imageの位置を固定するためスクロールの度にJavaScriptで位置を計算して設定したが、フラフラ動いてなんか変。
何より無理し過ぎな気がしました。。。:sweat:

採用した方法

いや、もっといい方法はあるはずだ。他のサービスでこんなぎこちないの見たことないぞ。。。:thinking:
と、いろいろ調べていて、position: stickyに辿り着きました!
Excelの行固定みたいな機能が簡単に実現できるあのスタイルです。

position: stickyの説明
https://developer.mozilla.org/ja/docs/Web/CSS/position

メジャーなiOS、Androidのブラウザ対応も問題なさそう!
https://caniuse.com/#search=sticky

実装

HTML
<div id="searchScreen" style="position: relative; transform: translateY(0px);">
    <div id="image" style="position: sticky;">...</div>
    <div id="list">
        <div id="listHead" style="position: sticky;"></div>
        <div id="listBody">
            <ul>
                <li></li>
                :
            </ul>
        </div>
    </div>
</div>

See the Pen Sample2 by mitanih (@mitanih) on CodePen.

細かい調整は省略しますが、要は#imageと#listHeadにposition: stickyを設定するだけで解決しました!
windowのスクロールで#listが上に来るまでは、#imageがstickyしてくれる。#listが上までくると#listHeadがstickyし、windowのスクロールでそのまま#listBodyが下まで見られる。
まさに理想的なスタイル!
無理していない実装なので動きもスムーズです!:blush:

まとめ

JavaScriptやCSSのプロパティを覚えると、自力でゴリゴリ書けちゃうんですが、実機で確認すると動きがぎこちなくてなんか無理してる感じがすることってあると思います。
そんなときは視点を変えてもっとシンプルな方法を探すと、よりよい解決策が見つかるということに気付かされます。
プログラミングって、そんなことのくり返しのような気がする。。。

明日は、@ikkou さんによる「WebAR の現状確認 2019 Winter」です。
お楽しみに~!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Flaskで自然言語処理を使ったWebアプリを作りherokuでデプロイした

説明

Flaskで英文を入れると時制を解析するWebアプリを作りました。

環境

Python3がインストールされていることとGitHubで作成していることを前提に進めます。

Flaskアプリケーションの作成

ディレクトリ構成

.
├── static
│   ├── js
│   │   └── main.js
│   └── css
│       └── style.css
├── templates
│   └── index.html
├── main.py
└── analyzer.py

htmlを作成

templates/index.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>TENSE ANALYZER</title>
</head>

<body>
    <div id="container">
        <div id="title">
            <h1>TENSE ANALYZER</h1>
        </div>
    </div>
</body>

</html>

Flaskのインストール

pip install Flask

main.pyを作成

main.py
from flask import *

app = Flask(__name__)


@app.route("/")
def init():
    return render_template('index.html')


if __name__ == "__main__":
    # debugはデプロイ時にFalseにする
    app.run(host='127.0.0.1', port=5000, debug=True)

アプリケーションを立ち上げる

python main.py

http://127.0.0.1:5000/ を開いて"TENSE ANALYZER"の文字が表示されていれば成功。

フォームを作る

htmlにフォームを追加

templates/index.html
...
<div id="container">
        <div id="title">
            <h1>TENSE ANALYZER</h1>
        </div>

        <div id="analyze">
            <h2>英文を入力してEnterキーを押してください</h2>
            <form id="form" action="/input" method="post" name="formStr">
                <input id="textField" class="input" type="text" name="str" value="{{value_str}}">
                <input class="submit" type="submit" name="" value="送信">
            </form>
        </div>

        <div class="result">
            <h2>結果</h2>
            <p>{{value_str}}</p>
        </div>
    </div>
...

formタグにaction="/input"
inputタグにvalue="{{value_str}}", name="str"
の属性を追加。

フォームから送信した値を受け取る処理をmain.pyに追加

flask_socketioをインストール
pip install flask_socketio

main.py
from flask import *
from flask_socketio import SocketIO

app = Flask(__name__)
socketio = SocketIO(app, async_mode=None)  # [変更] socketioを追加

value_str = ""  # [変更] value_strを定義


@app.route("/")
def init():
    # value_strをhtml側で使えるようにする
    return render_template('index.html', value_str=value_str)

#  action="/input"
@app.route("/input", methods=["GET", "POST"])
def get_form():
    global value_str
    # フォームの値を受け取る
    try:
        value_str = request.form['str']  # name="str"のinputタグの値を取得
    # ページ読み込み時
    except:
        value_str = ""
    # index.html内にて{{ value_str }}で挿入できる
    return render_template('index.html', value_str=value_str)


if __name__ == "__main__":
    # debugはデプロイ時にFalseにする
    socketio.run(app, host='127.0.0.1', port=5000, debug=True)  # [変更] 


index.htmlの<p>{{value_str}}</p>の部分にフォームから送信した値が表示されれば成功。

NLPで解析部分を実装

今回はNLTK(Natural Language Toolkit)というライブラリを使用しました。

NLTKをインストール

pip install nltk

analyzer.pyを作成

analyzer.py
analyzer.py
import re
import nltk
nltk.download('punkt')
nltk.download('averaged_perceptron_tagger')


class Analyzer:
    def tense_analyze(self, value_str):
        if(len(value_str) > 0 and "." != value_str[-1]):
            value_str = value_str+"."
        s = value_str
        morph = nltk.word_tokenize(value_str)
        result = nltk.pos_tag(morph)
        tense = ''
        q = 0
        will = re.search('will', value_str)
        for i in range(len(s.split())):
            b = int(i)
            if result[b][1] != 'VBP' and result[b][1] != 'VBZ' and result[b][1] != 'VBG' and result[b][1] != 'VB' and result[b][1] != 'VBD' and result[b][1] != 'VBN':
                q = q+1
            elif (result[b][1] == 'VBP' or result[b][1] == 'VBZ' or result[b][1] == 'VBD' or result[b][1] == 'VB') and (result[b][0] != 'am' and result[b][0] != 'are' and result[b][0] != 'was' and result[b][0] != 'were' and result[b][0] != "be" and result[b][0] != 'is'):
                if result[b][0] == 'have' or result[b][0] == 'has' or result[b][0] == 'had':
                    if result[b+1][1] == 'VBN' or result[b+2][1] == 'VBN' or result[b+1][1] == 'VBD' or result[b+2][1] == 'VBD':
                        if result[b+1][0] == 'been':
                            if result[b+2][1] == 'VBG' or result[b+3][1] == 'VBG':
                                tense = 'PerfectContiuous'
                                break
                            else:
                                tense = 'Perfect'
                                break
                        else:
                            tense = 'Perfect'
                            break
                    else:
                        tense = 'Simple'
                        break
                else:
                    tense = 'Simple'
            elif result[b][1] == "VBP" or result[b][0] == "was" or result[b][0] == "were" or result[b][1] == "VBZ" or result[b][0] == "be" or result[b][0] == "is" or result[b][0] == "VBD":
                if result[b+1][1] == 'VBG':
                    tense = 'Continuous'
                    break
                else:
                    tense = 'Simple'
                    break

        for j in range(len(s.split())):
            c = int(j)
            if tense == 'Simple':
                if result[c][1] == 'VBD':
                    tense = 'PastSimple'
                    break
                elif result[c][1] == 'VBD' and (result[c-1][1] == 'VBP' or result[c-1][1] == 'VBD' or result[c-1][1] == 'VBZ'):
                    tense = 'PastSimple'
                    break
                elif (not will) and result[c][1] == "VBP" or result[c][1] == "VBZ":
                    tense = "PresentSimple"
                elif will and (result[c-1][1] != 'VBP' or result[c-1][1] != 'VBD' or result[c-1][1] != 'VBZ'):
                    tense = "FutureSimple"
                else:
                    q = q+1
            elif tense == "Perfect":
                if result[c][0] == 'had':
                    tense = 'PastPerfectSimple'
                    break
                elif not will and (result[c][0] == "have" or result[c][0] == "has"):
                    tense = "PresentPerfectSimple"
                elif will and result[c][0] == "have":
                    tense = "FuturePerfectSimple"
            elif tense == "Continuous":
                if result[c][0] == 'was' or result[c][0] == 'were':
                    tense = 'PastContinuous'
                    break
                elif (not will) and result[c][1] == "VBP" or result[c][1] == "VBZ":
                    tense = "PresentContinuous"
                elif will:
                    tense = "FutureContinuous"
                else:
                    q = q+1
            elif tense == "PerfectContiuous":
                if result[c][0] == 'had':
                    tense = "PastPerfectContinuous"
                elif not will and (result[c][0] == 'have' or result[c][0] == "has"):
                    tense = 'PresentPerfectContinuous'
                elif will:
                    tense = "FuturePerfectContinuous"
            else:
                q = q+1
        value_str.strip()

        return value_str, tense

Analyzerクラスに文章の時制の手がかりとなる部分と時制を返す関数def tense_analyze(self, value_str):を作成。
アルゴリズムの説明等は省略します。

main.py内でanalyzer.pyをimportして使う

main.py
from flask import *
from flask_socketio import SocketIO
# analyzer.pyをimport
import analyzer

app = Flask(__name__)
socketio = SocketIO(app, async_mode=None)

# [削除]value_str = ""
result = []  # [変更] 結果を格納する配列


@app.route("/")
def init():
    # value_strをhtml側で使えるようにする
    # [変更] value_str -> result
    return render_template('index.html', result=result)

#  action="/input"
@app.route("/input", methods=["GET", "POST"])
def get_form():
    global value_str
    # フォームの値を受け取る
    try:
        value_str = request.form['str']  # name="str"のinputタグの値を取得
    # ページ読み込み時
    except:
        value_str = ""

    # tense_analyze関数の実行
    Analyzer = analyzer.Analyzer()
    value_str, tense = Analyzer.tense_analyze(value_str)

    # 結果をresultに追加
    appendList = []
    if(value_str):
        appendList.append(value_str)
    if(tense):
        appendList.append(tense)
    result.append(appendList)

    # [変更] value_str -> result
    # index.html内にて{{ result }}で挿入できる
    return render_template('index.html', result=result)


if __name__ == "__main__":
    # debugはデプロイ時にFalseにする
    socketio.run(app, host='127.0.0.1', port=5000, debug=True)  # 変更


index.htmlでresultを表示する。

Python用のテンプレートエンジンJinja2をインストール
pip install Jinja2

Jinja2をインストールすることでhtml内でif文やfor文が使えます。
Jinja2のドキュメント

templates/index.html
...
  <div class="result">
            <h2>結果</h2>
            <!-- result配列の長さが0より大きい場合表示 -->
            {% if result|length > 0 %}
            <div class="result-content">
                <div class="items">
                    <!-- resultの中身ををループで表示(ここではreverseで逆順にしています) -->
                    {% for r in result|reverse %}

                    <!-- r[0](文章)が存在するなら表示する -->
                    {% if r[0] %}

                    <div class="item">
                        <p class="value_str">{{ r[0] }}</p>

                        <!-- r[1](時制)が存在するなら表示する -->
                        {% if r[1] %}
                        <p class="tense">{{ r[1] }}</p>
                        {% endif %}
                        <!--  -->
                    </div>

                    {% endif %}
                    <!--  -->

                    {% endfor %}
                    <!--  -->
                </div>
            </div>
            {% endif %}
            <!--  -->
        </div>
...

リセットボタンを作成

index.html

templates/index.html
...
<div class="result-content">
        <form id="reset" action="/reset" method="post">
                <button name="reset_result">結果をリセットする</button>
        </form>
<div class="items">
...

formにaction="/reset"属性を追加

main.py

main.py
...

@app.route("/")
def init():
    # value_strをhtml側で使えるようにする
    return render_template('index.html', result=result)

#  action="/reset"
@app.route("/reset", methods=["GET", "POST"])
def reset_result():
    global result
    result = []
    return render_template('index.html', result=result)


#  action="/input"
@app.route("/input", methods=["GET", "POST"])
...


CSSとJavaScript

CSS

style.css
static/css/style.css
/* css reset */

body {
    font-family: 'Inconsolata', monospace;
    background-color: #fdfeff;
    display: flex;
    padding: 0 1rem;
}

h1, h2, h3 {
    color: rgb(70, 70, 70);
    margin-bottom: 0;
}

h1 {
    font-size: 4vw;
}

h2 {
    font-size: 3vw;
}

h3 {
    font-size: 2vw;
}

p {
    margin: 0;
    color: dimgrey;
    font-size: 1.6vw;
    font-weight: bold;
}

ul, li {
    padding: 0;
}

li {
    list-style-type: none;
    font-size: 1.6vw;
    font-weight: bold;
    color: dimgrey;
}

li::before {
    content: "・";
}

input {
    background: white;
}

input:-webkit-autofill, input:-webkit-autofill:hover, input:-webkit-autofill:focus, input:-webkit-autofill:active {
    -webkit-text-fill-color: rgb(70, 70, 70);
    -webkit-box-shadow: 0 0 0px 1000px white inset;
    box-shadow: 0 0 0px 1000px white inset;
}

input:focus {
    outline: 0;
}

button {
    color: dimgrey;
}

button:hover {
    cursor: pointer;
}

button:focus {
    outline: 0;
}

/*  */

/* container */

#container {
    margin: auto;
    width: 80vw;
}

@media screen and (max-width:768px) {
    #container {
        width: 90vw;
    }
}

#container>div {
    margin-bottom: 2rem;
}

/*  */

/* nlp */

#nlp {
    /* border-radius: 5px; */
    /* background: #34456314; */
}

#form {
    margin: 0.5rem 0;
}

#form .input {
    height: 25px;
    width: calc(100% - 15px);
    padding: 5px;
    margin-left: 1px;
    background: transparent;
    font-size: 1.8vw;
    border: 2px solid rgb(70, 70, 70);
}

#form .submit {
    display: none;
}

.result-content {
    margin: 0.5rem 0;
    padding: 5px 10px;
    background-color: #f1f1f1;
    border-radius: 5px;
}

.result-content #reset {
    padding: 5px 0;
}

.result-content #reset button {
    margin: auto 0 0 0;
    padding: 5px 10px;
    display: block;
    border: 1px solid #fdfeff;
    background: #fdfeff;
    border: 2px solid dimgray;
    border-radius: 5px;
    font-size: 1.6vw;
    font-weight: bold;
    transition: all 0.3s ease;
}

.result-content #reset button:hover {
    background: dimgrey;
    color: #fdfeff;
}

.result-content .items {
    overflow: scroll;
    max-height: 50vh;
}

.result-content .items .item {
    display: flex;
    flex-wrap: wrap;
    padding-bottom: 0.5rem;
    border-bottom: 1px solid #34456314;
    margin: 1rem 0;
}

.result-content p {
    padding: 5px 0;
    font-size: 1.8vw;
}

.result-content .tense {
    background: #fdfeff;
    border-radius: 15px;
    padding: 5px 10px;
}

/*  */

#task li {
    color: aliceblue;
    padding: 5px;
    display: inline-block;
    border-radius: 5px;
    background: #344563;
}

JavaScript

static/js/main.js
// ウィンドウ読み込み時にフォームにfocusする
window.onload = function () {
    document.formStr.str.focus()
}

html内で読み込む

html内でcssとJavaScriptを読み込むのを忘れずに

templates/index.html
<head>
...
        <script src="{{ url_for('static', filename='js/main.js') }}"></script>
        <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>

Herokuにデプロイ

ディレクトリ構成

.
├── static
│   ├── js
│   │   └── main.js
│   └── css
│       └── style.css
├── templates
│   └── index.html
├── main.py
├── analyzer.py
├── Procfile
└── requirements.txt

Procfileを作成

gunicornをインストール

pip install gunicorn

Procfile

Procfile
web: gunicorn [ファイル名]:app --log-file=-

mainの部分は自分で作ったファイル名に合わせる(今回はmain.pyだからmain)

Procfile
web: gunicorn main:app --log-file=-

requirements.txtを作成

以下のコマンドを実行すると、requirements.txt何に必要なライブラリ等が自動的に書き込まれる。

$ pip freeze > requirements.txt

requirements.txt
autopep8==1.4.4
Click==7.0
Flask==1.1.1
Flask-SocketIO==4.2.1
gunicorn==20.0.4
itsdangerous==1.1.0
Jinja2==2.10.3
MarkupSafe==1.1.1
nltk==3.4.5
pycodestyle==2.5.0
python-engineio==3.10.0
python-socketio==4.4.0
six==1.13.0
Werkzeug==0.16.0

Herokuに登録する

このページでアカウントを作成する。

Herokuをインストールする

brew tap heroku/brew && brew install heroku

Herokuにログインする

heroku login

Herokuアプリを作成

heroku create [アプリ名]

今回は、tenseにします。

heroku create tense


$ heroku create tense
Creating ⬢ tense... !
 ▸    Name tense is already taken

既に使用されてたため変更

heroku create english-tense-analyzer


$ heroku create english-tense-analyzer
Creating ⬢ english-tense-analyzer... done
https://english-tense-analyzer.herokuapp.com/ | https://git.heroku.com/english-tense-analyzer.git

デプロイ

以下のコマンドを実行するだけで完了です

git push heroku master

heroku openを実行するとブラウザでWebアプリケーションが開きます。

終わりに

感想

使用してみて、とても軽く、簡単に手軽に作れるという点で学習コストが低く、使いやすいフレームワークだと思いました。
機械学習を使ってWebアプリケーションを作りたい方や少し試したいという方には丁度いいかと思います。
Djangoなどの他のPythonのWebフレームワークはまだ使用したことがないので試してみたいです。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HTMLコーディングで良く使うwebサービス様

HTMLのコード整形ツール

はちゃめちゃ大混乱なHTMLを治す時。<button>とか<a>はインデントされないので、その辺は手動で調整。
https://lab.syncer.jp/Tool/HTML-PrettyPrint/

Base64エンコーダー

Emmetでもできるけど、プロジェクト内に画像を移動するのすら面倒な時にここでエンコードしてそのままcssにぶち込む。
https://lab.syncer.jp/Tool/Base64-encode/

beforeやafter疑似要素のcontentプロパティで日本語の文字化けを回避する方法

cssの content:""; 内で文字化け防ぐ用。
https://webllica.com/css-content-property-mojibake/

CSS三角形作成ツール

吹き出しパーツデザイン用。
http://apps.eky.hk/css-triangle-generator/ja

SVGデータをCSSのbackground-image向けのBase64コードにかんたん変換ツール

background-imageにSVG使いたいよ
https://blog.s0014.com/posts/2017-01-19-il-to-svg/

CSS3のtransition-timing-functionの値、cubic-bezier()に関して

jquery Easingとか使ってる時、CSS側でもアニメーション揃えたい時
http://www.knockknock.jp/archives/184

Placehold.jp

ダミー画像とりあえず入れたいよ
http://placehold.jp/

HTMLタグ除去

テキストだけ抜いてサイトの調査とかしたいよ
https://crocro.com/tools/item/del_html_tag.html

Geocoding - 住所から緯度経度を検索

場所の緯度経度確認したいとき
https://www.geocoding.jp/

正規表現

どれが何やったっけなるとき
http://shanabrian.com/web/regular_expression.php

You might not need jQuery

いい加減脱jqueryしたいとき
http://youmightnotneedjquery.com/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LPを作るときに役立ちそうなTips集

この記事は、 ドワンゴ Advent Calendar 2019 の21日目の記事です。

はじめに

2017年はエンジニアさんで埋められるカレンダーの中、わりと緊張しながらデザイナーの僕も「コンポーネント指向フロントエンド開発におけるデザイナーの参画について」という記事を書くことができました。

記事としても反響を頂き、登壇までさせていただくなど、大変大きな一歩になる記事でした。

さて、あれから2年経ったわけですが、 今年の9月にニコニコ生放送の開発を離れ、特設ランディングページ(以下LPと表記)やポータルサイトなどを作る部署に異動しました

異動による環境/技術スタックの変化

部署の異動により、環境が大きく変化しました

ニコニコ生放送

  • 規模の大きなプロダクト・チーム
  • プラットフォームのUIデザイン
  • React/TypeScript/scss/css-modules
  • 堅牢な機能開発・ワークフロー

現在の部署

  • ペライチ ~ 3ページ、多くても5テンプレートの軽量~ポータル程度のページを2人作業
  • グラフィックデザイン、演出、アニメーションやインタラクションを重視
  • レスポンシブが標準
  • ejs/scss/vanila js + babel/jQuery ~ Nuxt(Vue)
  • イベント合わせの納期・期間限定の時間の制約、変更が大きい短期開発

のようになり、大きく環境が変わりました。

環境が新しくなったことにより、まだ異動してわずか3ヶ月満たないぐらいですが、非常に多くの知見を得ました。
今回はそのなかでも特に有用そうな技術的Tipsを、実例とともに細々と紹介していきます。

Intersection Observer API

LPでは、要素がウィンドウ内に表示されたときにアニメーションする というようなことがとても多くあります。
今まで、これを書くには「jQueryなどでscrollTopとウィンドウサイズとその要素の位置関係を計算」したり、in-view.jsなどのライブラリを使うことが多かったと思います。

よく出てくる割には、計算が面倒だったり、ライブラリを探す手間、scroll/resizeイベントの抜け漏れ、イベントの発生回数によるパフォーマンスの低下など厄介なことが多くありました。

今ではこれは、Intersection Observer APIというブラウザ標準のAPIとして提供されており、これによってかなり楽に実装できるようになり、イベントの発生回数もscroll/resizeと比べるとかなり少なく、パフォーマンスも良くなります。
基本的にIE以外はすでに実装されている上、polyfillも準備されており、スムーズに導入できます。

使用例: 「画面内に入ってきたらフェードインさせる」をサクッと書く

HTML

index.html
<div class="foo-section">...</div>
<div class="bar-section">...</div>

SCSS

will-change を入れる必然性はないですが、LPでは面積の大きな要素を動かすことも多く、なめらかなアニメーションによる快適な体験が求められるので、僕は will-change も書いておくことが多いです。

style.scss
@mixin fade {
  transition: opacity .4s ease, transition .4s ease;
  will-change: opacity, transition;

  &[data-intersecting="false"],
  &:not([data-intersecting="true"] {
    opacity: 0;
    transform: translateY(32px);
  }

  &[data-intersecting="true"] {
    opacity: 1;
    transform: translateY(0);
  }
}

.foo-section {
  @include fade;
}

.bar-section {
  @include fade;
}

JavaScript(es6/babel環境)

polyfillは npm install intersection-observer などで入れておくと便利です

script.js
// polyfillの読み込み
require('intersection-observer');

// observeしたときのaction定義
const action = (entry) => {
  // entry.target に検出したDOMが入ってくる
  // entry.isIntersecting に画面内かどうかが入ってくる
  entry.target.setAttribute('data-intersecting', entry.isIntersecting);
}

// IntersectionObserverの利用
const observer = new IntersectionObserver(entries => {
  entries.forEach(entry => { 
    action(entry);
  });
});

// observeするDOMを取得
// 他の要素にも適応したい際は、ここにどんどん追加しておくだけでOK
const targets = [
  document.querySelector('.foo-section'),
  document.querySelector('.bar-section')
];

// observeする
targets.forEach(target => {
  observer.observe(target);
});

これだけでよくあるアニメーションをサクッと実装することができます。なれると何も見ずに5分程度で作ることができ、要素の追加/削除にも柔軟に適応できるため非常に重宝しています。

LPに限らず、Reactのようなコンポーネント指向な世界でも、scrollやresizeはwindowのようなglobalなものを持ちがちになってしまうので、このようなAPIがあるのは非常に嬉しいことだと思います。

ECSSによるHTML/CSSのワークフロー

LPでは、書き捨てで納期も短いものも多く、要素の変更にも柔軟に対応する必要があるため、厳密で堅牢や再利用性というよりは、終盤で最低限秩序が崩れない(namespaceがある/変更で崩れないよう要素とスタイルが1:1対応である)、理解が容易なものが良いです。

そのため、BEMよりラフ(だと思っている)で冗長なECSSを採用しています。

ECSSの概要と考え方のまとめ が非常にわかりやすいですが、要点は以下の1行です。

namespace-ComponentName_ChildNode-variant

現在は namespace = ページ名を2文字にしたprefixとして採用しています。

使用例: カンプからHTMLをおこし、SCSSファイルを作成する

僕がコーディングを担当したページではないため、実際とは異なりますが、良いサンプルだったので ハレスタ の以下の部分(大人の事情で画像はぼかしました)をHTMLに起こしてみようと思います。

スクリーンショット

topページなので、namespaceは tp- とします。
RootのComponentNameは ScheduleSection とします。

schedule-section.ejs
<section class="tp-ScheduleSection">
  <div class="tp-ScheduleSectionBackground" aria-hidden="true">
    <div class="tp-ScheduleSectionBackground_DecorationText">Pick up</div>
    <img class="tp-ScheduleSectionBackground_Image" src="" alt="">
  <div>
  <header class="tp-ScheduleSectionHeader">
    <h2 class="tp-ScheduleSectionHeader_Heading-en">Schedule</h2>
    <div class="tp-ScheduleSectionHeader_Heading-ja" aria-hidden="true">スケジュール</div>
  </header>
  <div class="tp-ScheduleContent">
    <section class="tp-SchedulePickUpContainer">
      <div class="tp-SchedulePickUpThumbnail">
        <img class="tp-SchedulePickUpThumbnail_Image" src="<%= list[i].imageUrl %>" alt="">
        <div class="tp-SchedulePickUpThumbnail_Filter">
      </div>
      <div class="tp-SchedulePickUpInfo">
        <time class="tp-SchedulePickUpDateTime" datatime="略">
          <span class="tp-SchedulePickUpDateTime_Date">
            <span class="tp-SchedulePickUpDateTime_Month"><%= list[0].date.month %></span>
            <span class="tp-SchedulePickUpDateTime_DateSeparator">/</span>
            <span class="tp-SchedulePickUpDateTime_Day"><%= list[0].date.day %></span>
          </span>
          <span class="tp-SchedulePickUpDateTime_WeekDay">(<%= list[0].date.weekday %>)</span>
          <span class="tp-SchedulePickUpDateTime_Time"><%= list[0].date.startTime %><%= list[0].date.endTime %></span>
        </time>
        <h3 class="tp-SchedulePickUpTitle"><%= list[0].title %></h3>
        </div>
    </section>
    <div class="tp-ScheduleSubContainer">
      <% for(let i = 1; i < list.length; i++) { %>
        <section class="tp-ScheduleSubContainer">
          <div class="tp-ScheduleSubThumbnail">
            <img class="tp-ScheduleSubThumbnail_Image" src="<%= list[i].imageUrl %>" alt="">
            <div class="tp-ScheduleSubThumbnail_Filter">
          </div>
          <div class="tp-ScheduleSubInfo">
            <time class="tp-ScheduleSubDateTime" datatime="略">
              <span class="tp-ScheduleSubDateTime_Date">
                <span class="tp-ScheduleSubDateTime_Month"><%= list[i].date.month %></span>
                <span class="tp-ScheduleSubDateTime_DateSeparator">/</span>
                <span class="tp-ScheduleSubDateTime_Day"><%= list[i].date.day %></span>
              </span>
              <span class="tp-ScheduleSubDateTime_WeekDay">(<%= list[i].date.weekday %>)</span>
              <span class="tp-ScheduleSubDateTime_Time"><%= list[i].date.startTime %><%= list[i].date.endTime %></span>
            </time>
            <h3 class="tp-ScheduleSubTitle"><%= list[i].title %></h3>
          </div>
        </section>
      <% } %>
    </div>
  </div>
</section>

長くなりましたが、上記のような感じになります。

命名/HTML設計の際のポイント

class名とHTML要素は1:1の関係にする

後述するJSでのDOMの拾い方にも影響しますが、基本的にclass名は1文書でユニークになるようにします(liなど繰り返すもの以外)。また、1つのelementに複数のclass名が当たらないようにします。
仮になにか属性を付けたいときはVariantか、変化するものであればdata属性やaria属性を利用します。
レイアウトやスタイルで繰り返すパターンが有る際は、mixinやextendを利用するようにします。(スタイルの話をclass名に持ち込まず、スタイルの話はスタイルファイルで解決するようにする。)

ChildNode よりも ComponentName がたくさんあるような状態になるようどんどん区切ることを心がける

単純に書いていくとたった2階層で ChildNode になってしまうため、ComponentNameを意識して増やしていくことが大切です。

divが必要になったが命名に迷ったらとりあえず {ChildName}Container としておいて、あとからリネーム

上の ComponentName の積極利用と合わせての話ですが、 ComponentName の名付けは結構難しいです。これは、全体を書いたあとで分かるというケースが経験的には多く、全部書いてみてあとからリファクタするほうが早いので、ひとまず Container という名前を便利に使って {ChildName}Container としてしまって、あとから全置換で対応するほうが早くできる事が多いです。

また、LPだと特に、以下のような名付け/HTML設計のパターンを予めやっておくと有効です。

Backgroundでも要素として作り、Sectionの直下に持ってくる

LPでは演出を盛ることが多く、Backgroundにも演出を入れることが多いため、cssの background プロパティや、疑似要素では限界になることが多いです。そのため、最初からBackgroundという要素を持っておくと柔軟に対応できます。

h1~h6タグには Header ではなく Heading

これらのタグに Header を指定すると、 <header> がきたときに困ることが多いので、 Heading という名前が有効です

サムネイルの <img> はとりあえず <div> で囲っておく

UI要素としてのサムネイルもフィルターやhoverで拡大のような装飾、インタラクションをもたせることが多いため、 Thumbnail > Thumbnail_Image / Thumbnail_Filter のように、常に <img> にWrapperをもたせるようにしておくと、あとからアニメーションを入れるなどのときに有効になります。

ECSSで命名した後のSCSSの生成

OneClickCSS という、HTMLを投げるとセレクタを吐き出してくれるという便利なサービスがあります。

スクリーンショット 2019-12-02 2.25.52.png

画像のようにHTMLをペーストし、「SimpleCSS」 ボタンで一発でセレクタを吐き出してくれ、これをコピーして.scssとして保存すればそれであとはスタイルを追加していくだけのscssの雛形が完成します。

ECSSで書くことによるJSでのアクセス

今まで、jsでアクセスするHTML要素は js- prefixをつけたり、idをつけたりすることが多かったとおもいます。
しかし、これではHTMLがjsに依存を持ってしまいます。
例えば、jsで扱う要素の数が増えると、必ずHTMLになにか手を加えることになります。
変更の多いLPでは地味に手数が増える作業になります。
これは、jsはHTMLに依存するが、HTMLはjsに依存しないようにして、依存の方向性を単方向にするほうが良いと思います。

そこで、ECSSではclass名と要素が基本的に1:1の関係になっているため、そのまま document.querySelector('.ns-ComponentName_ChildName') で呼び出しています。
HTMLのclass名をインターフェースとし、JSとCSSはそれを参照する、HTMLはJSとCSSに依存しないようにすることで、あまり複雑にならずに保つことができます。

(querySelectorにはパフォーマンス上の懸念があるため、場合によっては最適化することはあります)

ECSSの冗長性とclass名と要素が1:1の関係になっているおかげで、このようなスピーディで、最後までclass名のスコープで悩まないワークフローを成立させることができます。

all: unset

普段、reset.css, normalize.css, sanitize.cssなどを使うことが多いと思いますが、詳細度への配慮や、環境によってreset, normalize, sanitizeどれになっているかの確認、また作るときの選定にも地味に時間がかかります。

そこで、cssには all というプロパティがあります。

https://developer.mozilla.org/ja/docs/Web/CSS/all

この all: unset は デフォルトの値がinheritでないものはすべてinitializeしてくれる便利なプロパティで、reset.cssに近いものですが、上書きなどをあまり考えずに使うことができ、cssも見やすくなるため有用だと思います。

使用例: <a><button> が交じるけど同じスタイルのmixinで共通化したい

all: unset; だと以下のように書けます

@mixin blue-button-style() {
  display: inline-block;
  padding: 12px 24px;
  min-width: 120px;
  font-size: 14px;
  line-height: 1.25;
  text-align: center;
  white-space: nowrap;
  color: blue;
  border: 2px solid blue;
  border-radius: 4px;

  &:hover {
    cursor: pointer;
  }
}

.anchor {
  all: unset;
  @include blue-button-style;
}

.button {
  all: unset;
  @include blue-button-style;
}

というように書くと、 <button><a> に対して同じボタンのスタイルを適応させることができます。

スクリーンショット
(アカウント取るのが面倒だったのでスクショですみません :bow:

all: unsetとのセットで、他のページやプロジェクトにも使い回すことができて非常に便利だと思います。

使用上の注意

IE, Edgeが非対応の上、Safariで意図通りに動かないケースがあります(特にcolor周り)。

↓safariでの表示
Safariでの表示

polyfillがあるのですが、これでも意図通りに動かないケースを確認しています。
https://www.npmjs.com/package/postcss-all-unset

ですので、現在では以下のようにmixinを作る方法が現実的です。

mixinの作り方( @supports を使わない)

上記のpolyfillから、以下のようなmixinをつくり、いらない/使わないものを軽量化するというのが一つの手です。

ひとまずそのままコピペ

@mixin all-unset() {
  animation: none 0s ease 0s 1 normal none running;
  backface-visibility: visible;
  background: transparent none repeat 0% 0% / auto auto padding-box border-box scroll;
  border: medium none currentColor;
  border-radius: 0;
  border-collapse: separate;
  //(...以下略)
}

出典を書いておき、使わないプロパティをコメントアウトしておくと、ある程度保守性の高い、コントロールされたmixinにできるかと思います。

@mixin all-unset() {
  // polyfill: https://www.npmjs.com/package/postcss-all-unset から改変
  background: transparent none repeat 0% 0% / auto auto padding-box border-box scroll;
  border: medium none currentColor;
  border-radius: 0;
  border-collapse: separate;
  // (...以下略)

  // ==== 以下は使わない ====
  // animation: none 0s ease 0s 1 normal none running;
  // backface-visibility: visible;
  // (...以下略)
}

mixinの作り方( @supports を使う / 少し未来の話)

将来的にsafariがきれいに動けば、chromeやfirefoxではうまく動くので、 @supports 構文をつかったほうが、インスペクタに大量のプロパティが出てこなくなって良いと思います。

単純に考えると、mixinは以下のようになると思います。

@mixin all-unset() {
  @supports (all: unset) {
    all: unset;
  }

  @supports not (all: unset) {
    background: transparent none repeat 0% 0% / auto auto padding-box border-box scroll;
    border: medium none currentColor;
    border-radius: 0;
    border-collapse: separate;
    // (...以下略)
  }
}

がこれでは

.anchor {
  @include all-unset;
  color: red;
}

と書いたとき、展開されるcssは

.anchor {
  color: red;
}

@supports (all: unset) {
  .anchor {
    all: unset;
  }
}

@supports not (all: unset) {
  .anchor {
    background: transparent none repeat 0% 0% / auto auto padding-box border-box scroll;
    border: medium none currentColor;
    border-radius: 0;
    border-collapse: separate;
    // (...以下略)
  }
}

となってしまい、あとに書いたはずのcolorが打ち消されてしまいます。

そのため、mixinとそれのincludeは以下のように書く必要があります。

@mixin all-unset() {
  @supports (all: unset) {
    all: unset;
    @content;
  }

  @supports not (all: unset) {
    background: transparent none repeat 0% 0% / auto auto padding-box border-box scroll;
    border: medium none currentColor;
    border-radius: 0;
    border-collapse: separate;
    // (...以下略)
    @content;
  }
}

.anchor {
  @include all-unset() {
    color: red;
  }
}

この書き方は将来的にall: unsetが実用段階になったときに、もしかしたらちょっと置換がしづらいかもしれません。

WAI-ARIAの利用

.isShow.hide など、表示非表示のクラス名は人によってぶれがちですが、 WAI-ARIA の利用によって割と楽になります。

こちらは Webアクセシビリティ Advent Calendar 2019 の 14日目の記事として以下ページに書きました。合わせてご参照ください。

https://qiita.com/ln-north/items/8571782a9ec28ce622f4

終わりに

非常に長くなってしまいましたが、以上がTipsの紹介になります。

部署を移って期間が短いため、まだ正直いろんな感覚になれていないところがあるのですが、ぐいぐいコードを書くことが多くなり、ロゴなどのグラフィックデザインをすることも増え、非常に充実した日々を送っています。

現在すこし規模の大きいページも受けるようになったため、ejs/scss/jsからNuxt(Vue).jsでの開発にも挑戦しています。

エフェクトやモーションについても、シェーダ、パーティクル、WebGL、pixi.jsやGLSLやなど、演出手法、そしてその実現方法についてもたくさん勉強を重ねる必要を感じています。

また、そこで知見が溜まってきたら小出しに紹介できるように頑張りたいと思います。

ここまで読んでいただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CBcloudにフロントエンドエンジニアとして入社して行ったこと

現在、私はCBcloud株式会社でエンジニアとして、フロントエンドに限らず業務に携わっています。
今回の記事では入社当時、フロントエンドエンジニアとしてやってきたことをつらつら書いていきます。

前職の話

前職は完全にフロントエンド実装のみを専門とする会社でした。
サイトリニューアルや店舗予約系のWEBサービスのフロントエンド実装を担当していました。
jQueryを使い、モーダル、カルーセル、スムーズスクロールのようなWebサイトでよくあるコンテンツを作っていました。
実務では最後までjQuery1本でした。JSフレームワークは社内・社外の技術勉強会を通して触れていました。

一つ言えるのは、前職でのJavaScript研修がなければ、今も確実に「JavaScriptよくわからん」の状態ですし、リファレンスもまともに読めず、他の言語を学ぶ際にも応用できなかったでしょう。

転職してCBcloudに入社した

どうして入ったのか(入れたのか)

2019年12月現在、社員数を着々と伸ばし続け60人を超える会社に成長しているCBcloudですが、
私が入社する直前の2017年4月時点では10人ほどの会社でした。

CBcloudのサービスである「軽town」(のちの「PickGo」)を開発しているエンジニアは1人の状態で、
フロントエンド専門のエンジニアはおらず、サービスを紹介するウェブサイトからサービスサイトまで頑張って実装していました。

そんなわけで、「バックエンドはよくわからないけどフロントエンドならできます」という私とはちょうどタイミングや利害が一致し、入社することになりました。
「今の基準で面談・面接を受けていたら入れるかどうか」というのはたまに思います。
ともあれタイミングと、自分の強み・弱みを相手にちゃんと話せるかどうかは大事だなと思います。

私と同じ時期にもう一人デザイナー兼エンジニアが入ったことで、
2017年5月末から、PickGoのエンジニアは3人となります。

PickGoとは

荷物を送りたい人と荷物を運ぶことの出来る軽貨物者の配送ドライバーをマッチングさせ、
届けたい場所に即時でモノを届けることを実現する配送マッチングサービスです。

おおきく分けてサービスが2つに分かれています。

  • 企業が配送依頼をするWebサービス(PickGo Business)
  • 配送依頼をドライバーが確認し、エントリー~配送までを管理するスマートフォンアプリ(PickGo Driver)

PickGoのサブサービス的な個所のフロントエンド実装

まだあまりJavaScriptは触っておらず、HTMLとCSS修正が主でした。
バックエンド実装やJavaScriptとフロントエンドをつなぐ部分は任せきりでした。
リリース作業も任せていたため、本当にフロントエンドだけやっていました。

私のスキルがどの程度かまだ理解されていなかったので、
様子を見ながら起用されていたと思います。
フロントエンド的には「CSSの使い方を修正するだけでも割とコミットできそう」と当たりをつけたりしてました。

ウェブサイト立ち上げ

法人向けサイト作成

入社当時、既に軽townドメインを使った状態でドライバー向けWEBサイトはリリース済みでしたが、
PickGoの法人向けのPRサイトがまだありませんでした。
「デザインはあるがフロントエンド実装できる人間がいない!」という状況下で、私が入社したため実装を任されました。

当時渡されたデザインがビジネスサイトにしてはポップ過ぎたため、CEOからの要望もあり、
構成はほぼそのままで、色調を抑えつつデザイン画から実装しなおしました。

https://www.pickgo.town/business

現在は完全リニューアルしており、当時の面影は微塵もありません。
ともあれpickgoドメインは継続して使っており、SEO的には少しでも長い間存続している方がいいので、
2017年時点でリリースできたのはよかったのかなと思います。

個人向けサイト作成

後述の個人向けサービス開始前の布石として、個人向けサイトをリリースしました。
こちらはほぼ出来上がっているHTML・CSSがあり、そこに必要な文言・画像・セクションを足したものをリリースした形となります。
作業自体はフロントエンドを触れる人間なら誰でも出来るもので、特別なことはありません。

リリース直前、キャッチコピーとなる言葉の添削があり、
いろいろ考えた末に、私の案である「5000円から引っ越しを始めよう!」で行くことになりました。

https://www.pickgo.town/personal

例によって完全リニューアルされており当時の面影はありません。

統合ページ作成+リダイレクト

  • ドライバー向け
  • 法人向け
  • 個人向け

訴求対象が異なる3つのサイトが出来上がり、それをまとめるページも必要となったため作成しました。
3つのサイトにリンクするためのページです。

https://www.pickgo.town/

やはり完全リニューアルされていますが、トップページ → 各サービスサイトという流れはこの段階で確立しました。

ロゴ変更

入社してすぐ、「サービス名を変える」ということになり、軽townからPickGoにサービス名が変わることになります。
それに伴い、Webサービス全体のロゴを変えることになりました。
ロゴを変えるだけではなく、メイン色も緑から青に変わりました。

ログイン画面UI再実装

また、ビジネス側のログイン画面はこの時に再実装し、デザインを大幅に変えました。
要素数の少ない画面なので、当時のデザインを活かすよりも実装しやすいように完全に構造を変えました。
デザイン画は特になかったので、必要な要素を小綺麗にまとめた感じにしました。

この画面はまだ当時のデザインのままなので、ログイン画面は長く使われるし、一度リリースしたらめったに変更されないぞ!と思います。
(機能改修は何度か入っていますが、デザイン自体はそのまま保たれています)

アプリのUIデザイン変更

ドライバー向け一覧と詳細のデザインを変えました。
デザイン画があったのでその通りに実装した感じです。
UIデザインはここから後も少しずつ変わっていくことになります。
ここ以外もそうですが、とにかくCSSクラス単体での指定に置き換えていってます。

PayPal対応

PayPayじゃないよ。
PayPalAPIのバックエンド実装は(2日目の記事を書いている)徳盛氏に任せ、
HTML実装部分を担当しました。
具体的にはPayPalのAPIを使うためのHTMLの属性設定と、PayPal登録用ボタン・案内テキストのUI作成です。

UIの実装自体は苦労するところは特になかったのですが、
フレームワークや言語の解説ではないAPIドキュメントを読んで理解し、HTMLに適切な属性を当てていくのは初めての経験で、
かなり苦労しました。「この属性はこういう意味だよ」と書かれているだけで、具体的なHTML実装の例が書いているわけではないので、「結局どう実装するのが正解なんだ!」という目に何度も陥りました。

それでもドキュメントだけでなく、PayPalの担当者の方とSlack上で質問することができたこともあり、
なんとか完成することができました。

具体的には下記のようなHTMLです。nameとvalueは対応する決まったもの(適切な単語)を入れる必要がありました。
2行ならまだしも16項目分、間違えずに実装する必要がありました。

<input type="hidden" name="currency_code" value="JPY">
<input type="hidden" name="billing_country" value="JP">

PickGo Personal

ロゴの変更やアプリの改修も一段落し、次が大型の案件となるPickGoの個人向けサービスの開発でした。

メイン機能となるのはチャット機能でしたが、私はそこにはほぼ触れず、他のエンジニア2人に任せていました。
その代わり、それ以外の画面すべてのフロントエンド実装をしました(といっても10画面程度ですが)。
土壇場でヘルプページに説明を足したりしました。
PickGo Personalは結果的に3ヶ月ほどでリリースした形でした。

個人向け=一般向けのサービスであるため、テレビで取り上げられることが多く、
アプリの画面が大きく映し出されるたびに、嬉しいやら恥ずかしいやら何とも言えない思いをしました。

取材

前職と大きく異なっていると感じたのが取材です。
10人規模だった入社当時でも既に報道各社からの取材の依頼があり、オフィス内にテレビカメラが入ってきていました。
事業として「物流クライシス」という社会問題に取り組んでいるため、物流系の雑誌はもとより、
現在でも有名なテレビ番組、ニュース番組で取り上げられることがあります。

私自身はカメラに映りこむことは多少あったものの、取材に直接応じる役目を負うことはなく、
代わりに、自分が作ったサービスや紹介ページがこれでもかと映ってくれています。

ドキュメント化

入社当時のGitHubのWikiは真っ白でした。
一人で開発している段階だと頭に入っていればよかったものも、複数人で開発するとなると話は別です。
フロントエンドのコーディングガイドラインや、ページの作り方からまとめはじめ、
自分が関わっているPickGo関係のリポジトリwikiのページ数を合計すると、現在までに60ページほど書きました。
初版はほぼ私ということになります。

一度まとめておけば、忘れても書いてあることから何とかなりますし、
細かいことをいちいち覚えておく必要がなくなる上、必要に応じて他の人が加筆することもできます。
その辺はある程度認識しているつもりでしたので、頼まれずともやっていました。自分にとって必要なので。

Slackの絵文字追加

Slackは使われていたものの、私が入った頃はカスタム絵文字がほとんどありませんでした。
なので、自分が使いたいリアクションをとにかく足していきました。
今では社員の皆さんがジェネレータを使うなどして自作することも増え、充実したラインナップになっています。

スタンプでリアクションを付けまくるというのは、前職からずっとやっている気がします。
数えてみたら、カスタム絵文字は2019年12月現在までに私一人で214個追加したようです。

採用

個人アプリのリリース後は年末まで採用活動に振り切っており、
開発した記憶があまりありません。1日中面談やソーシングをしていました。
前職ではエンジニア面談をする機会がなかったので初めての経験でした。

人を採用するって難しいと思いました。
なかなか採用内定が出ることがなく、私が良いと思っても他の人たちが首を縦に振るような人がなかなか現れませんでした。
スキルとマインドを両方見た上で判断するわけですが、
圧倒的なスキルを持つ人には合うポジションを用意することができなかったりするわけで……。

それでも他の皆さんの頑張りもあり、2017年末までに1人、2018年1月からさらに2人のエンジニアを迎えることになりました。

まとめ

つらつら書いていきましたが、採用以外は

  • サイトを作った
  • UIを作った
  • ドキュメント化
  • Slack絵文字

以上! ということになります。
前職だとそれだけでずっと働けましたが、CBcloudでは限界を感じることになります。
次の記事ではその辺りの話から続けて書いていければと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む