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!
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() });
},
},
};
}
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,
};
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");
};
};
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()],
});
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>
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>
Does this actually work properly...? Of course, it does!
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.
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
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! 🚀
Top comments (2)
Very interesting post! I'm curious about the feasibility of combining Vue 2 and Vue 3 components in the same Astro project. I have a Vuepress project with numerous Vue 2 components, and while I'm not ready to rewrite them all for Vue 3, I'm interested in developing new components with Vue 3. My plan is to migrate my Vue 2 components to an Astro project and gradually integrate Vue 3 components. Over time, I aim to upgrade the Vue 2 components one by one. Do you think this approach would work well with Astro's architecture?
I'm happy to hear that the article interests you! Yes, while I haven't confirmed all of vue2 features work fine, I think it is technically possible.