element-plus源码与二次开发:开发规范与发布
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
也可直接点击下载
下载后的语言文件
需要注意的是.vitepress
在文件系统中默认是隐藏的,需要在命令行工具或vscode中操作
将两个zh-CN
文件夹按照目录结构放到docs的对应目录下,即一个放到docs
根目录下,一个放到docs/.vitepress/crowdin/zh-CN
。
重新执行pnpm docs:dev
.DS_Store报错
在手动复制完文件后再次执行pnpm docs:dev
,可能会报错
编辑docs/.vitepress/build/crowdin-generate.ts
重新启动后,就可以切换为中文了
注意这里因为语言识别的缓存逻辑,首次如果进入过/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配置在页面引入了一些脚本,其中就包括国际化相关的脚本
languages
即.vitepress/corwdin
下语言名字数组
注入.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
关键字相关逻辑与文件
移除埋点相关逻辑
- 在
head.ts
中移除googletagmanager
、hotjar
相关代码; - 移除
analytics.ts
- 移除文档各处对
analytics.ts
的调用
配置左侧菜单
.vitepress/config.mds
中的自定义主题配置项themeConfig
配置了nav
、sidebars
等
右上角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>
源代码跳转链接
每个组件的文档底部会有「当前组件」、「当前文档页」的源码页面,贡献者
在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.ts
中createGitHubUrl
的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
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
- 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
所以
- 创建
packages/theme-chalk/src/aaa.scss
- 在
packages/theme-chalk/src/index.scss
中引入aaa.scss
类名规范
类名遵循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-width
、map.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
中添加字段是无效的,需要保证英文语言文件中存在该字段(值可以为空),或者可以修改多语言处理逻辑,以中文语言为模板,甚至直接使用中文语言文件,移除英文语言。
先在/en-US/pages/component.json中添加aaa组件菜单
同样的,再在中文语言文件/zh-CN/pages/component.json中添加中文菜单
重启文档pnpm docs:dev
,这是可以看到菜单已经出来了
创建组件文档页面
创建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>
发布
源代码使用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:
- pnpm i 装包
- pnpm update:version 从环境变量中读取版本号,覆盖三个包的version等字段
- build
- 分别发布三个包
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"
手动发布
- 修改npm源为私有仓库
- 进入
packages/element-plus/package.json
修改版本号 - 在根目录执行
pnpm build
打包 - 进入
dist/element-plus
手动发布
internal/eslint-config
和internal/metadata
是类似的流程,我们一般不需要这两个包
转载自:https://juejin.cn/post/7257787885863731255