一、现状
个人目前所有笔记、文章都是在自建站点 昆仑虚 上进行统一管理
昆仑虚 编辑器是 Markdown
格式的, 对于 掘金
等大部分平台, 编辑文章、发布文章都是支持 Markdown
格式, 所以平常我都是直接将全文内容直接复制过去, 直接发布即可!!!
但是呢? 如果我想将 昆仑虚 上的文章发布到微信公众号就麻烦了, 鬼知道为什么微信公众号编辑器为啥不支持 Markdown
格式, 目前的做法就是将 Markdown
内容复制到 墨滴(一个 Markdown 转微信公众号平台) 上, 将其转为微信公众号编辑器支持的格式!!!
二、目标
那么本次目标或者说是需求, 就是需要编辑器上增加一个按钮, 点击按钮自动将 Markdown
内容转为微信公众号编辑器支持的格式, 并将内容添加到粘贴板...
三、解析
3.1 确认格式
首先需要先确认下, 微信公众号编辑器支持的到底是啥格式的内容? 又或者说在 墨滴编辑器 上我们复制出来的内容格式是啥? 这样才能有解决思路...
登录 墨滴编辑器 随便写点内容, 尝试进行复制....
将复制内容, 黏贴到编辑器中, 发现原来复制的居然是 html
字符串, 当然这个结果其实也是情理之中...
但是, 事情真的这么简单吗? 当然不是啦!!! 如下所示我们如果直接将一段 html
字符串复制到微信公众号编辑器, 它并没有像预期中一样展示, 而是展示了 html
原文
<h1 style="color: #bae637;">标题</h1>
<p style="color: #4096ff;">正文内容</p>
揭秘揭秘揭秘揭秘!!! 经过 google
最终确定, 微信公众号
支持的内容是 text/html
格式的数据流, 也就是如果我们只是手动复制一段 html
到 微信公众号
是不行的, 因为它的数据格式并不是 text/html
如下代码, 通过 navigator.clipboard.write
将 html
文本, 以 text/plain
以及 text/html
的格式写入剪切板, 同时通过 $0.addEventListener
将方法绑定到当前选中的第一个节点的 click
事件上; 这里之所以通过事件方式触发 navigator.clipboard.write
是因为为了安全 navigator.clipboard
只能够通过 用户手动
触发, 才能生效!!!
const clipboard = async () => {
const html = `
<h1 style="color: #bae637;">标题</h1>
<p style="color: #4096ff;">正文内容</p>
`;
await navigator.clipboard.write([
new window.ClipboardItem({
// text/html 格式数据, 为了能够将内容复制到微信公众号
'text/html': new Blob([html], { type: 'text/html' }),
// text/plain 格式数据, 为了能够将内容作为文本复制到编辑器
'text/plain': new Blob([html], { type: 'text/plain' }),
}),
]);
window.alert('已复制到剪贴板');
};
// chrome 调用, $0 表示当前选中的 html 节点
$0.addEventListener('click', clipboard);
测试发现, 微信公众号编辑器确实可以正常解析我们复制的内容咯
3.2 获取完整 html
下面我们只剩下一个难点, 那就是如何获取 Markdown
预览后的全部 html
内容
当然这里你也许会觉着, 这不是很简单吗? 当点击复制按钮时, 直接读取预览界面对应 dom
节点的 innerHtml
不就好了!!!
事情哪有这么简单的呢, 我们还是以 墨滴编辑器 来演示, 我们尝试获取下读取预览界面对应 dom
节点的 innerHtml
, 会发现和我们复制到的 html
内容还是有很大区别的, 复制到的 html
是带有每个节点的所以样式属性的(style
属性), 但是呢 innerHtml
只是单纯获取节点的 html
不会带有外联的 CSS
样式!!!
那么根本问题回到, 我们要如何根据一个指定 DOM
节点, 获取到一个完整的 html
, 同时需要将每个节点的所有 CSS
属性, 转为内联的形式!!!
最后经过一顿 google
, 终于让我找到一个 npm
包 juice, 通过它我们就可以将给定的 HTML
中的 CSS
属性全部内联到 style
属性中...
// 有代码如下:
var juice = require('juice');
var result = juice("<style>div{color:red;}</style><div/>");
// 最后结果:
<div style="color: red;"></div>
3.3 另一种方式
在翻资料时, 还找到另一种实现方式, 如下代码直接选中所有 DOM
节点并执行 execCommand
进行拷贝, 这样也能够拿到所有带内联样式的 html
, 这里可以参考: Markdown-微信 格式转换器、Markdown-Weixin
const clipboardDiv = document.getElementById('#output')
clipboardDiv.focus()
window.getSelection().removeAllRanges()
const range = document.createRange()
range.setStartBefore(clipboardDiv.firstChild)
range.setEndAfter(clipboardDiv.lastChild)
window.getSelection().addRange(range)
if (document.execCommand('copy')) {
window.alert('已复制到剪贴板')
} else {
window.alert('未能复制到剪贴板,请全选后右键复制')
}
但是这样拷贝就不太可控, 无法对复制的节点进行一些修改(或者说实现起来比较麻烦)
四、正式开始
4.1 加按钮
首先呢先给 Markdown
预览页面加个小按钮
export default () => {
const handleCopy = useCallback(() => {}, [])
return (
<Icon
title="复制代码"
type="icon-copy"
onClick={handleCopy}
/>
)
}
4.2 获取所有 CSS 规则
下面我们需要获取到所有 CSS
规则, 这样才能借用 juice 将 html
转为带有内联样式的 html
字符串
这里我们可通过原生 API
document.styleSheets, 获取到当前页面中所有 CSS
规则; 如下代码所示: 通过 document.styleSheets
获取到所有 CSS
规则, 并合并为字符串
// 返回当前页面所以样式规则: .body{...} .main{...}
export default () => [...document.styleSheets].reduce((total, styleSheet) => {
const list = [...styleSheet.cssRules].map((rule) => {
// 过滤掉以 .ant 开头的规则: 因为预览模块没用到 antd 的组件, 所以干脆过滤掉 antd 的样式
// 过滤掉以 :root 开头的规则: 因为 juice 暂不支持 CSS 变量, 所以就把 :root 上的规则过滤掉(大部分都是 CSS 变量定义)
if (/^(\.ant|:root)/.test(rule.cssText)) {
return '';
}
return rule.cssText;
});
return [...total, ...list];
}, []).join('\n');
4.3 转换并复制
拿到 CSS
后接下来就可以尝试将要复制的 Dom
节点的 html
进行一个转换
const handleCopy = useCallback(async () => {
const sourceHtml = previewRef.current.outerHTML;
const css = getAllCSS();
const options = {
removeStyleTags: true, // 移除 Style 标签(暂时无法生效, 已有人提交 issues: https://github.com/Automattic/juice/issues/470)
inlinePseudoElements: true, // 是否将伪元素转为 span 标签
};
const transform = juice(`<style>${css}</style>${sourceHtml}`, options);
const resHtml = transform.replace(/<style>[\s\S]*?<\/style>/, ''); // 先手动移除 Style
// 复制: 将文本内容以 text/html、text/plain 格式写入剪切板
await navigator.clipboard.write([
new window.ClipboardItem({
'text/html': new Blob([resHtml], { type: 'text/html' }),
'text/plain': new Blob([resHtml], { type: 'text/plain' }),
}),
]);
window.alert('复制成功!!!');
}, []);
下面是测试结果, 基本上能够完成正常的复制了, 但是对于代码块部分, 还是出现样式丢失问题!!!
对比原页面 html
、juice
转换出来的 html
以及复制到微信公众编辑器的 html
, 发现 juice
转换出来的并没有问题, 主要问题出在将 html
复制到微信公众号编辑器时, 被转换了!!!
经测试, 姑且认为微信公众号编辑器不支持 div
标签, 会默认转为 p
标签, 同时经测试对于嵌套的 p
标签也是不支持的!! 最后发现对于 section
微信公众号编辑器是支持的, 并且还能够被嵌套使用! 那就很简单咯, 只要将所有 div
标签换成 section
即可, 修改后代码如下:
const handleCopy = useCallback(async () => {
const sourceHtml = previewRef.current.outerHTML;
const css = getAllCSS();
const options = {
removeStyleTags: true, // 移除 Style 标签(暂时无法生效, 已有人提交 issues: https://github.com/Automattic/juice/issues/470)
inlinePseudoElements: true, // 是否将伪元素转为 span 标签
};
const transform = juice(`<style>${css}</style>${sourceHtml}`, options);
+ const resHtml = transform
+ .replace(/<style>[\s\S]*?<\/style>/, '') // 先手动移除 Style
+ .replace(/(?<=\/?)div(?=[\s>])/ig, 'section'); // div 标签转为 section
// 复制: 将文本内容以 text/html、text/plain 格式写入剪切板
await navigator.clipboard.write([
new window.ClipboardItem({
'text/html': new Blob([resHtml], { type: 'text/html' }),
'text/plain': new Blob([resHtml], { type: 'text/plain' }),
}),
]);
window.alert('复制成功!!!');
}, []);
最下来看下修改后的效果吧
五、样式优化
到这一步, 想要的需求基本就已经完成, 下面剩余一些收尾工作可能就是:
- 交互优化: 目前复制成功的提示只是简单
alert
出提示语, 这里需要改掉
- 样式优化: 需要针对微信工作号, 定制一些特定的样式, 比如暗色模式下样式的兼容
const COPY_CUSTOM_STYLE = `
<style>
@media (prefers-color-scheme: light) {
}
@media (prefers-color-scheme: dark) {
.brick-markdown-preview-light * {
color: rgba(255, 255, 255, 0.85);
}
.brick-markdown-preview-light blockquote {
background: #3b3456;
border-color: #241f3a;
}
}
</style>
`;
const handleCopy = useCallback(async () => {
const sourceHtml = previewRef.current.outerHTML;
const css = getAllCSS();
const options = {
removeStyleTags: true, // 移除 Style 标签(暂时无法生效, 已有人提交 issues: https://github.com/Automattic/juice/issues/470)
inlinePseudoElements: true, // 是否将伪元素转为 span 标签
};
const transform = juice(`<style>${css}</style>${sourceHtml}`, options);
const replaceHtml = transform
.replace(/<style>[\s\S]*?<\/style>/, '') // 先手动移除 Style
.replace(/(?<=\/?)div(?=[\s>])/ig, 'section'); // div 标签转为 section
const copyContent = [`${COPY_CUSTOM_STYLE}${replaceHtml}`];
// 复制: 将文本内容以 text/html、text/plain 格式写入剪切板
await navigator.clipboard.write([
new window.ClipboardItem({
'text/html': new Blob([copyContent], { type: 'text/html' }),
'text/plain': new Blob([copyContent], { type: 'text/plain' }),
}),
]);
window.alert('复制成功!!!');
}, []);
七、参考
大家好, 我是墨渊君, 如果您喜欢我的文章可以:
- 关注公众号: 「昆仑虚F2E」获取最新文章。
- GitHub: github.com/MoYuanJun
- 个人网站(昆仑虚, 虽然现在没啥东西): www.kunlunxu.cc