秒杀系统优化方案(上)吐血整理 - 开拖拉机的蜡笔小新 - 博客园

mikel阅读(612)

来源: 秒杀系统优化方案(上)吐血整理 – 开拖拉机的蜡笔小新 – 博客园

前一段时间好好研究了秒杀的问题,我把里面的问题好好总结了,可以说是比较全面的了,真的是吐血整理了。

由于我先是在word中整理的,格式都整理得比较好,放到博客上格式挺难调,暂时按word的格式来吧,有时间了在好好排版下。

主要需要解决的问题有两个:

  1. 高并发对数据库产生的压力
  2. 竞争状态下如何解决库存的正确减少(超卖问题)

优化的思路:

1) 尽量将请求拦截在系统上游

2)读多写少经量多使用缓存
3) redis缓存 +RabbitMQ+ mySQL 批量入库

1.   初始秒杀设计

1.1 业务分析

秒杀系统业务流程如下:

由图可以发现,整个系统其实是针对库存做的系统。用户成功秒杀商品,对于我们系统的操作就是:1.减库存。2.记录用户的购买明细。下面看看我们用户对库存的业务分析:

记录用户的秒杀成功信息,我们需要记录:1.谁购买成功了。2.购买成功的时间/有效期。这些数据组成了用户的秒杀成功信息,也就是用户的购买行为。

为什么我们的系统需要事务?

1.若是用户成功秒杀商品我们记录了其购买明细却没有减库存。导致商品的超卖

2.减了库存却没有记录用户的购买明细。导致商品的少卖。对于上述两个故障,若是没有事务的支持,损失最大的无疑是我们的用户和商家。在MySQL中,它内置的事务机制,可以准确的帮我们完成减库存和记录用户购买明细的过程。

1.2  难点分析

当用户A秒杀id为10的商品时,此时MySQL需要进行的操作是:

1.开启事务。2.更新商品的库存信息。3.添加用户的购买明细,包括用户秒杀的商品id以及唯一标识用户身份的信息如电话号码等。4.提交事务。

若此时有另一个用户B也在秒杀这件id为10的商品,他就需要等待,等待到用户A成功秒杀到这件商品,然后MySQL成功的提交了事务他才能拿到这个id为10的商品的锁从而进行秒杀,而同一时间是不可能只有用户B在等待,肯定是有很多很多的用户都在等待竞争行级锁。秒杀的难点就在这里,如何高效的处理这些竞争?如何高效的完成事务?

1.3 功能实现

我们只是实现秒杀的一些功能:1.秒杀接口的暴露。2.执行秒杀的操作。3.相关查询,比如说列表查询,详情页查询。我们实现这三个功能即可。

1.4 数据库设计

Seckill秒杀表单

Success_seckill购买明细表

在购买明细表中seckill_id和user_phone是联合主键,当重复秒杀的时候,加入ignore防止报错,只是会返回0,表示重复秒杀。

INSERT ignore INTO success_killed(seckill_id,user_phone,state)
VALUES (#{seckillId},#{userPhone},0)

在购买明细表中seckill_id和user_phone是联合主键,当重复秒杀的时候,加入ignore防止报错,只是会返回0,表示重复秒杀。

INSERT ignore INTO success_killed(seckill_id,user_phone,state)
VALUES (#{seckillId},#{userPhone},0)

1.5 DAO层设计

秒杀表的DAO:减库存(id,nowtime)、由id查询商品、由偏移量查询商品

购买明细表的DAO:插入购买明细、根据商品id查询明细SucceesKill对象(携带Seckill对象)—mybatis的复合查询

减库存和增加明细的sql

复制代码
<update id="reduceNumber">
        UPDATE seckill
        SET number = number-1
        WHERE seckill_id=#{seckillId}
        AND start_time <![CDATA[ <= ]]> #{killTime}
        AND end_time >= #{killTime}
        AND number > 0;
</update>
<insert id="insertSuccessKilled">
        <!--当出现主键冲突时(即重复秒杀时),会报错;不想让程序报错,加入ignore-->
        INSERT ignore INTO success_killed(seckill_id,user_phone,state)
        VALUES (#{seckillId},#{userPhone},0)
 </insert>
复制代码

 

1.6 Service层设计

暴露秒杀地址(接口)DTO

复制代码
public class Exposer {
    //是否开启秒杀
    private boolean exposed;
    //加密措施
    private String md5;
    private long seckillId;
    //系统当前时间(毫秒)
    private long now;
    //秒杀的开启时间
    private long start;
    //秒杀的结束时间
    private long end;}
复制代码

 

封装执行秒杀后的结果:是否秒杀成功

复制代码
public class SeckillExecution {
    private long seckillId;
    //秒杀执行结果的状态
    private int state;
    //状态的明文标识
    private String stateInfo;
    //当秒杀成功时,需要传递秒杀成功的对象回去
    private SuccessKilled successKilled;}
复制代码

 

秒杀过程

接口暴露:

复制代码
public Exposer exportSeckillUrl(long seckillId) {
        //缓存优化
        Seckill seckill = getById(seckillId);
        //若是秒杀未开启
        Date startTime = seckill.getStartTime();
        Date endTime = seckill.getEndTime();
        //系统当前时间
        Date nowTime = new Date();
        if (startTime.getTime() > nowTime.getTime() || endTime.getTime() < nowTime.getTime()) {
            return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime());
        }
        //秒杀开启,返回秒杀商品的id、用给接口加密的md5
        String md5 = getMD5(seckillId);
        return new Exposer(true, md5, seckillId);
}
复制代码

如果当前时间还没有到秒杀时间或者已经超过秒杀时间,秒杀处于关闭状态,那么返回秒杀的开始时间和结束时间;如果当前时间处在秒杀时间内,返回暴露地址(秒杀商品的id、用给接口加密的md5)

为什么要进行MD5加密?

我们用MD5加密的方式对秒杀地址(seckill_id)进行加密,暴露给前端用户。当用户执行秒杀的时候传递seckill_id和MD5,程序拿着seckill_id根据设置的盐值计算MD5,如果与传递的md5不一致,则表示地址被篡改了。

 

为什么要进行秒杀接口暴露的控制或者说进行秒杀接口的隐藏?

现实中有的用户回通过浏览器插件提前知道秒杀接口,填入参数和地址来实现自动秒杀,这对于其他用户来说是不公平的,我们也不希望看到这种情况。所以我们可以控制让用户在没有到秒杀时间的时候不能获取到秒杀地址,只返回秒杀的开始时间。当到秒杀时间的时候才

返回秒杀地址即seckill_id以及根据seckill_id和salt加密的MD5,前端再次拿着seckill_id和MD5才能执行秒杀。假如用户在秒杀开始前猜测到秒杀地址seckill_id去请求秒杀,也是不会成功的,因为它拿不到需要验证的MD5。这里的MD5相当于是用户进行秒杀的凭证。

 

执行秒杀:

复制代码
 //秒杀是否成功,成功: 增加明细,减库存;失败:抛出异常,事务回滚
    @Transactional
    public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)  throws SeckillException, RepeatKillException, SeckillCloseException {
        if (md5 == null || !md5.equals(getMD5(seckillId))) {
            //秒杀数据被重写了
            throw new SeckillException("seckill data rewrite");
        }
        //执行秒杀逻辑:增加购买明细+减库存
        Date nowTime = new Date();
        try {
            //先增加明细,然后再执行减库存的操作
            int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
            //看是否该明细被重复插入,即用户是否重复秒杀
            if (insertCount <= 0) {
                throw new RepeatKillException("seckill repeated");
            } else {
                //减库存,热点商品竞争
                int updateCount = seckillDao.reduceNumber(seckillId, nowTime);
                if (updateCount <= 0) {
                    //没有更新库存记录,说明秒杀结束或者是已经卖完 rollback
                    throw new SeckillCloseException("seckill is closed");
                } else {
                    //秒杀成功,得到成功插入的明细记录,并返回成功秒杀的信息 commit
                    SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
                    return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);
                }
            }
        } catch (SeckillCloseException e1) {
            throw e1;
        } catch (RepeatKillException e2) {
            throw e2;
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            //所以编译期异常转化为运行期异常
            throw new SeckillException("seckill inner error :" + e.getMessage());
        }
    }
复制代码

首先检查用户是否已经登录,查看cookie中是否有phone的信息,如果没有,返回没有注册的错误信息。

接着执行秒杀,首先验证md5,看地址是否被篡改。先增加明细(为什么要先增加明细见后面优化的过程),看是否该明细被重复插入,即用户是否重复秒杀,如果是,抛异常。然后减库存,因为sql在减库存的时候判断了当前时间和秒杀时间是否对应,如果数据库update返回0没有更新库存记录,说明秒杀结束;或者是库存已经没有主动抛出错误rollback。(前面在获取秒杀地址的时候已经挡住了秒杀关闭的请求(没到时间或者时间已过),然后从获取到秒杀地址到执行秒杀还可能会在这段时间秒杀结束)

最后秒杀成功,得到购买明细信息,接着commit。

注意事务在这里的处理:

Spring事务异常回滚,捕获异常不抛出就不会回滚

1.7 Web层设计

交互流程

2.   初始优化设计

红色部分代表可能高并发的点,绿色表示没有影响

2.1 详情页缓存

通过CDN缓存静态资源,来抗峰值。

动静态数据分离

详情页静态资源是部署在CDN节点中,也就是说访问静态资源或者详情页是不用访问我们的系统的。

限流小技巧:用户提交之后按钮置灰,禁止重复提交 

为什么要单独ajax请求获取服务器的时间?

为了保持时间一致,因为详情页放在CDN上和系统存放的位置是分离的。

2.2 秒杀接口地址缓存

无法使用CDN是因为,CDN适合的请求的资源是不易变化的。

秒杀接口是变化的,可以使用redis服务端缓存可以用集群抗住非常大的并发。1秒钟可以承受10万qps。多个Redis组成集群,可以到100w个qps

