Practice of Programming

プログラム とか Linuxとかの話題

Product-Led Growthまとめ

この記事は Wano Group Advent Calendar 2022の4日目の記事となります。他にも、24日(STREAM DECK互換の仮想デバイスの紹介)と25日(Notionとtandemで作る相談しやすいリモートワーク開発のすすめ)に記事を書いているので、そちらもどうぞ。

グループ会社のEDOCODE(メインで仕事してます)のAdvent Calendar で、12/6にDockerの記事を書きますので、よろしければ、そちらもどうぞ。

この記事について

立場的に技術のこと書くべきなのかもしれませんが、リクエストがあったので、「Product-Led Growth」という本のまとめを書きます(要約と個人的な考えの追記となっています)。

なお、以下の目次を見てもおわかりと思いますが、とっても長いです。

Product-Led Growthとは?

ウェス・ブッシュによるプロダクト開発の本ですが、副題に「『セールスがプロダクトを売る時代』から『プロダクトがプロダクトを売る時代』へ」とあります。 プロダクトを成長させるための戦略や効果的なプロダクト開発、それにまつわる考え方について書かれています。

まじで良い本なので、こんなまとめを読んでる暇があったら、今すぐ、買って読んだほうが良いです。Kindleなら数秒で手に入りますし、英語なら、サイトで無料で読むことができます。

いや、それでもちょっと...という方は、時間が無駄になるかもしれませんが、以下の紹介を読んでから、本書を読む気になってください(ならなかったらごめんなさい)。

※文章内で、PLGと書いている場合は、Prodcut-Led Growthの書籍のことを指しています

ちなみに、時々「UXデザインの法則」の話が出てきますが、こちらも良書です。下記にまとめていますので、よろしければどうぞ。PLGの内容と非常に相性が良いのではないかと思います。

UXデザインの法則を読んで感銘を受けたのでまとめた

セールス主導型 vs プロダクト主導型

字句通りですが、セールス主導型のプロダクトというのは、セールスの人員が、見込み客リストにコンタクトを取って、顧客獲得するとか、オンボーディングに人手をかけるとか、広告を大々的に打って、顧客を獲得するとかそういうものです。セールスに重きを置いてプロダクトを成長させるということです。 プロダクト主導型というのは、見込み客に端的に言えばプロダクトを使わせる・体験させることによって、そのプロダクトの価値をユーザーにいち早く体験させることで、ターゲットになりうるユーザーを逃さず、成長につなげるというような感じです。

顧客を獲得するのに、セールス主導型は基本的にコストがかかります。比較して、プロダクト主導型ではコストは低く抑えられます。顧客獲得単価を低く抑えられるということは、その分サービスの利用金額も低く抑えることができ、競争優位性が高まります。

フリートライアル、フリーミアム

この2つのモデルのプロダクトがプロダクト主導型に向いています。この2種類の違いは何か、改めて書くと。

  • フリートライアル ... 一定期間は無料で使うことができる。無料期間が終わると有料プランに切り替わる
  • フリーミアム ... ずっと無料で使えるが、有料プランにすることで使える機能や回数とかが増えたりする

フリートライアルとフリーミアムの実際の例を出すと

みたいな感じですね。

自分のプロダクトが、フリートライアル、フリーミアム、どちらが向いているのかを判断するために、プロダクトの成長戦略について考える必要があります。

プロダクトの成長戦略

下記の3つの戦略のうち、自分のプロダクトにあったものを選ぶべきです。

ドミナント型 - 安価 & 大規模

全タイプの顧客を狙う差別化戦略です。低価格で機能を提供し、あらゆるタイプの顧客をターゲットとします。SpotifyNetflixのようなサービスですね。 「5000万ユーザーくらいを顧客にできないと成り立たない」との説もある(*)とのことですが、どこまで必要かは会社の目指す規模にもよりそうです。 ただ、安価なので、5000万はだいぶ言い過ぎなんじゃないかとは思いますが、それなりのユーザー数は必要かと思います。

ディスラプティブ型 - 既存より低価格 & 既存以下以上の規模

既存サービスを過剰と感じている顧客を狙う戦略。例えばタスク管理サービスのJiraに対する、Trelloみたいな感じですかね(買収されてますけど)。機能を落としても、よりユーザーが使いやすいものを作って、乗り換えさせる戦略です。

差別化型 - 既存より高価格 & 既存以下以上の規模

既存のサービスが不十分だと感じている顧客を狙う戦略。ディスラプティブ型とは逆に、既存サービスのできないところを補ったプロダクトを開発し、乗り換えさせる戦略です。 グループ会社のEDOCODEが開発しているPUSHCODEは、WebPushのサービスですが、他のサービスよりもAPIに力を入れているので、差別化型の戦略と言えるかと思います。

ブルーオーシャンレッドオーシャン

プロダクト主導が向いているかどうか判断するために、自分のプロダクトがすでに市場が成立しており競争過多な市場(レッドーオーシャン)にいるのか、今から市場を作り出す(ブルーオーシャン)ものなのかを把握する必要があります。

ブルー or レッド?という二者択一ではなく、セグメントによってはブルーにもなりうるし、レッドにもなりうるということもあります。 例えば、大企業向けに提供している同様のプロダクトは複数あるが、中小企業向けに提供しているプロダクトは少ない、または、無いということもありえます。

Slackに対するChatworkは、(当初の)Slackが英語インタフェースでAPIや連携サービスが充実していて開発者受けが良かったのに対して、別のセグメント(日本人向け、非開発者より)を狙っての戦略だったかもしれません。知らんけど。

ブルーオーシャン

ブルーオーシャンはこれから作り出す市場なので、競争相手はいないが、プロダクトをすぐに理解してくれる顧客も少ないということになります。 プロダクトの価値を顧客がすぐに理解できるようなものであればよい(Spotifyなど)ですが、そうでなければ、プロダクトを理解してもらうまでに手厚いサポートが必要になるかもしれませんので、コストをかける必要があります。 結果として、セールス主導型が合うようです。

レッドオーシャン

レッドオーシャンでは競争相手は多いですが、同じようなプロダクトがすでにあるので、顧客はプロダクトをすぐに理解することができます。 そのため、手厚いサポートはそこまで必要ないことが多く、プロダクトの力で顧客のオンボーディングを低コストに抑えられる可能性が高いので、プロダクト主導型にあっています。

ブルーオーシャンのほうが良いと思っていたが...

個人的に、プロダクトを新しく作るならブルーオーシャンのほうが良いよなーと思っていたのですが、プロダクト主導型のサービスにおいてはそうでもないということですね。

確かに、グループ会社のTuneCore Japanが始まった当初は市場はブルーオーシャンでしたが、ユーザー数の伸びも緩慢でした。 今は、TuneCore Japanが市場を開拓してユーザー数が増えました。後発のサービスが出ても、このタイプのプロダクトについてユーザーがすぐ理解できてしまいますね。先駆者にはメリットもありますが、当然ですが、メリットに甘えていられるというわけではないですね。

トップダウンボトムアップ型の販売戦略

販売戦略によってもプロダクト主導で行くべきかどうかが別れます。

トップダウン

トップダウン型の販売戦略とは、企業の意思決定者に導入を決定させるやり方です。大企業の全体に一つのシステムを導入させようというような場合は、トップダウン型になります。経営層やそこに近い人たちに売り込んで、全社的に採用してもらうようなやり方です。 プロダクトの導入までに数ヶ月から数年かかる場合もあります。

営業やサポートに人も時間もお金もかかるので、セールス主導型のプロダクトに向いています。

ボトムアップ

ボトムアップ型の販売戦略は、大きな決裁権のある人というよりは、実務者が実際に使ってみて、導入されていく形です。 実務者が簡単に使いはじめられ、理解しやすいものである必要があります。フリーミアム、フリートライアルが向いています。 消費者向けのプロダクトであれば、そもそもトップダウンということはないですね。

プロダクト主導型のプロダクトに向いています。

プロダクトの価値をいかに速く示せるか?

プロダクト主導型のプロダクトでは、プロダクトの価値をユーザーにいかに速く示す(体験させる)かが、重要です。フリーミアム、フリートライアルであれば、すぐにユーザーがプロダクトを体験することができます。

フリーミアムかフリートライアルか?

自分のプロダクトがプロダクト主導が向いているとわかった場合に、フリーミアムとフリートライアルのどちらが向いているのかを考える必要があります。

productled.com に、下記のクイズが用意されています。クイズに答えることで、どちらが向いているか判定することができます。 https://productled.com/quiz/

ただ、二択ではなく、フリーミアムとフリートライアルのハイブリッドという選択も可能です。

ハイブリッド型

既存のプロダクトとは別に新規事業としてたちあげて、いずれかのモデルを試してみるとか、フリーミアムだけど、ブロックされている機能をフリートライアルで提供できるとか、フリートライアルから有料版に移行しないユーザーに、フリーミアムの案内をだすとか、などがあるようです。

ユーザーに価値を示すと言っても?

本当に自分たちのプロダクトの価値を理解しているのが、正しく伝えられているのかを確認することが必要です。UCD Frameworkが紹介されています。

UCD Framework

UCDは、Understand、Communicate、Deliver で、このサイクルを回して、プロダクトの価値とユーザーがプロダクトから期待する価値をギャップがないものにしていくフレームワークです。

  1. Understand(理解する) ... 主観的分析、データドリブンアプローチ、ストレステスト
  2. Communicate(伝える) ... 経済的分析、マーケットリサーチ、適切な価格ページの開発
  3. Deliver(提供する) ... Valueのギャップの特定、定常的な最適化

プロダクトの価値の理解(Understand)

顧客はプロダクトを買っているわけではなく、プロダクトの生み出す「成果(=アウトカム)」を買っています。 Slackは「チャットツールを売っている」わけではなくて、「チームの生産性やコミュニケーションの向上を売って」いて、顧客はそれを望んでいます。

1. 機能的対価

プロダクトのメイン機能。顧客が実際に解決したい問題に対応するための機能に対する対価。

2. 感情的対価

使っていることで得られる感情的な対価。何らかの発見が得られるとか、喜びが得られるとか。 UXデザインの法則に「ピークエンドの法則」というものがあります。プロダクトの感情的対価のヒントになるかと思います。

3. 社会的対価

使っていることで得られる社会的な対価。使っていることによる他者(上司、部下、同僚、外部)からの評価など。「あのサービス使ってるんだ、イケてる」みたいに思われたりすることとかも、含まれますね。

プロダクトから得られる価値を測る

プロダクトの価値を測るためのバリューメトリクスには、2種類のものがあります。

  1. 機能的メトリクス ... 機能の使用頻度によって価格がスケールするようなケース
  2. 対価ベースのメトリクス ... 例えば、動画の視聴数が顧客に利益を生むようなケース

機能を差別化することで価格をあげると、高い解約率を招く場合があり、その代わりに、バリューメトリクスを設定するほうが75%低い解約率になるとのことです。対価ベースのバリューメトリクスであれば、さらに40%低い解約率になるとのことです。

バリューメトリクスの例

  • Slack ... メッセージ数
  • PayPal ... レベニューの量
  • calendly ... スケジュールされたMeetingの数
  • Notion ... 招待されたプロジェクトメンバーの数
  • INTERCOM ... 開始されたチャットの会話の数
  • DropBox ... 共有フォルダの数

https://drive.google.com/file/d/1mK3I5lrsBSTJSi2yk2J69vX797A9dLLf/view page. 7

良いバリューメトリックスの3つの条件

  1. ユーザーにとって理解しやすいもの
  2. ユーザーがプロダクトから得られる価値と連動している
  3. ユーザーがプロダクトから価値を得れば得るほど、大きくなる
ユーザー数課金はたいてい間違っている

