首页 > 其他分享 >Vue3 + TS 搭建组件库

Vue3 + TS 搭建组件库

时间:2023-12-02 15:35:03浏览次数:37  
标签:vue Vue3 TS pnpm 组件 import true icon

开始

在编写组件库之前,我们首先要对整个代码项目的解构有一个清晰的划分,以及用到的大多数规范,和代码风格有一个约定,这篇文章主要就围绕着下面图中的几个问题展开描述一下。

image.png

1、搭建 monorepo 环境

我们使用pnpm当做包管理工具,用pnpm workspace来实现monorepo。可以看下面参考文章里面的介绍,结合官网有个基础的了解。下面我们正式开始搭建。

新建一个文件夹z-vue3-ui

npm install pnpm -g # 全局安装pnpm
pnpm init # 初始化package.json配置⽂件 私有库
pnpm install vue typescript -D # 全局下添加依赖

添加.npmrc文件,

shamefully-hoist = true  // 作用依赖包都扁平化的安装在node_modules下面

创建tsconfig.json文件

{
  "compilerOptions": {
    "module": "ESNext", // 打包模块类型ESNext
    "declaration": false, // 默认不要声明⽂件
    "noImplicitAny": false, // ⽀持类型不标注可以默认any
    "removeComments": true, // 删除注释
    "moduleResolution": "node", // 按照node模块来解析
    "esModuleInterop": true, // ⽀持es6,commonjs模块
    "jsx": "preserve", // jsx 不转
    "noLib": false, // 不处理类库
    "target": "es6", // 遵循es6版本
    "sourceMap": true,
    "lib": [
      // 编译时⽤的库
      "ESNext",
      "DOM"
    ],
    "allowSyntheticDefaultImports": true, // 允许没有导出的模块中导⼊
    "experimentalDecorators": true, // 装饰器语法
    "forceConsistentCasingInFileNames": true, // 强制区分⼤⼩写
    "resolveJsonModule": true, // 解析json模块
    "strict": true, // 是否启动严格模式
    "skipLibCheck": true, // 跳过类库检测
    "types": ["unplugin-vue-define-options"] // sfc 添加 name属性的包需要的
  },
  "exclude": [
    // 排除掉哪些类库
    "node_modules",
    "**/__tests__",
    "dist/**"
  ]
}

在项目根目录下面创建pnpm-workspace.yaml配置文件。

packages:
  - "packages/**" # 存放所有组件
  - docs # 文档
  - play # 测试组件

2、创建组件测试环境

pnpm create vite play --template vue-ts
cd play
pnpm install

在根目录新建一个typings目录,用来存放项目中通用的自定义的类型,然后把用vite创建的play/src下面的vite-env.d.ts移动到typings下面去。

启动测试项目, 在根目录下面的package.json下面添加scripts脚本。

  "scripts": {
    "dev": "pnpm -C play dev"
  }

测试环境搭建完成,下面开始搭建packages下面的文件目录了。

image.png

3、引入 scss,并式实现 Bem

先手动在根目录下面创建如下目录

packages
    ├─components # 存放所有的组件
    ├─utils # 存放⼯具⽅法
    └─theme-chalk # 存放对应的样式

在执行下面的命令,在各自的根目录下面创建package.json文件。

cd components && pnpm init
cd theme-chalk && pnpm init
cd utils && pnpm init

这个时候需要手动修改每个包的名字,让其属于z-vue3-ui的子包,我们分别进行以下的修改,在对应package.json文件中修改其name属性的值。

@z-vue3-ui /components
@z-vue3-ui/theme-thalk
@z-vue3-ui/utils;

然后执行一下命令,将这三个包安装在根目录下面,注意名字哦。

pnpm i @z-vue3-ui/components -w
pnpm i @z-vue3-ui/theme-chalk -w
pnpm i @z-vue3-ui/utils -w

下面我们就开始实现Bem规范了。

1. Bem Js 实现部分

先来实现在js中创建class的几个函数。

utils/create.ts

// block 代码块
// element 元素
// modifier 装饰

