设计并实现一个 Vue 组件库,支持主题定制、按需引入、TypeScript 类型提示和自动化测试。

咳咳,麦克风测试,1、2、3,喂喂喂。大家好,我是今天的主讲人,很高兴能和大家一起聊聊如何打造一个现代化的 Vue 组件库。

今天咱们不搞虚的,直接上干货。目标明确:我们要创建一个支持主题定制、按需引入、TypeScript 类型提示,并且拥有自动化测试的 Vue 组件库。这听起来有点复杂,但别怕,我会尽量用大家都能听懂的语言,一步一步拆解这个过程。

一、项目初始化:从零开始的旅程

首先,咱们得有个家,也就是项目目录。推荐使用 Vue CLI 来初始化项目,它能帮我们省去很多配置上的麻烦。

vue create my-awesome-ui

在选择特性时,务必勾选 TypeScript 和 Unit Testing。如果你想玩点高级的,可以选择 Vue Router 和 Vuex,但对于组件库来说,它们并不是必须的。

初始化完成后,你会得到一个基本的 Vue 项目结构。接下来,我们需要对这个结构进行一些调整,使其更适合组件库的开发。

二、目录结构:井井有条的家

一个清晰的目录结构对于组件库的维护至关重要。我推荐以下结构:

my-awesome-ui/
├── packages/         # 组件源码目录
│   ├── button/       # Button 组件
│   │   ├── src/        # Button 组件源码
│   │   │   └── Button.vue
│   │   ├── index.ts    # Button 组件入口
│   │   └── style/      # Button 组件样式
│   │       └── index.scss
│   ├── input/        # Input 组件
│   │   └── ...
│   └── ...
├── src/              # 演示和文档
│   ├── App.vue
│   └── main.ts
├── tests/            # 单元测试
│   └── unit/
│       └── example.spec.ts
├── typings/          # 类型声明文件
│   └── vue-shim.d.ts
├── vue.config.js      # Vue CLI 配置文件
├── package.json       # 项目依赖和脚本
└── tsconfig.json      # TypeScript 配置文件
  • packages: 这里存放我们所有的组件源码。每个组件都有自己的独立目录,包含 src(组件源码)、index.ts(组件入口)和 style(组件样式)。
  • src: 这个目录主要用于组件库的演示和文档。我们可以通过修改 App.vuemain.ts 来展示我们的组件。
  • tests: 存放单元测试代码,确保组件的质量。
  • typings: 存放类型声明文件,为 TypeScript 提供类型支持。

三、编写第一个组件:Hello World!

咱们先从一个简单的 Button 组件开始。在 packages/button/src/Button.vue 中写入以下代码:

<template>
  <button class="my-button" :class="typeClass" @click="handleClick">
    {{ text }}
  </button>
</template>

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

export default defineComponent({
  name: 'MyButton',
  props: {
    text: {
      type: String,
      default: 'Button',
    },
    type: {
      type: String,
      default: 'default',
      validator: (value: string) => ['default', 'primary', 'success', 'warning', 'danger'].includes(value),
    },
  },
  computed: {
    typeClass() {
      return `my-button--${this.type}`;
    },
  },
  methods: {
    handleClick() {
      this.$emit('click');
    },
  },
});
</script>

<style lang="scss">
.my-button {
  padding: 10px 20px;
  border-radius: 4px;
  border: 1px solid #ccc;
  background-color: #fff;
  cursor: pointer;

  &--primary {
    background-color: #409EFF;
    color: #fff;
    border-color: #409EFF;
  }

  &--success {
    background-color: #67C23A;
    color: #fff;
    border-color: #67C23A;
  }

  &--warning {
    background-color: #E6A23C;
    color: #fff;
    border-color: #E6A23C;
  }

  &--danger {
    background-color: #F56C6C;
    color: #fff;
    border-color: #F56C6C;
  }
}
</style>

这个 Button 组件接收 texttype 两个 prop。text 用于设置按钮的文本,type 用于设置按钮的类型(default, primary, success, warning, danger)。

接下来,我们需要创建一个入口文件 packages/button/index.ts,用于导出这个组件:

import Button from './src/Button.vue';
import { App } from 'vue';

