vue组件原型调用

相信很多人用vuejs构建应用时都会用到一些全局方法,比如发ajax请求时喜欢用axios挂载到vue原型上,如下:

// 引入vue和axios
import Vue from 'vue'
import axios from 'axios'
// 然后挂载到原型上
Vue.prototype.$axios = axios

// 用axios.get()方法可以这样用
this.$axios.get()

这样确实方便,不用每个用到axios的组件都去引入
类似如此,当我们要用到一些操作dom的方法时要怎么做呢,上面的例子纯属js的封装,没有涉及到dom;下面我用一个全局提示组件为例,类似element-ui的message组件为大家演示一遍如何封装一个包含操作dom的的全局组件的,步骤主要有3步:

1, 在componenets/Message 目录下新建一个Message.vue组件
<template>
<transition name="fade">
    <div class="message" :class="type" v-show="show">
      <i class="icon"></i>
      <span class="text">{{text}}</span>
    </div>
</transition>
</template>

<script type="text/ecmascript-6">
  export default {
    name: 'message',
    props: {
      type: {
        type: String,
        default: 'info',
      },
      text: {
        type: String,
        default: ''
      },
      show: {
        type: Boolean,
        default: false
      }
    }
  }
</script>

<style scoped lang="stylus">
//......
</style>
2, 在componenets/Message目录准备一个index.js
import Message from './Message.vue'

const MESSAGE = {
  duration: 3000, // 显示的时间 ms
  animateTime: 300, // 动画时间,表示这个组件切换show的动画时间
  install(Vue) {
    if (typeof window !== 'undefined' && window.Vue) {
      Vue = window.Vue
    }
    Vue.component('Message', Message)

    function msg(type, text, callBack) {
      let msg
      let duration = MESSAGE.duration
      if (typeof text === 'string') {
        msg = text
      } else if (text instanceof Object) {
        msg = text.text || ''
        if (text.duration) {
          duration = text.duration
        }
      }
      let VueMessage = Vue.extend({
        render(h) {
          let props = {
            type,
            text: msg,
            show: this.show
          }
          return h('message', {props})
        },
        data() {
          return {
            show: false
          }
        }
      })
      let newMessage = new VueMessage()
      let vm = newMessage.$mount()
      let el = vm.$el
      document.body.appendChild(el) // 把生成的提示的dom插入body中
      vm.show = true
      let t1 = setTimeout(() => {
        clearTimeout(t1)
        vm.show = false  //隐藏提示组件,此时会有300ms的动画效果,等动画效果过了再从body中移除dom
        let t2 = setTimeout(() => {
          clearTimeout(t2)
          document.body.removeChild(el) //从body中移除dom
          newMessage.$destroy()
          vm = null // 设置为null,好让js垃圾回收算法回收,释放内存

          callBack && (typeof callBack === 'function') && callBack() 
      // 如果有回调函数就执行,没有就不执行,用&&操作符,
      // 只有&&左边 的代码为true才执行&&右边的代码,避免用面条代码:
        }, MESSAGE.animateTime)
      }, duration)
    }

// 挂载到vue原型上,暴露四个方法
    Vue.prototype.$message = {
      info(text, callBack) {
        if (!text) return
        msg('info', text, callBack)
      },
      success(text, callBack) {
        if (!text) return
        msg('success', text, callBack)
      },
      error(text, callBack) {
        if (!text) return
        msg('error', text, callBack)
      },
      warning(text, callBack) {
        if (!text) return
        msg('warning', text, callBack)
      }
    }
  }
}
export default MESSAGE

上面的代码关键点就是用Vue.extend()构造出一个Vue子类实例,(注意我这里模板渲染只用到render函数,没有用template选项,因为template选项 要求装Vue时要加入模板编译器那块代码,用render函数更加简洁,只需要装运行时版本,Vue体积更加小);然后调用$mount()方法生成需要的dom,再拿到对应的$el,实例内部自己维护插入dom和移除dom的操作,对外暴露了四个方法info、success、error、warning方便不同的场景调用;类似的组件还有confrim组件、alert组件等,大同小异。

3,在main.js中引入components/Message/index.js,以插件形式安装

