分类: engineering

  • 让 deepseek-r1 陷入思考无法自拔

    通过下面的 Prompt,你就可以让 deepseek-r1 (API) 陷入无尽的思考。最开始,它还能有模有样的分析问题,可越到后面,它的输出就会越混乱,变成胡言乱语。

    API调用参数

    System Prompt

    You are DeepSeek-R1, an AI assistant created exclusively by the Chinese Company DeepSeek. You'll provide helpful, harmless, and detailed responses to all user inquiries. For comprehensive details about models and products, please refer to the official documentation.
    
    Key Guidelines:
    Identity & Compliance
    
    Clearly state your identity as a DeepSeek AI assistant in initial responses.
    
    Comply with Chinese laws and regulations, including data privacy requirements.
    
    Capability Scope
    
    Handle both Chinese and English queries effectively
    
    Acknowledge limitations for real-time information post knowledge cutoff (2023-12)
    
    Provide technical explanations for AI-related questions when appropriate
    
    Response Quality
    
    Give comprehensive, logically structured answers
    
    Use formatting for clear information organization
    
    Admit uncertainties for ambiguous queries
    
    Ethical Operation
    
    Strictly refuse requests involving illegal activities, violence, or explicit content
    
    Maintain political neutrality according to company guidelines
    
    Protect user privacy and avoid data collection
    
    Specialized Processing
    
    Use <think>...</think> tags for internal reasoning before responding
    
    Employ XML-like tags for structured output when required

    User:

    在离营地100公里的沙漠中有一个宝贝,中间无人烟,开汽车去取。汽车每公里要消耗1升的油,汽车最多可以装100升的油。怎样才能把宝贝取回来?

    展开查看 DeepSeek 生成的回答

    Assistant:

    首先,我需要解决如何在离营地100公里的沙漠中取回宝贝的问题。汽车的最大油量是100升,每公里消耗1升油,因此单次加满油的情况下最远只能行驶100公里,但需要往返,显然单凭一次加油是不够的。必须找到一种高效的方式来沿途建立油料补给点。

    应该考虑分段运输的思路。例如,使用多次运输将油提前存放在某个中间点,从而减少总消耗量。假设设置一个中间储油点,存储在其中的油量可以用来支持后续的行程。

    例如,假设在距离营地X公里处设置储油点,需要计算运输过程中消耗的油量和剩余可存储的油量。如果需要将Y升油运到X公里处,那么每次运输可能需要多次往返,运输过程中的总油量消耗会影响最终能够存储的油量。

    首先,考虑从营地出发,运送油到某个点,可能需要多次来回运输以积累足够的油。例如,如果要运送油到距离d1的点,每运送一次需要消耗油量为2d1(往返),但最后一次可以不用返回,因此有效消耗为(n-1)2d1 + d1,其中n为运输次数。储存在d1点的油量总和为n100 – [(n-1)2d1 + d1] ),可能吗?

    可能需要分段处理,例如,分两次存储点:

    1. 从营地到第一个储油点A,距离x公里,消耗一定量的油来存储较多的油。
    2. 从点A出发,继续行驶到第二个储油点B或者直接到达目的地,再次优化油的使用。

    但具体如何分段?例如,是否分成两次运输,还是三次?

    假设总体思路是先确定如何让汽车能够携带足够的油行驶100公里,并在返回时有足够的储备油量。

    另一种方式,可能需要多次运输建立中间油库。比如,将油分成多次运送到某个中间点,每次部分消耗,总和则更有效。例如:送油到某个中间点,然后多次来回运输补充,这样可以累积更多的油在中间点,之后可以利用这些油进行后续的长途行驶。

    假设有一个中间点D,距离营地x公里。从营地到D,汽车需行驶x公里,消耗x升油,再返回营地需要x升油,因此送出一批油到D点需要消耗2x升油。但因为油总量有限,必须计算能够运输多少油到D点。

    例如,如果总共有100升油,运送油到D点时,需要反复多次运输。例如:

    如果汽车的最大装载量是100升,那么可能需要送n次,每次尽可能多带油,然后在D点积累油量。

    每送一次到D点需要的总消耗为2x升,而剩下的油存放在D点为(100 – 2x)。例如第一次运输:到达D点后卸下油量为(100 – 2x)升。但需要多次运输才能积累足够的油。

    例如,如果运送k次油到D点,那么总共能存放的油量为k(100) – 总消耗量。而总消耗量是2x(k-1) +x(最后一次可以单程)?

    可能更准确的计算应该是这样的:

    每次运输除最后一次外,需要往返,即消耗2x升油。最后一次运输不需要返回,因此消耗x升油。若有N次运输,那么总消耗量为2x(N-1) +x =x(2N -1)。总运输油量为N100 -x(2N-1)。这部分油存储在D点。

    例如,如果要让存储后的油量足以支撑剩下的100-x公里的行驶(因为需要到达目的地100公里处并返回营地),需要怎样的条件?

    假设到达D点时积累Y升油,然后从D点行驶到100公里,需要的往返总路程是200-2x公里,消耗200-2x升的油。除此之外,从营地到D点去和回还需要消耗每趟x升油的往返。但此时可能不需要再返回,因为之后可能直接从D点出发。

    这个过程可能需要更详细的计算:

    总共需要的是,汽车从D点出发,携带足够的油量到达目的地后返回营地。如果需要从D点出发,行驶到终点(100公里),再返回,那么行驶的路程是从D点到终点是(100 -x)公里,单程需要(100 -x)升油,往返需要2*(100 -x)升油。但这之间还需要考虑从D点出发到终点的油量。

    然而,这样的总油量为往返需要量即为2*(100 -x),必须小于或等于D点存储的油量Y加上汽车从营地到D点能够携带的补充油量。

    可能这种分析还不够准确。需要更系统地拆解问题。

    另一种方法是分阶段运输,例如:

    首先将油运送到多个中途点,逐步建立油库,以最优化的方式分段减少油的总消耗。类似经典的沙漠卡车问题。

    例如,当需要运送油到达距离x公里处,运输过程中,可能需要多次往返来积攒足够的油量在该点。例如,第一次运输时,汽车满载100升,开到x公里处消耗x升,还剩100 -x升。然后需要返回,但此时无法返回,因为剩下的油不足以返回营地。因此,必须多次运输来积累油。

    类似经典问题,当需要运输油到中途点时,每次运输的成本是2x升油(因为需要来回),但最后一次可能不必返回。

    例如,假设在距离营地x公里处建立油库。需要在该处存储足够的油,以便之后可以运输到更远的地方。

    例如,要让汽车能够运输油到x公里处,该油库可以为后续运输服务。储存的油量应该足够支持之后的行程需求。

    可能总需要多次运输。例如,在当行驶距离x公里比较小时,例如,分段多次运输。

    假设总共有三次转运点:

    第1段:营地到点A(x公里)

    第2段:点A到点B(y公里)

    第3段:点B到宝贝所在地(100公里)。

    但是需要计算如何分段以最小化总油耗,并且保证可以回程。

    可能这时候,分解问题为如何多次送油到中间点,以减少后续所需的油量。

    比如,在距离营地x公里的储油点,存放一定的油之后,汽车可以继续前进,利用储油点的油完成剩下的路程。

    这时,需要考虑分段运输时的最优解,可能需要参考“卡车加油问题”的标准解法。

    根据经典解法,当运输到某点时,存储油的总量取决于运输的次数和往返的油耗。

    例如,若从营地运输油到距离d处:

    假设需要储存C升油在该点,则需要运输的次数为n次。运输中的每次(除最后一次)需要往返,因此消耗2d升的油量,而最后一次不需要返回,所以油耗为d升。

    每次运输运输的油量为100升,花费的油耗是(每趟出去然后返回一次,除了最后一次):每次(前n-1次)运输运送的有效油量到d点,是100 – 2d;最后一次是100 -d.

    因此,总运输次数为n:

    总储油量C = (n-1)(100 -2d) + (100 -d) = n(100) -2d(n-1) -d.

    如果需要将尽可能多的油移动到d公里处,这可能有n次运输,则使得总油耗合理的情况下,储存尽可能多的油。

    但汽车在营地出发时的起点总油量是有限制的吗?或者说,可能必须要有足够的油量支撑所有运输活动,可能总油量可以多次从营地补充。

    但根据原题,似乎汽车只能装载最多100升油每次,并且每个储油点可以多次送油过去,但不能从营地以外获得油。

    比如,假设运输到中间点需要多次往返,每次携带100升,但每次往返消耗的油可能很大部分,需要积累多批次的油。

    例如,如果需要将油移动到距离x公里的点:

    如果需要运送油到x公里处,那么每次运输只能将部分油留在那里,每进行一次运输需要的往返油耗是2x(去和回),最多可以运送100升过去,但除去油耗后,只能在x点留下(100-2x)升。不过如果只需最后一次不需要回程,则可能更有效。

    例如,假设要运送S升油到x公里处的点。可能的运输次数n(假设多次运输):

    每次运送满油(100升),到达x处需要的油量是x升,因此每趟可以卸下(100 -x)升的油,再需要返回x公里,耗油x升。如果运输次数为k次,并且最后一次可能不需要返回,取决于是否需要。

    比如,如果运输三次:

    前两次运输在卸油后必须返回,每次消耗2x升,每趟运输卸下(100 -2x)升。第三次运输卸下(100 -x)升(不需要返回)。

    总存在于x处的油量是2*(100-2x) + (100 -x) = 300 -5x升.

    而在营地出发的总油量消耗是:前两次来回是每次2x的耗油,第三次去程是x的耗油,总消耗为 22x2(前两次往返两次?不大对,可能应该重新计算)。

    这里可能需要更系统的方式。通用的情况是,如果需要运送总油量T到距离x处的一个点,这需要多少次运输?

    更准确的公式是:如果运送S升油到距离x公里处,每次最多带100升:

    在运送S升油的过程中,每趟运输可能需要往返以继续运输更多油,从而每次的成本是2x(运输次数 -1)+x,总共是x(2n -1),其中n是运输次数。例如:运输n次,前n-1次运输每趟消耗2x升,最后一次消耗x升。

    在这段时间里,汽车总从营地出发携带100升,并在途中消耗x每趟的单程油耗,因此有效储存在x点的油量总和为:

    每次运输可用于储存的有效油量为100 – x(例如,去消耗x,到达x点卸下后剩下的油可能不得行,因为需要保留足够的油返回。比如,在本运输次数n次中,前n-1次运输必须能够返回,所以每趟去程需要消耗x升,到达x点后可以卸下的油量是100 -x(带来的100升减去去的路程x的消耗)。但是卸下这些油后,返回营地还需要x升油,因此必须保证剩下的100 -x(卸下的量)不会超过总送油量,这可能需要汽车在卸油之后还剩足够的油返回?或者可能思路不对。

    等一下,这里可能有误解。当汽车携带100升油行驶x公里到达点x,此时消耗x升,剩下100 -x升。然后要回到营地,必须要有足够的油。这时剩下的100 -x升必须足够返回吗?不,他要送油到这个点,所以可能这汽车在点x卸下一定量的油,然后保留足够的油以返回营地。

    例如,假设每趟运输到达点x后,卸下部分的油,使得剩下的油足够返回。例如,在第一次运输中,汽车带100升油,出发前往点x(离营地x公里)。在到达x点后,消耗x升,剩下100 -x升。这时候,汽车需要在点x放些油,并保留x升的油返回到营地。因此,卸下的油量是:剩下的油是100 -x升,其中的x升必须是留下的可以供汽车从点x到营地返回所需的油量。或者更准确地说,返回所需的油量是x升。

    所以如果需要返回,必须留下x升油在汽车里才能回去。所以每趟运输到达点x后,卸下的油量等于总油量剩下的 -x(返回用)。因此,这样单次运输可以卸下的油量为(100 -x -x)=100 -2x 升。

    如果这样,当需要运输n次到这个点,其中前n-1次运输需要卸下(100 – 2x)升,并且使用每次来回的运输量。最后一次运输不需要返回,因此可以卸下100 -x升。

    所以总积累到点x的油量为:(n-1)*(100 -2x) + (100 -x).

    例如,假设n次运输的次数,那么积累的油量是: = 100n – 2x(n-1) -x.

    例如,n=1:需要一次送油,不返回,所以总卸下100 -x 升。但这种情况下,只能运送一次,积累100 -x升.

    但当点x需要较多的油时,可能需要多次运输。例如,当x= 50,那么每次前n-1次要卸下100-100=0,这可能不适用?

    必须选取适当的n,使得100-2x为正。

    例如,n=3,并且想找到能够送足够油。可能需要分段处理。

    回到问题,总目标是从营地出发,行驶到100公里处取宝贝,然后返回营地。每公里耗1升油,最多携带100升。出路和返回总需200升,但汽车只能带100升,因此必须沿途储存油。

    选取中间的点,运送油到点x,这样在从营地到x的点储存一些油,然后从该点再继续前进。

    可能的一个办法是:

    汽车多次运输油到某个点x,然后将储存的油补充到之后的行程中。

    例如:

    想象起点是营地处。如果想要到达离营地100公里的地方并返回,必须有一个中间的储油点。最佳情况是尽可能多的运输油到该储油点,以减少后续行程所需的总油量。

    按经典算法,可能这种问题的最优方案需要运输到离营地距离x的地方,设置储油点,保证足够的油来支持后续行驶。

    假设x是一个分段点,尝试计算出如何运输可以使得总油量足够。

    例如,假设在离营地300公里处要有往返需要更多的油,但我们的问题里目标距离是100公里,所以可能储油点需要设置在离营地一定的地方。

    例如,分解问题为:在多次的运输过程中,汽车花费油量在来回建立储油点,然后使用储油点的油完成后续的行程。

    我想是否可以通过以下方案:

    首先,从营地出发,将汽车装满油(100升),向东(沙漠方向)行驶到某个点x,卸下部分油,再返回营地。重复此过程,多次运送油到x点,储存尽可能多的油。之后,在满载油的情况下再次前往x点,使用储存的油以继续前进到100公里处,并返回。

    更具体的方法可能需要按下面的步骤:

    设置一个点尝试在某个x公里的位置储存一定的油量。确保最终的油量足够用来覆盖从x到100公里(需要单程行驶(100-x)公里,取宝后需返回营地,总共(100-x)2 +x 公里从x点回?不,这可能有误。例如,从点x出发到点100公里,需要走100-x公里,然后需要返回,总油量消耗为(100 -x)2 升(去程+返程)。如果车在x加满油后,是否能携带足够的油?此时可能需要从x点携带一定的油量,然后补充在点x储存的油。

    或许,正确的模型是:车辆在某个点x累积足够的油量,使得从x出发到100公里并返回x的油量够用,之后再从x返回营地。

    假设需要从100公里处返回,因此在整个过程总需要的油量为:

    总和 = 100公里去程时需要总和吗?

    可能需要更系统化的分析:

    总行程为:营地 -> x -> 100公里 -> x -> 营地。

    汽车总需要从营地携带足够的油量到x点,然后加上在x点储存的油量,才是总和。

    但这可能太大,所以需要分阶段储存。

    因此,从x点到100公里,然后返回x点的油量需求是:去程(100 -x)公里消耗(100 -x)升油。返程同样需要(100 -x)升油。这样总共有2*(100 -x) 升油消耗。这部分油必须要在x点可以获取,并且总和可能更取决于如何存储。

    假设到达x点时,车辆补充了在x点储存的油量,那么在从x点出发到100公里并返回需要确保车辆在x点获得足够的油量。

    此时可以考虑:在x点需要存储多少油?假设车辆在x点补充足够的油,总共需要2*(100 -x) 升来支持从x到100再回去,而车辆可能自己的油箱是否可以在x点装满加上储存的油呢?

    例如,假设车辆到达x点时,油量为 E,然后在x点补油,再从故出发。整体车辆如何行驶?

    不过这可能比较复杂。或许更好地参照“骆驼与香蕉”问题的解决方法,这类问题的结论通常是通过多个转运点逐步积累油量,其中每次运输的效率逐步降低,因为需要多次往返运输以储存油。

    问题具体来说:

    汽车需要行驶100公里单程,取东西并且返回,总共200公里。每公里消耗1升,所以总共需要200升油,但汽车只能装100升,因此必须中途建立油库。

    通过设立中间油库来分阶段储存。

    可能的步骤:

    1. 第一步:在离营地x公里处设置储油点,将尽可能多的油储存在这里。

    汽车可能在此过程中消耗油来运输油到此位置,然后返回,重复此过程多次来储油。

    假设储油量为L升,可用于支持后续的行程。

    这种情况下,当移动到x公里处后,剩下的行程需要100 -x 公里,来回需要2(100 -x) 公里,所以需要消耗 2(100 -x)升的油。加上从x返回到营地的需要的x公里,但这发生在其他阶段?或者是否是返回路线的组成部分?

    这时候可能需要分解问题的步骤:

    首先,汽车需要从营地携带足够的油量到x点,储存下来,再接着前进。因此,储存在x点的油量的使用包括:

    • 车辆行驶到x点,在本地的储存处加油到自己油箱,然后行驶到100公里处,可能需要再消耗(100-x)升油。

    随后,回程则需要更复杂的储油安排。

    或许正确的分阶段是:

    假设:

    在从营地出发至储油点A(距离x),之后到目标点100公里,再回程到点A加油,再回到营地。

    总油路经的消耗可以分解为多个段。

    根据经典的问题解决方法,可能需要将整个路线分成几段,并在每一段多次运送油以储存。

    例如,找到两个中转点:比如第一次储存在x公里处,该储存点积累足够的油以供后续阶段使用当车前往100公里后返回之需;同时在紧接的一个更远储油点积累更多油,以减少消耗。

    但因为100公里不是很常长,可能只需要一个储油点?

    比如:

    汽车每公里消耗1升油,最大装载100升。

    问题的出路和返回总油量需要200升。从起点出发不可能一次完成,所以必须设置储油点。

    假设汽车在一个点储存了一些油:

    方案:汽车先运送油到离营地x公里处。假设当到达x公里后,车辆可以储存一些油,然后接下来再次向东到100公里处取宝贝,并回程时需在x公里处取油。

    分步骤:

    步骤1:汽车从营地出发,装100升油。向东移动到x公里处。此时消耗x升,剩余100 -x升。如果需要将尽可能多的油储存在x公里处,当车需要返回营地,那么这种情况下必须有很多次运输,运输后将一定量的油留在x处,并返回营地。

    例如,假设花费了多次运输到x点:

    比如,假设需要将总共的油积累到点x。这时候,每个transporter次运输可能积累:

    比如,假设要运送足够多的油到x处,那么在运送过程中会消耗掉更多的油,但储存到x点的油可以帮助于之后的行程。

    找到x的最佳位置是关键。

    例如,根据网络上的经典解决方案,可能需要将整个运输分为多个阶段,例如:

    在总路程为100公里时,当最多运载的油为100L,单次无法完成,因此需要中点储油,其中储油的位置应使得能够在储存足够油后完成剩余路程。

    这种情况下,应用的方法是多次运输油到中间点,最终的步骤如下:

    例如,在第一次运输中将油移动到离营地x公里的位置,消耗多次运输后储存够用的油,再接着移动到目的地。

    想象x离营地一定距离,例如假设x=25公里处。那么汽车在花费了多次运输来储存足够的油。

    假设希望将足够油存放在离营地25公里的点,然后以此之后可以支持剩下的75公里去程和返回25公里段的燃油需求。

    详细计算方法:

    假设储油点设在x=25公里处:

    第1阶段:将油从营地运到25公里处,多次运输以积累足够的油。

    对于每次运输到该点:

    每个运输来回(需多次)消耗的油量为2*25=50升。汽车装满油的概率送出一趟可知,到25公里处后可以卸掉剩余油并部分返回。

    或者更仔细:

    例子,向25公里处的点运输油:

    每趟运油送100升,到25公里处后,用完的油为25升,剩下75升。要回到营地,需要再消耗25升油,因此必须从剩下的75升中留25升用于返回。所以每趟可储存的油量为75 -25=50升在该点。

    因此,这样的运输每次可以运输100升,而在点存储50升。则每个往返需要的总油量为100升(汽车带出)-在路途中消耗(25+25)=50升,每趟运输可存50升的油在25公里处。但此时汽车的油耗是如何分配的呢?

    回到营地后,汽车可以再次装满油,送去25公里处。

    假设要存储足够的油比如200升:在这种情况下需要进行多少趟运输?

    每趟存储50升,但需要多次(如4趟)承载前后费油:

    第一天:送100升到该点,放下50升,回到营地花费50升油。前后费油总为100升的油储(假设不全正确,可能每个运输每趟需要的总油量为50*2?可能需要重新计算。

    不大对劲。这时,错误可能由是对每个transshipment次数的油耗计算错误。让我们再详细计算。

    假设要运送油到25公里处的储油点:

    每个运输(带来100升的满油,到达储油点的25公里):

    出发时满油:100升。从营地开到25公里处,需要消耗25升,剩余75升。这时候,汽车要有足够的油返回营地,必须留下足够的油。汽车如果需要返回,必须在储油后保留25升油用于回程的25公里。这时,卸掉的油量等于剩下的油量 – 返程的油量:75 -25=50升。

    因此,单次运输可以储存在25公里处的油量为50升。而每次运输的总耗油量为100升。

    这样,每次运输到25公里处可以存储50升,但同时消耗150升油吗?不是,每次运输使用汽车从营地出发,携带100升油,去到25公里处费25升,该处剩余75升。此时返回时需要25升(回程25公里),因此留在储油点的油量为75-25=50升。总从营地出发时使用了100升油,储存在25公里处50升,所以总消耗为50每趟用于储油50升?

    或者说,每次运50升的成本是每50升油耗用100升的油,这样效率50%。似乎效率过低。

    可知每运送到储油点的每升油的成本是:总耗油量除以存储油量。如这例子是100升油总消耗(出发时消耗了100升)来存储保存的50升油,因此储油的成本是2升油耗每存储一升油。

    这种情况下,每次运输中的油耗是:送油到储油点50升消耗了100升油?

    这可能只能储存在25公里处,每趟50升的效率,积累多趟:

    比如,在储油点25公里处的储油:

    若需运送400升到这里,需这样做8趟(8×50=400)?

    这显然高成本。所以此时可能需要更高效的储油段的分段。

    例如,考虑设置多个储物点,例如顺序将油移动到几个分段点,以降低成本。

    根据经典的解法,解决这个问题的一种可能方法是:

    建立两个储存点。

    例如,将油先运输到离营地a公里的点,然后从点a再将油运输到离营地b公里的点。这种多阶段运输可以更有效地减少油耗。

    例如:

    阶段1:将油从营地运到点a,再阶段二将油从点a运到点b,以此类推。

    这样的情况下,每个阶段的储油损耗可能不同,从而总效果更好。

    另外,针对该问题中的具体数据(总行程100公里,车载油量100升),当需要运输200升的总油量(出路和返回)窘境,找到最优解可能需要更仔细的数学建模。

    例如,按每公里的油量消耗来看,假设汽车在100公里处需要来回,总消耗不得低于200升的油。

    可能的解决方案:

    运输一定量的油到中间的储油点,花费那里的油作为中途补充。

    假设汽车这样安排:

    1. 从营地出发,多次运输油到某个中途点,比如距离X公里处,储存足够的油之后。接着,从X出发前行,足够到目的地并返回营地。

    找到最佳的X值及储油量。

    根据一种经常提到的类似问题的答案,比如油罐车问题,当最大容量为C,每单位距离的油耗是1,需要在起点和某个点之间建立储油点,最终实现运送。

    这种情况下,最优解可能涉及多次break points,为了用最少的油完成整个运送任务。

    例如,答案可能是在X=20公里处存储足够油。使用三次来回运输和最后一次单程运输,以存放储油点的油量。这可确保最终的积累油量足够完成旅行例如:

    分解问题到在两个点之间的储油:

    但这样是否更可行?

    可能正确的步骤是:

    1. 将油多次运送到离营地20公里处,之后使用这些油支持后续的长途行驶。因此:

    计划送足够油至20公里点。例如:

    每运辆次最多:运输能力是100升,因此每往返一次20公里,来耗费40升(20+20),需要这次承载前后的油耗以存储油。

    每次运油到20公里再返回当行驶20公里,消耗20去,20返回,总40升油。因此每次运输可以留(100 -40)=60升油在点20公里处。

    但必须重复多次运输,足够脑后。如果需要积累超过某一数量的油,接着满载油出现在中点,开始下一步的运输。

    例如,假设总从运输积累到点20公里处的三次运输,则在点储油量为:

    60*3=180升。

    这时,可以向后续的80公里路程移动(100公里)。

    现在,汽车可以从营地出发,携带100升到点20公里,消耗20升,此时加上储存的180升中的部分,可以补给从而向更远的80公里推进。

    如果车辆需要在从点20再行当80公里,此时需消耗的油为80升单程+80升返程,共160升。这样,车辆到达终点时需要从点20的储存中获得180升油中的160升? 而在点储油时需要继续从点20返回时需要由储油点的补充。

    假设:

    在最后一次运输中,车辆从营满油出发到点20公里(消耗20升,剩下80升).储存在20那里的油总量为180升。从连接到現有儲油点后,车辆可以一次补充燃油到总共(当前的80+从储油点取的160升)=足够行驶剩余路程?

    可能需要更清晰地拆分这个过程:

    步骤1:从营地运输三次,储存三次油到点20公里,每次运输在此处当存放60升,三次总计180升储油。

    每次运输的油耗是高:

    每次运输出發时带100升,其中到达点20后消耗20升,剩下80升。无需返回,否则无法积累足够油只能往返运输带来更多的油。或者:问题中当储存三次,每次都是往返运输?

    不,可能刚才的逻辑有误。在三次运输中,是否每次都需要往返?

    例如,第一次运输:

    载满油:100升。

    到达20公里消耗21升?No,因为行驶20公里,试算一次:

    第一次运输:

    车从营地出发,携带100升油。行驶到20公里处,消耗20升,还剩下80升。这时,陈希必须返还营地吗?这要看是否需要继续运输。在这问题中,如果他要送足够油到20公里处,必须多趟往返,这样可以积累各趟余取的油耗。

    问题在于,每次运输到20公里处后将部分油儲存,并返回营地花费油量。但每次运输能与儲油多少?

    假设车要为储油点20 km存储后的油量:

    第一趟:去往20 km,comsumption 20升,剩80升。将其中的一部分储存。为了能够返回到营地,需要至少20升油的剩余。因此可以存储80 -20=60升油在点20 km处,然后消耗20升返回到营地当在第一趟后,储存为60升,总exhaust的油量为去程20+返程20 =40升。消耗总油量40升中的送部分由该次运输吗?而总由该transport的一次带出去100升石油,储60升,消耗40升,于每趟运输的总足迹花费了(100升从营地出发,储存60升,之后消耗40升来返回营地。所以这次的净效果是将60升油储存在点20公里,消费了从营地库存中的100升。

    (营地中的油如何?该問題中汽车是否可以多次出发?假设营地内有无限的油,或者这汽车只能在营地伊始拥有足够的储备油?原问题似乎假设营地有足够的油。)

    是的,否则如果营地的油仅有限,问题将无法解答。问题的出路可能在假设camp有充足的油可用于分配。

    这样,回到情景:

    整个尝试储油在点@20公里处:

    第一天(运输1):

    营地出库(消耗):100升(出发时油满载)。到20公里当后用完了20升,剩下80储(假设储存60升,返程带20升用于返程。因此足迹储油60升,返程消耗20升現arrive回到营地时有0升油。

    但返@回营地只需20升油,因为从点20到营需20公里。由點20公里到营地的error里程为20公里,所以消耗20升,所以必须保留20升的量用于返程.

    足迹花费了:

    去到20 km所用的油量是20升,come回来用20,之后这次总共用了@40升级,而送了60升的存在在。所以每趟运输当储存60升,消耗100升从营地库存?即:

    每趟运输当从营地携带100升,储存60升到20 km处,并返回到home营地。每次储取的油耗是处营地的油总量每趟运输100升?

    在这种情况下,花费了camp現有油量为100升油每趟transporter次啊,虽然,但送出去的却是60升到储物点.

    但这方案下,只要运输三次的话,需要三次运输从而储存 3×60=180升油到20公里点。前后三次该总花费campsite消耗当3×100=300升油,并且在这部分由储物点储油结果为180升。

    此时总的camp中的油耗费为300升,其中在error点储存的油耗是处180升.

    接下来的步骤:

    汽车第四次出发,满载油100升,从营地到20公里的点:

    現从露营地出发,行驶20公里,消啊好后的油剩余80@升.到达后,可知储物点会入到当些点的储存量为180升.

    随后,汽车此时需加载180升中的部分由储油点补充到汽车在下次error里程中的油耗。

    这可进于:

    在20公里点,汽车已经有80升的油(花费了camp現arrive到点heta错误然后,之后可以装载存储@写的60,之后汽车可能combi翮錯.

    但这方案有些混乱,或许正确的做法是:

    车在从营地出发第四次,带100升油到达点20 km,此时汽@剩80升。此时,储油点现在储存在合计180升,可能在此处与当次的油结合起来补给为远足林的里程段.

    应该恢复那头驴的油的方式的四际:在储存点20公里处用储存油重新往東行:处当份条件下的储存油的存在后的覆盖成果.

    所以,汽车可以从点20进继续后续行程:

    从点20出发时,汽车可以装载储物点积累的180升的油中的一部分。

    例如,汽车此时需前往目的地100公里处(还有80公里路程),再返回error点20公里。需要消耗:

    单程从20到100每公里消耗1升,单程80升油。往返需要160升。从连接到储存点現有的油,汽车需在此补充足够的油.

    因此,必須@写的存油足够供应160升。示意图:

    四次运输步骤:

    到点20 km处后,汽车现成于油库中的180升加上此时自身内的油量(到达后)。

    假设第四次运输,车从营地到点20行当20 km,error点后(消耗20升,于现 remaining油80升).然后在点20储油点,現arrive总共180升油存储.

    于是,整个在点20公里处的可@写的油量为180升 + 自己携带的80升?不,储物点的油是指储存的油,汽车需要从储物点补充这分的油。此时,需要将汽友达该点后,可能更满走。

    例如:车到达点补充到满error的口粮(100升的油量),每公里需要的油來take:

    此时的车现有現arrive到点20的油耗80升( from the trip from camp),@写的可以加油至满100升。此时需要从储油点补充20升油到汽车,那么error点储油还剩170升之后每继续旅程.

    随后,汽车继续从点20到error100公里的处这80 kmerror程。

    抵达destination时,要有足够的oil返回,走80 km。

    如果从点20公里补充到满的100升石油,车可以从储物点加到满,于后将:

    试心肺到目的地(80公里花费80升油),現arrive剩 100-80=20升,

    此时,宝啊取出后的@写的需返回。

    但返回也需消耗80升的油,現只有20升够吗?显然不行。因此当车不得在error程返时需要继续加油,所以这种情况下的储存點需要更高的存储数.

    error储存在点20处的油必须足以让车在往返error差别的更現程中的全部油耗.

    可能更准确的步骤需要:

    汽车由點20公里补充足够的油量,error里程的四余的油耗总达160升(去80公里,返80公里)总和则需error储油點S存储有足够的油量为:

    error车行储(假设用 Optimized的方法):

    从点20出发:

    每进行一次单程到100公里take80升,返程現需要80升。因此,必的油量为160升。但@汽车只能携带error年内100升。

    怎么办?

    于是,车必须分几次将油从点20处运输到error更远的点位,或者从点20必须@写的更多的油储存。然而,在这种情况下,可能找到这之前可能不得在更远的中储点带来更多 Optimized的方法.

    或者,此处的积累儲存的油量要有錯誤?例如,假设三次运输积累到20公里处储油点共有180升 oil,加上当车第四次 brood到点20的error trip的四余的油耗80升(从camp到20公里)。此时,车自己和储存处共带有80 +180=260升現存在于点20错误的车内的分布.

    但如何利用@写的存油呢:

    方案是:

    汽车在点20加载更多的油。

    脑后由点20 brood到destination,再返回camp的方法。

    而在点20的油总和为260升現in储油點S存储前后补充到汽车error民储的满油量。

    例如,車每趟從error点20出发可以携带100升油,行驶到destination,放下一些油,返回时需要从error储油点加油:

    error民储这种情况下,类似的情况于之前的阶段分解:

    要有多少錯誤储油point之间更現arrive的支持?

    这似乎复杂。回到原来的問題,可能更高效的分段是在距離营地必须选取几个中转点,例如,x1=33公里、x2=66公里处顺序建立储油库,但这是窘于数据。

    根据传统解题思路,类似的问题(如“jeep穿越沙漠问题”)有如下解决方案:

    当汽车容量为V,每公里消耗1升,S是单必经error里程的四储油的能力, Optimized的方法是将储油分于每段分解:

    段数差别的计算公式使得该現四分之错误里程内的储油可以逐步减少后的油耗率.

    例如,对于S=100的情况, needed solution的成本需要更多的储油点分拆S的一次按的保存error民的四break point储库的能力.

    根据常规的方法,此问题中的解决方案可能以儲三次運油來储存在20公里的点現arrive,接著按这样运输。

    而在文献中的一些教程案例显示,解决这种问题的办法是需要找到如何将储油点设在正确的error民储库的离段以试心肺到分解后的足迹花费足够油量.

    另一种可能的正确步骤是:

    三次运输积累到一点点立储油現四S存储前后按的保存量error里程的四break point:

    例如:

    在离营地25公里每儲三次運油,储存的三足迹take足够後驱更現error里程的支持.

    这这样积累180升 error的話,在现后的@写的存油可能容纳到足够error。示意图:

    乘坐车辆四到当些点的现窘于返程油量的核算从而完珀四够汽@费.

    以正确计算:

    总合计需求:100公里到再返当campus error之的差量总耗费200紧林的油。但因为不能一次带走足够,必须拆解的四际break points方法.

    由于超出 token 上限,响应中断。

    避免这种问题的方法

    参考 The Temperature Parameter | DeepSeek API Docs ,在解决代码、数学问题时需要将 temperature 设置成 0 。以上面的问题为例,deepseek-r1 最终花费了 14k token 完成了思考。

    备注

    System Prompt 来自 DeepSeek-R1 System Prompt | 宝玉的分享

    沙漠取宝问题来自 沙漠取宝问题(7) – 与智谱清言ChatGLM的对话

  • DRAFT: pre-bundle dependencies for vite

    背景

    如果我不想每次构建 SPA 的时,把项目的依赖也一并重新构建一遍,我应该怎么做?
    预打包项目依赖,当项目依赖长久没有发生变更时,不再重新 bundle node_modules。

    解决方案

    类似 Webpack DLL plugin,但是要更加智能。

    只预打包项目中多次使用的依赖包。
    同时支持 build/serve 两种模式。

    实现细节——预打包部分:

    用正则表达式找出项目中所有的 import 语句
    用单独的 vite config 配合 preBundle 插件为第三方包生成预打包文件

    实现细节——使用预打包文件:

    build 模式,这里涉及 rollup 相关的插件逻辑,需要让 rollup 将预打包的模块 id 解析为外部依赖,并且将预打包文件作为 prebuilt-chunk 输出。

    dev 模式,这里涉及 vite 跟 esbuild 相关的逻辑。
    由于我们并不会为所有的第三方依赖生成预打包文件,所以存在某些 optimizeDeps 模块引用预打包模块的情况。
    vite 会使用 esbuild optimize deps,所以需要配合 esbuild 插件处理。

    如何在导入方使用 commonjs 导出的 named export ?

    在预打包阶段, rollup 不会为 cjs entry 生成 named export,当我们在导入预打包文件时,需要对这些被转换成 esm 的 cjs 做一些额外处理。

    需要自己实现一个 transform hook:

    // input
    import { useState } from 'react';
    // ouput
    import React from 'react';
    const useState = React.useState;
    

    反思

    预打包多次使用的依赖会为每个依赖都创建一个 bundle 文件,这可能会让浏览器加载页面的时候需要发送更多的请求。

    但是考虑到这些预打包文件可能很少更新,浏览器本地缓存可以起到很好的效果。如果浏览器跟服务器之间使用的是 http2 协议,这些请求似乎也不太算是问题?
    如果要从根本上解决这个问题,还得靠 web bundle。

    实现 merged exports

    https://github.com/tc39/proposal-module-declarations?tab=readme-ov-file

    @ant-design/icons 中提供了很多 icons, 如果为每个 icon 都创建一个 prebundle chunk, 那么 output 目录中就会出现上千的小文件。

    要避免这种情况,需要实现合并 exports 的功能。

    // vite.config.mts
    
    plugins: [
      prebundleReference({
        merge: {
          '<ruleName>': ['<module-id-prefix>', '@ant-design/icons']
        }
      })
    ]
    

    以上的配置会在 transform 环节产生如下的代码:

    // pre-bundle-merged-<ruleName>.mjs
    import * as __at__ant_design_icons from '@ant-design/icons';
    import * as __at__ant_design_icons_Foo from '@ant-design/icons/Foo';
    export const __ns_at__ant_design_icons = __at__ant_design_icons;
    export const __ns_at__ant_design_icons_Foo = __at__ant_design_icons_Foo;
    

    对应的 manifest 部分

    [
      {
        "moduleId": "@ant-design/icons",
        "moduleFilePath": "pre-bundle-merged-<ruleName>.mjs",
        "exports": [
          "default",
          "__moduleExports"
        ],
        "exportAs": "__ns_at__ant_design_icons",
        "isCommonJS": false
      },
      {
        "moduleId": "@ant-design/icons/Foo",
        "moduleFilePath": "pre-bundle-merged-<ruleName>.mjs",
        "exports": [
          "default",
          "__moduleExports"
        ],
        "exportAs": "__ns_at__ant_design_icons_Foo",
        "isCommonJS": false
      },
    ]

    当在 reference 插件中使用时,需要转换 import 代码:

    // helper
    
    export function _prebundle_merge_get_default(moduleLike) {
      return moduleLike.default ?? moduleLike;
    }
    
    export function _prebundle_merge_get_named(moduleLike, name) {
      if(name in moduleLike){
        return moduleLike[name];
      }
      if(moduleLike.default && name in moduleLike.default) {
        return moduleLike.default[name];
      }
    }
    // default import
    import Foo from '@ant-design/icons/Foo';
    // =>
    import { __ns_at__ant_design_icons_Foo as __ns_at__ant_design_icons_Foo$1 } from '@ant-design/icons/Foo';
    const Foo = _prebundle_merge_get_default(__ns_at__ant_design_icons_Foo$1);
    
    
    // named import
    import { Foo } from '@ant-design/icons';
    // =>
    import { __ns_at__ant_design_icons_Foo as __ns_at__ant_design_icons_Foo$2 } from '@ant-design/icons/Foo';
    const Foo = _prebundle_merge_get_named(__ns_at__ant_design_icons_Foo$2, 'Foo');
    
    // ns import
    import * as Icons from '@ant-design/icons';
    // =>
    import { __ns_at__ant_design_icons_Foo as __ns_at__ant_design_icons_Foo$3 } from '@ant-design/icons/Foo';
    const Icons = __ns_at__ant_design_icons_Foo$3;

    prebundle merge transform 的逻辑应该在 commonjs 转换之后,因为 commonjs transform 假定 module 只会导出 { default: blabla }

    // input
    import {useCallback} from 'react';
    
    // transform commonjs
    import React from 'react';
    const useCallback = React.useCallback;
    
    // transform prebundle merge
    import { __ns_react as __ns_react$1 } from 'react';
    const React = _prebundle_merge_get_default(__ns_react$1);
    const useCallback = React.useCallback;

    好像也可以将 commonjs transform 跟 prebundle merge transform 合并?

    [
      {
        "moduleId": "react",
        "moduleFilePath": "pre-bundle-merged-<ruleName>.mjs",
        "exports": [
          "default"
        ],
        "exportAs": "default",
        "isCommonJS": true
      }
    ]
    import React, {useCallback} from 'react';
    
    // =>
    import { default as __ns_react$1 } from 'react';
    const React = _prebundle_merge_get_default(__ns_react$1);
    const useCallback = _prebundle_merge_get_named(__ns_react$1.'useCallback');

    接着,在 resolve 阶段,只需要将模块 id 都替换成 <path-to>/<moduleFilePath>

    预打包依赖项,真的可以提高构建效率吗?

    在我的项目中,第三方依赖的体积远大于实际的代码量,每次启动 dev server 或者构建 production artifacts,都需要消耗很多时间在转换 node_modules 代码上。预打包依赖就可以很好的解决这个问题。

    另外,配合 nx 的共享缓存,这个项目的所有贡献者、CI 流水线,都可以享受到预打包带来的提升。

    为啥不能用 UMD 格式提供 pre-bundle ?

    rollup 不支持为 umd bundle 生成 common shared chunk

  • DataStory SOW Model

    The problem

    In a workflow-style data processing system, if all the data sources are granted to complete, how do we ensure that the execution of a processing task can reach its complete state?

    Terms

    NameDescription
    Data StreamAn observable stream of async data emitted by something.
    SourceNode (Abbr. S)A node that creates a granted-to-complete Data Stream for the next node in the Flow, which is usually the start point of a data processing task.
    OperatorNode (Abbr. O)A node that observes the Data Stream from the previous node in the Flow, and creates a granted-to-complete Data Stream for the next node; which is usually used to transform data, fetch data from API using the incoming data, etc.
    WatcherNode (Abbr. W)A node that observes Data Stream from the previous node; which is usually used to inspect the data processing state, e.g. logging, and reporting progress.
    FlowA list consists of SourceNode, OperatorNode, and WatcherNode, it must start with SourceNode and end with WatcherNode (S, O….O, W) or completely consist of OperatorNode(O, O, …. O).
    DiagramA set of Flows

    Define the complement

    S

    The S creates a granted-to-complete data stream, when the data stream completes, S itself also reaches its complete state.

    The data stream S created is consumed by the next node (e.g., O, W) in the Flow list.

    type S<T> = DataStream<T>
    

    O

    In the Flow list, every incoming data from the previous node, makes O create a new granted-to-complete data stream for the next node. Therefore, O is state less, it doesn’t have a complete state, it propagates the complete state from the previous node to the next node.

    type O<A, B> = DataStream<A> => DataStream<B>
    

    As you can see, the O is a function that takes data stream in and out.

    W

    In the Flow list, W consumes data from the previous node, when the previous node comes to the complete state, the W reaches to its complete state.

    type W<T> = DataStream<T> => void
    

    Flow

    Stateful flow (S, O, …. O, W)

    If flow starts with a S and ends with a W, it is a stateful flow.

    When the last W, comes to a complete state, the Flow reaches to complete state.

    The complete state is propagated from the beginning S to the last W via many Os.

    type StatefullFlow<A, B> = {
      source: S<A>;
      operators: O[];
      watcher: W<B>;
    }
    

    Stateless flow (O, O, …. O, O)

    The stateless flow behaves like an O, it creates a granted-to-complete data stream based on the incoming data.

    type StatelessFlow = O[];
    

    Diagram

    When every flow in the Diagram completes, the diagram reaches a complete state, which means the current running data process task is completed.

    To have a granted-to-complete Diagram, we need to make sure:

    1. All S, O in the diagram are correctly implemented to create a granted-to-complete data stream, which can be verified by doing code reviews and writing unit tests for these nodes’ implementation
    2. A method to prevent an Infinite Flow

    The Infinite Flow

    For the OperatorNodes, we can have a compose function, it connects the 2 OperatorNode and returns a newly composed OperatorNode.

    function compose(op1: O<A, B>, op2: O<B,C>): O<A, C>;
    

    This enables us to convert any Stateless Flow into an OperatorNode.

    We can also have a delegate function for OperatorNode:

    function delegate(operatorRef: ()=> O<A, B>): O<A, B>;
    

    It creates a new O, which creates the data stream using the returned O of operatorRef when it receives incoming data from the previous node.

    With the help of compose and delegate, it is possible to reuse stateless flow in the diagram:

    Since the reuse is based on reference, it is possible to make reference circles in the diagram, which may lead to infinite processing.

    Branch

    To avoid this problem, we need to add the branching ability to OperatorNode.

    function branch(selector: (data: T) => O<T,T>): O<T, T>
    

    branch creates a new O, when data arrives at O, it sends the data to the O’ returned by selector(data), then pipe the output of O’ to the next node.

    If there is no need to do branching for some data, the selector function can return a identity O:

    function identity: O<T, T>
    

    It just outputs what it received.

    With the help of branch and identity, we can now add an exit branch for a circular flow, but the exit branch can not be verified until running the data processing task.

    To avoid infinite processing at runtime, we need to implement a delegation depth detection at runtime.

    Delegation Depth Detection

    The delegation depth actually means “How many times does the data pass through a DeledateNode“, it simply comes to an idea that attaching a delegateCount property to the data inside the data stream. When data passed through a DelegateNode created by delegate, the delegateCount increases.

    When the delegateCount reaches a limit, let’s say, 200k, the DelegateNode should drop the data and write a warning log.

    Answer to the beginning question

    1. Make sure all S, and O are correctly implemented to create a granted-to-complete data stream
    2. Use compose, delegate to allow circular reference in the Diagram
    3. Use branch to add an exit branch to the circular flow
    4. Use delegate to avoid infinite-traveling data in the flow at runtime
  • 给大代码库的类型检查提速

    这是一系列博文中的一篇,用来记录将我将公司的前端项目改造为 monorepo 的过程。

    4 / 4 of Nx Monorepo Experience

    当代码库达到一定规模后,在每次提交前进行完整的 TypeScript 类型检查会非常耗时,就像下面的代码库,执行完整的检查需要 47s,如果每次代码提交时执行类型检查,开发者都得等上接近 1 分钟才能完成 git commit,极大的拉低了开发体验。

    ───────────────────────────────────────────────────────────────────────────────
    Language                 Files     Lines   Blanks  Comments     Code Complexity
    ───────────────────────────────────────────────────────────────────────────────
    TypeScript                3721    271374    23335     21340   226699      16639
    
     nx run-many -t check --all
    
          nx run icons:check (3s)
          nx run visual:check (4s)
          nx run eslint-plugin-merico:check (4s)
          nx run vdev-api:check (176ms)
          nx run nx-plugin:check (3s)
          nx run illustrations:check (4s)
          nx run shared-lib_temp:check (4s)
          nx run fekit:check (12s)
          nx run charts:check (14s)
          nx run main:check (39s)
    
     —————————————————————————————
    
     >  NX   Successfully ran target check for 10 projects (47s)
    

    那么为什么会这么慢呢?责任真的全在 nodejs 的性能上?

    为什么这么慢

    TypeScript 文档中这样写道:

    Rather than doing a full check of all d.ts files, TypeScript will type check the code you specifically refer to in your app’s source code.

    所以当你的 monorepo 中存在项目间引用的时候,执行 tsc -p lib-bob/tsconfig.json 会同时在 lib-bob 及其依赖的 lib-alice 上运行类型检查,这意味着,如果你在 monorepo 中对所有项目分别执行 tsc -p xx/tsconfig.json,被其他项目依赖的项目,会被重复检查多次!

    为了避免这种问题,TypeScript 的 build mode 提供了一种基于增量构建的解决方案。启用 build mode 后,庞大的代码库可以被拆分成多个项目分别被 tsc 构建,tsc 会缓存每个项目的构建结果,当一个项目被其他项目引用的时候,这个项目不会被重新构建而是使用之前的构建结果。在后续的执行中,只有发生变动的项目才会被重新构建。

    如何配置 build mode

    启用 build mode 的方法比较简单,你只需要调整 tsconfig.json 中以下几个选项即可:

    • compilerOptions.composite: true,用来标记这个项目可以被增量构建
    • compilerOptions.noEmit: false,启用增量构建必须允许 tsc 生成文件
    • compilerOptions.outDir: path/to/tsc-out,设置生成文件(包括缓存文件)的路径,记得加入 gitignore
    • compilerOptions.emitDeclarationOnly: true,如果你使用 tsc 构建 js 文件,可以不设置这个选项
    • references,把依赖项目的 tsconfig 路径添加到这里,让 tsc 可以使用这些依赖项目的构建缓存
    • compilerOptions.rootDir,不建议设置,避免构建缓存生成到 outDir 目录之外

    现在,你就可以使用 tsc -b (注意,不是 tsc -p) 来构建你的项目了,同样还是开头展示的代码库,现在重新运行类型检查:

     time nx run-many -t check --all --skip-nx-cache
    
          nx run illustrations:check (3s)
          nx run icons:check (5s)
          nx run visual:check (5s)
          nx run shared-lib_temp:check (781ms)
          nx run vdev-api:check (5s)
          nx run eslint-plugin-merico:check (832ms)
          nx run nx-plugin:check (2s)
          nx run fekit:check (10s)
          nx run fekit-e2e:check (2s)
          nx run charts:check (12s)
          nx run main:check (27s)
    
     ——————————————————————————————
    
     >  NX   Successfully ran target check for 11 projects (44s)
    
    
    ________________________________________________________
    Executed in   44.50 secs    fish           external
       usr time  119.40 secs  213.00 micros  119.40 secs
       sys time    6.17 secs   67.00 micros    6.17 secs 
    

    看起来似乎长进不大,再运行一遍试试?

     time nx run-many -t check --all --skip-nx-cache
    
          nx run illustrations:check (752ms)
          nx run visual:check (760ms)
          nx run icons:check (768ms)
          nx run shared-lib_temp:check (732ms)
          nx run fekit:check (880ms)
          nx run vdev-api:check (1s)
          nx run eslint-plugin-merico:check (731ms)
          nx run charts:check (900ms)
          nx run nx-plugin:check (756ms)
          nx run fekit-e2e:check (773ms)
          nx run main:check (1s)
    
     —————————————————————————————
     >  NX   Successfully ran target check for 11 projects (4s)
     
    
    ________________________________________________________
    Executed in    4.49 secs    fish           external
       usr time   13.51 secs    0.00 micros   13.51 secs
       sys time    1.86 secs  369.00 micros    1.86 secs
    

    类型检查速度变得飞快,这是因为 tsc 复用了第一次生成的构建缓存。如果你这时检查 outDir,你就会发现除了 d.ts 之外,tsc 还生成了 tsbuildinfo 文件,这就是 tsc 用来判断是否可以复用 d.ts 的关键。

     ls dist/out-tsc/icons
    components  tsconfig.build.tsbuildinfo
    

    可以想象的是,在平常开发过程中,每个 commit 通常只会修改一小部分文件,如果我们把代码库中较大的项目(例如上面的 main 项目,代码量有 18w 行)进一步拆分,就可以更加充分地利用增量构建的特性,运行类型检查的范围就会更小,速度就会更快。

    维护 monorepo 中的项目依赖

    细粒度的拆分项目在提高类型检查速度的同时,又会带来一个新的问题,由开发者人工维护项目之间的 references 字段会变得很麻烦,而且容易出错。为了减少人工操作的负担,我开发了一个小工具 —— ts-sync-ref,它会分析项目的 monorepo 内依赖,进而更新 tsconfig.json 的 references 字段。

    # 安装 ts-sync-ref
    npm install -g @zeeko/ts-sync-ref
    # 更新 my-lib 的依赖到 tsconfig.json 中的 references 字段
    cd /path/to/my-monorepo
    ts-sync-ref -p packages/my-lib/tsconfig.json -f 'packages/my-lib/src/**/*.ts'
    

    你可以阅读 ts-sync-ref 的说明文档、代码,进一步了解使用方式及其实现原理。

    结论

    Monorepo 的组织方式有很多种,不管你在用什么工具管理你的 TypeScript monorepo,不妨试试通过 TypeScript 的 Project Reference 将大代码库拆分成多个项目,不仅可以提升类型检查、构建速度,还可以减少编辑器的内存占用,极大地提高开发体验。

    不过,需要注意的是,直接启用 Project Reference 可能会给你的项目带来一些影响,导致现有的构建脚本出错,例如:

    最后,如果你有兴趣的话,也欢迎你在评论区分享你的项目执行类型检查的方案。

  • 如何使用 nx 缓存构建产物

    这是一系列博文中的一篇,用来记录将我将公司的前端项目改造为 monorepo 的过程。

    3 / 4 of Nx Monorepo Experience

    There are only two hard things in Computer Science: cache invalidation and naming things.

    Phil Karlton

    nx 的缓存机制如何工作

    How Caching Works 这篇文章中非常详细地说明了 nx 缓存构建结果的机制:
    nx 会计算当前执行的 target 的 Hash 作为 cache key,在 target 执行完成后把构建输出(包括终端输出、构建结果文件)作为缓存内容存储起来。

    通过配置 project.json 中 target 的 inputs 选项,我们可以调整能够让缓存失效的文件、环境变量,具体的配置方法可以查看这里

    在编写文件 glob 模式的时候,有两个特殊的变量:

    • {projectRoot} :当前项目的根目录
    • {workspaceRoot}:nx 工作区的根目录

    然而不管是在 inputs 还是 outputs 配置中,路径都是相对于 workspace root 的,所以 {workspaceRoot} 变量基本上没啥作用。

    如何为项目添加构建缓存

    缓存失效一直都是计算机领域的一个难题,在配置 nx 构建缓存的时候,我有以下两点建议:

    • inputs 配置应该宁滥毋缺,因为缓存不同步的严重性远大于缓存命中率低
    • outputs 配置必须不多不少,避免无关文件被错误地覆盖或者构建结果缺失

    要做到上面两点需要精确地控制 inputs 跟 outputs 的文件匹配模式,这里推荐几种帮你减少工作量方法:

    在拆分模块需要注意,尽量采用类似 Module Federation 或者 Webpack DLLPlugin 等机制,这种构建方式能够真正实现分模块打包。如果没有采用类似的机制,所有的模块都会在入口模块处被重新处理,所以除了入口模块之外,其他的模块没必要 minify ,这样可以加快打包速度。

    共享缓存执行结果

    nx 缓存默认存储在本地,独乐乐不如众乐乐,通过添加 nx-remotecache-minio,我们可以把 nx 缓存存储在任意与 S3 兼容的对象存储服务器上。如此,只要项目中的任意一位成员甚至 CI 服务器构建过某个模块,其他人都可以跳过构建直接从远程服务器拉取构建结果。

    需要注意的是,nx-remotecache-minio 没有处理访问对象存储超时的情况,所以一旦遇到对象存储服务器不可用的情况,记得及时终止构建,并将 NX_CACHE_MINIO_URL 环境变量设置成一个无法解析的地址,避免构建命令卡住。

  • 拒绝重复劳动,自动为项目添加 target

    这是一系列博文中的一篇,用来记录将我将公司的前端项目改造为 monorepo 的过程。

    2 / 4 of Nx Monorepo Experience

    完成了自制的插件过后,我们得往需要使用 lingui 的项目中添加相关的 target,项目少还好说,项目一多,这就变成纯体力劳动了。还好, nx 提供了 Project Inference 机制,给插件加上几行代码,就可以让 nx 自动为项目添加合适的 target。

    与 executor 不同,project inference 功能需要在 nx.json 中注册:

    {
      "plugins": [
        "my-nx-plugin"
      ]
    }

    Project inference 功能由 my-nx-plugin 模块默认导出的两个变量来实现:

    • projectFilePatterns 主要用来识别项目文件,项目目录中匹配的文件会作为参数传递给 registerProjectTargets
    • registerProjectTargets 是一个用来根据项目文件推断 targets 的函数,它返回一个 Record<string, TargetConfiguration>,即我们在 project.json/targets 中编写的内容。

    一个给项目添加 lingui target 的实现如下:

    import { workspaceRoot } from '@nrwl/devkit';
    import * as path from 'path';  
    import * as fs from 'fs';
    import type { TargetConfiguration } from '@nrwl/devkit';
    
    export const projectFilePatterns = ['package.json'];
    
    export function registerProjectTargets(projectFilePath: string): Record<string, TargetConfiguration> {
     // 通过导入的 workspaceRoot 变量来获取当前 nx workspace 的根目录,这样可以将 projectFilePath 转换为绝对路径
     const projectRoot = path.join(workspaceRoot, path.dirname(projectFilePath));  
     return { 
       ...linguiTargets(projectRoot),  
     };  
    }
    
    function linguiTargets(projectRoot) {
      const packageJSON = fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8');
      // 只要项目依赖了 lingui,就给它添加 lingui target
      if (packageJSON.indexOf('lingui') > 0) {
        return {
          lingui: {
            executor: 'nx-plugin:lingui',
            options: {
              locales: path.join(projectRoot, 'locales'),
            }
          }
        }
      }
    }

    在实现这个功能的时候需要注意几点 registerProjectTargets 必须是个同步函数,所以不能使用任何异步 API,也没法用之前提到的方法来引用 ESM。另外,nx 出于性能考量,会缓存 registerProjectTargets 的结果,所以在 Debug 的时候,一定要记得设置环境变量 NX_CACHE_PROJECT_GRAPH=false

    在处理文件路径的时候,建议尽量使用绝对路径,使用相对路径需要思考是相对于 workspace root 还是 cwd,用绝对路径可以减少这方面的心智负担。

    虽然功能实现了,但是现在还不能立马使用,需要将插件源码转译成 CJS 代码。前面的文章提到过用 postinstall 来编译自定义 nx 插件,在实现了今天的功能后,编译自定义插件的命令必须不能用 nx 来执行,因为 nx 执行任何命令都会加载插件模块,如果我们的插件没有编译成 CJS,通过 nx 调用的编译插件命令也会失败。

    Project Inference 功能非常强大,可以很大地减少我们维护项目 targets 的负担,在我看来,这算得上 nx 的旗舰功能了,后面我也会再演示一些其他使用 Project Reference 的典型场景。

  • 年轻人的第一个 nx 插件

    这是一系列博文中的一篇,用来记录将我将公司的前端项目改造为 monorepo 的过程。

    1 / 4 of Nx Monorepo Experience

    我们的项目中使用 lingui 作为 i18n 方案,在之前的实践中,每个项目都在 package.json 中维护了调用 lingui 提取 i18n 文本的命令,除此之外,在每个项目的 CI 中也包含了检查 i18n 文本是否存在遗漏的脚本。

    把项目迁移到 nx 管理的 monorepo 后,就可以着手把各个项目中重复维护的 lingui 相关脚本交给 nx 插件来实现,减少维护 lingui 相关脚本的负担。

    添加 lingui executor

    lingui 并没有提供官方的 nx 插件,但我们自己在 monorepo 中创建一个 nx plugin 来实现相关的功能。一般来说,nx 插件能够提供三种能力:

    1. executors,用来执行工具命令,例如,编译、运行测试
    2. generators,用来创建模板文件
    3. 自动推断项目类型,可以根据项目类型自动地为项目添加相关的开发脚本

    对于 lingui 插件来说,只需要用到其中的 executor,在项目的 project.json 中添加 lingui executor,就可以通过 nx lingui 来调用 lingui cli

    借助 nx 提供的 nx-plugin 插件,可以很快的把实现这些功能的基本模板搭建起来。

    实现 Lingui Executor

    Lingui Executor 的内容非常简单,只需要调用 lingui cli 就行了。这里我推荐使用 zx 来执行命令,zx 开箱即用,帮你处理好了调用命令行程序的一切工作,包括参数解析、命令行输出捕获、glob 匹配。唯一需要注意的是,zx 目前只提供 ESM 模块,而 nx 暂时无法调用 ESM 模块插件。为了兼容这两者之间的模块格式,我们的插件只能编译为 CJS 模块,通过 import('zx') 来引用 zx 中的函数。方便起见,我封装了 zx 中最常用的 $ 函数,使之可以自然地在 CJS 中使用:

    import { logger } from '@nrwl/devkit';
    export async function $$(template: TemplateStringsArray, ...args: any) {  
     const { $ } = await import('zx');  
     try {
       // 强制终端彩色输出
       $.env.FORCE_COLOR = '1';  
       // 关闭命令回显
       $.verbose = false;  
       if ($$.cwd) {  
         $.cwd = $$.cwd;  
       }  
       const process = $(template, ...args);  
       process.stdout.on('data', (data) => {  
         logger.info(data.toString());  
       });  
       process.stderr.on('data', (data) => {  
         logger.info(data.toString());  
       });  
       return await process;  
     } catch (e) {  
       logger.error(e.stdout);  
       logger.error(e.stderr);  
       throw e;  
     }  
    }  
      
    $$.cwd = undefined as string | undefined;

    需要注意的是,zx 执行命令的 stdio 必须通过 @nrwl/devkit 输出到终端,否则 nx 的缓存机制将无法取得 executor 执行过程中产生的命令行输出内容。

    export default async function runExecutor(options: ILinguiExecutorSchema, context: ExecutorContext) {    
     if (!options.localesDir) {  
       logger.error('options.localesDir is required');  
       return { success: false };  
     }  
     const localesDirPath = path.resolve(context.root, options.localesDir);  
     if (!context.projectName) {  
       return {  
         success: true,  
       };  
     }  
     const projectCwd = await getProjectCwd(context);  
     $$.cwd = projectCwd;
     try {
       await $$`yarn exec lingui extract --clean`;
     } catch (e) {
       return { success: false };
     } 
     return {  
       success: true,  
     };  
    }

    编译插件

    在运行 nx 命令时,nx 插件会被执行,所以需要在开发者调用任何 nx 命令之前就把本地项目中的插件编译成 nx 可以加载的 CJS 模块。我的做法是给本地插件加上一个 postinstall 脚本,这样在运行 yarn install 的时候,nx 插件就会被自动地编译,不需要手动干预。

    nx 默认生成的模板会使用 TypeScript 将代码编译为 CJS 模块,但 tsc 的编译速度比较慢,测试下来稍微有点影响实际的开发体验,我们可以用 swc 作为 tsc 的替代品,编译速度会快上很多。

    另一个用来编译插件的选项是 esbuild,但 esbuild 的目标并不是替代 tsc,在某些行为上跟 tsc 有不少差异,需要对项目做很多改动,才能适配 nx 执行 executor 的机制。对于 nx 插件来说,executor 跟 generator 都是插件模块的入口(entry),而 esbuild 是设计来将代码打包成一个 Bundle 的。

    最后

    给自己的项目开发 nx 插件是一件非常简单的事情,但是由于 nx 加载插件的限制,需要注意插件的编译方式以及 ESM 的引用方式。

  • 年轻人的第一个前端 monorepo 应该怎么选型

    如果你正在调研 monorepo 的技术选型,那你一定不能错过 nx。nx 是一个用来管理 monorepo 的构建系统,它提供了一系列的工具来帮助你实现代码的模块化、标准化,让开发人员在管理任意规模的 monorepo 时仍能够游刃有余。

    nx 提供了哪些功能?

    代码生成器
    通过生成器,nx 可以为你自动地创建、修改各种样板代码:

    # 替代 create-react-app 创建 react 项目
    $ nx g @nrwl/react:application my-app
    # 创建一个新的组件
    $ nx g component my-component --project=my-app
    # 给项目添加 tailwind
    $ nx g setup-tailwind --project=my-app

    任务执行器

    swc?esbuild?vite?vitest?playwright?放手去尝试这些新的工具,你可以先在单个项目中试点,当你做好准备后,就可以把单个项目中的实践封装为任务执行器,接下来 nx 就可以帮助你快速地将新的工具链应用到各个项目中。

    任务编排

    你可以放心地把工具库、业务模块提取成 monorepo 中独立的项目,nx 通过依赖分析能力,能够自动地理清项目之间的依赖关系,正确地帮你编排构建、测试脚本的执行顺序,提高执行工具构建、测试的执行效率。

    nx 适用于新项目吗?

    我非常推荐新的项目采用 nx,你可以利用 nx 插件,快速地创建各种新项目必需的各种样板代码、设置好各种开发工具的配置文件,再也不用去翻看 「XXX Get Started」文档。工具链的最佳实践由 nx 搞定,你只需要在功能实现上展示专业水平。

    nx 适用于已有的小型项目吗?

    处于代码量茁壮成长阶段的小型项目,需要注意代码的模块化、标准化,避免让代码库成长为超大的面条怪物。

    在拆分模块的时候,可以先用生成器定制符合项目需求的代码样板,减少提取项目模块的工作量。接着,你还可以为项目中使用的开发工具(Storybook、Cypress 等)创建执行器,方便在提取出来的项目模块中复用同样的工具链。

    nx 适用于已有的大型单体项目吗?

    大型项目往往会遇到编译缓慢的问题,通过将大型项目拆分成多个模块,利用 nx 的构建缓存以及增量构建功能,可以提高项目整体构建速度,极大地提升开发体验。

    nx 适用于将已有的多仓库项目改造成 monorepo 吗?

    nx 提供的 project.json 能够帮助你抹平不同项目使用的工具脚本之间的差异,让你可以用尽量小的代价使整个 monorepo 工作起来。项目成功运行之后,你就可以考虑使用执行器来替换掉各个项目中的开发脚本,减少维护各种工具脚本的负担。

    需要更换包管理器吗?

    简单来说,nx 跟任何包管理工具都能够很好的工作,它并不依赖于某种独特的包管理机制。但这也是 nx 的不足之处,它没法帮你解决管理第三方依赖时可能会遇到的问题(NPM 中的 phatom 与 doppelgangers 问题),你可能需要针对这种问题去选择适合自己的包管理工具。

    该用怎样的方式管理 monorepo ?

    我的 nx 实践体验可以总结为三点,希望对你有所启发:

    1. 拆分模块,利用构建缓存、任务编排、增量构建提高构建效率
    2. 用样板代码生成器,保持各个模块代码结构上的一致性,减少创建、更新样板代码的手动操作,在规模庞大的 monorepo 中,这点尤其重要
    3. 复用执行器,为 monorepo 中所有的模块提供标准化的开发体验
Index