Git 命令/配置

查看 git 详细命令描述

1
2
# 会在浏览器打开本地离线文档
git help [option]

查看仓库配置文件

查看本地仓库配置

1
2
cd [项目路径]
cat ./git/config

查看全局仓库配置

1
git config --global -l

设置用户名/邮箱

用户信息会体现在 commit 提交信息中

设置本地用户

1
2
git config user.name "xxx"
git config user.email "xxx@.com"

设置全局用户

1
2
git config --global user.name "xxx"
git config --global user.email "xxx@.com"

如果本地用户没有设置会优先使用全局的用户设置, 如果存在多个用户可以设置本地用户信息

git add 做了什么

执行 git add . 会将工作区的文件加入到暂存区,体现在 git objects 中会增加一个 .git/objects/[文件sh1前2位]/[文件sh1第3位到40位]] 的文件。[sha1] 的计算逻辑是:

1
2
3
h = sha1()
h.update("blob [文件长度]\0")
h.update('[文件内容]')

可以使用 cat-file 命令查看文件的 类型和内容,可以看见用户文件的类型的 blob,这是第一种 object 类型

1
2
3
4
5
6
7
8
9
10
git cat-file -t [sha1]  #查看类型
git cat-file -p [sha1] #查看内容
git cat-file -s [sha1] #查看文件长度

#查看当前目录有所文件的信息
git ls-files -s

# 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a/aa.js
# 文件权限 对象名称 文件名称

解析 objects 文件的原始内容

1
2
3
4
5
6
7
8
9
10
11
12
const fs = require("fs");
const zlib = require("zlib");

// 创建解压流
const inflate = zlib.createInflate();
const readStream = fs.createReadStream(
"./c6dd0164fe0eb4fde767f9e731a6c8ade0b69f"
);
const writeStream = fs.createWriteStream("bbb.txt");

// 处理数据流
readStream.pipe(inflate).pipe(writeStream);

git commit 做了什么

查看执行 commit 之后新增文件的类型为 commit,commit 信息通过 object 文件保存,这是第二种 object 类型

1
git cat-file -t [sha1] #commit

查看 commit 后生成文件的内容

1
2
3
4
5
6
7
8
git cat-file -p [sha1]

#tree de9ca8d60d61e2dbff58571e243256f0d084e030 tree对象文件名字(sha1)
#parent ef911d46ddddb69cfb0149bbc7217f582c668f21 上一次提交的文件名字(sha1)
#author sunzhiqi <sunzhiqi@live.com> 1724649175 +0800 作者 邮箱 时间戳 时区
#committer sunzhiqi <sunzhiqi@live.com> 1724649175 +0800

#2 提交的文件内容

继续查看 tree 文件的内容, tree 类型描述提交的文件结构,这是第三个 object 类型

1
2
3
4
5
6
7
8
9
git cat-file -p [sha1]

#100644 blob b44836b41abbfc0640d4dd88fe587a9a145e5203 LICENSE
#100644 blob b9c2b056b82135218b26420e8479c56554b54afb README.md
#100644 blob 4b9a8a29f6956ea1742e42354c5ba36fe717a2ed a.txt
#100644 blob 9f478040b9109d4b1d25ad7f11528ed9a682f063 b.txt

# 如果提交内容中有文件夹,那么会有一个单独的 tree 描述文件夹的信息
#100644 tree xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx folder

另外 commit 操作会影响 HEAD 指针的指向

1
2
3
4
5
6
# 查看HEAD指向的分支
cat .git/HEAD #ref: refs/heads/master

# 查看分支对应的文件

cat .git/refs/heads/master #ref: f45e2d0b45ca63806d263ae31276a3fbe4a7229d

Git 通过 3 种类型的 object files 组织文件

  • commit 类型,记录了提交的快照信息
    提供 parent 指向了上一个提交 hash
    提供 tree 指向了本次提交的快照

  • tree 类型,记录了目录结构
    每次提交都会生成一个新的 tree 文件
    tree 文件记录了目录下的文件信息,可能是 blob 文件,也可能是另一个 tree

  • blob 类型,记录了文件内容
    blob 文件可以在不同的 tree 之间被复用

文件状态流转

  • Untracked 未跟踪, 未被提交到暂存区
    使用 git add . 将文件加入到暂存区

  • Modified 已修改,提交暂存区的文件在工作区被修改了
    用 git add . 将文件提交修改到暂存区

  • Staged 已暂存,提交到暂存区的文件
    git commit 将暂存区的文件提交到仓库区

  • Unmodified 代码仓库中的状态

diff 查看文件差异

1
2
3
4
5
# 对比工作区和暂存区
git diff

# 对比暂存区和仓库区
git diff --cached

branch operations

分支的本质是指向 commit 的指针, 使用自定义名称代指 commit hash

git checkout 可以用于切换分支,也可以用于切换到某个 commit, 当使用这个命令后会提示正在处于一个分离头指针的状态。

1
git checkout [hash]

如果相对当前 commit 修改需要依附于一个分支,可以使用 git switch -c [branchName], 在旧的版本中可以使用 git checkout -b [branchName]

fast-forward

从 master 切换到 feature 分支,如果 master commit 没有变化, 那么可以直接将 master 的指针指向 feature

如果分支已经合并,可以使用 git reset ORIG_HEAD 回到 merge 之前的 commit

commit 回滚后修改的文件会在本地显示为 untracked 或 modified 也就是在 feature 分支中修改或新增的文件。

如果需要丢弃这些修改可是使用 git restore [filename]git restore . 丢弃所有修改。

如果有添加在暂存区的文件,可以使用 git restore --staged . 丢弃暂存区的修改

3 way merge

如果 feature 分支落后于 master 分支,也就是在 feature 分支上提交了 commit 的同时, master 分支也提交了 commit, 那么在合并的时候会产生一个新的 commit 记录这两个分支的合并。查看这次新分支的提交内容

1
2
3
4
5
6
7
8
git cat-file -p 4ba28b350b0a8f0b

tree 444ca23d83cc70878ddb01e5288a4322ded5ac0c
parent 57086086b9bd92402227d58e7c6b87e285376910
parent 720f3a44023361a4bad5586fe769ccc50f1bc1ab
author sunzhiqi <sunzhiqi@live.com> 1744018132 +0800
committer sunzhiqi <sunzhiqi@live.com> 1744018132 +0800

可以看到有两个 parent 指向,分别是 feature 最新的 commit hash 和 master 分支的合并之前的 commit hash。

conflict 3 way merge

如果两个分支都修改了同一个文件,那么在合并的时候会产生冲突。

1
2
3
4
5
<<<<<< HEAD
master 的内容
=======
feature 的内容
>>>>>> feature

手动解决冲突后使用 git add . 提交暂存区, 然后执行 git commit 完成合并操作

rebase

rebase 会将当前分支落后的于 master 分支的 commit 整合到当前分支,这样当前分支包含了 master 分支的最新 commit, 所以再次在 master 分支上执行 merge 操作就可以实现 fast-forward 合并。

rebase 操作通常避免在 master 分支上执行,并且会修改已经提交的 commit hash,这可能会导致于已经提交到远程的 commit 冲突,多人协作中应该禁止。

1
2
3
4
git checkout feature
git rebase master
git checkout master
git merge feature

remote branch

1
2
3
4
5
6
git branch -r  # 查看远程分支
git branch -a # 查看本地和远程分支

git remote add origin [url] # 关联远程仓库
git remote -vv # 查看远程仓库
git remote show origin # 查看远程仓库详情
fetch

fetch 操作会将远程分支的最新 commit 信息下载到本地,但是不会自动合并到当前分支,也就是不会移动 HEAD 指针。

如果远程分支删除,需要本地同步可以使用以下命令:

1
2
3
4
git fetch --prune

# 或者
git remote prune

fetch 操作会使用一个非常重要的文件,**.git/FETCH_HEAD**, 它记录所有远程分支的最新 commit 信息。并且当 git fetch 执行的时候会将当前分支信息置于文件顶部,其余的分支信息排在后面,这样 git pull 命令会利用最前面的分支信息合并分支

pull

git pull 操作会修改/创建 ORIG_HEAD 指针,并且移动 HEAD 指向 merge 之前的 commit 可以用于回滚操作。

push
1
2
3
4
5
6
7
8
9
10
11
12
13
14
git push origin master

# 删除远程分支
git push origin --delete feature
git push origin -d feature

# 推送本地标签到远程
git push origin --tags

# 关联远程分支
git branch --set-upstream-to=origin/feature feature

git branch --set-upstream origin feature
git branch -u origin feature
cherry-pick

当分支无法合并,需要把某个分支的 commit 合并到另一个分支,可以使用 cherry-pick。