大抵のサービスがユーザー数で課金していますが、ユーザー数が増えたからといって、ユーザーが得られる価値が高まるかどうかは不明なので、ユーザー数に対して課金するのは良くないと書かれています(Slackのようなコミュニケーションツールは別)。

ユーザー課金チェックリストがあるので、これがすべてYesでなければ、ユーザー数課金にするべきではないとのことです。

バリューメトリクスを見つける方法
1. 条件に合致するメトリクスを探す

プロダクトのメトリクスになりそうなものをリストアップして、前述の3つの条件にあてはまるかどうか確認すると良いとのことです。

2. データドリブン・アプローチ

コアユーザーと解約ユーザーの行動の分析して、バリューメトリクスを作るアプローチです。

コアユーザーは...

  1. 普段どのように使っているか?
  2. プロダクトでやらないことは?
  3. オンボーディングの際に、最初にやること
  4. プロダクトから価値を多く得て成功している人の共通点

解約ユーザーは...

  1. コアユーザーとのユーザージャーニーの違いは?
  2. 具体的にどのようなクティビティが違うか?解約ユーザーが達成できたこと、達成できなかったこと。
  3. ターゲット市場内のユーザーだったか?
  4. 主な解約理由は何か?
バリューメトリクスの有効性判断(ストレステスト)

バリューメトリクスを作っても主観的に判断しているだけだと正確ではないです。 作ったバリューメトリクスを、ユーザーに価格面から「最も好ましいもの」「最も好ましくないもの」を選んでもらいます。

「ユーザーが好ましいと思っている」=「ユーザーが対価を受け取っているもの」なので、自分たちが設定したバリューメトリクスがユーザーの考えと合っているか確認できます。

プロダクトの価値を伝える(Communicate)

プロダクト主導型のビジネスでは売上モデルと顧客獲得モデルはは密接しており、連動しています。 プロダクト主導型のビジネスでは、プロダクトによって顧客を獲得するので、プロダクトがまずければ顧客は獲得できないですし、プライシングが複雑で理解しにくいなら、サインアップするユーザー数にも影響がでます。 この両輪がうまく回っていないと、成長もできません。

プライシングモデルと顧客獲得モデルを正しく設定する方法

1. 料金ページを複雑にしない

「UXデザインの法則」に、「ヒックの法則」というのがあります。「意思決定をさせたいなら選択肢は少ないほうが良い」というものです。 大抵のサービスで、プランの選択肢は3つ程度に抑えられています。 PLGでは、5秒テスト(5秒以内に選択できる)に合格できないなら、顧客獲得機会を失っているとあります。

2. 有料プランにアップグレードする必要がなくなるような無料プランは作らない

顧客が無料でい続けてしまってはビジネスにならないので、無料プランですべて済んでしまうようなプラン設計では困ります。

3. ダウングレードしやすい設計にしない

有料サービスをフリーミアムにしようとする場合に、有料ユーザーが無料プランに移ってしまう可能性がありますが、有料プランのユーザーを分析して、無料にする機能しか使っていないユーザーがどれくらいいるかとか、無料にすることで見込める新たな顧客はどれくらいか、とかを考えると良いようです。ユーザー数や配信数であれば、減ることは少ないですね。

プライシング戦略

需要供給に基づいて決める「適正判断型」や、サービスにかかっているコストに利益をプラスする「コスト・プラス型」や、競合の価格と比較して決める「競合ベース」。プロダクトが提供する価値をもとに決める「バリューベース」と、価格を決定する方法は複数ありますが、SaaSの価格を決定するならバリューベースでやるべき、とのことです。

プロダクトの価格の決め方

「経済的価値分析」と「市場調査とユーザー調査」の2つの方法が紹介されており、データやユーザー数が少なく、ユーザーと直接価格について話せる機会がない場合は、前者。逆であれば、後者が良く、後者のほうが正確とのこです。

経済的分析

経済的価値分析では、プロダクトの価値のところで上がっていた、3つの対価。 「機能的対価」「感情的対価」「社会的対価」を実際の金額にしてみて計算する(社会的対価は計算が難しいのでオプショナル)。 その価値の1/10の価格にすると良いとのことです。ユーザーが支払う価格の10倍の価値を提供するということですね。 例えば、あるサービスを使うことによって、今まで一日かかっていた作業が数分で済むようになるなら、1日分のその人のコストが機能的な対価といえますね。 感情的な対価は、この機能を使って得られらる機能的対価にたいして、いくら払ってもよいかということですが、「この仕事が自動化できるんだったら、毎月1万円くらい安いもんだ」みたいなことですね。社会的対価と同様で、ちょっと測りにくい気もしますけどね。

市場調査とユーザー調査

市場調査とユーザー調査では、価格をいくつか用意して、「高すぎる」「安くない」「高くない」「安すぎる」価格を選んでもらうという方法が書かれています。より単純化して、「納得できる」「高い」価格の二択でも良いとのことです。

分析方法は、この辺に書いています。

価値を提供する(Deliver)

プロダクトの説明を読んだり、説明動画を見たりして理解する「知覚価値」と実際使ってみたときの「体験価値」にギャップがある(バリュー・ギャップがある)とユーザーはがっかりします。この2つが一致するのが理想です。 バリュー・ギャップが大きいと、ユーザーはプロダクトを二度と使わなくなるので、バリュー・ギャップの解消に力を入れる必要があります。 「できると思って登録したのに、できないんかー(できるけど、えらい複雑だなーとか)」みたいな経験ありますよね。それがバリュー・ギャップです。

バリューギャップの解消

下記にあげるようなバリュー・ギャップを解消していく必要があります。

アビリティ・デッド

プロダクトが提供すべき価値をユーザーが受け取れていない状態です。別に機能がバグっているという話だけではなく、仮登録したけど、実際の機能は使わずに離脱してしまった、というのも含まれます。

(プロダクトの提供者が)顧客がプロダクトを購入する理由を理解していない

顧客がプロダクトに期待している価値を正しく理解して、その価値を可能な限り最短で体感できるようにする必要があります。

価値の提供に失敗している

ユーザーへの事前の認知(プロモーションやサイトの説明)と実際提供される価値が違っていれば、それは問題です。 単純に機能的なものもあれば、すぐに使えると書いているのに、実際は一週間経たないと使えない、みたいな時間的なものもあり得ます。

UCDフレームワークで考えてみる

Wanoのグループ会社の事業の例で考えてみました。D は実装部分なので、わからない場合は省略しますというのと、関わっていない事業も多いので、テキトーに想像しました。数分で考えたものなので、超、雑です。

なお、下記のバリューメトリックスの中で「3つの条件」と書いているのは、先に紹介した、下記の3条件のことです。

  1. ユーザーにとって理解しやすいもの
  2. ユーザーがプロダクトから得られる価値と連動している
  3. ユーザーがプロダクトから価値を得れば得るほど、大きくなる
TuneCore Japan

チューンコアジャパンのサービスです。僕はほぼほぼ関わってないので、テキトーなことを書いています。

  1. Understand(プロダクトの価値)
    • 価値
      • 音楽を発表したい・販売したいという人が簡単に配信でき、売上を得ることができる。
    • 対価
      • 機能的対価
        • 自分の曲を様々なストアに配信できる
      • 感情的対価
        • ストアに楽曲が並ぶ
        • リリースして世間の人に聞いてもらえる
        • 売上が上がる
      • 社会的対価
        • 多くの人に聞いてもらうことでアーティストとしての認知度が上がる
    • バリューメトリクス
      • リリース数
        • リリース自体が目的の場合3つの条件にあてはまるが、売上が目的の場合は3つ目の条件(価値を獲れば得るほど大きくなる)は当てはまらない
      • 楽曲の売上
        • 3つの条件に当てはまりそうです
  2. Communicate(伝える)
    • 現在のプランはリリース数による課金です
      • そもそも、フリーミアムでもフリートライアルでもないですが、クーポンやキャンペーンででフリートライアル化している場合はあります
  3. Deliver(届ける)
    • 省略
ポイントモール

僕がメインで関わっているEDOCODEの事業ですが、そもそもフリーですね。 普段から使ったほうがお得ですが、ふるさと納税とか旅行とかで割と大きい金額使うときは、使わない手はないですよ!(「自分の使っているクレジットカード名 ポイントモール」とかで検索してみてください)

  1. Understand(プロダクトの価値)
    • 価値
      • ポイントモールを経由して買い物をすることで、通常より多くのクレジットカードのポイントを貯めることができる
    • 対価
      • 機能的対価
        • クレジットカードのポイントが貯められる
      • 感情的対価
        • ポイントが溜まって嬉しい
        • たまったポイントで買い物をお得にできる
      • 社会的価値
        • 特に無い気がしますが、余計にポイントが貯められていることを自慢できるかも?
    • バリューメトリクス
      • 獲得ポイント数
        • 3つの条件にあてはまりそうです
  2. Communicate(伝える)
    • そもそもフリーなので、料金設計はないです
    • サイトに来た人に価値を伝える工夫は必要そうです
  3. Deliver(届ける)
    • モールによりますが、ポイントを獲得した結果を見ることができます(ただし、仕組み上タイムラグが大きい)
    • モールによりますが、獲得できるであろうポイントの対象ショップを見ることができます(タイムラグが数日あります)
PUSHCODE

EDOCODEのもう一つの事業です。こちらにはそんなに関わってないので、テキトーなことを書いています。

  1. Understand(プロダクトの価値)
    • 価値
      • WebPushを通じて、サイトの運営者がユーザーとより良いコミュニケーションができるようになる
    • 対価
      • 機能的対価
        • ユーザーにWebPush通知を好きなタイミングで送れることで、タイミングを逃さずユーザーにアプローチできる
        • サイトのPVを増やすことができる
        • カゴ落ち防止に使うことができる
      • 感情的対価
        • 許諾数や通知に対する反応がわかることで、サイトに集まるユーザーが増えていることが実感できる
        • カゴ落ち防止からあがった売上を確認することで、売上が上がっていることが実感できる
      • 社会的価値
        • 効果的にWebPushを使うことで集客や売上を上げることができ、社内で評価される
    • バリューメトリクス
      • 配信数
        • 3つの条件にあてはまりそうですが、コミュニケーションが片思いに終わる場合もあるので、3つ目の(価値を得れば得るほど...)条件は使い方に依存しそうです
      • 配信から上がる成果(PVや売上等)
        • 3つの条件に当てはまりそうですが、1つめの条件(理解しやすいか)はちょっと考えどころというか見せ方に工夫がいるかもしれません
  2. Communicate(伝える)
    • 現状のプランは配信数です。一定配信数以下は無料のフリーミアムなプランです
    • 配信から上がる成果をプランに組み入れるのは、利用者にわかりにくいような気はしますが、ECサイトに特化したプランとかであれば、ありかも?
  3. Deliver(届ける)
    • 省略

トリプルAスプリント

プロダクトを改善するために、何が問題なのかを、迅速に特定し、解決し、効果を測定しなければなりません。 そのためのプロセスモデルです。

  1. Analyze(分析)
  2. Ask(質問)
  3. Act(実践)
Anayze(分析)

ビジネスのインプットとアウトプットを特定し、それぞれを分析します。

アウトプットは、例えば、以下のようなものです。

  • サインアップ数
  • 有料会員へのアップグレード者数
  • ARPU(顧客平均単価)
  • 顧客解約率
  • ARR(年間経常収益)
  • MRR(月間経常収益)

これらのアウトプットを生み出しているインプットが何なのかを特定する必要があります。

Ask(質問)
  1. 目的はどこか?
    • 売上とか、ビジネスがもっている目標
  2. 目的にたどり着くには何をすればよいか?
    • 解約率、ARPU、顧客数のいずれに注力すべきか?
    • 通常、解約率を下げればARPUは改善されるので、その後に顧客数を伸ばすことでARRが伸びる
    • それぞれがビジネスにどのような影響を与えているか把握する
  3. どのインプットに賭ければ良いか?
    • 注力すべきところがわかったら、それに一番効くインプットは何なのかを特定する。

改善すべき/導入すべきインプットが見つかったら、ICEフレームワークで、優先順位をつける。

ICEフレームワークは、

  1. Inpact(影響度) ... インプットがアウトプットに与える影響度
  2. Confidence(自信度) ... インプットがアウトプットを改善する自信がどれくらいあるか
  3. East(容易度) ... どれくらい容易に導入できるか

この3つを各5点ずつ配点して、合計点が高いものが優先度が高いとみなすという方法です。

Act(実践)

優先順位が付けられたら、後は実践するだけです。

ボウリングレーン・フレームワーク

穴を開ける方じゃなくて、ピンを倒す方のボウリングです。

「プロダクトの価値を顧客が正しく体験できている状態」を、

  • 「ボール」が「顧客の現在地」
  • 「ボウリングのピン」が「得られる成果」
  • 「ピンに届くまで」が成果が得られるまでの「時間経過」

を表しています。「ボール」が「ピン」に届いたら、「顧客」が「プロダクトからの成果」を得たということになります。

例えば、超単純に

  1. Web広告からプロダクトのサイトに遷移 => ボールの開始位置
  2. プランページを読む => ピンまでの途中(1)
  3. サインアップ => ピンまでの途中(2)
  4. 何かしらの成果を得る => ピンが倒れる

といった感じです。

この過程でプロダクトからユーザーが離れてしまうことを、ボールがガターに落ちることで表しています。

やることは、まず、

  1. ストレートラインを作成する
  2. プロダクトバンパーを設置する
  3. コミュニケーションバンパーを構築する

ストレートラインとは?

ユーザーが脇道にそれないように、プロダクトの価値を最短で感じ取ってもらえる最短距離をストレートラインと呼んでいます。

ユーザーがプロダクトの成果を得るために、それに「必須のもの」「オプショナルなもの」「不要なもの」のようなラベリングをして、ストレートラインは必須なもののみにして、最短で価値を提供できるようにすべきです。

バンパー

子供がボウリングで遊ぶときに、両脇にバンパーを設置しているの見たことありますかね?あれです。 プロダクトバンパーとコミュニケーションバンパーは、ユーザーがガターに落ちないように、防御するためのものです。

プロダクトバンパー

プロダクトバンパーは下記のようなもので、必要なものを取り入れたほうが良いです。

  • ウェルカムメッセージ
    • サインアップしたタイミングでユーザーに送るメッセージ
  • プロダクトツアー
    • 最短でユーザーに価値を届けられるように、ユーザーに何をするべきなのかを伝える。余計な選択肢は隠す。
  • プログレスバー
    • 価値が体感できるまで、現時点で何%まで来ているのかを視覚的にわかるようにする
  • オンボーディング・チェックリスト
    • プログレスバーが100%まで終わった後に、やるべき細かいタスクをチェックリストとして提示する
  • オンボーディング・ツールチップ
  • エンプティステート
    • プロダクトに最初にログインしたときに、ユーザーがこれから何をしないといけないかを見せる。
コニュニケーションバンパー

コミュニケーションバンパーは以下のようなものです。

  • ユーザーオンボーディング・メール
    • ウェルカムメール
    • 利用ガイドメール
    • セールスタッチメール
    • ...
  • プッシュ通知
  • 説明動画
  • ダイレクトメール

省略しますが、このあたりに、どういう観点で作るべきか、文例まで含めて載ってますのでかなり参考になると思います。

ユーザーごとの平均収益(ARPU)をあげる

ユーザーは誰か?というのは、疑問を挟む余地がないようにも思えますが、単にアカウントを作った人ではなくて、利用料を払っているユーザーとみなしたほうが良いとのことです。 ユーザーの定義が決まったら、ARPU(ユーザーごとの平均収益)の計算は以下のとおりです。

ARPU = MRR(月間経常収益) ÷ ユーザー数

ARPUを上げることがプロダクトの成長につながります。

チャーンビーストの退治

チャーン(解約)は低いに越したことはありません。PLGでは、「顧客維持率を5%あげるだけで、売上を25-95%改善できる」と書いています。

チャーンは単にユーザー数をみるだけでは不十分で、下記の3種類を見たほうが良いとのことです。

  1. カスタマーチャーン ... 一定期間に失ったユーザー数
  2. レベニューチャーン ... 一定期間に失った売上額
  3. アクティビティチャーン ... 解約リスクがあるとされるユーザー数

おしまい

以上、Product-Led Growthのまとめでした。書籍(Webでも)の方では、実例も豊富ですし、実際どうやったら良いか、まで書かれているので、ぜひそちらを読まれるのをお勧めします。

この記事の内容はほぼ書籍の内容ですが、https://productled.com/ にも関連情報は結構あり、参考になりました。

きっとこんなまとめ読んでる暇があったら、本買えば良かったと思うことでしょう!(その前に、長いからここまでたどり着く前に本を買ってるかもしれませんが)

人材募集

現在、Wanoグループでは人材募集をしています。興味のある方は下記を参照してください。 group.wano.co.jp

EDOCODEについては、以下をご参照ください。 go.edocode.co.jp

PulseAudioの入出力やボリューム設定をコマンドで行う

AfterShokz(現:Shokz)のOpenCommを使っていますが、Ubuntu で利用していると、プロファイルがA2DPを選択されてしまい、毎朝仕事をする前に、Handsfree Head Unit(HFP)に選択し直さないといけないという作業があったんですが、いい加減ちゃんと調べようと思って調べました。

pactl list cards を使って利用可能なプロファイルを検索する

下記のコマンドを実行すると、

LANG=C pactl list cards |less |grep -E 'Name|profile'

下記のようになります(他にも色々でますが)。

        Name: bluez_card.**_**_**_**_**_**
                        Part of profile(s): a2dp_sink, handsfree_head_unit
                        Part of profile(s): handsfree_head_unit

上記の通り、下記の2つのプロファイルがあります。

  • a2dp_sink
  • handsfree_head_unit

pacmd set-card-profile でプロファイルを指定する

下記のようにpacmdコマンドに、カード名とprofileを渡して実行すると、プロファイルが切り替わります。

pacmd set-card-profile bulez_card.**_**_**_**_**_** a2dp_sink
pacmd set-card-profile bulez_card.**_**_**_**_**_** handsfree_head_unit

pacmd set-default-source で入力(マイク)も選択する

a2dp_sink を選んでいると、マイクをShokzにできないので、会議とかに使う場合は、HFPの方を選択します。その場合には、マイクも設定したいですね。

ちなみに、PulsAudio の sinks というのは、出力(スピーカー)のことで、sources というのは、入力(マイク)のことになります。

pactl list sources

で、検索できるんですけども、A2DPを選んでいると、出てきませんので、サウンド設定で変更してください。

pactl list sources |grep bluez_source 

下記のようでてきます。

        Name: bluez_source.**_**_**_**_**_**.handsfree_head_unit

pacmd set-default-source で入力を選択する。

pacmd set-default-source bluez_source.**_**_**_**_**_**.handsfree_head_unit

音量も設定する

A2DPHFPの切り替えはこれで大丈夫なんですが、mixerで同じ音量にあわせているとA2DPHFPで音量が微妙に異なって聞こえます。 骨伝導イヤホン使ってると、声質によりますが、音量が大きいと、すごいこそばゆい感覚になる時があり、大きい音量だと微妙な場合があります。

こちらもコマンドで設定できます。

※ちなみに、A2DPの出力ボリュームが0になってることに気づかず、なんで聞こえないんやろって、なってました...ので、聞こえない人は、pavucontrol --tab=4 とかで調節しましょう(以下に紹介するコマンド使ったら良いんですが)

pactl set-sink-volume で、出力の音量を設定する

下記のコマンドでsinksを特定します。これも、A2DP選んでいると、HFPの設定は出てこないので、設定で選び直してから、それぞれ特定してください。

LANG=C pactl list sinks| grep Name | grep bluez_sink

下記のように出てきます。

        Name: bluez_sink.**_**_**_**_**_**.a2dp_sink

サブコマンドのset-sink-volume で設定できます。

pactl  set-sink-volume bluez_sink.*_*_*_*_*_*.handsfree_head_unit 75%
pactl  set-sink-volume bluez_sink.*_*_*_*_*_*.a2dp_sink 90%

pactl set-source-volume で、入力の音量を設定する

下記のコマンドでsource を特定します。

LANG=C pactl list sources| grep Name |grep bluez

下記のようにでてきます。

        Name: bluez_sink.**_**_**_**_**_**.a2dp_sink.monitor

サブコマンドのset-source-volume で設定できます。

pactl set-source-volume bluez_sink.**_**_**_**_**_**.a2dp_sink.monitor 135%

マイク音量が小さいとよく言われるので、大きめにしてみました。

終わり

このコマンドを、こないだ作ったSTREAM DECKをコントロールするプログラムに組み込んで、仕事用のページとか、ZoomやMeetが、Active windowになったら、コマンドを実行するみたいにしてみました。後は、PodCastYoutubeとかのbookmark的なページにいったら、A2DPに変えるようにしてみました。

ちなみに、OpenCommについて

仕事で一日中つけっぱなしで使っていますが、かなり気に入ってます。

朝から晩まで10時間以上仕事でつけっぱなしでも(ずっと会議してるわけではないですが、僕の仕事上、会議はかなり多い)、充電切れない。 充電忘れたときでも、5分の充電で2時間通話くらいの充電してくれるので、昼休みとかに充電しとけば基本問題ないかなと思います。

欠点としては、頭痛のときには、つけっぱなしはちょっとしんどいかも、くらいです(僕は頭痛もちではないので、そんなことはあんまりない)。

STREAM DECKで遊ぶために学んだPythonのこと

STREAM DECKについて

拡張キーボードと呼ばれるもので、ボタンにショートカットを割り当てたりできるものです。

Linuxでも使えますが、Linuxで使えるGUIアプリ(streamdeck-ui)がイマイチなので、使う気になれなかった...のですが、Pythonのライブラリがあるので、自分で作れば良くね?と思って、作りました。

作ったもの

github.com

Linuxで使うようのものなのですが、Active Windowの切り替えにしか、Linuxに依存するコードがないので、もうちょいなんとかしたら良いんじゃないかとも思いつつ、他のOSなら別にこんなものを使う必要もないのだろうと思って、あまりやる気が起きないです。そして、一通り作ってしまったので、これ以上やるかな...というところもちょっと謎。

上のコードの中に、下記のような機能が入っています。

  • YAMLでキーに何を表示、割り当てられるかを書ける
    • URL からfavicon取ってくる(ルートパスに /favicon.ico 付けてるだけですが)
    • 画像として、URLを指定できる
    • ページ切り替え指定も簡単に書ける
  • Active Windowの切り替えでページを変えられる
  • アラート機能(定期的に何かをチェックして、ページを切り替える)
  • ゲーム
    • 記憶力系
    • 神経衰弱(対戦あり)
    • 三目並べ(対戦)
    • もぐらたたき
  • アプリ
    • アナログ時計
    • ストップウォッチ
    • カレンダー(現在年月日表示)

久しぶりのPython

10年以上前くらいに、Mailmanの拡張みたいなことをした覚えがあります。遠い昔なので、今回、初めて触るくらいな感じでしたが、色々やらかしつつも、割と簡単に書けました(まだ間違えてる可能性は大ですが)

今回、目的から入ってしまったので、いつもはチュートリアルみたいなのをやるんですが、特にやらずに書いてしまいました。あんまり良くないですね。 学習時間&コード書いた時間は大体2,3日くらいではないかと思いますので、まだ中途半端な理解のところばっかです。

