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

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 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子2 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年2 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子2 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina2 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路3 小时前
React--Fiber 架构
前端·react.js·架构
伍哥的传说3 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js
qq_424409194 小时前
uniapp的app项目,某个页面长时间无操作,返回首页
前端·vue.js·uni-app
我在北京coding4 小时前
element el-table渲染二维对象数组
前端·javascript·vue.js
布兰妮甜4 小时前
Vue+ElementUI聊天室开发指南
前端·javascript·vue.js·elementui