1
2
3
4
5
# 切换到目标分支
git cherry-pick [commitID]

# 合并多个 commit
git cherry-pick [commitID1] [commitID2]

tag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
git tag v1.0

# 创建带注释的标签 -a 表示创建带注释的标签
git tag -a v1.0 -m 'tag message'

# 指定任意commit打标签
git tag -a v1.0 [commitID]

# 查看标签
git tag

# 查看标签详情
git show v1.0

# 删除标签
git tag -d v1.0

git tag v1.0 创建轻量标签存放在 .git/refs/tags 目录下

git tag -a v1.0 -m 'tag message 除了保存在 .git/refs/tags 目录下,还会创建一个对象文件保存在 .git/objects 下,类型是 tag, 这是第四种类型的 object,同时保存信息还包括注释,时间,作者

submodule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 添加子模块
git submodule add [url]
git submodule add ./path/to/submodule

# 首次 clone 项目不会包含子模块的内容
# 进入到子模块目录 执行
git submodule update --init --recursive

# 再次更新子模块
git submodule update --remote


# 删除子模块
git rm submodule
rm -rf .git/modules/submodule

# 从 .gitmodules 中移除该 submodule
vim .gitmodules

# 从 .git/config 中移除该 submodule
vim .git/config

我提交了什么

1
2
git show
git log -n1 -p

想修改最近的提交信息

提交信息后(commit),发现信息写错了,想修改最近一条提交信息(如果想修改当前提交以前的信息)。

1
2
3
4
git commit --amend --only [filename] -m 'update'

# 会弹出交互式命令行手动提交
git commit --amend --only [filename]

--only 参数非常重要,只对当前正在修改的文件进行提交,而不包括暂存区中的其他文件。

怎么看是否落后与主分支

1
2
git fetch origin
git status
1
2
3
4
5
6
7
8
9
10
11
12
13
# oneline  提交显示为一行
# graph 左侧显示一个 ASCII 图形,用于展示分支和合并的关系
# decorate 提交信息中附加上相关的分支、标签等引用名称

# 查看远程分支所有提交历史
git log --oneline --graph --decorate origin/master

# 查看所有分支的提价历史
git log --graph --all

# 指定分支

git log --graph origin/master master

从分支上可以看出,主分支并没有 fff 的提交,最终合并在本地的 master 分支

取消上次提交中的某个文件

一次提交中包括, a.txt, b.txt 现在想取消 a.txt 的提交,也就是恢复为上一次的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看上一次提交文件的内容
git show [commitID]:a.txt

# 将工作区中的文件恢复为上一次提交的内容
git checkout HEAD^ a.txt

# 也可以使用 commitId
git checkout [commitId] a.txt

# 将恢复的文件加入暂存区 -A 参数可以将删除文件也从暂存区删除
git add .

# 提交修改到暂存区,沿用上一次的ID,并且不修改提交描述
git commit --amend --no-edit

如果当前的的历史已经提交到远程,由于并没有改变提交历史,所以可以安全的 git push -f

删除 上一次/多个 Commit

极不建议使用,虽然可以用 reset 重置为上一级的状态,但是会丢失当前的修改,而且会破坏提交历史。

1
2
3
# HEAD^ 上一个提交状态
# hard 改变暂存区和工作目录到指定的提交
git reset HEAD^ --hard

使用 soft 参数可以避免工作区被回滚,但提交历史已经被改变,如果你的分支还会被其他人使用,多人写作中会有安全问题。避免在公共分支中使用 git reset,

1
git reset HEAD^ --soft

对于公共分支唯一安全的做法是使用 revert

1
2
3
4
5
git revert [commitId]

# 如果有冲突,解决冲突后执行
git add .
git revert --continue

修改 commit 之后不能 push

  • 可能你已经将历史 push 到远程,但是又修改了本地的最近的 commit, 导致 git 理解为你的 commit 和 远程的不同需要先合并分支。
    尽可能避免修改一个已经推送到远程的历史,如果必须要这样做,只能使用 git push -f

reset 之后想找回

1
2
3
# 使用reflog查看头指针的移动历史
git reflog
git reset --hard [commitId]

提交文件的一部分/分别提交到两个 commit

1
2
3
4
5
6
7
8
9
10
# 使用交互式命令行操作
git add -p [filename]

#y - 将该 hunk 添加到暂存区
#n - 不将该 hunk 添加到暂存区
#s - 拆分当前 hunk 成更小的块
#e - 手动编辑当前 hunk
#q - 退出暂存模式
#a - 将当前文件的所有剩余 hunk 添加到暂存区
#d - 不将当前文件的任何剩余 hunk 添加到暂存区

使用 e 手动编辑的模式时:

  • 如果想要取消删除行的修改,需要将行前的 - 替换为 空字符
  • 如果想要取消新增行的修改, 需要将 + 行整体删除

添加到暂存区中后执行 commit 操作,接着再次执行一次 git add . 提交剩余的修改。

有临时工作又不想提交当前的修改

1
2
3
4
5
6
7
8
9
10
11
12
# 使用 stash 命令暂存当前修改
git stash
git stash push -m "描述" #为stash添加描述

# 切换到需要工作的分支进行开发
git checkout [branch]
# 在当前分支中想要合并最新的主分支,也可以使用暂存功能
git pull

# 工作结束后切换回原来的分支,并从暂存中取出内容
git checkout [main branch]
git stash pop

仅 stash 未暂存的更改,而保留暂存区的内容

1
2
3
4
5
6
# 已经git add 的内容不想放到stash中
# 只把刚在工作区修改的内容放到stash中
git stash --keep-index

# 如果有文件还没有git add添加过,想要stash这些文件
git stash -u

微前端 ④ qiankun

什么是 qiankun

qiankun 是一个微前端的解决方案,对 single-spa 进行了封装。

执行流程

CSS 沙箱如何实现

strictStyleIsolation 严格模式, 通过 ShadowDOM 实现,将子应用最外层的元素升级为 ShadowDOM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var containerElement = document.createElement("div");
containerElement.innerHTML = appContent;
var appElement = containerElement.firstChild;
if (strictStyleIsolation) {
if (supportShadowDOM) {
var innerHTML = appElement.innerHTML;
appElement.innerHTML = "";
var shadow;
if (appElement.attachShadow) {
shadow = appElement.attachShadow({
mode: "open",
});
} else {
shadow = appElement.createShadowRoot();
}
shadow.innerHTML = innerHTML;
}
}

另一种是开启了 experimentalStyleIsolation 实验性沙箱配置,原理是尝试通过遍历 sheet 样式表的每一条样式,为每条样式添加一个私有化的选择器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 给子应用的最外层元素设置一个私有属性,用于添加选择器
// css.QiankunCSSRewriteAttr 默认私有化属性 data-qiankun
// appInstanceId 子应用的 name 属性
appElement.setAttribute(css.QiankunCSSRewriteAttr, appInstanceId);

// 创建私有化的选择器 prefix =>div[data-qiankun="setting"]
var prefix = ""
.concat(tag, "[")
.concat(QiankunCSSRewriteAttr, '="')
.concat(appName, '"]');

// 获取并便利所有的 style 标签
var styleNodes = appElement.querySelectorAll("style") || [];
_forEach(styleNodes, function (stylesheetElement) {
// 创建新的style标签,并将当前style中的样式复制到新标签中
var styleNode = document.createElement("style");
styleNode.appendChild(
document.createTextNode(stylesheetElement.textContent || "")
);

// 获取 CSSStyleSheet 对象,并将 cssRules 转为数组, 遍历并重写规则
var sheet = styleNode.sheet;
var rules = arrayify(sheet.cssRules);
var css = "";
rules.forEach(function (rule) {
switch (rule.type) {
case RuleType.STYLE:
/**
1 对于 html body :root 几个根元素
会直接用 prefix 替换掉,防止对基座应用样式污染

2 html + body 格式的演示qiankun认为是非标准的样式不做处理

3 html > body html body 性质的选择器会将 html 替换为空字符串

4 检查 a,html or *:not(:root) 根元素前插入了其他字符的选择器
同样需要替换为 a,[prefix] *:not([prefix]) 的形式

5 其他选择器会在选择器前添加前缀
[prefix] .text {}

最后拼接字符串
*/

css += ruleStyle(rule, prefix);
break;
case RuleType.MEDIA:
case RuleType.SUPPORTS:
/**
媒体查询 @media screen and (max-width: 300px) {}
能力检测 @supports (display: grid) {}

会递归调用重写方法,将中间的选择器重写

*/
css += ruleStyle(rule, prefix);
break;
}
});
});

js 沙箱

快照沙箱

如果平台不支持 Proxy ,则使用快照沙箱。 通过记录 window 对象上的属性变化,会复或保存状态。

这种方式的沙箱只关注应用激活和卸载时的差异,并不能控制属性的来源,因为是在全局 window 对象上进行操作,所以全部应用的对全局属性的操作,都会反映在 window 对象上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
function iter(win, cb) {
for (const k in win) {
if (win.hasOwnProperty(k) || k === "clearInterval") {
cb(k);
}
}
}

class SnapshotSandBox {
deleteProps = new Set();
modifyPropsMap = {};
proxy = window;
snapshot = {};

active() {
this.snapshot = {};
// 激活时记录当前的window快照,不关心 window 上是否有其他应用的属性。
// 只关心本次激活和卸载时属性的变化, 就是子应用的属性变化。
iter(window, (k) => {
this.snapshot[k] = window[k];
});

// 之前卸载时检测出修改或添加的属性,本次激活时要添加回来
// 虽然添加和修改的属性都是在 window 上,但是应用卸载后可能被其他应用删除或重写
// 这里使用记录值恢复之前的状态

Object.keys(this.modifyPropsMap).forEach((k) => {
window[k] = this.modifyPropsMap[k];
});

// 删除卸载时记录的删除值
// 同样因为其他同框架的子应用可能会添加相同属性

this.deleteProps.forEach((k) => {
delete window[k];
});

this.sandboxRunning = true;
}

inactive() {
this.deleteProps.clear();
this.modifyPropsMap = {};

// 和刚激活应用时的快照对比,检查新增或修改的属性
iter(window, (k) => {
if (window[k] !== this.snapshot[k]) {
this.modifyPropsMap[k] = window[k];
}
});

// 快照中有,但是当前 window 上没有,那么属性被删除

iter(this.snapshot, (k) => {
if (!window.hasOwnProperty(k)) {
this.deleteProps.add(k);
// 恢复环境, 应用中删除的属性,退出应用是需要还原
window[k] = this.snapshot[k];
}
});
this.sandboxRunning = false;
}
}

由于快照沙箱,对属性的操作都是在 window 上,因此多子应用的时候无法隔离子应用的状态,会导致冲突。只能适用于单实例。

proxy 沙箱
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
const createFakeWindow = (globalContext, speed) => {
const fakeWindow = {};
const propertiesWithGetter = new Map();
// 获取 window 静态属性
Object.getOwnPropertyNames(globalContext)
.filter((p) => {
// 属性描述符
// configurable 为假表示不能 修改,不能删除

const dec = Object.getOwnPropertyDescriptor(globalContext, p);
return dec?.configurable;
})
.forEach((p) => {
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
if (descriptor) {
const hasGetter = Object.prototype.hasOwnProperty.call(
descriptor,
"get"
);

// FAQ window 引用属性,为什么需要要修改属性描述符
if (
p === "self" ||
p === "window" ||
p === "parent" ||
p === "top" ||
(speed && p === "document")
) {
descriptor.configurable = true;

// 属性访问器和, writable 属性描述符不能同时指定
if (!hasGetter) {
descriptor.writable = true;
}
}

// 记录所有所有访问器得属性
if (hasGetter) propertiesWithGetter.set(p, true);

// FAQ 所有 configurable 为假得属性,就一定是环境级别属性属性么
Object.defineProperties(fakeWindow, p, Object.freeze(descriptor));
}
});
// Object.keys(window).forEach(k=>{
// if( has)
// const propDescriptor = Object.getOwnPropertyDescriptor(window,k);
// c
// })
};

let activeSandboxCount = 0;

class ProxySandBox {
sandboxRunning = true;
activeSandboxCount = 0;
document = document;
constructor(name, globalContext = window, opts) {
this.name = name;
this.globalContext = globalContext;
this.type = "proxy";
const { updatedValueSet } = this;
const { speedy } = opts || {};
const { fakeWindow, propertiesWithGetter } = createFakeWindow(
globalContext,
!!speedy
);

const proxy = new Proxy(fakeWindow, {
get(target, p) {
// this.registerRunningApp()

// 处理对一些特殊属性值得处理

//Symbol.unscopables 提供了一种机制,允许对象指定哪些属性在 with 语句中不应该被暴露为局部变量。如果一个属性在对象的 Symbol.unscopables 列表中,它将不会出现在 with 语句的作用域中。
// const obj = {
// a: 1,
// b: 2,
// [Symbol.unscopables]: {
// b: true // 使 'b' 在 with 语句中不可见
// }
// };

if (p === Symbol.unscopables) return unscopables;
if (p === "window" || p === "self" || p === "globalThis") {
return proxy;
}

// 如果获取得上级 window, 允许逃逸
if (p === "top" || p === "parent") {
if (globalContext === globalContext.parent) {
return proxy;
}
return globalContext[p];
}

if (p === "hasOwnProperty") {
// proxy.hasOwnProperty.call({a},'a')
return function hasOwnProperty(key) {
if (this !== proxy && this !== null && typeof this === "object") {
return Object.prototype.hasOwnProperty.call(this, key);
}

// proxy.hasOwnProperty("a")
return (
fakeWindow.hasOwnProperty(key) ||
globalContext.hasOwnProperty(key)
);
};
}

if (p === "document") {
return this.document;
}

if (p === "eval") {
return eval;
}
// customProp {configurable:false} => {configurable:true}
const actualTarget = propertiesWithGetter.has(p)
? globalContext
: p in target
? target
: globalContext;
const value = actualTarget[p];

// 校验了是不是frozen的属性, 如果是需要直接返回
// propertyDescriptor.configurable === false
// && (propertyDescriptor.writable === false || (propertyDescriptor.get && !propertyDescriptor.set)),
if (isPropertyFrozen(actualTarget, p)) {
return value;
}
// 非原生全局属性直接返回 addEventListener
// isNativeGlobalProp 枚举了 所有的全局属性
// useNativeWindowForBindingsProps 记录所有执行时需要绑定原生window方法 [fetch,true]
if (!isNativeGlobalProp(p) && !useNativeWindowForBindingsProps.has(p)) {
return value;
}

// 如果不处理 fetch.bind(this) 实际时绑定的proxyWindow 导致报错
const boundTarget = useNativeWindowForBindingsProps.get(p)
? nativeGlobal
: globalContext;

// 仅绑定 isCallable && !isBoundedFunction && !isConstructable 的函数对象,如 window.console、window.atob 这类,不然微应用中调用时会抛出 Illegal invocation 异常
// 目前没有完美的检测方式,这里通过 prototype 中是否还有可枚举的拓展方法的方式来判断
// @warning 这里不要随意替换成别的判断方式,因为可能触发一些 edge case(比如在 lodash.isFunction 在 iframe 上下文中可能由于调用了 top window 对象触发的安全异常)
const rebindTarget2Fn = (target, fn) => {
const isCallable = (fn) =>
typeof fn === "function" && fn instanceof Function;
// bind 函数的名称会以 bound开头 "bound fn"
const isBoundedFunction = (fn) =>
fn.name.indexOf("bound ") === 0 && !fn.hasOwnProperty("prototype");

const isConstructable = () => {
const hasPrototypeMethods =
fn.prototype &&
// class有constructor属性
fn.prototype.constructor === fn &&
// 构造器需要有原型链
Object.getOwnPropertyNames(fn.prototype).length > 1;
if (hasPrototypeMethods) return true;
// 假设以下视为构造函数
// 1. 有 prototype 并且 prototype 上有定义一系列非 constructor 属性
// 2. 函数名大写开头
// 3. class 函数

let constructable = hasPrototypeMethods;
if (!constructable) {
const fnString = fn.toString();
const constructableFunctionRegex = /^function\b\s[A-Z].*/;
const classRegex = /^class\b/;
constructable =
constructableFunctionRegex.test(fnString) ||
classRegex.test(fnString);
}

return constructable;
};

if (
isCallable(fn) &&
!isBoundedFunction(fn) &&
!isConstructable(fn)
) {
const boundValue = Function.prototype.bind.call(fn, target);

// 拷贝原有方法的静态属性
Object.getOwnPropertyNames(fn).forEach((key) => {
// boundValue might be a proxy, we need to check the key whether exist in it
if (!boundValue.hasOwnProperty(key)) {
Object.defineProperty(
boundValue,
key,
Object.getOwnPropertyDescriptor(fn, key)
);
}
});

// 如果原方法 prototype 属性设置了不可枚举,会导致原型链丢失
// 赋值的时候不能使用 = 或 Object.assign, 因为赋值操作会向上查询原型链
// 如果描述符被设 writable =false 或没有 set 属性访问器,会抛出错误
// Cannot assign to read only property 'prototype' of function
if (
fn.hasOwnProperty("prototype") &&
!boundValue.hasOwnProperty("prototype")
) {
Object.defineProperty(boundValue, "prototype", {
value: fn.prototype,
enumerable: false,
writable: true,
});
}

// 有一些库会使用 /native code/.test(fn.toString()) 检测是不是原生的方法
// 如果不特殊处理,所有toString 返回的都是 [object fakeWindow] 或 [object Window]]
if (typeof fn.toString === "function") {
const valueHasInstanceToString =
fn.hasOwnProperty("toString") &&
!boundValue.hasOwnProperty("toString");
const boundValueHasPrototypeToString =
boundValue.toString === Function.prototype.toString;

if (valueHasInstanceToString || boundValueHasPrototypeToString) {
// valueHasInstanceToString? 有自定义的 toString 使用原方法
// : 使用原生的 toString
const originToStringDescriptor =
Object.getOwnPropertyDescriptor(
valueHasInstanceToString ? fn : Function.prototype,
"toString"
);

Object.defineProperty(
boundValue,
"toString",
Object.assign(
{},
originToStringDescriptor,
originToStringDescriptor?.get
? null
: { value: () => fn.toString() }
)
);
}
}
}
};
return rebindTarget2Fn(boundTarget, value);
},
});
}

active() {
if (!this.activeSandboxCount) activeSandboxCount += 1;
this.sandboxRunning = true;
}
inActive() {
this.sandboxRunning = false;
}
patchDocument(doc) {
this.document = doc;
}
}

