Le but de cet article est de donner une marche à suivre pour migrer à sa propre vitesse d'un front rails classique (ici à base de Erb) vers une solution basée sur un front en JS (ici en React) géré par Webpacker.
DISCLAMER : Ceci n'est pas un tuto React. Pour un tuto basique pour React, vous pouvez aller ici.
Présentation de l'app
On va prendre ici une application assez basique comme exemple. C'est réplicable sur des applications de taille normale, mais c'est plus long.
Il s'agit d'une seule page pour visualiser des posts et 3 actions, Create/Update/Delete. La page est protégée par une authentification via devise et affiche au besoin des erreurs de créations/update. De plus, l'application vérifie que vous avez bien les droits d'effectuer une action avant de la faire via la gem pundit.
Vous pourrez trouver le code ici.
Ceci devrait ressembler peu ou prou à la stack d'une application standard de Rails.
Présentation de Webpacker
Webpacker est un moyen d'utiliser facilement Webpack en conjonction avec Rails.
Qu'est ce que Webpack ? C'est un outil pour compacter et compiler des assets ensemble, à l'image de l'assets pipeline de Rails. Il est très utilisé dans l'écosystème JavaScript.
(Si vous vous demandez pourquoi j'ai écrit "facilement", je vous renvoie vers mon article sur le sujet.)
Pourquoi feriez-vous ça ?
Plusieurs raisons peuvent vous pousser à ajouter Webpacker dans votre application.
- Vous voulez utiliser facilement les nombreux packages disponibles de l'écosystème JavaScript sans les brancher dans l'assets pipeline.
- Vous voulez amener React/Vue/Else dans votre application et ne voulez pas recréer une application front ex-nilo, mais déplacer petit à petit des bouts de votre app vers une de ces applications, sans recoder toutes les vues de Devise par exemple.
Le présent article s'adresse plus au point 2 qu'au point 1, même si vous pourriez utiliser pas mal de points de cet article pour vous aider dans le premier cas.
Initialisation de Webpacker
Si vous voulez suivre la vidéo en même temps : Youtube (jusqu'à 26:52)
On va commencer par faire le bout de plomberie qui va nous permettre de travailler ensuite. C'est à dire amener une app React minimaliste à l'intérieur d'une de nos vues.
Ces étapes là vont aller assez vite.
Installation de Webpacker
On va directement suivre la doc de webpacker pour ça (et on va prendre soin d'utiliser la version @next de webpacker qui apporte un bon lot de fonctionnalités).
On ajoute a notre Gemfile
:
gem 'webpacker', '>= 4.0.x'
Puis on installe les différents packages
bundle install
yarn add @rails/webpacker@next
Enfin on initialise Webpacker
bundle exec rails webpacker:install
Ici vous avez déjà webpacker presque opérationnel. On va effectuer quelques petites modifications dans notre config/webpacker.yml :
- Remplacer tous les
localhost
par0.0.0.0
. Ça fait que vous pouvez accéder à votre site sur n'importe quelle URL et vous aurez accès à Webpack. - Changer
extract_css
àtrue
, ça fera que tous vos fichiers CSS importés seront automatiquement exportés. - En development, changez
hmr
àtrue
. Vous ne déclencherez plus de full reload à chaque changement de component dans React.
Installation de React
On laisse webpacker ajouter les fichiers minimaux pour notre app React :
bundle exec rails webpacker:install:react
On va ensuite renommer ça et déplacer des fichiers. C'est l'occasion de parler un peu du dossier app/javascript
. Vous aurez dedans un dossier spécial nommé packs
.
Dans ce dossier ne devra aller que les endpoints que vous voulez exposer dans votre application (ici on n'en veut qu'un seul : application.js
).
- Supprimez le fichier
app/javascript/packs/application.js
. - Renommez le fichier
app/javascript/packs/hello-react.jsx
enapp/javascript/packs/application.jsx
. - Créez un dossier
app/javascript/src
. C'est ici que nous mettrons tous les autres fichiers de l'application. - Créez un fichier
app/javascript/src/App.jsx
. Dans ce fichier créez un component React minimaliste. - Éditez votre fichier
app/javascript/packs/application.jsx
pour importer le componentApp
et lerender
. Ensuite, supprimez le component par défaut contenu dans le fichier.
Normalement si vous avez bien tout fait, vos fichiers devraient ressembler à :
app/javascript/packs/application.jsx// Run this example by adding <%= javascript_pack_tag 'hello_react' %> to the head of your layout file,
// like app/views/layouts/application.html.erb. All it does is render <div>Hello React</div>div> at the bottom
// of the page.
import React from "react";
import ReactDOM from "react-dom";
import App from "../src/App";
document.addEventListener("DOMContentLoaded", () => {
ReactDOM.render(<App />, document.getElementById("app-container"));
});
import React, { Component } from "react";
export default class App extends Component {
render() {
return <div>Hello from React :)</div>;
}
}
Votre "App" est prête à être plug dans vos views classiques.
Plug de votre application dans votre/vos views
Dans votre view (ici /app/view/posts/index.js
), on va ajouter en fin de fichier la ligne :
<%= javascript_pack_tag 'application' %>
De plus, on va ajouter la div qui recevra notre app à terme :
<div id="app-container"></div>
Il ne reste plus qu'à tester maintenant.
Lancez votre app webpack-dev-server
Il faut lancer les deux serveurs dans 2 terminaux différents :
# Votre serveur Rails classique
rails s
# Le serveur de dev webpack qui va se charger de recompiler vos JS a chaque changement.
bin/webpack-dev-server
Votre première vue
Méthodologie
Je vais vous donner quelques conseils (et avis) quant à l'architecture de votre App et comment procéder à la migration :
- Vos "Données" (ici les posts et le current_user) devraient vivre dans votre component App et être donné aux enfants. Un seul point de vérité.
- Vos méthodes pour récupérer vos données et faire des actions devraient aller dans
app/javascript/src/APIs
(ici dans un fichierposts.js
). - Vos components devraient aller dans un dossier
app/javascript/src/components
. - Déplacez votre erb tel quel et éditez-le dans le component. Ça vous évitera d'oublier des choses.
- Commencez par afficher les choses puis ajoutez ensuite les actions une par une avec votre backend.
Récupérer des données
Si vous voulez suivre la vidéo en même temps : Youtube (jusqu'à 53:13)
Pour récupérer nos données et les afficher on va procéder en 4 étapes.
1. Faire en sorte que rails réponde du JSON.
On utilise respond_to
et format
(doc), et on renvoie du JSON (pour des raisons de facilité je conseille d'utiliser ActiveModel Serializer)
Pour une utilisation facile mais pratique de ActiveModel Serializer il faut configurer son adapteur en mode :json
.
ActiveModelSerializers.config.adapter = :json
On crée nos serializers de cette façon :
app/serializers/post_serializer.rbclass PostSerializer < ActiveModel::Serializer
attributes :id, :text
belongs_to :author, serializer: AuthorSerializer
end
class AuthorSerializer < ActiveModel::Serializer
attributes :id, :username
end
Ces fichiers permettent de définir les informations qui seront renvoyées pour chaque type d'objet.
Puis on édite notre controller pour transformer notre méthode index depuis :
app/controller/posts_controller.rbdef index
@new_post = Post.new
@posts = Post.all.order(created_at: :desc).includes(:author)
end
Vers :
app/controller/posts_controller.rbdef index
@posts = Post.all.order(created_at: :desc).includes(:author)
respond_to do |format|
format.html do
@new_post = Post.new
end
format.json do
render json: @posts
end
end
end
Le but ici étant de créer un comportement différent quand on nous demandera du JSON.
Le format de réponse de notre API sera :
{
"posts": [
{
"id": "…",
"text": "text post",
"author": { "id": "…", "username": "zaratan" }
},
…
]
}
Il va être temps d'appeler cette API depuis votre app React \o/.
2. Créer un fichier pour faire des appels API et lire du JSON.
On crée un fichier simple en JS pour faire cet appel en utilisant fetch:
app/javascript/src/APIs/posts.jsexport const fetchPosts = async () => {
// On va chercher la donnée
const postsResponse = await fetch("/posts", {
// Ces headers nous permettent de dire a notre app Rails: Je veux du json PLZ
headers: {
"Content-Type": "application/json",
Accept: "application/json"
}
});
// On parse le body de la réponse en json pour obtenir un objet JS.
const postsJSON = await postsResponse.json();
// On supprime le fait que les posts soient rangés dans "posts"
return postsJSON.posts;
};
3. Appeler ces méthodes dans le component App.
Notre component App va être responsable des données.
On lui déclare donc un state :
state = {
posts: []
};
Puis, une fois que le component est chargé sur la page, on lui dit d'aller chercher les données :
import { fetchPosts } from './APIs/posts';
[…]
export default class App extends Component {
state = {
posts: []
}
refreshPosts = async () => {
const posts = await fetchPosts();
this.setState({
posts,
});
};
componentDidMount = async () => {
await this.refreshPosts();
};
[…]
}
4. Se servir de ces données pour afficher des components.
On se sert ensuite des données qui seront dans notre state à un moment pour afficher les différents Post.
Envoyer des données et actions
Si vous voulez suivre la vidéo en même temps : Youtube (jusqu'à 1:12:56)
Vous procédez de même pour les différentes actions en ajoutant les méthodes correspondantes dans le fichier d'API et dans le controller.
Example du create :
app/controller/posts_controller.rbdef create
post = Post.create!(create_params.merge(author: current_user))
respond_to do |format|
format.html do
redirect_to root_path
end
format.json do
render json: post
end
end
end
export const addPost = async ({ text }) => {
const postResponse = await fetch("/posts", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json"
},
body: JSON.stringify(addCsrf({ post: { text } }))
});
const postJSON = await postResponse.json();
return postJSON;
};
Les points importants sont :
Le CSRF
Dans toutes les méthodes qui ne sont pas des GET, vous allez avoir besoin de renseigner le CSRF (généré automatiquement par Rails).
Personnellement, je le gère de cette façon :
J'ai une méthode addCsrf
:
const addCsrf = object => {
const token = document.querySelector("meta[name=csrf-token]").content;
const key = document.querySelector("meta[name=csrf-param]").content;
object[key] = token;
return object;
};
que j'utilise pour compléter les body de mes requêtes :
body: JSON.stringify(addCsrf({ post: { text } })),
La gestion des erreurs
Personnellement je renvoie des trames JSON ressemblant à :
{ "errors": ["str_1", …]}
Et après ?
Le Routing
J'utilise le routing de Rails tant que je ne peux pas migrer complètement à React. Chaque page à sa propre App (que je renomme page, par exemple PostsPages, UserPage, etc…)
Le CSS
Vous pouvez ensuite migrer votre css dans React en utilisant une des nombreuses (soupir) solutions disponibles (Ma préférée étant StyledComponents en ce moment mais j'en ferai peut-être un article à part entière).
Le déploiement
Sur Heroku vous n'avez rien à faire de plus :) C'est pas magique ?
La pagination
ActiveModelSerializers gère très bien la pagination avec Kaminari :)
Conclusion
Voici le code final de l'exercice : https://github.com/denispasin/rails_to_webpack/pull/1
Ça devrait faire un bon article pour débuter une migration d'un Front Rails vers React. Je ne conseille en aucun cas de tout migrer d'un coup. Le travail peut être titanesque et vous avez clairement mieux à faire de votre temps.
À bientôt <3