Библиотека компонентов Vue 3: анализ исходного кода с добавлением элементов

Vue.js

Библиотека компонентов на основе Vue3element-plusОфициально выпущенный, element-plus используетсяTypeScript + Composition APIРефакторинг нового проекта. Официальный список следующих крупных обновлений, в этой статье будет читатьсяelement-plus, проанализировать рефакторинг исходного кода со следующих аспектов в целом и в деталях.Рекомендуется клонировать код компонента перед чтением этой статьи.

  • Разрабатывать с помощью TypeScript
  • Уменьшите связанность и упростите логику с помощью Vue 3.0 Composition API.
  • Используйте новые функции Vue 3.0 Teleport для рефакторинга компонентов класса монтирования.
  • Глобальный API Vue 2.0 переключился на API экземпляра Vue 3.0
  • интернационализация
  • Официальная документация Веб-сайт Упаковка
  • Библиотека компонентов и упаковка стилей
  • Поддерживайте и управляйте проектами с Lerna

Связанный с машинописью

element-plus вводит машинописный текст в дополнение к настройке соответствующегоeslintПравила проверки, плагины, определенияtsconfig.jsonКроме того, упаковкаes-moduleПри форматировании библиотеки компонентов я использовал некоторыеrollupплагин.

  • @rollup/plugin-node-resolve
  • rollup-plugin-terser
  • rollup-plugin-typescript2
  • rollup-plugin-vue
// build/rollup.config.bundle.js
import { nodeResolve } from '@rollup/plugin-node-resolve'
import { terser } from 'rollup-plugin-terser'
import typescript from 'rollup-plugin-typescript2'
const vue = require('rollup-plugin-vue')
export default [{
  // ... 省略前面部分内容 
  plugins: [
   terser(),
   nodeResolve(),
   vue({
     target: 'browser',
     css: false,
     exposeFilename: false,
   }),
   typescript({
     tsconfigOverride: {
       'include': [
         'packages/**/*',
         'typings/vue-shim.d.ts',
       ],
       'exclude': [
         'node_modules',
         'packages/**/__tests__/*',
       ],
     },
   }),
 ],
}]

@rollup/plugin-node-resolveУпаковка зависимых пакетов npm

rollup-plugin-terserсжатый код

rollup-plugin-vueУпакуйте файл vue, стиль css будет упомянут позже.gulpобрабатывать.

rollup-plugin-typescript2компилируется сtypescriptДа, нод-модули и файлы, связанные с тестами, исключены из конфигурации.Помимо включения реализации компонента, включение также включаетtypings/vue-shim.d.tsдокумент.

используется в плагинеtypings/vue-shim.d.tsфайл объявления типа (начиная с.d.tsФайл в конце будет автоматически проанализирован), который определяет некоторые объявления глобального типа, и эти переменные ограничения типа могут использоваться непосредственно в файлах ts или vue. Также используйте расширенную пару шаблоновimport XX from XX.vueИмпортированные переменные дают подсказки типа.

// typings/vue-shim.d.ts
declare module '*.vue' {
  import { defineComponent } from 'vue'
  const component: ReturnType<typeof defineComponent>
  export default component
}
declare type Nullable<T> = T | null;
declare type CustomizedHTMLElement<T> = HTMLElement & T
declare type Indexable<T> = {
  [key: string]: T
}
declare type Hash<T> = Indexable<T>
declare type TimeoutHandle = ReturnType<typeof global.setTimeout>
declare type ComponentSize = 'large' | 'medium' | 'small' | 'mini'

Кромеd.tsВне файла объявление типа props в element-plus использует vue3propType. Уведомление к следующему примеру, использование типа реквизита выполняет проптип в соответствии с нашим определением конструктора самоуправления, а затем в сочетании с типографией Type Crought. Используются другие виды не-декларации реквизитinterface.

import { PropType } from 'vue'
export default defineComponent({
  name: 'ElAlert',
  props: {
    type: {
      type: String as PropType<'success' | 'info' | 'error' | 'warning'>,
      default: 'info',
    }
  }
})

Дополнительную поддержку машинописного текста vue3 можно посмотретьофициальная документация

Composition API

Официальное описание использует API Vue 3.0 Composition для уменьшения связанности и упрощения логики. Использование Composition API и повторное использование хуковvue-3-playgroundИнтуитивно понятный и краткий пример представлен в реализации демонстрации корзины покупок.

Чтобы узнать об использовании часто используемого Composition API, вы можете ознакомиться с этой краткой статьей.Быстро используйте последние 15 распространенных API-интерфейсов Vue3.

