Rails Rails 5.1 使用 yarn 和 webpack 实战 (vue, 构建等)

reducm · April 30, 2017 · Last by reducm replied at June 18, 2019 · 17169 hits
Topic has been selected as the excellent topic by the admin.

之前就有听到 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, 本人水平有限,有问题请大家及时指出。

Rails 5.1 配置 Webpack

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'
  1. 执行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
    
  2. 执行rails webpacker:check_yarn,项目根目录便会多了一个package.jsonyarn.lock文件,熟悉 node 的同学知道,这是 npm 和 yarn 会用到的包管理文件,里面还可以添加一些执行命令例如 scripts 等

  3. 执行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

了解完 webpack 初始化后执行了的操作和生成的目录结构,下面我们来试试在开发环境里跑跑 vue

  1. 上面我们看到,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

  2. 接下来把 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,你就可以加载好对应的静态文件
    
  3. 接下来要讲讲的是开发环境下启动项目的变化。以前我们在开发环境下一般跑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就可以看到结果了。

  4. 这时我们随便改改 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' %>

获取 webpack 静态文件的 asset_pack_path helper

过去 assets pipeline 年代,我们可以在 view 里使用asset_pathimage_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') %>";

生产环境 deploy webpack 打包

什么?原来的assets:precompile会自动跑新的 taskswebpacker:compile?会自动处理页面 xxxx_pack_tag 引入的打包文件?webpack 的 production 环境还会自动读取配好的 cdn host? 那基本上如果你的 cdn 不是用回源而是用华顺 ruby-china 上传又拍云的方式,只需上传脚本多加个 public/packs 的文件夹就好了...

更新

开发环境中,例如加载字体文件遇到 CORS 的跨域问题解决方法

//development.server.js里头加入:
headers: {
    'Access-Control-Allow-Origin': '*'
}

加入 Staging 环境时,CDN HOST 的问题

开发中常见我们会加入预生产环境 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

所以如果用 capistrano 或者 mina 的话,不需要对 webpacker 做任何处理么?我还没尝试新的体系,但是很奇怪搜 capistrano webpacker 搜不到相关的内容 mina webpacker 倒是有 PR

Reply to jasl

capistrano 实测是不用的,跟以前一样跑 assets:precompile就行了。但是会有一个 yarn install deps 的过程,慢得飞起,而把 node_modules 设置为 link_dir 会出问题。😂

Reply to jasl

https://github.com/rails/webpacker#deployment

Webpacker hooks up a new webpacker:compile task to assets:precompile, which gets run whenever you run assets:precompile.

Reply to jasl

内存小的机器小心内存炸,webpack babel 编译还是很吃内存的

是不是使用 webpack 之后,全部的 js 都通过 yarn 去安装了,不能混着以前的 gem 一起用?

Reply to QueXuQ

可以的,原来的 assets pipeline 还在

huacnlee mark as excellent topic. 03 May 11:51

👍期待下集可以顺便讲讲和 turbolinks 的和谐相处~~

Reply to imwildcat

改改估计应该也可以支持 webpack compile assets local with capistrano

Reply to xofred2

当然可以的,引入 webpack 和 yarn,复杂的部分只是在于开发和构建上,结果出来都是在页面引用 JS 而已。只要处理好 assets pipeline 和 pack 出来的静态文件引用顺序,两者是可以并存的。

另外,打开 webpacker 的源码,找到 javascript_pack_tag, 实际上也是调用了 javascript_include_tag


  def javascript_pack_tag(name, **options)
    javascript_include_tag(Webpacker::Manifest.lookup("#{name}#{compute_asset_extname(name, type: :javascript)}"), **options) #从生成出来的manifest.json找到对应静态文件编译后的文件名
  end

```

```json
//manifest.json example
{
  "app.js": "/packs/app-c34667aca39442f786a0.js",
  "application.js": "/packs/application-912810e12140b29375d2.js",
  "page/Actor.js": "/packs/page/Actor-81e43cb17759d171104e.js",
  "page/Actors.js": "/packs/page/Actors-638afba3889927791378.js",
  "page/Movie.js": "/packs/page/Movie-c4cdf69886b211c14574.js",
  "page/Movies.js": "/packs/page/Movies-f04878a089ecec487810.js",
  "page/Search.js": "/packs/page/Search-f02e3e7031931ef876b9.js",
  "page/Tag.js": "/packs/page/Tag-75d6da5528f200354587.js",
  "page/Tags.js": "/packs/page/Tags-8aa0b6c7cc811618c51f.js",
  "router/index.js": "/packs/router/index-10afda1a26fd89c89880.js",
  "store/action_types.js": "/packs/store/action_types-aa75ce66ee2e3a622d51.js",
  "store/index.js": "/packs/store/index-5389b0582084ca0e4751.js",
  "store/module/actor.js": "/packs/store/module/actor-b67a50e248408fa8fa6a.js",
  "store/module/movie.js": "/packs/store/module/movie-20441ad04b207c3337cc.js",
  "store/module/search.js": "/packs/store/module/search-c7cae47a3a1058a7a6b5.js",
  "store/module/tag.js": "/packs/store/module/tag-8952d2f3ce45bf2fd3f7.js",
  "store/mutation_types.js": "/packs/store/mutation_types-e74d876aaaabbe43d43f.js"
}

