Home > php教程 > PHP开发 > body text

Integrate Rails, Webpack, Vuejs

高洛峰
Release: 2016-11-08 14:33:41
Original
1417 people have browsed it

Foreword

If you have ever read my article on getting started with vue + webpack, I mentioned in it that it is easy to combine with traditional MVC frameworks (Rails, ASP.NET MVC), etc. This article is mainly used to introduce how it is done and why I say this.

Let me admit it first! I don't always need to use SPA's architecture. While it's nice, for many small projects, or should we say projects where the UI/UX design itself is very simple - it's a waste of time. Therefore, the approach of this article is aimed at those whose project is not to use SPA with API architecture, but who feel that the existing MVC framework is sufficient for your development needs and want to retain most of the functions provided by these frameworks (this article Introduced for Rails only).

There are many ways to integrate Rails and Webpack on the Internet, and they are actually very good. This article only introduces one of the integration methods that I think is just right. Many of the concepts are not new, they are just compiled from past learning experiences.

Requirements

Let’s first talk about our requirements and why we don’t use asset pipeline. In fact, it’s simply the following list:

We want to use newly supported syntax such as ES2015, which requires Babel.

Many packages compiled on bower are incomplete, unsupported, or have other problems that cause us to use npm.

We want to manage dependency issues through modules.

assets-org and gem are not that great for managing front-end asset bundles.

I hope the deployment can be as simple as possible.

Preparation

Generally speaking, this integrated architecture uses npm (yarn is also available) to manage front-end resources, while gem is only responsible for the back-end part.

Install nodejs and npm

Install ruby ​​and Rails

Install Postgre SQL (since we will demonstrate deployment to Heroku, we will use Postgre SQL)

Step 1 - Create a project

# 由於示範部署至 Heroku
# 因此無法使用 sqlite3
$ rails _5.0.0.1_ new [project_name] --database postgresql
$ cd [project_name]
$ bundle install
$ rails db:create

$ rails g controller pages index # 建立測試頁面
$ rails server # 完成建立一個 Rails 專案
# 開啟 http://localhost:3000/pages/index
Copy after login

Import npm

$ npm init --yes

# 過程中,如果需要測試一下 webpack 指令則需要安裝全域
$ npm i -g webpack webpack-dev-server

# 安裝所需的前端函式庫
$ npm i jquery@^2.1.4 -S
$ npm i jquery-ujs@~1.1.0-1 -S
$ npm i lodash@~3.0.0 -S
$ npm i vue -S

# 安裝開發環境所需的套件與函式庫
$ npm i webpack \
    webpack-dev-server \
    webpack-manifest-plugin \
    extract-text-webpack-plugin -D

$ npm i babel-core \
    babel-loader babel-runtime \
    babel-plugin-transform-runtime \
    babel-preset-es2015 -D # Babel 相關

$ npm i coffee-loader coffee-script -D

$ npm i css-loader \
    style-loader \
    node-sass \
    sass-loader -D

$ npm i exports-loader -D # 匯出檔案
$ npm i expose-loader -D # 將物件加到全域(Javascript)
$ npm i file-loader url-loader -D
$ npm i imports-loader -D # 使用的模組可相依於全域
Copy after login

To facilitate your quick installation Package.json is provided below