// z-button
// z-button__element--disable

/**
 *
 * @param prefixName 前缀名
 * @param blockName 代码块名
 * @param elementName 元素名
 * @param modifierName 装饰符名
 * @returns  说白了 ,就是提供一个函数,用来拼接三个字符串,并用不同的符号进行分隔开来
 */
function _bem(prefixName, blockName, elementName, modifierName) {
  if (blockName) {
    prefixName += `-${blockName}`;
  }
  if (elementName) {
    prefixName += `__${elementName}`;
  }
  if (modifierName) {
    prefixName += `--${modifierName}`;
  }
  return prefixName;
}

/**
 *
 * @param prefixName 前缀
 * @returns
 */
function createBEM(prefixName: string) {
  const b = (blockName?) => _bem(prefixName, blockName, "", "");

  const e = (elementName) =>
    elementName ? _bem(prefixName, "", elementName, "") : "";

  const m = (modifierName) =>
    modifierName ? _bem(prefixName, "", "", modifierName) : "";

  const be = (blockName, elementName) =>
    blockName && elementName
      ? _bem(prefixName, blockName, elementName, "")
      : "";
  const bm = (blockName, modifierName) =>
    blockName && modifierName
      ? _bem(prefixName, blockName, "", modifierName)
      : "";
  const em = (elementName, modifierName) =>
    elementName && modifierName
      ? _bem(prefixName, "", elementName, modifierName)
      : "";
  const bem = (blockName, elementName, modifierName) =>
    blockName && elementName && modifierName
      ? _bem(prefixName, blockName, elementName, modifierName)
      : "";
  const is = (name, state?) => (state ? `is-${name}` : "");
  return {
    b,
    e,
    m,
    be,
    bm,
    em,
    bem,
    is,
  };
}

export function createNamespace(name: string) {
  const prefixName = `z-${name}`;
  return createBEM(prefixName);
}

下面我们找个地方,说一下上面的bem怎么使用。因为现在我们的代码都是ems的,在node环境中跑起来不方便,所以就在play测试的小模块中演示了。

const bem = createNamespace("icon");

console.log(bem.b());
console.log(bem.e("wrapper"));
console.log(bem.m("disabled"));
console.log(bem.is("checked", true));
console.log(bem.bem("box", "element", "disabled"));

image.png

2. Bem scss 部分

theme-chalk
├── package.json
└── src
    ├── icon.scss
    ├── index.scss
    ├── mixins
    │   ├── config.scss
    │   └── mixins.scss

config.scss

$namespace: "z";
$element-separator: "__"; // 元素连接符
$modifier-separator: "--"; // 修饰符连接符
$state-prefix: "is-"; // 状态连接符

* {
  box-sizing: border-box;
}

mixins.scss

@use "config" as *;
@forward "config";

// z-icon
@mixin b($block) {
  $B: $namespace + "-" + $block;
  .#{$B} {
    @content;
  }
}

// z-icon.is-xxx
@mixin when($state) {
  @at-root {
    &.#{$state-prefix + $state} {
      @content;
    }
  }
}

// .z-icon--primary

@mixin m($modifier) {
  @at-root {
    #{& + $modifier-separator + $modifier} {
      @content;
    }
  }
}

// z-icon__header
@mixin e($element) {
  @at-root {
    #{& + $element-separator + $element} {
      @content;
    }
  }
}

index.scss

@use "./icon.scss";

icon.scss

@use "./mixins/mixins.scss" as *;

