MoreRSS

site iconLiHan | 李寒修改

研究生,中国传媒大学
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

LiHan | 李寒的 RSS 预览

Python 笔试备忘

2026-03-13 22:24:00

Featured image of post Python 笔试备忘

1. 输入输出处理 (Input/Output)

Python ACM 模式输入模板

在笔试(ACM模式)中,Python 的 input() 有时较慢,推荐使用 sys.stdin.readline

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import sys

# 1. 快速读取一行字符串(去除末尾换行符)
def input():
 return sys.stdin.readline().strip()

# 2. 读取一个整数
n = int(input())

# 3. 读取一行以空格分隔的整数列表
nums = list(map(int, input().split()))

# 4. 读取多行输入(行数已知)
n = int(input())
lines = [input() for _ in range(n)]

# 5. 读取多行输入(行数未知,直到 EOF)
try:
 while True:
 line = input()
 if not line: break
 parts = list(map(int, line.split()))
 # do something
except EOFError:
 pass

常见输入模式案例 (Case Study)

案例:跳过首行/处理固定行数

很多题目第一行是 Case 数 T,后面跟 T 行数据。

推荐写法 (Standard Way): 利用 sys.stdin 是一个迭代器的特性,先手动读取第一行消耗掉,再循环。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import sys

# 假设第一行是 T,后面是 T 行 "a b"
# 1. 消耗掉第一行
sys.stdin.readline()

# 2. 循环处理剩下的行
for line in sys.stdin:
 line = line.strip()
 if not line: continue
 parts = line.split()
 a, b = int(parts[0]), int(parts[1])
 print(a + b)

案例:N 个整数可能跨行 (The read() trick)

题目说“输入 N 个整数”,但数据可能写在同一行,也可能分多行。 千万不要只用 sys.stdin.readline().split(),因为它只读一行,如果数据换行了就会报错。

万能写法 (Recommended): 使用 sys.stdin.read().split() 读取所有剩余输入,它会自动忽略所有空白符(空格、换行、制表符)。这是 ACM 模式处理复杂输入的神器。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import sys

# 1. 一次性读入所有剩余内容 -> 字符串列表
# input_data = ['3', '10', '20', '30']
input_data = sys.stdin.read().split()

# 2. 转为迭代器 (Iterator) 方便逐个提取
iterator = iter(input_data)

try:
 # 提取第一个数 N
 n = int(next(iterator))

 # 提取剩下的 N 个数
 nums = []
 for _ in range(n):
 nums.append(int(next(iterator)))

 print(sum(nums))

except StopIteration:
 pass

案例:单行秒杀 (One-Liner)

如果确定输入即为一行空格分隔的整数(如:1 2 3 4 5),直接使用 map

1
2
nums = list(map(int, sys.stdin.readline().split()))
print(sum(nums))

案例:二维矩阵处理 (Matrix)

题目输入 nm 列的矩阵。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import sys

# 1. 读入 n, m
line_data = sys.stdin.readline().split()
if not line_data: exit()
n, m = int(line_data[0]), int(line_data[1])

# 2. 读入矩阵 (List of Lists)
# 方式 A: 列表推导式 (如果需要后续 DP,必须存下来)
matrix = [list(map(int, sys.stdin.readline().split())) for _ in range(n)]

# 方式 B: 逐行累加 (如果只是求和,不需要存矩阵,空间 O(1))
total_sum = 0
for _ in range(n):
 total_sum += sum(map(int, sys.stdin.readline().split()))
print(total_sum)

输出技巧

  • 列表转字符串: print(" ".join(map(str, ans)))
  • 格式化输出 (Formatting):
    • 保留小数: f"{x:.3f}" (保留 3 位小数,自动四舍五入) -> 1.235
    • 补前导零: cnt.zfill(5)f"{cnt:05d}" -> 00123
    • 百分比: f"{x:.2%}" -> 12.34%
  • 快速输出: 必须输出大量行 (10^5 级) 时,print 可能超时。
    1
    
    sys.stdout.write('\n'.join(map(str, result_list)) + '\n')
    

2. 循环与控制流 (Loops & Control Flow)

while 循环 (While Loop)

  • 特定次数或条件: 比如 BFS 队列循环,二分查找。
1
2
3
4
5
6
7
8
# 1. 基本用法
while i < n:
 i += 1
 if i % 2 == 0: continue # 跳过本次
 if i > 100: break # 跳出循环

# 2. 常见坑: 死循环
# 务必检查 while 条件内的变量是否在变化 (i+=1, l=mid+1)

for 循环 (For Loop)

  • range() 函数: range(start, stop, step) 左闭右开。

    • range(5) -> 0, 1, 2, 3, 4
    • range(1, 4) -> 1, 2, 3
    • range(5, 0, -1) -> 5, 4, 3, 2, 1 (倒序遍历常考)
  • enumerate(): 同时获取索引和值 (推荐)。

1
2
for i, val in enumerate(nums):
 print(i, val)
  • zip(): 同时遍历两个列表。
1
2
3
names = ['a', 'b']; scores = [90, 80]
for name, score in zip(names, scores):
 print(name, score)
  • else 子句: 当循环正常结束(没有被 break 中断)时执行。
    • 常见场景:判断质数,查找失败后的兜底逻辑。
1
2
3
4
5
6
for x in nums:
 if x == target:
 print("Found")
 break
else:
 print("Not Found") # 若循环走完都没 break,则执行

3. 基础数据结构 (Data Structures)

常用容器

1. 列表 (List)

  • 创建:
    • arr = [] (空列表)
    • arr = [0] * n (初始化 n 个 0, 比如 DP 数组)
    • matrix = [[0] * m for _ in range(n)] (n 行 m 列二维数组,千万别用 [[0]*m]*n)
  • 常用操作:
    • arr.append(x): 末尾添加 O(1)
    • arr.pop(): 弹出末尾 O(1)
    • arr.insert(i, x): 在 i 处插入 O(N) (慎用)
    • len(arr): 长度 O(1)
    • arr.sort(): 原地排序 O(N log N)
    • arr.reverse(): 原地翻转 O(N)
  • 切片 (Slicing): arr[start:end:step]
    • arr[::-1]: 翻转列表

2. 集合 (Set)

  • 创建:
    • s = set() (空集合,注意不是 {},{} 是空字典)
    • s = {1, 2, 3}
    • s = set(arr) (列表转集合去重)
  • 常用操作:
    • s.add(x): 添加 O(1)
    • s.remove(x): 删除 O(1) (不存在报错)
    • x in s: 判断存在 O(1)
  • 集合运算:
    • s1 | s2 (并), s1 & s2 (交), s1 - s2 (差)

3. 字典 (Dict / HashMap)

  • 创建:
    • d = {} (空字典)
    • d = {'a': 1, 'b': 2}
  • 常用操作:
    • d['k'] = v: 设置/更新
    • val = d.get('k', default): 安全获取 (不存在返回 default)
    • key in d: 判断 key 是否存在 O(1)
    • del d['k']: 删除 key
  • 遍历:
    • for k, v in d.items(): ... (遍历键值对)
    • for k in d: ... (只遍历键)

高级数据结构 (collections / heapq)

