在与后台理顺授权、登录逻辑之后,就开始小程序的页面结构开发了。

既然用上了 vant,所以也就直接拿来用了 https://youzan.github.io/vant-weapp/#/tabbar。

使用vant组件库的好处,不用自己画图标,这对于一个开发人员的我来说,很重要不用自己自己画图标啊,自己画的是神马连自己都不知道。所以,带图标库的组件库,是令我非常欢喜的。

tabbar的使用需要结合wx小程序的官方文档 https://developers.weixin.qq.com/miniprogram/dev/framework/ability/custom-tabbar.html

在 app.json 声明 tabbar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"tabBar": {
  "custom": true,
  "backgroundColor": "#ffffff",
  "list": [{
    "pagePath": "pages/about/about",
    "text": "关于"
  }, {
    "pagePath": "pages/home/home",
    "text": "首页"
  }, {
    "pagePath": "pages/my/my",
    "text": "我的"
  }]
},

wx对自定义的tabbar有相应的介绍,在代码根目录下添加入口文件

1
2
3
4
custom-tab-bar/index.js
custom-tab-bar/index.json
custom-tab-bar/index.wxml
custom-tab-bar/index.wxss

需要在 custom-tab-bar/index.json 中,增加 vant 依赖

1
2
3
4
"usingComponents": {
  "van-tabbar": "@vant/weapp/tabbar/index",
  "van-tabbar-item": "@vant/weapp/tabbar-item/index"
}

在 custom-tab-bar/index.wxml 增加

1
2
3
4
5
<van-tabbar active="" bind:change="onChange">
  <van-tabbar-item icon="search">关于</van-tabbar-item>
  <van-tabbar-item icon="home-o">首页</van-tabbar-item>
  <van-tabbar-item icon="search">我的</van-tabbar-item>
</van-tabbar>

在 custom-tab-bar/index.js 增加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
data: {
  selected: 0,
  list: [
    "/pages/about/about",
    "/pages/home/home",
    "/pages/my/my"
  ]
}
...
{
  methods: {
    onChange(event) {
      const selected = event.detail
      this.setData({ selected })
      wx.switchTab({ url: this.data.list[selected]})
    }
  }
}

这里有会问题,tabbar的激活状态是不能正常显示的。

实际上 switchTab 切换过去时,会实例化一个 tabbar,实例化的新 tabbar 中的 selected 会默认为 0,所以会有这一问题。

wx 提供了 getTabbar() 方法来获取当前页面的 tabbar。所以,需要在 Page 完成初始化之后,调用

1
2
3
4
5
6
// /pages/about/about
Page({
  onShow: function() {
    this.getTabBar().setData({selected: 0})
  }
})

需要在每一个页面的 onShow 方法都调用一次,设置上不同的 selected 的值。但是,怎么才能自动地选择正确的 selected 的值呢?可以通过路由判断。怎么获取当前页面的路由呢?

注意到了 vant 示例中有代码块,帮忙解决了问题。

wx 可以通过 getCurrentPages() 得到 App 的页面栈。

PageObject[] getCurrentPages() 获取当前页面栈。数组中第一个元素为首页,最后一个元素为当前页面。

1
2
3
4
5
6
7
8
9
10
11
// custom-tab-bar/index.js 
// 在 methods 里增加
{
  methods: {
    init: {
      const page = getCurrentPages().pop();
      const selected = this.data.list.findIndex(item => item === `/${page.route}`)
      this.setData({ selected })
    }
  }
}

所以,可以将tab页面的 onShow 中的代码调整为

1
this.getTabBar().init()

即可,统一让 tabbar 的内部方法去处理 selected 的值。

但是,这样会有一个问题,Page 每次切换的时候,都会重新设置这个 selected 值,如果页面都自有 tabbar,能不能只设置一次呢?页面每次加载时会不会重新构建 tabbar 实例呢?

但是,通过日志查看,getTabBar() 中的 __wxWebviewId__ 是不变的,但奇怪的是 tabbar 中的 selected 的值是相互影响的。

即,切换过的 tab 页面后,三个页面只有三个 __wxWebviewId__的值,无论切换几次。但,如果从tab1切换到tab2后,在 Page => this.getTabBar().init() => this.selected 的值会是0, 而不是之前 tab2 实例化过的selected 值 1。

tabbar中的 __wxExparserNodeId__ 的值是三个,和 __wxWebviewId__ 是对应、固定的,难道 wx 对 tabbar 进行了 data 域的数据共享?

tabbar 挂载到页面上,会单独生成,不会重新生成,但会存在 data 域的数据共享。

另外在社区中发现,可以通过 Component 替换 Page,通过使用 Component 的 pageLifetimes => show方法来达到 Page 中的 onShow 效果。

可能会有很多坑吧,一个一个记录下来

小程序出来不长时间的很久之前,做了一个“丢丢测运”的小程序,没有后端的逻辑;全部使用前端进行计算操作。 当初,纯粹地想体验一下小程序的开发过程。但当时的那个对我来说,只是简单的前端开发,没有真正使用到wx提供的任何API。

最近,在折腾一下小程序,记录下来踩坑的过程吧。


折腾之前,跟朋友了解到,现在比较多的使用到的第三方的组件库有 vant。之前小程序使用的是官方的组件,页面相对简单,这次就使用 vant 组件。

好,第一坑就是来自 vant。

在使用wx开发工具,构建好一个小程序时候,需要安装 vant 组件库,相关参照 https://youzan.github.io/vant-weapp/#/quickstart。

1
2
3
4
5
6
7
* npm i @vant/weapp -S --production
* 开发工具上构建 npm

<!-- 如果没有成功,尝试以下过程 -->
* npm init # 重新初始化
* npm i @vant/weapp -S --production
* 开发工具上构建 npm

看过小程序的官网,应该知道,安装组件时建议使用 production 以减少包大小。“构建 npm”的作用是,将 node_modules 的依赖包,复制到 miniprogram_npm 目录中。(想到了什么呢,其实手动走这个流程应该也是可以的,不过我没有尝试过)

vant 在示例网站上,显示的使用是1.x,但实际上我安装后的版本是 0.5.x。部分组件是不存在的,node_modules下的组件目录结构也是不一样的。

使用指定版本安装,提示说没有找到,很尴尬。后来直接在 package.json 中指定资源的 github 链接,是能下载下来了。当时的最新版本是 v1.2.0,这是一个有坑的版本。

问题类似于 https://github.com/youzan/vant-weapp/issues?q=bem , 可能是团队在兼容wepy时引入的问题,由于时间关系没有去细究,社区也比较活跃,修复应该也比较快的。

果不其然,github然后连续出了 0.5.28 的版本,后又出了 1.2.1 的版本,修复了相关问题。

这,就是使用第三方组件库带来的风险。上手就遇到组件库的问题,比较打击人的动力啊。 所以,在选择引入第三方库时,需要考虑库的活跃度的问题,慎重。

Comments

朋友需要帮忙,使用地图进行数据的展示。网上,不少人使用 mapbox,看了些示例与文档,手动折腾了一下,确实是强大。

下面将折腾的过程记录下来。

先说一下需求:朋友需要对某个城市的路线中的一些路线进行颜色的特殊标识,比如,广州有很多道路,需要将有包含人名的道路标识出来。另外,人名分男女,用不同颜色进行区分显示。

地图信息,本质是不同图层的叠加,比如地形图、卫星图、道路图等,需要显示哪些图层,勾选就可以,也可以同时显示多个图层,它们会叠加,类似ps。

那么,要实现需求,需要完整的广州道路图、标识的道路图。

  • 第一步,我们需要现有的广州道路的地图,一个底图。

翻阅了官网,发现一个浅色系的地图 mapbox://styles/mapbox/light-v9,这个地图适合当背景,这样标识的道路图可以选择较亮眼的颜色。

  • 第二步,需要有标识人名道路的图层。

怎么拥有这个图层呢?首先,需要理解,图层的本质是道路的数据,只要有道路的数据就可以了。

那么怎么有需要标识的道路数据呢?如果别人整理过,可以直接拿geo的坐标数据集。但是,一般人不会刚好想到你的需求,刚好整理了出来,刚好分享到网络上。

由于是自己选择的道路,所以数据需要自己整理,Mapbox 这点就很好了。地图道路的数据实际上是一系列的坐标点,Mapbox 允许你直接在地图在通过画线,来得到这些数据。也就是,可以通过直观的操作,得到geo的数值数据。

mapbox 有 dataset 和 tileset 的概念, dataset 就是数据集,tileset贴图集。dataset 就是用来存储标志的路线坐标数据的, 如果直接使用数值数据渲染,可以足够了。

