Tech

Tech

縞模様(ストライプ)を作るrepeating-linear-gradient

まっすぐなシマシマ CSSでシマシマな縞模様(ストライプ)を作るためにパッと浮かぶのは本来グラデーションを作るために使われる background-image: inear-gradient() と background-size を使ったテクニックです。 .bg-stripe { background-image: linear-gradient(90deg, /* 90deg = to right */ black 0%, /* 黒 */ black 50%, /* 黒 */ white 50%, /* 白 */ white 100% /* 白 */ ); background-repeat: repeat; /* 縦横どちらも繰り返す */ background-size: 20px auto; /* 白・黒・縞どうしのサイズはどちらも10px(=20px÷2) */ } 範囲のうち半分(0〜50%)を黒、もう半分(50%〜100%)を白とすることで、グラデーションといいつつ境界のハッキリとしたボーダーを描画し、かつ background-size によって繰り返しグループ1回分のサイズ単位を制御することで以下のような縞模様ができます。「黒・白」で1グループなので、10pxごとの縞模様にしたければ倍の20pxが単位になります。 ※画像はイメージのため black は #999 に変更しています。 ただ、この方法では 0deg (to top )や 90deg(to right )のように、垂直・平行でないとうまくいきません。135deg のように傾けてしまうと、以下のようなバラン型になってしまいます。これはこれで使う場面もありそうですが……。 斜めのシマシマ 垂直・平行方向にまっすぐでないシマシマを作るには頭に「repeating-」(繰り返す)を付けた repeating-linear-gradient を使います。 こちらなんとCSSのグラデーション表現が誕生した頃には既に使えるプロパティだったようで、IE(Internet Explorer)10での表示も可能なようです。 .bg-repeating-stripe { background-image: repeating-linear-gradient(135deg, black 0px, /* 黒 */ black 10px, /* 黒 */ white 10px, /* 白 */ white 20px /* 白 */ ); /* background-sizeやbackground-repeatは指定しない(auto)でも繰り返される */ } これにより単位ごとに背景をリピートする方法では難しい斜めの縞模様ができます。 つまりグラデーションの内部で繰り返しを行った状態を背景画像として提供してくれるので、サイズ指定を background-image 内で行ってしまえば、 background-size や background-repeat はデフォルトのままシマシマで埋め尽くすことができます。 もちろん「まっすぐなシマシマ」にも repeating-linear-gradient を使うことができますが、 linear-gradient の感覚でサイズやリピートを指定してしまうと「外」と「内」の二重で繰り返しが発生してしまい、うまくいかないので注意してください。 失敗例 100%まで繰り返しているため、 linear-gradient と同じになってしまう例 .bg-repeating-repeating-stripe { background-image: repeating-linear-gradient(135deg, /* 90deg = to right */ black 0%, /* 黒 */ black 50%, /* 黒 */ white 50%, /* 白 */ white 100% /* 白 */ ); background-repeat: repeat; /* 内部でもう繰り返してるのに外からも繰り返す */ background-size: 20px auto; /* linear-gradientと同じ結果になる */ } サイズを制限してしまい二重に繰り返すことで表示が刻まれてしまう例 .bg-repeating-repeating-stripe { background-image: repeating-linear-gradient(135deg, /* 90deg = to right */ black 0px, /* 黒 */ black 10px, /* 黒 */ white 10px, /* 白 */ white 20px /* 白 */ ); background-repeat: repeat; /* もう繰り返してるのにもう一回繰り返す */ background-size: 20px auto; /* 斜めの時点で縞のサイズ≠背景の繰り返し幅のためズレる */ } また repeating- による繰り返しグラデーションは他のグラデーション形式にも対応しており、 repeating-radial-gradient repeating-conic-gradient も同様に利用できます。円形のストライプを使う機会なんてかなりサイケデリックな場面しかなさそうですが……🌀 使う頻度に対して対応環境が思った以上に広くてびっくりしたプロパティでした。(きちんとやるなら三角関数的な計算は必須です) せっかくの午年ですしシマシマを作りたいときは積極的に活用していきましょう!

ページトップを判定したいだけなのに

