Form Objectって何。

PF進捗報告会お疲れ様でした〜映画の予告編共有、コンビニ飯レコメンド、乃木坂46の推しメンレコメンド、みなさん個性的だったな〜参考になるコードを散見したから自分のPF作成に参考にさせていくことにします〜

さて、今回はFormObjectについて取り扱っていこうと思う。

FormObjectとは

>単一のフォーム送信で複数の ActiveRecord モデルを更新したい場合に、その永続化ロジックをカプセル化できるデザインパターン

 

ActiveModel::Model というモジュールを include することで利用できる。

永続化ロジックとは

>dbに保存するかしないかを判断するロジックのこと。DBに保存することを永続化させると言う。

ActiveModelとは

>Activerecordを継承しないクラスでもActiveRecordと同じような便利メソッドが使えるようになる優れもの。

 

Ruby におけるObject Relation Mapping(ORM)

FormObjectを使用するメリット

単一のフォーム送信を作成する時にFormObjectを使わない場合、validationに必要なコードが多くなってしまう。

一方で、FormObjectを作成した場合、モデルを作成した場合とほとんど同じような記述で事足りるようになる。そのおかげで、可読性が向上する。

具体的には、formobjectを作成したいモデルに、下記コードを付け加えてあげる。

class SearchArticlesForm
  include ActiveModel::Model
  include ActiveModel::Attributes

例えばsignup_form.rbを作成したい場合は下記のようにかく。

class SignupForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  include Nickname
  include Email
  include CryptedPassword

  attr_reader :user

  attribute :nickname, :string
  attribute :email, :string
  attribute :password, :string
  attribute :password_confirmation, :string

バリデーションを作成(dbにdataを保存しない)

Form Objectを使用しない場合


class FeedbacksController < ApplicationController
  def new
  end

  def create
    if params[:title].present? && params[:body].present?
      AdminMailer.feedback(params[:title], params[:body]).deliver_later
      redirect_to home_path, notice: 'フィードバックを送信しました'
    else
      @error_messages = []
      @error_messages << 'タイトルを入力してください' if params[:title].blank?
      @error_messages << '本文を入力してください' if params[:body].blank?
      render :new
    end
  end
end

<%= form_with url: feedbacks_path, local: true do %>
  <% @error_messages && @error_messages.each do |message| %>
    <%= message %>
  <% end %>
  <%= label_tag :title %>
  <%= text_field_tag :title, params[:title] %>
  <%= label_tag :body %>
  <%= text_area_tag :body, params[:body] %>
  <%= submit_tag %>
<% end %>

Form Objectを使用した場合

class Feedback
  include ActiveModel::Model ・・・①

  attr_accessor :title, :body ・・・②

  validates :title, :body, presence: true ・・・②

  def save
    return false if invalid?
    AdminMailer.feedback(title, body).deliver_later
    true
  end
end

①ActiveModelを継承することで、validatesを使うことができる。

②セッターとゲッターを一気に定義している

③ @error_messages << 'タイトルを入力してください' if params[:title].blank?と

@error_messages << '本文を入力してください' if params[:body].blank?

を短縮させている。


class FeedbacksController < ApplicationController
  def new
    @feedback = Feedback.new
  end

  def create
    @feedback = Feedback.new(feedback_params)
    if @feedback.save
      redirect_to home_path, notice: 'フィードバックを送信しました'
    else
      render :new
    end
  end

  private

  def feedback_params
    params.require(:feedback).permit(:title, :body)
  end
end
<%= form_with model: @feedback, local: true do |f| %>
  <% if @feedback.errors.any? %>
    <% @feedback.errors.full_messages.each do |message| %>
      <%= message %>
    <% end %>
  <% end %>
  <%= f.label :title %>
  <%= f.text_field :title %>
  <%= f.label :body %>
  <%= f.text_area :body %>
  <%= f.submit %>
<% end %>

ActiveRecordを継承させることで、永続化ロジックをスッキリ書くことができた。

 
  @error_messages = []
  @error_messages << 'タイトルを入力してください' if params[:title].blank?
  @error_messages << '本文を入力してください' if params[:body].blank?
    
    //下記記述だけで上記コードの役割を果たす。
    validates :title, :body, presence: true

form objectを使ってみよう - メドピア開発者ブログ

検索機能の追加 - yoshi14m3185's blog

Rails Design Patterns: Form Object

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社

GitHub - solnic/virtus: [DISCONTINUED ] Attributes on Steroids for Plain Old Ruby Objects

axiosを理解するためのPromise,async,await学習

axiosのthenやcatchがわからない。そしてPromiseやasync/awaitとかも出てきてしまうから余計に頭が混同してまう。

今回は、ちょっと一旦その単語調べさせて〜と半べそかきながら参考文献を読み漁って脳内パッチワークをしたログになります。#いい歳して半べそかくな。

axiosとは

axiosとはブラウザやnode.js上で動くPromiseベースのHTTPクライアントです。非同期にHTTP通信を行いたいときに容易に実装できます。

でた、Promise。HTTPクライアントってことは、サーバーにリクエストをなげる何かってことなのかな。

どうやって使うのか

そうやらgetとpostでサーバーに対してリクエストを投げられるらしい。

GET通信

GET通信はaxios.getメソッドを使用する。

第一引数にパラメータ付きのURLを指定し、

.then()で通信が成功した際の処理を書いている。

.catch()はエラーにの処理を書く。

.finally()は通信が成功しても失敗しても常に実行される。

axios
  .get('/user?ID=12345')
  .then(function (response) {    // handle success  
    console.log(response);
  })
  .catch(function (error) {    // handle error  
  console.log(error);
})
.finally(function () {    // always executed
});

POST通信

POST通信はaxios.postメソッドを使用する。

