[JavaScript, VueJS, TypeScript] Из Vue 2 на Vue 3 – Migration Helper

Автор Сообщение
news_bot ®

Стаж: 6 лет 3 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
10-Июн-2021 03:34

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

Похожие новости: Теги для поиска: #_javascript, #_vuejs, #_typescript, #_vue, #_vue2, #_vue3, #_migration, #_helper, #_migratsija (миграция), #_pomoschnik (помощник), #_composition, #_kompozitsii (композиции), #_npm, #_javascript, #_vuejs, #_typescript
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 13-Май 00:27
Часовой пояс: UTC + 5