bt365体育投注.主頁欢迎您!!

    <acronym id="zvmrr"></acronym>
    <td id="zvmrr"></td>
  • <tr id="zvmrr"><label id="zvmrr"></label></tr>
  • <acronym id="zvmrr"></acronym>
  • 袁钰涵

    袁钰涵 查看完整档案

    北京编辑  |  填写毕业院校  |  填写所在公司/组织 www.wgoodp.icu/u/yuanhan_5f5f19f9dabdc 编辑
    编辑

    而受苦又是一个坏习惯

    个人动态

    袁钰涵 赞了文章 · 1月27日

    官宣了!Apache ECharts 毕业成为 Apache 软件基金会顶级项目!

    2021 年 1 月 26 日,德克萨斯州威明顿市 Apache Blog 原文

    Apache 软件基金会(ASF)是 350 多个开源项目和计划的全志愿开发者、管理者和孵化者,今天宣布 Apache? ECharts?成为顶级项目(TLP)。

    Apache ECharts 是一个直观、可交互、强大的可视化图表库,非常适合用来作为商业级的图表演示。该项目在 2013 年起源于百度,2018 年 1 月进入 Apache 孵化器。

    “我们在 Apache 软件基金会孵化 ECharts 是一个明智的决定,”Apache ECharts VP 羡辙说,“通过学习并采纳 Apache Way,我们的社区更加健康和多样化,这改善了 ECharts,使其成为可视化专业人士和爱好者更具吸引力和竞争力的选择。”

    Apache ECharts 用 JavaScript 编写,基于支持 Canvas 和 SVG 的 ZRender 渲染引擎,提供了一系列动态的、高度可定制的图表类型,包括线图、柱图、散点图、饼图、雷达图、K 线图、仪表图、漏斗图、热力图等。功能包括:

    • 超过 20 种图表类型以及自定义图表
    • 多维度数据分析和处理
    • 开箱即用的交互组件
    • 多设备的响应式设计
    • 大数据性能优化
    • 服务器端渲染
    • 通过渐进式渲染,实现千万级数据的实时交互

    以及各种扩展应用:

    • 3D 可视化和其他丰富的特效
    • Python、R、Julia 和其他语言封装了基于 ECharts 的可视化库
    • 适配包括微信小程序、百度智能程序在内的多种平台

    可以在 https://echarts.apache.org/ex... 找到 ECharts 的数据可视化例子。

    该项目最近发布了最新版本 ECharts 5,它提供了千万数据的渲染能力,并支持符合 W3C 的 Web Accessibility Initiative Accessible Rich Internet Applications Suite(WAI-ARIA)标准的可访问性要求。

    image

    在 ECharts 核心功能的基础上,ECharts 5 通过在动画叙事、优化的可视化设计、交互体验、性能提升、开发体验、国际化、可访问性等方面的 15 项新功能和改进,让开发者更容易讲述数据背后的故事。

    阿里巴巴、亚马逊、百度、GitLab、英特尔和腾讯等公司,以及 Apache Superset 数据可视化软件等解决方案使用了 Apache ECharts 来开发他们的可视化产品。该项目的受欢迎程度正在持续增长,截至目前,GitHub 上的 Star 数量超过 44000,npm 上的周下载量也超过 250000 次。

    “我们今天生活的世界是由软件和数据驱动的,”GitHub 的首席运营官 Erica Brescia 说。“有了 Apache ECharts,全世界的开发者都可以访问一个强大的、免费的、开源的数据可视化库。我们很高兴看到该项目在 GitHub 上蓬勃发展。恭喜 Apache ECharts 毕业成为 Apache 软件基金会的顶级项目。”

    “Apache ECharts 可以帮助可视化专家和数据分析师轻松创建各种各样的可视化,对我们分析和探索数据背后的故事非常有帮助。”可视化学术先锋人物、浙江大学陈为教授说。

    “我们很高兴见证 ECharts 在 Apache 孵化器中的愉快过程。”百度高级经理祖明说。“我们的社区与来自许多国家和组织的个人一起成长,他们在错误修复、问题解决和新功能实现方面做出了贡献。”

    “当 Apache Superset 社区研究可视化库以重建核心可视化插件时,ECharts 脱颖而出,成为绝对的最佳选择。”Apache Airflow 和 Superset 的最初创建者、Apache Superset VP 的 Maxime Beauchemin 说,“它拥有无与伦比的可视化种类,丰富且可组合的可视化语法、直观且设计良好的 API、灵活且性能卓越的渲染引擎、非常精简的依赖项,以及 ASF 在长期承诺使用一个开源项目时提供的一系列重要保障。”

    “通过 Apache 孵化器指导 ECharts 社区是一件很愉快的事情,”ASF 成员、Apache ECharts 孵化导师 Dave Fisher 说,“他们接受了社区主导开发的 Apache 方式,鼓励那些有兴趣帮助改进 ECharts 的人做出贡献,成为其成长社区的一部分。”

    “对于 ECharts 社区来说,这是一个激动人心的时刻,”羡辙补充道,“我们正在保持持续的增长,也非常欢迎对此感兴趣的人成为我们的开发者和维护者。”

    在“5 分钟了解可视化利器 Apache ECharts”中,你可以了解到更多 ECharts 的功能——这是一个由 Apache ECharts 社区成员创建的新视频。

    https://www.bilibili.com/vide...

    可用性和监督

    Apache ECharts 软件在 Apache License v2.0 下发布,并由一个自选的积极贡献者团队监督。项目管理委员会(PMC)指导项目的日常运作,包括社区开发和产品发布。有关下载、文档和参与 Apache ECharts 的方法,请访问 http://echarts.apache.orghttps://twitter.com/ApacheECh...

    关于 Apache 孵化器

    Apache 孵化器是希望成为 Apache 软件基金会工作一部分的项目和代码库的主要进入途径。所有来自外部组织的代码捐赠和现有的外部项目都要通过孵化器进入 ASF,以达到以下目的 1)确保所有的捐赠符合 ASF 的法律标准;以及 2)开发新的社区,遵守我们的指导原则。所有新接受的项目都必须进行孵化,直到进一步的审查表明基础设施、通信和决策过程已经稳定下来,与其他成功的 ASF 项目一致。虽然孵化状态不一定反映了代码的完整性或稳定性,但它确实表明该项目尚未得到 ASF 的完全认可。欲了解更多信息,请访问 http://incubator.apache.org/

    关于 Apache 软件基金会 (ASF)

    Apache 软件基金会(ASF)成立于 1999 年,是世界上最大的开源基金会,管理着 2.27 亿多行代码,并 100% 免费向公众提供价值超过 200 亿美元的软件。ASF 的全志愿者社区从最初的 21 位创始人监督 Apache HTTP 服务器发展到现在的 813 位个人成员和 206 个项目管理委员会,他们通过 ASF 的择优程序,与近 8000 位 Committer 合作,成功地领导了 350 多个 Apache 项目和计划。Apache 软件几乎是每一个终端用户计算设备的组成部分,从笔记本电脑到平板电脑,再到企业的移动设备和关键任务应用。Apache 项目为大部分的互联网提供了动力,管理着 exabytes 的数据,执行着 teraflops 的操作,并在几乎每个行业中存储着数十亿的对象。商业友好和允许的 Apache 许可证 v2 是一个开放源码的行业标准,帮助启动了价值十亿美元的公司,并使全球无数的用户受益。ASF 是美国 501 (c)(3) 非营利性慈善组织,由个人捐款和企业赞助商资助,包括 Aetna、阿里巴巴云计算、亚马逊网络服务、Anonymous、百度、彭博社、Budget Direct、Capital One、Cloudera、Comcast、Didi Chuxing、Facebook、Google、Handshake、华为、IBM、Microsoft、Pineapple Fund、Red Hat、Reprise Software、Target、腾讯、Union Investment、Verizon Media 和 Workday。欲了解更多信息,请访问 http://apache.org/https://twitter.com/TheASF

    ? Apache 软件基金会。 "Apache"、 "ECharts"、 "Apache ECharts"、 "Airflow"、 "Apache Airflow"、 "Superset"、 "Apache Superset" 和 "ApacheCon" 是 Apache 软件基金会在美国和/或其他国家的注册商标或商标。所有其他品牌和商标都是其各自所有者的财产。

    特别提醒:Apache ECharts 5 将于 2021.01.28 20:00 进行线上发布会。届时,将由众多 Apache ECharts PMC 和 Committers 为大家详细介绍 ECharts 5 的新功能,帮助大家快速上手拥有强大叙事能力的 Apache ECharts 5!长按下面的二维码关注直播间~

    image

    查看原文

    赞 22 收藏 0 评论 0

    袁钰涵 赞了文章 · 1月27日

    通俗易懂的JUC源码剖析-CopyOnWriteArrayList

    前言

    众所皆知,ArrayList是线程不安全的,它的所有方法都没有加锁,那么有没有线程安全并且性能高的类呢?那就是CopyOnWriteArrayList

    实现原理

    首先来看它的关键数据结构:

    /** The lock protecting all mutators */
    final transient ReentrantLock lock = new ReentrantLock();
    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;

    可以看到,底层和ArrayList一样,用数组来保存元素,但它多了把独占锁lock,来保证线程安全。

    下面直接进入主题,看看它的add()方法如何实现的:

    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true; } finally {
            lock.unlock();
        }
    }

    代码逻辑很清晰明了:
    第一步,获取数组的排他锁
    第二步,获取数组元素和长度n,拷贝一个n+1长度的新数组
    第三步,把待添加的元素e放在最后一个位置
    第四步,覆盖旧的数组,返回true表示添加成功
    第五步,释放锁

    简而言之,它的实现思路就跟它的命名一样,CopyOnWrite,“写时复制”,添加元素的时候,先复制数组,添加完成后覆盖掉旧的数组,这些步骤是在加锁的环境完成的,也就是说这个过程中不会有其他线程同时也在写数组,这就保证了写操作的线程安全。

    再来看set()方法:

    public E set(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            E oldValue = get(elements, index);
            // 如果新值与旧值不同,则拷贝一个新数组,并在index处设置新值
            if (oldValue != element) {
                int len = elements.length;
                Object[] newElements = Arrays.copyOf(elements, len);
                newElements[index] = element;
                setArray(newElements);
            } else {
                // Not quite a no-op; ensures volatile write semantics
                // 新值与旧值相同,为了保证volatile语义,也覆盖下数组,即使内容相同。
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

    再来看remove()方法

    public E remove(int index) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;
            // 如果删除的是最后一个元素
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));
            else {
                Object[] newElements = new Object[len - 1];
                // 分两步复制
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index + 1, newElements, index,
                numMoved);
                setArray(newElements);
            }
            return oldValue;
       } finally {
            lock.unlock();
       }
    }

    再来看get()方法:

    public E get(int index) {
        return get(getArray(), index);
    }
    final Object[] getArray() {
        return array;
    }
    private E get(Object[] a, int index) {
        return (E) a[index];
    }

    可以看到,它的get方法分为2步,先获取数组,再获取index位置的元素,这2步都是没有加锁的?为什么不需要加锁呢?

    上面提到,add()是先拷贝原数组,然后在拷贝的数组上操作的,在setArray()之前对原数组并没有影响,因此读的时候不需要加锁。虽然不需要加锁,但会出现数据弱一致性问题,下图说明

    线程A线程B
    a = getArray()
    remove(a, index)
    get(a, index)

    image.png

    在A线程获取了数组(a=array)后,还没有来得及获取index位置的元素a[index],线程B删除了index位置的元素,并将array引用指向新的数组(array=newArray),但是由于线程A用的是栈区的数组引用a,它引用的还是删除元素前的数组,因此它还是会访问到index这个被删除的元素,因此说会有数据的弱一致性问题,但不会抛ConcurrentModificationException异常。

    它的迭代器iterator也是有这种弱一致性的特性,迭代对象是数组的快照,迭代过程中,如果其他线程修改了数组,对迭代器来说是不可见的。
    代码如下:

    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }
    // COW = Copy On Write
    static final class COWIterator<E> implements ListIterator<E> {
        /** Snapshot of the array */
        // 数组元素的一份快照
        private final Object[] snapshot;
        /** Index of element to be returned by subsequent call to next.  */
        // 当前迭代的位置-光标
        private int cursor;
        private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            snapshot = elements;
       }
        public boolean hasNext() {
            return cursor < snapshot.length;
        }
        public boolean hasPrevious() {
            return cursor > 0;
        }
        @SuppressWarnings("unchecked")
        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }
     }

    由此可以看出,CopyOnWriteArrayList适合用在读多写少的场景,性能会比Vector快,因为Vector的所有方法都加了锁,包括读。

    最后提下,CopyOnWriteArraySet就是用CopyOnWriteArrayList实现的,所以原理大同小异,有兴趣的同学自己去看下源码吧。

    参考资料:
    《Java并发编程之美》

    查看原文

    赞 2 收藏 1 评论 0

    袁钰涵 赞了文章 · 1月27日

    《Java 面经手册》PDF,全书5章29节,417页11.5万字,完稿&发版!

    作者:小傅哥
    博客:https://bugstack.cn

    沉淀、分享、成长,让自己和他人都能有所收获!??

    一、前言

    我膨胀了?? ,在看了大部分以面试讲解的 Java 文章后,发现很多内容的讲解都偏向于翻译抄书说理论的给答案式讲解,最终把知识弄的云里雾里。

    就像我问你:

    • HashCode为什么用31作为乘数,你证明过吗?
    • 扰动函数的函数作用是什么,它还有什么场景在用?
    • 拉链寻址和开放寻址具体是什么表现,怎么解决的碰撞问题?
    • ThreadLocal 的实现中还有黄金分割点的使用,你知道吗?
    • CLH、MCS,都是怎么实现的公平锁,代码是什么样?
    • jvmti 可以用于非入侵的监控线程池状态,你用过吗?

    关于以上的问题,是不有种即使看过 Java 核心 API 的源码,也很难回答出来?

    这是因为 Java 代码本身就是基于数据结构和算法对数学逻辑的具体实现,而那些隐含在代码中的数学知识如果你不会,那么压根你就会忽略掉它,也就因此看不懂源码了。

    知识的视觉盲区,就像夜间开车。车灯不够亮你只能看到30%的视野,开的越快越危险!

    所以,此书并不是单纯的面试题,也不是内卷八股文。而是从一个单纯的和程序员有关的数学知识点开始,深入讲解 Java 的核心技术。并且每一章节都配有实践验证的源码,可以对照着一起撸才更有感觉!

    全书共计5章29节,417页11.5万字,耗时4个月完成。涵盖数据结构、算法逻辑、并发编程、JVM以及简历和互联网大厂面试等内容。

    ??鉴于作者水平有限,如果书中含有不易理解的内容,一定是作者在编写的过程中缺少必要的描述和严格的校准,感谢把你的意见或者疑问提交给我,也欢迎与我多一些交互,互相进步共同成长。

    二、简介

    Hello, world of java ! 你好,java的世界!

    欢迎来到这里,很高兴你能拿到这本书。如果你能坚持看完书中每章节的内容,那么不仅可以在你的面试求职上有所帮助,也更能让你对 Java 核心技术有更加深入的学习。

    《Java 面经手册》 是一本以面试题为入口讲解 Java 核心技术的 PDF 书籍,书中内容也极力的向你证实代码是对数学逻辑的具体实现为什么这么说? 当你仔细阅读书籍时,会发现这里有很多数学知识,包括:扰动函数、负载因子、拉链寻址、开放寻址、斐波那契(Fibonacci)散列法还有黄金分割点的使用等等。

    编码只是在确定了研发设计后的具体实现,而设计的部分包括:数据结构、算法逻辑以及设计模式等,而这部分数据结构和算法逻辑在 Java 的核心 API 中体现的淋漓尽致。那么,也就解释了为什么这些内容成为了热点面试题,虽然可能我们都会觉得这样的面试像是造火箭。

    那么,??汽车75马力就够奔跑了,那你怎么还想要2.0涡轮+9AT呢?大桥两边的护栏你每次走的时候都会去摸吗?那怎么没有护栏的大桥你不敢上呢?

    很多时候,你额外的能力才是自身价值的体现,不要以为你的能力就只是做个业务开发每天CRUD。其实有时候并不是产品让你写CRUD,而是因为你的能力只能产品功能设计成CRUD。

    就像数据结构、算法逻辑、源码技能,它都是可以为你的业务开发赋能的,也是写出更好、更易扩展程序的根基,所以学好这份知识非常有必要。

    所以,我非常建议你深度阅读此书,如果书中的知识点对你只是抛砖引玉,那么就更好了,你可以继续深入索取,吸纳更多的、更深的内容到自己的头脑中。

    1. 适合人群

    1. 具备一定编程基础,工作1-3年的研发人员
    2. 想阅读 Java 核心源码,但总感觉看不懂的
    3. 看了太多理论,但没有实践验证的
    4. 求职面试,总被面试题搞的死去活来的

    2. 我能学到什么

    1. 怎么写简历、怎么面大厂、怎么补充不足
    2. Java 核心API中用到的数据结构和算法逻辑
    3. 必会的数学知识,扰动函数、负载因子、拉链寻址、开放寻址、斐波那契(Fibonacci)散列法等
    4. 学到学习的能力,跟着作者的分析和学习方式,增强自己的学习能力

    3. 阅读建议

    本书虽然是源码分析、理论实践,但并不会让读者感觉枯燥。作者:小傅哥,在每一篇的知识里都写下了实践验证的结果,对于每一章节都有对应的源码实现。小伙伴在阅读的时候可以对照源码实践,并且在源码中还包括了一些必备的素材(10万单词表验证扰动函数)、工具、图标等,来让大家切身的体会到知识乐趣。也让所有认真阅读的读者,看后都能让懂了就是真的懂

    4. 书籍截图

    三、目录

    第 1 章 谈谈面试

    • 第 1 节:面试官都问我啥
    • 第 2 节:认知自己的技术栈盲区
    • 第 3 节:简历该怎么写
    • 第 4 节:大厂都爱聊啥

    第 2 章 数据结构和算法

    • 第 1 节:HashCode为什么使用31作为乘数
    • 第 2 节:HashMap 源码分析(上)
    • 第 3 节:HashMap 源码分析(下)
    • 第 4 节:2-3树与红黑树学习(上)
    • 第 5 节:2-3树与红黑树学习(下)
    • 第 6 节:ArrayList 详细分析
    • 第 7 节:LinkedList、ArrayList,插入分析
    • 第 8 节:双端队列、延迟队列、阻塞队列
    • 第 9 节:java.util.Collections、排序、二分、洗牌、旋转算法
    • 第 10 节:StringBuilder 与 String 对比
    • 第 11 节:ThreadLocal 源码分析

    第 3 章 码农会锁

    • 第 1 节:volatile
    • 第 2 节:synchronized
    • 第 3 节:ReentrantLock 和 公平锁
    • 第 4 节:AQS原理分析和实践运用
    • 第 5 节:AQS 共享锁,Semaphore、CountDownLatch

    第 4 章 多线程

    • 第 1 节:Thread.start() 启动原理
    • 第 2 节:Thread,状态转换、方法使用、原理分析
    • 第 3 节:ThreadPoolExecutor
    • 第 4 节:线程池讲解以及JVMTI监控

    第 5 章 JVM 虚拟机

    • 第 1 节:JDK、JRE、JVM
    • 第 2 节:JVM 类加载实践
    • 第 3 节:JVM 内存模型
    • 第 4 节:JVM 故障处理工具
    • 第 5 节:GC 垃圾回收

    四、PDF??下载

    版权说明11.5万字417页作者:小傅哥的原创书籍《Java 面经手册》,已上架 CSDN 付费下载平台,享受版权保护。但只设置最低下载价格:1.9元,感谢支持和理解。

    让人怪不好意思的,没接过广告的号主,只能这样收回点运营博客的服务器成本。更重要的是保护了版权!!!

    1. 可获得内容包括

    1. 《Java 面经手册》PDF 完整版书籍一本
    2. 完整版源码一份,共 27 个案例
    3. 可以加入面经专栏讨论群,添加我的微信:fustack,备注:面经入群

    2. 下载方式

    1. 公众号:bugstack虫洞栈,回复:面经手册,即可获得最新的下载链接。更新和补充会更换链接
    2. 添加小傅哥微信(fustack),备注:面经
    3. 直接点击本文结尾的,阅读原文,可以直接进入下载

    五、??收尾

    19年7月 ~ 20年初,是小傅哥做技术号主的迷糊状态,没有粉丝基础没有写作经验没有技术文章沉淀、当然也没有运营技巧,而一年后这样一个似乎是闯进了技术圈的该溜子终于在粉丝的包容、理解和支持上,慢慢成长起来了。也有了一本《重学Java设计模式》全网可统计到的 21 万+ 下载量GitHub 项目多次霸榜 Trending,成为全球热门项目。也积累了属于自己的一窝粉丝,感谢你们

    再说说《Java 面经手册》,本书到这里还不是结束,接下来还会继续编写,Spring、SpringBoot、Rpc、Mysql以及中间件相关的面经。同样,面经不只是面经,更是核心技术的学习和深入的了解。所有的内容的输出都是一个目的,让更多的人对知识能做到,让懂了就是真的懂!

    祝大家在学习过程中都有自己的收获和能力的提升,提前祝新年快乐,平安吉祥!

    查看原文

    赞 11 收藏 4 评论 2

    袁钰涵 发布了文章 · 1月26日

    警惕!一个针对安全研究人员进行攻击的组织出现了

    编译:袁钰涵

    谷歌威胁分析小组发现了一个攻击活动组织,经过几个月的努力,确认该活动组织是针对在不同的公司和组织中从事漏洞研究和开发的安全研究人员进行攻击。

    谷歌认为这个活动的参与者是朝鲜网军,攻击活动成立的本身,在于实施社会工程学的人员精通漏洞分析和研究,在过去一段时间中,他们采取了多种手段来锁定研究人员并发起攻击,后文将会讲述他们用了什么方法来锁定研究人员。

    谷歌威胁分析小组希望这篇文章能提醒安全研究界的朋友,如今他们成为了这群朝鲜政府支持派的攻击目标。

    如何联系研究人员?

    为了与安全研究人员获得联系并骗取他们信任,该组织建立了一个研究博客以及多个 Twitter 账号,借用以上两者与潜在目标进行互动。

    他们用这些 Twitter 账号发布指向博客的链接,同时声称博客中有人发现了漏洞并对漏洞以利用,发布了漏洞利用的视频,用账号之间互相转发视频扩大其影响力,还在其他用户的帖子下发布这类内容。

    这个组织所拥有的 Twitter 账号

    组织建立的博客中包含了已公开披露的漏洞文章和分析,还会包含某位研究人员研究漏洞后对博客的投稿,而这些被网站写出“投稿的研究人员”本人对此却完全不知情,组织如此做可能是为了在安全研究人员中获得更高的信任度。

    组织对公开披露的漏洞进行分析的示例

    尽管无法验证他们发布的所有漏洞的真实性与工作状态,但在组织声称他们发现了有效漏洞并使之运行时,可以知道他们所说是假的。

    2021 年 1 月 14 日,组织控制的账号通过 Twitter 分享了他们的 YouTube 视频,视频声称构造了 CVE-2021-1647(一个最近修复的 Windows Defender 漏洞)的利用程序,让它产生 cmd.exe shell,人们仔细观看视频后确认这是一个虚假漏洞。

    YouTube 上也有多条评论表示该视频是虚假伪造视频,根本没有可利用的漏洞。

    但组织不顾 You Tube 上的评论,用他们控制的其他 Twitter 帐号转发原帖,并评论“不是假冒视频”。

    组织对多个账号的“利用”

    如何锁定安全研究员?

    这个组织对安全研究人员建立最初的联系后,他们会询问研究人员是否希望在漏洞研究方面进行合作,然后为研究人员提供 Visual Studio 项目。

    Visual Studio 项目中包含利用此漏洞的源代码,但是在编译该Visual Studio项目的时候会自动生成存在恶意代码的DLL。

    DLL 是一个恶意软件,它会与组织控制的 C2 域进行通信,下图显示了 VS Build Event 的示例。

    生成 VS Project 文件时执行的 Visual Studio 命令

    除了通过骗取信任获得合作等一系列行动锁定攻击目标,有时该组织会直接在安全研究人员访问其博客后马上攻击。

    研究人员通过 Twitter 访问 blog.br0vvnn [.] io 的文章不久后,会发现系统被安装了恶意服务,内存后门将开始连接到攻击者拥有的命令和控制服务器。

    受攻击时研究人员的系统正在运行最新版本的 Windows 10 和 Chrome 浏览器版本。目前谷歌方面没有发现是否为 Chrome 的 0day 漏洞。

    这个组织已使用多个平台与潜在目标进行联系,这些平台包括了 Twitter,LinkedIn,Telegram,Discord,Keybase 和电子邮件。

    谷歌威胁分析小组在下面提供已知的组织帐户和别名,如果已与这些帐户中的任何一个进行了交流,或直接访问了他们的博客,就要小心了。

    如果担心自己会成为目标,谷歌威胁分析小组建议使用单独的物理机或虚拟机来进行研究活动。

    组织控制的网站和帐户

    研究博客

    https://blog.br0vvnn[.]io

    Twitter 帐户

    LinkedIn 帐户

    Keybase

    https://keybase.io/zhangguo

    Telegram

    https://t.me/james50d

    样本 Hash

    攻击者拥有的域名

    • angeldonationblog[.]com
    • codevexillium[.]org
    • investbooking[.]de
    • krakenfolio[.]com
    • opsonew3org[.]sg
    • transferwiser[.]io
    • transplugin[.]io

    攻击者通过入侵后控制的域名

    • trophylab[.]com
    • www.colasprint[.]com
    • www.dronerc[.]it
    • www.edujikim[.]com
    • www.fabioluciani[.]com

    C2 URLs

    • https[:]//angeldonationblog[.]com/image/upload/upload.php
    • https[:]//codevexillium[.]org/image/download/download.asp
    • https[:]//investbooking[.]de/upload/upload.asp
    • https[:]//transplugin[.]io/upload/upload.asp
    • https[:]//www.dronerc[.]it/forum/uploads/index.php
    • https[:]//www.dronerc[.]it/shop_testbr/Core/upload.php
    • https[:]//www.dronerc[.]it/shop_testbr/upload/upload.php
    • https[:]//www.edujikim[.]com/intro/blue/insert.asp
    • https[:]//www.fabioluciani[.]com/es/include/include.asp
    • http[:]//trophylab[.]com/notice/images/renewal/upload.asp
    • http[:]//www.colasprint[.]com/_vti_log/upload.asp

    主机 IOC

    Registry Keys

    • HKLMSOFTWAREMicrosoftWindowsCurrentVersionKernelConfig
    • HKLMSOFTWAREMicrosoftWindowsCurrentVersionDriverConfig
    • HKCUSOFTWAREMicrosoftWindowsCurrentVersionRunSSL Update

    文件路径

    • C:WindowsSystem32Nwsapagent.sys
    • C:WindowsSystem32helpsvc.sys
    • C:ProgramDataUSOShareduso.bin
    • C:ProgramDataVMwarevmnat-update.bin
    • C:ProgramDataVirtualBoxupdate.bin
    查看原文

    赞 2 收藏 2 评论 0

    袁钰涵 关注了用户 · 1月26日

    xiangzhihong @xiangzhihong

    著有《React Native移动开发实战》1,2、《Kotlin入门与实战》《Weex跨平台开发实战》、《Flutter跨平台开发与实战》和《Android应用开发实战》

    关注 7088

    袁钰涵 赞了文章 · 1月26日

    Flutter混合开发

    混合开发简介

    使用Flutter从零开始开发App是一件轻松惬意的事情,但对于一些成熟的产品来说,完全摒弃原有App的历史沉淀,全面转向Flutter是不现实的。因此使用Flutter去统一Android、iOS技术栈,把它作为已有原生App的扩展能力,通过有序推进来提升移动终端的开发效率。
    目前,想要在已有的原生App里嵌入一些Flutter页面主要有两种方案。一种是将原生工程作为Flutter工程的子工程,由Flutter进行统一管理,这种模式称为统一管理模式。另一种是将Flutter工程作为原生工程的子模块,维持原有的原生工程管理方式不变,这种模式被称为三端分离模式。

    在这里插入图片描述
    在Flutter框架出现早期,由于官方提供的混编方式以及资料有限,国内较早使用Flutter进行混合开发的团队大多使用的是统一管理模式。但是,随着业务迭代的深入,统一管理模式的弊端也随之显露,不仅三端(Android、iOS和Flutter)代码耦合严重,相关工具链耗时也随之大幅增长,最终导致开发效率降低。所以,后续使用Flutter进行混合开发的团队大多使用三端代码分离的模式来进行依赖治理,最终实现Flutter工程的轻量级接入。
    除了可以轻量级接入外,三端代码分离模式还可以把Flutter模块作为原生工程的子模块,从而快速地接入Flutter模块,降低原生工程的改造成本。在完成对Flutter模块的接入后,Flutter工程可以使用Android Studio进行开发,无需再打开原生工程就可以对Dart代码和原生代码进行开发调试。
    使用三端分离模式进行Flutter混合开发的关键是抽离Flutter工程,将不同平台的构建产物依照标准组件化的形式进行管理,即Android使用aar、iOS使用pod。也就是说,Flutter的混编方案其实就是将Flutter模块打包成aar或者pod库,然后在原生工程像引用其他第三方原生组件库那样引入Flutter模块即可。

    Flutter模块

    默认情况下,新创建的Flutter工程会包含Flutter目录和原生工程的目录。在这种情况下,原生工程会依赖Flutter工程的库和资源,并且无法脱离Flutter工程独立构建和运行。
    在混合开发中,原生工程对Flutter的依赖主要分为两部分。一个是Flutter的库和引擎,主要包含Flutter的Framework 库和引擎库;另一个是Flutter模块工程,即Flutter混合开发中的Flutter功能模块,主要包括Flutter工程lib目录下的Dart代码实现。
    对于原生工程来说,集成Flutter只需要在同级目录创建一个Flutter模块,然后构建iOS和Android各自的Flutter依赖库即可。接下来,我们只需要在原生项目的同级目录下,执行Flutter提供的构建模块命令创建Flutter模块即可,如下所示。

    flutter create -t module flutter_library    

    其中,flutter_library为Flutter模块名。执行上面的命令后,会在原生工程的同级目录下生成一个flutter_library模块工程。Flutter模块也是Flutter工程,使用Android Studio打开它,其目录如下图所示。
    在这里插入图片描述
    可以看到,和普通的Flutter工程相比,Flutter模块工程也内嵌了Android工程和iOS工程,只不过默认情况下,Android工程和iOS工程是隐藏的。因此,对于Flutter模块工程来说,也可以像普通工程一样使用 Android Studio进行开发和调试。
    同时,相比普通的Flutter工程,Flutter模块工程的Android工程目录下多了一个Flutter目录,此目录下的build.gradle配置就是我们构建aar时的打包配置。同样,在Flutter模块工程的iOS工程目录下也会找到一个Flutter目录,这也是Flutter模块工程既能像Flutter普通工程一样使用Android Studio进行开发调试,又能打包构建aar或pod的原因。

    Android集成Flutter

    在原生Android工程中集成Flutter,原生工程对Flutter的依赖主要包括两部分,分别是Flutter库和引擎,以及Flutter工程构建产物。

    • Flutter库和引擎:包含icudtl.dat、libFlutter.so以及一些class文件,最终这些文件都会被封装到Flutter.jar中。
    • Flutter工程产物:包括应用程序数据段 isolate_snapshot_data、应用程序指令段 isolate_snapshot_instr、虚拟机数据段vm_snapshot_data、虚拟机指令段vm_snapshot_instr以及资源文件flutter_assets。

    和原生Android工程集成其他插件库的方式一样,在原生Android工程中引入Flutter模块需要先在settings.gradle中添加如下代码。

    setBinding(new Binding([gradle: this]))
    evaluate(new File(
      settingsDir.parentFile,
      'flutter_library/.android/include_flutter.groovy'))
    

    其中,flutter_library为我们创建的Flutter模块。然后,在原生Android工程的app目录的build.gradle文件中添加如下依赖。

    dependencies {
        implementation project(":flutter")
    }

    然后编译并运行原生Android工程,如果没有任何错误则说明集成Flutter模块成功。需要说明的是,由于Flutter支持的最低版本为16,所以需要将Android项目的minSdkVersion修改为16。
    如果出现“程序包android.support.annotation不存在”的错误,需要使用如下的命令来创建Flutter模块,因为最新版本的Android默认使用androidx来管理包。

    flutter create --androidx -t module flutter_library

    对于Android原生工程,如果还没有升级到androidx,可以在原生Android工程上右键,然后依次选择【Refactor】→【Migrate to Androidx】将Android工程升级到androidx包管理。
    在原生Android工程中成功添加Flutter模块依赖后,打开原生Android工程,并在应用的入口MainActivity文件中添加如下代码。

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            View flutterView = Flutter.createView(this, getLifecycle(), "route1");
            FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
            addContentView(flutterView, layoutParams);
        }
    }

    通过Flutter提供的createView()方法,可以将Flutter页面构建成Android能够识别的视图,然后将这个视图使用Android提供的addContentView()方法添加到父窗口即可。重新运行原生Android工程,最终效果如下图所示。
    在这里插入图片描述
    如果原生Android的MainActivity加载的是一个FrameLayout,那么加载只需要将Flutter页面构建成一个Fragment即可,如下所示。

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            FragmentTransaction ft= getSupportFragmentManager().beginTransaction();
            ft.replace(R.id.fragment_container, Flutter.createFragment("Hello Flutter"));
            ft.commit();
        }
    }
    

    除了使用Flutter模块方式集成外,还可以将Flutter模块打包成aar,然后再添加依赖。在flutter_library根目录下执行aar打包构建命令即可抽取Flutter依赖,如下所示。

    flutter build apk --debug

    此命令的作用是将Flutter库和引擎以及工程产物编译成一个aar包,上面命令编译的aar包是debug版本,如果需要构建release版本,只需要把命令中的debug换成release即可。
    打包构建的flutter-debug.aar位于.android/Flutter/build/outputs/aar/目录下,可以把它拷贝到原生Android工程的app/libs目录下,然后在原生Android工程的app目录的打包配置build.gradle中添加对它的依赖,如下所示。

    dependencies {
      implementation(name: 'flutter-debug', ext: 'aar')   
    }

    然后重新编译一下项目,如果没有任何错误提示则说明Flutter模块被成功集成到Android原生工程中。

    iOS集成Flutter

    原生iOS工程对Flutter的依赖包含Flutter库和引擎,以及Flutter工程编译产物。其中,Flutter 库和引擎指的是Flutter.framework等,Flutter工程编译产物指的是 App.framework等。
    在原生iOS工程中集成Flutter需要先配置好CocoaPods,CocoaPods是iOS的类库管理工具,用来管理第三方开源库。在原生iOS工程中执行pod init命令创建一个Podfile文件,然后在Podfile文件中添加Flutter模块依赖,如下所示。

    flutter_application_path = '../flutter_ library/
    load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
    
    target 'iOSDemo' do
      # Comment the next line if you don't want to use dynamic frameworks
      use_frameworks!
      install_all_flutter_pods(flutter_application_path)
    
      # Pods for iOSDemo
      … //省略其他脚本
    end '
    

    然后,关闭原生iOS工程,并在原生iOS工程的根目录执行pod install命令安装所需的依赖包。安装完成后,使用Xcode打开iOSDemo.xcworkspace原生工程。
    默认情况下,Flutter是不支持Bitcode的,Bitcode是一种iOS编译程序的中间代码,在原生iOS工程中集成Flutter需要禁用Bitcode。在Xcode中依次选择【TAGETS】→【Build Setttings】→【Build Options】→【Enable Bitcode】来禁用Bitcode,如下图所示。
    在这里插入图片描述
    如果使用的是Flutter早期的版本,还需要添加build phase来支持构建Dart代码。依次选择【TAGGETS】→【Build Settings】→【Enable Phases】,然后点击左上角的加号新建一个“New Run Script Phase”,添加如下脚本代码。

    "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
    "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
    

    不过,最新版本的Flutter已经不需要再添加脚本了。重新运行原生iOS工程,如果没有任何错误则说明iOS成功集成Flutter模块。
    除了使用Flutter模块方式外,还可以将Flutter模块打包成可以依赖的动态库,然后再使用CocoaPods添加动态库。首先,在flutter_library根目录下执行打包构建命令生成framework动态库,如下所示。

    flutter build ios --debug

    上面命令是将Flutter工程编译成Flutter.framework和App.framework动态库。如果要生成release版本,只需要把命令中的debug换成release即可。
    然后,在原生iOS工程的根目录下创建一个名为FlutterEngine的目录,并把生成的两个framework动态库文件拷贝进去。不过,iOS生成模块化产物要比Android多一个步骤,因为需要把Flutter工程编译生成的库手动封装成一个pod。首先,在flutter_ library该目录下创建FlutterEngine.podspec,然后添加如下脚本代码。

    Pod::Spec.new do |s|
      s.name             = 'FlutterEngine'
      s.version          = '0.1.0'
      s.summary          = 'FlutterEngine'
      s.description      = <<-DESC
    TODO: Add long description of the pod here.
                           DESC
      s.homepage         = 'https://github.com/xx/FlutterEngine'
      s.license          = { :type => 'MIT', :file => 'LICENSE' }
      s.author           = { 'xzh' => '1044817967@qq.com' }
      s.source       = { :git => "", :tag => "#{s.version}" }
      s.ios.deployment_target = '9.0'
      s.ios.vendored_frameworks = 'App.framework', 'Flutter.framework'
    end
    

    然后,执行pod lib lint命令即可拉取Flutter模块所需的组件。接下来,在原生iOS工程的Podfile文件添加生成的库即可。

    target 'iOSDemo' do
        pod 'FlutterEngine', :path => './'
    end
    

    重新执行pod install命令安装依赖库,原生iOS工程集成Flutter模块就完成了。接下来,使用Xcode打开ViewController.m文件,然后添加如下代码。

    #import "ViewController.h"
    #import <Flutter/Flutter.h>
    #import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h>
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        UIButton *button = [[UIButton alloc]init];
        [button setTitle:@"加载Flutter模块" forState:UIControlStateNormal];
        button.backgroundColor=[UIColor redColor];
        button.frame = CGRectMake(50, 50, 200, 100);
        [button setTitleColor:[UIColor redColor] forState:UIControlStateHighlighted];
        [button addTarget:self action:@selector(buttonPrint) forControlEvents:UIControlEventTouchUpInside];
        [self.view addSubview:button];
    }
    
    - (void)buttonPrint{
        FlutterViewController * flutterVC = [[FlutterViewController alloc]init];
        [flutterVC setInitialRoute:@"defaultRoute"];
        [self presentViewController:flutterVC animated:true completion:nil];
    }
    
    @end

    在上面的代码中,我们在原生iOS中创建了一个按钮,点击按钮时就会跳转到Flutter页面,最终效果如下图所示。
    在这里插入图片描述
    默认情况下,Flutter为提供了两种调用方式,分别是FlutterViewController和FlutterEngine。对于FlutterViewController来说,打开ViewController.m文件,在里面添加一个加载flutter页面的方法并且添加一个按钮看来调用。

    Flutter模块调试

    众所周知,Flutter的优势之一就是在开发过程中使用热重载功能来实现快速调试。默认情况下,在原生工程中集成Flutter模块后热重载功能是失效的,需要重新运行原生工程才能看到效果。如此一来,Flutter开发的热重载优势就失去了,并且开发效率也随之降低。
    那么,能不能在混合项目中开启Flutter的热重载呢?答案是可以的,只需要经过如下步骤即可开启热重载功能。首先,关闭原生应用,此处所说的关闭是指关闭应用的进程,而不是简单的退出应用。在Flutter模块的根目录中输入flutter attach命令,然后再次打开原生应用,就会看到连接成功的提示,如下图所示。

    在这里插入图片描述
    如果同时连接了多台设备,可以使用flutter attach -d 命令来指定连接的设备。接下来,只需要按r键即可执行热重载,按R键即可执行热重启,按d键即可断开连接。
    在Flutter工程中,我们可以直接点击debug按钮来进行代码调试,但在混合项目中,直接点击debug按钮是不起作用的。此时,可以使用Android Studio提供的flutter attach按钮来建立与flutter模块的连接,进行实现对flutter模块的代码调试,如图下图所示。

    在这里插入图片描述
    上面只是完成了在原生工程中引入Flutter模块,具体开发时还会遇到与Flutter模块的通信问题、路由管理问题,以及打包等。

    查看原文

    赞 34 收藏 24 评论 1

    袁钰涵 赞了文章 · 1月26日

    Zookeeper思维导图

    zookeeper思维导图

    常见相关问题

    ZooKeeper是什么?

    ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,它是集群的管理者,监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户。 客户端的读请求可以被集群中的任意一台机器处理,如果读请求在节点上注册了监听器,这个监听器也是由所连接的zookeeper机器来处理。对于写请求,这些请求会同时发给其他zookeeper机器并且达成一致后,请求才会返回成功。因此,随着zookeeper的集群机器增多,读请求的吞吐会提高但是写请求的吞吐会下降。 有序性是zookeeper中非常重要的一个特性,所有的更新都是全局有序的,每个更新都有一个唯一的时间戳,这个时间戳称为zxid(Zookeeper Transaction Id)。而读请求只会相对于更新有序,也就是读请求的返回结果中会带有这个zookeeper最新的zxid。

    节点类型

    • PERSISTENT-持久化目录节点 客户端与zookeeper断开连接后,该节点依旧存在
    • PERSISTENT_SEQUENTIAL-持久化顺序编号目录节点 客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号
    • EPHEMERAL-临时目录节点 客户端与zookeeper断开连接后,该节点被删除
    • EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点。客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号

    集群模式

    在 ZooKeeper 集群中将服务器分成 Leader 、Follow 、Observer 三种角色服务器,在集群运行期间这三种服务器所负责的工作各不相同:

    • Leader 角色服务器负责管理集群中其他的服务器,是集群中工作的分配和调度者。
    • Follow 服务器的主要工作是选举出 Leader 服务器,在发生 Leader 服务器选举的时候,系统会从 Follow 服务器之间根据多数投票原则,选举出一个 Follow 服务器作为新的 Leader 服务器。
    • Observer 服务器则主要负责处理来自客户端的获取数据等请求,并不参与 Leader 服务器的选举操作,也不会作为候选者被选举为 Leader 服务器。

    Zookeeper工作原理

    Zookeeper 的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协议。

    Zab协议有两种模式,它们分别是恢复模式(选主)和广播模式(同步)。当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和 leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和Server具有相同的系统状态。

    Zookeeper 下 Server工作状态

    每个Server在工作过程中有三种状态:

    LOOKING:当前Server不知道leader是谁,正在搜寻

    LEADING:当前Server即为选举出来的leader

    FOLLOWING:leader已经选举出来,当前Server与之同步

    Zookeeper分布式锁(文件系统、通知机制)

    有了zookeeper的一致性文件系统,锁的问题变得容易。

    锁服务可以分为两类,一个是保持独占,另一个是控制时序。

    对于第一类,我们将zookeeper上的一个znode看作是一把锁,通过createznode的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。用完删除掉自己创建的distribute_lock 节点就释放出锁。 对于第二类, /distribute_lock 已经预先存在,所有客户端在它下面创建临时顺序编号目录节点,和选master一样,编号最小的获得锁,用完删除,依次方便。

    zookeeper是如何选取主leader的?

    当leader崩溃或者leader失去大多数的follower,这时zk进入恢复模式,恢复模式需要重新选举出一个新的leader,让所有的Server都恢复到一个正确的状态。

    Zk的选举算法有两种:一种是基于basic paxos实现的,另外一种是基于fast paxos算法实现的。系统默认的选举算法为fast paxos。

    1、Zookeeper选主流程(basic paxos)

    (1)选举线程由当前Server发起选举的线程担任,其主要功能是对投票结果进行统计,并选出推荐的Server;

    (2)选举线程首先向所有Server发起一次询问(包括自己);

    (3)选举线程收到回复后,验证是否是自己发起的询问(验证zxid是否一致),然后获取对方的id(myid),并存储到当前询问对象列表中,最后获取对方提议的leader相关信息(id,zxid),并将这些信息存储到当次选举的投票记录表中;

    (4)收到所有Server回复以后,就计算出zxid最大的那个Server,并将这个Server相关信息设置成下一次要投票的Server;

    (5)线程将当前zxid最大的Server设置为当前Server要推荐的Leader,如果此时获胜的Server获得n/2 + 1的Server票数,设置当前推荐的leader为获胜的Server,将根据获胜的Server相关信息设置自己的状态,否则,继续这个过程,直到leader被选举出来。

    通过流程分析我们可以得出:要使Leader获得多数Server的支持,则Server总数必须是奇数2n+1,且存活的Server的数目不得少于n+1. 每个Server启动后都会重复以上流程。

    在恢复模式下,如果是刚从崩溃状态恢复的或者刚启动的server还会从磁盘快照中恢复数据和会话信息,zk会记录事务日志并定期进行快照,方便在恢复时进行状态恢复。

    2、Zookeeper选主流程(basic paxos) fast paxos流程是在选举过程中,某Server首先向所有Server提议自己要成为leader,当其它Server收到提议以后,解决epoch和 zxid的冲突,并接受对方的提议,然后向对方发送接受提议完成的消息,重复这个流程,最后一定能选举出Leader。

    Zookeeper同步流程

    选完Leader以后,zk就进入状态同步过程。

    1、Leader等待server连接;

    2、Follower连接leader,将最大的zxid发送给leader;

    3、Leader根据follower的zxid确定同步点;

    4、完成同步后通知follower 已经成为uptodate状态;

    5、Follower收到uptodate消息后,又可以重新接受client的请求进行服务了。

    查看原文

    赞 3 收藏 2 评论 0

    袁钰涵 赞了文章 · 1月25日

    Redux在React Hook中的使用及其原理

    Hello, 各位勇敢的小伙伴, 大家好, 我是你们的嘴强王者小五, 身体健康, 脑子没病.
    ?

    本人有丰富的脱发技巧, 能让你一跃成为资深大咖.

    一看就会一写就废是本人的主旨, 菜到抠脚是本人的特点, 卑微中透着一丝丝刚强, 傻人有傻福是对我最大的安慰.

    欢迎来到小五随笔系列Redux在React Hook中的使用及其原理.

    浅谈Redux

    下面将从what, why, how to 三个方面来说说Redux

    第一问 what ? 什么是Redux

    将一个web应用拆分成视图层与数据层, Redux就是保存其数据的一个容器, 其本质就是维护一个存储数据的对象.

    • State : 一个存放数据的容器 (一个对象)
    const initState = {
      count: 0,
    }
    • Action : 一个 want to do 的过程 (计划要做一个什么样的操作)

      • ActionType是对Action的描述, 也是连接ActionReducer的桥梁
      • 本质上是一个由ActionTypepayload(数据)组成的对象
    export const increaseConstant = 'INCREASE' // ActionType
    
    {
       type: increaseConstant,
       payload,
    }  // Action
    • Reducer : 一个 to do 的过程 (执行Action计划的操作)
    case increaseConstant: // 当 ActionType 为 'INCREASE' 时, 执行count++
      return {
        ...state,
        count: payload + 1
      }

    第二问 why ? 为什么要使用Redux

    当你不知道是否需要使用Redux的时候, 那就是不需要使用.

    下面一组动图很好的描述了一个应用程序的开发过程, 及何时需要Redux.
    图片来源及原文链接

    • 游戏初期阶段, 数据单向传递, 父传子

    image

    • 游戏进入中期, 开始有少量非父子组件间需要通讯一些数据

    image

    • 游戏进入后期, 开始需要大量的数据通讯

    image

    • 此时, 就是Redux的用武之地了, 使用Redux后流程如下

    image

    第三问 how to ? 怎么使用Redux

    在说使用方法之前, 我们先来从头到尾模拟一个Redux的过程, 只要理解原理, 使用起来那不是小菜一碟.

    Let's go, come on baby!

    image

    下面我们就用一个简单的计数器的例子来模拟实现一个Redux的过程:

    创建一个count组件

    import React, { useState } from 'react'
    
    const CountItem = (props) => {
      const {
        count,
        increase,
      } = props
    
      return (
        <>
          {count}
          <button onClick={increase}>Count++</button>
        </>
      )
    }
    
    const Count = () => {
      const [count, setCount] = useState(0)
    
      const increase = () => {
        setCount(count + 1)
      }
    
      return (
        <CountItem
          count={count}
          increase={increase}
        />
      )
    }
    
    export default Count
    

    这样一个简单的count组件就创建好了, 现在, 我们想把对数据的操作单独封装成一个方法名为dispatch, 传递的参数为一个Action 格式: { type: xxx, payload: xxx }

    封装一个Dispatch函数

    const dispatch = (action) => {
      switch(action.type) {
        case 'INCREASE':
          return action.payload + 1
        default:
          break
      }
    }

    改写increase方法

    const increase = () => {
    -  setCount(count + 1)
    +  setCount(dispatch({type: 'INCREASE', payload: count}))
    }

    这时, 我们将action对象也抽离出来, 方便复用, 新建action.js.

    action.js => 返回action对象

    const increaseCount = (payload) => {
      return {
        type: 'INCREASE',
        payload
      }
    }

    改写increase方法

    const increase = () => {
    -  setCount(dispatch({type: 'INCREASE', payload: count}))
    +  setCount(dispatch(increaseCount(count)))
    }

    接下来我们把dispatch中的事件操作抽离到reducer中, 新建reducer.js.

    reducer.js => 进行数据操作

    const reducer = (state, action) => {
      const { type, payload } = action
      switch(type) {
        case 'INCREASE':
          return {
            ...state,
            count: payload + 1
          }
        default:
          return state
      }
    }

    改写dispatch函数

    const dispatch = (action) => {
      const state = {
        count,
      }
      
      const newState = reducer(state, action)
    
      return newState
    }

    改写increase方法

    const increase = () => {
    -  setCount(dispatch(increaseCount(count)))
    +  setCount(dispatch(increaseCount(count)).count)
    }

    接下来, 我们把set方法也拿到dispatch中, 让所有操作都在dispatch中完成.

    继续改造dispatch函数, 增加setter做映射

    const dispatch = (action) => {
      const state = {
        count,
      }
      
    +  const setter = {
    +    count: setCount
    +  }
      
      const newState = reducer(state, action)
      
    +  for (let key in newState) {
    +    setter[key](newState[key])
    +  }
    
    -  return newState
    }

    改写increase方法

    const increase = () => {
    -  setCount(dispatch(increaseCount(count)).count)
    +  dispatch(increaseCount(count))
    }

    这里我们可以看到, action.type是连接actionreducer的桥梁, 我们可以将actionType定义为常量单独保存.

    在action中增加actionType

    export const increaseConstant = 'INCREASE'
    
    // 替换 action 和 reducer 中的 'INCREASE' 为 increaseConstant

    基于现有场景, 如果我们有另一个功能, 而目前的reducer并不能帮助我们很好的把不同的功能划分开来, 我们改造一下reducer, 改造成一个对象, 用对象的key去区分功能.

    改写reducer

    const reducer = {
      count(state, action) {
        const { type, payload } = action
        switch(type) {
          case increaseConstant:
            return payload + 1
          default:
            break
        }
      },
    }

    这时我们要遍历reducer, 找到正确的key, 才能让程序正确执行, 我们新建combineReducers.js来完成这步操作.

    combineReducers

    const combineReducers = (reducer) => {
      return (state, action) => {
        let ret = {}
    
        for (let key in reducer) {
          ret[key] = reducer[key](state[key], action)
        }
        
        return {
          ...state,
          ...ret,
        }
      }
    }

    继续改下dispatch函数, 使其支持当前格式reducer.

    改写dispatch

    -  const newState = reducer(state, action)
    +  const newState = combineReducers(reducer)(state, action)

    至此, 一个redux的实现过程就完成了, 接下来, 我们实际用一用redux. 其实, 当完成上述操作的时候, 怎么用就已经说的差不多了.

    image

    Redux + React 使用

    action, reducer, Count组件同上, Count组件需要简单改写下.

    新建store.js

    import { createStore, combineReducers } from 'redux'
    import reducer from './recuder'
    
    const initState = {
      count: 0,
    }
    
    const store = createStore(
      combineReducers(reducer),
      initState,
    )
    
    export default store

    新建app.jsx, 引入store

    import { Provider } from 'react-redux'
    import store from './store'
    
    const App = () => {
      return (
        <Provider store={store}>
          <Count />
        </Provider>
      )
    }
    
    export default App

    改写Count组件

    import React from 'react'
    import { connect } from 'react-redux'
    import { increaseCount } from './action'
    
    const CountItem = (props) => {
      const {
        count,
        increase,
      } = props
    
      return (
        <>
          {count}
          <button onClick={increase}>Count++</button>
        </>
      )
    }
    
    const Count = (props) => {
      const {
        count,
        dispatch,
      } = props
    
      const increase = () => {
        dispatch(increaseCount(count))
      }
    
      return <CountItem count={count} increase={increase} />
    }
    
    export default connect(
      (state) => {
        return state
      },
      (dispatch) => {
        return { dispatch }
      }
    )(Count)
    

    接下来, 我们改写成hook的写法

    改写Count组件

    import React from 'react'
    - import { connect } from 'react-redux'
    + import { useSelector, useDispatch } from 'react-redux'
    import { increaseCount } from './action'
    
    const CountItem = (props) => {
      const {
        count,
        increase,
      } = props
    
      return (
        <>
          {count}
          <button onClick={increase}>Count++</button>
        </>
      )
    }
    
    const Count = () => {
    -  const {
    -    count,
    -    dispatch,
    -  } = props
      
    +  const count = useSelector(state => state.count)
    +  const dispatch = useDispatch()
    
      const increase = () => {
        dispatch(increaseCount(count))
      }
    
      return <CountItem count={count} increase={increase} />
    }
    
    - export default connect(
    -  (state) => {
    -      return state
    -  },
    -  (dispatch) => {
    -    return { dispatch }
    -  }
    - )(Count)
    
    + export default Count
    

    至此, 本篇文章就到此结束了, 大家可以写个复杂一点的组件练习下, 比如todoList, 手动滑稽. 逃了逃了, 我要去timi去了, 我的小伙伴还等我带他上白银呢, 一帮混子, 带不动.

    image

    查看原文

    赞 14 收藏 9 评论 8

    袁钰涵 发布了文章 · 1月25日

    当 AI 可以无痕换脸,一次面部信息的泄露,将会带来无法想象的危机

    近日,出现了一例用 AI 技术换脸实施的网络诈骗。

    1 月 10 日,小李的大学同学小王通过 QQ 联系小李,并问其借钱,小李谨慎地要求对方进行视频通话,想确认是否是小王再选择借钱,视频接通后,对方的确是小王,虽然视频中小王并没有说话,但小李还是打消了顾虑把钱借给了她。后来联系小王时,发现小王 QQ 被盗,小王对此事完全不知情。

    民警判断小李遇到了网络诈骗,诈骗团伙使用 AI 换脸,把小王的脸放到了一个有医院背景的人身上,以此骗取了小李的信任。

    小李回忆,与小王视频过程中,小王的脸也是只有轻微晃动,没有太大表情变化,甚至没有说话,这些其实都是疑点,不过当时的她没有太过注意。

    面部表情变化不大,脸部轻微晃动,也许在面部信息被窃取时,小王只是进行了眨眼、摇头一系列我们经常会遇到的实名认证操作,甚至没有意识到面部信息被窃取,脸就开始被用以犯罪了。

    换脸技术真的这么神奇吗?

    早在 2019 年便有一款成熟的换脸软件横空出世,名字非常简单,叫“ZAO”,源自“陌陌”,软件中用户不需要动态视频,只需要上传一张照片便能无痕替换掉各种韩剧、日剧、欧美剧中的男女主脸部,把其替换成自己,人脸合成效果足以以假乱真。

    不过此时的以假乱真不够成熟,只能在视频中看不出替换痕迹,仿佛是此人演的电视剧,但是为了保证自然度,无论用户上传的照片长相如何,都是套入了明星的脸型当中,于是出现许多视频“美则美矣,不似本人”。

    两年后的今天,根据小李遇到的诈骗案件我们可以发现,诈骗团伙并非是用小王的照片随意套入视频人物脸型,毕竟小李能确认这是同学的脸,大概率是使用小王的动态视频,截取脸部替换到了医院中另一个人身上,由此可见 AI 换脸技术在进一步成熟,以假乱真能力进一步提高,从“不似本人”进入到了“就是本人”的阶段。

    人们多少次因使用APP提取面部信息而掉进坑里

    image.png

    当初“ZAO”这款 APP 出现时,一大批网友为了能套入剧中男女主面部,体验一把与男神、女神“合作演戏”的快乐,纷纷下载 APP,毫无戒心地交出了自己的照片,一度出现了“造视频”的拥堵情况。

    但随着 APP 使用范围扩大,许多人注意到了其用户协议内容:

    如果您把用户内容中的脸换成您或其他人的脸,您同意或确保肖像权利人同意授予“ZAO”极其关联公司全球范围内完全免费、不可撤销、永久、可转授和可再许可的权利。

    仔细一看,这个协议完全就是把用户的脸当作免费资源,日后无论是用以任何用途,用户都不得拒绝,且不说视频平台可能会用以 AI 生成侵害用户名誉的视频,一旦公司出售用户面部信息,这些信息后续流入何处便与公司无关,而用户无法找到侵害肖像权的源泉。

    除了使用 APP 外,也有明星莫名被 AI 换脸,套入某个视频中,说出本身没有说过的话,视频流传出来仿佛成为了“远古珍贵视频”“明星人设崩塌证据”,即使明星进行了举报与辟谣,但难以保证多年后这类视频又成为“黑料实锤”。

    在中国人“眼见为实”的传统下,脸部信息的重要性更是不言而喻,无论我们是明星还是小透明,在这个 AI 越来越智能化的时代,都应该拥有对面部信息的保护意识。

    技术进步的同时,法律也在进步,但在法律之外,我们需要保护好自己

    2021 年 1 月 1 日,民法典正式实行后,针对肖像权保护的专门规范:?公民享有肖像权,未经本人同意,不得以营利为目的使用公民的肖像。

    《最高人民法院关于贯彻执行〈中华人民共和国民法通则〉若干问题的意见(试行)》(以下简称《民通意见》)第139条载明:?以营利为目的,未经公民同意利用其肖像做广告、商标、装饰橱窗等,应当认定为侵犯公民肖像权的行为。

    上述规范同时强调了肖像权侵权认定的两个要素——未经权利人同意以及以营利为目的,导致诉讼中被告常以行为的非营利性质主张不构成侵权。

    日常生活中,我们也需要注意保护个人信息,虽然法律的出台让不法商家少了许多空子可钻,但遇到诈骗此类已造成经济损失的行为,即使是把犯罪分子抓获,我们的损失也无法再回来。

    除了面部信息外,我们的定位信息、身份证信息、银行卡信息都需要注意保护,现在越来越多的 APP 会获取用户信息加以利用,于是出现普通美颜软件需要定位授权,视频软件需要摄像头可打开的授权,游戏软件需要读取手机照片等信息,还有面对软件要求实名认证时,需要判断实名认证的必要性以及注意其用户协议中是否注明将保护用户面部信息。

    即使我们只是普通用户,甚至有网友调侃“我都没有经济,谈何经济损失”,但技术能用以犯罪的用途我们是无法想象的,只有出现案件时才能后知后觉,日常生活中注意个人隐私、保护个人信息,这种做法与熟知法律一样重要。

    segmentfault 公众号

    查看原文

    赞 0 收藏 0 评论 0

    袁钰涵 发布了文章 · 1月24日

    Elastic更改开源协议抵制“白嫖”AWS回应,浪潮UBML开源,支持RISC-V架构的Android系统来了| 思否技术周刊

    ?值班编辑:袁钰涵


    1、AWS 开源:与社区一起逐步实现真正开源的 Elasticsearch

    近日,Elastic 在官网发文称将对 Elasticsearch 和 Kibana 在许可证方面进行了重大的更改,由开源 Apache 2.0 许可证改为采用 Elastic License 和 SSPL(服务器端公共许可证)。

    对于 Elastic 的这一决策,AWS 在 AWS 开源博客官方博客发表文章《Stepping up for a truly open source Elasticsearch》 — Elastic 正在破坏开放源代码本身的定义,而 AWS 将加紧创建和维护由开源 Elasticsearch 和 Kibana 获得 Apache 许可 2.0 版(ALv2)许可的分支。


    2、浪潮 UBML 低代码建模体系正式开源

    2021年1月20日,UBML 项目代码正式对外开放。目前开放的代码有建模标准(UBML-Standard)及 UBML-Models,包括面向后端开发的核心模型BE(Business-Entity)、VO(View-Model)和服务模型中的 EAPI(External-API)。项目团队在开放原子开源基金会的孵化过程中,将继续开放出更多组件以丰富生态的发展,欢迎更多的朋友共同参与。

    UBML(Unified-Business-Modeling-Language)是一种基于领域特定语言的、用于快速构件应用软件的低代码开发建模语言,是浪潮 iGIX 企业数字化能力平台的核心低代码建模体系。UBML 是开放原子开源基金会旗下的孵化项目。

    UBML 作为低代码开发平台的开发语言,是低代码开发平台的核心基础,包含开发语言无关性的建模标准(UBML-Standard),内置了基于 UBML 标准的全栈业务模型(UBML-Models),并提供了可与模型进行全生命周期交互的开发服务与套件(UBML-SDK)及支撑模型运行的运行时框架(UBML-Runtime)。未来,UBML 将引入更多低代码开发工具(UBML-Designer)等,形成完整的低代码开发平台。

    UBML 解决了什么问题?

    随着数字化转型成为主流,软件作为数字化转型的业务载体,其需求量发生了井喷式增长。Gartner 预计,2021 年市场对于应用开发的需求将五倍于 IT 公司的产能。为填补这一产量缺口,低代码/零代码技术是目前唯一可行的解决方案,必然会有越来越多企业引入这一技术。

    低代码开发是一种软件快速开发方式,可以让开发人员通过少量代码甚至零代码完成业务应用的开发。UBML 作为低代码开发平台的核心基础,致力于解决传统代码开发模式下成本高、门槛高,开发速度、灵活性、敏捷性差的问题。可以提高生产力,实现降本增效,为企业数字化转型提供动能。


    3、腾讯多环境配置及抓包调试平台 Nohost 正式开源

    腾讯开源公众号发布消息称由腾讯 IMWeb 前端团队打造的一个多环境配置及抓包调试平台正式开源。

    据悉 Nohost 覆盖了研发过程中开发联调、测试、产品体验三大阶段,提供跨端代理工具、支持一键切换体验测试环境、请求 mock、实时/历史抓包、代理转发等能力,解决了测试环境管理粗糙、易冲突、联调效率慢、配置复杂等问题,支撑了腾讯在线教育近年来数以万计的需求研发,提升了研发过程中联调体验环节的效率。

    Nohost 本身是一个通用化设计、可扩展的方案,在腾讯内部以及业界各大前端团队均有较为广泛的使用。在腾讯内有超过 80 个前端团队直接部署了 Nohost 或者基于 Nohost 能力扩展的应用,覆盖使用用户群体超过 1000 人。业界上也有多个公司(小赢科技、网易游戏、字节跳动、转转二手车等)的前端团队独立部署使用。

    更多配置或者更高级的应用,可以去 Nohost git 和Nohost 官网了解:
    官网:https://nohost.pro/

    4、开源服务器被曝 7 大漏洞!黑客可发起 DNS 缓存中毒攻击并远程执行恶意代码

    网络安全研究人员在 Dnsmasq 发现了 7 个漏洞,这是一个流行的开源软件服务器,用于缓存域名系统(DNS)响应。

    这 7 个漏洞被以色列研究公司 JSOF 统称为“ DNSpooq”,与之前披露的 DNS 架构的弱点相呼应,使得 Dnsmasq 服务器无法抵御一系列攻击。恶意攻击者可以发起 DNS 缓存中毒攻击并远程执行恶意代码。

    研究人员在今天发表的一份报告中指出: “我们发现,Dnsmasq 很容易受到偏离路径攻击者(也就是没有观察到 DNS 转发器与 DNS 服务器之间通信的攻击者)的 DNS 缓存中毒攻击。”

    由于这 7 个漏洞,攻击者可以在几秒钟到几分钟的时间内同时攻击多个域名,而且没有任何特殊操作。此外,安全研究人员还发现,Dnsmasq 的许多实例被错误地配置为侦听 WAN 接口,黑客可以直接发起网络攻击。

    Dnsmasq 是 DNS 伪装的缩写,是一个轻量级软件,具有 DNS 转发功能,用于本地缓存 DNS 记录,从而减少了上游名称服务器的负载,提高了性能。

    JSOF发现,截至 2020 年 9 月,大约有 100 万个 Dnsmasq 漏洞实例,该软件包含在 Android 智能手机中,以及数百万个来自 Cisco、Aruba、Technicolor、Redhat、Siemens、Ubiquiti 和 Comcast 的路由器和其他网络设备。


    5、CentOS停止维护后,TencentOS Server 要接过接力棒

    12月8号,CentOS官方博客发布一条题目为「CentOS Project shifts focus to CentOS Stream」的消息。内容为 CentOS 以前是作为上游供应商的下游版本存在(在上游供应商之后收到补丁和更新),而现在它将转移到上游版本(包含上游供应商之前测试补丁和更新)。

    大致意思是「在未来将从CentOS项目转移重心到CentOS Stream中」。也就是说CentOS 生命周期将被终止,CentOS将在不久后停止维护,不再更新,「免费」的 RHEL 再也没有了。。

    CentOS 作为 RHEL 的社区 fork 版本,被称为最稳定的发行版,也是世界上使用量最多的服务器发行版之一。虽然短时间内正在使用 CentOS 系统的服务器没有什么影响,但现在正在使用 CentOS 的用户们也要尽早考虑下服务器替换系统了。

    TencentOS Server

    TencentOS Serverr ( 又名Tencent Linux 简称Tlinux) 为我们带来一个不错的替代方案,它的用户态基于RHEL,是腾讯针对云场景研发的 Linux 操作系统,提供了专门的功能特性和性能优化,为云服务器实例中的应用程序提供高性能,且安全可靠的运行环境。

    Tencent Linux 不仅使用免费,在 CentOS(及发行版)上开发的应用程序也直接在 Tencent Linux 上运行,用户还可持续获得腾讯云的更新维护和技术支持,目前安装量已经超过了500万。

    TencentOS Server相比CentOS和Ubuntu等发行版的还有很多优势:

    • 经过腾讯大量内部业务十多年的验证和打磨;
    • 顶级内核专家团队的支持;
    • 包含关键的性能优化和针对云与容器场景的定制特性;
    • 强大的运营支持团队,可获得顶尖的商业支持;
    • 支持多计算架构,且提供企业级的稳定性和支持服务;
    内核代码已在 GitHub 开源:
    https://github.com/Tencent/Te...

    6、阿里平头哥成功将 Android 10 移植到 RISC-V 架构上

    近日,阿里巴巴旗下平头哥半导体公司(T-Head)于其官方芯片开放社区正式发文宣布,支持 RISC-V 架构的 Android 系统来了。

    ICE EVB是T-Head开发的基于XuanTie C910的高性能SoC板。ICE SoC已集成3个XuanTie C910内核(RISC-V 64)和1个GPU内核;优势是运行速度快、性价比高。

    该芯片可提供4K @ 60 HEVC / AVC / JPEG解码能力,以及各种用于控制和数据交换的高速接口和外设;适用于3D图形,视觉AI和多媒体处理。

    此端口基于android10-release(platform / manifest 1c222b02bde285fe1272b4440584750154d3882d)。现在T-HEAD开发的所有源代码都包含在资源库中,对此项目感兴趣的任何人都可以按照以下说明重现仿真器环境。

    代码已在 GitHub 开源:
    https://github.com/T-head-Sem...

    7、成功打造一个开源软件的秘诀在于开源社区

    健康的开源软件项目不能一蹴而就,应该慢慢培养。而培养的秘诀就在于开源社区的建设,如何吸引开源开发者?如何留住开源开发者?如何规避和解决与开源开发者的冲突?如何保持社区发展?以上这些问题是建设开源社区时不得不面对的。

    Joe Brockmeier(红帽)在LinuxCon 北美大会上分享了丰富的实践智慧值得我们学习。

    演讲节选:

    社区这个词出现的频率不断升高,很多人对它只有一个模糊的概念:「构建社区是一件好事」。

    但它到底意味着什么呢?Brockmeier 说:「我与许多不同的公司和项目合作过,他们经常表达出想要一个社区」,当我问道:「你想要什么样的社区?哪类人是你的目标人群?谁对你很重要?社区努力的方向是什么?」他们又往往不能回答。


    8、OSS Capital 创始人:为了开放的未来

    OSS Capital 的创始人兼首席执行官 Joseph Jacks 已经投身开源事业多年,他曾在一篇文章中总结了自己职业生涯,并表示将在未来几十年继续为开源贡献力量。

    以下是 Joseph Jacks 这篇文章的编译节选:

    • 为什么使用 COSS 一词?

    因为我不喜欢“开源公司”这个词,这个术语是错误的。开源实际上意味着非常特定的东西:当应用到软件源代码时,开源意味着几乎所有人都可以随时查看、运行、修改和分发代码。从这种意义上说,开源实际上是关于启用无权限的权利,而不是其他任何事情。同时,开源不代表免费。

    实际上公司的本质不是开源,因此,把一家公司称为“开源”是是矛盾的,这就像说羽毛是重的一样。一家公司绝不可能在任何时候让它的核心代码被外界看的,否则会让任何人对其进行“修改”或“商业化”。实际上,公司必须在本质上进行区分,以便找到合适的产品市场,聘用合适的人员,找到合适的投资者,有效地确定合适的策略等等。

    我认为商业开源(COSS)应该有自己的类别。从根本上来讲,基于 OSS 核心技术的公司在各个层面上都是不同的。因此,我认为世界需要一种新的原则来理解、描述和研究这种公司。我们需要一种新的术语、新的思维方式,来激励更多人去理解为什么这种开放核心的方法在整体上比旧的模式更好。

    • 直接OSS价值获取

    在未来的几十年中,我相信将会发生两件事:

    1.OSS 将继续推动绝大多数真正的基本数字技术的价值创造,以及越来越多的物理技术(原子的世界)的价值。

    2.价值捕获的钟摆将逐渐朝着另一个方向摆动—使世界朝着直接获取开源价值的方向发展,这要归功于一种新的小规模但呈指数级增长的商业类别的兴起:商业开源软件,我称之为“COSS”。


    9、微软开发工具产品副总裁 Amanda :2021 软件开发趋势

    近日,微软开发人员工具产品副总裁 Amanda Silver 在微软官方博客上更新了主题为“2021 年及以后的软件开发”博客文章。

    Amanda Silver 在文中讨论了如何在前所未有的需求中帮助发展和支持开发人才,提高开发人员的包容性和速度,并帮助工程团队通过开源和低代码工具进行扩展等问题,并对微软未来的软件开发计划进行了详细的阐述。

    以下为 Amanda Silver 文章的编译节选:

    2020 年是颠覆性的一年,企业与员工和客户之间的联系几乎在一夜之间发生了转变。企业开始迅速转向远程办公,在几个月内经历了原本可能需要数年的数字化转型。

    对于软件开发团队来说,他们迎来了对新功能的紧急要求,以便与客户和社区进行数字化互动。这种转变在很大程度上得益于成为“digital first responders”的开发人员的支持。开发人员将工作负载转移到云中,并找到了新的方法,可以在任何地方更快地编码、协作和发布软件。

    虽然我们看到的许多变化都是软件开发团队一直以来在进行的工作,但它们在疫情爆发期间开始飞速发展。2021 年,是时候反思这些巨大的变化,并考虑在我们过渡到混合工作环境时它们将产生的持久变化。这也是思考这些变化将如何影响软件开发的未来以及我们如何在共同构建更具弹性的未来中发挥作用的机会。

    image.png

    查看原文

    赞 0 收藏 0 评论 0

    袁钰涵 赞了文章 · 1月22日

    AWS 开源:与社区一起逐步实现真正开源的 Elasticsearch

    近日,Elastic 在官网发文称将对 Elasticsearch 和 Kibana 在许可证方面进行了重大的更改,由开源 Apache 2.0 许可证改为采用 Elastic License 和 SSPL(服务器端公共许可证)。

    对于 Elastic 的这一决策,AWS 在 AWS 开源博客官方博客发表文章《Stepping up for a truly open source Elasticsearch》 — Elastic 正在破坏开放源代码本身的定义,而 AWS 将加紧创建和维护由开源 Elasticsearch 和 Kibana 获得 Apache 许可 2.0 版(ALv2)许可的分支。

    以下为 AWS 开源博客发表的文章全文翻译。


    上周,Elastic 宣布他们将改变软件许可策略,将不再以 Apache License 2.0 版本(ALv2)发布 Elasticsearch 和Kibana 的新版本。取而代之的是,新版本的软件将在 Elastic License(限制了软件的使用方式)或 Server Side Public License(有一些限制让很多开源社区无法接受)下提供。这意味着 Elasticsearch 和 Kibana 将不再是开源软件。为了确保这两个软件包的开源版本仍然可用并得到很好的支持,包括在我们自己的产品中,我们今天宣布 AWS 将出面创建并维护一个 ALv2 授权的开源 Elasticsearch 和 Kibana 的分叉。

    这对 Elasticsearch 社区的 Open Distro 意味着什么?

    我们在 2019 年推出了 Open Distro for Elasticsearch,为客户和开发人员提供功能齐全的 Elasticsearch 发行版,提供ALv2授权软件的所有自由。Open Distro for Elasticsearch 是一个 100% 开源的发行版,它提供了几乎每个 Elasticsearch 用户或开发者都需要的功能,包括支持网络加密和访问控制。在构建 Open Distro 的过程中,我们遵循了 "上游先行 "的推荐开源开发实践。所有对Elasticsearch 的改动都以上游 pull requests 的形式发送(#42066, #42658, #43284, #43839, #53643, #57271, #59563, #61400, #64513),然后我们将 Elastic 提供的 "oss "构建包含在我们的发行版中。这确保了我们与上游开发者和维护者合作,而不是创建一个软件的 "fork"。

    选择分叉一个项目并不是一个轻率的决定,但是当一个社区的需求出现分歧时,这可能是一条正确的前进道路--就像这里的情况一样。开源软件的一个重要好处是,当这样的事情发生时,如果开发者有足够的动力,他们已经拥有了所有需要的权利,可以自己接手工作。这里有很多成功的案例,比如 Grafana 就是从 Kibana 3 的分叉中产生的。

    当AWS决定提供一个基于开源项目的服务时,我们确保我们有能力并准备好在必要时自己维护它。AWS 带来了多年与这些代码库合作的经验,同时也为 Elasticsearch 和 Apache Lucene(Elasticsearch构建的核心搜索库)做出了上游代码贡献--仅 2020 年就有超过 230 个Lucene 贡献。

    我们对 Elasticsearch 和 Kibana 的分叉将基于最新的 ALv2 授权代码库,7.10 版本。我们将在未来几周内发布新的 GitHub 仓库。随着时间的推移,这两个版本将被包含在现有的 Open Distro 发行版中,取代 Elastic 提供的 ALv2 构建。我们将长期参与其中,并将以促进健康和可持续的开源实践的方式开展工作--包括实现与贡献者社区共享项目治理。

    这对亚马逊 Elasticsearch 服务客户意味着什么?

    您可以放心,无论是 Elastic 的许可证变更,还是我们分叉的决定,都不会对您目前享受的 Amazon Elasticsearch 服务(Amazon ES)产生任何负面影响。今天,我们在 Amazon ES 上提供了 18 个版本的Elasticsearch,这些版本都不会受到许可证变更的影响。

    未来,Amazon ES 将由 Elasticsearch 和 Kibana 的新分叉提供支持。我们将继续提供新功能、修复和增强功能。我们致力于提供兼容性,以消除您更新客户端或应用程序代码的需要。就像我们今天所做的那样,我们将为您提供一个无缝的升级路径到新版本的软件。

    这一变化不会减缓我们为客户提供的增强速度。如果有的话,一个社区拥有的 Elasticsearch 代码库为我们提供了新的机会,使我们在提高稳定性、可扩展性、弹性和性能方面的进展更快。

    这对开源社区意味着什么?

    开发者出于许多原因而接受开放源码软件,其中最重要的原因可能是可以自由地在他们希望的地方和方式使用该软件。

    自 1998 年 "开源 "一词被提出以来,它就有了特定的含义。Elastic 关于 SSPL 是 "自由开放 "的说法是误导和错误的。他们试图宣称开源的好处,同时又在削去开源本身的定义。他们对 SSPL 的选择掩盖了这一点。SSPL 是一个非开源许可证,它的设计看起来像一个开源许可证,模糊了两者之间的界限。正如 Fedora 社区所说的那样,"[将 SSPL 视为'自由'或'开源'会导致[一个]阴影笼罩在 FOSS 生态系统的所有其他许可证上。"

    2018 年 4 月,当 Elastic 将他们的专有授权软件与 ALv2 代码共同混合时,他们在 "We Opened X-Pack "中承诺。"我们没有改变Elasticsearch、Kibana、Beats 和 Logstash 的任何 Apache 2.0 代码的授权--我们永远不会改变。" 上周,在违背了这一承诺之后,Elastic 更新了同一页面,并在脚注中写道:"情况有变"。

    Elastic 知道他们做的事情很蹊跷。社区已经告诉他们这一点(例如,见BrasseurQuinnDeVaultJacob)。这也是为什么他们觉得有必要写一个额外的虚张声势的博客(在他们最初的许可证更改博客之上),试图将他们的行为解释为 "AWS 让我们这么做"。大多数人并没有被愚弄。我们没有让他们做任何事情。他们认为,限制他们的许可证将锁定其他人提供托管 Elasticsearch 服务,这将让 Elastic 建立更大的业务。当然 Elastic 有权改变他们的许可证,拥有自己的决定。

    同时,我们对我们与 Open Distro for Elasticsearch 一起踏上的长期旅程感到兴奋。我们期待着为 Elasticsearch 和 Kibana 提供一个使用 ALv2 许可证的真正的开源选择,并与社区一起建设和支持这个未来。

    image.png

    查看原文

    赞 4 收藏 0 评论 0

    袁钰涵 赞了文章 · 1月22日

    vue3实战笔记 | 快速入门??

    前言

    vue3正式版已经发布好几个月了。相信有不少人早已跃跃欲试,这里根据这几天的项目经验罗列几点在项目中可能用到的知识点跟大家分享总结,在展开功能点介绍之前,先从一个简单的demo帮助大家可以快速入手新项目??

    案例??

    在正式介绍之前,大家可以先跑一下这个 demo 快速熟悉用法
    <template>
      <div>
        <el-button type="primary" @click="handleClick">{{
          `${vTitle}${state.nums}-${staticData}`
        }}</el-button>
        <ul>
          <li v-for="(item, index) in state.list" :key="index"> {{ item }} </li>
        </ul>
      </div>
    </template>
    
    <script lang="ts">
      import { defineComponent, reactive, ref, watch, onMounted, computed, nextTick } from 'vue';
      interface State {
        nums: number;
        list: string[];
      }
    
      export default {
        setup() {
          const staticData = 'static';
          let title = ref('Create');
          const state = reactive<State>({
            nums: 0,
            list: [],
          });
    
          watch(
            () => state.list.length,
            (v = 0) => {
              state.nums = v;
            },
            { immediate: true }
          );
          const vTitle = computed(() => '-' + title.value + '-');
    
          function handleClick() {
            if (title.value === 'Create') {
              title.value = 'Reset';
              state.list.push('小黑');
            } else {
              title.value = 'Create';
              state.list.length = 2;
            }
          }
    
          const getList = () => {
            setTimeout(() => {
              state.list = ['小黄', '小红'];
            }, 2000);
            nextTick(() => {
              console.log('nextTick');
            });
          };
    
          onMounted(() => {
            getList();
          });
    
          return {
            staticData,
            state,
            handleClick,
            title,
            vTitle,
          };
        },
      };
    </script>

    效果如下??

    vue3生命周期

    vue3的生命周期函数只能用在setup()里使用,变化如下??
    vue2vue3
    beforeCreatesetup
    createdsetup
    beforeMountonBeforeMount
    mountedonMounted
    beforeUpdateonBeforeUpdate
    updatedonUpdated
    beforeDestroyonBeforeUnmount
    destroyedonUnmounted
    activatedonActivated
    deactivatedonDeactivated

    扩展

    1. 可以看出来vue2的beforeCreatecreated变成了setup
    2. 绝大部分生命周期都是在原本vue2的生命周期上带上了on前缀

    使用

    在setup中使用生命周期:

    import {  onMounted } from 'vue';
    
    export default {
      setup() {
        onMounted(() => {
          // 在挂载后请求数据
          getList();
        })
      }
    };

    vue3常用api

    上述案例中使用了一些常用的api,下面带大家一一认识下我们的新朋友

    setup()

    setup函数是一个新的组件选项。作为在组件内使用Composition API的入口点。从生命周期钩子的视角来看,它会在beforeCreate钩子之前被调用,所有变量、方法都在setup函数中定义,之后return出去供外部使用

    该函数有2个参数:

    • props
    • context

    其中context是一个上下文对象,具有属性(attrsslotsemitparentroot),其对应于vue2中的this.$attrsthis.$slotsthis.$emitthis.$parentthis.$root

    setup也用作在tsx中返回渲染函数:

      setup(props, { attrs, slots }) {
        return () => {
          const propsData = { ...attrs, ...props } as any;
          return <Modal {...propsData}>{extendSlots(slots)}</Modal>;
        };
      },
    *注意:this关键字在setup()函数内部不可用,在方法中访问setup中的变量时,直接访问变量名就可以使用。

    扩展

    为什么props没有被包含在上下文中?

    1. 组件使用props的场景更多,有时甚至只需要使用props
    2. 将props独立出来作为一个参数,可以让TypeScript对props单独做类型推导,不会和上下文中其他属性混淆。这也使得setup、render和其他使用了TSX的函数式组件的签名保持一致。

    reactive, ref

    reactiveref都是vue3中用来创建响应式数据的api,作用等同于在vue2中的data,不同的是他们使用了ES6Porxy API解决了vue2 defineProperty 无法监听数组和对象新增属性的痛点

    用法

    <template>
      <div class="contain">
        <el-button type="primary" @click="numadd">add</el-button>
        <span>{{ `${state.str}-${num}` }}</span>
      </div>
    </template>
    
    <script lang="ts">
      import { reactive, ref } from 'vue';
      interface State {
        str: string;
        list: string[];
      }
    
      export default {
        setup() {
          const state = reactive<State>({
            str: 'test',
            list: [],
          });
          //ref需要加上value才能获取
          const num = ref(1);
          const numadd = () => {
            num.value++;
          };
          return { state, numadd, num };
        },
      };
    </script>

    效果如下??

    区别

    • 使用时在setup函数中需要通过内部属性.value来访问ref数据,return出去的ref可直接访问,因为在返回时已经自动解套;reactive可以直接通过创建对象访问
    • ref接受一个参数,返回响应式ref对象,一般是基本类型值(StringNmuberBoolean 等)或单值对象。如果传入的参数是一个对象,将会调用 reactive 方法进行深层响应转换(此时访问ref中的对象会返回Proxy对象,说明是通过reactive创建的);引用类型值(ObjectArray)使用reactive

    toRefs

    将传入的对象里所有的属性的值都转化为响应式数据对象(ref)

    使用reactive return 出去的值每个都需要通过reactive对象 .属性的方式访问非常麻烦,我们可以通过解构赋值的方式范围,但是直接解构的参数不具备响应式,此时可以使用到这个api(也可以对props中的响应式数据做此处理)

    将前面的例子作如下??修改使用起来更加方便:

    <template>
      <div class="contain">
        <el-button type="primary" @click="numadd">add</el-button>
    -    <span>{{ `${state.str}-${num}` }}</span>
    +    <span>{{ `${str}-${num}` }}</span>
      </div>
    </template>
    
    <script lang="ts">
      import { reactive, ref, toRefs } from 'vue';
      interface State {
        str: string;
        list: string[];
      }
    
      export default {
        setup() {
          const state = reactive<State>({
            str: 'test',
            list: [],
          });
          //ref需要加上value才能获取
          const num = ref(1);
          const numadd = () => {
            num.value++;
          };
    -      return { state, numadd, num };
    +      return { ...toRefs(state), numadd, num };
        },
      };
    </script>

    toRef

    toRef 用来将引用数据类型或者reavtive数据类型中的某个值转化为响应式数据

    用法

    • reactive数据类型
         /* reactive数据类型 */
          let obj = reactive({ name: '小黄', sex: '1' });
          let state = toRef(obj, 'name');
    
          state.value = '小红';
          console.log(obj.name); // 小红
          console.log(state.value); // 小红
    
          obj.name = '小黑';
          console.log(obj.name); // 小黑
          console.log(state.value); // 小黑
    • 引用数据类型
    <template>
      <span>ref----------{{ state1 }}</span>
      <el-button type="primary" @click="handleClick1">change</el-button>
      <!-- 点击后变成小红 -->
      <span>toRef----------{{ state2 }}</span>
      <el-button type="primary" @click="handleClick2">change</el-button>
      <!-- 点击后还是小黄 -->
    </template>
    
    <script>
      import { ref, toRef, reactive } from 'vue';
      export default {
        setup() {
          let obj = { name: '小黄' };
          const state1 = ref(obj.name); // 通过ref转换
          const state2 = toRef(obj, 'name'); // 通过toRef转换
          
          const handleClick1 = () => {
            state1.value = '小红';
            console.log('obj:', obj); // obj:小黄
            console.log('ref', state1); // ref:小红
          };
          
          const handleClick2 = () => {
            state2.value = '小红';
            console.log('obj:', obj); // obj:小红
            console.log('toRef', state2); // toRef:小红
          };
          return { state1, state2, handleClick1, handleClick2 };
        },
      };
    </script>

    小结

    • ref 是对原数据的拷贝,响应式数据对象值改变后会同步更新视图,不会影响到原始值。
    • toRef 是对原数据的引用,响应式数据对象值改变后不会改变视图,会影响到原始值。

    isRef

    判断是否是ref对象,内部是判断数据对象上是否包含__v_isRef属性且值为true。
        setup() {
          const one = ref(0);
          const two = 0;
          const third = reactive({
            data: '',
          });
          let four = toRef(third, 'data');
          const { data } = toRefs(third);
          
          console.log(isRef(one)); // true
          console.log(isRef(data)); // true
          console.log(isRef(four)); // true
          console.log(isRef(two)); // false
          console.log(isRef(third)); // false
        }

    unref

    如果参数为ref,则返回内部原始值,否则返回参数本身。内部是val = isRef(val) ? val.value : val的语法糖。
        setup() {
          const hello = ref('hello');
          console.log(hello); // { __v_isRef: true,value: "hello"... }
          const newHello = unref(hello);
          console.log(newHello); // hello
        }

    watch, watchEffect

    watch

    watch侦听器,监听数据变化

    用法和vue2有些区别

    语法为:watch(source, callback, options)

    • source:用于指定监听的依赖对象,可以是表达式,getter函数或者包含上述两种类型的数组(如果要监听多个值)
    • callback:依赖对象变化后执行的回调函数,带有2个参数:newValoldVal。如果要监听多个数据每个参数可以是数组 [newVal1, newVal2, ... newValN][oldVal1, oldVal2, ... oldValN]
    • options:可选参数,用于配置watch的类型,可以配置的属性有 immediate(立即触发回调函数)、deep(深度监听)
          let title = ref('Create');
          let num = ref(0);
          const state = reactive<State>({
            nums: 0,
            list: [],
          });
          
          // 监听ref
          watch(title, (newValue, oldValue) => {
             /* ... */
          });
    
          // 监听reactive
          watch(
            // getter
            () => state.list.length,
            // callback
            (v = 0) => {
              state.nums = v;
            },
             // watch Options
            { immediate: true }
          );
          
          // 监听多个ref
          watch([title, num], ([newTitle, newNum], [oldTitle, oldNum]) => {
            /* ... */
          });      
          
          // 监听reactive多个值
          watch([() => state.list, () => state.nums], ([newList, newNums], [oldList, oldvNums]) => {
            /* ... */
          });
    我们可以向上面一样将多个值的监听拆成多个对单个值监听的watch。这有助于我们组织代码并创建具有不同选项的观察者;watch方法会返回一个stop()方法,若想要停止监听,便可直接执行该stop函数
    watchEffect
    立即执行传入的一个函数,并响应式追踪其依赖,并在其依赖变更是重新运行该函数.
    <template>
      <div class="contain">
        <el-button type="primary" @click="numadd">add</el-button>
        <span>{{ num }}</span>
      </div>
    </template>
    
    <script lang="ts">
      import { ref, watchEffect } from 'vue';
      export default {
        setup() {
          const num = ref(1);
          const numadd = () => {
            num.value++;
          };
    
          watchEffect(() => {
            console.log(num.value); // 1,2,3...
          });
          return { numadd, num };
        },
      };
    </script>

    可以看到在组件初始化的时候该回调函数立即执行了一次,同时开始自动检测回调函数里头依赖的值,并在依赖关系发生改变时自动触发这个回调函数,这样我们就不必手动传入依赖特意去监听某个值了

    computed

    传入一个getter函数,返回一个默认不可手动修改的ref对象.
        setup() {
          let title = ref('Create');
          const vTitle = computed(() => '-' + title.value + '-');
          
          function handleClick() {
            if (title.value === 'Create') {
              title.value = 'Reset';
            } else {
              title.value = 'Create';
            }
          }
          }

    反转字符串:

    setup() {
        const state = reactive({
          value: '',
          rvalue: computed(() =>
            state.value
              .split('')
              .reverse()
              .join('')
          )
        })
        return toRefs(state)
      }

    provide, inject

    provide()inject()用来实现多级嵌套组件之间的数据传递,父组件或祖先组件使用 provide()向下传递数据,子组件或子孙组件使用inject()来接收数据
    // 父组件
    <script>
    import {provide} from 'vue'
    export default {
        setup() {
            const obj = ref('johnYu')
            // 向子孙组件传递数据provide(名称,数据)
            provide('name', obj)
        }
    }
    </script>
    
    // 孙组件
    <script>
    import {inject} from 'vue'
    export default {
        setup() {    
            // 接收父组件传递过来的数据inject(名称)
            const name = inject('name') // johnYu
            return {name}
        }
    }
    </script>

    getCurrentInstance

    getCurrentInstance方法用于获取当前组件实例,仅在setup和生命周期中起作用
    import { getCurrentInstance, onBeforeUnmount } from 'vue';
    
    const instance = getCurrentInstance();
    // 判断当前组件实例是否存在
    if (instance) {
        onBeforeUnmount(() => {
            /* ... */
         });
     }
    通过instance中的ctx属性可以获得当前上下文,通过这个属性可以使用组件实例中的各种全局变量和属性

    $Refs

    为了获得对模板中元素或组件实例的引用,我们可以同样使用ref并从setup()返回它
    <template>
      <div ref="root">This is a root element</div>
    </template>
    
    <script>
      import { ref, onMounted } from 'vue'
    
      export default {
        setup() {
            // 获取渲染上下文的引用
          const root = ref(null)
    
          onMounted(() => {
            // 仅在初次渲染后才能获得目标元素
            console.log(root.value) // <div>This is a root element</div>
          })
    
          return {
            root
          }
        }
      }
    </script>

    .sync

    在vue2.0中使用.sync实现prop的双向数据绑定,在vue3中将它合并到了v-model

    vue2.0

        <el-pagination
          :current-page.sync="currentPage1"
        >
        </el-pagination>

    vue3.0

        <el-pagination
          v-model:current-page="currentPage1"
        >
        </el-pagination>

    v-slot

    Child.vue

    <template>
      <div class="child">
        <h3>具名插槽</h3>
        <slot name="one" />
        <h3>作用域插槽</h3>
        <slot :data="list" />
        <h3>具名作用域插槽</h3>
        <slot name="two" :data="list" />
      </div>
    </template>
    
    <script>
    export default {
      data: function() {
        return {
          list: ['zhangsan', 'lisi']
        }
      }
    }
    </script>

    vue2用法

    <template>
      <div>
        <child>
          <div slot="one">
            <span>菜单</span>
          </div>
          <div slot-scope="user">
            <ul>
              <li v-for="(item, index) in user.data" :key="index">{{ item }}</li>
            </ul>
          </div>
          <div slot="two" slot-scope="user">
            <div>{{ user.data }}</div>
          </div>
        </child>
      </div>
    </template>

    vue3用法

    新指令v-slot统一slotslot-scope单一指令语法。速记v-slot可以潜在地统一作用域和普通插槽的用法。
    <template>
      <div>
        <child>
          <template v-slot:one>
            <div><span>菜单</span></div>
          </template>
          <template v-slot="user">
            <ul>
              <li v-for="(item, index) in user.data" :key="index">{{ item }}</li>
            </ul>
          </template>
          <template v-slot:two="user">
            <div>{{ user.data }}</div>
          </template>
          <!-- 简写 -->
          <template #two="user">
          <div>{{ user.data }}</div>
          </template>
        </child>
      </div>
    </template>

    Composition API 结合vuex4, Vue Router 4

    createStore,useStore,useRouter,useRoute

    vuex4中通过createStore创建Vuex实例,useStore可以获取实例,作用等同于vue2.0中的this.$store;

    Vue Router 4 useRouter可以获取路由器,用来进行路由的跳转,作用等同于vue2.0的this.$router,useRoute就是钩子函数相当于vue2.0的this.$route

    store/index.ts

    import {createStore} from 'vuex';
    const store = createStore({
      state: {
        user: null,
      },
      mutations: {
        setUser(state, user) {
          state.user = user;
        }
      },
      actions: {},
      modules: {}
    });

    router/index.ts

    import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
    import { scrollBehavior } from './scrollBehaviour.ts';
    
    const routes: Array<RouteRecordRaw> = [
      {
        path: '/',
        name: 'Home',
        component: () => import('/@/views/home.vue') // vite.config.vue中配置alias
      }
    ];
    
    const router = createRouter({
      history: createWebHashHistory(),
      routes,
      strict: true,
      scrollBehavior: scrollBehavior,
    });
    
    export default router;

    main.ts

    import { createApp } from 'vue';
    import App from './App.vue';
    import router from './router';
    import store from './store';
    import { getTime } from '/@/utils'
    
    const app = createApp(App);
    app.config.globalProperties.$getTime = getTime // vue3配置全局变量,取代vue2的Vue.prototype
    app.use(store).use(router)
    app.mount('#app');

    App.vue

    import { reactive } from "vue";
    import { useRouter } from "vue-router";
    import { useStore } from "vuex";
    import { ElMessage } from 'element-plus';
    export default {
      name: "App",
      setup() {
        const store = useStore();
        const router = useRouter();
        // 用户名和密码
        const Form = reactive({
          username: "johnYu",
          password: "123456",
        });
        // 登录
        function handelLogin() {
          store.commit("setUser", {
            username: Form.username,
            password: Form.password,
          });
          ElMessage({
            type: 'success',
            message: '登陆成功',
            duration: 1500,
          });
          // 跳转到首页
          router.push({
             name: 'Home',
             params: {
               username: Form.username
             },
          });
        }
        return {
          Form,
          handelLogin
          };
      }

    home.vue

      import { useRouter, useRoute } from 'vue-router';
      import Breadcrumb from '/@/components/Breadcrumb.vue';
    
      export default defineComponent({
        name: 'Home',
        components: {
          Breadcrumb,
        },
        setup() {
          const route = useRoute();
          // 接收参数
          const username = route.params.username;
          return {username}
        }
        })

    导航守卫

    由于使用 Composition API 的原因,setup函数里面分别使用onBeforeRouteLeaveonBeforeRouteUpdate 两个新增的 API 代替vue2.0中的beforeRouteLeavebeforeRouteUpdate
      import { onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router';
       setup() {
          onBeforeRouteUpdate((to) => {
            if (to.name === 'Home'){
                /* ... */
            }
          });
       }

    useLink

    useLink它提供与router-linkv-slot API 相同的访问权限,将RouterLink的内部行为公开为Composition API函数,用于暴露底层的定制能力
    <template>
      <div ref="root">This is a root element</div>
    </template>
    
    <script>
      import { computed } from 'vue';
      import { RouterLink, useLink } from 'vue-router';
    
      export default {
        name: 'AppLink',
    
        props: {
          ...RouterLink.props,
          inactiveClass: String,
        },
    
        setup(props) {
          const { route, href, isActive, isExactActive, navigate } = useLink(props);
          const isExternalLink = computed(
            () => typeof props.to === 'string' && props.to.startsWith('http')
          );
    
          return { isExternalLink, href, navigate, isActive };
        },
      };
    </script>

    插槽 prop 的对象包含下面几个属性:

    1. href:解析后的 URL。将会作为一个 a 元素的 href attribute。
    2. route:解析后的规范化的地址。
    3. navigate:触发导航的函数。会在必要时自动阻止事件,和 router-link 同理。
    4. isActive:如果需要应用激活的 class 则为 true。允许应用一个任意的 class。
    5. isExactActive:如果需要应用精确激活的 class 则为 true。允许应用一个任意的 class。

    扩展

    样式 scoped

    vue2

    /* 深度选择器 */
    /*方式一:*/
    >>> .foo{ }
    /*方式二:*/
    /deep/ .foo{ }
    /*方式三*/
    ::v-deep .foo{ }

    vue3

    /* 深度选择器 */
    ::v-deep(.foo) {}

    .env环境扩展

    vite中的.env文件变量名一定要以VITE_前缀

    .env文件

    VITE_USE_MOCK = true

    使用:

    import.meta.env.VITE_APP_CONTEXT

    使用Composition API替换mixin

    众所周知使用mixin的时候当我们一个组件混入大量不同的mixin的时候,会存在两个非常明显的问题:命名冲突和数据来源不清晰。
    • 每个mixin都可以定义自己的propsdata,它们之间是无感的,所以很容易定义相同的变量,导致命名冲突。
    • 另外对组件而言,如果模板中使用不在当前组件中定义的变量,那么就会不太容易知道这些变量在哪里定义的,这就是数据来源不清晰。

    以这个经典的Vue 2组件为例,它定义了一个"计数器"功能:

    //counter.js
    export default {
      data() {
        return {
          count: 0
        };
      },
      methods: {
        increment() {
          this.count++;
        }
      }
    }

    用法如下:

    <template>
      <div>
        {{ count }}
        <el-button @click="increment()">add</el-button>
      </div>
    </template>
    
    <script>
    import counter from './mixins/counter'
    import getTime from './mixins/getTime'
    
    export default {
      mixins: [counter,getTime]
    }
    </script>

    假设这边我们引用了counter和getTime两个mixin,则无法确认count和increment()方法来源,并且两个mixin中可能会出现重复命名的概率

    下面是使用Composition API定义的完全相同的组件:

    // counter.ts
    import { ref } from 'vue';
    
    export default function () {
        const count = ref(0);
        function increment() {
            count.value++;
        }
        return { count, increment };
    }
    
    <template>
      <div>
        {{ count }}
        <el-button @click="increment()">add</el-button>
      </div>
    </template>
    
    <script lang="ts">
      import { defineComponent } from 'vue';
      import counter from '/@/composables/counter';
    
      export default defineComponent({
        setup() {
          const { count, increment } = counter();
          return {
            count,
            increment,
          };
        },
      });
    </script>

    总结

    使用Composition API可以清晰的看到数据来源,即使去编写更多的hook函数,也不会出现命名冲突的问题。??

    Composition API 除了在逻辑复用方面有优势,也会有更好的类型支持,因为它们都是一些函数,在调用函数时,自然所有的类型就被推导出来了,不像 Options API 所有的东西使用 this。另外,Composition API 对 tree-shaking 友好,代码也更容易压缩。vue3的Composition API会将某个逻辑关注点相关的代码全都放在一个函数里,这样当需要修改一个功能时,就不再需要在文件中跳来跳去

    参考文章 ??

    ?? 快速使用Vue3最新的15个常用API

    ?? vue 3.x 如何有惊无险地快速入门

    扩展 ??

    如果你觉得本文对你有帮助,可以查看我的其他文章??:

    ?? 10个简单的技巧让你的 vue.js 代码更优雅??

    ?? 零距离接触websocket??

    ?? Web开发应了解的5种设计模式

    ?? Web开发应该知道的数据结构

    查看原文

    赞 28 收藏 20 评论 0

    袁钰涵 赞了文章 · 1月21日

    Bash 脚本进阶,经典用法及其案例

    前言

    在linux中,Bash脚本是很基础的知识,大家可能一听脚本感觉很高大上,像小编当初刚开始学一样,感觉会写脚本的都是大神。虽然复杂的脚本是很烧脑,但是,当我们熟练的掌握了其中的用法与技巧,再多加练习,总有一天也会成为得心应手的脚本大神。脚本在生产中的作用,想必小编我不说,大家也都知道,脚本写的6,可以省下很多复杂的操作,减轻自己的工作压力。好了,废话不多说,接下来,就是Bash脚本的用法展示。

    一、条件选择、判断

    1、条件选择if

    (1)用法格式

    if 判断条件 1 ; then

      条件为真的分支代码

    elif 判断条件 2 ; then

      条件为真的分支代码

    elif 判断条件 3 ; then

      条件为真的分支代码

    else

      以上条件都为假的分支代码

    fi

    逐条件进行判断,第一次遇为“真”条件时,执行其分支,而后结束整个if。

    (2)经典案例:

    ① 判断年纪

    #!/bin/bash
    read -p "Please input your age: " age
    if [[ $age =~ [^0-9] ]] ;then
        echo "please input a int"
        exit 10
    elif [ $age -ge 150 ];then
        echo "your age is wrong"
        exit 20
    elif [ $age -gt 18 ];then
        echo "good good work,day day up"
    else
        echo "good good study,day day up"
    fi

    分析:请输入年纪,先判断输入的是否含有除数字以外的字符,有,就报错;没有,继续判断是否小于150,是否大于18。

    ② 判断分数

    #!/bin/bash
    read -p "Please input your score: " score
    if [[ $score =~ [^0-9] ]] ;then
        echo "please input a int"
        exit 10
    elif [ $score -gt 100 ];then
        echo "Your score is wrong"
        exit 20
    elif [ $score -ge 85 ];then
        echo "Your score is very good"
    elif [ $score -ge 60 ];then
        echo "Your score is soso"
    else
        echo "You are loser"
    fi

    分析:请输入成绩,先判断输入的是否含有除数字以外的字符,有,就报错;没有,继续判断是否大于100,是否大于85,是否大于60。

    2、条件判断case

    (1)用法格式

    case $name in;

    PART1)

      cmd

    ;;

    PART2)

      cmd

    ;;

    *)

      cmd

    ;;

    esac

    注意:case 支持glob 风格的通配符:

      *: 任意长度任意字符

      ?: 任意单个字符

      [] :指定范围内的任意单个字符

      a|b: a 或b

    (2)案例:

    判断yes or no

    #!/bin/bash
    read -p "Please input yes or no: " anw
    case $anw in
    [yY][eE][sS]|[yY])
        echo yes
        ;;
    [nN][oO]|[nN])
        echo no
        ;;
    *)
        echo false
        ;;
    esac

    分析:请输入yes or no,回答Y/y、yes各种大小写组合为yes;回答N/n、No各种大小写组合为no。

    二、四个循环

    1、for

    (1)用法格式

    ① forname in 列表 ;do

      循环体

    done

    for (( exp1; exp2; exp3 )) ;do

      cmd

    done

    exp1只执行一次,相当于在for里嵌了while

    ③ 执行机制:依次将列表中的元素赋值给“变量名”; 每次赋值后即执行一次循环体; 直到列表中的元素耗尽,循环结束

    列表的表示方法,可以glob 通配符,如{1..10} 、*.sh ;也可以变量引用,如: seq 1 $name

    (2)案例

    ① 求出(1+2+...+n)的总和

    sum=0
    read -p "Please input a positive integer: " num
    if [[ $num =~ [^0-9] ]] ;then
        echo "input error"
    elif [[ $num -eq 0 ]] ;then
        echo "input error"
    else
        for i in `seq 1 $num` ;do
            sum=$[$sum+$i]
        done
        echo $sum
    fi
    unset zhi 

    分析:sum初始值为0,请输入一个数,先判断输入的是否含有除数字以外的字符,有,就报错;没有判断是否为0,不为0进入for循环,i的范围为1~输入的数,每次的循环为sum=sum+i,循环结束,最后输出sum的值。

    ② 求出(1+2+...+100)的总和

    for (( i=1,num=0;i<=100;i++ ));do
            [ $[i%2] -eq 1 ] && let sum+=i
    done
    echo sum=$sum

    分析:i=1,num=0;当i<=100,进入循环,若i÷2取余=1,则sum=sum+i,i=i+1。
    2、while

    (1)用法格式

    while 循环控制条件 ;do

      循环

    done

    循环控制条件;进入循环之前,先做一次判断;每一次循环之后会再次做判断;条件为“true” ,则执行一次循环;直到条件测试状态为“false” 终止循环

    (2)特殊用法(遍历文件的每一行):while read line; do控制变量初始化
      循环体
    done < /PATH/FROM/SOMEFILE
    cat /PATH/FROM/SOMEFILE | while read line; do
      循环体
    done
    依次读取/PATH/FROM/SOMEFILE文件中的每一行,且将行赋值给变量line

    (3)案例:

    ① 100以内所有正奇数之和

    sum=0
    i=1
    while [ $i -le 100 ] ;do
    if [ $[$i%2] -ne 0 ];then
        let sum+=i
        let i++
    else
        let i++
    fi
    done
    echo "sum is $sum"

    分析:sum初始值为0,i的初始值为1;请输入一个数,先判断输入的是否含有除数字以外的字符,有,就报错;没有当i<100时,进入循环,判断 i÷2取余 是否不为0,不为0时为奇数,sum=sum+i,i+1,为0,i+1;循环结束,最后输出sum的值。

    3、until循环

    (1)用法

    unitl 循环条件 ;do

      循环

    done

    进入条件:循环条件为true ;退出条件:循环条件为false;刚好和while相反,所以不常用,用while就行。

    (2)案例

    监控xiaoming用户,登录就杀死

    until pgrep -u xiaoming &> /dev/null ;do 
    sleep 0.5 
    done 
    pkill -9 -u xiaoming

    分析:每隔0.5秒扫描,直到发现xiaoming用户登录,杀死这个进程,退出脚本,用于监控用户登录。

    4、select循环与菜单
    (1)用法
    select variable in list
    do
      循环体命令
    done
    ① select 循环主要用于创建菜单,按数字顺序排列的示菜单项将显示在标准错误上,并显示PS3 提示符,等待用户输入
    ② 用户输入菜单列表中的某个数字,执行相应的命令
    ③ 用户输入被保存在内置变量 REPLY 中
    ④ select 是个无限循环,因此要记住用 break 命令退出循环,或用 exit 按 命令终止脚本。也可以按 ctrl+c退出循环
    ⑤ select 和 经常和 case 联合使用
    ⑥ 与for循环类似,可以省略 in list, 此时使用位置参量
    (2)案例:
    生成菜单,并显示选中的价钱

    PS3="Please choose the menu: "
    select menu in mifan huimian jiaozi babaozhou quit
    do
            case $REPLY in
            1|4)
                    echo "the price is 15"
                    ;;
            2|3)
                    echo "the price is 20"
                    ;;
            5)
                    break
                    ;;
            *)
                    echo "no the option"
            esac
    done

    分析:PS3是select的提示符,自动生成菜单,选择5break退出循环。

    三、循环里的一些用法

    1、循环控制语句

    (1)语法

    continue [N]:提前结束第N层的本轮循环,而直接进入下一轮判断;最内层为第1层

    break [N]:提前结束第N层循环,最内侧为第1层

    例:while CONDTITON1; do  CMD1
    if CONDITION2; then
      continue / break
    fi
      CMD2
    done
    (2)案例:

    ① 求(1+3+...+49+53+...+100)的和

    #!/bin/bash
    sum=0
    for i in {1..100} ;do
            [ $i -eq 51 ] && continue
            [ $[$i%2] -eq 1 ] && { let sum+=i;let i++; }
    done
    echo sum=$sum

    分析:做1+2+...+100的循环,当i=51时,跳过这次循环,但是继续整个循环,结果为:sum=2449

    ② 求(1+3+...+49)的和

    #!/bin/bash
    sum=0
    for i in {1..100} ;do
            [ $i -eq 51 ] && break
            [ $[$i%2] -eq 1 ] && { let sum+=i;let i++; }
    done
    echo sum=$sum

    分析:做1+2+...+100的循环,当i=51时,跳出整个循环,结果为:sum=625

    2、循环控制shift命令

    (1)作用
    用于将参数列表list左移指定次数,最左端的那个参数就从列表中删除,其后边的参数继续进入循环
    (2)案例:

    ① 创建指定的多个用户

    #!/binbash
    if [ $# -eq 0 ] ;then
            echo "Please input a arg(eg:`basename $0` arg1)"
            exit 1
    else
            while [ -n "$1" ];do
                    useradd $1 &> /dev/null
                    shift
            done
    fi

    分析:如果没有输入参数(参数的总数为0),提示错误并退出;反之,进入循环;若第一个参数不为空字符,则创建以第一个参数为名的用户,并移除第一个参数,将紧跟的参数左移作为第一个参数,直到没有第一个参数,退出。

    ② 打印直角三角形的字符

    #!/binbash
    while (( $# > 0 ))
    do
            echo "$*"
            shift
    done

    3、返回值结果
    true 永远返回成功结果
    : null command ,什么也不干,返回成功结果
    false 永远返回错误结果
    创建无限循环
    while true ;do
      循环体
    done

    4、循环中可并行执行,使脚本运行更快

    (1)用法
    for name in 列表 ;do
    {
      循环体
    }&
    done
    wait
    (2)实例:
    搜寻自己指定ip(子网掩码为24的)的网段中,UP的ip地址

    read -p "Please input network (eg:192.168.0.0): " net
    echo $net |egrep -o "<(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])>"
    [ $? -eq 0 ] || ( echo "input error";exit 10 )
    IP=`echo $net |egrep -o "^([0-9]{1,3}.){3}"`
    for i in {1..254};do
            {
            ping -c 1 -w 1 $IP$i &> /dev/null && 
            echo "$IP$i is up" 
            }&
    
    done
    wait

    分析:请输入一个IP地址例192.168.37.234,如果格式不是0.0.0.0 则报错退出;正确则进入循环,IP变量的值为192.168.37. i的范围为1-254,并行ping 192.168.37.1-154,ping通就输出此IP为UP。直到循环结束。

    四、信号捕获trap

    1、用法格式

    trap ' 触发指令' 信号,自定义进程收到系统发出的指定信号后,将执行触发指令,而不会执行原操作

    trap '' 信号,忽略信号的操作

    trap '-' 信号,恢复原信号的操作

    trap -p,列出自定义信号操作

    信号可以3种表达方法:信号的数字2、全名SIGINT、缩写INT

    2、常用信号

    1) SIGHUP: 无须关闭进程而让其重读配置文件

    2) SIGINT: 中止正在运行的进程;相当于Ctrl+c

    3) SIGQUIT: 相当于ctrl+

    9) SIGKILL: 强制杀死正在运行的进程

    15) SIGTERM :终止正在运行的进程(默认为15)

    18) SIGCONT :继续运行

    19) SIGSTOP :后台休眠

    9 信号,强制杀死,捕获不住

    3、案例

    ① 打印0-9,ctrl+c不能终止

    #!/bin/bash
    trap 'echo press ctrl+c' 2
    for ((i=0;i<10;i++));do
            sleep 1
            echo $i
    done

    分析:i=0,当i<10,每休眠1秒,i+1,捕获2信号,并执行echo press ctrl+c

    ② 打印0-3,ctrl+c不能终止,3之后恢复,能终止

    #!/bin/bash
    trap '' 2
    trap -p
    for ((i=0;i<3;i++));do
            sleep 1
            echo $i
    done
    trap '-' SIGINT
    for ((i=3;i<10;i++));do
            sleep 1
            echo $i
    done

    分析:i=0,当i<3,每休眠1秒,i+1,捕获2信号;i>3时,解除捕获2信号。

    五、脚本小知识(持续更新)
    1、生成随机字符 cat /dev/urandom
      生成8个随机大小写字母或数字 cat /dev/urandom |tr -dc [:alnum:] |head -c 8
    2、生成随机数 echo $RANDOM
      确定范围 echo $[RANDOM%7] 随机7个数(0-6)
           echo $[$[RANDOM%7]+31] 随机7个数(31-37)
    3、echo打印颜色字
    echo -e "033[31malong033[0m" 显示红色along
    echo -e "033[1;31malong033[0m" 高亮显示红色along
    echo -e "033[41malong033[0m" 显示背景色为红色的along
    echo -e "033[31;5malong033[0m" 显示闪烁的红色along
    color=$[$[RANDOM%7]+31]
    echo -ne "033[1;${color};5m*033[0m" 显示闪烁的随机色along
    六、分享几个有意思的小脚本

    1、9x9乘法表

    #!/bin/bash
    for a in {1..9};do
            for b in `seq 1 $a`;do
                    let c=$a*$b ;echo -e "${a}x${b}=$ctc"
            done
            echo    
    done 

    2、彩色等腰三角形

    #!/bin/bash
    read -p "Please input a num: " num
    if [[ $num =~ [^0-9] ]];then
            echo "input error" 
    else
            for i in `seq 1 $num` ;do
                    xing=$[2*$i-1]
                    for j in `seq 1 $[$num-$i]`;do
                            echo -ne " "
                    done
                    for k in `seq 1 $xing`;do
                            color=$[$[RANDOM%7]+31]
                            echo -ne "033[1;${color};5m*033[0m"
                    done
                    echo
            done
    fi 

    3、国际象棋棋盘

    #!/bin/bash
    red="033[1;41m  033[0m"
    yellow="033[1;43m  033[0m"
    
    for i in {1..8};do
            if [ $[i%2] -eq 0 ];then
                    for i in {1..4};do
                            echo -e -n "$red$yellow";
                    done
                    echo
            else
                    for i in {1..4};do
                            echo -e -n "$yellow$red";
                    done
                    echo 
            fi
    done

    好了,今天的内容就到这里了,其实小编的脚本功底也是小白级别的,大家一起努力,争取进阶为脚本大神!!!

    来源:https://www.cnblogs.com/along...

    image

    查看原文

    赞 8 收藏 6 评论 0

    袁钰涵 赞了文章 · 1月21日

    TypeScript 中提升幸福感的 10 个高级技巧

    用了一年时间的 TypeScript 了,项目中用到的技术是 Vue + TypeScript 的,深感中大型项目中 TypeScript 的必要性,特别是生命周期比较长的大型项目中更应该使用 TypeScript。

    以下是我在工作中总结到的经常会用到的 TypeScript 技巧。

    1. 注释

    通过 /** */ 形式的注释可以给 TS 类型做标记提示,编辑器会有更好的提示:

    /** This is a cool guy. */
    interface Person {
      /** This is name. */
      name: string,
    }
    
    const p: Person = {
        name: 'cool'
    }

    如果想给某个属性添加注释说明或者友好提示,这种是很好的方式了。

    2. 接口继承

    和类一样,接口也可以相互继承。

    这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。

    interface Shape {
        color: string;
    }
    
    interface Square extends Shape {
        sideLength: number;
    }
    
    let square = <Square>{};
    square.color = "blue";
    square.sideLength = 10;

    一个接口可以继承多个接口,创建出多个接口的合成接口。

    interface Shape {
        color: string;
    }
    
    interface PenStroke {
        penWidth: number;
    }
    
    interface Square extends Shape, PenStroke {
        sideLength: number;
    }
    
    let square = <Square>{};
    square.color = "blue";
    square.sideLength = 10;
    square.penWidth = 5.0;

    3. interface & type

    TypeScript 中定义类型的两种方式:接口(interface)和 类型别名(type alias)。

    比如下面的 Interface 和 Type alias 的例子中,除了语法,意思是一样的:

    Interface

    interface Point {
      x: number;
      y: number;
    }
    
    interface SetPoint {
      (x: number, y: number): void;
    }

    Type alias

    type Point = {
      x: number;
      y: number;
    };
    
    type SetPoint = (x: number, y: number) => void;

    而且两者都可以扩展,但是语法有所不同。此外,请注意,接口和类型别名不是互斥的。接口可以扩展类型别名,反之亦然。

    Interface extends interface

    interface PartialPointX { x: number; }
    interface Point extends PartialPointX { y: number; }

    Type alias extends type alias

    type PartialPointX = { x: number; };
    type Point = PartialPointX & { y: number; };

    Interface extends type alias

    type PartialPointX = { x: number; };
    interface Point extends PartialPointX { y: number; }

    Type alias extends interface

    interface PartialPointX { x: number; }
    type Point = PartialPointX & { y: number; };

    它们的差别可以看下面这图或者看 TypeScript: Interfaces vs Types

    所以檙想巧用 interface & type 还是不简单的。

    如果不知道用什么,记住:能用 interface 实现,就用 interface , 如果不能就用 type 。

    4. typeof

    typeof 操作符可以用来获取一个变量或对象的类型。

    我们一般先定义类型,再使用:

    interface Opt {
      timeout: number
    }
    const defaultOption: Opt = {
      timeout: 500
    }

    有时候可以反过来:

    const defaultOption = {
      timeout: 500
    }
    type Opt = typeof defaultOption

    当一个 interface 总有一个字面量初始值时,可以考虑这种写法以减少重复代码,至少减少了两行代码是吧,哈哈~

    5. keyof

    TypeScript 允许我们遍历某种类型的属性,并通过 keyof 操作符提取其属性的名称。

    keyof 操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。

    keyofObject.keys 略有相似,只不过 keyofinterface 的键。

    const persion = {
      age: 3,
      text: 'hello world'
    }
    
    // type keys = "age" | "text"
    type keys = keyof Point;

    写一个方法获取对象里面的属性值时,一般人可能会这么写

    function get1(o: object, name: string) {
      return o[name];
    }
    
    const age1 = get1(persion, 'age');
    const text1 = get1(persion, 'text');

    但是会提示报错

    因为 object 里面没有事先声明的 key。

    当然如果把 o: object 修改为 o: any 就不会报错了,但是获取到的值就没有类型了,也变成 any 了。

    这时可以使用 keyof 来加强 get 函数的类型功能,有兴趣的同学可以看看 _.gettype 标记以及实现

    function get<T extends object, K extends keyof T>(o: T, name: K): T[K] {
      return o[name]
    }

    6. 查找类型

    interface Person {
        addr: {
            city: string,
            street: string,
            num: number,
        }
    }

    当需要使用 addr 的类型时,除了把类型提出来

    interface Address {
        city: string,
        street: string,
        num: number,
    }
    
    interface Person {
        addr: Address,
    }

    还可以

    Person["addr"] // This is Address.

    比如:

    const addr: Person["addr"] = {
        city: 'string',
        street: 'string',
        num: 2
    }

    有些场合后者会让代码更整洁、易读。

    7. 查找类型 + 泛型 + keyof

    泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

    interface API {
        '/user': { name: string },
        '/menu': { foods: string[] }
    }
    const get = <URL extends keyof API>(url: URL): Promise<API[URL]> => {
        return fetch(url).then(res => res.json());
    }
    
    get('');
    get('/menu').then(user => user.foods);

    8. 类型断言

    Vue 组件里面经常会用到 ref 来获取子组件的属性或者方法,但是往往都推断不出来有啥属性与方法,还会报错。

    子组件:

    <script lang="ts">
    import { Options, Vue } from "vue-class-component";
    
    @Options({
      props: {
        msg: String,
      },
    })
    export default class HelloWorld extends Vue {
      msg!: string;
    }
    </script>

    父组件:

    <template>
      <div class="home">
        <HelloWorld
          ref="helloRef"
          msg="Welcome to Your Vue.js + TypeScript App"
        />
      </div>
    </template>
    
    <script lang="ts">
    import { Options, Vue } from "vue-class-component";
    import HelloWorld from "@/components/HelloWorld.vue"; // @ is an alias to /src
    
    @Options({
      components: {
        HelloWorld,
      },
    })
    export default class Home extends Vue {
      print() {
        const helloRef = this.$refs.helloRef;
        console.log("helloRef.msg: ", helloRef.msg); 
      }
    
      mounted() {
        this.print();
      }
    }
    </script>

    因为 this.$refs.helloRef 是未知的类型,会报错误提示:

    做个类型断言即可:

      print() {
        // const helloRef = this.$refs.helloRef;
        const helloRef = this.$refs.helloRef as any;
        console.log("helloRef.msg: ", helloRef.msg); // helloRef.msg:  Welcome to Your Vue.js + TypeScript App
      }

    但是类型断言为 any 时是不好的,如果知道具体的类型,写具体的类型才好,不然引入 TypeScript 冒似没什么意义了。

    9. 显式泛型

    $('button') 是个 DOM 元素选择器,可是返回值的类型是运行时才能确定的,除了返回 any ,还可以

    function $<T extends HTMLElement>(id: string): T {
        return (document.getElementById(id)) as T;
    }
    
    // 不确定 input 的类型
    // const input = $('input');
    
    // Tell me what element it is.
    const input = $<HTMLInputElement>('input');
    console.log('input.value: ', input.value);

    函数泛型不一定非得自动推导出类型,有时候显式指定类型就好。

    10. DeepReadonly

    readonly 标记的属性只能在声明时或类的构造函数中赋值。

    之后将不可改(即只读属性),否则会抛出 TS2540 错误。

    与 ES6 中的 const 很相似,但 readonly 只能用在类(TS 里也可以是接口)中的属性上,相当于一个只有 getter 没有 setter 的属性的语法糖。

    下面实现一个深度声明 readonly 的类型:

    type DeepReadonly<T> = {
      readonly [P in keyof T]: DeepReadonly<T[P]>;
    }
    
    const a = { foo: { bar: 22 } }
    const b = a as DeepReadonly<typeof a>
    b.foo.bar = 33 // Cannot assign to 'bar' because it is a read-only property.ts(2540)

    最后

    大佬们,觉得有用就赞一个呗。

    挺久没写原创技术文章了,作为 2021 第一篇原创技术文章,质量应该还可以吧 ??

    笔者的年终总结在这里: 前端工程师的 2020 年终总结 - 乾坤未定,你我皆黑马,希望能带给你一点启发。

    参考文章:

    推荐阅读

    查看原文

    赞 34 收藏 20 评论 0

    袁钰涵 赞了文章 · 1月21日

    EDAS 3.0 微服务测试最佳实践

    该文章是基于阿里云商业化产品 EDAS 3.0的微服务实践,如果您的团队具备较强的微服务测试能力,那么希望我们在微服务测试方面的实践和背后的思考,可以为您提供一些参考。

    前言

    随着云原生时代的到来,越来越多的应用生在云上,长在云上,且随着越来越多的企业开始上云,云原生也是企业落地微服务的最佳伴侣。但云上应用易测性受到了很大的挑战,如何提高云上应用易测性,增强DevOps能力,是微服务测试要解决的核心问题。
    在详细讲述微服务测试之前,先给大家讲一个场景。

    上图是一个典型的企业微服务应用架构图,为了考虑安全性,云上应用通常部署在云上虚拟局域网内,统一通过网关对外暴露服务。对于负责 Product Service 应用的同学来说,我只想测试一下该应用对应的服务是否可用,他会怎么做呢?

    • 方案一:进入该应用部署所在的机器(ECS)或者容器(Pod),通过 curl 命令验证该服务是否可用。
    • 方案二:将该应用暴露给公网访问,通过本地命令行工具或者 Postman 工具验证该服务是否可用。
    • 方案三:拉一条网络专线,打通云上专有网络VPC与办公网网络,通过本地命令行工具或者 Postman 工具验证该服务是否可用。

    从以上场景,我们可以总结出云上微服务测试几点问题:

    • 云上网络拓扑复杂
    • 暴露公网访问,会出现黑客攻击,引发安全风险
    • 拉一条网络专线,浪费资源成本

    明明只想要一个简单的测试能力,成本缺如此之高。上述场景还仅仅是一个简单的调试功能,如果是压测、自动化回归、巡检等其他测试及稳定性保障手段,不仅仅要解决上述场景遇到的问题,还需要自建工具,脑补一下,都觉得成本太高,因此,我们需要微服务测试来帮助我们解决这些问题,进一步加速软件交付效率。

    为什么我们需要微服务测试

    产品能力

    提供测试、压测、自动化回归、巡检等能力,形成一个微服务测试解决方案:

    试想一下,研发同学提交代码并部署,可以使用测试工具,验证服务逻辑正确性;可以使用压测工具,验证服务性能指标;验证通过后,开始进行冒烟测试,可以使用自动化回归工具,编写冒烟用例;冒烟通过后,开始进行历史功能回归,可以使用自动化回归工具,编写回归用例;回归通过后,提交测试验收,测试只需要验证新功能,新功能验证通过后,即可提交发布。发布后,进行线上环境验证,需要回归历史功能主流程,可以使用自动化回归工具,编写主流程回归用例,新功能手工验证;主流程回归通过且新功能验证通过,代表发布完成;研发同学,可以使用巡检工具,配置线上巡检;一旦巡检告警,即可先于用户发现问题,并解决问题。我们是将阿里巴巴沉淀的测试解决方案产品化输出,帮助云上业务实现高质量地实现快速交付。

    易用且安全

    开箱即用,无需关注专有网络VPC下的网络拓扑;安全可靠,拥有在办公网下的测试体验。
    试想一下,企业为了安全隔离,研发环境、测试环境、预发环境、生产环境部署在不同的专有网络VPC内,如果用户自建测试工具,需要解决测试工具到不同环境的网络互通问题,企业IT人员明明只想要一个简单的测试工具,却因为上云之后,要解决复杂的云上网络拓扑,远远没有结束,为了能够在办公网使用该测试工具,还需要保证该测试工具能够被办公网访问,此时又面临着网络安全的考验。我们希望有一个能够开箱即用且安全可靠的方案,能够让上云的企业IT人员拥有在办公网测试体验的测试工具。

    低成本

    弹性拉起测试机/施压机,用完销毁,能够大幅度降低构建测试工具需要的机器资源及人力成本。

    试想一下,企业上云是为了降低成本,应用托管极大地降低了资源成本和运维成本,但测试成本并没有降低。企业IT人员自建测试工具需要准备测试机/施压机,该部分机器长期占用且存在闲置,资源成本开销大,尤其是在性能压测场景,资源成本开销会更大。除了资源成本外,企业IT人员还需要研发测试工具,人力成本及时间成本非常高,基本上每个企业都需要一套测试工具。我们希望有一个低成本的方案,不仅提高企业的资源利用率,同时降低企业IT人员开发和维护测试工具的成本。

    微服务生态

    云上已提供了大量的微服务产品,解决了微服务应用的托管、治理、诊断,微服务测试补齐微服务能力。

    试想一下,如何测试一个微服务接口,需要了解接口入参和出参,如果是研发同学-服务提供者,可能比较熟悉该接口,如果是测试同学,甚至是其他研发同学,可能就需要文档,甚至是口口相传,微服务治理已经可视化应用的服务契约信息,结合服务契约信息,只需按照测试需要,选择应用->框架->服务->方法,配置测试参数,即可进行测试,降低了服务契约同步的成本。

    结合上述4点,测试同学只需负责用例编写+测试验收,接口调试、接口性能水位、用例自动化均可赋能给研发同学,就像早期DevOps一样,降低研发运维之间的反馈回路,提高软件交付效率,DevTest,降低研发测试之间的反馈回路,在保证交付质量的前提下,进一步提升软件交付效率,同时主动创建巡检任务,定时监控线上服务可用率,先于用户发现问题,解决问题。

    EDAS3.0 微服务测试实践

    前提条件:微服务应用已接入EDAS3.0
    下面我们来体验一下,EDAS上如何使用微服务测试的能力。

    服务测试

    1. 登录EDAS控制台,在页面左上角选择地域;
    2. 左侧导航栏选择:微服务治理 -> Spring Cloud -> 服务测试 -> 查询服务;
    3. 单击某个服务的详情 -> 展示元数据列表;
    4. 单击某个方法的测试 -> 进入测试页面(已帮助用户填充参数模板);
    5. 点击执行即可。



    服务压测

    1. 登录EDAS控制台,在页面左上角选择地域;
    2. 左侧导航栏选择:微服务治理 -> Spring Cloud -> 服务压测 -> 创建场景;
    3. 选择需要压测的应用 -> 选择框架 -> 选择服务 -> 选择方法;
    4. 填写压测参数,点击确认;
    5. 进入压测场景列表页,点击详情;
    6. 进入压测详情页,点击启动,等待施压机准备就绪;
    7. 点击详情,进入压测性能数据报告页,实时查看性能数据;



    自动化回归

    1. 登录EDAS控制台,在页面左上角选择地域;
    2. 左侧导航栏选择:微服务治理 -> Spring Cloud -> 自动化回归 -> 创建用例;
    3. 添加步骤
    • 选择应用 -> 选择框架 -> 选择服务 -> 选择方法;
    • 填写参数;
    • 断言/出参提取;
    1. 可以添加多个步骤;
    2. 保存用例;
    3. 点击执行;
    4. 通过执行历史,查看用例是否通过;



    服务巡检

    1. 登录EDAS控制台,在页面左上角选择地域;
    2. 左侧导航栏选择:微服务治理 -> Spring Cloud -> 服务巡检 -> 创建巡检任务;
    3. 选择需要巡检的应用 -> 选择框架 -> 选择服务 -> 选择方法;
    4. 填写巡检参数及断言内容,点击确认;
    5. 进入巡检任务列表页,点击启动,即开始巡检;
    6. 巡检失败时,可以通过失败记录进行查看,也可以添加告警,通过钉钉、短信、邮件的方式告警;


    微服务测试实现细节

    工具能力

    将阿里巴巴集团内实践的测试工具产品化输出,压测、自动化回归、巡检,降低用户研发工具的成本。

    网络互通

    利用阿里云现有网络打通技术方案(ENI挂载),打通云产品专有网络VPC与用户专有网络VPC应用安装微服务 Agent 时,主动将该应用所在的网络信息(专有网络VPC,虚拟交换机VSwitch,安全组SecurityGroup)上报至服务端,根据应用所在的网络信息,即可打通云产品专有网络VPC与用户专有网络VPC,实现云产品服务直接访问用户专有网络VPC部署的服务。

    弹性资源

    云产品使用自己的资源账号购买弹性机器,安装测试工具。

    服务契约

    微服务治理已经可视化服务契约信息,微服务测试直接查询服务契约信息即可

    不止是微服务测试

    本文介绍了微服务测试的几个能力,补齐了微服务生态测试的能力,即将推出智能流量测试:提供微服务架构下的流量生产录制生产回放、生产录制线下回放、测试用例自动化生成、回归测试场景自动化覆盖等能力,助力您的应用以更低的成本轻松完成测试验证,欢迎前来体验。

    除了EDAS(企业级分布式应用服务),微服务测试能力已被MSE(微服务引擎)集成,还将被AHAS、CSB、SAE等云产品集成。将微服务测试能力作为一个基础能力被更多云产品集成,另外,将跟更多微服务产品 ARMS (应用实时监控服务)、ACM(应用配置管理)等形成联动,助力保障云上业务稳定性,让业务永远在线。

    >>原文链接

    【阿里巴巴中间件】专注于微服务、容器服务、Serverless……等云原生热门话题,关注同名公众号获取更多精彩内容和福利
    Tips: 公众号后台回复“抽奖”,试试手气?

    查看原文

    赞 3 收藏 0 评论 0

    袁钰涵 发布了文章 · 1月21日

    50 年过去了,为何 SQL 仍那么重要?

    image.png

    编译:袁钰涵 | 发自:思否编辑部

    1971 年 的 3 月,英特尔推出了世界上第一个通用微处理器,即 [Intel 4004],成本 60 美元的它,拥有约 2,300 个晶体管。

    如今,最新款的 iPhone 拥有近?120 亿个晶体管,以及略高于 60 美元的成本。

    50 年带来的改变是巨大的,但是有的东西却一直不变。

    20 世纪 90 年代有一大批编程语言开始引入,其中 1996 年引入了 Java,岁月无声,这么多年过去了,SQL 在众多编程语言中仍旧拥有着属于自己的一席之地,甚至和 50 年前一样流行。

    这篇文章将从为何引入关系数据库以及 SQL 为什么越来越流行进行分享,同时思考我们能从中学到什么。

    数据库管理的早期历史——IDS 和 CODASYL

    1962年,Charles W. Bachman 在 General Electric 的一个小团队中工作。

    一年后,Charles W. Bachman 的团队推出了人们口中说的第一个数据库管理系统——the Intergrated Data Store(IDS)。

    10年后,巴赫曼对 IDS 计算的贡献获得了被人们成为计算机界诺贝尔奖的图灵奖。

    什么是IDS?

    19 世纪 60 年代初,计算机科学刚开始成为一个学术领域。

    也是在这个时期,美国信息交换标准码(ASCII)才被引入。

    想要了解 IDS,我们需要先了解推动其发展的两个主要力量:

    一,磁盘存储的推行

    移动 RAMAC 305

    1956年,IBM 推出了第一个商用硬盘驱动器: RAMAC 305。

    以前使用磁带驱动器的时候,需要依次在磁带中移动以检索特定的数据,但磁盘驱动器的引入使程序员可以直接跳转到磁盘上的某个位置来检索和更新数据,为程序员带来了极大的便利。

    只是这样一来,开发人员必须对于磁盘上的存储位置非常了解,受限于早期操作系统中的文件管理系统 ,磁盘带来的便利仅供经验丰富的程序员享受。

    于是开发人员需要一个能简化磁盘驱动器使用的解决方案。

    二、编程语言从低级向高级的迁移

    同时,计算机科学在采用曲线上,人们开始从创新者转变为早期的采用者。早期流行像 Assembly 这样的低级编程语言,但随着对语言的进一步使用,更多程序员出于可用性的考虑,改用像 COBOL 这样的高级编程语言。

    到这个时候,我们或许能猜到 IDS 为何存在了,它解决了磁盘存储的问题,同时方便了高级编程语言的使用。

    IDS 允许开发人员使用高级编程语言(例如COBOL)来构建可从磁盘存储中输入和检索数据的应用程序,因此,IDS 作为第一个数据库管理系统获得了殊荣。

    CODASYL——数据库管理的新标准

    1969年,数据系统语言委员会(CODASYL)发布了一份报告,报告中提出了数据库管理标准,巴赫曼是这个委员会的成员,这些标准主要来自于 IDS。

    CODASYL 数据模型引入了我们今天使用的数据库管理系统的许多核心功能,其中包括了:

    • 数据定义语言(DDL)
    • 数据操作语言(DML)

    最重要的是,IDS 和 CODASYL引入了一种新的数据建模方法,该方法影响了 SQL 的最终开发,那就是——网络数据模型。

    网络数据模型与关系模型

    数据模型是描述建模世界数据的标准。

    以前的分层数据模型使用树结构来描述数据,让关系限制在了一对多的组合中。

    新的网络模型允许记录具有多个父级,从而创建了一个“图结构”,多个父级存在后,网络模型便能对多对一以及多对多的关系进行建模。

    在网络模型中,表之间的关系存储在“集”中。每组都有一个所有者和一个或多个成员?,就像一个老师会拥有一位或多位学生一般。

    网络模型的主要优点之一就是集合中的相关记录通过指针直接连接。集合是用 next、prior、owner ponters 实现的,这类似于列表的链接,方便了开发者的查阅。

    网络数据模型的 低级性质(low-level nature) 提供了性能优势,但因为每个记录都必须存储指向其先前记录和父记录的额外指针,它让我们付出了高储存成本。

    关系模型的到来

    一个关系模型的例子

    1970年,即 IDS 推出 8 年后,埃德加·科德(Edgar F. Codd)在他的开创性论文“大型共享数据库的数据关系模型”中介绍了关系模型,这篇论文也为他赢得了图灵奖。

    埃德加·科德(Codd)表明,数据库中的所有数据都可以用元组(?SQL中的行)表示,这些元组被分组为关系?(?SQL中的表)。为了描述数据库查询,他发明了一种称为元组关系演算的一阶谓词逻辑?。

    元组关系演算引入了一种用于查询数据的声明性语言。在声明性编程语言中,程序员不需要说如何执行,只需要说想做什么,目的就可达成。

    对于开发人员而言,这种新的声明性语言要容易得多。关系模型将所有数据公开,开发人员可以通过一个命令从一个表中检索所有数据,或读取一行(这要感谢查询优化器),从此人们告别了用迷宫般的指针来查找数据的日子。

    关系与网络数据模型

    规范化是分解表以消除冗余,从而减少磁盘上数据占用量的过程。关系数据库通过规范化数据减少了网络数据库具有的高存储成本?。

    但是,为了处理规范化的数据,关系数据库必须将表加载到内存中,并利用计算能力将表“联接”在一起,它又增加了 CPU 成本。

    通过介绍如何使用关系模型方便老师查找所有班级和学生,你将会明白为何有如此高的 CPU 成本了:

    当老师开始输入需要查找的数据时,数据库系统将首先检索所有相关类,然后它再进行第二次操作,检索学生数据。在这个过程中,所有的数据都将存储在内存中,在返回结果之前,将运行第三次操作把数据合并。

    关系模型和网络模型之间的性能比较

    在使用实际数据的效果案例研究中, Raima 发现网络数据库模型的插入性能是关系模型的 23 倍,查询性能更是它的 123 倍。

    那么,为什么关系数据库是领先的数据库解决方案?

    关系模型修改的时候更灵活,它的声明性语法简化了程序员的工作。

    摩尔定律让计算机的性能获得极大的提高,计算成本持续下降,最终关系模型的计算成本被提高的生产率抵消了。

    时间线拉到 50 年后的今天,以上综合种种影响下,数据中心中最价格最贵的配置就是 CPU。

    SQL 的兴起与统治

    顺着时间往下走,我们遇见了我们所喜爱的 SQL。

    埃德加·科德(Codd)发表论文 4 年后,Donald Chamberlin 和 Raymond Boyce 发表了名为“ SEQUEL:一种结构化的英语查询语言”的论文。

    他们将 SEQUEL 描述为“对表格结构的一组简单操作,等效于一阶谓词演算”。IBM看到了这种潜力,并在 19 世纪 70 年代初迅速开发了 SEQUEL 的第一个版本,将其作为 System R 的一部分。

    后来因为与英国霍克·西德利飞机公司的商标发生冲突,SEQUEL 更名为 SQL。

    1986年,美国国家标准协会(ANSI)和国际标准化组织(ISO)发布了第一个正式的 SQL 标准:SQL-86,这是 SQL 被用了十年后走出的重要一步。该标准将 SQL分为几个部分:

    数据定义语言(DDL):?用于定义和修改架构和关系的命令

    数据操作语言(DML):?用于查询,插入和删除数据库信息的命令

    交易控制:?用于指定交易时间的命令

    完整性:?用于设置数据库信息约束的命令

    视图:?用于定义视图的命令

    授权:?用于指定用户访问权限的命令

    嵌入式SQL:?指定如何以其他语言嵌入SQL的命令

    从 1974 年到今天,许多人研发了语法,试图与 SQL 争夺查询语言的市场份额。这些新语法通常迎合了当时特定的新技术:

    [Lisp-] > [CLSQL .NET-]> [LINQ] Ruby on [Rails-]> [ActiveRecord]

    35年过去了,SQL 在数据库中仍无处不在,SQL如何维持其作为查询语言的统治地位,我们又可以从它的故事中学到什么?

    SQL 50 年统治的秘诀,以及我们可以学到什么

    2017年堆栈溢出开发人员调查

    我们从巴赫曼与第一个数据库管理系统 IDS 开始这个故事,讨论了磁盘存储和高级编程的转变以及如何需要一种新的数据处理方式,然后 CODASYL 进行了标准化的数据库管理,IDS 和 CODASYL 引入了新的网络数据模型,最后埃德加·科德(Codd)放弃了关系模型。

    八年就发生了以上的种种变化。

    在接下来的50年中,SQL如何成功地坚持下去?我认为有四个主要原因:

    一、建立在第一性原理上

    第一性原理是不能从任何其他命题或假设中推论得出的基本命题。例如,将碳氢化合物与氧气结合以产生化学反应,这是为每辆汽车的内燃机提供动力的最基本原理。

    1970年,埃德加·科德(Codd)为数据库创建了一个新的第一性原理:元组关系演算。这个新逻辑保住了关系模型与 SQL 的产生。在这三者中,元组关系演算是化学反应,关系模型是内燃机,SQL 是汽车。

    二、布什内尔定律

    仅凭第一性原理还不能保证成功,汇编程序与程序员可以一键输入的 1 和 0 某种程度上非常接近,但它仍被 COBOL(以及后来的C)取代了,因为它缺少了可用性。

    从网络到关系模型的转变,我们也看到了同样的故事。网络模型具有更快的性能,但是如今,每个公司都使用关系数据库,因为它初期使用起来非常简单。

    “最好的游戏总是容易上手但很难掌握”——诺兰·布什内尔(雅达利公司创始人)

    这句话中,诺兰·布什内尔(Nolan Bushnell)说出了让人们使用新产品的秘密。

    在难学又难掌握的汇编中,SQL 找到了完美的平衡。借助约 10 条 SQL 命令,任何人都可以从 20% 的知识中获得 80% 的学习效率。但是,要做到母版、索引、视图、优化,后续学习则还有很长的路要走。

    三、倾听反馈与适应环境

    查询语言不是永恒的整体,而是随时间变化适应环境的标准组,年复一年中,SQL 标准不断地调整,吸收用户的反馈完善着自己。

    从最初的构想开始统计,我们已经看到 10 种不同的 SQL 标准,每一个标准都有着很重要的更新。其中有 3 个更新非常大,这里列了出来:

    1、1999 年,增加了正则表达式匹配,递归查询(例如,传递闭包),触发器,对过程和流程控制语句的支持,非标量类型(数组)和一些面向对象的功能(例如结构化类型)。支持在 Java(SQL / OLB)中嵌入 SQL,反之亦然(SQL / JRT)。

    2、2003 年,引入了与 XML 相关的功能(SQL / XML),窗口函数,标准化序列以及具有自动生成的值(包括身份列)的列。

    3、2016 年,添加行模式匹配,多态表函数,JSON。

    SQL 还创造了其他产品可以使用的 Rails 构架。在 SQL 中没有强制语法,它只是为每个数据库提供了方便他们自行组建(T-SQL,MySQL,PSQL等)的标准。

    四、采用API

    SQL 成功的最终秘诀是应用程序编程接口(API)的兴起。API 的抽象化底层实现、仅公开开发人员需要的对象与操作,大大简化了编程。

    Hadoop 在 2006 年引入了分布式文件系统(HDFS),最初 SQL 语法无法访问,但 2013 年,Apache 创建了 Apache Impala,自此开发人员可以使用 SQL 查询 HDFS 数据库。API 让 SQL 以其特殊语法适应着新技术。

    总结

    SQL 是当今使用最广泛的编程语言之一,它始于现代计算的曙光,并由 2 位图灵奖获得者赋予了它生命。

    SQL 能保持其主导地位是因为:建立于第一性原理、布什内尔定律的出现、它的自适应性和 API 被使用。

    如果你有其他看法,可以留言评论,我们一同分享 SQL 成功多年的原因是什么。

    segmentfault 公众号

    查看原文

    赞 7 收藏 1 评论 2

    袁钰涵 赞了文章 · 1月20日

    YYDS: Webpack Plugin开发


    @[toc]
    ??作为一名踏足前端时间不长的小开发必须得聊一聊webpack,刚开始接触webpack时第一反应这是啥(⊙_⊙)? 怎么这么复杂,感觉好难呀,算了先不管这些!时间是个好东西呀,随着对前端工程化的实践和理解慢慢加深,跟webpack接触越来越多,最终还是被ta折服,不禁高呼一声“webpack yyds(永远滴神)!

    ??去年年中就想写一些关于webpack的文章,由于各种原因耽搁了(主要是觉得对webpack理解还不够,不敢妄自下笔);临近年节,时间也有些了,与其 "摸鱼"不如摸摸webpack,整理一些"年货"分享给需要的xdm!后续会继续写一些【 Webpack】系列文章,xdm监督···

    导读

    ??本文主要通过实现一个cdn优化的插件CdnPluginInject介绍下webpack的插件plugin开发的具体流程,中间会涉及到html-webpack-plugin插件的使用、vue/cli3+项目中webpack插件的配置以及webpack相关知识点的说明。全文大概2800+字,预计耗时5~10分钟,希望xdm看完有所学、有所思、有所输出!

    注意:文章中实例基于vue/cli3+工程展开!

    一、cdn常规使用

    index.html:

    <head>
      ···
    </head>
    <body>
      <div id="app"></div>
      <script data-original="https://cdn.bootcss.com/vuex/3.1.0/vuex.min.js"></script>
      <script data-original="https://cdn.bootcss.com/vue-router/3.0.2/vue-router.min.js"></script>
      ···
    </body>

    vue.config.js:

    module.exports = {
      ···
      configureWebpack: {
        ···
        externals: {
          'vuex': 'Vuex',
          'vue-router': 'VueRouter',
          ···
        }
      },

    二、开发一个webpack plugin

    webpack官网如此介绍到:插件向第三方开发者提供了 webpack 引擎中完整的能力。使用阶段式的构建回调,开发者可以引入它们自己的行为到 webpack 构建流程中。创建插件比创建 loader 更加高级,因为你将需要理解一些 webpack 底层的内部特性来实现相应的钩子!

    一个插件由以下构成:

    • 一个具名 JavaScript 函数。
    • 在它的原型上定义 apply 方法。
    • 指定一个触及到 webpack 本身的 事件钩子
    • 操作 webpack 内部的实例特定数据。
    • 在实现功能后调用 webpack 提供的 callback。
    // 一个 JavaScript class
    class MyExampleWebpackPlugin {
    // 将 `apply` 定义为其原型方法,此方法以 compiler 作为参数
     apply(compiler) {
       // 指定要附加到的事件钩子函数
         compiler.hooks.emit.tapAsync(
           'MyExampleWebpackPlugin',
           (compilation, callback) => {
             console.log('This is an example plugin!');
             console.log('Here’s the `compilation` object which represents a single build of assets:', compilation);
             // 使用 webpack 提供的 plugin API 操作构建结果
             compilation.addModule(/* ... */);
             callback();
           }
         );
     }
    }

    三、cdn优化插件实现

    思路:

    • 1、创建一个具名 JavaScript 函数(使用ES6class实现);
    • 2、在它的原型上定义 apply 方法;
    • 3、指定一个触及到 webpack 本身的事件钩子(此处触及compilation钩子:编译(compilation)创建之后,执行插件);
    • 4、在钩子事件中操作index.html(将cdnscript标签插入到index.html中);
    • 5、在apply方法执行完之前将cdn的参数放入webpack外部扩展externals中;
    • 6、在实现功能后调用 webpack 提供的 callback

    实现步骤:

    1、创建一个具名 JavaScript 函数(使用ES6class实现)

    ??创建类cdnPluginInject,添加类的构造函数接收传递过来的参数;此处我们定义接收参数的格式如下:

    modules:[
      {
        name: "xxx",    //cdn包的名字
        var: "xxx",    //cdn引入库在项目中使用时的变量名
        path: "http://cdn.url/xxx.js" //cdn的url链接地址
      },
      ···
    ]

    定义类的变量modules接收传递的cdn参数的处理结果:

    class CdnPluginInject {
      constructor({
        modules,
      }) {
        // 如果是数组,将this.modules变换成对象形式
        this.modules = Array.isArray(modules) ? { ["defaultCdnModuleKey"]: modules } : modules; 
      }
     ···
    }
    module.exports = CdnPluginInject;

    2、在它的原型上定义 apply 方法

    插件是由一个构造函数(此构造函数上的 prototype 对象具有 apply 方法)的所实例化出来的。这个 apply 方法在安装插件时,会被 webpack compiler 调用一次。apply 方法可以接收一个 webpack compiler 对象的引用,从而可以在回调函数中访问到 compiler 对象

    cdnPluginInject.js代码如下:

    class CdnPluginInject {
      constructor({
        modules,
      }) {
        // 如果是数组,将this.modules变换成对象形式
        this.modules = Array.isArray(modules) ? { ["defaultCdnModuleKey"]: modules } : modules; 
      }
      //webpack plugin开发的执行入口apply方法
      apply(compiler) {
        ···
      }
    
    module.exports = CdnPluginInject;

    3、指定一个触及到 webpack 本身的事件钩子

    ??此处触及compilation钩子:编译(compilation)创建之后,执行插件。

    ??compilation compiler 的一个hooks函数, compilation 会创建一次新的编译过程实例,一个 compilation 实例可以访问所有模块和它们的依赖,在获取到这些模块后,根据需要对其进行操作处理!

    class CdnPluginInject {
      constructor({
        modules,
      }) {
        // 如果是数组,将this.modules变换成对象形式
        this.modules = Array.isArray(modules) ? { ["defaultCdnModuleKey"]: modules } : modules; 
      }
      //webpack plugin开发的执行入口apply方法
      apply(compiler) {
        //获取webpack的输出配置对象
        const { output } = compiler.options;
        //处理output.publicPath, 决定最终资源相对于引用它的html文件的相对位置
        output.publicPath = output.publicPath || "/";
        if (output.publicPath.slice(-1) !== "/") {
          output.publicPath += "/";
        }
        //触发compilation钩子函数
        compiler.hooks.compilation.tap("CdnPluginInject", compilation => { 
         ···
      }
    }
    
    module.exports = CdnPluginInject;

    4、在钩子事件中操作index.html

    ??这一步主要是要实现 cdnscript标签插入到index.html ;如何实现呢?在vue项目中webpack进行打包时其实是使用html-webpack-plugin生成.html文件的,所以我们此处也可以借助html-webpack-plugin对html文件进行操作插入cdn的script标签。

    // 4.1 引入html-webpack-plugin依赖
    const HtmlWebpackPlugin = require("html-webpack-plugin");
    
    class CdnPluginInject {
      constructor({
        modules,
      }) {
        // 如果是数组,将this.modules变换成对象形式
        this.modules = Array.isArray(modules) ? { ["defaultCdnModuleKey"]: modules } : modules; 
      }
      //webpack plugin开发的执行入口apply方法
      apply(compiler) {
        //获取webpack的输出配置对象
        const { output } = compiler.options;
        //处理output.publicPath, 决定最终资源相对于引用它的html文件的相对位置
        output.publicPath = output.publicPath || "/";
        if (output.publicPath.slice(-1) !== "/") {
          output.publicPath += "/";
        }
        //触发compilation钩子函数
        compiler.hooks.compilation.tap("CdnPluginInject", compilation => { 
          // 4.2 html-webpack-plugin中的hooks函数,当在资源生成之前异步执行
          HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration
           .tapAsync("CdnPluginInject", (data, callback) => {   // 注册异步钩子
                //获取插件中的cdnModule属性(此处为undefined,因为没有cdnModule属性)
              const moduleId = data.plugin.options.cdnModule;  
              // 只要不是false(禁止)就行
              if (moduleId !== false) {    
                 // 4.3得到所有的cdn配置项
                let modules = this.modules[                    
                    moduleId || Reflect.ownKeys(this.modules)[0] 
                ];
                if (modules) {
                  // 4.4 整合已有的js引用和cdn引用
                  data.assets.js = modules
                    .filter(m => !!m.path)
                    .map(m => {
                      return m.path;
                    })
                    .concat(data.assets.js);
                  // 4.5 整合已有的css引用和cdn引用
                  data.assets.css = modules
                    .filter(m => !!m.style)
                    .map(m => {
                      return m.style;
                    })
                    .concat(data.assets.css); 
                }
              }
                // 4.6 返回callback函数
              callback(null, data);
            });
      }
    }
    
    module.exports = CdnPluginInject;

    接下来逐步对上述实现进行分析:

    • 4.1、引入html-webpack-plugin依赖,这个不用多说;
    • 4.2、调用html-webpack-plugin中的hooks函数,在html-webpack-plugin中资源生成之前异步执行;这里由衷的夸夸html-webpack-plugin的作者了,ta在开发html-webpack-plugin时就在插件中内置了很多的hook函数供开发者在调用插件的不同阶段嵌入不同操作;因此,此处我们可以使用html-webpack-pluginbeforeAssetTagGeneration对html进行操作;
    • 4.3、 在beforeAssetTagGeneration中,获取得到所有的需要进行cdn引入的配置数据;
    • 4.4、 整合已有的js引用和cdn引用;通过data.assets.js可以获取到compilation阶段所有生成的js资源(最终也是插入index.html中)的链接/路径,并且将需要配置的cdn的path数据(cdn的url)合并进去;
    • 4.5、 整合已有的css引用和cdn引用;通过data.assets.css可以获取到compilation阶段所有生成的css资源(最终也是插入index.html中)的链接/路径,并且将需要配置的css类型cdn的path数据(cdn的url)合并进去;
    • 4.6、 返回callback函数,目的是告诉webpack该操作已经完成,可以进行下一步了;

    5、设置webpack外部扩展externals

    ??在apply方法执行完之前还有一步必须完成:将cdn的参数配置到外部扩展externals中;可以直接通过compiler.options.externals获取到webpack中externals属性,经过操作将cdn配置中数据配置好就ok了。

    6、 callback

    ??返回callback,告诉webpack CdnPluginInject插件已经完成;

    // 4.1 引入html-webpack-plugin依赖
    const HtmlWebpackPlugin = require("html-webpack-plugin");
    
    class CdnPluginInject {
      constructor({
        modules,
      }) {
        // 如果是数组,将this.modules变换成对象形式
        this.modules = Array.isArray(modules) ? { ["defaultCdnModuleKey"]: modules } : modules; 
      }
      //webpack plugin开发的执行入口apply方法
      apply(compiler) {
        //获取webpack的输出配置对象
        const { output } = compiler.options;
        //处理output.publicPath, 决定最终资源相对于引用它的html文件的相对位置
        output.publicPath = output.publicPath || "/";
        if (output.publicPath.slice(-1) !== "/") {
          output.publicPath += "/";
        }
        //触发compilation钩子函数
        compiler.hooks.compilation.tap("CdnPluginInject", compilation => { 
          // 4.2 html-webpack-plugin中的hooks函数,当在资源生成之前异步执行
          HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration
           .tapAsync("CdnPluginInject", (data, callback) => {   // 注册异步钩子
                //获取插件中的cdnModule属性(此处为undefined,因为没有cdnModule属性)
              const moduleId = data.plugin.options.cdnModule;  
              // 只要不是false(禁止)就行
              if (moduleId !== false) {    
                 // 4.3得到所有的cdn配置项
                let modules = this.modules[                    
                    moduleId || Reflect.ownKeys(this.modules)[0] 
                ];
                if (modules) {
                  // 4.4 整合已有的js引用和cdn引用
                  data.assets.js = modules
                    .filter(m => !!m.path)
                    .map(m => {
                      return m.path;
                    })
                    .concat(data.assets.js);
                  // 4.5 整合已有的css引用和cdn引用
                  data.assets.css = modules
                    .filter(m => !!m.style)
                    .map(m => {
                      return m.style;
                    })
                    .concat(data.assets.css); 
                }
              }
                // 4.6 返回callback函数
              callback(null, data);
            });
          
          // 5.1 获取externals
             const externals = compiler.options.externals || {};
          // 5.2 cdn配置数据添加到externals
          Reflect.ownKeys(this.modules).forEach(key => {
            const mods = this.modules[key];
            mods
              .forEach(p => {
              externals[p.name] = p.var || p.name; //var为项目中的使用命名
            });
          });
          // 5.3 externals赋值
          compiler.options.externals = externals; //配置externals
          
          // 6 返回callback
          callback();
      }
    }
    
    module.exports = CdnPluginInject;

    ??至此,一个完整的webpack插件CdnPluginInject就开发完成了!接下来使用着试一试。

    四、cdn优化插件使用

    ??在vue项目的vue.config.js文件中引入并使用CdnPluginInject

    cdn配置文件CdnConfig.js:

    /*
     * 配置的cdn
     * @name: 第三方库的名字
     * @var: 第三方库在项目中的变量名
     * @path: 第三方库的cdn链接
     */
    module.exports = [
      {
        name: "moment",
        var: "moment",
        path: "https://cdn.bootcdn.net/ajax/libs/moment.js/2.27.0/moment.min.js"
      },
      ···
    ];

    configureWebpack中配置:

    const CdnPluginInject = require("./CdnPluginInject");
    const cdnConfig = require("./CdnConfig");
    
    module.exports = {
      ···
      configureWebpack: config => {
        //只有是生产山上线打包才使用cdn配置
        if(process.env.NODE.ENV =='production'){
          config.plugins.push(
            new CdnPluginInject({
              modules: CdnConfig
            })
          )
          }
      }
      ···
    }

    chainWebpack中配置:

    const CdnPluginInject = require("./CdnPluginInject");
    const cdnConfig = require("./CdnConfig");
    
    module.exports = {
      ···
      chainWebpack: config => {
        //只有是生产山上线打包才使用cdn配置
        if(process.env.NODE.ENV =='production'){
          config.plugin("cdn").use(
            new CdnPluginInject({
              modules: CdnConfig
            })
          )
          }
      }
      ···
    }

    ??通过使用CdnPluginInject

    • 1、通过配置实现对cdn优化的管理和维护;
    • 2、实现针对不同环境做cdn优化配置(开发环境直接使用本地安装依赖进行调试,生产环境适应cdn方式优化加载);

    五、小结

    ??看完后肯定有webpack大佬有一丝丝疑惑,这个插件不就是 webpack-cdn-plugin 的乞丐版!CdnPluginInject只不过是本人根据webpack-cdn-plugin源码的学习,结合自己项目实际所需修改的仿写版本,相较于webpack-cdn-plugin将cdn链接的生成进行封装,CdnPluginInject是直接将cdn链接进行配置,对于选择cdn显配置更加简单。想要进一步学习的xdm可以看看webpack-cdn-plugin的源码,经过作者的不断的迭代更新,其提供的可配置参数更加丰富,功能更加强大(再次膜拜)。

    重点:整理不易,觉得还可以的xdm记得 一键三连 哟!

    文章参考

    查看原文

    赞 14 收藏 4 评论 0

    袁钰涵 关注了用户 · 1月20日

    这是上帝的杰作 @master_yoda

    //loading...

    关注 127

    袁钰涵 赞了文章 · 1月20日

    Taro 入门

    简介

    Taro 是一个遵循 React 语法规范的开放式跨端跨框架解决方案,支持使用 React/Vue/Nerv 等框架来开发微信/京东/百度/支付宝/字节跳动/ QQ 小程序/H5 等应用,内置了UI组件,还有物料市场,只编写一套代码就能够适配到多端。

    Tara 遵循 React 语法,集成的是 Nerv 框架。

    Nerv是一款基于virtual dom技术的类React UI框架,它基于React标准,拥有和React一致的API与生命周期。得益于与React保持一致API的特性,Nerv可以无缝兼容到React的相关生态,例如react-routerreact-redux,以及使用React开发的组件,所以对于旧的React项目,可以无痛地将React替换成Nerv。

    环境准备

    Taro 仅提供一种开发方式:安装 Taro 命令行工具(Taro CLI)进行开发。
    在终端输入命令 npm i -g @tarojs/cli 安装 Taro CLI。
    Taro CLI 依赖于 Node.js 环境(>=8.0.0)。

    框架

    项目目录结构

    image

    编译配置

    编译配置存放于项目根目录下 config 目录中,包含三个文件:

    index.js: 通用配置
    dev.js : 开发环境配置
    prod.js : 生产环境配置

    入口文件

    每一个 Taro 应用都需要一个入口组件用来注册应用,入口文件默认是 src/app.js

    页面文件

    页面组件是每一项路由将会渲染的页面,Taro 的页面默认放在 src/pages 中,每一个 Taro 项目至少有一个页面组件。页面创建好后如果需要渲染展示,需要在项目入口文件 app.config.jspages 数组中进行指定。

    文件配置

    全局配置:可以通过 app.config.js 的文件进行全局配置, 配置页面文件的路径、窗口表现、设置网络超时时间、设置多 tab 等。

    一个简单的全局配置如下:

    // app.config.js
    export default {
      pages: [
        'pages/index/index',
        'pages/list/index'
      ],
      window: {
        backgroundTextStyle: 'light',
        navigationBarBackgroundColor: '#fff',
      }
    }
    未指定 entryPagePath 时,数组的第一项代表初始页面。
    新增/减少页面,都需要对 pages 数组进行修改。

    页面配置:每一个页面组件(例如 index.js)也会有一个页面配置(例如 index.config.js),可以在页面配置文件中设置页面的导航栏、背景颜色等参数。

    一个最简单的页面配置如下:

    // ./pages/index/index.jsx
    export default {
      navigationBarTitleText: '首页'
    }

    配置规范基于微信小程序的全局配置进行制定,所有平台进行统一。在配置文件中,Taro 并不关心框架的区别,Taro CLI 会直接在编译时在 Node.js 环境直接执行全局配置的代码,并把 export default 导出的对象序列化为一个 JSON 文件。因此,我们必须保证配置文件是在 Node.js 环境中是可以执行的,不能使用一些在 H5 环境或小程序环境才能运行的包或者代码,否则编译将会失败。

    项目配置

    为了能够适配到各个小程序平台,满足不同小程序平台配置文件不同的情况, Taro 支持为各个小程序平台添加不同的项目配置文件。

    通过 Taro 模板创建的项目都会默认拥有 project.config.json 文件,若要兼容到其他小程序平台,按如下对应规则来增加相应平台的配置文件,其配置与各自小程序平台要求的一致。

    小程序平台配置文件说明
    微信小程序project.config.json
    百度小程序project.swan.json
    头条小程序project.tt.json
    快应用project.quickapp.json配置文件中请勿配置 routerdisplay,这两个配置将由 Taro 生成
    QQ小程序project.qq.json

    组件生命周期与事件处理函数

    以Hooks为例:

    image

    css 工具

    在 Taro 中,我们可以自由地使用 CSS 预处理器和后处理器,使用的方法也非常简单,只要在编译配置添加相关的插件即可:

    // config/index.js
    const config = {
      designWidth: 750,
      sourceRoot: 'src',
      outputRoot: 'dist',
      plugins: [
        '@tarojs/plugin-sass', // 使用 Sass
        // '@tarojs/plugin-less', // 使用 Less
        // '@tarojs/plugin-stylus', // 使用 Stylus
      ],
      defineConstants: {},
      mini: {},
      h5: {}
    }
    
    module.exports = function (merge) {
      if (process.env.NODE_ENV === 'development') {
        return merge({}, config, require('./dev'))
      }
      return merge({}, config, require('./prod'))
    }

    除了 CSS 预处理器之外,Taro 还支持 CSS ModulesCSS-in-JS。 通过 自定义编译 ,还可以支持更多 CSS 工具。

    路由功能

    Taro 中,路由功能是默认自带的,不需要开发者进行额外的路由配置。

    入口组件和页面组件是通过配置文件来交互的,我们只需要在入口文件的 config 配置中指定好 pages,然后就可以在代码中通过 Taro 提供的 API 来跳转到目的页面,例如:

    // 跳转到目的页面,打开新页面
    Taro.navigateTo({
      url: '/pages/page/path/name'
    })
    
    // 跳转到目的页面,在当前页面打开
    Taro.redirectTo({
      url: '/pages/page/path/name'
    })

    尺寸单位

    在 Taro 中, 尺寸单位建议使用px, %,Taro 默认会对所有单位进行转换。当转成微信小程序的时候,尺寸单位将默认转换以 rpx为单位,当转成 H5 时将默认转换以 rem 为单位。

    如果你希望部分 px 单位不被转换成 rpx 或者 rem ,最简单的做法就是在 px 单位中增加一个大写字母,例如 Px 或者 PX 这样,则会被转换插件忽略。

    Taro 默认以 750px 作为换算尺寸标准,如果设计稿不是以 750px 为标准,则需要在项目配置 config/index.js 中进行设置,例如设计稿尺寸是 640px,则需要修改项目配置 config/index.js 中的 designWidth 配置为 640

    const config = {
      designWidth: 640,
    }

    目前 Taro 支持 750640828 三种尺寸设计稿,他们的换算规则如下:

    const DEVICE_RATIO = {
      '640': 2.34 / 2,
      '750': 1,
      '828': 1.81 / 2
    }

    多端开发

    由于不同的平台之间还是存在一些无法消除的差异,所以为了更好的实现跨平台开发,Taro 中提供了两种解决方案:

    1. 内置环境变量: process.env.TARO_ENV

    process.env.TARO_ENV 用于判断当前编译类型,目前有 weapp / swan / alipay / h5 / rn / tt / qq / quickapp 八个取值。通过这个变量来写对应一些不同环境下的代码,在编译时会将不属于当前编译类型的代码去掉,只保留当前编译类型下的代码。

    render () {
      return (
        <View>
          {process.env.TARO_ENV === 'weapp' && <ScrollViewWeapp />}
          {process.env.TARO_ENV === 'h5' && <ScrollViewH5 />}
        </View>
      )
    }

    缺点:虽然可以解决大部分跨端的问题,但是会让代码中充斥着逻辑判断,影响代码的可维护性,而且也让代码变得愈发丑陋。

    2. 统一接口的多端文件

    针对一项功能,如果多个端之间都有差异,开发者可以通过将文件修改成 原文件名.端类型 的命名形式实现多端文件。Taro 在编译时,会根据编译平台类型,将加载的文件变更为带有对应端类型文件名的文件,从而达到不同的端加载对应文件的目的。

    使用要点

    • 不同端的对应文件一定要对外暴露统一接口,统一调用方式,接受一致的参数
    • 最好有一个平台无关的默认文件,这样在使用 ts 的时候也不会出现报错
    • 引用文件的时候,只需要写默认文件名,不用带文件后缀

    多端文件简单的例子:

    1. 多端组件

      // 1. 定义多端组件
      -test
      --test.js // Test组件默认的形式,编译到微信小程序和H5之外的端
      --test.h5.js // Test 组件的 H5 版本
      --test.weapp.js // Test 组件的 微信小程序 版本
      
      // 2. 引用组件
      import Test from '../../components/test';
      
      <Test argA={1} argA={2} />
    2. 多端方法

      // 1.定义多端方法
      -utils
      --set_title.js
      --set_title.h5.js
      --set_title.weapp.js
      
      // 2. 使用方法
      import setTitle from '../utils/set_title'
      
      setTitle('页面标题')

    跨平台组件库

    相对于我们熟悉的 divspan 元素而言,在 Taro 中我们要全部使用跨平台组件进行开发。

    Taro 以 微信小程序组件库 为标准,结合 jsx 语法规范,定制了一套自己的组件库规范。基于这个原则,在小程序端,可以使用所有的小程序原生组件,在其他端,Taro提供了对应的组件库实现:

    • H5 端: @tarojs/components,需要引入的默认标准组件库
    • RN 端: @tarojs/components-rn

    开发规范

    • 首字母大写与驼峰式命名
    • 组件的事件传递都要以 on 开头

    组件种类

    image

    使用

    在使用时,我们需要先从 Taro 标准组件库 @tarojs/components 引用组件,再进行使用。

    import Taro, { Component } from '@tarojs/taro';
    // 使用 <View />、 <Text /> 组件
    import { View, Text } from '@tarojs/components';
    
    export default class C extends Component {
      render () {
        return (
          <View className='c'>
            <Text>c component</Text>
          </View>
        )
      }
    }

    Taro UI

    Taro UI 是一款基于 Taro 框架开发的多端 UI 组件库。

    特性

    • 基于 Taro 开发 UI 组件
    • 一套组件可以在 微信小程序支付宝小程序百度小程序H5 多端适配运行(ReactNative 端暂不支持)
    • 提供友好的 API,可灵活的使用组件

    组件种类

    image

    使用

    1. 引入组件样式

    引入组件样式有三种方式:

    • 全局引入(JS中): 在入口文件app.js中引入 taro-ui 所有的样式

      import 'taro-ui/dist/style/index.scss' // 引入组件样式
    • 全局引入(CSS中):app.scss 样式文件中 import 组件样式并按照文档说明使用

      @import "~taro-ui/dist/style/index.scss"; // 引入组件样式
    • 按需引入: 在页面样式或全局样式中 import 需要的组件样式

      @import "~taro-ui/dist/style/components/button.scss"; // 引入所需的组件样式

    2. 引入所需组件

    // index.js
    import { AtButton } from 'taro-ui';

    一个使用AtButton的完整例子:

    // src/pages/index/index.tsx
    import Taro, { Component, Config } from '@tarojs/taro'
    import { View } from '@tarojs/components'
    import { AtButton } from 'taro-ui'
    import './index.scss';
    
    export default class Index extends Component {
      config: Config = {
        navigationBarTitleText: '首页'
      }
      
      render () {
        return (
          <View className='index'>
             <AtButton type='primary'>按钮文案</AtButton>
          </View>
        )
      }
    }
    /* app.scss*/
    @import "~taro-ui/dist/style/index.scss"; 

    自定义主题

    Taro UI 目前只有一套默认的主题配色,为满足业务和品牌上多样化的视觉需求,UI 库支持一定程度的样式定制。(请确保微信基础库版本在 v2.2.3 以上)

    目前支持三种自定义主题的方式,可以进行不同程度的样式自定义:

    • SCSS 变量覆盖
    • globalClass 全局样式类
    • 配置 customStyle 属性(仅有部分组件支持,请查看组件文档,不建议使用)

    SCSS 主题变量覆盖

    Taro UI 的组件样式是使用 SCSS 编写的,如果你的项目中也使用了 SCSS,那么可以直接在项目中改变 Taro UI 的样式变量。

    1. 覆写的变量,需要在引入 taro ui 默认样式之前定义默认主题变量命名

    2. Slider, Switch 组件暂时不支持 SCSS 变量覆盖的方式自定义主题

    1. app.scss文件写入以下内容:

      // app.scss
      $color-brand: #6190E8; /* 改变主题变量 */
      
      @import "~taro-ui/dist/style/index.scss"; /* 引入 Taro UI 默认样式 */
    2. app.js中引入以上的样式文件即可

      // app.js
      import './app.scss';

    改变主题变量前后对比图:

    image

    全局样式类

    全局样式类是微信小程序定义的一套用于修改组件内部样式的方案。如果希望组件外样式类能够影响组件内部,可以在组件构造器中的 options.addGlobalClass 字段设置为 true(Taro UI 的组件均开启了此特性)。

    addGlobalClass 只对 Page 上的 class 起作用。换言之,如果在自定义的组件中使用 taro-ui,是无法在自定义组件内部通过 全局样式类 的方式去更改组件样式的。

    当开放了全局样式类,存在外部样式无意间污染组件样式的风险。由于 Taro UI 的组件样式采用 BEM 的命名方式,从一定程度上避免了样式污染的问题。
    /* page/index/index.js */
    import Taro from '@tarojs/taro'
    import { AtButton } from 'taro-ui'
    import "./index.scss"
    
    export default IndexPage extends Taro.Component {  
      render () {
        return <AtButton className='my-button' />
      }
    }
    
    /**
     * page/index/index.scss 必须在 Page 上
     * .at-button 为组件内部类名,只需要写一样的类名去覆盖即可
     **/
    .my-button .at-button {
      color: red;
    }

    设计思想与编译原理

    在 Taro 中先按照 Nerv语法编写一份源代码,然后通过编译工具将源代码编译成对应的代码。编译工具采用的是编译原理的思想,所谓编译原理,就是一个对输入的源代码进行语法分析,语法树构建,随后对语法树进行转换操作再解析生成目标代码的过程。

    image

    Taro标准:抹平多端差异

    基于编译原理,可以将 Taro 源码编译成不同端上可以运行的代码了,但是这对于实现多端开发还是远远不够。不同的平台都有自己的特性,每一个平台都不尽相同,这些差异主要体现在不同的组件标准与不同的 API 标准以及不同的运行机制上。

    以小程序和 Web 端为例:

    image

    可以看出小程序和 Web 端上组件标准与 API 标准有很大差异,这些差异仅仅通过代码编译手段是无法抹平的,例如不能直接在编译时将小程序的 <view /> 直接编译成 <div />,因为他们虽然看上去有些类似,但是他们的组件属性有很大不同。仅仅依靠代码编译,无法做到一致,同理,众多 API 也面临一样的情况。针对这样的情况,Taro 采用了定制一套运行时标准来抹平不同平台之间的差异。

    这一套标准主要以三个部分组成,包括标准运行时框架、标准基础组件库、标准端能力 API,其中运行时框架和 API 对应 @taro/taro,组件库对应 @tarojs/components,通过在不同端实现这些标准,从而达到去差异化的目的。

    taro build命令

    taro build 命令是整个 taro 项目的灵魂和核心,主要负责 多端代码编译(h5,小程序,React Native等)。不同的平台,编译传参不同,编译规则不同。

    // package.json 
    "scripts": {
        // 微信小程序 weapp
        "build:weapp": "taro build --type weapp",
        // h5
        "build:h5": "taro build --type h5",
        // 支付宝小程序 alipay
        "build:alipay": "taro build --type alipay",
        // ....
      },

    编译工作流与抽象语法树(AST)

    一般来说,将一种结构化语言的代码编译成另一种类似的结构化语言的代码包括以下几个步骤:
    image

    首先是 parse,将代码 解析(Parse)抽象语法树(Abstract Syntex Tree),然后对抽象语法树 AST 进行 遍历(traverse)替换(replace)(可以类比 DOM 树的操作),最后是 生成(generate),根据新的 AST 生成编译后的代码。

    Babel模块

    Babel 是一个通用的多功能的 JavaScript编译器,更确切地说是源码到源码的编译器,通常也叫做转换编译器(transpiler)。 Taro 项目的代码编译部分就是基于 Babel 的以下模块实现:

    • babylon Babylon 是 Babel 的解析器。最初是 从Acorn项目fork出来的。Acorn非常快,易于使用,并且针对非标准特性(以及那些未来的标准特性) 设计了一个基于插件的架构。
    • babel-traverse Babel Traverse(遍历)模块维护了整棵树的状态,并且负责替换、移除和添加节点。
    • babel-types Babel Types模块是一个用于 AST 节点的 Lodash 式工具库, 它包含了构造、验证以及变换 AST 节点的方法。 该工具库包含考虑周到的工具方法,对编写处理AST逻辑非常有用。
    • babel-generator Babel Generator模块是 Babel 的代码生成器,它读取AST并将其转换为代码和源码映射(sourcemaps)。
    • babel-template babel-template 是另一个虽然很小但却非常有用的模块。 它能让你编写字符串形式且带有占位符的代码来代替手动编码, 尤其是生成的大规模 AST的时候。 在计算机科学中,这种能力被称为准引用(quasiquotes)。

    参考文章:
    Taro 技术揭秘之taro-cli
    Taro 多端开发(上)

    查看原文

    赞 12 收藏 6 评论 1

    袁钰涵 发布了文章 · 1月20日

    一份待签收的邀请 | 移动应用开发问卷调查

    时至今日,很少有人再去讨论移动互联网是上半场还是下半场的问题,而是将精力放在那些占据时间、抢占市场的一个个移动应用当中。

    无论是内容 APP、社交 APP,还是工具 APP,他们支撑了我们“手机生活”的另一半。在获得这些 App 所提供服务的同时,大多数人会忽视此时此刻,移动开发者们面临着的变化、难题和挑战。

    为盘点 2020 年移动应用开发者生态现状,梳理现状背后的原因以及整个生态在变化中起到的影响作用,SegmentFault 思否特发起「2021 移动应用开发问卷调查」。

    期待每一位社区开发者积极参与到问卷的填写当中,您的每一个选择都将成为行业特征的具象化体现,我们也会结合大家的问卷调查结果,为广大开发者输出一份生态调研报告,和大家共同探讨移动应用生态的“下一步”。

    点击??链接或扫描图片上方二维码,即可参与问卷调查领取周边或红包~
    https://jinshuju.net/f/KHrnqW

    期待您的填写?

    查看原文

    赞 4 收藏 1 评论 0

    bt365体育投注