最后,当你需要用的时候就直接,特别适合在ajax回调函数里面用来提示

import Vue from 'vue'
import vMessage from './components/Message/index' 
Vue.use(vMessage)

this.$message.info('普通消息') 
this.$message.error('错误消息') 
this.$message.warning('警告消息') 
this.$message.success('成功消息')

密码学介绍及RSA算法概述

说来惭愧,我大学其实是信息安全专业的,但是啥都没学会,所以自学了web开始当一个搬运工。信息安全专业最重要的一门课就是密码学,下面我凭着残留的记忆还有没扔的大学教材和请教读研大学同学后总结的点,给大家介绍一下这门神奇的学科。

密码学介绍

密码编码学有两个分支,密码分析学和密码使用学,
分析学指破译一种加密方式的技巧,一个加密算法是否可靠都需要分析来证明。

使用学又分为三个分支:对称算法,非对称算法,密码协议。

对称算法是双方共享一个密钥,使用同样的方法进行加密和解密,现代的比如AES,DES,3DES等,古代的斯巴达密码棒和凯撒加密。

非对称算法用户会持有一个公私钥对儿。A想要给B发送信息,就需要A用B的公钥加密,B收到后使用私钥解密。常见的又RSA和椭圆曲线算法。
密码协议主要内容是如何搭配算法实现最优方案,对称与非对称有各自都优缺点,所以要一起使用才最好,最典型的例子就是传输层安全(TLS)方案,所有web浏览器都已使用这个方案,访问https的网站,我们发送信息时需要用服务器给的公钥进行加密,中间还涉及到CA,数字签名等等。

白话RSA算法


(这三个人目前还在世,MD5也是他们搞得)
RSA的出现并不是为了取代对称加密算法,因为RSA的执行需要很多计算,这也是为什么https的网站访问比较慢的原因,真正用来加密大量数据的还是对称加密算法,RSA为对称加密的公钥保驾护航。
RSA的底层原理就是整数的因式分解,两个大素数在乘积上很容易计算,但是对乘积的因式分解确实非常困难的,几乎是不可能完成的。

1.生成密钥

第一小步,bob选择两个质数(除了1和它本身以外不再有其他因数)
p = 5, q = 11
n = p*q = 55
计算p * q的欧拉函数 U(55) = (5-1)(11-1) = 40 (欧拉函数指的是在小于N的数环中于N互质的数字的个数 比如欧拉8 = 4 (1,3,4,7), 欧拉6 = 2 (1,5) )
欧拉函数最终可以推算出公式

点击查看推导过程

// 根据上面的公式js实现的欧拉函数
function isPrime(i) {
        for (var a = 2; a < i; a++) {
            if (i % a == 0) {
                return false;
            }
        }
        return true;
    }
    function getPrimes(n) {
        var current = n;
        for (var b = 2; b <= n; b++) {
            if (isPrime(b) && (n % b) == 0) {
                current *= (1 - 1 / b);
            }
        }
        return Math.round(current);
    }
    getPrimes(55)  // 40

第二小步,bob从1到40选择一个数字e=3

计算e对U(55)的模反逆元d( 费马小定理 ed ≡ 1 (mod φ(n)),自行搜索 )
等价于 3d – 1 = k40 —> 3x – 40y = 1 对这个二元一次方程求解。
使用 带入消元发 计算xy过程如下
40 = 3 * 13 + 1
然后把它们改写成“余数等于”的形式
1 = 40 – 3 * 13
然后一步一步替换,提取公因式得出 x=-13 y=-1 所以d=-13
在整数环{0,40}内-13对应27,所以私钥d为27。

计算出公钥为(n,e) = (55,3),私钥为(n,d) = (55, 27)
为什么说对称加密无法破解,当攻击者拿到公钥n,e,是无法推出私钥d的,因为根据公式ed ≡ 1 (mod φ(n)),算出d必须要知道n的欧拉函数是多少,φ(n)=(p-1)(q-1),如果能将n进行因数分解,就能算出d,可是大整数的因式分解是非常困难的,只有暴力破解。

2.加密与解密

