前言
感觉从进入前端后,做的都是ToB的产品,要不然就是大屏,每次在写那些重复性的表格表单的时候就觉得时间过得好快,感觉一天啥也没做就结束了,但是专注在写重复性代码里好开心啊,这样就不用动脑了!但是今年,就特别想动脑子了,先是用vue3 + antD搞了个给家里用的进销存管理系统前端部分,然后现在又开始琢磨弄一个常用的组件库。所以接下来就开始讲讲我整个搭建的过程吧!
创建组件库项目
基于vue3 + vite + Element Plus 技术框架,适用于PC端。
1. 安装vite脚手架
npm create vite@latest
在选择 variant 的时候,可以选择
customize with create-vue,相当于自定义安装,可以选择安装ESLint和Prettier,增加代码的规范,我是为了快就直接选择了JavaScript。
2. 改造项目结构
为了区分实际使用的包文件和测试文件
在根目录下新建packages目录,将原有的src目录修改为examples
由于项目的入口文件是src下的main.ts文件,所以需要将index.html文件里的地址修改为:
<script type="module" src="/examples/main.ts"></script>
3. 安装UI框架和npm依赖包
npm install element-plus --save
npm i
在main.ts里引入Element Plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import request from "@/api";
app.use(ElementPlus, {});
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
4. 编写组件
PS:如果你不想看写的过程,可以直接🚀dynamicModule使用文档
- 在packages目录下新建table目录、index.js文件
- 在table目录下新建table.vue和index.js文件
<template>
<div class="container">
<div class="table-control">
<div v-for="item in tableItems.tableSetting" :key="item.code" class="control-item">
<el-button
v-if="btIncludes.includes(item.btType)"
:type="item.type"
:link="item.btType === 'text'"
:plain="item.btType === 'secondary'"
:text="item.btType === 'threeLevel'"
:bg="item.btType === 'threeLevel'"
@click.stop="handleTableControl(item.label)"
>{{ item.label }}</el-button>
<el-button v-if="item.btType === 'iconTextBt'" :type="item.type"
@click.stop="handleTableControl(item.label)">
<img :src="getAssetsFile(`${item.icon}.png`)" alt="" class="icon-size" />{{ item.label }}
</el-button>
<el-tooltip
v-if="item.btType === 'iconBt'"
class="box-item"
:effect="item.effect"
:content="item.label"
:placement="item.placement">
<el-button link @click.stop="handleTableControl(item.label)">
<img :src="getAssetsFile(`${item.icon}.png`)" alt="" class="icon-size" />
</el-button>
</el-tooltip>
<el-tooltip
v-if="item.btType === 'listSet'"
class="box-item"
:effect="item.effect"
content="列设置"
:placement="item.placement"
>
<div class="checkbox-box" @click.stop="openPopover">
<el-popover v-model:visible="listSetting.visible" placement="right" :width="160" trigger="click">
<template #reference>
<el-button link>
<img :src="getAssetsFile(`${item.icon}.png`)" alt="" class="icon-size" />
</el-button>
</template>
<el-checkbox
v-model="listSetting.checkAll"
:indeterminate="listSetting.isIndeterminate"
@change="handleCheckAllChange"
>列展示</el-checkbox
>
<el-divider />
<el-checkbox-group v-model="listSetting.checkedColum" @change="handleCheckedChange">
<el-checkbox
v-for="colum in listSetting.columList"
:key="colum.code"
:label="colum.label"
:disabled="colum.isChecked"
>
{{ colum.label }}
</el-checkbox>
</el-checkbox-group>
</el-popover>
</div>
</el-tooltip>
</div>
</div>
<div class="table-wrap">
<el-table
ref="multipleTableRef"
:data="tableItems.dataSource"
@selection-change="handleSelectionChange"
stripe style="width: 100%"
>
<el-table-column v-if="tableItems,showSelection" type="selection" fixed width="55" />
<el-table-column type="index" width="70" label="序号" fixed :index="indexMethod" />
<template v-for="(item, index) in tableItems.columns" :key="index">
<el-table-column
v-if="item.show && !item.filters"
:prop="item.value"
:label="item.label"
:width="item.width"
:show-overflow-tooltip="item.showOverflow"
:fixed="item.showFixed"
:sortable="item.sortable"
>
<template #default="scope">
<span v-if="item.value === 'control'" v-for="(val, num) in tableItems.tableRowHandlers" :key="num">
<el-button link type="primary" size="small" @click="handleClick(val.type, scope.row)">{{val.label}}</el-button>
</span>
<span v-else-if="item.showImage">
<el-image :src="scope.row[`${item.value}`]" fit="scale-down" />
</span>
<span v-else>{{ scope.row[`${item.value}`] }}</span>
</template>
</el-table-column>
<el-table-column
v-if="item.filters"
:prop="item.value"
:label="item.label"
:width="item.width"
:show-overflow-tooltip="item.showOverflow"
:fixed="item.showFixed"
:filters="item.filters"
:filter-method="filterHandler">
<template #default="scope">
<span>{{ scope.row[`${item.value}`] }}</span>
</template>
</el-table-column>
</template>
</el-table>
<div class="pagination">
<el-pagination
v-model:current-page="tableItems.pagination.currentPage"
v-model:page-size="tableItems.pagination.pageSize"
:small="true"
:background="true"
layout="total, prev, pager, next, jumper"
:total="tableItems.pagination.total"
@current-change="handleCurrentChange"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts" name="DynamicTable">
import { reactive, ref, watch } from "vue";
import { TableColumnCtx } from 'element-plus';
const props = defineProps({
tableItems: {
type: Object,
require: true,
default: () => {
return {};
}
}
});
const emit = defineEmits(['handleTableControl']);
let tableColumns = reactive([]);
const listSetting = reactive({
visible: false,
checkAll: true,
isIndeterminate: false,
columList: [],
checkedColum: [] as any
});
const multipleSelection = ref([]);
const btIncludes = ['basic', 'secondary', 'threeLevel', 'text']
watch(
() => props.tableItems,
(val) => {
if (val) {
dataRender();
}
},
{ deep: true }
)
/**
* 获取assets静态资源
* @param url
*/
const getAssetsFile = (url: string) => {
return new URL(`../../examples/assets/icons/${url}`, import.meta.url).href;
}
/**
* 分页后序号连续
* @param index
*/
const indexMethod = (index: any) => {
const { currentPage, pageSize } = tablePagination;
return index + 1 + (currentPage - 1) * pageSize;
}
/**
* 分页跳转
* @param val
*/
const handleCurrentChange = (val: number) => {
const params = {
type: 'pagination',
value: val
}
emit('handleTableControl', params);
}
const dataRender = () => {
const { columns } = props.tableItems;
tableColumns = [ …columns ];
listSetting.columList = tableColumns;
multipleSelection.value = [];
showAllColum();
}
/**
* 表格操作事件
* @param type
*/
const handleTableControl = (type: string) => {
const params = {
type: type
}
if (type === '删除') {
params['value'] = multipleSelection.value;
}
emit('handleTableControl', params);
}
/**
* 表格数据操作
* @param type: 操作的类型
*/
const handleClick = (type: string, row: any) => {
const params = {
type: type,
value: row
}
emit('handleTableControl', params);
}
const handleSelectionChange = (val: any) => {
multipleSelection.value = val;
}
const filterHandler = (
value: string,
row: any,
column: TableColumnCtx<any>
) => {
const property = column['property']
return row[property] === value
}
// 展开列设置
const openPopover = () => {
listSetting.visible = true;
};
const showAllColum = () => {
listSetting.checkedColum = listSetting.columList.map((item: any) => {
if (item.show) {
return item.label;
}
});
};
/**
* 列全选
* @param val
*/
const handleCheckAllChange = (val: boolean) => {
if (val) {
tableColumns.map((item: any) => {
item.show = true;
});
} else {
tableColumns.map((item: any) => {
item.show = false;
});
}
showAllColum();
listSetting.isIndeterminate =
tableColumns.length > 0 && tableColumns.length < listSetting.columList.length;
};
/**
* 列选中
* @param value
*/
const handleCheckedChange = (value: string[]) => {
tableColumns.map((item: any) => {
item.show = value.includes(item.label);
});
showAllColum();
const checkedCount = value.length;
listSetting.isIndeterminate = checkedCount > 0 && checkedCount < listSetting.columList.length;
};
</script>
用于导出该组件,name在script标签上,所以获取名字的形参是__name
import DynamicTable from "./dynamic-table.vue";
DynamicTable.install = (App) => {
App.component(DynamicTable.__name, DynamicTable)
}
export default DynamicTable;
examples根目录下的index.js,用于导出所有的组件
import DynamicTable from "./table/dynamic-table.vue";
export { DynamicTable }
const components = [DynamicTable]
const install = (App) => {
components.forEach((item) => {
App.component(item.__name, item)
})
}
export default {
install
}
5. 修改配置项
- 在vite.config.ts修改打包配置
build: {
outDir: 'lib',
lib: {
// 指定组件编译入口文件
entry: resolve(__dirname, 'packages/index.js'),
name: 'Vue3DynamicModule',
fileName: 'vue3-dynamic-module'
},
// 打包配置
rollupOptions: {
external: ['vue'],
output: {
// 在 UMD 构建模式下位这些外部化的依赖提供全局变量
globals: {
vue: 'vue'
}
}
}
}
- 在package.json增加exports对象,最后打包使用的就是这里面的配置
"exports": {
"./lib/style.css": "./lib/style.css",
".": {
"import": "./lib/vue3-dynamic-module.mjs",
"require": "./lib/vue3-dynamic-module.umd.js"
}
}
- 在根目录下添加
.npmignore文件,忽略不需要上传到npm库的文件
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*
# 以下是新增的
# 要忽略目录和指定文件
.vscode
examples/
packages/
public/
vite.config.js
*.map
*.html
6. 在examples进行引入组件测试
新建一个table组件,直接在App.vue文件里引入:
<template>
<DynamicTable
:table-items="tableItems"
:pagination="pagination"
@handle-table-control="handleTableControl"
@data-change="tableChange"
></DynamicTable>
</template>
<script setup lang="ts">
import DynamicTable from "../../packages/table/dynamic-table.vue";
import { reactive } from "vue";
const tableItems = reactive({
header: [
{
label: '名称',
value: 'title',
width: '300',
show: true,
isChecked: true,
showOverflow: true,
showFixed: true,
filters: [
{ text: '巴洛克风盛宴', value: '巴洛克风盛宴' },
{ text: '大弯的生日', value: '大弯的生日' }
]
},
{
label: '图片源',
value: 'copyright',
width: '700',
show: true,
isChecked: false,
showOverflow: true,
showFixed: true
},
{
label: '壁纸',
value: 'url',
width: '200',
show: true,
isChecked: true,
showOverflow: false,
showFixed: false,
showImage: true
},
{
label: '更新时间',
value: 'startdate',
width: '200',
show: true,
isChecked: false,
showOverflow: false,
showFixed: false,
sortable: true
},
{
label: '操作',
value: 'control',
width: 'auto',
show: true,
isChecked: true,
showOverflow: false,
showFixed: false
}
],
tableControl: [
{
code: '3',
btType: 'iconTextBt',
type: 'primary',
label: '新建',
icon: 'add'
},
{
code: '4',
btType: 'iconTextBt',
type: 'danger',
label: '删除',
icon: 'del'
},
{
code: '6',
btType: 'iconBt',
type: 'primary',
label: '刷新',
effect: 'dark',
placement: 'top',
icon: 'refresh'
},
{
code: '7',
btType: 'listSet',
type: 'primary',
label: '列设置',
effect: 'dark',
placement: 'top',
icon: 'set'
}
],
tableData: [],
tableOperations: [
{
type: 'download',
label: '下载原图'
},
{
type: 'edit',
label: '编辑'
}
],
multiple: true
});
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
});
const handleTableControl = (params) => {
const { type, value } = params;
switch (type) {
case '刷新':
tableList();
break;
case '删除':
if (value.length === 0) {
ElMessage.warning('请选择至少一条数据!');
} else {
dialogData.title = '删除';
dialogData.info = '确定将选择的数据删除?';
dialogData.tipsVisible = true;
}
break;
case '新建':
dialogData.title = '新建';
Object.keys(formData).forEach(key => formData[key] = '');
dialogData.formVisible = true;
break;
default:
break;
}
}
const tableChange = (params: {val: number}) => {
const { type, value } = params;
switch (type) {
case 'pagination':
pagination.currentPage = value;
break;
case 'edit':
dialogData.title = '编辑';
formData = { …value }
dialogData.formVisible = true;
break;
default:
break;
}
}
</script>
7. 打包输出lib库
npm run build
输出目录lib,里面包含以下几个文件,
8. 上传到npm
首先你需要有一个npm账号,如果没有可以先去npm官网注册。接着你需要在项目下执行命令npm login,输入npm的用户、密码和邮箱,然后会给你发送一次性密码,接着就可以开始上传你的npm包了。
出现红框里表示已经上传成功了,你可以去npm里查看你的包情况。注意❗ 每次上传的时候都需要修改package.json里面的版本号,否则上传的时候会报错。
组件库源码
组件详细使用说明
🚀dynamicModule使用文档 欢迎大家使用交流,组件还不够完美,但是还在完善的路上...