Jak vytvořit univerzální knihovnu pro Vue 2 a 3

Jak už asi víte, loni v září Evan You oznámila novou verzi Vue (Vue 3.0 nebo „One Piece“ pro přátele) během Vue.js Global Event – ​​oficiální vydání zde.

Humbuk po upgradu kódu na nejnovější verzi Vue explodoval a všichni (včetně mě) byli dychtiví začít. Porušují však změny, zejména na globálním rozhraní API, a nutí autory knihoven/pluginů migrovat svůj kód na podporu nové verze a Composition API . Pokud chcete lépe pochopit, proč jsem napsal článek o tom, jak provést migraci z 2.x na 3.x zde - Jak migrovat svou knihovnu z Vue 2.x na Vue 3.x

Jako autor knihovny Vue musím říci, že migrace nebyla snadná práce, napodobování toho, co dělaly velké knihovny:oddělení podpory pro každou verzi cílení do samostatných branches a tags (main pro vue 2.xa next pro vue 3.x) nebo dokonce mít samostatné repo pro zajištění lepší izolace kódu.

Jak vysvětluje člen jádra Vue.js @antfu (Anthony Fu) v tomto příspěvku:

Je možné toho dosáhnout pomocí vývojového nástroje, který stejný @antfu vytvořil, s názvem Vue-demi.

Takže pokud máte zájem dozvědět se, jak vytvořit univerzální knihovnu/plugin pro obě verze Vue, tento článek je pro vás.

Vytvořit základní nastavení

Začněme vytvořením nového projektu pomocí vue-cli prompt.

vue create vue-universal-lib

Ujistěte se, že jste vybrali verzi 3.x pro Vue a zbytek nechám na vašich preferencích, ale důrazně doporučuji, abyste použili stejné možnosti, jaké zde popisuji, abyste byli na stejné stránce:

Vybrané možnosti:

  • Babel
  • Psopis
  • Linter
  • Použijte syntaxi komponenty ve stylu třídy Ne
  • Použít Babel spolu s TypeScript Ano
  • Vyberte si linter:ESLint + Prettier

Po několika sekundách budeme mít základní strukturu pro začátek. Pravděpodobně se budete muset zbavit některých věcí, jako je App.vue a main.ts protože budeme hlavně pracovat s index.ts soubor.

Najděte účel

Zní to epicky, že? Samostatná zábava najděte nezbytnost, některé funkce často používané při vývoji webu, které chcete implementovat ve Vue a učinit je opakovaně použitelnými, něco, o čem si myslíte, že přinese hodnotu jako knihovna/plugin.

Pro tento výukový program vytvoříme jednoduchou knihovnu, která vám umožní animovat čísla jako počítadlo , podobný tomuto:

Tento typ komponenty se často používá na vstupních stránkách k zobrazení KPI.

Špinavé ruce

Nejprve si vytvořte counter-number komponenta pod src/components/CounterNumber.ts pomocí defineComponent .

import { ref, defineComponent, h } from 'vue';

export const CounterNumber = defineComponent({
  name: 'Awesome',
  props,
  setup(props, ctx) {
    const value = ref(640);

    return () =>
      h(
        'span',
        {
          class: 'counter-number',
        },
        value,
      );
  },
});

V tuto chvíli to nechme jako prezentační komponentu bez animace, později přidáme funkcionalitu prostřednictvím komposovatelné funkce, abychom využili Vue3's Composition API.

Můžete si také všimnout, že zde není žádná šablona pro komponentu, setup funkce vrací vykreslovací funkci s <span> prvek držící hodnotu čítače. To je zamýšleno a bude vysvětleno v sekci Caveates příspěvku.

Pro účely ukázky vynechejte main.ts a App.vue otestujte novou komponentu pomocí npm serve .

Instalace pluginu

Pro vytvoření samotného pluginu vytvořte src/index.ts :

import { App, inject, InjectionKey } from 'vue';
import { CounterNumber } from './components/CounterNumber';

