likes
comments
collection
share

element-plus源码与二次开发:开发规范与发布

作者站长头像
站长
· 阅读数 15

element-plus组件库可以满足中后台大部分场景的需求,但有时为了定制功能和样式,不得不进行二次开发。本篇文章对element-plus(v2.3.7)的开发规范与发布流程进行讲解。

另外对于element-plus的构建与发布流程,可以阅读element-plus源码与二次开发:构建与发布流程分析

拉取element-plus源码

文章基于element-plus v2.3.7进行讲解。

安装依赖

pnpm i

组件文档的改造

执行命令:

pnpm docs:dev

这将生成国际化语言文件,并启动组件的文档网站,即element-plus.org/en-US

支持中文

文档的路由规则是 /[lang]/*, 根路由是当前语言,默认为en-US英语。项目源码中只包含英语文件,若想支持中文,需要手动拉取中文语言文件(element官方是在执行CI/CD时自动拉取)。关于国际化方案(crowdin),详见构建与发布流程分析中的关于docs:dev的详细解释。

下载语言文件的方式有两种:

  • 使用命令自动下载中文语言文件
cd doc
pnpm exec crowdin download -l zh-CN # 下载中文语言文件(需要在配置文件中设置token, element-plus官方是在CI/CD时写入token到配置文件)
  • 手动下载 crowdin命令依赖java运行环境,执行命令会自动安装环境,但是因为网络问题很难安装并且占用空间,因此如果不考虑CI/CD,可以手动下载到项目中,并且在.gitignore中移除zh-CN的忽略配置。

crowdin项目地址element-plus/zh-CN

element-plus源码与二次开发:开发规范与发布

也可直接点击下载

element-plus源码与二次开发:开发规范与发布下载后的语言文件

需要注意的是.vitepress在文件系统中默认是隐藏的,需要在命令行工具或vscode中操作

将两个zh-CN文件夹按照目录结构放到docs的对应目录下,即一个放到docs根目录下,一个放到docs/.vitepress/crowdin/zh-CN

重新执行pnpm docs:dev

.DS_Store报错

在手动复制完文件后再次执行pnpm docs:dev,可能会报错

element-plus源码与二次开发:开发规范与发布

这是因为在mac系统中访问过文件夹后会自动生成.DS_Store导致,需要手动删除掉 ```bash rm -rf ./docs/.vitepress/crowdin/.DS_Store ``` 或修改源码逻辑,忽略.DS_Store文件夹

编辑docs/.vitepress/build/crowdin-generate.ts

element-plus源码与二次开发:开发规范与发布

重新启动后,就可以切换为中文了

element-plus源码与二次开发:开发规范与发布

注意这里因为语言识别的缓存逻辑,首次如果进入过/en-US路由,直接修改路由为/zh-CN仍然会跳转回英文路由,需要在页面中手动切换一次中文。

<script lang="ts" setup>
  import { useLang } from '../../composables/lang'
  import homeLocale from '../../../i18n/pages/home.json'

  const lang = useLang() // 从页面路径取语言 如 https://element-plus.org/zh-CN 则lang: 'zh-CN'
  const homeLang = computed(() => homeLocale[lang.value])
</script>
<template>
  <h4>{{ homeLang['title'] }}</h4>
</template>

文档中根据路由获取语言

语言路由自动重定向逻辑

进入首页,会自动重定向到当前语言的路由,如/en-US,这里讲解一下相关逻辑。

.vitepress/config.mts中,通过head配置在页面引入了一些脚本,其中就包括国际化相关的脚本

element-plus源码与二次开发:开发规范与发布

languages.vitepress/corwdin下语言名字数组

element-plus源码与二次开发:开发规范与发布

element-plus源码与二次开发:开发规范与发布注入.vitepress/lang.js脚本

首先会读取localStorage中缓存的语言(key为preferred_lang),不存在则获取navigator.language,然后检查该语言在语言列表(window.supportLangs)中是否存在,不存在则取默认语言。

lang.js:

const defaultLang = 'en-US'
// 优先取缓存的语言
let userPreferredLang = localStorage.getItem(cacheKey) || navigator.language // navigator.language 浏览器使用的语言
// userPreferredLang在语言列表中不存在时取defaultLang的值
 // 缓存语言
localStorage.setItem(cacheKey, language)
userPreferredLang = language

// 当前路由与用户语言不一致时,自动跳转到用户语言
if (!location.pathname.startsWith(`/${userPreferredLang}`)) {
}

因此如果首次访问的英文页面,语言被缓存后需要手动修改语言,否则会被自动重定向到之前缓存的语言。

移除国际化

一般我们自己封装后,不需要英文,只需要中文就好

TODO

修改layout

移除赞助商广告、修改外链等

.vitepress/theme/index.ts会被自动识别为自定义主题,在index.ts中指定了Layout文件docs/vitepress/components/vp-app.vue,事实上theme/idnex.ts只是入口,所有的主题逻辑都在docs/vitepress下

移除赞助商广告

移除所有sponsor关键字相关逻辑与文件

element-plus源码与二次开发:开发规范与发布

移除埋点相关逻辑

  • head.ts中移除googletagmanagerhotjar相关代码;
  • 移除analytics.ts
  • 移除文档各处对analytics.ts的调用

配置左侧菜单

.vitepress/config.mds中的自定义主题配置项themeConfig配置了navsidebars

右上角github仓库图标与跳转

配置在docs/.vitepress/vitepress/composables/social-links.ts

import GitLabIcon from '~icons/ri/gitlab-fill' // 修改为gitlab图标

export const useSocialLinks = () => {
  return [
    {
      link: 'http://10.101.1.50/isbg/foundation/frontend/brand-plus', // 改为自己的仓库
      icon: GitLabIcon,
      text: 'GitLab',
    },
  ]
}
关于图标

docs中使用了@iconify-json/ri来导入remix icon的json数据,然后通过unplugin-icons/resolver的vite插件来处理(见docs/vite.config.ts),要使用icon,import Icon from '~icons/ri/<icon-name>'

<script>
import GitHubIcon from '~icons/ri/github-fill'
</script>
<template>
  <ElIcon v-if="icon" :size="24">
    <GitHubIcon />
  </ElIcon>
</template>

源代码跳转链接

每个组件的文档底部会有「当前组件」、「当前文档页」的源码页面,贡献者

element-plus源码与二次开发:开发规范与发布

在xxx上编辑此页面

上面对于国际化解释过,项目中本身只存在英文语言源码,其他语言在crowdin上,因此如果当前访问的英文页面,这里显示和跳转的是在github上编辑此页面,其他语言则是在crowdin上编辑。

对于我们把中文语言存到项目中了,这里就直接替换域名,跳转到中文语言就好了

模板组件:docs/.vitepress/vitepress/components/doc-content/vp-edit-link.vue

import { useEditLink } from '../../composables/edit-link'

const { url, text } = useEditLink()
// 默认项目中是只有默认语言英文的源码,其他语言则不可变编辑
const canEditSource = computed(() => {
  return lang.value === defaultLang
})
const url = computed(() => {
	// ... 拼接url
})
const text = computed(() => {
  return canEditSource.value
    ? editLink.value['edit-on-github'] // 默认语言
    : editLink.value['edit-on-crowdin'] // 其他语言
})

下面来修改跳转私有仓库,以gitlab为例

internal/build-constants/src/repo.ts中添加仓库地址

export const REPO_URL = `http://10.101.1.50/isbg/foundation/frontend/brand-plus`

修改.vitepress/config.mts

import { REPO_BRANCH, REPO_URL } from '@element-plus/build-constants'
export const config: UserConfig = {
  themeConfig: {
    repo: REPO_URL,
  }
}

注:之前替换右上角仓库源码,也可以从使用useData取repo字段了

修改docs/.vitepress/vitepress/utils/index.tscreateGitHubUrl的url拼接逻辑

export function createGitHubUrl(
  docsRepo: string,
  docsDir: string,
  docsBranch: string,
  path: string,
  folder = 'examples/',
  ext = '.vue'
) {
  // const base = isExternal(docsRepo) // 是否是外部链接
  //   ? docsRepo
  //   : `https://github.com/${docsRepo}`
  // return `${base.replace(endingSlashRE, '')}/edit/${docsBranch}/${docsDir ? `${docsDir.replace(endingSlashRE, '')}/` : ''
  //   }${folder || ''}${path}${ext || ''}`

  // 跳转gitlab的编辑页
  return `${docsRepo.replace(endingSlashRE, '')}/-/edit/${docsBranch}/${docsDir ? `${docsDir.replace(endingSlashRE, '')}/` : ''
    }${folder || ''}${path}${ext || ''}`
}

源代码

分别跳转组件源码和当前文档页源码地址。

逻辑在docs/.vitepress/plugins/markdown-transform.ts中,作为vite插件被配置在docs/vite.config.ts,直接修改对应URL生成逻辑:

const _REPO_URL = REPO_URL.replace(//$/, '')
const GITHUB_BLOB_URL = `${_REPO_URL}/-/blob/${REPO_BRANCH}`
const GITHUB_TREE_URL = `${_REPO_URL}/-/tree/${REPO_BRANCH}`
const transformComponentMarkdown = () => {
  const docUrl = `${GITHUB_BLOB_URL}/${docsDirName}/${lang}/component/${componentId}.md`
  const componentUrl = `${GITHUB_TREE_URL}/packages/components/${componentId}`
}

贡献者

在「源代码」逻辑下面,移除相关代码即可

移除footer

链接、讨论区

移除VPFooter即可

组件

创建组件

下面我们以创建一个el-aaa组件为例,该组件输出一个红色的aaa文本

执行pnpm gen [component name]可以用模板快速创建新组件,如

pnpm gen aaa

element-plus源码与二次开发:开发规范与发布

aaa/index.ts

对于创建出的组件,会使用withInstall方法进行包装,添加install方法,以便全局注册

import { withInstall } from '@element-plus/utils'
import Aaa from './src/aaa.vue'

export const ElAaa = withInstall(Aaa)
export default ElAaa

export * from './src/aaa'

创建出组件后,需要手动导出

  • packages/components/index.ts

element-plus源码与二次开发:开发规范与发布

  • packages/element-plus/component.ts
import { ElAaa } from '@element-plus/components/aaa'
// ...

export export default [
  ElAaa,
  // ...
]

aaa/src/aaa.ts

定义组件属性、emits和类型声明

import { buildProps } from '@element-plus/utils'

import type { ExtractPropTypes } from 'vue'
import type Aaa from './aaa.vue'

// 定义props
export const aaaProps = buildProps({
  /**
   * @description 是否显示
   */
  show: Boolean,
  /**
   * @description 文本颜色
   */
  color: {
    type: String,
    default: 'red',
    values: ['red', 'blue'], // 可选项
  },
})

export const aaaEmits = {
  close: (evt: MouseEvent) => evt instanceof MouseEvent,
}
export type AaaEmits = typeof aaaEmits

/** ExtractPropTypes 用来提取vue中声明式prop的某些参数类型,这里即提取所有参数类型 */
export type AaaProps = ExtractPropTypes<typeof aaaProps>
export type AaaInstance = InstanceType<typeof Aaa>

aaa/src/aaa.vue

<template>
  <div>
    <h1>aaa</h1>
  </div>
</template>

<script lang="ts" setup>
  import { onMounted } from 'vue'
  import { aaaProps, aaaEmits } from './aaa'

  // Vue 3.3+官方支持,<3.3 由Vue-macros支持
  defineOptions({
    name: 'ElAaa',
  })

  const props = defineProps(aaaProps)
  const emits = defineEmits(aaaEmits)

  onMounted(() => {
    console.log('mounted')
  })

  // init here
</script>

添加样式

样式文件

组件的样式文件单独放在packages/theme-chalk/src

  • 所有组件的样式文件入口都在src的根目录下,以[name].scss命名,如button.scss
  • index.scss引入所有组件的样式文件入口,编译出的css是全量样式
  • 若组件样式比较复杂,则创建[name]文件夹,将样式文件拆分到该文件夹下,如date-picker

