Как была разработана библиотека компонентов, рекомендованная Ю Даду?

внешний интерфейс Vue.js
Как была разработана библиотека компонентов, рекомендованная Ю Даду?

Эта статья участвовала в "Проект «Звезда раскопок»”, чтобы выиграть творческий подарочный пакет и бросить вызов творческим поощрительным деньгам.

Примечание. Чтобы сохранить пространство как можно более кратким, когда я вставляю исходный код в некоторых местах, я пытаюсь вставить исходный код, который лучше всего отражает содержание, которое необходимо объяснить, а другие повторяющиеся коды опущены, поэтому, если вы попытаетесь чтобы прочитать исходный код самостоятельно, вы можете найти несоответствия с кодом в статье. Репозиторий исходного кода, используемый в статье для запуска через Naive UI:GitHub.com/PF Том/наивный…

лаконичная абстракция

Front-end разработчики теперь почти неотделимы от библиотек компонентов пользовательского интерфейса, таких как Ant Design, Material Design и Naive UI, который недавно появился в экосистеме Vue.Библиотека компонентов предоставляет простую, гибкую и удобную форму. использования, например страницы. Наиболее распространенные способы использования кнопок:

<template>

  <n-button>Default</n-button>

  <n-button type="primary">Default</n-button>

  <n-button type="info" dashed>Default</n-button>

  <n-button type="success" dashed size="small">Default</n-button>

  <n-button text>Default</n-button>

  <n-button text tag="a" href="https://anyway.fm/news.php" type="warning"

    >安妮薇时报</n-button

  >

  <n-button disabled> 不许点 </n-button>

</template>

Приведенные выше несколько простых строк кода могут привести к следующим интересным эффектам:

Даже вы можете переключать скины одним щелчком мыши, например темный режим:

Конечно, мы также можем обрабатывать события, добавлять значки, обрабатывать загрузку и т. д. Просто задав некоторые реквизиты, мы можем получить красивую и практичную кнопку, которая действительно отличается от исходных тегов HTML...

теория айсберга

Несмотря на то, что библиотека компонентов обеспечивает гибкость и удобство, ее внутренние принципы не так просты, как ее использование, и столь же наводят на размышления, как приведенная выше диаграмма айсберга.

Давайте взглянем на CHANGELOG Naive UI, недавнего новичка в библиотеке компонентов Vue, чтобы получить представление о том, сколько времени уходит на написание вводной библиотеки компонентов:

Как видите, он выйдет 21 марта 2020 г.1.xверсии, в то время как в1.xЭто был долгий период обдумывания, проектирования и разработки, а должен был длиться более двух лет.

Чтобы запустить наивную кнопку пользовательского интерфейса, требуются примерно следующие файлы или коды:

.

|_____utils

| |____color

| | |____index.js

| |____vue

| | |____index.js

| | |____flatten.js

| | |____call.js

| | |____get-slot.js

| |____index.js

| |____naive

| | |____warn.js

| | |____index.js

| |____cssr

| | |____create-key.js

| | |____index.js

|_____internal

| |____loading

| | |____index.js

| | |____src

| | | |____Loading.jsx

| | | |____styles

| | | | |____index.cssr.js

| |____index.js

| |____icon-switch-transition

| | |____index.js

| | |____src

| | | |____IconSwitchTransition.jsx

| |____fade-in-expand-transition

| | |____index.js

| | |____src

| | | |____FadeInExpandTransition.jsx

| |____wave

| | |____index.js

| | |____src

| | | |____Wave.jsx

| | | |____styles

| | | | |____index.cssr.js

| |____icon

| | |____index.js

| | |____src

| | | |____Icon.jsx

| | | |____styles

| | | | |____index.cssr.js

|_____styles

| |____common

| | |_____common.js

| | |____light.js

| | |____index.js

| |____transitions

| | |____fade-in-width-expand.cssr.js

| | |____icon-switch.cssr.js

| |____global

| | |____index.cssr.js

|____config-provider

| |____src

| | |____ConfigProvider.js

|____button

| |____styles

| | |_____common.js

| | |____light.js

| | |____index.js

| |____src

| | |____Button.jsx

| | |____styles

| | | |____button.cssr.js

|____assets

| |____logo.png

|_____mixins

| |____use-style.js

| |____use-theme.js

| |____index.js

| |____use-form-item.js

| |____use-config.js

За, казалось бы, трудным

Несмотря на прохождение, казалось бы, простого<Button />Требуется много работы за кулисами, включая десятки файловых зависимостей, но для библиотеки компонентов сложность представляет собой приближение на порядок, т.е. от простого<Button />к комплексу<Form />, вообще-то в области библиотек компонентов 90% контента похоже, так что если вы понимаете<Button />В принципе можно сказать, что вы поняли почти 90% содержимого библиотеки компонентов, а оставшиеся 10% — это конкретная реализация конкретных компонентов.

Таким образом, чтобы понять ядро ​​библиотеки интерфейсных компонентов, вам все равно нужно понимать<Button />Разнообразная подготовительная работа, необходимая для прогона, который является первым высоким столбцом на приведенном выше рисунке, и разработки библиотеки компонентов должна быть сначала сосредоточена на проектировании, чтобы позволить по крайней мере одинButtonПлан бега.

Технологическая цепочка, стоящая за Button

мы начинаем сNaive UI Для объекта исследования детально проанализировать его<Button />Есть две интуитивные причины для различных принципов реализации:

  1. В его технологическом стеке преобладают Vite, Vue3 и TypeScript, что соответствует последнему технологическому стеку автора.
  2. По сравнению с другими библиотеками компонентов, она находится на относительно скомпрометированном уровне с точки зрения зрелости, популярности и совершенства кода, не слишком сложна, но требует относительно большего объема знаний и больше подходит для изучения и исследования ее принципов.

из шаблона

Если вы хотите понять компонент, конечно, в первую очередь нужно понять его скелет, что мы часто говорим о HTML/JSX.Во-первых, давайте взглянем на шаблон компонента Button наивного пользовательского интерфейса:

