在 Rails 中使用 Webpack

http://clarkdave.net/2015/01/how-to-use-webpack-with-rails/

http://m.oschina.net/blog/464093

webpack 是一个很强大的打包工具,主要用于前端开发,可以和 bowernpm 很好的配合工作。

它比 Rails 原本的前端管理模式要好很多,不过还是可以和 Sprockets 以及 asset pipeline 无缝链接,很好的放在一起工作。不过不爽的是,现在 webpack 的文档还是很难读,特别是你想把它跟 Rails 集成的时候。

如果你还没有用过 webpack,那么这里简单的列举一些它的优点:

  • 通过 NPM 或者 Bower 管理前端所有的 JS (和它们的依賴項)
  • 自动预处理 CoffeeScript, ES6, 之类的
  • 为所有的东西输出 source maps ,带 minimal 特效
  • 帮助你维不同的页面切割 JS 到不同的文件,而 ‘common’ 模块可以自动的在所有页面间共享
  • 把巨大模块划分成成小块,然后按需下载(通过 require.ensure)

如果上面说的这些东西还行,那么你可以继续看下去,看看怎么把这些东西集成到你现有的 Rails 工程上,或者重新做一个。还有,虽然这里举的是 Rails 作例子,但是集成 webpack 的好处,是所有那些类 Rails 框架都可以享受到的。

webpack 是否适合你

webpack 真的是一个超赞超棒的工具。但是你要用好它,你就必须接受整个 “JS 即是模块” 的理念。当你用那些流行的库的时候,比如说 jQuery, Backbone.js, 之类的,那还好说。但是你要知道,特别是当你打算把一个大工程都转成用 webpack 的时候,你要做好满地都是坑的心理准备。

虽然西部有一百万种死法,但是你通常会死于:

  • 模块没有定义入口(webpack 根本不知道你在说什么鬼)
  • 模块的 package.json/bower.json 文件无效
  • 模块往 window 上乱挂东西,然后跑去调用
  • 模块偷偷的给 jQuery 什么的加了点料,然后装无辜
  • 模块,耍流氓的,给你塞了一大堆你不要的东西

还好,上面这些东西其实用 webpack 也都是能解决的,而且还有不同招式。webpack 的文档,有提到,但对于细节总是用你懂的一带而过,然后总有各种跪舔党比如我,会详细的告诉你怎样解决上述问题。

那么,到底适合你的工程吗?

我新开了一个 Rails 工程

如果你遇见将会用很多 JS 的化,当然咯 - 没理由不试试看吧!

**我的工程超大,不过我们没有太多 JS (只有一些 jQuery, retina.js, 这样的)

感觉不太值得。webpack 只有当你用了超多模块,再加上你又写了巨多 JS 代码的时候,才会发光亮瞎你的狗眼。如果你只是用了几个 <script src='...'> 标签的话,你不会爽到的。

我的工程超大,但是我们的 JS 得超屌,一点问题都没有

搬家到 webpack 需要花点精力,你要是那么牛逼,自己玩去吧。

我的工程超大,而且我们的 JS 也乱成团毛,每次打开页面都要下载个 800KB 的 application.js 文件

你应该试试看用 webpack! 可能会花点事件,不过这篇指南会覆盖你需要知道的所有知识点。

如果你准备好了,那么让我们准备开始用 webpack 来打造一个 Rails 应用吧。

准备好 webpack 用的 Rails 环境

这篇文章不是什么所谓 webpack with Rails 的最佳实践,所以基本上所有的配置都是个人喜好,如果你不喜欢我放的目录的名字,那么你就自己弄一个好了。

脱 Sprockets

首先第一件事情就是要清空你的 app/assets/javascripts 目录。我们将把 webpack 输出的 bundles 文件放到这里,然后它们可以被 Sprockets 拿到。我们实际的 JS 代码将会被放到别处。

所以你应该把这个目录加到 .gitignore 去:

/app/assets/javascripts

这样做有两个原因:

  1. 生成的 bundles 通常会很大,并且会经常变,如果你把它们扔到 Github 上,真的就会给你发许多垃圾 Git 更新邮件。
  2. 我们将会把 webpack 集成到我们的发布流程里面,然后回把产品版本的包放到这里。所以,即使你把现在这个包放版本管理,我们在发布的时候,还是会用产品版本的换掉它。

当然,上述的建议是基于你将会用 webpack 管理你’所有’的 JS 前提的。当然,你可以用 webpack 只处理一部分 JS 包,这样你可以修改 .gitignore,只忽略一部分/app/assets/javascripts/bundle-

你的 Javascript 的新家

