Appearance
组件库打包
本章计划:
- 了解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之前没有模块的年代

全局变量+命名空间(namespace)
IIFE自执行函数,创建一个封闭的作用域,赋值给一个全局变量

缺点
- 依赖全局变量,污染作用域,不安全
- 依赖约定命名空间避免冲突,可靠性不高
- 需要手动管理依赖并控制执行顺序,容易出错
- 需要再最终上线前手动合并所有用到的模块
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
- 对于web应用来说:一般采用Javascript文件入口
- https://webpack.js.org
后起之秀-Rollup
Vite的工作原理
解决的问题
大型应用使用webpack等传统bundler会遇到性能瓶颈:非常慢。Vite就使用浏览器发展中的新特性来解决这个问题。
开发环境
Vite以原生ESM方式提供源码,这实际上是让浏览器接管了打包程序的部分工作。
- 依赖
- 使用esbuild进行预构建,esbuild由go编写,比基于Node.js的工具快10-100倍。
- 处理CommonJS以及UMD类型文件的兼容性,转为ESM以及ESM的导入形式
- 提高性能,在将多个模块合并成单个模块。因为原生ESM格式下,一个文件就是一次请求。
- 缓存,将预构建的依赖项缓存到node_modules/.vite中。
- 使用esbuild进行预构建,esbuild由go编写,比基于Node.js的工具快10-100倍。
- 源码
- 包含一些非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

main.ts中导入

然后在App.vue中使用

这里使用$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-devjavascript
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'
}
}
}
}后缀名的配置
没有配置之前

在package.json中配置之后
json
{
type: 'module'
}
其他的配置
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)但是我们会发现打包出来的代码库大小大大增加了

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

组件库打包第五部分 - 拆分构建脚本
基于上一节的问题,我们可以将打包配置文件拆分成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",
}
}npm link 测试打包内容

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-devpackage.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会报错

在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简介
功能:
- 允许用户从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"这个配置要去掉,否则不能发布

发布到 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"
}