4. 双端队列 (collections.deque)

  • 比 list 强在哪: list.pop(0) 是 O(N) 的(因为要移动数组),而 deque.popleft() 是 O(1) 的。
  • BFS 必备:
1
2
3
4
5
6
from collections import deque
q = deque([1, 2, 3])
q.append(4) # [1, 2, 3, 4]
q.appendleft(0) # [0, 1, 2, 3, 4]
head = q.popleft() # 0
tail = q.pop() # 4

5. 计数器 (collections.Counter)

  • 作用: 快速统计元素频率,实际上是一个 Dict 的子类。
1
2
3
4
5
from collections import Counter
s = "abacaba"
counts = Counter(s) # Counter({'a': 4, 'b': 2, 'c': 1})
counts['d'] # 返回 0 (不会报错)
top2 = counts.most_common(2) # [('a', 4), ('b', 2)]

6. 默认字典 (collections.defaultdict)

  • 作用: 自动处理 key 不存在的情况,省去 if key not in d 的判断。
1
2
3
4
5
6
7
8
from collections import defaultdict
# 初始化值默认为空列表 []
adj = defaultdict(list)
adj[0].append(1) # {0: [1]}

# 初始化值默认为 0
freq = defaultdict(int)
freq['apple'] += 1 # { 'apple': 1 }

7. 堆 (Heaps / Priority Queue)

  • 作用: 动态维护最小值/最大值,插入和删除最值都是 O(log N)。
  • 注意: Python 只有最小堆。想用最大堆?存入负数 -val
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import heapq
nums = [3, 1, 4, 1, 5]

# 1. 建堆 (O(N))
heapq.heapify(nums) # nums 变为 [1, 1, 4, 3, 5] (堆顺序)

# 2. 推入 (O(log N))
heapq.heappush(nums, 2)

# 3. 弹出最小值 (O(log N))
min_val = heapq.heappop(nums) # 1

# 4. 获取前 K 大元素 (O(N log K))
top3 = heapq.nlargest(3, nums) # [5, 4, 3]

3. 常用内置函数及技巧 (Built-in Functions & Tricks)

1. 字符与编码 (ASCII)

处理字符串偏移(如 ‘a’->‘b’)时必用。

  • ord(char): 字符 -> ASCII 数值。
    • ord('a') 是 97, ord('A') 是 65, ord('0') 是 48。
  • chr(int): ASCII 数值 -> 字符。
    • chr(97) 是 ‘a’。
1
2
3
# 字母移位(凯撒密码)
c = 'a'
next_c = chr(ord(c) + 1) # 'b'

2. 进制转换

  • bin(n): 转二进制字符串 (e.g., '0b101').
  • hex(n): 转十六进制字符串 (e.g., '0xa').
  • int(string, base): 任意进制转十进制。
    • int('101', 2) -> 5
    • int('A', 16) -> 10