@keyframes transform {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

@include b(icon) {
  width: 1em;
  height: 1em;
  line-height: 1em;
  display: inline-flex;
  vertical-align: middle;
  svg.loading {
    animation: transform 1s linear infinite;
  }
}

4. 编写 Icon 组件

目录结构如下:

components
├── icon
│   ├── index.ts
│   └── src
│       ├── icon.ts
│       └── icon.vue
└── package.json

icon.vue

<template>
  <i :class="bem.b()" :style="style">
    <slot></slot>
  </i>
</template>

<script lang="ts" setup>
import { computed, CSSProperties } from "vue";
import { createNamespace } from "../../../utils/create";
import { iconProps } from "./icon";
const bem = createNamespace("icon");

defineOptions({
  name: "ZIcon",
});

const props = defineProps(iconProps);

const style = computed<CSSProperties>(() => {
  if (!props.color && !props.size) {
    return {};
  }
  return {
    ...(props.size ? { "font-size": props.size + "px" } : {}),
    ...(props.color ? { color: props.color } : {}),
  };
});
</script>

icon.ts

import { ExtractPropTypes, PropType } from "vue";

export const iconProps = {
  size: [Number, String] as PropType<number | string>,
  color: String,
} as const;

export type IconProps = ExtractPropTypes<typeof iconProps>;

index.ts

import _Icon from "./src/icon.vue";
import { withInstall } from "@z-vue3-ui/utils/withInstall";

const Icon = withInstall(_Icon); // 生成带有 install 方法的组件

export default Icon; // 导出组件
export type { IconProps } from "./src/icon"; // 导出组件 props 的类型

// 这里为了给 volar 用的,具体可以看下面的文档
declare module "vue" {
  export interface GlobalComponents {
    ZIcon: typeof Icon;
  }
}

文档链接

image.png

编写一个方法用来把我们自己编写的组件包装成一个插件,方便后序导入使用,直接可以用Vue.use()

utils 下面的目录结构

utils
├── create.ts
├── package.json
└── withInstall.ts
import { Plugin } from "vue";

export type withInstallSFC<T> = T & Plugin;

// 给传入的组件添加一个 install 方法
export function withInstall<T>(comp: T) {
  (comp as withInstallSFC<T>).install = function (app) {
    const { name } = comp as unknown as { name: string };
    app.component(name, comp); // 这一块的类型还有点问题,还在研究中。
  };
  return comp as withInstallSFC<T>;
}
play
├── README.md
├── index.html
├── package.json
├── pnpm-lock.yaml
├── public
│   └── vite.svg
├── src
│   ├── App.vue
│   ├── assets
│   └── main.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

并且在main.ts中引入样式文件,并安装sass

mian.ts

import { createApp } from "vue";

import "@z-vue3-ui/theme-chalk/src/index.scss";
import App from "./App.vue";

createApp(App).mount("#app");

我们的 icon 内容并不由本库提供,需要安装另一个库,这个组件只是将其进行了整合

pnpm add @vicons/ionicons5 -w

App.vue

<script setup lang="ts">
import ZIcon from "@z-vue3-ui/components/icon";
import { AccessibilityOutline } from "@vicons/ionicons5";
</script>

<template>
  <div>
    <ZIcon>
      <AccessibilityOutline></AccessibilityOutline>
    </ZIcon>
  </div>
</template>

不出意外的话,现在已经可以看见下面的 icon 组建了

image.png

还有更详细的关于BEMElement实现主题的文章请参考下面这一篇,ElementUI 组件库样式与自动化设计

5、Eslint 配置

npx eslint --init

检验语法并提示错误行数

image.png

使用 js-module

image.png

项目采用语法

image.png

是否使用 ts

image.png

代码跑在哪里

image.png

这里需要我们手动使用pnpm进行包的安装

image.png

pnpm i eslint-plugin-vue@latest @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest eslint@latest -D -w
pnpm i @vue/eslint-config-typescript -D -w
module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  extends: [
    "eslint:recommended",
    "plugin:vue/vue3-recommended", // vue3解析 https://eslint.vuejs.org/
    "plugin:@typescript-eslint/recommended",
    "@vue/typescript/recommended",
  ],
  parserOptions: {
    ecmaVersion: "latest",
    parser: "@typescript-eslint/parser",
    sourceType: "module",
  },
  plugins: ["vue", "@typescript-eslint"],
  rules: {
    "vue/html-self-closing": "off",
    "vue/singleline-html-element-content-newline": "off",
    "vue/multi-word-component-names": "off",
    "vue/prefer-import-from-vue": "off",
  },
  globals: {
    defineOptions: "readonly",
  },
};

