注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

跟我一起探索 HTTP-HTTP缓存

web
概览 HTTP 缓存会存储与请求关联的响应,并将存储的响应复用于后续请求。 可复用性有几个优点。首先,由于不需要将请求传递到源服务器,因此客户端和缓存越近,响应速度就越快。最典型的例子是浏览器本身为浏览器请求存储缓存。 此外,当响应可复用时,源服务器不需要处理...
继续阅读 »

概览


HTTP 缓存会存储与请求关联的响应,并将存储的响应复用于后续请求。


可复用性有几个优点。首先,由于不需要将请求传递到源服务器,因此客户端和缓存越近,响应速度就越快。最典型的例子是浏览器本身为浏览器请求存储缓存。


此外,当响应可复用时,源服务器不需要处理请求——因为它不需要解析和路由请求、根据 cookie 恢复会话、查询数据库以获取结果或渲染模板引擎。这减少了服务器上的负载。


缓存的正确操作对系统的稳定运行至关重要。


不同种类的缓存


HTTP Caching 标准中,有两种不同类型的缓存:私有缓存共享缓存


私有缓存


私有缓存是绑定到特定客户端的缓存——通常是浏览器缓存。由于存储的响应不与其他客户端共享,因此私有缓存可以存储该用户的个性化响应。


另一方面,如果个性化内容存储在私有缓存以外的缓存中,那么其他用户可能能够检索到这些内容——这可能会导致无意的信息泄露。


如果响应包含个性化内容并且你只想将响应存储在私有缓存中,则必须指定 private 指令。


Cache-Control: private

个性化内容通常由 cookie 控制,但 cookie 的存在并不能表明它是私有的,因此单独的 cookie 不会使响应成为私有的。


请注意,如果响应具有 Authorization 标头,则不能将其存储在私有缓存(或共享缓存,除非 Cache-Control 指定的是 public)中。


共享缓存


共享缓存位于客户端和服务器之间,可以存储能在用户之间共享的响应。共享缓存可以进一步细分为代理缓存托管缓存


代理缓存


除了访问控制的功能外,一些代理还实现了缓存以减少网络流量。这通常不由服务开发人员管理,因此必须由恰当的 HTTP 标头等控制。然而,在过去,过时的代理缓存实现——例如没有正确理解 HTTP 缓存标准的实现——经常给开发人员带来问题。


Kitchen-sink 标头如下所示,用于尝试解决不理解当前 HTTP 缓存规范指令(如 no-store)的“旧且未更新的代理缓存”的实现。


Cache-Control: no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate

然而,近年来,随着 HTTPS 变得越来越普遍,客户端/服务器通信变得加密,在许多情况下,路径中的代理缓存只能传输响应而不能充当缓存。因此,在这种情况下,无需担心甚至无法看到响应的过时代理缓存的实现。


另一方面,如果 TLS 桥接代理通过在 PC 上安装来自组织管理的 CA 证书,以中间人方式解密所有通信,并执行访问控制等,则可以查看响应的内容并将其缓存。但是,由于证书透明度(certificate transparency)在最近几年变得很普遍,并且一些浏览器只允许使用证书签署时间戳(signed certificate timestamp)颁发的证书,因此这种方法需要应用于企业策略。在这样的受控环境中,无需担心代理缓存“已过时且未更新”。


托管缓存


托管缓存由服务开发人员明确部署,以降低源服务器负载并有效地交付内容。示例包括反向代理、CDN 和 service worker 与缓存 API 的组合。


托管缓存的特性因部署的产品而异。在大多数情况下,你可以通过 Cache-Control 标头和你自己的配置文件或仪表板来控制缓存的行为。


例如,HTTP 缓存规范本质上没有定义显式删除缓存的方法——但是使用托管缓存,可以通过仪表板操作、API 调用、重新启动等实时删除已经存储的响应。这允许更主动的缓存策略。


也可以忽略标准 HTTP 缓存规范协议以支持显式操作。例如,可以指定以下内容以选择退出私有缓存或代理缓存,同时使用你自己的策略仅在托管缓存中进行缓存。


Cache-Control: no-store

例如,Varnish Cache 使用 VCL(Varnish Configuration Language,一种 DSL逻辑来处理缓存存储,而 service worker 结合缓存 API 允许你在 JavaScript 中创建该逻辑。


这意味着如果托管缓存故意忽略 no-store 指令,则无需将其视为“不符合”标准。你应该做的是,避免使用 kitchen-sink 标头,但请仔细阅读你正在使用的任何托管缓存机制的文档,并确保你选择的方式可以正确的控制缓存。


请注意,某些 CDN 提供自己的标头,这些标头仅对该 CDN 有效(例如,Surrogate-Control)。目前,正在努力定义一个 CDN-Cache-Control 标头来标准化这些标头。


缓存的类型


启发式缓存


HTTP 旨在尽可能多地缓存,因此即使没有给出 Cache-Control,如果满足某些条件,响应也会被存储和重用。这称为启发式缓存


例如,采取以下响应。此回复最后一次更新是在 1 年前。


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2021 22:22:22 GMT

<!doctype html>


试探性地知道,整整一年没有更新的内容在那之后的一段时间内不会更新。因此,客户端存储此响应(尽管缺少 max-age)并重用它一段时间。复用多长时间取决于实现,但规范建议存储后大约 10%(在本例中为 0.1 年)的时间。


启发式缓存是在 Cache-Control 被广泛采用之前出现的一种解决方法,基本上所有响应都应明确指定 Cache-Control 标头。


基于 age 的缓存策略


存储的 HTTP 响应有两种状态:freshstalefresh 状态通常表示响应仍然有效,可以重复使用,而 stale 状态表示缓存的响应已经过期。


确定响应何时是 fresh 的和何时是 stale 的标准是 age。在 HTTP 中,age 是自响应生成以来经过的时间。这类似于其他缓存机制中的 TTL


以下面的示例响应为例(604800 秒是一周):


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Cache-Control: max-age=604800

<!doctype html>


存储示例响应的缓存会计算响应生成后经过的时间,并将结果用作响应的 age


对于该示例的响应,max-age 的含义如下:



  • 如果响应的 age 小于一周,则响应为 fresh

  • 如果响应的 age 超过一周,则响应为 stale


只要存储的响应保持新鲜(fresh),它将用于兑现客户端请求。


当响应存储在共享缓存中时,有必要通知客户端响应的 age。继续看示例,如果共享缓存将响应存储了一天,则共享缓存将向后续客户端请求发送以下响应。


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Cache-Control: max-age=604800
Age: 86400

<!doctype html>


收到该响应的客户端会发现它在剩余的 518400 秒内是新鲜(fresh)的,这是响应的 max-ageAge 之间的差异。


Expires 或 max-age


在 HTTP/1.0 中,新鲜度过去由 Expires 标头指定。


Expires 标头使用明确的时间而不是通过指定经过的时间来指定缓存的生命周期。


Expires: Tue, 28 Feb 2022 22:22:22 GMT

但是时间格式难以解析,也发现了很多实现的错误,有可能通过故意偏移系统时钟来诱发问题;因此,在 HTTP/1.1 中,Cache-Control 采用了 max-age——用于指定经过的时间。


如果 ExpiresCache-Control: max-age 都可用,则将 max-age 定义为首选。因此,由于 HTTP/1.1 已被广泛使用,无需特地提供 Expires


Vary 响应


区分响应的方式本质上是基于它们的 URL:


使用 url 作为键


但是响应的内容并不总是相同的,即使它们具有相同的 URL。特别是在执行内容协商时,来自服务器的响应可能取决于 AcceptAccept-LanguageAccept-Encoding 请求标头的值。


例如,对于带有 Accept-Language: en 标头并已缓存的英语内容,不希望再对具有 Accept-Language: ja 请求标头的请求重用该缓存响应。在这种情况下,你可以通过在 Vary 标头的值中添加“Accept-Language”,根据语言单独缓存响应。


Vary: Accept-Language

这会导致缓存基于响应 URLAccept-Language请求标头的组合进行键控——而不是仅仅基于响应 URL。


使用 url 和语言作为键


此外,如果你基于用户代理提供内容优化(例如,响应式设计),你可能会想在 Vary 标头的值中包含“User-Agent”。但是,User-Agent 请求标头通常具有非常多的变体,这大大降低了缓存被重用的机会。因此,如果可能,请考虑一种基于特征检测而不是基于 User-Agent 请求标头来改变行为的方法。


对于使用 cookie 来防止其他人重复使用缓存的个性化内容的应用程序,你应该指定 Cache-Control: private 而不是为 Vary 指定 cookie。


验证响应


过时的响应不会立即被丢弃。HTTP 有一种机制,可以通过询问源服务器将陈旧的响应转换为新的响应。这称为验证,有时也称为重新验证


验证是通过使用包含 If-Modified-SinceIf--Match 请求标头的条件请求完成的。


If-Modified-Since


以下响应在 22:22:22 生成,max-age 为 1 小时,因此你知道它在 23:22:22 之前是新鲜的。


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600

<!doctype html>


到 23:22:22 时,响应会过时并且不能重用缓存。因此,下面的请求显示客户端发送带有 If-Modified-Since 请求标头的请求,以询问服务器自指定时间以来是否有任何的改变。


GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If-Modified-Since: Tue, 22 Feb 2022 22:00:00 GMT

如果内容自指定时间以来没有更改,服务器将响应 304 Not Modified


由于此响应仅表示“没有变化”,因此没有响应主体——只有一个状态码——因此传输大小非常小。


HTTP/1.1 304 Not Modified
Content-Type: text/html
Date: Tue, 22 Feb 2022 23:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600

收到该响应后,客户端将存储的陈旧响应恢复为新鲜的,并可以在剩余的 1 小时内重复使用它。


服务器可以从操作系统的文件系统中获取修改时间,这对于提供静态文件的情况来说是比较容易做到的。但是,也存在一些问题;例如,时间格式复杂且难以解析,分布式服务器难以同步文件更新时间。


为了解决这些问题,ETag 响应标头被标准化作为替代方案。


ETag/If--Match


ETag 响应标头的值是服务器生成的任意值。服务器对于生成值没有任何限制,因此服务器可以根据他们选择的任何方式自由设置值——例如主体内容的哈希或版本号。


举个例子,如果 ETag 标头使用了 hash 值,index.html 资源的 hash 值是 deadbeef,响应如下:


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
ETag: "deadbeef"
Cache-Control: max-age=3600

<!doctype html>


如果该响应是陈旧的,则客户端获取缓存响应的 ETag 响应标头的值,并将其放入 If--Match 请求标头中,以询问服务器资源是否已被修改:


GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If--Match: "deadbeef"

如果服务器为请求的资源确定的 ETag 标头的值与请求中的 If--Match 值相同,则服务器将返回 304 Not Modified


但是,如果服务器确定请求的资源现在应该具有不同的 ETag 值,则服务器将其改为 200 OK 和资源的最新版本进行响应。



备注: 在评估如何使用 ETagLast-Modified 时,请考虑以下几点:在缓存重新验证期间,如果 ETagLast-Modified 都存在,则 ETag 优先。因此,如果你只考虑缓存,你可能会认为 Last-Modified 是不必要的。然而,Last-Modified 不仅仅对缓存有用;相反,它是一个标准的 HTTP 标头,内容管理 (CMS) 系统也使用它来显示上次修改时间,由爬虫调整爬取频率,以及用于其他各种目的。所以考虑到整个 HTTP 生态系统,最好同时提供 ETagLast-Modified



强制重新验证


如果你不希望重复使用响应,而是希望始终从服务器获取最新内容,则可以使用 no-cache 指令强制验证。


通过在响应中添加 Cache-Control: no-cache 以及 Last-ModifiedETag——如下所示——如果请求的资源已更新,客户端将收到 200 OK 响应,否则,如果请求的资源尚未更新,则会收到 304 Not Modified 响应。


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
ETag: deadbeef
Cache-Control: no-cache

<!doctype html>


max-age=0must-revalidate 的组合与 no-cache 具有相同的含义。


Cache-Control: max-age=0, must-revalidate

max-age=0 意味着响应立即过时,而 must-revalidate 意味着一旦过时就不得在没有重新验证的情况下重用它——因此,结合起来,语义似乎与 no-cache 相同。


然而,max-age=0 的使用是解决 HTTP/1.1 之前的许多实现无法处理 no-cache 这一指令——因此为了解决这个限制,max-age=0 被用作解决方法。


但是现在符合 HTTP/1.1 的服务器已经广泛部署,没有理由使用 max-age=0must-revalidate 组合——你应该只使用 no-cache


不使用缓存


no-cache 指令不会阻止响应的存储,而是阻止在没有重新验证的情况下重用响应。


如果你不希望将响应存储在任何缓存中,请使用 no-store


Cache-Control: no-store

但是,一般来说,实践中“不缓存”的原因满足以下情况:



  • 出于隐私原因,不希望特定客户以外的任何人存储响应。

  • 希望始终提供最新信息。

  • 不知道在过时的实现中会发生什么。


在这种情况下,no-store 并不总是最合适的指令。


以下部分更详细地介绍了这些情况。


不与其他用户共享


如果具有个性化内容的响应意外地对缓存的其他用户可见,那将是有问题的。


在这种情况下,使用 private 指令将导致个性化响应仅与特定客户端一起存储,而不会泄露给缓存的任何其他用户。


Cache-Control: private

在这种情况下,即使设置了 no-store,也必须设置 private


每次都提供最新的内容


no-store 指令阻止存储响应,但不会删除相同 URL 的任何已存储响应。


换句话说,如果已经为特定 URL 存储了旧响应,则返回 no-store 不会阻止旧响应被重用。


但是,no-cache 指令将强制客户端在重用任何存储的响应之前发送验证请求。


Cache-Control: no-cache

如果服务端不支持条件请求,你可以强制客户端每次都访问服务端,总是得到最新的 200 OK 响应。


兼容过时的实现


作为忽略 no-store 的过时实现的解决方法,你可能会看到使用了诸如以下内容的 kitchen-sink 标头:


Cache-Control: no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate

推荐使用 no-cache 作为处理这种过时的实现的替代方案,如果从一开始就设置 no-cache 就没问题,因为服务器总是会收到请求。


如果你关心的是共享缓存,你可以通过添加 private 来防止意外缓存:


Cache-Control: no-cache, private

no-store 丢失了什么


你可能认为添加 no-store 是选择退出缓存的正确方法。


但是,不建议随意授予 no-store,因为你失去了 HTTP 和浏览器所拥有的许多优势,包括浏览器的后退/前进缓存。


因此,要获得 Web 平台的全部功能集的优势,最好将 no-cacheprivate 结合使用。


重新加载和强制重新加载


可以对请求和响应执行验证。


重新加载强制重新加载操作是从浏览器端执行验证的常见示例。


重新加载


为了从页面错误中恢复或更新到最新版本的资源,浏览器为用户提供了重新加载功能。


在浏览器重新加载期间发送的 HTTP 请求的简化视图如下所示:


GET / HTTP/1.1
Host: example.com
Cache-Control: max-age=0
If--Match: "deadbeef"
If-Modified-Since: Tue, 22 Feb 2022 20:20:20 GMT

请求中的 max-age=0 指令指定“重用 age 为 0 或更少的响应”——因此,中间存储的响应不会被重用。


请求通过 If--MatchIf-Modified-Since 进行验证。


该行为也在 Fetch 标准中定义,并且可以通过在缓存模式设置为 no-cache 的情况下,在 JavaScript 中调用 fetch() 来重现(注意 reload 不是这种情况下的正确模式):


// 注意:“reload”不是正常重新加载的正确模式;“no-cache”才是
fetch("/", { cache: "no-cache" });

强制重新加载


出于向后兼容的原因,浏览器在重新加载期间使用 max-age=0——因为在 HTTP/1.1 之前的许多过时的实现中不理解 no-cache。但是在这个用例中,no-cache 已被支持,并且强制重新加载是绕过缓存响应的另一种方法。


浏览器强制重新加载期间的 HTTP 请求如下所示:


GET / HTTP/1.1
Host: example.com
Pragma: no-cache
Cache-Control: no-cache

由于这不是带有 no-cache 的条件请求,因此你可以确定你会从源服务器获得 200 OK


该行为也在 Fetch 标准中定义,并且可以通过在缓存模式设置为 reload 的情况下,在 JavaScript 中调用 fetch() 来重现(注意它不是 force-reload):


// 注意:“reload”——而不是“no-cache”——是“强制重新加载”的正确模式
fetch("/", { cache: "reload" });

避免重新验证


永远不会改变的内容应该被赋予一个较长的 max-age,方法是使用缓存破坏——也就是说,在请求 URL 中包含版本号、哈希值等。


但是,当用户重新加载时,即使服务器知道内容是不可变的,也会发送重新验证请求。


为了防止这种情况,immutable 指令可用于明确指示不需要重新验证,因为内容永远不会改变。


Cache-Control: max-age=31536000, immutable

这可以防止在重新加载期间进行不必要的重新验证。


删除存储的响应


基本上没有办法删除用很长的 max-age 存储的响应。


想象一下,来自 https://example.com/ 的以下响应已被存储。


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Cache-Control: max-age=31536000

<!doctype html>


一旦响应在服务器上过期,你可能希望覆盖该响应,但是一旦存储响应,服务器就无法执行任何操作——因为由于缓存,不再有请求到达服务器。


规范中提到的方法之一是使用不安全的方法(例如 POST)发送对同一 URL 的请求,但对于许多客户端而言,通常很难故意这样做。


还有一个 Clear-Site-Data: cache 标头和值的规范,但并非所有浏览器都支持它——即使使用它,它也只会影响浏览器缓存,而不会影响中间缓存。


因此,除非用户手动执行重新加载、强制重新加载或清除历史操作,否则应该假设任何存储的响应都将保留其 max-age 期间。


缓存减少了对服务器的访问,这意味着服务器失去了对该 URL 的控制。如果服务器不想失去对 URL 的控制——例如,在资源被频繁更新的情况下——你应该添加 no-cache,以便服务器始终接收请求并发送预期的响应。


请求折叠


共享缓存主要位于源服务器之前,旨在减少到源服务器的流量。


因此,如果多个相同的请求同时到达共享缓存,中间缓存将代表自己将单个请求转发到源,然后源可以将结果重用于所有客户端。这称为请求折叠


当请求同时到达时会发生请求折叠,因此即使响应中给出了 max-age=0no-cache,它也会被重用。


如果响应是针对特定用户个性化的,并且你不希望它在折叠中共享,则应添加 private 指令:


请求折叠


常见的缓存模式


Cache-Control 规范中有很多指令,可能很难全部理解。但是大多数网站都可以通过几种模式的组合来覆盖。


本节介绍设计缓存的常见模式。


默认设置


如上所述,缓存的默认行为(即对于没有 Cache-Control 的响应)不是简单的“不缓存”,而是根据所谓的“启发式缓存”进行隐式缓存。


为了避免这种启发式缓存,最好显式地为所有响应提供一个默认的 Cache-Control 标头。


为确保默认情况下始终传输最新版本的资源,通常的做法是让默认的 Cache-Control 值包含 no-cache


Cache-Control: no-cache

另外,如果服务实现了 cookie 或其他登录方式,并且内容是为每个用户个性化的,那么也必须提供 private,以防止与其他用户共享:


Cache-Control: no-cache, private

缓存破坏


最适合缓存的资源是静态不可变文件,其内容永远不会改变。而对于会变化的资源,通常的最佳实践是每次内容变化时都改变 URL,这样 URL 单元可以被缓存更长的时间。


例如,考虑以下 HTML:


<script src="bundle.js"></script>
<link rel="stylesheet" href="build.css" />
<body>
hello
</body>

在现代 Web 开发中,JavaScript 和 CSS 资源会随着开发的进展而频繁更新。此外,如果客户端使用的 JavaScript 和 CSS 资源的版本不同步,则显示将中断。


所以上面的 HTML 用 max-age 缓存 bundle.jsbuild.css 变得很困难。


因此,你可以使用包含基于版本号或哈希值的更改部分的 URL 来提供 JavaScript 和 CSS。一些方法如下所示。


# version in filename
bundle.v123.js

# version in query
bundle.js?v=123

# hash in filename
bundle.YsAIAAAA-QG4G6kCMAMBAAAAAAAoK.js

# hash in query
bundle.js?v=YsAIAAAA-QG4G6kCMAMBAAAAAAAoK

由于缓存根据它们的 URL 来区分资源,因此如果在更新资源时 URL 发生变化,缓存将不会再次被重用。


<script src="bundle.v123.js"></script>
<link rel="stylesheet" href="build.v123.css" />
<body>
hello
</body>

通过这种设计,JavaScript 和 CSS 资源都可以被缓存很长时间。那么 max-age 应该设置多长时间呢?QPACK 规范提供了该问题的答案。


QPACK 是一种用于压缩 HTTP 标头字段的标准,其中定义了常用字段值表。


一些常用的缓存头值如下所示。


36 cache-control max-age=0
37 cache-control max-age=604800
38 cache-control max-age=2592000
39 cache-control no-cache
40 cache-control no-store
41 cache-control public, max-age=31536000

如果你选择其中一个编号选项,则可以在通过 HTTP3 传输时将值压缩为 1 个字节。


数字“37”、“38”和“41”分别代表一周、一个月和一年。


因为缓存会在保存新条目时删除旧条目,所以一周后存储的响应仍然存在的可能性并不高——即使 max-age 设置为 1 周。因此,在实践中,你选择哪一种并没有太大的区别。


请注意,数字“41”具有最长的 max-age(1 年),但具有 public


public 值具有使响应可存储的效果,即使存在 Authorization 标头。



备注: 只有在设置了 Authorization 标头时需要存储响应时才应使用 public 指令。否则不需要,因为只要给出了 max-age,响应就会存储在共享缓存中。



因此,如果响应是使用基本身份验证进行个性化的,public 的存在可能会导致问题。如果你对此感到担忧,你可以选择第二长的值 38(1 个月)。


# response for bundle.v123.js

# If you never personalize responses via Authorization
Cache-Control: public, max-age=31536000

# If you can't be certain
Cache-Control: max-age=2592000

验证响应


不要忘记设置 Last-ModifiedETag 标头,以便在重新加载时不必重新传输资源。对于预构建的静态文件生成这些标头很容易。


这里的 ETag 值可能是文件的哈希值。


# response for bundle.v123.js
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: YsAIAAAA-QG4G6kCMAMBAAAAAAAoK

此外,可以添加 immutable 以防止重新加载时验证。


组合结果如下所示。


# bundle.v123.js
200 OK HTTP/1.1
Content-Type: application/javascript
Content-Length: 1024
Cache-Control: public, max-age=31536000, immutable
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: YsAIAAAA-QG4G6kCMAMBAAAAAAAoK

缓存破坏是一种通过在内容更改时更改 URL 来使响应在很长一段时间内可缓存的技术。该技术可以应用于所有子资源,例如图像。


备注: 在评估 immutable 和 QPACK 的使用时:如果你担心 immutable 会更改 QPACK 提供的预定义值,请考虑在这种情况下,immutable 部分可以通过将 Cache-Control 值分成两行来单独编码——尽管这取决于特定 QPACK 实现使用的编码算法。


Cache-Control: public, max-age=31536000
Cache-Control: immutable

主要资源


与子资源不同,主资源不能使用缓存破坏,因为它们的 URL 不能像子资源 URL 一样被修饰。


如果存储以下 HTML 本身,即使在服务器端更新内容,也无法显示最新版本。


<script src="bundle.v123.js"></script>
<link rel="stylesheet" href="build.v123.css" />
<body>
hello
</body>

对于这种情况,no-cache 将是合适的——而不是 no-store——因为我们不想存储 HTML,而只是希望它始终是最新的。


此外,添加 Last-ModifiedETag 将允许客户端发送条件请求,如果 HTML 没有更新,则可以返回 304 Not Modified


200 OK HTTP/1.1
Content-Type: text/html
Content-Length: 1024
Cache-Control: no-cache
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: AAPuIbAOdvAGEETbgAAAAAAABAAE

该设置适用于非个性化 HTML,但对于使用 cookie 进行个性化的响应(例如,在登录后),不要忘记同时指定 private


200 OK HTTP/1.1
Content-Type: text/html
Content-Length: 1024
Cache-Control: no-cache, private
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: AAPuIbAOdvAGEETbgAAAAAAABAAE
Set-Cookie: __Host-SID=AHNtAyt3fvJrUL5g5tnGwER; Secure; Path=/; HttpOnly

favicon.icomanifest.json.well-known 和无法使用缓存破坏更改 URL 的 API 端点也是如此。


大多数 Web 内容都可以通过上述两种模式的组合来覆盖。


有关托管缓存的更多信息


使用前面章节描述的方法,子资源可以通过缓存破坏来缓存很长时间,但主资源(通常是 HTML 文档)不能。


缓存主要资源很困难,因为仅使用 HTTP 缓存规范中的标准指令,在服务器上更新内容时无法主动删除缓存内容。


但是,可以通过部署托管缓存(例如 CDN 或 service worker)来实现。


例如,允许通过 API 或仪表板操作清除缓存的 CDN 将通过存储主要资源并仅在服务器上发生更新时显式清除相关缓存来实现更积极的缓存策略。


如果 service worker 可以在服务器上发生更新时删除缓存 API 中的内容,它也可以这样做。


作者:demo007x
来源:juejin.cn/post/7237022394790281271
收起阅读 »

Vue+Element-UI 中 el-table 动态合并单元格 :span-method 方法

web
合并单元格 记录一下工作时遇到的 el-table 合并单元格的需求,超详细😊 el-table官方提供了合并单元格的方法与返回格式 如下: 根据叙述有了如下思路: 因为后端返回的数据非统一, 可能不是按照类别排好的😨, 所以官网的例子满足不了所有的需求...
继续阅读 »
合并单元格


记录一下工作时遇到的 el-table 合并单元格的需求,超详细😊



el-table官方提供了合并单元格的方法与返回格式 如下:

在这里插入图片描述

根据叙述有了如下思路:

因为后端返回的数据非统一, 可能不是按照类别排好的😨, 所以官网的例子满足不了所有的需求所以我们通过遍历table的数据比较前后两个元素是否相等, 来构造一个spanArr用来存放rowspan, 最后通过rowspan的值来判断colspan的值😊.


案例如下, 这是我需要处理的一个表格:

需要根据数据动态的合并

在这里插入图片描述

对应的配置数组为

在这里插入图片描述


处理数据


因为获取的数据的非统一性, 我们首先要将数据根据我们想要合并的字段进行排序分组, 这里我实现了一个简单的方法来处理数据:


// data 为 表格数据 , params 为需要合并的字段
groupBy (data, params) {
const groups = {};
data.forEach(v => {
// 获取data中的传入的params属性对应的属性值
const group = JSON.stringify(v[params]);
// 把group作为groups的key,初始化value,循环时找到相同的v[params]时不变
groups[group] = groups[group] || [];
// 将对应找到的值作为value放入数组中
groups[group].push(v);
})
// 返回处理好的二维数组
return Object.values(groups);
},

此时打印一下我们的数据console.log(this.groupBy(this.tableListData.items, 'FirstIndex'))

在这里插入图片描述

如图, 我们已经将数据分好组并合并在一个数组中啦, FirstIndex相同的在一个数组


构造控制合并的数组spanArr


这里实现了一个方法, 用来构造一个spanArr数组赋予rowspan,即控制行合并



  • 接收重构数组 let arr = []

  • 设置索引 let pos = 0

  • 控制合并的数组 this.spanArr = []


先将groupby()处理好的数据再次用arr进行处理:连接所有数组成员为一个新数组

this.groupBy(this.tableListData.items, 'FirstIndex').map(v => (arr = arr.concat(v)))


现在处理好了数据,需要赋予原数据了:this.tableListData.items = arr


但是因为我是写在getSpanArr(data, params)方法中的,已经通过形参data将 this.tableListData.items传入了这里,如果想方便封装调用的话,不用每次使用都需要再次写入 this.tableListData.items = arr

于是想到一个办法,js数组的shift()和push()是直接修改数组所占内存的方法。

所以有:


arr.map(res => {
// 每次遍历都删除data && this.tableListData.items的第一个元素
data.shift()
// 每次遍历都将arr数组元素对应push进 data && this.tableListData.items
data.push(res)
})

还需要定义一个redata存放arr要合并字段的value

const redata = arr.map(v => v[params])


reduce处理spanArr数组 ⭐⭐


使用reduce方法比较redata前后两个元素是否相等,相等的话spanArr中对应索引的元素的值+1,并且在其后增加一个0占位(防止合并过后表格数据错位),否则的话增加一个1占位,并记录当前索引,往复循环,构造一个给 rowspan 取值判断合并的数组:


  const redata = arr.map(v => v[params])
redata.reduce((old, cur, i) => {
// old 上一个元素 cur 当前元素 i 索引
if (i === 0) {
// 第一次判断先增加一个 1 占位 ,索引为0
this.spanArr.push(1)
pos = 0
} else {
if (cur === old) {
this.spanArr[pos] += 1
this.spanArr.push(0)
} else {
this.spanArr.push(1)
pos = i
}
}
return cur
}, {})

看一下现在的数据spanArr, 这里传的参数为SecondIndex, 即表格的第二列

在这里插入图片描述

数组中大于0的数字就是我们数据中要合并的这组数据的数量, 同时也是这组数据需要合并的列数,而0就是代表这列不合并, 依次遍历,实现合并所选字段这一列的最终目的 如图理解:

在这里插入图片描述


返回最终结果


最后一步啦😊根据官方给的方法把我们处理好的spanArr传给rowspan即可


spanMethod({ row, column, rowIndex, columnIndex }) {
// 第一列
if (columnIndex === 0) {
const _row = this.spanArr[rowIndex];
const _col = _row > 0 ? 1 : 0;
return {
rowspan: _row,
colspan: _col
}
}
}

效果如图!

在这里插入图片描述


完整代码


就很nice, !!最后把完整代码贴上:


// ......
mounted() {
this.getSpanArr(this.tableListData.items, 'FirstIndex');
},
methods: {
groupBy (data, params) {
const groups = {}
data.forEach(v => {
const group = JSON.stringify(v[params])
groups[group] = groups[group] || []
groups[group].push(v)
})
return Object.values(groups)
},
getSpanArr (data, params) {
let arr = []
let pos = 0
this.spanArr = []
this.groupBy(data, params).map(v => (arr = arr.concat(v)))
arr.map(res => {
data.shift()
data.push(res)
})
const redata = arr.map(v => v[params])
redata.reduce((old, cur, i) => {
if (i === 0) {
this.spanArr.push(1)
pos = 0
} else {
if (cur === old) {
this.spanArr[pos] += 1
this.spanArr.push(0)
} else {
this.spanArr.push(1)
pos = i
}
}
return cur
}, {})
},
spanMethod({ row, column, rowIndex, columnIndex }) {
if (columnIndex === 0) {
const _row = this.spanArr[rowIndex];
const _col = _row > 0 ? 1 : 0;
return {
rowspan: _row,
colspan: _col
}
}
}
}

完美! 撒花!!!🎉🎉🎉


作者:小星星__
来源:juejin.cn/post/7238478149049483301
收起阅读 »

改写el-table表格排序, 支持多列排序远程排序!!!

web
改写el-table的默认排序 提示:在el-table封装的表格基础上改写排序方法 前言 我们在做表格的时候经常会遇到表头有一个排序的icon 用来对数据进行, el-table有自己的排序方法, 如下: 在列中设置sortable属性即可实现以该列为基...
继续阅读 »

改写el-table的默认排序


提示:在el-table封装的表格基础上改写排序方法




前言


我们在做表格的时候经常会遇到表头有一个排序的icon 用来对数据进行, el-table有自己的排序方法, 如下:



在列中设置sortable属性即可实现以该列为基准的排序,接受一个Boolean,默认为false。





一、el-table支持调接口排序吗?


el-table默认的排序支持从接口获取排序的数据



sortable: 对应列是否可以排序,如果设置为 custom,则代表用户希望远程排序,需要监听 Table 的 sort-change 事件



二、el-table支持多列排序吗?


默认的排序很简单, 加一个参数就可以了, 而且会自动根据数据进行排序, 但是我们会发现, 默认的排序只支持一列进行排序, 当我们排过一列之后在点击另一列的排序图标, 之前的排序就会消失😨.


三、如何实现多列远程排序?



  1. 自己写一个组件插入到表头的位置实现排序

  2. 根据el-table已有的属性以及抛出的方法实现多列排序


如果手动封装一个组件肯定能实现, 但是比较麻烦, 所以就研究了el-table相关了一些属性和方法, 思路如下:



header-cell-class-name: 表头单元格的 className 的回调方法,也可以使用字符串为所有表头单元格设置一个固定的className



在点击表头的时候排序的列以及是升降序保存到一个数组对象ordersList里, 然后通过header-cell-class-name属性设置选中的样式.


四、核心代码


	data: {
return {
ordersList: [],
}
}
// 点击表头
handleHeaderCLick(column){
if (column.sortable !== 'custom') {
return
}
if (!column.multiOrder) {
column.multiOrder = 'descending'
} else if (column.multiOrder === 'descending') {
column.multiOrder = 'ascending'
} else {
column.multiOrder = ''
}
this.handleOrderChange(column.property, column.multiOrder)

},
handleOrderChange (orderColumn, orderState) {
let result = this.ordersList.find(e => e.orderColumn === orderColumn)
if (result) {
result.orderState = orderState
} else {
this.ordersList.push({
orderColumn: orderColumn,
orderState: orderState,
})
}
// 调接口查询,在传参的时候把ordersList进行处理成后端想要的格式(这里是把数据抛出, 外部调用组件的地方处理)
this.sendInfo(this.ordersList, 'sort-change')
},
// 上面缺点是只能通过点击表头切换排序状态,点击小三角排序不会触发,处理sort-change事件和点击表头一样
sortChange({column}) {
// 有些列不需要排序,提前返回
if (column.sortable !== 'custom') {
return
}
if (!column.multiOrder) {
column.multiOrder = 'descending'
} else if (column.multiOrder === 'descending') {
column.multiOrder = 'ascending'
} else {
column.multiOrder = ''
}
this.handleOrderChange(column.property, column.multiOrder)
},
// 设置列的排序为我们自定义的排序
handleHeaderClass({ column }) {
column.order = column.multiOrder
}

这样外部拿到的就是一个所有排序的数组, 包括prop以及当前列的排序规则(ascending/descending/null), 将其处理成正确的入参格式即可.




在这里插入图片描述

在这里插入图片描述


如此, 就实现了多列远程排序, 欢迎大家一起讨论学习😊~


作者:小星星__
来源:juejin.cn/post/7238479015723089980
收起阅读 »

2023了,该用一下pnpm了

web
前言 大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题。 performant npm ,意味高性能的 npm。pnpm由 npm/yarn 衍生而来,解决了 npm/yarn 内部潜在的bug,极大的优化了性能,扩展了使用场景。...
继续阅读 »

前言


大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题


performant npm ,意味高性能的 npm。pnpm由 npm/yarn 衍生而来,解决了 npm/yarn 内部潜在的bug,极大的优化了性能,扩展了使用场景。被誉为"最先进的包管理工具"。


npm,yarn,pnpm的安装区别


首先我创建了三个文件夹分别是npm,yarn和pnpm用于比较三者之间的区别。首先初始化项目,然后安装了express来观察三个文件夹的区别。


npm和yarn的node_modules都是点开之后一眼看不到尽头。


image.png image.png


pnpm的node_modules略有区别。


image.png


npm/yarn 包结构分析


出现这种情况,是因为yarn和npm安装依赖包,会在node_modules下都平铺出来。现在安装了express,但express中也会有很多不同的依赖。在这些依赖里面有可能又引用了新的依赖,导致node_modules一点开就一望无际了。


初心是好的,因为平铺就可以复用很多依赖。如果说我package Apackage B都用了lodash@3.0.0这个包,那么使用平铺,我只需要下载一次lodash即可,节约了装包时间和存储空间。


但是真实开发情况往往是现在下载了五个依赖,其中A,B依赖引用了lodash@3.0.0,而C,D,E引用了lodash@4.0.0。咋整?


npmyarn目前给出的方案是将其中一个版本,(假设是)lodash@3.0.0的版本放在根目录的node_modules下面,而将需要lodash@4.0.0版本安装到C,D,E的node_modules下。如下所示,A, B可以直接使用lodash@3.0.0,而4,5,6想要使用就只能独自安装lodash@4.0.0


├─── lodash@3.0.0
├─── package-A
├─── package-B
├─── package-C
│ └── lodash@4.0.0
├─── package-D
│ └── lodash@4.0.0
├─── package-E
│ └── lodash@4.0.0

pnpm包结构分析


按照上文的例子,如果pnpm也安装五个包,A,B依赖引用了lodash@3.0.0,而C,D,E引用了lodash@4.0.0


├─ .pnpm
│ └── lodash@3.0.0
│ └── lodash@4.0.0
│ └── package-A@1.0.0
│ └── package-B@1.0.0
│ └── package-C@1.0.0
│ └── package-D@1.0.0
│ └── package-E@1.0.0
├──── package-A 符号链接
├──── package-B 符号链接
├──── package-C 符号链接
├──── package-D 符号链接
├──── package-E 符号链接

pnpm文件夹的node_modules下除了.pnpm文件夹外,就剩下一个package A,B,C,D,E,并且这五个包都是符号链接,它们真正的地址都是在.pnpm下。


也就是说,pnpm通过.pnpm/<name>@<version>/node_modules/<name>找到不同的包,这不仅解决了包重复下载的问题,还顺手解决了幽灵依赖的问题。



幽灵依赖:即开发者并未在package.json中下载相关包,但是在开发过程中却可以直接引用的问题。就是因为npm将依赖直接在node_modules下直接展开,导致开发者可以直接引用。问题就是当开发者升级一些包的时候,那些幽灵依赖可能并不存在于新的版本中,导致项目崩溃。



.pnpm store


pnpm牛皮的地方不只是借用了符号链接解决了包引用的问题,更是借助了硬链接解决了整个直接所有的项目依赖都给整合了,一个包全局只保存一份,并且是通过链接,速度比复制还要快的多。


借一张pnpm官网的图。


image.png


从图可以看出,.pnpm store就是依赖的实际存储位置,Mac/linux在{home dir}>/.pnpm-store/v3,windows在当前盘/.pnpm-store/v3。这样就会有个好处,你在多个项目使用的是同一个依赖时,一个包全局只保存一份,这也太省空间了吧。(只要你下载过一次,如果你没有清理.pnpm store,第二次就算你不联网照样能帮你install。)


pnpm store清理


但是,随着使用时间越长,pnpm store也会越来越大。并且随着项目版本的迭代,可能很多包都不再需要了pnpm store依旧会保留着它。此时我们需要定时清理一下。


未引用的包是系统上的任何项目中都未使用的包。 在大多数安装操作之后,包有可能会变为未引用状态,例如当依赖项变得多余时。


最好的做法是 pnpm store prune 来清理存储,但不要太频繁。 有时,未引用的包会再次被需要。 这可能在切换分支和安装旧的依赖项时发生,在这种情况下,pnpm 需要重新下载所有删除的包,会暂时减慢安装过程。


请注意,当 存储服务器正在运行时,这个命令是禁止使用的。


pnpm store prune

作者:simple_lau
来源:juejin.cn/post/7237856777588670521
收起阅读 »

身为Ikun,我想用console.log输出giegie打球的视频~

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。 事情是这样的,这天我醒来,觉得身为一个 “ikun”,我得向我的giegie看齐,早点把篮球水平练上去,早点追上我的giegie,于是我便开始了我的打球(打铁)...
继续阅读 »

前言


大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。


事情是这样的,这天我醒来,觉得身为一个 “ikun”,我得向我的giegie看齐,早点把篮球水平练上去,早点追上我的giegie,于是我便开始了我的打球(打铁)之旅~


May-28-2023 23-36-14.gif


突发奇想


晚上,当我在分析着我的打球视频时,我又感觉我做这些是远远不够的!我要将这一切融入到前端里,让别人知道,咱们 ”ikun“ 是一个爱giegie,也爱学习的团体!我想要达到以下的效果


May-27-2023 23-47-31.gif


于是我想,我能不能把这个打球(打铁)视频,在控制台里 console.log 出来呢?我在心里演练了一遍,我觉得是可行的,我的思路有两个


直接用 console.log 输出视频?


好吧,目前 console.log 不支持输出视频吧~此路不通啊!


细分成每一帧去输出?


这个方式的思路具体分为以下几步:



  • 捕获视频的每一帧

  • 将每一帧转换成图片

  • 使用console.log输出生图片


如何捕获视频的每一帧?


使用 video 的 requestVideoFrameCallback 方法即可,requestVideoFrameCallback() 是一个新的WEB API,2021 年 1 月 25 日提交的草案。requestVideoFrameCallback() 方法允许WEB开发者注册一个回调方法,回调方法在新视频帧发送到合成器时在渲染步骤中运行。这是为了让开发人员对视频执行高效的每帧视频操作,例如视频处理和绘制到画布上(截屏)、视频分析或与外部音频源同步。


如何将每一帧转换成图片?


这得使用 canvas来完成,主要依赖了两个方法



  • ctx.drawImage:将一帧画面画到 canvas 上

  • canvas.toDataURL('image/png'):将 canvas 画布上的图像转成base64的URL


console.log 能输出图片?


console.log 是可以输出图片了,这一特性很久前就有了~不信你们复制以下代码,去尝试一下~


image.png


console.log(
"%c image",
`background-image: url(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ca412c9f87fb4df1b5402a5ad64474f1~tplv-k3u1fbpfcp-watermark.image?);
background-size: contain;
background-repeat: no-repeat;
padding: 200px;
`

)

开始实现吧~


那我们开始实现吧,我这边的技术栈是 Vue3哦~


先看看效果


先看看效果是怎么样的


May-28-2023 23-36-26.gif


初始化代码


我们需要 video 和 canvas,这两个标签,前者是视频标签,后者是画布标签


<template>
<video ref="videoRef" width="640" height="360" controls playsinline muted>
<source src="./kunkun.mp4" />
您的浏览器不支持 video 标签。
video>
<canvas ref="canvasRef" width="640" height="360">canvas>
template>

封装 useIKun


封装一个 Vue3 的 hooks,名为 useIKun,用来处理 ikun 的打球视频转换图片输出~




接下来我们开始封装 useIKun


import { onMounted } from 'vue'
import type { Ref } from 'vue'

const CANVAS_WIDTH_BASE = 220
const CANVAS_HEIGHT_BASE = 220

export const useIKun = ({
videoInstance,
canvasInstance,
}: {
videoInstance: Ref
canvasInstance: Ref
}
) => {
onMounted(() => {
// 播放视频
handleVideoPlay()
})

// 获取dom实例
const getInstances = () => {
const video = videoInstance.value
const canvas = canvasInstance.value
return {
video,
canvas,
}
}

// 获取canvas尺寸信息
const getCanvasSize = () => {
const { video } = getInstances()
// videoWidht、videoHeight视频原始宽度、高度(单位:px)
const width = video.videoWidth
const height = video.videoHeight

const rate = height / width

return {
width: CANVAS_WIDTH_BASE,
height: CANVAS_HEIGHT_BASE * rate,
}
}

const handleVideoPlay = () => {
const video = videoInstance.value
const canvas = canvasInstance.value
video.oncanplay = () => {
const { width, height } = getCanvasSize()
canvas.width = width
canvas.height = height
video.play() // 播放视频

// 判断HTMLVideoElement是否支持requestVideoFrameCallback()方法
if ('requestVideoFrameCallback' in video) {
// 下此视频帧呈现时触发回调
video.requestVideoFrameCallback(updateVideo)
}
}
}

/*
* 根据当前视频帧绘制图片
*/

const updateVideo = () => {
const video = videoInstance.value
const canvas = canvasInstance.value
const ctx = canvas.getContext('2d')
if (ctx) {
const { width, height } = getCanvasSize()
ctx.drawImage(video, 0, 0, width, height) // 使用视频帧(当前帧)绘制canvas
const dataURL = canvas.toDataURL('image/png')
console.log(
'%c image',
`background-image: url(${dataURL});
background-size: contain;
background-repeat: no-repeat;
padding: 200px;
`
,
)
video.requestVideoFrameCallback(updateVideo)
}
}
}

console.clear() ?


我们可以看到效果出来了,控制台里确实“播放了视频”,但其实不是视频,其实是以非常快的速度,去打印出一帧一帧的图片出来,由于速度很快,所以给你一种在放视频的假象,不信你看其实控制台里不止一个画面哦~


May-28-2023 23-36-26.gif


所以我们咋办,每次输出图片的时候用 console.clear() 清除一下吗?我们可以试试~


  const updateVideo = () => {
const video = videoInstance.value
const canvas = canvasInstance.value
const ctx = canvas.getContext('2d')
if (ctx) {
const { width, height } = getCanvasSize()
ctx.drawImage(video, 0, 0, width, height) // 使用视频帧(当前帧)绘制canvas
const dataURL = canvas.toDataURL('image/png')
// 清除
+ console.clear()
console.log(
'%c image',
`background-image: url(${dataURL});
background-size: contain;
background-repeat: no-repeat;
padding: 200px;
`
,
)
video.requestVideoFrameCallback(updateVideo)
}
}

再来看看效果~ 显然这样是不行的,console.clear 会导致闪烁问题~


May-28-2023 23-36-37.gif


Gif图?


所以我又有新思路,将每一帧的图像收集起来,然后组成一个 Gif 图,然后输出在控制台,不就行了!!!


gifshot


想要完成这个事情,就要借助这个库——gifshot,他的作用是可以把你传给他的图像数组,组成一个 gif图


重新封装 useIKun


 import { onMounted, ref } from 'vue'
import type { Ref } from 'vue'
import gifshot from 'gifshot'

const CANVAS_WIDTH_BASE = 220
const CANVAS_HEIGHT_BASE = 220
const IMG_SPAN = 5

export const useIKun = ({
videoInstance,
canvasInstance,
}: {
videoInstance: Ref
canvasInstance: Ref
}
) => {
const imgs = ref<string[]>([])

onMounted(() => {
// 播放视频
handleVideoPlay()
// 监听播放结束
onVideoEnded()
})

const getInstances = () => {
const video = videoInstance.value
const canvas = canvasInstance.value
return {
video,
canvas,
}
}

const getCanvasSize = () => {
const { video } = getInstances()
// videoWidht、videoHeight视频原始宽度、高度(单位:px)
const width = video.videoWidth
const height = video.videoHeight

const rate = height / width

return {
width: CANVAS_WIDTH_BASE,
height: CANVAS_HEIGHT_BASE * rate,
}
}

/*
* 控制视频播放
*/

const handleVideoPlay = () => {
const { video, canvas } = getInstances()
video.oncanplay = () => {
const { width, height } = getCanvasSize()
canvas.width = width
canvas.height = height
video.play() // 播放视频

// 判断HTMLVideoElement是否支持requestVideoFrameCallback()方法
if ('requestVideoFrameCallback' in video) {
// 下此视频帧呈现时触发回调
video.requestVideoFrameCallback(updateVideo)
}
}
}

/*
* 根据当前视频帧绘制图片
*/

const updateVideo = () => {
const { video, canvas } = getInstances()
const ctx = canvas.getContext('2d')
if (ctx) {
const { width, height } = getCanvasSize()
ctx.drawImage(video, 0, 0, width, height) // 使用视频帧(当前帧)绘制canvas
const dataURL = canvas.toDataURL('image/png')
imgs.value.push(dataURL)
video.requestVideoFrameCallback(updateVideo)
}
}

/*
* 监听视频停止播放
*/

const onVideoEnded = () => {
const { video } = getInstances()
video.onended = () => {
console.log('播放完了')
console.log(imgs.value.length)
const currentImgs = imgs.value
const resultImgs: string[] = []
currentImgs.forEach((img, index) => {
// 稀释图片数组,怕太大太久~你们也可以选择不走这一步
if (index % IMG_SPAN === 0) {
resultImgs.push(img)
}
})
// gifshot转换gif
gifshot.createGIF(
{
fps: 10,
width: 220,
height: 500,
images: resultImgs,
},
(obj) => {
console.log(obj)
if (!obj.error) {
const url = obj.image
// 输出最终的gif地址
console.log(
'%c image',
`background-image: url(${url});
background-size: contain;
background-repeat: no-repeat;
padding: 200px;
`
,
)
}
},
)
}
}
}


结果


哎,,,这画质,虽然比较糙,但是也算是本 ikun 为 giegie做出的一点点贡献了~


May-27-2023 23-31-48.gif


结语 & 加学习群 & 摸鱼群


我是林三心



  • 一个待过小型toG型外包公司、大型外包公司、小公司、潜力型创业公司、大公司的作死型前端选手;

  • 一个偏前端的全干工程师;

  • 一个不正经的掘金作者;

  • 一个逗比的B站up主;

  • 一个不帅的小红书博主;

  • 一个喜欢打铁的篮球菜鸟;

  • 一个喜欢历史的乏味少年;

  • 一个喜欢rap的五音不全弱鸡

作者:Sunshine_Lin
来源:juejin.cn/post/7238195267286138939
收起阅读 »

根据高德地图,画个心给你对象吧

web
文章来源 因为逛掘金的时候,看到了这篇文章,但是文章写的没头没尾,于是我就按照他给的思路自己实现了一下,感觉效果来不错,就在这里分享给大家一下。 高德地图接入 用这个申请的应用的key可以进行sdk的接入。可以通过高德地图的文档API来查询具体的API如何使...
继续阅读 »

文章来源


因为逛掘金的时候,看到了这篇文章,但是文章写的没头没尾,于是我就按照他给的思路自己实现了一下,感觉效果来不错,就在这里分享给大家一下。


image.png


高德地图接入


用这个申请的应用的key可以进行sdk的接入。可以通过高德地图的文档API来查询具体的API如何使用。


本文中主要使用高德地图的4个API



  • 初始化地图

  • 绘制路径(PathSimplifier)

  • 点标记(Marker)

  • 信息窗体(InfoWindow)


具体步骤



  1. 需要初始化地图,首先你要有一个容器去绘制地图,所以你要在html下有一个div容器,id随便起,然后使用new AMap.Map(#id)的方式去初始化地图,初始化的时候还可以带入参数(中心点、zoom)等。


   this.map = new AMap.Map('container', {
center:[x,y],
zoom: 10
})

这里你可以去查询当前你所在位置的坐标作为中心点传入,zoom我的案例里没有使用,因为使用了zoom,初始化的时候会卡一下,不知道具体原因。



  1. 绑定事件,获取心型坐标。给map绑定点击事件,每点击一次打一个标记点,然后你画一个心型,记录路径点。此处需要画左右两条路径哦


 this.map.on('click', (e) => {
const position = [+e.lnglat.getLng(), +e.lnglat.getLat()]
const marker = new AMap.Marker({
position: [+e.lnglat.getLng(), +e.lnglat.getLat()],
})
if (!window.list) {
window.list = [position]
} else {
window.list.push(position)
}
marker.setMap(this.map)
})

image.png
可以通过window.list来查看所有标记点的坐标,记住这些坐标,后面画线的时候要用哦。(tips:可以先画一条线,然后再控制台把window.list清空,再画另一条线。两条线的坐标点数量尽量一致,防止出现先后抵达的问题。)



  1. 下面你已经找好了两条线的坐标,下面你需要画两条线了


const initPath = () => {
AMapUI.load(['ui/misc/PathSimplifier'], (PathSimplifier) => {
if (!PathSimplifier.supportCanvas) {
alert('当前环境不支持 Canvas!')
return
}
//启动页面
this.pathSimplifierIns = new PathSimplifier({
zIndex: 100,
map: this.map, //所属的地图实例
getPath: function (pathData, pathIndex) {
//返回轨迹数据中的节点坐标信息,[AMap.LngLat, AMap.LngLat...] 或者 [[lng|number,lat|number],...]
return pathData.path
},
})
this.pathSimplifierIns.setData([
{
name: '轨迹1',
path: this.path1,
},
{
name: '轨迹2',
path: this.path2,
},
])
var navg0 = this.pathSimplifierIns.createPathNavigator(
0, //关联第1条轨迹
{
loop: false, //循环播放
speed: 800,
pathNavigatorStyle: {
width: 40,
height: 40,
autoRotate: false, // 禁止调整方向
// 经过路径的样式
pathLinePassedStyle: {
lineWidth: 6,
strokeStyle: 'black',
dirArrowStyle: {
stepSpace: 15,
strokeStyle: 'red',
},
},
//设置头像 不需要可以删除
content: PathSimplifier.Render.Canvas.getImageContent(
aJpg,
onload,
onerror,
),
},
},
)
var navg1 = this.pathSimplifierIns.createPathNavigator(
1, //关联第1条轨迹
{
loop: false, //循环播放
speed: 800,
pathNavigatorStyle: {
width: 40,
height: 40,
autoRotate: false, // 禁止调整方向
// 经过路径的样式
pathLinePassedStyle: {
lineWidth: 6,
strokeStyle: 'blue',
dirArrowStyle: {
stepSpace: 15,
strokeStyle: 'red',
},
},
//设置头像 不需要可以删除
content: PathSimplifier.Render.Canvas.getImageContent(
bJpg,
onload,
onerror,
),
},
},
)
// 设置定时器,方式map加载卡顿时,动画先开始
setTimeout(() => {
navg0.start()
navg1.start()
// 设置途径路上的 说的话
navg1.on('move', (e) => {
const idx = navg1.getCursor().idx // 走到了第几个点
const list = [
'不开门一直敲',
'是一种打扰',
'不回复本身',
'就是一种回复',
'双向奔赴才有意义',
]
let text = ''
if(idx < 3) {
text = list[0]
} else if(idx < 8) {
text = list[1]
} else if(idx < 13) {
text = list[2]
} else if(idx < 17) {
text = list[3]
} else {
text = list[4]
}
const cont = `<div class="toptit">
<p>${text}</p>
</div>`


// 设置气泡
this.infoWindow.setContent(cont)
this.infoWindow.open(this.map, e.target.getPosition())
})
}, 3000)
this.pathSimplifierIns.renderLater()
})
}


  1. 上面代码把信息窗体漏了,信息窗体也需要初始化,在初始化地图的后就行


  mounted() {
this.map = new AMap.Map('container',)
this.infoWindow = new AMap.InfoWindow({
offset: new AMap.Pixel(0, 0),
})
}

成品展示



这个demo里面,没有设置头像和要说的话,因为头像icon无法放上去,后续需要的画 可以自己添加哦。


结语


520已经过去了,这个就等着七夕给你们对象制造点浪漫吧。。


作者:哈库拉马塔塔
来源:juejin.cn/post/7236593783843913787
收起阅读 »

前端 markdown 到 pdf 生成方案

web
前端 markdown 到 pdf 生成方案 (检查修订中...) 接到需求,需要把数据同学生成的 markdown 格式的 ChatGPT 日报在平台上进行展示,并提供可下载的 PDF 文件。这里简单记录下使用到的技术和遇到的问题。 1.  方案对比 这个是...
继续阅读 »

前端 markdown 到 pdf 生成方案


(检查修订中...)


接到需求,需要把数据同学生成的 markdown 格式的 ChatGPT 日报在平台上进行展示,并提供可下载的 PDF 文件。这里简单记录下使用到的技术和遇到的问题。


1.  方案对比


这个是现在项目中使用的方案,整体步骤如下,先有个全局的认识:


1.  下载云端的 markdown文件


2.  通过 markejs 把 markdown 字符串 解析成 html 字符串


3.  React 解析 html 字符串,通过 dangerouslySetInnerHTML 渲染成 DOM 结构


4.  通过 html2pdf 首先把 DOM 结构转为 Canvas,然后转为 Image,最终输出到 Pdf 文件提供下载


markdown 字符串到 html 字符串,直接选型了 github.com/markedjs/ma… ,它是一个比较高效的 markdown 解析库,使用简单,也有一些 hooks 方便我们获取解析过程和解析结果中的一些信息,比如我们需要生成一二三级标题的导航,可配置的东西也很多,感兴趣的可以看看


不过从 html 生成 pdf,倒腾了几个方案。最后确定使用了 html -> canvas -> pdf 的方案,主要优势是还原度高,简单。但是也存在图片失真,分页导致文字分割,文字不可复制等问题,不过这些缺点是在可接受范围的。


与之相反的方案是使用 文字版本的 PDF文件下载。思路是把 dom 结构生成一个个的 JSON 信息,通过 PdfMake GitHub - bpampuch/pdfmake: Client/server side PDF printing in pure JavaScript 框架,把文字输出到 PDF 文件上。这里有个最大的难点是 文字的处理,后面再展开谈谈


简单总结下:


方案框架选择优点缺点其他问题
Canvas图片[html2pdf.jsClient-side HTML-to-PDF rendering using pure JS.](ekoopmans.github.io/html2pdf.js…)处理简单,高还原文字无法复制,分页上元素被切割,容易失真(已有解决方案)内容过多时生成空白PDF文件(已有解决方案)
文字GitHub - bpampuch/pdfmake: Client/server side PDF printing in pure JavaScript文字可复制,内容清晰,不存在分页上元素被切割处理繁杂,中文和 emoji 暂时没有太好的处理方案(需要导入字体库,导致生成时间过长,试了下腾讯文档导出 pdf 里面的emoji表情都被过滤了)-


2.  Markdown -> HTML


这里借助 marked 可以很容易实现,直接上代码:


import { marked } from 'marked';
import DOMPurify from 'dompurify';

const [renderText, setRenderText] = useState('');

...
// 核心代码
setRenderText(DOMPurify.sanitize(marked.parse(text), { ADD_ATTR: ['target'] }));
const tokens = marked.lexer(text);
const headings = tokens
.filter((token) => {
return token.type === 'heading' && token.depth < 4;
})
.map((heading) => {
return {
level: heading.depth,
text: heading.text,
slug: heading.text
.toLowerCase()
.trim()
// remove html tags
.replace(/<[!/a-z].*?>/gi, '')
// remove unwanted chars
.replace(/[\u2000-\u206F\u2E00-\u2E7F\'!"#$%&()*+,./:;<=>?@[]^`{|}~]/g, '')
.replace(/\s/g, '-'),
};
});
...

...
// 下面是导航
{headings && Array.isArray(headings) && headings.length > 0 && (
<ul className="markdown-preview-nav">
{headings.map((item) => (
<li
className={`markdown-preview-nav-depth-${item.level}`}
key={item.slug}
onClick={debounce(debounceTime, () =>
{
handleNavigatorScroll(item.slug, contentRef, 10);
})}
>
<a className={`${navigator === item.slug ? 'active' : ''}`}>{item.text}</a>
</li>
))}
</ul>

)}

export function handleNavigatorScroll(curNavigator, contentRef, offset = 100) {
const anchorElement = document.getElementById(curNavigator);
if (!(anchorElement instanceof HTMLElement)) {
return;
}
if (!(contentRef.current instanceof HTMLElement)) {
return;
}
contentRef.current.scrollTo({
top: anchorElement?.offsetTop - offset,
behavior: 'smooth', // 平滑滚动
});
}
...

2.1 html 标签属性保留


GitHub - cure53/DOMPurify: DOMPurify - a DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG. DOMPurify works with a secure default, but offers a lot of configurability and hooks. Demo: DOMPurify 主要是防止 XSS 攻击,这个没有太多需要解释说明的


主要需要提醒的是,DOMPurify会把大部分标签的上的属性给过滤掉,比如 target 属性。所以我们在第二个参数上 加了 ADD_ATTR 配置,保留这个属性,因为需要传递 _blank ,允许用户通过新窗口打开链接


2.2 目录生成


另外一个需求是需要获取到 一二三级 标题,生成目录导航。我们可以通过 marked.lexer 获取到解析后的元素数组,把类型为 heading 并且层级小于 4 的元素挑选出来,组成我们的标题导航。


这里还有页面跳转的功能,我们需要跳转到和当前点击导航相匹配 id 的元素,主要通过 slug 来判断。一开始在网上找了下面一段代码:


heading.text.toLowerCase().replace(/[^(\w|\u4e00-\u9fa5)]+/g, '-')

发现不能 100% 匹配上所有的情况,导致失效。后面扒了下源码(如上面代码所示的几个 replace 函数)替换上去,功能正常。


不过更好的方式是使用 marked 自带的办法,如下所示(印象中有人提过这个 issues,所以刚去查了下,简单验证了下没有问题):


// add slug with occurrences by UziTech · Pull Request #20 · Flet/github-slugger · GitHub

const slugger = new marked.Slugger();

console.log(
slugger.slug(heading.text),

heading.text
.toLowerCase()
.trim()
// remove html tags
.replace(/<[!/a-z].*?>/gi, '')
// remove unwanted chars
.replace(/[\u2000-\u206F\u2E00-\u2E7F\'!"#$%&()*+,./:;<=>?@[]^`{|}~]/g, '')
.replace(/\s/g, '-'),
);

2.3 样式


产品对于样式这块并没有太多的要求,让参考 dumi - 为组件研发而生的静态站点框架 的样式。所以一开始扒拉了下它的样式文件过来用。但是发现需熬过并不是太尽如人意 。最终找了 Issues · sindresorhus/github-markdown-css · GitHub 来使用,GitHub 上使用的 markdown 样式库


import 'github-markdown-css/github-markdown-light.css';

不过这里也要提个小问题,一开始我是直接引用 github-markdown-css,测试反馈样式上有点问题,怎么是黑色的主题,看了下源码,发现使用了一个有意思的媒体查询:prefers-color-scheme - CSS:层叠样式表 | MDN ,学习了~。遂改成只用 light 主题的样式


2.4 <details> & <summary>


产品侧反馈存在大量用户的评论,想要能折叠起来,点击的时候才进行展示。一开始想着 React 来控制这种行为,但是后面想起 HTML5 本身也有类似原生的标签可以使用:details 标签(<details>: The Details disclosure element - HTML: HyperText Markup Language | MDN),于是就拿来用了。


它在 markdown 文件中如何使用,可以参考下下面的讨论:


gist.github.com/scmx/eca72d…


这里需要注意的一个问题是,

标签后面必须要加一个空行,否则会导致后续生成的 PDF 文件展示出现问题(具体是在PDF文件中,折叠的内容也会展示出来,但是不占据空间,导致内容重叠),原因不详(待研究),如下所示:


<details>
<summary>**重要评论** </summary>
// 这个空行是必须的!!!这个空行是必须的!!!这个空行是必须的!!!
>> 内容 1
</details>

还有另外一个问题是如下图所示:



我们使用 details 和 summary 标签的时候,在页面上是可以看到箭头的,但是生成 PDF 的话,箭头消失了,原因不详(待研究)。这里简单的处理方式是打了个补丁:


.markdown-body details ::marker,
.markdown-body details ::-webkit-details-marker {
font-size: 0;
}

.markdown-body details summary:before {
font-size: 14px;
content: '▶';
display: inline-block;
margin-right: 5px;
color: #24292f;
}

.markdown-body details[open] summary:before {
content: '▼';
}

2.5 <table>


生成出来的PDF文件中,会发现过宽的 table 元素会展示不全,可以添加下面的样式来解决:


.markdown-body table {
word-break: break-all;
}

3.  当前方案:HTML -> Canvas -> PDF


这块主要使用的是 GitHub - eKoopmans/html2pdf.js: Client-side HTML-to-PDF rendering using pure JS.,而它主要依赖的是 GitHub - niklasvh/html2canvas: Screenshots with JavaScriptGitHub - parallax/jsPDF: Client-side JavaScript PDF generation for everyone. 感兴趣的都可以去看看。整体流程引用它里面的内容:


.from() -> .toContainer() -> .toCanvas() -> .toImg() -> .toPdf() -> .save()

这里主要讲讲使用过程,以及遇到的问题。先上整体主要的代码:


import canvasSize from 'canvas-size';
import html2pdf from 'html2pdf.js';
import axios from 'axios';
import * as FileSaver from 'file-saver';

const markdownRef = useRef<HTMLElement>(null);
let isValidCanvas = true;

const worker = html2pdf()
.set({
pagebreak: { mode: ['avoid-all', 'css'] },
html2canvas: {
scale: 2,
useCORS: true,
onrendered: function (canvas) {
isValidCanvas = canvasSize.test({
width: canvas.width,
height: canvas.height,
});

if (isValidCanvas) {
worker
.toImg()
.toPdf()
.save(fileName + '.pdf')
.then(() => {
setRendering(false);
});
} else {
axios
.get(mdFileUrl, { responseType: 'blob' })
.then((res) => {
if (res?.data) {
FileSaver.saveAs(res.data, fileName + '.md');
}
setRendering(false);
})
.finally(() => setRendering(false));
}
},
},
margin: [0, 10, 0, 10],
image: {
type: 'jpeg',
quality: 1,
},
})
.from(markdownRef.current)
.toCanvas();

3.1 Canvas 过大,PDF输出空白


这个是使用 Canvas 方案的时候遇到的最大问题,差点弃坑。中间也修改了几个版本,最终代码如上所示。接下来会简单说说过程。


PDF 输出空白文件这个其实在官方 issues 上也有不少的提问的,比如这一条:github.com/eKoopmans/h…,整整50多条评论,遇到这个问题的人还是不少


主要原因还是浏览器的支持问题,浏览器会限制生成的 Canvas 元素尺寸,超过的话生成一个空白的 Canvas 元素:


The HTML canvas element is widely supported by modern and legacy browsers, but each browser and platform combination imposes unique size limitations that will render a canvas unusable when exceeded

引用:GitHub - jhildenbiddle/canvas-size: Determine the maximum size of an HTML canvas element and test support for custom canvas dimensions

3.1.1 文档拆分


针对这个问题,给出的解决方案大部分是把整个文档分成几个部分,生成小块的 Canvas ,然后一点点的渲染到 PDF文件 上去。


但是这里会有两个问题:


1.  文档拆分的标准是什么?这个我很难定下来,因为给到的 markdown 文件内容没有固定可拆分的标准


2.  使用 addPage 会生成新的一页,导致出现大量的空白位置(没找到可以在当前页面后继续添加内容的方法)


于是放弃了这个方案


3.1.2 兜底方案 - 判断文档是否过大


最终和产品协商的方案是,如果文档太大,我们提供原始的 markdown 文件供用户下载。那接下来的问题变成了,文档什么时候会过大?


官方针对这个问题贴了个链接:stackoverflow.com/questions/6…,不过已经是2014年的答案了,这份数据并不可靠。


不同浏览器的尺寸限制并不一样,项目中使用的是:GitHub - jhildenbiddle/canvas-size: Determine the maximum size of an HTML canvas element and test support for custom canvas dimensions,基本原理是生成一个 Canvas 元素然后收集相关的信息来判断尺寸的限制,引用它的一段话:


Unfortunately, browsers do not provide a way to determine what their limitations are, nor do they provide any kind of feedback after an unusable canvas has been created. This makes working with large canvas elements a challenge, especially for applications that support a variety of browsers and platforms.

This micro-library provides the maximum area, height, and width of an HTML canvas element supported by the browser as well as the ability to test custom canvas dimensions. By collecting this information before a new canvas element is created, applications are able to reliably set canvas dimensions within the size limitations of each browser/platform.

引用:GitHub - jhildenbiddle/canvas-size: Determine the maximum size of an HTML canvas element and test support for custom canvas dimensions

3.1.2.1 最初方案


于是乎,一开始采用了下面的判断方案:


const isValidCanvas = canvasSize.test({
width: markdownRef.current?.clientWidth,
height: markdownRef.current?.clientHeight,
});

直接拿了 DOM 结构元素的宽高来进行判断。一开始误认为生成的 PDF 文件会和页面上的元素宽高是一致的,所以采用了这种判断方式。但是实际上不是的,不传递 width / height 参数的时候,看到生成的 canvas 宽度基本都是 719 像素。问了下 Warp: The terminal for the 21st century,它的答复是这样的:


html2pdf是一个将HTML转换为PDF的工具,它使用了wkhtmltopdf引擎来进行转换。在转换过程中,wkhtmltopdf会将HTML渲染成一个canvas,然后将canvas转换为PDF。canvas的宽度默认为719像素,这是因为wkhtmltopdf使用的默认DPI为96,而71996dpi下A4纸的像素宽度。

暂时没有细细考究。所以我上面的判断方式是肯定存在问题的,后续也发现了很多通过这种方式生成的文档,存在空白的问题。需要调整优化


清晰度问题

另外针对图片失真的问题,解决方案是通过设置 html2canvas 参数来进行优化,比如设置 scale = 2,扩大 Canvas 元素的宽高来输出更加清晰的图片到 PDF 文件中,那我们使用 HTML 元素的宽高误差就更大了


3.1.2.2 最终版本


最终版本就是一开始贴出来的完整代码。不过也是几经修改才确定下来的,这其中遇到了以下一些问题:


获取宽高的方法

html2pdf 提供的方法基本都是基于 Promise 的工作流,产物一个个往下传递。但是翻阅了很久也没有找到一个方法可以去判断生成的 Canvas 的宽高,来决定是 resolve 继续执行 PDF 的生成,还是 reject 掉去执行兜底方案。


于是开始看 html2canvas 的文档,发现文档非常简单!也没有找到有用的信息。但是既然是开源项目,于是重施旧计 - 看源码。主要的搜索方向是找相关的方法,通过 on 关键字找到了 onrendered 函数,可以获取到生成的 Canvas 的信息。


不过要注意的是,这个方法已经被标记为废弃,后续版本也许不能使用了。其实最好的方式可以是单独引用 html2canvas 和 pdfjs,生成的 canvas 元素传递给 pdfjs,而不是使用 html2pdf 这个集成库,感兴趣的可以研究下。


于是乎我们的代码就从原来的 toPdf() 一步到位,变成了 toCanvas,然后在 onrendered 函数里面判断是否继续执行后续的 toImg 和 toPdf 方法了


3.2 下载的文件后缀丢失


有产品反馈下载完的文件没有 pdf 的后缀,我尝试了几个都是有的,于是要了相关的文件名信息,发现如果文件名中存在一些特殊的字符的时候就会产生这种情况,最终主动补全了后缀(开发过程中,一开始是手动写了,发现不写也没问题,然后去掉了,尴尬):


.save(fileName + '.pdf')

另外一种方法是可以把文件名中的特殊字符给过滤掉,但是为了保留原来的文件名,还是放弃了这种方案


3.3 跨域问题


需要加载图片的话,需要添加 Options | html2canvas useCORS 配置,并且源图片网站开放允许跨域的访问域名


3.4 元素切割问题


暂时没有发现太好的解决方案,只能从两方面去缓和,但还是存在,具体的配置说明可以看官网:html2pdf.js | Client-side HTML-to-PDF rendering using pure JS.


pagebreak: { mode: ['avoid-all', 'css'] },

margin: [0, 10, 0, 10], // top, left, bottom, right

因为切割的主要是分页的部分,我们把 top 和 bottom 的间隙都设置为0,尽可能缓和这种切割感。


而配合 pagebreak 属性中的 css mode,我们可以添加下面这段样式:


  * {
break-inside: avoid;
break-after: always;
break-before: always;
}

但是最终还是存在分页上被切割的元素,原因不详


4.  备选方案:HTML -> Text -> PDF


我认为备选方案是更加好的方案,但是却存在一些还没解决的问题,所以不建议在现网中使用。下面主要来讨论下这个方案开发过程中遇到的一些问题。


这个方案的核心是使用:GitHub - bpampuch/pdfmake: Client/server side PDF printing in pure JavaScript


pdfmake 是一个用于生成PDF文档的JavaScript库,服务端和客服端都可以使用。它的目标是简化PDF文档生成的复杂性,提供简单易用的API和清晰易读的文档定义。它支持多语言、自定义字体、图表、表格、图像、列表、页眉页脚等常见的PDF文档功能


我们可以在官方的 pdfmake.org/playground.… 上体验。简单贴一段官网中的案例:


// playground requires you to assign document definition to a variable called dd
var dd = {
content: [
{
text: 'This paragraph uses header style and extends the alignment property',
style: 'header',
alignment: 'center'
},
{
text: [
'This paragraph uses header style and overrides bold value setting it back to false.\n',
'Header style in this example sets alignment to justify, so this paragraph should be rendered \n',
'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Malit profecta versatur nomine ocurreret multavit, officiis viveremus aeternum superstitio suspicor alia nostram, quando nostros congressus susceperant concederetur leguntur iam, vigiliae democritea tantopere causae, atilii plerumque ipsas potitur pertineant multis rem quaeri pro, legendum didicisse credere ex maluisset per videtis. Cur discordans praetereat aliae ruinae dirigentur orestem eodem, praetermittenda divinum. Collegisti, deteriora malint loquuntur officii cotidie finitas referri doleamus ambigua acute. Adhaesiones ratione beate arbitraretur detractis perdiscere, constituant hostis polyaeno. Diu concederetur.'
],
style: 'header',
bold: false
}
],
styles: {
header: {
fontSize: 18,
bold: true,
alignment: 'justify'
}
}
}

最主要的话是做好 文档定义 这块的工作,传给 pdfmake ,生成文字版和清晰的PDF文件。


那我们如何通过 maked 生成的 html,翻译成 pdfmake 所需要的 JSON 格式的文档定义对象呢?观察上面的文档定义,content 主要是内容的定义,里面包含了内容主体,样式定义。其中的 style 指定了一个字符串,我们可以在 styles 中定义这个字符串对应的样式。


所以这里需要做的就是我们要把 html 中的每个标签,都用 pdfmake 的文档定义重新定义一份。可想而知,工作量会很大,幸亏这块已经有成熟的框架支持了:GitHub - Aymkdn/html-to-pdfmake: This module permits to convert HTML to the PDFMake format,参考 HTML to PDFmake online convertor 官网的案例,可以更快理解。


难点1 - 样式定义


借助 html-to-pdfmake ,标签的定义可以快速完成,但是我们还需要完成样式的定义,这又是一个不小的工作量,等于要我们上面提到的 GitHub - sindresorhus/github-markdown-css: The minimal amount of CSS to replicate the GitHub Markdown style 翻译一遍,否则和页面上展示的内容样式肯定不太一致,导致用户的疑惑。这块还没有精力去处理


难点2 - 字体支持


使用 pdfmake 默认提供的几个字体,并不支持中文和 emoji 表情,会看到一堆乱码,我们需要寻找一个能同时支持中英文和 emoji 表情的字体库。这里先说下结论,尚未找到这样的字体库,因为本身对字体库这块理解也不深。


这里还要考虑的一个问题是,即使找到了这样的字体库,它还需要支持不同操作系统的字体展示,我们知道平时打开PDF文件的时候,一些PDF阅读器如果是不支持的字体的话会做回退字体展示,但是有些PDF阅读器会直接全文档显示空白,这也是个头疼的事情。


后面找到了一个在 windows 、mac、安卓和苹果手机上都能正常展示的字体:GitHub - adobe-fonts/source-han-sans: Source Han Sans | 思源黑体 | 思源黑體 | 思源黑體 香港 | 源ノ角ゴシック | 본고딕,当然也少不了 chatgpt 的意见了:



而对于 emoji 文字的支持,推荐的是:fonts.google.com/noto,但是尝试了下没有成功使用,后续再研究了。这里考虑的方案是把 emoji 表情文字给过滤掉 😄。(其实发现腾讯文档导出PDF文件的时候,也是没有 emoji 表情的,估计确实不好处理)


先按照使用 source-han-sans 字体的方案来,参考:pdfmake 官方文档,我们就可以导入自己的字体库来使用了。官方推荐的是用在线的字体链接来导入,我们把文件上传到 cdn,然后下载下来使用就好了。这边建议预加载文字库,加快生成 PDF 的速度


欢迎收看~


作者:codytang
来源:juejin.cn/post/7234315967564103737
收起阅读 »

一行代码就能完成的事情,为什么要写两行

web
今天休息休息,复习一下使用的简洁运算方式以及常用的单行代码 三元运算符 用三元运算符代替简单的if else if (age < 18) {   me = '小姐姐'; } else {   me = '老阿姨'; } 改用三元运算符,一行就能搞定 m...
继续阅读 »

今天休息休息,复习一下使用的简洁运算方式以及常用的单行代码


三元运算符


用三元运算符代替简单的if else


if (age < 18) {
  me = '小姐姐';
} else {
  me = '老阿姨';
}

改用三元运算符,一行就能搞定


me = age < 18 ? '小姐姐' : '老阿姨';

复杂的判断三元运算符就有点不简单易懂了


const you = "董员外"
const your = "菜鸡本鸡"
const me = you ?"点再看":your?"点赞":"分享"

判断


当需要判断的情况不止一个时,第一个想法就是使用 || 或运算符


if(
    type == 1 ||
    type == 2 ||
    type == 3 ||
    type == 4 ||
){
   //...
}

ES6中的includes一行就能搞定


if( [1,2,3,4,5].includes(type) ){
   //...
}

取值


在写代码的时候,经常会用到取值的操作


const obj = {
    a:1,
    b:2,
    c:3,
}
//老的取值方式
const a = obj.a;
const b = obj.b;
const c = obj.c;

老的取值方式,直接用对象名加属性名去取值。如果使用ES6的解构赋值一行就能搞定


const {a,b,c} = obj;

获取对象属性值


在编程的过程中经常会遇到获取一个值并赋给另一个变量的情况,在获取这个值时需要先判断一下这个对象是否存在,才能进行赋值


if(obj && obj.name){
  const name = obj.name
}

ES6提供了可选连操作符?.,可以简化操作


const name = obj?.name;

反转字符串


将一个字符串进行翻转操作,返回翻转后的字符串


const reverse = str => str.split('').reverse().join('');

reverse('hello world');   // 'dlrow olleh'

生成随机字符串


生成一个随机的字符串,包含字母和数字


const randomString = () => Math.random().toString(36).slice(2);
//函数调用
randomString();

数组去重


用于移除数组中的重复项


const unique = (arr) => [...new Set(arr)];

console.log(unique([1, 2, 2, 2, 3, 4, 4, 5, 6, 6]));

数组对象去重


去除重复的对象,对象的key值和value值都分别相等,才叫相同对象


const uniqueObj = (arr, fn) =>arr.reduce((acc, v) => {if (!acc.some(x => fn(v, x))) acc.push(v);return acc;}, []);
 
uniqueObj([{id1, name'大师兄'}, {id2, name'小师妹'}, {id1, name'大师兄'}], (a, b) => a.id == b.id)
// [{id: 1, name: '大师兄'}, {id: 2, name: '小师妹'}]

合并数据


当我们需要合并数据,并且去除重复值时,你是不是要用for循环? ES6的扩展运算符一行就能搞定!!!


const a = [1,2,3];
const b = [1,5,6];
const c = [...new Set([...a,...b])];//[1,2,3,5,6]

判断数组是否为空


判断一个数组是否为空数组,它将返回一个布尔值


const notEmpty = arr => Array.isArray(arr) && arr.length > 0;

notEmpty([1, 2, 3]);  // true

交换两个变量


//旧写法
let a=1;
let b=2;
let temp;
temp=a
a=b
b=temp

//新写法
[a, b] = [b, a];

判断奇还是偶


const isEven = num => num % 2 === 0;

isEven(996)

获取两个数之间的随机整数


const random = (minmax) => Math.floor(Math.random() * (max - min + 1) + min);

random(150);

检查日期是否为工作日


传入日期,判断是否是工作日


const isWeekday = (date) => date.getDay() % 6 !== 0;
console.log(isWeekday(new Date(20211111)));
// false 
console.log(isWeekday(new Date(20211113)));
// true

高级


滚动到页面顶部


不用引入element-ui等框架,一行代码就能实现滚动到顶部


const goToTop = () => window.scrollTo(00);
goToTop();

浏览器是否支持触摸事件


通过判断浏览器是否有ontouchstart事件来判断是否支持触摸


const touchSupported = () => {
  ('ontouchstart' in window || window.DocumentTouch && document instanceof window.DocumentTouch);
}
console.log(touchSupported());

当前设备是否为苹果设备


前端经常要兼容andriod和ios


const isAppleDevice = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
console.log(isAppleDevice);
// Result: will return true if user is on an Apple device

复制内容到剪切板


使用 navigator.clipboard.writeText 来实现将文本复制到剪贴板


const copyToClipboard = (text) => navigator.clipboard.writeText(text);

copyToClipboard("双十一来了~");

检测是否是黑暗模式


用于检测当前的环境是否是黑暗模式,返回一个布尔值


const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches

console.log(isDarkMode)

网站变成黑白


有时候网站在某种特定的情况下,需要使整个网站变成黑白的颜色


filter:grayscale(100%)

只需要将这一行代码filter:grayscale(100%)放到body上,一下就能致黑



一行代码就能完成的事情,凭什么写两行!!!


作者:董员外
来源:juejin.cn/post/7150275723784585246
收起阅读 »

写出干净的 JavaScript 5 个小技巧

web
降低阅读负担,启发创作心智,轻松学习 JavaScript 技巧,日拱一卒,jym,冲~ 1. 将数字定义为常量 我们常常会用到数字,比如以下代码: const isOldEnough = (person) => { return person.g...
继续阅读 »



降低阅读负担,启发创作心智,轻松学习 JavaScript 技巧,日拱一卒,jym,冲~


take-it-easy-relax.gif


1. 将数字定义为常量


我们常常会用到数字,比如以下代码:


const isOldEnough = (person) => {
return person.getAge() >= 100;
}

谁知道这个 100 具体指的是什么?我们通常需要结合函数上下文再推测、判断这个 100 它可能是具体代表一个什么值。


如果这样的数字有多个的话,一定会很容易造成更大的困惑。


写出干净的 JavaScript:将数字定义为常量


即可清晰的解决这个问题:


const AGE_REQUIREMENT = 100;
const isOldEnough = (person) => {
return person.getAge() >= AGE_REQUIREMENT;
}

现在,我们通过声明常量的名字,即可立马读懂 100 是“年龄要求”的意思。修改时也能迅速定位、一处修改、多处生效。


2. 避免将布尔值作为函数参数


将布尔值作为参数传入函数中是一种常见的容易造成代码混乱的写法。


const validateCreature = (creature, isHuman) => {
if (isHuman) {
// ...
} else {
// ...
}
}

布尔值作为参数传入函数不能表示出明确的意义,只能告诉读者,这个函数将会有判断发生,产生两种或多种情况。


然而,我们提倡函数的单一职责原则,所以:


写出干净的 JavaScript:避免将布尔值作为函数参数


const validatePerson = (person) => {
// ...
}
const validateCreature = (creature) => {
// ...
}

3. 将多个条件封装


我们经常会写出这样的代码:


if (
person.getAge() > 30 &&
person.getName() === "simon" &&
person.getOrigin() === "sweden"
) {
// ...
}

不是不行,只是隔久了会一下子看不懂这些判断到底是要干嘛的,所以建议把这些条件用变量或函数进行封装。


写出干净的 JavaScript:将多个条件封装


const isSimon =
person.getAge() > 30 &&
person.getName() === "simon" &&
person.getOrigin() === "sweden";
if (isSimon) {
// ...
}

或者


const isSimon = (person) => {
return (
person.getAge() > 30 &&
person.getName() === "simon" &&
person.getOrigin() === "sweden"
);
};
if (isSimon(person)) {
// ...
}

噢,原来这些条件是为了判断这个人是不是 Simon ~


这样的代码是声明式风格的代码,更易读。


4. 避免否定的判断条件


条件判断中,使用否定判断,会额外造成一种思考负担。


比如下面的代码,条件 !isCreatureNotHuman(creature) 双重否定,读起来就会觉得有点费劲。


const isCreatureNotHuman = (creature) => {
// ...
}

if (!isCreatureNotHuman(creature)) {
// ...
}

写出干净的 JavaScript:避免否定的判断条件


改写成以下写法则读起来更轻松,虽然这只是一个很小的技巧,但是在大量的代码逻辑中,多处去遵循这个原则,肯定会很有帮助。


很多时候读代码就是读着读着,看到一个“很烂”的写法,就忍不了了,细节会叠加,千里之堤溃于蚁穴。


const isCreatureHuman = (creature) => {
// ...
}
if (isCreatureHuman(creature)) {
// ...
}

5. 避免大量 if...else...


这一点,本瓜一直就有强调:


🌰比如以下代码:


if(x===a){
res=A
}else if(x===b){
res=B
}else if(x===c){
res=C
}else if(x===d){
//...
}

改写成 map 的写法:


let mapRes={
a:A,
b:B,
c:C,
//...
}
res=mapRes[x]

🌰再比如以下代码:


const isMammal = (creature) => {
if (creature === "human") {
return true;
} else if (creature === "dog") {
return true;
} else if (creature === "cat") {
return true;
}
// ...
return false;
}

改写成数组:


const isMammal = (creature) => {
const mammals = ["human", "dog", "cat", /* ... */];
return mammals.includes(creature);
}

写出干净的 JavaScript:避免大量 if...else...


所以,当代码中出现大量 if...else... 时,多想一步,是否能稍加改造让代码看起来更加“干净”。




小结:上述技巧可能在示例中看起来不值一提,但是在实际的项目中,当业务逻辑复杂起来、当代码量变得很大的时候,这些小技巧一定能给出正面的作用、帮助,甚至超乎想象。



OK,以上便是本篇分享。点赞关注评论,为好文助力👍


我是掘金安东尼 🤠 100 万人气前端技术博主 💥 INFP 写作人格坚持 1000 日更文 ✍ 关注我,安东尼陪你一起度过漫长编程岁月 🌏



作者:掘金安东尼
来源:juejin.cn/post/7131994944067076127
收起阅读 »

前端如何实现同时发送多个相同请求时只发送一个?

web
原生实现 为了控制并发请求,可以使用以下两种常见的方式: 防抖 防抖是指在一段时间内多次触发同一事件,只执行最后一次触发的操作。在前端中,可以利用定时器来实现防抖的效果。具体实现方法如下: function debounce(func, delay) { ...
继续阅读 »

原生实现


为了控制并发请求,可以使用以下两种常见的方式:


防抖


防抖是指在一段时间内多次触发同一事件,只执行最后一次触发的操作。在前端中,可以利用定时器来实现防抖的效果。具体实现方法如下:


function debounce(func, delay) {
let timer;
return function() {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, arguments);
}, delay);
}
}

在发送请求的地方使用防抖函数,如下所示:


const sendRequest = debounce(() => {
// 发送请求的代码
}, 500);

上述代码中,sendRequest 是一个防抖函数,它将在 500ms 后执行。如果在 500ms 内再次触发 sendRequest 函数,计时器会被重新启动,并且等待 500ms,以确保只有最后一次触发才会发送请求。


节流


节流是指在一段时间内只执行一次操作。与防抖不同的是,节流是指在一定的时间间隔内只执行一次操作。在前端中,可以使用定时器来实现节流的效果。具体实现方法如下:


function throttle(func, delay) {
let timer;
return function() {
if (!timer) {
timer = setTimeout(() => {
func.apply(this, arguments);
timer = null;
}, delay);
}
}
}

在发送请求的地方使用节流函数,如下所示:


const sendRequest = throttle(() => {
// 发送请求的代码
}, 500);

上述代码中,sendRequest 是一个节流函数,它将在 500ms 后执行。如果在 500ms 内再次触发 sendRequest 函数,由于计时器还没有结束,函数不会执行任何操作。只有当计时器结束后才会再次触发请求。


Vue + Axios


在 Vue 中使用 Axios 发送请求,可以使用 Axios 的 CancelToken 来取消重复的请求,从而实现并发多个相同的请求只发送一个的效果。


具体实现方法如下:



  1. 创建 CancelToken


首先,需要创建一个 CancelToken,用于取消请求。在 Vue 中,可以在组件的 data 中定义一个 cancelToken 对象:


data() {
return {
cancelToken: axios.CancelToken.source().token
}
}


  1. 发送请求时使用 CancelToken


在发送请求时,需要将定义的 cancelToken 对象作为配置的 cancelToken 属性传递给 Axios:


axios.get(url, {
cancelToken: this.cancelToken
}).then(response => {
// 处理响应结果
}).catch(error => {
// 处理请求错误
});


  1. 取消重复的请求


在发送新的请求之前,需要取消之前正在进行的相同请求。可以通过判断上一次请求的 CancelToken 和当前请求的 CancelToken 是否相同来实现:


if (this.lastRequestCancelToken) {
this.lastRequestCancelToken.cancel('取消重复的请求');
}
this.lastRequestCancelToken = this.cancelToken;

上述代码中,lastRequestCancelToken 是用于保存上一次请求的 CancelToken 对象的变量。在发送新的请求之前,需要先取消之前正在进行的相同请求,并将当前的 CancelToken 对象赋值给 lastRequestCancelToken 变量。


完整的实现代码如下:


data() {
return {
cancelToken: axios.CancelToken.source().token,
lastRequestCancelToken: null
}
},
methods: {
fetchData() {
if (this.lastRequestCancelToken) {
this.lastRequestCancelToken.cancel('取消重复的请求');
}
this.lastRequestCancelToken = this.cancelToken;
axios.get(url, {
cancelToken: this.cancelToken
}).then(response => {
// 处理响应结果
}).catch(error => {
// 处理请求错误
});
}
}

上述代码中,fetchData 是发送请求的方法,在方法中使用了 CancelToken 来控制并发多个相同的请求只发送一个。


使用 React 实现


如果你正在使用 React,你可以使用 axios 库来实现控制只发送一个请求的功能。具体实现如下:


import React, { useState } from 'react';
import axios from 'axios';

function App() {
const [requestCount, setRequestCount] = useState(0);

function fetchData() {
if (requestCount === 0) {
setRequestCount(1);
axios.get('http://example.com/api/data')
.then(response => {
console.log(response.data);
setRequestCount(0);
})
.catch(error => {
console.error(error);
setRequestCount(0);
});
}
}

return (
<div>
<button onClick={fetchData}>Fetch Data</button>
</div>

);
}

export default App;

这段代码使用了 React 的 useState 钩子函数,定义了一个状态变量 requestCount,用于记录当前正在发送的请求数量。在发送请求之前,先检查是否已经有一个请求正在处理。如果没有,则将 requestCount 设为 1,表示有一个请求正在处理。在请求成功或失败后,将 requestCount 设为 0,表示请求已经处理完毕。


更多题目


juejin.cn/column

/7201…

收起阅读 »

偏爱console.log的你,肯定会觉得这个插件泰裤辣!

web
前言 毋庸置疑,要说前端调试代码用的最多的,肯定是console.log,虽然我现在 debugger 用的比较多,但对于生产环境、小程序真机调试,还是需要用到 log 来查看变量值,比如我下午遇到个场景:选择完客户后返回页面,根据条件判断是否弹窗: if (...
继续阅读 »

前言


毋庸置疑,要说前端调试代码用的最多的,肯定是console.log,虽然我现在 debugger 用的比较多,但对于生产环境、小程序真机调试,还是需要用到 log 来查看变量值,比如我下午遇到个场景:选择完客户后返回页面,根据条件判断是否弹窗:


if (global.isXXX || !this.customerId || !this.skuList.length) return

// 到了这里才会执行弹窗的逻辑

这个时候只能真机调试,看控制台打印的值是怎样的,但对于上面的条件,如果你这样 log 的话,那控制台只会显示:


console.log(global.isXXX, !this.customerId, !this.skuList.length)
false false false

且如果参数比较多,你可能就没法立即将 log 出的值对应到相应的变量,还得回去代码里面仔细比对。


还有一个,我之前遇到过一个项目里一堆 log,同事为了方便看到 log 是在哪一行,就在 log 的地方加上代码所在行数,但因为 log 那一刻已经硬编码了,而代码经常会添加或者删除,这个时候行数就不对了:




比如你上面添加了一行,这里的所有行数就都不对了



所以,我希望 console.log 的时候:



  1. 控制台主动打印源码所在行数

  2. 变量名要显示出来,比如上面例子的 log 应该是 global.isXXX = false !this.customerId = false !this.skuList.length = false

  3. 可以的话,每个参数都有分隔符,不然多个参数看起来就有点不好分辨


即源码不做任何修改:



而控制台显示所在行,且有变量名的时候添加变量名前缀,然后你可以指定分隔符,如换行符\n



因为之前有过 babel 插件的经验,所以想着这次继续通过写一个 babel plugin 实现以上功能,所以也就有了babel-plugin-enhance-log,那究竟怎么用?很简单,下面 👇🏻 我给大家说说。


babel-plugin-enhance-log


老规矩,先安装插件:


pnpm add babel-plugin-enhance-log -D
# or
yarn add babel-plugin-enhance-log -D
# or
npm i babel-plugin-enhance-log -D

然后在你的 babel.config.js 里面添加插件:


module.exports = (api) => {
return {
plugins: [
'enhance-log',
...
],
}
}

看到了没,就是这么简单,之后再重新启动,去你的控制台看看,小火箭咻咻咻为你刷起~



options


上面了解了基本用法后,这里再给大家说下几个参数,可以看下注释,应该说是比较清楚的:


interface Options {
/**
* 打印的前缀提示,这样方便快速找到log 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀
* @example
* console.log('line of 1 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀', ...)
*/

preTip?: string
/** 每个参数分隔符,默认空字符串,你也可以使用换行符\n,分号;逗号,甚至猪猪🐖都行~ */
splitBy?: boolean
/**
* 是否需要endLine
* @example
* console.log('line of 1 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀', ..., 'line of 10 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀')
* */

endLine?: boolean
}

然后在插件第二个参数配置即可(这里偷偷跟大家说下,通过/** @type {import('babel-plugin-enhance-log').Options} */可以给配置添加类型提示哦):



return {
plugins: [
['enhance-log', enhanceLogOption],
],
...
}

比如说,你不喜欢小 🚀,你喜欢猪猪 🐖,那可以配置 preTip 为 🐖🐖🐖🐖🐖🐖🐖🐖🐖🐖:



比如说,在参数较多的情况下,你希望 log 每个参数都换行,那可以配置 splitBy 为 \n



或者分隔符是;:



当然,你也可以随意指定,比如用个狗头🐶来分隔:



又比如说,有个 log 跨了多行,你希望 log 开始和结束的行数,中间是 log 实体,那可以将 endLine 设置为 true:





我们可以看到开始的行数是13,结束的行数是44,跟源码一致



实现思路


上面通过多个例子跟大家介绍了各种玩法,不过,我相信还是有些小伙伴想知道怎么实现的,那我这里就大致说下实现思路:


老规格,还是通过babel-ast-explorer来查看


1.判断到 console.log 的 ast,即 path 是 CallExpression 的,且 callee 是 console.log,那么进入下一步



2.拿到 console.log 的 arguments,也就是 log 的参数



3.遍历 path.node.arguments 每个参数



  • 字面量的,则无须添加变量名

  • 变量的,添加变量名前缀,如 a =

  • 如果需要分隔符,则根据传入的分隔符插入到原始参数的后面


4.拿到 console.log 的开始行数,创建一个包含行数的 StringLiteral,同时加上 preTip,比如上面的 🚀🚀🚀🚀🚀🚀🚀,或者 🐖🐖🐖🐖🐖🐖🐖🐖🐖🐖,然后 unshift,放在第一个参数的位置


5.拿到 console.log 的结束行数,过程跟第 4 点类似,通过 push 放到最后一个参数的位置



6.在这过程中需要判断到处理过的,下次进来就要跳过,防止重复添加


以下是源码的实现过程,有兴趣的可以看看:


import { declare } from '@babel/helper-plugin-utils'
import generater from '@babel/generator'
import type { StringLiteral } from '@babel/types'
import { stringLiteral } from '@babel/types'


const DEFAULT_PRE_TIP = '🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀'
const SKIP_KEY = '@@babel-plugin-enhance-logSkip'

function generateStrNode(str: string): StringLiteral & { skip: boolean } {
const node = stringLiteral(str)
// @ts-ignore
node.skip = true
// @ts-ignore
return node
}

export default declare<Options>((babel, { preTip = DEFAULT_PRE_TIP, splitBy = '', endLine = false }) => {
const { types: t } = babel
const splitNode = generateStrNode(splitBy)
return {
name: 'enhance-log',
visitor: {
CallExpression(path) {
const calleeCode = generater(path.node.callee).code
if (calleeCode === 'console.log') {
// add comment to skip if enter next time
const { trailingComments } = path.node
const shouldSkip = (trailingComments || []).some((item) => {
return item.type === 'CommentBlock' && item.value === SKIP_KEY
})
if (shouldSkip)
return

t.addComment(path.node, 'trailing', SKIP_KEY)

const nodeArguments = path.node.arguments
for (let i = 0; i < nodeArguments.length; i++) {
const argument = nodeArguments[i]
// @ts-ignore
if (argument.skip)
continue
if (!t.isLiteral(argument)) {
if (t.isIdentifier(argument) && argument.name === 'undefined') {
nodeArguments.splice(i + 1, 0, splitNode)
continue
}
// @ts-ignore
argument.skip = true
const node = generateStrNode(`${generater(argument).code} =`)

nodeArguments.splice(i, 0, node)
nodeArguments.splice(i + 2, 0, splitNode)
}
else {
nodeArguments.splice(i + 1, 0, splitNode)
}
}
// the last needn't split
if (nodeArguments[nodeArguments.length - 1] === splitNode)
nodeArguments.pop()
const { loc } = path.node
if (loc) {
const startLine = loc.start.line
const startLineTipNode = t.stringLiteral(`line of ${startLine} ${preTip}:\n`)
nodeArguments.unshift(startLineTipNode)
if (endLine) {
const endLine = loc.end.line
const endLineTipNode = t.stringLiteral(`\nline of ${endLine} ${preTip}:\n`)
nodeArguments.push(endLineTipNode)
}
}
}
},
},
}
})

对了,这里有个问题是,我通过标记 path.node.skip = true 来跳过,但是还是会多次进入:


if (path.node.skip) return
path.node.skip = true

所以最终只能通过尾部添加注释的方式来避免多次进入:



有知道怎么解决的大佬还请提示一下,万分感谢~


总结


国际惯例,我们来总结一下,对于生产环境或真机调试,或者对于一些偏爱 console.log 的小伙伴,我们为了更快在控制台找到 log 的变量,通常会添加 log 函数,参数变量名,但前者一旦代码位置更改,打印的位置就跟源码不一致,后者又得重复写每个参数变量名的字符串,显得相当的麻烦。


为了更方便地使用 log,我们实现了个 babel 插件,功能包括:



  1. 自动打印行数

  2. 可以根据个人喜好加上 preTip,比如刷火箭 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀,或者可爱的小猪猪 🐖🐖🐖🐖🐖🐖🐖🐖🐖🐖

  3. 同时,对于有变量名的情况,可以加上变量名前缀,比如 const a = 1, console.log(a) => console.log('a = ', a)

  4. 还有,我们可以通过配置 splitBy、endLine 来自主选择任意分隔符、是否打印结束行等功能


最后


不知道大家有没有在追不良人,我是从高三追到现在。今天是周四,不良人第六季也接近尾声了,那就谨以此文来纪念不良人第六季的完结吧~



好了,再说一句,如果你是个偏爱 console.log 的前端 er,那请你喊出:泰裤辣(逃~)


作者:暴走老七
来源:juejin.cn/post/7231577806189133884
收起阅读 »

我是这样实现并发任务控制的

web
尽管js是一门单线程的脚本语言,其同步代码我们是自上而下读取执行的,我们无法干涉其执行顺序,但是我们可以借助异步代码中的微任务队列来实现任务的并发任务控制。那我们用一个例子来带入一下。 如何使下面的代码按照我所想的效果来输出 function timeout(...
继续阅读 »

尽管js是一门单线程的脚本语言,其同步代码我们是自上而下读取执行的,我们无法干涉其执行顺序,但是我们可以借助异步代码中的微任务队列来实现任务的并发任务控制。那我们用一个例子来带入一下。


如何使下面的代码按照我所想的效果来输出


function timeout(time) {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, time)
})
}

function addTask(time, name) {
superTask
.add(() => timeout(time))
.then(() => {
console.log(`任务${name}完成`);
})
}

addTask(10000, 1) // 10000 3
addTask(5000, 2) // 5000 1
addTask(3000, 3) // 8000 2
addTask(4000, 4) // 12000 4
addTask(5000, 5) // 15000 5

就是使得两个任务并发执行,当一个任务执行完了,下一个任务就接在空出来的任务队列中,以此类推。
在代码中我们可以看到在add方法调用后接了.then那么说明我们必须在add函数执行完返回出一个Promise对象


我们在实现并发任务控制之前需要明确并发几个任务执行我们应该可以人工控制


class SuperTask {
constructor(executeCount = 2) {
this.executeCount = executeCount; // 并发执行的任务数
this.runningCount = 0; // 正在执行的任务数
this.tasks = []; // 任务队列
}

// 添加任务
add(task) {
return new Promise((resolve,reject) => {
this.tasks.push({
task,
resolve,
reject
}); // add方法将任务添加到任务队列中,后面我会解释为什么需要这么做

/*接下来就是判断正在执行的任务,是不是达到了并发任务数量*/
this._run(); // 为了代码的可读性,将这个另外写一个方法
})
}

// 执行任务队列中的任务
_run() {
// 如果正在执行的任务数小于并发执行的任务数,那么我们就将任务队列队头的元素取出来执行
if(this.tasks.length && this.runningCount < this.executeCount) {
//任务队列中有任务, 并发任务队列有空余,可以执行任务
this.runningCount++;
const { task,resolve,reject } = this.tasks.shift();
task().then(resolve,reject).finally(res => {
this.runningCount--;
this._run();
})
}
}
}

利用Promise.then微任务的特性,我们可以控制Promise的状态改变时,再去取任务队列中队头元素再执行,这个地方有一些细节,就是add函数执行时,返回出的是一个Promise对象,但是我们需要将他resolve以及reject保存下来,使得所有的任务并不是共用同一个Promsie对象,而是每一个任务执行完都是独立的Promise,所以我们需要将其的resolve、reject保存下来,当任务并发执行的队列调用时再使用,再每个task执行完之后的.then之后就说明这个任务已经执行完了,此时this.runningCount--;再递归调用_run函数,考虑代码的完整性,我们使用Promise.finally来执行后续任务,Promise.finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。


代码测试


const superTask = new SuperTask(2)  // 并发执行两个任务

function addTask(time, name) {
superTask
.add(() => timeout(time))
.then(() => {
console.log(`任务${name}完成`);
})
}

addTask(10000, 1) // 10000 3
addTask(5000, 2) // 5000 1
addTask(3000, 3) // 8000 2
addTask(4000, 4) // 12000 4
addTask(5000, 5) // 15000 5

image.png

个人见解


在js中实现并发任务控制,其核心在于借助我们可控制的Promise的状态何时改变,在其状态改变时,说明之前在执行的任务已经执行完毕或者报错了,那么就应该执行后一个任务,但是如果只单纯使用Promsie.then去调用的话,如果其中某一个函数报错,那后续的就不会执行了,所以我们使用Promsie.finally来递归调用,为了保证每一个任务不会相互受影响,我们将其resolve和reject一并保存,在其执行时使用保存的resolve和reject,在后面接.finally这样就实现了。以上就是我个人对于并发任务的想法与见解,当然方式有各种各样,欢迎大家留言讨论。


作者:寒月十九
来源:juejin.cn/post/7209863093672394811
收起阅读 »

内存的清道夫——函数的尾调用

web
函数的尾调用 尾调用是什么,它能解决什么问题,他的存在意味着什么,为什么我叫他内存的清道夫,下面我将带读者通过概念,作用,尾巴递归三个方面来学习使用函数的尾调用。 尾调用概念 尾调用指的是在函数的最后一步通过return调用另一个函数 function fn(...
继续阅读 »



函数的尾调用


尾调用是什么,它能解决什么问题,他的存在意味着什么,为什么我叫他内存的清道夫,下面我将带读者通过概念作用尾巴递归三个方面来学习使用函数的尾调用。


尾调用概念


尾调用指的是在函数的最后一步通过return调用另一个函数


function fn() {
   return _fn()
}

如上就是一个标准的函数尾调用


尾调用的作用


尾调用的作用非常重要,它可以为我们节省在函数调用时的内存,这也是我为什么叫它内存的清道夫,我们先来看一下函数执行的底层原来,再来了解尾调用是如何节省内存的


函数执行中的帧


伴随函数的执行,函数会在内存中生成一个调用帧,我们先设定A,B,C三个函数,通过下面的形式调用(注意下面是普通的函数调用)


fn_A() {
   fn_B()
}
fn_B() {
   fn_C()
}

如上:A函数执行,执行B函数,B函数内执行C函数,函数执行的过程是这样的:



  • A执行生成一个A调用帧,在内部执行B函数

  • B函数生成一个调用帧在A调用帧上方

  • B函数内执行C函数,C函数执行生成一个调用帧在B调用帧上方,其本质就是一个调用帧栈

  • C函数执行完毕,C调用帧出栈

  • B函数执行完毕,B调用帧出栈

  • A函数执行完毕,A调用帧出栈


通过以上过程就能了解函数执行中会生成调用帧占用内存,而不断地嵌套函数会占据越来越多的内存空间,下面我们来看看尾调用是如何改变这一过程达到优化的效果。


尾调用优化的过程


那么如果我们使用尾调用来执行函数内部的函数。它的过程是怎么样的?


fn_A() {
  return fn_B()
}
fn_B() {
  return fn_C()
}


  • A执行生成一个A调用帧入栈

  • 由于B函数在尾部执行,无需A的资源,所有A调用帧出栈,生成B调用帧入栈

  • B函数执行,尾部调用C函数,无需B的资源,B调用帧出栈出栈,生成C调用帧入栈

  • C执行结束,C调用帧出栈


尾调用:在执行A函数中的B函数的前就可以对A调用帧进行出栈处理,也就是说在这连续嵌套一过程中,栈中只有一个调用帧,大大节省了内存空间,成为一名合格的内存清道夫!


注意:真正的尾调用是需要考虑到资源的占用,即B函数执行不需要A函数内的资源,才能算是真正的尾调用


一种特殊的尾调用


当尾调用的函数是自身的时候就诞生了一种特殊的尾调用形式即尾递归


function fn() {
   return fn()
}

正常的递归使用如果过多的话会产生栈溢出的现象,所以可以使用尾递归来解决这个问题,我们来看下面的例子


function fn(n) {
   if(n === 1) return 1
   return n * fn(n-1)
}
console.log(fn(6))
; // 720

如上是一个普通的递归函数求阶乘,那么我们可以使用尾递归来优化这个过程


function fn(n, tol) {
 if (n === 1) return tol;
 return fn(n - 1, n * tol);
}
console.log(fn(6, 1)); // 720

尾递归的实现


需要注意的是我们只有在严格模式下,才能开启尾调用模式,所以在其他场景我们需要使用其他的解决方案来替代尾调用,尾递归也同理,因为尾递归的过程其实是循环调用,所以利用循环调用可以变相实现尾递归,这里涉及到了一个名词:蹦床函数


function trampoline(f) {
 while (f && f instanceof Function) {
   f = f();
}
 return f;
}

如上就是一个蹦床函数的封装,传入的参数是要进行递归的函数,其作用是代替递归,进行循环调用传入参数,下面我们来看看具体应用


function num (x,y) {
   if(y > 0) {
       return num(x+1,y-1)
  }else {
       return x
  }
}
num(1,10000) // Maximum call stack size exceeded

Maximum call stack size exceeded就是栈溢出的报错,递归直接使用如果次数过多就会造成这样的现象,那么我们下面搭配蹦床函数使用。


function trampoline(f) {
 while (f && f instanceof Function) {
   f = f();
}
 return f;
}

function num(x, y) {
 if (y > 0) {
   return num.bind(null, x + 1, y - 1);
} else {
   return x;
}
}
console.log(trampoline(num(1, 1000))); // 1001

通过蹦床函数将递归函数纳入,以循环的形式调用,最后得到结果,不会发生栈溢出现象,总结来看,尾调用是切断函数与尾调用函数之间的联系,用完即释放,藕断丝不连,不占用内存的效果。


最后


函数的尾调用就到这里啦!如有错误,请多指教,欢迎关注猪痞

作者:猪痞恶霸
来源:juejin.cn/post/7125958517600550919
恶霸的JS进阶专栏。

收起阅读 »

深度介绍瀑布流布局

web
瀑布流布局 瀑布流又称瀑布流式布局,是比较流行的一种网站页面 布局方式。多行等宽元素排列,后面的元素依次添加到后面,接下来,要开始介绍这种布局如何实现 Html代码以及效果展示: 代码: 先使用一个container容器作为父容器,里面分别装了十个子容器b...
继续阅读 »

瀑布流布局



瀑布流又称瀑布流式布局,是比较流行的一种网站页面 布局方式。多行等宽元素排列,后面的元素依次添加到后面,接下来,要开始介绍这种布局如何实现
Html代码以及效果展示:


代码:


1.png
先使用一个container容器作为父容器,里面分别装了十个子容器box,这十个子容器下面分别装载了各自的样品图片。


效果展示:


2.png


Div是块级元素,所以每一张图片分别占据了一行。


 



接下来介绍瀑布流css的代码以及效果实现



Css的代码展示如下图:


3.png


在对整个页面自带的边框进行清除之后,对父容器container使用了相对定位,让其不需要脱离文档流,给子容器加上该有的样式后将其下面装载的图片进行父容器宽的100%继承,一般在这里设置只需要考虑宽或者高的一种继承设置即可。


 


效果展示:


4.png


因为每一张图片的高度不一致,所以图片排列并没有按照顺序排列。


 



JS部分的代码以及效果展示:



总体思路:


首先我们要获取所有我们要摆放的照片,根据用户视窗的宽度以及每一张图片的宽度计算出每一行会占据多少张图片,再计算第一列图片的高度,将下一列的图片分别对应最低高度的图片在其后面进行存放布局展示,然后更新新的高度,重复上述的操作。


 


首先,我们要调用imgLocation函数,将父容器和子容器放进这个函数进行调用。


5.png


Winodw.onload()函数的意思是必须等待网页全部加载完毕,包括在内的图片,然后再执行包裹代码。



接下来详细介绍imgLocation函数内容



6.png


在imgLocation函数里面设置parent和content两个形参,这两个形参将会对应调用函数的时候传递过来的两个实参,用cparent变量和ccontent变量分别对应parent(就是父容器container)以及content(就是父容器下对应的第一层子容器box)


getChildElement()函数是自己写的,再来介绍一下这个函数的内容:这个函数的作用就是取得父容器中的某一层子容器


代码展示:


7.png


说明:


首先我们设置一个变量数组contentArr存放最终取到的所有子容器,再设置一个变量存放所取得整个父容器的标签,直接通过标签来取得,显示就是直接用的是数组的形式。然后写一个for循环函数,在所有的标签下寻找对应的类名,便将其存放在contentArr数组中,最后返回一个数组形式,因此ccontent是一个数组形式。


 


接下来再回到imgLocation函数中,在获取所有要摆放的图片之后,我们要进行计算从哪一张图片是需要被操作,被摆放位置,计算第一行能存放多少图片。


代码展示:


8.png


说明:
winWidth装载的是用户的视窗宽度,imgWidth装载的是一张图片的宽度,因为每一张图片的宽度都是一致的,因此可以直接固定随意写一张图片的宽度,num存放的就是第一行能存放多少图片的张数。


接下来要开始操作第num+1章图片,先拿到第一列所有的图片的高度,我们使用一个数组进行高度的存放。


代码展示:


9.png


说明:


循环遍历取得的每一个子容器,前num个子容器只需要取得他们的高度即可,就是第一行的图片的高度。接下来这部分是我们要进行操作的box,math.min()这个里面装的是数字不是数组,调用apply将这个找最小值的方法借给数组用取得最小高度,然后minIndex拿到最低高度的图片所在的位置,就是下标。将需要操作的子容器图片的样式变成绝对定位,距离顶部的距离就是最低图片的高度,距离左边就是图片的小标乘以图片的宽度,因为每张图片的宽度都是一致的。最后再更新最矮的那一列高度,重复循环再找到新的最矮的高度进行排列。



所有代码展示:



10.png


11.png



效果展示:



12.png


作者:用户7299721929423
来源:juejin.cn/post/7233597845918679099
收起阅读 »

Vuex状态更新流程你都会了,面试官敢不给你发offer?

web
什么是Vuex?Vuex官方解释:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。 说到Vuex其实很多小伙伴的项目中基本都引入了,对Vuex...
继续阅读 »

什么是Vuex?Vuex官方解释:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。



说到Vuex其实很多小伙伴的项目中基本都引入了,对Vuex的使用其实并不陌生!但面试的时候,面试官突然问起:你对Vuex是怎么理解的?或者怎么看待Vuex?


此刻我估计有些小伙伴回答得还是磕磕盼盼,那么这篇文章就从O到1,再彻底讲一下我对Vuex的理解


首先咱从面试的角度出发?


1.面试官第一个问题:Vuex有哪几种属性?


共有5个核心属性:stategettersmutationsactionsmodules 。小伙伴们这里一定要背哦!不仅要背出来,还要英文发音标准哦。这是必须要装的逼!不然第一题都挂了,就没有然后了!


2.面试官第二个问题:请你说说这几属性的作用?


此时小伙伴们就要注意了,你不能这么回答:state是定义数据的,等同与vue里面的data啥啥啥的... getters又等同与vue里面的计算属性啥啥啥的...


这样回答虽说问题不大,但总又感觉好像差点意思!显得不专业,半吊子(半桶水),培训机构刚那个啥一样(纯属玩笑,没有任何贬低的意思)


咱换个思路,直接从Vuex的使用层面跟面试官去讲!咱先看官网流程图


1845321-20200916095435719-171834298.png


解析这张图之前咱先想一下,我们怎么获取Vuex里面的数据? 如果配置过Vuex的小伙伴此时就会说:新建store文件->index.js,进行如下配置



//store/index.js
import Vue from 'vue' //引入vue
import Vuex from 'vuex' //引入vuex

Vue.use(Vuex) //在vue里面注入

export default new Vuex.Store({
state: { //存放状态
flag:'我是一个全局变量'
},
})


//全局入口文件main.js
import store from './store/index.js'
new Vue({
store,
...//省
})

这样一个简单的vuex就配置好了。接下来怎么去页面引用呢? 非常简单!



//组件
<template>
<div>{{$store.state.flag}}</div>
</template>

<script>
//省...
</script>




怎么样? 小伙伴们,复杂吗?


那此时小伙伴就问了,那为什么上面那个流程图那么复杂呀?


这里咱就要解开你的疑惑了,上面流程图表述得更多的是教你修改状态。而不是读取状态!


咱现在要修改Vuex的里面state的flag,小伙伴们怎么改呀?


<template>
<div>{{$store.state.flag}}</div>
<button @click='edit()' >修改</button>
</template>

<script>
export default {
methods:{
edit(){
this.$store.state.flag = '修改全局变量'
},
}
}
</script>


上面这样做能改掉吗? 很明确的告诉你们,能改,但是不建议也不允许这样改!


为什么呢? 没有为什么(可参考这篇博客)!肯定是按照上面流程图的方式走呀。不然要这个流程图干嘛呀?对吧!那按照流程图的意思是,你要改状态,必须在mutations上面定义方法去改! 该怎么改呢?这里咱就要修改一下store文件->index.js


import Vue from 'vue' //引入vue
import Vuex from 'vuex' //引入vuex

Vue.use(Vuex) //在vue里面注入

export default new Vuex.Store({
state: { //存放状态
flag:'我是一个全局变量'
},
mutations:{ //新增mutations配置项,里面写修改状态的方法
Edit(state){
state.flag = '修改全局变量'
}
}
})

完成store文件->index.js文件修改后,咱组件里面使用如下:


<template>
<div>{{$store.state.flag}}</div>
<button @click='edit()' >修改</button>
</template>

<script>
export default {
methods:{
edit(){
this.$store.commit('Edit') //直接调用vuex下面mutations的方法名
},
}
}
</script>


怎么样?伙伴们?简单吗? 咱再回顾一下上面的流程图


微信截图_20230521213746.png


咱上面的案例是不是就完全按照流程图的方式修改的状态? 细心的小伙伴很快就发现了,很显然不是呀!
没经过黄色的Actions 这个玩意啊?


这里要补充说明一下,我之前也被误导了很久。这也是这张图没表明的地方!跟伙伴们这么说吧,黄色的可以省略!什么意思呢? 就是你可以直接从绿色(组件)->红色(mutations)->蓝色(state状态)


就像我们上面的案例:直接在Vue Components组件视图里面,通过 $store.commit 调用 Vuex里面的方法,vuex里面的方法再修改Vuex的数据,总之一句话,Vuex的数据修改,只能Vuex自己来,外人别参与!


那下面咱为了巩固一下这个思想,再次演示一下



//vuex 文件
import Vue from 'vue' //引入vue
import Vuex from 'vuex' //引入vuex

Vue.use(Vuex) //在vue里面注入

export default new Vuex.Store({
state: { //存放状态
flag:'我是一个全局变量'
},
mutations:{ //新增mutations配置项,里面写修改状态的方法
Edit(state,data){ //data是组件传过来的参数
state.flag = data
}
}
})

//vue组件
<template>
<div>{{$store.state.flag}}</div>
<button @click='edit("red")' >红色</button>
<button @click='edit("yellow")' >黄色</button>
<button @click='edit("green")' >绿色</button>
</template>
<script>
export default {
methods:{
edit(color){
this.$store.commit('Edit',color) //commit 有2个参数,第一个是方法名称,第二个参数
},
}
}
</script>



再次通过案例,说明了想要修改Vuex状态。就要遵守Vuex修改流程,把你要修改的值动态传入,就像普通的函数传参一样!只不过,你是通过 $store.commit 调用的函数方法。


好了,讲到这里。其实vuex核心就已经讲完了,因为你们已经知道了,Vuex怎么获取数据,也知道了Vuex怎么修改数据!


伙伴们又要说了,你开始讲了有5个属性!现在才讲2个,一个state,一个mutations? 就这么糊弄?


别急呀,伙伴们。咱也得喝口水嘛! 其实接下来剩下的3个就属于辅助性作用的存在了


什么叫辅助性呢? 因为刚才我们说了,核心已经讲完,你们知道怎么获取,也知道怎么修改。一个状态知道怎么获取,怎么修改。那就是已经完了呀! 剩下的只是说,让你怎么更好的去获取状态,更好的去管理状态,以及特殊情况下怎么修改状态了


接下来咱先讲特殊情况下怎么修改状态。什么叫特殊情况?异步就叫特殊情况。


咱们刚才的一系列操作,是不是都属于同步操作呀? 那么咱想象一个场景,咱要修改的这个状态一开始是不知道的,什么情况下知道呢?必须调用后端接口,后端返回给你,你才知道! 此时聪明的小伙伴就说了:这还不简单吗? 我直接在函数里面发请求呗!如下



//vuex 文件
import request form '../utils/request.js'
import Vue from 'vue' //引入vue
import Vuex from 'vuex' //引入vuex

Vue.use(Vuex) //在vue里面注入

export default new Vuex.Store({
state: { //存放状态
flag:'我是一个全局变量'
},
mutations:{ //新增mutations配置项,里面写修改状态的方法
Edit(state){
request('/xxx').then(res=>{ //异步请求
state.flag = res.data
})
}
}
})


这样做虽然可以改。但是小伙伴们咱又回到了最初的话题,这样改合规吗?符合流程图流程么?


是不是显然不符合呀,咱刚才讲同步代码修改状态的时候,是不是也特意把Actions提了一嘴?


所以此时Actions作用就在此发挥出来了,废话少说,Actions就是用来定义异步函数的,每当我们要发起请求获取状态,就必须写在这个函数里面,如下:



//vuex 文件
import request form '../utils/request.js'
import Vue from 'vue' //引入vue
import Vuex from 'vuex' //引入vuex

Vue.use(Vuex) //在vue里面注入

export default new Vuex.Store({
state: { //存放状态
flag:'我是一个全局变量'
},
mutations:{ //新增mutations配置项,里面写修改状态的方法
Edit(state){
state.flag = state
}
},
actions:{ //新增actions配置项
GetData({commit}){ //定义获取异步数据的方法
request('/xxx').then(res=>{ //异步请求
commit('Edit',res.data)
})
}
}
})


//vue组件
<template>
<div>{{$store.state.flag}}</div>
<button @click='setData()' >调用异步</button>
</template>
<script>
export default {
methods:{
setData(color){
this.$store.dispatch('GetData') //通过dispatch调用vuex里面的actions下面定义的方法名
},
}
}
</script>



这样一来,是不是就跟流程图彻底对应上了?


微信截图_20230521213746.png


Vue Components组件视图里面,通过 $store.dispatch 调用 Vuex里面的actions下定义的异步方法,然后通过vuex里面的异步 $store.commit 再调用vuex里面的同步方法,最终由同步方法修改状态。


最后总结出:只要涉及到修改状态,必须调用同步里面方法。不管是在组件使用 $store.commit, 还是在 actions 里面使用commit ,都必须调用mutations里面定义的方法 !


通过以上的实例,我相信大家已经明白了整个Vuex修改状态的过程。也对官网的流程图有了更清晰的认知了。


接下来就只剩下2个最简单的属性了,getters与modules


getters类似于vue组件中的computed,进行缓存,对于Store中的数据进行加工处理形成新的数据!直接看实例:



//vuex 文件
import request form '../utils/request.js'
import Vue from 'vue' //引入vue
import Vuex from 'vuex' //引入vuex

Vue.use(Vuex) //在vue里面注入

export default new Vuex.Store({
state: { //存放状态
flag:'我是一个全局变量!',
name:'伙伴们'
},
getters:{ //新增mutations配置项,里面写修改状态的方法
flagName(state){
return state.name + state.flag
}
},

})


//vue组件
<template>
<div>{{$store.state.flagName}}</div>
</template>
<script>
export default {

}
</script>



最后咱们来讲modulesmodules针对比较大型的项目时才能发挥优势,不然你项目太小,整个维护的状态都不超过10,那就没必要用modules了!


modules其实就是把Vuex里面的所有功能进行一个更细化的拆分:



//vuex 文件
import request form '../utils/request.js'
import Vue from 'vue' //引入vue
import Vuex from 'vuex' //引入vuex

Vue.use(Vuex) //在vue里面注入

const moduleA = { //组件A需要单独维护的状态
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}

const moduleB = { //组件B需要单独维护的状态
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}

const store = new Vuex.Store({
state:{}, //全局状态
modules: { //整体合并
moduleA: moduleA,
moduleB: moduleB
}
})



//vue组件A
<template>
<div>{{$store.state[moduleA].flag}}</div> //调用Vuex模块里面的状态时,要加moduleA模块名
</template>
<script>
export default {

}
</script>


//vue组件B
<template>
<div>{{$store.state[moduleB].flag}}</div> //调用Vuex模块里面的状态时,要加moduleB模块名
</template>

<script>
export default {

}
</script>



怎么样?小伙伴们。。是不是也挺简单的。


文章到了这里也就把Vuex基本知识都讲完了。小伙伴们需要自己多加练习与消化! 把文章中所讲的Vuex更新状态的流程在脑海里面多回想几遍。有任何疑问,评论区留言吧!


作者:阳火锅
来源:juejin.cn/post/7235603140262084665
收起阅读 »

能把队友气死的8种屎山代码(React版)

web
前几天在前端技术群里聊起Code Review的事,大伙儿似乎都憋了一肚子气: 我觉得这份难言之隐应该要让更多人看到,就跟Henry约了个稿: 于是Henry赶在周末,一边带娃,一边给我抹眼泪整理(脱敏)出了这篇小小的屎山合集,供大家品鉴。 以下是正文。...
继续阅读 »

前几天在前端技术群里聊起Code Review的事,大伙儿似乎都憋了一肚子气:


图片


图片


我觉得这份难言之隐应该要让更多人看到,就跟Henry约了个稿:


图片


于是Henry赶在周末,一边带娃,一边给我抹眼泪整理(脱敏)出了这篇小小的屎山合集,供大家品鉴。


以下是正文。


(文字大部分是Henry所写,沐洒进行了一些精简和调整)




1. 直接操作DOM


const a = document.querySelector('.a');

const scrollListener = throttle(() => {
  const currentY = window.scrollY;

  if (currentY > 100) {
    a.classList.add('show');
  } else {
    a.classList.remove('show');
  }
}, 300);

window.addEventListener('scroll', scrollListener);
return () => {
  window.removeEventListener('scroll', scrollListener);
};

上面的代码在监听scroll方法的回调函数中,直接上手修改DOM的类名。众所周知,React属于响应式编程,大部份情况都不需要直接操作DOM,具体原因参考官方文档(react.dev/learn/manip…


优化方法也很简单,充分发挥响应式编程的优点,用变量代替即可:


const [refreshStatus, setRefreshStatus] = useState('');

const scrollListener = throttle(() => {
  if (tab.getBoundingClientRect().top < topH) {
    setRefreshStatus('show');
  } else {
    setRefreshStatus('');
  }
}, 300);

return <div className={['page_refresh', refreshStatus].join(' ')}/>;

2. useEffect不指定依赖


依赖参数缺失。


useEffect(() => {
    console.log('no deps=====')
    // code...
});

这样的话,每次页面有重渲染,该useEffect都会执行,带来严重的性能问题。例如我们项目中,这个useEffect内部执行的是第一点中的内容,即每次都会绑定一个scroll事件的回调,而且页面中有实时轮询接口每隔5s刷新一次列表,用户在该页面稍加停留,就会有卡顿问题出现。解决方案很简单,根据useEffect的回调函数内容可知,如果需要在第一次渲染之后挂载一个scroll的回调函数,那么就给useEffect第二个参数传入空数组即可,参考官方文档(react.dev/reference/r…


useEffect(() => {
    // code...
}, []);

3. 硬编码


硬编码,即一些数据信息或配置信息直接写死在逻辑代码中,例如


图片


这两行代码本意是从url上拿到指定的参数的值,如果没有,会用一个固定的配置做兜底。


乍一看代码逻辑很清晰,但再想深一层,兜底值具体的含义是什么?为什么要用这两个值来兜底?写这行代码的同学可能很快可以解答,但是一段时间之后,写代码的人和提需求的人都找不到了呢?


这个示例代码还比较简单,拿对应的值去后台可以找到对应的含义,如果是写死的是枚举值,而且还没有类型定义,那代码就很难维护了。


图片


解决此类问题,要么将这些内容配置化,即写到一个config文件中,使用清晰的语义化命名变量;要么,至少在硬编码的地方写上注释,交代清楚这里需要硬编码的前因后果。



沐洒


关于硬编码问题,我在之前的一篇关于“配置管理”的文章里有详细阐述和应对方案,感兴趣的朋友可以看看《小白也能做出满分前端工程:01 配置管理



4. 放任文件长度,只着眼于当下的需求


很多同学做需求、写代码都比较少从全局考虑,只关注到当前需求如何完成。从“战术”上来说没有问题,快速完成产品的需求、快速迭代产品也是大家希望看到的。


可一旦只关注“战术实现”而忽略“战略设计”,除非做的产品是月抛型的,否则一定会遇到旧逻辑难以修改的情况。


如果再加上一个文件被多达10余人修改过的情况,那么每改一行代码都会是一场灾难,例如最近接手的一个页面:


图片


单文件高达1600多行!哪怕去除300多行的注释,和300多行的模板,剩下的逻辑代码也有1000行左右,这种代码可读性就极其糟糕,必须进行拆分。


而很常见的是,由于每一任经手人都疏于考虑全局,导致大量代码毫无模块化可言,甚至出现多个useEffect的依赖是完全相同的:


图片


这里明显还有另一个问题:滥用hooks。


从行号可以看出来确实是相同的依赖写了多个useEffect,很明显是多个同学各写各的的需求引入的这些hooks。

这代码跑肯定是能跑的,但是很可能会出现多个hooks中修改同一个变量,导致其他地方在使用的时候需要搞一些很tricky的操作来修Bug。


5.变量无初始值


在typescript的加持下,对变量的类型定义可以说是日益严格了。可是在一些变量的类型定义比较复杂的情况下,可能一个变量的字段很多、层级很复杂,此时有些同学就可能想偷个懒了,例如:


const [variable, setVariable] = useState();

// some code...
const queryData = function({
    // some logic
    setVariable({ showtrue });
};

useEffect(() => {
    queryData();
}, []);

return variable.show ?  : null;

这里的问题很明显,如果queryData耗时比较长,在第一次渲染的时候,最后一行的variable.show就会报错了,因为variable的初始值是undefined。所以声明变量时,一定要根据变量的类型设置好有效默认值。


6. 三元选择符嵌套使用


网上很多人会推荐说用三元选择符代替简单的if-else,但几乎没有见过有人提到嵌套使用三元选择符的事情,如果看到如下代码,不知道各位读者会作何感想?


{condition1 === 1
    ? "数据加载中"
    : condition2
    ? "没有更多了"
    : condition3
    ? "当前没有可用房间"
    : "数据加载中"}

真的很难理解,明明只是一个简单的提示语句的判断,却需要拿出分析性能的精力去理解,多少有点得不偿失了。


这还只是一种比较简单的三元选择符的嵌套,因为当各个条件分支都为true时,就直接返回了,没有做更多的判断,如果再多做一层,都会直接把人的cpu的干爆炸了。 


替代方案: 



  1. 直接用if-else,可读性更高,以后如果要加逻辑也很方便。

  2. Early Return,也叫卫语句,这种写法能有效简化逻辑,增加可读性。


if (condition1 === 1return "数据加载中";
if (condition2) return "没有更多了";
if (condition3) return "当前没有可用房间";
return "数据加载中";

虽然不嵌套的三元选择符很简单,但是在例如jsx的模版中,仍然不建议大量使用三元选择符,因为可能会出现如下代码:


return (
    condition1 ? (
        <div className={condition2 ? cls1 : cls2}>
            {condition3 ? "111" : "222"}
            {condition4 ? (
                a : b} />
            ) : null
        

    ) : (
        
            {condition6 ? children1 : children2}
        

    )
)

类似的代码在我们的项目中频繁出现,模版中大量的三元选择符导致文件内容拉得很长,很容易看着看着就不记得自己在哪个逻辑分支上了。


像这种简单的三元选择符,做成一个简单的memo变量,哪怕是在组件内直接写变量定义(例如:const clsName = condition2 ? cls1 : cls2),最终到模板的可读性也会比上述代码高。


7. 逻辑不拆分


React hooks可以很方便地帮助开发者聚合逻辑抽离成自定义hooks,千万不要把一个页面所有的useState、useEffect等全都放在一个文件中:


图片


其实从功能上可以对页面进行拆分,拆分之后这些变量的定义也就可以拆出去了。其中有一个很简单的原则就是,如果一个逻辑同时涉及到了useState和useEffect,那么就可以一并抽离出去成为一个自定义hooks。例如接口请求大家一般都是直接在业务逻辑中做:


const Comp = () => {
    const [data, setData] = useState({});
    const [loading, setLoading] = useState(false);
    
    useEffect(() => {
        setLoading(true);
        queryData()
            .then((response) => {
                setData(response);
            })
            .catch((error) => {
                console.error(error);
            })
            .finally(() => {
                setLoading(false);
            });
    });
    
    if (loading) return "loading...";
    
    return <div>{data.text}div>;
}

根据上面的原则,和数据拉取相关的内容涉及到了useState和useEffect,这整块逻辑就可以拆出去,那么最终就只剩下:


const Comp = () => {
    const { data, loading } = useQueryData();
    
    if (loading) return "loading...";
    
    return 
{data.text}
;
};

这样下来,Comp组件就变得身份清爽了。大家可以参考阿里的ahooks库,里面收集了很多前端常用的hooks,可以极大提升开发效率和减少重复代码。


8. 随意读取window对象的值


作为大型项目,很容易需要依赖别的模板挂载到window对象的内容,读取的时候需要考虑到是否有可能拿不到window对象上的内容,从而导致js报错?例如:


window.tmeXXX.a.func();

如果这个tmeXXX所在的js加载失败了,或者是某个版本中没有a这个属性或者func这个函数,那么页面就会白屏。


好啦,最近CR常出现的8种屎山代码都讲完了,你写过哪几种?你们团队的代码中又有哪些让你一口老血喷出来的不良代码呢?欢迎评论区告诉我。


作者:沐洒
来源:juejin.cn/post/7235663093748138021
收起阅读 »

原来JS可以这么实现继承

web
当我们在编写代码的时候,有一些对象内部会有一些方法(函数),如果将这些函数在构造函数内部声明会导致内存的浪费,因为实例化构造函数得到不同的实例对象,其内部都有同一个方法,但是占据了不同的内存,就存在内存浪费问题。于是乎我们就需要用到继承。 什么是继承? 通过某...
继续阅读 »

当我们在编写代码的时候,有一些对象内部会有一些方法(函数),如果将这些函数在构造函数内部声明会导致内存的浪费,因为实例化构造函数得到不同的实例对象,其内部都有同一个方法,但是占据了不同的内存,就存在内存浪费问题。于是乎我们就需要用到继承。


什么是继承?


通过某种方式让一个对象可以访问到另一个对象中属性和方法,我们将这种方法称之为继承(inheritance)


如果一个类B继承自另一个类A,就把B称之为A的子类,A称之为B的父类或者超类


如何实现继承?


1、原型链继承


// 原型链的继承
SuperType.prototype.getSuperValue = function () {
return this.property;
}
function SuperType() {
this.property = true
}

Type.prototype = new SuperType();

function Type() {
this.typeproperty = false
}

console.log(Type.prototype);

var instance = new Type()

console.log(instance.getSuperValue()); // true

让SuperType的实例对象赋给Type的原型,Type就能继承到SuperType的属性和方法


image.png


优点:原型链继承容易上手,书写简单,父类可以复用,被多个子类继承。


缺点:会在子类实例对象上共享父类所有的引用类型实例属性,(子类改不动父类的原始类型),更改一个子类的引用属性,其他子类均受影响;子类不能改父类传参。


2、经典继承(伪造对象)


// 经典继承
SuperType.prototype.name = '寒月十九'

function SuperType(age) {
this.color = ['red', 'green', 'blue'],
this.age = age
}

function Type(age) {
SuperType.call(this,age)
}

var instance = new Type(18)
console.log(instance);
console.log(instance.color);

经典继承就是借助this的显示绑定,将SuperType的指向绑定到Type身上,使得我们可以直接访问到SuperType身上的属性。


image.png


优点:解决了原型链继承子类不能向父类传参的问题和原型共享的问题。


缺点:方法不能复用;子类继承不到父类原型上的属性,只能继承到父类实例对象上的属性。


3、组合继承(原型链继承 + 经典继承)


// 组合继承 (伪经典继承)
SuperType.prototype.sayName =function() {
console.log(this.name);
}

function SuperType(name) {
this.name = name,
this.color = ['red', 'green', 'blue']
}
function Type(age,name) {
this.age = age
SuperType.call(this,name)
}

Type.prototype = new SuperType()
Type.prototype.constructor = Type

//Type.prototype被替换了,所以要补充一个constructor属性,指向自身,这样new Type得到的实例对象就有constructor属性

Type.prototype.sayAge = function() {
console.log(this.age)
}

var instance = new Type(20,'寒月十九');
instance.sayAge();

组合继承就是将上面两种继承方式结合起来,先将SuperType的this指向显示绑定到Type,然后再替换Type的原型,再添加上构造器属性指向自身,使得new Type()得到的实例对象就具备构造属性,并且可以继承到SuperType的属性。


image.png


优点:解决了原型链继承和经典继承的缺点造成的影响。


缺点:每一次都会调用两次父类的构造函数,一次是在创建子类原型上,另一次是在子类构造函数内部。


4、原型式继承


(1)借助构造函数实现对象的继承


// 原型式继承
function object(obj) {
function newFn() {}
newFn.prototype = obj;
return new newFn();
};
var person = {
name: '寒月十九',
age: 20,
like: {
sport: 'coding'
}
};
let newObj = object(person);

image.png


(2)Object.create()


var person = {
name: '寒月十九',
age: 20,
like: {
sport: 'coding'
}
};
let newPerson = Object.create(person,{sex: 'boy'});

image.png


优点:不需要单独构建构造函数。


缺点:属性中的引用地址会在相关对象中共享。


5、寄生式继承


function createPerson(original) {
var clone = Object.create(original)
clone.say = function() { // 增强这个对像
console.log('hello');
}
return clone;
};
var person = {
name: '寒月十九',
age: 20,
like: {
sport: 'coding'
}
};
let newPerson =createPerson(person);


寄生式继承是原型式继承的加强版,它结合原型式继承和工厂模式,创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回对象。


image.png


优点:上手简单,不用单独创建构造函数。


缺点:寄生式继承给对象添加函数会导致函数难以重用,因此不能做到函数复用而效率降低;引用类型的属性始终都会被继承所共享。


寄生组合式继承


// 寄生组合式继承
SuperType.prototype.sayName = function() {
console.log(this.name);
};
SuperType.prototype.like = {
a: 1,
b: 2
};
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green']
};

function Type(name, age) {
this.age = age;
SuperType.call(this, name);
}

var anotherPrototype = Object.assign(Type.prototype, SuperType.prototype);
// anotherPrototype.constructor = Type

Type.prototype = anotherPrototype; // new SuperType()

寄生组合继承是为降低父类构造函数的开销。通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。


image.png


优点:高效,只调用一个次父构造函数,不会在原型上添加多余的属性,原型链还能保持不变;开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。


缺点:代码较复杂。


class类继承


class Parent{
constructor(name) {
this.name = name;
this.hobbies = ["running", "basketball", "writting"];
}
getHobbies() {
return this.hobbies;
}
static getCurrent() { // 静态方法,只能类本身调用
console.log(this);
}
}

class Child extends Parent {
constructor(name) {
super(name);
}
}

var c1 = new Child('寒月十九');
var c2 = new Child('十九');

image.png


作者:寒月十九
来源:juejin.cn/post/7173708454975471646
收起阅读 »

从JS执行过程彻底讲清楚闭包、作用域链、变量提升等

web
前言 今天和大家一起 来 弄清楚一段 JavaScript 代码,它是如何执行的呢? 进而彻底讲明白闭包和作用于链的含义。 JavaScript 是一门高级语言,需要转化成机器指令,才能在电脑的 CPU 中运行。使 JavaScript 代码转换成机器指令,是...
继续阅读 »

前言


今天和大家一起 来 弄清楚一段 JavaScript 代码,它是如何执行的呢? 进而彻底讲明白闭包和作用于链的含义。

JavaScript 是一门高级语言,需要转化成机器指令,才能在电脑的 CPU 中运行。使 JavaScript 代码转换成机器指令,是通过 JavaScript 引擎来完成的。

JavaScript 引擎在把 JavaScript 代码转换成机器指令过程中,先对 JavaScript 代码进行解析(词法分析,语法分析),生成 AST 树,然后在通过一些列操作转换成机器指令,从而在 CPU 中运行。今天带大家详细讲解一下相关概念,并通过一个具体的案例加深大家对相关概念的理解。


JavaScript 执行过程


JavaSc 是一门高级语言,JavaScript 引擎会先把 JavaScript 代码转换成机器指令,先对 JavaScript 代码进行解析(词法分析,语法分析),生成 AST 树,然后转换成机器指令,进而会才能 CPU 中运行。

如下图所示:


JS执行过程.png


JS 执行过程,我们会遇到一些名词,这里在前面先做个解释


名词解释
ECS (Execution Context Stack) 执行上下文栈/调用栈以栈的形式调用创建的执行上下文。JavaScript 引擎内部实现了一个执行上文栈,目的就是为了执行代码。只要有代码执行,一定是在执行上下文栈中执行的。
GECGEC(Global Execution Context)全局执行上下文在执行全局代码前创建。 代码想要执行一定经过调用栈(上个关键词),也就意味着代码是以函数的形式被调用。但是全局代码(比如:定义变量、定义函数等)并不是函数形式,我们并不能主动调用代码,而被动的需要浏览器去调用代码。起到该作用的就是全局执行上下文,先解析全局代码然后执行。
FEC(Functional Execution Context)函数执行上下文在执行函数前创建。如果遇到函数的主动调用,就会生成一个函数执行上下文,入栈到函数调用栈中;当函数调动完成之后,就会执行出栈操作
VO(Variable Object)变量对象早期 ECMA 规范中的变量环境,对应 Object。该对象保存了当前执行上下文中的变量和函数地址(也就是当前作用域)。
VE(Variable Environment 变量环境最新 ECMA 规范中的变量环境,对应环境记录。 在最新 ECMA 的版本规范中:每一个执行上下文会关联到一个变量环境(Variable Environment,简称 VE),在执行代码中变量和函数的声明会作为环境记录(Environment Record)添加到变量环境中。对于函数来说,参数也会被作为环境记录添加到变量环境中。 简单来讲:1. 也就是相比于早期的版本规范,对于变量环境,已经去除了 VO 这个概念,提出了一个新的概念 VE;2. 没有规定 VE 必须为 Object,不同的 JS 引擎可以使用不同的类型,作为一条环境记录添加进去即可;3. 虽然新版本规范将变量环境改成了 VE,但是 JavaScript 的执行过程还是不变的,只是关联的变量环境不同,将 VE 看成 VO 即可;
GO(Global Object)全局对象全局对象,解析全局代码时创建,GEC 中关联的 VO 就是 GO
AO (Activation Object)函数对象函数对象,解析函数体代码时创建,FEC 中关联的 VO 就是 AO

名词太多不容易理解,这里不用去记,下面用到的时候重新从这里查找即可。


⚠️⚠️❗️❗️ 下面的小章节是按照特定顺序讲解的,讲解了代码生成执行过程。


解析阶段(编译器伪代码)



  1. 创建一个全局对象 GO/window(全局作用域)

  2. 词法分析。词法分析就是将我们写的代码块分解成词法单元。

  3. 检查语法是否有错误。语法分析是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。 并检查你的代码有没有什么低级的语法错误,如果有,引擎会停止执行并抛出异常。

  4. 给全局对象 GO 赋值(GO/VO 中不止包括变量自身,还包含其他的上下文等)


如果遇到了函数,编译阶段是不会去解析他,仅仅是在堆内存中创建了 FO 对象(会记录他的 parent scope 和 当前代码块),在 GO 中定义的函数变量会指向此变量。


生成全局对象的伪代码是什么? (变量提升考点)



  1. 从上到下查找,遇到 var 声明,先去全局作用域查找是否有同名变量,如有忽略当前声明,没有则添加声明变量为 GO 对象的属性,值为 undefined,并为变量分配内存。

  2. 遇到 function,如有同名变量,则将值替换为 function 函数,没有则添加到 GO,并分配内存并赋值。

  3. ES6 中的 class 声明也存在提升,不过它和 let、const 一样,被约束和限制了,其规定,如果再声明位置之前引用,则是不合法的,会抛出一个异常。


创建全局对象有什么用?



  • 所有的作用域(scope)都可以访问该全局对象;

  • 对象里面会包含一些全局的方法和类,像 Math、Date、String、Array、setTimeout 等等;

  • 其中有一个 window 属性是指向该全局对象自身的;

  • 该对象中会收集我们上面全局定义的变量,并设置成 undefined;

  • 全局对象是非常重要的,我们平时之所以能够使用这些全局方法和类,都是在这个全局对象中获取的;


什么是变量提升?


面试经常问是因为工作中经常因为他出现 BUG。
什么是变量提升:通常 JS 引擎会在正式执行之前先进行一次预编译,在这个过程中,首先将变量声明及函数声明提升至当前作用域的顶端,然后进行接下来的处理。



  • 函数提升只针对具名函数,而对于赋值的匿名函数(表达式函数),并不会存在函数提升。

  • 【提升优先级问题】函数提升优先级高于变量提升,且不会被同名变量声明覆盖,但是会被变量赋值后覆盖。而且存在同名函数与同名变量时,优先执行函数。


console.log(a);      //f a()
console.log(a()); //1
var a=1;
function a(){
console.log(1);
}
console.log(a); //1
a=3
console.log(a()) //a not a function

🤔 思考:(一道腾讯面试题)


var a=2;
function a() {
console.log(3);
}
console.log(typeof a);

为什么会进行变量提升?



  • 【比较信服的一种说法】正是由于第一批 JavaScript 虚拟机编译器上代码的设计失误,导致变量在声明之前就被赋予了 undefined 的初始值,而又由于这个失误产生的影响(无论好坏)过于广泛,因此在现在的 JavaScript 编译器中仍保留了变量提升的“特性”。

  • 【提升性能,这是预编译的好处,与变量提升没有没有关系】解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间

  • 【但也带了很多弊端】声明提升还可以提高 JS 代码的容错性,使一些不规范的代码也可以正常执行


现在讲完了变量赋值过程,接下来我们了解一下,全局执行上下文和函数执行上下文。


什么是 全局执行上下文 和 函数执行上下文?


全局执行上下文和函数执行上下文,大致也分为两个阶段:编译阶段和执行阶段。

解析过程中,获得了三个重要的信息(上下文包含的重要信息)【上下文对象中包含的信息有哪些?】:



  1. VO(Variable Object)对象:该对象保存了当前执行上下文中的变量和函数地址(也就是当前作用域)。

  2. 作用域链:VO(当前作用域) + ParentScope(父级作用域) 【在函数部分重要讲解】

  3. this 的指向: 视情况而定。


什么是作用域?


JavaScript 中的作用域说的是变量的可访问性和可见性。也就是说整个程序中哪些部分可以访问这个变量,或者说这个变量都在哪些地方可见。

作用域两个重要作用是 安全 和 命名压力(可以在不同的作用域下面定义相同的变量名)。

Javascript 中有三种作用域:



  1. 全局作用域

  2. 函数作用域

  3. 块级作用域
    作用域链:当在 Javascript 中使用一个变量的时候,首先 Javascript 引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域。


记住两句话:



  1. 父级作用域在编译阶段就已经确定了。

  2. 查找变量就是按照作用域链查找(找到近停止)[]

    也可以这么理解:作用域链是 AO 对象上的一个变量[scopeChain] 里面的变量是 当前的 VO+parentVO,当某个变量不存在时会顺着 parentVO 向上查找,直到找到为止。


什么是词法作用域?


词法作用域(也叫静态作用域)从字面意义上看是说作用域在词法化阶段(通常是编译阶段)确定而非执行阶段确定的。 JavaScript 的作用域是词法作用域。

例如:


let number = 42;
function printNumber() {
console.log(number);
}
function log() {
let number = 54;
printNumber();
}
// Prints 42
log();

上面代码可以看出无论printNumber()在哪里调用console.log(number)都会打印42

动态作用域不同,console.log(number)这行代码打印什么取决于函数printNumber()在哪里调用。

如果是动态作用域,上面console.log(number)这行代码就会打印54

使用词法作用域,我们可以仅仅看源代码就可以确定一个变量的作用范围,但如果是动态作用域,代码执行之前我们没法确定变量的作用范围。


什么是执行上下文?


简单的来说,执行上下文是一种对 Javascript 代码执行环境的一种抽象概念,也就是说只要有 Javascript 代码运行,那么它就一定是运行在执行上下文中。


Javascript 一共有三种执行上下文:



  • 全局执行上下文。

    这是一个默认的或者说基础的执行上下文,所有不在函数中的代码都会在全局执行上下文中执行。它会做两件事:创建一个全局的 window 对象(浏览器环境下),并将 this 的值设置为该全局对象,另外一个程序中只能有一个全局上下文。

  • 函数执行上下文。

    每次调用函数时,都会为该函数创建一个执行上下文,每一个函数都有自己的一个执行上下文,但注意是该执行上下文是在函数被调用的时候才会被创建。函数执行上下文会有很多个,每当一个执行上下文被创建的时候,都会按照他们定义的顺序去执行相关代码(这会在后面会说到)。

  • Eval 函数执行上下文。

    eval 函数中执行的代码也会有自己的执行上下文,但由于 eval 函数不会被经常用到,这里就不做讨论了。(译者注:eval 函数容易导致恶意攻击,并且运行代码的速度比相应的替代方法慢,因为不推荐使用)。


执行上下文栈(调用栈)?


了解了什么是全局对象后,下面就来聊聊代码具体执行的地方。JS 引擎为了执行代码,引擎内部会有一个执行上下文栈(Execution Context Stack,简称 ECS),它是用来执行代码的调用栈。


ECS 如何执行?先执行谁呢?



  • 无疑是先执行我们的全局代码块;

  • 在执行前全局代码会构建一个全局执行上下文(Global Execution Context,简称 GEC);

  • 一开始 GEC 就会被放入到 ECS 中执行;
    GEC 主要包含三个内容(和 FEC 基本一样): VO,作用域链,this 的指向。


调用栈(ECS)、全局执行上下文、函数执行上下文(FEC)三者大致的关系如下:


调用栈与全局执行上下文与函数执行上下文三者关系.png


函数执行上下文



在执行全局代码遇到函数如何执行呢?




  • 在执行的过程中遇到函数,就会根据函数体创建一个函数执行上下文(Functional Execution Context,简称 FEC),并且加入到执行上下文栈(ECS)中。

  • 函数执行上下文(FEC)包含三部分内容:

    • AO:在解析函数时,会创建一个 Activation Objec(AO);

    • 作用域链:由函数 VO 和父级 VO 组成,查找是一层层往外层查找;

    • this 指向:this 绑定的值,在函数执行时确定;



  • 其实全局执行上下文(GEC)也有自己的作用域链和 this 指向,只是它对应的作用域链就是自己本身,而 this 指向为 window。


变量环境和记录(VO 和 VE)


上文中提到了很多次 VO,那么 VO 到底是什么呢?下面从 ECMA 新旧版本规范中来谈谈 VO。

在早期 ECMA 的版本规范中:每一个执行上下文会被关联到一个变量环境(Variable Object,简称 VO),在源代码中的变量和函数声明会被作为属性添加到 VO 中。对应函数来说,参数也会被添加到 VO 中。



  • 也就是上面所创建的 GO 或者 AO 都会被关联到变量环境(VO)上,可以通过 VO 查找到需要的属性;

  • 规定了 VO 为 Object 类型,上文所提到的 GO 和 AO 都是 Object 类型;
    在最新 ECMA 的版本规范中:每一个执行上下文会关联到一个变量环境(Variable Environment,简称 VE),在执行代码中变量和函数的声明会作为环境记录(Environment Record)添加到变量环境中。对于函数来说,参数也会被作为环境记录添加到变量环境中。

  • 也就是相比于早期的版本规范,对于变量环境,已经去除了 VO 这个概念,提出了一个新的概念 VE;

  • 没有规定 VE 必须为 Object,不同的 JS 引擎可以使用不同的类型,作为一条环境记录添加进去即可;

  • 虽然新版本规范将变量环境改成了 VE,但是 JavaScript 的执行过程还是不变的,只是关联的变量环境不同,将 VE 看成 VO 即可;


什么是闭包?


MDN 上解释:闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。
也可以简单:函数 + 函数定义时的词法环境。


具体实例来理解整个执行过程


var name = 'curry'

console.log(message)

var message = 'I am new-coder.cn'

function foo() {
var name = 'foo'
console.log(name)
}

var num1 = 1
var num2 = 2

var result = num1 + num2

foo()

如下图:图中描述了上面这段代码在执行过程中所生成的变量。


JS代码执行过程


图中三个步骤的详细描述:



  1. 初始化全局对象。



  • 这里需要注意的是函数存放的是地址,会指向函数对象,与普通变量有所不同;

  • 从上往下解析 JS 代码,当解析到 foo 函数时,因为 foo 不是普通变量,并不会赋为 undefined,JS 引擎会在堆内存中开辟一块空间存放 foo 函数,在全局对象中引用其地址;

  • 这个开辟的函数存储空间最主要存放了该函数的父级作用域和函数的执行体代码块;




  1. 构建一个全局执行上下文(GEC),代码执行前将 VO 的内存地址指向 GlobalObject(GO)。




  2. 将全局执行上下文(GEC)放入执行上下文栈(ECS)中。




  3. 从上往下开始执行全局代码,依次对 GO 对象中的全局变量进行赋值。





  • 当执行 var name = 'curry'时,就从 VO(对应的就是 GO)中找到 name 属性赋值为 curry;

  • 接下来执行 console.log(message),就从 VO 中找到 message,注意此时的 message 还为 undefined,因为 message 真正赋值在下一行代码,所以就直接打印 undefined(也就是我们经常说的变量作用域提升);

  • 后面就依次进行赋值,执行到 var result = num1 + num2,也是从 VO 中找到 num1 和 num2 两个属性的值进行相加,然后赋值给 result,result 最终就为 50;

  • 最后执行到 foo(),也就是需要去执行 foo 函数了,这里的操作是比较特殊的,涉及到函数执行上下文,下面来详细了解;



  1. 遇到函数是怎么执行的
    继续来看上面的代码执行,当执行到 foo()时:



  • 先找到 foo 函数的存储地址,然后解析 foo 函数,生成函数的 AO;

  • 根据 AO 生成函数执行上下文(FEC),并将其放入执行上下文栈(ECS)中;

  • 开始执行 foo 函数内代码,依次找到 AO 中的属性并赋值,当执行 console.log(name)时,就会去 foo 的 VO(对应的就是 foo 函数的 AO)中找到 name 属性值并打印;



  1. 如此下去函数执行完成后会进行出栈,直到栈为空。代码出栈,如果出栈中发现当前上下文中的一些变量仍然被引用(形成了闭包),那就会将此出栈的上下文移动到堆中。


参考链接



作者:大熊全栈分享
来源:juejin.cn/post/7235463575300702263
收起阅读 »

前端开发:关于diff算法详解

web
前言 前端开发中,关于JS原生的内容和前端算法相关的内容一直都是前端工作中的核心,不管是在实际的前端业务开发还是前端求职面试,都是非常重要且必备的内容。那么本篇博文来分享一个关于前端开发中必备内容:diff算法,diff算法在前端实战中和前端求职面试中都是必...
继续阅读 »

前言



前端开发中,关于JS原生的内容和前端算法相关的内容一直都是前端工作中的核心,不管是在实际的前端业务开发还是前端求职面试,都是非常重要且必备的内容。那么本篇博文来分享一个关于前端开发中必备内容:diff算法,diff算法在前端实战中和前端求职面试中都是必备知识,整理总结一下,方便查阅使用。



diff算法是什么?


diff算法其实就是用于比较新旧虚拟DOM节点的差异性的算法。众所周知,每一个虚拟DOM节点都会有一个唯一标识,即Key,diff算法把树形结构按照层级分解,只比较同级元素,不同层级的节点只有创建和删除操作,通过对比新旧节点的Key来判断当前节点是否改变,把两个节点不同的地方存储在patch对象中,然后利用patch记录的消息进行局部更新DOM操作。


注意:若输入的新节点不是虚拟DOM , 那么需要将DOM节点转换为虚拟DOM才行,也就是说diff算法是针对虚拟DOM的。


patch()函数


patch函数其实就是用于节点上树,更新DOM的函数,也就是将新旧节点进行比较的函数。


diff算法的诞生


想必大家都知道,前端领域中在之前传统的DOM操作非常昂贵,数据的改变往往需要更新 DOM 树上的多个节点,可谓是牵一发而动全身,所以虚拟DOM和Diff算法的诞生就是为了解决上述问题。


前端的Web界面由 DOM 树来构成,当某一部分发生变化的时候,其实就是对应的某个 DOM 节点发生了变化。在 Vue中,构建 UI 界面的思路是由当前状态决定界面,前后两个状态就对应两套界面,然后由 Vue来比较两个界面的区别,本质是比较 DOM 节点差异当两个节点不同时应该如何处理,分为两种情况:一、节点类型不同;二、节点类型相同,但是属性不同。了解它们就需要对 DOM 树进行 Diff 算法分析。


diff算法的优势


diff算法的性能优势在于对比新旧两个 DOM节点的不同的时候,只对比同一级别的 DOM 节点,一旦发现有不同的地方,后续的DOM子节点将被删掉而不再作对比操作。使用diff算法提高了更新DOM的性能,不用再把整个页面全部删除后重新渲染;使用diff算法让虚拟DOM只包括必须的属性,不再把真实DOM的全部属性都拿出来。


diff算法的示例


这里先来以Vue来介绍一下diff算法的示例,这里直接在vue文件的模板中进行一个简单的标签实现,需要被vue处理成虚拟DOM,然后渲染到真实DOM中,具体代码如下所示:


//标签设置

//相对应的虚拟DOM结构

const dom = {

type: 'div',

attributes: [{id: 'content'}],

children: {

type: 'p',

attributes: [{class: 'sonP'}],

text: 'Hello'

}

}

通过上面的代码演示可以看到,新建标签之后,系统内存中会生成对应的虚拟DOM结构,由于真实DOM属性有很多,无法快速定位是哪个属性发生改变,然后通过diff算法能够快速找到发生变化的地方,然后只更新发生变化的部分渲染到页面上,也叫打补丁。


虚拟DOM


虚拟DOM是保存在程序内存中的,它只记录DOM的关键信息,然后结合diff算法来提高DOM更新的性能操作,在程序内存中比较差异,最后给真实DOM打补丁更新操作。


diff算法的比较规则


diff算法在进行比较操作的规则是这样的:



  1. 新节点前和旧节点前;

  2. 新节点后和旧节点后;

  3. 新节点后和旧节点前;

  4. 新节点前和旧节点后。


只要符合一种情况就不会再进行判断,若没有符合的,就需要循环来寻找,移动到旧前之前操作。结束查找的前提是:旧节点前<旧节点后 或者 新节点后>新节点前。


image.png


diff算法的三种比较方式


diff算法的比较方式有三种,分别如下所示:


方式一:根元素发生改变,直接删除重建


也就是同级比较,根元素发生改变,整个DOM树会被删除重建。如下示例:


//旧的虚拟DOM
<ul id="content">
<li class="sonP">hello</li>
</ul>
//新的虚拟DOM
<div id="content">
<p class="sonP">hello</p>
</div>

方式二:根元素不变,属性改变,元素复用,更新属性


这种方式就是在同级比较的时候,根元素不变,但是属性改变之后更新属性,示例如下所示:


//旧的虚拟DOM
<div id="content">
<p class="sonP">hello</p>
</div>
//新的虚拟DOM
<div id="content" title="hello">
<p class="sonP">hello</p>
</div>

方式三:根元素不变,子元素不变,元素内容发生变化


也就是根元素和子元素都不变,只是内容发生改变,这里涉及到三种小的情况:无Key直接更新、有Key但以索引为值、有Key但以id为值。


1、无Key直接更新


无Key直接就地更新,由于v-for不会移动DOM,所以只是尝试复用,然后就地更新;若需要v-for来移动DOM,则需要用特殊 attribute key 来提供一个排序提示。示例如下所示:


<ul id="content">
<li v-for="item in array">
{{ item }}
<input type="text">
</li>
</ul>

<button @click="addClick">在下标为1的位置新增一行</button>
export default {
data(){
return {
array: ["11", "44", "22", "33"]
}
},
methods: {
addClick(){
this.array.splice(1, 0, '44')
}
}
};

2、有Key但以索引为值


这里也是直接就地更新,通过新旧虚拟DOM对比,key存在就直接复用该标签更新的内容,若key不存在就直接新建一个。示例如下所示:


-

{{ item }}

在下标为1的位置新增一行

export default {

data(){

return {

array: ["11", "44", "22", "33"]

}

},

methods: {

addClick(){

this.array.splice(1, 0, '44')

}

}

};

通过上面代码可以看到,通过v-for循环产生新的DOM结构, 其中key是连续的, 与数据对应一致,然后比较新旧DOM结构, 通过diff算法找到差异区别, 接着打补丁到页面上,最后新增补一个li,然后从第二元素以后都要更新内容。


3、有Key但以id为值


由于Key的值只能是唯一不重复的,所以只能以字符串或数值来作为key。由于v-for不会移动DOM,所以只是尝试复用,然后就地更新;若需要v-for来移动DOM,则需要用特殊 attribute key 来提供一个排序提示。


若新DOM数据的key存在, 然后去旧的虚拟DOM里找到对应的key标记的标签, 最后复用标签;若新DOM数据的key存在, 然后去旧的虚拟DOM里没有找到对应的key标签的标签,最后直接新建标签;若旧DOM结构的key, 在新的DOM结构里不存在了, 则直接移除对应的key所在的标签。


<ul id="content">
<li v-for="object in array" :key="object.id">
{{ object.name }}
<input type="text">
</li>
</ul>

<button @click="addClick">在下标为1的位置新增一行</button>
export default {
data(){
return {
array: [{id:11,name:"11"}, {id:22,name:"22"}, {id:33,name:"33"}]
}
},
methods: {
addClick(){
this.array.splice(1, 0,{id:44,name: '44'})
}
}
};

最后


通过本文关于前端开发中关于diff算法的详细介绍,diff算法不管是在实际的前端开发工作中还是在前端求职面试中都是非常关键的知识点,所以作为前端开发者来说必须要掌握它相关的内容,尤其是从事前端开发不久的开发者来说尤为重要,是一篇值得阅读的文章,重要性就不在赘述。欢迎关注,一起交流,共同进步。


作者:三掌柜
来源:juejin.cn/post/7235534634775347261
收起阅读 »

我给我的博客加了个在线运行代码功能

web
获取更多信息,可以康康我的博客,所有文章会在博客上先发布随记 - 记录指间流逝的美好 (xiaoyustudent.github.io) 前言 新的一年还没过去,我又开始搞事情了,偶尔一次用到了在线编辑网页代码的网站,顿时想到,能不能自己实现一个呢?(PS:反...
继续阅读 »

获取更多信息,可以康康我的博客,所有文章会在博客上先发布随记 - 记录指间流逝的美好 (xiaoyustudent.github.io)


前言


新的一年还没过去,我又开始搞事情了,偶尔一次用到了在线编辑网页代码的网站,顿时想到,能不能自己实现一个呢?(PS:反正也没事干),然后又想着,能不能用在博客上呢,这样有些代码可以直接展现出来,多好,说干就干,让我们康康怎么去实现一个在线编辑代码的功能吧。(PS:有瑕疵,还在学习!勿喷!orz)


大致介绍


大概的想法就是通过iframe标签,让我们自己输入的内容能够在iframe中显示出来,知识点如下,如果有其他问题,欢迎在下方评论区进行补充!



  1. 获取输入的内容

  2. 插入到iframe中

  3. 怎么在博客中显示



当然也有未解决的问题:目前输入的js代码不能操作输入的html代码,查了许多文档,我会继续研究的,各位大佬如果有想法欢迎讨论



页面搭建


页面搭建很简单,就是三个textarea块,加4个按钮,就直接上代码了


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>在线编辑器</title>
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
<script>
$(document).ready(function () {
$('.HTMLBtn').click(function () {
$("#cssTextarea").fadeOut(function () {
$("#htmlTextarea").fadeIn();
});
})

$('.CSSBtn').click(function () {
$("#htmlTextarea").fadeOut(function () {
$("#cssTextarea").fadeIn();
});
})
});
</script>
<style>
* {
padding: 0;
margin: 0;
}

body,
html {
width: 100%;
height: 100%;
}

.main {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
height: 100%;
}

.textarea-box {
display: flex;
flex-direction: column;
width: calc(50% - 20px);
padding: 10px;
background: rgba(34, 85, 85, 0.067);
}

.textarea-function-box {
display: flex;
flex-direction: row;
justify-content: space-between;
}

.textarea-function-left,.textarea-function-right {
display: flex;
flex-direction: row;
}

.textarea-function-left div,
.textarea-function-right div {
padding: 5px 10px;
border: 1px solid rgb(9, 54, 99);
border-radius: 3px;
cursor: pointer;
}

.textarea-function-left div:not(:first-child) {
margin-left: 10px;
}

#htmlTextarea,
#cssTextarea {
height: calc(100% - 30px);
width: calc(100% - 20px);
margin-top: 10px;
padding: 10px;
overflow-y: scroll;
background: #fff;
}

.html-div {
background-color: cadetblue;
margin-top: 10px;
flex: 1;
}

.iframe-box {
width: 50%;
flex: 1;
overflow: hidden;
}
</style>
</head>

<body>
<div class="main">
<div class="textarea-box">
<div class="textarea-function-box">
<div class="textarea-function-left">
<div class="HTMLBtn">HTML</div>
<div class="CSSBtn">CSS</div>
</div>
<div class="textarea-function-right">
<input type="text" id="input_name">
<div class="run">运行</div>
<div class="download">保存</div>
</div>
</div>
<textarea id="htmlTextarea" placeholder="请输入html代码"></textarea>
<textarea id="cssTextarea" placeholder="请输入css代码" style="display: none;"></textarea>
</div>
<div class="iframe-box">
<iframe style="height: 100%;width: 100%;" src="" frameborder="0"></iframe>
</div>
</div>
</body>
</html>

忽略我的样式,能用就行!!


运行代码


这里是核心功能,应该怎么把代码运行出来呢,我这里用的是iframe,通过获取iframe元素,然后把对应的代码插入进去


$('.run').click(function () {
var htmlTextarea = document.querySelector('#htmlTextarea').value;
var cssTextarea = document.querySelector('#cssTextarea').value;
htmlTextarea += '<style>' + cssTextarea + '</style>'
// 获取html和css代码
let frameWin, frameDoc, frameBody;
frameWin = document.querySelector('iframe').contentWindow;
frameDoc = frameWin.document;
frameBody = frameDoc.body;
// 获取iframe元素

$(frameBody).html(htmlTextarea);
// 使用jqury的html方法把代码插入进去,这样能够直接执行
})

这样一个基本的在线代码编辑网页就完成了,接下来,我们看下怎么把这玩意给用在博客当中!


hexo设置


首先我们需要创建一个文件夹,用来放置我们写好的在线的html文件。在source文件夹下新建文件online,并且设置禁止渲染此文件夹,打开_config.yml文件,并设置以下


skip_render: online/*

页面设置


我目前想到的办法就是保存文件,然后在hexo里使用,添加以下代码


<div class="textarea-function-right">
<input type="text" id="input_name">
<div class="download">保存</div>
<!-- .... -->
</div>

<script>
function fake_click(obj) {
var ev = document.createEvent("MouseEvents");
ev.initMouseEvent(
"click", true, false, window, 0, 0, 0, 0, 0
, false, false, false, false, 0, null
);
obj.dispatchEvent(ev);
}

function export_raw(name, data) {
var urlObject = window.URL || window.webkitURL || window;
var export_blob = new Blob([data]);
var save_link = document.createElementNS("http://www.w3.org/1999/xhtml", "a")
save_link.href = urlObject.createObjectURL(export_blob);
save_link.download = name;
fake_click(save_link);
}

$(document).ready(function () {
$(".download").click(function () {
let scriptStr = $('html').first().context.getElementsByTagName("script")[2].innerHTML
var htmlTextarea = document.querySelector('#htmlTextarea').value != "" ? document.querySelector('#htmlTextarea').value : '""';
var cssTextarea = document.querySelector('#cssTextarea').value != "" ? document.querySelector('#cssTextarea').value : '""';
let htmlStr = $('html').first().context.getElementsByTagName("html")[0].innerHTML.replace(scriptStr, "").replace('<div class="download">保存</div>', "").replace('<input type="text" id="input_name">',"").replace("<script><\/script>", "<script>$(document).ready(function(){document.querySelector('#htmlTextarea').value = `" + htmlTextarea + "`;document.querySelector('#cssTextarea').value = `" + cssTextarea + "`;})<\/script>")
let n = $('#input_name').val()!=""?$('#input_name').val():"text";
export_raw(n+'.html', htmlStr);
})
})
</script>

可能很多同学会好奇为啥我这里用的script标签框起来,我们看下这个图片和这个代码


script.png


et scriptStr = $('html').first().context.getElementsByTagName("script")[2].innerHTML

很简单,我们保存后的代码,是没有这一段js代码的,所以需要替换掉,而这里一共有3个script块,最后一个,也就是下标为2的script块会被替换掉。同理,后面替换掉保存按钮,input输入框(输入框是输入文件名称的,默认名称是text)。


同时这里把我们输入的数据,通过js代码的方式加入进保存后的文件里,实现打开文件就能看到我们写的代码。之后我们把保存后的文件放在刚才我们创建的online文件夹下


text.png


hexo里面使用


使用就很简单了,我们通过iframe里面的src属性即可


<iframe src="/online/text.html" style="display:block;height:400px;width:100%;border:0.5px solid rgba(128,128,128,0.4);border-radius:8px;box-sizing:border-box;"></iframe>

展示图


show.png


作者:新人打工仔
来源:juejin.cn/post/7191520909709017144
收起阅读 »

判断数组成员的几种方法

web
在开发中经常需要我们在数组中查找元素又或者是判断元素是否存在,所以我列举了几种常用的方法供掘友参考学习。 indexOf() 首先想到的就是indexOf()方法,查找元素,并返回第一个找到的位置索引 [1,2,3,2].indexOf(2)  // 1 ...
继续阅读 »



在开发中经常需要我们在数组中查找元素又或者是判断元素是否存在,所以我列举了几种常用的方法供掘友参考学习。


indexOf()


首先想到的就是indexOf()方法,查找元素,并返回第一个找到的位置索引


 [1,2,3,2].indexOf(2)  // 1

他还支持第二个可选参数,指定开始查找的位置


 [1,2,3,2].indexOf(2,2)  // 3

但是indexOf()有个问题,他的实现是由===作为判断的,所以这容易造成一些问题,比如他对于NaN会造成误判


[NaN].indexOf(NaN) // -1
console.log(NaN === NaN) // false

如上,由于误判,没有找到匹配元素,所以返回-1,而在ES6对数组的原型上新增了incudes()方法,他可以代替indexOf(),下面来看看这个方法。


includes()


在ES6之前只有字符串的原型上含有include()方法来判断是否包含字串,而数组在ES6中也新增了include()方法来判断是否包含某个元素,下面来看看如何使用。


[1,2,3].includes(2) // true

数组实例直接调用,参数为要查找的目标元素,返回值为布尔值。而且他能很好地解决indexOf()的问题:


[NaN].includes(NaN) // true

如上includes()可以正确地判断NaN的查找问题,而includes()是用来判断是否包含,查找条件也比较单一,那么如果想要自定义查找条件,比如查找的范围,可以使用这么一对方法:find()与findIndex()接下来看一看他们是如何使用的。


find()与findIndex()


find()findIndex()可以匹配数组符合条件的元素


find()


find()支持三个参数,分别为valueindexarr,分别为当前值,当前位置,与原数组,,返回值为符号条件的值


let arr = [1,2,10,6,19,20]
arr.find((value,index,arr) => {
   return value > 10
}) // 19

如上,我以元素大于10为范围条件,返回了第一个符合范围条件的值:19。而find()可以返回符合条件的第一个元素,那么我们要是想拿到符合条件的第一个元素索引就可以使用findIndex()


findIndex()


findIndex()find相似也支持三个参数,但是返回值不同,其返回的是符合条件的索引


let arr = [1,2,10,6,19]
arr.findIndex((value,index,arr) => {
   return value > 10
}) // 4

例子与find()相同,返回的是19对应的索引


对于NaN值


find()findIndex()NaN值也不会误判,可以使用Object.is()来作为范围条件来判断NaN值,如下


[NaN].find((value)=> {
   return Object.is(NaN,value)
}) // NaN

如上例子,findIndex()也同理


最后


判断元素在某数组中是否存在的四种方法就说到这里,对掘友有所帮助的话就点个小心心吧,也欢迎关

作者:猪痞恶霸
来源:juejin.cn/post/7125632393821552677
注我的JS进阶专栏。

收起阅读 »

JS实现继承的几种方式

web
继承作为面向对象语言的三大特性之一,可以在不影响父类对象实现的情况下,使得子类对象具有父类对象的特性;同时还能再不影响父类对象行为的情况下扩展子类对象独有的特性,为编码带来了极大的便利。 下面我们就来看看 JavaScript 中都有哪些实现继承的方法。 原...
继续阅读 »

继承作为面向对象语言的三大特性之一,可以在不影响父类对象实现的情况下,使得子类对象具有父类对象的特性;同时还能再不影响父类对象行为的情况下扩展子类对象独有的特性,为编码带来了极大的便利。



下面我们就来看看 JavaScript 中都有哪些实现继承的方法。


原型链继承


原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针。



原型链继承的主要思想是:重写子类的prototype属性,将其指向父类的实例



下面我们结合代码来了解一下。



function Animal (name) {

  // 属性

  this.name = name

  this.type = 'Animal'

  // 实例函数

  this.sleep = function () {

    console.log(this.name + '正在睡觉');

  }

}

// 原型函数

Animal.prototype.eat = function (food) {

  console.log(`${this.name}正在吃${food}`);

}




// 子类

function Cat (name) {

  this.name = name

}

// 原型继承

Cat.prototype = new Animal()

// 将Cat的构造函数指向自身

Cat.prototype.constructor = Cat




let cat = new Cat('Tom')

console.log(cat.name) // Tom

console.log(cat.type) // Animal

cat.sleep() // Tom正在睡觉

cat.eat('猫罐头') // Tom正在吃猫罐头


在子类Cat中,我们没有增加type属性,因此会直接继承父类Animaltype属性。


在子类Cat中,我们增加了name属性,在生成子类实例时,name属性会覆盖父类Animal属性值。


同样因为Catprototype属性指向了Animal类型的实例,因此在生成实例Cat时,会继承实例函数和原型函数。



需要注意:
Cat.prototype.constructor = Cat


如果不将Cat原型对象的constructor属性指向自身的构造函数,那将指向父类Animal的构造函数。



原型链继承的优点


简单,易于实现


只需要设置子类的prototype属性指向父类的实例即可。


可通过子类直接访问父类原型链属性和函数


原型链继承的缺点


子类的所有实例将共享父类的属性


子类的所有实例将共享父类的属性会带来一个很严重的问题,父类包含引用值时,子类的实例改变该引用值会在所有实例中共享。



function Animal () {

  this.skill = ['eat', 'jump', 'sleep']

}

function Cat () {}

Cat.prototype = new Animal()

Cat.prototype.constructor = Cat




let cat1 = new Cat()

let cat2 = new Cat()

cat1.skill.push('walk')

console.log(cat1.skill) // ["eat", "jump", "sleep", "walk"]

console.log(cat2.skill) // ["eat", "jump", "sleep", "walk"]


在子类实例化时,无法向父类的构造函数传参


在通过new操作符创建子类的实例时,会调用子类的构造函数,而在子类的构造函数中并没有设置与父类关联,从而导致无法向父类的构造函数传递参数。


无法实现多继承


子类的prototype只能设置一个值,设置多个值时,后面的值会覆盖前面的值。


构造函数继承(借助 call)



构造函数继承的主要思想:在子类的构造函数中通过call()函数改变thi的指向,调用父类的构造函数,从而将父类的实例的属性和函数绑定到子类的this上。




  // 父类

function Animal (age) {

  // 属性

  this.name = 'Animal'

  this.age = age

  // 实例函数

  this.sleep = function () {

    console.log(this.name + '正在睡觉');

  }

}

// 原型函数

Animal.prototype.eat = function (food) {

  console.log(`${this.name}正在吃${food}`);

}

function Cat (name) {

  // 核心,通过call()函数实现Animal的实例的属性和函数的继承

  Animal.call(this)

  this.name = name

}




let cat = new Cat('Tom')

cat.sleep() // Tom正在睡觉

cat.eat() // Uncaught TypeError: cat.eat is not a function


通过代码可以发现,子类可以正常调用父类的实例函数,而无法调用父类原型上的函数,这是因为子类并没有通过某种方式来调用父类原型对象上的函数


构造继承的优点


解决了子类实例共享父类属性的问题


call()函数实际时改变父类Animal构造函数中this的指向,然后调用this指向了子类Cat,相当于将父类的属性和函数直接绑定到了子类的this中,成了子类实例的熟属性和函数,因此生成的子类实例中是各自拥有自己的属性和函数,不会相互影响。


创建子类的实例时,可以向父类传参



   // 父类

function Animal (age) {

  this.name = 'Animal'

  this.age = age

}

function Cat (name, parentAge) {

  // 在子类生成实例时,传递参数给call()函数,间接地传递给父类,然后被子类继承

  Animal.call(this, parentAge)

  this.name = name

}




let cat = new Cat('Tom', 10)

console.log(cat.age)


可以实现多继承


在子类的构造函数中,可以多次调用call()函数来继承多个父对象。


构造函数的缺点


实例只是子类的实例,并不是父类的实例


因为我们并未通过原型对象将子类与父类进行串联,所以生成的实例与父类并没有关系。


只能继承父类实例的属性和函数,并不能继承原型对象上的属性和函数


与上面原因相同。


无法复用父类的构造函数


因为父类的实例函数将通过call()函数绑定到子类的this中,因此子类生成的每个实例都会拥有父类实例的引用,这会造成不必要的内存消耗,影响性能。


组合继承



组合继承的主要思想:结合构造继承和原型继承的两种方式,一方面在子类的构造函数中通过call()函数调用父类的构造函数,将父类的实例的属性和函数绑定到子类的this中;另一方面,通过改变子类的prototype属性,继承父类的原型对象上的属性和函数。




// 父类

function Animal (age) {

  // 实例属性

  this.name = 'Animal'

  this.age = age

  this.skill = ['eat', 'jump', 'sleep']

  // 实例函数

  this.sleep = function () {

    console.log(this.name + '正在睡觉')

  }

}

// 原型函数

Animal.prototype.eat = function (food) {

  console.log(`${this.name}正在吃${food}`)

}




// 子类

function Cat (name) {

  // 通过构造函数继承实例的属性和函数

  Animal.call(this)

  this.name = name

}

// 通过原型继承原型对象上的属性和函数

Cat.prototype = new Animal()

Cat.prototype.constructor = Cat




let cat = new Cat('Tom')

console.log(cat.name) // Tom

cat.sleep() // Tom正在睡觉

cat.eat('猫罐头') // Tom正在吃猫罐头


组合继承的优点


既能继承父类实例的属性和函数,又能继承原型对象上的属性和函数


既是子类的实例,又是父类的实例


不存在引用属性共享的问题


构造函数作用域优先级比原型链优先级高,所以不会出现引用属性共享的问题。


可以向父类的构造函数中传参


组合继承的缺点


父类的实例属性会被绑定两次


在子类的构造函数中,通过call()函数调用了一次父类的构造函数;在改写子类的prototype属性,生成的实例时又调用了一次父类的构造函数。


寄生组合继承


组合继承方案已经足够好,但是针对其存在的缺点,我们仍然可以进行优化。


在进行子类的prototype属性的设置时,可以去掉父类实例的属性的函数



  //父类

function Animal (age) {

  // 实例属性

  this.name = 'Animal'

  this.age = age

  this.skill = ['eat', 'jump', 'sleep']

  // 实例函数

  this.sleep = function () {

    console.log(this.name + '正在睡觉')

  }

}

// 原型函数

Animal.prototype.eat = function (food) {

  console.log(`${this.name}正在吃${food}`)

}

// 子类

function Cat (name) {

  // 继承父类的实例和属性

  Animal.call(this)

  this.name = name

}

// 继承父类原型上的实例和属性

Cat.prototype = Object.create(Animal.prototype)

Cat.prototype.constructor = Cat

let cat = new Cat('Tom')




console.log(cat.name) // Tom

cat.sleep() // Tom正在睡觉

cat.eat('猫罐头') // Tom正在吃猫罐头



其中最关键的语句:






Cat.prototype = Object.create(Animal.prototype)






只取父类Animal的prototype属性,过滤掉Animal的实例属性,从而避免了父类的实例属性绑定两次。



这种寄生组合式继承方式,基本可以解决前几种继承方式的缺点,较好地实现了继承想要的结果,同时也减少了构造次数,减少了性能的开销。


整体看下来,这六种继承方式中,寄生组合式继承是这里面最优的继承方式。


总结


image.png


作者:蜡笔小群
来源:juejin.cn/post/7168856064581091364
收起阅读 »

我们在搜索一个问题的时候浏览器究竟做了什么

web
1+1=?,这个问题一直困扰着我,这天摸鱼的时间,我打开浏览器,在地址栏中输入http://www.baidu.com,按下回车,从这时起,我的疑虑从1+1=?变成了打开百度时浏览器到底做了什么工作? 这算是一个面试常见题,反正我被提问了无数次 TuT 为什...
继续阅读 »

1+1=?,这个问题一直困扰着我,这天摸鱼的时间,我打开浏览器,在地址栏中输入http://www.baidu.com,按下回车,从这时起,我的疑虑从1+1=?变成了打开百度时浏览器到底做了什么工作?



这算是一个面试常见题,反正我被提问了无数次 TuT

为什么面试官总喜欢提问这个问题?一般面试官问这个问题,是为了考察前端的广度和深度。



在浏览器地址栏键入URL地址


当我们在浏览器地址栏输入一个URL地址后,浏览器会开一个线程来对我们输入的URL进行解析处理。


1. DNS域名解析


我们在浏览器中输入的URL通常是一个域名,浏览器并不认识域名,它只知道IP,而我们平时记住的一般是域名,所以需要一个解析过程,让浏览器认得我们输入的内容。



  • IP地址:IP地址是某一台主机在互联网上的地址

  • 域名:和IP差不多意思,也是主机在互联网上的地址,人们一般记住的是域名,更有实质性意义。如果把IP比作经纬度,那么域名就是街道地址。

  • DNS:Domain Name System,负责提供域名和IP对应的查询服务,即域名解析系统;

  • URL:Uniform Resource Locator,统一资源定位符,其实就是我们说的‘网址’。


由此可以推断出,当我们敲下一行url地址时,浏览器首先做的是,先到DNS问问这个url的实际位置在哪里?


2. 发起请求



  • 协议:把数据打包成一种传输双方都看得懂的形式。

  • http:Hyper Text Transfer Protocol,超文本传输协议,通常运行在TCP之上。超文本就是用文本的方式来传输超越文本的内容,例如图片、音频等等。

  • TCP:Transmission Control Protocol,传输控制协议,是传输层的协议,用于保持通信的完整性。

  • 服务器:其实就是一台主机(电脑),目标资源可以存放在它的存储空间中。


拿到ip地址后,浏览器向目标地址发起http或者https协议的网络请求。

HTTP请求的本质是TCP/IP的请求构建。建立连接时需要进行3次握手进行验证,断开连接时需要4次挥手进行验证,保证传输的可靠性。


握手挥手过程有点复杂,具体的另出一篇文章说明!


3次握手


三次握手1.gif



  • 第一次握手:客户端发送网络包,服务端收到。

    服务端得出结论:客户端的发送能力、服务端的接收能力正常

  • 第二次握手:服务端发包,客户端收到。

    客户端得出结论:服务端的接收、发送能力正常,客户端的接收、发送能力正常。但是服务器并不能确认客户端接收能力是否正常。

  • 第三次握手:客户端发包,服务端收到。

    服务端得出结论:客户端的接收、发送能力正常,服务器的发送、接收能力正常。


三次握手.gif


4次挥手



  • 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号,此时客户端处于FIN_WAIT1状态。

    即发出连接释放报文段,并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1状态,等待服务端的确认。

  • 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,并把客户端的序列号值 +1 作为 ACK 的报文序号列值,表明已经收到客户端的报文,此时服务端处于 CLOSE_WAIT 状态。

  • 第三次挥手:服务器端关闭与客户端的连接,发送一个 FIN Seq=N 给客户端。

  • 第四次挥手:客户端返回 ACK 报文确认,确认序号 ACK 未收到的序号 N+1


四次挥手.gif


3. 服务器响应


服务器上运行的WEB服务软件根据http协议的规则,返回URL中指定的资源。

HTTP请求到达服务器,服务器进行对应的处理,最后要把数据传给浏览器,即返回网络响应。

和请求部分类似,网络响应具有三个部分:响应行、响应头和响应体,发起请求后,服务器会返回一个状态码,以示下一步的操作。


响应完成后TCP连接会断开吗


首先需要判断一下Connection字段,如果请求头或响应头中包含Connection: Keep-Alive,表示建立了持久连接,这样TCP会一直保持,之后请求统一站点的资源会复用这个连接;否则会断开TCP连接,响应流程结束。


状态码


状态码由3位数组成,第一个数字定义了响应的类别:



  1. 1xx:表示请求已接收,继续处理;

  2. 2xx:表示请求已被成功接收;

  3. 3xx:要想完成请求必须进行更进一步的操作;

  4. 4xx:客户端有误,请求有语法错误或请求无法实现;

  5. 5xx:服务器端错误,服务器未能实现合法的请求。


服务器返回相应文件


请求成功后,服务器会返回相应的网页,浏览器接收到响应成功的报文后会开始下载网页,至此,通信结束。


浏览器解析渲染页面


浏览器在接收到HTML/css/js文件之后是怎么将页面渲染在浏览器上的?

浏览器在拿到服务器返回的网页之后,首先会根据顶部定义的 DTD 类型进行对应的解析,解析过程将被交给内部的 GUI 渲染线程来处理。




  • 浏览器:浏览器是一个本地应用程序,它根据一定的标准引导页面加载和渲染页面

  • css:Cascading Style Sheets,层叠样式表,用于html中各元素样式显示

  • JS:JavaScript,在web应用中控制用户和网页应用的交互逻辑



参考文章



  1. 超详细讲解页面加载过程

  2. 网页加载过程简述


作者:在因斯坦
来源:juejin.cn/post/7174744415221579832
收起阅读 »

十分钟,让你学会Vue的这些巧妙冷技巧

web
前言 写了两年的Vue,期间学习到好几个提高开发效率和性能的技巧,现在把这些技巧用文章的形式总结下来。 1. 巧用$attrs和$listeners $attrs用于记录从父组件传入子组件的所有不被props捕获以及不是class与style的参数,而$lis...
继续阅读 »

前言


写了两年的Vue,期间学习到好几个提高开发效率和性能的技巧,现在把这些技巧用文章的形式总结下来。


1. 巧用$attrs$listeners


$attrs用于记录从父组件传入子组件的所有不被props捕获以及不是classstyle的参数,而$listeners用于记录从父组件传入的所有不含.native修饰器的事件。那下面的代码作例子:


Vue.component('child', {
props: ['title'],
template: '<h3>{{ title }}</h3>'
})

new Vue({
data:{a:1,title:'title'},
methods:{
handleClick(){
// ...
},
handleChange(){
// ...
}
},
template:'
<child class="child-width" :a="a" b="1" :title="title" @click.native="handleClick" @change="handleChange">'
,

})

则在<child/>在中



  • attrs的值为{a:1,b:"1"}

  • listeners的值为{change: handleChange}


通常我们可以用$attrs$listeners作组件通信,在二次封装组件中使用时比较高效,如:


Vue.component("custom-dialog", {
// 通过v-bind="$attrs"和v-on="$listeners"把父组件传入的参数和事件都注入到el-dialog实例上
template: '<el-dialog v-bind="$attrs" v-on="$listeners"></el-dialog>',
});

new Vue({
data: { visible: false },
// 这样子就可以像在el-dialog一样用visible控制custom-dialog的显示和消失
template: '<custom-dialog :visible.sync="visible">',
});

再举个例子:


Vue.component("custom-select", {
template: `<el-select v-bind="$attrs" v-on="$listeners">
<el-option value="选项1" label="黄金糕"/>
<el-option value="选项2" label="双皮奶"/>
</el-select>`
,
});

new Vue({
data: { value: "" },
// v-model在这里其实是v-bind:value和v-on:change的组合,
// 在custom-select里,通过v-bind="$attrs" v-on="$listeners"的注入,
// 把父组件上的value值双向绑定到custom-select组件里的el-select上,相当于<el-select v-model="value">
// 与此同时,在custom-select注入的size变量也会通过v-bind="$attrs"注入到el-select上,从而控制el-select的大小
template: '<custom-select v-model="value" size="small">',
});

2. 巧用$props


$porps用于记录从父组件传入子组件的所有被props捕获以及不是classstyle的参数。如


Vue.component('child', {
props: ['title'],
template: '<h3>{{ title }}</h3>'
})

new Vue({
data:{a:1,title:'title'},
methods:{
handleClick(){
// ...
},
handleChange(){
// ...
}
},
template:'
<child class="child-width" :a="a" b="1" :title="title">'
,

})

则在<child/>在中,$props的值为{title:'title'}$props可以用于自组件和孙组件定义的props都相同的情况,如:


Vue.component('grand-child', {
props: ['a','b'],
template: '<h3>{{ a + b}}</h3>'
})

// child和grand-child都需要用到来自父组件的a与b的值时,
// 在child中可以通过v-bind="$props"迅速把a与b注入到grand-child里
Vue.component('child', {
props: ['a','b'],
template: `
<div>
{{a}}加{{b}}的和是:
<grand-child v-bind="$props"/>
</div>
`

})

new Vue({
template:'
<child class="child-width" :a="1" :b="2">'
,
})

举一个我在开发中常用到$props的例子:


element-uiel-tabstab-click事件中,其回调参数是被选中的标签 tab 实例(也就是el-tab-pane实例),此时我想获取el-tab-pane实例上的属性如name值,可以有下面这种写法:


<template>
<div>
<el-tabs v-model="activeTab" @tab-click="getTabName">
<el-tab-pane name="123" label="123">123</el-tab-pane>
<el-tab-pane name="456" label="456">456</el-tab-pane>
</el-tabs>
</div>
</template>

<script>
export default {
name: "App",
data() {
return {
activeTab: "123",
};
},
methods: {
getTabName(tab) {
console.log(tab.$props.name);
},
},
};
</script>

此时效果如下所示,可看到控制台打印出我们点击的tab上的name属性值:


tab-prop.gif


示例代码放在sandbox里。


3. 妙用函数式组件


函数式组件相比于一般的vue组件而言,最大的区别是非响应式的。它不会监听任何数据,也没有实例(因此没有状态,意味着不存在诸如createdmounted的生命周期)。好处是因只是函数,故渲染开销也低很多。


把开头的例子改成函数式组件,代码如下:


<script>
export default {
name: "anchor-header",
functional: true, // 以functional:true声明该组件为函数式组件
props: {
level: Number,
name: String,
content: String,
},
// 对于函数式组件,render函数会额外传入一个context参数用来表示上下文,即替代this。函数式组件没有实例,故不存在this
render: function (createElement, context) {
const anchor = {
props: {
content: String,
name: String,
},
template: '<a :id="name" :href="`#${name}`"> {{content}}</a>',
};
const anchorEl = createElement(anchor, {
props: {
content: context.props.content, //通过context.props调用props传入的变量
name: context.props.name,
},
});
const el = createElement(`h${context.props.level}`, [anchorEl]);
return el;
},
};
</script>

4. 妙用 Vue.config.devtools


其实我们在生产环境下也可以调用vue-devtools去进行调试,只要更改Vue.config.devtools配置既可,如下所示:


// 务必在加载 Vue 之后,立即同步设置以下内容
// 该配置项在开发版本默认为 `true`,生产版本默认为 `false`
Vue.config.devtools = true;

我们可以通过检测cookie里的用户角色信息去决定是否开启该配置项,从而提高线上 bug 查找的便利性。


5. 妙用 methods


Vue中的method可以赋值为高阶函数返回的结果,例如:


<script>
import { debounce } from "lodash";

export default {
methods: {
search: debounce(async function (keyword) {
// ... 请求逻辑
}, 500),
},
};
</script>

上面的search函数赋值为debounce返回的结果, 也就是具有防抖功能的请求函数。这种方式可以避免我们在组件里要自己写一遍防抖逻辑。


这里有个例子sandbox,大家可以点进去看看经过高阶函数处理的method与原始method的效果区别,如下所示:


high-class-method.gif


除此之外,method还可以定义为生成器,如果我们有个函数需要执行时很强调顺序,而且需要在data里定义变量来记录上一次的状态,则可以考虑用生成器。


例如有个很常见的场景:微信的视频通话在接通的时候会显示计时器来记录通话时间,这个通话时间需要每秒更新一次,即获取通话时间的函数需要每秒执行一次,如果写成普通函数则需要在data里存放记录时间的变量。但如果用生成器则能很巧妙地解决,如下所示:


<template>
<div id="app">
<h3>{{ timeFormat }}</h3>
</div>
</template>

<script>
export default {
name: "App",
data() {
return {
// 用于显示时间的变量,是一个HH:MM:SS时间格式的字符串
timeFormat: "",
};
},
methods: {
genTime: function* () {
// 声明存储时、分、秒的变量
let hour = 0;
let minute = 0;
let second = 0;
while (true) {
// 递增秒
second += 1;
// 如果秒到60了,则分加1,秒清零
if (second === 60) {
second = 0;
minute += 1;
}
// 如果分到60了,则时加1,分清零
if (minute === 60) {
minute = 0;
hour += 1;
}
// 最后返回最新的时间字符串
yield `${hour}:${minute}:${second}`;
}
},
},
created() {
// 通过生成器生成迭代器
let gen = this.genTime();
// 设置计时器定时从迭代器获取最新的时间字符串
const timer = setInterval(() => {
this.timeFormat = gen.next().value;
}, 1000);
// 在组件销毁的时候清空定时器和迭代器以免发生内存泄漏
this.$once("hook:beforeDestroy", () => {
clearInterval(timer);
gen = null;
});
},
};
</script>

页面效果如下所示:


gen-method.gif


代码地址:code sandbox


但需要注意的是:method 不能是箭头函数



注意,不应该使用箭头函数来定义 method 函数 (例如 plus: () => this.a++)。理由是箭头函数绑定了父级作用域的上下文,所以 this 将不会按照期望指向 Vue 实例,this.a 将是 undefined



6. 妙用 watch 的数组格式


很多开发者会在watch中某一个变量的handler里调用多个操作,如下所示:


<script>
export default {
data() {
return {
value: "",
};
},
methods: {
fn1() {},
fn2() {},
},
watch: {
value: {
handler() {
fn1();
fn2();
},
immediate: true,
deep: true,
},
},
};
</script>

虽然fn1fn2都需要在value变动的时候调用,但两者的调用时机可能不同。fn1可能仅需要在deepfalse的配置下调用既可。因此,Vuewatch的值添加了Array类型来针对上面所说的情况,如果用watchArray的写法处理可以写成下面这种形式:


<script>
watch:{
'value':[
{
handler:function(){
fn1()
},
immediate:true
},
{
handler:function(){
fn2()
},
immediate:true,
deep:true
}
]
}
</script>

7. 妙用$options


$options是一个记录当前Vue组件的初始化属性选项。通常开发中,我们想把data里的某个值重置为初始化时候的值,可以像下面这么写:


this.value = this.$options.data().value;

这样子就可以在初始值由于需求需要更改时,只在data中更改即可。


这里再举一个场景:一个el-dialog中有一个el-form,我们要求每次打开el-dialog时都要重置el-form里的数据,则可以这么写:


<template>
<div>
<el-button @click="visible=!visible">打开弹窗</el-button>
<el-dialog @open="initForm" title="个人资料" :visible.sync="visible">
<el-form>
<el-form-item label="名称">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="form.gender">
<el-radio label="male"></el-radio>
<el-radio label="female"></el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>

<script>
export default {
name: "App",
data() {
return {
visible: false,
form: {
gender: "male",
name: "wayne",
},
};
},
methods: {
initForm() {
this.form = this.$options.data().form;
},
},
};
</script>

每次el-dialog打开之前都会调用其@open中的方法initForm,从而重置form值到初始值。如下效果所示:


option_data.gif


以上代码放在sanbox


如果要重置data里的所有值,可以像下面这么写:


Object.assign(this.$data, this.$options.data());
// 注意千万不要写成下面的样子,这样子就更改this.$data的指向。使得其指向另外的与组件脱离的状态
this.$data = this.$options.data();

8. 妙用 v-pre,v-once


v-pre


v-pre用于跳过被标记的元素以及其子元素的编译过程,如果一个元素自身及其自元素非常打,而又不带任何与Vue相关的响应式逻辑,那么可以用v-pre标记。标记后效果如下:


image.png


v-once



只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。



对于部分在首次渲染后就不会再有响应式变化的元素,可以用v-once属性去标记,如下:


<el-select>
<el-option
v-for="item in options"
v-once
:key="item.value"
:label="item.label"
:value="item.value"
>
{{i}}</el-option
>
</el-select>

如果上述例子中的变量options很大且不会再有响应式变化,那么如例子中用上v-once对性能有提升。


9. 妙用 hook 事件


如果想监听子组件的生命周期时,可以像下面例子中这么做:


<template>
<child @hook:mounted="removeLoading" />
</template>

这样的写法可以用于处理加载第三方的初始化过程稍漫长的子组件时,我们可以加loading动画,等到子组件加载完毕,到了mounted生命周期时,把loading动画移除。


初次之外hook还有一个常用的写法,在一个需要轮询更新数据的组件上,我们通常在created里开启定时器,然后在beforeDestroy上清除定时器。而通过hook,开启和销毁定时器的逻辑我们都可以在created里实现:


<script>
export default {
created() {
const timer = setInterval(() => {
// 更新逻辑
}, 1000);
// 通过$once和hook监听实例自身的beforeDestroy,触发该生命周期时清除定时器
this.$once("hook:beforeDestroy", () => {
clearInterval(timer);
});
},
};
</script>

像上面这种写法就保证了逻辑的统一,遵循了单一职责原则。


作者:村上小树
来源:juejin.cn/post/7103066172530098206
收起阅读 »

一点点Vue性能优化方案分享

web
我们在开发Vue项目时候都知道,在vue开发中某些问题如果前期忽略掉,当时不会出现明显的效果,但是越向后开发越难做,而且项目做久了就会出现问题,这就是所说的蝴蝶效应,这样后期的维护成本会非常高,并且项目上线后还会影响用户体验,也会出现加载慢等一系列的性能问题,...
继续阅读 »

我们在开发Vue项目时候都知道,在vue开发中某些问题如果前期忽略掉,当时不会出现明显的效果,但是越向后开发越难做,而且项目做久了就会出现问题,这就是所说的蝴蝶效应,这样后期的维护成本会非常高,并且项目上线后还会影响用户体验,也会出现加载慢等一系列的性能问题,下面举一个简单的例子。



举个简单的例子



如果加载项目的时候加载一张图片需要0.1s,其实算不了什么可以忽略不计,但是如果我有20张图片,这就是2s的时间, 2s的时间不算长一下就过去了,但是这仅仅的只是加载了图片,还有我们的js,css都需要加载,那就需要更长的时间,可能是5s,6s...,比如加载时间是5s,用户可能等都不会等,直接关闭我们的网站,最后导致我们网站流量很少,流量少就没人用,没人用就没有钱,没有钱就涨不了工资,涨不了工资最后就是跑路了😂。通过上面的例子可以看出性能问题是多么的重要甚至关系到了我们薪资😂,那如何避免这些问题呢?废话不多说,下面分享一下自己在写项目的时用到的一些优化方案以及注意事项。



1.不要将所有的数据都放在data中



可以将一些不被视图渲染的数据声明到实例外部然后在内部引用引用,因为Vue2初始化数据的时候会将data中的所有属性遍历通过Object.definePrototype重新定义所有属性;Vue3是通过Proxy去对数据包装,内部也会涉及到递归遍历,在属性比较多的情况下很耗费性能



<template>
<button @click="updateValue">{{msg}}</button>
</template>

<script>
let keys=true;
export default {
name:'Vueinput',
data(){
return {
msg:'true'
}
},
created(){
this.text = 'text'
},
methods:{
updateValue(){
keys = !keys
this.msg = keys?'true':'false'
}
}

}
</script>

2.watch 尽量不要使用deep:true深层遍历



因为watch不存在缓存,是指定监听对象,如果deep:true,并且监听对象类型情况下,会递归处理收集依赖,最后触发更新回调



3. vue 在 v-for 时给每项元素绑定事件需要用事件代理



vue源码中是通过addEventLisener去给dom绑定事件的,比如我们使用v-for需要渲染100条数据并且并为每个节点添加点击事件,如果每个都绑定事件那就存在很多的addEventLisener,这里不用说性能上肯定不好,那我们就需要使用事件代理处理这个问题



<template>
<ul @click="EventAgent">
<li v-for="(item) in mArr" :key="item.id" :data-set="item">{{item.day}}</li>
</ul>
</template>

<script>
let keys=true;
export default {
name:'Vueinput',
data(){
return {
mArr:[{
day:1,
id:'xx1'
},{
day:2,
id:'xx2'
},{
day:2,
id:'xx2'
},
...
]
}
},
methods:{
EventAgent(e){
// 注意这里 在项目中千万不要写的这么简单,我只是为了方便理解才这么写的
console.log(e.target.getAttribute('data-set'))
}
}

}
</script>

4. v-for尽量不要与v-if一同使用



vue的编译过程是template->vnode,看下面的例子



// 假设data中存在一个arr数组
<div id="container">
<div v-for="(item,index) in arr" v-if="arr.length" key="item.id">{{item}}</div>
</div>


上面的例子有可能大家经常这么做,其实这么做也能达到效果但是在性能上面不是很好,因为Ast在转化为render函数的时候会将每个遍历生成的对象都会加入if判断,最后在渲染的时候每次都每个遍历对象都会判断一次需要不需要渲染,这样就很浪费性能,为了避免这个问题我们把代码稍微改一下



<div id="container" v-if="arr.length"> 
<div v-for="(item,index) in arr" >{{item}}</div>
</div>


这样就只判断一次就能达到渲染效果了,是不是更好一些那



5. v-for的key进行不要以遍历索引作为key


<template>
<ul>
<li v-for="(item,index) in mArr" :key="item.id
<input type="checkbox" :value="item.is" />
</li>
<button @click="remove">
移除
</button>
</ul>
</template>

<script>
let keys=true;
export default {
name:'Vueinput',
data(){
return {
mArr:[{is:false,id:1},{is:false,id:2}
]
}
},
methods:{
remove(e){
console.log('asd')
this.mArr.shift()
}
}

}
</script>


  • 默认是都没有选中,当我们在页面中选中第一个


image.png



  • 点击下面的移除按钮,再看效果


image.png



  • 调整一下代码


<template>
<ul>
<li v-for="(item,index) in mArr" :key="index">
<input type="checkbox" :value="item.is" />
</li>
<button @click="remove">
移除
</button>
</ul>
</template>

<script>
let keys=true;
export default {
name:'Vueinput',
data(){
return {
mArr:[{is:false,id:1},{is:false,id:2}
]
}
},
methods:{
remove(e){
console.log('asd')
this.mArr.shift()
}
}

}
</script>


还是选中状态,就很神奇,解释一下为什么这么神奇,因为我们选中的是0索引,然后点击移除后索引为1的就变为0,Vue的更新策略是复用dom,也就是说我索引为1的dom是用的之前索引为0的dom并没有更改,当然没有key的情况也是如此,所以key值必须为唯一标识才会做更改



6. SPA 页面采用keep-alive缓存组件


<template>
<div>
<keep-alive>
<router-view></router-view>
</keep-alive>
</div>
</template>


使用了keep-alive之后我们页面不会卸载而是会缓存起来,keep-alive底层使用的LRU算法(淘汰缓存策略),当我们从其他页面回到初始页面的时候不会重新加载而是从缓存里获取,这样既减少Http请求也不会消耗过多的加载时间



7. 避免使用v-html




  1. 可能会导致xss攻击

  2. v-html更新的是元素的 innerHTML 。内容按普通 HTML 插入, 不会作为 Vue 模板进行编译 。



8. 提取公共代码,提取组件的 CSS



将组件中公共的方法和css样式分别提取到各自的公共模块下,当我们需要使用的时候在组件中使用就可以,大大减少了代码量



9. 首页白屏-loading



当我们第一次进入Vue项目的时候,会出现白屏的情况,为了避免这种尴尬的情况,我们在Vue编译之前使用加载动画避免



 
<!DOCTYPE html>
<html>
<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">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>Vue</title>
<style>

</style>
</head>
<body>
<noscript>
<strong>We're sorry but production-line doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app">
<div id="loading">
loading
</div>
</div>
<!-- built files will be auto injected -->
</body>
</html>



加loading只是解决白屏问题的一种,也可以缩短首屏加载时间,就需要在其他方面做优化,这个可以参考后面的案例



10. 拆分组件



主要目的就是提高复用性、增加代码的可维护性,减少不必要的渲染,对于如何写出高性能的组件这里就不展示了,自己可以多看看那些比较火的UI库(Element,Antd)的源码



11. 合理使用 v-if 当值为false时内部指令不会执行,具有阻断功能



如果操作不是很频繁可以使用v-if替代v-show,如果很频繁我们可以使用v-show来处理



key 保证唯一性 ( 默认 vue 会采用就地复用策略 )



上面的第五条已经讲过了,如果key不是唯一的情况下,视图可能不会更新。



12. 获取dom使用ref代替document.getElementsByClassName


mounted(){
console.log(document.getElementsByClassName(“app”))
console.log(this.$refs['app'])
}


document.getElementsByClassName获取dom节点的作用是一样的,但使用ref会减少获取dom节点的消耗



13. Object.freeze 冻结数据



首先说一下Object.freeze的作用




  • 不能添加新属性

  • 不能删除已有属性

  • 不能修改已有属性的值

  • 不能修改原型

  • 不能修改已有属性的可枚举性、可配置性、可写性


data(){
return:{
objs:{
name:'aaa'
}
}
},
mounted(){
this.objs = Object.freeze({name:'bbb'})
}


使用Object.freeze处理的data属性,不会被getter,setter,减少一部分消耗,但是Object.freeze也不能滥用,当我们需要一个非常长的字符串的时候推荐使用



14. 合理使用路由懒加载、异步组件



当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。然后当路由被访问的时候才加载对应组件,这样就更加高效了



// 未使用懒加载的路由
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "@/views/Home";
import About from "@/views/About";

Vue.use(VueRouter);

const routes = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/about",
name: "About",
component: About
}
];
// 使用懒加载
import Vue from "vue";
import VueRouter from "vue-router";
Vue.use(VueRouter);
const Home = () => import('../views/Home')
const About = () => import('../views/About')

const routes = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/about",
name: "About",
component: About
}
];

15. 数据持久化的问题



数据持久化比较常见的就是token了,作为用户的标识也作为登录的状态,我们需要将其储存到localStoragesessionStorage起来每次刷新页面Vuex从localStoragesessionStorage获取状态,不然每次刷新页面用户都需要重新登录,重新获取数据




  • localStorage 需要用户手动移除才能移除,不然永久存在。

  • sessionStorage 关闭浏览器窗口就失效。

  • cookie 关闭浏览器窗口就失效,每次请求Cookie都会被一同提交给服务器。


16. 防抖、节流



这两个算是老生常谈了,就不演示代码了,下面介绍一个场景,比如我们注册新用户的时候用户输入昵称需要校验昵称的合法性,考虑到用户输入的比较快或者修改频繁,这时候我们需要使用节流,间隔性的去校验,这样就减少了判断的次数达到优化的效果。后面我们还需要需要用户手动点击保存才能注册成功,为了避免用户频繁点击保存并发送请求,我们只监听用户最后一次的点击,这时候就用到了节流操作,这样就能达到优化效果



17. 重绘,回流



  • 触发重绘浏览器重新渲染部分或者全部文档的过程叫回流

    • 频繁操作元素的样式,对于静态页面,修改类名,样式

    • 使用能够触发重绘的属性(background,visibility,width,height,display等)



  • 触发回流浏览器回将新样式赋予给元素这个过程叫做重绘

    • 添加或者删除节点

    • 页面首页渲染

    • 浏览器的窗口发生变化

    • 内容变换





回流的性能消耗比重绘大,回流一定会触发重绘,重绘不一定会回流;回流会导致渲染树需要重新计算,开销比重绘大,所以我们要尽量避免回流的产生.



18. vue中的destroyed



组件销毁时候需要做的事情,比如当页面卸载的时候需要将页面中定时器清除,销毁绑定的监听事件



19. vue3中的异步组件



异步组件与下面的组件懒加载原理是类似,都是需要使用了再去加载



<template>
<logo-img />
<hello-world msg="Welcome to Your Vue.js App" />
</template>

<script setup>
import { defineAsyncComponent } from 'vue'
import LogoImg from './components/LogoImg.vue'

// 简单用法
const HelloWorld = defineAsyncComponent(() =>
import('./components/HelloWorld.vue'),
)
</script>


20. 组件懒加载


<template>
<div id="content">
<div>
<component v-bind:is='page'></component>
</div>

</div>
</template>

<script>

// ---* 1 使用标签属性is和import *---
const FirstComFirst = ()=>import("./FirstComFirst")
const FirstComSecond = ()=>import("./FirstComSecond")
const FirstComThird = ()=>import("./FirstComThird")

export default {
name: 'home',
components: {

},
data: function(){
return{
page: FirstComFirst
}
}

}
</script>


原理与路由懒加载一样的,只有需要的时候才会加载组件



21. 动态图片使用懒加载,静态图片使用精灵图



  • 动态图片参考图片懒加载插件 (github.com/hilongjw/vu…)

  • 静态图片,将多张图片放到一起,加载的时候节省时间


22. 第三方插件的按需引入



element-ui采用babel-plugin-component插件来实现按需导入



//安装插件
npm install babel-plugin-component -D
// 修改babel文件

module.exports = {
presets: [['@babel/preset-env', { modules: false }], '@vue/cli-plugin-babel/preset'],
plugins: [
'@babel/plugin-proposal-optional-chaining',
'lodash',
[
'component',
{
libraryName: 'element-ui',
styleLibraryName: 'theme-chalk'
},
'element-ui'
],
[
'component',
{
libraryName: '@xxxx',
camel2Dash: false
},
]
]
};

23. 第三方库CDN加速


//vue.config.js

let cdn = { css: [], js: [] };
//区分环境
const isDev = process.env.NODE_ENV === 'development';
let externals = {};
if (!isDev) {
externals = {
'vue': 'Vue',
'vue-router': 'VueRouter',
'ant-design-vue': 'antd',
}
cdn = {
css: [
'https://cdn.jsdelivr.net/npm/ant-design-vue@1.7.2/dist/antd.min.css', // 提前引入ant design vue样式
], // 放置css文件目录
js: [
'https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js', // vuejs
'https://cdn.jsdelivr.net/npm/vue-router@3.2.0/dist/vue-router.min.js',
'https://cdn.jsdelivr.net/npm/ant-design-vue@1.7.2/dist/antd.min.js'
]
}
}



module.exports = {
configureWebpack: {
// 排除打包的某些选项
externals: externals
},
chainWebpack: config => {
// 注入cdn的变量到index.html中
config.plugin('html').tap((arg) => {
arg[0].cdn = cdn
return arg
})
},

}
//index.html

<!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">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
<!-- 引入css-cdn的文件 -->
<% for(var css of htmlWebpackPlugin.options.cdn.css) { %>
<link rel="stylesheet" href="<%=css%>">
<% } %>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<!-- 放置js-cdn文件 -->
<% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%=js%>" ></script>
<% } %>
<div id="app"></div>
</body>
</html>

最后



以上的优化方案不紧在代码层面起到优化而且在性能上也起到了优化作用,文章内容主要是从Vue开发的角度和部分通过源码的角度去总结的,文章中如果存在错误的地方,或者你认为还有其他更好的方案,请大佬在评论区中指出,作者会及时更正,感谢!



作者:摸鱼的汤姆
来源:juejin.cn/post/7116163839644663822
收起阅读 »

10 个超棒的 JavaScript 简写技巧

web
今天我要分享的是10个超棒的JavaScript简写方法,可以加快开发速度,让你的开发工作事半功倍哦。 开始吧! 1.合并数组 普通写法: 我们通常使用Array中的concat()方法合并两个数组。用concat()方法来合并两个或多个数组,不会更改现有的数...
继续阅读 »

今天我要分享的是10个超棒的JavaScript简写方法,可以加快开发速度,让你的开发工作事半功倍哦。


开始吧!


1.合并数组


普通写法:


我们通常使用Array中的concat()方法合并两个数组。用concat()方法来合并两个或多个数组,不会更改现有的数组,而是返回一个新的数组。请看一个简单的例子:


let apples = ['🍎', '🍏'];
let fruits = ['🍉', '🍊', '🍇'].concat(apples);

console.log( fruits );
//=> ["🍉", "🍊", "🍇", "🍎", "🍏"]


简写写法:


我们可以通过使用ES6扩展运算符(...)来减少代码,如下所示:


let apples = ['🍎', '🍏'];
let fruits = ['🍉', '🍊', '🍇', ...apples]; // <-- here

console.log( fruits );
//=> ["🍉", "🍊", "🍇", "🍎", "🍏"]


2.合并数组(在开头位置)


普通写法:
假设我们想将apples数组中的所有项添加到Fruits数组的开头,而不是像上一个示例中那样放在末尾。我们可以使用Array.prototype.unshift()来做到这一点:


let apples = ['🍎', '🍏'];
let fruits = ['🥭', '🍌', '🍒'];

// Add all items from apples onto fruits at start
Array.prototype.unshift.apply(fruits, apples)

console.log( fruits );
//=> ["🍎", "🍏", "🥭", "🍌", "🍒"]


简写写法:


我们依然可以使用ES6扩展运算符(...)缩短这段长代码,如下所示:


let apples = ['🍎', '🍏'];
let fruits = [...apples, '🥭', '🍌', '🍒']; // <-- here

console.log( fruits );
//=> ["🍎", "🍏", "🥭", "🍌", "🍒"]


3.克隆数组


普通写法:


我们可以使用Array中的slice()方法轻松克隆数组,如下所示:


let fruits = ['🍉', '🍊', '🍇', '🍎'];
let cloneFruits = fruits.slice();

console.log( cloneFruits );
//=> ["🍉", "🍊", "🍇", "🍎"]


简写写法:


我们可以使用ES6扩展运算符(...)像这样克隆一个数组:


let fruits = ['🍉', '🍊', '🍇', '🍎'];
let cloneFruits = [...fruits]; // <-- here

console.log( cloneFruits );
//=> ["🍉", "🍊", "🍇", "🍎"]


4.解构赋值


普通写法:


在处理数组时,我们有时需要将数组“解包”成一堆变量,如下所示:


let apples = ['🍎', '🍏'];
let redApple = apples[0];
let greenApple = apples[1];

console.log( redApple ); //=> 🍎
console.log( greenApple ); //=> 🍏


简写写法:


我们可以通过解构赋值用一行代码实现相同的结果:


let apples = ['🍎', '🍏'];
let [redApple, greenApple] = apples; // <-- here

console.log( redApple ); //=> 🍎
console.log( greenApple ); //=> 🍏


5.模板字面量


普通写法:


通常,当我们必须向字符串添加表达式时,我们会这样做:


// Display name in between two strings
let name = 'Palash';
console.log('Hello, ' + name + '!');
//=> Hello, Palash!

// Add & Subtract two numbers
let num1 = 20;
let num2 = 10;
console.log('Sum = ' + (num1 + num2) + ' and Subtract = ' + (num1 - num2));
//=> Sum = 30 and Subtract = 10


简写写法:


通过模板字面量,我们可以使用反引号(``),这样我们就可以将表达式包装在${…}`中,然后嵌入到字符串,如下所示:


// Display name in between two strings
let name = 'Palash';
console.log(`Hello, ${name}!`); // <-- No need to use + var + anymore
//=> Hello, Palash!

// Add two numbers
let num1 = 20;
let num2 = 10;
console.log(`Sum = ${num1 + num2} and Subtract = ${num1 - num2}`);
//=> Sum = 30 and Subtract = 10


6.For循环


普通写法:


我们可以使用for循环像这样循环遍历一个数组:


let fruits = ['🍉', '🍊', '🍇', '🍎'];

// Loop through each fruit
for (let index = 0; index < fruits.length; index++) {
console.log( fruits[index] ); // <-- get the fruit at current index
}

//=> 🍉
//=> 🍊
//=> 🍇
//=> 🍎


简写写法:


我们可以使用for...of语句实现相同的结果,而代码要少得多,如下所示:


let fruits = ['🍉', '🍊', '🍇', '🍎'];

// Using for...of statement
for (let fruit of fruits) {
console.log( fruit );
}

//=> 🍉
//=> 🍊
//=> 🍇
//=> 🍎


7.箭头函数


普通写法:


要遍历数组,我们还可以使用Array中的forEach()方法。但是需要写很多代码,虽然比最常见的for循环要少,但仍然比for...of语句多一点:


let fruits = ['🍉', '🍊', '🍇', '🍎'];

// Using forEach method
fruits.forEach(function(fruit){
console.log( fruit );
});

//=> 🍉
//=> 🍊
//=> 🍇
//=> 🍎


简写写法:


但是使用箭头函数表达式,允许我们用一行编写完整的循环代码,如下所示:


let fruits = ['🍉', '🍊', '🍇', '🍎'];
fruits.forEach(fruit => console.log( fruit )); // <-- Magic ✨

//=> 🍉
//=> 🍊
//=> 🍇
//=> 🍎


8.在数组中查找对象


普通写法:


要通过其中一个属性从对象数组中查找对象的话,我们通常使用for循环:


let inventory = [  {name: 'Bananas', quantity: 5},  {name: 'Apples', quantity: 10},  {name: 'Grapes', quantity: 2}];

// Get the object with the name `Apples` inside the array
function getApples(arr, value) {
for (let index = 0; index < arr.length; index++) {

// Check the value of this object property `name` is same as 'Apples'
if (arr[index].name === 'Apples') { //=> 🍎

// A match was found, return this object
return arr[index];
}
}
}

let result = getApples(inventory);
console.log( result )
//=> { name: "Apples", quantity: 10 }


简写写法:


上面我们写了这么多代码来实现这个逻辑。但是使用Array中的find()方法和箭头函数=>,允许我们像这样一行搞定:


// Get the object with the name `Apples` inside the array
function getApples(arr, value) {
return arr.find(obj => obj.name === 'Apples'); // <-- here
}

let result = getApples(inventory);
console.log( result )
//=> { name: "Apples", quantity: 10 }


9.将字符串转换为整数


普通写法:


parseInt()函数用于解析字符串并返回整数:


let num = parseInt("10")

console.log( num ) //=> 10
console.log( typeof num ) //=> "number"


简写写法:


我们可以通过在字符串前添加+前缀来实现相同的结果,如下所示:


let num = +"10";

console.log( num ) //=> 10
console.log( typeof num ) //=> "number"
console.log( +"10" === 10 ) //=> true


10.短路求值


普通写法:


如果我们必须根据另一个值来设置一个值不是falsy值,一般会使用if-else语句,就像这样:


function getUserRole(role) {
let userRole;

// If role is not falsy value
// set `userRole` as passed `role` value
if (role) {
userRole = role;
} else {

// else set the `userRole` as USER
userRole = 'USER';
}

return userRole;
}

console.log( getUserRole() ) //=> "USER"
console.log( getUserRole('ADMIN') ) //=> "ADMIN"


简写写法:


但是使用短路求值(||),我们可以用一行代码执行此操作,如下所示:


function getUserRole(role) {
return role || 'USER'; // <-- here
}

console.log( getUserRole() ) //=> "USER"
console.log( getUserRole('ADMIN') ) //=> "ADMIN"


补充几点


箭头函数:


如果你不需要this上下文,则在使用箭头函数时代码还可以更短:


let fruits = ['🍉', '🍊', '🍇', '🍎'];
fruits.forEach(console.log);


在数组中查找对象:


你可以使用对象解构和箭头函数使代码更精简:


// Get the object with the name `Apples` inside the array
const getApples = array => array.find(({ name }) => name === "Apples");

let result = getApples(inventory);
console.log(result);
//=> { name: "Apples", quantity: 10 }


短路求值替代方案:


const getUserRole1 = (role = "USER") => role;
const getUserRole2 = role => role ?? "USER";
const getUserRole3 = role => role ? role : "USER";


编码习惯


最后我想说下编码习惯。代码规范比比皆是,但是很少有人严格遵守。究其原因,多是在代码规范制定之前,已经有自己的一套代码习惯,很难短时间改变自己的习惯。良好的编码习惯可以为后续的成长打好基础。下面,列举一下开发规范的几点好处,让大家明白代码规范的重要性:



  • 规范的代码可以促进团队合作。

  • 规范的代码可以减少 Bug 处理。

  • 规范的代码可以降低维护成本。

  • 规范的代码有助于代码审查。

  • 养成代码规范的习惯,有助于程序员自身的
    作者:AK_哒哒哒
    来源:juejin.cn/post/7105967944613494792
    成长。

收起阅读 »

每个前端都应该掌握的7个代码优化的小技巧

web
本文将介绍7种JavaScript的优化技巧,这些技巧可以帮助你更好的写出简洁优雅的代码。 1. 字符串的自动匹配(Array.includes) 在写代码时我们经常会遇到这样的需求,我们需要检查某个字符串是否是符合我们的规定的字符串之一。最常见的方法就是使用...
继续阅读 »

本文将介绍7种JavaScript的优化技巧,这些技巧可以帮助你更好的写出简洁优雅的代码。


1. 字符串的自动匹配(Array.includes


在写代码时我们经常会遇到这样的需求,我们需要检查某个字符串是否是符合我们的规定的字符串之一。最常见的方法就是使用||===去进行判断匹配。但是如果大量的使用这种判断方式,定然会使得我们的代码变得十分臃肿,写起来也是十分累。其实我们可以使用Array.includes来帮我们自动去匹配。


代码示例:


// 未优化前的写法
const isConform = (letter) => {
if (
letter === "a" ||
letter === "b" ||
letter === "c" ||
letter === "d" ||
letter === "e"
) {
return true;
}
return false;
};

// 优化后的写法
const isConform = (letter) =>
["a", "b", "c", "d", "e"].includes(letter);

2.for-offor-in自动遍历


for-offor-in,可以帮助我们自动遍历Arrayobject中的每一个元素,不需要我们手动跟更改索引来遍历元素。


注:我们更加推荐对象(object)使用for-in遍历,而数组(Array)使用for-of遍历


for-of


const arr = ['a',' b', 'c'];
// 未优化前的写法
for (let i = 0; i < arr.length; i++) {
const element = arr[i];
console.log(element);
}

// 优化后的写法
for (const element of arr) {
console.log(element);
}
// expected output: "a"
// expected output: "b"
// expected output: "c"

for-in


const obj = {
a: 1,
b: 2,
c: 3,
};
// 未优化前的写法
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const value = obj[key];
// ...
}

// 优化后的写法
for (const key in obj) {
const value = obj[key];
// ...
}

3.false判断


如果你想要判断一个变量是否为null、undefined、0、false、NaN、'',你就可以使用逻辑非(!)取反,来帮助我们来判断,而不用每一个值都用===来判断


// 未优化前的写法
const isFalsey = (value) => {
if (
value === null ||
value === undefined ||
value === 0 ||
value === false ||
value === NaN ||
value === ""
) {
return true;
}
return false;
};

// 优化后的写法
const isFalsey = (value) => !value;

4.三元运算符代替(if/else


在我们编写代码的时候肯定遇见过if/else选择结构,而三元运算符可以算是if/else的一种语法糖,能够更加简洁的表示if/else


// 未优化前的写法
let info;
if (value < minValue) {
info = "Value is最小值";
} else if (value > maxValue) {
info = "Value is最大值";
} else {
info = "Value 在最大与最小之间";
}

//优化后的写法
const info =
value < minValue
? "Value is最小值"
: value > maxValue ? "Value is最大值" : "在最大与最小之间";

5.函数调用的选择


三元运算符还可以帮我们判断当前情况下该应该调用哪一个函数,


function f1() {
// ...
}
function f2() {
// ...
}
// 未优化前的写法
if (condition) {
f1();
} else {
f2();
}

// 优化后的写法
(condition ? f1 : f2)();

6.用对象代替switch/case选择结构


switch case通常是有一个case值对应一个返回值,这样的结构就类似于我们的对象,也是一个键对应一个值。我们就可以用我们的对象代替我们的switch/case选择结构,使代码更加简洁


const dayNumber = new Date().getDay();

// 未优化前的写法
let day;
switch (dayNumber) {
case 0:
day = "Sunday";
break;
case 1:
day = "Monday";
break;
case 2:
day = "Tuesday";
break;
case 3:
day = "Wednesday";
break;
case 4:
day = "Thursday";
break;
case 5:
day = "Friday";
break;
case 6:
day = "Saturday";
}

// 优化后的写法
const days = {
0: "Sunday",
1: "Monday",
2: "Tuesday",
3: "Wednesday",
4: "Thursday",
5: "Friday",
6: "Saturday",
};
const day = days[dayNumber];

7. 逻辑或(||)的运用


如果我们要获取一个不确定是否存在的值时,我们经常会运用if判断先去判断值是否存在,再进行获取。如果不存在我们就会返回另一个值。我们可以运用逻辑或(||)的特性,去优化我们的代码


// 未优化前的写法
let name;
if (user?.name) {
name = user.name;
} else {
name = "Anonymous";
}

// 优化后的写法
const name = user?.name || "Anonymous";

写在最后


伙伴们,如果你觉得我写的文章对你有帮助就给zayyo点一个赞👍或者关注➕都是对我最大的支持。当然你也可以加我微信:IsZhangjianhao,邀你进我的前端学习交流群,一起学习前端,

作者:zayyo
来源:juejin.cn/post/7169420903888584711
成为更优秀的工程师~

收起阅读 »

使用Vue3 + AR撸猫,才叫好玩

web
先来个预告效果图开场: 前言:浏览苹果官网时,你会看到发现每个设备在介绍页底部有这么一行文字:“用增强现实看看***”。使用苹果设备点击之后就能将该设备投放于用户所在场景视界,在手机摄像头转动的时候,也能看到物体对象不同的角度,感觉就像真的有一台手机放在你...
继续阅读 »

先来个预告效果图开场


cat (1).gif



前言:浏览苹果官网时,你会看到发现每个设备在介绍页底部有这么一行文字:“用增强现实看看***”。使用苹果设备点击之后就能将该设备投放于用户所在场景视界,在手机摄像头转动的时候,也能看到物体对象不同的角度,感觉就像真的有一台手机放在你的面前。(效果如下图。注意:由于该技术采用苹果自有的arkit技术,安卓手机无法查看)



微信图片_20211105154040.png


聪明的你可能已经想到了,为什么只能用苹果手机才能查看,那有没有一种纯前端实现的通用的web AR技术呢?


纯前端解决方案


纯前端技术的实现可以用下图总结:


image.png


以JSARToolKit为例:



  • 使用WebRTC获取摄像头信息,然后在canvas画布上绘制原图;

  • JSARToolKit计算姿态矩阵,进而渲染虚拟信息


image.png


实现核心步骤


image.png



  • (识别)WebRTC获取摄像头视频流;

  • (跟踪)Tracking.js 、JSFeat 、ConvNetJS 、deeplearn.js 、keras.js ;

  • (渲染)A-Frame、 Three.js、 Pixi.js 、Babylon.js


比较成熟的框架:AR.js


好比每个领域都有对应的主流开发框架,Web AR领域比较成熟框架的就是AR.js,它在增强现实方面主要提供了如下三大功能:



  1. 图像追踪。当相机发现一幅2D图像时,可以在其上方或附近显示某些内容。内容可以是2D图像、gif、3D模型(也可以是动画)和2D视频。案例:艺术品、学习资料(书籍)、传单、广告等等。

  2. 基于位置的AR。这种“增强现实”技术利用了真实世界的位置,在用户的设备上显示增强现实的内容。开发者可以利用该库使用户获得基于现实世界位置的体验。用户可以随意走动(最好是在户外)并通过智能手机看到现实世界中任何地点的 AR 内容。若用户移动和旋转手机,AR内容也会同步做出反应(这样一些AR内容就被“固定”到真实位置了,且会根据它们与用户的距离做出适当的变化)。这样的解决方案让我们做出交互式旅游向导成为可能,比如游客来到一个新的城市,游览名胜古迹、博物馆、餐馆、酒店等等都会更方便。我们也可以改善学习体验,如寻宝游戏、生物或历史学习游戏等,还可以将该技术用于情景艺术(视觉艺术体验与特定的现实世界坐标相结合)。

  3. 标记跟踪。当相机发现一个标记时,可以显示一些内容(这与图像跟踪相同)。标记的稳定性不成问题,受限的是形状、颜色和尺寸。可以应用于需要大量不同标记和不同内容的体验,如:(增强书籍)、传单、广告等。


开始上手体验AR.js


开发调试开启https


由于使用到摄像头敏感权限,调试时必须基于https环境打开才能正常运行。如果是以往,自己手动搭建个https环境调试对于很多新手来说还是比较麻烦耗费时间,好在最新的基于vite+vue3的脚手架搭建的项目,可以轻松用一行命令开启https访问。用脚手架初始化好代码之后,先修改package.json文件,在dev命令中加上--host暴露网络请求地址(默认不开启),如下图:


image.png


接着用下面命令运行即可开启https:


npm run dev -- --https

image.png


先跑跑官方demo,看看效果


学习一门新框架或语言,最好的方式就是先将官方demo跑起来体验看看。


下面是官方代码展示的案例效果(注:该录制动图体积较大,请耐心等待加载)


案例一


案例二


wow~ 是不是感觉还蛮有意思的?接下来正式进入文章的主题,开始撸猫吧🐱


开始



前面有提到,AR.js基于三种方式展示内容,下面将使用基于图像追踪(Image Tracking) 方式实现。顾名思义,图像追踪就是基于一张图片,根据图片的特性点识别图片并跟踪展示AR内容,例如当摄像头捕捉到该图片时,就可以将要展示的内容悬浮于该图片上方展示。



引入依赖库


Ar.js从版本3开始采用了新的架构,使用jsartoolkit5进行跟踪定位,而渲染库有两种方式可选:A-Frame 或 Three.js。A-Frame方式就是通过html标签的方式简化创建场景素材,比如说展示一张图片,可以直接使用 <a-image></a-image>方式展示。


修改index.html文件:


先将vue代码注入注释掉


image.png
然后引入依赖:


<script src='./src/sdk/ar-frame/aframe-master.min.js'></script>
<script src='./src/sdk/ar-frame/aframe-ar-nft.js'></script>

撸猫姿势一:展示猫图片


<body>
<a-scene embedded arjs>
<a-assets>
<img id="my-image" src="./src/assets/cat.jpg">
</a-assets>
<a-marker preset="hiro">
<a-image rotation="90 0 0" src="#my-image"></a-image>
<!-- <a-box position="0 0.5 0" material="color: green;"></a-box> -->
</a-marker>
<a-entity camera></a-entity>
</a-scene>
</body>

简单解释下上面的代码:



  1. <a-scene>声明一个场景,你可以理解相当于一个body元素,里面嵌入其他标签元素;

  2. <a-marker>标签声明的是标识图片,也就是相机识别到标识图片时,做相应的处理;这里采用插件预设的hiro图片,下面效果动图可以看到

  3. 使用<a-assets>包裹使用到的素材,相当于声明引入素材,接着在<a-marker>中使用


看下效果:


喵


撸猫姿势二:播放视频


除了展示图片,还可以展示视频,先看效果:


cat (1).gif


代码如下:


  <a-scene vr-mode-ui="enabled: false;" renderer='antialias: true; alpha: true; precision: mediump;' embedded
arjs='trackingMethod: best; sourceType: webcam; debugUIEnabled: false;'>


<a-assets>
<video
src="https://ugcydzd.qq.com/uwMROfz2r57CIaQXGdGnC2ddPkb5Wzumid6GMsG9ELr7HeBy/szg_52471341_50001_d4615c1021084c03ad0df73ce2e898c8.f622.mp4?sdtfrom=v1010&guid=951847595ac28f04306b08161bb6d1f7&vkey=3A19FB37CFE7450C64A889F86411FC6CE939A42CCDAA6B177573BBCB3791A64C441EFF5B3298E3ED4E99FFA22231772796F5E8A1FCC33FE4CAC487680A326980FFCC5C56EB926E9B4D20E8740C913D1F7EBF59387012BEC78D2816B17079152BC19FCEF09976A248C4B24D3A5975B243614000CAA333F06D850034DA861B01DCA1D53B546120B74F%22"
preload="auto" id="vid" response-type="arraybuffer" loop crossorigin webkit-playsinline muted playsinline>

</video>
</a-assets>

<a-nft videohandler type='nft' url='./src/assets/dataNFT/pinball' smooth="true" smoothCount="10"
smoothTolerance="0.01" smoothThreshold="5">

<a-video src="#vid" position='50 150 -100' rotation='90 0 180' width='300' height='175'>
</a-video>
</a-nft>
<a-entity camera></a-entity>
</a-scene>


<script>
window.onload = function () {
AFRAME.registerComponent('videohandler', {
init: function () {
var marker = this.el;

this.vid = document.querySelector("#vid");

marker.addEventListener('markerFound', function () {
// 识别到标识图,开始播放视频
this.vid.play();
}.bind(this));

marker.addEventListener('markerLost', function () {
// 丢失标识图,停止播放视频
this.vid.pause();
this.vid.currentTime = 0;
}.bind(this));
}
});
};
</script>

🐱:喵~是不是感觉更酷更好玩了?


撸猫姿势三: 配合声网技术,与你家的猫隔空喊话



如果你是一位前端开发者,相信你一定知道阮一峰这个大佬。曾经在他的每周科技周刊看到这么一个有趣的事情:在亚马逊某片雨林里,安装了录音设备,实时将拾取到的鸟叫声传到一个网站,你可以打开该网站听到该片雨林里的实时鸟叫声,简单的说就是该网站可以听到该片雨林的”鸟叫直播 "。(可惜现在一时找不到该网站网址)
而作为工作党,爱猫人士的我们,可能有着上述同样的情感需求:要出差几天,家里的猫一时没法好好照顾,想要实时看到家里的爱猫咋办?



买台监控摄像头呗


当然是打开声网找到解决方案:视频通话
(这里为声网文档点个赞,整个产品的文档分类规划的特别清晰,不像某些云服务产品文档像是垃圾桶里翻东西)


image.png


使用vue3写法改造文档demo


先安装依赖包:


"agora-rtc-sdk-ng": "latest"

app.vue中代码:


<script setup>
import AgoraRTC from "agora-rtc-sdk-ng";
import { ref } from "vue";

const joinBtn = ref(null);
const leaveBtn = ref(null);

let rtc = {
localAudioTrack: null,
client: null,
};

let options = {
appId: "2e76ff53e8c349528b5d05783d53f53c",
channel: "test",
token:
"0062e76ff53e8c349528b5d05783d53f53cIADkwbufdA1BIXWsCZ1oFKLEfyPRrCbL3ECbUg71dsv8HQx+f9gAAAAAEAACwxdSy/6RYQEAAQDK/pFh",
uid: 123456,
};

rtc.client = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });

rtc.client.on("user-published", async (user, mediaType) => {
await rtc.client.subscribe(user, mediaType);
console.log("subscribe success");

if (mediaType === "video") {
const remoteVideoTrack = user.videoTrack;
const remotePlayerContainer = document.createElement("div");
remotePlayerContainer.id = user.uid.toString();
remotePlayerContainer.textContent = "Remote user " + user.uid.toString();
remotePlayerContainer.style.width = "640px";
remotePlayerContainer.style.height = "480px";
document.body.append(remotePlayerContainer);
remoteVideoTrack.play(remotePlayerContainer);
}

if (mediaType === "audio") {
const remoteAudioTrack = user.audioTrack;
remoteAudioTrack.play();
}

rtc.client.on("user-unpublished", (user) => {
const remotePlayerContainer = document.getElementById(user.uid);
remotePlayerContainer.remove();
});
});

// 加入通话
const handleJoin = async () => {
await rtc.client.join(
options.appId,
options.channel,
options.token,
options.uid
);
rtc.localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack();
rtc.localVideoTrack = await AgoraRTC.createCameraVideoTrack();
await rtc.client.publish([rtc.localAudioTrack, rtc.localVideoTrack]);
const localPlayerContainer = document.createElement("div");
localPlayerContainer.id = options.uid;
localPlayerContainer.textContent = "Local user " + options.uid;
localPlayerContainer.style.width = "640px";
localPlayerContainer.style.height = "480px";
document.body.append(localPlayerContainer);
rtc.localVideoTrack.play(localPlayerContainer);
console.log("publish success!");
};
// 离开通话
const handleLeave = async () => {
rtc.localAudioTrack.close();
rtc.localVideoTrack.close();
rtc.client.remoteUsers.forEach((user) => {
const playerContainer = document.getElementById(user.uid);
playerContainer && playerContainer.remove();
});
await rtc.client.leave();
};
</script>

<template>
<div>
<button ref="joinBtn" @click="handleJoin" type="button" id="join">
加入
</button>
<button ref="leaveBtn" @click="handleLeave" type="button" id="leave">
离开
</button>
</div>
</template>

<style></style>


跑起来效果:
image.png


这时就相当于在家安装了一个摄像头,如果我们需要远程查看,就可以通过声网官方提供的一个测试地址加入通话


手机打开上述网址,输入你的项目appId跟token,可以看到成功加入通话:


IMG_8973.PNG


111636875423_.pic_hd.jpg


下方图片是手机摄像头捕捉到的画面,原谅我用猫照片代替😂


让视频画面跑在AR.js画面中


这个由于个人时间关系,暂时就不研究实现。这里提供一个想法就是:
单纯的视频画面看起来有点单调,毕竟有可能猫并不在视频画面中出现,结合撸猫姿势一提到的展示图片,其实我们可以在ar场景中视频区域周围,布置照片墙或其他酷炫一点的subject,这样的话我们打开视频即使看不到猫星人,也可以看看它的照片之类的交互。


结束语


本文借征文活动,简单入手了解了下web AR相关知识,在这几天学习的过程中觉得还是蛮好玩的,此文也当抛砖引玉,希望更多开发者了解AR相关的知识。


AR在体验上真的很酷,未来值得期待。


最近几年苹果一直致力于推进AR技术体验并带来相关落地产品,例如为了配合提升AR体验,带来雷达扫描,空间音频功能。值得一提的是,今年的苹果秋季发布会,苹果的邀请函也是利用到了AR + 空间音频技术,即使你不是果粉,当你实际上手体验的时候,你依然会真正发自内心的感觉:wow~cool。可以点此视频观看了解。


而目前的Web AR技术相比于苹果自有的ARkit技术,在体验上还存在一些差距(如性能问题,识别不稳定),同时缺乏生态圈,希望Web AR技术在未来得到快速发展,毕竟web端跨平台通用特性,让人人的终端都可以跑起来才是实现AR场景大规模应用的前提。


Facebook押注的元宇宙概念中,其实也包含了AR技术,所以在元宇宙世界到来之前,AR技术值得我们每一个前端开发者关注学习。


彩蛋


如果你问我最喜欢什么猫,我会说--“房东的猫”,~哈哈哈🐱~


1511636712878_.pic_hd.jpg


参考资料


AR.js官网


AR.js中文翻译文档


跨平台移动Web AR的关键技术 介绍及应用


声网文档


作者:码克吐温
来源:juejin.cn/post/7030342557825499166
收起阅读 »

Kindle 可旋转桌面时钟

web
前言 自己的 Kindle 吃灰很久了,想做个时钟用,但是网上可选的时钟网站比较少,这些时钟网站里面,要么太简单 界面也比较丑陋,要么内容太多 有些本末倒置了,要么网址特别长 输入网址的时候太麻烦。 干脆自己写一个,没多少代码。 (我的 Kindle 差不多十...
继续阅读 »

前言


自己的 Kindle 吃灰很久了,想做个时钟用,但是网上可选的时钟网站比较少,这些时钟网站里面,要么太简单 界面也比较丑陋,要么内容太多 有些本末倒置了,要么网址特别长 输入网址的时候太麻烦。


干脆自己写一个,没多少代码。
(我的 Kindle 差不多十年前的了,系统比较旧,导致需要处理 Kindle 的浏览器兼容性,这里花了一些时间)


使用


image.png


可以通过以下两个网址中的任意一个访问:



Github 项目开源地址:https://github.com/lecepin/kindle-time


配置项


可以将以下参数添加到网址的 URL 中进行生效。



  • fs: 设置字体大小. 默认 7。

  • r: 设置屏幕渲染。默认不旋转。主要用在屏幕横向显示,可以通过设置 90 来实现旋转成横屏。

  • l: 设置语言。默认中文显示。可以设置为 en 实现英文显示。


这些配置项,可以单独使用,也可以一起使用。


例如:http://ktime.leping.fun/?fs=10&r=90


image.png


保持屏幕常亮


在 Kindle 主页面的顶部搜索中,输入 ~ds 回车一下,即可开启屏幕常亮功能。关闭的话,只能通过重启 Kindle 实现。


作者:lecepin
来源:juejin.cn/post/7188831785097101349
收起阅读 »

Vue项目打包优化

web
最近做完了一个项目,但是打包之后发现太大了,记录一下优化方案 Element、Vant 等组件库按需加载 静态资源使用cdn进行引入 开启gzip压缩 路由懒加载 #首先看看啥也没做时打包的大小 可以使用 webpack-bundle-analyzer 插...
继续阅读 »

最近做完了一个项目,但是打包之后发现太大了,记录一下优化方案



  • Element、Vant 等组件库按需加载

  • 静态资源使用cdn进行引入

  • 开启gzip压缩

  • 路由懒加载


#首先看看啥也没做时打包的大小


可以使用 webpack-bundle-analyzer 插件在打包时进行分析



 


可以看到有2.5M的大小,下面就进行优化


Element、Vant 等组件库按需加载


可以看到,在打包的文件中,占据最大比例的是这两个组件库,我们可以使用按需加载来减小 按需加载按照官方文档来就行,需要注意配置bebel.config.js


  // 在bebel.config.js的plugins选项中配置    
["component", { "libraryName": "element-ui", "styleLibraryName": "theme-chalk" } ],
["import", { "libraryName": "vant", "libraryDirectory": "es", "style": true }, 'vant']

配置后的大小,可以明显的看到有减小



静态资源使用cdn进行引入


接下来占比最大的就是一些可以通过cdn进行引入的静态资源了


设置例外


vue.config.js文件中进行设置例外,不进行打包


设置例外的时候 key:value 中key的值为你引入的库的名字,value值为这个库引入的时候挂在window上的属性


    config.externals({
"vue":'Vue',
"vue-wordcloud":'WordCloud',
"@wangeditor/editor-for-vue":"WangEditorForVue",
})

然后在项目的 public/index.html 文件中进行cdn引入


cdn的话有挺多种的,比如bootcdnjsdelivr等,推荐使用jsdelivrnpm官方就是使用的这个


    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.runtime.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-wordcloud@1.1.1/dist/word-cloud.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@wangeditor/editor-for-vue@1/dist/index.js"></script>

完成后



gzip压缩


开启gzip压缩需要使用插件,可以用compression-webpack-plugin


vue.config.js文件中进行配置


chainWebpack: config => {
config.optimization.minimize(true)
},
configureWebpack: config => {
if (process.env.NODE_ENV === 'production') {
return {
plugins: [
new CompressionPlugin({
test: /\.js$|\.css$|\.html$/,
algorithm: 'gzip',
threshold: 10240,
deleteOriginalAssets: false,
})
]
}
}
}

完成后



在配置完成后,在本地并不能开启使用,需要配置Nginx进行支持才行


路由懒加载


路由懒加载的基础是webpackMagic Comments 官方文档


    // 在路由配置中,通过下面这种方式导入对应组件
component: () => import(/* webpackChunkName: "mColumn" */ '../mviews/ColumnView.vue')

完成后


路由懒加载并不会减小文件打包后的大小,但是可以让文件分为不同的模块,当路由跳转时,才加载当前路由对应的组件


这样就可以大大减少首屏白屏的时间,不用在第一次进入的时候就加载全部的文件



如图,当第一次进入的时候只会加载到home.xx.js,当进入另一个路由的时候就会去加载对应组件,如图中的socket.xx.js


作者:羞男
来源:juejin.cn/post/7224036517139939386
收起阅读 »

Vue 必备的这些操作技巧

web
🎈 键盘事件 在 js 中我们通常通过绑定一个事件,去获取按键的编码,再通过 event 中的 keyCode 属性去获得编码 如果我们需要实现固定的键才能触发事件时就需要不断的判断,其实很麻烦 let button = document.querySel...
继续阅读 »

🎈 键盘事件



  • js 中我们通常通过绑定一个事件,去获取按键的编码,再通过 event 中的 keyCode 属性去获得编码

  • 如果我们需要实现固定的键才能触发事件时就需要不断的判断,其实很麻烦


let button = document.querySelector('button')

button.onkeyup = function (e) {
console.log(e.key)
if (e.keyCode == 13) {
console.log('我是回车键')
}
}


  • vue 中给一些常用的按键提供了别名,我们只要在事件后加上响应的别名即可

  • vue 中常见别名有:up/向上箭头down/向下箭头left/左箭头right/右箭头space/空格tab/换行esc/退出enter/回车delete/删除


// 只有按下回车键时才会执行 send 方法
<input v-on:keyup.enter="send" type="text">


  • 对于 Vue 中未提供别名的键,可以使用原始的 key 值去绑定,所谓 key 值就是 event.key 所获得的值

  • 如果 key 值是单个字母的话直接使用即可,如果是由多个单词组成的驼峰命名,就需要将其拆开,用 - 连接


// 只有按下q键时才会执行send方法
<input v-on:keyup.Q="send" type="text">

// 只有按下capslock键时才会执行send方法
<input v-on:keyup.caps-lock="send" type="text">


  • 对于系统修饰符 ctrlaltshift 这些比较复杂的键使用而言,分两种情况

  • 因为这些键可以在按住的同时,去按其他键,形成组合快捷键

  • 当触发事件为 keydown 时,我们可以直接按下修饰符即可触发

  • 当触发事件为 keyup 时,按下修饰键的同时要按下其他键,再释放其他键,事件才能被触发。


// keydown事件时按下alt键时就会执行send方法
<input v-on:keydown.Alt="send" type="text">

// keyup事件时需要同时按下组合键才会执行send方法
<input v-on:keyup.Alt.y="send" type="text">


  • 当然我们也可以自定义按键别名

  • 通过 Vue.config.keyCodes.自定义键名=键码 的方式去进行定义


// 只有按下回车键时才会执行send方法
<input v-on:keydown.autofelix="send" type="text">

// 13是回车键的键码,将他的别名定义为autofelix
Vue.config.keyCodes.autofelix=13

🎈 图片预览



  • 在项目中我们经常需要使用到图片预览,viewerjs 是一款非常炫酷的图片预览插件

  • 功能支持包括图片放大、缩小、旋转、拖拽、切换、拉伸等

  • 安装 viewerjs 扩展


npm install viewerjs --save


  • 引入并配置功能


//引入
import Vue from 'vue';
import 'viewerjs/dist/viewer.css';
import Viewer from 'v-viewer';

//按需引入
Vue.use(Viewer);

Viewer.setDefaults({
'inline': true,
'button': true, //右上角按钮
"navbar": true, //底部缩略图
"title": true, //当前图片标题
"toolbar": true, //底部工具栏
"tooltip": true, //显示缩放百分比
"movable": true, //是否可以移动
"zoomable": true, //是否可以缩放
"rotatable": true, //是否可旋转
"scalable": true, //是否可翻转
"transition": true, //使用 CSS3 过度
"fullscreen": true, //播放时是否全屏
"keyboard": true, //是否支持键盘
"url": "data-source",
ready: function (e) {
console.log(e.type, '组件以初始化');
},
show: function (e) {
console.log(e.type, '图片显示开始');
},
shown: function (e) {
console.log(e.type, '图片显示结束');
},
hide: function (e) {
console.log(e.type, '图片隐藏完成');
},
hidden: function (e) {
console.log(e.type, '图片隐藏结束');
},
view: function (e) {
console.log(e.type, '视图开始');
},
viewed: function (e) {
console.log(e.type, '视图结束');
// 索引为 1 的图片旋转20度
if (e.detail.index === 1) {
this.viewer.rotate(20);
}
},
zoom: function (e) {
console.log(e.type, '图片缩放开始');
},
zoomed: function (e) {
console.log(e.type, '图片缩放结束');
}
})


  • 使用图片预览插件

  • 单个图片使用


<template>
<div>
<viewer>
<img :src="cover" style="cursor: pointer;" height="80px">
</viewer>
</div>
</template>


<script>
export default {
data() {
return {
cover: "//www.autofelix.com/images/cover.png"
}
}
}
</script>



  • 多个图片使用


<template>
<div>
<viewer :images="imgList">
<img v-for="(imgSrc, index) in imgList" :key="index" :src="imgSrc" />
</viewer>
</div>

</template>

<script>
export default {
data() {
return {
imgList: [
"//www.autofelix.com/images/pic_1.png",
"//www.autofelix.com/images/pic_2.png",
"//www.autofelix.com/images/pic_3.png",
"//www.autofelix.com/images/pic_4.png",
"//www.autofelix.com/images/pic_5.png"
]
}
}
}
</script>


🎈 跑马灯



  • 这是一款好玩的特效技巧

  • 比如你在机场接人时,可以使用手机跑马灯特效,成为人群中最靓的仔

  • 跑马灯特效其实就是将最前面的文字删除,添加到最后一个,这样就形成了文字移动的效果


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>跑马灯</title>
<style type="text/css">
#app {
padding: 20px;
}
</style>
</head>

<body>
<div id="app">
<button @click="run">应援</button>
<button @click="stop">暂停</button>
<h3>{{ msg }}</h3>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.0/dist/vue.min.js"></script>
<script>
new Vue({
el: "#app",
data: {
msg: "飞兔小哥,飞兔小哥,我爱飞兔小哥~~~",
timer: null // 定时器
},
methods: {
run() {
// 如果timer已经赋值就返回
if (this.timer) return;

this.timer = setInterval(() => {
// msg分割为数组
var arr = this.msg.split('');
// shift删除并返回删除的那个,push添加到最后
// 把数组第一个元素放入到最后面
arr.push(arr.shift());
// arr.join('')吧数组连接为字符串复制给msg
this.msg = arr.join('');
}, 100)
},
stop() {
//清除定时器
clearInterval(this.timer);
//清除定时器之后,需要重新将定时器置为null
this.timer = null;
}
}
})
</script>

</html>

🎈 倒计时



  • 对于倒计时技巧,应用的地方很多

  • 比如很多抢购商品的时候,我们需要有一个倒计时提醒用户开抢时间

  • 其实就是每隔一秒钟,去重新计算一下时间,并赋值到 DOM


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>倒计时</title>
</head>

<body>
<div id="app">
<div>抢购开始时间:{{count}}</div>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.0/dist/vue.min.js"></script>
<script>
new Vue({
el: "#app",
data() {
return {
count: '', //倒计时
seconds: 864000 // 10天的秒数
}
},
mounted() {
this.Time() //调用定时器
},
methods: {
// 天 时 分 秒 格式化函数
countDown() {
let d = parseInt(this.seconds / (24 * 60 * 60))
d = d < 10 ? "0" + d : d
let h = parseInt(this.seconds / (60 * 60) % 24);
h = h < 10 ? "0" + h : h
let m = parseInt(this.seconds / 60 % 60);
m = m < 10 ? "0" + m : m
let s = parseInt(this.seconds % 60);
s = s < 10 ? "0" + s : s
this.count = d + '天' + h + '时' + m + '分' + s + '秒'
},
//定时器没过1秒参数减1
Time() {
setInterval(() => {
this.seconds -= 1
this.countDown()
}, 1000)
},
}
})
</script>

</html>

🎈 自定义右键菜单



  • 在项目中,我们有时候需要自定义鼠标右键出现的选项,而不是浏览器默认的右键选项

  • 对于如何实现右键菜单,在 Vue 中其实很简单,只要使用 vue-contextmenujs 插件即可

  • 安装 vue-contextmenujs 插件


npm install vue-contextmenujs


  • 引入


//引入
import Vue from 'vue';
import Contextmenu from "vue-contextmenujs"

Vue.use(Contextmenu);


  • 使用方法

  • 可以使用 <i class="icon"></i> 可以给选项添加图标

  • 可以使用 style 标签自定义选项的样式

  • 可以使用 disabled 属性禁止选项可以点击

  • 可以使用 divided:true 设置选项的下划线

  • 可以使用 children 设置子选项


<style>
.custom-class .menu_item__available:hover,
.custom-class .menu_item_expand {
background: lightblue !important;
color: #e65a65 !important;
}
</style>


<template>
<div style="width:100vw;height:100vh" @contextmenu.prevent="onContextmenu"></div>
</template>


<script>
import Vue from 'vue'
import Contextmenu from "vue-contextmenujs"
Vue.use(Contextmenu);

export default {
methods: {
onContextmenu(event) {
this.$contextmenu({
items: [
{
label: "返回",
onClick: () => {
// 添加点击事件后的自定义逻辑
}
},
{ label: "前进", disabled: true },
{ label: "重载", divided: true, icon: "el-icon-refresh" },
{ label: "打印", icon: "el-icon-printer" },
{
label: "翻译",
divided: true,
minWidth: 0,
children: [{ label: "翻译成中文" }, { label: "翻译成英文" }]
},
{
label: "截图",
minWidth: 0,
children: [
{
label: "截取部分",
onClick: () => {
// 添加点击事件后的自定义逻辑
}
},
{ label: "截取全屏" }
]
}
],
event, // 鼠标事件信息
customClass: "custom-class", // 自定义菜单 class
zIndex: 3, // 菜单样式 z-index
minWidth: 230 // 主菜单最小宽度
});
return false;
}
}
};
</script>


🎈 打印功能



  • 对于网页支持打印功能,在很多项目中也比较常见

  • 而 Vue 中使用打印功能,可以使用 vue-print-nb 插件

  • 安装 vue-print-nb 插件


npm install vue-print-nb --save


  • 引入打印服务


import Vue from 'vue'
import Print from 'vue-print-nb'
Vue.use(Print);


  • 使用

  • 使用 v-print 指令即可启动打印功能


<div id="printStart">
<p>红酥手,黄縢酒,满城春色宫墙柳。</p>
<p>东风恶,欢情薄。</p>
<p>一怀愁绪,几年离索。</p>
<p>错、错、错。</p>
<p>春如旧,人空瘦,泪痕红浥鲛绡透。</p>
<p>桃花落,闲池阁。</p>
<p>山盟虽在,锦书难托。</p>
<p>莫、莫、莫!</p>
</div>
<button v-print="'#printStart'">打印</button>

🎈 JSONP请求



  • jsonp解决跨域 的主要方式之一

  • 所以学会在 vue 中使用 jsonp 其实还是很重要的

  • 安装 jsonp 扩展


npm install vue-jsonp --save-dev


  • 注册服务


// 在vue2中注册服务
import Vue from 'vue'
import VueJsonp from 'vue-jsonp'
Vue.use(VueJsonp)

// 在vue3中注册服务
import { createApp } from 'vue'
import App from './App.vue'
import VueJsonp from 'vue-jsonp'
createApp(App).use(VueJsonp).mount('#app')


  • 使用方法

  • 需要注意的是,在使用 jsonp 请求数据后,回调并不是在 then 中执行

  • 而是在自定义的 callbackName 中执行,并且需要挂载到 window 对象上


<script>
export default {
data() {...},
created() {
this.getUserInfo()
},
mounted() {
window.jsonpCallback = (data) => {
// 返回后回调
console.log(data)
}
},
methods: {
getUserInfo() {
this.$jsonp(this.url, {
callbackQuery: "callbackParam",
callbackName: "jsonpCallback"
})
.then((json) => {
// 返回的jsonp数据不会放这里,而是在 window.jsonpCallback
console.log(json)
})
}
}
}
</script>


正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿


作者:极客飞兔
来源:juejin.cn/post/7121564385151025165

收起阅读 »

10个超级实用的Set、Map使用技巧

web
Set是一种类似于数组的数据结构,但是它的值是唯一的,即Set中的每个值只会出现一次。Set对象的实例可以用于存储任何类型的唯一值,从而使它们非常适用于去重。 Map是一种键值对集合,其中每个键都是唯一的,可以是任何类型,而值则可以是任何类型。Map对象的实例...
继续阅读 »

freysteinn-g-jonsson-s94zCnADcUs-unsplash.jpg


Set是一种类似于数组的数据结构,但是它的值是唯一的,即Set中的每个值只会出现一次。Set对象的实例可以用于存储任何类型的唯一值,从而使它们非常适用于去重。


Map是一种键值对集合,其中每个键都是唯一的,可以是任何类型,而值则可以是任何类型。Map对象的实例可以用于存储复杂的对象,并且可以根据键进行快速的查找和访问。


以下是Set和Map的一些常用方法:


Set:



  • new Set(): 创建一个新的Set对象

  • add(value): 向Set对象中添加一个新的值

  • delete(value): 从Set对象中删除一个值

  • has(value): 检查Set对象中是否存在指定的值

  • size: 获取Set对象中的值的数量

  • clear(): 从Set对象中删除所有值


Map:



  • new Map(): 创建一个新的Map对象

  • set(key, value): 向Map对象中添加一个键值对

  • get(key): 根据键获取Map对象中的值

  • delete(key): 从Map对象中删除一个键值对

  • has(key): 检查Map对象中是否存在指定的键

  • size: 获取Map对象中的键值对数量

  • clear(): 从Map对象中删除所有键值对


Set和Map是非常有用的数据结构,它们可以提高程序的性能和可读性,并且可以简化代码的编写。


Set


去重


使用 Set 可以轻松地进行数组去重操作,因为 Set 只能存储唯一的值。


const arr = [1, 2, 3, 1, 2, 4, 5];
const uniqueArr = [...new Set(arr)];
console.log(uniqueArr); // [1, 2, 3, 4, 5]

数组转换


可以使用 Set 将数组转换为不包含重复元素的 Set 对象,再使用 Array.from() 将其转换回数组。


const arr = [1, 2, 3, 1, 2, 4, 5];
const set = new Set(arr);
const uniqueArr = Array.from(set);
console.log(uniqueArr); // [1, 2, 3, 4, 5]

优化数据查找


使用 Set 存储数据时,查找操作的时间复杂度为 O(1),比数组的 O(n) 要快得多,因此可以使用 Set 来优化数据查找的效率。


const dataSet = new Set([1, 2, 3, 4, 5]);

if (dataSet.has(3)) {
console.log('数据已经存在');
} else {
console.log('数据不存在');
}

并集、交集、差集


Set数据结构可以用于计算两个集合的并集、交集和差集。以下是一些使用Set进行集合运算的示例代码:


const setA = new Set([1, 2, 3]);
const setB = new Set([2, 3, 4]);

// 并集
const union = new Set([...setA, ...setB]);
console.log(union); // Set {1, 2, 3, 4}

// 交集
const intersection = new Set([...setA].filter(x => setB.has(x)));
console.log(intersection); // Set {2, 3}

// 差集
const difference = new Set([...setA].filter(x => !setB.has(x)));
console.log(difference); // Set {1}

模糊搜索


Set 还可以通过正则表达式实现模糊搜索。可以将匹配结果保存到 Set 中,然后使用 Array.from() 方法将 Set 转换成数组。


const data = ['apple', 'banana', 'pear', 'orange'];

// 搜索以 "a" 开头的水果
const result = Array.from(new Set(data.filter(item => /^a/i.test(item))));
console.log(result); // ["apple"]

使用 Set 替代数组实现队列和栈


可以使用 Set 来模拟队列和栈的数据结构。


// 使用 Set 实现队列
const queue = new Set();
queue.add(1);
queue.add(2);
queue.add(3);
queue.delete(queue.values().next().value); // 删除第一个元素
console.log(queue); // Set(2) { 2, 3 }

// 使用 Set 实现栈
const stack = new Set();
stack.add(1);
stack.add(2);
stack.add(3);
stack.delete([...stack][stack.size - 1]); // 删除最后一个元素
console.log(stack); // Set(2) { 1, 2 }

Map


将 Map 转换为对象


const map = new Map().set('key1', 'value1').set('key2', 'value2');
const obj = Object.fromEntries(map);

将 Map 转换为数组


const map = new Map().set('key1', 'value1').set('key2', 'value2');
const array = Array.from(map);

记录数据的顺序


如果你需要记录添加元素的顺序,那么可以使用Map来解决这个问题。当你需要按照添加顺序迭代元素时,可以使用Map来保持元素的顺序。


const map = new Map();
map.set('a', 1);
map.set('b', 2);
map.set('c', 3);
map.set('d', 4);

for (const [key, value] of map) {
console.log(key, value);
}
// Output: a 1, b 2, c 3, d 4

统计数组中元素出现次数


可以使用 Map 统计数组中每个元素出现的次数。


const arr = [1, 2, 3, 1, 2, 4, 5];

const countMap = new Map();
arr.forEach(item => {
countMap.set(item, (countMap.get(item) || 0) + 1);
});

console.log(countMap.get(1)); // 2
console.log(countMap.get(2)); // 2
console.log(countMap.get(3)); // 1

统计字符出现次数


使用Map数据结构可以方便地统计字符串中每个字符出现的次数。


const str = 'hello world';
const charCountMap = new Map();
for (let char of str) {
charCountMap.set(char, (charCountMap.get(char) || 0) + 1);
}
console.log(charCountMap); // Map { 'h' => 1, 'e' => 1, 'l' => 3, 'o' => 2, ' ' => 1, 'w' => 1, 'r' => 1, 'd' => 1 }

缓存计算结果


在处理复杂的计算时,可能需要对中间结果进行缓存以提高性能。可以使用Map数据结构缓存计算结果,以避免重复计算。


const cache = new Map();
function fibonacci(n) {
if (n === 0 || n === 1) {
return n;
}
if (cache.has(n)) {
return cache.get(n);
}
const result = fibonacci(n - 1) + fibonacci(n - 2);
cache.set(n, result);
return result;
}
console.log(fibonacci(10)); // 55

使用 Map 进行数据的分组


const students = [
{ name: "Tom", grade: "A" },
{ name: "Jerry", grade: "B" },
{ name: "Kate", grade: "A" },
{ name: "Mike", grade: "C" },
];

const gradeMap = new Map();
students.forEach((student) => {
const grade = student.grade;
if (!gradeMap.has(grade)) {
gradeMap.set(grade, [student]);
} else {
gradeMap.get(grade).push(student);
}
});

console.log(gradeMap.get("A")); // [{ name: "Tom", grade: "A" }, { name: "Kate", grade: "A" }]


使用 Map 过滤符合条件的对象


在实际开发中,我们常常需要在一个对象数组中查找符合某些条件的对象。此时,我们可以结合使用 Map 和 filter 方法来实现。比如:


const users = [
{ name: 'Alice', age: 22 },
{ name: 'Bob', age: 18 },
{ name: 'Charlie', age: 25 }
];
const userMap = new Map(users.map(user => [user.name, user]));
const result = users.filter(user => userMap.has(user.name) && user.age > 20);
console.log(result); // [{ name: 'Alice', age: 22 }, { name: 'Charlie', age: 25 }]

首先,我们将对象数组转换为 Map,以便快速查找。然后,我们使用 filter 方法来过滤符合条件的对象。


这里我们列举了一些使用SetMap的实用技巧,它们可以大大简化你的代码,并使你更有效地处理数据。SetMap是JavaScript中非常有用的数据结构,值得我们在编写代码时好好利用。


系列文章



我的更多前端资讯


欢迎大家技术交流 资料分享 摸鱼 求助皆可 —链接


作者:shichuan
来源:juejin.cn/post/7225425984312328252
收起阅读 »

JS中的高阶函数

web
JavaScript中的高阶函数是指可以接受其他函数作为参数或者返回一个函数作为结果的函数。这种函数在函数式编程范式中特别常见,允许用一种更抽象、更灵活的方式处理代码。在JavaScript中,函数可以像其他数据类型一样被传递和操作。 具体来说,高阶函数可以...
继续阅读 »

JavaScript中的高阶函数是指可以接受其他函数作为参数或者返回一个函数作为结果的函数。这种函数在函数式编程范式中特别常见,允许用一种更抽象、更灵活的方式处理代码。在JavaScript中,函数可以像其他数据类型一样被传递和操作。



具体来说,高阶函数可以有以下几种形式:



  1. 接受函数作为参数的高阶函数


function map(array, fn) {
let result = [];
for (let i = 0; i < array.length; i++) {
result.push(fn(array[i]));
}
return result;
}

let numbers = [1, 2, 3, 4, 5];
let squaredNumbers = map(numbers, function(x) {
return x * x;
});
console.log(squaredNumbers); // [1, 4, 9, 16, 25]

在上面的例子中,map函数接受一个数组和一个函数作为参数,然后使用该函数对数组中的每个元素进行转换,并返回转换后的结果。




  1. 返回函数的高阶函数


function multiplyBy(n) {
return function(x) {
return x * n;
};
}

let double = multiplyBy(2);
let triple = multiplyBy(3);
console.log(double(10)); // 20
console.log(triple(10)); // 30

在上面的例子中,multiplyBy函数返回一个函数,该函数可以将传入的参数乘以n。我们可以使用multiplyBy函数创建一个新的函数,然后使用该函数对不同的值进行乘法运算。




  1. 同时接受和返回函数的高阶函数


function compose(f, g) {
return function(x) {
return f(g(x));
};
}

function square(x) {
return x * x;
}

function addOne(x) {
return x + 1;
}

let addOneThenSquare = compose(square, addOne);
console.log(addOneThenSquare(3)); // 16

在上面的例子中,compose函数接受两个函数作为参数,然后返回一个新的函数,该函数首先对输入值应用g函数,然后将结果传递给f函数,并返回f(g(x))的结果。我们可以使用compose函数创建一个新的函数,该函数可以将其他两个函数的功能组合在一起,以实现更复杂的操作。



其实,即使是业务代码中也会有很多用到高阶函数的地方,比如数组的迭代方法(map、filter、reduce等)、定时器(setTimeout和setInterval),还有比较典型的函数柯理化、函数组合(compose)、偏函数等,通过使用高阶函数,我们可以将常见的操作抽象出来,并将它们作为可重用的函数进行封装,从而使代码更加简洁、灵活和易于维护。





在使用高阶函数时,有时候需要注意回调函数中的上下文问题。如果回调函数中的this关键字不是指向我们期望的对象,就会导致程序出现错误。为了解决这个问题,可以使用bindapplycall等方法来明确指定回调函数的上下文。


let obj = {
value: 0,
increment: function() {
this.value++;
}
};

let arr = [1, 2, 3, 4, 5];

arr.forEach(obj.increment.bind(obj));
console.log(obj.value); // 5

在上面的例子中,obj.increment.bind(obj)会返回一个新函数,该函数会将this关键字绑定到obj对象上。我们可以使用这个新函数来作为forEach方法的回调函数,以确保increment方法的上下文指向obj对象。



其余还有诸如函数副作用问题、内存占用问题和性能问题等。为了解决这些问题,可以使用一些优化技巧,比如明确指定回调函数的上下文、使用纯函数、使用函数柯里化或函数组合等。这些技巧可以帮助我们更加灵活地使用高阶函数,并提高代

作者:施主来了
来源:juejin.cn/post/7232838211030302777
码的性能和可维护性。

收起阅读 »

函数实现单例模式

web
单例模式 一般在前端实现单例模式,大多数都会使用类去实现,因为类的实现,看起来比较简单,下面是一个简单的例子。 class Foo { static instance; static init() { if (!this.instance) t...
继续阅读 »

wallhaven-gpqye7.jpg


单例模式


一般在前端实现单例模式,大多数都会使用类去实现,因为类的实现,看起来比较简单,下面是一个简单的例子。


class Foo {
static instance;
static init() {
if (!this.instance) this.instance = new Foo();
return this.instance;
}
constructor() {}
}

// 将单例实例化 并暴露出去
export default Foo.init()


如此,我们就实现了简单的单例模式,并且在其他文件引入的时候已经是实例化过一次的了,或者交由用户者自行调用 init 也是可以的



函数实现


而在函数的实现上,其实本身类就是函数的某种抽象,如果去掉这个 new 的话,单纯用函数又是怎么做的呢?


let ipcMainInstance;
export default () => {
const init = () => {
return {
name: "phy",
hobby: "play games"
};
};

return () => {
if (!ipcMainInstance) {
ipcMainInstance = init();
}
return ipcMainInstance;
};
};

使用


const ipcInit = createIpc();
ipcInit();


因为我们使用的是二阶函数进行 init,所以写法上是二次调用才是 init,每个人的设计写法不一样。



然而这种写法上,每次都要写一个 init 方法进行单例实例化的包裹,这明显是一个重复工作,我们是否可以将 init 方法独立成一个函子,让他帮我们自动将我们传进去的函数进行处理,返回来的就是一个单例模式的函数呢?


抽象单例模式函子


// 非void返回值
type NonVoidReturn<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R extends void
? never
: T
: any;

/**
* 创建单例模式的函子
* @param {function} fn
* @returns {any} fn调用的返回值 必须得有return 可推断
*/

const createSgp = <T extends (...args: any) => any>(fn: NonVoidReturn<T>) => {
let _instance: undefined | ReturnType<T>;

return () => {
if (!_instance) {
_instance = fn();
}
return _instance;
};
};

export default createSgp;


使用上



import createSgp from "./createSgp";

const useAuto = () => {
let count = 0;

const setCount = (num: number) => {
count = num;
};

const getCount = () => count

return {
getCount,
setCount
};
};

// 将其处理成单例模式 并且暴露出去
export default createSgp(useAuto);


如此我们就完成了单例模式的包裹处理,并且是一个单例模式的函数。



对于hooks使用单例模式函数的问题


其实上面的操作看起来很酷,实际上很少会用到,因为你得考虑到,我用单例模式的意义是什么,如果这个函数只需要调用一次,那么就有必要用单例模式,但是hooks一般用到的时候,都属于操作性逻辑,尽量不应在hooks里面去做hooks初始化时有函数自执行调用,这个调用应该交由用户去做,我是这么理解hooks的,而这也就导致,hooks不应该用单例了,而且hooks用单例会有bug,请看下面的代码:


  let count = 0;
const useCount = {
count,
add(num){
count += num
}
}

这里我就一次简化useCount的return出来的东西,那么我们思考下,如果说,这个add在外部调用了,那么这个count会变吗?答案是不会,为什么呢?



因为当前add操作的count,是外部的count,并不是return对象的count,这句话可能很绕,但是仔细思考,一开始useCount(),他return的count是长什么样,此时,他其实就是数字0,那么,add改的count真的是这个return对象的count吗?相信说到这里,你就懂为什么了。



那我如果真的要联动到这个count,怎么做呢?


  const useCount = {
count: 0,
add(num){
this.count += num
}
}


答案是,用到this,此时这个add操作的count就是此时return 对象的count了,而这也跟类一个原理了,因为类更改的成员属性,都是实例对象本身的,而不是外部的,所以,他能更新上。这个问题,也是后面我发现的,所以以此记录一下。



作者:phy_lei
来源:juejin.cn/post/7232499216529834039
收起阅读 »

小程序轮播图的高度如何与图片高度保持一致

web
一、存在现象 在原生小程序中,我们从服务器获取轮播图的数据,这些图片的数据都是有一定宽高的,我们需要去适配这些图片在不同手机上显示时的宽高,不然的话,在不同的设备上就会不同的效果,也就出现了所谓的bug,如下案例: 这是在iPhone Xr上的显示效果...
继续阅读 »

一、存在现象




  • 在原生小程序中,我们从服务器获取轮播图的数据,这些图片的数据都是有一定宽高的,我们需要去适配这些图片在不同手机上显示时的宽高,不然的话,在不同的设备上就会不同的效果,也就出现了所谓的bug,如下案例:




  • 这是在iPhone Xr上的显示效果:轮播图的指示点显示正常
    image.png




  • 这是在iPhone 5上的显示效果:轮播图的指示点就到图片下方去了
    image.png




二、解决方法


思路



  • 在图片加载完成后,获取到图片的高度,获取到之后进行赋值。这样的话,我们需要使用image标签的bindload属性,当图片加载完成时触发


image.png



  • 获取图片高度,可以当做获取这个轮播图组件的高度,这组件是小程序界面上的一个节点,可以使用获取界面上的节点信息APIwx.createSelectorQuery()来获取


const query = wx.createSelectorQuery()
query.select('#the-id').boundingClientRect()
query.selectViewport().scrollOffset()
query.exec(function(res){
res[0].top // #the-id节点的上边界坐标
res[1].scrollTop // 显示区域的竖直滚动位置
})



  • 节点信息查询 API 可以用于获取节点属性、样式、在界面上的位置等信息。最常见的用法是使用这个接口来查询某个节点的当前位置,以及界面的滚动位置。如下图所示,里面有我们所需要的height,我们将这个height赋值给swiper组件,再令image标签mode="widthFix",即可自动适应轮播图高度和图片的高度保持一致



    • widthFix:缩放模式,宽度不变,高度自动变化,保持原图宽高比不变

    • HeightFix:缩放模式,高度不变,宽度自动变化,保持原图宽高比不变




  • 这是iPhone Xr上的数据,height:152.4375
    image.png




  • 这是iPhone 5上的数据,height:118.21875
    image.png




实现



  • wxml:轮播图


<swiper class="swiper" autoplay indicator-dots circular interval="{{4000}}" style="height: {{swiperHeight}}px;">
<block wx:for="{{banners}}" wx:key="bannerId">
<swiper-item class="swiper-item">
<image class="swiper-image" src="{{item.pic}}" mode="widthFix" bindload="getSwiperImageLoaded"></image>
</swiper-item>
</block>
</swiper>


  • js:只展示获取图片高度的代码,像获取轮播图数据代码已省略


Page({
data: {
swiperHeight: 0, // 轮播图组件初始高度
},

// 图片加载完成
getSwiperImageLoaded() {
// 获取图片高度
const query = wx.createSelectorQuery();
query.select(".swiper-image").boundingClientRect();
query.exec((res) => {
this.setData({ swiperHeight: rect.height });
});
},
})


  • 在上述代码中getSwiperImageLoaded方法也可以进行抽离到utils中成为一个工具函数,并用Promise进行返回,方便其他地方需要使用到


export default function (selector) {
return new Promise((resolve) => {
const query = wx.createSelectorQuery();
query.select(selector).boundingClientRect();
query.exec(resolve)
});
}


  • 所以在上述的实现代码中getSwiperImageLoaded方法可以进行如下的优化:


getSwiperImageLoaded() {
// 优化
queryRect(".swiper-image").then((res) => {
const rect = res[0];
this.setData({ swiperHeight: rect.height });
});
},


  • 如此一来,在iPhone 5上的轮播图组件展示也正常


image.png



  • 最后,因为获取的是轮播图,那么获取的数据就不止一条,按以上代码逻辑,获取到多少条数据就会执行多少遍setData赋值操作,所以可以考虑使用防抖或者节流进行进一步优化。


作者:晚风予星
来源:juejin.cn/post/7232625387296129080
收起阅读 »

CSS小技巧之圆形虚线边框

web
虚线相信大家日常都用的比较多,常见的用法就是使用 border-style 控制不同的样式,比如设置如下边框代码: border-style: dotted dashed solid double; 这将设置顶部的边框样式为点状,右边的边框样式为虚线,底部的...
继续阅读 »

虚线相信大家日常都用的比较多,常见的用法就是使用 border-style 控制不同的样式,比如设置如下边框代码:


border-style: dotted dashed solid double;

这将设置顶部的边框样式为点状,右边的边框样式为虚线,底部的边框样式为实线,左边的边框样式为双线。如下图所示:



border-style 除了上面所支持的样式还有 groove ridge inset outset 3D相关的样式设置,关于 border-style 的相关使用本文并不过多介绍,有兴趣的可以看官方文档。本文主要介绍使用CSS渐变实现更自定义化的虚线边框,以满足需求中的特殊场景使用。如封面图所示的6种情况足以体现足够自定义的边框样式,接下来看实现方式。


功能分析


基于封面图分析实现这类虚线边框应该满足一下几个功能配置:



  • 虚线的点数量

  • 虚线的颜色,可以纯色,多个颜色,渐变色

  • 虚线的粗细程度

  • 虚线点之间的间隔宽度


由于我们是自定义的虚线边框,所以尽可能不增加额外的元素,所以虚线的内容使用伪元素实现,然后使用定位覆盖在元素内容的上方,那么你肯定有疑问了,既然是覆盖在元素的上方,那不上遮挡了元素本身吗?



来到本文自定义圆形虚线边框的关键部分,这里我们使用CSS mask 实现,并配合使用 -webkit-mask-composite: source-in 显示元素本身的内容。



-webkit-mask-composite: 属性指定了将应用于一个元素的多个蒙版图像合成显示。当一个元素存在多重 mask 时,我们就可以运用 -webkit-mask-composite 进行效果叠加。



代码实现


首先基于上面分析的几个功能配置进行变量定义,方便后续更改变量值即可调整边框样式。


--n:20;   /* 控制虚线数量 */
--d:8deg; /* 控制虚线之间的距离 */
--t:5px; /* 控制虚线的粗细 */
--c:red; /* 控制虚线的颜色 */

对应不同的元素传入不同的值:


<div class="box" style="--n:3;--t:8px;--d:10deg;--c:linear-gradient(45deg,red,blue)">3</div>
<div class="
box" style="--n:6;--t:12px;--d:20deg;--c:green">6</div>

然后给伪元素设置基础的样式,定位,背景色,圆角等。


.box::after {
content: "";
position: absolute;
border-radius: 50%;
background: var(--c);
}

按不同的元素传入不同的背景色,最终的效果是这样的。



继续设置在mask中设置一个重复的锥形渐变 repeating-conic-gradient,代码如下:


repeating-conic-gradient(
from calc(var(--d)/2),
#000 0 calc(360deg/var(--n) - var(--d)),
#0000 0 calc(360deg/var(--n))
)



  • from calc(var(--d)/2) 定义了渐变的起点,以虚线之间的距离除以2可以让最终有对称的效果




  • #000 0 calc(360deg/var(--n) - var(--d)):定义了第一个颜色为黑色(#000),起点位置为0,终止位置为360deg/var(--n) - var(--d)度,基于虚线之间的距离和虚线的个数计算出每段虚线的渐变终止位置




  • #0000 0 calc(360deg/var(--n)):定义了第二个颜色为透明色,起点位置为0,终止位置为基于虚线的个数计算,这样与上一个颜色的差即是 --d 的距离,也就是我们控制虚线之间的距离。




基于上述代码现在的界面是如下效果:



上面有提到 -webkit-mask-composite 是应用于一个元素的多个蒙版图像合成显示,所以我们这里需要在mask中再增加一个蒙板进行合成最终的效果。


增加以下代码到mask中:


linear-gradient(#0000 0 0) content-box

注意这里使用了content-box作为背景盒模型,这意味着背景颜色只会应用到元素的内容区域,这段代码将创建一个只在元素内容区域的水平线性渐变背景,且是完全透明的背景色。


为什么是内容区域,因为这里和padding有关联,我们将定义的控制虚线的粗细 --t:5px; 应用到了伪元素的 padding 中。


padding: var(--t);

这样刚刚新增的透明背景就只会应用到下图的蓝色内容区域,再结合 -webkit-mask-composite,即``只剩下 padding 部分的内容,也就是我们的自定义边框部分。



增加以下代码:


-webkit-mask-composite: source-in;

即是最终的效果,因为这里增加的mask背景是透明色,这里 -webkit-mask-composite 的属性不限制使用 source-in, 其他的好几个都是一样的效果,有兴趣的可以了解了解。



都已经到这一步了,是不是应该再增加一些效果呢,给这个圆形的边框增加动起来的效果看看,增加一个简单的旋转动画 animation: rotate 5s linear infinite;,这样看着是不是更有感觉,适用的场景就多了。



码上掘金在线预览:



最后


到此整体代码实现就结束了,看完是不是感觉挺简单的,基于伪元素设置锥形渐变 repeating-conic-gradient并配合-webkit-mask-composite实现自定义圆形虚线边框的效果。这里是设置了 border-radius:50%; 圆角最终呈现的是圆形,有兴趣的可以更改CSS代码试试其他的形状颜色间距等。


看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~


专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)


参考



codepen.io/t_afif/pen/…



作者:南城FE
来源:juejin.cn/post/7233052510553522213
收起阅读 »

我竟然完美地用js实现默认的文本框粘贴事件

web
前言:本文实际是用js移动控制光标的位置!解决了网上没有可靠教程的现状 废话连篇 默认情况对一个文本框粘贴,应该会有这样的功能: 粘贴文本后,光标不会回到所有文本的最后位置,而是在粘贴的文本之后 将选中的文字替换成粘贴的文本 但是由于需求,我们需要拦截粘...
继续阅读 »

前言:本文实际是用js移动控制光标的位置!解决了网上没有可靠教程的现状



废话连篇


默认情况对一个文本框粘贴,应该会有这样的功能:



  1. 粘贴文本后,光标不会回到所有文本的最后位置,而是在粘贴的文本之后

  2. 将选中的文字替换成粘贴的文本


但是由于需求,我们需要拦截粘贴的事件,对剪贴板的文字进行过滤,这时候粘贴的功能都得自己实现了,而一旦自己实现,上面2个功能就不见了,我们就需要还原它。


面对这样的需求,我们肯定要控制移动光标,可是现在的网上环境真的是惨,千篇一律的没用代码...于是我就发表了这篇文章。


先上代码


    <textarea id="text" style="width: 996px; height: 423px;"></textarea>
<script>
// 监听输入框粘贴事件
document.getElementById('text').addEventListener('paste', function (e) {
e.preventDefault();
let clipboardData = e.clipboardData.getData('text');
// 这里写你对剪贴板的私货
let tc = document.querySelector("#text");
tc.focus();
const start = (tc.value.substring(0,tc.selectionStart)+clipboardData).length;
if(tc.selectionStart != tc.selectionEnd){
tc.value = tc.value.substring(0,tc.selectionStart)+clipboardData+tc.value.substring(tc.selectionEnd)
}else{
tc.value = tc.value.substring(0,tc.selectionStart)+clipboardData+tc.value.substring(tc.selectionStart);
}

// 重新设置光标位置
tc.selectionEnd =tc.selectionStart = start
});
</script>


怎么理解上述两个功能?
第一个解释:
比如说现在文本框有:



染念真的很生气



如果我们现在在真的后面粘贴不要,变成



染念真的不要很生气|



拦截后的光标是在生气后面,但是我们经常使用发现,光标应该出现在不要的后面吧!
就像这样:



染念真的不要|很生气



第2个解释:



染念真的不要很生气



我们全选真的的同时粘贴求你,拦截后会变成



染念真的求你不要很生气|



但默认应该是:



染念求你|不要很生气



代码分析


针对第2个问题,我们应该先要获取默认的光标位置在何处,tc.selectionStart是获取光标开始位置,tc.selectionEnd是获取光标结束位置。
为什么这里我写了一个判断呢?因为默认时候,我们没有选中一块区域,就是把光标人为移动到某个位置(读到这里,光标在位置后面,现在人为移动到就是前面,这个例子可以理解不?),这个时候两个值是相等的。



233|333


^--- ^


1-- - 4


tc.selectionEnd=4,tc.selectionStart = 4



如果相等,说明就是简单的定位,tc.value = tc.value.substring(0,tc.selectionStart)+clipboardData+tc.value.substring(tc.selectionStart); ,tc.value.substring(0,tc.selectionStart)获取光标前的内容,tc.value.substring(tc.selectionStart)是光标后的内容。
如果不相等,说明我们选中了一个区域(光标选中一块区域说明我们选中了一个区域),代码只需要在最后获取光标后的内容这的索引改成tc.selectionEnd



|233333|


^----- ^


1----- 7


tc.selectionEnd=7,tc.selectionStart = 1



在获取光标位置之前,我们应该先使用tc.focus();聚焦,使得光标回到文本框的默认位置(最后),这样才能获得位置。
针对第1个问题,我们就要把光标移动到粘贴的文本之后,我们需要计算位置。
获得这个位置,一定要在tc.value重新赋值之前,因为这样的索引都没有改动。
const start = (tc.value.substring(0,tc.selectionStart)+clipboardData).length;这个代码和上面解释重复,很简单,我就不解释了。
最后处理完了,重新设置光标位置,tc.selectionEnd =tc.selectionStart = start,一定让selectionEnd和selectionStart相同,不然选中一个区域了。


如果我们在value重新赋值之后获取(tc.value.substr(0,tc.selectionStart)+clipboardData).length,大家注意到没,我们操作的是tc.value,value已经变了,这里的重新定位光标开始已经没有任何意义了!



载于我的博客

收起阅读 »

不一样的深拷贝

web
对于深拷贝这个概念在面试中时常被提起,面试官可能让你实现深拷贝需要考虑那些因素,或者直接让你手写封装一个深拷贝,那么今天就和大家探讨一下一个让面试官感到牛逼的深拷贝, 1.思考 众所周知普通的数据类型是值存储,而复杂类型是通过开辟内存空间来存储数据的,我们通过...
继续阅读 »

对于深拷贝这个概念在面试中时常被提起,面试官可能让你实现深拷贝需要考虑那些因素,或者直接让你手写封装一个深拷贝,那么今天就和大家探讨一下一个让面试官感到牛逼的深拷贝,


1.思考


众所周知普通的数据类型是值存储,而复杂类型是通过开辟内存空间来存储数据的,我们通过内存地址从而查找数据,为了可以完全得到一个与原对象一模一样但又没有内存地址关联的深拷贝,我们需要考虑的因素其实有很多,
1.Object.create()创造的对象 Object.create()详细介绍


  let obj = Object.create(null)
obj.name = '张三'
obj.age = 22

这个对象是一个没有原型的对象,大部分对象都有自己的原型,可以使用公共的方法,但这个却不行,我们是不是应该把它考虑进去?


2.symbol作为属性名的情况 Symbol详细介绍 以及
for in 详细介绍


let obj = {
name: 'aa',
age: 22,
[Symbol('a')]: '独一无二的'
}

对于带有symbol的属性,在 for in 的迭代中是不可枚举的,我们是不是需要考虑如何解决?


3.对于修改对象的属性描述 Object.defineProperty()


let obj = { name: 'ayu', age: 22, sex: '男' }
Object.defineProperty(obj, 'age', {
enumerable: true,
configurable: true,
value: 22,
writable: false
})

这里我们改写了原对象的属性描述,age变得无法枚举,for in 也失去效果,并且很多默认的属性描述信息,我们是不是在拷贝后也应该和原对象保持一致?


4.对象的循环引用


let obj = { name: 'ayu', age: 22, sex: '男' }
obj.e = e

obj对象中有个e的属性指向obj,造成相互引用,当我们在封装深拷贝时,主要是通过递归来逐层查找属性值的情况,然后对其进行操作,如果出现这个情况,就会死循环递归造成栈内存溢出,这种情况难道也不值得考虑嘛?


5.一些特殊的对象
都说万物皆对象,对象其实有很多类型,正则,日期(Date),等都需要特殊处理
而函数和数组就比较简单


6.深拷贝的多数要点
也就是当一个对象里面嵌套了多层对象,这个大家应该都知道,我们通常一般使用递归去处理,再结合上面分析的因素就可以封装函数了


const isComplexDataType = (obj) => (typeof obj === 'object' || typeof obj === 'function') && obj !== null
const deepClone = function (obj, hash = new WeakMap()) {
if (obj.constructor === Date) return new Date(obj) // 日期对象直接返回一个新的日期对象
if (obj.constructor === RegExp) return new RegExp(obj) //正则对象直接返回一个新的正则对象
//如果循环引用了就用 weakMap 来解决
if (hash.has(obj)) return hash.get(obj)
let allDesc = Object.getOwnPropertyDescriptors(obj)
//遍历传入参数所有键的特性
let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
//继承原型链
hash.set(obj, cloneObj)
for (let key of Reflect.ownKeys(obj)) {
cloneObj[key] =
isComplexDataType(obj[key]) && typeof obj[key] !== 'function' ? deepClone(obj[key], hash) : obj[key]
}
return cloneObj
}

思路
从deepclone这个函数开始说起



  1. 1.如果对象的构造器是Date构造器,则我们使用Dte构造器再构造一个Date

  2. 如果对象的构造器是正则构造器再构造一个正则

  3. WeakMap我们先不提,allDesc是拿到原对象所有的属性(可枚举以及不可枚举)以及对应的属性描述信息

  4. cloneObj是我们根据第三步拷贝的一个新的对象的信息,不过是一个浅拷贝,而且我们考虑了原型不存在的情况 Object.assin与Object.create的区别

  5. 通过for of 循环 Reflect.ownKeys(obj) Reflect.ownKeys()用法 (Reflect.ownKeys()可以遍历对象自身所有的属性(symbol,不可枚举都可以),然后重新将obj的key以及对应的值赋值给cloneObj,并且对obj[key]的值做了讨论,当它是对象并且不是函数时,我们递归处理,否则里面为普通值,直接赋给ObjClone


对于deepClone的第二个参数WeakMap来讲, 请大家想想最开始我们提到的一个问题,我们有一个对象,然后我们填了了一个属性,属性为这个对象,这是在相互引用,如果我们处理这样的对象,也使用递归处理,那么就是死循环,因此我们需要一个数据结构来解决,每次我们递归处理的时候,都把obj,以及赋值的cloneobj对应存储,当遇到死循环的时候直接return这个对象即可
WeakMap详细介绍·


(本文用到大量ES5以后的API,推荐阅读阮一峰老师的ES6,这样才能理解的透彻)

作者:当然是黑猫警长啦
来源:juejin.cn/post/7120893997718962213

收起阅读 »

简单理解Vue的data为啥只能是函数

web
前言 在学习vue的时候vue2只有在组件中严格要求data必须是一个函数,而在普通vue实例中,data可以是一个对象,但是在vue3出现后data必须一个函数,当时看着官方文档说的是好像是对象的引用问题,但是内部原理却不是很了解,今天通过一个简单的例子来说...
继续阅读 »

前言


在学习vue的时候vue2只有在组件中严格要求data必须是一个函数,而在普通vue实例中,data可以是一个对象,但是在vue3出现后data必须一个函数,当时看着官方文档说的是好像是对象的引用问题,但是内部原理却不是很了解,今天通过一个简单的例子来说明为啥data必须是一个函数


参考 (vue2data描述)


参考: (vue3data描述)


1.Vue3中的data


const { createApp } = Vue
const app = {
data: {
a: 1
},
template: `

{{a}}


`

}
createApp(app).mount('#app')

image.png
可以看到上来vue就给了警告说明data必须是一个函数 下面直接抛错


2.vue中的data


var app = new Vue({
el: '#app',
data: { a: 'hello world' }
})


这种写法是可以的,前面提过普通实例data可以是对象,但是在组件中必须是函数,
那么在vue2中难道普通实例就没有缺陷嘛?

答案:是有缺陷的,
比如这样


<div id="app1">{{ message }}div>
<div id="app2">{{ message }}div>


const data = { message: 'hello world' }
const vue1 = new Vue({
el: '#app1',
data
})

const vue2 = new Vue({
el: '#app2',
data
})


这样在页面中会显示2个内容为hello world的div标签
那么当我们通过实例去改变messag呢?


 vue1.message = 'hello Vue'

image.png


奇怪的事情发生了,我知识改变了vue1的实例中的数据,但是其他实例的数据也发生了改变,相信很简单就能看出来这应该是共用同一个对象的引用而导致的,这在开放中是非常不友好的,开发者很容易就产生连串的错误,vue2也知道这种缺陷只是没有在普通实例中去体现而已,只在组件中实现了对于data的约束


为了让大家更好的立即为啥data必须是一个函数,黑猫在此简单实现一个vue的实例然后来证明为啥data是一个函数,以及如果data不是一个函数,我们应该如何处理


3.证明data是函数以及原理实现


在实现简单原理之前,我们需要搞清楚Vue在创建实例之前,对于data到底做了什么事情简单来说就是:


vue 在创建实例的过程中调用data函数返回实例对象通过响应式包装后存储在实例的data上并且实例可以直接越过data上并且实例可以直接越过data访问属性


1.通过这句描述可以知道Vue是一个构造函数,并且传入的参数中有一个data的属性,我们可以$data去访问,也可以直接访问这个属性,并且我们需要对这个data做代理

那么简单实现如下


function Vue(options) {
this.$data = proxy(options.data())
}
function proxy(options) {
return new Proxy(options, {
get(target, key, value, receiver) {
return Reflect.get(target, key, value, receiver)
},
set(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
}
})
}
const data = function () {
return {
a: 'hello world'
}
}
const vue1 = new Vue({
data
})
const vue2 = new Vue({
data
})
vue1.$data.a = 'hello Vue'
console.log(vue1.$data.a) // hello Vue
console.log(vue2.$data.a) // hello world

通过简单实现可与看出来,当我们的data是一个函数的时候,在Vue的构造函数中,只有有实例创建就有执行data函数,然后返回一个特别的对象,所以当我们修改其中一个实例的时候并不会对其他实例的数据产生变化

那么当data不是一个函数呢 ,我们简单改下代码,代码如下


function Vue(options) {
this.$data = proxy(options.data)
}
function proxy(options) {
return new Proxy(options, {
get(target, key, value, receiver) {
return Reflect.get(target, key, value, receiver)
},
set(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
}
})
}
const data = {
a: 'hello world'
}
const vue1 = new Vue({
data
})
const vue2 = new Vue({
data
})
vue1.$data.a = 'hello Vue'
console.log(vue1.$data.a) // hello Vue
console.log(vue2.$data.a) // hello Vue

可以看出,由于共用一个对象,当代理的时候也是对同一个对象进行代理,那么当我们通过一个实例去改变数据的时候,就会影响其他实例的状态


4.如果data必须是一个对象呢?


假如有人提出如果data是一个对象,那么我们应该如何处理呢,其实也非常简单,在代理的时候我们可以将传入的data对象通过深拷贝即可,这样我们就不会使用相同引用的对象啦。

[深拷贝牛逼封装参考我以前的文章](不一样的深拷贝)


作者:当然是黑猫警长啦
来源:juejin.cn/post/7154664015333949470
收起阅读 »

javascript实现动态分页

web
之前分页都是使用框架给出的分页类来实现分页,当然,体验可能不是那么好。 这次在写YII2.0框架的后台管理系统的小例子的时候,我这也尝试了一下前后分离,用ajax来实现分页跳转。 那么前端的页码绘制及跳页等其他的样式,都是由JavaScript根据后台返回的数...
继续阅读 »

之前分页都是使用框架给出的分页类来实现分页,当然,体验可能不是那么好。


这次在写YII2.0框架的后台管理系统的小例子的时候,我这也尝试了一下前后分离,用ajax来实现分页跳转。


那么前端的页码绘制及跳页等其他的样式,都是由JavaScript根据后台返回的数据拼接而成。我的分页效果如下图所示:






 


大概就是上面的样子。


Html代码如下:对照第一张图片


<ul> 
    <li><span>1<span data-id="1"></span></span></li>
    <li><a data-id="2">2</a></li>
    <li><a data-id="3">3</a></li>
    <li><a data-id="4">4</a></li>
    <li><a data-id="5">5</a></li>
    <li><a data-id="6">6</a></li>
    <li><a data-id="7">7</a></li>
    <li><a data-id="8">8</a></li>
    <li><a data-id="false"> ... </a></li>
    <li><a data-id="11"> 11 </a></li>
    <li><a data-id="next"> &gt;&gt; </a></li>
</ul>

JavaScript代码如下:


我这里使用的是纯JavaScript代码,没有使用jquery,这个是考虑到兼容性的问题。


/**
* @name 绘制分页
* @author camellia
* @date 20200703
* @param pageOptions 这是一个json对象
* @param pageTotal 总页数
* @param curPage 当前页数
* @param paginationId  显示分页代码的上层DOM的id
*/

 function dynamicPagingFunc(pageOptions)
 {
    // 总页数
    var pageTotal = pageOptions.pageTotal || 1;
    // 当前页
    var curPage = pageOptions.curPage || 1;
    // 获取页面DOM对象
    var paginationId = document.getElementById(''+pageOptions.paginationId+'') || document.getElementById('pagination');
    // 如果当前页 大于总页数  当前页为1
    if(curPage>pageTotal)
    {
       curPage =1;
    }
    var html = "<ul>  ";
    /*总页数小于5,全部显示*/
    if(pageTotal<=5)
    {
       html = appendItem(pageTotal,curPage,html);
       paginationId.innerHTML = html;
    }
    /*总页数大于5时,要分析当前页*/
    if(pageTotal>5)
    {
       if(curPage<=4)
       {
          html = appendItem(pageTotal,curPage,html);
          paginationId.innerHTML = html;
       }
       else if(curPage>4)
       {
          html = appendItem(pageTotal,curPage,html);
          paginationId.innerHTML = html;
       }
    }
    // 显示到页面上的html字符串
    // var html = "<ul>  ";
    // html = appendItem(pageTotal,curPage,html);
    html += "</ul>";
    // 显示至页面中
    paginationId.innerHTML = html;
 }
 
 /**
  * @name 绘制分页内部调用方法,根据不同页码来分析显示样式
* @author camellia
* @date 20200703
  * @param pageTotal 总页数
  * @param curPage 当前页
  * @param html 显示在页面上的html字符串
  */

 function appendItem(pageTotal,curPage,html)
 {
    // 显示页
    var showPage = 8;
    // 总页数大于XX页的时候,中间默认...
    var maxPage = 9;
    // 开始页
    var starPage = 0;
    // 结束页
    var endPage = 0;
    // 首先当前页不为1的时候显示上一页
    if(curPage != 1)
    {
       html += "<li><a data-id = 'prev' > << </a></li> ";
    }
    // 当总页数小于或等于最大显示页数时,首页是1,结束页是最大显示页
    if(pageTotal <= maxPage)
    {
       starPage = 1;
       endPage = pageTotal;
    }
    else if(pageTotal>maxPage && curPage<= showPage)
    {
       starPage = 1;
       endPage = showPage;
       if(curPage == showPage)
       {
          endPage = maxPage;
       }
    }
    else
    {
       if(pageTotal == curPage)
       {
          starPage = curPage - 3;
          endPage = curPage;
       }
       else
       {
          starPage = curPage - 2;
          endPage = Number(curPage) + 1;
       }
 
       html += "<li><a data-id = '1'> 1 </a></li> ";
       html += "<li><a data-id='false'> ... </a></li> ";
    }
    var i = 1;
    for(let i = starPage;i <= endPage;i++)
    {
       if(i==curPage)
       {
          html += "<li ><span>"+ i +"<span data-id="+i+"></span></span></li>";
       }
       else
       {
          html += "<li ><a data-id = "+ i +">"+i+"</a></li>";
       }
    }
 
 
    if(pageTotal<=maxPage)
    {
       if(pageTotal != curPage)
       {
          html += "<li><a data-id='next' > >> </a></li> ";
       }
    }
    else
    {
       if(curPage < pageTotal-2)
       {
          html += "<li><a data-id='false'> ... </a></li> ";
       }
       if(curPage <= pageTotal-2)
       {
          html += "<li><a data-id = "+pageTotal+" > "+pageTotal+" </a></li> ";
       }
       if(pageTotal != curPage)
       {
          html += "<li><a data-id = 'next' > >> </a></li> ";
       }
    }
    return html;
 }

 调用上边的分页代码:


// 绘制分页码
 var pageOptions = {'pageTotal':result.pageNumber,'curPage':result.page,paginationId:'pages'};
 dynamicPagingFunc(pageOptions);

我这里把分页的样式是引用的公共css中的文件,这里就不展示了,将你的分页html代码把我的代码替换掉就好。


参数的聚体解释以及函数中用到的参数,备注基本都已给出。


下面这部分是点击各个页码时,请求数据及重回页码的部分


/**
 * @name 分页点击方法,因为页面html是后生成的,所以需要使用ON方法进行绑定
* @author camellia
* @date 20200703
 */

 $(document).on('click''.next'function()
 {
     layer.load(0, {shadefalse});
     // 获取当前页码
     var obj = $(this).attr('data-id');
     // 获取前一页的页码,点击上一页以及下一页的时候使用
     var curpages = $("li .sr-only").attr('data-id');
     // 点击下一页的时候
     if(obj == 'next')
     {
         obj = Number(curpages) + 1;
     }
     else if(obj == 'prev')// 点击上一页的时候
     {
         obj = curpages - 1;
     }
     $.ajax({
         //几个参数需要注意一下
         type"POST",//方法类型
         dataType"json",//预期服务器返回的数据类型
         url"?r=xxx/xxx-xxx" ,//url
         data: {'page':obj},
         successfunction (result)
         {
             // 将列表部分的html清空
             document.getElementById('tbody').innerHTML = '';
             // 重新绘制数据列表
             drawPage(result.dbbacklist);
             // 绘制分页码
             var pageOptions = {'pageTotal':result.pageNumber,'curPage':result.page,paginationId:'pages'};
             dynamicPagingFunc(pageOptions);
             layer.closeAll();
         },
         error : function() {
             alert("异常!");
         }
     });
 });

有好的建议,请在下方输入你的评论。


欢迎访问个人博客:guanchao.site


欢迎访问我的小程序:打开微信->发现->小程序->搜索“时间里的”


作者:camellia
来源:juejin.cn/post/7111487878546341919
收起阅读 »

差两个像素让我很难受,这问题绝不允许留到明年!

web
2022年8月8日,linxiang07 同学给我们的 Vue DevUI 提了一个 Issue: #1199 Button/Search/Input/Select等支持设置size的组件标准不统一,并且认真梳理了现有支持size属性的组件列表和每个组件大中小...
继续阅读 »

2022年8月8日,linxiang07 同学给我们的 Vue DevUI 提了一个 Issue:
#1199 Button/Search/Input/Select等支持设置size的组件标准不统一,并且认真梳理了现有支持size属性的组件列表和每个组件大中小尺寸的现状,整理了一个表格(可以说是提 Issue 的典范,值得学习)。



不仅如此,linxiang 同学还提供了详细的修改建议:



  1. 建议xs、 sm 、md、lg使用标准的尺寸

  2. 建议这些将组件的尺寸使用公共的sass变量

  3. 建议参考社区主流的尺寸

  4. 考虑移除xs这个尺寸、或都都支持xs


作为一名对自己有要求的前端,差两个像素不能忍


如果业务只使用单个组件,可能看不太出问题,比如 Input 组件的尺寸如下:



  • sm 24px

  • md 26px

  • lg 44px



Select 组件的尺寸如下:



  • sm 22px

  • md 26px

  • lg 42px



当 Input 和 Select 组件单独使用时,可能看不出什么问题,但是一旦把他俩放一块儿,问题就出来了。



大家仔细一看,可以看出中间这个下拉框比两边输入框和按钮的高度都要小一点。


别跟我说你没看出来!作为一名资深的前端,像素眼应该早就该练就啦!


作为一名对自己严格要求的前端,必须 100% 还原设计稿,差两个像素怎么能忍!


vaebe: 表单 size 这个 已经很久了 争取不要留到23年


这时我们的 Maintainer 成员 vaebe 主动承担了该问题的修复工作(必须为 vaebe 同学点赞)。



看着只是一个 Issue,但其实这里面涉及的组件很多。


8月12日,vaebe 同学提了第一个修复该问题的 PR:


style(input): input组件的 size 大小


直到12月13日(今天)提交最后一个 PR:


cascader组件 props size 在表单内部时应该跟随表单变化


共持续5个月,累计提交34个PR,不仅完美地修复了这个组件尺寸不统一的问题,还完善了相关组件的单元测试,非常专业,必须再次给 vaebe 同学点赞。



关于 vaebe 同学


vaebe 同学是今年4月刚加入我们的开源社区的,一直有在社区持续作出贡献,修复了大量组件的缺陷,完善了组件文档,补充了单元测试,还为我们新增了 ButtonGroup 组件,是一位非常优秀和专业的开发者。



如果你也对开源感兴趣,欢迎加入我们的开源社区,添加小助手微信:opentiny-official,拉你进我们的技术交流群!


Vue DevUI:github.com/DevCloudFE/…(欢迎点亮 Star 🌟)


--- END ---


我是 Kagol,如果你喜欢我的文章,可以给我点个赞,关注我的掘金账号和公众号 Kagol,一起交流前端技术、一起做开源!


封面图来自B站UP主亿点点不一样的视频:吃毒蘑菇真的能见小人吗?耗时六个月拍下蘑菇的生长和繁殖


2.png


作者:Kagol
来源:juejin.cn/post/7176661549115768889
收起阅读 »

vue单页面应用部署配置

web
前端 Vue是一款非常流行的JavaScript框架,它提供了一套高效、灵活、易于使用的前端开发工具。在实际开发中,我们通常会使用Vue来构建单页面应用(SPA),并将其部署到服务器上以便用户访问。本篇博客将介绍如何进行Vue单页面应用的部署配置。 构建生产版...
继续阅读 »

前端


Vue是一款非常流行的JavaScript框架,它提供了一套高效、灵活、易于使用的前端开发工具。在实际开发中,我们通常会使用Vue来构建单页面应用(SPA),并将其部署到服务器上以便用户访问。本篇博客将介绍如何进行Vue单页面应用的部署配置。


构建生产版本


首先,我们需要将Vue应用程序构建为生产版本,这可以通过运行以下命令来完成:


npm run build

该命令将生成一个dist目录,其中包含了生产版本的所有必要文件,例如HTML、CSS、JavaScript等。在部署之前,我们需要将这些文件上传到服务器上,并将其存储在合适的位置。


配置Nginx服务器


接下来,我们需要将Vue应用程序与Nginx服务器结合起来,以便处理HTTP请求和响应。下面是一个简单的配置示例:


server {
listen 80;
server_name example.com;

root /var/www/vue-app/dist;
index index.html;

location / {
try_files $uri $uri/ /index.html;
}
}

在上面的示例中,我们定义了一个名为“example.com”的虚拟主机,并指定了根目录即Vue应用程序所在的dist目录。同时,我们还设置了默认的index.html文件,并通过location指令来处理所有的HTTP请求。


配置HTTPS加密连接


如果需要启用HTTPS加密连接,我们可以通过以下方式来进行配置:


server {
listen 443 ssl;
server_name example.com;

root /var/www/vue-app/dist;
index index.html;

ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;

location / {
try_files $uri $uri/ /index.html;
}
}

在上面的示例中,我们使用ssl指令来启用SSL/TLS支持,并设置了证书和私钥文件的路径。同时,我们还将所有HTTP请求重定向到HTTPS连接,以确保数据传输的安全性。


配置缓存和压缩


为了提高Vue应用程序的性能和响应速度,我们可以配置缓存和压缩。下面是一个简单的配置示例:


server {
listen 80;
server_name example.com;

root /var/www/vue-app/dist;
index index.html;

location / {
try_files $uri $uri/ /index.html;

expires 1d;
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}
}


在上面的示例中,我们使用expires指令来定义缓存时间,并使用gzip指令来启用Gzip压缩。同时,我们还设置了需要进行压缩的文件类型,例如文本、CSS、JavaScript等。


总结


以上就是Vue单页面应用的部署配置步骤。首先,我们需要构建生产版本,并将其上传到服务器上。然后,我们需要通过Nginx服务器来处理HTTP请求和响应,以及启用HTTPS加密连接、缓存和压缩等功能。了解这些配置信息,将有助于我们更好地部署和管理

作者:爱划水de鲸鱼哥
来源:juejin.cn/post/7222651312072802359
Vue单页面应用程序

收起阅读 »

css卡片悬停

web
前言 今天分享一个简单的卡片鼠标悬停动画,初始显示一张图片,当鼠标移至卡片上方时,显示文字,先来看看预览效果: 代码实现 页面布局 <div class="view view-first"> <img src="./images...
继续阅读 »

前言


今天分享一个简单的卡片鼠标悬停动画,初始显示一张图片,当鼠标移至卡片上方时,显示文字,先来看看预览效果:


1.gif

代码实现


页面布局


<div class="view view-first">  
<img src="./images/1.webp" />
<div class="mask">
<h2>Title</h2>
<p>Your Text</p>
<a href="#" class="info">Read More</a>
</div>

</div>

这段代码了一个用于展示图片的容器 <div> 元素,其中包含了一个图片 <img> 元素和一个用于显示图片标题、文字和链接的 <div> 元素。这个容器使用了类名为 viewview-first 的 CSS 类来进行样式控制。


页面样式


.view {
width: 1080px;
height: 1430px;
margin: 10px auto;
border: 10px solid red;
overflow: hidden;
position: relative;
text-align: center;
box-shadow: 1px 1px 2px #e6e6e6;
cursor: pointer;
}
.view .mask, .view .content {
width: 1080px;
height: 1430px;
position: absolute;
overflow: hidden;
top: 0;
left: 0
}
.view h2 {
text-transform: uppercase;
color: #fff;
text-align: center;
font-size: 180px;
padding: 10px;
background: rgba(0, 0, 0, 0.6);
margin: 220px 0 0 0
}
.view p {
font-family: Georgia, serif;
font-style: italic;
font-size: 120px;
color: #fff;
padding: 10px 20px 20px;
text-align: center
}
.view a.info {
display: inline-block;
text-decoration: none;
padding: 7px 14px;
font-size: 60px;
background: #000;
color: #fff;
text-transform: uppercase;
box-shadow: 0 0 1px #000
}
.view a.info:hover {
box-shadow: 0 0 5px #000
}


.view-first img {
transition: all 0.2s linear;
}
.view-first .mask {
opacity: 0;
background-color: rgba(219,127,8, 0.7);
transition: all 0.4s ease-in-out;
}
.view-first h2 {
transform: translateY(-100px);
opacity: 0;
transition: all 0.2s ease-in-out;
}
.view-first p {
transform: translateY(100px);
opacity: 0;
transition: all 0.2s linear;
}
.view-first a.info{
opacity: 0;
transition: all 0.2s ease-in-out;
}

.view-first:hover img {
transform: scale(1.2);
}
.view-first:hover .mask {
opacity: 0.8;
}
.view-first:hover h2,
.view-first:hover p,
.view-first:hover a.info {
opacity: 1;
transform: translateY(0px);
}
.view-first:hover p {
transition-delay: 0.1s;
}
.view-first:hover a.info {
transition-delay: 0.2s;
}

这段 CSS 代码定义了 .view.view-first 这两个类的样式属性。其中,.view 类定义了容器的基本样式,包括宽度、高度、边距、背景颜色、阴影等。.view-first 类定义了容器在鼠标悬停时的效果,包括图片放大、遮罩层透明度变化、标题、文字和链接的透明度和位置变化等。这段代码通过使用伪类 :hover 来控制在鼠标悬停时的效果。同时,这段 CSS 代码中包含了一些过渡效果(transition),通过设置不同的过渡时间和延迟时间,实现了在鼠标悬停时的平滑动画效果。同时,通过使用透明度(opacity)、位移(transform: translateY())和缩放(transform: scale())等属性,实现了图片和文字的渐现和渐变效果。接下来对各个样式进行详细解释:


.view {
width: 1080px;
height: 1430px;
margin: 10px auto;
border: 10px solid red;
overflow: hidden;
position: relative;
text-align: center;
box-shadow: 1px 1px 2px #e6e6e6;
cursor: pointer;
}

设置容器元素的宽度和高度,margin: 10px auto;设置容器元素的外边距,使其在水平方向上居中,上下边距为 10 像素,text-align: center;文本的水平对齐方式为居中,box-shadow: 1px 1px 2px #e6e6e6;设置容器元素的阴影效果,水平和垂直偏移都为 1 像素,模糊半径为 2 像素,阴影颜色为 #e6e6e6。cursor: pointer;设置鼠标悬停在容器元素上时的光标样式为手型。


.view .mask, .view .content {
width: 1080px;
height: 1430px;
position: absolute;
overflow: hidden;
top: 0;
left: 0
}

选中类名为 "mask" 和 "content" 的元素,采用绝对定位,设置topleft偏移量为0。


.view h2 {
text-transform: uppercase;
color: #fff;
text-align: center;
font-size: 180px;
padding: 10px;
background: rgba(0, 0, 0, 0.6);
margin: 220px 0 0 0
}

对字体颜色和大小进行设置,文字水平居中,设置背景色等,text-transform: uppercase;设置标题文本转换为大写。


.view p {
font-family: Georgia, serif;
font-style: italic;
font-size: 120px;
color: #fff;
padding: 10px 20px 20px;
text-align: center
}
.view a.info {
display: inline-block;
text-decoration: none;
padding: 7px 14px;
font-size: 60px;
background: #000;
color: #fff;
text-transform: uppercase;
box-shadow: 0 0 1px #000
}
.view a.info:hover {
box-shadow: 0 0 5px #000
}

对子元素p标签和指定a标签进行字体样式进行设置,text-decoration: none;去除下划线,a元素在鼠标悬停状态下的添加阴影。


.view-first img { 
transition: all 0.2s linear;
}
.view-first .mask {
opacity: 0;
background-color: rgba(219,127,8, 0.7);
transition: all 0.4s ease-in-out;
}
.view-first h2 {
transform: translateY(-100px);
opacity: 0;
transition: all 0.2s ease-in-out;
}
.view-first p {
transform: translateY(100px);
opacity: 0;
transition: all 0.2s linear;
}
.view-first a.info{
opacity: 0;
transition: all 0.2s ease-in-out;
}

.view-first:hover img {
transform: scale(1.2);
}
.view-first:hover .mask {
opacity: 0.8;
}
.view-first:hover h2,
.view-first:hover p,
.view-first:hover a.info {
opacity: 1;
transform: translateY(0px);
}
.view-first:hover p {
transition-delay: 0.1s;
}
.view-first:hover a.info {
transition-delay: 0.2s;
}

对各元素在鼠标悬停状态下的样式进行设置,并添加动画效果,主要动画元素transform: scale(1.2);图片在悬停状态下缩放1.2倍,transform: translateY(0px);在y轴上偏移量,transition-delay: 0.1s;动画延迟时间,ease-in-out缓入缓出。


结语


以上便是全部代码了,总体比较简单,只需要使用一些简单的动画属性即可,喜欢的小伙伴可以拿去看看,根据自己想要的效果进行修改。


作者:codePanda
来源:juejin.cn/post/7223742591372312636
收起阅读 »

正则什么的,你让我写,我会难受,你让我用,真香!

web
哈哈,如题所说,对于很多人来说写正则就是”兰德里的折磨“吧。如果不是有需求频繁要用,根本就不会想着学它。(?!^)(?=(\\d{3})+ 这种就跟外星文一样。 但你要说是用它,它又真的好用。用来做做校验、做做字符串提取、做做变形啥的,真不错。最好的就是能 ...
继续阅读 »



哈哈,如题所说,对于很多人来说写正则就是”兰德里的折磨“吧。如果不是有需求频繁要用,根本就不会想着学它。(?!^)(?=(\\d{3})+ 这种就跟外星文一样。


image.png


但你要说是用它,它又真的好用。用来做做校验、做做字符串提取、做做变形啥的,真不错。最好的就是能 CV 过来直接用~


本篇带来 15 个正则使用场景,按需索取,收藏恒等于学会!!


千分位格式化


在项目中经常碰到关于货币金额的页面显示,为了让金额的显示更为人性化与规范化,需要加入货币格式化策略。也就是所谓的数字千分位格式化。



  1. 123456789 => 123,456,789

  2. 123456789.123 => 123,456,789.123


const formatMoney = (money) => {
return money.replace(new RegExp(`(?!^)(?=(\\d{3})+${money.includes('.') ? '\\.' : '$'})`, 'g'), ',')
}

formatMoney('123456789') // '123,456,789'
formatMoney('123456789.123') // '123,456,789.123'
formatMoney('123') // '123'

想想如果不是用正则,还可以用什么更优雅的方法实现它?


解析链接参数


你一定常常遇到这样的需求,要拿到 url 的参数的值,像这样:



// url

const name = getQueryByName('name') // fatfish
const age = getQueryByName('age') // 100

通过正则,简单就能实现 getQueryByName 函数:


const getQueryByName = (name) => {
const queryNameRegex = new RegExp(`[?&]${name}=([^&]*)(&|$)`)
const queryNameMatch = window.location.search.match(queryNameRegex)
// Generally, it will be decoded by decodeURIComponent
return queryNameMatch ? decodeURIComponent(queryNameMatch[1]) : ''
}

const name = getQueryByName('name')
const age = getQueryByName('age')

console.log(name, age) // fatfish, 100

驼峰字符串


JS 变量最佳是驼峰风格的写法,怎样将类似以下的其它声明风格写法转化为驼峰写法?


1. foo Bar => fooBar
2. foo-bar---- => fooBar
3. foo_bar__ => fooBar

正则表达式分分钟教做人:


const camelCase = (string) => {
const camelCaseRegex = /[-_\s]+(.)?/g
return string.replace(camelCaseRegex, (match, char) => {
return char ? char.toUpperCase() : ''
})
}

console.log(camelCase('foo Bar')) // fooBar
console.log(camelCase('foo-bar--')) // fooBar
console.log(camelCase('foo_bar__')) // fooBar

小写转大写


这个需求常见,无需多言,用就完事儿啦:


const capitalize = (string) => {
const capitalizeRegex = /(?:^|\s+)\w/g
return string.toLowerCase().replace(capitalizeRegex, (match) => match.toUpperCase())
}

console.log(capitalize('hello world')) // Hello World
console.log(capitalize('hello WORLD')) // Hello World

实现 trim()


trim() 方法用于删除字符串的头尾空白符,用正则可以模拟实现 trim:


const trim1 = (str) => {
return str.replace(/^\s*|\s*$/g, '') // 或者 str.replace(/^\s*(.*?)\s*$/g, '$1')
}

const string = ' hello medium '
const noSpaceString = 'hello medium'
const trimString = trim1(string)

console.log(string)
console.log(trimString, trimString === noSpaceString) // hello medium true
console.log(string)

trim() 方法不会改变原始字符串,同样,自定义实现的 trim1 也不会改变原始字符串;


HTML 转义


防止 XSS 攻击的方法之一是进行 HTML 转义,符号对应的转义字符:


正则处理如下:


const escape = (string) => {
const escapeMaps = {
'&': 'amp',
'<': 'lt',
'>': 'gt',
'"': 'quot',
"'": '#39'
}
// The effect here is the same as that of /[&<> "']/g
const escapeRegexp = new RegExp(`[${Object.keys(escapeMaps).join('')}]`, 'g')
return string.replace(escapeRegexp, (match) => `&${escapeMaps[match]};`)
}

console.log(escape(`

hello world



`
))
/*
<div>
<p>hello world</p>
</div>
*/


HTML 反转义


有了正向的转义,就有反向的逆转义,操作如下:


const unescape = (string) => {
const unescapeMaps = {
'amp': '&',
'lt': '<',
'gt': '>',
'quot': '"',
'#39': "'"
}
const unescapeRegexp = /&([^;]+);/g
return string.replace(unescapeRegexp, (match, unescapeKey) => {
return unescapeMaps[ unescapeKey ] || match
})
}

console.log(unescape(`
<div>
<p>hello world</p>
</div>
`
))
/*

hello world



*/


校验 24 小时制


处理时间,经常要用到正则,比如常见的:校验时间格式是否是合法的 24 小时制:


const check24TimeRegexp = /^(?:(?:0?|1)\d|2[0-3]):(?:0?|[1-5])\d$/
console.log(check24TimeRegexp.test('01:14')) // true
console.log(check24TimeRegexp.test('23:59')) // true
console.log(check24TimeRegexp.test('23:60')) // false
console.log(check24TimeRegexp.test('1:14')) // true
console.log(check24TimeRegexp.test('1:1')) // true

校验日期格式


常见的日期格式有:yyyy-mm-dd, yyyy.mm.dd, yyyy/mm/dd 这 3 种,如果有符号乱用的情况,比如2021.08/22,这样就不是合法的日期格式,我们可以通过正则来校验判断:


const checkDateRegexp = /^\d{4}([-\.\/])(?:0[1-9]|1[0-2])\1(?:0[1-9]|[12]\d|3[01])$/

console.log(checkDateRegexp.test('2021-08-22')) // true
console.log(checkDateRegexp.test('2021/08/22')) // true
console.log(checkDateRegexp.test('2021.08.22')) // true
console.log(checkDateRegexp.test('2021.08/22')) // false
console.log(checkDateRegexp.test('2021/08-22')) // false

匹配颜色值


在字符串内匹配出 16 进制的颜色值:


const matchColorRegex = /#(?:[\da-fA-F]{6}|[\da-fA-F]{3})/g
const colorString = '#12f3a1 #ffBabd #FFF #123 #586'

console.log(colorString.match(matchColorRegex))
// [ '#12f3a1', '#ffBabd', '#FFF', '#123', '#586' ]

判断 HTTPS/HTTP


这个需求也是很常见的,判断请求协议是否是 HTTPS/HTTP


const checkProtocol = /^https?:/

console.log(checkProtocol.test('https://medium.com/')) // true
console.log(checkProtocol.test('http://medium.com/')) // true
console.log(checkProtocol.test('//medium.com/')) // false

校验版本号


版本号必须采用 x.y.z 格式,其中 XYZ 至少为一位,我们可以用正则来校验:


// x.y.z
const versionRegexp = /^(?:\d+\.){2}\d+$/

console.log(versionRegexp.test('1.1.1'))
console.log(versionRegexp.test('1.000.1'))
console.log(versionRegexp.test('1.000.1.1'))

获取网页 img 地址


这个需求可能爬虫用的比较多,用正则获取当前网页所有图片的地址。在控制台打印试试,太好用了~~


const matchImgs = (sHtml) => {
const imgUrlRegex = /]+src="((?:https?:)?\/\/[^"]+)"[^>]*?>/gi
let matchImgUrls = []

sHtml.replace(imgUrlRegex, (match, $1) => {
$1 && matchImgUrls.push($1)
})
return matchImgUrls
}

console.log(matchImgs(document.body.innerHTML))

格式化电话号码


这个需求也是常见的一匹,用就完事了:


let mobile = '18379836654' 
let mobileReg = /(?=(\d{4})+$)/g

console.log(mobile.replace(mobileReg, '-')) // 183-7983-6654

觉得不错的话,给个赞吧,以后继续补充~~


作者:掘金安东尼
来源:juejin.cn/post/7111857333113716750
收起阅读 »

css实现弧边选项卡

web
实现效果 实现方式 主要使用了 radial-gradient transform perspective rotateX transform-origin 等属性 思路 只需要想清楚如何实现弧形三角即可。这里还是借助了渐变 -- 径向渐变 ...
继续阅读 »

实现效果



image.png



实现方式



主要使用了



等属性



思路




  • 只需要想清楚如何实现弧形三角即可。这里还是借助了渐变 -- 径向渐变


image.png



  • 其实他是这样,如下图所示,我们只需要把黑色部分替换为透明即可,使用两个伪元素即可:


image.png



  • 通过超出隐藏和旋转得到想要的效果


image.png


image.png



  • 综上


在上述 outside-circle 的图形基础上:



  1. 设置一个适当的 perspective 值

  2. 设置一个恰当的旋转圆心 transform-origin

  3. 绕 X 轴进行旋转



  • 动图演示


3.gif



代码



<!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>Document</title>
</head>
<style>
.g-container {
position: relative;
width: 300px;
height: 100px;
background: red;
border: 1px solid #277f9e;
border-radius: 10px;
overflow: hidden;
}
.g-inner {
position: absolute;
width: 150px;
height: 50px;
background: #fee6e0;
bottom: 0;
border-radius: 0 20px 0 20px;
transform: perspective(40px) scaleX(1.4) scaleY(1.5) rotateX(20deg) translate(-10px, 0);
transform-origin: 50% 100%;
}
.g-inner::before {
content: "";
position: absolute;
right: -10px;
width: 10px;
height: 10px;
top: 40px;
background: radial-gradient(circle at 100% 0, transparent, transparent 9.5px, #fee6e0 10px, #fee6e0);
}
.g-after {
position: absolute;
width: 150px;
height: 50px;
background: #6ecb15;
bottom: 49px;
right: 0;
border-radius: 20px 0 20px 0;
transform: perspective(40px) scaleX(1.4) scaleY(-1.5) rotateX(20deg) translate(14px, 0);
transform-origin: 53% 100%;
}
.g-after::before {
content: "";
position: absolute;
left: -10px;
top: 40px;
width: 10px;
height: 10px;
background: radial-gradient(circle at 0 0, transparent, transparent 9.5px, #6ecb15 10px, #6ecb15);
}
.g-inner-text,.g-after-text {
position: absolute;
width: 150px;
height: 50px;
line-height: 50px;
text-align: center;
}
.g-inner-text {
top: 50%;
left: 0;
}
.g-after-text {
top: 50%;
right: 0;
}
</style>
<body>
<div class="g-container">
<div class="g-inner"></div>
<div class="g-after"></div>
<div class="g-inner-text">选项卡1</div>
<div class="g-after-text">选项卡2</div>
</div>
</body>
</html>

参考文章:github.com/chokcoco/iC…


作者:Agony95z
来源:juejin.cn/post/7223580639710281787
收起阅读 »

极致舒适的Vue页面保活方案

web
为了让页面保活更加稳定,你们是怎么做的? 我用一行配置实现了 Vue页面保活是指在用户离开当前页面后,可以在返回时恢复上一次浏览页面的状态。这种技术可以让用户享受更加流畅自然的浏览体验,而不会被繁琐的操作打扰。 为什么需要页面保活? 页面保活可以提高用户...
继续阅读 »

为了让页面保活更加稳定,你们是怎么做的?


我用一行配置实现了


image.png



Vue页面保活是指在用户离开当前页面后,可以在返回时恢复上一次浏览页面的状态。这种技术可以让用户享受更加流畅自然的浏览体验,而不会被繁琐的操作打扰。



为什么需要页面保活?


页面保活可以提高用户的体验感。例如,当用户从一个带有分页的表格页面(【页面A】)跳转到数据详情页面(【页面B】),并查看了数据之后,当用户从【页面B】返回【页面A】时,如果没有页面保活,【页面A】会重新加载并跳转到第一页,这会让用户感到非常烦恼,因为他们需要重新选择页面和数据。因此,使用页面保活技术,当用户返回【页面A】时,可以恢复之前选择的页码和数据,让用户的体验更加流畅。


如何实现页面保活?


状态存储


这个方案最为直观,原理就是在离开【页面A】之前手动将需要保活的状态存储起来。可以将状态存储到LocalStoreSessionStoreIndexedDB。在【页面A】组件的onMounted钩子中,检测是否存在此前的状态,如果存在从外部存储中将状态恢复回来。


有什么问题?



  • 浪费心智(麻烦/操心)。这个方案存在的问题就是,需要在编写组件的时候就明确的知道跳转到某些页面时进行状态存储。

  • 无法解决子组件状态。在页面组件中还可以做到保存页面组件的状态,但是如何保存子组件呢。不可能所有的子组件状态都在页面组件中维护,因为这样的结构并不是合理。


组件缓存


利用Vue的内置组件<KeepAlive/>缓存包裹在其中的动态切换组件(也就是<Component/>组件)。<KeepAlive/>包裹动态组件时,会缓存不活跃的组件,而不是销毁它们。当一个组件在<KeepAlive/>中被切换时,activateddeactivated生命周期钩子会替换mountedunmounted钩子。最关键的是,<KeepAlive/>不仅适用于被包裹组件的根节点,也适用于其子孙节点。


<KeepAlive/>搭配vue-router即可实现页面的保活,实现代码如下:


<template>
<RouterView v-slot="{ Component }">
<KeepAlive>
<component :is="Component"/>
</KeepAlive>
</RouterView>
</template>

有什么问题?



  • 页面保活不准确。上面的方式虽然实现了页面保活,但是并不能满足生产要求,例如:【页面A】是应用首页,【页面B】是数据列表页,【页面C】是数据详情页。用户查看数据详情的动线是:【页面A】->【页面B】->【页面C】,在这条动线中【页面B】->【页面C】的时候需要缓存【页面B】,当从【页面C】->【页面B】的时候需要从换从中恢复【页面B】。但是【页面B】->【页面A】的时候又不需要缓存【页面B】,上面的这个方法并不能做到这样的配置。


最佳实践


最理想的保活方式是,不入侵组件代码的情况下,通过简单的配置实现按需的页面保活。


【不入侵组件代码】这条即可排除第一种方式的实现,第二种【组件缓存】的方式只是败在了【按需的页面保活】。那么改造第二种方式,通过在router的路由配置上进行按需保活的配置,再提供一种读取配置结合<KeepAlive/>include属性即可。


路由配置


src/router/index.ts


import useRoutersStore from '@/store/routers';

const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'index',
component: () => import('@/layout/index.vue'),
children: [
{
path: '/app',
name: 'App',
component: () => import('@/views/app/index.vue'),
},
{
path: '/data-list',
name: 'DataList',
component: () => import('@/views/data-list/index.vue'),
meta: {
// 离开【/data-list】前往【/data-detail】时缓存【/data-list】
leaveCaches: ['/data-detail'],
}
},
{
path: '/data-detail',
name: 'DataDetail',
component: () => import('@/views/data-detail/index.vue'),
}
]
}
];

router.beforeEach((to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
const { cacheRouter } = useRoutersStore();
cacheRouter(from, to);
next();
});

保活组件存储


src/stroe/router.ts


import { RouteLocationNormalized } from 'vue-router';

const useRouterStore = defineStore('router', {
state: () => ({
cacheComps: new Set<string>(),
}),
actions: {
cacheRouter(from: RouteLocationNormalized, to: RouteLocationNormalized) {
if(
Array.isArray(from.meta.leaveCaches) &&
from.meta.leaveCaches.inclued(to.path) &&
typeof from.name === 'string'
) {
this.cacheComps.add(form.name);
}
if(
Array.isArray(to.meta.leaveCaches) &&
!to.meta.leaveCaches.inclued(from.path) &&
typeof to.name === 'string'
) {
this.cacheComps.delete(to.name);
}
},
},
getters: {
keepAliveComps(state: State) {
return [...state.cacheComps];
},
},
});

页面缓存


src/layout/index.vue


<template>
<RouterView v-slot="{ Component }">
<KeepAlive :include="keepAliveComps">
<component :is="Component"/>
</KeepAlive>
</RouterView>
</template>

<script lang='ts' setup>
import { storeToRefs } from 'pinia';
import useRouterStore from '@/store/router';

const { keepAliveComps } = storeToRefs(useRouterStore());
</script>

TypeScript提升配置体验


import 'vue-router';

export type LeaveCaches = string[];

declare module 'vue-router' {
interface RouteMeta {
leaveCaches?: LeaveCaches;
}
}

该方案的问题



  • 缺少通配符处理/*/**/index

  • 无法缓存/preview/:address这样的动态路由。

  • 组件名和路由名称必须保持一致。


总结


通过<RouterView v-slot="{ Component }">获取到当前路由对应的组件,在将该组件通过<component :is="Component" />渲染,渲染之前利用<KeepAlive :include="keepAliveComps">来过滤当前组件是否需要保活。
基于上述机制,通过简单的路由配置中的meta.leaveCaches = [...]来配置从当前路由出发到哪些路由时,需要缓存当前路由的内容。


如果大家有其他保活方案,欢迎留言交流哦!


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

vue 递归组件 作用域插槽

web
开头 这里主要是根据 vue 递归组件 作用域插槽 代码的理解和el-tree是如何写的。 代码 父组件 <template> <div> <Tree :data="data"> <templa...
继续阅读 »

开头


这里主要是根据 vue 递归组件 作用域插槽 代码的理解和el-tree是如何写的。


代码


父组件


<template>
<div>
<Tree :data="data">
<template #default="{ title }">
<div class="prent">
{{ title + "+自定义" }}
</div>
</template>
</Tree>
</div>
</template>
<script>
import Tree from "./tree.vue";
export default {
components: {
Tree,
},
data() {
return {
data: [{
title: "父1",
children: [{
title: "子",
children:[{title:"孙",}]
}],
},{
title: "父2",
children:[{title:"子"}]
}]
};
}
};
</script>

子组件


<template>
<div class="tree">
<div v-for="item of data" :key="item.title">
<!-- 显示title标题 -->
<div class="title">
<!-- 插槽,这里也是把title传出去, A插槽 -->
<slot :title="item.title">
<!-- {{ item.title }} -->
</slot>
</div>
<!-- 如果存在子项则调用本身组件 递归 -->
<Tree v-if="item.children" :data='item.children'>
<!-- B 插槽 -->
<slot :title='item.title' />
</Tree>
</div>
</div>
</template>

<script>
export default {
name: 'Tree',
props: {
data: Array,
},
};
</script>

<style scoped>
.tree {
padding-left: 10px;
}

ul,
li {
list-style: none;
margin: 0;
padding: 0;
}
</style>

理解步骤,始终知道 -> 递归就是把最里面的放到最外面来,你就当 A插槽最后会被 B 插槽替代

所以,父组件的 default 插槽用的是 B 插槽,因此 B 插槽就暴露出一个 title 给父组件使用。


删掉 A 的title :


<template>
<div class="tree">
<div v-for="item of data" :key="item.title">
<!-- 显示title标题 -->
<div class="title">
<!-- 插槽,这里也是把title传出去, A插槽 -->
<slot :title="item.title">
<!-- {{ item.title }} -->
</slot>
</div>
<!-- 如果存在子项则调用本身组件 递归 -->
<Tree v-if="item.children" :data='item.children'>
<!-- B 插槽 -->
<slot :title='item.title' />
</Tree>
</div>
</div>
</template>

<script>
export default {
name: 'Tree',
props: {
data: Array,
},
};
</script>

<style scoped>
.tree {
padding-left: 10px;
}

ul,
li {
list-style: none;
margin: 0;
padding: 0;
}
</style>

结果:


image.png


由于可能只有一层,所以走不到 B 插槽,因此 A 插槽也需要暴露一个 title 给外面使用。


el-tree 的原理


父组件


<template>
<div>
<Tree :data="data">
<!-- C -->
<template #default="{ title }">
<div class="prent">
{{ title + "+自定义" }}
</div>
</template>
</Tree>
</div>
</template>
<script>
import Tree from "./tree.vue";
export default {
components: {
Tree,
},
data() {
return {
data: [{
title: "父1",
children: [{
title: "子",
children:[{title:"孙",}]
}],
},{
title: "父2",
children:[{title:"子"}]
}]
};
}
};
</script>

子组件


<template>
<div class="tree">
<div v-for="item of data" :key="item.title">
<!-- 显示title标题 -->
<div class="title">
<!-- 插槽,这里也是把title传出去, A -->
<slot :title="item.title">
<!-- {{ item.title }} -->
</slot>
</div>
<!-- 如果存在子项则调用本身组件 递归 -->
<Tree v-if="item.children" :data='item.children'>
<!-- B -->
<template #default="{ title }">
<div class="prent">
{{ title + "+自定义22" }}
</div>
</template>
</Tree>
</div>
</div>
</template>

<script>
import node from './node.js'
export default {
name: 'Tree',
components: {
node,

},
props: {
data: Array,
},
data() {
return {
tree: null,
}
},
created() {
if(!this.$parent.$scopedSlots.default) {
this.tree = this
}else {
this.tree = this.$parent.tree
}
},
};
</script>

<style scoped>
.tree {
padding-left: 10px;
}

ul,
li {
list-style: none;
margin: 0;
padding: 0;
}
</style>

结果:


image.png


这里可以看到,父组件的 C 和 子组件中的 B 都是使用到了 A 这个插槽。


这里我们只要能把 B 替换成父组件的 C 就完成了递归插槽。


子组件的代码转变


<template>
<div class="tree">
<div v-for="item of data" :key="item.title">
<!-- 显示title标题 -->
<div class="title">
<!-- 插槽,这里也是把title传出去 -->
<slot :title="item.title">
<!-- {{ item.title }} -->
</slot>
</div>
<!-- 如果存在子项则调用本身组件 递归 -->
<Tree v-if="item.children" :data='item.children'>
<template #default="{ title }">
<node :title="title">
</node>
</template>
</Tree>
</div>
</div>
</template>

<script>
import node from './node.js'
export default {
name: 'Tree',
components: {
node: {
props: {
title: String,
},
render(h) {
const parent = this.$parent;
const tree = parent.tree
const title = this.title
return (tree.$scopedSlots.default({ title }))
}
}
},
props: {
data: Array,
},
data() {
return {
tree: null,
}
},
created() {
if (!this.$parent.$scopedSlots.default) {
this.tree = this
} else {
this.tree = this.$parent.tree
}
},
};
</script>

<style scoped>
.tree {
padding-left: 10px;
}

ul,
li {
list-style: none;
margin: 0;
padding: 0;
}
</style>

这里搞了一个 node 的函数组件,node 函数组件拿到 子组件的 tree, tree也是一层层的保存着 $scopedSlots.default 其实就是 C 的那些编译节点。 然后把 title 传给了 C。


el-tree 源码贴图


image.png


tree


image.png


tree-node


image.png


image.png


image.png


image.png


总结


写的有点乱啊,这个只是辅助你理解 递归插槽,其实一开始都是懵逼了,多看下代码理解还是能看的懂的。


作者:晓欲望
来源:juejin.cn/post/7222931700438138937
收起阅读 »

不用刷新!用户无感升级,解决前端部署最后的问题

web
前端部署需要用户刷新才能继续使用,一直是一个老大难的用户体验问题。本文将围绕这个问题进行讲解,揭晓问题发生的原因及解决思路。 一、背景 网站发版过程中,用户可在浏览web页面时,可能会导致页面无法加载对应的资源,导致出现点击无反应的情况,严重影响用户体验。 二...
继续阅读 »

前端部署需要用户刷新才能继续使用,一直是一个老大难的用户体验问题。本文将围绕这个问题进行讲解,揭晓问题发生的原因及解决思路。


一、背景


网站发版过程中,用户可在浏览web页面时,可能会导致页面无法加载对应的资源,导致出现点击无反应的情况,严重影响用户体验。


二、问题分析


2.1 问题现象


网络控制台显示加载页面的资源显示404。


image.png


2.2 满足条件


发生这个现象,需要满足三个条件:



  1. 站点是SPA页面,并开启懒加载;

  2. 资源地址启用内容hash。(加载更快启用了强缓存,为了应对资源变更能及时更新内容,会对资源地址的文件名加上内容hash)。

  3. 覆盖式部署,新版本发布后旧的版本会被删除。


特别在容器部署的情况的SPA页面,很容易满足上诉三个条件。笔者在做公司的内部系统就踩过坑。


2.3 原因分析


浏览器打开页面后,会记录路由和资源路径的映射,服务器发版后,没有及时通知浏览器更新路由映射表。导致用户在发布前端打开的页面,在版本更新后,进入下一个路由加载上一个版本的资源失败,导致需要用户刷新才能正常使用。


image.png


三、解决方案


3.1 方案一:失败重试


3.1.1 思路整理:


既然加载失败了,就重试加载发版后的资源版本就行。增加一个manifest.json文件能够获取新版本对应的资源路径。


image.png


3.1.2 举例说明


以vue项目进行举例子说明:


第一步: 修改构建工具配置以生成manifest文件


使用vite构建的项目,可以在vite.config.ts增加配置build.manifest为true,用以生成manifest.json文件


export default defineConfig({
// 更多配置
build: {
//开启manifest
manifest: true,
cssCodeSplit: false //关闭单独生成css文件,方便demo演示
}
})

如果使用webpack构建的项目,可以使用webpack-manifest-plugin插件进行配置。


进行项目生产构建,生成manifest.json,内容如下:


 // 简单说明:文件内容是项目原始代码目录结构和构建生成的资源路径一一对应
{
"index.html": { // 页面入口
"dynamicImports": ["src/pages/page1.vue", "src/pages/page2.vue"],
"file": "assets/index-e170761c.js",
"isEntry": true,
"src": "index.html"
},
// page1对应单文件组件
"src/pages/page1.vue": {
"file": "assets/page1-515906ab1.js", // JS文件
"imports": ["index.html"],
"isDynamicEntry": true,
"src": "src/pages/page1.vue"
},
// page2对应单文件组件
"src/pages/page2.vue": {
"file": "assets/page2-9785c68c.js", // JS文件
"imports": ["index.html"],
"isDynamicEntry": true,
"src": "src/pages/page2.vue"
},
"style.css": {
"file": "assets/style-809e5baa.css",
"src": "style.css"
}
}

第二步,修改route文件,加上重试逻辑


在路由文件中,增加加载页面js失败的重试逻辑,通过新版的manifest.json来获取新版的页面js,再次加载。


import { createRouter, createWebHistory } from 'vue-router'


const router = createRouter({
history: createWebHistory('/'),
routes: [
{
path: '/page1',
// component: () => import(`../pages/page1.vue`), // 变更前
component: () => retryImport('page1'), // 变更后
},
{
path: '/page2',
// component: () => import(`../pages/page1.vue`),
component: () => retryImport('page2'),
},
]
})


async function retryImport(page) {
try {
// 加载页面资源
switch (page) {
case 'page1':
// 这里demo演示,没有使用dynamic-import-vars
return await import(`../pages/page1.vue`)
default:
return await import(`../pages/page2.vue`)
}
} catch (err: any) {
// 判断是否是资源加载错误,错误重试
if (err.toString().indexOf('Failed to fetch dynamically imported module') > -1) {
// 获取manifest资源清单
return fetch('/manifest.json').then(async (res) => {
const json = await res.json()
// 找到对应的最新版本的js
const errPage = `src/pages/${page}.vue`
// 加载新的js
return await import(`/${json[errPage].file}`)
})
}
throw err
}
}
export default router

3.1.3 总结


这个方案改造只涉及前端层,成本最低,但是无法做到多版本共存,只能适配部分发版变更,如果涉及删除页面的版本,最好增加一个容错页面。


3.2 方案二:增量部署


3.2.1 思路整理


生产环境发布改成增量发布,不再是覆盖式的发布,发版后旧版本依旧保留。


image.png


3.2.2 示例实践


需要改造构建配置,增加版本的概念,保证新旧版本不路径冲突


vite 构建工具示例:


// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
const version = require('./package.json').version

// 支持构建命令传入构建版本
const versionName = process.env.VERSION_NAME || version
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
build: {
manifest: true,
assetsDir: `./${versionName}`, // 版本号
}
})

webpack构建工具示例:


// webpack.config.js
const path = require('path');
// 支持构建命令传入构建版本
const versionName = process.env.VERSION_NAME || version
module.exports = {
//...
output: {
path: path.resolve(__dirname, `dist/${versionName}/assets`),
},
};

3.2.3 总结


需要CI/CD发版改造,由之前的容器部署改成静态部署(即文件上传对象存储的思路一样),这种增量发部署适配全部的场景,而且,支持多版本共存,能做到版本灰度放量。


四、总结


本文通过覆盖式部署在前端版本发布,导致用户不可使用的严重体验问题,分析问题发生根因,并给出两种解决思路。笔者结合公司的云设施,最后使用增量部署,并BFF层配合使用,多版本共存,线上启用通过配置指定启用哪个版本,再也不用赶着时间点去发版。


作者:azuo
来源:juejin.cn/post/7223196531143131194
收起阅读 »

VUE中常用的4种高级方法

web
1. provide/inject provide/inject 是 Vue.js 中用于跨组件传递数据的一种高级技术,它可以将数据注入到一个组件中,然后让它的所有子孙组件都可以访问到这个数据。通常情况下,我们在父组件中使用 provide 来提供数据,然后在...
继续阅读 »

1. provide/inject


provide/inject 是 Vue.js 中用于跨组件传递数据的一种高级技术,它可以将数据注入到一个组件中,然后让它的所有子孙组件都可以访问到这个数据。通常情况下,我们在父组件中使用 provide 来提供数据,然后在子孙组件中使用 inject 来注入这个数据。


使用 provide/inject 的好处是可以让我们在父组件和子孙组件之间传递数据,而无需手动进行繁琐的 props 传递。它可以让代码更加简洁和易于维护。但需要注意的是,provide/inject 的数据是非响应式的,这是因为provide/inject是一种更加底层的 API,它是基于依赖注入的方式来传递数据,而不是通过响应式系统来实现数据的更新和同步。


具体来说,provide方法提供的数据会被注入到子组件中的inject属性中,但是这些数据不会自动触发子组件的重新渲染,如果provide提供的数据发生了变化,子组件不会自动感知到这些变化并更新。


如果需要在子组件中使用provide/inject提供的数据,并且希望这些数据能够响应式地更新,可以考虑使用Vue的响应式数据来代替provide/inject。例如,可以将数据定义在父组件中,并通过props将其传递给子组件,子组件再通过$emit来向父组件发送数据更新的事件,从而实现响应式的数据更新。


下面是一个简单的例子,展示了如何在父组件中提供数据,并在子孙组件中注入这个数据:


<!-- 父组件 -->
<template>
<div>
<ChildComponent />
</div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
provide: {
message: 'Hello from ParentComponent',
},
components: {
ChildComponent,
},
};
</script>

//上面provide还可以写成函数形式
export default {
provide(){
return {
message: this.message
}
}
}


<!-- 子组件 -->
<template>
<div>
<GrandchildComponent />
</div>
</template>

<script>
import GrandchildComponent from './GrandchildComponent.vue';

export default {
inject: ['message'],
components: {
GrandchildComponent,
},
};
</script>


<!-- 孙子组件 -->
<template>
<div>
<p>{{ message }}</p>
</div>
</template>

<script>
export default {
inject: ['message'],
};
</script>


在上面的例子中,父组件中提供了一个名为 message 的数据,子孙组件中都可以使用 inject 来注入这个数据,并在模板中使用它。注意,子孙组件中的 inject 选项中使用了一个数组,数组中包含了需要注入的属性名。在这个例子中,我们只注入了一个 message 属性,所以数组中只有一个元素。


2. 自定义v-model


要使自定义的Vue组件支持v-model,需要实现一个名为value的prop和一个名为input的事件。在组件内部,将value prop 绑定到组件的内部状态,然后在对内部状态进行修改时触发input事件。


下面是一个简单的例子,展示如何创建一个自定义的输入框组件并支持v-model:


<template>
<input :value="value" @input="$emit('input', $event.target.value)" />
</template>

<script>
export default {
name: 'MyInput',
props: {
value: String
}
};
</script>


在上面的组件中,我们定义了一个value prop,这是与v-model绑定的数据。我们还将内置的input事件转发为一个自定义的input事件,并在事件处理程序中更新内部状态。现在,我们可以在父组件中使用v-model来绑定这个自定义组件的值,就像使用普通的输入框一样:


<template>
<div>
<my-input v-model="message" />
<p>{{ message }}</p>
</div>
</template>

<script>
import MyInput from './MyInput.vue';

export default {
components: {
MyInput
},
data() {
return {
message: ''
};
}
};
</script>


在上面的代码中,我们通过使用v-model指令来双向绑定message数据和MyInput组件的值。当用户在输入框中输入文本时,MyInput组件会触发input事件,并将其更新的值发送给父组件,从而实现了双向绑定的效果。


3. 事件总线(EventBus)


Vue事件总线是一个事件处理机制,它可以让组件之间进行通信,以便在应用程序中共享信息。在Vue.js应用程序中,事件总线通常是一个全局实例,可以用来发送和接收事件。


以下是使用Vue事件总线的步骤:


3.1 创建一个全局Vue实例作为事件总线:


import Vue from 'vue';
export const eventBus = new Vue();

3.2 在需要发送事件的组件中,使用$emit方法触发事件并传递数据:


eventBus.$emit('eventName', data);

3.3 在需要接收事件的组件中,使用$on方法监听事件并处理数据:


eventBus.$on('eventName', (data) => {
// 处理数据
});

需要注意的是,事件总线是全局的,所以在不同的组件中,需要保证事件名称的唯一性。


另外,需要在组件销毁前使用$off方法取消事件监听:


eventBus.$off('eventName');

这样就可以在Vue.js应用程序中使用事件总线来实现组件之间的通信了。


4. render方法


Vue 的 render 方法是用来渲染组件的函数,它可以用来替代模板语法,通过代码的方式来生成 DOM 结构。相较于模板语法,render 方法具有更好的类型检查和代码提示。


下面详细介绍 Vue 的 render 方法的使用方法:


4.1 基本语法


render 方法的基本语法如下:


render: function (createElement) {
// 返回一个 VNode
}

其中 createElement 是一个函数,它用来创建 VNode(虚拟节点),并返回一个 VNode 对象。


4.2 创建 VNode


要创建 VNode,可以调用 createElement 函数,该函数接受三个参数:



  • 标签名或组件名

  • 可选的属性对象

  • 子节点数组


例如,下面的代码创建了一个包含文本节点的 div 元素:


render: function (createElement) {
return createElement('div', 'Hello, world!')
}

如果要创建一个带有子节点的元素,可以将子节点作为第三个参数传递给 createElement 函数。例如,下面的代码创建了一个包含两个子元素的 div 元素:


render: function (createElement) {
return createElement('div', [
createElement('h1', 'Hello'),
createElement('p', 'World')
])
}

如果要给元素添加属性,可以将属性对象作为第二个参数传递给 createElement 函数。例如,下面的代码创建了一个带有样式和事件处理程序的 button 元素:


render: function (createElement) {
return createElement('button', {
style: { backgroundColor: 'red' },
on: {
click: this.handleClick
}
}, 'Click me')
},
methods: {
handleClick: function () {
console.log('Button clicked')
}
}

4.3 动态数据


render 方法可以根据组件的状态动态生成内容。要在 render 方法中使用组件的数据,可以使用 this 关键字来访问组件实例的属性。例如,下面的代码根据组件的状态动态生成了一个带有计数器的 div 元素:


render: function (createElement) {
return createElement('div', [
createElement('p', 'Count: ' + this.count),
createElement('button', {
on: {
click: this.increment
}
}, 'Increment')
])
},
data: function () {
return {
count: 0
}
},
methods: {
increment: function () {
this.count++
}
}


4.4 JSX


在使用 Vue 的 render 方法时,也可以使用 JSX(JavaScript XML)语法,这样可以更方便地编写模板。要使用 JSX,需要在组件中导入 VuecreateElement 函数,并在 render 方法中使用 JSX 语法。例如,下面的代码使用了 JSX 语法来创建一个计数器组件:


import Vue from 'vue'

export default {
render() {
return (
<div>
<p>Count:{this.count}</p>
<button onClick={this.increment}>Increment</button>
</div>

)
},
data() {
return { count: 0 }
},
methods: {
increment() {
this.count++
}
}
}


注意,在使用 JSX 时,需要使用 {} 包裹 JavaScript 表达式。


4.5 生成函数式组件


除了生成普通的组件,render 方法还可以生成函数式组件。函数式组件没有状态,只接收 props 作为输入,并返回一个 VNode。因为函数式组件没有状态,所以它们的性能比普通组件更高。


要生成函数式组件,可以在组件定义中将 functional 属性设置为 true。例如,下面的代码定义了一个函数式组件,用于显示列表项:


export default {
functional: true,
props: ['item'],
render: function (createElement, context) {
return createElement('li', context.props.item);
}
}

注意,在函数式组件中,props 作为第二个参数传递给 render<

作者:阿虎儿
来源:juejin.cn/post/7225921305597820985
/code> 方法。

收起阅读 »

记一次不规范使用key引发的惨案

web
前言 平时在使用v-for的时候,一般会要求传入key,有没有像我一样的小伙伴,为了省心,直接传索引index,貌似也没有遇到过什么问题,直到有一天,我遇到一个这样的需求 场景 在一个下单界面,我需要去商品列表选商品,然后在下单界面遍历显示所选商品,要求后选的...
继续阅读 »

前言


平时在使用v-for的时候,一般会要求传入key,有没有像我一样的小伙伴,为了省心,直接传索引index,貌似也没有遇到过什么问题,直到有一天,我遇到一个这样的需求


场景


在一个下单界面,我需要去商品列表选商品,然后在下单界面遍历显示所选商品,要求后选的排在前面,而且选好商品之后,需要在下单界面给每个商品选择发货地,发货地列表是通过商品id去接口取的,我的代码长这样:



  • 下单界面调用商品组件


// 这里每次选了商品都是从前插入:list.value = [...newList, ...list.value]
<Goods
v-for="(item, index) in list"
:key="index"
:goods="item">
</Goods>


  • 商品组件内部调用发货地组件


<SendAddress
v-model="address"
:product-no="goods.productNo"
placeholder="请选择发货地"
@update:model-value="updateValue"></SendAddress>


  • 发货地组件内部获取发货地址列表


onMounted(async () => {
getList()
})
const getList = async () => {
const postData = {
productInfo: props.productNo,
}
}

上述代码运行结果是,每次获取地址用的都是最开始选的那个商品的信息,百思不得其解啊,最后说服产品,不要倒序了,问题解决


解决过程


后来在研究前进刷新后退缓存时,关注到了组件的key,详细了解后才知其中来头


企业微信截图_16813558431830.png



重点:根据key复用或者更新,也就是key没有变化,就是复用,变化了在更新挂载,而onMounted是在挂载完成后执行,没有挂载的元素,就不会走onMounted



回到上述问题,当我们每次从前面插入数据,key的变化逻辑是这样的


结论


企业微信截图_16813564053499.png



最开始选中的商品key从1变成了2,最近选的是0。


而0和1是本来就存在的,只会更新数据,不会重新挂载,只有最开始选的那个商品key是全新的,会重新挂载,重新走onMounted。


所以每次选择数据后,拿去获取地址列表的商品信息都是第一个的



解决以上问题,把key改成item.productNo就解决了


作者:赖皮喵
来源:juejin.cn/post/7221357811287834680
收起阅读 »