Data rooms: let your business be successful | data room

Want more features? Work with partners better and organize projects more proficiently? Do you care about data protection and cell work with all of them? Then you ought to pay attention to . They may be used by normally from the Fortune-500 list, since the mentioned tool allows businesses to act on maximum!

More features and mobility

Virtual data rooms are a cloud storage area that allows a modern business to operate without any obstacles. Firstly, the development allows you to exchange data and work with them in a safe function. You will be able to share commercial and confidential documents with associates and clients around the world. Secondly, the platform permits faster and easier to determine transactions of any complexness, as well as execute examinations and audits. Thirdly, it will be easy and convenient for you to entice a specialist and organize the work of a remote control team.

It might be worth saying that data rooms allow you to quickly and efficiently work with papers. You will spend even less time on routine responsibilities and emphasis more in decision making and management top quality. associated with business even more mobile, deal with more automated and much easier, and all techniques are more secure. Indeed, it really is safety which is most important element of stable creation and powerful functioning.

More Trustworthiness and Security for Data

Data rooms have been built to wonderful web app security standards. This means that solutions and functions are the most efficient. The coders used the most modern strategies of encryption and key storage space, the most relevant anti-virus systems. As a result, the reliability of data rooms corresponds to the consistency of banking systems. Not merely the software is extremely secure, nonetheless also data centers. They may have strict physical access handles, as well as wonderful safety protocols in case of flame, flood or perhaps earthquake. And, of course , current file backups.

Do not forget that one of the most important aspects of control is that you simply. Before sending each file, it is you who will collection the get parameters, added restrictions. Plus the user who all receives the file will need to confirm his identity in many stages and will also be able to job only inside the parameters set for him. It is important to know that you can often cancel use of a file, regardless if it has already been downloaded. Each and every one user activities with files are saved in a wonderful journal, that you will have get.

Extra offers and best service plan

Electronic data rooms are a great way for you to do more, with less attempt. You can make the operation of the company much easier and more rewarding. High-quality technical support will also provide you with this. Ask questions and seek specialist advice whenever you want. Also, if the need takes place, they will be capable to help you with the development of unique functions, digitization and organization of documents. You can always count on the prompt image resolution of concerns and support for your organization.

If you want to know even more about best virtual data room, then simply just start using them. This can be carried out completely free with respect to thirty in the event you activate quality mode. Not only will you learn the info, but likewise check the top quality, simplicity and effectiveness on the software.

Gitlab ci 调试笔记 (一)

Gitlab ci 调试笔记 (一)

通常情况下,我们对ci的认识都停留在构建工具这一层面,对于许多使用方面的细节并不关注,所以我想写一下近段时间修改ci文件的一些细节。

1.release分支的问题

build_prod:
  stage: build_prod
  only:
    - /^release-.+/

通常我们会这样写线上的构建条件,但如果某些情况下,有一个分支被命名为release-xxx,就会错误的触发线上环境构建。

我们可以这样修改:

build_prod:
  stage: build_prod
  only:
    variables:
      - $CI_COMMIT_TAG =~ /^release-.+/

如果构建分支有tag,则ci会自动设置”CI_COMMIT_TAG”这个环境变量,在only中使用环境变量去判断,从而确保只有release-xxx的tag会触发构建。

2.手动操作

ci作为一款自动化工具,通常情况下所有的操作都由某些事件的触发而自动执行和完成,但有时,我们希望能受控的执行一些操作

deploy_prod:
  when: manual
  only:
    variables:
      - $CI_BUILD_TAG =~ /^release-.+/

只需要添加”when: manual”即可达到我们的期望,满足条件时,在ci/cd面板上会显示执行按钮,点击该按钮来执行我们期望的操作。

3.使用ssh密钥

ci能执行shell,那么,能不能ssh连接其他服务器,执行一些操作呢,显然是可以的,只需要配置一下。

使用ssh有一些前提,比如ssh密钥,同时还需要信任服务器(这一步我们在本地通常都是随手输入y信任)

我们将ssh密钥预先生成好,并放置到环境变量里待用,同时将KNOWN_HOSTS也放置到环境变量里。

publish:
  before_script:
    - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    - echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
    - chmod 644 ~/.ssh/known_hosts

似乎有点长,我们来解读一下。

首先判断一下ssh-agent是否存在,不存在则安装一下,
安装完成后运行ssh-agent,并将密钥和HOSTS添加进来,就完成了配置工作,接下来做什么就自行发挥了,帮你commit & push代码也是可以的。


本期内容不多,等下期再完善一下

如何设计一个好的前端架构

前言

什么时候好的前端架构,就是当有新的开发人员接触项目是,不会在理解数据跟踪及其背后体现的UI路径时不至遇上太多的问题。

什么样式好的架构

  • 易于管理
  • 方便理解
  • 规划清晰
  • 更新便捷
  • 组件化程度高
  • 流程方便

页面

页面代表着web应用中不同的目标,页面目录中的文件或者目录,代表着路由路径的目的地。这样,当我们通过路由并拆分出组件时,就能够便捷的径路与页面文件关联起来。
* 仅包含路由入口文件以及其所需要组建的关联
* 入口文件不应该包含完整逻辑,应该见逻辑根据功能拆分至不同的组件
* 规范命名,因为该文件代表着打包后的文件与路由组件

组件

组件越小,就越易于处理。将UI拆分成一个个小的组件。代码越少,我们对代码的掌控能力就越强,调试与必要时的更新就会越简单。可以通过以下方式:
* 将公共组件统一放到一个目录中
* 将每个文件的组件进行分类,确保其中不包含公共代码组件
* 尝试对组件进行概括,以便以后能在不同的场景中使用
* 将彼此相关的组件划分在一起。确保这些组件不会在目录之外的组件中使用

辅助函数

辅助函数应该强大且中立,辅助函数应该与渲染逻辑区分开,仅在需要使用的时候引用。其作用在于:
* 处理特定组件的逻辑
* 与浏览器规范相关
* 处理从后端接收到的数据,使其适用于业务
* 将公共的辅助函数划分到一起,便于管理

API服务

