TECH PLAY

WESEEK, Inc.

WESEEK, Inc. の技術ブログ

75

はじめに こんにちは、インターンの手塚です。今回は、GROWIに新たに実装されるAPIの制限について書いていきたいと思います。APIの制限とは、機械的にたくさんのリクエストが一気に送られるのを防ぐ機能です。そのために特定のエンドポイントに対して、一定時間内に一定回数以上のリクエストが送られたときに 429 too many request のエラーを返します。 動機 const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 10, // limit each IP to 10 requests per windowMs message: 'Too many requests sent from this IP, please try again after 15 minutes', }); ~~ 省略~~ app.post('/login' , apiLimiter , hoge); GROWIにはもともと、上のようにAPIの制限が実装されていましたが、閾値がハードコードされているので、OSSとしてGROWIを扱う人が簡単に制限を変更できるものではありませんでした。しかしユーザーさんから、「自分たちで環境変数などでAPIに制限をかけたい」という要望があったことで、今回の新しく可変的なAPI制限の機能を実装するに至りました。 設計 GROWIでは、もともとAPIの制限を実装するのに express-rate-limit というライブラリを使っていましたが、このライブラリでは柔軟にエンドポイントごとに設定を変えることができなかったため、新たに rate-limiter-flexible というライブラリを導入しています。 express-rate-limitドキュメント rate-limiter-flexibleドキュメント const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15分 max: 10, // 最大10リクエスト message: 'Too many requests sent from this IP, please try again after 15 minutes', }); ~~ 省略~~ app.post('/login' , apiLimiter , hoge); express-rate-limit では上のように、一つのrateLimitインスタンスがもつ制限の内容は変更できず、複数種類の制限をかけるには、複数のインスタンスを作成する必要がありました。 const opts = { points: 100, // 最大ポイント数 duration: 15 * 60 * 1000, // 15分 }; const rateLimiter = new RateLimiter(opts); rateLimiter.consume(key, 10); // 1リクエストで10ポイント消費 それに対し、 rate-limiter-flexible では、最大ポイントから、英クエストごとに一定のポイントを消費し、ポイントがなくなったら too many requests という少し特殊な制限方法にすることで、一つのインスタンスに対して、keyと消費するポイントを設定することで制限を柔軟に変更することができます。これが、 rate-limiter-flexible を採用した理由です。 keyについて rate-limiter-flexible では、keyと呼ばれる文字列に対して一定時間の制限を設け、apiに制限をかけるという仕組みになっています。GROWIでは、ログインユーザーに対しては、 ユーザーID + エンドポイント + リクエストメソッド というkeyを設定しています。こうすることで、同一IPアドレスから複数のログインユーザーのリクエストがあってもそれぞれのユーザーのリクエストに対して制限をかけることができます。ログインしていないゲストユーザーに対しては、 IPアドレス + エンドポイント + リクエストメソッド というkeyを設定しています。 rate limitの設定方法 ユーザー目線での設定のしやすさから、環境変数を用いて、サーバー起動時に環境変数から設定を取得し、APIの制限を実装するようにします。 API_RATE_LIMIT_010_LOGIN_ENDPOINT=/login API_RATE_LIMIT_010_LOGIN_METHODS=GET API_RATE_LIMIT_010_LOGIN_MAX_REQUESTS=20 ユーザーが設定可能な項目は、 エンドポイント 、 メソッド 、 1秒あたりの最大リクエスト数 で、環境変数を上のように設定することで、サーバー起動時に設定を取得できるようにし、同一エンドポイントに複数の設定がある場合はkeyを昇順でソートして、後ろに来る設定を優先するようにします。keyは上の例での、 010_LOGIN の部分です。 また、 /share/:pageId のように可変のURLに対応するため、 API_RATE_LIMIT_010_SHARE_ENDPOINT_WITH_REGEXP=/share/[0-9a-z]{24} API_RATE_LIMIT_010_SHARE_METHODS=GET API_RATE_LIMIT_010_SHARE_MAX_REQUESTS=20 このように、エンドポイント部分に正規表現を用いて設定ができるようにもしました。 rate limitの初期設定 export const DEFAULT_MAX_REQUESTS = 500; export const DEFAULT_DURATION_SEC = 60; export const DEFAULT_USERS_PER_IP_PROSPECTION = 5; ユーザーが全てのエンドポイントに対して制限を設定できるように全てのエンドポイントに制限を設定していますが、カスタマイズされていないエンドポイントには上の設定が適用され、60秒間に500回が最大リクエストとなります。この値は、ユーザーのカスタマイズされた値によって上書きされます。 const MAX_REQUESTS_TIER_1 = 5; const MAX_REQUESTS_TIER_2 = 20; const MAX_REQUESTS_TIER_3 = 50; const MAX_REQUESTS_TIER_4 = 100; 上のデフォルト制限の他に、GROWIでは上のような独自に作成した Tier の概念を用いて、 POST /login のような、あらかじめ少し厳しい制限が必要と思われるエンドポイントには初期値を設定しています。この値も、ユーザーのカスタマイズされた値によって上書きされます。 未ログインユーザーに対しては、デフォルトでは1IPアドレスあたり5人という想定で、許容するアクセス数を5倍にしています。この値も、 DEFAULT_USERS_PER_IP_PROSPECTION という環境変数でエンドポイントごとに設定することができ、このような仕組みにすることで、ログイン済みユーザーもゲストユーザーにも対応できるrate limitを実装しています。 実装 const apiRateLimiter = require('../middlewares/api-rate-limiter')(); ~~省略~~ app.use(apiRateLimiter); // 全てのエンドポイントに制限をかける ~~省略~~ ユーザーがすべてのエンドポイントにAPI制限をかけることができるようにするため、すべてのエンドポイントに対してデフォルトで制限をかけます。 ~~省略~~ const rateLimiter = new RateLimiterMongo(opts); // 環境変数から値を取得してくる ~~省略~~ // ポイントを消費して制限をかけるミドルウェア(本体) module.exports = () => { return async(req: Request & { user?: IUserHasId }, res: Response, next: NextFunction) => { const endpoint = req.path; // ログインしているか、していないかでkeyを作成する const keyForUser: string | null = req.user != null ? md5(`${req.user._id}_${endpoint}_${req.method}`) : null; const keyForIp: string = md5(`${req.ip}_${endpoint}_${req.method}`); // リクエストされたエンドポイントに対して、設定があるかを確認 let customizedConfig: IApiRateLimitConfig | undefined; const configForEndpoint = configWithoutRegExp[endpoint]; if (configForEndpoint) { customizedConfig = configForEndpoint; } else if (allRegExp.test(endpoint)) { keysWithRegExp.forEach((key, index) => { if (key.test(endpoint)) { customizedConfig = valuesWithRegExp[index]; } }); } // check for the current user if (req.user != null) { try { await consumePointsByUser(req.method, keyForUser, customizedConfig); } catch { logger.error(`${req.user._id}: too many request at ${endpoint}`); return res.sendStatus(429); } } // check for ip try { await consumePointsByIp(req.method, keyForIp, customizedConfig); } catch { logger.error(`${req.ip}: too many request at ${endpoint}`); return res.sendStatus(429); } return next(); }; }; リクエストが来てから、このミドルウェアが適用される流れは以下の通りです。 デフォルトの設定(初期設定)を読み込む 環境変数から設定(ユーザーカスタマイズ設定)を読み込む 二つの設定をマージしAPI制限の設定としてサーバーが保持 リクエストが来るたびに設定をチェックし、設定があれば適用 この順序で処理を行うことで、環境変数を用いた可変的なAPIの制限を実現しています。 最後に このrate limitの新機能は GROWI v5.1.0 でリリースされました。良かったらぜひ、使ってみてください! 以上、GROWIの新しいAPI制限についてでした。
アバター
はじめに こんにちは、エンジニアの Yohei です。 みなさんは VSCode のショートカットは使っていますか? コピーしたい単語がある時に、端から端までドラッグで範囲選択して、右クリックでオプションを開いて、コピーをクリックして、という丁寧な動作、 まさかやっていませんよね? ドキッとした方やショートカットを使ったことがない人にはピッタリの記事です。 この記事はより多くのユーザーが楽する方法を知るきっかけを増やすことを目的として執筆しました。 覚えるととても便利なショートカットをいくつか記載しましたので、ぜひ使いこなして周りの人にも教えてあげて下さい。 ショートカット一覧 サイドバー開閉 文字通り、サイドバーを開いたり閉じたりできます。 編集中ファイルの画面幅を確保したいときにオススメです。 Win Mac Ctrl + B CMD + B ショートカット一覧へ戻る フォルダ一覧を開く サイドバーのフォルダ・ファイルの一覧を開くことができます。 サイドバーが閉じている状態や・サイドバーでフォルダ一覧以外を開いた状態でも実行できます。 Win Mac Ctrl + Shift + E CMD + Shift + E ショートカット一覧へ戻る 検索を開く VSCode サイドバーの検索(SEARCH)を開くことができます。 特定の文字列を範囲選択した状態で実行すると、検索が開くと同時に、 選択したキーワードを含むファイルを検索してくれます 。 ある関数や変数がどのファイルで使われているかを検索したいときなどにオススメです。 Win Mac Ctrl + Shift + F CMD + Shift + F ショートカット一覧へ戻る ファイル検索 ファイル名を入力すると、キーワードに合致する、またはそれに近いファイル名のファイル一覧をサジェストしてくれます。 ファイル名がわかっている時はサイドバーから探すよりもこちらがオススメです。 Win Mac Ctrl + P CMD + P ショートカット一覧へ戻る ターミナル開閉 ターミナルの開閉ができます。 閉じられている状態からショートカットを入力するとターミナルが開き、 入力のフォーカスがターミナルに切り替わります 。 開いている状態からショートカットを入力するとターミナルが閉じられます。 編集ファイルを広く見たいときや、すぐさまターミナルを開きたいときオススメです。 Win Mac Ctrl + J CMD + J ショートカット一覧へ戻る ターミナル分割 それぞれ独立したターミナルを同一タブ上で分割することができます。 新規タブを開くよりも一覧性がありオススメです。 Win Mac Ctrl + Shift + 5 CMD + Shift + 5 ショートカット一覧へ戻る 単語範囲選択 カーソルの位置 に存在する単語が 自動的に範囲選択 されます。 連続で実行すると、次に見つかった同じ単語がマルチカーソルで順番に選択されていきます。 Win Mac Ctrl + D CMD + D ショートカット一覧へ戻る マルチカーソル カーソルが複製出現します。 複数対象への同時入力や削除 、そして 同時コピーや貼り付け もできます。 複数に対して同じ文字の入力や削除などの編集をしたい場合にオススメです。 Win Mac Alt + クリック Opt + クリック マウスのみ:クリックした箇所にカーソルが置かれる Ctrl + Alt + 上下キー Ctrl + Opt + 上下キー カーソルが上下に増えてゆく ショートカット一覧へ戻る 行コピー(未選択時) カーソルのある 行をまるごとコピー できます。 ※範囲選択されている場合はその範囲のみが対象になります。 コピーしたい行の好きなところにカーソルを置いて(またはクリックして)実行するだけです。 マウスによるご丁寧な範囲選択という苦行はこれで卒業できますね。 Win Mac Ctrl + C CMD + C ショートカット一覧へ戻る 行消し(未選択時) カーソルのある行を まるごと削除 できます。 ※範囲選択されている場合はその範囲のみが対象になります。 削除したい行の好きなところにカーソルを置いて(またはクリック)実行するだけです。 Win Mac Ctrl + X CMD + X ショートカット一覧へ戻る コードの上下複製 カーソルのある行のコードを、上下どちらかに複製することができます。 複数行を選択している場合、 選択されている行のすべてのコード が複製されます。 Win Mac Alt + Shift + 上下キー Opt + Shift + 上下キー ショートカット一覧へ戻る ファイルを閉じる 現在フォーカスしているファイルを閉じることができます。 閉じるボタンを押す必要はありません。 次に紹介されている 「閉じたファイルを再度開く」とセットで覚える のがオススメです。 Win Mac Ctrl + W CMD + W 閉じたファイルを再度開く 前回閉じたファイルを再度開くことができます。 連続で実行すると、最近閉じられたものから過去に順番にさかのぼってファイルを開きなおすことができます。 間違えてファイルを閉じてしまった場合や、過去のファイルを確認したい場合にオススメです。 Win Mac Ctrl + Shift + T CMD + Shift + T ショートカット一覧へ戻る 登録ショートカット一覧表示 ショートカットのコマンドが登録されている一覧を表示できます。 どのキーに対してどのようなコマンドが紐づいているか(keybinding)を確認することができます。 Win Mac Ctrl + K して Ctrl + S CMD + K して CMD + S チートシート 他にも種類が豊富にあるので、ぜひ公式の資料も目を通してみてください。 VSCode ショートカット for Windows VSCode ショートカット for Mac VSCode shortcut 公式ドキュメント まとめ いかがでしたか? 1つのショートカットで実現できる内容はいたってシンプルです。 そして特に私たちエンジニアの中には、このシンプルな作業を一日に何十回・何百回とやっている人もいるでしょう。 それらをショートカットに置き換えて少しでも早く開発できたなら、空いた時間でさらに開発することもできます。 塵も積もれば山となるのです。 コピペするために以下のコンボしてる方は今日で卒業しましょう。 クリック → マウスドラッグ → 右クリック → コピー選択 → クリック → 右クリック → ペースト選択 この記事を見て少しでも楽ができる人が増えることを切に願います。 ここまで読んでいただきありがとうございました。
アバター
はじめに こんにちは、Ryo です。本記事では、弊社サービスの GROWI.cloud でマネージドサービスとして提供している Keycloak と 情報共有ツール GROWI との連携方法について、シングルサインオンや SAML の認証フローも交えて解説します。 GROWI をご利用中の方で SAML 認証の設定にお悩みの方や、情報共有ツールをシングルサインオンの機能も込みで導入したいと検討されている方はぜひこの記事をご覧ください。 シングルサインオン (SSO) とは まずは、SAML について触れる前にシングルサインオンについて解説します。 シングルサインオン (SSO:Single Sign-On) とは1つの ID とパスワードで、シングルサインオンに対応している複数の Web サービスやアプリケーションにログインするための仕組みです。 メールやグループウェア、 SNS などの各サービスで個々に ID とパスワードを登録すると、利用するサービスごとに ID とパスワードの組み合わせを覚えておく必要があり、管理が煩雑化します。「メモに残す」や覚えやすい簡単なパスワードを設定したところで外に情報が漏れやすくなってしまい情報漏洩につながる可能性が増えてしまいます。 シングルサインオンでは、1組の ID とパスワードでサービスにログインできるので、認証情報を管理する負担を軽減してくれます。 シングルサインオンを実現する認証方式は、大きく分けて4つありますが本記事では SAML 認証方式について解説します。 SAML とは SAML(Security Assertion Markup Language)とは、シングルサインオンを実現するための認証方式です。異なるドメイン間でもユーザー認証を行うための認証情報をやり取りをするルール・プロトコルを指します。 SAML認証ではユーザー・SP (Service Provider)・IdP (Identity Provider)の間で認証の手続きが行われます。 SP SP (Service Provider)はその名の通り提供されているサービス・システムのことを指します。 この後解説いたします SAML の認証フローでは GROWI がこの SP にあたります。 IdP サービスやアプリケーションへのアクセスを試みるユーザーの ID やパスワードなどの認証情報を管理し提供する事業者やシステムを IdP と言います。 IdP に認証情報の管理を委ねることで SP はユーザーの認証情報の管理を行う必要がなくなります。 この後解説いたします SAML の認証フローでは GROWI.cloud でマネージドサービスとして提供している Keycloak がこの IdP にあたります。 Keycloak とは Keycloak は Redhat 社により開発されたオープンソースのアイデンティティ管理ソフトウェアです。 OpenID Connect や SAML などのシングルサインオンのプロトコルに対応し、 IdP として利用できます。 Keycloak が提供している主な機能は公式ドキュメントの Server Admin(日本語版) に機能について記載がありますのでご覧ください。 弊社サービスの GROWI.cloud では、オープンソースであること、Keycloak 自身が複数の認証プロバイダと連携して認証情報の管理ができることを理由に Keycloak を導入し、一部のプランでマネージドサービスとして提供しています。 GROWI.cloud の料金・機能について詳しくは こちら をご覧ください。 Keycloak と連携した GROWI の図解 この章では、 Keycloak を用いて GROWI に SAML 認証でログインするフローについて図を用いて解説します。 認証のフロー 解説 ユーザー A が GROWI にアクセスし、ログインの認証方法として SAML 認証を選択する GROWI は自身と関連付けられた Keycloak 上の realm でのログイン画面に飛ばす Keycloak で認証成功すると、認証結果を付加しつつ GROWI の SAML エントリポイントへリダイレクトする リダイレクトしてきたユーザー A から GROWI が認証結果を受け取ると無事に GROWI にログインできる 認証フローのイメージ 1 Keycloak × 複数 GROWI 1つの Keycloak に対して複数の GROWI を連携できます。 Keycloak に登録してある認証情報でどの GROWI にもログインできるようになるため特に規模の大きな組織においてユーザーの認証情報の管理に最適です。 GROWI.cloud での Keycloak 設定方法 GROWI.cloud ではビジネススタンダードプラン及びビジネスプロプランにて Keycloak をマネージドサービスとして提供しております。 GROWI.cloud で立ち上げた Keycloak にアクセスし Administration Console を選択する 「GROWI」レルム内でユーザーやロール、グループを作成する レルム(Realm)とは Keycloak におけるレルム(Realm)とはユーザー、ロール、クレデンシャル、グループらをまとめてセットで管理するための範囲を指します。 GROWI.cloud で提供される Keycloak はデフォルトで「GROWI」レルムが作成され、そのレルム内での認証情報を設定できます。 Keycloak の設定が完了後、GROWI.cloud の GROWI 詳細ページもしくは、Keycloak 詳細ページに行き対象の GROWI と連携させます 連携後、GROWI が再起動しますので再度立ち上がれば Keycloak との連携は完了です。 終わりに ここまでご覧いただきありがとうございます。 今回は、SSO の一種である SAML 認証方式の紹介と、その認証フローの図解を交えながら GROWI と Keycloak の連携方法を解説してみました。 本記事を参考に GROWI と Keycloak を連携し、シングルサインオンを体験してみてください。 参考資料 https://keycloak-documentation.openstandia.jp/master/ja_JP/server_admin/index.html#機能
アバター
皆さんこんにちは!WESEEK ソフトウェアエンジニアの 増山 です。 今回のブログでは、WESEEK で絶賛開発中の OSS Wiki システム GROWI の開発に参加する方法を解説していきたいと思います。 参加する目的別に主に 5種類 の方法が存在するので、それら全てについて説明します。 目次 機能リクエストをしたい、GROWI について議論をしたい そんなときは Github の Discussion を使います。 Discussion の使い方 GROWI の Github Discussion にアクセス "New Discussion" をクリック "Select Category" から Discussion の種類を選んで、タイトルと本文を記入 最後に "Start Discussion" を押して Discussion を作成 バグを見つけたので修正して欲しい こんなときは Github の Issue を使います。 Issue の使い方 GROWI の Github Issues にアクセス "New Issue" をクリック 選択肢の中から Issue の種類を選ぶ テンプレートが表示されるので、それに沿って Issue を作成 作成されると GROWI 開発チームに通知が飛ぶ仕組みになっています 自分でコードを書いて GROWI に反映したい こんなときは Github の Pull Request を使います。 Git に関する詳しい説明はここではしません。 Pull Request の使い方 GROWI を git clone する master ブランチから作業用ブランチを切り、コードを書く 作業用ブランチをプッシュ GROWI の Github Issues にアクセス "New pull request" をクリック 選択肢の中から Issue の種類を選ぶ ベースブランチを master に向けて PR を作成 作成されると GROWI 開発チームに通知が飛ぶ仕組みになっています 最後に CI が通っているかを確認 GROWI の会議に参加したい GROWI チームでは、 毎月誰でも参加可能 な GROWI Users Meetup というオープンなオンライン会議を開催しています。 最新バージョンの機能紹介や、テーマに沿って議論する"オープン村議"のコーナーなどがあります。 より身近に GROWI 開発に参加してみたいにおすすめです。Youtube Live でも配信しているので、「まずは見てみたい」という方も気軽に参加いただけます。 開発チームと連絡を取りたい 何か困ったことがある場合や、「PR 作ったぞ!早くみてくれ!」ってなった場合は #GROWI をつけてツイートしてください。 また、Slack のチャンネルでも気軽にご質問いただけます。 言語について GROWI のソースコードやコミットメッセージは基本的に英語で統一されていますが、Discussion、Issue、Pull Request など含め日本語を使っても OK です。 最後に ここまで記事をお読みいただきありがとうございました。 GROWI は OSS ですので、皆さんも気軽に開発にご参加いただけます。まずは Discussion で日頃 GROWI に対して思っていることを投稿してみてはいかがでしょうか? 皆さんと一緒に開発できるのを楽しみにしています! 次回 GROWI Users Meetup のお知らせ 次回 7月25日(月) の GROWI Users Meetup では、初の試みである "オープンペアプロ" を実施します。 GROWI 開発チームのインターン生と、この記事を書いている 増山 が皆さんにコードを書いているところをお見せしながら、 実際の GROWI 開発 を ペアプロ という形で行います! 参加申し込みは以下のリンクからどうぞ! connpass: https://weseek.connpass.com/event/252069/ TECH PLAY: https://techplay.jp/event/863484
アバター
こんにちは、 ryosuke です。 今回は、 ドキュメント作成ツールである Sphinx を使って、多言語の文章を作成する際に、困った点とその解決手段について取り上げます。 この記事では、このうちの「困った点」の話をします。 ドキュメント作成と多言語対応 自社開発の Web サービスを提供するにあたり、そのユーザーマニュアルを整備・公開するのも、サービス提供に必要な要素の一つです。 また、日本語のユーザーだけでなく、英語圏のユーザーも獲得したいのであれば、 Web サービスはもちろんのこと、必然的にユーザーマニュアルも多言語対応が必要となるでしょう。 Web で閲覧できる文章を書くためのドキュメント作成ツールは多数存在します。(この blog で使っている wordpress もその一つですね。) 各ツールでのドキュメント執筆方法も ( WYSIWYG を使えたり マークアップ言語 で記述する、など) 様々です。 それと同様に、多言語対応の方法もツールごとに異なります。 したがって、ツールの選定においては、検討項目として多言語対応の方式も検討項目として、考慮が必要です。 Sphinx の採用例 WESEEK 内のあるプロジェクトでは、ドキュメント作成ツールとして Sphinx を採用しています。 このプロジェクトでのドキュメント作成においては、下記を要件として捉え、ツールを選定しました。 アプリケーションと同様に git で文章を管理できること 完成したドキュメントは Web で閲覧できること 日本語と英語の2言語でドキュメントを作成すること 言語間でドキュメントの内容に乖離が起きない状態を維持できること 上記要件を念頭に複数のツールを検討した結果、 Sphinx を採用しました。 Sphinx には以下のような特徴があります。 代表的なドキュメント作成ツールの1つ reStructuredText(reST) という マークアップ言語で文章を記述する HTML や PDF など、いくつかの形式でドキュメントの出力が可能 ドキュメントの国際化(i18n)に対応 i18n 方式の比較 Sphinx は i18n 対応において「翻訳方式」といえる機構を備えているのが特徴です。 reST で書いた文章を「原文」として扱い、その原文中の各単語や段落に対して訳を定義する方式です。 これとは異なる方式として、単純に複数言語分の原稿を取り扱う方式があります。 この方式のわかりやすい例が、 GROWI のドキュメントサイトである GROWI Docs です。 ドキュメント作成ツールに VuePress を採用しています。 VuePress の Guide に記載の Internationalization や growi-docs のソースコード を眺めてみるとイメージがつきやすいと思います。 この方式の場合、言語毎に別々の(Markdown で書かれた)原稿ファイルが、各言語用のディレクトリを分けて配置してあるだけです。 したがって、あるページについて、日本語版と英語版の2つを用意したければ、各言語で記述された Markdown ファイルを用意し、各言語用のディレクトリにそれぞれ配置するだけです。 この方式は実にシンプルなので、理解しやすくとっかかり易いと言えるでしょう。 その反面、そのツールだけの機能では、各言語での内容の乖離がある事に気付きにくいという面もあります。 例えば、サービスに新機能を追加したので、既存の日本語用 markdown ファイルに文章を追加したとします。 このとき、英語の markdown ファイルも同様に文章を追加することを忘れた場合、日本語には記載があるが、英語にはないという不一致が発生します。 この不一致が起きていることを機械的に判断するのは困難です。 したがって、人手による2言語間の内容の一致状況を、ていねいに判断するという活動の維持が必要です。 一方で翻訳方式の場合、原文に文章を追加した時点では、翻訳文がない状態になりますから、この状態のままで英文のドキュメントを出力すると、追加した部分だけ翻訳されずに原文(日本語)が表示されます。 このような挙動をするので、(適切に翻訳はされていないものの)2言語間で内容の乖離が起きることはなくなります。 また、翻訳文章を読めば、唐突に別言語の文章が登場する状態になっているので、翻訳が不足している箇所も特定しやすく、翻訳忘れも是正しやすいのが利点です。 こういった方式の特徴について検討した結果、多言語間での内容の乖離が起きない状態の維持を重視し、翻訳方式を採用している Sphinx を採用しました。 Sphinx での i18n 対応の概要 では Sphinx ではどのようにして多言語の文章を作成するのでしょうか? Sphinx の公式サイト でも詳細な説明がありますが、ここでは、 Sphinx で i18n 対応をする際の作業の大筋の流れや、必要な file の前提知識を記載します。 (後に紹介する、 i18n 対応での課題を理解するために知っておく必要がある知識です) 原文の作成 まず、原文となるドキュメントを作成します。 Sphinx の場合 reST で記述します。 たとえば、下記のような内容の sample.rst ファイルを作成します。 * これはリストの1つ目です * これはリストの2つ目です * これはリストの3つ目です この原文を元に HTML に build したファイルを出力する場合、以下のコマンドを実行します。 sphinx-build -b html sourcedir builddir このコマンドを実行することで、 sample.rst に記述した原文から HTML ファイルが出力されます。 翻訳ファイルの生成 次に翻訳された HTML ファイルを出力したい場合は、以下のように追加の作業が必要です。 まず、翻訳対象の文字列を原文から抽出します。 そのために以下のコマンドを実行します。 sphinx-build -b gettext sourcedir builddir すると、 sample.pot というファイルが生成されます。 これを POT ファイルと呼びます。 このファイルは以下のような内容です。 1 #: sample.rst:1 msgid "これはリストの1つ目です" msgstr "" #: sample.rst:3 msgid "これはリストの2つ目です" msgstr "" #: sample.rst:5 msgid "これはリストの3つ目です" msgstr "" これは、翻訳対象となる文字列を列挙するファイルです。 このうちの msgid と書かれている部分に記述されている文字列が「翻訳対象の文字列」と扱われます。 先ほどの sphinx-build -b gettext sourcedir builddir というコマンドを実行することで、作成済みの reST ファイルの全ての文章を翻訳対象として自動的に POT ファイルが生成されます。 続いて、各翻訳先言語ごとに訳を記述するファイルを生成します。 例えば英語の訳を記述する場合、下記のようなコマンドを実行します。 sphinx-intl update -p builddir -l en すると、 locale/en/LC_MESSAGES ディレクトリに sample.po というファイルが生成されます。 これを PO ファイルと呼びます。 このファイルは以下のような内容です。 1 #: sample.rst:1 msgid "これはリストの1つ目です" msgstr "" #: sample.rst:3 msgid "これはリストの2つ目です" msgstr "" #: sample.rst:5 msgid "これはリストの3つ目です" msgstr "" 先ほど生成した POT ファイルとほとんど変わらない内容です。 PO ファイルは POT ファイルを元に生成されるファイルで、まだ訳が1つもない状態では POT ファイルと差がありません。 ただし、 PO ファイルと POT ファイルは役割が異なります。 PO ファイルは各「翻訳対象の文字列」を具体的にどう翻訳するのか記述します。 各 msgid の直下にある msgstr 部分を編集し、訳を記述します。 例えば、以下のように記述します。 #: sample.rst:1 msgid "これはリストの1つ目です" msgstr "This is first line of list." #: sample.rst:3 msgid "これはリストの2つ目です" msgstr "This is second line of list." #: sample.rst:5 msgid "これはリストの3つ目です" msgstr "This is third line of list." これで翻訳作業は終了です。 翻訳した文章の出力 最後に、訳が適用された HTML ファイルを出力するには以下のコマンドを実行します。 sphinx-build -b html sourcedir builddir -D language=en すると適切に翻訳された HTML ファイルが出力されます。 原文の修正と翻訳の修正 なお、原文を加筆・修正などした場合は、 POT ファイルを再生成し、 msgid のリストを最新化し、それを生成済みの PO ファイルに反映させる必要があります。 これを行うのは簡単で、原文修正後に再び以下のコマンドを実行するだけです。 sphinx-build -b gettext sourcedir builddir sphinx-intl update -p builddir -l en すると、既存の PO ファイルに新たな msgid と空の msgstr が追加されます。 この空の msgstr 部分に適切な訳を記述することで、原文に変更を加えた場合にも新たな訳を設定できます。 翻訳作業時の課題 さて、このような方法で i18n 化できる Sphinx ですが、翻訳作業を進めるにつれ、いくつかの課題が見えてきました。 その課題を紹介します。 reference による大量の差分 PO ファイルには、 msgid に書かれている原文がどのファイルの何行目に登場するのか表現できる、 「reference」という記述があります。 #: ../../source/some_namespace/content.rst:3 msgid "今日は晴れです。" msgstr "Today is sunny." #: で始まる行で、 行末の数字の直前にある : の手前までが原文の filepath で、その後の数字は原文の登場する行番号を示しています。 上記の例だと「 今日は晴れです。 という原文は、 ../../source/some_namespace/content.rst ファイルの 3行目にある」という意味になります。 msgid の文字列だけでは前後の文脈が分からないので、翻訳困難な場合がありますが、 reference の解釈に対応したエディタを使うことで、翻訳者は原文に簡単にアクセスでき、前後の文章を確認しながらよりよい訳文を検討する助けになります。 この reference ですが、原稿が更新され、原文が登場する行番号が変われば、更新されます。 たとえば、すでに数行ほどの文章が書かれた、下記のような reST ファイルと PO ファイルがあったとします。 1行目の文章です。 3行目の文章です。 5行目の文章です。 #: ../../source/some_namespace/content.rst:1 msgid "1行目の文章です。" msgstr "This is a paragraph on first line." #: ../../source/some_namespace/content.rst:3 msgid "3行目の文章です。" msgstr "This is a paragraph on third line." #: ../../source/some_namespace/content.rst:5 msgid "5行目の文章です。" msgstr "This is a paragraph on fifth line." この reST ファイルの3行目に新たな文章を追加したとします。 1行目の文章です。 後から追記した文章です。 3行目の文章です。 5行目の文章です。 この時、 PO ファイルを再生成すると、下記のように元々3行目以降にあった全ての原文の reference が更新されます。 #: ../../source/some_namespace/content.rst:1 msgid "1行目の文章です。" msgstr "This is a paragraph on first line." #: ../../source/some_namespace/content.rst:3 msgid "後から追記した文章です。" msgstr "This is a paragraph that was added later." #: ../../source/some_namespace/content.rst:5 msgid "3行目の文章です。" msgstr "This is a paragraph on third line." #: ../../source/some_namespace/content.rst:7 msgid "5行目の文章です。" msgstr "This is a paragraph on fifth line." こうして完成した PO ファイルを git commit し、 Pull Request で差分を見ると、下記のような差分として表示されます。 このように、差分として表示される箇所が膨れ上がってしまい、本質的な差分を認識するには、注意深く見比べるほか手段がありません。 文の自動改行による大量の差分 PO ファイルの msgid および msgstr の値は、複数行に分割して記述できます。 つまり、以下の2つの例は完全に同じ内容と解釈されます。 msgid "今日は晴れです。明日は曇りです。明後日は雨の可能性がありますが、現時点では降水確率は低めの予報です。" msgstr "Today is sunny. Tomorrow will be cloudy. The day after tomorrow there is a chance of rain, but at this time the chance of precipitation is forecast to be low." msgid "" "今日は晴れです。明日は曇りです。" "明後日は雨の可能性がありますが、現時点では降水確率は低めの予報です。" msgstr "" "Today is sunny. Tomorrow will be cloudy. The day after tomorrow " "there is a chance of rain, but at this time the chance of " "precipitation is forecast to be low." この仕様自体に問題はありません。 問題は、 reST ファイルから PO ファイルを生成するときに、文章の長さによっては msgid と msgstr の値が自動的に複数行に分割(整形)されることです。 この挙動は、例えば reST ファイルを以下のように修正した場合に、顕著に表れます。 -今日は晴れです。明日は曇りです。明後日は雨の可能性がありますが、現時点では降水確率は低めの予報です。 +今日は晴れです。明日は曇りです。明後日は雨の可能性があります。 その後、PO file に原文の更新を反映すると、以下のような差分が発生します。 こちらが修正前で、 msgid "" "今日は晴れです。明日は曇りです。" "明後日は雨の可能性がありますが、現時点では降水確率は低めの予報です。" msgstr "Today is sunny. Tomorrow will be cloudy. The day after tomorrow there is a chance of rain, but at this time the chance of precipitation is forecast to be low." こちらが修正後です。 msgid "今日は晴れです。明日は曇りです。明後日は雨の可能性があります。" msgstr "" "Today is sunny. Tomorrow will be cloudy. The day after tomorrow " "there is a chance of rain, but at this time the chance of " "precipitation is forecast to be low." まず、 msgid は、原文が短くなった影響で、複数行に分割されていた箇所が1行にまとめられます。 したがって、本来であれば削除された文字列部分だけ差分ハイライトされることが期待できるにもかかわらず、記述行が移動した分の差分まで現れてしまいます。 さらに、 msgstr も PO ファイルの更新時に自動的に行分割されます。 従って、過去に訳を msgstr に1行に記載していた場合、その値の長さによっては複数行に分割されてしまいます。 このように修正された msgstr を、最新の msgid にあわせて正しい訳に修正すると、下記のようになります。 msgid "" "今日は晴れです。明日は曇りです。明後日は雨の可能性があります。" msgstr "" "Today is sunny. Tomorrow will be cloudy. The day after tomorrow " "there is a chance of rain." こうして完成した PO ファイルを git commit し、 Pull Request で差分を見ると、下記のような差分として表示されます。 このように、差分として表示される箇所が膨れ上がってしまい、本質的にどのような差分が生じたのかを認知するには、注意深く見比べなければなりません。 翻訳漏れの検知機構不足 翻訳方式の i18n 機構を採用することのメリットに、「原文の変更に訳文が追従しやすい」という点があることを話しました。 しかし、 Sphinx では訳文がない原文があったとしても、警告などの発生なしに翻訳済みの HTML を出力できてしまいます。(すでに説明したとおり、訳がない文章は原文が表示されます。) Sphinx の設定項目などを調査しましたが、この挙動の変更はできないようです。 したがって、翻訳の網羅性を担保したい場合であっても、「原文の変更に追従していない訳文があったら build エラーにする」などの対応が取れません。 トラブル発生 Sphinx を採用したプロジェクトでは、上記で紹介した課題は、文章作成を開始してから早い段階で認識していましたが、 「Sphinx の仕様」として特段に課題点の改善をすることなく次々と文章作成を進めていました。 一見、順調に進み「課題はあるが大した問題にはならなかった」と思えましたが、ある日突然問題が発覚します。 「過去に訳したはずの文章が訳されずに掲載されている箇所がある」ことが判明したのです。 そのことに気付いたのも偶然でした。 原因は、別の文章の翻訳作業中に、該当の翻訳を「もう使用していない」と誤った認識で削除してしまったことでした。 本来ならば、 CI などによる機械的なチェックやレビューによって問題があることに気付くことが期待されます。 しかし、(原文と比べて翻訳が漏れている点の)機械的なチェックも不足していましたし、人の目によるレビューも「無用な差分が多い」ことによって、本質的な差分を見落とす(目が滑る)状況となり、必要な訳を削除してしまったことに気づけなかったのです。 次回予告 次回は、遂に問題を引き起こした課題に対し、我々のとった改善手段について紹介します。 本来は例の記述より先頭側にプロジェクト名や生成日などのメタデータが記述された箇所が数行ありますが、今回注目すべき点ではないので記述を省略しています。  ↩ ↩
アバター
こんにちは、システムエンジニアの Kota です。 以前の記事 で、 AWS に EC2 を構築して、Docker を install し、Hello world! と表示させました。今回は、同じことを Google Cloud Platform ( GCP ) で実践したいと思います。 対象の読者 開発環境で Docker を使っているけど、デプロイにも使いたい方 Docker、GCP に触れてみたい方 AWS は触ったことあるけど、GCP は触ったことがない方 インフラに興味がある方 関連する記事 Dockerの開発環境構築その1-DockerComposeの解説 Dockerの開発環境構築その2-Dockerfileの解説 [AWS]EC2内でDockerコンテナを起動して、ブラウザからアクセスする まずは、完成イメージをご紹介します。 完成イメージ 手順としては以下のようになります。 作業中は、完成イメージを持っているとより理解が深まりやすいと思います。 今回の作業は、GCP のアカウントが必要になりますので、まだお持ちでない方は下記のリンクよりアカウントを作成してください。 無料アカウント作成 では、早速やっていきましょう! 1. 新しいプロジェクトを作成する GCPのプロジェクトとは、Compute EngineやCloud StorageなどのGCPのサービスで作成するリソースをまとめて管理する単位です。 GCPでは、課金の対象がプロジェクト毎に分けられます。 プロジェクトの作成と管理 GCPのコンソールを開くと、デフォルトで、「My First Project」というプロジェクト名のプロジェクトが作成されていると思います。 今回は、「Hello World project」として、新しいプロジェクトを作成しましょう。 2. VPC と サブネットの構築 プロジェクトが作成できたら、次にVPCとサブネットを作成していきます。 GCP と AWS の VPC では以下のような違いがあります。 AWS の VPC リージョンの中に VPC が存在する( リージョンごとに VPC を作成 ) サブネットを AZ ごとに作成 IGW (インターネットゲートウェイ) は VPC に紐付く GCP の VPC 全リージョンに跨っており、グローバルで管理するネットワークになっている VPC 作成時に IP 指定がない( IP はサブネットに紐付く ) IGW を持たない( IGW はサブネットに紐付く ) それではプロジェクトのダッシュボードを開いて下さい。 サイドバーのネットワーキングからVPCネットワークを選択して下さい。 デフォルトでは、default という VPC ネットワークと、 default ネットワークに属する各リージョンのサブネットが表示されています。 上部の「 VPC ネットワークを作成」から作成画面を開きます。 VPCの名前は「hello-world-vpc」とします。 Descriptionは特に記入なしで、VPC ネットワーク ULA の内部 IPv6 範囲 は無効とします。 サブネット作成モードはカスタムを選択します。 サブネットの名前は、「hello-world-subnet」とします。 Descriptionは特に記入なしで、リージョンは「asia-northeast1」にします。 IP スタックタイプは、IPv4(シングル スタック)を選択します。 IPv4 範囲は 「192.168.1.0/24」と指定しておきます。 限定公開の Google アクセス、フローログ はオフを選択しておきます。 ファイアウォール ルールは後ほど編集します。 動的ルーティングモードはリージョンを選択し、最大伝送単位(MTU)はデフォルトの 1460 を選択します。 全て設定できたら、作成をボタンを押します。 一覧画面に「hello-world-vpc」が表示されています。 一覧の各VPCの名前をクリックすると詳細を閲覧でき、ここから各種設定の編集などを行うことができます。 3. ファイアウォールを設定する 前提として、GCP では VPC とインスタンスにファイアウォールを設定できます。 先ほど作成した VPC では、ファイアウォールが未設定ですので、現在の状態では、このネットワーク内にインスタンスを作成しても接続できない状態になっています。ここでは、ssh 接続するためのファイアウォールルールを設定していきます。(http 接続などのルールはインスタンス作成時に設定できます。)詳細画面から編集していきましょう。 VPCの一覧画面から先ほど作成した「hello-world-vpc」を選択し、詳細画面を開きます。 ファイアウォールルールのタブ > ファイアウォール ルールを追加を選択して下さい。 入力画面が開いたら、下記の通りに入力していきます。 4. VM インスタンスを立てる 次は、先ほど作成した VPC 内にインスタンスを立ち上げます。 コンソールの Compute Engine > VM インスタンスをクリックします。 初めてであれば、下記の画面が表示されると思いますので、インスタンスを作成をクリックします。 作成画面が開いたら、下記の通り設定していきます。 注意 今回はあくまでも実験用として http トラフィックを許可していますが、実運用として使う場合は、https 化して下さい。 入力が済んだら、作成ボタンをクリックします。 しばらくすると、一覧に表示されます。 5. SSH でインスタンスにアクセスする 次に 4. で作成したインスタンスに SSH 接続できるか確認してみましょう。 接続方法はいくつかありますが、今回は gcloud コマンドで接続したいと思います。 下記画像の赤枠内の▼をクリックします。 次に 「gcloud コマンドを表示」 クリックします。 すると、コマンドが表示されたモーダルが開きますので、表示されているコマンドをコピーし、 下部の「 CLOUD SHELL で実行」をクリックします。 画面下部にターミナルが表示されたと思いますので、先ほどコピーしたコマンドを貼り付け、実行して下さい。 ※ 初回は、 ssh キーを作成したり、パスワードを設定したりするかと思います。 下記のように表示されれば、接続成功です。 XXXXXX@cloudshell:~ (hello-wolrd-project-351708)$ gcloud compute ssh --zone "asia-northeast1-a" "hello-world-instance" --project "hello-wolrd-project-351708" Enter passphrase for key '/home/XXXXXX/.ssh/google_compute_engine': Welcome to Ubuntu 18.04.6 LTS (GNU/Linux 5.4.0-1075-gcp x86_64) * Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage System information as of Wed Jun 15 04:38:36 UTC 2022 System load: 0.0 Processes: 119 Usage of /: 37.5% of 9.52GB Users logged in: 0 Memory usage: 29% IP address for ens4: 192.168.1.2 Swap usage: 0% IP address for docker0: 172.17.0.1 * Super-optimized for small spaces - read how we shrank the memory footprint of MicroK8s to make it the smallest full K8s around. https://ubuntu.com/blog/microk8s-memory-optimisation 2 updates can be applied immediately. To see these additional updates run: apt list --upgradable New release '20.04.4 LTS' available. Run 'do-release-upgrade' to upgrade to it. *** System restart required *** Last login: Wed Jun 15 04:38:03 2022 from 34.81.211.70 XXXXXX@hello-world-instance:~$ そのまま、次の章でインスタンスに docker を install していきます。 6. VM インスタンス に Docker を install する こちらの Docker 公式のドキュメント を参考にインスタンスに Docker を install していきます。 まずは、apt の update と、必要な package を install します。 $ sudo apt-get update $ sudo apt-get install \ ca-certificates \ curl \ gnupg \ lsb-release 次に Docker の公式 GPG キーを追加します。 $ sudo mkdir -p /etc/apt/keyrings $ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg そして、以下のコマンドを実行してリポジトリを設定します。 $ echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null 上記のコマンドを実行したら、再度 apt を update し、Docker Engine、containerd、Docker Compose を install します。 $ sudo apt-get update $ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin 正常に install できているか確認してみましょう。 以下のコマンドを実行してください。 $ sudo docker info 下記のように表示されれば、install 成功です。 XXXXXXX@hello-world-instance:~$ sudo docker info Client: Context: default Debug Mode: false Plugins: app: Docker App (Docker Inc., v0.9.1-beta3) buildx: Docker Buildx (Docker Inc., v0.8.2-docker) compose: Docker Compose (Docker Inc., v2.6.0) scan: Docker Scan (Docker Inc., v0.17.0) Server: Containers: 0 Running: 0 Paused: 0 Stopped: 0 Images: 1 Server Version: 20.10.17 Storage Driver: overlay2 Backing Filesystem: extfs Supports d_type: true Native Overlay Diff: true userxattr: false Logging Driver: json-file Cgroup Driver: cgroupfs Cgroup Version: 1 Plugins: Volume: local Network: bridge host ipvlan macvlan null overlay Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog Swarm: inactive Runtimes: io.containerd.runtime.v1.linux runc io.containerd.runc.v2 Default Runtime: runc Init Binary: docker-init containerd version: 10c12954828e7c7c9b6e0ea9b0c02b01407d3ae1 runc version: v1.1.2-0-ga916309 init version: de40ad0 Security Options: apparmor seccomp Profile: default Kernel Version: 5.4.0-1078-gcp Operating System: Ubuntu 18.04.6 LTS OSType: linux Architecture: x86_64 CPUs: 2 Total Memory: 973.3MiB Name: hello-world-instance ID: TMIZ:S6PL:RWNQ:BYIQ:CKFC:J3AX:H6SY:2XT5:BEXO:7W33:Q5VV:OMKP Docker Root Dir: /var/lib/docker Debug Mode: false Registry: https://index.docker.io/v1/ Labels: Experimental: false Insecure Registries: 127.0.0.0/8 Live Restore Enabled: false WARNING: No swap limit support 今のままだと、docker コマンドを打つ際に、sudo を付けなければなりません。 これは、現在のユーザーに docker コマンドの操作権限がない為です。 ユーザーに操作権限を与える為、以下のコマンドを実行します。 $ sudo usermod -a -G docker [user name] ※ $ who で user name を確認できます。 上記のコマンドは、操作権限をもつ docker グループに ユーザー を加えるコマンドです。詳しく知りたい方は、 公式ドキュメント を参照して下さい。 グループの追加は、シェルを起動し直さないと反映されないので、一度 exit でサーバーからログアウトし、ログインし直して下さい。 sudo なしで、 docker info と実行し、先ほどと同じ内容の出力がされれば、グループへの追加が成功しています。 7. インスタンス内でコンテナを立ち上げ、ブラウザからアクセスする さて、それでは最後の工程です。 インスタンスに ssh で接続している状態だと思いますので、そのまま作業ディレクトリを作成しましょう。 XXXXXXX@hello-world-instance:~$ mkdir hello-world-docker XXXXXXX@hello-world-instance:~$ cd hello-world-docker/ そして、Hello world! と表示させる為の html ファイルを用意します。 XXXXXX@hello-world-instance:~/hello-world-docker$ vi hello-world.html vi が開きますので、i で insert mode にし、 Hello world! と記述して、esc キー、 :wq で保存します。 ls コマンドでディレクトリに今作成した html ファイルが作成されていると思います。 XXXXXX@hello-world-instance:~/hello-world-docker$ ls hello-world.html 次にDockerimage を作成する為に、Dockerfile を作成、編集していきます。 今回も 前回 と同様、webサーバーに nginx を使用します。先ほど作成した、html ファイルを nginx 上で表示させます。 XXXXXX@hello-world-instance:~/hello-world-docker$ vi Dockerfile Dockerfile FROM nginx COPY ./hello-world.html /usr/share/nginx/html/ nginx では、デフォルトの状態だと/usr/share/nginx/html/ 配下がブラウザからアクセスした際の初期表示になっているので、./hello-world.html をコピーします。 編集出来たら、先ほどと同じように保存して下さい。 そして、作成したDockerfile を元に image を build します。 下記のコマンドを実行します。 XXXXXX@hello-world-instance:~/hello-world-docker$ docker build -t hello-world-docker . build が完了したら、container を起動します。 下記のコマンドを実行して下さい。 XXXXXX@hello-world-instance:~/hello-world-docker$ docker run --rm -d -p 80:80 hello-world-docker 今回はあくまで実験として作業をしているので、--rm オプションをつけてコンテナ終了時に削除していますが、本番を想定する場合は、オプションを付与しなくても良いかも知れません。 docker ps コマンドでコンテナの STATUS が UP になっていれば、起動に成功しています。 XXXXXX@hello-world-instance:~/hello-world-docker$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES a2f44503699c hello-world-docker "/docker-entrypoint.…" 20 seconds ago Up 19 seconds 0.0.0.0:80->80/tcp, :::80->80/tcp peaceful_faraday ここまでで、全ての作業が完了です。 それではブラウザからアクセスしてみましょう。 コンソール画面で VM インスタンスの一覧を開き、4. で作成した インスタンスの外部IP をコピーし、ブラウザで IPアドレス / hello-world.html  でアクセスします。 無事に Hello world! と表示させることができました! さて、如何だったでしょうか? 今回は、GCP に VM インスタンス を立て、その中で Docker コンテナを起動させ、ブラウザからアクセスすることをハンズオン形式でやってみました。 前回 の AWS と概念が違う部分もありましたが、大筋は同じ流れで作業を進めて上手く表示させることができました。 今回も最後まで読んで頂き、ありがとうございました!
アバター
はじめに こんにちは、インターン生の手塚です。 今回はGROWIにおけるwebpackの設定について、調べてみたので記事にします。この記事はGROWIにおけるwebpackの設定に着目しているのでwebpackの基礎知識や、使い方の詳細は説明していません。webpackについてある程度の知識がある人に、プロジェクトへの活用例として参考にしてもらえればなと思っております。 webpackは設定が複雑で、そのため「webpack職人」と呼ばれる人たちが存在します。本当は、誰もが簡単に設定できるのが理想であり、Next.jsなどでは一部を自動的にやってくれたりもしています。ですが、まだwebpackがweb開発におけるモジュールバンドラーとして多く用いられているのは事実であり、webpackの知識は持っていて損はないでしょう。ということを先輩に言われたのでそういう思いで勉強しました。 そもそもwebpackとは 公式ドキュメント https://webpack.js.org/ webpackはいわゆる「モジュールバンドラー」と呼ばれるもので、設定ファイルの指示に基づいて複数のJSファイルやCSSファイル、画像ファイルなどを一つにまとめる機能を持っています。ブラウザを例に出すと、モジュールバンドラーを活用することで読み込むファイルの数が少なくなったり、バンドル化される際に無駄な行が省かれたりして効率よくファイルを読み込むことができます。そして、そのモジュールバンドラーの筆頭が「webpack」というわけです。 GROWIにおけるwebpackの使い方の概要・イメージ この記事はwebpack4系に関する情報になります。2022年6月時点での最新版は5.73.0なので、最新の情報は公式ドキュメントを確認してください https://webpack.js.org/ GROWIでは開発用と製品用の2種類のwebpack設定があり、それぞれの共通設定をまとめたファイルもあります。開発用と製品用の設定ファイルでそれぞれ共通の処理を呼び出しているイメージです。なのでwebpack設定関連のファイルは webpack.common.js webpack.dev.js webpack.prod.js の3つになります。 そして、それぞれのファイルに従ってJSファイルやCSSファイル、画像ファイルをまとめた上で、そのまとめられたファイルをブラウザ上で読み込むことでアプリが動いています。ここからはGROWIの実際の設定ファイルの中でもメインとなる共通の設定ファイルをみて説明していきます。webpackの設定はたくさんありますがこの記事はあくまでGROWIの設定の紹介なので登場しない設定もあります、ご了承ください。 GROWIにおけるwebpackの詳細設定 これが共通のファイルです。 // webpack.common.js const path = require('path'); const webpack = require('webpack'); /* * Webpack Plugins */ const WebpackAssetsManifest = require('webpack-assets-manifest'); const LodashModuleReplacementPlugin = require('lodash-webpack-plugin'); const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); /* * Webpack configuration * */ module.exports = (options) => { return { mode: options.mode, entry: Object.assign({ 'js/boot': './src/client/boot', // ~~省略~~ 'styles/style-hackmd': './src/styles-hackmd/style.scss', }, options.entry || {}), // Merge with env dependent settings output: Object.assign({ path: path.resolve(__dirname, '../public'), publicPath: '/', filename: '[name].bundle.js', }, options.output || {}), // Merge with env dependent settings externals: { jquery: 'jQuery', emojione: 'emojione', hljs: 'hljs', 'dtrace-provider': 'dtrace-provider', }, resolve: { extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], plugins: [ new TsconfigPathsPlugin({ configFile: path.resolve(__dirname, '../tsconfig.build.client.json'), extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], }), ], }, node: { fs: 'empty', }, module: { rules: options.module.rules.concat([ // ~~省略~~ { test: /\.(eot|woff2?|svg|ttf)([?]?.*)$/, use: 'null-loader', }, ]), }, plugins: options.plugins.concat([ new WebpackAssetsManifest({ publicPath: true }), // ~~省略~~ ]), devtool: options.devtool, target: 'web', // Make web variables accessible to webpack, e.g. window optimization: { namedModules: true, splitChunks: { cacheGroups: { style_commons: { test: /\.(sc|sa|c)ss$/, chunks: (chunk) => { // ignore patterns return chunk.name != null && !chunk.name.match(/style-|theme-|legacy-presentation/); }, name: 'styles/style-commons', minSize: 1, priority: 30, enforce: true, }, // ~~省略~~ }, }, minimizer: options.optimization.minimizer || [], }, performance: options.performance || {}, stats: options.stats || {}, }; }; これは共通のファイルですが、開発用、製品用の設定もあります。 それは、上の共通ファイルにもあるように options.module.rules.concat([ このようにして、共通のファイルと開発用、製品用の設定をそれぞれマージしています。 設定の詳細についてみていきましょう。 mode mode: options.mode, // 開発用、製品用でそれぞれ'development', 'production'を設定 この項目に設定できるのは、 production , development , none の3つです。productionモードでは不要な行が削除されたり、ブラウザが読み込むのに最適化されているのでデバッグに向いていません。なのでGROWIではそれぞれ、開発用では development 、本番用では production を設定しています。 entry entry: Object.assign({ 'js/boot': './src/client/boot', 'js/app': './src/client/app', // ~~省略~~ 'styles/theme-blackboard': './src/styles/theme/blackboard.scss', 'styles/style-hackmd': './src/styles-hackmd/style.scss', }, options.entry || {}), // Merge with env dependent settings この項目では読み込みを開始するファイル(エントリーポイント)を指定します。現在は Objct で指定していますが、 string や string[] などでも指定できます。大規模な開発になると複数のファイルを読み込んで複数のバンドルを生成することもあるので、その場合はエントリーポイントにチャンク名をつけることで出力先でチャンク名を利用することができ、わかりやすくすることができます。 output output: Object.assign({ path: path.resolve(__dirname, '../public'), publicPath: '/', filename: '[name].bundle.js', }, options.output || {}), // Merge with env dependent settings この項目ではバンドル化されたファイルの出力先を指定します。GROWIではpublicディレクトリ下に [name].bundle.js というバンドルファイルが生成されます。エントリーポイント設定の一番上の例でみると、 js/boot というチャンク名で ./src/client/boot のファイルが指定されているので /js/boot.bundle.js というファイルが public ディレクトリ下に生成されます。publicPathの項目では、outputに出力されたファイルが参照される先を指定します。GROWIの設定の場合、出力されたファイルは / ルートディレクトリから参照されます。 external externals: { jquery: 'jQuery', emojione: 'emojione', hljs: 'hljs', 'dtrace-provider': 'dtrace-provider', }, この項目では外部依存のままにしたいため、バンドル対象から外すものを設定しています。GROWIではjQueryやemojioneなどをこの項目に設定して、外部依存にしています。ここに設定せずにscriptでCDN読み込みをしていて、 import して使用しているとwebpackはモジュール解決できずにエラーが出てしまいます。 resolve resolve: { extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], plugins: [ new TsconfigPathsPlugin({ configFile: path.resolve(__dirname, '../tsconfig.build.client.json'), extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], }), ], }, この項目ではモジュールがバンドル化される際の設定をすることができます。 extensions ここではエントリーポイントのファイル拡張子を定義します。webpackはファイルを処理するとき、ここに定義された拡張子を配列の先頭から見ていき、当てはまったときに処理を開始します。 plugins ここではモジュールをバンドル化する際に使用するプラグインを定義します。GROWIでは TsconfigPathsPlugin というプラグインを用いています。 ts-loader を用いてJSのトランスパイルをするとき、 tsconfig.json の baseUrl , paths を用いている場合、このプラグインを入れないとpathsのエイリアスをwebpackが利用できません。 node node: { fs: 'empty', }, この項目ではnodeにおけるモジュールに対して、ポリフィルを行ったり、モックを入れたりすることを設定します。nodeのモジュールはブラウザからは利用できないため、適切に設定をしないまま実行すると「 fs がないよ」と怒られてしまいます。なのでGROWIでは empty を設定することで fs に空のオブジェクトをいれ、エラーを回避しています。 https://stackoverflow.com/questions/39249237/node-cannot-find-module-fs-when-using-webpack 上のリンクでも議論されていますが、これは少し古めの解決策になっています。 module module: { rules: options.module.rules.concat([ { test: /.(jsx?|tsx?)$/, exclude: { test: /node_modules/, exclude: [ // include as a result /node_modules\/codemirror/, ], }, use: [{ loader: 'ts-loader', options: { transpileOnly: true, configFile: path.resolve(__dirname, '../tsconfig.build.client.json'), }, }], }, { test: /locales/, loader: '@alienfast/i18next-loader', options: { basenameAsNamespace: true, }, }, /* * File loader for supporting images, for example, in CSS files. */ { test: /\.(jpg|png|gif)$/, use: 'file-loader', }, /* File loader for supporting fonts, for example, in CSS files. */ { test: /\.(eot|woff2?|svg|ttf)([?]?.*)$/, use: 'null-loader', }, ]), }, この項目ではそれぞれのモジュールがどのようにバンドル化されるかの詳細を設定します。 rules 公式ドキュメントによると、 rule には3つのパーツがあり、 Conditions , Results , nested Rules とされています。それぞれを解釈するなら、 条件 、 処理 、 ネストされた条件 となるかなと思います。この項目では基本的に、こんな条件の時はこういった処理をする、という設定をしています。一番上の例を見ると、正規表現で js, jsx, ts, tsx を指定して、 exclude で node_modules 配下のファイルは除外することを設定しています。さらに、その中で、 exclude で /node_modules/codemirror/ をしているので node_modules 配下のうち codemirror のみを含めます。ここまでがruleのうち、条件です。そしてこれらの条件に合致するファイルを use で指定したloaderで処理します。今回は ts-loader で処理しており、 option で transpileOnly を true にすることで型のチェックや型定義ファイルの出力を省略することでコンパイルにかかる時間を少なくしています。ここで型のチェックを行わない代わりに、GROWIではCIで tsc を実行し、チェックしています。 1番上の設定では上記のようなことを行っています。 plugins plugins: options.plugins.concat([ new WebpackAssetsManifest({ publicPath: true }), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), }), // ignore new webpack.IgnorePlugin(/^\.\/lib\/deflate\.js/, /markdown-it-plantuml/), new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), new LodashModuleReplacementPlugin({ flattening: true, }), new webpack.ProvidePlugin({ // refs externals jQuery: 'jquery', $: 'jquery', }), ]), ここではloaderでは提供できない追加機能を提供するプラグインを設定します。 WebpackAssetsManifest このプラグインでは元のファイル名とハッシュ化されたファイル名を対応させるためのJSONファイルを生成します。 webpack.DefinePlugin このプラグインではコンパイル時に設定されるグローバルな定数を設定することができます。 webpack.IgnorePlugin このプラグインではバンドル化される際に無視するものを定義します。例えば、localeファイルなどはここで設定することで使用しない大部分のファイルをバンドル時に無視することができます。 LodashModuleReplacementPlugin このプラグインではloadshの中で使われているもののみをバンドル化してバンドルファイルが肥大化するのを防いでいます。 webpack.ProvidePlugin このプラグインではモジュールを import や require で読み込まなくて自動的に読み込む機能を提供します。 devtool devtool: options.devtool, // 開発時のみ'cheap-module-eval-source-map'を指定 この項目では、ソースマップを生成するかまたどのように生成するかを設定しています。ソースマップによってバンドル化されたファイルと元のファイルの関連がわかるのでデバッグがしやすくなるため、開発時にはとても便利です。このオプションにはたくさんの種類があるのですが、GROWIではts-loaderでサポートされている cheap-module-eval-source-map を使用しています。製品版の設定ではこの項目は上書きされています。 target target: 'web', この項目では、生成したバンドルファイルのターゲットを設定します。例えばNode.jsの環境でrequireを使ってバンドルファイルを読み込む場合はここで node を設定したりするようですが、今回の目的はブラウザでHTMLから読み込むことなので web を指定します。 optimization optimization: { namedModules: true, splitChunks: { cacheGroups: { style_commons: { test: /\.(sc|sa|c)ss$/, chunks: (chunk) => { // ignore patterns return chunk.name != null && !chunk.name.match(/style-|theme-|legacy-presentation/); }, name: 'styles/style-commons', minSize: 1, priority: 30, enforce: true, }, commons: { test: /(src|resource)[\\/].*\.(js|jsx|json)$/, chunks: (chunk) => { // ignore patterns return chunk.name != null && !chunk.name.match(/boot/); }, name: 'js/commons', minChunks: 2, minSize: 1, priority: 20, }, vendors: { test: /node_modules[\\/].*\.(js|jsx|json)$/, chunks: (chunk) => { // ignore patterns return chunk.name != null && !chunk.name.match(/boot|legacy-presentation|hackmd-/); }, name: 'js/vendors', minSize: 1, priority: 10, enforce: true, }, }, }, minimizer: options.optimization.minimizer || [], }, この項目ではwebpackを実行するときの様々な最適化についての設定をしています。 namedModule この項目に true を設定することでモジュールに名前が設定され、デバッグがしやすくなります。 splitChunks この項目では「複数のエントリーポイント間で利用している共通モジュールをバンドルしたファイル」を出力するための設定をします。 splitChunks に cacheGroups を設定することで共通化するバンドルファイルをグループ化することができます。 1つ目の設定を例にみると scss, sass, css のファイルが複数のモジュールから利用されている場合、チャンクの名前がnullでなく、名前に「style-, theme-, legacy-presentation」が含まれない場合、 styles-style-commons にバンドルファイルが生成されます。さらに minSize で 1 が設定されているので1byte未満のモジュールは複数のモジュールで利用されていても共通化はしないような設定になっています。また、 priority が設定されているのでチャンクが複数の cacheGroup にまたがっているときには priority が高いグループが優先されます。 minimizer この項目に値をいれることで実行時に利用するデフォルトのminimizerを上書きすることができます。 performance この項目ではwebpackの様々な挙動を設定します。GROWIでは開発時に hints を false にすることでwebpackが何か変化を検知しても警告や注意を出さない設定にしています。 まとめ 以上がGROWIにおけるwebpackの設定です。最初にも書いたように、理想はこんな複雑な設定を書かなくてもだれでもモジュールバンドラーの設定ができることでしょう。しかし、webpackはまだまだ使われているため、webpackの設定を知っていないと困る場面があるかもしれません。今回はそんなwebpackの設定について調べてみたという記事でした。
アバター
UI flickering はイケてない WESEEK エンジニアの武井です。 みなさん、フロントエンドプログラミングやってますか? 今回のネタは「UI flickering」。React の初回レンダリングやステートを更新した際、こんな感じの挙動してないでしょうか。 出典: https://stackoverflow.com/questions/55032136/react-ui-flickering-when-state-updated この一瞬今描画しているアイテム消えてパッと次のアイテムが出てくるまでの一瞬の間が見える現象、こちらが「UI flickering」です。 和製英語だと「UIフリッカー」とか言われる事もありますがあまり一般的ではなく、「カクつき」「チラつき」みたいな表現の方が馴染みがあるかもしれません。 UI flickering は、普通に React で Backend/DB からデータ取ってきて更新するコンポーネントを何も考えずに書くと大体起こってしまいます。UI/UX が壊滅的に悪くなるものではないのでプロダクト・実装箇所によっては許容されるかもしれませんが、イケてるかイケてないかで言えばやっぱりあんまりイケてない部類に入ります。 今回はそれを撲滅するための3つの Tips を紹介します。 目次 対策1. ローディング中はスケルトンを表示する 一番単純な戦略は、スケルトンコンポーネントを用意し、ローディング中はそちらを表示することです。 const MyComponent = (): JSX.Element => { const [isLoading, setLoading] = useState(false); ... return isLoading ? <MyListSkelton length={data.length} /> : <MyList />; } MyList コンポーネントと同じような MyListSkelton を用意しておき、代わりにそれを表示するというものです。 このようなシーンではスピナーを表示することも多いと思いますが、実際に表示したいアイテム群と同じ幅・高さを持つスケルトンに置き換えることで UI flickering を抑制できます。 出典: https://mui.com/material-ui/react-skeleton スケルトンについては Material UI などが参考になります。 react-loading-skelton も汎用的で便利。React 以外でも使えるのだと Placehold-it も有名ですね。素の Bootstrap 5 にもこういうのあったらいいんだけどなあ。 SWR を使おう 対策の2つめの説明に入る前に、 SWR を紹介します。 SWR とは "stale-while-revalidate" の頭文字を取ったもので、HTTP 通信に於けるキャッシュ戦略の概念・仕様です。そして Vercel が作っている同名のライブラリ(実装)が存在し、React と一緒に使うことができます。 似たライブラリとしては React Query があります。 SWR のうれしみ いくつかありますが主要なものを挙げると、 データ取得の後、結果のキャッシュ化が簡単になる データ更新、再取得が必要になったときの手順が簡単になる そして、 「対策1. ローディング中はスケルトンを表示する」の isLoading の管理が簡単になる こちらがこのエントリーで SWR を推す理由です。 SWR 利用 Example import { useSWRxMyListData } from '../stores/mylist'; const MyComponent = (): JSX.Element => { const { data, error } = useSWRxMyListData(); ... const isLoading = error == null && data === undefined; return isLoading ? <MyListSkelton length={data.length} /> : <MyList />; } const fetcher = url => fetch(url).then(r => r.json()) export const useSWRxPage = (): SWRResponse<MyListData, Error> => { return useSWR<MyListData>('/_api/mylist', fetcher); }; SWR 結果から isLoading が作られるため、通信開始時に true にして、finally で false に戻してみたいな管理をする必要がありません。 蛇足 1 因みに WESEEK では JavaScript/TypeScript のコード中で null/undefined チェックを行う場合は if (value != null) {...} のように、厳密等価演算子ではなく等価演算子と null 値との比較に統一していますが、SWR でデータ取得前の状態を判定する場合は === undefined を利用しています。理由は取得データが null 値の場合があるからですね。プロダクトの中では珍しいコードになっています。 /Tips/JavaScript#判定式 対策2. 初期ステートを localStorage から取得する 続いての対策は、ある条件下・シチュエーションのもとで効果を発揮します。 例えば VSCode のようなアプリを作っているとして、サイドバーの開閉状態をサーバー側(DB側)で保持するようなシナリオを考えてみましょう。 ユーザーがアプリを使う時に、サイドバーの開閉状態を変えた 開閉状態を変更すると、その値が DB に保存される 次回アクセス時はサイドバー開閉状態が復元される ソースコードは次のようになるでしょう。 import { useSWRxMySettings } from '../stores/mysettings'; const App = (): JSX.Element => { const { data: mySettings } = useSWRxMySettings(userId); ... return ( <> <Sidebar isOpen={mySettings.isSidebarOpened} /> <Contents /> </>; ) } const fetcher = url => fetch(url, params).then(r => r.json()) export const useSWRxMySettings = (userId: string): SWRResponse<UISettings, Error> => { return useSWR<UISettings>( ['/_api/mysettings', { userId }], fetcher ); }; さて、ここで初回のレンダリングをどうするかが悩ましいところです。 コンポーネントの状態を正確に再現するための設定値はバックエンドが持っているのでまず API にアクセスすることは必須ですが、その通信結果を待たないとユーザーが期待する UI、つまりサイドバーを開いて描画するか閉じて描画するかを決定できません。 もちろん何も考えずに「デフォルトステートを閉じた状態」とすると、保存された設定値が「開いた状態」だった際に UI flickering が起きるでしょう。また、「対策1. ローディング中はスケルトンを表示する」はそもそも幅・高さをとっていいケースなのかどうかが確定していない今回は使えませんね。 一つの選択肢として、「バックエンドからの設定値が届くまで、画面全体をロード中状態としてよい」という仕様でよいと割り切るのであれば、初回レンダリングが遅くなることと引き換えに UI flickering を避けられます。その遅延を UX 的に許容できない場合は別の対策が必要です。 localStorage への保存の検討 このケースへの対策として、DB に保存した設定値を localStorage にも保存・同期する戦略を検討してみます。 ユーザーがアプリを使う時に、UI設定を変えた 上記結果が DB に保存される 同時にユーザーのブラウザの localStorage に同じ設定値が保存されるようにする 次回アクセス時、localStorage に入っている設定値を取り出し、SWR の初期キャッシュに入れる 大体のケースで REST の通信のレスポンスよりも localStorage からの値抽出の方が早いので、こちらを利用して初回レンダリングが行われる DB から取得した結果を適用し、再度レンダリングされる localStorage に入っている設定値と一致していれば、UI flickering が起こらない ではこれをどう実現すればいいでしょうか。 汎用 SWR middleware (保存版) はい、こちらをコピペでモジュール化してご利用ください。 解説は省きますが、SWR hook に指定可能な fallbackData に localStorage 由来の設定値を入れて初期化しています。 import { Middleware } from 'swr'; const generateKeyInStorage = (key: string): string => { return `swr-cache-${key}`; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type IStorageSerializer<Data = any> = { serialize: (value: Data) => string, deserialize: (value: string | null) => Data, } export const createSyncToStorageMiddlware = ( storage: Storage, storageSerializer: IStorageSerializer = { serialize: JSON.stringify, deserialize: JSON.parse, }, ): Middleware => { return (useSWRNext) => { return (key, fetcher, config) => { if (key == null) { return useSWRNext(key, fetcher, config); } const keyInStorage = generateKeyInStorage(key.toString()); let initData = config.fallbackData; // retrieve initial data from storage const itemInStorage = storage.getItem(keyInStorage); if (itemInStorage != null) { initData = storageSerializer.deserialize(itemInStorage); } const swrNext = useSWRNext(key, fetcher, { fallbackData: initData, ...config, }); return { ...swrNext, // override mutate mutate: (data, shouldRevalidate) => { return swrNext.mutate(data, shouldRevalidate) .then((value) => { storage.setItem(keyInStorage, storageSerializer.serialize(value)); return value; }); }, }; }; }; }; export const localStorageMiddleware = createSyncToStorageMiddlware(localStorage); export const sessionStorageMiddleware = createSyncToStorageMiddlware(sessionStorage); 使い方は、カスタムフック側を少し変えるだけで済みます。 use: [middlewareInstance] という形式でオプションを足します。 const fetcher = url => fetch(url, params).then(r => r.json()) export const useSWRxMySettings = (userId: string): SWRResponse<UISettings, Error> => { return useSWR<UISettings>( ['/_api/mysettings', { userId }], fetcher, { use: [localStorageMiddleware] }, // store to localStorage for initialization fastly ); }; これで、「対策1. ローディング中はスケルトンを表示する」を適用できないが可能な限り初期値セットを急ぎたい場合に対応できました。 蛇足 2 GROWI では実際に以下の場所で利用しています。 stores/middlewares/sync-to-storage.ts 利用側: UI モード取得部分 蛇足 3 もちろんこの対策は銀の弾丸ではありません。 localStorage に DB 値のコピーを保存すると言うことは、PCを2台(またはブラウザ2種類)を使い分けているケースで不整合が発生します。頻繁に利用環境を変えるユーザーにとっては、かえって UI flickering が起こりやすくなることもあるかもしれません。トレードオフを考慮し採否を検討してください。 対策3. Revalidate 中に前のステートを初期化しない (>= SWR@2.0.0) この対策は、SWR ライブラリのアップデートの先取りになります。 2022.06.13 時点で SWR ライブラリの最新バージョンは 1.3.0 ですが、開発チームは次のメジャーバージョンである 2.0.0 を開発中です。GitHub でβ版リリースを見ることができます 2.0.0-beta.1 のハイライトを見てみましょう。 公式の isLoading ステート 「SWR を使おう」セクションで isLoading ステートを作成するコードを紹介しました。こちらです。 const isLoading = error == null && data === undefined; 2.0.0 では error , data から作らなくても、useSWR を呼び出した結果からこの boolean を得られるようです。 const { data, isLoading, isValidating } = useSWR(STOCK_API, fetcher, { refreshInterval: 3000 }); // If it's still loading the initial data, there is nothing to display. // We return a skeleton here. if (isLoading) return <div className="skeleton" />; これだけだと単に使い勝手が良くなった程度ですが、更に UI flickering に効果があるオプションも追加されています。 keepPreviousData オプション 出典: https://github.com/vercel/swr/releases/tag/2.0.0-beta.1 このオプションを利用することで、データの再取得時、 isLoading が true の間も以前の data を返し続けてくれます。つまり、データ取得条件が変わってデータの再取得を行っている間に起こる UI flickering をオプション一つで抑制できるというわけです。 まとめ 幅・高さが決まっているコンポーネントは、データロード中にスケルトンを表示することで UI flickering を抑制できる SWR という stale-while-revalidate 戦略がある React 向けには同名のライブラリがあって便利 SWR は、HTTP リクエスト/レスポンス前後のキャッシュコントロールを肩代わりし、賢く管理してくれる プロダクトで独自実装が必要だったキャッシュコントロールのためのコードの多くが不要になる SWR 2.0.0 にはもっと便利で UI flickring に効果があるオプションも追加される!楽しみ! DB にしかない値を localStorage にも同期させることで、UI flickering を抑制できるケースもある UI flickering / UIフリッカー / 画面のカクつきチラつきを撲滅せよ 以上です。読んでいただいてありがとうございました。 よかったら Twitter の方もフォローしてください。技術ネタや組織作りについてたまに呟きます → https://twitter.com/yuki_takei 一緒に SWR 広めましょう
アバター
はじめに(執筆の動機) GROWI.cloud は、弊社が主に開発し、 OSS として GitHub に公開されている GROWI を、 SaaS としてクラウド版提供するサービスです。 本サービスでは、サービス上で立ち上げる GROWI のバージョンを適切にコントロールするために npm で公開されている semver ライブラリを利用しています。 実装の際に調べてみたところ、 https://devhints.io/semver など semver の概念のチートシートはありましたが、Node.js の semver ライブラリの日本語版で分かりやすいチートシートや参考記事が少ないなと感じたため、今後の誰かのためになるならと思い執筆しました。 ご参考になれば幸いです。 semverとは メジャー.マイナー.パッチ(-プレリリースprefix.ブレリリース番号) の3つ(プレリリースを入れると4つ)のリリースタイプ ( release type ) で構成される semver.org が提唱するバージョン採番の仕様 ※この記事では、あくまで Node.js の semver ライブラリチートシートとして活用されることを目的としているため、 semver の仕様自体については深く触れません。 semver(Node.jsライブラリ)のチートシート semver が node module として用意しているメソッドのうち、よく使われる機能について紹介していきます。 以降は、 import semver from 'semver'; をしたうえで記述しているコードととらえてください。 パース&バリデーション系 文字列を semver 形式でパースする 正常にパースできたら SemVer オブジェクトを、できない場合は null を返す semver.parse('1.2.3-alpha.1'); // SemVer Object 1.2.3-alpha.1 /** { "options": { "loose": false, "includePrerelease": false }, "loose": false, "raw": "1.2.3-alpha.1", "major": 1, "minor": 2, "patch": 3, "prerelease": [ "alpha", 1 ], "build": [], "version": "1.2.3-alpha.1" } */ バージョン(文字列)のバリデーション (semver のチェック) validation OK であればパースされたバージョン文字列を、NG の場合は null を返す semver.valid('1.2.3'); // '1.2.3' semver.valid('a.b.c'); // null 不要な文字列を除去してバージョンの文字列のみ抽出する semver.clean(' =v1.2.3 '); // '1.2.3' 文字列を semver に対応するバージョンに矯正する semver.coerce('v2'); // SemVer Object 2.0.0 (SemVer オブジェクトの構成は上に出しているため割愛) semver.coerce('42.6.7.9.3-alpha'); // SemVer Object 42.6.7 (SemVer オブジェクトの構成は先に出しているため割愛) 第一引数で渡したバージョンが、第二引数に指定した条件の範囲 (version range) を満たすかチェック 第二引数に指定できる条件 (range) の書き方は #レンジの書き方 を参照 const range = '1.x || >=2.5.0 <2.6.0 || 5.0.0 - 7.2.3'; semver.satisfies('1.2.3', range); // true semver.satisfies('2.3.4', range); // false semver.satisfies('2.5.4', range); // true semver.satisfies('8.0.0', range); // false 比較系 チェック対象のバージョンが比較対象のバージョン より高い かチェック prerelease version は、 non prerelease の version より低い semver.gt('4.5.12', '4.5.15'); // false semver.gt('4.5.12', '4.5.12-alpha.1'); // true semver.gt('4.5.12', '4.5.12'); // false チェック対象のバージョンが比較対象のバージョン より低い かチェック semver.lt('4.5.12', '4.5.15'); // true semver.lt('4.5.12', '4.5.12-alpha.1'); // false semver.lt('4.5.12', '4.5.12'); // false チェック対象のバージョンが比較対象のバージョン 以上 かチェック semver.gte('4.5.12', '4.5.15'); // false semver.gte('4.5.12', '4.5.12-alpha.1'); // true semver.gte('4.5.12', '4.5.12'); // true チェック対象のバージョンが比較対象のバージョン 以下 かチェック semver.lte('4.5.12', '4.5.15'); // true semver.lte('4.5.12', '4.5.12-alpha.1'); // false semver.lte('4.5.12', '4.5.12'); // true チェック対象のバージョンが比較対象のバージョンと同一かチェック semver.eq('4.5.12', '4.5.15'); // false semver.eq('4.5.12', '4.5.12-alpha.1'); // false semver.eq('4.5.12', '4.5.12'); // true チェック対象のバージョンと比較対象のバージョンの大小をチェック 第一引数を第二引数と比較して、小さい場合は -1, 大きい場合は 1, 合致する場合は 0 を返す semver.compare('4.5.12', '4.5.15'); // -1 semver.compare('4.5.12', '4.5.12-alpha.1'); // 1 semver.compare('4.5.12', '4.5.12'); // 0 第二引数に指定した比較条件で、チェック対象のバージョンと比較対象のバージョンを比較した結果を得る semver.cmp('4.5.12', '<=', '4.5.12'); // true semver.cmp('4.5.12', '===', '4.5.12'); // true semver.cmp('4.5.12', '!==', '4.5.12'); // false チェック対象のバージョンと比較対象のバージョンとの差分が度のリリースタイプにあるのかをチェック semver.diff('4.5.12', '4.5.12'); // null semver.diff('4.5.12', '4.5.13'); // 'patch' semver.diff('4.5.12', '4.3.13'); // 'minor' semver.diff('4.5.12', '5.3.13'); // 'major' semver.diff('4.5.12', '4.5.12-RC.1234543'); // 'prerelease' バージョン操作系 バージョンをインクリメントさせる semver.inc('4.5.12', 'prerelease', 'RC'); // '4.5.13-RC.0' semver.inc('4.5.12', 'patch'); // '4.5.13' semver.inc('4.5.12', 'minor'); // '4.6.0' semver.inc('4.5.12', 'major'); // '5.0.0' その他 各リリースタイプのバージョン番号のみ抽出 semver.major('4.5.12'); // 4 semver.minor('4.5.12'); // 5 semver.patch('4.5.12'); // 12 semver.prerelease('4.5.12'); // null semver.prerelease('4.5.12-RC.1234543'); // ['RC', 1234543] オプションについて semver に用意されているすべてのメソッドは、最後の引数にオプションオブジェクトを取ります。 すべてのオプションは、デフォルトではfalseです。 オプションオブジェクトは { key: value, key2: value2 } の形で同時に複数指定できます。 オプション一覧 ・ includePrerelease : true を指定することで、プレリリースのタグ付きバージョン指定をレンジに含めることができます。 ・ loose : true を指定することで、たとえば v2 などのような semver として有効でない文字列を許容することができます。 import semver from 'semver'; semver.satisfies('5.0.0-RC.1', '>=5.0.0-RC.0', { includePrerelease: true }); // true semver.satisfies('5.0.0-RC.1', '>=5.0.0', { includePrerelease: true }); // false レンジの書き方 レンジ (version range) とは、バージョンの範囲を示す条件 ドキュメントには以下の記載があります。 バージョン範囲は、範囲を満たすバージョンを指定するコンパレータのセットです (A version range is a set of comparators which specify versions that satisfy the range.) コンパレータは、オペレータ(演算子)とバージョンで構成されます (A comparator is composed of an operator and a version.) ココでは、 semver.satisfies(v, range) ( v が range を満たすかチェックするメソッド) を用いて記載していきます。 ※チルダ ( ~1.2.3 ) と、キャレット ( ^1.2.3 ) の範囲指定についての紹介は、今回は省略します。 比較演算子 import semver from 'semver'; // 4.5.12 未満 semver.satifsies('4.5.12', '<4.5.12'); // false // 4.5.12 以下 semver.satifsies('4.5.12', '=<4.5.12'); // true // 4.5.12 より高い semver.satifsies('4.5.12', '>4.5.12'); // false // 4.5.12 以上 semver.satifsies('4.5.12', '=>4.5.12'); // true // 4.5.12 semver.satifsies('4.5.12', '=4.5.12'); // true 条件の結合 ' '(半角空白) または '||' を用いて、条件を結合できます。 条件のAND結合 ' '(半角空白) で結合すると、結合して指定された条件全てに合致するかチェックされます。 つまり、条件を AND 結合したのと同等になります。 import semver from 'semver'; // 例) range = '>=4.5.1 <4.5.18'; // 4.5.1 以上 かつ 4.5.18 未満 semver.satifsies('4.5.12', range); // true semver.satifsies('4.6.2', range); // false 条件のOR結合 '||' で結合すると、結合して指定された条件のいずれかに合致するかチェックされます。 つまり、条件を OR 結合したのと同等になります。 import semver from 'semver'; // 例) range = '4.5.18 || 4.6.2'; // 4.5.18 または 4.6.2 semver.satifsies('4.5.12', range); // false semver.satifsies('4.5.18', range); // true semver.satifsies('4.6.2', range); // true 結合の優先順 一般的な AND → OR と優先順は変わりません。 import semver from 'semver'; // 例) range = '4.6.2 || >=4.5.1 <4.5.18'; // 4.6.2 または (4.5.1 以上 かつ 4.5.18 未満) semver.satifsies('4.5.12', range); // true semver.satifsies('4.5.19', range); // false semver.satifsies('4.6.2', range); // true -(ハイフン)範囲指定 '-'(ハイフン) つなぎでバージョンの範囲を指定することができます。 記法: 低いバージョン - 高いバージョン import semver from 'semver'; // 例) range = '4.5.12 - 5.0.4'; // 4.5.12 以上 ~ 5.0.4 未満 semver.satifsies('4.5.12', range); // true semver.satifsies('4.5.11', range); // false semver.satifsies('4.6.2', range); // true semver.satifsies('5.0.4', range); // false ワイルドカード指定 x , X , * のいずれかを使用して、[ メジャー , マイナー , パッチ ] の数値の1つを「代用」することができます。 また、番号の記載が無い場合は、 メジャー > マイナー > パッチ の順で数値があてはめられ、不足分は * で補完されます。 ・ 1 , 1.x , 1.*.* : すべて >=1.0.0 <2.0.0 と同義 GROWI.cloudでの実用例 実用例1:選択可能なバージョンの制限 GROWI では、 v4 系から v5 系バージョンへのアップグレードで、バージョン更新が不可逆となる変更がが入りました。 その際に、現在設定されているバージョンと変更先のバージョンをチェックしてバージョンの変更可否を判断する機構が実装されています。 import semver from 'semver'; /** * バージョン変更の可否をチェックし、バージョン変更ができない場合はその旨を説明するメッセージを配列で返す * * @param {string} fromGrowiVersion 変更前の現在のバージョン * @param {string} toGrowiVersion 選択中の変更後のバージョン * @param {Array<GrowiVersionChangeRestrictionConfig>} configs * @returns {Array<string>} バージョン変更の拒否メッセージ(リスト) */ const getVersionChangeRestrictedMessages = (fromGrowiVersion, toGrowiVersion, configs) => { return configs.filter((config) => { // RC 版含め変更前の GROWI のバージョンが sourceVersionRange に合致するか return semver.satisfies(fromGrowiVersion, config.sourceVersionRange, { includePrerelease: true }) // RC 版含め変更先の GROWI のバージョンが targetVersionRange に合致するか && semver.satisfies(toGrowiVersion, config.targetVersionRange, { includePrerelease: true }); }).map((config) => { return config.message }); }; 実用例2:バージョンの自動アップグレード機能 バージョンの自動アップグレード機能のバッチ処理で、最新のバージョンがリリースされたときに releasetAt 基準の最新バージョンではなく、 SemVer の概念における「最新のバージョン」が特定できるよう実装しています。 GrowiAppVersion テーブル のレコード例 (リリース日の降順) version releasedAt isStable isVisible 4.5.18 2022-04-15 08:25:16 0 1 5.0.1 2022-04-15 08:44:08 0 1 4.5.17 2022-04-07 10:06:15 0 1 4.5.16 2022-04-06 09:46:40 1 1 5.0.0 2022-04-01 16:08:24 0 1 5.0.0-RC.14 2022-03-30 13:24:37 0 0 実装 import semver from 'semver'; import { GrowiAppVersion } = from 'models'; /** * アップデート可能なバージョンのリストを返す * Semantic Versioning によって、バージョンの降順にソート * * @returns {Promise<Array<GrowiAppVersion>} semver の降順にソート */ async getAvailableGrowiAppVersions() { const growiAppVersions = await GrowiAppVersion.findAll({ where: { isVisible: true } }); return growiAppVersions.sort((v1, v2) => { return semver.rcompare(v1.version, v2.version, { includePrerelease: true }); }); } /** * 「常に最新のバージョンを利用する」 GrowiApp 全てのバージョンを更新する */ async updateGrowiAppsVersionBatch() { // 利用可能なGrowiAppVersionsを取得 const availableGrowiAppVersions = await getAvailableGrowiAppVersions(); // 安定版 の最新 version を取得 const latestStableVersionObj = availableGrowiAppVersions.find((val) => { return val.isStable }); // テスト版も含む 最新 version を取得 const latestVersionObj = availableGrowiAppVersions.find((val) => { return !isRcWithHashVersion(val.version); // -RC は含まない }); // 安定版 の最新 version console.log(latestStableVersionObj.version); // 4.5.16 // テスト版も含む 最新 version console.log(latestVersionObj.version); // 5.0.12 ... // バージョン更新処理 } semverで引っかかったポイント semver を使った実装を終えた今となっては理解できることですが、「 5.0.0-RC.1 というプレリリースのバージョンが 5.0.0 未満 のバージョンと判定される」という pre-release バージョンの扱いに当初戸惑いました。 なぜなら、 GROWI v5 から v4 へのバージョンダウングレードを拒否したい場合に、変更前のバージョンが (プレリリースを含む) v5 系かつ、変更後のバージョンが v4 系であることを判定するために以下の判定を行っても v5 RC バージョンを v5 系バージョンと判別することができず、v4系バージョンとして扱われているような挙動に見えたためです。 変更前バージョン判定: semver.satisfies('5.0.0-RC.0', '>=5'); // false ← `true` と判定したい 変更先バージョン判定: semver.satisfies('5.0.0-RC.0', '<5'); // true ← `false` と判定したい 最終的には、下記の判定とすることで、想定した挙動を得ることができました。 変更前バージョン判定: semver.satisfies('5.0.0-RC.0', '>=5.0.0-RC.0', { includePrerelease: true }); // true ← `true` を想定 変更先バージョン判定: semver.satisfies('5.0.0-RC.0', '<5.0.0-RC.0', { includePrerelease: true }); // false ← `false` を想定 最後に semver の仕様を理解しきれておらずに悩まされた部分もありましたが実装を経て理解できた部分があったので、似たような問題を抱えている方がいた際の助けになれば幸いです。 最後までご拝読いただきありがとうございました。 参考 今回の記事は、こちらのリンク先を参考に執筆しました。 https://www.npmjs.com/package/semver https://semver.org/lang/ja/ https://devhints.io/semver https://github.com/rstacruz/cheatsheets/blob/master/semver.md
アバター
はじめに こんにちは。 takayuki です。 今回は、 Google Chrome の検索エンジンの設定をして、素早く Google 英語版検索を行えるようにしたいと思います。 今回の記事は、 Google Chrome のみが対象です。 Firefox を始めとするその他のブラウザでは検証していません。執筆時に確認した Google Chrome のバージョンは、 102.0.5005.62 です。 日本語、英語の検索設定を頻繁に変更するのは煩わしい 技術的な調べ物を行うときに、情報量の多さを求めて Google 英語版で検索をしたくなることがあります。このようなとき、私は今まで Google の検索の設定を開き、言語を English に変更して検索を行っていました。 しかし、すぐにまた別のキーワードを日本語で検索したくなり、その都度検索の設定を変更するのに煩わしさを感じていました。 業務で検索はたくさん行いますので、検索環境を改善したいと思います。 まずはじめに、Google の検索の設定を英語にした状態で検索を行ったとき、どのようなリクエストが行われているか URL を見てみます。すると、下記のようになっていることが確認できます。 https://www.google.co.jp/search?q=[検索キーワード]&hl=en&ei=... hl=en と指定すると、英語版で検索が行えるようです。これは、 Google の検索の設定の言語が 日本語 に設定されている状態でも機能します。キーワードの検索時に、 hl=en クエリ文字列を簡単に付け外しできれば、 日本語と英語検索の切り替え が手軽に行えるようになりそうです。 英語版検索を素早く行う設定をする クエリ文字列の付け外しは、 Google Chrome の検索エンジンの設定を利用して実現できます。 Google Chrome 右上の ︙ をクリックし、設定を開きます。 検索エンジン > 検索エンジンとサイト内検索を管理する の順に開きます。 サイト内検索 右の 追加 をクリックします。 それぞれ下記のように入力します。 検索エンジン: Google US などわかりやすい名前を任意に入力します。 ショートカット: アドレスバーでこの検索の設定を呼び出す際のショートカットを入力します。 gle など短い文字列にすることをおすすめします。 URL: 検索結果ページのURLを入力します。 アドレスバーに入力したキーワードは %s に置き換えられます。下記のように入力します。 hl=en の他に、 gl=us が追加で付与されていますが、これを付けるとアメリカ国内で検索した状態にできます。 https://www.google.com/search?q=%s&gl=us&hl=en 設定ができたら、新しいタブを開きアドレスバーに gle と入力後、 Tab キーを押します。すると、 Google US を検索 と表示されるようになります。ここにキーワードを入力して検索をすると、英語の検索結果が表示されるようになります。 その他の活用方法 サイト内のページをアドレスバーから検索する Google Chrome の検索エンジンの設定を応用すると、アドレスバーから特定のサイトの検索結果ページを呼び出すこともできます。弊社で開発をしている GROWI を例に設定してみます。 GROWI は、検索を行うと下記のURLになります。 https://[GROWIのホスト名]/_search?q=[検索キーワード] q クエリ文字列に検索キーワードを渡せればよさそうです。 英語版検索を素早く行う設定をする で行ったように、下記の設定を追加します。 検索エンジン: GROWI ショートカット: grw URL: https://[GROWIのホスト名]/_search?q=%s 新しいタブを開き、アドレスバーに設定したショートカット grw を入力後 Tab キーを押し、続けて検索キーワードを入力します。 下記のように、直接 GROWI に検索が行えるようになりました。 特定のサイトを除外して検索できるようにする 検索をするときに、特定のサイトを検索結果から除外したいときがあると思います。これも Google Chrome の検索エンジンの設定 で実現できます。 Google で検索をする際に特定のサイトを除外するには、検索フィールドに -site:[除外したいサイトのホスト名] と指定します。 英語版検索を素早く行う設定をする で行ったように、下記の設定を追加します。 検索エンジン: Google ショートカット: googleopt など何でもよいです URL: https://www.google.co.jp/search?q=%s+-site%3A[除外したいサイトのホスト名] 除外したいサイトが複数ある場合は、その分 +-site%3A[除外したいサイトのホスト名] を追加します。 追加をしたら、右側の ︙ を押し、 デフォルトに設定 を選択します。 新しいタブを開き、アドレスバーに検索キーワードを入力し、検索を行います。すると、指定したサイトが除外された検索結果が表示されるようになります。 おわりに Google Chrome の検索エンジンの設定を利用して、検索を素早く行えるようになりました。 今回紹介した以外のサイトでも、特定のクエリ文字列を渡してリクエストするページであれば、この機能は利用できるので是非試してみてください。 参考 既定の検索エンジンを設定する
アバター
こんにちは、システムエンジニアの kouki です WESEEK では Rails と webpacker を使ったプロジェクトを多数担当しています。今回はその中で起こったトラブルと対処法についてご紹介します 最近では webpacker が公式に引退宣言 を表明したり Rails 7 になり、 jsbundling-rails , cssbundling-rails が台頭してきたり と assets 関連が盛り上がっていますが、まだ webpacker が現役なため、今回は webpacker にフォーカスを当てています (esbuild のビルドが速いと聞いて興味は沸いているので機会があればまた別の記事にて) おさらい: webpacker で minify を効かせる方法 Rails + webpacker を利用しているアプリにて production で assets (JS, CSS etc) を提供する際には下記のようなコマンドを実行します $ RAILS_ENV=production bin/rails assets:precompile これは webpacker が assets:precompile rake タスクに特定のタスクを挟み込むことで production build を実現しています また、 RAILS_ENV=production を指定するだけで、よしなに minify を働かせてくれるのは webpacker が下記のようなコードを提供してくれているからです TerserPlugin と CompressionPlugin を使って minify をしている個所 ( environments/production.js ) https://github.com/rails/webpacker/blob/5-x-stable/package/environments/production.js 利用しているプロジェクトの環境 (ハマった環境) では、今回トラブルに遭遇した環境を紹介します Ruby on Rails Rails '~> 6.0.3', '>= 6.0.3.2' webpacker ~5.4 webpacker (もとい JavaScript 側で利用しているライブラリ) React TypeScript (ポイント) devDependencies に typescript や @types/* を追加している production build をしてみたが ... エラー発生 ここから遭遇したエラーを紹介します 「さて、 assets:precompile を production でビルドするぞ」と意気込んで、下記コマンドを実行しました (実際は Dockerfile 内に記載しています) $ RAILS_ENV=production NODE_ENV=production bundle exec rake assets:precompile そうすると下記のようなエラーになってしまいました TS2580: Cannot find name 'require'. Do you need to install type definitions for node? Try `npm i @types/node`. (略) TS7016: Could not find a declaration file for module ... @types は devDependencies に追加済みのはずなのにおかしいですね 調査フェーズ 上記エラーに対処するために色々と調査をしたところ、下記のような状況になっていることが分かりました assets:precompile 実行後、 node_modules 配下を見ると @types npm パッケージが追加されていない それ以外にも devDependencies に記載しているパッケージがインストールされていない どうやら NODE_ENV=production の指定が入っていることにより、 dependencies のみ npm パッケージが install されている 次にそれぞれ (rails, webpacker) のコードを見て挙動を確認していきました webpacker の yarn install を実行するタスクは rails/rails の yarn:install を enhance している https://github.com/rails/webpacker/blob/v4.3.0/lib/tasks/webpacker/yarn_install.rake どうやら rails の中に含まれる yarn:install タスクは RAILS_ENV=production を指定すると NODE_ENV=production が指定されることになっている https://github.com/rails/rails/blob/v6.1.4.1/railties/lib/rails/tasks/yarn.rake それ以外にも GitHub の issue で下記のようなコメントを見つけました https://github.com/rails/webpacker/issues/117#issuecomment-282798638 Yes let's fix the root issue and move away from errant use of devDependencies. dhh (28 Feb 2017) 2017 時点でのコメントなので現在では意見が異なると思いますが、「devDependencies の誤った利用 (errant use of devDependencies.)」 と言われてしまうと困ってしまいますね 解決案候補 & 採用した解決方法 ある程度情報が揃ったので解決案の候補を出していきます ここでのやりたいことは 「yarn install 時には devDependencies のパッケージもインストールして、ビルド時にはNODE_ENV=production でビルドできること」 ということにしています ボツ案も含め、一旦はザッと思い浮かんだものをリストアップし、最終的に 5. の案にすることにしました それぞれの案の却下理由は下記の通りです devDependencies に記載しているパッケージを dependencies に移す 先述した GitHub issue にて提示されている方法 フロントエンド側からすると直感的ではない 各 npm パッケージのサイトでも devDependencies に追加する例示もあって、プロジェクトメンバーにそれを意識させたくない NODE_ENV=development で assets:precompile を実行 minify が効かない minify を効かせることもできるが手間がかかるし、トリッキー assets:precompile タスクを実行せず、rake タスクを分割して実行 こちらもトリッキーすぎる 「このプロジェクトでは assets:precompile は実行しないようにしてください」という注意書きにしたくない webpacker をアップデートしてみて、 assets:precompile を実行してみる integrity チェックが無くなった、という話もあるのでこれは有効な案 調査に時間がかかりそうだったため、今回は見送り [採用] yarn:install task を上書きする 最終的に案5を採用することにしました 自然な形で導入できる、かつ上書きをするタスクが局所的である、という点が採用ポイントです 下記のような rake タスクをプロジェクトの lib/tasks/yarn.rake というファイルパスで追加しました # assets:precompile 時に yarn install を NODE_ENV=development で実行させるモンキーパッチ # # refs: https://github.com/rails/webpacker/issues/405#issuecomment-750384173 if ENV['FORCE_YARN_INSTALL_IN_DEVELOPMENT'] Rails.application.config.after_initialize do Rake::Task['yarn:install'].clear # rails/rails v6.0.4.1 から引用 # https://github.com/rails/rails/blob/v6.0.4.1/railties/lib/rails/tasks/yarn.rake namespace :yarn do desc "Install all JavaScript dependencies as specified via Yarn" task :install do # 強制的に NODE_ENV=development で yarn install を実行させる node_env = 'development' system({ "NODE_ENV" => node_env }, "#{Rails.root}/bin/yarn install --no-progress --frozen-lockfile") end end # Run Yarn prior to Sprockets assets precompilation, so dependencies are available for use. if Rake::Task.task_defined?("assets:precompile") Rake::Task["assets:precompile"].enhance [ "yarn:install" ] end end end そして実行するコマンドは下記のようになりました $ FORCE_YARN_INSTALL_IN_DEVELOPMENT=true NODE_ENV=production bundle exec rake assets:precompile これであれば他の人が見たときに「 FORCE_YARN_INSTALL_IN_DEVELOPMENT って何?」ってなった時にコードを検索することで辿ることができます オプションを指定しない場合は先述したとおりエラーになるのですが、それは標準的な動きからあまり挙動を変えたくなかったためです まとめ 今回は「webpacker で TypeScript を使った時にハマった話」からの各種調査についてご紹介させていただきました 「あなたの webpacker、minify 効いてますか?」という文言はただ単に「皆さん、deploy されている環境で assets が minify されているか確認してますよね?」というお話でした こういった記事でも参考になる方がいらっしゃれば幸いです
アバター
はじめに こんにちは WESEEK でわりと何でもやっている haruhikonyan です。 今やデファクトとなりつつある kubernetes(以下k8s) ですが読者の皆様は k8s のオペレーションをする際のコンテキスト切り替えはにはどういったものを使っていますでしょうか。 以下のようなものがあると思います。 デフォルトの kubectl config use-context 割と使っている人が多そうな kubectx そして今回紹介する kubie を入れて kubie ctx すでにこれを使ってる人にはこの記事は必要ないかも もちろん他にも選択肢はあるかと思います。 コンテキスト切り替えと言えば、本番とステージングやテスト、開発でコンテキストを分けて運用をしているシステムは多くあるのかなと思います。 その中でステージングとかの設定を変更する際に、現状の本番の設定を確認するためにコンテキストを一時的に切り替えて設定を確認した後、またステージングにコンテキストを戻して作業を続けるという経験あるのではないでしょうか。 コンテキスト切り替えで事故が起きた ある日とある k8s エンジニア(自分ではない)がステージング環境で時間のかかる処理を実行していた。待ち時間に別コンソールで本番の設定を確認するために本番にコンテキストを切り替えたところ、ステージングへ実行中のコマンドのコンテキストが本番に向いてしまい、本番に対してコマンドを実行してしまった。。。 皆様もこんな感じに実際にやらかしてしまったり、ヒヤリハットが起きたことあるんじゃないでしょうか。 そんな一歩間違えれば取り返しのつかないことになりうるミスを少しでも防止できるかもしれない kubie ctx というコンテキスト切り替えコマンドの紹介です。 kubie ctx のうれしいところ kubie ctx はシェルの環境変数を使って切り替えるので別コンソールでコンテキストを変更した場合別のコンソールには伝搬しない 別ターミナルでコンテキストを本番に変更してデータ見たりしてももともと作業してるターミナルには影響がない コマンドを実行するターミナルさえ間違わなければ予期せず別コンテキストに対してコマンドを実行することは無い kubectl config use-context や kubectx は .config 内に値を直接書き込み変更するので別ターミナルを開いて作業をしていた場合でも切り替えが伝搬してしまう kubie ctx を実行したコンテキスト以外の設定が見えない状態になるので、 kubeclt --context や helmfile の context を指定してある状態など、うっかり別コンテキストへのコマンドを実行してしまったとしても安心 この状態で kubectx を実行した場合は kubie ctx で切り替えたコンテキストしか表示がされないので上からコンテキストの変更が不可能 kubie ctx のインストール方法 公式 に書かれている通りにインストールを行えば何ら難しくなくコマンドを利用できるようになります 終わりに やらかすとまじやばいことになりうるので kubie ctx 使ってリスクを減らそう!
アバター
はじめに 今回は、ブックマークレットを使って業務効率が少し上がったことについて話してみたいと思います。 ブックマークレットとは wikipedia では以下のように説明されています。 ブックマークレット (Bookmarklet) とは、ユーザーがウェブブラウザのブックマークなどから起動し、なんらかの処理を行う簡易的なプログラムのことである[注釈 1]。携帯電話のウェブブラウザで足りない機能を補ったり、ウェブアプリケーションの処理を起動する為に使われることが多い。 wikipedia - ブックマークレット 簡単に説明すると、ブラウザで開いているページに対して、任意の Javascript を実行できる機能です。ブックマークレットと似ているものとして、ブラウザの拡張機能があります。「hogehoge してくれる拡張機能が欲しいけどストアには存在しない。でも自分で拡張機能を作成するほどのものではない」という方には、ブックマークレットを作るのがおすすめかもしれません。 ブックマークレットでどんなことができるの? 例えば、1年間に Amazon で買った合計金額を出力してくれるブックマークレット( koyopro/amazon-calc.min.js )があります。これを使うとこんな感じになります。 ブックマークレットを使って業務効率をあげる ブックマークレットの開発から、登録、利用方法をご紹介します。 弊社では毎日、業務の終わりにスクラムミーティグというものを行います。これは、今日までに行ったタスクの進捗状況の報告と、次回のスクラムミーティングまでに行うタスクの予定を宣言するもので、スクラムシート(Google スプレッドシート)を使って行います。 実際に自分が使っているスクラムシートはこんな感じです。1つのタスクにつき、Redmine のチケット番号 + チケット名をコピーして貼り付けています。(書き方はメンバーによってバラバラですが...) Redmine 側ではこうなっています。 93528 という番号と、 /admin/audit-log にアクセスできる というタイトルを一回一回コピーしてスクラムシートに転記するという作業が地味に面倒臭いと思っています。 今回は、1クリックするだけで 93528 /admin/audit-log にアクセスできる という文字列をクリップボードに保存するためだけの簡単なブックマークレットを作ってみようと思います。 ブックマークレットのコード紹介 以下のコードを Redmine のチケットの URL 上で実行すると、Redmine のチケット番号 + チケット名がクリップボードに保存されます。 javascript: ( async() => { // Redmine のチケットの URL const redmineUrl = location.href; // チケット番号の取得 const ticketNum = redmineUrl.match(/[+-]?\d+/g); // チケットのタイトルの取得 let tiketTitle = document?.getElementById('issue_subject')?.value; // チケット番号かタイトルが null だったら "Copy failed" というアラートを出す if (ticketNum == null || tiketTitle == null) { alert('Copy failed'); return; } // チケット番号 + チケットタイトル をアラートし、クリップボードにコピーする const text = `${ticketNum} ${tiketTitle}`; try { await navigator.clipboard.writeText(text); alert(`Copied:${text}`); } catch { alert('Copy failed'); } } )(); アドレスバーに javascript:(任意の処理) という風に書いたものを貼り付けてエンターを押すと、任意の処理を実行できます。 ブックマークレットの登録と実行 ブックマークレットは名前の通り、ブラウザのブックマーク上に保存し、それをクリックすることで Javascript が実行されます。この一連の流れをご紹介します。 コードを書く。(今回は上のコードを登録します) コードを圧縮する 右の URL に遷移 https://userjs.up.seesaa.net/js/bookmarklet.html script にコードを入力 script(space removed) に圧縮された Javascript が表示されるのでコピー 適当なページでブックマーク登録ボタンを押す その他を押す 名前 には任意の名前、 URL には先ほどコピーした圧縮された Javascript を貼り付けて保存 https:<ドメイン>/issues/<チケット番号> 上で保存したブックマークレットをクリック おわりに 以上、ブックマークレットで業務効率が少し上がったお話でした。
アバター
はじめに こんにちは、システムエンジニアのかおりです。この記事では、弊社が提供するナレッジ共有サービス「GROWI」の脆弱性対応について取り上げたいと思います。 脆弱性に関する基本的な用語や、脆弱性対応の流れについて興味ある方が対象読者となっています。 脆弱性とは 脆弱性(ぜいじゃくせい)とは、コンピュータのOSやソフトウェアにおいて、プログラムの不具合や設計上のミスが原因となって発生した情報セキュリティ上の欠陥のことを言います。 引用: https://www.soumu.go.jp/main_sosiki/joho_tsusin/security/basic/risk/11.html 英語では Vulnerability と呼ばれるので、覚えておくと良いでしょう。 脆弱性を狙った攻撃にゼロデイ攻撃というものがありますが、今回お話しする内容は、まだ公開されていない善意で報告を受けた脆弱性の対応の話です。 ゼロデイ攻撃について詳しくは以下の記事をお読みください。 ゼロデイ攻撃とは ソフトウェアに脆弱性が含まれていると、第三者による不正アクセスや情報の改ざん、重要な機密情報の漏洩などの問題が生じてしまう可能性があるので、発覚した場合はできるだけ早めの対応をする必要があります。 脆弱性対応の基本的な流れ 脆弱性対応の基本的な流れは以下のようになっています。 ① 脆弱性の発見・報告 ② 再現確認 ③ 修正 ④ リリース ⑤ 公表 脆弱性が発覚した場合、修正しリリース、エンドユーザーにシステムのアップデートを促します。 弊社は、脆弱性が発見/報告された際の流れとして以下の二つの組織と連携し、公表まで行っています。 JPCERT/CC Huntr JPCERT/CC との連携実績 初めに、JPCERT/CC との連携実績についてお話しします。 JPCERT/CC とは、Japan Computer Emergency Response Team Coodiination Center の 略で、セキュリティインシデントに関する情報収集や、原因分析、対処法の考察などを行っている組織のことです。 脆弱性の発覚時には、IPAという組織が脆弱性の報告を受け付け、分析し、JPCERT/CCに報告します。そしてJPCERT/CCが開発者に連絡し、公表まで調整する流れになっています。 このIPA とJPCERT/CCが連携して公表まで行うことを、「情報セキュリティパートナーシップ」と呼びます。 以下の画像がわかりやすいので、ご覧ください。 引用元: 情報セキュリティ早期警戒パートナーシップの紹介 Huntr との連携実績 2021年末からは、 Huntr という OSSの脆弱性情報を取り扱っている海外の組織との連携も開始しました。 Huntr とは何者 ?? Huntrとは、OSSのセキュリティを守るプラットフォームのことで、脆弱性の発見者と開発者を繋ぐ窓口の役割を担っています。 また、脆弱性の報告者・修正者にはHuntrから賞金が支払われるので、脆弱性報告者は賞金を稼ぎ、OSS開発者は自社製品を改善できるという WinWIn の関係になっています。 過去レポートの一例 ここで、実際に過去に報告された脆弱性レポートを見てみましょう。 これは、認証のない遠隔の攻撃者が認証を回避し、ユーザーのコメントを削除する可能性があるとの報告で、v4.4.8で修正されました。 https://weseek.co.jp/ja/news/2022/01/21/growi-authentication-bypass/ 流れ CVE 採番・情報公開を行い、それを元にJPCERT/CCと連携し、JVN公表を行います。 脆弱性の情報はどこで確認できる? GROWIの脆弱性情報は、弊社HPや、GROWI.cloudのNEWSに公開されていますので、よろしければご覧ください。 https://weseek.co.jp/ja/news/ https://growi.cloud/news 緊急性の高い情報は Twitter の WESEEK 公式アカウント にも投稿しているので、リアルタイムに情報を得たい方はぜひフォローしてみてください。 報告していただいた方に GROWI はユーザーの皆さんと一緒に作り上げていくOSSだと考えています。脆弱性に関しても利用者の方から報告をいただけることは大変ありがたく、これまでいくつもの脆弱性修正報告を行えたことにチーム一同感謝しております。 それに対し何かお礼をすることはできないかと話し合った結果、以下2つを実施しようということになりました。 特典1 GROWIの Staff Creditに名前を記載 既にGROWIを利用中の方でも知らない方がいるかもしれませんが、実はGROWIの機能の一つに隠しコマンドがあります。 「command + /」を押すと、ショートカットキーの一覧が表示され、その項目の一つにコナミコマンドがあります。 「コントリビューターを表示」に記載されているコマンドを打つと、スタッフクレジットが表示され、GROWIのコントリビューターが表示されるという機能です。 一緒に脆弱性をハントしていただいたささやかなお礼として、希望者にはその「VULNERABILITY HUNTER」の項目に、お名前もしくはニックネームを記載させていただきます。 コナミコマンドとは、コナミ(ゲームソフトを販売している会社)から発売された、多数のコンピュータゲームに登場する隠しコマンドの一種 引用元: コナミ Wiki コナミコマンドは、「command + /」で確認できます。 特典2 GROWI オリジナルグッズのプレゼント 2つ目の特典として、GROWI オリジナルグッズをプレゼントします。 現在(2022/4/15)対象のグッズは、木工ロゴです。弊社エンジニアの田村さん(@hoge)に作成していただきました。 サイズは5種類、木材は6種類の中からお選びいただけます。一部のサイズでは希望があればキーホルダーをおつけすることもできます。 脆弱性を発見したら 脆弱性を発見したときの注意点として、TwitterやSlackのパブリックチャンネル等の不特定多数の人が閲覧できる場で「これ脆弱性かな?」とつぶやいたりしないよう、くれぐれもご注意ください。 以上を踏まえて、下記のいづれかでご報告ください。 IPAに届出を出す Huntr でアカウントを作成し、レポートを提出する GROWI Slack Workspace にてGROWI開発者にDMをする まとめ 脆弱性(Vulnerability)とは、情報セキュリティ上の欠陥のこと GROWI では JPCERT/CC、Huntr.dev と連携して脆弱性の公表まで行っています もし、脆弱性を発見した場合は情報が漏れないよう気をつけましょう 脆弱性を発見してくださった方にはお礼をご用意しています もし何かありましたらお気軽にご連絡ください。今後ともGROWIをよろしくお願いいたします。
アバター
この投稿は、弊社が提供する WESEEK TECH通信 の一環です。 WESEEK TECH通信とは、WESEEKのエンジニアがキャッチアップした技術に関する情報を、techブログを通じて定期的に発信していくものです。 はじめに 今回の記事では React における Global state の管理法についてさまざまな方法を、それぞれのメリットデメリットとともに解説します。 React で大規模な開発を行う際に Global state の管理法が定まっていると非常に開発が楽になるため、是非ともこのいずれかの方法を理解しておきましょう。 Global stateの管理が必要な理由 React において、例えばコンポーネントの構成が5階層になっていた場合に、一番上の階層のコンポーネントで定義している state を一番下の階層のコンポーネントに渡したい場合はどうすれば良いでしょうか。 上から下までのそれぞれのコンポーネントで state を props として渡してあげることで、一番下の階層のコンポーネントまで渡すことができます。 しかし、この方法では state をバケツリレーのように複数のコンポーネントにまたがって渡しているため、コードが複雑化してしまいます。また、中間のコンポーネントにとっては本来必要のない props を受け取っているため、再利用の難しいコンポーネントになってしまう可能性があります。 その他にも、中間に位置するコンポーネント達は state が更新されるたびに、再レンダリングの必要がないにもかかわらず再レンダリングされてしまうという問題があります。 これらの問題を解決し、無駄なコードや無駄な挙動を減らすために Global state が必要になるということです。 1. React Context 一番初めに紹介するのは、React の標準機能として備わっている useContext を使用した方法です。 この方法は React さえ導入されていれば使用できるため、追加で他のパッケージをインストールする必要がないことがメリットです。 まず初めに、ページ全体のテーマカラーを Global state として扱うことを想定して、一番上の Top コンポーネントで定義されたテーマカラーを一番下の Bottom コンポーネントに表示させるという例を用意します。 import { useState } from 'react' const Top = () => { const [themeColor] = useState('dark') return ( <Middle color={themeColor} /> ) } const Middle = (props) => { return ( <Bottom color={props.color} /> ) } const Bottom = (props) => { return ( <div>テーマカラーは {props.color} です。</div> ) } 中間の Middle コンポーネントでは、次の Bottom コンポーネントに props を渡すためだけに props を受け取っていることが分かります。 (この程度であれば全然問題はないですが) もし state の渡し先がずっと下層のコンポーネントであった場合は複雑なコードとなってしまいます。 この例を元に useContext を使用して、Top コンポーネントから Bottom コンポーネントに直接 state を渡せるように改善します。 React の context 機能を使用した Global state 管理の手順は非常にシンプルで、以下の4ステップで実施できます。 React.createContext で Context オブジェクトを作成する Context.Provider の value に対して state を渡す state の渡し先のコンポーネントを Context.Provider で囲う 渡し先のコンポーネントで React.useContext を使用して state を呼び出す 例を見ていきましょう。 import { useState, createContext, useContext } from 'react' const SampleContext = createContext('light') const Top = () => { const [themeColor] = useState('dark') return ( <SampleContext.Provider value={themeColor}> <Middle /> </SampleContext.Provider> ) } const Middle = () => { return ( <Bottom /> ) } const Bottom = () => { const color = useContext(SampleContext) return ( <div>テーマカラーは {color} です。</div> ) } まず、createContext で Context オブジェクトを作成します。 createContext の引数には Global state の初期値を設定できます。今回は light とします。 const SampleContext = createContext('light') 次に、Context.Provider の value に、Global state として管理する state を渡します。 そして、Context.Provider で Middle コンポーネントを囲います。 こうすることにより、Middle コンポーネントとその配下のコンポーネントで、useContext の使用が可能になります。 const Top = () => { const [themeColor] = useState('dark') return ( <SampleContext.Provider value={themeColor}> <Middle /> </SampleContext.Provider> ) } 最後に、createContext で作成した Context を useContext の引数に渡してあげることで Global state を呼び出すことができます。 const Middle = () => { return ( <Bottom /> ) } const Bottom = () => { const color = useContext(SampleContext) return ( <div>テーマカラーは {color} です。</div> ) } これにより、中間コンポーネントがいくつ存在していようと、直接最下層のコンポーネントで state を扱えるようになりました。 SampleContext.Provider を使用しているコンポーネント以外の箇所で Global state を更新したい場合は、useState を使用し state とそれを更新するための関数の両方を Context.Provider の value に渡してあげることで更新が可能となります。 以下の例では useState を使用したカスタムフックを作成しています。 import { useState } from 'react' const useThemeColor = () => { const [themeColor, setThemeColor] = useState('light') return { themeColor, setThemeColor } } export default useThemeColor import { createContext, useContext } from 'react' import useThemeColor from './hooks/useThemeColor' const SampleContext = createContext() const Top = () => { return ( <SampleContext.Provider value={useThemeColor()}> <Middle /> </SampleContext.Provider> ) } const Middle = () => { return ( <Bottom /> ) } const Bottom = () => { const context = useContext(SampleContext) return ( // ボタンを押した時に context.themeColor を 'dark' に更新する <button onClick={() => context.setThemeColor('dark')}>テーマカラー更新</button> ) } Global state が更新されるたびに、useContext でその Context を参照している全てのコンポーネントが再レンダリングされるため、1つの Context で全ての Global state を管理するのではなく、必要に応じて複数の Context を用意すると良いでしょう。 その他、React Context については React の公式ドキュメントに詳しく書かれているため、気になる方は是非ご覧ください。 コンテキストのガイド この React Context を使った Global state の管理は、今回紹介する方法の中で比較的お手軽に使うことができるので、小規模なプロジェクトではこの方法を用いるのが良いのではないかと思われます。 2. Redux Redux とは、React が扱う state の管理に特化したライブラリであり、現時点 (2022 年 4 月) における React の state 管理のデファクトスタンダードに近いポジションを取っています。 Redux - npm を見ると、Weekly Downloads が 2022 年 4 月の時点で約 780 万と非常に多く、全世界で多くの開発者に利用されていることが分かります。 Redux は特に大規模なプロジェクトに適していると言われています。Redux は Facebook が生み出した Flux というアーキテクチャに則って設計されていることが特徴です。 以下、公式ドキュメントより引用 Redux では、Store と呼ばれる場所に全ての State が保存されます。Action を Dispatch (送信) することで State を定義したり更新したりします。 Store の中の Reducer で State の更新を行います。Reducer で Dispatch された Action を受け取り、新しい State を返却します。 全体の流れを説明すると、 ActionCreator が Action を作成する Store に対して Action が Dispatch(送信)される Dispatch された Action が Reducer に渡される Reducer が新しい State を生成する Store から新しい State を呼び出して画面上に表示する となります。 Redux を導入することで、複雑な state の管理が可能になりますが、一方で学習コストが高いというデメリットが存在します。 そのため大規模なプロジェクトでの採用が適していると考えられます。 3. Recoil Recoil とは、Facebook 社が開発し 2020 年に公開された、非常に新しい state 管理のライブラリです。 Recoil を利用した Global state の管理は実装のハードルがとても低く、React Hooks の useState のような感覚で使用できる点が大きな特徴です。 Recoil は Redux と同様に、Facebook が生み出した Flux というアーキテクチャに則って設計されており、Atom や Selector と呼ばれる単位を使用してデータを管理します。 今回もページ全体のテーマカラーを Global state として扱うことを想定して、実際に Recoil を触ってみます。 import { RecoilRoot, atom, useRecoilState } from 'recoil' const themeColorState = atom({ key: 'themeColorState', default: 'light' }) const Top = () => { return ( <RecoilRoot> <Middle /> </RecoilRoot> ) } const Middle = () => { return ( <Bottom /> ) } const Bottom = () => { const [themeColor, setThemeColor] = useRecoilState(themeColorState) return ( <div>テーマカラーは {themeColor} です。</div> ) } atom は状態の一部を表しており、任意のコンポーネントから読み取りや書き込みを行うことができます。 key には他の atom と被らないような unique ID を付け、default に state の初期値を入れます。 const themeColorState = atom({ key: 'themeColorState', default: 'light' }) Recoil を使用するコンポーネントを RecoilRoot で囲みます。これにより、以下の例では Middle コンポーネントとその配下のコンポーネントで state を呼び出したり、更新したりできるようになります。 const Top = () => { return ( <RecoilRoot> <Middle /> </RecoilRoot> ) } 最後に、コンポーネント内で useRecoilState を使うことで、state の呼び出しと更新ができます。 useRecoilState から返される配列の1つ目の要素は state の現在の値であり、2 つ目の要素はそれを更新するための関数です。 const Middle = () => { return ( <Bottom /> ) } const Bottom = () => { const [themeColor, setThemeColor] = useRecoilState(themeColorState) return ( <div>テーマカラーは {themeColor} です。</div> ) } Recoil で一番多く使うことになるであろう useRecoilState は、React Hooks の useState とほとんど同じような使い方ができるため、既に React Hooks を使いこなせている人は非常に低い学習コストで Recoil を扱うことができます。 Recoil はまだ公開されて間もなく、現時点 (2022 年 4 月) ではまだ実験的な段階ですが、今後 state 管理ライブラリの主流になるとも言われています。 Recoil を使用することにより簡単に Global state を管理できるようになるため、プロジェクトの規模によらず非常におすすめです。 4. SWR SWR は Next.js を開発している Vercel が開発した、データ取得のための React Hooks ライブラリです。 SWR という名前は stale-while-revalidate というキャッシュ戦略から来ています。 基本的な使い方としては、useSWR という React Hooks を用いることで、API を通じたデータの取得・キャッシュを簡単に記述する手助けをしてくれます。 import useSWR from 'swr' const fetcher = () => fetch('/api/user') const Profile = () => { const { data, error } = useSWR('/api/user', fetcher) if (error) return <div>failed to load</div> if (!data) return <div>loading...</div> return <div>hello {data.name}!</div> } useSWR フックは key 文字列と fetcher 関数を受け取ります。 key はデータの一意な識別子で、fetcher に引数として渡されます。 fetcher はデータを返す任意の非同期関数で、fetch や Axios のようなツールを使うことができます。 通常はこのように、API を通じてデータを取得するために使用します。 しかし第二引数の fetcher を渡す箇所に null を渡すことにより、useSWR で Global state の管理ができるようになります。 const { data: themeColor, mutate: setThemeColor } = useSWR('themeColor', null, { initialData: 'light' }); useSWR から返ってくる data はデータの現在の値であり、mutate 関数を使用することでデータを更新できるため、React Hooks の useState のような使い方ができます。 そのため、こちらも低い学習コストで扱うことができます。 また、useSWR の第三引数には様々な option を指定でき、initialData では data の初期値を指定できます。 既にプロジェクト内でデータの取得用として SWR を導入している場合に、Global state 管理のために SWR を使用するというのが良いと思います。 まとめ React における Global state の管理法を4つご紹介しました。 個人的には最も簡単に扱うことのできる Recoil をおすすめしますが、ライブラリを導入せずに使用できる React Context や、複雑な state 管理が可能となる Redux など、自身のプロジェクトに合った Global state の管理法を導入しましょう。
アバター
はじめに こんにちは。普段の業務では Ruby を書いている takayuki です。今回は、久しぶりに電子工作・ IoT ネタです。 昨年、リモートワーク下でもコミュニケーションを円滑にとれるようにすることを目的に、趣味の電子工作を活かして多機能カメラ「 neibo 」を製作しました。 詳しくはこちらの記事で紹介しています。 Raspberry Piでコミュニケーションシンクロ率を上げる Raspberry Pi と Arduino で作る360度回転リモートワークカメラ(1) neibo を作ってみて、課題がいくつか見つかりました。それらの課題を解決すべく、 neibo 改良機を作ることにしました。 これから複数回にわたって neibo 改良機の製作記録について書いていきたいと思います。今回の記事では、 neibo 製作の経緯、改良機の設計と製造の一部について説明します。 neibo は、以前は neighbo という名前でしたが、書きづらかったので変更しました。 neibo のほうが親しみやすさもありますし。 neibo 改良機製作の経緯 neibo 初号機を製作して見えてきた課題は下記です。 市販品に比べてコストが高い 映像配信を行う際に CPU 負荷が高い ソフトウェアの安定性・拡張性が低い 手軽に利用できない 大きい 外観が美しくない 課題がたくさん見つかりました。 これらの課題を解決するために、新しい neibo を製作することに決めました。 製作の方針 まず、 neibo 改良機を製作する方針として、先行して neibo 研究開発機を作り、そこで得た知見をもとに neibo 量産機を作ろうと考えています。これまで、 neibo 初号機は 1 台しかありませんでしたが、 neibo 改良機では複数台が協調して動作する仕組みを考えています。詳しい構想・構成は、今後の記事に書いていきたいと思います。 今回からしばらくの記事では、 neibo 研究開発機を作っていく過程を紹介していきたいと思います。 neibo 研究開発機では、新しいハードウェアを試し、検討するため、一旦課題にあげていた「市販品に比べてコストが高い」は考えないこととします。 neibo 初号機では、 Raspberry Pi 4 Model B と Arduino Uno を用いていましたが、 neibo 改良機では Jetson Nano B01 を使ってみることにしました。 Jetson Nano を選択した理由は Raspberry Pi に比べて、高性能な GPU を搭載しているためです。 後述する「映像プロトコルの調査」に関連しますが、 neibo 初号機は Raspberry Pi で構築していましたが、もっとも処理能力を必要とするのは映像配信部分でした。 Jetson Nano が Raspberry Pi と比べて、映像処理にどれくらい差があるのか、試していきたいと思います。 Jetson Nano Raspberry Pi 4 Model B CPU Quad-core ARM Cortex-A57 MPCore processor Broadcom BCM2711, Quad core Cortex-A72 (ARM v8) 64-bit SoC @ 1.5GHz GPU NVIDIA Maxwell architecture with 128 NVIDIA CUDA® cores VideoCore IV Memory 2 GB / 4 GB 1 GB / 2 GB / 4 GB / 8 GB Video Encode H.265 / H.264 H.265 / H.264 Video Decode H.265 / H.264 H.265 / H.264 映像プロトコルの調査 neibo 初号機では、映像に関連する課題は大きく2つあります。 1つ目は、 CPU 負荷です。 Jetson Nano 、 Raspberry Pi は、どちらもハードウェアエンコード/デコードに対応しています。 neibo 初号機では、ブラウザから Google Meet を使用して映像の配信を行っていました。この方法ではハードウェアエンコード/デコードを利用できず、 CPU が非常に高負荷になってしまうのが課題でした。また、 neibo 初号機は、カメラからの映像の受像、 Google Meet での配信、モーターの制御のほとんどを Raspberry Pi の CPU で処理していたため、処理が追いつかずに画質が低下してしまうことも課題でした。 2つ目は、遅延です。 Google Meet は映像を配信するまでに1秒弱の遅延が発生します。 neibo は遠隔地にいる人がカメラを操作して状況を確認できるようにする機能を提供したいと考えているため、遅延は小さくしたいところです。 今回、映像プロトコルを決定するために下記を目標として調査を行っていきたいと思います。 CPU への負荷が低いこと 低遅延であること(0.1秒以内目標) CPU への負荷を減らすために、 neibo 改良機では映像処理を極力行わないようにします。カメラで受像した映像は、ハードウェアエンコード後に高性能な別のマシンに転送し、その別のマシンから Google Meet などで配信を行うことを考えています。 neibo 改良機から高性能な別のマシンへ転送するのに最適な映像配信プロトコルについて調査をしていきます。まずは、下記の技術要素を比較検討することにしました。 RTSP NDI WebRTC RTSP RTSP(Real Time Streaming Protocol) は、映像や音声などのリアルタイムデータを制御しオンデマンド配信を可能にするためのプロトコルです。 RTSP は再生、停止、録画など制御を扱うのみであり、映像や音声データの配信には一般的に RTP(Real-time Transport Protocol) を使用します。 細かな検証手順は今回は割愛しますが、試しに GStreamer で Mac の Web カメラの映像を RTSP で配信してみたところ、約 2.5 秒の遅延が発生する結果となりました。 NDI NDI(Network Device Interface) は、 NewTek 社によって開発、提供されている IP を利用した映像プロトコルです。こちらも対応している Web カメラが一部存在しています。 LAN で高品質に低遅延に映像を伝送することを目的としており、 1080p 60fps で 180Mbps と非常に多くの帯域を使用します。 NDI の SDK をダウンロードし、 Jetson Nano から配信を試してみました。正確に遅延を計測したわけではありませんが、体感上は遅延をほとんど感じることはなく、 0.1 秒以内の目標を達成できそうです。しかし、 NDI はソフトウェアでエンコード/デコードを行うためか CPU 使用率は高くなり、使用率が 100% に達すると、途端に映像の遅延が発生するようになりました。 WebRTC WebRTC(Web Real-Time Communication) はウェブアプリケーションやウェブサイトにて、仲介を必要とせずにブラウザー間で直接、データやオーディオ/ビデオストリームの送受信を可能にする技術です。 調べると、 Raspberry Pi や Jetson Nano のハードウェアエンコード/デコードを使用して、 WebRTC で低遅延で配信を実現している例があるので、期待できそうです。しかし、 WebRTC は一般的な利用は簡単ですが、応用的な使い方をしようとすると、仕様が複雑なため、途端に難易度が上がります。 今回の neibo 製作でも WebRTC の調査はまだあまり進められていません。今後の製作記録で紹介していきたいと思います。 スリップリングの製作の試行錯誤 映像プロトコルの調査の他にも、 neibo 初号機の課題を改善するために調査することがいくつかあります。その1つがスリップリングの改善です。 スリップリングとは、回転体に電力・信号を伝達できる回転コネクタのことです。この部品を使うと、何回転してもコードが絡まらなくなります。 neibo 初号機では、ツバメ無線製の SRG-2-14GC というスリップリングを採用しました。14 本線があり、USB信号、ステッピングモーターの電力を伝達させました。ツバメ無線製 SRG-2-14GC は、1万円弱と意外と高い部品でした。これだけに限らずスリップリングは値段が高めな傾向にあります。 高ければ、作ってしまえば安く抑えられるのではという安易な発想を思いつきました。 スリップリング製作1 早速設計したスリップリングがこちらです。だいぶ大がかりな装置になりました。 neibo 初号機でも使用した Fusion 360 を使用して設計しました。 neibo 初号機では主に MDF 板をレーザーカッターで切り出して組み立てていましたが、 neibo 改良機では 3D プリンタでの造形を考えています。外観の美しさを達成するためです。 部品の1つがこちらです。2つの金属リングの上に合計6つのこの部品が回転し、下部から受けた電力を上部ユニットに伝達します。金属リングに接する部分にはボールベアリングを配置しています。 組み立てて回転させてみたものがこちらの動画です。リンクをクリックしてご覧ください。 https://twitter.com/i/status/1477660689875759105 テスターで電圧を測定してみると、回転時に著しく電圧が低下しました。ボールベアリングは構造上接地面が小さくなるように作られているので、電力を伝達しようとすると安定しないようです。また、電蝕が起きやすく、ベアリングの寿命を縮めてしまうようです。このアイデアは失敗に終わりました。 スリップリング製作2 スリップリングのベアリングの代わりに、純粋な金属のリングをつけてみることにしました。 また、スリップリング製作1では幅をとってしまったので、少し改良しました。 金属のリングは旋盤で丸棒から削り出します。真鍮と銅の2種類を切削しました。 組み立てた部品の1つがこちらです。 さて、結果はどうだったのかというと、まだ完成させて通電試験は行っていません。ですが、数時間かけて旋盤加工をしたり、そもそも旋盤を導入する時点で既製品より高くついているため、このアイデアも失敗だったと言えます。 改良型の neibo では、電力(2極)のみを伝達させたいため、もっと安価な既製品のスリップリングを素直に採用したいと思います。 さいごに 長くなってしまうので、今回はここまでにしたいと思います。 次回は、CADで設計した neibo 全体の構造についての紹介予定です。
アバター
はじめに はじめまして、WESEEK にてエンジニアをしている、藤澤です。 この記事では、個人的に推している VS Code でターミナルを扱う際の Tips のようなものを紹介させていただきます。 VS Code のターミナルを使う利点として コーディングとオペレーションで同じウィンドウ、同じショートカットキーで作業できる GUI、CUI の良いとこ取りができる などがあると思っています。 そんな VS Code のターミナルを便利に使う方法の中でも、今回は code コマンド, devcontainer コマンドに関して紹介したいと思います。 codeコマンドとは ターミナルから VS Code でファイルを開いたり、diff を見れるコマンドです。 VS Code でコマンドパレット(F1 を押すか Windows であれば ctrl + shift + P、Mac であれば command + shift + p)を開き、 Shell Command: Install 'code' command in PATH を実行することでインストールできます。 使い方 基本的な使い方はターミナルを開き code file名 と入力することで VS Code でファイルを開けます。 以下ではファイルを開く以外に自分がよく使う code コマンドの使い方を紹介します。 ファイルのdiffを見る code -d hoge.txt fuga.txt と実行することでファイルの比較ができます。 ファイルを行数指定で開く code -g hoge.txt:150 のように -g をつけて最後にコロンで行数を指定してファイルを開けます。 標準出力をパイプでVS Codeに表示する 今回特に紹介したいのがこちらでコマンドの実行結果などをパイプによって VS Code へ表示できます。 以下のようにコマンドの標準出力をパイプで code - にわたすことで表示できます。 ls | code - このように VS Code によるシンタックスハイライトが効くので json や yaml などが見やすくなります。 curl の実行結果を受け取ったり、コマンドの出力を受け取って VS Code で検索、整形したりと応用が効くと思います。 devcontainerコマンド devcontainer を開く際に code コマンドによってディレクトリを開き、その後左下のアイコンをクリックして「Reopen in Container」を選択、とやるのは二度手間なので、それを改善する devcontainer コマンドを紹介します。 code コマンドをインストールしたのと同様にコマンドパレット(F1 を押すか Windows であれば ctrl + shift + P、Mac であれば command + shift + p)を開き Remote-Containers: Install devcontainer CLI を実行することでインストールできます。 devcontainer open hoge-devcontainer-directory と実行することで code コマンドを経由することなく devcontainer を開けます。 おわりに 今回は code コマンドと devcontainer コマンドについて紹介させていただきました。 特に標準出力を VS Code で表示する方法は、複雑なコマンドを打つ際に、途中で VS Code を経由しマルチカーソルとの連携や、置換したりすることで作業が単純になったりと色々応用が効くのでぜひ使ってみてください。 また VS Code は機能が豊富で開発も盛んなので今回紹介できなかった機能や、便利なショートカットなども今後紹介していきたいと思います。 参考 code コマンド https://code.visualstudio.com/docs/editor/command-line devcontainer コマンド https://code.visualstudio.com/docs/remote/devcontainer-cli devcontainer の構築方法 https://weseek.co.jp/tech/2331/ おまけで面白そうな機能 (今後紹介するかもしれません) https://code.visualstudio.com/docs/editor/integrated-terminal
アバター
こんにちは、エンジニアのYoheiです。 早速ですが、皆さんはテストコードを書いたことはありますか? テストを書くとなると腰が重くなってしまう方もいるかもしれませんが、実はテストを書くと良いことがたくさんあるんです。 テストについてよくわからない方、これからテストを書き始めるという方はぜひ読んでみてください! 前半は、テストの概要について説明します。 後半は、Javascriptの テスティングフレームワークである Jest を使って実際のコードを例に説明していきます。 まずはテストの概要について簡単に見ていきましょう。 テストとは ※学校でやるテストではありません。 ここでのテストとは、プログラムのテストです。 プログラムのテストとは、実装したプログラムが意図した通りに動いているかを検証するために行うものです。 テストの種類 テストには主に3つの種類が存在し、以下のように分類できます。 単体テスト 結合テスト システムテスト これらはよく「テストレベル」と呼ばれるそうです。 各テストレベルの役割 単体テスト:関数などプログラムの部品の最小単位のテスト 結合テスト:関数を組み合わせた機能のテスト システムテスト: 機能を組み合わせた、システムのテスト 上から下に向かってテストが対象とする機能の範囲が大きくなります。 テストの技法 テストの技法とはテストの仕方を意味します。 テストの技法の種類 簡単に以下2つを紹介します。 ホワイトボックステスト ブラックボックステスト ホワイトボックステスト システム内部の構造を理解した上で、それらがちゃんと意図通りに動作するかを確認するテストの方法をホワイトボックステストと呼びます。 例えばある関数の中で処理Aが実行されたら、次に処理B実行され、最後に処理Cが実行されるはずだ。といったことをテストします。 特徴はシステムの中の構造に着目していることです。 ブラックボックステスト システム内部の構造を理解する必要はなく、これをしたら結果はこうなるよねと、という意図通りにシステムが動作するかを確認するテストの方法をブラックボックステストと呼びます。 例えば、数字を2つ渡したら、足し算をした結果を返してくれる関数があるとします。 その関数に数字1と2を渡したら3が返されるはずだ、と言うことをテストします。 特徴はシステムの外から見た仕様に着目していることです。 その他 グレーボックステストやボトムアップテスト、トップダウンテストと呼ばれる技法も存在します。気になる方はぜひ調べてみてください。 テストコードを書くメリットデメリット メリット コードを書くメリットは様々ですが、 最終的にはサービスを利用してくれるユーザ様のためになるというところに帰結するでしょう。 ではどのようなメリットがあるのか見ていきます。 プログラミングコードの品質向上 バグの早期発見 バグの少ない機能開発 デグレ防止 心理的安心 デメリット 開発のスピードが落ちる テストコードの実装に時間がかかる 仕様変更によるテストコードのメンテナンス ※開発のスピードについては必ずしも落ちるとは言い切れないかもしれません。バグが発生した場合はその修正のコストが発生するからです。 テストのおおよその概要はつかめたでしょうか。 では実践に移りましょう! 実践編(Jest) 当記事では、 Node.js 上でテスティングフレームワークである Jest を使ってテストを書いていきます。 準備 まずは、以下のライブラリをインストールしましょう。 ※ Node.js での開発を前提としています。 ライブラリ Jest Javascript のテスティングフレームワークです。 インストール後、package.json に以下を追加しておきましょう。 { "scripts": { "test": "jest" } } メソッド 動作をテストをするメソッドをまとめた TestService class を定義した index.js ファイルと、テストをするための service.test.js 用意します。 テストを目的としているのでコードを深く理解する必要ありません。コピー&ペーストで進んでいきましょう。 index.js class TestService { multiplyNum(num, multiplyBy) { return num * multiplyBy; } saveNum(num, saveTo) { return new Promise((resolve) => { setTimeout(() => { saveTo.push(num); resolve('Saved'); }, 100); }) } multiplyAndSave(num, multiplyBy, saveTo) { const res = this.multiplyNum(num, multiplyBy); this.saveNum(res, saveTo); return res; } } module.exports = new TestService(); service.test.js // テスト対象の TestService クラスのインスタンスを index.js から読み込む const service = require('./index'); // ここから下にテストを記述 解説 各メソッドを解説します。 multiplyNum 第1引数の数値を、第2引数の数値で掛け算し、結果の数値を返すメソッド。 saveNum Promiseを返す非同期のメソッドです。 0.1 秒後に第1引数の数値を、第2引数の配列に格納します。 multiplyAndSave 同期的に multiplyNum を実行し、非同期的に saveNum を実行します。 multiplyNum の返り値を返すメソッドです。 計算はすぐして欲しいけど、データの保存は裏でよしなにやっといて〜、なシチュエーションを想定。 実践 基礎編 最初にテスト用の下地を用意します。 service.test.js に以下のコードを追加してください。 describe('TestService', () => { // ここにテストを追加 }); describe はいくつかの関連するテストをまとめるためのブロックで、この中にテストを書いていきます。今回は TestService というブロック名にしました。 test1 下地ができたので、まずは multiplyNum メソッドが正しく動作することを確認します。 以下の内容でテストしましょう。 2 x 2 は4になる service.test.js の describe 内に以下のコードを記述してください。 test('2 times 2 should be 4', () => { const result = service.multiplyNum(2, 2); expect(result).toBe(4); }); 解説 上記を解説します。 テストを書くときは test と書きます( it でも同様)。 第1引数にテスト名を、第2引数にテストの確認項目を含む関数を設定します。 3番目の引数 (任意) は タイムアウト値 (ミリ秒単位) で、中止するまでの待ち時間を指定します。 注意: デフォルトのタイムアウトは5秒です。 const result = service.multiplyNum(2, 2); このコードは見ての通り multiplyNum メソッドを呼び出して返り値を受け取っています。 メソッドが正しく動作していれば result には 4 が入るはずです。 この、 〇〇なはずだ 、という 期待 を書くのがテストになります。そのコードが以下になります。 expect(result).toBe(4); expect(A).toBe(B) => A は B なはずだ、という期待です。 では実際にテストを実行してみます。 実行 以下のコマンドで実行します。 npm test or yarn test 成功すると以下のようなログが表示されます。 % npm test > @ test /Users/*****/jest-test > jest PASS ./service.test.js TestService ✓ 2 times 2 should be 4 Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total test2 では、数値にマイナス2をかけた場合も同様に動くでしょうか。上記の test に続けてテストしてみましょう。 test('2 times 2 should be 4', () => { ... } test('2 times negative 2 should be negative 4', () => { const result = service.multiplyNum(2, -2); expect(result).toBe(-4); }); 実行 PASS ./service.test.js TestService ✓ 2 times 2 should be 4 (1 ms) ✓ 2 times negative 2 should be negative 4 Test Suites: 1 passed, 1 total Tests: 2 passed, 2 total 無事に成功したようです。 test3 次に非同期メソッドの saveNum メソッドをテストします。 saveNum メソッドが正しく動作することを確認するために以下の内容でテストしましょう。 数値の 100 と 空の配列 を渡して処理を実行すると、処理後の配列には数値の 100 が格納されている。 非同期メソッドも書き方はほぼ同じです。もし await をする場合は async を test メソッドの第2引数の始めにつけてあげるだけです。 test('number should be saved', async () => { const saveTo = []; await service.saveNum(100, saveTo); const expected = [100]; expect(expected).toEqual(expect.arrayContaining(saveTo)); }); 解説 分解してみていきましょう。 以下は空の配列を用意し、 saveNum メソッドの引数に 数字の100と一緒に渡して処理を実行しているだけです。 const saveTo = []; await service.saveNum(100, saveTo); 次のコードが読みにくいかもしれませんが実は簡単です。 const expected = [100]; expect(saveTo).toEqual(expect.arrayContaining(expected)); expect に渡した配列 saveTo が、 arrayContaining に渡した配列 expected が持つ要素を含んでいることを期待します。 実行 ではテストを実行してみましょう。 PASS ./service.test.js TestService ✓ 2 times 2 should be 4 (1 ms) ✓ 2 times negative 2 should be negative 4 (1 ms) ✓ number should be saved (105 ms) Test Suites: 1 passed, 1 total Tests: 3 passed, 3 total こちらのテストも無事成功しました。 その他にも多くの expect メソッドが存在するので気になる方は以下をご参考ください。 詳細はこちら: https://jestjs.io/ja/docs/expect 実践 応用編 最後に少しトリッキーな service.multiplyAndSave メソッドをテストします。 ※このメソッドは、内部で非同期メソッドである saveNum メソッド を await をせずに実行しています。 まずはこれまで通り書いてみましょう。 test4 test('returns array containing a multiplied number', async () => { const number = 10; const multiplyBy = 10; const saveTo = []; const numAfterMultiplied = await service.multiplyAndSave(number, multiplyBy, saveTo); const expectedNumAfterMultiplied = 100; const expectedArray = [100]; expect(numAfterMultiplied).toBe(expectedNumAfterMultiplied); expect(saveTo).toEqual(expect.arrayContaining(expectedArray)); }); 実行 テストを実行すると失敗します。 FAIL ./service.test.js TestService ✕ returns array containing a multiplied number (2 ms) ● TestService › returns array containing a multiplied number expect(received).toEqual(expected) // deep equality Expected: ArrayContaining [100] Received: [] 37 | 38 | expect(numAfterMultiplied).toBe(expectedNumAfterMultiplied); > 39 | expect(saveTo).toEqual(expect.arrayContaining(expectedArray)); | ^ 40 | }) 41 | }) at Object.<anonymous> (service.test.js:39:20) Test Suites: 1 failed, 1 total Tests: 1 failed, 3 passed, 4 total 解説 multiplyAndSave メソッドの内部で非同期に実行している saveNum メソッドは Promise を返す非同期なメソッドです。つまり、Promise が解決されていなくても処理は次に進んでいきます。 処理が expect を実行した段階ではまだ Promise が解決されていないため、 saveTo 変数は空の配列のまま expectedArray の値と比較されてしまい、 expect の条件を満たしていないためテストが失敗してしまいました。 これを解決するために saveNum メソッドの mock化を行います。 mock関数でできること 関数が持つ実際の実装を除去 関数の呼び出し(また、呼び出し時に渡されたパラメータも含め)をキャプチャ new によるコンストラクタ関数のインスタンス化をキャプチャ などなど書いてありますが、つまりはメソッドの振る舞いを変更できたりするわけです。 詳細はこちら: https://jestjs.io/ja/docs/mock-functions 今回これを使ってやることは以下の3つです。 jest.spyOn メソッドで、 saveNum メソッドを一旦内部の処理は何もしない null を返すだけの関数にmock化 mock 化したメソッドが multiplyAndSave メソッドの内部で呼び出された時に渡された引数を取得 saveNum メソッド の mock 化を解消(元の関数の状態に修正)し、取得した引数と共に saveNum メソッド を個別に呼び出し 修正 コードを次のように修正します。4つ目のテストを以下のコードで上書きしてください。 const syncMultiplyAndSave = async (number, multiplyBy, saveTo) => { const mockSaveNum = jest.spyOn(service, 'saveNum').mockReturnValue(null); const res = await service.multiplyAndSave(number, multiplyBy, saveTo); const argsCalledWithMockSaveNum = mockSaveNum.mock.calls[0]; mockSaveNum.mockRestore(); await service.saveNum(...argsCalledWithMockSaveNum); return res; } test('returns array containing a multiplied number', async () => { const number = 10; const multiplyBy = 10; const saveTo = []; const numAfterMultiplied = await syncMultiplyAndSave(number, multiplyBy, saveTo); const expectedNumAfterMultiplied = 100; const expectedArray = [100]; expect(numAfterMultiplied).toBe(expectedNumAfterMultiplied); expect(saveTo).toEqual(expect.arrayContaining(expectedArray)); }); 解説 新たに syncMultiplyAndSave 関数を定義し、 service.multiplyAndSave メソッドの代わりに呼び出しています。 mock化 const mockSaveNum = jest.spyOn(service, 'saveNum').mockReturnValue(null); これは、 syncMultiplyAndSave メソッド内で、 saveNum メソッドをmock化しています。 jest.spyOn は mock関数を返します。さらに、 .mockReturnValue(null) で mock関数が呼び出された際の返り値を null にしています。 メソッド呼び出し const res = await service.multiplyAndSave(number, multiplyBy, saveTo) 次に multiplyAndSave メソッドを通常通り実行します。 この時、内部で呼ばれる saveNum メソッドは mock関数になっており、null を返す以外は何もしません。 メソッド呼び出し時の引数取得 const argsCalledWithMockSaveNum = mockSaveNum.mock.calls[0]; メソッドを実行時に内部で呼び出された mock関数は、自身が呼び出された時に渡された引数を記憶しているのでそれを取得します。 詳細はこちら: https://jestjs.io/ja/docs/mock-function-api#mockfnmockcalls mock関数を元の状態に戻す mockSaveNum.mockRestore(); その後 mock関数の saveNum メソッドを元のメソッドに戻します。 saveNum を await 実行 最後に、ここまでに取得した引数を使って、 saveNum メソッドを個別に呼び出しています。 ここでは同期的に実行したいので、 await をつけています。 await service.saveNum(...argsCalledWithMockSaveNum) // 配列なので スプレッド構文を使用 テスト実行 もう一度テストを実行してみましょう。 PASS ./service.test.js TestService ✓ 2 times 2 should be 4 (1 ms) ✓ 2 times negative 2 should be negative 4 ✓ number should be saved (101 ms) ✓ returns array containing a multiplied number (106 ms) Test Suites: 1 passed, 1 total Tests: 4 passed, 4 total お疲れ様です、無事に成功しました。 まとめ お疲れ様でした。 当記事では、なるべく簡易な説明にするために、テストコードの記述は少なめにしましたが、プロジェクトによっては、さらにテスト用のダミーデータを用いて検証したりもします(ユーザーデータなど)。 今回私も実際のプロジェクトでテストコードを書いたことで、リリース前に多くのバグの早期発見・未然防止ができました。 特に複雑なコードを納期などがある中で書こうとすると、ほぼ必ず buggy なコードが紛れます(人間なのでしょうがないのでしょう)。 時間がないときはブラックボックステストだけでも書くと、アプリの品質をあげることができマスので、みなさんもぜひ可能な箇所からテストを書いてみてください。 最後に改めてメリット・デメリットを掲載しますでぜひご覧ください。 テストコードを書くメリットデメリット ここまでご覧いただきありがとうございました。 参照 https://www.qbook.jp/academy/curriculum/0001/lessons/ad00003/ https://jestjs.io/ja/
アバター
皆さんこんにちは!WESEEK ソフトウェアエンジニアの増山です。 今回は、GROWI を使うにあたって これさえ知っておけばいい基礎知識 をまとめました。GROWI は多機能 Wiki であるがために、どうしても「何から手をつけたらいいかわからない...」なんてことがあるかと思います。 この記事で GROWI を便利に使うための情報 を、効率よく学んでいってください! 記事の構成 「基本設定編」では、GROWI を始めてからまずやっておきたい設定をまとめています。こちらは 管理者向けの内容 になっています。 「便利機能編」では、GROWI の よく使う便利な機能 をまとめています。 ※ なおこの記事は個人〜数人の小規模チームでの利用を想定していますので、ユーザー管理・グループ管理などの大規模運用向けの情報は含まれていません。 目次 〜基本設定編〜 まずは簡単に GROWI をカスタマイズする方法を紹介していきます。 1. 「アプリ設定」で GROWI に名前をつける GROWI に名前をつけてみましょう。 トップページ左下の歯車のアイコンから管理画面に行きます。 "アプリ設定" をクリック "サイト名" の欄に好きな名前を入力して、更新ボタンを押します。 ページをリロードすると、画面左上に先ほど入力した名前が表示されています! 続けて、"コンフィデンシャル表示" をカスタマイズしてみましょう。 先程の "サイト名" 入力欄のすぐ下に、"コンフィデンシャル表示" の入力欄があります。 ここには例えば "社外秘" などと入力することで、画面上部のヘッダーに入力した文字を表示させることができます。これによって、ユーザー全員に「この GROWI の情報は社外秘だから口外しちゃダメですよ」ということを示すことができます。 2. 「テーマ設定」で GROWI のテーマ色を決める 管理画面の "カスタマイズ" > "テーマ" から、GROWI のテーマ色を設定してみましょう。 テーマには以下の2種類が存在します Dark/Light モード対応テーマ Dark/Light モード非対応テーマ Dark/Light モード対応テーマを選択した場合は、画面右上のユーザーアイコンをクリックしてモードを切り替えることができます。 3. 「レイアウト設定」でページの幅を調整する 管理画面の "カスタマイズ" > "レイアウト" から、ページを表示する幅を設定してみましょう。 デフォルトでは左右に余白を持つ設定になっています。画面の幅をできるだけ使ってコンテンツを表示したい場合は、 コンテンツ幅 100 % を選択しましょう。 さらにコンテンツ幅を広げたい場合は、画面右上のユーザーのアイコンをクリックして サイドバーモード を Drawer (左側)に設定しましょう。こうすることでサイドバーを格納でき、コンテンツを幅いっぱいに広げることができます。 〜便利機能編〜 ここからは便利機能を紹介していきます。 1. ページツリーでフォルダ構造のようにページを表示する 画面左側のサイドバーを開き、一番上のタブをクリックしてページツリーを表示させます。 ページツリーのアイテムは、以下の要素で構成されています。 開閉ボタン ページタイトル 配下に存在するページ数 3点リーダー ページ作成ボタン 開閉ボタンを押すと、子ページを表示できます 3点リーダーからは、ページ名の変更や、削除など複数の操作ができます +ボタンからは子ページをすぐに作成できます +ボタンをクリック ページ名を入力 エンターを押す ドラッグ&ドロップでページを移動する 移動させたいページを掴む 移動先のページの上に持っていき、ドロップする このとき色が変化している部分にページが移動します また、ページタイトル部分をクリックすると対象のページにアクセスできます。 2. 検索機能で読みたいページをすぐに見つける ページ上部の検索バーに見たいページのタイトルや、ページに含まれている単語を入力して検索する。 すると、検索ワードに当てはまったページが表示されます。 ここから ページツリー と同じように、3点リーダーからページ名の変更や削除などの操作ができます。 ※ PC 版の GROWI をご利用の方は / ショートカットキーですぐに検索ワードを入力し始めることができます。 3. ページの変更履歴と差分を確認する まず好きなページを編集します。 その後、画像の時計のアイコンをクリックして変更履歴を表示します。 ソースとターゲットを変更することで、選択されたページの差分を確認できます。さらに画像のクリップボードアイコンから、選択されたバージョンに対応するページのパーマリンクを取得することができます。 4. ユーザーホームでブックマークしたページを確認する 画面右上のユーザーアイコンをクリックして、 "ホーム" にいきます。 Bookmarks の下に、自分がブックマークしたページの一覧が表示されています。 また自分が作成したページも、その下の Recently Created から確認できます。 5. わからないことを質問する & バグを報告する こちらは GROWI 自体の機能ではないですが、知っておいて損はないので紹介しておきます。 GROWI を使っていてわからないことがあったら、 GROWI の Slack ワークスペース で質問することができます。 GROWI の Slack ワークスペース に登録 #help チャンネルで質問したいことをつぶやく 常に GROWI の開発者が見てくれているのでほとんどの問題を解決することができるはずです。増山(@taichi)もその中の一人です。気軽にご質問ください! 裏技. #GROWI をつけてツイートする 最後に、ここまで読んでいただいた方限定で 特別に 、「GROWI 開発チームに言いたいことを伝える裏技」を紹介します。 裏技といっても簡単で、 #GROWI をつけて Twitter でツイートするだけです。 "ページツリーの〇〇機能はめちゃ便利 #GROWI " "〇〇できる機能があったらいいなあ #GROWI " のようにツイートすることで、 GROWI 開発会議で話題が上がるかもしれません ! 是非ツイートしてみてください! 最後に 今回は「GROWI のおすすめ設定&便利機能8選!」ということで、まずは抑えておきたいことをまとめてみました。これで GROWI をより便利に使いこなせるようになっていただければ幸いです。 より詳しい内容については GROWI Docs が参考になると思います。もちろん Slack のチャンネルでも気軽にご質問いただけます! ここまでお読みいただきありがとうございました!
アバター
こんにちは。エンジニアの Ryo です。 本記事では、Docker と Visual Studio Code(以下、VSCode)の拡張機能を利用してプロジェクト内で devcontainer 環境を構築する方法をご紹介します。 突然ですが皆さん、開発環境を構築する際に特定のプログラム言語やライブラリのインストール作業、めんどうくさいと感じたことはありませんか? 特にホスト PC に直接インストールをする場合は、既に入っているものと競合しないか確認をしながら進めなければならないため、非常に手間がかかってしまいます。 今回紹介する devcontainer 環境を一度構築すれば、その後同様の開発環境を構築する際に、個別にプログラム言語やライブラリをインストールする必要がなくなるため、大幅な時間短縮を見込めます。複数人で開発する際に大きな効果を発揮します。 VSCodeで開発をしている方は大勢いらっしゃると思うので、ぜひこの記事を読んで快適な開発環境を手に入れてみてください。 今回使用した環境 macOS Catalina: v10.15.7 VSCode: v1.65.0 Remote – Containers: v0.224.2 Docker Desktop: v20.10.12 詳細なシステム要件は以下をご参照ください。 https://code.visualstudio.com/docs/remote/containers#_system-requirements devcontainer 環境とは devcontainer 環境は、VSCode の拡張機能「Remote - Containers」を使って構築できる環境です。Docker と VSCode が使えればどんな OS でも構築できます。 上の図のように、devcontainer 環境を立ち上げると Docker のコンテナ内に対して VSCode からアクセスし、コンテナ内にあるファイルの編集を VSCode 上で行うことができるようになります。 また、VSCode の設定や拡張機能、必要な言語・ライブラリのインストールなどは devcontainer.json や Dockerfile にまとめまておくことができます。 そのため、開発者は Docker と VSCode をインストールし、 VSCode 上でプロジェクトのリポジトリのディレクトリを開くだけで開発環境を利用できます。 簡単3ステップでぱぱっと環境構築 それでは、devcontainer 環境を構築してみましょう。 1. Docker のインストール 以下のリンクからインストールしてください。 https://www.docker.com/products/docker-desktop 2. VSCode で拡張機能「Remote - Containers」をインストール VSCode に拡張機能を入れていきます。今回はコンテナにアクセスするものなので「Remote - Containers」をインストールします。 3. 「Reopen in Container」を選択しセットアップ 拡張機能を入れればあと少しです。 まずは、VSCodeの画面左下の緑背景のアイコンをクリックします すると、画面上側の中央にメニューが表示されるので「Reopen in Container」をクリックして少し待ちます 作りたい環境を選択します 以上で devcontainer 環境の構築が完了です。 .devcontainerディレクトリについて 上記の手順で環境を作ると、下記のように .devcontainer というディレクトリが作成され配下に2つのファイルが自動で作成されます。 .devcontainer ├── devcontainer.json └── Dockerfile 2つのファイルを簡単に説明すると devcontainer.json 主に VSCode での設定を記述するためのファイルです プロジェクト内で共通化しておきたい VSCode の拡張機能や設定を書くことができます 参照: https://code.visualstudio.com/docs/remote/devcontainerjson-reference#_vs-code-specific-properties Dockerfile コンテナ自体の設定を記述するためのファイルです Dockerfile に関しては、過去に記事を公開しています。 詳しくは、 こちら の記事をご覧ください。 また Dockerfile ではなく docker-compose.yml を置いて読ませることもできます。 その際に、devcontainer.json ファイル内で以下の定義が必須です dockerComposeFile service 参照: https://code.visualstudio.com/docs/remote/devcontainerjson-reference#_docker-compose-specific-properties docker-compose に関しても過去に記事を公開しています。 詳しくは、 こちら の記事をご覧ください。 ※ 今回の記事では、devcontainer 環境の構築をゴールにしているため、プロジェクトによっては devcontainer 環境構築だけでなく dockerfile / docker-compose の拡充を進めておく必要があります。 tips! devcontainer.json ファイルの extensions に devcontainer 環境で利用する VSCode の拡張機能の ID を入力しておくことで、devcontainer 環境で共通の拡張機能を使うことができます。 ファイルに直接、拡張機能の ID を入力できますが、以下の画像にあるように追加したい拡張機能を検索し、「 Add to devcontainer.json 」を選択することで自動で devcontainer.json の extensions に記述してくれます。 まとめ ここまで読んでいただきありがとうございました。 今回は簡単 3 step で devcontainer 環境を作ってみました。VSCode さまさまですよね。 皆さんも是非、参画しているプロジェクトで devcontainer 環境構築してみてはいかがでしょうか? 参考記事 VSCode 公式ドキュメント https://code.visualstudio.com/docs/remote/containers#_system-requirements https://code.visualstudio.com/docs/remote/devcontainerjson-reference#_vs-code-specific-properties https://code.visualstudio.com/docs/remote/devcontainerjson-reference#_docker-compose-specific-properties Qiita https://qiita.com/kishibashi3/items/e20aecef45ed8341e739#docker-composeyml https://qiita.com/d0ne1s/items/d2649801c6f804019db7 Zenn https://zenn.dev/niisan/articles/9abd372ae86fc1
アバター