export interface VueCounterOptions {
  theme: string;
}

export interface VueCounterPlugin {
  options?: VueCounterOptions;
  install(app: App): void;
}

export const VueCounterPluginSymbol: InjectionKey<VueCounterPlugin> = Symbol();

export function VueCounterPlugin(): VueCounterPlugin {
  const VueCounterPlugin = inject(VueCounterPluginSymbol);
  if (!VueCounterPlugin) throw new Error('No VueCounterPlugin provided!!!');

  return VueCounterPlugin;
}

export function createVueCounterPlugin(
  options?: VueCounterOptions,
): VueCounterPlugin {
  const plugin: VueCounterPlugin = {
    options,
    install(app: App) {
      app.component('vue-counter', CounterNumber);
      app.provide(VueCounterPluginSymbol, this);
    },
  };

  return plugin;
}

Pojďme si to rozdělit na části, funkce createVueCounterPlugin vám umožní nainstalovat plugin přes install při použití createApp.use() ve vaší aplikaci.

Tím se přidá k app instance všech komponent, vlastností vaší knihovny, jak vidíte výše, s app.component('vue-counter', CounterNumber);

Chcete-li získat většinu rozhraní Composition API a být schopni vkládat do komponent knihovny věci jako options nebo utilities vytvoříme Symbol pluginu použít spolu s app.provide v install metoda, kde předáme samotný createVueCounterPlugin jako parametr. V tuto chvíli to může vypadat složitě, ale je to standardní způsob:


// index.ts

...
export const VueCounterPluginSymbol: InjectionKey<VueCounterPlugin> = Symbol();

export function VueCounterPlugin(): VueCounterPlugin {
  const VueCounterPlugin = inject(VueCounterPluginSymbol);
  if (!VueCounterPlugin) throw new Error('No VueCounterPlugin provided!!!');

  return VueCounterPlugin;
}

...

Chcete-li plugin nainstalovat a otestovat, přejděte na src/main.ts :

import { createApp } from 'vue';
import App from './App.vue';
import './assets/styles/main.css';

import { createVueCounterPlugin } from './';

const VueCounterPlugin = createVueCounterPlugin();

createApp(App).use(VueCounterPlugin).mount('#app');

Pokud chcete svému pluginu předat možnosti, můžete to udělat takto

const VueCounterPlugin = createVueCounterPlugin({ theme: 'light' });

Kouzlo za tím, co jsme udělali, je použití app.provide v metodě instalace pluginu je, že můžeme později vložit možnosti pluginu jako závislost.

Nyní přidáme CounterNumber komponentu do src/App.vue .

// App.vue

<template>
  <h2 class="font-bold text-2xl mb-8 text-gray-600">
    Vue Counter animation
  </h2>
  <div
    class="card bg-gray-100 rounded-xl p-8 auto shadow-lg mx-auto w-1/3 text-indigo-400 font-bold text-xl"
  >
    <vue-counter />
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';

export default defineComponent({
  name: 'App',
});
</script>

Pokud jste zvědaví na pomocné třídy, které jsem zde použil, je to úžasný TailwindCSS, který miluji pro vytváření rychlých prototypů. Můžete jej nainstalovat také podle tohoto návodu. Jen se ujistěte, že jste tyto závislosti přidali jako devDependencies na váš package.json nebo budou součástí vaší knihovny.

Podívejme se, jak to vypadá v prohlížeči s npm run serve

Animace a kompozice

Vypadá krásně, ale potřebuje více magie. Přidejme animaci náběhu pro počítadlo. Abychom dosáhli hladké animace, budeme používat knihovnu s názvem anime.js, která je opravdu lehká a nabízí jednoduché a jednoduché API.

Mohli bychom přidat logiku přímo na CounterNumber komponentu, ale protože jsme již mluvili o Composition API pojďme to použít pro tento účel.

