ylzsmallsun

ylzsmallsun 查看完整档案

上海编辑东华大学  |  软件工程 编辑sap  |  UI dev 编辑填写个人主网站
编辑

你就是太阳 蒸发了彷徨
所以挖开土壤 种下希望

个人动态

ylzsmallsun 评论了文章 · 2018-11-05

在微信小程序中绘制图表(part3)

本期大纲

1、饼图绘制
2、如何添加动画效果
3、使用rollup构建项目

相关阅读:
在微信小程序中绘制图表(part1)
在微信小程序中绘制图表(part2)

关注我的 github 项目 查看完整代码。

很久没更新了,最近事情比较多,今天来把坑填上!

饼图绘制

先看一下API

clipboard.png

下面开始(使用ES6语法编写,后面我们可以使用rollup编译成ES5的语法)

假设我们有这样的数据

const series = [
    {data: 15, color: '#7cb5ec'},
    {data: 35, color: '#f7a35c'},
    {data: 78, color: '#434348'},
    {data: 63, color: '#90ed7d'}
];

计算出各项所占的比例和开始的弧度

calPieData.js

export function calPieAngle (series) {
    // 计算数据总和
    let count = 0;
    series.forEach((item) => {
        count += item.data;
    });

    // 计算出开始的弧度和所占比例
    let startAngle = 0;
    return series.map((item) => {
        item.proportion = item.data / count;
        item.startAngle = startAngle;
        startAngle += 2 * Math.PI * item.proportion;
        return item;
    });
}

数据已经计算出来了,下面让我开始绘制吧

drawPieChart.js

import { calPieAngle } from 'calPieData'

export default function drawPieChart (series) {
    ...

    let pieSeries = calPieAngle(series);
    pieSeries.forEach((item) => {
        context.beginPath();
        // 设置填充颜色
        context.setFillStyle(item.color);
        // 移动到原点
        context.moveTo(100, 100);    
        // 绘制弧度
        context.arc(100, 100, 80, item.startAngle, item.startAngle + 2 * Math.PI * item.proportion);
        context.closePath();
        context.fill();
    });

    ...

}

调用drawPieChart(series)就可以得到下面的结果:

clipboard.png

很简单是不是,下面我们给各区块加上一个白色的分割线
因为arc实际上是绘制了一条路径,所以我们简单的stroke描边一下就可以了


...

context.setLineWidth(2);
context.setStrokeStyle('#ffffff');
pieSeries.forEach((item) => {
    context.beginPath();
    context.setFillStyle(item.color);
    context.moveTo(100, 100);    
    context.arc(100, 100, 80, item.startAngle, item.startAngle + 2 * Math.PI * item.proportion);
    context.closePath();
    context.fill();
    context.stroke();
})

...

clipboard.png

添加动画效果

首先让我们创建一个动画工具,这个动画工具能够传入一些自定义的参数,比如动画时间,能够有动画每一步的回调以及动画结束的回调

animation.js

export default function Animation (opts) {
    // 处理用户传入的动画时间,默认为1000ms
    // 因为用户有可能传入duration为0,所以不能用opts.duration = opts.duration || 1000 来做默认值处理
    // 否则用户传入0也会处理成默认值1000
    opts.duration = typeof opts.duration === 'undefined' ? 1000 : opts.duration;
    
    let startTimeStamp = null;

    function step (timestamp) {
        if (startTimeStamp === null) {
            startTimeStamp = timestamp;
        } 
        if (timestamp - startTimeStamp < opts.duration) {
            // 计算出动画的进度
            let process = (timestamp - startTimeStamp) / opts.duration;
            // 触发动画每一步的回调,传入进度process
            opts.onProcess && opts.onProcess(process);
            // 动画进行中,执行下一次动画
            requestAnimationFrame(step);
        } else {
            // 动画结束
            opts.onProcess && opts.onProcess(1);
            // 触发动画结束回调
            opts.onAnimationFinish && opts.onAnimationFinish();
        }
    }

    requestAnimationFrame(step);
}

动画使用了requestAnimationFrame,并且已经满足了我们上面定义的需求
在实战中,此处的动画都是线性的,一般我们还会加入缓动选项,比如缓入缓出,还有一点,在微信小程序真机中IOS设备是不支持requestAnimationFrame的,所以要做降级处理,使用setTimeout查看完整的代码

下面我们调用animation来完成动画效果

app.js

import Animation from 'animation'
import drawPieChart from 'drawPieChart'