API服务是指负责在参数特定的情况下,调用服务器以获取数据的代码。我们不应该直接从UI逻辑中调用服务。因为如果我们需要在很多位置实现相同的API调用,name对不同位置进行修改迭代将变得非常困难。
* 应该将API服务进行封装,单独做一个服务来实现
* 应将从服务器接收的数据直接返回给组件
* 应该能接收配置或者变量等,作为API服务的必要参数进行传递

Config

Config 当中应包含web应用运行所在的环境具体配置。确保将配置与实际代码拆分出来。
* 使用不同的文件对应不同的环境类型
* 根据获取不同的资源类型而有所不同

路由

路由是保障web应用使用体验的主要方式,路由决定这我们在应用中需要显示的不同页面的URL格式或者模式。
* 路由的命名应该尽可能简短
* 尽可能保持路由的正确顺序

Static文件

Static文件是指未包含在逻辑当中的文件。
* 应该根据其类型进行分组
* 尽可能降低文件体积

其他

  • 如果是在用npm管理的话,package.json 应该有完善的相关命令,来保证开发人员流程畅通
  • readme 要写的足够详尽,因为一个开发如果要想了解一个项目的话,都会先阅读readme

以上就是我总结的一些想法,现在前端发展迅速,整个设局对于项目架构的思路也是日新月异,我只是希望我写的这些能起到一些帮助。

Android Lint扫描规则说明(二)

主要内容

对Android Studio支持的六类Android Lint规则, 本文主要对Performance包含的32个项的说明,主要内容都是文档翻译,适当加一些自己的感想。

分类详细说明

高效使用资源

UnusedIds

未被使用的资源id,在layout文件中定义了资源ID从未被使用过,但有时候它们可以让layout更容易阅读,没有必要删除未使用的资源id。

Overdraw

过度绘制:一个绘制区域被绘制的次数多于一次。

如果给一个root view设置了背景图,就要给它设置一个background=nulltheme,否则绘制过程会先绘制themebackground,然后再绘制设置的背景图,完全覆盖之前绘制的theme.background,这是 过度绘制

这个检测器依赖于根据扫描Java代码找出哪些布局文件与哪些Activity相关联,目前它使用的是一种不精确的模式匹配算法。因此,可能会因错误地推断布局与活动的关联而给出错误的提醒。

如果想把一个背景图应用在多个页面上,可以考虑自定义theme,并把背景图设置在theme里,在layout中设置theme代替设置background。如果背景图中有透明的部分,并且希望他和theme的背景有层叠效果,那么可以选择先把两个背景合并成一个背景图之后,在定义到theme里。

VectorPath

关于SVG的使用,给出一篇参考文章:Android vector标签 PathData 画图超详解,Android Studio可以创建使用SVG绘制出的drawable图像资源。

UselessLeaf

没有包含任何View,也没有设置背景的Layout是多余的,可以去掉。让界面更趋于扁平,嵌套更高效。

UselessParent

如果一个包含ViewLayout没有兄弟层级的Layout,而他的外部ViewGroup又不是ScrollView或者root级别,那么这个Layout可以移除,让他包含的View直接包含在它的父层级的Layout中。让界面更趋于扁平,嵌套更高效。

TooDeepLayout

Layout嵌套过深会影响性能,考虑使用平铺类型的Layout代替。默认最深的View嵌套是10层,也可以通过环境变量ANDROID_LINT_MAX_DEPTH进行设置。System.getenv("ANDROID_LINT_MAX_DEPTH");语句获取,如何设置还没找到。

TooManyViews

Layout内有太多的View:一个Layout文件内有过多的View会影响性能。考虑使用复合drawables或其他技巧来减少这个布局中的视图数量。默认最多的数量是80个,可以通过环境变量ANDROID_LINT_MAX_VIEW_COUNT进行设置。据说这个变量可以用System.getenv("ANDROID_LINT_MAX_DEPTH");语句获取,如何设置还没找到。

NestedWeights

Weight嵌套:使用非0layout-weight值,需要Layout被测量两次,如果一个包含非0值的LinearLayout被嵌套在另一个包含非0值的LinearLayout内部,那么,测量次数就会呈指数级增长。

DuplicateDivider

这个主要是讲RecyclerView的分割线,com.android.support:recyclerview-v7 提供了一个类DividerItemDecoration设置分割线样式,这个类在早期的版本内没有包含,所以在更新为新的版本后,可以使用这个类重新设置分割线。
具体使用,参考文章:Android RecyclerView 使用完全解析

MergeRootFrame

FrameLayout在一个layout文件中是root且没有使用background或者padding等属性,通常使用一个merge标签代替FrameLayout会更高效。但是这要看上下文设置,所以在替换之前要确认你已经理解了merge标签的工作原理

UnusedResources

未使用的资源:多指的是drawable类型的资源。多余的drawable资源会让APP变大,编译过程变长。

InefficientWeight

当LinearLayout只有一个Widget且使用了android:layout_weight时,定义对应的width/height的值为0dp,Widget就会自动占满剩余空间。因为不需要预先计算自己的尺寸,这种方式更高效。

高效的设置

DisableBaselineAlignment

在使用LinearLayout实现空间的按比例分割时,LinearLayout的空间用layout_weight属性在所包含的几个layout中间分割,那么应该设置被分割LinearLayoutbaseLineAligned="false",这样可以加快分割空间所做的运算。

LogConditional

LogConditional:使用android.util.Log打印调试日志,一般只会在DEBUG模式下使用,在release是不需要打印调试日志的,在buildToolsVersion大于等于17时, BuildConfig提供两个一个DEBUG常量来标记是否处于DEBUG模式,我们可以用if(BuildConfig.DEBUG){}包裹调试日志语句,这样编译器会在编译生成release包时,删除这些语句。如果真的需要在release模式下打印调试日志,可以使用@SuppressLint("LogConditional")注解告诉编译器在release包中保留这些日志信息。

UnpackedNativeCode

APP使用System.loadLibrary()加载Native库时,android 6.0或者更新的版本可以在Manifest文件中application标签中添加属性android:extractNativeLibs="false",这样可以提交加载速度,降低APP占用的存储空间。

更高效的替代方案

FloatMath

不要使用FloatMath类进行数学计算,推荐使用Math类。