"dependencies": {
  "jquery": "^2.2.4",
  "jquery-ujs": "^1.1.0-1",
  "lodash": "^3.0.1",
  "vue": "^2.0.5"
},
"devDependencies": {
  "babel-core": "^6.18.2",
  "babel-loader": "^6.2.7",
  "babel-plugin-transform-runtime": "^6.15.0",
  "babel-preset-es2015": "^6.18.0",
  "babel-runtime": "^6.18.0",
  "coffee-loader": "^0.7.2",
  "coffee-script": "^1.11.1",
  "css-loader": "^0.25.0",
  "exports-loader": "^0.6.3",
  "expose-loader": "^0.7.1",
  "extract-text-webpack-plugin": "^1.0.1",
  "file-loader": "^0.9.0",
  "imports-loader": "^0.6.5",
  "node-sass": "^3.11.2",
  "sass-loader": "^4.0.2",
  "style-loader": "^0.13.1",
  "url-loader": "^0.5.7",
  "webpack": "^1.13.3",
  "webpack-dev-server": "^1.16.2",
  "webpack-manifest-plugin": "^1.1.0"
},
"babel": {
  "presets": [
    "es2015"
  ],
  "plugins": [
    [
      "transform-runtime",
      {
        "polyfill": false,
        "regenerator": true
      }
    ]
  ]
}
Copy after login

Step 2 - Organizational Structure

In order to make deployment and subsequent settings simpler, we chose to extract the javascript and css resource files to their original directories, while also retaining the default related functions of Rails . The rough structure will be as follows. We will put all front-end resources under the client directory. Of course, if you want to name it webpack or frontend, it is also OK.

.
├── /app
│   ├── /assets
│   ├── /controllers
│   ├── /views
│   └── ...
├── /bin
├── /config
├── /db
├── /public
├── ...
├── /client
│   ├── /fonts
│   ├── /images
│   ├── /javascripts
│   ├── /stylesheets
│   ├── development.config.js
│   ├── production.config.js
└── ...
Copy after login

Unless you are command-controlled, you can use the editor to create relevant files and directories

$ mkdir -p client/javascripts
$ mkdir client/fonts
$ mkdir client/images
$ mkdir client/stylesheets
# 新增 Entry Point 檔案
$ touch client/javascripts/application.js
$ touch client/javascripts/home.coffee # 測試 coffee 使用
$ touch client/fonts/.keep
$ touch client/images/.keep
$ touch client/stylesheets/home.scss

$ touch client/development.config.js
$ touch client/production.config.js
Copy after login

Note that because this project will have Nodejs mixed in, you should add relevant settings in .gitignore.

# Node
node_modules
jspm_packages
.npm
.eslintcache
npm-debug.log*
pids
*.pid
*.seed
*.pid.lock
.nyc_output
.grunt
.lock-wscript
build/Release
Copy after login

Simple verification example

javascripts/home.coffee

console.log "Hello, CoffeeScript!"
Copy after login

stylesheets/home.scss

/* 記得放一張圖片 */.home-banner { 
 background-image: url('../images/banner.png');
 }
Copy after login

javascripts/application.js

import styles from '../stylesheets/home.scss'
import Home from './home'
Copy after login

Step 3 - Configure webpack

development.config.js

var path = require('path')
var _ = require('lodash')
var webpack = require('webpack')
var assetsPath = path.join(__dirname, '..', 'public', 'assets')
var ExtractTextPlugin = require('extract-text-webpack-plugin')

var config = {
  context: path.join(__dirname, '..'),
  entry: {
    /* 定義進入點與其檔案名稱 */
    application: [
      path.join(__dirname, '/javascripts/application.js')
    ]
  },
  output: {
    path: assetsPath,
    filename: '[name]-bundle.js',
    publicPath: '/assets/'
  },
  resolve: {
    extensions: ['', '.js', '.coffee', '.json']
  },
  debug: true,
  displayErrorDetails: true,
  outputPathinfo: true,
  devtool: 'cheap-module-eval-source-map',
  module: {
    loaders: [
      {
        test: require.resolve('jquery'),
        loader: 'expose?jQuery'
      },
      {
        test: require.resolve('jquery'),
        loader: 'expose?$'
      },
      {
        test: /\.js$/,
        loader: 'babel',
        exclude: /node_modules/
      },
      {
        test: /\.coffee$/,
        loader: 'coffee'
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)\??.*$/,
        loader: 'url?limit=8192&name=[name].[ext]'
      },
      {
        test: /\.(jpe?g|png|gif|svg)\??.*$/,
        loader: 'url?limit=8192&name=[name].[ext]'
      },
      {
        test: /\.css$/,
        loader: ExtractTextPlugin.extract('style', 'css')
      },
      {
        test: /\.scss$/,
        loader: ExtractTextPlugin.extract('style', 'css!sass')
      }
    ]
  },
  plugins: [
    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery'
    }),
    new ExtractTextPlugin('[name]-bundle.css', {
      allChunks: true
    })
  ]
}

