打造自己的组件库(三)打包及发布

1. JavaScript模块化历史

JavaScript 模块化的发展历程是前端工程化演进的核心线索之一,它解决了代码组织、依赖管理、作用域隔离等关键问题。以下是其主要的几个阶段:

1.1. 蛮荒时代:全局变量 + IIFE (1995 - 2009)

  • 问题: 脚本通过 <script> 标签加载,所有变量暴露在全局作用域,极易造成命名冲突和污染。
  • 解决方案:
    • 命名空间: 用单个全局对象包裹变量(如 var MyApp = {}; MyApp.module = ...),减少冲突。
    • IIFE (Immediately Invoked Function Expression): (function() { ... })() 创建闭包作用域,私有化变量,通过返回值或参数传递依赖。
  • 特点: 手动管理依赖顺序,缺乏静态分析能力。

1.2. 规范萌芽期:CommonJS (2009)

  • 目标: 为服务器端 JavaScript (Node.js) 设计模块系统。
  • 核心概念:
    • module.exports 导出模块。
    • require() 同步加载依赖模块。
  • 特点:
    • 简单直观,适合服务端同步 I/O。
    • 缺点: 同步加载在浏览器(网络环境)会导致阻塞,性能差。

1.3. 浏览器优先:AMD (Asynchronous Module Definition) (2009 - 2011)

  • 代表库: RequireJS
  • 目标: 解决 CommonJS 在浏览器的同步加载问题。
  • 核心概念:
    • define(id?, dependencies?, factory):定义模块。
    • 依赖列表 (dependencies) 声明前置依赖。
    • 工厂函数 (factory) 接收依赖项并返回模块值。
    • 异步加载依赖。
  • 特点:
    • 明确依赖声明,适合浏览器环境。
    • 支持异步加载和模块并行下载。
    • 缺点: 语法相对复杂,开发体验不如 CommonJS 简洁。

1.4. 通用方案:UMD (Universal Module Definition) (2011 左右)

  • 目标: 编写同时兼容 AMD、CommonJS 和全局变量模式的模块。
  • 原理: 在模块定义开始时检测当前环境支持的模块系统,然后使用相应的方式导出。
  • 特点: 过渡方案,用于编写通用库。

1.5. 标准诞生:ES Modules (ESM) (2015 - Present)

  • 标准: ECMAScript 2015 (ES6) 正式引入语言层面的模块系统。
  • 核心语法:
    • 导出: export (命名导出、默认导出 export default)。
    • 导入: import ... from 'module'
  • 关键特性:
    • 静态化: 依赖关系在编译时(解析时)就能确定,支持 Tree Shaking 优化。
    • 异步加载: 浏览器原生支持异步加载。
    • 严格模式: 模块内默认启用严格模式。
    • 只读引入: import 绑定是只读的。
    • 循环引用: 有明确定义的处理机制。
    • 浏览器支持: 通过 <script type="module"> 标签加载。
  • 特点: 语法简洁、标准化、面向未来,是现代 JavaScript 开发的基石。

1.6. 构建工具与生态统一

  • 挑战: 早期浏览器对 ESM 支持不足,且项目中可能混用不同模块格式。
  • 解决方案: 打包工具 (Bundlers) 兴起:
    • 代表: Webpack, Rollup, Parcel, Vite, esbuild。
    • 作用:
      • 将各种格式的模块 (ESM, CommonJS, AMD, UMD, 甚至 Assets) 转换为标准 ESM 或兼容的代码。
      • 打包众多小文件为少数优化文件 (Bundle)。
      • 处理依赖解析和加载。
      • 应用各种转换和优化 (Babel 转译、压缩、Tree Shaking、代码分割等)。
  • Node.js 支持: .mjs 扩展名或 package.json 中设置 "type": "module" 启用 ESM。

1.7. 总结演进路线

  1. 无模块化 (全局污染) ->
  2. 伪模块化 (IIFE/命名空间) ->
  3. 服务端模块化 (CommonJS) ->
  4. 浏览器异步模块化 (AMD) ->
  5. 兼容方案 (UMD) ->
  6. 语言标准模块化 (ES Modules) ->
  7. 构建工具统一生态 (Bundlers)

1.8. 现状与未来

  • ES Modules (ESM) 是绝对的主流和标准方向。

  • 现代浏览器已广泛原生支持 ESM (<script type="module">)。

  • Node.js 稳定支持 ESM。

  • 打包工具 (如 Webpack, Rollup, Vite) 是构建复杂应用不可或缺的部分,它们处理模块转换、打包和优化,让开发者可以专注于使用 ESM 语法开发。

  • CommonJS 在 Node.js 生态(尤其是旧包)中仍大量存在,构建工具会处理兼容。

  • AMD 逐渐退出历史舞台,主要用于遗留项目。

    简单来说: JavaScript 模块化从混乱走向规范,最终由 ES Modules 一统江湖,而强大的打包工具则解决了历史包袱和工程化需求,让 ESM 得以在实践中大规模应用。

2. vite双引擎架构

Vite 的双引擎架构是其高性能的核心,通过 EsbuildRollup 的协同分工,分别在开发和生产阶段发挥各自优势。以下是其核心设计及工作原理:


2.1. Esbuild:开发阶段的性能引擎

Esbuild 基于 Go 语言编写,以极速构建能力支撑 Vite 的开发效率,主要承担三类任务:

  • 依赖预构建(Bundle 工具)

    • 解决第三方依赖的 ESM 格式兼容性问题(如 CommonJS 转 ESM)和避免海量 HTTP 请求。
    • 性能碾压传统工具(如 10 个 three.js 副本打包仅需 0.39 秒,而 Webpack 需 41 秒)。
    • 局限 :不支持 ES5 降级、const enum 语法,且无法自定义拆包策略。
  • 单文件编译(Transformer 工具)

    • 替代 Babel/TSC 编译 TS/JSX 文件,速度提升显著(50MB 文件编译:Esbuild 0.7s vs Babel 12.8s)。
    • 局限 :无类型检查能力,需依赖 tsc 或编辑器插件补全。
  • 代码压缩(Minifier 工具)

    • 生产环境默认压缩 JS/CSS 代码,性能远超 Terser(3.2MB 库压缩:Esbuild 361ms vs Terser 8798ms)。
    • 优势:共享 AST、Go 原生语言实现,避免重复解析。

2.2. Rollup:生产环境的构建基石

Rollup 负责生成优化的生产包,弥补 Esbuild 的功能局限,并提供三项关键能力:

  • CSS 代码分割
    • 异步模块中的 CSS 自动抽取为独立文件,提升缓存利用率。
  • 自动预加载
    • 为入口 Chunk 的依赖生成 <link rel="modulepreload">,加速资源加载。
  • 异步 Chunk 优化
    • 预加载公共依赖(如 Chunk A 和 B 共享的模块 C),减少网络请求瀑布。
  • 插件生态兼容
    • Vite 插件基于 Rollup 接口设计,开发阶段通过 Plugin Container 模拟 Rollup 调度,生产环境直接复用。

2.3. 双引擎协作流程

Vite 根据运行环境动态调度双引擎,实现效率与稳定性的平衡:

阶段 Esbuild 角色 Rollup 角色
开发环境 预构建依赖、编译单文件、实时压缩
生产环境 仅代码压缩(通过 Rollup 插件集成) 打包代码、CSS 分割、优化加载策略

协作示例:

  • 开发启动:Esbuild 预构建依赖 → 浏览器按需加载 ESM 模块。
  • 生产构建:Rollup 打包时调用 Esbuild 压缩插件,兼顾功能与性能。

2.4. Esbuild 与 Rollup 核心对比

特性 Esbuild Rollup
语言/性能 Go,极速(10--100× 传统工具) JS,稳定但较慢
开发阶段作用 预构建、单文件编译、压缩
生产阶段作用 压缩(插件集成) 代码打包、优化、插件调度
局限性 无类型检查、低兼容性、功能扩展性弱 生态成熟,但速度不如 Esbuild

2.5. 总结

