マークダウンで文章を書けるのが結構好きで、普段使いのメモアプリはInkdropを使っています。
このサイトはWordpressなんですが、マークダウンで記事を書いていきたいのと、新しいこと学びたいってことでGatsby使ってブログサイトを構築してみました。
チュートリアルざっとやっただけで無機質だけど、今後デザイン入れたりプラグインいれてカスタマイズしていきたいです。
マークダウンで文章を書けるのが結構好きで、普段使いのメモアプリはInkdropを使っています。
このサイトはWordpressなんですが、マークダウンで記事を書いていきたいのと、新しいこと学びたいってことでGatsby使ってブログサイトを構築してみました。
チュートリアルざっとやっただけで無機質だけど、今後デザイン入れたりプラグインいれてカスタマイズしていきたいです。
Railsではフォームやリンクを生成するヘルパーを使う時、共通して data-confrm
という属性をつけると、確認アラートを表示することができました。しかし、Rails7においてはこれだけでは動きませんでした。
そもそも、どのようにこの動作を実現しているかといえば、 rails-ujs
というgem(中身はjs)のおかげです。rails-ujs
はrails5.1.0以降、rails本体に取り込まれました。
https://github.com/rails/rails/tree/main/actionview/app/assets/javascripts
rails-ujs
の役割として、confirmの制御、非getリクエストの実行、非同期リクエストの実行、サブミットボタンの非活性制御を行うライブラリとなっています。railsの便利機能を実現するためのフロントエンドで動く小さなjavascriptです。
Rails7においては、このgemはデフォルトでインストールされていません。つまり rails-ujs
を単体でインストールすれば動くということです。
npm install @rails/ujs --save
実態としては小さなjsであるため、npm経由でインストール可能です。このjsをassetsパイプライン経由などで読み込ませることで今までの開発体験を取り戻すことができます。
しかし、なぜRails7ではデフォルトでインストールされていないか、そこを知っておく必要があるでしょう。
Rails7では、新しいフロントエンドフレームワークとして、TurboとStimlus(2つ合わせてhotwiredと呼ばれます)が導入されました。Turbo自体は rails-ujs
の置き換えという立場ではありませんが、rails-ujs
の機能を一部取り込んでいます。それが今回、data-confirm
が動作しな理由の一つです。
Turboを使用する場合、data-confirm
の代わりに data-turbo-confirm
を使用します。
form_tag "#action", method: "post" ,data: { turbo_confirm: "送信しますか?" }
そしてこの属性は常にformタグに設定する必要があります。これが一癖。例えば、form
を生成するbutton_to
の場合は、下記のように指定する必要があります。
= button_to "削除", "#button", method: "delete", form: { data: { turbo_confirm: "削除しますか?" } }
こちらも data-turbo_confirm
に置き換えです。
data-turbo_method
を指定すると、data-turbo_confirm
が有効になるようです。
= link_to "削除リンク", "#button", data: { turbo_method: "get", turbo_confirm: "削除しますか?" }
[css/js]bundling-railsの使用を前提としているため、開発環境のサーバー立ち上げは ./bin/dev
を使用します。今回の環境の構成は下記の通りです。
Rails 7.0.3.3 / Ruby 3.0.0 / Bundler 2.3.9 / Nodejs 12 / MySQL 8.0
FROM ruby:3.0.0-buster
# for nodejs
RUN curl -fsSL https://deb.nodesource.com/setup_12.x | bash -
# for yarn
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt-get update -qq && apt-get install -y build-essential nodejs yarn vim zlib1g-dev liblzma-dev patch
RUN node -v
RUN yarn -v
RUN gem uninstall bundler
RUN gem install bundler -v 2.3.9
RUN mkdir /tmp/app-init
WORKDIR /tmp/app-init
ADD Gemfile .
ADD Gemfile.lock .
RUN bundle install
RUN mkdir /app
WORKDIR /app
services:
mysql:
image: mysql:8.0
restart: always
cap_add:
- SYS_NICE
environment:
TZ: Asia/Tokyo
MYSQL_ROOT_PASSWORD: password
volumes:
- ./mysql-confd:/etc/mysql/conf.d
rails:
build: .
tty: true
command: >
sh -c "
rails db:create && rails db:migrate &&
rm -f tmp/pids/server.pid &&
yarn install
./bin/dev
"
environment:
TZ: Asia/Tokyo
NODE_ENV: development
RAILS_ENV: development
volumes:
- .:/app
ports:
- "3000:3000"
depends_on:
- mysql
[mysqld]
default_authentication_plugin=mysql_native_password
web: bin/rails server -p 3000 -b 0.0.0.0
js: yarn build --watch
css: yarn build:css --watch
default: &default
adapter: mysql2
encoding: utf8mb4
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: root
password: password
host: mysql
ruby-3.0.0の公式イメージをベースとして、nodejs12、yarn、その他サーバーライブラリのインストールを行い、bundle installを実行してイメージ化しておきます。
こうするとbundle installをキャッシュしてくれるので起動が早いはずです。
mysql8.0コンテナと、railsコンテナの2つを実行します。
mysqlコンテナは、mysql-confd/default_authentication.cnf
に設置した設定ファイルの入ったディレクトリをコンテナ内にマウントします。設定ファイルは、パスワード認証を許可する設定が書かれています。
railsコンテナは、db:create db:migrate
を実行したのち、./bin/dev
を実行します。このとき、tty: true
オプションをつけなれけばならないようです。
Dockerコンテナの外からHTTPアクセスできるようにする必要があります。Railsサーバーの起動オプションに -b 0.0.0.0
を追加することで、127.0.0.1
/localhost
以外からのアクセスを許可します。
コンテナtoコンテナのホスト名はdocker-compose.ymlで定義した名前でアクセスできるようになります。host
にmysql
を指定します。
docker-compose build
docker-compose up
Gemfileの追加があったら、buildとupをやり直します。(startではボリュームが作り直されない)
時間を相当無駄にした。
tailwind.config.jsのcontentを確認してみましょう。
'./app/views/**/*.html.erb'
↓
'./app/views/**/*.html.slim'
言い訳だけど、知ってたんです。本当に。
知ってたのに思い出せなかった。
Rails7では、jsをバンドルせずにimportmapでjsを直接読み込む方式がデフォルトとなりました。しかしwebpack等でのバンドル方式の選択肢を完全に失ったわけではありません。特定のターゲット(ES5など)にトランスパイルすることができます。レガシーブラウザをサポートする要件下ではバンドル方式を選択することもあるでしょう。そこで来たるRails7時代に、我々アプリケーションエンジニアにとってどのような手段があるのか、Railsの基礎知識をアップデートしていくのがこの記事の目的になります。早速始めましょう!
かつてのRailsではSprocketsでバンドルして単一のjsファイルとして配信する方法が常識でした。Sprocketsはトランスパイルはしないものの、minifyする仕組みを持っていました。
Rails7からは、importmapまたはwebpack/esbuild/rollupでのバンドルの方法を採用できます。rails newする際にオプションを明示しない限り、デフォルトでimportmapがが使用されます。railsとimportmapの組み合わせはimportmap-railsというGemが受け持ちます。もしくは、-j [webpack/esbuild/rollup]
を指定すると、jsbundling-railsというGemがよしなにしてくれます。
importmapはトランスパイルを行いませんので、最新のブラウザをターゲットとする場合以外には採用が難しいかもしれません。その場合はwebpackやesbuildなどのバンドラーでES6やES5などをターゲートとしてトランスパイルしたものを配信する式を採用するのが良いでしょう。
importmapを使用するには、bundlerでimportmap-railsをインストールし、コマンドを実行します。
./bin/rails importmap:install
サードパーティのESModuleを追加するには、コマンドを実行します。Railsの場合、app/javascript/application.jsもまた、sprocketsでビルドされたものをimportmap経由でロードされます。
./bin/importmap pin react react-dom
レイアウトやビューファイルではjavascript_importmap_tagsヘルパーがjsを呼び出す起点となります。
<%= javascript_importmap_tags %>
jsbundlingを使用するには、bundlerでjsbundling-railsをインストールし、インストールコマンドを実行します。
./bin/rails javascript:install:[esbuild|rollup|webpack]
注)npx -v
が7.1以下の場合はpackage.json
にbuild設定が自動で追加されないので、手動で追記する必要があります。
"scripts": {
"build": "esbuild app/javascript/*.* --bundle --outdir=app/assets/builds"
}
サードパーティのESModuleを追加するには、yarnやnpmコマンドでプロジェクトルートにインストールしたライブラリをimportして使用します。
yarn add react react-dom
開発中には、./bin/dev
コマンドを使用することで、RailsサーバーとJSビルドウォッチャーの両方を同時に起動できます。cssbundling-railsを使用している場合、cssビルドウォッチャーも同時に起動します。事前にgemをインストールしておきましょう。(foremanがなければ自動でgemがインストールされます)
gem install foreman
esbuildの場合、デフォルトのターゲットはesnext(es6)です。変更したい場合は、package.json
のbuildコマンドでtargetを指定できます。
webpackの場合はwebpack.config.json
、rollupの場合はrollup.config.js
でビルド設定を行うことができます。
esbuildでのトランスパイルは、ターゲットがデフォルトでesnextのため、es6をターゲットにしておくとよさそうでした。なお、esbuildではes5向けのトランスパイルをサポートしていないため、IEをターゲットとする場合はwebpackを使うとよさそうです。
"build": "esbuild app/javascript/*.* --target=es6 --bundle --outdir=app/assets/builds",
使い慣れたSprocketsのアセットパイプラインを使用できます。Rails7では加えてバンドルの必要なTailwind CSS, Bootstrap, Bulma, PostCSS,Dart Sassなどを組み込むためにcssbundling-railsというGemを使用できます。cssbundling-railsでバンドルされたCSSはassets/builds
に出力され、アセットパイプラインを介して配信されます。
cssbundling-railsを使用するには、bundlerでcssbundling-railsをインストールし、コマンドを実行します。
./bin/rails css:install:[tailwind|bootstrap|bulma|postcss|sass]
注)npx -v
が7.1以下の場合はpackage.json
にbuild設定が自動で追加されないので、手動で追記する必要があります。
"scripts": {
"tailwindcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css"
}
開発中には、./bin/dev
コマンドを使用することで、RailsサーバーとCSSビルドウォッチャーの両方を同時に起動できます。jsbundling-railsを使用している場合、jsビルドウォッチャーも同時に起動します。事前にforeman gemをインストールしておきましょう。(foremanがなければ自動でgemがインストールされます)
tailwindcssを導入し@importを使用して別のcssファイルをバンドルするには、postcss-importをyarn add
します。詳細はこちら。
Rails7からWebpack等のBundlerを使用しないアプリケーション開発手法が主軸となるようです。その中でESModuleをブラウザ上で直接フェッチするような形がとられています。ImportMapについて学びましょう!
ImportMapとは、import句でモジュールを取得する際の取得先URLを制御する仕組みです。webpackやrollupなどのバンドラーを使用せずにESModuleを読み込むことのできるブラウザ環境で使用します。
import moment from "moment";
import { partition } from "lodash";
バンドラーを使用しない環境では上記のような記載をした場合エラーをスローします。それを回避するにはURLを指定する必要があるでしょう。
import moment from "/node_modules/moment/src/moment.js";
import { partition } from "/node_modules/lodash-es/lodash.js";
そこでImportMapを提供することで、モジュール名を指定するだけでどこから取得すべきかをマッピングすることができます。
<script type="importmap">
{
"imports": {
"moment": "/node_modules/moment/src/moment.js",
"lodash": "/node_modules/lodash-es/lodash.js"
}
}
</script>
Rails7ではconfig/importmap.rbの設定ファイルを介してImportMapの設定を行います。
Rails.application.importmap do
end
ImportMapは<%= javascript_importmap_tags%>を介して<head>タグに挿入されます。
<%= javascript_importmap_tags%>
これにより、application.jsなどのjavascriptからモジュールを参照できるようになります。application.jsもまた、これを介してインポートされます。
Rails7ではES Module Shimsをインポートします。これによりESModule非対応ブラウザの場合でも動作が保証されます。
インストールコマンドでimportmapを使う準備を行います。
$ ./bin/rails importmap:install
Add Importmap include tags in application layout
insert app/views/layouts/application.html.erb
Create application.js module as entrypoint
create app/javascript/application.js
Ensure JavaScript files are in the Sprocket manifest
append app/assets/config/manifest.js
Configure importmap paths in config/importmap.rb
create config/importmap.rb
Copying binstub
create bin/importmap
例えばvueを使いたい場合、importmap pinコマンドでマッピングします。自動的に2.6.14がマッピングされました。
$ ./bin/importmap pin vue
Pinning "vue" to https://ga.jspm.io/npm:vue@2.6.14/dist/vue.runtime.esm.js
しかし、自動でマッピングされるvueはブラウザでは動作しないものでしたので、browser esm互換のものへ手動で書き換えました。
pin "application"
pin "vue", to: "https://ga.jspm.io/npm:vue@2.6.14/dist/dist/vue.esm.browser.js"
実際に動かしてみる。
import Vue from 'vue'
var vm = new Vue({
el: '#example',
data: {
domain: 'HAZM.JP'
}
})
<div id="example">
<h1>{{domain}}</h1>
</div>
出力結果
importmapがhead内に提供され、vueがマッピングされた場所からインポートされたこと、そして動作を確認できました。
HTTP/2とES6がほとんどのブラウザで標準サポートされるようになった現代では、トランスパイルをせずともあらゆるJSを動作させることができました。importmapはその一端を担います。
Webカメラとマイクを利用するアプリケーションの開発で複数の端末で動作検証をしたいと思いました。
しかし、WebカメラとマイクはlocalhostまたはSSL化されたサイトでしか動作できないというブラウザ仕様があるため、localhost:3000で別のPCで動作するRailsアプリケーションにアクセスできるようにする必要があります。
ローカルで起動したRailsアプリケーションにアクセスする分には、localhost:3000でアクセス可能ですが、/etc/hostsにドメインを定義する方法では、localhostでアクセスする事ができません。
では、どうすれば良いか。Nginxでプロキシすることで、それが可能です。
Railsアプリケーションを起動しているPC:Ubuntu
アクセスしたいPC:Mac
まず、接続元であるPCにnginxをインストールし、nginxの設定ファイルでserverを定義します。設定内容はシンプルで、server_nameがlocalhostである3000番ポートをリッスンするserverで、別PCのローカルIPへproxyするというものです。
server {
listen 3000;
server_name localhost;
proxy_set_header X-Forwarded-Host $http_host;
location / {
proxy_pass http://192.168.1.12:3000;
}
}
注意点として、RailsはCSRF対策の仕組みがデフォルトで有効なため、適宜必要なヘッダを設定してあげる必要があります。
# プロキシ先にHostを知らせます。この設定が無いと、originと一致しないためCSRFチェックがエラーになります。
proxy_set_header X-Forwarded-Host $http_host;
以上!
今後、エンジニアとして社会で活躍していくには、より広い視野を持ってシステム全体を俯瞰して見る必要があると考え、インフラ面にも手を広げていこうと考えています。
業務ではAWSを使っていて、部分的な改修・調整作業は稀に行う事があるものの、必要な情報を集めて、その場凌ぎの対応することが多いです。また、システムの全体図は頭に入っているけれどそれらがどのような設定で組み合わさっているかまでは把握できていない事があります。
インフラ面の知識を叩き込めていないが故に、その領域の提案ができない事がもどかしい、とも感じます。
ということで、AWSの各種サービスがどのようなソリューションを提供し、何ができるのかを1つづつ知って行こう、という目的を持って、Daily AWSというTwitterアカウントを作りました。
このアカウントの目的は、AWSの各種サービスで何が実現できるのかを知ることです。それによって、プログラム側だけで解決しようとするのではなく、インフラ側で解決するという新しい提案ができるようになることを最終目的としています。
ツイートする前にやっていることは、ドキュメントを読み込んで、インスタンスを立ててみることです。一通り触ってみると、頭に入ってきて自分なりに解釈できるからです。本当は一つ一つの設定の挙動を検証したり、というところまでできれば良いですが、時間的な制約が大きいのと、毎日継続することを第2目標としているため、一定の解釈を持ってツイートに流しています。
継続できるようにがんばります。笑
業務案件で全文検索を使うことになった。とはいえ全文検索について全く知識がなかったのでここでインプットしておこうということで、いろいろ調べてみた。
コンピュータにおいて、複数の文書から特定の文字列を検索すること。複数文書にまたがって文書に含まれる全文を対象とした検索。
インデックスを作成するために検索対象の文書を解析・分割を行う。その手法として主に下記2手法がある。
全文検索技術
↓
全文/文検/検索/索技/技術/術
MySQLでは、バージョン5.6.4 より全文検索をサポートしている。しかし、これは空白で単語が区切られるラテン語ベースの言語に対してのみ有効である。
これに対し、バージョン5.7.6 よりCJK(日韓中文字)で使用できるn-gramパーサ がサポートされました。(組み込みのサーバープラグイン。サーバーの起動時に自動的にロードされる。)
同タイミングで MeCab全文パーサプラグイン も提供されていて、形態素解析によるインデクシングも可能。(RDSだと使えないらしい?要調査)
プロパティ | 説明 | 値 | 備考 |
innodb_ft_min_token_size | 単語の最小長 | 0~16 デフォルト3 | |
innodb_ft_max_token_size | 単語の最大長 | 10~84 デフォルト10 | |
ft_min_word_len | myIsamの場合はこれ(後で調べよう) | ||
ft_max_word_len | myIsamの場合はこれ(後で調べよう) | ||
ngram_token_size | n-gramパーサを使用する際の分割文字数 | デフォルト2 | |
innodb_ft_sort_pll_degree | インデックスを作成するスレッド数 | 1~32 デフォルト2 | 大きなテーブルにFULLTEXTインデックスを作成するときは、スレッドの数を増やすことを検討 |
innodb_ft_cache_size | 補助インデックステーブルごとのキャッシュ容量 | デフォルト 8000000bytes |
InnoDB FULLTEXTインデックスを作成すると、FTS_から始まるインデックステーブルのセットが作成される。これらを転置インデックスという。
ドキュメントがインサートされると、FULLTEXT INDEXにもインサートが実行される。小さなドキュメントの場合でもインサートアクセスが実行されることになり、補助インデックステーブルへのアクセスが競合する場合があります。これを回避するために、最近インサートされた行の挿入はキャッシュされる。キャッシュ領域がいっぱいになると、ディスクに書き込まれまれる。これにより競合を防いでいる。Innodb_ft_cache_sizeはこのキャッシュ容量を設定できる。
FULLTEXT INDEX列をもつレコードを削除すると、補助インデックステーブルへの同時アクセスで競合が発生する場合があります。これを防ぐために削除されたドキュメントIDをFTS_*_DELETED
テーブルに記録する。クエリ結果を返す前に、FTS_*_DELETED
テーブルの情報を使用して削除されたドキュメントIDを除外する。補助インデックステーブルの値は保持されるため容量は減らない。補助インデックステーブルの値を削除するには、OPTIMIZE TABLE
句を用いて実行できる。
一部引用元:
https://ja.wikipedia.org/wiki/%E5%85%A8%E6%96%87%E6%A4%9C%E7%B4%A2
https://dev.mysql.com/doc/refman/5.7/en/fulltext-search.html
友人と作っているWebプロダクトがあります。このサービスは特定のジャンルの人同士の友達探しをサポートするサービスであり、クローズドなやりとりが多く、サービスが盛り上がっているのかどうか?どれくらいの頻度でユーザ登録があるか?といったデータが把握しにくいという状況でした。1ヶ月前に公開したばかりのサービスでもあり管理画面を実装するほどでもないと思ったので、より簡単な方法を模索しました。友人との連絡はLINEグループを使用しています。そのため今回はLINEグループに通知をする方針としました。
個人開発は、いかに工数をかけずに実装するか?という観点を重要視しています。そこで記憶の片隅にあったIFTTT(イフト)を活用することにしました。
幸いなことに、IFTTTはLINE Notifyというサービスと簡単に連携することができます。LINE Notifyは様々なサービスからの通知を受信して、端末にメッセージ等を送信してくれるサービスです。このサービスの公式アカウントをLINEグループに招待することで、LINEグループへメッセージ通知を流し込むことが可能です。
さて、Webアプリケーションでユーザ通知が行われた場合、どのようにIFTTTに通知するでしょうか。最も簡単な方法は、Webhookでしょう。よって構成は下記の通りになります。
早速始めましょう。IFTTT
IFTTTでレシピを作成します。
IFTTTは「もし何かがあったら」「その時何をする」をセットで設定します。今回は「Webhookを受信したら」「LINE Notifyで通知する」という設定を行います。
If Thisを選択して、Webhookを設定します。
次に、Then Thatをクリックし、LINEを選択します。
初回はLINEアカウント連携に飛ばされると思うので、連携してください。Send messageのRecipientで、送信先のグループ名を選択します。ここで、「1:1でLINE Notifyから通知を受け取る」を選択すれば、自分だけに届くはずです。
Value1,2,3はプレースホルダです。これらの値はWebhookリクエスト毎に自由に設定でき、LINEにはその値で置き換えられたメッセージが送信されます。
これで設定完了です。
次に、WebアプリケーションからIFTTTにWebhookリクエストを投げる必要があります。事前に、エンドポイントのURLを取得しましょう。右上アイコン>My services>Webhooks>右上 Documents を選択しましょう。
WebアプリケーションからWebhookリクエストを送信する部分を実装します。
# LINE通知
# value1 ユーザ名
# value2 登録日時
# value3 URL
begin
uri = URI.parse(IFTTTから取得したエンドポイント)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
params = { value1: 値1, value2: 値2, value3: 値3}
headers = { "Content-Type" => "application/json" }
response = http.post(uri.path, params.to_json, headers)
ap response
rescue => e
Raven.capture_exception(e)
end
最後に、お手持ちのスマホのLINEでLINE Notifyを友達追加したあと、グループに招待しましょう。
喜んでもらえて、エンジニア冥利に尽きますね!
IFTTTを使うことで、Webhookを契機にLINE通知を行う仕組みを簡単に実現することができました。
IFTTTはLINEの他、Slackなど様々なサービスと連携することができます。活用方法によっては実装なしに便利な自動化ができるようになることでしょう。
個人的には、IFTTTを使った自動化を体験できたのは知見が広がったという意味でよかったかなと思います。それでは。