因为 app/assets/javascripts 现在被用来放生成的包,所以你需要一个新的目录来存放你实际的 Javascript。我喜欢在 app 目录下新建一个文件夹,不过当然你可以随便放到哪里:

app/frontend/javascripts

在这个文件夹底下,你可以创建一个文件叫 entry.js - 先按下不表,我们只是简单的贴些代码进去:

var _ = require('lodash');
_.times(5, function(i) {
  console.log(i);
});

安装 webpack 和 Bower

安装 webpack

因为 webpack 是一个 node.js 的应用,所以我们需要一个 package.json 文件在 Rails 的根目录。我们来做个简单的,我们用 webpack 来管理它自己的模块:

{
  "name": "my-rails-app",
  "description": "my-rails-app",
  "version": "1.0.0",
  "devDependencies": {
    "webpack": "~1.4.13",
    "expose-loader": "~0.6.0",
    "imports-loader": "~0.6.3",
    "exports-loader": "~0.6.2",
    "lodash": "~2.4.1"
  },
  "dependencies": {}
}

就像你看到的那样,我们把 webpack 放到依赖项里面,然后还有一些有可能用到的 webpack 的 loader(稍后我们会用到它们)。

现在让我们运行 npm install 然后你你会看到都下载到了 node_modules里。哦,当然,你有 node.js 对吧,要是没有的话,安装好,然后再试一次 :)

然后,你要全局安装一下 webpack,这样你就可以调用 webpack 命令行咯:

$ npm install -g webpack

安装 Bowser(可选)

webpack 有个很棒的功能就是不会强迫你用任何指定的包/依赖管理工具。默认用的是 npm,它有许多前端的模块不过,有许多前端模块用的是 Bower,这又是另外一个为 web 设计的包管理工具。

如果你只是需要一些流行的,维护良好的库,比如说 jQuery,underscore,lodash,之类的,那么你都不需要 Bower,只要 npm 就够了。你只要简单的把你的前端依赖包扔到package.json 然后 npm 安装就好。

但是,如果你需要访问更多的库,或者你只是喜欢用 Bower,配置也很简单。首先确保 bower 命令行安装好:

$ npm install -g bower

然后创建一个 bower.json 文件到 Rails 根目录:

{

“name”: “my-rails-app”,

“version”: “1.0.0”,

“description”: “my-rails-app”,

“dependencies”: {

“jquery”: “~1.11.0”,

“lodash”: “~2.4.1”

}

}

好了,我们这样就有了一个最简单的 bower.json 文件,指定好 jQuery 和 lodash 的依赖。然后你在 Rails 的根目录下运行 bower install, bower 就会安装那些库到 bower_components/ , 还有它们的依赖包。

记住,和 npm 不一样,bower 的依赖是平铺结构的。所以如果你指定 jQuery 版本是 1.x 但是某些指定依赖项最小是 jQuery 2.x 的话,你需要自己的去解决它

同时使用 bower 和 npm

如果你想同时用 bower 和 npm 的话,也没啥能阻止你的,它们各有各的依赖设置。比如说你想从 npm 拿 jQuery 和 Backbone,然后从 Bower 拿另外一些不太流行的模块。实际上,webpack 的文档有说,你应该用 npm 来替换 bower

简而言之,npm(CommonJS 风格)模块通常来说更加简单易懂,对 webpack 更容易优化,也就是能生成更小的包和更快的编译事件。

而实际上,这可能没那么大区别。比如说在那种巨型模块,比如 React ,也许从 npm 一次性加载, webpack 可能会有更好的优化,不过我还是建议你用 bower,这样你就只需要在一个地方管理你的依賴項了。

配置 webpack

当然你可以从命令行执行,然后带上一堆参数,不过这要是放在远程复杂的应用上面就歇菜咯,所以我们从 webpack 配置文件开始。

在 Rails 根目录下创建一个: webpack.config.js

var path = require('path');
var webpack = require('webpack');

var config = module.exports = {
  // the base path which will be used to resolve entry points
  context: __dirname,
  // the main entry point for our application's frontend JS
  entry: './app/frontend/javascripts/entry.js',
};

当然最终这个配置文件将会变成一个相当复杂样子,所以我们从最简单的开始,然后解释每部分是干什么用的。由于我们这篇指南里面会继续添加更多的配置,webpack 文档 里面有我们需要的配置属性的概述。

现在我们只需要一个入口文件,但是该属性也可以接受数组或者对象作为命名的入口点,我们之后会讲到。这里的重点是,该文件是你前端 JS 的核心 。任何没有被这个文件引用的(或者某些依賴項引用的)将不会被编译到包里。

