Futurefinger

Futurefinger 查看完整档案

杭州编辑浙江大学  |  电子商务 编辑某金融公司  |  web前端工程师 编辑 www.xxxx.com 编辑
编辑

纸上得来终觉浅,绝知此事要躬行。
/*

      ___                _
     / __|___  ___  __ _| |___
    | (_ / _ \/ _ \/ _` |   -_)
     \___\___/\___/\__, |_\___|
                   |___/
     
    */

个人动态

Futurefinger 赞了文章 · 11月12日

数组去重(JavaScript 为例)

数组去重,就是在数组中查找相同的元素,保留其中一个,去除其他元素的程。

从这句话揭示了数组去重的两个关键因素:

  1. 找到重复项
  2. 去除重复项

本文告诉你在遇到去重问题时该如何思考,并以 JavaScript 为例,进行详细解释。使用 JavaScript 示例主要是因为它环境比较好找,而且直接对象 (Plain Object) 用起来很方便。

JavaScript 的环境:Node.js 或者浏览器的开发者控制台。

找到重复项

找到重复项最关键的算法是判定元素是否相同。判定相同,说起来似乎很简单 —— 用比较运算符就好了嘛!真的这么简单吗?

用 JavaScript 来举个例:

const a = { v: 10 };
const b = { v: 10 };

肉眼观察,这里的 ab 相同吧?但是 JavaScript 不这么认为:

console.log(a == b);    // false
console.log(a === b);   // false

肉眼观察和程序比较使用了不同的判断方法。肉眼观察很直接的采用了字符串比对的方法,而程序压根没管是不是数据相同,只是直接判断它们是不是同一个对象的引用。我们一般会更倾向于使用符合人眼直观判断的方法,所以可能会想到使用 JSON.stringify() 把对象变成字符串来判断:

console.log(JSON.stringify(a) === JSON.stringify(b));   // true

现在如果我们把 ab 略作改变,又该如何?

const a = { v: 10, n: "1" };
const b = { n: "1", v: 10 };

乍一看,ab 不同。用 JSON.stringify() 的结果来比对,也确实不同。但是仔细一看,他们的属性是完全相同的,唯一的区别在于属性的顺序不一样。那么到底顺序应不应该作为一个判断相同的依据呢?

这个问题现在真没法回答。“该如何”取决于我们的目标,也就是业务需求。

从上面的例子我们可以了解:判断相同并不是一个简单的事情,根据不同的业务要求,需要选择不同的判断方法;而不同的判断方法,可能产生不同的判断结果。

接下来先讲讲常见的判断方法。

最直接的:比较运算符

比较运算符主要用于比较基本类型的值,比如字符串、数、布尔等。

普通比较运算符 (==) 在比较不同类型的值时,会先把它们转换为相同类型再来比较;而严格比较运算符 (===) 则更为严格,会直接将类型不同值判定为不同。这些都是基本的 JavaScript 语法知识。现代开发中为了能更好的利用工具,除极少数特殊情况外,都应该使用 === 来进行判断。尤其是在 TypeScript 中,几乎都不会出现 == 了。

JavaScript 中,比较运算符不会比较对象属性,只会比较对象的引用是否相同。如果要比较对象具体信息,需要用到接下来讲到的方法。

完整信息比对

顾名思议,就是对对象或者数组进行完整地比对,将对象的属性,或者数组的元素拿出来一一对比,判断是否完全相同。比如前面示例中的 ab,在不考虑属性顺序的情况下,他们都有相同的属性 vn,而且每个属性的值也相同,所以我们可以判定他们是完全相同的。

这需要通过遍历属性的方式来比较:

function compare(a, b) {
    // 先判断属性个数,如果属性个数不等,那肯定不相同
    const aEntries = Object.entries(a);
    const bEntries = Object.entries(b);
    if (aEntries.length !== bEntries.length) {
        return false;
    }

    // 再遍历逐一判断属性,只要有一个不等,那就整个不相同
    for (const [key, value] of aEntries) {
        if (b[key] !== value) { return false; }
    }

    return true;
}

上例中那个简单的 compare 函数似乎可以达到目的,但是很容易被证伪:

  • 如果 ab 是数组怎么办?
  • 如果 ab 是多层属性结构怎么办?

    比如:{ v: 10, n: "1", c: { m: true } }

逻辑更严密的比较方法需要判断类型,不同的类型进行不同的比较;同时,对于多层次的属性结构,通过递归深入比较(注意阅读注释)。

function deepCompare(a, b) {
    // 类型相同,值或引用相同,那肯定相同
    if (a === b) { return true; }

    // 如果 a 或者 b 中有一个是 null 或 undefined,那二者不同,
    // 因此在这个条件下,a 和 b 可能相同的情况已经在前一条分支中过滤掉了。
    // 同时这个分支结合上一条分支,排除掉了 null 和 undefiend 的情况,之后不用判空了。
    if (a === null || b === null || a === undefined || b === undefined) {
        return false;
    }

    const [aType, bType] = [a, b].map(it => typeof (it));
    // 如果 a 和 b 类型不同,那就不同
    if (aType !== bType) { return false; }

    // 我们重点要深入判断的是对象和数组,它们的 typeof 运算结果都是 "object",
    // 其他类型就简单判断。前面已经处理了等值和空值的情况,剩下的就直接返回 false 了
    if (aType !== "object") { return false; }

    if (Array.isArray(a)) {
        // 作为数组进行比较,数组是一个单独的逻辑,
        // 使用 IIFE 封装是为了保证能 return,避免混入后面的逻辑。
        // 所以这里的 IIFE 不是必须的。
        return (() => {
            if (a.length !== b.length) { return false; }
            for (const i in a) {
                if (!deepCompare(a[i], b[i])) {
                    return false;
                }
            }
            return true;
        })();
    }

    // 使用之前的逻辑判断对象,记得把属性值判断那里改成递归判断,
    // 使用 IIFE 封装逻辑
    return (() => {
        // 先判断属性个数,如果属性个数不等,那肯定不相同
        const aEntries = Object.entries(a);
        const bEntries = Object.entries(b);
        if (aEntries.length !== bEntries.length) {
            return false;
        }

        // 再遍历逐一判断属性,只要有一个不等,那就整个不相同
        for (const [key, value] of aEntries) {
            if (!deepCompare(value, b[key])) { return false; }
        }

        return true;
    })();
}

上面的 deepCompare 可以处理大部分直接对象和数组数据的比较,但仍然会有一些特殊的情况处理不了,比如对象中存在循环引用时 deepCompare 会陷入死循环。这个 deepCompare 只是简单介绍了一下完整信息对比思路,在生产环境中可以使用 Lodash 的 _.isEqual() 方法。

关键信息比对

完整信息比对消耗较大。对于某些特定的业务对象来说,可能会有一些标识性的属性用来进行快速判定。比如对于用户信息来说,通常会有用户 ID 能唯一识别用户,所以比对的时候只需要简单的比对用户 ID 就可以了。比如,下面的 u1u2

const u1 = {
    userId: 123,
    name: "James",
};

const u2 = {
    userId: 123,
    phone: "12345678900"
}

虽然 u1u2 有着不同的属性,但是关键信息是相同的,所以可以认定为同一人的信息。只是比对过后,我们可能需要对这两个信息进行一个选择,或者进行合并。这将是后面“去重”要干的事情。

HASH 比对

去重的过程中通常需要对一个对象进行多轮比对,如果不能使用关键信息快速比较,每次都进行完整信息比对可能会非常耗时——尤其是对象层次较深而且数据庞大的时候。这种情况下我们可以考虑 HASH 比对。也就是根据对象的属性,计算出来一个相对唯一的 HASH 值,每次比对时只需要检查 HASH 值是否相同,就会非常快速。

比如对上述示例中的 ab 对象,可以使用这样一个简单的 HASH 算法:

// 算法仅用于示意,并未验证其有效性

const computeHash = (() => {
    /**
     * @param {string?} s
     * @return {number}
     */
    function hashString(s) {
        if (!s) { return 0; }
        let hash = 0x37;
        for (let i = 0; i < s.length; i++) {
            hash = (hash << 4) ^ s.charCodeAt(i);
        }
        return hash;
    }

    /**
     * @param {{v: number, n: string}} obj
     * @return {number}
     */
    return function (obj) {
        const hn = hashString(obj.n);
        const hv = obj.v;
        return Math.pow(hv, 7) ^ hn;
    }
})();

computeHash 函数也使用 IIFE 进行了封装,主要是想把 hashString() 作为一个内部算法保护起来,不被外部直接调用。

然后,可以分别计算 ab 的 Hash 值用于比较

const [aHash, bHash] = [a, b].map(it => computeHash(it));
console.log(aHash, bHash);

console.log(computeHash(a) === computeHash(b));

// 9999809 9999809
// true

不过,在去重的过程中,每次都调用 computeHash 仍然不能达到减少消耗 CPU 的目的。所以应该使用一个属性把 HASH 缓存起来。对于可以改变原对象的情况,直接找个无害的名称,比如 _hash 作为属性名保存起来就好:

// 假设去重数组叫 list

for (const it of list) {
    it._hash = computeHash(it);
}

如果不能改变原对象,可以考虑对原对象进行一层封装:

// 去重前封装,这里封装成数组,也可以封装成对象
const wrapped = list.map(it => ([it, computeHash(it)]));

// 去重后拆封
const result = resultList.map(it => it[0]);

使用 Hash 的办法的确是可以较大程度地节约比较时间,但它仍然存在两个问题:

  1. 计算 Hash 需要知道参与比较的单个元素结构
  2. Hash 存在碰撞,也就是说,可能存在两个不同的对象算出相同的 Hash。好的 Hash 算法可以降低碰撞概率,但不能杜绝。

综合比对:hashCode + equals

鉴于 Hash 算法存在碰撞的可能 ,我们在比较时并不能完全信任 Hash 比较。我们知道:

  1. 相同对象 Hash 得到的结果相同
  2. 不同对象的 Hash 存在碰撞的可能

可以总结出:Hash 不同时,计算出这个 Hash 的对象一定不同。

因此,我们可以使用 Hash 来进行快速失败计算,也就是比较 Hash 不同时,这两个对象一定不同,直接返回 false。比较 Hash 相同,再进行细致地比对,也就是完整信息比对。那么这个算法的示意就是:

function compare(a, b) {
    if (computeHash(a) !== computeHash(b)) { return false; }
    return deepCompare(a, b);
}

这就是我们常说的 hashCode + equals 比对方法。像 Java、C# 等语言都在 Object 基类中定义了 hash code 和 equals 接口,方便用于快速比较。JavaScript 虽然没有定义这样的接口,但是可以自己在写对象的时候进行实现。如果使用 TypeScript 和 class 语法,还可以有更强的静态检查来确保这两个方法得以实现。

在数组去重过程中,关于判定相同的方法就介绍这些。接下来是介绍“去重”这一过程。

去除重复项

有去除就有保留。我们首先要确定保留什么,去除什么。

在数组去重的过程中,通常会保留数组中找到的第一个非重复对象,并将其作为参照对象,拿数组中后面的元素跟它进行比较。下面是一个典型的去重过程:

典型去重(不改变原数组)

function makeUnique(arr) {
    // 结果集,也是非重复对象参照集
    const result = [];
    for (const it of arr) {
        // 遍历数组,检查数组中每个元素是否存在于 result 中,
        // 已存在则抛弃,未存在则加入 result
        if (!result.find(rIt => compare(it, rIt))) {
            result.push(it);
        }
    }
    return result;
}

这个算法不会改变原数组,去除重复项的结果会保存到一个新的数组中返回出来。

Lodash 中也提供了很方便的去重方法 _.uniqWith(arr, equals)equals 是用于比较两个对象是否相同的函数,可以用上面定义的 compare 函数,或者干脆就用 Lodash 提供的 _.isEqual(),所以使用 Lodash 去重很简单:

const result = _.uniqWith(list, _.isEqual);

直接从原数组去重

function makeUnique(arr) {
    for (let i = 0; i < arr.length; i++) {
        // 在之前的对象中检索,看是否已经存在
        for (let j = 0; j < i; j++) {
            if (compare(arr[j], arr[i])) {
                // 若在之前的部分中已经存在,删除当前元素,
                // 注意删除后,后面的元素会前移,所以 i 值不应该改变,
                // 考虑到下次循环前会进行 i++,所以先 i--
                arr.splice(i, 1);
                i--;
            }
        }
    }
}

直接从原数组去重时,已经遍历过的元素一定是非重复的,可以作为非重复项缓存来使用。所以这里不需要再单独定义一个缓存,直接使用数组的前半部分就好,因此第 2 重循环中的 j 值范围是 [0, i)

基于 Hash 算法的去重

function makeUnique(arr, hashCode, equals = () => true) {
    // 用新的对象将 Hash 值和原对象封装起来,
    // 为了方便阅读,这里使用了对象封装,而不是前面示例中的数组封装
    const wrapped = arr.map(value => ({
        hash: hashCode(value),
        value
    }));
    
    // 遍历去重的算法和前面典型去重算法一样
    const wrappedResult = [];
    for (const it of wrapped) {
        if (!wrappedResult.find(rIt =>
            it.hash === rIt.hash
            // 如果 hash 相同,还需要细致对比。
            // 不过默认的 equals 放弃了细致对比(直接返回 true)
            && equals(it, rIt)
        )) {
            wrappedResult.push(it);
        }
    }

    // 去重后的结果要解除封装后返回
    return wrappedResult.map(it => it.value);
}

Lodash 的 uniqBy()

Lodash 也提供了 _.uniqBy(arr, identity),用于根据 identity 的计算结果来判断重复,注意,这并不是基于 hashCode + equals 的判重算法。_.uniqBy() 方法并没有提供第三个参数,不能进行细致比较,所以它要求 identity 参数要能找到或算出唯一识别对象的值。

所以 _.uniqBy() 多数是用于对对象的唯一值属性判断,比如:

_.uniqBy(users, user => user.id);

如果需要对对象的多个属性进行联合判断,也就是非唯一关键信息比对时,_.uniqWith()_.uniqBy() 更合适。

保留最后一个,或者合并

通常我们认为重复的对象是完全一样的,所以保留找到的第 1 个,而将后面出现的删除掉。但是如果通过关键信息比对,这些被判定重复的对象就有可能不完全一样。这种情况下,根据业务需求,可能存在两种处理方式:

  1. 保留最后一个。可扩展为保留最近的、版本号最大的等。
  2. 合并重复对象,比如前面“关键信息比较”示例中的 u1u2

保留最后一个,就是找到重复项之后,把非重复项缓存中的那一个给替换掉。以经典去重为例,因为要改变目标数组的元素,所以 find() 就不好用了,应该改为 findIndex()

function makeUnique(arr) {
    const result = [];
    for (const it of arr) {
        const index = result.findIndex(rIt => compare(it, rIt));
        if (index < 0) {
            // 没找到仍然加入
            result.push(it);
        } else {
            // 找到了则替换
            result[index] = it;
        }
    }
    return result;
}

其中 else 的部分可以进一步判断,比如比较两个对象的 version 属性,留大舍小:

if (index < 0) { result.push(it); }
else if (it.version > result[index].version) {
    // 当新元素的 version 比较大时替换结果中的旧元素
    result[index] = it;
}

而合并也是一样的在 else 分支进行处理,比如

if (index < 0) { result.push(it); }
else {
    Object.assign(result[index], it);
}

因为不需要替换元素,而且 Object.assign 会直接修改第 1 个参数的对象,所以用 find() 也是可以的:

const found = result.find(rIt => compare(it, rIt));
if (!found) { result.push(it); }
else { Object.assign(found, it); }

小结

数组去重是一个老生长谈的问题,从众多提问者的疑惑来看,主要问题是在查找重复项上,找不到正确的判断重复的办法,本文的第一部分详细介绍了判断对象相同的方法。

另外常见的一个疑惑在于不能正确把握删除数组元素之后的元素序号。关于这个问题,只需要关注到,删除数组元素会改变后序元素的序号就容易理解了。当然,如果不改变原数组,处理起来会更方便也更不容易出错。

在进行“完整信息比对”的时候,请注意到 deepCompare 是一个很“重”的方法,不仅存在大量的判断,还需要进行递归。如果我们的对象结构明确,在很大程度上可以简化比对过程。TypeScript 无疑可以很好地约束对象结构,在 TypeScript 类型约束下,采用“关键信息比对”方法对对象的部分属性或所有属性进行比对 (equals),再适当结合 hashCode 算法,可以极大的提高比对效率。

TypeScript 已经成为前端必备技能之一,欢迎来到我的《TypeScript从入门到实践 【2020 版】》课程,好好地学一盘。


边城客栈

请关注公众号边城客栈

看完别走,点个赞 ⇓ 啊,或者 ⇘ 请作者喝咖啡

查看原文

赞 17 收藏 9 评论 1

Futurefinger 赞了文章 · 11月12日

ES6-ES10知识整合合集

目录

  • ECMAScript
  • ES2015
  • 新特性的分类
  • ES6-ES10学习版图
  • 基本语法链接整合

历经两个月,终于把所有的ES6-ES10的知识点都发布完成了,这里进行一个小的整合,以后方便查阅资料用。
这些东西打印出来A4纸也有120多页,一本小书的样子( ̄▽ ̄)/

