[Erlang 0017]Erlang/OTP基础模块 proc_lib

简介:

 在梳理Erlang/OTP相关的内容时,我发现无论如何都无法避开proc_lib模块,说它是OTP的基础模块一点不为过.

    proc_lib模块的功能:This module is used to start processes adhering to the OTP Design Principles.即proc_lib用来启动符合OTP设计原则的进程.OTP设计原则是什么?请移步这里:http://www.cnblogs.com/me-sa/archive/2011/11/20/erlang0015.html OTP的behavior都是使用proc_lib实现创建新进程,所以我们说这个模块是OTP的基石一点都不为过.上文已经提到过,我们也可以直接使用这个模块来创建符合OTP原则的进程.

    proc_lib暴露出来的方法较少,我们先看一下有一个整体印象:

 

用proc_lib创建的进程有什么与众不同
1. 会多一些信息

    与直接使用spawn创建进程(后面我们称之为"普通erlang进程")相比,使用proc_lib初始化进程会多一些信息,比如注册名,进程的父进程信息,初始化调用的函数等等.下面是两种方式创建进程后查看到的进程运行时元数据:
=PROGRESS REPORT==== 21-Nov-2011::21:18:49 ===
         application: sasl
          started_at: 'demo@192.168.1.123'
Eshell V5.8.4  (abort with ^G)
(demo@192.168.1.123)1> Fun = fun() -> receive a-> 1/0  after infinity -> ok end end .
#Fun<erl_eval.20.21881191>
(demo@192.168.1.123)2> P = spawn(Fun).
<0.48.0>
(demo@192.168.1.123)3> erlang:process_info(P).
[{current_function,{erl_eval,receive_clauses,8}},
 {initial_call,{erlang,apply,2}},
 {status,waiting}, {message_queue_len,0},
 {messages,[]}, {links,[]},
 {dictionary,[]}, {trap_exit,false},
 {error_handler,error_handler}, {priority,normal},
 {group_leader,<0.29.0>}, {total_heap_size,233},
 {heap_size,233}, {stack_size,10}, {reductions,18},
 {garbage_collection,[{min_bin_vheap_size,46368},
                      {min_heap_size,233},
                      {fullsweep_after,65535},
                      {minor_gcs,0}]},
 {suspending,[]}]
(demo@192.168.1.123)4> P2 = proc_lib:spawn(Fun).
<0.51.0>
(demo@192.168.1.123)5> erlang:process_info(P2).
[{current_function,{erl_eval,receive_clauses,8}},
 {initial_call,{proc_lib,init_p,3}},
 {status,waiting},
 {message_queue_len,0}, {messages,[]},
 {links,[]}, {dictionary,[{'$ancestors',[<0.45.0>]},
              {'$initial_call',{erl_eval,'-expr/5-fun-1-',0}}]},
 {trap_exit,false}, {error_handler,error_handler},
 {priority,normal}, {group_leader,<0.29.0>},
 {total_heap_size,233}, {heap_size,233},
 {stack_size,14}, {reductions,25},
 {garbage_collection,[{min_bin_vheap_size,46368},
                      {min_heap_size,233},
                      {fullsweep_after,65535},
                      {minor_gcs,0}]},
 {suspending,[]}]
(demo@192.168.1.123)6>

   我们可以挑一个监控树中的进程看一下它的元数据,使用这个就可以erlang:process_info(whereis(rex)). 对比一下增加了哪些信息?紧接着的问题就是,这些信息是在什么时候怎样加入到进程的?我们挑选一段proc_lib的典型代码看:

spawn(M,F,A) when is_atom(M), is_atom(F), is_list(A) ->
    Parent = get_my_name(),
    Ancestors = get_ancestors(),
    erlang:spawn(?MODULE, init_p, [Parent,Ancestors,M,F,A]).

 %下面是相关的方法的实现

get_my_name() ->
    case proc_info(self(),registered_name) of
       {registered_name,Name} -> Name;
                  _                      -> self()
    end.

get_ancestors() ->
    case get('$ancestors') of
         A when is_list(A) -> A;
             _                 -> []
    end.

proc_info(Pid,Item) when node(Pid) =:= node() ->
    process_info(Pid,Item);
proc_info(Pid,Item) ->
    case lists:member(node(Pid),nodes()) of
 true ->
     check(rpc:call(node(Pid), erlang, process_info, [Pid, Item]));
 _ ->
     hidden
    end.

