element-plus源码与二次开发:构建与发布流程分析
前言
对于中后台项目的基础组件,一般element-plus
和ant design
就够用了,但有时我们还是想对功能做一些补充和调整,如使用功能更强大的vxe-table
替代element-plus
的table
,或者很多公司的UI会有自己的一套设计理念,对默认的样式并不认可(我司也是其中之一),就需要基于现有组件库进行二次开发。
二次开发的方案有两种:
- 作为依赖引入:新建组件库,将原组件库作为依赖引入,在不入侵原组件库源码的基础上进行二次开发和导出。
- 源码开发:fork原组件库源码,直接修改原组件库。
优点 | 缺点 | |
---|---|---|
作为依赖引入 | - 上手难度低,无需了解源码- 后期容易同步原组件库更新 | - 较复杂的改动难以实现 |
源码开发 | - 完全可控,可自由改造任何功能与样式 | - 后期同步原组件库更新需根据更新日志手动同步代码- 需要掌握原组件库源码及其开发规范 |
我这里目前使用的vue
,改造element-plus
,首先尝试上手更简单的方案一,对于基础一些样式来说还好搞定,但是遇到很常见的需要改动dom结构的改造,这种方式就很难操作。于是放弃方案一,直接fork源码进行开发。
另外方案二的主要缺点是当原组件库有更新时,无法快速合并,需要手动介入,但是考虑到基础组件库其实有比较强的稳定性,还是可以接受的。
本篇对element-plus
二次开发的第一步:构建与发布流程进行分析。主要是对package.json
中命令的解读,可以参照我fork出的源码进行阅读,里面有一些更为详细的注释。
对于实际的二次开发及发布流程,可以阅读下篇文章:element-plus源码与二次开发:开发规范与发布
解析
基于v2.3.7版本
首先拉取element-plus源码,执行pnpm i
安装依赖。这里可以拉取我fork出的版本,添加了一些详细的注释。
monorepo
element-plus
使用pnpm来搭建monorepo
工程。
比较大的仓库一般会拆解为多个包,为了方便管理和维护这些互相依赖的包,就有了monorepo
(单一代码仓库)的概念,一方面可以将这些包集中在一个git仓库中,同时在发布时各个包又保持独立性。
在之前一般使用yarn workspace
+lerna
来实现monorepo
,如今pnpm
的workspace成为主流。
在根目录下有文件pnpm-workspace.yaml
,这个文件内声明了当前项目内部可以引用的包,在根目录执行pnpm i
后,会在node_modules
中创建这些包的软链,无需再手动link。
packages:
- packages/*
- docs
- play
- internal/*
如/internal
下有build-utils
文件夹,该文件夹中的package.json
中声明了包名@element-plus/build-utils
,那么在当前项目的任意位置可以直接使用
import { epRoot } from '@element-plus/build-utils'
peerDependencies
"peerDependencies": {
"vue": "^3.2.0"
}
开发过组件库的应该明白这个字段的意义,组件库依赖vue
, 但vue
包一般由使用者在项目中安装,组件库在被使用时应该引用主项目的vue
,因此在开发组件库时将vue
声明到peerDependencies
中,在主项目安装组件库时不会额外安装vue
,若主项目没有符合版本条件的vue
,会提示警告(但不会报错)。
scripts命令
对一些关键命令做下说明
cz - 提交commit
根据git规范,提交commit
dev - 启动调试项目
"dev": "pnpm -C play dev"
npm -C <directory> <command>
指定目录作为工作目录,即在<directory>
中执行<command>
命令,这里是执行play
文件夹中的dev
命令。
play
子包只是一个简单的开发调试包,是一个最简的vite+vue3工程,引入了本地的组件库。当要调试某个组件时,可启动该项目进行调试(我个人一般在docs文档中直接调试,同时修改文档)
gen - 创建新组件
"gen": "bash ./scripts/gc.sh"
使用模板快速创建新组建,使用方式:
pnpm gen <component-name>
如: pnpm gen input-list
会在packages/components
下创建出input-list
文件夹,里面包含组件的基础模板,可以自行创建一个测试组件来查看一下组件模板。
gen:version - 生成版本号文件
"gen:version": "tsx scripts/gen-version.ts"
执行 scripts/gen-version.ts
,根据环境变量或 packages/element-plus/package.json
中的version字段,在 packages/element-plus/
下生成 version.ts
文件, 只有一句,导出当前组件库版本号。
export const version = '0.0.0-dev.1'
用于在构建时(full-bundle.ts
)提供rollup的banner参数
update:version - 部署前更新版本号
从环境变量中获取TAG_VERSION
和TAG_VERSION
,写入到:
- element-plus 或 @element-plus/nightly
- @element-plus/eslint-config
- @element-plus/metadata
三个包的package.json中的version
和gitHead
字段。
这里的环境变量在执行CI/CD时会写入。
clean - 清除dist目录
"clean": "pnpm run clean:dist && pnpm run -r --parallel clean"
pnpm run -r --parallel <command>
是以并行(parallel)方式执行所有子包的<command>
命令(如果有这个命令的话)
删除根目录下的dist
目录,并执行所有子包的clean
命令,即删除所有包的dist
目录
build - 构建文档和组件库
关键命令,在下面进行详解
build:theme - 编译样式文件
编译样式文件,具体参考下文build
命令的buildThemeChalk
任务
docs:dev - 调试文档项目
"docs:dev": "pnpm run -C docs dev"
启动组件库文档docs
项目,基于vitepress。
docs
中的命令如下,生成国际化语言对象并启动vitepress文档:
"dev": "pnpm gen-locale && vitepress dev ."
"gen-locale": "rimraf .vitepress/i18n && tsx .vitepress/build/crowdin-generate.ts"
即实际执行命令:
- 清空历史语言文件
.vitepress/i18n
- 执行
tsx .vitepress/build/crowdin-generate.ts
将多种语言文件合并为一个语言文件对象 - 执行
vitepress dev .
启动本地开发
合并语言文件, 如:
// 英语源文件:en-US/pages/home.json
{
"title": "component doc"
}
// 中文源文件:zh-CN/pages/home.json
{
"title": "组件文档"
}
// 经`crowdin-generate.ts`处理后,生成`i18n/pages/home.json`
{
"en-US": {
"title": "component doc"
},
"zh-CN": {
"title": "组件文档"
}
}
在文档代码中使用时,会取页面访问路径的第一段路由来判断语言
<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]) // => { "title": "组件文档" }
</script>
<template>
<h4>{{ homeLang['title'] }}</h4>
</template>
如element-plus.org/zh-CN 则取zh-CN语言,默认是en-US。
实际上源码中只有en-US
的英语语言文件,使用crowdin处理多语言翻译,其他版本的语言文件是使用CI/CD命令在crowdin
上下载进来的,所以要先将其他语言(如中文)下载下来,再执行这个命令,否则是只有英文的。
crowdin
在crowdin创建项目 -> 上传需要翻译的文件到项目中 -> 在crowdin上自动或手动翻译 -> 下载翻译后的其他语言文件到本地
简单介绍下crowdin
的基本使用流程:
-
首先在官网上创建出项目,配置原文件的语言和需要翻译的语言。
-
使用@crowdin/cli上传。
@crowdin/cli
是使用crowdin
官方出的帮助上传和下载翻译文件的辅助工具。使用@crowdin/cli
对本地项目进行初始化,生成crowdin.yml
配置文件:
'project_id': '473874'
'api_token': 'API_TOKEN_PLACEHOLDER'
'base_path': '.'
'base_url': 'https://api.crowdin.com'
'preserve_hierarchy': true
# files字段配置需要翻译(上传)的源文件,和翻译后的文件下载路径
files: [
{
# source 需要翻译(上传)的源文件
'source': '.vitepress/crowdin/en-US/**/*.json',
# translation 需要下载的翻译后的文件
# %locale% 是翻译的语言如`zh-CN`,`%original_file_name%`是原文件名如`home.json`
'translation': '.vitepress/crowdin/%locale%/**/%original_file_name%',
},
{
'source': 'en-US/**/*.md',
'translation': '%locale%/**/%original_file_name%',
},
]
上面配置的意思是将
.vitepress/crowdin/en-US/
下的所有json
文件en-US/
下的所有markdown
文件
全部上传到crowndin
平台进行翻译,将翻译后的文件下载回到对应的目录。
crowdin upload sources # 上传需要翻译的源文件到平台
crowdin download # 下载翻译后的文件
crowdin download -l zh-CN # 只下载中文
源文件上传后在平台上进行操作,可以自动翻译,如果觉得不够准确也可以进入具体文件的编辑页手动辅助翻译,最后生成翻译后的文件,再使用命令将翻译后的文件下载到本地。
当前element-plus
项目的crowdin地址:element-plus/zh-CN
在进行CI/CD操作时,会执行upload
和download
操作,相关流程见workflows/staging-docs.yml
(github action)
docs:build - 构建组件文档
构建生产环境docs
文档项目
docs:serve
本地启动构建出的生产环境docs
进行测试
docs:gen-locale - 生成多语言文件
"gen-locale": "rimraf .vitepress/i18n && tsx .vitepress/build/crowdin-generate.ts"
参见上文
docs:crowdin-credentials - 生成CROWDIN_TOKEN
从环境变量中获取CROWDIN_TOKEN
的值,将docs/crowdin.yml
中的API_TOKEN_PLACEHOLDER
占位字符串进行替换。
crowdin.yml
是上文提到的@crowdin/cli
生成的配置文件。
stub - 构建internal下的包
"stub": "pnpm run -r --parallel stub"
执行各包的stub
命令, 各个包(internal文件夹下的build
、build-constant
、build-utils
三个工具包)实际执行的命令是 unbuild --stub
unbuild
unbuild是一个打包工具,它基于rollup
,支持typescript
,支持生成commonjs
和esmodule
和类型声明。这些功能不需要额外配置,因为内部集成了一些rollup
插件,另外它使用esbuild来转换js代码,比原生rollup
要快。
--stub
一般在开发时,我们通常使用监听模式,监听文件变化重新触发编译,如rollup,每次修改文件都会重新打包,非常耗时。--stub
模式使用了jiti
,在运行时实时编译源文件,而不是传统的预编译,因此执行一次unbuild --stub
打包命令后,只会对入口文件进行编译,使用jiti接管入口文件,然后进程就会退出,不会监听源文件变化。在代码执行时,入口被jiti接管,jiti会在运行时实时读取源文件,实时编译。
使用--stub
并不会将源码完全编译,只是简单将入口使用编译为使用jiti
运行。
以internal/build/
包为例,原入口文件是index.ts
(文件具体内容并不重要),执行unbuild --stub
,查看dist
中的产物,只有三个文件:
分别是cjs、esm规范和类型声明文件,也对应package.json中指定的包入口:
"main": ".dist/index.cjs",
"module": ".dist/index.mjs",
"types": ".dist/index.d.ts",
常规预编译的模式来说,index.cjs和index.mjs应该包含所有的代码,而看一下dist/index.mjs
:
import jiti from "file:///xxx/node_modules/jiti/lib/index.js";
export default jiti(null, { interopDefault: true })('/xxx/internal/build/src/index');
只是引入jiti
,将原入口文件进行了包装。
在这个文件执行时,jiti
会接管后续所有的import
和require
,在运行时进行实时编译。
这样,在本地开发时,就只需初始化时编译一次入口文件(postinstall
命令会在项目装包后自动执行此初始化工作),后续不用监听文件变化,每次调用时也能保证是使用的最新代码。
jiti
传统模式开发使用了ts
和esmodule
语法的node应用时,都是采用预编译的方案,监听文件变化,每次修改文件进行编译。
而jiti 可以让 node在 运行时直接支持typescript
和 esmodule
代码,底层原理是它在运行时拦截了模块加载请求(require
或import
),将代码转换为可执行函数并进行缓存。
基本使用方式:
a.ts:
export default 'aaa'
main.ts:
import a from './a'
const b: number = 123
console.log(a, b)
index.js:
const jiti = require("jiti")(__filename);
jiti("./main.ts");
执行
node index.js
aaa 123 # 输出
无需配置,可经过jiti
包装后,可直接使用ts
和esm
语法,因为在执行时jiti
会对他们实时编译。因此也无需监听文件变化每次重新编译了。
prepare - Husky钩子脚本
安装Husky钩子脚本
postinstall - 自动预执行命令
执行pnpm i 后会自动执行,主要是执行了pnpm stub
,编译internal
下的三个包入口
"postinstall": "pnpm stub && concurrently \"pnpm gen:version\" \"pnpm run -C internal/metadata dev\""
先执行pnpm stub
命令,再并行执行gen:version
和pnpm run -C internal/metadata dev
pnpm stub
即如上面所解释,使用jiti
编译三个包的入口文件。
internal/metadata
中的scripts
:
"build": "run-p "build:*"",
"build:contributor": "tsx src/contributor.ts",
"build:components": "tsx src/components.ts",
"dev": "DEV=1 pnpm run build"
设置环境变量DEV=1
,然后以并行方式执行
build:contributor
生成贡献者到metadata/dist/contrbutors.json
build:components
生成组件名数组到metadata/dist/components.json
build - 构建文档和组件库
使用rollup执行构建
"build": "pnpm run -C internal/build start"
执行internal/build
目录里的start命令
"start": "gulp --require @esbuild-kit/cjs-loader -f gulpfile.ts"
gulp默认不支持esm和ts,@esbuild-kit/cjs-loader
通过使用esbuild
实时将esm
和ts
转换为CommonJS
在gulpfile.ts
中, 分别以串行和并行的方式执行了一些gulp
命令(这些命令的具体逻辑在internal/build/src/tasks/
下):
- 清除
dist
目录 - 创建
dist/element-plus
目录 - 并行执行任务
buildModules
:
- 编译出
package.json
中main
和module
对应的文件 - 将
packages
下的文件(排除test、mock等相关文件夹),以.js
、.ts
、.vue
文件作为入口,使用rollup
编译为esm
和cjs
,保持原有文件目录结构。「所有第三方包作为外部模块(internal
)不进行打包」; element-plus
作为根目录,其下的文件被提到顶层,其他文件夹作为子文件夹,保持原有目录结构theme-chalk
下都是样式文件(.scss
),因此未被打包
buildFullBundle
: 打包完整bundle
,除了vue
,全部打包进bundle
,并在package.json
中声明unpkg
、jsDelivr
字段,以作为这两个公共CDN的默认入口。generateTypesDefinitions
: 生成packages
下的类型声明文件到dist/types/packages
下buildHelper
为组件生成代码提示文件(vetur
和webstorm
)- 串行任务
buildThemeChalk
: 复制源文件(scss
)、编译组件的样式文件(css
)到dist/theme-chalk
下;生成全量样式文件(index.css
)copyFullStyle
: 将全量样式文件index.css
移动到dist
目录
- 并行执行任务
copyTypesDefinitions
: 将上面生成的类型声明文件,按照目录结构同时拷贝到es(esm)
和lib(cjs)
下copyFiles
: 将element-plus
的package.json
、根目录README.md
、global.d.ts
拷贝到dist/element-plus
export default series(
// withTaskName只是给函数加上displayName属性,这样在gulp执行时,就可以显示出函数的名字
withTaskName('clean', () => run('pnpm run clean')),
withTaskName('createOutput', () => mkdir(epOutput, { recursive: true })),
parallel(
// runTask: 在 internal/build目录下,使用child_process执行 pnpm run start xxx 即执行src/tasks中的gulp的xxx任务
runTask('buildModules'),
runTask('buildFullBundle'),
runTask('generateTypesDefinitions'),
runTask('buildHelper'),
series(
withTaskName('buildThemeChalk', () =>
run('pnpm run -C packages/theme-chalk build')
),
copyFullStyle
)
),
parallel(copyTypesDefinitions, copyFiles)
)
buildModules
将packages下的所有js、ts、vue文件使用rollup打包,保持原有文件目录结构。
这里需要注意的是,使用vue-marcros与vue3
的语法做了扩展和兼容
// 将packages下的所有js、ts、vue文件使用rollup打包,保持原有文件目录结构
export const buildModules = async () => {
// excludeFiles 排除['node_modules', 'test', 'mock', 'gulpfile', 'dist']下的文件
const input = excludeFiles(
await glob('**/*.{js,ts,vue}', {
cwd: pkgRoot,
absolute: true,
onlyFiles: true,
})
)
// 使用rollup的[JavaScript API](https://www.rollupjs.com/guide/javascript-api)
const bundle = await rollup({
input,
plugins: [
ElementPlusAlias(), // 将@element-plus/theme-chalk开头的import路径替换为element-plus/theme-chalk,并标记为外部模块
VueMacros({
// [官方文档](https://vue-macros.sxzz.moe/zh-CN/guide/getting-started.html)
// 用于实现尚未被 Vue 正式实现的提案或想法。提供更多宏和语法糖到 Vue 中,其中某些功能在新版本的Vue中已经官方实现了
// 具体扩展的宏,参阅: https://vue-macros.sxzz.moe/zh-CN/macros/
setupComponent: false,
setupSFC: false,
plugins: {
vue: vue({
isProduction: false,
}),
vueJsx: vueJsx(),
},
}),
nodeResolve({
// 查找到外部(node_modules)模块
// [Node resolution algorithm](https://nodejs.org/api/modules.html?spm=a2c6h.24755359.0.0.4f4461advo8dzO#modules_all_together)
extensions: ['.mjs', '.js', '.json', '.ts'], // 自动查找扩展名
}),
commonjs(), // 将CommonJS模块转换为ES6,以便rollup可以处理它们
esbuild({
// 使用 esbuild 执行代码转换和压缩
sourceMap: true,
target,
loaders: {
'.vue': 'ts',
},
}),
],
// 标记所有element-plus的dependencies、peerDependencies以及'@vue'开头的包为external(外部包)
// 在使用时,babel一般会忽略node_modules里的包,但是因为这里已经转为了es5语法,所以语法是没有问题的。另外虽然babel不会处理,但是webpack、rollup等打包工具仍会处理依赖关系,最终将这里忽略的外部包打包到最终产物里。
external: await generateExternal({ full: false }), // 标记外部模块,不打包进bundle
treeshake: false,
})
// 使用Promise.all, 遍历buildConfigEntries(esm和cjs)生成的options,多次调用bundle.write()方法生成文件
await writeBundles(
bundle,
buildConfigEntries.map(([module, config]): OutputOptions => {
return {
format: config.format, // esm | cjs
dir: config.output.path, // dist/element-plus/(es | lib)
exports: module === 'cjs' ? 'named' : undefined,
preserveModules: true, // 保留模块结构,打包产物和源码文件结构一致。使用原始模块名作为文件名,为所有模块创建单独的 chunk,而不是创建尽可能少的 chunk
preserveModulesRoot: epRoot, // 以element-plus为产物根目录,打包后原element-plus作为根目录,其他模块在此目录下保持原目录结构
sourcemap: true,
entryFileNames: `[name].${config.ext}`, // chunks入口文件名
}
})
)
}
buildModules任务打包的产物
Vue Macros
在使用rollup打包时,使用了一个插件unplugin-vue-macros
,这个插件用来实现一些尚未被Vue正式实现的提案或想法,提供更多宏和语法糖到Vue中。其中某些功能在新版本的Vue中已经获得了官方支持。
如defineOptions
,可以通过 defineOptions
宏在 <script setup>
中使用选项式 API,也就是说可以在一个宏函数中设置 name
, props
, emits
, render
,此功能在Vue 3.3版本开始官方支持,在 Vue >= 3.3 中,vue macros会默认关闭此功能兼容,使用官方实现。
<script setup lang="ts">
import { useSlots } from 'vue'
defineOptions({
name: 'Foo',
inheritAttrs: false,
})
const slots = useSlots()
</script>
更多宏请查阅全部宏
buildFullBundle
构建浏览器可直接引用的完整bundle
,与buildModules
逻辑类似,只是将除peerDependencies
(即vue
)外的所有依赖都打包进去。另外,字体文件(local
文件夹)是单独打包的,CDN引用的时候单独引用。
async function buildFullEntry(minify: boolean) {
// 打包完整bundle
// minify控制是否执行minify,生成.min.js文件
// 具体逻辑解释可参考buildModules
}
async function buildFullLocale(minify: boolean) {
// 打包字体文件
}
buildFullBundle打包产物
在package.json中声明unpkg
、jsDelivr
字段,以作为这两个公共CDN的默认入口
"unpkg": "dist/index.full.js",
"jsdeliver": "dist/index.full.js"
jsDelivr & unpkg
jsDelivr和unpkg是免费的开源公共CDN,可以按照固定的规则直接通过他们加载公开的npm和github静态资源文件。
以jsDelivr加载npm资源为例,//cdn.jsdelivr.net/npm/<package>[@version][/file]
, 若不指定版本号和文件,默认会首先查找package.json中的jsDelivr
字段指定的文件
<link
rel="stylesheet"
href="//cdn.jsdelivr.net/npm/element-plus/dist/index.css"
/>
<script src="//cdn.jsdelivr.net/npm/element-plus"></script>
generateTypesDefinitions
使用ts-morph,将/typings/env.d.ts
和packages/
下的所有文件生成类型声明文件,生成到dist/types
下
import { Project } from 'ts-morph'
import * as vueCompiler from 'vue/compiler-sfc'
/**
* 生成类型定义文件,包括/packages下的所有文件和/typings/env.d.ts
* [ts-morph](https://ts-morph.com/) 是一个用来访问和操作typescript AST的库,可以重构、生成、检查和分析ts代码、生成类型定义等
*
* 1. 将packages下的所有文件作为源文件,并且对vue文件做处理,只提取script部分(对setup类型的脚本使用@vue/compiler-sfc编译为普通脚本)
* 2. 类型检查
* 3. 生成类型声明文件到dist/types 目录结构以types为根目录与源文件一致
*/
export const generateTypesDefinitions = async () => {
// 创建ts-morph项目
const project = new Project({})
// 处理(vue文件)和添加源文件,包括/typings/env.d.ts和packages/下的所有文件
const sourceFiles = await addSourceFiles(project)
// 类型检查 ts-morph提供的api
typeCheck(project)
// emit 生成js和类型声明文件,emitOnlyDtsFiles: true 指定只生成类型声明文件
await project.emit({
emitOnlyDtsFiles: true,
})
// 对生成的类型文件内容做后续处理
// 将 @element-plus/theme-chalk 替换为 element-plus/theme-chalk
// 将 @element-plus 替换为 element-plus/es
}
// 添加和处理源文件,包括/typings/env.d.ts和packages/下的所有文件
async function addSourceFiles(project: Project) {
// 加载源文件/typings/env.d.ts
project.addSourceFileAtPath(path.resolve(projRoot, 'typings/env.d.ts'))
// ...
// 如果是vue文件,需要将其转换为js文件
// 1. 对于vue文件,只提取script脚本
// 2. 如果不是setup脚本,直接输出脚本内容
// 3. 如果是setup脚本,将其编译为标准js内容输出
// 将vue setup语法转为标准js语法 如:
/**
<script setup>
import {ref} from 'vue'
const a = ref(1)
</script>
*/
// 转换后:
/**
"import {ref} from 'vue'\n" +
'\n' +
'export default {\n' +
' setup(__props, { expose: __expose }) {\n' +
' __expose();\n' +
'\n' +
'const a = ref(1)\n' +
'\n' +
'const __returned__ = { a, ref }\n' +
"Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })\n" +
'return __returned__\n' +
'}\n' +
'\n' +
'}',
*/
// 将package/下的所有文件添加到project中
}
生成类型声明文件到dist/types中
buildHelper
使用components-helper读取markdown文档,自动生成组件代码提示文件
- 生成
vetur
的组件代码提示文件attributes.json
和tags.json
- 生成
webstorm
的组件代码提示文件web-types.json
volar可直接使用tsconfig.json
中的类型声明配置,因此有类型声明文件就够了,根据element-plus
的官方文档中volar配置,在tsconfig.json
中配置,读取element-plus/global
:
"compilerOptions": {
"types": ["element-plus/global"]
}
这个文件来自根目录的global.d.ts
global.d.ts
这里引入了全部组件的类型定义,但是并没有找到自动更新该文件的地方(如执行gen
命令创建新组件),那么在创建新组件时需要在这里手动引入新组件的类型声明。
buildThemeChalk
所有组件的样式文件并没有主动引入,单独存放在package/theme-chalk
下。
buildThemeChalk
任务复制源样式文件(scss
),编译为css
,压缩,文件名加el-
前缀,输出到packages/theme-chalk/dist/
下
样式文件目录的规则:
- 所有组件的样式文件入口都在
src
的根目录下,以[name].scss
命名,如button.scss
index.scss
引入所有组件的样式文件入口,编译出的css是全量样式- 若组件样式比较复杂,则创建
[name]
文件夹,将样式文件拆分到该文件夹下,如date-picker
编译后结果,src内是源scss文件,dark是黑暗模式的变量
转载自:https://juejin.cn/post/7257736231882407995