nginx 证书自动续期

目标使用 acme.sh 提供的自动化工具实现证书自动续期。

updete 2025-11-05

安装, 参考 acme.sh 官方文档

自动 DNS Api 集成,准备对应 DNS 厂商的 认证信息

注意 腾讯云 DNS 认证需要使用 DnsPod token, 而不是 Api key

设置 DNS 厂商的环境变量, 以腾讯云为例:

1
2
export DP_Id="id"
export DP_Key="token"

执行生成证书命令

1
2
3

# 注意根域名必须填写,不填写自动化部署的时候会找不到路径
/root/.acme.sh/acme.sh --issue  --dns dns_dp -d '*.iftrue.me' -d 'iftrue.me' 

证书创建成功后使用自动化部署命令可以实现自动续期

1
2
# 使用以下命令,自动部署到 nginx 文件路径,在到期前会自动续期
/root/.acme.sh/acme.sh --install-cert -d *.iftrue.me  --key-file /etc/nginx/conf.d/_club.key --fullchain-file /etc/nginx/conf.d/_club.crt --reloadcmd "service nginx reload"

域名授权/申请证书 (域名厂商)

整个过程参考 ACME 自动化快速入门

https://freessl.cn/ 选择 ACME 自动化 注册账号并登录

首先添加域名并在购买的云服务器上验证,验证通过后申请证书。

选择相关域名后会提示申请证书命令。 例如:

1
bash /root/acme.sh/.acme.sh --issue -d <你的域名> --dns dns_dp --server https://acme.freessl.cn/v2/DV90/directory/xxxx-xxx

找到合适的目录执行脚本,证书文件会在当前执行目录中生成。 需要注意 .acme.sh 的路径是否可以访问, 如果无法找到该命令,可以切换为相对根路径的地址。

执行后会生成证书文件,如下

1
2
3
4
[Thu Aug  1 22:23:17 CST 2024] Your cert is in: /root/.acme.sh/home.iftrue.me_ecc/home.iftrue.me.cer             # 证书文件 对应 nginx 中 cert.pem
[Thu Aug 1 22:23:17 CST 2024] Your cert key is in: /root/.acme.sh/home.iftrue.me_ecc/home.iftrue.me.key # 私钥文件 对应 nginx 中 cert.key
[Thu Aug 1 22:23:17 CST 2024] The intermediate CA cert is in: /root/.acme.sh/home.iftrue.me_ecc/ca.cer
[Thu Aug 1 22:23:17 CST 2024] And the full-chain cert is in: /root/.acme.sh/home.iftrue.me_ecc/fullchain.cer

自动续期

执行 ACME 自动化快速入门中提示的脚本, 执行命令的位置需要在生成证书的路径下。

1
2
3
4
bash /root/acme.sh/.acme.sh --install-cert -d <你的域名>
--key-file /etc/nginx/conf.d/cert.key # nginx 配置路径中证书的路径
--fullchain-file /etc/nginx/conf.d/cert.pem # nginx 配置路径中私钥的路径
--reloadcmd "nginx -s reload" # 重启 nginx 命令

acme.sh 会在证书还有 30 天到到期时尝试自动续期。

微前端 ② single-spa

single-spa 是一个将多个 JavaScript 微前端整合到一个前端应用程序中的框架。下面是一些优点:

  • 在同一个页面上使用多个框架而不刷新页面
  • 独立部署微前端
  • 使用新框架编写代码,而无需重写现有的应用程序

single-spa 比较好的解决了以下问题:

  • 应用加载顺序和依赖管理
  • 路由管理
  • 版本管理和升级
  • 调试和监控

依赖管理

single-spa 通过 systemjs 实现了应用资源的依赖管理

webpack 配置

微前端应用需要适当改造,将输出的模式更改为 systemjs, 并且实现 single-spa 指定的接口。single-spa 官方提供了配置工具,用来降低配置的成本。

webpack-config-single-spa 用于输出 webpack 打包文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
output: {
// 输出格式改为 systemjs
libraryTarget: "system",
},
devServer: {
// 任意路径下都返回 spa 应用入口文件
historyApiFallback: true
// 允许对子应用的跨域调用
headers: {
"Access-Control-Allow-Origin": "*",
},
},
// 会增加一个入口文件,动态指定 __webpack_public_path__
// 避免子应用中异步加载的资源请求了主应用的路径
new SystemJSPublicPathPlugin(
//...
),
// 处理开发模式下独立启动子应用显示页面
!isProduction &&
new StandaloneSingleSpaPlugin({
//...
}),
};

前端项目配置

项目的入口需要使用 single-spa-react 改造, 帮助实现 single-spa 要求的生命周期函数。

1
2
3
4
5
6
7
8
9
10
const lifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: Root,
domElementGetter: () => document.querySelector("#aa"),
errorBoundary(err, info, props) {
// Customize the root error boundary for your microfrontend here.
return null;
},
});

下面是 singleSpaReact 的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const opts = {
/** 接收到的参数*/
};
const SingleSpaContext = opts.React.createContext();

opts.SingleSpaRoot = createSingleSpaRoot(opts);

// 将传入的新组件包装成新的 Root 组件

function createSingleSpaRoot() {
// 为了支持 React 15 所以 single-spa 并没有使用 useEffect
// 并且使用 函数 模拟 ES6 class 实现
function SingleSpaRoot() {}
SingleSpaRoot.prototype = Object.create(opts.React.Component.prototype);

// 绑定自定义钩子函数
SingleSpaRoot.prototype.componentDidMount = function () {
setTimeout(this.props.mountFinished);
};

// 绑定自定义钩子函数
SingleSpaRoot.prototype.render = function () {
return this.props.children;
};

opts.SingleSpaRoot = createSingleSpaRoot(opts);

const lifecycles = {
bootstrap: bootstrap.bind(null, opts),
mount: mount.bind(null, opts),
unmount: unmount.bind(null, opts),
};

// 暴露接口方法
return lifecycles;
}

实现 bootstrap 接口方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
function mount(opts, props) {
const whenMounted = function () {
resolve(this);
};

const elementToRender = getElementToRender(opts, props, whenMounted);

// 包装 rootComponent 传入额外的 props
function getElementToRender(opts, props, mountFinished) {
const elementToRender = opts.React.createElement(opts.rootComponent, props);

// 绑定 context
// ...

// 绑定 errorBoundary
// ...

elementToRender = opts.React.createElement(
opts.SingleSpaRoot,
{
...props,
mountFinished,
updateFinished() {
//...
},
unmountFinished() {
//...
},
},
elementToRender
);

return elementToRender;
}
const domElement = opts.domElementGetter();

if (opts.ReactDOMClient?.createRoot) {
opts.renderType = "createRoot";
} else {
opts.renderType = "render";
}
const renderResult = reactDomRender({
elementToRender,
domElement: opts.domElementGetter(),
reactDom: opts.ReactDOMClient || opts.ReactDOM,
renderType: opts.renderType,
});

// 挂载组件
function reactDomRender({
reactDom,
renderType,
elementToRender,
domElement,
}) {
const renderFn = reactDom[renderType];

switch (renderType) {
case "createRoot":
case "unstable_createRoot":
case "createBlockingRoot":
case "unstable_createBlockingRoot": {
const root = renderFn(domElement);
root.render(elementToRender);
return root;
}
case "hydrateRoot": {
const root = renderFn(domElement, elementToRender);
return root;
}
case "hydrate":
default: {
renderFn(elementToRender, domElement);
// The renderRoot function should return a react root, but ReactDOM.hydrate() and ReactDOM.render()
// do not return a react root. So instead, we return null which indicates that there is no react root
// that can be used for updates or unmounting
return null;
}
}
}

opts.domElements[props.name] = domElement;
opts.renderResults[props.name] = renderResult;
}

实现 unmount 接口方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function unmount(opts, props) {
return new Promise((resolve) => {
opts.unmountResolves[props.name] = resolve;

const root = opts.renderResults[props.name];

if (root && root.unmount) {
// React >= 18
const unmountResult = root.unmount();
} else {
// React < 18
(opts.ReactDOMClient || opts.ReactDOM).unmountComponentAtNode(
opts.domElements[props.name]
);
}
delete opts.domElements[props.name];
delete opts.renderResults[props.name];
});
}

实现 update 接口方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function update(opts, props) {
return new Promise((resolve) => {
if (!opts.updateResolves[props.name]) {
opts.updateResolves[props.name] = [];
}

opts.updateResolves[props.name].push(resolve);

const elementToRender = getElementToRender(opts, props, null);
const renderRoot = opts.renderResults[props.name];
if (renderRoot && renderRoot.render) {
// React 18 with ReactDOM.createRoot()
renderRoot.render(elementToRender);
} else {
// React 16 / 17 with ReactDOM.render()
const domElement = chooseDomElementGetter(opts, props)();
// This is the old way to update a react application - just call render() again
getReactDom(opts).render(elementToRender, domElement);
}
});
}

singleSpa 源码

  • 首先定义子应用加载数组,以及各个阶段的状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const apps = [];

const NOT_LOADED = "NOT_LOADED";
const LOADING_SOURCE_CODE = "LOADING_SOURCE_CODE";
const NOT_BOOTSTRAPPED = "NOT_BOOTSTRAPPED";
const BOOTSTRAPPING = "BOOTSTRAPPING";
const NOT_MOUNTED = "NOT_MOUNTED";
const MOUNTING = "MOUNTING";
const MOUNTED = "MOUNTED";
const LOAD_ERROR = "LOAD_ERROR";
const SKIP_BECAUSE_BROKEN = "SKIP_BECAUSE_BROKEN";

// 主应用启动状态
let isStart = false;
  • singleSpa 提供 registerApplication 方法用于注册子应用,记录子应用必要信息, 同时会执行 reroute 方法,如何路径匹配,会加载子应用, 但是不会渲染子应用,子应用渲染需要等待 start 方法执行 isStart 标记为 true 后才会挂载子应用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function registerApplication(appNameOrConfig) {
// app 必须是返回 promise 的函数
// customProps 必须是对象
appNameOrConfig.customProps = appNameOrConfig.customProps || {};
// activeWhen 是一个路径匹配函数的数组

appNameOrConfig.activeWhen = Array.isArray(appNameOrConfig.activeWhen)
? appNameOrConfig.activeWhen
: [appNameOrConfig.activeWhen];
appNameOrConfig.activeWhen = appNameOrConfig.activeWhen.map((path) =>
typeof path === "function" ? path : pathToActiveWhen(path)
);

apps.push({
loadErrorTime: null,
status: NOT_LOADED,
...appNameOrConfig,
});

reroute();
}
  • singleSpa 提供 start 方法用于在注册后启动主应用, 在 start 方法中重写路由监听事件,可以让 singleSpa 响应路由的变化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
function start() {
isStart = true;
patchHistoryApi(opts);
reroute();
}

let urlRerouteOnly;
let originalReplaceState;
function createPopStateEvent(state, originalMethodName) {
// https://github.com/single-spa/single-spa/issues/224 and https://github.com/single-spa/single-spa-angular/issues/49
// We need a popstate event even though the browser doesn't do one by default when you call replaceState, so that
// all the applications can reroute. We explicitly identify this extraneous event by setting singleSpa=true and
// singleSpaTrigger=<pushState|replaceState> on the event instance.
let evt;
try {
evt = new PopStateEvent("popstate", { state });
} catch (err) {
// IE 11 compatibility https://github.com/single-spa/single-spa/issues/299
// https://docs.microsoft.com/en-us/openspecs/ie_standards/ms-html5e/bd560f47-b349-4d2c-baa8-f1560fb489dd
evt = document.createEvent("PopStateEvent");
evt.initPopStateEvent("popstate", false, false, state);
}
evt.singleSpa = true;
evt.singleSpaTrigger = originalMethodName;
return evt;
}

function patchedUpdateState(updateState, methodName) {
return function () {
const urlBefore = window.location.href;
const result = updateState.apply(this, arguments);
const urlAfter = window.location.href;

if (urlBefore !== urlAfter) {
// 手动触发 popstate 事件,让不同的子应用可以响应路由的变化。
window.dispatchEvent(
createPopStateEvent(window.history.state, methodName)
);
}
return result;
};
}

function patchHistoryApi() {
urlRerouteOnly = true;
originalReplaceState = window.history.replaceState;
function urlReroute() {
reroute([], arguments);
}
window.addEventListener("hashchange", urlReroute);
//调用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件。popstate 事件只会在浏览器某些行为下触发
//比如点击后退按钮(或者在 JavaScript 中调用 history.back() go()方法)。即,在同一文档的两个历史记录条目之间导航会触发该事件。
window.addEventListener("popstate", urlReroute);

const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;

// history 改变的时候需要主动触发事件
window.history.pushState = patchedUpdateState(
window.history.pushState,
"pushState"
);
window.history.replaceState = patchedUpdateState(
originalReplaceState,
"replaceState"
);
}
  • reroute 方法控制了子应用生命周期的执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
function reroute(
pendingPromises = [],
eventArguments,
silentNavigation = false
) {
const { appsToUnload, appsToUnmount, appsToLoad, appsToMount } =
getAppChanges();

let appsThatChanged,
cancelPromises = [];

if (start) {
appsThatChanged = appsToUnload.concat(
appsToLoad,
appsToUnmount,
appsToMount
);
return performAppChanges();
}
if (!start) {
appsThatChanged = appsToLoad;
return loadApps();
}

function loadApps() {
return Promise.resolve().then(() => {
const loadPromises = appsToLoad.map((app) => {
if (app.loadPromise) return app.loadPromise;
if (app.status !== NOT_LOADED && app.status !== LOAD_ERROR) {
return app;
}

app.status = LOADING_SOURCE_CODE;

return (app.loadPromise = Promise.resolve()
.then((val) => {
// val 获取到子项目暴露的接口
return (loadPromise = app.app(app));
})
.then((val) => {
app.status = NOT_BOOTSTRAPPED;
app.bootstrap = val.bootstrap;
app.mount = val.mount;
app.unmount = val.unmount;
app.unload = val.unload;
delete app.loadPromise;
}));
});
let succeeded;

return Promise.all(loadPromises);
});
}

function performAppChanges() {
return Promise.resolve().then(() => {
return Promise.all(cancelPromises).then(() => {
appsToUnmount.map((app) =>
app.unmount({ name: app.name }).then(() => (app.status = NOT_MOUNTED))
);

loadApps().then((res) => {
const loadThenMountPromises = appsToLoad.map((app) => {
console.log(app);
app.status = BOOTSTRAPPING;

return Promise.resolve(app.loadPromise)
.then((res) => {
return app.bootstrap();
})
.then(() => {
return Promise.resolve().then(() => {
app.status = MOUNTING;
return app
.mount({
name: app.name,
})
.then(() => {
app.status = MOUNTED;
});
});
});
});
});

appsToMount.map((app) => {
app.mount({ name: app.name }).then(() => (app.status = MOUNTED));
});
});
});
}
}

function getAppChanges() {
const appsToUnload = [],
appsToUnmount = [],
appsToLoad = [],
appsToMount = [];

let appsThatChanged,
cancelPromises = [];
// We re-attempt to download applications in LOAD_ERROR after a timeout of 200 milliseconds
const currentTime = new Date().getTime();

console.log(apps);
apps.forEach((app) => {
const appShouldBeActive =
app.status !== SKIP_BECAUSE_BROKEN && app.activeWhen[0](window.location);

switch (app.status) {
case LOAD_ERROR:
if (appShouldBeActive && currentTime - app.loadErrorTime >= 200) {
appsToLoad.push(app);
}
break;
case NOT_LOADED:
case LOADING_SOURCE_CODE:
if (appShouldBeActive) {
appsToLoad.push(app);
}
break;
case NOT_BOOTSTRAPPED:
case NOT_MOUNTED:
if (!appShouldBeActive) {
appsToUnload.push(app);
} else if (appShouldBeActive) {
appsToMount.push(app);
}
break;
case MOUNTED:
if (!appShouldBeActive) {
appsToUnmount.push(app);
}
break;
// all other statuses are ignored
}
});

return { appsToUnload, appsToUnmount, appsToLoad, appsToMount };
}