Android早期版本因为浮点运算性能的原因,推荐使用FloatMath代替Math类进行数学计算。随着硬件和系统的发展,这个问题已经不复存在,甚至经过JIT优化之后的Math类运算速度会比FloatMath更快,所以,在Android F以上版本的系统上,可以直接使用Math类,而不是FloatMath。

UseValueOf

某些类构造新对象时,建议使用工厂方法,而不是new关键字声明新的对象。例如,new Intger(0)就可以使用Integer.valueOf(0)替代,工厂方法会使用更少的内存,因为它会让值相等的对象使用同一个实例。

ViewHolder

在给ListViewGradView之类的列表实现Adapter时,不能每次getView调用都去inflate一个新的layout,如果接口参数中给出了一个可以复用的View对象,就可以使用这个对象而不是重新生成。这个应该都很熟悉,也很简单基础了。

UseSparseArrays

KeyInteger类型的HashMap可以使用SparseArray代替,性能更好。可以使用替代HashMap的有SparseBooleanArray、SparseIntArray、SparseLongArray和泛型类SparseArray,每个对应的类型代表Value的类型。如果在某些情况一定要用HashMap实现,则可以用@SuppressLint注解抑制Lint检查。

WakelockTimeout

关于week lock的使用,这里提供一篇博客文章:Android 功耗分析之wakelock

UseCompoundDrawables

在一个TextView的四周有只具有展示作用的ImageView时,建议删除ImageView改用compound drawables:drawableTop, drawableLeft, drawableRight,drawableBottom,drawablePadding替代方案实现。

有关泄漏的提醒

Recycle

缺少recycle()调用:许多资源例如:TypedArrays, VelocityTrackers在使用完之后需要调用recycle()方法回收资源。

ViewTag

4.0版本系统之前,View.setTag(int, Object)的实现方式中,会把Object存储在一个静态的map里并且使用的是强引用。这就意味着如果这个Object包含了对Context对象的引用,这个Context就是泄漏了。

传递一个View做参数,这个View就能提供一个对创建它的Context的引用。类似的,View holders内包含View,也会有Context与这个View相关联。

HandlerLeak

Handler引用泄漏:声明Handler的子类如MyHandler为内部类,如果MyHandler类对象关联Looper.getMainLooper()或者Looper.getMainLooper().getQueue()时,会阻止无用的外部类对象被垃圾回收,导致泄漏。如果对应main thread 的关联,就不会有这个问题。

应对方法,声明MyHandler为静态内部类,并用WeakReference的方式持有一个外部类对象,MyHandler使用这个对象操作外部类的属性和方法。

DrawAllocation

绘制过程中的内存分配:避免在布局绘制过程中分配内存给新的对象。因为这些操作调用频率比较高,频繁分配内存会唤起垃圾回收,中断UI绘制,导致卡顿。

StaticFieldLeak

非静态内部类具有对其外部类对象的隐式引用。

如果外部类Fragment或者Activity,那么这个引用意味着长时间运行的处理程序/加载器/任务(handler/loader/task)将持外部类对象的引用,从而防止外部类对象被回收。

同理,长时间运行的处理程序/加载器/任务(handler/loader/task)对Fragment或者Activity的直接引用,也会造成泄漏。

ViewModel类应该禁止引用View或者non-application类型的Context对象。

代码提醒

AnimatorKeep

属性动画默认支持的属性如下面列表。如果超出这些范围,会通过反射调用本地定义的函数。声明一个属性动画对象例如:ObjectAnimator.ofFloat(view, "rotation", 0, 360) 中的“rotation”就是要操作的属性,如果属性不在下面的列表中例如ObjectAnimator.ofFloat(view, "position", 0, 360),就需要本地定义一个对应的方法setPosition(float position),并且这个方法需要加上@keep注解,防止被当做无用方法清理掉。

    static {
        PROXY_PROPERTIES.put("alpha", PreHoneycombCompat.ALPHA);
        PROXY_PROPERTIES.put("pivotX", PreHoneycombCompat.PIVOT_X);
        PROXY_PROPERTIES.put("pivotY", PreHoneycombCompat.PIVOT_Y);
        PROXY_PROPERTIES.put("translationX", PreHoneycombCompat.TRANSLATION_X);
        PROXY_PROPERTIES.put("translationY", PreHoneycombCompat.TRANSLATION_Y);
        PROXY_PROPERTIES.put("rotation", PreHoneycombCompat.ROTATION);
        PROXY_PROPERTIES.put("rotationX", PreHoneycombCompat.ROTATION_X);
        PROXY_PROPERTIES.put("rotationY", PreHoneycombCompat.ROTATION_Y);
        PROXY_PROPERTIES.put("scaleX", PreHoneycombCompat.SCALE_X);
        PROXY_PROPERTIES.put("scaleY", PreHoneycombCompat.SCALE_Y);
        PROXY_PROPERTIES.put("scrollX", PreHoneycombCompat.SCROLL_X);
        PROXY_PROPERTIES.put("scrollY", PreHoneycombCompat.SCROLL_Y);
        PROXY_PROPERTIES.put("x", PreHoneycombCompat.X);
        PROXY_PROPERTIES.put("y", PreHoneycombCompat.Y);
    }
ObsoleteSdkInt

无用的SDK版本检查:Android SDK的版本更新比较快,许多API的使用都需要通过检查SDK版本防止出现not found之类的崩溃。在APP迭代的过程中提升了minSdkVersion的值就会导致部分SDK版本检查不再需要。

这种SDK版本检查会引起不必要的资源搜索。

DevModeObsolete

以前,文档中建议在productFlavors中创建一个dev product。设定minSdkVersion 21,在开发过程中激活multidexing加速构建过程。现在已经不需要这么做了,在新版的IDE和Gradle插件中,会自动地识别所连接设备的API level,如果链接的设备API level大于等于21,就会自动打开multindexing,就跟之前设置了dev product的效果一样。

参考:Enable Android MultiDex

ObsoleteLayoutParam

无用的LayoutParam:当给Widget使用了所在Layout没有提供的LayouParam时,会有这个提示。这种情况一般出现在修改Layout类型时没有同时修改内部Widget的LayoutParam设置或者把一个Widget从一个Layout拷贝到另一个不同类型的Layout内部。