課題 よくある「トップページに戻る」ボタンが画面固定されている場合について、自分がページトップにいれば非表示(非活性)・いなければ表示(活性)するようにしたい。 問題 もっとも容易に考えられる条件はY方向のスクロール値 window.scrollY が「ゼロか」「ゼロでないか」の時である。 つまりスクロールイベントが発生するたびに window.scrollY === 0 かを判定すればよいのだが、 window.addEventListener(’scroll’, … ) はご存知の通り(?)悪名高く、DebounceやThrottleといったテクニックがないと不要なスクロール処理が過剰発生することで知られている。 ただ、これらの「処理を間引く」ためのテクニックを使った場合、間引いているために少し反応が遅れて見えてしまったり細かいUI上の違和感が現れることもあり、適切でない場面もある印象。 そのため具体的な要素(ヘッダーが隠れた時)などの条件であればIntersection Observerで監視してあげるのがスマートでよいのだが、「ページトップか」を判断するにはそのためだけに見えない1pxの要素をページの一番上に置いて〜などのハックを使うことになるため少し煩わしい。 タイトル通りやりたいのはただ自分がページトップにいるかどうかを判定したいだけなのに……。 解決 アクセスした時点では活性状態にしたくないため、あらかじめ「トップページに戻る」ボタンには inert 属性を付けておく。 <a href="#" id="pagetop" inert>トップページに戻る</a> ※ hidden (非表示)と違いinert (非活性)属性だけでは非表示にはならないため、別途CSSにて非表示するための記載が必要。 /* CSSアニメーションを付けたい場合は別途transitionなどを使ってください */ #pagetop[inert] { visibility: hidden; } この前提をもってJavascript上で #pagetop の表示・非表示= inert の真・偽を切り替える。 function pagetopToggle() { let ticking = false; const pagetop = document.querySelector('#pagetop'); window.addEventListener('scroll', function() { if (!ticking) { window.requestAnimationFrame(function() { // 画面のフレームレートに応じて処理を間引く const isTop = window.scrollY === 0; // scrollTop: 0ならtrue if ( pagetop.inert !== isTop ) { // isTopがinertの有無と一致しなくなった時 pagetop.inert = isTop; // ページトップ(true)かトップでないか(false)を一致させる } ticking = false; }); ticking = true; } }, { passive: true }); // 既存のスクロール処理を邪魔しない=preventDefaultを使用しないことを明示 } もちろん window.scrollY はスクロールのたびに取得されるが、 inert の付け外し=画面上の変更処理については「ページトップでなくなった」「ページトップに戻った」の2回のみ働くため、パフォーマンス上はIntersection Observerによる監視と同等の軽量さが得られる(はず)。 また、念のため requestAnimationFrame ないし ticking によって画面のフレームレートごとに最低限の「間引き」も行っている。おそらくこれが一番お手軽かも。 ※別解:animation-timeline 動くか試せていないのとパフォーマンス的な観点では未知数だが、CSSの animation-timeline が使えるようになればこれだけで済むらしい(すばらしすぎる……)。 ただ、現状ではFirefoxなどにpolyfillが必要なためいずれはJSでCSSをゴニョゴニョする時代も終わるのかもしれないという希望だけ……。 https://caniuse.com/mdn-css_properties_animation-timeline body { animation-timeline: scroll(); animation-range: 0 1px; } #pagetop { visibility: hidden; /* content-visibility: none; */ animation: toggle-pagetop linear both; } @keyframes toggle-pagetop { to { visibility: visible; /* content-visibility: visible; */ } }

Live Sass Compilerをやめてnpmスクリプトに移行する

概要 これまでVS Codeの拡張機能「Live Sass Compiler」を使用してSCSSをコンパイルしていましたが、環境(設定やバージョン)によって出力されるCSSに差分が生じる問題が発生しました。 この問題を解決し、チーム全員が同じ設定でコンパイルできるように、npm scriptsを利用した管理に移行した際の手順をまとめます。 起きていた問題 VS Codeの拡張機能設定を同じにしても、出力結果に微妙な差分が出る。 解決策:npmスクリプトへの移行 プロジェクト自体にコンパイル環境を持たせることで、OSやエディタ環境に左右されない「共通のコンパイル設定」を構築します。 1.Node.jsのバージョン設定 古いバージョン(v14など)を使用していると、最新のSass機能が使えない可能性があるため、安定したバージョンに変更します。今回は nodenv を使用してプロジェクトのローカルバージョンを指定しました。 # 現在のバージョン確認 node -v # v14.17.1 (古い) # 利用可能なバージョンを確認して切り替え nodenv versions nodenv local 20.18.2 # 切り替わったか確認 node -v # v20.18.2 # 切り替わったか確認 node -v # v20.18.2 2.Sassパッケージのインストール プロジェクトのルートディレクトリで公式の Sass(Dart Sass)をインストールします。 # package.jsonがない場合は初期化 npm init -y # Sassを開発用依存関係(devDependencies)としてインストール npm install -D sass 3.package.json にスクリプトを追加 package.json の scripts セクションにコンパイル用のコマンドを追記します。これにより、パスの設定やオプション(Source Mapの有無など)を共通化できます。 コード スニペット { "scripts": { "sass:watch": "sass --watch src/scss:dist/css --style expanded --no-source-map", "sass:build": "sass src/scss:dist/css --style compressed --no-source-map" } } --watch: ファイルの変更を監視して自動コンパイル --style expanded: 開発時に読みやすい形式で出力 --style compressed: 本番公開用に圧縮して出力 --no-source-map: 今回はソースマップを出力しない設定 4.実行方法 チームメンバーはリポジトリをクローンした後、以下のコマンドを叩くだけで全員同じ設定で監視が始まります。 # 初回のみ npm install # 監視開始 npm run sass:watch 5. .gitignore の確認 不要なファイルがリポジトリに混入しないよう、.gitignore ファイルに以下が記述されているか確認します。 node_modules .DS_Store

