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
image-20250615132651811

结论

  • 首要格式-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中

image-20250615145429349

打包之后发现,第一,后缀名是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",
  }
}
image-20250615161813112

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

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

image-20250615162026338

那么我们就可以在要发布到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"
}