实现输入框@人名功能

<!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>Document</title>
    <style>
      .label {
        display: inline-block;
        border: solid 1px #ccc;
        padding: 10px 20px;
      }
      .output {
        padding: 20px;
        margin: 20px 0;
        width: 400px;
      }

      #myInput {
        width: 400px;
        border: solid 1px #ccc;
        padding: 10px 20px;
      }
    </style>
  </head>
  <body>
    <div class="output" id="myOutput"></div>
    <div contenteditable="plaintext-only" id="myInput"></div>
    <div><button id="myButton">submit</button></div>
    <div class="label" contenteditable="false" data-name="张三">@张三</div>
    <div class="label" contenteditable="false" data-name="李四">@李四</div>
    <div class="label" contenteditable="false" data-name="王五">@王五</div>
    <script>
      let inputEl = document.getElementById("myInput")
      let outputEl = document.getElementById("myOutput")
      let buttonEl = document.getElementById("myButton")
      let labelsEl = document.querySelectorAll(".label")

      buttonEl.addEventListener("click", (e) => {
        let liEl = document.createElement("div")
        liEl.innerText = inputEl.innerText
        outputEl.appendChild(liEl)
      })
      labelsEl.forEach((m) => {
        m.addEventListener("click", function (e) {
          console.log(this.dataset)
          let spanEl = document.createElement("span")
          spanEl.innerHTML = "@" + this.dataset.name + " "
          spanEl.style = "color:blue"
          spanEl.contentEditable = "false"
          inputEl.appendChild(spanEl)
        })
      })
    </script>
  </body>
</html>

 

实现页面元素滚动到容器底部的2中常用方案

方案1 让容器的滚动条top值等于容器的滚动条高度
containerEl.scrollTop = containerEl.scrollHeight

方案2 让元素的最后一项执行scrollIntoView(false)

具体见demo吧

<!DOCTYPE html>
<html lang="en">
  <head>
    <style>
      .container {
        border: solid 1px #ccc;
        width: 200px;
        height: 200px;
        overflow-y: auto;
      }
    </style>
  </head>
  <body>
    <button id="btn1">to bottom 1</button>
    <button id="btn2">to bottom 2</button>
    <div class="container">
      <div class="item">first item</div>
      <div class="item">item</div>
      <div class="item">item</div>
      <div class="item">item</div>
      <div class="item">item</div>
      <div class="item">item</div>
      <div class="item">item</div>
      <div class="item">item</div>
      <div class="item">item</div>
      <div class="item">item</div>
      <div class="item">item</div>
      <div class="item">item</div>
      <div class="item">item</div>
      <div class="item">item</div>
      <div class="item">item</div>
      <div class="item">item</div>
      <div class="item">item</div>
      <div class="item">item</div>
      <div class="item">last item</div>
    </div>
    <script>
      var containerEl = document.querySelector(".container")
      var btn1El = document.querySelector("#btn1")
      var btn2El = document.querySelector("#btn2")
      btn1El.addEventListener("click", function () {
        containerEl.scrollTop = containerEl.scrollHeight
      })
      btn2El.addEventListener("click", function () {
        let lastItemEl = document.querySelectorAll(".item")
        lastItemEl[lastItemEl.length - 1].scrollIntoView(false)
      })
    </script>
  </body>
</html>

 

极简js的观察者模式实现

最近项目中遇到一个小需求,需要用到on和emit的场景,夸两个项目。其中一个项目中已经集成rxjs,但是另一个项目没有。为了快速解决问题,避免杀鸡用牛刀(主要是没时间解释rxjs)。今天又写了一个简易的观察者实现。

需求是这样的:在子页面去订阅show和hide这2个事件,然后在父页面异步的去eimt这2个事件

实现原理:
1. 在2个页面中使用一个client对象作为中介;
2. 使用subObj最为一个订阅者来保存各种事件;
3. 然后client里面提供on方法让子页面可以调用,来把事件保存的subObj这个订阅者中为将来调用做准备;
4. 创建一个emit方法来触发subObj订阅者里面的各种事件;
5. 异步来调用emit方法来触发需要的事件