const Button = defineComponent({

  name: 'Button',

  props: {},

  setup(props) {},

  render() {

    // 第一部分

    // n

    const { $slots, mergedClsPrefix, tag: Component } = this;

    const children = flatten(getSlot(this));



    return (

      <Component 

        ref="selfRef"

        // 第二部分

        class={[

          `${mergedClsPrefix}-button`,

          `${mergedClsPrefix}-button--${this.type}-type`,

          {

            [`${mergedClsPrefix}-button--disabled`]: this.disabled,

            [`${mergedClsPrefix}-button--block`]: this.block,

            [`${mergedClsPrefix}-button--pressed`]: this.enterPressed,

            [`${mergedClsPrefix}-button--dashed`]: !this.text && this.dashed,

            [`${mergedClsPrefix}-button--color`]: this.color,

            [`${mergedClsPrefix}-button--ghost`]: this.ghost, // required for button group border collapse

          },

        ]}

        tabindex={this.mergedFocusable ? 0 : -1}

        type={this.attrType}

        style={this.cssVars}

        disabled={this.disabled}

        onClick={this.handleClick}

        onBlur={this.handleBlur}

        onMousedown={this.handleMouseDown}

        onKeyup={this.handleKeyUp}

        onKeydown={this.handleKeyDown}

        >

        // 第三部分

        {$slots.default && this.iconPlacement === "right" ? (

          <div class={`${mergedClsPrefix}-button__content`}>{children}</div>

        ) : null}

        // 第四部分

        <NFadeInExpandTransition width>

          {{

            default: () =>

              $slots.icon || this.loading ? (

                <span

                  class={`${mergedClsPrefix}-button__icon`}

                  style={{

                    margin: !$slots.default ? 0 : "",

                  }}

                >

                  <NIconSwitchTransition>

                    {{

                      default: () =>

                        this.loading ? (

                          <NBaseLoading

                            clsPrefix={mergedClsPrefix}

                            key="loading"

                            class={`${mergedClsPrefix}-icon-slot`}

                            strokeWidth={20}

                          />

                        ) : (

                          <div

                            key="icon"

                            class={`${mergedClsPrefix}-icon-slot`}

                            role="none"

                          >

                            {renderSlot($slots, "icon")}

                          </div>

                        ),

                    }}

                  </NIconSwitchTransition>

                </span>

              ) : null,

          }}

        </NFadeInExpandTransition>

        // 第三部分

        {$slots.default && this.iconPlacement === "left" ? (

          <span class={`${mergedClsPrefix}-button__content`}>{children}</span>

        ) : null}

        // 第五部分

        {!this.text ? (

          <NBaseWave ref="waveRef" clsPrefix={mergedClsPrefix} />

        ) : null}

        // 第六部分

        {this.showBorder ? (

          <div

            aria-hidden

            class={`${mergedClsPrefix}-button__border`}

            style={this.customColorCssVars}

          />

        ) : null}

        // 第六部分

        {this.showBorder ? (

          <div

            aria-hidden

            class={`${mergedClsPrefix}-button__state-border`}

            style={this.customColorCssVars}

          />

        ) : null}

    </Component>

    )

  }

});

Видно, что упомянутый выше основной дисплей<Button />Шаблонная часть компонента на основе Vue3defineComponentопределять компоненты на основеrenderМетод использует форму JSX для написания шаблона, и часть шаблона в основном разделена на 6 частей, которые отмечены в коде комментариями:

  1. В основном связанные с атрибутами, есть три основных атрибута:$slots,mergedClsPrefix,tag$slotsВ поле Vue объект, которому принадлежит дочерний узел,mergedClsPrefixЭто префикс пространства имен всей библиотеки компонентов. В простом пользовательском интерфейсе этот префиксn,tagУказывает, с какой меткой должен отображаться этот компонент, по умолчанию<button />, вы также можете заменить его на<a />, сделать кнопку похожей на ссылку

  1. В основном определяютButtonСвязанные свойства:

    1. вclassЗатем по входящим атрибутам определить, какой принадлежитtype:primary,info,warning,success,errorИ какое государство в настоящее время находится в:disabled,block,pressed,dashed,color,ghost, согласно этимtypeи укажите соответствующие имена классов для определения стилей CSS, которым принадлежат соответствующие имена классов для компонента.
    2. tabIndexозначает использованиеtabключ, будет ли выбрана эта кнопка,0указывает, что его можно выбрать,-1Указывает, что его нельзя выбрать;
    3. typeвыражается какbutton,submit,resetи другие типы кнопок, так что кнопки могут быть интегрированы в<Form />компоненты для выполнения более сложных операций, таких как инициирование отправки форм;
    4. styleЭто передача необходимых переменных CSS для этого компонента, то есть переменных CSS, и вsetupфункция, она пройдетuseTheme(будет рассмотрено позже) крючок для крепленияButtonСвязанные стили, которые широко используют переменные CSS для настройки различных свойств CSS компонентов, а также для управления глобальным переключением тем, например темным режимом и т. д.
    5. disabledОн должен контролировать, работает ли эта кнопка,trueДелегат отключен, неработоспособен,falseПредставляет работоспособный по умолчанию
    6. Остальные являются соответствующими обработчиками событий:click,blur,mouseup,keyup,keydownЖдать

  1. в основном решаютiconPlacementдляleft,right, форма отображения дочерних узлов компонента, то есть, когда значок находится слева и справа, дочерние узлы распределяются как<span />или<div />Отображается в виде метки, когдаright, установлен в<div />Это для лучшей обработки макета и позиционирования

  1. Для контента, связанного со значками,NFadeInExpandTransitionЧтобы управлять анимацией перехода появления и исчезновения значка,NIconSwitchTransitionконтрольloadingПереключение анимации перехода для иконок формы и других иконок

  1. когда кнопка не начинается сtextКогда узел отображается в виде узла, должна быть пульсация для обработки обратной связи по нему.В приведенном выше видео вы также можете видеть, что будет соответствующий эффект пульсации, когда вы нажимаете кнопку, чтобы дать обратную связь по щелчку.Нет Волновой эффект

  1. главным образом через<div />Чтобы смоделировать границу компонента:borderа такжеstate-border, первое в основном статично, по умолчанию обрабатывается цвет границы, ширина и т. д., второе обрабатывается в разных состояниях:focus,hover,active,pressedждатьborderстиль

Вы можете увидеть оба из них в действии на практическом примере:

.n-button .n-button__border {

    border: var(--border);

}



.n-button .n-button__border, .n-button .n-button__state-border {

    position: absolute;

    left: 0;

    top: 0;

    right: 0;

    bottom: 0;

    border-radius: inherit;

    transition: border-color .3s var(--bezier);

    pointer-events: none;

}



.n-button:not(.n-button--disabled):hover .n-button__state-border {

    border: var(--border-hover);

}



.n-button:not(.n-button--disabled):pressed .n-button__state-border {

    border: var(--border-pressed);

}



style attribute {

    --bezier: ;

    --bezier-ease-out: ;

    --border: 1px  ;

    --border-hover: 1px  ;

    --border-pressed: 1px  ;

    --border-focus: 1px  ;

    --border-disabled: 1px  ;

}

можно увидетьstate-borderВ основном для работы с некоторыми эффектами, которые будут динамически изменяться, например, вhover,pressedЭффект отображения границы в других состояниях иborderОн отвечает за начальный эффект по умолчанию.

Поняв содержание, относящееся к основному шаблону, вы можете выразить сомнения относительно содержания, которое чаще всего появляется при объяснении всего шаблона, а именно:

  • ${mergedClsPrefix}-button
  • ${mergedClsPrefix}-button--${``this``.type}-type
  • ${mergedClsPrefix}-button__content
  • ${mergedClsPrefix}-button--disabled

Почему существует такой странный способ написания классов CSS? и при присвоении свойств компонентам:

  • style``={``this``.cssVars}

Типичный пример:

const cssVars = {

  // default type

  color: "#0000",

  colorHover: "#0000",

  colorPressed: "#0000",

  colorFocus: "#0000",

  colorDisabled: "#0000",

  textColor: textColor2,

}

Зачем вам нужно назначать кучу переменных CSS?

Если вы озадачены этими вопросами и хотите изучить их обоснование, то вам следует вздохнуть с облегчением, а затем сосредоточиться и продолжить чтение следующей части статьи: искусство организации стилей.