使ったPythonのバージョン

Ubuntuのパッケージで入れたのそのまま使っていて、3.10.4となります。

実現したかったこと

下記みたいな感じでやりたいことが増えていってしまった感じです。

最初の

  1. 設定をGUIじゃなくて、設定ファイルっぽく書きたい
  2. Active Windowの切り替えを検知して、ページの切り替えをしたい

やってるうちにやりたくなったこと

  1. 設定は動的に読み直したい
  2. アプリみたいなものも作りたい
  3. ゲームも作りたい

さらにやりたくなったこと

  1. アプリやゲームも設定ファイルから読み込ませたい

関数定義(def)

def 名前(引数):
   # 処理
   return ret

引数にデフォルト値をつけたい

def hoge(x, y, z=3):
  return z

この場合、

hoge(1,2) # 3が返ります

ただ、default値が設定されていないものは、必須となりますので、

hoge()

は、エラーになります。

また、先にdefault値あり、後でなし、みたいな指定はできません。

def hoge(x=1, y, z):
  return z

これは、エラーになります。

関数内でglobal変数を使うとき

def hoge(args):
   global X

のようにします。

signal

Active Windowの切り替えなんですが、当初、スレッドとかどうせ難しいんじゃないかなぁ、と敬遠して、fork & signal でやっていました(やらなきゃよかった)。

import os

current_pid = os.getpid()
child_pid = os.fork()

if child_pid == 0:
  # 子プロセスで、stream deck のメイン動作
else:
  # 親プロセスで、active window のチェック

子プロセス側に、こんな感じで、singal に対するhandlerを登録し、

        _handler_switch     = lambda signum, frame: self.handler_switch(signum, frame)
        signal.signal(signal.SIGUSR1, _handler_switch)     # sinbal to check active window switching

親プロセスで、切り替わりが判明したら、下記のように、signal を送っていました。

os.kill(self.child_pid, signal.SIGUSR1)

なお、親プロセスには、SIGCHLDを受け取れるようにしておきましょう。

        signal.signal(signal.SIGCHLD, lambda signum, frame: self.handler_sigchld(signum, frame))

とやって、まぁ、動いていたのですが、pythonのthreadは思ったより簡単だったので、後々threadに変更しました。

クラスの書き方

signal の例を見てわかるとおりですが、今は、Classで書いてますが、最初はそうではなかったです。 Classは下記のように、定義します。

class ClassName:

  class_var = None # クラス変数
  # コンストラクタ
  def __init__ (self):
    self.member_var = None # メンバ変数
     pass
  # メソッド
  def method(self, args):
     pass

self が必要なので、Perlが頭によぎりました。

使う側では、

from "パス" import ClassName

i = ClassName()
i.method(1)

のようにして、インスタンスを取って、メソッドを呼び出すことが出来ます。

サブクラス

サブクラスはクラス名の横に(親クラス)のように定義します。

class Parent:

  def __init__(self, args):
    if args.get("data") is not None:
      self.data = args["data"]

class SubClass(Parent):

   def __init__(self, args):
      super().__init__(args) # 親を呼びたい場合。何もしたくなければ、pass と書いておけばよいです。

pass

何回かでてきましたが、python には、pass という文法上書かないと行けないけど、何もしたくない場合に、使うものがあります。

コンストラクタを定義しないといけないけど、def __init__(self): の次の行に何も書かないとエラーなので、passとだけ書いておく、と言ったふうに使います。

def __init__(self):
   pass

文字列(str)、数字(int,float)、リスト(list)、タプル(tuple)、辞書(dict)

str

"hoge"
'hoge'

特に、single quote, double quoteでの差はないです。

文字の部分を取る

"hoge"[0] # h
"hogehoge"[1:3] #og

フォーマットして文字列化する

下記のように{} に当てはまりますが、

"{}.{},{}".format(1,2,3) # 1.2.3 となる

順番を明示することも出来ます。

"{2}.{1},{0}".format(1,2,3) # 3.2.1 となる

sprintf的なことをする

また、sprintfのようなことをしたければ、

"{2:02d}.{1:3d},{0:6.3f}".format(1,2,3) # 03.  2, 1.000 のようになる

int, float

a = 1 # int
b = 1.1 #float

文字列 + 数字みたいなことをするとエラーになります。

a = '1'
print(a+1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "int") to str

数字を文字列にする場合は、str(i) のように、str で囲めばよいです。逆なら、int,floatで囲めばOK。

リスト

array = [1,2,3,4]
len(array) # 4
array[0] # 1
array.append(1) # 末尾に追加
array.insert(0, "A") # 先頭に追加
array.pop(-1) # 末尾を取る
array.pop(0) # 先頭を取る

tuple

[] ではなくて、()で囲めば、tupleとなります。

t = (1,2,3,4)
t[0] # 1
t[3] # 4

ですが、増やしたり、削除したり操作するようなことはできない(count, index しかメソッドがない)ので、固定的な決まりのデータ集合(時、分、秒とか)に使うと良さそうです。

tupleはdictのキーにも使えます。

tupleの存在をブログを書きながら思い出したので、tupleで書くべきところをリストで書いちゃってるなーと...いうところが、結構あるなー。

dict

dictは、キーバリュー構造。他言語ならハッシュとか、ハッシュマップですね。

conf = {
  "A": {
     1: 123,
     2: 223,
  },
}

こんな感じです。

dcit のサブクラスにCounterというのもあるようですが、調べていません。

docs.python.org

dict のキーを取得する

for key in conf.keys():
  print(key)

dict に対するアクセス

値を取るには、

conf["A"]

のようにしてアクセス出来ますが、未定義のものについてアクセスするとエラーになります。そのため、

conf.get("A")

のようにしてアクセスします。

a = conf.get("A")
if a is not None:
  print("Aがあるよ")

といった感じですね。

dict を key/valueセットで取り出す

for item in conf.items():
  print(i[0], i[1])

といった感じです。

条件、比較

and, or, is, not, ==, =!

なお、先の例にもありましたが、Pythonでは、おなじみの&&||は使えず、and or で条件を書きます。

True, False, Noneや、typeの比較に関しては、is が使えます(==, != でも良い)。他は、==!=で比較します。

リストの中にある値がふくまれているかどうか

array = [0,1,2]
if n in array:
  print("{} is in the list".format(n))

型のチェック

typeを使い、strintかどうかを調べます。

if type(a) is str:
    print("文字列")

無名関数

lambda 引数,.. : 式 のように書きます。

f = lambda arg: 10 + arg
f(10) # 20

yaml を読み込む

最初はハードコーディングしていたのですが、毎回起動し直すのも面倒なので、動的に読み込むことにしました。

import yaml
import time
import os

# 省略

    def load_conf_from_file(self):
        statinfo = os.stat(self._config_file)
        if self._config_file_mtime < statinfo.st_mtime
          f = open(self._config_file)
          conf = yaml.safe_load(f)

          self._KEY_CONFIG = conf["key_config"]

          self._config_file_mtime = statinfo.st_mtime

こんな感じです。

yaml には load と safe_load の2つのメソッドがありますが、セキュリティ上の理由で safe_load を使うべきです。loadは互換性のために残されているだけです。

os.stat から取れる、statinfo には、st_mtime というメンバ変数があり、変更時刻が入っています。 こちらを、インスタンスのメンバに持たせてやって、古くなったら読み直しするようにしています。

thread

当初 fork & signal でやっていたものの、やっぱり threadでやるべきだよなぁ、ということでthreadにしました。 thread間のデータのやり取りに、Queue, Event, または、単にインスタンスのメンバ変数という方法もあります。

thread とQueue

簡単なthreadプログラムは、下記のような感じになります。

import threading
import time
import queue
import sys

queue = queue.Queue()

def put(): 
    i = 0
    while True:
        i += 1
        queue.put(i)
        time.sleep(1)
        try:
            v = queue.get_nowait()
            if v["stop"]:
                break
        except:
            pass
    sys.exit()

def check():
    while True:
        v = queue.get()
        print("receive: {}".format(v))
        if v > 2:
            stop = True
            queue.put({"stop": 1})
            break

    sys.exit()

t1 = threading.Thread(target=put,args=())
t2 = threading.Thread(target=check,args=())

t1.start()
t2.start()

t1.join()
t2.join()

queue はthread間でメッセージのやり取りができますが、get の場合、取るものがないとblockします。 get_nowait() (get(False)と同じ)は、blockしませんが、エラーになるので、try が必要になります。

ですが、3つ以上のthreadで使うには使いにくいですね。

thread 間でインスタンスのメンバ変数にアクセスできる

最初、queueも使っていたのですが、使いにくい...というか、インスタンスの場合、何も考えずに値を共有できるので、そっちにしました。

import threading
import time
import sys

class Dummy:
  stop = False
  def __init__(self):
    self.i = 0

  def put(self):
      self.i = 0
      while True:
          time.sleep(1)
          self.i += 1
          if self.stop:
              break
      sys.exit()

  def check(self):
      while True:
          time.sleep(1)
          print("set in put: {}".format(self.i))
          if self.i > 2:
              self.stop = True
              break
      sys.exit()

if __name__ == '__main__':
    d = Dummy()

    t1 = threading.Thread(target=lambda: d.put(), args=())
    t2 = threading.Thread(target=lambda: d.check(), args=())

    t1.start()
    t2.start()

    t1.join()
    t2.join()

このようにするだけで、put 側で、setした値が、check側で読み取れますし、check 側でセットしたstopについても、put側で読み取れます。

滅茶楽(ロックとかしないといけないケースはあると思いますが、今回は特に不要でした)。

クラスと main を一緒に書きたい

先程の例のように、クラスとmainを一緒に書きたい場合は、

if __name__ == '__main__':

この後に、プログラムを書けばよいです。

try 構文

例外を補足するために、tryが使えます。

try:
   # 処理
except Exception as e:
  print(e)
else: # 省略化
  # 正常処理
finally: # 省略化
  # 共通処理

Exceptionのところは、具体的なエラーを書くことで、個別のエラーに対する処理が書けます。Exceptionは何にでも引っかかります。

例外時に何もしないなら、

except:
  pass

のように書くと良いです(いや、良くないのでは?)

Loop処理

for や while が使えます。

for

for i in range(0, 5):
   print(i)

while

while True:
  i += 1
  print(i)
  if i> 10:
     break

ネストしたループのbreak

若干注意が必要と言うか、label的なものがないんですね。

for i in range(1, 5):
    for n in range(1, 5):
        print("{} * {} = {}".format(i, n, i*n))
        if i * n > 15:
            break
    else:
        continue
    break

こんな感じで書くようです。わかりにくい。

下記で良いんじゃない?と思いますね。

found = False
for i in range(1, 5):
    for n in range(1, 5):
        print("{} * {} = {}".format(i, n, i*n))
        if i * n > 15:
           found = True
            break
    if found:
      break

random

0から9までのランダムな数を返す。

random.randint(0, 9)

0.2から10までの、ランダムな浮動小数点の値を返す。

random.uniform(0.2, 10)

スリープ

少数を渡してもOKです。

time.sleep(n)

datetime

日付はこんな感じ。

d = datetime.datetime.now()
y = now.year
m = now.month
d = now.day
wday = now.week_day() # 0がMonday, 6 がSunday
# 英語に変えるなら、こんな感じ
wday = {0: "Mon", 1: "Tue", 2: "Wed", 3: "Thu", 4: "Fri", 5: "Sat", 6: "Sun"}[now.weekday()]
# てか、strftime使えばいい
wday =- t.strftime("%a")

package とモジュール

package はディレクト

package_name
   |- __init__.py
   |- module_a.py
   |- module_b.py

