Yuchen's Blog

BrainStorming


  • 首页

  • 归档

  • 标签

使用scp2快速进行前端局域网部署

发表于 2020-07-16

前端局域网部署一直都是我跑shell脚本,每次都要手动输入密码,偶然间看到了滴滴团队在掘金的文章,在打包完成后,使用scp2 进行代码上传,过程非常丝滑!

滴滴 webapp 5.0 Vue 2.0 重构经验分享

scp2

代码

新建文件 scp2.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const client = require('scp2');
client.scp('dist.zip', {
host: 'xx.xx.xx.xx',
username: 'name',
password: 'pass',
path: 'path'
}, (err) => {
if (err) {
console.log(err);
} else {
console.log('Finished!');
}
});

npm scripts

1
2
3
scripts: {
"build:local": "vue-cli-service build && zip -r dist.zip dist && node scp2.js && rm -rf dist.zip"
}

Function.prototype.call()

发表于 2020-07-10

老知识点常学常新

定义

call()方法使用一个指定的 this值和单独给出的一个或多个参数来调用一个函数。

语法

function.call(thisArg, arg1, arg2, ...)

  • thisArg可选,在 function 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。
  • arg1, arg2, ...指定的参数列表

使用

call()允许为不同的对象分配和调用属于一个对象的函数/方法。

call() 提供新的 this 值给当前调用的函数/方法。你可以使用 call 来实现继承:写一个方法,然后让另外一个新的对象来继承它(而不是在新对象中再写一次这个方法)。

示例

  • 使用call方法调用父构造函数

在一个子构造函数中,你可以通过调用父构造函数的 call 方法来实现继承,类似于 Java 中的写法。下例中,使用 Food 和 Toy 构造函数创建的对象实例都会拥有在 Product 构造函数中添加的 name 属性和 price 属性,但 category 属性是在各自的构造函数中定义的。

1
2
3
4
5
6
7
8
9
10
11
function Product(name, price) {
this.name = name;
this.price = price;
}
function Food(name, price) {
Product.call(this, name, price);
this.category = 'food';
}
const cheese = new Food('feta', 5);
  • 使用call方法调用匿名函数

由DAG图的环路检测学习Set和Map

发表于 2020-07-08

业务上有一个需求是检测工作流模板是否是DAG图,DAG图即有向无环图,常见的解决方法是拓扑排序,如果存在环路,则不能进行拓扑排序。

拓扑排序

拓扑排序是有向无环图所有顶点的线性序列。

满足两个条件:

  1. 每个顶点只出现一次
  2. 若存在从A到B的路径,那么序列中A出现在B之前

环路检测算法

  1. 找到入度为0的点,输出该点
  2. 删除该点及该点的所有出边
  3. 重复步骤一二,直到输出图中所有节点,如果图中有节点未输出,则存在环路

实现

例如下图存在环路

1. 确定数据格式

1
2
3
4
5
6
7
8
9
const edges = [
{ source: 'start', target: '1' },
{ source: '1', target: '2' },
{ source: '2', target: '3' },
{ source: '3', target: '2' },
{ source: 'start', target: '4' },
{ source: '4', target: '3' }
];
const nodes = ['start', '1', '2', '3', '4'];

2.创建邻接表存储图的信息

1
2
3
4
5
6
7
8
9
10
11
12
const nodeSet = new Set(nodes);
const graph = new Map();
for (let i = 0; i < edges.length; i++) {
const { source, target } = edges[i];
// 将该边加入到邻接表中
if (!graph.has(source)) {
graph.set(source, []);
}
const sourceArr = graph.get(source);
sourceArr.push(target);
graph.set(source, sourceArr);
}
1
2
3
4
5
6
7
8
9
10
// graph 数据
Map {
'start' => [ '1', '4' ],
'1' => [ '2' ],
'2' => [ '3' ],
'3' => [ '2' ],
'4' => [ '3' ]
}
// node 数据
Set { 'start', '1', '2', '3', '4' }

3.找到入度为0的点,删除所有出边

1
2
3
4
5
// 'start'为第一个入度为0的节点
graph.delete('start');
nodeSet.delete('start');
// 删掉的节点缓存起来
stack.push('start');

删除start之后的 graph 与 node

