学习Element Plus组件库(一):项目搭建

前言

现在前端开发越来越多的项目都采用 monorepo 的方式管理代码;比如 Vue3 和 ElementPlus 都是采用了pnpm的monorepo。monorepo可以在一个项目仓库中管理多个模块/包,共用基础配置,本地互相依赖的项目调试非常方便。

pnpm monorepo 搭建

安装pnpm

shell 复制代码
npm install -g pnpm

新建一个文件夹,初始化package.json

shell 复制代码
pnpm init

在根目录新建一个.npmrc文件

ini 复制代码
# 提升所有依赖到根node_modules目录下
shamefully-hoist = true

在根目录新建一个pnpm-workspace.yaml文件,用来声明对应的工作区。

yaml 复制代码
packages:
  - play # 存放组件测试的代码
  - docs # 存放组件的文档
  - packages/* # 存放组件相关代码
  - build # 存放打包相关的代码

packages目录下又有多个包:

  • components:存放组件代码
  • theme-chalk:存放公共样式和组件样式
  • utils:存放一些工具方法
  • constants:存放一些常量

每个包都有自己的package.json,比如components目录下:

json 复制代码
{
  "name": "@storm/components",
  "version": "1.0.0",
  "description": "all components",
  "main": "index.ts",
  "module": "index.ts"
}

其他包分别为:@storm/constants@storm/theme-chalk@storm/utils

初步的目录结构为:

js 复制代码
|------ build
|------ docs
|------ packages
|   |------ components
|   |   └── package.json
|   |------ constants
|   |   └── package.json
|   |------ theme-chalk
|   |   └── package.json
|   |------ utils
|       └── package.json
|------ play
|------ .npmrc
|------ package.json
|------ pnpm-workspace.yaml
|------ README.md

然后在根目录执行:

shell 复制代码
pnpm install @storm/components -w
pnpm install @storm/constants -w
pnpm install @storm/theme-chalk -w
pnpm install @storm/utils -w

-w 表示安装到根目录下的 package.json 中,这样方便开发的时候几个包互相进行调用,安装后 package.json 中的内容:

json 复制代码
"dependencies": {
  "@storm/components": "workspace:*",
  "@storm/constants": "workspace:*",
  "@storm/theme-chalk": "workspace:*",
  "@storm/utils": "workspace:*"
}

TypeScript 初始化配置

先安装一下开发时所需要的依赖:

shell 复制代码
pnpm install vue typescript @types/node -D -w

接下来初始化一下ts的配置,在根目录新建 tsconfig.json

json 复制代码
{
  "compilerOptions": {
    "module": "ESNext", // 指定生成什么模块代码
    "declaration": false, // 默认不生成声明文件
    "noImplicitAny": true, // 类型不标注可以默认any
    "removeComments": false, // 是否删除注释
    "moduleResolution": "node", // 按照node模块来解析
    "esModuleInterop": true, // 简化对导入CommonJS模块的支持
    "jsx": "preserve", // 不转换jsx
    "target": "ES6", // 遵循es6版本
    "sourceMap": true,
    "lib": [ // 获得ECMAScript和dom的ts声明
      "ESNext",
      "DOM"
    ],
    "allowSyntheticDefaultImports": true, // 允许使用默认导入没有默认导出的模块
    "experimentalDecorators": true, // 装饰器语法
    "forceConsistentCasingInFileNames": true, // 强制区分大小写
    "resolveJsonModule": true, // 解析json模块
    "strict": true, // 严格模式
    "skipLibCheck": true, // 跳过对所有.d.ts文件的类型检查
  },
  "exclude": [ // 排除的目录和文件
    "node_modules",
    "dist/**"
  ]
}

Play环境

之后编写的组件都会放在 play 中运行和调试,所以需要在play目录下创建一个开发环境,在根目录执行:

shell 复制代码
pnpm create vite play --tempate vue-ts
// 进入play目录执行
pnpm install
// 启动项目
pnpm run dev

如果希望在根目录运行play项目,可以在根目录的 package.json 的 scripts 中配置:

json 复制代码
  "scripts": {
    "dev": "pnpm -C play dev"
  }

BEM的使用

BEM 规范下的命名格式:

  • 模块和模块之间使用 - 连接
  • Block 与 Element 之间使用 __ 连接
  • Block/Element 与 Mofifier 之间使用 -- 连接

比如 ElementPlus el-switch 组件的结构:

html 复制代码
<div class="el-switch is-checked">
  <input class="el-switch__input" type="checkbox">
  <span class="el-switch__core">
    <div class="el-switch__action"></div>
  </span>
</div>

通过JS生成BEM规范

在utils目录下新建一个 create.ts 文件:

ts 复制代码
function _bem(prefixName: string, blockSuffix: string, element: string, modifier: string) {
  if (blockSuffix) {
    prefixName += `-${blockSuffix}`
  }
  if (element) {
    prefixName += `__${element}`
  }
  if (modifier) {
    prefixName += `--${modifier}`
  }
  return prefixName
}
function createBEM(prefixName: string) {
  // 创建模块 el-switch
  const b = (blockSuffix: string = '') => _bem(prefixName, blockSuffix, '', '')
  // 创建元素 el-switch__input
  const e = (element: string = '') => element ? _bem(prefixName, '', element, '') : ''
  // 创建块修饰 el-switch--large
  const m = (modifier: string = '') => modifier ? _bem(prefixName, '', '', modifier) : ''
  // 创建元素块 el-switch-on-color
  const be = (blockSuffix: string = '', element: string = '') =>
    blockSuffix && element ? _bem(prefixName, blockSuffix, element, '') : ''
  // 创建块修饰 el-switch-on-color--large
  const bm = (blockSuffix: string = '', modifier: string = '') =>
    blockSuffix && modifier ? _bem(prefixName, blockSuffix, '', modifier) : ''
  // 创建元素修饰 el-switch__input--large
  const em = (element: string = '', modifier: string = '') =>
    element && modifier ? _bem(prefixName, '', element, modifier) : ''
  // 创建块元素修饰 el-switch-on-color__input--large
  const bem = (blockSuffix: string = '', element: string = '', modifier: string = '') =>
    blockSuffix && element && modifier ? _bem(prefixName, blockSuffix, element, modifier) : ''
  // 创建状态 is-success
  const is = (name: string, state?: string | boolean) => {
    return name && (state ?? true) ? `is-${name}` : ''
  }

  return {
    b,
    e,
    m,
    be,
    bm,
    em,
    bem,
    is
  }
}
// 用于创建BEM命名空间
export function createNamespace(name: string) {
  // 默认命名前缀
  const prefixName = `s-${name}`
  return createBEM(prefixName)
}

在utils目录下新建一个 index.ts 文件作为工具库统一导出的入口:

ts 复制代码
export * from './create'

然后在页面中使用:

html 复制代码
<template>
  <!-- 会生成s-name的类名 -->
  <div :class="bem.b()"></div>
</template>

<script setup lang="ts">
import { createNamespace } from '@storm/utils'
const bem = createNamespace('name')
</script>

通过scss生成BEM样式规范

theme-chalk 目录下新建src目录,然后在src目录下新建mixins目录,接着在mixins目录下新建2个文件:config.scss和mixins.scss。

scss 复制代码
// config.scss
$namespace: "s";
$element-separator: "__";
$modifier-separator: "--";
$state-prefix: "is-";
scss 复制代码
// mixins.scss
@use "config" as *; // @import可能有多次引用的问题
@forward "config"; 

// .s-button
@mixin b($block) {
  $B: $namespace + "-" + $block;
  .#{$B} {
    // @include后 样式会替换@content
    @content;
  }
}
// s-button.is-xxx
@mixin when($state) {
  // 输出到全局作用域
  @at-root {
    &.#{$state-prefix + $state} {
      @content;
    }
  }
}
// s-button__header
@mixin e($element) {
  @at-root {
    #{& + $element-separator + $element} {
      @content;
    }
  }
}
// s-button--primary
@mixin m($modifier) {
  @at-root {
    #{& + $modifier-separator + $modifier} {
      @content;
    }
  }
}

组件的注册

因为后续会有很多组件,每个组件注册都需要添加一个install方法,所以在 packages/utils/with-install.ts 封装一个公共的方法:

ts 复制代码
// 每个组件既可以通过app.use来使用 也可以通过import来使用
import { Plugin } from "vue"

type SFCWithInstall<T> = T & Plugin
export const withInstall = <T, E extends Record<string, any>>(
  main: T, extra?: E
) => {
  (main as SFCWithInstall<T>).install = (app): void => {
    // 比如注册checkbox组件同时会注册checkbox-group组件
    const comps = [main, ...Object.values(extra ?? {})]
    for (const comp of comps) {
      app.component(comp.name, comp)
    }
  }
  return main as SFCWithInstall<T> & E
}

实践一下

写一个简单的Icon组件实践一下BEM规范,需要先安装一下sass,在根目录执行:

shell 复制代码
pnpm install sass -D -w

在packages目录下的components目录新建一个icon目录,创建一下目录结构:

js 复制代码
|------ packages
|   |------ components
|   |   |------ icon
|   |   |   |------ src          # 组件入口目录
|   |   |   |   |------ icon.vue # 组件代码
|   |   |   |   └── icon.ts  # Ts类型和组件属性
|   |   |   └── index.ts     # 组件入口文件
|   |   └── package.json

ExtractPropTypes 可以把构造函数类型转换成对应的类型,StringConstructor => string。PropType 可以对类型进行更加详细的申明。

ts 复制代码
// icon.ts
import { PropType, ExtractPropTypes } from "vue";

export const iconProps = {
  color: String,
  size: [Number, String] as PropType<number | string>
}
// ExtractPropTypes可以将props类型抽取出来给外部使用
export type IconProps = ExtractPropTypes<typeof iconProps>

vue3版本大于3.3,内部已经实现defineOptions方法。

html 复制代码
// icon.vue
<template>
  <i
    :class="bem.b()"
    :style="style"
  >
    <slot />
  </i>
</template>

<script setup lang="ts">
import { createNamespace } from '@storm/utils'
import { iconProps } from './icon'
import { computed } from 'vue'
defineOptions({ name: 'SIcon' })
const props = defineProps(iconProps)

const bem = createNamespace('icon')
const style = computed(() => {
  if (!props.size && !props.color) return {}
  return {
    ...(props.size ? { 'font-size': props.size + 'px' } : {}),
    ...(props.color ? { 'color': props.color } : {})
  }
})
</script>
ts 复制代码
// index.ts
import { withInstall } from '@storm/utils'
import _Icon from './src/icon.vue'
// 添加install方法
export const Icon = withInstall(_Icon)
export default Icon
export * from './src/icon'

在theme-chalk目录下新建一个icon.scss文件,然后新建一个index.scss作为所有样式文件导出的入口:

scss 复制代码
// icon.scss
@use "./mixins/mixins.scss" as *;
// 供别的组件loading状态使用
@keyframes rotating {
  0% {
    transform: rotateZ(0deg);
  }
  100% {
    transform: rotateZ(360deg);
  }
}

@include b(icon) {
  display: inline-flex;
  width: 1em;
  height: 1em;
  vertical-align: middle;
  @include when(loading) {
    animation: rotating 2s linear infinite;
  }
}
scss 复制代码
// index.scss
@use "./icon.scss";

然后在play目录的main.ts下引入组件和样式:

ts 复制代码
import { createApp } from 'vue'
import App from './App.vue'
import Icon from '@storm/components/icon'
import '@storm/theme-chalk/src/index.scss'

const plugins = [
  Icon
]
const app = createApp(App)
plugins.forEach(plugin => app.use(plugin))

app.mount('#app')

看下运行效果:

相关推荐
丁总学Java19 分钟前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
懒羊羊大王呀30 分钟前
CSS——属性值计算
前端·css
道爷我悟了1 小时前
Vue入门-指令学习-v-html
vue.js·学习·html
无咎.lsy1 小时前
vue之vuex的使用及举例
前端·javascript·vue.js
fishmemory7sec1 小时前
Electron 主进程与渲染进程、预加载preload.js
前端·javascript·electron
fishmemory7sec1 小时前
Electron 使⽤ electron-builder 打包应用
前端·javascript·electron
工业互联网专业2 小时前
毕业设计选题:基于ssm+vue+uniapp的校园水电费管理小程序
vue.js·小程序·uni-app·毕业设计·ssm·源码·课程设计
豆豆2 小时前
为什么用PageAdmin CMS建设网站?
服务器·开发语言·前端·php·软件构建
计算机学姐2 小时前
基于SpringBoot+Vue的在线投票系统
java·vue.js·spring boot·后端·学习·intellij-idea·mybatis
twins35203 小时前
解决Vue应用中遇到路由刷新后出现 404 错误
前端·javascript·vue.js