Спросите помощника
0/400
Вопрос обрабатывается ИИ-сервисом и сохраняется для улучшения ответов. Не вводите персональные данные.

    REEEL Architecture Best Practices

    Правила построения приложений на REEEL. Синтаксис описан в спеке формата ( packages/workspace/reeel_element_parser/docs/reeel_toon_format.md ) — здесь когда что выбирать . Правила визуальной вёрстки — в index.md . Ярусы: MUST — нарушение даёт баг или техдолг; SHOULD — дефолт стиля, отступай осознанно.

    0. Сигилы несут семантику (MUST)

    $name — чтение/подписка на переменную. @name на стороне значения — объявление выходной переменной, в которую пишет элемент. Это работает и для именованных ( data:@products ), и для позиционных аргументов ( MapValue $routePath @tab ). Направление потока данных всегда видно из сигилов — не из имён параметров.

    Поток данных

    1. Данные между экранами — только через маршрут (MUST)

    Идентичность — path-параметром, объект — вторым аргументом navigate , чтение — генератором RouteData . Путь собирается вложенным template string прямо в вызове:

    // main.reeel
    AppRoute "/detail/:id"
    
    // карточка
    ListTile onTap:(){navigator.show($"/detail/${item['id']}", item)}
    
    // detail.reeel
    RouteData id:@productId extra:@product
      Text {product['title'] ?? ""}

    Тем же генератором читается и то, КАКОЙ маршрут открыт, — этим оболочка подсвечивает активный пункт. Работает по обе стороны навигатора: и на экране маршрута, и в @Macros id:shell над ним. Путь статический ( /detail/:id → /detail ), поэтому таблица не зависит от значений параметров:

    @AppNavBar
      RouteData path:@routePath
        MapValue $routePath @tab "/home":0 "/catalog":1
          NavigationBar selectedIndex:$tab

    Переменная, которую файл не объявил, — всегда ошибка: если путь не прочитан RouteData , взяться ему неоткуда.

    Антипаттерн — глобальная Storage-переменная как «передатчик» ( onTap:(){selected_product=index; navigator.show("/detail")} ): скрытая связность, ломает deep link, требует повторного парсинга данных.

    2. extra эфемерален — без фолбэк-инфраструктуры (MUST)

    extra есть только при навигации внутри приложения. Экран читает поля как {product['field'] ?? ""} — на прямом заходе они пустые, и это норма. НЕ добавляй на экран деталей JsonData-фолбэк «достань по id, если extra нет»: экран не должен повторно загружать данные ради edge-кейса.

    3. Списки: данные → данные, не «скрой строку» (MUST)

    Фильтрация и трансформация — генераторами, список получает уже готовые данные:

    FilterData from:$products data:@filtered count:@filteredCount
      where:(item){isEmpty(query) || contains(lowerCase(item['title'] ?? ""), lowerCase(query))}
      ListView.builder itemCount:$filteredCount
        @Macros id:itemBuilder index:0
          $ProductCard item:{filtered[index]}

    Visibility visible:{...} внутри itemBuilder для фильтрации запрещён: ломает счётчики, скролл и состояния «пока пусто».

    4. Каждый сабскрипт на данных — с ?? (MUST)

    Выходы генераторов равны null , пока данные не загрузились; сабскрипты на null деградируют в null , а не падают. Поэтому каноническая форма — {products[index]['title'] ?? ""} ; голый сабскрипт в вёрстке — баг по построению.

    5. Карточке данных — объект целиком (SHOULD)

    $ProductCard item:{products[index]} — поля распаковываются внутри макроса ( {item['title'] ?? ""} ). Не протаскивай по пять параметров через каждый call site. Касается макросов-представлений данных; макросу-кнопке объект не нужен.

    Выражения

    6. Маппинг вместо тернарных цепочек, в обе стороны (SHOULD)

    Тернарник глубже одного уровня — сигнал, что нужен MapValue . Короткая форма — дефолт: первый позиционный — вход, второй — выход, остальные key:value — таблица, несовпавшее падает на ПЕРВУЮ запись:

    MapValue $routePath @tab "/home":0 "/catalog":1 "/favorites":2 "/profile":3

    Обратное направление (индекс → значение в колбэках) — pick() :

    onDestinationSelected:(index){navigator.show(pick(index, "/home", "/catalog", "/favorites", "/profile"))}

    Длинные таблицы выноси в #map на отдельной строке; именованная форма ( value: / to: / default: ) допустима, когда нужна явность — default: перекрывает фолбэк первой записи.

    7. Коллекции в Storage — литералы и иммутабельные операции (MUST)

    Динамические ключи в Storage невозможны — коллекция живёт в одном ключе. Объявляй литералом, мутируй функциями коллекций (каждая возвращает НОВУЮ коллекцию — присваивание нотифицирует подписчиков и персистит):

    Storage @favorites:[]
      IconButton onPressed:{favorites=toggle(favorites, productId)}
        Icon {has(favorites, productId) ? "favorite" : "favorite_border"}

    Скалярные элементы сравниваются по значению ( has / toggle / remove ); списки объектов — по ключу через *Where -варианты:

    Storage @cities:[]
      ListTile onTap:(){cities=add(cities, city)}
      IconButton onPressed:{cities=removeWhere(cities, "name", city['name'])}

    Живая переменная — настоящий List/Map (сабскрипты работают напрямую), в бекинг-хранилище она уходит JSON-строкой автоматически. Литералы: [a, b] — список, [key:value, …] — map, [] / [:] — пустые; литералы — для констант, $ref внутри не разворачивается.

    Тема и локализация

    Цвета — токенами $theme.* , не hex (SHOULD)

    color:$theme.primary , не color:#9B8AFB . Токены ambient (доступны в любом файле без объявления), следуют теме проекта/бренда и переключаются ( theme.set("dark") ). Hex-литерал допустим лишь там, где цвет действительно одноразовый и вне темы.

    Строки — ключами $strings.* , не литералами (SHOULD)

    Text $strings.welcome , аргументы/плюрал — {strings.t("greeting", name:$user)} . Строки живут в data/strings/<locale>.arb , язык переключается strings.set("ru") . Литералы — только для не-текста (символы, эмодзи) или временных заглушек.

    Ambient-чтения прикрывать ?? (MUST)

    Dotted-чтение неизвестного токена/ключа даёт null : {theme.accent ?? "#000000"} , {strings.title ?? "—"} . Не объявляй свои @theme / @strings — затенят ambient. Подробнее: Тема и локализация .

    Каркас приложения

    8. Роут = файл; пути с : — в кавычках (MUST)

    AppRoute "/detail/:id" резолвится в detail.reeel по статическим сегментам. Без кавычек : парсится как разделитель именованного параметра.

    9. Двойной Scaffold с фиксированными ролями (SHOULD)

    Внешний Scaffold — в shell-макросе AppMainBuilder : только персистентный chrome ( bottomNavigationBar , фон), не пересоздаётся при переходах. Внутренний — на каждом экране: его собственный AppBar . AppBar в shell не поднимать — пришлось бы реактивно перестраивать его по маршруту. backgroundColor дублируется на обоих уровнях осознанно: внутренний Scaffold без явного цвета закрасит фон темой Material.

    10. В шаблонах и примерах нет мёртвого UI (MUST для шаблонов)

    Каждый контрол делает что-то наблюдаемое. Декоративный «выбор темы» без эффекта хуже отсутствия контрола: шаблон — учебник, мёртвый контрол учит неправильному.

    11. Ссылка на файл — от корня проекта (MUST)

    // ✗ относительно файла — работает в редакторе, ломается после публикации
    Image src:"../shared/logo.png"
    
    // ✓ от корня проекта
    Image src:"data/images/logo.png"

    Путь от корня — единственная форма, о которой договорились все, кто его читает: загрузчик приложения, компилятор сайта, статический экспорт и манифест ревизии. «Текущий файл» такой точкой отсчёта быть не может: раскрытие макроса переносит разметку между файлами, и на момент резолва это уже не тот файл, где строка написана.

    Отдельно про .. : он не просто не рекомендуется — он не синхронизируется . И клиент, и сервер отклоняют путь с dot-сегментом, так что такая ссылка называет файл, которого в опубликованном проекте быть не может. При этом в редакторе он открывается с диска, поэтому ошибку видно только после публикации — и поэтому анализатор сообщает о ней как об error .

    Картинки живут в data/images/ (вложенность любая). Что уже есть в проекте, показывает reeel_assets — вместе с размерами и весом.

    12. Файл длиннее 250 строк или 12 КБ — делится на компоненты (SHOULD)

    Что наступит раньше. Самостоятельные куски — секция экрана, бит ролика — уходят в reeel/components/<name>.reeel как @ -макросы с import там, где используются; внешние переменные, которые кусок читает, передаются параметрами. Причина практическая: файл такого размера не переписать целиком в один ответ агента вместе с рассуждением, а каждая правка в нём — поиск по чужому коду. Анализатор предупреждает file_too_long ; исправление ( reeel_diagnostics {fix:true, file:"…"} — без file файл не режется) или reeel_extract_component выносят кусок, расставляют импорт и сообщают, что куда переехало.

    @Name на верхнем уровне файла объявляет макрос; @Macros index:0 — слот шаблона одного элемента *Repeat / itemBuilder , а не объявление. @Macros Ball … — ошибка ( macro_slot_as_declaration ), правильно @Ball … .

    Сцена motion держит в корне часы, *Layer in:/out: , звук и субтитры; каждый бит — свой макрос. JsonData — РОДИТЕЛЬ *Scene , не сосед: два корня превью показывает галереей.

    menu_baseline