Je cherche depuis quelques temps à créer des clients multi-plate-formes (IOS, Android), mais qui tournent aussi sur un ordinateur (Web). Pour le back-end, Rails reste mon framework préféré. Côté front-end, je ne veux pas me mettre à faire du natif -je crois au code portable-, j'ai récemment travaillé avec Ionic 1 & 2, mais je suis moins enthousiaste. Ionic est super pour tout ce qu'il proposent en "buildt-in" et AngularJs 2 se base sur du TypeScript qui lui permet de porter les solutions en natif sur divers plate-formes, mais je trouve tout ça assez lourd et beaucoup trop spécifique à mon goût.

Je veux que mon code soit le plus léger, portable et proche du JavaScript possible.

J'entends parler autour de moi de plus en plus de développeurs basculant sur du React. Framework développé par Facebook et Airbnb, ça apporte un sérieux certain, le nombre de "core-developers" est aussi bien plus important que sur Angular ! Même si ces deux frameworks sont maintenus par les GAFA et autres NATU - dont je me méfie énormément -, je me suis mis à suivre des cours sur Codecademy et j'ai vraiment apprécié le concept.

React semble répondre à plusieurs de mes objectifs front-end. Tout d'abord, le concept du buffer de DOM est bon, ensuite, ce n'est pas un MVC ce qui évite les configurations alambiquées pour l'utilisation des composants que l'on crée, et finalement c'est vraiment proche du JavaScript, fluide -donc propre-

Objectif

Mon objectif est de tester la création d'une petite application SaaS Rails utilisant ReactJs en front, développée en BDD/TDD qui puisse tourner sur un navigateur et être facilement intégrable à Cordova.

Dans cet article je vais présenter comment créer rapidement une interface ReactJS listant des tâches à réaliser tout en utilisant la BDD avec cucumber.

Mise en place

J'ai besoin de Ruby 2.4.1, de NodeJs 6.4, de Rails 5.1.0 ainsi que de bundler, yarn et foreman.

Pourquoi ?

  • Rails 5.1 : Assets pipeline est mort, vive webpacker. La version 5.1 de Rails apporte une intégration légère et complète des outils développés en Javascript (webpacker).
  • NodeJs 6.4 : Depuis que Heroku a fortement déconseillé l'utilisation de la gem therubyracer, il nous faut un interpréteur JS pour ExecJS.
  • Yarn : webpacker utilise yarn (le système de package JS développé par Facebook) pour charger les dépendances JS.
  • Foreman : Il me permet de lancer Rails et le webpaker en même temps.
nvm install 6.4
nvm use 6.4
npm install --global yarn
rvm install 2.4.1
rvm use 2.4.1
gem install rails --version=5.1.0
gem install bundler foreman

Configuration

Je commence par créer le répertoire de mon projet et j'y entre.

mkdir todo_list
cd todo_list

Ensuite, je défini ma version par défaut de nodeJS en 6.4 afin d'éviter à chaque fois de faire un nvm use 6.4 (sous Cloud9 par exemple).

nvm alias default 6.4 

Foreman

Foreman demande de mettre dans un fichier Procfile les commandes pour chaque serveur/job que l'on veut lancer. Hors, Heroku utilise ce fichier pour lancer l'application (même avec heroku local).

Pour ce faire, je crée deux fichiers : Procfile.dev et .foreman.

Comme ça en production comme en développement, je n'ai qu'une seule commande à passer : foreman start.

# Procfile.dev
web: bundle exec rails s -p $PORT -b $IP
webpacker: ./bin/webpack-dev-server --inline true --hot true
#.foreman
procfile: Procfile.dev

Création du projet

Je commence par créer le projet.

rails _5.1.0_ new . --skip-bundle

ReactJs propose une gem rails vraiment bien réalisée que l'on peut installer à l'aide de webpacker.

J'ajoute les composants nécessaires dans mon Gemfile

# Gemfile
ruby '2.4.1'
gem 'webpacker'
gem "react-rails"

J'installe tout.

bundle install
rails webpacker:install
rails webpacker:install:react
rails generate react:install

J'ai de nouveaux répertoires - app/javascript/ (La logique applicative front) - packs (Pour le rendu côté serveur) - components (Tous les composants Reacts) - config/webpack/ (La configuration de webpack)