ejs-cliとnpm scriptsで静的サイト制作環境を構築する

概要 以前、Sassのコンパイル環境をnpm scriptsで構築しましたが、今回はさらにHTMLをEJSに置き換えて、ヘッダーやフッターなどの共通パーツを効率的に管理できる環境を作ります。 1. 必要なツールのインストール EJSをコマンドラインから変換(コンパイル)するために ejs-cli を導入します。また、複数コマンドを並列実行するために npm-run-all なども合わせてインストールします。 npm install -D ejs-cli onchange npm-run-all 2. package.json の設定 package.json の scripts 部分に、EJSを変換するためのコマンドを追加します。 "scripts": { "build:ejs": "ejs-cli --base-dir src/ejs \"**/*.ejs\" --out public", "scss": "..." (今のSCSSコマンド) } 3. 「共通パーツ」と「ページ本体」の分割 header部分を_header.ejsに移動、footer部分を_footer.ejsに移動した後ページで両方を読み込みます。 <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>トップページ</title> <link rel="stylesheet" href="/css/style.css"> </head> <body> <%- include('_header') %> <main> <p>ここがメインコンテンツです。管理が楽!</p> </main> <%- include('_footer') %> </body> </html> 4. ページごとにタイトルやJSを動的に変える include する際にオブジェクトを渡すことで、ページごとにタイトルや読み込むJSファイルを制御できます。 _header.ejs(タイトル制御): <% let titleParts = []; if (typeof title !== 'undefined' && title !== '') { titleParts.push(title); } titleParts.push('サイト名'); %> <title><%= titleParts.join(' | ') %></title> 注意: 記述ミス(.の欠落やスペースの有無)で動かなくなることがあるため、EJS内のスクリプトタグは慎重に記述しましょう...。 _footer.ejs <script src="/js/common.js"></script> <% if (typeof pageJs !== 'undefined') { %> <% pageJs.forEach(function(url) { %> <script src="<%= url %>"></script> <% }); %> <% } %> 5.【重要】発生した問題と解決策 問題1:共通パーツ用のファイル(_footer.html等)が出力される_から始まるファイルは出力不要なので、package.json のコマンドで除外設定を行います。 "build:ejs": "ejs-cli --base-dir src/ejs \"**/!(_)*.ejs\" --out public", 問題2:でリンクが機能しないpublic フォルダをルートとして認識させる必要があります。 .vscode/settings.json に以下を追記します。 { "liveServer.settings.root": "/public" } 6. npm scripts の完成形 最終的な package.json の設定です。SassとEJSの「監視(watch)」を1つのコマンドで同時に実行できるようにします。 { "scripts": { "sass:watch": "sass --watch src/scss:public/css --style expanded --no-source-map", "sass:build": "sass src/scss:public/css --style compressed --no-source-map", "ejs:build": "ejs-cli --base-dir src/ejs \"**/!(_)*.ejs\" --out public", "ejs:watch": "onchange \"src/ejs/**/*.ejs\" -- npm run ejs:build", "build": "npm-run-all -p sass:build ejs:build", "start": "npm-run-all -p sass:watch ejs:watch" } } 7. チーム向けREADMEの更新 移行後は、以下のルールをチームに周知します。 開発開始: npm start を実行 ソース管理: src フォルダを編集する 画像・JS: これらは直接 public フォルダ内に配置する(※ビルドフローに入れない場合) 拡張機能: Live Sass CompilerなどのVS Code拡張機能は競合するためオフにする

【OSSでWiki構築】第2回(環境構築編)

