使用 tsup 构建项目库的实践过程
记录使用 tsup 打包同时支持浏览器和 Node.js 的加解密工具库,以及处理 jsencrypt 运行环境兼容问题的过程。

最近同时在做前端项目和 Strapi 后端服务。写到加解密功能时,我发现相同的代码需要反复复制,于是决定把它整理成一个工具库。
常见的打包工具有 Webpack、Vite 和 Rollup。继续寻找更轻量的方案时,我发现了 tsup:配置简单、构建速度快,正好符合这次需求。
介绍一下 tsup
tsup 是一个用于打包 Web 脚本的轻量级工具,它基于 esbuild,旨在提供快速且简单的打包体验。tsup 支持多种输出格式,如 CommonJS、ES 模块、UMD 等
特点
- 快速打包:利用 esbuild 的高性能,tsup 能够非常快速地打包 TypeScript 项目
- 多格式输出:支持多种输出格式,包括 CommonJS (cjs)、ES 模块 (esm)、UMD 等
- 零配置:默认情况下,tsup 可以自动检测项目配置并进行打包,无需复杂的配置文件
- 灵活配置:可以通过命令行参数或配置文件进行灵活配置
- 树摇优化:自动进行树摇优化,减少最终包的大小
- TypeScript 支持:原生支持 TypeScript,无需额外配置
如何使用
先使用 npm init 初始化项目,再安装 tsup:
shell
npm i tsup -D
# Or Yarn
yarn add tsup --dev
# Or pnpm
pnpm add tsup -D
安装
在根目录下手动建一个tsup.config.ts文件,这个就是tsup的配置文件,这里我列出我的配置文件
js
import { defineConfig } from 'tsup';
export default defineConfig([
{
format: ['esm', 'cjs', 'iife'], // 指定输出的模块格式。常见的格式有 esm(ES 模块)、cjs(CommonJS)、iife(立即执行函数表达式)等
entry: ['./example/index.js'], // 指定项目的入口文件。可以是一个或多个文件路径
outDir: 'lib', // 指定输出文件的目录
platform: 'neutral', // 指定目标平台。常见的值有 neutral(中立平台,适用于所有环境)、browser(浏览器环境)、node(Node.js 环境)
globalName: 'WebUtils', //当输出格式为 iife 时,指定全局变量的名称
outExtension({ format }) { // 自定义输出文件的扩展名。接收一个对象参数,包含当前的格式信息
if (format === 'iife') return { js: '.browser.js' };
return { js: `.${format}.js` };
},
minify: true, // 是否启用代码压缩
sourcemap: true, // 是否生成源映射文件
shims: true, // 是否启用 polyfills 和 shims,以确保兼容性
clean: true, // 清理输出目录
dts: true, // 是否生成 TypeScript 声明文件
}
]);
我已经在后面都标注说明了,配置项已经写在注释里,这里不再逐项展开。
编写加解密代码
这里使用 crypto-js 和 jsencrypt,两个仓库,一个用于对称加密,一个用于非对称加密,这两个兼容浏览器端和 Node.js 环境
js
import CryptoJS from "crypto-js";
import JSEncrypt from "jsencrypt"
/**
* 对称加密
* @param word
* @param iv
* @param key
* @returns {string}
*/
function aesEncrypt(word, iv, key) {
let str = typeof word == "string" ? word : JSON.stringify(word);
const data = CryptoJS.enc.Utf8.parse(str);
const encrypted = CryptoJS.AES.encrypt(data, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.toString();
}
/**
* 对称解密
* @param word
* @param iv
* @param key
* @returns {string}
*/
function aesDecrypt(word, iv, key) {
const decrypt = CryptoJS.AES.decrypt(word, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return CryptoJS.enc.Utf8.stringify(decrypt);
}
/**
* 获取加密的密钥
* @param iv
* @param key
*/
function getCrypto({iv = '', key = ''} = {}) {
return {
CryptoJS,
iv: CryptoJS.enc.Utf8.parse(iv),
key: CryptoJS.enc.Utf8.parse(key), //16位
}
}
/**
* 非对称加密
* @param str
* @param publicKey
* @returns {*}
*/
const rsaEncrypt = (str, publicKey) => {
let encrypted = new JSEncrypt() // 创建加密对象实例
encrypted.setPublicKey(publicKey) // 设置公钥
return encrypted.encrypt(str) // 对内容进行加密
}
/**
* 非对称解密
* @param str
* @param privateKey
*/
const rsaDecrypt = (str, privateKey) => {
const decrypted = new JSEncrypt() // 创建解密对象实例
decrypted.setPrivateKey(privateKey) // 设置私钥
return decrypted.decrypt(str) // 拿私钥解密内容
}
/**
* 非对称生成Key
* @param keySize
*/
const getJSEncrypt = (keySize = '2048') => {
const crypt = new JSEncrypt({default_key_size: keySize});
return new Promise((resolve) => {
crypt.getKey(function () {
return resolve({
privateKey: crypt.getPrivateKey(),
publicKey: crypt.getPublicKey()
})
})
})
}
export {
aesEncrypt,
aesDecrypt,
getCrypto,
rsaEncrypt,
rsaDecrypt,
getJSEncrypt
}
打包构建
执行npx tsup

构建成功,并且我也生成了lib目录下的文件
测试脚本
我写了一个例子进行测试
js
import {aesEncrypt,aesDecrypt, getCrypto,rsaDecrypt,rsaEncrypt,getJSEncrypt} from "../packages/index";
const {iv,key} = getCrypto({
iv: '1234567890123456',
key: '1234567890123456'
})
getJSEncrypt().then((res)=>{
console.log(res)
})
使用node进行测试
shell
node ./lib/index.cjs.js

发现jsencrypt出现了window,这肯定就不行了哇,Node.js 环境里没有 window,于是我逛了下百度,找到了答案,也就是在代码的前面加上window=this
这是当时打包后的临时兼容处理,我先手动加入 window = this 进行验证。
兼容性提醒:
window = this会污染全局对象,也不适合现代 ESM 和严格模式。生产项目更适合拆分浏览器与 Node.js 入口,或改用原生支持 Node.js 的加密实现。

测试正常,这里我生成了一个非对称密钥对

自动加入 window = this
当实现自动化的时候,我就想到了gulp, Gulp 是一个流式构建系统,用于自动化前端工作流程。它使用 JavaScript 和 Node.js,通过一系列插件和任务来处理文件和资源,它的特点如下
- 流式处理:Gulp 使用 Node.js 流来处理文件,这意味着文件可以在内存中被处理,而不需要多次读写磁盘,从而提高性能。
- 简洁的 API:Gulp 的 API 非常简洁,易于理解和使用。
- 丰富的插件生态系统:Gulp 拥有庞大的插件生态系统,可以轻松找到你需要的插件,如编译 LESS/SASS、压缩 CSS/JS、图像优化等。
- 任务自动化:Gulp 可以自动化常见的开发任务,如代码检查、文件合并、代码压缩、文件监听等。
使用 Gulp
在根目录下新建文件gulpfile.js,内容如下
js
const {src, dest} = require('gulp');
const insert = require('gulp-insert');
function defaultTask(cb) {
src('lib/index.cjs.js') // 指定要编辑的文件路径
.pipe(insert.prepend('window = this;')) // 向文件开头插入指定的字符串
.pipe(dest('lib')); // 指定输出目录
cb();
}
exports.default = defaultTask
注意gulp的写法,gulp一直在升级,写法和使用方式一直在变化
执行 Gulp
测试成功

回归功能:完整加解密代码
安全提醒:示例已移除真实密钥,并把 RSA 默认长度调整为 2048 位。固定的 AES 密钥和 IV 仅适合演示,不要直接用于生产环境。
js
import CryptoJS from "crypto-js";
import JSEncrypt from "jsencrypt"
/**
* 对称加密
* @param word
* @param iv
* @param key
* @returns {string}
*/
function aesEncrypt(word, iv, key) {
let str = typeof word == "string" ? word : JSON.stringify(word);
const data = CryptoJS.enc.Utf8.parse(str);
const encrypted = CryptoJS.AES.encrypt(data, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.toString();
}
/**
* 对称解密
* @param word
* @param iv
* @param key
* @returns {string}
*/
function aesDecrypt(word, iv, key) {
const decrypt = CryptoJS.AES.decrypt(word, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return CryptoJS.enc.Utf8.stringify(decrypt);
}
/**
* 获取加密的密钥
* @param iv
* @param key
*/
function getCrypto({iv = '', key = ''} = {}) {
return {
CryptoJS,
iv: CryptoJS.enc.Utf8.parse(iv),
key: CryptoJS.enc.Utf8.parse(key), //16位
}
}
/**
* 非对称加密
* @param str
* @param publicKey
* @returns {*}
*/
const rsaEncrypt = (str, publicKey) => {
let encrypted = new JSEncrypt() // 创建加密对象实例
encrypted.setPublicKey(publicKey) // 设置公钥
return encrypted.encrypt(str) // 对内容进行加密
}
/**
* 非对称解密
* @param str
* @param privateKey
*/
const rsaDecrypt = (str, privateKey) => {
const decrypted = new JSEncrypt() // 创建解密对象实例
decrypted.setPrivateKey(privateKey) // 设置私钥
return decrypted.decrypt(str) // 拿私钥解密内容
}
/**
* 非对称生成Key
* @param keySize
*/
const getJSEncrypt = (keySize = '2048') => {
const crypt = new JSEncrypt({default_key_size: keySize});
return new Promise((resolve) => {
crypt.getKey(function () {
return resolve({
privateKey: crypt.getPrivateKey(),
publicKey: crypt.getPublicKey()
})
})
})
}
export {
aesEncrypt,
aesDecrypt,
getCrypto,
rsaEncrypt,
rsaDecrypt,
getJSEncrypt
}
测试代码
js
import {aesEncrypt,aesDecrypt, getCrypto,rsaDecrypt,rsaEncrypt,getJSEncrypt} from "../packages/index";
const {iv,key} = getCrypto({
iv: '1234567890123456',
key: '1234567890123456'
})
// getJSEncrypt().then((res)=>{
// console.log(res)
// })
//
const privateKey = '<TEST_PRIVATE_KEY>'
const publicKey = '<TEST_PUBLIC_KEY>'
const str1 = aesEncrypt('123456', iv, key)
console.log(str1)
console.log(aesDecrypt(str1, iv, key))
console.log('------')
const str2 = rsaEncrypt('abcdefg', publicKey)
console.log(str2)
console.log(rsaDecrypt(str2, privateKey))
执行结果,均没有问题

源码我放在了github上面有兴趣的看一看https://github.com/hangjob/web-utils