[JavaScript, VueJS, TypeScript] Из Vue 2 на Vue 3 – Migration Helper
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
ПредысторияБыла у меня курсовая по веб-разработке, делать очередной интернет-магазин как-то не хотелось, и решил я написать помощник миграции из 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] @teqfw/di
- [Информационная безопасность, Криптография, JavaScript, Node.JS, Криптовалюты] Поиск коллизий в SHA-256 на платформе Node.js при помощи Bitcoin Hasher
- [JavaScript, ReactJS] react-router: Три метода рендеринга маршрутов (компонентный, рендеринговый и дочерний) (перевод)
- [Разработка веб-сайтов, TypeScript, Лайфхаки для гиков] Карманная книга по TypeScript. Часть 4. Подробнее о функциях (перевод)
- [JavaScript, Веб-аналитика, Интернет-маркетинг, Повышение конверсии, Поисковая оптимизация] Как настроить Facebook Conversion API с помощью GTM Server Side
- [Работа с видео, JavaScript, Программирование, Видеоконференцсвязь] Streaming multiple RTSP IP cameras on YouTube and/or Facebook
- [JavaScript, Программирование, Карьера в IT-индустрии] Публичное техническое собеседование на мидл фронтенд-разработчика: 15 июня в 19.00
- [Разработка веб-сайтов, Работа с видео, Программирование, Видеоконференцсвязь] Стриминг множества RTSP IP камер на YouTube и/или Facebook
- [Информационная безопасность, IT-эмиграция] Из России выслали представителя RIPE NCC в Восточной Европе из-за обвинений в шпионаже
- Google представил сервис для наглядного отслеживания зависимостей
Теги для поиска: #_javascript, #_vuejs, #_typescript, #_vue, #_vue2, #_vue3, #_migration, #_helper, #_migratsija (миграция), #_pomoschnik (помощник), #_composition, #_kompozitsii (композиции), #_npm, #_javascript, #_vuejs, #_typescript
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:02
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
ПредысторияБыла у меня курсовая по веб-разработке, делать очередной интернет-магазин как-то не хотелось, и решил я написать помощник миграции из 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'); } } 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, }; } }
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 } }, 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) 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) || [] } }
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] – т.е. то же самое, что и в коде export default {
name: 'HelloWorld', data: () => ({ some: 0, another: 0, foo: ['potato'], }), methods: { somePlus() { this.some++; }, anotherPlus() { this.another++; }, }, }; // 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, }; }, }; =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:02
Часовой пояс: UTC + 5