这种无用的LayoutParam在运行时会引起无效的属性解析,也会误导阅读这些代码的人。所以应该把这些无用的属性删除掉。

其他

WearableBindListener
UseOfBundledGooglePlayServices

js实现一键打印网页中所有的图片

最近写了一个小工具,可以一键打印网页中所有的图片链接,效果如下:

实现思路主要分为两部分,一是如何获取网页中所有的图片链接,二是如何在浏览器的控制台打印图片。封装形式用了便捷的书签的方式,可以点一个标签来运行。

(代码链接在最后)

获取网页中所有图片链接

网页中图片主要有三种形式:一是img标签,二是css中的背景图片,三是在style中设置的背景图片。我们分别获取一下这三种图片:

img标签

用dom查询的api获取所有的img标签,返回src的数组

function getDomImage() {
    let imgList = [].slice.call(document.querySelectorAll('img')).map(item => item.src);
    return imgList;
}

有style属性的元素的背景图片

首先通过*[style]选出所有有style属性的标签,然后把内容拼成一个css格式的字符串。之后对这个字符串使用正则匹配url()中的链接,然后放到数组中返回。

function getStyleImage() {
    const imgList = [];
    let styleEles = [].slice.call(document.querySelectorAll("*[style]"));
    styleEles && styleEles.map(styleEle => {
        const styleStr = Object.entries(styleEle.style).filter(item => item[1]).map(item => item[0] + ':' + item[1]).join(';');
        let styleImages = styleStr.match(/url\((.*)\)/g);
        styleImages = styleImages && styleImages.map(item => item.replace(/url\(['"]*([^'"]*)['"]*\)/,'$1'));
        if(styleImages) imgList.push(...styleImages);
    });
    return imgList;
}

css中的背景图片

首先选出所有的style元素,然后获取textContent,之后也是通过正则匹配url()中的链接,然后放入数组返回。

function getCssImage() {
    const styleEles = document.querySelectorAll('style');
    return [].slice.call(styleEles).map(styleEle => {
        const css = styleEle.textContent;
        const cssImages = css.match(/url\((.*)\)/g);
        return cssImages && cssImages.map(item => item.replace(/url\((.*)\)/,'$1')) || [];
    });
}

去重

获取到这三种图片之后,合并到一个数组中。

    function getImages() {
        return getDomImage().concat(...getCssImage()).concat(...getStyleImage());
    }

但现在的数组中可能有重复的图片,因为一个图片可能在页面上出现多次。我们可以通过set来去重。

function uniqueArr(arr) {
    return Array.from(new Set(arr))
}

控制台打印图片

现在有了所有图片的链接,下一步就是打印到控制台了。浏览器console支持%c指定css样式,可以通过background-image的方式来设置图片。这是一种hack的方式。

先打印了一堆的空格,留出空间来显示背景图,然后在这段空白的文字区域实现图片。

    function formatConsole(list) {

        if (window.console) {
            var cons = console;
            if (cons) {
                list.forEach(item => {
                    cons.log("%c\n                                                                                    ", "font-size:100px;background:url('"+ item+"') no-repeat;background-size:contain;");
                    cons.log(item);
                });
            }
        }
    }

这三步完成之后组合调用一下:

let imgs = getImages();
imgs = uniqueArr(imgs);
formatConsole(imgs);

现在获取网页所有图片并打印到控制台的功能已经完成了。

浏览器新建标签

功能已经完成了,可是怎么使用呢。开发一个chrome插件可以,而且不止可以打印到控制台,还可以传到服务器之类的,能做更多的事情。

但我们现在只想打印到控制台,可以使用一种简单的方式:浏览器书签。

在浏览器中新建一个书签,比如内容为javascript:alert(1);,那么你在书签栏点击这个标签的时候就会执行这段js。

基于此,我们只需要把上面的代码再包装一层就可以用了:

javascript: (function() {
    //xxx
})()

总结

至此,我们已经实现了一键打印网页中所有的图片。可以新建一个书签,然后内容复制下面链接中的代码,之后点击书签栏的书签就可以运行了。

代码链接

console-all-image

vue组件原型调用

相信很多人用vuejs构建应用时都会用到一些全局方法,比如发ajax请求时喜欢用axios挂载到vue原型上,如下:

// 引入vue和axios
import Vue from 'vue'
import axios from 'axios'
// 然后挂载到原型上
Vue.prototype.$axios = axios

// 用axios.get()方法可以这样用
this.$axios.get()

这样确实方便,不用每个用到axios的组件都去引入
类似如此,当我们要用到一些操作dom的方法时要怎么做呢,上面的例子纯属js的封装,没有涉及到dom;下面我用一个全局提示组件为例,类似element-ui的message组件为大家演示一遍如何封装一个包含操作dom的的全局组件的,步骤主要有3步:

1, 在componenets/Message 目录下新建一个Message.vue组件
<template>
<transition name="fade">
    <div class="message" :class="type" v-show="show">
      <i class="icon"></i>
      <span class="text">{{text}}</span>
    </div>
</transition>
</template>

<script type="text/ecmascript-6">
  export default {
    name: 'message',
    props: {
      type: {
        type: String,
        default: 'info',
      },
      text: {
        type: String,
        default: ''
      },
      show: {
        type: Boolean,
        default: false
      }
    }
  }
</script>

<style scoped lang="stylus">
//......
</style>
2, 在componenets/Message目录准备一个index.js
import Message from './Message.vue'