Vytvořte useCounter.ts soubor pod src/composables a exportujte funkci nazvanou useCounter takhle:

import { ref } from 'vue';

import anime from 'animejs/lib/anime.es.js';

export function useCounter() {
  const count = ref(0);
  const counter = {
    value: 0,
  };

  anime({
    targets: counter,
    duration: 2000, // 2000ms
    value: 640,
    easing: 'easeOutQuad',
    update: () => {
      count.value = Math.round(counter.value);
    },
  });

  return {
    count,
  };
}

Importujeme tovární funkci s názvem 'anime' z 'animejs/lib/anime.es.js' a předáváme cíl (v tomto případě obj obsahující ref s hodnotou, která má být animována).

anime() funkce přijímá mnoho parametrů pro přizpůsobení chování animace, jako je trvání , zpoždění , zmírnění a zpětná volání, jako je aktualizace který se spustí pokaždé, když animace aktualizuje cílový objekt. Zajímavé je, že jako vlastnost můžete předat stejnou vlastnost, kterou chcete animovat, v tomto případě value , ve výše uvedeném příkladu bude hodnota od 0 do 640. Další informace o animejs API zkontrolujte dokumenty

Vraťte se ke svému CounterNumber.ts a získejte použití count.value uvnitř span takhle:

export const CounterNumber = defineComponent({
  name: 'Awesome',
  props,
  setup(props, ctx) {
    const { count } = useCounter();

    return () =>
      h(
        'span',
        {
          class: 'counter-number',
        },
        count.value,
      );
  },
});

Nyní se vraťte do prohlížeče a obnovte stránku, abyste viděli, jak se počítadlo pohybuje od 0 na 640 za 2 sekundy.

Udělejte jej přizpůsobitelný

V tuto chvíli jsou všechny hodnoty napevno zakódovány, ale protože děláme knihovnu, měly by být tyto parametry pro animaci přizpůsobitelné, a proto by měly být předány jako rekvizity komponentě a dolů do kompoziční funkce.

Nejprve přidáme několik rekvizit, které dávají smysl:


// src/components/Counternumber

const props = {
  from: {
    type: [Number, String],
    default: 0,
  },
  to: {
    type: [Number, String],
    required: true,
    default: 0,
  },
  duration: {
    type: Number,
    default: 1000, // Duration of animation in ms
  },
  easing: {
    type: String,
    default: 'easeInOutQuad',
  },
  delay: {
    type: Number,
    default: 0, // Delay the animation in ms
  },
};

export const CounterNumber = defineComponent({
  name: 'Awesome',
  props,
  setup(props, ctx) {
    const { count } = useCounter(props);
...
  },
});

Ujistěte se, že jste předali rekvizity na useCounter(props) funkce;

Přejděte na App.vue a vytvořte nějaké proměnné, které se předají komponentě jako rekvizity:

<template>
  <h2 class="font-bold text-2xl mb-8 text-gray-600">Vue Counter animation</h2>
  <div
    class="card bg-gray-100 rounded-xl p-8 auto shadow-lg mx-auto w-1/3 text-indigo-400 font-bold text-xl"
  >
    <vue-counter :from="0" :to="640" :duration="3000" :delay="2000" />
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';

export default defineComponent({
  name: 'App',,
});
</script>

Nakonec se vraťte na useCounter.ts a předejte rekvizity anime instance

import { ref } from 'vue';

import anime from 'animejs/lib/anime.es.js';

export function useCounter(props: any, emit: any) {
  const count = ref(0);
  const counter = {
    value: props.from,
  };

  anime({
    targets: counter,
    duration: props.duration,
    value: props.to,
    delay: props.delay,
    easing: props.easing || 'linear',
    update: () => {
      count.value = Math.round(counter.value);
    },
  });

  return {
    count,
  };
}

Samozřejmě bychom museli přidat další kód, abychom vytvořili novou instanci anime objektu pokaždé, když se změní rekvizita, ale pro rozsah článku je více než dostačující.