Docker ComposeでOutlineとDBを連携させる Docker Composeによる構築手順 1. はじめに この記事は、オープンソースのWikiツール「Outline」を、Docker Composeを利用してローカルPC上に構築するまでの一連の手順をまとめたものです。 最終的に、自己署名証明書による安全なHTTPS環境でOutlineが動作する状態を目指します。 第1回はこちら。【OSSでWiki構築】第1回:OSSの選定 使用技術スタック アプリケーション: Outline 実行環境: Docker / Docker Compose データベース: PostgreSQL キャッシュ: Redis リバースプロキシ (HTTPS化): Caddy 2. 事前準備 構築を始める前に、以下のツールがPCにインストールされていることを確認します。 Docker Desktop: 公式サイトからダウンロード。 (macOSの場合) Homebrewを使ったインストールが便利です: brew install --cask docker テキストエディタ: Visual Studio Codeなど、コードを編集できるもの。 ターミナル (コマンドラインツール) 3. 構築手順 ステップ1: プロジェクトフォルダの作成 まず、作業用のフォルダを作成し、その中に移動します。 # 例としてデスクトップに作成 cd ~/Desktop # フォルダを作成 mkdir outline-project # 作成したフォルダに移動 cd outline-project ステップ2: hostsファイルにカスタムドメインを追加 localhostに紐づくブラウザの強力なキャッシュ(HSTS)を回避し、クリーンな状態でローカルHTTPS環境を構築するため、PCのhostsファイルにoutline.testというカスタムドメインを追加します。 ターミナルで以下のコマンドを実行し、hostsファイルを開きます。(パスワード入力が必要です) sudo nano /etc/hosts ファイルの末尾に、以下の一行を追記します。 127.0.0.1 outline.test Control + O → Enterで保存し、Control + Xでエディタを終了します。 ステップ3: Caddyの設定ファイルを作成 HTTPS通信を担当する「SSL執事」であるCaddyの設定ファイルを作成します。 outline-projectフォルダ内に、Caddyfileという名前のファイル(拡張子なし)を新規作成し、以下の内容を記述します。 outline.test { # これはインターネットに公開しない、内部用の証明書を使うという指示 tls internal # outline.testに来たアクセスを、outlineコンテナの3000番ポートに中継する reverse_proxy outline:3000 } ステップ4: シークレットキーの生成 Outlineのセキュリティ設定に必要な、2つのランダムな文字列(シークレットキー)を生成します。 ターミナルで以下のコマンドを2回実行し、表示された文字列をそれぞれメモしておきます。 openssl rand -hex 32 ステップ5: docker-compose.ymlの作成と編集 プロジェクトの心臓部である設計図docker-compose.ymlを作成します。 outline-projectフォルダ内にdocker-compose.ymlを新規作成し、以下の内容を貼り付けます。 その後、SECRET_KEYとUTILS_SECRETの値を、ステップ4で生成した2つのキーに置き換えます。 # 動かしたいコンテナ(サービス)を定義していく services: # 1番目のサービス:Caddy(リバースプロキシ兼SSL執事) # ブラウザからのアクセスを全て受け取り、安全なHTTPS通信を提供する caddy: image: caddy:2-alpine restart: unless-stopped # PCのポート80(HTTP)と443(HTTPS)をCaddyコンテナに接続する # CaddyがHTTPからHTTPSへのリダイレクトも自動で行う ports: - "80:80" - "443:443" # Caddyの設定ファイル(Caddyfile)をコンテナ内に読み込ませる volumes: - ./Caddyfile:/etc/caddy/Caddyfile # 2番目のサービス:PostgreSQLデータベース db: image: postgres:15-alpine restart: unless-stopped volumes: - postgres_data:/var/lib/postgresql/data environment: POSTGRES_USER: outline POSTGRES_PASSWORD: StrongPassword POSTGRES_DB: outline # 3番目のサービス:Redis(キャッシュなど一時データ用) redis: image: redis:7-alpine restart: unless-stopped # 4番目のサービス:Outline本体 outline: image: outlinewiki/outline:latest restart: unless-stopped # ポートはCaddyが全て管理するため、Outlineは外部にポートを公開しない depends_on: - db - redis environment: # ステップ4で生成した2つのキーに置き換える SECRET_KEY: 'ここに1つ目のキーを貼り付け' UTILS_SECRET: 'ここに2つ目のキーを貼り付け' # アプリケーションの公開URL(httpsとカスタムドメインに変更) URL: '<https://outline.test>' DATABASE_URL: 'postgres://outline:StrongPassword@db:5432/outline' REDIS_URL: 'redis://redis:6379' PGSSLMODE: 'disable' # データを保存しておくボリューム(保管庫)を定義 volumes: postgres_data: ステップ6: コンテナの起動 全ての準備が整いました。ターミナルで以下のコマンドを実行し、コンテナを起動します。 docker compose up -d 初回はイメージのダウンロードに数分かかる場合があります。 ステップ7: 動作確認とアクセス コンテナが正常に起動したか、以下のコマンドで確認します。 docker compose ps 4つのサービス(caddy, db, redis, outline)がすべてUpまたはrunningになっていればOKです。outlineのSTATUSが(healthy)に変わるまで1〜2分待ちます。 Webブラウザで以下のURLにアクセスします。https://outline.test ブラウザに「保護されていない通信」などの警告が表示されますが、これは正常な動作です。「詳細設定」などをクリックし、「outline.testにアクセスする(安全ではありません)」を選択して先に進みます。 Outlineの初期設定画面が表示されれば、環境構築は成功です! 4. まとめ 以上の手順で、4つのコンテナが連携して動作する、安全なローカルHTTPS環境のOutlineを構築することができました。 構築過程で発生した様々なエラーとその解決策については、別の記事でまとめます。