加密公式为 m^e ≡ c (mod n)
解密公式为 c^d ≡ m (mod n)
已知明文m = 14,公钥(55,3)
14^3 = c(mod55)
算出 c = 49
解密
(49^27)mod55 = 14 解密成功
需要注意的一点是浏览器里计算Math.pow(49,27)%55 = 36。49的27次方已经超出了大多数语言的最大安全数值,所以我们在计算的时候需要自己想办法提公因式,结合律交换律之类的。
为了算出正确的14我自己实现了一个方法

Math.bigM = (s,n,m) => {
    let mi_gap = 1
    while(Math.pow(s,mi_gap)<Number.MAX_SAFE_INTEGER){
        mi_gap += 1
    }
    mi_gap -= 1
    let mi_gap_mi = Math.floor(n/mi_gap)
    let mi_gap_mo = n%mi_gap
    return Math.pow(Math.pow(s,mi_gap)%m, mi_gap_mi)*(Math.pow(s,mi_gap_mo)%m)%m
}

挑战全网最通俗易懂三门问题解析

之前看综艺节目时候看到了主持人和嘉宾在玩抽奖,玩就是经典的三门问题,看弹幕里很多人讨论发现了解这个坑的人不多,所以我就讲解一下。

问题描述:舞台上有三个门,一个后面有汽车,另外两个什么都没有,主持人开天眼知道所以情况。叫观众上台选择一个门,然后主持人会打开一扇空的门,然后剩下两个门问观众要不要更换选择。

答案:一定要换,不换,中奖的概率就是1/3,换了中奖概率就变成2/3了。

网上的思路都是先假设开某个,然后算出概率,然后再开哪个再算概率,然后又是一顿穷举巴拉巴拉才得出结果。好像大学概率论老师也是这么教的,具体的我忘记了。

另一种思路只需要画几张图就能说清楚


将三个门分为两部分,用户选择的1号门是一部分,剩下的2,3是一部分,那么奖品在用户部分的概率是1/3,在剩下部分的概率是2/3,然后剩下的部分主持人还给排除了一个门,那么2/3概率全部由3号门承担。所以1号门和3号门选哪个显而易见。
同理可以延伸到四门问题八门问题百门问题。
下面看一下百门问题

百门问题有个很重要的条件必须要先声明,就是主持人是只开一扇门还是会开98扇留两个,这两个种概率差别很大,三门问题是因为只剩一个门能开了,不需要这个条件。
假设用户选择了2号门,奖品在50号门

开一扇门的情况:

2号门有奖概率是1/100 = 0.01,剩下99扇门里有奖概率是99/100,主持人排除掉一个错误选项后,98扇门分担99/100的概率,所以每个门中奖概率是(99/100)/98 = 0.010102041,这样用户换不换的概率差不多,换只能提高0.0001的概率。

开98扇门错误门的情况

当主持人从剩余的99扇门给你排除了98扇门后,剩的那一扇门中奖的概率就成了 99/100。这时候用户选择更换,能提高99倍中奖概率。这和三门问题一样,只不过样本变大后更能说明更换是正确的。

由这个思路继续延伸下去,可以算出N门问题,M个选项,主持人排除K个门等等概率,这我就不多算了。

大部分情况玩游戏主持人是不会给你打开这么多门排雷,只会打开一个,于是我写了一段代码模拟了一下N个门,1个奖项,主持人只打开一扇门的情况:

function test(door_number) {
    console.log(`${door_number}门问题`)
    let right_times = 0
    let wrong_times = 0
    for(let i = 0; i &lt; 5000; i++){
        big_win(door_number,true)?++right_times:++wrong_times
    }
    console.log('=======选择换,中奖不中奖的概率===================')
    console.log(`选对概率是:${(right_times/50).toFixed(2)}%`)
    console.log(`选错概率是:${(wrong_times/50).toFixed(2)}%`)
    let right_times_2 = 0
    let wrong_times_2 = 0
    for(let i = 0; i &lt; 5000; i++){
        big_win(door_number,false)?++right_times_2:++wrong_times_2
    }
    console.log('=======选择不换,中奖不中奖的概率===================')
    console.log(`选对概率是:${(right_times_2/50).toFixed(2)}%`)
    console.log(`选错概率是:${(wrong_times_2/50).toFixed(2)}%`)
}
const floor = Math.floor.bind(Math)
const random = Math.random.bind(Math)
function big_win(door_number, hasChange) {
    let list = []
    list.length = door_number
    // 1为有奖 0为没有奖
    list.fill(0)
    // 生成待选项,其中一个是有钱的
    let present_index = floor(random() * door_number)
    list[present_index] = 1
    // 模拟用户随机选择一个选项
    let user_choose_idx = floor(random() * door_number)
    //模拟主持人从余下的选项中揭晓一个错误的
    let host_choosen_idx = floor(random() * door_number)
    while (host_choosen_idx === user_choose_idx || host_choosen_idx === present_index) {
        host_choosen_idx = floor(random() * door_number)
    }
    if(hasChange){
        // 用户从余下的先选项里再抽一个
        let user_choose_idx_2 = floor(random() * door_number)
        while(user_choose_idx_2 === user_choose_idx || user_choose_idx_2 ===host_choosen_idx){
            user_choose_idx_2 = floor(random() * door_number)
        }
        if(list[user_choose_idx_2] === 1){
            return true
        }else{
            return false
        }
    }else{
        if(list[user_choose_idx] === 1){
            return true
        }else{
            return false
        }
    }
}

按上面的思路,四门问题获奖概率是3/8,五门问题是4/15,换和不换各模拟5000次
测试结果

模拟结果和预想结果一致。

CSS属性:prefers-reduced-motion

之前在查阅关于如何减少资源的文章是,发现了一个有意思的属性,prefers-reduced-motion。我也不知道咋翻译,直译过来就叫动效喜好度吧。有两个选项no-preference无偏好和reduce减少。这是一个媒体查询属性,专属于ios系统。他是根据ios用户在系统设置中 通用->辅助功能->减弱动态效果里设置的值来进行检测。

准备测试一下,我随手用了一分钟写了一个我也不知道啥样的动画,然后按照MDN上的写法加上媒体查询属性。具体代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
    <style>
        @keyframes ani {
            from {
                transform: matrix3d()
            }

            to {
                transform: matrix3d(3, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1, 0, -60, -50, 0, 1)
            }
        }
        .ani {
            width: 100px;
            height: 100px;
            background: pink;
            animation: ani 1s ease-in-out infinite both;
            margin: auto;
            margin-top: 20%;
        }
        @media (prefers-reduced-motion: reduce) {
            .ani {
                animation: none;
            }
        }
    </style>
</head>
<body>
    <div class="ani"></div>
</body>
</html>

然后发送到手机上测试。
这是没开启动态减弱的效果
\

[/video]
这是开启后的效果,啥都不动了

总结:
这个属性目前还不常用,但我觉得还是比较有用。我的手机买来后就一直是开启动效减弱的,虽然ios系统级别的动画都已经优化掉了,但是我打开一些别的网站,不用prefers-reduced-motion还一堆元素花里胡哨乱飞的我看着难受。对这个网站的第一印象就会大打折扣。
延伸思路,这个属性如果大规模使用,对于我们这种面向少儿非常多动画的产品就类似于多了一个Lite版。甚至还可以用来做git到jpg的降级,因为对于像我这种不喜欢看动效的用户来说,简单静态的资源最好了,还能顺便减少资源大小。

<picture>
  <source srcset="no-motion.jpg" media="(prefers-reduced-motion: reduce)"></source> 
  <img srcset="animated.gif alt="brick wall"/>
</picture>

所以以后我在写代码的时候会注意用到这个功能,造福你我他,希望国内开发环境越来越好。

Lottie-Android, iOS,web全平台动画解决方案

Lottie是由Airbnb维护的一个动画解决方案,他可以将设计做的AE动画导出成一个JSON文件,我们在调用的时候直接读取即可,然后使用官方库进行解析。就可以做到对动画的控制,比如播放、停止、暂停、倍速、方向等。我们甚至可以加一些监视器,比如动画完成、动画循环,动画开始等。

优点

1.专业的人做专业的事,动画完全交给设计,程序员只负责展示,提高工作效率与质量
2.方便前端对动画的控制
3.100%还原度。
4.json文件大小会比图片gif由优势
5.由canvas svg渲染,不失真,效果好