В дополнение к переписыванию компонентов с использованием нового Composition API, element-pluspackages/hooksНесколько повторно используемых файлов ловушек извлекаются из каталога.

Используется элементами управления, такими как автозаполнение, ввод и т. д.use-attrsНапример, главное, что нужно сделать, это наследовать связанные свойства и события, что-то вроде$attrsа также$listenerфункция, но была выполнена некоторая фильтрация для удаления некоторых свойств и привязок событий, которые не нужно наследовать.

watchEffect(() => {
    const res = entries(instance.attrs)
      .reduce((acm, [key, val]) => {
        if (
          !allExcludeKeys.includes(key) &&
          !(excludeListeners && LISTENER_PREFIX.test(key))
        ) {
          acm[key] = val
        }
        return acm
      }, {})
    attrs.value = res
  })

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

  1. Происхождение свойств, представленных в контексте рендеринга, неясно. Например, при использовании нескольких примесей для чтения шаблона компонента может быть сложно определить, из какой примеси введено конкретное свойство.
  2. Конфликт пространства имен. Миксины могут конфликтовать с именами свойств и методов.

Преимущества хуков

  1. Свойства, открытые для шаблонов, имеют явное происхождение, потому что они являются значениями, возвращаемыми функциями Hook.
  2. Значение, возвращаемое функцией Hook, может быть названо произвольно, поэтому конфликта пространств имен не возникает.

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

element-plus использует новые функции vue3 для нескольких монтируемых компонентов.Teleport, эта новая функция может помочь нам переместить элемент, который он обертывает, в указанный нами узел.

Teleport предоставляет простой способ, позволяющий нам контролировать, под каким родительским узлом HTML отображается в DOM, без необходимости прибегать к глобальному состоянию или разбивать его на два компонента. -- Официальная документация Vue

Заглянув на официальный сайт, мы обнаружим, что Dialog, Drawer, Tooltip и Popover с использованием Popper добавили новый.append-to-bodyАтрибуты. Возьмем диалог в качестве примера: Если для appendToBody установлено значение false, телепорт будет отключен, а DOM все равно будет отображаться в текущей позиции.Если это значение равно true, содержимое диалогового окна будет помещено под телом.

<template>
  <teleport to="body" :disabled="!appendToBody">
    <transition
      name="dialog-fade"
      @after-enter="afterEnter"
      @after-leave="afterLeave"
    >
        ...
     </transition>
  </teleport>
</tamplate>

В исходном element-ui всплывающая подсказка и всплывающая подсказка также размещаются непосредственно в теле, которое изначально использовалось vue-popper.js.document.body.appendChildЧтобы добавить элементы в тело, element-plus использует Teleport для реализации соответствующей логики.

Глобальный API — Пример API

Когда мы устанавливаем библиотеку компонентов, метод use выполнит метод установки для глобального монтирования компонента. Давайте сначала посмотрим, как глобальный API написан в Vue 2.x element-ui:
Метод Vue.component для привязки глобальных компонентов
Vue.use связывает глобальные пользовательские директивы
Vue.prototype связывает глобальные переменные и глобальные методы

const install = function(Vue, opts = {}) {
  locale.use(opts.locale);
  locale.i18n(opts.i18n);
  
  // Vue.component 方法绑定全局组件
  components.forEach(component => {
    Vue.component(component.name, component);
  });
  
  // Vue.use 绑定全局自定义指令
  Vue.use(InfiniteScroll);
  Vue.use(Loading.directive);
 
  // Vue.prototype 绑定全局变量和全局方法
  Vue.prototype.$ELEMENT = {
    size: opts.size || '',
    zIndex: opts.zIndex || 2000
  };
  Vue.prototype.$loading = Loading.service;
  Vue.prototype.$msgbox = MessageBox;
  Vue.prototype.$alert = MessageBox.alert;
  Vue.prototype.$confirm = MessageBox.confirm;
  Vue.prototype.$prompt = MessageBox.prompt;
  Vue.prototype.$notify = Notification;
  Vue.prototype.$message = Message;
};

Но в Vue 3.0 любой API, который меняет поведение Vue Globally, теперь будет перемещен в экземпляр приложения, то есть приложение, сгенерированное CreateApp, и соответствующий API также был соответственно изменен.

Давайте посмотрим на element-plus с использованием Vue 3.0, глобальный API переписан в API экземпляра.

