前端UI组件库的打包与发布
UI组件库的打包是指“将开发完成的业务代码处理成可在生产环境中运行,并供用户在浏览器上使用”的过程。浏览器可以视为用户使用的生产环境,除浏览器外,常见的生产环境还有Node.js。Node.js通常是现代前端开发过程所运行的环境,例如我们常在Node.js环境下使用Vue和React,相当于Node.js是它们的生产环境。Node.js在13.2.0版本之前仅支持CommonJS模块,在13.2.0版本之后才开始支持ESM模块。为了让开发人员在不同版本的Node.js环境下运行UI组件库,需要提供CommonJS和ESM两种模式来打包UI组件库。
除上述环境外,打包UI组件库后的代码会进行压缩处理,需要通过source map文件映射回未压缩的代码,方便开发人员调试以及定位代码的出错位置,最后还要将scss文件打包成传统的css文件,用于提供全量UI组件库的css样式。在UI组件库的打包过程中,需要完成的工作如下:
- 提供浏览器端的代码包,可以是UMD或IIFE。
- 提供Node.js环境的CommonJS模块和ESM模块代码包。
- 提供全局引入的css样式,并按需加载css样式。
- 提供source map文件。
- 对UI组件库打包的代码进行压缩。
了解Rollup
Rollup是一个用于js模块的打包工具,它将小的代码片段编译成更大,更复杂的代码,例如库或应用程序。 它使用ES6版本的js中包含的新标准化代码模块格式,而不是以往的CommonJS和AMD等特殊解决方案。
Rollup的配置简单直观,能够生成轻量、高效的构建文件,尤其适用于构建JS库或框架。它还支持代码拆分、按需加载等功能,有助于优化和提升前端应用的性能。
Rollup具有以下特点。
- 高效:Rollup通过静态分析的方式,只打包必要的代码和依赖,打包过程更高效,打包后的产物也更小。
- 可扩展性强:Rollup支持各种插件和加载器扩展其功能,可以轻松地与Babel、ts、eslint等工具集成。
- 模块化:Rollup基于ES6模块,支持使用ES6模块化地语法和特性,例如import和export。
- 代码分割:Rollup支持将代码分割成多个小块,并支持按需加载或并行加载,以优化性能。
- Tree Shaking:Rollup可以消除不必要地代码和依赖,以减少最终地产物大小。
- 多格式输出:AMD、CommonJS、ES6 modules、UMD、IIFE、等。
使用Rollup打包UI组件库只需进行全局安装,在终端执行指令npm install --global rollup。
接下来的UI组件库打包过程将围绕Rollup的JS API特性展开,实现UMD、ES以及CJS这3种模块的打包。
初始化Build打包目录
Rollup有以下两种配置方式。
- 配置文件+命令行:可以提供一个可选的Rollup配置文件,以简化命令行的使用,并启用高级Rollup功能。
- JS API:一个可以在Nodejs中使用的JS API。这种方式相比起第一种方式会更加灵活。
新建build目录,并在build目录下执行npm init -y,为build生成package.json文件作为一个打包工具包。
Rollup的基础配置
如果你的ui组件库是使用Vue开发的,打包UI组件库就是将Vue文件编译成JS文件的过程,如果使用了ts,同样需要将以.ts为后缀的文件编译成js文件。在rollup中,推荐使用@vitejs/plugin-vue插件识别vue文件,使用@rollup/plugin-node-resolve插件解析npm包。对于ts,则可以使用rollup-plugin-esbuild插件进行编译。
根据上述说明,首先在build目录下安装上述3个插件。
{
"name": "build",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@rollup/plugin-node-resolve": "^16.0.0",
"@vitejs/plugin-vue": "^5.2.1",
"rollup": "^4.30.1",
"rollup-plugin-esbuild": "^6.1.1"
}
}
Rollup打包主要有4个步骤:
- 配置打包入口文件。
- 配置所需插件。
- 配置输出文件格式。
- 打包输出目录。
在build目录下新建src目录以及umdBuild.js文件,用于实现umd格式的打包,代码如下:
import { rollup } from "rollup";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import vue from "@vitejs/plugin-vue";
import esbuild from "rollup-plugin-esbuild";
/**
* node.js 核心方法 url、path
*/
import { resolve } from "path";
import { pkgRoot, outputUmd } from "./common.js"
// umd打包
export const umdBuildEntry = async () => {
const writeBundles = await rollup({
input: resolve(pkgRoot, "index.js"), // 配置打包入口文件
plugins: [ // 配置插件
vue(),
nodeResolve({ extensions: ['.ts'] }),
esbuild(),
],
external: ["vue"], // 排除不进行打包的npm包
});
writeBundles.write({
})
};
umdBuildEntry()
配置打包路径
在打包UI组件库时需要配置打包文件的入口、插件、格式和输出目录。
/**
* node.js 核心方法 url、path
*/
//common.js
import { fileURLToPath } from "url";
import { resolve, dirname } from "path";
export const outputPkgDir = 'hzu'//输出目录
export const filePath = fileURLToPath(import.meta.url);
export const dirName = dirname(filePath);
export const rootDir = resolve(dirName, "..", ".."); // 获取UI组件库 “根目录”
export const pkgRoot = resolve(rootDir, "packages"); // 获取UI组件包的目录,入口
console.log('filePath', filePath)
console.log('dirName', dirName)
console.log('rootDir', rootDir)
console.log('pkgRoot', pkgRoot)
export const outputDir = resolve(rootDir, outputPkgDir)
// es
export const outputEsm = resolve(rootDir, outputPkgDir, "es")
// lib
export const outputCjs = resolve(rootDir, outputPkgDir, "lib")
// dist
export const outputUmd = resolve(rootDir, outputPkgDir, "dist")
UMD打包
UMD打包是一种将js库或模块打包成可以在不同环境中使用的通用格式的方法。UMD打包同时兼容CommonJS、AMD和全局变量的使用方式,因此可以在项目的<script>中引入通过UMD打包后的产物,直接在浏览器中以访问全局变量的方式使用。
输出UMD组件包
完成了UI组件库打包文件入口的路径后,在打包时,只需要配置打包文件格式的模式以及打包后产物出书的目录即可。
import { rollup } from "rollup";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import vue from "@vitejs/plugin-vue";
import esbuild from "rollup-plugin-esbuild";
/**
* node.js 核心方法 url、path
*/
import { resolve } from "path";
import { pkgRoot, outputUmd } from "./common.js"
// umd打包
export const umdBuildEntry = async () => {
const writeBundles = await rollup({
input: resolve(pkgRoot, "index.js"), // 配置打包入口文件
plugins: [ // 配置插件
vue(),
nodeResolve({ extensions: ['.ts'] }),
esbuild(),
],
external: ["vue"], // 排除不进行打包的npm包
});
writeBundles.write({
format: "umd",//指定生成的包的格式。这里是“全量打包”,也就是通用模块,定义格式为“umd”
file: resolve(outputUmd, "index.full.js"),//生成指定的文件。
name: "HzuUI",//自定义包的全局变量名称,也是打包后的产物可以访问的变量名称。将属性format指定为iife或umd打包格式,属性name是必选的
globals: {//定义UI组件库打包后所需要依赖的变量。目前,我们开发的UI组件库使用的是Vue.js 3,因此需要告诉Rollup,Vue是外部依赖的,vue模块的全局变量是“Vue”
vue: "Vue",
},
})
};
umdBuildEntry()
然后进入build/src目录,并在终端执行命令行node ./umdBuild.js,打包生成UMD格式的组件包。
ESM、CJS模块化打包
ESM和CJS是js中使用的不同模块。ESM是现代浏览器和Node.js支持的标准模块,CJS是传统意义上在Node.js中使用的模块系统。
ESM是ECMAScript标准中定义的模块化规范,它是现代js开发中推荐的模块化方案。ESM分别使用import和export关键字来导入和导出模块。
CommonJS是Node.js中使用的模块化规范,也可以在其他环境中使用,如使用Rollup、Browserify、Webpack等工具进行打包。CommonJS分别使用require和module.exports来导入和导出模块。
ESM、CJS打包输出
UMD包属于全量模式打包,也就是将所有的组件打包为一份js文件,通过在浏览器中使用<script>标签引入组件。经过UMD打包的文件大,并且无法支持按需加载。为了使打包的组件库支持按需加载模式,需要使用ESM和CJS打包模式实现按需加载,也可以在打包过程中实现Tree shaking(去除js中无用的代码)优化。
import glob from "fast-glob";
import { rollup } from "rollup";
import path from 'path'
import { nodeResolve } from "@rollup/plugin-node-resolve";
import postcss from "rollup-plugin-postcss"
import vue from "@vitejs/plugin-vue";
import esbuild from "rollup-plugin-esbuild";
/**
* node.js 核心方法 url、path
*/
import { pkgRoot, outputEsm, outputCjs, outputPkgDir } from "./common.js"
//打包入口文件
export const moduleBuildEntry = async () => {
const input = await glob("**/*.{js,ts,vue}", {
cwd: pkgRoot,
absolute: true, // 绝对路径
onlyFiles: true, // 文件的路径,不需要目录
})//获取的文件使array数据的集合
const writeBundles = await rollup({
input, // 配置打包入口文件
plugins: [ // 配置插件
compileStyleEntry(),
vue(),
nodeResolve({ extensions: ['.ts'] }),
esbuild(),
postcss({
pextract: true, // css通过链接引入
}),
],
external: [ // 排除不进行打包的npm包
'vue',
'@vueuse/core',
'async-validator',
],
});
writeBundles.write({
format: "esm",//打包格式
dir: outputEsm,//打包后组件输出的目录
preserveModules: true,//使打包后的组件库模块结构和源码的模块结构保持一致,需要设置为true
entryFileNames: `[name].mjs`,//入口文件名称。
sourcemap: true,
})
writeBundles.write({
format: "cjs",
dir: outputCjs,
preserveModules: true,
entryFileNames: `[name].cjs`,
sourcemap: true,
});
};
// 重写@import路径,重写每个组件style/index.js引用样式的路径
const compileStyleEntry = () => {
const themeEntryPrefix = `@ui-library/theme/src/`
return {
name: 'compile-style-entry',
resolveId (id) {
if (!id.startsWith(themeEntryPrefix)) return
return {
id: id.replaceAll(themeEntryPrefix, `${outputPkgDir}/theme/src/`),
external: 'absolute',
}
}
}
}
moduleBuildEntry()
测试模块化组件包
完成组件库打包后,可以在本地模拟npm包测试,无须发布至npm官网。在本地模拟npm包中,通过npm link命令与全局node_modules包建立全局链接。
{
"name": "hzu",
"version": "1.0.0",
"description": "自定义组件包",
"main": "./lib/index.cjs",
"module": "./es/index.mjs",
"directories": {
"lib": "lib"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
-
建立全局链接
每个npm包中都有一个package.json文件,本地模拟npm包也不例外。进入hzu目录,执行npm init -y命令,自动生成package.json文件。在package.json文件中定义main和module两个关键字,分别指向打包后的lib/index.cjs和es/index.mjs,然后执行命令npm link。npm link命令可以为一个任意位置的npm包与全局的node_module建立链接,在系统中做快捷映射,建立链接之后即可在本地进行模拟测试。 -
本地测试
安装打包后的组件包,进行目标目录(一般是前端工程)执行命令npm link hzu。hzu是package.json文件属性name的值,也就是包的名称。
3.全局引入
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import UILibrary from 'hzu'//引入examples目录node_modules目录中的hzu依赖,而不是之前工程中的packages开发包
//import UILibrary from "../../packages"
import '@ui-library/theme/src/index.scss' //全量引入组件样式
const app = createApp(App)
app.use(UILibrary) //全局注册组件,如果按需加载可以屏蔽该行代码
app.mount('#app')
4.按需引入
<script setup>
//import { HzuMessage, HzuMessageBox} from '@ui-library/components';
//import { Close, Eye,Show,Hide } from '@ui-library/icons';
//import { HzuButton } from '@ui-library/components';
import { HzuMessage, HzuMessageBox} from 'hzu';
import { Close, Eye,Show,Hide } from 'hzu';
import { ref, h } from 'vue';
import { HzuButton } from 'hzu'; //如果使用全局注册组件,那么只需要在main.js引入即可,如果是按需加载组件,那么就需要在用到的地方import
</script>
Gulp打包scss文件
scss文件基本上是现代前端开发的标配,但是浏览器并不支持scss文件,因此需要将scss文件编译为css文件。
gulp-sass是一个Gulp插件,用于将scss文件编译成css文件。
gulp-sass的优势如下:
- 高效性:gulp-sass可以快速将scss文件编译成css文件,提高开发效率。
- 实时预览:结合浏览器自动刷新等工具,实时预览更改后的样式。
- 易于配置:灵活的选项和参数允许开发人员根据需求定制编译过程。
使用gulp-sass需要先安装gulp和gulp-sass依赖包,执行以下命令:
- npm install gulp
- npm install gulp-sass //高于4版本的是不自带sass编译器的,因此还需要安装sass编译器的包
- npm install sass
全量打包css和按需加载打包css
全量打包css是指将所有组件的css文件合并为一个单独的文件。全量打包的优势在于减少http请求次数,提高页面加载速度,并简化管理和部署过程。然而,需要注意的是,全量打包css可能导致文件体积过大,反而影响网页性能。因此,在全量打包css时,需要考虑对css文件进行压缩和优化,以确保在减少请求次数的同时,保持较小的文件大小和高效的加载速度。
按需加载打包css的方式和全量打包css类似,只是指定的入口和输出的目录不同。
在build目录下新建styleBuild.js文件,用于打包css样式,并引入gulp和gulp-sass,代码如下:
import gulp from "gulp";
import dartSass from "sass";
import gulpSass from "gulp-sass";
import autoprefixer from "gulp-autoprefixer";
import cleanCSS from "gulp-clean-css";
import gulpConcat from "gulp-concat";
//删除文件或者文件夹
import { deleteAsync } from "del"
import { rootDir, pkgRoot, outputDir, outputUmd } from "./common.js";
/**
* 按需加载打包CSS
*/
const buildScssModules = async () => {
const sass = gulpSass(dartSass);
await new Promise((resolve) => {
gulp.src(`${rootDir}/packages/theme/src/**/*.scss`)
.pipe(sass.sync()) // 编译
.pipe(autoprefixer({ cascade: false })) // 兼容
.pipe(cleanCSS()) // 压缩
.pipe(gulp.dest(`${outputDir}/theme`))
.on("end", resolve); // 监听流完成
});
deleteFiles()
};
/**
* 全量打包CSS
*/
const buildScssFull = async () => {
const sass = gulpSass(dartSass);
await new Promise((resolve) => {
gulp.src(`${pkgRoot}/theme/src/index.scss`)//指定打包入口
.pipe(sass.sync()) // 编译
.pipe(autoprefixer({ cascade: false })) // 兼容浏览器,自动根据所使用的css属性添加浏览器前缀,如-webkit-、-ms-等
.pipe(cleanCSS()) // 压缩css
.pipe(gulpConcat('index.min.css'))//合并到指定文件
.pipe(gulp.dest(outputUmd)) // dist
.on("end", resolve); // 监听流完成
});
};
/**
* 拷贝scss
*/
const cloneScss = async () => {
await new Promise((resolve) => {
gulp.src(`${pkgRoot}/theme/src/**/*`)
.pipe(gulp.dest(`${outputDir}/theme/src`))
.on("end", resolve); // 监听流完成
});
}
/**
* 删除指定文件或文件夹
*/
const deleteFiles = async () => {
await deleteAsync(
[`${outputDir}/theme/index.css`, `${outputDir}/theme/common`], //index.css是全量打包css,common目录是在开发ui组件库时定义的scss变量和混合指令,都可以删除
{ force: true }//设置为true,表示可以跨当前目录删除文件
)
}
export const buildStyle = async () => {
await Promise.all([cloneScss(), buildScssFull(), buildScssModules()]);
};
buildStyle()
Gulp多任务
Gulp多任务是指一种功能强大的机制,允许用户定义一组相关的任务进行批量处理,无须单独定义和调用每个任务。这种机制允许用户以模块化的方式住址构建过程,并避免编写重复的代码。
series()和parallerl()
Gulp执行多任务的常用方式分为串行和并行,分别对应series()和parallerl()。可以简单理解为:串行时一个一个任务执行,只有上一个任务执行完成,才会进入下一个任务,并行是可以同时执行多个任务。
在打包UI组件库时,包含UMD、ESM和CJS和CSS样式以及删除等动作,属于多任务,由于打包时执行的任务并不多,并且各个函数之间不存在相互依赖关系,因此可以使用串行或并行方式。
//index.js
export * from "./files.js"
export * from "./umdBuild.js"
export * from "./moduleBuild.js"
export * from "./styleBuild.js"
//gulpgile.js
import gulp from "gulp";
import { deletePkg, umdBuildEntry, moduleBuildEntry, buildStyle, copyPackage } from "./src/index.js"
export default gulp.series(
gulp.series(deletePkg, umdBuildEntry, moduleBuildEntry, buildStyle, copyPackage )
)
//files.js
import gulp from "gulp";
//删除文件或者文件夹
import { deleteAsync } from "del"
import { outputDir, pkgRoot } from "./common.js"
/**
* 存在包,则先删除,确保ui组件库是最新的的文件,如果没有不删除组件包,那么会遗留历史文件
*/
export const deletePkg = async () => {
await deleteAsync([outputDir], { force: true })
}
//npm包和package.json文件之间有着密切的关系,package.json文件是npm包的清单和配置文件,它给出了一个node项目的基本信息、依赖项和允许脚本,并标识了项目的名称和版本号。npm包的发布需要package.json文件存在,并且必须有package.json文件的属性name和version,如果没有,则无法正常执行npm install命令。
//在package目录下新建package.json文件,复制该文件
// 复制package.json
export const copyPackage = async () => {
await new Promise((resolve) => {
gulp.src(`${pkgRoot}/package.json`)
.pipe(gulp.dest(`${outputDir}`))
.on("end", resolve); // 监听流完成
});
}
//buid/package.json
"scripts": {
"start": "gulp --require @esbuild-kit/cjs-loader -f gulpfile.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
整个工程目录如下所示:
npm发布
npm是node.js的一个包管理器和分发工具。npm是node.js平台默认的包管理工具,也是世界上最大的软件注册表,包含超过60万个包的结构,可以使用户轻松跟踪依赖项和版本。npm是一个开放源代码的命令行工具,用于安装,更新和管理node模块。它允许用户从一个集中的仓库中下载和安装公共的node模块。npm随着node一起安装,解决了node代码部署中的许多问题,如包的管理(包括安装、卸载、更新、查看、搜索、发布等),并详细记录了每个包的信息(包括作者、版本、依赖、授权信息等),从而将开发人员从繁琐的包管理工作中解放出来,专注于功能的开发。
package.json文件
package.json是一个基于json格式的文件,它在node.js、前端项目、包管理器等项目中扮演这非常重要的角色。package.json出出现在node.js应用程序或者模块的根目录中,充当项目的清单文件。package.json文件中包含项目的各种必要信息,包括但不限于项目名称、版本、描述、作者、许可证信息,以及更重要的依赖关系管理和脚本定义。
package.json包含的字段如下:
- name:包的名称,显示在npm平台中的名称,用户使用安装、引用包的名称。
- version:包版本号。格式为“主版本号.次版本号.修订号-先行版(alpha/beta).公测修正版”,如1.0.0-alpha、1.0.0-rc.1。如果存在相同的版本号,则无法发布至npm平台。
- private:是否私有。发布至npm时将其设置为false。
- author:项目的作者信息。
- contributors:项目贡献者,由多个人组成,如[{name:“xxxx”,emial:“”}]。
- keywords:包的关键词。字符串数组,用于增加包的曝光量。在npm平台中搜索,在结果列表中可以看到搜索的包和对应的描述。
- description:项目的简要描述,用于增加包的曝光率。作用与keywords类似。
- main:指定commonjs或es模块的入口文件。
- module:指定es模块入口文件。
- scripts:用于定义运行命令的脚本。
- dependencies:项目依赖的生产环境包。
- devDependencies:项目依赖的开发环境包。
- peerDependencies:对等依赖包。提示用户需要安装peerDependencies所指定依赖的包。可以简单理解为:对于我们开发的库、插件等,需要指定某个范围版本的依赖包才能正常运行,如果它们不是在指定范围内的版本,则会报错或发出警告。
- sideEffects:用于标识某些模块时无副作用的,不应该被Tree-shaking移除。
- license:项目的许可证信息。开源许可证是一种法律协议,用于明确开源软件的使用规则,可以选择ISC、MIT。
- homepage:主页信息,项目首页的url地址。
- bugs:问题反馈的url地址,可以是email地址。如bugs:{url:‘xxxx’,email:“xxxx”}。
- responsity:项目源代码仓库所在位置,如responsity:{type:“git”,url:“git+https://github.com/xxxx.git”}。
版本号 | 范围 |
---|---|
^1.2.3 | >=1.2.3 ~ <2.0.0-0 |
^0.2.3 | >=0.2.3 ~ <0.3.0-0 |
^0.0.3 | >=0.0.3 ~ <0.0.4-0 |
^1 | >=1.0.0 ~ <2.0.0-0 |
~1.2.3 | >=1.2.3 ~ <1.3.0 |
~1.2 | >=1.2.0 ~ <1.3.0 |
~1 | >=1.0.0 ~ <2.0.0 |
- npm1、npm2版本
如果出现主项目和工具包共同所需的依赖包版本不兼容的情况,那么会使主项目依赖一个包,工具包依赖一个包。 - npm3~npm6版本
如果出现主项目和工具包共同所需要的依赖包版本不兼容的情况,会出现提示警告,并需要自己手动安装依赖。 - npm7以及以上版本
如果出现主项目和工具包共同所需的依赖包版本不兼容的情况,会提示错误信息,无法完成对包的依赖。
发布组件库
在npm官方网站中注册自己的账号,并在npm平台中确认是否存在和package.json文件的name属性相同名称的组件包,如果存在,则需要修改name的值,确保其是唯一的。
- 命令行登录
在根目录终端中执行pnpm run build命令打包成功后,会自动生成hzu组件包,然后进入hzu目录,在终端中执行命令npm login,登录npm平台,在登录过程中需要输入账号、密码、邮箱、验证码等。 - 浏览器登录
如果你安装的是9版本以上的npm,那么在终端执行npm login命令时,会提示你按下“ENTER”键。 - 发布确定镜像源
发布确定镜像源是为了确定发布的平台,由于在开发项目过程中可能使用了非npm官方的镜像源,如cnpm镜像等。因此,为了将项目顺利发布至npm官方平台,可以先执行命令行npm config set registry=https://registry.npmjs.org,将镜像源改到npm官方平台。然后执行npm login,登录成功后,在执行npm publish。