1
2
3
4
5
Map { '1' => [ '2' ],
'2' => [ '3' ],
'3' => [ '2' ],
'4' => [ '3' ] }
Set { '1', '2', '3', '4' }

graph中,key代表线的出点,value代表线的入点,将value去重后,与nodeSet 取差集就得到下一批入度为0的节点。

1
2
3
4
5
6
7
8
9
10
nodeSet.forEach(() => {
const diff = getZeroNode(graph, nodeSet);
if (diff.length) {
diff.forEach((item) => {
stack.push(item);
graph.delete(item);
nodeSet.delete(item);
});
}
});
1
2
3
4
5
6
7
8
// 找到入度为0的节点
function getZeroNode(graph, nodeSet) {
// graph 取 value 去重
const graphArr = new Set([...graph.values()].flat());
// 与node Set 取差集
const diff = new Set([...nodeSet].filter((x) => !graphArr.has(x)));
return [...diff];
}

4.判断栈内节点数,若小于原始节点数则存在环路

1
2
3
4
if (stack.length < nodes.length) {
return true;
}
return false;

实践中用到的Set和Map实例方法

Set

类似数组,但是成员的值唯一。

  • delete(value) 删除某个值,返回布尔值,表示是否删除成功
  • has(value) 返回布尔值,表示参数是否为成员

数组去重

[...new Set(array)]

并集

new Set([...a, ...b])

交集

new Set([...a].filter(x => b.has(x)))

差集

new Set([...a].filter(x => !b.has(x)))

Map

类似对象,也是键值对的集合。但是"键"不限于字符串,各种类型都可以做为键。

  • set(key, value) 设置key对应的键值
  • get(key) 读取key对应的键值
  • has(key) 返回布尔值,表示某个键是否再Map数据结构中
  • delete(key) 删除某个键

Map的遍历方法

  • values() 返回键值的遍历器

前端项目内网linux构建踩坑

发表于 2020-06-19

公司搞了一套Devops流程,在生产环境构建的时候遇到了一系列坑

问题

1
2
3
4
5
6
7
8
9
18:05:08 gyp verb ensuring nodedir is created /root/.node-gyp/12.18.0
18:05:08 gyp verb created nodedir /root/.node-gyp
18:05:08 gyp http GET https://nodejs.org/download/release/v12.18.0/node-v12.18.0-headers.tar.gz
18:05:13 gyp WARN install got an error, rolling back install
18:05:13 gyp verb command remove [ '12.18.0' ]
18:05:13 gyp verb remove using node-gyp dir: /root/.node-gyp
18:05:13 gyp verb remove removing target version: 12.18.0
18:05:13 gyp verb remove removing development files for version: 12.18.0
18:05:13 gyp ERR! configure error

在指定下载源后,发现node-gyp 总是会去node官网下载node-v12.18.0-headers.tar.gz这个包,没有从指定的源下载,所以一直失败。

后来在github查到了相关
issue

解决方案

先把node-v12.18.0-headers.tar.gz下载好放到机器上

1
2
npm config set tarball ${path}
// path是node-v12.18.0-headers.tar.gz的路径

JavaScript参数按值传递

发表于 2020-06-16

定义

ECMAScript中所有函数的参数都是按值传递的。

案例一

1
2
3
4
5
6
7
const value = 1;
function test(v) {
v = 2;
console.log(v); // 2
}
test(value);
console.log(value); // 1

修改前:

栈内存 堆内存
value 1
v 1

修改后:

栈内存 堆内存
value 1
v 2

案例二

1
2
3
4
5
6
7
8
9
const obj = {
value: 1
};
function test(o) {
o.value = 2;
console.log(o.value); //2
}
test(obj);
console.log(obj.value); // 2

修改前:

栈内存 堆内存
obj,o 指针地址 {value: 1}

修改后:

栈内存 堆内存
obj,o 指针地址 {value: 2}

案例三

1
2
3
4
5
6
7
8
9
const obj = {
value: 1
};
function test(o) {
o = 2;
console.log(o); //2
}
test(obj);
console.log(obj.value); // 1

修改前:

栈内存 堆内存
obj,o 指针地址 {value: 1}

修改后:

栈内存 堆内存
obj,o 指针地址 {value: 1}
o 2

