注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

pnpm 是凭什么对 npm 和 yarn 降维打击的

web
大家最近是不是经常听到 pnpm,我也一样。今天研究了一下它的机制,确实厉害,对 yarn 和 npm 可以说是降维打击。 那具体好在哪里呢? 我们一起来看一下。 我们按照包管理工具的发展历史,从 npm2 开始讲起: npm2 用 node 版本管理工具把...
继续阅读 »

大家最近是不是经常听到 pnpm,我也一样。今天研究了一下它的机制,确实厉害,对 yarn 和 npm 可以说是降维打击。
那具体好在哪里呢? 我们一起来看一下。



我们按照包管理工具的发展历史,从 npm2 开始讲起:


npm2


用 node 版本管理工具把 node 版本降到 4,那 npm 版本就是 2.x 了。


768C1B00093D82D19D2CC333F3221670.jpg


然后找个目录,执行下 npm init -y,快速创建个 package.json。


然后执行 npm install express,那么 express 包和它的依赖都会被下载下来:


FB54F396F6A73093CE052A6881AF7C50.jpg
展开 express,它也有 node_modules:


E652FB00C06BA36FA8861E3B785981BF.jpg
再展开几层,每个依赖都有自己的 node_modules:


75AC9B15A99383C9EA9E5E4EF8302588.jpg
也就是说 npm2 的 node_modules 是嵌套的。


这很正常呀?有什么不对么?


这样其实是有问题的,多个包之间难免会有公共的依赖,这样嵌套的话,同样的依赖会复制很多次,会占据比较大的磁盘空间。


这个还不是最大的问题,致命问题是 windows 的文件路径最长是 260 多个字符,这样嵌套是会超过 windows 路径的长度限制的。


当时 npm 还没解决,社区就出来新的解决方案了,就是 yarn:


yarn


yarn 是怎么解决依赖重复很多次,嵌套路径过长的问题的呢?


铺平。所有的依赖不再一层层嵌套了,而是全部在同一层,这样也就没有依赖重复多次的问题了,也就没有路径过长的问题了。


我们把 node_modules 删了,用 yarn 再重新安装下,执行 yarn add express:


这时候 node_modules 就是这样了:


7AF387F155588612B92C329F91D30BFA.jpg


全部铺平在了一层,展开下面的包大部分是没有二层 node_modules 的:


BBBA3B5F68AB691541FD51569E5B1316.jpg


当然也有的包还是有 node_modules 的,比如这样:


B5E0DBF3C7E6FBEEDC2CC90D350A278C.jpg
为什么还有嵌套呢?


因为一个包是可能有多个版本的,提升只能提升一个,所以后面再遇到相同包的不同版本,依然还是用嵌套的方式。


npm 后来升级到 3 之后,也是采用这种铺平的方案了,和 yarn 很类似:


67B0E1280BAD542944AB08A35CCE88C3.jpg
当然,yarn 还实现了 yarn.lock 来锁定依赖版本的功能,不过这个 npm 也实现了。


yarn 和 npm 都采用了铺平的方案,这种方案就没有问题了么?


并不是,扁平化的方案也有相应的问题。


最主要的一个问题是幽灵依赖,也就是你明明没有声明在 dependencies 里的依赖,但在代码里却可以 require 进来。


这个也很容易理解,因为都铺平了嘛,那依赖的依赖也是可以找到的。


但是这样是有隐患的,因为没有显式依赖,万一有一天别的包不依赖这个包了,那你的代码也就不能跑了,因为你依赖这个包,但是现在不会被安装了。


这就是幽灵依赖的问题。


而且还有一个问题,就是上面提到的依赖包有多个版本的时候,只会提升一个,那其余版本的包不还是复制了很多次么,依然有浪费磁盘空间的问题。


那社区有没有解决这俩问题的思路呢?


当然有,这不是 pnpm 就出来了嘛。


那 pnpm 是怎么解决这俩问题的呢?


pnpm


回想下 npm3 和 yarn 为什么要做 node_modules 扁平化?不就是因为同样的依赖会复制多次,并且路径过长在 windows 下有问题么?


那如果不复制呢,比如通过 link。


首先介绍下 link,也就是软硬连接,这是操作系统提供的机制,硬连接就是同一个文件的不同引用,而软链接是新建一个文件,文件内容指向另一个路径。当然,这俩链接使用起来是差不多的。


如果不复制文件,只在全局仓库保存一份 npm 包的内容,其余的地方都 link 过去呢?


这样不会有复制多次的磁盘空间浪费,而且也不会有路径过长的问题。因为路径过长的限制本质上是不能有太深的目录层级,现在都是各个位置的目录的 link,并不是同一个目录,所以也不会有长度限制。


没错,pnpm 就是通过这种思路来实现的。


再把 node_modules 删掉,然后用 pnpm 重新装一遍,执行 pnpm install。


你会发现它打印了这样一句话:


FA450CB6BE37F7AEDADDD7AF8CB5EBF9.jpg


包是从全局 store 硬连接到虚拟 store 的,这里的虚拟 store 就是 node_modules/.pnpm。


我们打开 node_modules 看一下:


DD21BA4ABF8516795C6BC205C18793E3.jpg
确实不是扁平化的了,依赖了 express,那 node_modules 下就只有 express,没有幽灵依赖。


展开 .pnpm 看一下:


25BF2AA593655F0A20232371A43AB81A.jpg
所有的依赖都在这里铺平了,都是从全局 store 硬连接过来的,然后包和包之间的依赖关系是通过软链接组织的。


比如 .pnpm 下的 expresss,这些都是软链接:


6F84C353D1CFE72E2F820B62C9A3B96E.jpg
也就是说,所有的依赖都是从全局 store 硬连接到了 node_modules/.pnpm 下,然后之间通过软链接来相互依赖。


官方给了一张原理图,配合着看一下就明白了:


0E694CA43CC1E52ED6AF8BCD50882004.jpg
这就是 pnpm 的实现原理。


那么回过头来看一下,pnpm 为什么优秀呢?


首先,最大的优点是节省磁盘空间呀,一个包全局只保存一份,剩下的都是软硬连接,这得节省多少磁盘空间呀。


其次就是快,因为通过链接的方式而不是复制,自然会快。


这也是它所标榜的优点:


image.png


相比 npm2 的优点就是不会进行同样依赖的多次复制。


相比 yarn 和 npm3+ 呢,那就是没有幽灵依赖,也不会有没有被提升的依赖依然复制多份的问题。


这就已经足够优秀了,对 yarn 和 npm 可以说是降维打击。


总结


pnpm 最近经常会听到,可以说是爆火。本文我们梳理了下它爆火的原因:


npm2 是通过嵌套的方式管理 node_modules 的,会有同样的依赖复制多次的问题。


npm3+ 和 yarn 是通过铺平的扁平化的方式来管理 node_modules,解决了嵌套方式的部分问题,但是引入了幽灵依赖的问题,并且同名的包只会提升一个版本的,其余的版本依然会复制多次。


pnpm 则是用了另一种方式,不再是复制了,而是都从全局 store 硬连接到 node_modules/.pnpm,然后之间通过软链接来组织依赖关系。


这样不但节省磁盘空间,也没有幽灵依赖问题,安装速度还快,从机制上来说完胜 npm 和 yarn。


pnpm 就是凭借这个对 npm 和 yarn 降维打击的。


作者:JEECG官方
来源:juejin.cn/post/7260283292754919484
收起阅读 »

前端发展:走进行业迷茫的迷雾中

web
引言 2023年,前端开发作为IT行业中备受关注的领域之一,正在经历着巨大的挑战和变革。然而,在当前行业不景气、失业率居高不下以及裁员潮席卷而来的情况下,许多人开始质疑前端开发的未来前景以及学习它是否依然有意义。本文将探讨这个问题并试图给出一些启示。 第一部...
继续阅读 »

引言


image.png
2023年,前端开发作为IT行业中备受关注的领域之一,正在经历着巨大的挑战和变革。然而,在当前行业不景气、失业率居高不下以及裁员潮席卷而来的情况下,许多人开始质疑前端开发的未来前景以及学习它是否依然有意义。本文将探讨这个问题并试图给出一些启示。


第一部分:前端的价值


image.png
前端开发作为网页和移动应用程序开发的重要组成部分,扮演着连接用户与产品的桥梁。前端技术的发展不仅推动了用户体验的提升,也对整个互联网行业产生了深远的影响。随着移动互联网的普及和技术的进步,前端在用户与产品之间的交互变得越来越重要。


对于企业而言,拥有优秀的前端开发团队意味着能够提供更好的用户体验、增强品牌形象、吸引更多用户和扩大市场份额。因此,前端开发的技能依然是企业争相追求的核心能力之一。


第二部分:行业不景气的背后


image.png
然而,正如每个行业都经历高低起伏一样,前端开发也面临着行业不景气带来的挑战。2023年,全球经济增长乏力、市场竞争激烈以及萧条的就业市场等因素,使得许多公司紧缩预算、停止招聘,并导致了失业率的上升和裁员的潮水。


在这种情况下,前端开发者需要重新审视自己的技能和市场需求。他们需要具备综合能力,包括对最新前端技术的深入了解、与其他团队成员的良好沟通合作能力以及持续学习和适应变化的能力。


第三部分:自我调整与进阶


image.png
面对市场变化和就业压力,前端开发者需要主动调整自己的发展路径。以下是一些建议:



  1. 多元化技能:学习并精通多种前端框架和库,如React、Vue.js和Angular等。同时,了解后端开发和数据库知识,拥有全栈开发的能力,将会让你在就业市场上更具竞争力。

  2. 学习与实践并重:不仅仅是学习新知识,还要将所学应用于实际项目中。积累项目经验,并在GitHub等平台分享你的作品,以展示自己的能力和潜力。同时,参加行业内的比赛、活动和社区,与他人交流并学习他们的经验。

  3. 持续学习:前端技术发展日新月异,不断学习是必需的。关注行业的最新趋势和技术,参加培训、研讨会或在线课程,保持对新知识的敏感度和学习能力。


第四部分:面对就业市场的挑战


image.png
在面对行业不景气和裁员的情况下,重新进入就业市场变得更加具有挑战性。以下是一些建议:



  1. 提升个人竞争力:通过获得认证、实习或自主开发项目等方式,提升自己在简历中的竞争力。扩展自己的专业网络,与其他开发者和雇主建立联系。

  2. 寻找新兴领域:探索新兴的技术领域,如大数据、人工智能和物联网等,这些领域对前端开发者的需求逐渐增加,可能为你提供新的机会。

  3. 转型或深耕细分领域:如果市场需求不断减少,可以考虑转型到与前端相关的领域,如UI设计、交互设计或用户体验设计等。或者在前端领域深耕细分领域,在特定行业或特定技术方向上寻找就业机会。


结论


image.png
虽然当前的行业环境确实严峻,但前端开发作为连接用户与产品的重要纽带,在未来依然有着广阔的发展空间。关键在于前端开发者要不断自我调整与进阶,持续学习并适应市场需求。通过多元化技能、学习实践、提升个人竞争力以及面对市场挑战,前端开发者依然可以在这个变革

作者:Jony_men
来源:juejin.cn/post/7260330862289371173
时代中谋得一席之地。

收起阅读 »

树结构的数据扁平化

web
function flattenTree(data) { data = JSON.parse(JSON.stringify(data)); var res = []; while(data.length) { var n...
继续阅读 »

function flattenTree(data) {
data = JSON.parse(JSON.stringify(data));
var res = [];
while(data.length) {
var node = data.shift();
if (node.children && node.children.length) {
data = data.concat(node.children);
}
delete node.children;
res.push(node);
}
return res;
}


我们用一个数据来测试:



var tree = [{
id: 1,
name: '1',
children: [{
id: 2,
name: '2',
children: [{
id: 3,
name: '3',
children: [{
id: 4,
name: '4'
}]
}, {
id: 6,
name: '6'
}]
}]
}, {
id: 5,
name: '5'
}]


使用:



console.log(flattenTree(tree));


打印结果:


image.png


作者:tntxia
来源:juejin.cn/post/7260500913848090661
收起阅读 »

用Vue.js构建一个Web3应用像,像开发 Web2 一样熟悉

web
作为一名涉足去中心化网络的前端 JavaScript 开发人员,您可能遇到过许多 Web3 开发解决方案。但是,这些解决方案通常侧重于钱包集成和交易执行,这就造成了学习曲线,偏离了熟悉的 Web2 开发体验。 但不用担心!有一种解决方案可以无缝衔接 Web2...
继续阅读 »

作为一名涉足去中心化网络的前端 JavaScript 开发人员,您可能遇到过许多 Web3 开发解决方案。但是,这些解决方案通常侧重于钱包集成和交易执行,这就造成了学习曲线,偏离了熟悉的 Web2 开发体验。


但不用担心!有一种解决方案可以无缝衔接 Web2 和 Web3,它就是 Juno



网址:https://juno.build/



在本篇博文中,我们将探讨如何利用 Vue 和 Juno 的强大功能来开发去中心化应用程序(dApps)。加入我们的旅程,揭开 Juno 的神秘面纱,让您轻松创建非凡的去中心化体验!





导言


在我之前的博文中,我讨论了 React[1]Angular[2] 这两个流行的 JavaScript 前端框架的类似解决方案。如果这两个框架中的任何一个是您的首选,我建议您浏览这些具体的文章,以获得量身定制的见解。


Juno如何工作


如果你还不了解 Juno,它是一个功能强大的开源区块链即服务平台,旨在让去中心化应用程序开发变得更加容易。可以把它想象成一个无服务器平台,类似于谷歌Firebase或AWS Amplify等流行服务,但增加了区块链技术的优势。Juno 完全在区块链上运行您的应用程序,确保完全去中心化和安全的基础设施。


通过利用Internet Computer[3]区块链网络和基础设施,Juno 为您创建的每个应用程序引入了一个名为 “Satellites” 的独特概念。这些 Satellites 作为强大的智能合约,封装了您的整个应用程序,包括 JavaScript、HTML 和图像文件等网络资产,以及简单的数据库、文件存储和身份验证机制。通过 Juno,您可以完全控制应用程序的功能和数据。


构建一个 Dapp


让我们开始构建我们的第一个去中心化应用程序,简称“dapp”。在这个例子中,我们将创建一个笔记应用程序,允许用户存储和检索数据条目,以及上传文件。


本教程和代码示例使用了 Vue Composition API。


初始化


在将 Juno 集成到应用程序之前,需要创建一个 satellite。该过程在文档[4]中有详细的解释。


此外,还需要安装SDK。


npm i @junobuild/core

完成这两个步骤后,您可以在 Vue 应用程序的根目录(例如 App.vue)中使用 satellite ID 初始化 Juno。这将配置库与您的智能合约进行通信。


<script setup lang="ts">
import { onMounted } from 'vue'
import { initJuno } from '@junobuild/core'

onMounted(
  async () =>
    await initJuno({
      satelliteId'pycrs-xiaaa-aaaal-ab6la-cai'
    })
)
</script>

<template>
<h1>Hello World</h1>
</template>

配置完成!现在,您的应用程序已经可以用于 Web3 了!😎


身份验证


为了确保用户身份的安全性和匿名性,需要对用户进行登录和注销。要做到这一点,可以将相关函数绑定到应用程序中任何位置的 call-to-action 按钮。


<script setup lang="ts">
import { signIn, signOut} from '@junobuild/core'
</script>

<button @click="signIn">Sign-in</button>
<button @click="signOut">Sign-out</button>

为了与其他服务建立无缝集成,库和 satellite 组件在用户成功登录后自动在您的智能合约中生成新条目。此功能使库能够在任何数据交换期间有效地验证权限。


为了监视并深入了解该条目,从而访问有关用户状态的信息,Juno提供了一个名为authSubscribe() 的可观察函数。您可以根据需要灵活地多次使用此函数。然而,你也可以创建一个在整个应用中有效传播用户信息的 store。


import { ref, type Ref } from 'vue'
import { defineStore } from 'pinia'
import { authSubscribe, type User } from '@junobuild/core'

export const useAuthStore = defineStore('auth', () => {
  const user: Ref<User | null | undefined> = ref(undefined)

  const unsubscribe = authSubscribe((u) => (user.value = u))

  return { user, unsubscribe }
})

这样,在应用程序的顶层订阅它就变得非常方便。


<script setup lang="ts">
import { useAuthStore } from '../stores/auth.store'
import { storeToRefs } from 'pinia'

const store = useAuthStore()
const { user } = storeToRefs(store)
</script>

<template>
  <template v-if="user !== undefined && user !== null">
    <slot /
>
  </template>

  <template v-else>
    <p>Not signed in.</
p>
  </template>
</
template>

存储文档


Juno提供了一个名为“Datastore”的功能,旨在将数据直接存储在区块链上。Datastore 由一组集合组成,其中每个集合保存文档,这些文档由您选择的键唯一标识。


在本教程中,我们的目标是存储笔记,因此必须按照文档中提供的说明创建一个集合。为集合选择合适的名称,例如“notes”。


一旦设置好应用程序并创建了必要的集合,就可以利用库的 setDoc 函数将数据持久化到区块链上。此功能使您能够安全且不变地存储笔记。


import { setDoc } from "@junobuild/core";

// TypeScript example from the documentation
await setDoc<Example>({
  collection"my_collection_key",
  doc: {
    key"my_document_key",
    data: myExample,
  },
});

由于集合中的文档是通过唯一的密钥来标识的,因此我们使用 nanoid[5] 来创建密钥--这是一种用于 JavaScript 的微型字符串 ID 生成器。


<script lang="ts" setup>
import { ref } from 'vue'
import { setDoc } from '@junobuild/core'
import { nanoid } from 'nanoid'

const inputText = ref('')

const add = async () => {
  const key = nanoid()

  await setDoc({
    collection'notes',
    doc: {
      key,
      data: {
        text: inputText.value
      }
    }
  })
}
</script>

<template>
  <textarea rows="5" placeholder="Your diary entry" 
            v-model="inputText">
</textarea>

  <button @click="add">Add</button>
</template>


请注意,为简单起见,本教程提供的代码片段不包括适当的错误处理,也不包括复杂的表单处理。



检索文档列表


为了检索存储在区块链上的文档集合,我们可以使用库的 listDocs 函数。这个多功能函数允许加入各种参数,以方便数据过滤、排序或分页。


出于本教程的目的,我们将保持示例的简约性。我们的目标是在挂载组件时简单地列出所有用户数据。


<script lang="ts" setup>
import { listDocs } from '@junobuild/core'
import { onMounted, ref } from 'vue'

const items = ref([])

const list = async () => {
  const { items: data } = await listDocs({
    collection'notes'
  })

  items.value = data
}

onMounted(async () => await list())
</script>

<template>
  <p v-for="(item, index) in items">
    <span>
      {{ index + 1 }}
    </span>
    <span>{{ item.data.text }}</span>
  </p>
</template>

文件上传


在去中心化网络上存储数据是一项复杂的任务。然而,Juno 的设计旨在为需要轻松存储和检索用户生成内容(如照片或文件)的应用程序开发人员简化这一过程。


在处理文档时,第一步是按照文档[6]中提供的说明创建一个集合。在本教程中,我们将重点实施图片上传,因此该集合可以恰当地命名为 “images”。


为确保存储数据的唯一性和正确识别,每个文件都有唯一的文件名和路径。这一点非常重要,因为数据是在网络上提供的,每条数据都应该有一个独特的 URL。


要实现这一点,我们可以使用用户唯一ID的文本表示形式和每个上传文件的时间戳的组合来创建一个键。通过访问我们之前在存储中声明的属性,我们可以检索相应的用户键。


<script lang="ts" setup>
import { ref } from 'vue'
import { useAuthStore } from '@/stores/auth.store'
import { storeToRefs } from 'pinia'
import { uploadFile } from '@junobuild/core'

const file = ref(undefined)

const store = useAuthStore()
const { user } = storeToRefs(store)

const setFile = (f) => (file.value = f)

const upload = async () => {
  // Demo purpose therefore edge case not properly handled
  if ([nullundefined].includes(user.value)) {
    return
  }

  const filename = `${user.value.key}-${file.value.name}`

  const { downloadUrl } = await uploadFile({
    collection'images',
    data: file.value,
    filename
  })

  console.log('Uploaded', downloadUrl)
}
</script>

<template>
  <input type="file" @change="(event) => setFile(event.target.files?.[0])" />

  <button @click="upload">Upload</button>
</template>

一旦一个资源被上传,一个 downloadUrl 返回,它提供了一个直接的 HTTPS 链接,可以在web上访问上传的资源。


列出资源


为了检索存储在区块链上的资产集合,我们可以利用库提供的 listAssets 函数。这个函数在参数方面提供了灵活性,允许我们根据需要对文件进行过滤、排序或分页。


与前面的文档示例类似,我们将保持这个示例的简约性。


<script lang="ts" setup>
import { listAssets } from '@junobuild/core'
import { onMounted, ref } from 'vue'

const assets = ref([])

const list = async () => {
  const { assets: images } = await listAssets({
    collection'images'
  })

  assets.value = images
}

onMounted(async () => await list())
</script>

<template>
  <img loading="lazy" :src="asset.downloadUrl" v-for="asset in assets" />
</template>

部署 🚀


在开发和构建应用程序之后,下一步是将其部署到区块链上。为此,您需要在终端中执行以下命令来安装 Juno 命令行接口(CLI):


npm i -g @junobuild/cli

安装过程完成后,您可以按照文档[7]中的说明并从终端登录来访问您的 satellite。这将使你的机器能够控制你的 satellite。


juno login

最后,您可以使用以下命令部署项目:


juno deploy

恭喜你!您的 Vue dapp 现在已经上线,并完全由区块链提供支持。🎉


资源





原文:https://betterprogramming.pub/build-a-web3-app-with-vuejs-db1503ca20d2


是哒是哒说


参考资料


[1]

React: https://betterprogramming.pub/build-a-web3-app-with-react-js-6353825baf9a

[2]

Angular: https://levelup.gitconnected.com/develop-an-angular-app-on-blockchain-9cde44ae00b7

[3]

Internet Computer: https://internetcomputer.org/

[4]

文档: https://juno.build/docs/add-juno-to-an-app/create-a-satellite

[5]

nanoid: https://github.com/ai/nanoid

[6]

文档: https://juno.build/docs/build/storage#collections-and-rules

[7]

文档: https://juno.build/docs/miscellaneous/cli#login

[8]

https://juno.build/docs/intro: https://juno.build/docs/intro

[9]

GitHub 代码库: https://github.com/buildwithjuno/examples/tree/main/vue/diary



作者:程序员张张
来源:mdnice.com/writing/26615feb73924bb4821f543e0f041fa4
收起阅读 »

前端开发如何给自己定位?初级?中级?高级!

web
引言 在快速发展的互联网时代,前端开发一直处于高速增长的趋势中。作为构建用户界面和实现交互功能的关键角色,前端开发人员需要不断提升自己的技能和能力,以适应变化的行业需求。本文将为前端开发人员提供一个能力定位指南,帮助他们了解自己在前端领域的定位,内容参考阿里前...
继续阅读 »

引言


在快速发展的互联网时代,前端开发一直处于高速增长的趋势中。作为构建用户界面和实现交互功能的关键角色,前端开发人员需要不断提升自己的技能和能力,以适应变化的行业需求。本文将为前端开发人员提供一个能力定位指南,帮助他们了解自己在前端领域的定位,内容参考阿里前端面试指南,P6/P6+/P7的能力标准。


目录



0.掌握图形学,webgl或熟练使用threejs框架,熟练canvas相关渲染及动画操作的优先。

1.熟练掌握JavaScript。

2.熟悉常用工程化工具,掌握模块化思想和技术实现方案。

3.熟练掌握React前端框架,了解技术底层。同时了解vue以及angular等其他框架者优先。

4.熟练掌握react生态常用工具,redux/react-router等。

5.熟悉各种Web前端技术,包括HTML/XML/CSS等,有基于Ajax的前端应用开发经验。

6.有良好的编码习惯,对前端技术有持续的热情,个性乐观开朗,逻辑性强,善于和各种背景的人合作。

7.具有TS/移动设备上前端开发/NodeJS/服务端开发等经验者优先。



0.掌握图形学,webgl或熟练使用threejs框架,熟练canvas相关渲染及动画操作的优先。


初级:



  • 学习过图形学相关知识,知道矩阵等数学原理在动画中的作用,知道三维场景需要的最基础的构成,能用threejs搭3d场景,知道webgl和threejs的关系。

  • 知道canvas是干嘛的,聊到旋转能说出canvas的api。

  • 知道css动画,css动画属性知道关键字和用法(换句话说,电话面试会当场出题要求口喷css动画,至少能说对大概,而不是回答百度一下就会用)。

  • 知道js动画,能说出1~2个社区js动画库,知道js动画和css动画优缺点以及适用场景。

  • 知道raf和其他达到60fps的方法。


中级:



  • 如果没有threejs,你也能基于webgl自己封装一个简单的threejs出来。

  • 聊到原理能说出四元数,聊到鼠标操作能提到节流,聊到性能能提到restore,聊到帧说出raf和timeout的区别,以及各自在优化时候的作用。

  • 知道怎样在移动端处理加载问题,渲染性能问题。

  • 知道如何结合native能力优化性能。

  • 知道如何排查性能问题。对chrome动画、3d、传感器调试十分了解。


高级:



  • 搭建过整套资源加载优化方案,能说明白整体方案的各个细节,包括前端、客户端、服务端分别需要实现哪些功能点、依赖哪些基础能力,以及如何配合。

  • 设计并实现过前端动画引擎,能说明白一个复杂互动项目的技术架构,知道需要哪些核心模块,以及这些模块间如何配合。

  • 有自己实现的动画相关技术方案产出,这套技术方案必须是解决明确的业务或技术难点问题的。为了业务快速落地而封装一个库,不算这里的技术方案。如果有类似社区方案,必须能从原理上说明白和竞品的差异,各自优劣,以及技术选型的原因。


1.熟练掌握JavaScript。


初级:



  • JavaScript各种概念都得了解,《JavaScript语言精粹》这本书的目录都得有概念,并且这些核心点都能脱口而出是什么。这里列举一些做参考:

  • 知道组合寄生继承,知道class继承。

  • 知道怎么创建类function + class。

  • 知道闭包在实际场景中怎么用,常见的坑。

  • 知道模块是什么,怎么用。

  • 知道event loop是什么,能举例说明event loop怎么影响平时的编码。

  • 掌握基础数据结构,比如堆、栈、树,并了解这些数据结构计算机基础中的作用。

  • 知道ES6数组相关方法,比如forEach,map,reduce。


中级:



  • 知道class继承与组合寄生继承的差别,并能举例说明。

  • 知道event loop原理,知道宏微任务,并且能从个人理解层面说出为什么要区分。知道node和浏览器在实现loop时候的差别。

  • 能将继承、作用域、闭包、模块这些概念融汇贯通,并且结合实际例子说明这几个概念怎样结合在一起。

  • 能脱口而出2种以上设计模式的核心思想,并结合js语言特性举例或口喷基础实现。

  • 掌握一些基础算法核心思想或简单算法问题,比如排序,大数相加。


2.熟悉常用工程化工具,掌握模块化思想和技术实现方案。


初级:



  • 知道webpack,rollup以及他们适用的场景。

  • 知道webpack v4和v3的区别。

  • 脱口而出webpack基础配置。

  • 知道webpack打包结果的代码结构和执行流程,知道index.js,runtime.js是干嘛的。

  • 知道amd,cmd,commonjs,es module分别是什么。

  • 知道所有模块化标准定义一个模块怎么写。给出2个文件,能口喷一段代码完成模块打包和执行的核心逻辑。


中级:



  • 知道webpack打包链路,知道plugin生命周期,知道怎么写一个plugin和loader。

  • 知道常见loader做了什么事情,能几句话说明白,比如babel-loader,vue-loader。

  • 能结合性能优化聊webpack配置怎么做,能清楚说明白核心要点有哪些,并说明解决什么问题,需要哪些外部依赖,比如cdn,接入层等。

  • 了解异步模块加载的实现原理,能口喷代码实现核心逻辑。


高级:



  • 能设计出或具体说明白团队研发基础设施。具体包括但不限于:

  • 项目脚手架搭建,及如何以工具形态共享。

  • 团队eslint规范如何设计,及如何统一更新。

  • 工具化打包发布流程,包括本地调试、云构建、线上发布体系、一键部署能力。同时,方案不仅限于前端工程部分,包含相关服务端基础设施,比如cdn服务搭建,接入层缓存方案设计,域名管控等。

  • 客户端缓存及预加载方案。


3.熟练掌握React前端框架,了解技术底层。同时了解vue以及angular等其他框架者优先。


初级:



  • 知道react常见优化方案,脱口而出常用生命周期,知道他们是干什么的。

  • 知道react大致实现思路,能对比react和js控制原生dom的差异,能口喷一个简化版的react。

  • 知道diff算法大致实现思路。

  • 对state和props有自己的使用心得,结合受控组件、hoc等特性描述,需要说明各种方案的适用场景。

  • 以上几点react替换为vue或angular同样适用。


中级:



  • 能说明白为什么要实现fiber,以及可能带来的坑。

  • 能说明白为什么要实现hook。

  • 能说明白为什么要用immutable,以及用或者不用的考虑。

  • 知道react不常用的特性,比如context,portal。

  • 能用自己的理解说明白react like框架的本质,能说明白如何让这些框架共存。


高级:



  • 能设计出框架无关的技术架构。包括但不限于:

  • 说明如何解决可能存在的冲突问题,需要结合实际案例。

  • 能说明架构分层逻辑、各层的核心模块,以及核心模块要解决的问题。能结合实际场景例举一些坑或者优雅的处理方案则更佳。


4.熟练掌握react生态常用工具,redux/react-router等。


初级:



  • 知道react-router,redux,redux-thunk,react-redux,immutable,antd或同级别社区组件库。

  • 知道vue和angular对应全家桶分别有哪些。

  • 知道浏览器react相关插件有什么,怎么用。

  • 知道react-router v3/v4的差异。

  • 知道antd组件化设计思路。

  • 知道thunk干嘛用的,怎么实现的。


中级:



  • 看过全家桶源码,不要求每行都看,但是知道核心实现原理和底层依赖。能口喷几行关键代码把对应类库实现即达标。

  • 能从数据驱动角度透彻的说明白redux,能够口喷原生js和redux结合要怎么做。

  • 能结合redux,vuex,mobx等数据流谈谈自己对vue和react的异同。


高级:



  • 有基于全家桶构建复杂应用的经验,比如最近很火的微前端和这些类库结合的时候要注意什么,会有什么坑,怎么解决


5.熟悉各种Web前端技术,包括HTML/XML/CSS等,有基于Ajax的前端应用开发经验。


初级:



  • HTML方面包括但不限于:语义化标签,history api,storage,ajax2.0等。

  • CSS方面包括但不限于:文档流,重绘重排,flex,BFC,IFC,before/after,动画,keyframe,画三角,优先级矩阵等。

  • 知道axios或同级别网络请求库,知道axios的核心功能。

  • 能口喷xhr用法,知道网络请求相关技术和技术底层,包括但不限于:content-type,不同type的作用;restful设计理念;cors处理方案,以及浏览器和服务端执行流程;口喷文件上传实现;

  • 知道如何完成登陆模块,包括但不限于:登陆表单如何实现;cookie登录态维护方案;token base登录态方案;session概念;


中级:



  • HTML方面能够结合各个浏览器api描述常用类库的实现。

  • css方面能够结合各个概念,说明白网上那些hack方案或优化方案的原理。

  • 能说明白接口请求的前后端整体架构和流程,包括:业务代码,浏览器原理,http协议,服务端接入层,rpc服务调用,负载均衡。

  • 知道websocket用法,包括但不限于:鉴权,房间分配,心跳机制,重连方案等。

  • 知道pc端与移动端登录态维护方案,知道token base登录态实现细节,知道服务端session控制实现,关键字:refresh token。

  • 知道oauth2.0轻量与完整实现原理。

  • 知道移动端api请求与socket如何通过native发送,知道如何与native进行数据交互,知道ios与安卓jsbridge实现原理。


高级:



  • 知道移动端webview和基础能力,包括但不限于:iOS端uiwebview与wkwebview差异;webview资源加载优化方案;webview池管理方案;native路由等。

  • 登陆抽象层,能够给出完整的前后端对用户体系的整体技术架构设计,满足多业务形态用户体系统一。考虑跨域名、多组织架构、跨端、用户态开放等场景。

  • mock方案,能够设计出满足各种场景需要的mock数据方案,同时能说出对前后端分离的理解。考虑mock方案的通用性、场景覆盖度,以及代码或工程侵入程度。

  • 埋点方案,能够说明白前端埋点方案技术底层实现,以及技术选型原理。能够设计出基于埋点的数据采集和分析方案,关键字包括:分桶策略,采样率,时序性,数据仓库,数据清洗等。


6.有良好的编码习惯,对前端技术有持续的热情,个性乐观开朗,逻辑性强,善于和各种背景的人合作。


初级:



  • 知道eslint,以及如何与工程配合使用。

  • 了解近3年前端较重要的更新事件。

  • 面试过程中遇到答不出来的问题,能从逻辑分析上给出大致的思考路径。

  • 知道几个热门的国内外前端技术网站,同时能例举几个面试过程中的核心点是从哪里看到的。


高级:



  • 在团队内推行eslint,并给出工程化解决方案。

  • 面试过程思路清晰,面试官给出关键字,能够快速反应出相关的技术要点,但是也要避免滔滔不绝,说一堆无关紧要的东西。举例来说,当时勾股老师面试我的时候,问了我一个左图右文的布局做法,我的回答是:我自己总结过7种方案,其中比较好用的是基于BFC的,float的以及flex的三种。之后把关键css口喷了一下,然后css就面完了。


7.具有TS/移动设备上前端开发/NodeJS/服务端开发等经验者优先。



  • 根据了解的深度分初/中/高级。

  • 知道TS是什么,为什么要用TS,有TS工程化实践经验。

  • 知道移动端前端常见问题,包括但不限于:rem + 1px方案;预加载;jsbridge原理等。

  • 能说出大概的服务端技术,包括但不限于:docker;k8s;rpc原理;中后台架构分层;缓存处理;分布式;响应式编程等。


5. 结论与进一步学习


本文为前端开发人员提供了一个能力定位指南,帮助他们了解自己在前端领域的定位,并提供了具体的代码示例来巩固学习成果。通过不断学习和实践,前端开发人员可以逐步提升自己的能力,从初级到中级再到高级。但请注意,在实际工作中,不同公司和项目对于各个级别的要求可能会有所不同。


为了进一步提高自己的水平,前端开发人员可以考虑以下学习路径和资源:



  • 阅读官方文档和教程,如MDN、React官方文档等;

  • 参与开源项目,并与其他开发人员进行交流和合作;

  • 关注前端开发的博客和社区,如Medium、Stack Overflow等;

  • 参加在线或线下的前端开发培训课程;

  • 阅读经典的前端开发书籍,如《JavaScript高级程序设计》、《CSS权威指南》等。


通过持续学习和实践,相信每个前端开发人员都可以不断成长,并在前端领域中取得更好的成就。祝愿大家在前端开

作者:Jony_men
来源:juejin.cn/post/7259961208794628151
发的道路上越走越远!

收起阅读 »

给同学解决问题有感——天下前端是一家!

web
   在毕设如火如荼进行的过程中,大家设计xxx系统时都会有各种各样的界面,这不就到了本菜鸟的领域!hhh,小时候的画家梦也算实现了一半,只不过画笔变成了code~    最近,给两位同学解决了前端方面的问题,但都不是我学的javascript语言,摸索着平时...
继续阅读 »

   在毕设如火如荼进行的过程中,大家设计xxx系统时都会有各种各样的界面,这不就到了本菜鸟的领域!hhh,小时候的画家梦也算实现了一半,只不过画笔变成了code~


   最近,给两位同学解决了前端方面的问题,但都不是我学的javascript语言,摸索着平时学到的前端思想,还是成功的解决了这些问题,有感而发,记录下来~



  •    第一位出场的是正在自学python的学习委员,也是一位准研究生。他遇到的问题是,在a项目里定义了一个复杂界面,在b项目里定义了一个简单页面。他找到我的时候说,启动了b项目,但打开的却是a项目定义的页面。报错如下:


d60d9f2414f5a2867091a5a14db6e54.png
    看了他的页面,这路由和我学的不长得一毛一样!


8bb4fc7bab1580d1baa6692d0f2b801.png
  打开他的浏览器页面,看看页面的网络请求,404,我第一反应会不会是他路由的问题,导致找不到这个页面,显示了之前项目的界面。。。


dc111956dfdb5bc954fd74c3bfdea11.png
  但我转念一想,404 应该不会显示另一个项目的界面呀,除非请求的是之前项目的服务器。再注意到warning中的,use a server instead,这不就是换一个服务器,那一定是之前的端口被占用了,所以相当于请求之前的服务器。于是,搬出来我只会一个cd的小黑窗:


1684658842987.png
   解决占用之后,重启项目,完整的展示了新项目中的页面~
这个问题准确的说属于计网,但前后端的思想还是在里面。果然还是基础的东西~



  • 第二位出场的是一位在做安卓应用(毕设项目)的女同学,躺在床上收到她的连环问,导航栏隐藏?我直接惊坐起:


1684659076469.png
   仔细听了她的描述之后,在有了导航栏之后,页面某些按钮的位置发生了偏差,如下图:


4637c8740c325359481bc6fb139af42.png
  原本卡其色圆圈应该和下面蓝色圆圈重合,通关之后,显示下面的颜色。
因为下面一层的按钮是嵌套的背景图片里,所以不知道固定位置,不能用绝对定位控制两个按钮的位置完全重合(她是用可视化拖动的方法做的页面)。
让导航栏透明肯定是有办法的,但我想如果只透明 但仍然占据文档流,那还是没用呀!


1684659743090.png
  找到网友的方法,尝试之后,模拟机显示确实ok,隐藏了导航栏,位置也消除了偏差,在鼠标接触到导航栏位置时,导航栏显现,很人性化!但她在手机上通过apk安装包查看,还是有一定的偏差,我想是因为屏幕尺寸的问题吗?看了他的代码用的都是相对单位dp,应该可以自适应的呀,这个就不懂了,毕竟适配所有机型的问题,我在实习的时候也很头疼!




PS:
在此应该鸣谢一下我的老师和队友,坚定让我自己做了前后端分离的一个项目,自己建数据库,自己写接口,对前后端请求的
作者:MindedMier
来源:juejin.cn/post/7235458133505491005
发送接收还是有更细致的了解!
收起阅读 »

这一篇浏览器事件循环,可能会颠覆部分人的对宏任务和微任务的理解🤪🤪🤪

web
在这两天里看到一篇文章,发现好像很多人都把事件循环给搞混了,到底是宏任务先执行还是微任务先执行。在写这篇文章之前,我也随机挑选了几位幸运观众来问这个问题,好像大多都是说微任务先执行。 那么从这篇文章里,我们就来探讨一下到底是哪个先执行。 什么是进程 进程是计算...
继续阅读 »

在这两天里看到一篇文章,发现好像很多人都把事件循环给搞混了,到底是宏任务先执行还是微任务先执行。在写这篇文章之前,我也随机挑选了几位幸运观众来问这个问题,好像大多都是说微任务先执行。


那么从这篇文章里,我们就来探讨一下到底是哪个先执行。


什么是进程


进程是计算机系统中正在运行的程序的实例。它是操作系统对一个正在运行的程序的抽象表示,负责管理程序的执行和资源分配。


通常它包括堆栈,例如临时数据,如函数参数、返回地址和局部变量和数据段,其中数据段包括全局变量。


进程还可能包括堆,这是在进程运行时动态分配的内存。在 JavaScript 中,堆和栈的内存分配是通过不同方式进行的:




  1. 堆内存分配:



    • JavaScript 中的对象、数组和函数等复杂数据类型都存储在堆内存中;

    • 使用 new 关键字或对象字面量语法创建对象时,会在堆内存中动态分配相应的内存空间;

    • 堆内存的释放由垃圾回收机制自动处理,当一个对象不再被引用时,垃圾回收机制会自动回收其占用的堆内存,释放资源;




  2. 栈内存分配:



    • JavaScript 中的基本数据类型,如数字、布尔值和字符串以及函数的局部变量保存在栈内存中;

    • 栈内存的分配是静态的,编译器在编译阶段就确定了变量的内存空间大小;

    • 当函数被调用时,会在栈内存中创建一个称为栈帧 stack frame 的数据结构,用于存储函数的参数、局部变量、返回地址等信息;

    • 当函数执行完毕或从函数中返回时,对应的栈帧会被销毁,栈内存中的数据也随之释放;




在操作系统中,每个进程都有自己的地址空间、状态和控制信息。进程可以独立运行,与其他进程离开来,互不干扰。它们可以同时进行,并通过进程间通信机制进行交互。


进程在执行时会改变状态。进程状态,部分取决于进程的当前活动。每个进程可能处于以下状态:



  • 新的: 进程正在创建;

  • 运行: 指令正在运行;

  • 等待: 进程等待发生某个时间,如 I/O 完成或收到信号;

  • 就绪: 进程等待分配处理器;

  • 终止: 进程已经完成执行;


下图完整地显示了一个状态图:
20230725131814


什么是线程


线程是进程中的一个执行路径,是进程的组成部分。在同一个进程中的多个线程共享进程的资源,如内存空间和文件句柄等。不同线程之间可以并发执行,各自堵路地完成特定的任务。


进程和线程之间的关系如下:



  • 一个进程可以创建多个线程,这些线程共享同一个地址空间和资源,能够并发地执行任务;

  • 线程是在进程内部创建和销毁的,它们与进程共享进程的上下文,包括打开的文件、全局变量和堆内存等;

  • 每个进程至少包含一个主线程,主线程用于执行进程的主要业务逻辑。其他线程可以作为辅助线程来完成特定的任务;


主线程是一个程序中的特殊线程,它是程序的入口点和主要执行线程。


在许多编程语言和操作系统中,主线程是程序启动后自动创建的线程,负责执行程序的主要业务逻辑。主线程会按照顺序执行代码,从程序的入口点开始,直到程序结束或主线程显式终止。


你可以理解成一个篮球场,整个篮球场就是一个进程,就是一块能供线程使用的内存。


而线程就是篮球场上的每一个队员,每个人都有不同的职责。


Chrome: 多进程架构浏览器


许多网站包含活动内容,如 JavaScriptFlushHTML5 等,以便提供丰富的、动态的 Web 浏览体验。遗憾的是,这些 Web 应用程序也可能包含软件缺陷,从而导致响应迟滞,有的甚至网络浏览器崩溃。如果一个 Web 浏览器只对一个网站进行浏览,那么这不是一个大问题。


但是,现代 Web 浏览器提供标签是浏览,它运行 Web 浏览器的一个实例,同时打开多个网站,而每个标签代表一个网站。要在不同网站之间切换,用户只需点击响应的标签。这种安排如下图所示:


20230725154126


这种方法的一个问题是: 如果任何标签的 Web 应用程序崩溃,那么整个进程,包括所有其他标签所显示的网站也会崩溃。


GoogleChrome Web 浏览器通过多进程架构的设计解决这以问题。Chrome 具有多种不同类型的进程,这里我们主要讲讲其中三个:



  • 浏览器进程: 主要负责界面显示、用户交互、子进程管理,同时提供存储等功能;

  • 网络进程: 网络进程负责处理浏览器内发起的所有网络请求,例如加载网页、资源文件,如图片、CSS JavaScriptXMLHttpRequest 和 Fetch API 等请求;

  • 渲染进程: 主要负责渲染网页的逻辑。主要处理 HTML、Javascript、图像等等。一般情况下,对应于新标签的每个网站都会创建一个新的渲染进程。因此,可能会有多个渲染进程同时活跃;


如下图所示:
20230725155622


例如我只打开了两个标签也,浏览器就会开辟两个两个不同的进程,两者之间相互独立,一个崩掉了不会另外一个。



这个目前是这样子的,但是后续可能会改,根据 chrome 文档说明,后期可能会修改成每个站点开启一个进程,例如,你访问 b 站,而再 b 站里面的所有页面都不会开启新的渲染进程了,详情请看 chrome 官方文档



渲染主线程是如何工作的


渲染主线程是浏览器中最繁忙的线程,需要它处理的任务包括但不限于:



  • 解析 HTML;

  • 解析 CSS;

  • 计算样式;

  • 布局;

  • 处理图层;

  • 每秒把⻚面画 60 次;

  • 执行全局 JavaScript 代码;

  • 执行事件处理函数;

  • 执行计时器的回调函数;


等等事情,当浏览器的网络线程收到 HTML 文档后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列,在事件循环的作用下,渲染主线程取出消息队列中的渲染任务,并开启渲染流程。整个过程分为多个阶段,分别是: 解析 HTML、样式计算、布局、分层、绘制、分块、光栅化、画,每个阶段都有明确的输入输出,上一个阶段的输出会成为下一个阶段的输入。


首先解析的是 HTML 文档,这里我们忽略 CSS 的文件,如果主线程解析到 script 位置,会停止解析 HTML,转而等待 JavaScript 文件下载好,主线程将 JavaScript 代码解析执行完成后,才能继续解析 HTML。这是因为 JavaScript 代码的执行过程中可能会修改当前的 DOM 树,所以 DOM 树的生成必须暂停。这就是 JavaScript 会阻塞 HTML 解析的根本原因。
20230726082307



前面的这句话中有两个关键字很关键字,画好重点,它们分别是 下载解析



要处理这么多的事情,浏览器给渲染进程采用了多线程,它主要包含了以下线程:



  • 主线程: 主线程负责解析 HTMLCSSJavaScript,构建 DOM 树、CSSOM 树和渲染树,并进行页面布局和绘制。它还处理用户交互,执行 JavaScript 代码以及其他页面渲染相关的任务;

  • 合成线程: 合成线程负责将渲染树转换为图层,并执行图层的合成操作;

  • 网络线程: 网络线程负责处理网络请求和数据传输。当浏览器需要加载网页、图片或其他资源时,网络线程负责发送请求并接收响应数据;

  • 定时器线程: 定时器线程负责管理定时器事件,包括 setTimeoutsetInterval 等。它用于在指定的时间间隔内触发预定的任务;

  • 事件处理线程: 当用户进行交互操作,如点击按钮、滚动页面、输入文本等,需要触发相应的事件处理函数;


由于主线程和合成线程是并行执行的,这就可能导致这两个线程之间存在数据交互的问题。例如,当主线程和合成线程都需要访问相同的共享资源时,就需要进行同步,以避免竞态条件等问题。


这里就设计到消息队列的作用: 主线程和合成线程之间通过消息队列进行通信。主线程将渲染任务和图层数据等信息封装成消息,并将消息放入消息队列中。合成线程从消息队列中获取消息,并执行相应的图层合成操作。


浏览器中出现消息队列是为了处理异步任务和事件。在浏览器当中,有许多人任务是在后台执行或者将来某个事件触发时才执行的,例如:



  • 异步操作: 比如通过 AJAX 请求从服务器获取数据、读取本地文件等,这些操作需要等待网络请求或者文件读取完成后再处理响应的数据;

  • 定时器: 通过 setTimeoutsetInterval 设置的定时器任务,需要在指定的时间间隔后执行;

  • 事件处理: 当用户进行交互操作,如点击按钮、滚动页面、输入文本等,需要触发相应的事件处理函数;


20230726091257


如上图所示,当渲染主线程正在执行一个 JavaScript 函数,执行到一半的时候用户点击了按钮或者碰到了一个定时器,也就是 setTimeout。因为在我们的渲染进程里面是有定时器线程的,定时器线程监听到有这个定时器操作。那么该线程会将 setTimeout 里面的事件处理函数 (setTimeout 的第一个回调函数) 作为一个任务拿去排队。


因为消息队列采用的是队列的数据结构,当渲染主线程将所有任务情况之后,然后从消息队列中拿去最旧的那个任务,假设消息队列之前没有任务的情况下,就拿出 setTimeout 这个事件处理函数。如果在该函数当中又遇到了类似的事件处理函数或者定时器,按照前面的步骤。依此循环,直到所有任务执行完成。


整个过程,就被称之为事件循环。


什么是异步


JavaScript 是一门单线程的编程语言.意味着在一个特定的时间点,只能有一个代码块在执行。当执行一个同步任务时,如果任务需要很长时间才能完成,如网络请求、文件读取等,整个程序会被阻塞,导致用户界面无响应,甚至造成卡顿的问题。这种情况在 Web 应用中尤其常见,因为 JavaScript 经常与网络请求、DOM 操作等耗时任务打交道。


常见的异步操作包括:



  • 网络请求: 发送 HTTP 请求并等待服务器响应时,通常使用异步方式,以允许程序继续执行其他操作;

  • 定时器: 设置定时器,在一段时间后执行某个任务,也是异步操作的一种;

  • 事件处理: 为 DOM 元素注册事件监听器是一种常见的异步任务。当特定事件触发时,相应的事件处理函数将被异步调用,例如 addEventListener


渲染主线程负责处理网页的构建、布局、绘制和用户交互等任务,而异步编程使得我们可以在主线程执行同步代码的同时,处理耗时的异步操作,例如网络请求、文件读写等,以提高程序的性能和用户体验。在 JavaScript 中,通过事件循环机制,异步编程实现了一种非阻塞的执行方式,使得浏览器能够高效地处理各种任务,同时保持用户界面的响应性。


例如你要执行一个 setTimeout:


setTimeout(() => {
console.log(111);
}, 3000);

console.log(222);

在这段代码当中,如果让渲染主线程去等待这个定时器任务执行完再去执行下一个任务,就会导致主线程长期处于阻塞的状态,从而导致浏览器页面长期见不到效果,可能要砸电脑了。


20230726095635



整个流程你可以理解为这样,但也可能不完全正确。



等到整个计时结束,再执行 console.log(222) 的代码,这种模式就叫作同步。整个时候消息队列还有很多任务在等待,可能还存在一些渲染页面的任务,有可能直接导致整个页面卡死。


所以为了这个问题,浏览器采用了异步的方式来解决这个问题,因为渲染主线程承担着及其重要的工作,无论如何都不能阻塞。


20230726100527


当计时开始之后,我就不管你了,就例如你是服务,餐厅里面来了客人,客人点完了菜,你把菜单交给后厨,你就可以先不管了,继续服务下一个客人,也就是从消息队列中拿下一个任务。当计时结束之后,会把该回调函数放入到消息队列末尾.等到剩下的任务完成之后,你就可以给客人端菜了。


任务优先级


任务没有优先级,在消息队列中先进先出,但消息队列是有优先级的。


事件循环在过去的说法中,任务分为两个队列,一个是宏任务,一个是微任务。但是现在已经没有了宏任务的说法了。



因为我在年初的时候就写过相关事件循环的文章,且有在 mdn 上搜索过宏任务的相关概念,但现在在 mdn 已经完全搜索不到了。



根据 W3C 的最新解释:



  • 每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列。在一次事件循环当中,浏览器可以根据实际情况从不同的队列中取出任务执行;

  • 浏览器必须准备好一个微任务队列,微队列中的任务优先所有其他任务执行;


相关 W3C 连接


Chrome 的实现中,至少包含了下面的队列:



  • 延时队列: 用于存放计时器到达后的回调任务,优先级 ;

  • 交互队列: 用于存放用户操作后产生的事件任务,优先级 ;

  • 微队列: 用户存放需要最快执行的任务,优先级最高;


虽然浏览器最新规范是这样,但是你用之前的宏任务和微任务去答题也完全没有问题的,但是输出的顺序是完全没有变的,况且这篇文章主要内容也不是讲这个,那么在之后的代码中我们就继续以宏任务和微任务的来讲。


宏任务和微任务 重点来啦!!!


宏任务是一组异步任务,这些任务通常由浏览器的事件触发器发起,并在主线程中按照顺序执行。常见的宏任务包括:



  • setTimeoutsetInterval;

  • I/O 操作,例如读取文件、网络请求;

  • DOM 事件,例如点击事件、输入事件;

  • requestAnimationFrame;

  • script 标签;


微任务是一个细微的异步任务,它的执行时机在宏任务之后、渲染之前。微任务通常在一个宏任务执行完毕后立即执行,而不需要等待其他宏任务。这使得微任务的执行优先级比宏任务高。常见的微任务包括:



  • Promiseresolvereject 回调;

  • async/await 中的异步函数;

  • MutationObserver;


很重要的一点来了,为什么说 script 标签是宏任务呢?


如果忘记了,你再看看我们前面中说到的 下载解析 两个关键字。


script 标签包含的 JavaScript 代码在浏览器中执行时,被认为是宏任务。这是因为执行 script 标签内的代码需要进行一系列的操作,包括解析、编译和执行。主要有以下几个理由:



  • 解析和编译: 当浏览器遇到 script 标签时,它会停止当前的文档解析过程,并开始解析 script 内的 JavaScript 代码。解析器将逐行读取代码,并将其转换为可执行的内部表示形式。这个解析和编译的过程是一个比较耗时的操作,需要占用大量的 CPU 资源;

  • 阻塞页面渲染: 由于脚本的执行通常会修改当前页面的结构和样式,浏览器必须等待脚本执行完毕后再进行页面的渲染。也就是说,当浏览器执行 script 标签时,它会阻塞页面的渲染,直到脚本执行完毕才会继续渲染;

  • 可能引起网络请求:在 script 标签中,可以使用外部的 JavaScript 文件引用,例如 <script src="example.js"></script>。当浏览器遇到这样的情况时,它会发起一个网络请求去下载该文件,并等待文件下载完成后再执行。网络请求通常是一个比较耗时的操作,因此将其作为宏任务可以确保脚本的执行按照正确的顺序进行;


总结起来,script 标签被认为是宏任务是因为它需要解析、编译和执行 JavaScript 代码,并且会阻塞页面的渲染。此外,如果使用了外部 JavaScript 文件,还可能引起网络请求,进一步增加了执行时间。这些特性使得 script 标签的执行与其他微任务(如 Promise)不同,被归类为宏任务。


如果你依然觉得理由不够充分的话,请看以下代码:


<!-- 脚本 1 -->
<script>
// 同步
console.log("start1");
// 异步宏
setTimeout(() => console.log("timer1"), 0);
new Promise((resolve, reject) => {
// 同步
console.log("p1");
resolve();
}).then(() => {
// 异步微
console.log("then1");
});
// 同步
console.log("end1");
</script>

<!-- 脚本 2 -->
<script>
// 同步
console.log("start2");
// 异步宏
setTimeout(() => console.log("timer2"), 0);
new Promise((resolve, reject) => {
// 同步
console.log("p2");
resolve();
}).then(() => {
// 异步微
console.log("then2");
});
// 同步
console.log("end2");
</script>

该代码的输出结果如下所示:
20230726105526


如果 script 标签不是宏任务,普通任务的话,是不是应该先执行 start2end2 再执行 then1


所以根据此结论,整个浏览器循环应该是 先执行 宏任务 -> 同步代码 -> 微任务,直到当前宏任务中的微任务清理完毕,继续执行下一个宏任务,以此类推。


最后我再抛出一个问题,没有你这个 script 这个宏任务的出现,你哪来的微任务?


一个事件循环过程模型如下,当调用栈为空时,执行以下步骤:



  1. 选择任务队列中最旧的任务(队列是一个先进先出的队列,最旧的那个就是最先进的,这里是 任务 A);

  2. 如果任务 A为空(意味着任务队列为空),跳转到第6步;

  3. 将当前运行的任务设置为任务 A;

  4. 运行任务 A,意味着运行回调函数;

  5. 运行结束,将当前的任务设置为空,删除任务 A;

  6. 执行微任务队列:

    1. 选择微任务队列中最早的任务 X;

    2. 如果任务 X,代表这微任务为空,跳转到步骤6;

    3. 将当前运行的任务设置为任务 X,并运行该任务;

    4. 运行结束,将当前正在运行的任务设置为空,删除任务 X;

    5. 选择微任务队列中下一个最旧的任务,可以理解为第n+1个入队的,跳转到步骤2;

    6. 完成微任务队列;



  7. 跳转到第1步;


这个事件循环过程模型如下图所示:


image.png


值得注意的是,当一个任务在宏任务队列中正在运行时,可能会注册新事件,因此可能会创建新任务,下面是两个新创建的任务:



  • Promise.then(...) 是一个回调任务:当 promisefulfilled/rejected:任务将被推入当前轮事件循环中的微任务队列;当promisepending:任务将在下一轮事件循环中被推入微任务队列(可能是下一轮);


案例


接下来我们通过一些案例来加深对事件循环的理解。


案例一


setTimeout(() => {
console.log("time1");

new Promise((resolve) => {
resolve();
}).then(() => {
new Promise((resolve) => {
resolve();
}).then(() => {
console.log("then4");
});

console.log("then2");
});
});

new Promise((resolve) => {
console.log("p1");
resolve();
}).then(() => {
console.log("then1");
});

最后的输出结果为 p1 then1 time1 then2 then4,下面就来分析一下这个结果的由来:



  1. 代码首先遇到settimeout,是一个宏任务,里面的代码不会被执行;

  2. 接着代码往下执行,遇到 new Promise(...)中的回调函数是一个同步任务,直接执行;

  3. 直接输出 "p1",调用 resolve(),Promise 的状态变为 fuifilled,当 promise 状态变为 fulfilled/rejected时,任务将被推入当前轮事件循环中的微任务队列,所以后面的 then(...) 会被加入到微任务队列里面;

  4. 主线程中的同步代码执行完,从微任务中取出最旧的那个任务,也就是 then(...),输出 then1,此时微任务队列为空;

  5. 继续执行宏任务,也就是这个 settimeout,代码从上往下执行,首先输出 time1;

  6. 在下面的代码中又遇到了 new Promise(...),并且调用了 resolve(),then(...)被加入到微任务队列中,此时的同步任务已经执行完毕,直接执行这个 then(...);

  7. 又是遇到 new Promise(....),又是调用的 resolve(),所以 then() 方法会被添加到微任务队列中,代码往下执行,输出 "then2",此时微任务then(...)中的代码全部执行完毕;

  8. 此时同步任务执行完毕,继续执行微任务中的 then(...),输出 "then4";

  9. 所有代码运行完毕,程序结束;


案例二


<script>
console.log(1);

setTimeout(() => {
console.log(5);
});

new Promise((resolve) => {
resolve();
}).then(() => {
console.log(3);
});
console.log(2);
</script>

<script>
console.log(4);
</script>

这段代码的最后的输出结果是: 1 2 3 4 5,具体代码执行过程有以下步骤:
首先提醒一点,script 标签本身是一个宏任务,当页面出现多个 script 标签的时候,浏览器会把script 标签作为宏任务来解析。当前实例中两个 script 标签,它们会一次加入到宏任务队列中。



  1. console.log(...) 是同步代码,1首先会被输出,代码往下执行;

  2. 遇到 settimeout(),会被加入到宏任务队列中;

  3. then(...) 会被加入到微任务队列中,代码继续往下执行;

  4. console.log(...) 为同步认为输出 2;

  5. 此时同步任务执行完毕,转而执行微任务 then(...),输出 3;

  6. 当前宏任务执行完毕,此时同步任务和微任务都为空,取出最旧的宏任务,也就是第二个 script 标签;

  7. 输出 4,此时同步代码和微任务队列都为空,继续执行下一个宏任务,也就是 settimeout;

  8. 输出 5;


案例三


async function foo() {
console.log("start");
await bar();
console.log("end");
}

async function bar() {
console.log("bar");
}

console.log(1);

setTimeout(() => {
console.log("time");
});

foo();

new Promise((resolve) => {
console.log("p1");
resolve();
}).then(() => {
console.log("p2");
});

console.log(2);

这段代码的最后的输出结果是: 1 start bar p1 2 end p2 time,下面就来分析一下这段代码的执行过程:



  1. 前面两个是函数定义,不执行,遇到 console.log(),输出 1;

  2. 代码继续往下执行,遇到 settimeout(),代码加入到宏任务队列之中,代码往下执行;

  3. 调用 foo,输出 start;

  4. await 等待 bar() 调用的返回结果;

  5. 执行 bar() 函数,输出 bar;

  6. await 相当于 Promise.then(...),代码被加入到微任务队列中,所以 end 还不执行;

  7. 代码往下执行,遇到 new Promise(...),p1 直接输出,then() 又继续被加入到微任务队列中;

  8. 代码继续往下执行,遇到 console.log(2),输出 2;

  9. 此时主线程代码快为空,执行微任务队列中最旧的那个任务,继续执行 await 后续代码,输出 end;

  10. 执行 then() ,输出 p2;

  11. 最后执行 settimeout,输出 time;


案例四


Promise.resolve()
.then(() => {
console.log(0);
return Promise.resolve(4);
})
.then((res) => {
console.log(res);
});

Promise.resolve()
.then(() => {
console.log(1);
})
.then(() => {
console.log(2);
})
.then(() => {
console.log(3);
})
.then(() => {
console.log(5);
})
.then(() => {
console.log(6);
});

这个案例中,因为每一个 then() 都是一个微任务,所以首先执行的是0,代码继续往下执行,输出同级的 then(),也就是输出 1


如果 Promise 内返回的对象具有可调用的 then() 方法,则会在微任务队列中再插入一个任务,这就慢了一拍,如果这个 then() 方法是来源于 Promise 的,则因为是异步又慢了一拍,所以一共是慢了拍,所以 Promise.resolve(4) 的结果等到 23 输出完成,console.log(res) 的结果才会被输出;


所以该案例的最终结果输出的是 0 1 2 3 4 5 6


参考资料



总结


整个浏览器循环应该是 先执行 宏任务 -> 同步代码 -> 微任务,直到当前宏任务中的微任务清理完毕,继续执行下一个宏任务,以此类推。


最后分享两个我的两个开源项目,它们分别是:



这两个项目都会一直维护的,如果你也喜欢,欢迎 star 🥰🥰🥰


作者:Moment
来源:juejin.cn/post/7259927532249710653
收起阅读 »

今日算法09-青蛙跳台阶问题

web
一、题目描述 题目链接:leetcode.cn/problems/qi… 难易程度:简单 一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。 答案需要取模 1e9+7(1000000007),如计算初始结果为...
继续阅读 »

一、题目描述



题目链接:leetcode.cn/problems/qi…


难易程度:简单



一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。


答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。


示例1
输入:n = 2
输出:2

示例2
输入:n = 7
输出:21

示例3
输入:n = 0
输出:1




二、解题思路


动态规划


当 n 为 1 时,只有一种覆盖方法:





当 n = 2 时,有两种跳法:





跳 n 阶台阶,可以先跳 1 阶台阶,再跳 n-1 阶台阶;或者先跳 2 阶台阶,再跳 n-2 阶台阶。而 n-1 和 n-2 阶台阶的跳法可以看成子问题,该问题的递推公式为:





也就变成了斐波那契数列问题,参考:今日算法07-斐波那契数列


复杂度分析


时间复杂度 O(N) :计算 f(n) 需循环 n 次,每轮循环内计算操作使用 O(1) 。


空间复杂度 O(1) : 几个标志变量使用常数大小的额外空间。


三、代码实现


public int JumpFloor(int n) {
   if (n <= 2)
       return n;
   int pre2 = 1, pre1 = 2;
   int result = 0;
   for (int i = 2; i < n; i++) {
       result = pre2 + pre1;
       pre2 = pre1;
       pre1 = result;
  }
   return result;
}


推荐阅读



封面




今日算法系列,题解更新地址:studeyang.tech/2023/0725.h…



作者:杨同学technotes
来源:juejin.cn/post/7259543257708658747
收起阅读 »

媒体查询,响应式设计?帮帮我!

web
什么是媒体查询?媒体查询是一种 CSS 语言特性,它允许作者根据设备或窗口的特性有条件地应用 CSS 规则来查看应用程序。最常见的情况是根据视口宽度应用 CSS 规则,这样 CSS 作者就能根据窗口或设备的大小创建相应的组件和布局。但这也可能延伸到用户是否偏好...
继续阅读 »

什么是媒体查询?
媒体查询是一种 CSS 语言特性,它允许作者根据设备或窗口的特性有条件地应用 CSS 规则来查看应用程序。最常见的情况是根据视口宽度应用 CSS 规则,这样 CSS 作者就能根据窗口或设备的大小创建相应的组件和布局。但这也可能延伸到用户是否偏好浅色或深色模式,甚至是用户的可访问性偏好,以及更多属性。



什么是响应式设计?


随着各种设备类型和屏幕尺寸的增多,网络应用程序为用户提供更加量身定制的可视化展示,并针对用户首选交互方式的屏幕尺寸进行优化,已变得越来越重要。


响应式设计可以通过多种技术组合来实现,包括有条件地应用 CSS 规则的媒体查询、容器查询,以及根据它们所包含的内容(例如 flexbox 或 grid)选择灵活的布局。在本文中,我们将重点关注媒体查询和响应式布局,但随着浏览器支持程度的增加,容器查询也需要记住。在撰写本文时,它们还没有准备好进入普及阶段,但可以用于渐进式增强


什么是移动优先设计?


移动优先设计是在设计和构建响应式 web 应用时可以采用的原则。理想情况下,这种方法应该在流程的所有阶段--从开始到结束--都作为指导原则。对于设计来说,这意味着原型或 UI 设计的第一次迭代应该专注于移动的体验,然后再转向更宽的视口尺寸。


虽然你可以从另一个方向(宽优先)来处理 Web 应用程序,但随着屏幕空间的增加,在视觉上重新组织组件要比试图将组件塞进更小的屏幕空间容易得多。


类似的规则也适用于开发过程。一般来说,您应该为基本情况(最窄的屏幕)编写标记和样式,并在必要时逐步为更宽的屏幕应用条件样式。


虽然你可以从另一个方向来处理这个问题,或者使用窄优先和宽优先的混合方法,但这会使你的样式更难以理解,并增加了其他人在审查或维护时的精神负担。当然,也有一些例外情况,编写少量的宽优先规则会更简单,所以请谨慎行事。


CSS 像素对比设备像素


当苹果在2011年推出 iPhone 4时,它是第一款采用高密度显示屏的主流智能手机。早期的 iPhone 的显示分辨率为320x480px,当 iPhone 推出所谓的 “视网膜显示屏” 时 -- 在相同的物理显示宽度下,分辨率提高了一倍,达到640x960px -- 这带来了挑战。不希望用户面临他们不断问自己的情况,“这是什么,蚂蚁的网站?”,一个巧妙的解决方案被设计了出来,iPhone 4将遵循 CSS 规则,就好像它仍然是一个320 x480 px 的设备,并简单地以两倍的密度渲染。这使得现有的网站可以按预期运行,而不需要任何代码更改 - 当为 Web 引入新技术时,您会发现这是一个常见的主题。


由此,创建了术语 CSS 像素和设备像素。


W3C CSS 规范将设备像素定义为:



设备像素是设备输出上能够显示其全部颜色范围的最小面积单位。



CSS 像素(也称为逻辑像素或参考像素)由 W3C CSS 规范定义为:



参考像素是设备像素密度为96 dpi 并且与读取器的距离为一臂长的设备上的一个像素的视角。因此,对于28英寸的标称臂长,视角约为0.0213度。因此,对于在臂长处的阅读,lpx 对应于约0.26mm(1/96英寸)。



参考像素的96 DPI 规则并不总是严格遵守,并且可以根据设备类型和典型的观看距离而变化。
设备像素比率(或 dppx)是每个 CSS 像素使用多少设备像素的一维因子。设备像素比通常是整数(例如,整数)。1、2、3),因为这使得缩放更简单,但并不总是(例如,1.25、1.5等)。


如何使我的网站响应?


默认情况下,移动的浏览器将假定网站的设计不适合这种设备的较窄视口。为了向后兼容,这些浏览器可能会呈现一个网站,就好像屏幕更大,然后缩小到适合更小的屏幕。这不是一个理想的体验,用户经常需要缩放和平移页面,但允许网站的功能主要是因为它最初创建的。


要告诉浏览器某个站点正在为所有视口大小提供优化的体验,您可以在文档中包含以下 Meta 标记:


<meta name="viewport" content="width=device-width, initial-scale=1" />

非矩形显示器


如今,一些设备具有圆角或显示遮挡(诸如显示凹口、相机孔穿孔或软件覆盖),这意味着整个矩形对于用于内容来说是不“安全”的,因为它可能被部分地或完全地遮挡。


默认情况下,此类设备上的浏览器将在“安全区”内接矩形和与文档背景匹配的垂直或水平条内显示内容。


有一些方法可以允许内容扩展到这个区域,避免黑边邮筒的丑陋,但这是一个更高级的功能,不是必需的。


要退出默认的黑边和邮筒,并声明您的应用程序可以适当地处理屏幕的安全和不安全区域,您可以包含以下 Meta 标记:


<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />

文本大小


移动的浏览器还可能人为地增大字体大小,以使文本更具可读性。如果您的网站已经提供了适当大小的文本,您可以包含以下 CSS 来禁用此行为:


html {
-moz-text-size-adjust: none;
-webkit-text-size-adjust: none;
text-size-adjust: none;
}

虽然在无障碍标准中没有规定文本的最小大小,但在大多数情况下 16px 是一个很好的最低限度。


对于输入字段,如果字体大小小于 16px,浏览器可能会在聚焦时放大。有一些方法可以禁用这种行为,例如在 Meta viewport 中设置 maximum-scale=1.0,但强烈建议不要这样做,因为这可能会干扰依赖缩放的用户。更好的解决方案是确保 font-size 大小至少为 16px


什么是断点?


样式中的断点是指条件样式规则停止或开始应用于页面以响应视口大小的点。最常见的是指 min-widthmax-width,但也可以应用于 height


在媒体查询中,这些断点(768px479px)将像这样使用:


@media (min-width: 768px) {
// 宽度 > 768px的条件样式
}

@media (max-width: 479px) {
// 宽度 <= 479px的条件样式
}

当遵循移动优先设计原则时,大多数时候应该使用媒体查询。


还需要注意的是,min-*max-* 查询适用于包含范围,因此在定义断点两侧的样式时,不应使用相同的像素值。


同样重要的是要注意,当放大页面时,以 CSS 像素为单位的视口大小可能会沿着明显的设备像素比率而变化。这可能会导致视口实际上表现得好像其长度是小数值的情况。


@media (max-width: 479px) {
// 宽度 < 479px的条件样式
}

@media (min-width: 480px) {
// 宽度 >= 480px的条件样式
}

在上面的示例中,如果视口(作为缩放的结果)报告为 479.5px,则两个条件规则块都不适用。相反,例如额外的小数值 0.98px 通常应用于查询 max-width


为什么要这么说? 0.02px 是早期版本的 Safari 支持的 CSS 像素的最小分割。参见 WebKit bug #178261


CSS 在 Media Queries Level 4规范中引入了范围查询的概念,其中 <><=, 和 >= 可用于表达性更强的条件,包括包含和排除范围。在撰写本文时,所有主流浏览器都支持这些功能,然而,在 iOS 等平台上的支持还不够。


@media (width < 480px) {
// 宽度 < 480px的条件样式
}

@media (width >= 480px) {
// 宽度 >= 480px的条件样式
}

我应该选择哪些断点?


这是一个经常被问到的问题,但坦率地说,这并不重要,只要一个 Web 应用程序在你选择的断点之间的所有屏幕尺寸上都能正常工作。你也不想选择太多或太少。


iPhone 在2007年首次推出时,屏幕分辨率为320x480px。按照惯例,所有智能手机的视口宽度至少为320 CSS 像素。当建立一个响应式网站时,你至少应该满足这个宽度的设备。


最近,网络变得更容易被一类设备访问,这些设备适合更经典的外形,称为功能手机,以及可穿戴技术。这些设备通常具有小于320px 的视口宽度。


一些设备,如 Apple Watch,将表现得好像它们有一个320px 的视口,以允许与未专门针对极小视口进行优化的网站兼容。如果要声明您确实处理 Apple watch 的较窄视口,请在文档中包含以下 Meta 标记:


<meta name="disable-adaptations" content="watch" />

如果您使用的是设计系统或组件库(例如 Material UI、Bootstrap 等)它为您提供了自己的默认断点,您可能会发现坚持使用这些断点是有益的。


如果你选择自己的断点,有一些历史上相关的断点可以作为灵感:



  • 320px - 智能手机视口的最小宽度

  • 480px - 智能手机和平板电脑之间的近似边界

  • 768px - 最初的 iPad 分辨率为768 x1024 px

  • 1024px - 同上

  • 1280px - 16:9 720p(HD)显示器的标准宽度


通常,给断点命名是个好主意。但是,不要试图称它们为“移动”,“平板电脑”和“桌面”之类的名称。虽然在平板电脑的早期,移动,平板电脑和桌面之间的界限更加清晰,但现在有如此广泛的设备和视口尺寸,以至于这些设备之间的界限变得模糊。如今,我们有屏幕尺寸比一些平板电脑更大的可折叠手机,以及让台式机和笔记本电脑屏幕相形见绌的平板电脑屏幕。


将特定范围称为“平板电脑”或“台式机”可能会让您陷入为单一类型的设备(例如平板电脑)进行构建和设计的陷阱。假设“移动的”或“平板”视口将总是使用触摸屏)。相反,您应该专注于构建在各种设备上工作的体验。


响应式布局技术


有两种 CSS 布局算法特别适合响应式设计:



  • Flexbox

  • Grid


FLEXBOX


Flexbox 是一种 CSS 布局算法,它允许我们指定子元素在页面上的排列方式。此控件应用于特定方向(称为 flex 轴)。


虽然 flexbox 可以用于呈现多行(带换行),但一行中的内容元素不会改变其他行中元素的排列方式。这意味着除非明确设置 flex 项的宽度,否则它们的排列方式可能不一致。如果需要,CSS Grid 可能更合适。


Flex Wrap


使用 Flexbox 时可以不使用媒体查询,而是依靠 flex-wrap 属性,使内容可以根据内容大小多次跨轴。设置 flex-wrap: wrap 将意味着内容在下方( flex-direction: row)或右侧( flex-direction: column)换行。您还可以设置 flex-wrap: wrap-reverse 使内容在上方或左侧换行。


Flex Direction


通常情况下,对于水平空间有限的窄视口,设计可能要求垂直排列内容,但对于屏幕空间较大的宽视口,则可能改为水平排列内容。


.className {
display: flex;
flex-direction: column;
}

@media (min-width: 768px) {
.className {
flex-direction: row;
}
}

长期以来,媒体查询需要在顶层定义,但当相关规则没有在大型样式表中共存时,这会增加维护负担。在撰写本文时,浏览器尚未广泛支持这种做法,但许多工具和预处理器都允许这样做。


.className {
display: flex;
flex-direction: column;

@media (min-width: 768px) {
flex-direction: row;
}
}

GRID


Grid 是一种 CSS 布局算法,它允许我们指定子元素在页面上的排列方式。Grid 允许开发人员指定元素在行和列之间的排列方式。


就可实现的布局类型而言,它与 flexbox 有重叠之处,但也有显著区别。使用 Grid 布局时,网格项会根据横轴和纵轴上的网格轨道进行约束和对齐。


以下是与响应式设计搭配使用的常见布局技术的几个示例。


Columns


设计师通常会使用 12 栏网格(或窄视口的 4 栏网格)。您可以使用 grid-template-columns 在 CSS 中复制这种模式。结合断点,您就可以轻松分配类,使元素跨越特定的列数。


Google的Una Kravets在One Line Layouts开发站点上分享了一些交互式示例。


RAM(重复、自动、最小最大)


另一种网格布局技术通常称为RAM(重复,自动,最小最大)。我鼓励你去看看One Line Layouts开发站点上的交互式示例


当你事先不知道网格需要多少列,而是希望在一些预设的范围内让内容的大小来决定列数时,RAM 是最有用的。auto-fitauto-fill 的工作方式类似,但当项目数量少于填充一行的数量时会发生什么情况除外。


// 网格项目将始终至少150像素宽,
// 并将伸展以填满所有可用空间
.auto-fit {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}

// 网格项目将始终至少150像素宽,
// 并且会伸展直到有足够的空间
// (如果有)添加匹配的空网格轨道
.auto-fill {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}

Grid Template Areas


网格模板区域是用于响应式布局的最强大工具之一,可让您以直观的方式将元素排列到网格上。


举个例子,我们可能会有一个包含页眉、主要部分、侧边栏和页脚的布局,它们都在狭窄的视口上垂直排列。不过,将 grid-template-areas 区域与 grid-template-columnsgrid-template-rows 结合使用,我们可以用相同的标记将这些元素重新排列成网格模式。


代码示例:


.layout {
display: grid;
grid-template-areas:
"header"
"main"
"sidebar"
"footer";
grid-template-rows: auto 1fr auto auto;
}

@media (min-width: 768px) {
.layout {
grid-template-areas:
"header header"
"main sidebar"
"footer footer";
grid-template-columns: 1fr 200px;
grid-template-rows: auto 1fr auto;
}
}

.header {
grid-area: header;
}

.main {
grid-area: main;
}

.sidebar {
grid-area: sidebar;
}

.footer {
grid-area: footer;
}

<div class="layout">
<header class="header">Headerheader>
<main class="main">Mainmain>
<aside class="sidebar">Sidebaraside>
<footer class="footer">Footerfooter>
div>

响应图像


在高密度显示屏上,根据 CSS 像素而不是设备像素来调整图片大小,可能会导致图片质量低于用户的预期,尤其是在显示清晰的文本或矢量资源时,会显得格外刺眼。因此,为用户提供更高密度的图片是有意义的。


如果可能,请使用基于矢量的图像 (SVG)。矢量不是指定像素的光栅,而是描述在屏幕上绘制某些内容的过程,这一过程可以放大/缩小到任何屏幕尺寸,但始终保持清晰。矢量图像通常适用于简单的插图、图标或徽标。它们不适用于照片。



请注意,SVG 可以嵌入光栅图像。如果是这种情况,又无法获得真正的矢量图像,最好直接使用光栅图像。这是因为光栅图像在 SVG 中使用 base64 编码,与普通二进制文件相比,文件大小会增大。


对于光栅图像,有几种为高密度显示指定多个图像源的方法,允许浏览器选择最适合特定设备的图像源。


对于静态大小的图像,你可以使用 x 描述符(指定最佳设备像素比)指定。例如,如果您有一个图标或徽标,显示宽度为44px,你可以创建该图像的多个不同版本,并指定如下内容:


<img
srcset="
/path/
to/img-44w.png 1x,
/path/
to/img-66w.png 1.5x,
/path/
to/img-88w.png 2x,
/path/
to/img-132w.png 3x
"

/>


重要的是,这些 x 描述符只是一种提示,设备仍可能出于各种原因(如用户选择了节省带宽的规定)选择较低分辨率的版本。


在 CSS 中使用 image-set()(注意浏览器的支持并不完善)对background-image也可以采用类似的技术:


.selector {
height: 44px;
width: 44px;
/* 对于不支持 image-set() 的浏览器使用 2x 回退 */
background-image: url(/path/to/img-88w.png);
/* Safari 只支持 -webkit-image-set() */
background-image: -webkit-image-set(
url(/path/to/img-44w.png) 1x,
url(/path/to/img-66w.png) 1.5x,
url(/path/to/img-88w.png) 2x,
url(/path/to/img-132w.png) 3x
);
/* 标准语法 */
background-image: image-set(
url(/path/to/img-44w.png) 1x,
url(/path/to/img-66w.png) 1.5x,
url(/path/to/img-88w.png) 2x,
url(/path/to/img-132w.png) 3x
);
}

对于随着页面大小调整而改变大小的图像,可以组合使用 srcsetsizes 属性。例如。


<img
srcset="
/path/to/img-320w.jpg 320w,
/path/to/img-480w.jpg 480w,
/path/to/img-640w.jpg 640w,
/path/to/img-960w.jpg 960w,
/path/to/img-1280w.jpg 1280w
"

sizes="(min-width: 768px) 480px, 100vw"
/>


在上面的例子中,我们在 srcset 中以不同实际宽度(320px、480px、640px、960px、1280px)的多个不同图像渲染。在 sizes 属性中,我们告诉浏览器这些图像将默认以视口宽度的 100% 显示,然后对于768px 和更宽的视口,图像将以固定的480 CSS 像素宽度显示。然后,浏览器将根据设备像素比为设备选择最佳图像渲染(尽管这只是一个提示,浏览器可以选择使用更高或更低的分辨率选项)。


使用 WebP 和 AVIF 等现代图像格式压缩技术,当图像以 2 倍密度显示时,文件大小通常只比 1 倍版本略有增加。而且,当设备像素比大于 2 时,收益会越来越小。因此,您可以只包括优化的2x 图像。Google Chrome 团队的开发者倡导者 Jake Archibald 写了一篇博客文章讨论了这一问题,并强调了一个事实:你的大多数用户可能都在使用高密度显示器浏览网页。

作者:chansee97
来源:juejin.cn/post/7259605860603576375

收起阅读 »

纯C文件推理Llama 2

web
这段项目可以让你通过PyTorch从头开始训练Llama 2 LLM架构模型,然后将权重保存到一个原始二进制文件中,再将其加载到一个仅有500行的简单C文件(run.c)中,该文件推断模型,目前仅支持fp32。在作者的云Linux开发平台上,一个维度为288的...
继续阅读 »

这段项目可以让你通过PyTorch从头开始训练Llama 2 LLM架构模型,然后将权重保存到一个原始二进制文件中,再将其加载到一个仅有500行的简单C文件(run.c)中,该文件推断模型,目前仅支持fp32。在作者的云Linux开发平台上,一个维度为288的6层6头模型(约15M个参数)推断速度约为每秒100个令牌;在M1 MacBook Air上推断速度也差不多。作者有些惊喜地发现,采用这种简单方法,可以以高度交互的速度运行相当大的模型(几千万个参数)。




参考文献:
https://github.com/karpathy/llama2.c

作者:阿升
来源:mdnice.com/writing/6f98f171b14e4050bf627afe59ccb82a

收起阅读 »

也许跟大家不太一样,我是这么用TypeScript来写前端的

web
一、当前一些写前端的骚操作 先罗列一下见到过的一些写法吧:) 1. interface(或Type)一把梭 掘金上很多文章,一提到 TypeScript,那不得先用 interface 或者 type 来声明个数据结构吗?像这样: type User = { ...
继续阅读 »

一、当前一些写前端的骚操作


先罗列一下见到过的一些写法吧:)


1. interface(或Type)一把梭


掘金上很多文章,一提到 TypeScript,那不得先用 interface 或者 type 来声明个数据结构吗?像这样:


type User = {
nickname: string
avatar?: string
age: number
}

interface User {
nickname: string
avatar?: string
age: number
}

然后其他方法限制下入参类型,搞定,我掌握了 TypeScript 了,工资不得给我涨3000???



这里说明一下, 我司 不允许 直接使用 interface type 来定义非装饰器参数和配置性参数之外其他 任何数据类型



2. 类型体操整花活


要么把属性整成只读了,要么猪狗类型联合了,要么猪尾巴搞丢了,要么牛真的会吹牛逼了。


类型体操确实玩出了很多花活。 昨天说过了:TypeScript最好玩的就是类型体操, 也恰好是最不应该出现的东西


3. hook 的无限神话


不知道什么时候开始,hook 越来越流行。 听说不会写 hook 的前端程序员,已经算不上高阶程序员了, 不 use 点啥都展示不出牛逼的水平。


4. axios 拦截大法好


随便搜索一下 axios 的文章, 没有 拦截器 这个关键词的文章都算不上 axios 的高端用法了。


二、 我们一些不太一样的前端骚操作


昨天的文章有提到一些关于在前端使用 装饰器 来实现一些基于配置的需求实现, 今天其实想重点聊一聊如何在前端优雅的面向对象。


写过 JavaSpringBootJPA 等代码的后端程序员应该非常熟悉的一些概念:



  • 抽象: 万物都可抽象成相关的类和对象

  • 面向对象: 继承、封装、多态等特性的面向对象设计思维

  • 切面: 没有什么是切一刀解决不了的,如果一刀不行, 那就多来几刀。

  • 注解: 没有什么常量是不能使用注解来配置的, 也没有什么注解是切面想切还能躲得掉的

  • 反射: 没有什么是暴力拿取会失败的, 即使失败也没有异常是丢不出来的

  • 实体: 没有什么是不能抽象到实体上的, 万物皆唯一。

  • 很多: 还有很多,以上描述比较主观和随意。


于是我们开始把后端思维往前端来一个个的转移:)


1. 抽象和面向对象


与后端的交互数据对象、 请求的API接口都给抽象到具体的类上去,于是有了:



  • Service API请求类


abstract class AbstractService{
// 实现一个抽象属性 让子类们实现
abstract baseUrl!: string

// 再实现一些通用的 如增删改查之类的网络请求
// save()

// getDetail()

// deleteById()

// select()

// page()

// disabled()

// ......
}


  • Entity 数据实体基类


abstract class AbstractBaseEntityextends AbstractService> {
abstract service!: AbstractService

// 任何数据都是唯一的 ID
id!: number

// 再来实现一些数据实体的更新和删除方法
save(){
await service.save(this.toJson())
Notify.success("新增成功")
}

delete(){
service.deleteById(this.id)
Notify.success("删除成功")
}

async validate(scene: EntityScene):Promise<void>{
return new Promise((resolve,reject)=>{
// 多场景的话 可以Switch
if(...){
Notify.error("XXX校验失败")
reject();
}
resove();
})
}
// ......
}



  • 子类的实现:)


class UserEntity extends AbstractUserEntity<UserService>{
service = new UserService()

nickname!: string
age!: number
avatar?: string

// 用户是否成年人
isAdult(): boolean{
return this.age >= 18
}

async validate(scene: EntityScene): Promise<void> {
return new Promise((resove,reject)=>{
if(!this.isAdult()){
Notify.error("用户未成年, 请确认年龄")
reject();
}
await super.validate(scene)
})
}

}


  • View 视图调用


<template>
<el-input v-model="user.nickname"/>
<el-button @click="onUserSave()">创建用户el-button>
template>
<script setup lang="ts">
const user = ref(new UserEntity())
async function onUserSave(){
await user.validate(EntityScene.SAVE);
await user.save()
}
script>

2. 装饰器/切面/反射


装饰器部分的话,昨天的文章有提到一些了,今天主要所说反射和切面部分。


TypeScript 中, 其实装饰器本身就可以理解为一个切面了, 这里与 Java 中还是有很多不同的, 但概念和思维上是基本一致的。


反射 ReflectTypeScript 中比较坑的一个存在, 目前主要是依赖 reflect-metadata 这个第三方库来实现, 将一些元数据存储到 metadata 中, 在需要使用的时候通过反射的方式来获取。 可以参考这篇文章:TypeScript 中的元数据以及 reflect-metadata 实现原理分析


在实际使用中, 我们早前用的是 class-transformer 这个库, 之前我对这个库的评价应该是非常高的: “如果没有 class-transformer 这个库, TypeScript 狗都不写。”


确实很棒的一个库,但是在后来,我们写了个通用的内部框架, 为了适配 微信小程序端 以及 uniapp 端, 再加上有一些特殊的业务功能以及 class-transfromer 的写法和命名方式我个人不太喜欢的种种原因, 我们放弃了这个库, 但我们仿照了它的思想重新实现了一个内部使用的库,做了一些功能的阉割和新特性的添加。


核心功能的一些说明




  • 通过反射进行数据转换



    如将后端API返回的数据按照前端的数据结构强制进行转换, 当后端数据返回乱七八糟的时候,保证前端数据在使用中不会出现任何问题, 如下 demo



    class UserEntity {
    @Type(String) phone!: string;
    @Type(RoleEntity) roleInfo!: RoleEntity:
    @Type(DeptEntity) @List @Default([]) deptInfoList!: DeptEntity[]
    @Type(Boolean) @Default(false) isDisabled!: boolean
    }



  • 通过反射进行配置的存储和读取



    这个在昨天的文章中有讲到一部分, 比如配置表单、表格、搜索框、权限 等





3. 再次强调面向对象


为了整个前端项目的工程化、结构化、高度抽象化,这里不得不再次强调面向对象的设计:)




  • 这是个拼爹的社会



    一些通用的功能,一旦有复用的可能, 都可以考虑和尝试让其父类进行实现, 如需要子类传入一些特性参数时, 可以使用抽象方法或抽象属性(这可是Java中没有的)来传入父类实现过程中需要的特性参数。





  • 合理的抽象分层



    将一些特性按照不同的抽象概念进行组合与抽离,实现每个类的功能都是尽可能不耦合,实现类的单一职责。如存在多继承, 在考虑实现类的实现成本前提下,可考虑抽象到接口 interface 中。





  • 还有很多,有空再一一列举




4. 严格但又有趣的 tsdoc


我们先来看一些注释的截图吧:)








一些详细的注释、弃用的方法、选填的参数、传入参数后可能影响或依赖的其他参数,在注释里写好玩的 emoji或者图片,甚至是 直接在注释里写调用 demo, 让调用方可以很轻松愉快的对接调用, 玩归玩, 确实对整体项目的质量有很大的帮助。


三、 写在最后


中午跟同事吃饭聊了聊现在国内大前端的一个状态, 当时聊到一个关键词 舒适区, 还有前端整个技术栈过于灵活的一些优缺点, 几个大老爷们都发出了一些感慨, 如果前端能够更标准化一些, 像 Java 一样, 说不定前端还能上升几个高度。


我们基于今天文章里的一些设计写了一些DEMO,但目前不太方便直接开源,如果有兴趣,可以私聊我获取代码链接。


是的, 我还是那个 Java 仔, 是, 也不仅仅是。

作者:Hamm
来源:juejin.cn/post/7259562014417813564

收起阅读 »

请自信的大声告诉面试官forEach跳不出循环

web
如果面试官,或者有人问你foreach怎么跳出循环,请你大声的告诉ta,跳不出!!!!!!!!!! foreach 跳不出循环 为什么呢? 先看看foreach大体实现。 Array.prototype.customForEach = function (fn...
继续阅读 »

如果面试官,或者有人问你foreach怎么跳出循环,请你大声的告诉ta,跳不出!!!!!!!!!!


foreach 跳不出循环


为什么呢?


先看看foreach大体实现。


Array.prototype.customForEach = function (fn) {
for (let i = 0; i < this.length; i++) {
fn.call(this, this[i], i, this)
}
}

list.customForEach((item, i, list) => {
console.log(item, i, list)
})

let list = [1,2,3,4,5]

list.forEach((item,index,list)=>{
console.log(item,index,list)
})

list.customForEach((item,index,list)=>{
console.log(item,index,list)
})




两个输出的结果是一样的没啥问题,这就是foreach的大体实现,既然都知道了它的实现,那么对它为什么跳不出循环♻️应该都知道了,再不清楚的话,再看一下下面的例子。



function demo(){
return 'demo'
}

function demo2(){
demo()
return 'demo2'
}

demo()


在demo2函数里面调用demo函数,demo函数的return能阻止demo2函数下面的执行吗?很明显不行啊,demo函数里的return跟demo2函数一点关系都没有。现在你再回头看看foreach的实现,就明白它跳不出循环一清二楚了。


有点同学说不是可以通过抛出错误跳出循环吗?是的。看看下面例子。



let list = [1,2,3,4,5]

try {
list.forEach((item, index, list) => {
if (index === 2) {
throw new Error('demo')
}
console.log(item)
})
} catch (e) {
// console.log(e)
}




结果是我们想要,但是你看代码,哪个正常人会这样写代码?是非foreach不用吗?还是其他的循环关键字不配呢。


end


有反驳在评论区,show me your code !!!!!!!!!


作者:啥也不懂的前端
来源:juejin.cn/post/7259595485090906149
收起阅读 »

初学矩阵

web
前言 矩阵是人类的瑰宝,矩阵里数字与数字通过关系组在一起。正如大道无形,用不同的视角去解读数的关系,它就有不同的作用。大道至简,难的是解读道的心。(作者发癫中...) 让我们放开的自己的心,不要限制它的解读,(san +++) 下面进行简单的描述。 矩阵 (M...
继续阅读 »

前言


矩阵是人类的瑰宝,矩阵里数字与数字通过关系组在一起。正如大道无形,用不同的视角去解读数的关系,它就有不同的作用。大道至简,难的是解读道的心。(作者发癫中...)


让我们放开的自己的心,不要限制它的解读,(san +++)


下面进行简单的描述。


矩阵 (Matrix)


定义


矩阵由 m 行 n 列 组成的方队,即为 m * n 的矩阵。 其组成的元素可以为实数,虚数。


好了开写。


我这边定义一个枚举类型,因为我想通过矩阵计算对象某个属性。
但是实现实在是有点生草。最开始的时候准备封装个矩阵类,然后通过它进行计算。
但是现实中我那微不足道的 OOP 水平撑不下去了,在矩阵的灵活扩展想法败北了┭┮﹏┭┮。
最后是个四不像的实现。


// 定义矩阵类型
type IMatrix<T = number> = T[][];

class Matrix {
// 获取矩阵常用的坐标集合操作
static getRow<T = number>(matrix: IMatrix<T>, rowIndex: number) {
return matrix?.[rowIndex]
};
static getCol<T = number>(matrix: IMatrix<T>, colIndex: number) {
return matrix.map(row => row[colIndex])
};
static getMatrixLen<T = number>(matrix: IMatrix<T>) {
return {
rowLen: matrix.length,
colLen: Math.max(...matrix.map(row => row.length)),
}
};
}

上面就是获取矩阵常用简化。


这里关于使用类静态方法的考虑是因为我觉得相对比较直观,虽然现在有 esm 现代模块化方案,可以基于文件即模块,但是考虑到更直观的抽象关系,我选择这种方式。第一调用的时候,不会 esm import 那样少了直观的从属关系,esm 的文件即模块确实很方便,但引用代码如果没有插件的话,需要追踪对应的文件模块的话,只能从函数名语义入手。


在项目中有时候会碰到大杂烩语义的文件,比如一个 util文件 会承当各种逻辑封装而失去模块的意义,用类作为一个抽象空间是一种相对方法。


同型矩阵/单位矩阵


同型矩阵就是矩阵之间的行数列数均相同,则视矩阵之间的关系为同型矩阵。符合同型矩阵是一些计算逻辑的前置判断。


单位矩阵是矩阵主对角线之间的数为 1 ,其余为 0 。其实就是行列坐标相同的点就是 1 ,其余就是 0。


interface IGetMatrixValue<T = number> {
(matrixItem: T) : number;
}
//...
static isHomotypeMatrix<T = number>(matrix1: IMatrix<T>, matrix2: IMatrix<T>): boolean {
if (matrix1.length !== matrix2.length) {
return false;
}
if (this.getMatrixLen(matrix1) !== this.getMatrixLen(matrix2)) {
return false;
}

return true;
};

static isUnitMatrix<T = number>(matrix: IMatrix<T>, getMatrixVal?: IGetMatrixValue<T>) {
const handleGetMatrixValue = getMatrixVal || getDefaultMatrixItem;

for (let i = 0; i < matrix.length; i++) {
const row = matrix[i];
for (let j = 0; j < row.length; j++) {
const isSameIdx = i === j;
const val = handleGetMatrixValue(matrix[i][j] as any);

if (isSameIdx && val !== 1) {
return false;
}

if (!isSameIdx && val !== 0) {
return false;
}
}
}

return true;
};


当时写到这里,我感觉脑子乱乱的,可能是经常熬夜吧。
第二个获取单元行数,是不是有点不一样的。其实从这里,我才意识这里正因为我的定义函数因为它太灵活,导致我这边要处理更多的边际逻辑。(当时大脑宕机中...😐)


我希望我的代码可以使用对象运算,但是如何标准的写出可扩展的函数,或许是我要去学习的。 Lodash 源码获取是个不错的选择,但是总有一些事情,让我没有机会。


同型矩阵加/减


同型矩阵同个行列位置的进行加减运算,所以我写道一般,还是抽了一个计算同个位置逻辑的函数,并把其它情况交给调用者自己扩展运算吧。


interface ICustMatrixAWithB<T> {
(matrixAItem: T, matrixBItem: T) : T;
}

static computeHomotypeMatrix<T = number>(matrix1: IMatrix<T>, matrix2: IMatrix<T>, custom: ICustMatrixAWithB<T>): IMatrix<T> {
if (!this.isHomotypeMatrix(matrix1, matrix2)) {
throw new Error('该矩阵非二维数组');
}
const { rowLen, colLen } = this.getMatrixLen(matrix1)

const nextMatrix: T[][] = [];

for (let i = 0; i < rowLen; i++) {
nextMatrix[i] = [];
for (let j = 0; j < colLen; j++) {
nextMatrix[i][j] = custom(matrix1[i]?.[j], matrix2[i]?.[j]);
}
}
return nextMatrix;
};

static addHomotypeMatrix(matrix1: IMatrix<number>, matrix2: IMatrix<number>) {
return this.computeHomotypeMatrix(matrix1, matrix2, (num1, num2) => num1 + num2);
};

static subHomotypeMatrix(matrix1: IMatrix<number>, matrix2: IMatrix<number>) {
return this.computeHomotypeMatrix(matrix1, matrix2, (num1, num2) => num1 - num2);
};

写到这里也说我最纠结的,因为我的封装设计出现了问题,最后一层胶水层没法解决,只能交由调用者使用 computeHomotypeMatrix 去实现自己的加减逻辑。


但我不知道要如何去解决,如果你有办法请留言教教我吧。


这里的逻辑,也让我想起了若川大佬的 vant 组件源码共读中的计算逻辑。只是想不起具体细节,时间真的是改变一切,生物的宿命真是难以跨域。


矩阵乘法 矩阵相乘/矩阵标量


矩阵和标量的乘积,标量指的是一个数,数和矩阵相乘等于数与矩阵的每个元素相乘


矩阵相乘则是比较奇怪,我不太了解原理。是这么一个公式,矩阵1 m * n , 矩阵2 n * p , 当前矩阵的列数等于后矩阵的行数的时候的才可以进行相乘,可以得到这么一个新矩阵 m * p。矩阵的每一项等于 当前行数的前矩阵的列数的项 * 当前的列数的后矩阵的行数的项,两个数组之间的每一元素相乘后累加成项的值


//...
static mapMatralItem<T>(matrix: IMatrix<T>, map: (matrix: T) => T) {

const nextMatrix: IMatrix<T> = [];

for (let i = 0; i < matrix.length; i++) {
const row = matrix[i] || [];
nextMatrix[i] = [];

for (let j = 0; j < row.length; j++) {
const item = row[j];
nextMatrix[i][j] = map(item);
}
}

return nextMatrix;
};
// 数乘
static multipleItemMatrix(matrix: IMatrix<number>, multiple: number) {
return this.mapMatralItem(matrix, (item) => item * multiple);
};
// 矩阵相乘
static multiplyMatrix(matrix1: IMatrix<number>, matrix2: IMatrix<number>) {
const { colLen, rowLen: nexRowLen } = this.getMatrixLen(matrix1);
const { rowLen, colLen: nexColLen } = this.getMatrixLen(matrix2);

if (colLen !== rowLen) {
/** A m * n * B n * p = C m *p */
throw new Error('矩阵乘法必须,前矩阵列数等于后矩阵行数');
}

const nextMatrix: IMatrix = [];

for (let i = 0; i < nexRowLen; i++) {
nextMatrix[i] = [];
const curCol = this.getCol(matrix1, i);
for (let j = 0; j < nexColLen; j++) {
const curRow = this.getRow(matrix2, j);
const len = Math.max(curCol.length, curRow.length);
const computed = Array.from({
length: len,
}, (_, idx) => {
const curColVal = curCol[idx] || 0;
const curRowVal = curRow[idx] || 0;
return curColVal * curRowVal;
});

nextMatrix[i][j] = computed.reduce((acc, cur) => acc + cur, 0);
}
}

return nextMatrix;
};

mapMatralItem 函数封装,它不提供具体的逻辑,只是提供矩阵每个元素的类似数组 map 的能力,但也没有比数组好,因为它再设计的时候少了更多的环境参数。


矩阵转秩


矩阵转秩代表矩阵的行列坐标相互交换,前面有提到的对角线坐标在转秩后仍然还在同一个位置,其它都是 0 交换后也变,所以也可以得出单位矩阵的秩等于单位的结论。


// 矩阵转秩
static randConversionMatrix<T>(matrix: IMatrix<T>) {
const { rowLen, colLen } = this.getMatrixLen(matrix);
const nextMatrix:IMatrix<T> = [];
for (let i = 0; i < colLen; i++) {
nextMatrix[i] = [];
for (let j = 0; j < rowLen; j++) {
nextMatrix[i][j] = matrix[j][i];
}
};

return nextMatrix;
};

矩阵 共轭


这些不懂,暂时跳过,当初不好好学习,上个好的学校 ┭┮﹏┭┮。


矩阵快速幂运算


快速幂是什么,其实就是减少指数,转为同等的底数,来减少计算次数。
看了好久才懂,太菜了。
比方说,一个 2 ** 8 ,我们通过二分指数的方式把底数扩大 (2 ** 2 ) ** 4 -> (4 ** 2) * 2 最后就变小,指数越大效率越高


/**
* 快速幂运算
* @param a 底数
* @param pow 阶乘
*/

const multiQuick = (a: number, pow: number) => {
let curPow = pow, result = 1;

while(curPow) {
if (curPow === 0) {
result *= 1;
} else if (curPow === 1) {
result *= a;
curPow = 0;
} else if (curPow % 2 === 1) {
result *= result * a;
curPow -= 1;
} else if (curPow % 2 === 0) {
result *= result;
curPow >>= 1;
}
};

return result;
};

static multilpyQuiickMatrix(matrix: IMatrix<number>, pow: number) {
return this.mapMatralItem(matrix, (num) => multiQuick(num, pow))
};

这里有小知识点二进制也算复习了,很多东西学了忘,学了忘,只有真正意识到它的价值,才能接纳它。


这里也可以看得出来,虽然我封装得 map 函数不够好,但是确实确实简化很多过程表述。只是从一个数据向另一个数据迁移。


结语


本次文章记录也到到此为止,感谢人生中让我成长的一切,感谢每一个让我快乐的人。


最近行情真的不好,有时候在想,我除了编程还有技能吗?可惜好像没有发现,我学历低,没有大厂背景,真的失业了可能就很难找到工作。


最近也是高考结束了期间,有一批学子成为了准大学生。记得当年报考,我最后还是说服父母说了报考移动应用开发专业。那时候的梦想真的是想学习编程的来开发游戏,然而现在感觉工作了,那股热情却萎了。


人生且叹且前

作者:孤独之舟
来源:juejin.cn/post/7258191640564334653
行,缘分渐行渐无书。

收起阅读 »

希尔排序,我真的领悟了

web
之前文章我们讲到过 冒泡排序、选择排序、插入排序 都是原地的,并且时间复杂度都为O(n^2) 的排序算法。那么今天我们来讲一下希尔排序,它的时间复杂度为O(n*logn)。那这个算法是怎么做到的呢?我们这回一次看个透。 首先再回顾一下 冒泡、选择、插入这3个排...
继续阅读 »

之前文章我们讲到过 冒泡排序选择排序插入排序 都是原地的,并且时间复杂度都为O(n^2) 的排序算法。那么今天我们来讲一下希尔排序,它的时间复杂度为O(n*logn)。那这个算法是怎么做到的呢?我们这回一次看个透。


首先再回顾一下 冒泡选择插入这3个排序。这三个排序都有一个共同的特点,就是每次比较都会得到当前的最大值或者最小值。有人会说这是屁话,但你细品,为什么很多人都会去刻意背10大排序算法,本质就是因为自己的思想被困住了(什么每轮比较得出最大值的就是冒泡,得出最小值的就是选择等等),假设你从没有接触过排序算法,我还真不相信你不会排序,最差的情况就是做不出原地呗,时间复杂度最差也是N^2,就像下面这样:


let data = [30, 20, 55, 10, 90];

for (let index = 0; index < data.length; index++){
for (let y = 0; y < data.length - index; y++){
if(data[y] > data[y+1]){
[ data[y], data[y+1] ] = [ data[y+1], data[y] ];
}
}
}


data的长度是5,就循环5次,每轮比较中都要得出当前轮次的最大值。那么在每一轮中,如何得出最大值呢?那就再来一次遍历。


上述思想我们会发现,它在时间复杂度上是突破不了 O(n^2) 的限制的。原因在于你是两两比较(一次只能在两个数中得到最大值,一次只能给两个数排序)


如何突破限制呢?那就一次比较多个,就像下面这样:


let data = [30, 20, 55, 10, 90];

// 1、我们对data数组进行拆分,拆分规则:步长为2的数据放到一个集合里。
// 2、根据上面的拆分规则,我们可以将data数组拆成2个子数组。分别是:[30, 55, 90]、[20, 10]
// 3、分别对这2个子数组进行排序,排序后的子数组分别是:[30, 55, 90]、[10, 20]
// 4、将上面的子数组合并为一个新的数组data1:[30, 10, 55, 20, 90]
// 5、修改拆分规则,对修改后的data1数组进行拆分,步长为1。
// 6、因为步长为1,所以相当于对data1数组进行整体排序。

那么如何用代码表示呢?请继续阅读。


第一步、确定步长


这里我们以不断均分,直到均分结果大于0为原则来确定步长:


let data = [30, 20, 55, 10, 90];

// 维护步长集合
let gapArr = [];

let temp = Math.floor(data / 2);

while(temp > 0){
gap.push(temp);
temp = Math.floor(temp / 2);
}


第二步、得到间隔相等的元素


这一步其实本质上就是将间隔相等的元素放在一起进行比较


这意味着我们不用分割数组,只要保证原地对间隔相同的元素进行排序即可。


let data = [30, 20, 55, 10, 90];

let gapArr = [2, 1];

for (let gapIndex = 0; gapIndex < gapArr.length; gapIndex++){
// 当前步长
let curGap = gapArr[gapIndex];
// 从当前索引项为步长的地方开始进行比较
for(let index = curGap; index < data.length; index++){
let curValue = data[index]; // 当前的值
let prevIndex = index - curGap; // 间隔为gap的前一项索引
while(prevIndex >= 0 && curValue < data[prevIndex]){
// 这里面的while就代表着gap相等的数据项之间的比较...
prevIndex = prevIndex - curGap;
}
}
}

第二层的for循环、最里面的while循环需要我们好好理解一下。


就以上面的数据为例,我们先不考虑数据交换的问题,只考虑上面的写法是如何把gap相等的元素联系到一块的,现在我们来推演一下:




从上图我们看到,第一次while循环因为不满足条件,导致没有被触发。紧接着index++,我们来推演一下这种状态下的数据:



继续index++,此时我们来推演一下这种状态下的数据:




经过我们一轮间隔(gap)的分析,我们发现这种 for + while 的方法能够满足我们对间隔相等的元素进行排序。因为我们通过这种方式可以获取到间隔相等的元素


此时,我们终于可以进入到了最后一步,那就是对间隔相等的元素进行排序


第三步、对间隔相等的元素进行排序


在上一步的基础上,我们来完成相应元素的排序。


// 其它代码都不变......
for(let index = curGap; index < data.length; index++){
let curValue = data[index]; // 当前的值
let prevIndex = index - curGap; // 间隔为gap的前一项索引
while(prevIndex >= 0 && curValue < data[prevIndex]){
// 新增代码 ++++++
data[prevIndex + curGap] = data[prevIndex];
prevIndex = prevIndex - curGap;
}
// 新增代码 ++++++
data[prevIndex + curGap] = curValue;
}

现在我们来对排序的过程进行一下数据推演:


注意,这里我们只演示 curGap === 2 && index === data.length - 1 && data === [30, 20, 55, 10, 9] 的情况。


读到这里大家可能会发现我们突然换了数据源,因为原先的数据源的最后一项正好是最大值,不方便看到数据比较的全貌,所以在这里我们将最后一项改为了最小值。




开启while循环如下:




紧接上图,第二次进入while循环如下:




第二次循环结束后,此时的prevIndex < 0,因为未能进入到第三次的while循环:




至此,我们完成了本轮的数据推演。


在本轮数据推演中,我们会发现它跟之前的两两相比,区别在于它一次可能会比较很多个元素,更具体的说就是,它的一次for循环里,可以比较多个元素对,并将这些元素对进行排序。


第四步、源码展示


function hillSort(arr){
let newData = Array.from(arr);
// 增量序列集合
let incrementSequenceArr = [];
// 数组总长度
let allLength = newData.length;
// 获取增量序列
while(incrementSequenceArr[incrementSequenceArr.length - 1] != 1){
let increTemp = Math.floor(allLength / 2);
incrementSequenceArr.push(increTemp);
allLength = increTemp;
}
for (let gapIndex = 0; gapIndex < incrementSequenceArr.length; gapIndex++){
// 遍历间隔
let gap = incrementSequenceArr[gapIndex]; // 获取当前gap
for (let currentIndex = gap; currentIndex < newData.length; currentIndex++){
let preIndex = currentIndex - gap; // 前一个gap对应的索引
let curValue = newData[currentIndex];
while(preIndex >= 0 && curValue < newData[preIndex]){
newData[preIndex + gap] = newData[preIndex];
preIndex = preIndex - gap;
}
newData[preIndex + gap] = curValue;
}
}
return newData;
}

最后


又到分别的时刻啦,在上述过程中如果有讲的不透彻的地方,欢迎小伙伴里评论留言,希望我说的对你有启发,我们下期再见啦~~

作者:小九九的爸爸
来源:juejin.cn/post/7258180488359018557

收起阅读 »

随着鼠标移入,图片的切换跟着修改背景颜色(Vue3写法)

web
先看看效果图吧 下面来看实现思路 又是摸鱼的下午,无聊来实现了一下这个效果,记录一下,说不定以后有这需求,记一下放到官网上也是OK的, 我这里提供一种实现方法,当然你们想用放大加模糊也是可以的,想怎么来就怎么来 1.背景颜色不是固定的,是随着图片的切换动态...
继续阅读 »

先看看效果图吧


image.png


image.png


下面来看实现思路


又是摸鱼的下午,无聊来实现了一下这个效果,记录一下,说不定以后有这需求,记一下放到官网上也是OK的,
我这里提供一种实现方法,当然你们想用放大加模糊也是可以的,想怎么来就怎么来


1.背景颜色不是固定的,是随着图片的切换动态改变


原理:

1.当鼠标移入到某一张图片时,拿到这张图片

2.我们就可以把这张图片画到canvas里,就可以获取到每一个像素点

3.我们的背景是需要渐变的,我们是需要三种颜色的渐变,当然也可以有很多种,看你们的心情

4.我们就要计算出前三种的主要颜色,但是每个像素点的颜色非常非常多,好多颜色也非常相近,我们通过肉眼肯定看不出来的,这个时候就要用到计算机了

5.需要一种近似算法(颜色聚合算法)了,就是把好多相近的颜色聚合成一种颜色,当然我们就要用到第三方库(colorthief)了


准备好html


<template>
<div class="box">
<div class="item" v-for="item in 8" :key="item" :class="item === hoverIndex ? 'over' : ''">
<img crossorigin="anonymous" @mouseenter="onMousenter($event.target, item)" @mouseleave="onMousleave"
:src="`https://picsum.photos/438/300?id=${item}`" alt=""
:style="{ opacity: hoverIndex === -1 ? 1 : item === hoverIndex ? 1 : 0.2 }">
// 设置透明度
</div>
</div>
</template>


scss


.box {
height: 100vh;
display: flex;
justify-content: space-evenly;
align-items: center;
flex-wrap: wrap;
background-color: rgb(var(--c1), var(--c2), var(--c3));
}

.item {
border: 1px solid #fff;
margin-top: 50px;
transition: 0.8s;
padding: 5px;
box-shadow: 0 0 10px #00000058;
background-color: #fff;
}

img {
transition: .8s;
}

npm安装colorthief库


npm i colorthief

导入到文件中


import ColorThief from "colorthief";

因为这是一个构造函数,所以需要创建出一个实例对象


const colorThief = new ColorThief()
const hoverIndex = ref<number>(-1) //设置变换样式的响应式变量

重点函数:鼠标移入事件onMousenter


getPalette(img,num) img是dom元素,是第三库需要将其画入到canvas中,所以需要在img标签中添加一个允许跨域的属性 crossorigin="anonymous",不然会报错

num是需要提取几种颜色,同样也会返回多少个数组

返回的是一个promise,需要await


const onMousenter = async (img: EventTarget | null, i: number) => {
hoverIndex.value = i //将响应式变量改成自身,样式就生效了
const colors = await colorThief.getPalette(img, 3)
console.log(colors); //获取到三个数组,将其数组改造成rgb格式
const [c1, c2, c3] = colors.map((c: string[]) => `rgb(${c[0]},${c[1]},${c[2]})`)//将三个颜色解构出来
html.style.setProperty('--c1', c1) //给html设置变量,下面有步骤
html.style.setProperty('--c2', c2)
html.style.setProperty('--c3', c3)
}

鼠标移出事件


将响应式变量初始化,将背景颜色改为白色


const onMousleave = () => {
hoverIndex.value = -1
html.style.setProperty('--c1', '#fff')
html.style.setProperty('--c2', '#fff')
html.style.setProperty('--c3', '#fff')
}

获取html根元素


const html = document.documentElement

在主文件index.html给html设置渐变变量


<style>
html{
background-image: linear-gradient(to bottom, var(--c1), var(--c2),var(--c3));
}
</style>

image.png
需要注意的是colorthief使用的时候需要给img设置跨域,不然会报错,还有就是给html设置渐变变量


🔥🔥🔥好的,到这里基本上就已经实现了,看着代码也不多,也没啥技术含量,全靠三方库干事,主要是记录生活,方便未来cv


作者:井川不擦
来源:juejin.cn/post/7257733186158903356
收起阅读 »

几何算法:判断两条线段是否相交

web
‍ ‍大家好,我是前端西瓜哥。 如何判断两条线段(注意不是直线)是否有交点? 传统几何算法的局限 上过一点学的西瓜哥我,只用高中学过的知识,还是可以解这个问题的。 一条线段两个点,可以列出一个两点式(x - x1) / (x2 - x1) = (y - y1)...
继续阅读 »


‍大家好,我是前端西瓜哥。


如何判断两条线段(注意不是直线)是否有交点?


传统几何算法的局限


上过一点学的西瓜哥我,只用高中学过的知识,还是可以解这个问题的。


一条线段两个点,可以列出一个两点式(x - x1) / (x2 - x1) = (y - y1) / (y2 - y1)),两条线段是两个两点式,这样就是 二元一次方程组 了 ,就能求出两条直线的交点。


然后判断这个点是否在其中一条线段上。如果在,说明两线段相交,否则不相交。


看起来不错,但这里要考虑直线垂直或水平于坐标轴的特殊情况,还有两条直线平行导致没有唯一解的情况,除数不能为 0 的情况。


特殊情况实在是太多了,能用是能用,但不好用。


那么,有其他的更好的解法吗?


有的,叉乘。


叉乘是什么?


叉乘(cross product)是线性代数的一个概念,也叫外积、叉积、向量积,是在三维空间中两个向量的二元运算的结果,该结果为一个向量。


但那是严格意义上的。实际也可以用在二维空间的二维向量中,不过此时它们的叉乘结果变成了标量。


假设向量 A 为 (x1, y1),向量 B 为 (x2, y2),则叉乘 AxB 的结果为 x1 * y2 - x2 * y1


(注意叉乘不满足交换律)


在几何意义上,这个叉乘结果的绝对值对应两个向量组成的平行四边形的面积。


此外可通过符号判断向量 A 变成向量 B 的旋转方向。


如果叉乘为正数,说明 A 变成 B 需要逆时针旋转(旋转角度小于 180 度);


如果为负数,说明 A 到 B 需要顺时针旋转;


如果为 0,说明两个向量平行(或重合)


叉乘解法的原理


回到题目本身。


假设线段 1 的端点为 A 和 B,线段 2 的端点为 C 和 D。


图片


我们可以换另一个角度去解,即判断线段 1 的两个端点是否在线段 2 的两边,然后再反过来比线段 2 的两点是否线段 1 的两边。


这里我们可以利用上面 叉乘的正负代表旋转方向的特性


以上图为例, AB 向量到 AD 向量位置需要逆时针旋转,AB 向量到 AC 向量则需要顺时针,代表 C 和 D 在 AB 的两侧,对应就是两个叉乘相乘为负数。


function crossProduct(p1: Point, p2: Point, p3: Point)number {
  const x1 = p2[0] - p1[0];
  const y1 = p2[1] - p1[1];
  const x2 = p3[0] - p1[0];
  const y2 = p3[1] - p1[1];
  return x1 * y2 - x2 * y1;
}

const [a, b] = seg1;
const [c, d] = seg2;

// d1 的符号表示 AB 旋转到 AC 的旋转方向
const d1 = crossProduct(a, b, c);


只是判断了 C 和 D 在 AB 线段的两侧还不行,因为可能还有下面这种情况。


图片


所以我们还要再判断一下,A 和 B 是否在 CD 线的的两侧。计算过程同上,这里不赘述。


一般实现


type Point = [numbernumber];

function crossProduct(p1: Point, p2: Point, p3: Point): number {
  const x1 = p2[0] - p1[0];
  const y1 = p2[1] - p1[1];
  const x2 = p3[0] - p1[0];
  const y2 = p3[1] - p1[1];
  return x1 * y2 - x2 * y1;
}

function isSegmentIntersect(
  seg1: [Point, Point],
  seg2: [Point, Point],
): boolean {
  const [a, b] = seg1;
  const [c, d] = seg2;

  const d1 = crossProduct(a, b, c);
  const d2 = crossProduct(a, b, d);
  const d3 = crossProduct(c, d, a);
  const d4 = crossProduct(c, d, b);

  return d1 * d2 < 0 && d3 * d4 < 0;
}

// 测试
const seg1: [PointPoint] = [
  [00],
  [11],
];
const seg2: [PointPoint] = [
  [01],
  [10],
];

console.log(isSegmentIntersect(seg1, seg2)); // true


注意,这个算法认为线段的端点刚好在另一条线段上的情况,不属于相交。


考虑点在线段上或重合


如果你需要考虑线段的端点刚好在另一条线段上的情况,需要额外在叉乘为 0 的情况下,再判断一下线段 1 的端点是否在另一个线段的 x  和 y 范围内。


对应的算法实现:


type Point = [numbernumber];

function crossProduct(p1: Point, p2: Point, p3: Point): number {
  const x1 = p2[0] - p1[0];
  const y1 = p2[1] - p1[1];
  const x2 = p3[0] - p1[0];
  const y2 = p3[1] - p1[1];
  return x1 * y2 - x2 * y1;
}

function onSegment(p: Point, seg: [Point, Point]): boolean {
  const [a, b] = seg;
  const [x, y] = p;
  return (
    x >= Math.min(a[0], b[0]) &&
    x <= Math.max(a[0], b[0]) &&
    y >= Math.min(a[1], b[1]) &&
    y <= Math.max(a[1], b[1])
  );
}

function isSegmentIntersect(
  seg1: [Point, Point],
  seg2: [Point, Point],
): boolean {
  const [a, b] = seg1;
  const [c, d] = seg2;

  const d1 = crossProduct(a, b, c);
  const d2 = crossProduct(a, b, d);
  const d3 = crossProduct(c, d, a);
  const d4 = crossProduct(c, d, b);

  if (d1 * d2 < 0 && d3 * d4 < 0) {
    return true;
  }
 
  // d1 为 0 表示 C 点在 AB 所在的直线上
  // 接着会用 onSegment 再判断这个 C 是不是在 AB 的 x 和 y 的范围内
  if (d1 === 0 && onSegment(c, seg1)) return true;
  if (d2 === 0 && onSegment(d, seg1)) return true;
  if (d3 === 0 && onSegment(a, seg2)) return true;
  if (d4 === 0 && onSegment(b, seg2)) return true;

  return false;
}

// 测试
const seg1: [PointPoint] = [
  [00],
  [11],
];
const seg2: [PointPoint] = [
  [01],
  [10],
];
const seg3: [PointPoint] = [
  [00],
  [22],
];
const seg4: [PointPoint] = [
  [11],
  [10],
];
// 普通相交情况
console.log(isSegmentIntersect(seg1, seg2)); //  true
// 线段 1 的一个端点刚好在线段 2 上
console.log(isSegmentIntersect(seg3, seg4)); // true


结尾


总结一下,判断两条线段是否相交,可以判断两条线段的两端点是否分别在各自的两侧,对应地需要用到二维向量叉乘结果的正负值代表向量旋转方向的特性。


我是前端西瓜哥,关注我,学习更多几何算法。



作者:前端西瓜哥
来源:juejin.cn/post/7257547252540751909

收起阅读 »

我给项目加了性能守卫插件,同事叫我晚上别睡的太死

web
点击在线阅读,体验更好链接现代JavaScript高级小册链接深入浅出Dart链接现代TypeScript高级小册链接 引言 给组内的项目都在CICD流程上更新上了性能守卫插件,效果也还不错,同事还疯狂夸奖我 接下里进入我们的此次的主题吧 由于我组主要是负...
继续阅读 »

点击在线阅读,体验更好链接
现代JavaScript高级小册链接
深入浅出Dart链接
现代TypeScript高级小册链接

引言


给组内的项目都在CICD流程上更新上了性能守卫插件,效果也还不错,同事还疯狂夸奖我


WX20230708-170807@2x.png


接下里进入我们的此次的主题吧



由于我组主要是负责的是H5移动端项目,老板比较关注性能方面的指标,比如首页打开速度,所以一般会关注FP,FCP等等指标,所以一般项目写完以后都会用lighthouse查看,或者接入性能监控系统采集指标.



WX20230708-141706@2x.png


但是会出现两个问题,如果采用第一种方式,使用lighthouse查看性能指标,这个得依赖开发自身的积极性,他要是开发完就Merge上线,你也不知道具体指标怎么样。如果采用第二种方式,那么同样是发布到线上才能查看。最好的方式就是能强制要求开发在还没发布的时候使用lighthouse查看一下,那么在什么阶段做这个策略呢。聪明的同学可能想到,能不能在CICD构建阶段加上策略。其实是可以的,谷歌也想到了这个场景,提供性能守卫这个lighthouse ci插件


性能守卫



性能守卫是一种系统或工具,用于监控和管理应用程序或系统的性能。它旨在确保应用程序在各种负载和使用情况下能够提供稳定和良好的性能。



Lighthouse是一个开源的自动化工具,提供了四种使用方式:




  • Chrome DevTools




  • Chrome插件




  • Node CLI




  • Node模块




image.png


其架构实现图是这样的,有兴趣的同学可以深入了解一下


这里我们我们借助Lighthouse Node模块继承到CICD流程中,这样我们就能在构建阶段知道我们的页面具体性能,如果指标不合格,那么就不给合并MR


剖析lighthouse-ci实现


lighthouse-ci实现机制很简单,核心实现步骤如上图,差异就是lighthouse-ci实现了自己的server端,保持导出的性能指标数据,由于公司一般对这类数据敏感,所以我们一般只需要导出对应的数据指标JSON,上传到我们自己的平台就行了。


image.png


接下里,我们就来看看lighthouse-ci实现步骤:





    1. 启动浏览器实例:CLI通过Puppeteer启动一个Chrome实例。


    const browser = await puppeteer.launch();




    1. 创建新的浏览器标签页:接着,CLI创建一个新的标签页(或称为"页面")。


    const page = await browser.newPage();




    1. 导航到目标URL:CLI命令浏览器加载指定的URL。


    await page.goto('https://example.com');




    1. 收集数据:在加载页面的同时,CLI使用各种Chrome提供的API收集数据,包括网络请求数据、JavaScript执行时间、页面渲染时间等。





    1. 运行审计:数据收集完成后,CLI将这些数据传递给Lighthouse核心,该核心运行一系列预定义的审计。





    1. 生成和返回报告:最后,审计结果被用来生成一个JSON或HTML格式的报告。


    const report = await lighthouse(url, opts, config).then(results => {
    return results.report;
    });




    1. 关闭浏览器实例:报告生成后,CLI关闭Chrome实例。


    await browser.close();



// 伪代码
const puppeteer = require('puppeteer');
const lighthouse = require('lighthouse');
const {URL} = require('url');

async function run() {
// 使用 puppeteer 连接到 Chrome 浏览器
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});

// 新建一个页面
const page = await browser.newPage();

// 在这里,你可以执行任何Puppeteer代码,例如:
// await page.goto('https://example.com');
// await page.click('button');

const url = 'https://example.com';

// 使用 Lighthouse 进行审查
const {lhr} = await lighthouse(url, {
port: new URL(browser.wsEndpoint()).port,
output: 'json',
logLevel: 'info',
});

console.log(`Lighthouse score: ${lhr.categories.performance.score * 100}`);

await browser.close();
}

run();

导出的HTML文件


image.png


导出的JSON数据


image.png


实现一个性能守卫插件


在实现一个性能守卫插件,我们需要考虑以下因数:





    1. 易用性和灵活性:插件应该易于配置和使用,以便它可以适应各种不同的CI/CD环境和应用场景。它也应该能够适应各种不同的性能指标和阈值。





    1. 稳定性和可靠性:插件需要可靠和稳定,因为它将影响整个构建流程。任何失败或错误都可能导致构建失败,所以需要有强大的错误处理和恢复能力。





    1. 性能:插件本身的性能也很重要,因为它将直接影响构建的速度和效率。它应该尽可能地快速和高效。





    1. 可维护性和扩展性:插件应该设计得易于维护和扩展,以便随着应用和需求的变化进行适当的修改和更新。





    1. 报告和通知:插件应该能够提供清晰和有用的报告,以便开发人员可以快速理解和处理任何性能问题。它也应该有一个通知系统,当性能指标低于预定阈值时,能够通知相关人员。





    1. 集成:插件应该能够轻松集成到现有的CI/CD流程中,同时还应该支持各种流行的CI/CD工具和平台。





    1. 安全性:如果插件需要访问或处理敏感数据,如用户凭证,那么必须考虑安全性。应使用最佳的安全实践来保护数据,如使用环境变量来存储敏感数据。




image.png


// 伪代码
//perfci插件
const puppeteer = require('puppeteer');
const lighthouse = require('lighthouse');
const { port } = new URL(browser.wsEndpoint());

async function runAudit(url) {
const browser = await puppeteer.launch();
const { lhr } = await lighthouse(url, {
port,
output: 'json',
logLevel: 'info',
});
await browser.close();

// 在这里定义你的性能预期
const performanceScore = lhr.categories.performance.score;
if (performanceScore < 0.9) { // 如果性能得分低于0.9,脚本将抛出错误
throw new Error(`Performance score of ${performanceScore} is below the threshold of 0.9`);
}
}

runAudit('https://example.com').catch(console.error);


使用


name: CI
on: [push]
jobs:
lighthouseci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- run: npm install && npm install -g @lhci/cli@0.11.x
- run: npm run build
- run: perfci autorun


性能审计


const lighthouse = require('lighthouse');
const puppeteer = require('puppeteer');
const nodemailer = require('nodemailer');

// 配置邮件发送器
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: 'your-email@gmail.com',
pass: 'your-password',
},
});

// 定义一个函数用于执行Lighthouse审计并处理结果
async function runAudit(url) {
// 通过Puppeteer启动Chrome
const browser = await puppeteer.launch({ headless: true });
const { port } = new URL(browser.wsEndpoint());

// 使用Lighthouse进行性能审计
const { lhr } = await lighthouse(url, { port });

// 检查性能得分是否低于阈值
if (lhr.categories.performance.score < 0.9) {
// 如果性能低于阈值,发送警告邮件
let mailOptions = {
from: 'your-email@gmail.com',
to: 'admin@example.com',
subject: '网站性能低于阈值',
text: `Lighthouse得分:${lhr.categories.performance.score}`,
};

transporter.sendMail(mailOptions, function(error, info){
if (error) {
console.log(error);
} else {
console.log('Email sent: ' + info.response);
}
});
}

await browser.close();
}

// 使用函数
runAudit('https://example.com');


接下来,我们分步骤大概介绍下几个核心实现


数据告警


// 伪代码
const lighthouse = require('lighthouse');
const puppeteer = require('puppeteer');
const nodemailer = require('nodemailer');

// 配置邮件发送器
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: 'your-email@gmail.com',
pass: 'your-password',
},
});

// 定义一个函数用于执行Lighthouse审计并处理结果
async function runAudit(url) {
// 通过Puppeteer启动Chrome
const browser = await puppeteer.launch({ headless: true });
const { port } = new URL(browser.wsEndpoint());

// 使用Lighthouse进行性能审计
const { lhr } = await lighthouse(url, { port });

// 检查性能得分是否低于阈值
if (lhr.categories.performance.score < 0.9) {
// 如果性能低于阈值,发送警告邮件
let mailOptions = {
from: 'your-email@gmail.com',
to: 'admin@example.com',
subject: '网站性能低于阈值',
text: `Lighthouse得分:${lhr.categories.performance.score}`,
};

transporter.sendMail(mailOptions, function(error, info){
if (error) {
console.log(error);
} else {
console.log('Email sent: ' + info.response);
}
});
}

await browser.close();
}

// 使用函数
runAudit('https://example.com');

处理设备、网络等不稳定情况


// 伪代码

// 网络抖动
const { lhr } = await lighthouse(url, {
port,
emulatedFormFactor: 'desktop',
throttling: {
rttMs: 150,
throughputKbps: 1638.4,
cpuSlowdownMultiplier: 4,
requestLatencyMs: 0,
downloadThroughputKbps: 0,
uploadThroughputKbps: 0,
},
});


// 设备
const { lhr } = await lighthouse(url, {
port,
emulatedFormFactor: 'desktop', // 这里可以设定为 'mobile' 或 'desktop'
});


用户登录态问题



也可以让后端同学专门提供一条内网访问的登录态接口环境,仅用于测试环境



const puppeteer = require('puppeteer');
const lighthouse = require('lighthouse');
const fs = require('fs');
const axios = require('axios');
const { promisify } = require('util');
const { port } = new URL(browser.wsEndpoint());

// promisify fs.writeFile for easier use
const writeFile = promisify(fs.writeFile);

async function runAudit(url, options = { port }) {
// 使用Puppeteer启动Chrome
const browser = await puppeteer.launch();
const page = await browser.newPage();

// 访问登录页面
await page.goto('https://example.com/login');

// 输入用户名和密码
await page.type('#username', 'example_username');
await page.type('#password', 'example_password');

// 提交登录表单
await Promise.all([
page.waitForNavigation(), // 等待页面跳转
page.click('#login-button'), // 点击登录按钮
]);

// 运行Lighthouse
const { lhr } = await lighthouse(url, options);

// 保存审计结果到JSON文件
const resultJson = JSON.stringify(lhr);
await writeFile('lighthouse.json', resultJson);

// 上传JSON文件到服务器
const formData = new FormData();
formData.append('file', fs.createReadStream('lighthouse.json'));

// 上传文件到你的服务器
const res = await axios.post('https://your-server.com/upload', formData, {
headers: formData.getHeaders()
});

console.log('File uploaded successfully');

await browser.close();
}

// 运行函数
runAudit('https://example.com');

总结


性能插件插件还有很多需要考虑的情况,所以,不懂还是来私信问我吧,我同事要请

作者:linwu
来源:juejin.cn/post/7253331974051823675
我吃饭去了,不写了。

收起阅读 »

村镇级别geojson获取方法

web
前言 公司需要开发某个村镇的网格地图,个大搜索引擎找了地图板块都只能到村镇级的,在高德地图上搜索出来只是一个标记并没有详细的网格分布,并使用BigMap等工具尝试也只能到村镇不能到具体下面的网格。下文将介绍一种思路用于获取村镇的geojson。 准备工作 ...
继续阅读 »

前言


公司需要开发某个村镇的网格地图,个大搜索引擎找了地图板块都只能到村镇级的,在高德地图上搜索出来只是一个标记并没有详细的网格分布,并使用BigMap等工具尝试也只能到村镇不能到具体下面的网格。下文将介绍一种思路用于获取村镇的geojson。
1.png


准备工作



  • 需要转换村镇的png/svg图

  • Vector Magin用于将png转换为svg工具

  • Svg2geojson工具,git地址:Svg2geojson

  • geojson添加属性工具:geojson.io(需要T子)

  • geojson压缩工具:mapshaper(需要T子)


整体思路


2.jpeg


PNG转SVG


导入png图片


3.png


配置Vector Magic参数


我这里是一直点击下一步直到出现转换界面,这里也可基于自己的图片配置参数。出现下面界面表示已经转换完成,这里选择Edit Result能够对转换完成的svg进行编辑
image.png


Vector Magic操作



  • Pan(A)移动画布

  • Zap(D)删除某块区域

  • Fill(F)对某块区域进行填充颜色

  • Pencil(X)使用画笔进行绘制

  • Color(C)吸取颜色


操作完成点击Update完成svg更新


保存为svg


image.png



如果有svg图片本步骤可以省略,另外如果是UI出的svg图片注意边与边不能重合,不能到时候只能识别为一块区域



SVG转换为GeoJson


安装工具


npm install svg2geojson


获取村镇经纬度边界


使用BigMap选择对应的村镇,获取边缘四个点的经纬度并记录
uTools_1689736099805.png


编辑svg加入边界经纬度


<MetaInfo xmlns="http://www.prognoz.ru">
<Geo>
<GeoItem
X="0" Y="0"
Latitude="最右边的lat" Longitude="最上边的lng"
/>
<GeoItem
X="1445" Y="1047"
Latitude="最左边的lat" Longitude="最下边的lng"
/>
</Geo>
</MetaInfo>

最终的svg文件如下
image.png


转换svg


svg2geojson canggou.svg


使用geojson.io添加对应的属性


image.png
右边粘贴转换出来的geojson,点击对应的区域即可添加属性


注意事项⚠️



  1. 转换出来的geojson可能复制到geojson.io不能使用,可以先放到mapshaper里面然后导出geojson再使用geojson.io使用。

  2. 部分区域粘连问题(本来是多个区域,编辑时却是同一个区域),需要使用Vector Magin重新编辑下生成出来的svg,注意边界。


最终效果


PS:具体使用geojson需要自己百度下,下面是最终呈现的效果,地图有点丑请忽略还未来得及优化
image.png

收起阅读 »

我终于成功登上了JS 框架榜单,并且仅落后于 React 4 名!

web
前言 如期而至,我独立开发的 JavaScript 框架 Strve.js 迎来了一个大版本5.6.2。此次版本距离上次大版本发布已经接近半年之多,为什么这么长时间没有发布新的大版本呢?主要是研究 Strve.js 如何支持单文件组件,使代码智能提示、代码格式...
继续阅读 »

前言


如期而至,我独立开发的 JavaScript 框架 Strve.js 迎来了一个大版本5.6.2。此次版本距离上次大版本发布已经接近半年之多,为什么这么长时间没有发布新的大版本呢?主要是研究 Strve.js 如何支持单文件组件,使代码智能提示、代码格式化方面更加友好。之前也发布了 Strve SFC,但是由于其语法规则的繁琐以及是在运行时编译的种种原因,我果断放弃了这个方案的继续研究。而这次的版本5.6.2成功解决了代码智能提示、代码格式化方面友好的问题,另外还增加了很多锦上添花的特性,这些都归功于我们这次版本成功支持JSX语法。熟悉React的朋友知道,JSX语法非常灵活。 而 Strve.js 一大特性也就是灵活操作代码块,这里的代码块我们可以理解成函数,而JSX语法在一定场景下也恰恰满足了我们这种需求。


那么,我们如何在 Strve 项目中使用JSX语法呢?我们在Strve项目构建工具 CreateStrveApp 预置了模版,你可以选择 strve-jsx 或者 strve-jsx-apps 模版即可。我们使用 CreateStrveApp 搭建完 Strve 项目会发现,同时安装了babelPluginStrvebabelPluginJsxToStrve,这是因为我们需要使用 babelPluginJsxToStrve 将 JSX 转换为标签模版,之后再使用babelPluginStrve 将标签模版转换为 Virtual DOM,进而实现差异化更新视图。


尝试


我既然发布出了一个大版本,并且个人还算比较满意。那么下一步我如何推广它呢?毕竟毛遂自荐有时候还是非常有意义的。所以,我打算通过js-framework-benchmark 这个项目评估下性能。


js-framework-benchmark 是什么?我们这里就简单介绍下 js-framework-benchmark,它是一个用于比较 JavaScript 框架性能的项目。它旨在通过执行一系列基准测试来评估不同框架在各种场景下的性能表现。这些基准测试包括渲染大量数据、更新数据、处理复杂的 UI 组件等。通过运行这些基准测试,可以比较不同框架在各种方面的性能优劣,并帮助开发人员选择最适合其需求的框架。js-framework-benchmark 项目提供了一个包含多个流行 JavaScript 框架的基准测试套件。这些框架包括 Angular、React、Vue.js、Ember.js 等。每个框架都会在相同的测试场景下运行,然后记录下执行时间和内存使用情况等性能指标。通过比较这些指标,可以得出不同框架的性能差异。这个项目的目标是帮助开发人员了解不同 JavaScript 框架的性能特点,以便在选择框架时能够做出更加明智的决策。同时,它也可以促进框架开发者之间的竞争,推动框架的不断改进和优化。


那么,我们就抱着试试的心态去运行下这个项目。


测试


我们进入js-framework-benchmark Github主页,然后 clone 下这个项目。


git clone https://github.com/krausest/js-framework-benchmark.git

然后,我们 clone 到本地之后,打开 README.md 文件找到如何评估框架。大体浏览之后,我们得出的结论是:通过使用自己的框架完成js-framework-benchmark规定的练习项目。


01.png


那么,我们就照着其他框架已经开发完成的示例进行开发吧!在开发之前,我们必须要了解js-framework-benchmark 中有两种模式。一种是keyed,另一种是non-keyed。在 js-framework-benchmark 中,"keyed" 模式是指通过给数据项分配一个唯一标识符作为 "key" 属性,从而实现数据项与 DOM 节点之间的一对一关系。当数据发生变化时,与之相关联的 DOM 节点也会相应更新。而 "non-keyed" 模式是指当数据项发生变化时,可能会修改之前与其他数据项关联的 DOM 节点。因为 Strve 暂时没有类似唯一标识符这种特性,所以我们选择non-keyed模式。


我们打开项目下/frameworks/non-keyed文件夹,找一个案例框架看一下它们开发的项目,我们选择 Vue 吧!
我们根据它开发的样例迁移到自己的框架中去。为了测试新版本,我们将使用JSX语法进行开发。


import { createApp, setData } from "strve-js";
import { buildData } from "./data.js";

let selected = undefined;
let rows = [];

function setRows(update = rows.slice()) {
setData(
() => {
rows = update;
},
{
name: TbodyComponent,
}
);
}

function add() {
const data = rows.concat(buildData(1000));
setData(
() => {
rows = data;
},
{
name: TbodyComponent,
}
);
}

function remove(id) {
rows.splice(
rows.findIndex((d) => d.id === id),
1
);
setRows();
}

function select(id) {
setData(
() => {
selected = +id;
},
{
name: TbodyComponent,
}
);
}

function run() {
setRows(buildData());
selected = undefined;
}

function update() {
for (let i = 0; i < rows.length; i += 10) {
rows[i].label += " !!!";
}
setRows();
}

function runLots() {
setRows(buildData(10000));
selected = undefined;
}

function clear() {
setRows([]);
selected = undefined;
}

function swapRows() {
if (rows.length > 998) {
const d1 = rows[1];
const d998 = rows[998];
rows[1] = d998;
rows[998] = d1;
setRows();
}
}

function TbodyComponent() {
return (
<tbody $key>
{rows.map((item) => (
<tr
class={item.id === selected ? "danger" : ""}
data-label={item.label}
$key
>

<td class="col-md-1" $key>
{item.id}
</td>
<td class="col-md-4">
<a onClick={() => select(item.id)} $key>
{item.label}
</a>
</td>
<td class="col-md-1">
<a onClick={() => remove(item.id)} $key>
<span
class="glyphicon glyphicon-remove"
aria-hidden="true"
>
</span>
</a>
</td>
<td class="col-md-6"></td>
</tr>
))}
</tbody>

);
}

function MainBody() {
return (
<>
<div class="jumbotron">
<div class="row">
<div class="col-md-6">
<h1>Strve-non-keyed</h1>
</div>
<div class="col-md-6">
<div class="row">
<div class="col-sm-6 smallpad">
<button
type="button"
class="btn btn-primary btn-block"
id="run"
onClick={run}
>

Create 1,000 rows
</button>
</div>
<div class="col-sm-6 smallpad">
<button
type="button"
class="btn btn-primary btn-block"
id="runlots"
onClick={runLots}
>

Create 10,000 rows
</button>
</div>
<div class="col-sm-6 smallpad">
<button
type="button"
class="btn btn-primary btn-block"
id="add"
onClick={add}
>

Append 1,000 rows
</button>
</div>
<div class="col-sm-6 smallpad">
<button
type="button"
class="btn btn-primary btn-block"
id="update"
onClick={update}
>

Update every 10th row
</button>
</div>
<div class="col-sm-6 smallpad">
<button
type="button"
class="btn btn-primary btn-block"
id="clear"
onClick={clear}
>

Clear
</button>
</div>
<div class="col-sm-6 smallpad">
<button
type="button"
class="btn btn-primary btn-block"
id="swaprows"
onClick={swapRows}
>

Swap Rows
</button>
</div>
</div>
</div>
</div>
</div>
<table class="table table-hover table-striped test-data">
<component $name={TbodyComponent.name}>{TbodyComponent()}</component>
</table>
<span
class="preloadicon glyphicon glyphicon-remove"
aria-hidden="true"
>
</span>
</>

);
}

createApp(() => MainBody()).mount("#main");


其实,我们虽然使用了JSX语法,但是你会发现有很多特性并不与JSX语法真正相同,比如我们可以直接使用 class 去表示样式类名属性,而不能使用 className 表示。


评估案例项目开发完成了,我们下一步就要测试一下项目是否符合评估标准。


npm run bench non-keyed/strve

02.gif


测试标准包括:




  • create rows:创建行,页面加载后创建 1000 行的持续时间(无预热)




  • replace all rows:替换所有行,替换表中所有 1000 行所需的时间(5 次预热循环)。该指标最大的价值就是了解当页面上的大部分内容发生变化时库的执行方式。




  • partial update:部分更新,对于具有 10000 行的表,每 10 行更新一次文本(进行 5 次预热循环)。该指标是动画性能和深层嵌套数据结构开销等方面的最佳指标。




  • select row:选择行,在单击行时高亮显示该行所需的时间(进行 5 次预热循环)。




  • swap rows:交换行,在包含 1000 行的表中交换 2 行的时间(进行 5 次预热迭代)。




  • remove row:删除行,在包含 1,000 行的表格上移除一行所需的时间(有 5 次预热迭代),该指标可能变化最少,因为它比库的任何开销更多地测试浏览器布局变化(因为所有行向上移动)。




  • create many rows:创建多行,创建 10000 行所需的时间(没有预热),该指标更容易受到内存开销的影响,并且对于效率较低的库来说,扩展性会更差。




  • append rows to large table:追加行到大型表格,在包含 10000 行的表格上添加 1000 行所需的时间(没有预热)。




  • clear rows:清空行,清空包含 10000 行的表格所需的时间(没有预热),该指标说明了库清理代码的成本,内存使用对这个指标的影响很大,因为浏览器需要更多的 GC。




最终,Strve 顶住了压力,通过了测试。


03.gif


看到了successful run之后,觉得特别开心!那种成就感是任何事物都难以代替的。


跑分


我们既然通过了测试,那么下一步我们将与前端两大框架Vue、React进行比较跑分,我们先在我自己本地环境上跑一下,看一下效果。


性能测试基准分为三类:



  • 持续时间

  • 启动指标

  • 内存分配


持续时间


04.png


启动指标


05.png


内存分配


06.png


总体而言,我感觉还不错,毕竟跟两个大哥在比较。到这里我还是觉得不够,跟其他框架比比呢!


提交


只要框架通过了测试,并且按照提交PR的规定提交,是可以被选录到 js-framework-benchmark 中去的。


好,那我们就去试试!


07.png


又一个比较有成就感的事!提交的PR被作者合并了!


成绩单


我迫不及待的去榜单上看下我的排名,会不会垫底啊!


因为浏览器版本发布的时差问题,暂时 Official results ( 官方结果 ) 还没有发布最新结果,我们可以先来 Snapshot of the results ( 快照结果 ) 中查看。


我们打开下方网址就可以看到JS框架的最新榜单了。


https://krausest.github.io/js-framework-benchmark/current.html

我们在持续时间这个类别下从后往前找,目前63个框架我居然排名 50 名,并且大名鼎鼎的 React 排名45名。


08.png


我们先不激动,我们再看下启动指标类别。Strve 平均分数是1.04,我看了看好几个框架分数是1.04。Strve 可以排到前8名。


09.png


我们再稳一下,继续看内存分配这个类别。Strve 平均分数是1.40,Strve 可以排到前12名。


10.png


意义


js-framework-benchmark 的测试结果是相对准确的,因为它是针对同样的测试样本和基准测试情境进行比较,可以提供框架之间的相对性能比较。然而,需要注意的是,这个测试结果也只是反映了测试条件下的性能表现。框架实际的性能可能还会受到很多方面的影响。
此外,js-framework-benchmark 测试结果也不应该成为选择框架的唯一指标。在选择框架时,还需要考虑框架的生态、开发效率、易用性等多方面因素,而不仅仅是性能表现。


虽然,Strve 跟 React 比较是有点招黑,但是不妨这样想,榜样的力量是巨大的!只有站在巨人的肩膀上才能望得更远!


Strve 要走的路还有很长,入选JS框架榜单使我更加明确了方向。我觉得做自己喜欢做得事情,这样才会有意义!


加油


Strve 要继续维护下去,我也会不断学习,继续精进。



Strve 源码仓库:github.com/maomincodin…




Strve 中文文档:maomincoding.gitee.io/strve-doc-z…



谢谢大家的阅读!如果大家觉得Strve不错,麻烦帮我点下Star吧!


作者:前端历劫之路
来源:juejin.cn/post/7256250499280158776
收起阅读 »

web端实现远程桌面控制

web
阐述 应着标题所说,web端实现远程桌面控制,方案有好几种,达到的效果就是类似于向日葵一样可以远程桌面,但是操作方可以不用安装客户端,只需要一个web浏览器即可实现,桌面端需写一个程序用来socket连接和执行Windows指令。 实现方案 使用webSock...
继续阅读 »

阐述


应着标题所说,web端实现远程桌面控制,方案有好几种,达到的效果就是类似于向日葵一样可以远程桌面,但是操作方可以不用安装客户端,只需要一个web浏览器即可实现,桌面端需写一个程序用来socket连接和执行Windows指令。


实现方案


使用webSocket实现web端和桌面端的实时TCP通讯和连接,连接后桌面端获取自己的桌面流以照片流的形式截图发送blob格式给web端,web端再接收后将此格式解析再赋值在img标签上,不停的接收覆盖,类似于快照的形式来呈现画面,再通过api获取web端的鼠标事件和键盘事件通过webSocket发送给客户端让他执行Windows事件,以此来达到远程控制桌面控制效果。


Demo实现


因为为了方便同事观看,所以得起一个框架服务,我习惯用vue3了,但大部分都是js代码,可参考改写。


html只需一行搞定。


<div>
<img ref="imageRef" class="remote" src="" alt="">
</div>

接下来就是socket连接,用socket库和直接new webSocket都可以连接,我习惯用库了,因为多一个失败了可以自动连接的功能,少写一点代码🤣,库也是轻量级才几k大小。顺便把socket心跳也加上,这个和对端协商好就行了,一般都是ping/pong加个type值,发送时记得处理一下使用json格式发送,如果连接后60秒后没有互相发送消息客户端就会认为你是失联断开连接了,所以他就会强行踢掉你连接状态,所以心跳机制还是必不可少的。


import ReconnectingWebSocket from 'reconnecting-websocket'
const remoteControl = '192.168.1.175'
const scoketURL = `ws://${remoteControl}:10086/Echo`
const imageRef = ref()

onMounted(() => {
createdWebsocket()
})

const createdWebsocket = () => {
socket = new ReconnectingWebSocket(scoketURL)
socket.onopen = function () {
console.log('连接已建立')
resetHeart()
}
socket.onmessage = function (event) {
// console.log(event.data)
const objectDta = JSON.parse(event.data)
console.log(objectDta)
if (objectDta.type === 100) {
resetHeart()
}
}
socket.onclose = function () {
console.log('断开连接')
}
}

let heartTime = null // 心跳定时器实例
let socketHeart = 0 // 心跳次数
let HeartTimeOut = 20000 // 心跳超时时间
let socketError = 0 // 错误次数
// socket 重置心跳
const resetHeart = () => {
socketHeart = 0
socketError = 0
clearInterval(heartTime)
sendSocketHeart()
}
const sendSocketHeart = () => {
heartTime = setInterval(() => {
if (socketHeart <= 3) {
console.log('心跳发送:', socketHeart)
socket.send(
JSON.stringify({
type: 100,
key: 'ping'
})
)
socketHeart = socketHeart + 1
} else {
reconnect()
}
}, HeartTimeOut)
}
// socket重连
const reconnect = () => {
socket.close()
if (socketError <= 3) {
clearInterval(heartTime)
socketError = socketError + 1
console.log('socket重连', socketError)
} else {
console.log('重试次数已用完的逻辑', socketError)
clearInterval(heartTime)
}
}

成功稳定连接后那么恭喜你完成第一步了,接下来就是获取对端发来的照片流了,使用socket.onmessageapi用来接收对端消息,需要转一下json,因为发送的数据照片流很快,控制台直接刷屏了,所以简单处理一下。收到照片流把blob格式处理一下再使用window.URL.createObjectURL(blob)赋值给img即可。


socket.onmessage = function (event) {
// console.log(event.data)
if (event.data instanceof Blob) { // 处理桌面流
const data = event.data
const blob = new Blob([data], { type: "image/jpg" })
imageRef.value.src = window.URL.createObjectURL(blob)
} else {
const objectDta = JSON.parse(event.data)
console.log(objectDta)
if (objectDta.type === 100) {
resetHeart()
}
}
}

此时页面可以呈现画面了,并且是可以看得到对面操作的,但让人挠头的是,分辨率和尺寸不对,有上下和左右的滚动条显示,并不是百分百的,解决这个问题倒是不难,但如需考虑获取自身的鼠标坐标发送给对端,这个坐标必须准确无误,简单来说就是分辨率自适应,因为web端使用的电脑屏幕大小是不一样的,切桌面端发送给你的桌面流比如是全屏分辨率的,以此得做适配,这个放后面解决,先来处理鼠标和键盘事件,纪录下来并发送对应的事件给桌面端。记得去除浏览器的拖动和鼠标右键事件,以免效果紊乱。


const watchControl = () => { // 监听事件
window.ondragstart = function (e) { // 移除拖动事件
e.preventDefault()
}
window.ondragend = function (e) { // 移除拖动事件
e.preventDefault()
}
window.onkeydown = function (e) { // 键盘按下
console.log('键盘按下', e)
socket.send(JSON.stringify({ type: 0, key: e.keyCode }))
}
window.onkeyup = function (e) { // 键盘抬起
console.log('键盘抬起', e)
socket.send(JSON.stringify({ type: 1, key: e.keyCode }))
}
window.onmousedown = function (e) { // 鼠标单击按下
console.log('单击按下', e)
console.log(newPageX, 'newPageX')
console.log(newPageY, 'newPageY')
socket.send(JSON.stringify({ type: 5, x: pageX, y: pageY }))
}
window.onmouseup = function (e) { // 鼠标单击抬起
console.log('单击抬起', e)
socket.send(JSON.stringify({ type: 6, x: pageX, y: pageY }))
}
window.oncontextmenu = function (e) { // 鼠标右击
console.log('右击', e)
e.preventDefault()
socket.send(JSON.stringify({ type: 4, x: pageX, y: pageY }))
}
window.ondblclick = function (e) { // 鼠标双击
console.log('双击', e)
}
window.onmousewheel = function (e) { // 鼠标滚动
console.log('滚动', e)
const moving = e.deltaY / e.wheelDeltaY
socket.send(JSON.stringify({ type: 7, x: e.x, y: e.y, deltaY: e.deltaY, deltaFactor: moving }))
}
window.onmousemove = function (e) { // 鼠标移动
if (!timer) {
timer = setTimeout(function () {
console.log("鼠标移动:X轴位置" + e.pageX + ";Y轴位置:" + e.pageY)
socket.send(JSON.stringify({ type: 2, x: pageX, y: pageY }))
timer = null
}, 60)
}
}
}

现在就可以实现远程控制了,发送的事件类型根据桌面端服务需要什么参数协商好就成,接下来就是处理分辨率适配问题了,解决办法大致就是赋值img图片后拿到他的参数分辨率,然后获取自身浏览器的宽高,除以他的分辨率再乘上自身获取的鼠标坐标就OK了,获取img图片事件需要延迟一下,因为是后面socket连接后才赋值的图片,否则宽高就一直是0,加在watchControl事件里面,发送时坐标也要重新计算。


const watchControl = () => {
console.dir(imageRef.value)
imgWidth.value = imageRef.value.naturalWidth === 0 ? 1920 : imageRef.value.naturalWidth// 图片宽度
imgHeight.value = imageRef.value.naturalHeight === 0 ? 1080 : imageRef.value.naturalHeight // 图片高度
clientHeight = document.body.offsetHeight

......

window.onmousedown = function (e) { // 鼠标单击按下
console.log('单击按下', e)
const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率
const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight))
console.log(newPageX, 'newPageX')
console.log(newPageY, 'newPageY')
socket.send(JSON.stringify({ type: 5, x: newPageX, y: newPageY }))
}
}

现在就几乎大功告成了,坐标稳定发送,获取的也是正确计算出来的,下面再做一些socket加密优化,还有事件优化,集成到项目里面离开时还是要清除所有事件和socket连接,直接上完整全部代码。


<template>
<div>
<img ref="imageRef" class="remote" src="" alt="" />
</div>
</template>

<script setup>
import ReconnectingWebSocket from 'reconnecting-websocket'
import { Base64 } from 'js-base64'

onMounted(() => {
createdWebsocket()
})

const route = useRoute()
let socket = null
const secretKey = 'keyXXXXXXX'
const remoteControl = '192.168.1.xxx'
const scoketURL = `ws://${remoteControl}:10086/Echo?key=${Base64.encode(secretKey)}`
const imageRef = ref()
let timer = null
const clientWidth = document.documentElement.offsetWidth
let clientHeight = null
const widthCss = (window.innerWidth) + 'px'
const heightCss = (window.innerHeight) + 'px'
const imgWidth = ref() // 图片宽度
const imgHeight = ref() // 图片高度

const createdWebsocket = () => {
socket = new ReconnectingWebSocket(scoketURL)
socket.onopen = function () {
console.log('连接已建立')
resetHeart()
setTimeout(() => {
watchControl()
}, 500)
}
socket.onmessage = function (event) {
if (event.data instanceof Blob) { // 处理桌面流
const data = event.data
const blob = new Blob([data], { type: 'image/jpg' })
imageRef.value.src = window.URL.createObjectURL(blob)
} else {
const objectDta = JSON.parse(event.data)
console.log(objectDta)
if (objectDta.type === 100) {
resetHeart()
}
}
}
socket.onclose = function () {
console.log('断开连接')
}
}

const handleMousemove = (e) => { // 鼠标移动
if (!timer) {
timer = setTimeout(function () {
const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率
const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight))
// console.log(newPageX, 'newPageX')
// console.log(newPageY, 'newPageY')
// console.log('鼠标移动:X轴位置' + e.pageX + ';Y轴位置:' + e.pageY)
socket.send(JSON.stringify({ type: 2, x: newPageX, y: newPageY }))
timer = null
}, 60)
}
}
const handleKeydown = (e) => { // 键盘按下
console.log('键盘按下', e)
socket.send(JSON.stringify({ type: 0, key: e.keyCode }))
}
const handleMousedown = (e) => { // 鼠标单击按下
console.log('单击按下', e)
const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率
const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight))
console.log(newPageX, 'newPageX')
console.log(newPageY, 'newPageY')
socket.send(JSON.stringify({ type: 5, x: newPageX, y: newPageY }))
}
const handleKeyup = (e) => { // 键盘抬起
console.log('键盘抬起', e)
socket.send(JSON.stringify({ type: 1, key: e.keyCode }))
}

const handleMouseup = (e) => { // 鼠标单击抬起
console.log('单击抬起', e)
const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率
const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight))
console.log(newPageX, 'newPageX')
console.log(newPageY, 'newPageY')
socket.send(JSON.stringify({ type: 6, x: newPageX, y: newPageY }))
}

const handleContextmenu = (e) => { // 鼠标右击
console.log('右击', e)
e.preventDefault()
const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率
const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight))
console.log(newPageX, 'newPageX')
console.log(newPageY, 'newPageY')
socket.send(JSON.stringify({ type: 4, x: newPageX, y: newPageY }))
}

const handleDblclick = (e) => { // 鼠标双击
console.log('双击', e)
}

const handleMousewheel = (e) => { // 鼠标滚动
console.log('滚动', e)
const moving = e.deltaY / e.wheelDeltaY
socket.send(JSON.stringify({ type: 7, x: e.x, y: e.y, deltaY: e.deltaY, deltaFactor: moving }))
}

const watchControl = () => { // 监听事件
console.dir(imageRef.value)
imgWidth.value = imageRef.value.naturalWidth === 0 ? 1920 : imageRef.value.naturalWidth// 图片宽度
imgHeight.value = imageRef.value.naturalHeight === 0 ? 1080 : imageRef.value.naturalHeight // 图片高度
clientHeight = document.body.offsetHeight

window.ondragstart = function (e) { // 移除拖动事件
e.preventDefault()
}
window.ondragend = function (e) { // 移除拖动事件
e.preventDefault()
}
window.addEventListener('mousemove', handleMousemove)
window.addEventListener('keydown', handleKeydown)
window.addEventListener('mousedown', handleMousedown)
window.addEventListener('keyup', handleKeyup)
window.addEventListener('mouseup', handleMouseup)
window.addEventListener('contextmenu', handleContextmenu)
window.addEventListener('dblclick', handleDblclick)
window.addEventListener('mousewheel', handleMousewheel)
}

let heartTime = null // 心跳定时器实例
let socketHeart = 0 // 心跳次数
const HeartTimeOut = 20000 // 心跳超时时间
let socketError = 0 // 错误次数
// socket 重置心跳
const resetHeart = () => {
socketHeart = 0
socketError = 0
clearInterval(heartTime)
sendSocketHeart()
}
const sendSocketHeart = () => {
heartTime = setInterval(() => {
if (socketHeart <= 3) {
console.log('心跳发送:', socketHeart)
socket.send(
JSON.stringify({
type: 100,
key: 'ping'
})
)
socketHeart = socketHeart + 1
} else {
reconnect()
}
}, HeartTimeOut)
}
// socket重连
const reconnect = () => {
socket.close()
if (socketError <= 3) {
clearInterval(heartTime)
socketError = socketError + 1
console.log('socket重连', socketError)
} else {
console.log('重试次数已用完的逻辑', socketError)
clearInterval(heartTime)
}
}

onBeforeUnmount(() => {
socket.close()
console.log('组件销毁')
window.removeEventListener('mousemove', handleMousemove)
window.removeEventListener('keydown', handleKeydown)
window.removeEventListener('mousedown', handleMousedown)
window.removeEventListener('keyup', handleKeyup)
window.removeEventListener('mouseup', handleMouseup)
window.removeEventListener('contextmenu', handleContextmenu)
window.removeEventListener('dblclick', handleDblclick)
window.removeEventListener('mousewheel', handleMousewheel)
})
</script>

<style scoped>
.remote {
width: v-bind(widthCss);
height: v-bind(heightCss);
}
</style>

现在就算是彻底大功告成了,加密密钥或者方式还是和对端协商,流畅度和清晰度也不错的,简单办公还是没问题的,和不开会员的向日葵效果差不多,后面的优化方式大致围绕着图片压缩来做应该能达到更加流畅的效果,如果项目是https的话socket服务也要升级成wss协议,大致就这样,若有不正确的地

作者:小胡不糊涂
来源:juejin.cn/post/7256970964533297211
方还请指正一番😁😁。

收起阅读 »

前端基建原来可以做这么多事情

web
前端基建是指在前端开发过程中,为提高开发效率、代码质量和团队协作而构建的一些基础设施和工具。下面是前端基建可以做的一些事情: 脚手架工具:开发和维护一个通用的脚手架工具,可以帮助团队快速初始化项目结构、配置构建工具、集成常用的开发依赖等。 组件库:开发...
继续阅读 »

guide-cover-2.2d36b370.jpg


前端基建是指在前端开发过程中,为提高开发效率、代码质量和团队协作而构建的一些基础设施和工具。下面是前端基建可以做的一些事情:




  1. 脚手架工具:开发和维护一个通用的脚手架工具,可以帮助团队快速初始化项目结构、配置构建工具、集成常用的开发依赖等。




  2. 组件库:开发和维护一个内部的组件库,包含常用的UI组件、业务组件等,提供给团队成员复用,减少重复开发的工作量。




  3. 构建工具和打包工具:搭建和维护一套完善的构建和打包工具链,包括使用Webpack、Parcel等工具进行代码的压缩、合并、打包等工具,优化前端资源加载和性能。




  4. 自动化测试工具:引入自动化测试工具,如Jest、Mocha等,编写和维护测试用例,进行单元测试、集成测试、UI测试等,提高代码质量和可靠性。




  5. 文档工具:使用工具如JSDoc、Swagger等,生成项目的API文档、接口文档等,方便团队成员查阅和维护。




  6. Git工作流:制定和规范团队的Git工作流程,使用版本控制工具管理代码,方便团队协作和代码回退。




  7. 性能监控和优化:引入性能监控工具,如Lighthouse、Web Vitals等,对项目进行性能分析,优化网页加载速度、响应时间等。




  8. 工程化规范:制定并推广团队的代码规范、目录结构规范等,提高代码的可读性、可维护性和可扩展性。




  9. 持续集成和部署:搭建持续集成和部署系统,如Jenkins、Travis CI等,实现代码的自动构建、测试和部署,提高开发效率和代码质量。




  10. 项目文档和知识库:建立一个内部的项目文档和知识库,记录项目的技术细节、开发经验、常见问题等,方便团队成员查阅和学习。




通过建立和维护前端基建,可以提高团队的协作效率,减少重复劳动,提高代码质量和项目的可维护性。


当涉及到前端基建时,还有一些其他的事情可以考虑:




  1. 代码质量工具:引入代码质量工具,如ESLint、Prettier等,对代码进行静态分析和格式化,提高代码的一致性和可读性。




  2. 国际化支持:为项目添加国际化支持,可以通过引入国际化库,如i18next、vue-i18n等,实现多语言的切换和管理。




  3. 错误监控和日志收集:引入错误监控工具,如Sentry、Bugsnag等,实时监控前端错误,并收集错误日志,方便进行问题排查和修复。




  4. 前端性能优化工具:使用工具如WebPageTest、Chrome DevTools等,对项目进行性能分析和优化,提高页面加载速度、响应时间等。




  5. 缓存管理:考虑合理利用浏览器缓存和服务端缓存,减少网络请求,提升用户访问速度和体验。




  6. 移动端适配:针对移动端设备,采用响应式设计或使用CSS媒体查询等技术,实现移动端适配,保证页面在不同尺寸的设备上有良好的显示效果。




  7. 安全防护:对项目进行安全审计,使用安全防护工具,如CSP(Content Security Policy)、XSS过滤等,保护网站免受常见的安全攻击。




  8. 性能优化指标监控:监控和分析关键的性能指标,如页面加载时间、首次渲染时间、交互响应时间等,以便及时发现和解决性能问题。




  9. 前端日志分析:使用日志分析工具,如ELK(Elasticsearch、Logstash、Kibana)等,对前端日志进行收集和分析,了解用户行为和页面异常情况。




  10. 跨平台开发:考虑使用跨平台开发框架,如React Native、Flutter等,实现一套代码在多个平台上复用,提高开发效率。




以上是一些可以考虑的前端基建事项,根据项目需求和团队情况,可以选择适合的工具和技术进行实施。同时,持续关注前端领域的最新技术和工具,不断优化和改进前端基建,以提高开发效率和项目质量。


当涉及到前端基建时,还有一些其他的事情可以考虑:




  1. 编辑器配置和插件:为团队提供统一的编辑器配置文件,包括代码格式化、语法高亮、代码自动补全等,并推荐常用的编辑器插件,提高开发效率。




  2. 文档生成工具:使用工具如Docusaurus、VuePress等,为项目生成漂亮的文档网站,方便团队成员查阅和维护项目文档。




  3. Mock数据和接口管理:搭建一个Mock服务器,用于模拟后端接口数据,方便前端开发和测试,同时可以考虑使用接口管理工具,如Swagger等,方便接口的定义和调试。




  4. 前端监控和统计:引入前端监控工具,如Google Analytics、百度统计等,收集用户访问数据和行为信息,用于分析和优化用户体验。




  5. 移动端调试工具:使用工具如Eruda、VConsole等,帮助在移动端设备上进行调试和错误排查,提高开发效率。




  6. 自动化部署:配置自动化部署流程,将项目的代码自动部署到服务器或云平台,减少人工操作,提高发布效率和稳定性。




  7. 前端团队协作工具:使用团队协作工具,如GitLab、Bitbucket等,提供代码托管、项目管理、任务分配和团队沟通等功能,增强团队协作效率。




  8. 前端培训和知识分享:组织定期的前端培训和技术分享会,让团队成员相互学习和交流,推动技术的共享和提升。




  9. 客户端性能优化:针对移动端应用,可以使用工具如React Native Performance、Weex等,进行客户端性能优化,提高应用的响应速度和流畅度。




  10. 技术选型和评估:定期评估和研究前端技术的发展趋势,选择适用的技术栈和框架,以保持项目的竞争力和可持续发展。




以上是一些可以考虑的前端基建事项,根据项目需求和团队情况,可以选择适合的工具和技术进行实施。同时,持续关注前端领域的最新技术和工具,不断优化和改进前端基建,以提高开发效率和项目质量。


当涉及到前端基建时,还有一些其他的事情可以考虑:




  1. 统一的状态管理:引入状态管理工具,如Redux、Vuex等,帮助团队管理前端应用的状态,提高代码的可维护性和可扩展性。




  2. 前端日志记录:引入前端日志记录工具,如log4javascript、logrocket等,记录前端应用的运行日志,方便排查和解决问题。




  3. 前端代码扫描:使用静态代码扫描工具,如SonarQube、CodeClimate等,对前端代码进行扫描和分析,发现潜在的问题和漏洞。




  4. 前端数据可视化:使用数据可视化工具,如ECharts、Chart.js等,将数据以图表或图形的形式展示,增强数据的可理解性和可视化效果。




  5. 前端容灾和故障处理:制定容灾方案和故障处理流程,对前端应用进行监控和预警,及时处理和恢复故障,提高系统的可靠性和稳定性。




  6. 前端安全加固:对前端应用进行安全加固,如防止XSS攻击、CSRF攻击、数据加密等,保护用户数据的安全性和隐私。




  7. 前端版本管理:建立前端代码的版本管理机制,使用工具如Git、SVN等,管理和追踪代码的变更,方便团队成员之间的协作和版本控制。




  8. 前端数据缓存:考虑使用Local Storage、Session Storage等技术,对一些频繁使用的数据进行缓存,提高应用的性能和用户体验。




  9. 前端代码分割:使用代码分割技术,如Webpack的动态导入(Dynamic Import),将代码按需加载,减少初始加载的资源大小,提高页面加载速度。




  10. 前端性能监测工具:使用性能监测工具,如WebPageTest、GTmetrix等,监测前端应用的性能指标,如页面加载时间、资源加载时间等,进行性能优化。




以上是一些可以考虑的前端基建事项,根据项目需求和团队情况,可以选择适合的工具和技术进行实施。同时,持续关注前端领域的最新技术和工具,不断优化和改进前端基建,以提高

作者:服部
来源:juejin.cn/post/7256879435339628604
开发效率和项目质量。

收起阅读 »

无虚拟 DOM 版 Vue 进行到哪一步了?

web
前言 就在一年前的 Vue Conf 2022,尤雨溪向大家分享了一个非常令人期待的新模式:无虚拟 DOM 模式! 我看了回放之后非常兴奋,感觉这是个非常牛逼的新 feature,于是赶紧写了篇文章: 《无虚拟 DOM 版 Vue 即将到来》 鉴于可能会有...
继续阅读 »

前言


就在一年前的 Vue Conf 2022,尤雨溪向大家分享了一个非常令人期待的新模式:无虚拟 DOM 模式!


我看了回放之后非常兴奋,感觉这是个非常牛逼的新 feature,于是赶紧写了篇文章:



《无虚拟 DOM 版 Vue 即将到来》



鉴于可能会有部分人还不知道或者还没听过什么是 Vue 无虚拟 DOM 模式,我们先来简单的介绍一下:Vue 无虚拟 DOM 编译模式在官方那边叫 Vue Vapor Mode,直译过来就是:Vue 蒸汽模式。


为什么叫蒸汽模式呢?个人瞎猜的哈:第一次工业革命开创了以机器代替手工劳动的时代,并且是以蒸汽机作为动力机被广泛使用为标志的。这跟 Vue1 有点像,Vue 赶上了前端界的第一次工业革命(以声明式代替命令式的时代),此时的 Vue 还没有虚拟 DOM,也就是 Vue 的蒸汽时代。


不过万万没想到的是历史居然是个轮回,当年火的不行的虚拟 DOM 如今早已日薄西山、跌落了神坛,现在无虚拟 DOM 居然又开始重返王座。当然重返王座这个说法也不是很准确,只能说开始演变成为了一种新的流行趋势吧!这无疑让尤大想起了那个蒸汽时代的 Vue1,于是就起名为 Vapor


当然也有这么一种可能:自从为自己的框架起名为 Vue 之后,尤大就特别钟意以 V 开头的单词,不信你看:



  • Vue

  • Vite

  • Vetur

  • Volar

  • Vapor


不过以上那些都是自己瞎猜的,人家到底是不是那么想的还有待商榷。可以等下次他再开直播时发弹幕问他,看他会怎么回答。


但是吧,都过了一年多了,这个令我特别期待的新特性一点信都没有,上网搜到的内容也都是捕风捉影,这甚至让我开始怀疑 Vapor Mode 是不是要难产了?不过好在一年后的今天,Vue Conf 2023 如期而至,在那里我终于看到了自己所期待的与 Vapor Mode 有关的一系列信息。


正文



他说第三第四季度会主要开发 Vapor Mode,我听了以后直呼好家伙!合着这一年的功夫一点关于 Vapor Mode 相关的功能都没开发,鸽了一年多啊!




[译]:这是一个全新的编译策略,还是相同的模板语法一点没变,但编译后的代码性能更高。利用 Template 标签克隆元素 + 更精准的绑定,并且没有虚拟 DOM




他说 Vapor 是一个比较大的工程,所以会分阶段开发,他目前处在第一阶段。第一阶段是运行时,毕竟以前的组件编译出来的是虚拟 DOM,而 Vapor 编译出的则是真实 DOM,这需要运行时的改变。他们基本已经实现了这一点,现在正在做一些性能测试,测试效果很不错,性能有大幅度的提升。



下一阶段则是编译器,也就是说他们现在虽然能写出一些 Vapor Mode 的代码来测试性能,但写的基本上都是编译后的代码,人肉编译无疑了。



第三阶段是集成,第四阶段是兼容特殊组件,接下来进行每个阶段的详细介绍。


第一阶段



他们先实现了 v-ifv-for 等核心指令的 runtime,看来以前的 v-ifv-for 代码不能复用啊,还得重新实现。然后他们用 Benchmark 做了一些基准测试,效果非常理想,更合理的内存利用率,性能有着明显的提升。还有与服务端渲染兼容的一些东西,他们还是比较重视 SSR 的。


第二阶段



他们希望能生成一种中间语言,因为现在用 JSX 的人越来越多了,我知道肯定有人会说我身边一个用 JSX 的都没有啊(指的是 Vue JSX,不是 React JSX)咱们暂且先不讨论这种身边统计法的准确性,咱就说 Vue 的那些知名组件库,大家可以去看看他们有多少都用了 JSX 来进行开发的。只能说是 JSX 目前相对于 SFC 而言用的比较少而已,但它的用户量其实已经很庞大了:



我知道肯定还会有人说:这个统计数据不准,别的包依赖了这个包,下载别的包的时候也会顺带着下载这个包。其实这个真的没必要杠,哪怕说把这个数据减少到一半那都是每周 50 万的下载量呢!就像是国内 185 的比例很小吧?但你能说国内 185 的人很少么?哪怕比例小,但由于总数大,一相乘也照样是个非常庞大的数字。


Vue 以前是通过 render 函数来进行组件的渲染的,而如今 Vapor Mode 已经没有 render 函数了,所以不能再手写 render 了,来看一个 Vue 官网的例子:



由于 Vapor Mode 不支持 render 函数,如果想要拥有同样的灵活性那就只有 JSX,所以他们希望 SFCJSX 能编译成同一种中间语言,然后再编译为真实 DOM


第三阶段



尤大希望 Vapor Mode 是个渐进式的功能而不是破坏性功能,所以他们要做的是让 Vapor Mode 的代码可以无缝嵌入到你现有的项目中而不必重构。不仅可以选择在组件级别嵌入,甚至还可以选择在项目的性能瓶颈部分嵌入 Vapor Mode。如果你开发的是一个新项目的话,你也可以让整个项目都是 Vapor Mode,这样的话就可以完全删除掉虚拟 DOM 运行时,打包出来的尺寸体积会更小。


最牛逼的是还可以反向操作,还可以在无虚拟 DOM 组件里运行虚拟 DOM 组件。比方说你开发了款无虚拟 DOM 应用,但你需要组件库,组件库是虚拟 DOM 写的,没关系,照样可以完美运行!


第四阶段



这一阶段要让 Vapor 支持一些特殊组件,包括:



  • <transition>

  • <keep-alive>

  • <teleport>

  • <suspense>


等这一阶段忙完,整个 Vapor Mode 就可以正式推出了。


源码解析


本想着带大家看看源码,但非常不幸的是目前没在 GitHubVue 仓库里发现任何有关 Vapor Mode 的分支,可能是还没传呢吧。关注我,我会实时紧跟 Vue Vapor 的动态,并会试图带着大家理解源码。其实我是希望他能早点把源码给放出来的,因为一个新功能或者一个新项目就是在最初始的阶段最好理解,源码也不会过于的复杂,后续随着功能的疯狂迭代慢慢的就不那么易于理解了。而且跟着最初的源码也可以很好的分析出他的后续思路,想要实现该功能后面要怎么做,等放出下一阶段源码时就能很好的延续这种思路,这对于我们学习高手思路而言非常有帮助。


而且我怀疑会有些狗面试官现在就开始拿跟这玩意有关的东西做面试题了,你别看这项功能还没正式推出,但有些狗官就是喜欢问这些,希望能把你问倒以此来压你价。


我们经常调侃说学不动了,尤雨溪还纳闷这功能不影响正常使用啊?有啥学习成本呢?如果他真的了解国情的话就会知道学不动的压根就

作者:手撕红黑树
来源:juejin.cn/post/7256983702810181688
不是写法,而是源码!

收起阅读 »

Vite 开发环境为何这么快?

web
本文只是笔者作为一个初学者,在学习中与看了诸多业界的优秀实践文章之后的思考和沉淀,如果你在看的过程中觉得有些不妥的地方,可以随时和我联系,一起探讨学习。 提到 Vite,第一个想到的字就是 快,到底快在哪里呢?为什么可以这么快? 本文从以下几个地方来讲 快...
继续阅读 »

本文只是笔者作为一个初学者,在学习中与看了诸多业界的优秀实践文章之后的思考和沉淀,如果你在看的过程中觉得有些不妥的地方,可以随时和我联系,一起探讨学习。



提到 Vite,第一个想到的字就是 ,到底快在哪里呢?为什么可以这么快?
本文从以下几个地方来讲



  • 快速的冷启动: No Bundle + esbuild 预构建

  • 模块热更新:利用浏览器缓存策略

  • 按需加载:利用浏览器 ESM 支持


Vite 本质上是一个本地资源服务器,还有一套构建指令组成。



  • 本地资源服务器,基于 ESM 提供很多内建功能,HMR 速度很快

  • 使用 Rollup 打包你的代码,预配件了优化的过配置,输出高度优化的静态资源


快递的冷启动


No-bundle


在冷启动开发者服务器时,基于 Webpack 这类 bundle based 打包工具,启动时必须要通过 依赖收集、模块解析、生成 chunk、生成模块依赖关系图,最后构建整个应用输出产物,才能提供服务。


这意味着不管代码实际是否用到,都是需要被扫描和解析。


image.png


而 Vite 的思路是,利用浏览器原生支持 ESM 的原理,让浏览器来负责打包程序的工作。而 Vite 只需要在浏览器请求源码时进行转换并按需提供源码即可。


这种方式就像我们编写 ES5 代码一样,不需要经过构建工具打包成产物再给浏览器解析,浏览器自己就能够解析。


image.png
与现有的打包构建工具 Webpack 等不同,Vite 的开发服务器启动过程仅包括加载配置和中间件,然后立即启动服务器,整个服务启动流程就此结束。


Vite 利用了现代浏览器支持的 ESM 特性,在开发阶段实现了 no-bundle 模式,不生成所有可能用到的产物,而是在遇到 import 语句时发起资源文件请求。


当 Vite 服务器接收到请求时,才对资源进行实时编译并将其转换为 ESM,然后返回给浏览器,从而实现按需加载项目资源。而现有的打包构建工具在启动服务器时需要进行项目代码扫描、依赖收集、模块解析、生成 chunk 等操作,最后才启动服务器并输出生成的打包产物。


正是因为 Vite 采用了 no-bundle 的开发模式,使用 Vite 的项目不会随着项目迭代变得庞大和复杂而导致启动速度变慢,始终能实现毫秒级的启动。


esbuild 预构建


当然这里的毫秒级是有前提的,需要是非首次构建,并且没有安装新的依赖,项目代码中也没有引入新的依赖。


这是因为 Vite 的 Dev 环境会进行预构建优化。
在第一次运行项目之后,直接启动服务,大大提高冷启动速度,只要没有依赖发生变化就会直接出发热更新,速度也能够达到毫秒级。


这里进行预构建主要是因为 Vite 是基于浏览器原生**支持 **ESM 的能力实现的,但要求用户的代码模块必须是ESM模块,因此必须将 commonJSUMD 规范的文件提前处理,转化成 ESM 模块并缓存入 node_modules/.vite


在转换 commonJS 依赖时,Vite 会进行智能导入分析,即使模块导出时动态分配的,具名导出也能正常工作。


// 符合预期
import React, { useState } from 'react'

另一方面是为了性能优化


为了提高后续页面加载的性能,Vite 将那些具有许多内部模块的 ESM 依赖转为单个模块。


比如我们常用的 lodash 工具库,里面有很多包通过单独的文件相互导入,而 lodash-es这种 ESM 包会有几百个子模块,当代码中出现 import { debounce } from 'lodash-es'发出几百个 HTTP 请求,这些请求会造成网络堵塞,影响页面的加载。


通过将 lodash-es 预构建成一个单独模块,只需要一个 HTTP 请求。


那么如果是首次构建呢?Vite 还能这么快吗?


在首次运行项目时,Vite 会对代码进行扫描,对使用到的依赖进行预构建,但是如果使用 rollup、webpack 进行构建同样会拖累项目构建速度,而 Vite 选择了 esbuild 进行构建。



btw,预构建只会在开发环境生效,并使用 esbuild 进行 esm 转换,在生产环境仍然会使用 rollup 进行打包。



生产环境使用 rollup 主要是为了更好的兼容性和 tree-shaking 以及代码压缩优化等,以减小代码包体积


为什么选择 esbuild?


esbuild 的构建速度非常快,比 Webpack 快非常多,esbuild 是用 Go 编写的,语言层面的压制,运行性能更好


image.png


核心原因就是 esbuild 足够快,可以在 esbuild 官网看到这个对比图,基本上是 上百倍的差距。


前端的打包工具大多数是基于 JavaScript 实现的,由于语言特性 JavaScript 边运行边解释,而 esbuild 使用 Go 语言开发,直接编译成机器语言,启动时直接运行即可。


更多关于 Go 和 JavaScript 的语言特性差异,可以检索一下。


不久前,字节开源了 Rspack 构建工具,它是基于 Rust 编写的,同样构建速度很快



  • Rust 编译生成的 Native Code 通常比 JavaScript 性能更为高效,也意味着 rspack 在打包和构建中会有更高的性能。

  • 同时 Rust 支持多线程,意味着可以充分利用多核 CPU 的性能进行编译。而 Webpack 受限于 JavaScript 对多线程支持较弱,导致很难进行并行计算。


不过,Rspack 的插件系统还不完善,同时由于插件支持 JS 和 rust 编写,如果采用 JS 编写估计会损失部分性能,而使用 rust 开发,对于开发者可能需要一定的上手成本


image.png


同时发现 Vite 4 已经开始增加对 SWC 的支持,这是一个基于 Rust 的打包器,可以替代 Babel,以获取更高的编译性能。


**Rust 会是 JavaScript 基建的未来吗?**推荐阅读:zhuanlan.zhihu.com/p/433300816


模块热更新


主要是通过 WebSocket 创建浏览器和服务器的通信监听文件的改变,当文件被修改时,服务端发送消息通知客户端修改相应的代码,客户端对应不同的文件进行不同的操作的更新。


WebpackVite 在热更新上有什么不同呢?


Webpack: 重新编译,请求变更后模块的代码,客户端重新加载


Vite 通过监听文件系统的变更,只对发生变更的模块重新加载,只需要让相关模块的 boundary 失效即可,这样 HMR 更新速度不会因为应用体积增加而变慢,但 Webpack 需要经历一次打包构建流程,所以 HMR Vite 表现会好于 Webpack


核心流程


Vite 热更新流程可以分为以下:



  1. 创建一个 websocket 服务端和client文件,启动服务

  2. 监听文件变更

  3. 当代码变更后,服务端进行判断并推送到客户端

  4. 客户端根据推送的信息执行不同操作的更新


image.png


创建 WebSocket 服务


在 dev server 启动之前,Vite 会创建websocket服务,利用chokidar创建一个监听对象 watcher 用于对文件修改进行监听等等,具体核心代码在 node/server/index 下


image.png


createWebSocketServer 就是创建 websocket 服务,并封装内置的 close、on、send 等方法,用于服务端推送信息和关闭服务



源码地址:packages/vite/src/node/server/ws.ts



image.png


执行热更新


当接受到文件变更时,会执行 change 回调


watcher.on('change', async (file) => {
file = normalizePath(file)
// invalidate module graph cache on file change
moduleGraph.onFileChange(file)

await onHMRUpdate(file, false)
})

当文件发生更改时,这个回调函数会被触发。file 参数表示发生更改的文件路径。


首先会通过 normalizePath 将文件路径标准化,确保文件路径在不同操作系统和环境中保持一致。


然后会触发 moduleGraph 实例上的 onFailChange 方法,用来清空被修改文件对应的 ModuleNode 对象的 transformResult 属性,**使之前的模块已有的转换缓存失效。**这块在下一部分会讲到。



  • ModuleNode 是 Vite 最小模块单元

  • moduleGraph 是整个应用的模块依赖关系图



源码地址:packages/vite/src/node/server/moduleGraph.ts



onFileChange(file: string): void {
const mods = this.getModulesByFile(file)
if (mods) {
const seen = new Set<ModuleNode>()
mods.forEach((mod) => {
this.invalidateModule(mod, seen)
})
}
}

invalidateModule(
mod: ModuleNode,
seen: Set<ModuleNode> = new Set(),
timestamp: number = Date.now(),
isHmr: boolean = false,
hmrBoundaries: ModuleNode[] = [],
): void {
...
// 删除平行编译结果
mod.transformResult = null
mod.ssrTransformResult = null
mod.ssrModule = null
mod.ssrError = null
...
mod.importers.forEach((importer) => {
if (!importer.acceptedHmrDeps.has(mod)) {
this.invalidateModule(importer, seen, timestamp, isHmr)
}
})
}

可能会有疑惑,Vite 在开发阶段不是不会打包整个项目吗?怎么生成模块依赖关系图


确实是这样,Vite 不会打包整个项目,但是仍然需要构建模块依赖关系图,当浏览器请求一个模块时



  • Vite 首先会将请求的模块转换成原生 ES 模块

  • 分析模块依赖关系,也就是 import 语句的解析

  • 将模块及依赖关系添加到 moduleGraph

  • 返回编译后的模块给浏览器


因此 Vite 的 Dev 阶段时动态构建和更新模块依赖关系图的,无需打包整个项目,这也实现了真正的按需加载。


handleHMRUpdate


在 chokidar change 的回调中,还执行了 onHMRUpdate 方法,这个方法会调用执行 handleHMRUpdate 方法


handleHMRUpdate 中主要会分析文件更改,确定哪些模块需要更新,然后将更新发送给浏览器。


浏览器端的 HMR 运行时会接收到更新,并在不刷新页面的情况下替换已更新的模块。



源码地址:packages/vite/src/node/server/hmr.ts



export async function handleHMRUpdate(
file: string,
server: ViteDevServer,
configOnly: boolean,
): Promise<void> {
const { ws, config, moduleGraph } = server
// 获取相对路径
const shortFile = getShortName(file, config.root)
const fileName = path.basename(file)
// 是否配置文件修改
const isConfig = file === config.configFile
// 是否自定义插件
const isConfigDependency = config.configFileDependencies.some(
(name) => file === name,
)
// 环境变量文件
const isEnv =
config.inlineConfig.envFile !== false &amp;&amp;
(fileName === '.env' || fileName.startsWith('.env.'))
if (isConfig || isConfigDependency || isEnv) {
// auto restart server
...
try {
await server.restart()
} catch (e) {
config.logger.error(colors.red(e))
}
return
}
...
// 如果是 Vite 客户端代码发生更改,强刷
if (file.startsWith(normalizedClientDir)) {
// ws full-reload
return
}
// 获取到文件对应的 ModuleNode
const mods = moduleGraph.getModulesByFile(file)
...
// 调用所有定义了 handleHotUpdate hook 的插件
for (const hook of config.getSortedPluginHooks('handleHotUpdate')) {
const filteredModules = await hook(hmrContext)
...
}
// 如果是 html 文件变更,重新加载页面
if (!hmrContext.modules.length) {
// html file cannot be hot updated
if (file.endsWith('.html')) {
// full-reload
}
return
}

updateModules(shortFile, hmrContext.modules, timestamp, server)
}


  • 配置文件更新、.env更新、自定义插件更新都会重新启动服务 reload server

  • Vite 客户端代码更新、index.html 更新,重新加载页面

  • 调用所有 plugin 定义的 handleHotUpdate 钩子函数

  • 过滤和缩小受影响的模块列表,使 HMR 更准确。

  • 返回一个空数组,并通过向客户端发送自定义事件来执行完整的自定义 HMR 处理

  • 插件处理更新 hmrContext 上的 modules

  • 如果是其他情况更新,调用 updateModules 函数


流程图如下


image.png


updateModules 中主要是对模块进行处理,生成 updates 更新列表,ws.send 发送 updates 给客户端


ws 客户端响应


客户端在收到服务端发送的 ws.send 信息后,会进行相应的响应


当接收到服务端推送的消息,通过不同的消息类型做相应的处理,比如 updateconnectfull-reload 等,使用最频繁的是 update(动态加载热更新模块)和 full-reload (刷新整个页面)事件。



源码地址:packages/vite/src/client/client.ts



image.png


在 update 的流程里,会使用 Promise.all 来异步加载模块,如果是 js-update,及 js 模块的更新,会使用 fetchUpdate 来加载


if (update.type === 'js-update') {
return queueUpdate(fetchUpdate(update))
}

fetchUpdate 会通过动态 import 语法进行模块引入


浏览器缓存优化


Vite 还利用 HTTP 加速整个页面的重新加载。
对预构建的依赖请求使用 HTTP 头 max-age=31536000, immutable 进行强缓存,以提高开发期间页面重新加载的性能。一旦被缓存,这些请求将永远不会再次访问开发服务器。


这部分的实现在 transformMiddleware 函数中,通过中间件的方式注入到 Koa dev server 中。



源码地址:packages/vite/src/node/server/middlewares/transform.ts



若需要对依赖代码模块做改动可手动操作使缓存失效:


vite --force

或者手动删除 node_modules/.vite 中的缓存文件。


总结


Vite 采用 No Bundleesbuild 预构建,速度远快于 Webpack,实现快速的冷启动,在 dev 模式基于 ES module,实现按需加载,动态 import,动态构建 Module Graph。


在 HMR 上,Vite 利用 HTTP 头 cacheControl 设置 max-age 应用强缓存,加速整个页面的加载。


当然 Vite 还有很多的不足,比如对 splitChunks 的支持、构建生态 loader、plugins 等都弱于 Webpack。不过 Vite 仍然是一个非常好的构建工具选择。在不少应用中,会使用 Vite 来进行开发环境的构建,采用 Webpack5 或者其他 bundle base 的工具构建生产环境。


参考文章


zhuanlan.zhihu.com/p/467

325485

收起阅读 »

nest.js 添加 swagger 响应数据文档

web
基本使用 通常情况下,在 nest.js 的 swagger 页面文档中的响应数据文档默认如下 此时要为这个控制器添加响应数据文档的话,只需要先声明 数据的类型,然后通过@ApiResponse 装饰器添加到该控制器上即可,举例说明 todo.entity....
继续阅读 »

基本使用


通常情况下,在 nest.js 的 swagger 页面文档中的响应数据文档默认如下



此时要为这个控制器添加响应数据文档的话,只需要先声明 数据的类型,然后通过@ApiResponse 装饰器添加到该控制器上即可,举例说明


todo.entity.ts


@Entity('todo')
export class TodoEntity {
@Column()
@ApiProperty({ description: 'todo' })
value: string

@ApiProperty({ description: 'todo' })
@Column({ default: false })
status: boolean
}

todo.controller.ts


  @Get()
@ApiOperation({ summary: '获取Todo详情' })
@ApiResponse({ type: [TodoEntity] })
async list(): Promise<TodoEntity[]> {
return this.todoService.list();
}


@Get(':id')
@ApiOperation({ summary: '获取Todo详情' })
@ApiResponse({ type: TodoEntity })
async info(@IdParam() id: number): Promise<TodoEntity> {
return this.todoService.detail(id);
}

此时对应的文档数据如下显示


image-20230718012234692


如果你想要自定义返回的数据,而不是用 entity 对象的话,可以按照如下定义


todo.model.ts


export class Todo {
@ApiProperty({ description: 'todo' })
value: string

@ApiProperty({ description: 'todo' })
status: boolean
}

然后将 @ApiResponse({ type: TodoEntity }) 中的 TodoEntity 替换 Todo 即可。


自定义返回数据


然而通常情况下,都会对返回数据进行一层包装,如


{
"data": [
{
"name": "string"
}
],
"code": 200,
"message": "success"
}

其中 data 数据就是原始数据。要实现这种数据结构字段,首先定义一个自定义类用于包装,如


export class ResOp<T = any> {
@ApiProperty({ type: 'object' })
data?: T

@ApiProperty({ type: 'number', default: 200 })
code: number

@ApiProperty({ type: 'string', default: 'success' })
message: string

constructor(code: number, data: T, message = 'success') {
this.code = code
this.data = data
this.message = message
}
}

接着在定义一个拦截器,将 data 数据用 ResOp 包装,如下拦截器代码如下


transform.interceptor.ts


export class TransformInterceptor implements NestInterceptor {
constructor(private readonly reflector: Reflector) {}

intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> {
return next.handle().pipe(
map(data => {
const response = context.switchToHttp().getResponse<FastifyReply>()
response.header('Content-Type', 'application/json; charset=utf-8')
return new ResOp(HttpStatus.OK, data ?? null)
}),
)
}
}

此时返回的数据都会转换为 { "data": { }, "code": 200, "message": "success" } 的形式,这部分不为就本文重点,就不赘述了。


回到 Swagger 文档中,只需要 @ApiResponse({ type: TodoEntity }) 改写成 @ApiResponse({ type: ResOp<TodoEntity> }),就可以实现下图需求。


image-20230718012618710


自定义 Api 装饰器


然后对于庞大的业务而言,使用 @ApiResponse({ type: ResOp<TodoEntity> })的写法,肯定不如@ApiResponse({ type: TodoEntity })来的高效,有没有什么办法能够用后者的方式,却能达到前者的效果,答案是肯定有的。


这里需要先自定义一个装饰器,命名为 ApiResult,完整代码如下


import { Type, applyDecorators, HttpStatus } from '@nestjs/common'
import { ApiExtraModels, ApiResponse, getSchemaPath } from '@nestjs/swagger'

import { ResOp } from '@/common/model/response.model'

const baseTypeNames = ['String', 'Number', 'Boolean']

/**
* @description: 生成返回结果装饰器
*/

export const ApiResult = <TModel extends Type<any>>({
type,
isPage,
status,
}: {
type?: TModel | TModel[]
isPage?: boolean
status?: HttpStatus
}
) =>
{
let prop = null

if (Array.isArray(type)) {
if (isPage) {
prop = {
type: 'object',
properties: {
items: {
type: 'array',
items: { $ref: getSchemaPath(type[0]) },
},
meta: {
type: 'object',
properties: {
itemCount: { type: 'number', default: 0 },
totalItems: { type: 'number', default: 0 },
itemsPerPage: { type: 'number', default: 0 },
totalPages: { type: 'number', default: 0 },
currentPage: { type: 'number', default: 0 },
},
},
},
}
} else {
prop = {
type: 'array',
items: { $ref: getSchemaPath(type[0]) },
}
}
} else if (type) {
if (type && baseTypeNames.includes(type.name)) {
prop = { type: type.name.toLocaleLowerCase() }
} else {
prop = { $ref: getSchemaPath(type) }
}
} else {
prop = { type: 'null', default: null }
}

const model = Array.isArray(type) ? type[0] : type

return applyDecorators(
ApiExtraModels(model),
ApiResponse({
status,
schema: {
allOf: [
{ $ref: getSchemaPath(ResOp) },
{
properties: {
data: prop,
},
},
],
},
}),
)
}

其核心代码就是在 ApiResponse 上进行扩展,这一部分代码在官方文档: advanced-generic-apiresponse 中提供相关示例,这里我简单说明下


{ $ref: getSchemaPath(ResOp) } 表示原始数据,要被“塞”到那个类下,而第二个参数 properties: { data: prop } 则表示 ResOpdata 属性要如何替换,替换的部分则由 prop 变量决定,只需要根据实际需求构造相应的字段结构。


由于有些 类 没有被任何控制器直接引用, SwaggerModule 目前还无法生成相应的模型定义,所以需要 @ApiExtraModels(model) 将其额外导入。


此时只需要将 @ApiResponse({ type: TodoEntity }) 改写为 @ApiResult({ type: TodoEntity }),就可达到最终目的。


不过我还对其进行扩展,使其能够返回分页数据格式,具体根据实际数据而定,演示效果如下图:


image-20230718023729609


导入第三方接口管理工具


通过上述的操作后,此时记下项目的 swagger-ui 地址,例如 http://127.0.0.1:5001/api-docs, 此时再后面添加-json,即 http://127.0.0.1:5001/api-docs-json 所得到的数据便可导入到第三方的接口管理工具,就能够很好的第三方的接口协同,接口测试等功能。


image-20230718022612215


image-20230718022446188

收起阅读 »

echarts+dataV实现中国在线选择省市区地图

web
echarts+dataV实现中国在线选择省市区地图 利用 dataV 的地图 GEO JSON数据配合 echarts 和 element-china-area-data实现在线选择 省市区 地图 效果预览 可以通过自行选择省市区在线获取地图数据,配合 e...
继续阅读 »

echarts+dataV实现中国在线选择省市区地图


利用 dataV 的地图 GEO JSON数据配合 echarts 和 element-china-area-data实现在线选择 省市区 地图


效果预览




可以通过自行选择省市区在线获取地图数据,配合 echarts 渲染出来。


实现思路


先通过 regionData 中的数据配合组件库的 级联选择器 进行 省市区 的展示和选择,同时拿到选中 省市区 的 value 值去请求 dataV 中的 GEO JSON 数据


regionData 中的 省市区 数据结构为


elementChinaAreaData.regionData = [{
label: "北京市",
value: "11",
children: [{…}]
}, {
label: 'xxx',
value: 'xxx',
children: [{...}]
}]



  1. dataV 地图数据请求地址为 https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json 其中https://geo.datav.aliyun.com/areas_v3/bound/请求地址前缀不变, 100000_full是全国地图数据的后缀,每个 省市区 后缀不同

  2. regionData 中的 value 值逻辑是


省级为 2                 广东省 value  44
市级为 4 广州市 value 4401
区/县级为 6 天河区 value 440106
直辖市为 2 北京市 value 11
直辖市-市辖区级为 4 北京市-市辖区 value 1101

但是 dataV 后缀长度都是 6 位,好在不足 6 位的只需要用 0 补齐就可以和 dataV 请求后缀联动起来



  1. 直辖市 和 直辖市-市辖区是指同个地址,只是 regionData 多套了一层,所以应该请求同一个 value 后缀的地址,这里以直辖市的为准,下列是需要转换的直辖市,重庆市同时含有 区和县,但是 dataV 中没有做区分,regionData 又有做区分,统一成重庆市总的地图数据


const specialMapCode = {
'1101': '11', // 北京市-市辖区
'1201': '12', // 天津市-市辖区
'3101': '31', // 上海市-市辖区
'5001': '50', // 重庆市-市辖区
'5002': '50' // 重庆市-县
}


  1. dataV 请求地址到 区/县 级后不需要加 _full 后缀如 广东省-广州市-天河区 的请求地址为 https://geo.datav.aliyun.com/areas_v3/bound/440106.json


接合这几点我们可以缕清 regionDatadataV 数据接口地址的关系了,如果 value 长度不足 6 位,如 省/市 则需要用 0 补齐到 6位,且需要加 _full,而 县/区 则不用补 0 和 _full,直辖市-市辖区 则需要进行特殊处理,也只有四个直辖市,处理难度不大,下列代码是级联选择器选择后的处理逻辑,很好理解


const padMapCode = value => {
let currValue = specialMapCode[value] || value
return currValue.length < 6 ? currValue += `${'0'.repeat(6 - currValue.length)}_full` : currValue
}

其它页面展示代码请参考源码


源码


echart-China-map

作者:半生瓜i
来源:juejin.cn/post/7256610327131258940

收起阅读 »

Blitz:可以实时聊天的协同白板

web
书接上文 之前在 跨平台渲染引擎之路:类 Figma 的无限画布 中提到了想落地无限画布场景的渲染 SDK,最近业余时间基本都在折腾类似的事情,不过在形式和顺序上有些调整,首先先看下目前还比较粗糙的项目预览。 预览 项目地址:Blitz 体验地址:Blitz...
继续阅读 »

书接上文


之前在 跨平台渲染引擎之路:类 Figma 的无限画布 中提到了想落地无限画布场景的渲染 SDK,最近业余时间基本都在折腾类似的事情,不过在形式和顺序上有些调整,首先先看下目前还比较粗糙的项目预览。


预览


preview.gif


项目地址:Blitz


体验地址:Blitz - A collaborative whiteboard with chat functionality


目前前端项目直接运行后,协同编辑与音视频部分连接的是我个人的服务器,配置比较低,可能会出现不稳定的情况,后续会再做一些服务自动重启之类的保障措施,也会在 server 模块提供本地运行的机制,感兴趣的可以先 Star 关注一下~


项目目标


产品向



  1. 类似 Canva/Figma 的白板应用

  2. 支持多人实时协作,包括编辑、评论等

  3. 支持音视频聊天、实时通信、投屏等


技术向



  1. 覆盖前端客户端与后端服务器完整链路

  2. 矢量绘制、文字排版与渲染、特效、音视频等实现原理

  3. 集成一些流行或前沿的技术尝鲜,比如 AIGC、抠图、超分等

  4. 调研子技术点目前常见的技术选型,研究第三方库与自主实现/优化的方案


落地思路


最开始提到的落地思路上的调整主要是在以下几个方面上:


第一是在项目形式上,原计划只想搞几个按钮来进行操作,结果发现这太过简陋了,要做更复杂的功能和性能测试就很不方便,同时自己后续想进一步落地像文字、路径等引擎之上的元素级别的能力,因此考虑直接搭建一个类似 Canva 或 Figma 的白板应用,并逐渐迭代到可真正供用户使用的状态。


第二是在引擎开发的节奏上,上次调研后发现,在前端 Pixi.js 的性能其实已经算是 Top 级别的了,像 WASM 方向的 CanvasKit 也只能是不相上下(当然 CanvasKit 的定位其实是不太一样的),如果按照 Pixi 的设计重新用 C++ 或 Rust 实现一遍,那亲测性能是有 30% 左右的提升的,所以考虑先直接用 Pixi.js 作为第一步的引擎,后续逐步替换成 C++ 版本。


第三是会在画布之上从编辑器的角度将整个生态搭建起来,比如更多的元素、特效,以及实时协作、通信等,并且集成一些自己感兴趣的或者看到的有意思的第三方能力,跟市面上已有的产品形成差异化,比如在白板协作的同时能够进行视频聊天、视角跟随等。


因此,在落地顺序上会先搭建一个可以满足最小主流程的编辑器,并在其上逐渐补充能力和优化架构,这过程中会使用到许多优秀的第三方项目来先快速满足需求,最后再将第三方的内容逐渐替代成原生能力(如果第三方存在问题或自己的确有这个能力的话~)。


这项目不仅涉及渲染等多媒体技术,也是一个自己用来学习从前端到后端完整技术栈的项目,欢迎大家一起交流讨论,有想要的功能或者新奇的想法更可以提出来,一起共建或者我来尝试集成到项目中看看~。


编辑器


介绍一下目前编辑器已支持的能力所涉及的技术选型,以及落地过程相关知识点的整理。


无限画布


目前我是直接使用 Pixi 作为渲染引擎,因此只需要将视图中的 传递给 Pixi 的 Application 即可。不过现实情况下我们不可能创建一个无限大的 canvas,因此一般需要自定义一个 Viewport 的概念,用于模拟出无限缩放倍数以及无边界移动的效果。


社区中已经有一个 pixi-viewport 的项目支持类似效果了,但是实际使用过程中,发现其所使用的 Pixi 是之前的版本,与最新版本结合使用时会报错,另外我预先考虑考虑是将用户操作事件进行统一的拦截、分发和处理,该项目会把视口上的事件都接管过去,与我的设计思路相悖,不过 Viewport 的复杂度也不高,因此这部分目前是直接自主实现的。


代码文件:Viewport.ts


画笔


线条的绘制最开始使用的是 Paper.js ,效果和性能方式都很优异,而且也能完全满足后面形状、路径元素的实现,在交互上 Paper 也完整支持了基于锚点调整路径等操作。不过在引入 Pixi 后就需要考虑二者如何交互,目前为了先跑通最小流程先使用的 Pixi 的 Graphics ,后面在完成二者的兼容设计后大概率还是会换回来的。


代码文件:Brush.ts


交互


编辑器用户界面框架是基于 Vue3 落地的,在 UI 组件和风格上采用的是 Element-Plus 为主,这部分前端同学应该属于驾轻就熟的,目前只实现了简单的包围盒展示以及移动元素的操作。


用户认证


用户认证目前直接使用的是第三方的 Authing 身份云 ,不仅能够支持用户名、邮箱、手机等注册方式,微信、Github等第三方身份绑定也是齐全的,并且提供了配套的 UI 组件,拆箱即用,在眼下阶段可以节省很大的成本,帮助聚焦在其他核心能力的开发上。


协同编辑


协同算法目前主流的就是 OT 和 CRDT ,二者都有许多的论文和应用实践,我目前的方案是直接使用 Y.js 项目,目前还只是简单接入,后续这个模块的设计将参考 liveblocks 进行。


CRDT 在多人编辑场景的适用性方面,首先最终一致性保障上肯定是没问题的,同时 FigmaRoom.shPingcode WIKI 等成熟项目也都正在使用,Y.js 的作者在 Are CRDTs suitable for shared editing? 文章中也做了很多说明。从个人的使用体验来说,在工程应用方面,Y.js 相比自己基于 OT 的原理进行实现而言成本大大降低了,只需要进行数据层的 Binding 即可,至于内存方面的问题,其实远没有想象中那么严重,也有许多优化手段,综合评估来看,对于个人或小团队,以及新项目来说,Y.js 是一个相对更好的选择。


协同编辑是很大的一个课题,@doodlewind 的 探秘前端 CRDT 实时协作库 Yjs 工程实现 , @pubuzhixing 的 多人协同编辑技术的演进 都是很好的学习文章,后面自己有更多心得收获的话再进行分享。


代码文件:WhiteBoard.ts


音视频会议


音视频会议的技术实现方案是多样的,你可以选择直接基于 WebRTC 甚至更底下的协议层完全自主实现,也可以采用成熟的流媒体服务器和配套客户端 SDK 进行接入,前者更适合系统学习,但是我觉得后者会更平滑一些,不仅能够先快速满足项目需求,也能够从成熟的解决方案中学习成功经验,避免自己重复走弯路。


媒体服务器的架构目前主要有 Mesh、MCU、SFU 三种。纯 Mesh 方案无法适应多人视频通话,也无法实现服务端的各种视频处理需求,SFU 相比于 MCU,服务器的压力更小(纯转发,无转码合流),灵活性更好,综合看比较适合目前我的诉求。


在 SFU 的架构方向下,被 Miro 收购的 MediaSoup 是综合社区活跃性、团队稳定性、配套完善度等方面较好的选择。在编辑器侧 MediaSoup 提供了 mediasoup-client ,基于该 SDK 可以实现房间连接、音视频流获取与传输、消息通信等能力。


代码文件:VideoChat.ts


服务器


HTTPS 证书


MediaSoup 的服务端访问需要通过 HTTPS 协议,另外处于安全考虑,也建议前端与后端通信统一走 HTTPS ,证书的申请我是用的 Certbot ,按官方教程走就行,非常简单,如果遇到静态页面 HTTPS 访问异常的话,可以参考 该文章 调整下 Nginx 配置看看。


协同编辑


协同编辑我采用的是 Hocuspocus 的解决方案,其除了提供客户端的 Provider 外,也提供了对应服务端的 SDK,按照官网教程直接使用即可,也比较简单。


不过因为项目使用的是 HTTS 协议,因此 WebScoket 也需要使用 WSS 协议才行,Hocuspocus 没有提供这部分的封装,需要自己通过 Express 中转一层,这部分参考 express-ws 的 https 实现即可。


代码文件:server 目录下的 whiteboard.ts


音视频会议


MediaSoup 官方提供了一套基本可以拆箱即用的 demo ,目前我是直接将其 server 模块的代码改了改直接部署在了后端上。主要的修改点就是在端口、证书等配置上,另外项目编译的时候可能会有一些 TS 的 Lint 错误,视情况修改或跳过即可。可以直接参考这两篇文章:mediasoup部署mediasoup错误解决


代码文件:后面熟悉了该模块代码后再整理到项目里面,目前与官方无太大差异


CI/CD


这部分会等到项目的架构相对稳定,功能相对完整后再落地,特别是 CI 属于项目质量保障的重要一环,为了项目达成用户可用的目标是一定要做的。


下一步


从上述内容看其实目前我们也还不算完成了最小流程的编辑体验,比如作图记录的保存就没有做,而且已实现的能力都比较简陋,问题较多,因此下一个项目计划节点中会做的是:



  • 保存/读取作图记录

  • 元素缩放/旋转/删除

  • 导出作图结果

  • 音视频聊天的一些能力补充

  • 支持图片/文字元素

  • 快捷键


同时会将项目的代码框架再做一些完善,修复一些问题,届时会再结合过程中的技术点或浅或深地做一些分享。


最后


目前项目还处于最最开始的起步阶段,架构设计、代码规范等都存在暴力实现的情况,不过我基本会保持每天抽时间更新的状态(通过 Github记录也能看出来),计划今年内能够实现定好的项目目标。


在过程中会阶段性分享自己做的技术选型还有一些技术原理、优化细节等,目前我的路径大体是从第三方到自主实现,因此分享上也会是从浅到深,从大框架到具体技术这么一个节奏。之前我对前端/后端开发其实接触很少,所以许多知识都需要现学现用,比如这次的 Vue、Nginx 等等,欢迎看到的朋友有任何问题或者建议可以一起交流,甚至一起共建项目~


最后对 Blitz 感兴趣的可以点个 S

作者:格子林ll
来源:juejin.cn/post/7256393626681540645
tar ,万分感谢~

收起阅读 »

手撸一个 useLocalStorage

web
前言 最近在用 vue3 + typeScript + vite 重构之前的代码,想着既然都重写了那何不大刀阔斧的改革,把复杂的逻辑全部抽象成独立的 hook,不过官方称之为“组合式函数”(Composables),好家伙写着写着就陷入 “hook 陷阱” 了...
继续阅读 »

前言


最近在用 vue3 + typeScript + vite 重构之前的代码,想着既然都重写了那何不大刀阔斧的改革,把复杂的逻辑全部抽象成独立的 hook,不过官方称之为“组合式函数”(Composables),好家伙写着写着就陷入 “hook 陷阱” 了,啥都想用 hook 实现(自己强迫自己的那种🙃),下笔之前会先去vueuse上看看有没有现成可用的,没有就自己撸一个。


但回过头来发现有些地方确实刻意为之了,导致用起来不是那么爽,比如写了一个 usePxToRem hook,作用是把 px 转换为 rem,用法如下


import { usePxToRem } from './usePxToRem'

const { rem } = usePxToRem('120px')

初看确实没问题,但如果此时有两个px需要转换怎么办,下面这样写肯定不行的,会提示变量rem已经被定义了,不能重复定义。


import { usePxToRem } from './usePxToRem'

const { rem } = usePxToRem('120px')
const { rem } = usePxToRem('140px')

像这样变通下也是勉强能解决的。


import { usePxToRem } from './usePxToRem'

const { rem: rem1 } = usePxToRem('120px')
const { rem: rem2 } = usePxToRem('140px')
console.log(rem1, rem2)

但是总感觉有点麻烦不够优雅,重新思考下这个需求,好像不需要响应式,是不是更适合用函数 convertPxToRem 解决,所以说写着写着就掉进了 hook 陷阱了😂。


正文


扯远了回到正题,开发中经常需要操作 localStorage,直接用原生也没啥问题,如果再简单封装一下就更好了,用起来方便多了。


export function getLocalStorage(key: string, defaultValue?: any) {
const value = window.localStorage.getItem(key)

if (!value) {
if (defaultValue) {
window.localStorage.setItem(key, JSON.stringify(defaultValue))
return defaultValue
} else {
return ''
}
}

try {
const jsonValue = JSON.parse(value)
return jsonValue
} catch (error) {
return value
}
}

export function setLocalStorage(key: string, value: any) {
window.localStorage.setItem(key, JSON.stringify(value))
}

export function removeLocalStorage(key: string) {
window.localStorage.removeItem(key)
}

假设有个需求在页面上实时显示 localStorage 里的值,那么必须单独设置一个变量接收 localStorage 的值,然后一边修改变量一边设置 localStorage,这样写起来就有点繁琐了。


<template>
<div>
{{ user }}
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { getLocalStorage, setLocalStorage } from './localStorage';

const user = ref('')
user.value = getLocalStorage('user', '张三')
user.value = '李四'
setLocalStorage('user', user.value)
</script>

我想要的效果是一步搞定,像下面这样,是不是很优雅。


import { useLocalStorage } from './useLocalStorage'

const user = useLocalStorage('user', '张三')
user.value = '李四'

第一想法是从 vueues 上找现成的,毕竟这个需求太通用了,useLocalStorage 确实很好用,然后就在想能不能学习 vueuse 自己实现一个简单的 useLocalStorage,正好锻炼下。


第一步搭框架实现基本功能。


import { ref, watch } from "vue";

export function useLocalStorage(key: string, defaultValue: any) {
const data = ref<any>()

// 读取 storage
try {
data.value = JSON.parse(window.localStorage.getItem(key) || '')
} catch (error) {
data.value = window.localStorage.getItem(key)
} finally {
if (!data.value) {
data.value = defaultValue
}
}

// 上面只是读取 storage,并没有把更新后的值写入到 storage 中
// 接下来监听 data,每次更新都更新 storage 中值
watch(() => data.value, () => {
if (data.value === null) {
// 置为null表明要清空该值了
window.localStorage.removeItem(key)
} else {
if (typeof data.value === 'object') {
window.localStorage.setItem(key, JSON.stringify(data.value))
} else {
window.localStorage.setItem(key, data.value)
}
}
}, {
immediate: true
})

return data
}

虽然基本功能实现了,但有个问题,比如定义了一个 number 类型的 count 变量,正常情况下只能赋值数字,但这里赋值为字符串也是允许的,因为 data 设置 any 类型了,接下来想办法把类型固定住,比如一开始赋值为 number,后续更新只能是 number 类型,避免误操作。此时就不能使用 any 类型了,需要用范型来约束返回值了,至于范型是啥,请移步这里


我们约定好默认值 defaultValue 的类型就是接下来要操作的类型,稍作调整如下,这样返回值 datadefaultValue 的类型就一致了。


import { ref, watch } from "vue"
import type { Ref } from 'vue'

export function useLocalStorage<T>(key: string, defaultValue: T) {
const data = ref() as Ref<T>

// 读取 storage
try {
data.value = JSON.parse(window.localStorage.getItem(key) || '')
} catch (error) {
data.value = window.localStorage.getItem(key) as T
} finally {
if (!data.value) {
data.value = defaultValue
}
}

// 上面只是读取 storage,并没有把更新后的值写入到 storage 中
// 接下来监听 data,每次更新都更新 storage 中值
watch(() => data.value, () => {
if (data.value === null) {
// 置为null表明要清空该值了
window.localStorage.removeItem(key)
} else {
if (typeof data.value === 'object') {
window.localStorage.setItem(key, JSON.stringify(data.value))
} else {
window.localStorage.setItem(key, data.value as string)
}
}
}, {
immediate: true
})

return data
}

继续举例子看看,会发现IDE报错了,提示不能将类型“string”分配给类型“number”,至此改造第一步算是完成了。


const count = useLocalStorage('count', 1);
count.value = 2
count.value = '3'

image.png


来试试删除 count,IDE又报错了,提示不能将类型“null”分配给类型“number”,确实有道理。


image.png


那来点暴力的,在定义 data 的时候给一个 null 类型,就像这样 const data = ref() as Ref<T | null>,那么 count.value = null 就不会报错了,也能清空了。不过当我们这样写的时候问题又来了,count.value += 1,IDE会提示 “count.value”可能为 “null” ,确实在定义的时候给了一个 null 类型,那该怎么办呢?


可以用 get set 实现,在 get 的时候返回当前类型,在 set 的时候可以设置 null,然后 count.value 在设置的时候可以为 null 或者 number,在读取的时候只是 number 了。


type RemovableRef<T> = {
get value(): T
set value(value: T | null)
}

const data = ref() as RemovableRef<T>

至此一个简单的 useLocalStorage 算是实现了,顺便聊聊自己在开发 hook 时一些心得体验。



  1. 不要把所有功能写到一个 hook 中,这样没有任何意义,一定要一个功能一个 hook,功能越单一越好

  2. 有时候 hook 在初始化的时候需要传递一些参数,如果这些参数是给 hook 中某个函数使用的,那么最好是在调用该函数的时候传参,这样可以多次调用传不同的
    作者:胡先生
    来源:juejin.cn/post/7256620538092290107
    参数。

收起阅读 »

揭秘 html2Canvas:打印高清 PDF 的原理解析

web
1. 前言 最近我需要将网页的DOM输出为PDF文件,我使用的技术是html2Canvas和jsPDF。具体流程是,首先使用html2Canvas将DOM转化为图片,然后将图片添加到jsPDF中进行输出。 const pdf = new jsPDF({    ...
继续阅读 »

1. 前言


最近我需要将网页的DOM输出为PDF文件,我使用的技术是html2Canvas和jsPDF。具体流程是,首先使用html2Canvas将DOM转化为图片,然后将图片添加到jsPDF中进行输出。


const pdf = new jsPDF({     
unit: 'pt',    
format: 'a4',    
orientation: 'p',
});
const canvas = await html2canvas(element,
{
onrendered: function (canvas) {    
document.body.appendChild(canvas);  
}
}
);
const canvasData = canvas.toDataURL('image/jpeg', 1.0);
pdf.addImage(canvasData, 10, 10);
pdf.save('jecyu.pdf');

遇到了图片导出模糊的问题,解决思路是:



  1. 先html2canvas 转成高清图片,然后再传一个 scale 配置:


scale: window\.devicePixelRatio \* 3// 增加清晰度


  1. 为了确保图片打印时不会变形,需要按照 PDF 文件的宽高比例进行缩放,使其与 A4 纸张的宽度一致。因为 A4 纸张采用纵向打印方式,所以以宽度为基准进行缩放。


// 获取canavs转化后的宽度 
const canvasWidth = canvas.width;
// 获取canvas转化后的高度
const canvasHeight = canvas.height;
// 高度转化为PDF的高度 const height = (width / canvasWidth) \* canvasHeight;
// 1 比 1 进行缩放
pdf.addImage(data, 'JPEG', 0, 0, width, height);
pdf.save('jecyu.pdf');

要想了解为什么这样设置打印出来的图片变得更加清晰,需要先了解一些有关图像的概念。


2. 一些概念


2.1 英寸


F2FDB01D-EAF3-4056-BFB0-A2615285F55C.png

英寸是用来描述屏幕物理大小的单位,以对角线长度为度量标准。常见的例子有电脑显示器的17英寸或22英寸,手机显示器的4.8英寸或5.7英寸等。厘米和英寸的换算是1英寸等于2.54厘米。


2.2 像素


像素是图像显示的基本单元,无法再分割。它是由单一颜色的小方格组成的。每个点阵图都由若干像素组成,这些小方格的颜色和位置决定了图像的样子。


image.png

图片、电子屏幕和打印机打印的纸张都是由许多特定颜色和位置的小方块拼接而成的。一个像素通常被视为图像的最小完整样本,但它的定义和上下文有关。例如,我们可以在可见像素(打印出来的页面图像)、屏幕上的像素或数字相机的感光元素中使用像素。根据上下文,可以使用更精确的同义词,如像素、采样点、点或斑点。


2.3 PPI 与 DPI


PPI (Pixel Per Inch):每英寸包括的像素数,用来描述屏幕的像素密度。


DPI(Dot Per Inch):即每英寸包括的点数。   


在这里,点是一个抽象的单位,可以是屏幕像素点、图片像素点,也可以是打印的墨点。在描述图片和屏幕时,通常会使用DPI,这等同于PPI。DPI最常用于描述打印机,表示每英寸打印的点数。一张图片在屏幕上显示时,像素点是规则排列的,每个像素点都有特定的位置和颜色。当使用打印机打印时,打印机可能不会规则地打印这些点,而是使用打印点来呈现图像,这些打印点之间会有一定的空隙,这就是DPI所描述的:打印点的密度。


30E718E3-8D78-4759-8D3A-A2E428936DF7.png


在这张图片中,我们可以清晰地看到打印机是如何使用墨点打印图像的。打印机的DPI越高,打印出的图像就越精细,但同时也会消耗更多的墨点和时间。


2.4 设备像素


设备像素(物理像素)dp:device pixels,显示屏就是由一个个物理像素点组成,屏幕从工厂出来那天物理像素点就固定不变了,也就是我们经常看到的手机分辨率所描述的数字。


DF7FDA29-CBFC-41DD-8AD3-E4BB480C322F.png
一个像素并不一定是小正方形区块,也没有标准的宽高,只是用于丰富色彩的一个“点”而已。


2.5 屏幕分辨率


屏幕分辨率是指一个屏幕由多少像素组成,常说的分辨率指的就是物理像素。手机屏幕的横向和纵向像素点数以 px 为单位。


CB3C41C3-C15B-40E5-9724-60E7639A1B65.png

iPhone XS Max 和 iPhone SE 的屏幕分辨率分别为 2688x1242 和 1136x640。分辨率越高,屏幕上显示的像素就越多,单个像素的尺寸也就越小,因此显示效果更加精细。


2.6 图片分辨率


在我们所说的图像分辨率中,指的是图像中包含的像素数量。例如,一张图像的分辨率为 800 x 400,这意味着图像在垂直和水平方向上的像素点数分别为 800 和 400。图像分辨率越高,图像越清晰,但它也会受到显示屏尺寸和分辨率的影响。


如果将 800 x 400 的图像放大到 1600 x 800 的尺寸,它会比原始图像模糊。通过图像分辨率和显示尺寸,可以计算出 dpi,这是图像显示质量的指标。但它还会受到显示屏影响,例如最高显示屏 dpi 为 100,即使图像 dpi 为 200,最高也只能显示 100 的质量。


可以通过 dpi 和显示尺寸,计算出图片原来的像素数


719C1F90-0990-499B-AD74-0ED41A8825FD.png

这张照片的尺寸为 4x4 英寸,分辨率为 300 dpi,即每英寸有 300 个像素。因此它的实际像素数量是宽 1200 像素,高 1200 像素。如果有一张同样尺寸(4x4 英寸)但分辨率为 72 dpi 的照片,那么它的像素数量就是宽 288 像素,高 288 像素。当你放大这两张照片时,由于像素数量的差异,可能会导致细节的清晰度不同。


怎么计算 dpi 呢?dpi = 像素数量 / 尺寸


举个例子说明:


假设您有一张宽为1200像素,高为800像素的图片,您想将其打印成4x6英寸的尺寸。为此,您可以使用以下公式计算分辨率:宽度分辨率 = 1200像素/4英寸 = 300 dpi;高度分辨率 = 800像素/6英寸 = 133.33 dpi。因此,这张图片的分辨率为300 dpi(宽度)和133.33 dpi(高度)。需要注意的是,计算得出的分辨率仅为参考值,实际的显示效果还会受到显示设备的限制。


同一尺寸的图片,同一个设备下,图片分辨率越高,图片越清晰。  


A790AD33-7440-458B-8588-F32827C533BD.png


2.7 设备独立像素


前面我们说到显示尺寸,可以使用 CSS 像素来描述图片在显示屏上的大小,而 CSS 像素就是设备独立像素。设备独立像素(逻辑像素)dip:device-independent pixels,独立于设备的像素。也叫密度无关像素。


为什么会有设备独立像素呢?


智能手机的发展非常迅速。几年前,我们使用的手机分辨率非常低,例如左侧的白色手机,它的分辨率只有320x480。但是,随着科技的进步,低分辨率手机已经无法满足我们的需求了。现在,我们有更高分辨率的屏幕,例如右侧的黑色手机,它的分辨率是640x960,是白色手机的两倍。因此,如果在这两个手机上展示同一张照片,黑色手机上的每个像素点都对应白色手机上的两个像素点。


image.png

理论上,一个图片像素对应1个设备物理像素,图片才能得到完美清晰的展示。因为黑色手机的分辨率更高,每英寸显示的像素数量增多,缩放因素较大,所以图片被缩小以适应更高的像素密度。而在白色手机的分辨率较低,每英寸显示的像素数量较少,缩放因素较小,所以图片看起来相对较大。


为了解决分辨率越高的手机,页面元素越来越小的问题,确保在白色手机和黑色手机看起来大小一致,就出现了设备独立像素。它可以认为是计算机坐标系统中得到一个点,这个点代表可以由程序使用的虚拟像素。


例如,一个列表的宽度 300 个独立像素,那么在白色手机会用 300个物理像素去渲染它,而黑色手机使用 600个物理像素去渲染它,它们大小是一致的,只是清晰度不同。


那么操作系统是怎么知道 300 个独立像素,应该用多少个物理像素去渲染它呢?这就涉及到设备像素比。


2.8 设备像素比


设备像素比是指物理像素和设备独立像素之间的比例关系,可以用devicePixelRatio来表示。具体而言,它可以按以下公式计算得出。


设备像素比:物理像素 / 设备独立像素 // 在某一方向上,x 方向或者 y 方向

在JavaScript中,可以使用window.devicePixelRatio获取设备的DPR。设备像素比有两个主要目的:



  • 1.保持视觉一致性,以确保相同大小的元素在不同分辨率的屏幕上具有一致的视觉大小,避免在不同设备上显示过大或过小的问题。

  • 2.支持高分辨率屏幕,以提供更清晰、更真实的图像和文字细节。


开发人员可以使用逻辑像素来布局和设计网页或应用程序,而不必考虑设备的物理像素。系统会根据设备像素比自动进行缩放和适配,以确保内容的一致性和最佳显示效果。


3. 分析原理


3.1 html2canvas 整体流程


在使用html2canvas时,有两种可选的模式:一种是使用foreignObject,另一种是使用纯canvas绘制。


使用第一种模式时,需要经过以下步骤:首先将需要截图的DOM元素进行克隆,并在过程中附上getComputedStyle的style属性,然后将其放入SVG的foreignObject中,最后将SVG序列化成img的src(SVG直接内联)。


img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(new XMLSerializer().serializeToString(svg)); 4.ctx.drawImage(img, ....)

第二种模式是使用纯Canvas进行截图的步骤。具体步骤如下:



  1. 复制要截图的DOM,并将其附加样式。

  2. 将复制的DOM转换为类似于VirtualDOM的对象。

  3. 递归该对象,根据其父子关系和层叠关系计算出一个renderQueue。

  4. 每个renderQueue项目都是一个虚拟DOM对象,根据之前获取的样式信息,调用ctx的各种方法。


6C07042D-923E-4FFA-9C79-FF924D3E8512.png


3.2 分析画布属性 width、height、scale


通常情况下,每个位图像素应该对应一个物理像素,才能呈现完美清晰的图片。但是在 retina 屏幕下,由于位图像素点不足,图片就会变得模糊。


为了确保在不同分辨率的屏幕下输出的图片清晰度与屏幕上显示一致,该程序会取视图的 dpr 作为默认的 scale 值,以及取 dom 的宽高作为画布的默认宽高。这样,在 dpr 为 2 的屏幕上,对于 800 * 600 的容器画布,通过 scale * 2 后得到 1600 * 1200 这样的大图。通过缩放比打印出来,它的清晰度是跟显示屏幕一致的。


0A21E97D-FE73-47D9-9CE8-058696CCE58C.png


假设在 dpr 为 1 的屏幕,假如这里 scale 传入值为 2,那么宽、高和画布上下文都乘以 2倍。


A3086809-FA99-42A4-8C9E-8BA6C21395E4.png


为什么要这样做呢?因为在 canvas 中,默认情况下,一个单位恰好是一个像素,而缩放变换会改变这种默认行为。比如,缩放因子为 0.5 时,单位大小就变成了 0.5 像素,因此形状会以正常尺寸的一半进行绘制;而缩放因子为 2.0 时,单位大小会增加,使一个单位变成两个像素,形状会以正常尺寸的两倍进行绘制。


如下例子,通过放大倍数绘制,输出一张含有更多像素的大图


// 创建 Canvas 元素 
const canvas = document.createElement('canvas');
canvas.width = 200;
canvas.height = 200;

// 获取绘图上下文
const ctx = canvas.getContext('2d');
// 绘制矩形
ctx.fillStyle = 'red';
ctx.fillRect(50, 50, 100, 100);
document.body.appendChild(canvas)

//== 放大2倍画布 ==//
const canvas2 = document.createElement('canvas'); //
// 改变 Canvas 的 width 和 height
canvas2.width = 400;
canvas2.height = 400;
const ctx2 = canvas2.getContext('2d');
// 绘制矩形
ctx2.scale(2, 2);
// 将坐标系放大2倍,必须放置在绘制矩形前才生效
ctx2.fillStyle = 'blue';
ctx2.fillRect(50, 50, 100, 100);
document.body.appendChild(canvas2)

3.3 为什么 使用 dpr * 倍数进行 scale


在使用html2Canvas时,默认会根据设备像素比例(dpr)来输出与屏幕上显示的图片清晰度相同的图像。但是,如果需要打印更高分辨率的图像,则需要将dpr乘以相应的倍数。例如,如果我们想要将一张800像素宽,600像素高,72dpi分辨率的屏幕图片打印在一张8x6英寸,300dpi分辨率的纸上,我们需要确保图片像素与打印所需像素相同,以保证清晰度。


步骤 1: 将纸的尺寸转换为像素


可以使用打印分辨率来确定转换后的像素尺寸。


假设打印分辨率为 300 dpi,纸的尺寸为 8x6 英寸,那么:


纸的宽度像素 = 8 英寸 * 300 dpi = 2400 像素


纸的高度像素 = 6 英寸 * 300 dpi = 1800 像素


步骤 2: 计算图片在纸上的实际尺寸


将图片的尺寸与纸的尺寸进行比例缩放,以确定在纸上的实际打印尺寸


图片在纸上的宽度 = (图片宽度 / 屏幕像素每英寸) * 打印分辨率


图片在纸上的高度 = (图片高度 / 屏幕像素每英寸) * 打印分辨率


图片在纸上的宽度 = (800 / 72) * 300 = 3333.33 像素(约为 3334 像素)


图片在纸上的高度 = (600 / 72) * 300 = 2500 像素


步骤 3: 调整图片大小和打印分辨率


根据计算出的实际尺寸,可以将图片的大小调整为适合打印的尺寸,并设置适当的打印分辨率。


图片在纸上的宽度为 3334 像素,高度为 2500 像素。


也就是说,在保持分辨率为 72 dpi 的情况下,需要把原来 800*600 的图片,调整像素为 3334 * 2500。如果是位图直接放大,就会变糊。如果是矢量图,就不会有问题。这也是 html2Canvas 最终通过放大 scale 来提高打印清晰度的原因。


在图片调整像素为 *3334 * 2500,虽然屏幕宽高变大了,但通过打印尺寸的换算,最终还是 6 8 英寸,分辨率 为 300dpi。


在本案例中,我们需要打印出一个可以正常查看的 pdf,对于 A4尺寸,我们可以用 pt 作为单位,其尺寸为 595pt * 841pt。 实际尺寸为  595/72 = 8.26英寸,841/72 =  11.68英寸。为了打印高清图片,需要确保每英寸有300个像素,也就是8.26 * 300 = 2478像素,11.68 * 300 = 3504 像素,也就是说 canvas 转出的图片必须要这么大,最终打印的像素才这么清晰。


而在绘制 DOM 中,由于调试时不需要这么大,我们可以缩放比例,比如缩小至3倍,这样图片大小就为826像素 * 1168像素。如果高度超过1168像素,则需要考虑分页打印。


下面是 pt 转其他单位的计算公式


function convertPointsToUnit(points, unit) {   
// Unit table from <https://github.com/MrRio/jsPDF/blob/ddbfc0f0250ca908f8061a72fa057116b7613e78/jspdf.js#L791>  
var multiplier;  
switch(unit) {    
case 'pt'
multiplier = 1;         
break;    
case 'mm'
multiplier = 72 / 25.4
break;    
case 'cm'
multiplier = 72 / 2.54
break;    
case 'in'
multiplier = 72;        
break;    
case 'px'
multiplier = 96 / 72;   
break;    
case 'pc'
multiplier = 12;        
break;    
case 'em'
multiplier = 12;        
break;    
case 'ex'
multiplier = 6;
break;
default:      
throw ('Invalid unit: ' + unit);  
}  
return points \* multiplier; }

4. 扩展


4.1 为什么使用大图片 Icon 打印出来还模糊


在理论上,一个位图像素应该对应一个物理像素,这样图片才能完美清晰地展示。在普通屏幕上,这没有问题,但在Retina屏幕上,由于位图像素点不足,图片会变得模糊。


EE3424CB-9DEB-4F55-B4A7-89736725C0E1.jpg


所以,对于图片高清问题,比较好的方案是两倍图片(@2x)


如:200x300(css pixel)img标签,就需要提供 400x600 的图片


如此一来,位图像素点个数就是原来的 4 倍,在 retina 屏幕下,位图像素个数就可以跟物理像素个数


形成 1:1 的比例,图片自然就清晰了(这也解释了为啥视觉稿的画布需要 x2


这里还有另一个问题,如果普通屏幕下,也用了两倍图片 ,会怎么样呢?


很明显,在普通屏幕下(dpr1),200X300(css pixel)img 标签,所对应的物理像素个数就是 200x300 个。而两倍图的位图像素。则是200x300*4,所以就出现一个物理像素点对应4个位图像素点,所以它的取色也只能通过一定的算法(显示结果就是一张只有原像素总数四分之一)


我们称这个过程叫做(downsampling),肉眼看上去虽然图片不会模糊,但是会觉得图片缺失一些锐利度。


11465C2B-AEBB-472D-9CFD-8922ADB72E5F.jpg


通常在做移动端开发时,对于没那么精致的app,统一使用 @2x 就好了。


10C2665B-6F90-4317-8AE9-A380F9F0ABA3.png


上面 100x100的图片,分别放在 100x100、50x50、25x25的 img 容器中,在 retina 屏幕下的显示效果


条码图,通过放大镜其实可以看出边界像素点取值的不同:




  • 图片1,就近取色,色值介于红白之间,偏淡,图片看上去可能会模糊(可以理解为图片拉伸)。




  • 图片2,没有就近取色,色值要么红,要么是白,看上去很清晰。




  • 图片3,就近取色,色值位于红白之间,偏重,图片看上去有色差,缺失锐利度(可以理解为图片挤压)。




要想大的位图 icon 缩小时保证显示质量,那就需要这样设置:


img {     
image-rendering:-moz-crisp-edges;    
image-rendering:-o-crisp-edges;    
image-rendering:-webkit-optimize-contrast;    
image-rendering: crisp-edges;    
-ms-interpolation-mode: nearest-neighbor;    
-webkit-font-smooting:  antialiased;
}

5. 总结


本文介绍了如何通过使用 html2Canvas 来打印高清图片,并解释了一些与图片有关的术语,包括英寸、像素、PPI 与 DPI、设备像素、分辨率等,同时逐步分析了 html2Canvas 打印高清图片的原理。



demo: github.com/jecyu/pdf-d…



参考资料


收起阅读 »

uni-app下App转微信小程序的操作经验

web
背景 就是老板觉得 app 比较难以开展,需要开发小程序版本方便用户引入; 个人觉得,我们的产品更偏向B端产品,需要公司整体入住,而不是散兵游勇的加入,没必要进行这样的引流,奈何我不是老板,那就干。 目前已经有二十几个页面及即时通信模块,已经可以稳定运行;...
继续阅读 »

背景



  1. 就是老板觉得 app 比较难以开展,需要开发小程序版本方便用户引入;

    1. 个人觉得,我们的产品更偏向B端产品,需要公司整体入住,而不是散兵游勇的加入,没必要进行这样的引流,奈何我不是老板,那就干。

    2. 目前已经有二十几个页面及即时通信模块,已经可以稳定运行;



  2. 后续新开发的功能要兼容到App和微信小程序;

  3. 同时还要按照新的ui进行修改页面样式。


关于APP代码转小程序的方案研究



  1. App的开发方案uni-app,本来就是留了兼容的方案的,但是目前有很多的业务,需要逐步测试优化;

  2. 原始开发过程一般以h5为基础,然后兼容app的各种版本;

  3. 开发过程,代码管理的考虑是需要切出一个新的打包小程序分支,这样对于基础的更新仍然在app端首先兼容开发,后续合并到具体的端开发分支上,然后做兼容问题处理,具体的分支如下:

    1. ft/base分支,仍旧以原本的App开发分支为准;

    2. ft/app分支,用做App的开发兼容测试;

      1. ft/app_android_qa,app的安卓端测试分支‘

      2. ...



    3. ft/mp分支,用做微信小程序开发兼容测试;




按着官方指导文档进行修改,对可预知的问题进行修改



  1. App正常,小程序、H5异常的可能性

    1. 代码中使用了App端特有的plus、Native.js、subNVue、原生插件等功能,如下的地点坐标获取功能;



  2. 微信小程序开发注意

    1. 这里一个很重要的问题,需要对原始的项目进行分包,不然是绝对不能提交发布的;




地点坐标获取功能


本次开发中的地理位置选择功能,在App下使用了原生的高德地图服务,在小程序下边就需要改成腾讯地图的位置选择服务uni.chooseLocation


高德地图、腾讯地图以及谷歌中国区地图使用的是GCJ-02坐标系,还好这两个使用的坐标系是一致的,否则就需要进行坐标的转换;


关联的bug报错:getLocation:fail the api need to be declared in the requiredPrivateInfos field in app.json/ext.json


// manifest.json,如下两个平台不需要同时配置
{
// App应用,使用高德地图
"sdkConfigs": {
"geolocation": {
"amap": {
"__platform__": ["ios", "android"],
"appkey_ios": "",
"appkey_android": ""
}
},
"maps": {
"amap": {
"appkey_ios": "",
"appkey_android": ""
}
},
},

// 在小程序下使用地图选择,使用腾讯地图
"mp-weixin": {
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于小程序位置接口的效果展示"
}
},
// 这里的配置是有效的
"requiredPrivateInfos": ["getLocation", "chooseLocation"],
}
}


契约锁



  1. 契约锁,app下使用的是webview直接打开签订的合同;

  2. 但是在小程序,需要引用契约锁的小程序插件页面;


// App下打开webview进行操作
await navTo('/pages/common/webview');
export const navTo = (url, query) => {
if (query) {
url += `?query=${encodeURIComponent(JSON.stringify(query))}`;
}
return new Promise((resolve, reject) => {
uni.navigateTo({
url,
success: (res) => {
resolve(res);
},
fail: (e) => {
reject(e);
},
});
});
};

// 微信小程序下的处理方式
// 如下打开插件页面
const res = await wx.navigateTo({
url: `plugin://qyssdk-plugin/${pageType}?ticket=${ticket}&hasCb=true&env=${baseUrl.qys_mp_env}`,
success(res) {},
fail(e) {},
});

微信小程序分包



  1. 原始的App版本是在pages下边进行平铺的,没任何分包;

  2. 小程序每个分包不能大于2M,主包也不能大于2M,分包总体积不超过 20M;

  3. 在小程序下,分包:

    1. 主包,包括基础的一些配置,资源文件等,还要包括几个tab页面;

    2. 分包,按照业务模块进行划分:

      1. "root": "pages/authenticate"

      2. "root": "pages/team",

      3. "root": "pages/salary",

      4. "root": "pages/employ",

      5. ...





  4. 分包之后需要相应的修改页面跳转的地址,当前版本主要在pages.json里边进行划分,所以需要修改的跳转地址并不是很多;


压缩资源文件大小



  1. 对static目录进行整理;

    1. 压缩图片文件;

    2. 对于不着急展示的图片采用远端加载的方式;



  2. 删除不需要的资源,如一些不兼容微信端的组件、不再用的组件等;


视频模块nvue页面的重写



  1. 原本的组件不支持小程序,后续只能重新写这块;

  2. 删除原本的App视频模块nvue页面;


即时通信模块的业务修改



  1. 这块的核心是推送即时消息,在小程序下很容易收不到,最后的方案是做一个新的页面,去提示下载打开App操作;

  2. 删除原本的App即时通信所引入的各种资源文件;


整体ui的修改



  1. 修改基础的样式定义变量;

    1. 修改uni.scss文件,修改为新的ui风格;



  2. 对硬页面的ui逐步修改;


小程序的按需注入


小程序配置:lazyCodeLoading,在 mp-weixin 下边配置;


直接运行代码,对着bug进行逐步修改


在开发工具中运行,查看控制台以及小程序评分、优化体验等的提示进行。


Error: 暂不支持动态组件[undefined],Errors compiling template: :style 不支持 height: ${scrollHeight}px 语法


其实就是 style 的一种写法的问题,语法问题:


:style="{height: `${scrollHeight}px`}">


:style="`height: ${scrollHeight}px`" => :style="{height: `${scrollHeight}px`}"


http://test.XXX.com 不在以下 request 合法域名列表中


配置request合法域名的问题,参考文档:developers.weixin.qq.com/miniprogram…,添加后正常。


Unhandled promise rejection


当 Promise 的状态变为 rejection 时,我们没有正确处理,让其一直冒泡(propagation),直至被进程捕获。这个 Promise 就被称为 unhandled promise rejection。


Error: Compile failed at pages/message/components/Chat.vue






只能删除后使用v-if进行判断展示;


无效的 page.json ["titleNView"]


也就是这里的头信息不能支持这个配置,直接删除。


代码质量的问题 / 代码优化


common/vendor 过大的问题



  1. uni-app 微信小程序 vendor.js 过大的处理方式和分包优化

    1. 使用运行时代码压缩;

      1. HBuilder 直接开启压缩,但是这样会编译过程变慢;

      2. cli 创建的项目可以在 package.json 中添加参数–minimize





  2. vendor.js 过大的处理方式

    1. 开启压缩;

    2. 分包,对一些非主包引用的资源引用位置进行修改;




总结



  1. 方向很重要,预先的系统选型要多考虑以后的需要,不要太相信老板的话,可能开始说不要,后边就要了;

  2. uni-app框架下,兼容多端的修改还是容易处理的,一般只会发生几类问题,有时候看起来很严重,其实并不严重;


以上只是个人见解,请指教

作者:qiuwww
来源:juejin.cn/post/7255879340223184956

收起阅读 »

熟读代码简洁之道,为什么我还是选择屎山

web
前言 前几天我写了一篇Vue2屎山代码汇总,收到了很多人的关注;这说明代码简洁这仍然是一个程序员的基本素养,大家也都对屎山代码非常关注;但是关注归关注,执行起来却非常困难;我明明知道这段代码的最佳实践,但是我就是不那样写,因为我有很多难言之隐; 没有严格的卡口...
继续阅读 »

前言


前几天我写了一篇Vue2屎山代码汇总,收到了很多人的关注;这说明代码简洁这仍然是一个程序员的基本素养,大家也都对屎山代码非常关注;但是关注归关注,执行起来却非常困难;我明明知道这段代码的最佳实践,但是我就是不那样写,因为我有很多难言之隐;


没有严格的卡口


没有约束就没有行动,比方说eslint,eslint只能减少很少一部分屎山,而且如果不在打包机器上配置eslint的话那么eslint都可以被绕过;对我个人而言,实现一个需求,当然是写屎山代码要来的快一些,我写屎山代码能够6点准时下班,要是写最佳实践可能就要7点甚至8点下班了,没有人愿意为了代码整洁度而晚一点下班的。


没有CodeReview,CodeReview如果不通过会被打回重新修改,直到代码符合规范才能提交到git。CodeReview是一个很好地解决团队屎山代码的工具,只可惜它只是一个理想。因为实际情况是根本不可能有时间去做CodeReview,连基本需求都做不完,如果去跟老板申请一部分时间来做CodeReview,老板很有可能会对你进行灵魂三连问:你做为什么要做CodeReivew?CodeReview的价值是什么?有没有量化的指标?对于屎山代码的优化,对于开发体验、开发效率、维护成本方面,这些指标都非常难以衡量,它们对于业务没有直接的价值,只能间接地提高业务的开发效率,提高业务的稳定性,所以老板只注重结果,只需要你去实现这个需求,至于说代码怎么样他并不关心;


没有代码规约


大厂一般都有代码规约,比如:2021最新阿里代码规范(前端篇)百度代码规范


但是在小公司,一般都没有代码规范,也就是说代码都无章可循;这种环境助长了屎山代码的增加,到头来屎山堆得非常高了,之后再想去通过重构来优化这些屎山代码,这就非常费力了;所以要想优化屎山代码光靠个人自觉,光靠多读点书那是没有用的,也执行不下去,必须在团队内形成一个规范约定,制定规约宜早不宜迟


没有思考的时间


另外一个造成屎山代码的原因就是没时间;产品经理让我半天完成一个需求,老大说某个需求很紧急,要我两天内上线;在这种极限压缩时间的需求里面,确实没有时间去思考代码怎么写,能cv尽量cv;但是一旦养成习惯,即使后面有时间也不会去动脑思考了;我个人的建议是不要总是cv,还是要留一些时间去思考代码怎么写,至少在接到需求到写代码之前哪怕留个5分钟去思考,也胜过一看到需求差不多就直接cv;


框架约束太少


越是自由度高的框架越是容易写出屎山代码,因为很多东西不约束的话,代码就会不按照既定规则去写了;比如下面这个例子:
stackblitz.com/edit/vue-4a…


这个例子中父组件调用子组件,子组件又调用父组件,完全畅通无阻,完全可以不遵守单向数据流,这样的话为了省掉一部分父子组件通信的逻辑,就直接调用父组件或者子组件,当时为了完成需求我这么做了,事后我就后悔了,极易引起bug,比如说下一次这个需求要改到这一部分逻辑,我忘记了当初这个方法还被父组件调用,直接修改了它,于是就引发线上事故;最后自己绩效不好看,但是全是因为自己当初将父子组件之间耦合太深了;


自己需要明白一件事情那就是框架自由度越高,越需要注意每个api调用的方式,不能随便滥用;框架自由不自由这个我无法改变,我只能改变自己的习惯,那就是用每一个api之前思考一下这会给未来的维护带来什么困难;


没有代码质量管理平台


没有代码质量管理平台,你说我写的屎山,我还不承认,你说我代码写的不好,逻辑不清晰,我反问你有没有数据支撑


但是当代码质量成为上线前的一个关键指标时,每个人都不敢懈怠;常见的代码质量管理平台有SonarQubeDeepScan,这些工具能够继承到CI中,成为部署的一个关键环节,为代码质量保驾护航;代码的质量成为了一个量化指标,这样的话每个人的代码质量都清晰可见


最后


其实看到屎山代码,每一个人都应该感到庆幸,这说明有很多事情要做了,有很多基建可以开展起来;推动团队制定代码规约、开发eslint插件检查代码、为框架提供API约束或者部署一个代码质量管理平台,这一顿操作起

作者:蚂小蚁
来源:juejin.cn/post/7255686239756533818
来绩效想差都差不了;

收起阅读 »

如何给你的个人博客添加点赞功能

web
最近在重构博客,想要添加一些新功能。平时有看 Josh W. Comeau 的个人网站,他的每篇文章右侧都会有一个心形按钮,用户通过点击次数来表达对文章的喜爱程度。让我们来尝试实现这个有趣的点赞功能吧! 绘制点赞图标 点赞按钮的核心是 SVG 主要由两部分组...
继续阅读 »

最近在重构博客,想要添加一些新功能。平时有看 Josh W. Comeau 的个人网站,他的每篇文章右侧都会有一个心形按钮,用户通过点击次数来表达对文章的喜爱程度。让我们来尝试实现这个有趣的点赞功能吧!


image.png


绘制点赞图标


点赞按钮的核心是 SVG 主要由两部分组成:



  • 两个爱心形状 ❤️ 的 path ,一个为前景,一个为背景

  • 一个遮罩 mask ,引用 rect 作为遮罩区域


首先使用 defs 标签定义一个 id 为 heart 的爱心形状元素,在后续任何地方都可以使用 use 标签来复用这个 “组件”。


其次使用 mask 标签定义了一个 id 为 mask 的遮罩元素,通过 rect 标签设置了一个透明的矩形作为遮罩区域。


最后使用一个 use 标签引用了之前定义的 heart 图形元素作为默认的初始颜色,使用另一个 use 标签,同样引用 heart 图形元素,并使用 mask 属性引用了之前定义的遮罩元素,用于实现填充颜色的遮罩效果。


点赞动画


接下来实现随着点赞数量递增时爱心逐渐被填充的效果,我们可以借助 CSS 中 transfrom 的 translateY 属性来完成。设置最多点击次数(这里我设置为 5 次)通过 translateY 来移动遮罩的位置完成填充,也就是说,读者需要点击 5 次才能看到完整的红色爱心形状 ❤️ 的点赞按钮。


除此之外我们还可以为点赞按钮添加更有趣交互效果:



  1. 每次点击时右侧会出现『 +1 』字样

  2. 用户在点击第 3 次的时候,填充爱心形状 ❤️ 点赞按钮的同时,还会向四周随机扩散 mini 爱心 💗


这里可以用 framer-motion 来帮助我们实现动画效果。


animate([
...sparklesReset,
['button', { scale: 0.9 }, { duration: 0.1 }],
...sparklesAnimation,
['.counter-one', { y: -12, opacity: 0 }, { duration: 0.2 }],
['button', { scale: 1 }, { duration: 0.1, at: '<' }],
['.counter-one', { y: 0, opacity: 1 }, { duration: 0.2, at: '<' }],
['.counter-one', { y: -12, opacity: 0 }, { duration: 0.6 }],
...sparklesFadeOut,
])

这样就完成啦,使劲儿戳下面的代码片段试试效果:



数据持久化


想要让不同用户看到一致的点赞数据,我们需要借助数据库来保存每一个用户的点赞次数和该文章的总获赞次数。每当用户点击一次按钮,就会发送一次 POST 请求,将用户的 IP 地址和当前点赞的文章 ID (这里我使用的文章标题,可以替换为任意文章唯一标识) 存入数据库,同时返回当前的用户合计点赞次数和该文章的总获赞次数


export async function POST(req: NextRequest, { params }: ParamsProps) {
const res = await req.json()
const slug = params.slug
const count = Number(res.count)
const ip = getIP(req)
const sessionId = slug + '___' + ip

try {
const [post, user] = await Promise.all([
db.insert(pages)
.values({ slug, likes: count })
.onConflictDoUpdate({
target: pages.slug,
set: { likes: sql`pages.likes + ${count}` },
})
.returning({ likes: pages.likes }),
db.insert(users)
.values({ id: sessionId, likes: count })
.onConflictDoUpdate({
target: users.id,
set: { likes: sql`users.likes + ${count}` },
})
.returning({ likes: users.likes })
])
return NextResponse.json({
post_likes: post[0].likes || 0,
user_likes: user[0]?.likes || 0
});
} catch (error) {
return NextResponse.json({ error }, { status: 400 })
}
}

同理,当用户再次进入该页面时,发起 GET 请求,获取当前点赞状态并及时渲染到页面。


回顾总结


点赞功能在互联网应用中十分广泛,自己手动尝试实现这个功能还是挺有趣的。本文从三方面详细介绍了这一实现过程:



  • 绘制点赞图标:SVG 的各种属性应用

  • 点赞动画:framer-motion 动画库的使用

  • 数据持久化:数据库查询


如果这篇文章对你有帮助,记得点赞!


本文首发于我的个人网站 leonf

ong.me

收起阅读 »

我教你怎么在Vue3实现列表无限滚动,hook都给你写好了

web
先看成果 无限滚动列表 无限滚动列表(Infinite Scroll)是一种在网页或应用程序中加载和显示大量数据的技术。它通过在用户滚动到页面底部时动态加载更多内容,实现无缝的滚动体验,避免一次性加载所有数据而导致性能问题。供更流畅的用户体验。但需要注意在实...
继续阅读 »

先看成果


动画.gif

无限滚动列表


无限滚动列表(Infinite Scroll)是一种在网页或应用程序中加载和显示大量数据的技术。它通过在用户滚动到页面底部时动态加载更多内容,实现无缝的滚动体验,避免一次性加载所有数据而导致性能问题。供更流畅的用户体验。但需要注意在实现时,要考虑合适的加载阈值、数据加载的顺序和流畅度,以及处理加载错误或无更多数据的情况,下面我们用IntersectionObserver来实现无线滚动,并且在vue3+ts中封装成一个可用的hook


IntersectionObserver是什么



IntersectionObserver(交叉观察器)是一个Web API,用于有效地跟踪网页中元素在视口中的可见性。它提供了一种异步观察目标元素与祖先元素或视口之间交叉区域变化的方式。
IntersectionObserver的主要目的是确定一个元素何时进入或离开视口,或者与另一个元素相交。它在各种场景下非常有用,例如延迟加载图片或其他资源,实现无限滚动等。



这里用一个demo来做演示


动画.gif

demo代码如下,其实就是用IntersectionObserver来对某个元素做一个监听,通过siIntersecting属性来判断监听元素的显示和隐藏。


 const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
console.log('元素出现');
} else{
console.log('元素隐藏');
}
});
});
observer.observe(bottom);


无限滚动实现


下面我们开始动手


1.数据模拟


模拟获取数据,比如分页的数据,这里是模拟的表格滚动的数据,每次只加载十条,类似于平时的翻页效果,这里写的比较简单,
在这里给它加了一个最大限度30条,超过30条就不再继续增加了


<template>
<div ref="container" class="container">
<div v-for="item in list" class="box">{{ item.id }}</div>
</div>

</template>
<script setup lang="ts">

const list: any[] = reactive([]);
let idx = 0;

function getList() {
return new Promise((res) => {
if(idx<30){
for (let i = idx; i < idx + 10; i++) {
list.push({ id: i });
}
idx += 10
}
res(1);
});
</script>

2.hook实现


import { createVNode, render, Ref } from 'vue';
/**
接受一个列表函数、列表容器、底部样式
*/

export function useScroll() {
// 用ts定义传入的三个参数类型
async function init(fn:()=>Promise<any[] | unknown>,container:Ref) {
const res = await fn();
}
return { init }
}


执行init就相当于加载了第一次列表 后续通过滚动继续加载列表


import { useScroll } from "../hooks/useScroll.ts";
onMounted(() => {
const {init} = useScroll()
//三个参数分别是 加载分页的函数 放置数据的容器 结尾的提示dom
init(getList,container,bottom)
});

3.监听元素


export function useScroll() {
// 用ts定义传入的三个参数类型
async function init(fn:()=>Promise<any[] | unknown>,container:Ref,bottom?:HTMLDivElement) {
const res = await fn();
// 使用IntersectionObserver来监听bottom的出现
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
fn();
console.log('元素出现');
} else{
console.log('元素隐藏');

}
});
});
observer.observe(bottom);
}
return { init }
}

4.hook初始化


获取需要做无限滚动的容器 这里我们用ref的方式来直接获取到dom节点 大家也可以尝试下用getCurrentInstance这个api来获取到


整个实例,其实就是类似于vue2中的this.$refs.container来获取到dom节点容器


根据生命周期我们知道dom节点是在mounted中再挂载的,所以想要拿到dom节点,要在onMounted里面获取到,毕竟没挂载肯定是拿不到的嘛



const container = ref<HTMLElement | null>(null);
onMounted(() => {
const vnode = createVNode('div', { id: 'bottom',style:"color:#000" }, '到底了~');
render(vnode, container.value!);
const bottom = document.getElementById('bottom') as HTMLDivElement;
// 用到的是createVNode来生成虚拟节点 然后挂载到容器container中
const {init} = useScroll()
//三个参数分别是 加载分页的函数 放置数据的容器 结尾的提示dom
init(getList,container,bottom)
});

这部分代码是生成放到末尾的dom节点 封装的init方法可以自定义传入末尾的提示dom,也可以不传,封装的方法中有默认的dom


优化功能


1.自定义默认底部提示dom


async function init(fn:()=>Promise<any[] | unknown>,container:Ref,bottom?:HTMLDivElement) {
const res = await fn();
// 如果没有传入自定义的底部dom 那么就生成一个默认底部节点
if(!bottom){
const vnode = createVNode('div', { id: 'bottom',style:"color:#000" }, '已经到底啦~');
render(vnode, container.value!);
bottom = document.getElementById('bottom') as HTMLDivElement;
}
// 使用IntersectionObserver来监听bottom的出现
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
fn();
console.log('元素出现');
} else{
console.log('元素隐藏');

}
});
});
observer.observe(bottom);
}

完整代码


import { createVNode, render, Ref } from 'vue';
/**
接受一个列表函数、列表容器、底部样式
*/

export function useScroll() {
async function init(fn:()=>Promise<any[] | unknown>,container:Ref,bottom?:HTMLDivElement) {
const res = await fn();
// 生成一个默认底部节点
if(!bottom){
const vnode = createVNode('div', { id: 'bottom' }, '已经到底啦~');
render(vnode, container.value!);
bottom = document.getElementById('bottom') as HTMLDivElement;
}
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
fn();
}
});
});
observer.observe(bottom);
}
return { init }
}


<template>
<div ref="container" class="container">
<div v-for="item in list" class="box">{{ item.id }}</div>
</div>

</template>
<script setup lang="ts">
import { onMounted, createVNode, render, ref, reactive } from 'vue';
import { useScroll } from "../hooks/useScroll.ts";
const list: any[] = reactive([]);
let idx = 0;
function getList() {
return new Promise((res,rej) => {
if(idx<=30){
for (let i = idx; i < idx + 10; i++) {
list.push({ id: i });
}
idx += 10
res(1);
}
rej(0)
});
}

const container = ref<HTMLElement | null>(null);
onMounted(() => {
const vnode = createVNode('div', { id: 'bottom' }, '到底了~');
render(vnode, container.value!);
const bottom = document.getElementById('bottom') as HTMLDivElement;
const {init} = useScroll()
init(getList,container,bottom)
});

</script>
<style scoped>
.container {
border: 1px solid black;
width: 200px;
height: 100px;
overflow: overlay
}

.box {
height: 30px;
width: 100px;
background: red;
margin-bottom: 10px
}
</style>

作者:一只大加号
来源:juejin.cn/post/7255149657769066551
>
收起阅读 »

作为开发人员,如何一秒洞悉文件结构?

web
曾经在处理复杂的文件结构时感到束手无策吗?别担心,说一个真正的解决方案——JavaScript中的tree-node包。它能以一种惊人的方式展示文件和文件夹的层次结构,让你瞬间掌握复杂的项目布局。 背景 在一个新项目中,你可能会面对各种文件,包括HTML、CS...
继续阅读 »

b60632618f4042c9a5aed99a0d176157.jpeg


曾经在处理复杂的文件结构时感到束手无策吗?别担心,说一个真正的解决方案——JavaScript中的tree-node包。它能以一种惊人的方式展示文件和文件夹的层次结构,让你瞬间掌握复杂的项目布局。


背景


在一个新项目中,你可能会面对各种文件,包括HTML、CSS、JavaScript、配置文件等等。起初,你可能不清楚这些文件的具体作用和位置,感到无从下手。而随着项目的发展,文件数量可能会急剧增加,你可能会渐渐迷失在文件的迷宫中,忘记了某个文件的用途或者它们之间的关联。


正是在这样的背景下,tree-node包闪亮登场!它为你呈现出一个惊人的树状结构,展示了项目中各个文件和文件夹之间的层次关系。通过运行简单的命令,你就能立即获得一个清晰而易于理解的文件结构图。无论是文件的嵌套层级、文件之间的依赖关系,还是文件夹的组织结构,一目了然。


一键安装,瞬间拥有超能文件管理能力!


无需复杂的步骤或繁琐的设置,只需在命令提示符或终端中输入一行命令,即可全局安装tree-node包:


npm install -g tree-node-cli

震撼视觉展示


tree-node包不仅仅是文件管理工具,它能以惊人的树状结构展示方式,为你带来震撼的视觉体验。使用treee命令,它能够在屏幕上呈现令人惊叹的文件和文件夹布局。无论是开发项目还是设计项目,你都能一目了然地了解整个文件结构。


示例: 假设你的项目文件结构如下:


- src
- js
- app.js
- css
- styles.css
- theme.css
- index.html
- public
- images
- logo.png
- banner.png
- index.html
- README.md

通过执行以下命令:


treee -L 3 -I "node_modules|.idea|.git" -a --dirs-first

你将获得一个惊艳的展示结果:


.
├───src
│ ├───js
│ │ └───app.js
│ ├───css
│ │ ├───styles.css
│ │ └───theme.css
│ └───index.html
├───public
│ ├───images
│ │ ├───logo.png
│ │ └───banner.jpg
│ └───index.html
└───README.md

这个直观的展示方式帮助你迅速理解整个文件结构,无需手动遍历文件夹层级。你可以清楚地看到哪些文件和文件夹属于哪个层级,方便你快速导航和查找所需资源,你也可以在上面注释文件的作用。


自定义控制


tree-node包提供了强大的自定义功能,让你对文件结构拥有绝对掌控。只需重新执行treee命令,tree-node-cli会自动展示最新的文件结构。再通过设置参数,你可以控制显示的层级深度、忽略特定文件夹,并决定是否显示隐藏文件。


配置参数:


-V, --version             输出版本号
-a, --all-files 打印所有文件,包括隐藏文件
--dirs-first 目录在前,文件在后
-d, --dirs-only 仅列出目录
-I, --exclude [patterns] 排除与模式匹配的文件。用 | 隔开,用双引号包裹。 例如 “node_modules|.git”
-L, --max-depth <n> 目录树的最大显示深度
-r, --reverse 按反向字母顺序对输出进行排序
-F, --trailing-slash 为目录添加'/'
-h, --help 输出用法信息

例如,使用以下命令可以显示三级深度的文件结构,并排除node_modules、.idea、objects和.git文件夹,同时显示所有文件,包括以点开头的隐藏文件:(这几个配置是最常见的,我基本是直接复制粘贴拿来就用


treee -L 3 -I "node_modules|.idea|objects|.git" -a --dirs-first


  • -L 3:指定路径的级别为3级。

  • -I "node_modules|.idea|objects|.git":忽略文件夹(正则表达式匹配。.git会匹配到.gitignore)。

  • -a:显示所有文件(默认前缀有"."的不会显示,例如".bin")。

  • --dirs-first:目录在前,文件在后(默认是字母排序)。


tree-node-cli的自定义控制没有繁琐的配置和操作,只需几个简单的参数设置执行命令,你就能根据自己的需求,定制化你的文件展示方式。


灵活应对文件变动


tree-node-cli不仅可以帮助你展示当前的文件结构,还可以灵活应对文件的变动。当你新增或删除了JS文件时,只需重新执行treee命令,tree-node-cli会自动更新并展示最新的文件结构。


示例:
假设在项目中新增了一个名为utils.js的JavaScript文件。只需在终端中切换到项目文件夹路径,并执行以下命令:


treee -L 3 -I "node_modules|.idea|objects|.git" -a --dirs-first

tree-node-cli将重新扫描文件结构,并在展示中包含新添加的utils.js文件:


.
├───src
│ ├───js
│ │ ├───utils.js
│ │ └───app.js
│ ├───css
│ │ ├───styles.css
│ │ └───theme.css
│ └───index.html
├───public
│ ├───images
│ │ ├───logo.png
│ │ └───banner.jpg
│ └───index.html
└───README.md

同样,如果你删除了一个文件,tree-node-cli也会自动更新并将其从展示中移除。


总结


不管你是开发者、设计师还是任何需要处理复杂文件结构的人,tree-node包都将成为你的得力助手。它简化了文件管理手动操作过程,提供了震撼的视觉展示,让你能够轻松地理解和掌握项目的文件结构。你还有更好的文件管理方法吗,欢迎在评论区分享你对文件管理的更好方法,让我们共同探讨文件管理的最佳实践。


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

CSS实现0.5px的边框的两种方式

web
方式一 <style> .border { width: 200px; height: 200px; position: relative; } .border::before { content: ""; position: abs...
继续阅读 »

方式一


<style>
.border {
width: 200px;
height: 200px;
position: relative;
}
.border::before {
content: "";
position: absolute;
left:0;
top: 0;
width: 200%;
height: 200%;
border: 1px solid blue;
transform-origin: 0 0;
transform: scale(0.5);
}
</style>

<div class="border"></div>

方式二


<style>
.border {
width: 200px;
height: 200px;
position: relative;
}
.border::before {
position: absolute;
box-sizing: border-box;
content: " ";
pointer-events: none;
top: -50%;
right: -50%;
bottom: -50%;
left: -50%;
border: 1px solid blue;
transform: scale(0.5);
}
</style>

<div class="border"></div>
作者:很晚很晚了
来源:juejin.cn/post/7255147749360156730

收起阅读 »

基于 Tauri, 我写了一个 Markdown 桌面 App

web
本文视频地址 前言 大家好,我是小马。 去年,我开发了一款微信排版编辑器 MDX Editor。它可以自定义组件、样式,生成二维码,代码 Diff 高亮,并支持导出 Markdown 和 PDF 等功能。然而,作为一个微信排版编辑器,它的受众面比较有限,并不适...
继续阅读 »

本文视频地址


前言


大家好,我是小马。


去年,我开发了一款微信排版编辑器 MDX Editor。它可以自定义组件、样式,生成二维码,代码 Diff 高亮,并支持导出 Markdown 和 PDF 等功能。然而,作为一个微信排版编辑器,它的受众面比较有限,并不适用于每个人。因此,我基于该编辑器开发了 MDX Editor 桌面版,它支持 Mac、Windows 和 Linux,并且非常轻量,整个应用的大小只有 7M。现在,MDX Editor 桌面版已经成为我的创作工具。如果你对它感兴趣,可以在文末获取。


演示


技术选型


开发 MDX Editor 桌面 App,我使用了如下核心技术栈:




  • React (Next.js)




  • Tauri —— 构建跨平台桌面应用的开发框架




  • Tailwind CSS —— 原子类样式框架,支持深色皮肤




  • Ant Design v5 —— 使用"Tree"组件管理文档树




功能与实现


1. MDX 自定义组件


MDX 结合了 Markdown 和 JSX 的优点,它让你可以在 Markdown 文档中直接使用 React 组件,构建复杂的交互式文档。如果你熟悉 React,你可以在 "Config" 标签页中自定义你的组件;如果你不是一个程序员,你也可以基于现有模板进行创作。例如,模板中的 "Gallery" 组件实际上就是一个 "flex" 布局。


代码



function Gallery({children}) {

return <div className="flex gallery">

{children}

</div>


}


文档写作


预览效果


2. 深色皮肤


对于笔记软件来说,深色皮肤已经成为一个不可或缺的部分。MDX Editor 使用 Tailwind CSS 实现了深色皮肤。



3. 多主题


编辑器内置了 10+个文档主题和代码主题,你可以点击右上方的设置按钮进行切换。



4. 本地文件管理


桌面 App 还支持管理本地文件。你可以选择一个目录,或者将你的文档工作目录拖入编辑器,便能够实时地在编辑器中管理文档。



当我在开发这个功能之前,我曾担心自己不熟悉 Rust,无法完成这个功能。但是,熟悉了 Tauri 文档之后,我发现其实很简单。Tauri 提供了文件操作的 API,使得我们不需要编写 Rust 代码,只需要调用 Tauri API 就能完成文件管理。


import { readTextFile, BaseDirectory } from '@tauri-apps/api/fs';

// 读取路径为 `$APPCONFIG/app.conf` 的文本文件

const contents = await readTextFile('app.conf', { dir: BaseDirectory.AppConfig });


文档目录树采用了 Ant Design 的 Tree 组件实现,通过自定义样式使其与整体皮肤风格保持一致,这大大减少了编码工作量。


5. 文档格式化


在文档写作的过程中,格式往往会打断你的创作思路。虽然 Markdown 已经完全舍弃了格式操作,但有时你仍然需要注意中英文之间的空格、段落之间的空行等细节。MDX Editor 使用了 prettier 来格式化文档,只需按下 command+s 就能自动格式化文档。



最后


如果你对这个编辑器感兴趣,可以在 Github 下载桌面版体验。如果你对实现过程感兴趣,也可以直接查看源码。如果您有任何好的建议,可以在上面提出 Issues,或者关注微信公众号 "JS

作者:狂奔滴小马
来源:juejin.cn/post/7255189463746986039
酷" 并留言反馈。

收起阅读 »

用Echarts打造自己的天气预报!

web
前言 最近刚刚学习了Echarts的使用,于是想做一个小案例来巩固一下。项目效果如下图所示: 话不多说,开始进入实战。 创建项目 这里我们使用vue-cli来创建脚手架: vue create app 这里的app是你要创建的项目的名称,进入界面我们选择安装...
继续阅读 »

前言


最近刚刚学习了Echarts的使用,于是想做一个小案例来巩固一下。项目效果如下图所示:


0.png


话不多说,开始进入实战。


创建项目


这里我们使用vue-cli来创建脚手架:
vue create app


这里的app是你要创建的项目的名称,进入界面我们选择安装VueRouter,然后就可以开始进行开发啦。


页面自适应实现


我们这个项目实现了一个页面自适应的处理,实现方式很简单,我利用了一个第三方的库,可以将项目中的px动态的转化为rem,首先我们要安装一个第三方的库
npm i lib-flexible
安装完成后,我们需要在 main.js中引入
import 'lib-flexible/flexible'
还要在项目中添加一个配置文件postcss.config.js,文件内容如下:


module.exports = {
plugins: {
autoprefixer: {},
"postcss-pxtorem": {
"rootValue": 37.5,
"propList": ["*"]
}
}
}

上述代码是一个 PostCSS 的配置示例,用于自动添加 CSS 属性的前缀和将像素单位转换为 rem 单位。


其中



  • autoprefixer 是一个 PostCSS 插件,用于根据配置的浏览器兼容性自动添加 CSS 属性的前缀,以确保在不同浏览器中的兼容性。

  • postcss-pxtorem 是另一个 PostCSS 插件,用于将像素单位转换为 rem 单位,以实现页面在不同设备上的自适应效果。在上述配置中,rootValue 设置为 37.5,这意味着 1rem 会被转换为 37.5px。propList 设置为 ["*"] 表示所有属性都要进行转换。


这样,我们在项目中任何一个地方写px,都会动态的转化成为rem,由于rem是一个中相对于根元素字体大小的CSS单位,可以根据根元素的字体大小进行动态的调整,达到我们一个也买你自适应的目的。


实时时间效果实现


在项目的左上角有一个实时显示的时间,我们是如何做到的呢?首先我们在数据源中定义一个loalTime字段,用来装我们的时间,然后可以通过 new Date() 函数返回当前的时间对象,但这个对象我们是无法直接使用的,需要通过toLocaleTimeString() 函数处理,将 Date 对象转换为本地时间的格式化字符串。


methods{
getLocalTime() {
return new Date().toLocaleTimeString();
},
}

仅仅是这样的话,我们获取的时间是不会动的,怎么让他动起来呢,答案是使用定时器:


created() {
setInterval(() => {
this.localTime = this.getLocalTime();
}, 1000);
},

我们使用了一个setInterval定时器函数,让他每秒钟触发一次,然后将返回的时间赋值给我们的数据源中的localTime,同时将他放在created这个生命周期中,确保一开始就能运行,这样,我们就得到了一个可以随当前时间变化的时间。


省市选择组件实现


这个功能自己实现较为麻烦,我们选择使用第三方的组件库,这里我们选择的是Vant,这是一个轻量级,可靠的移动端组件库,我们首先需要安装他


npm i vant@latest-v2 -S


由于我们使用Vue2进行开发,所以需要指定其版本,然后就是导入所以有组件:


import Vant from 'vant'; 
import 'vant/lib/index.css';
Vue.use(Vant);

由于我们只是在本地开发,所以我们选择导入所有组件,在正式开发中可以选择按需引入来达到性能优化的目的。


准备工作完毕,导入我们需要的组件:


<van-popup v-model="show" position="bottom" :style="{ height: '30%' }">
<van-area
title="标题"
:area-list="areaList"
visible-item-count="4"
@cancel="show = false"
columns-num="2"
@confirm="selectCity"
/>

</van-popup>

这里我们通过show的值来控制的组件的显示与否,点击确认按钮后,会执行selectVCity方法,该方法会将我们选择的省市返回,格式为一个包含地区编码和地区名称的一个对象数组。


天气信息的获取


我们获取天气的信息主要依靠高德地图提供的api来实现,高德地图为我们提供了很多丰富的地图功能,包括了实时天气和天气预报功能,首先我们要注册一下,成为开发者,并获取自己的密钥和key。


最后在index.html中引入:


<script type="text/javascript">
window._AMapSecurityConfig = {
securityJsCode: '你的密钥',
}
</script>
<script type="text/javascript" src="https://webapi.amap.com/maps?v=2.0&key=你的key"></script>

就可以进行开发了。我们首先需要在项目开始加载的时候显示我们当地的信息,所以需要获取我们的当前所处环境的IP地址,所以高德也为我们提供了方法:


initMap() {
let that = this;
AMap.plugin("AMap.CitySearch", function () {
var citySearch = new AMap.CitySearch();
citySearch.getLocalCity(function (status, result) {
if (status === "complete" && result.info === "OK") {
// 查询成功,result即为当前所在城市信息
// console.log(result.city);
that.getWeatherData(result.city);
}
});
});
},

通过AMap.CitySearch插件我们可以很容易的获取到我们当前的IP地址,然后将我们获取到的IP地址传入到getWeatherData() 方法中去获取天气信息,需要注意的是,因为要求项目一启动就获取信息,所以这个方法也是需要放在created这个生命周期中的。然后就是获取天气信息的方法:


getWeatherData(cityName) {
let that = this;
AMap.plugin("AMap.Weather", function () {
//创建天气查询实例
var weather = new AMap.Weather();

//执行实时天气信息查询
weather.getLive(cityName, function (err, data) {
console.log(err, data);
that.mapData = data;
});

//执行实时天气信息查询
weather.getForecast(cityName, function (err, data) {
that.futureMapData = data.forecasts;
console.log(that.futureMapData);

// 每天的温度
that.seriesData = [];
that.seriesNightData = [];
data.forecasts.forEach((item) => {
that.seriesData.push(item.dayTemp);
that.seriesNightData.push(item.nightTemp);
});

that.$nextTick(() => {
that.initEchart();
});
});
});
},

通过这个方法,我们只需要传入城市名就可以很轻松的获取到我们需要的天气信息,并同步到我们的数据源中,然后将其渲染到页面中去。


数据可视化的实现


面对一堆枯燥的数据,我们很难提起兴趣,这时候,数据可视化的重要性就体现出来了,数据可视化是指使用图表、图形、地图、仪表盘等可视化工具将大量的数据转化为具有可读性和易于理解的图像形式的过程。通过数据可视化,可以直观地呈现数据之间的关系、趋势、模式和异常,从而帮助人们更好地理解和分析数据。


而Echarts就是这样一个基于 JavaScript 的开源可视化图表库,里面有非常多的图表类型可供我们使用,这里我们使用比较简单的折线统计图来展示数据。


首先也是安装依赖


npm i echarts


然后就是在项目中引入


import * as echarts from "echarts";


然后就可以进行开发啦,现在页面中准备好一个容器,方便承载我们的图表


<div class="echart-container" ref="echartContainer"></div>


然后就是根据我们获取到的数据进行绘制:


initEchart() {
// 基于准备好的dom,初始化echarts实例
let myChart = echarts.init(this.$refs.echartContainer);

// 绘制图表
let option = {
title: {
text: "ECharts 入门示例",
},
tooltip: {},
xAxis: {
data: ["今天", "明天", "后天", "三天后"],
axisTick: {
show: false,
},
axisLine: {
lineStyle: {
color: "#fff",
},
},
},
yAxis: {
min: "-10",
max: "50",
interval: 10,
axisLine: {
show: true,
lineStyle: {
color: "#fff",
},
},
splitLine: {
show: true,
lineStyle: {
type: "dashed",
color: ["red", "green", "yellow"],
},
},
},
series: [
{
name: "白天温度",
type: "line",
data: this.seriesData,
},
{
name: "夜间温度",
type: "line",
data: this.seriesNightData,
lineStyle: {
color: "red",
},
},
],
};
myChart.setOption(option);
},

一个图表中有非常多的属性可以控制它的不同形态,具体的不过多阐述,可以查看Echarts的参考文档,然后我们就得到一个非常美观的折线统计图。同时不能忘记和省市区选择器进行联动,当我们切换省市的时候,手动触发一次绘制,并且将我们选择的城市传入,这样,我们就得到了一个可以实时获取全国各地天气的小demo。


以上就是主要功能的具体实现方法:代码地址


作者:严辰
来源:juejin.cn/post/7255161684526940220
>欢迎大家和我交流!

收起阅读 »

通过调试技术,我理清了 b 站视频播放很快的原理

web
b 站视频播放的是很快的,基本是点哪就播放到哪。 而且如果你上次看到某个位置,下次会从那个位置继续播放。 那么问题来了:如果一个很大的视频,下载下来需要很久,怎么做到点哪个位置快速播放那个位置的视频呢? 前面写过一篇 range 请求的文章,也就是不下载资源的...
继续阅读 »

b 站视频播放的是很快的,基本是点哪就播放到哪。


而且如果你上次看到某个位置,下次会从那个位置继续播放。


那么问题来了:如果一个很大的视频,下载下来需要很久,怎么做到点哪个位置快速播放那个位置的视频呢?


前面写过一篇 range 请求的文章,也就是不下载资源的全部内容,只下载 range 对应的范围的部分。


那视频的快速播放,是不是也是基于 range 来实现的呢?


我们先复习下 range 请求:



请求的时候带上 range:



服务端会返回 206 状态码,还有 Content-Range 的 header 代表当前下载的是整个资源的哪一部分:



这里的 Content-Length 是当前内容的长度,而 Content-Range 里是资源总长度和当前资源的范围。


更多关于 Range 的介绍可以看这篇文章:基于 HTTP Range 实现文件分片并发下载!


那 b 站视频是不是用 Range 来实现的快速播放呢?


我们先在知乎的视频试一下:


随便打开一个视频页面,比如这个:



然后打开 devtools,刷新页面,拖动下进度条,可以看到确实有 206 的状态码:



我们可以在搜索框输入 status-code:206 把它过滤出来:



这是一种叫过滤器的技巧:



可以根据 method、domain、mime-type 等过滤。




  • has-response-header:过滤响应包含某个 header 的请求




  • method:根据 GET、POST 等请求方式过滤请求




  • domain: 根据域名过滤




  • status-code:过滤响应码是 xxx 的请求,比如 404、500 等




  • larger-than:过滤大小超过多少的请求,比如 100k,1M




  • mime-type:过滤某种 mime 类型的请求,比如 png、mp4、json、html 等




  • resource-type:根据请求分类来过滤,比如 document 文档请求,stylesheet 样式请求、fetch 请求,xhr 请求,preflight 预检请求




  • cookie-name:过滤带有某个名字的 cookie 的请求




当然,这些不需要记,输入一个 - 就会提示所有的过滤器:



但是这个减号之后要去掉,它是非的意思:



和右边的 invert 选项功能一样。


然后点开状态码为 206 的请求看一下:




确实,这是标准的 range 请求。


我点击进度条到后面的位置,可以看到发出了新的 range 请求:



那这些 range 请求有什么关系呢?


我们需要分析下 Content-Range,但是一个个点开看不直观。


这时候可以自定义显示的列:


右键单击列名,可以勾选展示的 header,不过这里面没有我们想要的 header,需要自定义:



点击 Manage Header Columns



添加自定义的 header,输入 Content-Range:



这时候就可以直观的看出这些 range 请求的范围之间的关系:



点击 Content-Range 这一列,升序排列。


我们刷新下页面,从头来试一下:


随着视频的播放,你会看到一个个 range 请求发出:



这些 range 请求是能连起来的,也就是说边播边下载后面的部分。


视频进度条这里的灰条也在更新:



当你直接点击后面的进度条:



观察下 range,是不是新下载的片段和前面不连续了?


也就是说会根据进度来计算出 range,再去请求。


那这个 range 是完全随意的么?


并不是。


我们当前点击的是 15:22 的位置:



我刷新下页面,点击 15:31 的位置:



如果是任意的 range,下载的部分应该和之前的不同吧。


但是你观察下两次的 range,都是 2097152-3145727


也就是说,视频分成多少段是提前就确定的,你点击进度条的时候,会计算出在哪个 range,然后下载对应 range 的视频片段来播放。


那有了这些视频片段,怎么播放呢?


浏览器有一个 SourceBuffer 的 api,我们在 MDN 看一下:



大概是这样用的:



也就是说,可以一部分一部分的下载视频片段,然后 append 上去。


拖动进度条的时候,可以把之前的部分删掉,再 append 新的:



我们验证下,搜索下代码里是否有 SourceBuffer:


按住 command + f 可以搜索请求内容:



可以看到搜索出 3 个结果。


在其中搜索下 SourceBuffer:



可以看到很多用到 SourceBuffer 的方法,基本可以确认就是基于 SourceBuffer 实现的。


也就是说,知乎视频是通过 range 来请求部分视频片段,通过 SourceBuffer 来动态播放这个片段,来实现的快速播放的目的。具体的分段是提前确定好的,会根据进度条来计算出下载哪个 range 的视频。


那服务端是不是也要分段存储这些视频呢?


确实,有这样一种叫做 m3u8 的视频格式,它的存储就是一个个片段 ts 文件来存储的,这样就可以一部分一部分下载。



不过知乎没用这种格式,还是 mp4 存储的,这种就需要根据 range 来读取部分文件内容来返回了:



再来看看 b 站,它也是用的 range 请求的方式来下载视频片段:



大概 600k 一个片段:


下载 600k 在现在的网速下需要多久?这样播放能不快么?


相比之下,知乎大概是 1M 一个片段:



网速不快的时候,体验肯定是不如 b 站的。


而且 b 站用的是一种叫做 m4s 的视频格式:



它和 m3u8 类似,也是分段存储的,这样提前分成不同的小文件,然后 range 请求不同的片段文件,速度自然会很快。


然后再 command + f 搜索下代码,同样是用的 SourceBuffer:



这样,我们就知道了为什么 b 站视频播放的那么快了:


m4s 分段存储视频,通过 range 请求动态下载某个视频片段,然后通过 SourceBuffer 来动态播放这个片段。


总结


我们分析了 b 站、知乎视频播放速度很快的原因。


结论是通过 range 动态请求视频的某个片段,然后通过 SourceBuffer 来动态播放这个片段。


这个 range 是提前确定好的,会根据进度条来计算下载哪个 range 的视频。


播放的时候,会边播边下载后面的 range,而调整进度的时候,也会从对应的 range 开始下载。


服务端存储这些视频片段的方式,b 站使用的 m4s,当然也可以用 m3u8,或者像知乎那样,动态读取 mp4 文件的部分内容返回。


除了结论之外,调试过程也是很重要的:


我们通过 status-code 的过滤器来过滤除了 206 状态码的请求。



通过自定义列在列表中直接显示了 Content-Range:



通过 command + f 搜索了响应的内容:



这篇文章就是对这些调试技巧的综合运用。


以后再看 b 站和知乎视频的时候,你会不会想起它是基于 range 来实现的分段下载和播放呢?



更多调试技术可以看我的调试小册《前端调试通关秘籍》


作者:zxg_神说要有光
来源:juejin.cn/post/7255110638154072120

收起阅读 »

环信的那些”已读“功能实现及问题解决

写在前面你在调用环信的消息回执时,是否有以下的烦恼1、发送了消息已读回执,为什么消息列表页的未读数没有发生变化?2、发送了消息已读回执,为什么消息漫游拉取不到已读状态?如果你有这些烦恼,那就继续往下看一些歧义在这之前,我们需要先来统一确定两件事情第一:消息列表...
继续阅读 »

写在前面
你在调用环信的消息回执时,是否有以下的烦恼
1、发送了消息已读回执,为什么消息列表页的未读数没有发生变化?
2、发送了消息已读回执,为什么消息漫游拉取不到已读状态?
如果你有这些烦恼,那就继续往下看

一些歧义
在这之前,我们需要先来统一确定两件事情
第一:消息列表页
第二:聊天页面
接下来以环信vuedemo为例,看一下这两者


如图所示,红色圈起来的部分为消息列表页也叫会话列表页面,可通过会话列表的api拉取。

绿色圈起来的部分为聊天页面,可通过消息漫游的api拉取

注:聊天页面的数据获取不是必须调用消息漫游api,也可以存在本地从本地进行获取,这个可根据自己项目的需求以及业务逻辑来做调整,本文以消息漫游中的数据为例
插播:会话是什么,当和一个用户或者在一个群中发消息后,就会自动把对方加到会话列表中,可以通过调用会话列表去查询。需要注意,1、此api调用有延迟,建议只有初次登录时通过此api获取到初始会话列表的数据,后续都在本地进行维护。2、登陆ID不要为大小写混用的ID,拉取会话列表大小写ID混用会出现拉取会话列表为空

解决问题一:
在明确了会话列表页和聊天页面各代指的部分之后,我们先来解决第一个问题:发送了消息已读回执,为什么会话列表的未读数没有变化
原因:对于环信来讲,消息是消息,会话是会话,这是两个概念,消息已读和会话已读并没有做联动,也就是消息已读只是对于这条消息而言并不会对会话列表的未读数产生影响,他们是两个独立的个体。会话列表的未读数是针对整个会话而言
那么如何清除会话列表的未读数呢?——需要发送会话已读回执也就是channel ack,这里还需要注意一点,sdk是只负责数据传输的,改变不了页面层的渲染逻辑。所以在发送完channel ack后页面上渲染的未读数不会无缘无故就清0了,是需要重新调用api渲染的!!!!!

channelAck() {
let option = {
chatType: "", // 会话类型,设置为单聊。
type: "channel", // 消息类型。固定参数固定值,不要动它
to: "", // 接收消息对象(用户 ID)。
};
let msg = WebIM.message.create(option);
WebIM.conn
.send(msg)
.then((res) => {
console.log("%c>>>>>>>>会话已读回执发送成功", "color:#6ad1c7", res);
})
.catch((e) => {
console.log("%c>>>>>>>>>会话已读回执发送失败", "color:#ef8784", e);
});
},


会话已读回执发送成功之后,接收方会收到onChannelMessage回调监听

conn.addEventHandler("customEvent", {
onChannelMessage: (message) => {},
});



消息已读回执是需要发送readack,是针对于某一条消息而言。这里也需要注意一点,sdk是只负责数据传输的,改变不了页面层的渲染逻辑,所以已读未读在页面上的渲染也是需要自己处理一下

readAck() {
let option = {
type: "read", // 消息是否已读。固定参数固定值,不要动它
chatType: "singleChat", // 会话类型,这里为单聊。
to: "", // 消息接收方(用户 ID)。
id: "", // 需要发送已读回执的消息 ID。
};
let msg = WebIM.message.create(option);
WebIM.conn
.send(msg)
.then((res) => {
console.log("%c>>>>>>>>消息已读回执发送成功", "color:#6ad1c7", res);
})
.catch((e) => {
console.log("%c>>>>>>>>>消息已读回执发送失败", "color:#ef8784", e);
});
},



消息已读回执发送成功之后,接收方会收到onReadMessage回调监听

conn.addEventHandler("customEvent", {
onReadMessage: (message) => {},
});




插播:会话列表未读数计算规则,简单理解,如果这个会话是单个用户在一直输出的话,这个未读数会一直累加,但是只要对方回了这条消息,那么未读数就会从这条消息之后开始再计算

 解决问题二:
再来看一下第二个问题:为什么消息漫游中拉取不到消息的已读状态
原因:环信服务器是不记录消息状态的,也就是不会记录这条消息是否已读了,所以不会返回消息已读或者未读
那么如何来实现
1、自己本地进行记录消息状态
2、可以使用环信sdk提供的reaction功能来间接是实现已读未读

reaction实现已读未读简单示例

addReaction() {
WebIM.conn
.addReaction(
{
messageId: "",//消息ID
reaction: "read" //reaction
}
)
.then((res) => {
console.log("%c>>>>>>>>reaction添加成功", "color:#6ad1c7", res);
})
.catch((e) => {
console.log("%c>>>>>>>>>reaction添加失败", "color:#ef8784", e);
});
},






总结Q&A
Q:发送了消息已读回执,为什么消息列表页的未读数没有发生变化?
A:会话和消息是两个概念,会话已读是会话已读,消息已读是消息已读,消息已读无法改变会话列表的数据
Q:发送了消息已读回执,为什么消息漫游拉取不到已读状态?
A:环信的服务器不记录消息状态,需要自己本地存储或者使用reaction功能间接实现

收起阅读 »

小程序自定义导航栏

web
小程序布局 谈到导航栏与自定义导航栏,就需要解释一下微信小程序的布局了。在小程序开发中使用wx.getSystemInfoAsync() 方法可以获取到系统信息。 部分获取到的信息如上图(截取自微信小程序开发者文档),对我们理解布局有用的信息是以上...
继续阅读 »

小程序布局




  • 谈到导航栏与自定义导航栏,就需要解释一下微信小程序的布局了。在小程序开发中使用wx.getSystemInfoAsync() 方法可以获取到系统信息。


    image.png


    image.png




  • 部分获取到的信息如上图(截取自微信小程序开发者文档),对我们理解布局有用的信息是以上跟宽度高度相关的属性,如当前设备的屏幕高宽,可用高宽,以及saveArea





  • 上图展示我们从systemInfo获取到的数据的实际表现,以苹果X的刘海屏为例(所有安卓刘海屏原理类似):最外层的红色框即屏幕大小,蓝色框即安全区域字面意思也就是开发者所能操纵的页面区域,上面的黄色框即手机的状态栏,绿色区域即我们要自定义的navigationBar




  • 可见,导航栏紧贴safeArea的上部,如果使用原生导航栏,导航栏下方即是真正意义的可操控范围。




  • 实际上我们自定义的导航栏也是在这个safeArea内与胶囊对齐最为和谐。很关键的原因就是微信将右上角的胶囊按钮作为了内置组件,只有黑白两种颜色,即我们无法改变它的大小位置透明度等等,所以为了配合胶囊按钮,一般自定义的导航栏位置也与上图位置一致。




自定义navigationBar怎么做?


去掉原生导航栏。



  1. 将需要自定义navigationBar页面的page.json的navigationBarTitleText去掉。

  2. 加上 "navigationStyle":"custom" ,这样原生的导航栏就已经消失,甚至后退键也不会出现需要自定义。

  3. 另外,早在2016年微信已经开始适配沉浸式状态栏,目前几乎所有的机型里微信都是沉浸式状态栏,也就是说去掉原生导航栏的同时,整个屏幕已经成为可编程区域


计算navigationBarHeight。



  • 原生的胶囊按钮当然存在,那么下一步就需要你去定位出自定义的导航栏高度以及位置。

  • 对于不同的机型,对于不同的系统,状态栏以及胶囊按钮的位置都不确定,所以需要用到一定的计算,从而面对任何机型都可以从容判定。




  1. 使用wx.getSystemInfoSync() 获取到statusBarHeight,这样就确定了导航栏最基本的距离屏幕上方的距离。




  2. 使用wx.getMenuButtonBoundingClientRect() 获取到小程序的胶囊信息(注意这个api存在各种问题,在不同端表现不一致,后面会叙述这个api调用失败的处理情况),如下图,以下坐标信息以屏幕左上角为原点。





  3. 以下图为例,上面的红色框是statusBar,高度已知;下面的红色框是正文内容,夹在中间的就是求解之一navigationBarHeight;而黄色的是原生胶囊按钮也是在垂直居中位置,高度为胶囊按钮基于左上角的坐标信息已知,不难得出,navigationBarHeight = 蓝色框高度 × 2 + 胶囊按钮.height。(蓝色框高度 = 胶囊按钮.top - statusBarHeight






  1. 最后的计算公式为:navigationBarHeight = (胶囊按钮.top - statusBarHeight) × 2 + 胶囊按钮.height。navigationBar 距屏幕上方的距离即为navigationBarHeight

  2. 这种计算方法在各种机型以及安卓ios都适用。

  3. 针对"wx.getMenuButtonBoundingClientRect() "获取错误或者获取数据为0的极少数情况,只能够去模拟,对于android,一般navigationBarHeight为48px,而对于ios一般为40px,所有机型的胶囊按钮高度是32px。



代码实现



  • 获取本机信息,写在组件的attached生命周期中。


// components/Navigation/index.js
Component({
/**
* 组件的属性列表
*/

properties: {

},

/**
* 组件的初始数据
*/

data: {
navigationBarHeight: 40,
statusBarHeight:20,
},

/**
* 组件的方法列表
*/

methods: {

},
lifetimes: {
attached: function () {
const { statusBarHeight, platform } = wx.getSystemInfoSync();
const { top, height = 32 } = wx.getMenuButtonBoundingClientRect();// 胶囊按钮高度 一般是32 如果获取不到就使用32
// 判断胶囊按钮信息是否成功获取
if (top && top !== 0 && height && height !== 0) {
//获取成功进行计算
const navigationBarHeight = (top - statusBarHeight) * 2 + height;
console.log(navigationBarHeight)
// 导航栏高度
this.setData({
navigationBarHeight,
statusBarHeight
})
} else {
//获取失败使用默认的高度
this.setData({
navigationBarHeight: platform === "android" ? 48 : 40,
statusBarHeight
})
}
}
}
})



  • 组件模板编写


<view class="custom-nav" style="height: {{navigationBarHeight}}px;margin-top:{{statusBarHeight}}px;">
<view>
<image style="width: 40rpx;height:40rpx;" src="/images/location.svg" mode="" />
</view>
</view>


 .navigationBar.wxml 样式如下:


.custom-nav{
background-color:palegoldenrod;
display: flex;
align-items: center;
}
.custom-nav__title{
margin:auto
}

外部页面引用该组件如下,


.json文件,引入组件


{
"usingComponents": {
"my-navigation":"/components/Navigation"
},
"navigationStyle": "custom"
}

注意添加属性:"navigationStyle":"custom"  代表我们要自定义组件


.wxml代码如下:


<view>
<my-navigation></my-navigation>
<view class="page-container" style="background-color: rebeccapurple;">这里是页面内容</view>
</view>


最终效果
image.png


如果想要编写更加通用的组件,可以根据需求定义传入的参数和样式


参考链接


http://www.cnblogs.com/chenwo

作者:let_code
来源:juejin.cn/post/7254812719349858361
long/…

收起阅读 »

Progress 圆形进度条 实现

web
效果图 实现过程分析 简要说明 本文主要以 TypeScript + React 为例进行讲解, 但相关知识和这个关系不大. 不会也不影响阅读 dome 中使用到了 sass, 但用法相对简单, 不影响理解 HTML DOM 元素说明 <div c...
继续阅读 »

效果图



实现过程分析


简要说明



  • 本文主要以 TypeScript + React 为例进行讲解, 但相关知识和这个关系不大. 不会也不影响阅读

  • dome 中使用到了 sass, 但用法相对简单, 不影响理解


HTML DOM 元素说明


<div className="g-progress-wrap">
<div className="g-progress"></div>
<div className="g-circle">
<span className="g-circle-before"><i/></span>
<span className="g-circle-after"><i/></span>
</div>
<div className="g-text">
20%
</div>
</div>


  • g-progress-wrap 包裹 progress, 所有的内容都在这里面

  • g-progress 主要的区域

  • 为了保证圆环有圆角效果 g-circle 内的有 2 个小圆, 放置到圆环的开始和结尾

  • g-text 放置文字区域



上面已经介绍了 html, 因为主要的处理都在css, 所以接下来只说 css



第一步, 实现一个圆


.g-progress {
width: 100px;
height: 100px;
border-radius: 50%;
background: conic-gradient(#1677ff 0, #1677ff 108deg, #eeeeee 108deg, #eeeeee 360deg);
}

image.png




  • border-radius: 50%; 实现圆形




  • 使用 background 实现背景颜色



    • conic-gradient 创建了一个由渐变组成的图像,渐变的颜色变换围绕一个中心点旋转

    • 当角度为 0 - 108deg 时, 颜色为: #1677ff; 当角度为 108deg - 360deg 时, 颜色为: #eeeeee;




第二步, 实现圆环效果


.g-progress {
/* 新增代码 */
/* mask: radial-gradient(transparent, transparent 44px, #000 44.5px, #000 100%); */
-webkit-mask: radial-gradient(transparent, transparent 44px, #000 44.5px, #000 100%);
}

image.png




  • 通过使用 mask属性, 隐藏 中间区域的显示




  • radial-gradient 创建一个图像,该图像由从原点辐射的两种或多种颜色之间的渐进过渡组成



    • 当为 0 - 44px 时, 颜色为: transparent; 当为 44px - 100% 时, 颜色为: #000;

    • 设置为 transparent 时, transparent 的区域的颜色会被隐藏




  • 为什么不使用元素覆盖, 使用中间区域的隐藏



    • 如果用元素覆盖实现的话, 如果需要显示父级的背景色时, 没办法实现




第三步, 实现圆环的圆角效果


.g-circle {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
transform: rotate(-90deg);
&>span {
position: absolute;
top: 47px;
left: 50px;
width: 50%;
transform-origin: left;
&>i {
width: 3px;
height: 3px;
float: right;
border-radius: 50%;
background: #1677ff;
z-index: 1;
}
}
& .g-circle-after {
transform: rotate(0deg);
}
}

image.png


第四步, 文字效果处理


.g-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 16px;
color: #666666;
}

image.png


第五步, 进度变化时, 通过js更新


通过行内样式更新 rotate 的方式即可更新进度


参考文档


developer.mozilla.org/zh-CN/docs/…


developer.mozilla.org/zh-CN/docs/…


http://www.cnblogs.com/coco1s

作者:洲_
来源:juejin.cn/post/7254450297467781176
/p/15…

收起阅读 »

记录一次小程序开发中的各种奇葩bug

web
前段时间,跟好哥们儿商量了一下,帮他公司设计并开发一款宣传用的小程序。因为是宣传用的,所以对于后台数据几乎没什么需求,只需要用到一些接口,引导用户联系公司,且公司性质是古建筑装修,没有自己的服务器。所以我直接给他做的是个静态的小程序。 微信小程序的开发需要注意...
继续阅读 »

前段时间,跟好哥们儿商量了一下,帮他公司设计并开发一款宣传用的小程序。因为是宣传用的,所以对于后台数据几乎没什么需求,只需要用到一些接口,引导用户联系公司,且公司性质是古建筑装修,没有自己的服务器。所以我直接给他做的是个静态的小程序。


微信小程序的开发需要注意几个点:


1、主包不大于2M,分包不超过20M。
图片、视频等文件很容易占据大量空间,因此,作为没有服务器的静态页面,这些图片、视频资源,放在什么地方,然后再拿到网络链接地址,是非常关键的节省空间的方案。


2、微信小程序开发者工具,众所周知经常发神经,
莫名其妙弹出一些报错,也会有一些不兼容情况,其中的一些组件也是经常出现问题,比如媒体组件莫名其妙报“渲染层网络错误”的err。


在这次的miniProgram中,有一些功能的实现中,触发了各种奇怪bug。比如
自定义tabbar,
为了让tabbar能被自定义定制,我几乎把整个关于tabbar的开发文档读了个通透;而在定制之后又发现,
pc端模拟机上正常显示、真机预览正常显示,唯独真机调试中,tabbar不显示。
也不是不显示,我的小米8手机不显示,我两位朋友的iphone,一个显示一个不显示(过程中所有的配置是完全相同的)。


接下来就详细介绍一下我在开发中遇到的几个让我把头皮薅到锃亮的问题。


1、自定义tabbar组件


微信小程序app.json中可以直接配置tabbar。但默认的tabbar组件
不足以完全应付各类不尽相同的场景。


譬如,默认的tabbar上使用的icon
实际是png等格式的图片
而非iconfont,其大小也完全由图片本身大小决定,
无法通过css自定制。


为了解决不同业务需求,小程序也非常人性化的
允许tabbar自定义。
其方法如下:


1、在app.json的tabbar配置中,加上custom:true

2、原本的tabbar配置项必须写完整。

在custom:true之后,tabbar的所有样式皆由自定义组件控制(颜色等),但路径等需要填写正确,否则会报错路径找不到。如配置项中必须的属性不写完整,会导致报错,告诉你缺少必须的配置项属性,也不会解析出来。


    "custom": true,                                                  //自定义tabbar开启
"color": "#c7c7c7", //常态下文字颜色
"selectedColor": "#056f60", //被选中时文字颜色
"list": [
{
"iconPath": "images/tabBarIcon/index.png", //常态下icon图片的路径
"selectedIconPath": "images/tabBarIcon/index-action.png", //被选中时icon图片的路径
"text": "首页展览", //icon图片下的文字
"pagePath": "pages/index/index" //该tabbar对应的路由路径
},
{
"iconPath": "images/tabBarIcon/cases.png",
"selectedIconPath": "images/tabBarIcon/cases-action.png",
"text": "精选案例",
"pagePath": "pages/cases/cases"
},
{
"iconPath": "images/tabBarIcon/about.png",
"selectedIconPath": "images/tabBarIcon/about-action.png",
"text": "关于我们",
"pagePath": "pages/about/about"
},
{
"iconPath": "images/tabBarIcon/contact.png",
"selectedIconPath": "images/tabBarIcon/contact-action.png",
"text": "联系我们",
"pagePath": "pages/contact/contact"
}
]
},

3、创建一个自定义组件文件夹custom-tab-bar。

级别为component组件级别。里面包含一个微信小程序包必须的wxml、wxss、js、json文件。


在这里我使用了vant weapp组件库做的tabbar组件。组件上的icon用的是字节跳动的fontPark字体图标库。


<!-- components/tabBar/tabBar.wxml -->
<!-- active用于控制被选定的item -->
<van-tabbar class="tabbar"
active="{{ active }}"
inactive-color="#b5b5b5"
active-color="#056f60"
bind:change="onChange"
>
<van-tabbar-item class="tabbarItem"
wx:for="{{list}}" wx:key="id">
<view class="main">
<image class="selectedIcon"
src="{{item.selectedIconPath}}"
wx:if="{{item.id === active}}"
mode=""
/>
<image src="{{item.iconPath}}" wx:else mode="" class="icon"/>
<text class="txt">{{item.text}}</text>
</view>
</van-tabbar-item>
</van-tabbar>

/* components/tabBar/tabBar.wxss */
.main{
display: flex;
flex-direction: column;
justify-content:center;
align-items: center;

}
.tabbarItem{
background-color: #e9e9e9;
}
.selectedIcon, .icon{
width: 40rpx;
height: 40rpx;
margin-bottom: 10rpx;
}

Component({
data:{
active:0, //用来找到被选中的tabbar-Item
list:[
{
id:0,
iconPath: "/images/tabBarIcon/index.png", //iconPath这些地址换成自己的地
selectedIconPath:"/images/tabBarIcon/index-action.png", // 址,如果需要用icon图表,在
text:"首页展览", // vant中有说明如何在vant组件
pagePath:"pages/index/index" // 中集成vant以外的字体图标。
}, // 就是因为感觉太麻烦了,所以我
{ // 没有用icon图表,还是使用png
id:1,
iconPath: "/images/tabBarIcon/cases.png",
selectedIconPath:"/images/tabBarIcon/cases-action.png",
text:"精选案例",
pagePath:"pages/cases/cases"
},
{
id:2,
iconPath: "/images/tabBarIcon/about.png",
selectedIconPath:"/images/tabBarIcon/about-action.png",
text:"关于我们",
pagePath:"pages/about/about"
},
{
id:3,
iconPath: "/images/tabBarIcon/contact.png",
selectedIconPath:"/images/tabBarIcon/contact-action.png",
text:"联系我们",
pagePath:"pages/contact/contact"
}
]
},
computed:{

},
methods:{
//点击了tabbar的item后,拿到event.detail的值,根据值再进行路由跳转。
//需要注意的是,navigateTo、redirectTo的跳转方式不能跳到 tabbar 页面,
//reLaunch总是会关闭掉之前打开过的所有页,导致页面回退会直接退出小程序
//所以在此使用switchTab,跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面
onChange(event){

if(event.detail===0){
wx.switchTab({
url: '/pages/index/index',
})
}else if(event.detail===1){
wx.switchTab({
url: '/pages/cases/cases',
})
}else if(event.detail===2){
wx.switchTab({
url: '/pages/about/about',
})
}else if(event.detail===3){
wx.switchTab({
url: '/pages/contact/contact',
})
}
}

},
})

到这里完成了页面跳转功能。但会发现,当我们点击其他页面的tab时,并
没有让tabbar的图表发生变化,
始终在首页被选定。
这是因为data中的active并没有发生变化,依然是active:0


那么要解决这个问题,方案是在每个tabbar路由页面的js文件中,修改active的值。比如,当点击首页时,active=0,点击第二个页面cases时,active=1......以此类推。


//pages/index/index.js
Page({
onShow() {
if (typeof this.getTabBar === 'function' &&
this.getTabBar()) {
this.getTabBar().setData({
active: 0
})
}
}
})


//pages/cases/cases.js
Page({
onShow() {
//在自定义tabbar组件的情况下,即app.json中的tabbar配置项中,custom为true时,会提供一个api接口,
//this.getTabBar(),用于获取到tabbar组件,
//可以通过this.getTabBar().setData({})修改tabbar组件内的数据。
if (typeof this.getTabBar === 'function' &&
this.getTabBar()) {
this.getTabBar().setData({
active: 0
})
}
}
})

//......其他页面以此类推

直到这一步,整个自定义的tabbar组件算是完成。


出现过的BUG




  1. 因为tabbar在app.json文件中"tabbar"配置项配置过了,所以不用再在app.json中的usingComponent配置项进行引用。也无需在tabbar的路由页面的json文件中进行页面配置。




  2. 我曾在onChange(event){}方法中,添加了一行代码:this.setData({active: event.detail });
    57a5d6c616684605f393b89f49d07bf.png




这段代码在没有注释掉的时候,会导致组件在页面切换时发生跳动,处于一种混乱的状态。其原因大致是因为这行代码与page页onshow()时期的getTabBar().setData()有同样的active赋值效果,所以冲突,造成组件闪烁。



  1. 在整个项目完成后,我在使用真机调试时意外发现,模拟机上的tabbar正常显示并使用,但手机上却消失不见。


PC端:


7cb36075fc28472251777b33c085d0f.png


安卓mi8:


安卓.png


我找了很多帖子,没有发现能解决我问题的方案。然后我就问了前辈。前辈的手机是苹果系统,无论是预览、调试,都可以正常显示并使用tabbar,告知我可能是我手机问题,或许是我的手机有什么权限没开。


我又找到一位用苹果手机的同事。如果这位同事的手机也能正常使用,我就要再找一个安卓机的伙伴再测试一次,看看是否机型对代码有影响。


结果奇怪的是,我的这位朋友在进行真机调试时,也没有正常显示tabbar组件。


那么结果就不是安卓和苹果的系统问题。肯定与代码或者某种权限有关。


于是我花了两三个小时去一点点修改,一遍遍重复调试,直到终于找到问题关键所在:


1688803520185.png


这是微信小程序开发者工具中的详情界面,在本地设置中,有一个
启用条件编译
选项。把这个选项开启,tabbar就显示了;关掉这个选项,tabbar就消失了。


于是我开始搜索启用条件编译是什么意思:


2066f1267092e1b9c8c3570069f4cca.png


这是最后找到的结果。但是我并不明白为什么勾选这个会对tabbar有影响。都没有勾选的情况下,前辈的苹果手机就有显示,另一位同事的苹果手机又没有显示,而安卓机的我也一样没有显示。


如果有哪位大佬明白其中的原理,请一定要留言告诉我!!!


2、地图系统


地图系统应该是非常常见的功能,如果在公司的宣传类小程序中加入地图系统,会非常便于用户获取地址信息。


地图系统使用很简单,可以说没太大难度。只要给个map容器,然后给上必须的键值对:经(longitude)纬(latitude)度,如果需要,再给个scale,限制地图缩放的级别,其他的都可以在腾讯地图api的文档中查找需要用的属性。


如果小程序中地图没显示,就要去腾讯地图开放平台里面看看。因为这些地图系统的api都是需要密钥才能使用,所以
注册
api开放平台的账户是第一步,然后在上面的开发文档中选择微信小程序SDK中可以查阅文档。在右上角登录旁边有个控制台,里面创建一个实例,把自己的小程序appID填进去,这个时候小程序中的map应该就是可以正常显示并使用了。


如果需要在小程序的地图中加入标记点,就在map中加入markers,js中传入Obj obj格式的参数,就可以了,在腾讯地图的文档内也有。


地图系统并不难,只需要按照api规则来即可。


<map
longitude="不便展示"
latitude="不便展示"
scale="16"
markers="{{markers}}"
enable-zoom="{{false}}"
enable-scroll="{{false}}"
enable-satellite
style="width: 100%;"
/>

//以下键值对中的value,不加引号为数字类型数据,加引号为字符串类型数据。
Page({
data: {
markers: [{
id: 1, //标记点 id
longitude: 不便展示,
latitude: 不便展示,
iconPath: '/images/local.png',
height: 20,
width: 20,
title: '不便展示',
}],
},

openMap() {
//wx.openLocation()是地图功能的api,在调用该方法时,会跳转到地图
wx.openLocation({
longitude: 不便展示,
latitude: 不便展示,
scale: 18,
name: '不便展示', // 终点名称
});
}
})


3、奇奇怪怪的位置用swiper


一般而言swiper都会用在首页,用以承载轮播图。


不得不说,微信小程序自带的swiper组件虽然简单,但是好用,放上去之后加点属性和数据就可以直接用,比起bug频出的swiper插件还是舒服些。


但是swiper组件就不能用在其他地方吗?


当然可以咯,只要愿意,你就是把许多个业务员的名片用一个swiper组件去收纳,用户不嫌麻烦去一个一个翻的话,你就做呗!


这里,我在精选案例中用了两个swipwe,用来承载相册。


image.png


如图所示,这是两个swiper正在进行滚动动画。


当时在做这个时候,觉得那么多照片正好可以分成两类,一类是成品,一类是原料,让用户可以分类查看。但是我又不想让用户在看到两个相册时,觉得成品和材料就只有一张照片。一想,用swiper正好可以解决这个问题:


让用户看到轮播滚动的图片,每张图片存在时间不长,用户就会想点击放大的图片来延长查看时间,正好落入圈套,进入相册,看到所有图片。


首先是准备了两个view容器,然后在容器中放进swiper,对swiper进行for循环。这整个过程不难,循规蹈矩。但是有个难点,直到项目做完我也没能找到方案:


现在是两个view容器装了两套swiper,如果有更多的swiper,需要更多的view容器,假定数据一次性发过来,怎么样可以循环view的同时,将swiper里面的item也循环?


大概的样子就是:


<view wx:for="{{list1}}">
<swiper>
<swiper-item wx:for="{{item.list}}" wx:for-item="items">
<image src="{{items.src}}" />
<swiper-item>
</swiper>
</view>

数据结构大概是:


    data:{
list1:[
{list:[{title:"111",src:""},{title:"222",src:""},{title:"333",src:""},]},
{list:[{title:"444",src:""},{title:"555",src:""},{title:"666",src:""},]},
{list:[{title:"777",src:""},{title:"888",src:""},{title:"999",src:""},]},
{list:[{title:"aaa",src:""},{title:"bbb",src:""},{title:"ccc",src:""},]},
]
}

上面的代码在循环中肯定出现问题,但是我目前没有找到对应的方法解决。


4、总是有报错渲染层网络层出错


24ec761eb4c272a88fd96edda27bfac.png


这个问题我相信写小程序的应该都遇到过。目前我没找到什么有效解决方案。在社区看到说清除网络缓存。但是在下一次编译时又会出现。如果每次都要清除缓存,好像并不算是个解决问题的方案。


好在这个错误并不影响整体功能,我就

作者:NuLL
来源:juejin.cn/post/7254066710369763388
没有去做任何处理了。

收起阅读 »

搭建适用于公司内部的脚手架

web
前言 公司项目多了,且后续会增加更多项目,为了避免每次创建项目都是重复的copy,这里可以自己写一个适合公司的脚手架,就跟 vue-cli, create-react-app 类似。 简单描述下原理:首先你需要准备一个模板,这个模板可以存储在公司的git上,然...
继续阅读 »

前言


公司项目多了,且后续会增加更多项目,为了避免每次创建项目都是重复的copy,这里可以自己写一个适合公司的脚手架,就跟 vue-clicreate-react-app 类似。


简单描述下原理:首先你需要准备一个模板,这个模板可以存储在公司的git上,然后根据用户选择决定采用哪个分支。比如我们就有 h5模板web模板 两个分支。


然后这些模板会有一些我们自定义的特殊字符,让用户可以根据输入的内容替换。比如我在模板那边里有定义了 $$PROJECT_NAME$$ 这个特殊字符,通过命令行交互让用户输入创建的项目名: test-project ,最后我就通过node去遍历模板里的文件,找到这个字符,将 $$PROJECT_NAME$$ 替换成 test-project 即可。根据公司需求自己事先定义好一些特殊变量即可,主要用到的就是下面几个库。


package.json 里的 bin 字段


用于执行 可执行文件 ,当使用 npm 或 yarn 命令安装时,如果发现包里有该字段,那么会在 node_modules 目录下的 .bin 目录中复制 bin 字段链接的可执行文件,我们在调用执行文件时,可以不带路径,直接使用命令名来执行相对应的执行文件。




bin 文件里的 #! 含义


#! 符号的名称叫 Shebang,用于指定脚本的解释程序。


/usr/bin/env node 表示 系统可以在 PATH 目录中查找 node 程序


如果报错,说明没有在 PATH 中找到 node




npm link


npm link (组件库里用来在本地调试用的)是将整个目录链接到全局node_modules 中,如果有 bin 那么则会生成全局的可执行命令


npm link xxx (本地测试项目里使用), xxx 为 那个库的 package.jsonname。 是让你在本地测试项目中可以使用 xxx




  1. 库在开发迭代,不适合发布到线上进行调试。




  2. 可以帮助我们模拟包安装后的状态,它会在系统中做一个快捷方式映射,让本地的包就好像 install 过一样,可以直接使用。




  3. npm unlink 解除链接






commander —— 命令行指令配置


实现脚手架命令的配置, commander 中文文档


// 引入 program
const { program } = require('commander')

// 设置 program 可以输入的选项
// 每个选项可以定义一个短选项名称(-后面接单个字符)和一个长选项名称(--后面接一个或多个单词),使用逗号、空格或|分隔。
// 长选项名称可以作为 .opts() 的对象key
program.option('-p, --port <count>') // 必选参数使用 <> 表示,可选参数使用 [] 表示

// 解析后的选项可以通过Command对象上的.opts()方法获取,同时会被传递给命令处理函数。
const options = program.opts()

program.command('create <name>').action((fileName) => {
console.log({ fileName, options })
})

program.parse(process.argv)



chalk —— 命令行美化工具


可以美化我们在命令行中输出内容的样式,例如实现多种颜色,花里胡哨的命令行提示等。chalk 文档


安装 chalk 时一定要注意安装 4.x 版本(小包使用的是 4.0.0),否则会因为版本过高,爆出错误。


const chalk = require('chalk')
console.log(`hello ${chalk.blue('world')}`)
console.log(chalk.blue.bgRed.bold('Hello world!'))



inquirer —— 命令行交互工具


支持 input, number, confirm, list, rawlist, expand, checkbox, password,editor 等多种交互方式。 inquirer 文档


const inquirer = require('inquirer')

inquirer
.prompt([
/* 输入问题 */
{
name: 'question1',
type: 'checkbox',
message: '爸爸的爸爸叫什么?',
choices: [
{
name: '爸爸',
checked: true
},
{
name: '爷爷'
}
]
},
{
name: 'question2',
type: 'list',
message: `确定要创建${fileName}的文件夹吗`,
choices: [
{
name: '确定',
checked: true
},
{
name: '否'
}
]
}
])
.then((answers) => {
// Use user feedback for... whatever!!
console.log({ answers })
})
.catch((error) => {
if (error.isTtyError) {
// Prompt couldn't be rendered in the current environment
} else {
// Something else went wrong
}
})



ora —— 命令行 loading 效果


现在的最新版本为 es6 模块,需要用以前的版本,例如: V5.4.1 才是 cjs 模块 : ora 文档


const ora = require('ora')

const spinner = ora('Loading unicorns').start()

setTimeout(() => {
spinner.color = 'yellow'
spinner.text = 'Loading rainbows'
}, 1000)

spinner.succeed()



fs-extra —— 更友好的文件操作


是系统 fs 模块的扩展,提供了更多便利的 API,并继承了 fs 模块的 API。比 fs 使用起来更加友好。 fs-extra 文档




download-git-repo —— 命令行下载工具


从 git 中拉取仓库,提供了 download 方法,该方法接收 4 个参数。 download-git-repo 文档


/**
* download-git-repo 源码
* Download `repo` to `dest` and callback `fn(err)`.
*
* @param {String} repo 仓库地址
* @param {String} dest 仓库下载后存放路径
* @param {Object} opts 配置参数
* @param {Function} fn 回调函数
*/


function download(repo, dest, opts, fn) {}


【注】 download-git-repo 不支持 Promise


作者:pnm学编程
来源:juejin.cn/post/7254176076082249785

收起阅读 »

今天这个 Antd 咱们是非换不可吗?

web
最近在思考一个可有可无的问题: “我们是不是要换一个组件库?” 为什么会有这个问题? 简单同步一下背景,我效力于 Lazada 商家前端团队。从接手系统以来(近 2 年) 就一直使用着 Alibaba Fusion 这套组件库。据我所知淘系都是在使用这套组件...
继续阅读 »

最近在思考一个可有可无的问题:


“我们是不是要换一个组件库?”


为什么会有这个问题?



简单同步一下背景,我效力于 Lazada 商家前端团队。从接手系统以来(近 2 年) 就一直使用着 Alibaba Fusion 这套组件库。据我所知淘系都是在使用这套组件库进行业务开发,已经有 7 ~ 10 年了吧。我们团队花了 2 年时间从 @alife/next(内部版本已经不更新) 升级到了 @alifd/next,并在此之上建立了一套前端组件库体系。将 Lazada Seller Center 改了模样,在 Fusion 的基础上建立了一套支持整个 Lazada B 端业务的设计规范和业务组件库,覆盖页面 500+。



image.pngimage.png

在这样一个可以说牵一发动全身的背景下,为何还敢有这种想法?


不美



美的反义词,不应该是丑,而是庸俗



不能说 Fusion 丑,但绝对算不上美,这点应该没有争议吧。


虽然也可以在大量的主题样式定制的情况下也可以做到下面这样看上去还行的效果:


image.png


image.png


但说实话,这不能算出众。导致不出众的原因,可以从 Ant Design 上面寻找,Ant Design 的许多细节实现细到令人发指,比如:




  • 弹出窗的追踪动效


    iShot_2023-07-10_11.57.53.gif




  • 按钮的点击动效


    iShot_2023-07-10_11.59.46.gif




  • Tooltip 的箭头追踪


    iShot_2023-07-10_12.02.04.gif




  • NumberPicker 控制按钮放大


    iShot_2023-07-10_12.11.19.gif




这些细节决定了在它上层构建出的应用品质,同样是在一个基础上进行主题和样式的调整。有 Antd 这样品质的基础,就会让在此之上构建的应用品质不会很低,自然也能够带来更好的用户体验及产品品质。


迭代


拿 Antd 的源码和 Fusion 还是有蛮大的差距的,这些差距不只是技术水平的差距,可能在 10 年前他们的代码质量是差不多的,但贵在 Antd 是一个健康的迭代状态。


Antd 已经到了 5.x,Fusion 还是 1.x。这版本后背后意味着 Fusion 从 1.x 发布后就没有大的迭代和改动。即使是 DatePicker、Overlay 这类的组件重构也是提供一个 v2 的 Props 作为差别。


这背后其实反应出的是维护者对于这个库的 Vision (愿景),或许随着 Fusion 这边不断的组织变动,早就已经失去了属于它的那份 Vision。


所以当 Antd 已经在使用 cssinjs、:where、padding-block 这种超前到我都不能接受的东西时,Fusion 里面还充斥着各种 HOC 和 Class。


可以说,Fusion 已经是一个处于缺乏活力,得过且过的维护状态。如果我们不想让这种封闭结构所带来的长期腐蚀所影响,就需要趁早谋求改变。


性能、稳定


得益于上述许多“耗散结构”的好处,Antd 的性能也比 Fusion 要好上许多。许多能够使用 Hooks、CSS 解决的问题,都不会采用组件 JS 来处理,比如 responsive、space 等。


稳定性,既体现在代码的测试质量,又体现在 UI 交互的表现稳定性。比如,Dialog、Tooltip 随着内容高度的变化而动态居中的问题( Fusion overlay v2 有通过 CSS 来控制居中,已经修复)。在很长一段时间内,我们的开发者和用户都承受着元素闪动带来的不好体验。


还有诸如 Icon 不对齐、Label 不对齐,换行 Margin 不居中等等,使用者稍微不注意打开方式,就会可能出现非预期的表现,这些都需要使用者花费额外的精力去在上层处理修复。有些不讲究的开发者就直接把这些丢了用户,又不是不能用。


“又不是不能用” , 而我们不想要这样


投入


Antd 的投入有目共睹,一个 86K star,超过 25K 次提交的库,与 Fusion 的 4.4K star、4K commits。这种投入的比例完全不在一个量级,这还没有计算围绕 Antd 周边丰富的文档、套件等投入。


都是站在巨人的肩膀上,都是借力,没有理由不去选择一个活跃的、周全的、前沿的、生态丰富的巨人。


为什么这变成了问题?


那既然我都把 Antd 吹成这样了,为什么这还需要思考,这还是个问题?无脑换不就行了?


现有生态


或许社区的 Antd 生态非常强劲。但在内部,我们所有的生态都是围绕 Fusion 在建立。包括:



  • 设计规范

  • 业务组件(50+ 常用)

  • 模板 20+

  • 发布体系

  • 业务 External

  • ... 等等许多


切换 Antd,意味着需要对所有现有生态进行升级改造,这将会是一个粗略估计 500+ 小时巨大的投入。


这将意味着我们会拦一个巨大的活到身上,做好了大家用,做不好所有人喷。


影子很重


我们都会发现一个问题,所有 Antd 来做的业务都一眼能被认出来这是 Antd。


因为它太火了,做互联网的应该没有人没见过 Antd 做的页面吧。


辩证的来看,Ant Design 它就叫 “Design”,引入 Antd 还不要它的样式,那你到底想要什么?


“想要它的好看好用,还想让他看上去跟别人不一样”


别急眼,这看上去很荒谬,但这确实是在使用 Antd 时的一个很大诉求。


我认为 Antd 应该考虑像 Daisyui 这样提供多套的主题预设。


不是说这个能力 Antd 现在没有,相反 Antd 5 提供了一整套完整的 Design Token。


但插件体系或者说开放能力,真的需要在官方自己进来做上几个,才会发现会有这么多问题 😭


这就跟 Vite 如果不自己做几个插件,只是提供了插件系统,那它的插件系统大概率是满足不了真正的使用者的。


反正虽然 Antd 5.0 提供了海量的 Design Token,但我在精细化调整样式主题时,还是发现了许多不能调整的地方(就是没有提供这样的 TOKEN 出来)。


因为 cssinjs 的方案,说实话我也不知道应该用什么样的方式进行样式改写才算是最佳实践。


CSS 方案


可以说,近一两年,随着 Vue 3、Vite、Tailwind CSS 等项目的大火🔥,又重新引起了我们对样式的思考。


Unstyled 这个词反复的被 Radix UIHeadless UI 等为首的项目提及,衍生出来的:Shadcn UIArk UI 等热门项目都让人有种醍醐灌顶的感觉。


大概是从 React、Vue 出现开始,UI 的事情就被绑定在了组件库里面,和 JS 逻辑都做好了放一起交给使用者。


但在此之前,样式和 JS 库其实分的很开的。如果你不满意当前的 UI,你大可以换一套 UI 样式库。同样是一个 <button class="btn"></button>,换上不同的 CSS,他们的样式就可以完全不一样。


但前端发展到了今天,如果我想要对我们的样式进行大范围升级,从 Element 换到 Ant Design 很可能涉及到的是技术栈的全部更替。


所以面对 cssinjs,我不敢说这是一个未来的方向,我花了很长时间去了解和体会 cssinjs,也确实它在一些场景中表现出了一些优势:



  • 按需加载,我不用再使用 babel-plugin-import 这类插件

  • 样式不在冲突,完美prefix+ :where hash样式 Scope 运行时计算,必不冲突。微前端友好!

  • ES Module,Bundless 技术不断发展,如果有一天你需要使用 ES Module,你会发现 Antd 5.x 这个组件库不需要任何适配也可以运行的很好,因为它是纯 JS

  • SSR,纯 JS 运行,也可以做 CSS 提取,InlineStyle 也变得没有那么困难


但说实话,这些方案,在原子化 CSS 中也不是无解,甚至还能做的更好。


但 Ant Design 底层其实也是采用 Unstyled 方式沉淀出了一系列的 rc-* 组件,或许有一天这又会有所变化呢,谁知道呢。


总之,我非常不喜欢使用 Props 来控制 Style这件事情。


也非常不喜欢想要用一个 Button,在移动端和 PC 端需要从不同的组件库中导入。


所以,有答案了吗?


说实话,这个问题,我思考了很久。每次思考,仿佛抓到了什么,又仿佛没有抓到什么,其实写这篇文章也是把一些思考过程罗列下来,或许能想的更清楚。



最初科举考试是选拔官僚用的,其中一个作用是:筛选出那些能够忍受每天重复做自己不喜欢事情的人



或许畏惧变化、畏惧折腾,或许就应该用 Fusion ,因为可以确定的是 Antd 5 绝对不是最后一个大版本。


选择 Antd,也意味着选择迭代更快的底层依赖,意味着拥抱了更活跃的变化,意味着要持续折腾。


如果没有准备好这种心态,那即使换了 Antd,大概率也可能会锁定某个版本,或者直接拷贝一份,这种最粗暴的方式使用。然后进入下一个循环。


今天这个 Antd 咱们是非换不可吗?


我想我已经有了我的决定,你呢?


(ps. 为什么大家对暗黑模式这么不重视...)


(ps. 如果 Fusion 相关同学看到,别自责,这不怪

作者:YeeWang
来源:juejin.cn/post/7254559214588543034
你...)

收起阅读 »

为什么React一年不发新版了?

web
大家好,我卡颂。 遥想前几年,不管是React还是Vue,都在快速迭代版本,以至于很多同学抱怨学不动了。 而现在,React已经一年没更新稳定release了。 甚至有人认为,这就是前端已死最直接的证据: 那么,React最近一年为什么不发版了呢?是因为前...
继续阅读 »

大家好,我卡颂。


遥想前几年,不管是React还是Vue,都在快速迭代版本,以至于很多同学抱怨学不动了


而现在,React已经一年没更新稳定release了。


上一次发版还是22年6月


甚至有人认为,这就是前端已死最直接的证据:



那么,React最近一年为什么不发版了呢?是因为前端框架领域已经没有新活儿可整了么?React v19是不是遥遥无期了?


欢迎围观朋友圈、加入人类高质量前端交流群,带飞


最近一年React活跃吗?


不想看长文章的同学,这里一句话总结本文观点:



React之所以一年没发版,并不是因为无活可整,而是在完成框架从UI库到元框架的转型



首先,我们来看看,最近这一年React的更新活跃度是否降低?


从代码push量来看,最近一年甚至比release产出较多的前几年更活跃:



既然更活跃,那React这段时间到底在做什么呢?从代码增删行数可以一窥端倪,其中:




  • 绿色柱状代表代码增加行数




  • 红色柱状代表代码减少行数




  • 红色折线代表代码行数总体趋势





代码量变化来看,React历史上大体分为四个时期:




  • 13年开源,到17年之前的功能迭代期




  • 持续到18年的重构期(重构React Fiber架构)




  • 18~22年基于Fiber架构的新功能迭代期




  • 22年至今的重构期




功能迭代期重构期的区别在于:




  • 前者主要是在稳定的架构上迭代新特性




  • 后者一般重构底层架构的同时,重构老特性




剧烈的代码量波动通常发生在重构期。比如,在最近的重构期内,PR #25774删除了3w行代码。




这个PR主要改变React对于同一个子包,同时拥有.new.old两个文件的开发模式



最近一年React都在干啥?


明确了React最近一年处于重构期。那么,究竟是重构什么呢?


答案是 —— 将RSCReact Server Component,服务端组件)接入当前React体系内。


有同学会问:RSC只是个类似SSR的特性,为什么要实现他还涉及重构?


这是因为RSC不仅是一个特性,更是React未来主要的发展方向,其意义不亚于Hooks。所以,围绕RSC的迭代涉及大量代码的重构。比如:




  • SSR相关代码需要修改




  • SSR代码修改导致Suspense组件代码修改




  • Suspense的修改又牵扯到useEffect回调触发时机的变化




可以说是牵一发而动全身了。


RSC为什么重要


为什么RSCReact这么重要?要回答这个问题,得从开源项目的发展聊起。


开源项目要想获得成功,一定需要满足目标用户(开发者)的需求。


早期,React作为前端框架,满足了UI开发的需求。在此期间,React团队的迭代方向主要是:




  • 摸索更清晰的开发范式(发布了Error BoundraySuspenseHooks




  • 修补代码(发布新的Context实现)




  • 优化开发体验(发布CRA




  • 底层优化(重构Fiber架构)




可以发现,这些迭代内容中大部分(除了底层优化)都是直接面向普通开发者的,所以React文档(文档也是面向开发者的)中都有体现,开发者通过文档能直观的感受到React不断迭代。


随着前端领域的发展,逐渐涌现出各种业务开发的最佳实践,比如:




  • 状态管理的最佳实践




  • 路由的最佳实践




  • SSR的最佳实践




一些框架开始整合这些最佳实践(比如Next.jsRemix,或者国内的Umijs...)


到了这一时期,开发者更多是通过使用这些框架间接使用React


感受到这一变化后,React团队的发展方向逐渐变化 —— 从面向开发者的前端框架变为面向上层框架的元框架。


发展方向变化最明显的表现是 —— 文档中新出的特性普通开发者很少会用到,比如:




  • useTransition




  • useId




  • useMutableSource




这些特性都是作为元框架,给上层框架(或库)使用的。


上述特性虽然普通开发者很少用到,但至少文档中提及了。但随着React不断向元框架方向发展,即使出了新特性,文档中已经不再提及了。比如:




  • useOptimistic




  • useFormStatus




上述两个Hook想必大部分同学都没听过。他们是React源码中切实存在的Hook。但由于是元框架理念下的产物,所以React文档并未提及。相反,Next.js文档中可以看到使用介绍。


总结


React之所以已经一年没有发布稳定release,是因为发展方向已经从面向开发者转型为面向上层框架


在此期间的更新都是面向上层框架,所以开发者很难感知到React的变化。


但这并不能说明React停止迭代了,也不能据此认为前端发展的停滞。


如果一定要定量观察React最近一年的发展,距离React v19里程碑,已经大体过半了:


收起阅读 »

5分钟,带你迅速上手“Markdown”语法

web
本篇将重点讲解:Markdown的 “语法规范” 与 “上手指南”。 一、Markdown简介 Markdown是一种文本标记语言,它容易上手、易于学习,排版清晰明了、直观清晰。常用于撰写 “技术文档” 、 “技术博客” 、 “开发文档” 等等。 总之,如...
继续阅读 »

本篇将重点讲解:Markdown的 “语法规范”“上手指南”





一、Markdown简介


Markdown是一种文本标记语言,它容易上手、易于学习,排版清晰明了、直观清晰。常用于撰写 “技术文档”“技术博客”“开发文档” 等等。
总之,如果你是一名开发者,并且你有写博客的欲望与想法时,使用Markdown是你不二的选择。




二、Markdown语法


接下来,我们来看一下Markdown“标准语法”


我们看下大纲,其中包括:



1、标题


  • 标准语法:使用1~6“#”符 + “空格” + “你的标题”。


# 一级标题
## 二级标题
### 三级标题
#### 四级标题
##### 五级标题
###### 六级标题


  • 效果图解:




注:#和「标题」之间有一个空格,这是最标准的语法格式。
有些编辑器做了兼容,有的并没有。所以最好要加上空格。



2、列表


  • 标准语法:使用-符,在文本前加入-符即可。


- 文本1
- 文本2
- 文本3

如果你希望有序,在文本前加上1. 2. 3. 4. ...


1. 文本1
2. 文本2
3. 文本3


注:-1. 2. 等和文本之间要保留一个字符的空格。




  • 效果图解:



3、超链接



  • 标准语法:[链接名](链接url)




  • 效果图解:





4、图片


  • 标准语法:


![图片名](链接url)


  • 效果图解:



5、引用



  • 标准语法:> 文本




  • 效果图解:





6、斜体、加粗



  • 标准语法
    斜体*文本*
    加粗**文本**
    斜体&加粗***文本***




  • 效果图解:





7、代码块



  • 标准语法:
    ``` 你的代码 ```(前面3个点,后面3个点)




  • 效果图解:







8、表格


  • 标准语法:


dog | bird | cat
----|------|----
foo | foo | foo
bar | bar | bar
baz | baz | baz


  • 效果图解:





9、特殊标记


  • 标准语法:``


`特殊样式`


  • 效果图解:





10、分割线



  • 标准语法:--- 最少3个




  • 效果图解:





11、常用html标记

注意:html标记只适合辅助使用,不一定所有编辑器都能生效。



  • 标准语法:


换行符:<br/> (或者使用Markdown标准语法:空格+空格+回车,但我感觉不是很直观)
上:<sup>文本</sup>
下:<sub>文本</sub>



  • 效果图解:





三、Markdown优点



  • 纯文本,所以兼容性极强,可以用所有文本编辑器打开。

  • 让作者更专注于写作而不是排版。(大家都是技术人员嘛..)

  • 格式转化方便,markdown文本可以很轻松转成htmlpdf等等。(图个方便嘛)

  • 语法简单

  • 可读性强,配合表格、引用、代码块等等,让读者瞬间“懂
    作者:齐舞647
    来源:juejin.cn/post/7254107670012510245
    你”。

收起阅读 »

🤣泰裤辣!这是什么操作,自动埋点,还能传参?

web
前言 在上篇文章讲了如何通过手写babel插件自动给函数埋点之后,就有同学问我,自动插入埋点的函数怎么给它传参呢?这篇文章就来解决这个问题我讲了通过babel来实现自动化埋点,也讲过读取注释给特定函数插入埋点代码,感兴趣的同学可以来这里 给所有函数都添加埋...
继续阅读 »


前言


在上篇文章讲了如何通过手写babel插件自动给函数埋点之后,就有同学问我,自动插入埋点的函数怎么给它传参呢?这篇文章就来解决这个问题
我讲了通过babel来实现自动化埋点,也讲过读取注释给特定函数插入埋点代码,感兴趣的同学可以来这里





效果是这样的
源代码:


//##箭头函数
//_tracker
const test1 = () => {};

const test1_2 = () => {};

转译之后:


import _tracker from "tracker";
//##箭头函数
//_tracker
const test1 = () => {
_tracker();
};

const test1_2 = () => {};

代码中有两个函数,其中一个//_tracker的注释,另一个没有。转译之后只给有注释的函数添加埋点函数。
要达到这个效果就需要读取函数上面的注释,如果注释中有//_tracker,我们就给函数添加埋点。这样做避免了僵硬的给每个函数都添加埋点的情况,让埋点更加灵活。




那想要给插入的埋点函数传入参数应该怎么做呢?
传入参数可以有两个思路,



  • 一个是将参数也放在注释里面,在babel插入代码的时候读取下注释里的内容就好了;

  • 另一个是将参数以局部变量的形式放在当前作用域中,在babel插入代码时读取下当前作用域的变量就好;


下面我们来实现这两个思路,大家挑个自己喜欢的方法就好


参数放在注释中


整理下源代码


import "./index.css";

//##箭头函数
//_tracker,_trackerParam={name:'gongfu', age:18}
const test1 = () => {};

//_tracker
const test1_2 = () => {};


代码中,有两个函数,每个函数上都有_tracker的注释,其中一个注释携带了埋点函数的参数,待会我们就要将这个参数放到埋点函数里



关于如何读取函数上方的注释,大家可以这篇文章:(),我就不赘述了




准备入口文件


index.js


const { transformFileSync } = require("@babel/core");
const path = require("path");
const tracker = require("./babel-plugin-tracker-comment.js");

const pathFile = path.resolve(__dirname, "./sourceCode.js");

//transform ast and generate code
const { code } = transformFileSync(pathFile, {
plugins: [[tracker, { trackerPath: "tracker", commentsTrack: "_tracker",commentParam: "_trackerParam" }]],
});

console.log(code);



和上篇文章的入口文件类似,使用了transformFileSyncAPI转译源代码,并将转译之后的代码打印出来。过程中,将手写的插件作为参数传入plugins: [[tracker, { trackerPath: "tracker", commentsTrack: "_tracker"}]]。除此之外,还有插件的参数



  • trackerPath表示埋点函数的路径,插件在插入埋点函数之前会检查是否已经引入了该函数,如果没有引入就需要额外引入。

  • commentsTrack标识埋点,如果函数前的注释有这个,就说明函数需要埋点。判断的标识是动态传入的,这样比较灵活

  • commentParam标识埋点函数的参数,如果注释中有这个字符串,那后面跟着的就是参数了。就像上面源代码所写的那样。这个标识不是固定的,是可以配置化的,所以放在插件参数的位置上传进去


编写插件


插件的功能有:



  • 查看埋点函数是否已经引入

  • 查看函数的注释是否含有_tracker

  • 将埋点函数插入函数中

  • 读取注释中的参数


前三个功能在上篇文章(根据注释添加埋点)中已经实现了,下面实现第四个功能


const paramCommentPath = hasTrackerComments(leadingComments, options.commentsTrack);
if (paramCommentPath) {
const param = getParamsFromComment(paramCommentPath, options);
insertTracker(path, param, state);
}

//函数实现
const getParamsFromComment = (commentNode, options) => {
const commentStr = commentNode.node.value;
if (commentStr.indexOf(options.commentParam) === -1) {
return null;
}

try {
return commentStr.slice(commentStr.indexOf("{"), commentStr.indexOf("}") + 1);
} catch {
return null;
}
};

const insertTracker = (path, param, state) => {
const bodyPath = path.get("body");
if (bodyPath.isBlockStatement()) {
let ast = template.statement(`${state.importTackerId}(${param});`)();
if (param === null) {
ast = template.statement(`${state.importTackerId}();`)();
}
bodyPath.node.body.unshift(ast);
} else {
const ast = template.statement(`{
${state.importTackerId}(${param});
return BODY;
}`
)({ BODY: bodyPath.node });
bodyPath.replaceWith(ast);
}
};


上述代码的逻辑是检查代码是否含有注释_tracker,如果有的话,再检查这一行注释中是否含有参数,最后再将埋点插入函数。
在检查是否含有参数的过程中,用到了插件参数commentParam。表示如果注释中含有该字符串,那后面的内容就是参数了。获取参数的方法也是简单的字符串切割。如果没有获取到参数,就一律返回null



获取参数的复杂程度,取决于。先前是否就有一个规范,并且编写代码时严格按照规范执行。
像我这里的规范是埋点参数commentParam和埋点标识符_tracker必须放在一行,并且参数需要是对象的形式。即然是对象的形式,那这一行注释中就不允许就其他的大括号符号"{}"
遵从了这个规范,获取参数的过程就变的很简单了。当然你也可以有自己的规范



在执行插入逻辑的函数中,会校验参数param是否为null,如果是null,生成ast的时候,就不传入param了。



当然你也可以一股脑地传入param,不会影响结果,顶多是生成的埋点函数会收到一个null的参数,像这样_tracker(null)



第四个功能也实现了,来看下完整代码


完整代码


const { declare } = require("@babel/helper-plugin-utils");
const { addDefault } = require("@babel/helper-module-imports");
const { template } = require("@babel/core");
//judge if there are trackComments in leadingComments
const hasTrackerComments = (leadingComments, comments) => {
if (!leadingComments) {
return false;
}
if (Array.isArray(leadingComments)) {
const res = leadingComments.filter((item) => {
return item.node.value.includes(comments);
});
return res[0] || null;
}
return null;
};

const getParamsFromComment = (commentNode, options) => {
const commentStr = commentNode.node.value;
if (commentStr.indexOf(options.commentParam) === -1) {
return null;
}

try {
return commentStr.slice(commentStr.indexOf("{"), commentStr.indexOf("}") + 1);
} catch {
return null;
}
};

const insertTracker = (path, param, state) => {
const bodyPath = path.get("body");
if (bodyPath.isBlockStatement()) {
let ast = template.statement(`${state.importTackerId}(${param});`)();
if (param === null) {
ast = template.statement(`${state.importTackerId}();`)();
}
bodyPath.node.body.unshift(ast);
} else {
const ast = template.statement(`{
${state.importTackerId}(${param});
return BODY;
}`
)({ BODY: bodyPath.node });
bodyPath.replaceWith(ast);
}
};


const checkImport = (programPath, trackPath) => {
let importTrackerId = "";
programPath.traverse({
ImportDeclaration(path) {
const sourceValue = path.get("source").node.value;
if (sourceValue === trackPath) {
const specifiers = path.get("specifiers.0");
importTrackerId = specifiers.get("local").toString();
path.stop();
}
},
});

if (!importTrackerId) {
importTrackerId = addDefault(programPath, trackPath, {
nameHint: programPath.scope.generateUid("tracker"),
}).name;
}

return importTrackerId;
};

module.exports = declare((api, options) => {

return {
visitor: {
"ArrowFunctionExpression|FunctionDeclaration|FunctionExpression|ClassMethod": {
enter(path, state) {
let nodeComments = path;
if (path.isExpression()) {
nodeComments = path.parentPath.parentPath;
}
// 获取leadingComments
const leadingComments = nodeComments.get("leadingComments");
const paramCommentPath = hasTrackerComments(leadingComments,options.commentsTrack);

// 如果有注释,就插入函数
if (paramCommentPath) {
//add Import
const programPath = path.hub.file.path;
const importId = checkImport(programPath, options.trackerPath);
state.importTackerId = importId;

const param = getParamsFromComment(paramCommentPath, options);
insertTracker(path, param, state);
}
},
},
},
};
});



运行代码


现在可以用入口文件来使用这个插件代码了


node index.js

执行结果
image.png



运行结果符合预期



可以看到我们设置的埋点参数确实被放到函数里面了,而且注释里面写了什么,函数的参数就会放什么,那么既然如此,可以传递变量吗?我们来试试看


import "./index.css";

//##箭头函数
//_tracker,_trackerParam={name, age:18}
const test1 = () => {
const name = "gongfu2";
};

const test1_2 = () => {};

在需要插入的代码中,声明了一个变量,然后注释的参数刚好用到了这个变量。
运行代码看看效果
image.png
可以看到,插入的参数确实用了变量,但是引用变量却在变量声明之前,这肯定不行🙅。得改改。
需要将埋点函数插入到函数体的后面,并且是returnStatement的前面,这样就不会有问题了


const insertTrackerBeforeReturn = (path, param, state) => {
//blockStatement
const bodyPath = path.get("body");
let ast = template.statement(`${state.importTackerId}(${param});`)();
if (param === null) {
ast = template.statement(`${state.importTackerId}();`)();
}
if (bodyPath.isBlockStatement()) {
//get returnStatement, by body of blockStatement
const returnPath = bodyPath.get("body").slice(-1)[0];
if (returnPath && returnPath.isReturnStatement()) {
returnPath.insertBefore(ast);
} else {
bodyPath.node.body.push(ast);
}
} else {
ast = template.statement(`{ ${state.importTackerId}(${param}); return BODY; }`)({ BODY: bodyPath.node });
bodyPath.replaceWith(ast);
}
};

这里将insertTracker改成了insertTrackerBeforeReturn
其中关键的逻辑是判断是否是一个函数体,



  • 如果是一个函数体,就判断有没有return语句,

    • 如果有return,就放在return前面

    • 如果没有return,就放在整个函数体的后面



  • 如果不是一个函数体,就直接生成一个函数体,然后将埋点函数放在return的前面


再来运行插件:
image.png



很棒,这就是我们要的效果😃




完整代码


const { declare } = require("@babel/helper-plugin-utils");
const { addDefault } = require("@babel/helper-module-imports");
const { template } = require("@babel/core");
//judge if there are trackComments in leadingComments
const hasTrackerComments = (leadingComments, comments) => {
if (!leadingComments) {
return false;
}
if (Array.isArray(leadingComments)) {
const res = leadingComments.filter((item) => {
return item.node.value.includes(comments);
});
return res[0] || null;
}
return null;
};

const getParamsFromComment = (commentNode, options) => {
const commentStr = commentNode.node.value;
if (commentStr.indexOf(options.commentParam) === -1) {
return null;
}

try {
return commentStr.slice(commentStr.indexOf("{"), commentStr.indexOf("}") + 1);
} catch {
return null;
}
};

const insertTrackerBeforeReturn = (path, param, state) => {
//blockStatement
const bodyPath = path.get("body");
let ast = template.statement(`${state.importTackerId}(${param});`)();
if (param === null) {
ast = template.statement(`${state.importTackerId}();`)();
}
if (bodyPath.isBlockStatement()) {
//get returnStatement, by body of blockStatement
const returnPath = bodyPath.get("body").slice(-1)[0];
if (returnPath && returnPath.isReturnStatement()) {
returnPath.insertBefore(ast);
} else {
bodyPath.node.body.push(ast);
}
} else {
ast = template.statement(`{ ${state.importTackerId}(${param}); return BODY; }`)({BODY: bodyPath.node });
bodyPath.replaceWith(ast);
}
};


const checkImport = (programPath, trackPath) => {
let importTrackerId = "";
programPath.traverse({
ImportDeclaration(path) {
const sourceValue = path.get("source").node.value;
if (sourceValue === trackPath) {
const specifiers = path.get("specifiers.0");
importTrackerId = specifiers.get("local").toString();
path.stop();
}
},
});

if (!importTrackerId) {
importTrackerId = addDefault(programPath, trackPath, {
nameHint: programPath.scope.generateUid("tracker"),
}).name;
}

return importTrackerId;
};

module.exports = declare((api, options) => {

return {
visitor: {
"ArrowFunctionExpression|FunctionDeclaration|FunctionExpression|ClassMethod": {
enter(path, state) {
let nodeComments = path;
if (path.isExpression()) {
nodeComments = path.parentPath.parentPath;
}
// 获取leadingComments
const leadingComments = nodeComments.get("leadingComments");
const paramCommentPath = hasTrackerComments(leadingComments,options.commentsTrack);

// 如果有注释,就插入函数
if (paramCommentPath) {
//add Import
const programPath = path.hub.file.path;
const importId = checkImport(programPath, options.trackerPath);
state.importTackerId = importId;

const param = getParamsFromComment(paramCommentPath, options);
insertTrackerBeforeReturn(path, param, state);
}
},
},
},
};
});


参数放在局部作用域中


这个功能的关键就是读取当前作用域中的变量。


在写代码之前,来定一个前提:当前作用域的变量名也和注释中参数标识符一致,也是_trackerParam


准备源代码


import "./index.css";

//##箭头函数
//_tracker,_trackerParam={name, age:18}
const test1 = () => {
const name = "gongfu2";
};

const test1_2 = () => {};

//函数表达式
//_tracker
const test2 = function () {
const age = 1;
_trackerParam = {
name: "gongfu3",
age,
};
};

const test2_1 = function () {
const age = 2;
_trackerParam = {
name: "gongfu4",
age,
};
};

代码中,准备了函数test2test2_1。其中都有_trackerParam作为局部变量,但test2_1没有注释//_tracker


编写插件


if (paramCommentPath) {
//add Import
const programPath = path.hub.file.path;
const importId = checkImport(programPath, options.trackerPath);
state.importTackerId = importId;

//check if have tackerParam
const hasTrackParam = path.scope.hasBinding(options.commentParam);
if (hasTrackParam) {
insertTrackerBeforeReturn(path, options.commentParam, state);
return;
}

const param = getParamsFromComment(paramCommentPath, options);
insertTrackerBeforeReturn(path, param, state);
}

这个函数的逻辑是先判断当前作用域中是否有变量_trackerParam,有的话,就获取该声明变量的初始值。然后将该变量名作为insertTrackerBeforeReturn的参数传入其中。
我们运行下代码看看
image.png



运行结果符合预期,很好




完整代码


const { declare } = require("@babel/helper-plugin-utils");
const { addDefault } = require("@babel/helper-module-imports");
const { template } = require("@babel/core");
//judge if there are trackComments in leadingComments
const hasTrackerComments = (leadingComments, comments) => {
if (!leadingComments) {
return false;
}
if (Array.isArray(leadingComments)) {
const res = leadingComments.filter((item) => {
return item.node.value.includes(comments);
});
return res[0] || null;
}
return null;
};

const getParamsFromComment = (commentNode, options) => {
const commentStr = commentNode.node.value;
if (commentStr.indexOf(options.commentParam) === -1) {
return null;
}

try {
return commentStr.slice(commentStr.indexOf("{"), commentStr.indexOf("}") + 1);
} catch {
return null;
}
};

const insertTrackerBeforeReturn = (path, param, state) => {
//blockStatement
const bodyPath = path.get("body");
let ast = template.statement(`${state.importTackerId}(${param});`)();
if (param === null) {
ast = template.statement(`${state.importTackerId}();`)();
}
if (bodyPath.isBlockStatement()) {
//get returnStatement, by body of blockStatement
const returnPath = bodyPath.get("body").slice(-1)[0];
if (returnPath && returnPath.isReturnStatement()) {
returnPath.insertBefore(ast);
} else {
bodyPath.node.body.push(ast);
}
} else {
ast = template.statement(`{${state.importTackerId}(${param}); return BODY;}`)({BODY: bodyPath.node });
bodyPath.replaceWith(ast);
}
};


const checkImport = (programPath, trackPath) => {
let importTrackerId = "";
programPath.traverse({
ImportDeclaration(path) {
const sourceValue = path.get("source").node.value;
if (sourceValue === trackPath) {
const specifiers = path.get("specifiers.0");
importTrackerId = specifiers.get("local").toString();
path.stop();
}
},
});

if (!importTrackerId) {
importTrackerId = addDefault(programPath, trackPath, {
nameHint: programPath.scope.generateUid("tracker"),
}).name;
}

return importTrackerId;
};

module.exports = declare((api, options) => {

return {
visitor: {
"ArrowFunctionExpression|FunctionDeclaration|FunctionExpression|ClassMethod": {
enter(path, state) {
let nodeComments = path;
if (path.isExpression()) {
nodeComments = path.parentPath.parentPath;
}
// 获取leadingComments
const leadingComments = nodeComments.get("leadingComments");
const paramCommentPath = hasTrackerComments(leadingComments,options.commentsTrack);

// 如果有注释,就插入函数
if (paramCommentPath) {
//add Import
const programPath = path.hub.file.path;
const importId = checkImport(programPath, options.trackerPath);
state.importTackerId = importId;

//check if have tackerParam
const hasTrackParam = path.scope.hasBinding(options.commentParam);
if (hasTrackParam) {
insertTrackerBeforeReturn(path, options.commentParam, state);
return;
}

const param = getParamsFromComment(paramCommentPath, options);
insertTrackerBeforeReturn(path, param, state);
}
},
},
},
};
});


总结:


这篇文章讲了如何才埋点的函数添加参数,参数可以写在注释里,也可以写在布局作用域中。支持动态传递,非常灵活。感兴趣的金友们可以拷一份代码下来跑一跑,相信你们会很有成就感的。


下篇文章来讲讲如何在create-reate-app中使用我们手写的babel插件。



相关文章:



  1. 通过工具babel,给函数都添加埋点

  2. 通过工具babel,根据注释添加埋点


作者:慢功夫
来源:juejin.cn/post/7254032949229764669

收起阅读 »

作为一名前端给自己做一个算命转盘不过分吧

web
算命转盘 前言 给自己做一个算命转盘,有事没事算算命,看看运势挺好的(虽然我也看不懂)。 这个算命转盘我是实现在了自己的个人博客中的这里是地址,感兴趣可以点进去看看。 实现过程 开发技术:react + ts 该转盘主要是嵌套了三层 圆形滚动组件 来实现的,...
继续阅读 »

算命转盘


zodiac.gif

前言


给自己做一个算命转盘,有事没事算算命,看看运势挺好的(虽然我也看不懂)。


这个算命转盘我是实现在了自己的个人博客中的这里是地址,感兴趣可以点进去看看。


实现过程


开发技术:react + ts


该转盘主要是嵌套了三层 圆形滚动组件 来实现的,再通过 ref 绑定组件,调用其中的 scrollTo 方法即可使组件发生指定的滚动,再传入随机数,即可实现随机旋转效果,通过嵌套三层该组件实现三层的随机旋转,模拟“算命”效果。


// 这是精简后的代码
export default () => {
const onScrollCircle = () => {
const index = Math.floor(Math.random() * zodiacList.length)
scrollCircleRef.current?.scrollTo({index, duration: 1000})
}
return (
<>
<ScrollCircle ref={scrollCircleRef}></ScrollCircle>
<button onClick={() => onScrollCircle}>点击旋转</button>
</>

)
}

三层大致结构如下:具体代码可以看码上掘金



  • 转盘的第一层


export default () => {
return (
<ScrollCircle>
{list.map((item, index) => (
<ScrollCircle.Item
key={index}
index={index}
>

<CircleItem />
</ScrollCircle.Item>
))}
</ScrollCircle>

)
}


  • 转盘的第二层


const CircleItem = () => {
return (
<ScrollCircle>
{list.map((item, index) => (
<ScrollCircle.Item
key={index}
index={index}
>

<CircleItemChild />
</ScrollCircle.Item>
))}
</ScrollCircle>

)
}


  • 转盘的第三层


const CircleItemChild = () => {
return (
<ScrollCircle>
{list.map((item, index) => (
<ScrollCircle.Item
key={index}
index={index}
>

<div>
内容
</div>
</ScrollCircle.Item>
))}
</ScrollCircle>

)
}

圆形滚动组件


现在的 圆形滚动组件 支持展示到上下左右中各个方向上,要是大家使用过程中有什么意见可以提一下,我尽力实现,当然能提 pr 最好了(∪^ェ^∪)。


组件源码地址


线上Demo演示地址


image.png

主要是在旧版的基础上不断完善而来的,旧版圆形滚动组件的 往期文章


props等使用文档


ScrollCircle


属性名描述类型默认值
listLength传入卡片的数组长度number(必选)
width滚动列表的宽度string"100%"
height滚动列表的高度string"100%"
centerPoint圆心的位置"center" , "auto" , "left" , "right" , "bottom" , "top""auto (宽度大于高度时在底部,否则在右侧)"
circleSize圆的大小"inside" , "outside""outside (圆溢出包裹它的盒子)"

其他的属性...(篇幅问题就不全放上来了,可以直接去线上Demo演示地址查看)


centerPoint


主要通过该属性,将圆心控制到上下左右中间位置。


属性名描述
auto自动适应,当圆形区域宽度大于高度时,圆心会自动在底部,否则在右边
center建议搭配 circleSize='inside' 一起使用(让整个圆形在盒子内部)
left让圆心在左边
top让圆心在顶部
right让圆心在右边
bottom让圆心在底部

作者:滑动变滚动的蜗牛
来源:juejin.cn/post/7254014646779428922
收起阅读 »

vue3 表单封装遇到的一个有意思的问题

web
前言 最近在用 vue3 封装 element 的表单时遇到的一个小问题,这里就简单记录一下过程。话不多说直接上代码!!! 正文 部分核心代码 import { ref, defineComponent, renderSlot, type PropType, ...
继续阅读 »

前言


最近在用 vue3 封装 element 的表单时遇到的一个小问题,这里就简单记录一下过程。话不多说直接上代码!!!


正文


部分核心代码


import { ref, defineComponent, renderSlot, type PropType, type SetupContext } from 'vue';
import { ElForm, ElFormItem, ElRow, ElCol } from 'element-plus';
import type { RowProps, FormItemProps, LabelPosition } from './types';
import formItemRender from './CusomFormItem';
import { pick } from 'lodash-es';

const props = {
formRef: {
type: String,
default: 'customFormRef',
},
modelValue: {
type: Object as PropType<Record<string, unknown>>,
default: () => ({}),
},
rowProps: {
type: Object as PropType<RowProps>,
default: () => ({
gutter: 24,
}),
},
formData: {
type: Array as PropType<FormItemProps[]>,
default: () => [],
},
labelPosition: {
type: String as PropType<LabelPosition>,
default: 'right',
},
labelWidth: {
type: String,
default: '150px',
},
};

const elFormItemPropsKeys = [
'prop',
'label',
'labelWidth',
'required',
'rules',
// 'error',
// 'showMessage',
// 'inlineMessage',
// 'size',
// 'for',
// 'validateStatus',
];

export default defineComponent({
name: 'CustomForm',
props,
emits: ['update:modelValue'],
setup(props, { slots, emit, expose }: SetupContext) {
const customFormRef = ref();

const mValue = ref({ ...props.modelValue });

watch(
mValue,
(newVal) => {
emit('update:modelValue', newVal);
},
{
immediate: true,
deep: true,
},
);

// 表单校验
const validate = async () => {
if (!customFormRef.value) return;
return await customFormRef.value.validate();
};

// 表单重置
const resetFields = () => {
if (!customFormRef.value) return;
customFormRef.value.resetFields();
};

// 暴漏方法
expose({ validate, resetFields });

// col 渲染
const colRender = () => {
return props.formData.map((i: FormItemProps) => {
const formItemProps = { labelWidth: props.labelWidth, ...pick(i, elFormItemPropsKeys) };
return (
<ElCol {...i.colProps}>
<ElFormItem {...formItemProps}>
{i.formItemType === 'slot'
? renderSlot(slots, i.prop, { text: mValue.value[i.prop], props: { ...i } })
: formItemRender(i, mValue.value)}
</ElFormItem>
</ElCol>

);
});
};

return () => (
<ElForm ref={customFormRef} model={mValue} labelPosition={props.labelPosition}>
<ElRow {...props.rowProps}>
{colRender()}
<ElCol>
<ElFormItem labelWidth={props.labelWidth}>{renderSlot(slots, 'action')}</ElFormItem>
</ElCol>
</ElRow>
</ElForm>

);
},
});

<script setup lang="ts">
import CustomerForm from '/@/components/CustomForm';
const data = ref([
{
formItemType: 'input',
prop: 'name',
label: 'Activity name',
placeholder: 'Activity name',
rules: [
{
required: true,
message: 'Please input Activity name',
trigger: 'blur',
},
{ min: 3, max: 5, message: 'Length should be 3 to 5', trigger: 'blur' },
],
},
{
formItemType: 'select',
prop: 'region',
label: 'Activity zone',
placeholder: 'Activity zone',
options: [
{
label: 'Zone one',
value: 'shanghai',
},
{
label: 'Zone two',
value: 'beijing',
},
],
},
{
formItemType: 'inputNumber',
prop: 'count',
label: 'Activity count',
placeholder: 'Activity count',
},
{
formItemType: 'date',
prop: 'date',
label: 'Activity date',
type: 'datetime',
placeholder: 'Activity date',
},
{
formItemType: 'radio',
prop: 'resource',
label: 'Resources',
options: [
{ label: 'Sponsorship', value: '1' },
{ label: 'Venue', value: '2' },
],
},
{
formItemType: 'checkbox',
prop: 'type',
label: 'Activity type',
options: [
{ label: 'Online activities', value: '1', disabled: true },
{ label: 'Promotion activities', value: '2' },
{ label: 'Offline activities', value: '3' },
{ label: 'Promotion activities', value: '4' },
{ label: 'Simple brand exposure', value: '5' },
],
},
{
formItemType: 'input',
prop: 'desc',
type: 'textarea',
label: 'Activity form',
placeholder: 'Activity form',
},
{
formItemType: 'slot',
prop: 'test',
label: 'slot',
},
]);
const model = reactive({
name: '',
region: '',
count: 0,
date: '',
resource: '',
type: [],
desc: '',
test: '1111',
});
const formRef = ref();
const submitForm = () => {
const valid = formRef.value.validate();
if (valid) {
console.log(model);
} else {
return false;
}
};

const resetForm = () => {
formRef.value.resetFields();
};
</script>

<template>
<div class="wrap">
<CustomerForm
ref="formRef"
:v-model="model"
:formData="data"
>

<template #test="scope">
{{ scope.text }}
</template>
<template #action>
<el-button type="primary" @click="submitForm()">Create</el-button>
<el-button @click="resetForm()">Reset</el-button>
</template>
</CustomerForm>
</div>
</template>


<style scoped>
.wrap {
margin: 30px auto;
width: 600px;
height: auto;
}
</style>



问题现象


代码其实非常简单,运行起来也很正常很流畅😀😀😀,但是当我填写完表单后点击提交按钮,打印model的值时,发现值全没给上。


微信截图_20230709120015.png


原因分析


这里经过两年半的尝试,终于发现在定义model时,将const model = reactive({xxx}) 改为 const model = ref({xxx}) 后就正常了。思考了一下 ref 定义的对象,源码上最后通过 toReactive 还是被转化为 reactive,ref 用法上需要 .value, 数据上这两者应该没有什么不同。然后我就去把 reactive、ref 又看了看也没发现问题。在emit('update:modelValue', newVal) 处打印也是正常的。


watch( mValue,
(newVal) => {
console.log('newVal>>>', newVal)
emit('update:modelValue', newVal);
},
{ immediate: true, deep: true, }
);

最后有意思的是,我把 const model 改成 let model tmd居然也正常了,这就让我百思不得其解了😕😕😕


解决


其实上面 debugger 后,就确定了方向 肯定是emit('update:modelValue', newVal)这里出问题了,回到使用组件,把v-model 拆解一下,此时还看不出来问题。


1688879457596.jpg


换成:modelValue="model" @update:model-value="update(e)"问题立马出现了,ts已经提示了 model是常量!


微信截图_20230709131250.png


这样问题就非常明了了,这就解释了 let 可以 const 不行,但你好歹报个错啊 😤😤😤 坑死人不偿命,可见即使在 template 里面这样写@update:model-value="model = $event" ts 也无能为力!
回过头再来看看 ref 为啥可行呢?当改成ref时,


  const update = (e) => {
model.value = e;
};

update是要.value 的,修改常量对象里面属性是正常的。再想想 ref 的变量在 template 中 vue 已经帮我们解过包了,v-model 语法糖拿着属性直接赋值并不会产生问题。而常量 reactive 则不能修改,也可以在在里面再包裹一层对象,但这样就有点冗余了。


总结


总结起来就是,const 定义的 reactive 对象,v-model 去更新整个对象的时候失败,常量不能更改,也没有给出任何报错或提示!


唉!今年太难了。前端路漫漫其修远兮,还需

作者:Pluto5280
来源:juejin.cn/post/7253453908039123005
更加卷地而行!😵😵😵

收起阅读 »

极致舒适的Vue弹窗使用方案

web
一个Hook让你体验极致舒适的Dialog使用方式! Dialog地狱 为啥是地狱? 因为凡是有Dialog出现的页面,其代码绝对优雅不起来!因为一旦你在也个组件中引入Dialog,就最少需要额外维护一个visible变量。如果只是额外维护一个变量这也不是不...
继续阅读 »

一个Hook让你体验极致舒适的Dialog使用方式!


image.png


Dialog地狱


为啥是地狱?


因为凡是有Dialog出现的页面,其代码绝对优雅不起来!因为一旦你在也个组件中引入Dialog,就最少需要额外维护一个visible变量。如果只是额外维护一个变量这也不是不能接受,可是当同样的Dialog组件,即需要在父组件控制它的展示与隐藏,又需要在子组件中控制。


为了演示我们先实现一个MyDialog组件,代码来自ElementPlus的Dialog示例


<script setup lang="ts">
import { computed } from 'vue';
import { ElDialog } from 'element-plus';

const props = defineProps<{
visible: boolean;
title?: string;
}>();

const emits = defineEmits<{
(event: 'update:visible', visible: boolean): void;
(event: 'close'): void;
}>();

const dialogVisible = computed<boolean>({
get() {
return props.visible;
},
set(visible) {
emits('update:visible', visible);
if (!visible) {
emits('close');
}
},
});
</script>

<template>
<ElDialog v-model="dialogVisible" :title="title" width="30%">
<span>This is a message</span>
<template #footer>
<span>
<el-button @click="dialogVisible = false">Cancel</el-button>
<el-button type="primary" @click="dialogVisible = false"> Confirm </el-button>
</span>
</template>
</ElDialog>
</template>

演示场景


就像下面这样:


Kapture 2023-07-07 at 22.44.55.gif


示例代码如下:


<script setup lang="ts">
import { ref } from 'vue';
import { ElButton } from 'element-plus';

import Comp from './components/Comp.vue';
import MyDialog from './components/MyDialog.vue';

const dialogVisible = ref<boolean>(false);
const dialogTitle = ref<string>('');

const handleOpenDialog = () => {
dialogVisible.value = true;
dialogTitle.value = '父组件弹窗';
};

const handleComp1Dialog = () => {
dialogVisible.value = true;
dialogTitle.value = '子组件1弹窗';
};

const handleComp2Dialog = () => {
dialogVisible.value = true;
dialogTitle.value = '子组件2弹窗';
};
</script>

<template>
<div>
<ElButton @click="handleOpenDialog"> 打开弹窗 </ElButton>
<Comp text="子组件1" @submit="handleComp1Dialog"></Comp>
<Comp text="子组件2" @submit="handleComp2Dialog"></Comp>
<MyDialog v-model:visible="dialogVisible" :title="dialogTitle"></MyDialog>
</div>
</template>

这里的MyDialog会被父组件和两个Comp组件都会触发,如果父组件并不关心子组件的onSubmit事件,那么这里的submit在父组件里唯一的作用就是处理Dialog的展示!!!🧐这样真的好吗?不好!


来分析一下,到底哪里不好!


MyDialog本来是submit动作的后续动作,所以理论上应该将MyDialog写在Comp组件中。但是这里为了管理方便,将MyDialog挂在父组件上,子组件通过事件来控制MyDialog


再者,这里的handleComp1DialoghandleComp2Dialog函数除了处理MyDialog外,对于父组件完全没有意义却写在父组件里。


如果这里的Dialog多的情况下,简直就是Dialog地狱啊!🤯


理想的父组件代码应该是这样:


<script setup lang="ts">
import { ElButton } from 'element-plus';

import Comp from './components/Comp.vue';
import MyDialog from './components/MyDialog.vue';

const handleOpenDialog = () => {
// 处理 MyDialog
};
</script>

<template>
<div>
<ElButton @click="handleOpenDialog"> 打开弹窗 </ElButton>
<Comp text="子组件1"></Comp>
<Comp text="子组件2"></Comp>
</div>
</template>

在函数中处理弹窗的相关逻辑才更合理。


解决之道


🤔朕观之,是书之文或不雅,致使人之心有所厌,何得无妙方可解决?


依史记之辞曰:“天下苦Dialog久矣,苦楚深深,望有解脱之道。”于是,诸位贤哲纷纷举起讨伐Dialog之旌旗,终“命令式Dialog”逐渐突破困境之境地。


image.png


没错现在网上对于Dialog的困境,给出的解决方案基本上就“命令式Dialog”看起来比较优雅!这里给出几个网上现有的命令式Dialog实现。


命令式一


codeimg-facebook-shared-image (5).png


吐槽一下~,这种是能在函数中处理弹窗逻辑,但是缺点是MyDialog组件与showMyDialog是两个文件,增加了维护的成本。


命令式二


基于第一种实现的问题,不就是想让MyDialog.vue.js文件合体吗?于是诸位贤者想到了JSX。于是进一步的实现是这样:


codeimg-facebook-shared-image (7).png


嗯,这下完美了!🌝


doutub_img.png


完美?还是要吐槽一下~



  • 如果我的系统中有很多弹窗,难道要给每个弹窗都写成这样吗?

  • 这种兼容JSX的方式,需要引入支持JSX的依赖!

  • 如果工程中不想即用template又用JSX呢?

  • 如果已经存在使用template的弹窗了,难道推翻重写吗?

  • ...


思考


首先承认一点命令式的封装的确可以解决问题,但是现在的封装都存一定的槽点。


如果有一种方式,即保持原来对话框的编写方式不变,又不需要关心JSXtemplate的问题,还保存了命令式封装的特点。这样是不是就完美了?


那真的可以同时做到这些吗?


doutub_img (2).png


如果存在一个这样的Hook可以将状态驱动的Dialog,转换为命令式的Dialog吗,那不就行了?


它来了:useCommandComponent


image.png


父组件这样写:


<script setup lang="ts">
import { ElButton } from 'element-plus';

import { useCommandComponent } from '../../hooks/useCommandComponent';

import Comp from './components/Comp.vue';
import MyDialog from './components/MyDialog.vue';

const myDialog = useCommandComponent(MyDialog);
</script>

<template>
<div>
<ElButton @click="myDialog({ title: '父组件弹窗' })"> 打开弹窗 </ElButton>
<Comp text="子组件1"></Comp>
<Comp text="子组件2"></Comp>
</div>
</template>

Comp组件这样写:


<script setup lang="ts">
import { ElButton } from 'element-plus';

import { useCommandComponent } from '../../../hooks/useCommandComponent';

import MyDialog from './MyDialog.vue';

const myDialog = useCommandComponent(MyDialog);

const props = defineProps<{
text: string;
}>();
</script>

<template>
<div>
<span>{{ props.text }}</span>
<ElButton @click="myDialog({ title: props.text })">提交(需确认)</ElButton>
</div>
</template>

对于MyDialog无需任何改变,保持原来的样子就可以了!


useCommandComponent真的做到了,即保持原来组件的编写方式,又可以实现命令式调用


使用效果:


Kapture 2023-07-07 at 23.44.25.gif


是不是感受到了莫名的舒适?🤨


不过别急😊,要想体验这种极致的舒适,你的Dialog还需要遵循两个约定!


两个约定


如果想要极致舒适的使用useCommandComponent,那么弹窗组件的编写就需要遵循一些约定(其实这些约定应该是弹窗组件的最佳实践)。


约定如下:



  • 弹窗组件的props需要有一个名为visible的属性,用于驱动弹窗的打开和关闭。

  • 弹窗组件需要emit一个close事件,用于弹窗关闭时处理命令式弹窗。


如果你的弹窗组件满足上面两个约定,那么就可以通过useCommandComponent极致舒适的使用了!!



这两项约定虽然不是强制的,但是这确实是最佳实践!不信你去翻所有的UI框看看他们的实现。我一直认为学习和生产中多学习优秀框架的实现思路很重要!



如果不遵循约定


这时候有的同学可能会说:哎嘿,我就不遵循这两项约定呢?我的弹窗就是要标新立异的不用visible属性来控制打开和关闭,我起名为dialogVisible呢?我的弹窗就是没有close事件呢?我的事件是具有业务意义的submitcancel呢?...


doutub_img.png


得得得,如果真的没有遵循上面的两个约定,依然可以舒适的使用useCommandComponent,只不过在我看来没那么极致舒适!虽然不是极致舒适,但也要比其他方案舒适的多!


如果你的弹窗真的没有遵循“两个约定”,那么你可以试试这样做:


<script setup lang="ts">
// ...
const myDialog = useCommandComponent(MyDialog);

const handleDialog = () => {
myDialog({
title: '父组件弹窗',
dialogVisible: true,
onSubmit: () => myDialog.close(),
onCancel: () => myDialog.close(),
});
};
</script>

<template>
<div>
<ElButton @click="handleDialog"> 打开弹窗 </ElButton>
<!--...-->
</div>
</template>

如上,只需要在调用myDialog函数时在props中将驱动弹窗的状态设置为true,在需要关闭弹窗的事件中调用myDialog.close()即可!


这样是不是看着虽然没有上面的极致舒适,但是也还是挺舒适的?


源码与实现


实现思路


对于useCommandComponent的实现思路,依然是命令式封装。相比于上面的那两个实现方式,useCommandComponent是将组件作为参数传入,这样保持组件的编写习惯不变。并且useCommandComponent遵循单一职责原则,只做好组件的挂载和卸载工作,提供足够的兼容性



其实useCommandComponent有点像React中的高阶组件的概念



源码


源码不长,也很好理解!在实现useCommandComponent的时候参考了ElementPlus的MessageBox


源码如下:


import { AppContext, Component, ComponentPublicInstance, createVNode, getCurrentInstance, render, VNode } from 'vue';

export interface Options {
visible?: boolean;
onClose?: () => void;
appendTo?: HTMLElement | string;
[key: string]: unknown;
}

export interface CommandComponent {
(options: Options): VNode;
close: () => void;
}

const getAppendToElement = (props: Options): HTMLElement => {
let appendTo: HTMLElement | null = document.body;
if (props.appendTo) {
if (typeof props.appendTo === 'string') {
appendTo = document.querySelector<HTMLElement>(props.appendTo);
}
if (props.appendTo instanceof HTMLElement) {
appendTo = props.appendTo;
}
if (!(appendTo instanceof HTMLElement)) {
appendTo = document.body;
}
}
return appendTo;
};

const initInstance = <T extends Component>(
Component: T,
props: Options,
container: HTMLElement,
appContext: AppContext | null = null
) =>
{
const vNode = createVNode(Component, props);
vNode.appContext = appContext;
render(vNode, container);

getAppendToElement(props).appendChild(container);
return vNode;
};

export const useCommandComponent = <T extends Component>(Component: T): CommandComponent => {
const appContext = getCurrentInstance()?.appContext;

const container = document.createElement('div');

const close = () => {
render(null, container);
container.parentNode?.removeChild(container);
};

const CommandComponent = (options: Options): VNode => {
if (!Reflect.has(options, 'visible')) {
options.visible = true;
}
if (typeof options.onClose !== 'function') {
options.onClose = close;
} else {
const originOnClose = options.onClose;
options.onClose = () => {
originOnClose();
close();
};
}
const vNode = initInstance<T>(Component, options, container, appContext);
const vm = vNode.component?.proxy as ComponentPublicInstance<Options>;
for (const prop in options) {
if (Reflect.has(options, prop) && !Reflect.has(vm.$props, prop)) {
vm[prop as keyof ComponentPublicInstance] = options[prop];
}
}
return vNode;
};

CommandComponent.close = close;

return CommandComponent;
};

export default useCommandComponent;

除了命令式的封装外,我加入了const appContext = getCurrentInstance()?.appContext;。这样做的目的是,传入的组件在这里其实已经独立于应用的Vue上下文了。为了让组件依然保持和调用方相同的Vue上下文,我这里加入了获取上下文的操作!


基于这个情况,在使用useCommandComponent时需要保证它在setup中被调用,而不是在某个点击事件的处理函数中哦~


最后


如果你觉得useCommandComponent对你在开发中有所帮助,麻烦多点赞评论收藏😊


如果useCommandComponent对你实现某些业务有所启发,麻烦多点赞评论收藏😊


如果...,麻烦多点赞评论收藏😊


如果大家有其他弹窗方案,欢迎留言交流哦!


1632388279060.gif

收起阅读 »

前端业务代码,怎么写测试用例?

web
为什么前端写测试用例困难重重 关于不同测试的种类,网上有很多资料,比如单元、集成、冒烟测试,又比如 TDD BDD 等等,写测试的好处也不用多说,但是就前端来说,写测试用例,特别是针对业务代码测试用例写还是不太常见的事情。我总结的原因有如下几点: 搭建测试环...
继续阅读 »

为什么前端写测试用例困难重重


关于不同测试的种类,网上有很多资料,比如单元、集成、冒烟测试,又比如 TDD BDD 等等,写测试的好处也不用多说,但是就前端来说,写测试用例,特别是针对业务代码测试用例写还是不太常见的事情。我总结的原因有如下几点:



  • 搭建测试环境比较麻烦,什么 jest config、mock 这个、mock 那个,有那个时间写完 mock,都能写完业务代码了

  • 网上能找到的测试教程资料都是简单的 demo,与真实业务场景不匹配,看了这些 demo,还是不知道怎么写测试

  • 网上很难找到合适的模版项目,像 antd 这种都是针对公共 UI 组件的测试用例,对我们写业务逻辑的测试用例没有太大的参考价值

  • 业务需求改动频繁,导致维护测试用例的成本高


我最近在做一个 React Native 项目,想践行 TDD 开发,所以我花了几天时间,梳理了市面上常见的前端测试工具,看了 N 个前端测试实践的文章,最终选择了大道至简,只用下面两个库:



  • jest,不多说,最流行的类 react 项目的测试框架

  • react-test-renderer,用于测试组件 UI,搭配 jest 的快照功能一起使用,让测试 UI 变得不再繁琐


业务代码的测试用例之心法


不要这样写业务代码的测试用例


不要面向实现写测试用例,比如针对某个组件,把每个 props 都写一个测试用例,而 props 很有可能因为业务改动或重构等原因改动,导致我们也要改动相应的测试用例代码,尽管测试用例本身没有错误。


页面跳转、没有任何交互的静态页面、兼容性、


业务代码要怎么写测试


为了平衡开发时间和写测试用例的时间,我认为对于业务代码来说,测试用例不需要面面俱到,什么逻辑都写个测试用例。我们只需要关注用户交互相关的逻辑,具体来说,我会重点关注以下方面:



  • pure 组件的 UI 是否有对应的测试用例

  • 面向功能测试,比如用户输入、点击按钮、加载数据时的 UI、数据为空时的 UI

  • 针对工具函数的各种输入输出测试


写测试用例所需的成本由低到高依次是:

reducer → pure component → business component → DOM testing → e2e

其中 pure component 指的是只有 props 的,只负责渲染的 dummy component。Business compoent 指的是包含 store dispatcher、api fetch、副作用等业务逻辑的业务组件。


程度越靠后,测试的成本越高,所以我们可以花多些精力在测试组件和 reducer 上,少花时间在 DOM 测试和 e2e 测试上。而对于 reducer、pure component、business component 来说,它们的测试用例是相辅相成的,因为 business component 里就包括了 reducer 的使用和 pure component 的渲染,

所以测 business compoent,就等于侧面测到了 reducer 和 pure component。这个测试方法在 Redux 官网也有提到:

完全避免直接测试任何 Redux 代码,将其视为实现细节cn.redux.js.org/usage/writi…


案例:如何测试 pure component


Dumb Component 只用来接收 props 并进行展示,所以它更易于测试,我们只需要 mock 父组件传来的 props 即可,然后搭配 Jest 的 snapshot 快照来判断测试用例是否通过。


比如我们要测试 Tag Component,这个组件的功能很简单,就是展示标签 UI:


Pasted image 20230708154654.png


我们可以用快照测试来记录下这个组件的 UI,如果以后 UI 有改动,这条测试用例就会报错。比如我们现在多了一个业务逻辑,需要每个标签都自动带上 [],好比之前标签展示的是 text,根据业务逻辑,现在标签展示的是 [text]


我们修改 Tag 组件,添加相应的业务逻辑:


Pasted image 20230708155008.png


这时候跑测试用例,可以发现用例报错,而且我们可以报错结果知道组件的 UI 进行了哪些改动,如果这个改动是符合我们期待的,那么直接更新 snapshot 即可:


Pasted image 20230708155111.png


同时,提交代码的时候,这条测试用例对应的 snapshot 也会跟着一起 commit,在 Code Review 阶段我们可以根据 snapshot 来直观的看到组件 UI 进行了哪些改动,美滋滋啊。


如何对 Reducer 进行测试


用 Redux 作为状态管理工具时,一种比较好的编程范式是,让 Store 提供数据,组件只负责渲染数据。组件 UI 可能会因为业务变动而频繁的更改,而 Redux 中的数据逻辑不会经常更改,所以在没有任何像上面那种组件 UI 的快照测试时,可以优先测试 Redux,后期补上组件的快照测试。


工作流:



  1. 先写测试用例,开一个 snapshot

  2. 开启 jest --watch,编写 action 和 reducer 相关代码

  3. 当 snapshot 是我们期待的值,就保存这个 snapshot

  4. 完成测试用例
    作者:Kz
    来源:juejin.cn/post/7253102401452032055
    的编写

收起阅读 »