Webpacker

Je modifie la configuration de webpacker afin qu'il ne marche pas sur Rails, je configure config/webpacker.yml en modifiant la partie development comme suit.

development:
  <<: *default

  dev_server:
    host: 0.0.0.0
    port: 8081
    https: false

Installation de cucumber

Je commence par l'ajout de la gem.

# Gemfile
group :development, :test do
  gem 'cucumber-rails', :require => false
  gem 'database_cleaner'
end

Maintenant, je peux charger les gems et installer cucumber dans le projet.

bundle install
rails generate cucumber:install

Génération de la ressource

Je commence par générer ma ressource item

rails g resource item title:string description:string is_done:boolean --skip-assets
rails db:migrate RAILS_ENV=test

Ecriture des tests de comportement

Je souhaite pouvoir afficher ma liste de choses à faire, je crée donc une feature.

# todo_list/features/item.feature
Feature: Managing items
  As a user
  So that I can go to shopping 
  I want to see my shopping list

Background:
  Given the following items exist:
  |id|title |description|is_done|
  |1 |Item 1|bla        |false  |
  |2 |Item 2|blabla     |false  |

Scenario: the view should show the shopping list
  Given I am on the items index
  Then I should see "Item 1"

Dans un fichier features/step_definitions/items_steps.rb, je crée les fonctions suivantes.

#features/step_definitions/items_steps.rb
Given(/^the following items exist:$/) do |table|
  # table is a Cucumber::MultilineArgument::DataTable
  table.hashes.each do |obj|
    Item.create!(obj)
  end
end

Given(/^I am on the items index$/) do
  visit "items#index"
end

Then(/^I should see "([^"]*)"$/) do |arg1|
  assert page.has_content?(arg1)
end

Pour que cucumber fonctionne, je ajouter la méthode index au contrôleur.

# app/controller/items_controller.rb
class ItemsController < ApplicationController
  def index
    @items = Item.all
  end
end

Je crée aussi la vue à vide.

<!-- app/views/items/index.html.erb !->

De cette façon, il ne me reste plus qu'à faire afficher ma liste pour que mon test cucumber passe à vert.

Génération des composants React

Je suis un grand fan des générateurs !!

rails g react:component item title:string description:string is_done:boolean --es6

Notez bien le --es6 qui, au lieu de créer la classe avec var Item = React.createClass({}); va le faire avec class Item extends React.Component {}.

// app/assets/javascripts/components/Item.js
var React = require("react")
class Item extends React.Component {
  render () {
    return (
      <div>
        <div>Title: {this.props.title}</div>
        <div>Description: {this.props.description}</div>
        <div>Is Done: {this.props.isDone}</div>
      </div>
    );
  }
}

Item.propTypes = {
  title: React.PropTypes.string,
  description: React.PropTypes.string,
  isDone: React.PropTypes.bool
};
module.exports = Item

Fantastique. Maintenant je vais créer un conteneur d'items afin de l'ajouter à ma vue.

rails g react:component itemList items:array --es6

// app/assets/javascripts/components/ItemList.js
var React = require("react")
class ItemList extends React.Component {
  render () {
    return (
      <div>
        <div>Items: {this.props.items}</div>
      </div>
    );
  }
}

ItemList.propTypes = {
  items: React.PropTypes.array
};
module.exports = ItemList

Modification du conteneur d'items

Je vais modifier ItemList.js car le genérateur ne peut pas tout faire.

// app/assets/javascripts/components/ItemList.js
var React = require("react")
var Item = require("./Item");

class ItemList extends React.Component {
  render () {
    return (
      <div>
        {
          this.props.items.map(function(item){
            return <Item title={item.title} description={item.description} isDone={item.isDone} />;
          })
        }
      </div>
    );
  }
}

ItemList.propTypes = {
  items: React.PropTypes.array
};

module.exports = ItemList

Intégration dans ma vue

Il ne me reste plus qu'à appeler mon composant ItemList dans la vue index.html.erb.

<!-- app/views/items/index.html.erb !->
<%= react_component('ItemList', {items: @items}) %>

Je lance maintenant cucumber pour valider mes tests et voir tout passer au vert ... sauf que non !