//father page(play)
var client = {}; // 中介(sdk)

var subObj = {}; //订阅者

// 订阅函数
function sub(name, cb) {
  subObj[name] = cb;
}
// 触发事件函数
function emit(name) {
  subObj[name]();
}

//执行触发事件
setTimeout(function () {
  emit("show");
}, 2000);

setTimeout(function () {
  emit("hide");
}, 4000);

client.on = function (name, cb) {
  sub(name, cb);
};

//children page (live ui)
function show() {
  console.log("ui was show");
}
function hide() {
  console.log("ui was hide");
}

// 订阅事件
this.client.on("show", function () {
  show();
});
this.client.on("hide", function () {
  hide();
});

 

判断h5页面是否在微信小程序的web-view里面

思路是通过使用微信的SDK(https://res.wx.qq.com/open/js/jweixin-1.3.2.js)提供的wx.miniProgram.getEnv方法来判断当前环境。

直接用下面的代码,或者用我写好的页面直接放到小程序的web-view里面测试:https://liuxiaofan.com/test/mp.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>Document</title>
</head>
<body>
    1111
    <script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.3.2.js"></script>
    <script>
            var ua = navigator.userAgent.toLowerCase();
            if (ua.match(/MicroMessenger/i) == "micromessenger") {
                //ios的ua中无miniProgram,但都有MicroMessenger(表示是微信浏览器)
                wx.miniProgram.getEnv((res) => {
                    if (res.miniprogram) {
                        alert("在小程序里")
                    } else {
                        alert("不在小程序")
                    }
                })
            } else {
                alert("不在微信")
            }
    </script>
</body>
</html>

 

video事件及属性备忘

事件

<video src="test.mp4" controls width="400" height="300" id="video"></video>
  
  <script>
    var video = document.getElementById('video')

    // 1、loadstart:视频查找。当浏览器开始寻找指定的音频/视频时触发,也就是当加载过程开始时
    video.addEventListener('loadstart', function(e) {
      console.log('提示视频的元数据已加载')
      console.log(e)
      console.log(video.duration)            // NaN
    })

    // 2、durationchange:时长变化。当指定的音频/视频的时长数据发生变化时触发,加载后,时长由 NaN 变为音频/视频的实际时长
    video.addEventListener('durationchange', function(e) {
      console.log('提示视频的时长已改变')
      console.log(e)
      console.log(video.duration)           // 528.981333   视频的实际时长(单位:秒)
    })

    // 3、loadedmetadata :元数据加载。当指定的音频/视频的元数据已加载时触发,元数据包括:时长、尺寸(仅视频)以及文本轨道
    video.addEventListener('loadedmetadata', function(e) {
      console.log('提示视频的元数据已加载')
      console.log(e)
    })

    // 4、loadeddata:视频下载监听。当当前帧的数据已加载,但没有足够的数据来播放指定音频/视频的下一帧时触发
    video.addEventListener('loadeddata', function(e) {
      console.log('提示当前帧的数据是可用的')
      console.log(e)
    })

    // 5、progress:浏览器下载监听。当浏览器正在下载指定的音频/视频时触发
    video.addEventListener('progress', function(e) {
      console.log('提示视频正在下载中')
      console.log(e)
    })

    // 6、canplay:可播放监听。当浏览器能够开始播放指定的音频/视频时触发
    video.addEventListener('canplay', function(e) {
      console.log('提示该视频已准备好开始播放')
      console.log(e)
    })

    // 7、canplaythrough:可流畅播放。当浏览器预计能够在不停下来进行缓冲的情况下持续播放指定的音频/视频时触发
    video.addEventListener('canplaythrough', function(e) {
      console.log('提示视频能够不停顿地一直播放')
      console.log(e)
    })

    // 8、play:播放监听
    video.addEventListener('play', function(e) {
      console.log('提示该视频正在播放中')
      console.log(e)
    })

    // 9、pause:暂停监听
    video.addEventListener('pause', function(e) {
      console.log('暂停播放')
      console.log(e)
    })

    // 10、seeking:查找开始。当用户开始移动/跳跃到音频/视频中新的位置时触发
    video.addEventListener('seeking', function(e) {
      console.log('开始移动进度条')
      console.log(e)
    })

    // 11、seeked:查找结束。当用户已经移动/跳跃到视频中新的位置时触发
    video.addEventListener('seeked', function(e) {
      console.log('进度条已经移动到了新的位置')
      console.log(e)
    })

    // 12、waiting:视频加载等待。当视频由于需要缓冲下一帧而停止,等待时触发
    video.addEventListener('waiting', function(e) {
      console.log('视频加载等待')
      console.log(e)
    })

    // 13、playing:当视频在已因缓冲而暂停或停止后已就绪时触发
    video.addEventListener('playing', function(e) {
      console.log('playing')
      console.log(e)
    })

    // 14、timeupdate:目前的播放位置已更改时,播放时间更新
    video.addEventListener('timeupdate', function(e) {
      console.log('timeupdate')
      console.log(e)
    })

    // 15、ended:播放结束
    video.addEventListener('ended', function(e) {
      console.log('视频播放完了')
      console.log(e)
    })

    // 16、error:播放错误
    video.addEventListener('error', function(e) {
      console.log('视频出错了')
      console.log(e)
    })

    // 17、volumechange:当音量更改时
    video.addEventListener('volumechange', function(e) {
      console.log('volumechange')
      console.log(e)
    })

    // 18、stalled:当浏览器尝试获取媒体数据,但数据不可用时
    video.addEventListener('stalled', function(e) {
      console.log('stalled')
      console.log(e)
    })

    // 19、ratechange:当视频的播放速度已更改时
    video.addEventListener('ratechange', function(e) {
      console.log('ratechange')
      console.log(e)
    })
  </script>

属性

<!-- video 不支持 IE8及以下版本浏览器,支持三种视频格式:MP4,WebM 和 Ogg -->
  <video src="test.mp4" controls width="400" height="300"></video>

  <!-- 禁止下载 -->
  <video src="test.mp4" controls controlslist="nodownload" width="400" height="300"></video>

  <!-- 禁止下载,禁止全屏 -->
  <video src="test.mp4" controls controlslist="nodownload nofullscreen" width="400" height="300"></video>

  <!-- 自动播放 (不同浏览器的表现不一样) -->
  <video src="test.mp4" controls autoplay width="400" height="300"></video>

  <!-- 默认静音播放(可手动点开继续播放) -->
  <video src="test.mp4" controls muted width="400" height="300"></video>

  <!-- 循环播放 -->
  <video src="test.mp4" controls loop width="400" height="300"></video>

  <!-- 预加载 -->
  <video src="test.mp4" controls preload width="400" height="300"></video>

  <!-- 贴图 -->
  <video src="test.mp4" poster="poster.jpg" controls width="400" height="300"></video>

  <!-- 音量控制 -->
  <video src="test.mp4" poster="poster.jpg" controls width="400" height="300" id="_volume"></video>
  <script>
    var video = document.getElementById('_volume')
    video.volume = 2 // 取值范围:0 到 1,0 是静音,0.5 是一半的音量,1 是最大音量(默认值)
  </script>

  <!-- 播放时间控制 -->
  <video src="test.mp4" poster="poster.jpg" controls width="400" height="300" id="_time"></video>
  <script>
    var video = document.getElementById('_time')
    console.log(video.currentTime)  // 视频当前正在播放的时间(单位:s),进度条拖到哪就显示当前的时间
    video.currentTime = 60  // 默认从60秒处开始播放
  </script>

  <!-- 播放地址切换 (常见于切换超清  高清 流畅,不同画质的视频地址不同) -->
  <video src="test.mp4" controls autoplay width="400" height="300" id="_src"></video>
  <script>
    var video = document.getElementById('_src')
    console.log(video.src)     // http://127.0.0.1:8001/test.mp4   绝对地址,DOM 中是相对地址
    // video.src = 'test-2.mp4'   // 直接替换掉了原来的视频src
    setTimeout(() => {
      video.src = 'test-2.mp4'  // 播放到第 30s 的时候,自动切换视频
    }, 30000)
  </script>

  <!-- 备用地址切换 -->
  <video controls autoplay width="400" height="300" id="_source">
    <source src="test3.mp4" type="video/mp4" />
    <source src="test9.mp4" type="video/mp4" />
    <source src="test-2.mp4" type="video/mp4" />
  </video>
  <script>
    var video = document.getElementById('_source')
    setTimeout(() => {
      console.log(video.currentSrc)     // http://127.0.0.1:8001/test.mp4
    }, 1000)

    // HTTP 载入失败,状态码 404。媒体资源 http://127.0.0.1:8001/test3.mp4 载入失败。
    // HTTP 载入失败,状态码 404。媒体资源 http://127.0.0.1:8001/test9.mp4 载入失败。
    // http://127.0.0.1:8001/test-2.mp4
    // 当第一段视频加载失败时,自动加载下一段视频

 

将文本中的url转化为超链接,包含端口

今天接到一个需求,要求在聊天窗口中,用户输入的文本内容,如果里面有超链接地址,就自动转换为a标签,让用户可以点击。

这个需求N年前做的sass项目就实现过,不过这次要求要把端口号也解析出来,例如http://localhost:3010/这样,趁机复习一下正则。

废话不多说,直接上代码

// 将文本中的url转化为超链接,包含端口
export const handleTextUrl = text => {
  let result = text || ''
  let reg = /(http:\/\/|https:\/\/)((\w|=|\?|\.|\/|&|-|:)+)/g
  if (result) {
    result = text.replace(reg, '<a href="$1$2" target="_blank">$1$2</a>')
  }
  return result
}
// handleTextUrl('测试一个地址:http://wiki.kaikeba.cn/pages/viewpage.action?pageId=24184041')
// handleTextUrl('http://localhost:3010/')

 

防抖与节流

防抖

防抖要解决的问题:阻止一个事件被频繁触发。
例如:当我们监听浏览器的滚动条事件时,如果不做防抖,就会发生滚动一次鼠标滚轮,事件被触发6-8次。
应用场景:等用户的鼠标滚轮滚动到某一个地方,停止滚动1秒之后才执行函数。
解决思路:设置一个触发函数执行的「等待时间」,当「等待时间」结束之后才可以被执行。

function debounce(fn, wait) {
  var timer = null;
  return function () {
      var context = this
      var args = arguments
      if (timer) {
          clearTimeout(timer);
          timer = null;
      }
      timer = setTimeout(function () {
          fn.apply(context, args)
      }, wait)
  }
}

var fn = function () {
  console.log('boom')
}

setInterval(debounce(fn,500),1000) // 第一次在1500ms后触发,之后每1000ms触发一次

setInterval(debounce(fn,2000),1000) // 不会触发一次(我把函数防抖看出技能读条,如果读条没完成就用技能,便会失败而且重新读条)

节流

节流要解决的问题:和防抖一样,都是阻止一个事件被频繁触发
区别:防抖是「等待时间」结束之后再执行函数,而节流是先执行函数,然后再计时,确保计时没有结束之前函数不会再次执行。
应用场景:用户在网页的Input表单输入框中按住了某一个按键,但是我们要做到函数不被连续触发,而是每1秒执行1次

function throttle(fn, gapTime) {
  let _lastTime = null;

  return function () {
    let _nowTime = + new Date()
    if (_nowTime - _lastTime > gapTime || !_lastTime) {
      fn();
      _lastTime = _nowTime
    }
  }
}

let fn = ()=>{
  console.log('boom')
}

setInterval(throttle(fn,1000),10)

 

语义化版本控制规范(SemVer)

摘要

版本格式:主版本号.次版本号.修订号,版本号递增规则如下:

  1. 主版本号:当你做了不兼容的 API 修改,
  2. 次版本号:当你做了向下兼容的功能性新增,
  3. 修订号:当你做了向下兼容的问题修正。
    先行版本号及版本编译元数据可以加到“主版本号.次版本号.修订号”的后面,作为延伸。

规范

以下关键词 MUST、MUST NOT、REQUIRED、SHALL、SHALL NOT、SHOULD、SHOULD NOT、 RECOMMENDED、MAY、OPTIONAL 依照 RFC 2119 的叙述解读。(译注:为了保持语句顺畅, 以下文件遇到的关键词将依照整句语义进行翻译,在此先不进行个别翻译。)

  1. 使用语义化版本控制的软件必须(MUST)定义公共 API。该 API 可以在代码中被定义或出现于严谨的文件内。无论何种形式都应该力求精确且完整。

  2. 标准的版本号必须(MUST)采用 X.Y.Z 的格式,其中 X、Y 和 Z 为非负的整数,且禁止(MUST NOT)在数字前方补零。X 是主版本号、Y 是次版本号、而 Z 为修订号。每个元素必须(MUST)以数值来递增。例如:1.9.1 -> 1.10.0 -> 1.11.0。

  3. 标记版本号的软件发行后,禁止(MUST NOT)改变该版本软件的内容。任何修改都必须(MUST)以新版本发行。

  4. 主版本号为零(0.y.z)的软件处于开发初始阶段,一切都可能随时被改变。这样的公共 API 不应该被视为稳定版。

  5. 1.0.0 的版本号用于界定公共 API 的形成。这一版本之后所有的版本号更新都基于公共 API 及其修改内容。

  6. 修订号 Z(x.y.Z | x > 0)必须(MUST)在只做了向下兼容的修正时才递增。这里的修正指的是针对不正确结果而进行的内部修改。

  7. 次版本号 Y(x.Y.z | x > 0)必须(MUST)在有向下兼容的新功能出现时递增。在任何公共 API 的功能被标记为弃用时也必须(MUST)递增。也可以(MAY)在内部程序有大量新功能或改进被加入时递增,其中可以(MAY)包括修订级别的改变。每当次版本号递增时,修订号必须(MUST)归零。

  8. 主版本号 X(X.y.z | X > 0)必须(MUST)在有任何不兼容的修改被加入公共 API 时递增。其中可以(MAY)包括次版本号及修订级别的改变。每当主版本号递增时,次版本号和修订号必须(MUST)归零。

  9. 先行版本号可以(MAY)被标注在修订版之后,先加上一个连接号再加上一连串以句点分隔的标识符来修饰。标识符必须(MUST)由 ASCII 字母数字和连接号 [0-9A-Za-z-] 组成,且禁止(MUST NOT)留白。数字型的标识符禁止(MUST NOT)在前方补零。先行版的优先级低于相关联的标准版本。被标上先行版本号则表示这个版本并非稳定而且可能无法满足预期的兼容性需求。范例:1.0.0-alpha、1.0.0-alpha.1、1.0.0-0.3.7、1.0.0-x.7.z.92。

  10. 版本编译元数据可以(MAY)被标注在修订版或先行版本号之后,先加上一个加号再加上一连串以句点分隔的标识符来修饰。标识符必须(MUST)由 ASCII 字母数字和连接号 [0-9A-Za-z-] 组成,且禁止(MUST NOT)留白。当判断版本的优先层级时,版本编译元数据可(SHOULD)被忽略。因此当两个版本只有在版本编译元数据有差别时,属于相同的优先层级。范例:1.0.0-alpha+001、1.0.0+20130313144700、1.0.0-beta+exp.sha.5114f85。

  11. 版本的优先层级指的是不同版本在排序时如何比较。
    判断优先层级时,必须(MUST)把版本依序拆分为主版本号、次版本号、修订号及先行版本号后进行比较(版本编译元数据不在这份比较的列表中)。
    由左到右依序比较每个标识符,第一个差异值用来决定优先层级:主版本号、次版本号及修订号以数值比较,例如:1.0.0 < 2.0.0 < 2.1.0 < 2.1.1。
    当主版本号、次版本号及修订号都相同时,改以优先层级比较低的先行版本号决定。例如:1.0.0-alpha < 1.0.0。
    有相同主版本号、次版本号及修订号的两个先行版本号,其优先层级必须(MUST)透过由左到右的每个被句点分隔的标识符来比较,直到找到一个差异值后决定:
    只有数字的标识符以数值高低比较,有字母或连接号时则逐字以 ASCII 的排序来比较。
    数字的标识符比非数字的标识符优先层级低。
    若开头的标识符都相同时,栏位比较多的先行版本号优先层级比较高。
    范例:1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0。

为什么要使用语义化的版本控制?

这并不是一个新的或者革命性的想法。实际上,你可能已经在做一些近似的事情了。问题在于只是“近似”还不够。如果没有某个正式的规范可循,版本号对于依赖的管理并无实质意义。将上述的想法命名并给予清楚的定义,让你对软件使用者传达意向变得容易。一旦这些意向变得清楚,弹性(但又不会太弹性)的依赖规范就能达成。

举个简单的例子就可以展示语义化的版本控制如何让依赖地狱成为过去。假设有个名为“救火车”的函式库,它需要另一个名为“梯子”并已经有使用语义化版本控制的包。当救火车创建时,梯子的版本号为 3.1.0。因为救火车使用了一些版本 3.1.0 所新增的功能, 你可以放心地指定依赖于梯子的版本号大等于 3.1.0 但小于 4.0.0。这样,当梯子版本 3.1.1 和 3.2.0 发布时,你可以将直接它们纳入你的包管理系统,因为它们能与原有依赖的软件兼容。

作为一位负责任的开发者,你理当确保每次包升级的运作与版本号的表述一致。现实世界是复杂的,我们除了提高警觉外能做的不多。你所能做的就是让语义化的版本控制为你提供一个健全的方式来发行以及升级包,而无需推出新的依赖包,节省你的时间及烦恼。
如果你对此认同,希望立即开始使用语义化版本控制,你只需声明你的函式库正在使用它并遵循这些规则就可以了。请在你的 README 文件中保留此页连结,让别人也知道这些规则并从中受益。

FAQ

在 0.y.z 初始开发阶段,我该如何进行版本控制?

最简单的做法是以 0.1.0 作为你的初始化开发版本,并在后续的每次发行时递增次版本号。

如何判断发布 1.0.0 版本的时机?

当你的软件被用于正式环境,它应该已经达到了 1.0.0 版。如果你已经有个稳定的 API 被使用者依赖,也会是 1.0.0 版。如果你很担心向下兼容的问题,也应该算是 1.0.0 版了。

这不会阻碍快速开发和迭代吗?

主版本号为零的时候就是为了做快速开发。如果你每天都在改变 API,那么你应该仍在主版本号为零的阶段(0.y.z),或是正在下个主版本的独立开发分支中。

对于公共 API,若即使是最小但不向下兼容的改变都需要产生新的主版本号,岂不是很快就达到 42.0.0 版?

这是开发的责任感和前瞻性的问题。不兼容的改变不应该轻易被加入到有许多依赖代码的软件中。升级所付出的代价可能是巨大的。要递增主版本号来发行不兼容的改版,意味着你必须为这些改变所带来的影响深思熟虑,并且评估所涉及的成本及效益比。

为整个公共 API 写文件太费事了!

为供他人使用的软件编写适当的文件,是你作为一名专业开发者应尽的职责。保持专案高效一个非常重要的部份是掌控软件的复杂度,如果没有人知道如何使用你的软件或不知道哪些函数的调用是可靠的,要掌控复杂度会是困难的。长远来看,使用语义化版本控制以及对于公共 API 有良好规范的坚持,可以让每个人及每件事都运行顺畅。

万一不小心把一个不兼容的改版当成了次版本号发行了该怎么办?

一旦发现自己破坏了语义化版本控制的规范,就要修正这个问题,并发行一个新的次版本号来更正这个问题并且恢复向下兼容。即使是这种情况,也不能去修改已发行的版本。可以的话,将有问题的版本号记录到文件中,告诉使用者问题所在,让他们能够意识到这是有问题的版本。

如果我更新了自己的依赖但没有改变公共 API 该怎么办?

由于没有影响到公共 API,这可以被认定是兼容的。若某个软件和你的包有共同依赖,则它会有自己的依赖规范,作者也会告知可能的冲突。要判断改版是属于修订等级或是次版等级,是依据你更新的依赖关系是为了修复问题或是加入新功能。对于后者,我经常会预期伴随着更多的代码,这显然会是一个次版本号级别的递增。

如果我变更了公共 API 但无意中未遵循版本号的改动怎么办呢?(意即在修订等级的发布中,误将重大且不兼容的改变加到代码之中)

自行做最佳的判断。如果你有庞大的使用者群在依照公共 API 的意图而变更行为后会大受影响,那么最好做一次主版本的发布,即使严格来说这个修复仅是修订等级的发布。记住, 语义化的版本控制就是透过版本号的改变来传达意义。若这些改变对你的使用者是重要的,那就透过版本号来向他们说明。

我该如何处理即将弃用的功能?

弃用现存的功能是软件开发中的家常便饭,也通常是向前发展所必须的。当你弃用部份公共 API 时,你应该做两件事:(1)更新你的文件让使用者知道这个改变,(2)在适当的时机将弃用的功能透过新的次版本号发布。在新的主版本完全移除弃用功能前,至少要有一个次版本包含这个弃用信息,这样使用者才能平顺地转移到新版 API。

语义化版本对于版本的字串长度是否有限制呢?

没有,请自行做适当的判断。举例来说,长到 255 个字元的版本已过度夸张。再者,特定的系统对于字串长度可能会有他们自己的限制。

排列组合算法公式JS版

之前做过的彩票项目中用到了排列和组合公式,分享给需要的人。

/**
         * 排列结果  包括重复内容
         */
        arrCPermute: function (arr, num) {
            var r = [];
            (function f(t, a, n) {
                if (n == 0) return r.push(t);
                for (var i = 0, l = a.length - n; i <= l; i++) {
                    f(t.concat(a[i]), a.slice(i + 1), n - 1);
                }
            })([], arr, num);
            return r;
        },
        /**
         *  排列结果  不包括重复内容
         */
        arrCPermute1: function (arr, num) {
            var r = [];
            (function f(t, a, n) {
                if (n == 0) return r.push(t);
                for (var i = 0, l = a.length; i < l; i++) {
                    f(t.concat(a[i]), a.slice(0, i).concat(a.slice(i + 1)), n - 1);
                }
            })([], arr, num);
            return r;
        },
        /**
         * 排列公式(从n个不同元素中取出m个元素的组合数)
         * @param n
         * @param m
         * @returns {number}
         */
        mathP: function (n, m) {
            var c = 1;
            for (var i = n - m; i < n; c *= ++i);
            return c;
        },
        /**
         * 组合公式(从n个不同元素中取出m个元素的组合数)
         * @param n
         * @param m
         * @returns {number}
         */
        mathC: function (n, m) {
            var n1 = 1,
                n2 = 1;
            for (var i = n, j = 1; j <= m; n1 *= i--, n2 *= j++);
            return n1 / n2;
        },

双色球计算组合时可以这样使用

lotNum = util.mathC(redNum, 5) * util.mathC(blueNum, 2); //红球的组合*篮球的组合

 

微信小程序手写签名组件

业务需求:在微信小程序内的商家入驻协议页面的下面设置一个空白区域,可以让用户在手机屏幕上进行手指滑动,从而实现手写签名的功能。

设计思路:
1 创建一个canvas对象
2 监听用户手指触摸事件bindtouchstart=”start”
3 开始触摸时保存坐标x,y
4 触摸移动时bindtouchmove=”move” 执行绘制操作ctx.stroke
5 保存图片通过wx.canvasToTempFilePath把临时路径返回出去用户上传接口使用

全部代码:

WXML

<view>
   <canvas 
    class="c-autograph"
    style="width:{{canvasW}}px;height:{{canvasH}}px;"
    canvas-id="autograph" 
    bindtouchcancel="cancel" 
    bindtouchmove="move" 
    bindtouchstart="start" 
    bindtouchend="end" 
    bindlongtap="longtap" 
    disable-scroll="true" 
    binderror="error"
    ></canvas>
</view>

JS

/*
 * @Author: liuxiaofan
 * @Description: v2.0.0
 * @Date: 2020-03-10 15:56:34
 * @LastEditors: liuxiaofan
 * @LastEditTime: 2020-04-20 15:40:55
 */
// 采坑: 如果html标签里面写了 type="2d",会导致真机空白
var ctx = null; //canvas实例
var touchs = [];
Component({
    properties: {

    },
    data: {
        canvasW: 140,
        canvasH: 56,
        lineColor: '#333', // 笔划的颜色
        LineWidth: 3, //笔划粗细
        count: 0 //画了几笔
    },
    ready() {
        this.autoWidth()
        this.init()
    },
    methods: {
        autoWidth() {
            const dpr = wx.getSystemInfoSync().pixelRatio // 获取设备的屏幕分辨率是2x还是3x
            let canvasW, canvasH;
            if (dpr >= 2) {
                canvasW = this.data.canvasW * 2
                canvasH = this.data.canvasH * 2
            }
            if (dpr >= 3) {
                canvasW = this.data.canvasW * 2.21
                canvasH = this.data.canvasH * 2.21
            }
            
            // 最近小高度100
            if (canvasH < 100) {
                canvasH = 100
            }
            
            if (canvasW < 280) {
                canvasW = 280
            }

            this.setData({
                canvasW,
                canvasH,
            })
        },
        init: function (options) {
            ctx = wx.createCanvasContext('autograph', this); // 如果是组件的形式,第2个参数要写this
            ctx.setStrokeStyle(this.data.lineColor);
            ctx.setLineWidth(this.data.LineWidth);
            ctx.setLineCap('round'); // 笔触形态
            ctx.setLineJoin('round'); // 交叉点形态
        },
        // 画布的触摸移动开始手势响应
        start: function (event) {
            // console.log("触摸开始" + event.changedTouches[0].x);
            // console.log("触摸开始" + event.changedTouches[0].y);
            //获取触摸开始的 x,y
            let point = { x: event.changedTouches[0].x, y: event.changedTouches[0].y }
            touchs.push(point);
        },
        // 画布的触摸移动手势响应
        move: function (e) {
            let point = { x: e.touches[0].x, y: e.touches[0].y }
            touchs.push(point);
            if (touchs.length >= 2) {
                this.draw(touchs);
            }
        },
        // 画布的触摸移动结束手势响应
        end: function (e) {
            this.setData({
                count: this.data.count + 1
            })
            // console.info("触摸结束" + this.data.count);
            //清空轨迹数组
            for (let i = 0; i < touchs.length; i++) {
                touchs.pop();
            }

        },
        // 画布的长按手势响应
        longtap: function (e) {
            // console.log("长按手势" + e);
        },
        cancel() { },
        error: function (e) {
            // console.log("画布触摸错误" + e);
        },
        //绘制
        draw: function (touchs) {
            let point1 = touchs[0];
            let point2 = touchs[1];
            // console.info(point1, point2)
            touchs.shift();
            ctx.moveTo(point1.x, point1.y);
            ctx.lineTo(point2.x, point2.y);
            ctx.stroke();
            ctx.draw(true);
        },
        //清除操作
        clearClick: function () {
            //清除画布
            ctx.clearRect(0, 0, 750, 700);
            ctx.draw(true);
            this.setData({
                count: 0
            })
        },
        //保存图片
        saveClick: function (callback) {
            if (this.data.count < 3) {
                wx.showModal({
                    title: '提示',
                    content: '签名不符合规范,请重新签名',
                    showCancel: false
                })
            } else {
                wx.canvasToTempFilePath({
                    canvasId: 'autograph',
                    fileType: 'jpg',
                    destWidth: 800,
                    destHeight: 500,
                    success: function (res) {
                        callback && callback(res.tempFilePath) //把图片路径回调出去
                    },
                    fail(res) {
                        console.info('保存失败', res)
                    }
                }, this)
            }

        }
    }
})

在父组件内的使用方式

this.selectComponent("#autograph").saveClick(function (tempFilePath) {
           that.creatMerchant(tempFilePath)

       })