Skip to content

组件库打包

本章计划:

  • 了解 JavaScript 模块化发展的历史
    • 全局变量
    • common.js/AMD
    • ES6 modules
  • Bunder 的功能和作用
    • Vite(Rollup+ESbuild)在 dev 和 build 的过程为什么这么不同
    • Webpack
  • 学习使用 Vite(Rollup)实现打包打包
    • 代码入口文件
    • 配置文件
    • 生成多种文件类型
    • 生成样式文件
    • typescript 定义文件 d.ts
  • 发布到 npm
    • 本地测试
    • 注册以及发布到 npm
    • npm 钩子

JavaScript 模块发展历史

模块(modules)是什么?

javascript
from package import function

模块化的优点

  • 可维护性
  • 可复用性

ES6 之前没有模块的年代

image-20250615125044228

全局变量+命名空间(namespace)

IIFE 自执行函数,创建一个封闭的作用域,赋值给一个全局变量

image-20250615125255850

缺点

  • 依赖全局变量,污染作用域,不安全
  • 依赖约定命名空间避免冲突,可靠性不高
  • 需要手动管理依赖并控制执行顺序,容易出错
  • 需要再最终上线前手动合并所有用到的模块

Common.js

javascript
const bar = require(./bar)
module.exports = function() {

}

没法在浏览器里直接运行

AMD

  • 采用异步方式加载模块
  • 仅仅需要再全局环境定义 require 与 dfine,不需要其他的全局变量
  • 通过文件路径或模块自己声明的模块名定位模块
  • 提高了打包工具自动分析依赖并合并
  • 配合特定的 AMD 加载器使用,RequireJS
  • 同时诞生了很多类似的模块标准 CMD
javascript
define(function (require) {
  // 通过相对路径获取依赖模块
  const bar = require("./bar");
  // 模块产出
  return function () {};
});

ES6

javascript
// 通过相对路径获取依赖模块
import bar from "./bar";
// 模块产出
export default function () {}
  • 引入和暴露的方式更加多样
  • 支持复杂的静态分析

Bundler 是什么?

诞生原因

使用 import export 这种同步加载的方式在大多数浏览器中无法使用。

Bundler-打包工具

将浏览器不支持的模块进行编译,转换,合并最后生成的代码可以在浏览器端良好运行的工具。

Webpack

后起之秀-Rollup

https://rollupjs.org

Vite 的工作原理

解决的问题

大型应用使用 webpack 等传统 bundler 会遇到性能瓶颈:非常慢。Vite 就使用浏览器发展中的新特性来解决这个问题。

开发环境

Vite 以原生 ESM 方式提供源码,这实际上是让浏览器接管了打包程序的部分工作。

  • 依赖
    • 使用 esbuild 进行预构建,esbuild 由 go 编写,比基于 Node.js 的工具快 10-100 倍。
      • 处理 CommonJS 以及 UMD 类型文件的兼容性,转为 ESM 以及 ESM 的导入形式
      • 提高性能,在将多个模块合并成单个模块。因为原生 ESM 格式下,一个文件就是一次请求。
      • 缓存,将预构建的依赖项缓存到 node_modules/.vite 中。
  • 源码
    • 包含一些非 JavaScript 标准格式的文件,比如 JSX/CSS/Vue 等等,时常会被编辑。

生产环境

并没有使用 es modules 的格式,而是使用 Rollup 的形式构建。

为什么?

在生产环境中使用未打包的 ESM 仍然效率低下。Vite 附带了一套已经内置的构建优化以及构建命令,可以做到开箱即用。

为什么使用 Rollup,而没有选用上面说的 ESbuild?

Vite 目前的插件 API 与使用 esbuild 作为打包器并不兼容。尽管 esbuild 速度更快,但 Vite 采用了 Rollup 灵活的插件 API 和基础建设。

打包什么类型的文件?

  • CommonJs,es6 modules-需要特殊的 module bundler 支持
  • AMD 已经有点过时了-需要使用特殊的 Loader-require.js
  • 浏览器中直接使用-UMD
    • 通用的一种 Javascript 格式
    • 兼容 common.js,AMD,浏览器
    • https://github.com/umdjs/umd
    • Vue 和 React 都提供了这样的格式
    • 不是一种推荐的格式,太大了!不支持 tree shaking

结论

  • 首要格式-ES modules,并且提供支持 typescript 的 type 文件
  • 备选方案-UMD

Vue3 的插件系统

https://cn.vuejs.org/guide/reusability/plugins.html

javascript
import ElementPlus from "element-plus";

const app = createApp(App);