axios
  .post('/user', {  firstName: 'subaru',  lastName: 'nakano'})
  .then(function (response) {  console.log(response);})
  .catch(function (error) {  console.log(error);
});

第2引数に送信したいデータを指定することでPOST送信できる。

postアクションが行われたときに、/userパスに第二引数が渡されるようになっている。

例えばログインアクションを実装したい時に、postは以下のように使う。

Vuexに入れているからmutationに対して変更をcommitする流れを記述している。

const actions = {
  loginUser({ commit }, user) {# ...1
    axios
      .post('sessions', user) # ...2
      .then((res) => {#...3
        commit('setAuthUser', res.data);#...4
        router.push({ name: 'PreliquoTop' });
        alert('ログインに成功しました');
      })
      .catch((err) => console.log(err));
  }
  1. 第一引数にcommitを取り、第二引数のデータをmutationにコミットできるようにしている。
  2. sessionsコントローラーのpostアクション(createアクション)に対して、userをpostしている。
  3. Promiseのresolveの省略形。成功したときの処理を書いている。。
  4. postアクションが成功したら、そのセッションデータがsetAuthUserに渡るようになっている

Promiseって何

処理の順序に「お約束」を取り付けることができるもの、処理を待機することや、その結果に応じて次の処理をすることお約束するもの。

Promiseでは、処理されている状態を表す三つのメソッドがある。

  • pending未解決 (処理が終わるのを待っている状態)
  • resolved解決済み (処理が終わり、無事成功した状態)
  • rejected拒否 (処理が失敗に終わってしまった状態)

Promiseの基本形は下記のように書く。

function 非同期的な関数(成功時コールバック, 失敗時コールバック) {
  if (...) {
    成功時コールバック(成果)
  } else {
    失敗時コールバック(問題)
  }
}

//          ↓executor
new Promise(function (resolve, reject) {
  非同期的な関数(
    (成果) => resolve(成果), // 成功時コールバック関数
    (問題) => reject(問題),  // 失敗時コールバック関数
  )
})

ここで使用されているPromiseインスタンスを抜粋すると、下記のような書き方になっている。

const promise = new Promise((resolve, reject) => {});

{}の中身がresolveした場合は、.thenの中身が実行され、rejectされた場合は.catchの中身が実行される。

async/await

asyncとは

asyncは非同期関数を定義する関数宣言であり、関数の頭につけることで、Promiseオブジェクトを返す関数にすることができます

非同期関数を定義する関数宣言のこと。

以下のように関数の前にasyncを宣言することにより、非同期関数(async function)を定義できる。

async function sample() {}

awaitとは

awaitは、Promiseオブジェクトが値を返すのを待つ演算子です。await必ず、async function内で使います

async function内でPromiseの結果(resolve、reject)が返されるまで待機する(処理を一時停止する)演算子のこと

以下のように、関数の前にawaitを指定すると、その関数のPromiseの結果が返されるまで待機する。

async function sample() {
    const result = await sampleResolve();

    // sampleResolve()のPromiseの結果が返ってくるまで以下は実行されない
    console.log(result);
}

まとめると、

  • await → Promiseの値が取り出されるまで待つ。
  • async → awaitキーワードを使っている関数のあたまに付ける必要がある。
  • asyncとawaitを使うと、.thenと.catchを省略できるのでコードがスッキリする。

比較

async/awaitを使った場合と使わなかった場合の非同期処理を比較してみる。

#async/awaitを使わなかった場合。
function getServerStatusCode() {
    return new Promise(function(resolve, reject) {
        axios
            .get("<https://httpbin.org/status/200>")
            .then(response => resolve(response.status))
            .catch(error => reject(error.response.status))
    });
}

getServerStatusCode()
    .then(statusCode => console.log("生きてる", statusCode))
    .catch(statusCode => console.error("死んでる", statusCode))

#async/awaitを使った場合。
async function getServerStatusCode() {
  try {
    return (await axios.get("<https://httpbin.org/status/500>")).status
  } catch (error) {
    throw error.response.status
  }
}

getServerStatusCode()
    .then(statusCode => console.log("生きてる", statusCode))
    .catch(statusCode => console.error("死んでる", statusCode))

チェーンさせることもできる

.thenを組み合わせることで、「1番目の処理が成功したら、2番目の処理を実行させる」といったことも可能になる。

const promise = new Promise((resolve, reject) => {
  resolve("ヤッホー");
})
  .then((val) => {
    console.log(`then1: ${val}`);
    return val;
  })
  .catch((val) => {
    console.log(`catch: ${val}`);
    return val;
  })
  .then((val) => {
    console.log(`then2: ${val}`);
  });
#出力結果
then1: ヤッホー
then2: ヤッホー

catchの中身はエラー時のみなので、今回はthen1とthen2が表示されている。

連続した非同期処理(async/await構文)

awaitを利用すれば、then()で処理を繋げなくても連続した非同期処理が書ける。

function sampleResolve(value) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(value);
        }, 1000);
    })
}

async function sample() {
    return await sampleResolve(10) * await sampleResolve(10) + await sampleResolve(20);
}

async function sample2() {
    const a = await sampleResolve(10);
    const b = await sampleResolve(10);
    const c = await sampleResolve(20);
    return a * b + c;
}

sample().then((v) => {
    console.log(v); // => 120
});

sample2().then((v) => {
    console.log(v); // => 120
});

まとめ

  • Promise構文を使うと、javascriptに処理する順序を与えることができる。
  • .thenを繋げるコードは綺麗じゃない。そんな時はasync/awaitを使う。
  • asyncが文頭にあるfunctionはPromiseのレスポンスが返るまでawait内の処理を実行しない。

ざっくり、Promiseを使うとconsole.logの順番を制御できるんだな〜と認識できた。axios.postした後の順番を制御するためにpromiseが使われているのかな?アプリ制作が終わる頃にはマスターしてたい!

