2017-10-13 5 views
1

J'ai remarqué qu'il y a quelques opportunités de gain de performance pour reac-intl après avoir comparé intl.formatMessage({ id: 'section.someid' }) vs intl.messages['section.someid']. Voir plus ici: https://github.com/yahoo/react-intl/issues/1044Babel développement plugin pour reac-intl

La seconde est 5 fois plus rapide (et fait une énorme différence dans les pages avec beaucoup d'éléments traduits), mais ne semble pas être la voie officielle de le faire (je suppose qu'ils pourraient changer nom de la variable dans les versions futures). J'ai eu l'idée de créer un plugin babel qui fait la transformation (formatMessage (to messages [).) Mais j'ai du mal à le faire car la création de plugins babel n'est pas bien documentée (j'ai trouvé quelques tutoriels mais ça ne marche pas « ai ce que je dois) Je compris les bases, mais ne trouve pas le nom de la fonction de visiteur que j'ai besoin encore

Mon code boilerplate est actuellement:..

module.exports = function(babel) { 
    var t = babel.types; 
    return { 
    visitor: { 
     CallExpression(path, state) { 
     console.log(path); 
     }, 
    } 
    }; 
}; 

Alors voici mes questions:

  • Quelle méthode visiteur dois-je utiliser pour extraire les appels de classes - intl.formatMessage (est-ce vraiment CallExpression)?
  • Comment puis-je détecter un appel à formatMessage?
  • Comment détecter le nombre de paramètres dans l'appel? (le remplacement n'est pas supposé se produire s'il y a un formatage)
  • Comment je fais le remplacement? (Intl.formatMessage ({id: 'quelque chose'}?) À intl.messages [ 'quelque chose']
  • (en option) est-il un moyen de détecter si formatMessage vient vraiment de la bibliothèque réagira-intl

Répondre

1
?

Quelle méthode de visiteur dois-je utiliser pour extraire les classes appels - intl.formatMessage (est-ce vraiment CallExpression)

Oui, il est un CallExpression, il n'y a pas de noeud AST spécial pour un appel de méthode par rapport à? un appel de fonction, la seule chose qui change est le récepteur (appelé) .Quand vous vous demandez à quoi ressemble l'AST, vous pouvez utiliser le fantastique AST Explorer. En prime, vous pouvez même écrire le plugin Babel dans AST Explorer en sélectionnant Babel dans le menu Transform.

Comment puis-je détecter un appel à formatMessage?

Par souci de concision, je ne l'accent sur l'appel exact intl.formatMessage(arg), pour un vrai plug-in dont vous auriez besoin pour couvrir d'autres cas aussi bien (par exemple intl["formatMessage"](arg)) qui ont une autre représentation AST.

La première chose est d'identifier que l'appelé est intl.formatMessage. Comme vous le savez, il s'agit d'un simple accès aux propriétés d'un objet, et le noeud AST correspondant est appelé MemberExpression. Le visiteur reçoit le nœud AST correspondant, CallExpression dans ce cas, comme path.node. Cela signifie que nous devons vérifier que path.node.callee est un MemberExpression. Heureusement, c'est assez simple, car babel.types fournit des méthodes sous la forme isXX est le type de nœud AST.

if (t.isMemberExpression(path.node.callee)) {} 

Maintenant, nous savons que c'est un MemberExpression, qui a une object et un property qui correspondent à object.property. Nous pouvons donc vérifier si object est l'identifiant intl et property l'identifiant formatMessage. Pour cela, nous utilisons isIdentifier(node, opts), qui prend un second argument qui vous permet de vérifier qu'il a une propriété avec la valeur donnée. Toutes les méthodes isX sont de cette forme pour fournir un raccourci, pour plus de détails, voir Check if a node is a certain type. Ils vérifient également que le nœud n'est pas null ou undefined, donc le isMemberExpression n'était pas techniquement nécessaire, mais vous pourriez vouloir gérer un autre type différemment.

if (
    t.isIdentifier(path.node.callee.object, { name: "intl" }) && 
    t.isIdentifier(path.node.callee.property, { name: "formatMessage" }) 
) {} 

Comment puis-je détecter le nombre de paramètres dans l'appel? (Le remplacement n'est pas censé se produire s'il y a la mise en forme)

Le CallExpression a une propriété arguments, qui est un tableau des nœuds AST des arguments. Encore une fois, par souci de brièveté, je ne considérerai les appels qu'avec exactement un argument, mais en réalité, vous pourriez aussi transformer quelque chose comme intl.formatMessage(arg, undefined). Dans ce cas, il vérifie simplement la longueur de path.node.arguments. Nous voulons également que l'argument soit un objet, donc nous vérifions un ObjectExpression.

if (
    path.node.arguments.length === 1 && 
    t.isObjectExpression(path.node.arguments[0]) 
) {} 

Un ObjectExpression a une propriété properties, qui est un tableau de ObjectProperty noeuds. Vous pouvez vérifier techniquement que id est la seule propriété, mais je vais sauter cela ici et à la place seulement chercher une propriété id. Le ObjectProperty a un key et value, et nous pouvons utiliser Array.prototype.find() pour rechercher la propriété avec la clé étant l'identificateur id.

const idProp = path.node.arguments[0].properties.find(prop => 
    t.isIdentifier(prop.key, { name: "id" }) 
); 

idProp sera le ObjectProperty correspondant si elle existe, sinon il sera undefined. Quand ce n'est pas undefined, nous voulons remplacer le nœud.

Comment faire le remplacement? (Intl.formatMessage ({id: 'quelque chose'}?.) À intl.messages [ 'quelque chose']

Nous voulons remplacer l'ensemble CallExpression et Babel fournit path.replaceWith(node) La seule chose qui reste, crée l'AST Pour cela, nous devons d'abord comprendre comment intl.messages["section.someid"] est représenté dans l'AST est un MemberExpression comme était obj["property"] est un accès aux objets de propriété calculée, qui est également représenté comme un MemberExpression dans l'AST , mais avec la propriété computed définie sur true Cela signifie que intl.messages["section.someid"] est un MemberExpression avec un MemberExpression comme objet.

Rappelez-vous que ces deux sont sémantiquement équivalentes:

intl.messages["section.someid"]; 

const msgs = intl.messages; 
msgs["section.someid"]; 

Pour construire un MemberExpression nous pouvons utiliser t.memberExpression(object, property, computed, optional). Pour créer intl.messages nous pouvons réutiliser le intl de path.node.callee.object car nous voulons utiliser le même objet, mais changez la propriété. Pour la propriété, nous devons créer un Identifier avec le nom messages.

t.memberExpression(path.node.callee.object, t.identifier("messages")) 

Seuls les deux premiers arguments sont nécessaires et pour le reste, nous utilisons les valeurs par défaut (pour falsecomputed en option et null). Maintenant, nous pouvons utiliser cet MemberExpression comme objet et nous devons rechercher la propriété calculée (le troisième argument est true) qui correspond à la valeur de la propriété id, qui est disponible sur le idProp que nous avons calculé précédemment. Et enfin, nous remplaçons le nœud CallExpression par le nœud nouvellement créé.

if (idProp) { 
    path.replaceWith(
    t.memberExpression(
     t.memberExpression(
     path.node.callee.object, 
     t.identifier("messages") 
    ), 
     idProp.value, 
     // Is a computed property 
     true 
    ) 
); 
} 

code complet:

export default function({ types: t }) { 
    return { 
    visitor: { 
     CallExpression(path) { 
     // Make sure it's a method call (obj.method) 
     if (t.isMemberExpression(path.node.callee)) { 
      // The object should be an identifier with the name intl and the 
      // method name should be an identifier with the name formatMessage 
      if (
      t.isIdentifier(path.node.callee.object, { name: "intl" }) && 
      t.isIdentifier(path.node.callee.property, { name: "formatMessage" }) 
     ) { 
      // Exactly 1 argument which is an object 
      if (
       path.node.arguments.length === 1 && 
       t.isObjectExpression(path.node.arguments[0]) 
      ) { 
       // Find the property id on the object 
       const idProp = path.node.arguments[0].properties.find(prop => 
       t.isIdentifier(prop.key, { name: "id" }) 
      ); 
       if (idProp) { 
       // When all of the above was true, the node can be replaced 
       // with an array access. An array access is a member 
       // expression with a computed value. 
       path.replaceWith(
        t.memberExpression(
        t.memberExpression(
         path.node.callee.object, 
         t.identifier("messages") 
        ), 
        idProp.value, 
        // Is a computed property 
        true 
       ) 
       ); 
       } 
      } 
      } 
     } 
     } 
    } 
    }; 
} 

Le code complet et certains cas de test peuvent être trouvés dans this AST Explorer Gist.

Comme je l'ai mentionné à quelques reprises, c'est une version naïve et de nombreux cas ne sont pas couverts, qui sont admissibles à la transformation. Il n'est pas difficile de couvrir plus de cas, mais vous devez les identifier et les coller dans l'AST Explorer vous donnera toutes les informations dont vous avez besoin. Par exemple, si l'objet est { "id": "section.someid" } au lieu de { id: "section.someid" } il ne sera pas transformé, mais couvrant c'est aussi simple que de vérifier aussi un StringLiteral en plus d'une Identifier, comme ceci:

const idProp = path.node.arguments[0].properties.find(prop => 
    t.isIdentifier(prop.key, { name: "id" }) || 
    t.isStringLiteral(prop.key, { value: "id" }) 
); 

Je n'ai pas présenté aussi abstractions exprès pour éviter la charge cognitive supplémentaire, donc les conditions semblent très longues.

Ressources utiles:

+0

Michael, je vous remercie beaucoup pour la meilleure réponse. Je vais essayer aujourd'hui. :) –

+0

Merci encore une fois. Vous n'avez pas non plus aidé à résoudre mon problème, appris à faire des plugins mais aussi probablement aidé d'autres utilisateurs de reac-intl. J'ai partagé votre réponse ici dans le fichier reac-intl github https://github.com/yahoo/react-intl/issues/1044 –