app.use(ElementPlus);

一段代码给 vue 应用实例添加全局功能。它的格式是一个 object 暴露出一个 install()方法,或者一个 function

javascript
const myPlugin = {
  install(app, options) {
    // 配置此应用
  },
};

它没有严格的限制,一般有以下几种功能

  • 通过 app.component()和 app.directive()注册一到多个全局组件或自定义指令。
  • 通过 app.provide()使用一个资源可被注入进整个应用
  • 向 app.config.globalProperties 中添加一些全局实例属性或方法

test.plugins.ts

image-20250615134159767

main.ts 中导入

image-20250615134236285

然后在 App.vue 中使用

image-20250615134413088

这里使用$echo 会报错,可以全局定义类型

扩展全局属性:https://cn.vuejs.org/guide/typescript/options-api.html#augmenting-global-properties

创建组件的入口文件

实现组件的按需引入和全局引入

src/components/Button/index.ts

typescript
impirt type { App } from 'vue'
import Button from './Button.vue'

Button.install = (app: App) => {
  app.component(Button.name, Button)
}

export default Button
export * from './types' // 使用者也有可能想要类型文件

src/components/Collapse/index.ts

typescript
impirt type { App } from 'vue'
import Collapse from '@/components/Collapse/Collapse.vue'
import CollapseItem from '@/components/Collapse/CollapseItem.vue'

Collapse.install = (app: App) => {
  app.component(Collapse.name, Collapse)
}
CollapseItem.install = (app: App) => {
  app.component(CollapseItem.name, CollapseItem)
}

export default Collapse
export {
	CollapseItem
}
export * from './types'

src/components/Message/index.ts

Message 组件是通过方法的方式来实现

typescript
impirt type { App } from 'vue'
import Message from '@/components/Message/Message.vue'
import { createMessage, closeAll } from '@/components/Message/methods'

Message.install = (app: App) => {
  app.component(Message.name, Message)
}

export default Message
export {
	createMessage,
  closeAll
}
export * from './types'

src/index.ts

typescript
impirt type { App } from 'vue'

import Button from '@/components/Button'
import Collapse, { CollapseItem } '@/components/Collapse'
import Message, { createMessage, closeAll as closeMessageAll } from '@/components/Message'

const components = [
  Button,
  Collapse,
  CollapseItem,
  Message,
]

const install = (app: App) => {
  components.forEach(component => {
    app.component(component.name, component)
  })
}

export {
	install,
  Button,
  Collapse,
  CollapseItem,
  Message,
  createMessage,
  closeMessageAll
}

export default {
  install
}

简介 Rollup 的配置文件以及插件系统

rollup.config.js

json
npm install @rollup/plugin-json --save-dev
javascript
import json from '@rollup/plugin-json'

export default {
  input: 'main.js',
  output: {
    file: 'dist/bundle.js',
    format: 'es' // cjs都行
  },
  plugins: [json()]
}

执行以下命令打包

json
npx rollup --config rollup.config.js

打包组件库第一部分 - 小试牛刀

Vite 构建生产版本

https://cn.vitejs.dev/guide/build.html

当需要将应用部署到生产环境时,只需运行 vite build 命令。默认情况下,它使用<root>/index.html 作为其构建入口点,并生成能够静态部署的应用程序包。

自定义构建

typescript
// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      // https://rollupjs.org/configuration-options
    },
  },
});

库模式

https://cn.vitejs.dev/guide/build.html#library-mode

库模式各种配置项:https://cn.vitejs.dev/config/build-options.html#build-lib

vite.config.ts

typescript
build: {
  lib: {
    // 入口文件
    entry: resolve(__dirname, 'src/index.ts'),
    // name则是暴露的全局变量,并且在formats包含'umd'或'iife'时是必须的
    name: 'VElement',
    // 是输出的包文件名,默认fileName是package.json的name选项
    fileName: 'v-element',
    //默认formates是['es',umd]
    // formats: ['es'] 先不配置
  }
}

执行 npm run build-only,速度更快一些,这个命令在 package.json 中

打包之后发现,第一,后缀名是 mjs,第二打包之后的文件太大有 344k,源码并没有这么大,第三,有报错。

打包组件库第二部分 - 配置 rollup

vite.config.ts

typescript
build: {
  lib: {
    ...
  },
  rollupOptions: {
    external: ['vue'], // 不会把Vue打包进去
    outout: {
      exports: 'named',
      globals: {
        vue: 'Vue'
      }
    }
  }
}

后缀名的配置

没有配置之前

image-20250615150200782

在 package.json 中配置之后

json
{
  "type": "module"
}

image-20250615150258951

其他的配置

package.json

json
{
  "files": ["dist"], // 把dist上传到npm
  "main": "./dist/v-element.umd.cjs", // 入口文件
  "module": "./dist/v-element.js", // es modules的设置文件
  "exports": {
    ".": {
      "import": "./dist/v-element.js",
      "require": "./dist/v-element.umd.cjs"
    }
  }
}

exports 可以是字符串或对象,当是对象时可以指定不同的导出方式和入口文件

组件库打包第三部分 - 生成类型定义文件

json
npm install vite-plugin-dts -D

使用

vite.config.ts

typescript
import dts from "vite-plugin-dts";

export default defineConfig({
  plugins: [dts()],
});

通过这样配置打包之后会发现有很多我们不需要的 ts 类型文件,我们只需要 src/index.ts,还有 components 和 hooks 目录下的 ts 文件

那么就可以在根目录下创建 tsconfig.build.json,内容和 tsconfig.config.json 差不多,只需要改 include 属性,改为

json
{
  ...
  include: ['src/index.ts', 'src/components/**/*','src/hooks/**/*']
}

接着修改 vite.config.ts

typescript
export default defineConfig({
  plugins: [
    dts({
      tsconfigPath: "./tsconfig.build.json",
    }),
  ],
});

组件库打包第四部分- 生成样式文件

在 src/index.ts 中引入样式

typescript
import "./styles/index.css";

这样就可以打包样式了,打包出来的样式文件名是 style.css

如果我们想要自定义打包出来的 css 文件名,可以在

vite.config.ts 中配置

typescript
rollupOptions: {
  output: {
    assetFileNames: (chunkInfo) => {
      if (chunkInfo.name === "style.css") {
        return "index.css";
      }
      return chunkInfo.name as string;
    };
  }
}

这样打包出来就是 index.css

除了打包样式文件,还需要打包字体,我们需要在 src/index.ts 中引入

ty
import { library } from '@fortawesome/fontawesome-svg-core'
import { fas } from '@fortawesome/free-solid-svg-icons'

library.add(fas)

但是我们会发现打包出来的代码库大小大大增加了

image-20250615153611129

我们自然而然会想到可以在 vite.config.ts 中配置 external,排除对这些字体包的打包,但是这样又需要在 globals 中手动配置全局的依赖,比较繁琐

image-20250615153927741

组件库打包第五部分 - 拆分构建脚本

基于上一节的问题,我们可以将打包配置文件拆分成 2 个

vite.umd.config.ts

typescript
import { fileURLToPath, URL } from "node:url";
import { resolve } from "path";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
import VueMacros from "unplugin-vue-macros";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    VueMacros.vite({
      plugins: {
        vue: vue(),
        vueJsx: vueJsx(),
      },
    }),
  ],
  resolve: {
    alias: {
      "@": fileURLToPath(new URL("./src", import.meta.url)),
    },
  },
  build: {
    outDir: "dist/umd",
    lib: {
      entry: resolve(__dirname, "src/index.ts"),
      name: "VElement",
      fileName: "v-element",
      formats: ["umd"],
    },
    rollupOptions: {
      external: ["vue"],
      output: {
        exports: "named",
        globals: {
          vue: "Vue",
        },
        assetFileNames: (chunkInfo) => {
          if (chunkInfo.name === "style.css") return "index.css";
          return chunkInfo.name as string;
        },
      },
    },
  },
});

vite.es.config.ts

typescript
import { fileURLToPath, URL } from "node:url";
import { resolve } from "path";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
import VueMacros from "unplugin-vue-macros";
import dts from "vite-plugin-dts";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    VueMacros.vite({
      plugins: {
        vue: vue(),
        vueJsx: vueJsx(),
      },
    }),
    dts({
      tsconfigPath: "./build.json",
      outDir: "dist/types",
    }),
  ],
  resolve: {
    alias: {
      "@": fileURLToPath(new URL("./src", import.meta.url)),
    },
  },
  build: {
    outDir: "dist/es",
    lib: {
      entry: resolve(__dirname, "src/index.ts"),
      name: "VElement",
      fileName: "v-element",
      formats: ["es"],
    },
    rollupOptions: {
      external: [
        "vue",
        "@fortawesome/fontawesome-svg-core",
        "@fortawesome/free-solid-svg-icons",
        "@fortawesome/vue-fontawesome",
        "@popperjs/core",
        "axios",
      ],
      output: {
        assetFileNames: (chunkInfo) => {
          if (chunkInfo.name === "style.css") return "index.css";
          return chunkInfo.name as string;
        },
      },
    },
  },
});

