Using Vue 2 component in Astro, not Vue3 .... is it possible?

ktmouk

ktmouk

Posted on February 11, 2023

Using Vue 2 component in Astro, not Vue3 .... is it possible?

Introduction

Recently, I've become interested in Astro and Island Architecture and have started reading its documentation.

As you know, Astro has a great feature called UI Framework Integrations that allows you to write components using popular frameworks such as React!

List of UI frameworks

According to the official documentation, Astro already supports almost all of the popular frameworks: React, Vue3, and Svelte, which is great...... wait, where is Vue2?

Vue2 will reach EOL at the end of 2023

Seriously, I know that it has been announced in the official docs that Vue2 will be EOL, so there is no way Astro will support Vue2 in the future.

However, I am curious if it is technically possible for Astro to support Vue2.

Let's make Vue2 integration myself!

Fortunately, Astro's documentation already covers how to create a new integration and is easy to understand. And surprisingly, only three files need to be created to add a new integration: main.js, client.js, and server.js!

Now let's look into how to create a new integration.

main.js

This file is a sort of configuration file for the integration. When the astro:config:setup hook is called, Astro will add a renderer and update Vite settings according to this file.

import { AstroIntegration } from "astro";
import { createVuePlugin } from "vite-plugin-vue2";

function getRenderer() {
  return {
    name: "astro-vue2",
    clientEntrypoint: "astro-vue2/client.js",
    serverEntrypoint: "astro-vue2/server.js",
  };
}

function getViteConfiguration() {
  return {
    optimizeDeps: {
      include: ["astro-vue2/client.js", "vue"],
      exclude: ["astro-vue2/server.js"],
    },
    plugins: [createVuePlugin()],
  };
}

export default function (): AstroIntegration {
  return {
    name: "astro-vue2",
    hooks: {
      "astro:config:setup": ({ addRenderer, updateConfig }) => {
        addRenderer(getRenderer());
        updateConfig({ vite: getViteConfiguration() });
      },
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

I have added vite-plugin-vue2 to Vite's configuration to allow Vite to build Vue2 components.

And the most remarkable thing is the getRenderer function. This function defines two entry points: serverEntrypoint and clientEntrypoint.

The serverEntrypoint is called at build time and SSR (Server Side Rendering). And the clientEntrypoint is called at hydration time.

Now let's delve into the details of these entry points.

server.js

The serverEntrypoint returns two functions, check and renderToStaticMarkup, but both tasks are very simple.

import Vue from "vue";
import { SSRLoadedRenderer } from "astro";
import { createRenderer } from "vue-server-renderer";
import { buildScopedSlots } from "./scoped-slots";

const check = async (
  Component,
  _props,
  _children
) => {
  return !!Component["staticRenderFns"];
};

const renderToStaticMarkup =
  async (Component, props, slotted, _metadata) => {
    const instance = new Vue({
      render: (h) =>
        h(Component, { props, scopedSlots: buildScopedSlots(h, slotted) }),
    });
    const html = await createRenderer().renderToString(instance);
    return { html };
  };

export default {
  check,
  renderToStaticMarkup,
};
Enter fullscreen mode Exit fullscreen mode

The check function simply returns a boolean value indicating whether the given Component is the Vue2 component or not. so I used the staticRenderFns property, which only Vue2 has (although I don't know what the property is for...)

Also, The renderToStaticMarkup function should return a static HTML string based on the given Component. So I use the vue-server-renderer library to get the HTML from the Vue2 component.

client.js

The clientEntrypoint is simpler than the serverEntrypoint! Its role is to hydrate the Vue2 component based on the HTML created at SSR time.

import Vue from "vue";
import { buildScopedSlots } from "./scoped-slots";

Vue.config.ignoredElements.push("astro-slot");

export default (element: any) => {
  return async (
    Component: any,
    props: any,
    slotted: Record<string, string>,
    { client }: { client: string | null }
  ) => {
    new Vue({
      render: (h) =>
        h("astro-island", [
          h(Component, { props, scopedSlots: buildScopedSlots(h, slotted) }),
        ]),
    }).$mount(element, client !== "only");
  };
};
Enter fullscreen mode Exit fullscreen mode

Note that Astro has the client:only option that allows Astro to behave SPA-like. So I need to pass a boolean as the second argument to $mount to tell Vue2 if it should hydrate the component.

Let's use the Vue2 integration!

All done! Now let's try out this integration! Before trying it, I added the Vue2 integration to Astro's configuration in advance. The astro-vue2 package is the same as main.js.

import { defineConfig } from "astro/config";
import vue2 from "astro-vue2";

// https://astro.build/config
export default defineConfig({
  integrations: [vue2()],
});
Enter fullscreen mode Exit fullscreen mode

And I made a very simple Vue2 counter having a <slot />.

<template>
  <div>
    <button v-on:click="handleClick">Vue2 counter: {{ value }}</button>
    <p>slot: <slot /></p>
  </div>
</template>

<script>
export default {
  props: {
    defaultValue: {
      type: Number,
      required: true,
    },
  },
  data() {
    return {
      value: this.defaultValue,
    };
  },
  methods: {
    handleClick() {
      return this.value++;
    },
  },
};
</script>
Enter fullscreen mode Exit fullscreen mode

This component is then used in an Astro component. Also, In this astro file, another Astro component is passed to the Vue2 component as a child component.

---
// @ts-ignore
import Counter from "../components/Counter.vue";

import Child from "../components/Child.astro";
import Layout from "../layouts/Layout.astro";
---

<Layout title="Astro and Vue2">
  <main>
    <Counter defaultValue={10} client:visible>
      <Child name={"Astro component"} />
    </Counter>
  </main>
</Layout>
Enter fullscreen mode Exit fullscreen mode

Does this actually work properly...? Of course, it does!

The demo of Vue2 integration

I've created just three files, but all of Astro's functions work surprisingly fine. Also, you can build these components with the astro build command!

This code is on GitHub

All of the code used in the demo is available on GitHub. If you're interested, you can view the code in this repository.

GitHub logo ktmouk / astro-vue2

Astro with Vue 2.x

astro-vue2

This is the proof of concept if Astro can integrate with Vue 2, not 3. I got curious about whether Astro can support Vue2 technically and I made it. It isn't meant for production use.

How to run

It is easy to run this project, like below. If you don't install the pnpm, please install it referring to pnpm's installation in advaince.

# Run in dev mode.
pnpm install
pnpm dev

# Or build and look at it in preview mode. it also works fine!
pnpm install
pnpm preview
Enter fullscreen mode Exit fullscreen mode

Folder structure

This repository consists of two packages managed by the pnpm workspace.

/integration

This is the library for Astro can integrate with Vue2. I made it while referencing Vue3 plugin, which is offical integration.

/astro

This is the Astro project to test above plugin. It uses plugins at astro.config.mjs.

License

MIT




I hope this article will interest you in Astro! 🚀

💖 💪 🙅 🚩
ktmouk
ktmouk

Posted on February 11, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related