注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

为什么5.225.toFixed(2)!=5.23,令人摸不着头脑的银行家舍入法

web
前言 很多时候,我们在程序中计算数字,得到的结果也许并不和我们想象得一样,在我们大多数人的认知里几乎都是四舍五入法,但是程序中所呈现的好像并不是我们想要的结果。 今天就谈谈程序中的那匪夷所思得银行家舍入法(也会涉及到数字精度问题) 什么是银行家舍入法 银行家舍...
继续阅读 »

前言


很多时候,我们在程序中计算数字,得到的结果也许并不和我们想象得一样,在我们大多数人的认知里几乎都是四舍五入法,但是程序中所呈现的好像并不是我们想要的结果。

今天就谈谈程序中的那匪夷所思得银行家舍入法(也会涉及到数字精度问题)


什么是银行家舍入法


银行家舍入法,也称为四舍六入五留双或四舍六入五成双,是一种在计算机科学和金融领域广泛使用的舍入方法。


具体操作步骤如下:



  1. 如果被修约的数字小于5,则直接舍去;

  2. 如果被修约的数字大于5,则进行进位;

  3. 如果被修约的数字等于5,则需要查看5前面的数字。如果5前面的数字是奇数,则进位;如果5前面的数字是偶数,则舍去5,即修约后末尾数字都成为偶数。特别需要注意的是,如果5的后面还有不为0的任何数,则无论5的前面是奇数还是偶数,均应进位。


以上可以看出银行家舍入法得规则,当为5时,并不是所有得都会向前进一位,所以就可以知道5.225.toFixed(2)为什么不等于5.23了


举例


在浏览器的控制台中,我们可以试着打印一下


image.png
这个时候我们可以看到,哎,好像是符合我们的所认知得四舍五入法了,但是紧接着


image.png
这里看出,怎么又变成这样的了,这还是银行家舍入法呀,为了更严谨再试一下5前面为奇数时得结果


image.png
这里结果又变了,反而是整数大于等于4得正常了,但是小于4得又有些失常了,反而整数为1得总是按照咱们预想的结果在进行,这种结果让我大脑一片混乱,所以这到底是什么原因,导致结果不像是银行家舍入法,也不像是四舍五入法


在我掉了一花西币的头发后,终于想通了,是程序中的精度问题,我们所写的数字并不是表面那么纯粹,再次打印一下看看


image.png
现在可以清楚看出,我们所写的简单的数字后面并不见简单,之所以1.235和1.225使用toFiexd的时候都准确的四舍五入了,都是因为他的后面是多出来了0.0000000000几的数字,然而2.235就没有那么幸运了,所以2.235的0.005就被舍弃了!


解决方法


先说一种可行但不完全可行的解决方法,就是使用Math.round()
首先这个方法确实是js中提供的真正含义上的四舍五入的方法。


image.png
哎,这么一看,确实可行,既然简单的可以,那我们就试着进行复杂运算一下,再保留一下两位小数试试看


image.png
呕吼,错了,按我们正常来算应该是9.77,但却得到了9.76。

要知道程序中存在着精度问题,再我们算来这个式子的结果应该是9.765,但是在程序看来


image.png
可以说是无限趋近于9.765但还没有达到,然后就在Math.round这个方法中给舍弃掉了,这个方法似乎不完全可行


那么另外一招就是可行但有隐式风险的方式,就是在我们所算出来的结果后面添加0.0000000001,这样再让我们看一下结果


image.png
这样可以看出,无论使用哪种方法,都能达到我们所需的结果了,即使使用toFixed有了银行家舍入法的规则,依旧可以按我们所想的一样进行四舍五入,因为当我们加了0.000000001后,即使最后一位等于5了,5后面还有数字,它就会向前进一位,那如果说加了这0.000000001正好等于5然后又触发了银行家舍入法的规则,那只能说算你倒霉,这就是我说为什么会有隐式风险,有风险但很小。


当然还有一个方法就是自己写一个方法来解决这个问题


//有的时候也许传的参数就是计算过后的,无线趋近于5的数,可以根据需求来判断是否传入第二个参数
Number.prototype.myToFixed = function (n, d) {
//进来之后转为字符串 字符串不存在精度问题
const str = this.toString();
const dotIndex = str.indexOf(".");
//如果没有小数点传进来的就是整数,直接使用toFixed传出去
if (dotIndex === -1) {
return this.toFixed(n);
}
//当为小数的时候
const intStr = str.substring(0, dotIndex);
const decStr = str.substring(dotIndex + 1, str.length).split("");
//当大于5时,就进一
if (decStr[n] >= 5) {
decStr[n - 1] = Number(decStr[n - 1]) + 1;
const dec = decStr.slice(0, n).join("");
return `${intStr}.${dec}`;
} else {
//否则小于五时 先判断是否有第二个参数
if (d) {
//如果有就截取到第二个参数的位置
const newDec = decStr.splice(n, n + d);
let nineSum = 0;
//遍历循环有多少个9
for (let index = 0; index < newDec.length; index++) {
if (index != 0 && newDec[index] == 9) {
nineSum++;
}
}
//判断四舍五入后面的位置 是否为四 并且是否除了4之后全是9 或者 9的位数大于第二个传的参数
if (newDec[0] == 4 && (nineSum >= newDec.length - 2 || nineSum >= d)) {
//条件成立 就按5进一
decStr[n - 1] = Number(decStr[n - 1]) + 1;
const dec = decStr.slice(0, n).join("");
return `${intStr}.${dec}`;
} else {
//不成立则舍一
const dec = decStr.slice(0, n).join("");
return `${intStr}.${dec}`;
}
} else {
//没有第二个参数,小于五直接舍一
const dec = decStr.slice(0, n).join("");
return `${intStr}.${dec}`;
}
}
};

我们再进行测试一下


image.png


image.png
这样就是我们想要的结果了


总结


在程序中,银行家舍入法和数字的精度问题很多时候都会遇见,不论前端还是后端,然而处理这些数据也是比较头疼的事,我所讲的这些也许不能满足所有情况,但大多数情况都是可以处理的。


如果是相对于银行里这种对数字比较敏感的环境,这些参数的处理还需要更加谨慎的处理


写的如有问题,欢迎提出建议


作者:iceCode
来源:juejin.cn/post/7280430881952759862
收起阅读 »

看完这位小哥的GitHub,我沉默了

就在昨天,一个名为win12的开源项目一度冲上了GitHub的Trending热榜。 而且最近项目的Star量也在飙升,目前已经获得了2.2k+的Star标星。 出于好奇,点进去看了看。好家伙,项目README里写道这是一个14岁的初中生所打造的开源项目。...
继续阅读 »

就在昨天,一个名为win12的开源项目一度冲上了GitHub的Trending热榜。



而且最近项目的Star量也在飙升,目前已经获得了2.2k+的Star标星。



出于好奇,点进去看了看。好家伙,项目README里写道这是一个14岁的初中生所打造的开源项目。


即:在网页端实现了Windows 12的UI界面和交互效果。


这里也放几张图片感受一下。

登录页面

开始菜单

资源管理器

设置

终端命令行

AI Copilot

其他应用



这个项目的灵感来源于作者之前看到 Windows 12 概念版后深受启发,于是决定做一个Windows12网页版(就像之前的 Windows 11 网页版一样),可以让用户在网络上预先体验 Windows 12。


可以看到,这个项目是一个前端开源项目,而且由标准前端技术(HTML,JS,CSS)来实现,下载代码,无需安装,打开desktop.html即可。



项目包含:

  • 精美的UI设计
  • 流畅丰富的动画
  • 各种高级的功能(相较于网页版)

不仅如此,作者团队对于该项目的后续发展还做了不少规划和畅想。


  • 项目规划


  • 项目畅想


刚上面也说了,项目README里写道该项目的作者是一位14岁的初中生,网名叫星源,曾获得CSP普及组一等奖和蓝桥杯国赛三等奖。


作者出生于2009年,在成都上的小学和初中,目前刚上初三。


这样来看的话,虽说作者年龄很小,不过接触计算机和编程应该非常早,而且对计算机领域的知识和技术应该有着非常浓厚的兴趣。


从作者的个人主页里能看到,技术栈这块涉猎得还挺广泛。



作者自己表示如今上初三了,对于win12这个项目也不会做什么功能的更新了,后续的维护更新将交给其他贡献者成员。



文章的结尾也附上Windows 12网页版体验地址:tjy-gitnub.github.io/win12/desktop.html,感兴趣的同学可以自行体验。


聊到这里不得不说,人与人之间的差距确实挺大的。就像这位小哥,才14岁就已经精通前端技术了。


而14岁的我,当年在干嘛呢?


我想了又想。。


额,我好像在网吧里玩红警。。(手动doge)


作者:CodeSheep
链接:https://juejin.cn/post/7275978708644151354
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

硬盘坏了,一气之下用 js 写了个恢复程序

web
硬盘坏了,一气之下写了个恢复程序 师傅拯救无望 硬盘已经寄过去超过一周了,一问竟然是还没开始弄??? 再过一周,上来就问我分几个区?我要恢复哪些数据?我要恢复的数据在哪个位置? 那好吧,既然给了钱师傅也都放弃了,我也没什么好寄托希望的了。况且经过这三个...
继续阅读 »

硬盘坏了,一气之下写了个恢复程序


师傅拯救无望


硬盘已经寄过去超过一周了,一问竟然是还没开始弄???


2023-03-24-14-15-16.png


再过一周,上来就问我分几个区?我要恢复哪些数据?我要恢复的数据在哪个位置?


2023-03-24-14-18-50.png


2023-03-24-14-19-30.png


2023-03-24-14-20-05.png


那好吧,既然给了钱师傅也都放弃了,我也没什么好寄托希望的了。况且经过这三个星期的缓解,心情已经平复了很多,就像时光,回不来了就是回不来了。


自救之路


在把硬盘寄过去的时间里,等待师傅的修复结果的时间里,我并没有闲着(在摸鱼)。


经过调研,数据恢复方法通常有:



  • 硬件损坏,对坏的盘进行修复

  • 误删或逻辑错误等,文件扫描修复

  • git 重置恢复


很明显,这些都不适用于我现在的场景。因为师傅能不能修好是未知的,我只是数据盘没了,系统盘还在。由于 vscode 的数据目录空间占比较小,就没有搬迁到数据盘里,这刚好可以为恢复代码提供了可能。


这是因为新版 vscode 有一个时间线功能,这个时间线数据是默认存储在用户目录下的。


我从 C:/Users/love/AppData/Roaming/Code/User/History 目录中确实找到了很多名为 entries.json 的文件,结构如下:


{
// 配置版本
"version": 1,
// 原来文件所在位置
"resource": "file:///d%3A/git2/cloudcmd/.madrun.mjs",
// 文件历史
"entries": [
{
// 历史文件存储的名称
"id": "YFRn.mjs",
"source": "工作区编辑",
// 修改的时间
"timestamp": 1656583915880
},
{
"id": "Vfen.mjs",
"timestamp": 1656585664751
},
]
}

通过上面的文件大概可以看到,每一个时间点的文件都保存在另一个随机命名的文件里。而网上的方法基本都是自己一个个手动到目录里去根据最新的 id 去找对应的文件内容,然后创建文件并把内容复制出来。


这个过程恢复一两个文件还好,但我这可是要恢复整个 git 工作区,大概有几十个项目上千个文件。


这时候当然是在网上找找有没有什么 vscode 数据恢复 相关的工具,很遗憾找了大半天都没有找到。


气死我了,一气之下就自己写个!


恢复程序开发步骤


毕竟只要数据在磁盘上,无非就是一个文件读取操作的问题,还要拿在这水文章,见谅见谅。


首先考虑需求:



  • 我要实现一个自动扫描 vscode 数据目录

  • 然后以原始的目录结构还原出来,不需要我自己去创建文件夹和文件

  • 如果还原的文件最新的那份不是我想要的,我还能根据时间线进行对比和选择

  • 扫描出来有N个项目时,我可以指定只还原某此项目

  • 我可以搜索文件、目录名或文件内容进行还原

  • 为了方便,我还要一个看起来不太丑的操作界面


大概就上面这些吧。


然后考虑实现:


我要实现一个自动扫描 vscode 数据目录


要的就是我自己连数据目录和恢复地址也不需要填写,就能自动恢复的那种。那么就让程序来自动查找数据目录。经过调研,各版本的 vscode 的数据目录一般保存在这些地方:


参考: stackoverflow.com/a/72610691


  - win -- C:\Users\Mark\AppData\Roaming\Code\User\History
- win -- C:\Users\Mark\AppData\Roaming\Code - Insiders\User\History
- /home/USER/.config/VSCodium/User/History/
- C:\Users\USER\AppData\Roaming\VSCodium\User\History

大概有上面这些路径,当然不排除使用者故意把默认位置修改掉这种边缘情况,或者使用者就只想扫描某个数据目录的情况,所以我也要支持手动输入目录:


  let { historyPath, toDir } = req.body
const homeDir = os.userInfo().homedir
const pathList = [
historyPath,
`${homeDir}/AppData/Roaming/Code/User/History/`,
`${homeDir}/AppData/Roaming/Code - Insiders/User/History/`,
`${homeDir}/AppData/Roaming/VSCodium/User/History`,
`${homeDir}/.config/VSCodium/User/History/`,
]
historyPath = (() => {
return pathList.find((path) => path && fs.existsSync(path))
})()
toDir = toDir || normalize(`${process.cwd()}/re-store/`)

然后以原始的目录结构还原出来……


这就需要解析扫描到的时间线文件 entries.json 了。我们先把解析结果放到一个 list 中,以下是一个完整的解析方法。


然后再把列表转换为树型,与硬盘上的状态对应起来,这样便于调试数据和可视化。


function scan({ historyPath, toDir } = {}) {
const gitRoot = `${historyPath}/**/entries.json`

fs.existsSync(toDir) === false && fs.mkdirSync(toDir, { recursive: true })
const globbyList = globby.sync([gitRoot], {})

let fileList = globbyList.map((file) => {
const data = require(file)
const dir = path.parse(file).dir
// entries.json 地址
data.from = file
data.fromDir = dir
// 原文件地址
data.resource = decodeURIComponent(data.resource).replace(
/.*?\/\/\/(.*$)/,
`$1`
)
// 原文件存储目录
data.resourceDir = path.parse(data.resource).dir
// 恢复后的完整地址
data.rresource = `${toDir}/${data.resource.replace(/:\//g, `/`)}`
// 恢复后的目录
data.rresourceDir = `${toDir}/${path
.parse(data.resource)
.dir.replace(/:\//g, `/`)}
`

const newItem = [...data.entries].pop()
// 创建文件所在目录
fs.mkdirSync(data.rresourceDir, { recursive: true })
const binary = fs.readFileSync(`${dir}/${newItem.id}`, {
encoding: `binary`,
})
fs.writeFileSync(data.rresource, binary, { encoding: `binary` })
return data
})

const tree = pathToTree(fileList, { key: `resource` })
return tree
}

为了方便,我还要一个看起来不太丑的操作界面


我们要把文件树的形式展示出来,还要方便切换。后面决定使用 macos 的文件管理器风格,大概如下。


image.png


如果还原的文件最新的那份不是我想要的,我还能根据时间线进行对比和选择


理论上这里应该要做一个像 vscode 对比文件那样,有代码高亮功能,并且把有差异的字符高亮出来。


实际上,这个需求得加钱。


2023-03-24-15-09-25.png


由于界面是在浏览器里的,需要自动打开,浏览器与系统交互需要一个接口,所以我们使用 opener 来自动打开浏览器。


使用 get-port 来自动生成接口服务的端口,避免使用时出现占用。


  const opener = require(`opener`)
const { portNumbers, default: getPort } = await import(`get-port`)
const port = await getPort({ port: portNumbers(3000, 3100) })
const server = express()
server.listen(port, `0.0.0.0`, () => {
const link = `http://127.0.0.1:${port}`
opener(link)
})

封装成工具,我为人人


理论上我根本不需要什么 UI 界面,也不需要配置,因为我的文件都恢复出来了我还花时间去搞毛线?


实际上,万一别人也有这个恢复文件的需要呢?那么他只要运行下面这条命令代码就能立刻恢复到当前目录啦!


npx vscode-file-recovery

这就是恢复后的文件在硬盘里的样子啦:


2023-03-24-15-22-23.png


所有代码位于:



建议收藏,以备不时之需。/手动狗头


作者:程序媛李李李李李蕾
来源:juejin.cn/post/7213994684262826040
收起阅读 »

跨域漏洞,我把前端线上搞崩溃了

web
最近迁移前端资源到云厂商时,我遇到了一个奇怪的问题 —— 线上环境某个 CSS 资源跨域了,后果直接导致项目崩溃不可用。大领导直接找到了我的工位,激动的心,颤抖的手,问题原因就是找不到!!! 很奇怪,我已经提前设置了资源允许跨域访问,提测环境、测试环境也已经...
继续阅读 »

最近迁移前端资源到云厂商时,我遇到了一个奇怪的问题 —— 线上环境某个 CSS 资源跨域了,后果直接导致项目崩溃不可用。大领导直接找到了我的工位,激动的心,颤抖的手,问题原因就是找不到!!!


WX20230807-141353@2x.png


很奇怪,我已经提前设置了资源允许跨域访问,提测环境、测试环境也已经正常运行很久了,理论上不应该出现跨域问题。而且更奇怪的是,这个问题只出现在某个 CSS 文件上。


建议大家在阅读本文时结合目录一起查看。本文详细介绍了从跨域问题发现到跨域问题解决的整个过程,文章还简要提到了前端资源链路。结合上下文来看,对处理前端跨域问题具有一定的参考价值。希望对大家有所帮助。


什么是跨域问题?


跨域及其触发条件


跨域是指在 web 开发中,一个网页的源(origin)与另一个网页的源不同,即它们的协议、域名或端口至少有一个不同。跨域问题是由于浏览器的同源策略而产生的,它限制了一个网页中加载的资源只能来自同一个源,以防止恶意网站在未经允许的情况下访问其他网站的数据。


以下情况会触发跨域问题:



  1. 不同域名:当页面的域名与请求的资源的域名不一致时,会触发跨域问题,如从 example.com 页面请求资源来自 api.example.net

  2. 不同协议:如果页面使用了 https 协议加载,但试图请求非 https 资源,也会触发跨域问题。

  3. 不同端口:如果页面加载的是 example.com:3000,但试图请求资源来自 example.com:4000,同样会触发跨域问题。

  4. 不同子域名:即使是不同的子域名也会被认为是不同的源。例如,subdomain1.example.comsubdomain2.example.com 是不同的源。


image.png


跨域问题会影响到浏览器执行以下操作:



  • JavaScript的XMLHttpRequest或Fetch API请求其他源的资源。

  • 通过<img><link><script>等标签加载其他源的资源。

  • 使用CORS(跨源资源共享)机制实现跨域数据传输。


解决跨域的方法


解决跨域问题的方法有多种,具体的选择取决于你的应用场景。以下是一些常见的跨域解决方法:



  1. 跨域资源共享(CORS) :CORS是一种标准机制,通过在服务器端设置响应头来允许或拒绝跨域请求。这是解决跨域问题的最常见方法。

    • 在服务器端设置响应头中的Access-Control-Allow-Origin字段来指定允许访问的域名或使用通配符*表示允许所有域名访问。

    • 其他相关的CORS头,如Access-Control-Allow-MethodsAccess-Control-Allow-Headers,用于控制允许的HTTP方法和请求头。



  2. JSONP(JSON with Padding): 通过动态创建 <script> 标签来实现跨域请求的技术。服务器端返回的数据被包装在一个函数调用中,该函数名由客户端指定。虽然 JSONP 简单易用,但只支持GET请求,由于安全性较差(容易受到跨站脚本攻击),存在安全风险。
    // 客户端代码
    function handleResponse(data) {
    console.log('Received data:', data);
    }

    const script = document.createElement('script');
    script.src = 'https://example.com/api/data?callback=handleResponse';
    document.head.appendChild(script);


  3. 代理服务器:设置一个位于同一域的代理服务器,将跨域请求代理到目标服务器,并将响应返回给客户端。这个方法需要服务器端的额外配置。

  4. 跨文档消息传递: 使用window.postMessage()方法,可以在不同窗口或iframe之间进行跨域通信。

  5. WebSocket: WebSocket是一种双向通信协议,不受同源策略的限制。通过WebSocket,客户端和服务器可以建立持久连接进行跨域通信。

  6. Nginx反向代理: 使用 Nginx 或其他反向代理服务器可以将跨域请求转发到目标服务器,同时解决跨域问题。这种方法适用于前端无法直接控制目标服务器的情况。


每种方法都有其适用的场景和安全考虑,具体的选择取决于项目的需求和架构。


背景与跨域设置


image.png


项目背景介绍


最近我负责了一个前端迁移第三方云(阿里云)的工作,由于这是一个多项目组合成的微前端项目,我考虑在前端迁移中,尽可能统一各个应用项目流程、规范和技术。一是形成统一的规范和方式,二是团队项目各负责人按照方案迁移各自项目时避免因各自不一致导致出现问题。


而在这其中就存在着资源存储和加载不一致的情况,我遇到了三种不同的方法:




  1. 直接使用云存储提供的资源地址


    这是一种常见方式,但也伴随着一些潜在问题。首先,访问云资源可能会有一定的延迟,尤其对于大型文件或数据集。其次,公共云资源地址可能存在安全性和隐私风险,特别是涉及敏感信息时。此外,直接使用OSS资源地址可能导致资源管理分散,难以跟踪和监控资源的使用情况,也可能限制了一些高级功能的实现,如CDN缓存控制、分布式访问控制以及资源日志记录。


    https://company.oss-cn-beijing-internal.company.com/productdata/gateway/prod/assets/index-a12abcbf.css



  2. 使用配置了云存储的CDN域名地址


    这种方式是我比较推荐的方式,然而团队在配置这块链路上存在一些潜在问题。首先 CDN 请求打到了团队内部的一个老服务器,目前老服务器的稳定性不如预期,稳定性方面较差,出现过故障,影响用户体验。前端迁移到第三方云的主要目的之一就是解耦该服务,提供更稳定的前端资源环境。此外,该服务器与其他服务器存在依赖关系,增加了项目的复杂性和不稳定性,解耦是必要的。并且使用这个 CDN 的项目很多,随着时间推移,项目的增加可能会使得该资源地址的维护变得相当复杂。


    https://static.company.com/productdata/gateway/prod/assets/index-a12abcbf.css



  3. 直接加载服务器内部的前端资源


    直接加载服务器内部的前端资源是通过网站域名的代理访问服务器内部资源的一种方法。有几个项目使用这个种方式,这种方式具备简单、快捷等优势。然而,这种方式可能引入网络延迟和性能影响,因为资源请求需要经过团队内部的服务器。同时,存在单点故障的风险,如果内部服务器发生故障或不可用,将导致网站前端资源无法加载,可能对用户造成不便。它也依赖于团队内部的网络环境,需要保持网络的稳定性和可靠性以确保资源能够顺利加载。




为了统一这三种方式并规避潜在问题,我想到了一个综合性的前端资源请求链路方案。通过将 OSS 存储桶、CDN 和网关服务器相互结合,以提升资源分发速度和安全性,同时减轻 OSS 服务器的负载。此外,我还将所有资源引用集中到一个配置文件中,位于网关服务器,以便轻松进行维护和跟踪。(这里只是简要介绍,我将在后续文章分享详细细节


然而,在初步方案制定后,也需要考虑如何处理在同源策略下可能出现的跨域问题。


image.png


前端静态资源跨域设置


我在OSS存储桶的跨域设置中配置了允许跨域头,使得网页可以通过浏览器访问OSS资源,免受同源策略的限制。


image.png


为什么我会选择在 OSS 存储桶配置呢?


主要因为这个存储桶非常整洁,只有两个项目在使用,已经提前简单配置了跨域处理。而且这两个项目后续会按照前端迁移方案进行统一迁移处理,因此我认为直接在 OSS 存储桶配置跨域会更为简洁和可维护,我还和 SRE 老师调整了一下配置(然而,没想到恰恰因为我的这个行为,导致后面出现了跨域问题)。


此外,为了确保安全性,我采取了以下措施:



  • 将项目单独、分批迁移到阿里云 OSS。

  • 在网关服务器中使用nginx进行项目正则匹配,每次迁移就开放一个项目。
    location ~ ^/(gateway|message)/prod/

  • 项目在提测、测试环境都各自运行一段时间(有的甚至在1~2个月)。

  • 在未迁移到正式环境前,各项目按照各自排期计划进行过多次发版。


这些措施是为了确保有问题,可以在提测环境、测试环境中暴露出来。然而,在迁移第3个项目到正式服环境时,出现了问题。。。


奇怪的CSS资源跨域问题


为什么只有某个CSS文件受影响?


跨域问题通常由浏览器的同源策略引起,该策略限制了来自不同源的资源之间的交互。


如果资源有跨域问题,不应该只有某个CSS文件出现跨域问题呀?


3p55k2cus.png


分析后,我发现浏览器中 CSS 资源的返回头中缺少 CORS 头信息,截图如下:


image.png


正常情况下,应该是下图这样:


image.png


这时候我在想不应该呀,我已经在源站 OSS 存储桶配置了允许跨域头,这里的返回头中应用是要携带的,而且别的文件(如html、js)返回头中都是携带了允许跨域,但是为什么只有这个 CSS 资源的就没有呢?





需要注意的是,通常情况下,HTML 文件本身不受同源策略的限制,因此可以从不同源加载 CSS 文件。但如果 CSS 文件中包含引用其他跨域资源(如字体、图片等),那么同源策略仍然会生效,需要特别注意处理这些跨域资源的加载。


问题的深层原因分析


image.png


排除了自身导致的问题


面对这样一个看似简单的跨域问题,我做了一系列的排查和解决过程。首先,我排除了浏览器缓存、资源代码方面以及浏览器本身的问题,并同 SRE 老师否定了前端资源链路(如OSS、CDN)配置错误的可能性。随后,我们还仔细查看了网关日志,但未能发现问题。


一直没找到导致跨域问题出现的原因,我们也想到了直接在网关服务器或 CDN 中强制加入允许跨域头。然而我们一讨论,发现不行,因为 OSS 中已经配置了跨域,强制加入允许跨域头,会出现双重跨域问题;如果移除 OSS 中跨域头也不行,因为已经有两个项目已经直接引用阿里云地址,移除后那两个项目也会出现跨域问题。





寻求阿里云 CDN 运维工程师的帮助


结合我们自己的分析,我们认为是前端资源请求链路的哪个环节出现了问题,但是迟迟找不到原因,于是我们咨询了阿里云 CDN 运维工程师,因为阿里云 CDN 的日志隔天才出来,所以借此希望通过阿里云 CDN 运维老师能够查看下当天的 CDN 日志,从而找到问题。查看日志后,阿里云 CDN 运维老师也只是给出了日志显示一切正常,但随后我们继续沟通。


随后,给到了我们一个关键点:“OSS要能响应跨域头,请求头必须携带 Origin 请求头”。阿里云 CDN 运维老师也说传任何值都可以,但是我多次查看到浏览器请求已经携带了 Origin 请求头。如下图:


image.png


这就奇怪了!此时测试环境提测环境又无法复现 CORS 跨域问题,我们又不能直接在生产环境调试这个问题。


借助工具复现问题


于是我在思考是否能够在提测环境模拟出加载有问题资源的场景。我想到了可以通过拦截浏览器对提测环境的资源请求地址,并将其代理到具有问题的资源地址上来实现这个目的。为了实现这一方案,我使用了一个名为 GoRes 的谷歌浏览器插件。


image(2).png


成功复现,见下图:


3p55k2cus.png


随后,在多次代理调试中,我发现只有在正式服这个项目的资源地址中出现了这个问题。我和 SRE 老师一起再次确认了提测环境、测试环境和正式环境中各自网关服务器和 CDN 域名等的差异性,当然还是没发现问题!





问题逐渐浮现出水面


经过综合分析,我们怀疑 CDN 缓存可能是导致问题的原因。然而,我们无法直接查看缓存的资源,只能再次联系阿里云 CDN 的运维老师。经过多次沟通,我们得知如果客户端在第一次请求 CDN 时没有携带 Origin 请求头,CDN 就不会将 Origin 请求头传递到 OSS,OSS 因此不会响应跨域头,而后续 CDN 便会将没有跨域头的资源内容缓存下来。


这时我才意识到,OSS 内部存在着对 Origin 辨别的跨域处理机制。而在此之前,上传代码资源到 OSS 后,由于是正式环境,为了安全起见测试资源是否上传成功,我直接在浏览器中访问了一个 CSS 文件地址(当时请求到了资源,我还信心满满,丝毫没有注意到还有这么一个坑),但这一步的操作却间接成为了导致跨域问题出现的导火索


通常情况下,当网页加载跨域资源时,由于违反了同源策略,浏览器会自动添加源 Origin 到资源的请求头中。然而,由于我直接请求了 CSS 资源地址,未触发同源策略,浏览器也就没有自动添加 Origin 请求头,导致请求到的 OSS 资源中没有跨域头配置。这也就是为什么 CDN 缓存了没有跨域头的资源。


在网页加载资源时,由于 CDN 缓存了没有跨域头的资源,无论你如何请求,只要 CDN 缓存一直存在,浏览器加载的就是没有跨域头的资源。 因此,这也导致了资源跨域问题的出现。



本来是为了谨慎一点,提前验证资源是否已上传成功的操作,没想到却成为了跨域问题出现的导火索!!!



image.png


这个问题的教训很深刻,让我们意识到必须在向 OSS 请求资源时强制添加 Origin 请求头,以确保今后前端资源的稳定性。否则,这个问题将成为一个定时炸弹。我们决定在网关服务器上分工合作解决这个问题,以杜绝类似情况再次发生。这个经验教训也提醒了我和SRE老师要更加谨慎地处理类似操作,以避免潜在的问题。


如何稳定解决跨域问题


尽管我们已经找到了问题的根源,但是不排除是不是还有其他类似问题,为了保险起见,我决定还是缩小影响范围。在确保测试无问题后,逐步放开所有项目。


SRE 老师负责处理向 OSS 传递Origin请求头的部分,而我负责处理 Nginx location 的正则匹配项目的部分。以下是我们的网关服务器配置:


location ~ ^/(message|dygateway|logcenter)/tice/ { 
set $cors_origin "";
if ($http_origin = "" ) {
set $cors_origin 'static.example.com';
}
if ($http_origin != "") {
set $cors_origin $http_origin;
}
proxy_set_header Origin $cors_origin;
}



  • location ~ ^/(message|dygateway)/tice/:这是一个正则匹配,能更容易地添加或移除项目。




  • proxy_set_header Origin $cors_origin;:如果请求中包含 Origin 头部,它会被直接传递给 OSS;如果没有,它会被设置为一个值后再传递给 OSS。




配置完成后,直接在浏览器中请求下面这个资源地址,你会发现请求头并没有添加上去。这并不是配置出错,而是因为上面我们提到的CDN不仅缓存了资源,还缓存了请求头。


https://static.company.com/productdata/gateway-admin/prod/assets/index-a12abcbf.css

所以我们在这个资源的地址后面拼接了参数,相当于是请求新的 CDN 资源地址,此时可以发现跨域头已经添加上了。


https://static.company.com/productdata/gateway-admin/prod/assets/index-a12abcbf.css?abc=111

image.png


接下来就是在真实项目中测试下,首先在 CDN 后台刷新了有问题的项目资源文件目录,清除掉有跨域问题 CDN 资源缓存后。然后重新刷新浏览器,此时这个文件就成功加上了跨域的请求头,页面访问也正常了。





image.png


image.png


后面我又测试了多次,跨域问题彻底解决。为了避免以后出现类型的问题,所以我又整理了跨域资源共享(CORS)方案,希望对大家有用,请大家接着往下看。


跨域资源共享方案


image.png


跨域资源共享方案是解决前端资源跨域问题的最常见方法,可维护性强,配置简单,可以说这是业界普遍处理前端资源跨域的方式。下面我们将深入探讨三种不同的 CORS 配置方案,并分析各自的优缺点。


OSS存储桶配置跨域


我们都知道 OSS(对象存储服务)是阿里云提供的海量、安全、低成本、高可靠的云存储服务。但其实 OSS 也能设置跨域处理,可以让客户端前端应用从其他域名请求资源。


实施步骤:



  1. 登录阿里云控制台,找到对应的OSS存储桶。

  2. 进入存储桶的管理界面,选择“跨域设置”。

  3. 添加CORS规则,指定允许的来源、方法、头信息等。


image.png


优点:



  • 简单易用:配置简单,通过图形界面即可完成。

  • 安全性高:可以灵活控制允许访问的来源,减少安全风险。


缺点:



  • 依赖云服务商:此方法只适用于使用阿里云OSS的情况,不适用于其他云服务商或自建服务器。


注意:OSS存储桶配置完成跨域后,需要在请求 OSS 存储桶资源时,在请求头中配置 Origin。因为 OSS 内部的机制是 OSS 响应跨域头的前提是必须要携带源站Origin请求头。 建议大家强制配置必传 Origin 请求头,否则容易出现我这次的问题。使用OSS存储桶配置跨域制定方案时,可以参考我在上面的处理:“如何稳定解决跨域问题”。


网关服务器配置跨域


在网关服务器配置跨域,网关服务器通常配置了 Nginx 反向代理服务器。通过配置 Nginx location,可以实现对特定域名的允许跨域支持。


实施步骤:



  1. 修改nginx配置文件(通常位于/etc/nginx/nginx.conf),添加CORS相关配置。

  2. 配置Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers等头信息。


location / {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
}

优点:



  • 灵活性高:可以自由配置适应特定需求。

  • 适用性广:适用于各种服务器环境,不依赖特定云服务商。


缺点:



  • 配置复杂:需要熟悉nginx配置。


CDN配置跨域


CDN(内容分发网络)是一种通过将内容缓存到全球各地节点,加速用户访问速度的网络服务。但也能通过 CDN 配置 CORS,可以在边缘节点处实现跨域。


实施步骤:



  1. 登录CDN服务提供商的控制台,找到相应CDN加速域名。

  2. 进入域名配置界面,找到CORS配置选项。

  3. 添加CORS规则,指定允许的来源、方法、头信息等。


image.png


优点:



  • 高性能:CDN 服务通常提供全球分发,可以加速跨域请求,提供更好的性能。

  • 规模化:适用于大规模的Web应用,可支持高并发的跨域请求。


缺点:



  • 成本:使用 CDN 服务可能会产生额外的费用,特别是对于大量的数据传输。

  • 配置复杂性:相对于 OSS 或 Nginx,CDN 的配置可能会更为复杂,需要在控制台进行详细的设置。


注意:腾讯云 CDN 中有专门针对跨域设置的勾选项,只需要选中保存就行。


三种跨域处理方案各有优缺点,选择合适的方案取决于具体的业务需求和技术栈。我上面所说的也只供大家参考,毕竟 CDN、存储桶这种很大程度受限于云平台,这也是我把允许跨域配置在网关服务器的原因之一。可以综合考虑选择合适的方案或者结合多种方案来实现跨域资源共享。


u=2094032080,194978745&fm=30&app=106&f=JPEG.jpeg


结语


前端资源加载问题往往受多种因素的影响,包括 CDN 配置、资源请求链路、云存储配置等。因此,需要全面分析并综合考虑可能出现问题的任何风险点。也要合理使用浏览器插件工具、网络抓包工具和服务器日志分析等工具,可以帮助我们更快速地诊断和解决问题。如果问题复杂或涉及云服务配置,与云厂商的支持团队联系可以提供专业的帮助。


这是我关于资源跨域的一篇文章,里面关于定位问题和跨域方案希望对您有所帮助和参考。如果您需要进一步的协助或有任何问题,请随时提问!


作者:Sailing
来源:juejin.cn/post/7279429009796546623
收起阅读 »

别再用 float 布局了,flex 才是未来!

web
大家好,我是树哥! 前面一篇文章整体介绍了 CSS 的布局知识,其中说到 float 布局是 CSS 不断完善的副产物。而在 2023 年的今天,flex 这种布局方式才是未来!那么今天我们就来学习下 flex 弹性布局。 什么是 Flex 布局? 在经过了长...
继续阅读 »

大家好,我是树哥!


前面一篇文章整体介绍了 CSS 的布局知识,其中说到 float 布局是 CSS 不断完善的副产物。而在 2023 年的今天,flex 这种布局方式才是未来!那么今天我们就来学习下 flex 弹性布局。


什么是 Flex 布局?


在经过了长达 10 年的发展之后,CSS3 才终于迎来了一个简单好用的布局属性 —— flex。Flex 布局又称弹性布局,它使用 flexbox 属性使得容器有了弹性,可以自动适配各种设备的不同宽度,而不必依赖于传统的块状布局和浮动定位。


举个很简单地例子,如果我们想要实现一个很简单左侧定宽,右侧自适应的导航布局,如下图所示。


-w1239


在没有 flex 之前,我们的代码是这么写的。


<div>
<h1>4.1 两栏布局 - 左侧定宽、右侧自适应 - float</h1>
<div class="container">
<div class="left41"></div>
<div class="right41"></div>
</div>
</div>

/** 4.1 两栏布局 - 左侧定宽、右侧自适应 - float **/
.left41 {
float: left;
width: 300px;
height: 500px;
background-color: pink;
}
.right41 {
width: 100%;
height: 500px;
background-color: aquamarine;
}

这种方式不好的地方在于,我们还需要去理解 float 这个概念。一旦需要理解 float 这个概念,我们就会拖出一大堆概念,例如文档流、盒子模型、display 等属性(虽然这些东西确实应该学)。但对于 flex 来说,它就很简单,只需要设置一个伸缩系数即可,如下代码所示。


<div>
<h1>4.2 两栏布局 - 左侧定宽、右侧自适应 - flex</h1>
<div class="container42">
<div class="left42"></div>
<div class="right42"></div>
</div>
</div>

.container42 {
display: flex;
}
.left42 {
width: 300px;
height: 500px;
background-color: pink;
}
.right42 {
flex: 1;
width: 100%;
height: 500px;
background-color: aquamarine;
}

上面的代码里,我们只需要将父级容器设置为 flex 展示形式(display: flex),随后在需要自动伸缩的容器里设置属性即可。上面代码中的 flex: 1 表示其占据所有其他当行所剩的空间。通过这样的方式,我们非常方便地实现了弹性布局。


当然,上面只是一个最简单的例子,甚至还不是很能体现出 flex 的价值。flex 除了在响应式布局方面非常方便之外,它在对齐等方面更加方便,能够极大地降低学习成本、提高工作效率。


Flex 核心概念


对于 Flex 布局来说,其有几个核心概念,分别是:主轴与交叉轴、起始线和终止线、Flex 容器与 Flex 容器项。


主轴和交叉轴


在 Flex 布局中有一个名为 flex-direction 的属性,可以取 4 个值,分别是:



  • row

  • row-reverse

  • column

  • column-reverse


如果你选择了 row 或者 row-reverse,那么主轴(Main Axis)就是横向的 X 轴,交叉轴(Cross Axis)就是竖向的 Y 轴,如下图所示。


主轴是横向的X轴,交叉轴是竖向的Y轴


如果你选择了 column 或者 column-reverse,那么主轴(Main Axis)就变成是竖向的 Y 轴,交叉轴(Cross Axis)就是横向的 X 轴,如下图所示。


主轴是竖向的Y轴,交叉轴是横向的X轴


起始线和终止线


过去,CSS 的书写模式主要被认为是水平的,从左到右的。但现代的布局方式涵盖了书写模式的范围,所以我们不再假设一行文字是从文档的左上角开始向右书写的。


对于不同的语言来说,其书写方向不同,例如英文是从左到右,但阿拉伯文则是从右到左。那么对于这两种语言来说,其xx会有所不同 TODO。举个简单的例子,如果 flex-direction 是 row ,并且我是在书写英文。由于英文是从左到右书写的,那么主轴的起始线是左边,终止线是右边,如下图所示。


-w557


但如果我在书写阿拉伯文,由于阿拉伯文是从右到左的,那么主轴的起始线是右边,终止线是左边,如下图所示。


-w541


在 Flex 布局中,起始线和终止线决定了 Flex 容器中的 Flex 元素从哪个方向开始排列。 举个简单例子,如果我们通过 direction: ltr 设置了文字书写方向是从左到右,那么起始线就是左边,终止线就是右边。此时,如果我们设置的 flex-direction 值是 row,那么 Flex 元素将会从左到右开始排列。但如果我们设置的 flex-direction 值是 row-reverse,那么 Flex 元素将会从右到左开始排列。


在上面的例子中,交叉轴的起始线是 flex 容器的顶部,终止线是底部,因为两种语言都是水平书写模式。但如果有一种语言,它的书写形式是从底部到顶部,那么当设置 flex-direction 为 column 或 column-reverse 时,也会有类似的变化。


Flex 容器与 Flex 元素


我们把一个容器的 display 属性值改为 flex 或者 inline-flex 之后,该容器就变成了 Flex 容器,而容器中的直系子元素就会变为 flex 元素。如下代码所示,parent 元素就是 Flex 容器,son 元素就是 Flex 元素。


<style>
#parent {
display: flex;
}
</style>
<div id="parent">
<div id="son"></div>
</div>

Flex 核心属性


对于 Flex 来说,它有非常多的用法,但核心属性却相对较少。这里我只简单介绍几个核心属性,如果你想了解更多 Flex 的属性,可以去 Mozilla 官网查询,这里给个传送门:flex 布局的基本概念 - CSS:层叠样式表 | MDN


对于 Flex 布局来说,其核心属性有如下几个:



  1. flex-direction 主轴方向

  2. flex 伸缩系数及初始值

  3. justify-content 主轴方向对齐

  4. align-items 交叉轴方向对齐


flex-direction 主轴方向


如上文所介绍过的,flex-direction 定义了主轴的方向,可以取 4 个值,分别是:



  • row 默认值

  • row-reverse

  • column

  • column-reverse


一旦主轴确定了,交叉轴也确定了。主轴和交叉轴与后续的对齐属性有关,因此弄懂它们非常重要!举个很简单的例子,如下的代码将展示下图的展示效果。


.box {
display: flex;
flex-direction: row-reverse;
}

<div class="box">
<div>One</div>
<div>Two</div>
<div>Three</div>
</div>

-w538


如果你将 flex-direction 改成 column-reverse,那么将会变成如下的效果,如下图所示。


-w541


flex 伸缩系数及初始值


前面说到 Flex 布局可以很方便地进行响应式布局,其实就是通过 flex 属性来实现的。flex 属性其实是 flex-grow、flex-shrink、flex-basis 这三个参数的缩写形式,如下代码所示。


flex-grow: 1;
flex-shrink: 1;
flex-basis: 200px;
/* 上面的设置等价于下面 flex 属性的设置 */
flex: 1 1 200px;

在考虑这几个属性的作用之前,需要先了解一下 可用空间 available space 这个概念。这几个 flex 属性的作用其实就是改变了 flex 容器中的可用空间的行为。


假设在 1 个 500px 的容器中,我们有 3 个 100px 宽的元素,那么这 3 个元素需要占 300px 的宽,剩下 200px 的可用空间。在默认情况下,flexbox 的行为会把这 200px 的空间留在最后一个元素的后面。


-w537


如果期望这些元素能自动地扩展去填充满剩下的空间,那么我们需要去控制可用空间在这几个元素间如何分配,这就是元素上的那些 flex 属性要做的事。


flex-basis


flex-basis 属性用于设置 Flex 元素的大小,其默认值是 auto。此时浏览器会检查元素是否有确定的尺寸,如果有确定的尺寸则用该尺寸作为 Flex 元素的尺寸,否则就采用元素内容的尺寸。


flex-grow


flex-grow 若被赋值为一个正整数,flex 元素会以 flex-basis 为基础,沿主轴方向增长尺寸。这会使该元素延展,并占据此方向轴上的可用空间(available space)。如果有其他元素也被允许延展,那么他们会各自占据可用空间的一部分。


举个例子,上面的例子中有 a、b、c 个 Flex 元素。如果我们给上例中的所有元素设定 flex-grow 值为 1,容器中的可用空间会被这些元素平分。它们会延展以填满容器主轴方向上的空间。


但很多时候,我们可能都需要按照比例来划分剩余的空间。此时如果第一个元素 flex-grow 值为 2,其他元素值为 1,则第一个元素将占有 2/4(上例中,即为 200px 中的 100px), 另外两个元素各占有 1/4(各 50px)。


flex-shrink


flex-grow 属性是处理 flex 元素在主轴上增加空间的问题,相反 flex-shrink 属性是处理 flex 元素收缩的问题。如果我们的容器中没有足够排列 flex 元素的空间,那么可以把 flex 元素 flex-shrink 属性设置为正整数,以此来缩小它所占空间到 flex-basis 以下。


与flex-grow属性一样,可以赋予不同的值来控制 flex 元素收缩的程度 —— 给flex-shrink属性赋予更大的数值可以比赋予小数值的同级元素收缩程度更大。


justify-content 主轴方向对齐


justify-content 属性用来使元素在主轴方向上对齐,它的初始值是 flex-start,即元素从容器的起始线排列。justify-content 属性有如下 5 个不同的值:



  • flex-start:从起始线开始排列,默认值。

  • flex-end::从终止线开始排列。

  • center:在中间排列。

  • space-around:每个元素左右空间相等。

  • space-between:把元素排列好之后,剩余的空间平均分配到元素之间。


各个不同的对齐方式的效果如下图所示。


flex-start:


-w454


flex-end:


-w444


center:


-w449


space-around:


-w442


space-between:


-w453


align-items 交叉轴方向对齐


align-items 属性可以使元素在交叉轴方向对齐,它的初始值是 stretch,即拉伸到最高元素的高度。align-items 属性有如下 5 个不同的值:



  • stretch:拉伸到最高元素的高度,默认值。

  • flex-start:按 flex 容器起始位置对齐。

  • flex-end:按 flex 容器结束为止对齐。

  • center:居中对齐。

  • baseline:始终按文字基线对齐。


各个不同的对齐方式的效果如下图所示。


stretch:


-w448


flex-start:


-w439


flex-end:


-w438


center:


-w444


要注意的事,无论 align-items 还是 justify-content,它们都是以主轴或者交叉轴为参考的,而不是横向和竖向为参考的,明白这点很重要。


Flex 默认属性


由于所有 CSS 属性都会有一个初始值,所以当没有设置任何默认值时,flex 容器中的所有 flex 元素都会有下列行为:



  • 元素排列为一行 (flex-direction 属性的初始值是 row)。

  • 元素从主轴的起始线开始。

  • 元素不会在主维度方向拉伸,但是可以缩小。

  • 元素被拉伸来填充交叉轴大小。

  • flex-basis 属性为 auto。

  • flex-wrap 属性为 nowrap。


弄清楚 Flex 元素的默认值有利于我们更好地进行布局排版。


实战项目拆解


看了那么多的 Flex 布局知识点,总感觉干巴巴的,是时候来看看别人在项目中是怎么用的了。


-w1290


上面是我在 CodePen 找到的一个案例,这样的一个布局就是用 Flex 布局来实现的。通过简单的分析,其实我们可以拆解出其 Flex 布局方法,大致如下图所示。


-w1297


首先整体分为两大部分,即导航栏和内容区域,这部分的主轴纵向排列的(flex-direction: column),如上图红框部分。随后在内容区域,又将其分成了左边的导航栏和右边的内容区域,此时这块内容是横向排列的(flex-direction: row),如下上图蓝框部分。


剩下的内容布局也大致类似,其实就是无限套娃下去。由于偏于原因,这里就不继续深入拆解了,大致的布局思路已经说得很清楚了。


有了 Flex 布局之后,貌似布局也变得非常简单了。但纸上得来终觉浅,还是得自己实际动手练练才知道容易不容易,不然就变成纸上谈兵了!


总结


看到这里,关于 Flex 布局的核心点就介绍得差不多了。掌握好这几个核心的知识点,开始去实践练习基本上没有什么太大的问题了。剩下的一些比较小众的属性,等用到的时候再去查查看就足够了。


接下来更多的时间,就是找多几个实战案例实践,唯有实践才能巩固所学知识点。后面有机会,我将分享我在 Flex 布局方面的项目实践。


如果这篇文章对你有帮助,记得一键三连支持我!


参考资料



作者:树哥聊编程
来源:juejin.cn/post/7280054182996033548
收起阅读 »

看完这位小哥的GitHub,我沉默了

web
就在昨天,一个名为win12的开源项目一度冲上了GitHub的Trending热榜。 而且最近项目的Star量也在飙升,目前已经获得了2.2k+的Star标星。 出于好奇,点进去看了看。好家伙,项目README里写道这是一个14岁的初中生所打造的开源项目。...
继续阅读 »

就在昨天,一个名为win12的开源项目一度冲上了GitHub的Trending热榜。



而且最近项目的Star量也在飙升,目前已经获得了2.2k+的Star标星。



出于好奇,点进去看了看。好家伙,项目README里写道这是一个14岁的初中生所打造的开源项目。


即:在网页端实现了Windows 12的UI界面和交互效果。


这里也放几张图片感受一下。



  • 登录页面




  • 开始菜单




  • 资源管理器




  • 设置




  • 终端命令行




  • AI Copilot




  • 其他应用



这个项目的灵感来源于作者之前看到 Windows 12 概念版后深受启发,于是决定做一个Windows12网页版(就像之前的 Windows 11 网页版一样),可以让用户在网络上预先体验 Windows 12。


可以看到,这个项目是一个前端开源项目,而且由标准前端技术(HTML,JS,CSS)来实现,下载代码,无需安装,打开desktop.html即可。



项目包含:



  • 精美的UI设计

  • 流畅丰富的动画

  • 各种高级的功能(相较于网页版)


不仅如此,作者团队对于该项目的后续发展还做了不少规划和畅想。



  • 项目规划




  • 项目畅想



刚上面也说了,项目README里写道该项目的作者是一位14岁的初中生,网名叫星源,曾获得CSP普及组一等奖和蓝桥杯国赛三等奖。


作者出生于2009年,在成都上的小学和初中,目前刚上初三。


这样来看的话,虽说作者年龄很小,不过接触计算机和编程应该非常早,而且对计算机领域的知识和技术应该有着非常浓厚的兴趣。


从作者的个人主页里能看到,技术栈这块涉猎得还挺广泛。



作者自己表示如今上初三了,对于win12这个项目也不会做什么功能的更新了,后续的维护更新将交给其他贡献者成员。



文章的结尾也附上Windows 12网页版体验地址:tjy-gitnub.github.io/win12/desktop.html,感兴趣的同学可以自行体验。


聊到这里不得不说,人与人之间的差距确实挺大的。就像这位小哥,才14岁就已经精通前端技术了。


而14岁的我,当年在干嘛呢?


我想了又想。。


额,我好像在网吧里玩红警。。(手动doge)


作者:CodeSheep
来源:juejin.cn/post/7275978708644151354
收起阅读 »

蒙提霍尔问题

最近看韩国电视剧【D.P:逃兵追缉令】里面提到一个有趣的数学概率游戏 -> 蒙提霍尔问题 意思是:参赛者会看见三扇门,其中一扇门的里面有一辆汽车,选中里面是汽车的那扇门,就可以赢得该辆汽车,另外两扇门里面则都是一只山羊,让你任意选择其中一个,然后打开其余...
继续阅读 »



最近看韩国电视剧【D.P逃兵追缉令】里面提到一个有趣的数学概率游戏 -> 蒙提霍尔问题


意思是:参赛者会看见三扇门,其中一扇门的里面有一辆汽车,选中里面是汽车的那扇门,就可以赢得该辆汽车,另外两扇门里面则都是一只山羊,让你任意选择其中一个,然后打开其余两个门中的一个并且是山羊(去掉一个错误答案),这时,让你重新选择。那么你是会坚持原来的选择,还是换选另外一个未被打开过的门呢?


大家可以想一想如果是自己,我们是会换还是不会换?


好了,我当时看到后感觉很有意思,所以我简单写了一套代码,源码贴在下面,大家可以验证一下,先告诉大家,换赢得汽车的概率是2/3,不换赢得汽车的概率是1/3

<header>
<h1>请选择换不换?</h1><button class="refresh">刷新</button>
</header>
<section>
<div class="box">
<h2>1</h2>
<canvas width="300" height="100"></canvas>
<div class="prize">奖品</div>
</div>
<div class="box">
<h2>2</h2>
<canvas width="300" height="100"></canvas>
<div class="prize">奖品</div>
</div>
<div class="box">
<h2>3</h2>
<canvas width="300" height="100"></canvas>
<div class="prize">奖品</div>
</div>
</section>
<span>请选择号码牌</span>
<select name="" id="">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button class="confirm">确认</button>
<span class="confirm-text"></span>
<span class="opater">
<button class="change">换</button>
<button class="no-change">不换</button>
</span>
<p>
<strong>游戏规则:</strong>
<span>
上面有三个号码牌,其中一个号码牌的里面有汽车,选中里面是汽车的号码牌,
你就可以赢得该辆汽车,另外两个号码牌里面则都是一只山羊,
你任意选择其中一个,然后打开其余两个号码牌中的一个并且是山羊(去掉一个错误答案),
这时,你有一个重新选择的机会,你选择换还是不换?
</span>
</p>
.prize {
width: 300px;
height: 100px;
background-color: pink;
font-size: 36px;
line-height: 100px;
text-align: center;
position: absolute;
}

canvas {
position: absolute;
z-index: 2;
}

section {
display: flex;
}

.box {
width: 300px;
height: 200px;
cursor: pointer;
}

.box+.box {
margin-left: 8px;
}

header {
display: flex;
align-items: center;
}

header button {
margin-left: 8px;
height: 24px;
}
p {
width: 400px;
background-color: pink;
}
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
function getRandomNumber() {
return Math.random() > 0.5 ? 1 : 2;
}
let a1 = [0, 1, 2]
let i1 = undefined
let i2 = undefined
let isChange = false
const opater = document.querySelector('.opater')
opater.style.display = 'none'
// 随机一个奖品
const prizes = document.querySelectorAll('.prize')
let a0 = [0, 1, 2]
a0 = shuffleArray(a0)
a0.forEach((v,i) => {
const innerText = !!v ? '山羊' : '汽车'
prizes[i].innerText = innerText
})

const canvas = document.querySelectorAll('canvas')
const confirmText = document.querySelector('.confirm-text')
canvas.forEach(c => {
// 使用canvas实现功能
// 1. 使用canvas绘制一个灰色的矩形
const ctx = c.getContext('2d')
ctx.fillStyle = '#ccc'
ctx.fillRect(0, 0, c.width, c.height)
// 2. 刮奖逻辑
// 鼠标按下且移动的时候,需要擦除canvas画布
let done = false
c.addEventListener('mousedown', function () {
if (i1 === undefined) return alert('请先选择号码牌,并确认!')
if (!isChange) return alert('请选择换不换!')
done = true
})
c.addEventListener('mousemove', function (e) {
if (done) {
// offsetX 和 offsetY 可以获取到鼠标在元素中的偏移位置
const x = e.offsetX - 5
const y = e.offsetY - 5
ctx.clearRect(x, y, 10, 10)
}
})
c.addEventListener('mouseup', function () {
done = false
})
})
const confirm = document.querySelector('.confirm')
const refresh = document.querySelector('.refresh')
confirm.onclick = function () {
let select = document.querySelector('select')
const options = Array.from(select.children)
confirmText.innerText = `您选择的号码牌是${select.value},请问现在换不换?`
// 选择后,去掉一个错误答案
// i1是下标
i1 = select.value - 1
// delValue是值
let delValue = undefined
// 通过下标找值
if (a0[i1] === 0) {
delValue = getRandomNumber()
} else {
delValue = a0[i1] === 1 ? 2 : 1
}
// 通过值找下标
i2 = a0.indexOf(delValue)
// 选择的是i1, 去掉的是
const ctx = canvas[i2].getContext('2d')
ctx.clearRect(0, 0, 300, 100)
options.map(v => v.disabled = true)
confirm.style.display = 'none'
opater.style.display = 'inline-block'
}
const change = document.querySelector('.change')
const noChange = document.querySelector('.no-change')
change.onclick = function () {
isChange = true
const x = a1.filter(v => v !== i1 && v !== i2)
confirmText.innerText = `您确认选择的号码牌是${x[0] + 1},请刮卡!`
opater.style.display = 'none'
}
noChange.onclick = function () {
isChange = true
confirmText.innerText = `您确认选择的号码牌是${i1 + 1},请刮卡!`
opater.style.display = 'none'
}
refresh.onclick = function () {
window.location.reload()
}

作者:JoyZ
链接:https://juejin.cn/post/7278684023757553727
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

改了 3 个字符,10 倍的沙箱性能提升?!!

确实会慢,但不多 🤪 qiankun2 自发布以来,常被人诟病慢、有性能问题。虽在大部分场景下,这个问题表现的并不明显,不会对应用造成可感知的影响(2m JS 的解析约增加 200ms 耗时,单个函数调用增加耗时可忽略不计)。大部分情况下应用渲染慢,真的就是因...
继续阅读 »

确实会慢,但不多 🤪


qiankun2 自发布以来,常被人诟病慢、有性能问题。虽在大部分场景下,这个问题表现的并不明显,不会对应用造成可感知的影响(2m JS 的解析约增加 200ms 耗时,单个函数调用增加耗时可忽略不计)。大部分情况下应用渲染慢,真的就是因为你的 JS 太大(一个不分片的超大的 bundle),接口响应太长,UI 不够有「弹性」导致的。
但在面临一些 CPU 密集型的 UI 操作时,如图表、超量 DOM 变更(1000以上)等场景,确实存在明显的卡顿现象。所以我们也不好反驳什么,通常的解决方案就是推荐用户关闭沙箱来提升性能。


去年底我们曾尝试过一波优化,虽然略有成效,但整体优化幅度不大,因为有一些必要访问耗时省不掉,最终以失败告终。


重启优化之路 😤


近期有社区用户又提到了这个问题,加之年初的时候「获取」到了一些灵感,中秋假期在家决定对这个问题重新做一次尝试。
我们知道 qiankun 的沙箱核心思路其实是这样的:

const windowProxy = new Proxy(window, traps);

with(windowProxy) {
// 应用代码,通过 with 确保所有的全局变量的操作实际都是在操作 qiankun 提供的代理对象
${appCode}
}

此前主要的性能问题出在应用的代码会频繁的访问沙箱,比如 Symbol.unscopables 在图表场景很容易就达到千万级次的访问。
优化的思路也很简单,就是要减少全局变量在 proxy 里的 lookup 次数。比如可以先缓存起来,后续访问直接走作用域里的缓存即可:

const windowProxy = new Proxy(window, traps);

with(windowProxy) {
+ // 提前将一些全局变量通过 赋值/取值 从 proxy 里缓存下来
+ var undefined = windowProxy.undefined; var Array = windowProxy.Array; var Promise = windowProxy.Promise;
// 应用代码,通过 with 确保所有的全局变量的操作实际都是在操作 qiankun 提供的代理对象
${appCode}
}

看上去很完美,不过手上没有 windows 设备没法验证(M1性能太强测不出来),于是先提了个 pr


验证 👻


假期结束来公司,借了台 windows 设备,验证了一下。
糟了,没效果。优化前跟优化后的速度几乎没有变化。🥲


想了下觉得不应该啊,理论上来讲多少得有点作用才是,百思不得其解。


苦恼之际,突然好像想到了什么,于是做出了下面的修改:

const windowProxy = new Proxy(window, traps);

with(windowProxy) {
+ // 提前将一些全局变量通过 赋值/取值 从 proxy 里缓存下来
- var undefined = windowProxy.undefined; var Array = windowProxy.Array; var Promise = windowProxy.Promise;
+ const undefined = windowProxy.undefined; const Array = windowProxy.Array; const Promise = windowProxy.Promise;
// 应用代码,通过 with 确保所有的全局变量的操作实际都是在操作 qiankun 提供的代理对象
${appCode}
}

改动更简单,就是将 var 声明换成了 const,立马保存验证一把。


直接起飞!


场景 1:vue 技术栈下大 checkbox 列表变更





在有沙箱的情况下,耗时基本与原生框架的一致了。


场景 2:10000 个 dom 插入/变更


在 vue 的 event handler 中做原生的 10000 次 的 for 循环,然后插入/更新 10000 个 dom,记录中间的耗时:

<template>
<div>
<ul>
<li v-for="item in aaa" :key="item">{{ item }}</li>
</ul>
<button @click="test">test</button>
</div>
</template>

<script>
import logo from "@/assets/logo.png";
export default {
data() {
return {
aaa: 1
};
},
methods: {
test() {
console.time("run loop", 10000);

for (let index = 2; index < 1 * 10000; index++) {
this.aaa = index;
}

console.timeLog("run loop", 10000);

this.$nextTick(() => {
// 10000 个 dom 更新完毕后触发
console.timeEnd("run loop", 10000);
});
}
}
};
</script>
 

 

可以看到,这个优化后的提升已经不止 10 倍了,都超过 50 倍了,跟原生的表现基本无异。


如何做到的 🧙


完成最后的性能飞跃,实际上我只改了 3 个字符,就是把 with 里的 var 换成了 const,这是为什么呢?
其实我之前的这篇文章早就告诉了我答案:
ES 拾遗之 with 声明与 var 变量赋值
里面有一个重要的结论:
image.png
因为 windowProxy 里有所有的全局变量,那么我们之前使用 var 去尝试做作用域缓存的方案其实是无效的,声明的变量实际还是在全局的词法环境中的,也就避免不了作用域链的查找。而换成 const,就可以顺利的将变量写到 with 下的词法环境了。


one more thing 😂


至此,如果以后你的应用在微前端场景下表现的不尽如人意,请先考虑:


  1. 是否是应用的打包策略不合理,导致 bundle 过大 js 执行耗时过长
  2. 是否是前置依赖逻辑过多执行过慢(如接口响应),阻塞了页面渲染
  3. 是否是微应用的加载策略不合理,导致过晚的加载
  4. 没有加载过渡动画,只有硬生生的白屏

别再试图甩锅给微前端了,瑞思拜🫡。


作者:支付宝体验科技
链接:https://juejin.cn/post/7153509630155423752
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

比 React 快 30%?Gyron 是怎么做到的。

距离第一个试用版已经过去半年之久,想想就发了这篇文章,该文章起这种标题完全是被迫。之前也相继发表了一些相关的文章但是阅读量和点赞量寥寥无几,这种标题我也是看了一些自媒体发表的文章后学习而来。我也在尝试编写一些高质量的文章,如果你看到这篇文章请嘴下留情。 响应式...
继续阅读 »

距离第一个试用版已经过去半年之久,想想就发了这篇文章,该文章起这种标题完全是被迫。之前也相继发表了一些相关的文章但是阅读量和点赞量寥寥无几,这种标题我也是看了一些自媒体发表的文章后学习而来。我也在尝试编写一些高质量的文章,如果你看到这篇文章请嘴下留情。


响应式


Gyron.js是一款零依赖的响应式框架,和社区中大多数响应式方案一样,Gyron.js也是利用Proxy提供的能力进行的依赖收集和更新,理论上来说只要支持Proxy能力的环境都支持这套响应式的逻辑,只是需要修改(接入)对应的渲染部分。这里也要感谢 Vue 实现的响应式依赖更新方案,我们基本上参考着这套方案实现了自己的 API。


响应式核心部分就是 effect 结构,基本上所有提供出来的 api 都是在 effect 之上实现的,所以我们先来介绍 effect 这个数据结构。


我们先来看一眼 effect 长什么样子

export type Dep = Set<Effect>;

export type EffectScheduler = (...args: any[]) => any;

export type Noop = () => void;

export interface Effect {
// A 等于 self effect
// B 等于 other effect
// deps 是一个 Set 结构的 Effect ,用于存放 A 依赖的 B
// 当A不再使用时需要清除所有依赖 A 的 B
deps: Dep[];
// 在边界情况下强制执行依赖的B
allowEffect: boolean;
// 在首次执行时收集依赖的函数
scheduler: EffectScheduler | null;
// 存放所有手动设置的依赖
wrapper: Noop;
// 当需要更新时执行的函数
run: Noop;
// 当不再使用时清除依赖的B
stop: Noop;
}

effect 中有很多属性,我也在其中注释了每个属性的作用。每个属性都有自己的应用场景,比如 deps 就是用于卸载组件后清除所有组件依赖的数据,避免数据更新后组件被卸载,导致组件更新异常。其实响应式核心分为两个部分,第一个部分就是依赖收集,第二个部分就是响应更新,我们先来看看下面这张图。




上面这张图就是说明两个不同的数据之间的依赖关系,第一块(左上角)表明 variable proxy 2 依赖 variable proxy 1,在 variable proxy 2 中访问 variable proxy 1 时,会触发 variable proxy 1 的自动收集任务,当 variable proxy 1 的值更新后会触发依赖 variable proxy 2 的任务,也就是 run 或者 scheduler。那么我们是通过什么将这两个变量关联在一起的呢?我们引入了一个WeakMap数据effectTracks,用变量作为一个 key 值,然后在变更 variable proxy 1 时从这个模块变量中找到依赖再做更新,也就是图中右边部分。至此,依赖收集和依赖更新都已经完成,接下来,如何对应到组件上面呢?


上面我们介绍了两个响应式变量是如何完成这一整套响应式方案,那么我们把上述的变量变更为组件可不可以呢?答案是可以的。组件是什么?在 Gyron.js 中组件就是一个函数,一个被内装函数包装的函数。那么,组件在初次渲染时如何进行依赖收集呢?在讲解组件的依赖收集之前,我们先讲一讲另外一个模块变量activeEffect,这个变量主要用于组件初次渲染时保存组件的 effect 对象,然后在响应式数据 track 时,获取到组件的 effect 对象保存在上面讲的effectTracks模块变量中,在响应式数据发生变更后触发组件 effect 的 update 方法(也就是 run)来更新组件。这里值得一提的是,所有的更新我们全部都是异步,并且是可选支持中断继续模式的,这部分内容我们接下来再进行介绍。


好了,响应式的核心内容其实并不多,其实如何实现只是冰山一角,最主要的是其中的想法可以应用在你的业务之中,尽量少写一些恶心同事的代码。


任务调度


上面我们讲解了Gyron.js是如何做到响应式更新的,接下来我们说一说多个组件同时更新应该怎么处理呢?如果组件更新阻止了用户操作应该怎么办呢?如何在组件中拿到更新后的DOM呢?这些问题在平时开发中相信大多数开发中都遇到过,其实这些问题可以在编写业务代码时去进行优化,但是不怎么优雅,也可能会导致代码不可读,比如


获取更新后的 DOM

// 获取更新后的 D<DOM>
任务: [update component A, update component B, [update component C, [update component D]]]
等待任务更新完成: A、B、C、D
获取组件D更新后的DOM

为了提高开发效率和用户体验,开发者可以合理选择使用哪种模式更新组件。具体有哪些模式可以选择呢?答案是两种,第一种就是默认模式,组件更新全部在异步队列中完成。第二种模式就是在异步队列中的任务会受到外部状态控制。接下来我们分开讲一讲这两种模式。


第一种模式,我们可以使用Gyron.js暴露的FC方法定义组件,然后正常使用 JSX 去描述 UI,在组件更新时通过暴露的nextRender获取到更新后的 DOM。这是一个很常见的实现方式,这也和 Vue 的nextTick一样。我们重点讲讲另外一种更新模式。


延迟更新:组件更新的前面几步都是一样,有一个异步队列,但是延迟更新模式中有一个priority属性,当组件 effect 拥有这种属性的时候会自动根据这批组件的更新时间,或者用户操作来中断队列中后续任务的更新,当浏览器告诉我们,现在有空闲了可以继续任务时再继续未更新的任务。其实这种模式还可以更进一步,设定一个冷却时间,在冷却时间内再次发现相同的任务直接抛弃上一次相同的任务(根据任务 ID 来区分),这样做可以减少浏览器开销,因为这些任务在下一个周期中肯定会被覆盖。我们有计划的去实现这个内容,但不是现在。


第二种模式的实现完全得益于浏览器提供的 API,让这种模式实现变为可能,也让用户体验得到提升。


其实第二种模式后面的理念可以用在一些大型的编辑场景和文档协作中以此来提升用户体验,这也是在研究 React 的调度任务之后得出的结论。


所以,有人在反驳说看这些源码时没用时可以硬气的告诉他们,看完之后学到了什么。(不过不要盲目的去看,针对具体的问题去研究和借鉴)


复合直观的


如果你是一个 React 的用户,你会发现函数式组件的状态心智负担太高,不符合直觉,直接劝退新手。那么,什么是符合直观的代码?当我的组件依赖更新后组件的内容发生响应的更新即可,这也是响应式的核心。在Gyron.js中编写一个简单的响应式组件会如此简单。

import { FC, useValue } from "gyron";

interface HelloProps {
initialCount: number;
}

const Hello = FC<HelloProps>(({ initialCount = 0 }) => {
const count = useValue(initialCount);
const onClick = () => count.value++;

// Render JSX
return <div onClick={onClick}>{count.value}</div>;
});

上面定义了一个 Hello 的组件,这个组件接受一个参数 initialCount,类型为数字,组件的功能也很简单,当用户点击这个数字然后自增一。而如果要用 React 去实现或者 Vue 去实现这样一个功能,我们应该怎么做呢?


我们用 Vue 去实现一个一样的组件,代码如下(使用了 setup 语法)

<script lang="ts" setup>
import { ref } from "vue";

const props = withDefaults(
defineProps<{
initialCount: number;
}>(),
{
initialCount: 0,
}
);

const count = ref(props.initialCount);
function onClick() {
count.value++;
}
</script>

<template>
<div @click="onClick">{{ count }}</div>
</template>

那么我们用 React 也去实现一个一样的组件,代码如下

import { useState, useCallback } from "react";

export const Hello = ({ initialCount = 0 }) => {
const [count, setCount] = useState(initialCount);

const onClick = useCallback(() => {
setCount(count + 1);
}, [count, setCount]);

console.log("refresh"); // 每点击一次都会打印一次

return <div onClick={onClick}>{count}</div>;
};

好了,上面是不同框架实现的 Hello 组件。这里并不是说其它框架不好,只是我认为在表达上有一些欠缺。Vue2 中需要理解 this,并且没办法让 this 稳定下来,因为它可以在任何地方修改然后还无法被追踪,在 Vue3 中需要理解 setup 和 template 之间的关系,然后实现类型推断需要了解 defineXXX 这种 API。在 React 中想要更新组件需要注意 React 更新机制,比如内部状态何时才是预期的值,在遇到复杂的组件时这往往比较考验开发者的编码水平。


以上,Gyron.js是如何解决这些问题的呢?其实,这完全得益于 babel 的强大能力,让开发者不需要知道编译构建优化的知识也能介入其中,改变源码并能重新构建。如果想了解其中的用法可以去 babel 官网plugin 页面


然后,Gyron.js是如何解决上面提到的问题?我们以上面编写的一个简单组件 Hello 为例,介绍其中到底发生了什么。
首先,我们的组件用 FC 函数进行了一个包装,这里 FC 就好比一个标识符,在 AST 中属于 BinaryExpression 节点,然后函数体的返回值就是JSX.Element。我们有了这个规则,然后在 babel 中就可以根据这个规则定位到组件本身,再做修改。为了解决重复渲染的问题,我们需要把返回值做一些修改,把JSX.Element用一个函数进行包裹再进行返回。具体转换如下:

const Hello = FC(({ numbers }) => {
return <div>{numbers}</div>;
});
// ↓ ↓ ↓ ↓ ↓
const Hello = FC(({ numbers }) => {
return ({ numbers }) => <div>{numbers}</div>;
});


名词解释

组件函数:我们熟知的 JSX 组件

渲染函数:转换后的 JSX 函数,用于标记哪些部分是渲染部分,哪些是逻辑部分。类似于 Vue3 的 setup 和 render 的区别。



这是一个最简单的转换,但是这又引入了另外几个问题。第一,在JSX.Element中的元素内容是组件的参数,但是在下次渲染时取到的是顶层函数中的numbers,为了解决这个问题,我们将顶层函数中的第一个参数作为渲染函数中的第一个参数,然后在渲染函数中访问到的状态就是最新状态。


这其中还有一个问题,我在组件函数中访问 props 状态也无法保证是最新的,这时候就需要使用Gyron.js提供的onBeforeUpdate方法,这个方法会在组件更新之前调用,然后我们需要把组件函数中定义的 props 全部放进这个函数中,然后根据函数的 new props 去更新用户定义的 props。但是真实的使用场景比较复杂,比如可以这样定义({ a, ...b }) => {},将 props 的 a 单独拎出来,然后其余部分全部归纳到 b 中。


举一个简单的例子:

const Hello = FC(({ numbers }) => {
function transform() {
return numbers;
}
return <div>{transform()}</div>;
});
// ↓ ↓ ↓ ↓ ↓
import { onBeforeUpdate as _onBeforeUpdate } from "gyron";
const Hello = FC(({ numbers }) => {
_onBeforeUpdate((_, props) => {
var _props = props;
numbers = _props.numbers;
});
function transform() {
return numbers;
}
return <div>{transform()}</div>;
});

可以看到转换后的组件中多出了一个_onBeforeUpdate方法调用,其作用就是更新组件函数作用域中的 props。



小结:为了让用户在开发中编写符合直观的代码,Gyron.js在背后做了很多事情。这其实也是 babel 在实际项目中的一种使用方法。



极快的 hmr


hmr(hot module replacement )就是模块的热更新,其实这部分功能都是编译工具提供,我们只需要按照他们提供的 API 然后更新我们的组件。

if (module.hot) {
module.hot.accept("./hello.jsx", function (Comp1) {
rerender("HashId", Comp1);
});
}

以上代码我们的插件会自动插入,无需手动引入。(目前还只接入 vite,后续有计划的支持 webpack 等有 hot 模块的工具)


我们大致了解一下这其中发生了什么?首先,我们还是借助 babel 把每一个组件的名字和内容生成的 hash 值作为注释节点存放在模块中,然后通过编译工具获取到所有本模块的组件,然后通过注册 hot 事件重新渲染更新后的组件。


好了,讲解了编译工具提供的功能,这里着重讲解一下Gyron.js是如何做到重新渲染的。首先,我们通过编译工具获取到了组件 Hash 和组件函数,然后通过rerender函数执行重新渲染。那么rerender所需要的数据又是从哪里来的呢?其实,在实例第一次初始化的时候这个数据全部都收集到一个Map<string, Set<Component>>数据结构中,然后再通过Component上的 update 方法执行组件的更新。


SEO 友好


其实这段内容和Gyron.js本身关系不太大,但是没有Gyron.js提供的能力也很难办到。Gyron.js提供了 SSR(Server Side Render)的渲染模式,也就是我们熟知的服务端渲染。其中大致的原理就是服务端将实例渲染成字符串之后返回给浏览器,然后再通过客户端的hydrate功能让“静态”文本变的可响应。


以上是简单的用法,然后大致流程图如下所示:




为了让组件变得更通用,我们在所有组件的 props 上注入了一个变量告诉开发者当前处于何种模式的渲染当中,在服务端渲染当中时不能使用客户端提供的 API,在客户端渲染的过程中不能使用服务端的 API。

const App = ({ isSSR }) => {
// ...
if (!isSSR) {
document.title = "欢迎";
}
};
import { strict as assert } from "node:assert";
const App = ({ isSSR }) => {
// ...
if (isSSR) {
assert.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, "3"]], 4, 5]);
}
};

这是服务端渲染的方式,还有一种介于服务端渲染和客户端渲染之间,就是完全输出静态资源然后就可以部署到任何机器或者在线平台服务商中,比如app.netlify.comgithub.com等。这里不再介绍 SSR 模式的使用方法,可以去gyron.cc/docs/ssr这里有更详细的介绍。


所见即所得



这里介绍的是官方文档中的在线编辑器,相比于接入其它平台,我们占用的资源更少,功能齐全。



经过一段时间的折腾,终于弄出一个简单版的在线编辑器,支持实时预览、语法纠错、语法高亮、智能跳转等功能。


语言的话目前支持 jsx、tsx、less,并且还支持加载在线资源,比如import { h } from 'https://cdn.jsdelivr.net/npm/gyron'。因为所有数据都不保存在远端,只保存在本地,所以没有使用 standalone 沙盒技术隔离运行环境,也没有防范 xss 攻击。在线编辑器的目标就是让用户可以在线使用,支持用户编辑源代码,支持本地模块导入,支持实时预览,支持多个编辑器运行互不干扰。


目前这个编辑器支持本地编译和服务端编译,本地编译会让首屏加载变慢所以在线编辑器使用的服务端编译。现在,可以访问gyron.cc/explorer这个地址在线体验。


如果使用本地编译,这里面最终的就是需要实现一套本地的虚拟文件系统,让打包工具能够正常访问到对应的本地资源。而在 esbuild 中实现一套虚拟文件系统其实很简单,只需要编写一个插件,然后用 resolve 和 load 两种勾子就可以将本地文件输出到 esbuild 中。

const buildModuleRuntime = {
name: "buildModuleRuntime",
setup(build) {
build.onResolve({ filter: /\.\// }, (args) => {
return {
path: args.path,
namespace: "localModule",
};
});
build.onLoad({ filter: /\.\//, namespace: "localModule" }, async (args) => {
// 具体实现可以去github https://github.com/gyronorg/core/blob/main/packages/babel-plugin-jsx/src/browser.ts
const source = findSourceCode(config.sources, args.path);

if (source) {
const filename = getFileName(args, source.loader);
const result = await transformWithBabel(
source.code,
filename,
main,
true
);
return {
contents: result.code,
};
}
return {
contents: "",
loader: "text",
warnings: [
{
pluginName: "buildModuleRuntime",
text: `Module "${args.path}" is not defined in the local editor`,
},
],
};
});
},
};

然后会输出一个 module 文件,最终只需要将文件塞到 script 中让其运行。


在页面中引用多个编辑器,需要注意的是在不用这个 module 文件后及时删除。可以使用命名空间给 module 加上一个标签,新增和删除都使用这个命名空间作为变量控制当前运行时的资源。


作者:gyron
链接:https://juejin.cn/post/7212550699321917501
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

停止编写 API 函数

如果你正在开发一个前端程序,后端使用的 RESTFUL API,那你必须停止为每一个接口编写函数。 RESTFUL API 通常提供在不同实体上执行增删改查(CRUD)操作的一组接口。我们通常在我们的前端项目中为这些每一个接口提供一个函数,这些函数的功能非常的...
继续阅读 »

如果你正在开发一个前端程序,后端使用的 RESTFUL API,那你必须停止为每一个接口编写函数。


RESTFUL API 通常提供在不同实体上执行增删改查(CRUD)操作的一组接口。我们通常在我们的前端项目中为这些每一个接口提供一个函数,这些函数的功能非常的相似,只是为了服务于不用的实体。举个例子,假设我们有这些函数。

// api/users.js

// 创建
export function createUser(userFormValues) {
return fetch('users', { method: 'POST', body: userFormValues });
}

// 查询
export function getListOfUsers(keyword) {
return fetch(`/users?keyword=${keyword}`);
}

export function getUser(id) {
return fetch(`/users/${id}`);
}

// 更新
export updateUser(id, userFormValues) {
return fetch(`/users/${is}`, { method: 'PUT', body: userFormValues });
}

// 删除
export function removeUser(id) {
return fetch(`/users/${id}`, { method: 'DELETE' });
}

类似的功能可能存在于其他实体,例如:城市、产品、类别...但是我们可以用一个简单的函数调用来代替这些函数:

// apis/users.js
export const users = crudBuilder('/users');

// apis/cities.js
export const cities = crudBuilder('/regions/cities');


然后像这样去使用:

users.create(values);
users.show(1);
users.list('john');
users.update(values);
users.remove(1);

你可能会问为什么?有一些很好的理由:


  • 减少了代码行数:你编写的代码,和当你离开公司时其他人维护的代码
  • 强制执行 API 函数的命名约定,这可以增加代码的可读性和可维护性。例如你已经见过的函数名称: getListOfUsersgetCitiesgetAllProductsproductIndexfetchCategories等, 他们都在做相同的事情,那就是“获取实体列表”。使用这种方法,你将始终拥有entityName.list()函数,并且团队中的每个人都知道这一点。

所以,让我们创建crudBuilder()函数,然后再添加一些糖。


一个非常简单的 CRUD 构造器


对于上边的简单示例,crudBuilder()函数将非常简单:

export function crudBuilder(baseRoute) {
function list(keyword) {
return fetch(`${baseRoute}?keyword=${keyword}`);
}
function show(id) {
return fetch(`${baseRoute}/${id}`);
}
function create(formValues) {
return fetch(baseRoute, { method: 'POST', body: formValues });
}
function update(id, formValues) {
return fetch(`${baseRoute}/${id}`, { method: 'PUT', body: formValues });
}
function remove(id) {
return fetch(`${baseRoute}/${id}`, { method: 'DELETE' });
}

return {
list,
show,
create,
update,
remove
};
}

假设约定 API 路径并且给相应实体提供一个路径前缀,他将返回该实体上调用 CRUD 操作所需的所有方法。


但老实说,我们知道现实世界的应用程序并不会那么简单。在将这种方法应用于我们的项目时,有很多事情需要考虑:


  • 过滤:列表 API 通常会提供许多过滤器参数
  • 分页:列表 API 总是分页的
  • 转换:API 返回的值在实际使用之前可能需要进行一些转换
  • 准备:formValues对象在发送给 API 之前需要做一些准备工作
  • 自定义接口:更新特定项的接口不总是${baseRoute}/${id}

因此,我们需要可以处理更多复杂场景的 CRUD 构造器。


高级 CRUD 构造器


让我们通过上述方法来构建一些日常中我们真正使用的东西。


过滤


首先,我们应该在 list输出函数中处理更加复杂的过滤。每个实体列表可能有不同的过滤器并且用户可能应用了其中的一些过滤器。因此,我们不能对应用过滤器的形状和值有任何假设,但是我们可以假设任何列表过滤都可以产生一个对象,该对象为不同的过滤器名称指定了一些值。例如,我们可以过滤一些用户:

const filters = {
keyword: 'john',
createdAt: new Date('2020-02-10')
};

另一方面,我们不知道这些过滤器应该如何传递给 API,但是我们可以假设(跟 API 提供方进行约定)每一个过滤器在列表 API 中都有一个相应的参数,可以以'key=value'URL 查询参数的形式被传递。


因此我们需要知道如何将应用的过滤器转换成相对应的 API 参数来创建我们的 list 函数。这可以通过将 transformFilters 参数传递给 crudBuilder() 来完成。举一个用户的例子:

function transformUserFilters(filters) {
const params = [];
if (filters.keyword) {
params.push(`keyword=${filters.keyword}`);
}
if (filters.createdAt) {
params.push(`create_at=${dateUtility.format(filters.createdAt)}`);
}

return params;
}

现在我们可以使用这个参数来创建 list 函数了。

export function crudBuilder(baseRoute, transformFilters) {
function list(filters) {
let params = transformFilters(filters)?.join('&');
if (params) {
params += '?';
}

return fetch(`${baseRoute}${params}`);
}
}

转换和分页


从 API 接收的数据可能需要进行一些转换才能在我们的应用程序中使用。例如,我们可能需要将 snake_case 转换成驼峰命名或将一些日期字符串转换成用户时区。


此外,我们还需要处理分页。


我们假设来自 API 的分页数据都按照如下格式(与 API 提供者约定):

{
data: [], // 实体对象列表
pagination: {...} // 分页信息
}

因此,我们需要知道如何转换单个实体对象。然后我们可以遍历列表对象来转换他们。为此,我们需要一个 transformEntity 函数作为 crudBuilder 的参数。

export function crudBuilder(baseRoute, transformFilters, transformEntity, ) {
function list(filters) {
const params = transformFilters(filters)?.join('&');
return fetch(`${baseRoute}?${params}`)
.then((res) => res.json())
.then((res) => ({
data: res.data.map((entity) => transformEntity(entity)),
pagination: res.pagination
}));
}
}

list() 函数我们就完成了。


准备


对于 createupdate 函数,我们需要将 formValues 转换成 API 需要的格式。例如,假设我们在表单中有一个 City 的城市选择对象。但是 create API 只需要 city_id。因此,我们需要一个执行以下操作的函数:

const prepareValue = formValue => ({city_id: formValues.city.id});

这个函数会根据用例返回普通对象或者 FormData,并且可以将数据传递给 API:

export function crudBuilder(baseRoute, transformFilters, transformEntity, prepareFormValues) {
function create(formValues) {
return fetch(baseRoute, {
method: 'POST',
body: prepareFormValues(formValues)
});
}
}

自定义接口


在一些少数情况下,对实体执行某些操作的 API 接口不遵循相同的约定。例如,我们不能使用 /users/${id} 来编辑用户,而是使用 /edit-user/${id}。对于这些情况,我们应该指定一个自定义路径。


在这里我们允许覆盖 crud builder 中使用的任何路径。注意,展示、更新、移除操作的路径可能取决于具体实体对象的信息,因此我们必须使用函数并传递实体对象来获取路径。


我们需要在对象中获取这些自定义路径,如果没有指定,就退回到默认路径。像这样:

const paths = {
list: 'list-of-users',
show: (userId) => `users/with/id/${userId}`,
create: 'users/new',
update: (user) => `users/update/${user.id}`,
remove: (user) => `delete-user/${user.id}`
};

最终的 BRUD 构造器


这是创建 CRUD 函数的最终代码。

export function crudBuilder(baseRoute, transformFilters, transformEntity, prepareFormValues, paths) {
function list (filters) {
const path = paths.list || baseRoute;
let params = transformFilters(filters)?.join('&');
if (params) {
params += '?';
}

return fetch(`${path}${params}`)
.then((res) => res.json())
.then(() => ({
data: res.data.map(entity => transformEntity(entity)),
pagination: res.pagination
}));
}
function show(id) {
const path = paths.show?.(id) || `${baseRoute}/${id}`;

return fetch(path)
.then((res) => res.json())
.then((res => transformEntity(res)));
}
function create(formValues) {
const path = paths.create || baseRoute;

return fetch(path, { method: 'POST', body: prepareFormValues(formValues) });
}
function update(id, formValues) {
const path = paths.update?.(id) || `${baseRoute}/${id}`;

return fetch(path, { method: 'PUT', body: formValues });
}
function remove(id) {
const path = paths.remove?.(id) || `${baseRoute}/${id}`;

return fetch(path, { method: 'DELETE' });
}
return {
list,
show,
create,
update,
remove
}
}


Saeed Mosavat: Stop writing API functions


作者:七七喝可乐
链接:https://juejin.cn/post/7147993043923107870
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

实现滚动点赞墙

web
需要实现的效果如下: 需要将用户点赞的信息,一条一条的展示在页面顶部,这样的效果有多种实现方式,下面一一来了解一下吧~ 纯css实现 scss如下:(如果要将scss改为less,将$改为@就可以了) 当移动到第8行结束的时候,同屏出现的两行(第9行和第1...
继续阅读 »

需要实现的效果如下:



需要将用户点赞的信息,一条一条的展示在页面顶部,这样的效果有多种实现方式,下面一一来了解一下吧~


纯css实现



scss如下:(如果要将scss改为less,将$改为@就可以了)


当移动到第8行结束的时候,同屏出现的两行(第9行和第10行),就需要结束循环,重头开始了


这是一个上移的动画,动画执行的时间就是8s


itemShowTime+(itemShowTime + (oneCycleItemNum - oneScreenItemNum)(oneScreenItemNum) * (itemShowTime / $oneScreenItemNum)


$itemHeight: 60px; // 单个item的高度

$itemShowTime: 2s; // 单个item从完整出现到消失的时长
$oneCycleItemNum: 8; // 单个循环上移的item条数
$oneScreenItemNum: 2; // 同屏出现的item条数(不能大于 oneCycleItemNum)

$oneCycleItemTime: calc($itemShowTime + ($oneCycleItemNum - $oneScreenItemNum) * ($itemShowTime / $oneScreenItemNum));

@keyframes dynamics-rolling {
from {
transform: translateY(0);
}
to {
transform: translateY(-$itemHeight * $oneCycleItemNum);
}
}
.container {
height: 600px;

animation: dynamics-rolling $oneCycleItemTime linear infinite;
.div {
line-height: 60px;
}
}

.visibleView {
width: 100%;
height: 120px;
overflow: hidden;
background-color: skyblue;

}
.box {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}

简单的demo:



import React, { useEffect, useRef, useState } from 'react'
import styles from './index.module.scss'

const dataSource = new Array(50).fill(0).map((_, index) => index + 1)

export default function CycleScrollList() {
const [data, setData] = useState(dataSource.slice(0, 10))

return (
<div className={styles.box}>
<div className={styles.visibleView}>
<div className={styles.container}>
{
data.map((item, index) => (
<div key={ index } className={styles.div}>{ item }</div>
))
}
</div>
</div>
</div>

)
}

setInterval监听


css动画是定时的,所以可以定时更新列表内容,但是会有很明显的抖动,效果不太友好,应该是定时器的时间还不能太准




import React, { useEffect, useRef, useState } from 'react'
import styles from './index.module.scss'

const dataSource = new Array(50).fill(0).map((_, index) => index + 1)


export default function CycleScrollList() {
const [data, setData] = useState(dataSource.slice(0, 10))
const nextIndex = useRef(10) // 持续从 dataSource 拿数据的下一个 index

useEffect(() => {
const timer = setInterval(() => {
replaceData()
},4900)

return () => {
clearInterval(timer)
}
}, [])


const replaceData = () => {
let newData = []
if (nextIndex.current-5 < 0) {
newData = [...dataSource.slice(nextIndex.current-5) ,...dataSource.slice(0, nextIndex.current + 5)]
} else {
newData = [...dataSource.slice(nextIndex.current-5, nextIndex.current + 5)]
}
// 使用当前的后半份数据,再从 dataSource 中拿新数据
console.log(newData)
const nextIndexTemp = nextIndex.current + 5
const diff = nextIndexTemp - dataSource.length
if (diff < 0) {
nextIndex.current = nextIndexTemp
} else {
// 一轮数据用完,从头继续
nextIndex.current = diff
}
setData(newData)
}

return (
<div className={styles.box}>
<div className={styles.visibleView}>
<div className={styles.container}>
{
data.map((item, index) => (
<div key={ index } className={styles.div}>{ item }</div>
))
}
</div>
</div>
</div>

)
}

IntersectionObserver监听


监听第5个元素


如果第五个元素可见了,意味着不可见时,需要更换数据了


如果第五个元素不可见了,立刻替换数据


替换的数据如下:



使用IntersectionObserver监听元素,注意页面卸载时,需要去除绑定


tsx如下:



import React, { useEffect, useRef, useState } from 'react'
import styles from './index.module.scss'

const dataSource = new Array(50).fill(0).map((_, index) => index + 1)
const ITEM_5_ID = 'item-5'

export default function CycleScrollList() {
const [data, setData] = useState(dataSource.slice(0, 10))

const intersectionObserverRef = useRef<IntersectionObserver | null>()
const item5Ref = useRef<HTMLDivElement | null>(null)

const nextIndex = useRef(10) // 持续从 dataSource 拿数据的下一个 index
const justVisible5 = useRef<boolean>(false) // 原来是否为可视

useEffect(() => {
intersectionObserverRef.current = new IntersectionObserver((entries) => {
entries.forEach((item) => {
if (item.target.id === ITEM_5_ID) {
// 与视图相交(开始出现)
if (item.isIntersecting) {
justVisible5.current = true
}
// 从可视变为不可视
else if (justVisible5.current) {
replaceData()
justVisible5.current = false
}
}
})
})
startObserver()

return () => {
intersectionObserverRef.current?.disconnect()
intersectionObserverRef.current = null
}
}, [])

const startObserver = () => {
if (item5Ref.current) {
// 对第五个 item 进行监测
intersectionObserverRef.current?.observe(item5Ref.current)
}
}

const replaceData = () => {
let newData = []
if (nextIndex.current-5 < 0) {
newData = [...dataSource.slice(nextIndex.current-5) ,...dataSource.slice(0, nextIndex.current + 5)]
} else {
newData = [...dataSource.slice(nextIndex.current-5, nextIndex.current + 5)]
}
// 使用当前的后半份数据,再从 dataSource 中拿新数据
console.log(newData)
const nextIndexTemp = nextIndex.current + 5
const diff = nextIndexTemp - dataSource.length
if (diff < 0) {
nextIndex.current = nextIndexTemp
} else {
// 一轮数据用完,从头继续
nextIndex.current = diff
}
setData(newData)
}

return (
<div className={styles.box}>
<div className={styles.visibleView}>
<div className={styles.container}>
{
data.map((item, index) => (
index === 4 ?
<div id={ ITEM_5_ID } ref={ item5Ref } key={ index } className={styles.div}>{ item }</div>
:
<div key={ index } className={styles.div}>{ item }</div>
))
}
</div>
</div>
</div>

)
}

scss样式


$itemHeight: 60px; // 单个item的高度

$itemShowTime: 3s; // 单个item从完整出现到消失的时长
$oneCycleItemNum: 5; // 单个循环上移的item条数
$oneScreenItemNum: 3; // 同屏出现的item条数(不能大于 oneCycleItemNum)

$oneCycleItemTime: calc($itemShowTime + ($oneCycleItemNum - $oneScreenItemNum) * ($itemShowTime / $oneScreenItemNum));

@keyframes dynamics-rolling {
from {
transform: translateY(0);
}
to {
transform: translateY(-$itemHeight * $oneCycleItemNum);
}
}
.container {
height: 600px;

animation: dynamics-rolling $oneCycleItemTime linear infinite;
.div {
line-height: 60px;
}
}

.visibleView {
width: 100%;
height: 120px;
overflow: hidden;
background-color: skyblue;

}
.box {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}

作者:0522Skylar
来源:juejin.cn/post/7278244755825442853
收起阅读 »

用了策略模式之后,再也不用写那么多 if else 了,真香!

web
前言 从我个人理解来看,设计模式其实就藏在我们平时的代码中,只是有人把它们提、炼出来,赋予了一些专业的名词和定义,下面给大家介绍一个日常项目开发中非常实用的设计模式,也就是策略模式。 策略模式的定义 先来看下策略模式的定义:定义一系列的算法,把它们一个个封装起...
继续阅读 »

前言


从我个人理解来看,设计模式其实就藏在我们平时的代码中,只是有人把它们提、炼出来,赋予了一些专业的名词和定义,下面给大家介绍一个日常项目开发中非常实用的设计模式,也就是策略模式


策略模式的定义


先来看下策略模式的定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。


简单来说就是有多种选择,然后一般只会选择一种。从代码的角度来说就是,定义一系列的ifelseif,然后只会命中其中一个。


举个例子


话不多说,直接来看例子,比如我们需要计算员工工资,员工工资计算规则如下:



  • 高级工:时薪为25块/小时

  • 中级工:时薪为20块/小时

  • 初级工:时薪为15块/小时


按每天10小时的工作时长来算。


一、第一版实现:


const calculateSalary = function (workerLevel, workHours = 10) {
if (workerLevel === 'high') {
return workHours * 25
}
if (workerLevel === 'middle') {
return workHours * 20
}
if (workerLevel === 'low') {
return workHours * 15
}
}
console.log(calculateSalary('high')) // 250
console.log(calculateSalary('middle')) // 200

这段代码具有明显的缺点:



  • calculateSalary函数庞大,有许多的if else语句,这些语言需要覆盖所有的逻辑分支

  • calculateSalary函数缺乏弹性,如果新增一种员工等级higher,需要修改calculateSalary函数的内部实现,违反开放——封闭原则

  • 算法的复用性差


二、第二版实现(函数组合):


当然,我们可以使用函数组合的方式重构代码,把每一个if中的逻辑单独抽离成一个函数。


const workerLevelHigh = function (workHours) {
return workHours * 25
}

const workerLevelMiddle = function (workHours) {
return workHours * 20
}
const workerLevelLow = function (workHours) {
return workHours * 15
}

const calculateSalary = function (workerLevel, workHours = 10) {
if (workerLevel === 'high') {
return workerLevelHigh(workHours)
}
if (workerLevel === 'middle') {
return workerLevelMiddle(workHours)
}
if (workerLevel === 'low') {
return workerLevelLow(workHours)
}
}
console.log(calculateSalary('high', 10)) // 250
console.log(calculateSalary('middle', 10)) // 200

这样会提高算法的复用性,但这种改善十分有限,calculateSalary函数依旧庞大和缺乏弹性。


三、第三版实现(策略模式):


我们可以把不变的部分和变化的部分拆分开来。



  • 不变的部分:算法的使用方式不变,都是根据某个算法取得计算后的工资数额;

  • 变化的部分:算法的实现。


我们js的对象是key value的形式,这可以帮助我们天然的替换掉if else


因此,我们可以定义对象的两部分:



  • 针对变化的部分,我们可以定义一个策略对象,它封装了具体的算法,负责具体的计算过程

  • 针对不变的部分,我们提供一个Context函数,它接受客户的请求,随后把请求委托给策略对象。


const strategies = {
"high": function (workHours) {
return workHours * 25
},
"middle": function (workHours) {
return workHours * 20
},
"low": function (workHours) {
return workHours * 15
},
}

const calculateSalary = function (workerLevel, workHours) {
return strategies[workerLevel](workHours)
}
console.log(calculateSalary('high', 10)) // 250
console.log(calculateSalary('middle', 10)) // 200

策略模式的优缺点


从我个人在实际项目中的使用来看,策略模式的优缺点如下:


优点:



  • 代码复杂度降低:再也不用写那么多if else了。eslint其中有一项规则配置叫圈复杂度,其中一条分支也就是一个if会让圈复杂增加1,圈复杂度高的代码不易阅读和维护,用策略模式就能很好的解决这个问题;

  • 易于切换、理解和扩展:它将算法封装在独立的strategy中,比如你要在上面代码中加一个等级higherlower,直接更改策略对象strategies就行,十分方便。

  • 复用性高:策略模式中的算法可以复用在系统的其它地方,你只需要用将策略类strategies用export或者module.exports导出,就能在其他地方很方便的复用。


缺点:



  • 增加使用者使用负担:因为大量运用策略模式会在实际项目中堆砌很多策略类或者策略对象,这样项目的新人如果不熟悉这些策略类和策略对象,会增加他们的使用成本和学习成本,前期来说会比看if else更加难懂。


小结


以上就是我个人对策略模式的解读和了解啦,实际上项目中用策略模式的场景还是挺多的,因为在写业务代码中,很容易写出大量的if else,这时候就可以封装为策略模式,方便项目维护和扩展,从我个人的使用体验来看,还是相当香的。


大家喜欢在实际项目使用策略模式么,欢迎留言和讨论~


作者:han_
来源:juejin.cn/post/7279041076273610764
收起阅读 »

八百年不面试,一面试就面得一塌糊涂

web
前言 好久没面试了,最近看一些厂开始招人了,于是投了投,没想着过,主要是抱着学习的态度,看看自己哪里不足,没想到自己这么不足。。。 最近面了个试,某大厂,具体是哪个厂大家可以自行猜测,我看看有没有对的,哈哈哈哈 目前是三面,但是估计止步于三面了,然后我稍微整理...
继续阅读 »

前言


好久没面试了,最近看一些厂开始招人了,于是投了投,没想着过,主要是抱着学习的态度,看看自己哪里不足,没想到自己这么不足。。。


最近面了个试,某大厂,具体是哪个厂大家可以自行猜测,我看看有没有对的,哈哈哈哈


目前是三面,但是估计止步于三面了,然后我稍微整理了一下面试题,但是这里只说出我的思考,而不说出答案,至于为啥不写答案,我只能说,我自己不会。。。有些内容我需要进行学习,然后系统地、简单易懂地分享给大家


我说一下我自身的情况:主要技术栈是vue全家桶,算是能深挖的那种,其他的,react、webpack、vite、less、sass、tailwindcss、unocss、Nuxt、node等系列都会,但是说实话我是没法手写出来的,只停留在会用的程度,webgl、canvas等可视化方向还可以,毕竟我之前就是做这个的,算法还算可以,不说精通,但是一般题是可以做出来的,然后在基础方面,也就是js、css这块,我只能说了解吧,因为见过css大神coco这种的,就感觉自己css从熟悉变成了听说。


反正大概的就那样吧,会的比较多,比较杂,但是很多都不精通(不去看源码这种),关键的来了,我对于浏览器这块比较薄弱,计网、操作系统对我来说像噩梦一样,我觉得是我经验少吧,没能在工作中接触到这几个层面,所以我就是真的能答出来,也就是硬背的,并且不能举一反三,这也导致了这次面试的惨败



下面我说一下面试吧


注意,公司的技术栈主要是React(umi)这块,vue很少很少,然后会用node写一些中间件,大部分都是大前端



然后算法问题的话,也不在这里说,主要说一些口述的问题


一面


一面是对我来说最友好的一面了,基本上都是简单的一些基础问题


面试官主要是react技术栈,然后我和他说了,我主要是vue的,vue的原理可以,但是问我react的太深的问题我是不太会的,首先是自我介绍,然后开始问问题



  • pinia和vuex的区别,其实他想问我Redux和Mobx和其他React状态管理的区别,但是奈何我就会这几个,所以他索性问了问了我pinia

  • css实现DOM节点的水平居中有几种方式:我记得我说了四种,flex,text-align,margin,position,应该还有,但是一瞬间的话,脑袋瓦特了

  • 实现一个左右布局,左侧200px,右侧自适应,css写有几种方式:我说了浮动、定位、弹性盒、网格这四种

  • 检测js数据类型,typeof和instanceof区别,instanceof原理:这里我直接手写了instanceof,这个很简单

  • 浏览器输入url,到看到页面会发生什么:我当时懵了,我看过n个面经都说过这个问题,经典八股,但是我就是没背,只能磕磕巴巴说了一些(我八股真的不行,而且我不背这玩意)

  • 用Java的时候,对登录请求进行拦截,怎么处理的:这个很简单哈,为啥问Java,这是因为我简历上有,我之前从事过全栈,然后他就问了一下

  • 函数式编程的副作用是什么

  • 工作的经历,项目问题(这个占据了大部分的时间),其中有个问题可以分享一下,因为我用了wangeditor,他问我wangeditor的内核是什么


一面总体来说是很友好的,而且都答出来了,面试官很礼貌,面试感受非常好,第二天下午的时候通知二面


二面


噩梦的开始



  • 自我介绍

  • 公司项目问题(绝大部分时间)

  • vue、react数据绑定的区别

  • 我想存储一个客户端的数据,前端有哪些存储方式:后来就存储、内存的问题开始展开

  • pinia会进行数据的存储,它最终存在了哪里

  • js的内存是怎么进行管理的

  • 垃圾回收、内存泄漏,什么情况会导致内存泄漏

  • 闭包是什么,应用场景,怎么操作会产生内存泄漏

  • 你在工作时用的哪种协议

  • 除了http还有哪些通信协议(跟前端有关的)

  • websocket通信过程是怎么样的

  • 前端跨域相关问题

  • 代理相关问题

  • 服务和服务之间有没有跨域

  • 前端安全方面有哪些攻击方式

  • 该怎么处理呢

  • node有哪些框架可以处理脚本攻击(或者是库)


有些问题记不清了,后面有一些网络的问题,但是忘了,前面其实还好,而且问题是一步一步衍生出来的,这感觉很好,但是到网络安全这里,我就有点不会了,当时就感觉完犊子了,再见


然后过了三天,hr电话告诉我过了,约了三面,其实是比较吃惊的,我以为已经止步了


三面


最难受的一面



  • 自我介绍

  • 说说最近自己认为最好的项目,然后我说了一些,然后对方:就这?我一时语塞,开始紧张(项目占据了大多数时间)

  • 说说tcp三次挥手,为什么不能两次

  • tcp粘包,讲讲

  • 还有一些计网和操作系统的问题,这里是因为,我根本不会,所以压根没记住问题。。

  • 进程、线程区别,举个生动的例子

  • 讲讲多线程

  • 浏览器的核心线程和核心进程有哪些

  • MySQL的引擎

  • 现在有一个100tb的文件,让你一分钟之内把这个文件遍历出来,怎么做


计网和操作系统一塌糊涂,现在面试还没有反馈,凉凉了,而且看面试官的态度也能看出来是很不满意的


总结


平均时长在45min左右


几乎没问vue的任何问题,这是我最难受的,而且js、ts、css也几乎不问的,反正就是我上面的技术栈几乎一个没问,面试官主要就问你两处:你的工作经验(也就是你曾经的公司项目),以及计网和操作系统


因为我有做一些开源的项目和个人的项目,但是他们更在乎你之前公司的项目是什么样的


我自己的项目比较多,简历就有5.6页,但是没啥用,他们都没问


我也发现自己计网、操作系统这里太薄弱了,有时间还是得系统学习一下的,自己的确在开发中没遇到过这些,欠加思考


希望大家也能重视一下这里吧


作者:Shaka
来源:juejin.cn/post/7273682292538933306
收起阅读 »

帮你省时间,看看 bun v1.0 怎么用!

web
本文基于 Window Ubuntu WSL 环境测试,本文只选取重点,细节需查看文档 一、bun v1.0 做了什么? all in JavaScript/TypeScript app。看起真的很了不起! 作为JS/TS运行时 作为包管理工具和包运行...
继续阅读 »

本文基于 Window Ubuntu WSL 环境测试,本文只选取重点,细节需查看文档



一、bun v1.0 做了什么?



all in JavaScript/TypeScript app。看起真的很了不起!




  • 作为JS/TS运行时

  • 作为包管理工具和包运行器

  • 作为构建工具

  • 作为测试运行器

  • 对外提供 API


资源



二、安装 bun v1.0



bun 目前不支持 window 环境,但是可以在 wsl 中使用。



2.1) 各种安装方法



  • curl


curl -fsSL https://bun.sh/install | bash # 检查:which bun


  • 使用 npm 安装


npm install -g bun # 检查:which bun


  • 其他平台的安装方法



brew tap oven-sh/bun # for macOS and Linux
brew install bun # brew
docker pull oven/bun # docker

2.2) bun 提供的命令


命令描述
init初始化一个 bun 项目
run运行一个文件或者脚本
test运行测试
xbun x 的别名,类似于 npx
repl进入交互式环境
create使用模板创建项目
install安装依赖
add添加依赖
remove移除依赖
update更新依赖
link全局链接一个 npm 包
unlink移除全局链接的 npm 包
pm更多的包管理命令
build打包 TypeScript/JavaScript 文件到单个文件
update获取最新的 bun 版本

三、作为 JS/TS 运行时


bun index.js // 运行 js 文件
bun run index.ts // 运行 ts 文件
// 其他相关的 tsx/jsx/...

如果直接运行 index.tsx 没有任何依赖会报错:


const Ad = <div>ad</div>

console.log(Ad)

// bun index.tsx
// 错误:Cannot find module "react/jsx-dev-runtime" from "/xxx/index.tsx"

四、作为包管理工具和包运行器


4.1)初始化一个项目


bun init # 与 npm init -y 类似

4.2)使用脚手架


# 与 npx 类似, 以下可能常用的初始化项目的脚手架
bun create react-app
bun create remix
bun create next-app
bun create nuxt-app

五、作为构建工具



  • 初始化一个简单的项目


cd your_dir
bun init # 默认
bun add react react-dom # 添加依赖包
touch index.tsx


  • 添加 TSX 文件内容


import React from 'react'

const App = () => {
return <div>This is App</div>
}


  • 打包 bun build


bun build ./index.tsx --outfile=bundle.js


提示:bundle.js 中打包了 react 相关的包。



六、作为测试运行器


测试与 Jest 非常相似, 以下是官方示例:


import { expect, test } from "bun:test";

test("2 + 2", () => {
expect(2 + 2).toBe(4);
});

运行时测试:


bun test

速度很快,输出结果:


bun test v1.0.0 (822a00c4)

t.test.ts:
✓ 2 + 2 [1.03ms]

1 pass
0 fail
1 expect() calls
Ran 1 tests across 1 files. [92.00ms]

七、对外提供 API


项目描述
HTTP 服务处理 HTTP 请求和响应
WebSocket 套接字支持 WebSocket 连接
Workers 工具在后台运行多线程任务
Binary data处理二进制数据
Streams流处理
File I/O文件输入/输出操作
import.meta访问模块元信息
SQLite使用 SQLite 数据库
FileSystemRouter文件系统路由器
TCP socketsTCP 套接字通信
Globals全局变量和对象
Child processes创建子进程
Transpiler转译器
Hashing哈希函数和算法
Console控制台输出
FFI外部函数接口
HTMLRewriterHTML 重写和转换
Testing测试工具和框架
Utils实用工具函数
Node-APINode.js API 访问

八、展望



  • windows 支持


小结


本文主要讲解了 bun v1.0 中所做的事情,包含极速的运行时、一体化的包管理工具、内置测试运行器、构建应用(打包)和对象提供各种类型的 API(兼容 Node API(非完全)),如此功能能完整的 bun 你想尝试一下吗?


作者:进二开物
来源:juejin.cn/post/7277399972916428835
收起阅读 »

为什么react中的hooks都要放在顶部?

1. 使用场景: 公司开会的时候有师兄问到为什么hooks定义一般都写在顶部,对于这个问题我以前总结过,这次看了 react新文档后我给出更加详细的解释并给出具体代码来解释为什么要放在顶部。 2.官网解释: 1.官网截图镇楼: 2.那我写在条件语句中会怎样 ...
继续阅读 »

1. 使用场景:


公司开会的时候有师兄问到为什么hooks定义一般都写在顶部,对于这个问题我以前总结过,这次看了
react新文档后我给出更加详细的解释并给出具体代码来解释为什么要放在顶部。


2.官网解释:


1.官网截图镇楼:




2.那我写在条件语句中会怎样


我给出一段代码:其中const [message, setMessage] = useState('');写在了条件语句里面

import { useState } from 'react';

export default function FeedbackForm() {
const [isSent, setIsSent] = useState(false);
if (isSent) {
return <h1>Thank you!</h1>;
} else {
// eslint-disable-next-line
const [message, setMessage] = useState('');
return (
<form onSubmit={e => {
e.preventDefault();
alert(`Sending: "${message}"`);
setIsSent(true);
}}>
<textarea
placeholder="Message"
value={message}
onChange={e => setMessage(e.target.value)}
/>
<br />
<button type="submit">Send</button>
</form>
);
}
}

效果图:这是一个收集用户反馈的小表单。当反馈被提交时



 它应该显示一条感谢信息,当我点击确定时出现一条错误。




“渲染的 hooks 比预期的少”


3.那我不写在顶部可能会怎样


下方的const [message, setMessage] = useState('');并没有写在顶部

   import { useState } from 'react';

export default function FeedbackForm() {
const [isSent, setIsSent] = useState(false);
if (isSent) {
return <h1>Thank you!</h1>;
}
const [message, setMessage] = useState('');
return (
<form onSubmit={e => {
e.preventDefault();
alert(`Sending: "${message}"`);
setIsSent(true);
}}>
<textarea
placeholder="Message"
value={message}
onChange={e => setMessage(e.target.value)}
/>
<br />
<button type="submit">Send</button>
</form>
);
}
}

效果图:



 点击确认后:
同样出现这个错误:提前return导致后面一个hooks没有渲染。




4.原因分析


从源码的角度来说的话,React会在内部创建一个名为“Hooks”(中文为钩子)的数据结构来追踪每个组件的状态。


在函数组件中调用Hook时,React会根据Hook的类型将其添加到当前组件的Hooks链表中。然后,React会将这些Hooks存储在Fiber节点的“memoizedState”字段中,以便在下一次渲染时使用。


如果你在代码中多次调用同一个Hook,React会根据Hooks的顺序将其添加到当前组件的Hooks链表中。这样,React就可以确定哪个状态应该与哪个组件关联,并且能够正确地更新UI。


以下是一个示例代码片段:

import { useState, useEffect } from 'react';

function useCustomHook() {

const [count, setCount] = useState(0);

useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return [count, setCount];
}

export default function MyComponent() {

const [count, setCount] = useCustomHook();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

在上面的代码中,useCustomHook是一个自定义Hook,它使用useStateuseEffectHook来管理状态。在MyComponent中,我们调用自定义Hook并使用返回值来渲染UI。


由于useCustomHook只能在函数组件或其他自定义Hooks的最顶层调用,我们不能将它嵌套在条件语句、循环或其他函数内部。如果这样做,React将无法正确追踪状态并更新UI,可能导致不可预测的结果。如果我们条件渲染中使用可能导致没有引入useCustomHook(),从而导致错误。


总结描述就是创建了一个链表,当在条件语句中使用hooks时可能会导致前后两次链表不同,从而导致错误,所以我们必须尽可能避免这种错误从而写在顶部。


作者:北海天空
链接:https://juejin.cn/post/7223586612927971388
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

你可能并不需要useEffect

相信大家在写react时都有这样的经历:在项目中使用了大量的useEffect,以至于让我们的代码变得混乱和难以维护。 难道说useEffect这个hook不好吗?并不是这样的,只是我们一直在滥用而已。 在这篇文章中,我将展示怎样使用其他方法来代替useEff...
继续阅读 »

相信大家在写react时都有这样的经历:在项目中使用了大量的useEffect,以至于让我们的代码变得混乱和难以维护。


难道说useEffect这个hook不好吗?并不是这样的,只是我们一直在滥用而已。


在这篇文章中,我将展示怎样使用其他方法来代替useEffect。


什么是useEffect


useEffect允许我们在函数组件中执行副作用。它可以模拟 componentDidMount、componentDidUpdate 和componentWillUnmount。我们可以用它来做很多事情。但是它也是一个非常危险的钩子,可能会导致很多bug。


为什么useEffect是容易出现bug的


来看一个定时器的例子:

import React, { useEffect } from 'react'

const Counter = () => {
const [count, setCount] = useState(0)

useEffect(() => {
const timer = setInterval(() => {
setCount((c) => c + 1)
}, 1000)
})

return <div>{count}</div>
}

这是一个非常常见的例子,但是它是非常不好。因为如果组件由于某种原因重新渲染,就会重新设置定时器。该定时器将每秒调用两次,很容易导致内存泄漏。


怎样修复它?


useRef

import React, { useEffect, useRef } from 'react'

const Counter = () => {
const [count, setCount] = useState(0)
const timerRef = useRef()

useEffect(() => {
timerRef.current = setInterval(() => {
setCount((c) => c + 1)
}, 1000)
return () => clearInterval(timerRef.current)
}, [])

return <div>{count}</div>
}

它不会在每次组件重新渲染时设置定时器。但是我们在项目中并不是这么简单的代码。而是各种状态,做各种事情。


你以为你写的useEffect

useEffect(() => {
doSomething()

return () => cleanup()
}, [whenThisChanges])

实际上是这样的

useEffect(() => {
if (foo && bar && (baz || quo)) {
doSomething()
} else {
doSomethingElse()
}
// 遗忘清理函数。
}, [foo, bar, baz, quo, ...])

写了一堆的逻辑,这种代码非常混乱难以维护。


useEffect 到底是用来干啥的


useEffect是一种将React与一些外部系统(网络、订阅、DOM)同步的方法。如果你没有任何外部系统,只是试图用useEffect管理数据流,你就会遇到问题。



有时我们并不需要useEffect


1.我们不需要useEffect转化数据

const Cart = () => {
const [items, setItems] = useState([])
const [total, setTotal] = useState(0)

useEffect(() => {
setTotal(items.reduce((total, item) => total + item.price, 0))
}, [items])

// ...
}

上面代码使用useEffect来进行数据的转化,效率很低。其实并不需要使用useEffect。当某些值可以从现有的props或state中计算出来时,不要把它放在状态中,在渲染期间计算它。

const Cart = () => {
const [items, setItems] = useState([])
const [total, setTotal] = useState(0)

const totalNum = items.reduce((total, item) => total + item.price, 0)

// ...
}

如果计算逻辑比较复杂,可以使用useMemo:

const Cart = () => {
const [items, setItems] = useState([])
const total = useMemo(() => {
return items.reduce((total, item) => total + item.price, 0)
}, [items])

// ...
}

2.使用useSyncExternalStore代替useEffect


useSyncExternalStore


常见方式:

const Store = () => {
const [isConnected, setIsConnected] = useState(true)

useEffect(() => {
const sub = storeApi.subscribe(({ status }) => {
setIsConnected(status === 'connected')
})

return () => {
sub.unsubscribe()
}
}, [])

// ...
}

更好的方式:

const Store = () => {
const isConnected = useSyncExternalStore(
storeApi.subscribe,
() => storeApi.getStatus() === 'connected',
true
)

// ...
}

3.没必要使用useEffect与父组件通信

const ChildProduct = ({ onOpen, onClose }) => {
const [isOpen, setIsOpen] = useState(false)

useEffect(() => {
if (isOpen) {
onOpen()
} else {
onClose()
}
}, [isOpen])

return (
<div>
<button
onClick={() => {
setIsOpen(!isOpen)
}}
>
Toggle quick view
</button>
</div>
)
}

更好的方式,可以使用事件处理函数代替:

const ChildProduct = ({ onOpen, onClose }) => {
const [isOpen, setIsOpen] = useState(false)

const handleToggle = () => {
const nextIsOpen = !isOpen;
setIsOpen(nextIsOpen)

if (nextIsOpen) {
onOpen()
} else {
onClose()
}
}

return (
<div>
<button
onClick={}
>
Toggle quick view
</button>
</div>
)
}

4.没必要使用useEffect初始化应用程序

const Store = () => {
useEffect(() => {
storeApi.authenticate()
}, [])

// ...
}

更好的方式:


方式一:

const Store = () => {
const didAuthenticateRef = useRef()

useEffect(() => {
if (didAuthenticateRef.current) return

storeApi.authenticate()

didAuthenticateRef.current = true
}, [])

// ...
}

方式二:

let didAuthenticate = false

const Store = () => {
useEffect(() => {
if (didAuthenticate) return

storeApi.authenticate()

didAuthenticate = true
}, [])

// ...
}

方式三:

if (typeof window !== 'undefined') {
storeApi.authenticate()
}

const Store = () => {
// ...
}

5.没必要在useEffect请求数据


常见写法

const Store = () => {
const [items, setItems] = useState([])

useEffect(() => {
let isCanceled = false

getItems().then((data) => {
if (isCanceled) return

setItems(data)
})

return () => {
isCanceled = true
}
})

// ...
}

更好的方式:


没有必要使用useEffect,可以使用swr:

import useSWR from 'swr'

export default function Page() {
const { data, error } = useSWR('/api/data', fetcher)

if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>

return <div>hello {data}!</div>
}

使用react-query:

import { getItems } from './storeApi'
import { useQuery, useQueryClient } from 'react-query'

const Store = () => {
const queryClient = useQueryClient()

return (
<button
onClick={() => {
queryClient.prefetchQuery('items', getItems)
}}
>
See items
</button>
)
}

const Items = () => {
const { data, isLoading, isError } = useQuery('items', getItems)

// ...
}

没有正式发布的react的 use函数

function Note({ id }) {
const note = use(fetchNote(id))

return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
</div>
)
}

reference


http://www.youtube.com/watch?v=bGz…


作者:今天学到了
链接:https://juejin.cn/post/7182110095554117692
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

我的天!多个知名组件库都出现了类似的bug!

前言 首先声明,没有标题党哈! 以下我知道的国内知名react组件库全部都有这个bug,你们现在都能去复现,一个提pr的好机会就让给你们了,哈哈!复现组件库: 阿里系:ant design, fusion design,字节系:arco design...
继续阅读 »

前言


首先声明,没有标题党哈!


以下我知道的国内知名react组件库全部都有这个bug,你们现在都能去复现,一个提pr的好机会就让给你们了,哈哈!复现组件库:



本来字节还有一个semi design,结果我发现它没有Affix组件,也就是固钉组件,让他躲过一劫,他有这个组件我也觉得肯定会复现相同的bug。


Affix组件是什么,以及bug复现


Affix组件(固钉组件)能将页面元素钉在可视范围。如下图:




这个button组件,会在距离顶部80px的时候会固定在屏幕上(position: fixed),如下图:




如何复现bug


你在这个button元素任意父元素上,加上以下任意style属性


  • will-change: transform;
  • will-change: filter;
  • will-change: perspective;
  • transform 不为none
  • perspective不为none
  • 非safari浏览器,filter属性不为none
  • 非safari浏览器,backdrop-filter属性不为none
  • 等等

都可以让这个固定组件失效,就是原本是距离顶部80px固定。


我的组件库没有这个bug,哈哈


mx-design


目前组件不是很多,还在努力迭代中,不知道凭借没有这个bug的小小优点,能不能从你手里取一个star,哈哈


bug原因


affix组件无非都是用了fixed布局,我是如何发现这个bug的呢,我的组件库动画系统用的framer-motion,我本来是想在react-router切换路由的时候整点动画的,动画效果就是给body元素加入例如transform的变化。


然后我再看我的固钉组件怎么失效了。。。后来仔细一想,才发现想起来fixed布局的一个坑就是,大家都以为fixed布局相对的父元素是window窗口,其实是错误的!


真正的规则如下(以下说的包含块就是fixed布局的定位父元素):

  1. 如果 position 属性是 absolute 或 fixed,包含块也可能是由满足以下条件的最近父级元素的内边距区的边缘组成的:

  • transform 或 perspective 的值不是 none
  • will-change 的值是 transform 或 perspective
  • filter 的值不是 none 或 will-change 的值是 filter(只在 Firefox 下生效)。
  • contain 的值是 paint(例如:contain: paint;
  • backdrop-filter 的值不是 none(例如:backdrop-filter: blur(10px);

评论区有很多同学居然觉的这不是bug?


其实这个问题本质是定位错误,在这些组件库里,同样使用到定位的有,例如Tooltip,Select,Popuver等等,明显这些组件跟写Affix组件的不是一个人,其他组件这个bug是没有的,只有Affix组件出现了,所以你说这是不是bug。


还有,如果因为引用了Affix组件,这个固定元素的任一父元素都不能用以上的css属性,我作为使用者,我用了动画库,动画库使用transfrom做Gpu加速,你说不让我用了,因为引起Affix组件bug,我心里想凭啥啊,明明加两行代码就解决了。


最后,只要做过定位组件的同学,其复杂度在前端算是比较高的了,这也是为什么有些组件库直接用第三方定位组件库(floating-ui,@popper-js),而不是自己去实现,因为自己实现很容易出bug,这也是例如以上组件库Tooltip为什么能适应很多边界case而不出bug。


所以你想想,这仅仅是定位组件遇到的一个很小的问题,你这个都解决不了,什么都怪css,你觉得用户会这么想吗,一有css,你所有跟定位相关的组件全部都不能用了,你们还讲理不?


总之一句话,你不能把定位组件的复杂度高怪用户没好好用,建议去看看floating-ui的源码,或者之前我写的@popper-js定位组件的简要逻辑梳理,你就会意识到定位组件不简单。边界case多如牛毛。


解决方案


  • 首先是找出要固定元素的定位元素(定位元素的判断逻辑上面写了),然后如果定位元素是window,那么跟目前所有组件库的逻辑一样,所以没有bug,如果不是window,就要求出相对定位父元素距离可视窗口顶部的top的值
  • 然后在我们原本要定位的值,比如距离顶部80px的时候固定,此时80px再减去上面说的定位父元素距离可视窗口顶部的top的值,就没有bug了

具体代码如下:


  • offsetParent固定元素的定位上下文,也就是相对定位的父元素
  • fixedTop是我们要触发固定的值,比如距离可视窗口顶部80px就固定
affixDom.style.top = `${isHTMLElement(offsetParent) ? (fixedTop as number) - offsetParent.getBoundingClientRect().top : fixedTop}px`;

如何找出offsetParent,也就是定位上下文

export function getContainingBlock(element: Element) {
let currentNode = element.parentElement;
while (currentNode) {
if (isContainingBlock(currentNode)) return currentNode;
currentNode = currentNode.parentElement;
}
return null;
}

工具方法,isContainingBlock如下:

import { isSafari } from './isSafari';

export function isContainingBlock(element: Element): boolean {
const safari = isSafari();
const css = getComputedStyle(element);

// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
return (
css.transform !== 'none' ||
css.perspective !== 'none' ||
(css.containerType ? css.containerType !== 'normal' : false) ||
(!safari && (css.backdropFilter ? css.backdropFilter !== 'none' : false)) ||
(!safari && (css.filter ? css.filter !== 'none' : false)) ||
['transform', 'perspective', 'filter'].some((value) => (css.willChange || '').includes(value)) ||
['paint', 'layout', 'strict', 'content'].some((value) => (css.contain || '').includes(value))
);
}


本文完毕,求关注,求star!!!对于react组件库感兴趣的小伙伴,欢迎加群一起交流哦!


作者:孟祥_成都
链接:https://juejin.cn/post/7265121637497733155
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

彻底搞懂小程序登录流程-附小程序和服务端代码

web
编者按:本文作者奇舞团高级前端开发工程师冯通 用户登录是大部分完整 App 必备的流程 一个简单的用户系统需要关注至少这些层面 安全性(加密) 持久化登录态(类似cookie) 登录过期处理 确保用户唯一性, 避免出现多账号 授权 绑定用户昵称头像等信息 ...
继续阅读 »

编者按:本文作者奇舞团高级前端开发工程师冯通



用户登录是大部分完整 App 必备的流程


一个简单的用户系统需要关注至少这些层面



  • 安全性(加密)

  • 持久化登录态(类似cookie)

  • 登录过期处理

  • 确保用户唯一性, 避免出现多账号

  • 授权

  • 绑定用户昵称头像等信息

  • 绑定手机号(实名和密保方式)


很多的业务需求都可以抽象成 Restful 接口配合 CRUD 操作


但登录流程却是错综复杂, 各个平台有各自的流程, 反倒成了项目中费时间的部分, 比如小程序的登录流程



对于一个从零开始的项目来说, 搞定登录流程, 就是一个好的开始, 一个好的开始, 就是成功的一半


本文就以微信小程序这个平台, 讲述一个完整的自定义用户登录流程, 一起来啃这块难啃的骨头


名词解释


先给登录流程时序图中出现的名词简单做一个解释



  • code 临时登录凭证, 有效期五分钟, 通过 wx.login() 获取

  • session_key 会话密钥, 服务端通过 code2Session 获取

  • openId 用户在该小程序下的用户唯一标识, 永远不变, 服务端通过 code 获取

  • unionId 用户在同一个微信开放平台帐号(公众号, 小程序, 网站, 移动应用)下的唯一标识, 永远不变

  • appId 小程序唯一标识

  • appSecret 小程序的 app secret, 可以和 code, appId 一起换取 session_key


其他名词



  • rawData 不包括敏感信息的原始数据字符串,用于计算签名

  • encryptedData 包含敏感信息的用户信息, 是加密的

  • signature 用于校验用户信息是否无篡改

  • iv 加密算法的初始向量



哪些信息是敏感信息呢? 手机号, openId, unionId, 可以看出这些值都可以唯一定位一个用户, 而昵称, 头像这些不能定位用户的都不是敏感信息



小程序登录相关函数



  • wx.login

  • wx.getUserInfo

  • wx.checkSession


小程序的 promise


我们发现小程序的异步接口都是 success 和 fail 的回调, 很容易写出回调地狱


因此可以先简单实现一个 wx 异步函数转成 promise 的工具函数


const promisify = original => {
return function(opt) {
return new Promise((resolve, reject) => {
opt = Object.assign({
success: resolve,
fail: reject
}, opt)
original(opt)
})
}
}

这样我们就可以这样调用函数了


promisify(wx.getStorage)({key: 'key'}).then(value => {
// success
}).catch(reason => {
// fail
})

服务端实现


本 demo 的服务端实现基于 express.js



注意, 为了 demo 的简洁性, 服务端使用 js 变量来保存用户数据, 也就是说如果重启服务端, 用户数据就清空了




如需持久化存储用户数据, 可以自行实现数据库相关逻辑



// 存储所有用户信息
const users = {
// openId 作为索引
openId: {
// 数据结构如下
openId: '', // 理论上不应该返回给前端
sessionKey: '',
nickName: '',
avatarUrl: '',
unionId: '',
phoneNumber: ''
}
}

app
.use(bodyParser.json())
.use(session({
secret: 'alittlegirl',
resave: false,
saveUninitialized: true
}))

小程序登录


我们先实现一个基本的 oauth 授权登录



oauth 授权登录主要是 code 换取 openId 和 sessionKey 的过程



前端小程序登录


写在 app.js 中


login () {
console.log('登录')
return util.promisify(wx.login)().then(({code}) => {
console.log(`code: ${code}`)
return http.post('/oauth/login', {
code,
type: 'wxapp'
})
})
}

服务端实现 oauth 授权


服务端实现上述 /oauth/login 这个接口


app
.post('/oauth/login', (req, res) => {
var params = req.body
var {code, type} = params
if (type === 'wxapp') {
// code 换取 openId 和 sessionKey 的主要逻辑
axios.get('https://api.weixin.qq.com/sns/jscode2session', {
params: {
appid: config.appId,
secret: config.appSecret,
js_code: code,
grant_type: 'authorization_code'
}
}).then(({data}) => {
var openId = data.openid
var user = users[openId]
if (!user) {
user = {
openId,
sessionKey: data.session_key
}
users[openId] = user
console.log('新用户', user)
} else {
console.log('老用户', user)
}
req.session.openId = user.openId
req.user = user
}).then(() => {
res.send({
code: 0
})
})
} else {
throw new Error('未知的授权类型')
}
})

获取用户信息


登录系统中都会有一个重要的功能: 获取用户信息, 我们称之为 getUserInfo


如果已登录用户调用 getUserInfo 则返回用户信息, 比如昵称, 头像等, 如果未登录则返回"用户未登录"



也就是说此接口还有判断用户是否登录的功效...



小程序的用户信息一般存储在 app.globalData.userInfo 中(模板如此)


我们在服务端加上前置中间件, 通过 session 来获取对应的用户信息, 并放在 req 对象中


app
.use((req, res, next) => {
req.user = users[req.session.openId]
next()
})

然后实现 /user/info 接口, 用来返回用户信息


app
.get('/user/info', (req, res) => {
if (req.user) {
return res.send({
code: 0,
data: req.user
})
}
throw new Error('用户未登录')
})

小程序调用用户信息接口


getUserInfo () {
return http.get('/user/info').then(response => {
let data = response.data
if (data && typeof data === 'object') {
// 获取用户信息成功则保存到全局
this.globalData.userInfo = data
return data
}
return Promise.reject(response)
})
}

专为小程序发请求设计的库


小程序代码通过 http.get, http.post 这样的 api 来发请求, 背后使用了一个请求库


@chunpu/http 是一个专门为小程序设计的 http 请求库, 可以在小程序上像 axios 一样发请求, 支持拦截器等强大功能, 甚至比 axios 更顺手


初始化方法如下


import http from '@chunpu/http'

http.init({
baseURL: 'http://localhost:9999', // 定义 baseURL, 用于本地测试
wx // 标记是微信小程序用
})

具体使用方法可参照文档 github.com/chunpu/http…


自定义登录态持久化


浏览器有 cookie, 然而小程序没有 cookie, 那怎么模仿出像网页这样的登录态呢?


这里要用到小程序自己的持久化接口, 也就是 setStorage 和 getStorage


为了方便各端共用接口, 或者直接复用 web 接口, 我们自行实现一个简单的读 cookie 和种 cookie 的逻辑


先是要根依据返回的 http response headers 来种上 cookie, 此处我们用到了 @chunpu/http 中的 response 拦截器, 和 axios 用法一样


http.interceptors.response.use(response => {
// 种 cookie
var {headers} = response
var cookies = headers['set-cookie'] || ''
cookies = cookies.split(/, */).reduce((prev, item) => {
item = item.split(/; */)[0]
var obj = http.qs.parse(item)
return Object.assign(prev, obj)
}, {})
if (cookies) {
return util.promisify(wx.getStorage)({
key: 'cookie'
}).catch(() => {}).then(res => {
res = res || {}
var allCookies = res.data || {}
Object.assign(allCookies, cookies)
return util.promisify(wx.setStorage)({
key: 'cookie',
data: allCookies
})
}).then(() => {
return response
})
}
return response
})

当然我们还需要在发请求的时候带上所有 cookie, 此处用的是 request 拦截器


http.interceptors.request.use(config => {
// 给请求带上 cookie
return util.promisify(wx.getStorage)({
key: 'cookie'
}).catch(() => {}).then(res => {
if (res && res.data) {
Object.assign(config.headers, {
Cookie: http.qs.stringify(res.data, ';', '=')
})
}
return config
})
})

登录态的有效期


我们知道, 浏览器里面的登录态 cookie 是有失效时间的, 比如一天, 七天, 或者一个月


也许有朋友会提出疑问, 直接用 storage 的话, 小程序的登录态有效期怎么办?


问到点上了! 小程序已经帮我们实现好了 session 有效期的判断 wx.checkSession


它比 cookie 更智能, 官方文档描述如下



通过 wx.login 接口获得的用户登录态拥有一定的时效性。用户越久未使用小程序,用户登录态越有可能失效。反之如果用户一直在使用小程序,则用户登录态一直保持有效



也就是说小程序还会帮我们自动 renew 咱们的登录态, 简直是人工智能 cookie, 点个赞👍


那具体在前端怎么操作呢? 代码写在 app.js 中


onLaunch: function () {
util.promisify(wx.checkSession)().then(() => {
console.log('session 生效')
return this.getUserInfo()
}).then(userInfo => {
console.log('登录成功', userInfo)
}).catch(err => {
console.log('自动登录失败, 重新登录', err)
return this.login()
}).catch(err => {
console.log('手动登录失败', err)
})
}

要注意, 这里的 session 不仅是前端的登录态, 也是后端 session_key 的有效期, 前端登录态失效了, 那后端也失效了需要更新 session_key



理论上小程序也可以自定义登录失效时间策略, 但这样的话我们需要考虑开发者自己的失效时间和小程序接口服务的失效时间, 还不如保持统一来的简单



确保每个 Page 都能获取到 userInfo


如果在新建小程序项目中选择 建立普通快速启动模板


我们会得到一个可以直接运行的模板


点开代码一看, 大部分代码都在处理 userInfo....



注释里写着



由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回




所以此处加入 callback 以防止这种情况



但这样的模板并不科学, 这样仅仅是考虑了首页需要用户信息的情况, 如果扫码进入的页面也需要用户信息呢? 还有直接进入跳转的未支付页活动页等...


如果每个页面都这样判断一遍是否加载完用户信息, 代码显得过于冗余


此时我们想到了 jQuery 的 ready 函数 $(function), 只要 document ready 了, 就可以直接执行函数里面的代码, 如果 document 还没 ready, 就等到 ready 后执行代码


就这个思路了! 我们把小程序的 App 当成网页的 document


我们的目标是可以这样在 Page 中不会出错的获取 userInfo


Page({
data: {
userInfo: null
},
onLoad: function () {
app.ready(() => {
this.setData({
userInfo: app.globalData.userInfo
})
})
}
})

此处我们使用 min-ready 来实现此功能


代码实现依然写在 app.js 中


import Ready from 'min-ready'

const ready = Ready()

App({
getUserInfo () {
// 获取用户信息作为全局方法
return http.get('/user/info').then(response => {
let data = response.data
if (data && typeof data === 'object') {
this.globalData.userInfo = data
// 获取 userInfo 成功的时机就是 app ready 的时机
ready.open()
return data
}
return Promise.reject(response)
})
},
ready (func) {
// 把函数放入队列中
ready.queue(func)
}
})

绑定用户信息和手机号


仅仅获取用户的 openId 是远远不够的, openId 只能标记用户, 连用户的昵称和头像都拿不到


如何获取这些用户信息然后存到后端数据库中呢?


我们在服务端实现这两个接口, 绑定用户信息, 绑定用户手机号


app
.post('/user/bindinfo', (req, res) => {
var user = req.user
if (user) {
var {encryptedData, iv} = req.body
var pc = new WXBizDataCrypt(config.appId, user.sessionKey)
var data = pc.decryptData(encryptedData, iv)
Object.assign(user, data)
return res.send({
code: 0
})
}
throw new Error('用户未登录')
})

.post('/user/bindphone', (req, res) => {
var user = req.user
if (user) {
var {encryptedData, iv} = req.body
var pc = new WXBizDataCrypt(config.appId, user.sessionKey)
var data = pc.decryptData(encryptedData, iv)
Object.assign(user, data)
return res.send({
code: 0
})
}
throw new Error('用户未登录')
})

小程序个人中心 wxml 实现如下


<view wx:if="userInfo" class="userinfo">
<button
wx:if="{{!userInfo.nickName}}"
type="primary"
open-type="getUserInfo"
bindgetuserinfo="bindUserInfo">
获取头像昵称 </button>
<block wx:else>
<image class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover"></image>
<text class="userinfo-nickname">{{userInfo.nickName}}</text>
</block>

<button
wx:if="{{!userInfo.phoneNumber}}"
type="primary"
style="margin-top: 20px;"
open-type="getPhoneNumber"
bindgetphonenumber="bindPhoneNumber">
绑定手机号 </button>
<text wx:else>{{userInfo.phoneNumber}}</text>
</view>

小程序中的 bindUserInfo 和 bindPhoneNumber 函数, 根据微信最新的策略, 这俩操作都需要用户点击按钮统一授权才能触发


bindUserInfo (e) {
var detail = e.detail
if (detail.iv) {
http.post('/user/bindinfo', {
encryptedData: detail.encryptedData,
iv: detail.iv,
signature: detail.signature
}).then(() => {
return app.getUserInfo().then(userInfo => {
this.setData({
userInfo: userInfo
})
})
})
}
},
bindPhoneNumber (e) {
var detail = e.detail
if (detail.iv) {
http.post('/user/bindphone', {
encryptedData: detail.encryptedData,
iv: detail.iv
}).then(() => {
return app.getUserInfo().then(userInfo => {
this.setData({
userInfo: userInfo
})
})
})
}
}

代码


本文所提到的代码都可以在我的 github 上找到


小程序代码在 wxapp-login-demo


服务端 Node.js 代码在 wxapp-login-server


关于奇舞周刊


《奇舞周刊》是360公司专业前端团队「奇舞团」运营的前端技术社区。关注公众号后,直接发送链接到后台即可给我们投稿。



作者:奇舞精选
来源:juejin.cn/post/6844903702726180871
收起阅读 »

Bun 1.0 正式发布,爆火的前端运行时,速度遥遥领先!

web
9 月 8 日,前端运行时 Bun 1.0 正式发布,如今,Bun 已经稳定并且适用于生产环境。Bun 不仅是一个专注性能与开发者体验的全新 JavaScript 运行时,还是一个快速的、全能的工具包,可用于运行、构建、测试和调试JavaScript和Type...
继续阅读 »

9 月 8 日,前端运行时 Bun 1.0 正式发布,如今,Bun 已经稳定并且适用于生产环境。Bun 不仅是一个专注性能与开发者体验的全新 JavaScript 运行时,还是一个快速的、全能的工具包,可用于运行、构建、测试和调试JavaScript和TypeScript代码,无论是从单个文件还是完整的全栈应用。
image.png
2022年,Bun 发布,随即爆火,成为年度最火的前端项目:
image.png
Bun 的流行程度伴随着在去年夏天发布的第一个 Beta 版而爆炸性增长:仅一个月内,就在 GitHub 上获得了超过两万颗 Star。
star-history-202397.png



Bun 不仅仅是一个运行时。它也是:



  • 一个包管理器 (类似 Yarn、 NPM、 PNPM)

  • 一个构建工具 (类似 Webpack、 ESBuild、 Parcel)

  • 一个测试运行器

  • ...


所以 Bun 可以通过读取 package.json 来安装依赖项。Bun 还可以运行脚本。不管它做什么都比其他工具更快。Bun 在 JavaScript 生态系统的许多方面都有新的尝试,其中的重点是性能。它优先支持标准的 Web API,如 Fetch。它也支持许多 Node.js APIs,使其能与大多数 NPM 包兼容。



安装 Bun:


// npm
npm install -g bun

// brew
brew tap oven-sh/bun
brew install bun

// curl
curl -fsSL https://bun.sh/install | bash

// docker
docker pull oven/bun
docker run --rm --init --ulimit memlock=-1:-1 oven/bun

更新 Bun:


bun upgrade

下面就来看看 Bun 是什么,1.0 版本带来了哪些更新!


Bun:全能的工具包


JavaScript 成熟、发展迅速,并且有着充满活力和激情的开发者社区。然而,自14年前Node.js发布以来,JavaScript 的工具链变得越来越庞大和复杂。这是因为在发展过程中,各种工具被逐渐添加进来,但没有一个统一的集中规划,导致工具链缺乏整体性和效率,变得运行缓慢和复杂。


Bun 为什么会出现?


Bun的目标很简单,就是要消除JavaScript工具链的缓慢和复杂性,但同时保留JavaScript本身的优点。Bun希望让开发者继续使用喜欢的库和框架,并且无需放弃已经熟悉的规范和约定。


为了实现这个目标,可能需要放弃一些在使用Bun之后变得不再必要的工具:



  • Node.js:Bun 的一个可以直接替代的工具,因此不再需要以下工具:

    • node

    • npx:Bun 的 bunx 命令比 npx 快5倍。

    • nodemon:Bun 内置了监听模式,无需使用 nodemon

    • dotenvcross-env:Bun 默认支持读取.env文件的配置。



  • 转译器:Bun 可以运行.js.ts、``.cjs.mjs.jsx.tsx文件,因此不再需要以下工具:

    • tsc:仍然可以保留它用于类型检查!

    • babel.babelrc@babel/preset-*:不再需要使用 Babel 进行转译。

    • ts-nodets-node-esm:Bun 可以直接运行 TypeScript 文件。

    • tsx:Bun可以直接运行 TypeScript 的 JSX 文件。



  • 构建工具:Bun 具有一流的性能和与esbuild兼容的插件API,因此不再需要以下工具:

    • esbuild

    • webpack

    • parcel, .parcelrc

    • rollup, rollup.config.js



  • 包管理器:Bun 是一个与 npm 兼容的包管理器,可以使用熟悉的命令。它可以读取 package.json文件并将依赖写入node_modules目录,与其他包管理器的行为类似,因此可以替换以下工具:

    • npm, .npmrc, package-lock.json

    • yarn,yarn.lock

    • pnpm, pnpm.lock, pnpm-workspace.yaml

    • lern



  • 测试库:Bun是一个支持Jest的测试运行器,具有快照测试、模拟和代码覆盖率等功能,因此不再需要以下测试相关的工具:

    • jest, jest.config.js

    • ts-jest, @swc/jest, babel-jest

    • jest-extended

    • vitest, vitest.config.ts




尽管这些工具都有自己的优点,但使用它们时往往需要将它们全部集成在一起,这会导致开发过程变得缓慢和复杂。而Bun通过成为一个单一的工具包,提供了最佳的开发者体验,从性能到API设计都力求做到最好。


Bun:JavaScript 运行时


Bun是一个快速的JavaScript运行时。旨在提供出色的性能和开发体验。它的设计旨在解决开发过程中的各种痛点,使开发者的工作更加轻松和愉快。


与Node.js兼容


Bun 是可以直接替代 Node.js 的。这意味着现有的 Node.js 应用和 npm 包可以在 Bun 中正常工作。Bun 内置了对 Node.js API 的支持,包括:



  • 内置模块,如fspathnet

  • 全局对象,如__dirnameprocess

  • Node.js 模块解析算法(例如node_modules


尽管与 Node.js 完全兼容是不可能的,特别是一些依赖于v8版本的特性,但 Bun 几乎可以运行任何现有的 Node.js 应用。


Bun经过了与最受欢迎的Node.js包的兼容性测试,支持与Express、Koa、Hapi等服务端框架以及其他流行的全栈框架的无缝集成。开发者可以放心地在Bun中使用这些库和框架,并享受到更好的开发体验。
image.png
使用Next.js、Remix、Nuxt、Astro、SvelteKit、Nest、SolidStart和Vite构建的全栈应用可以在Bun中运行。


速度


Bun的速度非常快,启动速度比 Node.js 快 4 倍。当运行TypeScript文件时,这种差异会更加明显,因为在Node.js中运行TypeScript文件需要先进行转译才能运行。
image.png
Bun在运行一个简单的"Hello World" TypeScript文件时,比在Node.js中使用esbuild运行速度快5倍。


Bun使用的是Apple的WebKit引擎,而不是像Node.js和其他运行时一样使用Google的V8引擎。WebKit引擎是Safari浏览器的核心引擎,每天被数十亿的设备使用。它经过了长时间的实际应用和测试,具备快速和高效的特性。


TypeScript 和 JSX 支持


Bun内置了JavaScript转译器,因此可以运行JavaScript、TypeScript甚至JSX/TSX文件,无需任何依赖。


// 运行 TS 文件
bun index.ts

// 运行 JSX/TSX 文件
bun index.tsx

ESM 和 CommonJS 兼容


从CommonJS到ES模块的过渡一直是缓慢而充满挑战的。在引入ESM之后,Node.js花了5年时间才在没有--experimental-modules标志的情况下支持它。尽管如此,生态系统仍然充斥着CommonJS。


Bun 同时支持这两种模块系统。无论是使用CommonJS的.js扩展名、.cjs扩展名,还是使用ES模块的.mjs扩展名,Bun都会进行正确的解析和执行,而无需额外的配置。


甚至可以在同一个文件中同时使用importrequire()


import lodash from "lodash";
const _ = require("underscore");

Web API


Bun 内置支持浏览器中可用的Web标准API,如fetchRequestResponseWebSocketReadableStream等。


const response = await fetch("https://example.com/");
const text = await response.text();

开发者不再需要安装像node-fetchws这样的包。Bun内置的 Web API 是使用原生代码实现的,比第三方替代方案更快速和可靠。


热重载


Bun提供了热重载功能,可以在开发过程中实现文件的自动重新加载。只需在运行Bun时加上--hot参数,当文件发生变化时,Bun 就会自动重新加载你的应用,从而提高开发效率。


bun --hot server.ts

与像nodemon这样完全重新启动整个进程的工具不同,Bun 在重新加载代码时不会终止旧进程。这意味着HTTP和WebSocket连接不会断开,并且状态不会丢失。
hot (1).gif


插件


Bun 被设计为高度可定制的。
可以定义插件来拦截导入操作并执行自定义的加载逻辑。插件可以添加对其他文件类型的支持,比如.yaml.png。插件API的设计灵感来自于esbuild,这意味着大多数esbuild插件在 sBun 中也可以正常工作。


import { plugin } from "bun";

plugin({
name: "YAML",
async setup(build) {
const { load } = await import("js-yaml");
const { readFileSync } = await import("fs");
build.onLoad({ filter: /.(yaml|yml)$/ }, (args) => {
const text = readFileSync(args.path, "utf8");
const exports = load(text) as Record<string, any>;
return { exports, loader: "object" };
});
},
});

Bun API


Bun内部提供了针对开发者最常用需求的标准库API,并对其进行了高度优化。与Node.js的API不同,Node.js的API存在着向后兼容的考虑,而Bun的原生API则专注于提供更快速和更易于使用的功能。


Bun.file()


使用Bun.file()可以懒加载位于特定路径的文件。


const file = Bun.file("package.json");
const contents = await file.text();

它返回一个扩展了 Web 标准FileBunFile对象。文件内容可以以多种格式进行懒加载。


Bun.serve({
port: 3000,
fetch(request) {
return new Response("Hello from Bun!");
},
});

Bun每秒可以处理的请求比 Node.js 多 4 倍。


也可以使用tls选项来配置TLS(传输层安全协议)。


Bun.serve({
port: 3000,
fetch(request) {
return new Response("Hello from Bun!");
},
tls: {
key: Bun.file("/path/to/key.pem"),
cert: Bun.file("/path/to/cert.pem"),
}
});

Bun内置了对WebSocket的支持,只需要在websocket中定义一个事件处理程序来实现同时支持HTTP和WebSocket。而Node.js没有提供内置的WebSocket API,所以需要使用第三方依赖库(例如ws)来实现WebSocket的支持。因此,使用Bun可以更加方便和简单地实现WebSocket功能。


Bun.serve({
fetch() { ... },
websocket: {
open(ws) { ... },
message(ws, data) { ... },
close(ws, code, reason) { ... },
},
});

Bun 每秒可以处理的消息比在 Node.js 上使用 ws 库多 5 倍。


bun:sqlite


Bun内置了对 SQLite 的支持。它提供了一个受到better-sqlite3启发的API,但是使用本地代码编写,以达到更快的执行速度。


import { Database } from "bun:sqlite";

const db = new Database(":memory:");
const query = db.query("select 'Bun' as runtime;");
query.get(); // => { runtime: "Bun" }

在 Node.js 上,Bun 执行 SQLite 查询操作的速度比better-sqlite3快 4 倍。


Bun.password


Bun 还支持一些常见但复杂的API,不用自己去实现它们。


例如,可以使用Bun.password来使用bcryptargon2算法进行密码哈希和验证,无需外部依赖。


const password = "super-secure-pa$$word";
const hash = await Bun.password.hash(password);
// => $argon2id$v=19$m=65536,t=2,p=1$tFq+9AVr1bfPxQdh...

const isMatch = await Bun.password.verify(password, hash);
// => true

Bun:包管理器


Bun是一个包管理器。即使不使用Bun作为运行时环境,它内置的包管理器也可以加速开发流程。以前在安装依赖项时需要盯着npm的加载动画,现在可以通过Bun的包管理器更高效地进行依赖项的安装。


Bun可能看起来像你熟悉的包管理器:


bun install
bun add <package> [--dev|--production|--peer]
bun remove <package>
bun update <package>

安装速度


Bun的安装速度比 npm、yarn 和 pnpm 快好几个数量级。它利用全局模块缓存来避免从npm注册表中重复下载,并使用每个操作系统上最快速的系统调用。
image.png


运行脚本


很可能你已经有一段时间没有直接使用 Node 来运行脚本了。相反,通常使用包管理器(如npm、yarn等)与框架和命令行界面(CLI)进行交互,以构建应用。


npm run dev

你可以用bun run来替换npm run,每次运行命令都能节省 150 毫秒的时间。


这些数字可能看起来很小,但在运行命令行界面(CLI)时,感知上的差异是巨大的。使用"npm run"会明显感到延迟:
265893417-fbfb4172-5a91-4158-904f-55f2dbb0acde.gif而使用bun run则感觉几乎瞬间完成:
image.png
并不只是针对npm进行比较。实际上,bun run <command>的速度比yarn和pnpm中相应的命令更快。


脚本运行平均时间
npm run176ms
yarn run131ms
pnpm run259ms
bun run7ms 🚀

Bun:测试运行器


如果你以前在 JavaScript 中写过测试,可能了解 Jest,它开创了“expect”风格的API。


Bun有一个内置的测试模块bun:test,它与Jest完全兼容。


import { test, expect } from "bun:test";

test("2 + 2", () => {
expect(2 + 2).toBe(4);
});

可以使用bun test命令来运行测试:


bun test

还将获得 Bun 运行时的所有优势,包括TypeScript和JSX支持。


从Jest或Vite迁移很简单。@jest/globalsvitest的任何导入将在内部重新映射到bun:test,因此即使不进行任何代码更改,一切也将正常运行。


import { test } from "@jest/globals";

describe("test suite", () => {
// ...
});

在与 zod 的测试套件进行基准测试中,Bun比Jest快13倍,比Vite快8倍。
image.png
Bun的匹配器由快速的原生代码实现,Bun中的expect().toEqual()比Jest快100倍,比Vite快10倍。


可以使用bun test命令来加快 CI 构建速度,如果在Github Actions中,可以使用官方的oven-sh/setup-bun操作来设置Bun


name: CI
on: [push, pull_request]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: oven-sh/setup-bun@v1
- run: bun test

Bun会自动为测试失败的部分添加注释,以便在持续集成(CI)日志中更容易理解。这样,当出现测试失败时,可以直接从日志中读取Bun提供的注释,而不需要深入分析代码和测试结果,从而更方便地检查问题所在。
image.png


Bun:构建工具


Bun是一个JavaScript和TypeScript的构建工具和代码压缩工具,可用于将代码打包成适用于浏览器、Node.js和其他平台的形式。


bun build ./index.tsx --outdir ./build

Bun 受到了 esbuild 的启发,并提供了兼容的插件API。


import mdx from "@mdx-js/esbuild";

Bun.build({
entrypoints: ["index.tsx"],
outdir: "build",
plugins: [mdx()],
});

Bun 的插件 API 是通用的,这意味着它适用于打包工具和运行时。所以前面提到的.yaml插件可以在这里使用,以支持在打包过程中导入.yaml文件。


根据esbuild的基准测试,Bun比esbuild快1.75倍,比Parcel 2快150倍,比Rollup + Terser快180倍,比Webpack快220倍。
image.png
由于Bun的运行时和打包工具是集成在一起的,这意味着Bun可以做其他打包工具无法做到的事情。


Bun引入了JavaScript宏机制,可以在打包时运行JavaScript函数。这些函数返回的值会直接内联到打包文件中。


// release.ts
export async function getRelease(): Promise<string> {
const response = await fetch(
"https://api.github.com/repos/oven-sh/bun/releases/latest"
);
const { tag_name } = await response.json();
return tag_name;
}

// index.ts
import { getRelease } from "./release.ts" with { type: "macro" };

// release的值是在打包时进行评估的,并且内联到打包文件中,而不是在运行时执行。
const release = await getRelease();

bun build index.ts
// index.ts
var release = await "bun-v1.0.0";

Bun:可以做更多事


Bun 在 macOS 和 Linux 上提供了原生构建支持,但 Windows 一直是一个明显的缺失。以前,在 Windows 上运行 Bun 需要安装 Windows 子系统来运行Linux系统,但现在不再需要。


Bun 首次发布了一个实验性的、专为Windows平台的本地版本的 Bun。这意味着Windows用户现在可以直接在其操作系统上使用 Bun,而无需额外的配置。
image.png
尽管Bun的macOS和Linux版本已经可以用于生产环境,但Windows版本目前仍然处于高度实验阶段。目前只支持JavaScript运行时,而包管理器、测试运行器和打包工具在稳定性更高之前都将被禁用。性能方面也还未进行优化。


Bun:面向未来


Bun 1.0 只是一个开始。Bun 团队正在开发一种全新的部署JavaScript和TypeScript到生产环境的方式,期待 Bun 未来更好的表现!


作者:CUGGZ
来源:juejin.cn/post/7277387014046335010
收起阅读 »

Htmx 意外走红,我们从 React“退回去”后:代码行数减少 67%,JS 依赖项从 255 下降到 9

htmx 的走红 过去 Web 非常简单。URL 指向服务器,服务器将数据混合成 html,然后在浏览器上呈现该响应。围绕这种简单范式,诞生了各种 Javascript 框架,以前可能需要数月时间完成的一个应用程序基本功能,现在借助这些框架创建相对复杂的项目却...
继续阅读 »

htmx 的走红


过去 Web 非常简单。URL 指向服务器,服务器将数据混合成 html,然后在浏览器上呈现该响应。围绕这种简单范式,诞生了各种 Javascript 框架,以前可能需要数月时间完成的一个应用程序基本功能,现在借助这些框架创建相对复杂的项目却只需要数小时,我们节省了很多时间,从而可以将更多精力花在业务逻辑和应用程序设计上。


但随着 Web 不断地发展,Javascript 失控了。不知何故,我们决定向用户抛出大量 App,并在使用时发出不断增加的网络请求;不知何故,为了生成 html,我们必须使用 JSON,发出数十个网络请求,丢弃我们在这些请求中获得的大部分数据,用一个越来越不透明的 JavaScript 框架黑匣子将 JSON 转换为 html,然后将新的 html 修补到 DOM 中......


难道大家快忘记了我们可以在服务器上渲染 html 吗?更快、更一致、更接近应用程序的实际状态,并且不会向用户设备发送任何不必要的数据?但是如果没有 Javascript,我们必须在每次操作时重新加载页面。



现在,有一个新的库出现了,摒弃了定制化的方法,这就是 htmx。作为 Web 开发未来理念的一种实现,它的原理很简单:

  • 从任何用户事件发出 AJAX 请求。

  • 让服务器生成代表该请求的新应用程序状态的 html。

  • 在响应中发送该 html。

  • 将该元素推到它应该去的 DOM 中。


htmx 出现在 2020 年,创建者Carson Gross 说 htmx 来源自他于 2013 年研究的一个项目intercooler.js。2020 年,他重写了不依赖 jQuery 的 intercooler.js,并将其重命名为 htmx。然后他惊讶的发现 Django 社区迅速并戏剧性地接受了它!



图片来源:lp.jetbrains.com/django-deve…2021-486/


Carson Gross认为 htmx 设法抓住了开发者对现有 Javascript 框架不满的浪潮,“这些框架非常复杂,并且经常将 Django 变成一个愚蠢的 JSON 生产者”,而 htmx 与开箱即用的 Django 配合得更好,因为它通过 html 与服务器交互,而 Django 非常擅长生成 html。


对于 htmx 的迅速走红,Carson Gross 发出了一声感叹:这真是“十年窗下无人问,一举成名天下知(this is another example of a decade-long overnight success)”。


htmx 的实际效果


可以肯定的一点是 htmx 绝对能用,单从理论上讲,这个方法确实值得称道。但软件问题终究要归结于实践效果:效果好吗,能不能给前端开发带来改善?


在 DjangoCon 2022 上,Contexte 的 David Guillot 演示了他们在真实 SaaS 产品上实现了从 React 到 htmx 的迁移,而且效果非常好,堪称“一切 htmx 演示之母”(视频地址:http://www.youtube.com/watch?v=3GO…)。


Contexte 的项目开始于 2017 年,其后端相当复杂,前端 UI 也非常丰富,但团队非常小。所以他们在一开始的时候跟随潮流选择了 React 来“构建API绑定 SPA、实现客户端状态管理、前后端状态分离”等。但实际应用中,因为 API 设计不当,DOM 树太深,又需要加载很多信息,导致 UI“非常非常缓慢”。在敏捷开发的要求下,团队里唯一的 Javascript 专家对项目的复杂性表现得一无所措,因此他们决定试试 htmx。


九大数据提升



于是我们决定大胆尝试,花几个月时间用简单的 Django 模板和 htmx 替换掉了 SaaS 产品中已经使用两年的 React UI。这里我们分享了一些相关经验,公布各项具体指标,希望能帮同样关注 htmx 的朋友们找到说服 CTO 的理由!

  • 这项工作共耗费了约 2 个月时间(使用 21K 行代码库,主要是 JavaScript)

  • 不会降低应用程序的用户体验(UX)

  • 将代码库体积减小了 67%(由 21500 行削减至 7200 行)

  • 将 Python 代码量增加了 140%(由 500 行增加至 1200 行);这对更喜欢 Python 的开发者们应该是好事

  • 将 JS 总体依赖项减少了 96%(由 255 个减少至 9 个)


  • 将 Web 构建时间缩短了 88%(由 40 秒缩短至 5 秒)

  • 首次加载交互时间缩短了 50%至 60%(由 2 到 6 秒,缩短至 1 到 2 秒)

  • 使用 htmx 时可以配合更大的数据集,超越 React 的处理极限

  • Web 应用程序的内存使用量减少了 46%(由 75 MB 降低至 40 MB)



这些数字令人颇为意外,也反映出 Contexte 应用程序高度契合超媒体的这一客观结果:这是一款以内容为中心的应用程序,用于显示大量文本和图像。很明显,其他 Web 应用程序在迁移之后恐怕很难有同样夸张的提升幅度。


但一些开发者仍然相信,大部分应用程序在采用超媒体/htmx 方法之后,肯定也迎来显著的改善,至少在部分系统中大受裨益。


开发团队组成


可能很多朋友没有注意,移植本身对团队结构也有直接影响。在 Contexte 使用 React 的时候,后端与前端之间存在硬性割裂,其中两位开发者全职管理后端,一位开发者单纯管理前端,另有一名开发者负责“全栈”。(这里的「全栈」,代表这位开发者能够轻松接手前端和后端工作,因此能够在整个「栈」上独立开发功能。)



而在移植至 htmx 之后,整个团队全都成了“全栈”开发人员。于是每位团队成员都更高效,能够贡献出更多价值。这也让开发变得更有乐趣,因为开发人员自己就能掌握完整功能。最后,转向 htmx 也让软件优化度上了一个台阶,现在开发人员可以在栈内的任意位置进行优化,无需与其他开发者提前协调。


htmx 是传统思路的回归


如今,单页应用(SPA)可谓风靡一时:配合 React、Redux 或 Angular 等库的 JS 或 TS 密集型前端,已经成为创建 Web 应用程序的主流方式。以一个需要转译成 JS 的 SPA 应用为例:



但 htmx 风潮已经袭来,人们开始强调一种“傻瓜客户端”方法,即由服务器生成 html 本体并发送至客户端,意味着 UI 事件会被发送至服务器进行处理。



用这个例子进行前后对比,我们就会看到前者涉及的活动部件更多。从客户端角度出发,后者其实回避了定制化客户端技术,采取更简单的方法将原本只作为数据引擎的服务器变成了视图引擎。


后一种方法被称为 AJAX(异步 JavaScript 与 XML)。这种简单思路能够让 Web 应用程序获得更高的响应性体验,同时消除了糟糕的“回发”(postback,即网页完全刷新),由此回避了极其低效的“viewstate”等.NET 技术。


htmx 在很多方面都体现出对 AJAX 思路的回归,最大的区别就是它仅仅作为新的声明性 html 属性出现,负责指示触发条件是什么、要发布到哪个端点等。


另一个得到简化的元素是物理应用程序的结构与构建管道。因为不再涉及手工编写 JS,而且整个应用程序都基于服务器,因此不再对 JS 压缩器、捆绑器和转译器做(即时)要求。就连客户端项目也能解放出来,一切都由 Web 服务器项目负责完成,所有应用程序代码都在.NET 之上运行。从这个角度来看,这与高度依赖服务器的Blazor Server编程模型倒是颇有异曲同工之妙。


技术和软件开发领域存在一种有趣的现象,就是同样的模式迭起兴衰、周而复始。随着 SPA 的兴起,人们一度以为 AJAX 已经过气了,但其基本思路如今正卷土重来。这其中当然会有不同的权衡,例如更高的服务器负载和网络流量(毕竟现在我们发送的是数据视图,而不只是数据),但能让开发者多个选择肯定不是坏事。


虽然不敢确定这种趋势是否适用于包含丰富用户体验的高复杂度应用程序,但毫无疑问,相当一部分 Web 应用程序并不需要完整的 SPA 结构。对于这类用例,简单的 htmx 应用程序可能就是最好的解决方案。


参考链接:


news.ycombinator.com/item?id=332…


htmx.org/essays/a-re…


http://www.reddit.com/r/django/co…


mekhami.github.io/2021/03/26/…


http://www.compositional-it.com/news-blog/m…


作者:MobTech开发者
链接:https://juejin.cn/post/7156507227669413895
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Taro开发小程序记录-海报生成

Taro开发小程序记录-海报生成在公司开发的第一个项目是一个简单的Taro小程序项目,主要应用的则是微信小程序,由于是第一次使用Taro,所以在开发过程中遇到的一些问题和一些开发技巧,记录一下,方便以后进行查看,也与大家进行分享。 自定义海报 说到自定义海报...
继续阅读 »

Taro开发小程序记录-海报生成


在公司开发的第一个项目是一个简单的Taro小程序项目,主要应用的则是微信小程序,由于是第一次使用Taro,所以在开发过程中遇到的一些问题和一些开发技巧,记录一下,方便以后进行查看,也与大家进行分享。



自定义海报


说到自定义海报可以说是很多小程序中都会进行开发的内容,比如需要进行二维码的保存,然后再对二维码进行一点文字的修饰,涉及到这方面的时候我们就需要使用canvas了。


在实际开发的过程中,遇到了一些很坑的问题,当我们需要使用离屏canvas来进行绘制时,我们可能就会遇到问题(我自己就遇到了)。


对于安卓端,我们可以正常的使用OffscreenCanvas来创建离屏canvas,然后绘制相关内容,最后在使用Taro.canvasToTempFilePath方法保存到临时文件下,Taro.canvasToTempFilePath方法会返回文件路径,我们就可以通过获取到的文件路径来进行下载。


下面是安卓端的一个🌰,大家有需要也可以直接拿去使用


  • 需要使用到的方法
 /**
  * @description 获取二维码图像
  */
 export const qrCodeImage = async (qrCodeValue: string, size: number = 128) => {
     /* NOTE: 通过创建离屏canvas承载code */
     const context = createOffscreenCanvas('2d', size, size);
     QRCode.toCanvas(context, qrCodeValue, { width: size, height: size, margin: 1 });
     return (context as unknown as HTMLCanvasElement).toDataURL();
 };
 /**
  * @description 创建离屏canvas对象,width与height单位为px
  */
 export const createOffscreenCanvas = (type: '2d' | 'webgl', width: number = 100, height: number = 100) => {
     return Taro.createOffscreenCanvas({ type, width, height });
 };
 /**
  * @description 将传入的图片url转换成一个ImageElement对象
  */
 export const loadImageByUrlToCanvasImageData = async (url: string, width: number = 100, height: number = 100) => {
     const context = createOffscreenCanvas('2d', width, height);
     const imageElement = context.createImage();
     await new Promise(resolve => {
         imageElement.onload = resolve;
         imageElement.src = url;
    });
     return imageElement;
 };
 /**
  * @description 将canvas转成图片文件并保存在临时路径下
  */
 export const changeCanvasToImageFileAndSaveToTempFilePath = async (options: Taro.canvasToTempFilePath.Option) => {
     const successCallback = await Taro.canvasToTempFilePath(options);
     return successCallback.tempFilePath;
 };
 interface SettingOptions {
     title: string;
     titleInfo: {
         dx: number;
         dy: number;
         color?: string;
         font?: string;
    };
     imageUrl: string;
     imagePos: {
         dx: number;
         dy: number;
    };
     width: number;
     height: number;
 }
 /**
  * @description 获取二维码图像并设置标题
  */
 export const generateQrCodeWithTitle = async (option: SettingOptions): Promise<string> => {
     const {
         title,
         titleInfo,
         imageUrl,
         imagePos,
         width,
         height,
    } = option;
     const context = await createOffscreenCanvas('2d', width, height);
     const ctx = (context.getContext('2d') as CanvasRenderingContext2D);
     const imgElement: any = await loadImageByUrlToCanvasImageData(imageUrl, width, height);
     ctx.fillStyle = 'white';
     ctx.fillRect(0, 0, width, height);
     ctx.fillStyle = titleInfo.color || 'black';
     ctx.font = titleInfo.font || '';
     ctx.textAlign = 'center';
     ctx.fillText(title, titleInfo.dx || 0, titleInfo.dy || 0);
     ctx.drawImage(imgElement, imagePos.dx, imagePos.dy);
 
     const filePath = await changeCanvasToImageFileAndSaveToTempFilePath({
         canvas: (context as Canvas),
         width,
         height,
         fileType: 'png',
         destWidth: width,
         destHeight: height,
    });
     return filePath;
 };
 /**
  * @description 保存图片
  */
 export const saveImage = async (urls: string[], isLocal: boolean = true) => {
     let filePath = urls;
     if (!isLocal) {
         filePath = await netImageToLocal(urls);
    }
     await Promise.all(filePath.map(path => {
         return Taro.saveImageToPhotosAlbum({ filePath: path });
    }));
 
     return true;
 };
 /**
  * @description 加载在线图片,并返回临时图片文件地址
  */
 export const netImageToLocal = async (urls: string[]) => {
     const res = await Promise.all(urls.map((url:string) => {
         return Taro.downloadFile({ url });
    }));
 
     const result = res.map(data => {
         if (data.statusCode === 200) {
             return data.tempFilePath;
        }
         throw new Error(data.errMsg);
    });
 
     return result;
 };
 /**
  * @description 判断用户是否授权保存图片
  */
 export const checkHasAuthorizedSaveImagePermissions = async () => {
     const setting = await Taro.getSetting();
     const { authSetting } = setting;
     return authSetting['scope.writePhotosAlbum'];
 };
 /**
  * @description 下载图片,需要区分是本地图片还是在线图片
  */
 export const downloadImage = async (urls: string[], isLocal: boolean = true) => {
     const hasSaveImagePermissions = await checkHasAuthorizedSaveImagePermissions();
     if (hasSaveImagePermissions === undefined) {
         // NOTE: 用户未授权情况下,进行用户授权,允许保存图片
         await Taro.authorize({ scope: 'scope.writePhotosAlbum' });
         return await saveImage(urls, isLocal);
    } else if (typeof hasSaveImagePermissions === 'boolean' && !hasSaveImagePermissions) {
         return new Promise((resolve, reject) => {
             Taro.showModal({
                 title: '是否授权保存到相册',
                 content: '需要获取您的保存图片权限,请确认授权,否则图片将无法保存到相册',
                 success: (result) => {
                     if (result.confirm) {
                         Taro.openSetting({
                             success: async (data) => {
                                 if (data.authSetting['scope.writePhotosAlbum']) {
                                     showLoadingModal('正在保存...');
                                     resolve(await saveImage(urls, isLocal));
                                }
                            },
                        });
                    } else {
                         reject(new Error('未授予保存权限'));
                    }
                },
            });
        });
    }
     await saveImage(urls, isLocal);
     return true;
 };

  • 生成海报(二维码+标题头)

 /**
  * @description 获取二维码图像并设置标题
  */
 export const generateQrCodeWithTitle = async (option: SettingOptions): Promise<string> => {
     const {
         title,
         titleInfo,
         imageUrl,
         imagePos,
         width,
         height,
    } = option;
     const context = await createOffscreenCanvas('2d', width, height);
     const ctx = (context.getContext('2d') as CanvasRenderingContext2D);
     const imgElement: any = await loadImageByUrlToCanvasImageData(imageUrl, width, height);
     ctx.fillStyle = 'white';
     ctx.fillRect(0, 0, width, height);
     ctx.fillStyle = titleInfo.color || 'black';
     ctx.font = titleInfo.font || '';
     ctx.textAlign = 'center';
     ctx.fillText(title, titleInfo.dx || 0, titleInfo.dy || 0);
     ctx.drawImage(imgElement, imagePos.dx, imagePos.dy);
 
     const filePath = await changeCanvasToImageFileAndSaveToTempFilePath({
         canvas: (context as Canvas),
         width,
         height,
         fileType: 'png',
         destWidth: width,
         destHeight: height,
    });
     return filePath;
 };

  • 具体使用

 export const saveQrCodeImageWithTitle = async () => {
     const url = await qrCodeImage(enterAiyongShopUrl(), 160);
     const imgUrl: string = await generateQrCodeWithTitle({
         title: 'adsionli菜鸡前端',
         titleInfo: {
             dx: 95,
             dy: 20,
             font: '600 14px PingFang SC',
             color: 'black',
        },
         imageUrl: url,
         imagePos: {
             dx: 15,
             dy: 34,
        },
         width: 190,
         height: 204,
    });
     await downloadImage([imgUrl]);
 }


上面三块内容就可以组成我们的海报生成了,这里面的主要步骤不是很难,包括了几个方面:


  1. 用户授权鉴定,主要是是否允许保存,这里做了一点处理,就是可以在用户第一次授权不允许时,进行二次授权调起,这个可以看一下上面的downloadImage这个函数,以及用于判断用户是否授权的checkHasAuthorizedSaveImagePermissions这个函数
  2. 创建OffscreenCanvas并进行绘制,这里其实没有太多的难点,主要就是需要知道,如果我们使用image的内容的话,或者是一个图片的url时,我们需要先将其绘制到一个canvas上(这里可以获取imageElement对象,也可以直接使用canvas),这样方便我们后面进行drawImage时进行使用
  3. 图片保存,这里也有一个需要注意的点,如果图片(或二维码)是网络图片的话,我们需要处理以下,先将其转成本地图片,也就是通过netImageToLocal这个方法,然后再还给对应的将图片画在canvas上的方法。最后的保存很简单,我们可以直接使用Taro.canvasToTempFilePath这个方法转到临时地址,再通过downloadImage就可以搞定了。

感觉好像很麻烦,其实就四步:图片加载转化—>canvas绘制—>用户鉴权—>图片保存。


安卓端实现起来还是很简单的,但是这些方法对于ios端就出现了问题,如果按照上面的路线进行海报绘制保存的话,在ios端就会报一个错误(在本地开发的时候并不会抛出): canvasToTempFilePath:fail invalid viewId


这一步错误就是发生在Taro.canvasToTempFilePath这里,保存到临时文件时会触发,然后这一切的原因就是使用了OffscreenCanvas离屏canvas造成的。


所以为了能够兼容ios端的这个问题,有了以下的修改:


首先需要在我们要下载海报的pages中,添加一个Canvas,帮助我们可以获取CanvasElement

 <Canvas
     type='2d'
     id='qrCodeOut'
     className='aiyong-shop__qrCode'
 />

这里需要注意一下,我们需要添加一个type='2d'的属性,这是为了能够使用官方提供的获取Canvas2dContext的属性,这样就可以不使用createCanvasContext这个方法来获取了(毕竟已经被官方停止维护了)。


然后我们就可以获取一下CanvasElement对象了

 /**
  * @description 获取canvas标签对象
  */
 export const getCanvasElement = (canvasId: string): Promise<Taro.NodesRef> => {
     return new Promise(resolve => {
         const canvasSelect: Taro.NodesRef = selectQuery().select(`#${canvasId}`);
         canvasSelect.node().exec((res: Taro.NodesRef) => {
             resolve(res);
        });
    });
 };

注:这里又有一个小坑,我们在获取CanvasElement之后,如果直接进行绘制的话,这里存在一个问题,就是这个CanvasElementwidth:300、height:150被限制死了,所以我们需要自己在拿到CanvasElement之后,在设置一下width、height

 const canvasNodeRef = await getCanvasElement(canvas);
 let context;
 if (canvasNodeRef && canvasNodeRef[0].node !== null) {
     context = canvasNodeRef[0].node;
    (context as Taro.Canvas).width = width;
    (context as Taro.Canvas).height = height;
 }

好了,改造完成,这样就可以兼容ios端的内容了,实际我们只需要修改generateQrCodeWithTitle这个方法和page新增Canvas用于获取CanvasElement就可以了,其他可以不要动。修改后的generateQrCodeWithTitle方法如下:

 /**
  * @description 获取二维码图像并设置标题
  */
 export const generateQrCodeWithTitle = async (option: SettingOptions): Promise<string> => {
     const {
         title,
         titleInfo,
         imageUrl,
         imagePos,
         width,
         height,
         qrCodeSize,
         canvas,
    } = option;
     const canvasNodeRef = await getCanvasElement(canvas);
     let context;
     if (canvasNodeRef && canvasNodeRef[0].node !== null) {
         context = canvasNodeRef[0].node;
        (context as Taro.Canvas).width = width;
        (context as Taro.Canvas).height = height;
    }
     const ctx = (context.getContext('2d') as CanvasRenderingContext2D);
     const imgElement: Taro.Image = await loadImageByUrlToCanvasImageData(imageUrl, qrCodeSize.width, qrCodeSize.height);
     ctx.fillStyle = 'white';
     ctx.fillRect(0, 0, width, height);
     ctx.fillStyle = titleInfo.color || 'black';
     ctx.font = titleInfo.font || '';
     ctx.textAlign = 'center';
     ctx.fillText(title, titleInfo.dx || 0, titleInfo.dy || 0);
     ctx.drawImage((imgElement as HTMLImageElement), imagePos.dx, imagePos.dy, imgElement.width, qrCodeSize.height);
 
     const filePath = await changeCanvasToImageFileAndSaveToTempFilePath({
         canvas: (context as Canvas),
         width,
         height,
         fileType: 'png',
         destWidth: width,
         destHeight: height,
    });
     return filePath;
 };


如果大家不想让海报被人看到,那可以设置一下css

 .qrCode {
     position: fixed;
     left: 100%;
 }

这样就可以啦



突然发现内容可能有点多了,所以打算分成两篇进行Taro使用过程中的总结,开发完之后进行总结,总是可以让自己回顾在开发过程中遇到的问题的进一步进行思考,这是一个很好的进步过程,加油加油!!!


作者:Adsionli
链接:https://juejin.cn/post/7244452419458498616
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

chrome 搞事,下个月全面删除 Event.path

背景 前两天下午,测试同学反馈生产环境有个功能出现异常,然后给我发了张报错截图。  随后在测试同学的电脑复现并排查问题,通过异常断点,捕捉到异常信息如下:  可以看到异常的原因是 n.path 的值为 undefined,因此 n.path...
继续阅读 »

背景


前两天下午,测试同学反馈生产环境有个功能出现异常,然后给我发了张报错截图。 



随后在测试同学的电脑复现并排查问题,通过异常断点,捕捉到异常信息如下: 



可以看到异常的原因是 n.path 的值为 undefined,因此 n.path.find 等价于 undefined.find,因此程序报错。其中 n 是一个 Event 实例;path 是事件冒泡经过的节点组成的数组。n.path 有值的情况如下: 


Event.path 不是标准属性,常见于 chrome 浏览器,取不到值出现异常不足为奇,只要做个兼容就搞定了,当 Event.path 取不到值时就取 Event.composedPath()。那这是兼容性问题吗,事情好像没有这么简单。仔细对比上述两张截图可以发现,异常时 Event 实例甚至不存在 path 属性,这跟属性存在但值为空是两码事。


进一步排查


好好的 path 属性怎么就不翼而飞了,这是个神奇的问题。当我用自己电脑尝试复现问题时,发现功能正常且无法复现,事情变得更加神奇。两边浏览器都升到了最新版 chrome 108,区别是系统不同,一个是 windows 一个是 macOS。也就是 说同样的代码在同样的软件上跑出了不同结果,这说明可能不是代码或兼容性问题。


为找出真正的原因,我做了几组对照实验,以排除代码、硬件、操作系统和浏览器的影响。情况如下 :


分析这些结果,出现了更有意思的事:只有一种情况会出现异常,使用测试同学的电脑且浏览器是 chrome 108;当改变电脑、系统、浏览器、浏览器版本等因素时结果都是正常。 也就是说导致异常的因素居然不是单一的,而是多个因素组合(测试同学电脑+chrome+108 版本)产生的结果。



chromium issue 的助攻


从上面的结果看好像没办法再继续排查下去,不过从经验判断,多半是 chrome 又在搞事,这时候可以去 chromium issue 里找找蛛丝马迹,经过一番搜索找到了这条 issue: Issue 1277431: Remove event.path。 



issue 标题很直白,Event.path 将被删除。 从 issue 内容可以看到,这次搞事是从 2021 年 12 月 7 日开始,起因是 chromium 开发团队认为 Event.path 属于非标准 API,会导致 Firefox 等其他浏览器的兼容性问题,于是他们决定将其删除。目前这个变更在 chrome 108 属于灰度阶段,在即将发布的 chrome 109 上会全面应用,webview 则是从 109 版本开始逐步禁用。


变更详情和计划


另外 issue 中提到这个变更会在 console 中进行告警。 



console 中确实有这个告警,不过藏在 console 面板的右上角,不太容易发现,而且需要调用 Event.path 后才会显示。点进去之后会跳转到 Issues 面板并显示详细信息。
 



从图中可以看到这个变更属于 Breaking Change,即破坏性变更。另外可以看到变更详情链接版本计划链接。打开变更详情链接可以看到详细的说明、目的、状态、开发阶段等信息。 



打开版本计划链接可以看到,chrome 108 已经在 2022-11-29 正式发布(Stable Release Tue, Nov 29, 2022),chrome 109 将在 2023-01-10 正式发布(Stable Release Tue, Jan 10, 2023)。 



验证


由于英文水平有限,为了避免个人理解存在歧义,使用 chrome 的前瞻版本进行测试,以验证 chrome 108 之后的版本是否真的会应用这个变更。


  • 测试使用的系统为 macOS,浏览器版本包括:chrome-stable(108.0.5359.124)、chrome-beta(109.0.5414.36)、chrome-dev(110.0.5464.2)、chrome-canary(110.0.5477.0)。



  • 测试代码如下
<!DOCTYPE html>
<html lang="en">
<head></head>
<body>
<script>
function test() {
console.log("event.path is:", window.event.path);
}
</script>
<h1 onclick="test()">click me</h1>
</body>
</html>

  • 测试结果如下

chrome-stable(108.0.5359.124)在 macOS 下 Event.path 有值,结合上文的对照实验中 windows10 下一个有值一个为空。说明 chrome 108 中该变更属于灰度阶段。
image.png


chrome-beta(109.0.5414.36)、chrome-dev(110.0.5464.2)、chrome-canary(110.0.5477.0)在 macOS 下 Event.path 都为空,说明 chrome 109 之后全面删除了 Event.path 属性。
image.png


解决方案


先看影响范围,从项目维度来看,所有前端项目都可能受到影响;从代码维度来看,项目源码和第三方依赖都可能受影响。在 github 中搜索发现 swipperopenlayers 等第三方库中都有相关 issue。因此解决方案需要全面考虑:最好对所有项目都进行排查修复,另外不仅要排查源码,还要考虑第三方库。


根据官方建议及综合考虑,推荐在前端项目中统一添加如下 polyfill 代码:

  Object.defineProperty(Event.prototype, "path", {
get() {
return this.composedPath();
},
});

最后


chrome 109 预计在 2023-01-10 正式发布,届时会全面禁用 Event.path,所有源码中使用该属性或第三方库使用该属性的前端项目都可能会出现异常,还有 20 几天时间,建议尽快排查修复。


一些经验

  • 关注 devtools 中的 console、issue 等各种告警信息,有助于调试和排查问题、以及发现潜在的问题
    • 关注 chorme 迭代计划,有条件可以做前瞻性测试,预防未来可能发生的异常



    作者:Whilconn
    链接:https://juejin.cn/post/7177645078146449466
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    如何在上班时间利用终端控制台摸鱼🧐🧐🧐

    web
    作为一个资深的摸鱼小能手,班我们每天要上,终端也是我们也要每天要用到的,那么有什么办法可以在控制台终端中去摸鱼呢,那么在接下来的文章中我们就来看看它可以做到怎么样摸鱼。 简介 在我们开发的项目中,几乎有很多项目要都是使用 webpack 作为构建工具来进行开发...
    继续阅读 »

    作为一个资深的摸鱼小能手,班我们每天要上,终端也是我们也要每天要用到的,那么有什么办法可以在控制台终端中去摸鱼呢,那么在接下来的文章中我们就来看看它可以做到怎么样摸鱼。


    简介


    在我们开发的项目中,几乎有很多项目要都是使用 webpack 作为构建工具来进行开发的,在它进行构建的时候,会有一些信息会输出在控制台上面,如下图所示:


    20230910150719


    爱瞎折腾的朋友们可能就会想了,为什么 create-react-pp 也是用的 webpack 作为构建工具,为什么我的输出和它的输出是不一样的呢?


    20230910150945


    compiler


    通过查阅文档,我发现了问题所在,原来在 webpack 中它提供了一个 compiler 钩子,它用来监控文件系统的 监听(watching) 机制,并且在文件修改时重新编译。 当处于监听模式(watch mode)时, compiler 会触发诸如 watchRun, watchClose 和 invalid 等额外的事件。


    done 钩子就是当我们的代码被编译完成的时候被调用的。


    如何调用 done 钩子


    要想调用我们的 done 钩子,首先我们要引入 webpack 包,并把 webpack 配置传递给 webpack 函数,如下图所示:


    20230910151624


    接下来我们看看终端输出:


    20230910151749


    这些就是我们的一些 webpack 配置,在这个 compiler 对象上,它存在一个 hooks 对象,如下代码所示:


    compiler.hooks.done.tap("done", async (stats) => {
    console.log(11111111111111);
    });

    它会在代码编译完成阶段调用该回调函数:


    20230910152621


    咦,你会发现了,代码编译执行完成,我的终端上的输出会这么干净,是因为在输出控制台之前, 已经被我调用了一个函数清空了。


    通过这个函数,你可以情况控制台上的一些输出信息,如下代码所示:


    function clearConsole() {
    process.stdout.write(
    process.platform === "win32" ? "\x1B[2J\x1B[0f" : "\x1B[2J\x1B[3J\x1B[H"
    );
    }

    再调用以下,你会发现控制台上面很干净的,图下图所示:


    20230910153357


    要想这一些个性化的输出,我们直接在这个回调函数中打印输出就可以了,如果你要你输出的信息和项目中的信息有关,你可以利用 stats 这个参数:


    20230910160905


    大概就这样子,如果你想更好玩的话,你可以使用一些网络请求库,去获取一些网络资源:


    20230910161247


    去获取这些资源都是可以的呀。


    总结


    如果你的项目是使用的 webpack,并且要想在项目的开发中自定义,你可以通过 compiler.hooks 的方式去监听不同的钩子,然后通过不同的方式来实现不同的信息输出。


    源代码地址


    作者:Moment
    来源:juejin.cn/post/7277065056575848448
    收起阅读 »

    发送验证码后的节流倒计时丨刷新 & 重新进入页面,还原倒计时状态

    web
    前言   最近在做一个 H5 工具,需要手机号 + 验证码登录,很自然地,点击发送验证码后需要等待一段时间才能重新发送,用于请求节流,避免用户疯狂点击:     不过这里其实有个隐藏需求——如果仍然在冷却时间内,那么用户无论是刷新或是关闭页面,再次打开登...
    继续阅读 »

    前言


      最近在做一个 H5 工具,需要手机号 + 验证码登录,很自然地,点击发送验证码后需要等待一段时间才能重新发送,用于请求节流,避免用户疯狂点击:





     

      不过这里其实有个隐藏需求——如果仍然在冷却时间内,那么用户无论是刷新或是关闭页面,再次打开登录弹窗,需要直接展示正确的倒计时状态


    解决方案



    使用经典的 localStorage




    1. 发送验证码时,将发送时间 (lastSendingTime) 存入 localStorage,并开启 60 秒倒计时。

    2. 倒计时结束后,清除 localStorage 中的 lastSendingTime

    3. 重新进入页面时,若 localStorage 中存有 lastSendingTime,则说明仍处于冷却时间内,那么计算出剩余的倒计时 N,并开启 N 秒倒计时。


    Talk is cheap, show me the code!


      const [countdown, setCountdown] = useState(60) // 倒计时
    const [canSendCode, setCanSendCode] = useState(true) // 控制按钮文案的状态
    const [timer, setTimer] = useState() // 定时器 ID

    async function sendVerificationCode() {
    try {
    // network request...
    Toast.show({ content: '验证码发送成功' })
    startCountdown()
    setCanSendCode(false)
    } catch (error) {
    setCountdown(0)
    setCanSendCode(true)
    }
    }

    function startCountdown() {
    const nowTime = new Date().getTime()
    const lastSendingTime = localStorage.getItem('lastSendingTime')
    if (lastSendingTime) {
    // 若 localStorage 中存有 lastSendingTime,则说明仍处于冷却时间内,计算出剩余的 countdown
    const restCountdown = 60 - parseInt(((nowTime - lastSendingTime) / 1000), 10)
    setCountdown(restCountdown <= 0 ? 0 : restCountdown)
    } else {
    // 否则说明冷却时间已结束,则 countdown 为 60s,并将发送时间存入 localStorage
    setCountdown(60)
    localStorage.setItem('lastSendingTime', nowTime)
    }

    setTimer(
    setInterval(() => {
    setCountdown(old => old - 1)
    }, 1000),
    )
    }

    // 重新进入页面时,若 localStorage 中存有上次的发送时间,则说明还处于冷却时间内,则调用函数计算剩余倒计时;
    // 否则什么也不做
    useEffect(() => {
    const lastSendingTime = localStorage.getItem('lastSendingTime')
    if (lastSendingTime) {
    setCanSendCode(false)
    startCountdown()
    }

    return () => {
    clearInterval(timer)
    }
    }, [])


    // 监听倒计时,倒计时结束时:
    // * 清空 localStorage 中存储的上次发送时间
    // * 清除定时器
    // * 重置倒计时
    useEffect(() => {
    if (countdown <= 0) {
    setCanSendCode(true)
    localStorage.removeItem('lastSendingTime')
    clearInterval(timer)
    setCountdown(60)
    }
    }, [countdown])

    return (
    {canSendCode ? (
    <span onClick={sendVerificationCode}>
    获取验证码
    </span>

    ) : (
    <span>
    获取验证码({`${countdown}`})
    </span>

    )}
    )

    最终效果





    总结


      一开始感觉这是个很简单的小需求,可能 20min 就写完了,但实际花了两个多小时才把逻辑全部 cover 到,还是不能太自信啊~


    作者:Victor_Ye
    来源:juejin.cn/post/7277187894872014848
    收起阅读 »

    别再用 display: contents 了

    web
    文章讨论了在网站上使用"display: contents"属性可能导致的潜在问题。作者强调了这种做法可能破坏网页的语义结构,并可能对可访问性产生不利影响。文章还提到了一些潜在的解决方案,以帮助开发人员避免这些问题。 下面是正文~~ display: cont...
    继续阅读 »

    文章讨论了在网站上使用"display: contents"属性可能导致的潜在问题。作者强调了这种做法可能破坏网页的语义结构,并可能对可访问性产生不利影响。文章还提到了一些潜在的解决方案,以帮助开发人员避免这些问题。


    下面是正文~~


    display: contents 介绍


    CSS(层叠样式表)中的 display: contents 是一个相对较新的属性值,它对元素的布局和可视化有特殊的影响。当你对一个元素应用 display: contents,这个元素本身就像从DOM(文档对象模型)中消失了一样,而它的所有子元素则会升级到DOM结构中的下一个层级。换句话说,该元素的盒模型将被忽略,它的子元素会取而代之,就像直接插入到父元素中一样。


    假设我们有这样一个HTML结构:


    id="parent">
    id="child1">Child 1
    id="child2">Child 2

    正常情况下,#parent#child1#child2 的父元素,它们在DOM和布局中有一个明确的层级关系。


    现在,如果我们对 #parent 应用 display: contents


    #parent {
    display: contents;
    }

    在这种情况下,#parent 在页面布局中就像是“消失了”一样。它的所有子元素(这里是 #child1#child2)会直接升级到#parent所在的DOM层级。也就是说,在布局和渲染过程中,#child1#child2 将不再被视为 #parent 的子元素,而是像直接插入到 #parent 的父元素中一样。


    这样做的结果是,任何应用于 #parent 的布局和样式都不会影响到页面的渲染,但 #child1#child2 会像正常元素一样被渲染。


    主要用途:



    1. 语义改进:能够改进HTML结构,使其更符合语义,但不影响布局和样式。

    2. 布局优化:在某些复杂的布局场景中,它可以简化DOM结构,提高渲染性能。


    display: contents 和可访问性的长期问题


    从字面上看,这个CSS声明改变了其应用到的元素的显示属性。它使元素“消失”,将其子元素提升到DOM中的下一层级。


    这种声明在很多方面都可能是有用的。讽刺的是,其中一个用例就是改善你工作的底层语义。然而,这个声明一开始的效果有点过头了。


    CSS和可访问性


    不是每个人都意识到这一点,但某些CSS会影响辅助技术的工作方式。就像烧毁你的房子确实会成功地除去其中可能存在的蜘蛛一样,使用 display: contents 可能会完全消除某些元素被辅助技术识别的关键属性。


    简而言之,这会导致按钮不被声明为按钮,表格不被声明和导航为表格,列表也是如此,等等。


    换句话说:当人们说“HTML默认是可访问的”时,display: contents 彻底破坏了这个“默认”。这不好。


    可访问性从业者注意到了这个问题,并提出了完全合理的修复要求。特别值得一提的是Adrian Roselli的勤勉、有条理和实事求是的文档和报告工作。


    修复已经完成,浏览器也已经更新,我们得到了一个快乐的结局。对吗?并不是那么简单。


    回归问题


    在软件开发中,回归可能意味着几件事情。这个词通常用于负面语境,表达更新后的行为不小心恢复到以前,不太理想的工作方式。


    对于 display: contents,这意味着每个人的自动或近乎自动更新的浏览器抛弃了非常必要的错误修复,而没有任何警告或通知,就回到了破坏语义HTML与辅助技术交流的基础属性。


    这种类型的回归不是一个令人讨厌的 bug,而是破坏了 Web 可访问性的基础方面。


    Adrian注意到了这一点。如果你继续阅读我给你链接的部分,他继续注意到这一点。总之,我统计了关于 display: contents 的行为以不可访问的方式回归了16次的更新。


    看问题的角度


    制作浏览器是一件困难的事情。需要考虑很多、很多不同的事情,那还没考虑到软件的复杂性。


    可访问性并不是每个人的首要任务。我可以在这里稍微宽容一些,因为我主要是尝试用我拥有的东西工作,而不是我希望能有的东西。我习惯了应对由于这种优先级而产生的所有小问题、陷阱和杂项。


    然而,能够使用Web界面绝非小事。display: contents 的问题对使用它的界面的人们的生活质量有非常真实、非常可量化的影响。


    我还想让你考虑一下这种打地鼠游戏是如何影响可访问性从业者的。告诉某人他们不能使用一个闪亮的新玩具永远不会受到欢迎。然后告诉他们你可以,但后来又不能了,这会削弱信任和能力的认知。


    别用 display: contents


    现在,我不认为我们这个行业可以自信地使用 display: contents。过去的行为是未来行为的良好指标,而走向地狱的道路是由好意铺成的。


    我现在认为这个声明是不可预测的。常见的“只需用辅助技术测试其支持情况”的回应在这里也不适用——当前浏览器版本中该声明的期望行为并不能保证在该浏览器的未来版本中持续。


    这是一件罕见且令人不安的事情——整个现代Web都是建立在这样的假设之上,即这样的事情不会以这种方式停止工作。这不是互操作性问题,而是由于疏忽造成的伤害。


    display: contents 的回归给我们提供了一个小小的窗口,让我们看到浏览器制作的某些方面是如何(或不是如何)被优先考虑和测试的。


    人们可以发誓说像可访问性和包容性这样的事情是重要的,但当涉及到这个特定的CSS声明时,很明显大多数浏览器制造商是不可信的。


    这个声明在实践中的不稳定性代表了一种非常真实、非常严重的风险,即在你无法控制的情况下,可能会在你的网站或Web应用中引入关键的可访问性问题。


    作者:王大冶
    来源:juejin.cn/post/7275973778915573772
    收起阅读 »

    产品:请给我实现一个在web端截屏的功能!

    web
    一、故事的开始 最近产品又开始整活了,本来是毫无压力的一周,可以小摸一下鱼的,但是突然有一天跟我说要做一个在网页端截屏的功能。 作为一个工作多年的前端,早已学会了尽可能避开麻烦的需求,只做增删改查就行! 我立马开始了我的反驳,我的理由是市面上截屏的工具有很多的...
    继续阅读 »

    一、故事的开始


    最近产品又开始整活了,本来是毫无压力的一周,可以小摸一下鱼的,但是突然有一天跟我说要做一个在网页端截屏的功能。


    作为一个工作多年的前端,早已学会了尽可能避开麻烦的需求,只做增删改查就行!


    我立马开始了我的反驳,我的理由是市面上截屏的工具有很多的,微信截图、Snipaste都可以做到的,自己实现的话,一是比较麻烦,而是性能也不会很好,没有必要,把更多的时间放在核心业务更合理!


    结果产品跟我说因为公司内部有个可以用来解析图片,生成文本OCR的算法模型,web端需要支持截取网页中部分然后交给模型去训练,微信以及其他的截图工具虽然可以截图,但需要先保存到本地,再上传给模型才行。


    网页端支持截图后可以在在截屏的同时直接上传给模型,减少中间过程,提升业务效率。


    我一听这产品小嘴巴巴的说的还挺有道理,没有办法,只能接了这个需求,从此命运的齿轮开始转动,开始了我漫长而又曲折的思考。


    二、我的思考


    在实现任何需求的时候,我都会在自己的脑子中大概思考一下,评估一下它的难度如何。我发现web端常见的需求是在一张图片上截图,这个还是比较容易的,只需要准备一个canvas,然后利用canvas的方法 drawImage就可以截取这个图片的某个部分了。


    示例如下:


    <!DOCTYPE html>
    <html>
    <head>
    <title>截取图片部分示例</title>
    </head>
    <body>
    <canvas id="myCanvas" width="400" height="400"></canvas>
    <br>
    <button onclick="cropImage()">截取图片部分</button>
    <br>
    <img id="croppedImage" alt="截取的图片部分">
    <br>

    <script>
    function cropImage() {
    var canvas = document.getElementById('myCanvas');
    var ctx = canvas.getContext('2d');
    var image = new Image();

    image.onload = function () {
    // 在canvas上绘制整张图片
    ctx.drawImage(image, 0, 0, canvas.width, canvas.height);

    // 截取图片的一部分,这里示例截取左上角的100x100像素区域
    var startX = 0;
    var startY = 0;
    var width = 100;
    var height = 100;
    var croppedData = ctx.getImageData(startX, startY, width, height);

    // 创建一个新的canvas用于显示截取的部分
    var croppedCanvas = document.createElement('canvas');
    croppedCanvas.width = width;
    croppedCanvas.height = height;
    var croppedCtx = croppedCanvas.getContext('2d');
    croppedCtx.putImageData(croppedData, 0, 0);

    // 将截取的部分显示在页面上
    var croppedImage = document.getElementById('croppedImage');
    croppedImage.src = croppedCanvas.toDataURL();
    };

    // 设置要加载的图片
    image.src = 'your_image.jpg'; // 替换成你要截取的图片的路径
    }
    </script>
    </body>
    </html>

    一、获取像素的思路


    但是目前的这个需求远不止这样简单,因为它的对象是整个document,需要在整个document上截取一部分,我思考了一下,其实假设如果浏览器为我们提供了一个api,能够获取到某个位置的像素信息就好了,这样我将选定的某个区域的每个像素信息获取到,然后在一个像素一个像素绘制到canvas上就好了。


    我本以为我发现了一个很好的方法,可遗憾的是经过调研浏览器并没有为我们提供类似获取某个位置像素信息的API。


    唯一为我们提供获取像素信息的是canvas的这个API。


    <!DOCTYPE html>
    <html>
    <head>
    <title>获取特定像素信息示例</title>
    </head>
    <body>
    <canvas id="myCanvas" width="400" height="400"></canvas>
    <br>
    <button onclick="getPixelInfo()">获取特定像素信息</button>
    <br>
    <div id="pixelInfo"></div>

    <script>
    function getPixelInfo() {
    var canvas = document.getElementById('myCanvas');
    var ctx = canvas.getContext('2d');

    // 绘制一些内容到canvas
    ctx.fillStyle = 'red';
    ctx.fillRect(50, 50, 100, 100);

    // 获取特定位置的像素信息
    var x = 75; // 替换为你想要获取的像素的x坐标
    var y = 75; // 替换为你想要获取的像素的y坐标
    var pixelData = ctx.getImageData(x, y, 1, 1).data;

    // 提取像素的颜色信息
    var red = pixelData[0];
    var green = pixelData[1];
    var blue = pixelData[2];
    var alpha = pixelData[3];

    // 将信息显示在页面上
    var pixelInfo = document.getElementById('pixelInfo');
    pixelInfo.innerHTML = '在位置 (' + x + ', ' + y + ') 的像素信息:<br>';
    pixelInfo.innerHTML += '红色 (R): ' + red + '<br>';
    pixelInfo.innerHTML += '绿色 (G): ' + green + '<br>';
    pixelInfo.innerHTML += '蓝色 (B): ' + blue + '<br>';
    pixelInfo.innerHTML += 'Alpha (透明度): ' + alpha + '<br>';
    }
    </script>
    </body>
    </html>


    浏览器之所以没有为我们提供相应的API获取像素信息,停下来想想也是有道理的,甚至是必要的,因为假设浏览器为我们提供了这个API,那么恶意程序就可以通过这个API,不断的获取你的浏览器页面像素信息,然后全部绘制出来。一旦你的浏览器运行这个段恶意程序,那么你在浏览器干的什么,它会一览无余,相当于在网络的世界里裸奔,毫无隐私可言。


    二、把DOM图片化


    既然不能走捷径直接拿取像素信息,那就得老老实实的把document转换为图片,然后调用canvas的drawImage这个方法来截取图片了。


    在前端领域其实99%的业务场景早已被之前的大佬们都实现过了,相应的轮子也很多。我问了一下chatGPT,它立马给我推荐了大名鼎鼎的html2canvas,这个库能够很好的将任意的dom转化为canvas。这个是它的官网。


    我会心一笑,因为这不就直接能够实现需求了,很容易就可以写出下面的代码了:


    html2canvas(document.body).then(function(canvas) {
    // 将 Canvas 转换为图片数据URL
    var src = canvas.toDataURL("image/png");
    var image = new Image();
    image.src = src;
    image.onload = ()=>{
    const canvas = document.createElement("canvas")
    const ctx = canvas.getContext("2d");
    const width = 100;
    const height = 100;
    canvas.width = width;
    canvas.height = height;
    // 截取以(10,10)为顶点,长为100,宽为100的区域
    ctx.drawImage(image, 10, 10, width, height , 0 , 0 ,width , height);
    }
    });


    上面这段代码就可以实现截取document的特定的某个区域,需求已经实现了,但是我看了一下这个html2canvas库的资源发现并没有那么简单,有两个点并不满足我希望实现的点:


    1.大小


    当我们将html2canvas引入我们的项目的时候,即便压缩过后,它的资源也有近200kb:


    Screen Shot 2023-09-09 at 3.15.10 PM.png


    要知道整个react和react-dom的包压缩过后也才不到150kb,因此在项目只为了一个单一的功能引入一个复杂的资源可能并不划算,引入一个复杂度高的包一个是它会增加构建的时间,另一方面也会增加打包之后的体积。


    如果是普通的web工程可能情有可原,但是因为我会将这需求做到插件当中,插件和普通的web不一样的一点,就是web工程如果更新之后,客户端是自动更新的。但是插件如果更新了,需要客户端手动的下载插件包,然后再在浏览器安装,因此包的大小尽可能小才好,如果一个插件好几十MB的话,那客户端肯定烦死了。


    2.性能


    作为业内知名的html2canvas库,性能方面表现如何呢?


    我们可以看看它的原理,一个dom结构是如何变成一个canvas的呢!


    它的源码在这里:核心的实现是canvas-renderer.ts这个文件。


    当html2canvas拿到dom结构之后,首先为了避免副作用给原dom造成了影响,它会克隆一份全新的dom,然后遍历DOM的每一个节点,将其扁平化,这个过程中会收集每个节点的样式信息,尤其是在界面上的布局的几何信息,存入一个栈中。


    然后再遍历栈中的每一个节点进行绘制,根据之前收集的样式信息进行绘制,就这样一点点的绘制到提前准备的和传入dom同样大小的canvas当中,由于针对很多特殊的元素,都需要处理它的绘制逻辑,比如iframe、input、img、svg等等。所以整个代码就比较多,自然大小就比较大了。


    整个过程其实需要至少3次对整个dom树的遍历才可以绘制出来一个canvas的实例。


    这个就是这个绘制类的主要实现方法:


    Screen Shot 2023-09-09 at 4.08.30 PM.png


    可以看到,它需要考虑的因素确实特别多,类似写这个浏览器的绘制引擎一样,特别复杂。


    要想解决以上的大小的瓶颈。


    第一个方案就是可以将这个资源动态加载,但是一旦动态加载就不能够在离线的环境下使用,在产品层面是不能接受的,因为大家可以想一想如果微信截图的功能在没有网络的时候就使用不了,这个肯定不正常,一般具备工具属性的功能应该尽可能可以做到离线使用,这样才好。


    因此相关的代码资源不能够动态加载。


    二、dom-to-image


    正当我不知道如何解决的时候,我发现另外了一个库dom-to-image,我发现它打包后的大小只有10kb左右,这其实已经一个很可以接受的体积了。这个是它的github主页。好奇的我想知道它是怎么做到只有这么小的体积就能够实现和html2canvas几乎同样的功能的呢?于是我就研究了一下它的实现。


    dom-to-image的实现利用了一个非常灵活的特性--image可以渲染svg


    我们可以复习一下img标签的src可以接受什么样的类型:这里是mdn的说明文档


    可以接受的格式要求是:



    如果我们使用svg格式来渲染图片就可以是这样的方式:


    <!DOCTYPE html>
    <html>
    <head>
    <title>渲染SVG</title>
    </head>
    <body>
    <h1>SVG示例</h1>
    <img src="example.svg" alt="SVG示例">
    </body>
    </html>


    但是也可以是这样的方式:


    <!DOCTYPE html>
    <html>
    <head>
    <title>渲染SVG字符串</title>
    </head>
    <body>
    <div id="svg-container">
    <!-- 这里是将SVG内容渲染到<img>标签中 -->
    <img id="svg-image" src="data:image/svg+xml, <svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><circle cx='50' cy='50' r='40' stroke='black' stroke-width='2' fill='red' /></svg>" alt="SVG图像">
    </div>
    </body>
    </html>


    把svg的标签序列化之后直接放在src属性上,image也是可以成功解析的,只不过我们需要添加一个头部:data:image/svg+xml,


    令人兴奋的是,svg并不是只支持svg语法,也支持将其他的xml类型的语法比如html嵌入在其中。antv的x6组件中有非常多这样的应用例子,我给大家截图看一下:


    Screen Shot 2023-09-09 at 4.49.40 PM.png


    在svg中可以通过foreignObject这个标签来嵌套一些其他的xml语法,比如html等,有了这一特性,我们就可以把上面的例子改造一下:


    <!DOCTYPE html>
    <html>
    <head>
    <title>渲染SVG字符串</title>
    </head>
    <body>
    <div id="svg-container">
    <!-- 这里是将SVG内容渲染到<img>标签中 -->
    <img
    id="svg-image"
    src="data:image/svg+xml, <svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><circle cx='50' cy='50' r='40' stroke='black' stroke-width='2' fill='red' /><foreignObject>{ 中间可以放 dom序列化后的结果呀 }</foreignObject></svg>"
    alt="SVG图像"
    >

    </div>
    </body>
    </html>


    所以我们可以将dom序列化后的结构插到svg中,这不就天然的形成了一种dom->image的效果么?下面是演示的效果:


    <!DOCTYPE html>
    <html>
    <head>
    <title>渲染SVG字符串</title>
    </head>
    <body>
    <div id="render" style="width: 100px; height: 100px; background: red"></div>
    <br />
    <div id="svg-container">
    <!-- 这里是将SVG内容渲染到<img>标签中 -->
    <img id="svg-image" alt="SVG图像" />
    </div>

    <script>
    const perfix =
    "data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><foreignObject x='0' y='0' width='100%' height='100%'>";
    const surfix = "</foreignObject></svg>";

    const render = document.getElementById("render");

    render.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");

    const string = new XMLSerializer()
    .serializeToString(render)
    .replace(/#/g, "%23")
    .replace(/\n/g, "%0A");

    const image = document.getElementById("svg-image");

    const src = perfix + string + surfix;

    console.log(src);

    image.src = src;
    </script>
    </body>
    </html>


    Screen Shot 2023-09-09 at 5.18.12 PM.png


    如果你将这个字符串直接通过浏览器打开,也是可以的,说明浏览器可以直接识别这种形式的媒体资源正确解析对应的资源:


    data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><foreignObject x='0' y='0' width='100%' height='100%'><div id="render" style="width: 100px; height: 100px; background: red" xmlns="http://www.w3.org/1999/xhtml"></div></foreignObject></svg>

    实不相瞒这个就是dom-to-image的核心原理,性能肯定是不错的,因为它是调用浏览器底层的渲染器。


    通过这个dom-to-image我们可以很好的解决资源大小性能这两个瓶颈的点。


    三、优化


    这个库打包后的产物是umd规范的,并且是统一暴露出来的全局变量,因此不支持treeshaking。


    Screen Shot 2023-09-09 at 9.13.08 PM.png


    但是很多方法比如toJpeg、toBlob、等方法我们其实都用不到,所以打包了很多我们不需要的产物,于是其实我们可以把核心的实现自己写一遍,使用1-2kb的空间就可以做到这一点。


    经过以上的思考我们就可以基本上确定方案了:


    基于dom-to-image的原理,实现一个简易的my-dom-to-image,大约只需要100行代码左右就可以做到。


    然后将document.body转化为image,再从这个image中截取特定的部分。


    Screen Shot 2023-09-09 at 9.23.06 PM.png


    好了,以上就是我关于这个需求的一些思考,如果掘友也有一些其他非常有意思的需求,欢迎评论区讨论我们一起头脑风暴啊!!!


    四、最后的话


    以下是我的其他文章,欢迎掘友们阅读哦!


    保姆级讲解JS精度丢失问题(图文结合)


    shell、bash、zsh、powershell、gitbash、cmd这些到底都是啥?


    从0到1开发一个浏览器插件(通俗易懂)


    用零碎时间个人建站(200+赞)


    另外我有一个自己的网站,欢迎来看看 new-story.cn


    创作不易,如果您觉得文章有任何帮助到您的地方,或者触碰到了自己的知识盲区,请帮我点赞收藏一下,或者关注我,我会产出更多高质量文章,最后感谢您的阅读,祝愿大家越来越好。


    作者:Story
    来源:juejin.cn/post/7276694924137463842
    收起阅读 »

    谈谈干前端四年的几点感受

    19年毕业的我,最开始怀揣着无限憧憬进入这个行业 不知不觉,已经工作4年了,如果算上大四实习的时光,也接近5年了。 4年间换了两家公司。 对于工作,我也有过很多的困扰和迷茫,现在依旧在走一步看一步的状态。 或许一觉起来工作没了,都是概率事件。 为什么会有这篇文...
    继续阅读 »

    19年毕业的我,最开始怀揣着无限憧憬进入这个行业


    不知不觉,已经工作4年了,如果算上大四实习的时光,也接近5年了。


    4年间换了两家公司。


    对于工作,我也有过很多的困扰和迷茫,现在依旧在走一步看一步的状态。


    或许一觉起来工作没了,都是概率事件。


    为什么会有这篇文章?


    一是与行业内大佬山月交流了一次,解惑答疑,有所感悟,想记录下心中所想;


    二是呼应一年前的文章《谈谈干前端三年的几点感受》,对比看看自己想法变化。




    前端在一个公司定位


    一年前我说,前端在公司的定位是必要不重要,现在的想法依旧不变,只是对象变了。


    整个技术开发人员,在一个公司的定位都是处于必要但不重要的角色,可替代性非常高。


    可替代的属性越高,价值属性便越低。


    也许局部或者短期看,技术开发的薪资是高的,但这对于公司来说,是成本。


    如果公司要降本增效,最先压榨的也是这部分人员。


    有这样一种说法,“技术傍身,编程改变世界”等等,其实是有些误导人的。


    重要的从来都是想法,是渠道,不是技术。


    只要能想到,大概率都能实现,实现不了就加班想办法实现。


    能够提出想法的人,才处于一个公司重要的地位。


    就正如我看到的,一个公司,核心业务人员离职,公司上下极力挽留,一个开发离职,领导回复祝好。




    我们要学些什么


    学有价值的东西。


    何谓价值,价值就是经过时间考验,依旧不变的东西。


    我个人极其反对花太多精力深入研究各个技术的源码。


    技术说到底是工具,工具最重要的使用,不是本身,而且只要是工具,便都有替代品,


    有新的技术,又会有新的源码,学是学不完的。


    而计算机行业,不变的什么,有价值的是什么?


    是计算机网络,是计算机组成原理,是数据结构与网络,是操作系统,是信息安全,是项目管理,是软件测试。


    以上都是大学中计算机类的专业课程,这些年没有变过。


    具体一点,与前端不变的什么,有价值的是什么?


    是网络请求,是nginx,是性能优化,是前端工程化,是脚手架,是对UI的基本审美。


    当然了,如果做可视化,音视频,跨端方向等等也有属于自己的专业壁垒。


    以上我提到的都属于目前自己看到,前端通用知识。




    如何评价自己的薪资和技术水平


    其实,我们学到的99%的知识是无用的,或者学完不用就忘记了。


    我学习的目的很单纯,就是为了跳槽涨薪。


    让自己的实力能够匹配和市场对我的要求和我自己期望的薪资。


    那么,如何得知自己的薪资水平,是否符合年限,技术水平,是否符合市场要求?


    需要比较。


    我们常说,人要和自己比,不要和别人比,人别人,气死人。但其实这是句自我安慰的鸡汤,不是生活的真相。


    人得自知,不比较,如何自知呢?


    当然,这种事情很难和同事交流,也不建议问同行。


    问同事,同事水平参差不齐,给不了你准确的答案。


    问同行,同行也许自己也发展不顺,多半是同病相怜,或是能给你方向,但给不了方案。


    我的建议是做付费咨询,向行业内大佬求助。


    同行业过来人的经验,更靠谱一些,做不到感同身受,但能明白心中所想。


    我想看到这篇文章的各位,都关注过几位技术大佬,那就去主动搭讪,说明来意,付费咨询。


    “我的薪资目前和我的工作年限匹配吗?我的技术还应该补充哪部分?我应该如何学习某些知识,有没有推荐的学习路线和文章,等等之类的问题”


    而付费是获取能够心安理得的咨询,不要计较那一顿火锅的钱。


    但其实只要搭讪成功,大佬一般都不会收费。




    2023年了,还要不要往大厂努力?


    当然要啊,这个想法和一年前比,没有动摇。


    但是大厂今年都在降本增效,门槛更高了,面试更难了,工作更卷了。


    但这并不能成为放弃的借口。无论结果与否,人总得有个工作上的目标啊。


    正如我的前同事今年初送给我的一句话,


    “备考公务员或者向大厂努力,总得找个目标,找件事情去做吧。如果觉得大厂太卷,那就干一年就走,但这个经历会成为你永久的财富。”




    前端已死?


    今年上半年受chatGPT冲击,这个言论甚嚣尘上。


    我这里不讨论死不死的事情,只觉得这个问题很荒谬,多思考思考就会明白这句话,在创造概念,制造焦虑。


    仔细想想,这波言论最大的受益方还是 做职业教育的那帮人。




    拿多少钱干多少活还是干多少活拿多少钱?


    第一家公司一切都很好。


    我还是义无反顾的离开了,离开后公司发展得更好了。


    离职的直接原因就是,当时要前端使用uni-app做跨端应用,去替换客户端的工作。


    这项工作的直接影响就是,整个公司只前端部门加班,我疲惫不堪的同时,uni-app踩不完的坑,也身心俱疲。


    每当加班到很晚时,委屈总是涌上心头。


    受不了之时,就只剩一个走字。


    这时候,小兵心态就出现了,拿多少钱多少活,我就拿这点工资,整这么多活,我无法承担,只能摆烂了。


    当然,也有领导心态,你得先努力干,干出成果,我拿着成果才去争取涨薪。


    这中间就有一个认知偏差,双方因为角度不同,无法理解对方的心态和想法。


    领导觉得小兵不懂他的良苦用心,小兵觉得领导天天画饼。


    哪种做法是对的呢?


    得就事论事。


    如果这件事情,对你有成长,有帮助,比如做一些工程化,脚手架,性能优化的工作,肯定得先干出成果。


    如果这件事情,对自己是一种消耗,那还是持小兵心态吧。


    如何区分这件事情是对你的帮助还是对自己的消耗呢?


    其实自己最清楚。


    如果干这项工作时,总是充满期待,充满激情,加班也无怨无悔,那就是帮助。


    如果干这项工作时,总是身心俱疲,牢骚满腹,加班会委屈抱怨,那就是消耗。




    人都是不愿意被管理的


    这句话出自山月,我听后豁然开朗。


    今年听闻行业内很多公司严抓考勤,多了很多制度和会议,吐槽随处可见。


    新的领导,势必会带来新的管理制度,新的实施方案。


    人都是不愿意被管理的,所以会引起各种不适应,但是一般一个月后都会销声匿迹,因为已经适应了。


    无法评价这些变化的好与坏。


    身处其中的我们只有慢慢适应,打工到哪里都一样,只要被管理着,都需要面临不同的问题。




    最后


    其实,回顾毕业这些年,19年谣传资本寒冬,然后是防疫三年,到后来前端已死,到现在无法言状的行业颓势。


    正应了那句话,“今年是过去十年最差的一年,却可能是未来十年最好的一年。”


    然后呢,这句话想表达什么?仅仅是传播了一个情绪,放在近些年都受用。


    我想说,


    “大环境的整体劣势,不影响个人的局部优势,我们要想办法建立这种个人优势;”


    “种一棵树最好的时间是十年前,其次是现在。”


    作者:虎妞先生
    来源:juejin.cn/post/7258509816691834917
    收起阅读 »

    突然发现,前端好像没几个做到 CTO 的……

    web
    大家好,我 ssh,这几天,在推上看到了一个节奏,swyx 小哥发了一篇关于前端天花板的讨论,吸引了 150w 左右的阅读。主要是在讲前端天花板,前端人员被集中捆绑在低级别工程师这个行列中,通往 VP(技术副总裁) 或者 CTO 的大门是朝他们关上的。 而他...
    继续阅读 »

    大家好,我 ssh,这几天,在推上看到了一个节奏,swyx 小哥发了一篇关于前端天花板的讨论,吸引了 150w 左右的阅读。主要是在讲前端天花板,前端人员被集中捆绑在低级别工程师这个行列中,通往 VP(技术副总裁) 或者 CTO 的大门是朝他们关上的。


    微信图片_20230726161930.jpg


    而他发出这篇推文的起因,正是 swyx 正文里配的这篇文章截图:



    经过我查询,这是 honeycomb.io 的一篇博客 成为工程 VP 里的一段话。他们没有刻意贬低前端工程师,只是客观的描述了统计情况而已,这反而是更加令人悲观的。


    其实这个问题我也不止一次想过,尤其是有一些校招的同学特别喜欢思考这个问题,之前一次校招的宣讲会后答疑环节,也有不止一个同学过来问我这个问题。


    确实,仔细想想,国内的前端界比较出名的前端出身做到很高职位的,玉伯算是一个代表,后期他基本上已经成为一个产品设计方面的负责人了,脱离了单纯前端的范畴。主导设计了云凤蝶、语雀这些非常 nb 的产品。


    image.png


    image.png


    但是除了玉伯之外,让我们仔细想想,是不是大概率情况下,前端升到更高级别负责人的概率比后端要低很多呢?第一印象是如此,而且我以前在阿里没有隐藏职位的时候,在钉钉上直接搜索 title 来确认过这个问题。


    在阿里,资深前端专家则对应前端的 p9,资深技术专家对应后端的 p9,这两个职位的人数在我印象里是相差很悬殊的,很多倍的关系…… 而且我记得 p9 的前端非常稀少。这其实也侧面反应出大家的主观感受是确有其事的。


    写到这里,我深感焦虑,赶紧去问问万能的 AI:


    ai.sb


    卧槽,被辱骂了一通。拿出我大哥 Dan 也没用!


    回到正题,swyx 又提到,有人说只要成为全栈就好了。



    直接看看这张图:



    全栈并不是口头说说那么简单,有一个小型公司的 CTO 也现身说出了自己的看法:


    image.png


    后端普遍认为前端简单,在国外也一样



    前端成为产品总负责人,比成为技术 vp 的路径要概率更大一些,这也符合玉伯的发展路径:


    image.png


    关于这件事儿,Hacker News 也有一些讨论,不过质量比较差,走偏了:


    开始讨论后端的烂代码了


    讨论男女平等


    我的看法


    看完了几乎全部的讨论以后,我感觉国外的开发者对于前端天花板的看法和国内差不多,确实是认为有后端工程背景的人升为 VP/CTO 级别的概率比较高,而前端更倾向于在框架中日复一日的迷失。


    以我自己的职业经历来说,假设我在使用 React 技术栈,今天在用 redux,明天出了一个 redux-toolkit 来解决 redux 太烂的问题,你迁移过去了,学到了很多范式很充实。再过几个月,又来了个 recoil,又来个 jotai。好像在很忙碌的学习,但其实都没有脱离状态管理的范畴,就像是被困在小学里反复的读五年级,而后端的人可能去研究更广阔的东西了。比如:




    1. 稳定性:各种灾备方案,限流等操作。




    2. 高并发:延迟,tps。




    3. 一致性:数据正确性。




    而前端比较好的处境,就是在一家前端主导产品的公司(比如最近比较火的 AffiNE)参与核心功能的研发,那么可以接触到前端比较深入的一些技术,而且有一帮大牛同事可以陪你玩最新的技术栈。又或者是参与到大型公司的基础架构建设,我了解到的比如性能监控、低代码搭建、serveless 建设、自研 JS 引擎、自研 Rust 编译库,也可以获得比较深入的技术提升。


    不过,大部分人的整个职业生涯可能都在做一些 Vue 或者 React 的应用开发,后台管理系统、活动页等等。。。是不是就完了?人生没希望了?


    再问问 AI:



    我丢,这 AI 吃枪药了吧。


    不过他骂的也不无道理,安心做个平庸的前端又怎么样呢?比起很多职业来说,坐在电脑前敲敲你喜欢的代码,当个快乐的小前端,拿个 10-20k 的薪资,不够过日子的嘛?想想土木老哥在烈日下的样子?



    我对于平庸人生的看法,把注意力转移到自己的生活中,有一个可以坚持热爱的爱好(比如我自己就喜欢踢足球和健身)。做一个自信阳光的小骚年,不是也很不错吗?


    不要高杠杆买房,不要负债太多,保持一定的积蓄习惯,注意资产的合理配置。你肯定能比一般职业的人过得更好,欲望才是万恶之源。


    当然,这只是比较悲观的想法,如果你有一颗上进的心,拼到个资深工程师,有点管理能力的话,再争取个前端小 leader 当当,过上小资点的生活也没问题。


    我的意思是,人生短短几十年,职业不是生活的全部。假设你全心全意拼在工作上,到了 40 岁挣了一堆钱,落了一身的病。你觉得你真的快乐吗?如果钱是快乐的全部的话,李玟也不会得抑郁症,张朝阳也不会因为抑郁放弃公司管理跑去修行了。


    总结


    前端确实天花板比较低,不过那又咋样呢?最终能成为 VP 的人也没几个,如果你从小就就是天之骄子,目标是星辰大海,那你考上 985 的计算机系应该没什么问题,在校招的时候就果断选后端吧,确实有几率爬的更高点,但是付出相应的代价也是必要的(后端头发平均值明显低于前端)。


    屏幕截图 2023-07-29 052610.jpg


    否则,你就做个快乐的小前端,也比其他大多数职业过得舒服。


    作者:ssh_晨曦时梦见兮
    来源:juejin.cn/post/7261807670746513463
    收起阅读 »

    第一个可以在条件语句中使用的原生hook诞生了

    大家好,我卡颂。 在10月13日的first-class-support-for-promises RFC中,介绍了一种新的hook —— use。 use什么?就是use,这个hook就叫use。这也是第一个:可以在条件语句中书写的hook可以在其他hook...
    继续阅读 »

    大家好,我卡颂。


    在10月13日的first-class-support-for-promises RFC中,介绍了一种新的hook —— use


    use什么?就是use,这个hook就叫use。这也是第一个:

    • 可以在条件语句中书写的hook

    • 可以在其他hook回调中书写的hook


    本文来聊聊这个特殊的hook


    欢迎加入人类高质量前端框架研究群,带飞


    use是什么


    我们知道,async函数会配合await关键词使用,比如:

    async function load() {
    const {name} = await fetchName();
    return name;
    }

    类似的,在React组件中,可以配合use起到类似的效果,比如:

    function Cpn() {
    const {name} = use(fetchName());
    return <p>{name}</p>;
    }

    可以认为,use的作用类似于:

    • async await中的await

    • generator中的yield


    use作为读取异步数据的原语,可以配合Suspense实现数据请求、加载、返回的逻辑。


    举个例子,下述例子中,当fetchNote执行异步请求时,会由包裹NoteSuspense组件渲染加载中状态


    当请求成功时,会重新渲染,此时note数据会正常返回。


    当请求失败时,会由包裹NoteErrorBoundary组件处理失败逻辑。

    function Note({id}) {
    const note = use(fetchNote(id));
    return (
    <div>
    <h1>{note.title}</h1>
    <section>{note.body}</section>
    </div>
    );
    }

    其背后的实现原理并不复杂:

    1. Note组件首次renderfetchNote发起请求,会throw promise,打断render流程

    2. Suspense fallback作为渲染结果

    3. promise状态变化后重新触发渲染

    4. 根据note的返回值渲染


    实际上这套基于promise的打断、重新渲染流程当前已经存在了。use的存在就是为了替换上述流程。


    与当前React中已经存在的上述promise流程不同,use仅仅是个原语primitives),并不是完整的处理流程。


    比如,use并没有缓存promise的能力。


    举个例子,在下面代码中fetchTodo执行后会返回一个promiseuse会消费这个promise

    async function fetchTodo(id) {
    const data = await fetchDataFromCache(`/api/todos/${id}`);
    return {contents: data.contents};
    }

    function Todo({id, isSelected}) {
    const todo = use(fetchTodo(id));
    return (
    <div className={isSelected ? 'selected-todo' : 'normal-todo'}>
    {todo.contents}
    </div>
    );
    }

    Todo组件的id prop变化后,触发fetchTodo重新请求是符合逻辑的。


    但是当isSelected prop变化后,Todo组件也会重新renderfetchTodo执行后会返回一个新的promise


    返回新的promise不一定产生新的请求(取决于fetchTodo的实现),但一定会影响React接下来的运行流程(比如不能命中性能优化)。


    这时候,需要配合React提供的cache API(同样处于RFC)。


    下述代码中,如果id prop不变,fetchTodo始终返回同一个promise

    const fetchTodo = cache(async (id) => {
    const data = await fetchDataFromCache(`/api/todos/${id}`);
    return {contents: data.contents};
    });

    use的潜在作用


    当前,use的应用场景局限在包裹promise


    但是未来,use会作为客户端中处理异步数据的主要手段,比如:


    • 处理context

    use(Context)能达到与useContext(Context)一样的效果,区别在于前者可以在条件语句,以及其他hook回调内执行。


    • 处理state

    可以利用use实现新的原生状态管理方案:

    const currentState = use(store);
    const latestValue = use(observable);

    为什么不使用async await


    本文开篇提到,use原语类似async await中的await,那为什么不直接使用async await呢?类似下面这样:

    // Note 是 React 组件
    async function Note({id, isEditing}) {
    const note = await db.posts.get(id);
    return (
    <div>
    <h1>{note.title}</h1>
    <section>{note.body}</section>
    {isEditing ? <NoteEditor note={note} /> : null}
    </div>
    );
    }

    有两方面原因。


    一方面,async await的工作方式与React客户端处理异步时的逻辑不太一样。


    await的请求resolve后,调用栈是从await语句继续执行的(generatoryield也是这样)。


    而在React中,更新流程是从根组件开始的,所以当数据返回后,更新流程是从根组件从头开始的。


    改用async await的方式势必对当前React底层架构带来挑战。最起码,会对性能优化产生不小的影响。


    另一方面,async await这种方式接下来会在Server Component中实现,也就是异步的服务端组件。


    服务端组件与客户端组件都是React组件,但前者在服务端渲染(SSR),后者在客户端渲染(CSR),如果都用async await,不太容易从代码层面区分两者。


    总结


    use是一个读取异步数据的原语,他的出现是为了规范React在客户端处理异步数据的方式。


    既然是原语,那么他的功能就很底层,比如不包括请求的缓存功能(由cache处理)。


    之所以这么设计,是因为React团队并不希望开发者直接使用他们。这些原语的受众是React生态中的其他库。


    比如,类似SWRReact-Query这样的请求库,就可以结合use,再结合自己实现的请求缓存策略(而不是使用React提供的cache方法)


    各种状态管理库,也可以将use作为其底层状态单元的容器。


    值得吐槽的是,Hooks文档中hook的限制那一节恐怕得重写了。


    作者:魔术师卡颂
    链接:https://juejin.cn/post/7155673959084589070
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    从尤雨溪这两天微博募捐,思考开源如何赚大钱

    这两天,尤大在他的微博表示,他打算开启国内开源捐赠计划,截止本文发帖为止,已经有 6k / 月的固定充电了。 这个数额目前还是比较小的,企业级别的 sponsor 应该还没有出现,光靠个人捐赠的话这点钱真的完全不够团队开销的。 正巧我看到了 Ink 作者的...
    继续阅读 »

    这两天,尤大在他的微博表示,他打算开启国内开源捐赠计划,截止本文发帖为止,已经有 6k / 月的固定充电了。


    这个数额目前还是比较小的,企业级别的 sponsor 应该还没有出现,光靠个人捐赠的话这点钱真的完全不够团队开销的。






    正巧我看到了 Ink 作者的一篇文章,讲述他在开源软件如何稳定搞钱这方面的思考,觉得他的很多观点非常犀利,值得各位前端开发者同学一起学习,毕竟大家未来可能有搞开源的一天。
    接下来是他的这篇 Generating income from open source 的内容:


    最近,Ink 的知名度越来越高,并且已经被一些知名公司使用了一段时间。然而,与大多数其他开源项目一样,Ink没有任何收入。


    我开始研究各种选项,以改变这种情况,并以某种方式开始收费,这样它就可以支持我以及 Ink 和相关项目(如 Ink UIPastel) 的进一步开发。


    本文是我在这个主题上所学到的内容的简要版本。


    不起作用的方法


    以下是我认为维护者无法从他们的项目中获得收入的原因。


    依靠个人捐赠


    能够有人愿意支持你是很好的,但是每月 5 美元的捐赠无法维持生活。这是社区对你工作的感激的一种方式,但不应被视为稳定的收入来源。


    除非你是社区中极少数非常受欢迎的开发者之一,否则接受事实,不会有足够多的人订阅每月捐赠。


    尽管如此,我认为个人捐赠并不是答案。


    期望公司捐赠


    你构建了很火的项目,并在生产环境中稳定运行,他们从中获益良多。当然,他们肯定知道要回馈一下,毕竟他们赚了那么多钱,是这样的吗?


    我们需要最终明白一些简单的道理,改变我们的预期。


    经营业务意味着最大化收入和最小化支出。企业不会为了只是为了对你好点,而增加一个长期开支。(万恶的资本家)


    企业习惯于以金钱交换价值。开源维护者需要考虑到这一点。你提供价值,他们从中受益并为此付费。


    确实有一些拥有强大开源文化的公司可以持续给他们依赖的项目提供重大的每月捐赠,但不幸的是,他们是个例。


    完全依赖捐赠或赞助


    下面这句话,是不是很耳熟?



    请赞助我吧,这样我就可以继续开发我的开源项目。



    我们整了一个漂亮的 GitHub 赞助页面,然后坐在那里等待有人注册。你能想象一个企业采用类似的策略吗?它会在一个月内破产倒闭。


    我们需要理解我们的项目对公司所提供的价值,并开始收费,就像我们经营一家企业,销售一种有用的产品。


    认为没有人愿意付费或者定价不够高


    在几家中小型初创公司工作过后,我现在明白几年前自己有多么愚蠢,以为每月 200 块的订阅费是天价,或者公司不愿意为工具付费。纯属扯犊子。


    公司为员工解决日常问题和开发产品支付数百万的钞票。如果你的项目解决了他们的问题,使他们的团队不必自己解决,他们会支付比你认为的价值高 10 倍、100 倍甚至 1000 倍的费用。而且,他们会很满意。


    公司已经为各种工具和费用支付数万元每月。无论你要求什么,实际上对他们来说都是九牛一毛。把你的产品价格翻倍吧,没毛病。


    害怕或者羞于索要信用卡信息


    我们不需要为我们的工作收费找理由。没有什么可羞耻的。


    你为解决一个问题而付出你的努力。有人为了这个问题请你付费解决,别多虑了。


    有效方法


    我们喜欢抱怨没人支付维护者的费用,但实际上有很多建立在开源基础上的成功企业。以下是它们持续收入的秘诀:


    商业许可证


    Dave DeSandro 的Metafizzy提供各种 JavaScript 库,其中包括 Isotope - 用于创建灵活网格布局的库。Isotope 是开源的,但根据你的使用方式有不同的许可证



    1. 开源许可证。


    这个许可证允许在个人或开源项目中免费使用 Isotope。



    1. 商业许可证。


    这个许可证允许你在几乎任何商业应用中使用 Isotope。实际上,任何希望使用它的公司很可能需要购买商业许可证。


    商业许可证的定价根据使用人数而不同:

    • 单个开发者的费用为 25 美元。
    • 8 名开发者团队的费用为 110 美元。
    • 无限数量的开发者的费用为 320 美元。

    请注意,这些不是订阅,而是一次性付款。


    商业许可证本身是一份 PDF 文件,支付后通过 Gumroad 发送给你。



    1. 商业 OEM 许可证。


    该许可证适用于先前的商业许可证未涵盖的其他用途,特别是 UI 构建器、SDK 或工具包。对于商业 OEM 许可证没有公开的定价,这意味着它比前几个等级要贵得多。这些用例可能意味着 Isotope 作为用户界面或产品提供中的关键组成部分,因此公司愿意支付高额费用。


    我喜欢这种方法的原因


    这看起来是对开源进行收费最简单的方式,因为 Metafizzy 为同一份代码提供了不同的许可证,许可证本身是一个 PDF 文件。没有专业版,没有许可证密钥,也没有其他需要维护的东西。个人开发者可以免费使用同样的工具,而公司则支付合理的价格。


    为更多功能收费


    Mike Perham 的Sidekiq是一个在 Ruby 应用程序中基于 Redis 的后台作业的著名的库。Sidekiq 提供了 3 种不同的计划:



    1. 开源版。


    Sidekiq 免费提供一个有限的开源版本。尽管它被称为“开源”,但 LGPL 许可证似乎允许你在商业应用中使用免费版本。


    开源计划不提供任何客户支持,有问题就去提 GitHub Issue 吧。



    1. 专业版。


    专业版每月收费 99 美元(或 995 美元/年),提供更多的功能。例如,批处理后台作业、通过更高级的 Redis API 提供的增强可靠性。专业版还包括通过电子邮件提供的客户支持。



    1. 企业版。


    企业版根据你运行的 Sidekiq 实例数量,以 229 美元/月或更高的价格提供全部功能。


    Sidekiq 的表现非常出色,根据 Mike 在 Hacker News 的最新评论,它现在每年创造 1000 万美元的收入。


    有趣的是,他还提到,你可以通过其他开源 Ruby gem 组装 Sidekiq 的大多数付费功能,但是设置和维护起来需要很多时间。最终,你可能会得到一个比经过多次测试的 Sidekiq 还要糟糕的系统,所以购买功能齐全的 Sidekiq 似乎是明智之举。



    Sidekiq 的大多数商业功能都可作为开源软件包获得,但是当你将 3-6 个这些功能集成在一起时,复杂性会悄然而至。自己构建往往会导致一个比我精心策划的成熟、经过良好调试的系统还要差的系统。



    一旦你注册了 Sidekiq,你将获得访问私有 Ruby gem 服务器的权限,可以从中下载并更新应用程序中的sidekiq gem。他自己构建了这个系统,并表示不用花太多时间维护它。


    我喜欢这种方法的原因


    Sidekiq 首先是一个很棒的开源项目。在 Ruby 社区中,当你需要后台队列时,它成为了一个明显的选择。这是 Sidekiq 唯一的营销渠道。


    然后,开发人员向他们的朋友和公司的管理人员推荐 Sidekiq。随着他们的应用程序扩大,客户有明显的动机支付 Sidekiq 以解锁更多功能。


    托管版本


    最近,越来越多的企业将其整个产品开源,并提供托管版本以获取收费。

    • Plausible Analytics - 一个注重隐私的 Google Analytics 替代方案。托管版本每月起价 9 美元。
    • PostHog - 产品分析、功能标志、A/B 测试等多个数据工具的组合。托管版本采用按用量计费,前 100 万个事件免费,之后每个事件收费 0.0003068 美元。
    • Metabase - 数据库仪表板。托管版本每月起价 85 美元。

    这些只是我能想到的例子,还有许多类似的例子。


    我喜欢这种方法的原因


    你可以构建一次应用程序,并将相同版本作为开源和托管付费产品提供。你可能会想:“为什么有人愿意为可免费获得的东西付费”。然而,Plausible Analytics 每年收入 100 万美元,所以肯定有很多人愿意支付小额的月费来享受他们的产品,而不用自己搞乱七八糟的服务器啥的。


    收费维护和高级材料


    Moritz Klack、Christopher Möller、John Robb 和 Hayleigh Thompson 的React Flow是一个用于交互式流程图的 React 库。这是一个可持续的开源项目,与我以前见过的任何项目都不同。React Flow 为公司提供了一个专业版订阅,其中提供以下功能:

    • 访问专业版高级用例示例。
    • 优先解决 GitHub 上的问题。
    • 每月最多 1 小时的电子邮件支持。
    • 最有趣的是,我引用一下,“保持库的运行和维护,采用 MIT 许可证”。

    在整个定价页面上,大部分文案都集中在最后一点上。React Flow 不是一个容易用其他东西替代的库,所以公司很可能有兴趣确保它得到良好的维护,并继续使用 MIT 许可。


    John 在他们的博客上写了一篇优秀的文章,名为“Dear Open Source: let’s do a better job of asking for money”,我建议你阅读一下。我对此非常着迷,所以给 John 发了一封邮件,提出了一些后续问题,他非常友善地回答了我关于这个话题的许多宝贵的知识。


    以下是我从我们的邮件往来中总结出的要点:

    • 包装很重要。公司内部持有信用卡的人希望看到他们一直在看到的“定价”页面。GitHub 赞助页面行不通。React Flow 最初有一个这样的页面,但几乎没有获得任何收入。当他们推出一个类似 SaaS 的产品网站,并提供几个定价层次时,情况改善了。
    • 让大家发现专业版计划。React Flow 组件显示一个指向他们网站的链接,并要求开发人员在订阅专业版计划后将其删除。即使在不这样做的情况下删除它仍然完全合法和可以接受,但它作为一个不会强迫的好方法,可以促使人们查看专业版计划。
    • 公司在有支持的情况下更有安全感。React Flow 每月提供最多 1 小时的电子邮件支持,所以我自然而然地问如果客户花费的时间超过 1 小时会发生什么。John 表示,即使如此,他们还是会继续通过电子邮件提供支持,最后一切都会平衡,因为有很多客户根本不联系他们。他还认为,电子邮件支持会给人一种保险的感觉,因此公司知道如果有需要,他们可以找到他们,即使他们从未这样做过。
    • 为人们提供可以立即购买和访问的东西。我想知道那些对专业版客户可用的高级示例有多重要,因为与其他好处相比,它们似乎只是一种美好的附加功能。令人惊讶的是,John 有不同的看法。他坚信,购买后立即提供一些有价值的东西可以将他们的专业版计划与咨询公司或服务区分开来。这还为客户提供了一个参考点,他们可以在项目中使用并学习。此外,这还有助于吸引那些对 React Flow 感兴趣的公司。

    我喜欢这种方法的原因


    React Flow 以其出色的开源库而闻名,但他们找到了一种明智的方式在商业上获得收入。他们在定价、包装和支持方面的决策都非常明智,并成功地转化了开源用户为付费客户。


    这是我了解到的一些有关将开源项目变为可持续收入的方法。希望这些例子能给你提供一些灵感和启示!


    支持包


    最后但同样重要的是,你可以围绕你的开源工作建立一家咨询公司,并向依赖于该工作的公司提供专业知识支持。

    • Babel 在他们的Open Collective页面上提供了每年 2.4 万美元的计划,其中公司每月可以获得 2 小时的电子邮件或视频支持。
    • curl 提供商业支持,甚至包括开发定制功能和代码审核以了解你如何使用 curl。
    • Filippo Valsorda向公司提供每年五位数的保留协议。Filippo 与工程师会面,了解他们的需求,并在开发他的开源软件时确保这些需求得到满足。Filippo 是一个密码学专家,所以公司可以签订更昂贵的合同,以获得他在与密码学相关的任何事物上的专业知识,而不仅仅是他自己的项目。

    我喜欢这种方法的原因


    为公司提供付费支持使你的项目保持完全开源的同时,比 Pro 订阅带来更多的收入。这个过程很难,但对于一个习惯于作为员工工作的人来说,很有吸引力。


    结论


    偶尔会在 Hacker News 上看到人们讨论开源模式的缺点,护者没有从受益于他们工作的公司那里获得任何收入。


    这不公平。他们能做些什么?可以有多种可行的选项可以生成可持续的收入,也有许多成功的例子说明人们今天正在这样做,并且已经持续了很久。这也可能适用于你,快去试试吧,否则你永远不会知道。


    作者:ssh_晨曦时梦见兮
    链接:https://juejin.cn/post/7245584147456278589
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    图片转换成webp

    web
    webp的几个问题 1. 什么是webp? 最直接的就是一个图片的后缀是.webp而不是.png/.jpeg等,官方的说法就是由Google开发的一种用于图像压缩的现代格式,目的就是减小图片的大小从而提高网页加载速; 2. 是不是所有浏览器都支持webp图片?...
    继续阅读 »

    webp的几个问题


    1. 什么是webp?


    最直接的就是一个图片的后缀是.webp而不是.png/.jpeg等,官方的说法就是由Google开发的一种用于图像压缩的现代格式,目的就是减小图片的大小从而提高网页加载速;


    2. 是不是所有浏览器都支持webp图片?如何判断浏览器是否支持webp格式的图片


    不是所有的浏览器都支持 WebP 图片格式,但大多数主流的现代浏览器都已经支持了。以下是一些常见的浏览器对 WebP 格式的支持情况:



    • Google Chrome:支持 WebP 格式。

    • Mozilla Firefox:支持 WebP 格式。

    • Microsoft Edge:支持 WebP 格式。

    • Safari:从 Safari 14 开始,支持 WebP 格式
      要判断浏览器是否支持 WebP 格式的图片,可以使用 JavaScript 进行检测。以下是一种常用的方法:


    function isWebPSupported() {
    var elem = document.createElement('canvas');
    if (!!(elem.getContext && elem.getContext('2d'))) {
    // canvas 支持
    return elem.toDataURL('image/webp').indexOf('data:image/webp') === 0;
    }
    // canvas 不支持
    return false;
    }

    if (isWebPSupported()) {
    console.log('浏览器支持 WebP 格式');
    } else {
    console.log('浏览器不支持 WebP 格式');
    }


    上述代码通过创建一个 canvas 元素,并尝试将其转换为 WebP 格式的图片。如果浏览器支持 WebP 格式,则会返回一个以 "data:image/webp" 开头的数据 URL。


    通过这种方式,你可以在网页中使用 JavaScript 检测浏览器是否支持 WebP 格式,并根据需要提供适当的替代图片


    3. 图片转换成webp之后一定会比之前的图片更小吗?


    答案是否定的。一般来说,具有大量细节、颜色变化和复杂结构的图像可能会在转换为 WebP 格式后获得更好的压缩效果,反之有些转换后可能会比之前更大;所以最好是图片转换为 WebP 格式之前,建议进行测试和比较不同压缩参数和质量级别的结果,以找到最佳的压缩设置,对最终转换后变成更大的建议不做转换


    4. 如何将图片转换成webp



    • 图像编辑软件 如 Adobe Photoshop、GIMP 或在线工具,如 Google 的 WebP 编码器。这些工具可以让你将现有的图像转换为 WebP 格式,并选择压缩质量和压缩类型(有损或无损)

    • 插件转换webp插件文档链接接入


    image.png


    5. 项目中如何接入??


    思路:



    • 第一步肯定是转化将项目中的存储的图片文件通过插件转换出webp格式的图片

    • 判断网页运行的浏览器是否支持webp格式的图片,如果支持,将项目中所有使用png/jpeg的图片的全部替换成webp


    6. 转换出项目中图片的webp格式的图片


    const imagemin = require("imagemin");
    const imageminWebp = require("imagemin-webp");

    function transformToWebp(destination, filePaths) {
    await imagemin([filePath || `${destination}/*.{jpg,png}`], {
    destination: `${destination}/webp/`, // 转换出的webp图片放置在什么目录
    plugins: [imageminWebp({quality: 75})] // 使用imageminWebp转换转换质量级别设置多少
    })
    }

    具体到项目中,我们只希望转换我们当前正在开发的文件夹中的图片,而且已经转化的未作修改的就不要再重复转化; 如何知道哪些是新增的或者修改的呢? 想一想🤔️,是不是“git status”可以看到
    所以开始做如下调整


    // 获取git仓库中发生变更的文件列表
    function getGitStatusChangedImgFiles() {
    return String(execSync('git status -s'))
    .split('\n')
    .map(item => item.split(' ').pop()
    .filter(path => path.match(/\.(jpg)|(png)/))
    );
    };

    返回一个包含变更图片文件路径的数组['src/example/image/a.png','src/example/image/b.png', '……']


    const imgPaths = getGitStatusChangedImgFiles()
    async function transformAllChangedImgToWebp() {
    const resData = await promise.all(
    imgPaths.map(path => {
    const imgDir = path.replace(/([^\\/]+)\.([^\\/]+)/i, "") // src/banners/guardian_8/img/95_copy.png => src/banners/guardian_8/img/
    return transformToWebp(imgDir, path)
    })
    )
    const allDestinationPaths = resData.map((subArr) => subArr[0].destinationPath)
    // 如果这里我们想将生成的webp图片自动的add上去,那么就这样:
    execSync(`git add ${allDestinationPaths.join(" ")}`);
    }



    image.png


    什么时候转换成webp最好?


    我们在commit的时候进行转换图片,以及自动将转换的图片进行提交
    这样我们就可以运用git的钩子函数处理了;


    npm install husky --save-dev

    // .husky/pre-commit中
    #!/usr/bin/env sh
    . "$(dirname -- "$0")/_/husky.sh"

    current_branch=`git rev-parse --abbrev-ref HEAD`

    if [[ $current_branch === 'main']]; then
    # 生成 webp 图片
    npm run webp -- commit
    fi

    这样在我们commit时就会自动触发pre-commit钩子函数,在package.json中配置webp执行的脚步,执行上述transformAllChangedImgToWebp函数,然后在里面转换出webp图片并将新生成的webp自动git add上去,最后一并commit;


    知识点


    1. execSync是什么?


    execSync 是一个 Node.js 内置模块 child_process 中的方法,用于同步执行外部命令。在 Node.js 中,child_process 模块提供了一组用于创建子进程的函数,其中包括 execSync 方法。execSync 方法用于执行指定的命令,并等待命令执行完成后返回结果。


    const { execSync } = require('child_process'); const output = execSync(command, options);

    2. git status -s 会显示每个文件的状态信息



    • A:新增文件

    • M:修改文件

    • D:删除文件

    • R:文件名修改

    • C:文件的拷贝

    • U:未知状态


    image.png


    3. execSync('git status -s')返回值是什么?


    image.png


    通过String后就可以变成可见的字符串了,然后通过分割等就能拿到具体的修改的文件路径


    4. Husky是什么?


    Husky 是一个用于在 Git 提交过程中执行脚本的工具。它可以帮助开发人员在代码提交前或提交后执行一些自定义的脚本,例如代码格式化、代码质量检查、单元测试等。Husky 可以确保团队成员在提交代码之前遵循一致的规范和约定。


    Husky 的工作原理是通过在 Git 钩子(Git hooks)中注册脚本来实现的。Git 钩子是在特定的 Git 事件发生时执行的脚本,例如在提交代码前执行 pre-commit 钩子,或在提交代码后执行 post-commit 钩子。push代码前执行pre-push的钩子、编写提交信息时执行commit-msg的钩子可用于提交什么规范


    小结



    1. 通过execSync('git status -s')从中获取筛选当前新增/修改过的图片;

    2. 调用imagemin和imagemin-webp将图片转换出webp格式的图片

    3. husky的pre-commit中触发上述调用执行,并在里面顺道将新生成的webp一并add上去

    4. 至于后续生成的webp图片怎么使用,这将在下一篇文章中学习


    作者:东风t西瓜
    来源:juejin.cn/post/7260016275300155449
    收起阅读 »

    Token到底是什么?!

    web
    随着Web应用的发展,为了保证API通信的安全性,很多项目在进行设计时会采用JSON Web Token(JWT)的解决方案。 JWT是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为JSON对象。这种信息可以...
    继续阅读 »

    随着Web应用的发展,为了保证API通信的安全性,很多项目在进行设计时会采用JSON Web TokenJWT)的解决方案。


    JWT是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为JSON对象。这种信息可以被验证和信任,因为它是数字签名的。


    那么JWT中的Token到底是什么?接下来,我们将以登录功能为例进行Token的分析。


    登录流程


    很多小伙伴对登录的流程已经很熟悉了,我们来看一个最基本的后台系统的登录流程


    登录流程图.png


    流程图很清楚了,接下来我们使用 V2Koa 实现一个登录过程,来看看Token到底是什么


    Vue2 + Koa 实现登录


    前端代码


    1. 前端点击事件


    数据的校验就忽略掉,感兴趣的同学可自行书写或者找我要源码,直接看点击事件


    handleLogin() {
    this.$refs.loginForm.validate((valid) => {
    if (valid) {
    this.loading = true;
    // 这里使用了VueX
    this.$store
    .dispatch("user/login", this.loginForm)
    .then(() => {
    this.$router.push({ path: this.redirect || "/" });
    this.loading = false;
    })
    .catch(() => {
    this.loading = false;
    });
    } else {
    return false;
    }
    });
    }

    2. Vuex中的action


    校验通过后触发VueXUser模块的Login方法:


    async login(context, userInfo) {
    const users = {
    username: userInfo.mobile,
    password: userInfo.password
    }
    const token = await login(users)
    // 在这里大家可以对返回的数据进行更详细的逻辑处理
    context.commit('SET_TOKEN', token)
    setToken(token)
    }

    3. 封装的接口


    export function login(data) {
    return request({
    url: '/login',
    method: 'post',
    data
    })
    }

    以上三步,是我们从前端向后端发送了请求并携带着用户名和密码,接下来,我们来看看Koa中是如何处理前端的请求的


    Koa 处理请求


    首先介绍一下Koa



    Koa 基于Node.js平台,由 Express 幕后的原班人马打造,是一款新的服务端 web 框架



    Koa的使用极其简单,感兴趣的小伙伴可以参考官方文档尝试用一下


    Koa官网:koa.bootcss.com/index.html#…


    1. 技术说明


    在当前案例的koa中,使用到了jsonwebtoken的依赖包帮助我们去加密生成和解密Token


    2. 接口处理


    const { login } = require("../app/controller/user")
    const jwt = require("jsonwebtoken")
    const SECRET = 'test_';
    router.post('/login', async (ctx, next) => {
    const { username, password } = ctx.request.body
    // 这里是调用Controller中的login方法来跟数据库中的数据作对比,可忽略
    const userList = await login(username, password)

    if (!userList) {
    // 这里的errorModel是自己封装的处理错误的模块
    ctx.body = new errorModel('用户名或密码错误', '1001')
    return
    }

    // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓ ※ 重点看这里 ※ ↓↓↓↓↓↓↓↓↓↓↓↓↓↓
    const token = jwt.sign({ userList }, SECRET, { expiresIn: "1h" })

    ctx.body = {
    success: true,
    state: 200,
    message: 'login success',
    data: token
    };
    return;
    })

    关于 JWT


    上面的重点代码大家看到了,接下来具体给大家解释下JWT



    Jwt由三部分组成:headerpayloadsignature



    export interface Jwt {
    header: JwtHeader;
    payload: JwtPayload | string;
    signature: string;
    }

    header头部


    里面的包含的内容有很多,比如用于指定加密算法的alg、指定加密类型的typ,全部参数如下所示:


    export interface JwtHeader {
    alg: string | Algorithm;
    typ?: string | undefined;
    cty?: string | undefined;
    crit?: Array<string | Exclude<keyof JwtHeader, 'crit'>> | undefined;
    kid?: string | undefined;
    jku?: string | undefined;
    x5u?: string | string[] | undefined;
    'x5t#S256'?: string | undefined;
    x5t?: string | undefined;
    x5c?: string | string[] | undefined;
    }

    payload负载


    payload使我们存放信息的地方,里面包含了签发者过期时间签发时间等信息


    export interface JwtPayload {
    [key: string]: any;
    iss?: string | undefined;
    sub?: string | undefined;
    aud?: string | string[] | undefined;
    exp?: number | undefined;
    nbf?: number | undefined;
    iat?: number | undefined;
    jti?: string | undefined;
    }

    signature签名


    signature 需要使用编码后的 headerpayload以及我们提供的一个密钥(SECRET),然后使用 header 中指定的签名算法进行签名


    关于 jwt.sign()


    jwt.sign()方法,需要三个基本参数和一个可选参数:payloadsecretOrPrivateKeyoptions和一个callback


    export function sign(
    payload: string | Buffer | object,
    secretOrPrivateKey: Secret,
    options: SignOptions,
    callback: SignCallback,
    ): void;

    payload是我们需要加密的一些信息,这个参数对应上面koa代码中的{ userList },而userList则是我从数据库中查询得到的数据结果


    secretOrPrivateKey则是我们自己定义的秘钥,用来后续验证Token时所用


    options选项中有很多内容,例如加密算法algorithm、有效期expiresIn等等


    export interface SignOptions {
    /**
    * Signature algorithm. Could be one of these values :
    * - HS256: HMAC using SHA-256 hash algorithm (default)
    * - HS384: HMAC using SHA-384 hash algorithm
    * - HS512: HMAC using SHA-512 hash algorithm
    * - RS256: RSASSA using SHA-256 hash algorithm
    * - RS384: RSASSA using SHA-384 hash algorithm
    * - RS512: RSASSA using SHA-512 hash algorithm
    * - ES256: ECDSA using P-256 curve and SHA-256 hash algorithm
    * - ES384: ECDSA using P-384 curve and SHA-384 hash algorithm
    * - ES512: ECDSA using P-521 curve and SHA-512 hash algorithm
    * - none: No digital signature or MAC value included
    */

    algorithm?: Algorithm | undefined;
    keyid?: string | undefined;
    /** expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms.js). Eg: 60, "2 days", "10h", "7d" */
    expiresIn?: string | number | undefined;
    /** expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms.js). Eg: 60, "2 days", "10h", "7d" */
    notBefore?: string | number | undefined;
    audience?: string | string[] | undefined;
    subject?: string | undefined;
    issuer?: string | undefined;
    jwtid?: string | undefined;
    mutatePayload?: boolean | undefined;
    noTimestamp?: boolean | undefined;
    header?: JwtHeader | undefined;
    encoding?: string | undefined;
    allowInsecureKeySizes?: boolean | undefined;
    allowInvalidAsymmetricKeyTypes?: boolean | undefined;
    }

    callback则是一个回调函数,有两个参数,默认返回Token


    export type SignCallback = (
    error: Error | null,
    encoded: string | undefined,
    ) =>
    void;

    通过以上方法加密之后的结果就是一个Token


    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU3ZmVmMTY0ZTU0YWY2NGZmYzUzZGJkNSIsInhzcmYiOiI0ZWE1YzUwOGE2NTY2ZTc2MjQwNTQzZjhmZWIwNmZkNDU3Nzc3YmUzOTU0OWM0MDE2NDM2YWZkYTY1ZDIzMzBlIiwiaWF0IjoxNDc2NDI3OTMzfQ.PA3QjeyZSUh7H0GfE0vJaKW4LjKJuC3dVLQiY4hii8s


    总结


    在整个的Koa中,用到了jsonwebtoken这个依赖包,里面有sign()方法


    而我们前端所得到的数据通过sign()加密出来的包含自定义秘钥的一份用户信息而已


    至于用户信息中有什么内容,可以随便处理,比如用户的ID、用户名、昵称、头像等等


    那么这个Token后续有什么用呢?


    后续我们可以在前端的拦截器中配置这个Token,让每一次的请求都携带这个Token,因为Koa后续需要对每一次请求进行Token的验证


    比如登录成功后请求用户的信息,获取动态路由,再通过前端的router.addRoutes()将动态路由添加到路由对象中去即可


    作者:半截短袖
    来源:juejin.cn/post/7275211391102189628
    收起阅读 »

    移动端的「基金地图」是怎么做的?

    web
    🙋🏻‍♀️ 编者按:本文作者是蚂蚁集团前端工程师芒僧,今年 8 月份开始到 9 月底,「支付宝 - 基金」里面的指数专区进行了一波大改版升级,这次借助 F2 4.0 强大的移动端手势交互能力,我们尝试了一种多维度探索式选基决策工具,本文具体介绍了它是如何实现的...
    继续阅读 »

    🙋🏻‍♀️ 编者按:本文作者是蚂蚁集团前端工程师芒僧,今年 8 月份开始到 9 月底,「支付宝 - 基金」里面的指数专区进行了一波大改版升级,这次借助 F2 4.0 强大的移动端手势交互能力,我们尝试了一种多维度探索式选基决策工具,本文具体介绍了它是如何实现的。



    Kapture 2022-10-19 at 14.12.19.gif


    这次在 「支付宝 - 基金」里的【指数专区改版】需求,我们玩了一种很新的东西 🌝


    8月份开始到9月底,「支付宝 - 基金」里面的指数专区进行了一波大改版升级,这次借助 F2 4.0 强大的移动端手势交互能力,我们尝试了一种多维度探索式选基决策工具(如上动图所示)。


    简单来说,用户可以在一个散点图上根据 「收益」和「波动」 这两个维度全览对比整个市场里的指数基金,并选出适合自己的指数基金进行投资,这个功能我们愿称其为 「指数图谱」 🐶 。



    图谱是这个业务场景上的叫法,实际上图谱应该是关系图而非统计图.



    image.pngimage.pngimage.png


    功能已发布,页面访问路线如上


    先看看有哪些功能点



    1. 精细打磨的移动端手势交互,平移、缩放、横扫不在话下 :


    Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 19.00.49.gif Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 19.01.38.gif Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 19.43.17.gif


    依次为:缩放、平移、横扫



    1. 底部产品卡和图表的联动交互:


    Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 19.54.40.gif Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 19.54.40.gif


    依次为:点击图表上的气泡、滑动底部卡片



    1. 无惧数据点太多看不到细节,我们有自适应的气泡抽样展示和自动聚焦:


    Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 19.58.03.gif Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 20.03.46.gif


    依次为:抽样优化前、抽样优化后


    那么,怎么做的呢?


    最开始看到这个需求的时候,当时觉得可行性比较低。因为需求里面针对图谱的方案以及细节都特别模糊;不敢承诺各种功能和排期,所以先做了一轮比较完整的系分,增加一些说话的底气🫣


    📱 第一步:同类产品调研


    因为设计同学的灵感来自于 大众点评APP 上面的「美食地图」,所以第一步就是做了一次「同类产品调研」,仔细去看了一下 「美食地图」上究竟有哪些花样,有哪些体验优化的小细节,不看不知道,一看发现细节原来这么多啊 🤕:


    图表和卡片的交互联动点抽样展示列表视图和卡片视图可切换交互时卡片自动折叠散点懒加载上滑直接唤起详情页
    21.gif22.gif1658280205580-84108c85-793b-4318-89af-7504f3517613.gif24.gif25.gif1658280765505-5eb8bb05-30c5-45a0-bd81-9f494267b843.gif

    做完这一步之后,大概能够知道自己距离“成品”有多远的距离,方便自己评估工期;另外还可以在系分评审的时候把这些细节提出来,防止临近发布了突然发现某个交互逻辑有个致命的漏洞(别问我怎么知道的,要命的)。
    这波调研之后,最终我们在实现上致敬了「美食地图」50% 的体验细节优化 (狗头)。


    ⚙️ 第二步:功能点分析


    第二步就是从需求本身的角度做功能点的分析,这样可以方便我们拆分组件,为后续做分层设计打下基础,明白哪些是需要支持可扩展的。这一步大家都熟悉,就不赘述了:
    image.png


    📦 第三步:通用化设计


    有了功能点的分析之后,就可以进行通用化的设计了,这就来到了喜闻乐见的沉淀组件的设计环节 🌝


    我们希望这个功能不仅仅是纯业务代码**,期望下次能够复用大部分核心功能 **(理想很丰满),所以在系分的时候是往通用化的方向去设计的,这里主要做了三件事情:分层设计概念标准化核心流程定义



    1. 分层设计


    拆的逻辑是按最基础的 M(数据层) - C(控制层) - V(视图层) 拆分的。


    image.png


    有了分层设计和功能点分析之后,就可以知道哪些应该放到组件内,哪些接口应该被抽象成通用接口,哪些应该保留扩展性供使用者自己来定义,就可以画个表格了,一一决定哪些模块应该放到组件内:
    image.png



    1. 概念标准化


    下面来到造词环节,把一些常用的概念都定义成一个个名字,这样方便和后端、设计协同的时候效率更高,同时也方便自己定义清楚各个模型(类)。(这里其实取名越贴切越形象越好,有点考验语言能力了属实是)
    image.png



    1. 核心流程定义


    这一步是脑补环节,在脑子里跑一遍整体的流程,也是整个需求最核心的流程,比如这里会分成四种流程:初始化流程 、散点图交互流程、底部卡片交互流程、顶部tab交互流程


    进而可以将四种流程里面的各节点做一些归类,比如都会有图表渲染、数据补全、卡片渲染这些共同的节点,而这些节点就可以实现成具体模型里的具体方法。


    image.png


    🌝 第四步:难点分析


    根据上面拆分的各模块,列出哪些点是实现有困难的,耗时长的。这样就可以在评估工期的时候多 Battle 一下,还能砍砍需求,更可以让底层引擎/SDK来突破这些难点(比如找 F2 的核心开发者) :


    image.png
    image.png


    📃 最后一步:


    按照上述的设计进行代码编写。


    难点实现


    1. 移动端的图表手势交互体验优化


    开发之初,F2 只支持单轴(x或者y)的平移缩放,也不支持全方向交互;在 swipe 上的体验也不太好(阻尼感很强),所以在项目开发过程中, F2 完成了很多体验优化,打磨出很多细致入微的良好体验:



    • X轴、Y轴可同时开启平移、缩放

    • swiper 体验效果优化

    • 移出可视区之后的蒙层遮挡能力(view-clip)

    • zIndex 元素层叠渲染

    • 平移缩放性能优化


    2. 气泡抽样展示优化


    因为散点图上的点在初始化的缩放比例下分布非常密集,所以如果每个点上面都绘制一个气泡的话,就会显得密密麻麻的,根本无从下手(如下图1所示)。针对这样的问题,做了「气泡抽样展示」的优化。


    image.png


    实现方式上就是渲染前遍历所有的点,如果在这个点周围某个半径距离之内有其他点,那么就认为这个点是脏点(dirty point),最后筛选出所有“干净”的点进行气泡展示。


    如下图图1所示,灰色点(右上角)是干净点,而灰白色的点(偏中间的位置)因为其在圆圈半径范围之内有其他点存在,所以这个点是脏点。


    image.png



    多提一句,这样的过滤方式会使得密集区域的点都不会展示气泡,后续会进行优化。



    3. 获取到可视区内的所有点


    image.png
    由于做了气泡抽样展示,所以上图中的底部卡片只会展示用户可视区内散点图上有气泡的点(细心的盆友可以发现,散点图上有两种点,一种是带气泡的交互点,一种是不带气泡的缩略点)。那么就需要一个获取「可视区内所有的点」,实现思路如下:


    - 监听 PanEnd(平移结束)、PinchEnd(缩放结束), SwipeEnd(横扫结束)的事件
    - 获取到平移/缩放/横扫之后最新的 scales
    - 根据最新的 scales 里面的 x、y 的 range 过滤一遍图表原数据
    - 将脏点从上一步的结果过滤出去
    - 底部卡片根据上一步的结果进行渲染展示
    - 结束



    // 根据当前的缩放比例,拿到「可视区」范围内的数据
    function getRecordsByZoomScales(scales, data) {
    const { x: xScale, y: yScale } = scales;
    const { field: xField, min: xMin, max: xMax } = xScale;
    const { field: yField, min: yMin, max: yMax } = yScale;

    return data.filter((record) => {
    const isInView =
    record[xField] >= xMin &&
    record[xField] <= xMax &&
    record[yField] >= yMin &&
    record[yField] <= yMax;

    return isInView;
    });
    }


    // 使用时
    export default props => {
    // 图表原数据
    const { data } = props;

    function handlePanEnd (scales, data) {
    // 手动高亮下面这一行
    getRecordsByZoomScales(scales, data);
    }

    return (
    <ReactCanvas>
    <Chart>
    {/* ... */}
    <ScrollBar onPanEnd={handlePanEnd}/>
    </Chart>
    </ReactCanvas>

    )

    }

    4. 数据懒加载


    image.pngimage.png
    底部卡片的数量是由散点图上点的数量决定的,而每张卡上都有不少的数据量(基金产品信息、指数信息、标签信息),所以不能一次性就把所有点里关联的数据都查询出来(会导致接口返回数据过多)。


    这里采取的是懒加载的方式 ,每次只在交互后查询相邻 N+2/N-2 张的卡片数据,并且增加了一份内存缓存来存储已经查询过的卡片数据:


    image.png


    基本的流程图如下:


    - 触发散点图交互/滑动底部卡片
    - 读取缓存,过滤出没有缓存过的卡片
    - 发起数据调用,获取到卡片的数据
    - 写入缓存
    - 更新卡片数据,返回
    - 更新卡片视图,渲染完成

    实际线上效果


    项目上线之后,我们发现散点图区域的交互率(包含平移,缩放)非常高,可以看出用户对新类型的选基工具抱有新鲜感,也乐于去进行探索;也有部分用户能够通过工具完成决策或者进行产品之间的详细对比(即点击底部卡片上的详情按钮),起到了一个工具类产品的作用 🌝 。


    致谢


    感谢 AntV 以及 F2 对移动端图表交互能力的支持。


    作者:支付宝体验科技
    来源:juejin.cn/post/7176891015112949819
    收起阅读 »

    Vue3为什么推荐使用ref而不是reactive

    web
    为什么推荐使用ref而不是reactive reactive本身具有很大局限性导致使用过程需要额外注意,如果忽视这些问题将对开发造成不小的麻烦;ref更像是vue2时代option api的data的替代,可以存放任何数据类型,而reactive声明的数据类...
    继续阅读 »

    为什么推荐使用ref而不是reactive



    reactive本身具有很大局限性导致使用过程需要额外注意,如果忽视这些问题将对开发造成不小的麻烦;ref更像是vue2时代option apidata的替代,可以存放任何数据类型,而reactive声明的数据类型只能是对象;



    先抛出结论,再详细说原因:非必要不用reactive! (官方文档也有对应的推荐)


    官方原文:建议使用 ref() 作为声明响应式状态的主要 API。


    最懂Vue的人都这么说了:推荐ref!!!!!!


    image.png


    reactiveref 对比


    reactiveref
    ❌只支持对象和数组(引用数据类型)✅支持基本数据类型+引用数据类型
    ✅在 <script><template> 中无差别使用❌在 <script><template> 使用方式不同(script中要.value)
    ❌重新分配一个新对象会丢失响应性✅重新分配一个新对象不会失去响应
    能直接访问属性需要使用 .value 访问属性
    ❌将对象传入函数时,失去响应✅传入函数时,不会失去响应
    ❌解构时会丢失响应性,需使用toRefs❌解构对象时会丢失响应性,需使用toRefs


    • ref 用于将基本类型的数据(如字符串、数字,布尔值等)和引用数据类型(对象) 转换为响应式数据。使用 ref 定义的数据可以通过 .value 属性访问和修改。

    • reactive 用于将对象转换为响应式数据,包括复杂的嵌套对象和数组。使用 reactive 定义的数据可以直接访问和修改属性。


    原因1:reactive有限的值类型


    reactive只能声明引用数据类型(对象)


    let  obj = reactive({
      name: '小明',
      age : 18
    })

    ref既能声明基本数据类型,也能声明对象和数组;



    Vue 提供了一个 ref() 方法来允许我们创建可以使用任何值类型的响应式 ref



    //对象
    const state = ref({})
    //数组
    const state2 = ref([])

    原因2:reactive使用不当会失去响应:



    reactive一时爽,使用不恰当的时候失去响应泪两行,开开心心敲代码过程中,会感叹!!咦?怎么不行?为什么这么赋值失去响应了? 辣鸡reactive!!! 我要用 ref 👉👉yyds



    1. 给reactive赋一整个普通对象/reactive对象


    通常在页面数据回显时,需要将AJAX请求获取的对象直接赋值给响应式对象,如果操作不当就导致reactive声明的对象失去响应





    • 赋值一个普通对象


      let state = reactive({ count: 0 })
      //这个赋值将导致state失去响应
      state = {count: 1}



    • 赋值一个reactive对象



      如果给reactive的响应式对象赋值普通对象会失去响应,那么给它赋值一个reactive的响应式对象不就行了吗?下面试试看





    <template>
    {{state}}
    </template>    

    <stcirpt setup>
    const state = reactive({ count: 0 })
    //nextTick异步方法中修改state的值
    nextTick(() => {
    //并不会触发修改DOM ,说明失去响应了
    state = reactive({ count: 11 });
    });
    </stcirpt>

    nexTick中给state赋值一个reactive的响应式对象,但是DOM并没有更新!


    解决方法:



    1. 不要直接整个对象替换,对象属性一个个赋值


      let state = reactive({ count: 0 })
      //state={count:1}
      state.conut = 1



    2. 使用Object.assign


      let state = reactive({ count: 0 })
      // state = {count:1}   state失去响应
      state = Object.assign(state , {count:1})



    3. 使用ref定义对象



      非必要不用reactive



      let state = ref({ count: 0 })
      state.value={count:1}



    为什么同样是赋值对象ref不会失去响应而reactive会?

    ref 定义的数据(包括对象)时,返回的对象是一个包装过的简单值,而不是原始值的引用;



    就和对象深拷贝一样,是将对象属性值的赋值



    reactive定义数据(必须是对象),reactive返回的对象是对原始对象的引用,而不是简单值的包装。



    类似对象的浅拷贝,是保存对象的栈地址,无论值怎么变还是指向原来的对象的堆地址;


    reactive就算赋值一个新的对象,reactive还是指向原来对象堆地址



    2.将reactive对象的属性-赋值给变量(断开连接/深拷贝)


    这种类似深拷贝不共享同一内存地址了,只是字面量的赋值;对该变量赋值也不会影响原来对象的属性值



    let state = reactive({ count: 0 })
    //赋值
    // n 是一个局部变量,同 state.count
    // 失去响应性连接
    let n = state.count
    // 不影响原始的 state
    n++
    console.log(state.count) //0

    有人就说了,既然赋值对象的属性,那我赋值一整个对象不就是浅拷贝了吗?那不就是上面说的给响应式对象的字面量赋一整个普通对象/reactive对象这种情况吗?这种是会失去响应的


    3.直接reactive对象解构时


    • 直接解构会失去响应


    let state = reactive({ count: 0 })
    //普通解构count 和 state.count 失去了响应性连接
    let { count } = state
    count++ // state.count值依旧是0

    解决方案:



    • 使用toRefs解构不会失去响应



      使用toRefs解构后的属性是ref的响应式数据





    const state = reactive({ count: 0 })
    //使用toRefs解构,后的属性为ref的响应式变量
    let { count } = toRefs(state)
    count.value++ // state.count值改变为1

    建议: ref一把梭



    当使用reactive时,如果不了解reactive失去响应的情况,那么使用reactive会造成很多困扰!



    推荐使用ref总结原因如下:




    1. reactive有限的值类型:只能声明引用数据类型(对象/数组)




    2. reactive在一些情况下会失去响应,这个情况会导致数据回显失去响应(数据改了,dom没更新)


      给响应式对象的字面量赋一整个普通对象,将会导致reactive声明的响应式数据失去响应


      <template>
        {{state.a}}
        {{state.b}}
        {{state.c}}
      </template>

      <script>
      let state = reactive({ a:1,b:2,c:3 })
      onMounted(()=>{
          //通AJAX请求获取的数据,回显到reactive,如果处理不好将导致变量失去响应,
         //回显失败,给响应式数据赋值一个普通对象
         state = { a:11,b:22,c:333 }
        //回显成功,一个个属性赋值  
         state.a = 11
         state.b = 22
         state.c = 33
      })
      </script>

      上面这个例子如果是使用ref进行声明,直接赋值即可,不需要将属性拆分一个个赋值


      使用ref替代reactive:


      <template>
        {{state.a}}
        {{state.b}}
        {{state.c}}
      </template>

      <script>
      let state = ref({ a:1,b:2,c:3 })
      onMounted(()=>{
         //回显成功
         state.value = { a:11,b:22,c:333 }
      })
      </script>



    3. ref适用范围更大,声明的数据类型.基本数据类型和引用数据类型都行




    虽然使用ref声明的变量,在读取和修改时都需要加.value小尾巴,但是正因为是这个小尾巴,我们review代码的时候就很清楚知道这是一个ref声明的响应式数据;


    ref的.value小尾巴好麻烦!


    ref声明的响应式变量携带迷人的.value小尾巴,让我们一眼就能确定它是一个响应式变量!虽然使用ref声明的变量,在读取和修改时都需要加.value小尾巴,但是正因为是这个小尾巴,我们review代码的时候就很清楚知道这是一个ref声明的响应式数据;


    可能有些人不喜欢这个迷人小尾巴,如果我能自动补全阁下又如何应对?


    volar插件能自动补全.value (强烈推荐!!!!!!!)



    本人推荐ref一把梭,但是ref又得到处.value ,那就交给插件来完成吧!!!





    • valor 自动补全.value (不是默认开启,需要手动开启)




    • 不会有人不知道Vue3需要不能使用vetur要用valor替代吧?不会不会吧? (必备volar插件)




    volar设置自动填充value.gif
    可以看到当输入ref声明的响应式变量时,volar插件自动填充.value 那还有啥烦恼呢? 方便!


    本文会根据各位的提问和留言持续更新;


    @ 别骂了_我真的不懂vue 说(总结挺好的,因此摘抄了):



    reactive 重新赋值丢失响应是因为引用地址变了,被proxy代理的对象已经不是原来那个所以丢失响应了,其实ref也是一样的,当把.value那一层替换成另外一个有着.value的对象也会丢失响应 ref定义的属性等价于reactive({value:xxx})

    另外说使用Object.assign为什么可以更新模板

    Object.assign解释是这样的: 如果目标对象与源对象具有相同的键(属性名),则目标对象中的属性将被源对象中的属性覆盖,后面的源对象的属性将类似地覆盖前面的源对象的同名属性。

    那个解决方法里不用重新赋值,直接Object.assign(state,{count:1})即可,所以只要proxy代理的引用地址没变,就会一直存在响应性



    作者:我要充满正能量
    来源:juejin.cn/post/7270519061208154112
    收起阅读 »

    H5快速上手鸿蒙元服务(前端)

    web
    一、前言 鸿蒙元服务虽然与h5在很多地方虽然有相似之处,但还是有部分不同的地方,鸿蒙服务开发模式更接近与vue2版本,很多写法与其相似。该篇文章主要用于帮助有h5基础的伙伴能够快速上手鸿蒙元服务,并且对个人在开发过程中遇到的一些坑做个总结。 二、开发相关 项目...
    继续阅读 »

    一、前言


    鸿蒙元服务虽然与h5在很多地方虽然有相似之处,但还是有部分不同的地方,鸿蒙服务开发模式更接近与vue2版本,很多写法与其相似。该篇文章主要用于帮助有h5基础的伙伴能够快速上手鸿蒙元服务,并且对个人在开发过程中遇到的一些坑做个总结。


    二、开发相关


    项目目录


    a51bd7a8cf9c80848926b24be2b8a27.jpg


    cd3136d4b9ca22519a4934b79db8d4e.jpg
    前端部分主要看js目录下的文件目录即可,除default目录外,其他文件都是与服务卡片相关的。


    commom:存放公共配置文件方法等

    components:存放公共组件
    i18n:i18n相关

    media:存放静态文件,图片等

    pages:存放页面的目录,包括js,hml,css

    utils:存放工具方法,比如网络请求封装等

    app.js:全局文件,能够在这个文件中定义全局变量,拥有应用级的生命周期函数


    其他关键目录:


    supervisual:低代码相关

    config.json:项目配置相关,包括路由等


    config.json文件


    用于给整个项目进行一些关键配置


    定义路由


    image.png
    这种定义路由的方式,可能开发过微信小程序的伙伴会比较熟悉,在微信小程序中,一般第一个路径即是项目打开的页面,可惜在鸿蒙元服务中没有这个便捷的功能,designWidth用于定义页面以多宽的设计图来绘制,autoDesginWidth设为true,即是系统根据手机自动设置。


    config.json详细配置请看官方文档: developer.harmonyos.com/cn/docs/doc…


    HML


    HML是一套类HTML的标记语言,通过组件,事件构建出页面的内容。页面具备数据绑定、事件绑定、列表渲染、条件渲染和逻辑控制等高级能力,由鸿蒙内部实现。


    <!-- xxx.hml -->
    <div class="container">
    <text class="title">{{count}}</text>
    <div class="box">
    <input type="button" class="btn" value="increase" onclick="increase" />
    <input type="button" class="btn" value="decrease" @click="decrease" />
    <!-- 传递额外参数 -->
    <input type="button" class="btn" value="double" @click="multiply(2)" />
    <input type="button" class="btn" value="decuple" @click="multiply(10)" />
    <input type="button" class="btn" value="square" @click="multiply(count)" />
    </div>
    </div>

    // xxx.js
    export default {
    data: {
    count: 0
    },
    increase() {
    this.count++;
    },
    decrease() {
    this.count--;
    },
    multiply(multiplier) {
    this.count = multiplier * this.count;
    }
    };
    /* xxx.css */
    .container {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    left: 0px;
    top: 0px;
    width: 454px;
    height: 454px;
    }
    .title {
    font-size: 30px;
    text-align: center;
    width: 200px;
    height: 100px;
    }
    .box {
    width: 454px;
    height: 200px;
    justify-content: center;
    align-items: center;
    flex-wrap: wrap;
    }
    .btn {
    width: 200px;
    border-radius: 0;
    margin-top: 10px;
    margin-left: 10px;
    }

    看这段代码是不是就觉得很亲近了,在hml中通过“{{}}”的形式绑定数据,用@和on的方法来绑定事件,同时支持冒泡、捕获等方式。


    列表渲染for


    <!-- xxx.hml -->
    <div class="array-container" style="flex-direction: column;margin: 200px;">
    <!-- div列表渲染 -->
    <!-- 默认$item代表数组中的元素, $idx代表数组中的元素索引 -->
    <div for="{{array}}" tid="id" onclick="changeText">
    <text>{{$idx}}.{{$item.name}}</text>
    </div>
    <!-- 自定义元素变量名称 -->
    <div for="{{value in array}}" tid="id" onclick="changeText">
    <text>{{$idx}}.{{value.name}}</text>
    </div>
    <!-- 自定义元素变量、索引名称 -->
    <div for="{{(index, value) in array}}" tid="id" onclick="changeText">
    <text>{{index}}.{{value.name}}</text>
    </div>
    </div>


    tid等于vue中的key,id即为array每一项中的唯一属性,需要注意的是,与vue不同,在鸿蒙元服务中,tid是必须的,如果没有tid可能会引起运行异常的情况。


    条件渲染if和show


    <!-- xxx.hml -->
    //if
    <div class="container">
    <button class="btn" type="capsule" value="toggleShow" onclick="toggleShow"></button>
    <button class="btn" type="capsule" value="toggleDisplay" onclick="toggleDisplay"></button>
    <text if="{{visible}}"> Hello-world1 </text>
    <text elif="{{display}}"> Hello-world2 </text>
    <text else> Hello-World </text>
    </div>


    //show
    <!-- xxx.hml -->
    <div class="container">
    <button class="btn" type="capsule" value="toggle" onclick="toggle"></button>
    <text show="{{visible}}" > Hello World </text>
    </div>


    if和show相当于vue中的v-if和v-show,原理也一样。


    自定义组件使用(props和emit传值)


    <!-- template.hml -->
    <div class="item">
    <text>Name: {{name}}</text>
    <text>Age: {{age}}</text>
    <text class="text-style" onclick="childClicked" id="text" ref="animator">点击这里查看隐藏文本</text>
    </div>

    <!-- template.js -->
    export default {
    props:{
    name,
    age
    contentList
    }
    childClicked () {
    //获取标签对象
    //this.$element("text");
    //this.$element("text").currentOffset().y 获取属性;
    //通过ref的形式来获取
    //this.$refs.animator
    this.$emit('eventType1',{text:'123'});
    },
    };
    <!-- index.hml -->
    //注册
    <element name='comp' src='../../common/template.hml'></element>
    <div>
    //使用
    <comp name="Tony" age="18" content-list="contentList" @event-type1="textClicked"></comp>
    </div>

    <!-- template.js -->
    export default {
    textClicked (e) {
    //e.detail 拿到传过来的数据 e.detail.text
    },
    };


    注意:组件传递props和emit属性时,强制使用横杆连接的变量名进行传递,接收时,需要使用驼峰名进行接收,通过e.detail拿到emit传过来的参数,通过$element()方法或ref的形式来获取元素对象,其他用法基本和vue2相同。



    生命周期和插槽等用法参考官方文档developer.harmonyos.com/cn/docs/doc…


    通用事件


    developer.harmonyos.com/cn/docs/doc…


    内部系统组件


    image.png
    常用的组件包括:

    容器组件:dialog、div、滚动组件用于上拉加载(list、list-item、list-item-group)、popup、轮播组件(swiper)

    基础组件:image、text、span、input、label


    <div>
    <text>123</text>
    </div>


    注意:

    1.div组件内部不能够直接嵌入文字,需要通过text组件进行包裹

    2.list组件在相同方向的滚动不能嵌套使用,否则会造成滚动异常

    3.image标签有些图片格式不支持,需要转换为可支持的格式



    CSS


    华为鸿蒙元服务不支持less,sass等预编译语言,只支持css,相对于h5来说,还做了部分阉割,有些属性在h5能用,在鸿蒙元服务确用不了。


    元素标签默认样式


    需要注意的是,在元服务中,所有的div标签都是一个flex盒子,所以在我们使用div的时候,如果是纵向布局,那我们需要去手动改变flex-direction: column,更改主轴方向。


    //hml
    <div id="tabBarCon">
    <div id="tab1">
    </div>

    <div id="tab2" onclick="handleJumpToCart">
    </div>

    <div id="tab3" onclick="handleJumpToMine">
    </div>

    </div>
    //css
    .tabBarCon{
    flex-direction:column;
    }

    元素选择器


    image.png


    image.png
    只支持部分选择器和部分伪类选择器,像h5中的伪元素选择器都是不支持的,也不支持嵌套使用,由于不存在伪元素选择器,所以遇到有时候一些特殊场景时,我们只能在hml中去判断元素索引来添加动态样式。


    属性与h5中的差异


    属性鸿蒙元服务h5
    position只支持absolute、relative、fixed支持absolute、relative、fixed、sticky
    background渐变linear-gradient(134.27deg, #ff397e 0%, #ff074c 98%),渐变百分比不支持带小数点支持
    长度单位只支持px、百分比,不支持rem、em、vw、vhpx、百分比、rem、em、vw、vh
    多行文字省略text-overflow: ellipsis; max-lines: 1;(只能用于text组件)单行和多行使用属性不同

    JS


    特点:

    1.支持ES6

    2.用法和vue2相似


    // app.js
    export default {
    onCreate() {
    console.info('Application onCreate');
    },
    onDestroy() {
    console.info('Application onDestroy');
    },
    globalData: {
    appData: 'appData',
    appVersion: '2.0',
    },
    globalMethod() {
    console.info('This is a global method!');
    this.globalData.appVersion = '3.0';
    }
    };
    // index.js页面逻辑代码
    export default {
    data: {
    appData: 'localData',
    appVersion:'1.0',
    },
    onInit() {
    //获取全局属性
    this.appData = this.$app.$def.globalData.appData;
    this.appVersion = this.$app.$def.globalData.appVersion;
    },
    invokeGlobalMethod() {
    this.$app.$def.globalMethod();
    },
    getAppVersion() {
    this.appVersion = this.$app.$def.globalData.appVersion;
    }
    }

    data:定义变量

    onInit:生命周期函数

    getAppVersion:方法,不需要写在methods里面,直接与生命周期函数同级
    this.app.app.def:可以拿到全局对象,


    导入导出


    支持ESmodule


    //import
    import router from '@ohos.router';
    //export
    export const xxx=123;

    应用级生命周期


    image.png


    页面级生命周期


    image.png


    网络请求


    使用@ohos.net.http内置模块即可,下面是对网络请求做了一个简单封装,使用的时候直接导入,调用相应请求方法即可,可惜的鸿蒙元服务目前没法进行抓包,所以网络请求调试的时候只能通过打断点的形式进行调试。


    import http from '@ohos.net.http';


    import { invokeShowLogin } from '../common/invoke_user';

    export default {
    interceptors(response) {
    const result = JSON.parse(response.result || {});
    const {code,errno} = result
    if (errno === 1024 || code === 1005) {
    return invokeShowLogin();
    }

    return result;
    },

    get(url, data) {
    return http.createHttp().request(
    // 填写http请求的url地址,可以带参数也可以不带参数。URL地址需要开发者自定义。请求的参数可以在extraData中指定
    url,
    {
    method: http.RequestMethod.GET, // 可选,默认为http.RequestMethod.GET
    // 开发者根据自身业务需要添加header字段
    header: {
    'Content-Type': 'application/json'
    },
    // 当使用POST请求时此字段用于传递内容
    extraData: data,
    connectTimeout: 10*1000,
    readTimeout: 10*1000,
    }
    ).then(res=>{
    return this.interceptors(res);
    });
    },

    post(url, data) {
    return http.createHttp().request(
    // 填写http请求的url地址,可以带参数也可以不带参数。URL地址需要开发者自定义。请求的参数可以在extraData中指定
    url,
    {
    method: http.RequestMethod.POST, // 可选,默认为http.RequestMethod.GET
    // 开发者根据自身业务需要添加header字段
    header: {
    'Content-Type': ' application/x-www-form-urlencoded'
    },
    // 当使用POST请求时此字段用于传递内容
    extraData: data,
    connectTimeout: 10*1000, // 可选,默认为60s
    readTimeout: 10*1000, // 可选,默认为60s
    }
    ).then(res=>{
    return this.interceptors(res);
    });
    },

    postJson(url, data) {
    return http.createHttp().request(
    // 填写http请求的url地址,可以带参数也可以不带参数。URL地址需要开发者自定义。请求的参数可以在extraData中指定
    url,
    {
    method: http.RequestMethod.POST, // 可选,默认为http.RequestMethod.GET
    // 开发者根据自身业务需要添加header字段
    header: {
    'Content-Type': 'application/json'
    },
    // 当使用POST请求时此字段用于传递内容
    extraData: data,
    connectTimeout: 10*1000, // 可选,默认为60s
    readTimeout: 10*1000, // 可选,默认为60s
    }
    ).then(res=>{
    return this.interceptors(res);
    })
    }
    }

    数据存储


    只有本地持久化存储这种方式,关闭应用,数据不会丢失。


    storage.set({
    key: 'loginInfo',
    value: JSON.stringify({
    uid, skey
    }),
    });
    storage.get({
    key: 'userInfo',
    value: JSON.stringify(userInfo),
    });

    路由跳转


    <!-- index.hml -->
    <div class="container">
    <text class="title">This is the index page.</text>
    <button type="capsule" value="Go to the second page" class="button" onclick="launch"></button>
    </div>

    // index.js
    import router from '@ohos.router';
    export default {
    launch() {
    router.push ({
    url: 'pages/detail/detail',
    //携带的参数
    params:{a:123}
    });
    //router.back()
    //router.replace()
    },
    }.
    // detail.js
    import router from '@ohos.router';
    export default {
    data:{
    a:''
    }
    onInit(){
    //页面携带过来的参数可以直接使用
    //this.a
    }
    }

    官方文档链接



    1. config.json:developer.harmonyos.com/cn/docs/doc… &developer.harmonyos.com/cn/docs/doc…

    2. http请求:developer.harmonyos.com/cn/docs/doc…

    3. hml:developer.harmonyos.com/cn/docs/doc…

    4. css:developer.harmonyos.com/cn/docs/doc…

    5. js:developer.harmonyos.com/cn/docs/doc…

    6. 生命周期:developer.harmonyos.com/cn/docs/doc…

    7. 目录结构:developer.harmonyos.com/cn/docs/doc…


    作者:前端小萌新y
    来源:juejin.cn/post/7275945995609964563
    收起阅读 »

    我来聊聊面向模板的前端开发

    web
    在软件开发中,研发效率永远是开发人员不断追求的主题之一。于公司而言,在竞争激烈的互联网行业中,产出得快和慢也许就决定着公司的生死存亡;于个人而言,效率高了就可以少加班,多出时间去提升自己、发展爱好、陪伴家人,工作、生活两不误。 提升效率的途径,无外乎就是「方法...
    继续阅读 »

    在软件开发中,研发效率永远是开发人员不断追求的主题之一。于公司而言,在竞争激烈的互联网行业中,产出得快和慢也许就决定着公司的生死存亡;于个人而言,效率高了就可以少加班,多出时间去提升自己、发展爱好、陪伴家人,工作、生活两不误。


    提升效率的途径,无外乎就是「方法」和「工具」。以一个开发者的思维来想,就是将工作内容进行总结、归纳,从一组相似的工作内容中提炼共同点,抽象出解决这一类问题的方法,从而造出便于在今后的工作中更为快速解决这类问题的工具。这个「工具」可以是个函数、组件、中间件、插件,也可以是 IDE、其他开发工具的扩展,甚至是语言。


    面向组件


    在现代前端开发中,如果去问一个业务前端开发:「如何提升团队开发效率?」对方所回答的内容中,极有可能会出现「组件库」。没错,在前端工程化趋近完善的今天,在近几年 React、Vue 等组件化库/框架的影响下,面向组件开发的思维方式早已深入人心。


    组件库提效有限


    现在,组件库已经是一个前端团队的必备设施了,长远来看,团队一定且必须要有自己的组件库。开源的第三方组件库再好,对于一家企业的前端团队来说也只是短期用来充饥的,因为它们无法完全满足一家公司的业务场景,并且出于多终端支持的考虑,必定要进行二次开发或者自研。


    组件库有了,团队和公司中推广的效果也不错,绝大多数的人都在用。使用组件开发页面相对 jQuery 时代要每块功能区都得从

    等 HTML 标签码起来说确实提升了效率,然而有限;要搞出页面需要反复去引入组件,然后组合拼装出来,就像工厂流水线上的工人拼装零件,仍然要去做很多重复动作。


    只要觉得当前的开发方式重复的动作多了,就代表还能继续提效,得想个法子减少重复无意义动作。


    面向组件的开发方式,是现代前端页面开发提效的初级阶段,也是一个团队所要必经的阶段。


    更高层面的提效


    在之前写的文章中有段话——



    组件可以很简单,也可以很复杂。按照复杂程度从小到大排的话,可以分为几类:



    1. 基础组件;

    2. 复合组件;

    3. 页面;

    4. 应用。


    对,不用揉眼睛,你没有看错!


    站在更高的角度去看,「页面」和「应用」也是一种「组件」,只不过它们更为复杂。在这里我想要说的不是它们,而是「基础组件」和「复合组件」。



    文中提到了「页面」和「应用」也可以看作是种「组件」。虽然与当时的想法有些差异,但本文的内容就是要在那篇文章的基础上简单聊聊在「页面」层面的提效。


    一般来说,「页面」是用户所能看到的最大、最完整的界面,如果能在这个层面有个很好的抽象方案,在做业务开发时与单纯地面向组件开发相比,应该会有更大的提效效果。


    GUI 发展了几十年,人机交互的图形元素及布局方式已经相对固定,只要不是出现像 Google Glass 之类的革命性交互设备,就不会发生重大改变。在业务开发中界面形式更是千篇一律,尤其是 web 页面,尤其是中后台系统的 web 页面,一定可以通过什么方式来将这种「千篇一律」进行抽象。


    试着来回想下,自己所做过的中后台系统的绝大部分页面是不是我所描述的这样——


    页面整体是上下或左右布局。如果是上下布局的话,上面是页头,下面的左侧可能有带页面导航的侧边栏,或者没有侧边栏直接将页面导航全部集中在页头中,剩余区域是页面主体部分,承载着这个页面的主要数据和功能;如果是左右布局,左侧毋庸置疑就是有页面导航的侧边栏,页头跑到了右侧上面,其余是页面主体。


    中后台系统的主要功能就是 CRUD,即业务数据的增删改查,相对应的页面展现及交互形式就是列表页、表单页和详情页。列表页汇总了所有业务数据的简要信息,并提供了数据的增、删、改和更多信息查看的入口;表单页肩负着数据新增和修改的功能;详情页能够看到一条业务数据记录最完整的信息。


    每新增一个业务模块,就要又写一遍列表页、表单页和详情页……反复做这种事情有啥意思呢?既然这三种页面会反复出现,那干脆封装几个页面级别的组件好了,有新需求的时候就建几个页面入口文件,里面分别引入相应的页面组件,传入一些 props,完活儿!


    这种方式看起来不错,然而存在几个问题:



    • 没有描述出页面内容的结构,已封装好的页面组件对于使用者来说算是个黑盒子,页面内容是什么结构不去看源码不得而知;

    • 如果新需求中虽然需要列表页、表单页和详情页,但与已封装好的能够覆盖大部分场景的相关组件所支持的页面有些差异,扩展性是个问题;

    • 每来新需求就要新建页面入口文件然后在里面引入页面组件,还是会有很多无意义重复动作和重复代码,时间长了还是觉得烦。


    我需要一种既能看一眼就理解内容结构和关系,又具备较好扩展性,还能减少重复代码和无意义动作的方式——是的,兜了一个大圈子终于要进入正题了——面向模板开发。


    面向模板


    面向模板的前端开发有三大要素:模板;节点;部件。


    富有表达力的模板


    我所说的「模板」的主要作用是内容结构的描述以及页面的配置,观感上与 XHTML 相近。它主要具备以下几个特征:



    1. 字符全部小写,多单词用连接符「-」连接,无子孙的标签直接闭合;

    2. 包含极少的具备抽象语义的标签的标签集;

    3. 以特定标签的特定属性的形式支持有限的轻逻辑。


    为什么不选择用 JSON 或 JSX 来描述和配置页面?因为模板更符合直觉,更易读,并且中立。用模板的话,一眼就能几乎不用思考地看出都有啥,以及层级关系;如果是 JSON 或 JSX,还得在脑中进行转换,增加心智负担,并且拼写起来相对复杂。Vue 上手如此「简单」的原因之一,就是它「符合直觉」的设计。


    要使用模板去描述页面的话,就得自定义一套具有抽象语义的标签集。


    页面的整体布局可以用如下模板结构去描述:


    <layout>
    <header>
    <title>欧雷流title>
    <navs />
    header>
    <layout>
    <sidebar>
    <navs />
    sidebar>
    <content>...content>
    layout>
    <footer>...footer>
    layout>

    看起来是不是跟 HTML 标签很像?但它们并不是 HTML 标签,也不会进行渲染,只是用来描述页面的一段文本。


    整体布局可以描述了,但承载整个页面的主要数据和功能的主体部分该如何去描述呢?


    在上文中提到,我们习惯将中后台系统中与数据的增删改查相对应的页面称为「列表页」、「表单页」和「详情页」。虽然它们中都带有「页」,但真正有区别的只是整个页面中的一部分区域,通常是页面主体部分。它们可以被分别看成是一种视图形式,所以可以将称呼稍微改变一下——「列表视图」、「表单视图」和「详情视图」。一般情况下,表单视图和详情视图长得基本一样,就是一个能编辑一个不能,可以将它们合称为「表单/详情视图」。


    「视图」只描述了一个数据的集合该展示成啥样,并没有也没法去描述每个数据是什么以及长啥样,需要一个更小粒度的且能够去描述每个数据单元的概念——「字段」。这样一来,用来描述数据的概念和模板标签已经齐活儿了:


    <view>
    <field name="name" label="姓名" />
    <field name="gender" label="性别" />
    <field name="age" label="年龄" />
    <field name="birthday" label="生日" />
    view>

    虽然数据能够描述了,但还有些欠缺:表单/详情视图中想将字段分组展示没法描述;对数据的操作也没有描述。为了解决这两个问题,再引入「分组」和「动作」。这下,表单/详情视图的模板看起来会是这样:


    <view>
    <group title="基本信息">
    <field name="name" label="姓名" />
    <field name="gender" label="性别" />
    <field name="age" label="年龄" />
    <field name="birthday" label="生日" />
    group>
    <group title="宠物">
    <field name="dogs" label="🐶" />
    <field name="cats" label="🐱" />
    group>
    <action ref="submit" text="提交" />
    <action ref="reset" text="重置" />
    <action ref="cancel" text="取消" />
    view>

    模板很好地解决了内容结构描述和配置的问题,但如何去动态地调整结构和更改配置呢?在平常的业务页面开发时也许不会太凸显出问题,但碰到流程表单设计或页面可视化编辑这种灵活性很高的需求时,问题就会被暴露出来了。


    充满控制力的节点


    在这里,我要将定义好的标签集所拼成的模板解析成节点树,通过更改树的结构和节点的属性去影响页面最终的呈现效果。每个节点都会有节点的基本信息、对应标签的属性和一些节点操作方法:


    {
    name: "field",
    tag: "field",
    attrs: {
    name: "name",
    label: "姓名"
    },
    parent: {},
    children: [],
    remove: function() {},
    insert: function() {}
    }

    在页面模板化且节点化之后,理想情况下,页面长啥样已经不受如 React、Vue 等运行时技术栈的束缚,控制权完全在解析模板所生成的节点树上,要想改变页面的视觉效果时只需更改节点即可。


    极具表现力的部件


    页面内容的描述通过模板来表达了,页面内容的控制权集中到节点树中了,那么页面内容的呈现在这种体系下应该如何去做呢?负责这块的,就是接下来要说的面向模板开发的第三大要素——部件。


    「部件」这个词不新鲜,但在我所说的这个面向模板开发的体系中的含义,需要被重新定义一下:「部件」是一个可复用的,显示的信息排列可由用户改变的,可以进行交互的 GUI 元素。


    在这个面向模板开发的体系中,模板和节点树完全是中立的,即不受运行时的技术栈所影响;而部件是建立在运行时技术栈的基础之上,但不必限于同一个技术栈。也就是说,可以使用 React 组件,也可以用 Vue 组件。


    每个部件在使用前都需要注册,然后在模板中通过 widget 属性引用:


    <view widget="form">
    <group title="基本信息" widget="fieldset">
    <field name="name" label="姓名" widget="input" />
    <field name="gender" label="性别" widget="radio" />
    <field name="age" label="年龄" widget="number" />
    <field name="birthday" label="生日" widget="date-picker" />
    group>
    <group title="宠物" widget="fieldset">
    <field name="dogs" label="🐶" widget="select" />
    <field name="cats" label="🐱" widget="select" />
    group>
    <action ref="submit" text="提交" widget="button" />
    <action ref="reset" text="重置" widget="button" />
    <action ref="cancel" text="取消" widget="button" />
    view>

    这样,一个面向模板开发的普通表单页出来了!


    思想总结


    面向模板的开发方式很好,能够大幅度提高业务前端开发效率,一定程度上减少了业务系统的搭建速度;作为核心的模板和节点树是保持中立的,大大降低了运行时技术栈的迁移成本,且能够应对多端等场景。


    面向模板的开发方式初期投入成本很高,标签集、模板解析和部件注册与调用机制等的设计和实现需要较多时间,并且这仅仅是视图层,逻辑层也需要做出相应的变化,不能简单地用 props 和事件绑定进行处理了。


    这个体系建成之后,在业务开发上会很简单,但机制理解上会增加部分开发人员的心智负担。


    为了效率,一家公司里的业务前端开发到最后一定是面向模板,而非面向组件。


    作者:欧雷殿
    来源:juejin.cn/post/7274430147126493199
    收起阅读 »

    血压飙升!记一次关于手机号存储的前后端讨论

    起 事情是这样的,有一款产品需要实现国际化的注册需求,界面如下 : 涉及到的手机号码,因为要区分国家区号,所以和手机号是分开的,前端形态是分开存储的。一开始数据确实也是分开存的,么的问题。 本来是很简单的表单需求,结果出了幺蛾子。 承 对于前端来说,这就是两...
    继续阅读 »


    事情是这样的,有一款产品需要实现国际化的注册需求,界面如下 :




    涉及到的手机号码,因为要区分国家区号,所以和手机号是分开的,前端形态是分开存储的。一开始数据确实也是分开存的,么的问题。


    本来是很简单的表单需求,结果出了幺蛾子。



    对于前端来说,这就是两个字段,在表单传值和数据逻辑处理时,直接用两个字段无疑是最直接和简单的:

    const formData = {
    country_code: '86',
    phone: '13345431234'
    ...
    }

    但是,产品侧有一个强需求:手机号和其他的两个字段要做唯一性校验,不但要求三个至少存在一个,而且还要查别的业务库的数据表来校验这个注册人员的唯一性,有一套复杂的逻辑。总结一下就是,后端分开两个字段来判断很麻烦,这个也好理解,有耦合的逻辑重构要成本的。所以后端是这么要求数据的:

    // (86)13345431234
    phone: `(${country_code})${phone}`

    将国家码放在前缀括号里拼接为一个字符串。这样一来,前端的成本就相对高了一些,不但在表单提交时要拼接,而且在列表查询时,要按照括号匹配解析出前缀来动态显示

    const regex = /^\((\d+)\)(\d+)$/;
    const matches = phoneNumber.match(regex);
    // 如果匹配成功,返回国家码和号码的数组
    if (matches) {
    const countrycode = matches[1];
    const number = matches[2];
    return [countrycode, number];
    }

    就算这样,也可以接受,之前也不止一次挖过这种自己拼接自己解析的坑了。但是随后就有点让人血压上升了。



    由于业务扩展,上面的手机号,要传递给一款即时通讯软件 xxx 的 API 来做下发,下发侧则规定了,需要不带括号但是要有国家区号前缀的手机号码

    // 8613345431234
    phone: `${country_code}${phone}`

    这就有问题了,我们直接对接的后端需要括号的,下发侧对接的后端同学不需要括号。


    第一阶段


    血压上升 20%


    讨论让后端换成两个字段存储,这样更灵活,方便拼接;或者给下发侧发数据时去掉括号也可以。后端很为难:”唯一性判断还有其他的逻辑很复杂,有耦合性,拆开字段就要重构,工期又紧,刚完成就要改,误了工期只有我背锅;去掉括号不是不可以,但地方有点多,还要考虑批量数据的性能,而且已经进入测试了,不接受临时需求“。我一想有道理啊,就去问下发侧同学。


    第二阶段


    血压上升 60%


    问下发侧对接的同学,可以处理后再给 API下发吗?果断拒绝。他们也有理由的:”按照软件设计的规范,我们不应该非标处理这个问题的,下发的规范理应就是我们接口的规范,因为规范还有很多,这个地方妥协了,以后肯定还会有更多的非标处理,我们的代码就乱了。“


    我草,我一想之前那那么多屎山的由来,觉得更有理了,那怎么办?弄得好像前端这里没有理了。


    第三阶段


    🔥 血压上升 120% 🔥


    下发侧对接的同学反过来给我们前端提了建议,让我们在注册的时候,把前端两个字段就直接不用括号来拼接给后端,在需要解析的时候,使用特别手段解析出国家码。还好心的给我们搜索了一个解析手机号的 js 库:




    我只能说,我 TM 谢谢你。😭


    前端莫名其妙的凭空多了需求,而且加个库为了给后端擦屁股,还增加了打包体积,出了问题我们得首先背锅,这肯定不行。



    这起事件牵涉的技术问题并没有多难,甚至有些幼稚,但是却蕴含了不少上班族哲学,最主要的一点是:如何学会背锅的问题。锅不是不能背,但是背锅换来的应该是产品成功后更多的功劳回报,而不是这种暗箱操作,老板压根不知道,只有扛不住的人吃暗亏。


    个人觉得,这个应该是架构师或者技术总监的任务,提前了解上下游业务,并确定好字段与数据结构,而不是等用的时候才发现数据对不上。规划好用什么方案来开发。并将方案设计交于老板开会讨论得出定稿。之后再分派人员开发,就不会有这个事件了。



    类似的问题还有很多,前后端与下游计算价格进制与单位不统一,每次都要非标转换;



    最后的结局,产品出面调停,软磨硬泡,最终后端妥协了,夹在前端和下发侧之间, 确实为难。针对前端还是原来的逻辑不动,使用括号存储,他在下发侧拉取数据的时候做转换,去掉括号。其中涉及的性能问题和开发成本也申请了延长工期。


    Happy Ending !!😁


    血压恢复 0%





    方案设计注意事项:

    • 注意产品迁移的问题。同类型的产品,如果针对的客户群体不同,其表现形态可能完全不一样,产品迁移过程中,要提前分析出新产品的业务场景的差异,做好风险分析。
    • 提前确定产品上下游生态,做好风险管控。比如下发侧有 API 对接的,需熟悉其 API 文档与使用限制,并在服务不可用时设计告警方案等。

    作者:小肚肚肚肚肚哦
    链接:https://juejin.cn/post/7275576074589880372
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    现在小厂实习都问的这么难了吗😱「万物心选一面(北京+电商)(vue+小程序)」

    h5是怎么和微信小程序通信的?不同H5页面怎么通信的?webview是谁提供的?web-view 是往小程序中嵌入h5的容器。会自动铺满整个小程序页面。微信小程序和 uni-app 都提供了 web-view 组件。由于简历上有一...
    继续阅读 »

    h5是怎么和微信小程序通信的?不同H5页面怎么通信的?webview是谁提供的?

    1. web-view 是往小程序中嵌入h5的容器。会自动铺满整个小程序页面。微信小程序和 uni-app 都提供了 web-view 组件。
    2. 由于简历上有一段实习是移动端h5开发,让面试官误以为我做过微信小程序的内嵌h5页面,但其实实习中都是公司框架写的代码,页面也是直接嵌入公司的app中的,我并没有微信小程序开发的经验,现在整理答案也是死记硬背,mark一下看到的一篇讲的比较清楚的文章,以后学小程序了再来回顾。
      微信小程序web-view与H5 通信方式探索 - 掘金 (juejin.cn)
    3. h5 页面间通信其实就是前端跨页面通信(吧?)当时第一反应回答的是使用LocalStorage, 面试官又提出用户修改个人信息后返回页面更新信息的情况,回答的是我之前做表单有类似的场景,是向后端提交后在后端更新了数据,回退到原先的页面的时候在 created/activated 的时候获取数据。
      面试官:前端跨页面通信,你知道哪些方法? - 掘金 (juejin.cn)

    什么是微任务?什么是宏任务?


    如果说哪些操作是宏任务,哪些操作是微任务,那大部分同学都是比较清楚的:

    • 宏任务:script(整体代码)/setTimout/setInterval/setImmediate(node 独有)/requestAnimationFrame(浏览器独有)/IO/UI render(浏览器独有)
    • 微任务:process.nextTick(node 独有)/Promise.then()/Object.observe/MutationObserver

    这里面试官还追问了一句,Promise本身是微任务吗


    那这两者有具体的定义吗?老规矩,直接 mdn 开搜。mdn 中可以找到微任务(microtask),但是并没有宏任务或者(macrotask)的信息。但是在在 JavaScript 中通过 queueMicrotask() 使用微任务 - Web API 接口参考 | MDN 中我们可以发现,在文档中只有taskmicrotask,对应的就是事件循环中的任务队列task queue和微任务队列microtask queue


    文档中还提到了JavaScript 中的 promise 和 Mutation Observer API 都使用微任务队列去运行它们的回调函数,想来面试官的意思就是,Promise本身只是一个代理,Promise()是他的构造函数,真正被放进微任务队列的是Promise的then方法中的回调函数。


    文档中对任务和微任务的定义也比较冗长,我想能区分哪些是微任务,哪些是宏任务,说出他们分别会被放在任务队列和微任务队列以及他们的执行顺序(事件循环会持续调用微任务直至队列中没有留存的,再去调用任务队列)应该足够面试了。




    遍历对象有哪些方法,如果是对象的原型链上的属性会不会被遍历到?有什么办法可以排除原型链上的属性?


    直接上代码测试一波:

    Object.prototype.age=18;   // 修改Object.prototype  
    const person ={ name: "小明" };

    // 输出 name, age
    for(key in person){
    console.log(key)
    }

    // 输出 name
    Object.keys(person).forEach(key=>{
    console.log(key);
    })

    // 输出 小明, 18
    for(key in person){
    console.log(person[key])
    }

    // 输出 小明
    Object.values(person).forEach(value=>{
    console.log(value);
    })

    // 输出 name: 小明
    for (const [key, value] of Object.entries(person)) {
    console.log(`${key}: ${value}`);
    }

    很明显,for...in 会遍历到原型链上的属性,Object上的keysvaluesentires方法不会。
    看看 mdn 怎么说:



    for...in 语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性




    Object.keys()  静态方法返回一个由给定对象 自身 的可枚举的字符串键属性名组成的数组。
    Object.entries()  静态方法返回一个数组,包含给定对象 自有 的可枚举字符串键属性的键值对。


    Object.values()  静态方法返回一个给定对象的 自有 可枚举字符串键属性值组成的数组。



    那么如果仍然想使用 for...in 来遍历对象,并且不想要原型链上的属性,我们可以使用 Object.hasOwn 过滤掉它们:

    for (key in person) {
    if (person.hasOwn(key)) {
    console.log(key);
    }
    }


    如果指定的对象自身有指定的属性,则静态方法 Object.hasOwn()  返回 true。如果属性是继承的或者不存在,该方法返回 false



    组件通信有哪些方法?依赖注入的数据是不是响应式的?有什么办法让他保持响应式的?

    • props / $emit
    • $emit / $on (eventBus)
    • provide / inject
    • $attrs / $listeners
    • ref / $ref
    • $parent / $children
    • vuex / pinia

    Vue 组件间通信六种方式(完整版) - 掘金 (juejin.cn)


    vue更新dom是异步还是同步?如何不使用nexttick实现nexttick的功能?vue的更新是哪一种微任务?


    Vue更新DOM是异步的。这意味着我们在修改完data之后并不能立刻获取修改后的DOM元素。Vue需要通过nextTick方法才能获取最新的DOM。


    Vue在调用Watcher更新视图时,并不会直接进行更新,而是把需要更新的Watcher加入到queueWatcher队列里,然后把具体的更新方法flushSchedulerQueue传给nextTick进行调用。nextTick只是单纯通过Promise、setTimeout等方法模拟的异步任务。


    如果你想要不使用nextTick实现nextTick的功能,你可以使用Promise、setTimeout等方法来模拟异步任务。例如,你可以使用 Promise.resolve().then(callback) 或者 setTimeout(callback, 0) 来实现类似于nextTick的功能。


    至于Vue的更新是哪一种微任务,它取决于浏览器兼容性。Vue会根据浏览器兼容性,选用不同的异步策略。例如,如果浏览器兼容Promise,那么Vue就会使用Promise来实现异步更新。如果浏览器不兼容Promise但兼容MutationObserver,那么Vue就会使用MutationObserver来实现异步更新。如果浏览器既不兼容Promise也不兼容MutationObserver,那么Vue就会使用setImmediatesetTimeout来实现异步更新。


    vue能监听到数组的push方法吗?直接给响应式变量赋值一个新的数组会被监听到吗?


    这里讨论的都是vue2vue3当中这些问题都已经被proxy解决了。



    Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:

    • push()
    • pop()
    • shift()
    • unshift()
    • splice()
    • sort()
    • reverse()


    替换整个对象或数组就和操作其他类型的响应式数据没区别了,自然是可以检测到的。


    深入响应式原理 — Vue.js


    如果要把数组api,比如push pop这些都改成async、await的异步函数要怎么做?怎么拿到这些方法?怎么传参?


    这一问是我当时最蒙的,我到现在都不确定我是否领悟对了他要问什么,大致上的理解如下,如果有大佬知道的可以在评论区教学一下我这个小菜鸟。

    // 保存数组原型上的push方法
    const originalPush = Array.prototype.push;
    // 重写数组原型上的push方法
    Array.prototype.push = async function (...args) {
    // 模拟异步操作
    await new Promise(resolve => setTimeout(resolve, 500));
    return originalPush.apply(this, args);
    }
    async function test() {
    const arr = [1, 2, 3];
    await arr.push(4);
    console.log(arr);
    }
    test(); // [1, 2, 3, 4],需要延迟定时器中设定的时间才能打印出来

    在沸点一位大佬的提醒下,面试官可能想问的是这个JavaScript 异步数组 - 个人文章 - SegmentFault 思否


    总结


    总结就是一个字,菜。

    虽然是问的有点细致,但基本上都只能回答上来每一问的第一问,后面的深入追问就懵逼了。原因是因为自己基本上都是直接对着面经和八股文准备的,没有实践过,也没有看过相关的文档。之后还是要坚持把JavaScript高级程序设计和vue设计与实现啃完,不说把这些问题记得滚瓜烂熟能对答如流,起码也要在在面试官引导下应该有思路。


    作者:炫饭第一名
    链接:https://juejin.cn/post/7275141653420425277
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    血压飙升!记一次关于手机号存储的前后端讨论

    web
    本文是为了探讨技术架构管理的,不要带入实际生活 起 事情是这样的,有一款产品需要实现国际化的注册需求,界面如下 : 涉及到的手机号码,因为要区分国家区号,所以和手机号是分开的,前端形态是分开存储的。一开始数据确实也是分开存的,么的问题。 本来是很简单的表单...
    继续阅读 »

    本文是为了探讨技术架构管理的,不要带入实际生活




    事情是这样的,有一款产品需要实现国际化的注册需求,界面如下 :


    image.png


    涉及到的手机号码,因为要区分国家区号,所以和手机号是分开的,前端形态是分开存储的。一开始数据确实也是分开存的,么的问题。


    本来是很简单的表单需求,结果出了幺蛾子。



    对于前端来说,这就是两个字段,在表单传值和数据逻辑处理时,直接用两个字段无疑是最直接和简单的:


    const formData = {
    country_code: '86',
    phone: '13345431234'
    ...
    }

    但是,产品侧有一个强需求:手机号和其他的两个字段要做唯一性校验,不但要求三个至少存在一个,而且还要查别的业务库的数据表来校验这个注册人员的唯一性,有一套复杂的逻辑。总结一下就是,后端分开两个字段来判断很麻烦,这个也好理解,有耦合的逻辑重构要成本的。所以后端是这么要求数据的:


    // (86)13345431234
    phone: `(${country_code})${phone}`

    将国家码放在前缀括号里拼接为一个字符串。这样一来,前端的成本就相对高了一些,不但在表单提交时要拼接,而且在列表查询时,要按照括号匹配解析出前缀来动态显示


    const regex = /^\((\d+)\)(\d+)$/;
    const matches = phoneNumber.match(regex);
    // 如果匹配成功,返回国家码和号码的数组
    if (matches) {
    const countrycode = matches[1];
    const number = matches[2];
    return [countrycode, number];
    }

    就算这样,也可以接受,之前也不止一次挖过这种自己拼接自己解析的坑了。但是随后就有点让人血压上升了。



    由于业务扩展,上面的手机号,要传递给一款即时通讯软件 xxx 的 API 来做下发,下发侧则规定了,需要不带括号但是要有国家区号前缀的手机号码


    // 8613345431234
    phone: `${country_code}${phone}`

    这就有问题了,我们直接对接的后端需要括号的,下发侧对接的后端同学不需要括号。


    第一阶段


    血压上升 20%


    讨论让后端换成两个字段存储,这样更灵活,方便拼接;或者给下发侧发数据时去掉括号也可以。后端很为难:”唯一性判断还有其他的逻辑很复杂,有耦合性,拆开字段就要重构,工期又紧,刚完成就要改,误了工期只有我背锅;去掉括号不是不可以,但地方有点多,还要考虑批量数据的性能,而且已经进入测试了,不接受临时需求“。我一想有道理啊,就去问下发侧同学。


    第二阶段


    血压上升 60%


    问下发侧对接的同学,可以处理后再给 API下发吗?果断拒绝。他们也有理由的:”按照软件设计的规范,我们不应该非标处理这个问题的,下发的规范理应就是我们接口的规范,因为规范还有很多,这个地方妥协了,以后肯定还会有更多的非标处理,我们的代码就乱了。“


    我草,我一想之前那那么多屎山的由来,觉得更有理了,那怎么办?弄得好像前端这里没有理了。


    第三阶段


    🔥 血压上升 120% 🔥


    下发侧对接的同学反过来给我们前端提了建议,让我们在注册的时候,把前端两个字段就直接不用括号来拼接给后端,在需要解析的时候,使用特别手段解析出国家码。还好心的给我们搜索了一个解析手机号的 js 库:


    企业微信截图_1bfc9849-e9f2-4289-a4cb-01098e3dcf2e.png


    我只能说,我 TM 谢谢你。😭


    前端莫名其妙的凭空多了需求,而且加个库为了给后端擦屁股,还增加了打包体积,出了问题我们得首先背锅,这肯定不行。



    这起事件牵涉的技术问题并没有多难,甚至有些幼稚,但是却蕴含了不少上班族哲学,最主要的一点是:如何学会背锅的问题。锅不是不能背,但是背锅换来的应该是产品成功后更多的功劳回报,而不是这种暗箱操作,老板压根不知道,只有扛不住的人吃暗亏。


    个人觉得,这个应该是架构师或者技术总监的任务,提前了解上下游业务,并确定好字段与数据结构,而不是等用的时候才发现数据对不上。规划好用什么方案来开发。并将方案设计交于老板开会讨论得出定稿。之后再分派人员开发,就不会有这个事件了。



    类似的问题还有很多,前后端与下游计算价格进制与单位不统一,每次都要非标转换;



    最后的结局,产品出面调停,软磨硬泡,最终后端妥协了,夹在前端和下发侧之间, 确实为难。针对前端还是原来的逻辑不动,使用括号存储,他在下发侧拉取数据的时候做转换,去掉括号。其中涉及的性能问题和开发成本也申请了延长工期。


    Happy Ending !!😁


    血压恢复 0%


    39df05d4-0146-4fbc-9eda-384298424f19.jpg



    方案设计注意事项:



    • 注意产品迁移的问题。同类型的产品,如果针对的客户群体不同,其表现形态可能完全不一样,产品迁移过程中,要提前分析出新产品的业务场景的差异,做好风险分析。

    • 提前确定产品上下游生态,做好风险管控。比如下发侧有 API 对接的,需熟悉其 API 文档与使用限制,并在服务不可用时设计告警方案等。


    作者:小肚肚肚肚肚哦
    来源:juejin.cn/post/7275576074589880372
    收起阅读 »

    为什么日本的网站看起来如此不同

    web
    该篇文章讨论了日本网站外观与设计的独特之处。作者指出日本网站设计与西方设计存在明显差异。文章首先强调了日本网站的视觉风格,包括丰富的色彩、可爱的角色和复杂的排版。作者解释了这种风格背后的文化和历史因素,包括日本的印刷传统和动漫文化。文章还讨论了日本网站的信息密...
    继续阅读 »

    该篇文章讨论了日本网站外观与设计的独特之处。作者指出日本网站设计与西方设计存在明显差异。文章首先强调了日本网站的视觉风格,包括丰富的色彩、可爱的角色和复杂的排版。作者解释了这种风格背后的文化和历史因素,包括日本的印刷传统和动漫文化。
    文章还讨论了日本网站的信息密集型布局,这种布局适应了日本语言的特点,使得页面能够容纳大量文字和图像。此外,文章提到了日本网站的功能丰富性,如弹出式窗口和互动元素,以及这些元素在用户体验方面的作用。
    作者强调了日本网站在技术和创新方面的进步,尽管在过去存在技术限制。最后,文章提出了一些关于如何将日本网站设计的元素应用到其他文化中的建议。


    下面是正文~~~


    多年来,我朋友与日本的网站有过许多接触——无论是研究签证要求、计划旅行,还是简单地在线订购东西。而我花了很长时间才适应这些网站上的大段文字、大量使用鲜艳颜色和10多种不同字体的设计,这些网站就像是直接冲着你扔过来的。


    image.png


    虽然有许多网站都采用了更简约、易于导航的设计,适应了西方网站的用户,但是值得探究的是为什么这种更复杂的风格在日本仍然盛行。


    只是为了明确起见,这些不是过去的遗迹,而是维护的网站,许多情况下,它们最后一次更新是在2023年。


    image.png


    我们可以从几个角度来分析这种设计方法:



    • 字体和前端网站开发限制

    • 技术发展与停滞

    • 机构数字素养(或其缺乏)

    • 文化影响


    与大多数话题一样,很可能没有一个正确的答案,而是这个网站设计是随着时间的推移而相互作用的各种因素的结果。


    字体和前端网站开发限制


    对于会一些基本排版知识、掌握适当软件并有一些空闲时间的人来说,为罗马化语言创造新字体可能是一项有趣的挑战。然而,对于日语来说,这是一个完全不同层次的努力。


    要从头开始创建英文字体,需要大约230个字形——字形是给定字母的单个表示(A a a算作3个字形)——或者如果想覆盖所有基于拉丁字母表的语言,则需要840个字形。对于日语而言,由于其三种不同的书写系统和无数的汉字,需要7,000至16,000个字形甚至更多。因此,在日语中创建新字体需要有组织的团队合作和比其拉丁字母表的同行们更多的时间。



    这并不令人意外,因此中文和(汉字)韩文字体也面临着类似的工作量,这导致这些语言通常被称为CJK字体所覆盖。



    由于越来越少的设计师面对这个特殊的挑战,建立网站时可供选择的字体也越来越少。再加上缺乏大写字母和使用日文字体会导致加载时间较长,因为需要引用更大的库,这就不得不采用其他方式来创建视觉层次。


    以美国和日本版的星巴克主页为例:


    美国的:


    image.png


    日本的


    image.png


    就这样,我们就可以解释为什么许多日本网站倾向于用文字较多的图片来表示内容类别了。有时,你甚至会看到每个磁贴都使用自己定制的字体,尤其是在限时优惠的情况下。


    image.png


    技术发展/停滞与机构数字素养


    如果你对日本感兴趣,你可能对现代与过时技术之间的鲜明对比有所了解。在许多地方,先进的技术与完全过时的技术并存。作为世界机器人领导者之一的国家,在台场人工岛上放置了一座真人大小的高达雕像,却仍然依赖软盘和传真机,面对2022年Windows资源管理器关闭时感到恐慌。


    image.png


    在德国,前总理安格拉·默克尔在2013年称互联网为“未知领域”后,遭到全国范围的嘲笑。然而,这在2018年被前网络安全部长樱田义孝轻易地超越,他声称自己从未使用过电脑,并且在议会被问及USB驱动器的概念时,他被引述为“困惑不解”(来源)。


    对于那些尚未有机会窥探幕后幻象的人来说,这可能听起来很奇怪,但日本在技术素养方面严重落后于更新计划。因此,可以推断这些问题也在阻碍日本网站设计的发展。而具体来说,日本的网页设计正面临着这一挑战——只需在谷歌或Pinterest上搜索日本海报设计,就能看到一个非常不同和现代化的平面设计水平。


    image.png


    文化影响


    在分析任何设计选择时,不应低估文化习俗、倾向、偏见和偏好的影响。然而,“这是文化”的说法可能过于简单化,并被用作为各种差异辩解的借口。而且,摆脱自己的观点偏见是困难的,甚至可能无法完全实现。


    因此,从我们的角度来看,看这个网站很容易..


    image.png


    感觉不知所措,认为设计糟糕,然后就此打住。因为谁会使用这个混乱不堪的网站呢?


    这就是因为无知而导致有趣的见解被忽视的地方。现在,我没有资格告诉你日本文化如何影响了这种设计。然而,我很幸运能够从与日本本土人士的交谈中获得启发,以及在日本工作和生活的经验。


    与这个分析相关的一次对话实际上不是关于网站,而是关于YouTube的缩略图 - 有时候它们也同样令人不知所措。


    image.png


    对于习惯了许多西方频道所采用的极简和时尚设计——只有一个标题、重复的色彩搭配和有限的字体——上面的缩略图确实有些难以接受。然而,当我询问一个日本本土人士为什么许多极受欢迎频道的缩略图都是这样设计时,他对这种设计被视为令人困惑的想法感到惊讶。他认为日本的设计方法使视频看起来更加引人入胜,提供了一些信息碎片,从而使我们更容易做出是否有趣的明智决策。相比之下,我给他看的英文视频缩略图在他看来非常模糊和无聊。


    也许正是这种寻求信息的态度导致了我们的观念如此不同。在日本,对风险的回避、反复核对和对迅速做出决策的犹豫明显高于西方国家。这与更加集体主义的社会心态紧密相连——例如,在将文件发送给商业伙伴之前进行两次(或三次)检查可能需要更长时间,但错误的风险显著降低,从而避免了任何参与者丢面子的情况发生。


    尽管有人认为这只适用于足够高的赌注,而迷惑外国游客似乎不符合条件——搜索一下“Engrish”这个词,然后感谢我吧。


    回到网站设计,这种文化角度有助于解释为什么在线购物、新闻和政府网站在外部观察者看来常常是“最糟糕的罪犯”。毕竟,这些正是需要大量细节直接对应于做出良好购买决策、高效地保持最新信息或确保你拥有某个特定程序的所有必要信息的情况。


    有趣的是,关于美国人和中国/日本人如何感知信息,也有相当多的研究。几项研究的结果似乎表明,例如,日本人更加整体地感知信息,而美国人倾向于选择一个焦点来引导他们的注意力(来源)。这可能给我们提供了另一个线索,解释为什么即使在日语能力较高的情况下,西方人对这类网站也感到困难。


    后但并非最不重要的是,必须说的是,网站并不是在一个在线真空中存在。而且,各种媒体,从小册子或杂志到地铁广告,也使用了尽可能多地压缩信息的布局,人们可能已经习惯了这种无处不在的方式,以至于没有人曾经想过质疑它。


    长话短说,这并不是为了找到标题问题的绝对答案,也不是为了加强日本人独特性的观点,就像日本人论一样。相反,尤其是在看到了几次关注一个解释为“真正答案”的讨论之后,我想展示科技、历史和文化影响的广度,这些最终塑造了这种差异。


    作者:王大冶
    来源:juejin.cn/post/7272290608655941651
    收起阅读 »

    🤔️《你不知道的JavaScript》到底讲了些什么?

    开始之前 在计算机科学的领域中,JavaScript是一门无法忽视的重要语言,深受许多开发者的喜爱。然而,它背后隐藏的复杂性和奥秘许多开发者并不为知。《你不知道的JavaScript》这三卷之作,对我个人而言真的算是常看常新,从刚从事前端开发到如今已经独当一面...
    继续阅读 »

    开始之前


    在计算机科学的领域中,JavaScript是一门无法忽视的重要语言,深受许多开发者的喜爱。然而,它背后隐藏的复杂性和奥秘许多开发者并不为知。《你不知道的JavaScript》这三卷之作,对我个人而言真的算是常看常新,从刚从事前端开发到如今已经独当一面,从这本书中受益良多。因此在多次阅读后我选择用内容梗概+案例解析的形式将其精华部分记录下来,以供个人翻阅和与大家分享,那么我们开始吧


    上卷


    上卷主要针对语言核心的一些关键概念,如作用域、闭包、this等。本文将为笔者阅读过程中所总结和提炼的关键知识点与经典案例


    1. 作用域是什么?


    内容概览


    本章介绍了JavaScript中的作用域概念,解释了变量如何被储存以及如何被引用。


    实例分析

    var a = 2;

    function foo() {
    var a = 3;
    console.log(a); // 3
    }

    foo();

    console.log(a); // 2

    在这个例子中,我们看到a在全局作用域和foo函数的作用域中都有定义。函数内部的a不会影响到全局作用域中的a


    2. 词法作用域


    内容概览


    词法作用域意味着作用域是由函数声明的位置来决定的,而不是函数调用的位置。


    实例分析

    function foo() {
    console.log(a);
    }

    function bar() {
    var a = 3;
    foo();
    }

    var a = 2;

    bar(); // 2

    尽管foo函数在bar函数内部被调用,但foo函数的词法作用域仍然使其能够访问外部的变量a,所以输出为2。


    3. 函数与块作用域


    内容概览


    介绍了函数作用域和块作用域,以及如何利用它们来避免变量冲突和其他问题。


    实例分析

    if (true) {
    let a = 2;
    console.log(a); // 2
    }

    console.log(a); // ReferenceError

    使用let定义的变量具有块作用域,只能在声明它的块中访问。


    4. 提升


    内容概览


    解释了提升(hoisting)现象,即变量和函数声明会被移动到它们所在的作用域顶部。


    实例分析

    foo(); // "Hello"

    function foo() {
    console.log("Hello");
    }

    尽管函数foo在调用之后被声明,但由于提升,它仍然可以正常调用。


    5. 作用域闭包


    内容概览


    解释了闭包是如何工作的,以及它在JavaScript中的重要性。


    实例分析

    function makeGreeting(greeting) {
    return function(name) {
    console.log(greeting + ", " + name);
    };
    }

    let sayHello = makeGreeting("Hello");
    sayHello("Alice"); // "Hello, Alice"

    sayHello函数是一个闭包,它记住了创建它时的作用域,因此能够访问greeting变量。


    6. 词法分析和语法分析


    实例分析


    来看以下代码:

    function add(x, y) {
    return x + y;
    }

    let sum = add(5, 7);

    在词法分析阶段,这段代码可能被分解为多个词法单元:function, add, (, x, ,, y, ), {, return, +, ;, }, let, =, 5, 7 等。然后,语法分析器会将这些词法单元组合成AST。


    7. L查询与R查询


    实例分析

    function calculateArea(radius) {
    const pi = 3.141592653589793;
    return pi * radius * radius;
    }

    let r = 5;
    let area = calculateArea(r);

    在这个例子中,考虑let area = calculateArea(r);这行代码。对于calculateArea,它是RHS查询,因为我们需要获得这个函数的引用来执行它。而r也是RHS查询,因为我们正在获取它的值来传递给函数。


    calculateArea函数内,pi和两次radius的查询都是RHS查询,因为我们获取它们的值来执行乘法操作。而return语句中的计算结果则赋值给了隐式的返回值,这涉及到LHS查询。


    对于let r = 5;,这里的r是一个LHS查询,因为我们给它赋值了。


    中卷


    中卷的内容相比上卷来说更加深入且晦涩,其中包括令初学者头昏脑胀的面向对象编程与this原型链相关的知识,我将以更多的篇幅和更深入的案例来帮助大家进行理解


    1. 对象


    实例分析 1


    使用工厂函数和构造器来创建对象:

    function createPerson(name, age) {
    return {
    name,
    age,
    greet() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
    }
    };
    }

    const person1 = createPerson('Alice', 30);
    person1.greet();

    深入分析


    这是一个工厂函数的例子,允许我们快速创建具有相似属性和方法的对象。在此,greet方法是每个对象的一部分,这可能导致内存浪费,因为每次创建新对象时,都会为greet方法分配新的内存。


    实例分析 2


    使用getters和setters:

    const book = {
    title: 'In Search of Lost Time',
    author: 'Marcel Proust',
    get description() {
    return `${this.title} by ${this.author}`;
    },
    set description(value) {
    [this.title, this.author] = value.split(' by ');
    }
    };

    book.description = '1984 by George Orwell';
    console.log(book.title); // Outputs: 1984

    深入分析


    这个案例展示了如何利用对象的getters和setters来动态地管理对象的属性。通过setter,我们能够同时更新titleauthor,而getter则为我们提供了书的描述。


    2. 类


    实例分析 1


    多态的使用:

    class Animal {
    makeSound() {
    console.log('Some generic sound');
    }
    }

    class Dog extends Animal {
    makeSound() {
    console.log('Woof');
    }
    }

    const animal1 = new Animal();
    const animal2 = new Dog();

    animal1.makeSound(); // Outputs: Some generic sound
    animal2.makeSound(); // Outputs: Woof

    深入分析


    多态是面向对象编程中的一个关键概念,允许我们创建能够以多种形式表现的对象。在此,我们看到Dog类重写了Animal类的makeSound方法,实现了多态。


    实例分析 2


    静态方法的使用:

    class MathUtility {
    static add(x, y) {
    return x + y;
    }
    }

    console.log(MathUtility.add(5, 3)); // Outputs: 8

    深入分析


    这个案例展示了如何在类中使用静态方法。与实例方法不同,静态方法不需要创建类的实例就可以被调用。它们通常用于执行与类的实例无关的操作。


    3. 原型


    实例分析


    一个动态添加到原型的方法:

    function Cat(name) {
    this.name = name;
    }

    Cat.prototype.purr = function() {
    console.log(`${this.name} is purring.`);
    };

    const whiskers = new Cat('Whiskers');
    whiskers.purr(); // Outputs: Whiskers is purring.

    深入分析


    在此例中,我们后期将purr方法添加到Cat的原型中。这意味着即使在添加此方法后创建的所有Cat实例都可以访问它。这展示了原型继承的动态性质:我们可以在任何时候修改原型,这些更改会反映在所有继承了那个原型的对象上。


    4. this和对象原型


    JavaScript中的this是一个非常深入且经常被误解的主题。this并不是由开发者选择的,它是由函数调用时的条件决定的。


    实例分析


    考虑以下场景:

    function showDetails() {
    console.log(this.name);
    }

    const obj1 = {
    name: 'Object 1',
    display: showDetails
    };

    const obj2 = {
    name: 'Object 2',
    display: showDetails
    };

    obj1.display(); // Outputs: Object 1
    obj2.display(); // Outputs: Object 2

    深入分析


    在这里,showDetails函数查看this.name。当它作为obj1的方法被调用时,this指向obj1。当它作为obj2的方法被调用时,this指向obj2。这说明了this的动态性质:它是基于函数如何被调用的。


    5. 原型链


    当试图访问一个对象的属性或方法时,JavaScript会首先在该对象本身上查找。如果未找到,它会在对象的原型上查找,然后是原型的原型,以此类推,直到找到该属性或到达原型链的末尾。


    实例分析

    function Animal(sound) {
    this.sound = sound;
    }

    Animal.prototype.makeSound = function() {
    console.log(this.sound);
    }

    function Dog() {
    Animal.call(this, 'Woof');
    }

    Dog.prototype = Object.create(Animal.prototype);

    const dog = new Dog();
    dog.makeSound(); // Outputs: Woof

    深入分析


    当我们调用dog.makeSound()时,JavaScript首先在dog对象上查找makeSound。未找到后,它会在Dog的原型上查找。还是未找到,然后继续在Animal的原型上查找,最后找到并执行它。


    6. 行为委托


    行为委托是原型的一种使用模式,涉及到对象之间的关系,而不仅仅是克隆或复制。


    实例分析

    const Task = {
    setID: function(ID) { this.id = ID; },
    outputID: function() { console.log(this.id); }
    };

    const XYZ = Object.create(Task);

    XYZ.prepareTask = function(ID, Label) {
    this.setID(ID);
    this.label = Label;
    };

    XYZ.outputTaskDetails = function() {
    this.outputID();
    console.log(this.label);
    };

    const task = Object.create(XYZ);
    task.prepareTask(1, 'create demo for delegation');
    task.outputTaskDetails(); // Outputs: 1, create demo for delegation

    深入分析


    XYZ不是Task的复制,它链接到Task。当我们在XYZ对象上调用setIDoutputID方法时,这些方法实际上是在Task对象上运行的,但this指向的是XYZ。这就是所谓的委托:XYZ在行为上委托给了Task


    下卷


    下卷的内容相较于中卷就基础了很多,更偏向于实际应用方向


    1. 类型和语法


    实例分析 - 类型转换


    考虑以下的隐式类型转换:

    var a = "42";
    var b = a * 1;
    console.log(typeof a); // "string"
    console.log(typeof b); // "number"

    深入分析


    在这里,变量a是一个字符串,但当我们尝试与数字进行乘法操作时,它会被隐式地转换为一个数字。这是因为乘法操作符期望它的操作数是数字,因此JavaScript会尝试将字符串a转换为一个数字。


    2. 异步和性能


    实例分析 - Promises

    function fetchData() {
    return new Promise((resolve, reject) => {
    setTimeout(() => {
    resolve("Data fetched!");
    }, 2000);
    });
    }

    fetchData().then(data => {
    console.log(data); // Outputs: "Data fetched!" after 2 seconds
    });

    深入分析


    Promises 提供了一种更简洁、更具可读性的方式来处理异步操作。在上面的例子中,fetchData函数返回一个Promise。setTimeout模拟了异步数据获取,数据在2秒后可用。当数据准备好后,resolve函数被调用,then方法随后执行,输出数据。


    3. ES6及其以上的特性


    实例分析 - 使用箭头函数

    const numbers = [1, 2, 3, 4];
    const doubled = numbers.map(num => num * 2);
    console.log(doubled); // [2, 4, 6, 8]

    深入分析


    箭头函数提供了一种更简洁的方式来定义函数,尤其是对于那些简短的、无状态的函数来说。在上述例子中,我们使用箭头函数简洁地定义了一个函数,该函数将其输入值乘以2,并使用map方法将其应用到一个数字数组中。


    实例分析 - 使用async/await

    async function fetchDataAsync() {
    let response = await fetch('https://api.example.com/data');
    let data = await response.json();
    return data;
    }

    fetchDataAsync().then(data => console.log(data));

    深入分析


    async/await是ES7引入的特性,允许以同步的方式编写异步代码。在这个案例中,fetchDataAsync函数是一个异步函数,这意味着它返回一个Promise。await关键字使我们能够等待Promise解析,然后继续执行后面的代码。这消除了回调地狱,使异步代码更容易阅读和维护。


    4. 迭代器和生成器


    实例分析 - 使用生成器函数

    function* numbersGenerator() {
    yield 1;
    yield 2;
    yield 3;
    }

    const numbers = numbersGenerator();

    console.log(numbers.next().value); // 1
    console.log(numbers.next().value); // 2
    console.log(numbers.next().value); // 3

    深入分析


    生成器函数使用function*声明,并且可以包含一个或多个yield表达式。每次调用生成器对象的next()方法时,函数都会执行到下一个yield表达式,并返回其值。这使我们能够按需产生值,非常适用于大数据集或无限数据流。


    5. 增强的对象字面量


    实例分析

    const name = "Book";
    const price = 20;

    const book = {
    name,
    price,
    describe() {
    return `${this.name} costs ${this.price} dollars.`;
    }
    };

    console.log(book.describe()); // "Book costs 20 dollars."

    深入分析


    增强的对象字面量允许我们在声明对象时使用更简洁的语法。在这里,我们直接使用变量名作为键,并使用简短的方法定义形式。这使得对象声明更为简洁和可读。


    6. 解构赋值


    实例分析

    const user = {
    firstName: "Alice",
    lastName: "Smith"
    };

    const { firstName, lastName } = user;

    console.log(firstName); // Alice
    console.log(lastName); // Smith

    深入分析


    解构赋值允许我们从数组或对象中提取数据,并赋值给新的或已存在的变量。在此例中,我们从user对象中提取了firstNamelastName属性,并将它们赋值给了同名的新变量。


    7. 模块


    实例分析 - ES6模块导入和导出

    // math.js
    export function add(x, y) {
    return x + y;
    }

    export function subtract(x, y) {
    return x - y;
    }

    // app.js
    import { add, subtract } from './math.js';

    console.log(add(5, 3)); // 8
    console.log(subtract(5, 3)); // 2

    结语


    经过对《你不知道的JavaScript》上、中、下三卷的深入探索,我们更加清晰地理解了JavaScript这门语言的复杂性、深度和强大之处。这不仅仅是关于语法或是新特性,更是关于理解其背后的哲学和设计思想。作为开发者,真正的掌握并不只是会用,而是要知其所以然。此书为我们打开了一扇探索JavaScript的大门,但真正的旅程,才刚刚开始。我们的每一步前行,都是为了更好地理解、更精准地应用,为编写出更高效、更优雅的代码而努力。


    作者:J3LLYPUNK
    链接:https://juejin.cn/post/7274839607656890429
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    👣 我在语雀做图册 - 更整齐的瀑布流算法

    web
    🙋🏻‍♀️ 编者按:本文作者是蚂蚁集团前端工程师亦池。介绍了语雀图册功能背后的更整齐的瀑布流算法,还有一段和 chatGPT 纠缠的故事 🤪,一起来看看~ 🏞️ 介绍一下图册 先来看看我们语雀年前上线的图册功能: 欢迎大家使用图册更好的展示自己的图片,...
    继续阅读 »

    🙋🏻‍♀️ 编者按:本文作者是蚂蚁集团前端工程师亦池。介绍了语雀图册功能背后的更整齐的瀑布流算法,还有一段和 chatGPT 纠缠的故事 🤪,一起来看看~



    🏞️ 介绍一下图册


    先来看看我们语雀年前上线的图册功能:
    image.png


    image.png



    欢迎大家使用图册更好的展示自己的图片,秀秀最近的摄影作品、po一下最近的好吃的好玩的、晒几张靓照~
    目前图册只是上了一个基础的初版,还有很多地方在体验和产品设计上也会继续优化,包括针对单张图的删除、排序,图册的尺寸调整,更快捷的把各种来源的图片放进图册里,大家有一些想法也欢迎提建议~



    开发故事


    🧐 瀑布流能不能再整齐一些


    瀑布流是一个不新鲜的布局方式了,看到这个我第一反应自然是使用社区的开源库按需裁剪一下用起来。刚发布时也是这么上线的。扒过代码参考的开源库有:



    但第一版本其实回发生下图左侧尴尬的情况:
    image.png
    肉眼可见我们要的是上图右侧的效果。


    常见瀑布流算法的问题



    原因:社区主流的瀑布流计算思路都是将已知高度的图片(实现上可以是图片加载完成后获取高度触发重新布局)分发了列容器里,每列记录实时高度,对于每一张新来的图片分发规则是放入最短的那一列。专业点说是贪心算法的思想。



    所以当最后一张是长图时就会对布局的齐平性导致很大的冲击。(当然这不是说社区的方案都low,开源产品可能更多考虑了普适情况,譬如可能无法提前知道所有图片尺寸信息,全部加载完再重新布局一次又给用户带来干扰,甚至是懒加载场景更不好优雅的展示处理。)


    在语雀编辑器场景,我们对于要布局的那批图片是能拿到宽高信息的,完全可以对所有图片整体考虑,计算一个最优结果再渲染,可以做到不被最后一张长图影响整体。


    一开始我觉得这是个单纯的算法问题,可以抽象成将一个数字数组拆分成n个数组,使每个数组的数字和尽量接近,我觉得应该是有一种经典算法来解决这类问题的,譬如动态规划、背包问题之类的。


    这么经典的问题不如问chatGPT吧,此处插入一段和chatGPT纠缠的故事。结论是它没给我找到靠谱答案。感兴趣的可以展开后面章节的折叠块看看这个让人哭笑不得的过程🙄。


    💁‍♀️ 分析一下


    chatGPT没能给我正确答案,我又是个基础算法的渣渣,想先找个方向再进去研究怎么实现,于是请教了一下一个酷爱刷算法题的师妹,得到的方向是:“这是个负载均衡类型问题,具有NP hard复杂度,无法获得最佳解,只能求近似最优解,用动态规划的思想是没错的”。


    啥是NP hard复杂度,可以看后面的【基础知识】章节的科普。我也不清楚怎么证明这真的是一个NP hard复杂度的问题,但基础知识告诉我这类复杂度的问题往往复杂度是阶乘级别的,就是不是我们常见的O(n)、O(logn)、O(n^2)这种经典算法的复杂度,他们的复杂度叫做有多项式解。阶乘级别意味着暴力穷举,这往往是计算机无法接受的时间,也毫无算法可言。


    咱这个问题,求解最优解时,每一张图片的摆放都影响着后面图片的位置,每张图之间都有关联,想想似乎确实只有穷举才能真正的找到最优解。加上对师妹算法水平的信任,我开始把问题缩减到动态规划领域。


    那就拆解子问题,先计算子问题的近似最优解。


    🏄‍ ♀️解决方案


    核心思想:




    1. 计算平均值,让每一组的和尽量接近均值,最终每组和的差异才会最小

    2. 将原数组arr从大到小排序,降低分组时便利查找的复杂度

    3. 遍历原数组arr,从目标的n个分组的第一组开始装数据,直到它接近均值停止。这里注意接近的意思不是<=avg,而是在均值临界点,加上一个值num1 < avg后,和均值的差值是delta,往前遍历找(意味着num2 > num1)第一个没被分组的数据num2放入当前组后,num2 - avg < delta,如果是的则装num2,否则装num1。确保装的是最接近均值的数。

    4. 对于最后一个分组n-1要装数据时,需要确保arr的每一个数据都被分配完,并且各组结果最小,所以最后一组的策略不参考平均值,而是按和最小的分组去塞arr里的每一个数据。



    另外注意,对于已经分好组的数据打个标,以免被重复分组。


    这里我们是在拆解子问题



    • 把复杂的分组后每组方差最小的问题,转化为让每组和最接近平均值的问题,将整体的问题拆解成了n个组的问题

    • n个组塞值时,又是一个找数据使它最接近均值的子问题


    其中为了降低复杂度不搞遍历的最优,确实只做到了近似最优解。譬如放值前先做了排序,只要当前数据放进去 < avg都先无脑放,就会出现,譬如剩下的数据有[48, 25, 25], 均值是50,本来我们可以放[25,25]得到最接近均值的数据,但现在只放入了48。


    🤪 图片场景的特殊考虑因子


    当我把一个纯数学解放入瀑布流场景时,发现事情并没有这么简单,算法的最优还是要为图片展示场景效果的最优做一些让步


    参差感


    譬如你看这个是最优解么?
    image.png
    因为我们先做了排序,并且按排序的数据顺序做分配,所以长图它它它它都跑到同一列去了。image.png
    这个视觉上没有了参差美可受不了。


    于是在接近最优的做法上妥协一步。先把排序前n的数据挨个放到n组,让个高的先均匀分布。


    结合保留用户本来的顺序,是不是舒服一些:
    image.png


    这里依旧不是最佳效果,因为只取了前n个,试想我们如果是3组,5个长图,还是有一组全是长图。但长与短的边界实在无法敲定,除非再搞个每张图片高度的均值,大于均值一定阈值的数据先均匀分布到n组,但这种操作的数据越多,越影响到底部整体的平齐效果。所以还是只选了和组数相同的前n张这么处理。我估摸着大多数用户在文档里的图片是个辅助,不会搞出特别大数量级还夹杂很多长短分明的图。当前能保持一定数量级 (<10)展示上不会有太大问题。


    排序


    尽量得保证用户原图的顺序,所以需要记录原图的顺序,然后在分组完成后:




    1. 每列里按原图顺序重排下顺序

    2. 列与列之间按第一个图的顺序重排下顺序



    能做到尽量接近原顺序但不绝对。


    纯数字上[[25], [25], [25,25]][[25,25], [25], [25]]的分组没有差别。但是图片场景又不一样了:
    image.png
    这排列总透着一股奇怪image.png
    于是再让步牺牲一下复杂度:



    装最后一组数据分配余数之前,先把分配好的分组,先排序,组与组的和相等时优先放入排前面的数组。



    当前版本优缺点


    目前至少是在最平齐和图片参差感之间谋求的一个较优解,但绝不是最优解,理论上此类问题不穷举遍历获得不了最优解。但我们可以通过优化局部策略,使它更靠近最优解。不过一定是优于贪心算法把每张图放入高度最小列的做法。这里如果有深入研究过瀑布流的小伙伴有更优的方案,欢迎提供,让语雀的瀑布流更整齐~


    做事情咱也不能只说好的,对问题缄口不言,目前的问题有:



    • 前面也说过,如果大量图片,并且存在 分组张数n 的与其他图片长度拉开巨大差距的图片,排版还是不够有参差感

    • 先按大小排序,后分组,会对原图顺序造成偏差,很难复原严格的行列顺序,但用户还是能一定程度的干预排序,只是无法满足一定要求图A和图B不放入同一列这种诉求。从这个角度说,顺序上不如贪心算法方案更接近原顺序,贪心方案的最后一张长图问题其实可以通过主动拖拽顺序把长图放到前面来解决掉,但是这对用户的理解力要求太高了。


    anyway,以下的数据哪个算法也无法救🥲。目前列数是根据展示区宽度弹性计算的,这种想优雅可能要触发列数的改变规则了。
    image.png


    chatGPT的插曲


    点我展开查看哭笑不得的过程### 第1轮
    image.png
    一开始它给了我个贪心算法的不是最优解,得让它进阶


    第2轮


    image.pngimage.png
    看上去很高深,但这测试数据结果不对啊。
    我换个说法?是不是不能理解什么叫数字加和尽量接近


    第3轮


    image.png
    结果不对,继续让他换个解法


    第4轮


    image.png
    还是肉眼可见的不对,虽然我肉眼分的也不是最优解,最后我的算法告诉我是可以分成三组和都是80的:[80], [32, 32, 12, 3, 1], [30, 21, 20, 9]


    那么问题在哪呢,我尝试问了它一个很简单的问题:
    image.png
    原来加和都求不对,我放弃它了。。。


    :::warning
    综上:chatGPT能高效省事让你偷懒,但前提是你得能区分出它的答案靠不靠谱,如果你都不知道真相的问题仍给他,就会被忽悠了也不知道。另外别想用它帮你写笔试题了,它只根据语义生成,但并不真的运行代码,给的代码和结果可能完全不匹配。
    :::


    📔 基础知识


    资料:




    复杂度被分为两种级别:一种是O(1),O(log(n)),O(n^a)等,我们把它叫做多项式级的复杂度,因为它的规模n出现在底数的位置;另一种是O(a^n)和O(n!)型复杂度,它是非多项式级的,其复杂度计算机往往不能承受




    P问题: 如果一个问题可以找到一个能在多项式的时间里解决它的算法,那么这个问题就属于P问题
    NP问题: NP问题不是非P类问题。NP问题是指可以在多项式的时间里验证一个解的问题。NP问题的另一个定义是,可以在多项式的时间里猜出一个解的问题
    NPC问题:同时满足下面两个条件的问题就是NPC问题。首先,它得是一个NP问题;然后,所有的NP问题都可以约化到它。NPC问题目前没有多项式的有效算法,只能用指数级甚至阶乘级复杂度的搜索。
    **NP-Hard问题:**它满足NPC问题定义的第二条但不一定要满足第一条。NP-Hard问题同样难以找到多项式的算法,但它不列入我们的研究范围,因为它不一定是NP问题。即使NPC问题发现了多项式级的算法,NP-Hard问题有可能仍然无法得到多项式级的算法




    约化:(Reducibility,有的资料上叫“归约”)。简单地说,一个问题A可以约化为问题B的含义即是,可以用问题B的解法解决问题A,或者说,问题A可以“变成”问题B。通过对某些问题的不断约化,我们能够不断寻找复杂度更高,但应用范围更广的算法来代替复杂度虽然低,但只能用于很小的一类问题的算法。



    next:拼图


    接下来我们还会上线更灵活的拼图能力。**拼图算法可以实现任何尺寸的图片,保持原比例不裁剪,用户任意摆放位置,最终绘制成整齐的矩形,**这个算法实现也远比瀑布流复杂。


    譬如你可以这样:
    image.png
    也可以拖成这样:
    image.png


    还可以拖成这样:
    image.png


    甚至拖成这样:
    image.png


    等等等等...... 随意组合排序,最终都能整齐。


    等上线后我再写写拼图的故事~


    作者:支付宝体验科技
    来源:juejin.cn/post/7198370695079903291
    收起阅读 »

    产品经理:实现一个微信输入框

    web
    近期在开发AI对话产品的时候为了提升用户体验增强了对话输入框的相关能力,产品初期阶段对话框只是一个单行输入框,导致在文本内容很多的时候体验很不好,所以进行体验升级,类似还原了微信输入框的功能(只是其中的一点点哈🤏)。 初期认为这应该改动不大,就是把input换...
    继续阅读 »


    近期在开发AI对话产品的时候为了提升用户体验增强了对话输入框的相关能力,产品初期阶段对话框只是一个单行输入框,导致在文本内容很多的时候体验很不好,所以进行体验升级,类似还原了微信输入框的功能(只是其中的一点点哈🤏)。


    初期认为这应该改动不大,就是把input换成textarea吧。但是实际开发过程发现并没有这么简单,本文仅作为开发过程的记录,因为是基于uniapp开发,相关实现代码都是基于uniapp


    简单分析我们大概需要实现以下几个功能点:



    • 默认单行输入

    • 可多行输入,但有最大行数限制

    • 超过限制行术后内容在内部滚动

    • 支持回车发送内容

    • 支持常见组合键在输入框内换行输入

    • 多行输入时高度自适应 & 页面整体自适应


    单行输入


    默认单行输入比较简单直接使用input输入框即可,使用textarea的时候目前的实现方式是通过设置行内样式的高度控制,如我们的行内高度是36px,那么就设置其高度为36px。为什么要通过这种方式设置呢?因为要考虑后续多行输入超出最大行数的限制,需要通过高度来控制textarea的最大高度。


    <textarea style="{ height: 36px }" />


    多行输入


    多行输入核心要注意的就是控制元素的高度,因为不能随着用户的输入一直增加高度,我们需要设置一个最大的行数限制,超出限制后就不再增加高度,内容可以继续输入,可以在输入框内上下滚动查看内容。


    这里需要借助于uniapp内置在textarea@linechange事件,输入框行数变化时调用并回传高度和行数。如果不使用uniapp则需要对输入文字的长度及当前行高计算出对应的行数,这种情况还需要考虑单行文本没有满一行且换行的情况。


    代码如下,在linechange事件中获取到最新的高度设置为textarea的高度,当超出最大的行数限制后则不处理。


    linechange(event) {
    const { height, lineCount } = event.detail
    if (lineCount < maxLine) {
    this.textareaHeight = height
    }
    }

    这是正常的输入,还有一种情况是用户直接粘贴内容输入的场景,这种时候不会触发@linechange事件,需要手动处理,根据粘贴文本后的textarea的滚动高度进行计算出对应的行数,如超出限制行数则设置为最大高度,否则就设置为实际的行数所对应的高度。代码如下:


    const paddingTop = parseInt(getComputedStyle(textarea).paddingTop);
    const paddingBottom = parseInt(getComputedStyle(textarea).paddingBottom);
    const textHeight = textarea.scrollHeight - paddingTop - paddingBottom;
    const numberOfLines = Math.floor(textHeight / lineHeight);

    if (numberOfLines > 1 && this.lineCount === 1) {
    const lineCount = numberOfLines < maxLine ? numberOfLines : maxLine
    this.textareaHeight = lineCount * lineHeight
    }

    键盘发送内容


    正常我们使用电脑聊天时发送内容都是使用回车键发送内容,使用ctrlshiftalt等和回车键的组合键将输入框的文本进行换行处理。所以接下来要实现的就是对键盘事件的监听,基于事件进行发送内容和内容换行输入处理。


    首先是事件的监听,uniapp不支持keydown的事件监听,所以这里使用了原生JS做监听处理,为了避免重复监听,对每次开始监听前先进行移除事件的监听,代码如下:


    this.$refs.textarea.$el.removeEventListener('keydown', this.textareaKeydownHandle)
    this.$refs.textarea.$el.addEventListener('keydown', this.textareaKeydownHandle)

    然后是对textareaKeydownHandle方法的实现,这里需要注意的是组合键对内容换行的处理,需要获取到当前光标的位置,使用textarea.selectionStart可获取,基于光标位置增加一个换行\n的输入即可实现换行,核心代码如下:


    const cursorPosition = textarea.selectionStart;
    if(
    (e.keyCode == 13 && e.ctrlKey) ||
    (e.keyCode == 13 && e.metaKey) ||
    (e.keyCode == 13 && e.shiftKey) ||
    (e.keyCode == 13 && e.altKey)
    ){
    // 换行
    this.content = `${this.content.substring(0, cursorPosition)}\n${this.content.substring(cursorPosition)}`
    }else if(e.keyCode == 13){
    // 发送
    this.onSend();
    e.preventDefault();
    }

    高度自适应


    当多行输入内容时输入框所占据的高度增加,导致页面实际内容区域的高度减小,如果不进行动态处理会导致实际内容会被遮挡。如下图所示,红色区域就是需要动态处理的高度。



    主要需要处理的场景就是输入内容行数变化的时候和用户粘贴文本的时候,这两种情况都会基于当前的可视行数计算输入框的高度,那么内容区域的高度就好计算了,使用整个窗口的高度减去输入框的高度和其他固定的高度如导航高度和底部安全距离高度即是真实内容的高度。


    this.contentHeight = this.windowHeight - this.navBarHeight - this.fixedBottomHeight - this.textareaHeight;

    最后


    到此整个输入框的体验优化核心实现过程就结束了,增加了多行输入,组合键换行输入内容,键盘发送内容,整体内容高度自适应等。整体实现过程的细节功能点还是比较多,有实现过类似需求的同学欢迎留言交流~


    看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~


    作者:南城FE
    来源:juejin.cn/post/7267791228872753167
    收起阅读 »

    微信小程序 折叠屏适配

    web
    最近维护了将近的一年的微信小程序(某知名企业),突然提出要兼容折叠屏,这款小程序主要功能一些图表汇总展示,也就是专门给一些领导用的,也不知道为啥领导们为啥突然喜欢用折叠屏手机了,一句话需求,苦的还是咱们程序员,但没办法,谁让甲方是爸爸呢,硬着头皮改吧,好在最后...
    继续阅读 »

    最近维护了将近的一年的微信小程序(某知名企业),突然提出要兼容折叠屏,这款小程序主要功能一些图表汇总展示,也就是专门给一些领导用的,也不知道为啥领导们为啥突然喜欢用折叠屏手机了,一句话需求,苦的还是咱们程序员,但没办法,谁让甲方是爸爸呢,硬着头皮改吧,好在最后解决了,因为是甲方内部使用的小程序,这里不便贴图,但有官方案例图片,以供参考


    查看了微信官网
    大屏适配
    响应显示区域变化


    启用大屏模式


    从小程序基础库版本 2.21.3 开始,在 Windows、Mac、车机、安卓 WMPF 等大屏设备上运行的小程序可以支持大屏模式。可参考小程序大屏适配指南。方法是:在 app.json 中添加 "resizable": true


    看到这里我心里窃喜,就加个配置完事了?这也太简单了,但后面证明我想简单了,
    主要有两大问题:



    • 1 尺寸不同的情况下内容展示效果兼容问题

    • 2 预览版和体验版 大屏模式冷启动会生效,但热启动 和 菜单中点击重新进入小程、授权操作,会失效变成窄屏


    解决尺寸问题


    因为css的长度单位大部分用的 rpx,窄屏和宽屏展示差异出入较大,别说客户不认,自己这关就过不了,简直都不忍直视,整个乱成一片,尤其登录页,用了定位,更是乱上加乱。


    随后参考了官方的文档 小程序大屏适配指南自适应布局,方案对于微信小程序原生开发是可行的,但这个项目用的 uni-app开发的,虽然uni-app 也有对应的响应式布局组件,再加上我是个比较爱偷懒的人(甲方给的工期事件也有限制),不可能花大量时间把所有也页面重新写一遍布局,这是不现实的。


    于是又转战到uni-app官网寻找解决方案 uni-app宽屏适配指南


    内容缩放拉伸的处理 这一段中提出了两个策略



    • 1.局部拉伸:页面内容划分为固定区域和长宽动态适配区域,固定区域使用固定的px单位约定宽高,长宽适配区域则使用flex自动适配。当屏幕大小变化时,固定区域不变,而长宽适配区域跟着变化

    • 2.等比缩放:根据页面屏幕宽度缩放。rpx其实属于这种类型。在宽屏上,rpx变大,窄屏上rpx变小。


    随后看到这句话特别符合我的需求,哈哈 省事 省事 省事


    策略2省事,设计师按750px屏宽出图,程序员直接按rpx写代码即可。但策略2的实际效果不如策略1好。程序员使用策略1,分析下界面,设定好局部拉伸区域,这样可以有更好的用户体验


    具体实现


    1.配置 pages.json 的 globeStyle


    {
    "globalStyle": {
    "rpxCalcMaxDeviceWidth": 1200, // rpx 计算所支持的最大设备宽度,单位 px,默认值为 960
    "rpxCalcBaseDeviceWidth": 375, // rpx 计算使用的基准设备宽度,设备实际宽度超出 rpx 计算所支持的最大设备宽度时将按基准宽度计算,单位 px,默认值为 375
    "rpxCalcIncludeWidth": 750 // rpx 计算特殊处理的值,始终按实际的设备宽度计算,单位 rpx,默认值为 750
    },
    }

    2.单位兼容


    还有一点官方也提出来了很重要,那就很多时候 会把宽度750rpx 当成100% 使用,这在宽屏的设备上就会有问题, uniapp给了两种解决方案



    • 750rpx 改为100%

    • 另一种是配置rpxCalcIncludeWidth,设置某个特定数值不受rpxCalcMaxDeviceWidth约束


    想要用局部拉伸:页面内容划分为固定区域和长宽动态适配区域”的策略,单位必须用px


    添加脚本


    项目根目录新增文件 postcss.config.js 内容如下。则在编译时,编译器会自动转换rpx单位为px。


    // postcss.config.js

    const path = require('path')
    module.exports = {
    parser: 'postcss-comment',
    plugins: {
    'postcss-import': {
    resolve(id, basedir, importOptions) {
    if (id.startsWith('~@/')) {
    return path.resolve(process.env.UNI_INPUT_DIR, id.substr(3))
    } else if (id.startsWith('@/')) {
    return path.resolve(process.env.UNI_INPUT_DIR, id.substr(2))
    } else if (id.startsWith('/') && !id.startsWith('//')) {
    return path.resolve(process.env.UNI_INPUT_DIR, id.substr(1))
    }
    return id
    }
    },
    'autoprefixer': {
    overrideBrowserslist: ["Android >= 4", "ios >= 8"],
    remove: process.env.UNI_PLATFORM !== 'h5'
    },
    // 借助postcss-px-to-viewport插件,实现rpx转px,文档:https://github.com/evrone/postcss-px-to-viewport/blob/master/README_CN.md
    // 以下配置,可以将rpx转换为1/2的px,如20rpx=10px,如果要调整比例,可以调整 viewportWidth 来实现
    'postcss-px-to-viewport': {
    unitToConvert: 'rpx',
    viewportWidth: 200,
    unitPrecision: 5,
    propList: ['*'],
    viewportUnit: 'px',
    fontViewportUnit: 'px',
    selectorBlackList: [],
    minPixelValue: 1,
    mediaQuery: false,
    replace: true,
    exclude: undefined,
    include: undefined,
    landscape: false
    },
    '@dcloudio/vue-cli-plugin-uni/packages/postcss': {}
    }
    }


    大屏模式失效问题


    下面重头戏来了,这期间经历 蜿蜒曲折 ,到头来发现都是无用功,我自己都有被wx蠢到发笑,唉,


    样式问题解决后 开始着重钻研 大屏失效的问题,但看了官方的多端适配示例demo,人家的就是好的,那就应该有解决办法,于是转战github地址 下项目,谁知这项目暗藏机关,各种报错,让你跑不起来。。。。,让我一度怀疑腾讯也这么拉跨


    还好issues 区一位大神有解决办法 感兴趣的老铁可以去瞅瞅
    image


    1693664649860.jpg


    另外 微信小程序开发工具需要取消这两项,最后当项目跑起来后我还挺开心,模拟器上没有问题,但用真机预览的时候我啥眼了,还是窄屏,偶尔可以大屏,后面发现 冷启动是大屏,热启动和点击右上角菜单中的重新进入小程序按钮都会自己变成窄屏幕


    官方案例.gif批量更新.gif

    这是官方的项目啊,为啥人家的可以,我本地跑起来却不可以,让我一度怀疑这里有内幕,经过几轮测试还是不行,于是乎,我开始了各种询问查资料,社区、私聊、评论、github issues,最后甚至 统计出来了 多端适配示例demo 开发者的邮箱 挨个发了邮件,但都结果无一例外,全部石沉大海


    1693666642117.jpgwx-github-issues-110.jpg
    私聊.jpg评论.jpg
    wx-mini-dev.jpgimage.png

    结果就是,没有办法了,想看看是不是只有预览和体验版有问题,后面发布到正式版后,再看居然没问题了,就是这么神奇,也是无语!!!! 原来做了这么多无用功。。。。


    作者:iwhao
    来源:juejin.cn/post/7273764921456492581
    收起阅读 »

    详解JS判断页面是在手机端还是在PC端打开的方法

    web
    下面详细介绍一下如何判断页面是在手机端还是在PC端打开,并提供两条示例说明。 方法一:使用UA判断 UA(UserAgent)是指HTTP请求头中的一部分,用于标识客户端的一些信息,比如用户的设备类型、浏览器型号等等。因此,我们可以通过判断UA中的关键字来确定...
    继续阅读 »

    下面详细介绍一下如何判断页面是在手机端还是在PC端打开,并提供两条示例说明。


    方法一:使用UA判断


    UA(UserAgent)是指HTTP请求头中的一部分,用于标识客户端的一些信息,比如用户的设备类型、浏览器型号等等。因此,我们可以通过判断UA中的关键字来确定页面访问者的设备类型。下面是实现的代码:


    const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);

    if (isMobile) {
    console.log('当前在手机端');
    } else {
    console.log('当前在PC端');
    }

    代码解析:


    首先,我们使用正则表达式匹配navigator.userAgent中是否包含iPhoneiPadiPodAndroid这些关键字,如果匹配成功,则说明当前是在移动端。如果匹配失败,则说明当前是在PC端。


    需要注意的是,该方法并不100%准确,因为用户可以使用PC浏览器模拟手机UA,也有可能使用移动端浏览器访问PC网站。


    方法二:使用媒体查询判断


    媒体查询是CSS3的一个新特性,可以根据不同的媒体类型(比如设备屏幕的宽度、高度、方向等)来设置不同的CSS样式。我们可以利用媒体查询来判断页面是在手机端还是在PC端打开。下面是实现的代码:


    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8"/>
    <title>判断页面是在手机端还是在PC端</title>
    <style>
    /* 默认样式 */
    p {
    font-size: 24px;
    color: yellow;
    }
    /* 移动端样式 */
    @media (max-width: 767px) {
    p {
    font-size: 20px;
    color: green;
    }
    }
    </style>
    </head>
    <body>
    <p>测试内容</p>
    </body>
    </html>

    代码解析:


    在CSS中,我们使用@media关键字定义了一个媒体查询,当浏览器宽度小于等于767px的时候,p元素的字体大小和颜色都会发生改变,从而实现了对移动端的识别。如果浏览器宽度大于767px,则会使用默认样式。


    需要注意的是,该方法只能判断设备的屏幕宽度,不能确定设备的真实类型,因此并不太准确。


    总的来说,两种方法各有优缺点,具体选择哪种方法要根据自己的需求和场景来决定。一般来说,如果只是想简单地判断页面访问者的设备类型,使用第一种方法即可。如果需要根据设备类型来优化网站的布局和样式,可以使用第二种方法。


    作者:RuiRay
    来源:juejin.cn/post/7273746154642014262
    收起阅读 »

    强大的css计数器,你确定不来看看?

    web
    强大的 css 计数器 css content 属性有很多实用的用法,这其中最为强大的莫过于是计数器了,它甚至可以实现连 javascript 都不能够实现的效果,下面我们一起来研究一下吧。 css 计数器主要有 3 个关键点需要掌握。如下: 首先需要一个计...
    继续阅读 »

    强大的 css 计数器


    css content 属性有很多实用的用法,这其中最为强大的莫过于是计数器了,它甚至可以实现连 javascript 都不能够实现的效果,下面我们一起来研究一下吧。


    css 计数器主要有 3 个关键点需要掌握。如下:



    1. 首先需要一个计数器的名字,这个名字由使用者自己定义。

    2. 计数器有一个计数规则,比如是 1,2,3,4...这样的递增方式,还是 1,2,1,2...这样的连续递增方式。

    3. 计数器的使用,即定义好了一个计数器名字和计数规则,我们就需要去使用它。


    以上 3 个关键点分别对应的就是 css 计数器的 counter-reset 属性,counter-increment 属性,和 counter()/counters()方法。下面我们依次来介绍这三个玩意儿。


    counter-reset 属性


    counter-reset 属性叫做计数器重置,对应的就是创建一个计数器名字,如果可以,顺便也可以告诉计数器的计数起始值,也就是从哪个值开始计数,默认值是 0,注意是 0,而不是 1。例如以下一个示例:


    html 代码如下:


    <p>开始计数,计数器名叫counter</p>
    <p class="counter"></p>

    css 代码如下:


    .counter {
    counter-reset: counter;
    }

    .counter::before {
    content: counter(counter);
    }

    在浏览器中运行以上示例,你会看到如下图所示:


    counter-1.png


    可以看到计数器的初始值就是 0,现在我们修改一下 css 代码,如下所示:


    .counter {
    counter-reset: counter 1;
    }

    在浏览器中运行以上示例,你会看到如下图所示:


    counter-2.png


    这次我们指定了计数器的初始值 1,所以结果就是 1,计数器的初始值同样也可以指定成小数,负数,如-2,2.99 之类,只不过 IE 和 FireFox 浏览器都会认为是不合法的数值,当做默认值 0 来处理,谷歌浏览器也会直接显示负数,如下图所示:


    counter-3.png


    低版本谷歌浏览器处理小数的时候是向下取整,比如 2.99 则显示 2,最新版本则当成默认值 0,来处理,如下图所示:


    counter-4.png



    ps: 当然不推荐指定初始值为负数或者小数。



    你以为到这里就完了吗?还没有,计数器还可以指定多个,每一个计数器之间用空格隔开,比如以下代码:


    .counter {
    counter-reset: counter1 1 counter2 2;
    }

    .counter::before {
    content: counter(counter1) counter(counter2);
    }

    在浏览器中运行以上示例,你会看到如下图所示:


    counter-5.png


    除此之外,计数器名还可以指定为 none 和 inherit,也就是取消计数和继承计数器,这没什么好说的。


    counter-increment


    顾名思义,该属性就是计数器递增的意思,也就是定义计数器的计数规则,值为计数器的名字,可以是一个或者多个,并且也可以指定一个数字,表示计数器每次变化的数字,如果不指定,默认就按照 1 来变化。比如以下代码:


    .counter {
    counter-reset: counter 1;
    counter-increment: counter;
    }

    得到的结果就是: 1 + 1 = 2。如下图所示:


    counter-6.png


    再比如以下代码:


    .counter {
    counter-reset: counter 2;
    counter-increment: counter 3;
    }

    得到的结果就是: 2 + 3 = 5,如下图所示:


    counter-7.png


    由此可见,计数器的规则就是: 计数器名字唯一,每指定一次计数规则,计数器就会加一,每指定二次计数规则,计数器就会加二,……以此类推。


    计数规则不仅可以创建在元素上,也可以创建在使用计数器的元素上,比如以下代码:


    .counter {
    counter-reset: counter;
    counter-increment: counter;
    }

    .counter::before {
    content: counter(counter);
    counter-increment: counter;
    }

    我们不仅在类名为 counter 元素上创建了一个计数器规则,同样的也在 before 伪元素上创建了一个计数器规则,因此最后的结果就是: 0 + 1 + 1 = 2。如下图所示:


    counter-8.png


    总而言之,无论位置在何处,只要有 counter-increment,对应的计数器的值就会变化, counter()只是输出而已!计数器的数值变化遵循 HTML 渲染顺序,遇到一个 increment 计数器就变化,什么时候 counter 输出就输出此时的计数值。


    除此之外,计数器规则也可以和计数器一样,创建多个计数规则,也是以空格区分,比如以下示例代码:


    .counter {
    counter-reset: counter1 1 counter2 2;
    counter-increment: counter1 2 counter2 3;
    }

    .counter::before {
    content: counter(counter1) counter(counter2);
    counter-increment: counter1 4 counter2 5;
    }

    此时的结果就应该是计数器 1: 1 + 2 + 4 = 7,计数器 2: 2 + 3 + 5 = 10。如下图所示:


    counter-9.png


    同样的,计数器规则的值也可以是负数,也就是递减效果了,比如以下代码:


    .counter {
    counter-reset: counter1 1 counter2 2;
    counter-increment: counter1 -1 counter2 -3;
    }

    .counter::before {
    content: counter(counter1) counter(counter2);
    counter-increment: counter1 2 counter2 5;
    }

    此时的结果就应该是计数器 1: 1 - 1 + 2 = 2,计数器 2: 2 - 3 + 5 = 4。如下图所示:


    counter-10.png


    同样的计数规则的值也可以是 none 或者 inherit。


    counter


    counter 方法类似于 calc,主要用于定义计数器的显示输出,到目前为止,我们前面的示例都是最简单的输出,也就是如下语法:


    counter(name); /* name为计数器名 */

    实际上还有如下的语法:


    counter(name,style);

    style 参数和 list-style-type 的值一样,意思就是不仅可以显示数字,还可以显示罗马数字,中文字符,英文字母等等,值如下:


    list-style-type: disc | circle | square | decimal | lower-roman | upper-roman |
    lower-alpha | upper-alpha | none | armenian | cjk-ideographic | georgian |
    lower-greek | hebrew | hiragana | hiragana-iroha | katakana | katakana-iroha |
    lower-latin | upper-latin | simp-chinese-informal;

    比如以下的示例代码:


    .counter {
    counter-reset: counter;
    counter-increment: counter;
    }

    .counter::before {
    content: counter(counter, lower-roman);
    }

    结果如下图所示:


    counter-11.png


    再比如以下的示例代码:


    .counter {
    counter-reset: counter;
    counter-increment: counter;
    }

    .counter::before {
    content: counter(counter, simp-chinese-informal);
    }

    结果如下图所示:


    counter-12.png


    同样的 counter 也可以支持级联,也就是说,一个 content 属性值可以有多个 counter 方法,如:


    .counter {
    counter-reset: counter;
    counter-increment: counter;
    }

    .counter::before {
    content: counter(counter) '.' counter(counter);
    }

    结果如下图所示:


    counter-13.png


    counters


    counters 方法虽然只是比 counter 多了一个 s 字母,但是含义可不一样,counters 就是用来嵌套计数器的,什么意思了?我们平时如果显示列表符号,不可能只是单单显示 1,2,3,4...还有可能显示 1.1,1.2,1.3...前者是 counter 做的事情,后者就是 counters 干的事情。


    counters 的语法为:


    counters(name, string);

    name 就是计数器名字,而第二个参数 string 就是分隔字符串,比如以'.'分隔,那 string 的值就是'.',以'-'分隔,那 string 的值就是'-'。来看如下一个示例:


    html 代码如下:


    <div class="reset">
    <div class="counter">
    javascript框架
    <div class="reset">
    <div class="counter">&nbsp;angular</div>
    <div class="counter">&nbsp;react</div>
    <div class="counter">
    vue
    <div class="reset">
    <div class="counter">
    vue语法糖
    <div class="reset">
    <div class="counter">&nbsp;@</div>
    <div class="counter">&nbsp;v-</div>
    <div class="counter">&nbsp;:</div>
    </div>
    </div>
    </div>
    </div>
    </div>
    </div>
    </div>

    css 代码如下:


    .reset {
    counter-reset: counter;
    padding-left: 20px;
    }

    .counter::before {
    content: counters(counter, '-') '.';
    counter-increment: counter;
    }

    结果如下图所示:


    counter-14.png


    这种计数效果在模拟书籍的目录效果时非常实用,比如写文档,会有嵌套标题的情况,还有一个比较重要的点需要说明一下,就是显示 content 计数值的那个 DOM 元素在文档流中的位置一定要在 counter-increment 元素的后面,否则是没有计数效果的。


    总而言之,content 计数器是非常强大的,以上都只是很基础的用法,真正掌握还需要大量的实践以及灵感还有创意。


    作者:夕水
    来源:juejin.cn/post/7275176987358265355
    收起阅读 »

    前端埋点实现方案

    前端埋点的简介埋点是指在软件、网站或移动应用中插入代码,用于收集和跟踪用户行为数据。通过在特定位置插入埋点代码,开发人员可以记录用户在应用中的操作,如页面浏览、按钮点击、表单提交等。这些数据可以用于分析用户行为、优化产品功能、改进用户体验等目的。 埋点通常与...
    继续阅读 »

    前端埋点的简介

    • 埋点是指在软件、网站或移动应用中插入代码,用于收集和跟踪用户行为数据。

    • 通过在特定位置插入埋点代码,开发人员可以记录用户在应用中的操作,如页面浏览、按钮点击、表单提交等。

    • 这些数据可以用于分析用户行为、优化产品功能、改进用户体验等目的。




    埋点通常与数据分析工具结合使用,如Google Analytics、Mixpanel等,以便对数据进行可视化和进一步分析。


    前端埋点是指在前端页面中嵌入代码,用于收集和跟踪用户行为数据。


    通过埋点可以获取用户在网页或应用中的点击、浏览、交互等动作,用于分析用户行为、优化产品体验和进行数据驱动的决策。


    在前端埋点中,常用的方式包括:

    1. 页面加载埋点:用于追踪和监测页面的加载时间、渲染状态等信息。
    2. 点击事件埋点:通过监听用户的点击事件,记录用户点击了哪些元素、触发了什么操作,以及相关的参数信息。
    3. 表单提交埋点:记录用户在表单中输入的内容,并在用户提交表单时将这些数据发送到后台进行保存和分析。
    4. 页面停留时间埋点:用于记录用户在页面停留的时间,以及用户与页面的交互行为,如滚动、鼠标悬停等。
    5. AJAX请求埋点:在前端的AJAX请求中添加额外的参数,用于记录请求的发送和返回状态,以及相应的数据。

    埋点数据可以通过后端API或第三方数据分析工具发送到服务器进行处理和存储。


    在使用前端埋点时,需要注意保护用户隐私,遵守相关法律法规,并确保数据采集和使用的合法性和合规性。


    同时,还需设计良好的数据模型和分析策略,以便从埋点数据中获得有价值的信息。


    前端埋点设计


    前面说过,前端埋点是一种数据追踪的技术,用于收集和分析用户的行为数据。


    前端埋点设计方案有哪些?


    下面简单介绍一下:

    1. 事件监听:通过监听用户的点击、滚动、输入等事件,记录用户的操作行为。可以使用JavaScript来实现事件监听,例如使用addEventListener()函数进行事件绑定。

    2. 自定义属性:在HTML元素中添加自定义属性,用于标识不同的元素或事件。 例如,在按钮上添加data-*属性,表示不同的按钮类型或功能。当用户与这些元素进行交互时,可以获取相应的属性值作为事件标识。

    3.  发送请求:当用户触发需要追踪的事件时,可以通过发送异步请求将数据发送到后台服务器。 可以使用XMLHttpRequest、fetch或者第三方的数据上报SDK来发送请求。

    4. 数据格式:确定需要采集的数据格式,包括页面URL、时间戳、用户标识、事件类型、操作元素等信息。 通常使用JSON格式来封装数据,方便后续的数据处理和分析。

    5. 用户标识:对于需要区分用户的情况,可以在用户首次访问时生成一个唯一的用户标识,并将该标识存储在浏览器的cookie中或使用localStorage进行本地存储。

    6. 数据上报:将采集到的数据发送到后台服务器进行存储和处理。可以自建后台系统进行数据接收和分析,也可以使用第三方的数据分析工具,例如百度统计、Google Analytics等。

    7.  隐私保护:在进行数据采集和存储时,需要注意用户隐私保护。

    8.  遵守相关的法律法规,对敏感信息进行脱敏处理或加密存储,并向用户明示数据采集和使用政策。


    需要注意的是,在进行埋点时要权衡数据采集的成本与收益,确保收集到的数据具有一定的价值和合法性。


    同时,要注意保护用户隐私,遵守相关法律法规,尊重用户的选择和权益。


    前端埋点示例


    以下是一个完整的前端埋点示例


    展示了如何在网站上埋点统计页面浏览、按钮点击和表单提交事件

    • 在HTML中标识需要采集的元素或事件:
    <button id="myButton" data-track-id="button1">Click Me</button>

    <form id="myForm">
      <input type="text" name="username" placeholder="Username">
      <input type="password" name="password" placeholder="Password">
      <button type="submit">Submit</button>
    </form>

    在按钮和表单元素上添加了data-track-id自定义属性,用于标识这些元素。

    • 使用JavaScript监听事件并获取事件数据:
    // 监听页面加载事件
    window.addEventListener("load", function() {
      var pageUrl = window.location.href;
      var timestamp = new Date().getTime();
      var userData = {
        eventType: "pageView",
        pageUrl: pageUrl,
        timestamp: timestamp
        // 其他需要收集的用户数据
      };

      // 封装数据格式并发送请求
      sendData(userData);
    });

    // 监听按钮点击事件
    document.getElementById("myButton").addEventListener("click", function(event) {
      var buttonId = event.target.getAttribute("data-track-id");
      var timestamp = new Date().getTime();
      var userData = {
        eventType: "buttonClick",
        buttonId: buttonId,
        timestamp: timestamp
        // 其他需要收集的用户数据
      };

      // 封装数据格式并发送请求
      sendData(userData);
    });

    // 监听表单提交事件
    document.getElementById("myForm").addEventListener("submit", function(event) {
      event.preventDefault(); // 阻止表单默认提交行为

      var formId = event.target.getAttribute("id");
      var formData = new FormData(event.target);
      var timestamp = new Date().getTime();
      var userData = {
        eventType: "formSubmit",
        formId: formId,
        formData: Object.fromEntries(formData.entries()),
        timestamp: timestamp
        // 其他需要收集的用户数据
      };

      // 封装数据格式并发送请求
      sendData(userData);
    });

    通过JavaScript代码监听页面加载、按钮点击和表单提交等事件,获取相应的事件数据,包括页面URL、按钮ID、表单ID和表单数据等。

    • 发送数据请求:
    function sendData(data) {
      var xhr = new XMLHttpRequest();
      xhr.open("POST", "/track", true);
      xhr.setRequestHeader("Content-Type", "application/json");

      xhr.onreadystatechange = function() {
        if (xhr.readyState === 4 && xhr.status === 200) {
          console.log("Data sent successfully.");
        }
      };

      xhr.send(JSON.stringify(data));
    }

    使用XMLHttpRequest对象发送POST请求,将封装好的数据作为请求的参数发送到后台服务器的/track接口。

    • 后台数据接收与存储:

    后台服务器接收到前端发送的数据请求后,进行处理和存储。


    可以使用后端开发语言(如Node.js、Python等)来编写接口逻辑,将数据存储到数据库或其他持久化存储中。


    通过监听页面加载、按钮点击和表单提交等事件,并将相关数据发送到后台服务器进行存储和分析。


    根据具体项目需求,可以扩展和定制各种不同类型的埋点事件和数据采集。


    vue 前端埋点示例


    在Vue中实现前端埋点可以通过自定义指令或者混入(mixin)来完成。


    下面给出两种常见的Vue前端埋点示例:

    • 自定义指令方式:
    // 在 main.js 中注册全局自定义指令 track
    import Vue from 'vue';

    Vue.directive('track', {
      bind(el, binding, vnode) {
        const { event, data } = binding.value;
        
        el.addEventListener(event, () => {
          // 埋点逻辑,例如发送请求或记录日志
          console.log("埋点事件:" + event);
          console.log("埋点数据:" + JSON.stringify(data));
        });
      }
    });

    在组件模板中使用自定义指令:

    <template>
      <button v-track="{ event: 'click', data: { buttonName: '按钮A' } }">点击按钮A</button>
    </template>

    • 1. 混入方式:
    // 创建一个名为 trackMixin 的混入对象,并定义需要进行埋点的方法
    const trackMixin = {
      methods: {
        trackEvent(event, data) {
          // 埋点逻辑,例如发送请求或记录日志
          console.log("埋点事件:" + event);
          console.log("埋点数据:" + JSON.stringify(data));
        }
      }
    };

    // 在组件中使用混入
    export default {
      mixins: [trackMixin],
      mounted() {
        // 在需要进行埋点的地方调用混入的方法
        this.trackEvent('click', { buttonName: '按钮A' });
      },
      // ...
    };

    这两种方式都可以实现前端埋点,你可以根据自己的项目需求选择适合的方式。


    在实际应用中,你需要根据具体的埋点需求来编写逻辑,例如记录页面浏览、按钮点击、表单提交等事件,以及相应的数据收集和处理操作。


    使用自定义指令(Custom Directive)的方式来实现前端埋点


    在Vue 3中,你可以使用自定义指令(Custom Directive)的方式来实现前端埋点。


    一个简单的Vue 3的前端埋点示例:


    • 创建一个名为analytics.js的文件,用于存放埋点逻辑:
    // analytics.js

    export default {
      mounted(el, binding) {
        const { eventType, eventData } = binding.value;

        // 发送数据请求
        this.$http.post('/track', {
          eventType,
          eventData,
        })
        .then(() => {
          console.log('Data sent successfully.');
        })
        .catch((error) => {
          console.error('Error sending data:', error);
        });
      },
    };

    • 在Vue 3应用的入口文件中添加全局配置:
    import { createApp } from 'vue';
    import App from './App.vue';
    import axios from 'axios';

    const app = createApp(App);

    // 设置HTTP库
    app.config.globalProperties.$http = axios;

    // 注册全局自定义指令
    app.directive('analytics', analyticsDirective);

    app.mount('#app');

    • 在组件中使用自定义指令,并传递相应的事件类型和数据:
    <template>
      <button v-analytics="{ eventType: 'buttonClick', eventData: { buttonId: 'myButton' } }">Click Me</button>
    </template>

    在示例中,我们定义了一个全局的自定义指令v-analytics,它接受一个对象作为参数,对象包含了事件类型(eventType)和事件数据(eventData)。当元素被插入到DOM中时,自定义指令的mounted钩子函数会被调用,然后发送数据请求到后台服务器。


    注意,在示例中使用了axios作为HTTP库发送数据请求,你需要确保项目中已安装了axios,并根据实际情况修改请求的URL和其他配置。


    通过以上设置,你可以在Vue 3应用中使用自定义指令来实现前端埋点,采集并发送相应的事件数据到后台服务器进行存储和分析。请根据具体项目需求扩展和定制埋点事件和数据采集。


    使用Composition API的方式来实现前端埋点


    以下是一个Vue 3的前端埋点示例,使用Composition API来实现:

    • 创建一个名为analytics.js的文件,用于存放埋点逻辑:
    // analytics.js

    import { ref, onMounted } from 'vue';

    export function useAnalytics() {
      const trackEvent = (eventType, eventData) => {
        // 发送数据请求
        // 模拟请求示例,请根据实际情况修改具体逻辑
        console.log(`Sending ${eventType} event with data:`, eventData);
      };

      onMounted(() => {
        // 页面加载事件
        trackEvent('pageView', {
          pageUrl: window.location.href,
        });
      });

      return {
        trackEvent,
      };
    }

    • 在需要进行埋点的组件中引入useAnalytics函数并使用:
    import { useAnalytics } from './analytics.js';

    export default {
      name: 'MyComponent',
      setup() {
        const { trackEvent } = useAnalytics();

        // 按钮点击事件
        const handleClick = () => {
          trackEvent('buttonClick', {
            buttonId: 'myButton',
          });
        };

        return {
          handleClick,
        };
      },
    };

    • 在模板中使用按钮并绑定相应的点击事件:
    <template>
      <button id="myButton" @click="handleClick">Click Me</button>
    </template>

    在示例中,我们将埋点逻辑封装在了analytics.js文件中的useAnalytics函数中。在组件中使用setup函数来引入useAnalytics函数,并获取到trackEvent方法进行埋点操作。在模板中,我们将handleClick方法绑定到按钮的点击事件上。


    当页面加载时,会自动发送一个pageView事件的请求。当按钮被点击时,会发送一个buttonClick事件的请求。


    注意,在示例中,我们只是模拟了数据请求操作,请根据实际情况修改具体的发送数据请求的逻辑。


    通过以上设置,你可以在Vue 3应用中使用Composition API来实现前端埋点,采集并发送相应的事件数据。请根据具体项目需求扩展和定制埋点事件和数据采集。


    react 前端埋点示例


    使用自定义 Hook 实现


    当然!以下是一个 React 的前端埋点示例,


    使用自定义 Hook 实现:

    • 创建一个名为 useAnalytics.js 的文件,用于存放埋点逻辑:
    // useAnalytics.js

    import { useEffect } from 'react';

    export function useAnalytics() {
      const trackEvent = (eventType, eventData) => {
        // 发送数据请求
        // 模拟请求示例,请根据实际情况修改具体逻辑
        console.log(`Sending ${eventType} event with data:`, eventData);
      };

      useEffect(() => {
        // 页面加载事件
        trackEvent('pageView', {
          pageUrl: window.location.href,
        });
      }, []);

      return {
        trackEvent,
      };
    }

    • 在需要进行埋点的组件中引入 useAnalytics 自定义 Hook 并使用:
    import { useAnalytics } from './useAnalytics';

    function MyComponent() {
      const { trackEvent } = useAnalytics();

      // 按钮点击事件
      const handleClick = () => {
        trackEvent('buttonClick', {
          buttonId: 'myButton',
        });
      };

      return (
        <button id="myButton" onClick={handleClick}>Click Me</button>
      );
    }

    export default MyComponent;

    在示例中,我们将埋点逻辑封装在了 useAnalytics.js 文件中的 useAnalytics 自定义 Hook 中。在组件中使用该自定义 Hook 来获取 trackEvent 方法以进行埋点操作。在模板中,我们将 handleClick 方法绑定到按钮的点击事件上。


    当页面加载时,会自动发送一个 pageView 事件的请求。当按钮被点击时,会发送一个 buttonClick 事件的请求。


    请注意,在示例中,我们只是模拟了数据请求操作,请根据实际情况修改具体的发送数据请求的逻辑。


    通过以上设置,你可以在 React 应用中使用自定义 Hook 来实现前端埋点,采集并发送相应的事件数据。根据具体项目需求,你可以扩展和定制埋点事件和数据采集逻辑。


    使用高阶组件(Higher-Order Component)实现


    当然!以下是一个 React 的前端埋点示例,


    使用高阶组件(Higher-Order Component)实现:

    • 创建一个名为 withAnalytics.js 的高阶组件文件,用于封装埋点逻辑:
    // withAnalytics.js

    import React, { useEffect } from 'react';

    export function withAnalytics(WrappedComponent) {
      return function WithAnalytics(props) {
        const trackEvent = (eventType, eventData) => {
          // 发送数据请求
          // 模拟请求示例,请根据实际情况修改具体逻辑
          console.log(`Sending ${eventType} event with data:`, eventData);
        };

        useEffect(() => {
          // 页面加载事件
          trackEvent('pageView', {
            pageUrl: window.location.href,
          });
        }, []);

        return <WrappedComponent trackEvent={trackEvent} {...props} />;
      };
    }

    • 在需要进行埋点的组件中引入 withAnalytics 高阶组件并使用:
    import React from 'react';
    import { withAnalytics } from './withAnalytics';

    function MyComponent({ trackEvent }) {
      // 按钮点击事件
      const handleClick = () => {
        trackEvent('buttonClick', {
          buttonId: 'myButton',
        });
      };

      return (
        <button id="myButton" onClick={handleClick}>Click Me</button>
      );
    }

    export default withAnalytics(MyComponent);

    在示例中,我们创建了一个名为 withAnalytics 的高阶组件,它接受一个被包裹的组件,并通过属性传递 trackEvent 方法。在高阶组件内部,我们在 useEffect 钩子中处理页面加载事件的埋点逻辑,并将 trackEvent 方法传递给被包裹组件。被包裹的组件可以通过属性获取到 trackEvent 方法,并进行相应的埋点操作。


    在模板中,我们将 handleClick 方法绑定到按钮的点击事件上。


    当页面加载时,会自动发送一个 pageView 事件的请求。当按钮被点击时,会发送一个 buttonClick 事件的请求。


    请注意,在示例中,我们只是模拟了数据请求操作,请根据实际情况修改具体的发送数据请求的逻辑。


    通过以上设置,你可以在 React 应用中使用高阶组件来实现前端埋点,采集并发送相应的事件数据。


    当然根据具体项目需求,你还可以扩展和定制埋点事件和数据采集逻辑。


    作者:朦胧之
    链接:https://juejin.cn/post/7274084216286183460
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    如何突破技术瓶颈(适合P6以下)

    前言 最近在更新react组件库源码的文章,其实vue和其他框架都一样,就是我发现自己在一年前直接看这些源码(不调试),还是有点吃力的,然后就放弃了。 可最近不知道为啥,看这些源码对我来说没啥太大难度,直接干就完事了,不需要啥调试。自己好好回想了一下,为什么会...
    继续阅读 »

    前言


    最近在更新react组件库源码的文章,其实vue和其他框架都一样,就是我发现自己在一年前直接看这些源码(不调试),还是有点吃力的,然后就放弃了。


    可最近不知道为啥,看这些源码对我来说没啥太大难度,直接干就完事了,不需要啥调试。自己好好回想了一下,为什么会有这样的变化?也算帮助一些想突破自己技术瓶颈的同学。


    有新人在下面留言说看到很焦虑,刚进前端领域的同学,你们首要任务是能完成业务开发,此时业务开发带给你的提升是最明显的,文章更多的是帮助业务api用熟之后的想有突破的同学,不用焦虑,哈哈。而且话说回来了,我在平时工作中看到不想突破的人基本占百分90%,无论大小厂,所以不突破也没啥,大部分人只是仅仅当一个普通工作而已。


    结论


    首先我得出结论是:

    • 最开始不要自己去读源码,看别人的文章和视频即可,目的是先接触比自己能力层次高的代码,为超越现有的能力铺路(后面详细谈怎么做)
    • 平时注意积累一些手写题的思路,网上面经很多,主要不是写出来,是理解原理,理解大于一切,不理解的东西终究会忘记,我们要积累的是能力,能力是第一!(后面详细谈),设计模式里的发布订阅者模式必须要理解!这是写很多库常见的技巧。
    • 最后开始独立去看一些小的代码库,比如腾讯,阿里,字节的组件库,这些库大部分组件难度低。

    去哪里看视频和文章学源码


    视频


    最简易的就是跟着视频学,因为视频会把代码敲一遍,给你思考的时间,讲解也是最细的,很适合刚开始想造轮子的同学了解一些有难度的源码。


    举个例子:


    我当时看了koa的源码,了解了koa中间件的原理,我自己造了一个自动化发布脚本就利用了这个原理,redux中间件也是类似的原理,在函数式编程领域叫做compose函数,koa是异步compose,redux是同步compose,


    简单描述下什么是compose函数


    我把大象装进冰箱是不是要
    1、打开冰箱门
    2、装进去大象
    3、关冰箱门


    那么很多同学就会写一个函数

    function 装大象(){
    // 打开冰箱
    // 装大象
    // 关闭冰箱门
    }

    compose函数会把这个过程拆开,并且抽象化

    // 把装大象抽象为装东西函数
    function 装东西();
    function 打开冰箱();
    function 关闭冰箱();

    compose(打开冰箱函数, 装东西函数,关闭冰箱函数)

    此时compose把上面三个函数抽象为一个打开冰箱往里面装东西的函数,我们只需要把参数大象穿进去就抽象了整个过程

    compose(打开冰箱函数, 装东西函数,关闭冰箱函数)(大象)

    具体内容我还写过一篇文章,有兴趣的同学可以去看看:


    终极compose函数封装方案!


    这个大家应该有自己的去处,我自己的话很简单,视频一般去b站,就是bilibili,有些同学以为这是一个二次元网站是吧,其实里面免费的学习资料一抓一大把呢,啥都有。


    比如说我在b站看了很多linux入门教学视频,还有一个培训公开课,讲的都是源码,什么手写react hook,手写webpack,手写xxx,那个时候说实话,听了视频也不是很理解,但是我还是挺喜欢前端的,没咋理解就继续听。


    记住,我们需要短时间内提升能力,所以视频算是其中最快的了,其他方法不可能有这个来的快,并且没理解就算了,能理解多少是多少。


    学习是一个螺旋上升的过程,不是一下子就全懂或者全不懂的,都是每次比上一次更懂一点。除非你是天才,急不来的。


    视频搜索第二大去处就是论坛,一些论坛有各种各样的培训视频,这种论坛太多了,你谷歌或者百度一抓一大把。


    对了,谷歌是爸爸,你懂我意思,不要吝啬小钱。在搜索学习资料面前,百度就是个弟弟。


    文章


    文章一定记住,在精不在多。


    切记,每个人都处在不同的学习阶段,不要盲目追求所谓的大神文章,不一定适合你,比如说有些人刚接触前端,你去看有些有深度的文章对你没啥好处,浪费时间,因为你理解不了,理解不了的知识相当于没学,过两天就忘了。


    文章选择范围,比如掘金,知乎还有前端公众号,基本上就差不多了,选一两个你觉得你这个阶段能吸收的,好好精读,坚持个一年你会发现不一样的。


    额外的知识储备


    前端3年前主流的前端书我都读过,什么红宝书,权威指南都读了好几遍了。


    但有一本从菜鸟到高级-资深前端很推荐的一本是:JavaScript设计模式与开发实践(图灵出品)(腾讯的一位大哥写的,不是百度的那位,这两本书我都看过)


    里面的知识点很干很干,里面有非常多的技巧,比如说你的同事写了一个函数,你不想破坏函数,有什么办法拓展它(其实我觉得我想的这些题就比前端八股文好玩多了,是开放性的)

    • 技巧很多,比如面向切面编程,加个before或者after函数包装一下
    • 比如责任链模式
    • 比如刚才的compose函数
    • 比如装饰器模式

    确立自己的发展方向


    大家其实最后都要面对一个很现实的问题,就是35以后怎么办,我个人觉得你没有对标阿里P7的能力,落地到中小公司都难。


    所以我们看源码,看啥都是为了提升能力,延长职业寿命。


    那么如何在短时间内有效的提升,你就需要注意不能各种方向胡乱探索,前端有小游戏方向,数据可视化方向,B端后台系统方向,音视频方向等等


    我是做b端,那b端整个链路我就需要打通,组件库是我这个方向,所以我探索这里,还有node端也是,写小工具是必须的,但是你们说什么deno,其他的技术,我根本不在乎,没时间浪费在这些地方,当然除了有些业务上需要,比如之前公司有个ai标注需求,用canvas写了一个类似画板的工具,也算开拓了知识点,但这也不是我重点发展的方向,不深入。


    我做组件库是为了后面的低代码,低代码平台的整体设计思路我已经想好了,整体偏向国外开源的appsmith的那种方式,然后打通组件间通信的功能,我认为是能胜任稍微复杂的b端业务场景的,而且可以走很多垂直领域,比如网站建站,微信文章编辑器这种。所以我才开始研究组件库的,因为低代码大多数复杂功能都在组件上。


    工作上勇于走出舒适圈


    为什么这个跟看源码相关呢,如果你做过比较复杂的项目,你会发现很多现成的第三方库满足不了。比如说我自己遇到过的大型sass项目,ant design就满足不了,所以你才发现,源码看得少加上业务急,代码就烂,时间上就留不出自己偷偷学习的时间,如果你想长期从事软件开发,没有成长是一件很危险的事(钱多当我没说,哈哈),因为无论如何,有本事,总没错的。


    当你的业务难度上去的时候,会逼着你去提升能力,所以你如果想前端走的更远,建议不要在自己的舒适区太久,业务上选择一家比较难的公司,后面再跳槽就是沉淀这段时间的知识点了,当你能够有自信说,我现在带团队,从0到1再遇到那么难的业务时,能从容应对,恭喜你,你可以去面下阿里p7,不是为了这个工作啊,可以检验下是不是达到这个职位的标准了,我就喜欢偶尔面一下,也不是换工作,就是看看自己进步没


    作者:孟祥_成都
    链接:https://juejin.cn/post/7168671474234949662
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    读完React新文档后的8条收获

    不久前,React官网进行了重构。新官网将hook作为介绍的重头戏,重新通读了一遍react文档,有了一些自己的感悟。在此分享出来与各位共勉。 1. 换个角度认识Props与State Props与State是React中两个略有相似的概念。在一个React组...
    继续阅读 »

    不久前,React官网进行了重构。新官网将hook作为介绍的重头戏,重新通读了一遍react文档,有了一些自己的感悟。在此分享出来与各位共勉。


    1. 换个角度认识Props与State


    PropsState是React中两个略有相似的概念。在一个React组件中,它们作为数据来源可能同时存在,但它们之间有着巨大不同:

    1. Props更像函数中的参数。它主要用于组件之间的信息传递,它可以是任意类型的数据:数字、文本、函数、甚至于组件等等。
    2. State更像组件中的内存。它可以保存组件内部的状态,组件通过跟踪这些状态,从而渲染更新。
    import React, { useState } from 'react';

    // 父组件
    const ParentComponent = () => {
    const [count, setCount] = useState(0); // 使用state来追踪count的值

    return (
    <div>
    <ChildComponent age={25} />
    <p>Count: {count}</p>
    </div>
    );
    };

    // 子组件
    const ChildComponent = (props) => {
    const { age } = props; // 使用props来获取父组件传递的数据

    return (
    <div>
    <p>Age: {age}</p>
    </div>
    );
    };

    2. 不要嵌套定义组件


    在一个组件中直接定义其他组件,可以省去很多传递Props的工夫,看上去很好。但我们不应该嵌套定义组件,原因在于**嵌套定义组件会导致渲染速度变慢,也更容易出现BUG**。
    我们在嵌套定义组件的情况下,当父组件更新时,内部嵌套的子组件也会重新生成并渲染,子组件内部的state也会丢失。针对这种情况,我们可以通过两种方式来解决:

    1. 为子组件包上useMemo,避免不必要的更新;
    2. 不再嵌套定义,将子组件提至父组件外,通过Props传参。

    这里更推荐第二种方法,因为这样代码更少,结构也更清晰,组件迁移维护都会更方便一些。

    //🔴 Bad Case
    export default function Gallery() {
    function Profile() {
    // ...
    }
    // ...
    }
    //✅ Good Case
    function Profile() {
    // ...
    }

    export default function Gallery() {
    // ...
    }

    3. 尽量不要使用匿名函数组件


    因为类似export default () => {}的匿名函数组件书写虽然方便,但会让debug变得更困难
    如下是两种不同类型组件出错时的控制台的表现:

    1. 具名组件出错时的提示,可直接的指出错的函数组件名称: 


    1. 匿名函数出错时的提示,无法直观确定页面内哪个组件出了错: 



    4. 使用逻辑运算符&&编写JSX时,左侧最好不要是数字


    运算符&&在JSX中的表现与JS略有不同:

    • 在JS中,只有在&&左侧的值0nullundefinedfalse''等假值时,才会返回右侧值;
    • 在JSX中,React对于falsenullundefined并不会做渲染处理,所以进行&&运算后,如果左侧值为假被返回后,会出现与直觉不同的渲染结果:可能会碰到页面上莫名奇妙出现了一个0的问题。为了避免出现这种情况,可以在书写JSX时,为左侧的值加上!!来进行强制类型转换。
    const flag = 0
    //🔴 Bad Case
    {
    flag && <div>123</div>
    }
    //✅ Good Case 1
    {
    !!flag && <div>123</div>
    }
    //✅ Good Case 2
    {
    flag > 0 && <div>123</div>
    }

    关于JSX对各种常见假值的渲染,这里进行了总结:

    1. nullundefinedfalse:这些值在JSX中会被视为空,不会渲染任何内容。它们会被忽略,并且不会生成对应的DOM元素。
    2. 0NaN:这些值会在页面上渲染为字符串"0"、"NaN"。
    3. ''[]{}:空字符串会被渲染为什么都没有,不会显示任何内容。


    注:这里感谢@小明家的bin的评论提醒,他的见解对我起到了很大的启发作用。



    5.全写的 Fragment标签上可以添加属性key


    在map循环添加组件时,需要为组件添加key来优化性能。如果待循环的组件有多个时,可以在外层包裹<Fragment>...</Fragment>标签,在其上添加key添加在<Fragment>...</Fragment>来避免创建额外的组件。

    const list = [1,2,3]
    //🔴 Bad Case
    //不能添加key
    {
    list.map(v=><> <div>1-1</div> <div>1-2</div> </>)
    }
    //🔴 Bad Case
    //创建了额外的div节点
    {
    list.map(v=><div key={v}> <div>1-1</div> <div>1-2</div> <div/>)
    }
    //✅ Good Case
    {
    list.map(v=><Fragment key={v}> <div>1-1</div> <div>1-2</div> </Fragment>)
    }



    注意简写的Fragment标签<>...</>上不支持添加key



    6. 可以使用updater function,来在下一次渲染之前多次更新同一个state


    React中处理状态更新时采用了批处理机制,在下一次渲染之前多次更新同一个state,可能不会达到我们想要的效果。如下是一个简单例子:

    // 按照直觉一次点击后button中的文字应展示为3,但实际是1
    function Demo(){
    const [a,setA] = useState(0)

    function handler(){
    setA(a + 1);
    setA(a + 1);
    setA(a + 1);
    }

    return <button onclick={handler}>{a}</button>
    }

    在某些极端场景下,我们可能希望state的值能够及时发生变化,这时便可以采用setA(n => n + 1)(这种形式被称为updater function)来进行更新:

    // 一次点击后a的值会被更新为3
    function Demo(){
    const [a,setA] = useState(0)

    function handler(){
    setA(n => n + 1);
    setA(n => n + 1);
    setA(n => n + 1);
    }

    return <button onclick={handler}>{a}</button>
    }

    7. 管理状态的一些原则


    更加结构化的状态有助于提高我们组件的健壮性,以下是一些管理组件内状态时可以参考的原则:

    1. 精简相关状态:如果在更新某个状态时总是需要同步更新其他状态变量,可以考虑将它们合并为一个状态变量。
    2. 避免矛盾状态:避免出现两个或多个状态的值互相矛盾的情况。
    3. 避免冗余状态:一个状态可以由其余状态计算而来时,其不应单独作为一个状态来进行管理。
    4. 避免状态重复:当相同的数据在多个状态变量之间或嵌套对象中重复时,很难使它们保持同步。尽可能减少重复。
    5. 避免深度嵌套状态:嵌套层级过深的状态更新时相对麻烦许多,尽量保持状态扁平化。

    8. 使用useSyncExternalStore订阅外部状态


    useSyncExternalStore是一个React18中新提供的一个Hook,可以用于订阅外部状态。
    它的使用方式如下:

    import { useSyncExternalStore } from 'react';

    function MyComponent() {
    const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
    // ...
    }

    useSyncExternalStore接受三个参数:

    • subscribe:一个订阅函数,用于订阅存储,并返回一个取消订阅的函数。
    • getSnapshot:一个从存储中获取数据快照的函数。在存储未发生变化时,重复调用getSnapshot应该返回相同的值。当存储发生变化且返回值不同时,React会重新渲染组件。
    • getServerSnapshot(可选):一个在服务器端渲染时获取存储初始快照的函数。它仅在服务器端渲染和在客户端进行服务器呈现内容的hydration过程中使用。如果省略该参数,在服务器端渲染组件时会抛出错误。

    举一个具体的例子,我们可能需要获取浏览器的联网状态,并期望在浏览器网络状态发生变化时做一些处理。使用useSyncExternalStore可以很大程度上简化我们的代码,同时使逻辑更加清晰:

    //🔴 Bad Case
    function useOnlineStatus() {
    // Not ideal: Manual store subscription in an Effect
    const [isOnline, setIsOnline] = useState(true);
    useEffect(() => {
    function updateState() {
    setIsOnline(navigator.onLine);
    }

    updateState();

    window.addEventListener('online', updateState);
    window.addEventListener('offline', updateState);
    return () => {
    window.removeEventListener('online', updateState);
    window.removeEventListener('offline', updateState);
    };
    }, []);
    return isOnline;
    }

    function ChatIndicator() {
    const isOnline = useOnlineStatus();
    // ...
    }
    function subscribe(callback) {
    window.addEventListener('online', callback);
    window.addEventListener('offline', callback);
    return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
    };
    }

    // ✅ GoodCase
    function useOnlineStatus() {
    return useSyncExternalStore(
    subscribe, // React won't resubscribe for as long as you pass the same function
    () => navigator.onLine, // How to get the value on the client
    () => true // How to get the value on the server
    );
    }

    function ChatIndicator() {
    const isOnline = useOnlineStatus();
    // ...
    }

    结语


    文章的最后,再来一次无废话总结:

    1. 更清晰地认识了Props与State之间的区别。Props更像是函数的参数,用于组件之间的信息传递;而State更像是组件内部的内存,用于保存组件的状态并进行渲染更新。
    2. 不推荐在一个组件内部嵌套定义其他组件,因为这样会导致渲染速度变慢并容易产生BUG。推荐将子组件提到父组件外部并通过Props传递数据。
    3. 尽量避免使用匿名函数组件,因为在出错时会增加调试的难度。具名组件的出错提示更加直观和准确。
    4. 在使用逻辑运算符&&编写JSX时,左侧最好不要是数字。在JSX中,0会被当作有效的值,而不是假值,为了避免出现问题,可以在左侧的值加上!!进行强制类型转换。
    5. 当在使用全写的Fragment标签时,可以给Fragment标签添加属性key,以优化性能和避免创建额外的组件。
    6. 使用updater function的方式进行状态更新,可以确保在下一次渲染之前多次更新同一个state。这样可以避免批处理机制带来的问题。
    7. 在管理组件内状态时,可以遵循一些原则,如精简相关状态、避免矛盾状态、避免冗余状态、避免状态重复等,以提高组件的健壮性和可维护性。
    8. 使用useSyncExternalStore可以订阅外部状态,它是React 18中新增的Hook。通过订阅函数、获取数据快照的函数以及获取服务器初始快照的函数,我们可以简化订阅外部状态的代码逻辑。

    通过对React文档的深入学习和实践,我对React的理解更加深入了解,希望这些收获也能对大家在学习和使用React时有所帮助。


    作者:星始流年
    链接:https://juejin.cn/post/7233324859932442680
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    不要因bundle size太大责备开发者

    前言 大家好我是飞叶,一个前端UP主,最近外网有个新的SSR框架Qwik比较火,GitHub上它的仓库star数以每天100的速度在增长,今天就通过翻译一篇文章来引入QWIK。 当然如果你不想读这些,我也录了一个视频讲这篇文章。讲解视频在这里 原文链接 ht...
    继续阅读 »

    前言


    大家好我是飞叶,一个前端UP主,最近外网有个新的SSR框架Qwik比较火,GitHub上它的仓库star数以每天100的速度在增长,今天就通过翻译一篇文章来引入QWIK。


    当然如果你不想读这些,我也录了一个视频讲这篇文章。讲解视频在这里



    原文链接 http://www.builder.io/blog/dont-b…



    不要因bundle size太大责备开发者


    让我们谈谈我们构建 Web 应用程序所必须使用的工具,以及这些工具如何欺骗我们。


    开发者们共同的故事


    你要创建一个新项目,此时,你信心满满这个新站点会很快很流畅。
    在一开始,事情看起来确实如此,但是很快你的应用就变大了变复杂了,应用的开启性能变慢了。
    在不知不觉中,您手头上有一个巨大的应用程序,而您却无能为力地修复它。你哪里做错了?


    我们用的每个工具/框架都承诺提供更好、更快的结果,
    但我们通过访问整个互联网里的应用就知道,结果绝不是一个更好、更快的站点。
    谁应该为此负责?开发者吗?


    作为开发者,你是否有被告知:"就是你们搞砸了,你们偷工减料,才导致了一个性能差的站点。"


    这不是你的错


    如果一些网站速度较慢,而另一些网站速度较快,那么当然,责备开发人员可能是有道理的。
    但真实情况是:所有网站都很慢!
    当没有人成功时,你怎么能责怪开发者呢?问题是系统性的。也许这不是开发者的错。


    这是一个关于我们如何构建应用程序、我们使用的工具、工具做出的承诺以及我们最终遇到的缓慢站点的故事。


    只有一个结论。这些工具都过度承诺了,这是整个行业的系统性问题。这不仅仅是几个坏苹果,而是整个万维网。


    代码太多了


    我们都知道问题是什么:代码太多!我们非常擅长创建代码,浏览器无法跟上我们的脚步。
    每个人都告诉你你的网站有太多的 JavaScript,但没有人知道如何缩小它。


    这就像一个 YouTube 健身频道告诉你减肥所需要做的就是少摄入卡路里。
    简单的建议,但成功率令人沮丧。
    原因是该建议忽略了食欲。
    当你又饿又虚弱并且只想到食物时,又有几个人能做到 减少卡路里摄入的意愿呢?
    所以也许减肥成功的秘诀可能不是减少卡路里,而是如何控制你的食欲。


    这个例子很类似于 JavaScript 膨胀的情况。
    我们知道我们需要更少的 JavaScript,
    但是我们有太多需求,除了代码要写,还有太多工具和轮子要用,(才能满足需求)
    但是所有这些 代码 和工具 都会源源不断地使我们的应用越来越大。




    打包的演变历史


    让我们先看看我们是如何陷入这种境地的,然后再讨论前进的道路。


    第 0 代:串联


    在 ECMAScript 模块之前,什么都没有,只是文件。
    打包过程很简单。这些文件被连接在一起并包装在 IIFE 中。


    好处是很难向您的应用程序添加更多代码,因此bundle size保持较小。


    第 1 代:打包器


    ECMAScript 模块来了。
    打包器也出现了:WebPack、Rollup 等。


    然而,npm install 一个依赖并把它打包进去有点太容易了。很快,bundle size就成了一个问题。


    庆幸的是,这些打包器知道如何进行tree shaking和死代码消除。这些功能确保只有用到的的代码才被打包。


    第 2 代:延迟加载


    意识到bundle size过大的问题, 打包器开始提供延迟加载。
    延迟加载很棒,因为它允许将代码分解成许多chunks并根据需要交付给浏览器。
    这很棒,因为它允许从最需要的部分开始 分批交付应用程序。


    问题在于,在实践中,我们是使用框架来构建应用程序的,而框架对打包程序如何将我们的代码分解为延迟加载的块有很大影响。
    问题在于延迟加载块需要引入异步API调用。
    如果框架需要对您的代码进行同步引用,则打包器不能引入延迟加载的块。


    所以我们需要明白,虽然打包器声称他们可以延迟加载代码,而且这也是真的,
    但想做到延迟加载有个前提条件,即我们使用的框架得让开发者使用promise(来懒加载chunk),否则您可能没有太多选择。


    第 3 代:延迟加载不在渲染树中的组件


    框架迅速争先恐后地利用打包器的延迟加载功能,如今几乎所有人都知道如何进行延迟加载。
    但是有一个很大的警告!框架只能延迟加载不在当前渲染树中的组件。




    什么是渲染树?它是构成当前页面的一组组件。
    应用程序通常具有比当前页面上更多的组件。
    通常,渲染树包含视图(这是您当前在浏览器视口中看到的内容)内组件。
    和一部分视图之外的组件。


    假设一个组件在渲染树中。在这种情况下,框架必须下载组件,因为框架需要重建组件的渲染树,(这是hydration的一部分工作)。
    框架只能延迟加载当前不在渲染树中的组件。


    另一点是框架可以延迟加载组件,但总是包含行为。
    因为组件包含了行为,这个懒加载的单位就太大了。如果可以延迟加载的单位更小会更好。
    渲染组件不应要求下载组件的事件处理程序。
    框架应该只在用户交互时才下载事件处理程序,而不是作为组件渲染方法的一部分。根
    据您正在构建的应用程序的类型,事件处理程序可能代表您的大部分代码。
    所以耦合组件的渲染和行为的下载是次优的。


    问题的核心


    仅在需要重新渲染组件时才延迟加载组件渲染函数,并且仅在用户与事件处理程序交互时才延迟加载事件处理程序。
    这样才是最好的!
    默认应该是所有内容都是延迟加载的。


    但这种方法存在一个大问题。问题是框架需要协调其内部状态与 DOM。
    这意味着至少需要一次hydration,来进行完整渲染以重建框架的内部状态。
    在第一次渲染之后,框架可以对其更新进行更准确的把控,但问题已经产生了,因为代码已经下载了。所以我们有两个问题:

    • 框架需要下载并执行组件以在启动时重建渲染树。(请参阅hydration 是纯粹的开销)这会强制下载和执行渲染树中的所有组件。
    • 事件处理程序随组件一起提供,即使在渲染时不需要它们。包含事件处理程序会强制下载不必要的代码。

    因此,当今框架的现状是,必须急切地下载和执行 SSR/SSG 渲染树中的每个组件(及其处理程序)。
    使用当今的框架进行延迟加载有点说谎,因为您并不能在初始页面呈现时进行延迟加载。


    值得指出的是,即使开发人员将延迟加载边界引入 SSR/SSG 初始页面,也无济于事。
    框架仍需下载并执行 SSR/SSG 响应中的所有组件;因此,只要组件在渲染树中,框架就必须急切地加载开发人员试图延迟加载的组件。


    渲染树中组件的急切下载是问题的核心,开发人员对此无能为力。
    尽管如此,这并不能阻止开发人员因网站运行缓慢而受到指责。


    下一代:细粒度的延迟加载


    那么,我们该何去何从?显而易见的答案是我们需要更细粒度。该解决方案既明显又难以实施。我们需要:

    • 更改框架,这样它们就不会在hydration阶段急切地加载渲染树。
    • 允许组件渲染函数 独立于组件事件处理程序 单独下载。

    如果您的框架可以完成上述两个部分,那么用户将看到巨大的好处。
    应用程序的启动要求很少,因为启动时不需要进行渲染(内容已经在 SSR/SSG 处渲染)。
    下载的代码更少:当框架确定需要重新渲染特定组件时,框架可以通过下载渲染函数来实现,而无需下载所有事件处理程序。


    细粒度的延迟加载将是网站启动性能的巨大胜利。
    它要快得多,因为下载的代码量将与用户交互性成正比,而不是与初始渲染树的复杂性成正比。
    您的网站会变得更快,不是因为我们更擅长使代码更小,而是因为我们更擅长只下载我们需要的东西,而不是预先下载所有东西。




    入口点 entry point


    拥有一个可以进行细粒度延迟加载的框架是不够的。
    因为,要利用细粒度的延迟加载,您必须首先拥有要延迟加载的bundles。


    为了让打包器创建延迟加载的chunk,打包器需要每个块的入口点。
    如果您的应用程序只有单个入口点,则打包器无法创建多个chunks。
    如果您的应用程序只有单个入口点,即使你的框架可以进行细粒度的延迟加载,它也没有什么可以延迟加载的。


    现在创建入口点很麻烦,因为它需要开发人员编写额外的代码。
    在开发应用程序时,我们真的只能考虑一件事,那就是写功能。
    让开发人员同时考虑他们正在构建的功能和延迟加载对开发人员来说是不公平的。
    所以在实践中,为打包器创建入口点很麻烦。


    所需要的是一个无需开发人员考虑就可以创建入口点的框架。
    为打包程序创建入口点是框架的责任,而不是开发人员的责任。
    开发人员的职责是构建功能。
    该框架的职责是考虑应该如何完成该功能的底层实现。
    如果框架不这样做,那么它就不能完全满足开发人员的需求。


    担心切入点太多?


    目标应该是创建尽可能多的入口点。
    但是,有些人可能会问,这是不是就会导致下载很多小块而不是几个大块吗?答案是响亮的“不”。


    如果没有入口点,打包器就无法创建chunk。
    但是打包器可以将多个入口点放入一个chunk中。
    您拥有的入口点越多,您以最佳方式组装bundle的自由度就越大。
    入口点给了你优化bundle的自由。所以它们越多越好。


    未来的框架


    下一代框架将需要解决这些问题:

    • 拥有人们喜欢的开发体验DX。
    • 对代码进行细粒度的延迟加载。
    • 自动生成大量入口点以支持细粒度的延迟加载。

    开发人员将像现在一样构建他们的网站,但这些网站不会在应用程序启动时用下载和执行一个很大的bundle来压倒浏览器。


    Qwik是一个在设计时考虑到这些原则的框架。Qwik细粒度延迟加载是针对每个事件处理程序、渲染函数和effect的。


    结论


    我们的网站越来越大,看不到尽头。
    它们之所以大,是因为这些网站今天比以前做得更多——更多的功能、动画等。并且这种趋势将继续下去。


    上述问题的解决方案是对代码进行细粒度的延迟加载,这样浏览器就不会在初始页面加载时不堪重负。


    我们的打包工具支持细粒度的延迟加载,但我们的框架不支持。
    框架hydration强制渲染树中的所有组件在hydration时加载。(目前的SSR框架唯一的延迟加载是 当前不在渲染树中的组件。)
    即使事件处理程序可能是代码的大部分,并且hydration并不需要事件处理器代码,现在的SSR框架还是随组件的下载一并下载了事件处理程序.


    因为打包器可以细粒度的延迟加载,但我们的框架不能,我们无法识别其中的微妙之处。
    导致的结果就是我们将网站启动缓慢归咎于开发人员,
    因为我们错误地认为他们本可以采取一些措施来防止这种情况发生,尽管现实是他们在这件事上几乎没有发言权。


    我们需要将细粒度延迟加载设计为框架核心功能的新型框架(例如Qwik )。
    我们不能指望开发者承担这个责任;他们已经被各种功能淹没了。
    框架需要考虑延迟加载运行时以及创建入口点,以便打包程序可以创建块以进行延迟加载。
    下一代框架带来的好处将超过迁移到它们所花费的成本。


    作者:飞叶_前端
    链接:https://juejin.cn/post/7154948728871190535
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »