Echarts生成canvas后转图片保存

Posted on 2021-08-29 17:12:37
Comments: 0
Author: 可乐小可爱メ
1.背景

业务场景是 动态邮件报表(含各种echart图标) 定期发送不同人物;

    1.1 难点是 如何动态生成图形化报表图片插入到 email发送.

2. 解决思路

     两个方向解决:

        方法A: node-echarts 服务端绘制生成 保存到本地

        方法B: 客户端生成 转化 插入到email

3. 初探

    方法A 首先被排除, 

    node-charts 包 当前状态 1.1.4 • Public • Published 

    并且实际操作后, 确实如一些帖子分享所说 相关依赖安装失败。 暂时放弃

    方法B 的思路, 是基于已经有的PC项目(已通过echarts 有了相关图表),保存相关图标的 canvas到image, 然后 image 到服务器本地 然后 插入Email.

4. 技术方案

 

核心点是 通过  puppeteer 模拟访问获取.

5. 代码实现

    5.1 Node Server

const Express = require("express");
const request = require("request");
const app = new Express();

app.get("/", (req, res) => {
res.send("20001 server.");
});
app.get("/api", (req, res) => {
const { query } = req;
if (!query || !query.memberId) {
return res.json({ code: 10010, msg: "Request Error" });
}
// 模拟请求 Business Server
request(
{
url: "http://java.server.com/api" + query,
method: "xxx",
},
(err, response, data) => {
// 模拟数据返回
const option = {
title: {
text: "ECharts 入门示例",
},
tooltip: {},
legend: {
data: ["销量"],
},
xAxis: {
data: ["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"],
},
yAxis: {},
series: [
{
name: "销量",
type: "bar",
data: [5, 20, 36, 10, 10, 20],
},
],
};
if (err) {
// return res.json({
// code: 20010,
// msg: "JAVA Server Error",
// });
return res.json(option);
}
return res.json(data);
}
);
});

app.use("/static", Express.static("static"));

app.listen("20001", () => {
console.log("listen on: http://127.0.0.1:20001");
});

    5.2 Puppeteer.js

const puppeteer = require("puppeteer");
const target = "http://127.0.0.1:20001/static/?memberId=123";
const userAgent =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36";
puppeteer
.launch({
executablePath:
"C:\Program Files\Google\Chrome\Application\chrome.exe",
headless: false,
})
.then(async (browser) => {
const page = await browser.newPage();
page.setUserAgent(userAgent);
await page.goto(target);
await page.waitForSelector("button[title="save-btn"]").then(() => {
page.click("button[title="save-btn"]");
setTimeout(() => {
browser.close();
}, 2000);
});
});

    5.3 静态Html index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>canvas to image</title>
</head>

<body>
<div id="main" style="width: 600px; height: 400px"></div>
<button id="save-btn" title="save-btn">save</button>
</body>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.1.2/dist/echarts.min.js"></script>
<script type="text/javascript">
const xhr = new XMLHttpRequest();
const canvasWrap = document.getElementById("main");
const btn = document.getElementById("save-btn");
const { search } = window.location;
xhr.open("GET", "http://127.0.0.1:20001/api" + search);
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
const option = JSON.parse(xhr.response);
const myChart = echarts.init(canvasWrap);
myChart.setOption(option);
}
};
xhr.send();

btn.onclick = function () {
const canvas = canvasWrap.getElementsByTagName("canvas")[0];
const fileName = "xxxxs.jpg";
const dataImg = new Image();
const imgData = canvas.toDataURL("image/jpg");
dataImg.src = imgData;
const blob = dataURLtoBlob(imgData);
const objurl = URL.createObjectURL(blob);
const alink = document.createElement("a");
alink.href = objurl;
alink.download = fileName;
alink.click();

function dataURLtoBlob(dataurl) {
const arr = dataurl.split(",");
const mime = arr[0].match(/:(.*?);/)[1];
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime });
}
};
</script>
</html>

    5.4 定时任务 crontab/node-schedule 请参考之前文章 Node.js 定时任务(node-schedule,pm2,shell脚本)

    5.5 业务接口(JAVA server)根据实际情况 返回当前要生成的 Echarts图标相关参数


6. 注意点

    6.1 以上是win环境代码, 实际到 centos后实测 需要修改 puppeteer.js 中 launch参数

.launch({
headless: false,
args: ["--no-sanbox"],
executablePath: "/usr/bin/chromium-browser",

})

 并且需要更换非root用户 启动puppeteer, 实际执行中 会报相关错误,


需要安装相关依赖 开启配置项。 CentOS解决xhost: unable to open display

    6.2 未完全解决问题    

    当前脚本执行通过log,确认没有报错, 执行完成。 但(centos中)找不到保存的文件




canvas 自定义图片鼠标事件(放大缩小拖动)

