theme: channing-cyan
前言
自动化工具?npm 发包?样式按需引入?组件及样式自动引入?组件文档?单元测试?听着有点熟悉又带点陌生😱。。。别急,阅读本系列文章:将从工程化角度带你从0到1实现一个组件库,一站到底!
源码地址:https://github.com/Devil-Training-Camp/virtual-scroll-list-liudingkang
阅读完本篇,你的组件库将具有以下特点:
- 使用 rollup 作为打包工具 ✅🆕
- 支持 babel 和 esbuild 两种构建方式☑️
- 支持 cjs、esm 和浏览器直接引入✅🆕
- 支持组件样式按需引入❓🆕
- 自动引入☑️
- 接入eslint、commitlint 等静态检测工具✅🆕
- 能够进行 npm 发包和产出 changelog☑️
- 提供组件文档和组件示例☑️
- 接入单元测试☑️
话不多说直接开撸。
构建工具 rollup
构建工具选择的是 rollup 而不是 webpack(别问为啥不是 vite)。
Webpack 和 rollup:
- Webpack 的功能丰富,生态比 rollup 更加完善,基本所有可配置环节都做成了可配置,极度灵活,但也造成了学习成本高,配置复杂的缺点。而 rollup 使用起来简单,只需要针对处理加入不同的 plugins 即可。
- webpack 打包产物会加入处理代码,导致代码体积变大,rollup基本只对代码进行转换和整合,打包之后产物更小巧。
- Rollup 是基于 es module 实现的,es module 的静态解析使得 rollup 原生支持 tree-shaking;webpack2 开始支持且消除效果不好,webpack5 支持更好的 tree-shaking。
- Rollup 不支持 hmr。
通过对比可以发现:
在开发应用时,我们要面对各种文件类型打包和代码优化需求,webpack 强大的可配置性和生态更具优势。
如果我们只是构建第三方库,对打包没有那么高的要求,rollup 小巧且配置简单,并且良好的 tree-shaking 支持更加适合。
plugin
Rollup 常用的模块可以参考:https://github.com/rollup/awesome
我们选择了 rollup,需要一些基本的 plugin 来让 rollup 工作:
@rollup/plugin-node-resolve
可以让 rollup 找到外部模块。@rollup/plugin-commonjs
rollup 按照 es module 标准来加载模块,现在为了能够兼容 cjs 模块,使用这个 plugin 来将 cjs module 转化为 es module。
config
build/rollup.base.config.js
export default {
plugins: [
nodeResolve(), // 让 Rollup 可以找到外部模块
commonjs({
include: ['node_modules/**'],
}), // 转换 commonjs module 为 es module],
]
};
开发环境
配置开发环境让我们在开发组件时方便调试,在根目录添加 demo
作为开发调试目录:
其中 main.ts 作为调试环境入口文件:
import { createApp } from 'vue';
import Coms from ‘…/src’; // 组件目录
import App from ‘./src/demo1.vue’;
const app = createApp(App);
app.use(Coms);
app.mount(‘#app’);
rollup 配置
input / output
先来看 input output
,demo/main.ts
作为调试环境入口文件配置为 input
,注意我们需要打包成够在浏览器直接使用的格式, 所以output
输出格式为 umd
。
import base from './rollup.base.config.js';
export const devOptions = {
…base,
input: ‘demo/main.ts’,
output: [
{
format: ‘umd’,
file: ‘dist/main.js’,
globals: {
vue: ‘Vue’,
},
sourcemap: true,
},
],
external: [‘vue’],
};export default [devOptions];
html 模板 / server
需要在本地起一个 server 我们才能够调试,并且需要有一个 html 模板当作 server 的入口页面,然后在文件变化时通知 server 实时更新页面,因此需要:
rollup-plugin-serve
:rollup 的 server 服务插件rollup-plugin-livereload
:自动刷新页面rollup-plugin-delete
:每次打包时删除文件rollup-plugin-html2
:将打包后的文件注入模板 html
import livereload from 'rollup-plugin-livereload'; import serve from 'rollup-plugin-serve'; import html from 'rollup-plugin-html2';
import base from ‘./rollup.base.config.js’;
export const devOptions = {
…base,
plugins: [
…base.plugins,
del({ targets: ‘dist/*’ }), // 每次 build 之前删除 dist
html({
template: ‘demo/index.html’, // 模板路径
onlinePath: ‘.’,
}),
serve({
open: true, // 服务启动时自动打开 openPage
openPage: ‘/dist/index.html’,
port: 8080,
}),
livereload({
watch: ‘dist’,
}),
],
watch: { // rollup 监听文件变化
exclude: ‘node_modules/**’,
},
external: [‘vue’],
};
export default [devOptions];
处理文件
接下来我们来添加插件处理 .vue / .ts / .css
文件:
@vitejs/plugin-vue
+vue
: 注意rollup-plugin-vue
已经不再维护了,但 vite 插件是大部分兼容 rollup 的,实测可行。rollup-plugin-esbuild
:注意开发环境不需要使用babel
。rollup-plugin-styles
:处理样式。
import esbuild from 'rollup-plugin-esbuild'; import styles from 'rollup-plugin-styles'; import vue from '@vitejs/plugin-vue';
import base from ‘./rollup.base.config.js’;
export const devOptions = {
…base,
plugins: [
…base.plugins,
vue(),
esbuild({
include: /.[tj]s?$/,
}),
styles({
// 遵从 assetFileNames 路径
mode: ‘extract’,
}),
],
};export default [devOptions];
构建优化 external
这里有一个小 tips,常规方式我们会将 vue
打包在一起,这样会显著减缓 rollup
构建速度,我们可以配置 external
,将 vue
视为外部模块,然后通过 CDN
引入 vue
,rollup
不用再打包 vue
,将大大提升热更新效率。
export const devOptions = { ...base, input: 'demo/main.ts', output: [ { format: 'umd', file: 'dist/main.js', globals: { vue: 'Vue', // 配置 vue 全局变量名称 }, sourcemap: true, }, ], external: ['vue'], // 将 vue 视为外部模块 };
export default [devOptions];
在 html 模板当中引入 CDN
:
demo
开发测试
当你完成上述配置,在 packagejson 中加入命令:
"dev": "rollup -wc ./build/rollup.dev.config.js",
尝试运行,你的组件库就能够开始开发啦。
编译打包
生产环境和开发环境不同,开发环境在本地环境单一,生产环境往往需要代码具有更好的兼容性,更小的代码体积,同时需要考虑构建性能。接下来我们分别来看如何处理各类文件。
产物格式 es / cjs / umd
我们组件库需要支持 cjs / es 两种模块化方案,同时支持浏览器直接引入,所以还需产出 umd 格式。
配置 output.format
产出不同格式,注意仍然需要配置 external
,组件库本身不需要打包 vue
源码。
这里额外为 umd 产出了 min 压缩形式,如何压缩代码将在下一章节讲解,注意不要忘记为组件库配置全局变量名称 name
,这将作为浏览器直接引入组件库时组件库的全局变量名称。
import base from './rollup.base.config.js';
export default {
...base,
input: 'src/index.ts',
output: [
{
format: 'es',
file: 'dist/virtual-scroll-list.esm.js',
exports: 'named',
},
{
format: 'cjs',
file: 'dist/virtual-scroll-list.cjs.js',
exports: 'named',
},
{
format: 'umd',
name: 'VirtualScrollList',
file: 'dist/virtual-scroll-list.umd.js',
globals: {
vue: 'Vue',
},
exports: 'named', // 消除 export named 和 export default 同时存在警告
},
{
format: 'umd',
name: 'VirtualScrollList',
file: 'dist/virtual-scroll-list.umd.min.js',
exports: 'named',
globals: {
vue: 'Vue',
},
plugins: [minify()],
},
],
external: ['vue'],
};
处理 js / ts
babel
需要按照以下 plugin:
@rollup/plugin-babel
在 rollup 中使用 babel@babel/core
babel corecore-js
polyfill 库。@babel/preset-env
智能预设转换 js 语法。@babel/runtime
提供运行时的 polyfill module。避免全局污染@babel/runtime-corejs3
提供运行时的 polyfill module。比@babel/runtime
更全面,包含 ES 新特性@babel/plugin-transform-runtime
智能聚合模块中重复的 helper,以模块的方式导入
在根目录添加 .babelrc:
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": false
// "corejs": 3
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"helpers": true, // 默认 true 以模块的方式导入重复的 helper
"corejs": 3
}
]
]
}
只需要在 production 时启用 babel,加入 babel plugin 配置:
build/rollup.prod.config.js
export default {
...base.plugins,
plugins: [
babel({
exclude: ['node_modules/**'], // 排除的文件
babelHelpers: 'runtime', // 启用 runtime 方案注入 polyfill helper,主要是为了不与 .babelrc 中方案冲突
extensions: ['.js', '.ts'] // 要处理的文件类型
}),
]
};
typescript
要在 rollup 中使用 typescript 需要安装:
typescript
rollup-plugin-typescript2
rollup ts 插件
同时需要配置 tsconfig.json 文件:
{
"include": ["**/*.ts", "**/*.d.ts"], // 包含的文件
"exclude": ["./node_modules/*", "./dist/*"], // 排除的文件
"compilerOptions": {
"target": "ES2019", // 编译后的目标
"lib": ["ES2020", "DOM", "DOM.Iterable"], // 指定一组描述目标运行时环境的捆绑库声明文件
"module": "ESNext", // 指定生成代码的模块化方式
"esModuleInterop": true, // 简化对导入CommonJS模块的支持。这使得`允许合成默认导入`以实现类型兼容性。
"moduleResolution": "node", // 模块解析方式
"resolveJsonModule": true, // 支持解析 json 模块
"strict": true, // 严格类型检测
// "noUnusedLocals": true, // 在未读取局部变量时报错。
"noImplicitAny": false, // 无隐式 any
// "noImplicitThis": true, // 当`this`具有类型`any`时,启用错误报告
"typeRoots": ["./node_modules/@types"], // 指定一组类型文件夹
"isolatedModules": true, // 确保每个文件都可以安全地传输,而不依赖于其他导入
"declaration": true, // 自动生成声明文件
"outDir": "./dist", // 输出文件夹
"declarationDir": "./dist/types", // 输出时声明文件的文件夹
"skipLibCheck": true, // 跳过类型检查 .d.ts类型脚本中包含的ts文件
"allowJs": true // 允许 js
// "rootDirs": ["src", "demo"]
}
}
更多的配置说明可以参考:https://juejin.cn/post/7078666410339565576
同时还需要加入 rollup plugin 配置:
build/rollup.prod.config.js
export default {
plugins: [
typescript({
verbosity: 2,
check: true, // build 报错
useTsconfigDeclarationDir: true, // 使用 tsconfig.json 中的 declarationDir,而不是依据 output.file
}),
]
};
压缩优化
生产环境下进一步压缩代码体积,这里采用 terser 来压缩 es6 的代码,如果目标环境需要支持 es5,那么可以使用 uglify 来压缩。
需要安装 plugin
@rollup/plugin-terser
然后在 production 环境配置:
build/rollup.prod.config.js
export default {
...base.plugins,
plugins: [
terser(), // 压缩 es6+ 代码 / uglify 压缩 es5
]
};
SWC 和 esbuild
esbuild 给自己的定位是 bundler / compiler,SWC 是 compiler / bundler,相较于 babel,他们各有优劣:
- esbuild 和 SWC 的最大特点就是编译速度极快,比 babel 快几十倍。
- esbuild 具有一定的 compiler 能力,js 转化支持到 es6。SWC 能够支持转化 js 到 es5。
- babel 生态上占优,有丰富的 pugin 和社区。
本项目可以考虑使用 esbuild 来 build,可以使用 rollup-plugin-esbuild
,转化 js 依旧使用 babel。
处理 css / scss
打算采用 scss 预处理器来处理 css,同时采用 postcss 的 plugin 来提供 autoprefixer 和 压缩 css 的能力。
需要安装的 plugin:
-
rollup-plugin-postcss
rollup postcss 插件- extract:可导出单文件 css
- modules:可以配置 css module 相关
- 如果要使用 scss,安装 sass 即可,无需额外配置。
-
sass
-
postcss
-
autoprefixer
依据 browserslist 来自动为目标环境 css 属性添加浏览器私有前缀 -
cssnano
压缩 css
只需要在 production 环境配置:
build/rollup.prod.config.js
export default {
...base.plugins,
plugins: [
postcss({
plugins: [
autoprefixer(), // 依据 browserlist 自动加浏览器私有前缀
cssnanoPlugin(), // 压缩 css
],
extract: 'virtual-scroll-list.css', // 导出 css 为单文件
})
]
};
postcss-preset-env
这是一个 postcss plugin,目的是将 modern CSS 语法转化为大部分浏览器能够理解的,依据 browserslist
添加 postcss plugin。浏览器是否支持现代 css 语法的依据是 MDN 和 Can I Use。
它内置了很多 polyfill plugin,包括 autoprefixer
,可以配置 feature 来自定义 plugin 需求:
export default {
...base.plugins,
plugins: [
postcss({
plugins: [
postcssPresetEnv({
/* pluginOptions */
// features: {'nesting-rules': {noIsPseudoSelector: false,},},
}),
cssnanoPlugin(), // 压缩 css
],
extract: 'virtual-scroll-list.css', // 导出 css 为单文件
})
]
};
配置目标环境
在 package.json 中配置:
{
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
或者在 root 新建 .browserslistrc:
# 只统计占有率大于 1% 的浏览器
> 1%
# 超过 24 个月没有版本更新的浏览器被标记为 dead
not dead
# 兼容 ie 9
ie 9
# 只统计每个浏览器最新两个版本
last 2 versions
这些目标环境配置会被 babel polyfill / postcss-preset-env
等共享。
处理 vue
我们需要让 rollup 能够处理 .vue 文件:
rollup-plugin-vue
+@vue/compiler-sfc
: vue3.xrollup-plugin-vue
+vue-template-compiler
: vue2.x@vitejs/plugin-vue
+vue
: rollup-plugin-vue 已经不再维护了,但 vite 插件是大部分兼容 rollup 的,实测可行。
借助 @vue/compiler-sfc
,一个 .vue 文件会被编译成:
// main script import script from '/project/foo.vue?vue&type=script' // template compiled to render function import { render } from '/project/foo.vue?vue&type=template&id=xxxxxx' // css import '/project/foo.vue?vue&type=style&index=0&id=xxxxxx'
// attach render function to script
script.render = render// attach additional metadata
// some of these should be dev only
script.__file = ‘example.vue’
script.__scopeId = ‘xxxxxx’// additional tooling-specific HMR handling code
// using VUE_HMR_API global
export default script
@vue/compiler-sfc
rollup-plugin-vue / vue-loader / @vitejs/plugin-vue
都借助了 @vue/compiler-sfc
。
@vue/compiler-sfc
使用了 postcss
来处理 css,所以集成 postcss 相关 plugin 就很容易,比如 autoprefixer / cssnano,只要 install 即可。
针对 js/ts/jsx/tsx,@vue/compiler-sfc
会自动 load 对应的 plugin,然后结合我们自己配置的 plugin,使用 @babel/parser
来 parse,所以用户只需要少量配置即可接入 ts / scss。
// @vue/compiler-sfc
var parser$2 = require('@babel/parser');
parser$2.parse(content, {
plugins: resolveParserPlugins(
ext.slice(1),
parserPlugins,
filename.endsWith(".d.ts")
),
sourceType: "module"
}).program.body;
// resolve ParserPlugins
function resolveParserPlugins(lang, userPlugins, dts = false) {
const plugins = [];
if (lang === "jsx" || lang === "tsx") {
plugins.push("jsx");
} else if (userPlugins) {
userPlugins = userPlugins.filter((p) => p !== "jsx");
}
if (lang === "ts" || lang === "tsx") {
plugins.push(["typescript", { dts }]);
if (!plugins.includes("decorators")) {
plugins.push("decorators-legacy");
}
}
if (userPlugins) {
plugins.push(...userPlugins);
}
return plugins;
}
编译测试
当你来到这一步,让我们来测试一下构建,在 packagejson 中加入命令:
"build": "rollup -wc ./build/rollup.prod.config.js",
打包结果:
自动化工具
为你的组件库接入各种自动化工具,它将显著提高你的开发效率和体验。口说无凭,请往下看:
prettier
Prettier 用来格式化代码,prettier 是 opinionated 的,过多的选择不利于团队代码风格统一,让用户少做配置,遵守 prettier 规则即可。
Prettier 的优势在于:很少量的配置,并且不仅仅只是针对于 javascript,它可以格式化多种语言。
Lint 的优势在于:能够进行代码质量检测和修复。
所以这里结合两者的长处采用 prettier 来进行代码格式化,lint 类工具进行代码质量检测。
需要安装:
prettier
增加 .prettierrc.cjs 文件,因为我配置的 package.json:type = module,所以需要带上 cjs 来表明是一个 commonjs module。
module.exports = {
arrowParens: 'avoid', // (x) => {} 箭头函数参数只有一个时是否要有小括号。avoid:省略括号
bracketSpacing: true, // 对象/数组和内容间加空格 { name: 'tina' }
endOfLine: 'auto', // 结束行的形式
printWidth: 100, // 超过换行格数
proseWrap: 'preserve',
semi: true, // 句尾分号
singleQuote: true, // 单引号
tabWidth: 2, // tab 大小
useTabs: false, // 使用 空格缩进而不是 tab
trailingComma: 'all', // 多行打印尾随逗号
vueIndentScriptAndStyle: true, // 允许vue中的script及style 的标签缩进
singleAttributePerLine: true, // 每个元素属性一行
};
eslint + prettier
Eslint 可以用来检测 js 代码质量,我们选择使用 prettier 负责代码格式化,需要关闭和 prettier 冲突的配置,通过 plugin / extend config 形式可以对 eslint 功能进行扩充,我们需要安装:
- "
eslint-config-prettier
": 关闭和 prettier 冲突的 eslint 配置 - "
eslint-config-standard
": standard 规则集配置 - "
eslint-plugin-import
": import 规则集 - "
eslint-plugin-prettier
": 将 prettier 后的结果传递给 eslint,由 eslint 来处理错误提示 - "
eslint-plugin-vue
": vue eslint vue-eslint-parser
:解析 vue 的 eslint 解析器@typescript-eslint/eslint-plugin
:ts 规则集@typescript-eslint/parser
:解析 ts 的 eslint 解析器
接下来需要增加 .eslintrc.cjs 文件,需要注意:
- 这里
prettier
其实是一种简写方式,等价于eslint-config-prettier
。 - 一般 extend config 配置会自动导入 plugin,所以有些 plugin 无需配置。
- 需要配置 parser 才能正确的解析 vue / ts。
- Rules 中可以配置自定义规则,优先级较高。
module.exports = {
root: true, // 指定 config file 位置
env: {
browser: true, // 浏览器环境
},
// 扩展配置文件
extends: [
'standard', // es
'prettier', // 关闭和 prettier 冲突的 eslint 配置
'plugin:prettier/recommended', // 把 prettier rule 当做 eslint rule 来执行
'plugin:vue/vue3-recommended', // vue
'plugin:@typescript-eslint/recommended', // ts
],
parser: 'vue-eslint-parser', // to lint vue
// 解析器
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
parser: '@typescript-eslint/parser',
},
plugins: ['@typescript-eslint'],
rules: {},
};
同时需要配置 .eslintignore 文件来忽略掉一些文件:
node_modules
dist
demo
build
.husky
.vscode
.github
*.d.ts
然后还需要配置 script:
- 由于配置了
eslint-plugin-prettier
,在执行 eslint 命令时会将 prettier 处理的结果传递给 eslint,所以一个命令即可。 --fix
会执行修复操作。
"eslint": "eslint --ext .vue,.ts,.js src",
"eslint:fix": "eslint --ext .vue,.ts,.js src --fix"
stylelint + prettier
Stylelint 用于样式的质量检测,同时它也具备样式格式化能力,使用 prettier 进行样式格式化需要关闭他们冲突的 rule,同时需要 plugin 支持 scss / vue,和 eslint 类似,以 plugin / extends 配置 / 自定义 rules 来扩展 stylelint。
需要安装的包有:
- "
stylelint
" - "
stylelint-config-recommended-scss
": scss 推荐扩展配置 包含了stylelint-scss
和postcss-scss
- "
stylelint-config-standard
": stylelint 标准扩展配置 - "
stylelint-config-standard-vue
": vue 标准扩展配置,包含了 - "
stylelint-order
": css 属性排序规则集 - "
stylelint-config-prettier
": 关闭 stylelint 和 prettier 冲突的 配置 - "
stylelint-prettier
": 将 prettier 的结果传递给 stylelint,由 stylelint 来处理错误提示
然后配置 .stylelintrc.cjs 文件:
- 和 eslint 类似的 extends / plugin / rules 规则。
- overrides 选项可以为一些文件类型配置特殊的 parser,比如
stylelint-config-recommended-scss
,这个 extends 就配置了postcss-scss
来解析 scss,让 stylelint 能够作用于 scss 文件,同时还集成了stylelint-scss
scss 规则集。
module.exports = {
extends: [
'stylelint-config-standard', // stylelint standard 配置集
'stylelint-config-recommended-scss', // scss 推荐配置
'stylelint-config-prettier', // 关闭与 prettier 冲突 的配置项
'stylelint-config-standard-vue', // vue 配置
],
overrides: [ // 指定文件类型对应配置如:parser
// {
// files: ['src/*.(scss|css|vue)'],
// customSyntax: 'postcss-scss', // 解析器
// },
],
ignoreFiles: [],
plugins: ['stylelint-order', 'stylelint-prettier'],
rules: {
'prettier/prettier': true,
'order/order': ['custom-properties', 'declarations'],
'order/properties-order': [ // stylelint-order 配置 css 属性顺序
'position',
'top',
'right',
...
],
},
};
配置完这些,还需要配置 .stylelintignore 文件,也可以在 .stylelintrc.cjs 中配置 ignoreFiles:
node_modules
dist
demo
build
.husky
.vscode
.github
最后配置 script:
"stylelint": "stylelint "src/**/*.{css,scss,sass,vue}"",
"stylelint:fix": "stylelint "src/**/*.{css,scss,sass,vue}" --fix",
ls-lint
项目文件命令规范工具。规则十分简单:
ls
即所有目录适用规则,.dir
匹配文件夹规则,.js
匹配文件后缀,ignore
忽略目录- 支持 kebab-case | camelCase 等常见规则,也支持 RegExp。
参考官网https://ls-lint.org/2.0/configuration/the-basics.html#creating-configuration
安装 plugin
@ls-lint/ls-lint
配置:
ls:
.dir: kebab-case
.js: kebab-case | camelCase
.ts: kebab-case | camelCase
.vue: kebab-case
.d.ts: kebab-case
.tsx: kebab-case
.jsx: kebab-case
.css: kebab-case
.scss: kebab-case
.sass: kebab-case
.module.css: kebab-case
.module.sass: kebab-case
.module.scss: kebab-case
.config.js: pointcase
ignore:
- node_modules
- dist
- es
- lib
- .git
- .husky
- .vscode
docs
husky / git hooks
在此处,我们需要先了解以下两个 git hooks:
pre-commit
: 在执行 commit 前触发。可以用于在提交前运行代码格式化、代码检查等操作。如果该钩子以非零值退出,Git 将放弃此次提交。commit-msg
:在提交消息已经创建后触发。可以用于对提交消息进行额外的检查或修改。
Husky
可以将git
内置的钩子暴露出来,很方便地进行钩子命令注入,而不需要在.git/hooks
目录下写shell脚本了。husky 初始化后会在根目录生成 .husky
目录,然后 Git config 中 hooksPath = .husky
,自定义钩子在这个目录下,可以被团队其他成员共享。
接下来会介绍 husky 如何与其他工具配合。
commitlint
用于检查 Git 提交信息是否符合规范的工具。
需要安装:
- "
@commitlint/cli
": commitlint 命令行工具 - "
@commitlint/config-conventional
": 基于 Angular 规范的配置。
然后配置 commitlint.config.cjs:
module.exports = {
extends: [
'@commitlint/config-conventional', // 基于 conventional commits 的规范配置。
],
};
结合 husky
commitlint
常配合 husky 使用,这里直接使用命令添加 husky hook:
npx husky add .husky/commit-msg "npx --no -- commitlint --edit $1"
当我们提交 commit msg
触发 commit-msg
钩子时,这里的 commitlint --edit
会读取 ./.git/COMMIT_EDITMSG
里存放的最后一次 commit msg
,然后检测其是否符合规范。
lint-staged
我们知道当项目逐渐复杂时,静态检测工具检测所有文件几乎不可能,能否只对每次提交的代码进行检测呢?答案是肯定的,lint-staged
为我们提供操作 git 暂存区文件的能力。
结合其他工具
在根目录下配置 .lintstagedrc
,不难看出,这里匹配对应文件执行相应的 lint 命令。
{ "*": ["ls-lint"], "**/*.{js,vue,ts}": ["eslint --fix"], "{src,demo}/**/*.{css,scss,sass,vue}": ["stylelint --fix"] }
那么什么时候执行这些 lint 检测命令呢?答案是在输入 commit msg
之前。
添加 husky 对应 pre-commit
钩子:
#!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
如果暂存区的文件无法通过检测,那么此次 commit 失败,从而限制不符合规范的代码提交到仓库。
IDE plugin
以 vs code 为例, 安装官方提供的 plugin 即可。
vscode settings
可以在 .vscode/settings.json
文件下配置当前 workspace 编辑器行为,如保存自动格式化代码,tabSize 等,以保证项目在不同设备上表现一致。
{
"editor.codeActionsOnSave": { // 保存自动 fix
"source.fixAll": "never",
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit"
},
"editor.formatOnSave": true, // 自动保存
"editor.defaultFormatter": "esbenp.prettier-vscode", // 默认格式化工具
"editor.tabSize": 2,
"prettier.configPath": ".prettierrc.cjs",
"stylelint.configFile": ".stylelintrc.cjs",
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" // vue 文件默认格式化方式 和 volar 冲突
},
"stylelint.validate": [
"scss",
"css",
"vue"
],
"typescript.tsdk": "node_modules\typescript\lib" // 当前工作区 ts 版本和 项目 ts 版本冲突
}
总结
看到这里,大家应该对自动化工具有了初步了解,它一定程度上可以提高代码的质量、可读性和可维护性,减少 Bug 的产生,提升团队的开发效率和协作水平。自动化工具链环环相扣,限于文章篇幅,某些细节无法展开,有机会可以单独讲讲。
组件按需引入
前面我们进行了组件库整体打包,接下来我们来看如何构建来支持组件库按需引入。
我设想的每一个组件目录结构都应该如下:
D:.
├─dynamic-list
│ │ dynamic-list.css
│ │ dynamic-list.mjs
│ │ index.d.ts
│ │ index.mjs
│ │ useRO.d.ts
│ │ useRO.mjs
│ │
这样我们就可以单独引入某一个组件了,初步设想用 rollup 多 input entry 来分别 build 每一个组件:
{
...base,
input: getComponentEntries(),
output: [
{
format: 'es', // 产物 js 的格式
dir: 'dist/es', // 产物所在的目录
entryFileNames: '[name].mjs', // 产物 js 的名称,这里的 name 是 input entry 的 key
assetFileNames: '[name][extname]', // 产出资源的名称
}
],
}
这里的 input 通过 getComponentEntries 方法来获取:
const resolve = dir => { const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); return path.resolve(__dirname, '../', dir); };
// 获取导出入口
export function getComponentEntries() {
return Object.fromEntries(
glob
.sync(‘src/**/*.ts’)
.map(file => [
path.relative(‘src’, file.slice(0, file.length - path.extname(file).length)),
resolve(file),
]),
);
}
获取的 input 会类似如下,src 下每个组件文件都会有一条 entry:
{
'fixed-size-list/index': '绝对路径/src/fixed-size-list/index.ts'
}
上述配置的作用下,组件按照以下输出:
src/fixed-size-list/index.ts -----> dist/es/fixed-size-list/index.mjs
这样就实现了每一个组件都产出一个单独的入口。
样式按需引入
rollup-plugin-postcss
开启 extract 后能导出外部样式文件,但不能定义样式文件路径。
使用 rollup-plugin-styles
代替,他的mode: 'extract'
会遵循 assetFileNames,最终会产出到正确的位置:
D:.
├─dynamic-list
│ │ dynamic-list.css
│ │ dynamic-list.mjs
│ │ index.d.ts
│ │ index.mjs
│ │ useRO.d.ts
│ │ useRO.mjs
│ │
总结
本篇作为组件化工程化的第一篇章,至此,我们的组件库已经具有以下特点:
- 使用 rollup 作为打包工具 ✅🆕
- 支持 cjs、esm 和浏览器直接引入✅🆕
- 接入eslint、commitlint 等静态检测工具✅🆕
- 支持组件样式按需引入❓🆕
但请思考一下,我们的组件库构建速度是否合格?按需引入实现有没有什么问题?该怎样优化?
不足
打包速度慢
roullp + babel 冷启动需要近 21.6s,重复构建也要 4.5s,这还只是几个组件。能不能想办法提升构建速度,比如接入 esbuild / SWC 来提升构建速度?
组件库内部组件引用 chunk
组件库内部组件相互引用时,被引用的组件会被 rollup chunk。
如组件fixed-size-list
引用了list-item
,假设有如下配置:
export const esbuildOptions = {
... base,
input: {
'packages/fixed-size-list/fixed-size-list': 'src/packages/fixed-size-list/index.ts',
'packages/list-item/list-item': 'src/packages/list-item/index.ts',
},
output: [
{
format: 'es',
dir: 'dist',
entryFileNames: `[name].mjs`,
assetFileNames: '[name][extname]',
},
],
external: ['vue'],
plugins: [
... base.plugins,
del({ targets: 'dist/*' }), // 每次 build 之前删除 dist
vue(),
esbuild(),
styles({
// 遵从 assetFileNames 路径
mode: 'extract',
plugins: [
// 依据 browserlist 自动加浏览器私有前缀
autoprefixer(),
postcssPresetEnv(),
// // 压缩 css
cssnanoPlugin(),
],
}),
],
};
我所预期的 build 后目录结构如下:
├─packages
│ ├─fixed-size-list
│ │ │ fixed-size-list.css
│ │ │ fixed-size-list.mjs
│ └─list-item
│ │ list-item.css
│ │ list-item.mjs
实际上得到的目录结构:
│ index-18712f89.js
│ index.css
└─packages
├─fixed-size-list
│ fixed-size-list.css
│ fixed-size-list.mjs
└─list-item
list-item.mjs
rollup 会将 list-item
组件作为公共 chunk 输出到项目根目录。
公共 css 重复打包
rollup-plugin-postcss
或一些类似的 css 处理插件虽然可以配置 extract
将样式导出为外部文件,但是当我在多个组件中引入公共 css 时,会将公共的 css 分别打包进每一个组件样式,无法自动模块分割,造成代码体积增大。
后续
下一篇章,我们将带着这三个问题尝试对组件库进行优化。
这是一个从 https://juejin.cn/post/7369120920146984995 下的原始话题分离的讨论话题