有些东西遇到了网上查和自己整理一遍感觉还是不一样的,也希望自己可以慢慢有一种写作整理的习惯。语法是基础,还是要整体过一遍的,遇到了之后再查,心里没数都不知道从哪里查起。所以将每个部分做了个分类展示,这样查起来也好查✧(^_-✿

还是要对ECMAScript进行一下知识整理的

ECMAScript

ECMAScript通常看做JavaScript的标准化规范,实际上JavaScriptECMAScript的扩展语言,ECMAScript只是提供了最基本的语法。

每个前端人烂熟于心的公式:

JavaScript = ECMAScript + BOM + DOM

ES2015

  • 2015年开始保持每年一个版本的迭代,并且开始按照年份命名。
  • 相比于ES5.1的变化比较大
  • 自此,标准命名规则发生变化
  • ES6泛指是2015年之后的所有新标准,特指2015年的ES版本,以后要看清楚是特指还是泛指

新特性的分类

  • 解决原有语法上的一些问题或者不足
  • 对原有语法进行增强
  • 全新的对象、全新的方法、全新的功能
  • 全新的数据类型和数据结构

ES6-ES10学习版图

ES6-ES10学习版图

基本语法链接整合

ES6

ES7

ES8

ES9

ES10

查看原文

赞 63 收藏 55 评论 2

Futurefinger 赞了文章 · 9月28日

我来聊聊前端应用表现层抽象

本文首发于欧雷流。由于我会时不时对文章进行补充、修正和润色,为了保证所看到的是最新版本,请阅读原文

我们处于变化很快的时代,无论是商业还是科技。一家公司看上去商业很成功,也许前脚刚上市,后脚就因为什么而退市,甚至倒闭;一项看似高大上的技术横空出世,各类媒体争先恐后地撰文介绍,热度炒得老高,没准没多久就出现了竞争者、替代者。

在这样的大环境下,传统的「web 前端开发」演变成了「泛客户端开发」,前端开发者从「配置工程师」被「逼」成了「软件工程师」。开发变得更复杂了,要处理的问题更多了,从业难度不知提升了多少倍——前端早就不再简单。

在众多必须要处理的问题中的一个,就是表现层运行环境的兼容问题,像跨浏览器和跨端、平台、技术栈。注意,这里说的是「表现层」而不是「视图层」。

「表现层」与「视图层」

「表现层」的英文是「presentation tier」或「presentation layer」,具体是哪个取决于是物理上还是逻辑上划分;而「视图层」的英文是「view」。「表现层」是「视图层」的超集,根据前端应用的架构设计,它们既可以不等又可以相等。

表现层

「表现层」这个词出自经典的三层架构(或多层架构),是其中一个分层。三层架构包括数据层、逻辑层和表现层,一般用在 C/S 架构中。

三层架构

为什么会在这篇讲前端开发的文章中提到它?这是因为,虽然在一些前端应用中用不到,尤其是快餐式应用,但在企业级复杂前端应用中就十分需要一个前端的「三层架构」。

视图层

「视图层」则来自表现层常用的「model-view-whatever」模式中的「view」,即「视图」。至于说的时候在「视图」后面加个「层」字合不合适,就不在这里讨论了,文中皆使用「视图层」这个词。

运行环境兼容

跨浏览器

由于各浏览器厂商对标准实现的不一致以及浏览器的版本等原因,会导致特性支持不同、界面显示 bug 等问题的出现。但庆幸的是,他们基本是按照标准来的,所以在开发时源码的语法几乎没什么不同。

所谓的「跨浏览器」实际上就是利用浏览器额外的私有特性和技术或辅以 JS 对浏览器的 bug 进行「修正」与功能支持。

跨端、平台、技术栈

现在,绝大部分的前端开发者是在做泛客户端开发——开发 web 应用、客户端应用和各类小程序。

在做 web 应用时需要考虑 PC 端和移动端是分开还是适配?技术选型是用 React、Vue?还是用 Web Components?或是用其他的?做客户端应用、各类小程序时这些也会面临技术选型的问题。

如果公司某个业务的功能覆盖了上述所有场景,该如何去支撑?与跨浏览器不同的是,不同端、平台、技术栈的源码语法不一样,要满足业务需求就得各开发一遍。然而,这显然成本过高,并且风险也有些大。

那么,要怎么解决这个问题呢?从源头出发。根本的源头是业务场景,然后是产品设计,但这些都不是开发人员可掌控的,几乎无法改变。能够完全被开发人员所左右的基本只有开发阶段的事情,那就从这个阶段的源头入手——源码编写。

若是与业务相关的代码只需编写一次就能运行在不同的端、平台、技术栈上,那真是太棒了!这将会大大地降低成本并减少风险!

表现层的抽象

为了达到跨端、平台、技术栈的目的,需要将表现层再划分为抽象层、运行层和适配层。其中,抽象层是为了统一源码的编写方式,可以是 DSL、配置等,它是一种协议或约定;运行层就是需要被「跨」的端、平台、技术栈;适配层则是将抽象层的产物转换为运行层正常运行所需要的形式。

表现层中可以被抽象的大概有视图结构、组件外观、组件行为等。

视图结构

在 web 前端开发中,HTML 就是一种视图结构的抽象,描述了界面中都有什么,以及它们之间的层级关系。最终的显示需要浏览器解析 HTML 后调用操作系统的 GUI 工具库。

对于业务支撑来说,无论是 HTML 还是其他什么拼凑界面的方式,相对来说比较低级(是「low level」而不是「low」),视图单元的划分粒度比较细,在开发界面时就会花费更多的时间。

我们需要一种能够屏蔽一些不必关注的细节的视图结构抽象,在这个抽象中,每个视图单元都有着其在业务上的意义,而不是有没有都可以的角色。具体做法请看下文。

组件外观

大部分已存在的组件的视觉呈现是固定的,即某个组件的尺寸、形状、颜色、字体等无法被定制。如果同样的交互只是因为视觉上有所差异就要重新写组件,或者在组件外部重新写份样式进行覆盖,那未免也太痛苦了……

我们可以将那些希望能够被定制的视觉呈现抽象成「主题」的一部分,这部分可以被叫做「皮肤」。在进行定制时,分为线下和线上两种方式。

「线下」是指在应用部署前的开发阶段进行处理。在前端构建工具丰富的现在,写页面样式时已经不会去直接写 CSS,而是像 Sass 这种可编程式的预处理器。这样就可以抽取出一些控制视觉呈现的 Sass 变量,需要定制时通过在外部对变量赋值进行覆盖,而不需要费劲重写组件或样式。

「线上」则是部署后根据运行时数据动态改变。在皮肤定制即时预览和低代码平台等场景,是基本没机会去修改 Sass 变量并走一遍构建流程的,即使技术上能够办到。借助 CSS 自定义属性(CSS 变量)的力量可以较为方便地做到视觉呈现的运行时变更。

组件行为

组件除了外观,其行为也应当是可以定制的。看到「行为」这个词,第一反应就是跟用户操作相关的事情,然而这里还包括与组件内部结构相关的。

对于组件的外部来说,组件内部就是个黑盒子,其自身结构的组成部分有的可以被上文所说的视图结构所控制,有的则无能为力:

搜索组件

上图是一个比较复杂的搜索组件,虽然外观和布局看起来有所不同,但「它们」确实是同一个组件。外观不同的解决方案上面已经大体说明,这类视图结构无法控制的布局问题,需要枚举场景后在组件内进行支持,然后作为「主题」的一部分存在。

跟用户操作相关的行为有组件自身的交互规则及与业务逻辑的结合这两类。

交互规则又有两种:一种是像表单是在字段值发生改变时就校验还是在点击按钮时校验这样;另一种是像字段值是在输入框的值改变(input 事件)时更新还是失焦(change 事件)时更新这样,或是像下拉菜单的弹出层是在悬停(hover 事件)时出现还是点击(click 事件)时出现这样。

前者的解决方式与上面说的视图结构无法控制的布局问题差不多,后者则是需要组件支持事件映射,即外部可以指定组件某些交互的触发事件。当然,这两者同样也可以作为「主题」的一部分。

我们在写组件时有件事是需要极力避免却往往难以避免——组件中耦合业务逻辑。组件决定的应该只是外貌与交互形态,里面只有交互逻辑及控制展现的状态,不应该牵扯到任何具体业务相关的逻辑。只要长得一样、操作一样,那么就应该是同一个组件,具体业务相关的逻辑注入进去。

这段十分「个性化」的业务逻辑,说白了就是响应用户操作的变化以及业务数据的变化去更改组件内部的状态:

{
  // 组件事件
  events: {
    // 组件的一个点击事件
    'click-a': function() {},
    // 组件的另一个点击事件
    'click-b': function() {},
    // 组件的一个改变事件
    'change-c': function() {},
  },
  // 业务数据变化的回调
  watch: function( contextValue ) {},
}

运行时会注入一个上下文给上述对象方法的 this,组件还可以添加工具方法给上下文。该上下文的内置属性与方法有:

interface IDomainSpecificComponentContext {
  getState(key: string): any;
  setState(key: string, value: any): void;
  setState(stateMap: { [key: string]: any }): void;
}

视图结构描述

上面说了我们需要一种比 HTML 之类的更进一步的视图结构抽象,下面就来说说这部分的大体思路。

技术选型

在做视图结构抽象时最常用到的技术就是 XML-based 或 XML-like 以及 JSON-based 的某种技术。XML-base 和 XML-like 的技术都是符合 XML 语法的,唯一的区别是前者要完全符合 XML 的标准规范,像 Angular 和 Vue 的模板就是后者;同样的,JSON-based 的技术是完全符合 JSON 的标准规范的技术,像 JSON Schema。

自从 React 问世以来,其带来的 XML-like 的 JSX 也会被用于视图结构抽象,但基本仅限于编辑时(edit time)。一段 JSX 代码并不是纯声明式的,作为视图结构描述来说可读性较低,解析难度较高,并且通用性很低。

JSON-based 的技术对前端运行时最为友好,解析成本几乎为零;相反的,其可读性很低,JSON 结构是纵向增长的,指定区域内的表达力十分受限,无法很直观地看出层级关系与视图单元的属性:

{
  "tag": "view",
  "attrs": {
    "widget": "form"
  },
  "children": [{
    "tag": "group",
    "attrs": {
      "title": "基本信息",
      "widget": "fieldset"
    },
    "children": [{
      "tag": "field",
      "attrs": {
        "name": "name",
        "label": "姓名",
        "widget": "input"
      }
    }, {
      "tag": "field",
      "attrs": {
        "name": "gender",
        "label": "性别",
        "widget": "radio"
      }
    }, {
      "tag": "field",
      "attrs": {
        "name": "age",
        "label": "年龄",
        "widget": "number"
      }
    }, {
      "tag": "field",
      "attrs": {
        "name": "birthday",
        "label": "生日",
        "widget": "date-picker"
      }
    }]
  }, {
    "tag": "group",
    "attrs": {
      "title": "宠物",
      "widget": "fieldset"
    },
    "children": [{
      "tag": "field",
      "attrs": {
        "name": "dogs",
        "label": "🐶",
        "widget": "select"
      }
    }, {
      "tag": "field",
      "attrs": {
        "name": "cats",
        "label": "🐱",
        "widget": "select"
      }
    }]
  }]
}

如果一个应用的设计是不需要人工写视图结构描述的话,可以考虑使用 JSON-based 的技术。

像 Angular 和 Vue 的模板那种 XML-like 的技术是相对来说最适合做视图结构描述的——纯声明式,结构是向水平与垂直两个方向增长,无论是可读性还是表达力都更强,解析难度适中,并且具备通用性。

下面的模板代码所描述的内容与上面那段 JSON 代码一模一样,深呼吸,好好感受一下两者之间的差异:

<view widget="form">
  <group title="基本信息" widget="fieldset">
    <field name="name" label="姓名" widget="input" />
    <field name="gender" label="性别" widget="radio" />
    <field name="age" label="年龄" widget="number" />
    <field name="birthday" label="生日" widget="date-picker" />
  </group>
  <group title="宠物" widget="fieldset">
    <field name="dogs" label="🐶" widget="select" />
    <field name="cats" label="🐱" widget="select" />
  </group>
</view>

至此,视图结构描述最终该选用哪种技术,想必无须多言。

鸡哥(小鸡)

设计思路

毋庸置疑,模板的语法要符合 XML 语法是前提,再在此基础上根据需求进行定制、扩展。首先要定义标签集。所谓的「标签集」就是一个元素库,其中的每个元素都要具备一定语义,使其在业务上有存在意义。然后是制定描述元素的 schema 并实现其对应的解析、校验等逻辑。

元素 schema 大概是长这样:

// 属性值类型
type PropType = 'boolean' | 'number' | 'string' | 'regexp' | 'json';

// 属性描述符
type PropDescriptor = {
  type: PropType | PropType[];
  required: boolean; // 是否必需
};

// 元素 schema
type ElementSchema = {
  name: string; // 元素名
  tag?: string; // 标签名,不指定时取元素名
  props?: {
    [key: string]: PropDescriptor;
  };
  attrs?: {
    resolve: (key: string, val: any) => any;
  };
  // 节点行为,是作为父节点的子节点还是属性存在
  behavior?: {
    type: 'append' | 'attach';
    // 以下都用于 `type` 是 `'attach'` 时
    host?: string; // 宿主(属性名)
    keyed?: boolean; // 是否为键值对集合,值为 `true` 且 `merge` 为 `false` 时以节点 ID 为键
    merge?: boolean; // 当值为 `true` 时将 `reduce` 的返回值与 `host` 指定的属性的值进行合并后重新赋值给 `host`
    reduce?: (node: ITemplateNode) => any; // 转换节点信息
    restore?: (reduced: any, node?: ITemplateNode) => ITemplateNode | Partial<ITemplateNode>;
  };
};

可以看到 schema 中有 propsattrs,它们共同组成了模板元素的属性(XML attributes),区别是:模板解析后的属性如果是在 props 中定义的并且满足属性描述符的 typerequired 所指定的限制条件,会成为模板节点的 props 属性;剩余没在 props 中定义的则成为模板节点的 attrs 属性,通过 resolve 方法能够对属性根据自己的规则进行值的转换。

虽然在模板中元素总是以嵌套的形式展示出层级关系,但一个元素并不一定就是其父级的结构,还可能是配置。因此,元素 schema 中的 behavior 用于设置当前元素在模板解析后是作为一个节点的子节点存在还是作为某个属性存在。

上述的模板设计是纯视图结构描述的,并且只对元素这种「块」进行处理,我认为这样够用了。根据情况,可以扩展为像 Angular 和 Vue 的模板那样支持文本、插值和指令等。

如果懒癌发作并且没什么特殊需求,模板解析的工作可以交给魔改后的 Vue 2.6 编译器,再适配为模板节点树。

每个模板节点的结构大致为:

interface ITemplateNode {
  id: string;
  name: string;
  tag: string;
  props: {
    [key: string]: any;
  };
  attrs: {
    [key: string]: any;
  };
  parent: ITemplateNode | null;
  children: ITemplateNode[];
}

最后,通过适配层将模板节点树转为运行层的组件树,并把渲染的控制权也转交给了最终的运行环境。

总结

在一个复杂的前端应用中,如果不对其进行分层,那它的扩展性和可维护性等真的会不忍直视……通常是采用经典的三层架构,从下到上分别为数据层、逻辑层和表现层。本文以表现层为例,将其再次划分出抽象层、运行层和适配层这三层,实际上数据层和逻辑层也可以套用这种模式——就像在生日蛋糕上切上四刀——我称其为「九宫格」模型。

「九宫格」模型

在表现层的各种抽象中,本文着重阐述了视图结构描述的技术选型与设计思路,可以看出 XML-like 的模板从编写到解析再到渲染这一整条流程,与 Angular 和 Vue 的模板及 HTML 大体上一致;其他抽象只是稍微提了提,以后有机会再展开来说。

之前也写过几篇与模板相关的文章:从提效角度与「面向组件」做对比的《我来聊聊面向模板的前端开发》;从可定制性角度讲的《我来聊聊配置驱动的视图开发》;从低代码平台的核心理念「模型驱动」出发的《我来聊聊模型驱动的前端开发》。可以说,本文的内容是它们有关表现层描述的「根基」。

无论一家公司是不是做低代码平台的,或者内部有没有低代码平台,都应该从表现层抽象出视图结构描述,至少要有如此意识。


欢迎关注微信公众号【Coding as Hobby】(微信中搜「coding-as-hobby」)以及时阅读最新的技术文章~ ;-)