缺点

1.lottie文件库有些大,有五六百KB,需要考虑加载问题
2.动画仅仅是动画,不能对已存在的元素加动画,不如css3那么自由。
3.对设计的要求变高了,并且在导出时候需要做一些限制
4.部分AE效果不支持导出

使用

背景:我正在做一个录音功能,现在录音时长这样

话筒不能动,就是一张png图片。产品同学说要增强用户体验,加动画。于是设计妹子用AE做好了动画,问我“动画做完了怎么给你,序列图,gif还是你用代码自己写?”动画是这样的

其实哪种方式实现都不难,但9012年了,本着创新精神,我对设计妹子说“那些方式太老了,这次试点新花样”于是上网搜索“AE如何导出SVG”,就发现了这个库。

1.AE安装插件bodymovin

可以直接在adobe官方商店安装,或者github下载压缩包手动导入https://github.com/airbnb/lottie-web
安装成功后,打开ae文件点击窗口—>拓展—>bodymovin,会弹出下面的弹窗

选择要生成的文件,选择生成文件的位置点击render就生成了一份JSON文件,包含各个图层的动画信息。注意原ae文件中必须全为矢量图像,不得包含图片,否则无法render。

2.引入lottie-web并读取动画

lottie-web这个库支持npm引入和标签引入,npm里叫做lottie-web,标签引入就叫做bodymovin

选中一个标签当作容器,然后loadAnimation,动画就已经导入了,效果如图

是不是很简单,对程序员很友好,要是开拓思路,能实现很多效果,例如app的开机屏,下拉动画,菜单栏动画等。

扒公众号视频及插件Puppeteer介绍

介绍

PuppeteerJs是一款无头浏览器库,类似于命令行版的Chrome。写过爬虫和做过网站SEO的同学一定知道。他可以用来做非常多的事,提供的能力超过真正的浏览器。比如

1.将网页导出为PDF图片之类。
2.预渲染单页面应用然后爬下来。
3.自动提交表单键盘输入等。
4.修改UA及窗口尺寸进行UI测试。
5.创建一个拥有最新js特性的自动更新的测试环境。
6.对网站进行性能检测。
7.Chrome插件开发

项目由谷歌团队维护,目的有5点1.提供一个精简的规范库,突出显示DevTools协议的功能。2.为类似的测试库提供参考。其他框架可以采用Puppeteer作为它们的基础层。3.促进无头/自动化浏览器测试的采用。4.帮助谷歌自家的产品找bug,开发DevTools新功能。5.找到浏览器自动化测试的痛点,并解决它。

实战爬取微信公众号视频

首先找一个带视频的公众号,比如这个小米的https://mp.weixin.qq.com/s/MpxPN_Rhv20Jr5V_3lC_6g
里面是雷军和王源在打广告。视频全部使用的是腾讯视频的iframe。估计是为了方便打广告,在我印象中去年用的还是一个简单的video标签。
用浏览器打开后审查元素分析结构(其实就是搜一下有没有video标签)。

找到了两个标签,但是没有找到地址,于是向上看,去到这个iframe地址。打开后是腾讯视频的网站了。接着搜video标签,这下查到地址了。所以整体的流程就是1.打开公众号地址找到iframe。2.打开iframe地址找到video标签的地址。

部分代码
// 创建浏览器,有头模式运行
const browser = await puppeteer.launch({
    headless: false
  });
//设置ua,我用了华为p10的
 await page.setUserAgent(UA);
// 跳转到公众号地址
 await page.goto(URL);
// 等待1秒钟,因为公众号是SPA模式,我们需要等页面全部load完成
await page.waitFor(1000);
// 找到腾讯视频iframe的地址 跳转过去然后再等1秒种
const bodyHandle1 = await page.$('body');
const iframeURL = await page.evaluate(body =&gt; {
    return body.querySelector('iframe').src;
}, bodyHandle1);
await page.goto(iframeURL);
await page.waitFor(1000);
// 点击播放按钮,唯一一个<a>标签,地址会变为视频地址
await page.click('a')
// 拿到src
const videoSrc = page.url()
// 使用superagent插件来下载资源,生成本地mp4文件
superagent
    .get(videoSrc)
    .end((err, res) =&gt; {
    fs.writeFile(`${__dirname}/video.mp4`, res.body, function () {
        spinner.succeed('下完啦~~~')
        setTimeout(() =&gt; {
            process.exit()
        }, 1000)
    });
})
需要注意的点

