tinybear

tinybear 查看完整档案

苏州编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

一只熊,浪啊浪

个人动态

tinybear 收藏了文章 · 2019-01-25

记一次雪花效果

前言

最近,公司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.码字不容易,希望大家点个赞。前端路漫漫,与君共勉之。

查看原文

tinybear 评论了文章 · 2018-12-13

Canvas制作的下雨动画

简介

在codepen上看到一个Canvas做的下雨效果动画,感觉蛮有意思的。就研究了下,这里来分享下,实现技巧。效果可以见下面的链接。

霓虹雨: http://codepen.io/natewiley/full/NNgqVJ/

效果截图:
图片描述

Canvas动画基础

大家都知道,Canvas其实只是一个画板。我们可以应用canvas的api在上面绘制各种图形。
Canvas 2D 的API:
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D

那么Canvas绘制动画的步骤就是:

  1. 绘制第一帧图形(利用API绘图)

  2. 清空画板(应用clearRect()或fillRect())

  3. 绘制下一帧动画

用什么来控制动画每一帧的绘制时间呢?大家很容易想到 window.setInterval()和window.setTimeout()。没错用这两个也可以。除此之外,后来又出现一个新的方法:window.requestAnimationFrame(callback)。

requestAnimationFrame会告诉浏览器你要绘制一个动画。让浏览器要重绘时调用你指定的方法(callback)来绘制你的动画。
使用方法如下:


function anim() {
    ctx.fillStyle = clearColor;
    ctx.fillRect(0,0,w,h);
    for(var i in drops){
        drops[i].draw();
    }
    requestAnimationFrame(anim);
}

一般情况下优先使用requestAnimationFrame能保持动画绘制的频率和浏览器重绘的频率一致。不幸的是requestAnimationFrame的兼容性还不是很好。IE9以下和addroid 4.3以下好像不支持这个属性。不支持的浏览器要用setInterval或setTimeout做兼容。

雨滴下落效果

首先来讲讲雨滴下落的效果如何制作。雨滴其实是一个长方形,然后加残影。残影的绘制可以说是雨滴下落的关键。残影是通过在前进的方向每一帧都绘制一个半透明的背景和一个长方形,然后前面绘制的图形叠加产生的效果。由于前进方向的图形最后绘制,所以显得明亮,后面的图形叠加的比较多,所以视觉上减弱。整体看起来后面的就像残影。这里绘制具有透明度背景是关键,否则产生不了叠加效果。

那么来绘制个雨滴看看。首先准备一个画板:
html代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>霓虹雨</title>
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <style type="text/css">
        .bg {
            background: #000;
            overflow: hidden;
        }
    </style>

</head>
<body class="bg">
<canvas id="canvas-club"></canvas>
<script type="text/javascript" data-original="raindrop.js"></script>
</body>
</html>

我在js文件里绘制动画(raindrop.js),代码如下:

var c = document.getElementById("canvas-club");
var ctx = c.getContext("2d");//获取canvas上下文
var w = c.width = window.innerWidth;
var h = c.height = window.innerHeight;//设置canvas宽、高
var clearColor = 'rgba(0, 0, 0, .1)';//画板背景,注意最后的透明度0.1 这是产生叠加效果的基础

function random(min, max) {
    return Math.random() * (max - min) + min;
}

function RainDrop(){}
//雨滴对象 这是绘制雨滴动画的关键
RainDrop.prototype = {
    init:function(){
        this.x =  random(0, w);//雨滴的位置x
        this.y = 0;//雨滴的位置y
        this.color = 'hsl(180, 100%, 50%)';//雨滴颜色 长方形的填充色
        this.vy = random(4, 5);//雨滴下落速度
        this.hit = random(h * .8, h * .9);//下落的最大值
        this.size = 2;//长方形宽度
    },
    draw:function(){
        if (this.y < this.hit) {
            ctx.fillStyle = this.color;
            ctx.fillRect(this.x, this.y, this.size, this.size * 5);//绘制长方形,通过多次叠加长方形,形成雨滴下落效果
        }
        this.update();//更新位置
    },
    update:function(){
        if(this.y < this.hit){
            this.y += this.vy;//未达到底部,增加雨滴y坐标
        }else{
            this.init();
        }
    }
};

function resize(){
    w = c.width = window.innerWidth;
    h = c.height = window.innerHeight;
}

//初始化一个雨滴
var r = new RainDrop();
r.init();

function anim() {
    ctx.fillStyle = clearColor;//每一帧都填充背景色
    ctx.fillRect(0,0,w,h);//填充背景色,注意不要用clearRect,否则会清空前面的雨滴,导致不能产生叠加的效果
    r.draw();//绘制雨滴
    requestAnimationFrame(anim);//控制动画帧
}

window.addEventListener("resize", resize);
//启动动画
anim();

涟漪效果

接着来绘制涟漪效果。与绘制雨滴的方式类似,也是通过具有透明度的背景来叠加前面的图像产生内阴影的效果。

代码如下(rippling.js):

var c = document.getElementById("canvas-club");
var ctx = c.getContext("2d");//获取canvas上下文
var w = c.width = window.innerWidth;
var h = c.height = window.innerHeight;//设置canvas宽、高
var clearColor = 'rgba(0, 0, 0, .1)';//画板背景,注意最后的透明度0.1 这是产生叠加效果的基础

function random(min, max) {
    return Math.random() * (max - min) + min;
}

function Rippling(){}
//涟漪对象 这是涟漪动画的主要部分
Rippling.prototype = {
    init:function(){
        this.x = random(0,w);//涟漪x坐标
        this.y = random(h * .8, h * .9);//涟漪y坐标
        this.w = 2;//椭圆形涟漪宽
        this.h = 1;//椭圆涟漪高
        this.vw = 3;//宽度增长速度
        this.vh = 1;//高度增长速度
        this.a = 1;//透明度
        this.va = .96;//涟漪消失的渐变速度
    },
    draw:function(){
        ctx.beginPath();
        ctx.moveTo(this.x, this.y - this.h / 2);
        //绘制右弧线
        ctx.bezierCurveTo(
            this.x + this.w / 2, this.y - this.h / 2,
            this.x + this.w / 2, this.y + this.h / 2,
            this.x, this.y + this.h / 2);
        //绘制左弧线
        ctx.bezierCurveTo(
            this.x - this.w / 2, this.y + this.h / 2,
            this.x - this.w / 2, this.y - this.h / 2,
            this.x, this.y - this.h / 2);
        
        ctx.strokeStyle = 'hsla(180, 100%, 50%, '+this.a+')';
        ctx.stroke();
        ctx.closePath();
        this.update();//更新坐标
    },
    update:function(){
        if(this.a > .03){
            this.w += this.vw;//宽度增长
            this.h += this.vh;//高度增长
            if(this.w > 100){
                this.a *= this.va;//当宽度超过100,涟漪逐渐变淡消失
                this.vw *= .98;//宽度增长变缓慢
                this.vh *= .98;//高度增长变缓慢
            }
        } else {
            this.init();
        }

    }
};

function resize(){
    w = c.width = window.innerWidth;
    h = c.height = window.innerHeight;
}

//初始化一个涟漪
var r = new Rippling();
r.init();

function anim() {
    ctx.fillStyle = clearColor;
    ctx.fillRect(0,0,w,h);
    r.draw();
    requestAnimationFrame(anim);
}

window.addEventListener("resize", resize);
//启动动画
anim();

总结

这样大家对整个下雨效果的制作方法,应该有一定的了解了。Canvas用来绘制动画的效果确实能让人眼前一亮,让web的视觉效果提升一大截。发动自己的智慧,相信能做出更多奇妙的动画。这是我越来越喜欢web的原因之一吧 O(∩_∩)O~~。

查看原文

tinybear 赞了文章 · 2018-10-30

RxJS: 简单入门

Introduction to RxJS

1. 前言

1.1 什么是RxJS

RxJSReactiveX编程理念的JavaScript版本。ReactiveX来自微软,它是一种针对异步数据流的编程。简单来说,它将一切数据,包括HTTP请求,DOM事件或者普通数据等包装成流的形式,然后用强大丰富的操作符对流进行处理,使你能以同步编程的方式处理异步数据,并组合不同的操作符来轻松优雅的实现你所需要的功能。

1.2 RxJS可用于生产吗?

ReactiveX由微软于2012年开源,目前各语言库由ReactiveX组织维护。RxJSGitHub上已有8782个star,目前最新版本为5.5.2,并持续开发维护中,其中官方测试用例共计2699个。

谁在使用Rx

