分类 技术文章 下的文章

现在前端工作已经离不开各种框架了。在各种MVVM框架的基础上,还有各种UI框架。想要什么功能,首先想到的是找找相应的插件,即使那个功能实际上很简单。

这些框架极大的方便了开发,也使更多的人可以从事前端开发工作,而不必有太多的基础知识。

方便的开发牺牲了其他的一些东西。双向数据绑定和虚拟DOM消耗了更多的内存,在移动设备上还体现在消耗更多的电量。快速搭建界面的UI框架生成了非常多的冗余代码,DOM树臃肿不堪。因为不必想方设法地优化DOM结构,不必过于关注JavaScript语言本身,工作过程中创造性的成就感已经非常少,取而代之的是搬砖似的机械重复的劳累感。

硬件的提升给了实现这些东西的基础,硬件提升的性能也往往被软件的升级消耗掉,所以硬件需要不断升级。

其实大多数的情况我们要的就是实现目的,上面这些文字不带有感情色彩。

async 函数适合执行一些流程,不要试图使用 async 函数返回一些东西。有一个错误的例子就是在Vue的模板中绑定一个计算属性,这个计算属性里面使用 async 函数获取数据。示例代码如下:

HTML模板部分:

<div>{{computedValue}}</div>

JavaScript部分:

//......
computed: {
    async computedValue(){
        let val = await aAsyncMethod();
        return val;
    }
}

如果你这么做了,你永远只会得到如下字符串:

'[object Promise]'

为什么?

原因很简单,就像 MDN 上这篇文档 MDN - async函数 介绍的,async 遇到await的时候会立即返回一个Promise,把控制权交出,直到 await 后面的异步操作有结果(resolve或者reject)才会继续执行后面的代码。可以说,async 函数的返回值永远是一个Promise

END

有时候我们需要从后端接口返回的二进制流数据中读取文件内容。在控制台中查看二进制流,会看到下面这样的东西:

1.jpg

这就需要前端读取二进制数据。

使用fileReader

一般都是通过将二进制流转换为blob,然后使用fileReader读取。

axios和请求一个图片为例。
首先,如果请求获取的是二进制数据,需要设置请求类型:

request({
    //...
    responseType: 'arraybuffer'
    //...
})

获取到数据后,将接口返回的数据转换成blob

request({...}).then(r => {
    let myblob = new Blob([r], { type: 'image/*' })
})

接着,使用fileReader读取blob

request({...}).then(r => {
    let myblob = new Blob([r], { type: 'image/*' });
    let reader = new FileReader();
    reader.readAsDataURL(myblob);
    reader.onload = function () {
        console.log(reader.result);
    };
})

以上代码将会打印出下面这种字符:

data:base64; image/*, foiHirf89hHoheruiefhi......

这样的Data URL可以直接用在imgsrc属性上。

fileReader还提供了readAsArrayBuffer()readAsText()等方法,用来读取不同类型的内容。

使用URL.createObjectURL

使用URL.createObjectURL(blob)可以创建一个指向blob内容的url,类似下面这样:

URL.createObjectURL(blob);
//生成的url如下:
'blob:http://localhost:9999/0a147e01-97a4-482a-8aa3-e44a2bf79478''

这个url也可以用作imgsrc
使用这个方法需要注意,要在合适的时机调用如下方法释放资源:

URL.revokeObjectURL('生成的url');

这种方法也适合显示切片的流媒体。

END

我是不是相亲相爱一家人朋友圈看多了,竟然起了个这样的标题……咳咳,言归正传

其实这种情况只存在嵌套循环中。

下面两个数组中,obj1某一项的id等于obj2某一项的xid,想要通过循环把obj2每一项的value赋给obj1中与之对应的项:

const obj1 = [
  { id: 1, ... },
  ...
];

const obj2 = [
  { xid: 1, value: 1, ...},
  ...
]

不知道为什么,下面这种写法,外层循环只能循环到i = 3就不往下执行了:

let i = 0, j = 0, lenI = obj1.length, lenJ = obj2.length;

for (; i < lenI; i++) {
  for (; j < lenJ; j++) {
    if (obj1[i]['id'] === obj2[j]['xid']) {
      obj1[i]['value'] = obj2[j]['value'];
      break;
    }
  }
}

下面这样写就没问题:

for (let i = 0, len_i = obj1.length; i < len_i; i++) {
  for (let j = 0, len_j = obj2.length; j < len_j; j++) {
    if (obj1[i]['id'] === obj2[j]['xid']) {
      obj1[i]['value'] = obj2[j]['value'];
      break;
    }
  }
}

为啥呢?原因很简单,当内层循环执行的时候,j的值已经改变了,前几次循环因为触发了breakj的值还小于lenJ,所以还能继续执行,直到j == lenJ,内层循环就不会再执行了。

这个错误好愚蠢……

END

众所周知,<a>href属性除了能指向一个URL,还能指向锚点。锚点链接就不说了,这里说说它的特殊用法。

在初期开发,尤其是切图的时候,需要给a一个href属性,否则a可能会没有应有的样式,例如没有下划线,看起来就跟普通文本无异。如果仅仅这样写:

<a href>link</a>

又相当于

<a href="当前页面的URL"></a>

点击之后会刷新当前页面。
好多开发者喜欢写上一个#来表示这是一个空链接:

<a href="#">link</a>

这样倒是没有上面的问题,但又有新问题:页面会跳转到顶部。很多大网站都没有注意到这一点。
其实解决这个问题的方法很简单,只需要在#后面再添加一个不存在的锚点,点击a的时候就没有任何反应了:

<a href="#idonotcare">link</a>

简洁点也可以这样写:

<a href="##">link</a>
<a href="#?">link</a>

全凭你的喜欢好了。

不过这样也有点美中不足:点击之后,虽然页面没有反应,但会在浏览器的地址栏出现你设置的锚点。其实这无关紧要。不过如果你非要觉得别扭,那下面的方法可以解决这个小小的不足:

<a href="javascript:void(0)">link</a>

void总会返回undefined,你在void之后写啥都没关系。这样写除了要多输入几个字符,就没什么问题了。

但是,在鼠标放到这个链接上时,浏览器下面还是会显示这个链接的地址:javascript:void(0)。比如在Chrome中,对于追求完美的你来说,显示地址的小白条会破坏页面的整体感,这时候,你就只好用设置a的默认样式+去掉href属性+绑定click的方法了……

如果你想这么写也不是不可以:

<a href="javascript:undefined">link</a>
<a href="javascript:false">link</a>
<a href="javascript:1">link</a>
<a href="javascript:1+1">link</a>
<a href="javascript:function f(){}">link</a>

但是这样用要小心!虽然在一些浏览器里毫无反应,但是在Firefox里,页面会被表达式的值替换
至于类似下面这样的,千万别用就对了:

<a href="javascript:document.write('0')">link</a>
<a href="javascript:document.title">link</a>

如果你好奇地试了,那你的页面就会被替换成0或者网页标题,耶稣都救不了你,我说的~

END

1、如何使用

很多场景,比如给ajax请求传递参数时,参数往往是一个对象,对象的属性个数不是固定的,需要动态添加:

let params = {
    id: 1
};

if(name){
    params.name = name;
}

if(address){
    params.address= address;
}

现在ES6提供了扩展运算符,以上的代码可以改成下面这样:

let params = {
    id: 1,
    ...(name && {name}),
    ...(address && {address}),
}

如果nameaddress的值都存在且不是空字符串,结果就会是这样:

{
    id: 1,
    name: 'dong',
    address: '赤水沟子'
}

如果它们的值为,就会不存在nameaddress这两个属性:

{
    id: 1
}

2、原理

下面以name为例,说明它的原理。

如果name不为空,则

name && {name}

相当于

name && {name: name} //上面是ES6简写形式

相当于

if(name){
    return {
        name: name
    }
}else{
    return false;
}

如果name没有值,那么

...(name && {name})

相当于

...(false)

扩展运算符有以下规定:

1、如果扩展运算符后面是一个空对象,则没有任何效果
2、如果扩展运算符后面不是对象,则会自动将其转为对象

根据第2点,

...(false)

等同于

...Object(false)

等同于

...Boolean {false}

Boolean {false}没有任何属性,所以又等同于

...{}

根据第1点可以知道,这没有任何效果,不会向params添加任何属性。

END

经常看到这样的JavaScript代码:

var o = {
    f: function(){
        console.log(this);
    }
}

(0, o.f)();

这样做的目的是改变f的执行环境,也就是改变this。

直接调用o.f:

o.f();
//{f: ƒ}

再试试间接调用:

(0, o.f)();
//Window...

可以看出this已经是Window了。

逗号运算符会从左往右求值,并返回最后一个表达式的值,(0, o.f)返回的是一个函数,此时这个函数的作用域是全局,相当于:var f = o.f
必须要用0吗?其实任何表达式都可以:

(1, o.f);
(1+1, o.f);
(true, o.f);

()是不是必须的呢?要调用返回的函数,或者将返回的函数赋值给某个变量,括号就是必须的。下面的代码肯定是不能正确运行的:

0, o.f(); //this指向o
var f = 0, o.f //Unexpected token '.'

我们需要()来将语句转换为表达式,这也是经常用到的方法,比如下面的代码,需要将x = 2这条语句转换为表达式:

true && (x = 2)

END

JavaScript获取时间戳有多种方式。这里有一个具有纪念意义的时间:

var now = new Date('2022/02/02 22:22:22:222');
//Wed Feb 02 2022 22:22:22 GMT+0800 (中国标准时间)

我们想看看它的时间戳是不是也如此独特。下面是如何获取它的时间戳。

方法1:Dateparse方法

Date.parse(now);
//1643811742000

Date.parse接受一个表示时间的字符串,所以这里的now先转换成了字符串Wed Feb 02 2022 22:22:22 GMT+0800 (中国标准时间),然后再被解析成时间戳。因为每个浏览器实现它的方式不一致,所以建议不要用Date.parse解析时间字符串。见:Date.parse() - MDN
要注意的是,此方法返回的毫秒数都是0.

方法2:Date实例getTime方法

now.getTime();
//1643811742222

这个方法实际上跟方法1是等价的,方法1是显式调用,方法2是隐式调用,但此方法返回正常的毫秒数。

方法3:Date实例的valueOf方法

now.valueOf();
//1643811742222

这个方法返回一个Date对象的原始值。Date对象的原始值是基于Unix Time Stamp,即自1970年1月1日(UTC)起经过的毫秒数。

方法4:+操作符获取时间戳

+now;
//1643811742222

这个方法实际上调用的是now.valueOf()
+运算符在这里是一元运算符,会将后面的操作数转换成数字,等于Number()函数。数字是一种原始值。在JavaScript内部,会调用toPrimitive把对象转换成原始值,形式如下:

toPrimitive(input,preferedType?)

preferedType可选numberstring+会将preferedType看作是number,将按照下面的规则转换:

  1. 如果input是原始值,直接返回这个值;
  2. 否则,如果input是对象,调用input.valueOf(),如果结果是原始值,返回结果;
  3. 否则,调用input.toString()。如果结果是原始值,返回结果;
  4. 否则,抛出错误。

+now按照规则2调用了now.valueOf()
注意,Date.prototype.valueOf重写了Object.valueOf,所以会返回一个时间戳。

结果证明now的时间戳并不独特:(

END

0、保存Map实例

NOTE:

如何引入天地图SDK请参考以前的文章

首先要做的肯定是保存Map实例,可以保存在Vue实例上:

this.Map = new T.Map(......);

或者保存在window上:

window.Map = new T.Map(......);

1、创建标注

创建标注使用的是工具类。下面都用点(Marker)来作为例子。

在页面中创建一个按钮,使用它的click事件来打开标注工具

<button @click="btnClick">添加点</button>

为了看起来不违和,你可以把这个按钮的样式做成和地图原生控件一样。

click事件中打开标注工具:

btnClick(){
  //如果标注工具已经存在,先关闭它
  window.marktool && window.marktool.close();

  //创建一个Icon实例
  let icon = new T.Icon({
    iconUrl: 'https://x.y.z/1.png'
  });

  //实例化工具
  let marktool = new T.MarkTool(window.Map, {
    icon,
    follow: true
  });

  marktool.open();
  window.marktool = marktool;
}

现在,点击添加点按钮的时候,鼠标指针将变成你定义的icon的样式,点击地图,就会在点击的位置留下一个标记。

2、给标记添加自定义的数据

标记肯定代表了某个实际的东西,需要补充额外的内容。可以在上一步给marktool注册一个mouseup事件,给刚刚创建的标记添加一个click事件,以便我们进行更多的操作:

//别忘了先保存一下Vue实例this:
let _this = this;
//onMarkMarked是Vue的Method
//e.currentMarker代表当前标记
marktool.addEventListener('mouseup', function (e) {
  _this.onMarkMarked(e.currentMarker);
});

VueonMarkMarked方法中:

onMarkMarked(marker){
  let _this = this;
  //直接添加数据
  marker.MyData = {foo: 'bar', baz: 'balabala...'};
  //保存marker,使页面中任何代码都能访问,就能任意赋值了
  //但要注意,currentMark保存的是当前标记,创建新标记时,它代表的是新的标记
  _this.currentMark = marker;
}

Vue的其他方法中给它赋值:

onModelClosed(){
  this.currentMark.myname = this.inputName;
  //......

  //可以在控制台看到marker实例中包含自定义的数据
  console.log(this.currentMark);
}

3、保存标记数据

首先获取到标记:

let markers = window.Map.getOverlays();

返回的是一个标记数组,如果有多个标记,根据需要,可能需要循环处理。这里假设只有一个marker标记。
标记信息含有很多无用的信息,我们需要的一般都是标记的坐标、自定义的数据。标记的样式由于是提前定义好的,所以并不会通过标记获取。

let lnglat = markers[0].getLnglat();
let { myname, MyData } = markers[0];

此时就可以保存到数据库了。


END

UPDATE:8/16

天地图的JavaScript3.0 API中提供了一些方法,在4.0的文档中没有,但是仍然可用,比如设置地图的样式:

map.setMapType(...)

1、引入天地图API

按需引入,在需要用到天地图API的时候,动态创建script标签:

//某组件
...
mounted() {
    this.initMap();
},
...
//methods
initMap(){
    let script = document.getElementById('MapDivId');
    if (script) {
        return this._initMap();
    }

    const _this = this;
    script = document.createElement('script');
    script.id = 'MapDivId';
    script.src = 'https://tianditu.gov.cn/xxxxxxxx';
    script.onload = function () {
        _this._initMap();
    };

    document.body.appendChild(script);
}

二、初始化地图

...
_initMap() {
    //添加卫星图层,下面这个url是固定的,我只找到这种默认显示卫星图层的办法
    let satelliteLayerUrl = "http://t0.tianditu.gov.cn/img_w/wmts?" + "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles" + "&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=您的密钥";
    let layer = new T.TileLayer(satelliteLayerUrl, { minZoom: 3, maxZoom: 18 });
    window.GIS.Map = new T.Map('Map', { layers: [layer] });
    // 定位到一个默认的坐标,例如北京。这里的坐标是虚构的
    window.GIS.Map.centerAndZoom(new T.LngLat(123.45678, 30.1234), 12);

    //添加地图类型控件
    let ctrlMapType = new T.Control.MapType([
        {
            title: '卫星混合',
            icon: 'http://api.tianditu.gov.cn/v4.0/image/map/maptype/satellitepoi.png',
            layer: TMAP_HYBRID_MAP
        },
        {
            title: '地图',
            icon: 'http://api.tianditu.gov.cn/v4.0/image/map/maptype/vector.png',
            layer: TMAP_NORMAL_MAP
        }
    ]);
    window.GIS.Map.addControl(ctrlMapType);

    //添加缩放控件
    let ctrlZoom = new T.Control.Zoom();
    window.GIS.Map.addControl(ctrlZoom);
    ctrlZoom.setPosition(T_ANCHOR_BOTTOM_RIGHT);

    //添加比例尺控件
    let ctrlScale = new T.Control.Scale();
    window.GIS.Map.addControl(ctrlScale);
},
...

三、添加各种标注工具

首先编写HTML结构,定位到地图上的某个位置,并绑定一个click事件,这部分很简单,效果如下:

1.png

然后编写click事件代码:

<span class="icon xxxx" @click="openPolygonTool"></span>
openPolygonTool() {
    if (window.GIS.handler) window.GIS.handler.close();
    let handler = new T.PolygonTool(window.GIS.Map);
    handler.open();
    window.GIS.handler = handler;
}

效果如下:

2.png

四、保存标注数据

一般在打开标注工具的时候,注册一个相关事件,这样,在结束标注的时候,就能获取到标注的数据。多边形的结束事件是draw

openPolygonTool() {
    ...
    handler.addEventListener('draw', function(e){
        console.log(e);
    });
    handler.open();
    ...
}

到这里,就可以把需要的数据存储起来了。

END