所以 webpack 的静态文件要使用 turbolink,在页面里这样用就行

<%= javascript_pack_tag "application", :"data-turbolinks-track" => true %>
Reply to saiga

我把 node_modules 加入 linked_dirs,没遇到什么问题。

LINKED_DIRS = %w(
  data
  log
  tmp/pids tmp/cache tmp/sockets
  vendor/assets/bower_components
  node_modules
)
set :linked_dirs, fetch(:linked_dirs, []).push(*LINKED_DIRS)
Reply to saiga

我在 link node_modules 也出现问题了,排查是 postcss 导致的,需要在 webpack 的配置项 plugins 中加入如下配置

new webpack.LoaderOptionsPlugin({ options: { postcss: {} } }) 

@yuhaidonghd 没问题估计是大家依赖的组件版本不一样

"阮一峰老师的各种教程" ✋

官方文档 👌

用了 rails5 + wepack 做 docker 镜像的时候,与之前不用 webpack 有什么区别吗

为什么不前后端分开,前端去做那些,rails 只写 API 呢。

将来 app 开发也很容易调用 API

Reply to anklos

总有不需要这么干的时候的嘛。

webpack 这玩意给我的感觉就像以前写 Makefile 一样,一坨翔

Reply to jasl

capistrano 如果不想每次编译的话需要对 assets path 稍微改一下,我是直接把 pack 打包到 public/assets 里面了

可以用 foreman 这个 gem 一次启动 rails s 和 webpack dev server。不过默认的 web 端口会变成 5000。

更新了开发中遇到的一些小问题

22 Floor has deleted

写的已经够详细,可是不是很清楚怎么在项目中引入 bootstrap 或者 semantic-ui. 可否提供点思路?

Reply to helapu

下载 semantic-ui 的 js,css 文件到 vendor,然后在 application.css 和 application.js 中 require。

Reply to fzzf0618

现在项目不是已经用 webpack 了么 可否有适合 webpack 的方式呢

Reply to anklos

我也是這麼想,正在試驗這種想法。前後端代碼分離的話,前端可以有效利用 CDN 和緩存。

👍马上试试

楼主你好,最近升级项目,之前 Rails5.0.0.1 的时候为了用上 ES6 的特性,用了 babel-transpiler 和 browserfily-rails,现在升级到 5.1 直接用上了 webpack。本地开发的时候非常顺利,但是最后部署到服务器上的时候貌似 rails 的 assets pipeline 最后还是会把所有的 assets 打包成一个 application.js 文件,而且 webpack 使用的是 uglifierJS 最近的版本似乎不支持 es6 里面的某些语法,导致部署失败。

请问楼主有遇到过这个情况吗?

Reply to naivecai

我遇到了,解法:新建 .babelrc

{
  "presets": [
    [
      "env",
      {
        "modules": false,
        "targets": {
          "uglify": true
        }
      }
    ]
  ],
  "plugins": [
    "transform-object-rest-spread"
  ]
}

plugins 可选。

Reply to FrankFang

谢谢!回去试试。

但是还是不甚理解,为什么一定要写.babelrc 而不是在 config/webpack/production.js 里面对 webpack 进行配置呢?是因为需要使用 babel 来翻译成 ES5 的缘故吗?

Reply to naivecai

JS 社区喜欢一个工具一个 rc ……

我这的前端开发给了点反馈,在 Rails 项目中打包出来的 JS 比独立前端项目打包的 JS 大了很多(2-3 倍), webpack-dev-server 会莫名的报 nodejs 内存溢出。 所以,我这最后还是拆成前后端各一个项目来做了

Reply to oldfritter

Rails 里的 webpack 默认配置文件 shared.js,是会把 packs 文件夹下根目录 js 文件都当作 entry 打包,会导致不必要的 entry 重复打包的问题。这里可以手动指定一下自己的 entry,这样会大幅减少文件体积和提高打包速度。