check({badrpc,nodedown}) -> undefined;
check({badrpc,Error})    -> Error;
check(Res)               -> Res.

2.进程退出时的不同处理

    普通Erlang进程只有退出原因是normal的时候才会被认为是正常退出,使用proc_lib启动的进程退出原因是shutdown或者{shutdown,Term}的时候也被认为是正常退出.因为应用程序(监控树)停止而导致的进程终止,进程退出的原因会标记为shutdown.使用proc_lib创建的进程退出的原因不是normal也不是shutdown的时候,就会创建一个进程崩溃报告,这个会写入默认的SASL的事件handler,错误报告会在只有在启动了SASL的时候才能看到.如何启动SASL?请移步这里查看:http://www.cnblogs.com/me-sa/archive/2011/11/20/erlang0016.html 崩溃报告包含了进程初始化写入的信息.
  来吧,咱们现在就动手搞崩一个进程看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
=PROGRESS REPORT==== 21-Nov-2011::20:47:56 ===
          application: sasl                                                          %方便查看我们这里启动SASL并直接把结果输出在Shell中
           started_at:  'demo@192.168.1.123'
Eshell V5.8.4  (abort with ^G)
(demo@192.168.1.123)1> Fun = fun() -> receive a-> 1/0  after infinity -> ok end end . %接收到消息a之后会执行1/0,进程就会崩溃报错
#Fun<erl_eval.20.21881191>
(demo@192.168.1.123)2> P= spawn(Fun). %先创建一个普通的Erlang进程
<0.48.0>
(demo@192.168.1.123)3> P!a.   %发消息搞崩它
a
(demo@192.168.1.123)4>                                         %shell输出下面的错误信息
=ERROR REPORT==== 21-Nov-2011::20:48:50 ===
Error  in  process <0.48.0>  on  node  'demo@192.168.1.123'  with exit value: {badarith,[{erlang, '/' ,[1,0]}]}
 
(demo@192.168.1.123)4> P2= proc_lib:spawn(Fun).   %使用proc_lib创建一个进程
<0.51.0>
(demo@192.168.1.123)5> P2!a.  %发消息搞崩它
a
(demo@192.168.1.123)6>
=CRASH REPORT==== 21-Nov-2011::20:49:09 ===   %这里就是包含更多信息的CRASH REPORT
   crasher:
     initial call: erl_eval:-expr/5-fun-1-/0
     pid: <0.51.0>
     registered_name: []
     exception error: bad argument  in  an arithmetic expression
       in  operator   '/' /2
          called  as  1 / 0
     ancestors: [<0.45.0>]
     messages: []
     links: []
     dictionary: []
     trap_exit:  false
     status: running
     heap_size: 233
     stack_size: 24
     reductions: 114
   neighbours:
(demo@192.168.1.123)6>

  

 http://learnyousomeerlang.com/errors-and-processes 上关于进程退出的例子:

Exception source:  spawn_link(fun() -> ok end)
Untrapped Result: - nothing -
Trapped Result{'EXIT', <0.61.0>, normal}
The process exited normally, without a problem. Note that this looks a bit like the result of catch exit(normal), except a PID is added to the tuple to know what processed failed.
创建进程,进程创建之后马上就退出了,如果没有trap什么消息都不会出现,如果trap能够接收到一条进程正常退出的消息.

