组件库工程化(一):开发环境与构建打包


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 作为开发调试目录:

image.png

其中 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 outputdemo/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 引入 vuerollup 不用再打包 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 core
  • core-jspolyfill 库。
  • @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.x
  • rollup-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",

打包结果:

image.png

自动化工具

为你的组件库接入各种自动化工具,它将显著提高你的开发效率和体验。口说无凭,请往下看:

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-scsspostcss-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 下的原始话题分离的讨论话题