Эта статья — перевод оригинальной статьи 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)
Green21
23.07.2022 17:05А как потом скачать этот пакет/компонент и использовать в новом проекте? Дополнили бы статью примером использования - опубликовали бы его уже и кинули ссылку на npm. Создали бы новый проект с этим компонентом. На vue sfc playground ссылочкой тоже неплохо было бы поделиться чтобы воочию увидеть компонент.
А вообще английский такого уровня это маст хэв. Для кого интересно такие переводы?!
St_Mikhail
Боже, как же всё заморенно.
NX + Angular в 3 CLI команды уместится (кроме самого кода компонента).
Если кому интересно, могу аналог на Ангуляре в виде статьи оформить
eshimischi
Я лично предпочту связку PNPM + Turbo для описанного, а вообще NX тоже можно использовать хоть с vue, хоть с react, хоть с angular