在 mapbox 需要通过网络使用 dataset,需要生成相应的 tileset 才能使用。在 https://studio.mapbox.com/datasets 在查看自己的 dataset,在 https://studio.mapbox.com/tilesets 在查看自己的 tileset。


那么,先看看直接使用数值数据的例子。在这个例子中,需要将 dataset 中的坐标值,手动复制到相应的代码块中,效果见 index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
<!DOCTYPE html>
<html>

<head>
  <meta charset='utf-8' />
  <title>Display a map</title>
  <meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
  <script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.49.0/mapbox-gl.js'></script>
  <link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.49.0/mapbox-gl.css' rel='stylesheet' />
  <style>
    body {
      margin: 0;
      padding: 0;
      height: 100%;
      width: 100%;
    }

    /* 外部div样式 */
    #container {
      margin: 20px auto;
      width: 800px;
      height: 400px;
      position: relative;
    }

    #map {
      position: absolute;
      top: 0;
      bottom: 0;
      width: 100%;
    }
  </style>
</head>

<body>
  <!-- 增加外部div,限制高宽 -->
  <div id="container">
    <div id='map'></div>
  </div>

  <script>
    mapboxgl.accessToken = 'pk.eyJ1Ijoic2hhdGxlIiwiYSI6ImNqczF4NzgxdTA3dnc0NHA4aG5sdDk3Y2gifQ.Mr3uL3avjz6PD6zkCvwxPw';
    const map = new mapboxgl.Map({
      container: 'map',
      style: 'mapbox://styles/mapbox/light-v9', // 这是灰色地图
      center: [113.374233, 23.137433],
      zoom: 15
    });

    // 当 map 地图加载完成后,添加图层
    // 下面数据有三个特征 feature 数据, 一个是红色的路线,两个是天蓝色的路线
    // 可以通过js来控制加载哪些特征数据,就可以做到区别显示了
    map.on('load', function () {
      // 红色
      map.addLayer({
        'id': 'redlines',
        'type': 'line',
        'source': {
          'type': 'geojson',
          'data': {
            'type': 'FeatureCollection',
            'features': [{ // 这里是红色 dataset 
              'type': 'Feature',
              'properties': {
                'color': '#F7455D' // red
              },
              'geometry': {
                'type': 'LineString',
                'coordinates': [
                  [113.371858, 23.137009],
                  [113.37309, 23.136758],
                  [113.373807, 23.1366],
                  [113.374053, 23.136553]
                ]
              }
            }]
          }
        },
        'paint': {
          'line-width': 3,
          'line-color': ['get', 'color']
        }
      });

      // 天蓝
      map.addLayer({
        'id': 'bluelines',
        'type': 'line',
        'source': {
          'type': 'geojson',
          'data': {
            'type': 'FeatureCollection',
            'features': [{ // 这里是天蓝 dataset 
                'type': 'Feature',
                'properties': {
                  'color': '#00ffff'
                },
                'geometry': {
                  'type': 'LineString',
                  'coordinates': [
                    [
                      113.37421857524919,
                      23.13651914382298
                    ],
                    ...
                  ]
                }
              },
              { //这里是天蓝 另外一条路线
                'type': 'Feature',
                'properties': {
                  'color': '#00ffff'
                },
                'geometry': {
                  'type': 'LineString',
                  'coordinates': [
                    [
                      113.37539098241979,
                      23.13607741667701
                    ],
                    ...
                  ]
                }
              }
            ]
          }
        },
        'paint': {
          'line-width': 3,
          'line-color': ['get', 'color']
        }
      });
    });
  </script>

</body>

</html>

由于后期还需要区分操作显示,再次过了一下官网上的例子,有通过按钮点击,切换显示不同路线图层的方法,另外还有直接使用 tileset 进行异步请求数值数据的方法,不用复制数值数据了,效果见 switch.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
<!DOCTYPE html>
<html>

<head>
  <meta charset='utf-8' />
  <title>Show and hide layers</title>
  ...
</head>

<body>

  <style>
    #menu {
      background: #fff;
      position: absolute;
      z-index: 1;
      top: 10px;
      right: 10px;
      border-radius: 3px;
      width: 120px;
      border: 1px solid rgba(0, 0, 0, 0.4);
      font-family: 'Open Sans', sans-serif;
    }

    #menu a {
      font-size: 13px;
      color: #404040;
      display: block;
      margin: 0;
      padding: 0;
      padding: 10px;
      text-decoration: none;
      border-bottom: 1px solid rgba(0, 0, 0, 0.25);
      text-align: center;
    }

    #menu a:last-child {
      border: none;
    }

    #menu a:hover {
      background-color: #f8f8f8;
      color: #404040;
    }

    #menu a.active {
      background-color: #3887be;
      color: #ffffff;
    }

    #menu a.active:hover {
      background: #3074a4;
    }
  </style>

  <div id="container">
    <nav id="menu"></nav>
    <div id="map"></div>
  </div>

  <script>
    mapboxgl.accessToken = 'pk.eyJ1Ijoic2hhdGxlIiwiYSI6ImNqczF4NzgxdTA3dnc0NHA4aG5sdDk3Y2gifQ.Mr3uL3avjz6PD6zkCvwxPw';
    var map = new mapboxgl.Map({
      container: 'map',
      style: 'mapbox://styles/mapbox/light-v9',
      zoom: 15,
      center: [113.374233, 23.137433]
    });

    map.on('load', function () {
      map.addSource('red', {
        type: 'vector',
        url: 'mapbox://shatle.cjs1z8leu4hge2xpkx5s9faxa-814td' // 这个是 tileset 中的 Map ID
      });
      map.addLayer({
        'id': 'red',
        'type': 'line',
        'source': 'red',
        'source-layer': 'lcm-test',  // 这个是tileset 中的一图层名称
        'layout': {
          'visibility': 'visible',
          'line-join': 'round',
          'line-cap': 'round'
        },
        'paint': {
          'line-color': '#ff0000',
          'line-width': 1
        }
      });
      // 同理上一个数据,构建第二个分类数据
      map.addSource('blue', {
        type: 'vector',
        url: 'mapbox://shatle.cjs3g5w7k01us2wmudbbgjxks-6t25j'
      });
      map.addLayer({
        'id': 'blue',
        'type': 'line',
        'source': 'blue',
        'source-layer': 'lcmtest2',
        'layout': {
          'visibility': 'visible',
          'line-join': 'round',
          'line-cap': 'round'
        },
        'paint': {
          'line-color': '#00ffff',
          'line-width': 1
        }
      });
    });

    var toggleableLayerIds = ['red', 'blue'];

    for (var i = 0; i < toggleableLayerIds.length; i++) {
      var id = toggleableLayerIds[i];

      var link = document.createElement('a');
      link.href = '#';
      link.className = 'active';
      link.textContent = id;

      link.onclick = function (e) {
        var clickedLayer = this.textContent;
        e.preventDefault();
        e.stopPropagation();

        var visibility = map.getLayoutProperty(clickedLayer, 'visibility');

        if (visibility === 'visible') {
          map.setLayoutProperty(clickedLayer, 'visibility', 'none');
          this.className = '';
        } else {
          this.className = 'active';
          map.setLayoutProperty(clickedLayer, 'visibility', 'visible');
        }
      };

      var layers = document.getElementById('menu');
      layers.appendChild(link);
    }
  </script>

</body>

</html>

Comments

去年,项目组事情比较多,每个人的工作量都有些大,经常加班深夜,导致整体团队都很累,所以,急需招入小伙伴。 在之前的前端人员离职之后,增加开发人员越加紧迫。下面,整理一下面试的体会。

个人总结

参与的是技术面试,应试人员过来时会有一份笔试。当应试人员完成试卷时,我们才下去进行真正的面试。 我作为一个辅助的技术面试人员,主要是补充提问。

在面试的过程中,上级领导也给出了些不少的建议,指出改进的地方。

个人介绍

初期,作为技术面试,上来就会对应试人员进行题面的提问。 这里有个问题,忽略了人的基本属性。招聘进来的是人,一个需要参与团队的人,而不是简单的一个技术机器。

个人介绍,通常可以提前准备,也可以没有准备。但无论如何,从中都可以看出应该的基本语言表达能力。或者,如果连这准备都没有,可以看出是否重要此次的面试。

在应试人员的自我介绍时,只需要简单的抓住关键点,了解其基本表达能力。同时,面试人员可以快速的浏览其做题的情况,在心里有个大概的提问方向。

笔试提问

前端的题面,包括js/css/jQuery的基础、算法、工作常遇到的问题等。

js基础,可以看出应试人员在开发过程中的细心程度,但通常工作之后的应试人员表现得都不是很理想,反而是实习生在答案上表现得优异。

做为过来人,是知道其原因的,也很容易理解。实习生有足够的时间来准备各种面试,而还在工作的应试人员就不一样,他们需要完成正常工作中的任务。