Vite 的双引擎架构是 "开发求速,生产求稳" 的典范:

  • Esbuild 以极致性能加速开发流程(预构建、编译、压缩),牺牲部分功能换取秒级响应。
  • Rollup 以成熟生态保障生产包质量(代码分割、加载优化),并通过插件机制与 Esbuild 协同。
    这一设计使 Vite 在开发者体验与产物性能间取得平衡,成为现代前端工具链的标杆。

3. Vue3插件系统

我来详细解释 Vue3 插件系统,结合你的项目中的实际应用进行分析。

Read file: src/main.ts Ran tool Read file: src/until/theme.ts Ran tool Read file: src/components/button/Button.vue

3.1 Vue3 插件系统概述

Vue3 的插件系统是一个强大的扩展机制,允许开发者向 Vue 应用添加全局功能。插件可以是一个对象或函数,通过 app.use() 方法安装。

3.1.1 插件的基本结构

typescript 复制代码
// 插件对象形式
const plugin = {
  install(app, options) {
    // 插件逻辑
  }
}

// 插件函数形式
function plugin(app, options) {
  // 插件逻辑
}

3.1.2. 插件的安装方式

typescript 复制代码
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// 安装插件
app.use(plugin, options)

app.mount('#app')

3.2 在我的项目中的插件实现

1. 组件库插件实现

从你的 src/index.ts 可以看到完整的插件实现:

typescript 复制代码
import type { App } from 'vue';
import Button from "./components/button/Button.vue";
// 组件列表
const components = [Button]

// 插件install 方法
const install = (app: App): void => {
  components.forEach(component => {
    // 组件需要有name属性
    app.component(component.__name!, component);
  })
}

// 支持按需引入和全量引入
export default {
  install
}

export {
  Button
}

2. 插件系统的核心特性

全局组件注册
typescript 复制代码
const install = (app: App): void => {
  components.forEach(component => {
    app.component(component.__name!, component);
  })
}

4. 组件库打包-配置文件

vite.config.ts文件如下

ts 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
  plugins: [
      vue()
  ],
  // 不需要手动指定 postcss 配置,Vite 会自动读取 postcss.config.js
  build: {
    lib: {
      entry: path.resolve(__dirname, 'src/index.ts'),
      name: 'GxyElementUI',
      fileName: 'gxy-element-ui'
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        exports: 'named',
        globals: {
          vue: 'Vue'
        },
        assetFileNames: (chunkInfo)=>{
          if(chunkInfo.name === 'gxy-element-ui.css') {
            return 'index.css'
          }
          return chunkInfo.name as string
        }
      }
    }
  }
})

插件配置

  • @vitejs/plugin-vue:处理 Vue 单文件组件
  • vite-plugin-dts:自动生成 TypeScript 声明文件

库构建配置

  • entry :指定入口文件为 src/index.ts
  • name:UMD 格式的全局变量名
  • fileName:输出文件名前缀

Rollup 配置

  • external:将 Vue 设为外部依赖,不打包进库
  • exports: 'named':使用命名导出
  • globals:指定外部依赖的全局变量名
  • assetFileNames :自定义资源文件名,将 CSS 文件重命名为 index.css

5. 组件库打包 - 生成类型定义文件

5.1 类型定义文件生成概述

使用了 vite-plugin-dts 插件来自动生成 TypeScript 类型定义文件,这是现代组件库开发的标准做法。类型定义文件为使用者提供完整的类型支持,提升开发体验。

5.2 类型定义文件生成配置

在你的 Vite 配置文件中,vite-plugin-dts 的配置如下:

typescript 复制代码
// vite.config.ts
import dts from 'vite-plugin-dts'

export default defineConfig({
  plugins: [
    vue(),
    dts({
      tsconfigPath: './tsconfig.build.json'
    })
  ]
})

tsconfig.build.json

json 复制代码
{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "moduleResolution": "bundler",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "erasableSyntaxOnly": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedSideEffectImports": true
  },
  "include": ["src/index.ts", "src/components/**/*", "src/until/**/*"]
}

5.3 高级配置选项

typescript 复制代码
import dts from 'vite-plugin-dts'

