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 |
|
Building cross-compatible Components for Vue 2&3 with confidence
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
- Write compatible code that respects breaking changes
- Write tests that can be run against both versions
- Manage conflicting dependencies
- Decide how to bundle and publish the project
This is still under heavy development
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
export default {
props: {
value: String
},
methods: {
handleInput(e) {
this.$emit('input', e.target.value)
}
}
}
::right::
export default {
props: {
modelValue: String
},
emits: ['update: modelValue'],
methods: {
handleInput(e) {
this.$emit('update:modelValue', e.target.value)
}
}
}
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
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
- 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?
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::
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')
})
- 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
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
<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
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
{
"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
# 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
# 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"
}
}
# 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"
}
}
// 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
// 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
https://github.com/vue-bridge/template-monorepo
- 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!
Docs
https://vue-bridge.docs.netlify.app
Repo
https://github.com/vue-bridge/vue-bridge
Template
https://github.com/vue-bridge/template-monorepo
Twitter
https://twitter.com/VueBridge
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?