阅读视图

发现新文章,点击刷新页面。

每日一题-执行操作后的最大 MEX🟡

给你一个下标从 0 开始的整数数组 nums 和一个整数 value

在一步操作中,你可以对 nums 中的任一元素加上或减去 value

  • 例如,如果 nums = [1,2,3]value = 2 ,你可以选择 nums[0] 减去 value ,得到 nums = [-1,2,3]

数组的 MEX (minimum excluded) 是指其中数组中缺失的最小非负整数。

  • 例如,[-1,2,3] 的 MEX 是 0 ,而 [1,0,3] 的 MEX 是 2

返回在执行上述操作 任意次 后,nums 的最大 MEX

 

示例 1:

输入:nums = [1,-10,7,13,6,8], value = 5
输出:4
解释:执行下述操作可以得到这一结果:
- nums[1] 加上 value 两次,nums = [1,0,7,13,6,8]
- nums[2] 减去 value 一次,nums = [1,0,2,13,6,8]
- nums[3] 减去 value 两次,nums = [1,0,2,3,6,8]
nums 的 MEX 是 4 。可以证明 4 是可以取到的最大 MEX 。

示例 2:

输入:nums = [1,-10,7,13,6,8], value = 7
输出:2
解释:执行下述操作可以得到这一结果:
- nums[2] 减去 value 一次,nums = [1,-10,0,13,6,8]
nums 的 MEX 是 2 。可以证明 2 是可以取到的最大 MEX 。

 

提示:

  • 1 <= nums.length, value <= 105
  • -109 <= nums[i] <= 109

Nx带来极致的前端开发体验——任务缓存

前言

前面我们讲过,为了提高项目的构建速度,社区将大部分的精力放到构建工具上,例如rspack、esbuild、swc等,利用语言优势提升构建速度。而像 webpack 这种老牌构建工具则将优化方向放在缓存上,但是他缓存的是构建流程中的中间结果,例如每个文件经过 loader 转换后的产物。

而本章节要介绍的任务缓存是指缓存任务执行之后的产物,例如构建或者测试任务,对于一个 package 来说,如果他的代码没发生改变,下一次执行 build 命令时可以直接读取上一次的构建产物,而无需再次进行构建,因为每次重复构建或者测试同一段代码的成本是非常昂贵的。

下面我们将专注于 Nx 的任务缓存机制,一起学习它的功能使用和原理实现。

定义缓存任务

使用 nx 创建项目时默认启用了任务缓存,开发者也可以在根目录的 nx.json 中全局配置任务缓存,也可以在每个 package 的 package.json 中单独配置。

// nx.json
{
  "targetDefaults": {
        "build": {
            "cache": true
        }
        "test": {
            "cache": true
        }
    }
}

// package.json
{
    "name": "myreactapp",
    ...
    "targets": {
        "build": {
            "cache": true
        }
        "test": {
            "cache": true
        }
    }
}

以下面这个项目为例:

image.png

我们有一个 cache-test 项目,其依赖了 card 和 shop 这两个 package,当首次执行 cache-test 的 build 命令时,输出如下:

image.png

当修改 shop 中的代码再次执行cache-test 的 build 命令时,输出如下:

image.png

可以发现 card 并没有重新构建,而是读取的上一次构建的产物。除此之外我们分别在 shop 和 card 中添加了单测逻辑,并使用的 vitest,我们可以看下 test 任务的缓存逻辑是否生效(nx run-many --target=test),首次运行如下:

image.png

修改 card 的代码之后再次运行:

image.png

从输出结果可以看出,shop 读取的是缓存的结果。

缓存原理

在执行任何可缓存任务之前,比如 nx build myappnx test myapp,Nx 都会预先计算一段哈希值,这个哈希值表示了:**如果输入完全一样,那么输出也会完全相同。**也就是说,只要任务的所有输入条件都没变,Nx 就可以直接用之前的缓存结果(跳过实际执行),来极大加速构建或测试。

默认情况下,对于 nx test myapp 这种任务其计算哈希的输入会包括:

  • myapp 的源码以及其依赖项的源码。
  • 全局配置,比如 nx.json、tsconfig.base.json。
  • 第三方依赖版本。
  • 运行时环境,例如 node 版本。
  • 命令行参数

image.png

目前大部分 monorepo 项目管理工具都采用类似的策略实现任务缓存,例如rush.jsturborepo等。

配置输入条件

Nx 默认配置的输入条件是非常保守的,默认会把比较多的可能影响输出的内容算进去,避免漏掉导致复用过时结果,下面是 Nx 为 build 任务生成的默认输入条件:

//nx.json

{
    "namedInputs": {
        "default": [
            "{projectRoot}/**/*",
            "sharedGlobals"
        ],
    "production": [
        "default",
        "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
        "!{projectRoot}/tsconfig.spec.json",
        "!{projectRoot}/src/test-setup.[jt]s"
    ],
    "sharedGlobals": []
    },
    // ...
    "targetDefaults": {
        "build": {
            "inputs": ["production", "^production"],
            "cache": true
        }
    }
}

namedInputs 中定义的是一些通用的输入集,nx 默认定义了两个输入集 defaultproduction ,每个输入集中会通过特定的语法定义文件的匹配规则。而每个规则中的projectRoot 表示当前 package 的根目录。

targetDefaults 中配置了 build 任务的输入条件,第一个条件 production 很好理解,表示符合 production 输入集下的所有文件都作为输入内容。第二个 ^production 则表示其依赖的 package 中所有符合 production 输入集的所有文件都作为输入内容。

举个例子,我们希望将 *.md 文件从输入中排除,像 build 和 test 都不需要依赖 *.md 文件,为了实现这一点可以添加以下配置:

//nx.json
{
"namedInputs": {
        "production": [
            "default",
            "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
            "!{projectRoot}/tsconfig.spec.json",
            "!{projectRoot}/src/test-setup.[jt]s",
+           "!{projectRoot}/**/*.md"
        ]
    }
}

总结

在本章节中我们介绍了 Nx 的一个核心功能——任务缓存,任务缓存是通过直接缓存任务的最终输出结果来提升效率,这与构建工具的缓存机制有所不同,像 webpack 这种构建工具的缓存功能缓存的是构建过程的中间结果。

接着我们介绍了如何在项目中定义任务缓存,当我们使用 Nx 的插件生成代码时,Nx 会默认给任务自动配置缓存,并设置默认的缓存输入条件。除此之外 Nx 还允许用户配置缓存的输入条件,灵活控制缓存的实效性。

【搞发🌸活】不信书上那套理论!亲测Javascript能卡浏览器Reader一辈子~

点进来的前端佬,先别走!

让我详细给你逼逼叨!

93B1A00879A9B67271080936B8A2D89CE1D69417_size242_w423_h220.gif

在很久很久以前,前端圈就广泛流传,Javascript的加载和执行,都会阻塞浏览器Render。

然后过了这些日子,作为一名优秀的前端佬的意识爆发。

按照上面的说法,那是不是可以构造一个Javascript程序,让后续的CSS以及HTML文本永远都不能被解析Render到?

喔,觉的挺来劲的,说干就干!

image.png

前言

一开始构建了这么一个HTML,如下:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>demo</title>
</head>
<body>
  <h1 id="start" class="h1-title">开始渲染了</h1>
  <script>
    console.log(document.getElementById('start'))
  </script>
</body>
</html>
<script>
  // 此处插入代码
</script>
<h1 class="h1-title">看到这里就失败了!</h1>
<style>
  .h1-title {
    color: red;
  }
</style>

预想阻塞js代码会写在script标签里。

以上代码运行后如下:

image.png

以上展示的,因为没有填入代码,符合期望。

这里解释下为什么要将script脚本和h1要放在html之外。

因为根据各个资料上说,浏览器解读HTML文本就是从上往下解析的。当遇到</html>文档结束标签,就会开始生成DOM树+CSSOM树,并开始Render。

那我脑袋一拍,灵光一闪,自以为是的将需要Render的HTML和CSS放在</html>后,期望只Render第一行文字开始渲染了,而第二行文字看到这里就失败了!就永远得不到Render。

开始挑战!!!

方法一 递归

脑子第一个蹦出来的方法,就是用递归,来模拟JavaScript阻塞。

在上面HTML模板中填入如下代码:

function block() {
  Math.sqrt(Math.random());
  block();
}
block();

结果如下:

image.png

失败了,还在控制器里报了一个错误.RangeError: Maximum call stack size exceeded

oh,shit,明显这里我忽略了一个细节。

大家都知道的,Javascript是单线程运行机制。

而Javascript的函数分为解析和调用。解析有一个入栈的过程,调用有一个出栈过程。当入栈停止后,才会出栈被调用执行。而上面递归代码,构造了一个无限入栈的场景,结果就是直接撑爆内存。

很显然,浏览器识别到这种风险,直接作出报错处理。

失败~继续尝试!

方法二 while死循环

有了JS的单线程执行思路,顺理成章的,就有了使用while死循环,来模拟阻塞。

插入如下代码试一试。

while (true) {
  // 持续执行同步任务
  Math.sqrt(Math.random());
}

效果如下:

image.png

喔!成功了???? ★,°:.☆( ̄▽ ̄)/$:.°★

罗老师.gif

其实并没有~

之所以能有上面的效果,在于我使用了VSCODE中的Live Server插件,并构造了特殊的场景。基本原来就是Live Server是有热更新,我动态插入了</html>之后的代码到文件中。

image.png

究其原因,在现代浏览器中,浏览器有着强大的纠错机制。很多浏览器都不会遇到</html>就停止解析,忽略后续的文本。他们仍然会好心好意的将后续能看懂的文本,插入到<body>里去。

所以实际上,正常的去执行上面构造的代码,只能得到如下效果:

image.png

但现在离成功,也算走了一半!

动态插入的思路,让我想到了第三个方法。

方法三 按钮手动添加代码

这就是构造一个添加按钮,点击之后,动态添加上HTML标签和Script脚本。

初始是这个样子的:

image.png

HTML代码构造如下:

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>demo</title>
</head>
<body>
  <h1 id="start" class="h1-title">开始渲染了</h1>
  <button id="execute">添加</button>
  <script>
    function sleepBlocking(ms) {
      const start = Date.now();
      while (Date.now() - start < ms) {
        // 什么都不做,纯粹阻塞
      }
    }
    document.getElementById('execute').onclick = () => {
      now1 = new Date().getTime()
      // 在前
      document.getElementById('execute').insertAdjacentHTML('beforebegin', `
          <h1 class='h1-title'>看到这里就失败了!</h1>
          <style>
            .h1-title {
              color: red;
            }
          <\/style>
        `)
      // 在后
      const script = document.createElement("script");
      // 5秒后执行
      script.innerHTML = 'sleepBlocking(5000);console.log("休眠后", new Date().getTime() - now1)'
      console.log('所有脚本添加后', new Date().getTime() - now1)
      document.body.appendChild(script);
    }
  </script>
</body>
</html>

从上面的代码可以看到,我弄了一个阻塞执行的5秒函数。接下来预期的效果就是:

点击前,先展示黑色的文字开始渲染了

点击添加按钮后,经过5秒后,就会使得所有文字变红,并出现看到这里就失败了!的效果,最终如下图:

image.png

符合预期!!完美~

以上就是整个验证的思路了,个人觉的基本可以回答标题上的问题。Javascript是真的会阻塞浏览器Render!!

另外还有一种思路,就是使用stream来构造一个一直会执行的远程脚本,为避免无聊,这里就不尝试了,都是大差不差的。

如果还能看到这里的前端佬,那我想说在这个尝试的过程还有一个意外,就是我们经常会看到很多技术类文档,解说Event Loop,都会用上宏任务和微任务解释,个人觉的有点牵强不太行。感兴趣接着往下看!

方法四 构造永不结束的“宏任务”?

先贴下Event Loop的一些解释:

  1. 从宏任务的头部取出一个任务执行;
  2. 执行过程中若遇到微任务则将其添加到微任务的队列中;
  3. 宏任务执行完毕后,微任务的队列中是否存在任务,若存在,则挨个儿出去执行,直到执行完毕;
  4. GUI 渲染;
  5. 回到步骤 1,直到宏任务执行完毕;

按照上面思路,是不是我我构造一个永远不结束的宏任务,也可以阻塞Render???

现在我把Javascript代码替换成如下:

function setTime() {
  Math.sqrt(Math.random());
  setTimeout(() => {
     setTime()
  }, 1)
}
setTime()

然后我们看到的效果确实是这样的。

image.png

并没有阻止,定时器任务还在依旧运行代码。

所以,我是不太相信网上那些所谓的事件循环的解释了!

另外我自己去找权威书籍《JavaScript高级程序设计(第4版)》 和 《JavaScript权威指南(第7版)》,英文版本,连那些词都没得~

嗯....先这样吧。

看到这里,我是想说,我这篇表情包很克制了!前端佬们给点小心心吧♥(ˆ◡ˆԅ)

新疆马蹄打了北鼻.gif

3ms,晚上突然想到的解法

Problem: 6321. 执行操作后的最大 MEX

[TOC]

思路

思路看代码,我就举个例子

比如数组为[1,-10,7,13,6,8],value=5,变为正数并取余之后为[1,0,2,3,1,3],对应的flag数组为[1,2,1,2,0],找到的出现次数最少的数的频率,用min记录为0,此时的num为数组的索引4,所以最终返回为4

比如数组为[1,-10,7,13,6,8],value=7,变为正数并取余之后为[1,4,0,6,6,1],对应的flag数组为[1,2,0,0,1,0,2],找到的出现次数最少的数的频率,用min记录为0,此时的num为数组的索引2,所以最终返回为2

Code

###Java


