复制粘贴那些事

需求

这篇公众号文章是用typora上写的,这是一款大名鼎鼎的客户端markdown编辑器。

Markdown的语法简洁明了、学习容易,而且功能比纯文本更强,因此有很多人用它写博客。世界上最流行的博客平台WordPress和大型CMS如Joomla、Drupal都能很好的支持Markdown。完全采用Markdown编辑器的博客平台有Ghost和Typecho。 除此之外,由于我们有了RStudio这样的神级编辑器,我们还可以快速将Markdown转化为演讲PPT、Word产品文档、LaTex论文甚至是用非常少量的代码完成最小可用原型。在数据科学领域,Markdown已经广泛使用,极大地推进了动态可重复性研究的历史进程。

有了markdown,我彻底抛弃了word。

但是markdown的图片正常来说需要联网。尤其是公开发布的文档。联网时必备的条件。

市面上也有很多markdown编辑软件。我也在寻求好用的markdown编辑器。

我尝试过保存到为知笔记/网易云笔记/印象笔记。但是体验其实都非常差。尤其是我这种长篇累牍都文章。到最后都非常卡。

不知你是否用过博客园的markdown编辑器?印象最深的相信就是"丑陋"吧。但是博客园有一个很好的功能,就是使用微信或qq截图之后,ctrl+v,就自动生成了markdown格式的链接地址。这是其他笔记应用没有的。

![](https://img2018.cnblogs.com/blog/1011161/201908/1011161-20190814204338058-791418648.png)

这段markdown代码在经过渲染后就是一张图了。

我很喜欢如是这般把博客园作为自己的图床。直到某天外网访问它403了。

现在有了服务器,还写了一款markdown编辑器用于发布文档。

就自己实现一个图床吧!

项目基于vue,不过为了阐述方便,尽可能原生语法。

编辑器复制粘贴,起码发生以下事情:

  • 获取文件对象
  • 前端压缩图片文件算法
  • 服务器配置七牛cdn
  • 返回文件地址

获取文件对象

Clipboard API的Clipboard接口提供了一种读写操作系统剪贴板的方式。

而剪切板事件(copy/paste/cut)是这里涉及的主要的方法。

获取剪切板的内容可以用一个全局api来拿:window.setSelection().toString()

首先是要监听你的paste事件:

假设div##markdownContent是你要粘贴到区域:

// 二次开发内容
let markdownContent= document.querySelector('#markdownContent');
document.addEventListener('paste', function (event) {
    var items = event.clipboardData && event.clipboardData.items;
    var file = null;
    if (items && items.length) {
        // 检索剪切板items
        for (var i = 0; i < items.length; i++) {
            if (items[i].type.indexOf('image') !== -1) {
                file = items[i].getAsFile();
                break;
            }
        }
    }

      if(file){
      // 此时file就是剪切板中的图片文件
        console.log('file',file);

      // ...
    }

});

图片处理

七牛提供了上传压缩的服务。这个服务是付费的。当甲方比较穷,可以考虑在前端把它转小了再传。

原理是拿到file对象之后,new一个FileReader,以base64的形式读取。

const reader = new FileReader();
reader.readAsDataURL(file);

然而reader加载读取是需要时间的。为了优雅的编码。封装一个方法:

const getBase64 = () => {
    return new Promise((resolve, reject) => {
        reader.addEventListener('load', (e) => {
            resolve(e.target.result);
        });
    });
};

接下来就很舒服了。

let base64 = await getBase64();

压缩

base64是不会压缩图像质量的。但base64是canvas对象很喜欢的格式。

总的思路就是,把你粘贴的图片按照一定的比例,改为最小尺寸。

获取真实宽高

我如果拷贝一个千万级像素的大图。面对一堆base64编码,我又如何知道它的宽高?这时你需要构造一个dom。把它放进去。然后趁它加载完成后,拿下来。

所以接下来的做法和上面如出一辙:

const getProps = (img) => (new Promise((resolve, reject) => {
    img.onload = () => {
        resolve({
            width: img.width,
            height: img.height,
            rate: 0.01 * Math.round(100 * img.width / img.height)
        })
    }
}));
// 调用
let base64 = await getBase64();
let img = new Image();
img.src = base64;

// 计算比例 根据 宽/高 校正大小
const { rate, width, height } = await getProps(img);

很简单,很简单。

压缩

压缩的业务需求是这样的:

给定一个宽高极限maxWidthmaxHeight,如果图片没超过这个极限,就用原图无所谓。但超过了。就得执行一系列计算。

// 初始化canvas
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');

// canvas宽高配置。可以从其他文件引入
const canvasConfig = {
    maxWidth: 1200,
    maxHeight: 600
}
const { maxWidth, maxHeight } = canvasConfig;
const canvas_rate = canvasConfig.maxWidth / canvasConfig.maxHeight;

// 解比例
if(width<maxWidth&&height<maxHeight){
    canvas.width = width;
    canvas.height = height;
}else{
    canvas.width = rate > canvas_rate ? maxWidth : maxHeight * rate;
    canvas.height = rate > canvas_rate ? maxWidth / rate : maxHeight;
}

// 核心JS就这个
context.drawImage(img, 0, 0, canvas.width, canvas.height);
let dataURL=canvas.toDataURL('image/jpeg');

这个计算就是解比例。对笔者来说小学六年级难度。听说现在小孩小学二年级奥数就学了。

进入上传流程

canvas最后给到你的也是base64。但传一个blob对后端来说是很友的选择。因此把它转化为blob对象吧

// 转换base64为二进制文件
function dataURLtoBlob(data) {
    var tmp = data.split(',');
    tmp[1] = tmp[1].replace(/\s/g,'');
    var binary = atob(tmp[1]);
    var array = [];
    for(var i = 0; i < binary.length; i++) {
        array.push(binary.charCodeAt(i));
    }
    return new Blob([new Uint8Array(array)], {type: 'image/jpeg'});
}

//上传
let blob=dataURLtoBlob(dataURL);
let fd = new FormData();
fd.append("file", blob);
let res = await this.$post(this.$API.uploadImg, fd});

