Skip to content

组件库工程化与 npm 包开发

CSS 工程化 · vue-loader · 组件库按需引入 · monorepo 搭建 · npm 发布 · 开源协议


一、CSS 工程化

  • 预处理器(Less/Sass):变量、嵌套、mixin、循环、函数 → 编译为 CSS
  • PostCSS:处理 CSS 本身(Autoprefixer 加前缀、编译未来语法)
  • Webpack loadercss-loader(导入编译 CSS)+ style-loader(创建 style 标签插入 CSS),顺序:style-loader 在前,css-loader 在后
  • 解决的问题:CSS 文件组织/拆分、编码优化、构建打包、可维护性

二、vue-loader 原理详解

vue-loader 作用:让 webpack 支持 .vue 单文件组件(SFC),将 template/script/style 分别处理后合并为可在浏览器运行的 JS 文件

处理流程

  1. 解析 SFC:使用 @vue/component-compiler-utilsparse() 将源码解析为 SFC 描述符
    js
    descriptor = { template: {...}, script: {...}, styles: [...], customBlocks: [] }
  2. 生成 import 语句:为每个语言块生成带 querystring 的虚拟 import
    js
    import render from 'source.vue?vue&type=template'
    import script from 'source.vue?vue&type=script'
    import 'source.vue?vue&type=style&index=1'
    script.render = render
    export default script
  3. VueLoaderPlugin:修改 webpack 配置,注入 pitcher 规则匹配 ?vue&type=xxx 请求
  4. Pitcher 熔断:pitcher 的 pitch 方法根据 type 生成行内 loader 链,利用 pitch 返回值实现熔断(跳过后续 loader)

模板编译三阶段baseCompile):

  1. parse:模板字符串 → AST 抽象语法树
  2. optimize:标记静态节点(重新渲染时跳过,提升性能)
  3. generate:AST → render 函数字符串 → new Function() 转为可执行函数

运行时编译 vs 构建时编译

  • 运行时编译:使用完整版 Vue(含编译器),在浏览器中实时编译模板,体积大
  • 构建时编译:vue-cli 默认方式,webpack + vue-loader 在构建时完成编译,体积小速度快

三、组件库按需引入方式

方式一:直接引入组件路径

js
import Alert from 'xui/packages/alert'
Vue.use(Alert)
  • 优点:无需任何插件配置
  • 缺点:使用成本高,需记住每个组件路径;样式需手动引入

方式二:Babel 插件转换(ElementUI/Vant/antd 主流方案)

  • 插件:babel-plugin-component(ElementUI)、babel-plugin-import(Vant/antd)
  • 原理:在编译阶段通过 AST 转换,将 import { Alert } from 'xui' 自动转为 import Alert from 'xui/packages/alert'

方式三:Tree Shaking(Vant 同时支持)

  • 组件库发布多种模块格式:commonjs / umd / esmodule
  • 配置 package.json
    json
    {
      "main": "lib/index.js",
      "module": "es/index.js",
      "sideEffects": ["*.css"]
    }
  • sideEffects 作用:告诉打包工具哪些文件有副作用(如样式文件),不可被摇掉

方式四:unplugin-vue-components(自动导入,varlet 采用)

  • 无需手动 import,插件自动扫描模板中的组件使用并注册
  • 需为自定义组件库编写解析器(Resolver)

Resolver 函数实现

js
// vc-table/style/index.js — 样式索引文件
import '@xxx/vc/theme/table.scss'
import 'element-plus/es/components/table/style/css'
import 'element-plus/es/components/select/style/css'
js
// Resolver 函数 2.0 — 指向样式索引文件
export const VcResolver = () => {
  return async (componentName) => {
    if (!componentName.startsWith('Vc')) return
    const name = kebabCase(componentName.slice(2))
    return {
      name: componentName,
      from: '@xxx/vc',
      sideEffects: [`@xxx/vc/es/${name}/style/index`]
    }
  }
}

external 正则匹配external 默认全等匹配,需用正则:

js
external: [/^element-plus\/es\/components.*\/style\/css$/]

对比总结

方式代表库是否需配置自动化程度
直接路径引入
Babel 插件ElementUI/Vant/antd需配置 babel 插件
Tree ShakingVant需配置 module + sideEffects
unplugin-vue-componentsvarlet需配置插件 + Resolver高(全自动)

四、Vue3 组件库搭建全流程(pnpm monorepo + Vite + npm 发布)

pnpm monorepo 工作区配置