需要找到一个基础很好,又有工作经验的人,也不是简单地找一个年限够长的在职人员。关键在于,应试人员是否有意识去注意使用中的细节。

算法,应试人员基本都不怎么样,反而是实习生会好些。前端的工作,了解需求,对接后端的接口,并实现设计人员的交互,最大限度地提升用户体验。通常来说,后端返回的数据会避免过大的数据,以便于处理,致使前端的算法机能表现弱些。

在编程时还是需要注意细节,比如循环与零比较都是可以作为优化的点。现在编程中,习惯使用与underscode类似的lodash进行各操作,ES6也提供了较先进的方法,但最终还是希望有优化编辑的意识。

其中,令我好奇的是,有些人一直使用for循环,而不去寻找更为方便的underscore/lodash工具进行数据处理,这是不是也反应出一部分人对编程没有优化/简化的想法。

工作中遇到的问题,比如如何避免附件缓存等,都是常见的问题。如果实在没有遇到,也是考验应试人员的一个点。但如果面试过程良好,还会话面时进行相关提问。

笔试提问,可以看到应试人员基技术基础好不好,还可以看到其快速理解力,及对题目回答的表达能力等。工作中,这些都是基础的。

话面

抛开题目,进入话面。希望看到应试人员几点:

  • 生活方面是否适合当前团队。

    应试人员是哪里人,现居住在哪里。作为一个开发人员,工作中可能会有些意料不到事情,是否可以快速反应并到场解决问题。

    工作肯定会有各方面的压力,通常是怎么释放的。工作之余有什么爱好,比如运动、聚会什么的,也是值得参考的。即使碰到对编程极度热爱的人员,通常也会喜欢听歌、看电影。

    是否结婚,有无男女朋友,对象的工作地点,这些也会对是否成功入职有一定的影响,所以也是需要考虑的。

  • 技术知识点是否是团队需要的。

    技术知识点太多,首先会在简历上做了一层过滤,这可以过滤大部分的不是团队需要的人员。

    另外,出于开放的原则,会对部分未完全匹配的简介给予面试的机会,希望能够找出优秀的人员。

    比如,工作中只使用过 AngularJS 或 VueJS,而现有的工作需求是 React,那么也会给予机会面试。毕竟,很多框架类的东西是相通的。但,没有 React 项目经验的人员,需要注意提问其掌握框架的大致时间,了解其学习、快速上手的能力。

    如果掌握需求匹配的技术,还需要深入的了解技术细节,实践过程中遇到的问题和解决方法。

  • 理解问题能力

    理解问题,可以从笔试部分了解到。遇到些应试人员,在做笔试部分时就没有读懂题目。这原因可能是一时的疏忽,但本人觉得更多的是没有相应的知识概念,这点并不可耻,毕竟每个人的知识范围都是有限的。

    在话面过程中,我们也会提到一些技术或者其它的问题。通常,这些问题都是根据应试人员的经历来进行提问的,也有些关联性可能较远的问题,试图去了解到应试人员对熟悉领域中的问题是否有快速的反应能力,对不熟悉领域是否有解决和求知的欲望。

  • 正常沟通能力

    沟通能力,这是必须的,也是很重要的。

Comments

最近的炒币

最近,有同事比较热衷于炒虚拟币,都是打着区块链的技术,炒各种概念币种。

同事炒了国内的一个井通币,投入几千块钱,翻倍回来了,羡慕这敢行动的行为。

于是,自己也下载了些炒币应用,有火币、OKex、井通这三个,还有其它小的应用。

火币和OKex的交易应用上线美国地区的苹果商店;井通的应用没有上线苹果商店,也没有交易平台;其它只是用来跟进比较,没有使用。

井通币升到0.28时,我想跟进同事的脚步的,但是井通平台的充值只能通过QQ进行转账。确实比较山寨,我在联系充值客服时,没有成功,结果没有充上。结果,第二天就开始下跌了,直到今天0.06左右。

虽然有些庆幸没有入井通这一个坑,但是,我并不因此觉得自己多明智。我在意的是,为什么我没有进去的勇气?

于是上周末,我逼了自己一下,通过火币网卖了1500元的USDT币。但是,很多币币交易只能使用BTC和ETH进行交易,所以我又用USDT买了ETH,由于钱太少了,买不了多少的BTC,交易起来前面的小数点太多。

在买USDT之后,由于USDT的价格比国家的汇率要高很多,所以,买完之后,显示的价值已经少了100多块了。接着,交易ETH之后,手续费需要0.2%,同时当前的ETH又在下跌,整体看,少了二百多块。

前段时间我得到的结果时,只有新币发行时,才会有比较大的上涨幅度,类似于新股上市;发行比较久的币种上涨比较小,并且多数是在下跌的。

所以,只买卖新币种

不幸的是,整体的币种价值在下跌,我的原始资本一直在消耗。所以,决定开始行动。

中午,12点有发行新币种 THETA,我晚些进场,没有赶上较低的时候,当前已经比发行价上升了30%多。我直接买进,十几分钟后,在发行价45%左右出手,换回了ETH,赚了点。

下午2点又有新币种LET,根据上个经验,我快手地进入效果界面,直接交易出了所有的 ETH,但是,我回头一看,当前的涨幅已经是发行价的200%以上了,不到一分钟,就这个状态了,当我意识到时,已经下跌到60%多了,价值直接扣半,后悔。

买卖不能心急,宁愿错过,不能做错

需要看清本质,翻两倍肯定是危险的,不可取的。三天不到,资产减过半。

做事要勇敢,不能心急,但不要怯于尝试

Comments

react-dnd 简析 2

上篇文章有说到场景:卡片在不同的容器之间来回拖动。下面,对上篇文章中的代码整理一下:

上期代码

1
2
3
4
// ItemTypes
const ItemTypes = {
  CARD: 'Card'
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Card
const Card = React.Component {

  constructor(props) {
    super(props);
  }

  render() {
    const { children, id, text, connectDragSource } = this.props;
    return connectDragSource(
      <div  className={styles.card} >
        {text}
      </div>
    );
  }
}

const cardSource = {
  beginDrag(props) {
    return {
      id: props.id,
      index: props.index,
      text: props.text,
      boxId: props.boxid
    };
  },
  isDragging(props, monitor) {
    return props.id === monitor.getItem().id;
  }
};

function dragCollect(connect, monitor) {
  return {
    connectDragSource: connect.dragSource(),
    isDragging: monitor.isDragging()
  };
}

let DragCard = DragSource( ItemTypes.CARD, cardSource, dragCollect)( Card)

export default DragCard
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// CardBox
class CardBox extends React.Component {

  constructor(props) {
    super(props);
  }

  render() {
    const { childrenconnectDropTarget } = this.props;
    return connectDropTarget(
      <div className={styles.cardBox}>
        { children }
      </div>
    );
  }
}

const cardBoxTarget = {
  hover(props, monitor, component) {
    const hoverCard = monitor.getItem();
    if (hoverCard.id && props.boxid != card.boxId){
      props.changeCard(hoverCard.index, props.boxid);
    }
  }
};

function dropCollect(connect, monitor) {
  return {
    connectDropTarget: connect.dropTarget()
  };
}

let DropCardBox = DropTarget(ItemTypes.CARD, cardBoxTarget, dropCollect)(CardBox)

export default DropCardBox
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// Kanban

class Kanban extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      cards: [
        {id: 1, text: 'card 1', boxId: 1 },
        {id: 2, text: 'card 2', boxId: 1 },
        {id: 3, text: 'card 3', boxId: 2 },
        {id: 4, text: 'card 4', boxId: 2 },
      ],
      boxes: [
        {id: 1, boxId: 1},
        {id: 2, boxId: 2}
      ]
    }
  }

  changeCard(index, newBoxId) {
    const dragCard = this.state.cards[index]
    if (!dragCard){ return }

    dragCard.boxId = newBoxId

    this.setState(update(this.state, {
      cards: {
        $splice: [
          [index, 1],
          [index, 0, dragCard]
        ]
      }
    }));
  }

  render() {
    return (
      <div >
        { this.state.boxes.map( box => {
          return (
            <CardBox
              key={'cb-'+box.boxId}
              changeCard={this.changeCard}
              boxid={box.boxId}>
              { this.state.cards.map( (card, index) => {
                if (box.boxId==card.boxId)
                  return (
                    <Card
                      key={'c-'+card.id}
                      id={card.id}
                      index={index}
                      boxid={card.boxId}
                      text={card.text}
                      />
                  )
                else return '';
               })}
            </CardBox>
          )
        })}
      </div>
    );
  }
}

export default DragDropContext(HTML5Backend)(Kanban)