искусство организации

В области библиотек компонентов большая часть времени уходит на то, чтобы лучше и больше настроить организацию общей системы стилей.

Фреймворк Naive UI имеет интересную особенность: он не использует языки стилей предварительной и постобработки, такие как Less/Sass/PostCSS и т. д., а строит свой собственный CSS-подобный CSS с функциями SSR, которые встроены для фреймворка и к фреймворку отношения не имеет.Решение на JS:css-render, и разработал систему подключаемых модулей для этого решения. В настоящее время существует два основных подключаемых модуля:

Эта статья в основном посвящена объяснению КСО, поэтому я сосредоточусь только наplugin-bemсвязанный контент.

Текущий базовый сценарий использования css-render — это коллокация.plugin-bemИспользуйте плагины для написания легко организуемого CSS в стиле БЭМ в коде, подобном JS.Что касается того, почему это «классовое» решение CSS в JS, я объясню позже.

После устанавливаем соответствующий пакет:

$ npm install --save-dev css-render @css-render/plugin-bem

Его можно использовать следующим образом:

import CssRender from 'css-render'

import bem from '@css-render/plugin-bem'



const cssr = CssRender()

const plugin = bem({

  blockPrefix: '.ggl-'

})

cssr.use(plugin) // 为 cssr 注册 bem 插件



const { cB, cE, cM } = plugin

const style = cB(

  'container',

  [

    cE(

      'left, right', 

      {

        width: '50%'

      }

    ),

    cM(

      'dark', 

      [

        cE(

          'left, right',

          {

            backgroundColor: 'black'

          }

        )

      ]

    )

  ]

)



// 查看渲染的 CSS 样式字符串

console.log(style.render())



// 将样式挂载到 head 标签里面,可以提供 options

style.mount(/* options */)



// 删除挂载的样式

style.unmount(/* options */)

Эффект приведенного выше журнала заключается в следующем:

.ggl-container .ggl-container__left, 

.ggl-container .ggl-container__right {

  width: 50%;

}



.ggl-container.ggl-container--dark .ggl-container__left, 

.ggl-container.ggl-container--dark .ggl-container__right{

  background-color: black;

}

Вы можете видеть, что приведенный выше код в основном используетсяcB,cE,cMФункция для выполнения вложенной комбинации различных меток и стилей для достижения эффекта определения стандартных классов CSS и соответствующих стилей.Для дальнейшего объяснения функции этой библиотеки и эффекта, которого она достигает в наивном пользовательском интерфейсе, нам нужно сначала понять, что это БЭМ.

Что такое БЭМ?

B (блок), E (элемент), M (модификатор), а именно блоки, элементы и модификаторы, являются широко используемым соглашением об именах для классов, используемых в HTML/CSS:

 /* 块 */

.btn {}



 /* 依赖块的元素 */ 

.btn__price {}

.btn__text {}



 /* 修改块状态的修饰符 */

.btn--orange {} 

.btn--big {}
  • Вышеупомянутый средний блок (Блок), а именноbtn, представляет собой абстрактный новый компонент верхнего уровня, то есть блок не может содержать блок, а также рассматривается как родительский узел в дереве, используя.btnвыражать
  • Элемент, то естьprice,text, что означает, что он принадлежит блоку, является дочерним элементом этого блока, следует за блоком и отделяется двойным подчеркиванием..btn__price,.btn__textвыражать
  • Модификатор, т.е.orange,big, используется для изменения состояния блока, добавления определенной темы или стиля в блок, следования за блоком, разделенных двойным дефисом, использования.btn--orange,.btn--bigвыражать

Приведенная выше форма CSS отражается в HTML, и получается следующая структура:

<a class="btn btn--big btn--orange" href="">

  <span class="btn__price">¥9.99</span>

  <span class="btn__text">订购</span>

</a>

В основном есть несколько преимуществ использования этого стиля именования классов в стиле БЭМ:

  1. Он может представлять почти все элементы и их подчиненные отношения, причем связь ясна и семантика понятна.
  2. И даже если разработчики в других областях, таких как разработка на стороне клиента или дизайнеры, не понимают язык CSS, они могут узнать об элементах, их иерархических отношениях и состояниях из этого стиля именования.
  3. После создания аналогичной структуры именования вам нужно всего лишь изменить несколько имен классов, чтобы получить элементы разных стилей, такие как кнопки:
/* Block */

.btn {

  text-decoration: none;

  background-color: white;

  color: #888;

  border-radius: 5px;

  display: inline-block;

  margin: 10px;

  font-size: 18px;

  text-transform: uppercase;

  font-weight: 600;

  padding: 10px 5px;

}



/* Element */

.btn__price {

  background-color: white;

  color: #fff;

  padding-right: 12px;

  padding-left: 12px;

  margin-right: -10px; /* realign button text padding */

  font-weight: 600;

  background-color: #333;

  opacity: .4;

  border-radius: 5px 0 0 5px;

}



/* Element */

.btn__text {

  padding: 0 10px;

  border-radius: 0 5px 5px 0;

}



/* Modifier */

.btn--big {

  font-size: 28px;

  padding: 10px;

  font-weight: 400;

}



/* Modifier */

.btn--blue {

  border-color: #0074D9;

  color: white;

  background-color: #0074D9;

}



/* Modifier */

.btn--orange {

  border-color: #FF4136;

  color: white;

  background-color: #FF4136;

}



/* Modifier */

.btn--green {

  border-color: #3D9970;

  color: white;

  background-color: #3D9970;

}





body {

  font-family: "fira-sans-2", sans-serif;

  background-color: #ccc;

}

Вышеупомянутое нужно только изменить модификаторorange,green,blue,bigи так далее, можно получить разные эффекты:

Как работает CSS-рендеринг?

CSS Render — это, по сути, генератор CSS, который затем обеспечиваетmountа такжеunmountAPI для загрузки сгенерированной строки CSS в шаблон HTML и удаления тега стиля CSS из HTML.Реализует схему Sass/Less/CSS-in-JS с помощью плагина соглашения об именах БЭМ и переменных CSS.Может уменьшить повторяющаяся логика и размер пакета общего CSS.

После понимания BEM и приведенного выше введения в CSS Render давайте рассмотрим следующий код:

import CssRender from 'css-render'

import bem from '@css-render/plugin-bem'



const cssr = CssRender()

const plugin = bem({

  blockPrefix: '.ggl-'

})

cssr.use(plugin) // 为 cssr 注册 bem 插件



const { cB, cE, cM } = plugin

const style = cB(

  'container',

  [

    cE(

      'left, right', 

      {

        width: '50%'

      }

    ),

    cM(

      'dark', 

      [

        cE(

          'left, right',

          {

            backgroundColor: 'black'

          }

        )

      ]

    )

  ]

)



// 查看渲染的 CSS 样式字符串

console.log(style.render())



// 将样式挂载到 head 标签里面,可以提供 options

style.mount(/* options */)



// 删除挂载的样式

style.unmount(/* options */)

