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. 总结演进路线
- 无模块化 (全局污染) ->
- 伪模块化 (IIFE/命名空间) ->
- 服务端模块化 (CommonJS) ->
- 浏览器异步模块化 (AMD) ->
- 兼容方案 (UMD) ->
- 语言标准模块化 (ES Modules) ->
- 构建工具统一生态 (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 的双引擎架构是其高性能的核心,通过 Esbuild 和 Rollup 的协同分工,分别在开发和生产阶段发挥各自优势。以下是其核心设计及工作原理:
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 优化 :
- 预加载公共依赖(如 Chunk A 和 B 共享的模块 C),减少网络请求瀑布。
- 插件生态兼容 :
- Vite 插件基于 Rollup 接口设计,开发阶段通过
Plugin Container
模拟 Rollup 调度,生产环境直接复用。
- Vite 插件基于 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
的使用方法,特别是在组件库开发中的应用场景。
8.1 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
是组件库开发中非常有用的工具,有以下优点:
- 实时测试:修改代码后立即在测试项目中生效
- 无需发布:避免频繁发布到 npm 仓库
- 调试方便:可以直接调试组件库代码
- 开发效率:提高开发和测试效率
9. 发布到 npm
-
登录 npm(如未登录):
bashnpm login
先查看当前镜像代理
使用 nrm 管理源,切换到官方源
-
发布(首次需加
--access public
):
cmd
npm publish --access public
注意:package.json中要设置成false,name要取唯一的。
10. 用户如何使用
参考上面npm link中如何使用