这四个文件,ItemTypes.js/Card.js/CardBox.js/Kanban.js 为基本的模型文件,样式文件应该也要有的,这里就贴了。

新需求

对于看板来说,不同容器间的来回拖动是必要的。同时,如果想在单个容器中上下拖动,又需要怎么实现?

由于上篇文章的changeCard(index, newBoxId)只能调整卡片的boxId的属性,并不能改变其同一容器中的上下位置,所以需要调整一下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 增加一个参数 newIndex
changeCard(index, newBoxId, newIndex) {
  const dragCard = this.state.cards[index]
  if (!dragCard){ return }

  if (newBoxId && dragCard.boxId != newBoxId) {
    dragCard.boxId = newBoxId

    this.setState(update(this.state, {
      cards: {
        $splice: [
          [index, 1],
          [index, 0, dragCard]
        ]
      }
    }));
  } else if (newIndex && index != newIndex) {
    // newIndex 当卡片拖动到某个卡片的上面时,
    // newIndex 就是此某个卡片的 index
    // 将拖动的卡片 剪切到 此位置上
    //
    // dragCard.index ~= newIndex

    this.setState(update(this.state, {
      cards: {
        $splice: [
          [index, 1],
          [newIndex, 0, dragCard]
        ]
      }
    }))
  }
}

通过调整 index 的值来达到位置上的变化。


当卡片拖动到另一卡片上面时,需要获取到这一卡片的 index 的值,再将拖动卡片设置到新的位置上。

那么,需要将Card设置为可接受的容器DropTarget

参考之前的CardBox,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const cardTarget = {
  hover(props, monitor, component) {
    const dragIndex = monitor.getItem().index;
    const hoverIndex = props.index;

    // Don't replace items with themselves
    if (dragIndex === hoverIndex) {
      return;
    }

    // Determine rectangle on screen
    const hoverBoundingRect = findDOMNode(component).getBoundingClientRect();

    // Get vertical middle
    const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;

    // Determine mouse position
    const clientOffset = monitor.getClientOffset();

    // Get pixels to the top
    const hoverClientY = clientOffset.y - hoverBoundingRect.top;

    // Only perform the move when the mouse has crossed half of the items height
    // When dragging downwards, only move when the cursor is below 50%
    // When dragging upwards, only move when the cursor is above 50%

    // Dragging downwards
    if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
      return;
    }

    // Dragging upwards
    if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
      return;
    }

    // Time to actually perform the action
    props.changeCard(dragIndex, null, hoverIndex);

    // Note: we're mutating the monitor item here!
    // Generally it's better to avoid mutations,
    // but it's good here for the sake of performance
    // to avoid expensive index searches.
    monitor.getItem().index = hoverIndex;
  }
}

以上参考官方的例子,通过 component来获取纵向属性,来准确得到 newIndex 参数。 当拖动的高度没有达到卡片一半时,不会触发changeCard(..)方法。

1
2
3
4
5
6
7
function dropCollect(connect, monitor) {
  return {
    connectDropTarget: connect.dropTarget()
  };
}

let DropCard = DropTarget(ItemTypes.CARD, cardTarget, dropCollect)(Card);

dropCollect 基本是一样的。

那么,完成这点之后,Card是可拖动的,又是可以容器, 所以,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Card
const Card = React.Component {

  constructor(props) {
    super(props);
  }

  render() {
    const { children, id, text, isDragging, connectDragSource, connectDropTarget } = this.props;
    const opacity = isDragging ? 0.5 : 1;

    return connectDragSource( connectDropTarget(
      <div className={styles.card}
        style={ {opactiy: opacity} }
        >
        {text}
      </div>
    ));
  }
}

const cardSource = {
  //...
};

const cardTarget = {
  // ...
}

function dragCollect(connect, monitor) {
  //...
}

function dropCollect(connect, monitor) {
  // ...
}

let DropCard = DropTarget(ItemTypes.CARD, cardTarget, dropCollect)(Card);
let DragCard = DragSource(ItemTypes.CARD, cardSource, dragCollect)(DropCard)

export default DragCard

通过最后处绑定,Cardprops会多出来个值来connectDragSource/connectDropTarget

Card类中,根据 isDragging来判断设置卡片的透明度,这就是为什么之前要重新定义CardSource中的isDragging方法的缘由。

1
2
3
4
5
6
7
8
const cardSource = {
  beginDrag(props) {
    // ...
  },
  isDragging(props, monitor) {
    return props.id === monitor.getItem().id;
  }
};

因为,当一个拖动卡片的index1时,dnd默认会其标识为isDragging=true。而,拖动卡片到index=0时,由于changeCard,拖动卡片此时会从1 -> 0,由于isDdragging不会重新判断,所以,显示效果会是:

哪个卡片占原拖动卡片的位置,就会是有透明度

但这并不是我们所要的,所以,是否能拖动,使用唯一标识id来判断。

还要注意,Kanban中需要传入方法给Card

1
2
3
4
5
6
7
8
9
10
11
//...
this.state.cards.map( (card, index) => {
  return (
    //...
    <Card
      //...
      changeCard={this.changeCard}
     />)
    //...
})
//...

最后

真正使用时,还是需要定义DropTarget中的drop方法的,而文中只谈到了hover方法,这部分让大家自己去探索吧。

打完收功。

后记

其实,一个插件能不能用,除了了解其用法之外,还可能需要了解其性能。

就本人的5年前的小笔记本来说,200个普通文本卡片还是可以自由拖动的,效果还是可以的。

当到300时,部分快速拖动会失效,可能是排序和渲染上的耗时吧, 好点的机器可能会好点。

另外,从调试过程看出,dnd只会对有数据变化的卡片重新调用render()渲染方法,并不是所有都会重新渲染一遍的。

Comments

React-dnd 简析

对于开源插件或者代码来说,我更喜欢推荐其 github 地址,而不是官网地址:

https://github.com/gaearon/react-dnd

安装

1
2
npm install react-dnd -D
npm install react-dnd-html5-backend -D

react-dnd-html5-backend 是一个必要的可选组件,因为 html5 的拖拽API 支持拖动的阴影,总的来说,这插件是必要的。

详细的各名称就不在此说明了,直接介绍关键点吧。

基础

拖拽功能,首先需要知道两个基本的部分:拖起、放下。

拖起,就是鼠标放下并移动的过程;放下,就是鼠标放下拖动元素。 在这一整个过程中,需要声明哪个元素是可以拖动的,哪个元素是可以接收拖动元素的。

react-dnd 中,使用 DragSource 来声明拖动的元素,用DropTarget来声明接收拖动元素的容器。

下面将会通过 看板 的常用功能进行相关的介绍。

看板 基本会涉及到 卡片的上下拖动 和 卡片在不同区间的拖动。

实体 Card, CardBox, Kanban

为了实现基本的功能,需要拖动的实体 Card 有两个基本属性:

1
2
3
id
text // 用于显示卡片
boxId // 用于表示容器间的拖动

Card 实体的简单显示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Card = React.Component {

  constructor(props) {
    super(props);
  }

  render() {
    const { children, id, text } = this.props;
    return (
      <div  className={styles.card} >
        {text}
      </div>
    );
  }
}

CardBox 容器用来接收 Card, 其属性为

1
boxId

显示代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CardBox extends React.Component {

  constructor(props) {
    super(props);
  }

  render() {
    const { children } = this.props;
    return (
      <div className={styles.cardBox}>
        { children }
      </div>
    );
  }
}

children 表示显示的容器中的多个卡片。

最后,看板的代码就是循环了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Kanban extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      cards: [
        {id: 1, text: 'card 1', boxId: 1 },
        {id: 2, text: 'card 2', boxId: 1 },
        {id: 3, text: 'card 3', boxId: 2 },
        {id: 4, text: 'card 4', boxId: 2 },
      ],
      boxes: [
        {id: 1, boxId: 1},
        {id: 2, boxId: 2}
      ]
    }
  }

  render() {
    return (
      <div >
        { this.state.boxes.map( box => {
          return (
            <CardBox
              key={'cb-'+box.boxId}
              boxid={box.boxId}>
              { this.state.cards.map( (card, index) => {
                if (box.boxId==card.boxId)
                  return (
                    <Card
                      key={'c-'+card.id}
                      id={card.id}
                      index={index}
                      boxid={card.boxId}
                      text={card.text}
                      />
                  )
                else return '';
               })}
            </CardBox>
          )
        })}
      </div>
    );
  }
}

先用 boxes 进行列的划分,再通过其的boxId的判断,来划分 cards对应于哪个列中。

为什么这样子循环来显示卡片,而不是先进行分组,再对组进行渲染。 此处主要考虑到,只想用一个内存来存储所有的卡片。如果分组,即相当于将所有 的卡片分开为多个内存存储,本文不想让其间卡片的内存跳转频繁。