Приведенный выше код в основном выполняет следующую работу:

  1. Инициализируйте экземпляр CSS Render, затем инициализируйте экземпляр подключаемого модуля BEM и добавьте общий класс стиля..ggl-приставка

  2. Экспорт из плагина БЭМcB,cE,cMметод, а затем следуйте концепции БЭМ, чтобы упорядочить, вложить и объединить классы стилей на основе этих трех методов, чтобы сформировать наш окончательный класс стиля и соответствующий стиль

    1. прежде всегоcB, который определяет блочный элемент верхнего уровня какcontainer
    2. Тогда этот блок содержит два дочерних элемента, которыеcE, представляющий дочерние элементы, подчиненные родительскому блокуleftа такжеright, что соответствует примерноwidthстиль; иcM, модификатор, изменяющий родительский блокdark
    3. darkМодификатор, в свою очередь, содержит дочерний элемент, принадлежащийcE, что означает подчинение модифицируемому этим модификатором блоку, включая дочерние элементыleftа такжеright, что соответствует примерноbackgroundColorстиль

Поняв вышеупомянутые отношения иерархической вложенности, мы можем написать приведенное вышеstyleпровестиrenderПосле воздействия:

// .ggl- 前缀,以及 cB('container', [cE('left, right', { width: '50%' } )])

.ggl-container .ggl-container__left, 

.ggl-container .ggl-container__right {

  width: 50%;

}



// .ggl- 前缀,以及 cB('container', [cM('dark', [cE('left, right', { backgroundColor: 'black' } )])])

.ggl-container.ggl-container--dark .ggl-container--left, 

.ggl-container.ggl-container--dark .ggl-container__right {

 background-color: black;

}

можно увидетьcMОпределенный модификатор фактически является блоком прямой модификации, то есть при генерации класса он будет.ggl-container.ggl-container--darkОн записывается непосредственно с классом родительского блока, который принадлежит отношению модификации, а не подчиненному отношению.

Наивная организация стиля пользовательского интерфейса

Наивный пользовательский интерфейс в основном следует следующей логике с точки зрения организации стилей, по-прежнему взяв в качестве примера кнопку:

  • Подключить переменные CSS, где есть переменные по умолчанию и пользовательские переменные,cssVarsпередано на лейблstyleполе для крепления
  • Смонтируйте базовые стили, связанные с кнопками, стили, связанные с темой, и создайте имена классов CSS.
  • Смонтируйте глобальный стиль по умолчанию (этот шаг находится в конце, убедитесь, что глобальный стиль по умолчанию не будет перезаписан)

Выполнив указанные выше три шага, вы можете определить все классы и стили, связанные с кнопками, а также поддерживать настройку темы, перезагрузку темы и другие функции с помощью переменных CSS.

Вышеупомянутые три шага в основном вsetupвызов внутри функцииuseThemeХук, который обрабатывает монтирование стиля, связанного с кнопкой, и монтирование глобального стиля по умолчанию, а затем обрабатывает определение и использование переменных CSS:

const Button = defineComponent({

  name: "Button",

  props: buttonProps,

  setup(props) {

    const themeRef = useTheme(

      "Button",

      "Button",

      style,

      buttonLight,

      props,

      mergedClsPrefixRef

    );

    

    return {

      // 定义边框颜色相关

      customColorCssVars: computed(() => {}),

      // 定义 字体、边框、颜色、大小相关

      cssVars: computed(() => {}),

    }

  }

  render() {

    // 定义 button 相关的 CSS 变量

    <Component style={this.cssVars}>

      // 定义边框颜色独有的 CSS 变量

      <div class={`${mergedClsPrefix}-button__border`} style={this. customColorCssVars} />

      <div class={`${mergedClsPrefix}-button__state-border`} style={this. customColorCssVars} />

    </Component>

  }

});

Стили, связанные с горой Баттн

Монтаж, связанные с кнопкой, и содержание, связанное со содержанием монтажа в глобальном стилеButtonкомпонентsetupв методеuseThemeкрючки,useThemeфункция ловушки со следующей структурой:

/* 全局 CSS Variables 的类型 */

type ThemeCommonVars = typeof { primaryHover: '#36ad6a', errorHover: '#de576d', ... }



// Button 独有的 CSS Variable 类型

type ButtonThemeVars = ThemeCommonVars & { /*  Button 相关的 CSS Variables 的类型 */ }



// Theme 的类型

interface Theme<N, T = {}, R = any> {

  // 主题名

  name: N

  // 主题一些通用的 CSS Variables

  common?: ThemeCommonVars

  // 相关依赖组件的一些 CSS Variables,如 Form 里面依赖 Button,对应的 Button

  // 需要包含的 CSS Variables 要有限制

  peers?: R

  // 主题自身的一些个性化的 CSS Variables

  self?: (vars: ThemeCommonVars) => T

}



// Button Theme 的类型

type ButtonTheme = Theme<'Button', ButtonThemeVars >



interface GlobalThemeWithoutCommon {

  Button?: ButtonTheme

  Icon?: IconTheme

}



// useTheme 方法传入 props 的类型

type UseThemeProps<T> = Readonly<{

  // 主题相关变量,如 darkTheme

  theme?: T | undefined

  // 主题中可以被重载的变量

  themeOverrides?: ExtractThemeOverrides<T>

  // 内建主题中可以被重载的变量

  builtinThemeOverrides?: ExtractThemeOverrides<T>

}>



// 最终合并的 Theme 的类型

type MergedTheme<T> = T extends Theme<unknown, infer V, infer W>

  ? {

      common: ThemeCommonVars

      self: V

       // 相关依赖组件的一些 CSS Variables,如 Form 里面依赖 Button,对应的 Button

      // 需要包含的 CSS Variables 要有限制

      peers: W

       // 相关依赖组件的一些 CSS Variables,如 Form 里面依赖 Button,对应的 Button

      // 需要包含的 CSS Variables 要有限制,这些 CSS Variables 中可以被重载的变量

      peerOverrides: ExtractMergedPeerOverrides<T>

    }

  : T



useTheme<N, T, R>(

  resolveId: keyof GlobalThemeWithoutCommon,

  mountId: string,

  style: CNode | undefined,

  defaultTheme: Theme<N, T, R>,

  props: UseThemeProps<Theme<N, T, R>>,

  // n

  clsPrefixRef?: Ref<string | undefined>

) => ComputedRef<MergedTheme<Theme<N, T, R>>>

можно увидеть,useThemeОн в основном получает 6 параметров:

  • resolveIdПара "ключ-значение" для таргетинга в теме глобального стиля, здесь'Button'
  • mountIdСтили монтируются наheadПри маркировке,styleизid
  • styleМетка стиля и строка стиля, сгенерированные формой CSS Render компонента, то есть соответствующий скелет класса, класса и стиля, связанного с кнопкой, который содержит ряд переменных CSS, которые необходимо использовать.
  • defaultThemeдляButtonПеременные CSS по умолчанию, связанные с темой
  • propsПри использовании компонента для пользователя входящие свойства можно настроить так, чтобы они переопределяли переменные стиля по умолчанию.
  • clsPrefixRefПрефикс для общего класса стиля, в наивном пользовательском интерфейсе этоn

useThemeВозвращает коллекцию стилей, которая объединяет встроенные стили, глобально определенные стили, связанные с кнопкой, и пользовательские стили.ComputedRef<MergedTheme<Theme<N, T, R>>>.