example:

module.exports = {
  //手动指定两个入口
  entry: {
    'main': ["./app/javascript/packs/main"],
    'operate/main': ["./app/javascript/packs/operate/main"]
  },
  //下面是原来的配置
  //packPaths.reduce( 
    //(map, entry) => {
      //const localMap = map
      //const namespace = relative(join(paths.source, paths.entry), dirname(entry))
      //localMap[join(namespace, basename(entry, extname(entry)))] = resolve(entry)
      //return localMap
    //}, {}
  //),

  output: {
    filename: '[name].js',
    path: resolve(paths.output, paths.entry),
    publicPath
  },
........
}

Reply to reducm

我觉得你应该把不需要打包的文件都移出 packs,毕竟 pack 就是打包的意思。

$ app/javascript
# tree -L 1
.
├── components
├── icons.js
├── initializers
├── locales.yml
├── packs
├── routers
├── utils
└── vendors

还是觉得 assets pipeline 这一套比 webpack 好用啊

公司前端的小伙新技术玩得很溜,但这两个项目在开发期需要到服务端很多的交互数据去调试,最初后端和前端拆分开在两个项目里写,后来项目时间的原因后端的同学也加入到写前端代码,写完后后端同学拖代码再各种合并部署到staging测试,中间出现bug调试再上线再测试,浪费了不少时间,最后不得已地又把前端代码移到rails,又花了不少时间做项目构建, 手动把webpack和Rails前端管理体系结合,什么编译完后各种跑各种复制啊,前端里也要做环境区分啊,苦不堪言。

弱弱的问一句 前后端项目不是 分别部署吗,后端只提供接口,前端只负责页面展示那一块? 感觉不冲突啊

Reply to FrankFang

这种处理方式也是可行的。实际上我们现在一个在做的项目,已经会有多个分离出来的 App 入口,还有后台的前端部分也分离出来了,对于我们来说在 webpack 配置文件里做好入口判断会清晰点。

Reply to zouyu

真要对比出来,可能要新开篇去说了。只能说下对于小公司来说,webpacker 有点给我们带来这些好处: (1)以前前端同学写完后推代码,等运维同学拉代码部署到运行容器,接上数据查看有没出错,出错了还要重来一遍。现在直接本地可以同步效果,在同一个环境下不会再存在一些额外需要去把 API 服务器调跨域之类的问题。 (2) precompile 后的工作和 assets pipeline 差不多,原来的部署脚本也一步到位了 (3) 像基本我们百分百对外项目静态文件都用到 CDN 的,用手动分离的方式中间要做很多的整合工作。 (4) 和 assets pipeline 对比的优点就更不用说啦,yarn 对比 gem,babel es6 es7 特性对比 coffee,webpacker 自带处理的各种 vue-loader 啊,portcss 之类的,assets pipeline 都会很别扭。

综合地来说,就是对前端用得比较深的,都知道现在 webpacker 是大一统,前端最前沿的技术都在这里可以体验。assets pipeline 和 turbolinks 是能顶大部分的需求,还是看项目情况使用吧。

你好,你说的这个 require('calendar') 是在 ruby 哪里添加的,不太明白

Reply to KK

这是一个例子,注意目录结构,外部的 calendar.js 里的这句话,会引用calendar/目录下 index.js 文件

您好,我在配置 rails webpacker vue 的时候,在 main.js 中引入.scss 文件时候启动开发模式报错。

import Vue from 'vue/dist/vue.esm';
// import Vue from 'vue/dist/vue';
import router from './routes';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import locale from 'element-ui/lib/locale/lang/zh-CN'

import './styles/index.scss' // global css

Vue.use(ElementUI, { size: 'small', zIndex: 3000 });  // size用于改变组件的默认尺寸,zIndex设置弹出框的初始z-index(默认值:2000)
Vue.use(ElementUI, { locale })

const app = new Vue({
  router
}).$mount('#app')

index.scss 中引入了 variables.scss 和 sidebar.scss。 错误如下,

@mz2test 这个报错是 webpack 相关,scss 找不到变量的定义,建议检查下 loader 的加载有无问题

@reducm 问个问题,升级了之后,内联 $.xxxx 这种还能运行吗?(content_tag)

Reply to geniousli

@geniousli 可以的

不过目前在实际项目中,我们项目管理上还是使用完全的前后分离较为多。即前端分离出去,并没有继续使用 webpacker。

You need to Sign in before reply, if you don't have an account, please Sign up first.