const MESSAGE = {
  duration: 3000, // 显示的时间 ms
  animateTime: 300, // 动画时间,表示这个组件切换show的动画时间
  install(Vue) {
    if (typeof window !== 'undefined' && window.Vue) {
      Vue = window.Vue
    }
    Vue.component('Message', Message)

    function msg(type, text, callBack) {
      let msg
      let duration = MESSAGE.duration
      if (typeof text === 'string') {
        msg = text
      } else if (text instanceof Object) {
        msg = text.text || ''
        if (text.duration) {
          duration = text.duration
        }
      }
      let VueMessage = Vue.extend({
        render(h) {
          let props = {
            type,
            text: msg,
            show: this.show
          }
          return h('message', {props})
        },
        data() {
          return {
            show: false
          }
        }
      })
      let newMessage = new VueMessage()
      let vm = newMessage.$mount()
      let el = vm.$el
      document.body.appendChild(el) // 把生成的提示的dom插入body中
      vm.show = true
      let t1 = setTimeout(() => {
        clearTimeout(t1)
        vm.show = false  //隐藏提示组件,此时会有300ms的动画效果,等动画效果过了再从body中移除dom
        let t2 = setTimeout(() => {
          clearTimeout(t2)
          document.body.removeChild(el) //从body中移除dom
          newMessage.$destroy()
          vm = null // 设置为null,好让js垃圾回收算法回收,释放内存

          callBack && (typeof callBack === 'function') && callBack() 
      // 如果有回调函数就执行,没有就不执行,用&&操作符,
      // 只有&&左边 的代码为true才执行&&右边的代码,避免用面条代码:
        }, MESSAGE.animateTime)
      }, duration)
    }

// 挂载到vue原型上,暴露四个方法
    Vue.prototype.$message = {
      info(text, callBack) {
        if (!text) return
        msg('info', text, callBack)
      },
      success(text, callBack) {
        if (!text) return
        msg('success', text, callBack)
      },
      error(text, callBack) {
        if (!text) return
        msg('error', text, callBack)
      },
      warning(text, callBack) {
        if (!text) return
        msg('warning', text, callBack)
      }
    }
  }
}
export default MESSAGE

上面的代码关键点就是用Vue.extend()构造出一个Vue子类实例,(注意我这里模板渲染只用到render函数,没有用template选项,因为template选项 要求装Vue时要加入模板编译器那块代码,用render函数更加简洁,只需要装运行时版本,Vue体积更加小);然后调用$mount()方法生成需要的dom,再拿到对应的$el,实例内部自己维护插入dom和移除dom的操作,对外暴露了四个方法info、success、error、warning方便不同的场景调用;类似的组件还有confrim组件、alert组件等,大同小异。

3,在main.js中引入components/Message/index.js,以插件形式安装

最后,当你需要用的时候就直接,特别适合在ajax回调函数里面用来提示

import Vue from 'vue'
import vMessage from './components/Message/index' 
Vue.use(vMessage)

this.$message.info('普通消息') 
this.$message.error('错误消息') 
this.$message.warning('警告消息') 
this.$message.success('成功消息')

Android Lint扫描规则说明(一)

主要内容

对Android Studio支持的六类Android Lint规则, 本文主要对AccessibilityInternationalization 两中类型所包含的14个项的说明,主要内容都是文档翻译,适当加一些自己的感想。

Accessibility

可访问性的检查,除了第一项之外,其他项更像是为某些自动化的工具做的准备工作,不影响APP的运行。

ClickableViewAccessibility

可点击View的可访问性:如果重写onTouchEvent或使用OnTouchListener的View在检测到单击时没有实现performClick并调用它,则视图可能无法正确处理可访问性操作。理想情况下,处理单击操作的逻辑应该放在View#performClick中,因为当单击操作发生时,一些可访问性服务会调用performClick。

ContentDescription

非文本Widget描述:
– 首先,像ImageViews和ImageButtons这样的非文本Widget应该使用contentDescription属性指定文本对Widget进行说明,以便屏幕阅读器和其他可访问性工具能够充分描述和理解用户界面。
– 其次,如果一个非文本Widget在用户界面上只是一个装饰,不展示任何内容也不接受任何用户操作,就不需要给它提供描述性的contentDescription属性文本,而是使用tool属性ContentDescription抑制lint提醒。
– 第三,对于文本型Widget不能同时设置hintcontentDescription,否则hint将不会展示在界面上,只设置hint就可以。

参考:Make apps more accessible

GetContentDescriptionOverride

重写非文本Widget描述方法getContentDescription:重写View的getContentDescription方法,可能会阻止某些可访问性服务正确导航视图公开的内容。相反,当内容描述需要更改时,调用setContentDescription。

KeyboardInaccessibleWidget

键盘无法访问Widget:如果一个Widget声明了可以点击,但是没有声明可以获得焦点,这个Widget是无法通过键盘访问的,需要设置focusable=true。

LabelFor

缺少可访问标签:可编辑的控件例如EditText应该为hint属性赋值,或者在minSDKVersion大于等于17时,使用labelFor属性为EditText指定一个标签控件。标签控件可以是指定了text属性的文字控件如TextView,也可以是指定了contentDescription的非文字控件如ImageView。

如果被指定的标签控件如TextView在另外一个layout文件中,且使用layout属性引用了EditText所在的layout文件,可以ignore这个lint检查。

Internationalization

ByteOrderMark

查了一下这个ByteOrderMark,简称BOM,指的是一些标记字符。这种问题一般是“在不同编码格式的文件之间拷贝字符或者在某些文件系统上编辑了文件”导致的。

文件内BOM提醒:Lint会把文件中包含的BOM字符标记出来。因为Android工程中我们期望使用utf-8对文件和字符进行编码。BOM字符对utf-8来说不是必需的,而且有一些工具是不能正确处理带BOM字符的文本的。

参考:Android提示BOM错误排查UTF8最好不要带BOM

EnforceUTF8

资源文件编码格式非utf-8:XML文件对编码类型的支持比较广泛。然而有些工具不能正确某些类型编码的文件,而utf-8在Android应用中是一种被广泛支持的编码类型。使用utf-8对文件进行编码,可以防止在处理non-ASCII类型的字符时出现奇怪的问题。尤其是Gradle在合并XML类型的资源文件时,预先假定文件是使用utf-8进行编码的。

HardcodedText

硬编码文本属性:不要在layout文件或者代码中直接为文本控件设置text属性值。
– 在不同的位置多次使用相同的文本,如果需要修改文本则会引起多处修改。
– APP不能通过为文本提供一份翻译列表就可以适用新的语言的用户,而有很多工具可以快速的完成提供翻译列表的操作。
– 好的做法是,把text属性的文本值定义在string.xml文件中,方便国际化拓展。

SetTextI18n