查看原文

赞 13 收藏 9 评论 0

Futurefinger 赞了文章 · 2月4日

如何进行远程协作办公?

新冠疫情,随着春节的脚步一下就席卷了大江南北。这个春节,相信每一家,每一人都过得很不平静。作为大众而言,生活在继续,工作在继续,我们给武汉加油,给政府打气。面对疫情,最好的方式就是不给国家和他人添乱,在家好好整顿,积蓄疫情之后的力量。

很快就到了春节后重新开工的时间,严峻的疫情形势会让很多公司开始考虑到平衡员工健康和业务进展,其中一个必然的选项是远程办公。这虽是无奈之选,但也可能给未来的企业组织形态开启一个新认知,那就是其实已经有越来越多的企业、团队和组织在实践远程工作了。

一开始的远程办公,都是从跨区域组织开始的,因为团队的人分隔在北京、深圳和纽约,所以跨区域团队从一开始就需要面对和解决不在一起工作的问题。这个和因为疫情,大家被迫分割在不同的家里,本质上是一回事。以我们自己为例, Worktile 团队3年前成立上海和深圳团队,我们部分研发同事分散在杭州和湖北,由此在远程办公这件事上,也摸爬滚打了3年的时间。本文就是在当下这个形式下,将我们自己的远程办公经验总结为可以服务大家的远程办公指南,希望对即将复工的企业有些许的帮助和启示。

远程办公是每个企业的硬核能力

远程办公是每个企业的硬核能力,在非常时期更是。当疫情来袭,公司管理者在考量员工健康和业务进展的平衡中,需要尽早考虑远程办公的组织与管理方式。Worktile 本身有36个月的远程办公经验,所以我们HR小姐姐第一时间发布了未来4周的远程办公安排,从下周开始我们分散多地的同事将各自在家打造产品和业务了。

QQ20200128-205142@2x.png

(HR小姐姐发起的远程工作公告)

不夸张的说,未来的企业要在组织管理上,考虑远程办公的能力,就像解放军随时随地支起帐篷就能参加战斗一样,战时企业的运行要从演习变成真正的硬核能力才行。总体来说,远程办公有以下的价值和可能的问题,需要视情况考虑。

明显的价值
  • 很好解决了跨部门团队的现实场景。就像我们自己将北京、上海、深圳、西安、杭州、岳阳的同事连接在一起一样,远程工作是跨区域团队必须的选择。
  • 更好招聘合适的人。在 Worktile 团队,我们有好几位同事平常工作在北上广深之外的其他地方,远程办公将公司的人才范围扩展了N倍,因此公司可以更好的去选择合适的人才。对个体而言,这样的方式同样带来便利,很多时候平衡了家庭和房价。
  • 减少路上的浪费。北上广深杭这样的城市,每天通勤需要3个小时的人,不在少数,远程办公将极大解决这种时间和精力的浪费。
  • 说不定省了办公室的电费。少一个工位,企业一年减少2万左右的办公开销是算的过来的,那么这个价值就有点意义了。
  • 解决战时所需。疫情就是战场,企业必须生存,员工健康同样重要,如何以不在一起工作的方式,实现在一起工作的价值,让业务顺利开展,远程工作是唯一选择。
