Jimmy

Jimmy 查看完整档案

南京编辑南京邮电大学  |  软件工程嵌入式(NIIT) 编辑  |  填写所在公司/组织 www.codenoter.com 编辑
编辑

穷则独善其身,达则兼济天下。QQ:370555337,微信公众号:虞城工作室

个人动态

Jimmy 收藏了文章 · 2019-05-06

小程序如何生成海报分享朋友圈

项目需求写完有一段时间了,但是还是想回过来总结一下,一是对项目的回顾优化等,二是对坑的地方做个记录,避免以后遇到类似的问题。

需求

利用微信强大的社交能力通过小程序达到裂变的目的,拉取新用户。
生成的海报如下

clipboard.png

需求分析

1、利用小程序官方提供的api可以直接分享转发到微信群打开小程序
2、利用小程序生成海报保存图片到相册分享到朋友圈,用户长按识别二维码关注公众号或者打开小程序来达到裂变的目的

实现方案

一、分析如何实现

相信大家应该都会有类似的迷惑,就是如何按照产品设计的那样绘制成海报,其实当时我也是不知道如何下手,认真想了下得通过canvas绘制成图片,这样用户保存这个图片到相册,就可以分享到朋友圈了。但是要绘制的图片上面不仅有文字还有数字、图片、二维码等且都是活的,这个要怎么动态生成呢。认真想了下,需要一点一点的将文字和数字,背景图绘制到画布上去,这样通过api最终合成一个图片导出到手机相册中。

二、需要解决的问题

1、二维码的动态获取和绘制(包括如何生成小程序二维码、公众号二维码、打开网页二维码)
2、背景图如何绘制,获取图片信息
3、将绘制完成的图片保存到本地相册
4、处理用户是否取消授权保存到相册

三、实现步骤

这里我具体写下围绕上面所提出的问题,描述大概实现的过程

①首先创建canvas画布,我把画布定位设成负的,是为了不让它显示在页面上,是因为我尝试把canvas通过判断条件动态的显示和隐藏,在绘制的时候会出现问题,所以采用了这种方法,这里还有一定要设置画布的大小。

<canvas canvas-id="myCanvas" style="width: 690px;height:1085px;position: fixed;top: -10000px;"></canvas>

②创建好画布之后,先绘制背景图,因为背景图我是放在本地,所以获取 <canvas> 组件 canvas-id 属性,通过createCanvasContext创建canvas的绘图上下文 CanvasContext 对象。使用drawImage绘制图像到画布,第一个参数是图片的本地地址,后面两个参数是图像相对画布左上角位置的x轴和y轴,最后两个参数是设置图像的宽高。

const ctx = wx.createCanvasContext('myCanvas')

ctx.drawImage('/img/study/shareimg.png', 0, 0, 690, 1085)

③创建好背景图后,在背景图上绘制头像,文字和数字。通过getImageInfo获取头像的信息,这里需要注意下在获取的网络图片要先配置download域名才能生效,具体在小程序后台设置里配置。

获取头像地址,首先量取头像在画布中的大小,和x轴Y轴的坐标,这里的result[0]是我用promise封装返回的一个图片地址

let headImg = new Promise(function (resolve) {
        wx.getImageInfo({
          src: `${app.globalData.baseUrl2}${that.data.currentChildren.headImg}`,
          success: function (res) {
            resolve(res.path)
          },
          fail: function (err) {
            console.log(err)
            wx.showToast({
              title: '网络错误请重试',
              icon: 'loading'
            })
          }
        })
      })
      
let avatarurl_width = 60, //绘制的头像宽度
    avatarurl_heigth = 60, //绘制的头像高度
    avatarurl_x = 28, //绘制的头像在画布上的位置
    avatarurl_y = 36; //绘制的头像在画布上的位置
    
    ctx.save(); // 先保存状态 已便于画完圆再用
    ctx.beginPath(); //开始绘制
    //先画个圆   前两个参数确定了圆心 (x,y) 坐标  第三个参数是圆的半径  四参数是绘图方向  默认是false,即顺时针
    ctx.arc(avatarurl_width / 2 + avatarurl_x, avatarurl_heigth / 2 + avatarurl_y, avatarurl_width / 2, 0, Math.PI * 2, false);
    ctx.clip(); //画了圆 再剪切  原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内
    ctx.drawImage(result[0], avatarurl_x, avatarurl_y, avatarurl_width, avatarurl_heigth); // 推进去图片
    
这里举个例子说下如何绘制文字,比如我要绘制如下这个“字”,需要动态获取前面字数的总宽度,这样才能设置“字”的x轴坐标,这里我本来是想通过measureText来测量字体的宽度,但是在iOS端第一次获取的宽度值不对,关于这个问题,我还在微信开发者社区提了bug,所以我想用另一个方法来实现,就是先获取正常情况下一个字的宽度值,然后乘以总字数就获得了总宽度,亲试是可以的。

clipboard.png

let allReading = 97 / 6 / app.globalData.ratio * wordNumber.toString().length + 325;
ctx.font = 'normal normal 30px sans-serif';
ctx.setFillStyle('#ffffff')
ctx.fillText('字', allReading, 150);

④绘制公众号二维码,和获取头像是一样的,也是先通过接口返回图片网络地址,然后再通过getImageInfo获取公众号二维码图片信息

⑤如何绘制小程序码,具体官网文档也给出生成无限小程序码接口,通过生成的小程序可以打开任意一个小程序页面,并且二维码永久有效,具体调用哪个小程序二维码接口有不同的应用场景,具体可以看下官方文档怎么说的,也就是说前端通过传递参数调取后端接口返回的小程序码,然后绘制在画布上(和上面写的绘制头像和公众号二维码一样的)

