Appearance
组件库工程化与 npm 包开发
CSS 工程化 · vue-loader · 组件库按需引入 · monorepo 搭建 · npm 发布 · 开源协议
一、CSS 工程化
- 预处理器(Less/Sass):变量、嵌套、mixin、循环、函数 → 编译为 CSS
- PostCSS:处理 CSS 本身(Autoprefixer 加前缀、编译未来语法)
- Webpack loader:
css-loader(导入编译 CSS)+style-loader(创建 style 标签插入 CSS),顺序:style-loader 在前,css-loader 在后 - 解决的问题:CSS 文件组织/拆分、编码优化、构建打包、可维护性
二、vue-loader 原理详解
vue-loader 作用:让 webpack 支持 .vue 单文件组件(SFC),将 template/script/style 分别处理后合并为可在浏览器运行的 JS 文件
处理流程:
- 解析 SFC:使用
@vue/component-compiler-utils的parse()将源码解析为 SFC 描述符jsdescriptor = { template: {...}, script: {...}, styles: [...], customBlocks: [] } - 生成 import 语句:为每个语言块生成带 querystring 的虚拟 importjs
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 - VueLoaderPlugin:修改 webpack 配置,注入
pitcher规则匹配?vue&type=xxx请求 - Pitcher 熔断:pitcher 的 pitch 方法根据 type 生成行内 loader 链,利用 pitch 返回值实现熔断(跳过后续 loader)
模板编译三阶段(baseCompile):
- parse:模板字符串 → AST 抽象语法树
- optimize:标记静态节点(重新渲染时跳过,提升性能)
- 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 Shaking | Vant | 需配置 module + sideEffects | 中 |
| unplugin-vue-components | varlet | 需配置插件 + 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 关键字段
| 字段 | 作用 |
|---|---|
main | CJS 入口(dist/lib/index.js) |
module | ESM 入口(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():在替换节点后立即递归遍历,找到目标后添加属性并停止- 浏览器端解析 HTML:
document.implementation.createHTMLDocument('')创建虚拟文档 new Function()执行脚本:通过new Function('exports', 'module', scriptContent)在受控作用域内执行
六、npm 包开发
npm CLI 开发
核心步骤:
npm init初始化项目- 在
package.json中配置bin字段 - 入口文件首行必须添加 shebang:
#!/usr/bin/env node 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 关键字段补充
| 字段 | 作用 |
|---|---|
main | CJS 入口 |
module / jsnext:main | ESM 入口 |
browser | 浏览器环境入口(字符串或对象) |
types | 指向 types/index.d.ts,为 IDE 提供类型提示 |
发布文件优先级规则
| 优先级 | 规则 |
|---|---|
| 最高 | package.json 中 files 字段(白名单) |
| 次之 | .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.0 → 2.0.1 ~ 2.9.9 |
~ | 固定 major+minor,patch 可升 | ~2.0.0 → 2.0.1 ~ 2.0.9 |
npm 发布常见坑
- 邮箱未验证:官网注册后必须先验证邮箱
- 淘宝镜像问题:发布前需切回官方源:
npm config set registry https://registry.npmjs.org - 包名重复:发布前先在 npm 官网搜索包名
- 推荐官网注册:不要用
npm adduser注册
七、开源协议
| 协议 | 特点 |
|---|---|
| MIT | 最宽松,唯一要求:保留版权声明和许可提示 |
| BSD | 需包含版权提示和免责声明,其他无限制 |
| Apache 2.0 | 提供版权许可 + 专利许可,要求标注修改过的文件 |
| GNU GPL | 强传染性,使用 GPL 代码的产品必须也使用 GPL 协议 |
| GNU LGPL | GPL 的弱化版,允许在不传染的情况下链接使用 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.json 的 main/module/files | 四、package.json 关键字段 |
启动方式:pnpm dev(三服务协同:docs + server + ui)或 pnpm dev:ui(单独启动)
完整项目版本(ljf-ui,含 hooks/utils/docs 四包协作)见 ljf-ui 组件库实战
相关
- Webpack 原理与优化 — 构建流程、Loader/Plugin、分片优化
- ljf-ui 组件库实战 — 自定义 hooks、CSS 变量主题、:::demo 插件、防抖节流高级实现
- 微前端与 Monorepo — 架构设计、pnpm workspace