所以

  1. 创建packages/theme-chalk/src/aaa.scss
  2. packages/theme-chalk/src/index.scss中引入aaa.scss

element-plus源码与二次开发:开发规范与发布

类名规范

类名遵循BEM规范,在SFC文件中提供了命名hooks来帮助生产类名,在scss中提供了mixins来帮助命名

在组件中(components/aaa/src/aaa.vue)使用hooks创建类名:

useNamespace 用来创建和获取BEM规范的类名,自动加--el前缀等

<template>
  <div :class="ns.b()">
    <h1 :class="ns.e('title')">
      <span :class="{ [ns.m('blue')]: color === 'blue' }">aaa</span>
    </h1>
  </div>
</template>

<script lang="ts" setup>
import { useNamespace } from '@element-plus/hooks'

const ns = useNamespace('aaa') // 创建类名 ->  el-aaa
</script>
export  const useNamespace = (block: string, namespaceOverrides?: Ref<string | undefined>) => {
  // ...
  
  return {
    namespace, // 如 'button'
    b, // b() 为 'el-button'
    e, // e('icon') 为 'el-button__icon'
    m, // m('primary') 为 'el-button--primary'
    be, // be('primary', 'icon') 为 'el-button-primary__icon'
    em, // em('icon', 'primary') 为 'el-button__icon-primary'
    bm, // bm('primary', 'icon') 为 'el-button-primary--icon'
    bem, // bem('primary', 'icon', 'active') 为 'el-button-primary__icon--active'
    is, // is('loading', true) 为 'is-loading'
    // css
    cssVar, // cssVar({ color: 'red' }) 为 { '--el-color': 'red' }
    cssVarName, // cssVarName('color') 为 '--el-color'
    cssVarBlock, // cssVarBlock({ color: 'red' }) 为 { '--el-button-color': 'red' }
    cssVarBlockName, // cssVarBlockName('color') 为 '--el-button-color'
  }
}