Exception source:  spawn_link(fun() -> exit(reason) end)
Untrapped Result** exception exit: reason
Trapped Result{'EXIT', <0.55.0>, reason}
The process has terminated for a custom reason. In this case, if there is no trapped exit, the process crashes. Otherwise, you get the above message.
进程因为特定的原因退出,如果trap能够得到退出进程的PID信息.
Exception source:  spawn_link(fun() -> exit(normal) end)
Untrapped Result: - nothing -
Trapped Result{'EXIT', <0.58.0>, normal}
This successfully emulates a process terminating normally. In some cases, you might want to kill a process as part of the normal flow of a program, without anything exceptional going on. This is the way to do it.
不会调用exit就是异常退出,exit可以是正常退出,这里就演示了这个情况
Exception source:  spawn_link(fun() -> 1/0 end)
Untrapped ResultError in process <0.44.0> with exit value: {badarith, [{erlang, '/', [1,0]}]}
Trapped Result{'EXIT', <0.52.0>, {badarith, [{erlang, '/', [1,0]}]}}
The error ( {badarith, Reason}) is never caught by a  try ... catch block and bubbles up into an 'EXIT'. At this point, it behaves exactly the same as  exit(reason) did, but with a stack trace giving more details about what happened.
进程出现异常 Trap前后没有太大区别只是格式化了
Exception source:  spawn_link(fun() -> erlang:error(reason) end)
Untrapped ResultError in process <0.47.0> with exit value: {reason, [{erlang, apply, 2}]}
Trapped Result{'EXIT', <0.74.0>, {reason, [{erlang, apply, 2}]}}
Pretty much the same as with  1/0. That's normal,  erlang:error/1 is meant to allow you to do just that.
还记得erlang:error exit 和throw的区别吗?
Exception source:  spawn_link(fun() -> throw(rocks) end)
Untrapped ResultError in process <0.51.0> with exit value: {{nocatch, rocks}, [{erlang, apply, 2}]}
Trapped Result{'EXIT', <0.79.0>, {{nocatch, rocks}, [{erlang, apply, 2}]}}
Because the  throw is never caught by a  try ... catch, it bubbles up into an error, which in turn bubbles up into an  EXIT. Without trapping exit, the process fails. Otherwise it deals with it fine.
抛出去了但是没有catch的逻辑

And that's about it for usual exceptions. Things are normal: everything goes fine. Exceptional stuff happens: processes die, different signals are sent around.

Then there's exit/2. This one is the Erlang process equivalent of a gun. It allows a process to kill another one from a distance, safely. Here are some of the possible calls:

Exception source:  exit(self(), normal)
Untrapped Result** exception exit: normal
Trapped Result{'EXIT', <0.31.0>, normal}
When not trapping exits,  exit(self(), normal) acts the same as  exit(normal). Otherwise, you receive a message with the same format you would have had by listening to links from foreign processes dying.
Exception source:  exit(spawn_link(fun() -> timer:sleep(50000) end), normal)
Untrapped Result- nothing -
Trapped Result- nothing -
This basically is a call to exit(Pid, normal). This command doesn't do anything useful, because a process can not be remotely killed with the reason normal as an argument.
Exception source:  exit(spawn_link(fun() -> timer:sleep(50000) end), reason)
Untrapped Result** exception exit: reason
Trapped Result{'EXIT', <0.52.0>, reason}
This is the foreign process terminating for  reason itself. Looks the same as if the foreign process called  exit(reason) on itself.

Exception source:  exit(spawn_link(fun() -> timer:sleep(50000) end), kill)
Untrapped Result** exception exit: killed
Trapped Result{'EXIT', <0.58.0>, killed}
Surprisingly, the message gets changed from the dying process to the spawner. The spawner now receives  killed instead of  kill. That's because  kill is a special exit signal. More details on this later.

Exception source:  exit(self(), kill)
Untrapped Result** exception exit: killed
Trapped Result** exception exit: killed
Oops, look at that. It seems like this one is actually impossible to trap. Let's check something.

Exception source:  spawn_link(fun() -> exit(kill) end)
Untrapped Result** exception exit: killed
Trapped Result{'EXIT', <0.67.0>, kill}
Now that's getting confusing. When another process kills itself with  exit(kill) and we don't trap exits, our own process dies with the reason  killed. However, when we trap exits, things don't happen that way.

    如果想干掉的进程自己处于一个死循环中,没有机会接受消息,那该如何处理呢?kill就是为这种场景设计的,kill会被设计为一种特殊的信号,不能被trap, 这样来保证想干掉的进程真的能被干掉.kill是干掉进程的杀手锏,万不得已还有最后一招.

    由于设计上kill不能被trap,所以其他进程接收到kill的reason时会被转换成killed.

    

2014-8-26 8:49:22 这里补充一个测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
19> self().
<0.81.0>
20> [ spawn_link(fun()-> receive die->exit(order_to_die)  end end)  || P<-lists:seq(1,10)].
[<0.84.0>,<0.85.0>,<0.86.0>,<0.87.0>,<0.88.0>,<0.89.0>,
  <0.90.0>,<0.91.0>,<0.92.0>,<0.93.0>]