yaml
# pnpm-workspace.yaml
packages:
  - 'packages/**'
  - 'examples'
  • 根目录 package.json 设置 "private": true(整体不发布)
  • 子包间互相引用:"@uv-ui/hooks": "workspace:^1.0.0"(发布时去掉 workspace:
  • 公共依赖安装到根目录:pnpm install xxx -w

withInstall — 组件注册工具函数

js
export const withInstall = (comp) => {
  comp.install = (app) => {
    app.component(comp.name, comp)
  }
  return comp
}

Vite 库模式打包配置

js
export default defineConfig({
  build: {
    rollupOptions: {
      external: ['vue'],
      output: [
        { format: 'es', dir: 'dist/es', preserveModules: true, preserveModulesRoot: 'src' },
        { format: 'cjs', dir: 'dist/lib', preserveModules: true, preserveModulesRoot: 'src' }
      ]
    },
    lib: { entry: './index.js', formats: ['es', 'cjs'] }
  }
})
  • preserveModules: true:每个组件单独输出一个文件(支持按需引入)

package.json 关键字段

字段作用
mainCJS 入口(dist/lib/index.js
moduleESM 入口(dist/es/index.js),支持 Tree Shaking
style样式入口
files白名单,只有 dist 目录上传到 npm
publishConfig{ access: "public" } 发布 scope 包时必须

CSS 变量主题方案

scss
:root {
  --uv-button-primary: #409eff;
}
.uv-button { background: var(--uv-button-primary); }

五、Babel 插件开发实战(Vue SFC 解析)

场景:在线代码编辑工具需要将 Vue 单文件组件在浏览器端解析并预览。

Babel 插件核心结构

js
const plugin = (babel) => {
  const t = babel.types
  return {
    visitor: {
      ExportDefaultDeclaration(path) {
        path.replaceWith(
          t.expressionStatement(
            t.newExpression(t.identifier('Vue'), [path.get('declaration').node])
          )
        )
        path.traverse({
          ObjectExpression(p) {
            if (p.parent.type === 'NewExpression') {
              p.node.properties.push(
                t.objectProperty(t.identifier('el'), t.stringLiteral('#app')),
                t.objectProperty(t.identifier('template'), t.stringLiteral(templateContent))
              )
              p.stop()
            }
          }
        })
      }
    }
  }
}

关键技巧

  • path.traverse() + path.stop():在替换节点后立即递归遍历,找到目标后添加属性并停止
  • 浏览器端解析 HTMLdocument.implementation.createHTMLDocument('') 创建虚拟文档
  • new Function() 执行脚本:通过 new Function('exports', 'module', scriptContent) 在受控作用域内执行

六、npm 包开发

npm CLI 开发

核心步骤

  1. npm init 初始化项目
  2. package.json 中配置 bin 字段
  3. 入口文件首行必须添加 shebang:#!/usr/bin/env node
  4. npm publish 发布到 npmjs

commander(指令与参数处理)

js
const program = require('commander');
program
  .command('init')
  .description('初始化项目')
  .action(() => { /* init 逻辑 */ });

program
  .option('-s, --src <path>', '源文件路径')
  .option('-o, --out <path>', '输出路径')
  .action((options) => { /* 转换逻辑 */ });

program.parse(process.argv);

读取命令行参数

  • process.argv 返回数组,前两项非用户输入
  • 简单场景:process.argv.slice(2)
  • 参数较多时:minimist(轻量)或 commander(功能全)

npm 包核心知识

package.json 关键字段补充

字段作用
mainCJS 入口
module / jsnext:mainESM 入口
browser浏览器环境入口(字符串或对象)
types指向 types/index.d.ts,为 IDE 提供类型提示

发布文件优先级规则

优先级规则
最高package.jsonfiles 字段(白名单)
次之.npmignore 文件(黑名单)
再次.gitignore 文件(无 .npmignore 时生效)
最低无以上配置 → 所有文件都上传

peerDependencies(对等依赖)

  • 含义:「如果你安装了我,你最好也安装这些依赖」
  • 典型场景:组件库依赖宿主环境(如 element-plus 依赖 vue3)
json
{ "peerDependencies": { "vue": "^3.2.0" } }
  • 目录结构区别:peerDependencies 依赖提升到宿主 node_modules/ 下,宿主和子包共享

semver 语义化版本

版本格式major.minor.patch(如 1.2.3

先行版本号alpha(内部测试)→ beta(基本稳定)→ rc(Release Candidate)

版本范围符号

符号含义示例
无符号精确匹配1.0.0
^固定 major,minor/patch 可升^2.0.02.0.1 ~ 2.9.9
~固定 major+minor,patch 可升~2.0.02.0.1 ~ 2.0.9

npm 发布常见坑

  1. 邮箱未验证:官网注册后必须先验证邮箱
  2. 淘宝镜像问题:发布前需切回官方源:npm config set registry https://registry.npmjs.org
  3. 包名重复:发布前先在 npm 官网搜索包名
  4. 推荐官网注册:不要用 npm adduser 注册

七、开源协议

协议特点
MIT最宽松,唯一要求:保留版权声明和许可提示
BSD需包含版权提示和免责声明,其他无限制
Apache 2.0提供版权许可 + 专利许可,要求标注修改过的文件
GNU GPL强传染性,使用 GPL 代码的产品必须也使用 GPL 协议
GNU LGPLGPL 的弱化版,允许在不传染的情况下链接使用 LGPL 库

八、实战演示:ljf-ui 组件库

项目源码:packages/ui/mobile/,基于 Vue 3 + Vant + Vite 搭建的移动端组件库,运行在端口 7788。

以下 iframe 嵌入了组件库的实时开发服务器,可直接交互预览:

组件库架构对应本文知识点

实战对应章节
Vant 按需引入(unplugin-vue-components三、组件库按需引入方式
Vite 库模式打包(build.lib四、Vite 库模式打包配置
pnpm monorepo 工作区四、pnpm monorepo 工作区配置
package.jsonmain/module/files四、package.json 关键字段

启动方式:pnpm dev(三服务协同:docs + server + ui)或 pnpm dev:ui(单独启动)

完整项目版本(ljf-ui,含 hooks/utils/docs 四包协作)见 ljf-ui 组件库实战


相关