Yet another bootstrap theme.

2025-02-21
html2canvas生成图片、vue-plugin-hiprint生成PDF后第三方字体丢失

核心原理是,动态生成的canvas元素,要在绘制之前,插入到真实的DOM中去。

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
// 处理html2canvas 生成图片字体丢失
// 核心兼容代码
// 处理html2canvas字体偏移
const style = document.createElement("style")
document.head.appendChild(style)
style.sheet?.insertRule(`
body > div:last-child img {display: inline-block}
`)
// 处理html2canvas字体偏移

// 要转换为图片的DOM
var shareContent = qrEl.value!;
var width = shareContent.offsetWidth;
var height = shareContent.offsetHeight;
var scale = 3;

const canvas = document.createElement("canvas")
// 重点,指定的canvas 一定要在绘制内容前插入到真实DOM中
document.body.appendChild(canvas)

// canvas 尺寸设置为缩放的大小
canvas.width = width * 3
canvas.height = height * 3
// canvas 尺寸设置为缩放的大小

// canvas 样式设置为实际页面预览大小
canvas.style.width = `${width}px`
canvas.style.height = `${height}px`
// canvas 样式设置为实际页面预览大小

// 展示样式 不重要
canvas.style.position = "fixed";
canvas.style.zIndex = "3000"
canvas.style.left = "50%";
canvas.style.marginLeft = "-186px";
canvas.style.top = "62px";
// 展示样式 不重要

const el = await htmlToCanvas(shareContent, {
scale: scale,
width: width,
height: height,
useCORS: true,
canvas: canvas,
x: 0,
y: 0,
scrollX: 0,
scrollY: 0,
})

// 处理html2canvas字体偏移
style.remove()
// 处理html2canvas字体偏移

el.toBlob((blob) => {
const fileName = `收款码-${props.bill?.number || "无"}-${expireTime}.png`
const file = new File([blob!], fileName, {
type: "image/png",
});
// 移除canvas dom
canvas.remove();
})
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
// 处理 vue-plugin-hiprint 导出PDF 第三方字体丢失
// 在toPdf 方法中,html2canvas配置中,加入我们创建的canvas,并在html2canvas绘制之前,把创建的canvas插入到真实DOM中。
toPdf = function ( t, e, options ) {
var i = this;
var dtd = $.Deferred();
var isDownload = true;
if ( this.printPanels.length ) {
const canvas = document.createElement( 'canvas' )
var scale = 2
var r = o.a.mm.toPt( this.printPanels[0].width ),
a = o.a.mm.toPt( this.printPanels[0].height );
var w = o.a.pt.toPx( r );
canvas.width = w * scale
canvas.style.width = `${w}px`
document.body.appendChild( canvas )
var p = $.extend( {
scale: scale,
width: o.a.pt.toPx( r ),
x: 0,
y: 0,
useCORS: !0,
canvas: canvas,
}, options || {} ),
s = new jsPDF( {
orientation: 1 == this.getOrient( 0 ) ? "portrait" : "landscape",
unit: "pt",
format: this.printPanels[0].paperType ? this.printPanels[0].paperType.toLocaleLowerCase() : [r, a]
} ),
l = this.getHtml( t, options );
// 移除节点
if ( options && undefined != options.isDownload ) {
isDownload = options.isDownload
}
this.createTempContainer();
var u = this.getTempContainer();
this.svg2canvas( l ), u.html( l[0] );
canvas.height = l[0].offsetHeight * scale
canvas.style.height = `${l[0].offsetHeight}px`
var d = u.find( ".hiprint-printPanel .hiprint-printPaper" ).length;
$( l ).css( "position:fixed" ), html2canvas( l[0], p ).then( function ( t ) {
var n = t.getContext( "2d" );
n.mozImageSmoothingEnabled = !1, n.webkitImageSmoothingEnabled = !1, n.msImageSmoothingEnabled = !1, n.imageSmoothingEnabled = !1;
canvas.remove();

for ( var o = t.toDataURL( "image/jpeg" ), p = 0; p < d; p++ ) {
s.addImage( o, "JPEG", 0, 0 - p * a, r, d * a ), p < d - 1 && s.addPage();
}
if ( isDownload ) {
i.removeTempContainer(), e.indexOf( ".pdf" ) > -1 ? s.save( e ) : s.save( e + ".pdf" );
} else {
i.removeTempContainer();
let type = options.type || 'blob';
var pdfFile = s.output( type );
dtd.resolve( pdfFile );
}
} );
}
return dtd.promise();
}
Read More

