初めに
こんにちは。今回の記事ではwebpackを利用してJSやCSS、SCSSを一纏めにし、capistranoでデプロイする方法がようやくできたのでここに執筆します。
筆者はまだ、webpack自体がなんなのか非常に曖昧な所があり、この記事の執筆が完了するまでの間には明確に理解できているという願掛けも込めてどのような方法を取ったのか共有します。そのせいか、色々なツールの公式ドキュメントの直訳みたいなところになってしまっているところもありますが、追々直していきます汗
各ツールのバージョン
- Rails 5.2.0
- webpack 3.12.0
- ruby 2.5.1p57
- bootstrap-material-design-icons 2.2.0
- bootstrap.native 2.0.23(JSのみ利用)
- bootstrap 4.1.1(CSSのみ利用)
- vue 2.5.16
- その他、自作のJSやCSS,SASS
webpackとは
webpackとは、複数のJavaScriptを一つのファイルにまとめる事ができるモジュールバンドラです。仕組みとしては、アプリケーションで必要とする全てのモジュールをマッピングし、依存関係のグラフを内部的に構築している模様です。
webpack4以降では各アプリケーションの設定が基本的には必要なく、https://webpack.js.org/configuration/ に乗っ取ってカスタマイズすればあらかたコンパイルできる模様です。
さて、最初にJSのファイルをまとめると供述してしまったのですが、webpackは基本的に以下の5つの定義でグラフを構築します。
- Entry
- Output
- Loaders
- Plugins
- Mode
ここで、特に重要なのが、 Loaders
と Plugins
の二つです。
LoadersはJS以外の拡張子のファイルやモジュールを依存グラフに追加できるように変換してくれます。使い方として、
-
test
項目で変換対象のファイルを識別 -
use
項目で変換対象のファイルで利用するローダーを指定
する事でJS以外のファイルをwebpackに追加する事ができます。
さらにPluginsでは、Loadersで変換を行なったファイルに対して機能拡張を行なってくれます。ここで扱うPluginはnpmやYarnなどのパッケージマネージャ経由でインストールし、 require()
で利用したいプラグインを追加することができます。
webpackの概要はこのあたりにしておいて、次から Rails
アプリケーションの例でwebpackを導入してみましょう。
Railsにwebpackを導入
webpackを導入したRailsアプリを構築するために、新たにRailsアプリを作成します。
手順としてはhttps://techracho.bpsinc.jp/hachi8833/2017_12_26/49931 の記事を参考にします。
この記事の手順を要約すると、
-
app/javascripts
をapp/frontend
に名前変更 -
application.html.erb
のjavascript_include_tag "application"
をjavascript_pack_tag "application"
に置き換え -
application.html.erb
のstylesheet_link_tag 'application', media: 'all'
をjavascript_pack_tag "application"
に置き換え -
webpacker.yml
でバンドルを探索する場所をfrontend
に変更 -
application_controller.rb
でfrontend
をコントローラが見つけられるように指定
HTMLのテンプレートエンジンを slim
に置き換えている場合は適宜読み変えてください。
capistranoの導入
Capistrano
関係のGemは以下を入れています。
(中略)
gem 'capistrano'
gem 'capistrano-bundler'
gem 'capistrano-ext'
gem 'capistrano-rails'
gem 'capistrano-rbenv'
gem 'capistrano-npm'
gem 'capistrano3-puma'
gem 'capistrano-postgresql'
本当は npm
ではなく、より機能が豊富な yarn
でdeploy時にパッケージを追加するべきなのですが、一旦 npm
で都度deploy時にパッケージをインストールするようにします。
deploy.rb
は以下のように書きました。
(中略)
namespace :deploy do
desc "Make sure local git is in sync with remote."
task :check_revision do
on roles(:app) do
# unless `git rev-parse HEAD` == `git rev-parse origin/master`
# puts "WARNING: HEAD is not the same as origin/master"
# puts "Run `git push` to sync changes."
# exit
# end
end
end
desc 'Run rake npm install'
task :npm_install do
on roles(:web) do
within release_path do
execute("cd #{release_path} && npm install")
end
end
end
desc 'Initial Deploy'
task :initial do
on roles(:app) do
before "deploy:restart", "puma:start"
invoke "deploy"
end
end
desc "Restart application"
task :restart do
on roles(:app), in: :sequence, wait: 5 do
Rake::Task["puma:restart"].reenable
invoke "puma:restart"
end
end
before :starting, :check_revision
before 'deploy:assets:precompile', 'deploy:npm_install'
after :finishing, :compile_assets
after :finishing, :cleanup
end
先ほど申し上げた通り、deploy時にnpm installでdeploy先ディレクトリの node_modules配下にパッケージをインストールするようにします。
Webpackの設定
最後に、webpackの設定です。
const webpack = require('webpack');
const path = require('path');
/**
* Require webpack plugins
*/
const environment = require('./environment');
const ManifestPlugin = require('webpack-manifest-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
/**
* Entries
*/
const entries = {
application: ['./frontend/packs/application.js'],
markdown_preview: ['./frontend/packs/markdown_preview.js']
}
module.exports = Object.assign({}, environment.toWebpackConfig(), {
entry: entries,
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
},
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
use: [{
loader: "css-loader"
}, {
loader: "sass-loader"
}],
// use style-loader in development
fallback: "style-loader"
})
},
{
test: /\.scss$/,
use: ExtractTextPlugin.extract({
use: [{
loader: "css-loader"
}, {
loader: "sass-loader"
}],
// use style-loader in development
fallback: "style-loader"
})
},
{
test: /\.sass$/,
use: ExtractTextPlugin.extract({
use: [{
loader: "css-loader"
}, {
loader: "sass-loader"
}],
// use style-loader in development
fallback: "style-loader"
})
},
{
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
use: [{
loader: 'url-loader?mimetype=image/svg+xml'
}],
},
{
test: /\.woff(\d+)?(\?v=\d+\.\d+\.\d+)?$/,
use: [{
loader: 'url-loader?mimetype=application/font-woff'
}],
},
{
test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
use: [{
loader: 'url-loader?mimetype=application/font-woff'
}],
},
{
test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
use: [{
loader: 'url-loader?mimetype=application/font-woff'
}],
},
{
test: /\.(jpg|png|gif)$/,
use: [{
loader: 'file-loader?name=[name]-[hash].[ext]'
}],
}
]
},
plugins: [
new ManifestPlugin(), // manifest.jsonを出力するプラグイン
new ExtractTextPlugin({ // define where to save the file
filename: "./application.css",
allChunks: true,
}),
],
resolve: {
modules: [
'node_modules',
path.join(__dirname, 'frontend')
],
alias: {
vue: 'vue/dist/vue.esm.js'
},
extensions: ['.js', '.css', '.scss'],
}
})
各設定を見てみましょう。
- extract-text-webpack-plugin
extract-text-webpack-plugin
は、JSの中でimportされているcssファイルをJSの中で展開せずに、外部CSSファイルとして展開するために、定義しています。
- entries
const entries = {
application: ['./frontend/packs/application.js'],
markdown_preview: ['./frontend/packs/markdown_preview.js']
}
entriesでは、JSファイルを展開する際のファイル名を明示的に指定します。
今回のプロジェクトでは、javascript_pack_tag "application"
(および、vueのmarkdownプレビュー用のJS)を slimのheadに読み込むように設定しているため、
application.js
(プロジェクトで共通利用するjsファイル)とmarkdown_preview.js
と名前づけています。
- test
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
},
上のjsファイルでは、 node_modules
をコンパイルするのは避けたいため、 excludeでコンパイル対象から除外しています。
また、 babel-loader
のloaderを利用して、 JSをBabelでコンパイルできるようにしています。
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
use: [{
loader: "css-loader"
}, {
loader: "sass-loader"
}],
// use style-loader in development
fallback: "style-loader"
})
},
{
test: /\.scss$/,
use: ExtractTextPlugin.extract({
use: [{
loader: "css-loader"
}, {
loader: "sass-loader"
}],
// use style-loader in development
fallback: "style-loader"
})
},
{
test: /\.sass$/,
use: ExtractTextPlugin.extract({
use: [{
loader: "css-loader"
}, {
loader: "sass-loader"
}],
// use style-loader in development
fallback: "style-loader"
})
},
{
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
use: [{
loader: 'url-loader?mimetype=image/svg+xml'
}],
},
{
test: /\.woff(\d+)?(\?v=\d+\.\d+\.\d+)?$/,
use: [{
loader: 'url-loader?mimetype=application/font-woff'
}],
},
{
test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
use: [{
loader: 'url-loader?mimetype=application/font-woff'
}],
},
{
test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
use: [{
loader: 'url-loader?mimetype=application/font-woff'
}],
},
{
test: /\.(jpg|png|gif)$/,
use: [{
loader: 'file-loader?name=[name]-[hash].[ext]'
}],
}
]
},
plugins: [
new ManifestPlugin(), // manifest.jsonを出力するプラグイン
new ExtractTextPlugin({ // define where to save the file
filename: "./application.css",
allChunks: true,
}),
],
resolve: {
modules: [
'node_modules',
path.join(__dirname, 'frontend')
],
alias: {
vue: 'vue/dist/vue.esm.js'
},
extensions: ['.js', '.css', '.scss'],
}
})
対して、js以外のファイルは各ファイルに応じたloaderを利用してコンパイルできるようにします。
なお、extract-text-webpack-plugin
経由でコンパイルする場合は fallback
で style-loader
を使わないようにします。
style-loader
はJSのコード中でstylesheetをrequire出来るようになるloaderですが、extract-text-webpack-pluginで外部CSSファイルに展開したいので、これを定義してしまうと設定が競合してしまいます。
BootStrapやmaterial-iconsで定義されている画像もここで一緒にコンパイルします。
- webpack-manifest-plugin
webpack-manifest-plugin
では、webpackでコンパイルしたファイルよりmanifest.jsonを自動的に生成してくれます。 manifest.jsonは、Webアプリやサイトを表示する方法を制御してくれるjsonファイルです。
- resolve
resolve: {
modules: [
'node_modules',
path.join(__dirname, 'frontend')
],
alias: {
vue: 'vue/dist/vue.esm.js'
},
extensions: ['.js', '.css', '.scss'],
}
最後の resolve
の項目ですがこれにより、コンパイルされたモジュールの解決方法を定義します。
なお、今回、 Vue.js
も利用しているのですが、 Vue.js
の書き方によっては、ブラウザでアクセスした際にコンパイルが走るような設定をする必要があります。今回の書き方がそうでした。
そこで、webpackの alias
の設定を設定することで、 vueファイルをimport(require)できるようにしてあります。
以上で、BootStrap, material-icons, vue.js, 自作css, sassをwebpack経由でコンパイルすることができます。
後は
% bundle exec cap production deploy
でサーバデプロイをしてあげましょう。
まとめ
ということでwebpackによる設定を行い、capistranoでdeployする手順でした。
フロントエンドエンジニアの方には「なんだこの設定は」って突っ込まれそうな設定ですが(実際deployには1分程度かかる。主に、npm install)、及第点な設定は書けたような気がするので、今後はコンパイルが早くなるような設定を目指します。
また、Rails5.1よりAPIモードでrails newすることが可能となったので、vueでMVVMを任せてRailsはAPIとして通信するようなWebアプリを作っていきたいですね。
今回はここまで。