有可能存在的问题
  • 估计不适合巨大规模的团队。客观的说,远程办公并不十分适合巨大规模的团队,公司组织架构上,要考虑将团队以独立的单元切分,形成一个个可以接口化运行的小团队,这样形成的小团队就恰恰是远程工作的最佳配置。例如下面的特种部队配置:
  • 运行不好,同样带来效率的低下。远程办公,要求信息的透明度、工具化、流程化程度较高,对人本身的工作习惯要求也有,运行不恰当,同样带来信息过度分散,不能及时响应的弊端,反而影响效率和业务进展。
  • 不是所有的职能和行业都适合。

基本的远程办公原则

远程办公这件事,本身并不复杂,我们自己总结了以下几项小原则,能够对企业适配远程办公提供一定的认知支撑。

  • 远程办公,不适合大规模的团队和组织,所以有效的大公司小团队,实现简单扁平的团队层级,将组织切分成很多独立工作的特种部队单元,是实现远程办公的最重要原则。亚马逊有典型的2个披萨理论,而微软强调3111原则,我在《一百人研发团队管理的难题》这篇文章里对团队特种部队式的架构有过论述,这里不做详细展开。

dee2965c4f8a49d0c03d2d650717ea39.PNG

(一个研发团队的典型特种部队架构设计)

  • 自动化一切能自动化的。其实,工作场景中很多事情已经完全可以自动化完成了,例如我们研发团队基于用户故事将代码、bug、测试用例连接在一起,测试工作完成了,bug会自动同步给开发者。这种小自动化设计在工作场景有很多,可以极大减少不必要沟通,同时最大化透明工作信息。可以自动化的场景,还有代码提交之后的流水线,基于IM设计的各种机器人操作,在日报自动汇总每个人当日的完成工作。

IMG_0232.JPG

(Worktile Agile的用户故事)

  • 流程化一切能流程化的。流程化的价值是让组织的每个人按照统一的规则做事,因此流程化是减少“偏见”的利器,流程并不是企业管理的毒药,好的规则和流程,能给团队减少冲突,极大实现企业员工之间的契约。例如,用户端反馈的一个小需求,如何在不同的团队之间传递,并最终被产品采纳,然后上线到新的功能中,这个小小的事情就是一个公司内部运营的小流程。

ffa51f53-5b20-4cd8-a9e7-d9bb9a25f07d.png

(简化的用户需求反馈流程设计)

这种类似的流程化,还有很多很多可以被设计到公司的运营中,我将其称为企业运营的修水渠理论,可以详见我另一篇的分享《为什么每家公司都需要协作工具?

  • 定义统一且清晰的阶段性目标。不在一起工作,最有效的驱动方式是基于目标保持同频,最佳的工具是OKR。
  • 必要的仪式感。按规定的上下班时间,虽然不在一个物理办公室,但不同家里的时间是可以相对统一的;早上和下班前的例会;统一的业务数据同步;提交必要的日报,都是在远程办公中以仪式感的方式,让大家产生“我在工作”这样的同感。
  • 信息透明。实质上,通过 Worktile、Github 这样的协作工具,一定程度上解决了很大的信息透明问题。你的任务活动留存在 Worktile,你的代码提交留存到 Github,给团队之间足够的透明度,会极大避免面对面交流的必要性。
  • 解决孤独感。长期远程办公的个体是肯定会产生孤独感的,必要的仪式,必要的团建,周期性的聚会,仍然无法被代替。
  • 尽可能使用SaaS工具,能够用工具解决的,绝不依赖于人。越来越多不同业务的SaaS产品,为企业运营提供了非常多的自动化、数字化工具,从HR、财务、税务、报销、办公、文档、敏捷管理、代码库、电子签章、H5工具、问卷工具,应有尽有。当然,Worktile 也是其中必备的工具选项。

准备好的工具集

有了基本原则的共识,接下来要解决远程团队的工具了,不同公司在选择工具上肯定有非常大的差异,简单、易用、专业、价格、安全,应该是你考虑工具的首选,下面是我们团队在使用的各种工具集合:

  • 远程会议服务。我们团队使用的云视频服务主要是Zoom和亿联云视讯,Zoom相当简单易用,亿联云视讯有视频语音设备支持,每天的远程视频例会,白板分享演示,一应俱全。

image.png

(Zoom视频会议)

  • 代码协同。Github和Gitlab,是代码协同的领导者,我们同时利用Worktile Pipe将代码和研发用户故事链接起来,实现了很好的从代码到需求的整合。

image.png

  • 敏捷开发和看板。Worktile Agile和Worktile Testhub是我们针对研发场景产品线的核心子产品,分别针对研发流程和测试流程管理。上线以来受到非常多用户的喜爱,在用户体验和对敏捷的标准化、专业化支持上,收获了很多客户的芳心。这两个子产品同时也是我们研发团队每个进行敏捷迭代、计划会议、回顾会议、需求和缺陷规划的工具。

title-content-bg.png

(标准化、专业化的Scrum敏捷管理)

  • 提交日报。远程办公而言,对于管理者,可以考虑日报方式实现每天自下而上的信息传达,针对远程工作场景还是很有必要的。Worktile Teams协作产品线提供了可以自定义模板的简报支持,适合团队的日报需要。

QQ20200128-223746@2x.png

(一位销售经理的日报)

  • 文档协作。目前功能文档协作有N多非常好的工具选项,石墨文档、腾讯文档、WPS,我们自己使用最多的是腾讯文档和石墨文档。

image.png

(腾讯文档)

image.png

(石墨文档)

  • 共享日历。Calender是远程团队必备的工具集,发起会议、设定里程碑、制定拜访提醒,都是日程可以解决的事情。对团队而言,对Calender还有一项在团队共享时间和资源(例如会议室、投影仪)的要求,我们团队的日程应用的也是Worktile Teams协作产品中的日程模块。
  • 制定统一的OKR目标。Worktile Teams协作产品中的OKR模块,是国内第一家落地OKR的协作子应用,所以我们自己的目标管理,也是基于我们自己的OKR子应用落地的。
  • 对外营销工具。易企秀是这个领域的王者,市场团队基本上依赖于易企秀实现很多市场营销工具的制作和分发。
  • 问卷调查。例如统计大家此次春节的出行情况,方便HR做节后的行政工作安排,我们喜欢麦客表单这样有调性的产品。

image.png

(麦客表单)

  • Wiki系统。
  • Jenkins和K8S,CI和CD工具。
  • 共享网盘。

回到工具本身,还是要多说一句定理:重要的还是使用工具的人,而不是工具本身。

接下来是实施与落地远程办公

有了必要的远程办公原则,加上丰富而简单的远程办公工具集支持,是时候在团队里实施和落地远程办公这件事了。

一、定目标和计划

本身目标这件事无关乎是否远程,任何企业和团队都应该在定义目标上花费足够的心力和时间,然后驱动团队朝着目标前进。不过,对于远程办公来说,统一目标和进展,可能更加重要,将不同地方的同事统一到一个框架下,然后大家朝着一致的方向走,这是OKR的精髓,也是远程办公的必备方法论。

6d22eea8c13d29366cd5315f330cac40.JPG

(以月为单元的目标树,在全员透明同步)

二、将工作落实到项目和任务

无论是一个部门或者小组内部,还是部门与部门之间、小组与小组之间的跨团队协作,基于项目和任务让远程工作有了统一的计划载体、资源载体、时间载体和沟通载体。基于项目和看板,能够及时将不同时空工作的人们连接起来,基于事务的资源系统、沟通、成本核算、冲突解决,都可以基于任务和项目来展开,对管理者而言在看板上也可以一目了然的知道总体进展。

image.png

(项目和任务)

三、基于敏捷的研发流程管理

对于研发团队而言,开发者、产品经理、设计师、测试、客户成功、销售、市场,将不同的人以及不同地方的人组织起来,完成一个软件产品、App或者游戏的开发,是一件复杂的事情,我们分散的研发团队,整体是基于Scrum敏捷开发原则组织的,具体如下:

1、用户将产品想法反馈到【用户声音】或者告知客户成功经理。

image.png

(用户声音的产品界面)

2、客户成功经理,将客户需求、bug以一定的规范提交需求池项目,并按照一定的规则定义。

image.png

(需求池管理)

3、产品经理定期将来自于用户声音、客户经理和产品Roadmap,形成迭代计划,并组织一月一次的产品沟通会,产品沟通会将集中讨论近期迭代的大需求和大方向。

image.png

(迭代规划)

4、产品经理将需求复制到研发项目,并按照Epic、Feature和User Story做三级需求规划。

image.png

(需求三层分级管理)

5、Scrum Master组织Scrum计划会议,产品经理按照优先级自高至低对需求进行讲解,Scrum团队全体讨论并评估Story Point。依照团队速率表的统计数据决定当前Sprint能够完成的需求量。