2025-02-18
html2canvas生成图片文案偏移

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 style = document.createElement("style")
document.head.appendChild(style)
style.sheet?.insertRule("body > div:last-child img {display: inline-block}")
// 核心兼容代码

var shareContent = qrEl.value!;
var width = shareContent.offsetWidth;
var height = shareContent.offsetHeight;
var canvas = document.createElement("canvas");
var scale = 1;

canvas.width = width * scale;
canvas.height = height * scale;
canvas.getContext("2d")!.scale(scale, scale);

const res = await htmlToCanvas(shareContent, {
scale: scale,
canvas: canvas,
logging: true,
width: width,
height: height,
useCORS: true,
scrollX: 0,
scrollY: 0
})
style.remove() // 删除动态添加的style

res.toBlob((blob) => {
const url = URL.createObjectURL(blob!)
window.open(url)
})
Read More

2024-11-19
pnpm patch修改第三方包

  1. 第一步 生成需要修改的包的路径和版本号
1
2
3
4
5
6
7
8
9
10
# 生成包的一个修改路路径 (包名称@版本号)
pnpm patch <package-name><package-version>
# 会得到一个生成的patch 路径
# Patch: You can now edit the package at:

# /Users/zhangyu/work/ttzz/ttzzerp-app/node_modules/.pnpm_patches/pdfh5@1.4.9

# To commit your changes, run:

# pnpm patch-commit '/Users/zhangyu/work/ttzz/ttzzerp-app/node_modules/.pnpm_patches/pdfh5@1.4.9'
  1. 修改生成包的内容

    1
    2
    # file-path /Users/zhangyu/work/ttzz/ttzzerp-app/node_modules/.pnpm_patches/pdfh5@1.4.9
    code <file-path>
  2. 重新patch-commit 修改的包

1
2
# 生成patch目录,保存修改的diff信息 (file-path: /Users/zhangyu/work/ttzz/ttzzerp-app/node_modules/.pnpm_patches/pdfh5@1.4.9)
pnpm patch-commit <file-path>
  1. 重新启动项目
    1
    # 启动项目
Read More

2024-11-08
puppeteer根据页面生成PDF

需求

有一个网页版的报告需要打印成pdf, 供用户下载. 要求打印原版的网页, 美观漂亮. 

开始用手吧

安装不用多说, 老套路, 没啥子坑.

npm i puppeteer

网上抄一段打印pdf的代码. 