SVG角向渐变进度条

实现基本环形进度条

利用 circle 元素, 设置 stroke-width 来实现环形。

1
2
3
4
5
6
7
8
9
<circle
class="animate-item"
fill="none"
stroke="red"
stroke-width="20"
cx="50"
cy="50"
r="40"
></circle>

如果想实现部分环形, 可以使用 stroke-dasharray stroke-dashoffset, 偏移描边,来实现部分圆环

1
2
3
4
5
6
7
8
9
10
11
12
13
<circle
class="animate-item"
fill="none"
stroke="red"
stroke-width="20"
cx="50"
cy="50"
r="40"
stroke-dasharray="314 1000"
stroke-dashoffset="200"
stroke-linecap="round"
>
</circle>

因为 circle 的半径是 50,因此描边的路径长度是 Math.PI * 2 * 50 一个大于等于这个数值的描边就会可以覆盖整个圆环, 后面设置一个大一点的空白长度为 1000。

dashoffset 设置为 200 意味着描边向后偏移了 200, 那个对于第一段描边只剩下 314 - 200 的描边长度。

圆形绘制的起始点是 0 度角, 因此最终的效果如下。

一般环形位置是从 90 度角开始的, 所以可以给 circle 加上 transform 变换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<circle
class="animate-item"
fill="none"
stroke="red"
stroke-width="20"
cx="50"
cy="50"
r="40"
stroke-dasharray="314 1000"
stroke-dashoffset="200"
stroke-linecap="round"
transform="rotate(-90 50 50)"
>
</circle>

使用渐变色

SVG 支持线性渐变和镜像渐变, 可以创建一个线性渐变元素。 作为圆环的描边属性值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<svg
height="100"
width="100"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
>
<defs>
<linearGradient
x1="77.8982925%"
y1="11.2647007%"
x2="24.5398068%"
y2="89.7549679%"
id="linearGradient"
>
<stop stop-color="#002974" stop-opacity="0.21446132" offset="0%"></stop>
<stop stop-color="#4DD7FF" offset="99.9125874%"></stop>
</linearGradient>
</defs>
<circle
class="animate-item"
fill="none"
stroke="url(#linearGradient)"
stroke-width="20"
cx="50"
cy="50"
r="40"
stroke-dasharray="314 1000"
stroke-dashoffset="200"
stroke-linecap="round"
transform="rotate(-90 50 50)"
></circle>
</svg>

这里使用了一个有倾斜角度的线性渐变模拟, 色环的角向渐变。

实现角向渐变色

虽然 SVG 不支持角向渐变, 但是可以使用 [pattern] 作为描边属性值。

首先准备一张角向渐变的图片。

在 pattern 中定义并作为描边属性值使用。

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
<svg
height="100"
width="100"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
>
<defs>
<pattern
id="fill-img"
patternUnits="userSpaceOnUse"
width="100"
height="100"
>
<image xlink:href="./01.png" x="0" y="0" width="100" height="100"></image>
</pattern>
</defs>
<circle
class="animate-item"
fill="none"
stroke="url(#fill-img)"
stroke-width="20"
stroke-miterlimit="1"
cx="50"
cy="50"
r="40"
stroke-dasharray="314 1000"
stroke-dashoffset="200"
stroke-linecap="round"
transform="rotate(-74 50 50)"
></circle>
</svg>

由于 line-cap 占据了一定的 stroke 长度,所以 rotate 中并没有旋转到 90 度, 为 line-cap 占据的位置空出一点空间。

但是当 dashoffset 为 0 时,效果如下, 因此需要给 pattern 一个 transform 属性,用于抵消 circle 旋转的偏移。

1
2
3
4
5
6
7
8
9
10
11
<pattern id="fill-img" patternUnits="userSpaceOnUse" width="100" height="100">
<image
xlink:href="./01.png"
x="0"
y="0"
width="100"
height="100"
transform="rotate(74 50 50)"
>
</image>
</pattern>

最后添加动画属性, 初始时 dashoffset 在 314 的位置, 动画开始后将会回退到 0 的位置, 由此实现环形进度条动画效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<style>
.animate-item {
transition: stroke-dashoffset 1.5s ease;
}
</style>
<svg
height="100"
width="100"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
>
<defs>
<pattern
id="fill-img"
patternUnits="userSpaceOnUse"
width="100"
height="100"
>
<image
xlink:href="./01.png"
x="0"
y="0"
width="100"
height="100"
transform="rotate(74 50 50)"
></image>
</pattern>
</defs>
<circle
class="animate-item"
fill="none"
stroke="url(#fill-img)"
stroke-width="20"
stroke-miterlimit="1"
cx="50"
cy="50"
r="40"
stroke-dasharray="314 1000"
stroke-dashoffset="314"
stroke-linecap="round"
transform="rotate(-74 50 50)"
></circle>
</svg>
<script>
setTimeout(function () {
document
.querySelector(" .animate-item")
.setAttribute("stroke-dashoffset", 0);
}, 1000);
</script>

SVG 属性

stroke-dasharray

stroke 作为一个动词,有一画,划的意思。

在 SVG 中 stroke 作为一个描边属性,通过 stroke-dasharray 可以配置描边中的点和线的范式。换句话说它可以控制,描边中的线段与空白的规则。

作为一个外观属性,它也可以直接在写在 CSS 中。

1
2
3
4
5
6
/* 虚线长10,间距10,后面重复,虚线长10,间距10 */
stroke-dasharray = '10'
/* 虚线长10,间距5,后面重复,虚线长10,间距5 */
stroke-dasharray = '10, 5'
/* 虚线长20,间距10,虚线长5 后面是 间距20,虚线10,间距5 一直到下一个重复周期*/
stroke-dasharray = '20, 10, 5'

stroke-dashoffset

stroke-dashoffset 属性指定了 dash 模式到路径开始的距离, 它是一个偏移量,可以向前或向后偏移描边。

另外这是一个数值型的属性,因此可以使用 css 动画控制, 来实现路径动画。 这是一个角向渐变环形进度条的例子。

pattern

使用预定义的图形对一个对象进行填充或描边,就要用到 pattern 元素。pattern 元素让预定义图形能够以固定间隔在 x 轴和 y 轴上重复(或平铺)从而覆盖要涂色的区域。先使用 pattern 元素定义图案,然后在给定的图形元素上用属性 fill 或属性 stroke 引用用来填充或描边的图案。

CSS Grid 布局

grid 网格布局

可以设置元素 display 属性为 grid ,inline-grid,inline-grid 可以让 grid 元素保持内联的特性。[CSS 新世界中相关章节]

应用于容器的属性
网格数量与尺寸

grid-template-columnsgrid-template-rows属性主要用来指定网格的数量和尺寸等信息。[CSS 新世界 6.3.1]

grid-template: [grid-template-rows] / [grid-template-columns] 是缩写。

尺寸可用的 9 种数据类型,其中 fr 是单词 fraction 的缩写表示分数,按比例划分可自动分配的尺寸。

Service Worker 相关问题

注册

index.html

service worker 的 scope 范围与 sw.js 所在的路径相关,只能对 sw.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
25
26
27
28
29
30
31
32
const url = new URL(location.href);
if ("serviceWorker" in navigator) {
addEventListener("load", () => {
navigator.serviceWorker
.register("./sw.js", { scope: "/" })
.then((registration) => {
var serviceWorker;
if (registration.installing) {
serviceWorker = registration.installing;
// 这里可以处理正在安装状态
} else if (registration.waiting) {
serviceWorker = registration.waiting;
// 这里可以处理等待状态
} else if (registration.active) {
serviceWorker = registration.active;
emitCacheList(serviceWorker);
}

if (serviceWorker) {
serviceWorker.addEventListener("statechange", function (e) {
if (e.target.state === "activated") {
// Service Worker 已经激活,可以发送消息
emitCacheList(serviceWorker);
}
});
}
})
.catch((err) => {
console.warn("desktop_sw register fail.");
});
});
}

安装

1
2
3
this.addEventListener("install", function (event) {
console.log("install");
});

激活

不一定每次都能触发。前一个还在工作状态那么后一个就会进 waiting 阶段,只有等到前一个被 terminated 后,后一个才能完全替换 A 的工作

1
2
3
this.addEventListener("activate", function (event) {
console.log("activate");
});

更新

在这些条件中会执行更新操作

主动在每次注册后执行更新