ctx.drawImage('小程序码的本地地址', x轴, Y轴, 宽, 高)

⑥最终绘制完把canvas画布转成图片并返回图片地址

        wx.canvasToTempFilePath({
            canvasId: 'myCanvas',
            success: function (res) {
              canvasToTempFilePath = res.tempFilePath // 返回的图片地址保存到一个全局变量里
              that.setData({
                showShareImg: true
              })
              wx.showToast({
                title: '绘制成功',
              })
            },
            fail: function () {
              wx.showToast({
                title: '绘制失败',
              })
            },
            complete: function () {
              wx.hideLoading()
              wx.hideToast()
            }
          })

⑦保存到系统相册;先判断用户是否开启用户授权相册,处理不同情况下的结果。比如用户如果按照正常逻辑授权是没问题的,但是有的用户如果点击了取消授权该如何处理,如果不处理会出现一定的问题。所以当用户点击取消授权之后,来个弹框提示,当它再次点击的时候,主动跳到设置引导用户去开启授权,从而达到保存到相册分享朋友圈的目的。

// 获取用户是否开启用户授权相册
    if (!openStatus) {
      wx.openSetting({
        success: (result) => {
          if (result) {
            if (result.authSetting["scope.writePhotosAlbum"] === true) {
              openStatus = true;
              wx.saveImageToPhotosAlbum({
                filePath: canvasToTempFilePath,
                success() {
                  that.setData({
                    showShareImg: false
                  })
                  wx.showToast({
                    title: '图片保存成功,快去分享到朋友圈吧~',
                    icon: 'none',
                    duration: 2000
                  })
                },
                fail() {
                  wx.showToast({
                    title: '保存失败',
                    icon: 'none'
                  })
                }
              })
            }
          }
        },
        fail: () => { },
        complete: () => { }
      });
    } else {
      wx.getSetting({
        success(res) {
          // 如果没有则获取授权
          if (!res.authSetting['scope.writePhotosAlbum']) {
            wx.authorize({
              scope: 'scope.writePhotosAlbum',
              success() {
                openStatus = true
                wx.saveImageToPhotosAlbum({
                  filePath: canvasToTempFilePath,
                  success() {
                    that.setData({
                      showShareImg: false
                    })
                    wx.showToast({
                      title: '图片保存成功,快去分享到朋友圈吧~',
                      icon: 'none',
                      duration: 2000
                    })
                  },
                  fail() {
                    wx.showToast({
                      title: '保存失败',
                      icon: 'none'
                    })
                  }
                })
              },
              fail() {
                // 如果用户拒绝过或没有授权,则再次打开授权窗口
                openStatus = false
                console.log('请设置允许访问相册')
                wx.showToast({
                  title: '请设置允许访问相册',
                  icon: 'none'
                })
              }
            })
          } else {
            // 有则直接保存
            openStatus = true
            wx.saveImageToPhotosAlbum({
              filePath: canvasToTempFilePath,
              success() {
                that.setData({
                  showShareImg: false
                })
                wx.showToast({
                  title: '图片保存成功,快去分享到朋友圈吧~',
                  icon: 'none',
                  duration: 2000
                })
              },
              fail() {
                wx.showToast({
                  title: '保存失败',
                  icon: 'none'
                })
              }
            })
          }
        },
        fail(err) {
          console.log(err)
        }
      })
    }

总结

至此所有的步骤都已实现,在绘制的时候会遇到一些异步请求后台返回的数据,所以我用promise和async和await进行了封装,确保导出的图片信息是完整的。在绘制的过程确实遇到一些坑的地方。比如初开始导出的图片比例大小不对,还有用measureText测量文字宽度不对,多次绘制(可能受网络原因)有时导出的图片上的文字颜色会有误差等。如果你也遇到一些比较坑的地方可以一起探讨下做个记录,下面附下完整的代码
import regeneratorRuntime from '../../utils/runtime.js' // 引入模块
const app = getApp(),
  api = require('../../service/http.js');
var ctx = null, // 创建canvas对象
    canvasToTempFilePath = null, // 保存最终生成的导出的图片地址
    openStatus = true; // 声明一个全局变量判断是否授权保存到相册

