前言
一个即将35岁退休的前端老白兔,玩点非工作内容。看前提醒:本文内容未经过生产测试,纯属学习娱乐 。废话不多说,直接开干
正文
1. 技术选型
| 技术 | 描述 |
|---|---|
| Vue3.0 | ~ |
| Esbuild | 构建工具 |
| Typescript | 脚本 |
| Sass | Css预处理 |
| Postcss | Css代码转换工具 |
| Eslint | 语法规则和代码风格的检查工具 |
2. 目录结构
...
packages --------------- 组件库源码
- assets -------------- scss、font文件
- components ---------- 组件目录
- Button ------------- 组件目录
- index.ts ---------- 组件注册入口
- src --------------- 组件源码
- index.ts ----------- 组件库打包入口文件
examples -------------- 开发预览页面源码
- main.ts ------------ 预览页入口文件
- App.vue ------------ 预览页入口组件
- index.html --------- 预览页htlm模板
types ----------------- ts声明文件目录
.editorconfig --------- 编辑器配置文件
.eslintignore --------- eslint检查忽略目录
.eslintrc ------------- eslint检查配置文件
esbuild.config.js ------ esbuild配置文件
tsconfig.json --------- ts编译配置文件
components.js --------- 单组件映射表
package.json ---------- npm项目配置文件
...
3. 环境搭建
- 创建文件夹并进行npm初始化
npm init - 写入
「package.json」文件
{
"name": "fa-ui",
"version": "1.0.0",
"main": "lib/fa-ui.js",
"module": "lib/fa-ui.js",
"files": [
"lib",
"types",
"packages"
],
"types": "types/index.d.ts",
"type": "module",
"license": "MIT",
"author": "fallen",
"description": "description",
"scripts": {
"serve": "npm run clean && node esbuild.config.js serve",
"build": "npm run clean && node esbuild.config.js",
"clean": "rimraf ./lib ./examples/static",
"lint": "eslint --ignore-path .eslintignore .",
"lint:fix": "eslint --fix --ignore-path .eslintignore .",
"dts": "vue-tsc --declaration --emitDeclarationOnly",
"dts:check": "vue-tsc --noEmit"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.36.1",
"@typescript-eslint/parser": "^5.36.1",
"@vue/compiler-sfc": "^3.2.38",
"autoprefixer": "^10.4.8",
"browser-sync": "^2.27.10",
"esbuild": "^0.15.6",
"esbuild-plugin-filesize": "^0.3.0",
"esbuild-plugin-progress": "^1.0.1",
"esbuild-plugin-vue": "^0.2.4",
"esbuild-sass-plugin": "^2.3.2",
"eslint": "^8.23.0",
"eslint-plugin-vue": "^9.4.0",
"postcss": "^8.4.16",
"postcss-import": "^15.0.0",
"postcss-minify": "^1.1.0",
"postcss-preset-env": "^7.8.0",
"pre-commit": "^1.2.2",
"rimraf": "^3.0.2",
"sass": "^1.54.8",
"typescript": "^4.8.2",
"vue": "^3.2.38",
"vue-tsc": "^0.40.6"
},
"pre-commit": [
"lint"
]
}
- 写入后执行安装
npm install
4. eslint配置
.eslintrc 文件配置(配置根据自己需要进行调整)
{
"root": true,
"env": {
"node": true,
"browser": true
},
"globals": {
"window": true,
"process": true
},
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": "@typescript-eslint/parser",
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:vue/vue3-recommended"
],
"rules": {
"no-tabs": "off",
"indent": ["error", 2],
"quotes": ["error", "single"],
"comma-dangle": "error",
"semi": ["error", "never"],
"comma-spacing": "error",
"key-spacing": "error",
"keyword-spacing": "error",
"arrow-spacing": "error",
"block-spacing": ["error", "always"],
"object-curly-spacing": ["error", "always"],
"switch-colon-spacing": "error",
"space-before-blocks": "error",
"space-before-function-paren": "error",
"spaced-comment": "error",
"no-trailing-spaces": "error",
"space-infix-ops": ["error", { "int32Hint": false }],
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/ban-types": "off",
"vue/require-default-prop": "off",
"vue/require-explicit-emits": "off"
}
}
5. esbuild配置
编辑 esbuild.config.js 文件
import { build } from 'esbuild'
import vue from 'esbuild-plugin-vue'
import { sassPlugin } from 'esbuild-sass-plugin'
import progress from 'esbuild-plugin-progress'
import { esbuildPluginFileSize } from 'esbuild-plugin-filesize'
import postcss from 'postcss'
import autoprefixer from 'autoprefixer'
import postcssPresetEnv from 'postcss-preset-env'
import postcssImport from 'postcss-import'
import browserSync from 'browser-sync'
import components from './components.js'
// 判断当前环境
const isServe = process.argv.includes('serve')
// 包名
const libraryName = 'fa-ui.js'
// 封装esbuild本地服务插件
function servePlugin (serveOptions = {}) {
// 创建服务实例
const bs = browserSync.create('dev-server')
return {
name: 'devServer',
setup (build) {
build.onEnd(() => {
// 避免重复启动服务
if (!bs.active) {
// 初始化服务
bs.init({
port: 3000,
watch: true,
open: true,
...serveOptions
})
}
})
}
}
}
// 打包组件库
async function buildLibrary () {
// 全量打包
await build({
entryPoints: ['packages/index.ts'],
outfile: `lib/${libraryName}`,
bundle: true,
format: 'esm',
tsconfig: 'tsconfig.json',
treeShaking: true,
minify: true,
external: ['vue'],
loader: {
'.eot': 'file',
'.svg': 'file',
'.ttf': 'file',
'.woff': 'file'
},
plugins: [
sassPlugin({
async transform (source) {
const { css } = await postcss([
autoprefixer,
postcssPresetEnv(),
postcssImport()
]).process(source, { from: undefined })
return css
}
}),
vue(),
progress(),
esbuildPluginFileSize()
]
})
// 打包单文件组件
await build({
entryPoints: Object.values(components),
outdir: 'lib/components',
bundle: true,
format: 'esm',
tsconfig: 'tsconfig.json',
treeShaking: true,
minify: true,
external: ['vue'],
loader: {
'.eot': 'dataurl',
'.svg': 'dataurl',
'.ttf': 'dataurl',
'.woff': 'dataurl'
},
plugins: [
sassPlugin({
async transform (source) {
const { css } = await postcss([
autoprefixer,
postcssPresetEnv(),
postcssImport()
]).process(source, { from: undefined })
return css
}
}),
vue(),
progress()
]
})
}
// 打包预览页面
function buildExamples () {
build({
entryPoints: ['examples/main.ts'],
outdir: 'examples/static',
bundle: true,
tsconfig: 'tsconfig.json',
format: 'iife',
watch: true,
sourcemap: true,
loader: {
'.eot': 'file',
'.svg': 'file',
'.ttf': 'file',
'.woff': 'file'
},
plugins: [
sassPlugin({
async transform (source) {
const { css } = await postcss([
autoprefixer,
postcssPresetEnv(),
postcssImport()
]).process(source, { from: undefined })
return css
}
}),
vue(),
progress(),
servePlugin({
server: 'examples'
})
]
})
}
// 启动函数
async function start () {
if (isServe) {
buildExamples()
} else {
buildLibrary()
}
}
start()
6. 创建组件
- 创建
/packages/components/Button文件夹 Button文件夹下面创建index.ts「组件局部注册入口」 和src/index.vue「组件源码」- 创建
/packages/index.ts「全局注册入口文件」 - 创建
/packages/assets/scss/variable.scss「统一管理scss变量」 - 创建
/packages/assets/scss/index.scss「scss变量入口」 - 创建
/packages/assets/scss/index.scss「组件样式」,根据需要调整
文件目录大致如下:
7. 组件编写与注册
组件编写
编辑组件源码文件/packages/components/Button/src/index.vue
<template>
<button
class="fa-button"
:class="classNames"
@click="handleClick"
>
<span
v-if="slots.icon"
class="fa-button__icon"
>
<slot name="icon" />
</span>
<span
v-if="slots.default"
class="fa-button__inner"
>
<slot />
</span>
</button>
</template>
<script lang="ts" setup>
import { withDefaults, useSlots, reactive } from 'vue'
type ButtonTypes = 'default' | 'text'| 'primary' | 'success' | 'warning' | 'info' | 'danger' | undefined
type ButtonSize = 'medium' | 'small' | 'mini' | undefined
const props = withDefaults(defineProps<{
type?: ButtonTypes
disabled?: boolean
round?: boolean
plain?: boolean
circle?: boolean
size?: ButtonSize
}>(), {
type: 'default',
size: 'medium',
disabled: false,
round: false,
plain: false,
circle: false
})
const classNames = reactive({
'fa-button--text': props.type === 'text',
'fa-button--primary': props.type === 'primary',
'fa-button--success': props.type === 'success',
'fa-button--warning': props.type === 'warning',
'fa-button--info': props.type === 'info',
'fa-button--danger': props.type === 'danger',
'is-disabled': props.disabled,
'is-plain': props.plain,
'is-circle': props.circle,
'is-round': props.round,
[`size--${props.size}`]: !!props.size
})
const emit = defineEmits(['click'])
const slots = useSlots()
const handleClick = (e: Event) => {
if (props.disabled) {
return false
}
emit('click', e)
}
</script>
样式/packages/scss/button.scss
@import './variable';
/*
variable.scss代码如下
$--color-primary: #409EFF !default;
$--color-success: #67C23A !default;
$--color-warning: #E6A23C !default;
$--color-danger: #F56C6C !default;
$--color-info: #909399 !default;
$--color-border--default: #DCDFE6 !default;
$--color-font--default: #303133 !default;
$--font-size--mini: 12px !default;
$--font-size--small: 13px !default;
$--font-size--normal: 14px !default;
*/
@mixin button-hover($--font-color, $--background-color, $--border-color) {
&:hover {
color: $--font-color;
background-color: $--background-color;
border-color: $--border-color;
}
}
@mixin button-is-plain(
$--font-color--default,
$--background-color--default,
$--border-color--default,
$--font-color--hover,
$--background-color--hover,
$--border-color--hover
) {
&.is-plain {
color: $--font-color--default;
background-color: $--background-color--default;
border-color: $--border-color--default;
@include button-hover($--font-color--hover, $--background-color--hover, $--border-color--hover);
}
}
.fa-button {
border: {
width: 1px;
style: solid;
color: $--color-border--default;
}
line-height: 1;
border-radius: 4px;
color: $--color-font--default;
display: inline-block;
box-sizing: border-box;
padding: 12px 20px;
font-size: $--font-size--normal;
transition: all ease-in .2s;
background-color: #fff;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
white-space: nowrap;
text-align: center;
&.size--medium {
padding: 10px 20px;
}
&.size--small {
font-size: $--font-size--small;
padding: 9px 15px;
border-radius: 3px;
}
&.size-mini {
font-size: $--font-size--mini;
padding: 7px 15px;
border-radius: 3px;
}
&.is-disabled {
opacity: .5;
cursor: not-allowed;
}
&.is-round {
border-radius: 50px;
}
&.is-circle {
border-radius: 50%;
padding: 12px;
}
@include button-hover(
$--color-primary,
rgba($--color-primary, .2),
$--color-primary
);
@include button-is-plain(
$--color-font--default,
#fff,
$--color-border--default,
$--color-primary,
#fff,
$--color-primary
);
&--text {
border: 0;
color: $--color-primary;
@include button-hover(
lighten($--color-primary, 10%),
#fff,
#fff
);
}
&--primary {
background-color: $--color-primary;
color: #fff;
border-color: $--color-primary;
@include button-hover(#fff, lighten($--color-primary, 10%), $--color-primary);
@include button-is-plain(
$--color-primary,
rgba($--color-primary, .2),
$--color-primary,
#fff,
$--color-primary,
$--color-primary
);
}
&--success {
background-color: $--color-success;
color: #fff;
border-color: $--color-success;
@include button-hover(#fff, lighten($--color-success, 10%), $--color-success);
@include button-is-plain(
$--color-success,
rgba($--color-success, .2),
$--color-success,
#fff,
$--color-success,
$--color-success
);
}
&--warning {
background-color: $--color-warning;
color: #fff;
border-color: $--color-warning;
@include button-hover(#fff, lighten($--color-warning, 10%), $--color-warning);
@include button-is-plain(
$--color-warning,
rgba($--color-warning, .2),
$--color-warning,
#fff,
$--color-warning,
$--color-warning
);
}
&--danger {
background-color: $--color-danger;
color: #fff;
border-color: $--color-danger;
@include button-hover(#fff, lighten($--color-danger, 10%), $--color-danger);
@include button-is-plain(
$--color-danger,
rgba($--color-danger, .2),
$--color-danger,
#fff,
$--color-danger,
$--color-danger
);
}
&--info {
background-color: $--color-info;
color: #fff;
border-color: $--color-info;
@include button-hover(#fff, lighten($--color-info, 10%), $--color-info);
@include button-is-plain(
$--color-info,
rgba($--color-info, .2),
$--color-info,
#fff,
$--color-info,
$--color-info
);
}
&+.fa-button {
margin-left: 10px;
}
.fa-button__icon {
line-height: 1;
&+.fa-button__inner {
margin-left: 5px;
vertical-align: baseline;
}
}
}
局部注册
在/packages/components/Button/index.ts文件中编辑如下代码:
import { App } from 'vue'
import Button from './src/FaButton.vue'
import '../../assets/scss/button.scss'
Button.name = 'FaButton'
Button.install = (app: App) => {
app.component(Button.name, Button)
}
export default Button
全局注册
在/packages/index.ts文件中编辑如下代码:
import { App } from 'vue'
import Button from './components/Button'
import './assets/scss/index.scss'
const components = [
Button
]
// 全局注册
const install = (app: App) => {
components.forEach(component => {
app.component(component.name, component)
})
}
export {
Button,
install
}
export default {
install
}
致此,我们就完成了组件库源码部分的搭建开发,接下来我们还要需要一个预览测试组件功能的页面
8. 组件预览及功能测试
- 创建
/examples/App.vue - 创建
/examples/main.ts - 创建
/examples/index.html
预览页面编辑
main.ts
import { createApp } from 'vue'
import FaUI from '../packages'
import App from './App.vue'
const app = createApp(App)
app.use(FaUI)
app.mount('#app')
App.vue
<template>
<div class="btns">
<fa-button>
default
</fa-button>
<fa-button type="text">
text
</fa-button>
<fa-button type="success">
success
</fa-button>
<fa-button type="danger">
danger
</fa-button>
<fa-button type="info">
info
</fa-button>
<fa-button type="primary">
primary
</fa-button>
</div>
<div class="btns">
<fa-button plain>
default
</fa-button>
<fa-button
type="text"
plain
>
text
</fa-button>
<fa-button
type="success"
plain
>
success
</fa-button>
<fa-button
type="danger"
plain
>
danger
</fa-button>
<fa-button
type="info"
plain
>
info
</fa-button>
<fa-button
type="primary"
plain
>
primary
</fa-button>
</div>
</template>
<script lang="ts" setup>
</script>
<style>
.btns {
margin-bottom: 10px;
}
</style>
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>fa-ui</title>
<link rel="stylesheet" href="./static/main.css">
</head>
<body>
<div id="app"></div>
<script src="./static/main.js"></script>
</body>
</html>
9. 本地开发和生产打包
本地开发
npm run serve
# or
yarn serve
生产打包
npm run build
# or
yarn build
结束语
至此,《Vue3 + Esbuild + Typescript搭建组件库》的全流程结束。再次强调,本文内容未经过生产测试,纯属学习娱乐,如有错误还请指正,大家一起学习进步,感谢大家阅读~~~