class Solution {
    public int findSmallestInteger(int[] nums, int value) {
        //flag只用来保存取余之后的值的个数,也就是[0,value-1]
        int[] flag=new int[value];
        //暂存nums[i]用
        int temp;
        //遍历nums数组,将每个数数都和value取余,然后存放在flag中
        for(int i=0;i<nums.length;i++){
           temp=nums[i];
           //当nums[i]<0的时候,有两种情况
           //一种是value的倍数,取余直接为0,不能和不为0的合并,因为0+value直接溢出flag数组了
           //第二种取余不为0,直接加上value就行
           //当nums[i]>0的时候,直接取余就好
           //当nums[i]==0的时候,直接记录在flag中
           if(temp<0){
               if(temp%value==0){
                   temp=0;
               }else{
                   temp=temp%value+value;
               }
           }else if(temp>0){
               temp=temp%value;
           }
           flag[temp]++;
        }
        int num=0;
        int min=Integer.MAX_VALUE;
        //找到[0,value-1]中出现次数最少的数的频率,用min记录
        //并使用num记录出现次数最少的数
        for(int i=0;i<flag.length;i++){
           if(flag[i]<min){
               min=flag[i];
               num=i;
           }
        }
        //如果min为0,说明num就是我们要找的答案,否则num+value*min就是我们要找的答案
        if(min==0){
            return num;
        }else{
            return num+value*min;
        }

    }

}

贪心

解法:贪心

思维第一步

看到“任一元素加上或减去 value”,而且“可以操作任意次”,马上反馈出等价条件:

数 $x$ 可以变成任意满足 y mod value == x mod value 的数 $y$。

因此,我们可以把整个序列按元素取模的值分成 value 类,每一类元素都不会因为操作而变成其他类别里的元素。

思维第二步

既然我们要让“缺失的最小非负整数”最大,设第 $i$ 类里有 $k$ 个数,那么它们需要变成 $i, v + i, 2v + i, \cdots (k - 1)v + i$ 才能让 mex 尽可能大。

举个例子帮助大家理解,假设 $v = 5$,第 $2$ 类里有 $3$ 个数,那么它们需要变成 $2, 7, 12$ 才能让 mex 尽可能大。如果变成别的,比如 $2, 12, 17$,那因为缺失了 $7$(而且其他类别的数也没法变成 $7$),那 mex 至多就是 $7$ 了。如果是 $2, 7, 12$,那 mex 还有可能是 $17$,肯定这样做更优。

最终处理

既然我们已经确定了每个数都要变成什么,那完成变化以后,直接计算变化后序列的 mex 就是答案。

复杂度 $\mathcal{O}(n)$。

参考代码(c++)

###c++

class Solution {
public:
    int findSmallestInteger(vector<int>& nums, int v) {
        int n = nums.size();
        // vis[i] 表示变化后的序列里是否出现过元素 i
        // 答案肯定不超过 n
        // 因为最好情况就是 nums = [0, 1, ..., n - 1],这样 mex = n
        bool vis[n + 1];
        memset(vis, 0, sizeof(vis));
        
        // cnt[i] 表示第 i 类元素目前有几个
        int cnt[v];
        memset(cnt, 0, sizeof(cnt));
        for (int x : nums) {
            // t 是元素 x 所属的类别
            int t = (x % v + v) % v;
            // y 是元素 x 应该变成什么数
            int y = cnt[t] * v + t;
            if (y < n) vis[y] = true;
            cnt[t]++;
        }
        
        // 计算变化后序列的 mex
        for (int i = 0; i <= n; i++) if (!vis[i]) return i;
        return -1;
    }
};

同余分组(Python/Java/C++/C/Go/JS/Rust)

下文记 $m=\textit{value}$。

由于同一个数可以加减任意倍的 $m$,我们可以先把每个 $\textit{nums}[i]$ 变成与 $\textit{nums}[i]$ 关于模 $m$ 同余的最小非负整数,以备后用。关于同余的介绍,请看 模运算的世界:当加减乘除遇上取模

本题有负数,根据这篇文章中的公式,我们可以把每个 $\textit{nums}[i]$ 变成

$$
(\textit{nums}[i]\bmod m + m)\bmod m
$$

从而保证取模结果在 $[0,m)$ 中。

例如 $\textit{nums}=[1,-6,-4,3,5]$,$m=3$,取模后变成 $[1,0,2,0,2]$。

然后枚举答案:

  • 有没有与 $0$ 关于模 $m$ 同余的数?有,我们消耗掉一个 $0$。
  • 有没有与 $1$ 关于模 $m$ 同余的数?有,我们消耗掉一个 $1$。
  • 有没有与 $2$ 关于模 $m$ 同余的数?有,我们消耗掉一个 $2$。
  • 有没有与 $3$ 关于模 $m$ 同余的数?有,我们消耗掉一个 $0$。这个取模后等于 $0$ 的数,可以继续操作,变成 $3$。
  • 有没有与 $4$ 关于模 $m$ 同余的数?也就是看是否还有 $1$,没有,那么答案等于 $4$。

怎么知道还有没有剩余元素?用一个哈希表 $\textit{cnt}$ 统计 $(\textit{nums}[i]\bmod m + m) \bmod m$ 的个数。

本题视频讲解,欢迎点赞关注~

写法一

class Solution:
    def findSmallestInteger(self, nums: List[int], m: int) -> int:
        cnt = Counter(x % m for x in nums)
        mex = 0
        while cnt[mex % m]:
            cnt[mex % m] -= 1
            mex += 1
        return mex
class Solution {
    public int findSmallestInteger(int[] nums, int m) {
        int[] cnt = new int[m];
        for (int x : nums) {
            cnt[(x % m + m) % m]++; // 保证取模结果在 [0, m) 中
        }

        int mex = 0;
        while (cnt[mex % m]-- > 0) {
            mex++;
        }
        return mex;
    }
}
class Solution {
    public int findSmallestInteger(int[] nums, int m) {
        Map<Integer, Integer> cnt = new HashMap<>();
        for (int x : nums) {
            cnt.merge((x % m + m) % m, 1, Integer::sum);
        }

        int mex = 0;
        while (cnt.merge(mex % m, -1, Integer::sum) >= 0) {
            mex++;
        }
        return mex;
    }
}
class Solution {
public:
    int findSmallestInteger(vector<int>& nums, int m) {
        unordered_map<int, int> cnt;
        for (int x : nums) {
            cnt[(x % m + m) % m]++; // 保证取模结果在 [0, m) 中
        }

        int mex = 0;
        while (cnt[mex % m]-- > 0) {
            mex++;
        }
        return mex;
    }
};
int findSmallestInteger(int* nums, int numsSize, int m) {
    int* cnt = calloc(m, sizeof(int));
    for (int i = 0; i < numsSize; i++) {
        cnt[(nums[i] % m + m) % m]++; // 保证取模结果在 [0, m) 中
    }

    int mex = 0;
    while (cnt[mex % m]-- > 0) {
        mex++;
    }

    free(cnt);
    return mex;
}
func findSmallestInteger(nums []int, m int) (mex int) {
cnt := map[int]int{}
for _, x := range nums {
cnt[(x%m+m)%m]++ // 保证取模结果在 [0, m) 中
}

for cnt[mex%m] > 0 {
cnt[mex%m]--
mex++
}
return
}
var findSmallestInteger = function(nums, m) {
    const cnt = new Map();
    for (const x of nums) {
        const v = (x % m + m) % m; // 保证取模结果在 [0, m) 中
        cnt.set(v, (cnt.get(v) ?? 0) + 1);
    }

    let mex = 0;
    while ((cnt.get(mex % m) ?? 0) > 0) {
        cnt.set(mex % m, cnt.get(mex % m) - 1);
        mex++;
    }
    return mex;
};
use std::collections::HashMap;

impl Solution {
    pub fn find_smallest_integer(nums: Vec<i32>, m: i32) -> i32 {
        let mut cnt = HashMap::new();
        for x in nums {
            // 保证取模结果在 [0, m) 中
            *cnt.entry((x % m + m) % m).or_insert(0) += 1;
        }

        for mex in 0.. {
            if let Some(c) = cnt.get_mut(&(mex % m)) {
                if *c > 0 {
                    *c -= 1;
                    continue;
                }
            }
            return mex;
        }
        unreachable!()
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $\textit{nums}$ 的长度。由于加多少个数,就只能减多少个数,所以第二个循环至多循环 $\mathcal{O}(n)$ 次。
  • 空间复杂度:$\mathcal{O}(\min(n,m))$。哈希表中至多有 $\mathcal{O}(\min(n,m))$ 个元素。

写法二

lc2598-c.png{:width=500px}

此外,把哈希表换成数组更快。

class Solution:
    def findSmallestInteger(self, nums: List[int], m: int) -> int:
        cnt = [0] * m
        for x in nums:
            cnt[x % m] += 1

        i = cnt.index(min(cnt))
        return m * cnt[i] + i
class Solution {
    public int findSmallestInteger(int[] nums, int m) {
        int[] cnt = new int[m];
        for (int x : nums) {
            cnt[(x % m + m) % m]++;
        }

        int i = 0;
        for (int j = 1; j < m; j++) {
            if (cnt[j] < cnt[i]) {
                i = j;
            }
        }

        return m * cnt[i] + i;
    }
}
class Solution {
public:
    int findSmallestInteger(vector<int>& nums, int m) {
        vector<int> cnt(m);
        for (int x : nums) {
            cnt[(x % m + m) % m]++;
        }

        int i = ranges::min_element(cnt) - cnt.begin();
        return m * cnt[i] + i;
    }
};
int findSmallestInteger(int* nums, int numsSize, int m) {
    int* cnt = calloc(m, sizeof(int));
    for (int i = 0; i < numsSize; i++) {
        cnt[(nums[i] % m + m) % m]++;
    }

    int i = 0;
    for (int j = 1; j < m; j++) {
        if (cnt[j] < cnt[i]) {
            i = j;
        }
    }

    int ans = m * cnt[i] + i;
    free(cnt);
    return ans;
}
func findSmallestInteger(nums []int, m int) int {
cnt := make([]int, m)
for _, x := range nums {
cnt[(x%m+m)%m]++
}

i := 0
for j := 1; j < m; j++ {
if cnt[j] < cnt[i] {
i = j
}
}

return m*cnt[i] + i
}
var findSmallestInteger = function(nums, m) {
    const cnt = Array(m).fill(0);
    for (const x of nums) {
        cnt[(x % m + m) % m]++;
    }

    let i = 0;
    for (let j = 1; j < m; j++) {
        if (cnt[j] < cnt[i]) {
            i = j;
        }
    }

    return m * cnt[i] + i;
};
impl Solution {
    pub fn find_smallest_integer(nums: Vec<i32>, m: i32) -> i32 {
        let mut cnt = vec![0; m as usize];
        for x in nums {
            cnt[((x % m + m) % m) as usize] += 1;
        }

        let mut i = 0;
        for j in 1..m as usize {
            if cnt[j] < cnt[i] {
                i = j;
            }
        }

        m * cnt[i] + i as i32
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n+m)$,其中 $n$ 是 $\textit{nums}$ 的长度。
  • 空间复杂度:$\mathcal{O}(m)$。

相似题目

见下面数学题单的「§1.9 同余」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

Promise 常见面试题(持续更新中)

Promise 是现在前端中非常常用的一个构造函数,因为他的产生解决了传统开发中回调地狱的问题,也正因为经常用,所以在面试中就经常被问到相关的面试题,今天抽空整理了下常见的Promise 相关的面试题。

一、对Promise executor执行时机的了解

面试官问: 下面这段代码会输出什么?

console.log(1);

const promise = new Promise((resolve, reject) => {
  console.log(2);
  resolve(3);
  console.log(4);
});
console.log(5);

promise.then((res) => {
  console.log(res);
});
console.log(6);


输出结果如下:

image.png

很多初学者可能会觉得输出结果是1,5,6,2,3,4 因为他们觉得Promise 是异步的,但其实不是这样的,Promise 的异步是then方法是异步的,而executor(传给Promise构造函数的函数) 是同步执行的。

二、错误处理

1. then 的第二个参数和catch执行优先级的问题

请说出下面的打印结果

const promise = new Promise((resolve, reject) => {
  reject("出错了");
});

promise
  .then(
    (res) => {
      console.log(res);
    },
    (err) => {
      console.log("then 参数里面报出的错误:" + err);
    }
  )
  .catch((err) => {
    console.log("catch 参数里面报出的错误:" + err);
  });

输出结果如下:

image.png

这其实主要考察你在学习的时候善不善于思考,和测试各种情况,这里在then的第二个参数函数中和catch都进行了错误处理,如果你之前已经测试过了,相信能不加思考就能写出答案。但是如果你学习的时候没有思考过这个问题,这个时候可能就只能靠猜了。

2. 错误处理的穿透性

请问下面的代码会打印什么?

const promise = new Promise((resolve, reject) => {
  reject("出错了");
});

promise
  .then((res) => {
    console.log(res);
  })
  .then(
    (res) => {},
    (err) => {
      console.log("第二个then:" + err);
    }
  );

输出结果如下:

image.png

有很多初学者可能会回答什么都不输出,或者说报错,他们的想法是executor 里面执行的reject ,而第一个then里面里面又没有第二个参数进行处理,所以不会输出任何东西,但现实不是这样的,Promise在处理错误时具有穿透性,第一个then 没有处理时,会到下一个then 进行处理,如果所有的then都没有第二个参数,且没有catch 才会报错。

三、多个then 如何进行结果传递

请问下面代码会输出什么结果?

const promise = new Promise((resolve, reject) => {
  resolve("正确结果呀呀呀");
});

promise
  .then((res) => {
    console.log("第一个then:", res);
  })
  .then(
    (res) => {
      console.log("第二个then:" + res);
    },
    (err) => {
      console.log("第二个then:" + err);
    }
  );

正确输出结果如下:

image.png

这里如果你回答错了面试官可能就不会再问了,如果你回答对了,面试官可能还会继续追问如何让第二个then 和第一个then输出同样的结果呢,第一种方式直接在第一个then 的第一个函数中return res 即可,代码修改如下:

image.png

第二种方式: 在第一个then 的第一个函数中return 一个 新的Promise ,代码修改如下

image.png

查看输出结果:

image.png

可以看到是我们想要的结果

今天就写到这里了,感谢收看,后续还会持续更新,欢迎收藏加关注

跟着 AI 学 (一)- shell 脚本

echo "开始编译..."
CURRENT_PATH=`pwd`
CACHE_BASE_PATH=/data/cache/$PROJECT_NAME/$DEPOT_NAME/$CCI_JOB_NAME

# 根据参数决定是否清除缓存
if [ "$CLEAR_CACHE" = "true" ]; then
echo "清除 node_modules 缓存..."
rm -rf $CACHE_BASE_PATH/node_modules
fi

mkdir -p $CACHE_BASE_PATH/node_modules
docker run --network=host --rm \
-v $CURRENT_PATH:/root/workspace \
-v $CACHE_BASE_PATH/node_modules:/root/workspace/node_modules \
-v /data/cache/yarn/v6:/usr/local/share/.cache/yarn/v6 \
-e CI_ENV=1 \
coding-public-docker.pkg.coding.net/public/docker/nodejs:20-2024.01 \
/bin/bash -c "yarn install --frozen-lockfile && yarn build:sit && tar zcvf app-dom_front_sit.tar.gz dist"
echo "编译打包过程结束"

Shell 脚本逐行解析

脚本概述

这是一个用于 CODING CI/CD 构建流程的 Shell 脚本,主要功能是使用 Docker 容器编译前端项目并打包。


逐行代码解析

第 1 行:输出提示信息

echo "开始编译..."

说明: 使用 echo 命令输出提示信息到终端,告诉用户脚本开始执行了。


第 2 行:获取当前工作目录

CURRENT_PATH=`pwd`

说明:

  • pwd 命令获取当前工作目录的完整路径
  • 反引号 `pwd` 表示命令替换,会执行 pwd 命令并将结果赋值给变量
  • CURRENT_PATH 变量保存了当前目录路径
  • 例如:/root/workspace/your-project

第 3 行:构建缓存路径

CACHE_BASE_PATH=/data/cache/$PROJECT_NAME/$DEPOT_NAME/$CCI_JOB_NAME

说明:

  • 定义缓存基础路径变量
  • $PROJECT_NAME$DEPOT_NAME$CCI_JOB_NAME 是引用的环境变量(在脚本外部定义)
  • 这行构建了一个分层的缓存目录结构
  • 例如:/data/cache/myproject/frontend/build-job-01

第 5-9 行:条件清除缓存

if [ "$CLEAR_CACHE" = "true" ]; then
    echo "清除 node_modules 缓存..."
    rm -rf $CACHE_BASE_PATH/node_modules
fi

说明:

第 6 行 - 条件判断:

  • if 条件判断语句开始
  • [ ] 是测试命令(test 命令的另一种写法)
  • "$CLEAR_CACHE" 引用环境变量(加引号防止变量为空时出错)
  • = 是字符串相等比较运算符
  • then 表示如果条件为真,执行下面的命令

第 7 行 - 输出提示:

  • 输出清除缓存的提示信息

第 8 行 - 删除缓存:

  • rm -rf 递归强制删除目录及其内容
    • -r: 递归删除目录
    • -f: 强制删除,不提示确认

第 9 行 - 结束条件块:

  • fi 结束 if 语句块(if 的反向拼写)

第 11 行:创建缓存目录

mkdir -p $CACHE_BASE_PATH/node_modules

说明:

  • 创建缓存目录
  • -p 参数:如果父目录不存在则自动创建,且如果目录已存在不报错
  • 确保缓存目录存在,即使之前被删除了

第 12-18 行:启动 Docker 容器并执行编译

docker run --network=host --rm \
-v $CURRENT_PATH:/root/workspace \
-v $CACHE_BASE_PATH/node_modules:/root/workspace/node_modules \
-v /data/cache/yarn/v6:/usr/local/share/.cache/yarn/v6 \
-e CI_ENV=1 \
coding-public-docker.pkg.coding.net/public/docker/nodejs:20-2024.01 \
/bin/bash -c "yarn install --frozen-lockfile && yarn build:sit && tar zcvf app-dom_front_sit.tar.gz dist"

说明:

第 12 行 - Docker 运行命令:

  • docker run: 启动 Docker 容器
  • --network=host: 容器使用宿主机的网络栈(容器可以直接访问宿主机网络)
  • --rm: 容器退出后自动删除
  • \ 是行连接符,表示命令在下一行继续

第 13-15 行 - 挂载卷:

  • -v 挂载卷(volume),将宿主机目录映射到容器内
  • 格式:宿主机路径:容器内路径
  • 第 13 行:挂载当前项目目录到容器的 /root/workspace
  • 第 14 行:挂载 node_modules 缓存目录(实现依赖缓存复用)
  • 第 15 行:挂载 yarn 全局缓存目录(加速 yarn 包下载)

第 16 行 - 设置环境变量:

  • -e 设置容器内的环境变量
  • CI_ENV=1 告诉程序当前在 CI(持续集成)环境中运行

第 17 行 - 指定镜像:

  • 指定要使用的 Docker 镜像
  • 这是一个 CODING 提供的 Node.js 20 镜像(2024.01 版本)

第 18 行 - 容器内执行命令:

  • /bin/bash -c "命令": 在容器内执行 bash 命令
  • && 逻辑与操作符,前一个命令成功才执行下一个
  • yarn install --frozen-lockfile: 安装依赖,不更新 lockfile(保证依赖版本一致)
  • yarn build:sit: 执行 sit 环境的构建命令
  • tar zcvf app-dom_front_sit.tar.gz dist: 将 dist 目录打包成压缩文件
    • z: 使用 gzip 压缩
    • c: 创建归档文件
    • v: 显示详细过程
    • f: 指定文件名

第 19 行:输出完成信息

echo "编译打包过程结束"

说明: 输出完成提示信息,表示整个编译打包流程已结束。


执行环境说明

脚本执行层次

CODING 构建机器(宿主机)
├── Shell 脚本在这里执行
├── 代码目录(从 Git clone 过来)
└── 启动 Docker 容器
    └── 容器内执行编译命令

执行流程

  1. CODING 分配构建机器

    • 构建机器上已安装 Docker
    • 代码自动从 Git 仓库 clone 到工作目录
  2. Shell 脚本在宿主机上运行

    • 获取路径、准备缓存目录
    • 根据条件清除旧缓存
  3. 启动 Docker 容器

    • 容器提供隔离的、统一的编译环境
    • 通过卷挂载访问宿主机上的代码和缓存
  4. 容器内执行编译

    • 安装依赖、构建项目、打包产物
    • 产物保存在挂载的宿主机目录中

为什么这样设计?

层级 作用 优势
宿主机 运行脚本、管理资源、缓存 持久化缓存、灵活控制
Docker 容器 提供编译环境 环境一致性、版本锁定、隔离性

学习 Shell 脚本编写

1. 基础知识学习路径

  • 变量和引用: 变量定义、引用($VAR)、命令替换(`cmd`$(cmd)
  • 条件判断: if-then-else、测试表达式 [ ][[ ]]
  • 循环结构: forwhileuntil
  • 函数定义: 如何封装可重用代码
  • 输入输出: echoread、重定向(>>><)、管道(|

2. 常用命令掌握

文件操作:

  • ls - 列出目录内容
  • cd - 切换目录
  • mkdir - 创建目录
  • rm - 删除文件/目录
  • cp - 复制文件
  • mv - 移动/重命名文件

文本处理:

  • grep - 搜索文本
  • sed - 流编辑器
  • awk - 文本分析工具
  • cut - 剪切文本列
  • sort - 排序

系统信息:

  • pwd - 显示当前目录
  • whoami - 显示当前用户
  • ps - 进程状态
  • df - 磁盘使用情况
  • du - 目录空间使用

3. 实践建议

  1. 从简单脚本开始: 写一些自动化日常任务的小脚本
  2. 阅读现有脚本: 分析项目中的脚本(就像这个)
  3. 使用 shellcheck: 静态分析工具,帮助发现错误和不规范写法
  4. 学习最佳实践:
    • 变量加引号("$VAR")防止空格问题
    • 使用 set -e 遇错即停
    • 添加注释说明脚本功能
    • 检查命令执行结果

4. 推荐学习资源

  • 在线教程: Shell 脚本编程30分钟入门
  • 书籍: 《Linux命令行与shell脚本编程大全》
  • 实践平台: 在自己的项目中逐步应用
  • 工具: 使用 VSCode + ShellCheck 插件

5. 针对这个脚本的学习点

  • Docker 命令的使用: 理解容器化构建的优势
  • CI/CD 环境变量的应用: 如何在构建流程中传递配置
  • 缓存策略的实现: 优化构建速度的关键技术
  • 构建流程的自动化: 从代码到产物的完整流程

常见问题

Q: 为什么要用 Docker 容器编译?

A: 保证编译环境的一致性,避免"在我机器上可以运行"的问题。容器提供了固定版本的 Node.js、系统库等依赖。

Q: 缓存机制如何工作?

A: 通过挂载宿主机的缓存目录到容器,node_modules 和 yarn 缓存可以在多次构建之间复用,大幅提升构建速度。

Q: --frozen-lockfile 有什么作用?

A: 确保安装的依赖版本与 yarn.lock 文件完全一致,不会自动更新依赖版本,保证构建的可重复性。

Q: 如何调试这个脚本?

A: 可以在脚本中添加 set -x 开启调试模式,或添加更多 echo 语句输出关键变量值。

工作中的Ai工具汇总

背景

生活在AI的今天,coding可选择提效的大模型有很多,用对了事半功倍,下面分场景介绍下目前工作中的提效工具

vscode插件形式

GitHub Copilot

image.png

image.png

类别 具体内容
优点 1. 代码生成能力强:2. 多语言支持:覆盖 100 多种编程语言3. IDE 集成完善:支持 VS Code、等主流开发工具,无缝融入开发环境4. 提高开发效率:减少重复性代码编写,节省时间,提升编码速度
缺点 1. 存在隐私风险:2.需要翻墙 3.免费版本效率慢且无法使用calude模型

Lingma - Alibaba Cloud AI Coding Assistant

image.png

类别 具体内容
优点 1. 对中文支持更优:针对中文注释、中文语境的理解更精准,符合国内内开发者使用习惯 2. 本地化服务:无需翻墙即可使用,3.提供免费版本:基础功能免费
缺点 1.训练数据中开源代码占比相对较少,对部分国际主流框架的支持精度略逊 2.高级功能需订阅付费,

除此之外 你可以使用 百度等推出的vscode插件,对于问答形式,agent模型均较好的支持

cursor trae 等AI编辑器

image.png

Cursor是一款专注于 AI 辅助编程的编辑器,优势在于全局上下文和大项目支持

类别 具体内容
优点 1. 深度 AI 集成:作为原生 AI 驱动的编辑器,2.AI 功能与编辑体验深度融合,支持实时代码生成、重构建议和上下文对话式编程,交互流畅度高 2上下文理解强:能更好地结合整个项目文件结构和代码上下文生成建议 3.多模型支持
缺点 1.需要付费 且需要翻墙 2.非付费效率会排队较慢

deepwiki 辅助文档

DeepWiki 是由 Cognition Labs 基于其产品 Devin 开发的一款 AI 驱动的 GitHub 源码阅读与分析工具。它旨在帮助开发者更高效地阅读、理解和分析 GitHub 上的源码

使用方式如下 直接找到github地址练级 比如 https://github.com/vuejs/vue 直接将 github 替换为 deepwiki 比如 https://deepwiki.com/vuejs/vue 即可生成文档,可以当作一个rag进行提问,对于你了解项目 或者深入细节有很大提速

image.png

image.png

UI设计图转前端代码 v0

v0.app 是 Vercel 推出的一款 AI 驱动的低代码开发工具,核心是通过自然语言 prompt 快速生成可直接使用的网页界面,无需手动编写代码。

尝试了很多的图片转代码的工具 v0的效果是不错的 尤其是于tailwindcss 和 react的代码生成,

我常用的 prompt如下

你是前端开发工程师,你擅长于react 技术栈,且很擅长tailwindcss原子化css,请基于我给你的图片,生成前端代码

image.png

image.png

生成的效果如下,大致文档结构是ok的,细节处需要自己优化,对于付费模型 可以传入figma设计图地址,效果应该更好,我这里没有尝试付费

image.png

国内编程大模型地址

豆包www.doubao.com/

通义灵码lingma.aliyun.com/

deepseekdeepseek.com/

自测豆包对于AI代码编程支持较好,泛化能力也强,支持你出传入链接、图片、github仓库、代码文件夹等,对于基础代码建议使用豆包就可以

国外编程大模型推荐

claudeclaude.ai/new

geminigemini.google.com/

chatgptchatgpt.com/

国外这些大模型整体效果更高一点,尤其是 claude对于完整代码库、长文档解析,适合复杂项目的代码逻辑梳理 GitHub Copilot cursor 付费模式均支持 claude大模型,生成效果比较好

gemini的多模态生成比较好,尤其在 “根据截图生成前端代码”“识别图表并生成数据分析代码” 等场景表现突出,当然你得 图生代码完全可以交给他来搞

image.png

结尾

以上就用AI提效的流程汇总,可酌情取用

react项目开发—关于代码架构/规范探讨

社区一直讨论的一个主题,到底是react好,还是vue好?

我的答案

本人对这个问题的答案是这样的:

1、react给了我们更大的自由度,我们可以以任意的方式,组建我们的代码结构,我们可以操控的代码细节更多,也就能在更多的细节上面,对我们的代码进行更细致的优化。

2、react在书写的过程中,驱动我们对数据、UI有更清晰的认知。我们必须对他们的运行细节,ui变化,数据流向,有明确的认知边界,才能对我们的项目,进行清晰的掌控。

3、vue2,它是死板的,明确的定义了数据的申明在哪里,生命周期在哪里书写,函数和事件在哪里书写,我们几乎很少有可发挥的空间。

4、vue2,数据的双向绑定,让我们可以忽视数据内部真正的变化边界,我们需要的时候,直接无脑赋值就能得到我们想要的结果。

导致的结果

这是这两个框架,不同的api,给我们的客观印象。同时因为团队的差异,它们又导致了一些问题。

1、react框架的项目开发,它注重细节,注重每一个数据的驱动,这就导致了,用这个框架的前端团队必定要有一定的极客精神。它的过度自由,导致了我们甚至可以随意定义我们数据的管理规范、组件的划分、甚至任意的代码结构。

但是,这正是一个团队最可怕的东西。它导致了不可控。

2、vue2的项目开发,它是简单的,容易上手的,并且它就是一个固定的写法规范。

  • 它的vue文件,就是由标准的html、js、css三部分构成。
  • js里面,数据定义,生命周期,函数和事件,都有固定的地方。

这是vue2作为一个框架的缺点,代码可以控制的比react少,书写代码的细节上,性能可控性小。但是,作为团队视角,它提供了一个团队最重要的东西——相对简单、相对可控。

React项目,我们可以从哪些方面提升代码的水准

评价一个项目的代码水平到底好不好,有这么几个方面。

1、代码性能和质量。

代码性能和质量,有些是项目工程层面的内容,有些是代码实现方面的内容。

项目工程层面:代码包的体积大小、首次加载的效率、首屏渲染时间、用户可操作时间。

代码实现方面:渲染效率、是否卡顿、大数据量的处理、虚拟列表、图片加载、分块渲染等等。

2、代码整体的层级划分

目前,现阶段的大多数前端,能做到的基本的目录结构划分,也就是我们的src下面,有这么几个主体目录:

  • index.js/index.tsx,整个应用的入口文件。
  • router,整个应用的路由配置文件。
  • pages,整个应用的页面层级组件,一般router中的一个path,就对应这里的一个组件。
  • components,全局的基础组件。
  • service,api请求相关的服务封装。
  • redux,整个页面的数据管理存储。
  • utils,全局某些通用能力/配置,放置在里面。
  • assets,全局资源文件(图片资源、静态js库、svg、字体文件等等)

以上基本上是每一个前端工程的共识,但是除此之外,我们应该还有其他的共识。

1、页面pages,尽可能的组装业务,进行集中的资源调度。

也就是,我们尽可能的把业务相关的东西,都往pages层次的组件进行集成。

当我们从路由中,得到这路由对应的pages,往往预示着,它是一块相对独立的业务。比如:文件列表页、文件详情页等等。而对于人类而言,在一处地方看代码,比在多处看代码,更容易。

2、页面pages,尽可能的进行统一的数据管理。

在pages层面,进行统一的数据管理,数据管理往往是这么几种情况,从redux接入全局数据进行管理,向pages调度的子组件,通过props传递数据,或者针对Provider全局数据的注入的使用。(至于为什么,可以继续看下面数据管理部分)

3、页面pages,在拆分组件的时候,层级不应该过于多。

在pages的组件层面,很有可能,我们会遇到非常复杂的业务,导致我们的pages层面的组件,变得非常臃肿,这个时候,我们需要进行组件拆分,但是我建议,再只多拆分1层业务组件,不要拆分多个不同层级的组件。组件的层级过多,数据通信的复杂度就会提升,代码的可读性就会降低,如果这个时候,再配合不好的数据管理习惯,屎山代码,就已经形成了。(具体是为什么,可以继续看下面的组件拆分部分)

4、 每一个单元应具备原子性

pages层面,调度的每一个单元,应该具备一定的特性—原子性。

一个函数,只完成一件事情,比如事件响应,数据处理。

一个组件,它是纯粹的,它只和传递给它的props有关,和其他无关。

一个业务组件,也就是在pages过于复杂的情况下拆分出去的组件,虽然它具备业务属性,但是对于pages来说,它也是相对独立的。

3、组件的层级划分

个人比较推崇的组件层级划分方式:

  • pages层,pages层很简单,就是代表页面的意思,每一个路由path,它都对应一个page。
  • 业务组件层,一个page,很有可能很复杂,我们需要一定的设计,把这个页面分成几个块,然后由pages统一调度,实现我们的业务。
  • components层,也就是纯粹的UI组件,它只和props有关系,和其他无关。全局任意的地方,都可以调度。

为什么这么拆分呢?其实这是这么多年,针对真实业务场景,综合思考下来,得到的比较好的实践方式,形成这个组件划分的原则,是基于以下几个方面的思考。

1、辨识度高。

每一个路由,对应一个pages组件。

每一个components中的组件,都有与业务无关的纯UI组件。

根据业务的复杂度,中间产生了一层业务组件。

每一种组件,各司其职,边界清晰,方便我们看到一个组件,就知道这个组件是干嘛的的一种标识。

pages层的组件,进行统一的调度,数据管理,对接redux,组件拼接,事件交互等等。一个文件中的代码,阅读起来,也更加的容易。

业务组件,当pages层的组件,过于复杂的时候,我们把业务相对独立的单元,拆分出来,形成我们的业务组件。业务组件是可以对接redux,也可以有自己的数据状态,各种事件交互。(业务组件的核心,在于如何巧妙的进行边界设计,具体请参考下面的业务组件层的拆分思路)

components层的UI组件,纯粹的UI组件,与业务无关,在全局可复用。

2、结合业务拆分、代码可读性、复用性的一个综合结果。

多数情况下,我们可能没有一个清晰的思路,去做组件的划分工作。

遇到复杂的业务,我们本能的就进行组件的拆分,当时写的时候,没有考虑太多,但是写到一半,会发现,这个组件的变动,可能会引发其他组件的数据变动,我们就遇水搭桥,见招拆招,有些用redux解决,有些用props父子传递数据进行通信。

保持这样的习惯,我们可能拆分一个又一个组件,props传递了一层又一层,等过一段时间一看,自己都看不懂,自己写的代码是啥。

这是本能的,不想思考的,总想简单化的把项目完成。但是往往导致的结果是:本来简单的项目,代码写的越来越复杂!!

pages层面,负责数据的整体管理,组件调度,那么我们的通信就会比较方便,相当于pages层就是这个页面相关的业务的通信中心,我们基本上,能够通过props父子组件传递消息。

props父子组件,传递消息,注定了组件的层级不能过多,过多就会导致,数据就像套娃一样,一层又一层,导致可读性降低。

业务组件的出现,是为了解决,过于复杂的页面业务,我们可能需要进行拆分,把能够单独拆分出去的结构,独立出来,这样不仅从业务上进行了模块的拆分,也能提高不同模块的代码可读性。

那么,业务组件层的拆分思路是什么呢?

3、业务组件层的拆分思路

其实原则上,这里需要我们进行深入思考,哪些模块是独立的单元?页面中的哪些模块,和业务主体通信较少?

这其实就是核心,代码倒逼我们进行设计,进行深入思考,进行深入的业务理解。

思考点1:某一个模块,是不是相对独立的UI模块?

思考点2:某个模块是不是单独的业务单元?

思考点3:某个模块拆分出去,通信的代价到底大不大?

思考清楚这些,其实本质上,你思考的是,你的代码架构问题,你以什么样的视角,来解读你的UI、数据、业务的关联关系。

有了这些思考,你一定可以拆分相对合理的业务组件层。

4、数据管理的划分

多数前端,写代码的时候,并不会有数据中心,数据流向这些概念。

很多人还停留在,完成UI,渲染数据的层面。

如果是这样的思路,无论是组件划分,还是数据管理,注定做的一塌糊涂,代码成屎山是必然的。

组件划分的思路,其实也是一种数据管理的思路。

1、pages层,从天然的业务视角来看,他天然就是一块业务的集合,所以pages层作为一块单独的业务数据中心,它天然合适。

2、components层,我们定义了它,只和props相关,和其他无关,它被其他组件调度,天生注定了它通过props传递数据的行为模式。

3、业务层组件,我们定义了它是pages下面的一块单独业务,我们有必要对它进行合理的设计,它与pages组件的关系,是主模块与子模块的关系。可以通过props通信,也可以接入redux通信。

4、redux,大多数情况下,我们其实不需要用它,当数据的通信,不满足业务场景的时候,redux就是我们的解决方案。它真正的业务价值,在于跨pages的通信。比如,某个页面状态的更改,另一个页面状态,也跟着更改。

redux另一种常见用法:

redux的特性,在umi或者其他框架中,把redux作为数据管理中心来使用,所有的页面状态,业务相关的数据,都定义在redux中,所有的接口请求,用户行为,都是通过redux的dispath进行触发行为,来更改数据,数据通过props流入各个组件中。

这种方式,其实也是各种推崇的一种方式,但是这种方式对人的要求也高,表明了我们团队中的每一个人,都要熟悉并且接受这种数据管理的方式,才能写出相对一致,可读性高的代码。

只要其中的一部分人,不接受这样的数据管理方式,代码的管理它就会变得混乱,有些初始化数据,可能在pages组件中,有些可能发生在redux层,作为阅读者,很难排查代码执行的路径。

现实场景说明:

我们大多数情况下,对于redux的使用,并不清晰,redux的使用,是混乱的。

本不必要的数据,可能放在redux中管理。

本不必须接入redux的组件,非要接入redux。

redux中的dispath,调用的地方,千奇百怪。

数据的流向定义,完全杂乱无章。

这些东西,对于一个项目,都是灾难性的影响,它会导致,我们代码的迭代难度,急剧上升。

Vue 3 的组合式 API和传统选项式 API区别(vue2转vue3,两者差异)

选项式 API vs 组合式 API 深度对比

除了写法不同,选项式 API 和组合式 API 在设计理念、逻辑组织、类型支持、复用能力等方面都有本质区别。

1. 设计理念和思维模式的不同

选项式 API:基于"选项"的分类思维

// 选项式 API - 按功能类型分类
export default {
  // 数据相关放一起
  data() {
    return {
      users: [],
      loading: false,
      searchQuery: ''
    }
  },
  
  // 计算属性放一起
  computed: {
    filteredUsers() {
      return this.users.filter(user => 
        user.name.includes(this.searchQuery)
      )
    }
  },
  
  // 方法放一起
  methods: {
    async fetchUsers() {
      this.loading = true
      this.users = await api.getUsers()
      this.loading = false
    },
    
    updateQuery(query) {
      this.searchQuery = query
    }
  },
  
  // 生命周期放一起
  mounted() {
    this.fetchUsers()
  }
}

组合式 API:基于"逻辑关注点"的聚合思维

<script setup>
import { ref, computed, onMounted } from 'vue'

// 用户搜索逻辑 - 相关的代码放在一起
const searchQuery = ref('')
const users = ref([])
const loading = ref(false)

const filteredUsers = computed(() => {
  return users.value.filter(user => 
    user.name.includes(searchQuery.value)
  )
})

const fetchUsers = async () => {
  loading.value = true
  users.value = await api.getUsers()
  loading.value = false
}

const updateQuery = (query) => {
  searchQuery.value = query
}

// 生命周期也跟相关逻辑放在一起
onMounted(() => {
  fetchUsers()
})

// 另一个独立的逻辑关注点可以放在下面
const otherFeature = () => {
  // 相关状态和方法都在一起
}
</script>

2. 逻辑组织和复用能力的本质区别

选项式 API 的逻辑复用问题

// mixins/userMixin.js - 混入方式(容易冲突)
export default {
  data() {
    return {
      users: [],
      userLoading: false
    }
  },
  methods: {
    async fetchUsers() {
      this.userLoading = true
      this.users = await api.getUsers()
      this.userLoading = false
    }
  },
  mounted() {
    this.fetchUsers()
  }
}

// ComponentA.vue - 使用混入
export default {
  mixins: [userMixin],
  data() {
    return {
      // 可能跟混入中的 users 冲突
      products: [] 
    }
  },
  // 逻辑分散在不同选项中,难以追踪
}

组合式 API 的逻辑复用优势

// composables/useUsers.js - 组合式函数
export function useUsers() {
  const users = ref([])
  const loading = ref(false)
  
  const fetchUsers = async () => {
    loading.value = true
    users.value = await api.getUsers()
    loading.value = false
  }
  
  onMounted(() => {
    fetchUsers()
  })
  
  return {
    users,
    loading,
    fetchUsers
  }
}

// ComponentA.vue - 使用组合式函数
<script setup>
import { useUsers } from '@/composables/useUsers'

const { users, loading, fetchUsers } = useUsers()

// 可以同时使用多个组合式函数,不会冲突
const { products, fetchProducts } = useProducts()
</script>

3. 响应式系统的使用差异

选项式 API 的响应式

export default {
  data() {
    return {
      user: {
        name: 'John',
        profile: {
          age: 25
        }
      },
      items: [1, 2, 3]
    }
  },
  methods: {
    updateUser() {
      // Vue 2 中需要特殊处理数组和对象
      this.user.profile.age = 26 // 响应式更新
      this.items[0] = 999 // Vue 2 中不是响应式的!
      
      // Vue 2 的正确做法
      this.$set(this.items, 0, 999)
      this.items.splice(0, 1, 999)
    }
  }
}

组合式 API 的响应式

<script setup>
import { ref, reactive } from 'vue'

const user = reactive({
  name: 'John',
  profile: {
    age: 25
  }
})

const items = ref([1, 2, 3])

const updateUser = () => {
  user.profile.age = 26 // 响应式更新
  items.value[0] = 999 // 响应式更新
  
  // 更灵活的响应式操作
  const newItem = ref(100)
  items.value.push(newItem.value)
}
</script>

4. TypeScript 支持程度的巨大差异

选项式 API 的 TypeScript 支持有限

import { defineComponent } from 'vue'

export default defineComponent({
  data() {
    return {
      count: 0, // 类型可以推断为 number
      user: null as User | null // 需要类型断言
    }
  },
  
  computed: {
    // 计算属性的类型声明比较麻烦
    doubleCount(): number {
      return this.count * 2
    }
  },
  
  methods: {
    // 方法参数和返回值的类型声明
    updateUser(user: User): void {
      this.user = user
    }
  },
  
  // 生命周期钩子没有很好的类型提示
  mounted() {
    // this.$ 上的属性类型支持有限
  }
})

组合式 API 的完整 TypeScript 支持

<script setup lang="ts">
import { ref, computed } from 'vue'

interface User {
  id: number
  name: string
  email: string
}

// 完整的类型推断
const count = ref(0) // 自动推断为 Ref<number>
const user = ref<User | null>(null) // 明确的类型

// 计算属性的完整类型支持
const doubleCount = computed(() => count.value * 2) // 自动推断为 ComputedRef<number>

// 函数的完整类型支持
const updateUser = (newUser: User) => {
  user.value = newUser
}

// 自动补全和类型检查
user.value?.name // 完整的智能提示
</script>

5. 代码组织和维护性的对比

复杂组件在选项式 API 中的问题

export default {
  data() {
    return {
      // 多个功能的变量混在一起
      users: [],
      products: [], 
      orders: [],
      userLoading: false,
      productLoading: false,
      orderLoading: false,
      searchQuery: '',
      filterStatus: '',
      pagination: { page: 1, limit: 20 }
    }
  },
  
  computed: {
    // 多个功能的计算属性混在一起
    filteredUsers() { /* ... */ },
    filteredProducts() { /* ... */ },
    filteredOrders() { /* ... */ },
    paginatedUsers() { /* ... */ },
    paginatedProducts() { /* ... */ }
  },
  
  methods: {
    // 多个功能的方法混在一起
    fetchUsers() { /* ... */ },
    fetchProducts() { /* ... */ },
    fetchOrders() { /* ... */ },
    searchUsers() { /* ... */ },
    searchProducts() { /* ... */ }
  },
  
  mounted() {
    // 多个功能的初始化混在一起
    this.fetchUsers()
    this.fetchProducts()
    this.fetchOrders()
  }
}

组合式 API 的逻辑分离优势

<script setup>
import { useUsers } from './composables/useUsers'
import { useProducts } from './composables/useProducts'
import { useOrders } from './composables/useOrders'

// 每个功能独立,清晰分离
//从 `useUsers` 函数的返回值中提取出 `users`、`userLoading`、`filteredUsers`、`fetchUsers`、`searchUsers` 这些属性
const {
  users,
  userLoading,
  filteredUsers,
  fetchUsers,
  searchUsers
} = useUsers()

const {
  products,
  productLoading, 
  filteredProducts,
  fetchProducts,
  searchProducts
} = useProducts()

const {
  orders,
  orderLoading,
  filteredOrders, 
  fetchOrders
} = useOrders()

// 初始化各个功能
onMounted(() => {
  fetchUsers()
  fetchProducts()
  fetchOrders()
})
</script>

6. 学习曲线和心智模型

选项式 API 的学习曲线

// 相对平缓,符合传统 OOP 思维
export default {
  props: ['message'],     // 输入
  data() {               // 状态
    return { count: 0 }
  },
  computed: {            // 派生状态
    double() { return this.count * 2 }
  },
  methods: {             // 方法
    increment() { this.count++ }
  },
  watch: {               // 副作用
    count(newVal) { console.log(newVal) }
  },
  mounted() {            // 生命周期
    console.log('组件挂载')
  }
}

组合式 API 的学习曲线

<script setup>
import { ref, computed, watch, onMounted } from 'vue'

// 需要理解响应式基础
const count = ref(0)
const double = computed(() => count.value * 2)

// 需要理解作用域和闭包
const increment = () => {
  count.value++
}

// 需要理解生命周期注册
onMounted(() => {
  console.log('组件挂载')
})

// 需要理解侦听器机制
watch(count, (newVal) => {
  console.log(newVal)
})
</script>

7. 性能优化的差异

选项式 API 的性能优化

export default {
  data() {
    return {
      largeList: [] // 整个组件重新渲染
    }
  },
  methods: {
    updateItem(index, newValue) {
      // 需要特殊优化手段
      this.$set(this.largeList, index, newValue)
    }
  }
}

组合式 API 的性能优化

<script setup>
import { ref, shallowRef, markRaw } from 'vue'

// 更细粒度的响应式控制
const largeList = shallowRef([]) // 浅层响应式
const heavyObject = markRaw({     // 非响应式
  // 大型静态数据
})

// 更精确的更新控制
const updateItem = (index, newValue) => {
  const newList = [...largeList.value]
  newList[index] = newValue
  largeList.value = newList
}
</script>

8. 与第三方库集成的差异

选项式 API 的集成

import { mapState, mapActions } from 'vuex'

export default {
  computed: {
    ...mapState(['user', 'settings']),
    localComputed() { /* ... */ }
  },
  methods: {
    ...mapActions(['login', 'logout']),
    localMethod() { /* ... */ }
  }
  // 混合了 Vuex 和本地逻辑,难以区分
}

组合式 API 的集成

<script setup>
import { useStore } from 'vuex'
import { useLocalLogic } from './composables/useLocalLogic'

// 清晰的分离
const store = useStore()
const user = computed(() => store.state.user)
const login = () => store.dispatch('login')

// 本地逻辑
const { localData, localMethod } = useLocalLogic()
</script>

9. 实际项目中的选择建议

适合选项式 API 的场景

// 1. 简单的展示组件
export default {
  props: ['title', 'content'],
  template: `
    <div class="card">
      <h3>{{ title }}</h3>
      <p>{{ content }}</p>
    </div>
  `
}

// 2. 迁移 Vue 2 项目
// 3. 团队对选项式 API 更熟悉
// 4. 不需要复杂逻辑复用

适合组合式 API 的场景

<script setup>
// 1. 复杂的业务组件
const { 
  data, 
  pagination, 
  filters, 
  search, 
  loadData 
} = useDataTable()

const { 
  form, 
  validation, 
  submit, 
  reset 
} = useFormHandler()

// 2. 需要逻辑复用的场景
// 3. TypeScript 项目
// 4. 大型应用开发
</script>

总结

特性 选项式 API 组合式 API
设计理念 按选项分类 按逻辑关注点聚合
逻辑复用 Mixins(有冲突风险) 组合式函数(无冲突)
TypeScript 有限支持 完整支持
代码组织 功能分散在不同选项 相关逻辑集中
响应式 自动处理,但有限制 显式声明,更灵活
学习曲线 平缓 较陡峭
性能优化 组件级别 更细粒度控制
适用场景 简单组件、Vue 2 迁移 复杂组件、大型项目

核心区别: 选项式 API 是"怎么做",组合式 API 是"做什么"。选择哪种取决于项目复杂度、团队习惯和具体需求。

【vue篇】Vue 模板编译原理:从 Template 到 DOM 的翻译官

在 Vue 项目中,你写的:

<template>
  <div class="user" v-if="loggedIn">
    Hello, {{ name }}!
  </div>
</template>

最终变成了浏览器能执行的 JavaScript 函数。
这背后,就是 Vue 模板编译器 在默默工作。

本文将深入解析 Vue 模板编译的三大核心阶段parseoptimizegenerate,带你揭开 .vue 文件如何变成可执行代码的神秘面纱。


一、为什么需要模板编译?

🎯 浏览器不认识 <template>

<!-- 你写的 -->
<template>
  <div v-if="user.loggedIn">{{ user.name }}</div>
</template>

<!-- 浏览器看到的 -->
Unknown tag: template → 忽略 or 报错

✅ 解决方案:编译成 render 函数

// 编译后生成的 render 函数
render(h) {
  return this.user.loggedIn 
    ? h('div', { class: 'user' }, `Hello, ${this.user.name}!`)
    : null;
}

💡 render 函数返回的是 虚拟 DOM (VNode),Vue 拿它来高效更新真实 DOM。


二、模板编译三部曲

Template String 
     ↓ parse
   AST (抽象语法树)
     ↓ optimize
   优化后的 AST
     ↓ generate
   Render Function

第一步:🔍 解析(Parse)—— 构建 AST

目标:将 HTML 字符串转为 AST(Abstract Syntax Tree)

示例输入:

<div id="app" class="container">
  <p v-if="show">Hello {{ name }}</p>
</div>

输出 AST 结构:

{
  "type": 1,
  "tag": "div",
  "attrsList": [...],
  "children": [
    {
      "type": 1,
      "tag": "p",
      "if": "show",           // 指令被解析
      "children": [
        {
          "type": 3,
          "text": "Hello ",
          "static": false
        },
        {
          "type": 2,
          "expression": "_s(name)",  // {{ name }} 被编译
          "text": "{{ name }}"
        }
      ]
    }
  ]
}

🛠️ 如何实现?正则 + 状态机

编译器使用多个正则表达式匹配:

匹配内容 正则示例
标签开始 /<([^\s>/]+)/
属性 /(\w+)(?:=)(?:"([^"]*)")/
插值表达式 /{{\s*([\s\S]*?)\s*}}/
指令 /v-(\w+):?(\w*)/?

⚠️ 注意:Vue 的 parser 是一个递归下降解析器,比简单正则复杂得多,但原理类似。


第二步:⚡ 优化(Optimize)—— 标记静态节点

目标:提升运行时性能,跳过不必要的 diff

什么是静态节点?

  • 不包含动态绑定;
  • 内容不会改变;
  • 如:<p>纯文本</p><img src="/logo.png">

优化过程:

  1. 遍历 AST,标记静态根节点和静态子节点;
  2. 添加 static: truestaticRoot: true 标志。
{
  "tag": "p",
  "static": true,
  "staticRoot": true,
  "children": [
    { "type": 3, "text": "这是静态文本", "static": true }
  ]
}

运行时收益:

// patch 过程中
if (vnode.static && oldVnode.static) {
  // 直接复用,跳过 diff!
  vnode.componentInstance = oldVnode.componentInstance;
  return;
}

💥 对于大量静态内容(如文档页面),性能提升可达 30%+


第三步:🎯 生成(Generate)—— 输出 render 函数

目标:将优化后的 AST 转为可执行的 render 函数字符串

输入:优化后的 AST

输出:JavaScript 代码字符串

with(this) {
  return _c('div',
    { attrs: { "id": "app", "class": "container" } },
    [ (show) ?
      _c('p', [_v("Hello "+_s(name))]) :
      _e()
    ]
  )
}

🔤 代码生成规则

AST 节点 生成代码
元素标签 _c(tag, data, children)
文本节点 _v(text)
表达式 {{ }} _s(expression)
条件渲染 v-if (condition) ? renderTrue : renderFalse
静态节点 _m(index)(从 $options.staticRenderFns 中取)

💡 _c = createElement, _v = createTextVNode, _s = toString


三、完整流程图解

          Template
             │
             ▼
       [ HTML Parser ]
             │
             ▼
         AST (未优化)
             │
             ▼
      [ 静态节点检测与标记 ]
             │
             ▼
         AST (已优化)
             │
             ▼
     [ Codegen (生成器) ]
             │
             ▼
     Render Function String
             │
             ▼
     new Function(renderStr)
             │
             ▼
       可执行的 render()
             │
             ▼
        Virtual DOM
             │
             ▼
        Real DOM (渲染)

四、Vue 2 vs Vue 3 编译器对比

特性 Vue 2 Vue 3
编译目标 render 函数 render 函数
模板语法限制 较多(如必须单根) 更灵活(Fragment 支持多根)
静态提升 ✅✅ 更强的 hoist 静态节点
Patch Flag 动态节点标记,diff 更快
编译时优化 基础静态标记 Tree-shaking 友好,死代码消除
源码位置 src/compiler/ @vue/compiler-dom

💥 Vue 3 的编译器更智能,生成的代码更小、更快。


五、手写一个极简模板编译器(玩具版)

function compile(template) {
  // Step 1: Parse (简化版)
  const tags = template.match(/<(\w+)[^>]*>(.*?)<\/\1>/);
  if (!tags) return;

  const tag = tags[1];
  const content = tags[2];

  // Step 2: Optimize (判断是否静态)
  const isStatic = !content.includes('{{');

  // Step 3: Generate
  const renderCode = `
    function render() {
      return ${isStatic 
        ? `_v("${content}")` 
        : `_c("${tag}", {}, [ _v( _s(${content.slice(2,-2)})) ])`
      };
    }
  `;

  return renderCode;
}

// 使用
const code = compile('<p>{{ msg }}</p>');
console.log(code);
// 输出:function render() { return _c("p", {}, [ _v( _s(msg)) ]); }

🎉 这就是一个最简化的“编译器”雏形!


💡 结语

“Vue 模板编译器,是连接声明式模板与命令式 DOM 操作的桥梁。”

阶段 作用 输出
Parse 解析 HTML 字符串 AST
Optimize 标记静态节点 优化后的 AST
Generate 生成 JS 代码 render 函数

掌握编译原理,你就能:

✅ 理解 Vue 模板的底层机制;
✅ 写出更高效的模板(减少动态绑定);
✅ 调试编译错误更得心应手;
✅ 为学习其他框架(React JSX)打下基础。

【vue篇】Vue Mixin:可复用功能的“乐高积木”

在开发多个 Vue 组件时,你是否遇到过这样的问题:

“这几个组件都有相同的 loading 逻辑,要复制粘贴?” “如何共享通用的错误处理方法?” “有没有像‘插件’一样的功能可以注入?”

答案就是:Mixin(混入)

本文将全面解析 Vue Mixin 的核心概念使用场景潜在风险


一、什么是 Mixin?

Mixin 是一个包含 Vue 组件选项的对象,可以被“混入”到多个组件中,实现功能复用。

🎯 核心价值

  • 代码复用:避免重复编写相同逻辑;
  • 逻辑分离:将通用功能(如 loading、权限)抽离;
  • 渐进增强:为组件动态添加功能。

二、快速上手:一个 Loading Mixin 示例

场景:多个组件需要“加载中”状态

Step 1:创建 loading.mixin.js

// mixins/loading.mixin.js
export const loadingMixin = {
  data() {
    return {
      loading: false,
      errorMessage: null
    };
  },

  methods: {
    async withLoading(asyncFn) {
      this.loading = true;
      this.errorMessage = null;
      try {
        await asyncFn();
      } catch (err) {
        this.errorMessage = err.message;
      } finally {
        this.loading = false;
      }
    }
  },

  // 生命周期钩子
  created() {
    console.log('【Mixin】组件创建,初始化 loading 状态');
  }
};

Step 2:在组件中使用

<!-- UserProfile.vue -->
<script>
import { loadingMixin } from '@/mixins/loading.mixin';

export default {
  mixins: [loadingMixin],

  async created() {
    // 使用 mixin 提供的方法
    await this.withLoading(() => this.fetchUser());
  },

  methods: {
    async fetchUser() {
      // 模拟 API 调用
      await new Promise(r => setTimeout(r, 1000));
      this.user = { name: 'Alice' };
    }
  }
};
</script>

<template>
  <div v-if="loading">加载中...</div>
  <div v-else-if="errorMessage">错误:{{ errorMessage }}</div>
  <div v-else>用户:{{ user.name }}</div>
</template>

✅ 效果:UserProfile 组件自动拥有了 loadingerrorMessagewithLoading 方法。


三、Mixin 合并规则:当名字冲突了怎么办?

当 Mixin 和组件定义了同名选项,Vue 会按规则合并:

选项类型 合并策略
data 函数返回对象合并(浅合并)
methods / computed / props 组件优先,Mixin 的会被覆盖
生命周期钩子 两者都执行,Mixin 的先执行
watch 同名 watcher 都会执行
computed 组件优先

🎯 生命周期执行顺序

const myMixin = {
  created() {
    console.log('1. Mixin created');
  }
};

export default {
  mixins: [myMixin],
  created() {
    console.log('2. Component created'); // 后执行
  }
}

输出:

1. Mixin created
2. Component created

💥 Mixin 的生命周期永远先于组件自身执行


四、实战应用场景

✅ 场景 1:表单验证逻辑复用

// mixins/validation.mixin.js
export const validationMixin = {
  data() {
    return {
      errors: {}
    };
  },
  methods: {
    validateEmail(email) {
      const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (!re.test(email)) {
        this.errors.email = '邮箱格式不正确';
      } else {
        delete this.errors.email;
      }
    }
  }
};

✅ 场景 2:权限控制

// mixins/permission.mixin.js
export const permissionMixin = {
  mounted() {
    if (!this.$store.getters.hasPermission(this.requiredPermission)) {
      this.$router.push('/403');
    }
  }
};

// 组件中
export default {
  mixins: [permissionMixin],
  data() {
    return {
      requiredPermission: 'user:edit'
    };
  }
};

✅ 场景 3:第三方 SDK 集成

// mixins/analytics.mixin.js
export const analyticsMixin = {
  mounted() {
    this.$analytics.pageView(); // 记录页面访问
  },
  methods: {
    trackEvent(event, props) {
      this.$analytics.track(event, props);
    }
  }
};

五、Mixin 的“黑暗面”:潜在问题

❌ 问题 1:命名冲突(Name Collision)

// mixin 定义了 fetchData
const apiMixin = {
  methods: {
    fetchData() { /* ... */ }
  }
};

// 组件也定义了 fetchData
export default {
  mixins: [apiMixin],
  methods: {
    fetchData() { /* 覆盖了 mixin 的方法!*/ }
  }
}

⚠️ 组件的方法会覆盖 Mixin 的,可能导致逻辑丢失。


❌ 问题 2:隐式依赖(Implicit Dependency)

// mixin 依赖组件必须提供 `userId`
const userMixin = {
  async created() {
    this.userData = await fetch(`/api/users/${this.userId}`);
  }
};

如果组件没有定义 userId,就会报错,但没有明显提示


❌ 问题 3:来源不清晰(Source Ambiguity)

<template>
  <!-- 这个 `loading` 是哪来的? -->
  <div v-if="loading">加载中...</div>
</template>

🔍 开发者无法从模板直接看出 loading 是来自 Mixin 还是组件自身。


六、Vue 3 的替代方案:Composition API

// composables/useLoading.js
import { ref } from 'vue';

export function useLoading() {
  const loading = ref(false);
  const errorMessage = ref(null);

  const withLoading = async (asyncFn) => {
    loading.value = true;
    errorMessage.value = null;
    try {
      await asyncFn();
    } catch (err) {
      errorMessage.value = err.message;
    } finally {
      loading.value = false;
    }
  };

  return { loading, errorMessage, withLoading };
}
<!-- UserProfile.vue -->
<script setup>
import { useLoading } from '@/composables/useLoading';

const { loading, withLoading } = useLoading();

async function loadUser() {
  await withLoading(fetchUser);
}
</script>

✅ Composition API 的优势:

特性 Mixin Composition API
命名冲突 ❌ 易发生 ✅ 通过解构重命名
源头追踪 ❌ 困难 useXxx() 清晰可见
类型推导 ❌ 弱 ✅ TypeScript 友好
逻辑复用 ✅ 更灵活

💡 结语

“Mixin 是一把双刃剑:用得好,提升效率;用不好,制造混乱。”

方案 适用场景
Mixin Vue 2 项目、简单逻辑复用
Composition API Vue 3 项目、复杂逻辑、TypeScript

🚀 最佳实践建议:

  1. 优先使用 Composition API(Vue 3);
  2. ✅ 如果用 Mixin,命名清晰(如 useLoadingMixin);
  3. ✅ 避免在 Mixin 中引入隐式依赖
  4. ✅ 文档化 Mixin 的输入/输出

掌握 Mixin,你就能写出更 DRY(Don't Repeat Yourself)的代码。

【vue篇】Vue 2 响应式“盲区”破解:如何监听对象/数组属性变化

在 Vue 开发中,你是否遇到过这样的诡异问题:

“我明明改了 this.user.name,为什么页面没更新?” “this.arr[0] = 'new',视图怎么不动?” “Vue 不是响应式的吗?”

本文将彻底解析 Vue 2 的响应式限制,并提供五种解决方案,让你彻底告别“数据变了,视图没变”的坑。


一、核心问题:Vue 2 的响应式“盲区”

🎯 为什么直接赋值不触发更新?

// ❌ 无效:视图不更新
this.user.name = 'John';      // 对象新增属性
this.users[0] = 'Alice';      // 数组索引赋值

🔍 根本原因:Object.defineProperty 的限制

Vue 2 使用 Object.defineProperty 拦截:

  • ✅ 能监听 已有属性的修改
  • 不能监听
    • 对象新增属性
    • 数组索引直接赋值arr[0] = x);
    • 数组长度修改arr.length = 0)。

💥 Vue 无法“感知”这些操作,所以不会触发视图更新。


二、解决方案:五种正确姿势

✅ 方案 1:this.$set() —— Vue 官方推荐

// ✅ 对象新增属性
this.$set(this.user, 'name', 'John');

// ✅ 数组索引赋值
this.$set(this.users, 0, 'Alice');

// ✅ 等价于
Vue.set(this.user, 'name', 'John');

🎯 this.$set 的内部原理

function $set(target, key, val) {
  // 1. 如果是数组 → 用 splice 触发响应式
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1, val);
    return val;
  }
  
  // 2. 如果是对象
  const ob = target.__ob__;
  if (key in target) {
    // 已有属性 → 直接赋值(已有 getter/setter)
    target[key] = val;
  } else {
    // 新增属性 → 动态添加响应式
    defineReactive(target, key, val);
    ob.dep.notify(); // 手动派发更新
  }
  return val;
}

💡 $set = 智能判断 + 自动响应式处理


✅ 方案 2:数组专用方法 —— splice

// ✅ 修改数组某一项
this.users.splice(0, 1, 'Alice'); // 索引0,删除1个,插入'Alice'

// ✅ 新增元素
this.users.splice(1, 0, 'Bob'); // 在索引1前插入

// ✅ 删除元素
this.users.splice(0, 1); // 删除第一项

🎯 为什么 splice 可以?

Vue 2 重写了数组的 7 个方法

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

// 重写后,调用这些方法时会:
// 1. 执行原生方法
// 2. dep.notify() → 触发视图更新

✅ 这些方法是“响应式安全”的。


✅ 方案 3:对象整体替换

// ✅ 对象新增属性
this.user = { ...this.user, name: 'John' };

// ✅ 或
this.user = Object.assign({}, this.user, { name: 'John' });
  • ✅ 原理:重新赋值 → 触发 setter → 视图更新;
  • ❌ 缺点:失去响应式连接(如果 user 被深层嵌套)。

✅ 方案 4:初始化时声明属性

data() {
  return {
    user: {
      name: '',    // 提前声明
      age: null,
      email: ''    // 避免运行时新增
    }
  };
}

💡 最佳实践:data 中定义所有可能用到的属性


✅ 方案 5:使用 Vue.observable + computed

const state = Vue.observable({
  user: { name: 'Tom' }
});

// 在组件中
computed: {
  userName() {
    return state.user.name; // 自动依赖收集
  }
}
  • ✅ 适合全局状态;
  • ❌ 不推荐用于组件局部状态。

三、Vue 3 的革命性改进:Proxy 无所不能

import { reactive } from 'vue';

const state = reactive({
  user: {},
  users: []
});

// ✅ Vue 3 中,以下操作全部响应式!
state.user.name = 'John';        // 新增属性
state.users[0] = 'Alice';        // 索引赋值
state.users.length = 0;          // 修改长度
delete state.user.name;          // 删除属性

💥 Vue 3 使用 Proxy,能拦截 getsetdeleteProperty 等所有操作,彻底解决 Vue 2 的响应式盲区。


四、最佳实践清单

场景 推荐方案
Vue 2:对象新增属性 this.$set(obj, key, val)
Vue 2:数组索引赋值 this.$set(arr, index, val)arr.splice(index, 1, val)
Vue 2:批量更新数组 splice / push / pop
Vue 2:避免问题 初始化时声明所有属性
Vue 3:任何操作 直接赋值,Proxy 全部拦截

五、常见误区

❌ 误区 1:this.$set 只用于对象

// ❌ 错误:认为数组不需要 $set
this.users[0] = 'new'; // 不响应

// ✅ 正确
this.$set(this.users, 0, 'new');

❌ 误区 2:pushsplice 更好

// ✅ `splice` 更通用
this.users.splice(1, 0, 'Bob'); // 在中间插入
this.users.push('Bob');         // 只能在末尾

✅ 推荐:splice 是数组操作的“瑞士军刀”


💡 结语

“在 Vue 2 中,永远不要直接操作数组索引或对象新增属性。”

方法 是否响应式 适用场景
obj.key = val (已有) 修改已有属性
obj.newKey = val $set
arr[i] = val $setsplice
this.$set() 通用解决方案
splice() 数组操作首选

掌握这些技巧,你就能:

✅ 避免响应式失效的 bug;
✅ 写出更可靠的 Vue 代码;
✅ 理解 Vue 响应式的核心原理。

【vue篇】Vue.delete vs delete:数组删除的“陷阱”与正确姿势

在 Vue 开发中,你是否遇到过这样的问题:

“用 delete 删除数组项,视图为什么没更新?” “Vue.delete 和原生 delete 有什么区别?” “如何安全地删除数组元素?”

本文将彻底解析 deleteVue.delete删除数组时的根本差异。


一、核心结论:一个“打洞”,一个“重排”

操作 结果 响应式 视图更新
delete arr[index] 元素变 empty长度不变 ❌ 不响应 ❌ 不更新
Vue.delete(arr, index) 直接删除,长度改变 ✅ 响应式 ✅ 自动更新

💥 delete 只是“打了个洞”,而 Vue.delete 是真正的“移除”。


二、实战演示:同一个操作,两种结果

场景:删除数组第二项

const vm = new Vue({
  data: {
    users: ['Alice', 'Bob', 'Charlie']
  }
});

方式一:delete(错误方式)

delete vm.users[1];
console.log(vm.users); 
// ['Alice', empty, 'Charlie'] → 长度仍为 3!
  • 内存中users[1] 变为 empty slot
  • DOM 中:视图不会更新

⚠️ 控制台警告:

[Vue warn]: A value is trying to be set on a non-existent property...

方式二:Vue.delete(正确方式)

Vue.delete(vm.users, 1);
// 或 this.$delete(vm.users, 1)
console.log(vm.users); 
// ['Alice', 'Charlie'] → 长度变为 2!

✅ 视图自动更新,完美!


三、深入原理:为什么 delete 不行?

🔍 1. delete 的本质

let arr = ['a', 'b', 'c'];
delete arr[1];

// 等价于
arr[1] = undefined; // ❌ 错误理解
// 实际是:
Object.defineProperty(arr, 1, { configurable: true });
delete arr[1]; // 移除属性,但保留索引“空位”
索引:   0     1     2
值:   'a'   empty  'c'
  • 数组长度 不变
  • for...in 会跳过 empty 项;
  • Array.prototype 方法(如 map, filter)会跳过 empty

🔍 2. Vue 响应式的限制

Vue 2 使用 Object.defineProperty 拦截:

  • ✅ 能监听 arr[1] = newValue(赋值);
  • 不能监听 delete arr[1](删除属性)

💡 Vue 无法检测到“属性被删除”,所以不会触发视图更新。


🔍 3. Vue.delete 的内部实现

Vue.delete = function (target, key) {
  // 1. 执行原生 delete
  delete target[key];
  
  // 2. 手动触发依赖更新
  if (target.__ob__) {
    target.__ob__.dep.notify(); // 强制通知 watcher
  }
}

Vue.delete = delete + 手动派发更新


四、其他删除数组的方法(推荐)

✅ 1. splice() —— 最常用

vm.users.splice(1, 1); // 从索引1开始,删除1个
// ['Alice', 'Charlie']
  • ✅ 响应式(Vue 重写了 splice);
  • ✅ 支持删除多个元素;
  • ✅ 返回被删除的元素。

✅ 2. filter() —— 函数式编程

vm.users = vm.users.filter((user, index) => index !== 1);
// 或根据条件删除
vm.users = vm.users.filter(user => user !== 'Bob');
  • ✅ 不修改原数组,返回新数组;
  • ✅ 适合复杂条件删除;
  • ✅ 响应式(因为重新赋值)。

✅ 3. slice() + 解构

vm.users = [
  ...vm.users.slice(0, 1),
  ...vm.users.slice(2)
]; // 删除索引1
  • ✅ 函数式,不可变数据;
  • ✅ 适合组合多个片段。

五、Vue 3 的改进:Proxy 无所不能

import { reactive } from 'vue';

const state = reactive({
  users: ['Alice', 'Bob', 'Charlie']
});

// Vue 3 中,delete 也能触发更新!
delete state.users[1]; // ✅ 视图自动更新

💥 Vue 3 使用 Proxy,能拦截 deleteProperty,因此原生 delete 也响应式!


六、最佳实践清单

场景 推荐方法
删除指定索引 splice(index, 1)
删除满足条件的元素 filter(condition)
需要兼容 Vue 2 Vue.delete(array, index)
Vue 3 项目 delete array[index]
性能敏感场景 splice(原地修改)

💡 结语

“在 Vue 2 中,永远不要用 delete 删除数组!”

方法 是否响应式 是否推荐
delete arr[i] ❌ 绝对避免
Vue.delete(arr, i) ✅ Vue 2 推荐
arr.splice(i, 1) ✅ 首选
arr.filter(...) ✅ 函数式首选

掌握这些删除技巧,你就能:

✅ 避免视图不更新的 bug;
✅ 写出更健壮的 Vue 代码;
✅ 顺利过渡到 Vue 3 的响应式系统。

【vue篇】Vue 项目中的静态资源管理:assets vs static 终极指南

在 Vue 项目中,你是否遇到过这样的困惑:

assetsstatic 文件夹有什么区别?” “图片到底该放哪个文件夹?” “为什么有的资源路径变了,有的没变?”

本文将彻底解析 assetsstatic核心差异使用场景最佳实践


一、核心结论:一句话总结

assets 走构建流程(可处理),static 直接拷贝(不处理)。

维度 assets static
是否参与构建 ✅ 是 ❌ 否
是否被 webpack 处理 ✅ 是 ❌ 否
是否支持模块化导入 ✅ 是 ❌ 否
是否会被重命名(hash) ✅ 是 ❌ 否
是否支持 Tree-shaking ✅ 是 ❌ 否

二、详细对比:从构建流程说起

🔄 1. assets:构建流程的“参与者”

src/assets/logo.png
     ↓
  webpack 处理
     ↓
  压缩、转 base64、生成 hash 名
     ↓
dist/static/img/logo.2f1f87g.png

assets 的特点:

  • 参与构建:被 webpack 处理;
  • 优化处理
    • 图片压缩(image-webpack-loader);
    • 小图转 base64(减少 HTTP 请求);
    • 文件名加 hash(缓存优化);
  • 支持模块化导入
import logo from '@/assets/logo.png';
console.log(logo); // /static/img/logo.abc123.png
  • 路径动态化:路径由构建工具生成,不可预测

🔄 2. static:构建流程的“旁观者”

static/favicon.ico
     ↓
  直接拷贝
     ↓
dist/favicon.ico

static 的特点:

  • 不参与构建:原封不动拷贝到 dist
  • 无优化:不压缩、不转码、不加 hash;
  • 路径固定:访问路径 = / + 文件名
  • 适合“即插即用”资源
<!-- 直接通过绝对路径访问 -->
<link rel="icon" href="/favicon.ico">
<script src="/js/third-party.js"></script>

三、实战演示:同一个图片的不同命运

场景:项目中使用 logo.png

方式一:放在 assets

<template>
  <img :src="logo" alt="Logo">
</template>

<script>
import logo from '@/assets/logo.png';
// logo = "/static/img/logo.abc123.png"
</script>

优势

  • 图片被压缩,体积更小;
  • 文件名加 hash,缓存友好;
  • 支持按需加载。

方式二:放在 static

<template>
  <img src="/static/logo.png" alt="Logo">
</template>

优势