アクション | Vuex

async/await 入門(JavaScript) - Qiita

Promise - JavaScript | MDN

【ES6】 JavaScript初心者でもわかるPromise講座 - Qiita

Promiseの使い方、それに代わるasync/awaitの使い方 - Qiita

async/await 入門(JavaScript) - Qiita

【Ajax】axiosを使って簡単にHTTP通信 | Will Style Inc.|神戸にあるウェブ制作会社

webpackerって何〜(後編)

Webpacker手強かった、、

でも倒せた!成長!ここ二日間すごい足踏みしたけど、

また明日からVueの学習に戻れるから気持ち新たに頑張るぞいっ#ぞい

webpackの概要

webpackとはウェブコンテンツを構成するファイルをまとめてしまうツールです。一番多い使い方は、複数のJavaScriptを1つにまとめることでしょう。複数のJavaScriptをまとめるのは、いろんな利点があります。

(参考文献: https://ics.media/entry/12140/)

webpackerはwebpackをrails仕様にしたもの。

モジュールが使える

複数のJSファイルを1つにまとめるだけなら他のツールでもできますが、webpackの場合は標準仕様のES Modulesが使えたり、node_modulesのモジュールを結合できるといったメリットがあります。

標準のES Modulesを使うと変数の競合やグローバル汚染を防げるので開発時の安全性が高まります。さらには、コードの可読性が上がり、開発作業の分担やテストがしやすくなり、再利用性や保守性があがります。

(参考文献: https://ics.media/entry/12140/)

 

 

エラーとの戦い(終結 \^^/)

結論

  • yarn.lockとpackage.jsonのバージョンは同じにした方がいい。
  • css-loaderやsass-loaderなどのloaderとwebpackのバージョンの互換性が正しいかyarn checkを使って確認した方がいい。
  • エラー文をコピペしてググる前に、エラー文をよく見て、どこが真因なのか検討をつけておくと、エラー解決に役立つ記事を見つけやすい。

所感

謎解き感を楽しめてよかった!今後はこまめにデプロイをしていく。

エラーログ

The engine "node" is incompatible with this module. Expected version "^12.22.0 || ^14.17.0 || >=16.0.0". Got "12.16.2"

12.22.0か14.17.0か16.0.0を使って欲しい。現在のバージョンは12.16.2になってる

remote: -----> Installing node-v12.16.2-linux-x64
remote: -----> Installing yarn-v1.22.4
remote: -----> Detecting rake tasks
remote: -----> Preparing app for Rails asset pipeline
remote:        Running: rake assets:precompile
remote:        yarn install v1.22.4
remote:        [1/4] Resolving packages...
remote:        [2/4] Fetching packages...
remote:        info fsevents@2.3.2: The platform "linux" is incompatible with this module.
remote:        info "fsevents@2.3.2" is an optional dependency and failed compatibility check. Excluding it from installation.
remote:        info fsevents@1.2.13: The platform "linux" is incompatible with this module.
remote:        info "fsevents@1.2.13" is an optional dependency and failed compatibility check. Excluding it from installation.
remote:        error eslint@8.0.0: 

remote:        error Found incompatible module.
remote:        info Visit <https://yarnpkg.com/en/docs/cli/install> for documentation about this command.
remote:        Compiling...
remote:        Compilation failed:
remote:        yarn run v1.22.4
remote:        info Visit <https://yarnpkg.com/en/docs/cli/run> for documentation about this command.
remote:        
remote:        
remote:        error Command "webpack" not found.
remote:        
remote: 
remote:  !
remote:  !     Precompiling assets failed.
remote:  !
remote:  !     Push rejected, failed to compile Ruby app.
remote: 
remote:  !     Push failed

デプロイ時のエラーをnode version変更で対処v14.17.0⇨v12.16.2

この記事を参考にして下記コマンドを実行したところ、先のエラーは解消できた。

heroku buildpacks:add --index 1 heroku/nodejs

次に引っかかったエラーは以下の通り。

Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.


remote:        Compilation failed:
remote:        [webpack-cli] Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.
remote:         - configuration.node should be one of these:
remote:           false | object { __dirname?, __filename?, global? }
remote:           -> Include polyfills or mocks for various node stuff.
remote:           Details:
remote:            * configuration.node has an unknown property 'dgram'. These properties are valid:
remote:              object { __dirname?, __filename?, global? }
remote:              -> Options object for node compatibility features.
remote:            * configuration.node has an unknown property 'fs'. These properties are valid:
remote:              object { __dirname?, __filename?, global? }
remote:              -> Options object for node compatibility features.
remote:            * configuration.node has an unknown property 'net'. These properties are valid:
remote:              object { __dirname?, __filename?, global? }
remote:              -> Options object for node compatibility features.
remote:            * configuration.node has an unknown property 'tls'. These properties are valid:
remote:              object { __dirname?, __filename?, global? }
remote:              -> Options object for node compatibility features.
remote:            * configuration.node has an unknown property 'child_process'. These properties are valid:
remote:              object { __dirname?, __filename?, global? }
remote:              -> Options object for node compatibility features.
remote:        
remote: 
remote:  !
remote:  !     Precompiling assets failed.
remote:  !
remote:  !     Push rejected, failed to compile Ruby app.
remote: 
remote:  !     Push failed

この記事を参考にして下記コードをconfig/webpack/development.jsに追記したら解決した。

#config/webpack/development.js

const environment = require('./environment')
const customConfig = {
    resolve: {
      fallback: {
        dgram: false,
        fs: false,
        net: false,
        tls: false,
        child_process: false
      }
    }
  };
  
  environment.config.delete('node.dgram')
  environment.config.delete('node.fs')
  environment.config.delete('node.net')
  environment.config.delete('node.tls')
  environment.config.delete('node.child_process')
  
  environment.config.merge(customConfig);

今度は下記エラーに当たる。

ERROR in application. Module not found: Error: Can't resolve 'babel-loader' in '/Users/subaru/Portfolio/Preliquo'


ERROR in application
Module not found: Error: Can't resolve 'babel-loader' in '/Users/subaru/Portfolio/Preliquo'
resolve 'babel-loader' in '/Users/subaru/Portfolio/Preliquo'
  Parsed request is a module
  using description file: /Users/subaru/Portfolio/Preliquo/package.json (relative path: .)
    resolve as module
      looking for modules in /Users/subaru/Portfolio/Preliquo/node_modules
        single file module
          using description file: /Users/subaru/Portfolio/Preliquo/package.json (relative path: ./node_modules/babel-loader)
            no extension
              /Users/subaru/Portfolio/Preliquo/node_modules/babel-loader doesn't exist
            .js
          
ERROR in hello_vue
Module not found: Error: Can't resolve 'babel-loader' in '/Users/subaru/Portfolio/Preliquo'
resolve 'babel-loader' in '/Users/subaru/Portfolio/Preliquo'
  Parsed request is a module
  using description file: /Users/subaru/Portfolio/Preliquo/package.json (relative path: .)
    resolve as module
      looking for modules in /Users/subaru/Portfolio/Preliquo/node_modules
        single file module
         
webpack 5.58.2 compiled with 2 errors in 159 ms

この記事を参考にして下記手順を順を追って実行した。

  1. node_modulesを削除
  2. yarn addを実行
  3. yarn add babel-loader
  4. yarn add style-loader
  5. yarn add css-loader
  6. yarn add file-loader

その後bin/webpackを実行したところエラーが消えた。

bin/webpack
(node:79201) [DEP_WEBPACK_MAIN_TEMPLATE_RENDER_MANIFEST] DeprecationWarning: MainTemplate.hooks.renderManifest is deprecated (use Compilation.hooks.renderManifest instead)
(Use `node --trace-deprecation ...` to show where the warning was created)
(node:79201) [DEP_WEBPACK_CHUNK_TEMPLATE_RENDER_MANIFEST] DeprecationWarning: ChunkTemplate.hooks.renderManifest is deprecated (use Compilation.hooks.renderManifest instead)
(node:79201) [DEP_WEBPACK_MAIN_TEMPLATE_HASH_FOR_CHUNK] DeprecationWarning: MainTemplate.hooks.hashForChunk is deprecated (use JavascriptModulesPlugin.getCompilationHooks().chunkHash instead)
(node:79201) [DEP_WEBPACK_COMPILATION_NORMAL_MODULE_LOADER_HOOK] DeprecationWarning: Compilation.hooks.normalModuleLoader was moved to NormalModule.getCompilationHooks(compilation).loader
(node:79201) [DEP_WEBPACK_CHUNK_MODULES_ITERABLE] DeprecationWarning: Chunk.modulesIterable: Use new ChunkGraph API
(node:79201) [DEP_WEBPACK_COMPILATION_ASSETS] DeprecationWarning: Compilation.assets will be frozen in future, all modifications are deprecated.
BREAKING CHANGE: No more changes should happen to Compilation.assets after sealing the Compilation.
        Do changes to assets earlier, e. g. in Compilation.hooks.processAssets.
        Make sure to select an appropriate stage from Compilation.PROCESS_ASSETS_STAGE_*.
assets by status 35 KiB [cached] 2 assets
assets by chunk 2.88 MiB (auxiliary name: hello_vue)
  assets by status 395 bytes [emitted]
    asset 7c3f6e0fddc471ce8015.woff2?v=6.2.95 100 bytes [emitted] [immutable] [from: node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff2?v=6.2.95] (auxiliary name: hello_vue)
    asset d21ae6085a4cb737be2c.woff?v=6.2.95 99 bytes [emitted] [immutable] [from: node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff?v=6.2.95] (auxiliary name: hello_vue)
    asset 31c7fca795826a24c2fb.ttf?v=6.2.95 98 bytes [emitted] [immutable] [from: node_modules/@mdi/font/fonts/materialdesignicons-webfont.ttf?v=6.2.95] (auxiliary name: hello_vue)
    asset 9c0cf49af7dc5d9ad039.eot 98 bytes [emitted] [immutable] [from: node_modules/@mdi/font/fonts/materialdesignicons-webfont.eot] (auxiliary name: hello_vue)
  assets by status 2.88 MiB [compared for emit]
    asset media/fonts/materialdesignicons-webfont-5126326b.eot 1.04 MiB [compared for emit] (auxiliary name: hello_vue)
    asset media/fonts/materialdesignicons-webfont-aa2aa539.ttf 1.04 MiB [compared for emit] (auxiliary name: hello_vue)
    asset media/fonts/materialdesignicons-webfont-ca644a1a.woff 482 KiB [compared for emit] (auxiliary name: hello_vue)
    asset media/fonts/materialdesignicons-webfont-9d0c9ec4.woff2 336 KiB [compared for emit] (auxiliary name: hello_vue)
asset js/hello_vue-98d9baf8b3d9ef940414.js 4.15 MiB [emitted] [immutable] (name: hello_vue) 1 related asset
asset manifest.json 857 bytes [emitted]
runtime modules 2.86 KiB 15 modules
javascript modules 4.07 MiB
  modules by path ./node_modules/ 4.04 MiB 45 modules
  modules by path ./app/frontend/ 38.9 KiB 28 modules
asset modules 355 bytes (javascript) 493 bytes (asset)
  modules by path ./node_modules/@mdi/font/fonts/*.eot 84 bytes (javascript) 196 bytes (asset)
    ./node_modules/@mdi/font/fonts/materialdesignicons-webfont.eot?v=6.2.95 42 bytes (javascript) 98 bytes (asset) [built] [code generated]
    ./node_modules/@mdi/font/fonts/materialdesignicons-webfont.eot 42 bytes (javascript) 98 bytes (asset) [built] [code generated]
  data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAGElEQVQYlWNgYGCQwoKxgqGgcJA5h3yFAAs8BRWVSwooAAAAAElFTkSuQmCC 145 bytes [built] [code generated]
  ./node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff2?v=6.2.95 42 bytes (javascript) 100 bytes (asset) [built] [code generated]
  ./node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff?v=6.2.95 42 bytes (javascript) 99 bytes (asset) [built] [code generated]
  ./node_modules/@mdi/font/fonts/materialdesignicons-webfont.ttf?v=6.2.95 42 bytes (javascript) 98 bytes (asset) [built] [code generated]
./node_modules/vee-validate/dist/locale/ja.json 1.07 KiB [built] [code generated]
webpack 5.58.2 compiled successfully in 6028 ms

今度は下記エラーが生じた。

TypeError: Cannot read property 'tap' of undefined

こちらの記事を参考にして下記手順を実行。

  1. package.jsonからwebpackを削除
  2. yarn.lockを削除
  3. node_modulesを削除
  4. yarn installを実行

さらに下記エラーが生じたので、こちらの記事を参考にしてファイルを編集。またしても下記エラーに直面。

     [5/5] Building fresh packages...
remote:        Done in 38.60s.
remote:        Compiling...
remote:        Compilation failed:
remote:        [webpack-cli] Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema.
remote:         - configuration.resolve has an unknown property 'fallback'. These properties are valid:
remote:           object { alias?, aliasFields?, cachePredicate?, cacheWithContext?, concord?, descriptionFiles?, enforceExtension?, enforceModuleExtension?, extensions?, fileSystem?, ignoreRootsErrors?, mainFields?, mainFiles?, moduleExtensions?, modules?, plugins?, preferAbsolute?, resolver?, roots?, symlinks?, unsafeCache?, useSyncFileSystemCalls? }
remote:           -> Options for the resolver

resolveにfallbackというプロパティは含まれていません。

こちらの記事には確かにfallbackに関する説明があることから、fallbackというプロパティは存在すると考えた。

よくよくみてみると、fallbackはwebpack5から導入されているプロパティだと気づいた。webpacknのバージョンを下記の通り5以上に変更。

 "dependencies": {
..omit..
    "webpack": "5.54.0",
    "webpack-cli": "4.8.0"
..omit..
}

今度は下記注意書きが発生。おそらくcompileには成功したみたい。


WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
Entrypoints:
  hello_vue (2.58 MiB)
      js/hello_vue-bf9a6870745ffdd2935a.js

WARNING in webpack performance recommendations: 
You can limit the size of your bundles by using import() or require.ensure to lazy load some parts of your application.
For more info visit <https://webpack.js.org/guides/code-splitting/>

それでは、デプロイしてみる。

Subarunookpuro3:Preliquo subaru$ git push heroku main:main
<--omit-->
Discovering process types
remote:        Procfile declares types     -> rails, webpack
remote:        Default types for buildpack -> console, rake, web
remote: 
remote: -----> Compressing...
remote:        Done: 140.8M
remote: -----> Launching...
remote:        Released v22
remote:        <https://preliquo.herokuapp.com/> deployed to Heroku
remote: 
remote: Verifying deploy... done.
To <https://git.heroku.com/preliquo.git>
   e7fae38..d86d40e  main -> main

今の感情

\ (^^) /\ (^^) /\ (^^) /\ (^^) /\ (^^) /\ (^^) /\ (^^) /\ (^^) /\ (^^) /\ (^^) /\ (^^) /\ (^^) /\ (^^) /

webpackerて何〜(前編)

今日中にエラーを討伐できなかったので前半後半に分けるという愚行をすることをお許しください。。

Herokuにデプロイを試みたときに下記エラーにハマりました。

yarn install v1.22.4
remote:        [1/4] Resolving packages...
remote:        [2/4] Fetching packages...
remote:        info fsevents@2.3.2: The platform "linux" is incompatible with this module.
remote:        info "fsevents@2.3.2" is an optional dependency and failed compatibility check. Excluding it from installation.
remote:        info fsevents@1.2.13: The platform "linux" is incompatible with this module.
remote:        info "fsevents@1.2.13" is an optional dependency and failed compatibility check. Excluding it from installation.
remote:        error eslint@8.0.0: The engine "node" is incompatible with this module. Expected version "^12.22.0 || ^14.17.0 || >=16.0.0". Got "12.16.2"
remote:        error Found incompatible module.
remote:        info Visit <https://yarnpkg.com/en/docs/cli/install> for documentation about this command.
remote:        Compiling...
remote:        Compilation failed:
remote:        yarn run v1.22.4
remote:        info Visit <https://yarnpkg.com/en/docs/cli/run> for documentation about this command.
remote:        
remote:        
remote:        error Command "webpack" not found.

ハマりましたというか、ハマっています。なので、解決はしていませんが、これを機にWebpackerを学んでみたいと思います。

追記: 上記エラーは下記コマンドで解決しました。(参考文献:https://www.bokukoko.info/entry/2017/12/07/000000)

heroku buildpacks:add --index 1 heroku/nodejs

がしかし、下記のようなエラーを吐き出してしまっています。API schemaにそぐわないオブジェクトをWebpackは使用して初期化してしまっていますね〜と吐いています。

Compilation failed:
remote:        [webpack-cli] Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.
remote:         - configuration.node should be one of these:
remote:           false | object { __dirname?, __filename?, global? }
remote:           -> Include polyfills or mocks for various node stuff.
remote:           Details:
remote:            * configuration.node has an unknown property 'dgram'. These properties are valid:
remote:              object { __dirname?, __filename?, global? }
remote:              -> Options object for node compatibility features.
remote:            * configuration.node has an unknown property 'fs'. These properties are valid:
remote:              object { __dirname?, __filename?, global? }
remote:              -> Options object for node compatibility features.
remote:            * configuration.node has an unknown property 'net'. These properties are valid:
remote:              object { __dirname?, __filename?, global? }
remote:              -> Options object for node compatibility features.
remote:            * configuration.node has an unknown property 'tls'. These properties are valid:
remote:              object { __dirname?, __filename?, global? }
remote:              -> Options object for node compatibility features.
remote:            * configuration.node has an unknown property 'child_process'. These properties are valid:
remote:              object { __dirname?, __filename?, global? }
remote:              -> Options object for node compatibility features.
remote:        
remote: 
remote:  !

Webpackerって何

Webpackerは、汎用的なwebpackビルドシステムのRailsラッパーであり、標準的なwebpackの設定と合理的なデフォルト設定を提供します。

webpackをrails仕様にしたものをwebpackerというのだろうか。

まず前提として、webpackとwebpackerは別物です。webpackはJSのnpmのパッケージです。

(https://qiita.com/jesus_isao/items/1f519b2c6d53f336cadd)

ぐはっ。全くの別物なんかい。

Webpackerを構成するもの

loader

リソースをモジュール化するためのツールのこと。

webpackはモジュール化されたものをバンドルする(束ねる)ことができるツール。

画像ファイルやスタイルシートなどのリソースは、このローダーを通じてモジュール化させてあげることで、はじめてバンドルができるようになる。

(参考: https://www.fundely.co.jp/blog/tech/2020/01/22/180037/)

Babel

JavaScriptのトランスコンパイラー(変換ツール)

このBabelを介すことで、ES6(ECSAScript2015、ES2015)の標準化されたJavaScript構文を、ES6サポート対象外のIE11などでも解釈することができるようになる。

そのため、現在webpackを新しく導入する際は『webpack』と『Babel』とを組み合わせて使用するのが通例となっている。

plugin

webpackのバックボーン的な存在。

loaderのできないことを全てやってくれる。さまざまなpluginが用意されている。

Webpackのできること

  • webpackは、requireとかimportとかを解決して、IEでも読めるようにファイルをまとめ上げることができる。
  • loaderを使用することで、webpackの前処理でTypeScript → JavaScriptに変換したりもできる。
  • pluginを使用することで、webpackの後処理でJSにminifyをかけたりすることもできる。

参考:(https://qiita.com/jesus_isao/items/1f519b2c6d53f336cadd)

JavaScript」「CSS」「画像やフォント」といった静的アセットを管理できるファイルかな〜くらいにしか思っていなかったが、loaderやpluginといった記述が裏側ではなされているのか〜と知ったちなみに全く理解は出来ていない。

エラーとの戦い

先程のエラー文を調べたところ、全く同じエラーでつまづいている外国の方がいらっしゃいました。

I have encountered an error when using the latest version of Webpack (5.1.0). It looks like the configuration is throwing an error because the validation schema is too restrictive. Here is my webpack configuration file in a gist, and the error message I am seeing.

webpackの最新版を使っていて、validation の制限が厳しすぎるがためにconfigがエラーを吐いてしまっている。と書いてあります。

どうやらwebpack.config.jsの記述に誤りがあるようです。

でも、今自分が作成中のアプリにwebpack.config.jsなんてない、、

がしかし、この記事によると、webpacker.ymlがwebpack.config.jsの代わりになっているとのこと。

webpackerは、config/webpacker.ymlというwebpacker専用のymlの設定ファイルを読むことで、何をどう実行したらよいかを決定し、よしなにファイルを書き出してくれたりします。

つまり、webpacker.ymlの記述をなんとか弄ればエラーは解決できるってことか?

明日こそこのエラーを倒してherokuにデプロイをかましたいと思います。

下記参考文献を読み漁る1日にしたいです。。

Webpacker の概要 - Railsガイド

Webpack 5: configuration.node has an unknown property 'dgram'. These properties are valid: · Issue #11649 · webpack/webpack

webpack実行時にInvalid - Qiita

Webpack 5: configuration.node has an unknown property 'dgram'. These properties are valid: · Issue #11649 · webpack/webpack

GitHub - rails/webpacker: Use Webpack to manage app-like JavaScript modules in Rails

vue-loader

For example to add Vue support:

// config/webpack/rules/vue.js
const { VueLoaderPlugin } = require('vue-loader')

module.exports = {
  module: {
    rules: [
      {
        test: /\\.vue$/,
        loader: 'vue-loader'
      }
    ]
  },
  plugins: [new VueLoaderPlugin()],
  resolve: {
    extensions: ['.vue']
  }
}
// config/webpack/base.js
const { webpackConfig, merge } = require('@rails/webpacker')
const vueConfig = require('./rules/vue')

module.exports = merge(vueConfig, webpackConfig)

Getting Started | Vue Loader

webpack.config.jsがわからない - Qiita

base.js
const { environment } = require('@rails/webpacker')

const customConfig = {
  resolve: {
    fallback: {
      dgram: false,
      fs: false,
      net: false,
      tls: false,
      child_process: false
    }
  }
};

environment.config.delete('node.dgram')
environment.config.delete('node.fs')
environment.config.delete('node.net')
environment.config.delete('node.tls')
environment.config.delete('node.child_process')

environment.config.merge(customConfig);

module.exports = environment

Webpack Error - configuration.node has an unknown property 'fs'

https://gist.github.com/adarshbhat/3ec5950b66b78102da0cf46e51a3d633

VueとRailsを使ってログイン機能を実装する

結局フロントエンドをVue.js、バックエンドをRailsに任せるという構成にして、Vuexを導入することにした。

Vuexを導入した理由は、アプリ作成を進めていく中で画面数がプロトタイプよりも増えたから。階層が複雑になることによる弊害を回避したかった。

早速ログイン機能のロジックをVuexに置いて、ログイン画面(UserLogin.vue)でそのロジックを呼び出す流れの実装を進めた。

今回は頭の整理をするためにログイン時のWebAPI実行をまとめていきたい。

登場するファイル

必要なファイルは以下の通り。

  • src/plugins/axios/index.js
  • src/store/user.js
  • src/pages/UserLogin.vue
  • controllers/api/session_controller.rb

各ファイルの内容

src/plugins/axios/index.js

import axios from 'axios';

let csrf_token = document.getElementsByName('csrf-token')[0].content;
const axiosInstance = axios.create({
  baseURL: 'api',
  headers: { 'X-CSRF-TOKEN': csrf_token },
});

export default axiosInstance;

src/store/user.js

import axios from '../../plugins/axios'
import router from '../../router/index'
const state = {
    authUser: null,
};

const getters = {
    authUser: (state) => state.authUser,
};

const mutations = {
    setAuthUser(state, user){
        state.authUser = user;
    }
};

const actions = {
    loginUser({ commit }, user) {
        axios
          .post('session', user)
          .then((res) => {
              commit('updateAuthUser', res.data);
              router.push({ name: 'TopPage' });
              alert('ログインに成功しました')
          })
    }
};

export default {
    namespaced: true,
    state,
    getters,
    mutations,
    actions,

};

controllers/api/session_controller.rb

module Api

  class SessionsController < ApplicationController
    def create
      user=login(params[:email], params[:password])
      if user
        json_string = 
        UserSerializer.new(user).serializable_hash.to_json
        render json: json_string
      else
        head :unauthorized
      end 
    end
  end
end

src/pages/UserLogin.vue

<script>
import { mapActions } from 'vuex';
export default {
    data(){
        return{
            user: {
                email: '',
                password: '',
            },
            showPassword: false,
        };
    },
    methods: {
            ...mapActions('users', ['loginUser']),
            handleShowPassword(){
                this.showPassword = !this.showPassword;
            },
            handleLogin(){
                this.loginUser(this.user);
            },
    },
};
</script>

今回やりたい動作

登録したemailとpasswordをinputエリアに入力後ログインボタンを押す⇨トップページに遷移する。

ファイルの解説

API実行処理

実際にログイン部分を司っているのが、API処理を担うactionsになので、切り出してみた。

#src/store/module/user.js
const actions = {
    loginUser({ commit }, user) { #...1
        axios
          .post('session', user) #...2
          .then((res) => {
              commit('updateAuthUser', res.data); #...3
              router.push({ name: 'TopPage' }); #...4
              alert('ログインに成功しました')
          })
    }
};
  1. 第1引数のデータを第2引数に渡している。ちなみにmutationsの値を変えるためにcommitは存在している。
  2. axiosのpostアクション(createアクション)は第二引数にデータを渡す役割を持つ。
  3. 2をした後、アクションに成功したらupdateされたデータがres.dataに渡る。
  4. TopPageと命名されたコンポーネントに遷移させている。

コントローラーの挙動

2でpostアクションが実行されているが、sessions_controllerの中身を以下に記述しておく。

controllers/api/session_controller.rb

module Api

  class SessionsController < ApplicationController
    def create
      user=login(params[:email], params[:password])
      if user
        json_string = 
        UserSerializer.new(user).serializable_hash.to_json
        render json: json_string
      else
        head :unauthorized
      end 
    end
  end
end

sorceryのlogin(params[:email], params[:pasword])を使っている。

もしuserがtrueなら、レスポンス内容をjson形式に変換してクライアントに返却している。

jsonapi-serializerって何?

RailsAPIモードで開発をする際に、JSONを整形するためのgem

今回のAPI実行において、裏で行われている挙動は下記の通り。

  1. フロントがAPIサーバにリクエストを飛ばす。
  2. Webサーバはリクエスト内容に応じて、DBから値を持ってくる。
  3. そしてサーバはJSONAPIフォーマットでWebサーバに返す

いわゆるオーソドックスなHTTPリクエストとレスポンスのやりとりである。

この、3を行うためにjsonapi-serializerというものが存在しているみたいだ。

ログイン画面

src/pages/UserLogin.vue
<script>
import { mapActions } from 'vuex';
export default {
    data(){ #...1
        return{
            user: {
                email: '',
                password: '',
            },
            showPassword: false,
        };
    },
    methods: {
            ...mapActions('users', ['loginUser']), #...2
            handleShowPassword(){
                this.showPassword = !this.showPassword;
            },
            handleLogin(){ #...3
                this.loginUser(this.user);
            },
    },
};
</script>
  1. dataには今回使用しているuserデータを格納しておき、value(emailとpassword)もネストしてある。
  2. vuexに格納されているloginUserメソッドを、...mapActionsで呼び出している。importのようなものかと。
  3. 2で呼び出したloginUserメソッドをhandleLoginメソッドに擬態している。これで、クリックイベントが発火してhandleLoginが実行された時にloginUserと同じ挙動が行われるように出来た。

まとめ

  • ログイン機能を実装させることで、axiosによる非同期処理、vuexによる状態管理、jsonapi-serializer,mapActions,HTTPのやりとりに関してマルっと学べるのでいい勉強になる。

axiosライブラリを使ってリクエストする - Qiita

GitHub - jsonapi-serializer/jsonapi-serializer: A fast JSON:API serializer for Ruby (fork of Netflix/fast_jsonapi)

ステート | Vuex

MySQLエラー討伐ログ

「エラーに直面した時に、ネット上に沢山先人の知恵が転がっているので自分で解決しやすい」という理由でMySQLを選びました。

早速サーバー接続エラーにぶち当たりました。

先人の知恵をフル活用して対応していきたいと思います。

Github Actionsを実行した時、rspecの箇所で下記エラーを吐いた。

ActiveRecord::ConnectionNotEstablished:
  Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)

さっきまではrspec使えてたのにな〜と思い、えおーかる環境で下記コマンドを実行。

sudo mysql.server start

すると、下記エラーを吐いた。

Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)

エラー文をコピペして対応してみる。

ActiveRecord::ConnectionNotEstablished:

Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)

下記コマンドを実行すると、(2)の部分が(38)に変わる。

$ sudo touch /tmp/mysql.sock

mysqlが起動できない(Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)) - Qiita

Mysql2::Error::ConnectionError:Can't connect to local MySQL server through socket '/tmp/mysql.sock' (38)

下記コマンドを打つとMySQLに権限をつけることができるが、

The server quit without updating PID fileエラーが出る。

$ sudo chown mysql:mysql /tmp
$ sudo mysql.server start

ERROR! The server quit without updating PID file

PIDファイルをアップデートする前にMySQLをquitしてしまったことによるエラー。

【ケース1】 権限不足パターン

$sudo chown -R _mysql:_mysql /usr/local/var/mysql

解決せず。

【ケース2】 PIDファイル生成パターン

$ sudo touch /usr/local/var/mysql/****.local.pid

*****の部分には「uname -n」で確認できるユーザー名を入れる。

解決せず。

【ケース3】PID削除/MySQLサーバ再起動パターン

$ ps -ef | grep mysql
$ sudo kill -9 PID
$ sudo mysql.server start

これで成功した。

「「ERROR! The server quit without updating PID file」

突然sqlに接続できなくなり、現在はサーバーすら立ち上がらない状態です。|teratail

mysqlが起動できない(Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)) - Qiita

.syncのここが凄い!

コンポーネント間で値を受け渡しする流れが一生分からない。悩んでも仕方ないから、とりあえずインプット量を増やす。てなわけで、今回は.syncに関して学習していく。

 

.sync修飾子のどこが凄い?

 v-bind:title="doc.title"
 v-on:update:title="doc.title = $event"

この書き方を、以下のようにまとめることができるところ。

 :title.sync="doc.title"

.sync修飾子って何?

vueのカスタムイベントの一つ。

親が子に渡した値Aを、子が編集し、編集後の値Aを子から親に返す。

.syncを使うことで、受け取った値Aを親は親は比較的少ない記述量で反映させることができる。

どう使うんですか?

下記のように、v-bindに対して使う。

<text-document 
  :title.sync="doc.title"
></text-document>

公式Docを見て学習していく。

子は、methodで下記のように記述することで、titleのアップデートを親に送ることができる。

this.$emit('update:title', newTitle)

$emitを使ってnewTitleをupdate:titleに反映させて親に渡している。親は$eventで受け取ることができる。

親は、下記のように$eventを記述することでupdate:titleを取得している。

<text-document
  :title="doc.title"
  @update:title="doc.title = $event"
></text-document>

このパターンを .sync 修飾子で短く書くことができる。

<text-document 
  :title.sync="doc.title"
></text-document>

もっと分かりやすい記事ないの?

あります。

参考記事にあった、ユーザーのログインフォームを実装を通して理解を深めてみる。

コンポーネント

front/pages/signup.vue


<template>
      ...
      <!-- :name.sync 追加 -->
      <user-form-name
        :name.sync="params.user.name"
      />
      <!-- :email.sync 追加 -->
      <user-form-email
        :email.sync="params.user.email"
      />
      <!-- :password.sync 追加 -->
      <user-form-password
        :password.sync="params.user.password"
      />
export default {
  layout: 'beforeLogin',
  data () {
    return {
      isValid: false,
      // 追加
      params: { user: { name: '', email: '', password: '' } }
    }
  }
}

UserNameForm

<user-name-form
  :name="params.user.name" # : はv-bindの省略形
  @update:name="params.user.name = $event" # @はv-onの省略形
/> 
  1. バインド:nameで親 → 子へデータを送信し、
  2. 子でデータを編集。
  3. 子から編集後のデータを送信し、
  4. 親は@update:nameで受け取り、"params.user.name = $event"で値を代入する。

通常の書き方は上記のようになるが、.sync修飾子を使うと下記のように書くことができる。

<user-name-form
  :name.sync="params.user.name"
/>

子⇨親の連動バインディングが.syncで省略されている。

@update:nameで子から編集後のデータを受け取って$eventで親の該当箇所を更新する、という処理が.syncで完結できている。

ちなみに、v-bind は「:」v-onは「@」v-slotは「#」という記述ができる。

front/components/user/userFormName.vue

<template>
  <!-- v-model 追加 -->
  <v-text-field
    v-model="setName"
    label="ユーザー名を入力"
    placeholder="あなたの表示名"
    outlined
  />
</template>
<script>
export default {
props: {
    name: {
      type: String,
      default: ''
    }
  }
computed: {
    setName: {
      get () { return this.name },
      set (newVal) { return this.$emit('update:name', newVal) }
    }
  }
}
</script>
  1. v-modelに入力されたStringの値は、setNameに保存される。
  2. setNameにはgetterとsetterの二つがある。
  3. getterを通して、prop(親⇨子の値を受け取ることができるプロパティ )のnameにアクセスすることができる。
  4. getterで取得したthis.nameは、v-modelに入力された値に更新される。
  5. 更新された値はnewValとしてsetterに入り、this.$emit('update:name', newVal)によって親に渡される。

まとめ

  • .syncを使うことで、子から$emitされた値を親は簡潔な記述で受け取ることができる。
  • v-bind は「:」v-onは「@」v-slotは「#」
  • propは親⇨子の流れで値が移動する。

カスタムイベント - Vue.js

Nuxt.js 親子コンポーネント間の双方向データバインディングを実装する(3/4) - 独学プログラマ