尝试使用vite+vitest构建你的vue3组件并发布到npm你写的组件有测试用例吗?本文带你从开发组件,到编写测试用
本文内容略长,因此分为以下几点分别介绍:
- 封装el-table组件,除了使用JSON数组来配置表格列其实还有一种更为优雅的封装方式。
- WalTablePagination 组件的实现
- 使用Vite来构建项目,使用vue-test-utils来编写组件测试用例,使用Vitest来运行自动化组件测试程序。
- 将组件发布到npm上。
项目地址: wal-table-pagination
封装el-table组件,除了使用JSON数组来配置表格列其实还有一种更为优雅的封装方式
首先我觉得比较好的封装方式应该具有以下特点:
- 组件使用方式上以及API风格尽可能与el-table的使用方式保持一致,这样可以减少组件的上手难度。(不知道你是否遇到过类似的问题,拿到前同事封装的el-table一看各种奇怪的配置和参数。读懂这些配置已经够累了。)
- 组件的使用能够真正带来效率的提升(可以从上手API的难易程度,代码可维护性,代码可复用性等等角度体现出是否提升效率)
- 是否保留了原有组件的所有功能,比如官方的展开行功能、树形数据和懒加载等等。
说到这里推荐使用JSON配置table列的同学肯定会抬杠了,JSON配置列写法简单可以少写html代码等等! 那好,接下来我用两种写法的代码来对比下让你看得更加直观:
使用JSON配置表格列:
<template>
<WalTablePagination :tableData="tableData" :columns="columns"></WalTablePagination>
</template>
<script>
//列配置
const columns = [
{
label: '审核内容',
prop: 'content',
width: '220px',
slotName: 'content',
align: 'center',
},
{
label: '得分',
prop: 'score',
width: '220px',
slotName: 'score',
align: 'center',
},
{
label: '分类',
prop: 'industry',
width: '220px',
align: 'center',
},
{
label: '违规原因',
prop: 'sensitiveWord',
slotName: 'sensitiveWord',
align: 'left',
minWidth: '220',
},
]
const tableData = ref([])
</script>
我个人比较推荐的封装的使用方式:
<template>
<WalTablePagination :data="tableData" >
<el-table-column prop="content" label="审核内容" />
<el-table-column prop="score" label="得分" />
<el-table-column prop="industry" label="分类" />
<el-table-column prop="sensitiveWord" label="违规原因" />
</WalTablePagination>
</template>
<script>
const tableData = ref([])
</script>
通过刚才的代码写法上的对比很明显的看出,为了少写html代码的做法反而需要写更多的json数组,而且从代码量上对比json列的配置方式一点也没有少写代码!!!
反而第二种方式可以一目了然的看出你的组件最终渲染的html结构,从风格上来讲更加符合原有的el-table组件的写法。整个项目的代码风格保持了一致性,可扩展性更好(支持el-table的所有功能)!
WalTablePagination 组件实现
我期望的组件用法是符合el-table的api并且也自带分页功能。其实这个需求在企业业务需求中是非常高频的。下面是组件用法示例:
<template>
<WalTablePagination
style="width:800px;margin: 20px auto 0px;"
ref="waltablePagination"
v-loading="loading"
:max-height="500"
:data="tableData"
:total="pagination.total"
:current-page="pagination.currentPage"
@filter-change="handleFilterChange"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@pagination-current-change="handlePaginationCurrentChange"
>
<el-table-column type="selection" fixed="left" width="40px" />
<el-table-column prop="date" label="Date" />
<el-table-column prop="name" label="Name" />
<el-table-column
prop="address"
column-key="address"
:filters="[
{ text: 'No. 1 , Grove St, Los Angeles', value: 'No. 1 , Grove St, Los Angeles' },
{ text: 'No. 2 , Grove St, Los Angeles', value: 'No. 2 , Grove St, Los Angeles' },
{ text: 'No. 3 , Grove St, Los Angeles', value: 'No. 3 , Grove St, Los Angeles' }
]"
:filter-method="filterMethod"
min-width="300px"
label="Address" />
<el-table-column label="Operations" fixed="right">
<template #default="{ row }">
<el-button size="small" type="primary" @click="handleEdit(row)">编辑</el-button>
</template>
</el-table-column>
</WalTablePagination>
</template>
WalTablePagination实现原理
- WalTablePagination 作为父组件包裹 el-table、el-pagination 组件。
- 在 WalTablePagination 内部使用 useSlots().default() 接收传入的 el-table-column作为组件的slots处理后并作为 el-table的 slots使用。
- 在 WalTablePagination 内部使用 useAttrs() 接收组件的 props、events并且分发给el-table和el-pagination处理
因此我的 WalTablePagination 组件的html结构如下:
<template>
<div>
<el-table
ref="table"
v-bind="tableAttrs"
@select="select"
@select-all="selectAll"
@selection-change="selectionChange"
@cell-mouse-enter="cellMouseEnter"
@cell-mouse-leave="cellMouseLeave"
@cell-click="cellClick"
@cell-dblclick="cellDblclick"
@cell-contextmenu="cellContextmenu"
@row-click="rowClick"
@row-contextmenu="rowContextmenu"
@row-dblclick="rowDblclick"
@header-click="headerClick"
@header-contextmenu="headerContextmenu"
@sort-change="sortChange"
@filter-change="filterChange"
@current-change="tableCurrentChange"
@header-dragend="headerDragend"
@expand-change="expandChange"
>
<column :key="key" />
</el-table>
<el-pagination
v-bind="paginationAttrs"
@size-change="sizeChange"
@current-change="paginationCurrentChange"
@prev-click="prevClick"
@next-click="nextClick"
/>
</div>
</template>
v-bind 用来接收el-table和el-pagination组件的 props。
接下来就是 column 函数的实现思路:
- column是一个函数式组件并且返回了一个数组(数组是一个由el-table-column组成的vnodes)。
- column 可以对组件传入的 el-table-column 做进一步操作,比如fixed固定,列的显示隐藏等等功能。
- 我们需要对el-table-column 设置了 fixed="left"、fixed="right" 属性的列内容做一个位置固定。
tableSlots
tableSlots 用来保存 WalTablePagination 接收的原始 table 列:
const tableSlots = computed(() => {
const defaults = useSlots().default?.();
const tableLeft = []; //固定到左侧列
const tableRight = []; //固定到右侧列
const contents = []; //没有设置fixed属性的列就放到这里
defaults?.forEach((vnode) => {
if (isElTableColumn(vnode)) {
//vnode.props就是用户传入的props
const { fixed } = vnode.props || {};
if (fixed) {
if (fixed === "left") {
return tableLeft.push(vnode);
} else if (fixed === "right") {
return tableRight.push(vnode);
}
} else {
return contents.push(vnode);
}
}
});
return {
tableLeft,
tableRight,
contents,
};
});
得到了vnodes数组就可以将它返回给el-table去使用啦:
tableColumns
tableColumns 的作用就是对表格列作过滤。比如用户通过动态列的功能来设置列的显示隐藏功能。举个例子。当我们的表格列过多的时候全部展示在table中会变得比较拥挤,因此可以通过列配置来达到显示隐藏对应的表格列。这样用户就可以只看到自己比较关心的数据列。
// 收集动态修改visiable值后的列数据
const tableColumns = reactive({
slot: computed(() =>
tableSlots.value.contents.map(({ props }) => ({
prop: props.prop, // prop
label: props.label, // label
visiable: props.visiable || true, // 默认情况下表格列是可见的,并且通过设置false来隐藏对应的列
}))
),
storage: [],
render: computed(() => {
const slot = [...tableColumns.slot];
const storage = [...tableColumns.storage];
const result = [];
storage.forEach((props) => {
const index = slot.findIndex(({ prop }) => prop === props.prop);
if (index >= 0) {
result.push({
...props,
});
slot.splice(index, 1); // storage 里不存在的列
}
// slot 中没有找到的则会被过滤掉
});
result.push(...slot);
return result;
}),
});
finalSlot
finalSlot 表示最终需要传递给el-table的插槽。它会排除掉设置了visiable=false属性的列内容。
// 最终被呈现的slot
const finalSlot = computed(() => {
const { contents } = tableSlots.value;
const result = [];
tableColumns.render.forEach(({ prop, visiable }) => {
// 如果visiable为false则不渲染
if (!visiable) return;
// 从 slots.contents 中寻找对应 prop 的 VNode
const vnode = contents.find((vnode) => prop === vnode.props?.prop);
if (!vnode) return;
// 克隆 VNode 并修改部分属性
const cloned = cloneVNode(vnode);
result.push(cloned);
});
return result;
});
column
最终column函数的任务就是返回需要渲染的vnodes
const column = () => [tableSlots.value.tableLeft, finalSlot.value, tableSlots.value.tableRight]
WalTablePagination 的完整代码
完整代码见 github WalTablePagination
导出组件,编写组件的install方法
导出组件,编写组件的install方法。这样组件就可以被vue.use调用并注册了
// packages/wal-table-pagination/index.js
import WalTablePagination from "./src/wal-table-pagination.vue";
WalTablePagination.install = function (app) {
app.component(WalTablePagination.name, WalTablePagination);
};
export default WalTablePagination;
使用Vite来构建项目,使用vue-test-utils来编写组件测试用例,使用Vitest来运行自动化组件测试程序
构建项目的话我首选目前有着最快打包体验的Vite。Vite可以让你在开发阶段应用秒启动,从此告别了改一行代码需要等上40多秒的槽糕体验的webpack。
因为需要将项目发布到npm上,所以代码目录最好按照库的打包方式组织:
wal-table-pagination
├─ .gitignore //git忽略文件
├─ .npmignore //npm忽略文件
├─ .vscode //vscode配置文件
│ ├─ extensions.json
│ └─ launch.json
├─ examples // 项目运行示例代码
│ ├─ App.vue
│ └─ main.js
├─ index.html
├─ lib // 发布到npm上的库目录
│ ├─ wal-table-pagination.es.js
│ └─ wal-table-pagination.umd.js
├─ package-lock.json
├─ package.json
├─ packages // 源码目录,存放组件源码和测试用例
│ └─ wal-table-pagination
│ ├─ index.js
│ ├─ src
│ │ └─ wal-table-pagination.vue
│ └─ __tests__
│ ├─ table-test-common.js
│ ├─ trigger-event.js
│ └─ wal-table-pagination.test.js
├─ README.md
└─ vite.config.js
搭建 Vite 项目
兼容性注意 Vite 需要 Node.js 版本 >= 12.0.0。
使用npm
$ npm init vite@latest
给项目命名并按照提示操作即可初始化项目!
配置vite.config.js
//vite.config.js
import { resolve } from "path";
import { defineConfig } from "vite";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
server: {
port: "3000",
},
build: {
outDir: "lib",
lib: {
//库编译模式配置
entry: resolve(__dirname, "packages/wal-table-pagination/index.js"), //指定组件编译入口文件
name: "WalTablePagination",
fileName: (format) => `wal-table-pagination.${format}.js`,
},
rollupOptions: {
//rollup打包配置
external: ["vue", "element-plus"], // 指定外部依赖
output: {
// 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
globals: {
vue: "Vue",
"element-plus": "elementPlus",
},
},
},
},
test: {
// 使用 jsdom 模拟 DOM
// 这需要你安装 jsdom 作为对等依赖(peer dependency)
environment: "jsdom",
},
resolve: {
alias: {
"@": resolve(__dirname, "./packages"),
},
},
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
});
安装如下依赖:
element-plus //插件基于element-plus封装
@vue/test-utils //vue官方测试库
jsdom //dom库,用于模拟浏览器的dom
vitest //运行测试程序
完整的package配置如下:
//package.json
{
"name": "wal-table-pagination",
"author": "victor jiang",
"private": false,
"version": "1.1.0",
"description": "基于element-plus实现的table带分页组件",
"license": "MIT",
"homepage": "https://github.com/JZH189/wal-table-pagination#README.md",
"keywords": [
"vue3",
"element-plus",
"table",
"pagination"
],
"files": [ //需要上传到npm的文件目录
"lib"
],
"main": "lib/wal-table-pagination.umd.js", //cjs
"module": "lib/wal-table-pagination.es.js", //esm
"exports": {
"./lib/style.css": "./lib/style.css",
".": {
"import": "./lib/wal-table-pagination.es.js",
"require": "./lib/wal-table-pagination.umd.js"
}
},
"type": "module",
"scripts": {
"test": "vitest",
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"element-plus": "^2.2.16",
"vue": "^3.2.37"
},
"devDependencies": {
"@vitejs/plugin-vue": "^3.0.3",
"@vue/compiler-dom": "^3.2.38",
"@vue/test-utils": "^2.0.2",
"jsdom": "16.4.0",
"pretty": "^2.0.0",
"unplugin-auto-import": "^0.11.2",
"unplugin-vue-components": "^0.22.7",
"vite": "^3.0.7",
"vitest": "^0.20.0"
}
}
配置Vitest
Vitest 的主要优势之一是它与 Vite 的统一配置。如果存在,vitest 将读取你的根目录 vite.config.ts 以匹配插件并设置为你的 Vite 应用程序。例如,你的 Vite 有 resolve.alias 和 plugins 的配置将会在 Vitest 中起作用。如果你想在测试期间想要不同的配置,你可以:
- 创建 vitest.config.ts,优先级将会最高。
- 将 --config 选项传递给 CLI,例如 vitest --config ./path/to/vitest.config.ts。
- 在 defineConfig 上使用 process.env.VITEST 或 mode 属性(如果没有被覆盖,将设置为 test)有条件地在 vite.config.ts 中应用不同的配置。
如果要配置 vitest 本身,请在你的 Vite 配置中添加 test 属性。 你还需要使用 三斜线命令 ,同时如果是从 vite 本身导入 defineConfig,请在配置文件的顶部加上三斜线命令。
还可以参阅Vitest中的配置
编写测试用例
在packages/wal-table-pagination 目录中创建 tests 文件夹,并且新建和组件相同名称的.test.js文件。 这样当我们 npm run test 的时候Vitest会自动运行包含了.test.js的文件。
其实自己写测试用例可以帮助我们更好的发现组件的潜在bug。
单元测试
编写单元测试是为了验证小的、独立的代码单元是否按预期工作。一个单元测试通常覆盖一个单个函数、类、组合式函数或模块。单元测试侧重于逻辑上的正确性,只关注应用整体功能的一小部分。一般来说,单元测试将捕获函数的业务逻辑和逻辑正确性的问题。
组件测试
组件测试应该捕捉组件中的 prop、事件、提供的插槽、样式、CSS class 名、生命周期钩子,和其他相关的问题。组件测试不应该模拟子组件,而应该像用户一样,通过与组件互动来测试组件和其子组件之间的交互。例如,组件测试应该像用户那样点击一个元素,而不是编程式地与组件进行交互。
组件测试主要需要关心组件的公开接口而不是内部实现细节。对于大部分的组件来说,公开接口包括触发的事件、prop 和插槽。当进行测试时,请记住,测试这个组件做了什么,而不是测试它是怎么做到的。 这样我们的测试程序将会更加健壮。比如业务调整组件功能的时候,我们只需要保证对应的组件功能测试代码能够通过测试就可以了。记住这点是非常重要的。
将组件发布到npm
npm run build //将组件打包到lib
npm login
npm publish
最后打开npm官方网站,直接搜 wal-table-pagination 就可以找到刚才发布的 npm 包了。
转载自:https://juejin.cn/post/7143581859002187812