tag:crieit.net,2005:https://crieit.net/magazines/dala00/Twitter%E3%82%92%E4%BD%9C%E3%82%8B/feed [連載] Twitterを作るの投稿 - Crieit Crieitで連載「[連載] Twitterを作る」の最近の投稿 2018-10-26T12:48:25+09:00 https://crieit.net/magazines/dala00/Twitter%E3%82%92%E4%BD%9C%E3%82%8B/feed tag:crieit.net,2005:PublicArticle/14297 2016-08-09T01:11:30+09:00 2018-10-26T12:48:25+09:00 https://crieit.net/posts/Twitter-5 Twitterを作る 第5回 フォロー <p>引き続き他のユーザーをフォローする部分を進める。</p> <h3 id="モデル"><a href="#%E3%83%A2%E3%83%87%E3%83%AB">モデル</a></h3> <pre><code class="python">class Follow(models.Model): """フォロー""" user = models.ForeignKey(User, verbose_name='ユーザー', related_name='follows') target = models.ForeignKey(User, verbose_name='フォローユーザー', related_name='followers') created = models.DateTimeField(auto_now_add=True) </code></pre> <p>モデル定義はこれだけ。<br /> view側ではこれの一覧をjsonで返したり、<br /> あとは追加削除するだけで特筆することもない。</p> <h3 id="フォローボタン"><a href="#%E3%83%95%E3%82%A9%E3%83%AD%E3%83%BC%E3%83%9C%E3%82%BF%E3%83%B3">フォローボタン</a></h3> <p>ボタンはコンポーネント化し、画面のどこに配置しても同じように動作するようにする。<br /> 今回も実際に画面右のおすすめユーザーの一覧に表示したものと、<br /> そこのユーザー名をクリックして他ユーザーのプロフィール画面に表示されるものは同じもの。</p> <pre><code class="javascript"><MainFollowButton user={this.props.user} follows={this.props.follows} onFollowsUpdated={this.props.onFollowsUpdated} /> </code></pre> <p>userでターゲットユーザーを指定。<br /> followsは現在のフォロー一覧配列で、<br /> ボタンを押して追加、削除するとこの配列から新たな配列を作り、<br /> onFollowsUpdatedコールバックを呼び出して親にsetStateしてもらうことで適切に画面上が更新される。</p> <h3 id="自分のタイムラインの修正"><a href="#%E8%87%AA%E5%88%86%E3%81%AE%E3%82%BF%E3%82%A4%E3%83%A0%E3%83%A9%E3%82%A4%E3%83%B3%E3%81%AE%E4%BF%AE%E6%AD%A3">自分のタイムラインの修正</a></h3> <p>フォロー機能ができたので、自分のタイムラインにはフォローしている人の投稿も表示する必要がある。<br /> 取得するPostのfilterをユーザー指定からin指定に変更するだけ。</p> <h3 id="今後"><a href="#%E4%BB%8A%E5%BE%8C">今後</a></h3> <ul> <li>検索(MeCabで解析したワードやハッシュタグで検索)</li> <li>ユーザープロフィールの変更</li> <li>非ログイン状態でもアクセスできる箇所の調整</li> <li>OAuth導入によるAPIの作成</li> <li>切りが良いので一旦休載</li> </ul> <p>Dumitter</p> <p><a target="_blank" rel="nofollow noopener" href="https://alphabrend.sakura.ne.jp/dumitter/">https://alphabrend.sakura.ne.jp/dumitter/</a></p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/14299 2016-08-03T05:38:25+09:00 2018-10-02T20:27:21+09:00 https://crieit.net/posts/Twitter-4 Twitterを作る 第4回 コンポーネント構成考察 <p>Reactのコンポーネントの構成にちょっと悩んだのでメモ。</p> <p>現在ログイン後の画面はreact-routerでルーティングしている。(まだ1画面しかないが)<br /> コンポーネントの構成は下記のようになっている。</p> <ul> <li>Main (routing) <ul> <li>MainIndex (メインページ)</li> <li>MainHeader (共通ヘッダ)</li> <li>MainPosts (投稿一覧) <ul> <li>MainPost (投稿表示)</li> </ul></li> </ul></li> </ul> <p>一体何を悩んだかというと、MainHeaderは各ページで共通で利用するため、本当は元々下記のようにしていた。</p> <ul> <li>Main (routing) <ul> <li>MainHeader</li> <li>MainIndex (メインページ。ここをroutingで切り替える)</li> <li>MainHeader (共通ヘッダ)</li> <li>MainPosts (投稿一覧) <ul> <li>MainPost (投稿表示)</li> </ul></li> </ul></li> </ul> <h3 id="問題1"><a href="#%E5%95%8F%E9%A1%8C1">問題1</a></h3> <p>ただ、これだと問題があった。<br /> 投稿はMainHeader内にネストするフォームで行うが、その際に最新の投稿一覧をMainPosts側に読み込ませたい。<br /> しかしMainHeaderとMainPostsは親子関係で連なっていないため、連携ができない。</p> <h3 id="問題2"><a href="#%E5%95%8F%E9%A1%8C2">問題2</a></h3> <p>posts(投稿配列)のstateをどこに持たせるかという問題。<br /> なるべくMainで持たせたかった。何故かと言うと、MainHeaderには検索ボックスなどもあるので、<br /> それのハンドリングのコードも記述しなければならないが、<br /> MainHeaderを下の方においてrender時にDOM指定するとそのコードを何度も書かなければならない。</p> <h3 id="構成の解決"><a href="#%E6%A7%8B%E6%88%90%E3%81%AE%E8%A7%A3%E6%B1%BA">構成の解決</a></h3> <p>DOMにrefを付けることによって子コンポーネントのメソッドを呼べたりするのでその辺りも考えたが、</p> <p><a target="_blank" rel="nofollow noopener" href="http://js.studio-kingdom.com/react/guides/more_about_refs">Refsの詳細 | React 0.13 日本語リファレンス | js STUDIO</a></p> <p>の下の注意を読んで欲しい。</p> <blockquote> <p>Reactを使用したプログラムの開発経験が乏しいと、 アプリケーションで"何かを実現する"のに、すぐにrefsを使いたくなるかもしれません。</p> <p>このような場合は、stateがコンポーネント階層の何処で所有されるべきなのかを、もう少し注意深く考えてみて下さい。 その結果、より高い階層でstateが"所有"されるべきであると判明することがよくあります。</p> </blockquote> <p>つまり色々なところで使いまわしたい値はなるべく高い階層に置けということだ。<br /> なのでMainIndex、つまりルーティングで切り替えられる各ページのトップに置くことに決めた。</p> <h3 id="コード重複の解決"><a href="#%E3%82%B3%E3%83%BC%E3%83%89%E9%87%8D%E8%A4%87%E3%81%AE%E8%A7%A3%E6%B1%BA">コード重複の解決</a></h3> <p>上記方法で対応すると、各ページごとに同じコードを書かなければならなくなる。<br /> なのでそれは各ページのRootはMainBaseRouteComponentという継承用クラスを作り、<br /> 全てのrouterから呼び出されるページ(MainIndex等)はこれを継承することにした。<br /> これで全部のページでMainHeaderを配置してもいちいちhandlerメソッドを書かなくてよくなった。</p> <p>Dumitter<br /> <a target="_blank" rel="nofollow noopener" href="https://alphabrend.sakura.ne.jp/dumitter/">https://alphabrend.sakura.ne.jp/dumitter/</a></p> <p><a href="https://crieit.net/posts/Twitter-5">Twitterを作る 第5回 フォロー</a></p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/14300 2016-08-02T01:10:31+09:00 2018-10-02T20:26:50+09:00 https://crieit.net/posts/Twitter-3 Twitterを作る 第3回 投稿 <p>投稿機能の作成を進めた。実際の画面は下記のような感じでTwitterと構成は同じような形にしてある。<br /> 各パーツはmaterial-ui。すごく便利。</p> <p><a href="https://crieit.now.sh/upload_images/da575d4c4b83e987cb4d4a2e6c7b4ca25b0d18af5a469.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/da575d4c4b83e987cb4d4a2e6c7b4ca25b0d18af5a469.png?mw=700" alt="" /></a></p> <p>投稿ボタンを押すと下記のようなウィンドウが出てくる。</p> <p><a href="https://crieit.now.sh/upload_images/e7f029e271fc33d32e17cb59b545e8045b0d18b008f01.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/e7f029e271fc33d32e17cb59b545e8045b0d18b008f01.png?mw=700" alt="" /></a></p> <h3 id="モデルの作成"><a href="#%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AE%E4%BD%9C%E6%88%90">モデルの作成</a></h3> <p>モデルは下記のような感じになった。</p> <pre><code class="python">class Post(models.Model): """投稿""" user = models.ForeignKey(User, verbose_name='ユーザー', related_name='posts') repost = models.ForeignKey( "Post", verbose_name='リポスト', related_name='reposts', null=True ) citing = models.ForeignKey( "Post", verbose_name='引用', related_name='citings', null=True ) thread = models.ForeignKey( "Post", verbose_name='コメント先', related_name='comments', null=True ) body = models.CharField('内容', max_length=140) image = models.ImageField(upload_to='posts/') created = models.DateTimeField(auto_now_add=True) modified = models.DateTimeField(auto_now=True) deleted = models.BooleanField('削除済', default=False) def __str__(self): if len(self.body) > 10: return str(self.body)[:10] + '...' return self.body class PostWord(models.Model): """単語""" post = models.ForeignKey(Post, verbose_name='投稿', related_name='post_words') word = models.CharField('単語', max_length=64) created = models.DateTimeField(auto_now_add=True) def __str__(self): return self.word class PostTag(models.Model): """タグ""" post = models.ForeignKey(Post, verbose_name='投稿', related_name='post_tags') name = models.CharField('タグ', max_length=64) created = models.DateTimeField(auto_now_add=True) def __str__(self): return self.name </code></pre> <p>投稿フォームから投稿が行われると、Postとして保存される。<br /> この時にハッシュタグは正規表現で解析してPostTagとして保存する。</p> <p>あとはPostWordに投稿で使用されている単語などを保存してトレンド等に使う必要があると思うが、<br /> MeCabの準備などが必要だったりして面倒なのでとりあえず後回し。<br /> まずは大まかな流れを優先する。</p> <h3 id="投稿一覧"><a href="#%E6%8A%95%E7%A8%BF%E4%B8%80%E8%A6%A7">投稿一覧</a></h3> <p>jsonで読み込んでsetStateするだけで勝手に表示してくれる。<br /> 各投稿はmaterial-uiのcardそのまま。</p> <p>下にスクロールすると次の50件を読み込んで表示。<br /> 本家はガンガンスクロールできるのでレスポンスの良さや調整の努力がうかがえる。</p> <p>jsonについて、エンティティから自動で変換も試したが、<br /> リレーション部分を含ませることができなかったのと、<br /> よくよく考えると最終的には他人の投稿も含まれるのでセキュリティな問題を考えて、<br /> json用の辞書インスタンスの作成は手動で代入して作ることにした。</p> <h3 id="投稿"><a href="#%E6%8A%95%E7%A8%BF">投稿</a></h3> <p>投稿すると現在の一覧よりも最新の投稿がjsonで返ってくるので既存の配列にマージするだけ。</p> <h3 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h3> <p>次はReactのComponentの構成考察や、あとはフォロー機能を作らないと何も進まなそうだ。<br /> djangoのview側はデータ登録したり取得したりしてjsonを返すだけで何も特筆することがない。</p> <p>Dumitter</p> <p><a target="_blank" rel="nofollow noopener" href="https://alphabrend.sakura.ne.jp/dumitter/">https://alphabrend.sakura.ne.jp/dumitter/</a></p> <p><a href="https://crieit.net/posts/Twitter-4">Twitterを作る 第4回 コンポーネント構成考察</a></p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/14302 2016-07-24T21:26:15+09:00 2018-10-17T19:37:08+09:00 https://crieit.net/posts/Twitter-2 Twitterを作る 第2回 認証 <p>まずはユーザー登録。これがなければ何も話が進まない。<br /> ひとまず下記の第3回までを見てさくっと動作確認とユーザーテーブル作成まで行う。</p> <p><a target="_blank" rel="nofollow noopener" href="http://qiita.com/kaki_k/items/511611cadac1d0c69c54">Python Django入門 (1)</a></p> <p><a target="_blank" rel="nofollow noopener" href="http://qiita.com/kaki_k/items/7b178ad39394a031b50d">Python Django入門 (3)</a></p> <h3 id="ログイン前トップページ"><a href="#%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E5%89%8D%E3%83%88%E3%83%83%E3%83%97%E3%83%9A%E3%83%BC%E3%82%B8">ログイン前トップページ</a></h3> <p>material-uiでさくっと作成。Twitterのトップ画面を見ると今回必要そうなコンテンツはこれだけ。<br /> アカウント作成でsignupページヘ。</p> <p><a href="https://crieit.now.sh/upload_images/25c52bd03d41a78fe11eb47d165881035b0d18b4692e5.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/25c52bd03d41a78fe11eb47d165881035b0d18b4692e5.png?mw=700" alt="" /></a></p> <h3 id="ユーザー登録ページ"><a href="#%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E7%99%BB%E9%8C%B2%E3%83%9A%E3%83%BC%E3%82%B8">ユーザー登録ページ</a></h3> <p><a href="https://crieit.now.sh/upload_images/a519923405e2c9c1623e609656c0258d5b0d18b5e439f.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/a519923405e2c9c1623e609656c0258d5b0d18b5e439f.png?mw=700" alt="" /></a></p> <p>こちらも同様にぱぱっと作成。<br /> このページはユーザー登録とログインのサーバーへの通信が必要なのだが、<br /> そのままだとCSRFでエラーになるのでensure_csrf_cookieを設定している。<br /> また、後述するconfigライブラリを作成している。</p> <p>ちなみにログインを押すとモーダルが出てくる。</p> <p><a href="https://crieit.now.sh/upload_images/b521711f76f0a2b601d366c86199d9075b0d18b68a7b1.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/b521711f76f0a2b601d366c86199d9075b0d18b68a7b1.png?mw=700" alt="" /></a></p> <h3 id="configライブラリ"><a href="#config%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA">configライブラリ</a></h3> <p>全体の共通機能として下記のようなファイルを作成した。</p> <p>app/lib/config.js</p> <pre><code class="javascript">const config = { defaultHeaders: { "X-CSRFToken": getCookie('csrftoken'), "Accept": "application/json", }, url: (url) => { if (url.charAt(0) === '/') { url = url.substr(1); } return '/dumitter/' + url; }, } module.exports = config; </code></pre> <p>現在さくらサーバーのそのままのURL(SSL)なので、<br /> 今後独自ドメインを使う場合全箇所の修正は大変になるためurl補正メソッドを作成。<br /> (最初から独自ドメインなら絶対パスで良いのだが)</p> <p>また、ajaxの際にCSRFのトークンを送信するため、共通のデフォルトヘッダを定義している。<br /> (getCookieは検索すれば出てくる)</p> <h3 id="バリデーション &amp; 登録処理"><a href="#%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3+%26amp%3B+%E7%99%BB%E9%8C%B2%E5%87%A6%E7%90%86">バリデーション & 登録処理</a></h3> <p>postするとjsonを返却するアクション。<br /> resultがtrueなら成功でログイン済みなのでトップページへリダイレクト。<br /> falseならjsonに各フィールド名のエラーメッセージが入っているのでmaterial-uiのTextFieldのerrorTextにセット。</p> <p>バリデーションの処理は全部python側でやっている。<br /> js側でもいいかと思ったのだが、パスワードの細かいバリデーションがdjango側に入っているのでそちらに統一した。<br /> (設定ファイルで細かいパスワードのバリデーション方法を指定することができる)</p> <p>本当はもっとちゃんとした書き方がありそうだが、この処理はここだけだしいいかと思いバリデーションもベタ書き。</p> <pre><code class="python">from django.http.response import JsonResponse from django.contrib.auth import authenticate, login as authLogin from django.contrib.auth.models import User from django.core.validators import validate_email from django.contrib.auth.password_validation import validate_password from django.db import IntegrityError from django.core.exceptions import ValidationError import re def create(request): errors = {} if re.match(r'^[\da-zA-Z_]+$', request.POST['name']) is None: errors['name'] = '半角英数字で入力して下さい。' try: validate_email(request.POST['email']) except ValidationError as e: errors['email'] = e.messages try: validate_password(request.POST['password']) except ValidationError as e: errors['password'] = e.messages if len(errors) == 0: try: user = User.objects.create_user( username=request.POST['name'], email=request.POST['email'], ) except IntegrityError as e: if 'email' in str(e): errors['email'] = 'そのメールアドレスは既に使用されています。' elif 'name' in str(e): errors['name'] = 'そのユーザー名は既に使用されています。' if len(errors) != 0: errors['result'] = False return JsonResponse(errors) user.set_password(request.POST['password']) user.save() return login(request) def login(request): authed = authenticate( username=request.POST['name'], password=request.POST['password'], ) result = {'result': True} if authed is not None: authLogin(request, authed) else: result['result'] = False return JsonResponse(result) </code></pre> <p>新規登録後は自動的にログインするが、通常のログインのプログラムと全く同じだったので関数を流用している。</p> <p>ログイン後はトップページにリダイレクトするが、<br /> ここではログインの場合別のテンプレートを表示するようにしている。</p> <pre><code class="python">def index(request): if request.user.is_authenticated(): template = 'main.html' else: template = 'index.html' return render(request, template) </code></pre> <h3 id="React側"><a href="#React%E5%81%B4">React側</a></h3> <p>多分邪道かも知れないが下記のようなことを行っている。</p> <pre><code class="javascript">const element = document.getElementById('auth-regist'); if (element != undefined) { ReactDOM.render(<AuthRegist />, element); } </code></pre> <p>認証側はこれ以上ページを作るつもりはないので適当。<br /> 認証後はルーティングを使いたい。</p> <p>これで認証が完成したので次は投稿を作っていく予定。</p> <p><a target="_blank" rel="nofollow noopener" href="https://alphabrend.sakura.ne.jp/dumitter/">https://alphabrend.sakura.ne.jp/dumitter/</a></p> <p><a href="https://crieit.net/posts/Twitter-3">Twitterを作る 第3回 投稿</a></p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/14303 2016-07-24T07:47:26+09:00 2018-10-30T14:17:36+09:00 https://crieit.net/posts/Twitter-1 Twitterを作る 第1回 概要 <p>これからTwitterを作って行くところを少しずつ公開していきたいと思う。<br /> 下記のような環境で作成していく。</p> <h3 id="Django"><a href="#Django">Django</a></h3> <p>Djangoはpythonのフレームワーク。<br /> 慣れているCakePHPの方が早いが面白くないのでDjangoを練習しつつ作成。<br /> (python3.5 + Django1.9.7)</p> <p>ちなみにtwitterはRuby on RailsからJavaVMに移行していっている様で、pythonとは何の関係もなさそう。<br /> ただ、Instagramはdjangoで作成されているらしい。<br /> また、pythonはGoogleでよく利用されているし、良い言語であることは間違いなさそう。</p> <p>実際に、使ってみてまだちょっとしか経っていないがかなり好きになってきている。<br /> ひとまず現状の理由としては下記のようなものがあげられる。</p> <h4 id="管理画面が勝手に作られる"><a href="#%E7%AE%A1%E7%90%86%E7%94%BB%E9%9D%A2%E3%81%8C%E5%8B%9D%E6%89%8B%E3%81%AB%E4%BD%9C%E3%82%89%E3%82%8C%E3%82%8B">管理画面が勝手に作られる</a></h4> <p>adminページがあり、DBやモデルの作成が進んで行くと勝手に管理画面が作られていく。<br /> 仕様の細かい案件等では使いづらいかもしれないが、<br /> こうやって一人で勝手に作っていったり要件のゆるい案件では非常に役立つし無駄な工数削減になる。</p> <h4 id="情報が見つかりやすい"><a href="#%E6%83%85%E5%A0%B1%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%82%8A%E3%82%84%E3%81%99%E3%81%84">情報が見つかりやすい</a></h4> <p>公式のドキュメントにはわりと欲しい情報が載っているし、<br /> 検索すればそこそこすぐに情報が見つかりハマりにくい気がする。<br /> もしかすると英語前提かも知れないがプログラムなのであまり関係ないだろう。</p> <h4 id="進化してきている"><a href="#%E9%80%B2%E5%8C%96%E3%81%97%E3%81%A6%E3%81%8D%E3%81%A6%E3%81%84%E3%82%8B">進化してきている</a></h4> <p>何かやりたいことがある時、古い情報が見つかると、「面倒だな…」と思うことがあるが、<br /> 最新の情報をよくよく見ていると結構洗練されていたりする。<br /> 最新のWEB開発が好きな人は結構ハマるのではないかと思う。</p> <h4 id="面倒な部分"><a href="#%E9%9D%A2%E5%80%92%E3%81%AA%E9%83%A8%E5%88%86">面倒な部分</a></h4> <p>慣れていないと環境構築が結構面倒かもしれない。<br /> 今のところWindowsでMySQLに接続する方法が分かっていなかったりする。<br /> (サーバーにあげて動作確認している)</p> <h3 id="React + webpack + superagent + material-ui"><a href="#React+%2B+webpack+%2B+superagent+%2B+material-ui">React + webpack + superagent + material-ui</a></h3> <p>Twitterは基本的にSPAっぽいので、基本的にjavascriptで作成し、サーバー側はAPIとして動かしていく必要がある。<br /> そうなるとReactかAngular2になってくると思うが、今回はちょっと型管理が面倒なのでReactを選んだ。</p> <p>サーバーとの通信はsuperagentを利用し、webpackでビルドする。<br /> とりあえず、webpackで1ファイルになってくれるのはやはり非常に楽。<br /> 多くのストレスを軽減してくれる。</p> <p>UIはmaterial-uiを使用する。<br /> よく出来ていて、今回の様なアプリケーションの場合はすごく使いやすい。</p> <h3 id="アプリ名"><a href="#%E3%82%A2%E3%83%97%E3%83%AA%E5%90%8D">アプリ名</a></h3> <p>Djangoで作成するdummyのtwitterなのでDumitterとする。</p> <h3 id="方針"><a href="#%E6%96%B9%E9%87%9D">方針</a></h3> <p>可能な限り似せる。ただしすぐできる範囲。面倒なことは一切やらない。<br /> (ほぼmaterial-uiそのまま)</p> <p>あと記事の方針としてはなるべく進めていく状況や難しい箇所の説明を入れたいので、<br /> 他で調べれば出てくるような細かい技術は解説しない予定。<br /> あくまで娯楽であり教材ではない。</p> <h3 id="公開場所"><a href="#%E5%85%AC%E9%96%8B%E5%A0%B4%E6%89%80">公開場所</a></h3> <p><a target="_blank" rel="nofollow noopener" href="https://alphabrend.sakura.ne.jp/dumitter/">https://alphabrend.sakura.ne.jp/dumitter/</a><br /> (さくらレンタルで無理やり動かしてるので非常に重いです)</p> <p>次回から実際に開発状況を公開していく予定。</p> <p><a href="https://crieit.net/posts/Twitter-2">Twitterを作る 第2回 認証</a></p> だら@Crieit開発者