在每个样式文件中,引入的mixins中包含了与上面hooks同名的mixins

@use 'mixins/mixins' as *;
@use 'mixins/utils' as *;
@use 'common/var' as *;

// 使用b(组件名)  创建BEM格式类名
@include b(aaa) {
  @include e(title) {
    color: red;
  }
  @include m(blue) {
    color: blue;
  }
}

// 一些常使用的mixins
@include when(active) {} // is-active

首先引入mixins/mixins,里面包含了命名前缀变量、命名函数、

  • 命名前缀:
$namespace: 'el' !default;
$common-separator: '-' !default;
$element-separator: '__' !default;
$modifier-separator: '--' !default;
$state-prefix: 'is-' !default;
  • 命名函数 function.scss
// getCssVarName('button', 'text-color') => '--el-button-text-color'
@function getCssVarName($args...) {}

// getCssVar('button', 'text-color') => var(--el-button-text-color)
@function getCssVar($args...) {}

// getCssVarWithDefault(('button', 'text-color'), red) => var(--el-button-text-color, red)
@function getCssVarWithDefault($args, $default) {}

// 生成BEM规范的类名
// bem('block', 'element', 'modifier') => 'el-block__element--modifier'
@function bem($block, $element: '', $modifier: '') {}
  • common/var.scss

定义了基础颜色、背景色、边框等通用样式变量,如el-color-primary

$types: primary, success, warning, danger, error, info;