不知道腾讯视频是怎么标注用户的,正常浏览器端页面内用了自己写的webComponent。包含广告,推荐视频等业务功能,但是用无头浏览器就会变成一个简简单单的video标签,通过一个a标签触发。所以我们再分析页面结构的时候需要开着Puppeteer的有头模式。能够帮我们绕过一些坎儿。
无头浏览器渲染的腾讯视频结构

普通浏览器渲染的腾讯视频结构

展示


简易SVG路径编辑器

项目需要一个录制轨迹动画的功能,对于已存在的带有path的元素,可以使用anime.js使元素沿着轨迹运动,那么现在的问题就是如何生成带有自定义路径的元素。这就需要一个路径编辑器了。生成path格式的数据后,再创建svg元素。

需求

1.能够指定画板的宽高,位置,初始数据,回调函数
2.有两种模式,自由模式和连线模式。自由模式就是普通的画笔,连线模式是点击画线,并且可以通过控制点绘制二次贝塞尔曲线,基础点也可以调整位置
3.清除画板
4.插件形式

实现

首先新建SVGpath对象。

class SVGpath{
    constructor(opt){
        this.x = opt.x;
        this.y = opt.y;
        this.path = []
        ......
    }
    draw(){  //初始化函数
         this.render()
        ......
    }
    render(){} //生成UI界面函数
    mode_line(){} //连线模式
    mode_free(){} //自由模式
    clear(){} //清除函数
    cmode(){} //更换模式
    add_arc(){} //画圆
    add_line(){} //画线
}

视图层 创建canvas画布,确定取消按钮,更换模式按钮,HTML和CSS代码略
效果如图
效果图

自由模式
此模式比较简单,判断鼠标长按,在mousemove事件中把当前的鼠标位置作为一个数组[x,y]存入path数组中即可。有个小窍门是可以使用节流函数来画直线,减小画线的抖动。
值得注意的是渲染画线的函数drawLine(),因为要支持设置初始数据的功能,所以画线应该依据当前path中的数据,mousemove事件每次出发都要清空画布然后遍历path重新绘制线条。先存数据,再画线。这样就将数据和视图两个层面分开了。
效果如图

let isDrawing = false; //判断鼠标是否按住
const drawLine = () => {  //渲染图像的方法
    this.ctx.beginPath();
    for (let i = 0; i < this.path.length; i++) {
        this.ctx.lineTo(this.path[i][0], this.path[i][1]);
    }
    this.ctx.stroke();
};
this.canvas.onmousemove = throttle(e => {
        if (!isDrawing) return;
        this.path.push([e.offsetX, e.offsetY]);
        this.ctx.clearRect(0, 0, this.width, this.height);
        drawLine();
    },50,60);
this.canvas.onmouseup = () => (isDrawing = false);
this.canvas.mouseout = () => (isDrawing = false);

连线模式
连线模式稍微有些复杂,鼠标在画布上点击一下要生成一个黄色透明的圆作为基础点同时将坐标[x,y]写入path中。如果点到已存在的点上,要启用拖拽功能,鼠标变成pointer。两个相邻的基础点的中点会有一个控制点,来控制曲线的弧度。控制点与基础点存在一起。此时path中的数据格式为[x,y,[x1,y1]]
那么首先需要一个方法findIdx。传入坐标,返回当前坐标时候在点上,什么点,第几个点。

this.path.forEach((item, index) => {
    let tx = item[0]
    let ty = item[1]
    if (x < tx + 15 && x > tx - 15 && y < ty + 15 && y > ty - 15) {
        idx = { index, type: 'e' } //基础点
        return
    }
    if (!item[2]) {
        return;
    } else {
        const ix = item[2][0];
        const iy = item[2][1];
        if (x < ix + 15 && x > ix - 15 && y < iy + 15 && y > iy - 15) {
            idx = { index, type: 'q' } //控制点
        }
    }
});