  • 构建速度快(跳过处理);
  • 路径固定,适合第三方脚本引用。

劣势

  • 图片未压缩,体积大;
  • 无 hash,缓存更新困难。

四、何时使用 assets?何时使用 static

✅ 推荐使用 assets 的场景:

资源类型 示例
项目自用图片 logo、banner、icon
CSS/SCSS 文件 @import '@/assets/styles/main.scss'
字体文件 .woff, .ttf(可被 hash)
SVG 图标 可被 svg-sprite-loader 处理
需要按需引入的 JS 工具函数、配置文件

💡 原则:项目源码中直接引用的资源 → 放 assets


✅ 推荐使用 static 的场景:

资源类型 示例
第三方库 static/js/jquery.min.js
Favicon favicon.ico
Robots.txt SEO 爬虫规则
大型静态文件 PDF、视频(避免 webpack 处理)
CND 回退文件 当 CDN 失败时本地加载
<!-- 第三方库回退 -->
<script src="https://cdn.example.com/vue.js"></script>
<script>window.Vue || document.write('<script src="/static/js/vue.min.js"><\/script>')</script>

💡 原则:不希望被构建工具处理的资源 → 放 static


五、Vue CLI 项目结构示例

my-project/
├── public/               # Vue CLI 中 static 的新名字
│   ├── favicon.ico
│   ├── robots.txt
│   └── static/
│       └── js/
│           └── analytics.js
├── src/
│   ├── assets/           # 所有需要构建的资源
│   │   ├── images/
│   │   ├── fonts/
│   │   └── styles/
│   └── components/
└── package.json

⚠️ 注意:在 Vue CLI 3+ 中,static 文件夹已更名为 public


六、常见误区与最佳实践

❌ 误区 1:所有图片都放 static

<!-- 错误:大图未压缩,无 hash -->
<img src="/static/banner.jpg">

✅ 正确做法:

import banner from '@/assets/banner.jpg';
<img :src="banner">

❌ 误区 2:在 assets 中放第三方库

// ❌ 错误:第三方库应放 public
import 'jquery'; // 来自 node_modules 或 assets

✅ 正确做法:

<!-- 放 public,通过 script 标签引入 -->
<script src="/static/js/jquery.min.js"></script>

✅ 最佳实践清单

实践 说明
✅ 小图放 assets 转 base64,减少请求
✅ 大图放 assets 压缩,但不转 base64
✅ 第三方库放 public 避免重复打包
✅ 使用 require 动态加载 :src="require('@/assets/dynamic.png')"
✅ 配置 publicPath 部署到子目录时设置

💡 结语

assets 是你的‘智能资源库’,staticpublic)是你的‘原始文件仓库’。”