但这实现方法有个缺陷,页面渲染会有空元素,else return '' 会造成一个空的span DOM元素。

所以,注意,此处是用到了卡片的index的。

这是基本实体,它们包含关系为 Kanban > CardBox > Card

DragSource

api

DragSource(type, spec, collect)(MyComponent)

DragSource, 顾名思义,就是可拖动的元素。在本文场景中,拖动元素就是Card了。

API 中,有必要的三个参数,分别是type, speccollect

type

type 就是可拖动元素的名称,这会与 DropTargettype 有关联。实际中,名称就是一个字符串, 本文场景中,可以有:

1
2
3
const ItemTypes = {
  CARD: 'Card'
}
spec

spec 拖动元素的对象,注意,它必须是对象。它定义有四个方法,分别是beginDrag(props, monitor, component)endDrag(props, monitor, component)canDrag(props, monitor)isDragging(props, monitor),详细说明可链接到 Drag Source Specification

现在,主要对 beginDrag(props, monitor, component)isDragging(props, monitor) 进行说明。

beginDrag(props, monitor, component) 是必须声明的,它要求返回一个普通的对象,例如{ id: props.id }。 其实,它就是将你用的实体,选择拖动使用的属性进行返回。

isDragging(props, monitor) 是可选的,是用来判断一个元素是否是在拖动的过滤中。默认下,一个对象是否是拖动中, dnd 只会在拖动元素的开始拖动时设置其值。为什么要在这里突出说明一下,因为在操作场景中,初始化的设置在拖动的过程中, 很可能会不正确的,所以需要重新定义其方法实现。

在本文场景中,可以

1
2
3
4
5
6
7
8
9
10
11
12
13
const cardSource = {
  beginDrag(props) {
    return {
      id: props.id,
      index: props.index,
      text: props.text,
      boxId: props.boxid
    };
  },
  isDragging(props, monitor) {
    return props.id === monitor.getItem().id;
  }
};

monitorcomponent 还是需要大家了解一下的。 monitor 就是整合拖动实体和拖动状态的新对象,component粗略可以理解为 DOM 元素。 详情看 Drag Source Specification

collect

collect 为一个函数,而函数返回的是一个对象。本质起到桥接作用,将拖动元素的属性和拖动状态整合。

本文场景中,collect 可以为,

1
2
3
4
5
6
function dragCollect(connect, monitor) {
  return {
    connectDragSource: connect.dragSource(),
    isDragging: monitor.isDragging()
  };
}

综合以上,需要将 Card 声明为可拖动的元素,就是

1
let DragCard = DragSource( ItemTypes.CARD, cardSource, dragCollect)( Card);

同时,需要调整Card类的代码,将绑定引入的属性声明关联到元素中,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Card = React.Component {

  constructor(props) {
    super(props);
  }

  render() {
    const { children, id, text, connectDragSource } = this.props;
    return connectDragSource(
      <div  className={styles.card} >
        {text}
      </div>
    );
  }
}

DropTarget

api

DropTarget(types, spec, collect)(MyComponent)

就是接收拖动元素的容器元素。

types

注意,第一个参数是 types,也就是说,它可以接收多种类型,但也可以是字符串。本文场景中,就是ItemTypes.CARD

spec

同样是普通对象,但不同于拖动元素,容器元素中的说明参数中的方法有drop(props, monitor, component)hover(props, monitor, component)canDrop(props, monitor)

hover(props, monitor, component) 这里重点说一下此方法,因为这个比较常用,当然drop也很常用,但主要与后台交互时才会用到。比如,不同容器中的拖动,需要将放下时的boxId保存到后台,而hover是实时变化的,不适合与后台交互的action进行数据操作。

在拖动元素从一个容器元素,到另一容器元素时,其的boxid会发生变化,这才合理。

所以,需要在 CardBox 的中定义 changeCard(index, newBoxId) 的方法。 其中, index是所有cards列表中的index

本文场景中,

1
2
3
4
5
6
7
8
const cardBoxTarget = {
  hover(props, monitor, component) {
    const hoverCard = monitor.getItem();
    if (hoverCard.id && props.boxid != card.boxId){
      props.changeCard(hoverCard.index, props.boxid);
    }
  }
};
collect

类似于DropSource中的collect, 由于是容器,则没有拖动的属性了。

本文场景中,

1
2
3
4
5
function dropCollect(connect, monitor) {
  return {
    connectDropTarget: connect.dropTarget()
  };
}

综合上面,将BoxCard声明为拖动容器,就是

1
let DropCardBox = DropTarget(ItemTypes.CARD, cardBoxTarget, dropCollect)(CardBox);

同时,绑定后,需要在模型中关联渲染的元素,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CardBox extends React.Component {

  constructor(props) {
    super(props);
  }

  render() {
    const { children, connectDropTarget } = this.props;
    return connectDropTarget(
      <div className={styles.cardBox}>
        { children }
      </div>
    );
  }
}

另外

基本上,上面大致介绍了一个卡片在不同的容器间相互拖动的故事。

另外,需要指出的是,react-dnd 所有的元素需要使用 DragDropContext 来包裹起来,

1
DragDropContext(HTML5Backend)(Kanban)

上面其中有提到,卡片中容器间相互拖动,需要改变其 boxId 的值, 那么,需要在CardBox中传入changeCard(index, newBoxId) 方法属性。 所以,Kanban需要调整为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class Kanban extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      cards: [
        {id: 1, text: 'card 1', boxId: 1 },
        {id: 2, text: 'card 2', boxId: 1 },
        {id: 3, text: 'card 3', boxId: 2 },
        {id: 4, text: 'card 4', boxId: 2 },
      ],
      boxes: [
        {id: 1, boxId: 1},
        {id: 2, boxId: 2}
      ]
    }
  }

  changeCard(index, newBoxId) {
    const dragCard = this.state.cards[index]
    if (!dragCard){ return }

    dragCard.boxId = newBoxId

    this.setState(update(this.state, {
      cards: {
        $splice: [
          [index, 1],
          [index, 0, dragCard]
        ]
      }
    }));
  }

  render() {
    return (
      <div >
        { this.state.boxes.map( box => {
          return (
            <CardBox
              key={'cb-'+box.boxId}
              changeCard={this.changeCard}
              boxid={box.boxId}>
              { this.state.cards.map( (card, index) => {
                if (box.boxId==card.boxId)
                  return (
                    <Card
                      key={'c-'+card.id}
                      id={card.id}
                      index={index}
                      boxid={card.boxId}
                      text={card.text}
                      />
                  )
                else return '';
               })}
            </CardBox>
          )
        })}
      </div>
    );
  }
}

Comments

U should add gem rspec-rails in ur project’s Gemfile, then bundle.

Then, u need run command rails g rspec:install.

When u use rails commands, likes rails g model User xx:xx. It will help u to create spec/models/user_spec.rb file.

Yes, Rspec will check _spec.rb file as test file.

Give me some codes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
require 'rails_helper'

RSpec.describe User, :type => :model do
  context "signup with" do
    it "email and password, then success." do
      expect(User.signup("123@exmaple.com", "123456").errors.size).to eq(0)
    end
    it "illege email and password, then fail." do
      expect(User.signup("123sdf", "123456").errors.size).to eq(1)
    end

    it "email and no password, then fail. " do
      expect( User.signup("123@example.com", "").errors.size ).to eq(1)
    end
  end
end

and then, u can use rspec to test all _spec files, or u can only test user_spec.rb file with rspec spec/models/user_spec.rb command.

if no error, u can get

1
3 examples, 0 failures

It is not a good view. We just not work for testing, but say something to others.

U can run rspec --format doc, will get result:

1
2
3
4
5
6
7
User
  signup with
    email and password, then success.
    illege email and password, then fail.
    email and no password, then fail.

3 examples, 0 failures

Comments

ES6 In Depth: The Future

上周的ES6模块结束了为期四个多月的ES6新特性的这一系列的文章。

这文章主要会包括我们之前未讨论过的众多新特性,它们就如这JS语言大厦中的衣柜和奇怪的房子都是十分有趣的,也许还会一个地下室,或者有两个。如果你没有阅读过这一系统的其它文章,可以看这里 。这文章并不是好的开头。

额外提醒:之前提到的很多特性还没有得到广泛的支持。好了,让我们开始吧。

ES6部分特性的标准化来源于之前的其它标准过程,或者部分得到广泛的实现支持而没有标准化。

  • 类型数组(Typed arrays)/ArrayBuffer/DataView 这些是WebGL标准化的一部分,但现在也会运用于其它许多地方的API,包括Canvas、网络音频API和WebRTC。无论你处理多大的二进制文件或者数值数据,都是很方便的。

