原创@前端司南
本文是 基于Vite+AntDesignVue打造业务组件库[1] 专栏第 3 篇文章【实战案例:初探工程配置 & 图标组件热身】,我将从业务系统中最基础的图标组件入手,带着读者们练练手找找感觉,快速进入开发状态,顺便了解一些基本的前端工程配置。
在正式地开发组件之前,我们需要一点点准备工作。
为了提高开发效率,避免低级错误,我们有必要先引入一些工具,毫无疑问,ESLint, Prettier, StyleLint 可以先安排上,相关配置点到为止,不会一来就堆大量的配置。
首先我们把 VSCode 的相关插件安装好。
由于我们用的是 Vue3 开发组件库,Volar 也可以直接安装上!
我们还将这些插件加到了.vscode/extensions.json
中,这样别人打开这个项目时,VSCode 就会自动推荐 ta 安装相关的插件。
然后我们从 ESLint[2] 开始配置环境。
打开官网,可以看到官方已经给我们提供了相关命令,我们执行npm init @eslint/config
初始化一下。
会发现安装依赖过程中 Yarn 给我们抛了一个错误。在 workspaces 特性启用时,Yarn 默认认为我们执行yarn add
时是希望将依赖安装到某个 workspace 下面而不是工程的根目录下。而这里,我们需要将 eslint 的这些依赖安装到工程的根目录下,可以加上-W
参数手动安装一下依赖,这些依赖在上面的日志信息中可以找到。
yarn add -DW eslint-plugin-vue@latest eslint-config-standard-with-typescript@latest @typescript-eslint/eslint-plugin@^5.0.0 eslint@^8.0.1 eslint-plugin-import@^2.25.2 eslint-plugin-n@^15.0.0 eslint-plugin-promise@^6.0.0 typescript@*
此时可以发现 eslint 已经在报一些错误了。
而对于 indent,我习惯用 4 个 space,这里自定义一下 rule。
"rules": {
"indent": ["warn", 4]
}
改完之后,indent 相关的报错信息消失了,而其他的错误依旧在,此时,还只能通过右键菜单来进行 Format,不是特别方便。
为了方便使用和自动修复一些代码质量问题,我们把 VSCode 和 ESLint 的 Fix 能力结合一下。我们新增一个.vsocde/settings.json
,配置如下:
{
"editor.tabSize": 4,
"eslint.validate": ["javascript", "typescript", "javascriptreact", "typescriptreact", "vue"],
// 防止内置css校验和stylelint重复报错
"css.validate": false,
"less.validate": false,
"scss.validate": false,
"editor.formatOnSave": false,
// 代码保存动作
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.fixAll.stylelint": true
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[vue]": {
"editor.defaultFormatter": "Vue.volar"
},
"typescript.tsdk": "node_modules/typescript/lib"
}
接着验证一下是不是生效了,从下图中可以看到,保存代码可以自动 format 了。
基本的路子摸清后,我们可以完善一下 JavaScript 的编码风格规范了,闭着眼睛推荐eslint-config-airbnb-base
,具体规范可以参考airbnb/javascript
(请自行上github找一下),阅读一遍有助于培养良好的编码意识。
yarn add -DW eslint-config-airbnb-base eslint-plugin-import
eslint 关键配置:
extends: [
'eslint:recommended',
'plugin:import/recommended',
'airbnb-base'
],
plugins: ['import'],
It requires
eslint
andeslint-plugin-import
. eslint-plugin-import 是 eslint-config-airbnb-base 要求安装的,同时也是开发过程中的一个利器,保证我们能按预期使用 ES 的模块 import/export。
接着我们把负责样式风格和质量的 StyleLint[3] 也配置一下,这里顺手安装了几个 config,包括 StyleLint 的标准配置以及应用到 SCSS-like 文件 和 Vue 文件的特有配置。
yarn add -DW stylelint stylelint-config-standard stylelint-config-standard-scss stylelint-config-standard-vue postcss-html
postcss-html 是与 stylelint-config-standard-vue 配合使用的。
初始配置文件可以简单引入上面那几个 config。
module.exports = {
extends: [
'stylelint-config-standard',
'stylelint-config-standard-scss',
'stylelint-config-standard-vue/scss'
],
rules: {
indentation: 4
}
}
为了与 VSCode 更好地集成,我们修改一下.vscode/settings.json
,加入以下配置:
"stylelint.validate": [
"css",
"less",
"vue"
]
此时我们随意修改一下样式,测试一下效果,可以看到基本的提示和修复能力都有了。
项目中要不要使用 Prettier 取决于个人,没有强制的要求,毕竟没有 Prettier 之前,大家也活得挺好。做这个决定前要搞清楚 Prettier 和 ESLint / StyleLint 这类 Linter 扮演的角色分别是什么。简单说就是 Prettier 负责代码风格,而 Linter 负责代码质量。
引用官方文档的一句话:Prettier for formatting and linters for catching bugs!
读过 Prettier 的这篇文档[4]你就可以知道,Prettier 和 Linters 会有一些功能交叉和规则冲突。功能交叉指的是 Linter 除了负责代码质量外,本身也可以定义规则约束代码风格,这就有可能会与 Prettier 的代码风格产生冲突。这个时候,就需要通过 Linter 体系中的一些插件配置关掉一部分与 Prettier 有冲突的规则,尽量在风格上以 Prettier 为准,比如 eslint-config-prettier[5] 和 stylelint-config-prettier[6]。
我们安装一下 Prettier 和相关配套试试:
yarn add -DW prettier eslint-config-prettier stylelint-config-prettier
新建一个prettier.config.js
配置文件,写入一些简单的配置:
module.exports = {
tabs: false,
tabWidth: 4,
endOfLine: 'auto',
semi: false,
singleQuote: true
}
接着把eslint-config-prettier
和stylelint-config-prettier
配置好。
// eslint 配置
extends: [
// 引入 eslint-config-prettier
'prettier'
],
// stylelint 配置
extends: [
// 引入 stylelint-config-prettier
'stylelint-config-prettier'
],
此时,我们会发现随意修改 vue 文件后,对于一些低级的代码风格问题,VSCode 提示都没有了。
我去,这都不报错!看来是eslint-config-prettier
把有冲突的 rules 关得很彻底!好,这个时候我们需要把 Prettier 的输出反馈给 ESLint,让 ESLint 来做提示,这需要用到 eslint-plugin-prettier[7]。
Runs Prettier[8] as an ESLint[9] rule and reports differences as individual ESLint issues.
先安装一下依赖,
yarn add -DW eslint-plugin-prettier
然后把下面的 ESLint 配置做好,这相当于把 Prettier 作为 ESLint 检查工序中的一个环节了。
// .eslintrc.js
{
"plugins": ["prettier"],
"rules": {
"prettier/prettier": "error"
}
}
哥们儿,这感觉嘎嘎上来了,要的就是这个效果,看来这就是 Prettier 接管了 ESLint 一部分工作的精髓啊!
类似地,我们把stylelint-prettier
也安装一下。
yarn add -DW stylelint-prettier
修改配置:
// stylelint.config.js
{
"plugins": ["stylelint-prettier"],
"rules": {
"prettier/prettier": true
}
}
我们尝试把一些原有的 js 文件改成 ts,会发现 ESLint 先报了一个错,这是因为 ESLint 的内置 parser Espree[10] 不能处理 ts 文件,我们需要引入新的 parser。
对于 ESLint 和 TypeScript 的结合,我们主要关注这个仓库 typescript-eslint[11],这里面有我们需要的 @typescript-eslint/parser
和@typescript-eslint/eslint-plugin
。
我们先安装一下这些依赖:
yarn add -DW typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin
新建一个tsconfig.json
,基本内容如下:
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": false,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"baseUrl": "./",
"rootDir": ".",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"resolveJsonModule": true,
"allowJs": true,
"strictNullChecks": true,
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
},
"include": ["**/*.ts", "**/*.tsx", "**/*.vue", "**/tests/**/*.ts", "**/tests/**/*.tsx", "**/components.d.ts"],
"exclude": ["node_modules"]
}
接着在.eslintrc.js
中把 @typescript-eslint/parser
和@typescript-eslint/eslint-plugin
及相关配置处理好。
一切都比较符合预期,但是当我们打开一个.vue
文件时,会发现有报错信息:
Parsing error: "parserOptions.project" has been set for @typescript-eslint/parser. The file does not match your project config: packages\vue-pro-components\src\icon\icon.vue. The extension for the file (.vue) is non-standard. You should add "parserOptions.extraFileExtensions" to your config.
我的第一反应是认为我们配置的@typescript-eslint/parser
无法识别.vue
文件,这时候就需要用到vue-eslint-parser
了。
然而引入vue-eslint-parser
并把基本配置做好后,这个报错依然没有消失。想着关机一次试试,没有用。等了两天再打开,又不报错了,没想明白。不过vue-eslint-parser
肯定是少不了的。
TypeScript 在配合eslint-plugin-import
使用时,我们还需要配置一下eslint-import-resolver-typescript[12],这个在相关插件的文档中也有提到。否则会报这些错误:
yarn add -DW eslint-import-resolver-typescript
补充的关键配置如下:
// .eslintrc.js
extends: [
// ...
'plugin:import/typescript',
],
settings: {
// ...
'import/resolver': {
typescript: {
alwaysTryTypes: true, // always try to resolve types under `<root>@types` directory even it doesn't contain any source code, like `@types/unist`
// Multiple tsconfigs (Useful for monorepos)
// use an array of glob patterns
project: ['tsconfig.json', 'packages/*/tsconfig.json'],
},
},
},
我们测试一下能不能正常使用 less 开发,先把icon.vue
的style block
的lang
设置为less
试试。
由于 package vue-pro-components
中的文件都改成 ts 了,其中也包括入口文件index.ts
,所以我们还需要把package.json
的main
入口修改成index.ts
,才能顺利调试。
但是把 dev 环境跑起来后,还是报了 less 相关的错误。
由于 dev 环境是 package playground
的,只是它引用了 package vue-pro-components
中的Icon
组件,所以是 package playground
的环境中缺失less
,我们给它安装一下less
依赖。
lerna add less --scope=playground --dev
基本的环境准备好之后,我们来实现一个简单的 Icon 组件热热身。
虽然 UI 组件库都标配了 Icon 组件,但是这些图标通常来说是不够用的,很难满足不同项目的需求,所以有必要自己实现一个 Icon 组件,能够方便地管理和使用图标。
前端与 UI 设计师通常利用 iconfont 来进行图标协作,图标的表现形式有字体图标,SVG 图标等,我们就先从字体图标开始。
每个业务项目用到的图标肯定是有差异的,我们先选一些图标做个示例,为了方便,这里直接选用了一套阿里云官网官方图标库[13],然后把这些图标抄到自己的图标项目中。
大概看了一下,图标也挺多的,一个个加到购物车手也会很累。于是我观察了一下 DOM 结构,发现可以用脚本模拟一下点击加购物车的行为,那就不浪费时间了,直接上脚本。
[...document.querySelector(".collection-detail ul.block-icon-list").children].forEach(child => {
child.children[2].children[0].click()
})
图标复制到项目中后发现图标的默认命名有点呆,全是 icon-test11 这样的,辨识度太低。懒得一个个改名字,最后还是换了一个图标库 Hippo Design 官方图标库[14]。
顾名思义,字体图标本质上也是利用字体文件来展示图标的。字符的展示是依赖字符编码的,从 ASCII 到 Unicode,字符集也在不断丰富。计算机并不认识文字、符号或图标,本质上都是通过字符编码结合字体文件、排版引擎等来做渲染的。而 Unicode 预留了E000-F8FF
范围作为私有保留区域,这个区间的 Unicode 码可以用来自定义一些内容,那么用来做字体图标显然也是非常合适,前端根据 Unicode 码就能显示对应的图标。
PUA[15],即 Private Use Areas,私人使用区相同的代码点可被分配为不同的字符,因此用户可能因安装了某种字体,看到其显示为一种形态,但使用了其他字体的用户可能看到完全不同的字符。 这也就是说,不同的字体文件可以重复利用这个区域的 Unicode,但是可以展示出不同的形态,这也就可以理解为什么我们能展示各种各样的图标了。
然而直接用 Unicode 并不方便记忆和理解,所以我们会在 Unicode 编码基础上再封装一层,通过不同的 class 结合伪元素来表现图标,类似下面这样:
接上面,你首先需要有一个图标库对应的字体文件,而这个字体文件可以来源于 iconfont。
如果希望偷偷懒,或者不关注 iconfont cdn 的稳定性,你完全可以选择使用在线的 css 文件,这个 css 文件中也会引用在线的 ttf 等字体文件。
如果你关注内容的稳定性,不希望因为 iconfont cdn 问题导致业务损失,那么我建议把相关资源(包括 css 文件及其关联的字体文件)下载到项目中使用。
还有一个要考虑的问题,字体文件这些资源放在组件库中加载合适,还是放在业务项目中加载合适?
我想,应该是放在业务项目中加载字体文件等资源比较合适。因为不同的业务项目用到的图标库肯定是有差异的,如果把字体文件内置到图标组件中,就会导致图标库都是一样的,显然没法满足各个项目的需求。
而在我们的 monorepo 工程中,playground
就扮演着业务项目的角色,可以用来测试组件库的表现,所以我们先在playground
中引入生成的在线 css 文件。
字体等资源准备好之后,就可以思考怎么基于这些资源实现组件了。
我们知道,css 文件中已经将各个图标封装成 class 了。
只要我们引用这些 class,就能得到一个字体图标,我们来试试看:
<i class="iconfont vp-icon-layers"></i>
可以发现效果已经出来了。但是我们是写死测试的,要实现一个可复用的图标组件,显然还要预留一些属性交给外部配置,很容易想到的属性有:
color
属性控制颜色。font-size
控制图标的大小,但是通过font-size
只能控制一个大概的大小,并不等价于绝对意义上的宽高。下面是我设置font-size: 15px
的效果,可见真实的高度并不是15px
。如果你希望控制地很彻底,那就应该另外通过width
和height
去控制了。但是我认为大部分情况,没有这个必要,用font-size
粗略控制一下字体图标的大小就差不多了。
vp-icon-
作为前缀,虽然没什么大问题,但是最好留个配置项。我们把属性单独提到一个props.ts
中维护,利用 Vue 提供的 ExtractPropTypes
可以把属性的类型提取出来。
主体逻辑不是很复杂,首先必须引用一个基本的 class iconfont
,这个 class 是用于控制字体等基本属性的。
接着通过iconPrefix
和icon
的拼接组成一个完整的class
,用来映射到具体的图标。
其他的属性color
和size
就是辅助控制颜色和大小的。
组件名可以由文件名自动推断出来,但是为了和文件名解耦,我们还是希望定义一个组件name
。但是在 setup 语法糖下,Vue 官方并没有提供类似于defineProps
这样的编译宏,让我们方便地定义name
,唯一的办法是另外写一个普通的script
块,在其中的默认导出中包含name
字段,但是这显得很不方便。
还好已经有人通过插件解决了这个问题。但是在相关 RFC 的讨论中,似乎尤大也并未完全支持这种做法,具体见 https://github.com/vuejs/rfcs/discussions/430 ,所以是否采纳这种做法还值得考虑一番。
换个思路,咱们先在icon/index.ts
中扩展一下name
字段。
我们可以观察到,在 Volar 的加持下,模板中的组件类型显得还比较完善。
但是在 ts 上下文中,组件的类型似乎还未展示出来。
与此同时,组件还没有对应的install
方法,这样就不能单独作为一个插件被use
。我们借鉴一下其他 UI 组件库的做法,用一个withInstall
函数把组件包装一下。
考虑到这类通用的工具方法还可以要暴露给外部项目使用,我们可以把工具方法封装到@vue-pro-components/utils
这个包中。
接着 Icon 的导出部分就可以写成这样了:
而且我们能看到,这个时候 Icon 的类型提示也出来了。
在本节中,我们继续完善了一些工程化配置,但是在配置上也是点到为止,没有堆砌太多的插件或者配置项,以防让人眼花缭乱,无法抓到重点。接着,我们通过一个字体图标组件需求的实战,初步掌握了如何组织起一个组件。接下来,我们会继续通过一些实战案例查漏补缺,在实际运用中看看我们还缺失一些什么东西。如果您对我的专栏感兴趣,欢迎您订阅关注本专栏[16],接下来可以一同探讨和交流组件库开发过程中遇到的问题。
[1]
基于Vite+AntDesignVue打造业务组件库: https://juejin.cn/column/7140103979697963045
[2]
ESLint: https://eslint.org/
[3]
StyleLint: https://stylelint.io/
[4]
文档: https://prettier.io/docs/en/integrating-with-linters.html
[5]
eslint-config-prettier: https://github.com/prettier/eslint-config-prettier
[6]
stylelint-config-prettier: https://github.com/prettier/stylelint-config-prettier
[7]
eslint-plugin-prettier: https://github.com/prettier/eslint-plugin-prettier
[8]
Prettier: https://github.com/prettier/prettier
[9]
ESLint: https://eslint.org/
[10]
Espree: https://github.com/eslint/espree
[11]
typescript-eslint: https://github.com/typescript-eslint/typescript-eslint
[12]
eslint-import-resolver-typescript: https://github.com/import-js/eslint-import-resolver-typescript
[13]
阿里云官网官方图标库: https://www.iconfont.cn/collections/detail?spm=a313x.7781069.1998910419.d9df05512&cid=16472
[14]
Hippo Design 官方图标库: https://www.iconfont.cn/collections/detail?spm=a313x.7781069.1998910419.d9df05512&cid=22664
[15]
PUA: https://baike.baidu.com/item/%E7%A7%81%E4%BA%BA%E4%BD%BF%E7%94%A8%E5%8C%BA/61727452?fr=aladdin
[16]
https://juejin.cn/column/7140103979697963045: https://juejin.cn/column/7140103979697963045