Из Vue 2 на Vue 3 – Migration Helper

от автора

Предыстория

Была у меня курсовая по веб-разработке, делать очередной интернет-магазин как-то не хотелось, и решил я написать помощник миграции из 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)

Спасибо за внимание!

npm, git

ссылка на оригинал статьи https://habr.com/ru/post/562016/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *