TL; DR
- Cet article ne traite pas des frameworks JS de la liste TOP-3.
- Lors du développement sur un framework JS non TOP-3, vous devez résoudre un ordre de grandeur plus de problèmes techniques que prévu au début du développement
- L'histoire est basée sur des événements réels.
L'histoire a commencé avec un mini-projet, qui a été initialement développé à partir des bibliothèques backbone.js et marionette.js. Ce sont, bien sûr, les grandes bibliothèques qui ont commencé l'histoire du développement d'applications à page unique. Mais déjà à cette époque, ils étaient plus susceptibles d'avoir une valeur historique que pratique. Je mentionne seulement le fait que pour afficher un simple tableau il fallait créer: 1) un module avec une description du modèle, 2) un module avec une description de la collection, 3) un module avec la définition d'un modèle de vue, 4) un module avec la définition d'une collection de vue, 4) un modèle de ligne de tableau 5) modèle de table; 6) module de contrôleur. Avoir environ 10 entités dans une petite application - au tout début, vous disposiez déjà de plus de cinquante petits modules. Et ce n'est qu'un début. Mais maintenant, ce n'est pas ça.
À un moment donné, après six mois de demande, elle n'apparaissait toujours pas dans les résultats de la recherche. L'ajout de
prerender.io au projet (qui a ensuite utilisé le moteur phantom.js) a aidé, mais pas autant que prévu. Les nuages ont commencé à se rassembler sur l'application et au-dessus de moi, après quoi j'ai réalisé que je devais faire quelque chose très rapidement et efficacement, de préférence aujourd'hui. L'objectif que je me suis fixé était le suivant: passer au rendu serveur. Avec backbone.js et marionettejs, c'est presque impossible. En tout cas, le projet rendr sur backbone.js, qui a été développé sous la direction de Spike Brehm (l'auteur de l'idée d'applications isomorphes / universelles), collectant 58 contributeurs et 4184 likes sur github.com, a été arrêté en 2015 et n'était clairement pas destiné à un blitz d'une journée . J'ai commencé à chercher une alternative. J'ai exclu tout de suite le framework TOP-3 JS, car je n'avais pas de réserve de temps pour leur développement. Après une courte recherche, j'ai trouvé le framework JS en pleine croissance riot.js
github.com/riot/riot , qui compte actuellement 13 704 likes sur github.com, et, comme je l'espérais, il aurait bien pu atteindre le premier (ce qui, cependant, ne s'est pas produit).
Et oui, j'ai fermé la question (mais pas le même jour, mais pour les deux prochains jours de la journée) en transférant l'application sur le rendu du serveur à l'aide de riot.js. Et le même jour où cela s'est produit, le site est passé aux dix premières pages de résultats de recherche. Et en une semaine, je suis allé à la première page, d'où il ne part pas depuis plusieurs années.
Cela met fin à l'histoire à succès et commence l'histoire des défaites. Le projet suivant était beaucoup plus compliqué. Certes, il y avait un point positif dans la mesure où 99% des écrans d'application se trouvaient dans le compte personnel de l'utilisateur, il n'était donc pas nécessaire de rendre le serveur. Inspiré par la première expérience réussie d'utilisation de riot.js, j'ai commencé à promouvoir l'idée de consolider le succès et d'appliquer riot.js sur le frontend. Ensuite, il m'a semblé que, finalement, une solution a été trouvée combinant simplicité et fonctionnalité, comme promis par la documentation de riot.js. Comme j'avais tort!
Le premier problème que j'ai rencontré lorsqu'il était nécessaire de fournir au concepteur de mise en page HTML tous les outils de développement nécessaires. En particulier, nous avions besoin de plug-ins pour l'éditeur de code, un moteur dans lequel il serait possible de placer les composants et d'observer immédiatement le résultat, y compris avec une surcharge à chaud des composants (rechargement à chaud). Tout cela devait être donné sous la forme prête pour une exploitation industrielle dans un avenir proche, et tout cela ne l'était pas. En conséquence, la mise en page de l'application a commencé sur l'un des moteurs de modèles traditionnels, ce qui a conduit à l'étape ingrate de la traduction de documents HTML en composants riot.js.
Cependant, le principal problème qui est apparu à ce stade n'est même pas lié à la traduction de la mise en page du format de modèle en composants riot.js. Soudain, il s'est avéré que riot.js donne des messages complètement non informatifs sur les erreurs de compilation de modèles, ainsi que les erreurs d'exécution (cela est vrai pour toutes les versions de riot.js jusqu'à la version 4.0, qui a été complètement repensée). Il n'y avait aucune information non seulement sur la ligne dans laquelle l'erreur s'est produite, mais même sur le nom du fichier ou du composant dans lequel l'erreur s'est produite. Il a été possible de rechercher une erreur pendant plusieurs heures d'affilée, mais elle n'a toujours pas pu être trouvée. Et puis j'ai dû annuler toutes les modifications apportées au dernier état de fonctionnement.
Ce qui suit était un problème de routage. Le routage dans riot.js sort presque de la boîte
github.com/riot/route - au moins du même développeur. Cela lui a permis d'espérer une opération sans problème. Mais à un moment donné, j'ai remarqué que certaines pages sont surchargées de façon imprévisible. Autrement dit, une fois, la transition vers une nouvelle route peut se produire dans le mode d'application à page unique, et une autre fois, la même transition a surchargé tout le document HTML, comme lorsque vous travaillez avec une application Web classique. Dans ce cas, bien sûr, l'état interne a été perdu s'il n'avait pas encore été enregistré sur le serveur. (Actuellement, le développement de cette bibliothèque est arrêté et n'est pas utilisé avec riot.js 4.0.)
Le seul composant du système qui a fonctionné comme prévu était le gestionnaire d'état à flux minimal
github.com/jimsparkman/RiotControl . Certes, pour travailler avec ce composant, il était nécessaire de nommer et d'annuler les auditeurs des changements d'état beaucoup plus souvent que nous le souhaiterions.
L'idée initiale de cet article était la suivante: montrer par l'exemple de notre propre expérience avec le framework riot.js les tâches que le développeur aurait à résoudre, qui a décidé (décidé) de développer une application sur un framework JS ne figurant pas dans la liste TOP-3. Cependant, en cours de préparation, j'ai décidé de rafraîchir certaines pages de la documentation de riot.js dans ma mémoire, et j'ai donc appris qu'une nouvelle version de riot.js 4.0 a été publiée, qui a été complètement (à partir de zéro) repensée, qui peut être trouvée dans l'article du développeur de riot. js sur medium.com:
medium.com/@gianluca.guarini/every-revolution-begins-with-a-riot-js-first-6c6a4b090ee . De cet article, j'ai appris que tous les principaux problèmes qui m'inquiétaient et dont j'allais parler dans cet article ont été éliminés. Plus précisément, dans riot.js version 4.0:
- le compilateur est complètement réécrit (ou plutôt, il a été écrit pour la première fois parce que le moteur fonctionnait auparavant sur les expressions régulières) - cela affectait en particulier le contenu informatif des messages d'erreur
- en plus du rendu du serveur, hydrate a été ajouté sur le client - cela nous a finalement permis de commencer à écrire des applications universelles sans double rendu (la première fois sur le serveur et là sur le client en raison du manque de fonction hydrate () dans les anciennes versions)
- ajout d'un plugin pour les composants de surcharge à chaud github.com/riot/hot-reload
- et bien d'autres changements utiles
Ce n'est pas de la publicité, juste de l'arrière-plan. En fait, mes conclusions seront très ambiguës en termes de recommandations d'utilisation, et l'article en général n'est pas consacré à un cadre ou une bibliothèque spécifique, mais aux tâches qui doivent être résolues pendant le processus de développement.
Malheureusement, le travail effectué par les développeurs de riot.js n'a pas encore été correctement évalué par la communauté. Par exemple, la bibliothèque de rendu de serveur
github.com/riot/ssr pour les six mois qui se sont écoulés depuis le début de son développement a collecté trois co-contributeurs et trois likes sur github.com (tous les likes ne sont pas faits par des contributeurs, bien que l'un soit par le contributeur).
Par conséquent, au cours de la pièce, il a changé la direction de l'article, et au lieu de mémoires, il a essayé d'aller à nouveau, ayant un peu plus de connaissances et d'expérience, des versions plus avancées des bibliothèques et un temps libre illimité.
Alors c'est parti. Par exemple, une implémentation de l'application
github.com/gothinkster/realworld a été effectuée. Ce projet a été discuté plus d'une fois sur Habré. Pour ceux qui ne le connaissent pas, je décrirai brièvement son idée. Les développeurs de ce projet à l'aide de différents langages de programmation et frameworks (ou sans eux) résolvent le même problème: le développement d'un moteur de blog, avec des fonctionnalités similaires à la version simplifiée de medium.com. Il s'agit d'un compromis entre la complexité des applications réelles que nous devons développer au quotidien et todo.app, ce qui ne nous permet pas toujours d'apprécier vraiment le travail avec la bibliothèque ou le framework. Ce projet est respecté par les développeurs. Pour confirmer ce qui précède, je peux dire qu'il y a même une implémentation de Rich Harris (développeur majeur de sveltejs)
github.com/sveltejs/realworld .
Environnement de développement
Bien sûr, vous êtes prêt à vous lancer dans la bataille, mais pensez aux développeurs qui vous entourent. Dans ce cas, concentrez-vous sur la question de l'environnement de développement dans lequel vos collègues travaillent. S'il n'y a pas de plugins pour les principaux environnements de développement et les éditeurs de code de programme pour le framework avec lequel vous allez travailler, il est peu probable que vous soyez pris en charge. Par exemple, j'utilise l'éditeur Atom pour le développement. Pour lui, il existe un plugin anti-émeute
github.com/riot/syntax-highlight/tree/legacy , qui n'a pas été mis à jour depuis trois ans. Et dans le même référentiel, il y a un plugin pour sublime
github.com/riot/syntax-highlight - il est à jour et prend en charge la version actuelle de riot.js 4.0.
Cependant, le composant riot.js est un fragment valide d'un document HTML dans lequel le code JS est contenu dans le corps de l'élément de script. Tout fonctionne donc si vous ajoutez un type de document html pour l'extension * .riot. Bien sûr, il s'agit d'une décision forcée, car sinon il aurait été tout simplement impossible de continuer ici.
Nous avons mis en évidence la syntaxe dans un éditeur de texte, et maintenant nous avons besoin de fonctionnalités plus avancées, ce que nous avons l'habitude d'obtenir d'Eslint. Dans notre cas, le code JS des composants est contenu dans le corps de l'élément de script, j'espérais trouver et trouver un plugin pour extraire le code JS du document HTML -
github.com/BenoitZugmeyer/eslint-plugin-html . Après cela, ma configuration eslint a commencé à ressembler à ceci:
{ "parser": "babel-eslint", "plugins": [ "html" ], "settings": { "html/html-extensions": [".html", ".riot"] }, "env": { "browser": true, "node": true, "es6": true }, "extends": "standard", "globals": { "Atomics": "readonly", "SharedArrayBuffer": "readonly" }, "parserOptions": { "ecmaVersion": 2018, "sourceType": "module" }, "rules": { } }
La présence de plug-ins pour la coloration syntaxique et eslint n'est probablement pas la première chose à laquelle un développeur commence à penser lorsqu'il choisit un framework JS. En attendant, sans ces outils, vous risquez de rencontrer l'opposition de collègues et leur exode massif pour de «bonnes» raisons du projet. Bien que la seule et vraiment valable raison soit qu'ils soient mal à l'aise de travailler sans un arsenal complet de développeurs. Dans le cas de riot.js, le problème a été résolu par la méthode Columbus. Dans le sens où il n'y a en fait aucun plugin pour riot.js, mais en raison des particularités de la syntaxe du modèle riot.js, qui ressemble à un fragment d'un document HTML standard, nous couvrons 99% des fonctionnalités nécessaires à l'aide d'outils pour travailler avec un document HTML.
En plus des moyens d'écrire et de valider le code que nous venons d'examiner, les outils du développeur incluent des outils pour l'assemblage et le débogage rapides du projet, le rechargement à chaud des composants et des modules dans un navigateur Web lors de la modification du projet. Nous examinerons cette partie dans la section suivante.
Montage du projet
Nous avons réussi à nous habituer à la plupart des fonctionnalités nécessaires à la construction du projet, et même à arrêter de penser à ce qui pourrait être différent. Mais il pourrait en être autrement. Et, si vous avez choisi un nouveau framework JS, il est conseillé de s'assurer au préalable que tout fonctionne comme prévu. Par exemple, comme je l'ai déjà mentionné, le plus gros problème lors du développement sur les anciennes versions de riot.js était le manque de messages d'erreur de compilation et d'informations d'exécution sur le composant dans lequel cette erreur s'est produite. La vitesse de compilation est également importante. En règle générale, afin d'accélérer la vitesse de compilation, dans un cadre correctement construit, seule la partie modifiée est recompilée, de sorte que le temps de réaction aux modifications du texte du composant est minimal. Eh bien, c'est très bien si un rechargement à chaud des composants est pris en charge sans rechargement complet de la page dans le navigateur Web.
Par conséquent, je vais essayer de lister la liste de contrôle, ce à quoi vous devez porter une attention particulière lors de l'analyse des outils de construction du projet:
1. La présence d'un mode de développement et d'une application fonctionnelle
En mode développeur:
2. Messages informatifs sur les erreurs de compilation du projet (nom du fichier source, numéro de ligne dans le fichier source, description de l'erreur)
3. Messages informatifs sur les erreurs d'exécution (nom du fichier source, numéro de ligne dans le fichier source, description de l'erreur)
4. Réassemblage rapide des modules modifiés
5. Surcharge à chaud de composants dans le navigateur
En mode de fonctionnement:
6. La présence de versioning dans le nom de fichier (par exemple 4a8ee185040ac59496a2.main.js)
7. La disposition des petits modules en un ou plusieurs modules (morceaux)
8. Code se décomposant en morceaux à l'aide de l'importation dynamique
Dans la version 4.0 de riot.js, le module
github.com/riot/webpack-loader est apparu, ce qui correspond pleinement à la liste de contrôle donnée. Je n'énumérerai pas toutes les fonctionnalités de la configuration d'assemblage. La seule chose sur laquelle je voudrais attirer votre attention est que dans le projet en question, j'utilise des modules pour express.js: webpack-dev-middleware et webpack-hot-middleware, qui vous permettent de travailler sur un serveur pleinement fonctionnel immédiatement, dès la mise en page. Ceci, en particulier, permet le développement d'applications Web universelles / isomorphes. J'attire votre attention sur le fait que le module de surcharge à chaud du module n'est valable que pour un navigateur Web. Dans le même temps, le composant rendu côté serveur reste inchangé. Par conséquent, il est nécessaire d'écouter ses modifications et, au bon moment, de supprimer tout le code mis en cache par le serveur et de charger le code des modules modifiés. Comment faire cela pendant longtemps, je vais donc simplement fournir un lien vers la mise en œuvre:
github.com/apapacy/realworld-riotjs-effector-universal-hot/blob/master/src/dev_server.jsAcheminement
En paraphrasant un peu Leo Tolstoï, nous pouvons dire que tous les moteurs des frameworks JS sont similaires les uns aux autres, alors que tous les routages qui leur sont attachés fonctionnent à leur manière. Je rencontre souvent une classification conditionnelle des routeurs en deux types: déclarative et impérative. Je vais maintenant essayer de comprendre comment une telle classification est justifiée.
Prenons une courte excursion dans l'histoire. À l'aube d'Internet, les URL / URI correspondaient au nom du fichier hébergé sur le serveur. Nous parcourons plusieurs pages d'histoire à la fois et nous apprendrons l'avènement de l'architecture Model 2 (MVC). Dans cette architecture, un contrôleur frontal apparaît, qui remplit la fonction de routage. Je me demandais qui avait d'abord décidé du contrôleur frontal de sélectionner la fonction de routage dans un bloc séparé, qui envoie ensuite une demande à l'un des nombreux contrôleurs et n'a pas encore trouvé de réponse. Il semble qu'ils aient commencé à tout faire en même temps.
C'est-à-dire que le routage a déterminé l'action qui devrait être effectuée sur le serveur et (de manière transitoire via le contrôleur) la vue qui sera générée par le contrôleur. Lors du transfert du routage côté client (navigateur Web), une idée de la fonction de routage dans l'application était déjà formée. Et ceux qui se sont principalement concentrés sur le fait que le routage détermine l'action, ont développé le routage impératif, et ceux qui ont fait attention que, finalement, le routage détermine la vue qui devrait être montrée à l'utilisateur, ont développé le routage déclaratif.
En d'autres termes, lors du transfert d'un serveur vers un client pour le routage, ils «suspendaient» deux fonctions caractéristiques du routage du serveur (sélection d'une action et sélection d'une vue). De plus, de nouvelles tâches sont apparues - il s'agit de naviguer dans une application d'une seule page sans recharger complètement le document HTML, de travailler avec l'historique des visites et bien plus encore. Pour illustrer, je vais fournir des extraits de la documentation du routeur d'un framework méga-populaire:
... facilite la création d'applications SPA. Comprend les fonctionnalités suivantes
- Itinéraires / vues imbriqués
- Configuration du routeur modulaire
- Accès aux paramètres de route, requête, caractères génériques
- Voir l'animation de transition basée sur Vue.js
- Contrôle de navigation pratique
- Apposition automatique de la classe CSS active pour les liens
- Modes d'historique HTML5 ou hachage, avec basculement automatique dans IE9
- Comportement de défilement personnalisé
Dans cette option, le routage est clairement surchargé de fonctionnalités et doit repenser ses tâches côté client. J'ai commencé à chercher une solution adaptée à ma tâche. Comme critère principal, j'ai pris en compte que le routage:
- devrait fonctionner de la même façon côté client Web et côté serveur Web pour les applications Web universelles / isomorphes;
- devrait fonctionner avec n'importe quel cadre (y compris celui que j'ai choisi) ou sans.
Et j'ai trouvé une telle bibliothèque, c'est
github.com/kriasoft/universal-router . Si nous décrivons brièvement l'idée de cette bibliothèque, elle configure des itinéraires qui prennent une chaîne d'URL en entrée et appellent une fonction asynchrone en sortie, qui transmet l'URL analysée comme paramètre réel. Honnêtement, je voulais demander: c'est tout? Et comment alors tout le monde devrait-il travailler avec cela? Et puis j'ai trouvé un article sur medium.com
medium.com/@ippei.tanaka/universal-router-history-react-97ec79464573 , dans lequel une assez bonne option a été proposée, à l'exception peut-être de réécrire la méthode de l'historique push (), qui n'a pas J'ai besoin et que j'ai supprimé de mon code. Par conséquent, le fonctionnement du routeur côté client est défini approximativement comme ceci:
const routes = new UniversalRouter([ { path: '/sign-in', action: () => ({ page: 'login', data: { action: 'sign-in' } }) }, { path: '/sign-up', action: () => ({ page: 'login', data: { action: 'sign-up' } }) }, { path: '/', action: (req) => ({ page: 'home', data: { req, action: 'home' } }) }, { path: '/page/:page', action: (req) => ({ page: 'home', data: { req, action: 'home' } }) }, { path: '/feed', action: (req) => ({ page: 'home', data: { req, action: 'feed' } }) }, { path: '/feed/page/:page', action: (req) => ({ page: 'home', data: { req, action: 'feed' } }) }, ... { path: '(.*)', action: () => ({ page: 'notFound', data: { action: 'not-found' } }) } ]) const root = getRootComponent() const history = createBrowserHistory() const render = async (location) => { const route = await router.resolve(location) const component = await import(`./riot/pages/${route.page}.riot`) riot.register(route.page, component.default || component) root.update(route, root) } history.listen(render)
Maintenant, tout appel à history.push () lancera le routage. Pour naviguer à l'intérieur de l'application, vous devez également créer un composant qui encapsule l'élément HTML standard a (ancre), sans oublier d'annuler son comportement par défaut:
<navigation-link href={ props.href } onclick={ action }> <slot/> <script> import history from '../history' export default { action (e) { e.preventDefault() history.push(this.props.href) if (this.props.onclick) { this.props.onclick.call(this, e) } e.stopPropagation() } } </script> </navigation-link>
Gestion de l'état des applications
Au départ, j'ai inclus la bibliothèque mobx dans le projet. Tout a fonctionné comme prévu. Sauf que cela ne correspondait pas tout à fait à la tâche - l'étude que j'ai fixée au début de l'article. Je suis donc passé à
github.com/zerobias/effector . Il s'agit d'un projet très puissant. Il donne 100% de fonctionnalités redux (uniquement sans gros frais généraux) et 100% de fonctionnalités mobx (bien que dans ce cas, il sera nécessaire d'encoder un peu plus, mais encore moins si on le compare à mobx sans décorateurs)
La description du magasin ressemble à ceci:
import { createStore, createEvent } from 'effector' import { request } from '../agent' import { parseError } from '../utils' export default class ProfileStore { get store () { return this.profileStore.getState() } constructor () { this.success = createEvent() this.error = createEvent() this.updateError = createEvent() this.init = createEvent() this.profileStore = createStore(null) .on(this.init, (state, store) => ({ ...store })) .on(this.success, (state, data) => ({ data })) .on(this.error, (state, error) => ({ error })) .on(this.updateError, (state, error) => ({ ...state, error })) } getProfile ({ req, author }) { return request(req, { method: 'get', url: `/profiles/${decodeURIComponent(author)}` }).then( response => this.success(response.data.profile), error => this.error(parseError(error)) ) } follow ({ author, method }) { return request(undefined, { method, url: `/profiles/${author}/follow` }).then( response => this.success(response.data.profile), error => this.error(parseError(error)) ) } }
Cette bibliothèque utilise des réducteurs à part entière (ils sont appelés dans la documentation effector.js), ce que beaucoup de gens manquent dans mobx, mais avec beaucoup moins d'efforts de codage que redux. Mais l'essentiel n'est même pas cela. Ayant reçu 100% des fonctionnalités de redux et mobx, je n'ai utilisé qu'un dixième des fonctionnalités inhérentes à effector.js. D'où nous pouvons conclure que son utilisation dans des projets complexes peut considérablement enrichir les fonds des développeurs.
Test
TodoConclusions
Ainsi, le travail est terminé. Le résultat est présenté dans le référentiel
github.com/apapacy/realworld-riotjs-effector-universal-hot et dans cet article sur Habré.
Site de démonstration sur
realworld-riot-effector-universal-hot-pnujtmugam.now.shEt à la fin, je partagerai mes impressions sur le développement. Développer sur riot.js version 4.0 est assez pratique. De nombreuses constructions sont plus faciles à écrire que dans React. Il a fallu exactement deux semaines pour se développer sans fanatisme dans les heures après et le week-end. Mais ... Un petit mais ... Le miracle ne s'est pas reproduit. Le rendu du serveur dans React est 20 à 30 fois plus rapide. Les entreprises gagnent à nouveau. Cependant, deux bibliothèques de routage et de gestionnaire d'état intéressantes ont été testées.
apapacy@gmail.com
17 juin 2019