понялuseThemeПосле ввода и вывода функции-хука можно продолжить рассмотрение основной логики ее функции:

function useTheme(

  resolveId,

  mountId,

  style,

  defaultTheme,

  props,

  clsPrefixRef

) {

  if (style) {

    const mountStyle = () => {

      const clsPrefix = clsPrefixRef?.value;

      style.mount({

        id: clsPrefix === undefined ? mountId : clsPrefix + mountId,

        head: true,

        props: {

          bPrefix: clsPrefix ? `.${clsPrefix}-` : undefined,

        },

      });

      globalStyle.mount({

        id: "naive-ui/global",

        head: true,

      });

    };



    onBeforeMount(mountStyle);

  }



  const NConfigProvider = inject(configProviderInjectionKey, null);

  const mergedThemeRef = computed(() => {

    const {

      theme: { common: selfCommon, self, peers = {} } = {},

      themeOverrides: selfOverrides = {},

      builtinThemeOverrides: builtinOverrides = {},

    } = props;

    const { common: selfCommonOverrides, peers: peersOverrides } =

      selfOverrides;

    const {

      common: globalCommon = undefined,

    

        common: globalSelfCommon = undefined,

        self: globalSelf = undefined,

        peers: globalPeers = {},

      } = {},

    } = NConfigProvider?.mergedThemeRef.value || {};

    const {

      common: globalCommonOverrides = undefined,

     = {},

    } = NConfigProvider?.mergedThemeOverridesRef.value || {};

    const {

      common: globalSelfCommonOverrides,

      peers: globalPeersOverrides = {},

    } = globalSelfOverrides;

    const mergedCommon = merge(

      {},

      selfCommon || globalSelfCommon || globalCommon || defaultTheme.common,

      globalCommonOverrides,

      globalSelfCommonOverrides,

      selfCommonOverrides

    );



    const mergedSelf = merge(

      // {}, executed every time, no need for empty obj

      (self || globalSelf || defaultTheme.self)?.(mergedCommon),

      builtinOverrides,

      globalSelfOverrides,

      selfOverrides

    );

    return {

      common: mergedCommon,

      self: mergedSelf,

      peers: merge({}, defaultTheme.peers, globalPeers, peers),

      peerOverrides: merge({}, globalPeersOverrides, peersOverrides),

    };

  });



  return mergedThemeRef;

}

можно увидетьuseThemeОсновная логика состоит из двух частей:

  • Первая часть - монтироватьbuttonсвязанные стили сclsPrefix + mountId,ВключатьbuttonСкелет класса связанного стиля и смонтировать глобальный общий стиль вnaive-ui/global, а процесс монтирования этого стиля находится вonBeforeMountКогда вызывается хук, его можно уточнить в соответствии с порядком монтирования стиля, описанным ранее:

    • ПорядокsetupВернуть переменные CSS внутри, а затем пройдите меткуstyleРегистрация переменных CSS
    • Затем смонтируйте скелет стиля, связанный с кнопкой
    • Затем смонтируйте скелет глобального общего стиля, чтобы убедиться, что скелет стиля, связанный с кнопкой, не переопределяет глобальный общий стиль.
  • Вторая часть создает новый набор переменных темы для интеграции тем, определяемых пользователем, и тем, настроенных внутри.

    • Пользовательские темыprops:Включатьtheme,themeOverrides,builtinThemeOverrides
    • Тема внутренней конфигурацииNConfigProvider?.mergedThemeRef.valueа такжеNConfigProvider?.mergedThemeOverridesRef.value

Далее мы сосредоточимся на объяснении значения конкретных кодов и связанных с ними переменных этих двух частей.

в первой частиbuttonСоответствующие стили следующие:

import { c, cB, cE, cM, cNotM } from "../../../_utils/cssr";

import fadeInWidthExpandTransition from "../../../_styles/transitions/fade-in-width-expand.cssr";

import iconSwitchTransition from "../../../_styles/transitions/icon-switch.cssr";



