惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Recent Announcements
Recent Announcements
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
B
Blog
T
The Blog of Author Tim Ferriss
J
Java Code Geeks
腾讯CDC
D
Docker
G
Google Developers Blog
D
DataBreaches.Net
雷峰网
雷峰网
Blog — PlanetScale
Blog — PlanetScale
S
SegmentFault 最新的问题
The Cloudflare Blog
有赞技术团队
有赞技术团队
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Stack Overflow Blog
Stack Overflow Blog
大猫的无限游戏
大猫的无限游戏
量子位
美团技术团队
aimingoo的专栏
aimingoo的专栏
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Engineering at Meta
Engineering at Meta
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

Another

2026 🤔 xiamo’s 2025 AceXiamo’s 2024 我在做什么 聊一聊最近 ✏️ 【📝】抖音直播 & Mac 🛠️ 记一次微信聊天记录恢复 一些我比较喜欢的工具 🛠 闲话
【Pix】在客户端生成分享图
xiamo · 2024-02-26 · via Another

水一篇文章 😤
看到群友说分享图无法生成,闲来无事于是就自己动手敲了个前端实现

这里通过录屏可以看到,该图由服务端生成并回传了一段 base64 给前端

实际测试,该操作还是会占用一部分的 CPU 资源,假如修改成前端生成,那么依赖的则只是客户端机器

So,JS 源代码如下