ソート

sorted を使います。

tuple, list

sorted((1,2,3,4), reverse=True) # (4,3,2,1)
sorted([1,2,3,4], reverse=True) # [4,3,2,1]

dict をソートしたい

key に無名関数を渡すことができます。

a = {0:2, 1:1}
sorted(a.items(), key= lambda kv: kv[1]) # [(1, 1), (0, 2)]
sorted(a.items(), key= lambda kv: kv[0]) # [(0, 2), (1, 1)]

ファイルのopen

open 関数を使います。

with open(file_name, mode="w") as f:
   f.write("aaa")

みたいにします。mode は色々あります。バイナリにするときは、modeの文字の最後にbを付け加えます。

with を使っていますが、with を使うと、f はブロックが終わったときに、開放されます。

with

先程のwithの仕組みは、__enter__(self)と、__exit__(self, exc_type, exc_value, traceback) というメソッドで実現されています。自分で実装するなら、下記のように出来ます。

class Dummy:
    def __init__(self):
        pass
    
    def test(self):
        raise Exception

    def __enter__(self):
        print("ENTER")
        return self

    def __exit__(self, a, b, c):
        print("a:{}  b:{}  c:{}".format(a, b, c))
        print("EXIT")
        return True
    

if __name__ == '__main__':
    with Dummy() as d:
        d.test()

下記のようにすると、こんな感じで表示されます。

ENTER
a:<class 'Exception'>  b:  c:<traceback object at 0x7f5c6324e540>
EXIT

raise

先程使いましたが、例外を起こします。自分で例外を作る場合は、Exception を継承します。

class MyException(Exception):
    pass

class Dummy:
    def __init__(self):
        pass

    def test(self):
        raise MyException    

    def __enter__(self):
        print("ENTER")
        return self

    def __exit__(self, a, b, c):
        print("a:{}  b:{}  c:{}".format(a, b, c))
        print("EXIT")
        return True
 

if __name__ == '__main__':
    with Dummy() as d:
        d.test()

下記のように変わりました。

ENTER
a:<class '__main__.MyException'>  b:  c:<traceback object at 0x7f830fd414c0>
EXIT

httpアクセスしたい

requests というモジュールを使います。

            res = requests.get(icon_url)
            if res.status_code == requests.codes.ok:
                with open(icon_file, mode="wb") as f:
                    f.write(res.content)

動的にモジュールを読み込む

init.py で package_name以下のモジュールを全部読み込ませたい

from inspect import isclass
from pkgutil import iter_modules,extend_path
from pathlib import Path
from importlib import import_module
import re

# iterate through the modules in the current package
package_dir = extend_path(__path__, __name__)

for (_, module_name, _) in iter_modules(package_dir):
    # import the module and iterate through its attributes
    module = import_module(f"{__name__}.{module_name}")
    for attribute_name in dir(module):
        attribute = getattr(module, attribute_name)

        if isclass(attribute):
            # Add the class to this package's variables
            globals()[attribute_name] = attribute

文字列から、improt & オブジェクトの作成

m = importlib.import_module("package_name.module_name", "package_name")
o = getattr(o, "Class")(args)

PILで画像を描く

PILというモジュールで、画像を描くことが出来ます。

時計を描く

from PIL import Image, ImageDraw
import math
import datetime

class Clock:
    scale_x = 100
    scale_y = 100
    x = 0
    y = 0
    l = 0
    def __init__(self, scale_xy, xy, l):
        self.scale_x = scale_xy[0]
        self.scale_y = scale_xy[1]
        self.x = xy[0]
        self.y = xy[1]
        self.l = l

    def hour_pos (self, h, m, s):
        if h == 12:
            h = 0
        h *= 5
        h += (m / 12 + s / 60) / 60
        l = self.l * 0.7
        return self._pos(l, h)

    def min_pos(self, m, s):
        l = self.l * 0.9
        m += s / 60
        return self._pos(l, m)

    def sec_pos(self, second):
        return self._pos(self.l, second)

    def _pos (self, l, t):
        x = l * math.cos(2 * math.pi / 60 * (15 - t))
        y = l * math.sin(2 * math.pi / 60 * (15 - t)) * -1
        return [self.x + x, self.y + y]

    def get_current_hms(self):
        now = datetime.datetime.now()
        return [now.hour, now.minute, now.second]

    def get_current_clock_image(self, hms):
        im = Image.new('RGB', (self.scale_x, self.scale_y), (0, 0, 0))
        draw = ImageDraw.Draw(im)

        hour_xy = self.hour_pos(hms[0], hms[1], hms[2])
        min_xy = self.min_pos(hms[1], hms[2])
        sec_xy = self.sec_pos(hms[2])

        draw.line((self.x, self.y, hour_xy[0], hour_xy[1]), width=3, fill=(255,255,255))
        draw.line((self.x, self.y, min_xy[0],  min_xy[1]),  width=2)
        draw.line((self.x, self.y, sec_xy[0],  sec_xy[1]),  width=2, fill=(255,0,0))

        return im

    

if __name__ == '__main__':
    clock = Clock((110, 110), (50, 50), 100)
    im = clock.get_current_clock_image((10, 15, 30)) # 10:15:30
    im.save("clock.png")

こんな感じで時計が表示できます。

感想

という感じで特にまとまりもなく、使ったコードを書いていった感じですが、特に不満なく書けました。

エラーメッセージが親切だなーと、思いました。typo してるんじゃない?この変数名使いたいんじゃない?とか。

def hoge(i):
   return ii

hoge(1)

とか、やらかすと、下記のような感じで怒られます。

NameError: name 'ii' is not defined. Did you mean: 'i'?

あと、対話的interfaceで、メソッド補完してくれるので、ちょっとした動作を見たいときに便利です。