3. 数学与数字处理

  • 大整数 (Big Int): Python 的 int任意精度的(自动扩容),不会溢出。不用担心 C++ 中的 int vs long long 问题,可以直接计算超大整数。
  • divmod(a, b): 返回 (a // b, a % b),商和余数。
  • pow(a, b, mod): 快速幂求模 (a ** b) % mod(a**b)%mod 快得多
  • abs(x): 绝对值。
  • round(x, n): 四舍五入保留 n 位小数。
  • 快速幂 (Binary Exponentiation) 速记:
    • 用途: 计算 a^b,特别是 (a^b) % mod
    • 复杂度: 普通连乘是 O(b),快速幂是 O(log b)
    • 笔试必用: 只要看到“大指数 + 取模”,优先写 pow(a, b, mod)
    • 手写模板 (迭代版):
1
2
3
4
5
6
7
8
9
def qpow(a: int, b: int, mod: int) -> int:
 ans = 1
 a %= mod
 while b > 0:
 if b & 1:
 ans = (ans * a) % mod
 a = (a * a) % mod
 b >>= 1
 return ans
- **等价关系**: `qpow(a, b, mod) == pow(a, b, mod)`。
  • math 模块:
    • math.ceil(x), math.floor(x): 向上/向下取整 (注意: int 默认是向 0 取整)。
    • math.gcd(a, b): 最大公约数。
    • math.lcm(a, b): 最小公倍数 (Python 3.9+)。
    • math.pi: 圆周率 (3.1415926…),比用 3.14 精确。
    • math.sqrt(x): 平方根 (返回 float)。
    • math.isqrt(x): 整数平方根 (返回 int, 向下取整, 比 floor(sqrt) 准)。
    • math.inf: 无穷大 (常用于初始化最小值 min_val = float('inf'))。
  • 注意: / 得到的是 float,整数整除必须用 //

4. 字符串操作 (Strings)

字符串是不可变对象 (Immutable),所有修改操作都会返回新字符串。

  • 大小写:

    • s.lower(), s.upper(): 转小写/大写
    • s.capitalize(): 首字母大写
    • s.title(): 每个单词首字母大写
  • 查找与判断:

    • s.startswith(prefix), s.endswith(suffix): 前后缀判断
    • s.find(sub): 返回第一个索引,找不到返 -1 (推荐)
    • s.index(sub): 返回第一个索引,找不到报错 (慎用)
    • s.count(sub): 统计子串出现次数
    • s.isdigit(): 是否全数字
    • s.isalpha(): 是否全字母
    • s.isalnum(): 是否全数字或字母
  • 拆分与合并:

    • s.strip(): 去除两端空白 (经常用于处理 input)
    • s.split(sep): 按 sep 分割,默认按空白符 (空格、换行、制表)
      • line = " a b "; line.split() -> ['a', 'b'] (自动去空,结果无需再 strip)
    • sep.join(iterable): 将列表组合成字符串 (效率高,千万别用 + 循环拼接)
      • ' '.join(['a', 'b']) -> 'a b' (空格连接)
      • "".join(['a', 'b']) -> 'ab' (无缝拼接,替代 s += x)
  • List <-> Str 互转 (修改字符串技巧):

    • list(s): 字符串转字符列表 (为了修改)。
      • s = "abc"; chars = list(s) # ['a', 'b', 'c']
      • chars[0] = 'X'
    • "".join(chars): 列表转回字符串。
      • new_s = "".join(chars) # "Xbc"
  • 替换与切片:

    • s.replace(old, new, count): 替换,count 可选替换次数
    • s[::-1]: 字符串翻转 (最快写法)

5. 高阶函数与数据处理 (Functional & Data)

学会这些,一行代码顶十行。

map(function, iterable)

  • 作用: 将 function 作用于 iterable 中的每一个元素。
  • 返回: 迭代器 (iterator)。需要用 list() 包裹才能看到完整列表(除非只是用来遍历或求和)。
  • 常见用法:
    • 类型转换: map(int, ['1', '2']) -> [1, 2]
    • 多变量赋值: a, b = map(int, input().split())

filter(function, iterable)

  • 作用: 保留 function(x)True 的元素。
  • 用法:
    • nums = [1, 2, 3, 4]
    • evens = list(filter(lambda x: x % 2 == 0, nums)) -> [2, 4]

lambda 表达式 (匿名函数)

  • 作用: 定义短小的、一次性的函数。
  • 语法: lambda arguments: expression
  • 场景: 配合 sort, max, map, filter 使用。
    • 自定义排序: 按元组第二个元素排序 arr.sort(key=lambda x: x[1])
    • 自定义最大值规则: longest = max(strings, key=lambda s: len(s))

reduce(function, iterable) (from functools)

  • 作用: 对序列进行累积操作(折叠)。
  • 用法:
    • from functools import reduce
    • product = reduce(lambda x, y: x * y, [1, 2, 3, 4]) -> 24

sum(iterable, start)

  • 作用: 求和。不要忘了如果元素是列表,可以用 sum 展平一层(虽然慢点)。
  • 用法:
    • sum([1, 2, 3]) -> 6
    • sum([[1, 2], [3, 4]], []) -> [1, 2, 3, 4] (慎用,O(N^2))

sorted(iterable, key=…, reverse=…)

  • 作用: 返回一个新的已排序列表(不改变原列表)。
  • 区别: list.sort() 是原地排序,无返回值。
  • Tip: 只要是可以迭代的对象(字符串、元组)都可以丢给 sorted
    • sorted("bca") -> ['a', 'b', 'c']

6. 其他常用技巧

  • 解包 (Unpacking): a, b = b, a (交换); x, *rest = [1, 2, 3] (x=1, rest=[2,3]).
  • Zip: 同时遍历两个列表 for x, y in zip(list1, list2):.
  • Enumerate: 带索引遍历 for i, val in enumerate(list):.
  • Any/All: if any(x > 0 for x in nums): (是否存在); if all(...) (是否所有).

4. 常见算法模板 (Algorithms)

1. 查找与二分 (Binary Search)

Python 标准库 bisect 非常好用。

  • bisect.bisect_left(arr, x): 寻找插入点,使得左边都 < x (即第一个 >= x 的位置)。
  • bisect.bisect_right(arr, x): 寻找插入点,使得左边都 <= x (即第一个 > x 的位置)。

也可手动实现二分:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def binary_search(nums, target):
 l, r = 0, len(nums) - 1
 while l <= r:
 mid = (l + r) // 2
 if nums[mid] == target:
 return mid
 elif nums[mid] < target:
 l = mid + 1
 else:
 r = mid - 1
 return -1

2. 双指针 (Two Pointers) / 滑动窗口 (Sliding Window)

常用于处理数组、字符串子串问题。

1
2
3
4
5
6
7
l = 0
for r in range(len(arr)):
 current_val += arr[r]
 while not check(current_val): # 窗口不满足条件时收缩
 remove_val(arr[l])
 l += 1
 update_result()

3. 搜索 (BFS / DFS)

BFS (最短路径/层序遍历):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from collections import deque
q = deque([start_node])
visited = {start_node}
step = 0
while q:
 size = len(q)
 for _ in range(size):
 curr = q.popleft()
 if curr == target: return step
 for neighbor in adj[curr]:
 if neighbor not in visited:
 visited.add(neighbor)
 q.append(neighbor)
 step += 1

DFS (全排列/连通性):

1
2
3
4
5
6
7
8
def dfs(node, path):
 if node is None or meets_condition(path):
 return
 visited.add(node)
 for neighbor in adj[node]:
 if neighbor not in visited:
 dfs(neighbor, path + [neighbor])
 visited.remove(node) # 回溯

Tip: 遇到 RecursionError 需要增加递归深度:sys.setrecursionlimit(2000)

4. 并查集 (Union-Find)

处理集合合并、连通分量问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
parent = list(range(n))
def find(x):
 if parent[x] != x:
 parent[x] = find(parent[x]) # 路径压缩
 return parent[x]

def union(x, y):
 rootX = find(x)
 rootY = find(y)
 if rootX != rootY:
 parent[rootX] = rootY

5. 动态规划 (Dynamic Programming)

  • 背包问题 (0/1 背包,完全背包)
  • 股票买卖
  • 编辑距离,最长公共子序列 (LCS) 关键是定义状态 dp[i][j] 和状态转移方程。
1
2
3
4
5
# 0/1 背包一维优化
dp = [0] * (V + 1)
for i in range(N):
 for j in range(V, weights[i] - 1, -1):
 dp[j] = max(dp[j], dp[j - weights[i]] + values[i])

5. 时空复杂度 (Complexity)

时间复杂度优化 Tips

  1. 避免列表查找: if x in list 是 O(N),改为 if x in set 是 O(1) 平均。
  2. 字符串拼接: 永远不要用 s += "a" 在循环中拼接字符串(O(N^2)),使用 "".join(list_of_strings)(O(N))。
  3. 输入输出: 大量数据必须用 sys.stdin.readlinesys.stdout.write
  4. 排序: Python 的 Timsort 非常快,但如果只需 top K,用 heapq.nlargest 更优。
  5. 递归: 深度过深会爆栈 RecursionError,记得 sys.setrecursionlimit(20000)
  6. 列表生成: [x*2 for x in huge_list]append 快,也会比 map 好调试(虽然 map 有时极微快)。
  7. 缓存/记忆化: 使用 @functools.lru_cache(None) 装饰器自动缓存递归结果 (DP)。

空间复杂度优化 Tips

  1. 生成器 (Generators): 处理大序列尽量用生成器表达式 (x**2 for x in range(1000000)) 而不是列表 [...],这就只占一个迭代器内存。
  2. 原地操作: 尽量使用 nums.sort() (in-place) 而不是 sorted(nums) (new list),除非需要保留原数组。
  3. 稀疏数组: 遇到很大范围但稀疏的数据,用 dictdefaultdict 代替大数组。
  4. Slot: 在类中使用 __slots__ 可以极大减少对象内存占用(笔试通常不用,工程中有用)。
  5. Itertools: 善用 itertools.islice, chain, count 等,避免生成中间列表。

常见复杂度参考

  • N <= 20: O(2^N) DFS/Backtracking
  • N <= 100: O(N^4) DP
  • N <= 500: O(N^3) Floyd/DP
  • N <= 2000: O(N^2) Bubble Sort/DP
  • N <= 10^5: O(N log N) Sort/Heap/Merge Sort
  • N <= 10^6: O(N) Hash/Two Pointers/Linear Scan
  • N > 10^8: O(log N) Binary Search/Math

祝好运!

【本文为 AI 生成】Hugo + Pjax 实现无刷新博客体验:从音乐播放中断谈起

2026-03-11 14:23:00

Featured image of post 【本文为 AI 生成】Hugo + Pjax 实现无刷新博客体验:从音乐播放中断谈起

前言

在搭建个人博客的过程中,我一直有一个执念:希望网页底部的音乐播放器能够像网易云音乐那样,在页面切换时永不中断

传统的静态博客(如 Hugo 生成的站点)每一次点击链接,浏览器都会重新加载整个页面 (Full Page Reload)。这意味着:

  1. DOM 树被销毁重建。
  2. 所有 JavaScript 状态丢失。
  3. 音频/视频标签被重置 —— 这就是为什么音乐会停。

为了解决这个问题,我们需要引入 SPA (Single Page Application) 的概念,或者更轻量级的方案 —— Pjax (PushState + Ajax)

核心技术方案:Pjax

Pjax 的工作原理非常直观:

  1. 拦截 <a> 标签的点击事件。
  2. 使用 Ajax 请求新页面的 HTML 内容。
  3. 解析新 HTML,只提取我们需要更新的部分(例如主要内容区 .main-container)。
  4. 使用 history.pushState 修改浏览器的 URL地址栏,使其看起来像正常跳转。
  5. 替换 DOM 中的内容区。

通过这种方式,页脚 (Footer)侧边栏 (Sidebar) 可以保持不变,驻留在其中的音乐播放器自然也就不会中断了。

1. 引入 Pjax

首先在 <head> 中引入 Pjax 库(推荐使用 pjax 库而非老旧的 jquery-pjax):

1
<script src="https://cdn.jsdelivr.net/npm/pjax/pjax.min.js"></script>

2. 定制化配置 (The Tricky Part)

这是最关键的一步。为了保证 Stack 主题的正常渲染,如果你直接替换整个 body,播放器还是会挂掉。我们需要精准打击

我在 layouts/partials/head/custom.html 中进行了如下配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
var pjax = new Pjax({
 selectors: [
 "title",
 ".main-container", // 只替换内容区!
 "body" // 这里的处理很有讲究,见下文
 ],
 switches: {
 "body": function(oldEl, newEl, options) {
 // 我们只更新 body 的 class (用于切换暗色模式或页面特定样式)
 // 绝不替换 body 的 innerHTML,否则页脚脚本会被杀掉
 oldEl.className = newEl.className;
 },
 ".main-container": Pjax.switches.innerHTML, // 标准替换
 "title": Pjax.switches.outerHTML
 }
});

关键点:不仅要通过 CSS 选择器指定更新区域,还要自定义 switch 函数,确保 body 标签只更新属性而不重置内容。

踩坑与填坑

实现 Pjax 只是第一步,真正的挑战在于副作用

坑一:脚本不执行 (Mastodon 动态消失)

现象:跳转到 Timeline 页面,Mastodon 动态加载不出来。 原因:通过 innerHTML 插入的 HTML 片段中如果包含 <script> 标签,浏览器出于安全和规范考虑,通常不会执行它们

解决方案: 我们将初始化代码封装为全局函数,并在 Pjax 完成事件 (pjax:complete) 中手动调用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Check & Init Mastodon Logic
window.initMastodon = function() {
 if (!document.getElementById('mt-container')) return;
 // ... 初始化代码 ...
};

// 监听 Pjax 完成
document.addEventListener('pjax:complete', function () {
 window.initMastodon();
 // 其他需要重载的脚本,如 Google Analytics
 if (typeof gtag === 'function') {
 gtag('config', 'MEASUREMENT_ID', {'page_path': location.pathname});
 }
});

坑二:播放器状态丢失

现象:虽然使用了 Pjax,但用户有时会习惯性按 F5 刷新,或者 Pjax 请求超时回退到普通跳转,这时候音乐还是会断,且进度归零。

解决方案:状态持久化 (State Persistence)。

利用 localStorage 在播放器每秒更新时记录状态:

1
2
3
4
5
6
7
setInterval(() => {
 if (!ap.audio.paused) {
 localStorage.setItem('aplayer_time', ap.audio.currentTime);
 localStorage.setItem('aplayer_index', ap.list.index);
 localStorage.setItem('aplayer_paused', 'false');
 }
}, 1000);

在页面加载时(无论是 Pjax 还是普通加载),尝试恢复状态:

1
2
3
4
5
const savedTime = localStorage.getItem('aplayer_time');
if (savedTime) {
 ap.seek(parseFloat(savedTime));
 if (savedPaused === 'false') ap.play();
}

这里还有一个细节:audio 元素必须在元数据加载后才能 seek,所以需要监听 loadedmetadatacanplay 事件。

总结

通过引入 Pjax 并配合精细的生命周期管理,我们成功在静态博客上实现了类似 SPA 的流畅体验:

  1. 音乐不间断:Footer 区域脱离了页面刷新的生命周期。
  2. 加载极速:只请求部分 HTML,带宽消耗更低。
  3. 体验降级:即使 Pjax 失败,完善的状态恢复机制也能保证用户体验不割裂。

折腾博客的乐趣往往不在于写文章本身,而在于通过解决这些具体的技术问题,窥探现代 Web 开发的冰山一角。

面向外企就业的 LeetCode/NeetCode 记录

2026-03-10 10:24:00

Featured image of post 面向外企就业的 LeetCode/NeetCode 记录

LeetCode 刷题方法

路线图

算法清单

外企基石,必须闭眼手撕:

  • 哈希表
  • 双指针
  • 滑动窗口
  • 二分查找
  • 链表
  • 二叉树(含二叉搜索树 BST)

工程进阶,拉开差距的关键:

  • 回溯算法(排列/组合/子集)
  • 堆/优先队列
  • 栈与队列
  • 图论基础(仅限 BFS / DFS / 拓扑排序)

高级护城河,面试冲刺期再看:

  • 动态规划(仅限 1D DP 和基础背包/最长公共子序列)
  • 字典树 (Trie)
  • 并查集 (Union-Find)
  • 前缀和技巧

题单

设计模式

1. 单例模式 (Singleton) —— C++ 面试必考八股

  • 这是什么:保证一个类只有一个实例,并提供一个全局访问点。
  • 外企考点:不仅会让你手写,还会结合 C++ 问你“线程安全的单例怎么写?”(Meyers Singleton,利用局部静态变量的特性)。
  • 如何融合进你的项目:你的 GPU 监控探针(Agent)运行在 5090 服务器上,它需要读取配置信息(比如本机的 IP、监控频率)。这个 ConfigManager 配置管理器,就必须是一个单例。你在简历上面试时可以说:“为了避免多次读取配置文件造成 I/O 浪费,我用 C++11 的 std::call_once(或局部静态变量)实现了一个线程安全的单例配置中心。”

2. 观察者模式 (Observer) —— 监控系统的灵魂

  • 这是什么:一个对象状态改变,所有依赖它的对象都会收到通知。(也就是发布-订阅机制)。
  • 外企考点:解耦。面试官常问:“如果我加一个新的监控维度,你的代码需要大改吗?”
  • 如何融合进你的项目:你的 5070 Ti 是一直在变化的(温度忽高忽低,显存忽大忽小)。你可以把本地的显卡作为一个 Subject(被观察者),把负责发送网络请求的模块、负责写本地日志的模块作为 Observer(观察者)。显存一旦超过阈值,主动“通知”这些模块报警,而不是让它们写个 while(true) 死循环去轮询。

3. 工厂模式 (Factory) —— 告别满屏的 if-else

  • 这是什么:把创建对象的具体逻辑隐藏起来,提供一个统一的接口。
  • 外企考点:考察你对开闭原则(对扩展开放,对修改封闭)的理解。
  • 如何融合进你的项目:你的调度系统未来不仅能收集 5090 的数据,可能还要收集 AMD 显卡,甚至普通 CPU 的数据。写一个 DeviceFactory,传入 “NVIDIA”,它就吐出一个调用 NVML API 的对象;传入 “CPU”,它就吐出一个读取 /proc/cpuinfo 的对象。

4. 策略模式 (Strategy) —— 调度系统的核心

  • 这是什么:定义一系列算法,把它们一个个封装起来,并且使它们可相互替换。
  • 外企考点:极其高频的设计题。比如“设计一个打车软件的计费模块”。
  • 如何融合进你的项目:你的核心是算力调度。怎么调度?你可以有“轮询策略(Round Robin)”、“最低显存优先策略(Least Loaded)”。把这些策略抽象成接口,运行时动态插拔。这就是顶级的基础设施架构。

刷题记录

NeetCode Roadmap

Contains Duplicate (LeetCode 217)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 最经典的哈希表题。用一个 Set 来记录我们见过的数字,一边遍历一边查这个数字在不在 Set 里。
class Solution:
 def containsDuplicate(self, nums: List[int]) -> bool:
 seen = set() # 创建一个空的哈希集合

 for num in nums:
 if num in seen: # 查字典:这个数字我之前见过吗?
 return True # 见过!直接返回 True
 seen.add(num) # 没见过,把它记在小本本上

 return False # 全都查完了也没重复,返回 False

# time complexity:O(n)
# space complexity:O(n)
1
2
3
4
5
6
7
# Pythonic 的一行解法,利用 set() 去重特性,直接比较原数组长度和去重后 Set 的长度。
class Solution:
 def containsDuplicate(self, nums: List[int]) -> bool:
 return len(nums) != len(set(nums))

# time complexity:O(n)
# space complexity:O(n)
  • 如果你的数组非常大,且重复元素大概率出现在很靠前的位置,解法 1 更优,因为它能“及时止损”。
  • 如果你的数组没有重复元素,或者重复元素在最后面,解法 2 更优,因为底层 C 语言的执行速度会碾压 Python 的 for 循环。
summary:
  1. 时间复杂度 (Time Complexity)
  • $O(1)$ 常数级:终极目标。代码特征:直接通过索引取值 nums[0],或者在字典/集合中查值 if key in my_dict:
  • $O(\log n)$ 对数级:极快。代码特征:二分查找。每次操作砍掉一半(如 while left < right: 配合 mid)。
  • $O(n)$ 线性级:外企最爱的标准解法。代码特征:一层无嵌套的 for 循环,从头到尾扫一遍。
  • $O(n \log n)$:通常是排序的极限。代码特征:代码里调用了 nums.sort(),或者用了归并/快速排序。
  • $O(n^2)$ 平方级:面试危险信号。代码特征:两层嵌套的 for 循环。面试官通常会让你把它优化到 $O(n)$。
  1. 空间复杂度 (Space Complexity)
  • $O(1)$:只新建了几个变量(如双指针 left, right),没有开辟随数据规模增大的新空间。
  • $O(n)$:新建了一个和原数组一样大的字典(哈希表)、集合(Set)或新数组。外企极度喜欢用空间换时间。
  1. Hash (Dict / Set) 用法速查

底层皆为哈希表,查找/插入的时间复杂度均摊为 $O(1)$。

  • dict (字典):存键值对。

  • 初始化:d = {}

  • 增/改:d[key] = value

  • 查:if key in d:

  • set (集合):存无序不重复元素。

  • 初始化:s = set()

  • 增:s.add(value)

  • 查:if value in s:

  1. Pythonic 循环

抛弃 for(int i=0; i<n; i++),只记以下三种:

  • 只取值for num in nums:
  • 只取索引for i in range(len(nums)):
  • 既要索引又要值for i, num in enumerate(nums):
  1. 核心标准库 (绝不重复造轮子)
  • collections.Counter (计数器)
  • 场景:完成词频/元素频率统计。直接替代 count[i] = count[i] + 1 循环。
  • 用法
1
2
from collections import Counter
count = Counter(nums)
  • collections.deque (双端队列)
  • 场景:做 BFS(广度优先搜索)必用。两头增删都是 $O(1)$,绝不用普通 list 做队列。
  • 用法
1
2
3
4
from collections import deque
q = deque([1, 2])
q.append(3) # 尾部进队
q.popleft() # 头部出队
  • heapq (优先队列 / 堆)
  • 场景:解决 “Top K” (前K个最大/最小) 问题。替代 C++ 的 std::priority_queue
  • 用法
1
2
3
4
import heapq
heapq.heapify(nums) # $O(n)$ 原地建堆
heapq.heappush(nums, 2) # 压入元素
val = heapq.heappop(nums) # 弹出最小值
Valid Anagram (LeetCode 242)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20

## 1
class Solution:
 def isAnagram(self, s: str, t: str) -> bool:
 if len(s)!=len(t):
 return False
 temp = [0] * 26

 for i in range(len(s)):
 temp[ord(s[i])-ord('a')]=temp[ord(s[i])-ord('a')]+1
 temp[ord(t[i])-ord('a')]=temp[ord(t[i])-ord('a')]-1

 for i in range(26):
 if temp[i]!=0:
 return False

 return True

# time complexity:O(m+n)
# space complexity:O(1)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 2 神奇 Python 小函数

from collections import Counter
return Counter(s) == Counter(t)

# time complexity:O(m+n)
# space complexity:O(1)

# 3 或者

return sorted(s) == sorted(t)

# time complexity:O(nlogn+mlogm)
# space complexity:O(1)/O(n+m)
summary:
  1. ord(char) —— 字符转索引
  • 用法index = ord(char) - ord('a')
  • 场景:将 ‘a’-‘z’ 映射到 0-25,配合数组做计数。
  • 相关用法chr(code)ord() 的逆运算,如 chr(97) 返回 'a',常用于将计算后的索引还原为字符。
  1. sorted(s) —— 排序法
  • 用法return sorted(s) == sorted(t)
  • 场景:快速验证变位词(Anagram)。两个字符串排序后应完全一致。
  • 相关用法list.sort(reverse=True)sorted() 返回新列表,支持所有可迭代对象;list.sort() 是列表的原地排序(无返回值)。
  1. Counter —— 词频统计
  • 用法return Counter(s) == Counter(t)
  • 场景:工程代码中一行解决词频统计,底层是 Hash Map。
  • 相关用法Counter(s).most_common(k)。返回前 k 个高频元素,解决 Top K 问题的神器。
  1. [0] * n —— 数组快速初始化
  • 用法count = [0] * 26
  • 场景:建立固定长度的计数桶(Bucket),比 dict 更快更省空间。
  • 相关用法[[0] * n for _ in range(m)]。构建二维数组(矩阵)的正确写法,必须用列表推导式以避免引用拷贝错误。
Two Sum (LeetCode 1)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 直接暴力双循环遍历
class Solution:
 def twoSum(self, nums: List[int], target: int) -> List[int]:
 for i in range(len(nums)):
 for j in range(i + 1, len(nums)):
 if nums[i] + nums[j] == target:
 return [i, j]
 return []
# time complexity:O(n^2)
# space complexity:O(1)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 双指针排序

class Solution:
 def twoSum(self, nums: List[int], target: int) -> List[int]:
 A = []
 for i, num in enumerate(nums):
 A.append([num, i])

 A.sort()
 i, j = 0, len(nums) - 1
 while i < j:
 cur = A[i][0] + A[j][0]
 if cur == target:
 return [min(A[i][1], A[j][1]),
 max(A[i][1], A[j][1])]
 elif cur < target:
 i += 1
 else:
 j -= 1
 return []

# time complexity:O(nlogn)
# space complexity:O(n)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Hash Map 两遍遍历
class Solution:
 def twoSum(self, nums: List[int], target: int) -> List[int]:
 indices = {} # val -> index

 for i, n in enumerate(nums):
 indices[n] = i

 for i, n in enumerate(nums):
 diff = target - n
 if diff in indices and indices[diff] != i:
 return [i, indices[diff]]
 return []
# time complexity:O(n)
# space complexity:O(n)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Hash Map 一遍遍历
class Solution:
 def twoSum(self, nums: List[int], target: int) -> List[int]:
 prevMap = {} # val -> index

 for i, n in enumerate(nums):
 diff = target - n
 if diff in prevMap:
 return [prevMap[diff], i]
 prevMap[n] = i

# time complexity:O(n)
# space complexity:O(n)
summary:
  1. nums.index(val) —— 列表查找
  • 用法idx = nums.index(target - n)
  • 场景:查找值对应的索引。面试大忌:在 for 循环里用它,时间复杂度直接退化为 $O(n^2)$。因为它底层是线性扫描。
  • 相关用法hash_map[key]。面试官想看的是用哈希表将查找降维到 $O(1)$。
  1. Two Pointers —— 双指针法
  • 用法l, r = 0, len(nums) - 1,配合 while l < r
  • 场景仅适用于已排序数组。如果是乱序数组求 Two Sum,排序需 $O(n \log n)$ 且会打乱原始索引(需额外记录),不如哈希表法 $O(n)$ 优。
  • 相关用法nums.sort()。双指针的前置条件通常是数组有序。
  1. range(i + 1, n) —— 内层循环起点
  • 用法for j in range(i + 1, len(nums)):
  • 场景:暴力解法(Brute Force)中,内层循环必须从 i + 1 开始。
  • 相关意义:两层含义:一是避免自己加自己(题目限制);二是去重,避免计算了 (A, B) 又计算 (B, A)
  1. enumerate(nums) —— 枚举遍历
  • 用法for i, n in enumerate(nums):
  • 场景:需要同时获取索引 (index) 和值 (value)。这是 Two Sum 这类题的标准起手式。
  • 相关用法range(len(nums))(只拿索引)、for n in nums(只拿值)。不要在需要索引时手动维护一个 count 变量,太土。

words list

  • integer 整数
  • distinct 不同的
  • duplicate 重复的
  • constraint 约束条件
  • approach 方法
  • time/space complexity 时间/空间复杂度
  • valid 有效的
  • anagram 字谜(回文构筑法)
  • indices 索引
  • assume 假设
  • pitfall 陷阱
  • Brute Force 暴力解法
  • sublist 子列表
  • group 分组
  • enumerate 枚举
Group Anagrams (LeetCode 49)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# sorting 
class Solution:
 def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
 res = defaultdict(list)
 for s in strs:
 sortedS = ''.join(sorted(s))
 res[sortedS].append(s)
 return list(res.values())
# time complexity:O(m∗nlogn)
# space complexity:O(m*n)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# hashmap
class Solution:
 def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
 res = defaultdict(list)
 for s in strs:
 count = [0] * 26
 for c in s:
 count[ord(c) - ord('a')] += 1
 res[tuple(count)].append(s)
 return list(res.values())

# time complexity:O(m*n)
# space complexity:O(m*n)
summary:
  1. defaultdict(list) —— 默认字典
  • 用法res = defaultdict(list)
  • 场景:分组(Group)问题标配。当访问不存在的 key 时,自动创建一个空 list,省去了 if key not in d: d[key] = [] 的判断。
  • 相关用法defaultdict(int)。用于计数,默认值为 0。
  1. tuple(mutable_obj) ——不仅是转换
  • 用法key = tuple(count)
  • 场景字典的 Key 必须是不可变类型 (Immutable)。列表 (List) 是可变的,不能作为 Key,必须转化为元组 (Tuple) 才能作为哈希表的键。
  • 相关用法frozenset。不可变集合,也可以做 Key。
  1. list(d.values()) —— 字典值导出
  • 用法return list(res.values())
  • 场景:题目只需要返回分好组的列表(List of Lists),不需要 Key。
  • 相关坑点:Python 3 中 d.values() 返回的是一个视图对象 (View),必须用 list() 强转才能符合题目要求的返回类型 List[List[str]]
  1. ''.join(sorted(s)) —— 字符串标准化
  • 用法key = "".join(sorted(s))
  • 场景:将字符串排序并重组为用作 Key 的不可变对象。sorted(s) 返回的是列表 ['a', 'e', 't'](可变,不可做 Key),必须用 join 拼回字符串 "aet"
  • 相关用法tuple(sorted(s))。如果不拼回字符串,转成元组也可以做 Key。

tips

1. 语言与阵地

  • 语言死绑 Python:追求极简 API 和开发速度,避开 C++ 繁琐的模板代码。
  • 平台死绑美区:只在 LeetCode.com 刷,强迫适应全英文题目与约束条件。

2. 路线与开销

  • 唯一指定路线:只刷 NeetCode Roadmap。打通它 = 自动通关 Blind 75 + NeetCode 150。
  • 绝不打周赛:外企不考竞赛级偏门题,别浪费周末断联日。
  • 零氪金原则:绝不买 $297 NeetCode Premium。只在收到面试通知的前一个月,买 $35 LeetCode 官方会员突击公司专属题库 (Company Tags)。

3. 核心刷题 SOP (15分钟法则)

  • Step 1 (闭卷 15 分钟):拿到题自己想,没思路或写不出立刻停手,绝生死磕。
  • Step 2 (开卷偷师):看 NeetCode 免费视频,学 Python 模板和英文思路 (Think Out Loud)。
  • Step 3 (半闭卷手敲):关掉视频,凭记忆自己敲到 AC。允许随时查 Python API(如字典操作、内置函数),现阶段不要为默写语法浪费时间。

4. 兜底退路与国内大厂补丁 (Pivot 方案)

  • 唯一需要加练的(考前 1 个月突击):外企不考中间件的死记硬背。但如果在 2027 年春招/秋招前决定转面国内互联网业务岗,需额外花 1个月 狂背“国产特色八股文”:MySQL 底层(B+树索引)、Redis(缓存穿透/雪崩)、以及各类高并发锁机制。 仅作为临时补丁,勿在日常备战中分散精力。

5. NeetCode 的正确打开方式

NeetCode 的 Visualize the algorithm step by step 功能非常便于理解算法流程,至少对我来说,可视化地过一遍算法流程,就很清晰了。

链接

2027秋招外企计划

2026-03-06 13:50:00

Featured image of post 2027秋招外企计划

前言

经历了一学期的研究生生活,lihan 变得保守了很多,也不再执着于读博与当教授,外企的技术岗是当前的首选目标了。兼顾当前课业与课题组压力以及身心健康,制定了一个目标明确的学习计划,计划的核心是提升自己的技术能力,兼顾项目开发与实习经历以及英语能力,以健康的身体和积极的心态迎接秋招。

计划 ———— Lihan 的外企核心 SDE 备战「自动机」执行纲领

🎯 终极目标 (The North Star): 2027年秋招/暑期实习斩获一线外企(Microsoft, Amazon, NVIDIA等)核心 Infra/后端 开发岗 Offer,实现 965 工作制、高时薪,拥有绝对的生活主导权。


📅 第一部分:宏观路由与异常接管 (Macro Roadmap)

主线任务由算法与 C++/AI Infra 工程双轮驱动。前端开发仅作为前置 3 天的工具测试,不占主线资源。

📌 前置点火任务:MVP 启动冲刺 (限时 3 天)

  • 任务:借助 AI IDE 快速完成一个基于 Next.js 的 Todo WebApp,仅用于记录本计划的状态流转。
  • 强制约束:限时 3 天。绝不系统性学习官方文档,超时一分钟也必须强制封板,立刻转入算法与 C++ 主线。

🗺️ 两年半推进时间线

  • 阶段一:基建与探路期 (研一下,至 2026 年 8 月)

  • 算法:Python 刷通 LeetCode 经典 150 题(按分类),训练全英文“边写边说”能力。

  • 工程:启动「异构 GPU 算力监控系统」。用 C++ 配合 NVML 写出轻量级数据采集探针,跑通本地与远端 5090 服务器的底层硬件状态拉取。

  • 🛑 异常接管 (期末突击):学期末提前划出 3 周 纯粹用于期末复习。此期间【高能输出】状态冻结,一切为了保住 GPA,考完立刻解冻。

  • 阶段二:并发与重构攻坚期 (研二上,2026.09 - 2027.02)

  • 算法:二刷错题本,定向突击目标外企高频题库。

  • 工程:引入 Python FastAPI 或 Go 重构通信网关,解决跨网络并发与数据持久化。

  • 🛑 异常接管:同样预留 3 周 应对研二上期末考试。

  • 阶段三:狙击与收网期 (研二下,2027.03 - 2027.09)

  • 动作:精修全英文简历,开启 BQ(行为面试)的英文 Mock Interview。海投外企暑期实习。

  • 终局:依靠 3 个月的暑期实习斩获 Return Offer,或携极具深度的 C++ 底层项目降维打击秋招。


⚙️ 第二部分:核心状态机引擎 (State Machine Logic)

你的每一天只会处于以下五种状态之一,绝不允许出现“边学边玩”的模糊中间态。

  • 🟢 状态 A [高能输出]: 全神贯注执行算法刷题与 C++/Python 硬核底层开发。断绝外部通讯。
  • 🔵 状态 B [防御敷衍]: 处理导师横向任务与学校日常课程。大脑降频,能水则水,绝不多花一分精力。
  • 🟡 状态 C [合法狂欢]: 当日设定的【高能输出】任务达标后自动触发。毫无负罪感地打游戏、看剧。
  • 🟣 状态 D [战略停机]: 提前规划好的不插电日(如周日)。去山里徒步摄影,彻底抽离。禁止临时起意的旷工。
  • 🔴 状态 E [兜底模式]: 极度疲惫时触发。完成“最小可行性任务”(如盲打 1 道 LeetCode 简单题)后直接睡觉,保持惯性不断裂。

⏳ 第三部分:每日资源调度表 (Daily Scheduler 8:30-24:00)

针对每周只有四节课的现状,将上课时间直接视为【🔵 防御敷衍】模块。如果在课上,则挂机听讲,大脑后台构思代码或复习计网/OS理论。

时间块 状态机 核心执行逻辑与输入/输出 时长
08:30 - 09:30 系统冷开机 洗漱、早餐。 浏览开源社区或科技资讯,平缓启动大脑。 1.0h
09:30 - 11:30 🟢 高能输出 算法与理论底座 (Python/CS理论)
精力峰值时段。刷 1-2 道 LeetCode,强制英文口述思路。 2.0h
11:30 - 14:00 物理断电 午餐 + 深度午休。 雷打不动睡 40-60 分钟。 2.5h
14:00 - 17:00 🟢 高能输出 AI Infra 底层工程实战 (C++)
操作本地高配机器与远端服务器,死磕内存管理与网络并发。 3.0h
17:00 - 19:00 硬件维护 体能训练与晚餐。 健身房或操场,切换轨道,缓解颈椎压力。 2.0h
19:00 - 21:30 🔵 防御敷衍 导师横向与学校课业
降维处理杂活与作业。到点准时停手。(若此时在上课,则此模块平移至上课时段) 2.5h
21:30 - 23:00 🟢 / 🟡 判定 复盘或触发合法狂欢
若当日高能任务 100% 达成,立刻启动游戏;未达成则做最后冲刺与英文 Bug 复盘。 1.5h
23:00 - 24:00 内存清理 降温与休眠。 洗澡、看闲书、刷 B 站。24:00 强制熄灯休眠。 1.0h

学习记录

招聘信息

以下记录就业相关信息,以备查阅:

  • 2026 米哈游 城市专场 【程序/测试】
    • 日期:2026年03月25日 19:00-20:30(GMT+8)
    • 面向:28届 实习生
    • 地点:西安+西工大长安校区宣讲 启真楼1楼104
    • 岗位:测试
    • 链接:https://mp.weixin.qq.com/s/w9bp4LJTU6QW6lG9kIsHaw
    • 宣讲链接:https://mp.weixin.qq.com/s/Ds46VVli7xdG3VKowFYGKQ
    • 参与:线下面试前问卷+网申简历

TODO Web APP 开发

利用 AI 同时熟悉现代前端开发框架,快速搭建一个 Todo Web APP,作为本计划的状态流转记录工具。

现代前后端WEBAPP开发_方向系统学习记录

2026-02-06 01:00:00

Featured image of post 现代前后端WEBAPP开发_方向系统学习记录

现代前后端 WEBAPP 开发_方向系统学习记录

1. 基础概念

1.1 现代前后端 WEBAPP 思维

  • 前端与后端高度解耦,通过 API/HTTP/Socket 通信
  • 前端使用组件化、模块化框架(React + Next.js + TS)
  • 后端服务微服务化或插件化,每个工具独立容器化
  • 统一开发环境与生产环境,避免“本地可用、服务器报错”

1.2 核心技术理解

技术 作用
Windows 操作系统与 GUI 编辑器环境
WSL2 Linux 核心环境,和服务器一致
nvm Node 版本管理器,保证项目 Node 版本一致
Node.js JS/TS 运行环境
pnpm 高效包管理器,安装依赖与管理脚手架
Next.js + React + TS 现代前端框架,支持组件化与页面路由
Docker 插件化工具容器化,保证隔离和可移植性

2. 学习路线规划

阶段 学习目标 依赖
技术工具 [x] 开发环境搭建:Windows + WSL2 + VS Code Remote-WSL,Docker 容器化思维 WSL2 安装与配置、VS Code Remote-WSL、Docker&&K8s学习笔记
[ ] Node.js + TypeScript 入门:Node 运行机制、nvm 版本管理、npm / pnpm / yarn 使用、TypeScript 基础 Nodejs+TypeScript_技术工具学习记录
[ ] Next.js 现代前端开发:React + Next.js + TS、组件化开发、页面路由与 API 路由、插件化平台思维 Next.js 官方文档、React 官方文档、UI 组件库(Ant Design / Tailwind UI)、Docker 容器化工具
基础知识 了解前后端解耦与现代 WEBAPP 架构 HTTP/HTTPS、RESTful API、GraphQL、微服务与插件化服务思维
掌握模块化与依赖管理概念 CommonJS / ES Module、模块加载机制、包管理器原理
熟悉前端组件化设计与状态管理 React 组件设计模式、状态管理(Zustand / Redux / Context API)
产出学习 能搭建插件化 WEBAPP 平台,整合前端页面与 Docker 插件工具 Node + Next.js + Docker API 调用、前端组件化开发、插件化工具集成
通过小项目练习技术链 小工具插件(音乐播放器、计算器、小游戏、AI Agent)容器化 + 前端调用
建立完整开发与部署流程能力 Windows → WSL2 → nvm → Node → pnpm → Next.js → Docker 插件化平台

3. 技术依赖


4. 相关资料链接


5. 学习过程记录

2026-02-06

  • 理解现代前后端解耦 + 插件化平台思想
  • 搭建 Windows + WSL2 开发环境,VS Code Remote-WSL 测试

2026-02-07

  • 安装 nvm,切换 Node 18,理解 Node 运行机制
  • 熟悉 npm / pnpm / yarn 区别,选择 pnpm 作为包管理器

2026-02-08

  • 使用 pnpm create next-app 初始化 Next.js + TS 项目
  • 理解模块系统:.js / .mjs / CJS / ESM
  • 开始规划插件化工具目录结构

2026-02-09

  • 每个工具独立写 Dockerfile,测试容器启动与端口映射
  • 前端通过 API 路由调用 Docker 插件,实现初步平台功能
  • 整个开发流程在 WSL2 + Docker 环境下稳定运行

6. 技术流程图

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

Windows (GUI + 编辑器)
WSL2 (Linux 环境)
nvm (Node 版本管理)
Node.js (JS/TS 运行)
pnpm (包管理与项目脚手架)
Next.js + React + TypeScript (前端页面 + 组件化开发)
Docker (插件化工具容器化,隔离服务)
现代前后端 WEBAPP 工具平台

核心思维:环境统一 → Node 版本稳定 → 高效依赖管理 → 组件化前端 → 容器化服务 → 插件化平台


【Node.js + TypeScript】学习笔记

2026-02-06 01:00:00

Featured image of post 【Node.js + TypeScript】学习笔记

【Node.js + TypeScript】实操笔记

1. 简单介绍

  • Node.js 是基于 V8 引擎的 JavaScript 运行环境,可在服务器端执行 JS/TS
    • nvm 是 Node 版本管理工具,方便切换不同项目的 Node 版本
    • pnpm 是高效的包管理器,替代 npm/yarn,节省磁盘空间和安装时间
  • TypeScript 是 JavaScript 的超集,提供类型系统和编译时检查
  • 常见应用场景:Web 后端服务、CLI 工具、前端构建工具、插件化平台

2. 前置技术依赖

  • 熟悉 JavaScript 基础语法
  • 对 Linux / WSL2 命令行基础了解

3. 环境配置

  • 系统环境:Windows + WSL2(推荐 Linux 内核一致性)
  • Node 版本管理:nvm 安装 Node,切换版本
  • 包管理器:推荐 pnpm,安装与验证:

  • VS Code 配置

    • 安装 Remote-WSL 插件
    • 安装 TS / Node 插件,启用自动类型提示

4. 基础用法

4.1 Node.js 基础

4.1.1 Node.js 安装与基于 nvm 的版本管理

4.1.1.1 Node.js 安装
  • 类似于 python 的 conda,nvm(Node Version Manager)是一个用于管理多个 Node.js 版本的工具。它允许你在同一台机器上安装和切换不同版本的 Node.js,非常适合开发者在不同项目中使用不同版本的 Node.js。

  • 类似于 python 的 pip,pnpm 是一个高效的 JavaScript 包管理器,提供了更快的安装速度和更少的磁盘空间占用。它通过使用符号链接来共享依赖项,从而避免了重复安装相同的包。

Node.js 官网 提供了 Node.js 的安装命令,在选择了 系统环境Node 版本管理器包管理器Node.js 版本后,可以直接复制命令在终端中执行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 以在 WSL(Ubuntu24.04)下安装使用 nvm 进行版本管理并使用 pnpm 管理包的 Node.js 25.x 为例:

# 下载并安装 nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash

# 代替重启 shell
\. "$HOME/.nvm/nvm.sh"

## 若无效报错则重启 shell 继续后面的步骤

# 下载并安装 Node.js:
nvm install 25

# 验证 Node.js 版本:
node -v # Should print "v25.6.1".

# 安装 Corepack:
npm install -g corepack

# 下载并安装 pnpm:
corepack enable pnpm

# 验证 pnpm 版本:
pnpm -v

值得注意的是,nvm 和 pnpm 的安装步骤可能会因操作系统和环境的不同而有所差异,详见[nvm 和 pnpm 的安装注意事项](# 7.1-nvm-和-pnpm-的安装注意事项)。

4.1.1.2 Node.js 常用命令
4.1.1.3 nvm 常用命令
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 列出已安装的 Node.js 版本及别名
nvm ls

# 列出在线可安装的 Node.js 版本
nvm ls-remote

# 安装指定版本的 Node.js
nvm install <version>
## nvm install 25

# node 为最新版本的别名
nvm install node

# 设置别名
nvm alias my_alias v14.4.0

# 使用指定版本的 Node.js
nvm use <version>

# 卸载指定版本的 Node.js
nvm uninstall <version>

# 显示当前使用的 Node.js 版本
nvm current

# 获取 nvm 的安装路径
nvm which <version>

# 在指定版本下运行简单命令
nvm run <version> -- <command>
# 在指定版本下运行复杂命令
nvm exec <version> <command>

4.1.2 基于 pnpm 的包管理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14



---

## 5. 实践案例

* **搭建小型 API 服务**

 ```ts
 import express, { Request, Response } from 'express';
 const app = express();
 app.get('/ping', (req: Request, res: Response) => res.send('pong'));
 app.listen(3000, () => console.log('Server running on port 3000'));
  • Docker 容器化运行

    1
    2
    3
    4
    5
    6
    
    FROM node:18-alpine
    WORKDIR /app
    COPY package.json pnpm-lock.yaml ./
    RUN npm install -g pnpm && pnpm install
    COPY . .
    CMD ["npx", "ts-node", "src/index.ts"]
    
  • 实践经验

    • 使用 pnpm 速度快、占用少
    • 在 WSL2 中开发,Docker 与 Linux 环境一致

6. 常见问题与解决办法


7. Tips

7.1 nvm 和 pnpm 的安装注意事项

由于 Node.js 25.x 版本的最新更新,Corepack 在 Node.js 25.x 中已被移除,因此需要手动安装 Corepack 来使用 pnpm 包管理器。

在 Node.js 25.x 中安装 pnpm 的步骤比 Node.js 24.x 多了一步:

``bash

安装 Corepack:

npm install -g corepack

1
2
3
4
5
6
7

---

## 8. 参考资料

- [Node.js 官网](https://nodejs.org/)
- [nvm GitHub 仓库](https://github.com/nvm-sh/nvm)