例如,如果你Canvas需要渲染内容时没有你想要的,或者你希望手动操作更为高效时,你可以自己实现它:

1
2
3
4
5
6
7
var context = canvas.getContext("2d");
var image = context.getImageData(0, 0, canvas.width, canvas.height);
var pixels = image.data; // a Uint8ClampedArray object ect
// ... Your code here!
// ... Hack n the raw bits in `pixels`
// ... and then write them back to th canvas;
context.putImageData(image, 0, 0);

在标准化过程中,类型数组(Typed arrays)包括有常见了方法,如.slice().map().filter()

  • Promises 仅用一个自然段来描写promise,就像是吃饭只能吃一个薯片。不用管它有多难,这甚至是什么任何意义的事情。说什么?Promise可以用来对JS进行异步编程来构建代码块。它的作用会在后面提到。比如,当你调用fetch()时,不像普通代码块,它会立即返回一个promise对象,其数据获取会在后台进行。当有返回值时,它会调用你的相应代码。promise优于普通的callback函数,因为它的操作链真心很好。promise有很多有趣的操作,通常会是第一选择,你可以简单进行错误处理而不需要做过多的学习。它们已经在浏览器支持了。如果你还不一点都不知道promise,可以查看 Jake Archibald 的文章

  • 函数在代码块的作用域中 你不应该让这情况出现,但是,你可能已经无意地存在相关代码了。

在ES1-5中,下面的代码在技术上说是有问题的:

1
2
3
4
5
6
if (temperature > 100 ) {
  function chill() {
    return fan.switchOn().then(obtainLemonade);
  }
  chill();
}

这个函数声明是在if代码块中的,这理念上是不允许的。函数声明只能在顶级中,或者在函数体内的最外层。

但是,上面代码几乎能在所有主流的浏览器中运行,从某种程度上是这样子的。

但又不完全一样,在每个浏览器中其处理细节有些不一样。但是,某种程度上都是可以工作的,并且很多网页还在使用它。

不幸的是,Firefox和Safari还没有实现新的标准。对现在来说,可以使用一个函数的表达式来替代:

1
2
3
4
5
6
if (temperature > 100) {
  var chill = function() {
    return fan.switchOn().then(obtainLemonade);
  };
  chill();
}

多年来,代码块作用域函数(block-scoped function)没有标准化是因为向后兼容的限制并非常复杂。没有人认为他们可以解决这一问题。ES6处理这种奇怪规则的线程只能在非严格模式中使用。我不能在这里解释,但请相信我,使用严格模式。

  • 函数名称 所有的主流JS引擎都会在函数中有个非标准的.name属性来表示其名称。ES6对此进行了标准化,它会明智地对某些没有名称的函数推测出.name的属性:
1
2
3
> var lessThan = function(a, b) { return a<b;}
> lessThan.name
    "lessThan"

好事

  • Object.assign(target, …sources) 标准库中的新函数,类似于Underscore中的.extend()
  • 函数调用中的展开操作符(spread operator,实际是省略号,翻译为展开操作符因为作用就是将数据展开为多个值) 在这里Nutella并没有做什么,尽管Nutella尝着美味。但是,它依然是个美味的特性,我认为你们会喜欢的。

在之前的五月中,我们介绍了rest parameters,这为函数提供了一个可能接收任意数量的参数方式,它比那随意、笨拙的arguments对象更为友好。

1
2
3
4
function log(...stuff) { // stuff is the rest parameter.
  var rendered = stuff.map(renderStuff); // It's a real array.
  $("#log").add($(rendered));
}

那时我们并没有对函数中传递任意数量的参数的匹配语法进行说明,它相比fn.apply()更为友好。

1
2
// log all the vlaues from an array
log(...myArray);

好的,它可以作用于任意的迭代对象,所以你可以通过log(...mySet)来记录所有的事情。

并不像剩余参数,它可以在一个参数列表中使用多个展开操作符:

1
2
// kicks are before trids
log("Kicks:", ...kicks, "Trids:", ...trids);

展开操作符可以二维数组压平为一维数组:

1
2
3
4
> var smallArrays = [ [], ["one"], ["two", "twos"]];
> var onBigArray = [].concat(...smallArrays);
> oneBigArray
    ["one", "two", "twos"]

但也许只有我有这一个迫切的需求,如果是这样,我真要责骂Haskell了。

  • 构建数组时的展开操作符 同样回到五月,我们谈到解构中的”rest“剩余模式。这是一个可以将一数组中的任意数量值取出来的方式。
1
2
3
4
5
> var [head, ...tail] = [1, 2, 3, 4];
> head
    1
> tail
    [2, 3, 4]

猜测一下下面代码。这匹配的语法会将任意数量的元素整合为一个数组:

1
2
3
> var reunited = [head, ...tail];
> reunited
    [1, 2, 3, 4]

同样的,你可以在函数调用中拥有相同的规则:你可以在同一数组中使用多次展开操作符,等等。

  • 适时的尾部调用 如果让我在这里解释这个,对我来说还是难的。

为了更好地理解这一特性,没有比这计算机编程的结构和解析更合适开始学习的了。如果你感兴趣,你可以坚持读它。尾部调用会在 1.2.1部分 线性递归与迭代中有解释。ES6标准化中要求的实现是线性递归的方式,也就是那文章提到的。

没有任一主流的浏览器实现这一特性,这很难实现,但是所有的事情都在往好的方向发展。

文本(Text)

  • Unicode 版本升级 ES5要求实现至少支持Unicode的3.0版本字符。ES6则要求至少是Unicode 5.1.0。你可以在函数名称上使用Linear B

[Linear A]使用上还有点问题,因为它在Unicode7.0版本之前没有支持,也因为很难管理没有破译的语言进而的代码编写。

(尽管JS引擎支持在Unicode6.1版本中的emoji,但你还是不能使用这些表情作为变量的名称。出于某种原因,Unicode联盟不支持使用它们作为身份标识字符。)

  • 长的Unicode转义序列 如早期版本一样,ES6支持4个数值的Unicode转义序列。它们看着如\u212A。它们很好用,你可以在字符串中使用它们。或者,如果你想耍幽默同时你无论什么时候也不同进行代码检查,你可以使用它们作为变量的名称。但是,对于一个字符如U+1a3021,一个用头倒立的人的埃及象形文字,明显是有问题的。13021有五个数值,已经超过了四个。

在ES5中,你不得不编写两个转义符号,因为UTF-16的代理对(surrogate pair)。这实际上就感觉生活在暗黑时代:寒冷、悲惨、野蛮的。ES6就像是意大利的文化复兴,带来了巨大的变化:你现在可以编写\u{13021}

  • 对BMP字符有更好支持 .toUpperCase().toLowerCase()方法现在可以用于犹太字符中了。

同样的,String.fromCodePoint(...codePoints)函数与老式的String.fromCharCode(...codeUnits)差不多,只是前者会支持BMP的点编码。

  • Unicode 正则 ES6的正则表达式支持新的标签,u标签,因为除在BMP中正则表达式认为会将其认为是个单独的字符,而不是两个分离的编码单元。例如,没有u时,/./只会匹配半个字符“ ”,但/./u会匹配整个。

在正则在增加u的标签,还可以处理unicode的非大小写和长类型的unicode转义序列。至于完整的说明,可参考Mathias Bynens 非常详尽的文章

  • Sticky正则 对于非unicode的可以使用y标签,详情在此可查看。sticky正则表达式只会从由.lastIndex指定的偏移位开始查找,如果不要指定的位置找到,不会一直向下找,而是直接返回null

  • 官方的国际化指引 ES6实现了对国际化的支持,ECMA-402 the ECMAScript 2015 国际化说明,这额外的标准中指定了Intl对象。Firefox,chorme 和 IE 11+ 已经支持它了,Node 0.12 也支持。

数值(Numbers)

  • 二进制和八进制 如果你觉得数字 8,675,309 和 0x845fed并不适合你,而想要一种更为特殊的方式表达,你现在可以编写0o41057755(八进制)或者 0b100001000101111111101101(二进制)。

Number(str)现在也可以识别这格式Number("0b101010")字符串,会返回42。

(快速提醒:number.toString(base)parseInt(stsring, base)还是可以如原来一样转化数字,无论是转出还是转入,转换的过程中会根据base。)

  • Number函数和常量 这是非常好的地方。如果你对此感兴趣,你可以自己查看标准文档,可以从Number.EPSILON开始。