% python3
>>> a = [1,2,3]
>>> a.  # .の後にタブを打つ
a.append(    a.copy()     a.extend(    a.insert(    a.remove(    a.sort(      
a.clear()    a.count(     a.index(     a.pop(       a.reverse()  
>>> a.

なお、ドキュメントは、pydoc3 コマンドで見ることが出来ます。

JavaScriptのcanvasで遊んでいたらPCのパフォーマンスが上がった

はい。嘘です。

正しくは、PCが遅い原因がわかった。でした。顛末は最後に書いていますが、最初は、canvasの使い方から。

canvasの使い方

HTML

HTMLで下記のように書きます。

  <canvas id="canvas"  width="300" height="170"></canvas>

JavaScript

基本

cotnext
const c = document.getElementById('canvas');
const ctx = c.getContext('2d');

context を取得してから、描画をします。

ctx.beginPath()

beginPath をしないと、最後にいた座標から、線を描き出してしまう場合がありますが、これを呼んでおけば問題ないです。

stroke, fill

storke の場合は、線を描きますが、fillの場合は、中身を塗ります。

  • strokeRect, fillRect
  • stroke, fill

のように、対になっています。

座標

x= 0, y =0 は、左上になります。 最初に定義したcanvasだと、x=300, y = 170 が右下になります。

線を描く

書きたい場所に移動して、

ctx.moveTo(x, y);
ctx.lineTo(x, y);
ctx.stroke();

moveToで移動した、x, y から、lineToで指定した、x, y まで線を描画します。

最初に、moveToで、移動して、そこから、lineTo で線の終端をさし、stroke で線を描きます。

四角を描く

strokeRectを使います。

ctx.strokeRect(x, y, xの幅, yの幅)

円を描く

ctx.beginPath()。
ctx.arc( x, y, radius, startAngle, endAngle,  counterclockwise) ;
ctx.stroke();

radius は半径です。startAngleは開始角度で、endAngleは終了角度。counterclockwiseは、trueなら左回りに線を描きます。

startAngle = 0, endAngle = Math.PI * 2 であれば、真円が描けます。 半円であれば、endAngle = Math.PIです。

ctx.beginPath() をしないと、今いる座標から、円の開始点までの線も描画してしまうので、注意してください。

下向きの半円

ctx.beginPath();
ctx.arc( 10,10, 10, 0, Math.PI,  false) ;
ctx.stroke();

下向きの1/4の円

ctx.beginPath();
ctx.arc( 10,10, 10, 0, Math.PI / 2,  false) ;
ctx.stroke();

fill で中身を塗りつぶしたい場合は、

ctx.moveTo(10,10);
ctx.arc(10, 10, 10, 0, Math.PI / 2,  false) ;
ctx.fill();

fill を使う場合、moveToで移動した座標から、円の開始点から終了点を結んだ部分を塗ります。

なので、下記のようにすると、

ctx.moveTo(90,90);
ctx.arc( 40,40, 50, 0, Math.PI / 2, false) ;
ctx.fill();

外側が塗りつぶされます。

描画したものを消す

clearRect を使います。strokeRectと使い方は同じです。

ctx.clearRect(x, y, xの幅, yの幅);

これで、指定した座標の位置から、x, y の領域を削除します。消す領域ですが、若干多めに取っておいたほうが良いかもしれません。

textを描く

ctx.fillText("テキスト", x, y, 幅);

x, y の座標から幅分の領域に、指定したテキストを描画します。

x, y の位置ですが、テキストの左下が、座標の位置です。 一度描いたものを削除する場合、clearRectで指定するのは、y - フォントサイズ + アルファ くらいを指定するひつようがあります。 y の幅指定も、フォントサイズ+アルファくらいです。

フォントの指定は、下記のように行います。

ctx.font  = '9px serif';

canvas の良いところ

プログラマブルに絵を描けるので、配置場所や、スケールも自由に変更できます。




下向きにに複数の円が描かれる。

const c = new MyCircle({id: 'canvas', x: 0, y: 0, scale: 1, direction: false}); c.draw();

上向きに複数の円が描かれる。

const c = new MyCircle({id: 'canvas', x: 0, y: 100, scale: 1, direction: true}); c.draw();

と行った感じにできます。

PCのボトルネックに気づいた理由

アニメーションをしようと思って、setTimeoutで、canvasに描画しまくっていたわけです。

先程のclassに、下記のようなメソッドを追加した感じですね(実際はもっと色々やってた)。

   animate (max) {
        const ctx = this.ctx;
        max ||= 50;
        ctx.clearRect(0,0, this.canvas.width, this.canvas.height);
        const fn = (x) => {
            x ||= 0;
            ctx.beginPath();
            ctx.arc( this.x + x, this.y + x * (this.direction ? -1 : 1), x, 0, Math.PI * 2, false);
            ctx.stroke();
            if (x < max * this.scale) {
                setTimeout(() => {fn(x + 1)}, 1);
            }
        };
        fn();
    }

そうすると、描画がクソ重いのが、目に見えてわかるわけですね。

で、たまたま、外部ディスプレイが切れて、ノートPCの方の解像度が変更されたタイミングでスピードがかなり改善されました。

重い原因は何だったのか?

Ubuntuを使っているのですが、解像度の設定と、画面の拡大を指定することが出来ます。下記の赤ラインで引いたところです。

ここを調節していると、思った以上にパフォーマンスが落ちるようです。 Firefoxがまともに動かないレベルで重かったのですが、これを100%にしたら、普通に動きました。

というわけで、せっかくなので、canvasを使ったベンチマークを作りました。反復横飛びの片道で1とカウントしています。 (※遊びでつくっただけです。めっちゃ雑な計測です)

www.rwds.net

スペックは、下記な感じです。

外部ディスプレイ(3840x2160:100%) ノートPCのディスプレイ(3840x2160:100%)

外部ディスプレイ(3840x2160:100%) ノートPCのディスプレイ(1960x1200:100%)

外部ディスプレイ(3840x2160:100%) ノートPCのディスプレイ(3840x2160:125%)

外部ディスプレイ(3840x2160:125%) ノートPCのディスプレイ(3840x2160:125%)

外部ディスプレイ(3840x2160:150%) ノートPCのディスプレイ(3840x2160:125%)

外部ディスプレイ(3840x2160:125%) ノートPCのディスプレイ(1920x1200:100%)

外部ディスプレイ(3840x2160:125%) ノートPCのディスプレイ(3840x2160:100%)

結論

外部ディスプレイとノートPCのディスプレイをいずれかを拡大している場合に一番、パフォーマンスが劣化するようです。

なお、Windoows のSurface Pro 7 Core i7 1.3GHz だと、0.9/sec でした。スマホ(Xiaomi Readmi Note 10 Pro)も0.9/secでした。

IndexedDB の onupgradeneeded の実装例

最近、ブラウザに内蔵されているローカル用のDBのIndexedDBを使ってみました。

developer.mozilla.org

DBのスキーマを変更したいようなときに、onupgradeneeded を使いますが、ちょっとした説明と実装例を示します。

なお、動作確認したブラウザは、Chrome 102 です。

IndexedDBでは、DBを開くときにバージョンを渡す

下記の、1というのがバージョンです。

DBOpenRequest = window.indexedDB.open("Sample", 1);

DBがないか、バージョンが変わった場合に呼ばれる upgradeneeded

もともと、バージョンを持っていない場合、もしくは、ブラウザが保持しているバージョンがopenで指定されているものより低い場合に、 upgradeneeded というイベントが呼ばれます。addEventListenerで定義するか、onupgradeneeded に代入するかで実装できます。

下記のように、定義します。

DBOpenRequest.addEventListener("onversionchanged", event => {});
DBOpenRequest.onupgradeneeded = event => {};

event の中身

下記くらい知っていれば良いのではないでしょうか。

key value
event.oldversion ブラウザが保持しているバージョン(なければ 0)
event.newVersion openに書かれているバージョン
event.target.result IDBDatabaseオブジェクト
event.target.transaction onupgrade中のトランザクション

のようなものがあります。

event.oldversionevent.newversion の差分を確認して、必要な変更を当てる必要があります。

実装例

下記のように各バージョンごとに関数定義すると良いのではないかなと思います。

        DBOpenRequest.onupgradeneeded = event => {
            const oldVersion = event.oldVersion;
            const newVersion = event.newVersion;;
            const db = event.target.result;
            const migration = {
                "1": () => {
                    { // User master
                        const s = db.createObjectStore('user', {keyPath: "nickname"});
                        s.createIndex("nickname", "nickname", {unique: true});
                    }
                    { // logs
                        const s = db.createObjectStore("logs", {keyPath: "date"});
                        s.createIndex("date", "date", {unique: false});
                        s.createIndex("content", "content", {unique: false});
                    }
                },
                "2": () => {
                    { // logs (date => loggedDate に変えたくなった)
                        db.deleteObjectStore("logs");  // 消して作り直してます。中身とって、入れ直しとかしたら良いですね。
                        const s = db.createObjectStore("logs", {keyPath: "date"});
                        s.createIndex("loggedDate", "date", {unique: false});
                        s.createIndex("content", "content", {unique: false});
                    }
                },
                "3": () => {
                    { // logs (titleいるよね)
                        const s = event.target.transaction.objectStore("logs"); // transaction からとってこないとエラーになります
                        s.createIndex("title", "title", {unique: false});
                    }               
                }
            }

           // 最新バージョンになるまで差分を適用
            for (let v = oldVersion + 1; v <= newVersion; v++) {
                if (migration[v]) {
                    migration[v]();
                }
            }
        };

versionchange イベント

これは、試せてないのですが、別のタブとかで開いているときに、片方でDBのバージョンアップが走った場合に、起きるイベントのようです。 勝手にアップデートされる前に、DBに保存すると行った処理を実装するために使うようです。

例として、A, B のタブでSampleというDBを使っている。

そのときに、A で、upgradeが走るときに、B にversionchange イベントが発生するようです。

下記に詳しく載っています。

www.w3.org

React事始め

もはや、無数にありそうな話題なので、あまり公に書く意味はないですが、記録しとかないと忘れちゃうので(年齢的な問題で)。

下記の2つのドキュメントを見ながら進めていきますが、主にガイドの方の内容になります。 ja.reactjs.org ja.reactjs.org

Vue.jsはちょっと触ったことありますが、Reactは初めて触ります。

その前に、nvm のインストール

PC変えてから、まともにnode使ってなかったので、nvmからインストール

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
source ~/.zshrc 
nvm install --lts 16
nvm use 16

nvm以外にも色々あるようですね。

cam-inc.co.jp

開発準備

ここから、チュートリアルと主にガイドの内容を元に進めます。

npx create-react-app my-app
rm src/*
touch src/index.css

src/index.js に下記の内容を記載

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';

これで、下記で、http://localhost:3000 がブラウザで開きます。

npm start

ポートを変更したい場合は、

echo "PORT=3001" > .env

のようにして、.env内で、PORTを指定してやればよいです。

Hello, world! を表示する

index.js に下記を追加します。

const root = ReactDOM.createRoot(document.getElementById('root'));
const element = (<h1>Hello, world!</h1>);
root.render(element);

下記が表示されました。めでたし。

JXS

先程書いた、<h1>Hello, world!</h1> ですが、JSXというものです。JSXを使う理由は下記のように説明されています。

表示のためのロジックは、イベントへの応答や経時的な状態の変化、画面表示のためのデータを準備する方法といった、他の UI ロジックと本質的に結合したものであり、React はその事実を受け入れます。

マークアップとロジックを別々のファイルに書いて人為的に技術を分離するのではなく、React はマークアップとロジックを両方含む疎結合の「コンポーネント」という単位を用いて関心を分離します。後のセクションでコンポーネントについては改めて詳しく紹介しますが、現時点で JavaScript にマークアップを書くことが気にくわない場合、こちらの議論で考えが改まるかもしれません。

React で JSX を使うことは必須ではありませんが、ほとんどの人は JavaScript コード中で UI を扱う際に、JSX を見た目に有用なものだと感じています。また、JSX があるために React は有用なエラーや警告をより多く表示することができます。

なお、タグの中身がない場合(imgタグなど)は、XMLのような感じで、記述します。

<img src="/path/to/file" />

また、下記のようにJXS内で改行しても問題ありません。

const element = (<h1>
        Hello, {user.name}!
        </h1>
);

JSX内で、JavaScriptを実行する

JSX内では、{} の中に JavaScriptの処理を書くことができます。

const user = {
  name: "ktat"
};

const element = (<h1>Hello, {user.name}!</h1>);

このように変数を参照するだけでも、下記のように、関数を呼び出すこともできます。

function displayName (u) {
  return u.name;
}
const user = {
  name: "ktat"
};

const element = (<h1>Hello, {displayName(user)}!</h1>);
root.render(element);

{} をタグの属性の値の指定に利用する

<div class="name">name を設定したい場合です。

const user = {
  name: "ktat"
};

const element = (<h1 class="{user.name}">Hello, {user.name}!</h1>);
root.render(element);

上記のようにしたら...と思いますが、これだと、そのままの内容がクラス名となってしまいます。

クォートが不要です。下記のようにしましょう。

root.render(<h1 class={user.name}>Hello, {user.name}!</h1>);

無事に、クラスが設定できました。

と、思いましたが、class ではなくて、className と指定するのが、正しいようです。consoleに下記のメッセージが出ました。

react-dom.development.js:86 Warning: Invalid DOM property `class`. Did you mean `className`?
    at h1

なお、クラス名を複数渡したい場合は...、JavaScriptがかけるので、どのようにでもできます。

テンプレートリテラル

className={`${var1} ${var2}`}

関数(スペースで文字列を join する、classNamesという関数を作る)

className={classNames(var1, var2)}

文字列をHTMLとして使いたい

下記のようにすると、

let s = "&lt;";
root.render(<div>{s}</div>);

< が表示されるのではなくて、&lt; がそのまま表示されます。セキュリティのためだと思いますが、あえて、HTMLとして評価したいような場合は、 dangerouslySetInnerHTMLを使います。

    root.render(<span dangerouslySetInnerHTML={{__html: s}}></span>);

インストールが必要ですが、html-react-parser も使えます。

html-react-parser - npm

const htmlParse = require('html-react-parser');

しておいて、下記のように使えば、HTMLとして解釈されます。

let s = "&lt;";
root.render(<div>{ htmlParse(s) }</div>);

JSXとは?

JSXはオブジェクト表現であり、JXSの返すものをReact要素と呼びます。詳しくは、こちらを。

const element = (<h1>Hello, world!</h1>)

は、

const element = React.createElement(
  'h1',
  'Hello, world!'
);

の呼び出しにコンパイルされ、結果として、

const element = {
  type: 'h1',
  props: {
    children: 'Hello, world!'
  }
};

下記のようなオブジェクトになります。これをReact要素と呼びます。

{}で書いたJavaScriptも評価された結果のオブジェクトになります。

レンダーされた要素を更新する

React要素は一度要素を作成すると、子要素や属性を変更することができません。 前述の通り、変数や関数を指定したとしても、すでに評価済みなので、renderの後に、こんなことをしても何も起きません。

root.render(element);
 
setTimeout(() => { user.name = "hogehoge!"; root.render(element) }, 1000)

renderの後に、下記のように新しくReact要素を作って、renderすれば、更新はできます。

root.render(element);
 
setTimeout(() => {
  user.name = "hogehoge";
 
  const element2 = (<h1 className={user.name}>
        Hello, {displayName(user)}!
        </h1>
  );
 
  root.render(element2);
}, 1000);

1秒後に、ktat が、hogehoge に変わるようになります。

しかし、あまりにアレすぎるので、React要素を作るところを関数化してみます(※いずれにしても良い例ではありません)。

React要素を作る部分を関数化する

これで、1秒ごとに、切り替わって、何回目かも表示されるようになりました。

const root = ReactDOM.createRoot(document.getElementById('root'));
 
let n = 0;
function hello() {
    const userName = n++ % 2 == 0 ? 'ktat' : 'world';
    const element = (<h1>
        Hello, {userName}!<br />
        (count: {n})
        </h1>
    );
    root.render(element);
}

setInterval(hello, 1000);

※ root.render を何回も呼び出していますが、普通は、一回しか呼び出さないです。state付きコンポーネントでそれを実現します。と、書いてますね。

なお、Reactでは(というか、Vueとかもですが)、必要な部分のみが更新されるので、上記の例だと、world,ktat の部分、と、count: の右の数字の部分。動的に変更される部分以外は、変更されません。Developer tool でみるとわかります。

コンポーネントとprops

JavaScriptのfunctionを大文字で始めると、コンポーネントとなります。

function Hello(props) {
    return (<h1>
        Hello, {props.name}!
        </h1>
    );
}

これは、JSXで下記のように使えます。

const element = <Hello name="ktat" />;
root.render(element);

することで、以前と同様、Hello, ktat! が表示されます。

コンポーネント化したことで、下記のように名前を変えて、同じものを複数表示するのも簡単です。

const element = (
        <div>
          <Hello name="ktat" />
          <Hello name="world" />
          <Hello name="Internet" />
        </div>
);
 
root.render(element);

なお、propsは読み取り専用なので、変更してはいけません。

※最初、下記のように記載したのですが、エラーになりました。理由は追っていません。

const element = (
          <Hello name="ktat" />
          <Hello name="world" />
          <Hello name="Internet" />
);

state とライフサイクル を使う

state を使うためには、関数コンポーネントをクラスに変更する必要があります。

前述の、

function Hello(props) {
    return (<h1>
        Hello, {props.name}!
        </h1>
    );
}

こちらは、下記のように書いても等価となりますが、下記の形式で記載する必要があります。

class Hello extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

これに、state を導入します。

class Hello extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  render() {
    return <h1>Hello, {this.props.name} at {this.state.date.toLocaleTimeString()}</h1>;
  }
}

このままだと、時間が表示されるだけで、最初に表示されたままで変わりません。

マウント・アンマウント時にメソッドを実行する

Reactでは、DOMが描画されることをマウントといい、DOMが削除されるときをアンマウントといいます。 コンポーネントクラスにcompnentDidMountというメソッドを追加すると、マウントした後(DOMがレンダーされた後)に実行できるようになります。 また、componentWillUnmountというメソッドを追加することで、コンポーネントがアンマウントされて破棄される直前に呼び出されるようになります。

これを利用して、マウント時にtimerをセット、アンマウント前にtimerをクリアする(現在の例ではアンマウントするところがないので、ここでは特に不要といえば不要です)ようにします。 追加するものは、下記になります。

  componentDidMount() {
    this.timerID = setInterval(
      () => this.setDate(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

componentDidMount内で呼び出されているsetDateは、下記のようなもので、this.setStateを呼び出しています。これによって、Reactがstateが変更されたことがわかり、renderを再呼び出しします。

  setDate() {
    this.setState({ date: new Date() });
  }

これで、時間が動的に変わるようになりました。全体のコードは、下記のようになります。

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';

const root = ReactDOM.createRoot(document.getElementById('root'));


class Hello extends React.Component {
  constructor(props) {
    super(props);
    this.state = { date: new Date() };
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.setDate(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  setDate() {
    this.setState({ date: new Date() });
  }

  render() {
      return <h1>Hello, {this.props.name} at {this.state.date.toLocaleTimeString()}</h1>;
  }
}

const element = (
        <div>
          <Hello name="ktat" />
          <Hello name="world" />
          <Hello name="Internet" />
        </div>
);

root.render(element);

注意事項

setStateを必ず使う

this.state = { date: new Date() };

のように最初に定義しているのだから、setDate() でも、同様にすれば良いと思うかもしれませんが、setState を利用しないと、再Renderはされません。なので、必ず、stateを更新する際は、setStateを使います。

this.state に直接代入できるのは、constructorのみです。

this.propsthis.state は非同期に更新される

ここの例のままですが、下記のようにするのは、NGで、

// Wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});

下記のように、setStateに関数を渡す形にしないといけません。

// Correct
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));

setState での更新はマージされる

this.state = { a: 1, b: 2, c: {d: 1} };

のように、constructorで定義したとして、

setState(c:{e: 2});

のように更新すると、下記のように変更されます。

{
  a: 1,
  b: 2,
  c: {e: 2}
}

変更しなかったa,bは、そのままですが、変更されたc はまるっと変更({d: 1} => {e: 2})されます。

イベント処理

下記のように書くことで、aタグをクリックした際に、alertが表示されるようになります。

function  clickAtag() {
  alert(1);
}

const element = (
        <div>
          <a href="https://exapmle.com/" onClick={clickAtag}>click</a>
        </div>
);

root.render(element);

このままだと、https://example.com/に移動しますが、移動したくない場合は、falseを返すのではなくて、preventDefaultを使います。

function  clickAtag(e) {
  e.preventDefault();
  alert(1);
}

ここで使われている、eは、合成イベントと呼ばれるもので、ブラウザの互換性は気にしなくて良いとのことです。

Reactでは、addEventListenerを使うのではなくて、最初に要素を作成する際に、リスナを指定するとのことです。

コンポーネントクラスの中に書く

その場合は、下記のようにします。先に使った例のものに、入れます。

  render() {
      return (<div>
                <h1>Hello, {this.props.name} at {this.state.date.toLocaleTimeString()}</h1>
                <a href="https://exapmle.com/" onClick={this.clickAtag}>click</a>
              </div>);
  }
  clickAtag(e) {
    e.preventDefault();
    alert(1);
  }

イベント処理でstateを更新する

特に難しいことはないはずですね。今回、componentDidMount とかは不要なので削除しています。

class Hello extends React.Component {
  constructor(props) {
    super(props);
    this.state = { date: new Date() };
  }

  clickAtag(e) {
    e.preventDefault();
    this.setState({ date: new Date() });
  }

  render() {
      return (<div>
                <h1>Hello, {this.props.name} at {this.state.date.toLocaleTimeString()}</h1>
                <a href="https://exapmle.com/" onClick={this.clickAtag}>click</a>
              </div>);
  }
}

ですが、エラーになります。clickAtag内で、this が使えません。 下記のようにする必要があります。

                  <a href="https://exapmle.com/" onClick={this.clickAtag.bind(this)}>click</a>

これで、クリックするたびに時刻が変わるようになりました。

条件付きレンダー

これは、普通にif とか使ってるだけなので、特別なことはなさそうです。 例えば、時刻の末尾が0と5のときだけ、赤くしたいなら、

class Hello extends React.Component {
  constructor(props) {
    super(props);
    this.state = { date: new Date() };
  }

  clickAtag(e) {
    e.preventDefault();
    this.setState({ date: new Date() });
  }

  render() {
      let t = this.state.date.toLocaleTimeString();
      let c = t.match(/[05]$/) ? "color: red" : "color: black";
      return (<div>
                <h1>Hello, {this.props.name} at {t}</h1>
                <a style={{color: c}} href="https://exapmle.com/" onClick={this.clickAtag.bind(this)}>click</a>
              </div>);
  }
}

※ styleについても、全体を文字列として"color: red" のようには渡せないようです。

レンダーさせたくない

なお、rednerさせたくない場合は、nullを返すようにします。

以下のようにすると、0秒と5秒のときにクリックする(か、最初のrender時が0秒か5秒なら)と、コンポーネントはなくなってしまいます。

        if (t.match(/[05]$/)) {
            return null; 
        }

インラインの条件

下記のように、JSX内に条件を書くこともできます。

  render() {
      let t = this.state.date.toLocaleTimeString();
      return (<div>
                 <h1>Hello, {this.props.name} at {t}</h1>
                 <a style={{color:  t.match(/[05]$/) ? "red" : "black" }} href="https://exapmle.com/" onClick={this.clickAtag.bind(this)}>click</a>
              </div>);
      }
  }

&&|| も使えますが、falseな場合でも、条件の左の結果自体は、レンダリングされますので注意してください。

        let t = this.state.date.toLocaleTimeString();
        let n = 0
        return (<div>
                 <h1>Hello, {this.props.name} at {t}</h1>
                { n  && "not 0"}
              </div>);
        }

0が表示されてしまいます。

true/falseであれば、renderingされないので、結果が欲しくなければ、下記のようにtrue/falseが返るようにしてやればよいです。

        let t = this.state.date.toLocaleTimeString();
        let n = 0
        return (<div>
                 <h1>Hello, {this.props.name} at {t}</h1>
                { n !== 0  && "not 0"}
              </div>);
        }

一度変数に入れる

以下のように、一度変数に入れてもOKです。

  let term = 1;
 let component = "";
  if (term === 1) {
     component = <h1>aaa</h1>
  }
  return (<div>{component}</div>)

リストとkey

リストを使って、同じコンポーネントを複数表示させましょう。

  render() {
        let t = this.state.date.toLocaleTimeString();
        let names = ["World", "Internet"]
        let element = names.map((name) => <h1>Hello, {name}</h1>)
        return (<div>{element}</div>);
  }

ちゃんと表示されますが、consoleにwarningがでます。

Warning: Each child in a list should have a unique "key" prop.

素直にしたがって、keyとして同じものを与えてみます。

  render() {
        let t = this.state.date.toLocaleTimeString();
        let names = ["World", "Internet"]
        let element = names.map((name) => <h1 key={name}>Hello, {name}</h1>)
        return (<div>{element}</div>);
  }

これで、warningは消えます。

keyは要素の変更、追加、削除等をReactが識別ために使うということのようで、ユニークなID(兄弟要素の中でユニーク。グローバルでユニークでなくて良い)を使うべきです(別にHTMLのタグ内のidとかになるわけではないので、あくまで内部で管理されているはず)。

具体的にどう使うかは、まだ不明。

フォーム

フォームは、ユーザーが変更可能なものなので、そのままだとReactの制御下にならないのですが、valueにstateを入れることで、制御されたコンポーネントとすることができます。

class Hello extends React.Component {
  constructor(props) {
      super(props);
      this.state = { nValue: "", rValue: "", tValue: "" };
  }
  handleText(e) {
    this.setState({nValue: e.target.value});
  }

  handleTextArea(e) {
    this.setState({tValue: e.target.value});
  }
 
  handleRadio(e) {
          console.log(e);
    this.setState({rValue: e.target.value});
  }
 
  render() {
        return (<div>
                <form>
                <input type="text" name="n" value={this.state.nValue} onChange={this.handleText.bind(this)} /><br />
                <ul>  
                  <li><input type="radio" name="r" value="1" onChange={this.handleRadio.bind(this)} />1</li>
                  <li><input type="radio" name="r" value="2" onChange={this.handleRadio.bind(this)} />2</li>
                  <li><input type="radio" name="r" value="3" onChange={this.handleRadio.bind(this)} />3</li>
                  <li><input type="radio" name="r" value="4" onChange={this.handleRadio.bind(this)} />4</li>
                </ul>
                <textarea value={this.state.tValue} onChange={this.handleTextArea.bind(this)} /><br />
                nValue: {this.state.nValue}<br />
                rValue: {this.state.rValue}<br />
                </form>
                </div>);
  }
}

このようにすると、入力されたテキストや、選択された値が、下記の部分に動的に入ることになります。

                nValue: {this.state.nValue}<br />
                rValue: {this.state.rValue}
                tValue: {this.state.tValue}

他のformタグや複雑な使い方については、ドキュメントを読みましょう(そろそろ疲れてきた)

ちなみに、bind(this)をいっぱい書いてますが、constructorで下記のようにすれば、

  constructor(props) {
      super(props);
      this.state = { nValue: "", rValue: "", tValue: "" };
      this.handleRadio = this.handleRadio.bind(this); // こう書いておく
  }

としておけば、下記のように書くだけでOKです(じゃぁ、そう書けよっていう)。

{this.handleRadio}

stateの共有(リフトアップ)

説明は割愛していますが、state は各コンポーネントで独立していますので、一方で変更したものは、他方ではわかりません。 ですが、共有したいようなときもあるので、その場合は、以下のようにします。

2つのコンポーネントで入力されたものを別のコンポーネントで足し合わせるといったものとか。

そろそろ、疲れてきて、ドキュメントの通りにやっていないんですが、下記な感じでできますね。良いのかな。

lass Hello extends React.Component {
  constructor(props) {
      super(props);
      this.name = props.name
      this.state = { nValue: ""};
  }
  handleText(e) {
    this.setState({nValue: e.target.value});
    this.props.state.v[this.name] = e.target.value;
    this.props.change();

  }

  render() {
    return (
                <div> 
                <form>
                <input type="text" name="n" value={this.state.nValue} onChange={this.handleText.bind(this)} /><br />
                nValue: {this.state.nValue}<br />
                a: {this.props.state.v.a}<br />
                b: {this.props.state.v.b}<br />
                </form>
                </div>
    );
  }
}

class HelloSum extends React.Component {
  constructor(props) {
      super(props);  
      this.state = { v: {a: "", b: ""} };
  }
  changeState() {
      this.setState({});
  }

  render() {
        return (<div>
                <Hello name="a" state={this.state} change={this.changeState.bind(this)} />
                <Hello name="b" state={this.state} change={this.changeState.bind(this)} />
                mixed: {this.state.v.a + this.state.v.b}
                </div>);
  }
}

const element = (
        <div>
          <HelloSum name="" />
        </div>
);

root.render(element);
  1. HelloSumコンポーネントから、Helloに対して、自身のstateと、setStateするだけの関数をpropsとして渡す。
  2. Helloでは、textが変更されたら、handleText内で、this.props.stateを更新し、this.props.change()を呼び出して、空のsetState()をすることで、Renderする

ドキュメントだと、こんな感じな気がする。

class Hello extends React.Component {
  constructor(props) {
      super(props);
      this.name = props.name
      this.state = { nValue: "" };
  }
  handleText(e) {
    this.setState({nValue: e.target.value});
    this.props.change(this.name, e.target.value);

  }

  render() {
    return (
                <div>
                <form>
                <input type="text" name="n" value={this.state.nValue} onChange={this.handleText.bind(this)} /><br />
                nValue: {this.state.nValue}<br />
                a: {this.props.a}<br />
                b: {this.props.b}
                </form>
                </div>
    );
  }
}

class HelloSum extends React.Component {
  constructor(props) {
      super(props);  
      this.state = { v: {a: "", b: ""} };
  }
  changeState(name, v) {
      let d = this.state;
      d.v[name] = v
      this.setState(d);
  }

  render() {
        return (<div>
                <Hello name="a" a={this.state.v.a} b={this.state.v.b} change={this.changeState.bind(this)} />
                <Hello name="b" a={this.state.v.a} b={this.state.v.b} change={this.changeState.bind(this)} />
                mixed: {this.state.v.a + this.state.v.b}
                </div>);
  }
}

const element = (
        <div>
          <HelloSum name="" />
        </div>
);

root.render(element);
  1. props として、親コンポーネントのstateと、状態を変更するための関数を渡す
  2. コンポーネントでは、propsからもらっている値を表示しておく
  3. コンポーネントから入力されたら、親から渡された関数を呼んで、setStateが呼ばれて更新される

コンポジション vs 継承

継承は不要ということのようです。

ja.reactjs.org

React の流儀

後は、読み物なので、良いでしょう。

ja.reactjs.org

終わり

試しつつ、Blog書きながらというところもありますが、5-6時間かかったかな。疲れた。

参照

途中で言及していた、classNameとか、styleのJSXの記法とかは、下記を見ておく必要がありそうですね。

UXデザインの法則を読んで感銘を受けたのでまとめた

本の概要

会社の人に勧められて読んでみたところ、とても良かったです。 行動心理学の側面でユーザーにとって良いUXについて書かれています。

150ページ程度の本ですが、10の法則について、端的にまとまっており、事例もあるため、非常にわかりやすく、読んでいて納得感があります。

各法則を簡単に紹介したいと思います。

1. ヤコブの法則

ヤコブといえば、ユーザービリティエンジニア原論ですかね。 この法則は、「慣れ親しんだ見た目であれば同じように動くと、ユーザーは期待する」というものです。

どんな話かといえば、ドアノブのついたドアが、引き戸だったら、戸惑いますよね。そういう話です。 ドアノブがあれば、回してから、押すなり引くなりするのを想定するのが普通です。

新しく何かを作る場合でも、世の中にあるものを踏襲したほうがユーザーはわかりやすいです。 また、既存のものを大きく変えたい場合は、古いものも使えるようにしておかないと、ユーザーは離れていってしまう。

GoogleとかAWSとかコンソールのUIをちょいちょい変えますが、旧バージョンを使えるようにしていますね。

AWSのEC2のコンソール。UIを切り替えることができます。

2. フィッツの法則

「クリックさせたいとか、タップさせたいとかいう、ターゲットについて、十分に大きく、他のターゲットと十分に離れていて、簡単に届かないといけない」というものです。

スマホのソフトウェアキーボードとか、キー同士が十分に離れてないと押し間違えてしまいます。ただ、離れすぎていても使い勝手は悪いです。 OKとキャンセルが近接していて、十分に間隔が開いていなければ押し間違えてしまいます。よく使うターゲットがスマホの右上とか左上とかにあると、押しにくい...とかそういうことですね。

ラベルとフォームが並んでいるときに、ラベルをタップしたら、フォームにフォーカスが当たるというのも、この法則に則っています。

3. ヒックの法則

「意思決定をさせたいなら選択肢は少ないほうが良い」というものです。

例えば、有料プランが10種類もあると、ユーザーは何を選択したらいいか比較検討するものが多すぎるし、意思決定がなかなかできない。 なので、たいていサービスで有料プランは3つ程度に抑えられているし、さらに、おすすめのプランみたいな表示をして、ユーザーが意思決定にかける負担を減らしています。

tandem.chat の pricing のページ

Google Workspace の pricing のページ

もし複雑な意思決定が必要だったとしても、細かく分解して、それぞれの意思決定を小さくすればユーザーの負荷も減ります。

人間も、コンピュータ同様ワーキングメモリがあって、情報が多すぎるとワーキングメモリから溢れてしまう。 そうすると、見るべき情報が落ちてしまったり、イライラしてやめてしまったりする。

タスクいっぱい詰め込んでると、見落としが多くなったりしますね。わかりみが激しい...。

意思決定だけではなくて、ナビゲーションが多すぎるメニューや、機能が多すぎるリモコンなど、ユーザーが選択するのに、必要な情報が多すぎると、処理できなくなって諦めてしまう。

ただ、逆に単純化しすぎて、アイコンだけみたいになってしまっても、アイコンの意味を推測したりするのに負荷がかかっても良くないので、アイコンはラベルと一緒にしたほうが良いとか紹介されていました。

4. ミラーの法則

「人間が短期的に記憶できるのは、7(±2)まで」というものです。

これは誤解が多いというように紹介されていますが、7(±2)の文字数にしないといけないのではなく、ある程度の塊にチャンク化すれば良いということです。 携帯電話番号は11桁ですが、3-4-4の桁数で3つにチャンク化されているので、読みやすいし、覚えやすい。

チャンク自体の大きさが…という話でもなく、例えば、長い文章をだらだら書くのではなく、階層化や構造化する。 ちゃんと分類して、他と区別が付くようなチャンクにするということが重要とのことです。

5. ポステルの法則

「入力については寛容に扱い、出力については厳密に扱うべき」というものです。

住所は全角のみで入力しないといけない...というようなサイトがありますが、典型的な悪い例です。 全角・半角だろうが入力はどちらも許して、内部で正規化してやればいいだけです。

また、昨今ではいろいろなデバイスからアクセスされることがあります。PCだけしか、スマホだけとか、じゃなくて、両方から入力できるようにするほうが良いし、なんなら、ウエアラブルデバイスとかTVとかゲーム機とかVR機器から入力される場合もありえます。

サービスによっては、色んな言語を使う人からアクセスされる場合もありますので、言語によって単語の長さも異なります。 それに合わせてメニューを多少削ったりする必要もあるということにも言及されています。

6. ピークエンドの法則

「人間の記憶に残るのは、感情のピークのとき(ポジティブでもネガティブでも)と、最後に近いところ。また、人はネガティブな感情のところの方を思い出しやすい」というものです。

客観的、全体を平均的に見れば「良い」と判断できる事象でも、最後にネガティブな感情になると、それが非常に強く残るというものです。 遊園地に行って楽しかったけど、後半は雨に振られてびしょぬれになって散々だったとか。楽しかった記憶よりも、後半の悲惨な記憶のほうが残りやすい、みたいな感じです。

事例として、MailChimpというサービスでは、ユーザーがメールを初めて送ったらハイファイブする画像とメッセージがでるそうです(この話はProduct Led Growth)でも例にあがっていました。 Slackとかでも、all unread の未読をなくしたら、楽しげな画像がでたりしますね。

↑ Slackの all unread を全部読んだときの表示(The world is oyster については、こちらに解説がありました)

twitterのクジラ、Githubの404、Googleで検索結果が見つからなかった時に表示しているページがちょっと楽しい感じになっているのは、ネガティブピークを、ネガティブなもので無くす役目があるようです。

Googleで検索結果が見つからなかった時のページ(ちなみにクリックするとなにか釣れます)。

7. 美的ユーザビリティ効果

「デザインが美しいと、使い勝手が良いと思ってしまう」というものです。

明らかに使い勝手がわるいUIでも、デザインが美しいと、使いやすいと思ってしまう。

もし、本当にユーザビリティだけをテストをしたいなら、簡素なデザインでテストしたほうが良さそうです。

でも、本番ではデザインが美しければ、ユーザビリティの不備をごまかすことができるかもしれません。 

8. フォン・レストフル効果

「似たものが並んでいると、その中で異なるものが記憶に残りやすい」というものです。 逆に言うと、全部同じような感じだと記憶には残らないということですかね。

重要なアクションや情報は、色を変えるとかコントラストを変えるなどで注意を引くようにすると良いというものです。

フィッツの法則のところにあった、料金のところで、目立たせるというのは、これにも関連しそうですね。

ただ、コントラストの差も多用しすぎると逆に目立たなくなります。また、色覚障害の人(日本国内で320万人、男性は20人に1人が色覚に障害があるとのことです)にも考慮しないと、色を変えても一部の人には意味がないことになりそうです。

9. テスラーの法則

「どんなシステムでも、それ以上複雑さを減らすことができない、複雑性保存の法則というものがある」というものです。 本に挙げられている例ですと、メールを送る場合、From, To, Subject, Body というのが必須の複雑性としてあります。

この複雑性の負荷をシステムかユーザーが請け負う必要があります。

  • Fromは、ユーザー自身ですので自動的にシステムで決定できます。
  • Toは、ユーザーが入力することができますが、部分入力で推測等はシステムでできます。
  • Subject, Body は、ユーザーが書く必要があります(Bodyは、Gmail だと、アシストしてくれたりしますね)。

ECサイトの住所入力で、郵便番号を入力すれば住所を補完してくれるのも、システムがユーザーの代わりに請け負っているということです。

自分たちの作るシステムの複雑性を考慮して、システムに任せることができるなら、できる限りシステムにやらせるようにすれば、システムの複雑性に対するユーザーの負荷は減りますね。

10. ドハティのしきい値

「0.4秒未満でレスポンスを返すべき」というものです。

  • 0.1秒未満の遅延 ... ユーザーは気づかない
  • 0.1-0.3秒の遅延 ... ユーザーが気づき始める
  • 1秒以上の遅延 ... ユーザーは他のことを考え始める

最近のコンテンツ量の増大(昔(20年以上前)は100KB未満にしろとか書いてたな)とか、スマホのレイテンシとかを考えると、すべてを0.4秒未満に返すことはなかなか難しいと思いますが、

  • 0.4秒未満でガワだけ出しておいて、画像等を遅延ローディングする
  • ロード中を表す画像を表示する
  • プログレスバーを表示する(不正確でもOK)

といったことを行うことで、ユーザーに速くレスポンスを返す、動いているということを伝える、ということが重要とのことです。 プログレスバーは不正確でも良いとありますが、ファイルを大量コピーするときのプログレスバーは明らかに嘘やろ...という気分にしかなりませんが。

一方で面白いことが書いてありました。人が想像するよりも、あまりに速すぎると、逆にちゃんと動いているのか疑ってしまう、ということもあるようです。 例えば、100GBのファイルのアップロードに1秒かからなかったら、嘘でしょと思ってしまうと言った感じです(まぁ、これは嘘でしょが正しいでしょう)。

例えば、1000人のユーザーにメールを送るという処理が、0.1秒で終わったら、本当に終わってんのかな?って思う人もいるかもしれませんね。

終わり

というわけで、10の法則を簡単に説明してみました。実際の書籍には、論拠とかが書いてあったり、例がもっと豊富にあるので、ちゃんと読んでもらったほうが絶対良いと思います。法則以外にデザインシステムの話等にも触れています。

なんらかのサービスや、システムに関わるなら必読書じゃないかな、と思いました。