【OSSでWiki構築】第1回

OSSの選定 はじめに OSSを利用したWiki構築の取り組みです。 目的とゴール設定 業務で扱う機会が少ない「システム構築の技術」を習得し、アウトプットすることを主目的としています。 目的(メイン) システム構築の一連のプロセスを経験し、具体的な技術的知見(Docker、DB、設定など)を獲得する。 ゴール OSSを用いて、実用可能なWiki環境を構築し、完成させる。 共有事項 構築過程で遭遇した技術的な課題と解決策を参考情報として共有する。 候補OSSの初期比較検討と仮選定 OSS名特徴・OutlineモダンなUI/UX、Notionに近い使い勝手、チーム向けに特化。Reactベース。・wiki.js多機能で柔軟な構成、多数の認証方式に対応。Vue.jsベース。・GROWI日本発の多機能Wiki。Markdownに加え、Notionのような構造化も可能。・MediaWikiWikipediaで使われている技術。機能は豊富だが、デザインが古め。 Outlinewiki.jsGROWIMediaWikiUI/UXモダン。多機能。やや古さが残る。機能的。設定が複雑。古典的。技術スタックNode.js,React,PostgreSQLNode.js,Vue.js,複数DBNode.js,React,MongoDBPHP,MySQL構築難易度低(シンプル)中(多機能ゆえ)中(MongoDB導入が必要)高(PHP環境構築が必要) 【結論】 上記比較の結果、Outlineを検証のメインOSSとして仮選定し、構築を進めていきます。 技術スタックの決定 項目採用技術理由対象OSSOutline上記比較検討の結果より実行環境Docker / Docker Compose依存関係の分離と管理DBPostgreSQLOutlineの必須要件であるため。 Outlineについて 概要 チームのドキュメント、議事録、仕様書、日報などを整理し、簡単に検索・共有するためのプラットフォーム。自分のサーバーでデータを管理できるため、セキュリティやプライバシーを重視する組織に適している。 特徴 モダンで高速なエディタ: Markdown記法に対応しており、直感的で快適にドキュメントを作成できる。リアルタイムでの共同編集も可能。 階層的なドキュメント管理: ドキュメントを入れ子構造で整理できるため、情報が探しやすくなっている。 強力な検索機能: すべてのドキュメントを横断して高速に検索できる。 外部サービス連携: Slackと連携して通知を受け取ったり、FigmaやLoomなどのコンテンツを埋め込んだりすることができる。 セルフホストとオープンソース: 自分のインフラ上で運用するため、データを完全にコントロールできる。また、必要に応じてカスタマイズも可能。 公式サイト https://www.getoutline.com

Goのイテレーター・ジェネレータについて