Animation({
    duration: 1000,
    onProcess: (process) => {
        drawPieDataChart(series, process);
    }
});

修改一下drawPieDataChart function,能够接受process参数


...

export default function drawPieChart (series, process = 1) {
    ...
    // 将process传入给calPieAngle,计算出对应进度下的图表角度数据
    let pieSeries = calPieAngle(series, process);

...

同样,修改一下calPieAngle function,能够接受process参数


export function calPieAngle (series, process = 1) {
    ...

    // 计算出开始的弧度和所占比例
    let startAngle = 0;
    return series.map((item) => {
        // 计算出当前动画进度的比例
        item.proportion = item.data / count * process;
        item.startAngle = startAngle;
        startAngle += 2 * Math.PI * item.proportion;
        return item;
    });
}

好了,现在我们的动画就可以动起来了,类似这样

clipboard.png

使用rollup构建项目

Rollup is a next-generation JavaScript module bundler. Author your app or library using ES2015 modules, then efficiently bundle them up into a single file for use in browsers and Node.js.

也就是说rollup是一个前端构建工具,能够将我们的整个项目合并输出成一个最终的编译结果,上面我们编写代码的时候都是按照不同的功能放到不同的文件中,这样有利于后期的可持续性开发和维护,rollup正好能帮助我们构建出最后的编译结果

先安装rollup

npm install -g rollup

添加对ES6的支持

npm install --save-dev rollup-plugin-babel
npm install --save-dev babel-preset-es2015-rollup

创建.babelrc文件在项目根目录,告诉babel转义时使用哪个presets

{
  "presets": ["es2015-rollup"],
}

好了剩下最后一步,定义我们的rollup.config.js配置文件

import babel from 'rollup-plugin-babel';

export default {
  // 入口文件
  entry: 'app.js',
  // 输出格式,这里使用commonJS
  format: 'cjs',
  // 输出文件
  dest: 'dist/charts.js',
  // 使用babel进行ES6转ES5
  plugins: [
      babel({
          exclude: 'node_modules/**',
      })
  ]
};

rollup会从入口文件开始,查找我们的依赖(import),逐级往下深入,把依赖的文件全部收集起来并合并到一起,最后输出到我们定义的dest文件中

执行

rollup -c

好了,我们就得到了我们最后的项目编译文件charts.js

下期预告

下一期中我一起讨论下有技术含量的内容,关于图表中文案显示的检测碰撞问题,大概效果会是这样的,红框部分文案发生了碰撞,这里完成了避让,能够正常显示

clipboard.png

查看原文

ylzsmallsun 发布了文章 · 2018-10-20

关于flex-shrink如何计算的冷知识

先回顾一下flex-grow

假设有一个div内包含三个子div1, div2, div3,宽度分别200px.
对于flex-grow对于剩余空间分配比例的计算相信用过flex布局的都非常熟悉了。这里还是简单列一下计算公式:
假设div1, div2, div3的flex-grow分别设置为 1,2,3. 现在假定外层div的宽度是800px, 那么剩下的800px - 3*200px = 200px如何分配给三个子div呢?
三个div额外分配的空间如下:

div1:1 / (1 + 2 + 3) 200px = 1/6 200px

div2:2 / (1 + 2 + 3) 200px = 2/6 200px

div3:3 / (1 + 2 + 3) 200px = 3/6 200px

重点来了,flex-shink到底是如何计算的呢?

flex-shink属性主要是在外层div宽度不够的情况下,子div收缩一定的空间来抵消不够的那部分宽度。
举个栗子,现在三个子div的宽度是600px, 但是我给外层div的宽度设置成500px, 那么不够显示的600-500 px应该怎么让子div们收缩掉这100px。这个时候flex-shrink就派上用场了,那具体怎么计算呢?
有人说这个属性跟flex-grow类似,计算方式也差不多吧(其实是有差异的)。但具体怎么算,很多人都说不清,包括一个MDN, W3CShcool也没给出具体公式。另外,我发现很多点赞数很多的文章,给出的计算公式是错误的。这也是为什么我想要写这边文章的原因。

好了,不卖关子,来说说怎么计算收缩空间吧!
先贴出例子的代码:
html部分:

<div class="outer">
    <div class='div1'>1</div>
    <div class='div2'>2</div>
    <div class="div3">3</div>
</div>

css部分:

.outer {
  width: 500px;
  display: flex;
}
.outer div {
  height: 80px;
}

.div1 {
  flex: 1 1 100px;
  background: red;
}
.div2 {
  flex: 1 2 200px;
  background: yellow;
}
.div3 {
  flex: 1 3 300px;
  background: green;
}

先计算总权重TW = 100px 1(flex-shrink) + 200px 2(flex-shrink) + 300px *3(flex-shrink) = 1400px
也就是每个div的宽度乘以flex-shrink系数的总和。

每个div收缩的空间为:div的宽度 flex-shrink系数/ 总权重TW 需要收缩的总宽度(在我们的例子中是600px - 500px = 100px)

所以各div最后的宽度计算公式如下:

div1最后的宽度 = 100px - 100*1/1400 * 100px = 92.86px

div2最后的宽度 = 200px - 200*2/1400 * 100px = 171.42px

div3最后的宽度 = 300px - 300*3/1400 * 100px = 235.72px

demo地址:https://jsfiddle.net/lingzhen...

截图如下:

Tips: 看见别人的文章的一些公式时,一定不要偷懒,自己写个例子验证一下,不然明明被误导了,却还给点赞了收藏了。毕竟实践出真知!

BTW: 这个属性相对flex-grow来说确实用的很少,所以很多人都没研究过它具体到底是怎么算的。

查看原文

赞 1 收藏 1 评论 0

ylzsmallsun 回答了问题 · 2018-10-13

js instanceof判断问题

首先解释下instanceof,
instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。

A instanceof B // 检测构造函数B的原型是否有出现在对象A的原型链上。
所以你通过这种声明方式创建的简单数据类型:a = true是没办法用instanceof的。因为它不是一个对象实例。

对于 a.constructor === b.constructor // true
在a.constructor其实js内部做了一个自动装箱的操作。不然简单数据类型本身是不含属性和方法的。
回想一下我们是不是经常会写这样的代码 var a = '123'; if (a.length > 0) ...

关于js里的类型判断可以看下我的总结 https://segmentfault.com/a/11...

关注 5 回答 3

ylzsmallsun 发布了文章 · 2018-10-12

关于javascript中类型判断的那些疑惑

Javascript中数据类型分为两种:

  1. 简单数据类型:Undefined, NULL, Boolean, Number, String
  2. 复杂数据类型:Object

接下来我们就来看看怎么做数据类型判别吧?

首先来看看 typeof

TypeResult
Undefined"undefined"
Null"object" (see below)
Boolean"boolean"
Number"number"
String"string"
Symbol (new in ECMAScript 2015)"symbol"
Host object (provided by the JS environment)Implementation-dependent
Function object (implements [[Call]] in ECMA-262 terms)"function"
Any other object"object"

来点code demo吧

let a = undefined;
typeof a
"undefined"

let b = false;
typeof b
"boolean"

let c = 12;
typeof c
"number"

let d = '12';
typeof d
"string"

let f = function () {};
typeof f
"function"

接下来我们就来看看那些奇怪的现象吧

let str = new String('abc');
typeof str
"object"

let num = new Number(12);
typeof num
"object"

var func = new Function();
typeof func; 
"function"

typeof null
"object"

使用构造函数创建的变量,使用typeof判断会返回“object”结果,但是Function函数例外,由它创建的变量typeof返回的是“function”

接着就来说说typeof null == "object"。这个相信前端开发的小伙伴都知道是这个结果了,But why? 这其实是javascript第一个版本就存在的一个bug,历史原因可以看看这篇文章The history of typeof null

关于如何判断数组

let arr = [1, 2, 3];
typeof arr
"object"

上面这个结果大家应该不陌生,那该如何正确判断数组类型呢

  1. instanceof

    arr instanceof Array  //true
  2. isArray

    Array.isArray(arr) // true
  3. constructor.name

    arr.constructor.name  // "Array"

第三种用法用的人应该比较少,不少前端的的小伙伴都没用过。对于复杂类型Object,它的每个实例都有constructor属性。

instanceof vs isArray

当检测Array实例时, Array.isArray 优于 instanceof,因为Array.isArray能检测iframes.

var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[window.frames.length-1].Array;
var arr = new xArray(1,2,3); // [1,2,3]

// Correctly checking for Array
Array.isArray(arr);  // true
// Considered harmful, because doesn't work though iframes
arr instanceof Array; // false

这段代码是从MDN copy的。补充以下结果,发现第三种方法constructor.name也能正确判断出。

arr.constructor.name //"Array"

关于NaN

使用isNaN判断NaN。

isNaN(1/'a') // true

我们知道NaN == NaN结果是false,那如何判断两个NaN变量呢?

比较两个NaN变量,使用es6的Object.is()即可。

let nan1 = NaN
let nan2 = NaN
Object.is(nan1, nan2)
true
查看原文

赞 1 收藏 1 评论 0

ylzsmallsun 关注了标签 · 2018-10-12

react.js

React (sometimes styled React.js or ReactJS) is an open-source JavaScript library for creating user interfaces that aims to address challenges encountered in developing single-page applications. It is maintained by Facebook, Instagram and a community of individual developers and corporations.

关注 69561

ylzsmallsun 关注了标签 · 2018-10-12

git

Git是一个由林纳斯·托瓦兹为了更好地管理linux内核开发而创立的分布式版本控制/软件配置管理软件。需要注意的是和GNU Interactive Tools,一个类似Norton Commander界面的文件管理器相区分。

关注 18217

ylzsmallsun 关注了标签 · 2018-10-12

es6

ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

标准的制定者有计划,以后每年发布一次标准,使用年份作为版本。因为 ES6 的第一个版本是在 2015 年发布的,所以又称ECMAScript 2015(简称 ES2015)。

2016 年 6 月,小幅修订的《ECMAScript 2016 标准》(简称 ES2016)如期发布。由于变动非常小(只新增了数组实例的includes方法和指数运算符),因此 ES2016 与 ES2015 基本上是同一个标准,都被看作是 ES6。根据计划,2017 年 6 月将发布 ES2017。

标准请参读 ECMAScript® 2015 Language Specification

关注 2664

ylzsmallsun 关注了标签 · 2018-10-12

css3

层叠样式表(英语:Cascading Style Sheets,简写CSS),又称串样式列表,由W3C定义和维护的标准,一种用来为结构化文档(如HTML文档或XML应用)添加样式(字体、间距和颜色等)的计算机语言。目前最新版本是CSS2.1,为W3C的候选推荐标准。CSS3现在已被大部分现代浏览器支持,而下一版的CSS4仍在开发过程中。

关注 23364

ylzsmallsun 关注了标签 · 2018-10-12

webpack

Webpack 是一个前端资源加载/打包工具,只需要相对简单的配置就可以提供前端工程化需要的各种功能。

关注 4078

ylzsmallsun 发布了文章 · 2018-09-28

简单理解观察者模式(pub/sub)在前端中的应用

概念

观察者模式被广泛地应用于JavaScript客户端编程中。所有的浏览器事件(mouseover,keypress等)都是使用观察者模式的例子。这种模式的另一个名字叫“自定义事件”,意思是这些事件是被编写出来的,和浏览器触发的事件相对。它还有另外一个名字叫“订阅者/发布者”模式。
使用这个模式的最主要目的就是促进代码触解耦。在观察者模式中,一个对象订阅另一个对象的指定活动并得到通知,而不是调用另一个对象的方法。订阅者也被叫作观察者,被观察的对象叫作发布者或者被观察者(译注:subject,不知道如何翻译,第一次的时候译为“主体”,第二次译时觉得不妥,还是直接叫被观察者好了)。当一个特定的事件发生的时候,发布者会通知(调用)所有的订阅者,同时还可能以事件对象的形式传递一些消息。 (摘自javascript 模式一书)

维基百科定义:在软件架构中,发布-订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。

理解观察者模式:

摘自 http://www.cnblogs.com/tugenh...

JS传统事件就是一个观察者模式,之所以要有观察者模式,是因为有时候和传统事件无关的事件,比如:2个或者更多模块的直接通信问题,比如说我有个index.html页面,我有很多JS文件,比如:

a.js: function a(){}; b.js: function b(){}; c.js function c(){}; 等等。后面还有许多这样的JS,那么我要在index.html初始化这些函数的话,我需要这样调用a();b();c()等等,也就是说页面调用的时候 我要这样调用,增加了依赖性,我要知道有多少个函数要这样初始化调用,但是如果我们现在用观察者模式就不需要知道有哪些订阅者,比如一个模块(或者多个模块)订阅了一个主题(或者事件),另一个模块发布这个主题时候,订阅这个主题模块就可以执行了,观察者主要让订阅者与发布者解耦,发布者不需要知道哪些模块订阅了这个主题,它只管发布这个主题就可以了,同样订阅者也无需知道那个模块会发布这个主题,它只管订阅这个主题就可以了。这样2个模块(或更多模块)就实现了关联了。而不需要和上面代码一样,我要知道哪些模块要初始化,我要怎样初始化。这只是一个简单的列子解释观察者模式要使用在什么地方,我也看过很多博客关于这方面的资料,但是很多人写博客只是讲了如何实现观察者模式及观察者模式的好处,并没有讲我们什么时候该使用观察者模式,所以我列举了上面的列子,就是多个不同业务模块需要相互关联的时候,可以使用观察者模式。就好比requireJS,seaJS,KISSY解决依赖的问题一样(比如A依赖于B,B依赖于C,只要一个解决入口文件,其他都会异步加载出来一样)。也就是说各个模块之间的关联性可以使用观察者模式来设计。

代码实现:在该篇博客(https://www.cnblogs.com/Lucky...

(function(){
    var Event = {
        on: function (type, handler) {
            if (!this.handlers) {
                // this.handlers = {};    
                Object.defineProperty(this, "handlers", {
                    value: {},
                    enumerable: false,  // important 避免拷贝Event时污染handlers
                    configurable: true,
                    writable: true
                })
            }   
            if (typeof this.handlers[type] === 'undefined') {
                this.handlers[type] = [];
            }
            this.handlers[type].push(handler);
        },
        emit: function (eventName) {
            if (this.handlers[arguments[0]] instanceof Array) {
                var handlers = this.handlers[arguments[0]];
                for (var i=0; i<handlers.length; i++) {
                    handlers[i](arguments[1].message);
                }
            }
        },
        unsubscribe: function (type, handler) {
            if (this.handlers[type] instanceof Array) {
                var handlers = this.handlers[type];
                for (var i=0; i<handlers.length; i++) {
                    if (handlers[i] === handler) {
                        break;
                    }
                }
                handlers.splice(i, 1);
            }
        }
    };
  
    Event.on('test', function (message) {
        console.log(message);
    });
    Event.on('test', function () {
        console.log('test');
    });

    Event.emit('test', {message: 'hello world'});

    var person1 = {
        sayHello: function (message) {
            console.log(message);
        }
    };
    var person2 = {
        sayHello: function (message) {
            console.log(message);
        }
    };
    Object.assign(person1, Event);
    Object.assign(person2, Event);
    person1.on('call1', person1.sayHello);
    person2.on('call2', person2.sayHello);
    person1.emit('call1', {message:'person1 is calling call1.'}); // 'person1 is calling call1.' 
    person1.emit('call2', {message:'p111'}); //  no output
    person2.emit('call1', {message:'p222'}); //  no output
    person2.emit('call2', {message:'person2 is calling call2.'}); // 'person2 is calling call2'
    person1.unsubscribe('call1', person1.sayHello);
    person1.emit('call1', {message:'person1 is calling call1 again.'}); // no output
    person1.on('call1', person1.sayHello);
    person1.emit('call1', {message:'person1 is calling call1 again.'}); // person1 is calling call1 again.

    let paper = Object.assign({}, Event);
    let mike = {
        haveABreak: function (news) {
            let msg = 'Mike just read news: ' + news;
            console.log('-------------');
            console.log(msg);
        }
    };
    let tom = {
        haveABreak: function (news) {
            let msg = 'Tom just read news: ' + news;
            console.log(msg);
            console.log('-------------');
        }
    };
    paper.on('dailyNews', mike.haveABreak);
    paper.on('dailyNews', tom.haveABreak);
    paper.emit('dailyNews', {message: 'Coding is getting better.'});

})()

可在我的github上下载上述代码(https://github.com/ylzsmallsu...),并包含JavaScript模式中观察者模式例子。

此外,观察者模式还可用于实现数据绑定

思想:使用数据特性来为 HTML 代码进行绑定,所有被绑定在一起的 JavaScript 对象和 DOM 元素都会订阅一个 PubSub 对象。只要 JavaScript 对象或者一个 HTML 输入元素监听到数据的变化时,就会触发绑定到 PubSub 对象上的事件,从而其他绑定的对象和元素都会做出相应的变化。

参考文章

本篇文章主要是对查阅的几篇文档集中梳理,有兴趣的可以翻阅以上参考文章。

查看原文

赞 1 收藏 0 评论 0

认证与成就

  • 获得 28 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • Topic分享小程序

    这个小程序主要用于公司内部的一个技术分享小组,具有签到,抽奖,出题,答题,查看分享顺序与积分排行榜等功能。使得分享更加有趣,提高每位参与成员的参与感,最重要的当然还是使得大家通过这样一个分享会,互相学习到更多的知识。

注册于 2016-06-13
个人主页被 940 人浏览