一致性:当秒杀的对象改变的时候修改我们的数据库同时修改缓存。

原本查询秒杀商品时是通过主键直接去数据库查询的,选择将数据缓存在Redis,在查询秒杀商品时先去Redis缓存中查询,以此降低数据库的压力。如果在缓存中查询不到数据再去数据库中查询,再将查询到的数据放入Redis缓存中,这样下次就可以直接去缓存中直接查询到。

这里有一个继续优化的点:在redis中存放对象是将对象序列化成byte字节。

通过Jedis储存对象的方式有大概三种

  1. 本项目采用的方式:将对象序列化成byte字节,最终存byte字节;
  2. 对象转hashmap,也就是你想表达的hash的形式,最终存map;
  3. 对象转json,最终存json,其实也就是字符串

其实如果你是平常的项目,并发不高,三个选择都可以,这种情况下以hash的形式更加灵活,可以对象的单个属性,但是问题来了,在秒杀的场景下,三者的效率差别很大。

10w数据

时间

内存占用

存json

10s

14M

存byte

6s

6M

存jsonMap

10s

20M

存byteMap

4s

4M

取json

7s

取byte

4s

取jsonmap

7s

取bytemap

4s

bytemap最快啊,为啥不用啊,因为项目用了超级高性能的自定义序列化工具protostuff。

2.3 秒杀操作优化

Mysql真的低效吗?

在mysql端一条update压力测试约4wQPS,即使是现在最好的秒杀产品应该也达不到这个数字。

然而实际上远没有这么高的QPS,那么时间消耗在哪呢?

串行化操作,大量的堵塞

2.3.1 瓶颈分析

客户端执行update,当我们的sql通过网络发送到mysql的时候,这本身就有网络延迟在里面,并且还有GC的时间,GC又分为新生代GC和老年代GC,新生代会暂停所有的事务代码,也就是我们的java代码,一般在几十毫秒

也即是说如果由java客户端去控制这些事务的话,update减库存,网络延迟,update数据操作结果返回,然后执行GC;然后执行insert,发生网络延迟,等待insert执行结果返回,也可能出现GC,最后commit或者rollback。当这些执行完了之后,第二个等待行锁的线程才有可能拿到这个数据行的锁,再去执行update减库存。

不是我们的mysql慢,也不是java慢,可能存在我们的java客户端执行这些sql,然后等待这些sql的结果,再去做判断再去执行这些sql,这一长串的事务在java客户端执行,但是java客户端和数据库之间会有网络延迟,或者是GC这些时间也要加载事务的执行周期里面,而同一行的事务是串行化的。

那么我们的QPS分析就是所有的sql执行时间+网络延迟时间+可能的GC,这就是当前执行一行数据的时间。

优化的方向

2.3.2 简单优化

将原本先update(减库存)再进行insert(插入购买明细)的步骤改成:先insert再update。

为什么要先insertupdate

首先是在更新操作的时候给行加锁,插入并不会加锁,如果更新操作在前,那么就需要执行完更新和插入以后事务提交或回滚才释放锁。而如果插入在前,更新在后,那么只有在更新时才会加行锁,之后在更新完以后事务提交或回滚释放锁。

在这里,插入是可以并行的,而更新由于会加行级锁是串行的

也就是说是更新在前加锁和释放锁之间两次的网络延迟和GC,如果插入在前则加锁和释放锁之间只有一次的网络延迟和GC,也就是减少的持有锁的时间。

这里先insert并不是忽略了库存不足的情况,而是因为insert和update是在同一个事务里,光是insert并不一定会提交,只有在update成功才会提交,所以并不会造成过量插入秒杀成功记录。

2.3.3 深度优化

客户端逻辑事务SQLMYSQL端执行,完全屏蔽网络延迟和GCMYSQL只需告诉最终结果。

1. 阿里巴巴做了一个mysql源码层的修改方案,当执行完update之后,它会自动做回滚,回滚的条件影响的记录数是1,就会commit;如果是0就会rollback,不由java客户端来控制commit或者rollback,不给java客户端和mysql之间通信的网络延迟,本质上减低了网络延迟或者GC的干扰,但是这个成本高,要修改mysql源码,只有大公司能做。

2.我们可以将执行秒杀操作时的insert和update放到MySQL服务端的存储过程里,而Java客户端直接调用这个存储过程,这样就可以避免网络延迟和可能发生的GC影响。另外,由于我们使用了存储过程,也就使用不到Spring的事务管理了,因为在存储过程里我们会直接启用一个事务。

2.3.4 优化总结

 

预知后事如何,请看下篇分解:秒杀系统优化方案(下)吐血整理

ASP.NET Web 应用 Docker踩坑历程 - 毛毛虫 - 博客园

mikel阅读(767)

来源: ASP.NET Web 应用 Docker踩坑历程 – 毛毛虫 – 博客园