import type { App } from 'vue'
const plugins = [
  ElInfiniteScroll,
  ElLoading,
  ElMessage,
  ElMessageBox,
  ElNotification,
]
const install = (app: App, opt: InstallOptions): void => {
  const option = Object.assign(defaultInstallOpt, opt)
  use(option.locale)
  app.config.globalProperties.$ELEMENT = option    // 全局设置默认的size属性和z-index属性
  // 全局注册所有除了plugins之外的组件
  components.forEach(component => {
    app.component(component.name, component)
  })
  plugins.forEach(plugin => {
    app.use(plugin as any)
  })
}

Кроме того, есть некоторые отличия в стиле написания.Компонент класса сообщений добавляет метод $global в element-plus.Он перемещен вindex.tsв, Некоторые компоненты типа уведомлений о сообщениях размещены в плагинах с использованиемapp.useБудет вызываться метод установки в соответствующем компоненте index.ts, а код выглядит следующим образом:

(Message as any).install = (app: App): void => {
  app.config.globalProperties.$message = Message
}

глобализация

В пакетах есть папка локали для управления переключением языков.packages/locale/index.tsЕсть 2 метода, методtи методuse, tконтрольvueперевод замена текста в файле,useспособ изменить глобальный язык

// packages/locale/index.ts
export const t = (path:string, option?): string => {
  let value
  const array = path.split('.')
  let current = lang
  for (let i = 0, j = array.length; i < j; i++) {
    const property = array[i]
    value = current[property]
    if (i === j - 1) return template(value, option)
    if (!value) return ''
    current = value
  }
  return ''
}

Метод t в локали будет представлен в файле vue.

import { t } from '@element-plus/locale'

Затем вы можете использовать многоязычные значения ключей в шаблоне, например:label="t('el.datepicker.nextMonth')",tМетод поможет найти соответствующее значение в соответствующем языковом файле.
посмотри сноваuseметод, который бросаетuseМетод может установить глобальный тип языка, а также изменитьday.jsязыковая конфигурация. представлен в element-plusday.jsзаменить оригиналmoment.jsДля форматирования времени и обработки информации о часовом поясе.

export const use = (l): void => {
  lang = l || lang
  if (lang.name) {
    dayjs.locale(lang.name)
  }
}

После того, как наш бизнес-компонент представит element-plus, мы будем использовать этот метод использования для установки типа языка, см.официальная документация

Упаковка веб-сайта

Веб-сайт, также известный как веб-сайт документации, содержит примеры использования каждого элемента управления.website/entry.jsсередина

import ElementPlus from 'element-plus'

На самом деле, это должно было быть введеноpackages/element-plus/index.tsфайл, а затем вы можете использовать каждый компонент в пакетах в md, и изменения логики компонента могут вступить в силу немедленно.

В соответствии с element-ui, разработчик веб-сайта element-plus использует webpack для обслуживания и упаковки.vue-loaderиметь дело сvueфайл, использоватьbabel-loaderиметь дело сjs/tsфайлы, файлы стилей и значки шрифтов используют соответствующиеcss-loader,url-loaderЖдать.

Соответствующая конфигурация находится вwebsite/webpack.config.jsсередина

В документации показан основной файл md с использованиемwebsite/md-loader/index.jsсамореализованныйmd-loader, соответственно извлеченные из md<template>а также<script>content, преобразуйте пример компонента в md в строку vue, а затем передайтеvue-loaderобрабатывать.

rules: [
  {
    test: /\.vue$/,
    use: 'vue-loader',
  },
  {
    test: /\.(ts|js)x?$/,
    exclude: /node_modules/,
    loader: 'babel-loader',
  },
  {
    test: /\.md$/,
    use: [
      {
        loader: 'vue-loader',
        options: {
          compilerOptions: {
            preserveWhitespace: false,
          },
        },
      },
      {
        loader: path.resolve(__dirname, './md-loader/index.js'),
      },
    ],
  },
  {
    test: /\.(svg|otf|ttf|woff2?|eot|gif|png|jpe?g)(\?\S*)?$/,
    loader: 'url-loader',
    // todo: 这种写法有待调整
    query: {
      limit: 10000,
      name: path.posix.join('static', '[name].[hash:7].[ext]'),
    },
  },
]

Библиотека компонентов и упаковка стилей

Существует такой длинный список команд упаковки element-plus, среди которыхyarn build:libа такжеyarn build:lib-fullЭто полный пакет, который использует webpack для воспроизведения формата umd. Остальные используют rollup и gulp соответственно.

 "build": "yarn bootstrap && yarn clean:lib && yarn build:esm-bundle && yarn build:lib && yarn build:lib-full && yarn build:esm && yarn build:utils && yarn build:locale && yarn build:locale-umd && yarn build:theme"

Пакеты компонентов упаковки с роллапом