也许,这最有意思的新想法是“安全的整型”,它可以从-(2**25-1)+(2**53-1),包含边界值 。JS中的数值存在着范围。这一个在此范围中的整型都可以用JS数字表示,进而可以查看其临近的数字。简单来说,在这范围中操作++--得到的效果是期望所得的。超出这范围时,奇数是不能表示的,就如64位浮点的数字,同样这些数字的增加和减少也不能得到正确的结果(偶数也一样)。为了解决你代码中的这些问题,标准委员会提供了常量Number.MIN_SAFE_INTEGERNumber.MAX_SAFE_INTEGER,和一个判断方法Number.isSafeInteger(n)

Math.sign(x) 可获取数字的符号(1,0,-1对应于正零负)。

ES6还添加了Math.imul(x, y),它就是32位的带符号乘法。这是十分奇怪的需求,除非你实际工作中没有64位的整型或者大数值的整型。如果是这样,这还是挺方便的, 这有助于编译器,Emscripten 可以利用此函数在JS中计算64位的乘法。

类似的,Math.fround(x)可助于编译器对32位浮点数值的支持。

结束语

这就是所有特性了?

不。我还没有提到在所有内置迭代器中的对象通用原型,还有生成器函数的构造函数,Object.is(v1, v2)Symbol.species可以帮助进行子类内置操作,如数组和Promise。ES6 还有许多没有标准化的目标正在进行中。

我可以确认的是我肯定遗失些事情没有提到。

但是,如果你一直都跟着下来,你会对整体有个很好的印象。你可以知道今天你可以能用上的特性,如果你还这么做了,那么你正迈向更好的语言。

几天前,Josh Mock 提醒我说,他只只用的50行代码就说完了八个不同的ES6特性,但没有深入的想法。文章包含有模块、类、默认参数、Set、Map、模板字符串、前头函数、和let。(他忘记了for-of循环)

这也就是我的经验所得到的。文章对于新的特性在一块处理得很好,它们会真正影响到你日常编写的每一行代码。

同时,每个JS引擎都在积极跟进和优化这些我们之前几个月一直在讨论的特性。

只要我们的工作完成了,这语言也就完善好了。我们将不会不得不再次调整代码了,而我将不得不找其它工作了。

只是开个玩笑。ES7的提案已经提交了。这里可以暴露一点:

  • 幂操作符 2**8将会返回 256,这会是Firefox中的日版本中更新。

  • Array.prototype.includes(value) 如果这数组包含有指定的值时,会返回true。此会通过polyfill的方式在Firefox的日版本中。

  • SIMD 将128位的SIMD指令集提供给先进的CPU。这些指令集会对相邻的数组元素进行算术计算,可以动态地提升大范围的各种算术,对于流媒体音频、视频、密码、游戏、图片处理等都是相当有用的。更加底层的操作,更为强大。此会通过polyfill的方式在Firefox的日版本中。

  • 异步函数 我们在之前的生成器的文章中隐藏这一特性。异步函数类似于生成器,但区别时同步编程。当你调用一个生成器时,它会返回一个迭代器。当你调用一个异步函数时,它会返回一个promise。生成器可以使用yield的关键字来暂停和产出一个值;同步函数则会使用await关键字来暂停和等待一个promise。

这是很难用几句话来说完的。但异步函数会在ES7中有重大的调整和意义。

  • 类型化对象 这就是之前的类型数组。类型数组的元素拥有其类型,一个类型的对象是一个其属性被类型化后的对象。
1
2
3
4
5
6
7
8
9
10
// Create a new struct type. Every Point has two fields
// named x and y.
var Point = new TypedObject.StructType({
  x: TypedObject.int32,
  y: TypedObject.int32
});

// Now create an instance of that type.
var p = new Point({x: 800, y: 600});
console.log(p.x); // 800

你为了性能的原因会编写此代码。类似类型数组,类型对象会增加些编程的优势(减少内存的使用和提升速度),但是其每个对象、每个输入选项,相对于之前语言所有事情都会被静态地类型化,也就是被类型会之后,其值必须是那个类型。

这是作为JS的综合目标,并会在Firefox的日版本中实现。

  • 类和原型装饰器 装饰器就是一个属性、类、方法中的标签。通过一个例子来说明其是什么样子的:
1
2
3
4
5
6
7
8
import debug from "jsdebug";
class Person {
  @debug.logWhenCalled
  hasRoundHead(assert) {
    return this.head instanceof Spheroid;
  }
  ...
}

@debug.logWhenCalled就是所谓的装饰器,你可以想象这方法是做什么的。

这里的提案有解释其详细工作的过程,同时还包含许多的例子。

还有一个令人兴奋的特性我不得不提到,但它还在开发阶段并且不是语言的特性。

TC39, 这个ECMAScript的标准委员会,将会更为频繁地发布版本和有更多的公开进度。在ES5和ES6之间已经有六年。委员会对ES7的目标是在ES6的12个月之后,随后的标准版本会以12个月的节奏进行发布。上面提到的部分特性已经准时完成了,它们会跟上这火车并成为ES7的一部分的。那些还没有完成的,会在下列火车的时间表中。

很高兴分享了很多的ES6的好的新特性,同时也很高兴能够花时间来讨论这些特性,可能以后再也没有这时间了。

很高兴参与深入ES6的文章,我希望你们能喜欢它。保持联系。; )

Comments

ES6 In Depth: Modules

当我在2007年加入到Mozilla的JS团队时,有一笑话是经典的JS程序只有一行代码。

那时已经是Google Map开始的两年之后了。在此之前,最为卓越的JS代码就是表单的验证,那么你在<input onchange=>上的平均处理代码也就是一行,对的,已经足够了。

事情已经发生了变化。JS项目已经成长为令人惊奇的量级,同时社区中也开发了一些工具来适应规模的变化。其中一个最为基本的事情是你需要模块系统,它可以将你的工作分离到多个文件和目录中,但是它依然保证在必要时能访问其他部分,同时使得加载所有代码更为有效。所以实际上,JS已经有了模块系统,并且有几种。同时,还有些包管理工具来安装软件和复制其高层的依赖。你可能会想到ES6,它为JS带来了新的模块系统,确实有点晚了些。

好的,今天我们就开始来看ES6为这已经存在的系统增加了些什么功能,同时看一下可以使用它来做些什么标准和工具吧。但是首先,让我们开始查看ES6模块长什么样子吧。

模块基础

ES6的模块是一个包含有JS代码的模块。它没有module的关键字,一个模块几乎可以看成为一个脚本。这里有两个区别:

  • ES6模块会自动启用 严格模式,即使你没有在代码中使用”use strict”
  • 你可以在模块中使用importexport

让我们先来说一下export。在模块中的任何声明,默认都只会作用于此模块。如果你想在模块中的声明公开出去,也就是其它模块能使用它,你必须暴露这个特性(变量、函数等)。有些方式可以做到这点,最为简单的方式就是添加一个export的关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// kittydar.js - Find the locations of all the cats in an image.
//(Heather Arthur wrote this library for real)
// (but she didn't use modules, because it was 2013)

export function delectCats(cavas, options) {
  var kittydar = new Kittydar(options);
  return kittydar.detectCats(canvas);
}

export clas Kittydar {
  ... several methods doing image processing ...
}

// This helper function isn't exported.
function resizeCanvas() {
  ...
}

你可以export任何顶级的functionclassvarletconst

这也就是你需要知道有关模块的所有信息。你并不需要使用IIFE或者一个回调函数。你只要正常地声明你所需要的任何事情。因此,这代码就是模块,不是脚本,所有的声明的作用域都会在此模块中,并不会在全局所有的脚本和模块是可见的。将这些声明暴露出去,会使这成为此模块对外的公开API,这就是你想要的。

与暴露代码不同,在模块中的代码仅仅只是普通的代码。你可以使用全局字面量,如ObjectArray。如果你模块运行于浏览器中,你可以使用documentXMLHttpRequest

在一个分离的文件中,我们可能导入并使用detectCats()函数:

1
2
3
4
5
6
7
8
9
// domo.js - Kittydar demo program

import {detectCats} from "kittydar.js";

function go(){
  var canvas = document.getElementById("catpix");
  var cats = detectCats(canvas);
  drawRectangles(canvas, cats);
}

为了从一个模块中导入多个名称,你可以编写:

1
import {detectCats, Kittydar) from "kittydar.js";

当你运行一个含有import声明的模块时,需要导入的模块会先被加载,每个模块的代码体会根据整个依赖图进行深度遍历执行,并跳过已经执行的模块来避免循环加载。

这就是模块的基础内容了,这确实是十分简单的,;-)

导出列表

相比于在每个特性中进行导出的标记,你可以编写一个简单的列表来指出你想导出的所有名称,并放到大括号中:

1
2
3
4
5
export {detectCats, Kittydar};

// no export keyword required here
function detectCats(canvas, options) { ... }
class Kittydar { ... }