export default c([

  cB(

    "button",

    `

    font-weight: var(--font-weight);

    line-height: 1;

    font-family: inherit;

    padding: var(--padding);

    // .... 更多的定义

    `, [

    // border ,边框相关的样式类骨架

      cM("color", [

        cE("border", {

          borderColor: "var(--border-color)",

        }),

        cM("disabled", [

          cE("border", {

            borderColor: "var(--border-color-disabled)",

          }),

        ]),

        cNotM("disabled", [

          c("&:focus", [

            cE("state-border", {

              borderColor: "var(--border-color-focus)",

            }),

          ]),

          c("&:hover", [

            cE("state-border", {

              borderColor: "var(--border-color-hover)",

            }),

          ]),

          c("&:active", [

            cE("state-border", {

              borderColor: "var(--border-color-pressed)",

            }),

          ]),

          cM("pressed", [

            cE("state-border", {

              borderColor: "var(--border-color-pressed)",

            }),

          ]),

        ]),

      ]),

      // icon 相关的样式类骨架

      cE(

        "icon",

        `

      margin: var(--icon-margin);

      margin-left: 0;

      height: var(--icon-size);

      width: var(--icon-size);

      max-width: var(--icon-size);

      font-size: var(--icon-size);

      position: relative;

      flex-shrink: 0;

    `,

        [

          cB(

            "icon-slot",

            `

        height: var(--icon-size);

        width: var(--icon-size);

        position: absolute;

        left: 0;

        top: 50%;

        transform: translateY(-50%);

        display: flex;

      `,

            [

              iconSwitchTransition({

                top: "50%",

                originalTransform: "translateY(-50%)",

              }),

            ]

          ),

          fadeInWidthExpandTransition(),

        ]

      ),

      // content 子元素内容相关的样式类骨架

      cE(

        "content",

        `

      display: flex;

      align-items: center;

      flex-wrap: nowrap;

    `,

        [

          c("~", [

            cE("icon", {

              margin: "var(--icon-margin)",

              marginRight: 0,

            }),

          ]),

        ]

      ),

      // 更多的关于 backgroundColor、base-wave 点击反馈波纹,icon,content,block 相关的样式定义

    ],

    // 动画、过渡相关的样式类骨架

    c("@keyframes button-wave-spread", {

    from: {

      boxShadow: "0 0 0.5px 0 var(--ripple-color)",

    },

    to: {

      // don't use exact 5px since chrome will display the animation with glitches

      boxShadow: "0 0 0.5px 4.5px var(--ripple-color)",

    },

  }),

  c("@keyframes button-wave-opacity", {

    from: {

      opacity: "var(--wave-opacity)",

    },

    to: {

      opacity: 0,

    },

  }),

]);

Приведенный выше код, связанный с CSS Render, в конечном итоге создаст что-то вроде следующего:

.n-button {

    font-weight: var(--font-weight);

    line-height: 1;

    font-family: inherit;

    padding: var(--padding);

    transition:

      color .3s var(--bezier),

      background-color .3s var(--bezier),

      opacity .3s var(--bezier),

      border-color .3s var(--bezier);

  

}



.n-button.n-button--color .n-button__border {

  border-color: var(--border-color);

}



.n-button.n-button--color.n-button--disabled .n-button__border {

  border-color: var(--border-color-disabled);

}



.n-button.n-button--color:not(.n-button--disabled):focus .n-button__state-border {

  border-color: var(--border-color-focus);

}





.n-button .n-base-wave {



      pointer-events: none;

      top: 0;

      right: 0;

      bottom: 0;

      left: 0;

      animation-iteration-count: 1;

      animation-duration: var(--ripple-duration);

      animation-timing-function: var(--bezier-ease-out), var(--bezier-ease-out);

    

}



.n-button .n-base-wave.n-base-wave--active {

  z-index: 1;

  animation-name: button-wave-spread, button-wave-opacity;

}



.n-button .n-button__border, .n-button .n-button__state-border {



      position: absolute;

      left: 0;

      top: 0;

      right: 0;

      bottom: 0;

      border-radius: inherit;

      transition: border-color .3s var(--bezier);

      pointer-events: none;

    

}



.n-button .n-button__icon {



      margin: var(--icon-margin);

      margin-left: 0;

      height: var(--icon-size);

      width: var(--icon-size);

      max-width: var(--icon-size);

      font-size: var(--icon-size);

      position: relative;

      flex-shrink: 0;

    

}



.n-button .n-button__icon .n-icon-slot {



        height: var(--icon-size);

        width: var(--icon-size);

        position: absolute;

        left: 0;

        top: 50%;

        transform: translateY(-50%);

        display: flex;

      

}



.n-button .n-button__icon.fade-in-width-expand-transition-enter-active {



      overflow: hidden;

      transition:

        opacity .2s cubic-bezier(.4, 0, .2, 1) .1s,

        max-width .2s cubic-bezier(.4, 0, .2, 1),

        margin-left .2s cubic-bezier(.4, 0, .2, 1),

        margin-right .2s cubic-bezier(.4, 0, .2, 1);

    

}



.n-button .n-button__content {



      display: flex;

      align-items: center;

      flex-wrap: nowrap;

    

}



.n-button .n-button__content ~ .n-button__icon {

  margin: var(--icon-margin);

  margin-right: 0;

}



.n-button.n-button--block {



      display: flex;

      width: 100%;

    

}



.n-button.n-button--dashed .n-button__border, .n-button.n-button--dashed .n-button__state-border {

  border-style: dashed !important;

}



.n-button.n-button--disabled {

  cursor: not-allowed;

  opacity: var(--opacity-disabled);

}



@keyframes button-wave-spread {

  from {

    box-shadow: 0 0 0.5px 0 var(--ripple-color);

  }

  to {

    box-shadow: 0 0 0.5px 4.5px var(--ripple-color);

  }

}



@keyframes button-wave-opacity {

  from {

    opacity: var(--wave-opacity);

  }

  to {

    opacity: 0;

  }

}

можно увидетьbuttonСвязанные стили обрабатывают различные сценарии, используя стиль именования БЭМ:

  • border и state-border , о стилях в отключенном, нажатом, наведении, активном и других состояниях
.n-button.n-button--color:not(.n-button--disabled):focus .n-button__state-border {

  border-color: var(--border-color-focus);

}
  • Стиль пульсации при нажатии кнопки.n-button .n-base-wave
  • Стили, связанные со значками, в кнопках.n-button .n-button__icon
  • Стили для таких вещей, как текст в кнопках.n-button .n-button__content

В то же время вы можете видеть, что соответствующие CSS-переменные зарезервированы для различных свойств в стиле, включая box-shadow--ripple-color, значок ширина и высота--icon-size, анимация переходаtransitionиз--bezier, эти переменные оставляют место для последующей настройки различных стилей и тем.

То есть при проектировании системы стилей библиотеки компонентов шаблоны стилей, относящиеся к компонентам, определяются заранее с использованием стиля БЭМ, а затем переменные, связанные с темой, которые необходимо настроить, настраиваются с помощью переменных CSS для достижения эффекта настройка темы.

монтировать глобальный стиль

Глобальный стиль в основном связан с какой-то простой базовой конфигурацией, код выглядит следующим образом:

import { c } from "../../_utils/cssr";

import commonVariables from "../common/_common";



export default c(

  "body",

  `

  margin: 0;

  font-size: ${commonVariables.fontSize};

  font-family: ${commonVariables.fontFamily};

  line-height: ${commonVariables.lineHeight};

  -webkit-text-size-adjust: 100%;

`,

  [

    c(

      "input",

      `

    font-family: inherit;

    font-size: inherit;

  `

    ),

  ]

);

главным образом дляmargin,font-size,font-family,line-heightи другой сопутствующий контент — это стандартизация кода CSS, необходимая для совместимости с браузерами.Normalize.css: Make browsers render all elements more consistently..

вcommonVariablesследующим образом:

export default {

  fontFamily:

    'v-sans, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',

  fontFamilyMono: "v-mono, SFMono-Regular, Menlo, Consolas, Courier, monospace",



  fontWeight: "400",

  fontWeightStrong: "500",



  cubicBezierEaseInOut: "cubic-bezier(.4, 0, .2, 1)",

  cubicBezierEaseOut: "cubic-bezier(0, 0, .2, 1)",

  cubicBezierEaseIn: "cubic-bezier(.4, 0, 1, 1)",



  borderRadius: "3px",

  borderRadiusSmall: "2px",



  fontSize: "14px",

  fontSizeTiny: "12px",

  fontSizeSmall: "14px",

  fontSizeMedium: "14px",

  fontSizeLarge: "15px",

  fontSizeHuge: "16px",



  lineHeight: "1.6",



  heightTiny: "22px",

  heightSmall: "28px",

  heightMedium: "34px",

  heightLarge: "40px",

  heightHuge: "46px",



  transformDebounceScale: "scale(1)",

};

Вышеупомянутые общие переменные являются одними из самых основных «сырьевых материалов» для библиотеки компонентов пользовательского интерфейса для построения вверх, а также являются передовыми отраслевыми практиками, которые не рекомендуется изменять по умолчанию, например, определениеsizeЕсть 5 категорий, а именноtiny,small,medium,large,huge, определить шрифты, шрифты кода и т. д.

Определение и регистрация переменных CSS

Основной код этого блока:

const NConfigProvider = inject(configProviderInjectionKey, null);

const mergedThemeRef = computed(() => {

const {

  theme: { common: selfCommon, self, peers = {} } = {},

  themeOverrides: selfOverrides = {},

  builtinThemeOverrides: builtinOverrides = {},

} = props;

const { common: selfCommonOverrides, peers: peersOverrides } =

  selfOverrides;

const {

  common: globalCommon = undefined,

  [resolveId]: {

    common: globalSelfCommon = undefined,

    self: globalSelf = undefined,

    peers: globalPeers = {},

  } = {},

} = NConfigProvider?.mergedThemeRef.value || {};

const {

  common: globalCommonOverrides = undefined,

  [resolveId]: globalSelfOverrides = {},

} = NConfigProvider?.mergedThemeOverridesRef.value || {};

const {

  common: globalSelfCommonOverrides,

  peers: globalPeersOverrides = {},

} = globalSelfOverrides;

const mergedCommon = merge(

  {},

  selfCommon || globalSelfCommon || globalCommon || defaultTheme.common,

  globalCommonOverrides,

  globalSelfCommonOverrides,

  selfCommonOverrides

);



const mergedSelf = merge(

  // {}, executed every time, no need for empty obj

  (self || globalSelf || defaultTheme.self)?.(mergedCommon),

  builtinOverrides,

  globalSelfOverrides,

  selfOverrides

);

return {

  common: mergedCommon,

  self: mergedSelf,

  peers: merge({}, defaultTheme.peers, globalPeers, peers),

  peerOverrides: merge({}, globalPeersOverrides, peersOverrides),

};

Первый изinjectполучатьconfigProviderInjectionKeyсоответствующий контент, которыйconfigProviderInjectionKeyСоответствующее содержание определяется следующим образом:

provide(configProviderInjectionKey, {

  mergedRtlRef,

  mergedIconsRef,

  mergedComponentPropsRef,

  mergedBorderedRef,

  mergedNamespaceRef,

  mergedClsPrefixRef,

  mergedLocaleRef: computed(() => {

    // xxx

  }),

  mergedDateLocaleRef: computed(() => {

    // xxx

  }),

  mergedHljsRef: computed(() => {

    // ...

  }),

  mergedThemeRef,

  mergedThemeOverridesRef

})

Вы можете увидеть почти все конфигурации, включая rtl, значок, границу, пространство имен, clsPrefix, локаль (интернационализацию), дату, тему, themeOverrides и т. д. Основная цель здесь — получить конфигурацию, связанную с темой:

  • mergedThemeRef: Настраиваемая тема, например
<template>

  <n-config-provider :theme="darkTheme">

    <app />

  </n-config-provider>

</template>



<script>

  import { darkTheme } from 'naive-ui'



  export default {

    setup() {

      return {

        darkTheme

      }

    }

  }

</script>
  • mergedThemeOverridesRef: Настраиваемые переменные темы, такие как
const themeOverrides = {

    common: {

      primaryColor: '#FF0000'

    },

    Button: {

      textColor: '#FF0000'

      backgroundColor: '#FFF000',

    },

    Select: {

      peers: {

        InternalSelection: {

          textColor: '#FF0000'

        }

      }

    }

    // ...

  }

Вышеуказанные два в основном включают в себя глобальныеcommonсвязанные, иButtonсерединаcommonсвязанные юниформ-переменные,selfНекоторые переменные, связанные с настройкой кнопок, иButtonОграничения, связанные с использованием с другими компонентамиpeersПеременная.

отuseThemeвозврат в функции ловушкиthemeRefПозже,themeRefСвязанный контент будет использоваться для сборки различных стилей, задействованных в кнопке, в основном из следующих четырех направлений:

  • fontProps
  • colorProps
  • borderProps
  • sizeProps
cssVars: computed(() => {

  // fontProps

  // colorProps

  // borderProps

  // sizeProps

  

  return {

  // 处理 动画过渡函数、透明度相关的变量

    "--bezier": cubicBezierEaseInOut,

      "--bezier-ease-out": cubicBezierEaseOut,

      "--ripple-duration": rippleDuration,

      "--opacity-disabled": opacityDisabled,

      "--wave-opacity": waveOpacity,

      // 处理字体、颜色、边框、大小相关的变量

    ...fontProps,

    ...colorProps,

    ...borderProps,

    ...sizeProps,

  };

});

fontPropsСоответствующий код выглядит следующим образом:

const theme = themeRef.value;

const {

  self,

} = theme;

const {

  rippleDuration,

  opacityDisabled,

  fontWeightText,

  fontWeighGhost,

  fontWeight,

} = self;

const { dashed, type, ghost, text, color, round, circle } = props;

        // font

const fontProps = {

  fontWeight: text

    ? fontWeightText

    : ghost

    ? fontWeighGhost

    : fontWeight,

};

В основном он оценивает переменные CSS и значения, связанные со шрифтом в стандартном состоянии, когда кнопка отображается как текстовый узел, когда она отображается с прозрачным фоном и в стандартном состоянии.

colorPropsСоответствующий код выглядит следующим образом

let colorProps = {

  "--color": "initial",

  "--color-hover": "initial",

  "--color-pressed": "initial",

  "--color-focus": "initial",

  "--color-disabled": "initial",

  "--ripple-color": "initial",

  "--text-color": "initial",

  "--text-color-hover": "initial",

  "--text-color-pressed": "initial",

  "--text-color-focus": "initial",

  "--text-color-disabled": "initial",

};



if (text) {

  const { depth } = props;

  const textColor =

    color ||

    (type === "default" && depth !== undefined

      ? self[createKey("textColorTextDepth", String(depth))]

      : self[createKey("textColorText", type)]);

  colorProps = {

    "--color": "#0000",

    "--color-hover": "#0000",

    "--color-pressed": "#0000",

    "--color-focus": "#0000",

    "--color-disabled": "#0000",

    "--ripple-color": "#0000",

    "--text-color": textColor,

    "--text-color-hover": color

      ? createHoverColor(color)

      : self[createKey("textColorTextHover", type)],

    "--text-color-pressed": color

      ? createPressedColor(color)

      : self[createKey("textColorTextPressed", type)],

    "--text-color-focus": color

      ? createHoverColor(color)

      : self[createKey("textColorTextHover", type)],

    "--text-color-disabled":

      color || self[createKey("textColorTextDisabled", type)],

  };

} else if (ghost || dashed) {

  colorProps = {

    "--color": "#0000",

    "--color-hover": "#0000",

    "--color-pressed": "#0000",

    "--color-focus": "#0000",

    "--color-disabled": "#0000",

    "--ripple-color": color || self[createKey("rippleColor", type)],

    "--text-color": color || self[createKey("textColorGhost", type)],

    "--text-color-hover": color

      ? createHoverColor(color)

      : self[createKey("textColorGhostHover", type)],

    "--text-color-pressed": color

      ? createPressedColor(color)

      : self[createKey("textColorGhostPressed", type)],

    "--text-color-focus": color

      ? createHoverColor(color)

      : self[createKey("textColorGhostHover", type)],

    "--text-color-disabled":

      color || self[createKey("textColorGhostDisabled", type)],

  };

} else {

  colorProps = {

    "--color": color || self[createKey("color", type)],

    "--color-hover": color

      ? createHoverColor(color)

      : self[createKey("colorHover", type)],

    "--color-pressed": color

      ? createPressedColor(color)

      : self[createKey("colorPressed", type)],

    "--color-focus": color

      ? createHoverColor(color)

      : self[createKey("colorFocus", type)],

    "--color-disabled": color || self[createKey("colorDisabled", type)],

    "--ripple-color": color || self[createKey("rippleColor", type)],

    "--text-color": color

      ? self.textColorPrimary

      : self[createKey("textColor", type)],

    "--text-color-hover": color

      ? self.textColorHoverPrimary

      : self[createKey("textColorHover", type)],

    "--text-color-pressed": color

      ? self.textColorPressedPrimary

      : self[createKey("textColorPressed", type)],

    "--text-color-focus": color

      ? self.textColorFocusPrimary

      : self[createKey("textColorFocus", type)],

    "--text-color-disabled": color

      ? self.textColorDisabledPrimary

      : self[createKey("textColorDisabled", type)],

  };

}

Основная обработка осуществляется в четырех формах: обычная, текстовый узел, призрачный фон, прозрачный, пунктирная пунктирная линия, для разных состояний Стандарт,pressed,hover,focus,disabledи т. д. иметь дело с соответствующими свойствами и значениями CSS

borderPropsСоответствующий код выглядит следующим образом:

let borderProps = {

  "--border": "initial",

  "--border-hover": "initial",

  "--border-pressed": "initial",

  "--border-focus": "initial",

  "--border-disabled": "initial",

};

if (text) {

  borderProps = {

    "--border": "none",

    "--border-hover": "none",

    "--border-pressed": "none",

    "--border-focus": "none",

    "--border-disabled": "none",

  };

} else {

  borderProps = {

    "--border": self[createKey("border", type)],

    "--border-hover": self[createKey("borderHover", type)],

    "--border-pressed": self[createKey("borderPressed", type)],

    "--border-focus": self[createKey("borderFocus", type)],

    "--border-disabled": self[createKey("borderDisabled", type)],

  };

}

В основном это касается пяти различных состояний стандарта,pressed,hover,focus,disabledобработки в таких случаях.

здесьborderPropsНа самом деле, это в основном для определения всегоborderсвойства, а свойства, связанные с цветом границы, фактически передаются черезsetupвнутриcustomColorCssVarsОпределенный код выглядит следующим образом:

customColorCssVars: computed(() => {

    const { color } = props;

    if (!color) return null;

    const hoverColor = createHoverColor(color);

    return {

      "--border-color": color,

      "--border-color-hover": hoverColor,

      "--border-color-pressed": createPressedColor(color),

      "--border-color-focus": hoverColor,

      "--border-color-disabled": color,

    };

  })

sizePropsСоответствующий код выглядит следующим образом:

const sizeProps = {

  "--width": circle && !text ? height : "initial",

  "--height": text ? "initial" : height,

  "--font-size": fontSize,

  "--padding": circle

    ? "initial"

    : text

    ? "initial"

    : round

    ? paddingRound

    : padding,

  "--icon-size": iconSize,

  "--icon-margin": iconMargin,

  "--border-radius": text

    ? "initial"

    : circle || round

    ? height

    : borderRadius,

};

основная обработкаwidth,height,font-size,padding,icon,borderсодержание соответствующего размера, гдеmarginОн обрабатывается, когда монтируется глобальный стиль по умолчанию, значение по умолчанию равно 0.

резюме

Выполните три шага выше:

  1. Смонтируйте скелет класса стиля, связанного с кнопкой, оставив множество переменных CSS для пользовательских стилей.
  2. Смонтируйте глобальный стиль по умолчанию
  3. Соберите и определите соответствующие переменные CSS для заполнения скелета класса стиля

Мы успешно применили CSS Render, плагин BEM и переменные CSS, чтобы завершить дизайн общего стиля кнопки, который легко понять и настроить.

Однако также стоит отметить, что, глядя на логику обработки стилей в вышеуказанных компонентах, только определенные вsetupНа самом деле можно увидеть основные сценарии использования CSS Render: то есть все условия заранее стандартизированы, соответствующие CSS-переменные предварительно заданы, а затем даны

Необходимая обработка событий не может быть меньше

Наивный интерфейс в основном обеспечивает обработку следующих типов событий:

  • mousedown : handleMouseDown
  • keyup:handleKeyUp
  • keydown: handleKeyDown
  • click : handleClick
  • blur : handleBlur

Вы можете посмотреть код отдельно:

handleMouseDown:

const handleMouseDown = (e) => {

  e.preventDefault();

  if (props.disabled) {

    return;

  }

  if (mergedFocusableRef.value) {

    selfRef.value?.focus({ preventScroll: true });

  }

};

основная обработкаdisabledне отвечает, и если можноfocusслучай, звонитеselfRefСосредоточьтесь и активируйте соответствующий стиль.

handleKeyUp:

const handleKeyUp = (e) => {

  switch (e.code) {

    case "Enter":

    case "NumpadEnter":

      if (!props.keyboard) {

        e.preventDefault();

        return;

      }

      enterPressedRef.value = false;

      void nextTick(() => {

        if (!props.disabled) {

          selfRef.value?.click();

        }

      });

  }

};

основная обработкаEnter,NumpadEnterключ, определите, поддерживается ли работа с клавиатурой, и активируйте нажатия кнопок, если это необходимо.

handleKeyDown:

const handleKeyDown = (e) => {

  switch (e.code) {

    case "Enter":

    case "NumpadEnter":

      if (!props.keyboard) return;

      e.preventDefault();

      enterPressedRef.value = true;

  }

};

основная обработкаEnter,NumpadEnterkey, определите, поддерживается ли обработка клавиатуры, и при необходимости обновите ее.enterPressedRefзначение, флаг в настоящее времяkeydownпроходить.

handleClick:

const handleClick = (e) => {

  if (!props.disabled) {

    const { onClick } = props;

    if (onClick) call(onClick, e);

    if (!props.text) {

      const { value } = waveRef;

      if (value) {

        value.play();

      }

    }

  }

};

Вызовите соответствующую функцию обработки кликов в соответствии с состоянием и волновым эффектом щелчка кнопки воспроизведения под нетекстовым узлом.

handleBlur:

const handleBlur = () => {

  enterPressedRef.value = false;

};

возобновитьenterPressedRefЗначение флага в настоящее время размыто.

Резюме и перспективы

В этой статье анализируется весь процесс создания кнопки наивного пользовательского интерфейса слой за слоем и на уровне исходного кода.Можно обнаружить, что в области библиотеки компонентов большая часть идей тратится на то, как разработать расширяемую систему стилей, от Ant Design, Element Пользовательский интерфейс использует Less для организации системы стилей, а Material Design использует CSS-in-JS, напримерstyled-componentsДля организации системы стилей, а сейчас Naive UI использует CSS Render для организации системы стилей.Хотя существует множество форм организации системы стилей, на самом деле, насколько я понимаю, при проектировании классов стилей, соответствующих стилей, расширений стилей и настройка темы в целом должна оставаться одинаковой.

Если вы можете понять весь процесс работы с Button с помощью этой запутанной статьи и по-прежнему сохранять интерес к общему исходному коду и инженерному направлению Naive UI, вы можете следовать этой логике, чтобы понять принципы проектирования других компонентов, так же, как мне нравится изображение в начале, вы будете чувствовать себя все более и более простым, поскольку вы понимаете общий код:

Понимание дизайна исходного кода отличных библиотек и изучение исходного кода Дэниела может помочь нам понять передовой опыт в отрасли, отличные дизайнерские идеи, улучшить способ написания кода и стать лучшими разработчиками Мы поощряем вас и меня 💪!

использованная литература

❤️/ Спасибо за поддержку /

Выше приведено все содержание этого обмена, я надеюсь, что это поможет вам ^_^

Если вам понравилось, не забудьте поделиться, поставить лайк и добавить в избранное~

Добро пожаловать на общедоступный номеравтобус программиста, три брата из Byte, Shopee и Zhaoyin, делятся опытом программирования, техническими галантерейными товарами и планированием карьеры, чтобы помочь вам избежать окольных путей, чтобы попасть на большую фабрику.