记录一下使用 sqlalchemy-migrate 的问题。

官网: https://sqlalchemy-migrate.readthedocs.io/en/latest/download.html

说明

在寻找 python 层面,类 rails migration 的工具,发现了 sqlalchemy-migrate。 大致看着还可以,尝试使用中。

下载

1
pip install sqlalchemy-migrate

安装成功后,应该可以运行

1
migrate help

可以查看 migrate 脚本支持的功能命令。

使用

根据官网说明,手动尝试运行,但遇到了些问题。

创建迁移项目目录

1
migrate create migration "Example project"

此命令会创建一个迁移项目的文件夹 migration,里面包含有 versions 目录、manage.py、manage.cfg 文件。

构建数据库的基本结构

官网上,有

1
$ python migration/manage.py version_control mysql+mysqldb://root:@localhost/activity?charset=utf8mb4 migration

官网上说,这命令会在相应的数据库中增加版本控制的表 migrate_version。但在我的机器中,会出现错误

1
2
3
4
Traceback (most recent call last):
  File "migration/manage.py", line 2, in <module>
    from migrate.versioning.shell import main
ModuleNotFoundError: No module named 'migrate'

查阅文件 migration/manage.py 文件,

1
2
3
4
5
#!/usr/bin/env python
from migrate.versioning.shell import main

if __name__ == '__main__':
    main(debug='False')

报错没有正常找到 migrate 包,折腾了许久,没有找到有效的解决方案。 但本质上,migration/manage.py 文件也是使用 migrate 命令去实现迁移操作的, 所以,尝试手动使用 migrate 命令去实现数据迁移。

1
migrate version_control mysql+mysqldb://root:@localhost/activity?charset=utf8mb4 migration

运行上面的命令之后,查看数据库,会发现有数据表 migrate_version。

查看当前版本

1
2
migrate db_version mysql+pymysql://root:@localhost/activity?charset=utf8mb4 migration
# 0

创建迁移脚本

创建迁移脚本使用 script 命令实现

1
migrate script "add init tables" migration

命令会生成文件 migration/xxx_add_init_tables.py 文件,xxx 可以是 001 增序编号,也可以是日期时间。

查阅文件,有

1
2
3
4
5
6
7
8
9
10
11
12
13
from sqlalchemy import *
from migrate import *


def upgrade(migrate_engine):
    # Upgrade operations go here. Don't create your own engine; bind
    # migrate_engine to your metadata
    pass


def downgrade(migrate_engine):
    # Operations to reverse the above upgrade go here.
    pass

遇到问题:使用timestamp做为文件名前缀时,migrate包的部分版本处理有错误,建议使用普通数字。

增加一个 users 表, 有代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from sqlalchemy import *
from migrate import *

meta = MetaData()
account = Table(
    'account', meta,
    Column('id', Integer, primary_key=True),
    Column('login', String(40)),
    Column('passwd', String(40)),
)

def upgrade(migrate_engine):
    meta.bind = migrate_engine
    account.create()


def downgrade(migrate_engine):
    meta.bind = migrate_engine
    account.drop()

例子中,利用 sqlalchemy 构建表,并分别定义了 upgrade 、downgrade 方法。方法中,分别对 account 表进行了创建或删除。

代码中定义了表,需要将表落实到数据库中,运行命令

1
migrate upgrade mysql+pymysql://root:@localhost/activity?charset=utf8mb4 migration

命令会直接在数据库创建相应的表。

想删除表,可以执行

1
migrate downgrade mysql+pymysql://root:@localhost/activity?charset=utf8mb4 migration 0

0 表示要回滚到的版本。

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

既然用上了 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的文章,我希望你们能喜欢它。保持联系。; )