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

reducm · 2017年04月30日 · 最后由 reducm 回复于 2019年06月18日 · 15862 次阅读
本帖已被设为精华帖!

之前就有听到 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

jasl 回复

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

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.

jasl 回复

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

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

QueXuQ 回复

可以的,原来的 assets pipeline 还在

huacnlee 将本帖设为了精华贴 05月03日 11:51

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

imwildcat 回复

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

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 %>
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)
saiga 回复

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

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

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

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

官方文档 👌

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

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

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

anklos 回复

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

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

jasl 回复

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

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

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

22楼 已删除

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

helapu 回复

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

fzzf0618 回复

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

anklos 回复

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

👍马上试试

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

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

naivecai 回复

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

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

plugins 可选。

FrankFang 回复

谢谢!回去试试。

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

naivecai 回复

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

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

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
  },
........
}

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前端管理体系结合,什么编译完后各种跑各种复制啊,前端里也要做环境区分啊,苦不堪言。

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

FrankFang 回复

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

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 哪里添加的,不太明白

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 问个问题, 升级了之后, 内联 这种还能运行吗?(content_tag)

geniousli 回复

@geniousli 可以的

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

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册