Udělejte to univerzální

Tak skvělé, máme připravenou naši úžasnou knihovnu, v tuto chvíli je použitelná pouze na projektu s pro Vue 3 , jak můžeme dosáhnout izomorfní instalace?

To je místo vue-demi přichází na pomoc.

npm i vue-demi
# or
yarn add vue-demi

Přidejte vue a @vue/composition-api k závislostem vašeho pluginu, abyste určili, které verze podporujete.

// package.json

{
  "dependencies": {
    "vue-demi": "latest"
  },
  "peerDependencies": {
    "@vue/composition-api": "^1.0.0-beta.12",
    "vue": "^2.6.11 || >=3.0.5"
  }
}

Nyní přichází ta důležitá část 📝, udělat si k tomu poznámky:nahradit všechny importy pocházející z vue na vue-demi , jako tak:

import { defineComponent, ref } from 'vue';

Bude:

import { defineComponent, ref } from 'vue-demi';

Knihovna se přesměruje na vue@2 + @vue/composition-api nebo vue@3 na základě uživatelského prostředí.

To je mocné.

Sestavení konfigurace

Svůj balíček pluginů si můžete sestavit mnoha různými způsoby, webpack, vue-cli (také webpack), parser, rollup atd. Je to na vás, ale opravdu doporučuji použít rollup.js, je to skvělý balíček modulů, opravdu snadné se do něj dostat a používá se ve většině hlavních pluginů Vue, jako je Vue Router.

yarn add rollup rollup-plugin-vue rollup-plugin-typescript2 rollup-plugin-terser @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-replace -D

Také budeme muset trochu upravit konfiguraci, aby externalizovala vue-demi místo vue a nastavit jej jako globální v okamžiku sestavení. Protože rollup.config.js je poměrně velký, zde je odkaz na něj v ukázkovém repo.

V metodě createConfig ujistěte se, že máte vue-demi nastavte v globálních vlastnostech takto:

// rollup.config.js
...
output.globals = { 'vue-demi': 'VueDemi' };
...
const external = ['vue-demi'];

Nakonec přidáme script v package.json a cesty pro sestavení balíčku:

// package.json

"scripts": {
  "build": "rollup -c rollup.config.js",

}
"main": "dist/vue-universal-lib.cjs.js",
"browser": "dist/vue-universal-lib.esm.js",
"unpkg": "dist/vue-universal-lib.global.js",
"jsdelivr": "dist/vue-universal-lib.global.js",
"module": "dist/vue-universal-lib.esm-bundler.js",
"types": "dist/vue-universal-lib.d.ts",

Upozornění

Samozřejmě, že nejsou všechny růže 🌹 a jednorožci 🦄, případ použití vue-demi je spíše pro zásuvné moduly vue, které se příliš nespoléhají na komponenty vykreslování, protože funkce vykreslování Vue 2 a Vue 3 jsou zcela odlišné a dochází ke změnám mezi oběma, tj. v-model na komponentě, která očekává jinak pojmenované události ve Vue 2 vs 3 (ìvstup vs update:modelValue ).

Proto jsme pro definici komponenty použili funkci renderingu a .ts namísto .vue soubor. U této ukázkové knihovny to neovlivní konečný výsledek, ale je to něco, co musíte vzít v úvahu.

Jedním ze způsobů, jak případně upravit změny ve vaší lib komponentě, by bylo použití dalších rozhraní API z Vue Demi pomoci rozlišit uživatelská prostředí a provést určitou logiku specifickou pro verzi.

isVue2 isVue3

import { isVue2, isVue3 } from 'vue-demi';

if (isVue2) {
  // Vue 2 only
} else {
  // Vue 3 only
}

Jak již bylo řečeno, doufám, že tento článek byl dostatečně ilustrativní na cestě k vytvoření univerzálního pluginu pro Vue. Níže si poslechněte své myšlenky a otázky.

Šťastné kódování! 😎