export default defineConfig({
  plugins: [
    dts({
      tsconfigPath: './tsconfig.build.json',
      outDir: 'dist/types',
      include: ['src/**/*.ts', 'src/**/*.vue'],  // 指定包含文件
      exclude: ['src/**/*.test.ts'],            // 指定排除文件
      insertTypesEntry: true,                   // 插入类型入口
      copyDtsFiles: true,                       // 复制 .d.ts 文件
      skipDiagnostics: false,                   // 跳过诊断
      logLevel: 'info'                          // 日志级别
    })
  ]
})

6. 组件库打包-生成样式文件

6.1. 样式文件

在src/index.ts中引入index.scss 报错 src/index.ts

ts 复制代码
import type {App} from 'vue';
import Button from "./components/button/Button.vue";
import './assets/styles/index.scss'

// 组件列表
const components = [Button]

// 插件install 方法
const install = (app:App):void =>{
    components.forEach(component => {
        // 组件需要有name属性
        app.component(component.__name!, component);
    })
}
// 支持按需引入和全量引入
export default {
    install
}

export {
    Button
}

问题解决: 添加shims-scss.d.ts文件

ts 复制代码
declare module '*.scss' {
    const content: { [className: string]: string };
    export default content;
}

tsconfig.build.json中添加src/shims-scss.d.ts

json 复制代码
{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "erasableSyntaxOnly": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedSideEffectImports": true
  },
  "include": ["src/index.ts", "src/components/**/*", "src/until/**/*","src/shims-scss.d.ts"]
}

打包成功

图标库处理

在src/index.ts中引入图标

ts 复制代码
// 导入 FontAwesome 核心库
import { library } from '@fortawesome/fontawesome-svg-core'
// 导入solid图标集
import { fas } from '@fortawesome/free-solid-svg-icons'

// 将图标添加到库中
library.add(fas)

打包后,发现体积突然变大

7 组件库打包-拆分构建脚本

7.1 创建文件

在项目目录下新建vite.es.config.ts和vite.umd.config.ts,与vite.config.ts同级 vite.es.config.ts

ts 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import dts from 'vite-plugin-dts'
export default defineConfig({
    plugins: [
        vue(),
        dts({
            tsconfigPath: './tsconfig.build.json',
            outDir: 'dist/types'
        })
    ],
    build: {
        outDir: 'dist/es',
        lib: {
            entry: path.resolve(__dirname, 'src/index.ts'),
            name: 'GxyElementUI',
            fileName: 'gxy-element-ui',
            formats: ['es']
        },
        rollupOptions: {
            external: [
                'vue'
            ],
            output: {
                assetFileNames: (chunkInfo)=>{
                    if(chunkInfo.name === 'gxy-element-ui.css') {
                        return 'index.css'
                    }
                    return chunkInfo.name as string
                }
            }
        }
    }
})

vite.umd.config.ts

ts 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
    plugins: [vue()],
    // 不需要手动指定 postcss 配置,Vite 会自动读取 postcss.config.js
    build: {
        outDir: 'dist/umd',
        lib: {
            entry: path.resolve(__dirname, 'src/index.ts'),
            name: 'GxyElementUI',
            fileName: 'gxy-element-ui',
            formats: ['umd']
        },
        rollupOptions: {
            external: ['vue'],
            output: {
                exports: 'named',
                globals: {vue: 'Vue'},
                assetFileNames: (chunkInfo)=>{
                    if(chunkInfo.name === 'gxy-element-ui.css') {
                        return 'index.css'
                    }
                    return chunkInfo.name as string
                }
            }
        }
    }
})

vite.config.ts

ts 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import dts from 'vite-plugin-dts'
export default defineConfig({
  plugins: [
      vue(),
      dts({
        tsconfigPath: './tsconfig.build.json'
      })
  ],
  // 不需要手动指定 postcss 配置,Vite 会自动读取 postcss.config.js
  build: {
    lib: {
      entry: path.resolve(__dirname, 'src/index.ts'),
      name: 'GxyElementUI',
      fileName: 'gxy-element-ui'
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        exports: 'named',
        globals: {
          vue: 'Vue'
        },
        assetFileNames: (chunkInfo)=>{
          if(chunkInfo.name === 'gxy-element-ui.css') {
            return 'index.css'
          }
          return chunkInfo.name as string
        }
      }
    }
  }
})

