Progressive Petite-vue

Hi 👋, this is an article on progressive enchancement with petite-vue. Here I will tell you about its cool features (both as a separate tool and as part of Vue). Of course, it would be cool if you read the previous article on Petite-vue, there is a lot about lib in general, there are some basic examples, but “it’s okay” don’t read it. If you think about something in Vue, there are not so many differences (which, among other things, will be discussed here).

Well, I hope you’re “ready to action”, so let’s jump straight into the code.

Simple implementation of progressive enchancement

<title> Petite Vue Progressive Enchancement </title>
<style>
  [v-cloak] {display:none}
  body { background: #fff!important }
</style>

<script src="https://unpkg.com/petite-vue" defer init></script>
<script>
  const SCRIPTS = [
    "https://ahfarmer.github.io/calculator/static/js/main.b319222a.js",
  ]
  const CSS = [
    "https://ahfarmer.github.io/calculator/static/css/main.b51b4a8b.css",
  ]

  function embedScript(mount, src) {
    const tag = document.createElement("script")
    tag.src = src
    return mount.appendChild(tag)
  }
  function embedCSS(mount, src) {
    const tag = document.createElement("link")
    tag.rel = "stylesheet"
    tag.href = src
    return mount.appendChild(tag)
  }
  function tryToLoadSPA() {
    setTimeout(loadSPA, 1000)
  }
  function loadSPA() {
    const mount = document.getElementsByTagName("head")[0]
    SCRIPTS.map(src => embedScript(mount, src))
    CSS.map(src => embedCSS(mount, src))
  }
</script>

<body>
  <div id="root" v-scope="{num: 0}" v-cloak @mounted="tryToLoadSPA">
    {{ num }} <button @click="num++">+</button>
  </div>
</body>

There is probably a lot of incomprehensible things here. Probably it will help if you come right in to look at finished application, you poke around.

What happens in essence: while petite-vue has not loaded – we do not show anything, after loading petite-vue – we show app-counter and put styles and scripts on loading powerful React application in N (= 1) seconds, after loading the React application – petite-vue disappears.

As an experimental React application, I took (the first link from the documentation :)) this calculator

So, well, now let’s figure out what is written here and how you can improve it.

<style>
  [v-cloak] {display:none}
  body { background: #fff!important }
</style>

Attribute v-cloak allows you to hide part of the tree until the moment when petite-vue parsed it. In fact, you can set any style there, but the most logical thing is to hide an element that will not work as the user expects (this directive is also in regular Vue). I also set a white background, since the styles of the calculator change it, and I want to see if the page remains responsive while I load the scripts and styles (it’s just that the black text of my counter is not visible on the black one (in short, forget it)).

After the styles, you can see this javascript:

const SCRIPTS = [
  "https://ahfarmer.github.io/calculator/static/js/main.b319222a.js",
]
const CSS = [
  "https://ahfarmer.github.io/calculator/static/css/main.b51b4a8b.css",
]

function embedScript(mount, src) {
  const tag = document.createElement("script")
  tag.src = src
  return mount.appendChild(tag)
}
function embedCSS(mount, src) {
  const tag = document.createElement("link")
  tag.rel = "stylesheet"
  tag.href = src
  return mount.appendChild(tag)
}
function tryToLoadSPA() {
  setTimeout(loadSPA, 1000)
}
function loadSPA() {
  const mount = document.getElementsByTagName("head")[0]
  SCRIPTS.map(src => embedScript(mount, src))
  CSS.map(src => embedCSS(mount, src))
}

In fact, a global function is created here. tryToLoadSPAwhich will load a large SPA application based on some logic (I have a timeout for demonstration purposes). You can put data there performanceto load SPA depending on FCP or TTI or whatever … You can at this point make a pop-up window in which to ask the user or he wants to load extended version of the site… In general, the essence is clear. I took crutches for asynchronous loading of js and css with stackoverflow

Well, the final piece is the already bearded counter:

<div id="root" v-scope="{num: 0}" v-cloak @mounted="tryToLoadSPA">
  {{ num }} <button @click="num++">+</button>
</div>

If suddenly you see for the first time v-scope is a feature exclusively for petite-vue. In essence, you are asking $data field, while simultaneously initializing the child of the DOM tree as a Vue component (see article x for details).

Immediately another exclusive piece – @mounted… This is a directive that will execute the JS code every time the component is uploaded (in our case, only once). In normal Vue this field mounted component structures. By analogy with other directive names, it may seem that there is an event onmountedbut it is not – only petite-vue can handle / dispatch this event. Similarly, there is an event @unmountedthat fires when an item is removed from the tree.

Note that this element has id="root"… This is no coincidence – when a React application arrives, it will grind everything we write in petite-vue and no artifacts will appear.

Let’s take a look at the resulting performance graph.

v-effect

Of course, it’s worth agreeing that in the previous example we got a very powerful foundation for a progressive application, but I still disagree. The first question that will show the system is inconsistent – I use a callback @mounted, but what if he is already busy ???

Let’s try to come up with a solution using another exclusive Petite-vue v-effect… This is a very powerful directive that allows you to execute reactive scripts.

If you know about useEffect from React, then v-effect it’s such useEffect on minimal, which itself defines an array of dependencies and runs the inline script when some dependency changes. Let’s watch example from documentation:

<div v-scope="{ count: 0 }">
  <div v-effect="$el.textContent = count"></div>
  <button @click="count++">++</button>
</div>

Here the array of dependencies (input / external variables) is defined as [ $data.count ] and every time this variable is updated, the script $el.textContent = count will be executed over and over again.

Let’s now use this tool for our progressive example:

<div
  id="root"
  v-scope="{num: 0}"
  v-cloak
  v-effect="tryToLoadSPA()"
  @mounted="console.log('hi')"
>
  {{ num }} <button @click="num++">+</button>
</div>

Now we get the same result as in the first solution, but we have a free callback for @mounted

You can make sure that this example works for page

Custom directives

Well, what if I want to use and v-effect and @mounted??? Do you really have to write with a crutch? But what if I want to automatically build the application, and not write entrypoints manually every time right in the Javascript?

In this case, you can make a custom directive. In regular Vue this is also possible, but there is obviously a different set of possibilities :). In general, let’s try to do at least something – then we’ll figure it out.

