Предыстория
Была у меня курсовая по веб-разработке, делать очередной интернет-магазин как-то не хотелось, и решил я написать помощник миграции из Vue 2 (options-api) в Vue 3 (composition-api) с авторазделением на композиции с помощью алгоритма Косарайю по поиску областей сильной связности.
Для тех, кто не в теме, поясню, так выглядит код с options-api:
export default {
data () {
return {
foo: 0,
bar: 'hello',
}
},
watch: {
...
},
methods: {
log(v) {
console.log(v);
},
},
mounted () {
this.log('Hello');
}
}
и примерно так с composition-api:
export default {
setup (props) {
const foo = reactive(0);
const bar = reactive('hello');
watch(...);
const log = (v) => { console.log(v); };
onMounted(() => { log('hello'); });
return {
foo,
bar,
log,
};
}
}
Автоматическое разделение на композиции
Дабы не отходить от самой идеи композиций, помимо трансляции кода под новый синтаксис composition-api, было принято решение добавить и возможность разделения монолитного компонента на самостоятельные композиции, и их последующее переиспользование в главном компоненте. Как же это сделать?
Сначала зададимся вопросом, что же такое композиции? Для себя я ответил так:
Композиции – это самодостаточная группа блоков кода, отвечающих за один функционал, зависящих только друг от друга. Зависимости тут самое главное!
Блоками кода в нашем случае будем считать: свойства data, методы, вотчеры, хуки, и все то, из чего строится компонент Vue.
Теперь определимся на счёт зависимостей блоков кода между собой. С этим во Vue достаточно просто:
Если computed, method, hook, provide свойство внутри себя использует другие свойства, то оно от них и зависит
Если на свойство навешен вотчер, то вотчер зависит от наблюдаемого им свойства
и так далее :)
data: () => ({
array: ['Hello', 'World'], // block 1
}),
watch: {
array() { // block 2 (watch handler) depends on block 1
console.log('array changed');
},
},
computed: {
arrayCount() { // block 3
return this.array.length; // block 3 depends on block 1
},
},
methods: {
arrayToString() { // block 4
return this.array.join(' '); // block 4 depends on block 1
}
},
Допустим, мы смогли пройтись по коду и выделить все-все зависимости свойств между собой. Как всё это делить на композиции?
А теперь абстрагируемся от Vue, проблемы миграции, синтаксиса и т.д. Оставим только сами свойства и их зависимости друг с другом.
Выделим из этого ориентированный граф, где вершинами будут свойства, а ребрами - зависимости между свойствами. А теперь самое интересное!
Алгоритм Косарайю
Алгоритм поиска областей сильной связности в ориентированном графе. Заключается он в двух проходах в глубину по исходному и транспонированному графам и небольшой магии.
Никогда бы не подумал, что простое переписывание реализации из C на TS может быть таким проблемным :)
Так вот. Применяя данный алгоритм, мы и получим заветные композиции, состоящие из сгруппированных по связям свойств. Если же свойство оказалось одиноким и без пары, мы отнесем его к самому будущему компоненту, если же нет – выделим группу в одну композицию, которую будем переиспользовать.
Поиск зависимостей
Примечание: во всех функциях компонента в options-api свойства доступны через this
Здесь немного грусти, поскольку искать зависимости в .js приходится так:
const splitter = /this.[0-9a-zA-Z]{0,}/
const splitterThis = 'this.'
export const findDepsByString = (
vueExpression: string,
instanceDeps: InstanceDeps
): ConnectionsType | undefined => {
return vueExpression
.match(splitter)
?.map((match) => match.split(splitterThis)[1])
.filter((value) => instanceDeps[value])
.map((value) => value)
Да, просто проходясь регуляркой по строкому представлению функции в поисках всего, что идет после this.
:(
Более продвинутый вариант, но такой же костыльный:
export const findDeps = (
vueExpression: Noop,
instanceDeps: InstanceDeps
): ConnectionsType | undefined => {
const target = {}
const proxy = new Proxy(target, {
// прокси, который записывает в объект вызываемые им свойства
get(target: any, name) {
target[name] = 'get'
return true
},
set(target: any, name) {
target[name] = 'set'
return true
}
})
try {
vueExpression.bind(proxy)() // вызываем функцию в скоупе прокси
return Object.keys(target) || [] // все свойства которые вызвались при this.
} catch (e) { // при ошибке возвращаемся к первому способу
return findDepsByString(vueExpression.toString(), instanceDeps) || []
}
}
При использовании прокси вышло несколько проблем:
не работает с анонимными функциями
при использовании вызывается сама функция – а если вы там пентагон взламываете?
Создание файлов и кода
Вспомним зачем мы тут собрались: миграция.
Используя все вышеописанное, получив разбитые по полочкам свойства, нужно составить новый код в синтаксисе composition-api, то есть собрать строки, которые в конечном счете будут являться содержимыми файлов в проекте.
Для этого надо уметь представлять экземпляры объектов, строк, массивов и всего остального в их естественном, кодовом, виде. Вот эта функция:
const toString = (item: any): string => {
if (Array.isArray(item)) {
// array
const builder: string[] = []
item.forEach((_) => {
builder.push(toString(_)) // wow, it's recursion!
})
return `[${builder.join(',')}]`
}
if (typeof item === 'object' && item !== null) {
// object
const builder: string[] = []
Object.keys(item).forEach((name) => {
builder.push(`${name}: ${toString(item[name])}`) // wow, it's recursion!
})
return `{${builder.join(',')}}`
}
if (typeof item === 'string') {
// string
return `'${item}'`
}
return item // number, float, boolean
}
// Example
console.log(toString([{ foo: { bar: 'hello', baz: 'hello', }}, 1]);
// [{foo:{bar: 'hello',baz: 'hello'}},1] – т.е. то же самое, что и в коде
Про остальной говнокод я тактично промолчу :)
Итоговые строки мы записываем в новые файлы через простой fs.writeFile()
в ноде и получаем результат
Пример работы
Собрав всё это в пакет, протестировав и опубликовав, можно наконец увидеть результат работы.
Ставим пакет vue2-to-3 глобально (иначе не будет работать через консоль) и проверяем!
Пример HelloWorld.js
:
export default {
name: 'HelloWorld',
data: () => ({
some: 0,
another: 0,
foo: ['potato'],
}),
methods: {
somePlus() {
this.some++;
},
anotherPlus() {
this.another++;
},
},
};
Пишем в консоли: migrate ./HelloWorld.js
и получаем на выход 3 файла:
// CompositionSome.js
import { reactive } from 'vue';
export const CompositionSome = () => {
const some = reactive(0);
const somePlus = () => { some++ };
return {
some,
somePlus,
};
};
// CompositionAnother.js
import { reactive } from 'vue';
export const CompositionAnother = () => {
const another = reactive(0);
const anotherPlus = () => { another++ };
return {
another,
anotherPlus,
};
};
// HelloWorld.js
import { reactive } from 'vue';
import { CompositionSome } from './CompositionSome.js'
import { CompositionAnother } from './CompositionAnother.js'
export default {
name: 'HelloWorld',
setup() {
const _CompositionSome = CompositionSome();
const _CompositionAnother = CompositionAnother();
const foo = reactive(['potato']);
return {
foo,
some: _CompositionSome.some,
somePlus: _CompositionSome.somePlus,
another: _CompositionAnother.another,
anotherPlus: _CompositionAnother.anotherPlus,
};
},
};
Итого
На данный момент все это доступно и работает, но ещё есть некоторые баги со строковым представлением не анонимных функций и путями (в некоторых случаях фатально для linux систем)
В планах запилить миграцию для single-file-components
и .ts
файлов (сейчас работает только для .js
)
Спасибо за внимание!
sojey80135
options-api намного лучше смотрится
напиши пожалуйста миграцию с v2 на v3 options-api
ildardev Автор
На сколько я понимаю, options-api в vue3 отличается иной обработкой хуков beforeCreate/created, и мелкими особенностями с реактивностью, отсутствием $set и т.д.?
Изменение названия хуков достаточно просто реализуемо, но вот мелкие особенности vue 3 пока у меня еще не отлавливаются и не обрабатываются
Fragster
Это пока компоненты прям совсем простые. Чуть сложнее — и уже данные с методами мешаются. В composition api можно сгруппировать красиво, как-то так (выше код идет так же, как в возвращаемом объекте):
Fragster
кроме хука beforeRouteEnter