module.exports = config
Copy after login

production.config.js

var path = require('path')
var _ = require('lodash')
var webpack = require('webpack')
var assetsPath = path.join(__dirname, '..', 'public', 'assets')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var ManifestPlugin = require('webpack-manifest-plugin')

var config = {
  context: path.join(__dirname, '..'),
  entry: {
    application: path.join(__dirname, '/javascripts/application.js')
  },
  output: {
    path: assetsPath,
    filename: '[name]-bundle-[chunkhash].js',
    publicPath: '/assets/'
  },
  resolve: {
    extensions: ['', '.js', '.coffee', '.json']
  },
  debug: true,
  displayErrorDetails: true,
  outputPathinfo: true,
  devtool: 'cheap-module-eval-source-map',
  module: {
    loaders: [
      {
        test: require.resolve('jquery'),
        loader: 'expose?jQuery'
      },
      {
        test: require.resolve('jquery'),
        loader: 'expose?$'
      },
      {
        test: /\.js$/,
        loader: 'babel',
        exclude: /node_modules/
      },
      {
        test: /\.coffee$/,
        loader: 'coffee'
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)\??.*$/,
        loader: 'url?limit=8192&name=[name]-[hash].[ext]'
      },
      {
        test: /\.(jpe?g|png|gif|svg)\??.*$/,
        loader: 'url?limit=8192&name=[name]-[hash].[ext]'
      },
      {
        test: /\.css$/,
        loader: ExtractTextPlugin.extract('style', 'css')
      },
      {
        test: /\.scss$/,
        loader: ExtractTextPlugin.extract('style', 'css!sass')
      }
    ]
  },
  plugins: [
    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery'
    }),
    new ExtractTextPlugin('[name]-bundle-[chunkhash].css', {
      allChunks: true
    }),
    new ManifestPlugin({
      fileName: 'client_manifest.json'
    })
  ]
}

module.exports = config
Copy after login

完成之後執行,測試看看是否有地方錯誤。如果您對於 webpack 並不陌生可以閱讀設定檔理解一下。

$ webpack --config client/development.config.js
$ webpack --config client/production.config.js
Copy after login

查看 public/assets 目錄底下看看編譯的結果。

第四步 - 整合 Rails

到此您應該已經理解,我們就是將前端的部份交給 webpack 處理,然後遵循 Rails 的架構將最終的資源檔編譯輸出到 public/assets。

現在的問題是,我們該如何讀取編譯好的資源呢?

由於我們希望能夠盡可能的遵循 Rails 的慣例,因此下一步我們在 app/views/layouts/application.html.erb 放入

<%= client_stylesheet_link_tag &#39;application&#39; %>
<%= client_javascript_include_tag &#39;application&#39; %>
Copy after login

上面是我們希望的作法(看起來就像是預設支援),所以接著下來我們需要做一些修改好讓 Rails 支援上面兩個 helpers。

首先因為 webpack 加上 hash 的編譯結果,Rails 並無法得知對應的檔案。於是我們需要在 production.config.js 使用ManifestPlugin 匯出 manifest 讓 Rails 得知如何對應檔案。

config/application.rb

require_relative &#39;boot&#39;

require &#39;rails/all&#39;

Bundler.require(*Rails.groups)

module RailsWepback
  class Application < Rails::Application
    config.webpack = {
      asset_manifest: {}
    }
  end
end
Copy after login

新增 config/initializers/webpack.rb