复制代码
...
let browser = await puppeteer.launch();
let page = await browser.newPage();
let options = { waitUntil: 'networkidle0' };
page.setCookie(...cookies);
let url = `http://0.0.0.0/page/report/down?rid=3a1325b6`;
await page.goto(url, options);
let pdf = await page.pdf({format: 'A4', fullPage: true);
await page.close();
await browser.close();
...
res.set({ 'Content-Disposition': `attachment`, 'filename': 'report.pdf', 'Content-Type': 'application/octet-stream', 'Content-Length': pdf.length });
res.send(pdf);
复制代码

这段代码大部分执行都没啥子问题, 但没有输出report.pdf文件.  好吧, 逐一填坑.

坑点: Res的header不正确

修改res.set的内容, 把文件名写到Content-Disposition对象上去. 

旧: 

res.set({ 'Content-Disposition': 'attachment', 'filename': 'report.pdf', 'Content-Type': 'application/octet-stream', 'Content-Length': pdf.length })

修改后: 

res.set({ 'Content-Disposition': `attachment;filename=report.pdf`, 'Content-Type': 'application/octet-stream', 'Content-Length': pdf.length });

 经过修改, 这次输出了pdf文件. 

但是, 新坑来了, pdf只有一页, 并且显示不合, 仅显示网页上边中间的部分内容, 没有背景, 整个内容奇丑无比且, 看来是应该是pdf的设置不正确, 丢失了背景色, A4大小也不能适用于 1200宽的网页. 

坑点: 没有网页背景

这个好解决, 加入一个参数即可 :

let pdf = await page.pdf({
   format: 'A4', printBackground:
true });

 

坑点: 修改PDF页面大小以显示全部内容

这是花了我时间最多的地方, 并一度放弃改为图片输出. 但后来将就着找到解决办法. 

不通的办法: 

  • 删除format参数, 无效
  • 设置fullPage参数, 无效
  • 使用网上的自动滚动代码, 把网页滚到底部, 无效
  • 抄一段仅打印body元素内容,无效. 
  • 读取body的宽度, 并将page的viewport设置为该值, 无效. 
  • ...

是不是因为网页没有渲染完成呢? 因为是vue项目, 先下回代码来, 然后再由浏览器计算得到网页, 极有可能呀, 改吧. 

let options = { waitUntil: 'networkidle0' };
let options = { waitUntil: 'networkidle2' };
page.waitForTimeout(8000);
page.waitForFunction('window.renderdone', ...

上面全部失败, 根本不是解决问题的核心点. 

看来, 解决的途径还应该是读取正确的网页宽高值, 可上面的试过的方法怎么就不起作用了. 经详细检查, 发现, 读取的body的宽度值不正确, 高度总是0, 所以, 打印的pdf肯定不正确. 

打印了一下另外一个网站的网页, 我K, 竟然全页成功了. 可为什么我的不行呢?  我的网页是VUE写的, 与其他有什么不同呢? 最终都是生成html呀. ..

chrome 开发工具检查一下宽高值, 发现 body 元素真的是 0高度, 无语. 改用 id 为app的元素, 成功读取到了网页的宽高值

具体代码如下: 

复制代码
const dimensions = await page.evaluate(() => {
return {
        width: document.getElementById("app").scrollWidth + 100,
        height: document.getElementById("app").scrollHeight + 100,
        deviceScaleFactor: window.devicePixelRatio
    };
});
let pdf = await page.pdf({
        margin: {
        top: '0.5cm',
        bottom: '0.5cm',
        left: '0.5cm',
        right: '0.5cm'
        }, 
  printBackground:
true,
width: dimensions.width,
height: dimensions.height });
复制代码

顺便, 打印一下pdf的边距, 设成0.5cm, 并把网页的宽度高度各加了100, 以抵销边距的开销, 保证内容不丢失.

上面打印的网页只是一个示例页, 还不是最终的页面, 因为最终页面需要登录, 得需要解决登录的问题.

这个网站使用的cookie中存着用户信息 userInfo. 和一个原始的token, 在具体的网页中, 需要读取这两个cookie值完成用户的检查. 

于是加上cookie的内容: 

复制代码
let cookies = [{
        name: 'token',
        value: token
    }, {
        name: 'userInfo',
        value: userInfo,
}];
  page.setCookie(...cookies);
复制代码

坑点: Cookie的内容要写正确

我去, 竟然不起作用. 为什么?  查找资源, 原来cookie的对象内容不正确,要把域名, 过期时间, 路径等等都写上. 

最终的结果: 

复制代码
let cookies = [{
    name: 'token',
    value: token,
    path: '/',
    domain: conf.app.domain,
    expires: -1,
    httpOnly: false,
    secure: false,
    session: true
}, {
    name: 'userInfo',
    value: userInfo,
    path: '/',
    domain: conf.domain,
    expires: -1,
    httpOnly: false,
    secure: false,
    session: true
}];
page.setCookie(...cookies);
复制代码

这次读取到了cookie, 开心. 

最终的结果:

可以打印完整网页的pdf, 但是仅一页, 不像其他pdf一样, 一个页面一个页面的, 这个就不再改了, 一来是分页比较麻烦, 二来是即便是分了, 数据内容被拆到多页,反而不利于阅读.  三就是我不会拆页(汗).

 

完成pdf的打印, 又顺手试了一下png的打印, 没有问题, 只要把图片的大小设置好, 成功打印了一个png图片. 

好了, 线上部署吧. 

坑点: 服务器部署

使用的centos 7的服务器, 

正确上传代码,然后 "npm i"

正在所预料的不正确, 改为"cnpm i", 非常快, 100多M在服务器上是小case. 

启动项目, .. 尝试打印一个网页,  报错, 说什么browser没有启动. 

找解决方法, 明明是本机测试正常的代码, 到了服务器就不行呢, ... 可能是macos和centos的差异吧. 

网搜解决办法, 需要安装一些依赖的包: 

复制代码
yum install pango.x86_64 libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXtst.x86_64 cups-libs.x86_64 libXScrnSaver.x86_64 libXrandr.x86_64 GConf2.x86_64 alsa-lib.x86_64 atk.x86_64 gtk3.x86_64 ipa-gothic-fonts xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi xorg-x11-utils xorg-x11-fonts-cyrillic xorg-x11-fonts-Type1 xorg-x11-fonts-misc
复制代码

好了. 很快安装完毕, 终于可以启动了. 什么? 又报错了, 看了一下错误原因, brower启动不能在不使用sand-box时情况以root用户吂动. 

坑点: 使用chrome, 不能以root账号直接使用

搜索一下吧, 很快找到了答案: 

let browserOptions = { args: ['--no-sandbox', '--disable-setuid-sandbox'] };
let browser = await puppeteer.launch(browserOptions);

加上一个参数即可, 我懒得换服务器上的账号了, 就这样将就着吧. 

再打印一个pdf吧.... 怎么, 里面的汉字全成了框框与叉叉了! 应该是服务器上没有字体. 这个在其他项目上解决过, 这次就好办了.

坑点: 打印中文内容, 服务器上需要安装中文字体

轻车熟路: 

yum groupinstall Fonts

安装的也很快, 这次终于打印了一个完美的网页. 

散花了. 

 

Read More

2024-11-08
debian12安装chrome

谷歌浏览器是全球最受欢迎的网络浏览器之一,为用户提供功能强大、功能丰富且用户友好的体验。对于 Debian 12 书虫用户,安装 Google Chrome 可确保最佳的网页浏览性能和访问大量扩展程序。

Google-Chrome-logo

在 Debian 12 书虫上安装 Google Chrome

第 1 步。在我们安装任何软件之前,通过在终端中运行以下命令来确保您的系统是最新的非常重要:apt

1
2
sudo apt update
sudo apt install apt-transport-https lsb-release ca-certificates

此命令将刷新存储库,允许您安装最新版本的软件包。

第 2 步。在 Debian 12 上安装 Google Chrome。

  • 方法1:使用Chrome存储库安装谷歌浏览器

谷歌为Chrome提供了一个专用的存储库,简化了安装过程。使用以下命令将谷歌浏览器存储库添加到您的系统:

1
wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo gpg --dearmor -o /usr/share/keyrings/google-archive-keyring.gpg

接下来,执行以下命令以安装谷歌浏览器存储库:

1
echo "deb [signed-by=/usr/share/keyrings/google-archive-keyring.gpg] http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee /etc/apt/sources.list.d/google-chrome.list

将谷歌浏览器存储库添加到您的系统中后,现在使用以下命令安装谷歌浏览器:

1
2
sudo apt update
sudo apt install google-chrome-stable
  • 方法2:从官方网站安装谷歌浏览器

打开终端并执行以下命令下载谷歌浏览器软件包:

1
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb

接下来,导航到保存 Google Chrome 软件包的目录,然后执行以下命令来安装软件包:

1
sudo apt install ./google-chrome-stable_current_amd64.deb

包管理器将处理安装过程,包括任何必要的依赖项。

第 3 步。在 Debian 上访问 Google Chrome。

要从应用程序菜单启动谷歌浏览器,请单击屏幕左上角的“活动”按钮。然后,搜索“谷歌浏览器”并单击图标以打开浏览器。

google-chrome-browser

第 4 步。排查常见问题。

尽管安装过程通常很简单,但您可能会遇到一些常见问题。以下是解决这些问题的一些故障排除提示:

  • 依赖冲突:

如果在安装过程中遇到依赖项冲突,请运行以下命令进行修复:

1
sudo apt --fix-broken install
  • 安装错误:

如果出现安装错误,请删除所有部分安装,然后重新安装谷歌浏览器:

1
2
3
sudo apt purge google-chrome-stable
sudo apt autoremove
sudo apt install google-chrome-stable

感谢您使用本教程在 Debian 12 书虫上安装最新版本的 Chrome 网页浏览器。如需其他帮助或有用信息,我们建议您查看 Chrome 官方网站。

</article>
            </div>
Read More

2024-06-03
经纬恒润-演示


演示

JSON-TO-PAGE 介绍

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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
// type 标记了 这是一个列表页面
// runOnSaveId 对应这个页面的ID,代表使用该组件时的pageId。
// 页面json配置 通过配置对应flowId 去执行某些接口操作,
// config去配置一些表格相关的配置
// handleBtns 配置了每条数据,后面操作列对应按钮的操作逻辑和显隐控制等信息。
// formConfig 配置了该页面的筛选区域的配置。
{
"runOnSaveId": 218,
"name": "材料管理_管理",
"updateTime": "2024-05-28 15:53:12",
"id": 218,
"type": "table",
"flows": {
"getTableData": {
"flowId": 219
}
},
"handleBtns": [
{
"handleType": "openDialogPage",
"params": {
"pageId": 222,
"dialogProps": {
"dialogType": "updateFormData",
"height": "400px",
"width": "580px",
"title": "选择新的协作者",
"initFormData": {
"companyId": "{{targetId}}",
"companyName": "{{companyName}}"
}
}
},
"successTip": {
"content": "更换成功"
},
"btn": {
"color": "blue",
"size": "small",
"elevation": "0",
"density": "comfortable",
"label": "更换协作者"
},
"runFlowId": "223"
},
{
"handleType": "openFullDialogPage",
"params": {
"pageId": 236,
"dialogProps": {
"dialogType": "updateFormData",
"showFooter": false,
"width": "100%",
"theme": "myCustomLightTheme",
"title": "查看审批材料表数据",
"params": {
"id": "{{id}}"
}
}
},
"btn": {
"color": "blue",
"size": "small",
"elevation": "0",
"density": "comfortable",
"label": "查看审批"
},
"runFlowId": "231",
"runCancelFlowId": "232"
}
],
"config": {
"returnDataKey": "data.rows",
"returnTotalKey": "data.count",
"columns": [
{
"field": "title",
"title": "数据表名称"
},
{
"field": "brandName",
"title": "品牌"
},
{
"field": "companyName",
"title": "公司名称"
},
{
"field": "fitTypeStr",
"title": "类型"
},
{
"field": "user_name",
"title": "协作者"
},
{
"width": "80px",
"field": "status",
"title": "状态",
"fixed": "right",
"params": {
"renderProps": {
"size": "x-small",
"variant": "elevated",
"elevation": 0,
"rounded": "4px"
},
"renderOptions": [
{
"title": "未发起",
"value": -1,
"props": {
"color": "#58637D33",
"style": "color: #58637D"
}
},
{
"title": "审批中",
"value": 1,
"props": {
"color": "#3175FB33",
"style": "color: #3175FB"
}
},
{
"title": "通过",
"value": 2,
"props": {
"color": "#83C44733",
"style": "color: #83C447"
}
},
{
"title": "不通过",
"value": 3,
"props": {
"color": "#FF5B5833",
"style": "color: #FF5B58"
}
},
{
"title": "已撤回",
"value": 4,
"props": {
"color": "#58637D33",
"style": "color: #58637D"
}
}
]
},
"slots": {
"default": "tag"
}
},
{
"width": "220px",
"title": "操作",
"fixed": "right",
"slots": {
"default": "handleBtn"
}
}
],
"toolbarConfig": {
"buttons": [
{
"code": "uploadDatas",
"name": "添加材料表",
"handleType": "openDialogPage",
"status": "primary",
"runFlowId": 212,
"successTip": {
"content": "生成成功"
},
"pageId": 215,
"dialogProps": {
"width": "580px",
"height": "600px",
"dialogType": "getFormData",
"theme": "dark",
"title": "录入材料表信息"
}
}
],
"refresh": true,
"export": true,
"custom": true
},
"baseConfig": {
"border": true,
"showHeaderOverflow": true,
"showOverflow": "true",
"keepSource": true,
"id": "TEST_TABLE",
"height": "auto",
"rowConfig": {
"keyField": "id",
"isHover": true
},
"columnConfig": {
"resizable": true
},
"editConfig": {
"trigger": "click",
"mode": "cell"
}
},
"pagerConfig": {
"pageSize": 100,
"pageSizes": [
30,
50,
100
]
},
"formConfig": {
"titleWidth": 60,
"titleAlign": "left",
"items": [
{
"field": "brand_name",
"title": "品牌",
"titleWidth": "85px",
"span": 8,
"itemRender": {
"name": "$input",
"props": {
"placeholder": "请输入品牌"
}
}
},
{
"field": "fitType",
"title": "类型",
"titleWidth": "85px",
"span": 8,
"itemRender": {
"name": "$select",
"props": {
"placeholder": "请选择类型",
"clearable": true,
"transfer": false,
"options": [
{
"label": "主材",
"value": 1
},
{
"label": "基材",
"value": 2
}
]
}
}
},
{
"field": "user_name",
"title": "协作者",
"titleWidth": "85px",
"span": 8,
"itemRender": {
"name": "$input",
"props": {
"placeholder": "请输入协作者姓名"
}
}
},

{
"field": "companyId",
"title": "公司",
"span": 8,
"itemRender": {
"name": "$select",
"props": {
"placeholder": "请选择公司",
"transfer": false,
"clearable": true,
"option-props": {
"value": "value",
"label": "title"
},
"flowId": 216
}
}
},
{
"span": 24,
"align": "center",
"itemRender": {
"name": "$buttons",
"children": [
{
"props": {
"type": "submit",
"content": "筛选",
"status": "primary"
}
},
{
"props": {
"type": "reset",
"content": "重置",
"status": "primary"
}
}
]
}
}
]
}
}
}

列表页面

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
// type 表示了这是一个form组组件,在使用的时候去通过定义 pageId: 215,调用这个form组件。
{
"runOnSaveId": 215,
"name": "选择材料信息",
"updateTime": "2024-03-14 16:27:23",
"id": 215,
"type": "form",
"config": {
"formConfig": {
"style": {
"padding": "20px"
},
"itemStyle": {
"margin-bottom": "8px"
},
"textProps": {
"clearable": true
}
},
"formItems": [
{
"key": "companyId",
"value": "",
"requestFlowId": "216",
"watched": true,
"type": "select",
"handleChange": "initItemOptions",
"handleChangeParams": {
"initKeys": [
"brandId",
"user"
]
},
"rules": [
{
"type": "required",
"name": "公司"
}
],
"props": {
"label": "公司",
"variant": "solo-filled"
}
},
{
"key": "user",
"type": "autocomplete",
"requestFlowId": "241",
"requestParams": {
"companyId": "{{companyId}}"
},
"watched": true,
"rules": [
{
"type": "required",
"name": "员工手机号"
}
],
"props": {
"label": "员工手机号",
"placeholder": "选择材料专员飞书所绑定的手机号",
"variant": "solo-filled"
}
},
{
"key": "materialType",
"value": "",
"type": "select",
"watched": true,
"rules": [
{
"type": "required",
"name": "类型"
}
],
"props": {
"label": "类型",
"variant": "solo-filled",
"items": [
{
"title": "主材",
"value": 1
},
{
"title": "基材",
"value": 2
}
]
}
},
{
"key": "brandId",
"value": "",
"type": "autocomplete",
"requestFlowId": "217",
"requestParams": {
"companyId": "{{companyId}}"
},
"watched": true,
"rules": [
{
"type": "required",
"name": "品牌ID"
}
],
"props": {
"label": "品牌",
"variant": "solo-filled"
}
}
]
}
}

弹窗组件

Read More

2023-07-24
快手本地生活前端一面


面试时间 2023年7月20日16点

1. TCP协议为什么是三次握手,四次挥手,为什么断开时会多一次?

因为一个连接建立之前,并不需要考虑其他因素,但是在连接断开之前,客户端和服务端可能还存在其他通信操作,所以就需要多一次连接用来通知双方取消其他通信操作。

2. VUE&SSR 原理,在注水时框架做了什么操作,对比CSR有什么缺点?

Vue框架在浏览器注水时,会根据服务端返回的HTML文本对应生成VNode(虚拟DOM),并且将HTML文本中的静态State等数据初始化在VNode中作为初始化状态数据,以及绑定事件等操作。

3. HTTP协议在每个版本都改进了什么?
http各版本的改进都是什么?

4. WebPack&Vue组件在编译过程是什么样的,Vue3比Vue2做了哪些优化?
编译图示


  1. 将模板进行词法分析,转化为对应的ast树(JS描述对象,与虚拟DOM原理差不多)。

  2. 转换流程transform,对动态节点做一些标记

    • 标记动态节点(Block):指令、事件、插槽、动态属性、模板语法等,渲染时进行动态节点比对即可(靶向更新)。
    • 标记节点动态类型(patchFlag),后续更新时只更新该部分即可,减少比对内容(如文本、class)。
  3. 生成代码codegen – 虚拟dom

  4. 经过render方法将该虚拟dom挂载到宿主元素上

  5. render时直接比对动态节点。


Vue3源码-运行时编译模板&优化总结
Vue3模板编译优化

5. 笔试题
笔试内容

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
class Lottery {
users: string[] = []
count: number
constructor(users: string[], count: number) {
users.sort(() => Math.random() > .5 ? 1 : -1)
this.users = users
this.count = count
}

getRadomIndex() {
return Math.floor(Math.random() * (this.users.length + 1))
}

timer: any = null
goUsers: string[] = []
go() {
const startIndex = this.getRadomIndex()
this.timer = setTimeout(() => {
this.goUsers.push(this.users[startIndex])
this.timer && clearTimeout(this.timer)
this.timer = null
this.go()
}, 100)
}

clearTimeout() {
if (this.timer) {
clearTimeout(this.timer)
this.timer = null
}
}

getRes() {
return this.goUsers[this.goUsers.length - 1]
}

runTime = 0
start() {
this.runTime++
this.go()
}

stop() {
return this.runTime <= this.count ? this.getRes() : null
}
}
Read More

2023-05-17
webpack5编译速度优化

使用webpack5 默认的配置,在开发阶段编译速度太慢,优化完成之后,可以从60S的速度提升到1S的速度~

没优化之前

  • 初次启动项目需要将近100S
    初次启动项目 98S

  • 热更新耗时68S 热更新耗时68S

优化之后

  • 初次启动项目仅需要不到30S
    初次启动项目 30S

  • 热更新耗时不到2S 热更新耗时不到2S

1. 处理webpack缓存
官方文档

1
2
3
4
5
6
7
8
9
10
11
// webpack.config.js
module.exports = (env) => {
...,
// 缓存生成的 webpack 模块和 chunk,来改善构建速度。cache 会在开发 模式被设置成 type: 'memory' 而且在 生产 模式 中被禁用。 cache: true 与 cache: { type: 'memory' } 配置作用一致。 传入 false 会禁用缓存 默认 false
cache: env.WEBPACK_BUILD ? false :
{
type: 'filesystem' // 将 cache 类型设置为内存或者文件系统。memory 选项很简单,它告诉 webpack 在内存中存储缓存,不允许额外的配置 默认 memory
},
...

}

2. 处理babel-loader缓存
官方文档

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
// 在rules: babel-loader 中,如果 将node_modules 不排除的话,将会有不可计数的modules参与计算,会大大减慢编译速度。
// babel-loader options: 设置缓存之后,可以将loader的执行结果进行缓存,之后的变异将会读取缓存。

// webpack.config.js
module.exports = (env) => {
...,
module: {
rules: [
...,
{
test: /\.js$/,
exclude: {
and: [/node_modules/], // Exclude libraries in node_modules ...
not: [
// Except for a few of them that needs to be transpiled because they use modern syntax
/xiangxin-element/,
],
},
use: [
{
loader: "babel-loader",
options: {
cacheDirectory: !env.WEBPACK_BUILD,// 是否开启babel编译缓存 默认值为 false
cacheCompression: !!env.WEBPACK_BUILD, // 缓存文件是否压缩 默认值为 true 压缩
presets: [
[
"@babel/preset-env",
{
useBuiltIns: "usage",
corejs: {
version: 3,
proposals: true, // 使用尚在“提议”阶段特性的 polyfill
},
targets: "ie 11",
},
],
],
// 下面指的是在生成的文件中,不产生注释
comments: false,
},
},
],
},
...
],
},
...
}

3. 代码压缩在开发过程中,无需压缩代码
官方文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// minimize 开发过程中无需代码压缩

// webpack.config.js
module.exports = (env) => {
...,

optimization: {
...
minimize: !!env.WEBPACK_BUILD,
minimizer: [
// 设置打包不开启多线程
new TerserPlugin( {
parallel: !env.WEBPACK_BUILD,
extractComments: false,
} ),
],
},
...
}
Read More

2022-09-15
每日一题(2)

手写Promise

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
class MyPromise {
constructor (exe) {
// 最后的值,Promise .then或者.catch接收的值
this.value = undefined
// 状态:三种状态 pending success failure
this.status = 'pending'
// 成功的函数队列
this.successQueue = []
// 失败的函数队列
this.failureQueue = []
const resolve = (value) => {
const doResolve = () => {
// 将缓存的函数队列挨个执行,并且将状态和值设置好
if (this.status === 'pending') {
this.status = 'success'
this.value = value

while (this.successQueue.length) {
const cb = this.successQueue.shift()

cb && cb(this.value)
}
}
}

setTimeout(doResolve, 0)
}

const reject = (value) => {
// 基本同resolve
const doReject = () => {
if (this.status === 'pending') {
this.status = 'failure'
this.value = value

while (this.failureQueue.length) {
const cb = this.failureQueue.shift()

cb && cb(this.value)
}
}
}

setTimeout(doReject, 0)
}

exe(resolve, reject)
}

then (success = (value) => value, failure = (value) => value) {
// .then返回的是一个新的Promise
return new MyPromise((resolve, reject) => {
// 包装回到函数
const successFn = (value) => {
try {
const result = success(value)
// 如果结果值是一个Promise,那么需要将这个Promise的值继续往下传递,否则直接resolve即可
result instanceof MyPromise ? result.then(resolve, reject) : resolve(result)
} catch (err) {
reject(err)
}
}
// 基本筒成功回调函数的封装
const failureFn = (value) => {
try {
const result = failure(value)

result instanceof MyPromise ? result.then(resolve, reject) : resolve(result)
} catch (err) {
reject(err)
}
}
// 如果Promise的状态还未结束,则将成功和失败的函数缓存到队列里
if (this.status === 'pending') {
this.successQueue.push(successFn)
this.failureQueue.push(failureFn)
// 如果已经成功结束,直接执行成功回调
} else if (this.status === 'success') {
success(this.value)
} else {
// 如果已经失败,直接执行失败回调
failure(this.value)
}
})
}
// 其他函数就不一一实现了
catch () {

}
}
// 以下举个例子,验证一下以上实现的结果
const pro = new MyPromise((resolve, reject) => {
setTimeout(resolve, 1000)
setTimeout(reject, 2000)
})

pro
.then(() => {
console.log('2_1')
const newPro = new MyPromise((resolve, reject) => {
console.log('2_2')
setTimeout(reject, 2000)
})
console.log('2_3')
return newPro
})
.then(
() => {
console.log('2_4')
},
() => {
console.log('2_5')
}
)

pro
.then(
data => {
console.log('3_1')
throw new Error()
},
data => {
console.log('3_2')
}
)
.then(
() => {
console.log('3_3')
},
e => {
console.log('3_4')
}
)
// 2_1
// 2_2
// 2_3
// 3_1
// 3_4
// 2_5
Read More

2022-09-06
每日一题(1)

手写instanceOf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用while递归进行原型链的判断
const instanceOfFn = (obj: any, fn: Function): boolean => {
while (obj !== null) {
if (obj.__proto__ === fn.prototype) {
return true
} else {
return instanceOfFn(obj.__proto__, fn)
}
}
return false
}

console.log(instanceOfFn(null, String)) // false
console.log(instanceOfFn('str', String)) // true
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
class MyPromise {
constructor (exe) {
// 最后的值,Promise .then或者.catch接收的值
this.value = undefined
// 状态:三种状态 pending success failure
this.status = 'pending'
// 成功的函数队列
this.successQueue = []
// 失败的函数队列
this.failureQueue = []
const resolve = (value) => {
const doResolve = () => {
// 将缓存的函数队列挨个执行,并且将状态和值设置好
if (this.status === 'pending') {
this.status = 'success'
this.value = value

while (this.successQueue.length) {
const cb = this.successQueue.shift()

cb && cb(this.value)
}
}
}

setTimeout(doResolve, 0)
}

const reject = (value) => {
// 基本同resolve
const doReject = () => {
if (this.status === 'pending') {
this.status = 'failure'
this.value = value

while (this.failureQueue.length) {
const cb = this.failureQueue.shift()

cb && cb(this.value)
}
}
}

setTimeout(doReject, 0)
}

exe(resolve, reject)
}

then (success = (value) => value, failure = (value) => value) {
// .then返回的是一个新的Promise
return new MyPromise((resolve, reject) => {
// 包装回到函数
const successFn = (value) => {
try {
const result = success(value)
// 如果结果值是一个Promise,那么需要将这个Promise的值继续往下传递,否则直接resolve即可
result instanceof MyPromise ? result.then(resolve, reject) : resolve(result)
} catch (err) {
reject(err)
}
}
// 基本筒成功回调函数的封装
const failureFn = (value) => {
try {
const result = failure(value)

result instanceof MyPromise ? result.then(resolve, reject) : resolve(result)
} catch (err) {
reject(err)
}
}
// 如果Promise的状态还未结束,则将成功和失败的函数缓存到队列里
if (this.status === 'pending') {
this.successQueue.push(successFn)
this.failureQueue.push(failureFn)
// 如果已经成功结束,直接执行成功回调
} else if (this.status === 'success') {
success(this.value)
} else {
// 如果已经失败,直接执行失败回调
failure(this.value)
}
})
}
// 其他函数就不一一实现了
catch () {

}
}
// 以下举个例子,验证一下以上实现的结果
const pro = new MyPromise((resolve, reject) => {
setTimeout(resolve, 1000)
setTimeout(reject, 2000)
})

pro
.then(() => {
console.log('2_1')
const newPro = new MyPromise((resolve, reject) => {
console.log('2_2')
setTimeout(reject, 2000)
})
console.log('2_3')
return newPro
})
.then(
() => {
console.log('2_4')
},
() => {
console.log('2_5')
}
)

pro
.then(
data => {
console.log('3_1')
throw new Error()
},
data => {
console.log('3_2')
}
)
.then(
() => {
console.log('3_3')
},
e => {
console.log('3_4')
}
)
// 2_1
// 2_2
// 2_3
// 3_1
// 3_4
// 2_5
Read More