The work of the frontend part of the application in Ruby on Rails is the responsibility of the Sprockets library, which does not meet the needs of the modern frontend application. What exactly does not reach can be read, for example, here and here .
Although there are already enough articles on the webpack + rails bundle and there is even a special gem , I suggest looking at another bike that can also deploy.
So, all the frontend application will be in #{Rails.root}/frontend
. Standard assets
will only contain image files that are connected via image_tag
.
To start, you need Node JS, npm, the webpack itself and plugins for it. You also need to add the following to .gitignore
:
/node_modules /public/assets /webpack-assets.json /webpack-assets-deploy.json
When using the console utility, webpack loads the webpack.config.js
file.
In our case, it will be used to separate the different environments defined in the variable NODE_ENV
:
// frontend/webpack.config.js const webpack = require('webpack'); const merge = require('webpack-merge'); const env = process.env.NODE_ENV || 'development'; module.exports = merge( require('./base.config.js'), require(`./${env}.config.js`) );
In the basic configuration for all environments, we set general settings for directories, loaders, plug-ins. We also define entry points for the frontend application.
// frontend/base.config.js const path = require('path'); const webpack = require('webpack'); module.exports = { context: __dirname, output: { // path: path.join(__dirname, '..', 'public', 'assets', 'webpack'), filename: 'bundle-[name].js' }, // (entry point) entry: { // : ['./app/base-entry'], // // 2.0 application: ['./app/base-entry'], main_page: ['./app/pages/main'], admin_panel: ['./app/pages/admin_panel'] }, resolve: { // require extensions: ['', '.js', '.coffee'], modulesDirectories: [ 'node_modules' ], // require: // require('libs/some.lib') alias: { libs: path.join(__dirname, 'libs') } }, module: { loaders: [ // ES6 { test: /\.js$/, include: [ path.resolve(__dirname + 'frontend/app') ], loader: 'babel?presets[]=es2015' }, // CoffeeScript { test: /\.coffee$/, loader: 'coffee-loader' }, // Vue JS { test: /\.vue$/, loader: 'vue' }, // jquery // $ { test: require.resolve('jquery'), loader: 'expose?$!expose?jQuery' } ], }, plugins: [ // RAILS_ENV js new webpack.DefinePlugin({ __RAILS_ENV__: JSON.stringify(process.env.RAILS_ENV || 'development') ), }) ] };
The configuration for the development environment is different with the debug mode enabled and the source map. I use Vue JS, so I also added a small fix here to correctly display the source code of the components of the framework.
Also here we define loaders for styles, images and fonts (for production environments, the settings of these loaders will be different, taking into account the features of caching).
// frontend/development.config.js const webpack = require('webpack'); const AssetsPlugin = require('assets-webpack-plugin'); module.exports = { debug: true, displayErrorDetails: true, outputPathinfo: true, // source map devtool: 'eval-source-map', output: { // source map Vue JS devtoolModuleFilenameTemplate: info => { if (info.resource.match(/\.vue$/)) { $filename = info.allLoaders.match(/type=script/) ? info.resourcePath : 'generated'; } else { $filename = info.resourcePath; } return $filename; }, }, module: { loaders: [ { test: /\.css$/, loader: 'style!css?sourceMap' }, // resolve-url, // // *.scss { test: /\.scss$/, loader: 'style!css?sourceMap!resolve-url!sass?sourceMap' }, // { test: /\.(png|jpg|gif)$/, loader: 'url?name=[path][name].[ext]&limit=8192' }, // { test: /\.(ttf|eot|svg|woff(2)?)(\?.+)?$/, loader: 'file?name=[path][name].[ext]' } ] }, plugins: [ // -, // js css new AssetsPlugin({ prettyPrint: true }) ] };
For development, you still need a server that will give out static, monitor changes in files and do a regeneration as needed. A nice bonus - hot module replacement - changes are applied without reloading the page. In my case, it always works for styles, and Javascript is only for Vue JS components.
// frontend/server.js const webpack = require('webpack'); const WebpackDevServer = require('webpack-dev-server'); const config = require('./webpack.config'); const hotRailsPort = process.env.HOT_RAILS_PORT || 3550; config.output.publicPath = `http://localhost:${hotRailsPort}/assets/webpack`; ['application', 'main_page', 'inner_page', 'product_page', 'admin_panel'].forEach(entryName => { config.entry[entryName].push( 'webpack-dev-server/client?http://localhost:' + hotRailsPort, 'webpack/hot/only-dev-server' ); }); config.plugins.push( new webpack.optimize.OccurenceOrderPlugin(), new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin() ); new WebpackDevServer(webpack(config), { publicPath: config.output.publicPath, hot: true, inline: true, historyApiFallback: true, quiet: false, noInfo: false, lazy: false, stats: { colors: true, hash: false, version: false, chunks: false, children: false, } }).listen(hotRailsPort, 'localhost', function (err, result) { if (err) console.log(err) console.log( '=> Webpack development server is running on port ' + hotRailsPort ); })
For production, you can allocate CSS in a separate file using the extract-text-webpack-plugin
. Also various optimizations are applied for the generated code.
// frontend/production.config.js const path = require('path') const webpack = require('webpack'); const CleanPlugin = require('clean-webpack-plugin'); const ExtractTextPlugin = require("extract-text-webpack-plugin"); const CompressionPlugin = require("compression-webpack-plugin"); const AssetsPlugin = require('assets-webpack-plugin'); module.exports = { output: { // filename: './bundle-[name]-[chunkhash].js', chunkFilename: 'bundle-[name]-[chunkhash].js', publicPath: '/assets/webpack/' }, module: { loaders: [ // CSS { test: /\.css$/, loader: ExtractTextPlugin.extract("style-loader", "css?minimize") }, // sourceMap - { test: /\.scss$/, loader: ExtractTextPlugin.extract( "style-loader", "css?minimize!resolve-url!sass?sourceMap" ) }, { test: /\.(png|jpg|gif)$/, loader: 'url?limit=8192' }, { test: /\.(ttf|eot|svg|woff(2)?)(\?.+)?$/, loader: 'file' }, ] }, plugins: [ // , // developoment new AssetsPlugin({ prettyPrint: true, filename: 'webpack-assets-deploy.json' }), // js- // Webpack , new webpack.optimize.CommonsChunkPlugin( 'common', 'bundle-[name]-[hash].js' ), // CSS new ExtractTextPlugin("bundle-[name]-[chunkhash].css", { allChunks: true }), // ... new webpack.optimize.DedupePlugin(), new webpack.optimize.OccurenceOrderPlugin(), new webpack.optimize.UglifyJsPlugin({ mangle: true, compress: { warnings: false } }), // gzip new CompressionPlugin({ test: /\.js$|\.css$/ }), // new CleanPlugin( path.join('public', 'assets', 'webpack'), { root: path.join(process.cwd()) } ) ] };
In the application configuration, we will add a new option to enable / disable inserting webpack statics on the page. It is useful, for example, when running tests, when there is no need to generate statics.
# config/application.rb config.use_webpack = true
# config/environments/test.rb config.use_webpack = false
Create an initializer for parsing the manifest when the Rails application starts
# config/initializers/webpack.rb assets_manifest = Rails.root.join('webpack-assets.json') if File.exist?(assets_manifest) Rails.configuration.webpack = {} manifest = JSON.parse(File.read assets_manifest).with_indifferent_access manifest.each do |entry, assets| assets.each do |kind, asset_path| if asset_path =~ /(http[s]?):\/\//i manifest[entry][kind] = asset_path else manifest[entry][kind] = Pathname.new(asset_path).cleanpath.to_s end end end Rails.configuration.webpack[:assets_manifest] = manifest # Sprockets ; # webpack (. ) Sprockets Rails.application.config.assets.configure do |env| env.context_class.class_eval do include Webpack::Helpers end end else raise "File #{assets_manifest} not found" if Rails.configuration.use_webpack end
Also useful are webpack helpers webpack_bundle_js_tags
and webpack_bundle_css_tags
, which are wrappers for javascript_include_tag
and stylesheet_link_tag
. The argument is the name of the entry point from the webpack config
# lib/webpack/helpers.rb module Webpack module Helpers COMMON_ENTRY = 'common' def webpack_bundle_js_tags(entry) webpack_tags :js, entry end def webpack_bundle_css_tags(entry) webpack_tags :css, entry end def webpack_tags(kind, entry) common_bundle = asset_tag(kind, COMMON_ENTRY) page_bundle = asset_tag(kind, entry) if common_bundle common_bundle + page_bundle else page_bundle end end def asset_tag(kind, entry) if Rails.configuration.use_webpack manifest = Rails.configuration.webpack[:assets_manifest] if manifest.dig(entry, kind.to_s) file_name = manifest[entry][kind] case kind when :js javascript_include_tag file_name when :css stylesheet_link_tag file_name else throw "Unknown asset kind: #{kind}" end end end end end end
Add an auxiliary method to the base controller to connect the controller with the entry point
# app/controllers/application_controller.rb class ApplicationController < ActionController::Base attr_accessor :webpack_entry_name helper_method :webpack_entry_name def self.webpack_entry_name(name) before_action -> (c) { c.webpack_entry_name = name } end end
Now in the controller you can do this:
# app/controllers/main_controller.rb class MainController < ApplicationController webpack_entry_name 'main_page' end
Use in view:
<html> <head> <%= webpack_bundle_css_tags(webpack_entry_name) %> </head> <body> <%= webpack_bundle_js_tags(webpack_entry_name) %> </body> </html>
npm
commandNow all frontend libraries should be installed like this:
npm install <package_name> --save
It is highly desirable to “freeze” the exact versions of all packages in the npm-shrinkwrap.json
(equivalent to Gemfile.lock
). You can do this with the command (although npm
when installing / updating packages, keeps npm-shrinkwrap.json
relevance of npm-shrinkwrap.json
, it is better to be safe):
npm shrinkwrap --dev
For convenience, you can add webpack commands to the scripts
section for quick launch in package.json
:
"scripts": { "server": "node frontend/server.js", "build:dev": "webpack -v --config frontend/webpack.config.js --display-chunks --debug", "build:production": "NODE_ENV=production webpack -v --config frontend/webpack.config.js --display-chunks" }
For example, you can start the webpack server with the command:
npm run server
I chose an economical option: not to drag the entire JS zoo to the production server, but to build the webpack locally and upload it to the server using rsync
.
This is done by the command deploy:webpack:build
, the implementation of which is based on the capistrano-faster-assets game . The generation occurs conditionally: if there were changes in the frontend code or packages were installed / updated. If you wish, you can add your own conditions (files, folders, according to which diff
is made) by setting the variable :webpack_dependencies
. You also need to specify the local folder for the generated statics and the manifest file:
# config/deploy.rb set :webpack_dependencies, %w(frontend npm-shrinkwrap.json) set :local_assets_dir, proc { File.expand_path("../../public/#{fetch(:assets_prefix)}/webpack", __FILE__) } set :local_webpack_manifest, proc { File.expand_path("../../webpack-assets-deploy.json", __FILE__) }
The deploy:webpack:build
command deploy:webpack:build
runs automatically before standard deploy:compile_assets
.
The recipe code for capistrano itself:
# lib/capistrano/tasks/webpack_build.rake class WebpackBuildRequired < StandardError; end namespace :deploy do namespace :webpack do desc "Webpack build assets" task build: 'deploy:set_rails_env' do on roles(:all) do begin latest_release = capture(:ls, '-xr', releases_path).split[1] raise WebpackBuildRequired unless latest_release latest_release_path = releases_path.join(latest_release) dest_assets_path = shared_path.join('public', fetch(:assets_prefix)) fetch(:webpack_dependencies).each do |dep| release = release_path.join(dep) latest = latest_release_path.join(dep) # skip if both directories/files do not exist next if [release, latest].map{ |d| test "test -e #{d}" }.uniq == [false] # execute raises if there is a diff execute(:diff, '-Nqr', release, latest) rescue raise(WebpackBuildRequired) end info "Skipping webpack build, no diff found" execute( :cp, latest_release_path.join('webpack-assets.json'), release_path.join('webpack-assets.json') ) rescue WebpackBuildRequired invoke 'deploy:webpack:build_force' end end end before 'deploy:compile_assets', 'deploy:webpack:build' task :build_force do run_locally do info 'Create webpack local build' %x(RAILS_ENV=#{fetch(:rails_env)} npm run build:production) invoke 'deploy:webpack:sync' end end desc "Sync locally compiled assets with current release path" task :sync do on roles(:all) do info 'Sync assets...' upload!( fetch(:local_webpack_manifest), release_path.join('webpack-assets.json') ) execute(:mkdir, '-p', shared_path.join('public', 'assets', 'webpack')) end roles(:all).each do |host| run_locally do `rsync -avzr --delete #{fetch(:local_assets_dir)} #{host.user}@#{host.hostname}:#{shared_path.join('public', 'assets')}` end end end end end
Impressions of using in favor of the webpack: modularity out of the box, precise control of library versions and their easy updating, development server is not busy processing statics, deploying is faster and not loading by precompiling production server.
That's all ;)
Update! . If you use Sprockets in parallel (or something other than the webpack, it uses public/assets
), then to generate the webpack assets it is better to select a separate directory, for example: public/assets/webpack
(made the appropriate changes to the post). Now with the deployment you can run rsync
with the option - --delete
, so as not to accumulate unused assets on the production. This solution has a drawback: synchronization with deletion makes it impossible to roll back assets to a previous release. Therefore, when deploying, you need to make a backup of the manifest and restore it to the necessary version of assets in case of rollback.
Update 2 . Designed the integration process described above as a heme https://github.com/Darkside73/webpacked
Source: https://habr.com/ru/post/282584/
All Articles