选择 使用场景
assets 项目源码引用、需要优化、支持 hash
static / public 第三方资源、固定路径、避免构建

掌握这一原则,你就能:

✅ 优化项目性能;
✅ 减少打包体积;
✅ 提升缓存效率;
✅ 避免资源加载错误。

Node.js + vue3 大文件-切片上传全流程(视频文件)

Node.js + vue3 大文件-切片上传全流程(视频文件)

这个业务场景是在参与一个AI智能混剪视频切片的项目中碰到的,当时的第一版需求是视频文件直接上传,当时是考虑到视频切片不会很大,就默认用户直接上传,但后续需求调整,切片时长扩大且画质也许会有所提高,导致文件会很大。解决方案考虑过是否可以通过压缩来解决,但混剪视频需求,用户是极其在意画质的,因此就放弃这种方案,只能选择市面通用的方案,切片上传。

功能简述

  1. 支持手动上传、拖动上传。

  2. 支持切片上传,且上传时带有进度条。

    切片格式限制:Mp4,大小限制: 20M

  3. 支持断点续传(后续再添加...)

服务端(node.js)

Install

pnpm install express multer fluent-ffmpeg body-parser cors fs-extra

环境配置:由于多个切片需要合并成一个视频,因此本地机器需要配置 ffmpeg

# 验证是否安装了 ffmpeg
ffmpeg -v

文件目录结构

your-project-name
├─ index.js
├─ cache
├─ output
├─ utils
│  ├─ multer.js
├─ public
├─ dist

multer 配置

const multer = require('multer')
const fse = require('fs-extra')

/**
 * multer 配置
 * @param { string } path 上传文件的目录
 * @param { function } fileFilter 文件过滤
 * @returns { multer } multer 实例
 */
module.exports = (path, fileFilter) => {
  /**
   * 上传文件的目录
   */
  const storage = (path) => {
    return multer.diskStorage({
      // 上传文件的目录
      destination: (req, file, cb) => {
        cb(null, path)
      },
      // 上传文件的名称
      filename: (req, file, cb) => {
        const fileName = Buffer.from(file.originalname, 'latin1').toString('utf8')
        cb(null, fileName)
      }
    })
  }
  const config = {
    storage: storage(path)
  }
  /**
   * 文件过滤
   */
  if (fileFilter) {
    config.fileFilter = fileFilter
  }
  /**
   * 上传配置
   */
  return multer(config)
}

创建服务

const express = require('express')
const fse = require('fs-extra')
const fs = require('fs')
const multer = require('./utils/multer.js')
const { sep, resolve } = require('path')
const app = express()
const router = express.Router()
// multer 配置
const multerOption = multer(resolve(__dirname, `.${sep}cache`))

/**
 * 处理静态文件
 * 静态资源 token 校验
 */
express.static(resolve(__dirname,`.${sep}public`)))
app.use(express.static(resolve(__dirname, `.${sep}public`)))
app.use(express.static(resolve(__dirname, `.${sep}dist`)))
/**
 * 跨域
 */
app.use(cors())
/**
 * 请求参数
 */
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())

/**
 * 上传切片
 */
router.post('/upload/chunk', multerOption.single('file'), (req, res) => {
try {
    const { file } = req
    const { chunkIndex, name: fileName } = req.body
    const cachePath = resolve(__dirname, `.${sep}cache`)
    const filePath = resolve(cachePath, `.${sep}${fileName}`)
    // 创建hash目录
    createFolder(filePath)
    // 移动chunk到指定文件目录
    fs.renameSync(resolve(cachePath, `.${sep}${file.originalname}`), resolve(filePath, `.${sep}${file.originalname}`))
  } catch (e) {
    console.log('e', e)
    throw new Error(e.message)
  }
})

/**
 * 合并切片
 */
router.post('/upload/chunk', (req, res) => {
  try {
    const { name: fileName, tagIds } = req.bodyd
    const filePath = resolve(__dirname, `.${sep}cache${sep}${fileName}`)
    const outputPath = resolve(__dirname, `.${sep}output`)
    // 获取 分片 文件
    const chunks = fs.readdirSync(hashPath)
    // 排序分片
    chunks.sort((a, b) => {
      const numA = parseInt(a)
      const numB = parseInt(b)
      return numA - numB
    })
    // 合并分片
    chunks.map(chunkPath => {
      fs.appendFileSync(
        resolve(filePath, `.${sep}${fileName}.mp4`),
        fs.readFileSync(resolve(filePath, `.${sep}${chunkPath}`))
      )
    })
    // 移动视频到指定目录
    fs.renameSync(resolve(filePath, `.${sep}${fileName}.mp4`), resolve(outputPath, `.{sep}${fileName}.mp4`))
    // 删除分片
    chunks.map(chunkPath => {
      fs.unlinkSync(resolve(filePath, `.${sep}${chunkPath}`))
    })
    // 删除hash目录
    fs.rmdirSync(filePath)
  } catch (e) {
    throw new Error(e.message)
  }
})

/**
 * 创建文件夹
 * @param {String} path 文件夹路径
 */
createFolder(path) {
  try {
    if (fse.existsSync(path)) {
      return
    }
    fse.ensureDirSync(path)
  } catch (error) {
    throw new Error('[Create Folder]创建文件夹失败', error)
  }
}

/**
 * 启动服务
 */
try {
  const port = process.env.PORT || 8081 // 端口号
  const host = process.env.IP || '0.0.0.0' // 主机地址
  app.listen(port, host, () => {
    console.log(`服务已启动,访问地址:http://${host}:${port}`)
  })
} catch (error) {
  console.error('启动服务失败:', error)
}

客户端 (vue3 + element-plus)

<template>
  <div class="upload-video round-8 pd-16 border-box scroll-y">
    <div class="container" style="overflow: hidden;">
      <input ref="uploadRef" type="file" :multiple="uploadOptions.multiple" :accept="uploadOptions.accept" @change="handleSelectFile" />
      <!-- 等待上传 -->
      <div v-if="uploadStatus === 'waiting'" class="upload-box flex-center text-center pointer hover"
        @dragover="handlePreventDefault"
        @dragenter="handlePreventDefault"
        @drop="handleFileDrop"
        @click="handleClickUpload">
        <img src="@/assets/upload.png" alt="上传" class="upload-icon" />
        <div class="mg-l-8" style="line-height: 22px;">
          <p class="color-info font-12 ellipsis">拖拽到此区域上传或点击上传</p>
          <p class="color-info font-12 ellipsis">仅支持 .mp4 格式</p>
        </div>
      </div>
      <!-- 上传 -->
      <div v-else class="upload-box flex-center-column pd-16 border-box">
        <!-- 正在上传 -->
        <div v-if="uploadStatus === 'uploading'" class="flex-column jc-c" style="width: 100%; height: 100%;">
          <el-progress :percentage="progress" />
          <div class="font-12 color-info flex ai-c jc-sb">
            <el-button text type="info" size="small" loading style="margin-left: -8px;">
              <span v-if="chunkInfo.total" class="mg-l-4">
                {{ chunkInfo.uploaded !== chunkInfo.total ? `(${chunkInfo.uploaded}/${chunkInfo.total}) 正在上传...` : '上传成功,正在读取文件...' }}
              </span>
            </el-button>
            <el-button text type="danger" size="small" class="mg-r-16">
              取消
            </el-button>
          </div>
        </div>
        <!-- 上传完成 -->
        <div v-if="uploadStatus === 'success'" class="flex-center-column">
          <div class="preview-video mg-b-12 relative pointer" @click="handleClickPreview">
            <div v-if="isPreview" class="preview-video-mask" />
            <video ref="previewVideoRef" :src="previewUrl" preload="metadata" class="round-4" width="100%" height="100%" style="aspect-ratio: 16/9;" />
          </div>
          <span class="font-12 color-info flex ai-c" style="max-width: 326px;">
            <el-icon class="mg-r-4 font-14 color-success"><CircleCheckFilled /></el-icon>
            <span class="ellipsis">已选择文件【{{ fileInfo?.name }}】</span>
          </span>
          <el-button size="small" class="mg-t-8" type="primary" @click="handleClickUpload">重新上传文件</el-button>
        </div>
      </div>
    </div>
    <div class="form-box mg-t-16">
      <el-form :model="form" ref="formRef" label-position="top" :rules="formRules">
        <el-form-item label="视频名称" style="margin-bottom: 8px;" prop="name">
          <el-input v-model="form.name" type="textarea" :rows="5" resize="none" placeholder="请输入视频名称" clearable />
        </el-form-item>
        <el-form-item label="视频标签" prop="tags">
          <el-select v-model="form.tags" placeholder="请选择视频标签" clearable filterable multiple :disabled="!tags.length">
            <el-option v-for="item in tags" :key="item.id" :label="item.name" :value="item.id" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleConfirm">确定</el-button>
          <el-button type="info" @click="handleClickBack">返回</el-button>
        </el-form-item>
      </el-form>
    </div>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { getTagList, checkVideoChunkApi, uploadChunkApi, mergeChunkApi } from '@/api'

const router = useRouter()
const fileInfo = ref(null)

/**
 * 表单
 */
const form = reactive({
  name: '',
  tags: []
})

/**
 * 表单验证
 */
const formRules = {
  name: [{ required: true, message: '请输入视频名称', trigger: 'blur' }],
  tags: [{ required: true, message: '请选择视频标签', trigger: 'blur' }]
}

/**
 * 视频标签
 */
const tags = ref([])

/**
 * 上传视频的配置
 * @type {Object} { accept: 'video/mp4', multiple: true }
 */
const uploadOptions = {
  accept: ['video/mp4'],
  multiple: false
}

/**
 * 上传进度
 * @type {Number}
 */
const progress = ref(10)

