Как стать автором
Обновить

Как создать и развернуть библиотеку компонентов Vue в NPM

Время на прочтение7 мин
Количество просмотров7.2K
Автор оригинала: Adesoji Temitope

Эта статья — перевод оригинальной статьи Adesoji Temitope "How to Create and Deploy a Vue Component Library to NPM"

Также я веду телеграм канал “Frontend по-флотски”, где рассказываю про интересные вещи из мира разработки интерфейсов.

Вступление

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

  • Создание библиотеки компонентов Vue.

  • Регистрация компонентов библиотеки.

  • Настройка процесса сборки.

  • Локальное тестирование, а затем публикация в npm.

Создание библиотеки компонентов Vue

Настройка проекта

Начнем с создания нашего проекта Vue. Мы будем использовать yarn для управления пакетами.

Для начала запустим:

npm init

Для этого урока мы собираемся создать только один компонент в нашей библиотеке компонентов. Одна из вещей, которую следует учитывать при создании библиотеки в масштабе, — это разрешить импорт только отдельных компонентов, чтобы активировать tree shaking.

Мы будем использовать такую структуру папок:

- src /
  - components /
    - button /
      - button.vue
      - index.ts

    - index.ts

  - styles /
    - components /
        - _button.scss

    - index.scss

- Package.json
- rollup.config.js

Создание компонента кнопки

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

Добавьте это в файл button.vue.

<!-- src/components/button/button.vue -->
<template>
    <button
        v-bind="$attrs"
        :class="rootClasses"
        :type="type"
        :disabled="computedDisabled"
    >
      <slot></slot>
    </button>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
    name: 'DSButton',
    inheritAttrs: false,
    props: {
        /**
         * disabled status
         * @values true, false
         */
        disabled: {
            type: Boolean,
        },
        /**
        * Color of button
        * @values primary, secondary
        */
        variant: {
            type: String,
            validator: (value: string) => {
                return [
                    'primary',
                    'secondary'
                ].indexOf(value) >= 0
            }
        },
        /**
         * type of button
         * @values button, submit
         */
        type: {
            type: String,
            default: 'button',
            validator: (value: string) => {
                return [
                    'button',
                    'submit',
                    'reset'
                ].indexOf(value) >= 0
            }
        },
        /**
         * Size of button
         * @values sm, md, lg
         */
        size: {
            type: String,
            validator: (value: string) => {
                return [
                    'sm',
                    'md',
                    'lg'
                ].indexOf(value) >= 0
            }
        }
    },
    computed: {
        rootClasses() {
            return [
                'ds-button',
                'ds-button--' + this.size,
                'ds-button--' + this.variant
            ]
        },
        computedDisabled() {
            if (this.disabled) return true
            return null
        }
    }
})
</script>

Давайте стилизуем компонент, добавим классы вариантов и размеров на основе того, что указано в файле.

Добавьте это в файл _button.scss.

// src/styles/components/_button.scss
$primary: '#0e34cd';
$secondary: '#b9b9b9';
$white: '#ffffff';
$black: '#000000';
$small: '.75rem';
$medium: '1.25rem';
$large: '1.5rem';

.ds-button {
    position: relative;
    display: inline-flex;
    cursor: pointer;
    text-align: center;
    white-space: nowrap;
    align-items: center;
    justify-content: center;
    vertical-align: top;
    text-decoration: none;
    outline: none;

    // variant
    &--primary {
        background-color: $primary;
        color: $white;
    }
    &--secondary {
        background-color: $secondary;
        color: $black;
    }

    // size
    &--sm {
        min-width: $small;
    }
    &--md {
        min-width: $medium;
    }
    &--lg {
        min-width: $large;
    }
}

Регистрация компонентов библиотеки

Далее нам нужно зарегистрировать наши компоненты. Для этого мы импортируем все наши компоненты в один файл и создаем наш метод установки.

src/components/button/index.ts Этот файл экспортирует методы установки по умолчанию. В тех случаях, когда нам нужно импортировать только этот компонент кнопки в другие наши проекты, он также экспортирует кнопку, которую мы будем использовать чуть позже.

// src/components/button/index.ts
import { App, Plugin } from 'vue'

import Button from './button.vue'

export default {
    install(Vue: App) {
        Vue.component(Button.name, Button)
    }
} as Plugin