Button.install = (app: App) => {
  app.component(Button.name, Button);
};

export default Button;

这个入口文件导出了 Button 组件,并且添加了一个 install 方法。这个方法用于将组件注册为全局组件,方便在 Vue 应用中使用。

四、按需引入:只取所需,不浪费流量

为了支持按需引入,我们需要修改 vue.config.js 文件,配置 Webpack 的 chainWebpack 选项:

const { defineConfig } = require('@vue/cli-service')
const path = require('path');

module.exports = defineConfig({
  transpileDependencies: true,
  chainWebpack: (config) => {
    config.module
      .rule('js')
      .include
        .add(path.resolve(__dirname, 'packages'))
        .end()
      .use('babel-loader')
        .loader('babel-loader')
        .tap(options => {
          // 修改它的选项...
          return options
        })
  },
  configureWebpack: {
    resolve: {
      alias: {
        '@': path.resolve(__dirname, 'src'),
        'packages': path.resolve(__dirname, 'packages')
      }
    }
  }
})

这个配置告诉 Webpack,我们需要处理 packages 目录下的 JavaScript 文件。

然后,在 src/main.ts 中,我们可以按需引入 Button 组件:

import { createApp } from 'vue'
import App from './App.vue'
import Button from 'packages/button'

const app = createApp(App)

app.use(Button); // 全局注册

app.mount('#app')

这样,我们就实现了按需引入 Button 组件。

五、主题定制:千人千面,满足个性化需求

主题定制是组件库的重要特性。我们可以通过 CSS 变量来实现主题定制。

首先,在 packages/button/style/index.scss 中定义一些 CSS 变量:

:root {
  --my-button-background-color: #fff;
  --my-button-text-color: #333;
  --my-button-border-color: #ccc;
  --my-button-border-radius: 4px;
}

.my-button {
  background-color: var(--my-button-background-color);
  color: var(--my-button-text-color);
  border: 1px solid var(--my-button-border-color);
  border-radius: var(--my-button-border-radius);
  padding: 10px 20px;
  cursor: pointer;

  &--primary {
    --my-button-background-color: #409EFF;
    --my-button-text-color: #fff;
    --my-button-border-color: #409EFF;
  }

  // ...其他类型的样式
}

然后,在 Vue 应用中,我们可以通过修改 CSS 变量的值来改变组件的主题。例如,在 src/App.vue 中:

<template>
  <div class="app">
    <my-button text="Default"></my-button>
    <my-button type="primary" text="Primary"></my-button>
  </div>
</template>

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

export default defineComponent({
  name: 'App',
});
</script>

<style lang="scss">
.app {
  --my-button-background-color: #f0f0f0; // 修改默认背景色
  --my-button-text-color: #666;        // 修改默认文字颜色
  display: flex;
  gap: 10px;
  padding: 20px;
}
</style>

通过修改 .app 类的 CSS 变量,我们可以改变所有 Button 组件的默认样式。

六、TypeScript 类型提示:代码更健壮,开发更高效

TypeScript 的类型提示可以帮助我们避免很多潜在的错误,提高代码的健壮性。

我们已经在编写 Button 组件时使用了 TypeScript。为了让 TypeScript 能够正确地识别我们的组件,我们需要在 typings/vue-shim.d.ts 文件中添加类型声明:

import { ComponentCustomProperties } from 'vue';

declare module '@vue/runtime-core' {
  export interface ComponentCustomProperties {
    $myAwesomeUI: {
      version: string;
    };
  }
}

declare module 'vue' {
  export interface GlobalComponents {
    MyButton: typeof import('packages/button')['default'];
    // 其他组件
  }
}

这个类型声明告诉 TypeScript,我们有一个全局组件 MyButton,它的类型是 packages/button 导出的默认类型。

七、自动化测试:质量保障,让代码更安心

自动化测试是保证组件库质量的重要手段。我们可以使用 Jest 和 Vue Test Utils 来进行单元测试。

首先,我们需要创建一个测试文件 tests/unit/Button.spec.ts

import { shallowMount } from '@vue/test-utils';
import Button from 'packages/button';