Vue Tricks

发表于 2020-06-09

摘抄一些Vue的小技巧

监听组件声明周期

使用 @hook 即可监听组件生命周期。同样的, created 、 updated 等也可以使用此方法。

1
2
3
<template>
<List @hook:mounted="listenMounted" />
</template>

程序化事件侦听器

在组件销毁时要移除侦听器,肯定写过以下代码。this.timer 唯一的作用只是为了能够在 beforeDestroy 内取到计时器序号,除此之外没有任何用处。

1
2
3
4
5
6
7
8
9
10
export default {
mounted() {
this.timer = setInterval(() => {
console.log(Date.now())
}, 1000)
},
beforeDestroy() {
clearInterval(this.timer)
}
}

可以通过 $on 或 $once 监听页面生命周期销毁来解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default {
mounted() {
this.creatInterval( hello )
this.creatInterval( world )
},
creatInterval(msg) {
let timer = setInterval(() => {
console.log(msg)
}, 1000)
this.$once('hook:beforeDestroy', function() {
clearInterval(timer)
})
}
}

CSS样式穿透

样式穿透在css预处理器中使用才生效。

  • less 使用 /deep/
1
2
3
4
5
<style scoped lang="less">
.content /deep/ .el-button {
height: 60px;
}
</style>
  • scss 使用 ::v-deep
1
2
3
4
5
<style scoped lang="scss">
.content ::v-deep .el-button {
height: 60px;
}
</style>
  • stylus使用 >>>
1
2
3
4
5
<style scoped ang="stylus">
外层 >>> .custon-components{
height: 60px;
}
</style>

路由参数解耦

1
2
3
4
5
6
7
const router = new VueRouter({
routes: [{
path: /user/:id ,
component: User,
props: true
}]
})

将路由的 props 属性设置为 true 后,组件内可通过 props 接收到 params 参数。

1
2
3
4
5
6
7
8
export default {
props: [ id ],
methods: {
getParamsId() {
return this.id
}
}
}

另外还可以通过函数模式来返回 props

1
2
3
4
5
6
7
8
9
const router = new VueRouter({
routes: [{
path: /user/:id ,
component: User,
props: (route) => ({
id: route.query.id
})
}]
})

事件参数$event

$event 是事件对象的特殊变量,在一些场景能给我们实现复杂功能提供更多可用的参数

  • 原生事件:在原生事件中表现和默认的事件对象相同
1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div>
<input type="text" @input="inputHandler( hello , $event)" />
</div>
</template>
export default {
methods: {
inputHandler(msg, e) {
console.log(e.target.value)
}
}
}
  • 自定义事件:在自定义事件中表现为捕获从子组件抛出的值