能够判点击哪个点之后就可以写mouse事件了,在拖拽某个点时,实时改变对应path的值,不同的点有不同的move方法,为什么不把这两个方法合并为一个呢?因为那样会在mousemove的做太多判断,有性能问题,写入path的速度跟不上鼠标移动速度,只要移动快了,绘制就会停止,所以拆分成两个。

let isDrawing_idx = null // 控制点是否被点击
let isDrawing_e_idx = null // 基础点是否被点击
const move_e = e => { //拖拽基础点的事件
    const idx = findIdx(e.offsetX, e.offsetY);
    if (isDrawing_e_idx !== null) {
        this.path[isDrawing_e_idx][0] = e.offsetX
        this.path[isDrawing_e_idx][1] = e.offsetY
    }
}
const move_q = e => { //拖拽控制点的事件
    const idx = findIdx(e.offsetX, e.offsetY);
    if (isDrawing_idx !== null) {
        this.path[isDrawing_idx][2] = [e.offsetX, e.offsetY]
    }
}
this.canvas.onmousedown = e => {
    const idx = findIdx(e.offsetX, e.offsetY);
    if (idx.type === 'q') {
        isDrawing_idx = idx.index;
        this.canvas.onmousemove = move_q
        return;
    }
    if (idx.type === 'e') {
        isDrawing_e_idx = idx.index;
        this.canvas.onmousemove = move_e
        return;
    }
    this.path.push([e.offsetX, e.offsetY]);
    if (this.path.length > 1) {
        let len = this.path.length;
        let qx = (e.offsetX + this.path[len - 2][0]) / 2;
        let qy = (e.offsetY + this.path[len - 2][1]) / 2;
        this.path[len - 1].push([qx, qy]);
    }
};

目前数据部分已经完成了,就差绘制的方法,核心就是canvas的quadraticCurveTo方法,他接受四个参数,分别是基础点与控制点的坐标。在连线模式中所有的线段都是曲线,直线是没有弧度的曲线。控制点与基础点之间要用虚线方法setLineDash连接提示。
效果如图
事实

数组数据转字符串
draw_lines方法要做的就是遍历path,根据每一项item[0],item[1]画出黄色圆形基础点。同时使用quadraticCurveTo方法绘制一条曲线,quadraticCurveTo方法的前两个参数为下一项数组前两个数据,xy坐标。后两个参数为下一个项数组的第三个数据,作为控制点坐标。同时对于控制点要使用setLineDash方法连接到前后的基础点上。

let str = '';
array.forEach((item, index) =&gt; {
    let chr = 'L';
    if (index === 0) { chr = 'M' }
    if (item[2]) { chr = `Q${item[2][0] * ratio} ${item[2][1] * ratio}` }
    str += `${chr} ${item[0] * ratio} ${item[1] * ratio} `;
});
return str;

只要将获得的path数组使用这个方法转换一下就成了svg需要的路径字符串了。

效果展示


这个半圆弧对应的svg路径为
M 110 160 L 112 157 L 130 136 L 147 124 L 156 121 L 162 118 L 166 116 L 177 114 L 184 113 L 188 113 L 197 113 L 219 116 L 237 123 L 255 133 L 294 173 L 317 206 L 321 237 L 322 270 L 317 285 L 309 298 L 301 307 L 292 314 L 277 321 L 264 324 L 250 325 L 239 325 L 225 324 L 219 323 L 211 320 L 201 317 L 194 313 L 184 304 L 179 300


这个五角星对应的svg路径为
M 235 217 Q261 67 284 213 Q421 203 310 260 Q370 367 263 285 Q155 378 214 259 Q103 215 233 218

使用实例

import SVGpath,{path2string} from 'utils/getSVGpath';
const { x, y, width, height } = document.getElementById('wrap').getBoundingClientRect()

const params = {
    x:x,
    y:y,
    width:width,
    height:height,
    default:null,
    onSure:function(res){
        console.log(res.data)
    }
}
let svgpath = new SVGpath(params)
document.getElementById('start').onclick = () =&gt; svg.draw()

当点击页面上id为start的按钮后,就可以开始使用编辑器了,是不是很方便。