7.2 构建脚本

package.json

json 复制代码
 {
  "name": "gxy-element-ui",
  "private": false,
  "version": "0.0.51",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc -b && npm run build:umd &&  npm run build:es",
    "build:umd": "vite build --config vite.umd.config.ts",
    "build:es": "vite build --config vite.es.config.ts",
    "preview": "vite preview"
  },
  "main": "./dist/umd/gxy-element-ui.umd.cjs",
  "module": "./dist/es/gxy-element-ui.js",
  "types": "./dist/types/index.d.ts",
  "files": [
    "dist"
  ],
  "exports": {
    ".": {
      "import": "./dist/es/gxy-element-ui.js",
      "require": "./dist/umd/gxy-element-ui.umd.cjs"
    },
    "./dist/": {
      "import": "./dist/",
      "require": "./dist/"
    }
  },
  "peerDependencies": {
    "vue": "^3.0.0"
  },
  //....其余配置
 }

8. npm link共享本地开发包

我来详细解释 npm link 的使用方法,特别是在组件库开发中的应用场景。

npm link 是 npm 提供的一个功能,用于在本地开发环境中创建符号链接,让你可以在本地测试和调试 npm 包,而无需每次都发布到 npm 仓库。

工作原理

bash 复制代码
# npm link 创建两个链接:
# 1. 全局链接:将包链接到全局 node_modules
# 2. 本地链接:将全局链接链接到当前项目的 node_modules

8.2 在你的组件库项目中的使用

在组件库项目中创建全局链接

bash 复制代码
# 创建全局链接
npm link

8.3 在测试项目中链接组件库

bash 复制代码
# 进入测试项目目录
cd /path/to/your/test-project

# 链接到组件库
npm link gxy-element-ui

8.4 在测试项目中使用组件库

typescript 复制代码
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import GxyElementUI from 'gxy-element-ui'
import 'gxy-element-ui/dist/es/index.css'

const app = createApp(App)
app.use(GxyElementUI)
app.mount('#app')

页面组件使用

html 复制代码
<!-- App.vue -->
<template>
  <div>
    <Button type="primary">测试按钮</Button>
  </div>
</template>

<script setup lang="ts">
</script>

8.5 自动化脚本

json 复制代码
// package.json
{
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc -b && npm run build:umd && npm run build:es",
    "build:watch": "npm run build -- --watch",
    "link": "npm link",
    "unlink": "npm unlink"
  }
}

8.6 总结

npm link 是组件库开发中非常有用的工具,有以下优点:

  1. 实时测试:修改代码后立即在测试项目中生效
  2. 无需发布:避免频繁发布到 npm 仓库
  3. 调试方便:可以直接调试组件库代码
  4. 开发效率:提高开发和测试效率

9. 发布到 npm

  1. 登录 npm(如未登录):

    bash 复制代码
    npm login

    先查看当前镜像代理 使用 nrm 管理源,切换到官方源

  2. 发布(首次需加 --access public):

cmd 复制代码
   npm publish --access public

注意:package.json中要设置成false,name要取唯一的。

10. 用户如何使用

参考上面npm link中如何使用

相关推荐
灵犀学长2 分钟前
解锁HTML5页面生命周期API:前端开发的新视角
前端·html·html5
源码云商10 分钟前
基于 SpringBoot + Vue 的 IT 技术交流和分享平台的设计与实现
vue.js·spring boot·后端
江号软件分享11 分钟前
轻松解决Office版本冲突问题:卸载是关键
前端
致博软件F2BPM18 分钟前
Element Plus和Ant Design Vue深度对比分析与选型指南
前端·javascript·vue.js
慧一居士1 小时前
flex 布局完整功能介绍和示例演示
前端
DoraBigHead1 小时前
小哆啦解题记——两数失踪事件
前端·算法·面试
一斤代码7 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子7 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年7 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子7 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架