Blog officiel du projet Web Apps Conception
layout: post title: “Développer en LiquidJs” date: 2024-10-19 23:00:00 +0002 tags: node
LiquidJS est un moteur de template initialement développé par le directeur général de Shopify, Tobias Lütke et écrit en Ruby. C’est un projet open-source disponible sur Github. La première version remonte à 2008. Le balisage Liquid est très similaire au HTML avec quelques ajouts.
📝 Quelques exemples d’utilisation : webapps-conception/liquidjs-example.
LiquidJS est un moteur de modèles compatible Shopify / GitHub Pages simple, expressif et sûr en JavaScript pur. Le but de ce référentiel est de fournir une implémentation Liquid standard pour la communauté JavaScript. Liquid est à l’origine implémenté dans Ruby et utilisé par GitHub Pages, Jekyll et Shopify, voir Différences avec Shopify/liquid.
La syntaxe LiquidJS est relativement simple. Il existe 2 types de balises dans LiquidJS :
{%
et %}
.{{
et }}
.Live Demo : Before going into the details, here’s a live demo to play around: https://liquidjs.com/playground.html.
Les « Outputs » sont utilisées pour générer des variables, qui peuvent être transformées par des filtres, en HTML. Le modèle suivant insérera la valeur de username
dans la valeur de l’entrée :
<input type="text" name="user" value="{{username}}">
Les valeurs en sortie peuvent être transformées par des filtres avant la sortie. Pour ajouter une chaîne après la variable :
{{ username | append: ", welcome to LiquidJS!" }}
Les filtres peuvent être chaînés :
{{ username | append: ", welcome to LiquidJS!" | capitalize }}
Une liste complète des filtres pris en charge par LiquidJS peut être trouvée ici.
Les Tags
sont utilisées pour contrôler le processus de rendu du modèle, la manipulation des variables du modèle, l’interopérabilité avec d’autres modèles, etc. Par exemple, assign
peut être utilisé pour définir une variable qui peut être utilisée ultérieurement dans le modèle :
{% assign foo = "FOO" %}
Généralement, les balises apparaissent par paires avec une balise de début et une balise de fin correspondante. Par exemple:
{% if foo == "FOO" %}
Variable `foo` equals "FOO"
{% else %}
Variable `foo` not equals "FOO"
{% endif %}
Une liste complète des balises prises en charge par LiquidJS peut être trouvée ici.
Installer via npm :
npm install --save liquidjs
var { Liquid } = require('liquidjs');
var engine = new Liquid();
engine
.parseAndRender('{{name | capitalize}}', {name: 'alice'})
.then(console.log); // outputs 'Alice'
Démo de travail : Voici une démo fonctionnelle pour l’utilisation de LiquidJS dans Node.js : liquidjs/demo/nodejs/.
Les définitions de types pour LiquidJS sont également exportées et publiées, ce qui le rend plus agréable pour les projets TypeScript :
import { Liquid } from 'liquidjs';
const engine = new Liquid();
engine
.parseAndRender('{{name | capitalize}}', {name: 'alice'})
.then(console.log); // outputs 'Alice'
Démo de travail : Voici une démo fonctionnelle pour l’utilisation de LiquidJS dans TypeScript : [liquidjs/demo/typescript/}(https://github.com/harttle/liquidjs/blob/master/demo/typescript/){:target=”_blank”}
Des bundles UMD prédéfinis sont également disponibles :
<!--for production-->
<script src="https://cdn.jsdelivr.net/npm/liquidjs/dist/liquid.browser.min.js"></script>
<!--for development-->
<script src="https://cdn.jsdelivr.net/npm/liquidjs/dist/liquid.browser.umd.js"></script>
Démo de travail : Voici une démo vivante sur jsFiddle: jsfiddle.net/pd4jhzLs/1/, et le code source est également disponible en liquidjs/demo/browser/.
LiquidJS peut également être utilisé pour restituer un modèle directement depuis la CLI en utilisant npx
:
npx liquidjs --template '{{"hello" | capitalize}}'
Vous pouvez soit transmettre le modèle en ligne (comme indiqué ci-dessus), soit le lire à partir d’un fichier en utilisant le caractère @
suivi d’un chemin, comme ceci :
npx liquidjs --template @./some-template.liquid
Vous pouvez également utiliser la syntaxe @-
pour lire le modèle depuis stdin
:
echo '{{"hello" | capitalize}}' | npx liquidjs --template @-
Un contexte peut être transmis de la même manière (c’est-à-dire en ligne, à partir d’un chemin ou via stdin
). Les trois suivants sont équivalents :
npx liquidjs --template 'Hello, {{ name }}!' --context '{"name": "Snake"}'
npx liquidjs --template 'Hello, {{ name }}!' --context @./some-context.json
echo '{"name": "Snake"}' | npx liquidjs --template 'Hello, {{ name }}!' --context @-
Notez que vous ne pouvez utiliser stdin
avec le spécificateur @-
que pour un seul argument. Si vous essayez de l’utiliser à la fois pour --template
et --context
, vous obtiendrez une erreur.
La sortie rendue est écrite sur stdout
par défaut, mais vous pouvez également spécifier un fichier de sortie (si le fichier existe, il sera écrasé) :
npx liquidjs --template '{{"hello" | capitalize}}' --output ./hello.txt
Vous pouvez également transmettre un certain nombre d’options pour personnaliser le comportement de rendu du modèle. Par exemple, l’option –js-truthy peut être utilisée pour activer la véracité de JavaScript :
npx liquidjs --template @./some-template.liquid --js-truthy
La plupart des options disponibles via l’API JavaScript sont également disponibles à partir de la CLI. Pour obtenir de l’aide sur les options disponibles, utilisez npx liquidjs --help
.
Une démo ReactJS est également disponible sous liquidjs/demo/reactjs/.
Le constructeur Liquid accepte un objet simple comme option pour définir le comportement de LiquidJS. Toutes ces options sont facultatives, nous pouvons donc en spécifier n’importe laquelle, par exemple l’option cache
:
const { Liquid } = require('liquidjs')
const engine = new Liquid({
cache: true
})
API Document : Voici un aperçu de toutes les options. Pour les types et signatures exacts, veuillez vous référer à LiquidOptions API.
Le cache
est utilisé pour améliorer les performances en mettant en cache les structures de modèles précédemment analysées, en particulier dans les cas où nous analysons ou restituons des fichiers à plusieurs reprises.
La valeur par défaut est false
. Lorsque la valeur est true
, un cache LRU par défaut de taille 1024 sera activé. Et bien sûr, il peut s’agir d’un nombre qui indique la taille du cache souhaitée.
De plus, il peut également s’agir d’une implémentation de cache personnalisée. Voir Mise en cache pour plus de détails.
root est utilisé pour spécifier les répertoires de modèles permettant à LiquidJS de rechercher et de lire les fichiers de modèles. Peut être une chaîne unique ou un tableau de chaînes. Voir Render Files pour plus de détails.
layouts est utilisé pour spécifier des répertoires de modèles pour que LiquidJS recherche des fichiers pour {% layout %}
. Même format que root
et sera par défaut root
s’il n’est pas spécifié.
partials est utilisé pour spécifier des répertoires de modèles pour que LiquidJS recherche des fichiers pour {% render %}
et {% include %}
. Même format que root
et sera par défaut root
s’il n’est pas spécifié.
relativeReference est défini sur true
par défaut pour autoriser les noms de fichiers relatifs. Notez que les fichiers relativement référencés doivent également se trouver dans la racine correspondante. Par exemple, vous pouvez référencer un autre fichier comme {% render ../foo/bar %}
tant que ../foo/bar
se trouve également dans le répertoire partials
.
Note : pour des raisons historiques, il s’appelle DynamicPartials mais il fonctionne également pour les mises en page.
dynamicPartials indique s’il faut ou non traiter les arguments de nom de fichier dans les balises include, render et layout comme une variable. La valeur par défaut est true
. Par exemple, le rendu de l’extrait suivant avec scope { file: 'foo.html' }
inclura le foo.html
:
{% include file %}
En définissant dynamicPartials: false
, LiquidJS essaiera d’inclure le fichier nommé file
, ce qui est étrange mais permet une syntaxe plus simple si vos relations de modèle sont statiques :
{% liquid foo.html %}
Piège courant : LiquidJS définit par défaut cette option sur
true
pour être compatible avec shopify/liquid, mais si vous venez de eleventy, elle est définie surfalse
par défaut (voir Quoted Include Paths) qui, je crois, essaie d’être compatible avec Jekyll.
jekyllInclude est utilisé pour activer la syntaxe d’inclusion de type Jekyll-like. La valeur par défaut est false
, lorsqu’elle est définie sur true
:
dynamicPartials
est désormais défini par défaut sur false
(au lieu de true
). Et vous pouvez redéfinir dynamicPartials
sur true
.=
au lieu de :
pour séparer les valeurs-clés des paramètres.include
au lieu de la portée actuelle.Par exemple, dans le modèle suivant, name.html
n’est pas cité, header
et "HEADER"
sont séparés par =
et le paramètre header
est référencé par include.header
. Pour plus de détails, veuillez consulter include.
// entry template
{% include article.html header="HEADER" content="CONTENT" %}
// article.html
<article>
<header>{{include.header}}</header>
{{include.content}}
</article>
extname
définit le nom d’extension par défaut à ajouter aux noms de fichiers si le nom de fichier n’a pas de nom d’extension. La valeur par défaut est ''
, ce qui signifie qu’il est désactivé par défaut. En le réglant sur .liquid
:
{% render "foo" %} there's no extname, adds `.liquid` and loads foo.liquid
{% render "foo.html" %} there is an extname already, loads foo.html directly
Legacy Versions : Avant la version 2.0.1,
extname est défini par défaut sur
.liquid. Pour changer cela, vous devez définir explicitement
extname: ‘’`. Voir #41 pour plus de détails.
fs est utilisé pour définir une implémentation de système de fichiers personnalisée qui sera utilisée par LiquidJS pour rechercher et lire les fichiers modèles. Voir Abstract File System pour plus de détails.
globals est utilisé pour définir des variables globales disponibles pour tous les modèles, même en cas de render tag. Voir 3185 pour plus de détails.
jsTruthy est utilisé pour utiliser la véracité JavaScript standard plutôt que Shopify.
la valeur par défaut est false
. Par exemple, lorsqu’elle est définie sur true
, une chaîne vide sera évaluée comme false
avec jsTruthy
. Avec la véracité de Shopify, une chaîne vide est true
.
outputEscape peut être utilisé pour échapper automatiquement les chaînes de sortie. Il peut s’agir de "escape"
, "json"
ou (val: inconnu) => string
, la valeur par défaut est undefined
.
outputEscape : "escape"
les fait être échappées en HTML par défaut. Vous aurez besoin d’un filtre brut pour une sortie directe."json"
est utile lorsque vous utilisez LiquidJS pour créer des fichiers JSON valides.timezoneOffset est utilisé pour spécifier un fuseau horaire différent pour afficher les dates, votre fuseau horaire local sera utilisé s’il n’est pas spécifié. Par exemple, définir timezoneOffset: 0
pour afficher toutes les dates en UTC/GMT 00:00.
preserveTimezones est un booléen qui n’effectue que des horodatages littéraux. Lorsqu’ils sont définis sur true
, tous les horodatages littéraux resteront les mêmes lors de la sortie. Il s’agit d’une option d’analyseur, donc les objets Date transmis à LiquidJS en tant que données ne seront pas affectés. Notez que preserveTimezones
a une priorité plus élevée que timezoneOffset
.
dateFormat est utilisé pour spécifier un format par défaut pour les dates de sortie. %A, %B %-e, %Y at %-l:%M %P %z
sera utilisé s’il n’est pas spécifié. Par exemple, définissez dateFormat: %Y-%m-%dT%H:%M:%S:%LZ
pour afficher toutes les dates dans le format JavaScript Date.toJson().
Les options greedy, trimOutputLeft, trimOutputRight, trimTagLeft, trimTagRight sont utilisées pour éliminer les nouvelles lignes et les retraits supplémentaires dans les modèles autour des constructions liquides. Voir Whitespace Control pour plus de détails.
outputDelimiterLeft, outputDelimiterRight, tagDelimiterLeft, tagDelimiterRight sont utilisés pour personnaliser les délimiteurs pour LiquidJS Tags and Filters. Par exemple avec outputDelimiterLeft : <%=, outputDelimiterRight : %>
nous pouvons éviter les conflits avec d’autres langages :
<%= username | append: ", welcome to LiquidJS!" %>
strictFilters est utilisé pour affirmer l’existence du filtre. S’il est défini sur false
, les filtres non définis seront ignorés. Sinon, les filtres non définis provoqueront une exception d’analyse. La valeur par défaut est false
.
strictVariables est utilisé pour affirmer l’existence d’une variable. Si la valeur est false
, les variables non définies seront rendues sous forme de chaîne vide. Sinon, les variables non définies provoqueront une exception de rendu. La valeur par défaut est false
.
lenientIf modifie le comportement de strictVariables
pour permettre la gestion des variables facultatives. Si elle est définie sur true
, une variable non définie ne provoquera pas d’exception dans les deux situations suivantes : a) c’est la condition d’une balise if
, elsif
ou unless
; b) cela se produit juste avant un filtre par default
. Sans importance si strictVariables
n’est pas défini. La valeur par défaut est false
.
ownPropertyOnly masque les variables de portée des prototypes, ce qui est utile lorsque vous transmettez un objet non nettoyé dans LiquidJS ou que vous devez masquer les prototypes des modèles. La valeur par défaut est true
.
Nonexistent Tags : Les balises inexistantes génèrent toujours des erreurs lors de l’analyse et ce comportement ne peut pas être personnalisé.
Les ordres de paramètres sont ignorés par défaut, car ea {% for i in (1..8) reversed limit:3 %}
exécutera toujours la limit
avant reversed
, même si reversed
se produit avant la limit
. Pour que l’ordre des paramètres soit respecté, définissez orderedFilterParameters sur true
. Sa valeur par défaut est false
.
Pour un projet typique, il peut y avoir un répertoire de fichiers modèles, vous devrez définir le template root et appeler renderFile ou renderFileSync pour restituer un fichier spécifique.
Par exemple, vous disposez d’un répertoire de modèles comme celui-ci :
.
├── index.js
└── views/
├── hello.liquid
└── world.liquid
hello.liquid
contient une seule ligne name : {{name}}
.
Enregistrez maintenant le contenu suivant dans index.js
:
var engine = new Liquid({
root: path.resolve(__dirname, 'views/'), // root for layouts/includes lookup
extname: '.liquid' // used for layouts/includes, defaults ""
});
engine
.renderFile("hello", {name: 'alice'}) // will read and render `views/hello.liquid`
.then(console.log) // outputs "Alice"
Exécutez node index.js
et vous obtiendrez un résultat comme ceci :
> node index.js
name: alice
Noms des fichiers modèles transmis aux balises API renderFile, parseFile, renderFileSync, parseFileSync, et include, layout sont résolues par rapport à the root option.
Il peut s’agir d’un chemin de type chaîne (voir l’exemple ci-dessus) ou d’une liste de répertoires racine, auquel cas les modèles seront recherchés dans cet ordre. par ex.
var engine = new Liquid({
root: ['views/', 'views/partials/'],
extname: '.liquid'
});
Chemins relatifs : Les chemins relatifs dans
root
seront résolus par rapport àcwd()
.
Lorsque {% render "foo" %}
est rendu ou que liquid.renderFile('foo')
est appelé, les fichiers suivants seront recherchés et le premier fichier existant sera utilisé :
cwd()
/views/foo.liquidcwd()
/views/partials/foo.liquidSi aucun des fichiers ci-dessus n’existe, une erreur ENOENT
sera générée. Voici une démo pour Node.js : demo/nodejs.
Lorsque LiquidJS est utilisé dans le navigateur, disons que l’emplacement actuel est https://example.com/bar/index.html, seule la première racine sera utilisée et le fichier à récupérer est :
Si la récupération échoue, une erreur 404/500 ou des pannes de réseau par exemple, une erreur ENOENT
sera générée.
Voici une démo pour les navigateurs : demo/browser.
LiquidJS définit une interface de système de fichiers abstraite et l’implémentation par défaut est src/fs/fs-impl.ts pour Node.js et src/build/fs-impl-browser.ts pour le bundle de navigateur.
Le constructeur Liquid fournit une option fs pour spécifier l’implémentation du système de fichiers. Il est censé être utilisé pour définir une logique de récupération de modèle personnalisée, c’est-à-dire récupérer un modèle à partir d’une table de base de données, comme :
var engine = new Liquid({
fs: {
readFileSync (file) {
return db.model('Template').findByIdSync(file).text
},
async readFile (file) {
const template = await db.model('Template').findById(file)
return template.text
},
existsSync () {
return true
},
async exists () {
return true
},
contains () {
return true
},
resolve(root, file, ext) {
return file
}
}
});
Vulnérabilité de traversée de chemin : La valeur par défaut de
contain()
renvoie toujours true. Cela signifie que lorsque vous spécifiez un système de fichiers abstrait, vous devrez fournir uncontain()
approprié pour éviter d’exposer de telles vulnérabilités.
Pour faciliter le rendu sans fichiers, il existe une option de templates
permettant de spécifier un mappage des noms de fichiers et de leur contenu. LiquidJS lira les modèles du mappage.
const engine = new Liquid({
templates: {
'views/entry': 'header {% include "../partials/footer" %}',
'partials/footer': 'footer'
}
})
engine.renderFileSync('views/entry'))
// Result: 'header footer'
Notez que les options du système de fichiers telles que root
, layouts
, partials
, relativeReference
seront ignorées lorsque les templates
sont spécifiés.
Pour les fichiers modèles suivants :
// file: color.liquid
color: '{{ color }}' shape: '{{ shape }}'
// file: theme.liquid
{% assign shape = 'circle' %}
{% render 'color.liquid' %}
{% render 'color.liquid' with 'red' %}
{% render 'color.liquid', color: 'yellow', shape: 'square' %}
Le résultat sera :
color: '' shape: 'circle'
color: 'red' shape: 'circle'
color: 'yellow' shape: 'square'
Pour plus de détails, veuillez vous référer à la balise render.
L’extension “.liquid” : L’extension « .liquid » dans le
layout
,render
etinclude
peut être omise si l’instance Liquid est créée à l’aide de l’option extname : « .liquid ». Voir the extname option pour plus de détails.
Pour les fichiers modèles suivants :
// file: default-layout.liquid
Header
{% block content %}My default content{% endblock %}
Footer
// file: page.liquid
{% layout "default-layout.liquid" %}
{% block content %}My page content{% endblock %}
La sortie de page.liquid
:
Header
My page content
Footer
Pour plus de détails, veuillez vous référer à la balise layout.
LiquidJS est compatible avec les express template engines. Vous pouvez définir l’instance liquidjs sur l’option du view engine :
var { Liquid } = require('liquidjs');
var engine = new Liquid();
// register liquid engine
app.engine('liquid', engine.express());
app.set('views', './views'); // specify the views directory
app.set('view engine', 'liquid'); // set liquid to default
Démo de travail : Voici une démo fonctionnelle pour l’utilisation de LiquidJS dans Express.js : liquidjs/demo/express/.
L’option root continuera à fonctionner en tant que racine des modèles, comme vous pouvez le voir dans Render A Template File. De plus, l’option views
dans express.js (comme indiqué ci-dessus) sera également respectée. Supposons que vous ayez un répertoire de modèles tel que :
.
├── views1/
│ └── hello.liquid
└── views2/
└── world.liquid
Et vous définissez la racine du modèle pour liquidjs sur views1
et expressjs sur views2
:
var { Liquid } = require('liquidjs');
var engine = new Liquid({
root: './views1/'
});
app.engine('liquid', engine.express());
app.set('views', './views2'); // specify the views directory
app.set('view engine', 'liquid'); // set liquid to default
hello.liquid
et world.liquid
peuvent être résolus et rendus :
res.render('hello')
res.render('world')
Définir simplement l’cache option sur true activera la mise en cache des modèles, comme expliqué dans Caching. Il est recommandé d’activer le cache dans l’environnement de production, ce qui peut être fait en :
var { Liquid } = require('liquidjs');
var engine = new Liquid({
cache: process.env.NODE_ENV === 'production'
});
Dans un projet de site Web typique, nous aurons un répertoire de modèles de vues et ils seront rendus plusieurs fois. Dans un environnement de production, les fichiers modèles ne sont pas susceptibles d’être modifiés au fil du temps (en dehors des redéploiements). Il est donc logique de mettre en cache le contenu des fichiers et les modèles analysés (dans une sorte d’AST) pour améliorer les performances.
LiquidJS propose plusieurs façons de mettre en cache les modèles analysés pour améliorer les performances.
Les API .parse(), .parseFile(), .parseFileSync() sont utilisées pour analyser des modèles à partir d’une chaîne ou de fichiers. Le modèle de résultat peut ensuite être rendu plusieurs fois avec un contexte différent.
Analyser à partir de la chaîne :
var tpl = engine.parse('{{name | capitalize}}');
engine.renderSync(tpl, {name: 'alice'}) // 'Alice'
engine.renderSync(tpl, {name: 'bob'}) // 'Bob'
Analyser à partir du fichier :
var tpl = engine.parseFileSync('hello'); // contents of `hello.liquid`: {{name}}
engine.renderSync(tpl, {name: 'alice'}) // 'Alice'
engine.renderSync(tpl, {name: 'bob'}) // 'Bob'
La chaîne/le fichier du modèle n’est analysé qu’une seule fois et restitué plusieurs fois en utilisant un contexte différent. Les modèles de différents fichiers peuvent être stockés dans une Map
et peuvent être récupérés directement pour les rendus ultérieurs.
Le cache option peut être définie pour demander à liquidjs d’utiliser des modèles analysés en cache chaque fois que vous appelez renderFile ou renderFileSync.
var { Liquid } = require('liquidjs');
var engine = new Liquid({
cache: true
});
// liquidjs parses the hello.liquid, then renders it with {name: 'alice'}
engine.renderFileSync('hello', {name: 'alice'})
// liquidjs finds the cached template, then renders it with {name: 'bob'}
engine.renderFileSync('hello', {name: 'bob'})
L’évasion est importante dans tous les langages, y compris LiquidJS. Alors que l’échappement a 2 significations différentes pour un moteur de modèle :
Par défaut, la sortie n’est pas échappée. Bien que vous puissiez utiliser le filtre escape pour cela :
Saisir :
{{ "1 < 2" | escape }}
Sortie :
1 < 2
Il existe également des filtres escape_once, newline_to_br, strip_html pour vous permettre d’affiner votre sortie.
Dans les cas où les variables ne sont généralement pas fiables, outputEscape peut être défini sur "escape"
pour appliquer l’échappement par défaut. Dans ce cas, lorsque vous avez besoin de ne pas échapper une sortie, un filtre raw peut être utilisé :
Saisir :
{{ "1 < 2" }}
{{ "<button>OK</button>" | raw }}
Sortie :
1 < 2
<button>OK</button>
Pour désactiver le langage Liquid et les chaînes de sortie telles que { {
et {%
, la balise raw peut être utilisée.
Saisir :
{% raw %}
In LiquidJS, {{ this | escape }} will be HTML-escaped, but
{{{ that }}} will not.
{% endraw %}
Sortie :
In LiquidJS, {{ this | escape }} will be HTML-escaped, but
{{{ that }}} will not.
Dans les chaînes littérales du modèle LiquidJS, \
peut être utilisé pour échapper aux caractères spéciaux dans la syntaxe des chaînes. Par exemple:
Saisir :
{{ "\"" }}
Sortie :
"
// Usage: {% upper name %}
import { Value, TagToken, Context, Emitter, TopLevelToken } from 'liquidjs'
engine.registerTag('upper', {
parse: function(tagToken: TagToken, remainTokens: TopLevelToken[]) {
this.value = new Value(token.args, liquid)
},
render: function*(ctx: Context) {
const str = yield this.value.value(ctx); // 'alice'
return str.toUpperCase() // 'ALICE'
}
});
parse
: lit les jetons de remainTokens
jusqu’à votre jeton de fin.
render
: combinez les données de portée avec vos jetons analysés dans une chaîne HTML.
Pour une implémentation de balises complexe, vous pouvez également fournir une classe de balises :
// Usage: {% upper name:"alice" %}
import { Hash, Tag, TagToken, Context, Emitter, TopLevelToken, Liquid } from 'liquidjs'
engine.registerTag('upper', class UpperTag extends Tag {
private hash: Hash
constructor(tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(tagToken, remainTokens, liquid)
this.hash = new Hash(tagToken.args)
}
* render(ctx: Context) {
const hash = yield this.hash.render();
return hash.name.toUpperCase() // 'ALICE'
}
});
// Usage: {{ name | upper }}
engine.registerFilter('upper', v => v.toUpperCase())
Les arguments de filtre seront transmis à la fonction de filtre enregistrée, par exemple :
// Usage: {{ 1 | add: 2, 3 }}
engine.registerFilter('add', (initial, arg1, arg2) => initial + arg1 + arg2)
Voir les implémentations de filtres existantes ici : https://github.com/harttle/liquidjs/tree/master/src/filters
Dans certains cas, il est souhaitable de désactiver certaines balises/filtres (voir #324), vous devrez enregistrer une balise/filtre factice dans lequel une erreur correspondante est générée.
// disable a tag
const disabledTag = {
parse: function(token) {
throw new Error(`tag "${token.name}" disabled`);
}
}
engine.registerTag('include', disabledTag);
// disable a filter
function disabledFilter(name) {
return function () {
throw new Error(`filter "${name}" disabled`);
}
}
engine.registerFilter('plus', disabledFilter('plus'));
Comme indiqué dans Register Filters/Tags, nous pouvons accéder aux arguments de filtre directement dans la fonction de filtre comme :
// Usage: {{ 1 | add: 2, 3 }}
// Output: 6
engine.registerFilter('add', (initial, arg1, arg2) => initial + arg1 + arg2)
Lorsqu’il s’agit de filtres avec état, par exemple pour transformer un chemin d’URL en URL complète, nous devrons accéder à une origin
dans la portée actuelle :
// Usage: {{ '/index.html' | fullURL }}
// Scope: { origin: "https://liquidjs.com" }
// Output: https://liquidjs.com/index.html
engine.registerFilter('fullURL', function (path) {
const origin = this.context.get(['origin'])
return new URL(path, origin).toString()
})
Voir ce JSFiddle : https://jsfiddle.net/ctj364up/1/
Arrow Functions : ceci dans les fonctions fléchées est lié au contexte JavaScript actuel, vous devrez utiliser la syntaxe
function(){}
au lieu de la syntaxe()=>{}
pour accéder correctement àthis.context
.
Comme indiqué dans Register Filters/Tags, les paramètres de balise sont disponibles sur tagToken.args
sous forme de chaîne raw. Par exemple:
// Usage: {% random foo bar coo %}
// Output: "foo", "bar" or "coo"
engine.registerTag('random', {
parse(tagToken) {
// tagToken.args === "foo bar coo"
this.items = tagToken.args.split(' ')
},
render(context, emitter) {
// get a random index
const index = Math.floor(this.items.length * Math.random())
// output that item
emitter.write(this.items[index])
}
})
Voici une version JSFiddle : https://jsfiddle.net/ctj364up/2/
Parfois, nous avons besoin de balises plus dynamiques et souhaitons transmettre des valeurs à la balise personnalisée au lieu de chaînes statiques. Les variables dans LiquidJS peuvent être littérales (chaîne, nombre, etc.) ou une variable de la portée du contexte actuel.
Le modèle modifié suivant contient également 3 valeurs aléatoires, mais ce sont des valeurs au lieu de chaînes statiques. Le premier est une chaîne littérale, le deuxième est un identifiant, le troisième est une séquence d’accès à la propriété contenant deux identifiants.
{% random "foo" bar obj.coo %}
Il peut être difficile d’analyser tous ces cas manuellement, mais il existe une classe Tokenizer dans LiquidJS que vous pouvez utiliser.
const { Liquid, Tokenizer, evalToken } = require('liquidjs')
engine.registerTag('random', {
parse(tagToken) {
const tokenizer = new Tokenizer(tagToken.args)
this.items = []
while (!tokenizer.end()) {
// here readValue() returns a LiteralToken or PropertyAccessToken
this.items.push(tokenizer.readValue())
}
},
* render(context, emitter) {
const index = Math.floor(this.items.length * Math.random())
const token = this.items[index]
// in LiquidJS, we use yield to wait for async call
const value = yield evalToken(token, context)
emitter.write(value)
}
})
L’appel de cette balise dans scope { bar: "bar", obj: { coo: "coo" } }
donne exactement le même résultat que le premier exemple. Voir ce JSFiddle : https://jsfiddle.net/ctj364up/3/
Async and Promises : Les appels asynchrones dans LiquidJS sont implémentés directement par les générateurs, car nous pouvons appeler les générateurs de manière synchrone, donc cette implémentation de balise est également valable pour
renderSync()
,parseAndRenderSync()
,renderFileSync()
. Si vous devez attendre une promesse dans l’implémentation de la balise, remplacez simplementwait somePromise
paryield somePromise
et gardez* render()
au lieu deasync render()
fera l’affaire. Voir Sync et Async pour plus de détails.
Les paramètres nommés deviennent très pratiques lorsqu’il y a des paramètres facultatifs ou de nombreux paramètres, auquel cas l’ordre des paramètres n’a pas d’importance. C’est exactement pour cela que la classe Hash a été inventée.
{% random from:2, to:max %}
Dans l’exemple ci-dessus, nous essayons de générer un nombre aléatoire dans la plage [2, max]. Nous utiliserons Hash
pour analyser les paramètres from
et to
.
const { Liquid, Hash } = require('liquidjs')
engine.registerTag('random', {
parse(tagToken) {
// parse the parameters structure into `this.args`
this.args = new Hash(tagToken.args)
},
* render(context, emitter) {
// evaluate the parameters in `context`
const {from, to} = yield this.args.render(context)
const length = to - from + 1
const value = from + Math.floor(length * Math.random())
emitter.write(value)
}
})
Le rendu de {% random from:2, to:max %}
dans la portée { max: 10 }
générera un nombre aléatoire dans la plage [2, 10]. Voir ce JSFiddle : https://jsfiddle.net/ctj364up/4/
Les balises personnalisées peuvent avoir un modèle de contenu et peuvent être imbriquées. Cet article décrit comment implémenter des balises personnalisées composées d’une balise de début , d’une balise de fin et d’un contenu de modèle entre elles.
Nous commencerons avec une balise simple wrap
qui enveloppe son contenu dans un élément <div class="wrapper"></div>
:
{% wrap %}
{{ "hello world!" | capitalize }}
{% endwrap %}
Résultat attendu :
<div class='wrapper'>
Hello world!
</div>
Tout d’abord, register une balise avec le nom wrap
et analysez le contenu dans this.tpls
. Ici dans parse(tagToken, remainTokens)
,
tagToken
est le jeton actuel {% wrap %}
, etremainTokens
est un tableau de tous les jetons suivant {% wrap %}
jusqu’à la fin de ce fichier modèle.En gros, ce que nous devons faire est de prendre .shift()
suffisamment de balises jusqu’à remainTokens
ce que nous obtenions un jeton endwrap
(le nom peut être arbitraire, mais par convention, nous avons besoin qu’il soit endwrap
). Et s’il n’y a pas de endwrap
jusqu’à la fin du fichier modèle, nous devons lancer un tag-not-closed Error
.
engine.registerTag('wrap', {
parse(tagToken, remainTokens) {
this.tpls = []
let closed = false
while(remainTokens.length) {
let token = remainTokens.shift()
// we got the end tag! stop taking tokens
if (token.name === 'endwrap') {
closed = true
break
}
// parse token into template
// parseToken() may consume more than 1 tokens
// e.g. {% if %}...{% endif %}
let tpl = this.liquid.parser.parseToken(token, remainTokens)
this.tpls.push(tpl)
}
if (!closed) throw new Error(`tag ${tagToken.getText()} not closed`)
},
* render(context, emitter) {
emitter.write("<div class='wrapper'>")
yield this.liquid.renderer.renderTemplates(this.tpls, context, emitter)
emitter.write("</div>")
}
})
.renderTemplates()
peut être asynchrone, nous devons yield
attendre qu’il soit terminé. Pour plus de détails sur async
dans LiquidJS, veuillez vous référer à Sync et Async. Les autres parties de render()
la méthode est assez simples. Voici une version JSFiddle : https://jsfiddle.net/por0zcn1/3/
En ce qui concerne les balises complexes comme for et if, cela parse()
peut être très compliqué. Il existe un utilitaire ParseStream pour organiser les balises parse()
dans un style basé sur les événements. Voici une version réécrite parse()
en utilisant ParseStream
et qui fait exactement la même chose que l’exemple ci-dessus.
parse(tagToken, remainTokens) {
this.tpls = []
this.liquid.parser.parseStream(remainTokens)
.on('template', tpl => this.tpls.push(tpl))
// note that we cannot use arrow function because we need `this`
.on('tag:endwrap', function () { this.stop() })
.on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`) })
.start()
}
Voici une version JSFiddle : https://jsfiddle.net/por0zcn1/4/. Pour plus de simplicité, les exemples suivants sont implémentés à l’aide de ParseStream
.
La balise wrap
ci-dessus ne semble pas très utile, sans utiliser cette balise, nous pouvons de toute façon restituer le contenu. Nous allons maintenant implémenter une balise repeat
pour restituer le contenu 2 fois (nous pouvons également ajouter un paramètre pour restituer des fois arbitraires).
{% repeat %}
{{ repeat.i }}. {{ "hello world!" | capitalize }}
{% endrepeat %}`
Résultats attendus :
1. Hello world!
2. Hello world!
Comme vous l’avez remarqué, il existe un élément supplémentaire repeat.i
dans le contexte de repeat
. Cela est implémenté en manipulant le Context.
Contexte : Le contexte définit la valeur de chaque variable dans le modèle Liquid. Dans LiquidJS, un
Context
est constitué d’une pile deScopes
. Une portée est un objet simple comme celui spécifié dansengine.render(tpl, scope)
.
Chaque fois que nous entrons dans un nouveau Context, nous devons pousser un nouveau Scope. Et lorsque nous terminons le rendu et sortons du Context , nous extrayons le Scope du Context. Comme vous pouvez le voir dans l’implémentation suivante :
engine.registerTag('repeat', {
parse(tagToken, remainTokens) {
this.tpls = []
this.liquid.parser.parseStream(remainTokens)
.on('template', tpl => this.tpls.push(tpl))
.on('tag:endrepeat', function () { this.stop() })
.on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`) })
.start()
},
* render(context, emitter) {
const repeat = { i: 1 }
context.push({ repeat })
yield this.liquid.renderer.renderTemplates(this.tpls, context, emitter)
repeat.i++
yield this.liquid.renderer.renderTemplates(this.tpls, context, emitter)
context.pop()
}
})
C’est parse()
exactement la même chose que wrap
tag, on répète le contenu simplement en appelant .renderTemplates(this.tpls)
deux fois pendant render()
. Voici le JSFiddle : https://jsfiddle.net/por0zcn1/2/
Utilisez Push & Pop par paires :
context.push()
etcontext.pop()
doivent être utilisés par paires. L’échec depop()
la portée que vous avez poussée entraînera une fuite de la portée vers les modèles ultérieurs et peut corrompre la pile de contexte.
LiquidJS fournit également un mécanisme similaire à Shopify Drops, permettant aux auteurs de modèles d’intégrer des fonctionnalités personnalisées dans la résolution des valeurs de variables.
Drop pour JavaScript : L’interface Drop est implémentée différemment dans LiquidJS par rapport aux filtres intégrés et aux autres fonctionnalités de modèle. Étant donné que LiquidJS s’exécute en JavaScript, les Drops personnalisés doivent de toute façon être réimplémentés en JavaScript. Il n’y a aucune compatibilité entre les classes JavaScript et les classes Ruby.
import { Liquid, Drop } from 'liquidjs'
class SettingsDrop extends Drop {
constructor() {
super()
this.foo = 'FOO'
}
bar() {
return 'BAR'
}
}
const engine = new Liquid()
const template = `foo: {{settings.foo}}, bar: {{settings.bar}}`
const context = { settings: new SettingsDrop() }
// Outputs: "foo: FOO, bar: BAR"
engine.parseAndRender(template, context).then(html => console.log(html))
Comme indiqué ci-dessus, en plus de lire les propriétés à partir des portées de contexte, vous pouvez également appeler des méthodes. Il vous suffit de créer une classe personnalisée héritée de Drop
.
Méthodes asynchrones : LiquidJS est entièrement compatible avec l’asynchronisme. Vous pouvez renvoyer en toute sécurité une promesse dans vos méthodes
Drop
ou définir vos méthodes dansDrop
commeasync
.
Dans les cas où il n’existe pas d’ensemble fixe de propriétés, vous pouvez utiliser cette fonction liquidMethodMissing
pour résoudre dynamiquement la valeur d’un nom de variable.
import { Liquid, Drop } from 'liquidjs'
class SettingsDrop extends Drop {
liquidMethodMissing(key) {
return key.toUpperCase()
}
}
const engine = new Liquid()
// Outputs: "COO"
engine.parseAndRender("{{settings.coo}}", { settings: new SettingsDrop() })
.then(html => console.log(html))
liquidMethodMissing
prend en charge Promise, ce qui signifie que vous pouvez effectuer des appels asynchrones à l’intérieur. Un cas plus utile peut être la récupération dynamique de la valeur à partir de la base de données. En utilisant Drops, vous pouvez éviter de coder en dur chaque propriété dans le contexte. Par exemple :
import { Liquid, Drop } from 'liquidjs'
class DBDrop extends Drop {
async liquidMethodMissing(key) {
const record = await db.getRecordByKey(key)
return record.value
}
}
const engine = new Liquid()
const context = { db: new DBDrop() }
engine.parseAndRender("{{db.coo}}", context).then(html => console.log(html))
Les gouttes peuvent implémenter une valueOf()
méthode dont la valeur de retour peut être utilisée pour se remplacer elle-même dans la sortie. Par exemple :
import { Liquid, Drop } from 'liquidjs'
class ColorDrop extends Drop {
valueOf() {
return 'red'
}
}
const engine = new Liquid()
const context = { color: new ColorDrop() }
// Outputs: "red"
engine.parseAndRender("{{color}}", context).then(html => console.log(html))
toLiquid()
n’est pas une méthode de Drop
, mais elle peut être utilisée pour renvoyer un Drop. Dans les cas où vous avez une structure fixe dans laquelle context
vous ne pouvez pas modifier ses valeurs, vous pouvez implémenter toLiquid()
pour permettre à LiquidJS d’utiliser la valeur renvoyée au lieu de lui-même pour restituer les modèles.
import { Liquid, Drop } from 'liquidjs'
const context = {
person: {
firstName: "Jun",
lastName: "Yang",
name: "Jun Yang",
toLiquid: () => ({
firstName: this.firstName,
lastName: this.lastName,
// use a different `name`
name: "Yang, Jun"
})
}
}
const engine = new Liquid()
// Outputs: "Yang, Jun"
engine.parseAndRender("{{person.name}}", context).then(html => console.log(html))
Bien sûr, vous pouvez également renvoyer une PersonDrop
instance dans la toLiquid()
méthode et implémenter cette fonctionnalité dans PersonDrop
:
import { Liquid, Drop } from 'liquidjs'
class PersonDrop extends Drop {
constructor(person) {
super()
this.person = person
}
name() {
return this.person.lastName + ", " + this.person.firstName
}
}
const context = {
person: {
firstName: "Jun",
lastName: "Yang",
name: "Jun Yang",
toLiquid: function () { return new PersonDrop(this) }
}
}
const engine = new Liquid()
// Outputs: "Yang, Jun"
engine.parseAndRender("{{person.name}}", context).then(html => console.log(html))
toLiquid()
vs.valueOf()
Différence
valueOf()
est généralement utilisé pour définir comment la variable actuelle doit être rendue, tandis qu’iltoLiquid()
est souvent utilisé pour convertir un objet en Drop ou une autre portée fournie au modèle.valueOf()
est une méthode exclusive à Drops ; alors qu’elletoLiquid()
peut être utilisée sur n’importe quel objet de portée.valueOf()
est appelé lorsque la variable elle-même est sur le point d’être rendue, se remplaçant elle-même ; alors qu’iltoLiquid()
est appelé lorsque ses propriétés sont sur le point d’être lues.
LiquidJS lui-même implémente plusieurs gouttes intégrées pour faciliter l’écriture de modèles. Cette partie est compatible avec Shopify Liquid, car nous avons besoin que les modèles soient portables.
Utile pour vérifier si une variable de chaîne est false
, null
, undefined
, une chaîne vide ou une chaîne contenant uniquement des caractères vides.
{% unless author == blank %}
{{author}}
{% endif %}
Utile pour vérifier si un tableau, une chaîne ou un objet est vide.
{% if authors == empty %}
Author list is empty
{% endif %}
empty
mise en œuvre : Pour les tableaux et les chaînes, LiquidJS vérifie leurs.length
propriétés. Pour les objets, LiquidJS effectue des appelsObject.keys()
pour vérifier s’ils ont des clés.
nil
Drop est utilisé pour vérifier si une variable n’est pas définie ou définie comme null
ou undefined
, essentiellement équivalent à == null
la vérification JavaScript.
{% if nonexistent == nil %}
null variable
{% endif %}
Il existe encore plusieurs Drops pour des balises spécifiques, comme forloop
, tablerowloop
, block
, qui sont couverts par les documents de balises respectifs.
LiquidJS prend en charge les évaluations synchrones et asynchrones et peut être utilisé avec les promesses. Pour réutiliser le même ensemble d’implémentations de balises/filtres à la fois en synchronisation et en asynchrone, les balises LiquidJS sont implémentées en tant que générateurs.
Toutes les principales méthodes de Liquid prennent en charge à la fois la synchronisation et l’asynchronisation. Ces méthodes renvoient des promesses :
render()
renderFile()
parseFile()
parseAndRender()
evalValue()
La version synchrone des méthodes contient un Sync
suffixe :
renderSync()
renderFileSync()
parseFileSync()
parseAndRenderSync()
evalValueSync()
LiquidJS utilise une implémentation asynchrone basée sur un générateur pour prendre en charge à la fois l’asynchrone et la synchronisation dans une seule implémentation de balise. Par exemple, ci-dessous UpperTag
peut être utilisé à la fois dans engine.renderSync()
et engine.render()
.
import { TagToken, Context, Emitter, TopLevelToken, Value, Tag, Liquid } from 'liquidjs'
// Usage: {% upper "alice" %}
// Output: ALICE
engine.registerTag('upper', class UpperTag extends Tag {
private value: Value
constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(token, remainTokens, liquid)
this.value = new Value(token.args, liquid)
}
* render (ctx: Context, emitter: Emitter) {
const title = yield this.value.value(ctx)
emitter.write(title.toUpperCase())
}
})
Toutes les balises intégrées sont implémentées de cette manière et peuvent être utilisées en toute sécurité à la fois en synchronisation et en asynchrone (je l’appellerai sync-compatible). Pour rendre votre balise personnalisée sync-compatible, vous devrez :
* render()
, dans laquellereturn <Promise>
, etMais LiquidJS est compatible avec Promise, n’est-ce pas ? Vous pouvez toujours appeler des fonctions basées sur Promise et attendre cette promesse dans les implémentations de balises. Remplacez simplement await
par yield
. par exemple, nous appelons fs.readFile()
qui renvoie un Promise
:
* render (ctx: Context, emitter: Emitter) {
const file = yield this.value.value(ctx)
const title = yield fs.readFile(file, 'utf8')
emitter.write(title.toUpperCase())
}
Maintenant que cela * render()
appelle une API qui renvoie une promesse, elle n’est plus compatible avec la synchronisation .
Balises non compatibles avec la synchronisation : Les balises non compatibles avec la synchronisation sont également des balises valides et fonctionneront parfaitement pour les appels d’API asynchrones. Lorsqu’elles sont appelées de manière synchrone, les balises qui renvoient un
Promise
seront rendues sous la forme[object Promise]
.
Vous pouvez convertir un générateur en promesse en utilisant toPromise, par exemple :
import { TagToken, Context, Emitter, TopLevelToken, Value, Tag, Liquid, toPromise } from 'liquidjs'
// Usage: {% upper "alice" %}
// Output: ALICE
engine.registerTag('upper', class UpperTag extends Tag {
private value: Value
constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(token, remainTokens, liquid)
this.value = new Value(token.args, liquid)
}
async render (ctx: Context, emitter: Emitter) {
const title = await toPromise(this.value.value(ctx))
emitter.write(title.toUpperCase())
}
})
Si votre balise est destinée à être utilisée uniquement de manière asynchrone, elle peut être déclarée ainsi async render()
afin que vous puissiez l’utiliser await
directement dans son implémentation :
import { toPromise, TagToken, Context, Emitter, TopLevelToken, Value, Tag, Liquid } from 'liquidjs'
// Usage: {% upper "alice" %}
// Output: ALICE
engine.registerTag('upper', class UpperTag extends Tag {
private value: Value
constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(token, remainTokens, liquid)
this.value = new Value(token.args, liquid)
}
async render (ctx: Context, emitter: Emitter) {
const title = await toPromise(this.value.value(ctx))
emitter.write(`<h1>${title}</h1>`)
}
})
Pour garder le code source propre et indenté, nous ajoutons des espaces à nos modèles. LiquidJS offre des fonctionnalités de contrôle des espaces pour éliminer ces espaces indésirables dans le code HTML de sortie.
Par défaut, toutes les balises et lignes de balisage de sortie génèrent un NL (\n
) et des espaces s’il y a une indentation. Par exemple :
{% author = "harttle" %}
{{ author }}
Sorties (notez le lien vide) :
harttle
Nous pouvons inclure des tirets dans la syntaxe de votre balise ({{-
, -}}
, {%-
, -%}
) pour supprimer les espaces à gauche ou à droite. Par exemple :
{% assign author = "harttle" -%}
{{ author }}
Résultats :
harttle
Dans ce cas, il -%}
supprime l’espace blanc du côté droit de la assign
balise.
Alternativement, LiquidJS fournit ces options par moteur pour permettre le contrôle des espaces blancs sans modifier radicalement vos modèles :
trimTagLeft
trimTagRight
trimOutputLeft
trimOutputRight
LiquidJS ne supprimera aucun espace par défaut, c’est-à-dire que toutes les options ci-dessus sont par défaut à false. Pour plus de détails sur ces options, consultez les options.
En mode gourmand (activé par l’option gourmande), tous les caractères d’espacement consécutifs (y compris \n
) seront supprimés. Le mode gourmand est activé par défaut pour être conforme à Shopify/Liquid.
Un certain nombre de balises et de filtres peuvent être encapsulés dans un plugin, qui sera généralement installé via npm. Cet article fournit des informations sur la création et l’utilisation d’un plugin.
Un plugin liquidjs est une fonction simple qui prend la classe Liquid comme premier paramètre et l’instance Liquid pour this
. Nous pouvons appeler les API liquidjs this
pour effectuer certaines modifications, notamment les register filters and tags.
Nous allons maintenant créer un plugin pour mettre en majuscules chaque lettre de l’entrée, enregistrez l’extrait suivant dans upup.js
:
/**
* Inside the plugin function, `this` refers to the Liquid instance.
*
* @param Liquid: provides facilities to implement tags and filters.
*/
module.exports = function (Liquid) {
this.registerFilter('upup', x => x.toUpperCase());
}
Il suffit de transmettre la fonction du plugin dans la .plugin()
méthode :
const engine = new Liquid()
engine.plugin(require('./upup.js'));
engine
.parseAndRender('{{ "foo" | upup }}')
.then(console.log) // outputs "FOO"
Étant donné que cette bibliothèque exclut certaines fonctionnalités disponibles sur la plateforme Shopify mais pas sur le référentiel Shopify/liquid, consultez Différences avec Shopify/liquid.
Voici une liste de plugins qui complètent ces fonctionnalités. N’hésitez pas à ajouter les vôtres, ce fichier est modifiable publiquement.
Les opérateurs LiquidJS sont très simples et différents. Il existe 2 types d’opérateurs pris en charge :
==
, !=
, >
, <
, >=
, <=
or
, and
, contains
Ainsi, les opérateurs numériques ne sont pas pris en charge et vous ne pouvez même pas ajouter deux nombres comme cela {{a + b}}
, à la place nous avons besoin d’un filtre {{ a | plus: b}}
. En fait, +
c’est un nom de variable valide dans LiquidJS.
Les opérateurs logiques sont évalués de droite à gauche, voir la documentation Shopify.
Bien que Liquid soit indépendant de la plate-forme, il existe certaines différences avec la version Ruby, dont l’une est la truthy
valeur.
D’après le document Shopify, tout ce qui n’est pas false
et nil
est vrai. Mais en JavaScript, nous avons un système de types totalement différent, nous avons des types comme undefined
et nous ne faisons pas de distinction entre integer
et float
, donc les choses sont légèrement différentes :
valeur | véridique | fausseté |
---|---|---|
true | ✔️ | |
false | ✔️ | |
null | ✔️ | |
undefined | ✔️ | |
string | ✔️ | |
empty string | ✔️ | |
0 | ✔️ | |
integer | ✔️ | |
float | ✔️ | |
array | ✔️ | |
empty array | ✔️ |
Notez que LiquidJS utilise la vérité de Shopify par défaut. Mais il peut être basculé pour utiliser la vérité JavaScript standard en définissant l’ option jsTruthy
sur true
.
valeur | véridique | fausseté |
---|---|---|
true | ✔️ | |
false | ✔️ | |
null | ✔️ | |
undefined | ✔️ | |
string | ✔️ | |
empty string | ✔️ | |
0 | ✔️ | |
integer | ✔️ | |
float | ✔️ | |
array | ✔️ | |
empty array | ✔️ |
Lorsque le modèle ou le contexte de données ne sont pas fiables, il est essentiel d’activer les options de prévention DoS. LiquidJS propose 3 options à cet effet : parseLimit
, renderLimit
, et memoryLimit
.
La définition de ces options peut en grande partie garantir que votre instance LiquidJS ne se bloquera pas pendant des périodes prolongées ou ne consommera pas de mémoire excessive. Ces limites sont basées sur les API JavaScript disponibles, il ne s’agit donc pas de limites strictes précises, mais de seuils permettant d’éviter que votre processus ne tombe en panne ou ne se bloque.
const liquid = new Liquid({
parseLimit: 1e8, // typical size of your templates in each render
renderLimit: 1000, // limit each render to be completed in 1s
memoryLimit: 1e9, // memory available for LiquidJS (1e9 for 1GB)
})
Lorsqu’un parse()
ou render()
ne peut pas être complété dans le cadre d’une ressource donnée, il est rejeté.
parseLimit limite la taille (longueur des caractères) des modèles analysés à chaque .parse()
appel, y compris les partiels et les mises en page référencés. Étant donné que LiquidJS analyse les chaînes de modèles en un temps proche de O(n), la limitation de la longueur totale du modèle est généralement suffisante.
Un PC typique gère 1e8
(100 M) de caractères sans problème.
Restreindre la taille du modèle seul n’est pas suffisant car des boucles dynamiques avec un grand nombre peuvent se produire au moment du rendu. renderLimit atténue ce problème en limitant le temps consommé par chaque render()
appel.
{%- for i in (1..10000000) -%}
order: {{i}}
{%- endfor -%}
Le temps de rendu est vérifié pour chaque modèle (avant le rendu de chaque modèle). Dans l’exemple ci-dessus, il y a 2 modèles dans la boucle : order:
et {{i}}
, le temps de rendu sera vérifié 10000000x2 fois.
Pour les balises et les filtres chronophages au sein d’un seul modèle, le processus peut toujours se bloquer. Pour un rendu entièrement contrôlé, pensez à utiliser un gestionnaire de processus comme paralleljs.
Même avec un nombre réduit de modèles et d’itérations, l’utilisation de la mémoire peut augmenter de manière exponentielle. Dans l’exemple suivant, la mémoire double à chaque itération :
{% assign array = "1,2,3" | split: "," %}
{% for i in (1..32) %}
{% assign array = array | concat: array %}
{% endfor %}
memoryLimit limite les filtres sensibles à la mémoire pour éviter une allocation de mémoire excessive. Comme JavaScript utilise GC pour gérer la mémoire, memoryLimit
limite uniquement le nombre total d’objets alloués par les filtres sensibles à la mémoire dans LiquidJS et peut donc ne pas refléter l’empreinte mémoire réelle.