Posted on 2021-06-25 14:47:09
Comments: 1
Author: 可乐小可爱メ
1. 背景

业务需求是 自定义放大某个版本, 最后实现放弃了 canvas 绘制img 放大的思路, 这里记录一下。


2. 代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Canvas diy image</title>
<style>
canvas {
border: 1px dashed #eee;
width: 634px;
height: 673px;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
<script>
const canvas = document.querySelectorAll("#canvas")[0];
const context = canvas.getContext("2d");
const img = new Image();
let stemp = 10; // 步进
// 计算尺寸的函数
const roll = function (e) {
e = e || window.event;
if (e.wheelDelta) {
//IE,谷歌
if (e.wheelDelta > 0) {
//当滑轮向上滚动时
stemp--;
return e.wheelDelta / 100 + stemp;
}
if (e.wheelDelta < 0) {
//当滑轮向下滚动时
stemp++;
return e.wheelDelta / 100 + stemp;
}
} else if (e.detail) {
//Firefox
if (e.detail > 0) {
stemp--;
return e.wheelDelta / 100 + stemp;
}
if (e.detail < 0) {
stemp++;
return e.wheelDelta / 100 + stemp;
}
}
};
//给页面绑定滑轮滚动事件
if (document.addEventListener) {
document.addEventListener("DOMMouseScroll", roll, false);
}
canvas.onmousewheel = canvas.onmousewheel = roll; // 给canvas绑定滑轮滚动事件
let resize = 0;
let moveX = 0;
let moveY = 0;

//滚动滑轮触发scrollFunc方法
window.onload = function () {
img.src = "https://www.tiankele.cn/_nuxt/img/5e7bb66.png";
img.onload = function () {
drawImageScale();

canvas.onmousewheel = canvas.onmousewheel = function () {
resize = roll();
drawImageScale();
};

canvas.onmousedown = function (event) {
const { clientX, clientY } = event;
const preX = moveX;
const preY = moveY;
canvas.onmousemove = function (evt) {
//移动
canvas.style.cursor = "grabbing";
moveX = evt.clientX - clientX + preX;
moveY = evt.clientY - clientY + preY;
drawImageScale();
};
canvas.onmouseup = function () {
canvas.onmousemove = null;
canvas.onmouseup = null;
canvas.style.cursor = "default";
};
};
};
};
// 控制写入图片大小的函数
function drawImageScale() {
resize = resize || 1;
canvas.width = 634;
canvas.height = 673;
let imgWidth = 634 + resize * 20;
let imgHeight = 673 + resize * 20;
// let sx = imgWidth / 2 - canvas.width / 2
// let sy = imgHeight / 2 - canvas.height / 2
let dx = canvas.width / 2 - imgWidth / 2;
let dy = canvas.height / 2 - imgHeight / 2;
context.clearRect(0, 0, canvas.width, canvas.height);
// context.drawImage(img, 0, 0, imgWidth, imgHeight);
context.drawImage(
img,
dx,
dy,
imgWidth,
imgHeight,
moveX,
moveY,
canvas.width,
canvas.height
);
}
</script>
</html>


3. 参考

https://www.w3school.com.cn/tags/canvas_drawimage.asp

https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial


nodejs简易代理服务器

Posted on 2021-05-10 11:14:39
Comments: 0
Author: 可乐小可爱メ
1. 背景 

请求三方服务,因为服务器在海外, 网络延迟高, 实际部署上线后, 各种接口全都超时,严重影响实用效率.


2. 技术设计

  通过加一层 proxy server 做转发

        user --> client. --> proxy server --> business server .    

3. 代码实现

    3.1 代码结构

    

    3.2 

        proxy.js

const express = require("express");
const { createProxyMiddleware } = require("http-proxy-middleware");
const target = require("./.TARGET");

const app = express();
app.get("/", (req, res) => {
res.send("express 20010 home");
});

app.use(
"/api",
createProxyMiddleware({
target,
changeOrigin: true,
pathRewrite: {
"^/api": "", // rewrite path
},
})
);
app.listen(20010);

        

    shell.js

const fs = require("fs");
const shell = require("shelljs");
const { resolve } = require("path");
const order = process.argv.slice(2);
if (!order || order.length === 0) {
shell.echo("pls add target host!");
shell.exit(1);
}

const target = order[0].split("=")[1];
if (!target.startsWith("http")) {
return console.error("target error!");
}
fs.writeFile(
resolve(__dirname, ".TARGET"),
`module.exports = "${target}";`,
(err) => {
if (err) {
return console.error("Err: ", err);
}
const image = "sira-http-proxy";
shell.exec(`docker image rm ${image} -f`)
shell.exec(`docker container stop sira-proxy`);
shell.exec(`docker container rm sira-proxy`);
shell.exec(`docker image build -t ${image} .`);
shell.exec(
`docker container run --name="sira-proxy" -p 10000:20010 -itd ${image} `,
{
async: true,
}
);
}
);


    Dockerfile

FROM node:12.18.4
COPY . /app
WORKDIR /app
RUN npm install
EXPOSE 20010
CMD ["npx", "pm2-runtime", "proxy.js"]


3.3 执行命令

    node shell -t=https://www.tiankele.cn



SQL server 入门笔记

Posted on 2021-04-21 10:09:13
Comments: 0
Author: 可乐小可爱メ
INSERT INTO "tiankele"."dbo"."user" ( "name", "sex", "age") VALUES ( 'zhaoxiaoqi', 2, 28);

alter table [tiankele].[dbo].[user] add id int identity(1,1) not NULL;

DELETE FROM "tiankele"."dbo"."user" ;

CREATE TABLE "user" (
"uid" BIGINT identity(1,1) primary key not null,
"name" VARCHAR(50) NOT NULL,
"sex" INT NOT NULL,
"age" INT NOT NULL
);

SELECT * FROM "tiankele"."dbo"."user" WHERE sex = 1 ORDER BY "uid" DESC OFFSET 0 ROWS FETCH NEXT 1000 ROWS ONLY;


JIRA api上传附件

Posted on 2021-03-21 16:18:02
Comments: 0
Author: 可乐小可爱メ
1. 背景

需要一个支持系统, 面向非JIRA 直接用户, 调用JIRA 方便用户操作。

(此篇以  Issue attachments, add attachement 为例)

2. 设计实现



3. 代码

    3.1 后端部分

        3.1.1  API TOKEN

const { JIRA_TOKEN, JIRA_ACCOUNT } = require("../.secret");
const headers = {
Authorization: `Basic ${Buffer.from(JIRA_ACCOUNT + ":" + JIRA_TOKEN).toString(
"base64"
)}`,
Accept: "application/json",
"Content-Type": "application/json",
};


生成链接 : https://id.atlassian.com/manage-profile/security/api-tokens


        3.1.2 调 JIRA api

// ... 上下文
router.post(`/issue/attachment/:key`, (req, res) => {
const { key } = req.params;
const contentType = req.headers["content-type"];
const form = new Formidable.IncomingForm();
form.parse(req, async (err, fields, files) => {
if (err) {
return res.end({ code: 10001, err });
}
const rs = fs.createReadStream(files.file.path);
let value = [];
rs.on("data", (chunk) => {
value.push(chunk);
});
rs.on("error", (err) => {
res.json({ code: 10001, err });
});
rs.on("end", () => {
request(
{
url: `${JIRA_HOST}/rest/api/3/issue/${key}/attachments`,
method: "POST",
headers: {
Authorization,
Accept: "application/json",
"content-type": contentType,
"X-Atlassian-Token": "no-check",
},
formData: {
file: {
value: Buffer.from(value),
options: {
filename: files.file.name,
contentType: null,
},
},
},
},
(error, response, body) => {
if (error) {
log("jira api err: /issue/comment");
return res.json({ code: 10001, msg: "jira api err" });
}
res.status(response.statusCode);
res.send(body);
}
);
});
});
});

    3.2 前端部分

function uploadFile(e: any) {
const file = new window.File([e.file], e.file.name, { type: e.file.type });
const SIRATOKEN = window.localStorage.getItem("SIRA-TOKEN");
const formData = new FormData();
formData.append("file", file);
axios({
headers: {
Accept: "application/json",
"Content-Type": "multipart/form-data",
SIRATOKEN,
},
url: `/sira/jira/issue/attachment/${selectIssue}`,
method: "POST",
data: formData,
})
.then((res) => {
console.log("res: ", res);
})
.catch((e) => {
console.log("e: ", e);
});
}


4. 注意点

    4.1 这里 node服务,steam流 尝试转成pipe 避免大文件内存不够问题(暂时还没写出来), 需要加上 size 判断拦截.

    4.2 查看文件时,JIRA 文件地址做了 鉴权 302重定向,


并不能简单通过, 

res.set("Authorization", Authorization);
res.redirect(fileUrl);

这样处理来获取,因为真实资源路径是 JIRA 自己再次302而来。

这里的处理是 先 request 获取资源, 其中有当前资源的真实Uri,

router.get(`/file`, (req, res) => {
const {
query: { fileUrl },
} = req;
request(
{
method: "GET",
url: encodeURI(fileUrl),
headers: {
Authorization,
},
},
(error, response, body) => {
if (error) throw new Error(error);
if (response.statusCode === 200) {
res.redirect(response.request.uri.href);
} else {
res.status(response.statusCode);
res.json(response.body);
}
}
);
});


5. 资源链接

https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/


1
2
3