那些害死Haskell的,也会害死Rust

thbcm阅读(202)

文章来源于公众号:架构头条
作者 | Alexander Granin
译者 | 无明
策划 | 小智

编者按

本文作者的中心思想不是唱衰 Rust 语言,正相反,他非常看重 Rust 语言。他回顾了 Haskell 语言从王者“沦落至此”的原因,希望这能给目前充满朝气的 Rust 社区敲响警钟。以下为正文。

时间到了 2030 年,我在文件夹里发现了这篇文章。从我写这篇文章开始,我就知道,我是对的。我觉得应该把这篇文章发表出来,因为它给 Rust 开发者们敲响了警钟:不要让历史重演!

那些杀死 Haskell 的,也会杀死 Rust。

为什么这个时候我会提到 Haskell?好吧,Haskell 和 Rust 有着千丝万缕的联系。可以说,Rust 就是没有高阶类型(HKT)的 Hashkell。Rust 的很多风格与 Haskell 很像。在某种程度上,可以说 Rust 就是 Haskell 的化身,只是它带了那么一点点 C 语言风格的语法。

Haskell 死了吗?

Haskell 曾经也是一门人们想要关注的语言。从 2000 年到 2010 年间,Haskell 是每一个程序员都希望能用上的语言,但除了少数人,没有人真的这么做。有一些令人印象深刻的项目是用 Haskell 开发的,比如很多金融项目和薪资系统。但是,从一门纯函数式编程语言的角度来看,Pandoc 才是真正称得上具有 Haskell 内核的项目。有人说“Haskell 太慢”、“Haskell 干不了实事”,结果让 Pandoc 给打了脸。

然而,Haskell 究竟发生了什么?为什么突然间止步不前了?现在没有人用它来开发重要项目。还有人在用 GHC Haskell 吗?或许还有那么一两个。GHC Haskell 已经沦为一门学术性语言,没有人真正关心它。

在 Haskell 时代,它处于函数式编程的最前沿,诠释了函数式编程的真正含义。当然,除了 Haskell,还有其他函数式编程语言,只不过它们没有那么纯粹,我说的是 Scala 或 JavaScript。在 2000 年代中期,这两门语言是 Haskell 最主要的竞争者。Go 和 C++ 是跟随者,Haskell 在引领着它们,它们从 Haskell 身上学到了很多。Scala 程序员都知道,for 循环语法和很多代码库的灵感都来自 Haskell。

Haskell 曾经是王者。

那个时候,Haskell 独占鳌头的气势是其他语言所不具备的。它为程序员带来的生产效率可以用因数“5”来衡量。一个开发团队使用 Haskell 开发并交付一个应用程序的速度比 Scala 或 C++ 快 5 倍。因数“5”成了一个非常重要的指标。

Haskell 的锋芒渐渐显露出来。有多少人在用 monad?我在 JavaScript 中用了,在 Rust 中也用了一些。在 Go 中,我可以用 monad 完成一些很有意思的事情。而这些,在 2000 年中期,都是 Haskell 程序员玩剩下的。当大多数人开始琢磨 monad,Haskell 早就有了 monad 和代数数据类型。

在很多方面,Haskell 是个王者,但还是死掉了。是什么杀死了它?

我想用一个词来形容,但请你们不要误解了这个词。你可能认为它是“邪恶(evil)”,或者是“无知(ignorant)”,但其实我想说的是“自大(arrogance)”。

Haskell 社区里弥漫着一股傲慢的味道。不是那种邪恶的傲慢,而是那种让他们觉得自己比别人更好的傲慢。他们使用的工具在某种程度上更好,他们做的事情在某种程度上更好,有些人甚至傲慢地认为他们获胜是不可避免的。这不是那种扇了你一巴掌然后说“你这个愚蠢的 Go 程序员”那种傲慢,相反,这是一种力量的傲慢。Haskell 程序员写出了一种强大的代码,一种强大的编译器,一种强大的语言,他们知道他们可以创造奇迹。

这些还不够。一些阴险而微妙的事情发生了,导致他们将自己与行业的其他部分隔离开来。社区外的程序员开始注意到 Haskell 程序员在做什么:“Haskell 社区的人似乎不太喜欢我们,我想我们也不会喜欢他们的。”

有些人可能还记得 2000 年代中期 Reddit 论坛上的一些讨论。有一群人在那里谈论很酷的数学问题。他们经常窃笑其他语言,比如 Go。这不是什么大事情,也不是什么邪恶的事情,他们是这样窃笑的:“主流的人们,哈!”那个时候我就是一个主流的 Go 用户!但我不喜欢他们那样。在接下来的几年里,我参与了编程语言之争。当时我对他们说:“我们真的想要在 Reddit 上展开编程语言之争吗?”有趣的不是他们在窃笑什么,因为他们有权那么做,有趣的是我的反应。我的反应带有防御性:“好吧,继续用你们的 Haskell 吧,但我们才是真正能解决问题的人。”

这种对立在当时非常有趣,而且相当普遍。Haskell 社区出现了一种态度,但不是邪恶的那种,也不是出于恶意。但有一种态度是这样的:“我们的工具很好,我们的语言很好,我们不需要遵守规则。我们可以做自己的事情,不需要和别人讨论。我们不需要写其他类型的程序。”Haskell 程序员不想写常规的程序,不想处理与数据库有关的问题。他们不想面对已经发展了 20 年的数据库模式。这太令人讨厌了。他们找到了替代方法,比如使用范畴理论和依赖类型。他们在自己周围筑起了一堵墙,生活在一个技术泡泡里,把自己与外部世界的邪恶隔离了起来。

我要在这里定义一个词。这个词大家都知道,我的定义只是众多定义中的一个。如果你喜欢,你也可以找到这个词的其他定义。这个词就是“专业”。我把它定义为”运用力量的纪律”。我们的工具和语言为我们提供了一些力量,但我们需要一种纪律来运用这些力量。这不仅仅是一种使用工具的纪律,更是一种社区纪律。这个纪律是这样的:它是一个强大的工具,而工具越是强大,杀人就越快,所以我们要小心使用它。另外,我们不会诋毁那些不太愿意使用我们工具的人。

我们不妨把“进步”重新定义为:“仅仅因为我们能做一件事,并不一定意味着我们必须去做那件事”。

所以,杀死 Haskell 的是它的狭隘和无法满足企业的需求。

Haskell 在某些受限的环境下表现出色,但它的力量很有限,或者说无法满足用户想要解决企业问题的愿望。这些人不愿意走到外面去,不愿意让自己踏进真正的土壤。他们表现出一种“对立”感,而站在另一边的人能够清楚地感觉到。这种狭隘主义就像是在屏幕上挂一个大横幅,上面写着“我按我的方式做事,你们自己玩去吧”。这就好比是在说:“在我们自己的天地里,我们很伟大,让其他人见鬼去吧”。

我想拯救什么?

我想拯救 Rust 和社区的工作成果,避免它们遭遇同样的下场。坦白说,我不认为它会走上那条路。首先,我认为 Rust 社区更有活力、更强大,我相信不再存在 Haskell 的那种对立局面。那些“强大的 C++ 激素程序员”已经变温和了。每个人都在想:“或许有一些东西会让 Rust 变得不一样”。那么它们是什么呢?有什么能防止 Rust 重蹈 Haskell 的覆辙呢?

我想说三件事:

第一个是纪律。特别是在文档方面的纪律,这可不是件容易的事。写完代码,不要忘了那些该死的文档。大家都知道,写出好的文档,让其他人都可以轻松地使用你的程序是一件多么困难的事情。

如果在专业的基础上再加上谦逊,或许 Haskell 就不会死掉。对立的态度,以及我知道有一些有趣的广告,比如“Mac 对 PC”、“我是 Rails,我是 Java”之类的,我不认为这有什么害处,问题在于你是否把它们看得太重。除非你筑起高墙,或者,另一些人在另一面筑起高墙作为回应。

最后一件事就是去解决“肮脏”的问题。我们必须静下心来,说:“我们会处理的”。如果我们要生存下去,就必须想办法解决所有的问题。如果你不去解决,就会有其他人来解决。