asset_manifest = Rails.root.join(&#39;public&#39;, &#39;assets&#39;, &#39;client_manifest.json&#39;)

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

完成這些前置作業之後我們可以讀取到 manifest 了,但您知道的;原生的 Rails 並沒有剛剛那兩個 helpers ,我們需要在app/helplers/application_helper.rb 加上

def client_javascript_include_tag(name)
  filename = "#{name}-bundle.js"
  asset_url = Rails.application.config.asset_host
  src = "#{asset_url}/assets/#{filename}"

  if Rails.env.development?
  elsif Rails.configuration.webpack[:manifest]
    asset_name = Rails.configuration.webpack[:manifest]["#{name}.js"]
    if asset_name
      src = "#{asset_url}/assets/#{asset_name}"
    end
  end
  "<script src=\"#{src}\"></script>".html_safe
end

def client_stylesheet_link_tag(name)
  filename = "#{name}-bundle.css"
  asset_url = Rails.application.config.asset_host
  src = "#{asset_url}/assets/#{filename}"

  if Rails.env.development?
  elsif Rails.configuration.webpack[:manifest]
    asset_name = Rails.configuration.webpack[:manifest]["#{name}.css"]
    if asset_name
      src = "#{asset_url}/assets/#{asset_name}"
    end
  end
  "<link rel=\"stylesheet\" href=\"#{src}\">".html_safe
end
Copy after login

這上面的 asset_host 是為了讓事情單純一點,我們選擇在 config/environments/development.rb 和 production.rb 加上

# 路徑請依據實際的狀況調整
config.action_controller.asset_host = &#39;127.0.0.1:3000&#39;
Copy after login

您可以採取任何您覺得更好的方式取得路徑。

完成第一階段

到這一步,其實我們已經完成最上面我們列出的需求了。

先在 terminal 中執行

$ webpack --config client/development.config.js --watch

開啟另一個 session 執行

$ rails server

最後在 views/pages/index.html.erb 中的任一 tag 補上 class="home-banner" 您就可以觀察到變化。webpack 在背後一直觀察client 底下檔案的變化,Rails 的開發伺服器則負責原來的工作,載入那些編譯後的檔案。

只是這樣每一次要測試都要打兩條指令很麻煩。

優化開發指令

新增 npm scripts

再往下走之前,我們可以先把 webpack 的指令與其單配的參數先整理到 npm script

"scripts": {
  "build:dev": "webpack --config=client/development.config.js --display-reasons --display-chunks --progress --color --watch",
  "build": "webpack --config=client/production.config.js -p"
}
Copy after login

接著為了讓我們往後只使用一道指令就能夠輕鬆寫意的開發。最簡單的方式就是使用 foreman

$ gem install foreman

安裝完 foreman 之後我們需要設定 Procfile 讓其為我們同時啟動兩道指令。

新增 Procfile.dev

Procfile support 類型程式預設使用 Procfile 為設定檔,如果直接使用該檔案可能會遇到問題,例如:當我們要使用 Heroku 的話,後續可能在部署的時候產生問題,主要是目前的設定僅限於開發階段使用,於是我們改使用其他的檔名 Procfile.dev。

在專案跟目錄下新增 Procfile.dev

web: bundle exec rails server -p 3000
webpack: npm run build:dev
Copy after login

使用 foreman

$ foreman s -f Procfile.dev

啟動後您應該看到類似的訊息:

18:30:56 web.1     | started with pid 16693
18:30:56 webpack.1 | started with pid 16694
18:30:57 webpack.1 |
18:30:57 webpack.1 | > example@1.0.0 build:dev /Users/andyyou/Workspace/sandbox/rails_vuejs_integrate_1/example
18:30:57 webpack.1 | > webpack --config=client/development.config.js --display-reasons --display-chunks --progress --color --watch
18:30:57 webpack.1 |
Hash: 2fdcbe01c557b347442e
18:31:00 web.1     | => Booting Puma
18:31:00 webpack.1 | Version: webpack 1.13.3
18:31:00 web.1     | => Rails 5.0.0.1 application starting in development on http://localhost:3000
18:31:00 webpack.1 | Time: 2893ms
Copy after login

支援 Hot Reload

基本上到了上一步就已經可以滿足大多數的開發情境,不過您可能也聽多了關於 Hot Replacement Mode (HRM) 的優點,如果我們也想支援呢?

原理上很單純,我們只需要讓前端資源檔換成是由 webpack-dev-server 所提供即可。

新增 devserver.config.js

注意到基本上這邊只有加入 webpack/hot/dev-server 和 publicPath 不同的差異,可以有更精簡的方式,不過這邊為了讓之後維護比較明顯直覺一點所以將其獨立一個檔案:

var path = require(&#39;path&#39;)
var _ = require(&#39;lodash&#39;)
var webpack = require(&#39;webpack&#39;)
var assetsPath = path.join(__dirname, &#39;..&#39;, &#39;public&#39;, &#39;assets&#39;)
var ExtractTextPlugin = require(&#39;extract-text-webpack-plugin&#39;)

var config = {
  context: path.join(__dirname, &#39;..&#39;),
  entry: {
    application: [
      &#39;webpack/hot/dev-server&#39;,
      path.join(__dirname, &#39;/javascripts/application.js&#39;)
    ]
  },
  output: {
    path: assetsPath,
    filename: &#39;[name]-bundle.js&#39;,
    publicPath: &#39;http://localhost:8080/assets/&#39;
    /* publicPath: &#39;/assets/&#39; */
  },
  resolve: {
    extensions: [&#39;&#39;, &#39;.js&#39;, &#39;.coffee&#39;, &#39;.json&#39;]
  },
  debug: true,
  displayErrorDetails: true,
  outputPathinfo: true,
  devtool: &#39;cheap-module-eval-source-map&#39;,
  module: {
    loaders: [
      {
        test: require.resolve(&#39;jquery&#39;),
        loader: &#39;expose?jQuery&#39;
      },
      {
        test: require.resolve(&#39;jquery&#39;),
        loader: &#39;expose?$&#39;
      },
      {
        test: /\.js$/,
        loader: &#39;babel&#39;,
        exclude: /node_modules/
      },
      {
        test: /\.coffee$/,
        loader: &#39;coffee&#39;
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)\??.*$/,
        loader: &#39;url?limit=8192&name=[name].[ext]&#39;
      },
      {
        test: /\.(jpe?g|png|gif|svg)\??.*$/,
        loader: &#39;url?limit=8192&name=[name].[ext]&#39;
      },
      {
        test: /\.css$/,
        loader: ExtractTextPlugin.extract(&#39;style&#39;, &#39;css&#39;)
      },
      {
        test: /\.scss$/,
        loader: ExtractTextPlugin.extract(&#39;style&#39;, &#39;css!sass&#39;)
      }
    ]
  },
  plugins: [
    new webpack.ProvidePlugin({
      $: &#39;jquery&#39;,
      jQuery: &#39;jquery&#39;
    }),
    new ExtractTextPlugin(&#39;[name]-bundle.css&#39;, {
      allChunks: true
    })
  ]
}

module.exports = config
Copy after login

增加 npm scripts

"dev": "webpack-dev-server --config=client/devserver.config.js --inline --hot --no-info"
Copy after login

更新 app/helpers/application.rb

因為目前有兩種開發模式,所以我們透過環境變數 HRM=true 來區分支不支援 HRM 模式。這邊遭遇的問題就單純是路徑不一樣,您絕對可自行調整優化,而這篇文章旨在記錄這個流程與概念。

def client_javascript_include_tag(name)
  filename = "#{name}-bundle.js"
  asset_url = Rails.application.config.asset_host
  src = "#{asset_url}/assets/#{filename}"

  if Rails.env.development?
    if ENV["HRM"]
      src = "http://localhost:8080/assets/#{filename}"
    else
      src = src
    end
  elsif Rails.configuration.webpack[:manifest]
    asset_name = Rails.configuration.webpack[:manifest]["#{name}.js"]
    if asset_name
      src = "#{asset_url}/assets/#{asset_name}"
    end
  end
  "<script src=\"#{src}\"></script>".html_safe
end

def client_stylesheet_link_tag(name)
  filename = "#{name}-bundle.css"
  asset_url = Rails.application.config.asset_host
  src = "#{asset_url}/assets/#{filename}"

  if Rails.env.development?
    if ENV["HRM"]
      src = "http://localhost:8080/assets/#{filename}"
    else
      src = src
    end
  elsif Rails.configuration.webpack[:manifest]
    asset_name = Rails.configuration.webpack[:manifest]["#{name}.css"]
    if asset_name
      src = "#{asset_url}/assets/#{asset_name}"
    end
  end
  "<link rel=\"stylesheet\" href=\"#{src}\">".html_safe
end
Copy after login

Procfile.devserver

web: bundle exec rails server -p 3000
webpack: npm run dev
Copy after login

使用指令

# 平常開發模式
$ foreman s -f Procfile.dev

# 支援 Hot Reload
$ HRM=true foreman start -f Procfile.devserver

# Ctrl + C 中止
Copy after login

漸進式的 Vue.js 是 MVC 框架的好朋友

在 Javascript 當道的今天我想您很容易找到很多關於 SPA 的主流作法。但這篇文章要說明的是把 JS 當作配角的作法。

首先我們需要更新設定檔,使其支援主角 Vue.js v2

$ npm i -g vue-loader vue-hot-reload-api -D
Copy after login

webpack 所有的 config loader 的部分補上:

{
  test: /\.vue$/,
  loader: &#39;vue&#39;}
Copy after login

另外 resolve 的部分,因為我們需要 standalone 版本的功能所以需要下面設定:

resolve: {
  extensions: [&#39;&#39;, &#39;.js&#39;, &#39;.coffee&#39;, &#39;.json&#39;],  /**
   * Vue v2.x 之後 NPM Package 預設只會匯出 runtime-only 版本,若要使用 standalone 功能則需下列設定
   */
  alias: {
    vue: &#39;vue/dist/vue.js&#39;
  }
}
Copy after login

首先新增元件

$ mkdir client/javascripts/components
$ touch client/javascripts/components/Car.vue
Copy after login

Car.vue 範例程式如下

<script>
export default {
  data () {
    return {
      brand: &#39;BMW 3 Series&#39;,
      mileage: 0
    }
  },

  mounted () {
    this.handle = setInterval(() => {
      this.mileage++
    }, 1000)
  },

  destroyed () {
    clearInterval(this.handle)
  }
}
</script>

<style scoped>
$pink: pink;

.brand {
  color: $pink;
  font-size: 1.4em;
}
</style>
Copy after login

更新 application.js

import styles from &#39;../stylesheets/home.scss&#39;
import Home from &#39;./home&#39;
import Vue from 'vue'
import Car from './components/Car.vue'

document.addEventListener('DOMContentLoaded', function () {
  new Vue({
    el: '#app',
    data: {
      message: 'Hello, Rails with Vue.js'
    },
    components: {
      car: Car
    }
  })
})
Copy after login

app/views/pages/index.html.erb

<div id="app" v-cloak>
  <h1 class="home-banner">{{ message }}</h1>
  <car inline-template>
    <div>
      I am <span class="brand">{{ brand }}</span>! I runned {{ mileage }}.
      The important thing is the variable from controller#action wheel is <%= @wheel %>
    </div>
  </car>
</div>
Copy after login

如果您曾使用過 Vue.js 可能會覺得這樣好奇怪,為什麼 component 裡面沒有