describe('Button.vue', () => {
  it('renders text prop correctly', () => {
    const wrapper = shallowMount(Button, {
      props: {
        text: 'Hello World',
      },
    });
    expect(wrapper.text()).toBe('Hello World');
  });

  it('emits click event when clicked', async () => {
    const wrapper = shallowMount(Button);
    await wrapper.find('button').trigger('click');
    expect(wrapper.emitted('click')).toBeTruthy();
  });

  it('applies correct class based on type prop', () => {
    const wrapper = shallowMount(Button, {
      props: {
        type: 'primary',
      },
    });
    expect(wrapper.classes()).toContain('my-button--primary');
  });
});

这个测试文件包含了三个测试用例:

  • 测试 Button 组件是否正确渲染 text prop。
  • 测试 Button 组件是否在点击时触发 click 事件。
  • 测试 Button 组件是否根据 type prop 应用正确的 CSS 类。

然后,我们可以运行以下命令来执行测试:

npm run test:unit

如果所有测试用例都通过了,那么恭喜你,你的 Button 组件已经通过了单元测试。

八、发布你的组件库:让世界看到你的作品

当你的组件库开发完成后,你可以将其发布到 npm 上,让其他开发者也能使用你的作品。

首先,你需要创建一个 npm 账号。然后,在 package.json 文件中添加以下信息:

{
  "name": "my-awesome-ui",
  "version": "0.0.1",
  "description": "A Vue.js component library",
  "main": "dist/my-awesome-ui.umd.js",
  "module": "dist/my-awesome-ui.es.js",
  "exports": {
    ".": {
      "import": "./dist/my-awesome-ui.es.js",
      "require": "./dist/my-awesome-ui.umd.js"
    }
  },
  "files": [
    "dist",
    "packages"
  ],
  "keywords": [
    "vue",
    "component",
    "ui"
  ],
  "author": "Your Name",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/your-username/my-awesome-ui.git"
  },
  "bugs": {
    "url": "https://github.com/your-username/my-awesome-ui/issues"
  },
  "scripts": {
    "build": "vue-cli-service build --target lib --name my-awesome-ui packages/index.ts",
    "test:unit": "vue-cli-service test:unit"
  },
  "peerDependencies": {
    "vue": "^3.0.0"
  },
  "devDependencies": {
    // ...
  }
}
  • name: 组件库的名称,必须是唯一的。
  • version: 组件库的版本号,遵循 Semantic Versioning 规范。
  • description: 组件库的描述,用于在 npm 上展示。
  • main: CommonJS 规范的入口文件。
  • module: ES Module 规范的入口文件。
  • files: 需要发布的文件列表。
  • keywords: 关键词,用于在 npm 上搜索。
  • author: 作者。
  • license: 许可证。
  • repository: 代码仓库地址。
  • bugs: Bug 提交地址。
  • scripts: 构建和测试脚本。
  • peerDependencies: 依赖的 Vue 版本,必须是 Vue 3。

然后,我们需要修改 packages/index.ts 文件,导出所有组件:

import { App } from 'vue';
import Button from './button';

const components = [
  Button,
  // 其他组件
];

const install = (app: App): void => {
  components.forEach(component => {
    app.component(component.name, component);
  });
};

export {
  Button,
  // 其他组件
};

export default {
  install
};

接下来,运行以下命令来构建组件库:

npm run build

构建完成后,你会得到一个 dist 目录,里面包含了构建后的文件。

最后,运行以下命令来发布组件库:

npm publish

在发布之前,请确保你已经登录了 npm 账号。

好了,到这里,一个支持主题定制、按需引入、TypeScript 类型提示,并且拥有自动化测试的 Vue 组件库就完成了。

九、总结:回顾与展望

今天,我们一起学习了如何从零开始创建一个现代化的 Vue 组件库。我们讨论了项目初始化、目录结构、组件编写、按需引入、主题定制、TypeScript 类型提示、自动化测试和组件库发布。

当然,这只是一个简单的示例。在实际开发中,你可能需要考虑更多的问题,例如:

  • 组件的文档和演示。
  • 组件的国际化支持。
  • 组件的无障碍访问性。
  • 组件库的性能优化。

希望今天的分享能够帮助你更好地理解 Vue 组件库的开发。如果你有任何问题,欢迎随时提问。

感谢大家的聆听!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注