1
2
3
4
5
6
7
8
9
10
11
12
navigator.serviceWorker
.register("/sw.js", {
scope: "/",
})
.then((registration: any) => {
registration.update();
});

// 使用 workbox
wb.register().then((registration: any) => {
registration.update();
});

关闭

  • terminated
  • 关闭浏览器一段时间
  • 手动清除 serviceworker
  • 在 sw 安装时直接跳过 waiting 阶段 self.skipWaiting();

拦截请求

拦截请求 只有 activate 之后才能工作

页面本身的 URL 不在 Service Worker 的 scope 内时,Service Worker 并不会对该页面加载过程中的任何请求(包括在 scope 内的资源)有控制权。页面的控制权是从最顶层的文档开始的,如果顶层文档(页面)不在 Service Worker 的 scope 内,那么 Service Worker 就不能影响到从这个页面发出的任何请求,即使这些请求本身的目标 URL 是在 Service Worker 的 scope 范围内。

1
2
3
4
5
6
7
8
9
10
11
this.addEventListener("fetch", function (event) {
// 返回jSON HTML
const json = JSON.stringify({ a: 1 }, null, 2);
return event.respondWith(
new Response(json, {
headers: {
"content-type": "application/json;charset=UTF-8",
},
})
);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
// 拦击请求 只有activate之后才能工作
this.addEventListener("fetch", function (event) {
const html = "<html><div>html</div></html>";
return event.respondWith(
new Response(html, {
headers: {
"content-type": "text/html;charset=UTF-8",
},
})
);
// 重定向
return event.respondWith(Response.redirect("https://baidu.com", 301));
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
this.addEventListener("fetch", function (event) {
const url = new URL(event.request.url);
if (location.origin !== url.origin) return;
return event.respondWith(
caches.match(event.request).then((res) => {
if (res) return res;
return fetch(event.request).then((res) => {
if (!res || res.status !== 200 || res.type !== "basic") {
return res;
}
const clone = res.clone();
caches.open(config.CACHE_VERSION).then((caches) => {
caches.put(event.request, clone);
});
return res;
});
})
);
});

通信

客户端向 sw 发送消息,需要保证 sw 已经处于 activated 状态

1
2
3
4
5
6
7
// index.html
serviceWorker.postMessage({ a: 1 });

// sw.js
this.addEventListener("message", function (event) {
console.log("收到页面消息", event.data);
});

数据同步, sw 可以监听客户端发起的 sync 请求,当离线环境时,sw 在后台将任务挂起,当网络恢复会执行回调函数, 相同的 tag 在网络恢复后只会执行一次, tag 不能用于传输数据, 离线数据应该使用持久化保存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// index.html
navigator.serviceWorker.ready.then(function (registration) {
document.body.addEventListener("click", () => {
registration.sync
.register("data_sync")
.then(function () {
console.log("后台同步已触发");
})
.catch(function (err) {
console.log("后台同步触发失败", err);
});
});
});

// sw.js
self.addEventListener("sync", function (e) {
switch (e.tag) {
case "data_sync":
break;
default:
return;
}
});

workbox

workbox docs

  • 注册

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // index.html
    if ("serviceWorker" in navigator) {
    const { Workbox } = await import("workbox-window");
    const wb = new Workbox("sw.js");

    // 实现 window 与 sw 通信
    wb.addEventListener("activated", (event) => {
    const urlsToCache = [
    location.href,
    ...performance.getEntriesByType("resource").map((r) => r.name),
    ];
    wb.messageSW({
    type: "CACHE_URLS",
    payload: urlsToCache,
    });
    });
    wb.register();
    }
  • 使用预设

    通常不需要手动设置 workbox-precaching, 应该使用 workbox-build 或 workbox-webpack-plugin 自动生成依赖文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // sw.js

    import { pageCache, staticResourceCache, imageCache } from "workbox-recipes";
    import { precacheAndRoute } from "workbox-precaching";

    pageCache();
    staticResourceCache();
    imageCache();

    // 表示缓存所有 webpack 打包的 manifest 文件
    precacheAndRoute(self.__WB_MANIFEST || []);
  • 使用 workbox-precaching 缓存

    当应用首次载入 install 事件中,workbox-precaching 会查看你要下载的资源,删除重复的并使用 SW 事件下载并缓存资源。资源的 URL 中已经包含了可以用作缓存 key 的信息

  • 使用 workbox-webpack-plugin

    GenerateSW : 适用与预缓存文件,或有简单的缓存需求。 不适用与使用其他的 SW 特新,例如 Web Push, 或有自定一的缓存逻辑

    InjectManifest: InjectManifest 插件将生成一个要预缓存的 url 列表,并将该预缓存清单添加到现有的 service worker 文件中。否则它将使文件保持原样。

    适用于想要更多的控制 SW,缓存文件,自定义路由策略,想要使用其他的 SW 特性,

    会在 output.path 中生成 sw.js 文件,需要手动在 index.html 中引入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // webpack.config.js
    const { InjectManifest } = require("workbox-webpack-plugin");

    module.exports = {
    plugins: [
    new InjectManifest({
    swSrc: "sw.js",
    exclude: [
    /\.map$/,
    /manifest$/,
    /\.htaccess$/,
    /service-worker\.js$/,
    /sw\.js$/,
    ],
    }),
    ],
    };

什么是 Service Worker?它的作用是什么?

Service Worker 是一种运行在浏览器后台的 JavaScript 脚本,独立于页面上下文,并且与页面的生命周期分离。它提供了拦截和处理网络请求的能力,从而使得网页能够支持离线工作、缓存资源和后台同步等功能。它是 渐进式 Web 应用(PWA)的关键组成部分。

离线支持:缓存页面和资源,使得在没有网络连接时,应用仍能工作。
网络请求拦截:可以拦截网络请求并提供自定义响应,例如从缓存中获取资源,或将请求转发给网络。
后台同步:在应用有网络时自动同步数据。
推送通知:支持推送通知,允许在用户不活跃时向其发送消息。

Service Worker 的生命周期是什么样的?

注册:通过 navigator.serviceWorker.register() 方法注册 Service Worker。此时浏览器会检查是否需要安装新的 Service Worker。
安装(Install):注册后,如果没有有效的 Service Worker,浏览器会尝试安装它。在此阶段,通常会缓存一些静态资源。
激活(Activate):安装完成后,Service Worker 会激活。在此阶段,可以清理旧的缓存和控制页面。
控制:Service Worker 激活后,能够控制所有符合条件的页面,开始拦截网络请求。
更新:Service Worker 可能会定期检查更新,新的 Service Worker 安装并激活后,旧的 Service Worker 会被终止。

Service Worker 中的 fetch 事件是如何工作的?

fetch 事件是 Service Worker 中用于拦截和处理网络请求的核心事件。每当页面发起请求时,Service Worker 会通过 fetch 事件捕获请求,允许开发者决定如何响应这些请求(例如,从缓存中获取、从网络获取或自定义响应)。

1
2
3
4
5
6
7
8
9
self.addEventListener("fetch", function (event) {
event.respondWith(
caches
.match(event.request) // 优先从缓存中获取
.then(function (response) {
return response || fetch(event.request); // 如果缓存中没有,则从网络获取
})
);
});

event.respondWith() 方法用于返回一个 Response 对象,覆盖默认的网络请求处理方式。

什么是 Service Worker 的作用域?

Service Worker 的作用域是指它可以控制的 URL 范围。在注册时,可以指定作用域,它决定了 Service Worker 可以拦截哪些请求。默认情况下,Service Worker 会控制它所在路径下的所有页面和资源。

如果没有指定作用域,Service Worker 会默认控制当前路径及其子路径下的页面。

1
navigator.serviceWorker.register("/service-worker.js", { scope: "/" });

如何实现离线缓存?

离线缓存的基本思路是在 install 事件中缓存需要的资源,然后在 fetch 事件中拦截请求,从缓存中提供响应。
在安装时,将文件添加到缓存中,fetch 事件会拦截请求,优先从缓存中返回资源。

cache.add():将单个资源添加到缓存。
cache.addAll():将多个资源添加到缓存。
cache.put():将指定请求和响应对存入缓存。
caches.delete():删除指定缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
self.addEventListener("install", function (event) {
event.waitUntil(
caches.open("my-cache").then(function (cache) {
return cache.addAll([
"/index.html",
"/styles.css",
"/script.js",
"/offline.html",
]);
})
);
});

self.addEventListener("fetch", function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
return response || fetch(event.request); // 网络优先
})
);
});

self.skipWaiting() 和 self.clients.claim() 的作用是什么?

self.skipWaiting():在激活新的 Service Worker 时,跳过等待阶段,立即使新的 Service Worker 控制页面。这对实现即时更新非常有用。