image.png

(版本规划)

6、Scrum Master组织开发团队的Daily Scrum,团队成员通过看板上的信息同步自己的开发进度、计划和困难。

7、一个迭代结束,Scrum Master组织Sprint Review和回顾会议,检查当前迭代完成的成果,决定User Story是否成功完成并结束迭代,并在回顾会议输出回顾会议纪要。

image.png

(迭代回顾)

8、一个版本结束,产品经理根据对应迭代所完成的User Story和Bug生成版本日志,同步到Changelog和用户公告,设置用户心声的用户需求完成。

image.png

(版本更新公告)

9、产品经理组织产品同步会,将新的产品同步到业务团队,并收集新的需求和业务反馈。

正如之前提到的,在分布式团队和远程工作的要求下,SaaS版本的敏捷开发工具较之传统的白板、Excel和面对面会议都提供了更好的支持。譬如:

  • 团队成员的沟通和交流可以完全通过敏捷任务的状态变化、评论以及对评论的回应实时沟通,完全消除了异地协同的距离感。
  • 通过视频会议工具、Worktile Teams 的即时通讯模块和 Agile 内置的看板进行Daily Scrum,Sprint Review以及Retro会议。对于临时性的会议也完全通过在线会议实现,通过共享用户故事和任务,将会议的讨论内容通过评论进行记录,最后汇总在网盘或者Wiki工具中。
  • 对于研发和运维的协作问题,由于我们的运维本身就是一个分布式团队,因此在此前就已经通过Teams的项目模块搭建了一个运维支持平台。研发团队可以通过这个平台提交线上部署申请、日志搜索和数据的查询。
  • 通过Jenkins、Kubernetes等工具,我们将部署工作完全自动化,并且通过API将数据返回给Worktile,让所有开发人员以及产品经理能够了解目前各个环境的运行状况,甚至具体到某个需求的上线情况,而无需和对应的研发人员单独沟通。
四、建立畅通无阻的流程

就像前文阐述的意义,流程就是修水渠,就是给组织建立规矩的方式方法。面向远程工作,需要在跨区域团队之间建立简单而有效的流程,然后将流程同步到每个组织成员中,大家按照流程规则去协作,能够极大解决远程工作带来的沟通成本。

例如产品经理发布产品的流程,需要工程师做Alpha、Beta、RC和Production四级评审,需要测试输出测试报告,需要产品经理发布版本日志和博客说明,需要客户成功共同到关注新功能的客户,需要产品经理同步给销售了解,需要市场更新销售工具资料和输出新功能的PR,需要数据团队关注新版本的用户反馈,需要在wiki沉淀新版本的产品计划和相关内容资料,需要以项目核算新功能的ROI。所有这些需要,本质上集中于一个流程,流程链条相关各方并不需要过度的沟通,一切就像修好的水渠,自然流动到相关的环节,环节上的执行者也可以自然而然的执行。这就是流程的价值。

相同的流程还有非常多,值得在团队里去梳理和达成共识。而且,流程需要工具的支持,才能事半功倍。研发有产品发布流程、用户需求收集流程、灰度、CI/CD;市场有新品PR、活动组织、Webinar、用户广播;销售有合同流程、新客户拓展、与CS交接、市场发现、会销;其他HR、财务、管理层的各种场景,在企业里非常重要。

五、开会和例会

远程办公有一项很重要的事情就是:开会。开会这件事,对管理不成熟的团队来说,并不是一件简单的事情。会议需要明确的主题和发起者,会议需要确定的时间,会议需要落实为Todos和纪要,会议结束需要明确的结论。

对远程办公而言,我们非常关注例会的重要性,早上启动会和晚上总结会,是一个很好的方式将不同地方工作的小伙伴以仪式感的形式拉到一起行动。

六、日报

例会之后,我们同时要求每个人提交简单明确的当天日报,日报的价值在于管理,一方面团队负责人可以很好收集远程办公成员的情况,帮忙解决他遇到的问题,另一方面,日报是实现信息共享的很有效方式,某个人遇到的问题可以在日报中被其他人了解,并立即响应解决。

为不同类型工作性质的人,定义不同的日报模板,收集不同场景的信息是必要的。同时,日报本身也自然形成了一个巨大的知识库。

image.png

(研发周报模板)

image.png

(市场日报模板)

七、仪式感

远程办公,需要必要的仪式感,工作成员突破了物理的束缚,就像放飞的小鸟,需要一个无形的笼子让鸟儿有群居的感受。这就是仪式感想要达成的目标,例如:

  • 统一上下班时间
  • 统一例会
  • 统一日报
  • 周期性迭代目标和同步
  • 全员公告进度
  • 管理层统一
八、管理层的中心化设计

远程办公对管理本身是有挑战的,因此通过工具和在线连接,将团队以去中心化的方式分隔到不同的办公室、家或者咖啡店。因此,很有必要将管理层以中心化的方式连接起来,从而让不同的部门、业务是紧密配合与信息互通的。所以,我们同时对不同业务负责人成立虚拟化的管理委员会,将远程办公的不同人群,通过管理委员会连接起来,具体包括:

  • 每周为频度的例会,同步信息和目标
  • 不同业务之间的直连,让业务负责人成为跨团队的连接器
  • 以OKR为载体组织目标和计划
  • 组织技术委员会、营收委员会和战略委员会,集中力量办大事,打破远程和团队界限

面向疫情,我们能做点力所能及的

以上的远程办公实践中,很多场景是基于我们自己产品实现的,非常时期 Worktile 团队也希望做点力所能及的事情。除了我们长期支持的公益计划之外,在疫情期间,我们同时发起了以下特别疫情公益计划:

即日起至2020年5月1日,政府机构/医院/教育机构/公益组织,以及湖北地区企业,可免费使用Worktile团队协作产品的付费版所有功能,我们也会密切关注疫情态势并调整免费期限。

image.png

Worktile 官网:worktile.com
本文作者:Worktile CEO Anytao
文章首发于「 Worktile官方博客」,转载请注明出处。

查看原文

赞 8 收藏 4 评论 0

Futurefinger 提出了问题 · 1月8日

Vue.extend创建的组件propsData上接收的值改变了但是视图没有更新!

parent.vue
import videoView from "../videoView"


const comp_videoView = Vue.extend(videoView);
        console.log("instance===index===",index);
        const instance_videoView = (new comp_videoView({
          propsData:{
            // index:index,
            uid:id,
            userType:userType,
            userName:userName,
            userRole:userRole
          },
          data(){
            return $.dataObj
          }
        })).$mount();
        instance_videoView.index = index;
        // $.$set(instance_videoView,"index",index);
        console.log("nodetype:",instance_videoView,typeof index)
        document.getElementById("ag-canvas").appendChild(instance_videoView.$el);
        
        
videoView.vue
<template>
  <figure :id="`ag-item-${uid}`" class="ag-item" :ref="`ag_item_${uid}`">
    <div>component:uid:{{uid}}</div>
    <!-- <div
      :id="`player_${uid}`"
      style="width: 100%; height: 100%; position: relative; background-color: black; overflow: hidden;"
    >
      <video
        :id="`video${uid}`"
        style="width: 100%; height: 100%; position: absolute; object-fit: cover;"
        autoplay
        playsinline
      ></video>
      <audio :id="`audio${uid}`" autoplay playsinline></audio>
    </div>-->
    <div class="tool_panel_box" v-if="userType !== 1 && num === -1">
      <div class="top_bar">
        <div class="icofont_live ico_horn fr stop">
          <div class="box run" style="display:none;">
            <div class="wifi-symbol">
              <div class="wifi-circle first"></div>
              <div class="wifi-circle second"></div>
              <div class="wifi-circle third"></div>
            </div>
          </div>
          <i class="icofont_live zcwicoicon--"></i>
        </div>
        <i class="icofont_live zcwicoquanping fr" @click="handleSwitchWin"></i>
      </div>
      <div class="bottom_bar">
        <span>{{userRole}}:</span>
        <span>{{userName}}</span>
      </div>
    </div>
  </figure>
</template>

<script type='text/ecmascript-6'>
export default {
  props: {
    // index: {
    //   type: Number,
    //   required: true,
    //   default: -1
    // },
    uid: {
      type: Number,
      required: true
    },
    userRole:{
      type:String,
      required:true
    },
    userName:{
      type:String,
      required:true
    },
    userType:{
      type:Number,
      required:true,
      default:1
    },
    items: {
      type: Array,
      default: Array
    }
  },
  watch: {
    index(val){
      console.log("child--watch==index",val);
    }
    // items: {
    //   handler(newV, oldV) {
    //     if (newV.length !== 0) {
    //       // this.student = {...newV[this.index]}
    //       console.log("watch==items==newV", newV);
    //     }
    //   },
    //   deep: true
    // }
  },
  data() {
    return {
      // uid:undefined,
    };
  },
  methods: {
    initParams() {
      this.uid = this.items[this.index].getId();
    },
    videoWinPlay() {
      let $ = this;
      $.items[$.index].play(`ag-item-${$.items[$.index].getId()}`);
    },
    handleSwitchWin(){
      // 切换主屏
      console.log("切换主屏:",this.$refs[`ag_item_${this.uid}`]);
      console.log("样式====",this.$refs[`ag_item_${this.uid}`].getAttribute("style"))
      console.info("fq==",this.$parent,this.$root.$parent,this.userType);
      console.log("num==",this.num);
      this.$bus.$emit("ebs_test",{
        style:this.$refs[`ag_item_${this.uid}`].getAttribute("style"),
        uid:this.uid,

      });
      // this.userType = -1;

      // console.log("this.userType--xighai=",this.userType);
      console.log("change==num==",this.num);


    }
  },
  beforeUpdate() {
    console.log("子组件beforeUpdate", this.index);
    // let $ = this;
    // $.videoWinPlay();
  },
  created() {
    console.log("子组件created", this.index);
    // this.initParams();
  },
  mounted() {
    console.log("子组件mounted");
    // let $ = this;
    // $.videoWinPlay();
  }
};
</script>

