之前写过一篇 webpack 初步 https://ruby-china.org/topics/27537,后来随着深入使用做了一些修改和优化。这篇文章将更详细地介绍如何从 Rails Asset Pipeline 迁移到 webpack(包括 javascript、stylesheets 以及其他资源文件),并且在生产环境使用。
Rails Asset Pipeline 是个很不错的解决方案,如果前端代码不复杂的话还是非常推荐的。然而如果一个项目有如下的特征,建议还是使用 npm 来管理前端的 package,并用 webpack 或者其他工具来做前端工程化:
npm init
,按提示操作,会自动创建一个 package.json
文件// 安装 webpack
npm install webpack --save-dev
// webpack 命令行
npm install -g webpack
######注意:
npm install XXX --save-dev
和 npm install XXX -save
会自动把要安装的 package 添加到 package.json
文件里的 dependencies 和 devDependencies 部分。最终你的 package.json 文件差不多长这样,有一些 package 我们之后会提到。
{
"name": "my app",
"description": "my rails app using webpack",
"version": "1.0.0",
"dependencies": {
"jquery": "^2.1.4",
"jquery-ujs": "~1.1.0-1",
"lodash": "~3.0.0",
},
"devDependencies": {
"babel-core": "^5.8.25",
"babel-loader": "^5.3.2",
"babel-runtime": "^6.5.0",
"coffee-loader": "^0.7.2",
"coffee-script": "^1.10.0",
"css-loader": "^0.23.0",
"exports-loader": "~0.6.2",
"expose-loader": "~0.6.0",
"extract-text-webpack-plugin": "^0.9.1",
"file-loader": "^0.8.5",
"imports-loader": "~0.6.3",
"node-sass": "^3.4.2",
"sass-loader": "^3.1.2",
"style-loader": "^0.13.0",
"url-loader": "^0.5.7",
"webpack": "^1.12.14",
"webpack-manifest-plugin": "^1.0.0"
}
}
现在我们考虑把前端的 javascript、stylesheets、images 都抽出来,在原项目目录下创建一个新文件夹 frontend
,项目文件夹结构如下:
/app
/assets
/controllers
/models
/...
/config
/...
/frontend
/fonts
/images
/stylesheets
/javascripts
/development.config.js
/production.config.js
现在我们添加一些 javascript、stylesheet 和 image:
/frontend
/fonts
/images
/banner.jpg
/stylesheets
/home.scss
/javascripts
/home.coffee
/app.js
/development.config.js
/production.config.js
webpack 的配置文件一如既往的复杂,development.config.js
是开发环境的配置,加了一些注释,就不赘述了:
var path = require('path');
var _ = require('lodash');
var webpack = require('webpack');
var assetPath = path.join(__dirname, '../', 'public', 'assets');
var ExtractTextPlugin = require("extract-text-webpack-plugin");
var ManifestPlugin = require('webpack-manifest-plugin');
var config = module.exports = {
context: path.join(__dirname, '../'),
// 告诉 webpack 去哪里找 entry 文件
// webpack 按需加载和打包里面使用的 module,这里用 CommonJS/AMD/ES6 的语法都可以
entry: './frontend/javascripts/app.js',
// 开发环境 debug 的一些配置
debug: true,
displayErrorDetails: true,
outputPathinfo: true,
devtool: 'cheap-module-eval-source-map'
};
config.output = {
path: assetPath,
// 打包出来的文件会是 [文件名]_bundle.js
// 比如我们的 entry 叫 app.js,打包出来的文件就是 app_bundle.js
filename: '[name]_bundle.js',
publicPath: '/assets/'
};
config.resolve = {
extensions: ['', '.js', '.coffee', '.json'],
modulesDirectories: ['node_modules'],
root: path.resolve(__dirname),
};
config.plugins = [
// 如果多个文件里使用了 jquery,以下这个 plugin 可以让你不用每次都 require('jquery')
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery'
}),
// 用来抽取 js 文件里引用的 css 文件,最终的文件名也会是 [js文件名]_bundle.css 的形式
new ExtractTextPlugin('[name]_bundle.css', {
allChunks: true
})
];
// webpack 强大的 loader
config.module = {
loaders: [
// 下面两行将 jquery 暴露到外面的 $ 和 jQuery 里,这样 webpack 以外的 js 也可以顺利使用 jquery
{test: require.resolve("jquery"), loader: "expose?jQuery" },
{test: require.resolve("jquery"), loader: "expose?$" },
// 使用 babel-loader 来支持 es6 语法
{test: /\.js$/, loader: 'babel-loader'},
// 使用 coffee-loader 来编译 CoffeeScript
{test: /\.coffee$/, loader: 'coffee-loader'},
// 使用 url-loader 来编译字体文件和图片,如果文件小于8kb就直接变成 DataUrl
{test: /\.(woff|woff2|eot|ttf|otf)\??.*$/, loader: 'url-loader?limit=8192&name=[name].[ext]'},
{test: /\.(jpe?g|png|gif|svg)\??.*$/, loader: 'url-loader?limit=8192&name=[name].[ext]'},
// 使用 style-loader、css-loader 来打包 css,sass-loader 打包 sass
// 使用 ExtractTextPLugin 生成独立的 css 文件
{test: /\.css$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader')},
{test: /\.scss$/, loader: ExtractTextPlugin.extract('style', 'css!sass')}
]
};
这样就可以在 js 里引用其他 js/coffee、css/sass 啦,例如我们的 app.js
里可以这样写:
// 引用 home.scss
require('../stylesheets/home.scss');
// 引用 home.coffee
require('./home');
也可以在 css/sass 里引用 image 了,注意使用 url
方法(类似 Rails 里的 asset-url
)。例如我们的 home.scss
:
.home-banner {
background-image: url('../images/banner.jpg');
}
生产环境的话需要做一些调整:
config.output = {
...
// 给 js 加 fingerprint
filename: '[name]_bundle-[chunkhash].js',
// 假设我们的 cdn 是 http://cdn.test.com
publicPath: 'http://cdn7.test.com/assets/',
...
});
config.plugins = [
...
// 给抽出来的 css 加 fingerprint
new ExtractTextPlugin('[name]_bundle-[chunkhash].css', {
allChunks: true
}),
// 这个 pulgin 我们下一步介绍
new ManifestPlugin({
fileName: 'webpack_manifest.json'
}),
// 一些生产环境优化用的 plugin
new webpack.optimize.UglifyJsPlugin(),
new webpack.optimize.OccurenceOrderPlugin(),
...
];
config.module = {
loaders: [
...
// 给 image、font 等资源加 fingerprint
{test: /\.(woff|woff2|eot|ttf|otf)\??.*$/, loader: 'url-loader?limit=8192&name=[name]-[hash].[ext]'},
{test: /\.(jpe?g|png|gif|svg)\??.*$/, loader: 'url-loader?limit=8192&name=[name]-[hash].[ext]'},
...
];
};
开发环境运行下面的命令,webpack 会根据配置文件打包资源。按之前的例子,就会在 assets/public
里打包出 app_bundle.js
app_bundle.css
banner.jpg
并随时更新:
webpack --config frontend/development.config.js --display-reasons --display-chunks --progress --color
生产环境使用以下命令:
webpack --config frontend/production.config.js -p
webpack 打包完成后,在 Rails 里如何引用打包生成的资源呢?在开发环境可以直接使用文件名引入:
<%= javascript_include_tag 'app_bundle' %>
<%= stylesheet_link_tag 'app_bundle' %>
但是生产环境打包出来资源的名称加了 fingerprint,导致 Rails 找不到资源。这时候我们使用 webpack 的 ManifestPlugin
,它会在打包的时候生成一个 json 文件,里面有原文件和生成文件的对应关系。例如:
{
"banner.jpg": "banner-51aad7eb9e12db5cd6b1fd8688aadc8a.jpg",
"app.css": "app_bundle-d5c3643adae965258b70.css",
"app.js": "app_bundle-d5c3643adae965258b70.js",
}
在 webpack 里配置如下:
new ManifestPlugin({
fileName: 'webpack_manifest.json'
})
这样在 Rails 里我们可以依据这个 json 文件来写一些 helper,让 Rails 找到 webpack 打包出来的资源:
// config/application.rb 里添加配置
config.webpack = {
asset_manifest: {}
}
// 加一个 config/initializers/webpack 来加载这个配置
asset_manifest = Rails.root.join('public', 'assets', 'webpack_manifest.json')
if File.exist?(asset_manifest)
Rails.configuration.webpack[:manifest] = JSON.parse(
File.read(asset_manifest),
).with_indifferent_access
end
在 application_helper.rb
里添加加载 webpack 资源的 helper 方法:
// cdn_assets_url 是我们自定义的一个方法
// 如果是生产环境会返回该资源在我们自己 cdn 的 url
// 如果是开发环境直接返回本地的 url
def webpack_javascript_include_tag(name)
full_name = "#{name}_bundle.js"
src = cnd_assets_url("/assets/#{full_name}")
if Rails.configuration.webpack[:manifest]
asset_name = Rails.configuration.webpack[:manifest]["#{name}.js"]
if asset_name
src = cnd_assets_url("/assets/#{asset_name}")
end
end
"<script src=\"#{src}\"></script>".html_safe
end
def webpack_stylesheet_link_tag(name)
full_name = "#{name}_bundle.css"
src = cnd_assets_url("/assets/#{full_name}")
if Rails.configuration.webpack[:manifest]
asset_name = Rails.configuration.webpack[:manifest]["#{name}.css"]
if asset_name
src = cnd_assets_url("/assets/#{asset_name}")
end
end
"<link rel=\"stylesheet\" href=\"#{src}\">".html_safe
end
这样我们只需要用以上两个方法在 Rails 的 view 层引用资源就可以了。比如我们之前的例子里的 app_bundle.js
和 app_bundle.css
:
<%= webpack_javascript_include_tag 'app' %>
<%= webpack_stylesheet_link_tag 'app' %>
如果使用了 React,可以在开发环境配合 webpack 实现热替换(HMR)。需要先安装一个 webpack-dev-server
,它会默认运行在 8080 端口,然后实时根据代码改动打包更新 webpack 资源,并在浏览器更新 React 组件,而不需要刷新页面。
这么强大的功能如何使用?我们先在 webpack 配置里修改 publicPath:
config.output = {
...
publicPath: 'http://localhost:8080/assets/',
...
});
添加 react-hot-loader
来编译 js:
{test: /\.js$/, loaders: ['react-hot', 'babel-loader']},
使用以下命令打包:
webpack-dev-server --config frontend/development.config.js --hot --inline
在 Rails 里将我们之前写的两个 helper 方法再进一步改进就可以了:
def webpack_javascript_include_tag(name)
full_name = "#{name}_bundle.js"
src = cnd_assets_url("/assets/#{full_name}")
if Rails.env.development?
# 热替换
src = "http://localhost:8080/assets/#{full_name}"
elsif Rails.configuration.webpack[:manifest]
asset_name = Rails.configuration.webpack[:manifest]["#{name}.js"]
if asset_name
src = cnd_assets_url("/assets/#{asset_name}")
end
end
"<script src=\"#{src}\"></script>".html_safe
end
def webpack_stylesheet_link_tag(name)
full_name = "#{name}_bundle.css"
src = cnd_assets_url("/assets/#{full_name}")
if Rails.env.development?
# 热替换
src = "http://localhost:8080/assets/#{full_name}"
elsif Rails.configuration.webpack[:manifest]
asset_name = Rails.configuration.webpack[:manifest]["#{name}.css"]
if asset_name
src = cnd_assets_url("/assets/#{asset_name}")
end
end
"<link rel=\"stylesheet\" href=\"#{src}\">".html_safe
end
webpack 性能优化可以参考这篇文章 https://github.com/wyvernnot/webpack_performance/tree/master/moment-example。可通过添加别名、使用 cdn 来优化性能,这里就不一一列举了。建议先用 webpack 自带的 profile 方法分析出哪里是瓶颈后再调优。
Rails 完成了 webpack 的配置之后,前端完全融入了现在的前端生态,之后可以玩的就多了。比如最近我们在用 React + Redux 重构 T 社的编辑器(http://tshe.com/campaigns/new)就非常方便 :)