6、Prettier 配置

安装插件,并添加给 vscode 添加配置文件

.prettierrc.js

// 此处的规则供参考,其中多半其实都是默认值,可以根据个人习惯改写
module.exports = {
  printWidth: 80, // 单行长度
  tabWidth: 2, // 缩进长度
  useTabs: false, // 使用空格代替tab缩进
  semi: true, // 句末使用分号
  singleQuote: true, // 使用单引号
  quoteProps: "as-needed", // 仅在必需时为对象的key添加引号
  jsxSingleQuote: true, // jsx中使用单引号
  trailingComma: "all", // 多行时尽可能打印尾随逗号
  bracketSpacing: true, // 在对象前后添加空格-eg: { foo: bar }
  jsxBracketSameLine: true, // 多属性html标签的‘>’折行放置
  arrowParens: "always", // 单参数箭头函数参数周围使用圆括号-eg: (x) => x
  requirePragma: false, // 无需顶部注释即可格式化
  insertPragma: false, // 在已被preitter格式化的文件顶部加上标注
  proseWrap: "preserve", // 不知道怎么翻译
  htmlWhitespaceSensitivity: "ignore", // 对HTML全局空白不敏感
  vueIndentScriptAndStyle: false, // 不对vue中的script及style标签缩进
  endOfLine: "lf", // 结束行形式
  embeddedLanguageFormatting: "auto", // 对引用代码进行格式化
};

.prettierignore

node_modules
dist

编辑器配置文件

{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true
}

7、编辑器配置

.editorconfig

# http://editorconfig.org

root = true

[*] # 表示所有文件适用
charset = utf-8                      # 设置文件字符集为 utf-8
indent_style = space                 # 缩进风格(tab | space)
indent_size = 2                      # 缩进大小
end_of_line = lf                     # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true      # 去除行首的任意空白字符
insert_final_newline = true          # 始终在文件末尾插入一个新行