// 获取微信公众号二维码
  getCode: function () {
    return new Promise(function (resolve, reject) {
      api.fetch('/wechat/open/getQRCodeNormal', 'GET').then(res => {
        console.log(res, '获取微信公众号二维码')
        if (res.code == 200) {
          console.log(res.content, 'codeUrl')
          resolve(res.content)
        }
      }).catch(err => {
        console.log(err)
      })
    })
  },

  // 生成海报
  async createCanvasImage() {
    let that = this;
    // 点击生成海报数据埋点
    that.setData({
      generateId: '点击生成海报'
    })
    if (!ctx) {
      let codeUrl = await that.getCode()
      wx.showLoading({
        title: '绘制中...'
      })
      let code = new Promise(function (resolve) {
        wx.getImageInfo({
          src: codeUrl,
          success: function (res) {
            resolve(res.path)
          },
          fail: function (err) {
            console.log(err)
            wx.showToast({
              title: '网络错误请重试',
              icon: 'loading'
            })
          }
        })
      })
      let headImg = new Promise(function (resolve) {
        wx.getImageInfo({
          src: `${app.globalData.baseUrl2}${that.data.currentChildren.headImg}`,
          success: function (res) {
            resolve(res.path)
          },
          fail: function (err) {
            console.log(err)
            wx.showToast({
              title: '网络错误请重试',
              icon: 'loading'
            })
          }
        })
      })

      Promise.all([headImg, code]).then(function (result) {
        const ctx = wx.createCanvasContext('myCanvas')
        console.log(ctx, app.globalData.ratio, 'ctx')
        let canvasWidthPx = 690 * app.globalData.ratio,
          canvasHeightPx = 1085 * app.globalData.ratio,
          avatarurl_width = 60, //绘制的头像宽度
          avatarurl_heigth = 60, //绘制的头像高度
          avatarurl_x = 28, //绘制的头像在画布上的位置
          avatarurl_y = 36, //绘制的头像在画布上的位置
          codeurl_width = 80, //绘制的二维码宽度
          codeurl_heigth = 80, //绘制的二维码高度
          codeurl_x = 588, //绘制的二维码在画布上的位置
          codeurl_y = 984, //绘制的二维码在画布上的位置
          wordNumber = that.data.wordNumber, // 获取总阅读字数
          // nameWidth = ctx.measureText(that.data.wordNumber).width, // 获取总阅读字数的宽度
          // allReading = ((nameWidth + 375) - 325) * 2 + 380;
          // allReading = nameWidth / app.globalData.ratio + 325;
          allReading = 97 / 6 / app.globalData.ratio * wordNumber.toString().length + 325;
        console.log(wordNumber, wordNumber.toString().length, allReading, '获取总阅读字数的宽度')
        ctx.drawImage('/img/study/shareimg.png', 0, 0, 690, 1085)
        ctx.save(); // 先保存状态 已便于画完圆再用
        ctx.beginPath(); //开始绘制
        //先画个圆   前两个参数确定了圆心 (x,y) 坐标  第三个参数是圆的半径  四参数是绘图方向  默认是false,即顺时针
        ctx.arc(avatarurl_width / 2 + avatarurl_x, avatarurl_heigth / 2 + avatarurl_y, avatarurl_width / 2, 0, Math.PI * 2, false);
        ctx.clip(); //画了圆 再剪切  原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内
        ctx.drawImage(result[0], avatarurl_x, avatarurl_y, avatarurl_width, avatarurl_heigth); // 推进去图片

        ctx.restore(); //恢复之前保存的绘图上下文状态 可以继续绘制
        ctx.setFillStyle('#ffffff'); // 文字颜色
        ctx.setFontSize(28); // 文字字号
        ctx.fillText(that.data.currentChildren.name, 103, 78); // 绘制文字

        ctx.font = 'normal bold 44px sans-serif';
        ctx.setFillStyle('#ffffff'); // 文字颜色
        ctx.fillText(wordNumber, 325, 153); // 绘制文字

        ctx.font = 'normal normal 30px sans-serif';
        ctx.setFillStyle('#ffffff')
        ctx.fillText('字', allReading, 150);

        ctx.font = 'normal normal 24px sans-serif';
        ctx.setFillStyle('#ffffff'); // 文字颜色
        ctx.fillText('打败了全国', 26, 190); // 绘制文字

        ctx.font = 'normal normal 24px sans-serif';
        ctx.setFillStyle('#faed15'); // 文字颜色
        ctx.fillText(that.data.percent, 154, 190); // 绘制孩子百分比

        ctx.font = 'normal normal 24px sans-serif';
        ctx.setFillStyle('#ffffff'); // 文字颜色
        ctx.fillText('的小朋友', 205, 190); // 绘制孩子百分比

        ctx.font = 'normal bold 32px sans-serif';
        ctx.setFillStyle('#333333'); // 文字颜色
        ctx.fillText(that.data.singIn, 50, 290); // 签到天数

        ctx.fillText(that.data.reading, 280, 290); // 阅读时长
        ctx.fillText(that.data.reading, 508, 290); // 听书时长

        // 书籍阅读结构
        ctx.font = 'normal normal 28px sans-serif';
        ctx.setFillStyle('#ffffff'); // 文字颜色
        ctx.fillText(that.data.bookInfo[0].count, 260, 510); 
        ctx.fillText(that.data.bookInfo[1].count, 420, 532); 
        ctx.fillText(that.data.bookInfo[2].count, 520, 594); 
        ctx.fillText(that.data.bookInfo[3].count, 515, 710); 
        ctx.fillText(that.data.bookInfo[4].count, 492, 828); 
        ctx.fillText(that.data.bookInfo[5].count, 348, 858); 
        ctx.fillText(that.data.bookInfo[6].count, 212, 828); 
        ctx.fillText(that.data.bookInfo[7].count, 148, 726); 
        ctx.fillText(that.data.bookInfo[8].count, 158, 600); 

        ctx.font = 'normal normal 18px sans-serif';
        ctx.setFillStyle('#ffffff'); // 文字颜色
        ctx.fillText(that.data.bookInfo[0].name, 232, 530); 
        ctx.fillText(that.data.bookInfo[1].name, 394, 552); 
        ctx.fillText(that.data.bookInfo[2].name, 496, 614); 
        ctx.fillText(that.data.bookInfo[3].name, 490, 730); 
        ctx.fillText(that.data.bookInfo[4].name, 466, 850); 
        ctx.fillText(that.data.bookInfo[5].name, 323, 878); 
        ctx.fillText(that.data.bookInfo[6].name, 184, 850); 
        ctx.fillText(that.data.bookInfo[7].name, 117, 746); 
        ctx.fillText(that.data.bookInfo[8].name, 130, 621); 

        ctx.drawImage(result[1], codeurl_x, codeurl_y, codeurl_width, codeurl_heigth); // 绘制头像
        ctx.draw(false, function () {
          // canvas画布转成图片并返回图片地址
          wx.canvasToTempFilePath({
            canvasId: 'myCanvas',
            success: function (res) {
              canvasToTempFilePath = res.tempFilePath
              that.setData({
                showShareImg: true
              })
              console.log(res.tempFilePath, 'canvasToTempFilePath')
              wx.showToast({
                title: '绘制成功',
              })
            },
            fail: function () {
              wx.showToast({
                title: '绘制失败',
              })
            },
            complete: function () {
              wx.hideLoading()
              wx.hideToast()
            }
          })
        })
      })
    }
  },

  // 保存到系统相册
  saveShareImg: function () {
    let that = this;
    // 数据埋点点击保存学情海报
    that.setData({
      saveId: '保存学情海报'
    })
    // 获取用户是否开启用户授权相册
    if (!openStatus) {
      wx.openSetting({
        success: (result) => {
          if (result) {
            if (result.authSetting["scope.writePhotosAlbum"] === true) {
              openStatus = true;
              wx.saveImageToPhotosAlbum({
                filePath: canvasToTempFilePath,
                success() {
                  that.setData({
                    showShareImg: false
                  })
                  wx.showToast({
                    title: '图片保存成功,快去分享到朋友圈吧~',
                    icon: 'none',
                    duration: 2000
                  })
                },
                fail() {
                  wx.showToast({
                    title: '保存失败',
                    icon: 'none'
                  })
                }
              })
            }
          }
        },
        fail: () => { },
        complete: () => { }
      });
    } else {
      wx.getSetting({
        success(res) {
          // 如果没有则获取授权
          if (!res.authSetting['scope.writePhotosAlbum']) {
            wx.authorize({
              scope: 'scope.writePhotosAlbum',
              success() {
                openStatus = true
                wx.saveImageToPhotosAlbum({
                  filePath: canvasToTempFilePath,
                  success() {
                    that.setData({
                      showShareImg: false
                    })
                    wx.showToast({
                      title: '图片保存成功,快去分享到朋友圈吧~',
                      icon: 'none',
                      duration: 2000
                    })
                  },
                  fail() {
                    wx.showToast({
                      title: '保存失败',
                      icon: 'none'
                    })
                  }
                })
              },
              fail() {
                // 如果用户拒绝过或没有授权,则再次打开授权窗口
                openStatus = false
                console.log('请设置允许访问相册')
                wx.showToast({
                  title: '请设置允许访问相册',
                  icon: 'none'
                })
              }
            })
          } else {
            // 有则直接保存
            openStatus = true
            wx.saveImageToPhotosAlbum({
              filePath: canvasToTempFilePath,
              success() {
                that.setData({
                  showShareImg: false
                })
                wx.showToast({
                  title: '图片保存成功,快去分享到朋友圈吧~',
                  icon: 'none',
                  duration: 2000
                })
              },
              fail() {
                wx.showToast({
                  title: '保存失败',
                  icon: 'none'
                })
              }
            })
          }
        },
        fail(err) {
          console.log(err)
        }
      })
    }
  },