1.3 RxJS对项目代码的影响?

RxJS中的流以Observable对象呈现,获取数据需要订阅Observable,形式如下:

const ob = http$.getSomeList(); //getSomeList()返回某个由`Observable`包装后的http请求
ob.subscribe((data) => console.log(data));
//在变量末尾加$表示Observable类型的对象。

以上与Promise类似:

const promise = http.getSomeList(); // 返回由`Promise`包装的http请求
promise.then((data) => console.log(data));

实际上Observable可以认为是加强版的Promise,它们之间是可以通过RxJSAPI互相转换的:

const ob = Observable.fromPromise(somePromise); // Promise转为Observable
const promise = someObservable.toPromise(); // Observable转为Promise

因此可以在Promise方案的项目中安全使用RxJS,并能够随时升级到完整的RxJS方案。

1.4 RxJS会增加多少体积?

RxJS(v5)整个库压缩后约为140KB,由于其模块化可扩展的设计,因此仅需导入所用到的类与操作符即可。导入RxJS常用类与操作符后,打包后的体积约增加30-60KB,具体取决于导入的数量。

不要用 import { Observable } from 'rxjs'这种方式导入,这会导入整个rxjs库,按需导入的方式如下:
import { Observable } from 'rxjs/Observable' //导入类
import 'rxjs/add/operator/map' // 导入实例操作符
import 'rxjs/add/observable/forkJoin' // 导入类操作符

2. RxJS快速入门

2.1 初级核心概念

  • Observable
  • Observer
  • Operator

Observable被称为可观察序列,简单来说数据就在Observable中流动,你可以使用各种operator对流进行处理,例如:

const ob = Observable.interval(1000);
ob.take(3).map(n => n * 2).filter(n => n > 2);

第一步代码我们通过类方法interval创建了一个Observable序列,ob作为源会每隔1000ms发射一个递增的数据,即0 -> 1 -> 2。第二步我们使用操作符对流进行处理,take(3)表示只取源发射的前3个数据,取完第三个后关闭源的发射;map表示将流中的数据进行映射处理,这里我们将数据翻倍;filter表示过滤掉出符合条件的数据,根据上一步map的结果,只有第二和第三个数据会留下来。

上面我们已经使用同步编程创建好了一个流的处理过程,但此时ob作为源并不会立刻发射数据,如果我们在map中打印n是不会得到任何输出的,因为ob作为Observable序列必须被“订阅”才能够触发上述过程,也就是subscribe(发布/订阅模式)。

const ob = Observable.interval(1000);
ob.take(3).map(n => n * 2).filter(n => n > 0).subscribe(n => console.log(n));

结果:

2 //第2秒
4 //第3秒

上面代码中我们给subscribe传入了一个函数,这其实是一种简写,subscribe完整的函数签名如下:

ob.subscribe({
    next: d => console.log(d),
    error: err => console.error(err),
    complete: () => console.log('end of the stream')
})

直接给subscribe传入一个函数会被当做是next函数。这个完整的包含3个函数的对象被称为observer(观察者),表示的是对序列结果的处理方式。next表示数据正常流动,没有出现异常;error表示流中出错,可能是运行出错,http报错等等;complete表示流结束,不再发射新的数据。在一个流的生命周期中,errorcomplete只会触发其中一个,可以有多个next(表示多次发射数据),直到complete或者error

observer.next可以认为是Promisethen的第一个参数,observer.error对应第二个参数或者Promisecatch

RxJS同样提供了catch操作符,err流入catch后,catch必须返回一个新的Observable。被catch后的错误流将不会进入observererror函数,除非其返回的新observable出错。

Observable.of(1).map(n => n.undefinedMethod()).catch(err => {
    // 此处处理catch之前发生的错误
    return Observable.of(0); // 返回一个新的序列,该序列成为新的流。
});

2.2 创建可观察序列

创建一个序列有很多种方式,我们仅列举常用的几种:

Observable.of(...args)

Observable.of()可以将普通JavaScript数据转为可观察序列,点我测试

Observable.fromPromise(promise)

Promise转化为Observable点我测试

Observable.fromEvent(elment, eventName)

DOM事件创建序列,例如Observable.fromEvent($input, 'click')点我测试

Observable.ajax(url | AjaxRequest)

发送http请求,AjaxRequest参考这里

Observable.create(subscribe)

这个属于万能的创建方法,一般用于只提供了回调函数的某些功能或者库,在你用这个方法之前先想想能不能用RxJS上的类方法来创建你所需要的序列,点我测试

2.3 合并序列

合并序列也属于创建序列的一种,例如有这样的需求:进入某个页面后拿到了一个列表,然后需要对列表每一项发出一个http请求来获取对应的详细信息,这里我们把每个http请求作为一个序列,然后我们希望合并它们。
合并有很多种方式,例如N个请求按顺序串行发出(前一个结束再发下一个);N个请求同时发出并且要求全部到达后合并为数组,触发一次回调;N个请求同时发出,对于每一个到达就触发一次回调。
如果不用RxJS,我们会比较难处理这么多情形,不仅实现麻烦,维护更麻烦,下面是使用RxJS对上述需求的解决方案:

const ob1 = Observable.ajax('api/detail/1');
const ob2 = Observable.ajax('api/detail/2');
...
const obs = [ob1, ob2...];
// 分别创建对应的HTTP请求。
  1. N个请求按顺序串行发出(前一个结束再发下一个)
Observable.concat(...obs).subscribe(detail => console.log('每个请求都触发回调'));
  1. N个请求同时并行发出,对于每一个到达就触发一次回调
Observable.merge(...obs).subscribe(detail => console.log('每个请求都触发回调'));
  1. N个请求同时发出并且要求全部到达后合并为数组,触发一次回调
Observable.forkJoin(...obs).subscribe(detailArray => console.log('触发一次回调'));

3. 使用RxJS实现搜索功能

搜索是前端开发中很常见的功能,一般是监听<input />keyup事件,然后将内容发送到后台,并展示后台返回的数据。

<input id="text"></input>
<script>
    var text = document.querySelector('#text');
    text.addEventListener('keyup', (e) =>{
        var searchText = e.target.value;
        // 发送输入内容到后台
        $.ajax({
            url: `/search/${searchText}`,
            success: data => {
              // 拿到后台返回数据,并展示搜索结果
              render(data);
            }
        });
    });
</script>

上面代码实现我们要的功能,但存在两个较大的问题:

  • 多余的请求

当想搜索“爱迪生”时,输入框可能会存在三种情况,“爱”、“爱迪”、“爱迪生”。而这三种情况将会发起 3 次请求,存在 2 次多余的请求。

  • 已无用的请求仍然执行

一开始搜了“爱迪生”,然后马上改搜索“达尔文”。结果后台返回了“爱迪生”的搜索结果,执行渲染逻辑后结果框展示了“爱迪生”的结果,而不是当前正在搜索的“达尔文”,这是不正确的。

减少多余请求数,可以用 setTimeout 函数节流的方式来处理,核心代码如下:

<input id="text"></input>
<script>
    var text = document.querySelector('#text'),
        timer = null;
    text.addEventListener('keyup', (e) =>{
        // 在 250 毫秒内进行其他输入,则清除上一个定时器
        clearTimeout(timer);
        // 定时器,在 250 毫秒后触发
        timer = setTimeout(() => {
            console.log('发起请求..');
        },250)
    })
</script>

已无用的请求仍然执行 的解决方式,可以在发起请求前声明一个当前搜索的状态变量,后台将搜索的内容及结果一起返回,前端判断返回数据与当前搜索是否一致,一致才走到渲染逻辑。最终代码为:

<input id="text"></input>
<script>
    var text = document.querySelector('#text'),
        timer = null,
        currentSearch = '';

    text.addEventListener('keyup', (e) =>{
        clearTimeout(timer)
        timer = setTimeout(() => {
            // 声明一个当前所搜的状态变量
            currentSearch = '书'; 

            var searchText = e.target.value;
            $.ajax({
                url: `/search/${searchText}`,
                success: data => {
                    // 判断后台返回的标志与我们存的当前搜索变量是否一致
                    if (data.search === currentSearch) {
                        // 渲染展示
                        render(data);
                    } else {
                        // ..
                    }
                }           
            });
        },250)
    })
</script>

上面代码基本满足需求,但代码开始显得乱糟糟。我们来使用RxJS实现上面代码功能,如下:

var text = document.querySelector('#text');
var inputStream = Rx.Observable.fromEvent(text, 'keyup') //为dom元素绑定'keyup'事件
                    .debounceTime(250) // 防抖动
                    .pluck('target', 'value') // 取值
                    .switchMap(url => Http.get(url)) // 将当前输入流替换为http请求
                    .subscribe(data => render(data)); // 接收数据

RxJS能简化你的代码,它将与流有关的内部状态封装在流中,而不需要在流外定义各种变量来以一种上帝视角控制流程。Rx的编程方式使你的业务逻辑流程清晰,易维护,并显著减少出bug的概率。

个人总结的常用操作符:

类操作符(通常为合并序列或从已有数据创建序列)
合并forkJoin, merge, concat
创建of, from, fromPromise, fromEvent, ajax, throw
实例操作符(对流中的数据进行处理或者控制流程)
map, filter,switchMap, toPromise, catch, take, takeUntil, timeout, debounceTime, distinctUntilChanged, pluck
对于这些操作符的使用不再详细描述,请参阅网上资料。

中文官网 http://cn.rx.js.org/
附上个人翻译的一些文章

最后打个广告,蚂蚁金服大安全风控+团队招人,前端后台都要,简历发送至huitong.zht@antfin.com。

参考文章:构建流式应用:RxJS 详解

查看原文

赞 111 收藏 104 评论 10

tinybear 收藏了文章 · 2018-03-20

centos安装使用puppeteer和headless chrome

Google推出了无图形界面的headless Chrome之后,可以直接在远程服务器上直接跑一些测试脚本或者爬虫脚本了,猴开心!Google还附送了Puppeteer用于驱动没头的Chome。

阿里的Macaca也顺势写了Macaca-puppeteer,可以在Macaca上直接写通用的测试用例,在开发机上用图形界面看效果,上服务器走生产,岂不是美滋滋。

然鹅,可达鸭眉头一皱,发现事情并没有那么简单。

在阿里云的Centos 7.3上,安装puppeteer之后,会发现并不能启动官方的example:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({path: 'example.png'});

  await browser.close();
})();

依赖安装

仔细看错误栈,核心的错误是如下一段:

...node_modules/puppeteer/.local-chromium/linux-496140/chrome-linux/chrome: error while loading shared libraries: libpangocairo-1.0.so.0: cannot open shared object file: No such file or directory

TROUBLESHOOTING: https://github.com/GoogleChro...

原来puppet虽然帮你下了一个Chromium,但并没有帮你把依赖都装好。于是你要自己把那些so都装好。

官方给的是Ubuntu版本的各个so包的apt-get安装方式,centos版本居然没有放!于是遍历了各个issue之后,终于发现还是有人给出了centos的库名,在一个看起来并不相关的issue里

#依赖库
yum install pango.x86_64 libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXtst.x86_64 cups-libs.x86_64 libXScrnSaver.x86_64 libXrandr.x86_64 GConf2.x86_64 alsa-lib.x86_64 atk.x86_64 gtk3.x86_64 -y

#字体
yum install ipa-gothic-fonts xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi xorg-x11-utils xorg-x11-fonts-cyrillic xorg-x11-fonts-Type1 xorg-x11-fonts-misc -y

总算不用挨个去google了……

sandbox去沙箱

这时候你再去执行脚本,发现还是跑不起来。但是报错终于变了。这个时候变成了一个莫名其妙的错误:

(node:30559) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Failed to connect to chrome!
(node:30559) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

要疯掉了有没有,这啥玩意啊!!!!关键是这时候另外一个看起来是解决上面问题的issue,对这个错误进行了详细的讨论,然而直到今天(2017年9月27日)并没有讨论出什么结果。

网上很多讨论是说,直接调试那个Chrome。按照并不能解决问题的说法:直接去puppeteer的目录找到.local-chrome里面的Chromium执行文件,直接执行

./chrome -v --no-sandbox --disable-setuid-sandbox

(chrome:5333): Gtk-WARNING **: cannot open display: 

发现加上了--no-sanbox其实是能启动的,但是提示没有Gtk图形界面,那干脆加上--headless是不是就行了嘞?确实没有报错了。

回到puppeteer示例脚本,修改启动浏览器的代码,加上args:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']});
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({path: 'example.png'});

  await browser.close();
})();

啊哈,终于执行成功了。下载下来了example.com的截图看了一眼,简直泪流满面。

回想一下,Puppet本身估计自带了--headless,所以如果直接去命令行执行chrome,还是要带上--headless。

终于搞定这一切发现Macaca顺便还提供了一个基于Ubuntu的Macaca-puppeteer的Docker,艾玛这方便太多了,早知道不自己折腾了。

查看原文

tinybear 赞了文章 · 2018-03-07

SVG之ViewBox

最近开始看SVG Essentials。找不到中文版的,逼着自己看原版书,进度比较慢,不过边学技术边学英语还是挺有成就感的^_^。
目前看到Chapter 4,有必要先停下来整理下viewport这个知识点,个人感觉挺关键的。

viewBox用来干嘛?

比方说,我用svg画了个半径200px的圆

<circle cx="200" cy="200" r="200" fill="#fdd" stroke="none"></circle>

如果是在一个400*400的画布上,圆正好撑满整个画布,挺好的。
好了,然后我要把这个圆嵌入到自己的页面里的svg标签里去,页面的svg标签尺寸是由实际业务需要来定的,不一定正好是,可能大可能小,还可能不是正方形。

<svg style="width:150px; height:300px">
    <circle cx="200" cy="200" r="200" fill="#fdd" stroke="none"></circle>
</svg>

正常情况下面浏览器中显示是这个样子的
clipboard.png

产品见了肯定不乐意啊,“我要整个圆啊,谁要这种残次品啊”。
咋整的,要么把这个圆的代码改了吧。
可如果不是简简单单一个圆呢,而是一大堆复杂代码,改个鬼啊。
呵呵~此刻,便是viewBox用武之地!

<svg style="width:150px; height:300px" viewBox="0 0 400 400">
    <circle cx="200" cy="200" r="200" fill="#fdd" stroke="none"></circle>
</svg>

摇身一变就成这样了
clipboard.png

圆变小了,展示全了,还垂直居中了。
一件一件事来,看看发生了什么。

viewBox的四个参数分别代表:最小X轴数值;最小y轴数值;宽度;高度。
前两个暂时用不到,个人理解除非要对内部svg做整体位移,否则一般都是0 0,暂时先不做解释,重点关注后两个参数。
想象一下viewBox是个400*400的正方形,但是单位不是px,也不是任何一个css单位,就当是一个假的单位吧。在viewBox放了一个圆,这个圆的半径是200,单位也不是px,而是变成了和viewBox的单位一模一样的那个假的单位。为啥说是假的呢?因为这个单位代表的长度是会变的,接着看。
svg有个特点,在默认情况下,会调整这个viewBox的大小,让这个viewBox正好能被放进svg里去。拿上面例子来说,viewBox是个正方形,而svg的宽度比高度小,所以就把viewBox的大小缩小到和svg宽度一样,就能正好将viewBox放进svg里来了。所以现在viewBox的实际大小是个150px*150px的正方形。
所以现在可以确定的是,viewBox的一个单位代表的长度 = 150px/400 = 0.375px。
而viewBox内部的所有数值*0.375px才是真正的长度。那个circle的圆心实际上是在坐标75px, 75px的位置上,半径为75px。

圆的具体大小大概就是这么回事。
再看svg那个默认调整viewBox大小是怎么回事儿。
隐藏属性preserveAspectRatio粉墨登场!

preserveAspectRatio又用来干嘛?

英文直译:维持外观比例。好像还挺语义化的一属性名。
对,这是个属性,它和viewBox的关系特别密切。即使不显示声明这个preserveAspectRatio属性,viewBox也是会有这个属性的默认设置的,即preserveAspectRatio="xMidYMid meet"
显示声明方式如下:

<svg style="width:150px; height:300px" viewBox="0 0 400 400" preserveAspectRatio="xMidYMid meet">

说说preserveAspectRatio的这个参数。

第一个参数有9个不同值可选

xMinYMin,
xMinYMid,
xMinYMax,
xMidYMin,
xMidYMid,
xMidYMax,
xMaxYMin,
xMaxYMid,
xMaxYMax

是不是特有规律啊?x和y表示对齐的轴线,min,mid,max表示对齐的方式。min是往坐标小的方向对齐;mid居中对齐;max是往坐标大的方向对齐(顺带一提svg的坐标0刻度在左上角)。

第二个参数有两个值可选:meet和slice
meet就是前面那种自动调整viewBox到可以在svg画布中完全展示。非常类似css里background-size:contain
而slice是自动调整viewBox到撑满整个svg画布。非常类似background-size:cover

再回看第二张图里那个垂直居中的圆,就能明白为什么会“圆变小了,展示全了,还垂直居中了”。

其实说是preserveAspectRatio的第一个参数有9种值,在确定svg画布宽大于高,或者高大于宽的情况下,可以缩减到就3种值,其他值都是重复的,但在不确定画布尺寸时,还是要明确需求选择合适的参数值。

preserveAspectRatio还有个单独使用的参数:"none"。
这种时候viewBox会被拉伸到和svg画布相同尺寸,而内部的所有svg元素也会被等比拉伸,而不是维持原有比例。

第一个知识点整理完毕,待续~ 必须啃完人生第一本原版技术书!!!

查看原文

赞 80 收藏 40 评论 11

tinybear 提出了问题 · 2018-02-28

node的gm模块调用drawText绘制的文本模糊

图片描述

如图所示,绘制的文本是模糊的。
在mac的系统绘制的
代码如下:

    gm(100, 100, "#000000")
    .font('./华文黑体.ttf')
    .fontSize(12)
    .stroke('#ffffff', 1)
    .drawText(10, 50, "一二三高兴,奇怪")
    .write("./brandNewImg.png", function (err) {
        console.log('end')
    });

请教有没有办法处理,变得清晰。

关注 2 回答 1

tinybear 赞了文章 · 2018-01-05

WebAssembly 实践:如何写代码

本文不讨论 WebAssembly 的发展,只是一步一步地教你怎么写 WebAssembly 的各种 demo。文中给出的例子我都放在 GitHub 中了(仓库地址),包含了编译脚本和编译好的可执行文件,只需再有一个支持 WebAssembly 的浏览器就可以直接运行。

配置开发调试环境

安装编译工具

略。 参考官方 Developer’s GuideAdvanced Tools,需要安装的工具有:

安装过程挺繁琐的,得本地 clone 代码再编译。

安装浏览器

作为一个新技术,之所以说 WebAssembly 前途明媚,不仅是因为 W3C 成立了专门的 Webassembly Community Group,被标准认可;也是因为这次各大主流浏览器厂商(难得的)达成了一致,共同参与规范的讨论,在自家的浏览器里都实现了。

体验新技术,建议使用激进版浏览器,最新版本中都已经支持了 WebAssembly。

除了上边几个激进的浏览器,在主流版本里开启 flag 也是可以使用 WebAssembly 的:

  • Chrome: 打开 chrome://flags/#enable-webassembly,选择 enable

  • Firefox: 打开 about:configjavascript.options.wasm 设置为 true

快速体验 WebAssembly

想快速体验 WebAssembly ?最简单的办法就是找个支持 WebAssembly 的浏览器,打开控制台,把下列代码粘贴进去。

WebAssembly.compile(new Uint8Array(`
  00 61 73 6d  01 00 00 00  01 0c 02 60  02 7f 7f 01
  7f 60 01 7f  01 7f 03 03  02 00 01 07  10 02 03 61
  64 64 00 00  06 73 71 75  61 72 65 00  01 0a 13 02
  08 00 20 00  20 01 6a 0f  0b 08 00 20  00 20 00 6c
  0f 0b`.trim().split(/[\s\r\n]+/g).map(str => parseInt(str, 16))
)).then(module => {
  const instance = new WebAssembly.Instance(module)
  const { add, square } = instance.exports

  console.log('2 + 4 =', add(2, 4))
  console.log('3^2 =', square(3))
  console.log('(2 + 5)^2 =', square(add(2 + 5)))

})

里边这一坨奇怪的数字,就是 WebAssembly 的二进制源码。

运行结果

如果报错,说明你的浏览器不支持 WebAssembly ;如果没报错,代码的运行结果如下(还会返回一个 Promise):

2 + 4 = 6
3^2 = 9
(2 + 5)^2 = 49

其中 addsquare 虽然做的事情很简单,就是计算加法和平方,但那毕竟是由 WebAssembly 编译出来的接口,是硬生生地用二进制写出来的!

解释代码

上边的二进制源码一行 16 个数,有 4 行零两个,一共有 66 个数;每个数都是 8 位无符号十六进制整数,一共占 66 Byte。

WebAssembly 提供了 JS API,其中 WebAssembly.compile 可以用来编译 wasm 的二进制源码,它接受 BufferSource 格式的参数,返回一个 Promise。

那些代码里的前几行,目的就是把字符串转成 ArrayBuffer。先将字符串分割成普通数组,然后将普通数组转成 8 位无符号整数的数组;里的数字是十六进制的,所有用了 parseInt(str, 16)

new Uint8Array(
  `...`.trim().split(/[\s\r\n]+/g).map(str => parseInt(str, 16))
)

如果浏览器支持通过 <script type="module"> 的方式引入 wasm 文件,这些步骤都是多余的(他们有这个计划)。

然后,如果 WebAssembly.compile 返回的 Promise fulfilled 了,resolve 方法的第一个参数就是 WebAssembly 的模块对象,是 WebAssembly.Module 的实例。

然后使用 WebAssembly.Instance 将模块对象转成 WebAssembly 实例(第二个参数可以用来导入变量)。

通过 instance.exports 可以拿到 wasm 代码输出的接口,剩下的代码就和和普通 javascript 一样了。

注意数据类型

WebAssembly 是有明确的数据类型的,我那个例子里用的都是 32 位整型数(是不是看不出来…… 二进制里那些 7f 表示 i32 指令,意思就是32位整数),所以用 WebAssembly 编译出来的时候要注意数据类型。

如果你乱传数据,WebAssembly 程序也不会报错,因为在执行时会被动态转换(dynamic_cast),它支持传递模糊类型的数据引用。但是你如果给函数传了个字符串或者超大的数,具体会被转成什么就说不清了,通常是转成 0。

console.log(square('Tom')) // 0
console.log(add(2e+66, 3e+66)) // 0
console.log(2e+66 + 3e+66) // 5e+66

想了解更多关于数据类型的细节,可以参考:Data Types

把 C/C++ 编译成 WebAssembly

有一个在线 C++ 转 wasm 的工具: WasmExplorer

二进制代码简直不是人写的😂,还有其他方式能写 WebAssembly 吗?

有,那就是把其他语言编译成 WebAssembly 的二进制。想实现这个效果,不得不用到各种编译工具了。其中一个比较关键的工具是 Emscripten,它基于 LLVM ,可以将 C/C++ 编译成 asm.js,使用 WASM 标志也可以直接生成 WebAssembly 二进制文件(后缀是 .wasm)。

         Emscripten
source.c   ----->  target.js

     Emscripten (with flag)
source.c   ----->  target.wasm

工具如何安装就不讲了,在此只提醒一点:emcc 在 1.37 以上版本才支持直接生成 wasm 文件。

编写 C 代码

项目代码地址

首先新建一个 C 语言文件,假设叫 math.c 吧,在里边实现 addsquare 方法:

// math.c

int add (int x, int y) {
  return x + y;
}

int square (int x) {
  return x * x;
}

然后执行 emcc math.c -Os -s WASM=1 -s SIDE_MODULE=1 -o math.wasm 就可以生成 wasm 文件了。

代码解释

C 语言代码一目了然,就是写了两个函数,由于 C 语言里的函数都是全局的,这两个函数默认都会被模块导出。

不知道你有没有注意到,这个文件里没写 main 函数!没写入口函数,它自身什么也执行不了,但是可以把它当成一个库文件使用,所以我在也是用模块的方式编译生成的 wasm 文件。