はじめに Go 1.23でイテレータ機能が標準ライブラリに追加されました。本記事では、新しく導入されたiterパッケージの使い方と、従来のスライスベースの反復処理との違いについて、実行フローとパフォーマンスについてまとめていきます。 イテレータとは イテレータは、コレクションの要素を順次走査するための抽象化です。Goでは従来からrangeを用いたスライスの反復処理が可能でしたが、Go1.23からは関数ベースのカスタムイテレータが言語レベルでサポートされるようになりました。 // 従来のスライスベースの反復処理 for i, v := range []string{"a", "b", "c"} { fmt.Println(i, v) } ジェネレータの概要 ジェネレータは、値を遅延評価的に生成するイテレータの一種です。Pythonのyieldキーワードのような専用構文はありませんが、iterパッケージで定義された型を使って実現します。 iter パッケージの型定義 package iter type Seq[V any] func(yield func(V) bool) type Seq2[K, V any] func(yield func(K, V) bool) Seq[V] 単一の値を返すイテレータ Seq2[K, V] キーと値のペアを返すイテレータ(mapのrangeループに相当) yield関数の戻り値は継続フラグで、falseを返すとイテレーションが中断されます。 スライス vs ジェネレータ スライスとジェネレータの実装について比較していきます。 【パターン1】 スライスベースの実装 func Test_Slice(t *testing.T) { strings := createSlice(5) for _, s := range strings { fmt.Printf("Test_Slice: %s\\n", s) } } func createSlice(max int) []string { slice := make([]string, 0, max) for i := range max { fmt.Printf("createSlice: %d\\n", i) slice = append(slice, strconv.Itoa(i)) } return slice } 実行結果: createSlice: 0 createSlice: 1 createSlice: 2 createSlice: 3 createSlice: 4 Test_Slice: 0 Test_Slice: 1 Test_Slice: 2 Test_Slice: 3 Test_Slice: 4 スライス生成が完全に完了してから、rangeループによる反復処理が開始されます。 【パターン2】 ジェネレータベースの実装 func Test_Yield(t *testing.T) { stringGenerator := generateString(5) for s := range stringGenerator { fmt.Printf("Test_Yield: %s\\n", s) } } func generateString(max int) iter.Seq[string] { return func(yield func(string) bool) { for i := range max { fmt.Printf("generateString: %d\\n", i) if !yield(strconv.Itoa(i)) { return } } } } 実行結果: generateString: 0 Test_Yield: 0 generateString: 1 Test_Yield: 1 generateString: 2 Test_Yield: 2 generateString: 3 Test_Yield: 3 generateString: 4 Test_Yield: 4 生成と処理が交互に実行されています。これが遅延評価の特徴です。 実行フローの違い スライスの場合 createSliceが全要素を生成 スライスがメモリ上に確保される rangeが各要素を順次処理 ジェネレータの場合 rangeがiter.Seq型の関数を実行 yieldが呼ばれるたびにループ本体が実行される 次の要素が必要になるまで生成処理は進まない 注目すべきは、generateStringの戻り値である関数を明示的に呼び出していない点です。rangeキーワードがiter.Seq型を検出すると、自動的に関数を実行してイテレーションを開始します。 パフォーマンス特性の比較 項目スライスジェネレータメモリ使用量O(n) 全要素を保持O(1) 現在の状態のみ初期化コスト高い - 全要素を事前生成低い - 遅延生成反復処理速度高速 - メモリアクセスのみやや低速 - 毎回関数呼び出しCPU使用率低い(反復時)高い(関数呼び出しオーバーヘッド)早期終了時の効率無駄な生成が発生必要な分だけ生成 ベンチマーク例 func BenchmarkSlice(b *testing.B) { for i := 0; i < b.N; i++ { slice := createSlice(1000) for _, s := range slice { _ = s } } } func BenchmarkGenerator(b *testing.B) { for i := 0; i < b.N; i++ { gen := generateString(1000) for s := range gen { _ = s } } } 使い分けの指針 スライスを選ぶべきケース 全要素を複数回走査する必要がある データサイズが小さく、メモリに余裕がある 反復処理のパフォーマンスが重要 データを一度に取得するコストが低い ジェネレータを選ぶべきケース データサイズが大きく、メモリ効率が重要 要素生成のコストが高い(DB クエリ、API コールなど) 早期終了の可能性が高い(条件に合う最初の要素を探すなど) 無限シーケンスを扱う場合 実践例:無限シーケンス func infiniteCounter() iter.Seq[int] { return func(yield func(int) bool) { i := 0 for { if !yield(i) { return } i++ } } } // 最初の10個だけ取得 func Test_InfiniteCounter(t *testing.T) { count := 0 for n := range infiniteCounter() { fmt.Println(n) count++ if count >= 10 { break } } } このようなパターンはスライスでは実現できません。 まとめ Go1.23のイテレータ機能は、従来のスライスベースの反復処理に加えて、メモリ効率の良い遅延評価を実現できます。 スライスメモリと引き換えに高速な反復処理 ジェネレータCPU時間と引き換えにメモリ効率の良い遅延評価 適切なパターンを選択することで、パフォーマンスとリソース使用量のバランスを最適化してきましょう。 参考資料 Go 1.23 Release Notes - Iterators

GoでEnumを表現する2つのパターン

