技术实践

使用 tsup 构建项目库的实践过程

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

羊先生的头像
羊先生2026.08.17 · 7 分钟阅读 · 5.3k 阅读
使用 tsup 构建项目库的实践过程封面
文章正文还需 7 分钟

最近同时在做前端项目和 Strapi 后端服务。写到加解密功能时,我发现相同的代码需要反复复制,于是决定把它整理成一个工具库。

常见的打包工具有 Webpack、Vite 和 Rollup。继续寻找更轻量的方案时,我发现了 tsup:配置简单、构建速度快,正好符合这次需求。

介绍一下 tsup

tsup 是一个用于打包 Web 脚本的轻量级工具,它基于 esbuild,旨在提供快速且简单的打包体验。tsup 支持多种输出格式,如 CommonJS、ES 模块、UMD 等

特点

  1. 快速打包:利用 esbuild 的高性能,tsup 能够非常快速地打包 TypeScript 项目
  2. 多格式输出:支持多种输出格式,包括 CommonJS (cjs)、ES 模块 (esm)、UMD 等
  3. 零配置:默认情况下,tsup 可以自动检测项目配置并进行打包,无需复杂的配置文件
  4. 灵活配置:可以通过命令行参数或配置文件进行灵活配置
  5. 树摇优化:自动进行树摇优化,减少最终包的大小
  6. 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-jsjsencrypt,两个仓库,一个用于对称加密,一个用于非对称加密,这两个兼容浏览器端和 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

image

构建成功,并且我也生成了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
image

发现jsencrypt出现了window,这肯定就不行了哇,Node.js 环境里没有 window,于是我逛了下百度,找到了答案,也就是在代码的前面加上window=this

这是当时打包后的临时兼容处理,我先手动加入 window = this 进行验证。

兼容性提醒:window = this 会污染全局对象,也不适合现代 ESM 和严格模式。生产项目更适合拆分浏览器与 Node.js 入口,或改用原生支持 Node.js 的加密实现。

image

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

image

自动加入 window = this

当实现自动化的时候,我就想到了gulp, Gulp 是一个流式构建系统,用于自动化前端工作流程。它使用 JavaScript 和 Node.js,通过一系列插件和任务来处理文件和资源,它的特点如下

  1. 流式处理:Gulp 使用 Node.js 流来处理文件,这意味着文件可以在内存中被处理,而不需要多次读写磁盘,从而提高性能。
  2. 简洁的 API:Gulp 的 API 非常简洁,易于理解和使用。
  3. 丰富的插件生态系统:Gulp 拥有庞大的插件生态系统,可以轻松找到你需要的插件,如编译 LESS/SASS、压缩 CSS/JS、图像优化等。
  4. 任务自动化: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

测试成功
image

回归功能:完整加解密代码

安全提醒:示例已移除真实密钥,并把 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))

执行结果,均没有问题

image

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