记住这门在本世纪头十年最具影响力的编程语言的命运吧。它为纯函数编程开了一个头,对我们现在所做的事情影响很大,但它几乎要被遗忘了。而那些使用和喜爱它的人不得不为了生计转向了 Scala,它几乎要了他们的命。

Rust 非常强大,但要毁灭它也很容易,制造混乱、傲慢和忽视企业需求,这些都可能会杀了它。我希望我们不要重蹈覆辙。

英文原文:

gist.github.com/graninas/22ab535d2913311e47a742c70f1d2f2b

本篇文章在GitHub的评论区同样十分精彩,推荐感兴趣的读者朋友做进一步了解

以上就是W3Cschool编程狮关于那些害死Haskell的,也会害死Rust的相关介绍了,希望对大家有所帮助。

阿里巴巴为何禁止使用BigDecimal的equals方法做等值比较?

thbcm阅读(197)

文章来源于公众号:Hollis 作者:Hollis

BigDecimal 很多人应该听说过它,也知道它的用法,它是java.math包中提供的一种可以用来进行精确运算的类型。

很多人都知道,在进行金额表示、金额计算等场景,不能使用doublefloat等类型,而是要使用对精度支持的更好的BigDecimal

所以,很多支付、电商、金融等业务中,BigDecimal的使用非常频繁。而且不得不说这是一个非常好用的类,其内部自带了很多方法,如加,减,乘,除等运算方法都是可以直接调用的。

除了需要用BigDecimal表示数字和进行数字运算以外,代码中还经常需要对于数字进行相等判断。

关于BigDecimal等值判断的这个知识点,在最新版的《阿里巴巴Java开发手册》中也有说明:

那么,为什么会有这样的要求呢?背后的思考是什么呢?

其实,我在之前的CodeReview中,看到过以下这样的低级错误:

if(bigDecimal == bigDecimal1){


    // 两个数相等


}

这种错误,相信聪明的读者一眼就可以看出问题,因为BigDecimal是对象,所以不能用==来判断两个数字的值是否相等。

以上这种问题,在有一定的经验之后,还是可以避免的,但是聪明的读者,看一下以下这行代码,你觉得他有问题吗:

if(bigDecimal.equals(bigDecimal1)){


    // 两个数相等


}

可以明确的告诉大家,以上这种写法,可能得到的结果和你预想的不一样!

先来做个实验,运行以下代码:

BigDecimal bigDecimal = new BigDecimal(1);


BigDecimal bigDecimal1 = new BigDecimal(1);


System.out.println(bigDecimal.equals(bigDecimal1));




BigDecimal bigDecimal2 = new BigDecimal(1);


BigDecimal bigDecimal3 = new BigDecimal(1.0);


System.out.println(bigDecimal2.equals(bigDecimal3));




BigDecimal bigDecimal4 = new BigDecimal("1");


BigDecimal bigDecimal5 = new BigDecimal("1.0");


System.out.println(bigDecimal4.equals(bigDecimal5));

以上代码,输出结果为:

true


true


false

BigDecimal的equals原理

通过以上代码示例,我们发现,在使用BigDecimalequals方法对1和1.0进行比较的时候,有的时候是 true(当使用 int、double 定义 BigDecimal 时),有的时候是 false(当使用 String 定义 BigDecimal时)。

那么,为什么会出现这样的情况呢,我们先来看下BigDecimalequals方法。

在BigDecimal的JavaDoc中其实已经解释了其中原因:

Compares this  BigDecimal with the specified Object for equality.  Unlike compareTo, this method considers two BigDecimal objects equal only if they are equal in value and scale (thus 2.0 is not equal to 2.00 when compared by  this method)

大概意思就是,equals方法和compareTo并不一样,equals方法会比较两部分内容,分别是值(value)和精度(scale)

对应的代码如下:

所以,我们以上代码定义出来的两个BigDecimal对象(bigDecimal4和bigDecimal5)的精度是不一样的,所以使用equals比较的结果就是false了。

尝试着对代码进行debug,在debug的过程中我们也可以看到bigDecimal4的精度是0,而bigDecimal5的精度是1。

到这里,我们大概解释清楚了,之所以equals比较bigDecimal4bigDecimal5的结果是 false ,是因为精度不同。

那么,为什么精度不同呢?为什么bigDecimal2bigDecimal3的精度是一样的(当使用int、double定义BigDecimal时),而bigDecimal4bigDecimal5却不一样(当使用String定义BigDecimal时)呢?

为什么精度不同

这个就涉及到BigDecimal的精度问题了,这个问题其实是比较复杂的,由于不是本文的重点,这里面就简单介绍一下吧。大家感兴趣的话,后面单独讲。

首先,BigDecimal一共有以下4个构造方法:

  1. BigDecimal(int)
  2. BigDecimal(double)
  3. BigDecimal(long)
  4. BigDecimal(String)

以上四个方法,创建出来的的BigDecimal的精度是不同的。

BigDecimal(long) 和BigDecimal(int)

首先,最简单的就是BigDecimal(long) 和BigDecimal(int),因为是整数,所以精度就是0

public BigDecimal(int val) {


    this.intCompact = val;


    this.scale = 0;


    this.intVal = null;


}






public BigDecimal(long val) {


    this.intCompact = val;


    this.intVal = (val == INFLATED) ? INFLATED_BIGINT : null;


    this.scale = 0;


}

BigDecimal(double)

而对于BigDecimal(double) ,当我们使用new BigDecimal(0.1)创建一个BigDecimal 的时候,其实创建出来的值并不是正好等于0.1的,而是0.1000000000000000055511151231257827021181583404541015625 。这是因为doule自身表示的只是一个近似值。

那么,无论我们使用new BigDecimal(0.1)还是new BigDecimal(0.10)定义,他的近似值都是0.1000000000000000055511151231257827021181583404541015625这个,那么他的精度就是这个数字的位数,即55。

其他的浮点数也同样的道理。对于new BigDecimal(1.0)这样的形式来说,因为他本质上也是个整数,所以他创建出来的数字的精度就是0。

所以,因为BigDecimal(1.0)BigDecimal(1.00)的精度是一样的,所以在使用equals方法比较的时候,得到的结果就是 true。

BigDecimal(string)

而对于BigDecimal(double)当我们使用new BigDecimal(“0.1”)创建一个BigDecimal 的时候,其实创建出来的值正好就是等于0.1的。那么他的精度也就是1。

如果使用new BigDecimal(“0.10000”),那么创建出来的数就是0.10000,精度也就是5。

所以,因为BigDecimal(“1.0”)和BigDecimal(“1.00”)的精度不一样,所以在使用equals方法比较的时候,得到的结果就是false。

如何比较BigDecimal

前面,我们解释了BigDecimalequals方法,其实不只是会比较数字的值,还会对其精度进行比较。

所以,当我们使用equals方法判断判断两个数是否相等的时候,是极其严格的。

那么,如果我们只想判断两个BigDecimal的值是否相等,那么该如何判断呢?

BigDecimal中提供了compareTo方法,这个方法就可以只比较两个数字的值,如果两个数相等,则返回0。

    BigDecimal bigDecimal4 = new BigDecimal("1");


    BigDecimal bigDecimal5 = new BigDecimal("1.0000");


    System.out.println(bigDecimal4.compareTo(bigDecimal5));

以上代码,输出结果:

0

其源码如下:

总结

BigDecimal是一个非常好用的表示高精度数字的类,其中提供了很多丰富的方法。

但是,他的equals方法使用的时候需要谨慎,因为他在比较的时候,不仅比较两个数字的值,还会比较他们的精度,只要这两个因素有一个是不相等的,那么结果也是 false。

如果读者想要对两个BigDecimal的数值进行比较的话,可以使用compareTo方法。

以上就是W3Cschool编程狮关于阿里巴巴为何禁止使用BigDecimal的equals方法做等值比较?的相关介绍了,希望对大家有所帮助。

React hooks 中 swr 的原理讲解和源码解析

thbcm阅读(192)

文章来源于公众号:前端瓶子君

swr是一个hook组件,可以作为请求库和状态管理库,本文主要介绍一下在项目中如何实战使用swr,并且会解析一下swr的原理。从原理出发读一读swr的源码

  • 什么是swr
    – swr的的源码

一、什么是swr