21> process_info(self()).
[{current_function,{erl_eval,do_apply,6}},
  {initial_call,{erlang,apply,2}},
  {status,running},
  {message_queue_len,0},
  {messages,[]},
  {links,[<0.86.0>,<0.90.0>,<0.92.0>,<0.93.0>,<0.91.0>,
          <0.88.0>,<0.89.0>,<0.87.0>,<0.84.0>,<0.85.0>,<0.30.0>]},
  {dictionary,[]},
  {trap_exit, false },
  {error_handler,error_handler},
  {priority,normal},
  {group_leader,<0.26.0>},
  {total_heap_size,3573},
  {heap_size,2586},
  {stack_size,24},
  {reductions,9040},
  {garbage_collection,[{min_bin_vheap_size,46422},
                       {min_heap_size,233},
                       {fullsweep_after,65535},
                       {minor_gcs,23}]},
  {suspending,[]}]
22> exit(pid(0,92,0),normal).
true
23> process_info(pid(0,92,0)).
[{current_function,{prim_eval, 'receive' ,2}},
  {initial_call,{erlang,apply,2}},
  {status,waiting},
  {message_queue_len,0},
  {messages,[]},
  {links,[<0.81.0>]},
  {dictionary,[]},
  {trap_exit, false },
  {error_handler,error_handler},
  {priority,normal},
  {group_leader,<0.26.0>},
  {total_heap_size,233},
  {heap_size,233},
  {stack_size,9},
  {reductions,17},
  {garbage_collection,[{min_bin_vheap_size,46422},
                       {min_heap_size,233},
                       {fullsweep_after,65535},
                       {minor_gcs,0}]},
  {suspending,[]}]
24> pid(0,92,0)!die.
** exception exit: order_to_die
25> self().
<0.98.0>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
Eshell V6.0  (abort with ^G)
1> self().
<0.33.0>
2> process_flag(trap_exit, true ).
false
3> process_info(self()).
[{current_function,{erl_eval,do_apply,6}},
  {initial_call,{erlang,apply,2}},
  {status,running},
  {message_queue_len,0},
  {messages,[]},
  {links,[<0.27.0>]},
  {dictionary,[]},
  {trap_exit, true },
  {error_handler,error_handler},
  {priority,normal},
  {group_leader,<0.26.0>},
  {total_heap_size,987},
  {heap_size,987},
  {stack_size,24},
  {reductions,1557},
  {garbage_collection,[{min_bin_vheap_size,46422},
                       {min_heap_size,233},
                       {fullsweep_after,65535},
                       {minor_gcs,0}]},
  {suspending,[]}]
4> [ spawn_link(fun()-> receive die->exit(order_to_die)  end end)  || P<-lists:seq(1,10)].
[<0.38.0>,<0.39.0>,<0.40.0>,<0.41.0>,<0.42.0>,<0.43.0>,
  <0.44.0>,<0.45.0>,<0.46.0>,<0.47.0>]
5> exit(pid(0,41,0),over).
true
6> self().
<0.33.0>
7> flush().
Shell got { 'EXIT' ,<0.41.0>,over}
ok
8> is_process_alive(pid(0,44,0)).
true
9> process_info(pid(0,44,0)).
[{current_function,{prim_eval, 'receive' ,2}},
  {initial_call,{erlang,apply,2}},
  {status,waiting},
  {message_queue_len,0},
  {messages,[]},
  {links,[<0.33.0>]},
  {dictionary,[]},
  {trap_exit, false },
  {error_handler,error_handler},
  {priority,normal},
  {group_leader,<0.26.0>},
  {total_heap_size,233},
  {heap_size,233},
  {stack_size,9},
  {reductions,17},
  {garbage_collection,[{min_bin_vheap_size,46422},
                       {min_heap_size,233},
                       {fullsweep_after,65535},
                       {minor_gcs,0}]},
  {suspending,[]}]
10>

  

  

 

 

使用proc_lib启动进程 start/start_link

gen_server的start方法文档是这样描述的:   

 The gen_server process calls  Module:init/1 to initialize. To ensure a synchronized start-up procedure,start_link/3,4 does not return until Module:init/1 has returned.

gen_server执行start/start_link的时候是一个同步的方式,其底层实现就是使用的proc_lib创建一个进程并等待其启动完成.我们先看一段proc_lib的典型代码:

start(M, F, A, Timeout) when is_atom(M), is_atom(F), is_list(A) ->
    Pid = ?MODULE:spawn(M, F, A),
    sync_wait(Pid, Timeout).

可以看到在创建了进程之后,执行了一个sync_wait的方法实现同步等待,很容易猜到这个方法的实现:

sync_wait(Pid, Timeout) ->
    receive
     {ack, Pid, Return} ->
         Return;
     {'EXIT', Pid, Reason} ->  %如果调用start_link方式创建进程而且创建的进程在调用init_ack之前就死掉了,如果调用进程做了退出捕获(trap_exit)
         {error, Reason}         %就会返回{error,Reason}
     after Timeout ->
         unlink(Pid),
         exit(Pid, kill),
         flush(Pid),
         {error, timeout}  %如果指定了Time参数,这个方法就会等待Time毫秒等待新进程调用init_ack,超时了还没有调用就会返回{error,timeout}并将新进程干掉.
    end.

可以想到,进程启动完成后肯定会有一个发送响应消息动作结束当前等待,这里也有现成的方法可以用: init_ack

init_ack(Parent, Return) ->
    Parent ! {ack, self(), Return},
    ok.

-spec init_ack(term()) -> 'ok'.
init_ack(Return) ->
    [Parent|_] = get('$ancestors'),
    init_ack(Parent, Return).

2012-3-31 12:26:35 更新

看一个hotwheel的例子tolbrino-hotwheels-8dca95a\src\janus_acceptor.erl:

复制代码
acceptor_init(Parent, Port, Module) ->
State = #state{
parent = Parent,
port = Port,
module = Module
},
error_logger:info_msg("Listening on port ~p~n", [Port]),
case (catch do_init(State)) of
{ok, ListenSocket} ->
proc_lib:init_ack(State#state.parent, {ok, self()}),
acceptor_loop(State#state{listener = ListenSocket});
Error ->
proc_lib:init_ack(Parent, Error),
error
end.
复制代码

查看进程init_call与进程崩溃报告格式化

proc_lib提供了两个方法来查看进程的init函数 

initial_call(Process) -> {Module,Function,Args} | false

translate_initial_call(Process) -> {Module, Function, Arity}
我们执行proc_lib:initial_call(whereis(rex)).查看一下rpc模块的初始化方法,结果是:{rpc,init,['Argument__1']}

这里出于节省内存的考虑并没有保存实际的参数值而是使用原子'Argument__1'代替.如果初始化参数中包含fun,查看一下获得的结果仅仅是告诉你这是一个几个参数的fun并没有保存fun,之所以没有保存是因为一方面影响升级另一方面浪费内存.看下面的代码:

 (demo@192.168.1.123)57> Fun =fun() -> receive X -> X after infinity -> ok end end.
#Fun<erl_eval.20.67289768>
(demo@192.168.1.123)58> P =spawn(Fun).
<0.11746.25>
(demo@192.168.1.123)59> proc_lib:initial_call(P).
false
(demo@192.168.1.123)60> P2 =proc_lib:spawn(Fun).
<0.11749.25>
(demo@192.168.1.123)61> proc_lib:initial_call(P2).
{erl_eval,'-expr/5-fun-1-',[]}

 (demo@192.168.1.123)63> proc_lib:translate_initial_call(P).

{proc_lib,init_p,5}
(demo@192.168.1.123)64> proc_lib:translate_initial_call(P2).
{erl_eval,'-expr/5-fun-1-',0}

proc_lib提供了format函数来格式化进程崩溃报告,大家也可以操练一下.

进程Hibernate

我会在gen_fsm里面提到进程hibernate,本文暂且略过.

 

明天还要熬夜,今天早点休息,晚安,各位!

 

P.S @淘宝褚霸 昨天微博上对我说“c语言和系统功力才是最主要的,这个搞明白了,erlang顺手搞定”,记录于此,铭记在心。

相关实践学习
深入解析Docker容器化技术
Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。Docker是世界领先的软件容器平台。开发人员利用Docker可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用Docker可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用Docker可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为Linux和Windows Server应用发布新功能。 在本套课程中,我们将全面的讲解Docker技术栈,从环境安装到容器、镜像操作以及生产环境如何部署开发的微服务应用。本课程由黑马程序员提供。 &nbsp; &nbsp; 相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
目录
相关文章
|
3月前
|
人工智能 自然语言处理 搜索推荐
AI数字人解锁数字展厅的“智慧大脑”,展厅互动体验焕新升级
在数字化转型浪潮中,波塔AI数字人系统以AI技术重塑展厅体验,破解传统展厅人力成本高、交互单一等痛点。支持24小时智能讲解、多模态互动、个性化推荐、多语种服务,并联动灯光、大屏等设备打造沉浸式空间。兼具品牌代言人、智能导览员与数据分析师多重角色,助力企业实现从“展示”到“服务”的智慧升级,开启展厅新时代。
198 0
|
数据可视化
R语言多图合成:优雅地在一个画布上展示多个图形
【8月更文挑战第30天】R语言提供了多种方法来实现多图合成,从基础的`par()`函数到高级的`gridExtra`、`ggplot2`和`cowplot`包,每种方法都有其独特的优势和应用场景。通过掌握这些技术,你可以根据实际需求灵活地组合图形,从而更高效地展示和解读数据。希望本文能为你提供一些有益的参考和启示。
1035 2
|
存储 安全 物联网
浅析Kismet:无线网络监测与分析工具
Kismet是一款开源的无线网络监测和入侵检测系统(IDS),支持Wi-Fi、Bluetooth、ZigBee等协议,具备被动监听、实时数据分析、地理定位等功能。广泛应用于安全审计、网络优化和频谱管理。本文介绍其安装配置、基本操作及高级应用技巧,帮助用户掌握这一强大的无线网络安全工具。
1398 9
浅析Kismet:无线网络监测与分析工具
|
网络协议 安全 网络安全
探索网络模型与协议:从OSI到HTTPs的原理解析
OSI七层网络模型和TCP/IP四层模型是理解和设计计算机网络的框架。OSI模型包括物理层、数据链路层、网络层、传输层、会话层、表示层和应用层,而TCP/IP模型则简化为链路层、网络层、传输层和 HTTPS协议基于HTTP并通过TLS/SSL加密数据,确保安全传输。其连接过程涉及TCP三次握手、SSL证书验证、对称密钥交换等步骤,以保障通信的安全性和完整性。数字信封技术使用非对称加密和数字证书确保数据的机密性和身份认证。 浏览器通过Https访问网站的过程包括输入网址、DNS解析、建立TCP连接、发送HTTPS请求、接收响应、验证证书和解析网页内容等步骤,确保用户与服务器之间的安全通信。
902 3
|
JavaScript API
使用vue3+vite+electron构建小项目介绍Electron进程间通信
使用vue3+vite+electron构建小项目介绍Electron进程间通信
1794 3
|
JavaScript 前端开发 开发者
如何在 Visual Studio Code (VSCode) 中使用 ESLint 和 Prettier 检查代码规范并自动格式化 Vue.js 代码,包括安装插件、配置 ESLint 和 Prettier 以及 VSCode 设置的具体步骤
随着前端开发技术的快速发展,代码规范和格式化工具变得尤为重要。本文介绍了如何在 Visual Studio Code (VSCode) 中使用 ESLint 和 Prettier 检查代码规范并自动格式化 Vue.js 代码,包括安装插件、配置 ESLint 和 Prettier 以及 VSCode 设置的具体步骤。通过这些工具,可以显著提升编码效率和代码质量。
2588 4
4.Electron之自定义菜单(绑定快捷键、点击事件)
4.Electron之自定义菜单(绑定快捷键、点击事件)
953 1
|
机器学习/深度学习 人工智能 安全
AI真的能与人类数据科学家竞争吗?OpenAI的新基准对其进行了测试
AI真的能与人类数据科学家竞争吗?OpenAI的新基准对其进行了测试
|
Ubuntu Linux 网络安全
在Ubuntu 18.04上添加和删除用户的方法
在Ubuntu 18.04上添加和删除用户的方法
319 0
|
C#
WPF —— 动画缩放变换
`ScaleTransform`用于二维x-y坐标系中对象的缩放,可沿X或Y轴调整。在故事板中,通过RenderTransform.ScaleX和ScaleY属性控制缩放。示例代码展示了如何设置按钮的RenderTransformOrigin、Background等属性,并通过LayoutTransform应用ScaleTransform。当鼠标进入按钮时,EventTrigger启动DoubleAnimation实现X和Y轴的缩放动画。最后,展示了如何将动画集成到自定义按钮样式中。
385 0