Русский
Русский
English
Статистика
Реклама

Из 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}// Exampleconsole.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.jsimport { reactive } from 'vue';export const CompositionSome = () => {  const some = reactive(0);  const somePlus = () => { some++ };  return {    some,    somePlus,  };};// CompositionAnother.jsimport { reactive } from 'vue';export const CompositionAnother = () => {  const another = reactive(0);  const anotherPlus = () => { another++ };  return {    another,    anotherPlus,  };};// HelloWorld.jsimport { 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

Источник: habr.com
К списку статей
Опубликовано: 09.06.2021 20:16:20
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Javascript

Vuejs

Typescript

Vue

Vue2

Vue3

Migration

Helper

Миграция

Помощник

Composition

Композиции

Npm

Категории

Последние комментарии

© 2006-2021, personeltest.ru