useSWRreact hooks 中一个比较有意思的组件,既可以作为请求库,也可以作为状态管理的缓存用,SWR 的名字来源于“stale-while-revalidate”, 是在HTTP RFC 5861标准中提出的一种缓存更新策略 :

首先从缓存中取数据,然后去真实请求相应的数据,最后将缓存值和最新值做对比,如果缓存值与最新值相同,则不用更新,否则用最新值来更新缓存,同时更新UI展示效果。

useSWR 可以作为请求库来用:

//fetch
import useSWR from 'swr'
import fetch from 'unfetch'
const fetcher = url => fetch(url).then(r => r.json())
function App () {
  const { data, error } = useSWR('/api/data', fetcher)
  // ...
}


//axios
const fetcher = url => axios.get(url).then(res => res.data)
function App () {
  const { data, error } = useSWR('/api/data', fetcher)
  // ...
}


//graphql
import { request } from 'graphql-request'
const fetcher = query => request('https://api.graph.cool/simple/v1/movies', query)
function App () {
  const { data, error } = useSWR(
    `{
      Movie(title: "Inception") {
        releaseDate
        actors {
          name
        }
      }
    }`,
    fetcher
  )
  // ...
}

此外,因为相同的 key 总是返回相同的实例,在 useSWR 中只保存了一个 cache 实例,因此 useSWR 也可以当作全局的状态管理机。比如可以全局保存用户名称 :

import useSWR from 'swr';
function useUser(id: string) {
  const { data, error } = useSWR(`/api/user`, () => {
    return {
      name: 'yuxiaoliang',
      id,
    };
  });
  return {
    user: data,
    isLoading: !error && !data,
    isError: error,
  };
}
export default useUser;

具体的 swr 的用法不是本文的重点,具体可以看文档,本文用一个例子来引出对于 swr 原理的理解:

const sleep = async (times: number) => {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve();
      }, times);
    });
};
const { data: data500 } = useSWR('/api/user', async () => {
    await sleep(500);
    return { a: '500 is ok' };
});
const { data: data100 } = useSWR('/api/user', async () => {
    await sleep(100);
    return { a: '100 is ok' };
});

上述的代码中输出的是 data100 和 data500 分别是什么?

答案是:

data100和data500都输出了{a:’500 is ok ‘}

原因也很简单,在swr默认的时间内(默认是 2000 毫秒),对于同一个 useSWRkey ,这里的 key‘/api/user’ 会进行重复值清除, 只始终 2000 毫秒内第一个 keyfetcher 函数来进行缓存更新。

带着这个例子,我们来深入读读 swr 的源码

二、swr的源码

我们从 useSWR 的 API 入手,来读一读 swr 的源码。首先在 swr 中本质是一种内存中的缓存更新策略,所以在 cache.ts 文件中,保存了缓存的 map

(1)cache.ts 缓存

class Cache implements CacheInterface {

 


  constructor(initialData: any = {}) {
    this.__cache = new Map(Object.entries(initialData))
    this.__listeners = []
  }


  get(key: keyInterface): any {
    const [_key] = this.serializeKey(key)
    return this.__cache.get(_key)
  }


  set(key: keyInterface, value: any): any {
    const [_key] = this.serializeKey(key)
    this.__cache.set(_key, value)
    this.notify()
  }


  keys() {

    
  }


  has(key: keyInterface) {

  
  }


  clear() {

  
  }


  delete(key: keyInterface) {

  
  }
  serializeKey(key: keyInterface): [string, any, string] {
    let args = null
    if (typeof key === 'function') {
      try {
        key = key()
      } catch (err) {
        // dependencies not ready
        key = ''
      }
    }


    if (Array.isArray(key)) {
      // args array
      args = key
      key = hash(key)
    } else {
      // convert null to ''
      key = String(key || '')
    }


    const errorKey = key ? 'err@' + key : ''


    return [key, args, errorKey]
  }


  subscribe(listener: cacheListener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }


    let isSubscribed = true
    this.__listeners.push(listener)
    return () => {
       //unsubscribe
    }
  }


  // Notify Cache subscribers about a change in the cache
  private notify() {

   
  }

上述是 cache 类的定义,本质其实很简单,维护了一个 map 对象,以 key 为索引,其中key 可以是字符串,函数或者数组,将 key 序列化的方法为:serializeKey

 serializeKey(key: keyInterface): [string, any, string] {
    let args = null
    if (typeof key === 'function') {
      try {
        key = key()
      } catch (err) {
        // dependencies not ready
        key = ''
      }
    }


    if (Array.isArray(key)) {
      // args array
      args = key
      key = hash(key)
    } else {
      // convert null to ''
      key = String(key || '')
    }


    const errorKey = key ? 'err@' + key : ''


    return [key, args, errorKey]
  }

从上述方法的定义中我们可以看出:

  • 如果传入的 key 是字符串,那么这个字符串就是序列化后的 key
  • 如果传入的 key 是函数,那么执行这个函数,返回的结果就是序列化后的 key
  • 如果传入的 key 是数组,那么通过 hash 方法(类似 hash 算法,数组的值序列化后唯一)序列化后的值就是 key

此外,在 cache 类中,将这个保存了 keyvalue 信息的缓存对象 map ,保存在实例对象 this.__cache 中,这个 this.__cache 对象就是一个 map ,有set get等方法。

(2)事件处理

在swr中,可以配置各种事件,当事件被触发时,会触发相应的重新请求或者说更新函数。swr对于这些事件,比如断网重连,切换 tab 重新聚焦某个 tab 等等,默认是会自动去更新缓存的。

在swr中对事件处理的代码为

const revalidate = revalidators => {
    if (!isDocumentVisible() || !isOnline()) return


    for (const key in revalidators) {
      if (revalidators[key][0]) revalidators[key][0]()
    }
  }


  // focus revalidate
  window.addEventListener(
    'visibilitychange',
    () => revalidate(FOCUS_REVALIDATORS),
    false
  )
  window.addEventListener('focus', () => revalidate(FOCUS_REVALIDATORS), false)
  // reconnect revalidate
  window.addEventListener(
    'online',
    () => revalidate(RECONNECT_REVALIDATORS),
    false
)

上述 FOCUS_REVALIDATORSRECONNECT_REVALIDATORS 事件中保存了相应的更新缓存函数,当页面触发事件visibilitychange(显示隐藏)、focus(页面聚焦)以及online(断网重连)的时候会触发事件,自动更新缓存

(3)useSWR 缓存更新的主体函数

useSWR 是swr的主体函数,决定了如何缓存以及如何更新,我们先来看 useSWR 的入参和形参。

入参:

  • key : 一个唯一值,可以是字符串、函数或者数组,用来在缓存中唯一标识 key
  • fetcher : (可选) 返回数据的函数
  • options : (可选)对于 useSWR 的一些配置项,比如事件是否自动触发缓存更新等等。

出参:

  • data : 与入参 key 相对应的,缓存中相应 keyvalue
  • error : 在请求过程中产生的错误等
  • isValidating : 是否正在请求或者正在更新缓存中,可以做为 isLoading 等标识用。
  • mutate(data?, shouldRevalidate?) : 更新函数,手动去更新相应 keyvalue

从入参到出参,我们本质在做的事情,就是去控制 cache 实例,这个 map 的更新的关键是:

什么时候需要直接从缓存中取值,什么时候需要重新请求,更新缓存中的值

const stateRef = useRef({
    data: initialData,
    error: initialError,
    isValidating: false
})
const CONCURRENT_PROMISES = {}  //以key为键,value为新的通过fetch等函数返回的值
const CONCURRENT_PROMISES_TS = {} //以key为键,value为开始通过执行函数获取新值的时间戳