package.json

json
{
  "main": "./dist/umd/v-element.umd.cjs",
  "module": "./dist/es/v-element.js",
  "types": "./dist/types/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/es/v-element.js",
      "require": "./dist/umd/v-element.umd.cjs",
      "types": "./dist/types/index.d.ts"
    }
  },
  "scripts": {
    "build": "run-p build-es build-umd",
    "build-umd": "vite build --config vite.umd.config.ts",
    "build-es": "vite build --config vite.es.config.ts"
  }
}

VElement 是打包发布到 npm 的组件库项目,VikingTest 是本地使用发布的组件库的项目。

假如 VElement 的样式做了改动,我们想测试下看看是否生效,那就得改版本号,然后重新发布,本地使用的时候,重新安装最新的依赖,比较麻烦。

那么我们就可以在要发布到 npm 的项目中执行 npm link,接着在本地的测试项目 vikingtest 中执行 npm link v-element,这个 v-element 是发布到 npm 项目的文件名,要对应。

最终修正打包信息

将打包出来的 index.css 从 dist/es 目录移动到 dist 目录

json
npm install move-file-cli --save-dev

package.json

json
{
  "build": "npm run build-only && npm run move-style",
  "build-only": "run-p build-es build-umd",
  "move-style": "move-file dist/es/index.css dist/index.css"
}

另外一个问题,打包出来的 css 会报错

image-20250615163102203

在 package.json 中

json
{
  "exports": {
    "./dist": {
      "import": "./dist/",
      "require": "./dist/"
    }
  }
}

最后做一些收尾工作,精简 dependencies,我们使用第三方库的前提是使用 vue,否则没有意义,那么我们就可以将 vue 放入到 peerDependencies 中,但是同时我们开发中也需要用到 vue,dependencies 不需要 vue

json
{
  "peerDependencies": {
    "vue": "^3.2.45"
  },
  "devDependencies": {
    "vue": "^3.2.45"
  }
}

还有 sideEffects 中添加以下配置,这样 index.css 产生的副作用就不会被优化,确保样式的准确

json
{
  "sideEffects": ["dist/index.css"]
}

npm 简介

官网地址:https://www.npmjs.com/

功能:

  • 允许用户从 npm 服务器下载别人编写的第三方包到本地使用
  • 允许用户从 npm 服务器下载并安装别人编写的命令行程序到本地使用。
  • 允许用户将自己编写的包或命令行程序上传到 npm 服务器供别人使用

一些常见命令:

json
# 登陆,注意现在登录都是有两步验证的,会发一个数字到你的邮箱去
npm login
# 注册或者使用web 界面 https://www.npmjs.com/signup
npm adduser
# 查看是否登陆
npm whoami
# 特别注意,关闭淘宝代理,要不操作会失败
npm config ls

版本规则:Semver 标准语义化版本

https://semver.org/lang/zh-CN/

1.主版本号:当你做了不兼容的 API 修改

2.次版本号:当你做了向下兼容的功能性新增

3.修订号:当你做了向下兼容的问题修正

package.json 所有信息

https://docs.npmjs.com/cli/v10/configuring-npm/package.json

"private": "false"这个配置要去掉,否则不能发布

image-20250615165338590

发布到 npm

npm scripts

Pre&Post scripts

你 script 的名称前面加上 pre 或者 post,那么当运行这个命令的时候,pre 和 post 会自动在这个命令之前或者之后运行。

Life Cycle Scripts

  • prepare
    • 在 package 被 packed 之前运行
    • 在 package 被 published 之前运行
    • 在 npm install 的时候运行
  • prepublish(即将废弃)
  • prePublishOnly

npm publish 会触发的 hooks

  • prepublishOnly
  • publish
  • postpublish

package.json

json
{
  "scripts": {
    "prepublishOnly": "npm run build"
  }
}

这样在 publish 之前就会进行打包

文档地址

https://docs.npmjs.com/vli/v10/using-npm/scripts

npm 命名空间

  • @vikingmute/xxx

  • 前面的是命名空间名称,后面的是具体的包名

  • 命名空间通常用于避免包名冲突,并提供更好的包管理和组织,同时也可以提供更好的可读性和可发现性

  • 使用命名空间需要带特定参数,也就是公开,因为这个功能默认是 private 的并且收费的,公开就没有问题

  • json
    npm publish --access=public
  • 不使用命名空间就没有这个问题

package.json

json
{
  "name": "@vikingmute/element"
}