网页下载二维码压缩包_基于vue2、jszip与qrcode打包下载二维码_对比服务端生成压缩包
如何基于jszip与qrcode在网页上实现下载二维码的压缩包,对比服务端生成有什么优劣势,本文会详细说明
网页下载二维码压缩包_基于vue2、jszip与qrcode打包下载二维码_对比服务端生成压缩包-MakerLi

现在有一个需求是在网页上生成指定的二维码,按照以前的逻辑是在服务端生成二维码,并且再把文件打包压缩

引入QRCode:

import * as QRCode from "qrcode";
const qrOptions={                  // 二维码选项
        width: 500,               // 图片宽度
        margin: 1,                // 边距
        color: {
          dark: '#000000',      // 前景色
          light: '#FFFFFF00'      // 背景色
        }
      };
await QRCode.toFile(filePath, "xxxx", qrOptions);

就可以生成指定文件到服务器目录,

如果需要生成自定义的海报,则是有两种方式:

  1. puppeteer
  2.   生成一个网页,将二维码与样式以网页的形式截图,保存图片
import * as puppeteer from "puppeteer";
    const browser = await puppeteer.launch({
      headless: 'new',  // 使用无头模式
      args: ['--no-sandbox', '--disable-setuid-sandbox']  // 沙箱配置
    });
    
   await this.captureScreenshot(browser, { url:"xxx",name:"test" }, {
      urls: [],// 要截图的网页列表
      outputDir: './public/tempZipImg',          // 截图保存目录
      outputZipDir: './public/zip',          // 截图保存目录
      zipFileName: 'Charging QR Code (with Details).zip', // 压缩文件名
      viewport: { width: 1920, height: 1080 }, // 视口大小
      fullPage: true,                    // 是否截取整个页面
      timeout: 30000,                     // 页面加载超时时间(毫秒)
      qrOptions: {                  // 二维码选项
        width: 500,               // 图片宽度
        margin: 1,                // 边距
        color: {
          dark: '#000000',      // 前景色
          light: '#FFFFFF00'      // 背景色
        }
      }
    });
  /**
   * @description: 
   * @param {*} browser 无头浏览器对象
   * @param {*} urlInfo.url 二维码内容,此时是一个url
   * @param {*} urlInfo.name 二维码的名字
   * @param {*} config 部分配置
   * @return {*}
   * @Date: 2025-06-18 13:38:24
   */
  async captureScreenshot(browser, urlInfo, config) {
    const page = await browser.newPage();
    try {

      // 设置视口大小
      await page.setViewport(config.viewport);

      // 导航到目标URL 
      await page.goto(urlInfo.url, {
        waitUntil: 'networkidle2',
        timeout: config.timeout
      });

      // 截图路径
      const screenshotName = urlInfo.name+".png";
      const screenshotPath = join(config.outputDir, screenshotName);

      // 截取屏幕 
      await page.screenshot({
        path: screenshotPath,
        fullPage: config.fullPage
      });

      console.log('截图完成!');

      return {
        path: screenshotPath,
        name: screenshotName
      };
    } catch (error) { 
      // 返回null表示该URL截图失败
      return null;
    } finally {
      // 关闭页面
      await page.close();
    }
  }

这样的劣势就是每一张图片需要2-3s左右的时间,并且需要耗费服务器的性能

  1. Canvas

使用canvas可以自己绘制,就是需要一定的canvas基础

  import * as QRCode from "qrcode";
import { createCanvas, loadImage } from 'canvas';

  async canvasCode(urlInfo, config) {
    const options = {
      title: urlInfo.name,
      subtitle: "2",
      text: urlInfo.url,
      footer: "1",
      logoUrl: "./public/images/qrtest.png"

    }
    const canvasWidth = 500;
    const canvasHeight = 500;
    const canvas = createCanvas(canvasWidth, canvasHeight);
    const ctx = canvas.getContext('2d');

    // 绘制背景
    ctx.fillStyle = '#ffffff';
    ctx.fillRect(0, 0, canvasWidth, canvasHeight);

    // 绘制标题
    ctx.fillStyle = '#333333';
    ctx.font = 'bold 24px Arial';
    ctx.textAlign = 'center';
    ctx.fillText(options.title, canvasWidth / 2, 50);

    // 绘制副标题
    ctx.font = '16px Arial';
    ctx.fillText(options.subtitle, canvasWidth / 2, 85);

    // 生成二维码
    const qrCodeSize = 280;
    const qrCodeBuffer = await QRCode.toBuffer(options.text, {
      width: qrCodeSize,
      margin: 1
    });
    const qrCodeImage = await loadImage(qrCodeBuffer);

    // 绘制二维码
    const qrCodeX = (canvasWidth - qrCodeSize) / 2;
    const qrCodeY = 120;
    ctx.drawImage(qrCodeImage, qrCodeX, qrCodeY, qrCodeSize, qrCodeSize);

    // 绘制底部文本
    ctx.font = '14px Arial';
    ctx.fillText(options.footer, canvasWidth / 2, canvasHeight - 30);

    // 加载并绘制logo
    if (options.logoUrl) {
      try {
        const logoImage = await loadImage(join(options.logoUrl));
        const logoSize = 60;
        const logoX = (canvasWidth - logoSize) / 2;
        const logoY = canvasHeight - 100;

        // 绘制logo圆形背景
        ctx.beginPath();
        ctx.arc(logoX + logoSize / 2, logoY + logoSize / 2, logoSize / 2, 0, Math.PI * 2);
        ctx.fillStyle = '#ffffff';
        ctx.fill();
        ctx.closePath();

        // 绘制logo
        ctx.save();
        ctx.beginPath();
        ctx.arc(logoX + logoSize / 2, logoY + logoSize / 2, logoSize / 2 - 2, 0, Math.PI * 2);
        ctx.clip();
        ctx.drawImage(logoImage, logoX, logoY, logoSize, logoSize);
        ctx.restore();
      } catch (error) {
        console.error('Failed to load logo:', error);
        // 绘制默认logo占位符
        const logoSize = 60;
        const logoX = (canvasWidth - logoSize) / 2;
        const logoY = canvasHeight - 100;

        ctx.fillStyle = '#eeeeee';
        ctx.beginPath();
        ctx.arc(logoX + logoSize / 2, logoY + logoSize / 2, logoSize / 2, 0, Math.PI * 2);
        ctx.fill();

        ctx.fillStyle = '#999999';
        ctx.font = '12px Arial';
        ctx.fillText('LOGO', logoX + logoSize / 2, logoY + logoSize / 2 + 4);
      }
    }
    const fileName = urlInfo.name+".png";
    const filePath = join(config.outputDir, fileName);
    fs.writeFileSync(filePath, canvas.toBuffer('image/png'));
    return {
      path: filePath,
      name: fileName
    }
  }

生成完图片之后,需要打包压缩

import * as archiver from "archiver";
  /**
   * @description: 
   * @param {*} screenshotFiles 截图文件目录数组
   * @param {*} zipPath 输出的目录
   * @param {*} name
   * @return {*}
   * @Date: 2025-06-18 13:44:40
   */
  async fnCreateZipArchive(screenshotFiles, zipPath, name) {
 
    const output = fs.createWriteStream(zipPath);
    const archive = archiver('zip', { zlib: { level: 9 } });  // 最高压缩级别
    
    // 监听压缩过程中的事件
    output.on('close', () => { 
    });
    
    archive.on('error', (err) => {
        console.error('压缩过程中出错:', err);
        throw err;
    });
    
    // 将所有截图添加到压缩包
    archive.pipe(output);
    
    for (const file of screenshotFiles) {
        if (file) {
            archive.file(file.path, { name: file.name });
        }
    }
    
    // 完成压缩
    await archive.finalize();
    
    return zipPath;
  }

上诉都是服务器生成图片压缩的逻辑,下面是基于vue2页面的生成压缩包,好处是减少服务器压力