在 WebAssembly 官方给出的例子中,是写了 main 函数,而且是直接把 C 文件编译生成了 html + js + wasm 文件,实际上是生成了一个可以运行 demo,简单粗暴。生成的代码体积比较大,很难看懂里边具体做了什么。为了代码简洁,我这里只是生成 wasm 模块,没有其他多余文件,要想把它运行起来还需要自己写 html 和 js 读取并执行 wasm 文件。(完整代码

如果你也想直接生成可用的 demo,你可以再写个 main 函数,然后执行 emcc math.c -s WASM=1 -o math.html 就可以了。

如何运行 WebAssembly 二进制文件?

现在有了 wasm 文件,也有了支持 WebAssembly 的浏览器,怎么把它运行起来呢?

目前只有一种方式能调用 wasm 里的提供接口,那就是:用 javascript !

官方网站中有一篇 Understanding the JS API 介绍了如何用 JS API 加载并执行 wasm 文件,写的比较粗略。

WebAssembly 目前只设计也只实现了 javascript API,就像我刚开始提供的那个例子一样,只有通过 js 代码来编译、实例化才可以调用其中的接口。这也很好的说明了 WebAssembly 并不是要替代 javascript ,而是用来增强 javascript 和 Web 平台的能力的。我觉得 WebAssembly 更适合用于写模块,承接各种复杂的计算,如图像处理、3D运算、语音识别、视音频编码解码这种工作,主体程序还是要用 javascript 来写的。

编写加载函数 (loader)

在最开始的例子里,已经很简化的将执行 WebAssembly 的步骤写出来了,其实就是 【加载文件】->【转成 buffer】->【编译】->【实例化】。

function loadWebAssembly (path) {
  return fetch(path)                   // 加载文件        
    .then(res => res.arrayBuffer())    // 转成 ArrayBuffer
    .then(WebAssembly.instantiate)     // 编译 + 实例化
    .then(mod => mod.instance)         // 提取生成都模块
}

代码其实很简单,使用了 Fetch API 来获取 wasm 文件,然后将其转换成 ArrayBuffer,然后使用 WebAssembly.instantiate 这个一步到位的方法来编译并初始化一个 WebAssembly 的实例。最后一步是从生成的模块中提取出真正的实例对象。

完成了上边的操作,就可以直接使用 loadWebAssembly 这个方法加载 wasm 文件了,它相当于是一个 wasm-loader ;返回值是一个 Promise,使用起来和普通的 js 函数没什么区别。从 instance.exports 中可以找到 wasm 文件输出的接口。

loadWebAssembly('path/to/math.wasm')
  .then(instance => {
    const { add, square } = instance.exports
    // ...
  })

返回 Promise 不只是因为 fetch 函数,即使像最开始的例子那样把二进制硬编码,也必须要用 Promise 。因为 WebAssembly.compileWebAssembly.instantiate 这些接口都是异步的,本身就返回 Promise 。

更完整的加载函数

如果你直接使用上边那个 loadWebAssembly 函数,有可能会执行失败,因为在 wasm 文件里,可能还会引入一些环境变量,在实例化的同时还需要初始化内存空间和变量映射表,也就是 WebAssembly.MemoryWebAssembly.Table

/**
 * @param {String} path wasm 文件路径
 * @param {Object} imports 传递到 wasm 代码中的变量
 */
function loadWebAssembly (path, imports = {}) {
  return fetch(path)
    .then(response => response.arrayBuffer())
    .then(buffer => WebAssembly.compile(buffer))
    .then(module => {
      imports.env = imports.env || {}

      // 开辟内存空间
      imports.env.memoryBase = imports.env.memoryBase || 0
      if (!imports.env.memory) {
        imports.env.memory = new WebAssembly.Memory({ initial: 256 })
      }

      // 创建变量映射表
      imports.env.tableBase = imports.env.tableBase || 0
      if (!imports.env.table) {
        // 在 MVP 版本中 element 只能是 "anyfunc"
        imports.env.table = new WebAssembly.Table({ initial: 0, element: 'anyfunc' })
      }

      // 创建 WebAssembly 实例
      return new WebAssembly.Instance(module, imports)
    })
}

这个 loadWebAssembly 函数还接受第二个参数,表示要传递给 wasm 的变量,在初始化 WebAssembly 实例的时候,可以把一些接口传递给 wasm 代码。

调用 wasm 导出的接口

有了 loadWebAssembly 就可以调用 wasm 代码导出的接口了。

loadWebAssembly('./math.wasm')
  .then(instance => {
    const add = instance.exports._add
    const square = instance.exports._square

    console.log('2 + 4 =', add(2, 4))
    console.log('3^2 =', square(3))
    console.log('(2 + 5)^2 =', square(add(2 + 5)))
  })

比较奇怪的一点是,用 C/C++ 导出的模块,属性名上默认都带了 _ 前缀,asm.js 转成了 wasm 模块就不带。

在浏览器中的运行效果

参考刚才用 C 语言写出来的项目(代码地址),直接用浏览器打开 index.html 即可。能看到这样的输出(我使用的是 Chrome Canany 浏览器):

Preview in Browser

如果你打开开发者工具的 Source 面板,能够看到 wasm 的源代码,浏览器已经将二进制转换成了对等的文本指令)。

View Source

虽然是一个 wasm 文件,浏览器将它解析成了两个(也有可能更多),是因为我们输出了两个接口,每个文件都对应了一个接口的定义。可以理解为 Canary 浏览器为了方便看源码实现的 sourcemap 功能。

把 asm.js 编译成 WebAssembly

项目代码地址

刚才也介绍了 Emscripten 可以将 C/C++ 编译成 asm.js ,这是它的默认功能,加上 flag 才能生成 wasm 。

asm.js 是 javascript 的子集,是一种语法(不是一个前端工具库!),用了很多底层语法来标注数据类型,目的是提高 javascript 的运行效率,本身就是作为 C/C++ 编译的目标设计的(不是给人写的),可以理解为一种中间表示层语法 (IR, Intermediate Representation)。asm.js 出生于 WebAssembly 之前, WebAssembly 借鉴了这个思路,做的更彻底一些,直接跳过 javascript ,设计了一套新的平台指令。

编写 asm.js 代码

// math.js

function () {
  "use asm";

  function add (x, y) {
    x = x | 0;
    y = y | 0;
    return x + y | 0;
  }

  function square (x) {
    x = x | 0;
    return x * x | 0;
  }

  return {
    add: add,
    square: square
  };
}

上边定义了一个函数,并且声明了 "use asm",这样一来,这个函数就会被视为 asm.js 的模块,里边可以添加方法,通过 return 暴露给外部使用。

不过,目前只有 asm.js 才能转成 wasm,普通 javascript 是不行的! 因为 javascript 是弱类型语言,用法也比较灵活,本身就很难编译成强类型的指令。

使用 Binaryen 和 WABT

虽然 Emscripten 能生成 asm.js 和 wasm ,但是却不能把 asm.js 转成 wasm 。因为它是基于 LLVM 的,然而 asm.js 没法编译成 LLVM IR (Intermediate Representation)。想要把 asm.js 编译成 WebAssembly,就要用到他们官方提供的 BinaryenWABT (WebAssembly Binary Toolkit) 工具了。

原理和编译方法参考官方文档,整个过程大概是这样的:

        Binaryen             WABT
math.js   --->   math.wast   --->   math.wasm

用脚本描述大概是这样:

asm2wasm math.js -o math.wast
wast2wasm math.wast -o math.wasm

wast 是什么格式?

WebAssembly 除了定义了二进制格式以外,还定义了一份对等的文本描述。官方给出的是线性表示的例子,而 wast 是用 S-表达式(s-expressions) 描述的另一种文本格式。

上边的 asm.js 代码编译生成的 wast 文件是这样的:

(module
  (export "add" (func $add))
  (export "square" (func $square))

  (func $add (param $x i32) (param $y i32) (result i32)
    (return
      (i32.add
        (get_local $x)
        (get_local $y)
      )
    )
  )

  (func $square (param $x i32) (result i32)
    (return
      (i32.mul
        (get_local $x)
        (get_local $x)
      )
    )
  )
)

和 lisp 挺像的,反正比二进制宜读多了😂。能看出来最外层声明了是一个模块,然后导出了两个函数,下边紧接着是两个函数的定义,包含了参数列表和返回值的类型声明。如果对这种类似 lisp 的语法比较熟悉,完全可以手写 wast 嘛,只要装个 wast2wasm 小工具就可以生成 wasm 了。或者在这个在线 wast -> wasm 转换工具 里写 wast 代码,可以实时预览编译的结果,也可以下载生成的 wasm 文件。

在 WebAssembly 中调用 Web API

在 js 里能调用 wasm 里定义的方法,反过来,wasm 里能不能调用 javascript 写的方法呢?能不能调用平台提供的方法(Web API)呢?

当然是可以的。不过在 MVP (Minimum Viable Product) 版本里实现的功能有限。要想在 wasm 里调用 Web API,需要在创建 WebAssembly 实例的时候把 Web API 传递过去才可以,具体做法可以参考上边写的那个比较复杂的 loader。(通过 WebAssembly.Table 传变量相当麻烦)。

向 wasm 中传递 js 变量

在有了 loadWebAssembly 这个方法之后,就可以给 wasm 代码传递 js 变量和函数了。

const imports = {
  Math,
  objects: {
    count: 2333
  },
  methods: {
    output (message) {
      console.log(`-----> ${message} <-----`)
    }
  }
}