В дополнение к использованию веб-пакета для упаковки компонентов, element-plus также предоставляет еще одинes-moduleМетод упаковки и, наконец, публикация в npm — это продукты, упакованные с помощью webpack, а такжеrollupУпакованный пакет es-module.
rollupСоответствующая логика находится вbuild/rollup.config.bundle.jsв файле
Запись экспорт всех компонентов/packages/element-plus/index.ts, используя спецификацию es-module и, наконец, упакованный вlib/index.esm.jsсередина. Из-за использования плагина Typescript при упаковке окончательный сгенерированный файл является дополнением к полномуindex.esm.js, и каждый компонент в отдельностиlibдокумент.

// build/rollup.config.bundle.js
export default [
  {
    input: path.resolve(__dirname, '../packages/element-plus/index.ts'),
    output: {
      format: 'es',    // 打包格式为 es,可选cjs(commonJS) ,umd 等
      file: 'lib/index.esm.js',
    },
    external(id) {
      return /^vue/.test(id)
        || deps.some(k => new RegExp('^' + k).test(id))
    },
  }
]

Используйте gulp для упаковки файлов стилей и значков шрифтов.

Как и element-ui, файлы стилей и значки шрифтов упакованы с использованиемpackages/theme-chalk/gulpfile.js, упакуйте каждый файл scss в отдельный файл css, который содержит общие базовые стили, а также файлы стилей для каждого компонента.

// packages/theme-chalk/gulpfile.js
function compile() {
  return src('./src/*.scss')
    .pipe(sass.sync())
    .pipe(autoprefixer({ cascade: false }))
    .pipe(cssmin())
    .pipe(rename(function (path) {
      if(!noElPrefixFile.test(path.basename)) {
        path.basename = `el-${path.basename}`
      }
    }))
    .pipe(dest('./lib'))
}
function copyfont() {
  return src('./src/fonts/**')
    .pipe(cssmin())
    .pipe(dest('./lib/fonts'))
}

пройти сноваnpm scriptНекоторые операции копирования и удаления файлов в пакете, файлы стилей и значков шрифтов после упаковки в конечном итоге будут помещены вlib/theme-chalkПод содержанием.

cp-cli packages/theme-chalk/lib lib/theme-chalk && rimraf packages/theme-chalk/lib

резюме

Мы видим, что библиотека компонентов использует 3 инструмента упаковки:rollup,webpack,gulp.
Vue,ReactКогда библиотека с открытым исходным кодом начнет использовать накопитель, сборка будет быстрее, и тогда проект приложения в основном использует веб-пакет, потому что веб-пакет может использовать плагины и различныеloaderОбрабатывать другие ресурсы, отличные от javascript. Пакет скинов и файлы шрифтов используют gulp, который может быть более кратким, чем webpack, и его не нужно представлять.url-loader,css-loaderЖдать. Насколько я понимаю, webpack — это законченное решение, полностью функциональное, но громоздкое в настройке. rollup и gulp подходят для использования, когда требования к упаковке относительно просты, они более легкие и настраиваемые.

Представьте лерну

Небольшое изменение в целом, element-plus принимаетlernaЧто касается управления пакетами, lerna может отвечать за управление версией element-plus и компонентом, а также может публиковать каждый компонент в виде пакета npm (но в настоящее время element-plus имеет только полные пакеты в npm, а также пакет скина и многоязычные файлы). одного компонента теперь также помещаются в папку вместо каждого компонента). Каждый компонент имеет такойpackage.jsonдокумент

{
  "name": "@element-plus/message",
  "version": "0.0.0",
  "main": "dist/index.js",
  "license": "MIT",
  "peerDependencies": {
    "vue": "^3.0.0"
  },
  "devDependencies": {
    "@vue/test-utils": "^2.0.0-beta.3"
  }
}

а затем использовалworkspacesсоответствоватьpackagesкаталог, зависимости будут помещены в корневой каталогnode-modules, а не под каждым компонентом, чтобы можно было повторно использовать одни и те же зависимости, а структура каталогов была более четкой.

  // package.json
  "workspaces": [
    "packages/*"
  ]

Сценарий element-plus также предоставляет сценарий оболочки для создания основных файлов при разработке новых компонентов с использованиемnpm run genБазовая папка компонента может быть создана в пакетах.

"gen": "bash ./scripts/gc.sh",

наконец

У element-plus каждый день появляется больше коммитов, а некоторые функции все еще улучшаются Мы можем изучить дизайн компонентов и новые функции Vue3, прочитав исходный код библиотеки компонентов, а также мы можем внести свой вклад в официальный запрос на извлечение.

Рекомендуемое чтение