(() => {
  const addScript = (url) => {
    const scriptDom = document.createElement("script");
    scriptDom.src = url;
    document.head.appendChild(scriptDom);
  };
  addScript("https://file.qwq.link/js/html2canvas.min.js");
  addScript("https://file.qwq.link/js/qrcode.min.js");

  let container,
    topDiv,
    topImgContainer,
    topImg,
    topImgFilter,
    dateContainer,
    date,
    yearMonth,
    title,
    description,
    bottomDiv,
    leftBotDiv,
    logo,
    logoDesc,
    qrCodeDiv;
  let siteLogo, siteTitle, siteDesc;
  const DEFAULT_COVER = "/wp-content/themes/pix/img/banner.jpg";
  let fontSize = {};

  const cardDomInit = () => {
    container = document.createElement("div");
    container.style.display = "flex";
    container.style.flexDirection = "column";
    container.style.backgroundColor = "white";
    container.style.height = "100vh";
    container.style.aspectRatio = "624/1035";
    container.style.boxSizing = "border-box";
    container.style.padding = "15px";

    topDiv = document.createElement("div");
    container.appendChild(topDiv);
    topDiv.style.height = "80%";
    topDiv.style.display = "flex";
    topDiv.style.flexDirection = "column";
    topDiv.style.justifyContent = "flex-end";
    topDiv.style.position = "relative";
    topDiv.style.padding = "30px";
    topDiv.style.gap = "50px";

    topImgContainer = document.createElement("div");
    topDiv.appendChild(topImgContainer);
    topImgContainer.style.position = "absolute";
    topImgContainer.style.inset = "0";
    topImgContainer.style.textAlign = "center";

    topImg = document.createElement("div");
    topImgContainer.appendChild(topImg);
    topImg.style.backgroundSize = "cover";
    topImg.style.backgroundPosition = "center";
    topImg.style.position = "absolute";
    topImg.style.inset = "0";

    topImgFilter = document.createElement("div");
    topImgContainer.appendChild(topImgFilter);
    topImgFilter.style.position = "absolute";
    topImgFilter.style.inset = "0";
    topImgFilter.style.backgroundColor = "#00000050";

    dateContainer = document.createElement("div");
    topDiv.appendChild(dateContainer);
    dateContainer.style.position = "absolute";
    dateContainer.style.right = "20px";
    dateContainer.style.top = "20px";
    dateContainer.style.display = "flex";
    dateContainer.style.flexDirection = "column";
    dateContainer.style.color = "white";

    date = document.createElement("span");
    dateContainer.appendChild(date);
    date.style.fontWeight = "800";
    date.style.color = "white";

    yearMonth = document.createElement("span");
    dateContainer.appendChild(yearMonth);
    yearMonth.style.color = "white";
    yearMonth.style.marginLeft = "auto";

    title = document.createElement("span");
    topDiv.appendChild(title);
    title.style.position = "relative";
    title.style.fontWeight = "600";
    title.style.color = "white";

    description = document.createElement("div");
    topDiv.appendChild(description);
    description.style.position = "relative";
    description.style.fontWeight = "400";
    description.style.color = "#EBEFFE";
    description.style.minHeight = "180px";

    bottomDiv = document.createElement("div");
    container.appendChild(bottomDiv);
    bottomDiv.style.display = "flex";
    bottomDiv.style.height = "0";
    bottomDiv.style.padding = "20px";
    bottomDiv.style.flexGrow = "1";

    leftBotDiv = document.createElement("div");
    bottomDiv.appendChild(leftBotDiv);
    leftBotDiv.style.display = "flex";
    leftBotDiv.style.flexDirection = "column";
    leftBotDiv.style.justifyContent = "center";
    leftBotDiv.style.gap = "10px";
    leftBotDiv.style.flexGrow = "1";

    logo = document.createElement("img");
    leftBotDiv.appendChild(logo);
    logo.style.maxHeight = "70px";
    logo.style.maxWidth = "200px";
    logo.style.objectFit = "cover";

    logoDesc = document.createElement("span");
    leftBotDiv.appendChild(logoDesc);
    logoDesc.style.color = "#B4B4B4";

    qrCodeDiv = document.createElement("div");
    bottomDiv.appendChild(qrCodeDiv);
    qrCodeDiv.style.height = "100%";
    qrCodeDiv.style.aspectRatio = "1/1";
  };

  const loadSiteInfo = () => {
    siteLogo = document.querySelector(".top_logo a img")?.src;
    siteTitle = document.querySelector("title").innerText.split("-")[0];
    siteDesc = document.querySelector(
      ".index_banner .user_info .des"
    )?.innerText;
  };

  const withContent = ({
    dateOfDay,
    dateOfYearAndMonth,
    cover,
    cardTitle,
    content,
    siteLogo,
    siteDesc,
  }) => {
    topImg.style.backgroundImage = url(${cover});
    date.textContent = dateOfDay;
    yearMonth.textContent = dateOfYearAndMonth;
    title.textContent = cardTitle;
    description.textContent = content;
    logo.src = siteLogo;
    logoDesc.textContent = siteDesc;
  };

  const debounce = (fn, delay) => {
    let timer = null;
    return function () {
      if (timer) {
        clearTimeout(timer);
      }
      timer = setTimeout(() => {
        fn.apply(this, arguments);
      }, delay);
    };
  };

  const base64ForCard = (data) => {
    return new Promise((resolve) => {
      withContent(data);
      container.style.position = "absolute";
      container.style.left = "-9999px";
      document.body.appendChild(container);
      qrCodeDiv.innerHTML = "";
      new QRCode(qrCodeDiv, {
        text: data.link,
        height: qrCodeDiv.clientHeight,
        width: qrCodeDiv.clientWidth,
      });
      html2canvas(container, {
        logging: true,
        letterRendering: 1,
        allowTaint: true,
        useCORS: true,
      }).then((canvas) => {
        document.body.removeChild(container);
        resolve(canvas.toDataURL("image/png"));
      });
    });
  };

  const dataFromPost = (postId) => {
    const posterBoxAp = document.querySelector(
      #share_modal_${postId} .poster_box_ap
    );
    const postShareBox = document.querySelector(
      #share_modal_${postId} .post_share_box
    );
    const images = document.querySelectorAll(
      #post-${postId} .img_list .list_inner a img
    );
    let url = DEFAULT_COVER;
    if (images != null && images.length > 0) {
      const urlFromDom = images[0].src;
      urlFromDom.indexOf(location.origin) !== -1 && (url = urlFromDom);
    }
    const contentDom = document.querySelector(
      #post-${postId} .entry-content .t_content p
    );
    const linkDom = document.querySelector(
      #post-${postId} .entry-content .p_title a
    );
    const timeDom = document.querySelector(
      #post-${postId} .list_user_meta .name time
    );
    const date = new Date(timeDom.getAttribute("datetime"));

    return {
      posterBoxAp,
      postShareBox,
      data: {
        dateOfDay: date.getDate(),
        dateOfYearAndMonth: ${date.getFullYear()}/${date.getMonth() + 1},
        cover: url,
        cardTitle: ${siteTitle} - 片刻,
        content: contentDom.innerText,
        link: linkDom.href,
        siteLogo,
        siteDesc,
      },
    };
  };

  const SayCardInit = () => {
    document.querySelectorAll(".pix_share_btn").forEach((item) => {
      item.onclick = null;
      for (let i = 0; i < item.children.length; i++) {
        item.children[i].onclick = null;
      }
      const originalA = item.children[0];
      const postId = originalA.getAttribute("poster-data");
      originalA.classList.remove("cr_poster");
      item.onclick = () => {
        const { posterBoxAp, postShareBox, data } = dataFromPost(postId);
        base64ForCard(data).then((base64) => {
          const div = document.createElement("div");
          div.classList.add("poster_box");
          div.style.border = "1px solid rgb(240, 240, 240)";
          const img = new Image();
          img.crossOrigin = "Anonymous";
          img.src = base64;
          div.appendChild(img);
          posterBoxAp.innerHTML = "";
          posterBoxAp.appendChild(div);
          postShareBox.classList.remove("hide");
          const last = postShareBox.lastElementChild;
          last.href = base64;
          last.setAttribute("download", poster_${postId}.png);
        });
      };
    });
  };
  const windowSizeHanlder = () => {
    if (window.innerWidth < 800) {
      fontSize = {
        date: "40px",
        yearMonth: "12px",
        title: "20px",
        description: "14px",
        logoDesc: "16px",
      };
    } else {
      fontSize = {
        date: "70px",
        yearMonth: "18px",
        title: "25px",
        description: "18px",
        logoDesc: "20px",
      };
    }
    date.style.fontSize = fontSize.date;
    yearMonth.style.fontSize = fontSize.yearMonth;
    title.style.fontSize = fontSize.title;
    description.style.fontSize = fontSize.description;
    logoDesc.style.fontSize = fontSize.logoDesc;
  };
  cardDomInit();
  window.addEventListener("DOMContentLoaded", () => {
    windowSizeHanlder();
    loadSiteInfo();
    SayCardInit();
    const postItem = document.querySelector("#post_item");
    postItem?.addEventListener("DOMNodeInserted", () => {
      debounce(() => {
        SayCardInit();
      }, 50)();
    });
  });
})();

不过用下来,我觉得体验不是很好,首先是没有动画,当然,多折腾一下就好了,但是我比较懒 🫠

实现上,我觉得这件事需要配合编辑 PHP 来完成,仅仅靠 JS 能做到,但是不是很优雅,比如代码中我通过 JS 移除了原有的 click 监听以及一段会导致请求调用的 attr,然后重新加入了一个新的 click 监听...
不过对于使用者来说,仅 JS 还是会更加方便,毕竟只需要加入一个标签即可 🤔