下面我们将添加 output ,它指明我们将会吧编译好的包放到哪里。

config.output = {
  // this is our app/assets/javascripts directory, which is part of the Sprockets pipeline
  path: path.join(__dirname, 'app', 'assets', 'javascripts'),
  // the filename of the compiled bundle, e.g. app/assets/javascripts/bundle.js
  filename: 'bundle.js',
  // if the webpack code-splitting feature is enabled, this is the path it'll use to download bundles
  publicPath: '/assets',
};

现在我们添加 resolve 属性:

config.resolve = {
  // tell webpack which extensions to auto search when it resolves modules. With this,
  // you'll be able to do `require('./utils')` instead of `require('./utils.js')`
  extensions: ['', '.js'],
  // by default, webpack will search in `web_modules` and `node_modules`. Because we're using
  // Bower, we want it to look in there too
  modulesDirectories: [ 'node_modules', 'bower_components' ],
};

最后,plugins:

config.plugins = [
  // we need this plugin to teach webpack how to find module entry points for bower files,
  // as these may not have a package.json file
  new webpack.ResolverPlugin([
    new webpack.ResolverPlugin.DirectoryDescriptionFilePlugin('.bower.json', ['main'])
  ])
];

执行 webpack

在我们开始执行 webpack 之前,我们需要保证 Bower 的依賴項被安装好。如果你选择只用 NPM,那么运行 npm install 也就是你要做的所有啦。安装 Bower 依赖你要执行:

$ bower install

然后现在你应该会有个 bower_components/ 下面有 jquerylodash (如果你用的是上面的的 bower.conf 的话)

好了现在准备好了,让我们来执行一下 webpack 看看是不是所有的东西都可以用。在 Rails 根目录下,执行:

$ webpack -d --display-reasons --display-chunks --progress

这个命令会以 development 模式执行 webpack 一次,然后让它告诉你它做了什么。我们最终会把它做成自动执行。如果所有都没错的话,你会看到如下:

Hash: cfee07d10692c4ab1eeb
Version: webpack 1.4.14
Time: 548ms
        Asset    Size  Chunks             Chunk Names
    bundle.js  254088       0  [emitted]  main
bundle.js.map  299596       0  [emitted]  main
chunk    {0} bundle.js, bundle.js.map (main) 244421 [rendered]
    [0] ./app/frontend/javascripts/entry.js 73 {0} [built]
     + 2 hidden modules

上面说的是 webpack 创建了一个 “chunk” 叫做 bundle.js, 再有 sourcemap。所谓 chunk 是 webpack 切割你 Javascript 的块。现在我们给每个入口点创建一个 chunk。不过,如果你在许多个入口共享模块的话,或者用 code-splitting 功能(稍后讨论),那么 webpack 将会创建许多个 chunk,并且会将其命名成 1.1-bundle.js 之类的。

编译 webpack 包

如果你现在打开 app/assets/javascripts/bundle.js,你会看到你编译的 JavaScript。这个文件包含了许多个小(百把byte这样)的 webpack loader,用来协调你所有的模块,以及把它们提供给 require 依赖,这是标准 JavaScript 所没有的功能。

webpack 做的事情是,先通篇查找你的代码,然后把 require('lodash') 这样的替换成像这样:

var _ = __webpack_require__(/*! lodash */ 1);

这个 __webpack_require__ 方法,被注入到所有的模块里面,可以加载依赖。如果你是照着例子做的化,你可以在你的入口模块大概 50 行左右的地方看到像这样一段代码:

/* 0 */
/*!*******************************************!*\
  !*** ./app/frontend/javascripts/entry.js ***!
  \*******************************************/