TextView国际化:在调用TextView.setText给TextView赋值时,不能使用Number.toString例如图中的Integer.toString把数字转为字符串赋值,因为Number.toString不能正确处理分隔符和特定语言环境中的数字。

建议使用 String.format 指定合适的占位符进行赋值。

不能直接使用文本给TextView.setText,具体参照HardcodedText的说明。代码中可以使用@SuppressLint(“SetTextI18n”) 禁用lint的这项检查。

RelativeOverlap

RelativeOverlap是指RelativeLayout位于同一水平方向的两个Widget分别位于layout的左右两边,又都没有限制控件的长度,随着内容的增长,两个控件中间的距离不断缩小,最后会有部分重叠。所以,要控制一下边界。

Bidrrectional Text 双向文本

RtlEnabled

在API 17或更高版本上使用RTL属性需要在manifest文件的application标签中设置android:supportsRtl=”true”。如果已经开始在layout文件中加入RTL属性,但是没有完全使用RTL替代旧版的属性设置,可以设置android:supportsRtl=”false”规避lint的检查。

RtlCompat

API 17以后给文本控件提供了一个textAlignment属性来控制水平方向文字的对齐方式。但是,如果APP支持的版本包含小于 API 17 的版本,那么必须要设置gravity或者layout_gravity属性,因为老版本的系统会忽略textAlignment属性值。

RtlHardcoded

强制方向设置:在文本对齐和控件对齐中使用Gravity.LEFT/Gravity.RIGHT 的方式指定在左侧或者右侧对齐的情形,或在文字从右往左书写的国家或者地区造成困扰。使用Gravity.START/Gravity.END 就可以解决这个问题。同理,在layout文件中设置gravity/layout_gravity 属性时使用start/end 替代left/right

属性paddingLeft/paddingRight和属性layout_marginLeft/layout_marginRight也需要替换为paddingStart/paddingEndlayout_marginStart/layout_marginEnd

如果APP支持的最小API版本小于 API 17,那么需要同时提供left/right属性和start/end属性,因为低版本的系统会忽略start/end属性。

RtlSymmetry

margin|padding左右对称:如果对一个layout对象指定一边的内边距或外边距,那么应该对另一边指定同样大小的内边距或外边距。

参考文献

双向绑定Proxy比defineproperty优劣

前言

双向绑定其实已经是一个老掉牙的问题了,只要涉及到MVVM框架就不得不谈的知识点,但它毕竟是Vue的三要素之一

Vue三要素

  • 响应式: 例如如何监听数据变化,其中的实现方法就是我们提到的双向绑定
  • 模板引擎: 如何解析模板
  • 渲染: Vue如何将监听到的数据变化和解析后的HTML进行渲染

可以实现双向绑定的方法有很多,KnockoutJS基于观察者模式的双向绑定,Ember基于数据模型的双向绑定,Angular基于脏检查的双向绑定,本篇文章我们重点讲面试中常见的基于数据劫持的双向绑定。

常见的基于数据劫持的双向绑定有两种实现,一个是目前Vue在用的Object.defineProperty,另一个是ES2015中新增的Proxy,而Vue的作者宣称将在Vue3.0版本后加入Proxy从而代替Object.defineProperty,通过本文你也可以知道为什么Vue未来会选择Proxy。

严格来讲Proxy应该被称为『代理』而非『劫持』,不过由于作用有很多相似之处,我们在下文中就不再做区分,统一叫『劫持』。

基于Object.defineProperty双向绑定的特点

关于Object.defineProperty的文章在网络上已经汗牛充栋,我们不想花过多时间在Object.defineProperty上面,本节我们主要讲解Object.defineProperty的特点,方便接下来与Proxy进行对比。

极简版的双向绑定

我们都知道,Object.defineProperty的作用就是劫持一个对象的属性,通常我们对属性的getter和setter方法进行劫持,在对象的属性发生变化时进行特定的操作。
我们就对对象obj的text属性进行劫持,在获取此属性的值时打印’get val’,在更改属性值的时候对DOM进行操作,这就是一个极简的双向绑定。

const obj = {};
Object.defineProperty(obj, 'text', {
  get: function() {
    console.log('get val'); 
  },
  set: function(newVal) {
    console.log('set val:' + newVal);
    document.getElementById('input').value = newVal;
    document.getElementById('span').innerHTML = newVal;
  }
});

const input = document.getElementById('input');
input.addEventListener('keyup', function(e){
  obj.text = e.target.value;
})

Object.defineProperty的缺陷

Object.defineProperty的第一个缺陷,无法监听数组变化。 然而Vue的文档提到了Vue是可以检测到数组变化的,但是只有以下八种方法,vm.items[indexOfItem] = newValue这种是无法检测的。

push()
pop()
shift()
unshift()
splice()
sort()
reverse()

其实作者在这里用了一些奇技淫巧,把无法监听数组的情况hack掉了,以下是方法示例。

const aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
const arrayAugmentations = [];

aryMethods.forEach((method)=> {

    // 这里是原生Array的原型方法
    let original = Array.prototype[method];

   // 将push, pop等封装好的方法定义在对象arrayAugmentations的属性上
   // 注意:是属性而非原型属性
    arrayAugmentations[method] = function () {
        console.log('我被改变啦!');

        // 调用对应的原生方法并返回结果
        return original.apply(this, arguments);
    };

});

let list = ['a', 'b', 'c'];
// 将我们要监听的数组的原型指针指向上面定义的空数组对象
// 别忘了这个空数组的属性上定义了我们封装好的push等方法
list.__proto__ = arrayAugmentations;
list.push('d');  // 我被改变啦! 4

// 这里的list2没有被重新定义原型指针,所以就正常输出
let list2 = ['a', 'b', 'c'];
list2.push('d');  // 4

由于只针对了八种方法进行了hack,所以其他数组的属性也是检测不到的,其中的坑很多,可以阅读上面提到的文档。
我们应该注意到在上文中的实现里,我们多次用遍历方法遍历对象的属性,这就引出了Object.defineProperty的第二个缺陷,只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历,显然能劫持一个完整的对象是更好的选择。

Object.keys(value).forEach(key => this.convert(key, value[key]));