/**
 * 上传状态
 * waiting | uploading | success | fail
 */
const uploadStatus = ref('waiting')

/**
 * 阻止浏览器拖拽打开文件的默认行为
 * @param {Object} e
 */
const handlePreventDefault = (e) => {
  e.stopPropagation()
  e.preventDefault()
}

/**
 * 放开鼠标,拖拽结束时回调
 * @param {Object} e
 */
 const handleFileDrop = async (e) => {
  try {
    handlePreventDefault(e)
    const filesList = []
    const target = []
    const types = e.dataTransfer.types
    if (!types.includes('Files')) {
      ElMessage.warning('仅支持MP4文件!')
      return
    }
    // 特殊处理,不然直接看e的files始终为空
    target.forEach.call(e.dataTransfer.files, (file) => { filesList.push(file) }, false)
    if (!filesList.length) {
      return
    }
    const file = filesList[0]
    const fileEvent = {
      target: {
        files: [file]
      }
    }
    handleSelectFile(fileEvent)
  } catch (error) {
    console.error(error)
    uploadStatus.value = 'waiting'
  } finally {
    uploadRef.value.value = null
  }
}

const previewUrl = ref('')
/**
 * 手动选择本地文件
 * @param {Object} fileEvent
 */
const handleSelectFile = async (fileEvent) => {
  try {
    const { target } = fileEvent
    if (!target.files.length) {
      return
    }
    const file = target.files[0]
    console.log('🔅 ~ handleSelectFile ~ file:', file)
    // 校验文件
    if (file.type !== 'video/mp4') {
      ElMessage.warning('仅支持MP4文件!')
      return
    }
    uploadStatus.value = 'success'
    fileInfo.value = file
    // 设置视频名称 -- 去除文件后缀
    form.name = file.name.replace(/.mp4$/, '')
    previewUrl.value = URL.createObjectURL(file)
  } catch (error) {
    console.error(error)
    uploadStatus.value = 'waiting'
  } finally {
    uploadRef.value.value = null
  }
}

const uploadRef = ref(null)
/**
 * 点击上传按钮
 */
const handleClickUpload = () => {
  uploadRef.value.click()
}

const previewVideoRef = ref(null)
const isPreview = ref(true)
/**
 * 点击预览
 */
const handleClickPreview = () => {
  // 如果正在预览,则暂停
  if (!isPreview.value) {
    previewVideoRef.value.pause()
    isPreview.value = true
    return
  }
  // 如果未正在预览,则播放
  isPreview.value = false
  previewVideoRef.value.play()
}

/**
 * 点击返回
 */
const handleClickBack = () => {
  router.back()
}

/**
 * 分片信息
 */
const chunkInfo = reactive({
  total: 0,
  uploaded: 0
})

const formRef = ref(null)
/**
 * 点击确定
 */
const handleConfirm = async () => {
  // console.log('handleConfirm', fileInfo.value)
  try {
    await formRef.value.validate()
    // 检测视频-已上传了多少分片
    const chunkCheckInfo = await checkVideoChunkApi({ name: form.name })
    if (chunkCheckInfo.code === 1) {
      return
    }
    // 已上传分片数量
    const isUploadedChunkArr = chunkCheckInfo.data
    // 分片大小
    const chunkSize = 1024 * 1024 * 20 // 20MB
    // 切片总数量
    chunkInfo.total = Math.ceil(fileInfo.value.size / chunkSize)
    // 切片列表
    const chunkList = []
    for (let i = 0; i < chunkInfo.total; i++) {
      const start = i * chunkSize
      const end = Math.min(fileInfo.value.size, start + chunkSize)
      const chunk = fileInfo.value.slice(start, end)
      chunkList.push(chunk)
    }
    uploadStatus.value = 'uploading'
    //  上传切片
    for (let i = 0; i < chunkList.length; i++) {
      let chunkIndex = i + 1
      if (isUploadedChunkArr.includes(`${chunkIndex}`)) {
        chunkInfo.uploaded++
        continue
      }
      let blobFile = new File([chunkList[i]], `${chunkIndex}.mp4`)
      const formData = new FormData()
      formData.append('file', blobFile)
      formData.append('name', form.name)
      formData.append('chunkIndex', chunkIndex)
      const flag = await uploadChunkApi(formData, (evt) => {
        progress.value = 0
        progress.value = evt?.progress ? Math.floor(evt.progress * 100) : 0
      })
      if (flag.code === 1) {
        break
      }
      chunkInfo.uploaded++
    }
    // 合并切片
    await mergeChunkApi({
      name: form.name,
      tagIds: form.tags
    })
    uploadStatus.value = 'success'
    ElMessage.success('上传成功')
    router.push({
      path: '/list',
      query: {
        tagId: form.tags[0]
      }
    })
  } catch (error) {
    console.log(error)
  }
}

const getTagListData = async () => {
  try {
    const res = await getTagList()
    if (res.code === 0) {
      tags.value = res.data
    }
  } catch (error) {
    console.log(error)
  }
}


onMounted(() => {
  getTagListData()
})

</script>
<script>
export default {
  name: 'UploadVideo'
}
</script>
<style lang="scss" scoped>
.upload-video {
  width: 100%;
  height: 100%;
  background-color: var(--el-bg-color);
}

.upload-box {
  width: 100%;
  height: 220px;
  font-size: 16px;
  border-radius: 8px;
  background-color: var(--el-fill-color-light);
  .upload-icon {
    width: 160px;
  }
  &.hover {
    &:hover {
      border-color: #409EFF;
    }
  }
}

.preview-video {
  width: 220px;
  position: relative;
  object-fit: cover;
  aspect-ratio: 16/9;
  border-radius: 4px;
  background-color: var(--el-color-primary-light-9);
  .preview-video-mask {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: url('@/assets/play.png') no-repeat center center;
    background-size: 22% 30%;
    background-color: rgba(0, 0, 0, 0.5);
    border-radius: 4px;
  }
}

input[type="file"] {
  display: none;
}

:deep(){
  .el-form-item__label {
    margin-bottom: 4px;
  }
}
</style>

预览

未上传

image-20251015165314385.png

待上传

image-20251015174233866.png

上传中

image-20251015174351510.png

react高阶组件

一. 定义

  • 官方定义:参数为组件,返回值为新组件的函数

  • 本质:是函数而非组件,是对原有组件进行拦截封装的新组件,本质上是一种设计模式而非React API

 -   特点:
    -   接收一个组件作为参数
    -   返回一个新组件
    -   对新组件进行拦截和增强
  • 调用方式:const EnhancedComponent = higherOrderComponent(WrappedComponent)

  • 常见应用:

    • Redux中的connect函数(返回高阶组件)
    • React Router中的withRouter函数
  • 实现原理:

    • 结构:接收一个组件作为参数,返回一个新的增强组件
    • 命名规范:可通过displayName属性修改组件调试名称
    • 继承方式:新组件通常继承自PureComponent以获得性能优化

基础示例

import React, { PureComponent } from 'react'

// 定义一个高阶组件
function hoc(Cpn) {
  // 1.定义类组件
  class NewCpn extends PureComponent {
    render() {
      return <Cpn name="why"/>
    }
  }
  // 设置 displayName动态命名
  NewCpn.displayName = `HOC(${Cpn.displayName || Cpn.name || 'Component'})`;
  return NewCpn

  // 定义函数组件
  // function NewCpn2(props) {

  // }
  // return NewCpn2
}

class HelloWorld extends PureComponent {
  render() {
    return <h1>Hello World</h1>
  }
}

//直接命名
HelloWorld.displayName = 'HelloWorldComponent';

const HelloWorldHOC = hoc(HelloWorld)

export class App extends PureComponent {
  render() {
    return (
      <div>
        <HelloWorldHOC/>
      </div>
    )
  }
}

export default App

props增强

import { PureComponent } from 'react'

// 定义组件: 给一些需要特殊数据的组件, 注入props
function enhancedUserInfo(OriginComponent) {
  class NewComponent extends PureComponent {
    constructor(props) {
      super(props)

      this.state = {
        userInfo: {
          name: "clare",
          level: 1
        }
      }
    }

    render() {
      return <OriginComponent {...this.props} {...this.state.userInfo}/>
    }
  }

  return NewComponent
}

export default enhancedUserInfo

import React, { PureComponent } from 'react'
import enhancedUserInfo from './hoc/enhanced_props'
import About from './pages/About'


const Home = enhancedUserInfo(function(props) {
  return <h1>Home: {props.name}-{props.level}-{props.banners}</h1>
})


export class App extends PureComponent {
  render() {
    return (
      <div>
        <Home banners={["轮播1", "轮播2"]}/>   
      </div>
    )
  }
}

export default App

Context共享

import { createContext } from "react"

const ThemeContext = createContext()

export default ThemeContext



import ThemeContext from "../context/theme_context"

function withTheme(OriginComponment) {
  return (props) => {
    return (
      <ThemeContext.Consumer>
        {
          value => {
            return <OriginComponment {...value} {...props}/>
          }
        }
      </ThemeContext.Consumer>
    )
  }
}

export default withTheme



import React, { PureComponent } from 'react'
import withTheme from '../hoc/with_theme'
import ThemeContext from '../context/theme_context'



// export class Product extends PureComponent {
//   render() {
//     return (
//       <div>
//         Product:
//         <ThemeContext.Consumer>
//           {
//             value => {
//               return <h2>theme:{value.color}-{value.size}</h2>
//             }
//           }
//         </ThemeContext.Consumer>
//       </div>
//     )
//   }
// }

// export default Product

export class Product extends PureComponent {
  render() {
    const { color, size } = this.props

    return (
      <div>
        <h2>Product: {color}-{size}</h2>
      </div>
    )
  }
}

export default withTheme(Product)


import React, { PureComponent } from 'react'
import ThemeContext from './context/theme_context'
import Product from './pages/Product'

export class App extends PureComponent {
  render() {
    return (
      <div>
        <ThemeContext.Provider value={{color: "red", size: 30}}>
          <Product/>
        </ThemeContext.Provider>
      </div>
    )
  }
}

export default App

登录鉴权


function loginAuth(OriginComponent) {
  return props => {
    // 从localStorage中获取token
    const token = localStorage.getItem("token")

    if (token) {
      return <OriginComponent {...props}/>
    } else {
      return <h2>请先登录, 再进行跳转到对应的页面中</h2>
    }
  }
}

export default loginAuth

import React, { PureComponent } from 'react'
import loginAuth from '../hoc/login_auth'

export class Cart extends PureComponent {
  render() {
    return (
      <h2>Cart Page</h2>
    )
  }
}

export default loginAuth(Cart)


import React, { PureComponent } from 'react'
import Cart from './pages/Cart'

export class App extends PureComponent {
  constructor() {
    super()

    // this.state = {
    //   isLogin: false
    // }
  }

  loginClick() {
    localStorage.setItem("token", "hhh")

     this.setState({ isLogin: true })
    //this.forceUpdate()  //强制刷新用的较少
  }

  render() {
    return (
      <div>
        App
        <button onClick={e => this.loginClick()}>登录</button>
        <Cart/>
      </div>
    )
  }
}

export default App

二. 缺陷

  • 嵌套问题:需要包裹原组件,大量使用会产生深层嵌套
  • 调试困难:多层嵌套让props来源难以追踪
  • props劫持:可能意外覆盖传入的props(如name属性被覆盖)
  • 适用场景:类组件中仍常见,函数组件推荐使用Hooks

三. 其余高阶组件函数

memo组件作用

当父组件重新渲染时,React 默认会递归渲染所有子组件。memo 可以阻止子组件在 props 没有变化 时的重新渲染。

-   功能:类似PureComponent,对props进行浅比较(shallow compare),
-   原理:比较前后props差异决定是否重新渲染
-   本质:就是一个高阶组件,接收组件返回增强后的组件
import { useState, memo } from 'react';

// 使用 memo 包装子组件
const ChildComponent = memo(function ChildComponent({ name }) {
  console.log('ChildComponent 渲染了'); // 只有 name 变化时才会打印
  return <div>Hello, {name}!</div>;
});

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('Alice');

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        计数: {count}
      </button>
      <button onClick={() => setName('Bob')}>
        改名
      </button>
      <ChildComponent name={name} />
    </div>
  );
}

注意事项


// 注意:如果传递对象、数组或函数,memo 可能失效
const ChildComponent = memo(function ChildComponent({ user, onClick }) {
  console.log('ChildComponent 渲染了');
  return <div onClick={onClick}>Hello, {user.name}!</div>;
});

function ParentComponent() {
  const [count, setCount] = useState(0);
  
  // 每次都会创建新的对象和函数,导致 memo 失效
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>计数</button>
      <ChildComponent 
        user={{ name: 'Alice' }}  // 每次都创建新对象
        onClick={() => {}}         // 每次都创建新函数
      />
    </div>
  );
}

解决:使用 useMemo 和 useCallback 来保持引用稳定。

useMemo vs useCallback 对比

特性 useMemo useCallback
缓存函数 useMemo(() => fn, deps) useCallback(fn, deps)
缓存对象 useMemo(() => obj, deps) 不适用
返回值 缓存函数返回的值 直接缓存函数本身
等价关系 useCallback(fn, deps) = useMemo(() => fn, deps)

forwardRef作用

-   问题背景:函数组件无实例,无法直接绑定ref
-   解决方案:使用forwardRef将ref作为第二个参数传递
-   限制:仅适用于函数组件,类组件使用会报错

其余用法可查看: juejin.cn/post/718658…

import { useRef, forwardRef } from 'react';

// 简单的按钮组件
const FancyButton = forwardRef((props, ref) => {
return (
  <button ref={ref} style={{ padding: '10px 20px' }}>
    {props.children}
  </button>
);
});

function App() {
const buttonRef = useRef(null);

const focusButton = () => {
  buttonRef.current.focus(); // 聚焦按钮
};

return (
  <div>
    <FancyButton ref={buttonRef}>点击我</FancyButton>
    <button onClick={focusButton}>让上面按钮获得焦点</button>
  </div>
);
}

PDF中的图像与外部对象

PDF 里的“图像”和“外部对象”是什么? 简单来说,PDF 页面就像一张大画布,而 外部对象(XObject) 就是预先准备好、可以随时贴上去的小画布。 每张图片、每个可复用图形,都是一个 XO
❌