查看原文

Jimmy 收藏了文章 · 2019-01-31

半小时撸一个抽奖程序

需求总是很紧急,昨天正在开会收到人力需求,有时间做个抽奖吗?(now 下午四点12,年会五点开始。)还没能等我拒绝,人事又补了一句做不出来我们就不抽奖了,我擦瞬间感觉要是搞不出来会被兄弟们捅死的节奏,默默的删除了没时间做的消息,重新写了四个字名单给我。

还好去年前年都是我搞得很庆幸没被当场打脸,重启去年程序(需要收集全员头像并ps)时间显然不够,庆幸的是还有点经验,会议结束时间已经四点半了。

好了不扯淡了开始干活吧!

先屡一下思路

1、好看是好看不了了,别指望没设计没时间程序员搞出来的效果。
2、样式开始按钮、停止按钮、人员名单别列表、抽中名单列表。
3、点击开始,首先乱序名单列表保证每次抽奖列表顺序不一样,防止他们怀疑我作弊搞权重(就TM半小时哪有时间搞权重)时间紧任务重,直接用的lodash shuffle方法来乱序视图
4、随机数是肯定要有的,每隔200ms随机一个从0到人员个数(数组长度随机整数)
5、抽中人员和没抽中人员分两个数组存入localStorage,防止抽奖过程中刷新页面,纯静态不存本地那场面就尴尬了每次刷新完如果本次存储了从本地获取人员列表和中奖名单
6、点击end选中当前随机数在页面上高亮显示。

接下来看整体实现代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>2019抽奖程序</title>
    <script data-original="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script data-original="https://lib.baomitu.com/lodash.js/4.14.1/lodash.min.js"></script>
    <style>
        * {
            margin: 0;
            padding: 0;
        }
        
        .list-complete-item {
            transition: all 1s;
            display: inline-block;
            border: 1px solid #ccc;
            width: 80px;
            height: 80px;
            line-height: 80px;
            text-align: center
        }
        
        .draw-bg {
            background-color: red;
            transform: scale(1.5)
        }
        
        .list-complete-enter,
        .list-complete-leave-to {
            opacity: 0;
            transform: translateY(30px);
        }
        
        .list-complete-leave-active {
            position: absolute;
        }
        
        .draw {
            height: 100px;
        }
        
        button {
            padding: 5px 10px;
            margin: 20px;
        }
        
        li {
            float: left
        }
        
        .draw-list span {
            display: inline-block;
            padding: 10px;
            background: red;
            color: #fff
        }
    </style>