$ cucumber
Using the default profile...
Feature: Managing items
  As a user
  So that I can go to shopping 
  I want to see my shopping list 

  Background:                        # features/item.feature:6
    Given the following items exist: # features/step_definitions/item_steps.rb:2
      | id | title  | description | is_done |
      | 1  | Item 1 | bla         | false   |
      | 2  | Item 2 | blabla      | false   |

  Scenario: the view should show the shopping list # features/item.feature:12
    Given I am on the items index                  # features/step_definitions/item_steps.rb:9
    Then I should see "Item 1"                     # features/step_definitions/item_steps.rb:13
      Expected false to be truthy. (Minitest::Assertion)
      ./features/step_definitions/item_steps.rb:14:in `/^I should see "([^"]*)"$/'
      features/item.feature:14:in `Then I should see "Item 1"'

Failing Scenarios:
cucumber features/item.feature:12 # Scenario: the view should show the shopping list

1 scenario (1 failed)
3 steps (1 failed, 2 passed)
0m1.969s

C'est tout simplement que React n'a pas été lancé, heuresuement react-rails permet de faire du rendu côté serveur en ajoutant au helper {prerender: true};

<!-- app/views/items/index.html.erb !->
<%= react_component('ItemList', {items: @items}, {prerender: true}) %>

Ceci ne suffit pas à passer les tests ; il faut ajouter une précompilation par webpacker. Pour ce faire, makandra a créé une classe que je vais appeler.

Dans le fichier features/support/webpacker.rb j'ajoute la classe et son appel.

#features/support/webpacker.rb
module WebpackerTestSupport
  module_function def compile_once
    digest_file = Rails.root.join("tmp/webpacker_#{Rails.env}_digest")

    # Compute hash of all packable assets
    packable_contents = Dir[Webpacker::Configuration.source_path.join('**/*')]
      .sort
      .map { |filename| File.read(filename) if File.file?(filename) }
      .join
    digest = Digest::SHA256.hexdigest(packable_contents)

    # Do nothing if assets did not change
    return if digest_file.exist? && digest_file.read == digest

    if ENV['TEST_ENV_NUMBER'].to_i < 1
      # Remove any previously compiled files
      output_path = Webpacker::Configuration.output_path
      FileUtils.rm_r(output_path) if File.exist?(output_path)
      puts "Removed Webpack output directory #{output_path}"

      # Ask 1st worker to compile assets
      Webpacker.compile

      digest_file.write(digest)
    else
      loop do
        # Other parallel test workers wait until 1st worker has compiled assets
        break if digest_file.exist? && digest_file.read == digest
        sleep 0.1
      end
    end
  end
end

WebpackerTestSupport.compile_once

Je relance cucumber pour finalement tout voir passer au vert !

$ cucumber
Removed Webpack output directory /home/ubuntu/workspace/public/packs-test
Webpacker is installed 🎉 🍰
Using /home/ubuntu/workspace/config/webpacker.yml file for setting up webpack paths
[Webpacker] Compiling assets 🎉
[Webpacker] Compiled digests for all packs in /home/ubuntu/workspace/app/javascript/packs:
{"application.js"=>"/packs-test/application.js", "hello_react.js"=>"/packs-test/hello_react.js", "server_rendering.js"=>"/packs-test/server_rendering.js"}
Using the default profile...
Feature: Managing items
  As a user
  So that I can go to shopping 
  I want to see my shopping list

  Background:                        # features/item.feature:6
    Given the following items exist: # features/step_definitions/items_steps.rb:2
      | id | title  | description | is_done |
      | 1  | Item 1 | bla         | false   |
      | 2  | Item 2 | blabla      | false   |

  Scenario: the view should show the shopping list # features/item.feature:12
    Given I am on the items index                  # features/step_definitions/items_steps.rb:9
    Then I should see "Item 1"                     # features/step_definitions/items_steps.rb:13

1 scenario (1 passed)
3 steps (3 passed)
0m3.924s

Dans l'article suivant, je décris la création avec l'application du TDD.

Publié dans les catégories suivantes

javascriptruby
comments powered by Disqus

Téléphone

+33 637 700 504

Adresse

Bordeaux, 33300
France