【Pix】在客户端生成分享图

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

这里通过录屏可以看到,该图由服务端生成并回传了一段 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 还是会更加方便,毕竟只需要加入一个标签即可 🤔

Comments | 3 条评论
  • Evan

    介个是不是只能分享片刻,文章还是不行?哈哈哈哈哈。。我现在就是这样。字体也有些小了。哈哈

    • xiamo

      @Evan 嗯,文章分享我没写,其实最好还是用主题原有的

消息盒子
# 您需要首次评论以获取消息 #
# 您需要首次评论以获取消息 #

只显示最新10条未读和已读信息