背景
最近接触到的需求,前端生成一个带企业logo的二维码,并支持点击下载它。
实现
在前面的文章有讲到如何用 canvas 画二维码,在此基础上再画一个公司logo,并提供下载的方法供调用,再封装成 npm 插件
模块名称: qrcode-with-logos
github地址:github.com/zxpsuper/qr…
npm地址:www.npmjs.com/package/qrc…
由于简书不允许文章里有二维码,所以特把二维码打了马赛克
核心代码
将整个封装成一个 QrCodeWithLogo
类,并提供三个方法:
<pre class="prism-token token language-javascript" style="box-sizing: border-box; list-style: inherit; margin: 0.5em 0px; padding: 1em; color: rgb(204, 204, 204); background: rgb(80, 85, 107); border-radius: 3px; overflow: auto; font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; overflow-wrap: normal; text-align: left; white-space: pre; word-spacing: 0px; word-break: normal; line-height: 1.5; tab-size: 4; hyphens: none; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">interface IQrCodeWithLogo {
toCanvas(): Promise<any>;
toImage(): Promise<any>;
downloadImage(name: string): void;
}
class QrCodeWithLogo implements IQrCodeWithLogo {
option: BaseOptions;
constructor(option: BaseOptions) {
this.option = option;
return this;
}
toCanvas = () => {
return toCanvas.call(this, this.option);
};
toImage = () => {
return toImage.call(this, this.option);
};
downloadImage = (name: string) => {
saveImage(this.option.image, name);
};
}
1. toCanvas()
此方法用到了库qrcode
的toCanvas
方法
<pre class="prism-token token language-javascript" style="box-sizing: border-box; list-style: inherit; margin: 0.5em 0px; padding: 1em; color: rgb(204, 204, 204); background: rgb(80, 85, 107); border-radius: 3px; overflow: auto; font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; overflow-wrap: normal; text-align: left; white-space: pre; word-spacing: 0px; word-break: normal; line-height: 1.5; tab-size: 4; hyphens: none; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">export const toCanvas = (options: BaseOptions) => {
return renderQrCode(options)
.then(() => options)
.then(drawLogo);
};
这里先用qrcode
库画出二维码的canvas
<pre class="prism-token token language-javascript" style="box-sizing: border-box; list-style: inherit; margin: 0.5em 0px; padding: 1em; color: rgb(204, 204, 204); background: rgb(80, 85, 107); border-radius: 3px; overflow: auto; font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; overflow-wrap: normal; text-align: left; white-space: pre; word-spacing: 0px; word-break: normal; line-height: 1.5; tab-size: 4; hyphens: none; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">import QRCode = require("qrcode");
const toCanvas = promisify(QRCode.toCanvas);
export const renderQrCode = ({
canvas,
content,
width = 0,
nodeQrCodeOptions = {}
}: BaseOptions) => {
// 容错率,默认对内容少的二维码采用高容错率,内容多的二维码采用低容错率
// according to the content length to choose different errorCorrectionLevel
nodeQrCodeOptions.errorCorrectionLevel =
nodeQrCodeOptions.errorCorrectionLevel || getErrorCorrectionLevel(content);
return getOriginWidth(content, nodeQrCodeOptions).then((_width: number) => {
// 得到原始比例后还原至设定值,再放大4倍以获取高清图
// Restore to the set value according to the original ratio, and then zoom in 4 times to get the HD image.
nodeQrCodeOptions.scale = width === 0 ? undefined : (width / _width) * 4;
// @ts-ignore
return toCanvas(canvas, content, nodeQrCodeOptions);
});
};
promisify()是封装的一个方法,用于减少return promise时的代码,方便书写
<pre class="prism-token token language-javascript" style="box-sizing: border-box; list-style: inherit; margin: 0.5em 0px; padding: 1em; color: rgb(204, 204, 204); background: rgb(80, 85, 107); border-radius: 3px; overflow: auto; font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; overflow-wrap: normal; text-align: left; white-space: pre; word-spacing: 0px; word-break: normal; line-height: 1.5; tab-size: 4; hyphens: none; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">export const promisify = (f: Function): Function => {
return function() {
const args = Array.prototype.slice.call(arguments);
return new Promise(function(resolve, reject) {
args.push(function(err: object, result: object) {
if (err) reject(err);
else resolve(result);
});
f.apply(null, args);
});
};
};
画出canvas,紧接着判断是否有logo, 如果有就画logo,这里有两种模式:
- 一种是直接画图
ctx.drawImage(image, logoXY, logoXY, logoWidth, logoWidth);
,可拓展性不强。 - 一种是canvas叠加,使用
ctx.createPattern(canvasImage, "no-repeat");
可以实现更多复杂的效果
<pre class="prism-token token language-javascript" style="box-sizing: border-box; list-style: inherit; margin: 0.5em 0px; padding: 1em; color: rgb(204, 204, 204); background: rgb(80, 85, 107); border-radius: 3px; overflow: auto; font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; overflow-wrap: normal; text-align: left; white-space: pre; word-spacing: 0px; word-break: normal; line-height: 1.5; tab-size: 4; hyphens: none; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">export const drawLogo = ({ canvas, content, logo }: BaseOptions) => {
if (!logo) {
return;
}
// @ts-ignore
const canvasWidth = canvas.width;
const {
logoSize = 0.15,
borderColor = "#ffffff",
bgColor = borderColor || "#ffffff",
borderSize = 0.05,
crossOrigin,
borderRadius = 8,
logoRadius = 0
} = logo;
let logoSrc = typeof logo === "string" ? logo : logo.src;
let logoWidth = canvasWidth * logoSize;
let logoXY = (canvasWidth * (1 - logoSize)) / 2;
let logoBgWidth = canvasWidth * (logoSize + borderSize);
let logoBgXY = (canvasWidth * (1 - logoSize - borderSize)) / 2;
// @ts-ignore
const ctx = canvas.getContext("2d");
// logo 底色, draw logo background color
canvasRoundRect(ctx)(
logoBgXY,
logoBgXY,
logoBgWidth,
logoBgWidth,
borderRadius
);
ctx.fillStyle = bgColor;
ctx.fill();
// logo
const image = new Image();
if (crossOrigin || logoRadius) {
image.setAttribute("crossOrigin", crossOrigin || "anonymous");
}
image.src = logoSrc;
// 使用image绘制可以避免某些跨域情况
// Use image drawing to avoid some cross-domain situations
const drawLogoWithImage = (image: any) => {
ctx.drawImage(image, logoXY, logoXY, logoWidth, logoWidth);
};
// 使用canvas绘制以获得更多的功能
// Use canvas to draw more features, such as borderRadius
const drawLogoWithCanvas = (image: any) => {
const canvasImage = document.createElement("canvas");
canvasImage.width = logoXY + logoWidth;
canvasImage.height = logoXY + logoWidth;
canvasImage
.getContext("2d")
.drawImage(image, logoXY, logoXY, logoWidth, logoWidth);
canvasRoundRect(ctx)(logoXY, logoXY, logoWidth, logoWidth, logoRadius);
ctx.fillStyle = ctx.createPattern(canvasImage, "no-repeat");
ctx.fill();
};
// 将 logo绘制到 canvas上
// Draw the logo on the canvas
return new Promise((resolve, reject) => {
image.onload = () => {
logoRadius ? drawLogoWithCanvas(image) : drawLogoWithImage(image);
resolve();
};
});
};
2. toImage()
此方法利用之前的toCanvas()
方法,生成canvas后拿到 canvas.toDataURL()
的值,赋给<img>
的 src
即可。这里,我们加入了 download
downloadName
属性用于下载,因此在 toImage()
方法中判断,代码如下:
<pre class="prism-token token language-javascript" style="box-sizing: border-box; list-style: inherit; margin: 0.5em 0px; padding: 1em; color: rgb(204, 204, 204); background: rgb(80, 85, 107); border-radius: 3px; overflow: auto; font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; overflow-wrap: normal; text-align: left; white-space: pre; word-spacing: 0px; word-break: normal; line-height: 1.5; tab-size: 4; hyphens: none; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">export const toImage = (options: BaseOptions) => {
const canvas = document.createElement("canvas");
console.log("options", options);
options.canvas = canvas;
if (options.logo) {
if (isString(options.logo)) {
// @ts-ignore
options.logo = { src: options.logo };
}
// @ts-ignore
options.logo.crossOrigin = "Anonymous";
}
// @ts-ignore
return toCanvas(options).then(() => {
const { image = new Image(), downloadName = "qr-code" } = options;
let { download } = options;
// @ts-ignore
image.src = canvas.toDataURL();
if (download !== true && !isFunction(download)) {
return;
}
download = download === true ? (start: Function) => start() : download;
const startDownload: Function = () => {
saveImage(image, downloadName);
};
download && download(startDownload);
return new Promise((resolve, reject) => {
resolve();
});
});
};
export const saveImage = (image: Element, name: string) => {
// @ts-ignore
const dataURL = image.src;
const link = document.createElement("a");
link.download = name;
link.href = dataURL;
link.dispatchEvent(new MouseEvent("click"));
};
3. downloadImage(name)
提供一个主动调用下载图片的方法,传入文件名name, 其中用到 saveImage()
方法,这个在 toImage()
中也有用到。
下载文件的流程是:生成一个<a>
标签, 设置 href
值为 image
的 src
值,download
属性赋值文件名,然后给 <a>
主动一次点击事件即可。
<pre class="prism-token token language-javascript" style="box-sizing: border-box; list-style: inherit; margin: 0.5em 0px; padding: 1em; color: rgb(204, 204, 204); background: rgb(80, 85, 107); border-radius: 3px; overflow: auto; font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; overflow-wrap: normal; text-align: left; white-space: pre; word-spacing: 0px; word-break: normal; line-height: 1.5; tab-size: 4; hyphens: none; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">downloadImage = (name: string) => {
saveImage(this.option.image, name);
};
npm 发布
初次尝试 typescript
, 用的不够优美但无妨使用。 npm run build
构建出支持 umd 的文件,然后 npm login
, npm publish
即可。webpack 配置可查看 github 代码。
插件使用
下面是详细代码
<pre class="prism-token token language-javascript" style="box-sizing: border-box; list-style: inherit; margin: 0.5em 0px; padding: 1em; color: rgb(204, 204, 204); background: rgb(80, 85, 107); border-radius: 3px; overflow: auto; font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; overflow-wrap: normal; text-align: left; white-space: pre; word-spacing: 0px; word-break: normal; line-height: 1.5; tab-size: 4; hyphens: none; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><canvas id="canvas"></canvas> <img src="" alt="" id="image" />
<img id="image" alt="">
npm 模块导入:
<pre class="prism-token token language-javascript" style="box-sizing: border-box; list-style: inherit; margin: 0.5em 0px; padding: 1em; color: rgb(204, 204, 204); background: rgb(80, 85, 107); border-radius: 3px; overflow: auto; font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; overflow-wrap: normal; text-align: left; white-space: pre; word-spacing: 0px; word-break: normal; line-height: 1.5; tab-size: 4; hyphens: none; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">import QrCodeWithLogo from "qrcode-with-logos";
let qrcode = new QrCodeWithLogo({
canvas: document.getElementById("canvas"),
content: "https://github.com/zxpsuper",
width: 380,
// download: true,
image: document.getElementById("image"),
logo: {
src: "https://avatars1.githubusercontent.com/u/28730619?s=460&v=4"
}
});
qrcode.toCanvas().then(() => {
qrcode.toImage().then(() => {
setTimeout(() => {
qrcode.downloadImage("hello world");
}, 2000);
});
});
当然你也可以<script>
引入使用
<pre class="prism-token token language-javascript" style="box-sizing: border-box; list-style: inherit; margin: 0.5em 0px; padding: 1em; color: rgb(204, 204, 204); background: rgb(80, 85, 107); border-radius: 3px; overflow: auto; font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; overflow-wrap: normal; text-align: left; white-space: pre; word-spacing: 0px; word-break: normal; line-height: 1.5; tab-size: 4; hyphens: none; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><script src="https://zxpsuper.github.io/qrcode-with-logos/dist/QRcode-with-logo.js"></script>
<script>
let qrcode = new QrCodeWithLogo({
canvas: document.getElementById("canvas"),
content: "https://github.com/zxpsuper",
width: 380,
// download: true,
image: document.getElementById("image"),
logo: {
src: "https://avatars1.githubusercontent.com/u/28730619?s=460&v=4"
}
});
qrcode.toCanvas().then(() => {
qrcode.toImage().then(() => {
setTimeout(() => {
qrcode.downloadImage("hello world");
}, 2000);
});
});
</script>
在vue项目中的使用
<template>
<div>
<canvas id="canvas" />
<img id="image" src="" alt="" style="display:none" crossOrigin="Anonymous">
<el-button @click="downloadImg">下载</el-button>
</div>
</template>
<script>
import QrCodeWithLogo from 'qrcode-with-logos'
export default {
name: 'Blank',
data() {
return {
logoData: {
logo:
'https://fuss10.elemecdn.com/8/27/f01c15bb73e1ef3793e64e6b7bbccjpeg.jpeg',//logo的地址
url: '[https://www.baidu.com/](https://www.baidu.com/)',//扫描图片访问的地址
name:'下载图片'//图片name
}
}
},
watch: {
logoData(newVal) {
this.setImg(newVal)
}
},
methods: {
// 二维码
setImg(obj) {
const qrcode = new QrCodeWithLogo({
canvas: document.getElementById('canvas'),
content: obj.url,
width: 200,
image: document.getElementById('image'),
logo: {
src: obj.logo
}
})
qrcode.toCanvas().then(() => {
qrcode.toImage().then(() => {})
})
this.qrcode = qrcode
},
// 点击下载二维码
downloadImg() {
this.qrcode.downloadImage(this.logoData.name)
},
}
</script>
<style lang="scss" scoped>
</style>
一定要在服务端设置允许跨域,否则会报跨域
如果一开始没有设置允许跨域,之后才设置的,那么谷歌浏览器会有缓存,所以会继续报跨域,浏览器是这样的
这时候需要将控制台的Network下的Disable cache选中,之后可以再打开,清理浏览器的缓存,不清理的话,浏览器一直请求的都是缓存的接口信息
清过后的浏览器接口是这样的
来自:https://cloud.tencent.com/developer/article/1532938
追加:
之前写完之后,一直有报跨域,今天终于把跨域问题解决了。报错如下:
事情是这样的:qrcode.toCanvas().then()请求了一下图片,然后呢qrcode.toImage().then(() => {})又请求了一次图片,,如图所示:
其实qrcode.toImage().then(() => {})请求图片的时候是请求的本地缓存,因为上面已经请求过一次这个图片了,所以再次请求的时候浏览器发现本地缓存有,就请求了本地的缓存,这时候就跨域了,这时候呢我们就不能让他请求本地缓存,所以可以在qrcode.toImage().then(() => {})请求的时候给图片请求地址后加一个随机数,这样就不会触发浏览器的缓存机制了,具体操作如下:
// 获取二维码
setImg(obj) {
//这个是给toCanvas用的
const qrcode = new QrCodeWithLogo({
canvas: document.getElementById('canvas'),
content: obj.url,
width: 200,
image: document.getElementById('image'),
logo: obj.src
? {
src: obj.src
}
: null
})
//这个是给toImage用的
const qrcodeImage = new QrCodeWithLogo({
canvas: document.getElementById('canvas'),
content: obj.url,
width: 200,
image: document.getElementById('image'),
logo: obj.src
? {
src: obj.src + '?' + new Date().getTime()//在这里给图片地址加个随机数
}
: null
})
qrcode.toCanvas().then(() => {
qrcodeImage.toImage().then(() => {})
})
this.qrcode = qrcode
},