点击上方卡片“前端司南”关注我
您的关注意义重大
原创@前端司南
前言
本文是 基于Vite+AntDesignVue打造业务组件库[1] 专栏第 3 篇文章【实战案例:初探工程配置 & 图标组件热身】,我将从业务系统中最基础的图标组件入手,带着读者们练练手找找感觉,快速进入开发状态,顺便了解一些基本的前端工程配置。
引入Formatter/Linter工具
在正式地开发组件之前,我们需要一点点准备工作。
为了提高开发效率,避免低级错误,我们有必要先引入一些工具,毫无疑问,ESLint, Prettier, StyleLint 可以先安排上,相关配置点到为止,不会一来就堆大量的配置。
首先我们把 VSCode 的相关插件安装好。
由于我们用的是 Vue3 开发组件库,Volar 也可以直接安装上!
我们还将这些插件加到了.vscode/extensions.json
中,这样别人打开这个项目时,VSCode 就会自动推荐 ta 安装相关的插件。
ESLint
然后我们从 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
接着我们把负责样式风格和质量的 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 之前,大家也活得挺好。做这个决定前要搞清楚 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
}
}
TypeScript
我们尝试把一些原有的 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
我们测试一下能不能正常使用 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 图标等,我们就先从字体图标开始。
准备一个 iconfont 项目
每个业务项目用到的图标肯定是有差异的,我们先选一些图标做个示例,为了方便,这里直接选用了一套阿里云官网官方图标库[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>
可以发现效果已经出来了。但是我们是写死测试的,要实现一个可复用的图标组件,显然还要预留一些属性交给外部配置,很容易想到的属性有:
图标的名称:用来唯一确认一个图标,一个名称对应一个 class,这个 class 会对应一个唯一的 Unicode 编码。
图标的颜色:字体图标本身也是一个“字”,和普通的字没啥区别,所以可以用
color
属性控制颜色。图标的大小:同上,可以用
font-size
控制图标的大小,但是通过font-size
只能控制一个大概的大小,并不等价于绝对意义上的宽高。下面是我设置font-size: 15px
的效果,可见真实的高度并不是15px
。
如果你希望控制地很彻底,那就应该另外通过width
和height
去控制了。但是我认为大部分情况,没有这个必要,用font-size
粗略控制一下字体图标的大小就差不多了。
图标的 class 前缀:目前也是写死的
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
END
如果觉得这篇文章还不错
点击下面卡片关注我
来个【分享、点赞、在看】三连支持一下吧
“分享、点赞、在看” 支持一波