loadWebAssembly('path/to/source.wasm', imports)
  .then(instance => {
    // ...
  })

上边的代码里给 wasm 模块传递了三个对象: Mathobjectsmethods,分别对应了 Web API 、普通 js 对象、使用了 Web API 的 js 函数。属性名和变量名都并没什么限制,是可以随便起的,把它传递给 loadWebAssembly 方法的第二个参数就可以传递到 wasm 模块中了。

真正实现传递的是 loadWebAssembly 的这行代码:

new WebAssembly.Instance(module, imports)

获取并使用从 js 传递的变量

既然 wasm 的代码最外层声明的是一个模块,我们能向外 export 接口,当然也可以 import 接口。完整代码如下:

(module
  (import "objects" "count" (global $count f32))
  (import "methods" "output" (func $output (param f32)))
  (import "Math" "sin" (func $sin (param f32) (result f32)))

  (export "test" (func $test))
  (func $test (param $x f32)
    (call $output (f32.const 42))
    (call $output (get_global $count))
    (call $output (get_local $x))
    (call $output
      (call $sin
        (get_local $x)
      )
    )
  )
)

这段代码也是在最外层声明了一个 module,然后前三行是 import 语句。首先从 objects 中导入 count 属性,并且在代码里声明为全局的 $count 变量,格式是 32 位浮点数。

(import "objects" "count" (global $count f32))

然后从 methods 中导入 output 方法,声明为一个接受 32 位浮点数作为参数的函数 $output

(import "methods" "output" (func $output (param f32)))

最后从 Math 中导入 sin 方法,声明为一个接受 32 位浮点数作为参数的函数 $sin,返回值也是 32 位浮点数。这样一来就把 js 传递的对象转成了自身模块中可以使用变量。

(import "Math" "sin" (func $sin (param f32) (result f32)))

调用 js 中定义的接口

接下来是定义并且导出了一个 test 函数,接受一个 32 位浮点数作为参数。在 wast 的语法里 call 指令用来调用函数,get_global 用来获取全局变量的值,get_local 用来获取局部变量的值,只能在函数定义中使用。这样来看,test 函数 里执行了四条命令,首先调用 $output 输出了一个常量 42;然后调用 $output 输出全局变量 $count ,这个值是通过 import 获取来的;接着又输出了函数的参数 $x;最后输出了函数参数 $x 调用 Web API $sin 计算后的结果。

  (func $test (param $x f32)
    (call $output (f32.const 42))
    (call $output (get_global $count))
    (call $output (get_local $x))
    (call $output
      (call $sin
        (get_local $x)
      )
    )
  )

编译执行

通过 west2wasm source.wast -o source.wasm 可以生成 wasm 文件,然后使用 loadWebAssembly 编译 wasm 文件。

loadWebAssembly('path/to/source.wasm', imports)
  .then(instance => {
    const { test } = instance.exports
    test(2333)
  })

会得到如下结果:

-----> 42 <-----
-----> 666 <-----
-----> 2333 <-----
-----> 0.9332447648048401 <-----

代码虽然简单,但是实现了向 wasm 中传递变量,并且能在 wasm 中调用 Mathconsole 这种平台接口。如果想要绕过 javascript 直接给 wasm 传参,或者在 wasm 里直接引用 DOM API,就得看他们下一步的计划了。参考 GC / DOM / Web API Integration

结语

根据这篇《如何画马》的教程,相信你很快就能用 WebAssembly 写出来 Angry Bots 这样的游戏啦~ 💪

如何画马

查看原文

赞 88 收藏 141 评论 37

tinybear 赞了回答 · 2017-12-25

解决当在Webpack里配置了别名之后,Webstorm里可以设置路径提示吗

可以的
windows:

File > Settings > Languages and Frameworks >JavaScript | Webpack 

macOS:

WebStorm | Preferences | Languages and Frameworks | JavaScript | Webpack

配置指定的webpack配置

图片描述

参考:

https://www.jetbrains.com/hel...
https://blog.jetbrains.com/we...

关注 5 回答 3

tinybear 赞了文章 · 2017-07-05

Redux异步方案选型

作为react社区最热门的状态管理框架,相信很多人都准备甚至正在使用Redux。

由于Redux的理念非常精简,没有追求大而全,这份架构上的优雅却在某种程度上伤害了使用体验:不能开箱即用,甚至是异步这种最常见的场景也要借助社区方案。

如果你已经挑花了眼,或者正在挑但不知道是否适合,或者已经挑了但不知道会不会有坑,这篇文章应该适合你。

本文会从一些常见的Redux异步方案出发,介绍它们的优缺点,进而讨论一些与异步相伴的常见场景,帮助你在选型时更好地权衡利弊。

简单方案

redux-thunk:指路先驱

Github:https://github.com/gaearon/redux-thunk

Redux作者Dan写的中间件,因官方文档出镜而广为人知。

它向我们展示了Redux处理异步的原理,即:

Redux本身只能处理同步的Action,但可以通过中间件来拦截处理其它类型的action,比如函数(Thunk),再用回调触发普通Action,从而实现异步处理,在这点上所有Redux的异步方案都是类似的

而它使用起来最大的问题,就是重复的模板代码太多:

//action types
const GET_DATA = 'GET_DATA',
    GET_DATA_SUCCESS = 'GET_DATA_SUCCESS',
    GET_DATA_FAILED = 'GET_DATA_FAILED';
    
//action creator
const getDataAction = function(id) {
    return function(dispatch, getState) {
        dispatch({
            type: GET_DATA, 
            payload: id
        })
        api.getData(id) //注:本文所有示例的api.getData都返回promise对象
            .then(response => {
                dispatch({
                    type: GET_DATA_SUCCESS,
                    payload: response
                })
            })
            .catch(error => {
                dispatch({
                    type: GET_DATA_FAILED,
                    payload: error
                })
            }) 
    }
}

//reducer
const reducer = function(oldState, action) {
    switch(action.type) {
    case GET_DATA : 
        return oldState;
    case GET_DATA_SUCCESS : 
        return successState;
    case GET_DATA_FAILED : 
        return errorState;
    }
}

这已经是最简单的场景了,请注意:我们甚至还没写一行业务逻辑,如果每个异步处理都像这样,重复且无意义的工作会变成明显的阻碍。

另一方面,像GET_DATA_SUCCESSGET_DATA_FAILED这样的字符串声明也非常无趣且易错。

上例中,GET_DATA这个action并不是多数场景需要的,它涉及我们将会提到的乐观更新,保留这些代码是为了和下面的方案做对比

redux-promise:瘦身过头

由于redux-thunk写起来实在是太麻烦了,社区当然会有其它轮子出现。redux-promise则是其中比较知名的,同样也享受了官网出镜的待遇。

它自定义了一个middleware,当检测到有action的payload属性是Promise对象时,就会:

  • 若resolve,触发一个此action的拷贝,但payload为promise的value,并设status属性为"success"

  • 若reject,触发一个此action的拷贝,但payload为promise的reason,并设status属性为"error"

说起来可能有点不好理解,用代码感受下:

//action types
const GET_DATA = 'GET_DATA';

//action creator
const getData = function(id) {
    return {
        type: GET_DATA,
        payload: api.getData(id) //payload为promise对象
    }
}

//reducer
function reducer(oldState, action) {
    switch(action.type) {
    case GET_DATA: 
        if (action.status === 'success') {
            return successState
        } else {
               return errorState
        }
    }
}

进步巨大! 代码量明显减少! 就用它了! ?

请等等,任何能明显减少代码量的方案,都应该小心它是否过度省略了什么东西,减肥是好事,减到骨头就残了。

redux-promise为了精简而做出的妥协非常明显:无法处理乐观更新

场景解析之:乐观更新

多数异步场景都是悲观更新(求更好的翻译)的,即等到请求成功才渲染数据。而与之相对的乐观更新,则是不等待请求成功,在发送请求的同时立即渲染数据

最常见的例子就是微信等聊天工具,发送消息时消息立即进入了对话窗,如果发送失败的话,在消息旁边再作补充提示即可。这种交互"乐观"地相信请求会成功,因此称作乐观更新

由于乐观更新发生在用户操作时,要处理它,意味着必须有action表示用户的初始动作

在上面redux-thunk的例子中,我们看到了GET_DATA, GET_DATA_SUCCESSGET_DATA_FAILED三个action,分别表示初始动作异步成功异步失败,其中第一个action使得redux-thunk具备乐观更新的能力。

而在redux-promise中,最初触发的action被中间件拦截然后过滤掉了。原因很简单,redux认可的action对象是 plain JavaScript objects,即简单对象,而在redux-promise中,初始action的payload是个Promise。

另一方面,使用status而不是type来区分两个异步action也非常值得商榷,按照redux对action的定义以及社区的普遍实践,个人还是倾向于使用不同的type,用同一type下的不同status区分action额外增加了一套隐形的约定,甚至不符合该redux-promise作者自己所提倡的FSA,体现在代码上则是在switch-case内再增加一层判断。

redux-promise-middleware:拔乱反正

redux-promise-middleware相比redux-promise,采取了更为温和和渐进式的思路,保留了和redux-thunk类似的三个action。

示例:

//action types
const GET_DATA = 'GET_DATA',
    GET_DATA_PENDING = 'GET_DATA_PENDING',
    GET_DATA_FULFILLED = 'GET_DATA_FULFILLED',
    GET_DATA_REJECTED = 'GET_DATA_REJECTED';
    
//action creator
const getData = function(id) {
    return {
        type: GET_DATA,
        payload: {
            promise: api.getData(id),
            data: id
        }
    }
}

//reducer
const reducer = function(oldState, action) {
    switch(action.type) {
    case GET_DATA_PENDING :
        return oldState; // 可通过action.payload.data获取id
    case GET_DATA_FULFILLED : 
        return successState;
    case GET_DATA_REJECTED : 
        return errorState;
    }
}

如果不需要乐观更新,action creator可以使用和redux-promise完全一样的,更简洁的写法,即:

const getData = function(id) {
    return {
        type: GET_DATA,
        payload: api.getData(id) //等价于 {promise: api.getData(id)}
    }
}

此时初始actionGET_DATA_PENDING仍然会触发,但是payload为空。

相对redux-promise于粗暴地过滤掉整个初始action,redux-promise-middleware选择创建一个只过滤payload中的promise属性XXX_PENDING作为初始action,以此保留乐观更新的能力。

同时在action的区分上,它选择了回归type的"正途",_PENDING_FULFILLED _REJECTED等后缀借用了promise规范 (当然它们是可配置的) 。

它的遗憾则是只在action层实现了简化,对reducer层则束手无策。另外,相比redux-thunk,它还多出了一个_PENDING的字符串模板代码(三个action却需要四个type)。

社区有类似type-to-reducer这样试图简化reducer的库。但由于reducer和异步action通常是两套独立的方案,reducer相关的库无法去猜测异步action的后缀是什么(甚至有没有后缀),社区也没有相关标准,也就很难对异步做出精简和抽象了。

redux-action-tools:软文预警

无论是redux-thunk还是redux-promise-middleware,模板代码都是显而易见的,每次写XXX_COMPLETED这样的代码都觉得是在浪费生命——你得先在常量中声明它们,再在action中引用,然后是reducer,假设像redux-thunk一样每个异步action有三个type,三个文件加起来你就得写九次!

国外开发者也有相同的报怨:

clipboard.png

有没有办法让代码既像redux-promise一样简洁,又能保持乐观更新的能力呢?

redux-action-tools是我给出的答案:

const GET_DATA = 'GET_DATA';

//action creator
const getData = createAsyncAction(GET_DATA, function(id) {
    return api.getData(id)
})

//reducer
const reducer = createReducer()
    .when(getData, (oldState, action) => oldState)
    .done((oldState, action) => successState)
    .failed((oldState, action) => errorState)
    .build()

redux-action-tools在action层面做的事情与前面几个库大同小异:同样是派发了三个action:GET_DATA/GET_DATA_SUCCESS/GET_DATA_FAILED。这三个action的描述见下表:

typeWhenpayloadmeta.asyncPhase
${actionName}异步开始前同步调用参数'START'
${actionName}_COMPLETED异步成功value of promise'COMPLETED'
${actionName}_FAILED异步失败reason of promise'FAILED'

createAsyncAction参考了redux-promise作者写的redux-actions ,它接收三个参数,分别是:

  1. actionName 字符串,所有派生action的名字都以它为基础,初始action则与它同名

  2. promiseCreator 函数,必须返回一个promise对象

  3. metaCreator 函数可选,作用后面会演示到

目前看来,其实和redux-promise/redux-promise-middleware大同小异。而真正不同的,是它同时简化了reducer层! 这种简化来自于对异步行为从语义角度的抽象:

当(when)初始action发生时处理同步更新,若异步成功(done)则处理成功逻辑,若异步失败(failed)则处理失败逻辑

抽离出when/done/failed三个关键词作为api,并使用链式调用将他们串联起来:when函数接收两个参数:actionName和handler,其中handler是可选的,donefailed则只接收一个handler参数,并且只能在when之后调用——他们分别处理`${actionName}_SUCCESS` 和 `${actionName}_FAILED`.

无论是action还是reducer层,XX_SUCCESS/XX_FAILED相关的代码都被封装了起来,正如在例子中看到的——你甚至不需要声明它们! 创建一个异步action,然后处理它的成功和失败情况,事情本该这么简单。

更进一步的,这三个action默认都根据当前所处的异步阶段,设置了不同的meta(见上表中的meta.asyncPhase),它有什么用呢?用场景说话:

场景解析:失败处理与Loading

它们是异步不可回避的两个场景,几乎每个项目会遇到。

以异步请求的失败处理为例,每个项目通常都有一套比较通用的,适合多数场景的处理逻辑,比如弹窗提示。同时在一些特定场景下,又需要绕过通用逻辑进行单独处理,比如表单的异步校验。

而在实现通用处理逻辑时,常见的问题有以下几种:

  1. 底层处理,扩展性不足

    function fetchWrapper(args) {
        return fetch.apply(fetch, args)
            .catch(commonErrorHandler)
    }

    在较底层封装ajax库可以轻松实现全局处理,但问题也非常明显:

    一是扩展性不足,比如少数场景想要绕过通用处理逻辑,还有一些场景错误是前端生成而非直接来自于请求;

    二是不易组合,比如有的场景一个action需要多个异步请求,但异常处理和loading是不需要重复的,因为用户不需要知道一个动作有多少个请求。

  2. 不够内聚,侵入业务代码

    //action creator
    const getData = createAsyncAction(GET_DATA, function(id) {
        return api.getData(id)
            .catch(commonErrorHandler) //调用错误处理函数
    })

    在有业务意义的action层调用通用处理逻辑,既能按需调用,又不妨碍异步请求的组合。但由于通用处理往往适用于多数场景,这样写会导致业务代码变得冗余,因为几乎每个action都得这么写。

  3. 高耦合,高风险

    也有人把上面的方案做个依赖反转,改为在通用逻辑里监听业务action:

    function commonErrorReducer(oldState, action) {
        switch(action.type) {
        case GET_DATA_FAILED:
        case PUT_DATA_FAILED:
        //... tons of action type
            return commonErrorHandler(action)
        }
    }

    这样做的本质是把冗余从业务代码中拿出来集中管理。

    问题在于每添加一个请求,都需要修改公共代码,把对应的action type加进来。且不说并行开发时merge冲突,如果加了一个异步action,但忘了往公共处理文件中添加——这是很可能会发生的——而异常是分支流程不容易被测试发现,等到发现,很可能就是事故而不是bug了。

通过以上几种常见方案的分析,我认为比较完善的错误处理(Loading同理)需要具备如下特点:

  • 面向异步动作(action),而非直接面向请求

  • 不侵入业务代码

  • 默认使用通用处理逻辑,无需额外代码

  • 可以绕过通用逻辑

而借助redux-action-tools提供的meta.asyncPhase,可以轻易用middleware实现以上全部需求!

import _ from 'lodash'
import { ASYNC_PHASES } from 'redux-action-tools'

function errorMiddleWare({dispatch}) {
  return next => action => {
    const asyncStep = _.get(action, 'meta.asyncStep');

    if (asyncStep === ASYNC_PHASES.FAILED) {
      dispatch({
        type: 'COMMON_ERROR',
        payload: {
          action
        }
      })
    }
    
    next(action);
  }
}

以上中间件一旦检测到meta.asyncStep字段为FAILED的action便触发新的action去调用通用处理逻辑。面向action、不侵入业务、默认工作 (只要是用createAsyncAction声明的异步) ! 轻松实现了理想需求中的前三点,那如何定制呢?既然拦截是面向meta的,只要在创建action时支持对meta的自定义就行了,而createAsyncAction的第三个参数就是为此准备的:

import _ from 'lodash'
import { ASYNC_PHASES } from 'redux-action-tools'

const customizedAction = createAsyncAction(
  type, 
  promiseCreator, //type 和 promiseCreator此处无不同故省略
  (payload, defaultMeta) => {
    return { ...defaultMeta, omitError: true }; //向meta中添加配置参数
  }
)

function errorMiddleWare({dispatch}) {
  return next => action => {
    const asyncStep = _.get(action, 'meta.asyncStep');
    const omitError = _.get(action, 'meta.omitError'); //获取配置参数

    if (!omitError && asyncStep === ASYNC_PHASES.FAILED) {
      dispatch({
        type: 'COMMON_ERROR',
        payload: {
          action
        }
      })
    }
    
    next(action);
  }
}

类似的,你可以想想如何处理Loading,需要强调的是建议尽量用增量配置的方式进行扩展,而不要轻易删除和修改meta.asyncPhase

比如上例可以通过删除meta.asyncPhase实现同样功能,但如果同时还有其它地方也依赖meta.asyncPhase(比如loadingMiddleware),就可能导致本意是定制错误处理,却改变了Loading的行为,客观来讲这层风险是基于meta拦截方案的最大缺点,然而相比多数场景的便利、健壮,个人认为特殊场景的风险是可以接受的,毕竟这些场景在整个开发测试流程容易获得更多关注。

进阶方案

上面所有的方案,都把异步请求这一动作放在了action creator中,这样做的好处是简单直观,且和Flux社区一脉相承(见下图)。因此个人将它们归为相对简单的一类。

action_creator

下面将要介绍的,是相对复杂一类,它们都采用了与上图不同的思路,去追求更优雅的架构、解决更复杂的问题

redux-loop:分形! 组合!

众所周知,Redux是借鉴自Elm的,然而在Elm中,异步的处理却并不是在action creator层,而是在reducer(Elm中称update)层:

elm_arch

图片来源于: https://github.com/jarvisaoie...

这样做的目的是为了实现彻底的可组合性(composable)。在redux中,reducer作为函数是可组合的,action正常情况下作为纯对象也是可组合的,然而一旦涉及异步,当action嵌套组合的时候,中间件就无法正常识别,这个问题让redux作者Dan也发出感叹 There is no easy way to compose Redux applications并且开了一个至今仍然open的issue,对组合、分形与redux的故事,有兴趣的朋友可以观摩以上链接,甚至了解一下Elm,篇幅所限,本文难以尽述。

而redux-loop,则是在这方面的一个尝试,它更彻底的模仿了Elm的模式:引入Effects的概念并将其置入reducer,官方示例如下:

import { Effects, loop } from 'redux-loop';
import { loadingStart, loadingSuccess, loadingFailure } from './actions';

export function fetchDetails(id) {
  return fetch(`/api/details/${id}`)
    .then((r) => r.json())
    .then(loadingSuccess)
    .catch(loadingFailure);
}

export default function reducer(state, action) {
  switch (action.type) {
    case 'LOADING_START':
      return loop(
        { ...state, loading: true },
        Effects.promise(fetchDetails, action.payload.id)
      ); // 同时返回状态与副作用

    case 'LOADING_SUCCESS':
      return {
        ...state,
        loading: false,
        details: action.payload
      };

    case 'LOADING_FAILURE':
      return {
        ...state,
        loading: false,
        error: action.payload.message
      };

    default:
      return state;
  }
}

注意在reducer中,当处理LOADING_START时,并没有直接返回state对象,而是用loop函数将state和Effect"打包"返回(实际上这个返回值是数组[State, Effect],和Elm的方式非常接近)。

然而修改reducer的返回类型显然是比较暴力的做法,除非Redux官方出面,否则很难获得社区的广泛认同。更复杂的返回类型会让很多已有的API,三方库面临危险,甚至combineReducer都需要用redux-loop提供的定制版本,这种"破坏性"也是Redux作者Dan没有采纳redux-loop进入Redux核心代码的原因:"If a solution doesn’t work with vanilla combineReducers(), it won’t get into Redux core"。

对Elm的分形架构有了解,想在Redux上继续实践的人来说,redux-loop是很好的参考素材,但对多数人和项目而言,最好还是更谨慎地看待。

redux-saga:难、而美

Github: https://github.com/yelouafi/r...

另一个著名的库,它让异步行为成为架构中独立的一层(称为saga),既不在action creator中,也不和reducer沾边。

它的出发点是把副作用 (Side effect,异步行为就是典型的副作用) 看成"线程",可以通过普通的action去触发它,当副作用完成时也会触发action作为输出。

import { takeEvery } from 'redux-saga'
import { call, put } from 'redux-saga/effects'
import Api from '...'

function* getData(action) {
   try {
      const response = yield call(api.getData, action.payload.id);
      yield put({type: "GET_DATA_SUCCEEDED", payload: response});
   } catch (e) {
      yield put({type: "GET_DATA_FAILED", payload: error});
   }
}

function* mySaga() {
  yield* takeEvery("GET_DATA", getData);
}

export default mySaga;

相比action creator的方案,它可以保证组件触发的action是纯对象,因此至少在项目范围内(middleware和saga都是项目的顶层依赖,跨项目无法保证),action的组合性明显更加优秀。

而它最为主打的,则是可测试性和强大的异步流程控制

由于强制所有saga都必须是generator函数,借助generator的next接口,异步行为的每个中间步骤都被暴露给了开发者,从而实现对异步逻辑"step by step"的测试。这在其它方案中是很少看到的 (当然也可以借鉴generator这一点,但缺少约束)。

而强大得有点眼花缭乱的API,特别是channel的引入,则提供了武装到牙齿级的异步流程控制能力。

然而,回顾我们在讨论简单方案时提到的各种场景与问题,redux-saga并没有去尝试回答和解决它们,这意味着你需要自行寻找解决方案。而generator、相对复杂的API和单独的一层抽象也让不少人望而却步。

包括我在内,很多人非常欣赏redux-saga。它的架构和思路毫无疑问是优秀甚至优雅的,但使用它之前,最好想清楚它带来的优点(可测试性、流程控制、高度解耦)与付出的成本是否匹配,特别是异步方面复杂度并不高的项目,比如多数以CRUD为主的管理系统。

场景解析:竞态

说到异步流程控制很多人可能觉得太抽象,这里举个简单的例子:竞态。这个问题并不罕见,知乎也有见到类似问题

简单描述为:

由于异步返回时间的不确定性,后发出的请求可能先返回,如何确保异步结果的渲染是按照请求发生顺序,而不是返回顺序?

这在redux-thunk为代表的简单方案中是要费点功夫的:

function fetchFriend(id){
    return (dispatch, getState) => {
        //步骤1:在reducer中 set state.currentFriend = id;
        dispatch({type: 'FETCH_FIREND', payload: id}); 

        return fetch(`http://localhost/api/firend/${id}`)
            .then(response => response.json())
            .then(json => { 
                //步骤2:只处理currentFriend的对应response
                const { currentFriend } = getState();
                (currentFriend === id) && dispatch({type: 'RECEIVE_FIRENDS', playload: json})
            });
    }
}

以上只是示例,实际中不一定需要依赖业务id,也不一定要把id存到store里,只要为每个请求生成key,以便处理请求时能够对应起来即可。

而在redux-saga中,一切非常地简单:

import { takeLatest } from `redux-saga`

function* fetchFriend(action) {
  ...
}

function* watchLastFetchUser() {
  yield takeLatest('FETCH_FIREND', fetchFriend)
}

这里的重点是takeLatest,它限制了同步事件与异步返回事件的顺序关系。

另外还有一些基于响应式编程(Reactive Programming)的异步方案(如redux-observable)也能非常好地处理竞态场景,因为描述事件流之间的关系,正是整个响应式编程的抽象基石,而竞态在本质上就是如何保证同步事件与异步返回事件的关系,正是响应式编程的用武之地。

小结

本文包含了一些redux社区著名、非著名 (恩,我的redux-action-tools) 的异步方案,这些其实并不重要。

因为方案是一家之作,结论也是一家之言,不可能放之四海皆准。个人更希望文中探讨过的常见问题和场景,比如模板代码、乐观更新、错误处理、竞态等,能够成为你选型时的尺子,为你的权衡提供更好的参考,而不是等到项目热火朝天的时候,才发现当初选型的硬伤。

查看原文

赞 88 收藏 149 评论 14

tinybear 关注了标签 · 2017-02-09

关注 2161

认证与成就

  • 获得 53 次点赞
  • 获得 25 枚徽章 获得 0 枚金徽章, 获得 9 枚银徽章, 获得 16 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-05-02
个人主页被 900 人浏览