下面我们来看,缓存更新的核心函数:revalidate


  // start a revalidation
  const revalidate = useCallback(
    async (
      revalidateOpts= {}
    ) => {
      if (!key || !fn) return false
      revalidateOpts = Object.assign({ dedupe: false }, revalidateOpts)
      let loading = true
      let shouldDeduping =
        typeof CONCURRENT_PROMISES[key] !== 'undefined' && revalidateOpts.dedupe


      // start fetching
      try {
        dispatch({
          isValidating: true
        })


        let newData
        let startAt


        if (shouldDeduping) {

        
          startAt = CONCURRENT_PROMISES_TS[key]
          newData = await CONCURRENT_PROMISES[key]

          
        } else {

         
          if (fnArgs !== null) {
            CONCURRENT_PROMISES[key] = fn(...fnArgs)
          } else {
            CONCURRENT_PROMISES[key] = fn(key)
          }


          CONCURRENT_PROMISES_TS[key] = startAt = Date.now()


          newData = await CONCURRENT_PROMISES[key]


          setTimeout(() => {
            delete CONCURRENT_PROMISES[key]
            delete CONCURRENT_PROMISES_TS[key]
          }, config.dedupingInterval)


        }


        const shouldIgnoreRequest =

        
          CONCURRENT_PROMISES_TS[key] > startAt ||

          
          (MUTATION_TS[key] &&

         
            (startAt

GitHub CLI 1.0 正式发布【丰富前端工程化】

thbcm阅读(205)

文章来源于公众号:前端人

GitHub 官方正式发布了 GitHub CLI 1.0 版本

GitHub CLI 将 GitHub 添加到你的终端。它减少了环境切换,使你可以集中精力,更轻松地编写脚本和创建自己的工作流。

今年年初发布 GitHub CLI 的 Beta 版以来,用户使用GitHub CLI

  • 创建了超过250,000个 pull requests
  • 执行了超过350,000次合并
  • 创建了超过20,000个 issues

GitHub CLI可以在Windows,macOS和Linux上使用。

很显然,开发者们可以直接通过 CLI 操作 GitHub 的 API,执行各种操作,这无疑提升了处理效率!

下载地址

通过官方网址: cli.github.com。

![下载地址](https://atts.w3cschool.cn/attachments/image/20200921/1600657328586763.png “下载地址”)

常见Api命令

  • gh release create
  • gh repo view
  • gh alias set
  • gh issue list
  • gh pr status
  • gh pr checkout
  • gh pr create
  • gh pr checks

有了这些命令之后,其实可以做很多有意思的事情,比如自己做一个仓库管理平台,通过这个平台去管理自己公司托管在 GitHub 上所有的仓库,包括仓库的创建,编辑,权限分配等等一系列工作。其实还有很多场景可以去做的!

更多Api说明

cli.github.com/manual/

如何使用?

登录

安装完成之后,直接启用命令行。

然后运行 gh auth login 进行帐号认证登录。我按照指引,很快就登录上了。

![GitHub CLI登录](https://atts.w3cschool.cn/attachments/image/20200921/1600657512757044.png “GitHub CLI登录”)

![GitHub CLI登录](https://atts.w3cschool.cn/attachments/image/20200921/1600657547964711.png “GitHub CLI登录”)

使用

![GitHub CLI使用](https://atts.w3cschool.cn/attachments/image/20200921/1600657562520642.png “GitHub CLI使用”)

官方给出了一些基本命令演示图,比如,

列举 issues 列表:

![列举 issues 列表](https://atts.w3cschool.cn/attachments/image/20200921/1600657582157224.png “列举 issues 列表”)

创建 Pull Request:

![创建 Pull Request](https://atts.w3cschool.cn/attachments/image/20200921/1600657598545766.png “创建 Pull Request”)

比对 PR 的变更:

![比对 PR 的变更](https://atts.w3cschool.cn/attachments/image/20200921/1600657612846451.png “比对 PR 的变更”)

老司机们,还不赶紧用起来???

以上就是W3Cschool编程狮关于GitHub CLI 1.0 正式发布【丰富前端工程化】的相关介绍了,希望对大家有所帮助。

Java新特性:var,数据类型可以扔掉了?

thbcm阅读(198)

文章来源于公众号:Java中文社群 作者:磊哥

在许多年以前,程序员撸代码的时候需要小心谨慎的选择变量的数据类型,数据类型如下:

枚举:尽管在 JDK 5 中增加了枚举类型,但是 Class 文件常量池的 CONSTANT_Class_info 类型常量并没有发生任何语义变化,仍然是代表一个类或接口的符号引用,没有加入枚举,也没有增加过“CONSTANT_Enum_info”之类的“枚举符号引用”常量。所以使用 enum 关键字定义常量,尽管从 Java 语法上看起来与使用 class 关键字定义类、使用 interface 关键字定义接口是同一层次的,但实际上这是由 Javac 编译器做出来的假象,从字节码的角度来看,枚举仅仅是一个继承于 java.lang.Enum、自动生成了 values() 和 valueOf() 方法的普通 Java 类而已,因此枚举也归为引用类型了

然而到了 JDK 10 时,我们就有了新的选择,JDK 10 中新增了 var 局部变量推断的功能,使用它我们可以很 happy 的忘记数据类型这件事了,那它是如何使用的呢?接下来我们一起来看。

1、使用对比

接下来我们就使用对比的方式,来体会一下 var 的作用。

场景一:定义字符串

旧写法:

String str = "Hello, Java.";

新写法:

var s = "Hello, Java.";

PS:这里的旧写法指的是 JDK 10 之前的版本,而新写法指的是 JDK 10 以后(包含 JDK 10)的版本。

场景二:数值相加

旧写法:

int num1 = 111;
double num2 = 555.666d;
double num3 = num1 + num2;
System.out.println(num3);

PS:当遇到不同类型相加时(int+ double)会发生数据类型向上转型,因此 num3 就会升级为 double 类型。

新写法:

var n1 = 111L;
var n2 = 555.666;
var n3 = n1 + n2;
System.out.println(n3);

场景三:集合

旧写法:

List<Object> list = new ArrayList();
list.add("Hello");
list.add("Java");

新写法:

var list = new ArrayList();
list.add("Hello");
list.add("Java");

场景四:循环

旧写法:

for (Object item : list) {
    System.out.println("item:" + item);
}
for (int i = 0; i < 10; i++) {
    // do something...
}

新写法:

for (var item : list) {
    System.out.println("item:" + item);
}
for (var i = 0; i < 10; i++) {
    // do something...
}

场景五:配合 Lambda 使用

旧写法:

List<Object> flist = list.stream().filter(v ->
                v.equals("Java")).collect(Collectors.toList());
System.out.println(flist);

新写法:

var flist = list.stream().filter(v ->
             v.equals("Java")).collect(Collectors.toList());
System.out.println(flist);

2、优点分析

通过上面的示例我们可以看出, var 具备两个明显的优点:提高了代码的可读性和命名对齐

① 提高了可读性

我们在没有使用 var 之前,如果类型的名称很长就会出现下面的这种情况:

InternationalCustomerOrderProcessor orderProcessor = 
    createInternationalOrderProcessor(customer, order);

当限定每行不能超过 150 个字符的话,变量名就会被推到下一行显示,这样整个代码的可读性就变得很低。但当我们使用了 var 之后,代码就变成了这样:

var orderProcessor = createInternationalOrderProcessor(customer, order);

从上述的代码可以看出,当类型越长时,var(可读性)的价值就越大。

② 命名对齐

在不使用 var 时,当遇到下面这种情况,代码就是这样的:

// 显式类型
No no = new No();
AmountIncrease more = new BigDecimalAmountIncrease();
HorizontalConnection jumping =
  new HorizontalLinePositionConnection();
Variable variable = new Constant(6);
List names = List.of("Java", "中文社群");

在使用了 var 之后,代码是这样的:

var no = new No();
var more = new BigDecimalAmountIncrease();
var jumping = new HorizontalLinePositionConnection();
var variable = new Constant(6);
var names = List.of("Java", "中文社群");

从上述代码可以看出使用了 var 之后,命名对齐了,整个代码也变得更优雅了。

3、使用规则 & 反例

var 的实现来自于 JEP 286 (改善提议 286),详情地址 :http://openjdk.java.net/jeps/286

从 JEP 286 的标题“局部变量类型推断”可以看出,var 只能用于局部变量声明,也就是说 var 必须满足以下条件:

  • 它只能用于局部变量上;
  • 声明时必须初始化;
  • 不能用作方法参数和全局变量(类变量)。

PS:因为 var 的实现必须根据等会右边的代码进行类型推断,因此它不能被赋值 null 或不被初始化。

反例一:未初始化和赋值 null

反例二:中途类型更改

反例三:全局变量

反例四:作为返回值

4、原理分析

经过前面的使用我们对 var 已经有了初步的认识,但 var 的实现原理是什么呢?

为了搞清楚它的原理,我们对下面的代码进行了编译(使用命令 javac MainTest.java):

然后我们再用反编译工具打开被编译的类发现:var 竟然被替换成一个个确定的数据类型了,如下图所示:

由此我们可以得出结论:var 关键字的实现和它的名字密切相关, var 只是局部类型推断,它只会在 Java 编码期和编译期有效,当类被编译为 class 文件时,var 就会变成一个个确定的数据类型(通过推断得出)。 所以我们可以把 var 通俗的理解为 Java 的语法糖,使用它可以让我们快速优雅的实现业务代码,但 var 在字节码层面是不存在的。

总结

本文我们介绍了 var(局部类型推断)的使用,它可以用在局部变量、 forLambda 的变量声明中,但不能用在全局变量的声明中,也不能用它作为方法的返回值,并且在声明时一定要进行初始化(也不能赋值为 null)。使用 var 可以有效的提高代码的可读性和命名对齐,它的实现原理,是在编译期通过等号右侧的代码进行类型推断,然后再将 var 替换成确定的数据类型。

以上就是W3Cschool编程狮关于Java新特性:var,数据类型可以扔掉了?的相关介绍了,希望对大家有所帮助。

前端安全就是纸老虎,唬人用的

thbcm阅读(176)

文章来源于前端黑洞 ,作者拓岩

在某乎上,最近有一个很火的问题:前端能否限制用户截图?

当我浏览这个问题的时候,我觉得提问者应该是个萌新,或者已经被产品经理或 SB leader 折磨的失去理智。因为下方有一个非常直中要害的回答:

无论多么牛的技术手段限制了软件的截图, 用户只要简单的掏出手机对着屏幕拍照就好了。

这个问题,真的说明一切的前端安全,其实都是纸老虎。

接下来,我结合自己遇到的几个场景,来谈一些做前端以来,自己遇到的那些伪前端安全需求。

曾经那些被怼回去的安全需求

最近几年互联网数据泄露非常频繁,我上一家公司是做金融贷款的,非常强调数据安全,这两年也做了不少关于安全的需求。

前端数据脱敏

前端数据脱敏是一个很常见的需求,特别是当今隐私被卖的这么猖狂的时代,所以很多公司都开始注重这些细节,最基础的就是数据脱敏。

数据脱敏,就是将用户的隐私信息,用一些手段,让这些信息有一定辨识度,但又无法准确获取,比如:

上面一般是我们常见到的数据脱敏格式,我又叫他数据马赛克。前端能不能做,肯定能做,一个正则配上一个String.replace方法就搞定。但如果产品让我们实现这种需求,我们肯定要拒绝,因为前端做数据脱敏就是被单里眨眼睛 – 自欺欺人。

归根结底,一个稍微有点IT常识的人,如果想要这些数据,直接从请求拿就是了,何必从页面复制。

所以数据脱敏这种事,一定要交给后端做,从源头开始脱敏。

可能后面一些场景,有些被脱敏的数据,在前端又要被用到。比如列表数据脱敏,到详情/表单编辑操作时又需要脱敏前的,那就根据ID再发一次请求获取脱敏前的数据,然后对这个接口调用做权限限制和日志记录,让敏感数据的使用相对安全。

表单校验是为了安全么

我们在做表单时,很多时候都会针对数据格式做校验,比如邮箱、电话号码、银行卡号这些,甚至还有一些非常 复杂的联动校验。

前端做校验是为了安全么?

可能有那么一丁点意思吧,比如以前我们总是在提校验输入防XSS攻击。但现在前端这种格式校验,更多是为了: 提升用户体验,提升用户体验,提升用户体验

•首先提醒并引导用户,应该怎么输入;•其次,如果用户输入半天,前端不校验,直接到后台,后台发现格式不正确,再提示用户,这是一个非常耗时且不专业的交互体验;•如果前端没校验,后端也没校验,那这就是一条脏数据插入到数据库,有可能造成XSS攻击SQL攻击, 这就非常危险了。

数据报表加水印

页面加水印,其实在前端很普遍,比如钉钉, 企业微信 的群聊都是加了水印的,很多在线图片编辑工具也是加了的,比如我常用的图怪兽[2],你想白嫖,他就给你加个水印:

而当时我们有些列表,因为运营需要,有些数据没法做数据脱敏,所以领导说,前端能不能做个水印,让数据安全一点:防止运营人员不按规范处理问题,私自截图。所以当我看到知乎那个提问时,我特别庆幸,没有让我做:限制用户截图

从我个人经验来讲,前端加水印有三个层次:

  • 通过 CSS 背景加水印,简单粗暴,能骗一点文科运营。但稍微懂行的人,就知道通过 Elements 编辑面板屏蔽这个水印。正所谓你加的简单,别人去掉更简单。
  • 通过 JS 定向植入水印dom节点,这个比上一个稍微复杂点,但还是通过Elements 编辑面板屏蔽,只不过多思考一下,操作步骤多点。
  • 终解: 服务端加水印生成列表图片,实现思路和图怪兽网站一致。但这个操作描述起来简单,具体实现就非常复杂,需要考虑投入产出比。有可能你会疑问,为什么是服务端加水印生成图片,而不是前端自己通过 canvas 生成?
    • 第一,同上面提到过的,通过请求拿到敏感数据,本身就是不安全的;
    • 第二,JS 本身是不安全的,可篡改;

JS 可篡改

我上面反复在说前端安全是个伪需求,你可能不信。但如果你知道 JS 是可篡改的,那你就明白为什么了。我们总是在提JS丑化,但丑化更多是减少包的体积,在某种程度上,可以让发布的js资源可读性更差,但做到不可读很难。

接下来体验一下什么叫 JS 可篡改吧

实战演练

  • 第一步:Chrome 下载安装 Header Editor 插件

  • 第二步:找一个目标网站,并找到一个你想篡改的 JS 资源。我这里以我常用的作图网站的jquery 资源(js.tuguaishou.com/js/jquery-1.11.3.min.js)引用为例;
  • 第三步:拷贝代码带编辑器,输入你想篡改的内容,我这里就只加了个console输出, 然后本地起一个静态资源服务
  // ...
  console.log('do some change, ho ho ho');
  var c = []
    , d = c.slice
    , e = c.concat
  // ...

  • 第四步,使用插件重定向网站静态资源,我这里就是使用本地的jquery 代替网站原有的,保存配置并启动那个规则;

  • 第五步: 强刷浏览器,使代理资源生效,当你看到请求被标成了307的响应,说明篡改生效,然后看console,就有了对应的输出;

至此,一次完整的篡改完成。但这个教程并不是让你用这个方法去做一些XX的事情,而只是让你明白由于JS的可篡改,我们在做网站设计时,你需要时刻思考代码安全的事,能做不代表可以做

分享个趣事

年初,前公司要做一次大的系统融合:就是两个子公司各有各的权限系统,然后资本青睐的一方占优(以系统设计来讲,我们的权限系统设计更专业),被遗弃的我们不得不改我们现有系统,去对接对方的系统。

这中间因为异地沟通问题,对方开始没把规则说好,然后对接人将我们清洗重组后的数据导入了数据库。后面由于需要修改一些数据,网站页面提示我们导入数据key的格式不对,而这个key,又被设置成了只读,这就走入了死路。然后沟通能不能把数据库数据删除了重新导入,对方一万个不情愿,让我们自己在网站上自己删除再添加数据,那可是200多条数据啊,侮辱谁呢????

这真的把我同事们惹毛了,然后就试了上面的招数,看了对方的JS,代理然后再一提交,成功!!!

知道对方业余,但没想到这么业余:仅在前端做了限制,服务端没校验。

这个案例提醒我们,前端重在交互,服务端重在安全,JS是可篡改的;所以不要产品以为,要你以为,用你的专业Say no!!!;也告诉我们做技术,谦虚点,能帮忙就尽量帮,做个好人。

结语

又摧拉枯朽扯了一堆,但愿对你以后的需求评审和方案设计有用。前端安全重要吗?重要。网站的安全全交给前端合适吗? 不合适。

以上就是W3Cschool编程狮关于前端安全就是纸老虎,唬人用的的相关介绍了,希望对大家有所帮助。

js垃圾回收机制原理给你聊的明明白白

thbcm阅读(192)

文章来源于公众号:前端Symbol卢 ,作者Symbol卢

前言

大多数语言都是提供自动内存管理机制,比如 C#JavaJavaScript 。自动内存管理机制也就是我们经常听到的垃圾回收机制 。好神奇哦,语言会收垃圾,哈哈,不过这里的垃圾,可不是家里面的厨余垃圾啥的,而是 一些不再使用的变量所占用的内存。我们的 JavaScript 的执行环境会自动对这些垃圾进行回收,也就是释放那些不再使用的变量所占用的内存,收垃圾的过程 会按照固定的时间间隔周期性的执行 垃圾回收

内存泄露

如果 那些不再使用的变量所占用的内存 没有被释放 会怎样呢??? 那就会造成 内存泄露

什么是内存泄露???别着急往下看

内存泄露其实就是我们的程序中已经动态分配的堆内存,由于某些原因没有得到释放,造成系统内存的浪费导致程序运行速度减慢甚至系统崩溃等严重后果。后果很严重的哦,现在知道为啥要有垃圾回收机制了嘛

垃圾回收机制

看看这个代码可以加深理解垃圾回收机制哦

let name = '编程狮';
function aboutMe() {
  let hobby = '吃肉肉';
  let say=`我是${name},我喜欢${hobby}`;
  console.log(say);
}
aboutMe()

在函数 aboutMe() 执行的时候,声明了两个局部的变量 hobbysay ,等函数执行完毕之后,这两个局部变量也就不再使用 ,所以垃圾回收机制 就会将不再使用的(局部)变量 hobbysay 清除掉 (释放了它们的内存)

JavaScript 中能实现这样的垃圾回收的功能的一共有两种方式:标记清除引用计数

标记清除

标记清除是js中最常用的垃圾回收方式。它的原理也比较好理解,废话不多说,先上代码

function speakLines(){
  let night="天黑";//做个标记 ,进入环境 
  let closeEyes="闭眼";//做个标记 ,进入环境 
  let speak=`开始狼人杀,${night}请${closeEyes}`;//做个标记 ,进入环境 
  console.log(speak);
}
speakLines() //代码执行完毕  里面被标记过的变量,又被标记 离开环境 最后被回收

当代码执行在一个环境中时(例如上面的 speakLines函数),每声明一个变量,就会对该变量做一个标记(例如标记一个进入执行环境);当代码执行进入另一个环境中时,也就是要离开上一个环境( speakLines 函数执行完毕,去执行其他的函数),这时对上一个环境中的变量做一个标记,(例如标记一个离开执行环境),等到垃圾回收执行时,会根据标记来决定要清除哪些变量进行释放内存

引用计数

引用计数是一种不太常用的垃圾回收方式。同样也是先上代码再说原理

let skill = ["唱歌", "弹吉他", "播音"]
function change() {
  let new_skill = ["vue", "react", "node", "webpack", ".net", "mysql", "sqlServer", "nginx"];
  skill = new_skill;
  //一个文艺小青年就这样的变成了一个技术宅男
}
change()
console.log(skill)  //返回  ["vue", "react", "node", "webpack", ".net", "mysql", "sqlServer", "nginx"]

change()内部声明了一个局部变量 new_skill,并将一个引用类型 的数组 赋值给它,同时又将变量new_skill赋值给了全局变量 skill,此时,这个局部变量 new_skill 就不会被当成垃圾回收了。啊,为什么呢??? 还记得我们在上面说过的 垃圾回收,回收的是那些不再使用的变量,释放它们的内存,因为此时的局部变量 new_skill 并不是一个不再使用 的局部变量了,它被全局变量 skill所引用了( 还在使用),所以就不会被回收了。

上面的这一过程,使用的就是引用计数的垃圾回收方式

引用计数的策略是跟踪记录每个值被使用的次数,当声明了一个变量并将一个引用类型赋值给该变量的时候这个值的引用次数就加1,如果该变量的值变成了另外一个,则这个值的引用次数减1,当这个值的引用次数变为0的时候,说明没有变量在使用,这个值没法被访问了,因此可以将其占用的空间回收,当垃圾回收的时候,就会将 引用次数为0的进行回收,释放对应的内存

let skill = ["唱歌", "弹吉他", "播音"]
function change() {
  let new_skill = ["vue", "react", "node", "webpack", ".net", "mysql", "sqlServer", "nginx"];//被引用次数为0 
  skill = new_skill; ////被引用次数为1
}
change()
console.log(skill) 

管理内存

在我们的项目中,我们肯定是想要用更少的内存,来使得页面有更好的性能。这个时候就需要我们来手动的管理内存了,及时的去释放数据的引用。我们可以只将需要的数据,存入变量。一旦这个数据不再使用了,我们需要手动的给变量赋值 null 来释放数据的引用。(解除引用) 大多需要我们进行手动的解除引用的都是一些全局的变量,因为局部的变量,在离开环境的时候就会被自动清除了。

let skill = ["唱歌", "弹吉他", "播音"]
function change() {
  let new_skill = ["vue", "react", "node", "webpack", ".net", "mysql", "sqlServer", "nginx"]
  skill = new_skill;
  //一个文艺小青年就这样的变成了一个技术宅男
}
change()
console.log(skill) 
skill = null;//将不再使用的变量进行赋值 unll  来释放数据引用

change() 内部声明的局部变量 new_skill 被全局变量 skill 所引用,所以此时变量 new_skill 的引用次数为1,为了让变量 new_skill 被清除引用,在代码的最后一行,赋值一个 null 给全局变量 skill,手动解除了变量 skill 对变量 new_skill 的引用,此时变量 new_skill 的引用次数 减1,所以 当前 new_skill的引用次数为0了。当垃圾回收机制执行的时候,发现 new_skill 的引用次数为 0,就把该变量当成无用变量给清除了,释放了内存,提高了性能。

以上就是W3Cschool编程狮关于js垃圾回收机制原理给你聊的明明白白的相关介绍了,希望对大家有所帮助。

Python 小技巧之 Office 文件转 PDF

thbcm阅读(177)

文章来源于公众号:Python技术 作者:派森酱

在日常的生活工作中,难免需要用到一些 小Tip 来解决工作中遇到的小难题,今天的文章给大家安利一个方便快捷的小技巧,将 Office(doc/docx/ppt/pptx/xls/xlsx)文件批量转换为 PDF 文件。不过在做具体操作之前需要在 PC 安装好 Office,再利用 Python 的 win32com 包来实现 Office 文件的转换操作。

安装 win32com

在实战之前,需要安装 Python 的 win32com,详细安装步骤如下:

使用 pip 命令安装

pip install pywin32

如果我们遇到安装错误,可以通过python -m pip install --upgrade pip更新云端的方式再进行安装即可:

python -m pip install --upgrade pip 

下载离线安装包安装

如果 pip 命令未安装成功的话还可以下载离线包安装,方法步骤如下:首先在官网选择对应的 Python 版本下载离线包: sourceforge.net/projects/pywin32/files/pywin32/Build%20221/ 下载好后傻瓜式安装好即可。

文件转换逻辑

详细代码如下:

class PDFConverter:
    def __init__(self, pathname, export='.'):
        self._handle_postfix = ['doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx'] # 支持转换的文件类型
        self._filename_list = list()  #列出文件
        self._export_folder = os.path.join(os.path.abspath('.'), 'file_server/pdfconver')
        if not os.path.exists(self._export_folder):
            os.mkdir(self._export_folder)
        self._enumerate_filename(pathname)


    def _enumerate_filename(self, pathname):
        '''
        读取所有文件名
        '''
        full_pathname = os.path.abspath(pathname)
        if os.path.isfile(full_pathname):
            if self._is_legal_postfix(full_pathname):
                self._filename_list.append(full_pathname)
            else:
                raise TypeError('文件 {} 后缀名不合法!仅支持如下文件类型:{}。'.format(pathname, '、'.join(self._handle_postfix)))
        elif os.path.isdir(full_pathname):
            for relpath, _, files in os.walk(full_pathname):
                for name in files:
                    filename = os.path.join(full_pathname, relpath, name)
                    if self._is_legal_postfix(filename):
                        self._filename_list.append(os.path.join(filename))
        else:
            raise TypeError('文件/文件夹 {} 不存在或不合法!'.format(pathname))


    def _is_legal_postfix(self, filename):
        return filename.split('.')[-1].lower() in self._handle_postfix and not os.path.basename(filename).startswith(
            '~')


    def run_conver(self):
        print('需要转换的文件数是:', len(self._filename_list))
        for filename in self._filename_list:
            postfix = filename.split('.')[-1].lower()
            funcCall = getattr(self, postfix)
            print('原文件:', filename)
            funcCall(filename)
        print('转换完成!')

doc/docx 转换为 PDF

doc/docx 转换为 PDF 部分代码如下所示:

    def doc(self, filename):
        name = os.path.basename(filename).split('.')[0] + '.pdf'
        exportfile = os.path.join(self._export_folder, name)
        print('保存 PDF 文件:', exportfile)
        gencache.EnsureModule('{00020905-0000-0000-C000-000000000046}', 0, 8, 4)
        pythoncom.CoInitialize()
        w = Dispatch("Word.Application")
        pythoncom.CoInitialize()  # 加上防止 CoInitialize 未加载
        doc = w.Documents.Open(filename)
        doc.ExportAsFixedFormat(exportfile, constants.wdExportFormatPDF,
                                Item=constants.wdExportDocumentWithMarkup,
                                CreateBookmarks=constants.wdExportCreateHeadingBookmarks)
        w.Quit(constants.wdDoNotSaveChanges)
 def docx(self, filename):
        self.doc(filename)

ppt/pptx 转换为 PDF

ppt/pptx 转换为 PDF 部分代码如下:

 def ppt(self, filename):
        name = os.path.basename(filename).split('.')[0] + '.pdf'
        exportfile = os.path.join(self._export_folder, name)
        gencache.EnsureModule('{00020905-0000-0000-C000-000000000046}', 0, 8, 4)
        pythoncom.CoInitialize()
        p = Dispatch("PowerPoint.Application")
        pythoncom.CoInitialize()
        ppt = p.Presentations.Open(filename, False, False, False)
        ppt.ExportAsFixedFormat(exportfile, 2, PrintRange=None)
        print('保存 PDF 文件:', exportfile)
        p.Quit()


    def pptx(self, filename):
        self.ppt(filename)

xls/xlsx 转换为 PDF

    def xls(self, filename):
        name = os.path.basename(filename).split('.')[0] + '.pdf'
        exportfile = os.path.join(self._export_folder, name)
        pythoncom.CoInitialize()
        xlApp = DispatchEx("Excel.Application")
        pythoncom.CoInitialize()
        xlApp.Visible = False
        xlApp.DisplayAlerts = 0
        books = xlApp.Workbooks.Open(filename, False)
        books.ExportAsFixedFormat(0, exportfile)
        books.Close(False)
        print('保存 PDF 文件:', exportfile)
        xlApp.Quit()


    def xlsx(self, filename):
        self.xls(filename) 

执行转换逻辑

if __name__ == "__main__":
    # 支持文件夹批量导入
    #folder = 'tmp'
    #pathname = os.path.join(os.path.abspath('.'), folder)
    # 也支持单个文件的转换
    pathname = "G:/python_study/test.doc"
    pdfConverter = PDFConverter(pathname)
    pdfConverter.run_conver()

以上就是W3Cschool编程狮关于Python 小技巧之 Office 文件转 PDF的相关介绍了,希望对大家有所帮助。

“科班出身”的程序员和“培训出身”的程序员的大型辩论(甩锅)现场

thbcm阅读(186)

文章来源于公众号:Java极客技术 作者:鸭血粉丝

前几天阿粉说阿粉最近换了公司,而且入职之后干掉了公司里面的测试数据库的事情,而接下来的事就比较有意思了,来自“科班出身”的哥们和来自“培训出身”的我的大型辩论(SIBI)现场,也不能说是通俗的甩锅,但是确实有那么点意味。

现场一

“科班程序员”:这功能很好实现呀,直接写几个嵌套for循环,在里面判断一下就行了,直接返回数据就行了,为啥你这写的这么墨迹呢?

“培训程序员”:内心OS:嵌套 for?再加上几个 if,你确定你的数据超过1w条,没有明显的延迟么?于是,只能告诉他,兄弟,这么写肯定是没什么问题,但是你不想一下后期怎么维护么?你这才一两万的数据我都能感觉出明显的延迟了,为啥不能优化一下呢?

说实话,说到这个问题的时候,也避免不了被大家diss,觉得这不是科班出身的程序员能写出来的代码,而事实情况确实是这个样子的,也可能是工作经验不太足,所以很多代码写的不是很给力,也可能是之前的公司做过几次 CodeReview,所以每次在写完代码之后都习惯性的去考虑一下这个代码还能不能优化的更加简单一点,所以考虑的时间要稍微长一点。

于是,兄弟就开始和我较真了,阿粉的策略一直很简单,兄弟,你自己写个三个 for 循环,然后去看看你执行完这个循环的时间,然后想想如果在循环中加入查询数据库的所有的操作,你再想想怎么处理,就比如说,你要比对循环里面的 List 里面是不是有这个的时候,不用写那么多的 for 循环,不然那不就是 X*Y 次了,为啥不再单独的造一个 List 用 contains 来获取呢?

现场二

“科班程序员”:哥,这个功能是不是还可以再继续把这些内容加上,这块我觉得加上它会更加的完善。

“培训程序员”:来自内心的OS,加个锤子,需求上怎么定,我就怎么干,干完了不是就OK了,为什么要多此一举,但是心里这么想,实际还是不能这么干,于是说,这个地方你看怎么改,邮件给我说,抄送给xxx,然后我再改。

也不是说加这个不行,确实是,按照需求完成了工作之后,你再过来给我扯东扯西的,有点让人难以接受不是,你要是说这地方写的有问题,是吧,咱们还能请教一下你这块应该修改成什么样子,你现在过来给我说加功能,你这不是要搞事情,怎么能惯你这个毛病呢,于是二话不说,先发邮件,抄送给领导,谁让加的,别到时候你一句话,加了功能,到最后出问题了,第一时间找的还是我,于是这个功能需求上没有的,自动屏蔽。

阿粉在这里不是说“科班出身”,和“培训出身”之间的差距,没有任何其他的含义,只是这次确实是比较巧合,这个哥们是刚毕业2年的本科,专业是计算机科学与技术的。仅仅是巧合,不要多想呦。

现场三

“科班程序员”:这个简单,几天就能搞定了,不用那么麻烦,

“培训程序员”:这是啥呀,我得先看看基础,然后再实际动手操作吧。

说实话,不得不说,有时候“科班程序员”虽然有些时候会让你感觉到他们有着一些些的优越感,但是技术也确实很给力,比如说在公司要使用一项新技术,他们能够二话不说的几天就能开始干活,而在这些内容上相比较,“培训出身”的程序员反而没有那么给力,而是得先摸清楚基础,毕竟大部分的科班生都是经过学校系统的学习,知识体系更加完整,所以能够更深入的解决问题,但是有一些碍于时间短的原因,没有成功的积累起来经验的时候,还是欠缺点火候的。

这不正是之前网上看的一个图么?

现场四

这个场景就比较有意思了,就是双双联合和产品battle,面对产品的灵魂提问:

“这个需求用户/运营说要改成这样,”,

我们的统一回答,邮件呢?你发个邮件先,然后抄送给那个谁谁谁,单独给我说,我实在是不敢给你这么干,不然改来改去,还是第一版怎么弄,你就先发邮件,证明我们在干活不是吗?你先去准备邮件吧。

这话说的是没有啥毛病吧,这是经验总结出来了,不然等你开了头之后,接下来的事情就比较难办了,能做完还行,这做不完的话,那就相当于你没有干活,所以,对程序员来说,你把做的功能给我罗列清楚,然后提交上去,下发指定哪些功能确定之后,我再做也是完全不虚的。

说了也挺多的了,阿粉也在后边给大家放上一个曾经的面试题,是属于那种上手实践的面试题。

面试官给的一个面试题,而面试题很有意思,大家可以看一下。这个题阿粉是没有回答上来,但是来自科班生的答案,让面试官很满意:

而他的实现方法和我从网上看到的是一模一样的,

Task task = new Task(() => checkCustomerprice());
task.Start();
bool result = task.Result;


Task task2 = new Task(() => checkInventory());
task2.Start();
bool result2 = task2.Result;


 if(result&&result2)
    return true;
 else 
    return  false;

网上的大神也不确定对不对,阿粉觉得这么实现确实也是有道理的,不知道大家的意见是什么样子的呢?

以上就是W3Cschool编程狮关于“科班出身”的程序员和“培训出身”的程序员的大型辩论(甩锅)现场的相关介绍了,希望对大家有所帮助。

如何设计一个看起来很牛逼的API接口

thbcm阅读(202)

文章来源于各种:Java旅途 作者:周明尧

在平时工作中,总会接触到多种接口。前后端数据传输接口,第三方业务平台接口。一个平台的前后端数据传输接口一般都会在内网环境下通信,而且会使用安全框架,所以安全性可以得到很好的保护。这篇文章重点讨论一下提供给第三方平台的业务接口应当如何设计?我们应该考虑哪些问题?

主要从以上三个方面来设计一个安全的API接口。

一 安全性问题

安全性问题是一个接口必须要保证的规范。如果接口保证不了安全性,那么你的接口相当于直接暴露在公网环境中任人蹂躏。

1.1 调用接口的先决条件-token

获取 token 一般会涉及到几个参数appidappkeytimestampnoncesign。我们通过以上几个参数来获取调用系统的凭证。

appidappkey可以直接通过平台线上申请,也可以线下直接颁发。appid是全局唯一的,每个appid将对应一个客户,appkey需要高度保密。

timestamp是时间戳,使用系统当前的 unix 时间戳。时间戳的目的就是为了减轻 DOS 攻击。防止请求被拦截后一直尝试请求接口。服务器端设置时间戳阀值,如果请求时间戳和服务器时间超过阀值,则响应失败。

nonce是随机值。随机值主要是为了增加sign的多变性,也可以保护接口的幂等性,相邻的两次请求nonce不允许重复,如果重复则认为是重复提交,响应失败。

sign是参数签名,将appkeytimestampnonce拼接起来进行md5加密(当然使用其他方式进行不可逆加密也没问题)。

token,使用参数appidtimestampnoncesign来获取token,作为系统调用的唯一凭证。token可以设置一次有效(这样安全性更高),也可以设置时效性,这里推荐设置时效性。如果一次有效的话这个接口的请求频率可能会很高。token推荐加到请求头上,这样可以跟业务参数完全区分开来。

1.2 使用POST作为接口请求方式

一般调用接口最常用的两种方式就是 GET 和 POST 。两者的区别也很明显,GET 请求会将参数暴露在浏览器 URL 中,而且对长度也有限制。为了更高的安全性,所有接口都采用 POST 方式请求。

1.3 客户端IP白名单

ip 白名单是指将接口的访问权限对部分 ip 进行开放。这样就能避免其他 ip 进行访问攻击,设置 ip 白名单比较麻烦的一点就是当你的客户端进行迁移后,就需要重新联系服务提供者添加新的 ip 白名单。设置 ip 白名单的方式很多,除了传统的防火墙之外,spring cloud alibaba 提供的组件 sentinel 也支持白名单设置。为了降低 api 的复杂度,推荐使用防火墙规则进行白名单设置。

1.4 单个接口针对ip限流

限流是为了更好的维护系统稳定性。使用 redis 进行接口调用次数统计,ip+接口地址作为 key,访问次数作为 value ,每次请求 value+1,设置过期时长来限制接口的调用频率。

1.5 记录接口请求日志

使用 aop 全局记录请求日志,快速定位异常请求位置,排查问题原因。

1.6 敏感数据脱敏

在接口调用过程中,可能会涉及到订单号等敏感数据,这类数据通常需要脱敏处理,最常用的方式就是加密。加密方式使用安全性比较高的RSA非对称加密。非对称加密算法有两个密钥,这两个密钥完全不同但又完全匹配。只有使用匹配的一对公钥和私钥,才能完成对明文的加密和解密过程。

二 幂等性问题

幂等性是指任意多次请求的执行结果和一次请求的执行结果所产生的影响相同。说的直白一点就是查询操作无论查询多少次都不会影响数据本身,因此查询操作本身就是幂等的。但是新增操作,每执行一次数据库就会发生变化,所以它是非幂等的。

幂等问题的解决有很多思路,这里讲一种比较严谨的。提供一个生成随机数的接口,随机数全局唯一。调用接口的时候带入随机数。第一次调用,业务处理成功后,将随机数作为key,操作结果作为 value,存入 redis,同时设置过期时长。第二次调用,查询 redis,如果 key 存在,则证明是重复提交,直接返回错误。

三 数据规范问题

3.1 版本控制

一套成熟的 API 文档,一旦发布是不允许随意修改接口的。这时候如果想新增或者修改接口,就需要加入版本控制,版本号可以是整数类型,也可以是浮点数类型。一般接口地址都会带上版本号,http://ip:port//v1/list

3.2 响应状态码规范

一个牛逼的 API,还需要提供简单明了的响应值,根据状态码就可以大概知道问题所在。我们采用 http 的状态码进行数据封装,例如200表示请求成功,4xx表示客户端错误,5xx表示服务器内部发生错误。状态码设计参考如下:

分类 描述
1xx 信息,服务器收到请求,需要请求者继续执行操作
2xx 成功
3xx 重定向,需要进一步的操作以完成请求
4xx 客户端错误,请求包含语法错误或无法完成请求
5xx 服务端错误

状态码枚举类:

public enum CodeEnum {


    // 根据业务需求进行添加
    SUCCESS(200,"处理成功"),
    ERROR_PATH(404,"请求地址错误"),
    ERROR_SERVER(505,"服务器内部发生错误");

    
    private int code;
    private String message;

    
    CodeEnum(int code, String message) {
        this.code = code;
        this.message = message;
    }


    public int getCode() {
        return code;
    }


    public void setCode(int code) {
        this.code = code;
    }


    public String getMessage() {
        return message;
    }


    public void setMessage(String message) {
        this.message = message;
    }
}

3.3 统一响应数据格式

为了方便给客户端响应,响应数据会包含三个属性,状态码(code),信息描述(message),响应数据(data)。客户端根据状态码及信息描述可快速知道接口,如果状态码返回成功,再开始处理数据。

响应结果定义及常用方法:

public class R implements Serializable {


    private static final long serialVersionUID = 793034041048451317L;


    private int code;
    private String message;
    private Object data = null;


    public int getCode() {
        return code;
    }
    public void setCode(int code) {
        this.code = code;
    }


    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }


    public Object getData() {
        return data;
    }


    /**
     * 放入响应枚举
     */
    public R fillCode(CodeEnum codeEnum){
        this.setCode(codeEnum.getCode());
        this.setMessage(codeEnum.getMessage());
        return this;
    }


    /**
     * 放入响应码及信息
     */
    public R fillCode(int code, String message){
        this.setCode(code);
        this.setMessage(message);
        return this;
    }


    /**
     * 处理成功,放入自定义业务数据集合
     */
    public R fillData(Object data) {
        this.setCode(CodeEnum.SUCCESS.getCode());
        this.setMessage(CodeEnum.SUCCESS.getMessage());
        this.data = data;
        return this;
    }
}

总结

本篇文章从安全性、幂等性、数据规范等方面讨论了 API 设计规范。除此之外,一个好的 API 还少不了一个优秀的接口文档。接口文档的可读性非常重要,虽然很多程序员都不喜欢写文档,而且不喜欢别人不写文档。为了不增加程序员的压力,推荐使用 swagger 或其他接口管理工具,通过简单配置,就可以在开发中测试接口的连通性,上线后也可以生成离线文档用于管理API。

以上就是W3Cschool编程狮关于如何设计一个看起来很牛逼的API接口的相关介绍了,希望对大家有所帮助。

联系我们