</head>

<body>
    <div id="list-complete-demo" class="demo">
        <button v-on:click="start">start</button>
        <button v-on:click="end">end</button>
        <div class="draw-list">
            <span v-for="item in target">{{item}}</span>
        </div>
        <transition-group name="list-complete" tag="p">
            <span v-for="item in arrList" v-bind:key="item" class="list-complete-item" :class="{'draw-bg': item == value}">
                {{ item }}
            </span>
        </transition-group>
    </div>
    <script>
        new Vue({
            el: '#list-complete-demo',
            data: {
                arrList: [
                    "张三",
                    "李四",
                    "王五",
                    "赵六",
                    "陈七",
                    "张扒",
                    "李十四",
                    "王十五",
                    "赵十六",
                    "陈十七",
                    "张二三",
                    "李二四",
                    "王二五",
                    "赵二六",
                    "陈二七",
                    "张二扒",
                    "李三四",
                    "王三五",
                    "赵三六",
                    "陈三七"
                ],
                target: [],
                index: -1,
                timer: null,
                value: '',
                status: true
            },
            mounted() {
                if (!localStorage.getItem('initData')) {
                    localStorage.setItem('initData', JSON.stringify(this.arrList))
                } else {
                    this.arrList = JSON.parse(localStorage.getItem('initData'))
                }
                if (localStorage.getItem('drawList')) {
                    this.target = JSON.parse(localStorage.getItem('drawList'))
                }

            },
            methods: {
                start() {
                    if (this.status) {
                        if (this.index != -1) {
                            this.arrList.splice(this.index, 1)
                            localStorage.setItem('initData', JSON.stringify(this.arrList))
                        }
                        this.shuffle()
                        setTimeout(() => {
                            this.recursive()
                        }, 800)
                        this.status = !this.status
                    }
                },
                randomIndex: function() {
                    this.index = Math.floor(Math.random() * this.arrList.length)
                    return this.index
                },
                remove: function() {
                    this.arrList.splice(this.randomIndex(), 1)
                },
                shuffle: function() {
                    this.arrList = _.shuffle(this.arrList)
                },
                recursive() {
                    clearTimeout(this.timer)
                    this.timer = setTimeout(() => {
                        this.value = this.arrList[this.randomIndex()]
                        this.recursive()
                    }, 200)
                },
                end() {
                    if (this.status) {
                        return
                    }
                    clearTimeout(this.timer)
                    this.index = this.randomIndex()
                    this.value = this.arrList[this.index]
                    this.target.push(this.value)
                    localStorage.setItem('drawList', JSON.stringify(this.target))
                    this.status = !this.status
                }
            }
        })
    </script>
</body>

</html>

体验下效果
Kapture-2019-01-30-at-16.06.34.gif

需求搞定,经现场测试目前没发现什么问题!如有疑问随时回复留言!

查看原文

Jimmy 发布了文章 · 2019-01-30

Mac下npm报错

1、今天在Mac下运行npm命令时不知什么原因报错如下:

☁  myhexo_blog [master] ⚡ node -v                            
dyld: Library not loaded: /usr/local/opt/icu4c/lib/libicui18n.59.dylib
  Referenced from: /usr/local/bin/node
  Reason: image not found
[1]    33278 abort      node -v

2、解决方案:

☁  myhexo_blog [master] ⚡ brew uninstall --force node 
Uninstalling node... (4,152 files, 47.9MB)
☁  myhexo_blog [master] ⚡ brew uninstall icu4c && brew install icu4c 
Error: Refusing to uninstall /usr/local/Cellar/icu4c/63.1
because it is required by php and php@7.1, which are currently installed.
You can override this and force removal with:
  brew uninstall --ignore-dependencies icu4c
☁  myhexo_blog [master] ⚡  brew unlink icu4c && brew link icu4c –force
Unlinking /usr/local/Cellar/icu4c/63.1... 0 symlinks removed
Error: No such keg: /usr/local/Cellar/–force
☁  myhexo_blog [master] ⚡ brew install node
Updating Homebrew...
==> Downloading https://homebrew.bintray.com/bottles/node-11.8.0.mojave.bottle.t
######################################################################## 100.0%
==> Pouring node-11.8.0.mojave.bottle.tar.gz
==> Caveats
Bash completion has been installed to:
  /usr/local/etc/bash_completion.d
==> Summary
🍺  /usr/local/Cellar/node/11.8.0: 3,938 files, 47.4MB

3、之后运行node -v显示如下:

☁  myhexo_blog [master] ⚡ node -v
v11.8.0

问题解决~ happy hacking...

查看原文

赞 0 收藏 0 评论 0

Jimmy 收藏了文章 · 2019-01-22

记一次雪花效果

前言

最近,公司UI小姐姐告诉我能不能做一个关于雪花的效果图,最好是能体现雪花的远近感(远的时候比较小 近的时候雪花比较大),我寻思良久,一开始用canvas做了一个雪花效果 感觉不是很满意,然后就该用了three.js做了一个关于雪花的效果。效果还行 给大家先看一下效果。

雪花整体效果

1:准备工作

为了能够显示任何带有three.js的东西,我们需要三件事:场景,相机和渲染器,这样我们就可以用相机渲染场景
代码:

function init() {
  container = document.createElement('div');
  container.className = 'snow';
    document.body.appendChild(container);
    camera = new THREE.PerspectiveCamera( 75, SCREEN_WIDTH / SCREEN_HEIGHT, 1, 10000 ); //透视投影相机
    camera.position.z = 1000;
    scene = new THREE.Scene();
    scene.add(camera);
    renderer = new THREE.CanvasRenderer();
    renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT);
    // console.log(SCREEN_WIDTH, SCREEN_HEIGHT);
    var material = new THREE.ParticleBasicMaterial( { map: new THREE.Texture(particleImage) } );

2:随机生成不同位置一定数量的雪花

for (var i = 0; i < 200; i++) {
        particle = new Particle3D( material);
        particle.position.x = Math.random() * 2000 - 1000;
        particle.position.y = Math.random() * 2000 - 1000;
        particle.position.z = Math.random() * 2000 - 1000;
        particle.scale.x = particle.scale.y =  1;
        scene.add( particle );
        particles.push(particle); 
    }

进行雪花位置优化

for(var i = 0; i < particles.length; i++)
    {
        var particle = particles[i]; 
        particle.updatePhysics(); 
        with(particle.position)
        {
            if(y < -1000) y += 2000; 
            if(x > 1000) x -= 2000; 
            else if(x <- 1000) x += 2000; 
            if(z > 1000) z -= 2000; 
            else if(z <- 1000) z += 2000; 
        }
    }

3:雪花远小近大的效果

雪花的远小近大的效果是通过改变相机的位置来的

    camera.position.x += (mouseX - camera.position.x ) * 0.05;
    camera.position.y += (- mouseY - camera.position.y ) * 0.05;
    camera.lookAt(scene.position); 
    renderer.render( scene, camera );

4:雪花的自由下落

这里是利用了gravity重力,让他下落的,我们也可以通过改变它的大小来改变速度。

Particle3D = function(material){
    THREE.Particle.call(this,material);
    this.velocity = new THREE.Vector3(0,-8,0);
    this.velocity.rotateX(randomRange(-45,45));
    this.velocity.rotateY(randomRange(0,360));
    this.gravity = new THREE.Vector3(0,0,0);
    this.drag=1;
};
Particle3D.prototype = new THREE.Particle();
Particle3D.prototype.constructor = Particle3D;
Particle3D.prototype.updatePhysics = function(){
    this.velocity.multiplyScalar(this.drag);
    this.velocity.addSelf(this.gravity);
    this.position.addSelf(this.velocity);
}

5:雪花旋转效果

至于雪花的旋转我也做了一定的优化

THREE.Vector3.prototype.rotateY=function(angle){
    cosRY = Math.cos(angle * Math.PI/180);
    sinRY = Math.sin(angle * Math.PI/180);
    var tempz = this.z;;
    var tempx = this.x;
    this.x = (tempx * cosRY) + (tempz * sinRY);
    this.z = (tempx * -sinRY) + (tempz * cosRY);
}

活动页

活动页效果就不细细说了,我就把雪花效果添加进去活动页。使用pointer-events:none,表示它将捕获不到任何点击,而只是让事件穿透到它的下面。

 .snow {
        position: fixed;
        top: 0;
        left: 0;
        z-index: 10000;
        transform: translate3d(0, 0, 0);
        width: 100%;
        height: 100%;
        pointer-events: none;
      }

其他

这个活动页还有一个问题,就是按住屏幕(往下轻滑),雪就卡住了一小会希望有大佬来帮我解决这个问题。鄙人不胜感激。

总结

作为一个即将毕业的大四学生,这是我来公司实习做的第一个活动页,希望可以帮助大家,互相学习,一起进步。当然也有一些不足之处,请大家多多指教。如果大家有什么好的想法的话可以联系我的qq:137032979.码字不容易,希望大家点个赞。前端路漫漫,与君共勉之。

查看原文

Jimmy 收藏了文章 · 2018-03-19

nginx 配置多 域名 + 多 https

最近项目要配置nginx多域名加https,刚好可以学习学习如何配置?之前配置了nginx+https但是没有加多域名,然后在网上搜索了一下如何使用,总结如下,分享一下。

1、nginx.conf配置

首先我们进入到nginx的配置文件nginx.conf文件,修改成如下代码:

服务器路径:/usr/lcoal/nginx/conf/nginx.conf