[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false

并安装EditorConfig for VS Code插件即可

8、lint-staged 配置

git init
pnpm install mrm husky lint-staged -w -D
npx mrm lint-staged

强制执行常规提交的可共享commitlint配置。与@commitlint/cli@commitlint/prompt-cli 一起使用。

pnpm install @commitlint/cli @commitlint/config-conventional -D -w
npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1"

commitlint.config.js

module.exports = {
  extends: ["@commitlint/config-conventional"],
  rules: {
    "type-enum": [
      2,
      "always",
      [
        "build", // 编译相关的修改,例如发布版本、对项⽬构建或者依赖的改动
        "chore", // 其他修改, ⽐如改变构建流程、或者增加依赖库、⼯具等
        "ci", // 持续集成修改
        "docs", // ⽂档修改
        "feat", //新特性、新功能
        "fix", // 修改 bug
        "perf", // 优化相关,⽐如提升性能、体验
        "refactor", // 代码重构
        "revert", // 回滚到上⼀个版本
        "style", // 代码格式修改
        "test", // 测试⽤例修改
      ],
    ],
  },
};

git commit -m"feat: 初始化⼯程"

9、Vitepress 编写组件文档

在根目录下面创建docs文件夹,用来存放文档。

1. 安装 vitepress

cd docs
pnpm init
pnpm install vitepress -D # 在doc⽬录下安装

package.json

  "scripts": {
    "dev": "vitepress dev ."
  },

然后在根目录下面的添加脚本

  "scripts": {
    "docs:dev": "pnpm -C docs dev",
  },

2. 创建第一篇文章

---
layout: home

hero:
  name: z-ui 组件库
  text: 基于 Vue 3 的组件库.
  tagline: 掌握 vue3 组件编写
  actions:
    - theme: brand
      text: 快速开始
      link: /guide/quickStart

features:
  - icon: 

标签:vue,Vue3,TS,pnpm,组件,import,true,icon
From: https://www.cnblogs.com/wp-leonard/p/17871662.html

相关文章

  • vue3使用富文本编辑器wangEditor 5,增加自定义下拉框,并动态改变下拉框内容
    官方资料wangEditor官网效果展示准备工作这里按照wangEditor官网提供的Vue3Demo操作就行,下面的内容可以直接跳过安装yarnadd@wangeditor/editor#或者npminstall@wangeditor/editor--saveyarnadd@wangeditor/editor-for-vue@next#或者npminstall@w......
  • Spring 中的 URL 处理工具类 UriComponentsBuilder
    UriComponentsBuilder是SpringFramework中的一个用于构建URI(UniformResourceIdentifier)和URL(UniformResourceLocator)的实用程序类。它提供了一种简单的方式来构建包含各种部分(如协议、主机、路径、查询参数等)的URI和URL,并支持对这些部分进行修改、替换和合并等操作。以下是......
  • Highcharts饼图的主要属性和网格线属性​
    需求在Highcharts中,需要更改图表里的网格线如何去完成;在Highcharts中,你可以通过设置不同的属性来自定义你的饼图,饼图的属性于其他图表存在差别。分析饼图属性:legend.enabled:控制图例的显示与隐藏。设置为false则隐藏图例,默认为true。legend.layout:设置图例的布局方式。可......
  • HTML5 Video视频组件支持的视频编码格式
    一、HTML5Video视频格式与浏览器的支持情况当前,<video>元素支持三种视频格式:MP4,WebM,和Ogg:浏览器MP4WebMOggInternetExplorerYESNONOChromeYESYESYESFirefoxYESYESYESSafariYESNONOOperaYES(从Opera25起)YESYESMP4=带有......
  • HarmonyOS之ArkTS-常用基本数据类型及使用
    ArtTS基本数据类型:包括number、string、boolean、array、枚举类型、unknown等number:数字类型,在程序中定义一个变量指定类型一定要小写number      看了截图大家肯定有点疑惑为什么变量后面要加一个;number这就是TS的缘故,这样是为了防止后面发生变异(可被用来放......
  • RTSP协议安防平台LiteNVR配置视频流播放时长的操作步骤
    今天我们来分享一下另一个关于鉴权的功能:LiteNVR的视频流地址鉴权。有很多用户在使用LiteNVR时都遇到一个同样的需求,那就是将分发的流地址分享给用户播放时,如何控制用户的播放时长呢?LiteNVR平台是基于RTSP/Onvif协议的视频接入、处理及分发平台,能够实现设备接入、实时视频直播、录......
  • 【JavaSE】一些常见API(Object、Objects、Math、System、BigDecimal、包装类、Arrays)
    Object类Object类介绍toString方法直接println(对象名),默认会自动调用(对象名.toString),而.toString默认是返回地址信息->全类名(包名+类名)@地址的十六进制哈希值,因此如果println(对象名)控制台没有输出地址值,说明该类一定重写了Object类的toString方法,比如String类和Arr......
  • HarmonyOS之ArkTS
    ArkTs是什么:它则是TS的超集,在TypeScript(简称TS)的基础上,扩展了声明式UI、状态管理等相应的能力。它会结合应用开发和运行的需求持续演进,包括但不限于引入分布式开发范式、并行和并发能力增强、类型系统增强等方面的语言特性 。它是HarmonyOS优选的主力应用开发语言ArkTS声明......
  • MATLAB时间序列数据重建与平滑:HANTS滤波
      本文介绍在MATLAB中,实现基于HANTS算法(时间序列谐波分析法)的长时间序列数据去噪、重建、填补的详细方法。  HANTS(HarmonicAnalysisofTimeSeries)是一种用于时间序列分析和插值的算法。它基于谐波分析原理,可以从观测数据中提取出周期性变化的信号成分,并进行数据插值和去噪......
  • java Object和Objects
    packagenet.elaina.ObjectTest;publicclasstest1{publicstaticvoidmain(String[]args){/*publicStringtoString()返回对象的字符串表示形式publicbooleanequals(Objectobj)比较两个对象是否相等......