Goには、Pythonなどの言語にあるような厳密なEnum型が存在しません。そのため、独自の型定義と定数を組み合わせてEnumを表現するのが一般的です。 今回は、実務でよく使われる2つの表現パターンについて紹介します。 【パターン1】 iota を活用した連番定義 iotaは、定数ブロック内で連番を自動的に割り振るための仕組みです。(Goに備わっている標準機能です) type ErrorCode int const ( BadRequest ErrorCode = iota + 1 // 1 Required // 2 InvalidChars // 3 InternalError // 4 ) iotaの初期値について Goの整数型のゼロ値は0です。未初期化の変数と明示的な値を区別するため、iota + 1として1から開始するのが一般的なプラクティスです。 定義したEnum値に対して文字列を返したい場合は、以下のように String() メソッドを定義します。 func (e ErrorCode) String() string { switch e { case BadRequest: return "BadRequest" case Required: return "Required" case InvalidChars: return "InvalidChars" case InternalError: return "InternalError" default: return fmt.Sprintf("ErrorCode(%d)", e) } } これにより、fmt.Printlnなどで出力する際に、数値ではなく意味のある文字列として扱えるようになります。 【パターン2】 mapベースで管理していく 定数で定義した値をキーとして、対応するメッセージをmapに格納する方法です。フォーマット可能な文字列や複雑なメタデータを関連づけたい場合に有効なパターンになります。 基本的な実装例 type ErrorCode int const ( BadRequest ErrorCode = iota + 1 Required InvalidChars InternalError ) var messageMap = map[ErrorCode]string{ BadRequest: "Bad Request error", Required: "%sを入力してください", // ... 続く } func (e ErrorCode) Message() string { if msg, ok := messageMap[e]; ok { return msg } return "Unknown Error Msg" } パラメータをつけた実装例 func (e ErrorCode) FormatMessage(args ...interface{}) string { template := e.Message() return fmt.Sprintf(template, args...) } msg := Required.FormatMessage("ユーザー名") // 出力: ユーザー名を入力してください ▶︎ こちらで試せます まとめ GoでEnum的な構造を実現する際は、要件に応じて使い分けましょう。 パターン1 シンプルで高速な文字列変換が必要な場合 パターン2動的なメッセージ生成や複雑なメタデータ管理が必要な場合 基本的にはパターン1をベースにし、複雑な関連データが必要になった段階でパターン2を検討するのが、Goらしいのかなと思っています。

スムーススクロールはJSでやるべき

みなさん、スムーススクロール(慣性スクロール)してますか。 jQueryの世界ではいにしえより伝わる以下のコードがあり、 # で始まるIDへのアンカーリンクの「スクロールがヌルっと動くため」だけにコピペで動くコードとして重宝されてきました。 $(function(){ $('a[href^="#"]').click(function(){ var speed = 400; // ページ中のどこに行くにも4秒で着く var href= $(this).attr("href"); var target = $(href == "#" || href == "" ? 'html' : href); var position = target.offset().top; $('body,html').animate({scrollTop:position}, speed, 'swing'); return false; }); }); ただ近年、このようなスムーススクロールを実装したい「だけ」であれば、以下のCSS1行の記述だけで完結するようになりました。 html { scroll-behavior: smooth; /* これだけ……だが…… */ } イージング(アニメーションの緩急)やデュレーション(辿り着くまでの時間=スピード)は各種ブラウザの取り決めに従うため調整用のオプションはありませんが、先ほどのjQueryで行っていたスムーススクロール程度の動き・要望についてはこちらのCSSのみで事足りる便利な記述です。 https://caniuse.com/?search=scroll-behavior 日本ではモバイル天下一ブラウザであるSafariについても2022年から(遅くない……?)対応しているためほぼ問題なく活用でき、実際 scroll-behavior: smooth; について調べると、滑らかなスクロールはこれだけで解決!という記事ばかり引っかかるようになりました。めでたしめでたし ……はたして本当にそうでしょうか……? 全てのスクロールバグの生みの親 たとえばLPペライチ内でのアンカーリンクのためだけにスムーススクロールの処理を書くのは……といった程度の場合には先ほどのCSSによる置き換えが有効ではあります。 ただし、サイトの規模が大きくなるほど scroll-behavior: smooth; は実用に耐えない場面が多いです。 つまりWeb記事の通り「これだけで解決!」と単純に思って reset.css や style.css の頭なんかに書いてしまうと他プラグインとの兼ね合いが悪く、原因不明のスクロール関連バグにつまづくことが多くなり……結局便利なスムーススクロールを使えなくなってしまう場面が稀にあります。 悪さをしている事例 原因不明といいつつほとんどは先の scroll-behavior: smooth; の記述があるためなのですが、具体的には以下のような事例が挙げられます。 https://seeder.site/web/610.html Swiper.jsでいつものように無限ループさせていたら、1周目を終わろうとしていたときに高速移動している。 https://note.com/chino8234/n/n538c02cf5dd0 「アンカーリンクでTOPページの特定のセクションに飛ばそうとしたのに、なぜか毎回“次のセクション”に遷移してしまう…」 https://x.com/tak_dcxi/status/1781708160484692054?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1781708160484692054%7Ctwgr%5E6865e621c16247d4df7f699d0f88d3aa328110a0%7Ctwcon%5Es1_c10&ref_url=https%3A%2F%2Fwww.tak-dcxi.com%2Farticle%2Fsmooth-scroll-implementation-example%2F (https://www.tak-dcxi.com/article/smooth-scroll-implementation-example/ ) ページ遷移後にスクロールで待たせる挙動はCSSのスムーススクロールでも同じ。JSならこの挙動を無効にできるけどCSSではできない。 https://x.com/tami_design/status/1815737711099248742?s=20 GSAPのScrollTriggerとscroll-behavior: smooth;は同時に使用しない横スクロールを実装したのですがリロードや画面リサイズすると表示が崩れたりして安定しなかった… ※こちら、GSAPのアニメーションがやけに重いと思ったら原因これだったので実体験です。 主にスライダー(カルーセル)やスクロールアニメーション系のプラグイン、ページ遷移時のアニメーションによくない影響を及ぼしていることがわかります。近頃はこういったプラグインもアニメーションの軽量化のためCSS animationを活用していることが多いのですが、そちらとCSS側の「全てをスムーススクロール」させる命令が干渉・衝突しているためかと思われます。 しかも scroll-behavior: smooth; は html に当てないとその効力を発揮しないため、必ずグローバル(ページ内すべて)のスクロールに対して反映されることになり……、衝突は避けられません。これではいけませんね こういった問題が発生した時の対策も必ず「scroll-behavior: smooth;を外してCSSでスムーススクロールしない」になってしまうため、実装をラクしたサボったツケを喰らうことになります。 そこでサイトの規模が大きくなりそうだけどCSSではなく、かつjQueryにも依存しないJavaScriptでスムーススクロールするための代替コードを考えてみました。 scroll-behavior: smooth; の代替メソッド 具体的に辿り着きたい対象=ターゲットがはっきりしている場合、JSでは scrollIntoView() というメソッドを活用することができます。こちらに behavior: "smooth" という引数を与えると、CSSで行っていたスムーススクロールと同じ動きを行うことができます。 element.scrollIntoView({ behavior: "smooth", /* CSSで指定したものと同じ振る舞い */ block: 'start' /* 上揃えの位置にスクロールする */ }); ここでいう element (または window )がスムーススクロールで辿り着きたい対象となります。 また、この behavior: "smooth" は直接スクロール位置の座標を指定する scrollTo() メソッドでも利用することができます。 window.scrollTo({ behavior: "smooth", /* CSSで指定したものと同じ振る舞い */ top: 0 /* topからの位置が0 → ページトップへのスクロール */ }); /* 余談:scrollIntoView()で書きたい場合はelementを「document.body」とするとよいらしい document.body.scrollIntoView({ behavior: "smooth", block: 'start' }); */ どちらのメソッドであってもCSSで scroll-behavior: smooth; とした際と全く同じブラウザ側で定められた動きのスムーススクロールが行われます。 実用例 例えば冒頭のjQueryによる記載は以下のように置換できます。 const anchors = document.querySelectorAll('a[href^="#"]'); // 「#」で始まるアンカーリンクすべてを取得 anchors.forEach(anchor => { // アンカーリンクそれぞれにクリック時のイベントを設定 anchor.addEventListener('click', (e) => { e.preventDefault(); // 本来のアンカーリンク挙動によるスクロールをキャンセル const href = anchor.getAttribute('href'); if ( href === '#' || href == "" ) { // hrefが「#」のみまたは空=ページトップに戻る場合 window.scrollTo({ behavior: "smooth", // スムーススクロール top: 0 // ページトップ }); } else { // hrefが「#◯◯◯」の場合 const target = document.querySelector(href); if (target) { // 「#◯◯◯」のID=アンカーリンク先が見つかった場合、その位置にスクロール target.scrollIntoView({ behavior: 'smooth', // スムーススクロール block: 'start' // 上端合わせ }); } } }); }); また繰り返すようですが behavior: "smooth" によるスムーススクロールはCSSと同じくブラウザ側でスクロール挙動を行うため、 scroll-margin-top のようなCSSによるスクロール位置の調整も問題なく適用され(てしまい)ます。 シンプルな固定ヘッダー分だけずらすようであれば以下のようなクラスを用意しておけばわざわざJSでヘッダーの高さを取得してスクロール位置を計算する……などの処理は必要ありません。(固定ヘッダーの高さが幅によって可変する場合は、きちんとJSで計算する必要がありますが……) .anchor-target { scroll-margin-top: 100px; /* ヘッダーの高さ分スクロール位置をずらす */ } ※上記CSSの適用には :target 疑似要素を使う方法もありますが、今回は e.preventDefault(); によって本来のアンカーリンク挙動をキャンセルしているため、スクロールの際 # で始まるハッシュ(URLフラグメント)が付かない= :target 要素が機能しません。そのため仮に .anchor-target というクラスをアンカーリンクさせたいID位置に付ける想定をしています。 (他にルート相対 /#◯◯◯ で始まる場合などの考慮も場合によっては必要ですが)あくまでページ内アンカーリンクのスムーススクロール化であれば上記のJSで事足りるかと思います。 便利なプロパティほど思わぬところや実装が進んだところでのつまづきに繋がるので、スムーススクロール「だけ」の機能であっても正しく・意図した挙動になるよう心がけて実装しましょう。