注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

你会用nginx部署前端项目吗

web
前端项目的部署以前一直是把静态资源放到后端工程中,随后端部署一起部署。随着前后端分离开发模式的流行,前端项目可以单独部署了,目前最流行的方式使用nginx来部署。 对于前端项目来说,nginx主要有两个功能: 对静态资源做托管,即作为一个静态资源服务器; 对...
继续阅读 »

前端项目的部署以前一直是把静态资源放到后端工程中,随后端部署一起部署。随着前后端分离开发模式的流行,前端项目可以单独部署了,目前最流行的方式使用nginx来部署。


对于前端项目来说,nginx主要有两个功能:



  • 对静态资源做托管,即作为一个静态资源服务器

  • 对动态资源做反向代理,即代理后台接口服务,防止跨域


路由配置


nginx配置最多就是路由配置,路由配置又有几种写法。


1. =


location = /111/ {
default_type text/plain;
return 200 "111 success";
}

location 和路径之间加了个 =,代表精准匹配,也就是只有完全相同的 url 才会匹配这个路由。


image.png


在路径后面添加了aa,那么就不是精确匹配了,所以是404


image.png


2. 不带 =


代表根据前缀匹配,后面可以是任意路径


location /222 {
default_type text/plain;
// 这里的 $uri 是取当前路径。
return 200 $uri;
}

image.png


3. 支持正则匹配~


// 匹配以/333/bbb开头,以.html结尾的路径
location ~ ^/333/bbb.*\.html$ {
default_type text/plain;
return 200 $uri;
}

image.png


上面的~是区分大小写的,如果不区分大小写是~*


// 匹配以/333/bbb开头,以.html结尾的路径
location ~* ^/333/bbb.*\.html$ {
default_type text/plain;
return 200 $uri;
}

4. ^~代表优先级


下面的配置有两个路径是以/444开头的:


location ~* ^/444/AAA.*\.html$ {
default_type text/plain;
return 200 $uri;
}
location ~ /444/ {
default_type text/plain;
return 200 $uri;
}

如果访问/444/AAA45.html,就会直接命中第一个路由,如果我想命中/444/呢? 加上^就好了。


location ^~ /444/ {
default_type text/plain;
return 200 $uri;
}

也就是说 ^~ 能够提高前缀匹配的优先级。


总结一下,location 语法一共有四种:




  1. location = /aaa 是精确匹配 /aaa 的路由;




  2. location /bbb 是前缀匹配 /bbb 的路由。




  3. location ~ /ccc.*.html 是正则匹配,可以再加个 * 表示不区分大小写 location ~* /ccc.*.html;




  4. location ^~ /ddd 是前缀匹配,但是优先级更高。




这 4 种语法的优先级是这样的:


精确匹配(=) > 高优先级前缀匹配(^~) > 正则匹配(~ / ~*) > 普通前缀匹配


root 与 alias


nginx指定文件路径有两种方式rootaliasrootalias主要区别在于nginx如何解释location后面的uri,这会使两者以不同的方式将请求映射到服务器文件上。



  1. root的处理结果是:root路径 + location路径;

  2. alias的处理结果是:使用alias路径替换location路径;


alias是一个目录别名的定义,root则是最上层目录的定义。


需要注意的是alias后面必须要用/结束,否则会找不到文件的,而root则可有可无。另外,alias只能位于location块中。


root示例:


location ^~ /test/ {
root /www/root/html/;
}

如果一个请求的 uri 是 /test/a.html时,web服务器将会返回服务器上的/www/root/html/test/a.html的文件。


alias示例:


location ^~ /test/ {
alias /www/root/html/new_test/;
}

如果一个请求的 uri 是 /test/a.html 时,web服务器将会返回服务器上的/www/root/html/new_test/a.html的文件。


注意, 这里是new_test,因为alias会把location后面配置的路径丢弃掉,把当前匹配到的目录指向到指定的目录。


二级目录


有时候需要在一个端口下,部署多个项目,那么这时可以采用二级目录的形式来部署。


采用二级目录来部署会有一些坑,比如,当我请求 http://xxxxxxxx.com/views/basedata 的时候,浏览器自动跳转到了http://xxxxxxxxm:8100/store/views/basedata/


这是什么原因呢?


最根本的问题是因为http://xxxxxxxx.com/views/basedata后面没有/,所以就触发了nginx301重定向,重定向到了http://xxxxxxxxm:8100/store/views/basedata/,因此,只要避免触发重定向即可。


如果你能忍受直接使用如 http://xxxxxxxxm:8100/store/views/basedata/ 这样最后带/的地址,那也就没有什么问题了。


那为什么会触发重定向呢?


当用户请求 http.xxxxxx.cn/osp 时,这里的 $uri 就是 /ospnginx 会尝试到alias或 root 指定的目录下找这个文件。


如果存在名为 {alias指定目录}/osp 的文件,注意这里是文件,不是目录,就直接把这个文件的内容发送给用户。


很显然,目录中没有叫 osp 的文件,于是就看 osp/,增加了一个 /,也就是看有没有名为 {alias指定目录}/osp/ 的目录。


即,当我们访问 uri 时,如果访问资源为一个目录,并且 uri 没有以正斜杠 / 结尾,那么nginx 服务就会返回一个301跳转,目标地址就是要加一个正斜杠/


一种最简单的方式就是直接访问一个具体的文件,如 http.xxxxxx.cn/osp/index.html,这样就不会发生重定向了。但是,这样方式不够优雅,每次都要输入完整的文件路径。


另一种方式是调整 nginx 中关于重定向的配置,nginx 重定向中的三个配置:absolute_redirectserver_name_in_redirectport_in_redirect


absolute_redirect通过该指令控制 nginx 发出的重定向地址是相对地址还是绝对地址:


1、如果设置为 off,则 nginx 发出的重定向将是相对的,没有域名和端口, 也就没有server_name_in_redirectport_in_redirect什么事儿了。


image.png


加了这个配置后,尽管也会发生重定向,但是不会在路径上加上域名和端口了。


2、如果设置为 on,则 nginx 发出的重定向将是绝对的;只有 absolute_redirect 设置为 onserver_name_in_redirectport_in_redirect 的设置才有作用。


image.png


nginx 开启 gzip 静态压缩提升效率


gzip 是一种格式,也是一种 linux 下的解压缩工具,我们使用 gzipapp.js 文件压缩后,原始文件就变为了以.gz结尾的文件,同时文件大小从 42571 减小到 11862。


image.png


目前,对静态资源压缩有两种形式:



  • 动态压缩: 服务器在返回任何的静态文件之前,由服务器对每个请求压缩在进行输出。

  • 静态压缩:服务器直接使用现成的扩展名为 .gz 的预压缩文件,直接进行输出。


我们知道 gzipCPU 密集型的,实时动态压缩比较消耗 CPU 资源。为进一步提高 nginx 的性能,我们可以使用静态 gzip 压缩,提前将需要压缩的文件压缩好,当请求到达时,直接发送压缩好的.gz文件,如此就减轻了服务器 CPU 的压力,提高了性能。


因此,我们一般采用静态压缩的方式,实现静态压缩有以下两个步骤:


1. 生成gzip压缩文件


在利用webpack打包的时候,我们就把文件进行压缩,配置如下:


const isProduction = process.env.NODE_ENV === 'production'

if (isProduction) {
config.plugins.push(
new CompressionWebpackPlugin({
// 采用gzip进行压缩
algorithm: 'gzip',
test: /\.js$|\.html$|\.json$|\.css/,
threshold: 10240
})
)
}

可以看到,多生成了一个以.gz结尾的文件,然后把.gz后缀的文件上传到服务器中即可。


image.png


2. 在 nginx 开启支持静态压缩的模块


nginx配置中加上如下配置:


gzip_static on;

如果不加的话,访问的时候就会找不到,报404错误,因为服务器只有.gz的文件,没有原始文件。


总结


前端项目nginx部署主要的配置基本上就是上面提到的这些。


首先是location路由的四种写法;


接着就是分清楚rootalias的区别;


当项目较多时需要使用二级路由时,需要注意重定向的配置;


如果你的项目文件较大,可以开启gzip

作者:小p
来源:juejin.cn/post/7270902621065560120
压缩提升传输效率。

收起阅读 »

坏了!要长脑子了...这么多前端框架没听过

web
市面上有很多不同的 JavaScript 框架,很难对它们一一进行跟踪。在本文中,我们将重点介绍最受欢迎的几种,并探讨开发人员喜欢或不喜欢它们的原因。 React 官网链接 React 是一个用于构建用户界面的 JavaScript 库。它由 Faceboo...
继续阅读 »

市面上有很多不同的 JavaScript 框架,很难对它们一一进行跟踪。在本文中,我们将重点介绍最受欢迎的几种,并探讨开发人员喜欢或不喜欢它们的原因。


React



官网链接


React 是一个用于构建用户界面的 JavaScript 库。它由 Facebook 和一个由个人开发者和公司组成的社区维护。React 可以作为开发单页或移动应用程序的基础。然而,React 只关心将数据呈现给 DOM,因此创建 React 应用程序通常需要使用额外的库来进行状态管理、路由和与 API 的交互。React 还用于构建可重用的 UI 组件。从这个意义上讲,它的工作方式很像 Angular 或 Vue 等 JavaScript 框架。然而,React 组件通常以声明式方式编写,而不是使用命令式代码,这使得它们更容易阅读和调试。正因为如此,许多开发人员更喜欢使用 React 来构建 UI 组件,即使他们不使用它作为整个前端框架。


优点:



  • React 快速而高效,因为它使用虚拟 DOM 而不是操纵真实的 DOM。

  • React 很容易学习,因为它的声明性语法和清晰的文档。

  • React 组件是可重用的,使代码维护更容易。


缺点:



  • React 有一个很大的学习曲线,因为它是一个复杂的 JavaScript 库。

  • React 不是一个成熟的框架,因此它需要使用额外的库来完成许多任务。


Next.js



官网链接


Next.js 是一个 javascript 库,支持 React 应用程序的服务器端渲染。这意味着 next.js 可以在将 React 应用程序发送到客户端之前在服务器上渲染它。这有几个好处。首先,它允许您预呈现组件,以便当用户请求它们时,它们已经在客户机上可用。其次,它允许爬虫更容易地索引你的内容,从而为你的 React 应用程序提供更好的 SEO。最后,它可以通过减少客户机为呈现页面而必须执行的工作量来提高性能。


以下是开发者喜欢 Next.js 的原因:




  • js 使得无需做任何配置就可以轻松地开始服务器端渲染。




  • js 会自动对应用程序进行代码拆分,以便每个页面只在被请求时加载,这可以提高性能。
    缺点:




  • 如果不小心,next.js 会使应用程序代码库变得更复杂,更难以维护。




  • 一些开发人员发现 next.js 的内置特性固执己见且不灵活。




Vue.js



官网链接


Vue.js 是一个用于构建用户界面和单页应用程序的开源 JavaScript 框架。与 React 和 Angular 等其他框架不同,Vue.js 被设计为轻量级且易于使用。Vue.js 库可以与其他库和框架结合使用,也可以作为创建前端 web 应用程序的独立工具使用。Vue.js 的一个关键特性是它的双向数据绑定,当模型发生变化时,它会自动更新视图,反之亦然。这使得它成为构建动态用户界面的理想选择。此外,Vue.js 还提供了许多内置功能,如模板系统、响应系统和事件总线。这些特性使创建复杂的应用程序成为可能,而不必依赖第三方库。因此,Vue.js 已经成为近年来最流行的 JavaScript 框架之一。


优点:



  • Vue.js 很容易学习,因为它的小尺寸和清晰的文档。

  • Vue.js 组件是可重用的,这使得代码维护更容易。

  • 由于虚拟 DOM 和异步组件加载,Vue.js 应用程序非常快。


缺点:



  • 虽然 Vue.js 很容易学习,但如果你想掌握它的所有功能,它有一个很大的学习曲线。

  • Vue.js 没有像其他框架那样多的库和工具。


Angular



官网链接


Angular 是一个 JavaScript 框架,用于用 JavaScript、html 和 Typescript 构建 web 应用和应用。Angular 是由 Google 创建和维护的。Angular 提供了双向数据绑定,这样对模型的更改就会自动传播到视图。它还提供了一种声明性语法,使构建动态 ui 变得容易。最后,Angular 提供了许多有用的内置服务,比如 HTTP 请求处理,以及对路由和模板的支持。


优点:



  • Angular 有一个庞大的社区和许多可用的库和工具。

  • Angular 很容易学习,因为它有组织良好的文档和清晰的语法。


缺点:



  • 虽然 Angular 很容易学习,但如果你想掌握它的所有特性,它有一个很大的学习曲线。

  • Angular 不像其他一些框架那样轻量级。


Svelte



官网链接


简而言之,Svelte 是一个类似于 React、Vue 或 Angular 的 JavaScript 框架。然而,这些框架使用虚拟 DOM(文档对象模型)来区分视图之间的变化,而 Svelte 使用了一种称为 DOM 区分的技术。这意味着它只更新 DOM 中已更改的部分,从而实现更高效的呈现过程。此外,Svelte 还包括一些其他框架没有的内置优化,例如自动批处理 DOM 更新和代码分割。这些特性使 Svelte 成为高性能应用程序的一个很好的选择。


优点:




  • Svelte 有其他框架没有的内置优化,比如代码分割。




  • 由于其清晰的语法和组织良好的文档,Svelte 很容易学习。
    缺点:




  • 虽然 Svelte 很容易学习,但如果你想掌握它的所有功能,它有一个很大的学习曲线。




  • Svelte 没有像其他框架那样多的库和工具。




Gatsby



官网链接


Gatsby 是一个基于 React 的免费开源框架,可以帮助开发人员构建快速的网站和应用程序。它使用尖端技术,使建立网站和应用程序的过程更加高效。它的一个关键特性是能够预取资源,以便在需要时立即可用。这使得盖茨比网站非常快速和响应。使用 Gatsby 的另一个好处是,它允许开发人员使用 GraphQL 查询来自任何来源的数据,从而使构建复杂的数据驱动应用程序变得容易。此外,Gatsby 附带了许多插件,使其更易于使用,包括用于 SEO,分析和图像优化的插件。所有这些因素使 Gatsby 成为构建现代网站和应用程序的一个非常受欢迎的选择。


优点:




  • 由于使用了预取,Gatsby 网站的速度和响应速度非常快。




  • 由于对 GraphQL 的支持,Gatsby 使构建复杂的数据驱动应用程序变得容易。




  • Gatsby 附带了许多插件,使其更易于使用。
    缺点:




  • 虽然 Gatsby 很容易使用,但如果你想掌握它的所有功能,它有一个很大的学习曲线。




  • Gatsby 没有像其他框架那样多的库和工具。




Nuxt.js



官网链接


js 是一个用于构建 JavaScript 应用程序的渐进式框架。它基于 Vue.js,并附带了一组工具和库,可以轻松创建可以在服务器端和客户端呈现的通用应用程序。js 还提供了一种处理异步数据和路由的方法,这使得它非常适合构建高度交互的应用程序。此外,nuxt.js 附带了一个 CLI 工具,可以很容易地构建新项目并构建、运行和测试它们。使用 nuxt.js,您可以创建快速、可靠和可扩展的令人印象深刻的 JavaScript 应用程序。


优点:




  • js 易于使用和扩展。




  • 由于服务器端渲染,nuxt.js 应用程序快速响应。
    缺点:




  • 虽然 nuxt.js 很容易使用,但如果你想掌握它的所有功能,它有一个很大的学习曲线。




  • nuxt.js 没有像其他框架那样多的库和工具。




Ember.js



官网链接


Ember.js 以其优于配置的约定方法而闻名,这使得开发人员更容易开始使用该框架。它还为数据持久化和路由等常见任务提供了内置库,从而加快了开发速度。尽管 Ember.js 有一个陡峭的学习曲线,但它为开发人员提供了创建富 web 应用程序的灵活性和强大功能。如果你正在寻找一个前端 JavaScript 框架来构建 spa, Ember.js 绝对值得考虑。


优点:




  • Ember.js 使用约定而不是配置,这使得它更容易开始使用框架。




  • Ember.js 为数据持久化和路由等常见任务提供了内置库。




  • Ember.js 为开发人员创建富 web 应用程序提供了很大的灵活性和能力。
    缺点:




  • Ember.js 有一个陡峭的学习曲线。




  • Ember.js 没有像其他框架那样多的库和工具。




Backbone.js



官网链接


Backbone.js 是一个轻量级 JavaScript 库,允许开发人员创建单页面应用程序。它基于模型-视图-控制器(MVC)体系结构,这意味着它将数据和逻辑从用户界面中分离出来。这使得代码更易于维护和扩展,也使创建复杂的应用程序变得更容易。Backbone.js 还包含了许多使其成为开发移动应用程序的理想选择的特性,例如将数据绑定到 HTML 元素的能力以及对触摸事件的支持。因此,对于想要创建快速响应的应用程序的开发人员来说,Backbone.js 是一个受欢迎的选择。


优点:




  • js 是轻量级的,只是一个库,而不是一个完整的框架。




  • js 很容易学习和使用。




  • Backbone.js 具有很强的可扩展性,可以使用许多第三方库。
    缺点:




  • Backbone.js 不像其他框架那样提供那么多的内置功能。




  • 与其他一些框架相比,Backbone.js 只有一个小社区。




结论


总之,虽然有许多不同的 JavaScript 框架可供选择,但最流行的框架仍然相对稳定。每一种都有自己的优点和缺点,开发人员在决定在他们的项目中使用哪一种时必须权衡。虽然没有一个框架是完美的,但每个框架都有一些可以使开发更容易或更快的东西。


在选择框架时,每个人都应该考虑他们项目的具体需求,以及他们团队的技能和他们必须投入学习新框架的时间。通过考虑所有这些因素,您可以为您的项目选择最好的 JavaScript 框架!


参考链接:
blog.risingstack.com/

best-javasc…

收起阅读 »

虚拟dom

vue中的虚拟dom 简介 首先vue会把模板编译成render函数,运行render函数生成虚拟dom 虚拟dom通过 diff算法 对比差异 渲染不同的部分 然后更新视图 为什么会需要虚拟dom 在主流框架 Angular , Vue.js (1.0)...
继续阅读 »

vue中的虚拟dom


简介



首先vue会把模板编译成render函数,运行render函数生成虚拟dom


虚拟dom通过 diff算法 对比差异 渲染不同的部分 然后更新视图



为什么会需要虚拟dom


在主流框架 Angular , Vue.js (1.0)React 中都有一个共同点,那就是它们都不知道哪些状态(state)变了。因此就需要进行比对,在React中使用的虚拟dom比对, Angular 中使用的是脏检查的流程



而在 Vue.js中使用的是变化侦测的方式,它在一定程度上知道具体哪些状态发生了变化,这样就可以通过更细粒度的绑定来更新视图。也就是说,在Vue.js中,当状态发生变化时,它在一定程度上知道哪些节点使用了这个状态,从而对这些节点进行更新操作,根本不需要比对



但是这样做的代价就是,粒度太细,每一个都有对应的 watcher 来观察状态变化,这样就会浪费一些内存开销,绑定的越多开销越大,如果这运用在一个大型项目中,那么他的开销无疑是非常大的


因此从 Vue.js 2.0 开始借鉴 React 中的虚拟DOM ,组件级别是一个watcher实例,就是说即便一个组件内有10个节点使用了某个状态,但其实也只有一个watcher在观察这个状态的变化。


什么是虚拟dom


虚拟DOM是通过状态生成一个虚拟节点树(vnode) ,然后使用虚拟节点树进行渲染。 在渲染之前,会使用新生成的虚拟节点树和上一次生成的虚拟节点树进行对比 (diff算法) ,只渲染不同的部分



虚拟节点树其实是由组件树建立起来的整个虚拟节点(Virtual Node,也经常简写为vnode)树



在Vue.js中,我们使用模板来描述状态DOM之间的映射关系。Vue.js通过编译将模板转换成渲染函数(render),执行渲染函数就可以得到一个虚拟节点树,使用这个虚拟节点树就可以渲染页面


模板编译成render函数


将模板编译成渲染函数可以分两个步骤,先将模板解析成AST(Abstract Syntax Tree,抽象语法树),然后再使用AST生成渲染函数。


但是由于静态节点不需要总是重新渲染,所以在生成AST之后、生成渲染函数之前这个阶段,需要做一个操作,那就是遍历一遍AST,给所有静态节点做一个标记,这样在虚拟DOM中更新节点时,如果发现节点有这个标记,就不会重新渲染它。所以,在大体逻辑上,模板编译分三部分内容:

  • 将模板解析为AST
  • 遍历AST标记静态节点
  • 使用AST生成渲染函数

虚拟dom做了什么


虚拟DOM在Vue.js中所做的事情其实并没有想象中那么复杂,它主要做了两件事。

  • 提供与真实DOM节点所对应的虚拟节点vnode。
  • 将虚拟节点vnode和旧虚拟节点oldVnode进行比对,然后更新视图。

对两个虚拟节点对比是虚拟dom 中最核心的算法 (diff),它可以判断出哪些节点发生了变化,从而只对发生了变化的节点进行更新操作


小结


虚拟DOM是将状态映射成视图的众多解决方案中的一种,它的运作原理是使用状态生成虚拟节点,然后使用虚拟节点渲染视图。


为什么会需要虚拟dom

框架设计

Vue 和 React 框架设计理念都是基于数据驱动的,当数据发生变化时 就要去更新视图,要想知道在页面众多元素中改动数据的元素 并根据改动后的数据去更新视图 是非常困难的



所以 Vue 和 React 中都会有一个 Render函数 或者类似于Render函数的功能,当数据变化时 全量生成Dom 元素
如果是直接操作 真实Dom 的话 是很昂贵的,就会严重拖累效率,所以就不生成真实的Dom,而是生成虚拟的Dom
当数据变化时就是 对象 和 对象 进行一个对比 ,这样就能知道哪些数据发生了改变 从而去操作改变的数据后的Dom元素



这也是一个 “妥协的结果”


跨平台

现阶段的框架他不仅仅能在浏览器里面使用,在小程序,移动端,或者桌面端也可以使用,但是真实Dom仅仅指的是在浏览器的环境下使用,因此他不能直接生成真实Dom ,所以选择生成一个在任何环境下都能被认识的虚拟Dom
最后根据不同的环境,使用虚拟Dom 去生成界面,从而实现跨平台的作用 --- 一套代码在多端运行


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

不会封装hook? 看下ahooks这6个hook是怎么做的

1 useUpdate 在react函数组件中如何强制组件进行刷新? 虽然react没有提供原生的方法,但是我们知道当state值变化的时候,react函数组件就会刷新,所以useUpdate就是利用了这一点,源码如下:import { useCallback...
继续阅读 »

1 useUpdate


在react函数组件中如何强制组件进行刷新? 虽然react没有提供原生的方法,但是我们知道当state值变化的时候,react函数组件就会刷新,所以useUpdate就是利用了这一点,源码如下:

import { useCallback, useState } from 'react';

const useUpdate = () => {
const [, setState] = useState({});

return useCallback(() => setState({}), []);
};

export default useUpdate;

可以看到useUpdate的返回值函数,就是每次都用一个新的对象调用setState,触发组件的更新。


2 useMount


react函数组件虽然没有了mount的生命周期,但是我们还会有这种需求,就是在组件第一次渲染之后执行一次的需求,就可以封装useEffect实现这个需求, 只需要把依赖项设置成空数组,那么就只在渲染结束后,执行一次回调:

import { useEffect } from 'react';

const useMount = (fn: () => void) => {

useEffect(() => {
fn?.();
}, []);
};

export default useMount;


3 useLatest


react函数组件是一个可中断,可重复执行的函数,所以在每次有state或者props变化的时候,函数都会重新执行,我们知道函数的作用域是创建函数的时候就固定下来的,如果其中的内部函数是不更新的,那么这些函数获取到的外部变量就是不会变的。例如:

import React, { useState, useEffect } from 'react';
import { useLatest } from 'ahooks';


export default () => {
const [count, setCount] = useState(0);

useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, []);

return (
<>
<p>count: {count}</p>
</>
);
};

这是一个定时更新count值的例子,但是上边的代码只会让count一直是1,因为setInterval中的函数在创建的时候它的作用域就定下来了,它拿到的count永远是0, 当执行了setCount后,会触发函数的重新执行,重新执行的时候,虽然count值变成了1,但是这个count已经不是它作用域上的count变量了,函数的每次执行都会创建新的环境,而useState, useRef 等这些hooks 是提供了函数重新执行后保持状态的能力, 但是对于那些没有重新创建的函数,他们的作用域就永远的停留在了创建的时刻。 如何让count正确更新, 简单直接的方法如下,在setCount的同时,也直接更新count变量,也就是直接改变这个闭包变量的值,这在JS中也是允许的。

import React, { useState, useEffect } from 'react';
import { useLatest } from 'ahooks';


export default () => {
let [count, setCount] = useState(0);

useEffect(() => {
const interval = setInterval(() => {
count = count + 1
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, []);

return (
<>
<p>count: {count}</p>
</>
);
};

setCount是为了让函数刷新,并且更新函数的count值,而直接给count赋值,是为了更新定时任务函数中维护的闭包变量。 这显然不是一个好的解决办法,更好的办法应该是让定时任务函数能够拿到函数最新的count值。
useState返回的count每次都是新的变量,也就是变量地址是不同的,应该让定时任务函数中引用一个变量地址不变的对象,这个对象中再记录最新的count值,而实现这个功能就需要用到了useRef,它就能帮助我们在每次函数刷新都返回相同变量地址的对象, 实现方式如下:

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

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

const latestCount = useRef(count)
latestCount.current = count

useEffect(() => {
const interval = setInterval(() => {
setCount(latestCount.current + 1)
}, 1000)
return () => clearInterval(interval)
}, [])

return (
<>
<p>count: {count}</p>
</>
)
}


可以看到定时函数获取的latestCount永远是定义时的变量,但因为useRef,每次函数执行它的变量地址都不变,并且还把count的最新值,赋值给了latestCount.current, 定时函数就可以获取到了最新的count值。
所以这个功能可以封装成了useLatest,获取最新值的功能。

import { useRef } from 'react';

function useLatest<T>(value: T) {
const ref = useRef(value);
ref.current = value;

return ref;
}

export default useLatest;


上边的例子是为了说明useLatest的作用,但针对这个例子,只是为了给count+1,还可以通过setCount方法本身获取,虽然定时任务函数中的setCount页一直是最开始的函数,但是它的功能是可以通过传递函数的方式获取到最新的count值,代码如下:

  const [count, setCount] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setCount(count=>count+1)
}, 1000)
return () => clearInterval(interval)
}, [])

4 useUnMount


有了useMount就会有useUnmount,利用的就是useEffect的函数会返回一个cleanup函数,这个函数在组件卸载和useEffect的依赖项变化的时候触发。 正常情况 我们应该是useEffect的时候做了什么操作,返回的cleanup函数进行相应的清除,例如useEffect创建定时器,那么返回的cleanup函数就应该清除定时器:

 const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
count = count + 1
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, []);

所以useUnMount就是利用了这个cleanup函数实现useUnmount的能力,代码如下:

import { useEffect } from 'react';
import useLatest from '../useLatest';

const useUnmount = (fn: () => void) => {
const fnRef = useLatest(fn);

useEffect(
() => () => {
fnRef.current();
},
[],
);
};

export default useUnmount;


使用了useLatest存放fn的最新值,写了一个空的useEffect,依赖是空数组,只在函数卸载的时候执行。


5 useToggle和useBoolean


useToggle 封装了可以state在2个值之间的变化,useBoolean则是利用了useToggle,固定2个值只能是true和false。 看下他们的源码:

function useToggle<D, R>(defaultValue: D = false as unknown as D, reverseValue?: R) {
const [state, setState] = useState<D | R>(defaultValue);

const actions = useMemo(() => {
const reverseValueOrigin = (reverseValue === undefined ? !defaultValue : reverseValue) as D | R;

const toggle = () => setState((s) => (s === defaultValue ? reverseValueOrigin : defaultValue));
const set = (value: D | R) => setState(value);
const setLeft = () => setState(defaultValue);
const setRight = () => setState(reverseValueOrigin);

return {
toggle,
set,
setLeft,
setRight,
};
// useToggle ignore value change
// }, [defaultValue, reverseValue]);
}, []);

return [state, actions];
}

可以看到,调用useToggle的时候可以设置初始值和相反值,默认初始值是false,actions用useMemo封装是为了提高性能,没有必要每次渲染都重新创建这些函数。setLeft是设置初始值,setRight是设置相反值,set是用户随意设置,toggle是切换2个值。
useBoolean则是在useToggle的基础上进行了封装,让我们用起来对更加的简洁方便。

export default function useBoolean(defaultValue = false): [boolean, Actions] {
const [state, { toggle, set }] = useToggle(defaultValue);

const actions: Actions = useMemo(() => {
const setTrue = () => set(true);
const setFalse = () => set(false);
return {
toggle,
set: (v) => set(!!v),
setTrue,
setFalse,
};
}, []);

return [state, actions];
}

总结


本文介绍了ahooks中封装的6个简单的hook,虽然简单,但是可以通过他们的做法,学习到自定义hook的思路和作用,就是把一些能够重用的逻辑封装起来,在实际项目中我们有这个意识就可以封装出适合项目的hook。


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

你的代码不堪一击!太烂了!

前言 小王,你的页面白屏了,赶快修复一下。小王排查后发现是服务端传回来的数据格式不对导致,无数据时传回来不是 [] 而是 null, 从而导致 forEach 方法报错导致白屏,于是告诉测试,这是服务端的错误导致,要让服务端来修改,结果测试来了一句:“服务端返...
继续阅读 »

前言


小王,你的页面白屏了,赶快修复一下。小王排查后发现是服务端传回来的数据格式不对导致,无数据时传回来不是 [] 而是 null, 从而导致 forEach 方法报错导致白屏,于是告诉测试,这是服务端的错误导致,要让服务端来修改,结果测试来了一句:“服务端返回数据格式错误也不能白屏!!” “好吧,千错万错都是前端的错。” 小王抱怨着把白屏修复了。


刚过不久,老李喊道:“小王,你的组件又渲染不出来了。” 小王不耐烦地过来去看了一下,“你这个属性data 格式不对,要数组,你传个对象干嘛呢。”老李反驳: “ 就算 data 格式传错,也不应该整个组件渲染不出来,至少展示暂无数据吧!” “行,你说什么就是什么吧。” 小王又抱怨着把问题修复了。


类似场景,小王时不时都要经历一次,久而久之,大家都觉得小王的技术太菜了。小王听到后,倍感委屈:“这都是别人的错误,反倒成为我的错了!”


等到小王离职后,我去看了一下他的代码,的确够烂的,不堪一击!太烂了!下面来吐槽一下。


一、变量解构一解就报错


优化前

const App = (props) => {
const { data } = props;
const { name, age } = data
}

如果你觉得以上代码没问题,我只能说你对你变量的解构赋值掌握的不扎实。



解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于 undefinednull 无法转为对象,所以对它们进行解构赋值时都会报错。



所以当 dataundefinednull 时候,上述代码就会报错。


优化后

const App = (props) => {
const { data } = props || {};
const { name, age } = data || {};
}

二、不靠谱的默认值


估计有些同学,看到上小节的代码,感觉还可以再优化一下。


再优化一下

const App = (props = {}) => {
const { data = {} } = props;
const { name, age } = data ;
}

我看了摇摇头,只能说你对ES6默认值的掌握不扎实。



ES6 内部使用严格相等运算符(===)判断一个变量是否有值。所以,如果一个对象的属性值不严格等于 undefined ,默认值是不会生效的。



所以当 props.datanull,那么 const { name, age } = null 就会报错!


三、数组的方法只能用真数组调用


优化前:

const App = (props) => {
const { data } = props || {};
const nameList = (data || []).map(item => item.name);
}

那么问题来了,当 data123 , data || [] 的结果是 123123 作为一个 number 是没有 map 方法的,就会报错。


数组的方法只能用真数组调用,哪怕是类数组也不行。如何判断 data 是真数组,Array.isArray 是最靠谱的。


优化后:

const App = (props) => {
const { data } = props || {};
let nameList = [];
if (Array.isArray(data)) {
nameList = data.map(item => item.name);
}
}

四、数组中每项不一定都是对象


优化前:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => `我的名字是${item.name},今年${item.age}岁了`);
}
}

一旦 data 数组中某项值是 undefinednull,那么 item.name 必定报错,可能又白屏了。


优化后:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => `我的名字是${item?.name},今年${item?.age}岁了`);
}
}

? 可选链操作符,虽然好用,但也不能滥用。item?.name 会被编译成 item === null || item === void 0 ? void 0 : item.name,滥用会导致编辑后的代码大小增大。


二次优化后:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
}

五、对象的方法谁能调用


优化前:

const App = (props) => {
const { data } = props || {};
const nameList = Object.keys(data || {});
}

只要变量能被转成对象,就可以使用对象的方法,但是 undefinednull 无法转换成对象。对其使用对象方法时就会报错。


优化后:

const _toString = Object.prototype.toString;
const isPlainObject = (obj) => {
return _toString.call(obj) === '[object Object]';
}
const App = (props) => {
const { data } = props || {};
const nameList = [];
if (isPlainObject(data)) {
nameList = Object.keys(data);
}
}

六、async/await 错误捕获


优化前:

import React, { useState } from 'react';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const res = await queryData();
setLoading(false);
}
}

如果 queryData() 执行报错,那是不是页面一直在转圈圈。


优化后:

import React, { useState } from 'react';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
try {
const res = await queryData();
setLoading(false);
} catch (error) {
setLoading(false);
}
}
}

如果使用 trycatch 来捕获 await 的错误感觉不太优雅,可以使用 await-to-js 来优雅地捕获。


二次优化后:

import React, { useState } from 'react';
import to from 'await-to-js';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const [err, res] = await to(queryData());
setLoading(false);
}
}

七、不是什么都能用来JSON.parse


优化前:

const App = (props) => {
const { data } = props || {};
const dataObj = JSON.parse(data);
}

JSON.parse() 方法将一个有效的 JSON 字符串转换为 JavaScript 对象。这里没必要去判断一个字符串是否为有效的 JSON 字符串。只要利用 trycatch 来捕获错误即可。


优化后:

const App = (props) => {
const { data } = props || {};
let dataObj = {};
try {
dataObj = JSON.parse(data);
} catch (error) {
console.error('data不是一个有效的JSON字符串')
}
}

八、被修改的引用类型数据


优化前:

const App = (props) => {
const { data } = props || {};
if (Array.isArray(data)) {
data.forEach(item => {
if (item) item.age = 12;
})
}
}

如果谁用 App 这个函数后,他会搞不懂为啥 dataage 的值为啥一直为 12,在他的代码中找不到任何修改 dataage 值的地方。只因为 data 是引用类型数据。在公共函数中为了防止处理引用类型数据时不小心修改了数据,建议先使用 lodash.clonedeep 克隆一下。


优化后:

import cloneDeep from 'lodash.clonedeep';

const App = (props) => {
const { data } = props || {};
const dataCopy = cloneDeep(data);
if (Array.isArray(dataCopy)) {
dataCopy.forEach(item => {
if (item) item.age = 12;
})
}
}

九、并发异步执行赋值操作


优化前:

const App = (props) => {
const { data } = props || {};
let urlList = [];
if (Array.isArray(data)) {
data.forEach(item => {
const { id = '' } = item || {};
getUrl(id).then(res => {
if (res) urlList.push(res);
});
});
console.log(urlList);
}
}

上述代码中 console.log(urlList) 是无法打印出 urlList 的最终结果。因为 getUrl 是异步函数,执行完才给 urlList 添加一个值,而 data.forEach 循环是同步执行的,当 data.forEach 执行完成后,getUrl 可能还没执行完成,从而会导致 console.log(urlList) 打印出来的 urlList 不是最终结果。


所以我们要使用队列形式让异步函数并发执行,再用 Promise.all 监听所有异步函数执行完毕后,再打印 urlList 的值。


优化后:

const App = async (props) => {
const { data } = props || {};
let urlList = [];
if (Array.isArray(data)) {
const jobs = data.map(async item => {
const { id = '' } = item || {};
const res = await getUrl(id);
if (res) urlList.push(res);
return res;
});
await Promise.all(jobs);
console.log(urlList);
}
}

十、过度防御


优化前:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
const info = infoList?.join(',');
}

infoList 后面为什么要跟 ?,数组的 map 方法返回的一定是个数组。


优化后:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
const info = infoList.join(',');
}

后续


以上对小王代码的吐槽,最后我只想说一下,以上的错误都是一些 JS 基础知识,跟任何框架没有任何关系。如果你工作了几年还是犯这些错误,真的可以考虑转行。


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

🤭新的广告方式,很新很新!

哈哈 会爬树的金鱼,树上的金鱼呦😀 前言 老板:针对上半年业绩发表一下大家的看法,大家自由发言,已经定好晚餐和夜宵了有需要的自取୧(๑•̀⌄•́๑)૭ 产品A:根据大数据分析公司产品的广告收入较低拖后腿,建议扩宽曝光渠道! 产品B:对! 大数据分析公司产品的广...
继续阅读 »

哈哈 会爬树的金鱼,树上的金鱼呦😀


前言


老板:针对上半年业绩发表一下大家的看法,大家自由发言,已经定好晚餐和夜宵了有需要的自取୧(๑•̀⌄•́๑)૭


产品A:根据大数据分析公司产品的广告收入较低拖后腿,建议扩宽曝光渠道!


产品B:对! 大数据分析公司产品的广告大部分来自移动端,pc端曝光率较少,可以增加一下曝光率,据统计移动端大部分广告收入来自首屏广告,我觉得pc也可以加上


程序员: PC哪有首屏广告啊,行业都没有先例


老板:这个好,没有先例! 我们又可以申请专利了Ψ( ̄∀ ̄)Ψ 搞起!!今年公司专利指标有着落了


程序员: 这也太影响体验了


产品A:就说能不能做吧, 今天面试的那个应届生不错能倒背chromium源码,还能手写react源码并且指出优化方案


程序员: 我做!!! ╭( ̄m ̄*)╮


先来个全屏遮罩🤔


这还不简单,直接一个定位搞定

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>XX百货</title>
<style>
#ADBox {
background: #fff;
position: fixed;
width: 100%;
height: 100%;
display: none;
}
</style>
</head>
<body>
<div id="ADBox">广告</div>
</body>
</html>


搞定提测!


重来没接过这么简单的需求,送业绩这是╮(╯﹏╰)╭


第一次提测🤐


测试A: 送业绩这是? 产品说要和移动端一模一样,你这哪一样了?? 直系看需求文档!!


程序员: 需求文档就一句话, 和移动端一样的开屏广告, 这那不一样了?


测试A: 这哪一样了?? 你家移动端广告露个顶部出来?看看哪个app广告不是全屏的???


程序员: 啥? 还要全屏?? 行...


// 必须用点击事件触发才能全屏
document.addEventListener("click", async (elem) => {
const box = document.getElementById("ADBox");
if (box.requestFullscreen) {
box.requestFullscreen();
}
setTimeout(() => {
const state = !!document.fullscreenElement;
// 是否全屏状态
if (state) {
// 取消全屏
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
}, 5 * 1000);
});

搞定提测


第二次提测🙄


产品A: 嗯...有点感觉了,这鼠标去掉都遮住广告了,万一广告商不满意投诉怎么办?


程序员: 鼠标这么小这么能遮住广告??


产品B: 看我鼠标? (大米老鼠标PC主题)


程序员: ...

<style>
#ADBox {
background: #fff;
position: fixed;
width: 100%;
height: 100%;
// 隐藏广告Box让用户点任意地方激活
opacity: 0;

}
</style>

提测...


第三次提测🤕


测试A: 为啥还有鼠标???


程序员: 怎么那可能还有?


测试A: 过来看,鼠标不动的话还是会显示鼠标哦,动一下才消失


程序员: ##..行, 那我直接锁指针

    <script>
let pointerLockElement = null;
// 指针锁定或解锁
document.addEventListener(
"pointerlockchange",
() => {
// 锁定的元素是否为当前的元素 -- 没啥也意义可以去掉
if (document.pointerLockElement === pointerLockElement) {
console.log("指针锁定成功了。");
} else {
pointerLockElement = null;
console.log("已经退出锁定。");
}
},
false
);
// 锁定失败事件
document.addEventListener(
"pointerlockerror",
() => {
console.log("锁定指针时出错。");
},
false
);

// 锁定指针,锁定指针的元素必须让用户点一下才能锁定
function lockPointer(elem) {
// 如果已经存锁定的元素则不操作
if (document.pointerLockElement) {
return;
}
if (elem) {
pointerLockElement = elem;
elem.requestPointerLock();
}
}

// 解除锁定
function unlockPointer() {
document.exitPointerLock();
}

// 必须用点击事件触发才能全屏
document.addEventListener("click", async () => {
const box = document.getElementById("ADBox");
if (box.requestFullscreen) {
box.requestFullscreen();
box.style.opacity = 1;
box.style.display = "block";
lockPointer(box);
}
// 5秒后解除锁定
setTimeout(() => {
const state = !!document.fullscreenElement;
// 是否全屏状态
if (state) {
// 取消全屏
if (document.exitFullscreen) {
document.exitFullscreen();
unlockPointer();
box.style.display = "none";
}
}
}, 5 * 1000);
});
</script>

提测...


第四次提测😤


测试A: Safari上失效哦


程序员: 额....

<script>

// requestFullscreen 方法兼容处理
function useRequestFullscreen(elem) {
const key = ['requestFullscreen', 'mozRequestFullScreen', 'webkitRequestFullscreen', 'msRequestFullscreen']
for (const value of key) {
if (elem[value]) {
elem[value]()
return true
}
}
return false
}

// document.exitFullscreen 方法兼容处理
document.exitFullscreenUniversal = document.exitFullscreen || document.webkitExitFullscreen || document.mozCancelFullScreen

// fullscreenElement 对象兼容处理
function getFullscreenElement() {
const key = ['fullscreenElement', 'webkitFullscreenElement']
for (const value of key) {
if (document[value]) {
return document[value]
}
}
return null
}

// fullscreenchange 事件兼容处理
addEventListener("fullscreenchange", endCallback);
addEventListener("webkitfullscreenchange", endCallback);

// requestPointerLock 方法在Safari下不可与 requestFullscreen 方法共用一个事件周期 暂无解决方法,必须让用户点两次鼠标,第一次全屏,第二次锁鼠标
// 同一事件周期内会出现的问题: 1.有小机率会正常执行, 2.顶部出现白条(实际上是个浏览器锁鼠标的提示语,但显示异常了) 3.锁定鼠标失败

</script>


结尾😩


产品A: 效果不错,但还有点小小的瑕疵,为啥要鼠标点一下才能弹广告,改成进入就弹窗吧


程序员: 要不还是找上次那个应届生来吧,改chromium源码应该能实现╭∩╮(︶︿︶)╭∩╮


效果预览: http://www.npmstart.top/BSOD.html


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

项目部署之后页面没有刷新怎么办?

web
最近项目部署成功之后,突然产品找我,上线之后,页面没有生效,这是怎么回事?我这是第一次部署这个项目,也不太清楚历史问题,接下来就慢慢寻找答案吧, 如果心急的可以直接看后面的总结,下面我们好好聊聊缓存的问题。 浏览器输入url之后,就会进行下面一系列判断,来实现...
继续阅读 »

最近项目部署成功之后,突然产品找我,上线之后,页面没有生效,这是怎么回事?我这是第一次部署这个项目,也不太清楚历史问题,接下来就慢慢寻找答案吧, 如果心急的可以直接看后面的总结,下面我们好好聊聊缓存的问题。


浏览器输入url之后,就会进行下面一系列判断,来实现页面渲染。



首先讲一下常见的http缓存~


HTTP缓存常见的有两类:



  • 强缓存:可以由这两个字段其中一个决定





    • expires

    • cache-control(优先级更高)





  • 协商缓存:可以由这两对字段中的一对决定





    • Last-Modified,If-Modified-Since

    • Etag,If--Match(优先级更高)




强缓存


使用的是express框架


expires


app.get('/login', function(req, res){
// 设置 Expires 响应头
const time = new Date(Date.now() + 300000).toUTCString()
res.header('Expires', time)
res.render('login');
});

然后我们在前端页面刷新,我们可以看到请求的资源的响应头里多了一个expires的字段, 取消Disable cache



刷新



勾选Disable cache



但是,Expires已经被废弃了。对于强缓存来说,Expires已经不是实现强缓存的首选。


因为Expires判断强缓存是否过期的机制是:获取本地时间戳,并对先前拿到的资源文件中的Expires字段的时间做比较。来判断是否需要对服务器发起请求。这里有一个巨大的漏洞:“如果我本地时间不准咋办?”


是的,Expires过度依赖本地时间,如果本地与服务器时间不同步,就会出现资源无法被缓存或者资源永远被缓存的情况。所以,Expires字段几乎不被使用了。现在的项目中,我们并不推荐使用Expires,强缓存功能通常使用cache-control字段来代替Expires字段。


cache-control


其实cache-control跟expires效果差不多,只不过这两个字段设置的值不一样而已,前者设置的是秒数,后者设置的是毫秒数


app.get('/login', function(req, res){
// 设置 Expires 响应头
// const time = new Date(Date.now() + 300000).toUTCString()
// res.header('Expires', time)
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'max-age=300')
res.render('login');
});

前端页面响应头多了cache-control这个字段,且300s内都走本地缓存,不会去请求服务端



Cache-Control:max-age=N,N就是需要缓存的秒数。从第一次请求资源的时候开始,往后N秒内,资源若再次请求,则直接从磁盘(或内存中读取),不与服务器做任何交互。


Cache-control中因为max-age后面的值是一个滑动时间,从服务器第一次返回该资源时开始倒计时。所以也就不需要比对客户端和服务端的时间,解决了Expires所存在的巨大漏洞。


Cache-control的多种属性:developer.mozilla.org/zh-CN/docs/…


但是使用最多的就是no-cache和no-store,接下来就重点学习这两种


no-cache和no-store


no_cache是Cache-control的一个属性。它并不像字面意思一样禁止缓存,实际上,no-cache的意思是强制进行协商缓存。如果某一资源的Cache-control中设置了no-cache,那么该资源会直接跳过强缓存的校验,直接去服务器进行协商缓存。而no-store就是禁止所有的缓存策略了。


app.get('/login', function(req, res){
// 设置 Expires 响应头
// const time = new Date(Date.now() + 300000).toUTCString()
// res.header('Expires', time)
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'no-cache')
res.render('login');
});

no-cache(进行协商缓存,下次再次请求,没有勾选控制台Disable cache,状态码是304)



app.get('/login', function(req, res){
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'no-store')
res.render('login');
});

no-store(每次都请求服务器的最新资源,没有缓存策略)



强制缓存就是以上这两种方法了。现在我们回过头来聊聊,Expires难道就一点用都没有了吗?也不是,虽然Cache-control是Expires的完全替代品,但是如果要考虑向下兼容的话,在Cache-control不支持的时候,还是要使用Expires,这也是我们当前使用的这个属性的唯一理由。


协商缓存


与强缓存不同的是,强缓存是在时效时间内,不走服务端,只走本地缓存;而协商缓存是要走服务端的,如果请求某个资源,去请求服务端时,发现命中缓存则返回304,否则则返回所请求的资源,那怎么才算命中缓存呢?接下来讲讲


Last-Modified,If-Modified-Since


简单来说就是:



  • 第一次请求资源时,服务端会把所请求的资源的最后一次修改时间当成响应头中Last-Modified的值发到浏览器并在浏览器存起来

  • 第二次请求资源时,浏览器会把刚刚存储的时间当成请求头中If-Modified-Since的值,传到服务端,服务端拿到这个时间跟所请求的资源的最后修改时间进行比对

  • 比对结果如果两个时间相同,则说明此资源没修改过,那就是命中缓存,那就返回304,如果不相同,则说明此资源修改过了,则没命中缓存,则返回修改过后的新资源


基于last-modified的协商缓存实现方式是:



  1. 首先需要在服务器端读出文件修改时间,

  2. 将读出来的修改时间赋给响应头的last-modified字段。

  3. 最后设置Cache-control:no-cache


三步缺一不可。


app.get('/login', function(req, res){
// 设置 Expires 响应头
// const time = new Date(Date.now() + 300000).toUTCString()
// res.header('Expires', time)

const { mtime } = fs.statSync(path.join(__dirname, 'public/index.css')) // 读取最后修改时间
console.log(mtime.toUTCString(), '--------')
// 响应头的last-modified字段
res.header('last-modified', mtime.toUTCString())
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'no-cache')
res.render('login');
});


当index.css发生改变再次请求时



终端输出的时间变化



服务端的时间跟last-modified的值是一致的



使用以上方式的协商缓存已经存在两个非常明显的漏洞。这两个漏洞都是基于文件是通过比较修改时间来判断是否更改而产生的。


1.因为是更具文件修改时间来判断的,所以,在文件内容本身不修改的情况下,依然有可能更新文件修改时间(比如修改文件名再改回来),这样,就有可能文件内容明明没有修改,但是缓存依然失效了。


2.当文件在极短时间内完成修改的时候(比如几百毫秒)。因为文件修改时间记录的最小单位是秒,所以,如果文件在几百毫秒内完成修改的话,文件修改时间不会改变,这样,即使文件内容修改了,依然不会 返回新的文件。


为了解决上述的这两个问题。从http1.1开始新增了一个头信息,ETag(Entity 实体标签)


Etag,If--Match


ETag就是将原先协商缓存的比较时间戳的形式修改成了比较文件指纹。


其实Etag,If--Match跟Last-Modified,If-Modified-Since大体一样,区别在于:



  • 后者是对比资源最后一次修改时间,来确定资源是否修改了

  • 前者是对比资源内容,来确定资源是否修改


那我们要怎么比对资源内容呢?我们只需要读取资源内容,转成hash值,前后进行比对就行了!


app.get('/login', function(req, res){
// 设置 Expires 响应头
// const time = new Date(Date.now() + 300000).toUTCString()
// res.header('Expires', time)

// const { mtime } = fs.statSync(path.join(__dirname, 'public/index.css')) // 读取最后修改时间
// console.log(mtime.toUTCString(), '--------')
// 响应头的last-modified字段
// res.header('last-modified', mtime.toUTCString())


// 设置ETag
const ifMatch = req.header['if-none-match']
const hash = crypto.createHash('md5')
const fileBuf = fs.readFileSync(path.join(__dirname, 'public/index.css'))
hash.update(fileBuf, 'utf8')
const etag = `"${hash.digest('hex')}"`
console.log(etag, '---etag----')
// 对比hash值
if (ifMatch === etag) {
res.status = 304
} else {
res.header('etag', etag)
// ctx.body = fileBuffer
}
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'no-cache')
res.render('login');
});


当资源发生改变时,状态码变成200,更新缓存


比如更改css样式



ETag也有缺点



  • ETag需要计算文件指纹这样意味着,服务端需要更多的计算开销。如果文件尺寸大,数量多,并且计算频繁,那么ETag的计算就会影响服务器的性能。显然,ETag在这样的场景下就不是很适合。

  • ETag有强验证和弱验证,所谓将强验证,ETag生成的哈希码深入到每个字节。哪怕文件中只有一个字节改变了,也会生成不同的哈希值,它可以保证文件内容绝对的不变。但是,强验证非常消耗计算量。ETag还有一个弱验证,弱验证是提取文件的部分属性来生成哈希值。因为不必精确到每个字节,所以他的整体速度会比强验证快,但是准确率不高。会降低协商缓存的有效性。


值得注意的一点是,不同于cache-control是expires的完全替代方案(说人话:能用cache-control就不要用expiress)。ETag并不是last-modified的完全替代方案。而是last-modified的补充方案(说人话:项目中到底是用ETag还是last-modified完全取决于业务场景,这两个没有谁更好谁更坏)。


disk cache & memory cache


磁盘缓存+内存缓存,这两种缓存不属于http缓存,而是本地缓存了~


我们直接打开掘金官网,点击network,类型选择all



可以看的很多请求,这里请求包括了静态资源+接口请求


这里我们能够看的很多请求的size中有很多是disk cache(磁盘缓存)


也有一些图片是memory cache(内存缓存)



这两者有什么区别呢?


disk cache: 磁盘缓存,很明显将内容存储在计算机硬盘中,很明显,这种缓存可以占用比较大的空间,但是由于是读取硬盘,所以速度低于内存


memory cache: 内存缓存,速度快,优先级高,但是大小受限于计算机的内存大小,很大的资源还是缓存到硬盘中


上面的浏览器缓存已经有三个大点了,那它们的优先级是什么样的呢?


缓存的获取顺序如下:


1.内存缓存


2.磁盘缓存


3.强缓存


4.协商缓存


如果勾选了Disable cache,那磁盘缓存都不存在了,之还有内存缓存



我还发现,勾选了Disable cache,就base64图片一定会在内存缓存中,其他图片则会发起请求;而不勾选了Disable cache,则大多数图片都在内存缓存中




CDN缓存


CDN缓存是一种服务端缓存,CDN服务商可以将源站上的资源缓到其各地的边缘服务器节点上。当用户访问该资源时,CDN再通过负载均衡将用户的请求调度到最近的缓存节点上,有效减少了链路回源,提高了资源访问效率及可用性,降低带宽消耗。


如果客户端没有命中缓存,那接下来就要发起一次网络请求,根据网络环境,一般大型站点都会配置CDN,CDN会找一个最合适的服务节点接管网络请求。CDN节点都会在本地缓存静态文件数据,一旦命中直接返回,不会穿透去请求应用服务器。并且CDN会通过在不同的网络,策略性地通过部署边缘服务器和应用大量的技术和算法,把用户的请求指定到最佳响应节点上。所以会减少非常多的网络开销和响应延迟。


如果没有部署CDN或者CDN没有命中,请求最终才会落入应用服务器,现在的http服务器都会添加一层反向代理,例如nginx,在这一层同样会添加缓存层,代表技术是squid,varnish,当然nginx作为http服务器而言也支持静态文件访问和本地缓存技术,当然也可以使用远程缓存,如redis,memcache,这里缓存的内容一般为静态文件或者由服务器已经生成好的动态页面,在返回用户之前缓存。


如果前面的缓存机制全部失效,请求才会落入真正的服务器节点。


总结


1.如果页面是协商缓存,如何获取页面最新内容?


协商缓存比较好办,那就刷新页面,不过需要勾选Disable cache,但是用户不知道打开控制台怎么办?


那就右击页面的刷新按钮,然后选择硬性重新加载,或者清空缓存并硬性重新加载,页面就获取到最新资源了



2.如果页面没有设置cache-control,那默认的缓存机制是什么样的?



默认是协商缓存,这也符合浏览器设计,缓存可以减少宽度流量,加快响应速度


3.如果项目重新部署还是没有更新,怎么办?


在确定项目已经部署成功


这样子,可以去问一下公司的运维同事,这个项目是否有CDN缓存


如果项目的域名做了CDN缓存,就需要刷新CDN目录,来解决缓存问题了,不然就只能等,等CDN策略失效,来请求最新的内容


向如下配置的缓存策略,只有过30天才会去真正服务器去请求最新内容



当然你可以测试一下是否为CDN缓存,在url后面拼接一个参数,就能够获取到最新资源了,比如有缓存的链接是baidu.com/abc


你可以在浏览器中输入baidu.com/abc&t=1234来…


当然特定场景,我们不能随意给链接后面添加参数,所以这也只适用于测试一下是否有CDN缓存


所以最好的解决办法还是需要让运维同事去刷新目录,这样就能快速解决CDN缓存问题。


参考链接


juejin.cn/post/712719…


juejin.cn/post/717756…


xiaolincoding.com/

network/2_h…

收起阅读 »

Android 时钟翻页效果

web
背景 今天逛掘金,发现了一个有意思的web view效果,想着Android能不能实现一下捏。 原文链接:juejin.cn/post/724435… 具体实现分析请看上文原文链接,那我们开始吧! 容器 val space = 10f //上下半间隔 val...
继续阅读 »

背景


今天逛掘金,发现了一个有意思的web view效果,想着Android能不能实现一下捏。


image.png
原文链接:juejin.cn/post/724435…


具体实现分析请看上文原文链接,那我们开始吧!


容器


val space = 10f //上下半间隔
val bgBorderR = 10f //背景圆角
//上半部分
val upperHalfBottom = height.toFloat() / 2 - space / 2
canvas.drawRoundRect(
0f,
0f,
width.toFloat(),
upperHalfBottom,
bgBorderR,
bgBorderR,
bgPaint
)
//下半部分
val lowerHalfTop = height.toFloat() / 2 + space / 2
canvas.drawRoundRect(
0f,
lowerHalfTop,
width.toFloat(),
height.toFloat(),
bgBorderR,
bgBorderR,
bgPaint
)

image.png


绘制数字


我们首先居中绘制数字4


val number4 = "4"
textPaint.getTextBounds(number4, 0, number4.length, textBounds)
//居中显示
val x = (width - textBounds.width()) / 2f - textBounds.left
val y = (height + textBounds.height()) / 2f - textBounds.bottom
canvas.drawText(number4, x, y, textPaint)

image.png


接下来我们将数字切分为上下两部分,分别绘制。


val number4 = "4"
textPaint.getTextBounds(number4, 0, number4.length, textBounds)
val x = (width - textBounds.width()) / 2f - textBounds.left
val y = (height + textBounds.height()) / 2f - textBounds.bottom
// 上半部分裁剪
canvas.save()
canvas.clipRect(
0f,
0f,
width.toFloat(),
upperHalfBottom
)
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
// 下半部分裁剪
canvas.save()
canvas.clipRect(
0f,
lowerHalfTop,
width.toFloat(),
height.toFloat()
)
canvas.drawText(number4, x, y, textPaint)

image.png


翻转卡片


如何实现让其旋转呢?
而且还得是3d的效果了。我们选择Camera来实现。
我们先让数字'4'旋转起来。


准备工作,通过属性动画来改变旋转的角度。


private var degree = 0f //翻转角度
private val camera = Camera()
private var flipping = false //是否处于翻转状态
...
//动画
val animator = ValueAnimator.ofFloat(0f, 360f)
animator.addUpdateListener { animation ->
val animatedValue = animation.animatedValue as Float
setDegree(animatedValue)
}
animator.doOnStart {
flipping = true
}
animator.doOnEnd {
flipping = false
}
animator.duration = 1000
animator.interpolator = LinearInterpolator()
animator.start()
...

private fun setDegree(degree: Float) {
this.degree = degree
invalidate()
}

让数字'4'旋转起来:


  override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 居中绘制数字4
val number4 = "4"
textPaint.getTextBounds(number4, 0, number4.length, textBounds)
val x = (width - textBounds.width()) / 2f - textBounds.left
val y = (height + textBounds.height()) / 2f - textBounds.bottom

if (!flipping) {
canvas.drawText(number4, x, y, textPaint)
} else {
camera.save()
canvas.translate(width / 2f, height / 2f)
camera.rotateX(-degree)
camera.applyToCanvas(canvas)
canvas.translate(-width / 2f, -height / 2f)
camera.restore()
canvas.drawText(number4, x, y, textPaint)
}
}

file.gif

我们再来看一边效果图:
我们希望将卡片旋转180度,并且0度-90度由上半部分完成,90度-180度由下半部分完成。


我们调整一下代码,先处理一下上半部分:


...
val animator = ValueAnimator.ofFloat(0f, 180f)
...
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val space = 10f //上下半间隔
//上半部分
val upperHalfBottom = height.toFloat() / 2 - space / 2
...
// 居中绘制数字4
val number4 = "4"
textPaint.getTextBounds(number4, 0, number4.length, textBounds)
val x = (width - textBounds.width()) / 2f - textBounds.left
val y = (height + textBounds.height()) / 2f - textBounds.bottom

if (!flipping) {
//上半部分裁剪
canvas.save()
canvas.clipRect(
0f,
0f,
width.toFloat(),
upperHalfBottom
)
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
} else {
if (degree < 90) {
//上半部分裁剪
canvas.save()
canvas.clipRect(
0f,
0f,
width.toFloat(),
upperHalfBottom
)
camera.save()
canvas.translate(width / 2f, height / 2f)
camera.rotateX(-degree)
camera.applyToCanvas(canvas)
canvas.translate(-width / 2f, -height / 2f)
camera.restore()
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
}
}
}

效果如下:


upper.gif

接下来我们再来看一下下半部分:


override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val space = 10f //上下半间隔
//下半部分
val lowerHalfTop = height.toFloat() / 2 + space / 2

// 居中绘制数字4
val number4 = "4"
textPaint.getTextBounds(number4, 0, number4.length, textBounds)
val x = (width - textBounds.width()) / 2f - textBounds.left
val y = (height + textBounds.height()) / 2f - textBounds.bottom

if (!flipping) {
// 下半部分裁剪
canvas.save()
canvas.clipRect(
0f,
lowerHalfTop,
width.toFloat(),
height.toFloat()
)
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
} else {
if (degree > 90) {
canvas.save()
canvas.clipRect(
0f,
lowerHalfTop,
width.toFloat(),
height.toFloat()
)
camera.save()
canvas.translate(width / 2f, height / 2f)
val bottomDegree = 180 - degree
camera.rotateX(bottomDegree)
camera.applyToCanvas(canvas)
canvas.translate(-width / 2f, -height / 2f)
camera.restore()
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
}
}
}

lower.gif

那我们将上下部分结合起来,效果如下:


all.gif

数字变化


好!我们完成了翻转部分,现在需要在翻转的过程中将数字改变:

我们还是举例说明:数字由'4'变为'5'的情况。我们思考个问题,什么时候需要改变数字?

上半部分在翻转开始的时候,上半部分底部显示的数字就应该由'4'变为'5',但是旋转的部分还是应该为'4',
下半部分开始旋转的时候底部显示的数字还是应该为'4',而旋转的部分该为'5'。


canvas.save()
canvas.clipRect(
0f,
0f,
width.toFloat(),
upperHalfBottom
)
canvas.drawText(number5, x, y, textPaint)
canvas.restore()
// 下半部分裁剪
canvas.save()
canvas.clipRect(
0f,
lowerHalfTop,
width.toFloat(),
height.toFloat()
)
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
//=====⬆️=====上述的代码显示的上下底部显示的内容,即上半部分地步显示5,下半部分显示4
if (degree < 90) {
//上半部分裁剪
canvas.save()
canvas.clipRect(
0f,
0f,
width.toFloat(),
upperHalfBottom
)
camera.save()
canvas.translate(width / 2f, height / 2f)
camera.rotateX(-degree)
camera.applyToCanvas(canvas)
canvas.translate(-width / 2f, -height / 2f)
camera.restore()
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
//=====⬆️=====上述的代码表示上半部分旋转显示的内容,即数字4
} else {
canvas.save()
canvas.clipRect(
0f,
lowerHalfTop,
width.toFloat(),
height.toFloat()
)
camera.save()
canvas.translate(width / 2f, height / 2f)
val bottomDegree = 180 - degree
camera.rotateX(bottomDegree)
camera.applyToCanvas(canvas)
canvas.translate(-width / 2f, -height / 2f)
camera.restore()
canvas.drawText(number5, x, y, textPaint)
canvas.restore()
//=====⬆️=====上述的代码表示下半部分旋转显示的内容,即数字5
}

效果图如下:大伙可以在去理一下上面数字的变化的逻辑。


a.gif

最后我们加上背景再看一下效果:


a.gif

小结


上述代码仅仅提供个思路,仅为测试code,正式代码可不

作者:蹦蹦蹦
来源:juejin.cn/post/7271518821809438781
能这么写哦 >..<

收起阅读 »

虚拟列表 or 时间分片

前言 最近在做一个官网,原本接口做的都是分页的,但是客户提出不要分页,之前看过虚拟列表这个东西,所以进行一下了解。 为啥要用虚拟列表呢! 在日常工作中,所要渲染的也不单单只是一个li那么简单,会有很多嵌套在里面。但数据量过多,同时渲染式,会在 渲染样式 跟 布...
继续阅读 »

前言


最近在做一个官网,原本接口做的都是分页的,但是客户提出不要分页,之前看过虚拟列表这个东西,所以进行一下了解。


为啥要用虚拟列表呢!


在日常工作中,所要渲染的也不单单只是一个li那么简单,会有很多嵌套在里面。但数据量过多,同时渲染式,会在 渲染样式 跟 布局计算上花费太多时间,体验感不好,那你说要不要优化嘛,不是你被优化就是你优化它。


进入正题,啥是虚拟列表?


可以这么理解,根据你视图能显示多少就先渲染多少,对看不到的地方采取不渲染或者部分渲染。




这时候你完成首次加载,那么其他就是在你滑动时渲染,就可以通过计算,得知此时屏幕应该显示的列表项。


怎么弄?


备注:很多方案对于动态不固定高度、网络图片以及用户异常操作等形式处理的也并不好,了解下原理即可。


虚拟列表的实现,实际上就是在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。


1、计算当前可视区域起始数据索引(startIndex)

2、计算当前可视区域结束数据索引(endIndex)

3、计算当前可视区域的数据,并渲染到页面中

4、计算startIndex对应的数据在整个列表中的偏移位置startOffset并设置到列表上


由于只是对可视区域内的列表项进行渲染,所以为了保持列表容器的高度并可正常的触发滚动,将Html结构设计成如下结构:

<div class="infinite-list-container">
<div class="infinite-list-phantom"></div>
<div class="infinite-list">
<!-- item-1 -->
<!-- item-2 -->
<!-- ...... -->
<!-- item-n -->
</div>
</div>


  • infinite-list-container 为可视区域的容器

  • infinite-list-phantom 为容器内的占位,高度为总列表高度,用于形成滚动条

  • infinite-list 为列表项的渲染区域

    接着,监听infinite-list-containerscroll事件,获取滚动位置scrollTop

  • 假定可视区域高度固定,称之为screenHeight

  • 假定列表每项高度固定,称之为itemSize

  • 假定列表数据称之为listData

  • 假定当前滚动位置称之为scrollTop

  •   则可推算出:

    • 列表总高度listHeight = listData.length * itemSize
    • 可显示的列表项数visibleCount = Math.ceil(screenHeight / itemSize)
    • 数据的起始索引startIndex = Math.floor(scrollTop / itemSize)
    • 数据的结束索引endIndex = startIndex + visibleCount
    • 列表显示数据为visibleData = listData.slice(startIndex,endIndex)

      当滚动后,由于渲染区域相对于可视区域已经发生了偏移,此时我需要获取一个偏移量startOffset,通过样式控制将渲染区域偏移至可视区域中。

    • 偏移量startOffset = scrollTop - (scrollTop % itemSize);

时间分片


那么虚拟列表是一方面可以优化的方式,另一个就是时间分片。


先看看我们平时的情况


1.直接开整,直接渲染。




诶???我们可以发现,js运行时间为113ms,但最终 完成时间是 1070ms,一共是 js 运行时间加上渲染总时间。

PS:

  • 在 JS 的 EventLoop中,当JS引擎所管理的执行栈中的事件以及所有微任务事件全部执行完后,才会触发渲染线程对页面进行渲染
  • 第一个 console.log的触发时间是在页面进行渲染之前,此时得到的间隔时间为JS运行所需要的时间
  • 第二个 console.log是放到 setTimeout 中的,它的触发时间是在渲染完成,在下一次 EventLoop中执行的

那我们改用定时器


上面看是因为我们同时渲染,那我们可以分批看看。

let once = 30
let ul = document.getElementById('testTime')
function loopRender (curTotal, curIndex) {
if (curTotal <= 0) return
let pageCount = Math.min(curTotal, once) // 每页最多20条
setTimeout(_ => {
for (let i=0; i<pageCount;i++) {
let li = document.createElement('li')
li.innerHTML = curIndex + i
ul.appendChild(li)
}
loopRender(curTotal - pageCount, curIndex + pageCount)
}, 0)
}
loopRender(100000, 0)

这时候可以感觉出来渲染很快,但是如果渲染复杂点的dom会闪屏,为什么会闪屏这就需要清楚电脑刷新的概念了,这里就不详细写了,有兴趣的小朋友可以自己去了解一下。

可以改用 requestAnimationFrame 去分批渲染,因为这个关于电脑自身刷新效率的,不管你代码的事,可以解决丢帧问题。

let once = 30
let ul = document.getElementById('container')
// 循环加载渲染数据
function loopRender (curTotal, curIndex) {
if (curTotal <= 0) return
let pageCount = Math.min(curTotal, once) // 每页最多20条
window.requestAnimationFrame(_ => {
for (let i=0; i<pageCount;i++) {
let li = document.createElement('li')
li.innerHTML = curIndex + i
ul.appendChild(li)
}
loopRender(curTotal - pageCount, curIndex + pageCount)
})
}
loopRender(100000, 0)

还可以改用 DocumentFragment


什么是 DocumentFragment



DocumentFragment,文档片段接口,表示一个没有父级文件的最小文档对象。它被作为一个轻量版的 Document使用,用于存储已排好版的或尚未打理好格式的XML片段。最大的区别是因为 DocumentFragment不是真实DOM树的一部分,它的变化不会触发DOM树的(重新渲染) ,且不会导致性能等问题。

可以使用 document.createDocumentFragment方法或者构造函数来创建一个空的 DocumentFragment

ocumentFragments是DOM节点,但并不是DOM树的一部分,可以认为是存在内存中的,所以将子元素插入到文档片段时不会引起页面回流。



当 append元素到 document中时,被 append进去的元素的样式表的计算是同步发生的,此时调用 getComputedStyle 可以得到样式的计算值。而 append元素到 documentFragment 中时,是不会计算元素的样式表,所以 documentFragment 性能更优。当然现在浏览器的优化已经做的很好了, 当 append元素到 document中后,没有访问 getComputedStyle 之类的方法时,现代浏览器也可以把样式表的计算推迟到脚本执行之后。

let once = 30 
let ul = document.getElementById('container')
// 循环加载渲染数据
function loopRender (curTotal, curIndex) {
if (curTotal <= 0) return
let pageCount = Math.min(curTotal, once) // 每页最多20条
window.requestAnimationFrame(_ => {
let fragment = document.createDocumentFragment()
for (let i=0; i<pageCount;i++) {
let li = document.createElement('li')
li.innerHTML = curIndex + i
fragment.appendChild(li)
}
ul.appendChild(fragment)
loopRender(curTotal - pageCount, curIndex + pageCount)
})
}
loopRender(100000, 0)

其实同时渲染十万条数据这个情况还是比较少见的,就当做个了解吧。


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

CSS命名太头疼?这个Vite插件自动生成,让你解放双手!

web
CSS样式一直以来都是一个让前端开发者头疼的问题,随着前端工程化的发展,使用原子CSS进行样式开发正变得越来越流行。相比传统的CSS样式书写方式,原子CSS可以让我们以更模块化和可复用的方式进行样式的编码。但是手动编写大量原子类样式也比较烦琐。有没有办法自动生...
继续阅读 »

CSS样式一直以来都是一个让前端开发者头疼的问题,随着前端工程化的发展,使用原子CSS进行样式开发正变得越来越流行。相比传统的CSS样式书写方式,原子CSS可以让我们以更模块化和可复用的方式进行样式的编码。但是手动编写大量原子类样式也比较烦琐。有没有办法自动生成原子CSS类呢?今天我要介绍的Vite插件atom-css-generator就可完美实现这一功能。


原子CSS简介


原子CSS(Atomic CSS)将传统的CSS类拆分成一个个独立的、原子级的类,每个类仅包含一个CSS属性,例如:



.p-10 {
padding: 10px;
}

.bg-red {
background: red;
}

相比于传统的CSS类,原子类具有以下特点:



  • 原子:每个类只包含一个CSS属性,拆分到最小粒度

  • 独立:类名语义明确,可以任意组合使用而不会产生冲突

  • 可复用:一个原子类可以重复使用在不同的组件中


使用原子CSS的优势在于:



  • 更模块化:样式属性高内聚、解耦

  • 更可维护:不同类名称、不同文件,避免影响

  • 更灵活:组件样式由原子类组合,更容易扩展和维护


但是编写大量原子类也比较麻烦,多达几千个类定义都可能出现。有没有自动生成的方式呢?


atom-css-generator插件介绍


atom-css-generator是一个Vite插件,它可以通过解析Vue组件中的class,自动生成对应的原子CSS定义


安装和配置


使用npm或yarn安装:


Copy code

npm install atom-css-generator

在vite.config.js中引入插件:


js

Copy code

import atomCssGenerator from 'atom-css-generator';

export default {
plugins: [
atomCssGenerator({
outputPath: 'assets/styles'
})
]
}

主要的配置项有:



  • outputPath:指定生成的CSS文件输出目录,默认为public


使用方式



  1. 在Vue组件的template中,使用特定格式的class,例如:


html

Copy code

<template>
<div class="bg-red fc-white p-20">
<!-- ... -->
</div>
</template>


  1. 构建项目时,插件会自动生成对应的原子CSS类定义:


css

Copy code

.bg-red {
background-color: red;
}

.fc-white {
color: white;
}

.p-20 {
padding: 20px;
}


  1. style.css会被自动生成到指定的outputPath中,并注入到HTML文件头部。


支持的类名格式


插件支持多种格式的类名规则生成,包括:



  • 颜色类名:bg-red、fc-333

  • 间距类名:p-20、ml-10

  • 尺寸类名:w-100、h-200

  • Flexbox类名:jc-center、ai-stretch

  • 边框类名:bc-333、br-1-f00-solid

  • 布局类名:p-relative、p-fixed

  • 文字类名:fs-14、fw-bold


等等,非常全面。


而且也内置了一些预设的实用样式类,比如文字截断类te-ellipsis。


原理简析


插件主要通过以下处理流程实现自动生成原子CSS:



  1. 使用@vue/compiler-sfc解析Vue文件,获取模板内容

  2. 通过正则表达式提取模板中的class名称

  3. 根据特定类名规则,生成对应的CSS定义

  4. 将CSS写入style.css文件中,并注入到HTML中


同时,插件还会在热更新时自动检查新添加的类名,从而动态更新style.css。


总结


通过atom-css-generator这个插件,我们可以非常轻松地在Vue项目中使用原子CSS样式,而不需要手动编写。它省去了我们大量重复的工作,使得样式的维护和扩展更加简单。


如果你也想尝试在自己的项目中引入原子CSS,不妨试试这个插件。相信它能给你带来意想不到的便利!
GitHub地址

收起阅读 »

自建”IT兵器库”,你值得一看!

web
现在市面的组件库,我们用的越发熟练,越发爆嗨,只需cv就能完成需求,为何不爆嗨!!! 常用库有 element、antd、iView、antd pro,这些组件库都是在以往的需求开发当中进行总结提炼,一次次符合我们的需求,只要有文档,只要有示例,页面开发都是小...
继续阅读 »

现在市面的组件库,我们用的越发熟练,越发爆嗨,只需cv就能完成需求,为何不爆嗨!!!


常用库有 element、antd、iView、antd pro,这些组件库都是在以往的需求开发当中进行总结提炼,一次次符合我们的需求,只要有文档,只要有示例,页面开发都是小问题,对吧,各位优秀开发前端工程师。


接下来,根据开发需求,一步步完成一个组件的开发,当然可能自己的思路,并不是最好的,欢迎大家留言讨论,一起进步


需求:


动态列表格,就是一个表格,存在默认列,但是支持我们操控,实现动态效果


实现效果


默认表格配置


image.png


默认列配置


image.png


动态列组件支持查询


image.png


动态列组件支持勾选


image.png


动态列组件支持清空


image.png


动态列组件支持一键全选


image.png


动态列组件支持一键清空


image.png


功能点划分



  • 表格默认列和动态列组件默认选中项 实现双向绑定

  • 动态列组件 增删改与表格 实现双向绑定

  • 动态列组件 实现搜索

  • 动态列组件 实现单点控制 添加与删除

  • 动态列组件 实现一键控制功能 全选与清空

  • 动态列组件 实现恢复初始态


使用到组件(Antd 组件库哈)



  • Table

  • Pagination

  • Modal

  • Input

  • Button

  • Checkbox


动态列组件区域划分



  • 头部标题

  • 头部提示语

  • 核心内容区



    • 核心区域头部功能按钮





    • 搜索区域





    • 左边所有内容项





    • 待选内容项




动态列组件最终可支持配置项


  open?: boolean // Modal状态
setOpen?: React.Dispatch> // 控制Modal状态
modalTitle?: string | React.ReactNode
modalWidth?: number
modalHeadContent?: React.ReactNode
leftHeadContent?: React.ReactNode | string
rightHeadContent?: React.ReactNode | string
modalBodyStyle?: any
searchPlaceholder?: string
modalOk?: (val, isUseDefaultData?: boolean) => void // 第二个参数 内部数据处理支持
enableSelectAll?: boolean // 是否开启全选功能
selectData: SelectItem[] // 下拉框数据
isOutEmitData?: boolean
defaultSelectKeyList?: string[] // 默认选中的key(当前表格列)自定义数据(外部做逻辑处理)
initSelectKey?: string[] // 初始表格选中的key 自定义数据(外部做逻辑处理)
curColumns?: any[] // 当前表格列 内部做逻辑处理
originColumns?: any[] // 原始表格列 内部做逻辑处理
isDelHeadCol?: boolean // 删除头部columnKey 例如序号 只有内部处理数据时生效
isDelTailCol?: boolean // 删除尾部columnKey 例如操作 只有内部处理数据时生效
isDelHeadAndTail?: boolean // 删除头尾部columnKey 只有内部处理数据时生效

动态列组件布局



    

// 头部内容区
{modalHeadContent}

// 以下维核心区


// 核心区-左边

// 核心区-功能按钮 - 一键全选
{enableSelectAll && (

onCheckBoxChange([], e)} checked={checkAll} indeterminate={indeterminate}>
全选


)}
{leftHeadContent || ''}



// 核心区-左搜索
{childSearchRender({ curData: leftSelectData })}

// 核心区-左列表区域
{selectItemRender(leftSelectData)}



// 核心区-右边


{rightHeadContent || ''}

// 核心区-功能按钮 - 一键清空
handleRightClearSelectData()}>
清空



// 核心区-右搜索
{childSearchRender({ curData: rightSelectData }, true)}

// 核心区-右列表区域
{selectItemRender(rightSelectData, true)}






动态列组件-列表渲染


const selectItemRender = (listArr = [], isRight = false) => {
return (


// 数据遍历形式
{listArr?.map(({ label, value, disabled = false }) => (

{!isRight && (

{label}

)}
// 判断是否是 右边列表区域 添加删除按钮
{isRight && {label}}
{isRight && (



)}

))}


)
}

动态列组件-搜索渲染


const childSearchRender = (childSearchProps: any, isRight = false) => {
// eslint-disable-next-line react/prop-types
const { curData } = childSearchProps
return (
{
onSearch(e, curData, isRight)
}}
allowClear
/>
)
}

动态列组件样式


.content-box {
width: 100%;
height: 550px;
border: 1px solid #d9d9d9;
}
.content-left-box {
border-right: 1px solid #d9d9d9;
}
.content-left-head {
padding: 16px 20px;
background: #f5f8fb;
height: 48px;
box-sizing: border-box;
}
.content-right-head {
padding: 16px 20px;
background: #f5f8fb;
height: 48px;
box-sizing: border-box;

&-clear {
color: #f38d29;
cursor: pointer;
}
}
.content-right-box {
}
.content-left-main {
padding: 10px 20px 0 20px;
height: calc(100% - 50px);
box-sizing: border-box;
}
.content-right-main {
padding: 10px 20px 0 20px;
height: calc(100% - 50px);
box-sizing: border-box;
}
.right-head-content {
font-weight: 700;
color: #151e29;
font-size: 14px;
}
.modal-head-box {
color: #151e29;
font-size: 14px;
height: 30px;
}
.icon-box {
color: #f4513b;
}
.ant-checkbox-group {
flex-wrap: nowrap;
}
.left-select-box {
height: 440px;
padding-bottom: 10px;
}
.right-select-box {
height: 440px;
padding-bottom: 10px;
}
.ant-checkbox-wrapper {
align-items: center;
}
.display-box {
height: 22px;
}

功能点逐一拆解实现


点1:表格默认列和动态列组件默认选中项 实现双向绑定



  • 首先,先写一个表格啦,确定列,这个就不用代码展示了吧,CV大法

  • 其次,把表格原始列注入动态列组件当中,再者注入当前表格列当前能选择的所有项

  • 当前能选择所有项内容参数示例


[
{ label: '项目编码', value: 'projectCode' },
{ label: '项目名称', value: 'projectName' },
{ label: '项目公司', value: 'company' },
{ label: '标段', value: 'lot' },
]




  • 动态组件内部默认选中当前表格列

  • 这里需要把表格列数据 进行过滤 映射成 string[]


   内部是通过checkbox.Grop 实现选中 我们只需要 通过一个状态去控制即可 `selectKey`
<Checkbox.Group onChange={onCheckChange} className="flex-col h-full flex" value={selectKey}>
...
Checkbox.Group>

动态列组件 单点控制 增删改



  • 增,删,改就是实现 左右边列表的双向绑定

  • 监听 左边勾选事件 + 右边删除事件 + 一键清空事件

  • 通过左右两边的状态 控制数据即可

  • 状态


  const [originSelectData, setOriginSelectData] = useState([]) // 下拉原始数据 搜索需要
const [originRightSelectData, setOriginRightSelectData] = useState([])
const [rightSelectData, setRightSelectData] = useState([])
const [selectKey, setSelectKey] = useState([])
const [transferObj, setTransferObj] = useState({})
const [indeterminate, setIndeterminate] = useState(false)
const [checkAll, setCheckAll] = useState(false)
const [leftSelectData, setLeftSelectData] = useState([])
const [defaultSelectKey, setDefaultSelectKey] = useState([])
const [originSelectKey, setOriginSelectKey] = useState([])

const onCheckChange = checkedValues => {
// 往右边追加数据
const selectResArr = checkedValues?.map(val => transferObj[val])
setSelectKey(checkedValues) // 我们选中的key (选项)
setRightSelectData(selectResArr) // 右边列表数据
setOriginRightSelectData(selectResArr) // 右边原数据(搜索时候需要)
}

const deleteRightData = key => {
const preRightData = rightSelectData
const filterResKeyArr = preRightData?.filter(it => it.value !== key).map(it => it.value) // 数据处理 只要你不等于删除的key 保留
const filterResItemArr = preRightData?.filter(it => it.value !== key)
setRightSelectData(filterResItemArr) // 更新右边数据 即可刷新右边列表
setOriginRightSelectData(filterResItemArr) // 更新右边数据
setSelectKey(filterResKeyArr) // 更新选中的key 即可刷新左边选中项
}

  const handleRightClearSelectData = () => {
// 这就暴力了塞
setSelectKey([])
setRightSelectData([])
setOriginRightSelectData([])
}

动态列组件 实现搜索



  • 搜索,就是改变一下数据 视觉上看起来有过滤的效果塞

  • 刚才我们不是多存了一份数据源嘛

  • 出来见见啦~


const onSearch = (val, curData, isRight = false) => {
const searchKey = val
// 这个是同时支持 左右两边
// 做个判断
if (!isRight) {
// 在判断一下是否有搜索内容 因为也需要清空的啦
if (searchKey) {
// 有,我就过滤呗
const searchResArr = curData?.filter(item => item.label.includes(searchKey))
setLeftSelectData(searchResArr)
}
if (!searchKey) {
// 没有 我就把原本数据还给你呗
setLeftSelectData(originSelectData)
}
}
// 右边 一样
if (isRight) {
if (searchKey) {
const searchResArr = curData?.filter(item => item.label.includes(searchKey))
setRightSelectData(searchResArr)
}
if (!searchKey) {
setRightSelectData(originRightSelectData)
}
}
}

动态列组件 增删改与表格 实现数据绑定



  • 里面的数据 处理好了 直接再关闭的时候 丢给外面嘛

  • 把右边的内容(也就是选中的key)返回给表格

  • 表格再自己构造


  const handleOk = (colVal, isUseDefaultCol) => {
`colVal` : 选中的列key
`isUseDefaultCol`:是否使用默认列
// table column字段组装
const normalColConstructor = (
title,
dataIndex,
isSort = true,
colWidth = 150,
isEllipsis = false,
render = null
) => {
const renderObj = render ? { render } : {}
return {
title,
dataIndex,
sorter: isSort,
width: colWidth,
ellipsis: isEllipsis,
key: dataIndex,
...renderObj,
}
}
const statusRender = text => approvalStatusRender(text)
const dateRender = (text, record) => {dayjs(text).format('YYYY-MM-DD')}
const newColArr = []
// 定制化处理 (其实还有2.0)
colVal?.forEach(({ label, value }, index) => {
let isSort = false
let renderFn = null
const isSubmissionAmount = value === 'submissionAmount'
const isApprovalAmount = value === 'approvalAmount'
const isReductionRate = value === 'reductionRate'
const isInitiationTime = value === 'initiationTime'

// 特定的业务场景 特殊状态渲染
const isStatus = value === 'status'
// 特定的业务场景 时间类型 加上排序
if (isApprovalAmount || isInitiationTime || isReductionRate || isSubmissionAmount) {
isSort = true
}
if (isStatus) {
renderFn = statusRender
}
// 普通列 已就绪
// 普通列 标题 拿label就ok
newColArr.push(normalColConstructor(label, value, isSort, 100, true, renderFn))
})

// 最后在头部追加一个序号
newColArr.unshift({
title: '序号',
dataIndex: 'orderCode',
width: 45,
render: (_: any, record: any, index) => tablePageSize * (tablePage - 1) + index + 1,
})
// 最后在尾部部追加一个操作
newColArr.push({
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 50,
render: (text, row: DataType) => (



),
})

if (colVal?.length) {
if (isUseDefaultCol) {
setColumns([...originColumns])
} else {
setColumns([...newColArr])
}
} else {
setColumns([...originColumns])
}
}

// 解决表格存在子列 -- 先搞定数据结构 -- 在解决表格列内容填充 -- 无需捆绑
// 遍历拿到新增列数组 匹配上代表 需要子数组 进行转换
// eslint-disable-next-line consistent-return
colVal?.forEach(({ label, value }, index) => {
// DesignHomeDynamicCol[value] 返回的值能匹配到 说明存在 嵌套列
const validVal = DesignHomeDynamicClassify[DesignHomeDynamicCol[value]]
const isHasChild = newColChildObj[validVal]
const titleText = DesignHomeDynamicLabel[value]
if (validVal) {
// 如果已经有孩子 追加子列
if (isHasChild) {
newColChildObj[validVal] = [...isHasChild, normalColConstructor(titleText, value)]
} else {
// 则 新增
newColChildObj[validVal] = [normalColConstructor(titleText, value)]
}
} else {
// 普通列 已就绪
// 普通列 标题 拿label就ok
newColArr.push(normalColConstructor(label, value, false, 100, true))
}
})

动态列组件 实现恢复初始态 实现双向绑定



  • 这个就更简单啦 再点击确定的时候 传一个 isUseDefaultData:true

  • 只是这个isUseDefaultData 的逻辑判断问题

  • 当动态列组件 点击恢复默认列 我们只需把 当初传进来的 原始列数据 更新到 selectKey 状态即可


const handleDefaultCol = () => {
// 这里是考虑到组件灵活性 数据可由自己处理好在传入
if (isOutEmitData) {
setSelectKey(initSelectKey)
} else {
// 这里是使用 内部数据处理逻辑
setSelectKey(originSelectKey)
}
}

const handleOk = () => { 
// 数据比对 是否使用默认校验
// originColumnMapSelectKey 源数据与传出去的数据 进行比对
const originRightMapKey = originRightSelectData?.map(it => it.value)
// 采用 lodash isEqual 方法
const isSame = isEqual(originSelectKey, originRightMapKey)
// 判断外部是否有传 确定事件 handleOk
if (modalOk) {
modalOk(originRightSelectData, isSame)
}
setOpen(false)
}

const handleOk = (colVal, isUseDefaultCol) => {
... 一堆代码
// 当用户清空以后 还是恢复表格默认状态
if (colVal?.length) {
// 恢复默认列
if (isUseDefaultCol) {
setColumns([...originColumns])
} else {
// 否则就拿新数据更新
setColumns([...newColArr])
}
} else {
setColumns([...originColumns])
}
}

动态列组件 实现一键控制功能 全选与清空



  • 这就是Vip版本的噻

  • 但是也简单 无非就是操作多选框 无非多选框就三种态

  • 未选 半选 全选

  • 既然我们下面的逻辑已处理好 这个其实也很快的锅

  • 首先,就是下面数据变化的时候 我们上面需要去感应

  • 其次就是 上面操作的时候 下面也需要感应

  • 最后 双向数据绑定 就能搞定 没有那么神秘

  • 一步一步来 先分别把 上下事件处理好


const onCheckBoxChange = (dataArr = [], e = null) => {
// 判断所有数据长度
const allLen = originSelectData?.length
// 根据当前选中数据长度 判断是 多选框三种状态当中的哪一种
const checkLen = e ? selectKey?.length : dataArr?.length // 全选
const isAllSelect = allLen === checkLen // 半选
const isHalfSelect = allLen > checkLen
// 然后再判断一下是点击一键全选事件 触发还是 点击下面选项的时候触发
// 点击一键全选 能拿到事件的 e.target 从而来判断
// 这里是操作下面按钮的时候 触发
if (!e) {
// 如果没有选中
if (checkLen === 0) {
// 恢复未选状态
setCheckAll(false)
setIndeterminate(false)
return ''
}
if (isAllSelect) {
// 如果是全选 改为全选态
setCheckAll(true)
setIndeterminate(false)
}
if (isHalfSelect) {
// 半选态
setIndeterminate(true) // 这个控制 多选框的半选态
setCheckAll(false)
}
}
// 这个就是用户操作 一键全选按钮触发
if (e) {
// 如果当前长度为0 那么应该更新为全选
if (checkLen === 0) {
setCheckAll(true)
setIndeterminate(false)
setSelectKey(originSelectData?.map(it => it.value))
}
// 如果已经全选 就取消全选
if (isAllSelect) {
setCheckAll(false)
setIndeterminate(false)
setSelectKey([])
}
// 如果是半选态 就全选
if (isHalfSelect) {
setCheckAll(true)
setIndeterminate(false)
setSelectKey(originSelectData?.map(it => it.value))
}
}
}

const onCheckChange = checkedValues => {
// 往右边追加数据
const selectResArr = checkedValues?.map(val => transferObj[val]) setSelectKey(checkedValues)
setRightSelectData(selectResArr)
setOriginRightSelectData(selectResArr)
}


  • 我们两个事件都处理好 那么开始进行联动

  • 意思就是 我们拿什么去控制这两个机关 核心是不是就是 选中的选项啊

  • 有两种解法,第二种可能有点绕



    • 一、就是在下面每个选项改变的时候 都去调一下 上面一键全选事件;然后上面变化的时候 也去调一下 下面选项的事件 (常规)缺点:容易漏 一变多改





    • 二、监听选项key的变化 去触发开关 避免我们第一种缺点 (优解)同时解决了确保外面传进来的 表格列 使得下拉框选中这些项 生效




// 这里通过 useEffect() 实现监听 确保外面传进来的 表格列 使得下拉框选中这些项 生效 
useEffect(() => {
onCheckBoxChange(selectKey)
onCheckChange(selectKey)
// eslint-disable-next-line react-hooks/exhaustive-deps },
[selectKey]
)

结束


都看到这里了,不留点痕迹,是怕我发现么?

作者:造更多的轮子
来源:juejin.cn/post/7266463919139684367

收起阅读 »

卸下if-else 侠的皮衣!- 适配器模式

web
🤭当我是if-else侠的时候 😶怕出错 给我一个功能,我总是要写很多if-else,虽然能跑,但是维护起来确实很难受,每次都要在一个方法里面增加逻辑,生怕搞错,要是涉及到支付功能,分分钟炸锅 😑难调试 我总是不知道之前写的逻辑在哪里,一个方法几百行逻辑,而且...
继续阅读 »

🤭当我是if-else侠的时候


😶怕出错


给我一个功能,我总是要写很多if-else,虽然能跑,但是维护起来确实很难受,每次都要在一个方法里面增加逻辑,生怕搞错,要是涉及到支付功能,分分钟炸锅


😑难调试


我总是不知道之前写的逻辑在哪里,一个方法几百行逻辑,而且是不同功能点冗余在一起!这可能让我牺牲大量时间在这查找调试中


🤨交接容易挨打


当你交接给新同事的时候,这个要做好新同事的白眼和嘲讽,这代码简直是坨翔!这代码简直是个易碎的玻璃,一碰就碎!这代码简直是个世界十大奇迹!


🤔脱下if-else侠的皮衣


先学习下开发的设计原则


单一职责原则(SRP)



就一个类而言,应该仅有一个引起他变化的原因



开放封闭原则(ASD)



类、模块、函数等等应该是可以扩展的,但是不可以修改的



里氏替换原则(LSP)



所有引用基类的地方必须透明地使用其子类的对象



依赖倒置原则(DIP)



高层模块不应该依赖底层模块



迪米特原则(LOD)



一个软件实体应当尽可能的少与其他实体发生相互作用



接口隔离原则(ISP)



一个类对另一个类的依赖应该建立在最小的接口上



在学习下设计模式


大致可以分三大类:创建型结构型行为型

创建型:工厂模式 ,单例模式,原型模式

结构型:装饰器模式,适配器模式,代理模式

行为型:策略模式,状态模式,观察者模式


上篇文章学习了 策略模式,有兴趣可以过去看看,下面我们来学习适配器模式


场景:将一个接口返回的数据,转化成列表格式,单选框数据格式,多选框数据格式


用if-else来写,如下


let list = []
let selectArr = []
let checkedArr = []

http().then(res =>{
//处理成列表格式
this.list = this.handel(res,0)
//处理成下拉框模式
this.selectArr = this.handel(res,1)
//处理成多选框模式
this.checkedArr = this.handel(res,2)
})
handel(data,num){
if(num == 0){
....
}else if(num ==1){
....
}else if(num ==2){
....
}
}

分下下问题,假如我们多了几种格式,我们又要在这个方法体里面扩展,违反了开放封闭原则(ASD),所以我们现在来采用适配器模式来写下:


//定义一个Adapter
class Adpater {
data = [];
constructor(){
this.data = data
}
toList(col){
//col代表要的字段,毕竟整个data的字段不是我们都想要的
//转列表
return this.data.map(item =>{
const obj = {}
for(let e of col){
const f = e.f
obj[f] = item[f];
}
return obj
})
}
//用了map的省略写法,来处理转化单选的格式
toSelect(opt){
const {label,value} = opt
return this.data.map(item => ({
label,
value
}))
}
//同上处理转化多选的格式
toChecked(opt){
const {field} = opt
return this.data.map(item => ({
checked:false,
value:item[field]
}))
}
}

//下面是调用这个适配类
let list = []
let selectArr = []
let checkedArr = []
http.then(data =>{
const adapter = new Adatpter(data)
//处理列表
list = adapter.toList(['id','name','age'])
//处理下拉
selectArr = adapter.toSelect({
label:'name'
value:'id'
})
//处理多选
checkedArr = adapter.toChecked({
field:'id'
})
})

这个扩展性就能大大提高,看着也会好看很多,可以通过继承等等方式来扩展,继承方式下次有机会再来写代码,文章先到这里!


结尾


遵守设计规则,脱掉if-else的皮衣,善用设计模式,加

作者:向乾看
来源:juejin.cn/post/7265694012962537513
油,骚年们!给我点点赞,关注下!

收起阅读 »

当文字成为雨滴:HTML、CSS、JS创作炫酷的"文字雨"动画!

web
简介 大家好,今天要给大家带来一个Super Cool的玩意儿😎! 在本篇技术文章中,将介绍如何使用HTML、CSS和JavaScript创建一个独特而引人注目的"文字(字母&数字)"雨🌨️动画效果。通过该动画,展现出的是一系列随机字符将从云朵中下落...
继续阅读 »

简介


大家好,今天要给大家带来一个Super Cool的玩意儿😎!


rain-preview.gif


在本篇技术文章中,将介绍如何使用HTML、CSS和JavaScript创建一个独特而引人注目的"文字(字母&数字)"雨🌨️动画效果。通过该动画,展现出的是一系列随机字符将从云朵中下落像是将文字变成雨滴从天而降,营造出与众不同的视觉效果;


HTML


创建一个基本的HTML结构,这段HTML代码定义了一个容器,其中包含了"云朵"和"雨滴"(即文字元素)。基本结构如下:



  • 首先是类名为container的容器,表示整个动画的容器;

  • 其次是类名为cloud的容器,表示云朵的容器;

  • 接着是cloud容器中的文字元素,表示雨滴(即文字元素);
    然后引入外部创建的css和js文件,可以先定义几个text容器,用于调整样式;


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Text Rain Animation</title>

<link rel="stylesheet" href="./css/style.css">
</head>
<body>
<div class="container">
<div class="cloud">
<!-- <div class="text">a</div> -->
<!-- <div class="text">b</div> -->
<!-- <div class="text">c</div> -->
<!-- 雨滴将会在这里出现 -->
</div>
</div>

<script src="./js/main.js"></script>
</body>
</html>

CSS


CSS是为文字雨效果增色添彩的关键,使动画效果更加丰富,关于一些 CSS 样式:



  • 使用了自定义的颜色变量来为背景色和文本颜色提供值,有助于使代码易于维护和修改;

  • 利用CSS的阴影效果和动画功能,创造逼真的"云朵"和流畅的"雨滴"动画;


* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

:root {
--body-color: #181c1f;
--primary-color: #ffffff;
}

body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: var(--body-color);
}

.container {
width: 100%;
height: 400px;
display: flex;
justify-content: center;
border-bottom: 1px solid rgba(255, 255, 255, .1);
/* 添加一个从下往上线性渐变的镜像效果,增加视觉层次感 */
-webkit-box-reflect: below 1px linear-gradient(transparent, transparent, transparent, transparent, #0005);
}

.cloud {
position: relative;
top: 50px;
z-index: 100;

/* 横向云朵 */
width: 320px;
height: 100px;
background-color: var(--primary-color);
border-radius: 100px;

/* drop-shadow函数将阴影效果应用于投影图像 */
filter: drop-shadow(0 0 30px var(--primary-color));
}
.cloud::before {
content: "";
/* 左侧小云朵 */
width: 110px;
height: 110px;
background-color: var(--primary-color);
border-radius: 50%;
position: absolute;
top: -50px;
left: 40px;

/* 右侧大云朵 */
box-shadow: 90px 0 0 30px var(--primary-color);
}

.cloud .text {
position: absolute;
top: 40px;
height: 20px;
line-height: 20px;

text-transform: uppercase;
color: var(--primary-color);
/* 为文字添加阴影,看上去发光,增加视觉效果 */
text-shadow: 0 0 5px var(--primary-color), 0 0 15px var(--primary-color), 0 0 30px var(--primary-color);
transform-origin: bottom;
animation: animate 2s linear forwards;
}

@keyframes animate {
0% {
transform: translateX(0);
}

70% {
transform: translateY(290px);
}

100% {
transform: translateY(290px);
}
}

通过关键帧动画 @keyframes animate 定义文字运动的过程,在这里是垂直移动290px,也就是向下移动,模拟下雨的状态。当然为了让文字雨效果更加好看,还可以引入一下字体库;



Warning


-webkit-box-reflect:可将元素内容在特定方向上进行轴对称反射;


但是该特性是非标准的,请尽量不要在生产环境中使用它!


目前只有webkit内核的浏览器支持,如:谷歌浏览器、Safari浏览器。在火狐浏览器中是不支持的;



JavaScript


最后,使用JavaScript来实现文字雨的效果。通过动态生成并随机选择字符,可以实现让这些字符(雨滴)从.cloud(云朵)中降落的效果。JavaScript 脚本逻辑:



  • 首先,定义函数 generateText() 并创建字符集,定义函数 randomText() 通过从给定的字符集中随机选择一个字符返回;

  • 接下来,编写 rain() 函数,在函数内部,首先选取 .cloud 元素同时创建一个新的 <div>元素作为字符节点,设置元素文本内容为函数返回的字符,并添加类名;

  • 然后,利用 Math.random() 方法生成一些随机值,将这些随机值应用到创建的 <div> 元素上,包括:

    • 字符距离左侧位置,在 .cloud 容器的宽度区间;

    • 字体大小,最大不超过32px;

    • 动画周期所需的时间(动画持续时间),2s内;



  • 最后,将其<div>添加到 .cloud 元素中,使用 setTimeout() 函数在2秒后将文字节点从 .cloud 元素中移除,模拟雨滴落地消失效果;


定时器: 为了让字符(雨滴)持续下落,使用 setInterval 函数和一个时间间隔值来调用 rain() 函数。这样就是每20毫秒就会生成一个新的字符(雨滴)节点并添加到云朵中。


// 生成字母和数字数组
function generateText() {
const letters = [];
const numbers = [];

const a = "a".charCodeAt(0);

for (let i = 0; i < 26; i++) {
letters.push(String.fromCharCode(a + i));

if (i < 9) {
numbers.push(i + 1);
}
};

return [...letters, ...numbers];
};

// 从生成的数组中随机取出一个字符
function randomText() {
const texts = generateText();
const text = texts[Math.floor(Math.random() * texts.length)];

return text;
};

function rainEffect() {
const cloudEle = document.querySelector(".cloud");
const textEle = document.createElement("div");

textEle.innerText = randomText();
textEle.classList.add("text");

const left = Math.floor(Math.random() * 310);
const size = Math.random() * 1.5;
const duration = Math.random();
const styleSheets = {
left: `${left}px`,
fontSize: `${0.5 + size}em`,
animationDuration: `${1 + duration}s`,
};
Object.assign(textEle.style, styleSheets);

cloudEle.appendChild(textEle);
setTimeout(() => {
cloudEle.removeChild(textEle);
}, 2000);
};

// 每隔20ms创建一个雨滴元素
setInterval(() => rainEffect(), 20);

结论


通过HTML、CSS和JS的紧密合作,成功创建了一个炫酷的"文字雨"动画效果,这个动画可以增加网页的吸引力! 不要犹豫🖐️,动手尝试一下,或者甚至你也可以根据自己的需求对文字、样式和动画参数进行调整,进一步改善和扩展这个效果;


希望这篇文章对你在开发类似动画效果时有所帮助!另外如果你对这个案例还有任何问题,欢迎在评论区留

作者:掘一
来源:juejin.cn/post/7270648629378367528
言或联系(私信)我。谢谢阅读!🎉

收起阅读 »

别再用unload了:拥抱浏览器生命周期API

web
以前的前端页面很少有涉及页面的生命周期相关的话题,我们最熟悉的还是这几个load, beforeunload, unload 事件,用来监听页面加载完、离开页面之前、页面卸载事件并做一些相应的处理,比如保存需要持久化的数据,或者上报一些埋点数据。首先我们先理下...
继续阅读 »

以前的前端页面很少有涉及页面的生命周期相关的话题,我们最熟悉的还是这几个load, beforeunload, unload 事件,用来监听页面加载完、离开页面之前、页面卸载事件并做一些相应的处理,比如保存需要持久化的数据,或者上报一些埋点数据。首先我们先理下现有问题:


unload事件的问题


上面的这些事件(特别是unload)会有以下一些问题:



  • 用户主动触发的事件,无系统自动触发的一些状态监控

  • 无法稳定触发,特别是在手机端

  • 前进/后退时无法进入浏览器缓存(back/forward cache),导致用户来回切换时加载慢

  • 用户离开时埋点数据可能无法稳定上报

  • 无法追踪用户在页面上的完整生命周期


虽然现在计算机的算力硬件越来越强,但是现在我们同时需要处理的事情越来越多,开的Tab也就越来越多,一直被人诟病的比如chrome的疯狂吃内存的问题也很是头疼,接近顶配的电脑可能很快就被chrome给耗尽😂,所以现在的chrome版本会根据系统资源消耗程度来对不活动的Tab冻结或直接销毁来释放内存及减少电量的消耗。


针对这些问题现代浏览器提供了多个生命周期相关的事件来让开发者可以监听,并在触发时做相应处理。这里主要介绍几个最常用的而且对埋点相对比较重要的节点的生命周期事件。


页面是否可见: visibilitychange


visibilitychange事件涉及的场景会比较多,主要有用户切换tab导航到其他页面关闭Tab最小化窗口手机端切换app等,可在document上添加该事件,回调里通过document.visibilityState可以知道当前Tab是否隐藏了:


document.addEventListener(
'visibilitychange',
(e) => {
const isTabVisible = document.visibilityState === 'visible'
console.log('tab is visible: ', isTabVisible)
},
{
capture: true,
}
)

以上事件需要在捕获阶段进行监听(包括下面讲到的其他相关事件),避免被业务代码阻止冒泡,而且有些是window层面的,没有冒泡的阶段,所以需要在capture阶段执行。


页面的加载与离开: pageshow/pagehide


首先,这事件名是真的很容易让人理解错😭(pageshow/pagehide感觉才是上面visibilitychange所表示的意义)。


pageshow主要在页面新加载或被浏览器冻结后重新解冻时触发, 可通过e.persisted来确定是如何加载的:


window.addEventListener(
'pageshow',
(e) => {
// true 为之前冻结现在解冻,false相当于重新加载或新加载
console.log('e.persisted: ', e.persisted)
},
{
capture: true,
}
)

pagehide本质上是对unload事件的真正替换且具有更稳定的触发时机,我们可以在这里对一些埋点事件或者其他一些小批量的数据进行上报(sendBeacon或fetch, 下面讲到),这里还有个需要注意的是visibilitychange触发范围更广,也就是页面进来和离开时也会触发,和pageshow/pagehide同时触发(都触发时在这2个之前),所以如果业务需要区分页面离开还是用户仅仅是切换tab时,需要在不同的事件回调里做不同的处理。


页面离开时的数据上报


当需要在用户离开页面时(pagehide触发时)稳定的上报一些数据,我们一般会使用navigator.sendBeacon()方法:


// queued = true 说明浏览器接收请求,马上会上报出去,false的话可能业务要做其他处理
const queued = navigator.sendBeacon('/api', data)

当然sendBeacon有大小限制,一般在64KB以下,超出很可能会失败,所以我们在这里上报时要控制大小,如果数据量较大,建议提前上报一部分,比如在visibilitychange时(用户切换tab时)先上报一部分,确保只留64KB以下的数据放到最后。
除了sendBeacon之外我们还可以用fetch上报,通过设置keepalive: true来达到sendBeacon一样的效果,当然也有一样的大小限制,这里可以做兼容处理:


function send(body) {
if (navigator.sendBeacon) {
navigator.sendBeacon('/api', body)
return
}
fetch('/api', {
body,
method: 'POST',
keepalive: true
})
}

其他生命周期事件


除了上面常用的生命周期之外,浏览器的生命周期API还提供了页面的其他状态如:freeze, resume,freeze一般在用户导航到其他页面并且满足进入back/forward cache的条件时,或者用户在其他tab,浏览器根据系统资源自动使其进入冻结状态,当用户通过浏览器的返回按钮或重新切回到该Tab时,会触发resume事件说明页面继续,紧接着会触发pageshow事件,说明页面又进来了。关于更多的说明可以参考Page Lifecycle API


电脑息屏、休眠时


还有一些场景是电脑休眠或者只是关闭屏幕时,页面的生命周期以及页面里的一些定时器会有哪些变化?


休眠比较简单,定时器、网络连接这些都会被暂停,如果不想丢失数据,需要在pagehide做处理;息屏时可在document.visibilityState !== 'visible'时做处理,相当于页面不可见了,而且页面里的定时器不会被停掉但是可能会被浏览器延时处理,比如正常代码里是5s执行一次,此时可能会变成30s或者1min执行一次来节省资源。


总结


以上主要介绍了如何使用现代浏览器提供的生命周期API来在页面的不同阶段做相应的处理,pagehide事件主要用来替换unload, 关于beforeunload事件在有些应用中还是需要的,但是我们应该有选择性的添加该事件及在合适的时间移除监听该事件,比如未保存的数据已经保存完毕时可以移除,当又有新改动时再监听。


还有很多其他的生命周期事件可以让开发者能对用户在页面里/外的整个生命周期有更好的理解,以此来分析并提升网站的整体体验。笔者后面会写一篇如何跟踪用户在浏览器里的同一tab/不同tab之间的来回操作、记录并上报,并能够在后台进行session回放的文章,通过这些扩展能力相较于纯粹的埋点能更好地理

作者:jasonboy7
来源:juejin.cn/post/7269983642155663419
解用户行为以此来优化产品体验🍻。

收起阅读 »

新一代前端工具链Rome:革新前端开发

web
在前端开发领域,每时每刻都在涌现着各种新的工具和框架。而 Rome,作为一款新一代前端工具链,引起了广泛的关注和热议。它不仅提供了卓越的性能,还整合了各种强大的功能,使前端开发变得更加高效。本文将深入介绍 Rome,并为你提供一些代码示例,帮助你更好地了解和使...
继续阅读 »

在前端开发领域,每时每刻都在涌现着各种新的工具和框架。而 Rome,作为一款新一代前端工具链,引起了广泛的关注和热议。它不仅提供了卓越的性能,还整合了各种强大的功能,使前端开发变得更加高效。本文将深入介绍 Rome,并为你提供一些代码示例,帮助你更好地了解和使用这个令人激动的工具。


image.png
image.png


Rome 是什么?


Rome 是一个全新的前端工具链,旨在重新定义前端开发体验。它是一个一站式解决方案,涵盖了许多前端开发中常见的问题和任务。Rome 的主要目标是提供一致性和高性能,以加速前端开发流程。它的核心特点包括:


1. 依赖管理


Rome 提供了强大的依赖管理系统,可用于管理你的项目中的依赖关系。它支持 JavaScript、TypeScript 和 Flow,并能够准确地分析和处理依赖项,以确保你的项目始终保持一致。


// 安装依赖
rome deps add react

// 查看依赖树
rome deps list

这个特性非常有用,因为它将所有的依赖关系都纳入统一管理,无需依赖其他工具。


2. 代码格式化


Rome 自带了一个先进的代码格式化工具,可帮助你统一项目中的代码风格。无需争论缩进或分号,Rome 将自动处理这些问题。


# 格式化整个项目
rome format

代码格式化对于团队协作和维护项目非常重要,它可以消除代码风格的争议,使代码更易读。


3. 静态类型检查


Rome 集成了强大的静态类型检查器,可以在编码过程中捕获潜在的类型错误,提高代码质量和可维护性。它支持多种类型系统,包括 TypeScript、Flow 等。


// 检查类型
rome check

这个特性有助于减少运行时错误,提前发现潜在的问题。


4. 构建工具


Rome 提供了一套强大的构建工具,可用于将你的代码编译成浏览器可执行的代码。这有助于优化性能并减小最终部署包的大小。


# 构建项目
rome build

Rome 的构建工具支持多种目标,你可以轻松地将项目构建成不同的输出格式。


5. 包管理


Rome 不仅支持 JavaScript 包管理,还可以处理 CSS、图片、字体等多种资源。这意味着你可以在一个地方管理所有资源,而无需额外的工具。


// 导入 CSS 文件
import './styles.css';

这个特性使得资源管理变得更加一致和方便。


Rome 的安装和配置


现在,让我们来看看如何安装 Rome 并进行基本配置。


步骤 1:安装 Rome


你可以使用 npm 或 yarn 安装 Rome。这里以 npm 为例:


npm install -g rome

安装完成后,你可以在终端中运行 rome -v 来确认 Rome 是否成功安装。


步骤 2:初始化项目


在你的项目目录中,运行以下命令来初始化一个 Rome 项目:


rome init

这将创建一个 .romerc.js 文件,其中包含了


项目的配置信息。


步骤 3:配置选项


你可以编辑 .romerc.js 文件来配置 Rome 的选项,以满足你的项目需求。例如,你可以指定项目的目标环境、依赖管理方式、构建选项等。


// .romerc.js
module.exports = {
target: 'browser',
module: {
type: 'commonjs',
},
build: {
minify: true,
},
};

使用 Rome


一旦你的项目配置好了,就可以开始使用 Rome 提供的工具来进行开发。以下是一些常用的命令和示例:


运行 linter 来检查代码风格和潜在问题。


rome check

运行格式化程序来自动格式化你的代码。


rome format

运行类型检查以确保类型安全。


rome typecheck

构建你的项目。


rome build

运行你的应用程序。


rome run

自定义和插件


Rome 还支持自定义插件和配置。你可以编写自己的插件,或者将现有的插件集成到你的项目中,以满足特定需求。


// .romerc.js
module.exports = {
custom: {
myPlugin: {
enabled: true,
options: {
// 自定义选项
},
},
},
};

在这个示例中,我们启用了一个名为 "myPlugin" 的自定义插件,并提供了一些自定义选项。


Rome发展前景




  1. 性能优化: Rome 的核心目标之一是提供高性能。未来,它将不断进行性能优化,以确保更快的构建和更快的开发体验。随着前端项目变得越来越复杂,性能优化将成为前端开发的重要问题,Rome 在这方面有望发挥重要作用。




  2. 更好的类型检查: Rome 集成了静态类型检查器,帮助开发者在编码阶段捕获潜在的类型错误。未来,这个类型系统可能会进一步增强,提供更多的类型推断和错误检查功能。




  3. 更多的插件和扩展: Rome 的自定义插件和配置功能使得开发者可以根据自己的需求扩展 Rome。随着社区的不断壮大,预计会有更多的插件和扩展出现,为开发者提供更多的选择和解决方案。




  4. 更广泛的应用领域: Rome 目前主要用于前端开发,但未来可能会扩展到其他领域,如后端开发或跨平台开发。这将使 Rome 成为一个更加通用的工具链,适用于各种不同类型的项目。




  5. 更丰富的文档和教程: 随着 Rome 的发展,预计会有更多的文档、教程和学习资源出现,帮助开发者更好地掌握和使用 Rome。这将有助于扩大 Rome 的用户群体。




  6. 更强大的生态系统: Rome 的生态系统将继续扩大,包括各种开发工具、编辑器插件和整合解决方案。这将使 Rome 成为一个完整的开发生态系统,为开发者提供一站式的解决方案。




总之,Rome作为一个新一代前端工具链,充满了潜力,未来有望在前端开发领域取得更多的成功和影响力。开发者可以继续关注 Rome 的发展,利用它来提高前端开发效率和质量。


结语


Rome 是一个前端工具链的新星,它为前端开发者提供了许多强大的功能,以提高开发效率和代码质量。虽然 Rome 还在不断发展和改进中,但它已经展现出了巨大的潜力。无论你是新手还是经验丰富的前端开发者,都值得一试 Rome,看看它如何改变你的前端开发流程。在使用 Rome 时,记得查阅官方文档以获取更多详细信息和示例代码。希望这篇文章能帮助你入门 Ro

作者:侠名风
来源:juejin.cn/post/7269745800178925603
me 并开始在你的项目中使用它。

收起阅读 »

JS 不写分号踩了坑,但也可以不踩坑

web
踩的坑 写一个方法将秒数转为“xx天xx时xx分xx秒”的形式 const ONEDAYSECOND = 24 * 60 * 60 const ONEHOURSECOND = 60 * 60 const ONEMINUTESECOND = 60 functi...
继续阅读 »

踩的坑


写一个方法将秒数转为“xx天xx时xx分xx秒”的形式


const ONEDAYSECOND = 24 * 60 * 60
const ONEHOURSECOND = 60 * 60
const ONEMINUTESECOND = 60

function getQuotientandRemainder(dividend,divisor){
const remainder = dividend % divisor
const quotient = (dividend - remainder) / divisor
return [quotient,remainder]
}

function formatSeconds(time){
let restTime,day,hour,minute
restTime = time
[day,restTime] = getQuotientandRemainder(restTime,ONEDAYSECOND)
[hour,restTime] = getQuotientandRemainder(restTime,ONEHOURSECOND)
[minute,restTime] = getQuotientandRemainder(restTime,ONEMINUTESECOND)
return day + '天' + hour + '时' + minute + '分' + restTime + '秒'
}
console.log(formatSeconds(time)) // undefined天undefined时undefined分NaN,NaN秒

按照这段代码执行完后,day、hour、minute这些变量得到的都是 undefined,而 restTime 则好像得到一个数组。

问题就在于 13、14、15、16 行之间没有添加分号,导致解析时,没有将这三行解析成三条语句,而是解析成一条语句。最终的表达式就是这样的:


restTime = time[day,restTime] = getQuotientandRemainder(restTime,ONEDAYSECOND)[hour,restTime] = getQuotientandRemainder(restTime,ONEHOURSECOND)[minute,restTime] = getQuotientandRemainder(restTime,ONEMINUTESECOND)

那执行的过程相当于给 restTime 进行赋值,表达式从左往右执行,最终表达式的值为右值。最右边的值就是 getQuotientandRemainder(restTime,ONEMINUTESECOND),由于在计算过程中 restTime 还没有被赋值,一直是 undefined,所以经过 getQuotientandRemainder 计算后得到的数组对象每个成员都是 NaN,最终赋值给 restTime 就是这样一个数组。


分号什么时候会“自动”出现


有时候好像不写分号也不会出问题,比如这种情况:


let a,b,c
a = 1
b = 2
c = 3
console.log(a,b,c) // 1 2 3

这是因为,JS 进行代码解析的时候,能够识别出语句的结束位置并“自动添加分号“,从而能够解析出“正确”的抽象语法树,最终执行的结果也就是我们所期待的。

JS 有一个语法特性叫做 ASI (Automatic Semicolon Insertion),就是上面说到的”自动添加分号”的东西,它有一定的插入规则,在满足时会为代码自动添加分号进行断句,在我们不写分号的时候,需要了解这个规则,才能不踩坑。(当然这里说的加分号并不是真正的加分号,只是一种解析规则,用分号来代表语句间的界限)


ASI 规则


JS 只有在出现换行符的时候才会考虑是否添加分号,并且会尽量“少”添加分号,也就是尽量将多行语句合成一行,仅在必要时添加分号。


1. 行与行之间合并不符合语法时,插入分号


比如上面那个自动添加分号的例子,就是合并多行时会出现语法错误。

a = 1b = 2 这里 1b 是不合法的,因此会加入分号使其合法,变为 a = 1; b = 2


2. 在规定[no LineTerminator here]处,插入分号


这种情况很有针对性,针对一些特定的关键字,如 return continue break throw async yield,规定在这些关键字后不能有换行符,如果在这些关键字后有了换行符,JS 会自动在这些关键字后加上分号。
看下面这个例子🌰:


function a(){
return
123
}
console.log(a()) // undefined

function b(){
return 123
}
console.log(b()) // 123

在函数a中,return 后直接换行了,那么 return 和 123 就会被分成两条语句,所以其实 123 根本不会被执行到,而 return 也是啥也没返回。


3. ++、--这类运算符,若在一行开头,则在行首插入分号


++ 和 -- 既可以在变量前,也可以在变量后,如果它们在行首,当多行进行合并时,会产生歧义,到底是上一行变量的运算,还是下一行变量的运算,因此需要加入分号,处理为下一行变量的运算。


a
++
b
// 添加分号后
a
++b

如果你的预期是:


a++ 
b

那么就会踩坑了。


4. 在文件末尾发现语法无法构成合法语句时,会插入分号


这条和 1 有些类似


不写分号时需要注意⚠️


上面的 ASI 规则中,JS 都是为了正确运行代码,必须按照这些规则来分析代码。而它不会做多余的事,并且在遵循“尽量合并多行语句”的原则下,它会将没有语法问题的多行语句都合并起来。这可能违背了你的逻辑,你想让每行独立执行,而不是合成一句。开头贴出的例子,就是这样踩坑的,我并不想一次次连续的对数组进行取值🌚。

因此我们要写出明确的语句,可以被合并的语句,明确是多条语句时需要加上分号。


(如果你的项目中使用了某些规范,它不想让你用分号,别担心,它只是不想让你在行尾用分号,格式化时它会帮你把分号移到行首)像这样:


// before lint
restTime = time;
[day, restTime] = getQuotientandRemainder(restTime, ONEDAYSECOND);
[hour, restTime] = getQuotientandRemainder(restTime, ONEHOURSECOND);
[minute, restTime] = getQuotientandRemainder(restTime, ONEMINUTESECOND);

// after lint
restTime = time
;[day, restTime] = getQuotientandRemainder(restTime, ONEDAYSECOND)
;[hour, restTime] = getQuotientandRemainder(restTime, ONEHOURSECOND)
;[minute, restTime] = getQuotientandRemainder(restTime, ONEMINUTESECOND)

参考


收起阅读 »

情人节这天,跟女友 研究点餐小程序的目录 是怎么搞的?

web
今天情人节(七夕特辑) 🏩 背景(餐厅) 现在去餐厅订餐,桌角那里差不多都会有一个二维码,叫你扫码自己手机上点菜。扫码进去,要么是公众号的,要么就是小程序。 那我们来看看一个以下案例: 除了点餐系统外,还有一些文档目录用到这种方式的目录导航锚点。比如说...
继续阅读 »

今天情人节(七夕特辑)



🏩 背景(餐厅)



现在去餐厅订餐,桌角那里差不多都会有一个二维码,叫你扫码自己手机上点菜。扫码进去,要么是公众号的,要么就是小程序。



那我们来看看一个以下案例:


Kapture 2023-08-21 at 16.16.17.gif


除了点餐系统外,还有一些文档目录用到这种方式的目录导航锚点。比如说文档,或者掘金的右侧的目录也用到了同样的效果。


Kapture 2023-08-22 at 09.31.45.gif


🔪 剖析原理


监听scroll事件,获取分类最开始的offsetTop,拿当前页面的scrollTop跟这些offsetTop比较,到了就把左边菜单栏的那个分类变颜色。至于点击,点到哪个分类,就找到对应的右侧分类的标题的offsetTop,通过window.scrollTo(0, 要点击分类的右侧内容的offsetTop)就可以使得内容对应滚动到该位置。


🥬 上菜实操


数据结构:【nestjs返回的数据】如若只是测试,可以写死某些数据即可。


image.png


image.png


http://127.0.0.1:5173/


界面布局:【左(分类)右(内容)】分为两个区域,两边均可滚动。


image.png


<template>
<div class="order">
<div class="category">
<div
v-for="(item, key) in goods"
:class="{
'category-name': true,
active: currentKey === key,
}"

:key="key"
@click="changeCategory(key)"
>
{{ item.categoryName }}
</div>
</div>
<div ref="content" class="content" @scroll="handleScroll">
<div
v-for="(item, key) in goods"
:key="key"
class="content-item"
ref="categoryRefs"
>
<div class="title">{{ item.categoryName }}</div>
<div
class="each-item"
v-for="good in item.goodsList"
:key="good.goodsCode"
>
<div class="image-url">
<img :src="good.imagePathSmall" />
</div>
<div class="desc">
<div class="goods-name">{{ good.goodsName }}</div>
<div class="goods-slogan">{{ good.goodsSlogan }}</div>

<div class="bottom">
<div>¥{{ good.goodsStandardList[0].acturalPrice }}</div>
<div>+</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

image.png


整体效果如下:


image.png


变量以及后端返回的商品列表:


image.png


逻辑:(一个是左侧点分类区域)、(一个是滚动右边内容,左侧分类要根据哪个分类变样式)


左侧点击分类区域:


image.png


content.value.scrollTo({
top: categoryRefs.value[key].offsetTop,
behavior: "smooth",
})

这里值得提的是,scrollTo加behavior方法进行滚动时,可以通过添加behavior参数来指定滚动的动画行为。(smooth 滚动行为具有平滑的动画效果,窗口会平滑地滚动到指定位置)


滚动右边内容:


image.png


当内容整体滚动的高度等于或者超过了某个分类的高度,那边就把左侧分类变样式就可以了,变样式通过key的方式来判断active: currentKey === key


注意点:


content.value.scrollTo({
top: categoryRefs.value[key].offsetTop,
behavior: "smooth",
});

因为我们设置的scrollTo是带有平滑滑动的属性的behavior: "smooth",所以导致点击左侧分类,自然而然也会触发到右侧的滚动事件,导致左侧跨多个点击的时候,中间所有的类都会改样式,问题如下:


Kapture 2023-08-22 at 10.46.59.gif


所以要解决这个问题,思路是,用一个变量,判断左侧点击彻底结束,右侧滚动事件才生效,解决办法如下:


image.png


最终效果


Kapture 2023-08-22 at 10.51.01.gif


🚶‍♀️ 总结消化


做点餐平台,重点(地图和定位服务支付平台和第三方支付配送跟踪);当然最后用户的体验设计也是很重要的,易用性、响应速度、交互设计的,都会影响到顾客当天来店里吃饭点餐的心情和体验。


以上是关于公众号或者小程序一般的点餐系统的大多数点餐页面滚动效果的研究。



爱在朝夕 不止七夕




☎️ 希望对大家有所帮助,如有错误,望不吝赐教,欢迎评论区留言互相学

作者:盏灯
来源:juejin.cn/post/7269786623813074996
习。


收起阅读 »

pnpm改造替换npm

Q: 为什么要迁移pnpm? 相比于npm,pnpm有一些优势:更快的安装速度: 在安装包时,pnpm使用了硬链接的方式,将已安装的包链接到新的目录下,而不是复制或下载包。这样,当你安装一个包的不同版本或者不同项目使用同一个包时,它们会共享已经安装的包,减少了...
继续阅读 »

Q: 为什么要迁移pnpm?


相比于npm,pnpm有一些优势:

  1. 更快的安装速度: 在安装包时,pnpm使用了硬链接的方式,将已安装的包链接到新的目录下,而不是复制或下载包。这样,当你安装一个包的不同版本或者不同项目使用同一个包时,它们会共享已经安装的包,减少了磁盘空间的占用,同时也加速了安装的速度。

  2. 更少的磁盘空间占用: 由于pnpm使用硬链接的方式共享已安装的包,因此相比于npm,pnpm占用更少的磁盘空间。

  3. 更好的本地缓存: pnpm会缓存包的元数据和二进制文件到本地缓存中,这样再次安装相同的包时,会从本地缓存中读取,而不是重新下载。这样可以提高安装包的速度,并减少网络带宽的消耗。

  4. 更好的多项目管理: pnpm可以管理多个项目的依赖,可以将相同的依赖安装在一个公共的位置,减少磁盘空间的占用,并且可以快速地切换项目之间的依赖关系。

  5. 更好的可重复性: pnpm使用了锁文件来保证安装包的版本一致性,同时也支持自定义的锁文件名称和路径。这样可以确保项目在不同的环境中的安装结果一致,增强了可重复性。


需要注意的是,pnpm相比于npm也存在一些缺点,例如兼容性问题、社区支持不如npm等。因此,在选择使用pnpm还是npm时,需要根据自己的实际需求和项目情况进行权衡。


Q: 上面提到的硬链接和符号链接是什么?


硬链接和符号链接都是文件系统中的链接方式,它们的作用是可以将一个文件或目录链接到另一个文件或目录上,从而实现共享或复制等功能。下面我来简单介绍一下它们的区别和示例。


硬链接


硬链接是指在文件系统中,将一个文件名链接到另一个文件上,使它们指向同一个物理数据块,也就是说,这两个文件名共享同一个inode节点。硬链接的本质是将一个文件名指向一个已存在的文件。


硬链接的特点:

  • 硬链接不能跨越不同的文件系统,因为inode节点只存在于一个文件系统中。
  • 硬链接可以看作是原文件的一个副本,它们的文件权限、拥有者、修改时间等都是相同的。
  • 删除硬链接并不会删除原文件,只有当所有的硬链接都被删除后,原文件才会被真正删除。

下面是一个硬链接的示例:

$ touch file1 # 创建一个文件
$ ln file1 file2 # 创建硬链接
$ ls -li file* # 查看文件inode节点
12345 -rw-r--r-- 2 user user 0 Apr 26 10:00 file1
12345 -rw-r--r-- 2 user user 0 Apr 26 10:00 file2

可以看到,file1和file2的inode节点是相同的,说明它们共享同一个物理数据块。


符号链接


也称之为软链接,符号链接是指在文件系统中,创建一个特殊的文件,其中包含了另一个文件的路径,通过这个特殊文件来链接到目标文件。符号链接的本质是将一个文件名指向一个路径。


符号链接的特点:

  • 符号链接可以跨越不同的文件系统,因为它们只是一个指向文件或目录的路径。
  • 符号链接指向的是目标文件或目录的路径,而不是inode节点,因此,目标文件或目录的属性信息可以独立于符号链接存在。
  • 删除符号链接不会影响目标文件或目录,也不会删除它们。

下面是一个符号链接的示例:

$ touch file1 # 创建一个文件
$ ln -s file1 file2 # 创建符号链接
$ ls -li file* # 查看文件inode节点
12345 -rw-r--r-- 1 user user 0 Apr 26 10:00 file1
67890 lrwxr-xr-x 1 user user 5 Apr 26 10:01 file2 -> file1

可以看到,file2是一个符号链接文件,它的inode节点和file1不同,而是一个指向file1的路径。


Q: 看到一些文章里说pnpm走的是硬链接,有的说用了软连接。到底走的是什么?


其实,pnpm是软连接和硬链接都用了。可以这么理解,pnpm在机器上某个地方存放安装好的所有依赖包,这些依赖包是独立于我们代码仓库的,这也是前面说的pnpm在安装速度和磁盘空间占用上的优点。而我们的代码库确实是先通过硬链接的方式来建立代码库和已安装过的依赖包之间的共享关系。可以打开代码库看到node_modules下有一个.pnpm文件夹,里面放的就是当前代码库建立的硬链接。




.pnpm下的文件都是一些名字很长的,长这样:




这里不用关心具体是什么,我们需要关心的是node_mpdules下我们认识的npm依赖包,它们正是通过软连接的方式来链接到.pnpm下的这些依赖包的。在vscode下,可以明显看到npm包后面的软连接标识:




如果想看一下这些软连接到底指向哪里的,可以:

# 进入node_modules目录
cd node_modules

# 枚举文件列表
ll 

 可以看到,这就是node_modules下软链接到.pnpm下的。


Q: 这个模式跟npm dedupe是不是很相似,有什么不同?


pnpm的硬链接模式和npm的dedupe功能是类似的,都是通过共享已安装的包来减少磁盘空间的占用,同时也可以提高安装包的速度。但它们之间还是存在一些不同:

  1. 原理不同: pnpm使用硬链接的方式共享已安装的包,而npm使用的是符号链接的方式共享已安装的包。硬链接是文件系统的一种特殊链接,它可以将一个文件链接到另一个文件上,使它们共享相同的内容。符号链接则是一个指向另一个文件或目录的特殊文件。

  2. 适用范围不同: pnpm的硬链接模式可以在多个项目之间共享已安装的包,而npm的dedupe功能只能在单个项目内共享已安装的包。

  3. 优势不同: pnpm的硬链接模式可以减少磁盘空间的占用和提高安装包的速度,而npm的dedupe功能只能减少磁盘空间的占用。

  4. 实现方式不同: pnpm使用了自己的包管理器和包存储库,而npm使用了公共的包管理器和包存储库。这也是导致它们之间存在差异的一个重要原因。


需要注意的是,无论是使用pnpm的硬链接模式还是npm的dedupe功能,都需要谨慎使用,以避免出现意外的错误。特别是在使用硬链接模式时,如果多个项目共享同一个包,需要注意不要在一个项目中修改了该包的文件,导致其他项目也受到影响。


Q: pnpm对于node版本有要求吗?


pnpm有对node版本的要求。官方文档中列出的最低支持版本是Node.js 10.x,推荐使用的版本是Node.js 14.x。如果使用的是较旧的Node.js版本,可能会导致安装和使用pnpm时出现错误。


我这里本来用的是Node14.x。因为其他原因,本次也给Node升级到16.x了。


Q: pnpm有类似npm ci的命令吗?



补充:npm ci主要是用于刚刚在download了一个仓库后,还没有node_modules的时候让npm完全根据package.json和package-lock.json的规范来install依赖包。相比较于直接走npm inpm ci会带来更精确的小版本版本号控制,因为npm i对于一些"^1.0.2"这样的版本号,可能会按照1.x.x这样的规范给你无感升级了,造成和之前某些包版本号之间的差异。
但是当本地已有node_modules的时候,就没办法用npm ci命令了。



是的,pnpm也有类似 npm ci 命令的功能,可以使用 pnpm install --frozen-lockfile 命令实现。它会根据 package-lock.jsonpnpm-lock.yaml 确定依赖关系,并且在安装期间不会更新任何包。此命令类似于 npm ciyarn install --frozen-lockfile 命令。


Q: pnpm@7搭配husky@8后commit一直失败怎么办?


这是因为hooks出问题了。某些代码库里会在commit时候会添加一些hook用来处理commit相关的事务,比如生成commit-id之类的。


husky@8后需要处理一下这个:

husky add .husky/commit-msg 'sh .git/hooks/commit-msg "$@"'

手动把之前.git/hooks下的脚本拷贝到.husky下。


友情提示:.git和.husky一般都是在项目根目录下的隐藏文件夹喲~


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

MP4 是不是该退休了?

web
背景 对于视频的在线播放,根据视频内容的传输模式可以分为点播和直播,分别用于预先录制内容的传输和实时传输,比如新闻报道、体育赛事都属于直播场景,电影、电视剧、课程视频都属于点播场景。 在 2000 年代初期,Flash 技术开始在 Web 上流行起来,它成为在...
继续阅读 »

背景


对于视频的在线播放,根据视频内容的传输模式可以分为点播直播,分别用于预先录制内容的传输和实时传输,比如新闻报道、体育赛事都属于直播场景,电影、电视剧、课程视频都属于点播场景。


在 2000 年代初期,Flash 技术开始在 Web 上流行起来,它成为在网页上展示视频的主要选择,因为当时没有其他方式能够在浏览器上流式的传输视频。


image.png


随着 HTML5 技术的逐渐成熟,HTML5 的 video 标签开始允许在没有 Flash 插件的情况下在浏览器中直接播放视频。


视频的在线播放主要的技术环节在于视频的解码、显示效率以及数据的传输效率,而 HTML5 的 video 标签将这两个环节进行解藕,开发人员不需要关心视频数据的解码、显示,只需要关心如何去优化数据的获取。


在点播的场景下,因为视频数据已经提前准备好,开发人员只需要制定 video 标签的 src 属性为对应的视频资源地址即可,但是在一些复杂的场景下比如需要根据网络状况做自适应码率、需要优化视频的首屏时间等,那么则需要对视频的一些规格参数以及相关的技术点做进一步的了解。


视频基础


视频帧率


视频的播放原理类似于幻灯片的快速切换。


image.png
每一次画面的切换称作为一帧,而帧率表示每秒切换的帧数,单位数 FPS,人类对画面切换频率感知度是有一个范围的,一般 60 FPS 左右是一个比较合适的范围,但这也需要结合具体的场景,比如在捕捉一个事物快速变化的瞬间时,需要准备足够的帧数才能捕捉到细微的变化,但是当需要拍摄一个缓慢的镜头效果时,帧率不需要太高。


帧率除了要考虑不通场景的播放内容,还需要结合播放设备的刷新频率,如果设备的刷新频率过低,多余的帧就会被丢弃。


视频分辨率


视频在播放时,显示在屏幕中的每一帧中的像素点数量都是相同的,像素是显示设备上发光原件的最小单位,最终呈现的画面是由若干个像素组合起来所展示的。


视频的分辨率是指视频每一帧画面的像素数量,通常以水平方向像素数量 x 垂直高度像素数量的形式表示。分辨率决定了图像的清晰度和细节程度,常见的分辨率有 1080P = 1920 * 1080,这是标准的纯高清分辨率,p 表示是逐行扫描的,与之对应的是 i 表示的是隔行扫描。


image.png


左边一列是逐行扫描,中间一列是隔行扫描,在隔行扫描中会丢失一些页面信息从而加快页面信息的收集。


当设备的分辨率高于视频的分辨率时,设备上的像素点就会多于视频显示所需的像素点,这时就会使用补间算法来为设备上那些未被利用的像素点生成色值信息,否则将导致屏幕上出现黑点,此时人从感官上就会觉得清晰度有所下降。如果视频的分辨率高于设备的分辨率时,则视频多出的信息会被丢弃。


视频格式


视频格式是一种特定的文件格式,用于存储和传输视频数据,它包含了视频图像、音频、字幕和其他相关媒体数据的编码信息,不同的视频格式采用不同的压缩算法和编码方式,以便在存储和传输的过程中有效的减少文件大小并保持高质量的图像。


常见的视频格式有 MP4、AVI、MOV 等,每种视频格式都有其特定的优势和使用场景,比如 MOV 在 Mac 系统上有很好的兼容性,适用于视频编辑。


MP4视频结构


MP4 文件由许多 Box 数据块组成,每个 Box 可以嵌套包含其他 Box,一级嵌套一级来存放媒体信息,这种层次化的结构使得 MP4 文件能够组织和存储各种不同类型的媒体数据和元数据,使其在播放和传输过程中具有灵活性和可扩展性。


image.png


虽然 Box 的类型非常多,但是并不是都是必须的,一般的 MP4 文件都是含有必须的 Box 和个别非必须 Box,下面使用 MP4Box.js 查看 MP4 的具体结构并介绍几个必须 Box:


image.png




  • ftyp


    File Type Box,一般在文件的开始位置,描述的文件的版本、兼容协议等。




  • mdat


    Media Data Box,媒体数据内容,是实际的视频的内容存储区域。该区域通常占整个文件99%+大小。


    image.png




  • moov


    MP4 的媒体数据信息主要存放在 Moov Box 中,是我们需要分析的重点。moov 的主要组成部分如下:




    • mvhd


      Movie Header Box,记录整个媒体文件的描述信息,如创建时间、修改时间、时间度量标尺、可播放时长等。


      image.png




    • udta


      保存自定义数据




    • track


      对于媒体数据来说,track 表示一个视频或音频序列,trak 区域至少存在一个,大部分情况是两个(音频和视频)。


      image.png






形象点来说,moov 可以比如成是整个视频的目录,想要播放视频的话,必须要先加载 moov 区域拿到视频文件目录才能播放视频内容。


为什么MP4视频首屏慢?


当我们在浏览器中打开一个 MP4 视频文件时,浏览器根据就会开始获取视频信息,下载视频 chunk,开始播放视频,通过抓包能够大致了解浏览加载视频过程:


image.png


从请求列表中可知,浏览器发送了三个请求,总耗时 55s ,该视频文件的 box 结构如下:


image.png


下面来具体看一下这三个请求:




  • 第一次请求


    image.png


    浏览器第一次请求时尝试通过 HTTP range request(范围请求)下载整个视频,但是实际只下载了 135 KB 整个请求就完成了,来分析一下具体流程:



    • 浏览器通过 Range: bytes= 0- 首先获取到了 ftyp 信息,这里 ftyp-box 大小为 32 字节;

    • 接下来继续尝试查找 free-box 区域,如果没有就跳过,这里 free-box 大小为 8 字节;

    • 接着尝试查找下一个区域(moov 或 mdat),结果不幸匹配到的区域是 mdat 区域,这时浏览器就会主动终止请求,尝试从尾部查找视频的 moov 区域,因为上面我们讲过 moov 作为视频文件的目录,在播放视频数据前必须先获取 moov 数据,紧接着开始了第二次请求。




  • 第二次请求


    在第一次请求中已经知道了整个视频文件的大小了,如何去确定请求的范围呢?由于 MP4 是由 Box 组成的,标准的 Box 开头的4个字节(32位)为这个 Box 的大小,该大小包括 Box Header 和 Box Body,这样浏览器在第一次请求后就可以确定文件中剩下未解析到的 Box 的开始的 Range 值了。


    image.png


    计算过程(单位字节):
    moov 大小 = 视频文件大小 - ftyp大小 - free大小 - mdat大小 = 22251375 - 32 - 8 - 22224468 = 26867。


    也就是说这一次请求的 range 的开始值最大值不能高于 22251375-26867 = 22251415。


    image.png


    可以看到发出去的请求 Range: bytes=22251374-∞ ,上面计算的 22251415-∞ 包含在内 ,请求到数据后,接下来就是解析 moov-box了,然后根据视频”目录“发起第三次请求。




  • 第三次请求


    根据第二次请求的 moov 解析后,开始下载”真正“的视频的内容准备播放,在第三次请求中,浏览器必须要缓存 4MB 左右才开始播放,




原因分析




  • 过多的数据请求。


    由于 MP4 文件的特殊性,浏览器必须先将 ftyp 、moov 等资源加载完毕之后才能去播放视频,而浏览器是从头部开始依次去加载这些资源,一旦视频资源存放顺序不对,浏览器会发送多次请求分别加载对应的资源。




  • 全量解析 moov


    播放 Mp4 音视频数据前需要先加载并解析 moov 数据,moov 的大小和视频长度成正比,更坏的情况是如果此时服务器没有配置 HTTP range request,浏览器无法跳过查找 moov 这一步,以至于需要下载整个文件。




如何借助HLS 优化视频播放的?


什么是HLS?


HLS 全称是 HTTP Live Streaming,是一个由 Apple 公司提出的基于 http 的媒体流传输协议,用于实时音视频流的传输,HLS 最初是为苹果设备和平台(如iOS和macOS)设计的,但如今已被广泛应用于各种平台和设备上,成为流媒体传输的主要标准之一。


HLS 协议由三部分组成:http、m3u8、ts,这三部分中,http 是传输协议,m3u8 是索引文件,ts是音视频的媒体信息。


HLS的优势和特点是什么?




  • 分段传输


    HLS 将整个音频或视频流切分成短的分段,通常每个分段持续几秒钟,这种分段的方式使得视频内容可以逐段加载和播放,从而提供更好的适应性和流畅性。




  • 基于HTTP协议


    HLS 使用 http 协议进行数据传输,这意味着它能够在标准的 http 服务器上运行,不需要专门的流媒体服务器。




  • 自适应码率


    HLS 支持自适应码率,根据网络带宽和设备性能,动态地选择合适的分辨率和比特率,以提供更好的观看体验。




  • 多码率支持


    媒体源可以同时提供不同分辨率和比特率的视频流,使得用户可以根据网络状况选择合适的码率。




  • 兼容性好


    由于 HLS 使用标准的 HTTP 协议,它在各种设备和平台上具有很好的兼容性,包括苹果设备、Android 设备、PC、智能电视等。在使用 http 播放 MP4 视频时,需要代理服务器支持 http range request 以获取视频的某一部分,但不是所有的代理服务器都对此有良好的支持,而 HLS 不需要,它对代理服务器的要求小很多。




HLS为什么首屏比MP4快?


上面讲过如果要播放 MP4 需要等待整个 moov box 加载完成,这个过程比较消耗时间和带宽,而在 HLS 协议中,分段传输是一个非常重要的特性,HLS 将整个音视频流切分成多个小的分段(ts 文件),这些分段可以被独立的下载和播放。


具体来说,HLS 的工作流程如下:




  • 切分分段:


    原始的音视频流被切分成短小的分段( .ts 文件),每个分段都包含了一小段时间范围内的音视频数据。




  • m3u8 文件:


    服务器生成一个 .m3u8 文件,它是一个播放列表,包含了所有分段的信息,如地址、时长等。播放器通过请求 .m3u8 文件来获取分段列表。




  • 分段请求:


    播放器根据 .m3u8 文件中的分段信息,逐个请求并加载 .ts 分段。




  • 逐段播放:


    播放器逐个播放已经加载的分段,实现连续的音视频播放。




因此,HLS 首屏播放的实现方式不需要像 MP4 那样等待整个文件的基本信息加载完成,而是通过分段传输的方式逐段加载和播放。这使得首屏播放更快速和响应,同时也为流媒体的适应性提供了更好的支持。


为什么选择TS格式文件?


TS(Transport Stream,传输流)是一种封装的格式,它的全称为 MPEG2-TS,主要应用于数字广播系统,譬如 DVB、ATSC 与 IPTV,传输流最初是为广播而设计的,后来通过在标准的188字节数据包中添加4字节的时间码(TC),从而使该数据包成为192字节的数据包,使其适用于数码摄像机,录像机和播放器。


TS(Transport Stream)流在流媒体领域具有多种优点,使得它成为广泛应用于数字电视、流媒体、广播等领域的传输格式之一。以下是TS流的一些优点:




  • 分段传输:


    TS 流将媒体数据切分成小的分段(Packet),每个分段通常持续数毫秒至几十毫秒。这种分段传输使得数据能够按需传输和加载,从而实现快速启动播放和逐段加载,提高了用户体验。




  • 容错性强:


    每个 TS 分段都具有自己的包头信息和校验机制,这使得 TS 流具有较强的容错性。即使在传输过程中发生丢包或错误,也只会影响某个分段,不会影响整个媒体流的播放。




  • 多路复用:


    多路复用的目的一般为了在一个文件流中能同时存储视频、音频、字幕等内容,而TS 流就支持将多个音视频流混合在一个文件中,每个流都有自己的 PID(Packet Identifier)。这使得 TS 流适用于同时传输多个媒体流的场景,如电视广播、有线电视等,提高了传输效率。




  • 支持多种编码格式:


    TS 流可以支持多种音视频编码格式,如H.264、H.265、AAC、MP3等,使其能够适应各种类型的媒体内容。




M3U8格式文件的构成


以下为一个 m3u8 格式文件内容的示例:


#EXTM3U
#EXT-X-VERSION:6
#EXT-X-KEY:METHOD=AES-128,URI="<https://xxxx?token=xxx>"
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:19
#EXTINF:12.000,
<https://xxxx/test/1.ts>
#EXTINF:7.500,
<https://xxxx/test/2.ts>
#EXTINF:13.000,
<https://xxxx/test/3.ts>
#EXTINF:9.720,
<https://xxxx/test/4.ts>
#EXT-X-ENDLIST

在以上示例中包含了 m3u8 文件常见的字段下面为一个M3U8文件可包含的基本字段及含义解释:



  • #EXTM3U 表明该文件是一个 m3u8 文件。每个 M3U8 文件必须将该标签放置在第一行。

  • #EXT-X-VERSION 指定 M3U8 版本号。

  • #EXT-X-KEY 媒体片段可以进行加密,而该标签可以指定解密方法。例如在上面的示例中,该字段指定了加密算法为 AES-128,密钥通过请求 https:xxxx?token=xxx 获取,以用于解密后续下载的 ts 文件。

  • EXT-X-MEDIA-SEQUENCE: 第一个 TS 分片的序列号。每个 TS 分片都拥有一个唯一的整型序列号,每个 TS 分片序列号按出现顺序依次加 1,如果该分片未指定则默认序列号从 0 开始。对于视频点播资源该字段一般是 0,但是在直播场景下,这个序列号标识直播段的起始位置。

  • #EXT-X-TARGETDURATION: 每个 TS 分片的最大的时长,单位为秒。

  • #EXT-X-DISCONTINUITY: 该标签表明其前一个切片与下一个切片之间存在中断。

  • #EXT-X-PLAYLIST-TYPE: 指定流媒体类型。

  • #EXT-X-ENDLIST: M3
    作者:西陵
    来源:juejin.cn/post/7268658252567691322
    U8 文件结束符。

收起阅读 »

为什么WebSocket需要前端心跳检测,有没有原生的检测机制?

web
本文代码 github、gitee、npm 在web应用中,WebSocket是很常用的技术。通过浏览器的WebSocket构造函数就可以建立一个WebSocket连接。但当需要应用在具体项目中时,几乎都会进行心跳检测。 设置心跳检测,一是让通讯双方确认对方...
继续阅读 »

本文代码 githubgiteenpm



在web应用中,WebSocket是很常用的技术。通过浏览器的WebSocket构造函数就可以建立一个WebSocket连接。但当需要应用在具体项目中时,几乎都会进行心跳检测。


设置心跳检测,一是让通讯双方确认对方依旧活跃,二是浏览器端及时检测当前网络线路可用性,保证消息推送的及时性。


你可能会想,WebSocket那么简陋的吗,居然不能自己判断连接状态?在了解前先来回顾一下计算机网络知识。


相关的网络知识


TCP/IP协议族四层结构:




  • 应用层:决定了向用户提供应用服务时通信的活动。HTTP、FTP、WebSocket都在该层




  • (TCP)传输控制层:控制网络中两台主机的数据传输:将应用层数据(有必要时对应用层报文分段,例如一个完整的HTTP报文进行分段)发送到目标主机的特定端口的应用程序。给每个数据标记源端口、目标端口、分段后的序号。




  • (IP)网络层:将IP地址映射为目标主机的MAC地址,然后将TCP数据包(有必要时对数据分片)加入源IP、目标IP等信息后经过链路层扔到网络上让其找到目标主机。




  • 链路层:为IP网络层进行发送、接收数据报。将二进制数据包与在网线传输的网络电信号进行相互转换。





TCP是可靠的连接,握手建立连接后,发送方每发送一个TCP报文(对应用层报文分段后形成多个TCP报文),都会期望对方在指定时间里返回已收到的确认消息,如果超时没有回应,会重复发送,确保所有TCP报文可以到达对方,被对方按顺序拼接成应用层需要的完整报文。


WebSocket协议支持在TCP 上层引入 TLS 层,建立加密通信。



WebSocket与HTTP的异同:




  • WebSocket和HTTP一样是应用层协议,在传输层使用了TCP协议,都是可靠的连接。WebSocket在建立连接时,可以使用已有的HTTP的GET请求进行握手:客户端在请求头中将WebSocket协议版本等信息发生到服务器,服务器同意的话,会响应一个101的状态码。就是说一次HTTP请求和响应,即可轻松转换协议到WebSocket。




  • WebSocket可以互相发起请求。当有新消息时,服务器主动通知客户端,无需客户端主动向服务器询问。客户端也可以向后端发送消息。而HTTP中请求只能由客户端发起。




  • WebSocket是HTML5的内容,HTTP则是超文本传输协议,比HTML5诞生更早。




  • 在应用层,WebSocket的每个报文(在WebSocket中叫数据帧)会比HTTP报文(必须包含请求行、请求头、请求数据)更轻量。



    • WebSocket每个数据帧只有固定、轻量的头信息,不会有cookie等或者自定义的头信息。并且建立通讯后是一对一的,不需要携带验证信息。但握手时的HTTP请求会自动携带cookie。

    • WebSocket在应用层就会将大的数据分拆到多个数据帧,而HTTP不会拆分每个报文。




WebSocket与与WebRTC的异同:



  • WebRTC是一种通讯技术,由谷歌发起,被广大浏览器实现。用来建立浏览器和浏览器间的通讯,如视频通话等。而WebSocket是一种经过抽象的协议,可以实现为通讯技术。用来建立浏览器和服务器间的通讯。


协议中的心跳检测机制


从网上检索的答案,WebSocket大概有两种从协议角度出发的,检测对方存活的方式:




  1. WebSocket只是一个应用层协议规范,其传输层是TCP,而TCP为长连接提供KeepAlive机制,可以定时发送心跳报文确认对方的存活,但一般是服务器端使用。因为是TCP传输控制层的机制,具体的实现要看操作系统,也就是说应用层接收到的连接状态是操作系统通知的,不同操作系统的资源调度是不一样的,例如何时发送探测报文(不包含有效数据的TCP报文)检测对方的存活,频率是多久,在不同的系统配置下存在差异。可能是2小时进行一次心跳检测,或许更短。如果连续没有收到对方的应答包,才会通知应用层已经断开连接。这就带来了不确定性。同时也意味着其它依赖该机制的应用层协议也会被影响。也就是说要利用这个过程进行检测,客户端要修改操作系统的TCP配置才行,在浏览器环境显然不行。




  2. WebSocket协议也有自身的保活机制,但需要通讯双方的实现。WebSocket通讯的数据帧会有一个4位的OPCODE,标记当前传输的数据帧类型,例如:0x8表示关闭帧、0x9表示ping帧、0xA表示pong帧、0x1普通文本数据帧等。http://www.rfc-editor.org



    • 关闭数据帧,在任意一方要关闭通道时,发送给对方。例如浏览器的WebSocket实例调用close时,就会发送一个OPCODE为连接关闭的数据帧给服务器端,服务器端接收到后同样需要返回一个关闭数据帧,然后关闭底层的TCP连接。

    • ping数据帧,用于发送方询问对方是否存活,也就是心跳检测包。目前只有后端可以控制ping数据帧的发送。但浏览器端的WebSocket实例上没有对应的api可用。

    • pong数据帧,当WebSocket通讯一方接收到对方发送的ping数据帧后,需要及时回复一个内容一致,且OPCODE标记为pong的数据帧,告诉对方我还在。但目前回复pong是浏览器的自动行为,意味着不同浏览器会有差异。而且在js中没有相关api可以控制。




综上所述,探测对方存活的方式都是服务器主动进行心跳检测。浏览器并没有提供相关能力。为了能够在浏览器端实时探测后端的存活,或者说连接依旧可用,只能自己实现心跳检测。


浏览器端心跳检测的必要性


首先我们先了解一下,目前的浏览器端的WebSocket何时会自动关闭WebSocket,并触发close事件呢?



  • 握手时的WebSocket地址不可用。

  • 其它未知错误。

  • 正常连接状态下,接收到服务器端的关闭帧就会触发关闭回调。


也就是说建立正常连接后,中途浏览器端断网了,或者服务器没有发送关闭帧就关了连接,总之就是在连接无法再使用的情况下,浏览器没有接收到关闭帧,浏览器则会长时间保持连接状态。此时业务代码不去主动探测的话,是无法感知的。


另外通讯双方保持连接意味着需要长时间占用对方的资源。对于服务器端来说资源是非常宝贵的。长时间不活跃的连接,可能会被服务器应用层框架"优化"释放掉。


前端实现心跳检测


实例化一个WebSocket:


function connectWS() {
const WS = new WebSocket("ws://127.0.0.1:7070/ws/?name=greaclar");
// WebSocket实例上的事件

// 当连接成功打开
WS.addEventListener('open', () => {
console.log('ws连接成功');
});
// 监听后端的推送消息
WS.addEventListener('message', (event) => {
console.log('ws收到消息', event.data);
});
// 监听后端的关闭消息,如果发送意外错误,这里也会触发
WS.addEventListener('close', () => {
console.log('ws连接关闭');
});
// 监听WS的意外错误消息
WS.addEventListener('error', (error) => {
console.log('ws出错', error);
});
return WS;
}

let WS = connectWS();

心跳检测需要用到的实例方法:


// 发送消息,用来发送心跳包
WS.send('hello');
// 关闭连接,当发送心跳包不响应,需要重连时,最好先关闭
WS.close();

定义发送心跳包的逻辑:


准备



  • 申请一个变量heartbeatStatus,记录当前心跳检测状态,有三个状态:等待中,已收到应答、超时。

  • 监听WS实例的message事件,监听到就将heartbeatStatus改为:已收到应答。

  • 监听WS实例的open事件,打开后启动心跳检测。


检测




  • 启动一个定时器A。




  • 定时器A执行,1.修改当前状态heartbeatStatus为等待中;2.发送心跳包;3.启动一个定时器B。



    • 发送心跳包后,后端需要立刻推送一个内容一样的心跳应答包给前端,触发前端WS实例的message事件,继而将heartbeatStatus改为已收到应答。




  • 定时器B执行,检测当前heartbeatStatus状态:




    • 如果是已收到应答,证明定时器A执行后,服务器可以及时响应数据。继续启动定时器A,然后不断循环。




    • 如果是等待中,证明连接出现问题了,走关闭或者检测流程。






let WS = connectWS();
let heartbeatStatus = 'waiting';

WS.addEventListener('open', () => {
// 启动成功后开启心跳检测
startHeartbeat()
})

WS.addEventListener('message', (event) => {
const { data } = event;
console.log('心跳应答了,要把状态改为已收到应答', data);
if (data === '"heartbeat"') {
heartbeatStatus = 'received';
}
})

function startHeartbeat() {
setTimeout(() => {
// 将状态改为等待应答,并发送心跳包
heartbeatStatus = 'waiting';
WS.send('heartbeat');
// 启动定时任务来检测刚才服务器有没有应答
waitHeartbeat();
}, 1500)
}

function waitHeartbeat() {
setTimeout(() => {
console.log('检测服务器有没有应答过心跳包,当前状态', heartbeatStatus);
if (heartbeatStatus === 'waiting') {
// 心跳应答超时
WS.close();
} else {
// 启动下一轮心跳检测
startHeartbeat();
}
}, 1500)
}

优化心跳检测


心跳检测异常,但close事件没有触发,大概率是双方之间的网络线路不佳,如果立马进行重连,会挤兑更多的网络资源,重连的失败概率更大,也可能阻塞用户的其它操作。


但也不排除确实是连接的问题,如服务器宕机、意外重启,同时没有告知浏览器需要把旧连接关闭。


所以一发生心跳不应答,个人推荐的做法是,发生延迟后,提醒用户网络异常正在修复中,让用户有个心理准备。然后多发一两个心跳包,连续不应答再提示用户掉线了,是否重连。如果中途正常了,就不需要重连,用户体验更好,对服务器的压力也更小。


// 以上代码需要修改的地方

// 添加一个变量来记录连续不应答次数
let retryCount = 0

WS.addEventListener('message', (event) => {
const { data } = event;
console.log('心跳应答了,要把状态改为已收到应答', data);
if (data === '"heartbeat"') {
// 复位连续不应答次数
retryCount = 0
heartbeatStatus = 'received';
}
})

// 在等待应答的函数中添加重试的逻辑
function waitHeartbeat() {
setTimeout(() => {
// 心跳应答正常,启动下一轮心跳检测
if (heartbeatStatus === 'received') {
return startHeartbeat();
}
// 更新超时次数
retryCount ++;
// 心跳应答超时,但没有连续超过三次
if (retryCount < 3) {
alert('ws线路异常,正在检测中。')
return startHeartbeat();
}

// 超时次数超过三次
WS.close();
}, 1500)
}

最后,为了方便大家共同进步,本文已经把相关的逻辑封装为一个类,并且在npm中可下载玩一

作者:小龟壳阿特greaclar
来源:juejin.cn/post/7268864806558515237
下,也已经开源到github上。

收起阅读 »

如何不花钱也能拥有一个属于自己的在线网站、博客🤩🤩🤩

作为一个资深的切图仔,我们难免会享有一个自己的博客,去分享一些文章啊或者一些学习笔记,那么这时候可能就需要一台服务器去把我们的项目部署到网上了,但是考虑到服务器价格昂贵,也并没有必要去花这么多钱搞这么一个东西。那么 Github pages 就为我们解决了这么...
继续阅读 »

作为一个资深的切图仔,我们难免会享有一个自己的博客,去分享一些文章啊或者一些学习笔记,那么这时候可能就需要一台服务器去把我们的项目部署到网上了,但是考虑到服务器价格昂贵,也并没有必要去花这么多钱搞这么一个东西。那么 Github pages 就为我们解决了这么一个烦恼,他能让我们不花钱也能拥有自己的在线网站。


什么是 GitHub Pages?


GitHub Pages 是 GitHub 提供的一个托管静态网站的服务。它允许用户将自己的代码仓库转化为一个在线可访问的网站,无需复杂的服务器设置或额外的托管费用。通过 GitHub Pages,我们可以轻松地创建个人网站、项目文档、博客或演示页面,并与其他开发者和用户分享自己的作品。


使用


要想使用这个叼功能,我们首先要再 Gayhub 上面建立一个仓库,如下图所示:




紧接着我们使用 create-neat 来创建一个项目,执行以下命令:

npx create-neat mmm

跟着提示选择相对应的项目即可,选择 vue 或者 react 都可以。




当项目创建成功之后我们进入到该目录并安装相关依赖包:

pnpm add gh-pages --save-dev

并在 package.json 文件中添加 homepage 字段,如下所示:

"homepage": "http://xun082.github.io/mmm"

其中 xun082 要替换为你自己 github 上面的用户名,如下图所示: 



而 mmm 替换为我们刚才创建的仓库名称。


接下来在 package.json 文件中 script 字段中添加如下属性:

  "scripts": {
"start": "candy-script start",
"build": "candy-script build",

"deploy": "gh-pages -d dist"
},

完整配置如下所示:




完成配置后我们将代码先提交到仓库中,如下命令所示:

git add .

git commit -m "first commit"

git branch -M main

git remote add origin https://github.com/xun082/mmm.git

git push -u origin main

这个时候我们的本地项目已经和远程 GayHub 仓库关联起来了,那么我们这个时候可以执行如下命令:

pnpm run build

首先执行该命令对我们的项目进行打包构建。打包完成之后会生成如下文件,请看下图:




接下来我们可以使用 gh-pages 将项目发布到网上面了

pnpm run deploy

使用该命令进行打包并且部署到网上,这个过程可能需要一点时间,可以在工位上打开手机开把王者了。
当在终端里出现 published 字段就说明我们部署成功了:




这个时候,访问我们刚才在 package.json 文件中定义的 homepage 字段中的链接去访问就可以正常显示啦!




总结


通过该方法我们可以不用花钱,也能部署一个属于自己的网站,如果觉得不错那就赶紧用起来吧!


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

实现一个简易的热🥵🥵更新

简单模拟一个热更新 什么是热更新 热更新是指在应用程序运行时,对程序的部分内容进行更新,而无需重启整个应用程序。通常,热更新是在不停止整个应用程序的情况下,将新的代码、资源或配置应用于正在运行的应用程序,以实现功能修复、性能优化或新增功能等目的。 热更新的...
继续阅读 »

简单模拟一个热更新


什么是热更新



热更新是指在应用程序运行时,对程序的部分内容进行更新,而无需重启整个应用程序。通常,热更新是在不停止整个应用程序的情况下,将新的代码、资源或配置应用于正在运行的应用程序,以实现功能修复、性能优化或新增功能等目的。



热更新的优点


实时反馈,提高开发效率:热更新允许开发人员在进行代码更改后立即看到结果,无需重新编译和重启应用程序。这大大减少了开发、测试和调试代码所需的时间,提高了开发效率。


保持应用程序状态:通过热更新,应用程序的状态可以在代码更改时得以保留。这意味着开发人员不需要在每次更改后重新导航到特定页面或重现特定的应用程序状态,从而节省了时间和精力。


webpack 中的热更新



在Webpack中,热更新(Hot Module Replacement,HMR)是一种通过在应用程序运行时替换代码的技术,而无需重新加载整个页面或应用程序。它提供了一种高效的开发方式,可以在开发过程中快速看到代码更改的效果,而不中断应用程序的状态。



原理如下:


客户端连接到开发服务器:当启动Webpack开发服务器时,它会在浏览器中创建一个WebSocket连接。


打包和构建模块:在开发模式下,Webpack会监视源代码文件的变化,并在代码更改时重新打包和构建模块。这些模块构成了应用程序的各个部分。


将更新的模块发送到浏览器:一旦Webpack重新构建了模块,它会将更新的模块代码通过WebSocket发送到浏览器端。


浏览器接收并处理更新的模块:浏览器接收到来自Webpack的更新模块代码后,会根据模块的标识进行处理。它使用模块热替换运行时(Hot Module Replacement Runtime)来执行以下操作:(个人理解就像是 AJAX 局部刷新的过程,不对请指出)


(1)找到被替换的模块并卸载它。


(2)下载新的模块代码,并对其进行注入和执行。


(3)重新渲染或更新应用程序的相关部分。


保持应用程序状态:热更新通过在模块替换时保持应用程序状态来确保用户不会失去当前的状态。这意味着在代码更改后,开发人员可以继续与应用程序进行交互,而无需手动重新导航到特定页面或重现特定状态。


代码模拟



在同一个目录下创建 server.js 和 watcher.js



server.js

const http = require("http");
const server = http.createServer((req, res) => {
res.statusCode = 200;
// 设置字符编码为 UTF-8,若有中文也不乱码
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("offer get!!!");
});

server.listen(7777, () => {
console.log("服务已启动在 7777 端口");
process.send("started");
});

// 监听来自 watcher.js 的消息
process.on("message", (message) => {
if (message === "refresh") {
// 重新加载资源或执行其他刷新操作
console.log("重新加载资源");
}
});


(1)使用 http.createServer 方法创建一个 HTTP 服务器实例,该服务器将监听来自客户端的请求。


(2)启动服务,当服务器成功启动并开始监听指定端口时,回调函数将被调用。


(3)通过调用 process.send 方法,我们向父进程发送一条消息,消息内容为字符串 "started",表示服务器已经启动。


(4)使用 process.on 方法监听来自父进程的消息。当收到名为 "refresh" 的消息时,回调函数将被触发。


(5)在回调函数中,我们可以执行资源重新加载或其他刷新操作。一般是做页面的刷新操作。


server.js 创建了一个简单的 HTTP 服务器。当有请求到达时,服务器会返回 "offer get!!!" 的文本响应。它还与父进程进行通信,当收到名为 "refresh" 的消息时,会执行资源重新加载操作。


watcher.js

const fs = require("fs");
const { fork } = require("child_process");

let childProcess = null;

const watchFile = (filePath, callback) => {
fs.watch(filePath, (event) => {
if (event === "change") {
console.log("文件已经被修改,重新加载");

// 如果之前的子进程存在,终止该子进程
childProcess && childProcess.kill();

// 创建新的子进程
childProcess = fork(filePath);
childProcess.on("message", callback);
}
});
};

const startServer = (filePath) => {
// 创建一个子进程,启动服务器
childProcess = fork(filePath);
childProcess.on("message", () => {
console.log("服务已启动!");
// 监听文件变化
watchFile(filePath, () => {
console.log("文件已被修改");
});
});
};

// 注意文件的相对位置
startServer("./server.js");


watcher.js 是一个用于监视文件变化并自动重新启动服务器的脚本。


(1)首先,引入 fs 模块和 child_processfork 方法,fork 方法是 Node.js 中 child_process 模块提供的一个函数,用于创建一个新的子进程,通过调用 fork 方法,可以在主进程中创建一个全新的子进程,该子进程可以独立地执行某个脚本文件。


(2)watchFile 函数用于监视指定文件的变化。当文件发生变化时,会触发回调函数。


(3)startServer 函数负责启动服务器。它会创建一个子进程,并以指定的文件作为脚本运行。子进程将监听来自 server.js 的消息。


(4)在 startServer 函数中,首先创建了一个子进程来启动服务器并发送一个消息表示服务器已启动。


(5)然后,通过调用 watchFile 函数,开始监听指定的文件变化。当文件发生变化时,会触发回调函数。


(6)回调函数中,会终止之前的子进程(如果存在),然后创建一个新的子进程,重新运行 server.js


watcher.js 负责监视特定文件的变化,并在文件发生变化时自动重新启动服务器。它使用 child_process 模块创建子进程来运行 server.js,并在需要重新启动服务器时终止旧的子进程并创建新的子进程。这样就可以实现服务器的热更新,当代码发生变化时,无需手动重启服务器,watcher.js 将自动处理这个过程。


效果图


打开本地的 7777 端口,看到了 'offet get!!!' 的初始字样




当修改了响应文字时,刷新页面,无需重启服务,也能看到更新的字样!


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

JavaScript中return await究竟有无用武之地?

web
我先回答:有的,参考文章末尾。 有没有区别?  先上一个Demo,看看async函数中return时加和不加await有没有区别: function bar() { return Promise.resolve('this from bar().'); ...
继续阅读 »

我先回答:有的,参考文章末尾。



有没有区别?


 先上一个Demo,看看async函数中return时加和不加await有没有区别:


function bar() {
return Promise.resolve('this from bar().');
}

async function foo1() {
return await bar(); // CASE#1 with await
}

async function foo2() {
return bar(); // CASE#2 without await
}

// main
(() => {
foo1().then((res) => {
console.log('foo1:', res); // res is string: 'this from bar().'
})
foo2().then((res) => {
console.log('foo2:', res); // res is string: 'this from bar().'
})
})();

 可能在一些社区或团队的编程规范中,有明确要求:不允许使用非必要的 return await。给出的原因是这样做对于foo函数而言,会增加等待bar函数返回的Promise出结果的时间(但其实它可以不用等,因为马上就要return了嘛,这个时间应留给foo函数的调用者去等)。


 如果你觉得上面的文字不大通顺,直接看代码,问:以上例子中,foo1()函数和foo2()函数的写法对程序的执行过程有何影响?


 先说结论:async 函数中 return await promise;return promise; 从宏观结果来看是一样的,但微观上有区别。


有什么区别?


 基于上面的Demo改造一下,做个试验:


const TAG = Symbol();
const RESULT = Promise.resolve('return from bar().');
RESULT[TAG] = 'TAG#RESULT';

function bar() {
return RESULT;
}

async function foo1() {
return await bar();
}

async function foo2() {
const foo2Ret = bar();
console.log('foo2Ret(i):', foo2Ret[TAG], foo2Ret === RESULT); // 'TAG#RESULT', true (1)
return foo2Ret; // without await
}

// main
(() => {
const foo1Ret = foo1();
console.log('foo1Ret:', foo1Ret[TAG], foo1Ret === RESULT); // undefined, false (2)
console.log('--------------------------------------------');
const foo2Ret = foo2();
console.log('foo2Ret(o):', foo2Ret[TAG], foo2Ret === RESULT); // undefined, false (3)
})();

 从注释标注的执行结果可以看到:



  • (1)处没有疑问,foo2Ret 本来就是 RESULT

  • (2)处应该也没有疑问,foo1Ret 是基于 RESULT 这个Promise的结果重新包装的一个新的Promise(只是这个Promise的结果和Result是一致的);

  • (3)处应该和常识相悖,竟然和(2)不一样?是的,对于 async 函数不管return啥都会包成Promise,而且不是简单的通过 Pomise.resolve() 包装。


 那么结论就很清晰了,async 函数中 return await promise;return promise; 至少有两个区别:



  1. 对象上的区别:

    • return await promise; 会先把promise的结果解出来,再构造成新的Promise

    • return await promise; 直接在promise的基础上构造Promise,也就是套了两个Promise(两层Promise的状态和结果是一致的)



  2. 时间上的区别:假设 bar() 函数耗时 10s

    • foo1() 中的写法会导致这10s消耗在 foo1() 函数的执行上

    • foo2() 的写法则会让10s的消耗在 foo2() 函数的调用者侧,也就是注释为main的匿名立即函数




 从对象上的区别看,不论怎样async函数都会构造新的Promise对象,有无await都节约不了内存;从时间上来看,总体的等待时长理论上是一样的,怎么写对结果都没啥影响嘛。


 举个不大恰当的例子:你的上司交给你一个重要任务让你完成后发邮件给他,你分析了下后发现任务需要同事A做一部分,遂找他。同事A完成他的部分需要2天。这个时候你有两个做法选择:一、做完自己的部分后等着A出结果,有结果后再发邮件回复上司;二、将自己的部分完成后汇报给上司,并跟和上司说已经告知A:让A等完成他的部分后直接回邮件给上司。


 如果,我是说假如果哈,如果,这个重要任务本来要求必须在12h内完成,但实际耗时了两天严重超标......请问上述例子中哪种做法更容易获取N+1大礼包?


到底怎么写?


 回到代码层,通过上述分析可以知道,一个主要是耗时归属问题,一个是async函数“总是”会返回的那个Promise对象不是由Promise.resolve()简单包装的(因为Promise.resolve(promise) === promise),可以得到两个编码指南:



强调下,async函数不是通过Pomise.resolve()简单包装的,其实进一步思考下也不难理解,因为它要考虑执行有异常的场景,甚至还可能根据不同的Promise状态做一些其他的操作(比如日志输出、埋点统计?我瞎猜的)



// 避免非必要的 return await 影响模块耗时统计的准确性
async function foo() {
return bar();
}

// 除非你要处理执行过程中的异常
async function foo() {
try {
return await bar();
} catch (_) {
return null;
}
}
// 或:
async function foo() {
return bar().catch(() => null);
}

// async 函数中避免对返回值再使用多余的 Pomise 包装
async function bar() {
return 'this is from bar().'; // YES
}
async function bar() {
return Promise.resolve('this is from bar().'); // !!! NO !!!
}

回到标题:JavaScript中return await有无用武之地?


答:有的,当需要消化掉依赖 Promise

作者:Chavin
来源:juejin.cn/post/7268593569781350455
执行中的异常时。

收起阅读 »

吐槽大会,来瞧瞧资深老前端写的垃圾代码

web
阅读提示:网页版带有主题和代码高亮,阅读体验更佳 🍉 忍无可忍,不吐不快。 本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。 知道了什么是烂代码,才能写出好代码。 别说什么代码和人有一个能跑就行的话,玩笑归玩笑...
继续阅读 »

阅读提示:网页版带有主题和代码高亮,阅读体验更佳 🍉



忍无可忍,不吐不快。


本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。


知道了什么是烂代码,才能写出好代码。


别说什么代码和人有一个能跑就行的话,玩笑归玩笑。


人都有菜的时候,写出垃圾代码无可厚非,但是工作几年了还是写垃圾代码,有点说不过去。


我认为的垃圾代码,不是你写不出深度优先、广度优先的算法,也不是你写不出发布订阅、策略模式的 if else


我认为垃圾代码都有一个共性:看了反胃。就是你看了天然的不舒服,看了就头疼的代码,比如排版极丑,没有空格、没有换行、没有缩进。


优秀的代码就是简洁清晰,不能用策略模式优化 if else 没关系,多几行代码影响不大,你只要清晰,就是好代码。


有了差代码、好代码的基本标准,那我们也废话不多说,来盘盘这些资深前端的代码,到底差在哪里。


---------------------------------------------更新------------------------------------------------


集中回答一下评论区的问题:


1、项目是原来某大厂的项目,是当时的外包团队写的,整体的代码是还行的。eslintcommitlint 之类的也是有的,只不过这些都是可以禁用的,作用全靠个人自觉。


2、后来项目被我司收购,原来的人也转为我司员工,去支撑其他项目了。这个项目代码也就换成我们这批新来的维护了。很多代码是三几年前留下的确实没错,谁年轻的时候没写过烂代码,这是可以理解的。只不过同为三年前的代码,为啥有人写的简单清晰,拆分清楚,有的就这么脏乱差呢?


3、文中提到的很多代码其实是最近一年写的,写这些功能的前端都是六七年经验的,当时因为项目团队刚组建,没有前端,抽调这些前端过来支援,写的东西真的非常非常不规范,这是让人难以理解的,他们在原来的项目组(核心产品,前端基建和规范都很厉害)也是这么写代码的吗?


4、关于团队规范,现在的团队是刚成立没多久的,都是些年轻人,领导也不是专业前端,基本属于无前端 leader 的状态,我倒是很想推规范,但是我不在其位,不好去推行这些东西,提了建议,领导是希望你简单写写业务就行(说白了我不是 leader 说啥都没用)。而且我们这些年轻前端大家都默认打开 eslint,自觉性都很高,所以暂时都安于现状,代码 review 做过几次,都没太大的毛病,也就没继续做了。


5、评论区还有人说想看看我写的代码,我前面文章有,我有几个开源项目,感兴趣可以自己去看。项目上的代码因为涉及公司机密,没法展示。


6、本文就是篇吐槽,也不针对任何人,要是有人看了不舒服,没错,说的就是你。要是没事,大家看了乐呵乐呵就行。轻喷~


文件命名千奇百怪


同一个功能,都是他写的,三个文件夹三种奇葩命名法,最重要的是第三个,有人能知道它这个文件夹是什么功能吗?这完全不知道写的什么玩意儿,每次我来看代码都得重新理一遍逻辑。


image.png


组件职责不清


还是刚才那个组件,这个 components 文件夹应该是放各个组件的,但是他又在底下放一个 RecordDialog.jsx 作为 components 的根组件,那 components 作为文件名有什么意义呢?


image.png


条件渲染逻辑置于底层


这里其实他写了几个渲染函数,根据客户和需求的不同条件性地渲染不同内容,但是判断条件应该放在函数外,和函数调用放在一起,根据不同条件调用不同渲染函数,而不是函数内就写条件,这里就导致逻辑内置,过于分散,不利于维护。违反了我们常说的高内聚、低耦合的编程理念。


image.png


滥用、乱用 TS


项目是三四年前的项目了,主要是类组件,资深前端们是属于内部借调来支援维护的,发现项目连个 TS 都没有,不像话,赶紧把 TS 配上。配上了,写了个 tsx 后缀,然后全是无类型或者 anyscript。甚至于完全忽视 TSESlint 的代码检查,代码里留下一堆红色报错。忍无可忍,我自己加了类型。


image.png


留下大量无用注释代码和报错代码


感受下这个注释代码量,我每次做需求都删,但是几十个工程,真的删不过来。


image.png


image.png


丑陋的、隐患的、无效的、混乱的 css


丑陋的:没有空格,没有换行,没有缩进


隐患的:大量覆盖组件库原有样式的 css 代码,不写类名实现 namespace


无效的:大量注释的 css 代码,各种写了类名不写样式的垃圾类名


混乱的:从全局、局部、其他组件各种引入 css 文件,导致样式代码过于耦合


image.png


一个文件 6 个槽点


槽点1:代码的空行格式混乱,影响代码阅读


槽点2:空函数,写了函数名不写函数体,但是还调用了!


槽点3:函数参数过多不优化


槽点4:链式调用过长,属性可能存在 undefined 或者 null 的情况,代码容易崩,导致线上白屏


槽点5:双循环嵌套,影响性能,可以替换为 reduce 处理


槽点6:参数、变量、函数的命名随意而不语义化,完全不知道有何作用,且不使用小驼峰命名


image.png


变态的链式取值和赋值


都懒得说了,各位观众自己看吧。


image.png


代码拆分不合理或者不拆分导致代码行数超标


能写出这么多行数的代码的绝对是人才。


尽管说后来维护的人加了一些代码,但是其初始代码最起码也有近 2000 行。


image.png


这是某个功能的数据流文件,使用 Dva 维护本身代码量就比 Redux 少很多了,还是能一个文件写出这么多。导致到现在根本不敢拆出去,没人敢动。


image.png


杂七杂八的无用 js、md、txt 文件


在我治理之前,充斥着非常多的无用的、散乱的 md 文档,只有一个函数却没有调用的 js 文件,还有一堆测试用的 html 文件。


实在受不了干脆建个文件夹放一块,看起来也要舒服多了。


image.png


less、scss 混用


这是最奇葩的。


image.png


特殊变量重命名


这是真大佬,整个项目基建都是他搭的。写的东西也非常牛,bug 很少。但是大佬有一点个人癖好,例如喜欢给 window 重命名为 G。这虽然算不上大缺点,但真心不建议大家这么做,window 是特殊意义变量,还请别重命名。


const G = window;
const doc = G.document;

混乱的 import


规范的 import 顺序,应该是框架、组件等第三方库优先,其次是内部的组件库、包等,然后是一些工具函数,辅助函数等文件,其次再是样式文件。乱七八糟的引入顺序看着都烦,还有这个奇葩的引入方式,直接去 lib 文件夹下引入组件,也是够奇葩了。


总而言之,css 文件一般是最后引入,不能阻塞 js 的优先引入。


image.png


写在最后


就先吐槽这么多吧,这些都是平时开发过程中容易犯的错误。希望大家引以为戒,不然小心被刀。


要想保持一个好的编码习惯,写代码的时候就得时刻告诉自己,你的代码后面是会有人来看的,不想被骂就写干净点。


我觉得什么算法、设计模式都是次要的,代码清晰,数据流向清晰,变量名起好,基本 80% 不会太差。


不过说这么多,成事在人。


不过我写了一篇讲解如何写出简洁清晰代码的文章,我看不仅是一年前端需要学习,六七年的老前端也需要看看。

作者:北岛贰
来源:juejin.cn/post/7265505732158472249

收起阅读 »

实现一个简易的热🥵🥵更新

web
简单模拟一个热更新 什么是热更新 热更新是指在应用程序运行时,对程序的部分内容进行更新,而无需重启整个应用程序。通常,热更新是在不停止整个应用程序的情况下,将新的代码、资源或配置应用于正在运行的应用程序,以实现功能修复、性能优化或新增功能等目的。 热更新的...
继续阅读 »

简单模拟一个热更新


什么是热更新



热更新是指在应用程序运行时,对程序的部分内容进行更新,而无需重启整个应用程序。通常,热更新是在不停止整个应用程序的情况下,将新的代码、资源或配置应用于正在运行的应用程序,以实现功能修复、性能优化或新增功能等目的。



热更新的优点


实时反馈,提高开发效率:热更新允许开发人员在进行代码更改后立即看到结果,无需重新编译和重启应用程序。这大大减少了开发、测试和调试代码所需的时间,提高了开发效率。


保持应用程序状态:通过热更新,应用程序的状态可以在代码更改时得以保留。这意味着开发人员不需要在每次更改后重新导航到特定页面或重现特定的应用程序状态,从而节省了时间和精力。


webpack 中的热更新



在Webpack中,热更新(Hot Module Replacement,HMR)是一种通过在应用程序运行时替换代码的技术,而无需重新加载整个页面或应用程序。它提供了一种高效的开发方式,可以在开发过程中快速看到代码更改的效果,而不中断应用程序的状态。



原理如下:


客户端连接到开发服务器:当启动Webpack开发服务器时,它会在浏览器中创建一个WebSocket连接。


打包和构建模块:在开发模式下,Webpack会监视源代码文件的变化,并在代码更改时重新打包和构建模块。这些模块构成了应用程序的各个部分。


将更新的模块发送到浏览器:一旦Webpack重新构建了模块,它会将更新的模块代码通过WebSocket发送到浏览器端。


浏览器接收并处理更新的模块:浏览器接收到来自Webpack的更新模块代码后,会根据模块的标识进行处理。它使用模块热替换运行时(Hot Module Replacement Runtime)来执行以下操作:(个人理解就像是 AJAX 局部刷新的过程,不对请指出)


(1)找到被替换的模块并卸载它。


(2)下载新的模块代码,并对其进行注入和执行。


(3)重新渲染或更新应用程序的相关部分。


保持应用程序状态:热更新通过在模块替换时保持应用程序状态来确保用户不会失去当前的状态。这意味着在代码更改后,开发人员可以继续与应用程序进行交互,而无需手动重新导航到特定页面或重现特定状态。


代码模拟



在同一个目录下创建 server.js 和 watcher.js



server.js


const http = require("http");
const server = http.createServer((req, res) => {
res.statusCode = 200;
// 设置字符编码为 UTF-8,若有中文也不乱码
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("offer get!!!");
});

server.listen(7777, () => {
console.log("服务已启动在 7777 端口");
process.send("started");
});

// 监听来自 watcher.js 的消息
process.on("message", (message) => {
if (message === "refresh") {
// 重新加载资源或执行其他刷新操作
console.log("重新加载资源");
}
});


(1)使用 http.createServer 方法创建一个 HTTP 服务器实例,该服务器将监听来自客户端的请求。


(2)启动服务,当服务器成功启动并开始监听指定端口时,回调函数将被调用。


(3)通过调用 process.send 方法,我们向父进程发送一条消息,消息内容为字符串 "started",表示服务器已经启动。


(4)使用 process.on 方法监听来自父进程的消息。当收到名为 "refresh" 的消息时,回调函数将被触发。


(5)在回调函数中,我们可以执行资源重新加载或其他刷新操作。一般是做页面的刷新操作。


server.js 创建了一个简单的 HTTP 服务器。当有请求到达时,服务器会返回 "offer get!!!" 的文本响应。它还与父进程进行通信,当收到名为 "refresh" 的消息时,会执行资源重新加载操作。


watcher.js


const fs = require("fs");
const { fork } = require("child_process");

let childProcess = null;

const watchFile = (filePath, callback) => {
fs.watch(filePath, (event) => {
if (event === "change") {
console.log("文件已经被修改,重新加载");

// 如果之前的子进程存在,终止该子进程
childProcess && childProcess.kill();

// 创建新的子进程
childProcess = fork(filePath);
childProcess.on("message", callback);
}
});
};

const startServer = (filePath) => {
// 创建一个子进程,启动服务器
childProcess = fork(filePath);
childProcess.on("message", () => {
console.log("服务已启动!");
// 监听文件变化
watchFile(filePath, () => {
console.log("文件已被修改");
});
});
};

// 注意文件的相对位置
startServer("./server.js");


watcher.js 是一个用于监视文件变化并自动重新启动服务器的脚本。


(1)首先,引入 fs 模块和 child_processfork 方法,fork 方法是 Node.js 中 child_process 模块提供的一个函数,用于创建一个新的子进程,通过调用 fork 方法,可以在主进程中创建一个全新的子进程,该子进程可以独立地执行某个脚本文件。


(2)watchFile 函数用于监视指定文件的变化。当文件发生变化时,会触发回调函数。


(3)startServer 函数负责启动服务器。它会创建一个子进程,并以指定的文件作为脚本运行。子进程将监听来自 server.js 的消息。


(4)在 startServer 函数中,首先创建了一个子进程来启动服务器并发送一个消息表示服务器已启动。


(5)然后,通过调用 watchFile 函数,开始监听指定的文件变化。当文件发生变化时,会触发回调函数。


(6)回调函数中,会终止之前的子进程(如果存在),然后创建一个新的子进程,重新运行 server.js


watcher.js 负责监视特定文件的变化,并在文件发生变化时自动重新启动服务器。它使用 child_process 模块创建子进程来运行 server.js,并在需要重新启动服务器时终止旧的子进程并创建新的子进程。这样就可以实现服务器的热更新,当代码发生变化时,无需手动重启服务器,watcher.js 将自动处理这个过程。


效果图


打开本地的 7777 端口,看到了 'offet get!!!' 的初始字样


image.png


当修改了响应文字时,刷新页面,无需重启服务,也能看到更新的字样!


image.png

收起阅读 »

孤独的游戏少年

本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。 楔子 又是一个闲暇的周末,正读一年级的我坐在床上,把象棋的旗子全部倒了出来,根据...
继续阅读 »

本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。



楔子


又是一个闲暇的周末,正读一年级的我坐在床上,把象棋的旗子全部倒了出来,根据颜色分为红色和绿色阵营,旗子翻盖为建筑,正面为单位,被子和枕头作为地图障碍,双手不断的移动着双方阵营的象棋,脑海中演练着星级争霸的游戏画面,将两枚不同阵营的旗子进行碰撞后,通过我对两枚旗子主观的判断,一方阵营的旗子直接被销毁,进入回收,一方的单位对建筑物全部破坏取得游戏胜利。因为我的父亲酷爱玩这款游戏,年少的我也被其玩法、画面所深深吸引,不过最主要的还是父亲获胜或者玩累后,能幸运的奖励我玩上几把。星际争霸成了我第一个启蒙游戏,那时候怎么样都获胜不了,直到发现了show me the money。因为Blizzard这个英文单词一直在游戏的启动界面一闪一闪,那时候还以为这款游戏的名字叫Blizzard,最后才发现,其实Blizzard是魔鬼的意思。


纸笔乐趣


小学一二年级的时候家里管得严,电视也不给我看,电脑直接上锁,作为家里的独生子女,没有同龄人的陪伴,闲暇时间要不就看《格林童话》、《安徒生童话》、《伊索寓言》,要不就打开这副象棋在那里自娱自乐。



起源



在某一天的音乐课上,老师喉咙不舒服,在教室播放猫和老鼠给我们观看,正当我看的津津有味,前面的同学小张突然转过头来,问我“要不要跟我玩个游戏”。小孩子一般都比较贪玩,我直接拒绝了他,“我想看猫和老鼠”。小张满脸失落转了回去,因为我是个特别怕伤害别人情绪的人,所以不忍心又用铅笔的橡皮端戳了戳他,问他“什么游戏,好玩不”。他顿时也来了精神,滔滔不绝的介绍起了这款游戏。


“游戏的规则非常的简单~~在一张纸上中间画一条分割线,双方有各自的基地,每人基地都有5点血量,双方通过猜拳,获胜的一方可以在自己阵营画一个火柴人,火柴人可以攻击对面的火柴人和基地,基地被破坏则胜利。管你听没听懂,试一把就完了!”



我呆呆的看着他,“这么无聊,赢了又怎样?”。他沉默了一会,好像我说的的确有点道理,突然想到了一个好办法,“谁获得胜利扇对方一巴掌”。我顿时来了兴趣。随着游戏逐渐深入,我们的猜拳速度和行动力也越来越快,最后发现扇巴掌还是太狠,改为扇对方的手背,这节课以双方手背通红结束了。


游戏改良


这个《火柴人对战》小游戏在班里火了一会儿,但很快就又不火了,我玩着玩着也发现没啥意思,总是觉得缺少点什么,但毕竟我也只是个没有吃APTX4869的小学生,想不出什么好点子。时间一晃,受到九年义务教育的政策,我也成功成为了一名初中生。在一节音乐课上,老师想让我们放松放松,给我们班看猫和老鼠,隔壁同桌小王撕了一张笔记本的纸,问我有没有玩过《火柴人对战》游戏,只能说,熟悉的配方,熟悉的味道。


当天晚上回到家,闲来无事,我在想这个《火柴人游戏》是不是可以更有优化,这种形式的游戏是不是可以让玩家更有乐趣。有同学可能会问,你那个年代没东西玩的吗?既然你诚心诚意发问,那我就大发慈悲的告诉你。玩的东西的确很多,但是能光明正大摆在课桌上玩的基本没有,一般有一个比较新鲜的好玩的东西,都会有一群人围了过来,这时候老师会默默站在窗户旁的阴暗角落,见一个收一个。


坐在家里的椅子上,我整理了一下思绪,突然产生了灵感,将《魔兽争霸》《游戏王》这两个游戏产生化学反应,游戏拥有着资源,单位,建筑,单位还有攻击力,生命值,效果,攻击次数。每个玩家每回合通过摇骰子的方式获得随机能源点,能源能够解锁建筑,建筑关联着高级建筑和单位,通过单位进行攻击,直至对方玩家生命值为0,那么如何在白纸上面显示呢?我想到比较好的解决方案,单位的画像虽然名字叫骷髅,但是在纸上面用代号A表示,建筑骷髅之地用代号1表示。我花了几天时间,弄了两个阵营,不死族和冰结界。立刻就拿去跟同桌试玩了一下,虽然游戏很丰富,但是有一个严重的弊端就是玩起来还挺耗费时间的,而且要人工计算单位之间的扣血量,玩家的剩余生命,在纸片上去完成这些操作,拿个橡皮擦来擦去,突然觉得有点蠢,有点尴尬,突然明白,一张白纸的承受能力是有限的。之后,我再也没有把游戏拿出来玩过,但我没有将他遗忘,而是深深埋藏在我的心里。


筑梦


直到大学期间《炉石传说》横空出世,直到《游戏王》上架网易,直到我的项目组完成1.0后迎来空窗期一个月,我再也蚌埠住了,之前一直都对微信小游戏很有兴趣,每天闲着也是闲着,所以我有了做一个微信小游戏的想法。而且,就做一款在十几年前,就已经被我设计好的游戏。


但是我不从来不是一个好学的人,领悟能力也很低,之前一直在看cocos和白鹭引擎学习文档,也很难学习下去,当然也因为工作期间没有这么多精力去学习,所以我什么框架也不会,不会框架,那就用原生。我初步的想法是,抛弃所有花里胡哨的动效,把基础的东西做出来,再作延伸。第一次做游戏,我也十分迷茫,最好的做法肯定是打飞机————研究这个微信项目如何用js原生,做出一个小游戏。



虽然微信小游戏刚出来的时候看过代码,但是也只是一扫而过,而这次带着目标进行细细品味,果然感觉不一样。微信小游戏打飞机这个项目是js原生使用纯gL的模式编写的,主要就是在canvas这个画布上面作展示和用户行为。

  // 触摸事件处理逻辑
touchEventHandler(e) {
e.preventDefault()

const x = e.touches[0].clientX
const y = e.touches[0].clientY

const area = this.gameinfo.btnArea

if (x >= area.startX
&& x <= area.endX
&& y >= area.startY
&& y <= area.endY) this.restart()
}

点击事件我的理解就是用户点击到屏幕的坐标为(x, y),如果想要一个按钮上面做处理逻辑,那么点击的范围就要落在这个按钮的范围内。当我知道如何在canvas上面做点击行为时,我感觉我已经成功了一半,接下来就是编写基础js代码。


首先这个游戏确定的元素分别为,场景,用户,单位,建筑,资源(后面改用能源替代),我先将每个元素封装好一个类,一边慢慢的回忆着之前游戏是如何设计的,一边编程,身心完全沉浸进去,已经很久很久没有试过如此专注的去编写代码。用了大概三天的时间,我把基本类该有的逻辑写完了,大概长这个样子



上面为敌方的单位区域,下方为我方的单位区域,单位用ABCDEFG表示,右侧1/1/1 则是 攻击力/生命值/攻击次数,通过点击最下方的icon弹出创建建筑,然后创建单位,每次的用户操作,都是一个点击。


一开始我设想的游戏名为想象博弈,因为每个单位每个建筑都只有名称,单位长什么样子的就需要玩家自己去脑补了,我只给你一个英文字母,你可以想象成奥特曼,也可以想象成哥斯拉,只要不是妈妈生的就行。



湿了


游戏虽然基本逻辑都写好了,放到整个微信小游戏界别人一定会认为是依托答辩,但我还是觉得这是我的掌上明珠,虽然游戏没有自己的界面,但是它有自己的玩法。好像上天也认可我的努力,但是觉得这个游戏还能更上一层楼,在某个摸鱼的moment,我打开了微信准备和各位朋友畅谈人生理想,发现有位同学发了一幅图,上面有四个格子,是赛博朋克风格的一位篮球运动员。他说这个AI软件生成的图片很逼真,只要把你想要的图片告诉给这个AI软件,就能发你一幅你所描绘的图片。我打开了图片看了看,说实话,质感相当不错,在一系列追问下,我得知这个绘图AI软件名称叫做midjourney



midjourney



我迫不及待的登录上去,询问朋友如何使用后,我用我蹩脚的英格力士迫不及待的试了试,让midjourney帮我画一个能源的icon,不试不要紧,一试便湿了,眼睛留下了感动地泪水,就像一个阴暗的房间打开了一扇窗,一束光猛地照射了进来。




对比我之前在iconfont下载的免费图标,midjourney提供这些图片简直就是我的救世主,我很快便将一开始的免费次数用完,然后氪了一个30美刀的会员,虽然有点肉痛,但是为了儿时的梦想,这点痛算什么


虽然我查找了一下攻略,别人说可以使用gpt和midjourney配合起来,我也试了一下,效果一般,可能姿势没有对,继续用我的有道翻译将重点词汇翻译后丢给midjourney。midjourney不仅可以四选一,还可以对图片不断优化,还有比例选择,各种参数,但是我也不需要用到那么多额外的功能,总之一个字,就是棒。


但当时的我突然意识到,这个AI如此厉害,那么会不会对现在行业某些打工人造成一定影响呢,结果最近已经出了篇报道,某公司因为AI绘图工具辞退了众多插画师,事实不一定真实,但是也不是空穴来风,结合众多外界名人齐心协力抵制gpt5.0的研发,在担心数据安全之余,是否也在担心着AI对人类未来生活的各种冲击。焦虑时时刻刻都有,但解决焦虑的办法,我有一个妙招,仍然还是奖励自己


门槛


当我把整个小游戏焕然一新后,便兴冲冲的跑去微信开放平台上传我的伟大的杰作。但微信突然泼了我一盆冷水,上传微信小游戏前的流程有点出乎意外,要写游戏背景、介绍角色、NPC、介绍模块等等,还要上传不同的图片。我的小游戏一共就三个界面,有六个大板块要填写,每个板块还要两张不同的图片,我当时人就麻了。我只能创建一个单位截一次图,确保每张图片不一样。做完这道工序,还要写一份自审自查报告。


就算做完了这些前戏,我感觉我的小游戏还是难登大雅之堂,突然,我又想到了这个东西其实是不是也能运行在web端呢,随后我便立刻付诸行动,创建一个带有canvas的html,之前微信小游戏是通过weapp-adapter这个文件把canvas暴露到全局,所以在web端使用canvas的时候,只需要使用document.getElementById('canvas')暴露到全局即可。然后通过http-server对应用进行启动,这个小游戏便以web端的形式运行到浏览器上了,终于也能理解之前为啥微信小游戏火起来的时候,很多企业都用h5游戏稍微改下代码进行搬运,原来两者之间是有异曲同工之妙之处的。


关于游戏





上面两张便是两个种族对应的生产链,龙族是我第一个创建的,因为我自幼对龙产生好感和兴趣,何况我是龙的传人/doge。魔法学院则是稍微致敬一下《游戏王》中黑魔导卡组吧。


其实开发难度最难的莫过于是AI,也就是人机,如何让人机在有限的资源做出合理的选择,是一大难题,也是我后续要慢慢优化的,一开始我是让人机按照创建一个建筑,然后创建一个单位这种形式去做运营展开,但后来我想到一个好的点子,我应该可以根据每个种族的特点,走一条该特点的独有运营,于是人机龙族便有了龙蛋破坏龙两种流派,强度提升了一个档次。


其实是否能上架到微信小游戏已经不重要了,重要的是这个过程带给我的乐趣,一步一步看着这个游戏被创建出来的成就感,就算这个行业受到什么冲击,我需要被迫转行,我也不曾后悔,毕竟是web前端让我跨越了十几年的时光,找到了儿时埋下的种子,浇水,给予阳光,让它在我的心中成长为一棵充实的参天大树


h5地址:hslastudio.com/game/


github地址: github.com/FEA-Dven/wa…


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

禁止别人调试自己的前端页面代码

web
🎈 为啥要禁止? 由于前端页面会调用很多接口,有些接口会被别人爬虫分析,破解后获取数据 为了 杜绝 这种情况,最简单的方法就是禁止人家调试自己的前端代码 🎈 无限 debugger 前端页面防止调试的方法主要是通过不断 debugger 来疯狂输出断点...
继续阅读 »

🎈 为啥要禁止?



  • 由于前端页面会调用很多接口,有些接口会被别人爬虫分析,破解后获取数据

  • 为了 杜绝 这种情况,最简单的方法就是禁止人家调试自己的前端代码


禁止调试


🎈 无限 debugger



  • 前端页面防止调试的方法主要是通过不断 debugger 来疯狂输出断点,因为 debugger 在控制台被打开的时候就会执行

  • 由于程序被 debugger 阻止,所以无法进行断点调试,所以网页的请求也是看不到的

  • 基础代码如下:


/**
* 基础禁止调试代码
*/

(() => {
function ban() {
setInterval(() => {
debugger;
}, 50);
}
try {
ban();
} catch (err) { }
})();

基础禁止调试


🎈 无限 debugger 的对策



  • 如果仅仅是加上面那么简单的代码,对于一些技术人员而言作用不大

  • 可以通过控制台中的 Deactivate breakpoints 按钮或者使用快捷键 Ctrl + F8 关闭无限 debugger

  • 这种方式虽然能去掉碍眼的 debugger,但是无法通过左侧的行号添加 breakpoint


取消禁止对策


🎈 禁止断点的对策



  • 如果将 setInterval 中的代码写在一行,就能禁止用户断点,即使添加 logpointfalse 也无用

  • 当然即使有些人想到用左下角的格式化代码,将其变成多行也是没用的


(() => {
function ban() {
setInterval(() => { debugger; }, 50);
}
try {
ban();
} catch (err) { }
})();

禁止断点


🎈 忽略执行的代码



  • 通过添加 add script ignore list 需要忽略执行代码行或文件

  • 也可以达到禁止无限 debugger


忽略执行的代码


🎈 忽略执行代码的对策



  • 那如何针对上面操作的恶意用户呢

  • 可以通过将 debugger 改写成 Function("debugger")(); 的形式来应对

  • Function 构造器生成的 debugger 会在每一次执行时开启一个临时 js 文件

  • 当然使用的时候,为了更加的安全,最好使用加密后的脚本


// 加密前
(() => {
function ban() {
setInterval(() => {
Function('debugger')();
}, 50);
}
try {
ban();
} catch (err) { }
})();

// 加密后
eval(function(c,g,a,b,d,e){d=String;if(!"".replace(/^/,String)){for(;a--;)e[a]=b[a]||a;b=[function(f){return e[f]}];d=function(){return"\w+"};a=1}for(;a--;)b[a]&&(c=c.replace(new RegExp("\b"+d(a)+"\b","g"),b[a]));return c}('(()=>{1 0(){2(()=>{3("4")()},5)}6{0()}7(8){}})();',9,9,"block function setInterval Function debugger 50 try catch err".split(" "),0,{}));

解决对策


🎈 终极增强防调试代码



  • 为了让自己写出来的代码更加的晦涩难懂,需要对上面的代码再优化一下

  • Function('debugger').call() 改成 (function(){return false;})['constructor']('debugger')['call']();

  • 并且添加条件,当窗口外部宽高和内部宽高的差值大于一定的值 ,我把 body 里的内容换成指定内容

  • 当然使用的时候,为了更加的安全,最好加密后再使用


(() => {
function block() {
if (window.outerHeight - window.innerHeight > 200 || window.outerWidth - window.innerWidth > 200) {
document.body.innerHTML = "检测到非法调试,请关闭后刷新重试!";
}
setInterval(() => {
(function () {
return false;
}
['constructor']('debugger')
['call']());
}, 50);
}
try {
block();
} catch (err) { }
})();

终极增强防调试

收起阅读 »

差点让我崩溃的“全选”功能

web
今天在实现电商结算功能的时候遇到了个全选功能,做之前不以为然,做之后抓狂。为了让自己全身心投入到对这个问题的攻克,我特意将其拿出来先创建一个demo单独实现下。不搞不知道,一搞吓一跳,下面是我demo在浏览器运行的截图: 开始,我是这样写代码的: f...
继续阅读 »

今天在实现电商结算功能的时候遇到了个全选功能,做之前不以为然,做之后抓狂。为了让自己全身心投入到对这个问题的攻克,我特意将其拿出来先创建一个demo单独实现下。不搞不知道,一搞吓一跳,下面是我demo在浏览器运行的截图:


1679229898519.png
开始,我是这样写代码的:


    for (let i = 0; i < aCheckbox.length; i++) {
aCheckbox[i].checked = this.checked;
}
});

for (let i = 0; i < aCheckbox.length; i++) {
aCheckbox[i].addEventListener("click", function () {
for (let index = 0; index < aCheckbox.length; index++) {
if (aCheckbox[index].checked) {
oAllchecked.checked = aCheckbox[index].checked;
} else {
oAllchecked.checked = !aCheckbox[index].checked;
}
}
});
}

点击全选这个功能不难,主要问题出现在如何保证另外两个复选框在其中一个没有选中的情况下,全选的这个复选框没有选中。苦思良久,最后通过查找资料看到了如今的代码:


    aCheckbox[i].addEventListener("click", function () {
let flag = true;
for (let index = 0; index < aCheckbox.length; index++) {
console.log(aCheckbox[index].checked);
if (!aCheckbox[index].checked) {
flag = false;
break;
}
}
oAllchecked.checked = flag;
});
}

功能完美就解决,第一个代码问题的原因是‘aCheckbox[index].checked’这个判断不能解决两个复选框什么时候一个选中一个没选中的问题。这个问题不解决也就不能让全选复选框及时更新正确的选中状态了。


而下面这个代码通过设置一个中间值flag,及时记录每个复选框按钮的选中状态,能准确的赋值给全选功能的复

作者:一个对前端不离不弃的中年菜鸟
来源:juejin.cn/post/7212942861518864421
选框按钮。于是这个需求就解决了~

收起阅读 »

前端使用a链接下载内容增加loading效果

web
问题描述:最近工作中出现一个需求,纯前端下载 Excel 数据,并且有的下载内容很多,这时需要给下载增加一个 loading 效果。 代码如下: // utils.js const XLSX = require('xlsx') // 将一个sheet转成最终...
继续阅读 »

  1. 问题描述:最近工作中出现一个需求,纯前端下载 Excel 数据,并且有的下载内容很多,这时需要给下载增加一个 loading 效果。

  2. 代码如下:


// utils.js
const XLSX = require('xlsx')
// 将一个sheet转成最终的excel文件的blob对象,然后利用URL.createObjectURL下载
export const sheet2blob = (sheet, sheetName) => {
sheetName = sheetName || 'sheet1'
var workbook = {
SheetNames: [sheetName],
Sheets: {}
}
workbook.Sheets[sheetName] = sheet
// 生成excel的配置项
var wopts = {
bookType: 'xlsx', // 要生成的文件类型
bookSST: false, // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
type: 'binary'
}
var wbout = XLSX.write(workbook, wopts)
var blob = new Blob([s2ab(wbout)], { type: 'application/octet-stream' })
// 字符串转ArrayBuffer
function s2ab(s) {
var buf = new ArrayBuffer(s.length)
var view = new Uint8Array(buf)
for (var i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xff
return buf
}
return blob
}

/**
* 通用的打开下载对话框方法,没有测试过具体兼容性
* @param url 下载地址,也可以是一个blob对象,必选
* @param saveName 保存文件名,可选
*/

export const openDownloadDialog = (url, saveName) => {
if (typeof url === 'object' && url instanceof Blob) {
url = URL.createObjectURL(url) // 创建blob地址
}
var aLink = document.createElement('a')
aLink.href = url
aLink.download = saveName + '.xlsx' || '1.xlsx' // HTML5新增的属性,指定保存文件名,可以不要后缀,注意,file:///模式下不会生效
var event
if (window.MouseEvent) event = new MouseEvent('click')
else {
event = document.createEvent('MouseEvents')
event.initMouseEvent(
'click',
true,
false,
window,
0,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null
)
}
aLink.dispatchEvent(event)
}

<el-button
@click="clickExportBtn"
>
<i class="el-icon-download"></i>下载数据
</el-button>
<div class="mongolia" v-if="loadingSummaryData">
<el-icon class="el-icon-loading loading-icon">
<Loading />
</el-icon>
<p>loading...</p>
</div>


clickExportBtn: _.throttle(async function() {
const downloadDatas = []
const summaryDataForDownloads = this.optimizeHPPCDownload(this.summaryDataForDownloads)
summaryDataForDownloads.map(summaryItem =>
downloadDatas.push(this.parseSummaryDataToBlobData(summaryItem))
)
// donwloadDatas 数组是一个三维数组,而 json2sheet 需要的数据是一个二维数组
this.loadingSummaryData = true
const downloadBlob = aoa2sheet(downloadDatas.flat(1))
openDownloadDialog(downloadBlob, `${this.testItem}报告数据`)
this.loadingSummaryData = false
}, 2000),

// css
.mongolia {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.9);
display: flex;
justify-content: center;
align-items: center;
font-size: 1.5rem;
color: #409eff;
z-index: 9999;
}
.loading-icon {
color: #409eff;
font-size: 32px;
}


  1. 解决方案探究:




  • 在尝试了使用 $nextTick、将 openDownloadDialog 改写成 Promise 异步函数,或者使用 async/await、在 openDownloadDialog 中添加 loadingSummaryData 逻辑,发现依旧无法解决问题,因此怀疑是 document 添加新元素与 vue 的 v-if 渲染产生冲突,即 document 添加新元素会阻塞 v-if 的执性。查阅资料发现,问题可能有以下几种:



    • openDownloadDialog 在执行过程中执行了较为耗时的同步操作,阻塞了主线程,导致了页面渲染的停滞。

    • openDownloadDialog 的 click 事件出发逻辑存在问题,阻塞了事件循环(Event Loop)。

    • 浏览器在执行 openDownloadDialog 时,将其脚本任务的优先级设置得较高,导致占用主线程时间片,推迟了其他渲染任务。

    • Vue 的批量更新策略导致了 v-if 内容的显示被延迟。




  • 查阅资料后找到了如下几种方案:





      1. 使用 setTimeout 使 openDownloadDialog 异步执行


      clickExport() {
      this.loadingSummaryData = true;

      setTimeout(() => {
      openDownloadDialog(downloadBlob, `${this.testItem}报告数据`);

      this.loadingSummaryData = false;
      });
      }




      1. 对 openDownloadDialog 内部进行优化



      • 避免大循环或递归逻辑

      • 将计算工作分批进行

      • 使用 Web Worker 隔离耗时任务


        • 在编写 downloadWorker.js 中的代码时,要明确这部分代码是运行在一个独立的 Worker 线程内部,而不是主线程中。





            1. 不要直接依赖或者访问主线程的全局对象,比如 window、document 等。这些在 Worker 内都无法直接使用。





            1. 不要依赖 DOM 操作,比如获取某个 DOM 元素。Worker 线程无法访问页面的 DOM。





            1. 代码执行的入口是 onmessage 回调函数,在其中编写业务逻辑。





            1. 和主线程的通信只能通过 postMessage 和 onmessage 发送消息事件。





            1. 代码应该是自包含的,不依赖外部变量或状态。





            1. 可以导入其他脚本依赖,比如用 import 引入工具函数等。





            1. 避免修改或依赖全局作用域,比如定义全局变量等。






        • 总之,编写 downloadWorker.js 时要注意它执行在一个独立环境中,只能通过消息事件与主线程通信。


          // 创建 Worker
          const worker = new Worker('downloadWorker.js');

          // 点击下载时向 Worker 发送消息
          function clickDownload() {

          showLoading();

          worker.postMessage({
          url: fileURL,
          filename: 'report.xlsx'
          });

          worker.onmessage = function(e) {
          // 收到下载完成的消息
          hideLoading();
          }

          }

          // 显示 loading
          function showLoading() {
          loading.style.display = 'block';
          }

          // 隐藏 loading
          function hideLoading() {
          loading.style.display = 'none';
          }

          // downloadWorker.js

          onmessage = function(e) {
          const url = e.data.url;
          const filename = e.data.filename;

          // 创建并点击链接触发下载
          const a = document.createElement('a');
          a.href = url;
          a.download = filename;
          a.click();

          postMessage('下载完成');
          }

          <div id="loading" style="display:none;">
          Downloading...
          </div>

          <button onclick="clickDownload()">
          Download
          </button>

          <script src="downloadWorker.js"></script>








      1. 使用 requestIdleCallback 进行调度


      clickExport() {
      this.loadingSummaryData = true;

      requestIdleCallback(() => {
      openDownloadDialog(downloadBlob, `${this.testItem}报告数据`);

      this.loadingSummaryData = false;
      });
      }



    • 当使用上述方案时发现里面的内容变得异常的慢,于是去问GPT后,得到以下答案:


      使用 requestIdleCallback 来执行 openDownloadDialog 确实可能会需要更多时间。


      因为 requestIdleCallback 会在主线程空闲时才执行回调函数,所以不像 setTimeout 可以直接按指定时间异步执行。


      requestIdleCallback 的优先级较低,只在主线程空闲时才会执行,所以 download 的速度会比 setTimeout 慢一些。


      但使用 requestIdleCallback 的优点是它不会占用主线程时间,可以避免阻塞页面渲染。


      所以需要权衡执行速度和避免阻塞之间的关系:




      • 如果下载速度是优先考虑的因素,可以继续使用 setTimeout 异步执行。




      • 如果避免阻塞渲染更重要,则 requestIdleCallback 是更好的选择。




      偶尔下载任务被延迟不是大问题,但频繁的下载任务阻塞会严重影响页面体验。


      此外,可以结合两者试试:


      clickExport() {

      this.loadingSummaryData = true;

      setTimeout(() => {

      requestIdleCallback(() => {
      openDownloadDialog(downloadBlob);
      });

      this.loadingSummaryData = false;

      }, 200);

      }

      setTimeout 先异步调度,然后在回调中使用 requestIdleCallback 避免阻塞。这样就可以兼顾下载速度和避免渲染阻塞了。





      1. 分析线程模型,找到具体的阻塞点



      • 使用 Performance 工具分析线程

      • debugger 及 console.log 打印关键函数的执行时间

      • 检查是否有非主线程的任务阻塞了主线程





      1. 调整 vue 的批量更新策略


      new Vue({
      config: {
      // 修改批量更新的队列长度
      batchUpdateDuration: 100
      }
      })

      作者:李卫泽
      来源:juejin.cn/post/7268050036474609683
      i>


收起阅读 »

const声明的变量还能修改?原理都在这了

web
前言 const关键字用于声明只读的常量,一旦被赋值,其值就不能再被改变。但是,这并不意味着该变量持有的值是不可变的。 本文将结合案例逐步探讨JavaScript中const声明的工作原理,解释为何这些看似常量的变量却可以被修改,并探讨其实际应用。 耐心看完,...
继续阅读 »

前言


const关键字用于声明只读的常量,一旦被赋值,其值就不能再被改变。但是,这并不意味着该变量持有的值是不可变的。


本文将结合案例逐步探讨JavaScript中const声明的工作原理,解释为何这些看似常量的变量却可以被修改,并探讨其实际应用。


耐心看完,你一定有所收获。


giphy.gif


正文


const关键字用于声明一个变量,该变量的值在其生命周期中不会被重新赋值。


现象


1. 基本数据类型


对于基本数据类型(如数字、字符串、布尔值),const确保变量的值不会改变。


const num = 42;
// num = 43; // 这会抛出错误

2. 对象


对于对象,仍然可以修改对象的属性,但不能重新赋值整个对象。


const girlfriend = {
name: "小宝贝"
};

girlfriend.name = "亲爱的"; // 这是允许的,因为你只是修改了对象的一个属性

// girlfriend = { name: "亲爱的" }; // 这会抛出错误,因为你试图改变obj的引用

假如你有个女朋友,也许并没有,但我们可以假设,她的名字或者你平时叫她的昵称是"小宝贝"。


有一天,你心血来潮,想换个方式叫她,于是叫她"亲爱的"。这完全没问题,因为你只是给她换了个昵称,她本人并没有变。


但是,如果有一天你看到另一个女生,你却说:“哎,这不是亲爱的吗?”这就出大问题了!因为你把一个完全不同的人当成了你的女朋友。


这就像你试图改变girlfriend的引用,把它指向了一个新的对象。


JavaScript不允许这样做,因为你之前已经明确地告诉它,girlfriend就是那个你叫"小宝贝"的女朋友,你不能突然把另一个人说成她。


154eb98c10eaf8356b5da0e44b9e9fe6.gif


简单来说,你可以随时给你的女朋友起个新昵称,但你不能随便把别的女生当成你的女朋友。


3. 数组


对于数组,你可以修改、添加或删除元素,但不能重新赋值整个数组。


const arr = [1, 2, 3];

arr[0] = 4; // 这是允许的,因为你只是修改了数组的一个元素
arr.push(5); // 这也是允许的,因为你只是向数组添加了一个元素

// arr = [6, 7, 8]; // 这会抛出一个错误,因为你试图改变arr的引用

假设arr是你的超市购物袋,里面有三个苹果,分别标记为1、2和3。


你检查了第一个苹果,觉得它不够新鲜,所以你把它替换成了一个新的苹果,标记为4。这就像你修改数组的一个元素。这完全可以,因为你只是替换了袋子里的一个苹果。


后来,你决定再放一个苹果进去,标记为5。这也没问题,因为你只是向袋子里添加了一个苹果。


苹果再变,袋子仍然是原来的袋子。


但是,当你试图拿个新的装着6、7、8的购物袋来代替你原来的袋子时,就不对了。你不能拿了一袋子苹果,又扔在那不管,反而又去拿了一袋新的苹果。


你礼貌吗?


f2e0de05371993107839d315b5639a30.jpg


你可以随时替换袋里的苹果或者放更多的苹果进去,但你不能拿了一袋不要了又拿一袋。


原理


在JavaScript中,const并不是让变量的值变得不可变,而是让变量指向的内存地址不可变。换句话说,使用const声明的变量不能被重新赋值,但是其所指向的内存中的数据是可以被修改的。


使用const后,实际上是确保该变量的引用地址不变,而不是其内容。


结合上面两个案例,女朋友和购物袋就好比是内存地址,女朋友的外号可以改,但女朋友是换不了的,同理袋里装的东西可以换,但袋子仍然是那个袋子。


当使用const声明一个变量并赋值为一个对象或数组,这个变量实际上存储的是这个对象或数组在内存中的地址,形如0x00ABCDEF(这只是一个示例地址,实际地址会有所不同),而不是它的内容。这就是为什么我们说变量“引用”了这个对象或数组。


实际应用


这种看似矛盾的特性实际上在开发中经常用到。


例如,在开发过程中,可能希望保持一个对象的引用不变,同时允许修改对象的属性。这可以通过使用const来实现。


考虑以下示例:


假设你正在开发一个应用,该应用允许用户自定义一些配置设置。当用户首次登录时,你可能会为他们提供一组默认的配置。但随着时间的推移,用户可能会更改某些配置。


// 默认配置
const userSettings = {
theme: "light", // 主题颜色
notifications: true, // 是否开启通知
language: "en" // 默认语言
};

// 在某个时间点,用户决定更改主题颜色和语言
function updateUserSettings(newTheme, newLanguage) {
userSettings.theme = newTheme;
userSettings.language = newLanguage;
}

// 用户调用函数,将主题更改为"dark",语言更改为"zh"
updateUserSettings("dark", "zh");

console.log(userSettings); // 输出:{ theme: "dark", notifications: true, language: "zh" }

在这个例子中,我们首先定义了一个userSettings对象,它包含了用户的默认配置。尽管我们使用const来声明这个对象,但我们仍然可以随后更改其属性来反映用户的新配置。


这种模式在实际开发中很有用,因为它允许我们确保userSettings始终指向同一个对象(即我们不会意外地将其指向另一个对象),同时还能够灵活地更新该对象的内容以反映用户的选择。


为什么不用let


以上所以案例中,使用let都是可行,但它的语义和用途相对不同,主要从这几个方面进行考虑:



  1. 不变性:使用const声明的变量意味着你不打算重新为该变量赋值。这为其他开发人员提供了一个明确的信号,即该变量的引用不会改变。在上述例子中,我们不打算将userSettings重新赋值为另一个对象,我们只是修改其属性。因此,使用const可以更好地传达这一意图。

  2. 错误预防:使用const可以防止意外地重新赋值给变量。如果你试图为const变量重新赋值,JavaScript会抛出错误。这可以帮助捕获潜在的错误,特别是在大型项目或团队合作中。

  3. 代码清晰度:对于那些只读取和修改对象属性而不重新赋值的场景,使用const可以提高代码的清晰度,可以提醒看到这段代码的人:“这个变量的引用是不变的,但其内容可能会变。”


一般我们默认使用const,除非确定需要重新赋值,这时再考虑使用let。这种方法旨在鼓励不变性,并使代码更加可预测和易于维护。


避免修改


如果我们想要避免修改const声明的变量,当然也是可以的。


例如,我们可以使用浅拷贝来创建一个具有相同内容的新对象或数组,从而避免直接修改原始对象或数组。这可以通过以下方式实现:


const originalArray = [1, 2, 3];
const newArray = [...originalArray]; // 创建一个原始数组的浅拷贝
newArray.push(4); // 不会影响原始数组
console.log(originalArray); // 输出: [1, 2, 3]
console.log(newArray); // 输出: [1, 2, 3, 4]

总结


const声明的变量之所以看似可以被修改,是因为const限制的是变量指向的内存地址的改变,而不是内存中数据的改变。这种特性在实际开发中有其应用场景,允许我们保持引用不变,同时修改数据内容。


然而,如果我们确实需要避免修改数据内容,可以采取适当的措施,如浅拷贝。


9a9f1473841eca9a3e5d7e1408145a4b.gif

收起阅读 »

好烦啊,为什么点个链接还让我确认一下?

web
万丈苍穹水更深,无限乾坤尽眼中 背景 最近经常看到各大SNS平台有这样一项功能,点击跳转链接,如果不是本站的链接,那么就会跳转到一个中转页,告诉你跳转了一个非本站链接,提醒你注意账号财产安全,如图: 很明显,这个是为了一定程度的避免用户被钓鱼,预防XSS或...
继续阅读 »

万丈苍穹水更深,无限乾坤尽眼中



背景


最近经常看到各大SNS平台有这样一项功能,点击跳转链接,如果不是本站的链接,那么就会跳转到一个中转页,告诉你跳转了一个非本站链接,提醒你注意账号财产安全,如图:


A6C73047-4041-4584-9F97-BA04C896D73E.png


很明显,这个是为了一定程度的避免用户被钓鱼,预防XSS或CRSF攻击,所以请不要像标题一样抱怨,多点一下也花不了2S时间。


原理


那么这个是如何实现的呢,原理其实很简单。


a标签的onclick事件可以被拦截,当返回false时不会默认跳转。


那么具体如何实现呢,拿掘金来举例:


        function SetSafeA(whiteDomList: string[], safeLink = 'https://link.juejin.cn/?target=') {
          const aArr = document.getElementsByTagName('a')
          Array.from(aArr).forEach(item=>{
            item.onclick = ()  => {
              let target = item.getAttribute('href')!
              if(/^\//.test(target)) {
                // 相对本站链接
                return true
              }
             const isSafe = undefined !==  whiteDomList.find(item=>{
                 return target.indexOf(item) !== -1
              })
              if(!isSafe) {
                window.open(`${safeLink}${target}`, '_blank')
              } else {
return true
}
              return false
            }
          })
        }

可以随便找一个网页在控制台执行一下,都能跳到掘金的中转页,中转页的代码就不写了^_^


实践


刚好最近遇到一个使用场景,公司APP产品里面都有各自用户协议,其中SDK协议我们都是直接跳转链接的,结果在部分渠道如小天才,步步高等对用户信息非常敏感的平台上,要求所有的链接必须要跳转到平台默认的安全浏览器上,不能在APP内打开。那么协议有很多如何快速处理呢。由于项目用到了vue,这里就想到使用指令,通过批量添加指令来达到快速替换,比如'<a' =>'<a v-link="x"',代码如下:


Vue.directive('outlink', {
  bind: (el, binding) => {
    el.outlink = () => {
      if (GetEnv() === 'app') {
        const from = isNaN(+binding.value) ? 1 : +binding.value
        const url = el.getAttribute('href')
        if (url && url !== '' && url != 'javascript:;') {
          window.location.href = `${GetSchemeByFrom(from)}://outside_webview?url=${url}`
        }
        return false
      }
    }
    el.onclick = el.outlink
  },
  unbind: (el) => {
    el.onclick = null
    delete el.outlink
  }
})

这里我们传入了from值来区分APP平台,然后调用APP提供的相应scheme跳转到客户端的默认浏览器,如下:


DE2DFA5F-ED19-4e74-97B6-2D19246D5D84.png


结语


链接拦截可以做好事,也可以做一些hack,希望使用的人保持一颗爱好和平的心;当然遇到让你确认安全的

作者:CodePlayer
来源:juejin.cn/post/7161712791089315877
链接时,也请你保持一颗感谢的心。

收起阅读 »

前端重新部署如何通知用户刷新网页?

1.目标场景 有时候上完线,用户还停留在老的页面,用户不知道网页重新部署了,跳转页面的时候有时候js连接hash变了导致报错跳不过去,并且用户体验不到新功能。 2.思考解决方案 如何去解决这个问题 思考中... 如果后端可以配合我们的话我们可以使用webSoc...
继续阅读 »



1.目标场景


有时候上完线,用户还停留在老的页面,用户不知道网页重新部署了,跳转页面的时候有时候js连接hash变了导致报错跳不过去,并且用户体验不到新功能。


2.思考解决方案


如何去解决这个问题
思考中...


如果后端可以配合我们的话我们可以使用webSocket 跟后端进行实时通讯,前端部署完之后,后端给个通知,前端检测到Message进行提示,还可以在优化一下使用EvnentSource 这个跟socket很像只不过他只能后端往前端推送消息,前端无法给后端发送,我们也不需要给后端发送。


以上方案需要后端配合,奈何公司后端都在忙,需要纯前端实现。


重新进行思考...


根据和小伙伴的讨论得出了一个方案,在项目根目录给个json 文件,写入一个固定的key值然后打包的时候变一下,然后代码中轮询去判断看有没有变化,有就提示。




果然是康老师经典不知道。




但是写完之后发现太麻烦了,需要手动配置json文件,还需要打包的时候修改,有没有更简单的方案,
进行第二轮讨论。


第二轮讨论的方案是根据打完包之后生成的script src 的hash值去判断,每次打包都会生成唯一的hash值,只要轮询去判断不一样了,那一定是重新部署了.




3.代码实现

interface Options {
timer?: number
}

export class Updater {
oldScript: string[] //存储第一次值也就是script 的hash 信息
newScript: string[] //获取新的值 也就是新的script 的hash信息
dispatch: Record<string, Function[]> //小型发布订阅通知用户更新了
constructor(options: Options) {
this.oldScript = [];
this.newScript = []
this.dispatch = {}
this.init() //初始化
this.timing(options?.timer)//轮询
}


async init() {
const html: string = await this.getHtml()
this.oldScript = this.parserScript(html)
}

async getHtml() {
const html = await fetch('/').then(res => res.text());//读取index html
return html
}

parserScript(html: string) {
const reg = new RegExp(/<script(?:\s+[^>]*)?>(.*?)<\/script\s*>/ig) //script正则
return html.match(reg) as string[] //匹配script标签
}

//发布订阅通知
on(key: 'no-update' | 'update', fn: Function) {
(this.dispatch[key] || (this.dispatch[key] = [])).push(fn)
return this;
}

compare(oldArr: string[], newArr: string[]) {
const base = oldArr.length
const arr = Array.from(new Set(oldArr.concat(newArr)))
//如果新旧length 一样无更新
if (arr.length === base) {
this.dispatch['no-update'].forEach(fn => {
fn()
})

} else {
//否则通知更新
this.dispatch['update'].forEach(fn => {
fn()
})
}
}

timing(time = 10000) {
//轮询
setInterval(async () => {
const newHtml = await this.getHtml()
this.newScript = this.parserScript(newHtml)
this.compare(this.oldScript, this.newScript)
}, time)
}

}

代码用法

//实例化该类
const up = new Updater({
timer:2000
})
//未更新通知
up.on('no-update',()=>{
console.log('未更新')
})
//更新通知
up.on('update',()=>{
console.log('更新了')
})

4.测试


执行 npm run build 打个包


安装http-server


使用http-server 开个服务




重新打个包npm run build




这样子就可以检测出来有没有重新发布就可以通知用户更新了。


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

作为一个前端为什么要学习 Rust ?

这里抛出一个问题 作为一个前端为什么要去学习 Rust ? 这是个好问题,有人可能觉得前端学习 Rust 意义不大,学习成本高,投入产出比低啥的,JavaScript、TypeScript 这些前端语言还没搞明白呢,为什么还要去学一门这么难的新语言? 那么今天...
继续阅读 »

这里抛出一个问题


作为一个前端为什么要去学习 Rust ?


这是个好问题,有人可能觉得前端学习 Rust 意义不大,学习成本高,投入产出比低啥的,JavaScript、TypeScript 这些前端语言还没搞明白呢,为什么还要去学一门这么难的新语言?


那么今天我就谈谈我自己对于这个问题的看法~,主要是分为 5 点:

  • 性能
  • 跨平台特性
  • 安全性
  • 职业视野
  • 职业竞争力

性能


Rust 可以给 Node.js 提供一个性能逃生通道,当我们使用 Node.js 遇到性能瓶颈或 CPU 密集计算场景的时候,便可以使用 Rust 编写 Native Addon 解决这个问题了,Native Addon 就是一个二进制文件,也就是 xxx.node 文件,比如 swc(对应 babel)、Rspack(对应Webpack)、Rome(对应 eslint、prettier、babel、webpack 等,目标是代替我们所熟悉的所有前端工具链...),上面提到的工具链就都是使用 Rust 编写的,性能比 Node.js 对应功能的包都有了极大的提高,同时 Rust 也是支持多线程的,你编写的多线程代码在 Node.js 中一样可以跑,这就可以解决了 Node.js 不擅长 CPU 密集型的问题。在前端架构领域目前 Rust 已经差不多是标配了,阿里、字节内部的前端基建目前都开始基于 Rust 去重构了。


跨平台


可以编写高性能且支持跨平台的 WebAssembly 扩展,可以在浏览器、IOT 嵌入式设备、服务端环境等地方使用,并且也拥有很不错的性能;和上面提到的 Native Addon 不一样, Native Addon 在不同的平台上都需要单独的进行编译,不支持跨平台;但是 WebAssembly 不一样,虽然它的性能没 Native Addon 好,但是跨平台成本很低,我编写的一份代码在 Node.js 中执行没问题,在 Deno 中跑也没问题,在 Java 或者 Go 中跑也都没问题,甚至在单片机也可以运行,只要引入对应的 Wasm 运行时即可。现在 Docker 也已经有 WebAssembly 版本了;同时 Rust 也是目前编写 WebAssembly 最热门的语言,因为它没有垃圾回收,性能高,并且有一个超好用的包管理器 cargo。


安全


Rust 编译器真的是事无巨细,它保证你编写的代码不会出低级错误,比如一些类型上的错误和内存分配上的错误,基本上只要 Rust 代码能够编译通过,就可以安心上线,在服务端、操作系统等领域来说这也是个很好的特性,Linux 系统和安卓系统内核都已经开始使用 Rust ,这还信不过嘛?


视野


Rust 可以提升自己在服务端领域的视野,Rust 不同于 Node.js 这个使用动态 JS 语言的运行时,它是一门正儿八经的静态编译型编程语言,并且没有垃圾回收,可以让我们掌握和理解计算机的一些底层工作机制,比如内存是如何分配和释放的,Rust 中使用所有权、生命周期等概念来保证内存安全,这对我们对于编程的理解也可以进一步提升,很多人说学习了 Rust 之后对自己编写其它语言的代码也有了更深的理解,毕竟计算机底层的概念都是相通的,开阔自己的编程思维。


职业竞争力


这个问题简单,你比别人多一门技能,比如 WebAssembly 和 Native Addon 都可以作为 Node.js 性能优化的一种手段,面试的时候说你会使用 Rust 解决 Node.js 性能问题,这不是比别人多一些竞争力吗?面试官那肯定也会觉得你顶呱呱~ 另外虽然目前 Rust 的工作机会比较少,但是也不代表没有,阿里和字节目前都有关于前端基建的岗位,会 Rust 是加分项,另外 Rust 在 TIOBE 编程语言榜排名中已经冲进了前 20,今年 6 月份是第 20 名,7 月份是第 17 名,流行度开始慢慢上来了,我相信以后工作机会也会越来越多的。


总结


不过,总的来说,这还是得看自己个人的学习能力,学有余力的时候可以学习一下 Rust,我自己不是 Rust 吹啊,我学习 Rust 的过程中真的觉得很有趣,因为里面的很多概念在前端领域中都是接触不到的,学了之后真的像是打开了新世界的大门,包括可以去看 Deno 的源码了,可以了解到一个 Js 运行时是怎么进行工作的,这些都是与我们前端息息相关的东西,即使哪天不做前端了,可以去转服务端或嵌入式方向,起码编程语言这一关不需要费多大力气了,Rust 是目前唯一一门从计算机底层到应用层都有落地应用的语言。不多说了,学就完事了,技多不压身嘛


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

卸下if-else 侠的皮衣!- 状态模式

web
🤭当我是if-else侠的时候 😶怕出错 给我一个功能,我总是要写很多if-else,虽然能跑,但是维护起来确实很难受,每次都要在一个方法里面增加逻辑,生怕搞错,要是涉及到支付功能,分分钟炸锅 😑难调试 我总是不知道之前写的逻辑在哪里,一个方法几百行逻辑,而且...
继续阅读 »

🤭当我是if-else侠的时候


😶怕出错


给我一个功能,我总是要写很多if-else,虽然能跑,但是维护起来确实很难受,每次都要在一个方法里面增加逻辑,生怕搞错,要是涉及到支付功能,分分钟炸锅


😑难调试


我总是不知道之前写的逻辑在哪里,一个方法几百行逻辑,而且是不同功能点冗余在一起!这可能让我牺牲大量时间在这查找调试中


🤨交接容易挨打


当你交接给新同事的时候,这个要做好新同事的白眼和嘲讽,这代码简直是坨翔!这代码简直是个易碎的玻璃,一碰就碎!这代码简直是个世界十大奇迹!


🤔脱下if-else侠的皮衣


先学习下开发的设计原则


单一职责原则(SRP)



就一个类而言,应该仅有一个引起他变化的原因



开放封闭原则(ASD)



类、模块、函数等等应该是可以扩展的,但是不可以修改的



里氏替换原则(LSP)



所有引用基类的地方必须透明地使用其子类的对象



依赖倒置原则(DIP)



高层模块不应该依赖底层模块



迪米特原则(LOD)



一个软件实体应当尽可能的少与其他实体发生相互作用



接口隔离原则(ISP)



一个类对另一个类的依赖应该建立在最小的接口上



在学习下设计模式


大致可以分三大类:创建型结构型行为型

创建型:工厂模式 ,单例模式,原型模式

结构型:装饰器模式,适配器模式,代理模式

行为型:策略模式,状态模式,观察者模式


之前文章学习了**策略模式适配器模式,有兴趣可以过去看看,下面我们来学习适配器模式**


场景:做四种咖啡的咖啡机


- 美式咖啡(american):只吐黑咖啡
- 普通拿铁(latte):黑咖啡加点奶
- 香草拿铁(vanillaLatte):黑咖啡加点奶再加香草糖浆
- 摩卡咖啡(mocha):黑咖啡加点奶再加点巧克力


用if-else来写,如下


class CoffeeMaker {
constructor() {
/**
这里略去咖啡机中与咖啡状态切换无关的一些初始化逻辑
**/

// 初始化状态,没有切换任何咖啡模式
this.state = 'init';
}

// 关注咖啡机状态切换函数
changeState(state) {
// 记录当前状态
this.state = state;
if(state === 'american') {
// 这里用 console 代指咖啡制作流程的业务逻辑
console.log('我只吐黑咖啡');
} else if(state === 'latte') {
console.log(`给黑咖啡加点奶`);
} else if(state === 'vanillaLatte') {
console.log('黑咖啡加点奶再加香草糖浆');
} else if(state === 'mocha') {
console.log('黑咖啡加点奶再加点巧克力');
}
}
}


分下下问题,假如我们多了几种格式,我们又要在这个方法体里面扩展,违反了开放封闭原则(ASD),所以我们现在来采用适配器模式来写下:


class CoffeeMaker {
constructor() {
// 初始化状态,没有切换任何咖啡模式
this.state = 'init';
// 初始化牛奶的存储量
this.leftMilk = '500';
}
stateToProcessor = {
that: this,
american() {
this.that.leftMilk = this.that.leftMilk - 100
console.log('咖啡机现在的牛奶存储量是:', this.that.leftMilk)
console.log('吐黑咖啡');
},
latte() {
this.american()
console.log('加点奶');
},
vanillaLatte() {
this.latte();
console.log('再加香草糖浆');
},
mocha() {
this.latte();
console.log('再加巧克力');
}
}

changeState(state) {
this.state = state;
if (!this.stateToProcessor[state]) {
return;
}
this.stateToProcessor[state]();
}
}

const mk = new CoffeeMaker();
mk.changeState('latte');
mk.changeState('mocha');



这个状态模式实际上跟策略模式很像,但是状态模式会关注里面的状态变化,就像上诉代码能检测咖啡牛奶量,去除了if-else,能很好的扩展维护



结尾


遵守设计规则,脱掉if-else的皮衣,善用设计模式,加油,骚年们!给我点点赞,关注下!

作者:向乾看
来源:juejin.cn/post/7267207014382829579

收起阅读 »

老板搞这些事情降低我写码生产力,我把他开除了

web
Hi 大家好我是 ssh,在工作的时候,我经常会感觉到很头疼,觉得自己的时间被各种各样的事情浪费了,可能是没用的会议,写得很垃圾的文档,各种为了达到某些目的而做的 Code Review 和工程化建设等等,正巧看到这篇 Top 7 Things That Ki...
继续阅读 »

Hi 大家好我是 ssh,在工作的时候,我经常会感觉到很头疼,觉得自己的时间被各种各样的事情浪费了,可能是没用的会议,写得很垃圾的文档,各种为了达到某些目的而做的 Code Review 和工程化建设等等,正巧看到这篇 Top 7 Things That Kill Developer Productivity,不敢私藏干货,赶紧来分享给大家,希望能共同避免



简介


几周前,我突然发现:在工作 4 小时内,我的工作时间和有效的编码时间差了整整 2 小时。为了重回正轨,我决定主动减少阻碍,来缩小这个差距,争取能写更多代码,把无关的事情抛开。这个时间差越大,我的效率就越低。


和其他行业的人相比,程序员在这方面遇到的困境更甚。这些障碍往往会导致码农信心下降、写代码和优化的时间变少,职业倦怠率更高。影响创造力和热情。


根据本周 CodeTime 的全球平均编码时间,约 45%的总编码时间都是消极编码。时间和资金都在被浪费。


低效的开发流程是罪魁祸首。


1. 会议


会议


低效的会议是导致开发人员生产力下降的最不必要的因素之一。编程需要心流。平均而言,进入心流状态大约需要 30 分钟。但是由于乱七八糟会议,专注力就被打断了,码农必须重复这个过程。


有时 10 分钟的会议硬拖到一个小时,这也很浪费时间。减少了用于实际编程和解决问题的时间。有些会议还需要码农无用的出席。如果这次会议和码农的专业知识无关,根本没必要让他们参会。


2. 技术债(Fix it Later)


技术债


技术债,简单来说就是“以后再修”的心态。先采用捷径实现,妄图后面有空再修改成更优的方式。


最开始,先让功能可用就行,而优化留到以后。短期来看这可能行得通,因为它可以加快项目进度,你可能在 deadline 前完成。但是反复这么做就会留下大量待完成的工作。会使维护、扩展和优化软件变得更加困难。


技术债会以多种方式阻碍码农的生产力。列举一些:




  • Code Review 的瓶颈:当技术债增加时,会增加 Code Review 所花费的时间。




  • 更多 Bug:由于关注点都在速度而不是优化上,会导致引入隐藏的 Bug。




  • 代码质量降低:只为了让它可以跑,会导致糟糕的代码质量、随意的 Code Review,甚至没有代码注释,随意乱写复杂的代码等。




上述所有点都需要额外的时间来处理。因此,这会拖长项目的时间线。


3. Code Review


Code Review


Code Review 需要时间,如果 Review 时间过长,会延迟新代码的集成并放缓整个开发过程。有时候码农提出 PR 但 Code Reviewer 没有时间进行审查。会码农处理下一任务的时间。在进行下一个任务的同时,再回头 Code Review 时会有上下文切换。会影响码农的专注力和生产力。


对于 Code Review,码农可能不得不参加多个会议,减少了码农的生产力。代码反馈往往不明确或过于复杂,需要进一步讨论来加深理解,解决问题通常需要更长时间。Code Review 对一个组织来说必不可少且至关重要,但是需要注意方式和效率。


4. 微观管理 (Micromanagement)(缺乏自治)


微观管理


微观管理是一种管理方式,主管密切观察和管理下属的工作。在码农的语境下,当管理者想要控制码农的所有编码细节时就发生了。这可能导致码农对他们的代码、流程、决策和创造力的降低。


举例来说:




  • 缺乏动力:微观管理可能表明组织对码农能力的信任不足。这样,码农很容易感到失去动力。




  • 缺乏创造力:开发软件是一项需要专注以探索创造性解决方案的创作性任务。但是微观管理会导致码农对代码的控制较少,阻碍码农的创造力。




  • 决策缓慢:码农必须就简单的决定向管理层寻求确认,在这个过程中大量时间被浪费。




在所有这些情况下,码农的生产力都会下降。


5. 职业倦怠


职业倦怠


职业倦怠是码农面临的主要问题之一。面对复杂具有挑战性的项目和紧迫的 deadline,以及不断提高代码质量的压力都可能导致职业倦怠。这最终会导致码农的生产力下降,会显著减弱码农的注意力和写代码的能力。


这也会导致码农的创造力和解决问题的能力下降。这最终会导致开发周期变慢。


6. 垃圾文档


垃圾文档


文档对码农至关重要,因为它传达有关代码、项目和流程的关键信息。垃圾文档可能会导致开发周期被延迟,因为码农需要花更多时间试图理解代码库、项目和流程。这会导致码农生产力降低。


在码农入职期间,提供垃圾文档会导致码农在设置项目、管理环境、理解代码等任务上花费更多时间。在缺乏清晰文档的情况下,维护和修改现有代码变得困难。由于担心破坏功能,码农可能会犹豫重构或进行更改。因此,码农的生产力将浪费在非核心任务上。


7. 痴心妄想的 Deadline


痴心妄想的Deadline


Deadline 是使码农发疯的原因之一。你必须在较短的时间窗口内完成大量工作时,你会很容易感到沮丧。这可能导致职业倦怠、代码质量差、疏忽 Code Review 等。这将导致技术债的积累。因此,码农的生产力会下降。


Deadline 对计划开发周期是必要的,但是通过设置不切实际的 Deadline 来向码农施加压力,会让他们承受压力。在压力下,整体生产力和代码质量都会下降。


总结


上文提到的会议、技术债积累、拖沓的 Code Review、微观管理、导致职业倦怠的压力、垃圾代码文档以及为项目设置不切实际的 Deadline 等因素会阻碍码农的生产力。我试图阐明软件开发人员在追求效率和创造性解决方案的过程中面临的挑战。


其中的重点是,这些挑战是可以通过在码农的健康和高生产力之间建立平衡来克服的。你可以使用一些码农工具来帮助管理你的生产力、深度专注和工作效率。


下面是一些可以帮助提高生产力的工具:




  • FocusGuard:这是一个 Chrome 扩展,可以通过屏蔽网站帮助你保持专注。




  • Code Time:这是一个 VSCode 扩展,用于跟踪你的编码时间和活动编码时间。




  • JavaScript Booster:这个 VSCode 扩展可以为代码重构提供建议。你也给其他编程语言找找这种扩展。




  • Hatica:虽然上述工具局限于一个任务:专注于编码,但 Hatica 可以处理更多工作。它通过改进工作流程、识别瓶颈和跟踪进度来帮助工程团队提高码农生产力。通过这种方式,它可以给码农节约大量的时间。在这里了解更多关于这个工程管理平台的信息。





作者:ssh_晨曦时梦见兮

来源:juejin.cn/post/7267578376050114614


收起阅读 »

类似chat-gpt的打字机效果

类似chat-gpt的打字机效果 展示效果: 实现思路:只要控制显示内容的长度就行了,每次加一点显示内容,然后一直播放闪烁动画,加载完了就停掉动画。 结论:单个字逐渐加载 + 闪烁动画 = 打字机效果 闪烁动画实现 通过css实现.cursor { ...
继续阅读 »

类似chat-gpt的打字机效果


展示效果:




实现思路:只要控制显示内容的长度就行了,每次加一点显示内容,然后一直播放闪烁动画,加载完了就停掉动画。


结论:单个字逐渐加载 + 闪烁动画 = 打字机效果


闪烁动画实现


通过css实现

.cursor {
position: absolute;
display: inline-block;
width: 2px;
height: 16px;
background-color: #000;
animation: blink 1s infinite;
transform: translate(2px, 3px);
}

@keyframes blink {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0;
}
}

展现效果:




完整代码

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>打字机效果</title>
<style>
.cursor {
position: absolute;
display: inline-block;
width: 2px;
height: 16px;
background-color: #000;
animation: blink 1s infinite;
transform: translate(2px, 3px);
}

@keyframes blink {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
.box1 {
line-height: 22px;
width: 300px;
font-size: 16px;
padding: 10px;
border: 1px solid pink;
margin-bottom: 10px;
min-height: 100px;
}
</style>
</head>
<body>
<div class="box1">
<span class="cursor"></span>
</div>
<button class="btn">添加文字</button>
<script>
const randomTextArr = ["萨嘎", '三', "agas", '大厦', '阿萨法施工', 'saf', '啊', '收到', '三个哈哈哈', '阿事实上事实上事实上', '事实上事实上少时诵诗书', '叫哦大家搜狗号度搜化工三打哈干撒的很尬山东干红手打很尬搜哈', '时间几节课MVvvvvvvvvvv啪啪啪啪啪啪PPT科技我IQ和瓦暖气,你', '撒啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊就你跟贵公司懂法守法收入与武器我先把发发花洒就你跟贵公司懂法守法收入与武器我先把发发花洒'];
const box1 = document.querySelector('.box1');
const btn = document.querySelector('.btn');
let showText = '';
let addTextArr = [];
let timer = null;

btn.onclick = () => {
getRandomText();
updateText();
}

function getRandomText() {
const randomTextArrLength = randomTextArr.length;
let randomNum = Math.random();
let addText = randomTextArr[Math.floor(Math.random() * randomTextArrLength)];
addTextArr.push(addText);
console.log(addText)
}

function updateText() {
let index = 0;
if (!timer) {
timer = setInterval(() => {
if (addTextArr.length > 0) {
if (index < addTextArr[0].length) {
box1.innerHTML = showText + addTextArr[0][index] + `<span></span>`;
showText += addTextArr[0][index];
index ++;
} else {
index = 0;
box1.innerHTML = showText;
addTextArr.shift();
}
} else {
clearInterval(timer);
timer = null;
}
}, 50)
}
}
</script>
</body>
</html>

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

让你兴奋不已的13个CSS技巧🤯

快来免费体验ChatGpt plus版本的,我们出的钱 体验地址:chat.waixingyun.cn 可以加入网站底部技术群,一起找bug,另外新版作图神器已上线 cube.waixingyun.cn/home 1.使用边框绘制一个三角形 在某些情况下,例如...
继续阅读 »

快来免费体验ChatGpt plus版本的,我们出的钱 体验地址:chat.waixingyun.cn 可以加入网站底部技术群,一起找bug,另外新版作图神器已上线 cube.waixingyun.cn/home


1.使用边框绘制一个三角形


在某些情况下,例如在工具提示中添加箭头指针时,如果你只需要简单的三角形,那么加载图片可能会过度。


仅使用CSS,您就可以通过边框创建一个三角形。


这是一个相当老的技巧。理想情况下,你会在一个宽度和高度都为零的元素上设置边框。所有的边框颜色都是透明的,除了那个将形成箭头的边框。例如,要创建一个向上指的箭头,底部边框是有颜色的,而左边和右边是透明的。无需包括顶部边框。边框的宽度决定了箭头的大小

.upwards-arrow {
width: 0;
height: 0;
border-left: 20px solid transparent;
border-right: 20px solid transparent;

border-bottom: 20px solid crimson;
}

这将创建一个像下面所示的向上指的箭头:




事例地址:codepen.io/chriscoyier…


2.交换元素的背景


z-index 属性规定了元素如何堆叠在其他定位元素上。有时,你可能会设置一个 z-index 属性让子元素的层级较低,结果却发现它隐藏在其父元素的背景之后。为了防止这种情况,你可以在父元素上创建一个新的堆叠上下文,防止子元素隐藏在其后面。创建堆叠上下文的一种方法是使用 isolation: isolate CSS样式声明。


我们可以利用这种堆叠上下文技术来创建悬停效果,该效果可以交换按钮的背景。例如:

button.join-now {
cursor: pointer;
border: none;
outline: none;
padding: 10px 15px;

position: relative;
background-color: #5dbea3;
isolation: isolate; /* If ommitted, child pseudo element will be stacked behind */
}

button.join-now::before {
content: "";
position: absolute;
background-color: #33b249;
top: 0;
left: 100%;
right: 0;
bottom: 0;
transition: left 500ms ease-out;

z-index: -1;
}

button.join-now:hover::before {
left: 0;
}

上述代码在鼠标悬停时交换了 button 的背景。背景的变化不会干扰前景的文本,如下面的gif所示:




3.将元素居中


可能,你已经知道如何使用 display: flex;display: grid; 来居中元素。然而,另一种不太受欢迎的在x轴上居中元素的方法是使用 text-align CSS属性。这个属性在居中文本时就能直接使用。要想在DOM中也居中其他元素,子元素需要有一个 inline 的显示。它可以是 inline-block 或任何其他内联...

div.parent {
text-align: center;
}

div.child {
display: inline-block;
}

4.药丸💊形状按钮


可以通过将按钮的边框半径设置为非常高的值来制作药丸形状的按钮。当然,边框半径应该高于按钮的高度。

button.btn {
border-radius: 80px; /* value higher than height of the button */
padding: 20px 30px;
background-color: #fdd835;
border: none;
color: black;
font-size: 20px;
}



按钮的高度可能会随着设计的改变而增加。因此,你会发现将 border-radius 设置为非常高的值是很方便的,这样无论按钮是否增大,你的css都能继续工作。


5.轻松为你的网站添加美观的加载指示器


对于开发者来说,将注意力转移到为你的网站创建一个美观的加载指示器上往往是一项乏味的任务。这种关注力更好地用于构建项目的其他重要部分,这些部分值得我们去关注。


当你在阅读时,很可能你也觉得这是个令人烦恼的难题。这就是为什么我花时间为你消除这个障碍,并精心准备了一个装有加载指示器的库,让你可以在你的梦想项目中“即插即用”。这是一个完整的集合,你只需要挑选出那个能点燃你心中火花💖的。只需看看这个库的简单用法,源代码在Github上可用。别忘了给个星星⭐


地址:http://www.npmjs.com/package/rea…




6.简易暗色或亮色模式


您只需要几行CSS代码,就可以在我们的网站上启用深色/浅色模式。您只需让浏览器知道,您的网站可以在系统的深色/浅色模式下正确显示。

html {
color-scheme: light dark;
}

注意: color-scheme 属性可以设置在除 html 之外的任何DOM元素上。


然后通过我们的网站设置控制背景颜色和文字颜色的变量,通过检查浏览器支持使其更加防弹:

html {
--bg-color: #ffffff;
--txt-color: #000000;
}

@supports (background-color: Canvas) and (color: CanvasText) {
:root {
--bg-color: Canvas;
--txt-color: CanvasText;
}
}

注意:如果你不在元素上设置 background-color ,它将继承浏览器定义的与深色/浅色主题匹配的系统颜色。这些系统颜色在不同的浏览器之间可能会有所不同。


明确设置 background-color 可以与 prefers-color-scheme 结合使用,以提供与浏览器默认设置不同的颜色阴影。


以下是暗/亮模式的实际应用。用户的偏好在暗模式和亮模式之间进行模拟。




7.使用省略号( ... )截断溢出的文本


这个技巧已经存在一段时间,用于美观地修剪长文本。但你可能仍然错过了它。你只需要以下的CSS:

p.intro {
width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

只需实施以下规则:



  • 明确的宽度,因此剪裁的边界将永远被达到。

  • 浏览器会将超出元素宽度的长文本进行换行。所以你需要阻止这种情况: white-space: nowrap; 。

  • 溢出的内容应被剪裁: overflow: hidden; 。

  • 当文本即将被剪切时,用省略号( ... )填充字符串: text-overflow: ellipsis; 。


结果看起来像这样:




8.将长文本截断为若干行


这与上述技巧略有不同。这次,文本被剪裁,将内容限制为一定的行数。

p.intro {
width: 300px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3; /* Truncate when no. of lines exceed 3 */
overflow: hidden;
}

输出看起来像这样:




9. 停止过度劳累自己写作 toprightbottomleft


在处理定位元素时,你通常会编写如下代码:

.some-element {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}

这可以通过使用 inset 属性来简化:

.some-element {
position: absolute;
inset: 0;
}

或者,如果你对 toprightbottomleft 有不同的值,你可以按照如下的顺序分别设置它们: inset: -10px 0px -10px 0px这种简写方式与margin 的工作方式相同。


10.提供优化过的图片


请尝试在浏览器的开发者工具中将网络速度调整到较慢,然后访问一个由高清图片组成的网站,比如 unsplash。这就是你的网站访客在网络速度较慢的地理区域尝试欣赏你的高清内容时所经历的痛苦。


但你可以通过 image-set CSS 技巧提供一种解救方法。


可以为浏览器提供选项,让它决定最适合用户设备的图片。例如:

.banner {
background-image: url("elephant.png"),
background-image: -webkit-image-set(
url("elephant.webp") type("image/webp") 1x,
url("elephantHD.webp") type("image/webp") 2x,
url("elephant.png") type("image/png") 1x,
url("elephantHD.png") type("image/png") 2x
);
}

上述代码将设置元素的背景图像。


如果支持 -webkit-image-set ,那么背景图像将会是一种优化的图像,也就是说,这将是一种支持的MIME类型的图像,且更适合用户设备的分辨率能力。


例如:由于更高质量的图像直接与更大的尺寸成正比,所以在网络状况差的情况下使用高分辨率设备的用户,会促使浏览器决定提供支持的低分辨率图像。让用户等待高清图像加载是不合逻辑的。


11. 计数器


你不必纠结于浏览器如何渲染编号列表。你可以利用 counters() 实现你自己的设计。以下是操作方法:

ul {
margin: 0;
font-family: sans-serif;

/* Define & Initialize Counter */
counter-reset: list 0;
}

ul li {
list-style: none;
}

ul li:before {
padding: 5px;
margin: 0 8px 5px 0px;
display: inline-block;
background: skyblue;
border-radius: 50%;
font-weight: 100;
font-size: 0.75rem;

/* Increment counter by 1 */
counter-increment: list 1;
/* Show incremented count padded with `.` */
content: counter(list) ".";
}



12.表单验证视觉提示


仅使用CSS,您就可以向用户显示有关表单输入有效性的视觉提示。我们可以在表单元素上使用 :valid:invalid CSS伪类,当其内容验证成功或失败时,应用适当的样式。


请考虑以下HTML页面结构:

<!-- Regex in pattern attribute means input can accept `firstName Lastname` (whitespace sepearated names) -->
<!-- And invalidates any other symbols like `*` -->
<input
type="text"
pattern="([a-zA-Z0-9]\s?)+"
placeholder="Enter full name"
required
/>
<span></span>

<span> 将用于显示验证结果。以下的CSS根据其验证结果来设置输入框的样式:

input + span {
position: relative;
}

input + span::before {
position: absolute;
right: -20px;
bottom: 0;
}

input:not(:placeholder-shown):invalid {
border: 2px solid red;
}

input:not(:placeholder-shown):invalid + span::before {
content: "✖";
color: red;
}

input:not(:placeholder-shown):valid + span::before {
content: "✓";
color: green;
}

地址:codepen.io/hane-smitte…


13. 一键选择文本


这个技巧主要是为了提升网站用户的复制和粘贴体验。使用 user-select: all ,可以通过一键实现简单的文本选择。所有位于该元素下方的文本节点都会被选中。


另一方面,可以使用 user-select: none; 来禁用文本选择。禁用文本选择的另一种方法是将文本放在 ::before::after CSS伪元素的 content: ''; 属性中。


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

谈谈国内前端的三大怪啖

web
因为工作的原因,我和一些外国前端开发有些交流。他们对于国内环境不了解,有时候会问出一些有趣的问题,大概是这些问题的启发,让我反复在思考一些更为深入的问题。 今天聊三个事情: 小程序 微前端 模块加载 小程序 每个行业都有一把银座,当坐上那把银座时,做什么...
继续阅读 »

因为工作的原因,我和一些外国前端开发有些交流。他们对于国内环境不了解,有时候会问出一些有趣的问题,大概是这些问题的启发,让我反复在思考一些更为深入的问题。


今天聊三个事情:



  • 小程序

  • 微前端

  • 模块加载


小程序



每个行业都有一把银座,当坐上那把银座时,做什么便都是对的。



“我们为什么需要小程序?”


第一次被问到这个问题,是因为一个法国的同事。他被派去做一个移动端业务,刚好那个业务是采用小程序在做。于是一个法国小哥就在被痛苦的中文文档和黑盒逻辑中来回折磨着 🤦。


于是,当我们在有一次交流中,他问出了我这个问题:我们为什么需要小程序?


说实话,我试图解释了 19 年国内的现状,以及微信小程序推出时候所带来的便利和体验等等。总之,在我看来并不够深刻的见解。


即便到现在为止,每次当我使用小程序的时候,依旧会复现这个问题。在 ChatGPT 11 月份出来的时候,我也问了它这个很有国内特色的问题:





看起来它回答的还算不错,至少我想如果它来糊弄那些老外,应该会比我做的更好些。


但如果扪心自问,单从技术上来讲。以上这些事情,一定是有其他方案能解决的。


所以从某种程度上来看,这更像是一场截胡的商业案例:


应用市场

全世界的互联网人都知道应用市场是非常有价值的事情,可以说操作系统最值钱的部分就在于他们构建了自己的应用市场。


只要应用在这里注册发行,雁过拔毛,这家公司就是互联网世界里的统治阶级,规则的制定者。


反之则需要受制于人,APP 做的再大,也只是应用市场里的一个应用,做的好与坏还得让应用商店的评判。


另外,不得不承认的是,一个庞大如苹果谷歌这样的公司,他们的应用商店对于普通国内开发者来说,确实是有门槛的。


在国内海量的 APP 需求来临之前,能否提供一个更低成本的解决方案,来消化这些公司的投资?


毕竟不是所有的小企业都需要 APP,其实他们大部分需求 Web 就可以解决,但是 Web 没牌面啊,做 Web 还得砸搜索的钱才有流量。(某度搜索又做成那样...)


那做操作系统?太不容易,那么多人都溺死在水里了,这水太深。


那有没有一种办法可以既能构建生态,又有 APP 的心智,还能给入驻企业提供流量?


于是,在 19 年夏天,深圳滨海大厦下的软件展业基地里,每天都在轮番播放着,做 XX小程序,拥抱下一个风口...


全新体验心智

小程序用起来挺方便的。


你有没有想过,这些美妙感觉的具体都来自哪些?以及这些真的是 Web 技术本身无法提供的吗?



  1. 靠谱感,每个小程序都有约束和规范,于是你可以将他们整整齐齐的陈放在你的列表里,仿佛你已经拥有了这一个个精心雕琢的作品,相对于一条条记不住的网页地址和鱼龙混杂的网页内容来说,这让你觉得小程序更加的有分量和靠谱。

  2. 安全感,沉浸式的头部,没有一闪而过的加载条,这一切无打扰的设计,都让你觉得这是一个在你本地的 APP,而不是随时可能丢失网页。你不会因为网速白屏而感到焦虑,尽管网络差的时候,你的 KFC 依旧下不了单 😂

  3. 沉浸感,我不知道是否打开网页时顶部黑黑的状态栏是故意留下的,还是不小心的... 这些限制都让用户非常强烈的意识到这是一个网页而不是 APP,而小程序中虽然上面也存在一个空间的空白,但是却可以被更加沉浸的主题色和氛围图替代。网页这个需求做不了?我不信。


H5小程序


  1. 顺滑感,得益于 Native 的容器实现,小程序在所有的视图切换时,都可以表现出于原生应用一样的顺滑感。其实这个问题才是在很多 Hybrid 应用中,主要想借助客户端来处理和解决的问题。类似容器预开、容器切换等技术是可以解决相关问题的,只是还没有一个标准。


我这里没有提性能,说实话我不认为性能在小程序中是有优势的(Native 调用除外,如地图等,不是一个讨论范畴)。作为普通用户,我们感受到的只是离线加载下带来的顺滑而已。


而上述提到的许多优势,这对于一个高品质的 Web 应用来说是可以做得到的,但注意这里是高品质的 Web 应用。而这种“高品质”在小程序这里,只是入驻门槛而已。


心智,这个词,听起来很黑话,但却很恰当。当小程序通过长期这样的筛选,所沉淀出来一批这样品质的应用时。就会让每个用户即便在还没打开一个新的小程序之前,也有不错体验的心理预期。这就是心智,一种感觉跟 Web 不一样,跟 APP 有点像的心智。


打造心智,这件事情好像就是国内互联网企业最擅长做的事情,总是能从一些细微的差别中开辟一条独立的领域,然后不断强化灌输本来就不大的差异,等流量起来再去捞钱变现。




我总是感觉现在的互利网充斥着如何赚钱的想法,好像永远赚不够。“赚钱”这个事情,在这些公司眼里就是圈人圈地抢资源,看看谁先占得先机,别人有的我也得有,这好像是最重要的事情。


很少有企业在思考如何创造些没有的市场,创造些真正对技术发展有贡献,对社会发展有推动作用的事情。所以在国内互联网圈也充斥着一种奇怪的价值观,有技术的不一定比赚过钱的受待见。


管你是 PHP 还是 GO,管你是在做游戏还是直播带货,只要赚到钱就是高人。并且端的是理所应当、理直气壮,有些老板甚至把拍摄满屋子的程序员为自己打工作为一种乐趣。什么情怀、什么优雅、什么愿景,人生就俩字:搞钱。


不是故意高雅,赚钱这件事情本身不寒碜,只是在已经赚到盆满钵满、一家独大的时候还在只是想着赚更多的钱,好像赚钱的目的就是为了赚钱一样,这就有点不合适。企业到一定程度是要有社会责任的,龙头企业每一个决定和举措,都有会影响接下来的几年里这个行业的价值观走向。



当然也不是完全没有好格局的企业,我非常珍惜每一个值得尊重的中国企业,来自一个蔚来车主。



小程序在商业上固然是成功的,但吃的红利可以说还是来自 网页 到 应用 的心智变革。将本来流向 APP 的红利,截在了小程序生态里。


但对于技术生态的发展却是带来了无数条新的岔路,小程序的玩法就决定了它必须生长在某个巨型应用里面,不论是用户数据获取、还是 API 的调用,其实都是取决于应用容器的标准规范。


不同公司和应用之间则必然会产生差异,并且这种差异是墒增式的差异,只会随着时间的推移越变越大。如果每个企业都只关注到自己业务的增长,无外部约束的话,企业必然会根据自己的业务发展和政策需要,选择成本较低的调整 API,甚至会故意制造一些壁垒来增加这种差异。


小程序,应该是 浏览器 与 操作系统 的融合,这本应该是推动这两项技术操刀解决的事情。


微前端


qiankun、wujie、single-spa 是近两年火遍前端的技术方案,同样一个问题:我们为什么需要微前端?


我不确定是否每个在使用这项技术的前端都想清楚了这个问题,但至少在我面试过的候选人中,我很少遇到对自己项目中已经在使用的微前端,有很深的思考和理解的人。


先说下我的看法:



  1. 微前端,重在解决项目管理而不在用户体验。

  2. 微前端,解决不了该优化和需要规范的问题。

  3. 微前端,在挽救没想清楚 MPA 的 SPA 项目。


没有万能银弹



银色子弹(英文:Silver Bullet),或者称“银弹”“银质子弹”,指由纯银质或镀银的子弹。在欧洲民间传说及19世纪以来哥特小说风潮的影响下,银色子弹往往被描绘成具有驱魔功效的武器,是针对狼人、吸血鬼等超自然怪物的特效武器。后来也被比喻为具有极端有效性的解决方法,作为杀手锏、最强杀招、王牌等的代称。



所有技术的发展都是建立在前一项技术的基础之上,但技术依赖的选择过程中一定需要保留第一性原理的意识。


当 React、Vue 兴起,当 打包技术(Webpack) 兴起,当 网页应用(SPA) 兴起,这些杰出的技术突破都在不同场景和领域中给行业提供了新的思路、新的方案。


不知从何时开始,前端除了 div 竟说不出其他的标签(还有说 View 的),项目中再也不会考虑给一个通用的 class 解决通用样式问题。


不知从何时开始,有没有权限需要等到 API 请求过后才知道,没有权限的话再把页面跳转过去申请。


不知从何时开始,大家的页面都放在了一个项目里,两个这样的巨石应用融合竟然变成了一件困难的事。


上面这些不合理的现状,都是在不同的场景下,不思考适不适合,单一信奉 “一招吃遍天” 下演化出的问题。


B 端应用,是否应该使用 SPA? 这其实是一个需要思考的问题。


微前端从某种程度上来讲,是认准 SPA 了必须是互联网下一代应用标准的产物,好像有了 SPA 以后,MPA 就变得一文不值。甭管页面是移动端的还是PC的;甭管页面是面对 C 端的还是 B 端的;甭管一个系统是有 20 个页面还是 200 个页面,一律行这套逻辑。


SPA 不是万能银弹,React 不是万能银弹,Tailwind 不是万能银弹。在新技术出现的时候,保持热情也要保持克制。



ps. 我也十分痛恨 React 带来的这种垄断式的生态,一个 React 组件将 HTML 和 Style 都吃进去,让你即使在做一个移动端的纯展示页面时,也需要背上这些称重的负担。



质疑 “墨守成规”,打开视野,深度把玩,理性消费。


分而治之


分治法,一个很基本的工程思维。


在我看来在一个正常商业迭代项目中的主要维护者,最好不要超过 3 个人,注意是主要维护者(Maintainer) 。


你应该让每个项目都有清晰的责任人,而不是某行代码,某个模块。责任人的理解是有归属感,有边界感的那种,不是口头意义上的责任人。(一些公司喜欢搞这种虚头巴脑的事情,什么连坐…)


我想大部分想引入微前端的需求都是类似 如何更好的划分项目边界,同时保留更好的团队协同。


比如 导航菜单 应该是独立收口独立管理的,并且当其更新时,应该同时应用于所有页面中。类似的还有 环境变量、时区、主题、监控及埋点。微前端将这些归纳在主应用中。


而具体的页面内容,则由对应的业务进行开发子应用,最后想办法将路由关系注册进主应用即可。


当然这样纯前端的应用切换,还会出现不同应用之间的全局变量差异、样式污染等问题,需要提供完善的沙箱容器、隔离环境、应用之间通信等一系列问题,这里不展开。


当微前端做到这一部分的时候,我不禁在想,这好像是在用 JavaScript 实现一个浏览器的运行容器。这种本应该浏览器本身该做的事情,难道 JS 可以做的更好?


只是做到更好的项目拆分,组织协同的话,引入后端服务,由后端管控路由表和页面规则,将页面直接做成 MPA,这个方案或许并不比引入微前端成本高多少。


体验差异


从 SPA 再回 MPA,说了半天不又回去了么。


所以不防想想:在 B端 业务中使用 SPA 的优势在哪里?


流畅的用户体验:

这个话题其实涵盖范围很广,对于 SPA 能带来的 “流畅体验”,对于大多数情况下是指:导航菜单不变,内容变化 发生变化,页面切换中间不出现白屏


但要做到这个点,其实对于 MPA 其实并没有那么困难,你只需要保证你的 FCP 在 500ms 以内就行。



以上的页面切换全部都是 MPA 的多页面切换,我们只是简单做了导航菜单的 拆分 和 SWR,并没有什么特殊的 preload、prefetch 处理,就得到了这样的效果。


因为浏览器本身在页面切换时会在 unload 之前先 hold 当前页面的视图不变,发起一下一个 document 的请求,当页面的视图渲染做到足够快和界面结构稳定就可以得到这样的效果。



这项浏览器的优化手段我找了很久,想找一篇关于它的博文介绍,但确实没找到相关内容,所以 500ms 也是我的一个大致测算,如果你知道相关的内容,可以在评论区补充,不胜感激。



所以从这个角度来看,浏览器本身就在尽最大的努力做这些优化,并且他们的优化会更底层、更有效的。


离线访问 (PWA)

SPA 确实会有更好的 PWA 组织能力,一个完整的 SPA 应用甚至可以只针对编译层做改动就可以支持 PWA 能力。


但如果看微前端下的 SPA 应用,需要支持 PWA 那就同样需要分析各子应用之间的元数据,定制 Service Worker。这种组织关系和定制 SW,对于元数据对于数据是来自前端还是后端,并不在意。


也就是说微前端模式下的 PWA,同样的投入成本,把页面都管理在后端服务中的 MPA 应用也是可以做到相同效果的。


项目协同、代码复用

有人说 SPA 项目下,项目中的组件、代码片段是可以相互之间复用的,在 MPA 下就相对麻烦。


这其实涉及到项目划分的领域,还是要看具体的需求也业务复杂度来定。如果说整个系统就是二三十个页面,这做成 SPA 使用前端接管路由高效简单,无可厚非。


但如果你本身在面对的是一个服务于复杂业务的 B 端系统,比如类似 阿里云、教务系统、ERP 系统或者一些大型内部系统,这种往往需要多团队合作开发。这种时候就需要跟完善的项目划分、组织协同和系统整合的方案。


这个时候 SPA 所体现出的优势在这样的诉求下就会相对较弱,在同等投入的情况下 MPA 的方案反而会有更少的执行成本。



也不是所有项目一开始就会想的那么清楚,或许一开始的时候就是个简单的 SPA 项目,但是随着项目的不断迭代,才变成了一个个复杂的巨石应用,现在如果再拆出来也会有许多迁移成本。引入微前端,则可以...



这大概是许多微前端项目启动的背景介绍,我想说的是:对于屎山,我从来不信奉“四两拨千斤”


如果没有想好当下的核心问题,就引入新的“银弹”解决问题,只会是屎山雕花。


项目协同,抽象和复用这些本身不是微前端该解决的问题,这是综合因素影响下的历史背景问题。也是需要一个个资深工程师来把控和解决的核心问题,就是需要面对不同的场景给出不同的治理方案。


这个道理跟防沙治沙一样,哪有那么多一蹴而就、立竿见影的好事。


模块加载


模块加载这件事情,从玉伯大佬的成名作 sea.js 开始就是一个非常值得探讨的问题。在当时 jQuery 的时代里,这是一个绝对超前的项目,我也在实际业务中体会过在无编译的环境下 sea.js 的便捷。



实际上,不论是微前端、低代码、会场搭建等热门话题离不开这项技术基础。


import * from * 我们每天都在用,但最终的产物往往是一个自运行的 JS Bundle,这来自于 Webpack、Vite 等编译技术的发展。让我们可以更好的组织项目结构,以构建更复杂的前端应用。


模块的概念用久了,就会自然而然的在遇到浏览器环境中,遇到动态模块加载的需求时,想到这种类似模块加载的能力。


比如在遇到会场千奇百怪的个性化营销需求时,能否将模块的 Props 开放出来,给到非技术人员,以更加灵活的方式让他们去做自由组合。


比如在低代码平台中,让开发者自定义扩展组件,动态的将开发好的组件注册进低代码平台中,以支持更加个性的需求。


在万物皆组件的思想影响下,把一整个完整页面都看做一个组件也不是不可以。于是在一些团队中,甚至提倡所有页面都可以搭建、搭建不了的就做一个大的页面组件。这样及可以减少维护运行时的成本,又可以统一管控和优化,岂不美哉。


当这样大一统的“天才方案”逐渐发展成为标准后,也一定会出现一些特殊场景无法使用,但没关系,这些天才设计者肯定会提供一种更加天才的扩展方案出来。比如插件,比如扩展,比如 IF ELSE。再后来,就会有性能优化了,而优化的 追赶对象 就是用原来那种简单直出的方案。


有没有发现,这好像是在轮回式的做着自己出的数学题,一道接一道,仿佛将 1 + 1的结论重新演化了一遍。



题外话,我曾经自己实现过一套通过 JSON Schema 描述 React 结构的 “库” ,用于一个低代码核心层的渲染。在我的实现过程中,我越发觉得我在做一件把 JSX 翻译成 JS 的事情,但 JSX 或者 HTML 本身不就是一种 DSL 么。为什么一定要把它翻译成 JSON 来传输呢?或者说这样的封装其本身有意义么?这不就是在做 PHP、ASP 直接返回带有数据的 HTML Ajax 一样的事情么。




传统的浏览器运行环境下要实现一个模块加载器,无非是在全局挂载一个注册器,通过 Script 插入一段新的 JS,该 JS 通过特殊的头尾协议,将运行时的代码声明成一个函数,注册进事先挂载好的注册器。


但实际的实现场景往往要比这复杂的多,也有一些问题是这种非原生方式无法攻克的问题。比如全局注册器的命名冲突;同模块不同版本的加载冲突;并发加载下的时序问题;多次模块加载的缓存问题 等等等等等...


到最后发现,这些事情好像又是在用 JS 做浏览器该做的事情。然而浏览器果然就做了,<script type="module"></script>,Vite 就主要采用这种模式实现了它 1 年之内让各大知名项目切换到 Vite 的壮举。


“但我们用不了,有兼容性问题。”


哇哦,当我看着大家随意写出的 display: grid 样式定义,不禁再次感叹人们对未知的恐惧。



import.meta 的兼容性是另外一个版本,是需要 iOS12 以上,详情参考:caniuse.com/?search=imp…


试想一下,现在的低代码、会场搭建等等各类场景的模块加载部分,如果都直接采用 ESM 的形式处理,这对于整个前端生态和开发体验来说会有多么大的提升。


模块加载,时至今日,本来就已经不再需要 loader。 正如 seajs 中写的:前端模块化开发那点历史



历史不是过去,历史正在上演。随着 W3C 等规范、以及浏览器的飞速发展,前端的模块化开发会逐步成为基础设施。一切终究都会成为历史,未来会更好。



结语


文章的结尾,我想感叹另外一件事,国人为什么一定要有自己的操作系统?为什么一定需要参与到一些规范的制定中?


因为我们的智慧需要有开花的土壤。如果没有自己掌握核心技术,就是只能在问题出现的时候用另类的方式来解决。最后在一番折腾后,发现更底层的技术只要稍稍一改就可以实现的更好。这就像三体中提到的 “智子” 一样,不断在影响着我们前进的动力和方向。


不论是小程序、微前端还是模块加载。试想一下,如果我们有自己的互联网底蕴,能决定或者影响操作系统和浏览器的底层能力。这些 “怪啖” 要么不会出现,要么就是人类的科技创新。



希望未来技术人不用再追逐 Write Once, Run Everywhere 的事情...



作者:YeeWang
来源:juejin.cn/post/7267091810366488632
收起阅读 »

最新前端技术趋势

前端的车轮滚滚向前,轮子造的越来越圆,速度造的越来越快,每个人都在适应这个轮子的节奏,稍微不注意就会被甩出车轮之外。狼狈不堪之外还会发自心底的大喊一声:别卷了!! 话虽这么说,但现实就是这样,无论是客观还是主观因素都不得不让你继续的往前走。既然是往前走, 那么...
继续阅读 »

前端的车轮滚滚向前,轮子造的越来越圆,速度造的越来越快,每个人都在适应这个轮子的节奏,稍微不注意就会被甩出车轮之外。狼狈不堪之外还会发自心底的大喊一声:别卷了!!


话虽这么说,但现实就是这样,无论是客观还是主观因素都不得不让你继续的往前走。既然是往前走,
那么能知道一些前面有啥东西岂不是更好,也许能少走弯路。



自己对前端23年大概的技术做了一些展望,想到什么写什么。毕竟谁都不知道会不会突然间又出了个frontEndGPT打翻了所有人的饭碗。



1 AI


最先说的肯定是AI,22年末,23年初的chatgpt让AI话题火的一塌糊涂,同时也被认为是一次重大的技术革新,技术革新带来的就是重塑,一切都要被重塑,你的职业,你的工作。
视觉层面的stable diffusionmidjourney已经对设计师产生了重大影响,而涉及到视觉ui层面的话,前端肯定是绕不开的部分。虽然目前没有直接的对前端产生影响,但下面的就一个就不只是对前端了,而是对整个程序员都产生了影响。

  • copilot


Your AI pair programmer. Trained on billions of lines of code, GitHub Copilot turns natural language prompts into coding suggestions across dozens of languages.


看看醒目的文字就知道,程序员或多或少都会被影响了。


还有什么CodeWhisperer, Cursor等也都是AI辅助编程。


更有FrontyLocofy等将图片AI分析为HTML文档,无代码快速建站,figma快速解析成代码等,虽然是提效不少,但谁能说这些不是对前端的一种重塑呢。


2 主流框架


随着React,Vue等框架进一步的普及,现在前端想要脱离它们的场景越来越少了。那么它们的下一步规划,也会对我们产生一些不小的影响。


react


react 18以后,react似乎是对create-react-app这种项目启动方式也不怎么主推了,毕竟速度摆在那里,没有任何优势。而使用直接竞争对手的产品似乎又不太合适,而直接说又不用又显得跟开源精神不吻合(当然竞争对手的一些基本特性也确实和现有的构建思想不太吻合)。


他们似乎采取一种围魏救赵的方式,着重宣传next的方式。next不仅仅是一个ssr的框架,同时它也支持csr,ssg等不同的方式(next13开始,对于客户端组件和服务端组件可以有了比较好的区分)。同时next与react有着千丝万缕的联系,而next正在进行一个新的构建工具的替换。
next采用了turbopack,也是Webpack作者TobiasKoppers的作品,官方说它更新速度比Vite也要快10倍、比Webpack快700倍


而react也应该大概率会引入turbopack(当然它如果继续搞前端脚手架的话)。当然也可能会直接使用next环境。


至于快多少,以及评价基准等我们可以看下turbopack真比Vite快10倍吗?


next


next最新版本也加入了很多的特性,比如server component理念,约定式路由的更改,流式渲染,客户端组件与服务端组件分离更简单,更好的构建速度等等功能。可以让开发体验,用户体验更好,性能也会有响应的提升。


vite


这个不用说了,优秀的构建速度以及越来越丰富的社区,让其在22年有了很大提升。随着浏览器的逐步升级,23年vite肯定也会是重大的一年。


webpack


虽然5有了好多的功能提升,不过速度似乎一直是一个绕不过去的坎。就连作者也已经开始搞turbopack了,虽然加入swc能让编译有很大提升,但是目前从我身边的人的了解看,越来越多的人开始转向vite等其它方式了


turbopack


是webpack作者去的新公司开发的一款基于rust的打包工具。官方明确说明就是为了替换webpack。 同时强调webpack是这十年最火的工具,那turbopack就定位成未来几年的工具。由于作者和webpack, Vercel, next, React这些之间千丝万缕的联系,很难不说未来React也许会和这个打包工具绑定上。下面是官方提供的速度参考




而除了turbopack外,同一团队还在做Turborepo


这是一款项目管理的工具,最主要的面向场景是Monorepo这种复杂的多项目管理


Monorepo有很多优势,但是在多个项目中会有很多复杂的构建过程和相对闭塞的构建步骤,每一次上线都是耗时严重。所以Turborepo是为了解决这个问题出现的,让一些构建重复的构建步骤提炼出来,基于整个Monorepo项目的维度来管理多个子项目。


同时对于单个CI的构建步骤,解决每台机器,每个人都要单独构建的问题,还提炼了类似store的方案,脱离了构建环境,只跟项目绑定,当然这个方案是否使用要看你自己,毕竟原理是把构建产物放到第三方储存,而第三方又不是一个类似npm的开源机构。


3 服务端


node从7,8年前的爆火,到现在的不温不火,前端语言介入服务端这个命题似乎现在是越来越清晰了。那就是定位,可以做网关,可以做转发,可以做一些数据代理合并,定位清晰node依然有自己的使用场景。
node也马上到了20版本,迎来了一些特性

  • esm的更好支持,

  • 测试功能更加丰富

  • V8 引擎更新至 11.3,与Chromium113版本大部分相同

  • 支持以虚拟机的方式,动态运行js代码

  • WebAssembly的支持(实验性的)


于此同时,node曾经的作者Ryan Dahl几年前搞的Deno似乎没有太多的消息,似乎在向商业化方向前进。打造出Deno Deploy及其即时边缘渲染SSR框架Deno Fresh


4 其它


WebAssembly, 元框架,ts,微前端?


引用:


http://www.infoq.cn/article/9qu…
turbo.build/pack/docs/w…
http://www.robinwieruch.de/web-develop…


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

2023 了,不会还要做官网吧!

web
随着 AIGC 的兴起,移动互联网仿佛一瞬间也不香了,吃到红利的头部应用还在稳坐泰山,后起者在各种 xx 已死的声浪中,不知所措,信心尽失。流量的走势左右了太多人的思绪,太多的决策,所以当你在 2023 年,PC 互联网已经凉到不能再凉的时候,接到一个官网的需...
继续阅读 »

随着 AIGC 的兴起,移动互联网仿佛一瞬间也不香了,吃到红利的头部应用还在稳坐泰山,后起者在各种 xx 已死的声浪中,不知所措,信心尽失。流量的走势左右了太多人的思绪,太多的决策,所以当你在 2023 年,PC 互联网已经凉到不能再凉的时候,接到一个官网的需求,你会怎么去做呢,耐心看完,希望对你有所帮助。


官网开发作为前端的传统艺能已经诞生很久了,与现在主流的 react/vuecsr 框架开发不同,官网对 seo 有一定要求,所以咱们的网页不再是开局一个空 html,渲染全靠 js 了。传统的官网开发,一般采用 WordPress 建站,很好的技术栈 php + html/css/js 前后台一把梭,方便快捷,架构成熟。不过当遇到公司的前后端技术栈是 java + react 时,这个方案显然对开发人员并不友好,作为一名思维跳脱,勇于承担的前端,灵光乍现,我们是不是可以自己独立的完成整个官网呢。


在掘金上搜了搜,居然还有现成的课程,看看时间还挺新的,不如就从它入手吧。




作为网站尊贵的会员,我用兑换券换到了这门课程,开始了如饥似渴的学习。文章内容还是比较浅显易懂的,跟着操作下一个网站的雏形就出现了。课程中的技术栈主要采用 next.js + strapi 完成对官网的开发,相信各位看官并不陌生,业界知名的 react ssr 框架和 headless cms 系统,基础的介绍就不多说了,来分享点实操中得到经验教训吧。


next.js 相关


收获到的知识


1. 如何调试 next.js 服务端渲染的逻辑


之前,我只会使用 console.log 在启动的命令行里去判断代码的问题,现在我知道了使用 cross-env NODE_OPTIONS='--inspect' next dev 命令,他会开启一个 nodejsinspect 调试窗口,因为 next.js 的系统对象都很复杂,层级深,如果 console.log 的话很觉得很烦人,通过如上的方式就能便捷。






2. 生成有参数的静态页面的方法


其实像官网流量挺小的,没有必要走静态导出的 ssg ,使用 ssr 也完全没有问题,但是鉴于对 k8s 扩容,服务器费用之类的知识不太了解,官网的迭代频率也很低,所以本次采用了 ssg 的方案,要求所有子页面都要生成, 这就不得不提到内置的 api名为 getStaticPaths了。他会在打包阶段,先从一个列表接口拿到所有的id,再执行 getStaticProps 中的逻辑,拿到所有详情数据。比如我们的故事详情页,共有两条数据,id为1,2,执行完后,就会在对应目录下生成两个 html






得到的经验教训


1. 移动端适配


一个简易的判断移动端的代码很容易写,先不纠结完备性,大致如下:

/mobile|android|iphone|ipad|phone/i.test(window.navigator.userAgent.toLowerCase())

然而在 next.jsssg 方案中你却很难拿到,因为 render 里不能有对 window 的直接使用。我想了很多取巧的方法,但是都不行,如果是 ssr 还可以用请求头的 user-agent 来判断,作为属性透传到组件中,如课程一样。但 ssg 你只能使用响应式,同时渲染2套布局,才是最简单的做法。仔细想想,因为 html 字符串的构建是在 build 的时候就已经决定了,所以不能动态的拼接成不同的 dom 也就很好理解了。



PS: 感谢 @wonderL17 指出,也可以使用nginx配置的方式,将对应路径转发到为移动端生成的页面。
如果页面复用逻辑多,且能很好支持响应式,可以使用响应式。如果完全是另一套风格,则可以使用配置ng方式去处理,更为优秀,代码会更好维护且生成的页面更小。



2. api目录的处理


next.js 中的 api 目录可以承担接口的功能,课程中在 api 层进行了一次中转,我对此呈保留意见,想法如下:

  • 如果 strapi 不能直接支持一定的并发量,那么使用 api 来代理请求一样不能达到。

  • 中间层越多,可能出现的协作编码问题就越多,比如 strapi 返回的数据不符合预期,有些人会去修改 strapi,有些人会去改 next.js 的 api,这样会造成项目维护的不统一。


其实简单的对 strapi 加个跨域的配置,就能很好的直接在每个组件内使用,这对于一般的官网是完全可以满足的,而且代码清晰整洁。


strapi 相关


strapi 暂时没有给我带来新的知识,因为没有对他的原理进行研究,最核心的功能,编辑类型,即可生成对应的增删改查界面是最值得学习的部分,可是也是他最成熟的部分,开发嘛,能用就行,所以只有使用上的踩坑记录。但不得不说,strapi 的模块设计的挺不错的(除开dashboard ui定制外),有兴趣的可以了解下代码原理和设计思路。


得到的经验教训


1. 数据库(!!!!!)


请一定不要用 sqlite,不然你会哭的。因为项目肯定是需要多人协作的,强大如你 merge 代码对你来说轻而易举,但是你会 merge db 文件吗?相信我,你不会。


所以不要等项目已成,你才想起来去换个数据库,那么你即将面临,如果将 sqlite 数据导出成 postgresql 或者 mysql 数据的问题,听上去并不难吧,我花了 1~2 天时间才把这个无意义的工作做完。虽然能查到很多方案,但开源的不好用,付费的不想试,还是老老实实导出 sql,再导入是最快的。(不详述了,都是辛酸,你懂的,装了一堆东西,要么环境问题,要么功能问题,迟迟无法完成迁移的痛)


所以一开始就选好数据库,很关键,这样既方便了协作开发,又方便了后续使用。因为 4.x? 具体是几不清楚,relations 排序不生效(postgresql | mysql),sqlite 没有这个问题,所以替换数据库后,升级下 strapi,我升级的是4.10.6,顺便可以把依赖里的 sharp 删掉,有时候要装很久。


2. OSS(!!!!)


oss 一样至关重要,如果你使用了 strapi 的媒体库功能,请一定先配置好自定义的 oss,使用这个库就好了 strapi-provider-upload-oss,配置起来很方便,如果你头铁,一开始就是不配,你将面临,将上传好的图片删除,再重新上传一遍,没错他没有批量替换的功能,只能手动操作,我也没有花很久,1个小时,重新换了70~80张图片(关键素材上次用完还删了)。


配置如下:

// config/plugins.js

module.exports = ({ env }) => ({
upload: {
config: {
provider: "strapi-provider-upload-oss", // full package name is required
providerOptions: {
accessKeyId: "xxxx", // 用你的 oss 配置把 xxx 换掉,但千万别上传到开源库里(如github, gitee)哦
accessKeySecret: "xxx",
region: "xxx",
bucket: "xxx",
uploadPath: "/strapi/static",
baseUrl: "xxx",
timeout: 3000,
secure: true,
},
},
},
});

3. 数据的格式化(!!!)


课程中自己定义 removeTime, removeAttrsAndId 等方法,对数据进行处理,可能写的比较早,还没有成熟的转换库,这里介绍下 strapi-plugin-transformer,可以快速的去掉没必要的层级结构和一些属性,相当好用。配置如下:

// config/plugins.js

module.exports = ({ env }) => ({
transformer: {
enabled: true,
config: {
responseTransforms: {
removeAttributesKey: true,
removeDataKey: true,
},
},
},
});

这个操作可以避免对数据进行过多的处理,也就意味着 src/api/xx 里的代码,你基本不需要手动修改了,大大提升了后台配置的开发效率。C端对数据的处理也更简单了


4. 一些小的注意点

  • config 目录下添加插件,需要创建 plugins.js 文件,少写了 s 会导致插件不生效。(PS:嗯,就是粗心的我建了 plugin.js 还怪人家插件不好使~)

  • .cache 还挺有用的,一些修改不生效,可以试试 build 后再重启试试。


一些拓展能力


strapi 的初始状态很难满足直接交付给运营配置,最大的坑点在于类型定义是英文的,还没有层级结构,这里参考了这篇文章提到的方案:juejin.cn/post/721922…,来对 dashboard 进行定制。功能主要分为3个步骤,patch-package 使用参考原文章即可。

  • 类型汉化


只需要修改 admin/app.js 即可


  • 类型层级&排序:.cache/admin/src/content-manger/pages/App/LeftMenu 文件

// 目录排序,1,1.1,2,2.1
function compareDirectories(formatter, dir1, dir2) {
// 提取目录中的数字和点号
const regex = /(\d+|\.)+/g;
const arr1 = dir1.match(regex);
const arr2 = dir2.match(regex);

if (!arr1 || !arr2) return formatter.compare(dir1, dir2);

// 比较每个部分的数字
for (let i = 0; i < Math.max(arr1.length, arr2.length); i++) {
const num1 = parseFloat(arr1[i]) || 0; // 如果无法解析为数字,默认为0
const num2 = parseFloat(arr2[i]) || 0;

if (num1 < num2) {
return -1;
} else if (num1 > num2) {
return 1;
}
}

// 如果所有部分都相同,则按照长度进行比较
if (arr1.length < arr2.length) {
return -1;
} else if (arr1.length > arr2.length) {
return 1;
}

return 0; // 目录完全相同
}



// Sort correctly using the language
// 这条注释下,用目录排序的方法替代原有 sort

// SubNavLink 内,使用 dangerouslySetInnerHTML 体现目录层级
<span dangerouslySetInnerHTML={{__html: link.title.replace(/([0-9]+.)/g, (a, b, index) => {
if (index <= 1) return '';

return '   ';
})}}></span>


这样你就能得到一个这样的配置目录




  • 项目部署

因为我这边项目是 ssg,修改完内容是需要触发流水线重新部署的,如果像想避免这个工作,可以增加一些按钮,触发 webhook。这样只要排版是够用的,就不需要开发介入了。


如下示例,修改的是 .cache/admin/src/pages/HomePage/index.js




以上改动想要生效,记得用 patch-package,这也是我说他的定制模块不友好的原因~


总结


next.jsstapi 的学习,踩坑,使用经验就是这么多了,起初发起这个项目,是因为我们官网的框架有些老了,用的 fis3,有时候法务、市场同学来找替换素材,都是没什么工作难度的事儿,浪费时间,也整的挺烦的,所以借着机会升级了一下,以后就事半功倍了。


至于在当今这个时代,官网的 seo 是否还有意义,是否还能够为公司带来不俗的自然增量,这个我最感兴趣的事儿,迟迟没能发起。


因为在公司,这是涉及很多部门(品牌,公关,法务,市场,产品,设计)的事儿,普通开发并不能调动资源,我也很期待,如果有幸能发起一个这样的项目,并不断通过技术上的优化为业务带来新的增量,那里可能会用到更多的贴合业务的 ssr 技术,到时候有机会再和大家分享下~


但我也想对技术感兴趣的朋友说,底层的技术重构也是很有魅力的一件事儿,尽管没有业务方的支持,如果你持之以恒的来做,复刻原产品,也能让这个产品在技术层面上焕然一新。


谨以此文,与君共勉!


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

解决扫码枪因输入法中文导致的问题

web
问题 最近公司项目上遇到了扫码枪因搜狗/微软/百度/QQ等输入法在中文状态下,使用扫码枪扫码会丢失字符的问题 思考 这种情况是由于扫码枪的硬件设备,在输入的时候,是模拟用户键盘的按键来实现的字符输入的,所以会触发输入法的中文模式,并且也会触发输入法的自动联想。...
继续阅读 »

问题


最近公司项目上遇到了扫码枪因搜狗/微软/百度/QQ等输入法在中文状态下,使用扫码枪扫码会丢失字符的问题


思考


这种情况是由于扫码枪的硬件设备,在输入的时候,是模拟用户键盘的按键来实现的字符输入的,所以会触发输入法的中文模式,并且也会触发输入法的自动联想。那我们可以针对这个来想解决方案。


方案一


首先想到的第一种方案是,监听keydown的键盘事件,创建一个字符串数组,将每一个输入的字符进行比对,然后拼接字符串,并回填到输入框中,下面是代码:


function onKeydownEvent(e) {
this.code = this.code || ''
const shiftKey = e.shiftKey
const keyCode = e.code
const key = e.key
const arr = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-']
this.nextTime = new Date().getTime()
const timeSpace = this.nextTime - this.lastTime
if (key === 'Process') { // 中文手动输入
if (this.lastTime !== 0 && timeSpace <= 30) {
for (const a of arr) {
if (keyCode === 'Key' + a) {
if (shiftKey) {
this.code += a
} else {
this.code += a.toLowerCase()
}
this.lastTime = this.nextTime
} else if (keyCode === 'Digit' + a) {
this.code += String(a)
this.lastTime = this.nextTime
}
}
if (keyCode === 'Enter' && timeSpace <= 30) {
if (String(this.code)) {
// TODO
dosomething....
}
this.code = ''
this.nextTime = 0
this.lastTime = 0
}
}
} else {
if (arr.includes(key.toUpperCase())) {
if (this.lastTime === 0 && timeSpace === this.nextTime) {
this.code = key
} else if (this.lastTime !== 0 && timeSpace <= 30) {
// 30ms以内来区分是扫码枪输入,正常手动输入时少于30ms的
this.code += key
}
this.lastTime = this.nextTime
} else if (arr.includes(key)) {
if (this.lastTime === 0 && timeSpace === this.nextTime) {
this.code = key
} else if (this.lastTime !== 0 && timeSpace <= 30) {
this.code += String(key)
}
this.lastTime = this.nextTime
} else if (keyCode === 'Enter' && timeSpace <= 30) {
if (String(this.code)) {
// TODO
dosomething()
}
this.code = ''
this.nextTime = 0
this.lastTime = 0
} else {
this.lastTime = this.nextTime
}
}
}


这种方案能解决部分问题,但是在不同的扫码枪设备,以及不同输入法的情况下,还是会出现丢失问题


方案二


使用input[type=password]来兼容不同输入的中文模式,让其只能输入英文,从而解决丢失问题


这种方案网上也有不少的参考

# 解决中文状态下扫描枪扫描错误

# input type=password 取消密码提示框


使用password密码框确实能解决不同输入法的问题,并且Focus到输入框,输入法会被强制切换为英文模式


添加autocomplete="off"autocomplete="new-password"属性


官方文档:
# 如何关闭表单自动填充


但是在Chromium内核的浏览器,不支持autocomplete="off",并且还是会出现这种自动补全提示:


image.png


上面的属性并没有解决浏览器会出现密码补全框,并且在输入字符后,浏览器还会在右上角弹窗提示是否保存:


image.png


先解决密码补全框,这里我想到了一个属性readonly,input原生属性。input[type=password]readonly
时,是不会有密码补全的提示,并且也不会弹窗提示密码保存。


那好,我们就可以在输入前以及输入完成后,将input[type=password]立即设置成readonly


但是需要考虑下面几种情况:



  • 获取焦点/失去焦点时

  • 当前输入框已focus时,再次鼠标点击输入框

  • 扫码枪输出完成最后,输入Enter键时,如果清空输入框,这时候也会显示自动补全

  • 清空输入框时

  • 切换离开页面时


这几种情况都需要处理,将输入框变成readonly


我用vue+element-ui实现了一份,贴上代码:


<template>
<div class="scanner-input">
<input class="input-password" :name="$attrs.name || 'one-time-code'" type="password" autocomplete="off" aria-autocomplete="inline" :value="$attrs.value" readonly @input="onPasswordInput">
<!-- <el-input ref="scannerInput" v-bind="$attrs" v-on="$listeners" @input="onInput"> -->
<el-input ref="scannerInput" :class="{ 'input-text': true, 'input-text-focus': isFocus }" v-bind="$attrs" v-on="$listeners">
<template v-for="(_, name) in $slots" v-slot:[name]>
<slot :name="name"></slot>
</template>
<!-- <slot slot="suffix" name="suffix"></slot> -->
</el-input>
</div>
</template>

<script>
export default {
name: 'WispathScannerInput',
data() {
return {
isFocus: false
}
},
beforeDestroy() {
this.$el.firstElementChild.setAttribute('readonly', true)
this.$el.firstElementChild.removeEventListener('focus', this.onPasswordFocus)
this.$el.firstElementChild.removeEventListener('blur', this.onPasswordBlur)
this.$el.firstElementChild.removeEventListener('blur', this.onPasswordClick)
this.$el.firstElementChild.removeEventListener('mousedown', this.onPasswordMouseDown)
this.$el.firstElementChild.removeEventListener('keydown', this.oPasswordKeyDown)
},
mounted() {
this.$el.firstElementChild.addEventListener('focus', this.onPasswordFocus)
this.$el.firstElementChild.addEventListener('blur', this.onPasswordBlur)
this.$el.firstElementChild.addEventListener('click', this.onPasswordClick)
this.$el.firstElementChild.addEventListener('mousedown', this.onPasswordMouseDown)
this.$el.firstElementChild.addEventListener('keydown', this.oPasswordKeyDown)

const entries = Object.entries(this.$refs.scannerInput)
// 解决ref问题
for (const [key, value] of entries) {
if (typeof value === 'function') {
this[key] = value
}
}
this['focus'] = this.$el.firstElementChild.focus.bind(this.$el.firstElementChild)
},
methods: {
onPasswordInput(ev) {
this.$emit('input', ev.target.value)
if (ev.target.value === '') {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
})
}
},
onPasswordFocus(ev) {
this.isFocus = true
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
})
},
onPasswordBlur() {
this.isFocus = false
this.$el.firstElementChild.setAttribute('readonly', true)
},
// 鼠标点击输入框一瞬间,禁用输入框
onPasswordMouseDown() {
this.$el.firstElementChild.setAttribute('readonly', true)
},
oPasswordKeyDown(ev) {
// 判断enter键
if (ev.key === 'Enter') {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
})
}
},
// 点击之后,延迟200ms后放开readonly,让输入框可以输入
onPasswordClick() {
if (this.isFocus) {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
}, 200)
}
},
onInput(_value) {
this.$emit('input', _value)
},
getList(value) {
this.$emit('input', value)
}
// onChange(_value) {
// this.$emit('change', _value)
// }
}
}
</script>

<style lang="scss" scoped>
.scanner-input {
position: relative;
height: 36px;
width: 100%;
display: inline-block;
.input-password {
width: 100%;
height: 100%;
border: none;
outline: none;
padding: 0 16px;
font-size: 14px;
letter-spacing: 3px;
background: transparent;
color: transparent;
// caret-color: #484848;
}
.input-text {
font-size: 14px;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
pointer-events: none;
background-color: transparent;
::v-deep .el-input__inner {
// background-color: transparent;
padding: 0 16px;
width: 100%;
height: 100%;
}
}

.input-text-focus {
::v-deep .el-input__inner {
outline: none;
border-color: #1c7af4;
}
}
}
</style>


至此,可以保证input[type=password]不会再有密码补全提示,并且也不会再切换页面时,会弹出密码保存弹窗。
但是有一个缺点,就是无法完美显示光标。如果用户手动输入和删除,使用起来会有一定的影响。


我想到过可以使用模拟光标,暂时不知道可行性。有哪位同学知道怎么解决的话,可以私信我,非常感谢


作者:香脆又可口
来源:juejin.cn/post/7265666505102524475
收起阅读 »

你还在傻傻的打开页面输用户名和密码?来我教你实现自动化

web
背景 Hello~,大家好! 本文和各位分享一个有趣的事情! 我司主要的客户是银行,随着银行对信息安全越来越重视,我司积极配合银行防范信息安全,因此我司产品都从之前的外网开发引入了深信服的云桌面开发。由于笔者用的 Mac 电脑,云桌面 win7,天差地别的体验...
继续阅读 »

背景


Hello~,大家好! 本文和各位分享一个有趣的事情!


我司主要的客户是银行,随着银行对信息安全越来越重视,我司积极配合银行防范信息安全,因此我司产品都从之前的外网开发引入了深信服的云桌面开发。由于笔者用的 Mac 电脑,云桌面 win7,天差地别的体验我就先忍了😭,但是从 Mac 电脑桌面到进入云桌面,需要登录一些莫名其妙的 VPN、网址,这个过程是令人反感的、重复的、也是非常恶心的,每天都要这样、要等待很久......🤮



  1. 打开Mac上的xxxTrust并登录

  2. 打开Chrome

  3. 打开xxx网址

  4. 输入用户名、密码(不能自动填充那种)

  5. 点击登录

  6. 进入云桌面资源页面,点击一个资源,会自动调起 Mac 上安装的一个什么 VDIxxx 的软件

  7. 成功进入云桌面


我丢,各位来说说,这个过程是不是很恶心。作为一个技术人,我们不能忍受这种机械式的操作,我们要去做出改变。不能让自己一直做这些重复的恶心操作,于是我就想着 能不能像我打开 Mac 桌面的一个应用那样,中间的步骤自动完成,直到进入云桌面? 这就是笔者本文的主题。


实现自动化


其实笔者也没有一步到位——能不能像我打开 Mac 桌面的一个应用那样,中间的步骤自动完成,直到进入云桌面?。实现过程中,有一些新想法,接下来就分享一下我从开始有这个想法到实现的过程:


ConsoleSnippets


一开始: 我已经登录了 xxxTrust,笔者在浏览器已经打开了xxx网址,只是不想输密码,我就想起浏览器开发者工具 ConsoleSnippets,可以在里面写一些脚本,然后可以快捷执行:


image.png

打开控制台 -> 快捷键 command + p -> 输入!,选择执行哪个 Snippets -> 回车。看下效果:
1.gif


感觉还行是吧,那接着来,我们现在进入了资源管理的界面,接下来需要手动点击打开一个云桌面的资源,同样地,接着建一个 Snippets


image.png

看下效果:
1.gif


OK!成功进入,但是现在还需要我们手动去执行脚本,而且要执行两个。于是就有了新的想法。


篡改猴


能不能在对应的页面自动执行上面写好的脚本?


此时笔者想到了一个 Chrome 插件 —— 篡改猴,也叫油猴脚本,简单理解它的作用就是可以在你指定的网页中执行你写入的脚本。那就装一个呗!(Chrome商店需要🪜,可直接使用Edge浏览器)


image.png

打开管理面板,新建两个脚本:
image.png


编辑:
截屏2023-08-11 11.46.22.png


截屏2023-08-11 12.43.48.png

然后改一下设置,在 document-end 执行我们的脚本,此时可能还获取不到dom,因此使用 setTimeout
截屏2023-08-11 12.45.57.png
再来看下效果:


1.gif
鼠标一下没动哦!你以为这就完了?并没有,接着看。


自动化打开应用


能不能自动打开xxxTrustChrome,并自动打开登录的网址?


咱们一开始就说了,在访问网址之前,还需要链接VPN、打开浏览器,那就来吧!


截屏2023-08-11 13.21.17.png

在Mac上有两个东西可以完成自动操作:


截屏2023-08-11 13.22.20.png
通过自动操作配置出来的,也支持转变成快捷指令。我理解这俩应该是差不多的东西,来看下我们如何实现:


image.png
操作步骤:

  1. 打开 xxxTrust

  2. 通过 AppleScript,设置延时6秒,因为打开上面的 app 过后,有一个自动登录的过程,我们设置的长一点

  3. 打开 Chrome

  4. 打开登录网址


配置自动化操作和快捷指令都是可视化的,很容易上手,在此不过多介绍,感兴趣的掘友可自行探索哦!至于AppleScript,我只能告诉你是 GPT 教我这么写的。


最后一步就是把这个快捷指令发布到桌面:
截屏2023-08-11 13.33.00.png


看下最终的效果:
1.gif


这不,又多了几分钟摸鱼时间!😂


总结


笔者通过真实的一个场景,借助 Snippets篡改猴快捷指令自动操作 等工具实现自动化完成进入云桌面的一系列流程。笔者只实现了 Mac 的,windows 系统肯定也有类似的工具等待各位去探索(比如 python 脚本应该就能实现打开应用等操作)。


除此之外呢,还想表达一个观点就是——我们应该把那些机械式的活交给机械去做,比如在平时的开发中,总是CRUD?能不能高效CRUD?对吧!把这些对自己能力提升没有意义的工作,想办法用程序去实现了,岂不是美滋滋!


好了,本次分享就到此结束了,感谢阅读哦!


如果本文对你有一点点帮助,点个赞支持一下吧,你的每一个【】都是我创作的最大动力 ^_^


作者:Lvzl
来源:juejin.cn/post/7265744160694911028
收起阅读 »

axios使用异步方式无感刷新token,简单,太简单了

🍉 废话在前 写vue的伙伴们无感刷新token相信大家都不陌生了吧,刚好,最近自己的一个项目中就需要用到这个需求,因为之前没有弄过这个,研究了一个上午,终于还是把它拿下了,小小的一个token刷新😏。 下面接着分析一下踩到的坑以及解决思路 🍗 接着踩坑 我...
继续阅读 »

🍉 废话在前


写vue的伙伴们无感刷新token相信大家都不陌生了吧,刚好,最近自己的一个项目中就需要用到这个需求,因为之前没有弄过这个,研究了一个上午,终于还是把它拿下了,小小的一个token刷新😏。




下面接着分析一下踩到的坑以及解决思路


🍗 接着踩坑


我按照之前传统的方式在返回拦截器里面进行token刷新,正常的数据可以返回,但是这个时候会有比较麻烦的地方,就是请求的数据可以在拦截器里面得到,但是不能渲染到界面上(看到这里的时候我是懵的)。


看一下代码

service.interceptors.response.use(
response => {
const res = response.data
//刷新token的时候,可以从这里拦截到新数据,但是没有显示在页面上
console.log('拦截数据:',res)
if (loadingInstance) {
loadingInstance.close();
NProgress.done();
}
switch (res.code) {
case 200:
return res
case 401:
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '令牌过期',
message: '当前身份信息超过三天已失效,请您重新登录',
duration: 0
});
break;
default:
Message({
message: res.message || '请求错误',
type: 'error',
duration: 5 * 1000
})
break;
}
},
error => {
switch (error.response.status) {
case 401:
MessageBox.confirm('身份认证已过期,是否刷新本页继续浏览?', '提示', {
confirmButtonText: '继续浏览',
cancelButtonText: '退出登录',
type: 'warning'
}).then(() => {
axios.post('/api/token/refresh/', {
refresh: localStorage.getItem("retoken")
}).then(response => {
let res = response.data || {}
if (res.code == 200) {
let token = res.data.result;
localStorage.setItem("token", token.access);
localStorage.setItem("retoken", token.refresh);
error.response.config.baseURL = '';
error.response.config.headers['Authorization'] = 'Bearer ' + localStorage.getItem("token")
window.location.reload();
} else {
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '令牌过期',
message: '当前身份信息超过三天已失效,请您重新登录',
duration: 0
});
}
}).catch(err => {
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '认证失败',
message: '信息认证失败,请重新登录',
duration: 0
});
})
}).catch(() => {
router.push({
path: "/login",

})
Message({
message: '已退出',
type: 'success',
})
localStorage.clear();
});
break;
case 404:
Notification.error({
title: '404错误',
message: '服务器请求错误,请联系管理员或稍后重试。错误状态码:404',
});
break
default:
Notification.error({
title: '请求错误',
message: '服务器请求错误,请联系管理员或稍后重试',
});
break;
}
if (loadingInstance) {
loadingInstance.close();
NProgress.done();
}
return Promise.reject(error);

}
)

懵归懵,好在已经发现这个问题了,剩下的怎么解决呢?


当时想的是和后端配合,让后端直接发一个token过期的时间戳给我,我直接把这个时间戳放到localStrage里面,通过这个localStrage,直接在前端进行判断token的过期时间进行请求拦截,如果当前请求的时间大于了这个localStrage里面的时间,就说明token过期了,我这边就需要重新请求token了,而不要后端去进行token的验证。


信心满满的弄了一下,发现行不通,token过期的时候,后端直接一个错误401,前端就又回到解放前了。而且用时间戳的方式很容易出现bug,且在前端进行token时长的验证,很容易出现问题。因此,我还是觉得再研究一下上面那一段代码。


当然踩到坑不只是这个,还有百度和chatGPT,真的,看了一下没有找到一个可行的,总结一下主要有以下几种

  1. 状态管理vuex

  2. 路由router

  3. 时间戳(和我刚刚那种方式差不多)

  4. 依赖注入inject

  5. 刷新界面(我最开始那种方式,但是刷新的时候会出现页面白屏,且用户如果在页面上有一些自己输入的数据也会被清空,用户体验感不好)


以上方式我先不管行不行,但是麻烦是肯定的,做前端讲究的就一个字:“懒”
不是,应该是 “高效快捷”
所以这些方式就pass掉了


只能想想为什么拦截器里面可以得到数据,为什么页面位置得不到数据了


🥩 解决思路


下面是我的解决思路,有不对的地方还请看到的大神指出来一下😁


当用户发起请求的时候,因为刷新token的http状态码是401,这个时候axios的响应拦截器就直接进行错误捕获了,到了这里,因为数据已经返回了,但是因为是错误数据,页面得到的这个数据不可用且当前请求已经结束了,当然这里对状态码401是进行处理了的(应该获取token了)。


采用普通的获取方式来获取token,因为异步的原因,我们获取token的同时页面也在做刷新,token获取的同时,界面也刷新完毕了(但是是没有数据的,不做错误捕获会报错),因此我们在获取token完毕,且用新的token去获取数据时,拦截器里面会有数据,但是界面已经休息了,就不会把拦截器里面的新数据刷新到页面了。


因此这个地方需要对获取token的过程进行一下请求阻塞,把获取token的请求变成同步的。到这里就差不多了。直接把响应拦截器里面的error函数变成同步不就行了吗,async + await可以出来了。


以上是自己当时的想法,简单说来就是 页面刷新需要慢我获取token一步 ,通过这个方式也确实做到了无感刷新🤣


希望以上的能帮助到你,有什么好的思路也欢迎评论区指出。


🍓完整代码


这个是用了2.13.2版本的element-ui以及nprogress的一个axios代码模块,包含了一个下载文件的模块。


如果需要的话可以根据自己的需求来进行修改

import axios from 'axios'
import {
Message,
Loading,
Notification,
} from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import router from '@/router/index'
const baseURL = '/api'
const service = axios.create({
baseURL,
timeout: 6000
})
NProgress.configure({
showSpinner: false
}) // NProgress Configuration
let loadingInstance = undefined;
service.interceptors.request.use(
config => {
NProgress.start()
loadingInstance = Loading.service({
lock: true,
text: '正在加载,请稍候...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.3)'
})
config.headers['Authorization'] = 'Bearer ' + localStorage.getItem("token")
return config;
},
error => {
return Promise.reject(error)
}
)

service.interceptors.response.use(
response => {
const res = response.data

if (loadingInstance) {
loadingInstance.close();
NProgress.done();
}
switch (res.code) {
case 200:
return res
case 401:
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '认证失效',
message: '当前身份信息超过三天已失效,请您重新登录',
duration: 0
});
break;
default:
Message({
message: res.message || '请求错误',
type: 'error',
duration: 5 * 1000
})
break;
}
},
async error => {
switch (error.response.status) {
case 401:
const err401Data = error.response.data || {}
if (err401Data.code !== "token_not_valid") {
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '认证失败',
message: '身份信息认证失败,请您重新登录',
duration: 0
});
return
}
try {
const res = await service.post('/token/refresh/', {
refresh: localStorage.getItem("retoken")
})
if (res.code == 200) {
let token = res.data.result;
localStorage.setItem("token", token.access);
localStorage.setItem("retoken", token.refresh);
error.response.config.baseURL = ''
error.response.config.headers['Authorization'] = 'Bearer ' + localStorage.getItem("token")
return service(error.response.config)
} else {
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '认证失败',
message: '当前身份认证信息已失效,请您重新登录',
duration: 0
});
}
} catch (error) {
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '认证失败',
message: '身份认证失败,请您重新登录,失败原因:' + error.message,
duration: 0
});
}

break
case 404:
Notification.error({
title: '404错误',
message: '服务器请求错误,请联系管理员或稍后重试。错误状态码:404',
});
break
default:
Notification.error({
title: '请求错误',
message: '服务器请求错误,请联系管理员或稍后重试',
});
break;
}
if (loadingInstance) {
loadingInstance.close();
NProgress.done();
}
return Promise.reject(error);

}
)

// 文件下载通用方式
export const requestFile = axios.create({
baseURL,
timeout: 0, //关闭超时时间
});

requestFile.interceptors.request.use((config) => {
config.headers['Authorization'] = 'Bearer ' + localStorage.getItem("token") //携带的请求头
config.responseType = 'blob';
loadingInstance = Loading.service({
lock: true,
text: '正在下载,请稍候...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.3)'
})
return config;
});

requestFile.interceptors.response.use(
(response) => {
let res = response.data;
if (loadingInstance) {
loadingInstance.close()
}
// const contentType = response.headers['content-type'];//获取返回的数据类型
let blob = new Blob([res], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" //文件下载以及上传类型为 .xlsx
});
let url = window.URL.createObjectURL(blob);
// 创建一个链接元素
let link = document.createElement('a');
link.href = url;
link.download = '产品列表.xlsx'; // 自定义文件名
link.click();


},
(err) => {
Message({
message: '操作失败,请联系管理员',
type: 'error',
})
if (loadingInstance) {

loadingInstance.close()
}
}
);


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

🎉前端开发书籍推荐🎉

文章首发公众号:萌萌哒草头将军,欢迎关注 🎉SolidJS响应式原理和简易实现🎉 做为多年学习JavaScript的开发者,一路走来,几多坎坷,回头再看来时的路,特别感谢下面几本书带我入行前端。 💎《JavaScript高级程序设计》 推荐指数:⭐⭐⭐⭐⭐...
继续阅读 »

文章首发公众号:萌萌哒草头将军,欢迎关注



🎉SolidJS响应式原理和简易实现🎉


做为多年学习JavaScript的开发者,一路走来,几多坎坷,回头再看来时的路,特别感谢下面几本书带我入行前端。


💎《JavaScript高级程序设计》




推荐指数:⭐⭐⭐⭐⭐


推荐理由:内容扎实,且不深奥。


这本书在我大学的时候就已经买了,当时经济实力有限,买的是影印版,不过也被我研读了多遍,并且做了详细的记录。这本书见证了我的前端之路,一直被我珍藏至今。




现在已经出了第四版了。前端入门进阶宝典,所以也被广大开发者称为《红宝书》。


💎《Javascript权威指南》




推荐指数:⭐⭐⭐⭐⭐


推荐理由:内容全面,讲解详细,配合红宝书效果更佳。


该书被称为《犀牛书》,书如其名,真的是权威指南,即使你买了红宝书,我也推荐你买一本,因为这两本对相同的知识讲解,侧重点不同。


比如对于闭包,红宝书很详细,从作用域,到作用域链,再到活动对象,讲解由浅到深,十分详细,而《犀牛书》中仅仅是给出闭包的概念,然后举例说明差异。


但是《犀牛书》对于类型转换toStringvalueof讲解则十分详细,《红宝书》则浅浅的带过。


《犀牛书》更像是一本字典,有不懂的问题,可以及时查漏补缺。


💎《图解HTTP》




推荐指数:⭐⭐⭐⭐


推荐理由:图文并茂,简洁明了


这本书作为第三本推荐,绝不是空穴来风,大量的图来解释枯燥的概念,形象生动,老少皆宜。


前端开发一大部分的时间都是在和后端的接口打交道,而Http无疑是沟通的桥梁。


读完这本书,你将会了解到网络分层模型Http协议和TCP/IP的关系、后端的数据怎么从服务端到达浏览器的、常用Http状态码的含义、请求头的各种含义、你输入url浏览器发生了什么等热门面试题的答案。


我买的是三件套《图解Http》《图解TCP/IP》《图解网络硬件》,《图解网络硬件》一点也不推荐,不懂的硬件直接百度吧,还是彩色图片。如果你是做运维相关的前端开发,《图解TCP/IP》同样值得一看。




💎《数据结构与算法JavaScript描述》




推荐指数:⭐⭐⭐⭐


推荐理由:进阶利器,闭眼入就对了。


大多数同学选择前端,主要还是因为数据结构和算法方面比较薄弱,但是这本书却使用了简洁的方法实现了各种数据结构和算法。


对于难懂的数据结构,有详细的结构图解释,是我读过最容易理解的版本了,不过这本书目前还是ES5语法版本实现,我在前面的文章中使用ES6语法实现过,并做了部分笔记。


👉【数据结构】我的学习笔记




💎《JavaScript设计模式》




推荐指数:⭐⭐⭐


推荐理由:常见的设计模式都有,讲解的比较简单易懂,但是实现比较简陋。


这本书是你入门中级后继续提升的有利法宝,不管什么框架,底层都逃不出两三个设计模式的,所以十分推荐你进阶的时候去读它。


目前有两本名为《JavaScript设计模式》的书,我买的是徐涛翻译版本影印版(和红宝书一起买的),但是最近查阅发现流行的是张容铭著作的版本,这里请自行斟酌买哪个版本。


我买的这个版本将设计模式分为创建型、行为型和结构型三种,前面部分分别讲解了十三种设计模式,后半部分讲解了老牌框架JQuery设计的各种设计模式,虽然从现在的情况看JQuery已经凉了,但是它的设计智慧,真的令人敬佩。


这本书的缺点也是语法版本较旧,不过我也写了最新语法的部分笔记。


👉超级简单的设计模式,看不懂你来打我


今天的内容就这些了,如果你有更好的书籍,可以告诉我!


现在,关注我的公众号会有送书福利,具体请在公众号回复:活动,即可查看详情


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

推荐 6 个 火火火火 的开源项目

本期推荐开源项目目录:1、ChatGPT 网页应用(AI)2、AI 换脸(AI)3、API 调用 Midjourney 进行 AI 画图(AI)4、如何使用 Open AI 的 API?(AI)5、中华古诗词数据库6、动画编程 01. ChatGPT 网页应用...
继续阅读 »

本期推荐开源项目目录:

1、ChatGPT 网页应用(AI)
2、AI 换脸(AI)
3、API 调用 Midjourney 进行 AI 画图(AI)
4、如何使用 Open AI 的 API?(AI)
5、中华古诗词数据库
6、动画编程


01. ChatGPT 网页应用


基于 ChatGPT-Next-Web 二次开发的 ChatGPT 网页付费系统,包含用户管理模块和后台看板。


ChatGPT-Admin-Web 付费系统包含七个模块,包括:内容接口、用户系统、支付、敏感词过滤、自由聊天、分销、收益


编辑


添加图片注释,不超过 140 字(可选)


开源地址:github.com/AprilNEA/Ch…



编辑


添加图片注释,不超过 140 字(可选)


02. AI 换脸


适用于视频聊天的 AI 换脸模型,你可以使用这个 AI 模型替换摄像头中的面部或视频中的面部。这是一些例子:


编辑


添加图片注释,不超过 140 字(可选)


开源地址:github.com/iperov/Deep…


03. API 调用 Midjourney 进行 AI 画图


通过代理 MidJourney 的 Discord 频道,实现 api 形式调用AI绘图。


前提是你要注册 Midjourney 账号、并在 Discord 创建在自己的频道和机器人,然后就可以根据这个项目的指引一步步去使用 Api 调用 Midjourney 了。


开源地址:github.com/novicezk/mi…


编辑


添加图片注释,不超过 140 字(可选)


04. 如何使用 Open AI 的 API?


Open AI-Cook Book 是一本 Open AI 的 API 使用指南,提供了一些通过 Open AI 的 API 搭建任务的示例代码。


开源地址:github.com/openai/open…


编辑


添加图片注释,不超过 140 字(可选)


05. 中华古诗词数据库


为了让古诗词这个人类瑰宝传承下去,中华古诗词数据库诞生了。


这个项目整理了中华大量的古诗词,支持 Json 格式。数据库包含唐宋两朝近一万四千古诗人的作品, 接近 5.5 万首唐诗、26 万宋诗. 两宋时期1564位词人,21050首词。


开源地址:github.com/chinese-poe…


编辑


添加图片注释,不超过 140 字(可选)



  1. 动画编程


Motion Canvas 是一个 TypeScript 库,可以通过编程的方式生成动画,并提供所述动画的实时预览的编辑器。


开源地址:github.com/motion-canv…




编辑


添加图片注释,不超过 140 字(可选)


历史盘点


逛逛 GitHub 每天推荐一个好玩有趣的开源项目。历史推荐的开源项目已经收录到 GitHub 项目,欢迎 Star:

地址:https://github.com/Wechat-ggGitHub/Awesome-GitHub-Repo


作者:逛逛GitHub
链接:https://juejin.cn/post/7240690534075318309
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

this指向哪?

web
谁调用就指向谁 this 指向哪?先记住一句话:谁调用就指向谁,记住这句话就成功一大半了。 var userName = '张三' function fn() { var userName = '李四' console.log(this.use...
继续阅读 »

谁调用就指向谁


this 指向哪?先记住一句话:谁调用就指向谁,记住这句话就成功一大半了。


var userName = '张三'
function fn() {
var userName = '李四'
console.log(this.userName) // 张三
}
fn()

函数fn相当于挂在window,调用fn()等同于window.fn(),所以fn可以看做是被window调用,此时this便指向window,所以输出结果是函数外的userName变量。


再看下面的示例:


var userName = '张三'
var obj = {
userName: '李四',
fn: function() {
console.log(this.userName) // 李四
}
}

obj.fn()

函数fn被定义在对象obj里,通过obj.fn()调用,回到我们开头的那句话:谁调用就指向谁fnobj调用,所以this指向obj,输出结果是李四。


如果使用windowd.obj.fn()调用呢?结果还是一样的,因为最终是obj调用,所以this指向的还是obj


接着往下看下面这段代码:


var userName = '张三'
var obj = {
userName: '李四',
fn: function() {
console.log(this.name) // 张三
}
}

var func = obj.fn
func()

输出结果为张三,因为代码中只是将obj.fn赋值给func,此时函数fn还没有被调用,真正调用是在func()func挂在window下,所以最终相当于是window调用了函数fn,所以this指向的是window


还是应了那句话:谁调用就指向谁


其他情况


1、作为一个函数被直接调用


当函数被直接调用,没有挂在任何对象上时,this指向window


var userName = '张三'
var obj = {
userName: '李四',
fn: function() {
function a() {
console.log(this.userName) // 张三
}
a()
}
}
obj.fn()

这里在fn中定义了一个函数a并调用,注意这里函数a只是在函数fn内,它并没有挂在哪个对象上,也没有通过其他方式被调用,而是作为一个函数被直接调用,所以this指向window


2、箭头函数


箭头函数的特点是没有自己的this也不能通过new调用。箭头函数通过作用域向上查找,找到离包裹它最近的那一个普通函数的this作为自己的this,如果没有则指向全局对象window


var userName = '张三'
var obj = {
userName: '李四',
fn: function() {
var func = () => {
console.log(this.userName) // 李四
}
func()
},
a: {
b: () => {
console.log(this.userName) // 张三
}
}
}
obj.fn()
obj.a.b()

func是一个箭头函数,箭头函数通过向上查找找到fnfnthis指向obj,所以箭头函数的this指向的也是obj,输出结果:李四。


obj.a.b()则指向window,输出结果:张三。


3、new 构造函数


当函数通过使用new调用时,new过程中会创建一个新的对象,而this便是指向这个新创建的对象。


function fn(name) {
this.userName = name
this.age = 18
console.log(this) // {userName: '张三', age: 18}
}
let data = new fn('张三')
// new的同时内部会默认把this做回返回值返回
console.log('data: ', data) // data: {userName: '张三', age: 18}

如何改变this指向


1、call、apply、bind


可以通过callapplybind的第一个参数传值作为this值,改变this指向。


obj.fn('hello') // hello,张三
obj.fn.call({ userName: '李四' }, 'hello') // hello,李四
obj.fn.apply({ userName: '王五' }, ['hello']) // hello,王五
obj.fn.bind({ userName: '老六' }, 'hello')() // hello,老六

2、call、apply、bind区别


从上面的使用可以看到,这三个API的参数形式除了第一个参数外是有一些区别的



  • callbind是多参数的形式:call(thisArg, arg1, arg2...)

  • apply是数组的形式:call(thisArg, [arg1, arg2...])


另外一个区别是:bind的返回值是一个函数,需要自己再手动调用一次,而callapply<

作者:狂砍2分4篮板
来源:juejin.cn/post/7265534390506635319
/code>则不用。

收起阅读 »

面试官:“只会这一种懒加载实现思路?回去等通知吧”

web
思路一:监听滚动事件 监听滚动事件指的是:通过监听页面的滚动事件,判断需要懒加载的元素是否进入可视区域。当元素进入可视区域时,动态加载对应的资源。这种方式需要手动编写监听滚动事件的逻辑,可能会导致性能问题,如滚动时的抖动和卡顿。 关键 API getBo...
继续阅读 »

思路一:监听滚动事件



监听滚动事件指的是:通过监听页面的滚动事件,判断需要懒加载的元素是否进入可视区域。当元素进入可视区域时,动态加载对应的资源。这种方式需要手动编写监听滚动事件的逻辑,可能会导致性能问题,如滚动时的抖动和卡顿。



关键 API



  • getBoundingClientRect() 方法返回的对象包含以下属性:



  • top:元素上边缘相对于视窗的距离。

  • right:元素右边缘相对于视窗的距离。

  • bottom:元素下边缘相对于视窗的距离。

  • left:元素左边缘相对于视窗的距离。

  • width:元素的宽度(可选)。

  • height:元素的高度(可选)。


可以看下下面的图。
image.png


来看看代码示例


html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lazy Loading Exampletitle>
<style>
img {
width: 100%;
height: 300px;
display: block;
}
style>
head>
<body>
<img data-src="https://via.placeholder.com/300x300?text=Image+1" src="" alt="Image 1">
<img data-src="https://via.placeholder.com/300x300?text=Image+2" src="" alt="Image 2">
<img data-src="https://via.placeholder.com/300x300?text=Image+3" src="" alt="Image 3">
<script>
function isInViewport(element) {
const rect = element.getBoundingClientRect();
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
const windowWidth = window.innerWidth || document.documentElement.clientWidth;

return (
rect.
top >= 0 &&
rect.
left >= 0 &&
rect.
bottom <= windowHeight &&
rect.
right <= windowWidth
);
}

function lazyLoad() {
const images = document.querySelectorAll('img[data-src]');
for (const img of images) {
if (isInViewport(img)) {
img.
src = img.getAttribute('data-src');
img.
removeAttribute('data-src');
}
}
}

window.addEventListener('scroll', lazyLoad);
window.addEventListener('resize', lazyLoad);
window.addEventListener('DOMContentLoaded', lazyLoad);

script>
body>
html>

这里的思路就相对简单了,核心思路就是判断这个元素在不在我的视口里面。


同样的思路下面还有另一种实现方式,是用 offsetTopscrollTopinnerHeight做的,这个很常见,我们就不说了。


思路二:Intersection Observer API



这是一种现代浏览器提供的原生 API,用于监控元素与其祖先元素或顶级文档视窗(viewport)的交叉状态。当元素进入可视区域时,会自动触发回调函数,从而实现懒加载。相比于监听滚动事件,Intersection Observer API 更高效且易于使用。



关键 API



  • Intersection Observer API:创建一个回调函数,该函数将在元素进入或离开可视区域时被调用。回调函数接收两个参数:entries(一个包含所有被观察元素的交叉信息的数组)和 observer(观察者实例)。


其中 entries 值得一提,entries 是一个包含多个 IntersectionObserverEntry 对象的数组,每个对象代表一个观察的元素(target element)与根元素(root element)相交的信息。IntersectionObserverEntry 对象包含以下属性:



  1. intersectionRatio: 目标元素和根元素相交区域占目标元素总面积的比例,取值范围为 0 到 1。

  2. intersectionRect: 目标元素和根元素相交区域的边界信息,是一个 DOMRectReadOnly 对象。

  3. isIntersecting: 布尔值,表示目标元素是否正在与根元素相交。

  4. rootBounds: 根元素的边界信息,是一个 DOMRectReadOnly 对象。

  5. target: 被观察的目标元素,即当前 IntersectionObserverEntry 对象所对应的 DOM 元素。

  6. time: 观察到的相交时间,是一个高精度时间戳,单位为毫秒。


这个最近我们组里的姐姐封了一个懒加载组件,通过单例模式 + Intersection Observer API,然后在外部控制 v-if 就好了,非常 nice。


由于代码保密性,我这里只能提供一个常规实现方法qwq


html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lazy Loading Example - Intersection Observertitle>
<style>
img {
width: 100%;
height: 300px;
display: block;
}
style>
head>
<body>
<img data-src="https://via.placeholder.com/300x300?text=Image+1" src="" alt="Image 1">
<img data-src="https://via.placeholder.com/300x300?text=Image+2" src="" alt="Image 2">
<img data-src="https://via.placeholder.com/300x300?text=Image+3" src="" alt="Image 3">
<script>
function loadImage(img) {
img.
src = img.getAttribute('data-src');
img.
removeAttribute('data-src');
}

function handleIntersection(entries, observer) {
entries.
forEach(entry => {
if (entry.isIntersecting) {
loadImage(entry.target);
observer.
unobserve(entry.target);
}
});
}

function lazyLoad()
script>
body>
html>


思路三:虚拟列表



对于长列表数据,可以使用虚拟列表技术来实现懒加载。虚拟列表只会渲染当前可视区域内的列表项,当用户滚动列表时,动态更新可视区域内的内容。这种方式可以大幅减少 DOM 节点的数量,提高页面性能。



这个我之前专门有文章聊过:b站面试官:如果后端给你 1w 条数据,你如何做展示?


就不赘述啦~


那么这就是本篇文章的全部内容啦~


作者:阳树阳树
来源:juejin.cn/post/7265329025899823159
收起阅读 »

动态样式去哪儿了?

web
本文作者是蚂蚁集团前端工程师豆酱,众所周知,antd v5 使用了 CSS-in-JS 技术从而支持混合、动态样式的需求。相对的它需要在运行时生成样式,这会造成一定的性能损耗。因此我们研发了组件库级别的 @ant-design/cssinjs 库,通过一定的约...
继续阅读 »

本文作者是蚂蚁集团前端工程师豆酱,众所周知,antd v5 使用了 CSS-in-JS 技术从而支持混合、动态样式的需求。相对的它需要在运行时生成样式,这会造成一定的性能损耗。因此我们研发了组件库级别的 @ant-design/cssinjs 库,通过一定的约束提升缓存效率,从而达到性能优化的目的。不过我们并不止步于此。我们可以通过一些逻辑,直接跳过运行时生成样式的阶段。


动态样式去哪儿了?


如果你研究过 Ant Design 的官网,你会发现 Ant Design 的组件并没有动态插入 来控制样式,而是通过 CSS 文件来控制样式:

image.png


image.png


document.head 里有几个 css 文件引用:



  • umi.[hash].css

  • style-acss.[hash].css


前者为 dumi 生成的样式内容,例如 Demo 块、搜索框样式等等。而后者则是 SSR 生成的样式文件。在定制主题文档中,我们提过可以通过整体导出的方式将页面中用到的组件进行预先烘焙,从而生成 css 文件以供缓存命中从而提升下一次打开速度。这也是我们在官网中使用的方式。所以 Demo 中的组件,其实就是复用了这部分样式。 等等!CSS-in-JS 不是需要在运行时生成样式的 hash 然后通过 进行对齐的么?为什么 css 文件也可以对齐?不用着急,我们慢慢看。

CSS-in-JS 注水


应用级的 CSS-in-JS 方案会对生成的样式计算出 hash 值,并且将其存入 Cache 中。当下次渲染时,会先从 Cache 中查找是否存在对应的样式,如果存在则直接使用,否则再生成一次。这样就可以避免重复生成样式,从而提升性能。


image.png


每个动态插入到页面中的样式同样以为 hash 作为唯一标识符。如果页面中已经存在该 hash 的 ,则说明 SSR 中做过 inline style 注入。那么 就不用再次创建。 你可以发现,虽然 的节点创建可以省略,但是因为 hash 依赖于计算出的样式内容。所以即便页面中已经有可以复用的样式内容,它仍然免不了需要计算一次。实属不划算。

组件级 CSS-in-JS


组件级别的 CSS-in-JS 一文中,我们提过。Ant Design 的 Cache 机制并不需要计算出完整的样式。对于组件库而言,只要通过 Token 和 ComponentName 就可以确定生成样式一致性,所以我们可以提前计算出 hash 值: 也因此,我们发现可以复用这套机制,实现在在客户端侧感知组件样式是否已经注入过。


SSR HashMap


在 @ant-design/cssinjs 中,Cache 本身包含了每个元素对应的 style 和 hash 信息。过去的 extractStyle 方法只取 Cache 中 style 的内容进行封装:


// e.g. Real world path is much more complex

{

  "bAMbOo|Button": ["LItTlE"":where(.bAMbOo).ant-btn { color: red }"],

  "bAMbOo|Spin": ["liGHt"":where(.bAMbOo).ant-spin { color: blue }"]

}

提取:


:where(.bAMbOo).ant-btn {

  color: red;

}

:where(.bAMbOo).ant-spin {

  color: blue;

}

为了复用样式,我们更进一步。将 path 和 hash 值也进行了抽取:


{

  "bAMbOo|Button": "LItTlE",

  "bAMbOo|Spin""liGHt"

}

并且也打成 css 样式:


// Just example. Not real world code

.cssinjs-cache-path {

  content'bAMbOo|Button:LItTlE;bAMbOo|Spin:liGHt';

}

这样 SSR 侧就将我们所需的信息全部留存了下来,接下去只需要在客户端进行提取即可。


CSR HashMap


在客户端则简单的多,我们通过 getComputedStyle 提取 HashMap 信息留存即可:


// Just example. Not real world code

const measure = document.createElement('div');

measure.className = 'cssinjs-cache-path';

document.body.appendChild(measure);

 

// Now let's parse the `content`

const { content } = getComputedStyle(measure);

在组件渲染阶段,useStyleRegister 在计算 CSS Object 之前,会先在 HashMap 中查找 path 是否存在。如果存在,则说明该数据已经通过服务端生成。我们只需要将样式从现有的 里提取出来即可:

// e.g. Real world path is much more complex

{

  "bAMbOo|Button": ["LItTlE""READ_FROM_INLINE_STYLE"],

  "bAMbOo|Spin": ["liGHt""READ_FROM_INLINE_STYLE"]

}

而对于 CSS 文件提供的样式(比如官网的使用方式),它不像 会被移除,我们直接标记为来自于 CSS 文件即可。和 inline style 一样,它们会在 useInsertionEffect 阶段被跳过。

// e.g. Real world path is much more complex

{

  "bAMbOo|Button": ["LItTlE""__FROM_CSS_FILE__"],

  "bAMbOo|Spin": ["liGHt""__FROM_CSS_FILE__"]

}

总结


CSS-in-JS 因为运行时的性能损耗而被人诟病。而在 Ant Design 中,如果你的应用使用了 SSR,那么在客户端侧就可以直接跳过运行时生成样式的阶段从而提升性能。当然,我们会继续跟进 CSS-in-JS 的发展,

作者:支付宝体验科技
来源:juejin.cn/post/7265249262329839672
为你带来更好的体验。

收起阅读 »

Axios的封装思路与技巧

web
Axios的封装思路与技巧 提示:文中使用的为ts代码,对ts不熟悉的同学可以删除所有类型降级为js代码,不影响使用 前言 项目中或多或少会有一些需要接口发送请求的需求,与其复制粘贴别人在业务中对请求方法的使用,不如自己花点时间研究项目中请求方法的实现,这样在...
继续阅读 »

Axios的封装思路与技巧


提示:文中使用的为ts代码,对ts不熟悉的同学可以删除所有类型降级为js代码,不影响使用


前言


项目中或多或少会有一些需要接口发送请求的需求,与其复制粘贴别人在业务中对请求方法的使用,不如自己花点时间研究项目中请求方法的实现,这样在处理请求出现的问题时能够更好的定位问题原因。本文循序渐进的介绍了如何对axios进行封装实现自己项目中的请求方法,希望各位同学在阅读后能有一定的体会,如有问题还请大家在评论区指正。


1.创建axios实例


创建一个axios实例,在这里为实例进行一些配置(如超时时间)。但是要注意,不要在此处配置一些动态的属性,如headers中的token,具体的原因我们会在后面提起


import axios from 'axios'
const instance = axios.create({
 timeout: 1000 * 120, // 超时时间120s
})

为实例配置拦截器(请求拦截器,响应拦截器)


可能有些同学对axios不太熟悉,不了解请求拦截器和响应拦截器的作用,这里会简单介绍一下


请求拦截器:在请求发送前进行拦截,或者对请求错误进行拦截


instance.interceptors.request.use(
 config => {
   // config为AxiosRequestConfig的一个实例,它是包含请求配置参数的对象
   // 在这里可以在请求发送前做一些处理,如向config实例中添加属性,取消请求,设置loading等  
   return config
},
 // 这里是请求报错时的拦截方法,这里直接返回一个状态为reject的promise
 // 实际测试时,即使前端请求报错并且未到达后端,也没有触发这里的钩子函数
 error => Promise.reject(error),
)

响应拦截器:在响应被.then.catch处理前拦截


instance.interceptors.response.use(
 response => {
   // 响应成功的场景
   // 在这里可以关闭loading或者对响应的返参对象response进行处理
   return response
},
 error => {
   // 响应失败的场景
   // http状态码不为2xx时就会进入,根据项目要求处理接口401,404,500等情况
   // 返回的promise也可以根据项目要求进行修改
   return Promise.reject(error)
},
)

这样我们就创建了一个可用的axios实例,对于实例的一些其他配置可以参考axios官网


2.创建Abstract类进一步封装


在创建了一个axios实例之后,我们就可以使用它去发送请求了,但是出于减少重复代码的目的,我们不在业务代码中直接使用axios实例去发送各种请求,而是选择去做进一步的封装让整体的代码更加简洁


通常来说,我在项目中更喜欢用面向对象的方式去对axios做进一步的封装,使用这种方式的优点会在后面进行说明 创建一个类,起名可以按自己的喜好来,这里我写的是Abstract,因为它的主要作用是做为一个底层的类让其他类去继承,在这里我们提供一些属性的配置,以及一些基础的请求方法


import axios from './axios'
import type { AxiosRequest, CustomResponse } from './types/index'
class Abstract {
 // 配置接口的baseUrl,这里用的是vite环境变量,可以根据需求自行修改
 protected baseURL: string = import.meta.env.VITE_BASEURL
 // 配置接口的请求头,这里仅简单配置一下
 protected headers: object = {
   'Content-Type': 'application/json;charset=UTF-8',
}
 // 提供类的构造器,可以在这里修改一些基础参数如baseUrl
 constructor(baseURL?: string) {
   this.baseURL = baseURL ?? this.baseURL
}
 // 重点!发起请求的方法
 // 这里的T是ts中泛型的用法,主要用于控制接口返回的类型,不熟悉ts的同学可以略过
 private apiAxios<T = any>({
   baseURL = this.baseURL,
   headers = this.headers,
   method,
   url,
   data,
   params,
   responseType
}: AxiosRequest): Promise<CustomResponse<T>> {
 // 在这里加上请求头的好处在于,每次请求时都会动态读取存储的token值
 // 正如前面所说的,不要在创建axios实例时在header上配置token是因为,浏览器除非刷新,否则只会创建一次axios实例,它的header上的token的值不会发生变化,如果涉及到用户退出等清除token的操作,下次登录时获得的新token不会被使用
 Object.assign(headers, {
   // 根据情况使用localStorage或sessionStorage
   token: localStorage.getItem('token')        
})
 return new Promise((resolve, reject) => {
   axios({
     baseURL,
     headers,
     method,
     data,
     url,
     params,
     responseType,
  })
    .then(res => {
       // 在这里处理http2xx的接口,根据业务的需要进行一些处理,返回一个成功的promise
       // 这里仅为演示,直接返回了原始的res
       resolve(res)
    })
    .catch(err => {
       // 在这里处理http不成功的状态,并根据业务的需要进行一个处理,返回一个失败的promise
       reject(err)
    })
  })
}
 // 通常我还会在基础类上封装一些现成的请求方法,如Get Post等,可以根据自己的需要封装其他的请求方法
 protected getReq<T = any>({ baseURL, headers, url, data, params, responseType, messageType }: AxiosRequest) {
   return this.apiAxios<T>({
     baseURL,
     headers,
     method: 'GET',
     url,
     data,
     params,
     responseType,
     messageType,
  })
}
   
 protected postReq<T = any>({ baseURL, headers, url, data, params, responseType, messageType }: AxiosRequest) {
   return this.apiAxios<T>({
     baseURL,
     headers,
     method: 'POST',
     url,
     data,
     params,
     responseType,
     messageType,
  })
}

3.继承Abstract类实现业务相关的请求类


这样,我们就成功封装了一个Axios的基础类,接下来可以创建一个新的业务类去继承它并使用。这里我们创建一个User类,代表用户相关的请求


import Abstract from '@/api/abstract'

class User extends Abstract {
 constructor(baseUrl?: string) {
   super(baseUrl)
}

 // post请求
 login(data: unknown) {
   return this.postReq({
     data,
     url: 'back/v1/user/login',
  })
}
 
 // get请求
 getUser(param: { id: string })
   return this.getReq({
     param,
     url: 'back/v1/user/getUser'
  })
}
 
 // 需要修改请求头的Content-Type,如表单上传
 saveUser(data: any) {
   const formData = new FormData()
   Object.keys(data).forEach(key => {
     formData.append('file', data[key])
  })
   return this.postReq({
     data,
     headers: {
       'Content-Type': 'multipart/form-data',
    },
     url: 'back/v1/user/saveUser',
  })
}

export default User

文件创建好了之后我们就可以引用到具体项目中使用了


// 可以在这里传入baseUrl,这也是基于类封装的好处,我们可以实例化多个user并使用不同的baseUrl
const userInstance = new User()
const res = await userInstance.login({
 username: 'xxx',
 password: 'xxx'
})
作者:kamesan
来源:juejin.cn/post/7264749103125184527

收起阅读 »

我家等离子电视也能用的移动端适配方案

web
前几天我的领导“徐江”让我把一个移动端项目做一下适配,最好让他在家用等离子电视也能看看效果,做不出来就给我“埋了”,在这种情况下才诞生了这篇文章~ 什么是移动端适配 移动端适配是指在不同尺寸的移动端设备上,页面能相对达到合理的显示或者保持统一的等比缩放效果 ...
继续阅读 »

前几天我的领导“徐江”让我把一个移动端项目做一下适配,最好让他在家用等离子电视也能看看效果,做不出来就给我“埋了”,在这种情况下才诞生了这篇文章~



什么是移动端适配


移动端适配是指在不同尺寸的移动端设备上,页面能相对达到合理的显示或者保持统一的等比缩放效果


移动端适配的两个概念



  1. 自适应:根据不同的设备屏幕大小来自动调整尺寸、大小

  2. 响应式:会随着屏幕的实时变动而自动调整,是一种自适应


1.png

而在我们日常开发中自适应布局在pc端和移动端应用都极为普遍,一般是针对不同端分别做自适应布局,如果要想同时兼容移动端和pc端,尤其是等离子电视这样的大屏幕,那么最好还是使用响应式布局~


移动端适配-视口(viewport)


在一个浏览器中,我们可以看到的区域就是视口(viewport),我们在css中使用的fixed定位就是相对于视口进行定位的。

在pc端的页面中,我们不需要对视口进行区分,因为我们的布局视口和视觉视口都是同一个。而在移动端是不太一样的,因为我们的移动端的网页往往很小,有可能我们希望一个大的网页在移动端上也可以完整的显示,所以在默认情况下,布局视口是大于视觉视口的。


  <style>
.box {
width: 100px;
height: 100px;
background-color: orange;
}
</style>
<body>
<div class="box"></div>
</body>


pc端展示效果
3.png


移动端展示效果
4.png
从上图可以看出在移动端上同样是100px的盒子,但是却没有占到屏幕的1/3左右的宽度,这是因为在大部分浏览器上,移动端的布局视口宽度为980px,我们在把pc端的页面切换成移动端页面时,右上角也短暂的显示了一下我们的布局视口是980px x 1743px。

所以在移动端下,我们可以将视口划分为3种情况:



  1. 布局视口(layout viewport)

  2. 视觉视口(visual layout)

  3. 理想视口(ideal layout)


这些概念的提出也是来自于ppk,是一位世界级前端技术专家。
贴上大佬的文章链接quirksmode.org/mobile/view…


1.jpeg

所以我们相对于980px布局的这个视口,就称之为布局视口(layout viewport),而在手机端浏览器上,为了页面可以完整的显示出来,会对整个页面进行缩小,那么显示在可见区域的这个视口就是视觉视口(visual layout)。


3.png


4.png

但是我们希望设置的是100px就显示的是100px,而这就需要我们设置理想视口(ideal layout)。


// initial-scale:定义设备宽度与viewport大小之间的缩放比例
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

移动端适配方案



  1. 百分比设置

  2. rem单位+动态html的font-size

  3. flex的弹性布局

  4. vw单位


在我们移动端适配方案中百分比设置是极少使用的,因为相对的参照物可能是不同的,所以百分比往往很难统一,所以我们常常使用的都是后面3种方案。


rem单位+动态html的font-size


rem单位是相对于html元素的font-size来设置的,所以我们只需要考虑2个问题,第一是针对不同的屏幕,可以动态的设置html不同的font-size,第二是将原来的尺寸单位都转换为rem即可。



talk is cheap, show me the code



/*
* 方案1:媒体查询
* 缺点:需要针对不同的屏幕编写大量的媒体查询,且如果动态的修改尺寸,不会实时的进行更新
*/

<style>
@media screen and (min-width: 320px) {
html {
font-size: 20px;
}
}
@media screen and (min-width: 375px) {
html {
font-size: 24px;
}
}
@media screen and (min-width: 414px) {
html {
font-size: 28px;
}
}
.box {
width: 5rem;
height: 5rem;
background-color: orange;
}
</style>

<body>
<div class="box"></div>
</body>

/*
* 方案2:编写js动态设置font-size
*/

<script>
const htmlEl = document.documentElement;

function setRem() {
const htmlWidth = htmlEl.clientWidth;
const htmlFontSize = htmlWidth / 10;
htmlEl.style.fontSize = htmlFontSize + "px";
}
// 第一次不触发,需要主动调用
setRem();

window.addEventListener("resize", setRem);
</script>


<style>
.box {
width: 5rem;
height: 5rem;
background-color: orange;
}
</style>


<body>
<div class="box"></div>
</body>


但是写起来感觉还是好麻烦,如果可以的话我希望白嫖-0v0-


5.png

所以我选择 postcss-pxtorem,vscode中也可以下载到相关插件哦,一鱼多吃,😁


vw单位


/*
* 方案1:手动换算
*/

<style>
/** 设置给375的设计稿 */
/** 1vw = 3.75px */
.box {
width: 26.667vw;
height: 26.667vw;
background-color: orange;
}
</style>

/*
* 方案2:less/scss函数
*/

@vwUnit: 3.75;

.pxToVw(@px) {
result: (@px / @vwUnit) * 1vw;
}
.box {
width: .pxToVw(100) [result];
height: .pxToVw(100) [result];
background-color: orange;
}

当然白嫖党永不言输,我选择 postcss-px-to-viewport,贴一下我的配置文件~


module.exports = {
plugins: {
"postcss-px-to-viewport": {
unitToConvert: "px", //需要转换的单位,默认为"px"
viewportWidth: 750, // 视窗的宽度,对应设计稿的宽度
viewportUnit: "vw", // 指定需要转换成的视窗单位,建议使用 vw,兼容性现在已经比较好了
fontViewportUnit: "vw", // 字体使用的视口单位

// viewportWidth: 1599.96, // 视窗的宽度,对应设计稿的宽度
// viewportUnit: 'vw', // 指定需要转换成的视窗单位,建议使用 vw
// fontViewportUnit: 'vw', // 字体使用的视口单位

unitPrecision: 8, // 指定`px`转换为视窗单位值的小数后 x位数
// viewportHeight: 1334, //视窗的高度,正常不需要配置
propList: ["*"], // 能转化为 rem的属性列表
selectorBlackList: [], //指定不转换为视窗单位的类,可以自定义,可以无限添加,建议定义一至两个通用的类名
minPixelValue: 1, // 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值
mediaQuery: false, // 允许在媒体查询中转换
replace: true, //是否直接更换属性值,而不添加备用属性
// exclude: [], //忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件
landscape: false, //是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape)
landscapeUnit: "rem", //横屏时使用的单位
landscapeWidth: 1134 //横屏时使用的视口宽度
}
}
};

rem单位和vw单位的区别


rem事实上是作为一个过渡方案,它利用的也是vw的思想,并且随着前端的发展,vw的兼容性已经越来越好了,可以说具备了rem的所有优势。

但是我们假想一个这样的场景,我们希望网页在达到800px的时候页面布局不需要继续扩大了,这个时候如果我们采用的是rem布局,我们可以使用媒体查询设置max-width,而vw则始终是以视口为单位,自然不容易处理这样的场景。

当然,vw相比于rem,存在以下优势:



  1. 不需要计算font-size大小

  2. 不存在font-size继承的问题

  3. 不存在因为某些原因篡改了font-size导致页面尺寸混乱的问题

  4. vw更加的语义化

  5. 具备rem的所有优点


所以在开发中也更推荐大家使用vw单位进行适配。


6.jpeg


作者:魔术师Grace
来源:juejin.cn/post/7197623702410248251
收起阅读 »