听说Docker这玩意挺长时间了,新建Web应用的时候,也注意到有个启用Docker的选项。
前两天扫了一眼《【大话云原生】煮饺子与docker、kubernetes之间的关系》,觉得有点意思,决定试试Docker。
然后被坑、百度…
现将整个过程记录一下

一、新建项目

点击创建,被通知“需要安装Docker Desktop”,于是下载、安装、按要求重启电脑。

运行Docker Desktop,被通知“需要安装WSL 2”,于是又下载、安装。

二、运行项目

按 F5 运行项目,vs停在 Info: C:\Users\catzhou\vsdbg\vs2017u5 exists, deleting.不动了。

百度到《visual studio 容器工具首次加载太慢 vsdbg\vs2017u5 exists, deleting 的解决方案》这篇文章,依葫芦画瓢搞定。

再次 F5,成果如下:

吐槽一下:下载vsdbug的两个包速度实在太慢了(为此特意安装了迅雷),然后创建文件夹、4个文本文件颇不容易。俺把vs2017u5打了个包(版本是:17.0.10712.2),你直接下载解压到vsdbg即可。

三、发布到Docker Hub

  1. Docker Hub注册了一个用户
  2. Docker Desktop登录
    登陆后

    变成了
  3. 发布




    点击发布,成果如下:

四、部署到 阿里云-轻量应用服务器-Docker应用镜像

花了60元人民币,买了一个月的Docker应用镜像服务器

  1. 远程连接到服务器
  2. 切换到root账号
    sudo su root
  3. 拉取镜像
    docker pull catzhou2021/webapp1
  4. 查看镜像
    docker images
  5. 创建容器c1并后台运行
    docker run --name=c1 -p 12345:80 -d catzhou2021/webapp1
  6. 查看是否正常运行
    curl http://localhost:12345
  7. 设置防火墙-添加规则

  8. 浏览器访问

如此,大功告成。

PHP判断数组是否为空的常用方法(五种方法)-php教程-PHP中文网

mikel阅读(1753)

来源: PHP判断数组是否为空的常用方法(五种方法)-php教程-PHP中文网

本文介绍了PHP开发中遇到的数组问题,小编在这里给大家总结了5中方法关于php判断数组是否为空问题,需要的朋友参考下
本文介绍了PHP开发中遇到的数组问题,这里介绍了判断PHP数组为空的5种方法,有需要的朋友可以借鉴参考一下。

1. isset功能:判断变量是否被初始化

说明:它并不会判断变量是否为空,并且可以用来判断数组中元素是否被定义过

注意:当使用isset来判断数组元素是否被初始化过时,它的效率比array_key_exists高4倍左右

1

2

3

4

5

6

7

8

9

$a = ”;

$a[‘c’] = ”;

if (!isset($a)) echo ‘$a 未被初始化’ . “”;

if (!isset($b)) echo ‘$b 未被初始化’ . “”;

if (isset($a[‘c’])) echo ‘$a 已经被初始化’ . “”;

// 显示结果为

// $b 未被初始化

// $a 已经被初始化

2. empty功能:检测变量是否为”空”

说明:任何一个未初始化的变量、值为 0 或 false 或 空字符串”” 或 null的变量、空数组、没有任何属性的对象,都将判断为empty==true

注意1:未初始化的变量也能被empty检测为”空”

注意2:empty只能检测变量,而不能检测语句

1

2

3

4

5

6

7

8

$a = 0;

$b = ”;

$c = array();

if (empty($a)) echo ‘$a 为空’ . “”;

if (empty($b)) echo ‘$b 为空’ . “”;

if (empty($c)) echo ‘$c 为空’ . “”;

if (empty($d)) echo ‘$d 为空’ . “”;

3. var == null功能:判断变量是否为”空”

说明:值为 0 或 false 或 空字符串”” 或 null的变量、空数组、都将判断为 null

注意:与empty的显著不同就是:变量未初始化时 var == null 将会报错。

1

2

3

4

5

6

7

8

9

10

$a = 0;

$b = array();

if ($a == null) echo ‘$a 为空’ . “”;

if ($b == null) echo ‘$b 为空’ . “”;

if ($c == null) echo ‘$b 为空’ . “”;

// 显示结果为

// $a 为空

// $b 为空

// Undefined variable: c

4. is_null功能:检测变量是否为”null”

说明:当变量被赋值为”null”时,检测结果为true

注意1:null不区分大小写:$a = null; $a = NULL 没有任何区别

注意2:仅在变量的值为”null”时,检测结果才为true,0、空字符串、false、空数组都检测为false

注意3:变量未初始化时,程序将会报错

1

2

3

4

5

6

7

8

9

$a = null;

$b = false;

if (is_null($a)) echo ‘$a 为NULL’ . “”;

if (is_null($b)) echo ‘$b 为NULL’ . “”;

if (is_null($c)) echo ‘$c 为NULL’ . “”;

// 显示结果为

// $a 为NULL

// Undefined variable: c

5. var === null功能:检测变量是否为”null”,同时变量的类型也必须是”null”

说明:当变量被赋值为”null”时,同时变量的类型也是”null”时,检测结果为true