// res...balabala

你会发现这个dataURLtoBlob和我代码风格不大一样。因为是我复制来的。涉及的非前端api,看不来,这回就当一回API调用工程师吧。

加水印

实际上你可以告诉用户,你的图片是有版权的。说白了也就是加水印。

// 添加水印
context.font = "28px Georgia";
context.fillText('♥️一粒小麦 djtao.net', 30, 60);

可以看到效果:

详情将在canvas一文中讲述。

后端处理

如果直接发送到七牛。那后端配合的就是发送一个token。做的事情简单的令人发指。

七牛有一个nodejs的token生成器。你要做的,就是在进入页面时,请求token生成器。

sudo npm install qiniu -s
// 主要是图片上传
const qiniu = require('qiniu')
export const getQiniuToken = async (ctx, next) => {
      // ak和sk可以在七牛个人中心拿
    var accessKey = '。。。';
    var secretKey = '。。。';
    var mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
    var options = {
        scope: 'storage',//你的空间名
    };
    var putPolicy = new qiniu.rs.PutPolicy(options);
    // 上传凭证
    var uploadToken = putPolicy.uploadToken(mac);
    ctx.body = setResponseData(SUCCESS_CODE, SUCCESS_MSG, uploadToken);
}

看到进入页面,就直接拿到token了。

上传

所以直接回到前端。

//上传
let blob = dataURLtoBlob(dataURL);
let fd = new FormData();
fd.append("file", blob);
fd.append('token',this.token)
let res = await axios.post('http://up-z2.qiniup.com/', fd);
let url='http://markdown.djtao.net/'+res.data.key;

你的空间在哪个区域的机房,就post哪个地址。

那么你就拿到url啦!

组装粘贴内容

还记得那串markdown源码吗?组装一下:

const pasteContnet=`![](${url})`

在markdown编辑器里怎么使用?

两行代码解决:

var clipboardData = event.clipboardData || window.clipboardData;
event.target.value=event.target.value+'\n'+pasteContnet;

最后我们来测试一下。

测试图片地址:

原图:

上传后:

其中转换前尺寸3984*3656,大小为2.5M 转换后尺寸为900X600,大小为193k。

需求完成。

原文发布于微信公众号 - 一Li小麦(gh_c88159ec1309)

原文发表时间:2019-08-15

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券