Proxy实现的双向绑定的特点

Proxy在ES2015规范中被正式发布,它在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写,我们可以这样认为,Proxy是Object.defineProperty的全方位加强版

Proxy可以直接监听对象而非属性

我们还是以上文中用Object.defineProperty实现的极简版双向绑定为例,用Proxy进行改写。

const input = document.getElementById('input');
const p = document.getElementById('p');
const obj = {};

const newObj = new Proxy(obj, {
  get: function(target, key, receiver) {
    console.log(`getting ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  set: function(target, key, value, receiver) {
    console.log(target, key, value, receiver);
    if (key === 'text') {
      input.value = value;
      p.innerHTML = value;
    }
    return Reflect.set(target, key, value, receiver);
  },
});

input.addEventListener('keyup', function(e) {
  newObj.text = e.target.value;
});

我们可以看到,Proxy直接可以劫持整个对象,并返回一个新对象,不管是操作便利程度还是底层功能上都远强于Object.defineProperty。

Proxy可以直接监听数组的变化

当我们对数组进行操作(push、shift、splice等)时,会触发对应的方法名称和length的变化,我们可以借此进行操作,以上文中Object.defineProperty无法生效的列表渲染为例。

const list = document.getElementById('list');
const btn = document.getElementById('btn');

// 渲染列表
const Render = {
  // 初始化
  init: function(arr) {
    const fragment = document.createDocumentFragment();
    for (let i = 0; i &lt; arr.length; i++) {
      const li = document.createElement('li');
      li.textContent = arr[i];
      fragment.appendChild(li);
    }
    list.appendChild(fragment);
  },
  // 我们只考虑了增加的情况,仅作为示例
  change: function(val) {
    const li = document.createElement('li');
    li.textContent = val;
    list.appendChild(li);
  },
};

// 初始数组
const arr = [1, 2, 3, 4];

// 监听数组
const newArr = new Proxy(arr, {
  get: function(target, key, receiver) {
    console.log(key);
    return Reflect.get(target, key, receiver);
  },
  set: function(target, key, value, receiver) {
    console.log(target, key, value, receiver);
    if (key !== 'length') {
      Render.change(value);
    }
    return Reflect.set(target, key, value, receiver);
  },
});

// 初始化
window.onload = function() {
    Render.init(arr);
}

// push数字
btn.addEventListener('click', function() {
  newArr.push(6);
});

很显然,Proxy不需要那么多hack(即使hack也无法完美实现监听)就可以无压力监听数组的变化,我们都知道,标准永远优先于hack。

Proxy的其他优势

Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的。
Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改。
Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利。
当然,Proxy的劣势就是兼容性问题,而且无法用polyfill磨平,因此Vue的作者才声明需要等到下个大版本(3.0)才能用Proxy重写。

基于数据的动态转换过滤器

这里直接使用 Vue作为例子,我们通过模拟一个不断有新数据产生的实时数据集,然后我们需要通过一个过滤参数将过滤后的数据展示出来。

<template>
    <div class="userinfo">
        <div>
        <label for="type">Type Filter: </label>
        <select name="type" id="type" v-model="typeFilter">
            <option value="none">None</option>
            <option v-for="type in typesSet" :value="type" :key="type">{{type}}</option>
        </select>
        <ul>
            <li v-for="item in filteredtypeDataset" :key="item.timestamp">Timestamp: {{item.timestamp}} - Type: {{item.type}} - Value: {{item.value}}</li>
        </ul>
        </div>
    </div>
</template>

“`vue.js
<script>
export default {
name: 'Userinfo',
data() {
return {
dataset: [],
typeFilter: 'none',
typesSet: [ 'nini', 'honghong', 'mingming' ],
};
},
props: [],
components: {},
computed: {
filteredtypeDataset() {
if (this.typeFilter === 'none') {
return this.dataset
}
return this.dataset.filter(item => item.type === this.typeFilter)
}
},
created() {},
methods: {
},
mounted() {
// 模拟流式数据集
setInterval(() => {
const randomType = this.typesSet[Math.round(Math.random() * (this.typesSet.length – 1))]
this.dataset.push({
type: randomType,
timestamp: Date.now(),
value: Math.random().toString(32).substr(2)
})
}, 1e3)
}
};
</script>

<pre><code class="line-numbers">进行构建的数据应用中,也是根据这个 typeSet 来提前生成了一个用于过滤数据的过滤器。

但有的时候前端的数据应用并不知道来自其他数据服务的数据内容究竟有哪些过滤项,那么我们便需要根据数据应用所得到的实际数据来生成过滤器。
“`js

methods: {
mockDataSource(typesSet) {
const dataset = [];
const timer = setInterval(() =&gt; {
const randomType = typesSet[Math.round(Math.random() * (typesSet.length – 1))]
dataset.push({
type: randomType,
timestamp: Date.now(),
value: Math.random().toString(32).substr(2)
})
}, 1e3)
return { dataset, stop() {
clearInterval(timer)
}
}
}
},
mounted() {
// 模拟流式数据集
setInterval(() =&gt; {
const dataSource = this.mockDataSource(Array(10).fill(1).map((_, i) =&gt; `type${i + 1}`)).dataset.type
this.dataset.push({
type: dataSource,
timestamp: Date.now(),
value: Math.random().toString(32).substr(2)
})
}, 1e3)
}

这段代码中我们模拟了一个包含多种可过滤数据type的流式数据集,且该数据集过滤字段内容是“不可预知”的。

使用 Prisma GraphQL API

概览

Prisma API 基于 HTTP 协议。这意味着您可以使用任何您喜欢的 HTTP 工具/库与 Prisma API 进行通信。

以下是 createUser mutation 的 HTTP POST 请求的结构:

Header

  • Authorization:携带用于验证请求的 service token(前缀 Bearer); 仅在使用 service secret 部署服务时才需要。

  • Content-Type:指定请求正文(JSON)的格式,通常为 application/json

Body(JSON)

  • query:要发送到 API 的 GraphQL 操作; 请注意,尽管该字段被称为 query,但它也用于 mutations!

  • variables:一个 JSON 对象,包含在提交的 GraphQL 操作中定义的 query 中的变量。

此页面上的所有示例均基于具有以下 service 配置的 Prisma service:

prisma.yml

datamodel: datamodel.prisma
secret: my-secret-42

datamodel.prisma

type User {
  id: ID! @unique
  name: String!
}

curl

curl 是一个命令行工具,除此之外,它允许您向 URL 发送 HTTP 请求。

以下是使用 curl 发送 createUser mutation 到 Prisma API 的方法:

curl '__YOUR_PRISMA_ENDPOINT__' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer __YOUR_SERVICE_TOKEN__' \
--data-binary '{"query":"mutation ($name: String!) { createUser(data: { name: $name }) { id } }","variables":{"name":"Sarah"}}'

要试用此示例,请使用 Prisma service 中的相应值替换 __YOUR_PRISMA_ENDPOINT____YOUR_SERVICE_TOKEN__ 占位符,并将相应的代码段粘贴到终端中。

这相对于向 Prisma API 发送了以下的 mutation 请求:

mutation {
  createUser(data: {
    name: "Sarah"
  }) {
    id
  }
}

试试这个例子:

  1. 用 Prisma service 中的相应值替换 __YOUR_PRISMA_ENDPOINT____YOUR_SERVICE_TOKEN__占位符

  2. 将相应代码段粘贴到终端中,然后按Enter键

fetch(Node.JS)

fetch 允许您使用 JavaScrip t向 URL 发送 HTTP 请求。

以下是在 node 脚本中,使用 fetch 向 Prisma API 发送 createUser mutation 的方法:

const fetch = require('node-fetch')

const endpoint = '__YOUR_PRISMA_ENDPOINT__'

const query = `
mutation($name: String!) {
  createUser(data: {
    name: $name
  }) {
    id
  }
}
`

const variables = { name: 'Sarah' }

fetch(endpoint, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization:
      'Bearer __YOUR_SERVICE_TOKEN__',
  },
  body: JSON.stringify({ query: query, variables: variables }),
})
  .then(response =&gt; response.json())
  .then(data =&gt; console.log(JSON.stringify(data)))

这相当于向 Prisma API 发送了如下 mutation:

mutation {
  createUser(data: {
    name: "Sarah"
  }) {
    id
  }
}

试试这个例子:

  1. 用 Prisma service 中的相应值替换__YOUR_PRISMA_ENDPOINT____YOUR_SERVICE_TOKEN__占位符

  2. 将相应代码段存储在名为的 script.js 的 JavaScript 文件中

  3. script.js 同一目录下,运行 yarn add node-fetch 安装 node-fetch

  4. 使用以下 terminal 命令行执行脚本: node script.js

graphql-request

graphql-request 库是 fetch 上包装的一个轻量级库,您可以使用它来保存和编写样板代码。它主要用于需要与 GraphQL API 通信的脚本和较小的应用程序。

使用 request

request 不支持将 headers 传递给请求(尚未)。因此,此特定示例假定您的 Prisma 服务已部署,并且没有 service secret。

const { request } = require('graphql-request')

const query = `
mutation($name: String!) {
  createUser(data: {
    name: $name
  }) {
    id
  }
}
`

const variables = { name: 'Sarah' }

request('__YOUR_PRISMA_ENDPOINT__', query,  variables)
  .then(data =&gt; console.log(data))

试试这个例子:

  1. 用 Prisma service 中的相应值替换__YOUR_PRISMA_ENDPOINT____YOUR_SERVICE_TOKEN__占位符

  2. 将相应代码段存储在名为的 script.js 的 JavaScript 文件中

  3. script.js 同一目录下,运行库 yarn add graphql-request 命令安装 graphql-request

  4. 使用以下 terminal 命令行执行脚本: node script.js

使用 GraphQLClient

const { GraphQLClient } = require('graphql-request')

const client = new GraphQLClient('__YOUR_PRISMA_ENDPOINT__', {
  headers: {
    Authorization: 'Bearer __YOUR_SERVICE_TOKEN__',
  },
})

const query = `
mutation($name: String!) {
  createUser(data: {
    name: $name
  }) {
    id
  }
}
`

const variables = { name: 'Sarah' }

client.request(query, variables)
  .then(data =&gt; console.log(data))

试试这个例子:

  1. 用 Prisma service 中的相应值替换__YOUR_PRISMA_ENDPOINT____YOUR_SERVICE_TOKEN__占位符

  2. 将相应代码段存储在名为的 script.js 的 JavaScript 文件中

  3. script.js 同一目录下,运行库 yarn add graphql-request 命令安装 graphql-request

  4. 使用以下 terminal 命令行执行脚本:node script.js

GraphQL Playground

GraphQL Playground 是一个 GraphQL IDE,允许您向 GraphQL API 发送 queries,mutations 和 subscriptions。

您可以通过将终端,导航到 service 的 prisma.yml 所在目录,然后运行以下命令,来打开 Prisma API 的 Playground:

prisma playground

Apollo Client

Apollo Client 是一个复杂的 GraphQL client 库,常用于大型前端应用程序。虽然所有前面的例子中都使用的类似工具来发送 queries 和 mutations 一样,Apollo Client 暴露了用于发送 queries 和 mutations 的专用方法:querymutate

query 查询

const { ApolloClient } = require('apollo-boost')
const gql = require('graphql-tag')

const endpoint = 'https://eu1.prisma.sh/nikolas-burk/demodofin/dev'

const client = new ApolloClient({
  uri: endpoint
});

const query = gql`
  query {
    users {
      id
      name
    }
  }
`

client.query({
  query: query,
})
  .then(data =&gt; console.log(data))

mutate 变换

const { ApolloClient } = require('apollo-boost')
const gql = require('graphql-tag')

const endpoint = 'https://eu1.prisma.sh/nikolas-burk/demodofin/dev'

const client = new ApolloClient({
  uri: endpoint
});

const mutation = gql`
  mutation($name: String!) {
    createUser(data: {
      name: $name
    }) {
      id
    }
  }
  `

const variables = { name: 'Sarah' }

client.mutate({
  mutation: mutation,
  variables: variables
})
  .then(data =&gt; console.log(data))

参考链接:https://github.com/graphql/graphql-js