注意1:在判断为”null”上,全等于和is_null的作用相同

注意2:变量未初始化时,程序将会报错

总结:

PHP中,”NULL” 和 “空” 是2个概念。

isset 主要用来判断变量是否被初始化过

empty 可以将值为 “假”、”空”、”0″、”NULL”、”未初始化” 的变量都判断为TRUE

is_null 仅把值为 “NULL” 的变量判断为TRUE

var == null 把值为 “假”、”空”、”0″、”NULL” 的变量都判断为TRUE

var === null 仅把值为 “NULL” 的变量判断为TRUE

注意:在判断一个变量是否真正为”NULL”时,大多使用 is_null,从而避免”false”、”0″等值的干扰。

以上所述是小编给大家介绍的PHP判断数组是否为空的常用方法(五种方法),希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对PHP中文网的支持!

更多PHP判断数组是否为空的常用方法(五种方法)相关文章请关注PHP中文网!

Electron从零开始——介绍 - 知乎

mikel阅读(806)

来源: Electron从零开始——介绍 – 知乎

其实在 Electron 出现之前,如果你问我做桌面应用需要什么,我的第一个想法是 CC++C#Java 以及微软的 Visual Basic 等等语言(Sorry 我不是个苹果党,第一时间想不起 Object-C),外加体积巨大的 IDE(比如我到今天依然不喜欢的 Visual Studio 和曾经为了玩 Minecraft Mod 开发装的 Eclipse 等等),当然也还有各种编辑器 + 编译器的组合,比如 Notepad++  GCC……

我认识 Electron 项目应该是比较晚的了,是因为突然有一天微软发布了 Visual Studio Code 这个跨平台、开源、免费的编辑器,然后在浏览这个应用的文档时,我突然发现这个东东居然用的是 TypeScript + Electron 开发出来的,而 TypeScript 其实就是 JavaScript 的超集……这个冲击了我的世界观,要知道直接使用 JavaScript 开发桌面应用以前不是没有过,但是能够提供像是 VS Code 这么好用且顺滑的感觉,在之前是不敢想象的,甚至为了一个小小的工具,我都要在 VS 里面先做窗口界面,然后用 C# 在那里搞来搞去,可能是对桌面端应用的不熟悉,我做这种工具总是没有很顺手的感觉(对,这方面我的确很渣);而 Electron 的出现,让我有了可以使用自己熟悉的语言以及简单的编辑工具,就可以做出一个小桌面应用的希望。

当然认真学了一阵时间之后,我发现它其实就是把 V8 引擎单独包装起来,使之成为可以在桌面运行的类浏览器平台,从而使得我们写的 SPA 独立运行在系统桌面段;又因为它其实是基于 SPA 的,所以跨平台就成为了其最基础的能力,除了在调用系统功能和最终打包的时候需要考虑到不同平台的差异,其他方面完全不用考虑,使得同一版本的应用在不同系统上也可以保持一致功能,而不需要重新开发一个不同内核、不同细节项、不同技术栈的应用了,对于个人开发者,小团队乃至大型团队的中小型项目,都是个非常非常棒的特性。

我的作品本身其实也很渣,技术层面并没有什么特别的地方,甚至最初的两个小应用,还是用了 JQuery 作为 DOM 操作技术栈,其中一个基本没有在 Github 上面更新过,就单纯的是写出来玩的,一个读书应用(这个就不截图了,太渣自己看了都不好意思);另一个有持续更新一段时间,但是也没有完成,因为最近在大改,从 JQuery 改为 Vue,所以可以看看以前的样子:


这是一个投资理财的辅助分析工具,包括了三个小模块:股票、指数基金以及可转债,因为是辅助分析,所以有些部分是需要自己已经了解理论才能一眼看懂的,其界面如下(没做菜单,因为按钮对于不是很了解电脑的人更直观)。

主界面:

股票模块主界面(图表这里就不截图了,因为在修改,目前不太好看):

股票模块——白马组合(以防有人说我荐股,这里把股票名称和代码隐藏了):

股票模块——便宜组合(关键信息已隐藏):

股票模块——券商股:

指数基金模块(这里主要是行业指数的博格公式,长投温度考虑到可以很容易的获取,所以没有做进来):

点击查看博格公式,可以看到计算结果:

因为上述这些都是共有的信息,而且无论是否明白博格公式,我们也不可能参照旧数据来进行投资,所以这里没有隐藏;这里应用做的事情,其实就是简单的把需要手工计算的东西自动计算并汇总,所以没什么技术含量的。

可转债模块——可转债组合(关键信息已隐藏):

行情简表和待发转债按钮会直接打开集思录:

关于界面:

以上就是我这个简单的小工具的演示,当然或许做成移动端更好,毕竟现在使用电脑进行投资的人不多了,不过在实际使用的过程中,我学习到的投资理财工具有很多地方还是需要计算和分析,使用电脑明显优于移动端的体验,所以第一步先做了桌面端的应用;外加我现在算是转了行,而且最近正在培训阶段,精力也不太好分配,所以改动技术栈还是需要很久的(一天可能就只剩下了2个小时左右的闲暇,我还要更新喜马拉雅上面的有声小说,所以只有周末是比较好的空暇时间了),这样我打算和这个专栏一样,慢慢的来,正好可以重新梳理自己对 Electron 的理解,并且让自己的应用“升级”为正式版。

好了,这篇文章少见的扯了一大堆废话,接下来先好好的了解一下 Electron 究竟是个什么东东。


就像我上面提到过的,Electron 可以让我们使用纯 JavaScript 调用丰富的原生 API 来创建桌面应用;可以把它看作是专注于桌面应用而不是 web 服务器的 io.js 的一个变体。

当然这不意味着 Electron 是绑定了 GUI 库的 JavaScript;相反,Electron 使用 web 页面作为它的 GUI,所以可以把它看作成一个被 JavaScript 控制的,精简版的 Chromium 浏览器——从这个角度来理解,我们就明白了,所有和系统的交互功能,Electron 这个平台已经搞定了,我们所需要的就是创建一个自己喜欢的界面,一个或多个核心的功能,以及调用它提供给我们的 API 就好。

因为 Electron 本身其实也是 Node.js 的一个第三方项目,所以在开发的时候,我们首先需要已经安装了 Node.js,以及包管理器 NPM,外加一个顺手的编辑器:我个人用 VS Code 很顺手,当然如果喜欢用别的编辑器也没问题,比如前文提到的 Notepad++,或者著名的 Sublime TextAtom(话说这个也是 Electron 做的)、Komodo Edit……等等等等,都可以;基本准备就完成了。

接下来,用 NPM 在本地安装一个 Electron 的副本即可,可以选择全局安装或者针对项目单独安装:

npm i -g electron

# 或者
npm i --save-dev electron
当然我这里因为已经安装过,所以相当于升了个级

一个基础的 Electron 项目结果如下:

my-electron-app/
├── package.json
├── main.js
└── index.html

因为 Node.js 对项目结构以及命名还是有点要求的,所以我个人推荐用一些 Electron Cli 工具来进行项目启动和初始化,这样会相对省一些事,比如我之前使用过的 electron-forge,或者直接搭配 Vue 来建立的 electron-vue 都可以;具体的 cli 工具我们可以按照自己的喜好选择,如果更倾向用原生 JS 或者想要自己指定框架的,electron-forge 是个好选择:

我们也可以注意到,因为它是用了很多比较老的第三方库,所以有不少警告信息

这里稍稍演示一下通过 electron-forge 来建立新项目的过程,其实很简单,只要新建一个文件夹,定位到文件夹之后通过 init 命令指定当前文件夹为新项目:

electron-forge init

此时 electron-forge 会自动检查当前环境,并安装对应的模块:

这里要留意,如果 NPM 的映像是指向淘宝的话,整个过程会快很多。

完成后,初始配置就结束了:

此时,我们就可以打开 src 目录下的 index.js 来查看初始代码了:

import { app, BrowserWindow } from 'electron';

// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require('electron-squirrel-startup')) { // eslint-disable-line global-require
  app.quit();
}

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;

const createWindow = () => {
  // Create the browser window.
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
  });

  // and load the index.html of the app.
  mainWindow.loadURL(`file://${__dirname}/index.html`);

  // Open the DevTools.
  mainWindow.webContents.openDevTools();

  // Emitted when the window is closed.
  mainWindow.on('closed', () => {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    mainWindow = null;
  });
};

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);

// Quit when all windows are closed.
app.on('window-all-closed', () => {
  // On OS X it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', () => {
  // On OS X it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (mainWindow === null) {
    createWindow();
  }
});

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.

这样我们就可以在这些初始代码上面,进行自己想要的修改了,当然我们下一章节会开始看看,这些基础的代码究竟是个什么意思;不过目前来看,应该已经很清晰了,毕竟 electron-forge 生成的初始代码,已经有了详尽的注释帮助我们理解了。

其实这个初始代码已经可以运行了,只要运行如下命令:

electron-forge start

可以看到,初始的界面是包括了菜单栏、主界面以及控制台的,而控制台的出现就分明体现了它本质上是个浏览器的特点,这也使得我们在本地及时的进行调整、修复和新功能测试变得非常简单。

PHP调用阿里云短信接口报错的解决 - 童年的回忆 - 博客园

mikel阅读(1201)

来源: PHP调用阿里云短信接口报错的解决 – 童年的回忆 – 博客园

调用短信接口错误如下:

cURL error 60: SSL certificate problem: unable to get local issuer certificate (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for https://dysmsapi.aliyuncs.com/?Action=SendSms&Format=json&Version=2017-05-25&Timestamp=2021-07-31T03%3A45%3A33Z&…….

这是因为没有配置信任的服务器HTTPS验证。默认情况下,cURL被设为不信任任何CAs,因此浏览器无法通过HTTPs访问你服务器。

到下面站点下最新的pem文件

https://curl.se/docs/caextract.html

 

复制这个文件到php的安装目录下

打开php.ini文件,搜索curl.cainfo  去掉前面的#注释   填上该文件的绝对路径,如下图所示:

 

注意:openssl扩展需要开启

重启nginx/Apapche服务器,问题解决

 Android Studio编辑器中间竖线的去除_芸香大官人的博客-CSDN博客_android studio 中间竖线

mikel阅读(837)

来源: (2条消息) Android Studio编辑器中间竖线的去除_芸香大官人的博客-CSDN博客_android studio 中间竖线

题外(多次没能记住竖线的去除,第一次写博客,突然觉得,人类文明得以长存,在于文字的保存。)

本方法基于Android Studio版本:Android Studio 3.3.2
进入【File】=>【Setting】=>【Editor】=>【General】=>【Appearance】
去除勾选“Show hard wrap guide(configured in Code Style options)”
点击【Apply】或者【OK】,如图所示:

————————————————
版权声明:本文为CSDN博主「芸香大官人」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/RoseChilde/article/details/88586684

关于JS的可选参数,该知道的都在这里了 - 简书

mikel阅读(1570)

来源: 关于JS的可选参数,该知道的都在这里了 – 简书

前言

我是一枚土生土长的iOS程序猿,之所以会写关于JS的文章,主要是因为我最近在负责组里的部分运营需求,所以写前端也逐渐比较多,于是乎学习JS势在必行,所以就开写了。

正文

我们都知道函数是js里的一等公民,并且在js里,你声明一个函数——你可以定制多个参数,与此同时,你在调用该函数的时候不需要传入所有的参数,它就能正常执行——只不过这些参数默认就是undefined而已。所以似乎js的函数天生就带有可选参数这个功能,只不过在你不进行定制的时候它们都具有一个“统一”的值罢了。
所以,我们也知道,除非函数里就实现了针对某个参数为undefined时的行为,让参数为undefined是比较危险的。

该如何实现函数内的可选参数,我们将用js里的构造方法来举例(假如我们要实现一个Person类):

function Person(name, age, height, weight) {
    this.name = name;
    this.age = age;
    this.height = height;
    this.weight = weight;
}

通常我们都会这么实现,现在假想我们要将heightweight参数设为可选参数,可选参数的实质就是令未被赋值的参数具有一个默认值,直白地处理,我们可以写成这样:

function Person(name, age, height, weight) {
    var nHeight = height || 0;
    var nWeight = weight || 0; 
    this.name = name;
    this.age = age;
    this.height = nHeight ;
    this.weight = nWeight ;
}

但是因为我们这里的参数只是简单的赋值给属性,所以我们可以这么写:

function Person(name, age, height, weight) {
    this.name = name;
    this.age = age;
    this.height = height || 0;
    this.weight = weight || 0;
}

面对这样的实现,不难发现它还有点问题——这种实现永远只能把可选参数连续地声明在函数的末端,必要的参数必须得放前面,因为它只能这样生成:

var person = new Person("Turtle", 23);

假如我是ageweight为可选参数呢?

function Person(name, age, height, weight) {
    this.name = name;
    this.age = age || 0;
    this.height = height;
    this.weight = weight || 0;
}
// 我就不能这样生成Person对象了
var person = new Person("Turtle", "170cm");

因为这样子赋值,没法让170cm赋到height属性上,只会赋到age属性上,这显然不是我们想要的。

一种简便的解决方法是不定义这么多的参数赋值,而统一使用一个对象来进行赋值:

function Person(options) {
    this.name = options.name;
    this.age = options.age || 0;
    this.height = options.height;
    this.weight = options.weight || 0;
}

var options = {
    name: "Turtle",
    height: "170cm",
};
var person = new Person(options);
//or
var person2 = new Person({
    name: "Turtle",
    height: "170cm",
});

而在es6里,它支持了为参数提供默认值,所以你可以这么干:

function Person({name, age = 0, height, weight = 0} = {}) {
    this.name = name;
    this.age = age;
    this.height = height;
    this.weight = weight;
}
// 效果和上面一致

作者:turtleeeee
链接:https://www.jianshu.com/p/55fc2be7e0f0
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 tp中U方法在传值变量时的运用_chevinswift的博客-CSDN博客

mikel阅读(768)

来源: (3条消息) tp中U方法在传值变量时的运用_chevinswift的博客-CSDN博客

U方法用于完成对URL地址的组装,特点在于可以自动根据当前的URL模式和设置生成对应的URL地址,格式为:

U(‘地址’,’参数’,’伪静态’,’是否跳转’,’显示域名’);

1 //比如操作成功跳转到Store模块下的Ump控制器中的lists方法
2 $this->success('新增成功',U('Strore/Ump/lists'));
1 //跳转时带着参数的话
2 $this->success('新增成功',U('Store/Ump/lists','type=1&id=1'));

当在模板中使用U方法时,好处在于:一旦你的环境变化或者参数设置改变,你不需要更改模板中的任何代码。

在模板中的调用格式需要采用 {:U(‘地址’, ‘参数’…)} 的方式

<!--在模板中使用U方法 -->
{:U('Store/Ump/lists','type=1&id=1')}
{:U('Article/index','category='.$vo['name'])}

 

有一点重要的那就是传变量值时,例如{$news.id}

 

  1. <volist name=“news” id=“news”>
  2. <ul>
  3. <li class=“news_li”>
  4. <a href=“{:U(‘News/news_detail’,’id=’.$news[‘id’])}”>
  1. <span class=“news_content”>{$news.content}</span></a>
  2. <span class=“news_time”>{$news.time}</span>
  3. </li>
  4. </ul>
  5. </volist>

重点在于传值时要把变量通过”.”+变量的索引来写

C-Lodop打印服务没启动怎么办 - 花谢悦神 - 博客园

mikel阅读(1312)

来源: C-Lodop打印服务没启动怎么办 – 花谢悦神 – 博客园

C-Lodop作为服务,解决了高版本火狐谷歌不支持np插件问题,支持跳出来浏览器的限制,支持所有浏览器,默认是只需安装一次,以后每次开机自启动,但是如果禁止了开机启动项等问题,会造成之后突然出现没启动状况,或每次重启机器没自启动。

没启动的可能原因:
1.CLodopPrint.exe进程被人为或意外故障杀死。
2.没设置开机自启动,禁用了c-lodop开机启动项。
3.当前操作用户权限不足,无系统管理员权限。(可用管理员重装试试,直接覆盖安装即可)
4.注意安全软件禁用c-lodop. 目前常用的360及金山已进行了安全认证,其他的杀毒软件请加入白名单,或直接上报提交对应的杀毒软件服务器。
5.旧版端口被占用。(新版c-lodop是双端口,占用可能很小)
查看一下当前安装的C-Lodop版本,与lodop官方发布的最新版本对比,更新到支持双端口的版本。

C-Lodop没启动,可能造成的现象:
1.提示“CLodop云打印服务(localhost本地)未安装启动!点击这里执行安装,安装后请刷新页面。”
2.预览界面由本地预览界面变成远程预览界面。
3.写入文件,打印设计,打印维护,提示“不能远程打印设计”“不能远程打印维护”“不能远程写文件”。
4.js调试提示http://localhost:8000/CLodopFuncs.js访问不到,无法链接,请求超时等。
(https版提示无法连接到的是https://localhost:8443/CLodopFuncs.js)
Failed to load resource: net:ERR_CONNETION_REFUSED http://localhost:8000/CLodopfuncs.js?Priotity=1
Failed to load resource: net:ERR_CONNETION_REFUSED http://localhost:18000/CLodopfuncs.js?Priotity=1
Uncaught TypeError:Cannot read property ‘PRINT_INIT’ of undefined
C-Lodop不启动,获取不到CLodopFuncs.js,获取不到LODOP对象,里面的函数也就是提示undefined没有定义。

解决方法:
1.没安装,需要安装C-Lodop,可在官网下载中心下载。
2.已安装,没启动,可参考上面的 没启动的可能原因,排查一下。
3.已安装,已启动,这种情况属于已经启动却访问不到CLodopfuncs.js,很可能是因为启动端口和引用端口不一致引起的,可参考本博客另一篇专门介绍C-Lodop端口的博文。如何设置C-Lodop打印控件的端口

如何启动:
默认是开机自启动的,首先要排除上面那些项。
1.双击桌面上的C-Lodop快捷方式,可快速重启C-Lodop(也可在开始菜单找到该项重启C-Lodop)。
2.最新版(2018-09更新的最新版),修改了该提示
原提示:“CLodop云打印服务(localhost本地)未安装启动!点击这里执行安装,安装后请刷新页面。”
新版修改后的提示:“Web打印服务CLodop未安装启动,点击这里下载执行安装
(若此前已安装过,可点这里直接再次启动),成功后请刷新本页面。”
新版的提示中有“可点这里直接再次启动”,点提示的那个位置就能快速重启C-Lodop,不必再去双击桌面的C-Lodop快捷方式。

补充说明:出现远程预览界面和提示”不能远程……”,如果是集中打印等方式是正常现象,打印设计维护写文件都是 在客户端本地打印方式上可使用。
集中打印,广域网AO端桥打印,广域网AO打印,作为服务器安装的C-Lodop可以设置定时重启,在空闲时定时重启一下。

其他相关博文:
提示“Web打印服务CLodop未安装启动”的各种原因和解决方法

CLodop.js提示:has been blocked by CORS policy: The request client is not a secure context and the

CORS跨域问题:

升级谷歌浏览器最新chrome94版本后,提示Access to XMLHttpRequest at ‘http://localhost:xxxx/api‘ from origin ‘http://xxx.xxx.com:xxxx’ has been blocked by CORS policy: The request client is not a secure context and the resource is in more-private address space `local`.

解决办法:

打开浏览器,进入chrome://flags/页面

搜索Block insecure private network requests

设置为Disabled,Relaunch就好了。