self.clients.claim():在 Service Worker 激活后,立即接管当前打开的页面,使其受该 Service Worker 控制,避免等待页面重新加载。

1
2
3
4
5
6
7
self.addEventListener("install", function (event) {
self.skipWaiting(); // 立即激活
});

self.addEventListener("activate", function (event) {
event.waitUntil(self.clients.claim()); // 立即接管控制
});

如何清理旧的缓存?

在 Service Worker 的 activate 事件中,可以清理旧的缓存。例如,在更新时清理不再需要的缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
self.addEventListener("activate", function (event) {
var cacheWhitelist = ["my-cache-v2"]; // 新的缓存名称
event.waitUntil(
caches.keys().then(function (cacheNames) {
return Promise.all(
cacheNames.map(function (cacheName) {
if (!cacheWhitelist.includes(cacheName)) {
return caches.delete(cacheName); // 删除不在白名单中的缓存
}
})
);
})
);
});

如何使用 Service Worker 支持推送通知

推送通知依赖于 Service Worker 的 push 事件。当接收到推送消息时,Service Worker 会在后台处理并显示通知。

1
2
3
4
5
6
7
8
9
10
self.addEventListener("push", function (event) {
var options = {
body: event.data.text(),
icon: "/icon.png",
badge: "/badge.png",
};
event.waitUntil(
self.registration.showNotification("Push Notification", options)
);
});

Service Worker 的缓存策略有哪些

Cache-first:优先从缓存获取资源,如果没有,再从网络获取。
Network-first:优先从网络获取资源,如果失败,再从缓存中获取。
Stale-while-revalidate:先从缓存获取过时的资源,然后异步从网络获取最新的资源,并更新缓存。

如何使用 Service Worker 实现离线数据同步?

在网络恢复的时候,sync 会自动触发。

1
2
3
4
5
6
7
8
9
10
11
//客户端,注册监听事件
navigator.serviceWorker.ready.then((registration) => {
registration.sync.register("syncData");
});

// service worker
self.addEventListener("sync", (event) => {
if (event.tag === "syncData") {
event.waitUntil(syncData());
}
});

Service Worker 如何与多个标签页共享数据

Service Worker 可以通过 客户端 API 与多个标签页进行通信。clients.matchAll() 可以获取所有控制的页面客户端,postMessage 用于与页面通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 页面中
self.addEventListener("message", function (event) {
console.log("Message from client:", event.data);
event.ports[0].postMessage("Hello from Service Worker!");
});

// service worker
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage("Hello from the page!");
navigator.serviceWorker.addEventListener("message", function (event) {
console.log("Message from service worker:", event.data);
});
}

如何在 Service Worker 中使用 self.registration.update()

self.registration.update() 用于强制浏览器检查 Service Worker 是否有更新。如果应用程序需要强制立即检查更新,可以调用该方法。

1
2
3
4
5
self.addEventListener("activate", function (event) {
event.waitUntil(
self.registration.update() // 强制更新 Service Worker
);
});

CSS Flex 布局

flex 弹性布局

可以设置元素 display 属性为 flexinline-flex, inline-flex 可以让 flex 元素保持内联的特性。[CSS 新世界中相关章节]

弹性设置
  • flex-grow:当有剩余空间时,元素延伸占据剩余空间的规则

  • flex-shrink:剩余空间不足时,元素收缩的规则

  • flex-basis: 元素基础宽度,类似于 width 但如果设置了 auto 以外的值,优先级别 width 高.

    flex-basis 属性下的最小尺寸是由内容决定的,而 width 属性下的最小尺寸是 width 属性的计算值决定的。也就是内容过长的时候,设置了 width 可能溢出,而设置了 flex-basis 宽度是最小内容宽度。

CSS 常见问题

BFC 及其应用

block formatting context (块级格式化上下文), BFC 元素可以隔离子元素对外部元素的影响。[CSS 世界中相关章节]

如果一个元素是块级元素,满足以下任一情况会触发 BFC:

  • <html> 元素
  • float 的值不为 none
  • overflow 的值为 auto、scroll 或 hidden;
  • display 的值为 table-cell,table-row,table-caption 和 inline-block 中的任何一个;
  • position 的值不为 relative 和 static。

BFC 可以解决以下问题:

  • 避免 margin 重叠,将元素的外层元素变成 BFC 元素,因为 BFC 的隔离性,可以避免子元素与外层元素 margin 重叠
    但是如果 BFC 元素有上下 margin, 仍然会与外层元素边距重叠

  • 可以让文字环绕图片时,文字自动填充图片右侧空间,而无需设置固定的宽度。如果想要文字于图片保持距离,可使用 margin 等,但是不能使用 p 标签的 margin-left

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <style>
    img {
    width: 100px;
    height: 100px;
    float: left;
    }
    p {
    overflow: hidden;
    }
    </style>
    <div class="a">
    <img src="./01.png" alt="" />
    <p>xx</p>
    </div>
    • 使用 display:table-cell 实现自适应的两栏布局,由于 table-cell 的特性是宽度不会超过父容器的宽度,所以可以给一个很大的宽度
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <style>
    .wrapper {
    width: 100%;
    }
    img {
    width: 100px;
    float: left;
    }
    .right {
    display: table-cell;
    word-break: break-all;
    width: 9999px;
    }
    </style>
    <div class="wrapper">
    <img src="./01.png" alt="" />
    <div class="right">xxxx</div>
    </div>

overflow 不同值得区别

overflow 属性原本的作用指定了块容器元素的内容溢出时是否需要裁剪。[CSS 世界中相关章节]

  • 注意剪裁得部分是
  • 除非 overflow-x 和 overflow-y 的属性值都是 visible,否则 visible 会当成 auto 来解析
  • <html> <textarea> 默认会有滚动条,因为 auto 作为默认值。
  • 滚动条的产生会影响表格等样式,方法 1 可以空出右边的滚动条的宽度,方法 2 可以不设最后一列宽度
  • body 设置为 absolute, 宽度 100vw,可以解决滚动条晃动的问题。

三栏布局

  • float 实现,主要元素的排列顺序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <style>
    .container {
    width: 100%;
    overflow: hidden;
    }
    .left {
    float: left;
    width: 20%;
    }
    .right {
    float: right;
    width: 20%;
    }
    .center {
    margin-left: 20%;
    margin-right: 20%;
    background: lightgray;
    }
    </style>
    <div class="container">
    <div class="left">Left</div>
    <div class="right">Right</div>
    <div class="center">Center</div>
    </div>
  • flex 实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <style>
    .container {
    display: flex;
    }
    .left {
    flex: 0 0 20%;
    }
    .center {
    flex: 1;
    }
    .right {
    flex: 0 0 20%;
    }
    </style>
    <div class="container">
    <div class="left">Left</div>
    <div class="center">Center</div>
    <div class="right">Right</div>
    </div>
  • grid 布局

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <style>
    .container {
    display: grid;
    grid-template-columns: 20% 1fr 20%;
    }
    </style>
    <div class="container">
    <div class="left">Left</div>
    <div class="center">Center</div>
    <div class="right">Right</div>
    </div>
  • 使用 absolute 绝对定位布局

  • 使用 table 布局

    table 中未设置宽度的单元格会自动占据剩余的空间

    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
    <style>
    .container {
    display: table;
    width: 100%;
    }
    .left,
    .right {
    min-width: 100px;
    }
    </style>
    <table class="container">
    <thead>
    <tr>
    <td width="100px"></td>
    <td></td>

    <td width="100px"></td>
    </tr>
    </thead>
    <table class="container">
    <tbody>
    <tr>
    <td class="cell left">Left</td>
    <td class="cell center">Center</td>
    <td class="cell right">Right</td>
    </tr>
    </tbody>
    </table>
    </table>

    使用 div 也可以由同样的效果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <style>
    .container {
    display: table;
    width: 100%;
    }

    .left,
    .right {
    min-width: 100px;
    }

    .cell {
    display: table-cell;
    }
    </style>
    <div class="container">
    <div class="cell left">Left</div>
    <div class="cell center">Center</div>
    <div class="cell right">Right</div>
    </div>
  • float + bfc

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    <style>
    .wrapper {
    clear: both;
    }

    .middle {
    width: 100%;
    float: left;
    }

    .main {
    margin-left: 100px;
    margin-right: 100px;
    }

    .left {
    float: left;
    width: 100px;
    margin-left: -100%;
    }

    .right {
    float: right;
    width: 100px;
    margin-left: -100%;
    }
    </style>
    <div class="wrapper">
    <div class="middle">
    <div class="main">中间</div>
    </div>
    <div class="left">左栏</div>
    <div class="right">右栏</div>
    </div>

calc 函数

通常配合变量使用,实现动态计算效果。[CSS 新世界 4.5] [兼容性]

  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信