实际场景:

parent.vue 组件里去修改extend创建组件里的userType值,这个值是发生改变了,但是videoView视图没有同步更新?

关注 2 回答 1

Futurefinger 收藏了文章 · 2019-10-24

Vue 动态创建 component

Vue 动态创建 component

angular 中 可以通过 ComponentFactoryResolver 来动态创建 component , 在平时使用 vue 的过程中也没有了解到这方面的信息。于是就花时间研究了一下。

Vue 的组件可以通过两种方式来声明,一种是通过 Vue.component,另外一种则是 Single File Components(SFC)

以下除非特别说明,组件都是全局注册的

Vue.component 方式

首先来看 Vue.component 方式的。

Vue.component('button-counter',{
   data: function () {
    return {
      count: 0
    }
  },
  template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
});

在上面的代码中我们声明了一个叫做 button-counter 的组件。如果在常规情况下使用的话,只需要在页面上写对应的 <button-counter></button-counter> 标签就够了。

那么通过编程方式怎么处理呢?

官方文档中我们可以看到,我们可以通过 Vue.component('component-name') 的方式来获取到 组件。而组件实例又有 $mount 这样一个方法,官方对于 $mount 的解释如下:

$mount 方法有两个参数

  • {Element | string} [elementOrSelector]
  • {boolean} [hydrating]

If a Vue instance didn’t receive the el option at instantiation, it will be in “unmounted” state, without an associated DOM element. vm.$mount() can be used to manually start the mounting of an unmounted Vue instance.

If elementOrSelector argument is not provided, the template will be rendered as an off-document element, and you will have to use native DOM API to insert it into the document yourself.

The method returns the instance itself so you can chain other instance methods after it.

那我们是否可以通过这种方式来达到我们的需求呢?

还不够!

为什么?

因为 Vue.component 返回的结果是一个 function!它返回的并不是 组件实例,而是一个构造函数。

那到这里其实我们就清楚了。 对于 Vue.component 声明的组件,我们先通过 Vue.component 获取它的构造函数,再 new 出一个组件实例,最后 通过$mount 挂载到 html 上。

下面是完整的代码:

Vue.component("button-counter", {
  data: function() {
    return {
      count: 0
    };
  },
  template:
    '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
});

Vue.component("app", {
  data: function() {
    return {
      count: 0
    };
  },
  template:
    '<div> <h1>App Component</h1><button @click="insert">click to insert new Component</button> <div id="appId"> </div></div>',
  methods: {
    insert() {
      const component = Vue.component("button-counter");
      const instance = new component();
      instance.$mount("#appId");
    }
  }
});

new Vue({
  el: "#app"
});

https://codepen.io/YoRolling/...

SFC

在实际工作中,大部分都是用官方的脚手架 vue-cli 生成项目,用的也是 SFC 这种方式。

我们的 button-counter 如果用 SFC 方式实现的话应该是这样子的:

<template>
    <button v-on:click="count++">You clicked me {{ count }} times.</button>
</template>

<script>
export default {
  name: "ButtonCounter",
  data() {
    return {
      count: 0
    };
  }
};
</script>

那么是否可以通过 export 出的对象来实现我们的需求呢?

首先我们来看,在 SFC 中, 我们在 script 中 export 了一个对象出去,那么通过这个对象应该是可以达到要求的。

先来看看 import 之后这个对象是什么样子的。

图片描述

可以发现 import 得到的对象要比我们在 组件中声明的多了一些属性和方法。

Vue.component 模式中,我们先获取到组件的构造函数,然后构造实例,通过实例的一些方法来处理数据和挂载节点。

很显然,现有数据不能满足我们。如果我们能将这个对象转化成一个组件的构造函数,那我们就可以利用上面的方案来实现我们的需求了。

那么,究竟需要怎么转换呢?

没错! 就是 Vue.extend 这个大兄 dei!我们看下官方的说明。

Create a “subclass” of the base Vue constructor. The argument should be an object containing component options.

The special case to note here is the data option - it must be a function when used with Vue.extend().

通过传入一个包含 Component options 的对象, Vue.extend 帮助我们创建一个 继承了 Vue constructor 的子类,也就是我们之前需要的构造函数。

好了,得到了构造函数,接下来的工作就简单了 。实例化,然后挂载。

下面就是完整的代码:

<template>
  <div id="app">
    <div>
    <img width="25%" data-original="./assets/logo.png">
  </div>
    <div>
    <button @click="insert">click me to insert ButtonCounter</button>
  </div>
    <div id="container"></div>
  </div>
</template>

<script>
import ButtonCounter from './components/ButtonCounter';
import Vue from 'vue';
export default {
  name: 'App',
  components: {
    ButtonCounter,
  },
  methods: {
    insert() {
      const bcConstructor = Vue.extend(ButtonCounter);
      const instance = new bcConstructor();
      instance.$mount('#container');
    },
  },
};
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>    

在线查看 https://codesandbox.io/s/m59r3547zy

https://codesandbox.io/s/m59r...

结束

Happy Ending。

到这里,通过编程的方式动态创建组件挂载到HTML 的两种方式就完成了。

再次感到看文档的重要性。 🤨

👻 如有错误,欢迎大家斧正!

查看原文

Futurefinger 收藏了文章 · 2019-10-23

支付宝小程序版 iView 组件库全新发布

iView alipay

一套高质量的支付宝小程序 UI 组件库

使用 Antmove 小程序转换器基于 iview-weapp 转换得到。

文档

大部分组件都保留了原有的用法,只有一两个做了少许的改动。支付宝版本文档近期推出。

快速上手

使用之前

在开始使用 iView alipay 之前,你需要先阅读 支付宝小程序自定义组件 的相关文档。

如何使用

下载 iView 的代码,将 dist 目录拷贝到自己的项目中。然后按照如下的方式使用组件,以 Button 为例,其它组件在对应的文档页查看:

  1. 添加需要的组件。在页面的 json 中配置(路径根据自己项目位置配置):
"usingComponents": {
    "i-button": "../../dist/button/index"
}
  1. 在 axml 中使用组件:
<i-button type="primary" onClick="handleClick">这是一个按钮</i-button>

IDE 预览

下载本项目到本地,使用支付宝 IDE 打开 iview-alipay 即可预览。

预览时请在项目详情设置中开启 component2 编译,详情可参考支付宝小程序自定义组件使用介绍

预览

  • 支付宝小程序

License

MIT

感谢

感谢 TalkingData 团队对 ivew-weapp 的贡献,本项目基于 iview-weapp 转换而来,并做了支付宝小程序平台的适配兼容。

查看原文

Futurefinger 赞了文章 · 2019-10-23

支付宝小程序版 iView 组件库全新发布

iView alipay

一套高质量的支付宝小程序 UI 组件库

使用 Antmove 小程序转换器基于 iview-weapp 转换得到。

文档

大部分组件都保留了原有的用法,只有一两个做了少许的改动。支付宝版本文档近期推出。

快速上手

使用之前

在开始使用 iView alipay 之前,你需要先阅读 支付宝小程序自定义组件 的相关文档。

如何使用

下载 iView 的代码,将 dist 目录拷贝到自己的项目中。然后按照如下的方式使用组件,以 Button 为例,其它组件在对应的文档页查看:

  1. 添加需要的组件。在页面的 json 中配置(路径根据自己项目位置配置):
"usingComponents": {
    "i-button": "../../dist/button/index"
}
  1. 在 axml 中使用组件:
<i-button type="primary" onClick="handleClick">这是一个按钮</i-button>

IDE 预览

下载本项目到本地,使用支付宝 IDE 打开 iview-alipay 即可预览。

预览时请在项目详情设置中开启 component2 编译,详情可参考支付宝小程序自定义组件使用介绍

预览

  • 支付宝小程序

License

MIT

感谢

感谢 TalkingData 团队对 ivew-weapp 的贡献,本项目基于 iview-weapp 转换而来,并做了支付宝小程序平台的适配兼容。

查看原文

赞 4 收藏 2 评论 0

Futurefinger 收藏了文章 · 2019-10-14

在vue中实现Monaco Editor自定义提示功能

这次接到一个需求,要在浏览器的IDE中支持自定义提示功能,如下所示:

clipboard.png

可以看到,它可以根据用户输入的内容来一项一项排除,只显示完全匹配的那一项。
项目的框架是Vue,编辑器用的是Monaco Editor

什么是Monaco Editor