/***/ function(module, exports, __webpack_require__) {

  var _ = __webpack_require__(/*! lodash */ 1);
  _.times(5, function(i) {
    console.log(i);
  });

在 Rails 视图中嵌入 webpack 包

如你所想,你只要像正常那样,插入编译之后的 JavaScript 包就可以了:

<%= javascript_include_tag 'bundle' %>

现在我们有了最基本的架子了,让我们开始讨论一些,做大应用的必须的功能吧。

全局模块(e.g.jQuery)

现在,你应该有 require 的概念了。通常,如果你想用 jQuery,你可以这样:

$ = require('jquery');
$('p').show();

但是,你可能回想:

  1. 把 jQuery 暴露给所有的模块,这样你就不需要每次在每个地方都写 $ = require('jquery') 了。
  2. 把 jQuery 作为一个全局变量,比如 window.$,这样它即使在模块之外也能用到。如果你打算在 Rails 里面以“松耦合”方式来用 JavaScript 的话,这确实是必须的。

这些对 webpack 来说都可以实现,虽然文档说得实在太含糊了。为了实现第一点 - 暴露 jQuery 给所有的模块 - 我们要用到 ProvidePlugin。所以,把它加到你的 webpack config 的插件列表里面:

new webpack.ProvidePlugin({
  $: 'jquery',
  jQuery: 'jquery',
})

现在这会把 $jQuery 变量注入所有的模块里面,你再也不需要去 require 它们了。

第二步 - 把 jQuery 暴露给 window - 我们需要一个 loader。在 webpack 中,loader 会给文件带来些变化。比如,我们之后会演示怎么通过 loader 把 CoffeeScript 转化为 JavaScript。

expose loader 会把暴露行为从模块转移到全局上下文,在我们的例子里面是 window。你可以在 webpack 的配置文件中配置 loader,比如我们需要把 CoffeeScript 转换,但是你也可以在require 模块的时候指定,在我看来,这样会更加明确的表达你 expose loader 的意图。

然后,在你的你的 entry.js 文件顶行,添加这样一句:

require('expose?$!expose?jQuery!jquery');

我知道,这个表达式看起来很赞,是不是!我们实际上执行了 expose loader 两次,把 jQuery 添加到 window.$window.jQuery

expose loader 工作方式像这样:require(expose?<libraryName>!<moduleName>)<libraryName> 部分就是 window.libraryName,而 <moduleName> 部分是你加载进来的模块,这里是指jquery。你可以链式的加载,通过 !来划分,就像我们上面做的那样。

Source maps

你可能注意到了 webpack 会自动的把 bundle.js.map 生成到了你的输出文件夹底下。webpack 生成的 source maps 相当赞。你可以下载一个包(而不是 10+ 个单独的文件,因为那样好慢),但是可以可以在独立文件中看到错误信息,因为它们确实在你的文件系统里面。而且当然,如果你用的是 CoffeeScript,那么你确实能在实际的 CoffeeScript 文件里面查看错误信息。

但是,默认的 Sprockets 会把 source map 打乱,给他们加该死的 ; ,导致浏览器不能解析它们。你可以像下面这样处理掉它们:

Rails.application.config.assets.configure do |env|
  env.unregister_postprocessor 'application/javascript', Sprockets::SafetyColons
end

把这个加到 config/initializers/assets.rb(或者对老版本的 Rails 直接放到 config/application.rb)。然后你清理一下 Sprockets 缓存: $ rm -r tmp/cache

现在,当你拿到异常,或者在浏览器查看加载的代码的时候,你可以看到实际的文件(e.g.entry.js)而不是巨大的包文件。

Virtual source paths

在 Chrome 中,当你在 inspector 的 Source 页查看的时候,webpack 默认生成的 source map 会把所有的东西都放到一个 ‘pseudo path’,webpack://。你可以把下面这句加到你 webpack 的 config.output 去,让它看起来好一点:

devtoolModuleFilenameTemplate: '[resourcePath]',
devtoolFallbackModuleFilenameTemplate: '[resourcePath]?[hash]',

好了,现在你的 ‘virtual’ 代码会被放到 domain > assets 文件夹底下了。

Sprockets 缓存和 source map

以我的经验, Sprockets 存 source map 方式相当激进。如果你觉得它们看起来和你想的不太一样,那么第一件事情你应该先清理 tmp/cache.

加载 CoffeScript 和其他变种语言

我们可以用 loader 来自动的转换那些用 CoffeeScript 或者类似语言写的模块。就像用 expose loader(上面演示的那样),你可以在 require 部分完成,不过我觉得这比把 loader 放到 webpack config 更好,这样我们就可以在原生的 JS 上 require ConffeeScript 了。

首先,在你的 package.json 添加 coffee-loader 模块,像这样:

$ npm install coffee-loader@0.7.2 --save-dev

现在,在我们的 webpack config,更新 config.resolve.extensions 列表,这样我们就可以调用 .coffee 文件而不需要指定扩展名了:

extensions:['', '.js', '.coffee']

最后,我们加一个 loader:

config.module = {
  loaders: [
    { test: /\.coffee$/, loader: 'coffee-loader' },
  ],
};

现在创建一个新的 CoffeeScript 文件: app/frontend/javascripts/app.coffee

_ = require('lodash')

module.exports = class App
  start: ->
    _.times 3, (i) -> console.log(i)

我们可以更新我们的 entry.js 来请求这个 CoffeeScript 模块:

require('expose?$!expose?jQuery!jquery');
var App = require('./app');

var app = new App();
app.start();

现在再执行 webpack ,然后回到你的浏览器,你会看到一切都如你所愿显示。如果你让 source maps 工作正常的话(如前面演示),你也能看到 CoffeeScript 源码的错误。

代码分割和懒加载模式

webpack 内置的一个很棒的功能就是它可以把一些特定的模块分割成它自己的 JS 文件,并只有当页面请求它们的时候才会加载进去。比如说,假设你再用 Ace code editor。它真的很棒,并且真的超强力,不过当然也很重,有 ~300KB。如果你只用这个 editor 的某些功能的话,是不是只有在需要的时候才加载这样更有意义?

用 webpack 你就可以用 require.ensure 来按需加载模块。 webpack 会指出哪个模块可以用于懒加载,然后把它们放到它们自己的 “chunk” 中。当你的代码被使用的时候,就会触发require.ensure 部分,webpack 会通过 JSONP 来下载这部分模块,这样你的代码就得以继续了。比如说:

function Editor() {};
Editor.prototype.open = function() {
  require.ensure(['ace'], function(require) {
    var ace = require('ace');
    var editor = ace.edit('code-editor');
    editor.setTheme('ace/theme/textmate');
    editor.gotoLine(0);
  });
};

var editor = new Editor();
$('a[data-open-editor]').on('click', function(e) {
  e.preventDefault();
  editor.open();
});

虽然这是个有点作的例子,不过你应该明白我上面说的意思了,只有当 editor 打开的时候,才会去下载和导入 ace 模块。

现在让我们来考虑一下多入口点和通用模块,这在大型应用中,用来组织前端 JS 是一种非常棒的方式。

多 entry points

使用了 webpack 之后,你很快就会发现它是专门给单页 JS 应用设计的,一般来说都会用一个或两个 JS 文件来渲染整个应用。

大多数的 Rails 应用有不同的页面,并且它们调用 JS 的方式通常都会像这样:

(function() {
  $('[data-show-dropdown]').on('click', function(e) {
    e.preventDefault();
    window.app.ui.showDropDown();
  })
)();

当你的应用开始成长,开始使用框架,比如 Backbone.js,在 View 里面来集中封装那些分散的方法调用以及事件处理,但是你仍然会这样:

(function() {
  var view = new window.app.views.SignupPageView({
    el: $('#signup-page')
  });
  view.render();
)();

这种方式可以概括成:“加载所有的 JS 库,然后在 Rails ‘bootstrap’ 页面的时候只用一点就好”。描述的不够准确,不过你应该懂的。

webpack 很灵活,这种处理方式还是游刃有余的,不过还有另外一种选择 - 如果适合你的应用的话 - 那就是不要让 Rails view 来处理 JS,让 webpack 接管每个页面要用到的 JS。让我们来比较一下这两种方式:

一个页面一个入口

在 Rails view 中不放 JavaScript 这种方式是很赞的。全都放在页面这种方式可以让你改变 HTML 的时候不用去处理缓存的 JS,而反过来,把 JS 只存在它们自己的文件中这种方式,可以让你分离 HTML 和 JS。

不过,这种方式有个显著的缺点。对于大多数 Rails 应用来说,“每页”的 JavaScript 量大概也就是几行,就像我们上面的 SignupPageView 的例子一样。这样的划分让我们需要额外增多请求次数,不值得。

除非你的页数不多,但是每页上 JS 不少,我建议还是不要这样做,虽然它在模式上面确实很棒,但是实际运作的时候,这会导致更多的开销。

如果我们上面的例子采用这种方式的化,我们将会有多个入口点文件:users_signup.js, users_login.js, photos.js, 等等之类。

每个文件看起来应该会像这样:

var SignupView = require('./views/users/signup');
var view = new SignupView();
view.render({
  el: $('[data-view-container]')
});

在 Rails view 里面我们只需要在页面中放一个有 data-view-container 属性的元素,再引用两个包,就这样。不需要 <script> 标签。

<%= javascript_include_tag 'users-signup-bundle' %>

在这个例子中看起来是很赞的,但是如果你有好多页面都是这样的话,你会觉得很操蛋。所以看看另外一种方式。

一个入口;多个模块

对于大多数 Rails 应用来说,这是最好的方式。你可以指定一个入口点(也许有几个也行)然后选择性暴露一些模块给全局(window)这样你可以在内置 <script> 标签里面用它们。

这里的方针是,对于 Rails 的 <script> 标签,尽可能少暴露全局量。不需要把你所有的通用方法或者内部模块给暴露出去,如果你用不到的话。

在我们上面的例子中,我们想把 Backbone Views 暴露出去,因为这是我们封装的逻辑。当然,不一定要用 Backbone。任何对象都可以导出和暴露出去,所以你可以用任何你想要的模式。

通过使用 webpack 的 expose-loader 我们可以把模块暴露给全局上下文。这里有些样板用例。

第一个建议是暴露全局对象(e.g. $app),然后把我们要用的全局模块挂载上去(或者我们在 Rails view 里面要用到的任何东西)。

// entry.js
var $app = require('./app');

$app.views.users.Signup = require('./views/users/signup');
$app.views.users.Login = require('./views/users/login');

// app.js
module.exports = {
  views = {
    users: {}
  }
}

# ./views/users/signup.coffee
module.exports = class SignupView extends Backbone.View
  initialize: ->
    # do awesome things

接下来我们要暴露我们的 app 模块,可以用 loader:

loaders: [
  {
    test: path.join(__dirname, 'app', 'frontend', 'javascripts', 'app.js'),
    loader: 'expose?$app'
  },
]

这会把 app.js 里面 module.exports 的模块挂到 window.$app,在 Rails view 里面的 <script> 标签里可以这样用:

(function() {
  var view = new $app.views.users.Signup({ el: $('#signup-page') });
  view.render();
})();

现在我们有了一个全局上下文,还有个最小化的模块暴露。app 对象现在非常简单,不过应该会变成在这里为每张页面启动任何需要的东西。比如说我们可以把 app 对象做成单例:

// app.js
var App = function() {
  this.views = {
    users: {}
  };
}

App.prototype.start = function() {
  // send CSRF tokens for all ajax requests
  $.ajaxSetup({
    beforeSend: function(xhr) {
      xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'));
    }
  });
}

module.exports = new App(); 

如果你不需要一个复杂的 app 对象,你可以从 entry.js 暴露另外的对象,就像暴露 $app 那样 - 当然你不要重复暴露 app.js 模块了。

多入口和共享模块组合

作为上述补充,在巨型 Rails 应用里面,做成多入口点应该会更有意义。简单的例子是,有 “public” 区(e.g. 登录页面,注册页面)以及 “authed” 区(只有登录认证之后才能看到的页面)。

在认证后的页面上,你的应用应该有比公共区更多更复杂的 JS,所以为什么不做成两个入口点,这样在访问公共区页面的时候可以下载更小的包?

当然,你可以用 Sprockets,比如说,javascripts/public-application.jsjavascripts/private-application.js,但是 webpack 有更棒的方式来共享依赖模块,把它们归类到一个通用 JS 文件。

比如说,很有可能你在公共区和私有取都会用到 jQuery。当然,你用 Sprockets 可以创建第三个 JS 文件, javascripts/shared-dependencies.js,但是 webpack 会通过分析你的代码,然后自动完成这个过程,它总会给你创建一个優化過的通用文件。如果你要用单入口模式,那 webpack 就不会给你创建这个通用文件了。

要打开这个功能,很简单,你只需要把 CommonsChunkPlugin 加到你的插件列表:

plugins: [
  new webpack.optimize.CommonsChunkPlugin('common-bundle.js')
]

它会在你的输出目录创建一个叫做 common-bundle.js 的文件,会包含很多(小的) webpack 引导代码,以及那些 webpack 判定被用到了多个入口点的通用模块。你可以把这个通用文件,和正常的入口文件一起,插入每张页面。

<%= javascript_include_tag 'common-bundle' %>
<%= javascript_include_tag 'public-bundle' %>

在生产模式使用 webpack

说到现在,上面那些内容已经足够你在 Rails 里面用 webpack 了,不过如果你要把 webpack 用到生产环境中的话,还有些事情是要完成的。

尤其是,当你 Rails 发布到生产环境中的时候,你当然会想压缩你的 JS,然后做成缓存增量添加(就像 Sprockets 自动完成那样)。

webpack 本来就支持压缩,以及缓存,不过你要用到 Rails 上,还得做些调整。

创建多个 webpack 配置文件

到目前为止,我们用的都是单一 webpack.config.js 文件。现在我们需要做一些改变,这样我们就可以在开发和生产环境选用不同的配置文件了。

让我们开始来创建一个 ‘base’ 配置,这是我们开发和生产配置文件都要用到的部分。创建 config/webpack/main.config.js 。在这里我们需要所有的基础配置,比如说你的入口点之类的。如果你一直按着文章说的来做的化,那么你只要从你的 webpack.config.js 文件里面拷贝一份就可以了。

var path = require('path');
var webpack = require('webpack');

var config = module.exports = {
  context: path.join(__dirname, '../', '../'),
};

config.entry = {
  // your entry points
};

config.output = {
  // your outputs
  // we'll be overriding some of these in the production config, to support
  // writing out bundles with digests in their filename
}

然后创建 config/webpack/development.config.js

var webpack = require('webpack');
var _ = require('lodash');
var config = module.exports = require('./main.config.js');

config = _.merge(config, {
  debug: true,
  displayErrorDetails: true,
  outputPathinfo: true,
  devtool: 'sourcemap',
});

config.plugins.push(
  new webpack.optimize.CommonsChunkPlugin('common', 'common-bundle.js')
);

因为这是开发模式配置文件,所以我们打开 debug, sourcemaps以及其他一些 webpack 的功能。

最后,创建 config/webpack/production.config.js

var webpack = require('webpack');
var ChunkManifestPlugin = require('chunk-manifest-webpack-plugin');
var _ = require('lodash');
var path = require('path');

var config = module.exports = require('./main.config.js');

config.output = _.merge(config.output, {
  path: path.join(config.context, 'public', 'assets'),
  filename: '[name]-bundle-[chunkhash].js',
  chunkFilename: '[id]-bundle-[chunkhash].js',
});

config.plugins.push(
  new webpack.optimize.CommonsChunkPlugin('common', 'common-[chunkhash].js'),
  new ChunkManifestPlugin({
    filename: 'webpack-common-manifest.json',
    manfiestVariable: 'webpackBundleManifest',
  }),
  new webpack.optimize.UglifyJsPlugin(),
  new webpack.optimize.OccurenceOrderPlugin()
);

在我们的生产配置文件里,我们重写了输出配置,对包块分配了名字 - 这些都是 webpack 自动能完成的,我们还告诉它需要把包生成到我们的 public/asset 文件夹下面,这些就像是asset:precompile 会做的那样。

我们还引入了 ChunkManifestPlugin,它会生成一个 JSON 文件,包含链接到包名的数字 ID。我们之后会说到为何要用它。同样,你要把这个插件加到你的 package.json 文件的devDependency 中。

"chunk-manifest-webpack-plugin": "~0.0.1"

在配置文件的最后,我们引入了 UglifyJsPlugin,它会压缩该压缩的一切,还有 OccurenceOrderPlugin,它会按引用频度来排序 ID,以便达到减少文件大小的效果。

webpack 预编译

在 Rails 发布的时候,asset:precompile 会被触发,执行打包和压缩任务,然后输出到 public/assets 去。当然 Sprockets 不会知道知道我们用了 webpack assets(而且,实际上,应该忽略它们),所以我们需要用一个新的任务来处理它们。

你可以用你喜欢的方式处理它们,不过我用的是 rake:lib/tasks/webpack.rb

namespace :webpack do
  desc 'compile bundles using webpack'
  task :compile do
    cmd = 'webpack --config config/webpack/production.config.js --json'
    output = `#{cmd}`

    stats = JSON.parse(output)

    File.open('./public/assets/webpack-asset-manifest.json', 'w') do |f|
      f.write stats['assetsByChunkName'].to_json
    end
  end
end

很简单对吧!我们调用 webpack,把我们的生产配置文件传进去,然后让它把结果做成一个 JSON 对象返回。结束的时候,我们解析 JSON,然后把它写到我们的 “assets manifest” 文件,看起来应该像这样:

{
  "common": "common-4cdf0a22caf53cdc8e0e.js",
  "authenticated": "authenticated-bundle-2cc1d62d375d4f4ea6a0.js",
  "public":"public-bundle-a010df1e7c55d0fb8116.js"
}

这和 Sprockets 做预编译 assets 时候生成的 manifest 文件是一样的 - 它会让我们的应用在把包引入 view 的时候用真实的文件名。

在 Rails 中添加 webpack 配置选项

打开 config/application.rb 然后添加:

config.webpack = {
  :use_manifest => false,
  :asset_manifest => {},
  :common_manifest => {},
}

我们会追加一些 helper ,会用上这些配置值的。下面,新建一个 initializer config/initializers/webpack.rb

if Rails.configuration.webpack[:use_manifest]
  asset_manifest = Rails.root.join('public', 'assets', 'webpack-asset-manifest.json')
  common_manifest = Rails.root.join('public', 'assets', 'webpack-common-manifest.json')

  if File.exist?(asset_manifest)
    Rails.configuration.webpack[:asset_manifest] = JSON.parse(
      File.read(asset_manifest),
).with_indifferent_access
  end

  if File.exist?(common_manifest)
    Rails.configuration.webpack[:common_manifest] = JSON.parse(
      File.read(common_manifest),
).with_indifferent_access
  end
end

好了,如果我们的 webpack[:use_manifest]true 的话,我们会预加载这些 manifest 文件并储存它们,以便之后访问。

下一步,你应该已经猜到了,就是为生产环境设置true(或者其他类似的生产环境,比如说 staging):

# config/environments/production.rb
config.webpack[:use_manifest] = true

在 view 中导入预编译 assets

最后一步 - 我们需要写两个 helper 来包含这些 assets(以及它们的 digest)到 view。

第一个 helper 我们叫做 webpack_bundle_tag:

# app/helpers/application_helper.rb

def webpack_bundle_tag(bundle)
  src =
    if Rails.configuration.webpack[:use_manifest]
      manifest = Rails.configuration.webpack[:asset_manifest]
      filename = manifest[bundle]

      "#{compute_asset_host}/assets/#{filename}"
    else
      "#{compute_asset_host}/assets/#{bundle}-bundle"
    end

  javascript_include_tag(src)
end

这个 helper 只是简单的检查一下我们是不是用了 manifest。如果用了,它就会去清单里面查找真实文件名;如果没有的话,它就会加载标准的包文件名。

第二个 helper 我们命名为 webpack_manifest_script。它会调用我们之前提到的 common manifest。为了解释我们怎么用它,让我们来看一个假设的通用清单文件:

{
  "0": "0-bundle-850438bac52260f520a1.js",
  "2": "2-bundle-15c08c5e4d1afb256c9a.js",
  "5": "authenticated-bundle-2cc1d62d375d4f4ea6a0.js"
}

你应该记得之前说过的,webpack 会给每个包都生成一个 ID,用以尽量减小文件大小。所以,每个编译之后的包 webpack 会生成一个内部 ID。默认的, webpack 会保存这些 ID 在common 包。问题在于,如果你改变了这些包,也就是说 common 包需要进行更新(目的是为了更新 common manifest),这样缓存会清空那些不需要的东西。

感谢有 ChunkManifestPlugin, webpack 可以被指定**不要**把这些直接写到 common 包里面。取而代之的是,把清单独立出来,当在浏览器运行的时候,将会查找全局变量webpackBundleManifest(写在插件的配置文件中)。所以我们的第二个 helper 可以简单的这样:

# app/helpers/application_helper.rb
def webpack_manifest_script
  return '' unless Rails.configuration.webpack[:use_manifest]
  javascript_tag "window.webpackManifest = #{Rails.configuration.webpack[:common_manifest]}"
end

好了就这样!在你的 layout 里面你可以看到我们这样用 helper:

<%= webpack_manifest_script %>
<%= webpack_bundle_tag 'common' %>
<%= webpack_bundle_tag 'public' %>

然后,在生产环境中,我们会这样:

    <script>
    //<![CDATA[ window.webpackManifest = {"0":"0-bundle-bdbd995368b007bb18a7.js","2":"2-bundle-7ad34cf6445d875d8506.js","3":"3-bundle-f8745c8bc2319252b6de.js","4":"4-bundle-ec8f5ae62f2e8da11aa1.js","5":"authenticated-bundle-933816ada9534488d12f.js","6":"public-bundle-8eb73d97201bd2e4951b.js"}
    //]]>
    </script>
    <script src="https://abc.cloudfront.net/assets/common-71a050793d79ce393b1e.js"></script>
    <script src="https://abc.cloudfront.net/assets/public-bundle-8eb73d97201bd2e4951b.js"></script>

在发布时执行 webpack 编译

如果你有用 Capistrano,就非常简单,只是要把一个自定义的 precompile_assets 任务追加到 Sprockets 的 precompile 之后,用来编译 webpack assets。

namespace :assets do
  task :precompile_assets do
    run_locally do
      with rails_env: fetch(:stage) do
        execute 'rm -rf public/assets'
        execute :bundle, 'exec rake assets:precompile'
        execute :bundle, 'exec rake webpack:compile'
      end
    end
  end
end

在这种情况,我在本地执行这些操作,因为我喜欢本地预编译,然后在 rsync 。你可能希望也喜欢这种方式,避免在你的生产环境上安装 webpack 和它所有的依赖。当然这也让编译会快很多 - webpack 生产时编译有可能会慢很多,如果你有许多模块的话。

在开发环境用 webpack

development.config.js 会简单多得多。只要这样调用就好:

webpack --config config/webpack/development.config.js --watch --colors

这种方式工作得很好,即使配合其他一些命令比如说 foreman,因为你可以在命令行执行 foreman start 的同事运行 Rails 和 webpack,像这样:

web: rails server -p $PORT
webpack: webpack --config config/webpack/development.config.js --watch --colors

好了,我想这样已经足够了。希望你可以在 Rails 里面顺利的开始用上 webpack!如果你有什么问题,请告诉我 :)