<template>
    <a-dropdown>
        <a-menu slot="overlay">
            <a-menu-item key="1" @click=fnDownZipQRCode(1)> 下载普通二维码 </a-menu-item>
            <a-menu-item key="2" @click=fnDownZipQRCode(2)> 下载样式二维码 </a-menu-item>
        </a-menu>
        <pButton>
            <div class="downQRCodeBox">
                下载二维码 <a-icon type="down" />
                <div class="downQRCodeBody">
                    <div v-for="(qrcode, index) in qrcodeImages" :key="index" class="qrcode-item">
                        <div :id="'qrcode-' + index"></div>
                    </div>
                </div>
            </div>
        </pButton>
    </a-dropdown>

</template>

<script>
 
import QRCode from 'qrcode';
import JSZip from 'jszip';
import { saveAs } from 'file-saver';
import { formatinvalidChars } from "@/common-components/utils/util";
export default {
    name: "DownQRCodeButton",
    components: {},
    props: {
       
    },
    data() {
        return {
            qrcodeImages: [],
            sStationName: "",
            qrOptions: {
                width: 500,
                color: {
                    dark: '#000000',
                    light: '#ffffff00',//透明背景
                },
            }
        };
    },
    methods: {
        async fnDownZipQRCode(type) {

            try {
                let res = {
                    data:{
                        name:"名称",
                        aQRcode:['xxx:yyy','xxx:yyy']
                    }
                }
                if (res.success) {
                    this.sStationName = res.data.name ? (formatinvalidChars(res.data.name) + " ") : "";
                    this.qrcodeImages = res.data.aQRcode;
                    if(res.data.aQRcode.length==0){
                        //需要提示
                        return
                    }
                    this.generateQRCodes(type);
                } else {
                    console.error(res);

                }
            } catch (error) {
                if (error && error.code) {
                     //提示错误
                }

            }

        },
        // 生成二维码
        async generateQRCodes(type) {

            await new Promise(resolve => setTimeout(resolve, 100));
            try {
                for (let i = 0; i < this.qrcodeImages.length; i++) {
                    const content = this.qrcodeImages[i];
                    const qrcodeId = "qrcode-"+i;
                    // 生成二维码
                    await this.generateQRCode(qrcodeId, content, type);
                }
                await this.downloadZip(type);

            } catch (error) {

                console.error('生成二维码失败:', error);
            } finally {

            }
        },
        // 生成单个二维码
        generateQRCode(elementId, content, type = 1) {

            return new Promise(async (resolve, reject) => {
                // 确保元素存在
                const container = document.getElementById(elementId);
                if (!container) {
                    reject(new Error("容器" +elementId+ "不存在"));
                    return;
                }

                // 清空容器并创建新的Canvas
                container.innerHTML = '';
                if (type == 2) {
                    // const canvas = document.createElement('canvas');
                    const canvas = await this.generateCombinedCanvas(
                        'https://lihuanting.com/upload/1617020718890.png', // Logo URL
                        '公司名称',                      // 标题
                        content,    // 二维码内容
                        '扫描二维码访问网站\n2023年10月1日' // 底部文本(多行)
                    );

                    console.log('canvas: ', canvas);
                    container.appendChild(canvas);
                    resolve();
                } else {
                    const canvas = document.createElement('canvas');
                    container.appendChild(canvas);
                    // 生成二维码
                    QRCode.toCanvas(canvas, content, {
                        width: this.qrOptions.width,
                        margin: 1,
                        color: this.qrOptions.color
                    }, (error) => {
                        if (error) {
                            reject(error);
                        } else {
                            resolve();
                        }
                    });
                }

            });
        },
        async generateCombinedCanvas(logoUrl, title, qrContent, bottomText) {
            console.log('logoUrl, title, qrContent, bottomText: ', logoUrl, title, qrContent, bottomText);
            // 创建Canvas
            const canvas = document.createElement('canvas');
            const ctx = canvas.getContext('2d');

            // 设置尺寸(可根据需要调整)
            const width = 500;
            const logoSize = 80;
            const qrSize = 100;
            const padding = 20;

            // 计算总高度
            const height = padding * 4 + logoSize + qrSize + 60; // 额外空间用于文本
            canvas.width = width;
            canvas.height = height;
            console.log('height: ', height);
            console.log('width: ', width);

            // 清空背景
            ctx.fillStyle = '#ffffff';
            ctx.fillRect(0, 0, width, height);

            // 1. 绘制Logo(圆形裁剪)
            try {
                const logoImg = await this.loadImage(logoUrl);
                const logoX = (width - logoSize) / 2;
                const logoY = padding;
                // // 绘制Logo
                ctx.drawImage(logoImg, logoX, logoY, logoSize, logoSize);
            } catch (error) {
                console.log('error: ', error);
                // Logo加载失败时绘制默认图标
                const logoX = (width - logoSize) / 2;
                const logoY = padding;

                ctx.fillStyle = '#eeeeee';
                ctx.beginPath();
                ctx.arc(logoX + logoSize / 2, logoY + logoSize / 2, logoSize / 2, 0, Math.PI * 2);
                ctx.fill();

                ctx.fillStyle = '#999999';
                ctx.font = '16px Arial';
                ctx.textAlign = 'center';
                ctx.fillText('LOGO', width / 2, padding + logoSize / 2 + 6);
            }

            // 2. 绘制标题文本
            ctx.font = 'bold 20px Arial';
            ctx.fillStyle = '#333333';
            ctx.textAlign = 'center';
            ctx.fillText(title, width / 2, padding * 2 + logoSize);

            // 3. 生成并绘制二维码
            const qrX = (width - qrSize) / 2;
            const qrY = padding * 3 + logoSize + 20;

            let qrCodeImage = await new Promise((resolve, reject) => {

                QRCode.toDataURL(qrContent, {
                    width: qrSize,

                }, (error, res) => {
                    console.log('error,res: ', error, res);
                    if (error) reject(error);

                    var img = new Image();
                    img.onload = function (e) {
                        console.log('e: ', e);
                        // 图像加载完成后,将其绘制到画布上
                        ctx.drawImage(img, qrX, qrY); // 这里可以指定绘制的位置和大小
                        resolve(res)
                    };
                    img.src = res; // 设置图像的源为数据URL

                })
            });
 

            // 4. 绘制底部文本
            ctx.font = '14px Arial';
            ctx.fillStyle = '#666666';

            // 支持多行文本(用\n分隔)
            const lines = bottomText.split('\n');
            lines.forEach((line, index) => {
                ctx.fillText(
                    line,
                    width / 2,
                    qrY + qrSize + padding + 20 * (index + 1)
                );
            });

            return canvas;
        },
        loadImage(url) {
            return new Promise((resolve, reject) => {
                const img = new Image();
                img.onload = () => resolve(img);
                img.onerror = reject;
                img.src = url;
            });
        },
        // 下载压缩包
        async downloadZip(type) {
            try {
                const zip = new JSZip();
                const folder = zip.folder(null);

                // 添加每个二维码到压缩包
                for (let i = 0; i < this.qrcodeImages.length; i++) {
                    const content = this.qrcodeImages[i];
                    let [equipmentNo, sequence] = content.split(":");
                    const qrcodeId = "qrcode-"+i;
                    const canvas = document.getElementById(qrcodeId).querySelector('canvas');

                    if (canvas) {
                        const imgData = canvas.toDataURL('image/png').split(',')[1];
                        const fileName = equipmentNo+" - "+sequence+".png";//二维码文件名:{桩编号} - {枪口号}
                        folder.file(fileName, imgData, { base64: true });

                    }
                }

                // 生成压缩包并下载
                const extFileName = type == 1 ? "Charging QR Code" : "Charging QR Code (with Details)";
                const zipName = this.sStationName+extFileName+".zip";//文件名:

                zip.generateAsync({ type: 'blob' }, (metadata) => {
                }).then((content) => {
                    saveAs(content, zipName);

                });
            } catch (error) {

                console.error('下载压缩包失败:', error);
            } finally {
                this.qrcodeImages = [];
            }
        },
    }
};
</script>
<style lang="less" scoped>
.downQRCodeBox {
    position: relative;

    .downQRCodeBody {
        width: 1px;
        height: 1px;
        position: absolute;
        right: 0;
        top: 0;
        background: red;
        z-index: 999999;
        overflow: hidden;
    }
}
</style>