之前就有听到 Rails 5.1 会迎来大更新,主题是 Loving Javascript,并会迎来 yarn 和 webpack,目前前端小伙伴们最强大的工具。相信稍有关注前端发展的同学,均听过当今前端圈子里流行的种种名词:es6 es7 vue react react-native cs-modules等等
, 因为 node 的出现,良好的包管理、服务端渲染等等新特性,另得前端的同学们写的代码在执行前有了更多的构建方式和选择,前端发展空前繁荣。然而,作为走在 web 开发提供最佳实践的 Rails,直到 5.0 的版本,还是用着 3.1 版本时出现 assets pipeline 作为前端文件包管理,虽然有不少贡献者提供诸如browserify-rails vue-rails
等等的 gem,企图为 Rails 带来多少便利,但仍难改变 Gem 方式管理前端库的更新不及时、es6/es7 新特性难支持,构建不灵便等缺陷。
我司最近其中两个项目霸屏 和 电商,一个用到 vue,一个是要求用 react,正是前端交互均比较复杂的例子,使用 webpack 去做构建都是必须的。公司前端的小伙新技术玩得很溜,但这两个项目在开发期需要到服务端很多的交互数据去调试,最初后端和前端拆分开在两个项目里写,后来项目时间的原因后端的同学也加入到写前端代码,写完后后端同学拖代码再各种合并部署到 staging 测试,中间出现 bug 调试再上线再测试,浪费了不少时间,最后不得已地又把前端代码移到 rails,又花了不少时间做项目构建,手动把 webpack 和 Rails 前端管理体系结合,什么编译完后各种跑各种复制啊,前端里也要做环境区分啊,苦不堪言。
现在 Rails 5.1 出来后,一切都过去了!yarn 解决了 js 之前模块交叉依赖、版本混乱和重复加载的问题,强大的构建工具 webpack,能帮助你处理文件依赖,按需加载,还有强大的 webpack-dev-server,你所能想到当前大部分前端构建和优化的问题,它有很好的解决方案。并且,它俩现在也能跟 Rails 很好的结合在一起了!
当然引入新的解决方案,也会带来新的问题。例如 es6 的语法,webpack 的配置等,都会带来新的学习成本。你可能会说为啥前端要搞得那么复杂吧,但他们也是切切实实解决了很多实际问题,优化了过去很多的不足。本文不会去深入讲 webpack es6 等前端范畴的技术细节,webpack es6 react 等,推荐大家可以去看看阮一峰老师的各种教程,深入浅出。文章引用了官方 Webpacker gem, 本人水平有限,有问题请大家及时指出。
Rails5.1 新项目使要使用 webpack,只需要执行
gem install rails -v '5.1'
rails new [your_project] --webpack
#也可以 rails new [your_project] --webpack=react 直接装上react
楼主是从旧项目升级,新开个 rails5 的 git 分支,项目是从4.2
升级到5.1
, 修改 Gemfile
gem 'rails', '5.1'
gem 'webpacker'
执行bundle update rails
,可能在运行的过程中会遇到一些 rails 4.2 时例如 sass-rails, coffee-rails, rspec-rails, jbuilder 之类的依赖问题,我是比较粗暴地去掉版本号跑过。跑完后,运行rails webpacker
会发现 Rails 已经多了自带的各种 webpack 命令。
jasav% rails webpacker
Available webpacker tasks are:
webpacker:install
webpacker:compile
webpacker:check_node
webpacker:check_yarn
webpacker:verify_install
webpacker:yarn_install
webpacker:install:react
webpacker:install:vue
webpacker:install:angular
执行rails webpacker:check_yarn
,项目根目录便会多了一个package.json
和yarn.lock
文件,熟悉 node 的同学知道,这是 npm 和 yarn 会用到的包管理文件,里面还可以添加一些执行命令例如 scripts 等
执行rails webpacker:install
, 项目新建一个文件夹config/webpack
,里面包含了开发环境、开发环境 webpack 服务器和生产环境等设置 (对了,记得在项目的.gitignore
里面加入 node_modules,不然会把开发环境里的安装包都提交到项目代码里了)。同时,在 app 目录下多了个javascript
目录 (注意不是 app/assets/javascripts),里面还有个packs
的目录,用了 webpacker 后,以后各种前端文件就写在这里了,区别于以前的app/assets
目录。觉得config/webpack
配置比较多,简单说说每个文件的作用
▾ webpack
▸ loaders //各种文件预处理的模块,例如vue,coffee,assets等,下面的shared.js会require到webpack的module里头
configuration.js //主要是加载paths.yml文件,定义好项目路径,包括还指定了CDN前缀
development.js //开发环境配置文件,看到里头合并了shared.js, 还有用到sourcemap
development.server.js //开发环境下,webpack-dev-server的运行配置文件
development.server.yml //webpack-dev-server的host,port配置文件
paths.yml //目录路径配置文件,例如制定了source是app/javascript
production.js //生产环境配置文件
shared.js //顾名思义叫shared.js,会被每个环境都合进去的一个主配置文件,例如要添加各种loader module, 或者添加plugin,都写在这里
test.js //测试环境的配置文件
了解完 webpack 初始化后执行了的操作和生成的目录结构,下面我们来试试在开发环境里跑跑 vue
上面我们看到,rails 加了直接安装 vue 的 task, 执行
rails webpacker:install:vue
执行后我们会发现先跑了yarn add
的安装指令,随后package.json
文件多了 vue、vue-loader、vue-template-compiler 几个库,同时还贴心地在app/javascript/packs
里加了两个文件:app.vue 和 hello_vue.js
接下来把 js 引入到我们的页面,在 assets pipeline 年代,我们写好了 css 和 js 后,会在例如layouts/application.html.erb
加入<%= stylesheet_link_tag 'xxx' %>
或者<%= javascript_include_tag 'xxx' %>
。在 webpack 年代,做法也是类似,我们可以在layouts/application.html.erb
用以下标签引入静态文件:
#这是这个示例的
<%= javascript_pack_tag 'hello_vue' %>
#也举举官方的例子, 例如你有如下入口
// app/javascript/packs/calendar.js
require('calendar')
#例如你有如下的目录结构
app/javascript/calendar/index.js // gets loaded by require('calendar'),自动会读引入目录下的index.js
app/javascript/calendar/components/grid.jsx
app/javascript/calendar/styles/grid.sass
app/javascript/calendar/models/month.js
<%# app/views/layouts/application.html.erb %>
<%= javascript_pack_tag 'calendar' %>
<%= stylesheet_pack_tag 'calendar' %>
通过Rails的xxxx_pack_tag helper,你就可以加载好对应的静态文件
接下来要讲讲的是开发环境下启动项目的变化。以前我们在开发环境下一般跑rails s
就够了,如果用到一些 sidekiq、solr 之类的库,也会多开一个窗口跑其他的进程。Rails 5.1 在引入 webpack 后,在开发时还要启动
bin/webpack-dev-server
听上去多开个窗口很麻烦(用 tmux, 谁用谁知道),但 webpack-dev-server 除了增量构建、livereload 外,还有 Hot Module Replacement等好处。另外,楼主之前在用默认配置跑 webpack-dev-server 时,见到静态文件加载编译完日志一切正常,但用 localhost 发现会有请求打不进 webpack 的问题,只要修改config/webpack/development.server.yml
的 host 为 127.0.0.1 就好,这时 Rails 的 xxx_pack_tag helper 也会自动修改 host。
hello_vue.js 代码很简单,引入 app.vue,然后在 html body 上显示个大大的"Vue"。这时我们访问http://localhost:3000
就可以看到结果了。
这时我们随便改改 app.vue 里面的文字或 css,也可以立刻见到 webpack-dev-server 检测到文件变动,之后页面会自动更新,cool!
webpack 另外一个带来的好处是可以在 js 代码直接引入图片/svg 之类的静态文件,并处理运行环境时的依赖关系,这里直接引用官方 gem 的例子
// React component example
// app/javascripts/packs/hello_react.jsx
import React from 'react'
import ReactDOM from 'react-dom'
import helloIcon from '../hello_react/images/icon.png'
import '../hello_react/styles/hello-react.sass'
const Hello = props => (
<div className="hello-react">
<img src={helloIcon} alt="hello-icon" />
<p>Hello {props.name}!</p>
</div>
)
webpack 遇到 js 文件里引入到静态文件或图片,会自动把依赖到的文件打包到 output(public/packs),所以要注意,上面的文件例如引入了 css 文件,除了 js 文件外,我们在页面也要 include css 进来。
<%= stylesheet_pack_tag 'hello_react' %>
过去 assets pipeline 年代,我们可以在 view 里使用asset_path
或image_tag
等去调用 assets 里面的静态文件,又或者可以在 scss 里使用 image-url 等 helper,Rails 会帮我们处理不同环境下对这些静态文件的引用路径问题。
现在 webpacker 也提供了一个asset_pack_path
的 helper,方便我们在不同地方引用javascirpt/pack
下的静态文件,
<%= asset_pack_path 'hello_react.css' %>
<% # => "/packs/hello_react.css" %>
<img src="<%= asset_pack_path 'calendar.png' %>" />
<% # => <img src="/packs/calendar.png" /> %>
那如果我们想在 webpack 管理的文件里,引入过去 assets pipeline(sprockets assets) 里的静态文件呢?得以于rails-erb-loader
(loaders 里),我们只要给 js 文件加个 erb 后缀,就可以通过 helper 引用回 app/assets 里的静态文件
// app/javascript/my_pack/example.js.erb
<% helpers = ActionController::Base.helpers %>
var railsImagePath = "<%= helpers.image_path('rails.png') %>";
什么?原来的assets:precompile
会自动跑新的 taskswebpacker:compile
?会自动处理页面 xxxx_pack_tag 引入的打包文件?webpack 的 production 环境还会自动读取配好的 cdn host? 那基本上如果你的 cdn 不是用回源而是用华顺 ruby-china 上传又拍云的方式,只需上传脚本多加个 public/packs 的文件夹就好了...
更新
//development.server.js里头加入:
headers: {
'Access-Control-Allow-Origin': '*'
}
开发中常见我们会加入预生产环境 staging, 如果 staging 环境也需要使用到 cdn host,webpacker 里的configuration.js
需要做如下的修改
const ifHasCDN = env.ASSET_HOST !== undefined && ( env.NODE_ENV === 'production' || env.NODE_ENV === "staging")
const devServerUrl = `http://${devServer.host}:${devServer.port}/${paths.entry}/`
const publicUrl = ifHasCDN ? `${env.ASSET_HOST}/${paths.entry}/` : `/${paths.entry}/`
const publicPath = ( env.NODE_ENV !== 'production' && env.NODE_ENV !== "staging") ? devServerUrl : publicUrl