server {
        listen       80;
        server_name  www.qitenai.com qitenai.com;
        return       301 https://www.qitenai.com$request_uri;redirect http to https

        location / {
            root   /data/wwwroot/dist;
            try_files $uri $uri/ /index.html;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
}

值得注意的是,nginx.conf配置文件设置了

include /usr/local/nginx/conf/custom/*.conf

所以,在custom文件夹下我们可以添加自定义文件,如我的域名配置文件:qitenai.com.conf

2、qitenai.com.conf配置

服务器路径:/usr/lcoal/nginx/conf/custom/qitenai.com.conf

server {
    listen 443 ssl;
    server_name www.qitenai.com qitenai.com;
    ssl_certificate   /usr/local/nginx/cert/qitenai.com/214474132640003.pem;
    ssl_certificate_key  /usr/local/nginx/cert/qitenai.com/myserver.key;
    location / {
        root   /data/wwwroot/dist;
        #index  index.html index.htm;
        try_files $uri $uri/ /index.html;
    }
}

紧接着我们来配置第二个域名:hxc100.com.config,代码如下:

3、hxc100.com.conf配置

服务器路径:/usr/lcoal/nginx/conf/custom/hxc100.com.conf

   server {
        listen       80;
        server_name  www.hxc100.com hxc100.com;
        return       301 https://www.hxc100.com$request_uri;
        location / {
            root   /data/wwwroot/dist;
            try_files $uri $uri/ /index.html;
        }
    }

   server {
        listen       443 ssl;
        server_name  www.hxc100.com hxc100.com;
        ssl_certificate      /usr/local/nginx/cert/hxc100.com/214478868080003.pem;
        ssl_certificate_key  /usr/local/nginx/cert/hxc100.com/214478868080003.key;

        location / {
            root   /data/wwwroot/dist;
            try_files $uri $uri/ /index.html;
        }
    }

最后,我们重启下nginx,我们使用的是自动化脚本来重启,代码如下:

#!/bin/bash
fuser -k 80/tcp

if [ $? -eq 0 ]
   then
        echo "正在启动nginx..."
        /usr/local/nginx/sbin/nginx
        if [ $? -eq 0 ]
                then
                    echo "启动成功!"
        fi
fi

启动成功后,我们分别在浏览器中输入:qitenai.com和hxc100.com,分别观察是否已经设置成功,如下所示,我们已经设置成功!
clipboard.png

clipboard.png

查看原文

Jimmy 发布了文章 · 2018-03-16

python爬取QQ空间说说并生成词云

原理是利用python来模拟登陆QQ空间,对一个QQ的空间说说内容进行爬取,把爬取的内容保存在txt文件中,然后根据txt文件生成词云。

以下是生成的词云图

我的环境:Mac,Anaconda,Python2.7,以及各种用到的Python

先来说下Anaconda

Anaconda 是一个可用于科学计算的 Python 发行版,支持 Linux、Mac、Windows系统,内置了常用的科学计算包。它解决了官方 Python 的两大痛点。

  • 第一:提供了包管理功能,Windows 平台安装第三方包经常失败的场景得以解决,
  • 第二:提供环境管理的功能,功能类似 Virtualenv,解决了多版本Python并存、切换的问题。

conda 是 Anaconda 下用于包管理和环境管理的工具,功能上类似 pip 和 vitualenv 的组合。安装成功后 conda 会默认加入到环境变量中,因此可直接在命令行窗口运行命令 conda

conda 的环境管理与 virtualenv 是基本上是类似的操作。

# 查看帮助
conda -h 
# 基于python3.6版本创建一个名字为python36的环境
conda create --name python36 python=3.6 
# 激活此环境
source activate python36 # linux/mac
# 再来检查python版本,显示是 3.6
python -V  
# 退出当前环境
source deactivate python36 
# 删除该环境
conda remove -n python36 --all
# 查看所以安装的环境
conda info -e

conda 的包管理功能可 pip 是一样的,当然你选择 pip 来安装包也是没问题的。

# 安装 matplotlib 
conda install matplotlib
# 查看已安装的包
conda list 
# 包更新
conda update matplotlib
# 删除包
conda remove matplotlib

在 conda 中 anything is a package。conda 本身可以看作是一个包,python 环境可以看作是一个包,anaconda 也可以看作是一个包,因此除了普通的第三方包支持更新之外,这3个包也支持。比如:
Anaconda 的镜像地址默认在国外,用 conda 安装包的时候会很慢,目前可用的国内镜像源地址有清华大学的。修改 ~/.condarc (Linux/Mac)

channels:
 - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
 - defaults
show_channel_urls: true

如果使用conda安装包的时候还是很慢,那么可以考虑使用pip来安装,同样把 pip 的镜像源地址也改成国内的,豆瓣源速度比较快。修改 ~/.pip/pip.conf (Linux/Mac)

[global]
trusted-host =  pypi.douban.com
index-url = http://pypi.douban.com/simple

环境搭建好之后就可以开始愉快地玩数据分析了。

爬取动态内容

  1. 因为动态页面的内容是动态加载出来的,所以我们需要不断下滑,加载页面
  2. 切换到当前内容的frame中,也有可能不是frame,这里需要查看具体情况
  3. 获取页面源数据,然后放入xpath中,然后读取
  # 下拉滚动条,使浏览器加载出动态加载的内容,
  # 我这里是从1开始到6结束 分5 次加载完每页数据
        for i in range(1,6):
            height = 20000*i#每次滑动20000像素
            strWord = "window.scrollBy(0,"+str(height)+")"
            driver.execute_script(strWord)
            time.sleep(4)

        # 很多时候网页由多个<frame>或<iframe>组成,webdriver默认定位的是最外层的frame,
        # 所以这里需要选中一下说说所在的frame,否则找不到下面需要的网页元素
        driver.switch_to.frame("app_canvas_frame")
        selector = etree.HTML(driver.page_source)
        divs = selector.xpath('//*[@id="msgList"]/li/div[3]')

生成词云

生成词云需要用到的库:

  1. wordcloud, 生成词云
  2. matplotlib, 生成词云图片
  3. jieba,显示中文。
#coding:utf-8

from wordcloud import WordCloud
import matplotlib.pyplot as plt
import jieba

#生成词云
def create_word_cloud(filename):
    text= open("{}.txt".format(filename)).read()
    # 结巴分词
    wordlist = jieba.cut(text, cut_all=True)
    wl = " ".join(wordlist)

    # 设置词云
    wc = WordCloud(
        # 设置背景颜色
       background_color="white",
         # 设置最大显示的词云数
       max_words=2000,
         # 这种字体都在电脑字体中,一般路径
       font_path='/System/Library/Fonts/PingFang.ttc',
       height= 1200,
       width= 1600,
        # 设置字体最大值
       max_font_size=100,
     # 设置有多少种随机生成状态,即有多少种配色方案
       random_state=30,
    )

    myword = wc.generate(wl)  # 生成词云
    # 展示词云图
    plt.imshow(myword)
    plt.axis("off")
    plt.show()
    wc.to_file('py_book.png')  # 把词云保存下

if __name__ == '__main__':
    create_word_cloud('qq_word')

所有完整代码已放github

github地址https://github.com/Jimmy9876/...

参考:
https://foofish.net/anaconda-...

查看原文

赞 1 收藏 1 评论 0

Jimmy 收藏了文章 · 2018-03-08

Linux Crontab之每天八点发短信给女朋友

都说程序猿没有女朋友=_=,汗,为什么要黑我们帅气的程序猿一族,今天来搞一波用Linux的Crontab定时任务每天给女朋友发短信

在这里我用的是阿里大于的短信SDK,每条短信价格为0.045元,冲个一块钱够用大半月了,美滋滋...

百度阿里大于,进入官网后注册一个账号,然后进入控制台,下载一波SDK,作为一个搞PHP的,当然下载了PHP的SDK,其他SDK也可自行下载。

图片描述

下载完之后,我们看一下目录结构

图片描述

这个fileTest文件就是用来发短信的,我们需要编辑它,不过在此之前,需要先在官网控制台创建短信模板,关于创建模板这里就不一一细说了,官网都有教程,创建完之后我们编辑一下这个所谓的fileTest文件,打开文件,写入。

以下是我的,仅供参考==

#!/usr/bin/php -q
<?php
include "TopSdk.php";
date_default_timezone_set('Asia/Shanghai');

$date1 = strtotime('2015-12-23');  //把日期转换成时间戳
$date_english = strtotime('2017-06-17');//英语六级时间
$date2 = time(); //取当前时间的时间戳
$nowtime=strftime("%y年-%m月-%d日 ",$date2); //格式化输出日期
$days=round(($date2-$date1)/3600/24);  //四舍五入
$days1=round(($date_english-$date2)/3600/24);//四舍五入求英语考试剩余天数
$week=date("N",time()+3600*24);//判断星期几
// $week=7;
$num=mt_rand(0,9);
$num2=mt_rand(0,9);

for($i=0;$i<2;$i++)
{
    if($i==0)
    {
        $name = '不省心的女朋友';
        $c = new TopClient;
        $c ->appkey='';//写入对应key
        $c ->secretKey='';//写入对应key
        $req = new AlibabaAliqinFcSmsNumSendRequest;
        $req ->setExtend( "" );
        $req ->setSmsType( "normal" );
        $req ->setSmsFreeSignName( "短信签名" );
        $req ->setSmsParam( "{name:'$name',time:'$days',num:'$num'}" );
        $req ->setRecNum( "你女朋友的手机号" );
        $req ->setSmsTemplateCode( "短信模板号" );
        $resp = $c ->execute( $req );
    }
    else 
    {
        $name = '帅气的xxx';
        $c = new TopClient;
        $c ->appkey='';//写入对应key
        $c ->secretKey='';//写入对应key
        $req = new AlibabaAliqinFcSmsNumSendRequest;
        $req ->setExtend( "" );
        $req ->setSmsType( "normal" );
        $req ->setSmsFreeSignName( "短信签名" );
        $req ->setSmsParam( "{name:'$name',time:'$days',num:'$num2'}" );
        $req ->setRecNum( "你的手机" );
        $req ->setSmsTemplateCode( "短信模板号" );
        $resp = $c ->execute( $req );
    }
}
?>

写完之后终端运行一下php fileTest.php,测试一下是否能正常收到短信,如果可以就OK了,接下来我们要将他放到服务器上面去,把整个文件夹拷贝到服务器上,接下来,就要用到Linux的crontab定时任务了

首先,简单介绍一下

通过crontab 命令,我们可以在固定的间隔时间执行指定的系统指令或 shell script脚本。时间间隔的单位可以是分钟、小时、日、月、周及以上的任意组合。这个命令非常适合周期性的日志分析或数据备份等工作。

介绍几条命令

crontab -e: 编辑某个用户的crontab文件内容。如果不指定用户,则表示编辑当前用户的crontab文件。

crontab -l:显示某个用户的crontab文件内容,如果不指定用户,则表示显示当前用户的crontab文件内容。

介绍一下crontab的文件格式

分 时 日 月 星期 要运行的命令

  • 第1列分钟0~59
  • 第2列小时0~23(0表示子夜)
  • 第3列日1~31
  • 第4列月1~12
  • 第5列星期0~7(0和7表示星期天)
  • 第6列要运行的命令

更多的内容请看http://linuxtools-rst.readthe...

好了,我们现在只需要会这些就OK。

我们在服务器上运行crontab -e

然后在文件中最后一行加入00 08 * * * php /path/to/你的短信代码文件夹/fileTest.php,前面参数可以自行修改,我这个是每天早上八点,设置完之后,运行一下crontab -l看看是否已保存设置,全都搞定了之后,我们就可以静静等待早上八点的短信了

图片描述

图片描述

当然还有很多玩法,就等各位自行发挥了哈哈哈哈!

微信订阅号

图片描述

查看原文

认证与成就

  • 获得 80 次点赞
  • 获得 18 枚徽章 获得 0 枚金徽章, 获得 3 枚银徽章, 获得 15 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-01-21
个人主页被 886 人浏览