vscode是我们经常在用的编辑器,它的前身是微软的一个叫Monaco Workbench的项目,而Monaco Editor就是从这个项目中成长出来的一个web编辑器,他们很大一部分的代码都是共用的,所以Monaco EditorVSCode在编辑代码,交互及UI上几乎是一摸一样的。不同的是,两者的平台不一样,Monaco Editor基于浏览器,而VSCode基于electron,所以功能上VSCode更加健全,性能比较强大。

用法

  1. 安装

    npm install monaco-editor --save
  2. 使用

    <div id="monaco" class="monaco-editor"></div>
    import * as monaco from 'monaco-editor';
    this.fileEditor = this.monaco.editor.create(document.getElementById('monaco'), {
      value: null,
      language: 'sql'  // 这里以sql为例
    })
    
    this.fileEditor.dispose(); // 使用完后销毁

    这里引入monaco要注意,在react中以下面方式引入:

    import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';

实现自定义提示功能

查看了资料后,发现在monaco中有提供一个提示功能的方法registerCompletionItemProvider,具体实现如下:

this.monaco.languages.registerCompletionItemProvider('sql', { // 这里以sql语言为例
  provideCompletionItems () {
    return [{
      label: '${_DB',  // 显示的提示内容
      kind: this.monaco.languages.CompletionItemKind['Function'], // 用来显示提示内容后的不同的图标
      insertText: '{_DB', // 选择后粘贴到编辑器中的文字
      detail: '' // 提示内容后的说明
    }];
  },
  triggerCharacters: ['$'] // 触发提示的字符,可以写多个
});

以上的用法,我试了一下之后发现,虽然triggerCharacters的值是数组,可以有多个,但是里面的字符串只能识别一个字符。一开始的需求是输入${_之后提示${_DB,但是由于不能识别多个字符,只能做到出现$就提示。

还有一个问题就是registerCompletionItemProvider的第一个参数只能是字符串,如果有多种语言只能叠加重复写,恰巧我的需求是有多种语言,所以只能如下解决,也就是每种语言都写了一遍:

['json', 'yaml', 'php', 'go', 'sql', 'java', 'markdown', 'plaintext'].map(item => {
  this.monaco.languages.registerCompletionItemProvider(item, {
    provideCompletionItems () {
      return [{
        label: '${_DB',
        kind: this.monaco.languages.CompletionItemKind['Function'],
        insertText: '{_DB',
        detail: ''
      }];
    },
    triggerCharacters: ['$']
  });
});

需求是${_DB:key:value,也就是说在输入${_DB后,再输入一个:提示出key,在key之后输入:提示value

这里又碰到一个问题,需要知道当前输入的内容来判断是$还是:,而且后面两个触发提示的符号同是:,无法区分,只能通过识别:的位置来判断是提示key还是value,所以还要知道当前输入的:之前的内容。

那么只有在provideCompletionItems这一步判断,但是查遍了资料没有发现这样的参数,provideCompletionItems只有modelpositiontoken这几个参数,后来发现model中的getLineContent方法可以获取指定行的所有内容,而position可以获取当前输入行的行数和列数,于是就有了以下解决方法:

this.monaco.languages.registerCompletionItemProvider(item, {
  provideCompletionItems (model, position) {
    // 获取当前行数
    const line = position.lineNumber
    
    // 获取当前列数
    const column = position.column
    
    // 获取当前输入行的所有内容
    const content = model.getLineContent(line)
    
    // 通过下标来获取当前光标后一个内容,即为刚输入的内容
    const sym = content[column - 2]
    
    if (sym === '$') {
      return [{
        label: '${_DB',
        kind: this.monaco.languages.CompletionItemKind['Function'],
        insertText: '{_DB',
        detail: ''
      }];
    }
    
    return [{
      label: ':abb',
      kind: this.monaco.languages.CompletionItemKind['Function'],
      insertText: 'abb',
      detail: ''
    },
    {
      label: ':bc',
      kind: this.monaco.languages.CompletionItemKind['Function'],
      insertText: 'bc',
      detail: ''
    }];
  },
  triggerCharacters: ['$', ':']
});

能获取光标后的第一个内容,后面的内容就都能获取啦,如果识别到前面的内容是${_DB就提示key,否则提示value

最后总结下来就是一定要多看文档,勤于测试就能解决问题啦~

啦啦啦~ 交差去啦~

查看原文

Futurefinger 赞了文章 · 2019-10-14

在vue中实现Monaco Editor自定义提示功能

这次接到一个需求,要在浏览器的IDE中支持自定义提示功能,如下所示:

clipboard.png

可以看到,它可以根据用户输入的内容来一项一项排除,只显示完全匹配的那一项。
项目的框架是Vue,编辑器用的是Monaco Editor

什么是Monaco Editor

vscode是我们经常在用的编辑器,它的前身是微软的一个叫Monaco Workbench的项目,而Monaco Editor就是从这个项目中成长出来的一个web编辑器,他们很大一部分的代码都是共用的,所以Monaco EditorVSCode在编辑代码,交互及UI上几乎是一摸一样的。不同的是,两者的平台不一样,Monaco Editor基于浏览器,而VSCode基于electron,所以功能上VSCode更加健全,性能比较强大。

用法

  1. 安装

    npm install monaco-editor --save
  2. 使用

    <div id="monaco" class="monaco-editor"></div>
    import * as monaco from 'monaco-editor';
    this.fileEditor = this.monaco.editor.create(document.getElementById('monaco'), {
      value: null,
      language: 'sql'  // 这里以sql为例
    })
    
    this.fileEditor.dispose(); // 使用完后销毁

    这里引入monaco要注意,在react中以下面方式引入:

    import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';

实现自定义提示功能

查看了资料后,发现在monaco中有提供一个提示功能的方法registerCompletionItemProvider,具体实现如下:

this.monaco.languages.registerCompletionItemProvider('sql', { // 这里以sql语言为例
  provideCompletionItems () {
    return [{
      label: '${_DB',  // 显示的提示内容
      kind: this.monaco.languages.CompletionItemKind['Function'], // 用来显示提示内容后的不同的图标
      insertText: '{_DB', // 选择后粘贴到编辑器中的文字
      detail: '' // 提示内容后的说明
    }];
  },
  triggerCharacters: ['$'] // 触发提示的字符,可以写多个
});

以上的用法,我试了一下之后发现,虽然triggerCharacters的值是数组,可以有多个,但是里面的字符串只能识别一个字符。一开始的需求是输入${_之后提示${_DB,但是由于不能识别多个字符,只能做到出现$就提示。

还有一个问题就是registerCompletionItemProvider的第一个参数只能是字符串,如果有多种语言只能叠加重复写,恰巧我的需求是有多种语言,所以只能如下解决,也就是每种语言都写了一遍:

['json', 'yaml', 'php', 'go', 'sql', 'java', 'markdown', 'plaintext'].map(item => {
  this.monaco.languages.registerCompletionItemProvider(item, {
    provideCompletionItems () {
      return [{
        label: '${_DB',
        kind: this.monaco.languages.CompletionItemKind['Function'],
        insertText: '{_DB',
        detail: ''
      }];
    },
    triggerCharacters: ['$']
  });
});

需求是${_DB:key:value,也就是说在输入${_DB后,再输入一个:提示出key,在key之后输入:提示value

这里又碰到一个问题,需要知道当前输入的内容来判断是$还是:,而且后面两个触发提示的符号同是:,无法区分,只能通过识别:的位置来判断是提示key还是value,所以还要知道当前输入的:之前的内容。

那么只有在provideCompletionItems这一步判断,但是查遍了资料没有发现这样的参数,provideCompletionItems只有modelpositiontoken这几个参数,后来发现model中的getLineContent方法可以获取指定行的所有内容,而position可以获取当前输入行的行数和列数,于是就有了以下解决方法:

this.monaco.languages.registerCompletionItemProvider(item, {
  provideCompletionItems (model, position) {
    // 获取当前行数
    const line = position.lineNumber
    
    // 获取当前列数
    const column = position.column
    
    // 获取当前输入行的所有内容
    const content = model.getLineContent(line)
    
    // 通过下标来获取当前光标后一个内容,即为刚输入的内容
    const sym = content[column - 2]
    
    if (sym === '$') {
      return [{
        label: '${_DB',
        kind: this.monaco.languages.CompletionItemKind['Function'],
        insertText: '{_DB',
        detail: ''
      }];
    }
    
    return [{
      label: ':abb',
      kind: this.monaco.languages.CompletionItemKind['Function'],
      insertText: 'abb',
      detail: ''
    },
    {
      label: ':bc',
      kind: this.monaco.languages.CompletionItemKind['Function'],
      insertText: 'bc',
      detail: ''
    }];
  },
  triggerCharacters: ['$', ':']
});

能获取光标后的第一个内容,后面的内容就都能获取啦,如果识别到前面的内容是${_DB就提示key,否则提示value

最后总结下来就是一定要多看文档,勤于测试就能解决问题啦~

啦啦啦~ 交差去啦~

查看原文

赞 13 收藏 11 评论 10

认证与成就

  • 获得 26 次点赞
  • 获得 308 枚徽章 获得 15 枚金徽章, 获得 98 枚银徽章, 获得 195 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-10-08
个人主页被 1.5k 人浏览