export的列表并不一定需要在文件的第一行,它可以出现在模块的顶级的任意地方。你可以拥有多个export列表,或者与其它的export混合起来用,只要保证导出的名称不超过一次就好。

对导入导出重命名

有时,一个导入的名称可能会与其它你同样需要使用的名称是一样的,所以ES6允许你对导入的名称进行重命名:

1
2
3
4
5
6
// suburbia.js

// Both these modules export something named `flip`.
// To import them both, we must rename at least one.
import {flip as flipOmelet} from "eggs.js";
import {flip as flipHouse} from "real-estate.js";

类似的,你可以对导出的名称进行重命名。有时可能会出现,你希望以不同的名称导出相同的值,如下:

1
2
3
4
5
6
7
8
9
10
11
// unlicensed_nuclear_accelerator.js - media streaming without drm
// (not a real library, but maybe it should be)

function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};

默认导出

设计的新标准是能与已经存在的CommonJS和AMD模块能正常溶合,所以当你一个Node项目,并且完成命令npm install lodash。你的ES6代码可以从lodash导入单独的函数:

1
2
3
import {each, map} from "lodash";

each([3,2,1], x=> console.log(x));

但是,你可能已经习惯看到_.each而不是each,并且你希望编写代码依然保持这方式。或者自从有 这个,你还想使用_作为一个函数。

为了做到这点,你可以稍微地调整为不同的语法:导入模块时不使用大括号:

1
import _ from "lodash";

这其实就是等价于import {default as _} from "lodash"。所有的CommonJS和AMD模块都会为ES6提供一个default的导出名称,这其实就类似于你require()这模块,也就是exports对象。

ES6模块被设计为可以让你导出多个事物,但在已经存在的CommonJS模块中,默认的导出是包含所有事物。例如,有名的colors包在我说之前并没有任何对ES6的支持。它就是一个CommonJS的集合,就像很多在npm的包一样。但是,你可以明确地告诉ES6代码需要导入的内容。

1
2
// ES6 equivalent of `var colors = require("colors/safte");`
import colors from "colors/safe"

如果你希望你的ES6模块有个默认的导出,那很容易。这并没有什么魔法性的操作,它就像其它的导出操作,只是将其命名为"default"。你可以使用我们之前提到的重命名的语法:

1
2
3
4
5
let myObject = {
  field1: value1,
  field2: value2
};
export {myObject as default};

或者使用缩写:

1
2
3
4
export default {
  field1: value1,
  field2: value2
}

关键字export default后面可以接任意的值:一个函数,一个类,一个你命名的对象字面量。

模块对象

十分抱歉,这部分内容有些长。但是,这并不仅仅是JS特有的。因为某些原因,所有语言中的模块系统都试图做到足够的小、方便。幸运,还剩一个特性。好吧,是两个。

1
import * as cows from "cows";

当你使用import *时,它导入的是一个模块命名对象。它的属性是这一模块的所有导出。所以,如果”cow”模块导出有一个名为moo()的函数,那么在此行代码导入”cows”之后,你可以使用cows.moo()

聚合模块

有时,主模块可能会将其它模块的包导入进来,同时会将它们又以非统一的方式导出。为了简化这一类的代码,下面有个聚合的从导入到导出的缩写:

1
2
3
4
5
6
7
8
9
10
// world-foods.js - good stuff from all over

// import "sri-lanka" and re-export some of its exports
export {Tea, Cinnamon} from "sri-lanka";

// import "equatorial-guinea" and re-export some of its exports
export {Coffee, Cocoa} from "equatorial-guinea";

// import "singapore" and export ALL of its exports
export * from "singapore";

每个这些export-from的表达式都类似于import-from,只是调整为export。并不像导入一样,这并不会重新添加导出的绑定到主模块的作用域中。所以,如果你想在world-foods.js中编写代码使用Tea,就不要使用这一缩写。你会发现找不到这字面量。

如果在导出”singapore”时会与其它的导出有冲突,会产生一个错误,所以请小心使用export *

好了,我们介绍完语法了。让我们开始些有趣的事情。

import实际上做了什么

你会相信 它没什么吗?

好吧,你不是那么好欺骗的。好的,你会相信标准委员会几乎没有对import进行说明?这是好事吗?

ES6的模块加载的整个详情可以移步到 其实现,它的实现详情可移步于 详细说明

粗略来讲,当你告诉JS引擎在运行一个模块时,它不得不进行如下四个步骤:

  • 1、解析:读取模块资源的实现代码并检查语法错误
  • 2、加载:递归地加载所有需要导入的模块,这一部分还没有标准化。
  • 3、链接:对于每个加载完成的模块,会为其创建一个作用域,并将此模块声明绑定到此作用域中,包括从其它模块导入过来的事物。

这就是import {cake} from "paleo"的部分。但是,如果”paleo”模块并没有导出任何名为”cake”的字面量,你将会得到一个错误。这是很坏的体验,因为你已经实际上运行了一些JS代码了。

  • 4、运行:最后,开始运行每个新加载的模块体。这时候,import的处理进程已经完成了,所以当代码执行到含有import声明的代码行时,没有什么会发生。

看?我之前就告诉你答案是“没什么”。我对编程语言并不会撒谎。

但是,现在我们开始接触这模块系统的有趣部分了,这是个感觉好玩的点。因为模块系统并没有指定怎么加载模块,你可以在开始时候,找出在资源代码中所有的import声明。ES6的其中一个实现方式,是将所有的工作都放到编译阶段,并将所有的模块捆绑放入一个文件中,才发送给网络上。webpack这工具实际上就是这么做的。

这是个大的话题,因为加载脚本会花费网络的时间。当你每次获取时,你会查找其import的声明,时间就会成倍增长了。比较天真的加载方式是会发送多个网络请求,但通过webpack,这不仅仅是今天使用ES6模块,你会自动得到所有软件工程所要到达的运行时优点。

ES6模块加载的详情还是与原始计划的一样,并构建起来的。其中一个原因就是因为没有统一怎么实现此特性,所以它并不是最后的标准。我希望某人能够指出来,因为就如我们看到的,模块的加载确实需要标准化,而且打包非常有用,是不能放弃的。

静态 vs 动态,或者:规则或打破规则

作为动态语言,JS已经拥有其令人惊奇的静态模块系统。

  • 所有的importexport都只允许在模块的顶级声明,导入导出没有额外的限制条件,但你不能在函数作用域中使用。

  • 所有的导出定义必须是在资源代码中存在有明确的名称。你不能通过编程来循环一个数组并导出一堆的名称。

  • 模块对象是冻结的。不能通过hack的方式来操作模块对象,polyfill方式的也不行。

  • 在任一模块代码运行之前,所有模块依赖必须加载完成、解析和关联上。按照要求,不允许import导入模块懒加载的。

  • 对于import的错误没有任何的恢复机制。一个App可能会有上百的模块,如果有任何的加载或者关联失败,所有代码将不会运行。你不能在try/catch中使用import。(这里有个优点,因为这模块系统是静态的,所以webpack可以在编译阶段检查出可能存在的错误。)

  • 在没有加载完依赖之前,不允许运行模块中的任何代码。这意味着如果依赖没有加载完成时,模块本身不知道怎么控制代码的运行。

如果你需求是静态的话,这模块系统是十分好的。但是,有时你可能需要些hack,是不是?

这就是为什么你需要编程API来处理与ES6的import/export语法相违背的模块系统加载机制。例如,webpack includes an API 你可以使用“code splitting”,按需求对某些模块进行懒加载。这个API可以让你打破上面所提到的多数规则。

ES6的模块语法是十分静态的(晕,什么叫十分静态),同时它也是好的,因为这样可以缩短其编译工具的时间。但是,通过编程后的加载API,这静态的语法已经可以动态操作了。

什么时候可以使用这ES6的模块?

现在为了使用模块,你需要一个编译器,如 Traceur 或者 Babel。早些时候,Gaston I. Silva 有一文章 来说明怎么为Web编译ES6代码。在这一文章中,Gaston 已经有一个例子是有关ES6模块的。这有个Axel Rauschmayer 编写的例子,它使用Bable和webpack。

模块系统主要由 Dave Herman 和 Sam Tobin-Hochstadt 设计,他们为此模块系统的静态化辩护,同时为此长年与包括我在内的很多人抗争着。Jon Coppeard 实现了 Firefox 中的模块。另外的JS 加载器标准化也正在进行中,人们所希望的在HTML中添加<script type=module>的特性也会随之而来。

这就是ES6。

这些实在是太有意思了,以致我并不想结束。也许,我们只是完成了部分的故事情节。我们可以讨论些ES6说明中零碎的特性,但它们又不能足够单独写成文章。也许,将来会对这些进行讨论。请在下周加入我们,一起对深入ES6进行个完美的总结吧。