<title> Petite Vue Progressive Enchancement </title>
<style>
  [v-cloak] {display:none}
  body { background: #fff!important }
</style>

<script type="module">
  import { createApp } from "https://unpkg.com/petite-vue?module"

  const SCRIPTS = [
    "https://ahfarmer.github.io/calculator/static/js/main.b319222a.js",
  ]
  const CSS = [
    "https://ahfarmer.github.io/calculator/static/css/main.b51b4a8b.css",
  ]

  function embedScript(mount, src) {
    const tag = document.createElement("script")
    tag.src = src
    return mount.appendChild(tag)
  }
  function embedCSS(mount, src) {
    const tag = document.createElement("link")
    tag.href = src
    tag.rel = "stylesheet"
    return mount.appendChild(tag)
  }
  function tryToLoadSPA() {
    setTimeout(loadSPA, 1000)
  }
  function loadSPA() {
    const mount = document.getElementsByTagName("head")[0]
    SCRIPTS.map(src => embedScript(mount, src))
    CSS.map(src => embedCSS(mount, src))
  }

  function App() {
    return { num: 0 }
  }

  const progressiveDirective = (ctx) => {
    tryToLoadSPA()
  }

  createApp({ App, tryToLoadSPA })
    .directive('progressive', progressiveDirective)
    .mount()
</script>

<body>
  <div id="root" v-scope="App()" v-cloak v-progressive>
    {{ num }} <button @click="num++">+</button>
  </div>
</body>

Here we have replaced v-effect and @mounted on the v-progressive… This is the directive we added like this:

const progressiveDirective = (ctx) => {
  tryToLoadSPA()
}

createApp({ App, tryToLoadSPA })
  .directive('progressive', progressiveDirective)
  .mount()

We have a very simple case – we need to execute something on the mount of the element to which the directive is attached, so we don’t use ctx the context available to the directive, but simply call the required function there.

But in ctx a bunch of utilities are transferred (from the docks):

const myDirective = (ctx) => {
  // элемент, на который привязана директива
  ctx.el

  // необработанное значение, передающееся в директиву
  // для v-my-dir="x" это будет "x"
  ctx.exp

  // дополнительный аргумент через ":"
  // v-my-dir:foo -> "foo"
  ctx.arg

  // массив модификаторов
  // v-my-dir.mod -> { mod: true }
  ctx.modifiers

  // можно вычислить выражение ctx.exp
  ctx.get()

  // можно вычислить произвольное выражение
  ctx.get(`${ctx.exp} + 10`)

  ctx.effect(() => {
    // это аналог v-effect, будет вызыватся при изменении значения ctx.get()
    console.log(ctx.get())
  })

  return () => {
    // колбек, который вызывается при unmountе элемента
  }
}

// добавляем директиву к глобальной области petite-vue
createApp().directive('my-dir', myDirective).mount()

You can improve our example if you don’t hardcode the constants. SCRIPTS and CSS, and pass inside the directive v-progressive an array of entrypoints and automatically parse and load everything (csss are separate from js). But this is a lot of code that you don’t want to just insert here – the idea is clear :).

Custom constraints

It seems that we figured out everything about extensibility, now another problem has emerged: I use mustache / handlebars / jinja / something else where there are already limiters {{ and }}

In such a case, you can change the petite-vue delimiters by passing …

createApp({
  $delimiters: ['$<', '>$']
}).mount()

In place $< and >$ naturally it can be anything. It is better to make the length shorter and in the pattern such characters are not encountered too often (you need to think about the search speed in the string :)).

Conclusion.min.js

I don’t want to write something here, because I didn’t tell you about everything that is in petite-vue. However, I talked about all the unique (different from Vue) features that allow building directly on top of the DOM faster and more efficiently. In general, it’s ok …

You can see my examples here

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *