Skip to content

Latest commit

 

History

History
806 lines (659 loc) · 15.6 KB

File metadata and controls

806 lines (659 loc) · 15.6 KB
theme layout info download drawings
vuetiful
cover
## Bridging the Gap - Building cross-compatible Vue Components with confidence Presentation slides for Vuejs Amsterdam, June 2nd-3rd 2022.
/slides-export.pdf
persist

Bridging the Gap

Building cross-compatible Components for Vue 2&3 with confidence


layout: section

What is "the gap"?


layout: full-image title: Canyon in need of a bridge image: ./assets/canyon1.jpeg


<style> div.cliff-item { position: absolute; } </style>
Vue 2
Vue 3
breaking changes
dependency conflicts
publishing strategies
Testing issues
code duplication

layout: big-points titleRow: true title: We have to...

  1. Write compatible code that respects breaking changes
  2. Write tests that can be run against both versions
  3. Manage conflicting dependencies
  4. Decide how to bundle and publish the project

preload: false clicks: 6

Introducing Vue-Bridge

This is still under heavy development


layout: big-points titleRow: true title: "Vue 2.7 will make this easier"

  • Vue 2.7-alpha was released this week
  • Supports Composition API in Vue 2
  • vue-demi / @vue/composition-api not required anymore
  • allows optimization of a few things in VueBridge

It might be worth to wait until 2.7 drops


layout: section

1. Writing compatible code


cols: '1-1' titleRow: true title: "Example: v-model on Components"

Vue 2

export default {
  props: {
    value: String
  },
  methods: {
    handleInput(e) {
      this.$emit('input', e.target.value)
    }
  }
}

::right::

Vue 3

export default {
  props: {
    modelValue: String
  },
  emits: ['update: modelValue'],
  methods: {
    handleInput(e) {
      this.$emit('update:modelValue', e.target.value)
    }
  }
}

cols: '2-1' titleRow: true title: "Example: v-model - Manual Fix"

export default {
  props: {
    modelValue: String
  },
  emits: ['update: modelValue'],
  model: {
    prop: 'modelValue',
    event: 'update:modelValue'
  },
  methods: {
    handleInput(e) {
      this.$emit('update:modelValue', e.target.value)
    }
  }
}

::right::

  • Manual work every time
  • Easy to miss
  • Tricky with Typescript

cols: '2-1' titleRow: true title: "Example: v-model - Vue-Bridge helper"

import {defineComponent } from '@vue-bridge/runtime'

export default defineComponent({
  props: {
    modelValue: String
  },
  emits: ['update: modelValue'],
  methods: {
    handleInput(e) {
      this.$emit('update:modelValue', e.target.value)
    }
  }
})

::right::

  • Write component for Vue 3 (future-proof)
  • @vue-bridge/runtime adds model config
  • works fine with TS

layout: section

Unit testing against both Versions


layout: big-points titleRow: true title: Challenges for unit Testing

  • Two versions of @vue/test-utils required (v1 vs. v2)
  • Subtle API differences between those versions

How to run the same tests against two different versions?


cols: '1-1' titleRow: true title: Example - Unit test

Vue 3 (test-utils v2)

import { mount } from '@vue/test-utils'

test('displays message', () => {
  const wrapper = mount(MessageComponent, {
    props: {
      msg: 'Hello world'
    },
    global: {
      provide: {
        store: mockStore
      }
    }
  })

  // Assert the rendered text of the component
  expect(wrapper.text()).toContain('Hello world')
})

::right::

Vue 2 (test-utils v1)

import { mount } from '@vue/test-utils'

test('displays message', () => {
  const wrapper = mount(MessageComponent, {
    propsData: {
      msg: 'Hello world'
    },
    provide: {
      store: mockStore
    }
  })

  // Assert the rendered text of the component
  expect(wrapper.text()).toContain('Hello world')
})

cols: '1-1' titleRow: true title: Example - Unit test

Using @vue-bridge/testing

- import { mount } from '@vue/test-utils'
+ import { mount } from '@vue-bridge/testing'

test('displays message', () => {
  const wrapper = mount(MessageComponent, {
    props: {
      msg: 'Hello world'
    },
    global: {
      provide: {
        store: mockStore
      }
    }
  })

  // Assert the rendered text of the component
  expect(wrapper.text()).toContain('Hello world')
})
import { mount } from '@vue-bridge/testing'


test('displays message', () => {
  const wrapper = mount(MessageComponent, {
    props: {
      msg: 'Hello world'
    },
    global: {
      provide: {
        store: mockStore
      },
    }
  })

  // Assert the rendered text of the component
  expect(wrapper.text()).toContain('Hello world')
})

::right::

  • Write in v2 style for Vue 3
  • Options transformed for Vue 2
  • TS Support
  • More tricks in the docs later

layout: section

Build-Time Optimizations


cols: '1-1' titleRow: true title: Build-time features

import { defineConfig } from 'vite'
import { createVuePlugin } from 'vite-plugin-vue2'
import { vueBridge } from '@vue-bridge/vite-plugin'

export default defineConfig({
  plugins: [
    createVuePlugin(),
    vueBridge({
      vueVersion: '2',
    }),
  ],
})

::right::

  • Support for version-specific style blocks
  • Support for version-specific imports
  • alias trickery for easier source-sharing in monorepos

cols: '1-1' titleRow: true title: Version-specific styles

<template>
  <div class="root-wrapper">
    <slot />
  </div>
</template>
<style scoped v2>
  .root-wrapper /deep/ .class-in-slot {
    color: red;
  }
</style>
<style scoped v3>
  .root-wrapper  :deep(.class-in-slot) {
    color: red;
  }
</style>

::right::

  • Different deep selectors
  • Different transition styles
  • escape hatch for various style fixes

cols: 1-1 titleRow: true title: Version-specific imports

import Teleport from './custom/teleport.ts?v-bridge'
// Compiled for Vue 3
import Teleport from './custom/teleport.vue3.ts'
// Compiled for Vue 2
import Teleport from './custom/teleport.vue2.ts'

::right::

  • put version-specific code into dedicated *.vueX.js files
  • import with ?v-bridge query param
  • @vue/bridge/vite-plugin will resolve file matching the target

layout: section

Managing conflicting dependencies


cols: 1-1 titleRow: true title: Dependency issues in flat repositories

{
  "name": "my-vue-package",
  "dependencies": {
    "lodash": "^4.17.21",
    "@vueuse/core": "^8.5.0"
  },
  "devDependencies": {
    "vue": "^3.2.32",
    "vue2": "npm:vue@^2.6.14",
    "@vitejs/plugin-vue": "^2",
    "vite-plugin-vue2": "^2",
    "vue-template-compiler": "^2.6.14",
  },
  "peerDependencies": {
    "vue": "^2.6 || ^3.2"
  }
}

::right::

Flat repositories are bad because:

  • they require package aliases
  • ...which can lead to peer Dependency issues on install
  • Packages relying on vue-demi make process cumbersome
A better solution: workspaces

cols: 1-1 titleRow: true title: Clean Dependencies with Workspaces (1)

# Folder Structure

lib-vue2/
├─ package.json
├─ vite.config.js
├─ src/                 # --> Symlink to /lib-vue3/src

lib-vue3/
├─ package.json
├─ vite.config.js
├─ src/
  ├─ index.js
  ├─ MyComponent.vue

package.json
pnpm-workspaces.yaml
vite.config.shared.js   # shared build config

cols: 1-1 titleRow: true title: Clean Dependencies with Workspaces (2)

# Folder Structure

lib-vue2/
├─ package.json
├─ vite.config.js
├─ src/                 # --> Symlink to /lib-vue3/src

lib-vue3/
├─ package.json
├─ vite.config.js
├─ src/
  ├─ index.js
  ├─ MyComponent.vue

package.json
pnpm-workspaces.yaml
vite.config.shared.js   # shared build config

::right::

{
  "name": "vue2-my-package",
  "dependencies": {
    "lodash": "^4.17.21",
    "@vueuse/core": "^8.5.0"
  },
  "devDependencies": {
    "@vue/test-utils": "^1.0.0",
    "vite-plugin-vue2": "^2",
    "vue": "^2.6.14",
    "vue-template-compiler": "^2.6.14",
  },
  "peerDependencies": {
    "vue": "^2.6"
  }
}

cols: 1-1 titleRow: true title: "Clean Dependencies with Workspaces (3)"

# Folder Structure

lib-vue2/
├─ package.json
├─ vite.config.js
├─ src/                 # --> Symlink to /lib-vue3/src

lib-vue3/
├─ package.json
├─ vite.config.js
├─ src/
  ├─ index.js
  ├─ MyComponent.vue

package.json
pnpm-workspaces.yaml
vite.config.shared.js   # shared build config

::right::

{
  "name": "vue3-my-package",
  "dependencies": {
    "lodash": "^4.17.21",
    "@vueuse/core": "^8.5.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^2",
    "@vue/test-utils": "^2.0.0-0",
    "vue": "^3.2.34",
  },
  "peerDependencies": {
    "vue": "^3.2.34"
  }
}

layout: section

Bundling & Publishing


cols: '1-1' titleRow: true title: Bundling and Publishing as two Packages

// Vue 3
import { MyComponent } from 'vue3-my-package'
// Vue 2
import { MyComponent } from 'vue2-my-package'
lib-vue3
├─ dist/
  ├─ index.js # Vue 3 Bundle
├─ package.json
lib-vue2
  dist/
  ├─ index.js # Vue 2 Bundle
├─ package.json
{
  "name": "vue{2,3}-my-package"
  "exports": {
    ".": {
      "import": "./dist/index.js"
    },
  }
}

::right::

  • Dedicated bundles for Vue 2 and Vue 3
  • Separate packages for Vue 2 and Vue 3
  • Clean separation of dependencies
  • straightforward generation of type declarations

cols: '1-1' titleRow: true title: Bundling and Publishing as one Package

// Vue 3
import { MyComponent } from 'vue-my-package'
// Vue 2
import { MyComponent } from 'vue-my-package/vue2'
lib-vue3
  ├─ dist/
    ├─ index.js # Vue 3 Bundle
  dist-vue2/
    ├─ index.js # Vue 2 Bundle
  package.json
{
  "name": "vue-my-package",
  "exports": {
    ".": {
      "import": "./dist/index.js"
    },
    "./vue2": "./dist-vue2/index.js"
  }
}

::right::

  • Dedicated bundles for Vue 2 and Vue 3
  • One package to install regardless of target version
  • Possibly more conflicts with peerDependencies
  • Trickier to get right with type declarations

layout: big-points title: Template Demo

Demo Time!

https://github.com/vue-bridge/template-monorepo


title: Docs


layout: big-points titleRow: true title: Vue-Bridge needs you!

  • Add missing Documentation chapters
  • Review & proof-read existing Documentation
  • Provide additional Templates
  • Contribute eslint rules
  • Write proper end-to-end tests
  • ... and so much more!

Become a contributor!


layout: big-points title: Links Collection


layout: outro preload: false title: Outro twitter: '@Linus_Borg' repository: 'github.com/linusborg' hostedSlides: 'https://github.com/LinusBorg/talk-cross-compatible-vue-components'

You made it! We're done!

Questions?

Slidev Logo

This talk was built with Slidev

https://sli.dev