export {
    Button as DSButton
}

src/components/index.ts Давайте импортируем сюда все компоненты из нашей папки компонентов. Поскольку у нас есть только наш компонент кнопки, мы импортируем его.

// src/components/index.ts
import Button from './button'

export {
    Button
}

src/styles/index.scss Позволяет импортировать все стили компонентов в index.scss в нашей папке стилей. Это помогает нам иметь один источник экспорта для всех наших стилей.

// src/styles/index.scss
@import "components/_button";

src/index.ts Давайте импортируем все компоненты в index.ts в нашей папке src. Здесь мы создаем наш метод установки для всех компонентов. Мы экспортируем DSLibrary по умолчанию, а также экспортируем все наши компоненты.

// src/index.ts
import { App } from 'vue'

import * as components from './components'

const DSLibrary = {
    install(app: App) {
        // Auto import all components
        for (const componentKey in components) {
            app.use((components as any)[componentKey])
        }
    }
}

export default DSLibrary

// export all components as vue plugin
export * from './components'

Давайте создадим файл с именем shim-vue.d.ts, чтобы помочь нам импортировать файлы Vue в наши файлы TypeScript и удалить все вызванные им ошибки линтинга.

// src/shim-vue.d.ts
declare module '*.vue' {
  import type { DefineComponent } from 'vue';
  const component: DefineComponent<{}, {}, any>;
  export default component;
}

Настройка процесса сборки

При сборке библиотеки нам нужно создать пакет и минификацию библиотеки, которая будет использоваться совместно с npm, для этого мы будем использовать Rollup. Rollup — это сборщик модулей для JavaScript, который компилирует небольшие фрагменты кода в нечто большее.

Установите rollup и все необходимые модули.

npm i -D rollup rollup-plugin-vue rollup-plugin-terser rollup-plugin-typescript2 @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-node-resolve

Сборка компонента (Код Vue)

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

  • ES module

  • CommonJS

Давайте создадим файл rollup.config.js в корневой папке и вставим это туда.

import { text } from './build/banner.json'
import packageInfo from './package.json'

import vue from 'rollup-plugin-vue'
import node from '@rollup/plugin-node-resolve'
import cjs from '@rollup/plugin-commonjs'
import babel from '@rollup/plugin-babel'
import { terser } from 'rollup-plugin-terser'
import typescript from 'rollup-plugin-typescript2';

import fs from 'fs'
import path from 'path'

const baseFolderPath = './src/components/'
const banner = text.replace('${version}', packageInfo.version)

const components = fs
    .readdirSync(baseFolderPath)
    .filter((f) =>
        fs.statSync(path.join(baseFolderPath, f)).isDirectory()
    )

const entries = {
    'index': './src/index.ts',
    ...components.reduce((obj, name) => {
        obj[name] = (baseFolderPath + name)
        return obj
    }, {})
}

const babelOptions = {
    babelHelpers: 'bundled'
}

const vuePluginConfig = {
    template: {
        isProduction: true,
        compilerOptions: {
            whitespace: 'condense'
        }
    }
}

const capitalize = (s) => {
    if (typeof s !== 'string') return ''
    return s.charAt(0).toUpperCase() + s.slice(1)
}

export default () => {
    let config = []

    if (process.env.MINIFY === 'true') {
        config = config.filter((c) => !!c.output.file)
        config.forEach((c) => {
            c.output.file = c.output.file.replace(/.m?js/g, r => `.min${r}`)
            c.plugins.push(terser({
                output: {
                    comments: '/^!/'
                }
            }))
        })
    }
    return config
}

Далее мы создаем папку сборки в корне нашего проекта и добавляем в нее файл с именем banner.json. Мы хотим, чтобы наши сборки содержали текущую версию приложения каждый раз, когда мы делаем сборку. Этот файл уже импортирован в файл rollup.config.js, и мы используем версию пакета из нашего package.json для обновления версии.

{
    "text": "/*! DS Library v${version} */
"
}

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

entries: путь к файлам, которые мы хотим объединить в пакет.

external: необходим сторонний пакет Vue.

output.format: формат файла на выходе

output.dir: директория для бандла

output.banner: текст, добавленный в начало файла

plugins: указать методы, используемые для настройки rollup

Сначала мы создаем сборку esm для каждого компонента в нашей библиотеке:

config = [{
  input: entries,
  external: ['vue'],
  output: {
     format: 'esm',
     dir: `dist/esm`,
     entryFileNames: '[name].mjs',
     chunkFileNames: '[name]-[hash].mjs',
  },
  plugins: [
      node({
          extensions: ['.vue', '.ts']
      }),
      typescript({
          typescript: require('typescript')
      }),
      vue(vuePluginConfig),
      babel(babelOptions),
      cjs()
   ],
}],

Далее мы создаем единую сборку esm для всех компонентов:

config = [
    ...,
    {
       input: 'src/index.ts',
       external: ['vue'],
       output: {
           format: 'esm',
           file: 'dist/ds-library.mjs',
           banner: banner
       },
       plugins: [
           node({
               extensions: ['.vue', '.ts']
           }),
           typescript({
               typescript: require('typescript')
           }),
           vue(vuePluginConfig),
           babel(babelOptions),
           cjs()
       ]
   }
],

Затем мы создаем сборку cjs для каждого компонента в нашей библиотеке:

config = [
  ...,
  ...,
 {
     input: entries,
     external: ['vue'],
     output: {
         format: 'cjs',
         dir: 'dist/cjs',
         exports: 'named'
     },
     plugins: [
         node({
             extensions: ['.vue', '.ts']
         }),
         typescript({
             typescript: require('typescript')
         }),
         vue(vuePluginConfig),
         babel(babelOptions),
         cjs()
    ]
 }
],

После этого мы создаем единую сборку cjs для каждого компонента в нашей библиотеке.

config = [
  ...,
  ...,
  ...,
 {
     input: 'src/index.ts',
     external: ['vue'],
     output: {
         format: 'umd',
         name: capitalize('ds-library'),
         file: 'dist/ds-library.js',
         exports: 'named',
         banner: banner,
         globals: {
             vue: 'Vue'
         }
     },
     plugins: [
         node({
             extensions: ['.vue', '.ts']
         }),
         typescript({
             typescript: require('typescript')
         }),
         vue(vuePluginConfig),
         babel(babelOptions),
         cjs()
     ]
 }
],

Наконец, мы обновляем наш package.json с помощью нашей команды скрипта. Нам нужны как rimraf (чтобы удалить нашу старую папку dist перед созданием нового пакета), так и clean-css (чтобы минимизировать наш файл css), поэтому давайте установим:

npm i -D rimraf clean-css-cli

теперь давайте обновим наш скрипт package.json

build:vue = rollup и минификацияbuild:style = объединил наши стили из scss в css, добавил текст нашего баннера (номер версии, как мы сделали выше) и сохранил в dist/ds-library.css, а затем создал уменьшенную версию.

Для текста баннера в нашем файле css нам нужно создать файл print-banner.js внутри папки сборки. Он берет текст нашего баннера и записывает его в файл.

// build/print-banner.js
const packageInfo = require('../package.json')
const { text } = require('./banner.json')

process.stdout.write(text.replace('${version}', packageInfo.version))
process.stdin.pipe(process.stdout)

build:lib = удалить папку dist, собрать код vue и стили, publish:lib = запустить нашу команду build lib, а затем опубликовать в npm

"scripts": {
    "build:vue": "rollup -c && rollup -c --environment MINIFY",
    "build:vue:watch": "rollup -c --watch",
    "build:style": "sass --no-charset ./src/styles/index.scss | node ./build/print-banner.js > dist/ds-library.css && cleancss -o dist/ds-library.min.css dist/ds-library.css",
    "build:lib": "rimraf dist && npm run build:vue && npm run build:style",
    "publish:lib": "npm run build:lib && npm publish"
},
"peerDependencies": {
   "vue": "^3.0.0"
},

Локальное тестирование, а затем публикация в npm

Теперь, когда мы закончили, мы можем протестировать локально, запустив npm link в корневом каталоге этого репозитория, а также запустив npm link «имя пакета» в корневом каталоге нашего тестового проекта. После этого пакет будет доступен для использования в нашем тестовом пакете.

После тестирования вы можете запустить npm publish:lib для сборки и развертывания в npm.

Теги:
Хабы:
Всего голосов 3: ↑3 и ↓0+3
Комментарии4

Публикации

Истории

Работа

Ближайшие события