1
2
3
4
5
6
7
8
// 子组件
export default {
methods: {
customEvent() {
this.$emit( custom-event , some value )
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div>
<my-item v-for="(item, index) in list" @custom-event="customEvent(index, $event)" />
</div>
</template>
export default {
methods: {
customEvent(index, e) {
console.log(e) // some value
}
}
}

watch高阶使用

  • 立即执行

watch 是在监听属性改变时才会触发,有些时候,我们希望在组件创建后 watch 能够立即执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default {
data() {
return {
name: Joe
}
},
watch: {
name: {
handler: sayName ,
immediate: true <--------
}
},
methods: {
sayName() {
console.log(this.name)
}
}
}
  • 深度监听

在监听对象时,对象内部的属性被改变时无法触发 watch ,我们可以为其设置深度监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export default {
data: {
studen: {
name: Joe ,
skill: {
run: {
speed: fast
}
}
}
},
watch: {
studen: {
handler: sayName ,
deep: true <-------
}
},
methods: {
sayName() {
console.log(this.studen)
}
}
}
  • 触发监听执行多个方法

使用数组可以设置多项,形式包括字符串、函数、对象

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
export default {
data: {
name: Joe
},
watch: {
name: [
sayName1 ,
function(newVal, oldVal) {
this.sayName2()
},
{
handler: sayName3 ,
immaediate: true
}
]
},
methods: {
sayName1() {
console.log( sayName1==> , this.name)
},
sayName2() {
console.log( sayName2==> , this.name)
},
sayName3() {
console.log( sayName3==> , this.name)
}
}
}

CSV文件上传后台解析乱码问题

发表于 2019-11-05

1. 问题描述

后台用node来解析前端上传的CSV文件,由于不同系统CSV文件编码格式不同,导致后台解析出现乱码。

2. 解决思路

找出CSV编码格式 —> 用该编码方式解码 —> csv-parse解析内容

3. 实践

NPM库 jschardet 可以识别出一个Buffer数据所使用的编码格式。

用法如下

1
2
3
const jschardet = require("jschardet");
jschardet.detect("\xc3\xa0\xc3\xad\xc3\xa0\xc3\xa7\xc3\xa3");
// { encoding: "UTF-8", confidence: 0.9690625 }

支持如下编码

  • Big5, GB2312/GB18030, EUC-TW, HZ-GB-2312, and ISO-2022-CN (Traditional and Simplified Chinese)
  • EUC-JP, SHIFT_JIS, and ISO-2022-JP (Japanese)
  • EUC-KR and ISO-2022-KR (Korean)
  • KOI8-R, MacCyrillic, IBM855, IBM866, ISO-8859-5, and windows-1251 (Russian)
  • ISO-8859-2 and windows-1250 (Hungarian)
  • ISO-8859-5 and windows-1251 (Bulgarian)
  • windows-1252
  • ISO-8859-7 and windows-1253 (Greek)
  • ISO-8859-8 and windows-1255 (Visual and Logical Hebrew)
  • TIS-620 (Thai)
  • UTF-32 BE, LE, 3412-ordered, or 2143-ordered (with a BOM)
  • UTF-16 BE or LE (with a BOM)
  • UTF-8 (with or without a BOM)
  • ASCII

4. 代码

1
2
3
4
5
6
7
8
9
const jschardet = require("jschardet")
const iconv = require('iconv-lite');
const csvParser = require('csv-parse');
// 识别文件编码格式
const { encoding } = jschardet.detect(buffer);
// 按照原格式解码文件
const content = iconv.decode(buffer, encoding);
// 解析文件内容
csvParser(content);

如何在2D CANVAS 中渲染3D图像

发表于 2019-04-03

原文地址

在探索如何实践的最佳方法时,我们想出了一个十分有趣的原型:从一个充满颗粒的旋转球开始。

See the Pen render-3d-in-2d-canvas by yuchenyao (@yuchenyao) on CodePen.

阅读全文 »

ES2019中的新特性

发表于 2019-03-04

文本为翻译计划的第一篇文章,原文地址

ES2019最近已完成制定,这意味着JavaScript将有很多新特性供大家学习。这些特性目前可以在Chrome 73中使用。

  • Array#{flat, flatMap}
  • Object.fromEntries
  • String#{trimStart, trimEnd}
  • Symbol#description
  • try {} catch {} optional binding
  • JSON superset
  • well-formed JSON.stringify
  • stable Array#sort
  • revised Function#toString

遗憾的是,在写此文的时候还没有基于V8 7.3的Node.js版本,目前的Node.js 12 仍然是基于V8 7.1。然而,V8 v7.0已经包含了ES2019的大部分特性,如下图所示。本文中我们仍然使用Node.js 11.6(V8 v7.0.276)。

Array#flat()和Array#flatMap()

flat() 和 flatMap()在Chrome 69 和 V8 6.9 中是可用的,因此你需要 Node.js 11。

Array#flat()跟Lodash的_.flattenDepth()方法类似。flat()方法接受一组嵌套数组,并将该嵌套数组扁平化。

1
[[1, 2], [3, 4], [5, 6]].flat(); // [1, 2, 3, 4, 5, 6]

默认情况下,flat()仅扁平化一层:

1
2
// [1, 2, 3, 4, 5, 6]
[[[1, 2]], [[3, 4]], [[5, 6]]].flat(2);

但是,flat()接收一个表示深度的参数来确定你想要扁平化的深度。

1
2
// [1, 2, 3, 4, 5, 6]
[[[1, 2]], [[3, 4]], [[5, 6]]].flat(2);

新的flatMap()方法相当于在使用map()之后调用flat()。如果你的map()返回一个数组,这将非常方便。

1
2
3
4
const oddNumbers = [1, 3, 5, 7, 9];
// [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
const allNumbers = oddNumbers.flatMap(num => ([num, num + 1]));

因为flatMap()的回调参数返回了一个数组,flatMap()扁平化了该数组。上面的方法类似于:

1
const allNumbers = oddNumbers.map(num => ([num, num + 1])).flat();

使用flatMap()时有一个优雅的小技巧,你可以一步实现filter()和map()。 你可以在flatMap()回调中通过返回一个空数组过滤掉一个参数。

1
2
// [ 1, 3, 5, 7, 9 ]
const oddNumbers = allNumbers.flatMap(num => num % 2 === 0 ? [] : num);

Object.fromEntries()

Object.fromEntries()方法目前在所有版本的Node.js中都无法使用,你可以npm安装 object.fromentriespolyfill。

Object.fromEntries()试图使Map到Object的转换变得简单。

1
2
Object.fromEntries(new Map([['hello', 'world'], ['foo', 'bar']]));
// // { hello: 'world', foo: 'bar' }

一个灵巧的副作用是你可以把键值对数组转化为JavaScipt对象。

1
2
Object.fromEntries([['hello', 'world'], ['foo', 'bar']]);
// { hello: 'world', foo: 'bar' }

该方法的主要功能是将map类型转换为object类型,但是键值对的转换仍然是有帮助的。

例如,MangoDB 不允许用.符号来存储key。在MangoDB中存储带有.的key时要使用键值对数组。 Object.fromEntries()和它的反函数 Object.entries()使对象与键值对数组之间的转换变得容易了很多。

1
2
3
4
5
6
7
8
9
10
const packages = { 'lodash.get': '4.x', 'object.fromentries': '2.x' };
// Stores `[ [ 'lodash.get', '4.x' ], [ 'object.fromentries', '2.x' ] ]`
await mongoose.model('Project').create({ packages: Object.entries(packages) });
await mongoose.model('Project').findOne().then(doc => {
// Convert `[ [ 'lodash.get', '4.x' ], [ 'object.fromentries', '2.x' ] ]`
// back into a native JavaScript object.
return Object.assign(doc, { packages: Object.fromEntries(doc.packages) });
});

Symbol#description

Symbol 首次出现在ES2015中,用来解决属性间的命名冲突。一个Symbol对象内的属性名都是独一无二的。

1
2
3
4
5
const sym1 = Symbol('foo');
const sym2 = Symbol('foo');
const obj = { [sym1]: 'bar', [sym2]: 'baz' };
console.log(obj[sym1]); // 'bar'
console.log(obj[sym2]); // 'baz'

Symbol()的第一个参数被称为description。description不是id,多个symbol对象可以拥有相同的description并且互相之间没有冲突。symbol对象的description完全是为了调试功能。

在ES2019之前,如果你想访问symbol对象的description,只能使用.tostring()。

1
2
3
const sym = Symbol('foo');
sym.toString(); // Symbol('foo')

在 Node.js 11中你可以这么访问:

1
sym.description; // foo

description属性是不可写的。

1
2
3
sym.description; // foo
sym.description = 'bar';
sym.description; // still "foo"

可选的catch绑定

在Node.js 8中,使用try/catch时你总是需要在catch中声明一个变量,即使你用不到该变量。

1
2
3
4
5
try {
// ...
} catch (err) {
// Need to specify `err`, even if it isn't used.
}

ES2019中可选的catch绑定能让你跳过在catch中定义变量。可选的catch绑定在Node.js 10和Chrome 66以上版本中是可用的。

1
2
3
4
5
try {
// ...
} catch {
// No `err` variable
}

ANT-FORM

发表于 2018-07-25

对象的扩展运算符,用于去除参数对象的所有可遍历属性,拷贝到当前对象之中。

1
2
3
let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }

克隆一个对象有两种写法

1
2
3
let aClone = { ...a };
// 等于
let aClone = Object.assign({}, a);

扩展运算符合并两个对象

1
2
3
let ab = { ...a, ...b};
// 等于
let ab = Object.assign({}, a, b);
123
Yuchen Yao

Yuchen Yao

21 日志
10 标签
GitHub 微博
© 2017 - 2020 Yuchen Yao
由 Hexo 强力驱动
主题 - NexT.Mist