perspectographe.fr

journal de recherches

Une routine de rendu de pages destinées à l'impression via Vue,

Tags : vuejs, impression

Nous avons souvent eu besoin de générer des médias paginés destinés à l’impression (d’abord via wkhtmltopdf, utilisant une version figée du moteur de rendu Webkit), puis via PhantomJS (utilisant Chrome headless), puis enfin, actuellement, via Puppeteer, nous assurant des mises à jour fréquentes de Chromium.

Il s’est avéré, d’expérience, plus simple de simuler des directives de layout (gestion des sauts de page, gestion des tailles de page, des marges tournantes) en concevant des composants dédiés qu’en utilisant les directives CSS correspondantes (@page, page-break, etc.). Malheureusement, même Chrome qui semble poussé vers la génération d’imprimé est encore très fragile dans son interprêtation de page-break.

Lab212 - rendu d'un projet prêt à être imprimé Lab212 - rendu d’un projet prêt à être imprimé

Vue fut une belle surprise en ce sens, en réduisant drastiquement la quantité de code nécessaire à répliquer ces directives CSS, tout en créant une structure agréable au travail de l’imprimé.

Ainsi, notre séquence de pages ci-dessus s’exprime de cette manière :

<page v-for="(page, index) in pages">
  <page-inner :index="index" total="total">
     <page-fragment 
        v-for="fragment in page.content"
        :fragment="fragment"
        @rendered="renderStep"/>
  </page-inner>
</page>

Cependant, les contenus du site ne nous proviennent bien sûr pas sous forme paginée. La conversion se fait, dans cet exemple réduit, au niveau du composant hébergeant cette séquence de pages.

/* Un utilitaire adapte le projet à une séquence ayant du sens en imprimé */
import { transformProject } from './transformers';
import { MAX_A4_INNER_HEIGHT } from './layout/constants';

export default {
  props: {
    project: {},
  },
  data() {
    return {
      pages: [[]],
      currentPage: 0,
      contentChunks: [],
    };
  },
  computed: {
    contents() {
      return transformProject(this.project);
    },
  },
  methods: {
    /* layout/0 */
    /* renderStep/0 */
  },
  async mounted() {
    this.layout();
  },
};

On observe donc que chaque fragment de page (rendu par un composant différent selon son type) émet un évènement “rendered” après son rendu. Cela permet d’attendre des ressources externes (images, etc.) et d’être certains de leur chargement (et donc dimensionnement) sans risquer de créer de course de données ou d’effectuer une étape de rendu trop tôt. Le composant “Fragment de page” n’est donc pas plus complexe que cela :

 <component :is="getComponentType(fragment)" :fragment="fragment" @stable="$emit('rendered')"/>

Cette flexibilité permet d’implémenter layout/0 et renderStep/0 d’une manière très simple :

Attendre que Vue ait terminé son cycle de rendu. Sélectionner l’élement DOM correspondant à la page courante dans le flux de page.

S’il possède du contenu en débord, ajouter une page, supprimer de la page courante son dernier fragment, et l’ajouter à la page fraîchement ajoutée. Incrémenter le compteur suivant la page actuelle.

Sinon, il suffit pour chaque composant d’attendre qu’il émette rendered, ce qui appelle renderStep et place le prochain fragment.

{
/* data, computed, etc. ...*/
async layout() {
  this.contentStash = this.contents.slice(0);
  await new Promise(resolve => this.$nextTick(resolve));
  this.renderStep();
},
async renderStep() {
  await new Promise(resolve => this.$nextTick(resolve));
  const wrapper = this.$el.querySelectorAll('.page-inner')[this.currentPage];
  if ((wrapper.getBoundingClientRect().height) > MAX_A4_INNER_HEIGHT) {
    this.pages.push([]);
    this.contentStash.unshift(this.pages[this.currentPage].pop());
    this.currentPage += 1;
    this.renderStep();
  } else if (this.contentStash.length > 0) {
    this.pages[this.currentPage].push(this.contentStash.shift());
  }
}};

Cette simplicité d’implémentation tient à Vue, qui permet ici de contenir l’état nécessaire à implémenter cela sans générateurs, ni continuations, etc. Le composant enveloppant la séquence est responsable de garder trace des fragments placés, de la page courante, et de l’état du déroulé.

Bien entendu, l’implémentation ci-dessus ne termine pas si un fragment lui-même est plus grand que la zone utile de contenu. Il est très simple d’ajouter la logique nécessaire à cela, ou de s’assurer de cette pré-condition directement via les styles du fragment.