/* 
	这些颜色变量由packages/theme-chalk/src/mixins/_var.scss
	中的set-css-color-type声明,实际只声明了(3,5,7,8,9)几个级别,若要使用其他级别,需对其进行修改
*/
$colors: (
  'white': #ffffff,
  'black': #000000,
  'primary': (
    'base': #409eff,
    'light-1': mix($color-white, #409eff, 10%),
    'light-2': mix($color-white, #409eff, 20%),
    ...
    'light-9': mix($color-white, #409eff, 90%),
    'dark-2': mix($color-black, #409eff, 20%)
  ),
  'success': (
    'base': #67c23a,
    'light-1': mix($color-white, #67c23a, 10%),
    'light-2': mix($color-white, #67c23a, 20%),
    ...
    'light-9': mix($color-white, #67c23a, 90%),
    'dark-2': mix($color-black, #67c23a, 20%)
  )
  ...
);

$font-size: (
  'extra-large': 20px,
  'large': 18px,
  'medium': 16px,
  'base': 14px,
  'small': 13px,
  'extra-small': 12px
);
$text-color: ();
$border-color: ();
$fill-color: ();
$bg-color: ();
$border-width: 1px !default;
$border-style: solid !default;
$border-color-hover: getCssVar('text-color', 'disabled') !default;

$button: () !default;
$button: map.merge(
  (
    'font-weight': getCssVar('font-weight-primary'),
    'border-color': map.get($border-color, ''),
  	//...
  ),
  $button
);

// 更多变量可以直接看packages/theme-chalk/src/commomn/var.scss
// 有些变量是映射(Map),如$font-size。 有些是值,如$border-width

// 另外还包含了一些具体某些组件使用的变量
// 这些组件变量可能被其他组件使用
// 如$checkbox-bordered-padding-left,除了被checkbox.scss引用,还被radio.scss引用

另外还包含一些具体组件的变量,这些变量也因为可能被其他组件使用,如$button变量,除了被Button使用,可能还被Input等组件使用。

变量在组件的样式文件中(如button.scss)有两种形式使用

  • 直接使用,如$border-widthmap.get($button, 'font-weight')
  • 变量值是map类型时,可以设置为局部css变量使用,如对于$button
@include b(button) {
	// 将map类型的$button的所有属性,在.el-button内部注册为变量
  // 如: --el-button-font-weight: 400
  @include set-component-css-var('button', $button);
}

@include b(button) {
  // 使用注册的变量
  font-weight: getCssVar('button', 'font-weight')
}

theme-chalk/src/var.scss

创建一些全局css变量

--el-color-white
--el-color-black
--text-color
--box-shadow
...

创建组件文档

添加左侧菜单

菜单配置取自i18n/pages/component.json,而上文说过i18n文件由crowdin生成。

添加菜单标准流程应该是先修改crodiwn/en-US/pages/component.json,然后在crowdin上翻译,再下载翻译文件到本地。

这里我们就直接修改本地文件了,注意,语言文件的处理是以en-US为模板的,只在本地的zh-CN下的component.json中添加字段是无效的,需要保证英文语言文件中存在该字段(值可以为空),或者可以修改多语言处理逻辑,以中文语言为模板,甚至直接使用中文语言文件,移除英文语言。

element-plus源码与二次开发:开发规范与发布先在/en-US/pages/component.json中添加aaa组件菜单

element-plus源码与二次开发:开发规范与发布同样的,再在中文语言文件/zh-CN/pages/component.json中添加中文菜单

重启文档pnpm docs:dev,这是可以看到菜单已经出来了

element-plus源码与二次开发:开发规范与发布

创建组件文档页面

创建docs/zh-CN/component/aaa.md(文件名与创建菜单时的link对应,vitepress的规则)

---
title: Aaa 测试组件
lang: zh-CN
---

# Aaa 测试组件

测试用,该组件输出一个红色的 aaa 文本

## 基础用法

直接使用

:::demo 直接引入展示

aaa/basic

:::

:::demo包裹的内容,会加载docs/examples/**/*.vue的内容,显示预览效果和代码展示

docs/examples/aaa/basic.vue:

<template>
  <div class="aaa-container">
    <el-aaa />
  </div>
</template>
<style scoped>
  .aaa-container {
  }
</style>

element-plus源码与二次开发:开发规范与发布

发布

源代码使用git action发布流程

对于源代码的发布流程来说,发布时执行.github/workflows/publish-npm.yml的action,他在github上的执行日志可以查看github.com/element-plu…

摘取关键步骤进行说明:

首先会从推送的git tag获取当前版本号,写入环境变量,然后执行scrips/publish.sh

# 从GITHUB_REF中提取版本号,赋值给环境变量TAG_VERSION
# TAG_VERSION的格式是 refs/tags/v1.0.0  提取出的版本号就是v1.0.0
- name: Get version
	run: echo "TAG_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV

- name: Build&publish
  run: sh ./scripts/publish.sh
	# 指定环境变量
  env:
    NODE_AUTH_TOKEN: ${{secrets.NPM_PUBLISH_TOKEN}}
    TAG_VERSION: ${{env.TAG_VERSION}}
    GIT_HEAD: ${{env.GIT_HEAD}}
    REGISTRY: https://registry.npmjs.com/
    FORCE_COLOR: 2
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    NODE_OPTIONS: --max-old-space-size=4096

publish.sh:

  1. pnpm i 装包
  2. pnpm update:version 从环境变量中读取版本号,覆盖三个包的version等字段
  3. build
  4. 分别发布三个包
pnpm i --frozen-lockfile
pnpm update:version

pnpm build

cd dist/element-plus
npm publish
cd -

cd internal/eslint-config
npm publish
cd -

cd internal/metadata
pnpm build
npm publish
cd -

echo "✅ Publish completed"

手动发布

  1. 修改npm源为私有仓库
  2. 进入packages/element-plus/package.json修改版本号
  3. 在根目录执行pnpm build打包
  4. 进入dist/element-plus手动发布

internal/eslint-configinternal/metadata是类似的流程,我们一般不需要这两个包