注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

实现小红书响应式瀑布流

web
前言 瀑布流布局,不管是在pc端还是手机端都很常见,但是我们通常都是列固定。今天来实现一下小红书的响应式瀑布流。后面有仓库地址。 正文 还是先来看看效果 原理: 对每一个item都使用绝对定位,left和top都是0,最后根据容器大小、item的heig...
继续阅读 »

前言


瀑布流布局,不管是在pc端还是手机端都很常见,但是我们通常都是列固定。今天来实现一下小红书的响应式瀑布流。后面有仓库地址。


正文


还是先来看看效果


动画23.gif
原理:



对每一个item都使用绝对定位,left和top都是0,最后根据容器大小、item的height通过计算来确定item的transform值



接下来从易到难来解析一下实现


初始化数据


列表怎么可以没有数据,先来初始化一下数据
code.png


确定列数及列大小


由于是响应式,我们要去监听列表容器的大小变化并记录容器宽度,这样才能做出相应的处理


code.png


根据监听得到的容器大小信息,我们可以确定每行个数和每一个item的宽度
code.png


确定列表中item位置


确定item的位置,那么我们只需要确定transform值就可以了,这也是整个实现的核心。我们还需要解决几个问题



  1. 对还不知道item的高度,怎么确定

  2. 我们希望把新的item放置在最低高度的旧item下方,这样全部渲染完每一列的高度才不会相差很多。


code.png


item放置的原理图,放置在当前最低高度的下面
image.png


更新item高度


当我们第一次运行的时候,每一个item的高度一定都是随机生成的,现在我们要确定item的实际高度。在这里我们还可优化一下,使用懒加载和底部加载,提升性能。这两个在这里就不讲了,不懂的可以去搜一下。


下面代码一共两个作用



  1. 记录容器滚动值,传递给每一个item,用于判断是否加载图片。

  2. 判断是否请求添加数据
    code.png


根据滚动值判断是否加载图片,加载图片后触发父亲更新高度函数
code.png


父亲接受到新的高度并更新高度,然后去重新计算transform值和item高度
code.png


完整代码


仓库地址


结语


感兴趣的可以去试试


作者:卸任
来源:juejin.cn/post/7270160291411886132
收起阅读 »

明明 3 行代码即可轻松实现,Promise 为何非要加塞新方法?

web
给前端以福利,给编程以复利。大家好,我是大家的林语冰。 00. 观前须知 地球人都知道,JS 中的异步编程是 单线程 的,和其他多线程语言的三观一龙一猪。因此,虽然其他语言的异步模式彼此互通有无,但对 JS 并不友好,比如 Actor 模型等。 这并不是说 J...
继续阅读 »

给前端以福利,给编程以复利。大家好,我是大家的林语冰。


00. 观前须知


地球人都知道,JS 中的异步编程是 单线程 的,和其他多线程语言的三观一龙一猪。因此,虽然其他语言的异步模式彼此互通有无,但对 JS 并不友好,比如 Actor 模型等。


这并不是说 JS 被异步社区孤立了,只是因为 JS 天生和多线程八字不合。你知道的,要求 JS 使用多线程,就像要求香菜恐惧症患者吃香菜一样离谱。本质上而言,这是刻在 JS 单线程 DNA 里的先天基因,直接决定了 JS 的“异步性状”。有趣的是,如今 JS 也变异出若干多线程的使用场景,只是比较非主流。


ES6 之后,JS 的异步编程主要基于 Promise 设计,比如人气爆棚的 fetch API 等。因此,最新的 ES2024 功能里,又双叒叕往 Promise 加塞了新型静态方法 Promise.withResolvers(),也就见怪不怪了。


00-promise.png


问题在于,我发现这个新方法居然只要 3 行代码就能实现!奥卡姆剃刀原则告诉我们, 若无必要,勿增实体。那么这个鸡肋的新方法是否违背了奥卡姆剃刀原则呢?我决定先质疑、再质疑。


当然,作为应试教育的漏网之鱼,我很擅长批判性思考,不会被第一印象 PUA。经过三天三夜的刻意练习,机智如我发现新方法果然深藏不露。所以,本期我们就一起来深度学习 Promise 新方法的技术细节。


01. 静态工厂方法


Promise.withResolvers() 源自 tc39/proposal-promise-with-resolvers 提案,是 Promise 类新增的一个 静态工厂方法


静态的意思是,该方法通过 Promise 类调用,而不是通过实例对象调用。工厂的意思是,我们可以使用该方法生成一个 Promise 实例,而无须求助于传统的构造函数 + new 实例化。


01-factory.png


可以看到,这类似于 Promise.resolve() 等语法糖。区别在于,传统构造函数实例化的对象状态可能不太直观,而这里的 promise 显然处于待定状态,此外还“买一送二”,额外附赠一对用于改变 promise 状态的“变态函数” —— resolve()reject()


ES2024 之后,该方法可以作为一道简单的异步笔试题 —— 请你在一杯泡面的时间里,实现一下 Promise.withResolvers()


如果你是我的粉丝,根本不慌,因为新方法的基本原理并不复杂,参考我下面的实现,简单给面试官表演一下就欧了。


02-mock.png


可以看到,这个静态工厂方法的实现难点在于,如何巧妙地将变态函数暴露到外部作用域,其实核心逻辑压缩后有且仅有 3 行代码。


这就引发了本文开头的质疑:新方法是否多此一举?难道负责 JS 标准化的 tc39 委员会也有绩效考核,还是确实存在某些不为人知的极端情况?


02. 技术细节


通过对新方法进行苏格拉底式的“灵魂拷问”和三天三夜的深度学习,我可以很有把握地说,没人比我更懂它。


首先,与传统的构造函数实例化不同,新方法支持无参构造,我们不需要在调用时传递任何参数。


03-new.png


可以看到,构造函数实例化要求传递一个执行器回调,偷懒不传则直接报错,无法顺利实例化。


其次,变态函数的设计更加自由。


04-local.png


可以看到,传统的构造函数中,变态函数能且仅能作为局部变量使用,无法在构造函数外部调用。而新方法同时返回实例及其变态函数,这意味着实例和变态函数处于同一级别的作用域。


那么,这个设计上的小细节有何黑科技呢?


假设我们想要一个 Promise 实例,但尚未知晓异步任务的所有细节,我们期望先将变态函数抽离出来,再根据业务逻辑灵活调用,请问阁下如何应对?


ES2024 之前,我们可以通过 作用域提升 来“曲线救国”,举个栗子:


05-cache.png


可以看到,这种方案的优势在于,诉诸作用域提升,我们不必把所有猫猫放在一个薛定谔的容器里,在构造函数中封装一大坨“代码屎山”;其次,变态函数不被限制在构造函数内部,随时随地任你调用。


该方案的缺陷则在于,某些社区规范鼓励“const 优先”的代码风格,即 const 声明优先,再按需修改为 let 声明。


这里的变态函数被迫使用 let 声明,这意味着存在被愣头青意外重写的隐患,但为了缓存赋值,我们一开始就不能使用 const 声明。从防御式编程的角度,这可能不太鲁棒。


因此,Promise.withResolvers() 应运而生,该静态工厂方法允许我们:



  • 无参构造

  • const 优先

  • 自由变态


03. 设计动机


在某些需要封装 Promise 风格的场景中,新方法还能减少回调函数的嵌套,我把这种代码风格上的优化称为“去回调化”。


举个栗子,我们可以把 Node 中回调风格的 API 转换为 Promise 风格,以 fs 模块为例:


06-hell.png


可以看到,由于使用了传统的构造函数实例化,在封装 readFile() 的时候,我们被迫将其嵌套在构造函数内部。


现在,我们可以使用新方法来“去回调化”。


07-fs.png


可以看到,传统构造函数嵌套的一层回调函数就无了,整体实现更加扁平,减肥成功!


粉丝请注意,很多 Node API 现在也内置了 Promise 版本,现实开发中不需要我们手动封装,开箱即用就欧了。但是这种封装技巧是通用的。


举个栗子,瞄一眼 MDN 电子书搬运过来的一个更复杂的用例,将 Node 可读流转换为异步可迭代对象。


08-stream.png


可以看到,井然有序的代码中透露着一丝无法形容的优雅。我脑补了一下如何使用传统构造函数来实现上述功能,现在还没缓过来......


04. 高潮总结


从历史来看,Promise.withResolvers() 并非首创,bluebird 的 Promise.defer() 或 jQuery 的 $.defer() 等库就提供了同款功能,ES2024 只是换了个名字“新瓶装旧酒”,将其标准化为内置功能。


但是,Promise.withResolvers() 的标准化势在必行,比如 Vite 源码中就自己手动封装了同款功能。


09-vite.png


无独有偶,Axios、Vue、TS、React 等也都在源码内部“反复造轮子”,像这种回头率超高的代码片段我们称之为 boilerplate code(样板代码)。


重复乃编程之大忌,既然大家都要写,不如大家都别写,让 JS 自己写,牺牲小我,成全大家。编程里的 DRY 原则就是让我们不要重复,因为很多 bug 就是重复导致的,而且不好统一管理和维护,《ES6 标准入门教程》科普的 魔术字符串 就是其中一种反模式。


兼容性方面,我也做过临床测试了,主流浏览器广泛支持。


10-can.png


总之,Promise.withResolvers() 通过将样板代码标准化,达到了消除重复的目的,原生实现除了性能更好,是一个性价比较高的静态工厂方法。


参考文献



粉丝互动


本期话题是:你觉得新方法好评几颗星,为什么?你可以在本文下方自由言论,文明科普。


欢迎持续关注“前端俱乐部”,给前端以福利,给编程以复利。


坚持阅读的小伙伴可以给自己点赞!谢谢大家的点赞,掰掰~


26-cat.gif


作者:前端俱乐部
来源:juejin.cn/post/7391745629876469760
收起阅读 »

终于找到一个比较好用的前端国际化方案了

web
在开发Vue/React应用,一直对现有的多语言方案不是很满足,现在终于出了一个比较满意好用的了。VoerkaI18n本节以标准的Nodejs应用程序为例,简要介绍VoerkaI18n国际化框架的基本使用。vue或react应用的使用流程也基本相同,可以参考V...
继续阅读 »

在开发Vue/React应用,一直对现有的多语言方案不是很满足,现在终于出了一个比较满意好用的了。

VoerkaI18n

本节以标准的Nodejs应用程序为例,简要介绍VoerkaI18n国际化框架的基本使用。

vuereact应用的使用流程也基本相同,可以参考Vue集成React集成

myapp
|--package.json
|--index.js

在本项目的所有支持的源码文件中均可以使用t函数对要翻译的文本进行包装,简单而粗暴。

// index.js
console.log(t("中华人民共和国万岁"))
console.log(t("中华人民共和国成立于{}",1949))

t翻译函数是从myapp/languages/index.js文件导出的翻译函数,但是现在myapp/languages还不存在,后续会使用工具自动生成。voerkai18n后续会使用正则表达式对提取要翻译的文本。

第一步:安装命令行工具

安装@voerkai18n/cli到全局。

> npm install -g @voerkai18n/cli
> yarn global add @voerkai18n/cli
> pnpm add -g @voerkai18/cli

第二步:初始化工程

在工程目录中运行voerkai18n init命令进行初始化。

> voerkai18n init 

上述命令会在当前工程目录下创建languages/settings.json文件。如果您的源代码在src子文件夹中,则会创建在src/languages/settings.json

settings.json内容如下:

{
"languages": [
{
"name": "zh",
"title": "zh"
},
{
"name": "en",
"title": "en"
}
],
"defaultLanguage": "zh",
"activeLanguage": "zh",
"namespaces": {}
}

上述命令代表了:

  • 本项目拟支持中文英文两种语言。
  • 默认语言是中文(即在源代码中直接使用中文)
  • 激活语言是中文(代表当前生效的语言)

注意:

  • 可以修改该文件来配置支持的语言、默认语言、激活语言等。可支持的语言可参阅语言代码列表
  • voerkai18n init是可选的,voerkai18n extract也可以实现相同的功能。
  • 一般情况下,您可以手工修改settings.json,如定义名称空间。
  • voerkai18n init仅仅是创建languages文件,并且生成settings.json,因此您也可以自己手工创建。
  • 针对js/typescriptreact/vue等不同的应用,voerkai18n init可以通过不同的参数来配置生成ts文件或js文件。
  • 更多的voerkai18n init命令的使用请查阅这里

第三步:标识翻译内容

接下来在源码文件中,将所有需要翻译的内容使用t翻译函数进行包装,例如下:

import { t } from "./languages"
// 不含插值变量
t("中华人民共和国")
// 位置插值变量
t("中华人民共和国{}","万岁")
t("中华人民共和国成立于{}年,首都{}",1949,"北京")

t翻译函数只是一个普通函数,您需要为之提供执行环境,关于t翻译函数的更多用法见这里

第四步:提取文本

接下来我们使用voerkai18n extract命令来自动扫描工程源码文件中的需要的翻译的文本信息。 voerkai18n extract命令会使用正则表达式来提取t("提取文本")包装的文本。

myapp>voerkai18n extract

执行voerkai18n extract命令后,就会在myapp/languages通过生成translates/default.jsonsettings.json等相关文件。

  • translates/default.json : 该文件就是从当前工程扫描提取出来的需要进行翻译的文本信息。所有需要翻译的文本内容均会收集到该文件中。
  • settings.json: 语言环境的基本配置信息,包含支持的语言、默认语言、激活语言等信息。

最后文件结构如下:

myapp
|-- languages
|-- settings.json // 语言配置文件
|-- translates // 此文件夹是所有需要翻译的内容
|-- default.json // 默认名称空间内容
|-- package.json
|-- index.js

如果略过第一步中的voerkai18n init,也可以使用以下命令来为创建和更新settings.json

myapp>voerkai18n extract -D -lngs zh en de jp -d zh -a zh

以上命令代表:

  • 扫描当前文件夹下所有源码文件,默认是jsjsxhtmlvue文件类型。
  • 支持zhendejp四种语言
  • 默认语言是中文。(指在源码文件中我们直接使用中文即可)
  • 激活语言是中文(即默认切换到中文)
  • -D代表显示扫描调试信息,可以显示从哪些文件提供哪些文本

第五步:人工翻译

接下来就可以分别对language/translates文件夹下的所有JSON文件进行翻译了。每个JSON文件大概如下:

{
"中华人民共和国万岁":{
"en":"<在此编写对应的英文翻译内容>",
"de":"<在此编写对应的德文翻译内容>",
"jp":"<在此编写对应的日文翻译内容>",
"$files":["index.js"] // 记录了该信息是从哪几个文件中提取的
},
"中华人民共和国成立于{}":{
"en":"<在此编写对应的英文翻译内容>",
"de":"<在此编写对应的德文翻译内容>",
"jp":"<在此编写对应的日文翻译内容>",
"$files":["index.js"]
}
}

我们只需要修改该文件翻译对应的语言即可。

重点:如果翻译期间对源文件进行了修改,则只需要重新执行一下voerkai18n extract命令,该命令会进行以下操作:

  • 如果文本内容在源代码中已经删除了,则会自动从翻译清单中删除。
  • 如果文本内容在源代码中已修改了,则会视为新增加的内容。
  • 如果文本内容已经翻译了一部份了,则会保留已翻译的内容。

总之,反复执行voerkai18n extract命令是安全的,不会导致进行了一半的翻译内容丢失,可以放心执行。

第六步:自动翻译

voerkai18n支持通过voerkai18n translate命令来实现调用在线翻译服务进行自动翻译。

>voerkai18n translate --appkey <在百度翻译上申请的密钥> --appid <在百度翻译上申请的appid>

在项目文件夹下执行上面的语句,将会自动调用百度的在线翻译API进行翻译,以现在的翻译水平而言,您只需要进行少量的微调即可。关于voerkai18n translate命令的使用请查阅后续介绍。

第七步:编译语言包

当我们完成myapp/languages/translates下的所有JSON语言文件的翻译后(如果配置了名称空间后,每一个名称空间会对应生成一个文件,详见后续名称空间介绍),接下来需要对翻译后的文件进行编译。

myapp> voerkai18n compile

compile命令根据myapp/languages/translates/*.jsonmyapp/languages/settings.json文件编译生成以下文件:

  |-- languages
|-- settings.json // 语言配置文件
|-- idMap.js // 文本信息id映射表
|-- index.js // 包含该应用作用域下的翻译函数等
|-- storage.js
|-- zh.js // 语言包
|-- en.js
|-- jp.js
|-- de.js
|-- formatters // 自定义扩展格式化器
|-- zh.js
|-- en.js
|-- jp.js
|-- de.js
|-- translates // 此文件夹包含了所有需要翻译的内容
|-- default.json
|-- package.json
|-- index.js

第八步:导入翻译函数

第一步中我们在源文件中直接使用了t翻译函数包装要翻译的文本信息,该t翻译函数就是在编译环节自动生成并声明在myapp/languages/index.js中的。

import { t } from "./languages"   

因此,我们需要在需要进行翻译时导入该函数即可。

但是如果源码文件很多,重次重复导入t函数也是比较麻烦的,所以我们也提供了一个babel/vite等插件来自动导入t函数,可以根据使用场景进行选择。

第九步:切换语言

当需要切换语言时,可以通过调用change方法来切换语言。

import { i18nScope } from "./languages"

// 切换到英文
await i18nScope.change("en")
// 或者VoerkaI18n是一个全局单例,可以直接访问
await VoerkaI18n.change("en")

i18nScope.changeVoerkaI18n.change两者是等价的。

一般可能也需要在语言切换后进行界面更新渲染,可以订阅事件来响应语言切换。

import { i18nScope } from "./languages"

// 切换到英文
i18nScope.on("change",(newLanguage)=>{
// 在此重新渲染界面
...

})
//
VoerkaI18n.on("change",(newLanguage)=>{
// 在此重新渲染界面
...
})

@voerkai18n/vue@voerkai18n/react提供了相对应的插件和库来简化重新界面更新渲染。

第十步:语言包补丁

一般情况下,多语言的工程化过程就结束了,voerkai18n在多语言实践考虑得更加人性化。有没有经常发现这样的情况,当项目上线后,才发现:

  • 翻译有误
  • 客户对某些用语有个人喜好,要求你更改。
  • 临时要增加支持一种语言

一般碰到这种情况,只好重新打包构建工程,重新发布,整个过程繁琐而麻烦。 现在voerkai18n针对此问题提供了完美的解决方案,可以通过服务器来为应用打语言包补丁动态增加语言支持,而不需要重新打包应用和修改应用。

方法如下:

  1. 注册一个默认的语言包加载器函数,用来从服务器加载语言包文件。
import { i18nScope } from "./languages"

i18nScope.registerDefaultLoader(async (language,scope)=>{
return await (await fetch(`/languages/${scope.id}/${language}.json`)).json()
})
  1. 将语言包补丁文件保存在Web服务器上指定的位置/languages/<应用名称>/<语言名称>.json即可。
  2. 当应用启动后会自动从服务器上加载语言补丁包合并,从而实现动为语言包打补丁的功能。
  3. 利用该特性也可以实现动态增加临时支持一种语言的功能

更完整的说明详见动态加载语言包语言包补丁功能介绍。


作者:渔夫正在掘金
来源:juejin.cn/post/7275944565885485116
收起阅读 »

使用Tauri快速搭建桌面项目

web
什么是 Tauri Tauri 是一个跨平台的 GUI 框架,与 Electron 的理念相似。Tauri 的前端部分同样基于 Web 技术,但它的后端则采用了 Rust 语言。Tauri 可以创建体积更小、运行更快且更加安全的跨平台桌面应用。 与 Elect...
继续阅读 »

什么是 Tauri


Tauri 是一个跨平台的 GUI 框架,与 Electron 的理念相似。Tauri 的前端部分同样基于 Web 技术,但它的后端则采用了 Rust 语言。Tauri 可以创建体积更小、运行更快且更加安全的跨平台桌面应用。


Electron 不同,Tauri 并没有内置 Chromium,因此打包后的应用体积要比 Electron 小很多,启动速度更快,内存和 CPU 占用率也更低。


然而,由于没有内置 Chromium,Tauri 使用系统原生的 WebView 来渲染网页,这可能导致不同系统之间的页面表现存在差异。同时,Tauri 的后端需要使用 Rust 进行开发,这对前端开发人员来说可能会有一定的上手成本。


好在 Tauri 已经为我们封装了大部分 API,即使不懂 Rust,也可以开发出一款简单的应用。


预先准备


我们以 macOS 为例:


1. 首先安装 Xcode 命令行工具


在终端中执行以下命令:


xcode-select --install

如果已经安装过 Xcode 命令行工具,则可以直接进行下一步。


2. 安装 Rust


在 macOS 上安装 Rust,请打开终端并输入以下命令:


curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

安装成功后,终端将显示以下内容:


Rust is installed now. Great!


请确保重新启动终端以使更改生效。


快速开始


创建项目


Tauri 官方提供了多种项目模板,可以快速搭建项目:


# pnpm
pnpm create tauri-app

# npm
npm create tauri-app

# yarn
yarn create tauri-app

按照提示选择自己喜欢的模板。


这里我们选择 react 开发前端页面。


一路回车后,打开项目文件夹。执行安装依赖命令:pnpm i。依赖安装完成后,执行 pnpm tauri dev 命令启动项目。这时便会启动一个应用,如下图所示:


image.png


开发


Tauri 的开发非常容易上手,我们先来看下项目文件结构:


项目文件结构


是不是和 Vite 的目录结构一样?


没错,这就是一个常规的 Vite 目录结构,唯一的区别是增加了一个 src-tauri 文件夹,这里面是 Rust 部分的代码,也就是后端代码。


打包


首先,我们需要修改默认的包标识符,位置在 src-tauri > tauri.conf.json > tauri > bundle > identifier


修改包标识符


这里我们随便填写一个标识符 com.example.app,保存,然后执行命令:pnpm tauri build 就可以正常打包了。


tauri.conf.json 文件是我们的应用配置文件,包含了应用的基本信息。


打包完成后,就可以在 tauri-app/src-tauri/target/release/bundle 目录下找到我们的应用。


打包后的应用


现在我们只构建了 macOS 下的应用。


应用界面


打开之后就可以看到我们的应用了。


参考文档



作者:虾卡拉卡
来源:juejin.cn/post/7388842078798823433
收起阅读 »

学会Grid之后,我觉得再也没有我搞不定的布局了

web
说到布局很多人的感觉应该都是恐惧,为此很多人都背过一些很经典的布局方案,例如:圣杯布局、双飞翼布局等非常耳熟的名词; 为了实现这些布局我们有很多种实现方案,例如:table布局、float布局、定位布局等,当然现在比较流行的肯定是flex布局; flex布局属...
继续阅读 »

说到布局很多人的感觉应该都是恐惧,为此很多人都背过一些很经典的布局方案,例如:圣杯布局双飞翼布局等非常耳熟的名词;


为了实现这些布局我们有很多种实现方案,例如:table布局float布局定位布局等,当然现在比较流行的肯定是flex布局


flex布局属于弹性布局,所谓弹性也可以理解为响应式布局,而同为响应式布局的还有Grid布局


Grid布局是一种二维布局,可以理解为flex布局的升级版,它的出现让我们在布局方面有了更多的选择,废话不多说,下面开始全程高能;



本篇不会过多介绍grid的基础内容,更多的是一些布局的实现方案和一些小技巧;



常见布局


所谓的常见布局只是我们在日常开发中经常会遇到的布局,例如:圣杯布局双飞翼布局这种名词我个人觉得不用太过于去在意;


因为这类布局最后的解释都会变成几行几列,内容在哪一行哪一列,而这些就非常直观的对标了grid的特性;


接下来我们来一起看看一些非常常见的布局,并且用grid来实现;


1. 顶部 + 内容


image.png


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}

body {
display: grid;
grid-template-rows: 60px 1fr;
height: 100vh;
}

.header {
background-color: #039BE5;
}

.content {
background-color: #4FC3F7;
}

.header,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="content">Content</div>
</body>
</html>


2. 顶部 + 内容 + 底部


image.png


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}

body {
display: grid;
grid-template-rows: 60px 1fr 60px;
height: 100vh;
}

.header {
background-color: #039BE5;
}

.content {
background-color: #4FC3F7;
}

.footer {
background-color: #039BE5;
}

.header,
.content,
.footer {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="content">Content</div>
<div class="footer">Footer</div>
</body>
</html>


这里示例和上面的示例唯一的区别就是多了一个footer,但是我们可以看到代码并没有多少变化,这就是grid的强大之处;


可以看码上掘金的效果,这里的内容区域是单独滚动的,从而实现了headerfooter固定,内容区域滚动的效果;


实现这个效果也非常简单,只需要在content上加上overflow: auto即可;




3. 左侧 + 内容


image.png


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}

body {
display: grid;
grid-template-columns: 240px 1fr;
height: 100vh;
}

.left {
background-color: #039BE5;
}

.content {
background-color: #4FC3F7;
}

.left,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}


</style>
</head>
<body>
<div class="left">Left</div>
<div class="content">Content</div>
</body>
</html>


这个示例效果其实和第一个是类似的,只不过是把grid-template-rows换成了grid-template-columns,这里就不提供码上掘金的示例了;



4. 顶部 + 左侧 + 内容


image.png


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}

body {
display: grid;
grid-template-rows: 60px 1fr;
grid-template-columns: 240px 1fr;
height: 100vh;
}

.header {
grid-column: 1 / 3;
background-color: #039BE5;
}

.left {
background-color: #4FC3F7;
}

.content {
background-color: #99CCFF;
}

.header,
.left,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}

</style>
</head>
<body>
<div class="header">Header</div>
<div class="left">Left</div>
<div class="content">Content</div>
</body>
</html>


这个示例不同点在于header占据了两列,这里我们可以使用grid-column来实现,grid-column的值是start / end,例如:1 / 3表示从第一列到第三列;


如果确定这一列是占满整行的,那么我们可以使用1 / -1来表示,这样如果后续变成顶部 + 左侧 + 内容 + 右侧的布局,那么header就不需要修改了;



5. 顶部 + 左侧 + 内容 + 底部


image.png


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}

body {
display: grid;
grid-template-areas:
"header header"
"left content"
"left footer";
grid-template-rows: 60px 1fr 60px;
grid-template-columns: 240px 1fr;
height: 100vh;
}

.header {
grid-area: header;
background-color: #039BE5;
}

.left {
grid-area: left;
background-color: #4FC3F7;
}

.content {
grid-area: content;
background-color: #99CCFF;
}

.footer {
grid-area: footer;
background-color: #6699CC;
}

.header,
.left,
.content,
.footer {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="left">Left</div>
<div class="content">Content</div>
<div class="footer">Footer</div>
</body>
</html>


这个示例的小技巧是使用了grid-template-areas,使用这个属性可以让我们通过代码来直观的看到布局的样式;


这里的值是一个字符串,每一行代表一行,每个字符代表一列,例如:"header header"表示第一行的两列都是header,这里的header是我们自己定义的,可以是任意值;


定义好了之后就可以在对应的元素上使用grid-area来指定对应的区域,这里的值就是我们在grid-template-areas中定义的值;





码上掘金中的效果可以看到,左侧的菜单和内容区域都是单独滚动的,这里的实现方式和第二个示例是一样的,只需要需要滚动的元素上加上overflow: auto即可;



响应式布局


响应式布局指的是页面的布局会随着屏幕的大小而变化,这里的变化可以是内容区域大小可以自动调整,也可以是页面布局随着屏幕大小进行自动调整;


这里我就用掘金的页面来举例,这里只提供一个思路,所以不会像上面那样提供那么多示例;


1. 基础布局实现


移动端布局


image.png



以移动端的效果开始,掘金的移动端的布局就是上面的效果,这里我简单的将页面分为了三个部分,分别是headernavigationcontent


注:这里不是要100%还原掘金的页面,只是为了演示grid布局,具体页面结构和最后实现的效果会有非常大的差异,这里只会实现一些基础的布局;



<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}

body {
display: grid;
grid-template-areas:
"header"
"navigation"
"content";
grid-template-rows: 60px 48px 1fr;
height: 100vh;
}

.header {
grid-area: header;
background-color: #039BE5;
}

.navigation {
grid-area: navigation;
background-color: #4FC3F7;
}

.content {
grid-area: content;
background-color: #99CCFF;
}


.header,
.navigation,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}

</style>
</head>
<body>
<div class="header">Header</div>
<div class="navigation">Navigation</div>
<div class="content">Content</div>
</body>
</html>

iPad布局


image.png



这里是需要借助媒体查询来实现的,在媒体查询中只需要调整一下grid-template-rowsgrid-template-columns的值即可;


由于这里的效果是上面一个的延伸,为了阅读体验会移除上面相关的css代码,只保留需要修改的代码;



<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>

.right {
display: none;
background-color: #6699CC;
}

@media (min-width: 1000px) {
body {
grid-template-areas:
"header header"
"navigation navigation"
"content right";
grid-template-columns: 1fr 260px;
}

.right {
grid-area: right;

display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="navigation">Navigation</div>
<div class="content">Content</div>
<div class="right">Right</div>
</body>
</html>

PC端布局


image.png



和上面处理方式相同,由于Navigation移动到了左侧,所以还要额外的修改一下grid-template-areas的值;


这里就可以体现grid的强大之处了,我们可以简单的修改grid-template-areas就可以实现一个完全不同的布局,而且代码量非常少;


为了居中显示内容,我们需要在左右两侧加上一些空白区域,可以简单的使用.来实现,这里的.表示一个空白区域;


由于内容的宽度基本上是固定的,所以留白区域简单的使用1fr进行占位即可,这样就可以平均的分配剩余的空间;



<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
@media (min-width: 1220px) {
body {
grid-template-areas:
"header header header header header"
". navigation content right .";
grid-template-columns: 1fr 180px minmax(0, 720px) 260px 1fr;
grid-template-rows: 60px 1fr;
}
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="navigation">Navigation</div>
<div class="content">Content</div>
<div class="right">Right</div>
</body>
</html>

完善一些细节


QQ录屏20231210000552.gif



最终的布局大概就是上图这样,这里主要处理的各个版块的间距和响应式内容区域的大小,这里的处理方式主要是使用column-gap和一个空的区域进行占位来实现的;


这里的column-gap表示列与列之间的间距,值可以是pxemrem等基本的长度属性值,也可以使用计算函数,但是不能使用弹性值fr


空区域进行占位留间距其实我并不推荐,这里只是演示grid布局可以实现的一些功能,具体的实现方式还是要根据实际情况来定,这里我更推荐使用margin来实现;


完整代码如下:



<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}

body {
display: grid;
grid-template-areas:
"header header header"
"navigation navigation navigation"
". . ."
". content .";
grid-template-columns: 1fr minmax(0, 720px) 1fr;
grid-template-rows: 60px 48px 10px 1fr;
column-gap: 10px;
height: 100vh;
}

.header {
grid-area: header;
background-color: #039BE5;
}

.navigation {
grid-area: navigation;
background-color: #4FC3F7;
}

.content {
grid-area: content;
background-color: #99CCFF;
}

.right {
display: none;
background-color: #6699CC;
}

@media (min-width: 1000px) {
body {
grid-template-areas:
"header header header header"
"navigation navigation navigation navigation"
". . . ."
". content right .";
grid-template-columns: 1fr minmax(0, 720px) 260px 1fr;
}

.right {
grid-area: right;

display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
}

@media (min-width: 1220px) {
body {
grid-template-areas:
"header header header header header"
". . . . ."
". navigation content right .";
grid-template-columns: 1fr 180px minmax(0, 720px) 260px 1fr;
grid-template-rows: 60px 10px 1fr;
}
}

.header,
.navigation,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}

</style>
</head>
<body>
<div class="header">Header</div>
<div class="navigation">Navigation</div>
<div class="content">Content</div>
<div class="right">Right</div>
</body>
</html>

简单复刻版




码上掘金上的效果来说已经完成了大部分的布局和一些效果,目前来说就是还差一些交互,还有一些细节上的处理,感兴趣的同学可以自行完善;



异型布局


异性布局指的是页面中的元素不是按照常规的流式布局进行排版,又或者说不规则的布局,这里我简单的列出几个布局,来看看grid是如何实现的;


1. 照片墙


image.png


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
background: #f2f3f5;
overflow: auto;
}

body {
display: grid;
grid-template-columns: repeat(12, 100px);
grid-auto-rows: 100px;
place-content: center;
gap: 6px;
height: 100vh;
}

.photo-item {
width: 200px;
height: 200px;
clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}

</style>
</head>
<body>

</body>
<script>
function randomColor() {
return '#' + Math.random().toString(16).substr(-6);
}

let row = 1;
let col = 1;
for (let i = 0; i < 28; i++) {
const div = document.createElement('div');
div.className = 'photo-item';
div.style.backgroundColor = randomColor();
div.style.gridRow = `${row} / ${row + 2}`;
div.style.gridColumn = `${col} / ${col + 2}`;

document.body.appendChild(div);
col += 2;
if (col > 11) {
row += 1;
col = row % 2 === 0 ? 2 : 1;
}
}
</script>
</html>


这是一个非常简单的照片墙效果,如果不使用grid的话,我们大概率是会使用定位去实现这个效果,但是换成grid的话就非常简单了;


而且代码量是非常少的,这里就不提供码上掘金的 demo 了,感兴趣的同学可以将代码复制下来自行查看效果;



2. 漫画效果


image.png




在漫画中有很多类似这种不规则的漫画框,如果使用定位的话,那么代码量会非常大,而且还需要计算一些位置,使用grid的话就非常简单了;


可以看到这里还有一个气泡文字显示的效果,按照页面书写顺序,气泡是不会显示的,这里我们可以使用z-index来实现,这里的z-index值越大,元素就越靠前;


而且气泡文字效果也是通过grid来进行排版的,并没有使用其他的布局来实现,代码量也并不多,感兴趣的同学可以自行查看;



3. 画报效果


image.png




在一个画报中,我们经常会看到文字和图片混合排版的效果,由于这里直接使用的是渐变的背景,而且我的文字都是随意进行排列的,没有什么规律,所以看起来会比较混乱;


在画报效果中看似文字排版非常混乱不规则,但是实际上设计师在设计的时候也是会划分区域的,当然用定位也是没问题的,但是使用grid的话就会简单很多;


我这里将页面划分为12 * 12区域的网格,然后依次对不同的元素进行单独排列和样式的设置;



流式布局


流式布局指的是页面的内容会随着屏幕的大小而变化,流式布局也可以理解为响应式布局;


但是不同于响应式布局的是,流式布局的布局不会像响应式布局那样发生变化,只是内容会随着轴进行流动;


通常这种指的是grid-template-columns: repeat(auto-fit, minmax(0, 1fr))这种;


直接看效果:


QQ录屏20231210222012.gif




这里有两个关键字,一个是auto-fit,还有一个是auto-fill,在行为上它们是相同的,不同的是它们在网格创建的不同,



image.png



就像上面图中看到的一样,使用auto-fit会将空的网格进行折叠,可以看到他们的结束colum的数字都是6;


像我们上面的实例中不会出现这个问题,因为我们使用了响应式单位fr,只有使用固定单位才会出现这个现象;


感兴趣的同学可以将minmax(200px, 1fr)换成200px尝试;



对比 Flex 布局


在我上面介绍了这么多的布局场景和案例,其实可以很明显的发现一件事,那就是我使用grid进行的布局基本上都是大框架;


当然上面也有一些布局使用flex也是可以实现的,但是我们再换个思路,除了flex可以做到上面的一些布局,float布局、table布局、定位布局其实也都能实现;


不同的是float布局、table布局、定位布局基本上都是一些hack的方案,就拿table布局来说,table本身就是一个html标签,作用就是用来绘制表格,被拿来当做布局的一种方案也是迫不得已;


web布局发展到现在的我们有了正儿八经可以布局的方案flex,为什么又要出一个grid呢?


grid的出现绝对不是用来替代flex的,在我上面的实现的一些布局案例中,也可以看到我还会使用flex


我个人理解的是使用grid进行主体的大框架的搭建,flex作为一些小组件的布局控制,两者搭配使用;


flex能实现一些grid不好实现的布局,同样grid也可以实现flex实现困难的布局;


本身它们的定位就不痛,flex作为一维布局的首选,grid定位就是比flex高一个维度,它的定位是二维布局,所以他们之间没有必要进行对比,合理使用就好;


总结


上面介绍的这么多基于grid布局实现的布局方案,足以看出grid布局的强大;


grid布局的体系非常庞大,本文只是梳理出一些常见的布局场景,通过grid布局去实现这些布局,来体会grid带来的便利;


可能需要完全理解我上面的全部示例需要对grid有一定的了解才可以,但是都看到这里了,不妨去深挖一下;


grid布局作为一项强大的布局技术,有望在未来继续发展,除了我上面说到的布局,grid还有很多小技巧来实现非常多的布局场景;


碍于我的见识和文笔的限制,我这次介绍grid肯定是有很多不足的,但是还是希望这篇文章能为你对于布局相关能有新的认识;


作者:田八
来源:juejin.cn/post/7310423470546354239
收起阅读 »

我为什么选择Next.js+Supabase做全栈开发

web
作为一名前端工程师,选择合适的技术栈对项目的成功至关重要,我最近一个星期尝试了下这两个技术栈的组合,大概在一个星期就写了一个小 SAAS,总共 10 多个页面。在本文中,我将分享为什么我选择Next.js 14和Supabase作为全栈开发的首选组合,并通过最...
继续阅读 »


作为一名前端工程师,选择合适的技术栈对项目的成功至关重要,我最近一个星期尝试了下这两个技术栈的组合,大概在一个星期就写了一个小 SAAS,总共 10 多个页面。在本文中,我将分享为什么我选择Next.js 14和Supabase作为全栈开发的首选组合,并通过最新的代码示例和比较数据,直观地展示这个选择带来的诸多优势。


Next.js 14: 现代React应用的革新框架


默认服务器组件的优势



Next.js 14默认使用服务器组件,这对于提升性能和开发体验至关重要。


例如,一个简单的服务器组件如下:


// app/page.js
async function getData() {
const res = await fetch('https://api.example.com/data')
return res.json()
}

export default async function Home() {
const data = await getData()
return <div>Welcome to {data.name}div>
}

在这个例子中,Home组件是一个异步的服务器组件,它可以直接进行数据获取,而无需使用useEffect或getServerSideProps。


App Router: 更强大的路由系统


Next.js 14采用了新的App Router,提供了更灵活和直观的路由方式:


app/
page.js // 对应路由 /
about/
page.js // 对应路由 /about
posts/
[id]/
page.js // 对应路由 /posts/1, /posts/2, 等

Server Actions: 无需API路由的表单处理



Next.js 14引入了Server Actions,允许我们直接在服务器上处理表单提交,无需单独的API路由:


// app/form.js
export default function Form() {
async function handleSubmit(formData) {
'use server'
// 在服务器上处理表单数据
const name = formData.get('name')
// ...处理逻辑
}

return (
<form action={handleSubmit}>
<input type="text" name="name" />
<button type="submit">Submitbutton>

form>
)
}

这个能力好用到哭,不用再写API路由了,直接在页面上处理表单提交。代码简单了不止一点点。


Supabase: 开源Firebase替代品的崛起


数据库即服务的便利性


Supabase提供了PostgreSQL数据库即服务,使用起来非常简单:


import { createClient } from '@supabase/supabase-js'

const supabase = createClient('YOUR_SUPABASE_URL', 'YOUR_SUPABASE_KEY')

// 插入数据
const { data, error } = await supabase
.from('users')
.insert({ name: 'John', email: 'john@example.com' })

实时功能的强大支持


Supabase的实时订阅功能让实现实时更新变得轻而易举:


import { useEffect } from 'react'
import { createClient } from '@supabase/supabase-js'

const supabase = createClient('YOUR_SUPABASE_URL', 'YOUR_SUPABASE_KEY')

function RealtimeData() {
useEffect(() => {
const channel = supabase
.channel('*')
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'users' }, payload => {
console.log('New user:', payload.new)
})
.subscribe()

return () => {
supabase.removeChannel(channel)
}
}, [])

return <div>Listening for new users...div>
}

身份认证和授权的简化


Supabase内置的身份认证系统大大简化了用户管理:


const { data, error } = await supabase.auth.signUp({
email: 'example@email.com',
password: 'example-password',
})

Next.js 14 + Supabase: 完美的全栈开发组合


开发效率的显著提升


结合Next.js 14和Supabase,我们可以快速构建全功能的Web应用。以下是一个简单的例子,展示了如何在Next.js 14的服务器组件中使用Supabase:


// app/posts/page.js
import { createClient } from '@supabase/supabase-js'

const supabase = createClient('YOUR_SUPABASE_URL', 'YOUR_SUPABASE_KEY')

export default async function Posts() {
const { data: posts } = await supabase.from('posts').select('*')

return (
<div>
{posts.map(post => (
<div key={post.id}>{post.title}div>

))}
div>
)
}

这个例子展示了Next.js 14服务器组件如何与Supabase无缝集成,直接在服务器端获取数据并渲染。


与其他技术栈的对比


为了更直观地展示Next.js 14+Supabase的优势,我们来看一个更新后的比较表格:


特性Next.js 14+SupabaseMERN StackFirebaseDjango
默认服务器组件N/A
App Router
Server Actions
实时数据库需配置需配置
SQL支持✅ (PostgreSQL)❌ (默认NoSQL)❌ (NoSQL)
身份认证需配置
学习曲线
全栈JavaScript
开源

选型优势的直观感受



  1. 开发速度:使用Next.js 14+Supabase,你可以在几小时内搭建起一个包含用户认证、数据库操作和实时更新的全栈应用。

  2. 代码量减少:得益于Next.js 14的服务器组件和Supabase的简洁API,代码量可以减少40%-60%。

  3. 性能提升:通过Next.js 14的默认服务器组件和自动代码分割,页面加载速度可以提升40%-70%。

  4. 学习成本:虽然新概念(如服务器组件)需要一定学习时间,但整体学习曲线比传统全栈开发更平缓,2-3周即可上手。

  5. 维护简化:单一语言(TypeScript)贯穿全栈,加上Next.js的文件约定和Supabase的声明式API,大大减少了维护的复杂度。

  6. 可扩展性:Supabase基于PostgreSQL,为未来的扩展提供了更多可能性,而Next.js的渐进式框架特性也允许逐步采用高级功能。


一些想法


Next.js 14和Supabase是现代全栈开发的最佳选择,它们的结合提供了前所未有的开发体验和性能优势。如果你正在寻找一个全栈开发的新方向,不妨试试Next.js 14和Supabase,相信你会爱上这个组合。而且 supabase 学了也很划算,即便你想做 react native,Flutter,他都可以作为你坚实的后端。




作者:brzhang
来源:juejin.cn/post/7389925676520226825
收起阅读 »

大厂都在”偷偷“用语义化标签,你却还在div?

web
引言 在我们日常浏览网页的时候,通常会看到各种各样的内容,如文字、图片、视频等。这些内容背后都有一个共同的语言,那就是HTML(超文本标记语言)。HTML是构建网页的基础,它就像建筑物的框架,决定了网页的基本结构和布局。 然而,仅仅有结构是不够的。如果网页只是...
继续阅读 »

引言


在我们日常浏览网页的时候,通常会看到各种各样的内容,如文字、图片、视频等。这些内容背后都有一个共同的语言,那就是HTML(超文本标记语言)。HTML是构建网页的基础,它就像建筑物的框架,决定了网页的基本结构和布局。


然而,仅仅有结构是不够的。如果网页只是简单地用一些基础标签堆砌而成,那么浏览器、搜索引擎甚至我们自己在后期维护时,都会感到非常吃力。


这时候,HTML的语义化标签就显得尤为重要。语义化标签不仅能使网页结构更加清晰,还能帮助搜索引擎更好地理解和索引网页内容。


什么是HTML语义化标签?


HTML语义化标签,就是那些带有特定含义的标签,它们告诉浏览器和搜索引擎,每一部分内容是什么。这就好比是给每个内容部分都贴上了一个清晰的标签,让所有人都能明白这个部分是用来做什么的


举个例子,假设你在看一本书,书的封面、目录、章节标题等都是明确标示出来的,这样你就能快速找到自己想看的部分。同样,HTML语义化标签也是为了让网页的内容更加明晰易懂。比如:



  • <header> 标签用来定义网页的头部内容,通常包含导航栏、Logo等信息;

  • <nav> 标签专门用于定义导航链接,这样搜索引擎就能更好地理解网站的结构;

  • <article> 标签用于定义独立的内容,比如一篇新闻文章或者博客帖子

  • ……


通过使用这些语义化标签,不仅提高了网页的可读性维护性,还能帮助搜索引擎更准确地抓取和排名内容,从而提升网站的SEO效果


为什么使用语义化标签?


使用HTML语义化标签有很多好处,它们不仅能让代码更清晰,还能带来实际的效果和便利。



  1. 提高网页的可读性和结构化



    • 语义化标签让HTML代码更加直观,其他开发者在阅读和维护代码时,可以快速理解每个部分的作用。这有助于团队合作和项目的长期维护。



  2. 有助于搜索引擎优化(SEO)



    • 搜索引擎通过爬虫程序抓取网页内容,并根据网页的结构和内容进行索引。使用语义化标签可以帮助搜索引擎更好地理解网页的层次和重点内容,从而提升网站在搜索结果中的排名。



  3. 无障碍支持



    • 语义化标签对辅助技术(如屏幕阅读器)非常重要,它们可以帮助视障用户更好地理解网页内容。例如,使用<nav>标签可以让屏幕阅读器快速跳转到导航部分。

    • 还记得浏览器内置的“沉浸阅读器”吗?它们也大多基于语义化标签提供服务。例如掘金的文章都是用<article>标签包裹的,所以你可以在掘金文章页面启用沉浸阅读器,而且精准的获取了文章的主体内容。

    • image.png




另外不得不说,目前苹果对语义化标签的使用是最炉火纯青的。怪不得都说苹果优雅,现在算是在前端上见识到了这个细节怪……


image.png


其实还有很多大厂都在使用,但都是偷偷地使用。它们没有全局使用语义化标签,而是在特定的关键位置使用语义化标签来 “谄媚” 一下搜索引擎或浏览器提供的无障碍功能


所以我相信很多人还是非常支持div一把梭的,只要老板不限制,想怎么做就怎么做。不过如果你也能学习大厂,在漫天div下加一点语义化标签的小巧思,骗过搜索引擎和浏览器,这不是很香吗?


所以,本文着重介绍那些搜索引擎和浏览器有特别支持的语义化标签,搞定他们就搞定了一大半!


常用的语义化标签


搜索引擎钟爱的语义化标签


搜索引擎(如Google、Bing等)特别关注某些HTML语义化标签,因为这些标签能够帮助它们更好地理解网页的结构和内容,从而改进搜索结果的质量。


以下是一些被搜索引擎特别关注的语义化标签:



  1. <header>



    • 搜索引擎会识别<header>标签中的内容,通常包括页面的标题导航链接等,有助于理解网页的整体结构和主要部分



  2. <nav>



    • <nav>标签标示出导航链接区域,帮助搜索引擎理解网站的链接结构和页面之间的关系,有助于内部链接的优化


    image.png

  3. <article>



    • <article>标签表示独立的内容块,如新闻文章博客帖子等。搜索引擎会特别关注这些标签,认为其包含主要的内容



  4. <footer>



    • <footer>标签包含页脚内容,通常包括版权信息联系信息等,搜索引擎会利用这些信息来补充网页的相关性数据。



  5. <main>



    • <main>标签标示出页面的主要内容区域,帮助搜索引擎更快地定位和抓取主要内容,而忽略导航栏、页脚等次要部分。




浏览器的无障碍功能


现代浏览器具备许多无障碍功能(accessibility features),这些功能可以帮助有特殊需求的用户更好地浏览网页。


以下是一些关键的无障碍功能:



  1. 屏幕阅读器支持



    • 屏幕阅读器是一种软件工具,可以将网页内容转换为语音或盲文,帮助视障用户浏览网页。语义化标签可以极大地提升屏幕阅读器的效率和准确性。例如,<nav>标签可以让用户快速跳转到导航部分,而<article>标签则可以帮助用户找到主要的文章内容



  2. 键盘导航



    • 无障碍浏览器允许用户通过键盘进行导航,语义化标签如<header><nav><main><footer>等,可以帮助键盘用户快速跳转到页面的不同部分,提高浏览效率


    <header>
    <h1>网站标题</h1>
    </header>
    <nav>
    <!-- 导航内容 -->
    </nav>
    <main>
    <h2>主要内容标题</h2>
    <p>这是主要内容区域。</p>
    </main>
    <footer>
    <p>版权所有 &copy; 2024 公司名称</p>
    </footer>


  3. 高对比度模式



    • 一些浏览器提供高对比度模式,帮助视觉有障碍的用户更容易阅读内容。使用正确的语义化标签和良好的结构,可以确保在高对比度模式下内容的可读性和可访问性。


    <section>
    <h2>章节标题</h2>
    <article>
    <h3>文章标题</h3>
    <p>文章内容...</p>
    </article>
    <aside>
    <h3>附加内容</h3>
    <p>例如广告或链接...</p>
    </aside>
    </section>


  4. ARIA(可访问性富互联网应用)标签



    • 虽然ARIA标签不是HTML语义化标签的一部分,但它们可以补充HTML标签,提供更多的无障碍信息。例如,aria-labelaria-labelledby等属性可以为非文本元素提供文本描述,帮助辅助技术更好地解释内容。


    <button aria-label="关闭">X</button>
    <div role="dialog" aria-labelledby="dialogTitle" aria-describedby="dialogDescription">
    <h2 id="dialogTitle">对话框标题</h2>
    <p id="dialogDescription">对话框内容描述。</p>
    </div>



语义化标签的实际应用


为了更好地理解语义化标签的使用方法,让我们通过一个具体的案例来展示它们的实际应用。


假设我们要创建一个简单的博客页面,包含标题、导航栏、文章内容、侧边栏和页脚。下面是一个示例代码:


<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>我的博客</title>
<style>
body { font-family: Arial, sans-serif; }
header, nav, article, aside, footer { margin: 20px; padding: 10px; border: 1px solid #ccc; }
nav ul { list-style-type: none; padding: 0; }
nav ul li { display: inline; margin-right: 10px; }
aside { float: right; width: 30%; }
article { float: left; width: 65%; }
</style>
</head>
<body>
<header>
<h1>我的博客</h1>
<nav>
<ul>
<li><a href="#home">首页</a></li>
<li><a href="#about">关于我</a></li>
<li><a href="#contact">联系我</a></li>
</ul>
</nav>
</header>
<section>
<article>
<h2>文章标题</h2>
<p>这里是文章的正文内容。</p>
</article>
<aside>
<h2>侧边栏</h2>
<p>这里是一些附加内容,比如广告或链接。</p>
</aside>
</section>
<footer>
<p>版权所有 Dikkoo; 2024 我的博客</p>
</footer>
</body>
</html>

回顾一下


在这个案例中,我们使用了多个语义化标签来组织页面内容:



  • <header> 包含网站的标题和导航栏。

  • <nav> 用于定义导航链接区域。

  • <section> 用于分隔主要内容区域,包含文章和侧边栏。

  • <article> 定义了独立的文章内容。

  • <aside> 包含附加内容,如侧边栏。

  • <footer> 包含页面的底部信息。


怎样合理运用语义化标签?


为了充分发挥HTML语义化标签的优势,以下是一些最佳实践建议



  1. 规划页面结构,提前设计



    • 在编写HTML之前,先绘制页面的结构图,明确各部分的功能和内容。根据设计选择合适的语义化标签,这样可以避免在编写过程中频繁修改结构。



  2. 保持代码简洁



    • 语义化标签旨在使代码更清晰,因此应尽量避免过度嵌套标签使用多余的标签。使用语义化标签替代大量的<div><span>,使代码更加简洁和易读。



  3. 合理嵌套标签



    • 语义化标签应按照其语义进行嵌套。例如,将<nav>放在<header>内,表示导航是头部的一部分;将<section><article>合理地嵌套在一起,表示内容的层次结构。



  4. 遵循HTML规范



    • 确保使用语义化标签时符合HTML规范,不要滥用标签。例如,不要将<header>标签用在每个段落中,而应仅用于页面或章节的头部。




作者:Dikkoo
来源:juejin.cn/post/7388056946121113637
收起阅读 »

怎样实现每次页面打开时都清除本页缓存?

web
"```markdown 每次页面加载时清除本页缓存可以通过多种方式实现,具体方法取决于需要的粒度和数据类型。以下是一些常见的技术: 使用meta标签(HTML): <meta http-equiv=\"cache-control\" content=\...
继续阅读 »

"```markdown
每次页面加载时清除本页缓存可以通过多种方式实现,具体方法取决于需要的粒度和数据类型。以下是一些常见的技术:


使用meta标签(HTML):


<meta http-equiv=\"cache-control\" content=\"no-cache, no-store, must-revalidate\">
<meta http-equiv=\"pragma\" content=\"no-cache\">
<meta http-equiv=\"expires\" content=\"0\">

使用JavaScript:


// 清除整个页面缓存
window.location.reload(true);

// 清除特定资源的缓存
const url = 'https://example.com/style.css';
fetch(url, {
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
}).then(response => {
// 处理响应
});

// 清除localStorage
localStorage.clear();

// 清除sessionStorage
sessionStorage.clear();

使用HTTP头信息(服务端设置):


// Express.js 示例
app.use((req, res, next) => {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
next();
});

使用框架或库功能:


例如,React中可以通过key属性强制重新渲染组件来清除缓存:


function App() {
const [key, setKey] = useState(0);

const resetPage = () => {
setKey(prevKey => prevKey + 1);
};

return (
<div key={key}>
{/* 页面内容 */}
<button onClick={resetPage}>重置页面</button>
</div>

);
}

清除浏览器缓存:


用户可以手动清除浏览器缓存来达到相同的效果。这通常通过浏览器设置或开发者工具的Network面板来实现。


综上所述,实现每次页面加载时清除本页缓存可以根据具体情况选择合适的方法。无论是通过HTML标签、JavaScript代码、服务器端设置还是框架功能,都可以有效地控制和管理页面的缓存行为,确保用户获得最新和最准确的内容。



作者:打野赵怀真
来源:juejin.cn/post/7389643363160965130
收起阅读 »

百亿补贴为什么用 H5?H5 未来会如何发展?

web
23 年 11 月末,拼多多市值超过了阿里。我想写一篇文章《百亿补贴为什么用 H5》,没有动笔;24 年新年,我想写一篇《新的一年,H5 会如何发展》,也没有动笔。 眼看着灵感就要烂在手里,我决定把两篇文章合为一篇,与大家分享。当然,这些分析预测只是个人观点,...
继续阅读 »

23 年 11 月末,拼多多市值超过了阿里。我想写一篇文章《百亿补贴为什么用 H5》,没有动笔;24 年新年,我想写一篇《新的一年,H5 会如何发展》,也没有动笔。


眼看着灵感就要烂在手里,我决定把两篇文章合为一篇,与大家分享。当然,这些分析预测只是个人观点,如果你有不同的意见,欢迎在评论区讨论交流。


拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的 H5、小程序武学。


百亿补贴为什么用 H5


我们先看两张图,在 Android 手机开发者模式下,开启显示布局边界,可以看到「百亿补贴」是一个完整大框,说明「百亿补贴」在 App 内是 H5;拷贝分享链接,在浏览器打开,可以看到资源中有 react 名字的 js 文件,说明「百亿补贴」技术栈大概率是 React。


pdd-stack.png


不只是拼多多,我特地确认了,京东、淘宝的的「百亿补贴」技术栈也是 H5。


pdd-jd-taobao.png


那么,为什么电商巨头会在「百亿补贴」这种重要活动上选择 H5 呢?用 H5 有什么好处呢?


H5 技术已经成熟


第一个原因,也是最基础的原因,就是 H5 技术已经成熟,能够完整地实现功能。具体来说:


浏览器兼容性不断提高


自 2008 年 HTML5 草案发布以来,截止 2024 年,HTML5 已有 16 年历史。16 年间,主流浏览器对 HTML5、CSS3 和 JavaScript 的标准语法兼容性一直持续改进,22 年微软更是亲手盖上了 IE 棺材板。虽然 Safari(iOS 浏览器)的兼容性仍然备受诟病,但总体来说兼容成本已经变得可以接受。


主流框架已经成熟


前端最主流的两大框架 Vue 和 React 已经成熟。它们的成熟体现在多个方面:



  • 从时间的角度看,截止 2024 年,React 已经发布了 11 年,而 Vue 已经发布了 10 年。经过多年的发展,前端开发者已经非常熟悉 React 和 Vue,能熟练地应用它们进行开发。

  • 从语法的角度看,自 React16.8 发布 Hooks,以及 Vue3 发布 Composition API 以来,两大框架语法基本稳定,不再有大的变化。前端开发者可以更加专注于业务逻辑,无需过多担心框架语法的变动。

  • 从未来发展方向看,React 目前致力于推广 React Server Component 1;Vue 则在尝试着无 VDom 的 Vapor 方向,并计划利用 Rust 重写 Vite 2。这表明旧领域不再有大的颠覆,两大框架已经正寻求新的发展领域。


混合开发已经成熟


混合开发是指将原生开发(Android 和 iOS)和 Web 开发结合起来的一种技术。简而言之,它将 H5 嵌入到移动应用内部运行。近些年,业界对混合开发的优势和缺陷已经有清晰的认识,并针对其缺陷进行了相应的优化。具体来说:



  • 混合开发的优势包括开发速度快、一套代码适配 Android 和 iOS,以及实现代码的热更新。这意味着程序员能更快地编写跨平台应用,及时更新应用、修复缺陷;

  • 混合开发的缺陷则是性能较差、加载受限于网络。针对这个缺陷,各大 App、以及云服务商如阿里云 3 和腾讯云 4 都推出了自己的离线包方案。离线包方案可以将网页的静态资源(如 HTML、CSS、JS、图片等)缓存到本地,用户访问 H5 页面时,可以直接读取本地的离线资源,从而提升页面的加载速度。可以说,接入离线包后,H5 不再有致命缺陷。


前端基建工具已经成熟


近些年来,业界最火的技术话题之一,就是用 Rust 替代前端基建,包括:用 Rust 替代 Webpack 的 Rspack;用 Rust 替代 Babel 的 SWC;用 Rust 替代 Eslint 的 OxcLint 等等。


前端开发者对基建工具抱怨,已经从「这工具能不能用」,转变为「这工具好不好用」。这种「甜蜜的烦恼」,只有基建工具成熟后才会出现。


综上所述,浏览器的兼容性提升、主流框架的成熟、混合开发的发展和前端基建工具的完善,使 H5 完全有能力承载「百亿补贴」业务。


H5 开发成本低


前文我们已经了解到,成熟的技术让 H5 可以实现「百亿补贴」的功能。现在我们介绍另一个原因——H5 开发成本低。


「百亿补贴」需要多个 H5


「百亿补贴」的方式,是一个常住的 H5,搭配上多个流动的 H5。(「常住」和「流动」是我借鉴「常住人口」和「流动人口」造的词)



  • 常住 H5 链接保持不变。站外投放的链接基本都是常住 H5 的,站内首页入口链接也是常住 H5 的,这样方便用户二次访问。

  • 流动 H5 链接位于常住 H5 的不同位置,比如头图、侧边栏等。时间不同、用户不同、算法不同,流动 H5 的链接都会不同,流动 H5 可以区分用户,方便分发流量。


    具体来看,拼多多至少有三个流量的分发点,第一个是可点击的头图,第二个是列表上方的活动模块,第三个是右侧浮动的侧边栏,三者可以投放不同的链接。最近就分别投放 3.8 女神节链接、新人链接和品牌链接:



pdd-activity.png


「百亿补贴」需要及时更新


不难想到,每到一个节日、每换一个品牌,「百亿补贴」就需要更新一次。


有时还需要为一些品牌定制化 H5 代码。如果使用其他技术栈,排期跟进通常会比较困难,但是使用 H5 就能够快速迭代并上线。


H5 投放成本低


我们已经「百亿补贴」使用 H5 技术栈的两个原因,现在来看第三个原因——H5 适合投放。


拼多多的崛起过程中,投放到其他 App 的链接功不可没。早期它通过微信等社交平台「砍一刀」的模式,低成本地吸引了大量用户。如今,它通过投放「百亿补贴」策略留住用户。


H5 的独特之处,在于它能够灵活地在多个平台上进行投放,其他技术栈很难有这样的灵活性。即使是今天,抖音、Bilibili 和小红书等其他 App 中,「百亿补贴」的 H5 链接也随处可见。


pdd-advertisement.png


拼多多更是将 H5 这种灵活性发挥到极致,只要你有「百亿补贴」的链接,你甚至可以在微信、飞书、支付宝等地方直接查看「百亿补贴」 H5 页面。


wechat-flybook-alipay.png


综上所述,能开发、能快速开发、且开发完成后能大量投放,是「百亿补贴」青睐 H5 的原因。


H5 未来会如何发展


了解「百亿补贴」选择 H5 的原因后,我们来看看电商巨头对 H5 未来发展的影响。我认为有三个影响:


H5 数量膨胀,定制化要求苛刻


C 端用户黏性相对较低,换一个 App 的成本微不足道。近年 C 端市场增长缓慢,企业重点从获取更多的新客变成留住更多的老客,很难容忍用户丢失。因此其他企业投放活动 H5 时,企业必须也投放活动 H5,电商活动 H5 就变得越来越多。


这个膨胀的趋势不仅仅存在于互联网巨头的 App 中,中小型应用也不例外,甚至像 12306、中国移动、招商银行这种工具性极强的应用也无法幸免。


12306-yidong-zhaoshang.png


随着市场的竞争加剧,定制化要求也变得越来越苛刻,目的是让消费者区分各种活动。用互联网黑话来说,就是「建立用户心智」。在可预见的未来,尽管电商活动 H5 结构基本相同,但是它们的外观将变得千差万别、极具个性。


fluid.png


SSR 比例增加,CSR 占据主流


在各家 H5 数量膨胀、竞争激烈的情况下,一定会有企业为提升 H5 的秒开率接入 SSR,因此 SSR 的比例会增加。


但我认为 CSR 依然会是主流,主要是因为两个原因:



  1. SSR 需要额外的服务器费用,包括服务器的维护、扩容等。这对于中小型公司来说是一个负担。

  2. SSR 对程序员技术水平要求比 CSR 更高。SSR 需要程序员考虑更多的问题,例如内存泄露。使用 CSR 在用户设备上发生内存泄露,影响有限;但是如果在服务器上发生内存泄露,则是会占用公司的服务器内存,增加额外的成本和风险。


因此,收益丰厚、技术雄厚的公司才愿意使用 SSR。


Monorepo 比例会上升,类 Shadcn UI 组件库也许会兴起


如前所述,H5 的数量膨胀,代码复用就会被着重关注。我猜测更多企业会选择 Monorepo 管理方式。所谓 Monorepo,简单来说,就是将原本应该放到多个仓库的代码放入一个仓库,让它们共享相同的版本控制。这样可以降低代码复用成本。


定制化要求苛刻,我猜测社区中类似 Shadcn UI 的 H5 组件库或许会兴起。现有的 H5 组件库样式太单一,即使是 Shadcn UI,也很难满足国内 H5 的定制化需求。然而,Shadcn UI 的基本思路——「把源码下载到项目」,是解决定制化组件难复用的问题的好思路。因此,我认为类似 Shadcn 的 H5 组件库可能会逐渐兴起。


总结


本文介绍了我认为「百亿补贴」会选用 H5 的三大原因:



  • H5 技术已经成熟

  • H5 开发成本低

  • H5 投放成本低


以及电商巨头对 H5 产生的三个影响:



  • 数量膨胀,定制化要求苛刻

  • SSR 比例增加,CSR 占据主流

  • Monorepo 比例增加,类 Shadcn UI 组件库也许会兴起


总而言之,H5 开发会越来越专业,对程序员要求会越来越高。至于这种情况是好是坏,仁者见仁智者见智,欢迎大家在评论区沟通交流。


拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的 H5、小程序武学。


Footnotes




作者:小霖家的混江龙
来源:juejin.cn/post/7344325496983732250
收起阅读 »

前端开发中过度封装的现象与思考

web
前言 作为公司内的一名高级前端码喽,大大小小也封装过了不少组件和功能,我逐渐意识到封装并非全是优点,也会存在一些不可忽视的潜在劣势。 在项目中,我们急切地对各种功能和 UI 进行封装,却在不经意间忽略了封装可能带来的额外成本与潜在问题。比如,在之前的一个项目中...
继续阅读 »

前言


作为公司内的一名高级前端码喽,大大小小也封装过了不少组件和功能,我逐渐意识到封装并非全是优点,也会存在一些不可忽视的潜在劣势。


在项目中,我们急切地对各种功能和 UI 进行封装,却在不经意间忽略了封装可能带来的额外成本与潜在问题。比如,在之前的一个项目中,为了实现一个看似简单的列表展示功能,我将数据获取、渲染逻辑以及交互处理都塞进了一个繁杂的组件中。后续当需要对列表的某一特定功能进行细微调整时,由于封装的过度复杂,修改工作变得极为棘手,耗费了大量时间去梳理内部的逻辑关系。


还有一次我在对一个表单验证功能的封装时,为追求过高的通用性,添加了过多的配置选项和繁杂的验证规则。这不但增加了代码量,还使得新加入团队的成员在使用时感到困惑,理解和运用这个封装的成本大幅提高。如果让我在写标准代码和学习过度封装的组件之间做选择,我绝对毫不犹豫的选择写标准代码。


一、前端功能封装的优势



  1. 可以提高代码复用性
    在众多项目中,常碰到类似的数据请求、表单验证等功能需求。将这些功能封装成独立的函数或模块,能极大提升代码的复用程度。例如,我们成功封装了一个通用的数据获取函数,在不同页面中仅需传入各异的参数,就能顺利获取所需数据,无需反复编写请求逻辑。

  2. 有效增强代码的可维护性
    封装后的功能代码相对独立,当需要对功能进行修改或优化时,只需在封装的模块内操作,不会对其他使用该功能的部分产生任何影响。如此一来,代码的维护工作变得更加清晰、易于掌控。

  3. 大幅提升代码的可读性
    通过为封装的功能赋予清晰、有意义的函数名和详尽的参数说明,其他开发者能够迅速理解其功能和使用方式,这样也极大提高团队协作的效率。写到这里我突然想起曾经在一个屎山项目中看到过的aaaa、Areyouok、jiashizheng等变量和函数名,我花了好久的时间才把它们修改正常...


二、前端功能封装的劣势



  1. 事极必反


    有时为追求极致的封装效果,可能会对一些简单且复用频率不高的功能进行封装,这反倒会增加代码的复杂程度和理解成本。例如,一个仅仅用于计算两个数之和的简单功能,若过度封装,可能会令后续的开发者感到迷茫。


    代码示例:


    function add(a, b) {
    return a + b;
    }

    // 过度封装
    function complexAdd(a, b) {
    if (typeof a!== 'number' || typeof b!== 'number') {
    throw new Error('输入必须为数字');
    }
    const result = a + b;
    // 一些额外的复杂逻辑
    return result;
    }

    在一个小型项目中,仅仅为了计算两个数字的和,使用了复杂的封装函数complexAdd,导致新同事在理解和使用时花费了过多时间,而原本简单的add函数就能满足需求。


  2. 可能隐藏底层实现细节


    过度封装或许会让使用功能的开发者对其内部实现一无所知。当问题出现时,可能需要耗费更多时间去理解封装内部的逻辑,进而影响问题的排查和解决效率。



三、UI 二次封装的优势



  1. 成功统一风格和交互


    在大型项目中,保障 UI 的一致性至关重要。通过对基础 UI 组件进行二次封装,能够明确统一的样式、交互行为和响应式规则。例如,对按钮组件进行二次封装,设定不同状态下的颜色、尺寸和点击效果。


  2. 显著提高开发效率


    开发人员能够直接运用封装好的 UI 组件,迅速搭建页面,无需在样式和交互的调整上耗费大量时间。


  3. 方便后期维护和更新


    当需要对 UI 进行整体风格的调整或优化时,只需修改封装的组件,所有使用该组件的页面都会自动更新,大幅减少了维护的工作量。



四、UI 二次封装的劣势



  1. 过度封装的危害



    • 增加不必要的代码量和复杂度,致使应用的加载性能降低。例如,一个简单的输入框组件,如果过度封装了很多复杂的逻辑和样式,可能会使代码体积过大。

    • 可能引入过多的抽象层次,让代码变得难以理解和调试。复杂的封装结构可能让开发者在排查 UI 问题时感到无从下手。

    • 过度复杂的封装在频繁的渲染和更新操作中,可能会导致性能瓶颈,影响用户体验。


    代码示例:


    // 过度封装的输入框组件
    class OverlyComplexInput extends React.Component {
    constructor(props) {
    super(props);
    this.state = { value: '' };
    }

    handleChange = (e) => {
    // 复杂的处理逻辑
    this.setState({ value: e.target.value });
    // 更多的额外操作
    }

    render() {
    return (
    <input
    value={this.state.value}
    onChange={this.handleChange}
    // 过多的样式和属性设置
    />

    );
    }
    }

    在一个性能要求较高的页面中,使用了过度封装的输入框组件,导致页面加载缓慢,用户输入时出现明显的卡顿。


  2. 灵活性受限
    过于严格的封装可能限制了开发者在特定场景下对 UI 进行个性化定制的能力。有时候,某些页面可能需要独特的样式或交互效果,而过度封装的组件无法满足这些特殊需求。

  3. 版本兼容性问题
    当对封装的 UI 组件进行更新时,可能会与之前使用该组件的页面产生兼容性问题。新的版本可能改变了组件的行为、样式或接口,导致使用旧版本组件的页面出现显示异常或功能失效。


所以在实际的开发过程中,我们需要权衡封装带来的好处和潜在的问题。封装应该是有针对性的,基于实际的复用需求和项目的规模。同时,要保持封装的适度性,避免过度封装带来的负面影响。只有这样,才能真正提高前端开发的效率和质量。


作者:漠水
来源:juejin.cn/post/7387731346733121551
收起阅读 »

部署完了,样式不生效差点让我这个前端仔背锅

web
大家好,作为今年刚毕业的大聪明(小🐂🐎),知道今年不好就业,费心费力的面试终于进了个小公司,然后我的奇妙之旅开始了。 叮!闹钟响了!牙都不刷了,直接起床出门去上班,身为🐂🐎就要有早起吃草干活的觉悟,所以我是个很合格的食草动物。只不过,下面的信息,让我一天当动...
继续阅读 »

大家好,作为今年刚毕业的大聪明(小🐂🐎),知道今年不好就业,费心费力的面试终于进了个小公司,然后我的奇妙之旅开始了。



叮!闹钟响了!牙都不刷了,直接起床出门去上班,身为🐂🐎就要有早起吃草干活的觉悟,所以我是个很合格的食草动物。只不过,下面的信息,让我一天当动物的好心情都没了。


无标题.png


部署?因为该公司人数比较少,也没有自动化部署操作,只能让前端build一下dist包然后丢给后端,让后端部署,然后该项目也比较急,我刚来,也不好意思@后端,恰巧跟我对接的后端也是刚来不熟悉,所以我就跟负责人继续扯皮拖延时间了。(真是不好意思,假的)↓↓↓


无标题ED.png
终于经过不断努力(后端),部署好了,但是,我的样式乱了,为什么呢?看了一下网络和源代码,样式文件都请求到了,问了后端,一致说是我前端的问题,我懵了,然后快马加鞭查找问题


无标题.png


解决


ok,那我先看打包的dist文件有样式有问题吗,使用npm安装个 http-server 运行打包后的index.html,运行打开运行后的地址,欧克样式没乱,文件引用也没问题,那到底是什么鬼影响的呢?


在我百思不得其解的时候,我打开了神器--csdn,搜索打包dist部署后样式文件不生效,就遇到了这个神文t.csdnimg.cn/WbQyq
欧克,按照博主说的Nginx没有配置这两个东西而导致的,我有点不确定是不是,有点犹犹豫豫的找到后端说帮我在Nginx配置文件的http加下这两个配置,然后样式就好了,完美!后端由于疏忽,没加上就把问题推给我,结果被我扳回一城。


include   mime.types;
default_type application/octet-stream;


  1. include mime.types;



    • 这个指令告诉Nginx去包含一个文件,这个文件通常包含了定义了MIME类型(Multipurpose Internet Mail Extensions)的配置。MIME类型指定了文件的内容类型,例如文本文件、图像、视频等。通过包含 mime.types 文件,Nginx可以识别不同类型的文件并正确地处理它们。

    • 示例:假设 mime.types 文件中定义了 .html 文件为 text/html 类型,Nginx在处理请求时会根据这个定义设置正确的HTTP响应头。



  2. default_type application/octet-stream;



    • 这个指令设置了默认的MIME类型。如果Nginx无法根据文件扩展名或其他方式确定响应的MIME类型时,就会使用这个默认类型。

    • application/octet-stream 是一个通用的MIME类型,表示未知的二进制数据。当服务器无法识别文件类型时,会默认使用这个类型。例如,如果请求的文件没有合适的MIME类型或没有被 mime.types 文件中列出,Nginx就会返回 application/octet-stream 类型。

    • 这种设置对于确保未知文件类型的安全传输很有用,因为浏览器通常会下载这些文件而不是尝试在浏览器中打开它们。




总之,添加 include mime.types; 和 default_type application/octet-stream; 配置后,Nginx能够正确地识别和处理CSS文件的MIME类型,从而确保浏览器能够正确加载和应用CSS样式。


所以,前端仔不能只当前端仔,还是要好好学点服务端的知识,不然锅给你,你还认为这是你该背的锅。



以上是开玩笑的描述,只是为了吸引增加阅读量



作者:世上只有一个唐广君
来源:juejin.cn/post/7388696625689051170
收起阅读 »

axios中的那些天才代码!看完我实力大涨!

web
axios的两种调用方式 经常调接口的同学一定非常熟悉aixos下面的两种使用方式: axios(config) // 配置式请求 axios({ method: 'post', url: '/user/12345', }); axios.po...
继续阅读 »

axios的两种调用方式


经常调接口的同学一定非常熟悉aixos下面的两种使用方式:



  • axios(config)


// 配置式请求
axios({
method: 'post',
url: '/user/12345',
});


  • axios.post(url, config)


// 简洁的写法
axios.post('/user/12345')

不知道各位大佬有没有思考过这样的问题:



axios到底是个什么东西?我们为什么可以使用这两种方式请求接口呢?axios是怎么设计的?



axios原理简析


为了搞明白上面的问题,我们先按照传统思路仿照axios源码实现一个简单的axios。


手写一个简单的axios


创建一个构造函数


function Axios(config){
this.defaults = config; // 配置对象
this.interceptors = { // 拦截器对象
request:{},
response:{}
}
}

上面的代码中,我们实现了一个基本的Axios类,但它还不具备任何功能。我们现在给它添加功能。


原型上添加方法


Axios.prototype.request = function(config){
console.log('发送Ajax 请求 type:' +config.method)
}
Axios.prototype.get = function(){
return this.request({method:'GET'})
}
Axios.prototype.post = function(){
return this.request({method: 'POST'})
}

上面的代码中,我们在request属性上创建了一个通用的接口请求方法,get和post实际都调用了request,但内部传递了不同的参数,这和axios(config)、axios.post()有异曲同工之妙。


参考aixos的用法, 现在,我们需要创建实例对象


let aixos = new Axios(config)  

创建后的axios包含defaultsinterceptors属性,其对象原型__proto__上(指向Axios的prototype)包含request、get及post方法,因此,我们现在可以使用aixos.post()的方法模拟调用接口了。


但注意,此时aixos只是一个实例对象,不是一个函数!我们似乎也没办法做到改造代码使用aixos(config)的形式调用接口!


aixos是如何实现的呢?


aixos中的天才想法


为了即能使用axios(config)又能使用axios.get(),axios的核心伪逻辑如下


function Axios(config){
this.defaults = config; // 配置对象
this.interceptors = { // 拦截器对象
request:{},
response:{}
}
}

Axios.prototype.request = function(config){
console.log('发送Ajax 请求 type:' +config.method)
}
Axios.prototype.get = function(){
return this.request({method:'GET'})
}
Axios.prototype.post = function(){
return this.request({method: 'POST'})
}

function createInstance(config) {
//注意instance是函数
const instance = Axios.prototype.request;
instance.get = Axios.prototype.get
instance.post = Axios.prototype.post
return instance;
}

let axios = createInstance();

通过上述的伪代码,我们可以知道axios是createInstance()函数的返回值instance



  • instance 是一个函数,因此,axios也是一个函数,可以使用axios(config);

  • instance也是一个对象(js万物皆对象),其原型上有get方法和post方法,因此,我们可以使用axios.post()。




我们看看aixos的源码


aixos的源码实现


 function createInstance(config) {
//实例化一个对象
var context = new Axios(config); //但是不能直接当函数使用

var instance = Axios.prototype.request.bind(context);
//instance 是一个函数,并且可以 instance({}),

//将Axios.prototype 对象中的方法添加到instance函数中,让instance拥有get、post、request等方法属性
Object.keys(Axios.prototype).forEach(key => {
// console.log(key); //修改this指向context
instance[key] = Axios.prototype[key].bind(context);
})
//总结一下,到此instance自身即相当于Axios原型的request方法,
//然后又给instance的属性添加了上了Axios原型的request、get、post方法属性
//然后调用instance自身或instance的方法属性时,修改了this指向context这个Axios实例对象

//为instance函数对象添加属性 default 与 intercetors
Object.keys(context).forEach(key => {
instance[key] = context[key];
})

return instance;
}

可以说,上面的代码真的写的精妙绝伦啊!


注意这里,为什么要修改this的指向


var instance = Axios.prototype.request.bind(context);

首先,requset 是Axios原型对象上的方法,其方法内部的this指向的是其实例化对象context!


Axios.prototype.request = function request(config) {
/*eslint no-param-reassign:0*/
// Allow for axios('example/url'[, config]) a la fetch API
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}

config = mergeConfig(this.defaults, config);

// Set config.method
if (config.method) {
config.method = config.method.toLowerCase();
} else if (this.defaults.method) {
config.method = this.defaults.method.toLowerCase();
} else {
config.method = 'get';
}

// Hook up interceptors middleware
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);

this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});

this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});

while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}

return promise;
};

因此,如果我们直接使用Axios.prototype.request()就会出现问题,因为这事reques方法内部的this会指向错误,导致函数不能运行,因此,我们必须将this重新指向其实例化对象。


作者:石小石Orz
来源:juejin.cn/post/7387029190620184611
收起阅读 »

后端同事下班早,前端排序我来搞

web
写掘金博客有一小段时间了,我发现了一个秘密。文章阅读量小了吧,很心烦,阅读量大了吧,更心烦。很多特别懂特别会的大哥们就会来“指点”我。感谢这些大哥们的“指点”,使我进步。后来我和群里的小伙伴们讨论了一下,为什么掘金文章会有那么多大哥们来“开心指点”呢?大概是这...
继续阅读 »

写掘金博客有一小段时间了,我发现了一个秘密。文章阅读量小了吧,很心烦,阅读量大了吧,更心烦。很多特别懂特别会的大哥们就会来“指点”我。感谢这些大哥们的“指点”,使我进步。后来我和群里的小伙伴们讨论了一下,为什么掘金文章会有那么多大哥们来“开心指点”呢?大概是这么几种情况:



1 他们爱学习,爱分享,但是即将要被裁员了,所以心情不是太好,怎么办呢?去掘金上指点他们去,让他们知道自己很会很懂;


2 他们在团队中就是翘楚,指点完了团队的人,然后不放心,怕世界不足够完美,反正自己也要被裁员了,有时间,然后补偿没拿够,心情不太好,去给他们指点指点去;


3 他们一直觉得自己不太懂,但是看见文章又想指点指点,所以呢,在家找了3个月工作这段时间,一边学习,然后看大家的文章,学习过程中呢,看哪里觉得不太合适,指点指点,希望趁着阅读量高的文章,好希望有人能发现他们,直接邀约他们入职;


这种人呢,我们总结了一下,他们可以叫“黑哥会”,意思就是黑哥们,比较会,啥都会那种,嗯,希望黑哥会的成员们早日找到心仪的工作,在家闲着不好的。



好啦,在文章正式开始之前呢,告诉大家个好消息,本文点赞,友善评论,友善建议的大哥大姐们,2024年的后半年,一定能够心想事成,工作顺利,家庭和睦,一直到永久。


321... 文本正式开始。


1 未排序的数据


今天早上来了公司,我赶紧喊老张,问:新来的前端妹子这么快就被你搞定啦?听说昨晚你俩10点一起出的公司?是不是,快说。 老张,猛地抬头,问:你咋知道的?我保密工作做这么好。 我说:门口的李大爷说的。你快说说什么情况啊。


老张说:别瞎说,昨天后端下班早,把接口就给妹子了,妹子本来以为调一调接口,传几个参数完事,结果发现后端给的数据没有排序,但看了产品文档,发现,又要根据学生姓名按字母排序,又要根据分数排序,又要根据年龄排序,又要根据日期排序,直接把妹子气的快哭了,所以我就帮他弄了弄。然后就弄到10点了呗,一起出的公司而已,别瞎想。


但是妹子为了感谢我,告诉了我一个好消息,过会儿和你说。我说:你快点说。老张说:你先听我把功能说完,我再告诉你。


你看,后端就一个接口,给的数据大概是这样子:


const users = [
{"name": "小张伟", "age": 19, "score": 55, "dateTime": '2021-03-03 15:33:10'},
{"name": "张三", "age": 22, "score": 65, "dateTime": '2023-03-03 10:10:10'},
{"name": "李四", "age": 30, "score": 87, "dateTime": '2024-04-03 10:10:10'},
{"name": "阿斌", "age": 50, "score": 90, "dateTime": '2021-03-03 10:10:10'},
{"name": "曹小操", "age": 1300, "score": 23, "dateTime": '1021-05-08 10:10:10'},
{"name": "小张灰", "age": 31, "score": 15, "dateTime": '1994-03-04 08:33:10'},
];

2 根据属性排序


这是一个杂乱的json型数组,但是要根据属性进行排序。我们目前做了3种类型的实现


2.1 引入工具库


这里说一个高效便捷功能丰富的前端JS库,首先引入js-tool-big-box工具库。


执行安装命令:



npm install js-tool-big-box



引入dataBox对象,排序的这些公共方法被放到了这个对象下面:


import { dataBox } from 'js-tool-big-box';

2.2 数值型排序


数值型排序呢,就是,你看,age 和 score 都是数值型的,我们把这些归结为一类进行排序。


2.2.1 根据age从小到大的排序

代码如下:


const ageResult1 = dataBox.sortByNumber(users, 'age');
console.log('age是数值型,从小到大,排序后的值为:', ageResult1);

结果如下:


image.png


2.2.2 根据age从大到小的排序

代码如下:


const ageResult2 = dataBox.sortByNumber(users, 'age', 1);
console.log('age是数值型,从大到小,排序后的值为:', ageResult2);

结果如下:


image.png


2.2.3 根据score从低到高的排序

代码如下:


const ageResult3 = dataBox.sortByNumber(users, 'score');
console.log('score是数值型,从低到高,排序后的值为:', ageResult3);

结果如下:


image.png


2.2.4 根据score从高到低的排序

代码如下:


const ageResult4 = dataBox.sortByNumber(users, 'score', 1);
console.log('score是数值型,从大到小,排序后的值为:', ageResult4);

结果如下:


image.png


2.3 中文按字母排序


比如我们的姓名,很多时候需要按字母从A到Z来展示,这个时候就可以用下面这个方法来快速实现:


2.3.1 按字母从A到Z排序

代码如下:


const nameResult1 = dataBox.sortByletter(users, 'name');
console.log('比如name,我们按照字母顺序排序后为:', nameResult1);

结果如下:


image.png


2.3.2 按字母从Z到A排序

代码如下:


const nameResult2 = dataBox.sortByletter(users, 'name', 1);
console.log('比如name,我们按照字母顺序倒序排序后为:', nameResult2);

结果如下:


image.png


2.3.3 注意

需要注意的是,我们这里只是传入了name的属性,如果这个json中有其他中文属性,也是可以使用这个方法进行按字母排序的,很灵活。


2.4 按日期时间排序


比如我们例子中的时间,按时间排序也是非常实用且常见的需求,


2.4.1 按时间从早到晚排序

代码如下:


const timeResult1 = dataBox.sortByTime(users, 'dateTime');
console.log('以时间从早到晚排序后的值为:', timeResult1);

结果如下:


image.png


2.4.2 按时间从晚到早排序

代码如下:


const timeResult2 = dataBox.sortByTime(users, 'dateTime', 1);
console.log('以时间从晚到早排序后的值为:', timeResult2);

结果如下:


image.png


2.4.3 注意

需要注意的是,我们例子中只是传入了dateTime属性,如果json对象中有其他的是时间格式的属性值,也可以把属性传入,就可以进行字段的属性排序啦,很便捷。


3 最后


把效果展示完了,我赶紧催促老张说:你刚才跟我说的好消息呢?老张悄声说:妹子和门口老大爷,还有咱们公司老板都姓李,你品去吧。妹子跟我说了,她跟她爸爸说:这个季度的优秀就是我。我一听也跟着高兴起来,希望看到这篇文章的大哥大姐们,也都能像老张一样,升职加薪,变得越来越优秀




最后告诉你个消息:
js-tool-big-box的npm地址是(js-tool-big-box 的npm地址)


js-tool-big-box的git仓库地址(js-tool-big-box的代码仓库地址)


作者:经海路大白狗
来源:juejin.cn/post/7384419675073789991
收起阅读 »

因为打包太慢,我没吃上午饭

web
事情的起因是这样的: 鄙人呢,在公司负责一个小小的后台管理系统。 这天中午临近饭点的时候,测试小哥坐我工位旁边现场监督我改一个Bug。 Bug本身它倒是不复杂,甚至时隔几天之后我已不记清具体内容是什么。当时只见我是三下C两下V、提交、合并测试分支、登录Jeki...
继续阅读 »

事情的起因是这样的:


鄙人呢,在公司负责一个小小的后台管理系统。


这天中午临近饭点的时候,测试小哥坐我工位旁边现场监督我改一个Bug。


Bug本身它倒是不复杂,甚至时隔几天之后我已不记清具体内容是什么。当时只见我是三下C两下V、提交、合并测试分支、登录Jekins点击deploy,一顿操作如行云流水一般丝滑。


wyzjy.gif


说时迟,那时快。公司给 极品廉价劳动力们 我们安排的午饭送到了,一人一份。但你也知道,这两年经济下行,无人幸免;有的人食不果腹,有的人衣不蔽体, 保不齐谁饿的紧,拿了两份饭,那可就意味着有一个人要饿肚子,我可万万不希望那个倒霉蛋是我。


看着Jekins的deploy进度条,我对测试小哥说:


“你先回去,等会发完了你再看一下,应该没有问题,我先去干饭了”


说罢,我转头看向测试小哥:他面无表情盯着屏幕,似乎是默许了我的提议。于是我便准备起身——


只见他头也不回一手把我按住,缓缓吐出四个字:


“看完再吃”


...


...


...


大约半个小时后,KFC。


我:“我都告诉你了,不会有问题,先干饭,你非不听”


测试小哥:“......”


我:“这下好了吧,上个月的工资还没发,现在又来付费上班”


测试小哥:“我就问你,星期四的这个辣翅,它香不香”


我:“香”


image.png


罪魁祸首


所以,项目到底deploy了多久?


f7c9f47e6cbb14b80745a39c3946832.png


Jekins的记录中可以看到,编译打包环节耗时基本在五六分钟,最近几次成功构建的整体耗时平均在5分50秒。


这个项目本身呢,说大不大,说小也不算小。是个普通CRUD页面居多的管理后台,没有太多其他乱七八糟的东西。如果以页面、组件数量的维度来看:


使用资源管理器在项目的/src目录下通配*.vue可以看到有561个文件


image.png

image.png


说实话,这样的体量打包5-6分钟,属实有点过分。


我又找来公司的一个巨石应用来对比:由于巨石应用历史比较悠久,横跨了多个技术栈(HandleBars模板引擎、使用jQuery的原生HTML、Vue2),不能只看SFC的数量,所以这次就来比较一下,用来存放页面文件的文件夹的体积


image.png


我的项目








image.png


巨石应用


先不算其他的资源,单就页面文件体积已经接近5倍,如果其他东西都算上,打包时间就算没有5倍,一两倍总是要有的吧?


结果呢,时间甚至更短


image.png

好好好,有活干了。为了避免下次面试官问我对webpack做过什么优化时复述那些网上千篇一律的答案,现在就来实际操作一下。



本文真实记录了一次对项目构建耗时、产物体积优化的过程。没有对知识点系统的梳理,主要突出的是思路:面对问题时的解决思路。



日志分析


曾经有位技术能力超强的架构师说过:遇到问题不要慌,先看日志。


既然这么慢的构建过程是发生在Jekins上,那就先来捋一捋Jekins的log,有没有什么值得注意的地方。


这个项目的构建脚本中,抛开(Jekins)工具的准备、从git上拉取代码以及最后的部署这些动作,只看跟前端的打包有关的部分,命令很简单,只有四行:


rm -rf node_modules
rm package-lock.json
npm i
npm run build

在日志中体现如下图:


0c4ebfe0835a4da8d477713af40285d.png


35c974d100d807a636a530608ae602f.png


开局就是一记暴击!


v7m5.gif


11:22:08 + npm i
11:25:59 + added 1863 packages from 1199 contributors balabala...
...
...
11:25:59 + npm run build
...
...

合着这五六分钟的打包时间,安装依赖就占了一大半,阿西巴!


在继续往下进行之前,请允许我先介绍些项目的其他背景:



deploy脚本拉取代码这一步简单来说就是:cd进项目目录 -> git pull -> 切换至要构建的分支


项目的开发人员较少,算我在内三个人


项目的依赖变动频率十分低,以月或数月为单位



背景铺垫完了,开始研究npm i为什么这么耗时,相关的命令有三句:


rm -rf node_modules
rm package-lock.json
npm i

其中npm i这句是必须的,没什么好说的;rm -rf node_modulesrm package-lock.json这两句是变量,挨个做耗时的对比测试。


首先,我在本地使用跟Jekins上相同的node版本(14.16.1),使用相同的npm源(官方源),新建一个目录clone项目代码:



  1. 完整执行三行命令,耗时与Jekins上相差无几

  2. (此时已经有了package-lock.json文件)执行rm -rf node_modules + npm i,耗时极短

  3. (此时已经有了node_modules目录)执行rm package-lock.json + npm i,耗时也极短


第三步其实没什么意义,npm文档中有提到,这种情况就相当于梳理了node_modules的结构并生成了package-lock.json,并没有安装任何东西。
image.png


而步骤一和步骤二之间为何耗时相差比较大,可以参考这篇我觉得npm install流程写的比较好的文章:简单来说就是花费了大量时间去远端获取包信息


结合项目背景一,我们的package-lock.json会提交到仓库,每次肯定都是最新的,所以Jekins deploy脚本中rm package-lock.json这一步属实是没有必要,本地计算完了到Jekins上又来一遍,纯纯的浪费时间。


联系运维哥把测试环境的deploy脚本修改一下,去掉了rm package-lock.json这一句,测试下耗时:


c7085e76a3aed2877d8b6fdfb846fb9.png

如图中下边两次构建,【编译打包】环节时间都去到了2分30秒左右,直接缩短了一半多。部署成功后,在测试环境的页面咔咔一顿点,似乎也没有什么依赖包引起的报错。


效果是不是很显著,你以为这就完了?不不不,图中那条49秒的构建我可不是大意截进去的,伪装成不经意的失误,就是为了丝滑的承上启下


QwN4E.gif


既然这个项目的开发人员很少,而且依赖的添加/更新频率又极低,也就意味着每次npm i所安装的东西,基本都是一样的,既然都一样,为什么我还要rm -rf node_modules再安装?


想象一下你日常本地开发时,如果某次需求要用到一个新的npm包,你一定是先npm install xxx,除非碰到了依赖冲突,否则不会清除node_moduels重新安装。


明明npm提供了梳理依赖树、只做局部更新的逻辑,我们却偏偏每次清除node_modulesinstall,这种行为吧,我感觉就像明明是个Vue项目,却在里边到处使用Document API


哎嘿,我就不用你的响应式,就是玩~


lKGp.gif


冒着被打的风险又私聊了运维哥,把rm -rf node_modules去掉,再发布了一次看看效果


677c1174550bece7d6fabb58a09597a.png

优秀!打包时间从5分多直接干到了50秒,优化率80%+!


本文结束!



在正式结束前,觉得还是有必要补充两点



  1. 各位读者在做打包优化时,部署脚本是否清除package-lock.jsonnode_modules还是主要取决于项目实际情况和团队协作模式,不能因为一味的追求构建速度而导致频繁的构建失败/安装依赖失败[滑稽]

  2. 如果您经过深思熟虑后觉得还是有必要清除package-lock.jsonnode_modules,山人还有一计可供大王优化构建速度:打包时离不开babel,但babel又是个老大难,好在它能缓存转换结果。一般情况下缓存会放在项目目录下的/node_modules/.cache/,那我们把删除node_modules的命令稍微改那么亿点:


    find node_modules/ -mindepth 1 -maxdepth 1 ! -name '.cache' -exec rm -rf {} +


    删除node_modules里面除了/.cache目录以外的其他内容,这样在构建过程中babel还是能使用到之前的缓存。那速度,体验过的都说好!(看babel-loader的缓存文件有多大)


    0716d75b5c769cef10689560947061b.png










全面升级


如果是本着以后不影响吃午饭的目的,那现在确实可以结束了。但我自幼便深受中国四大名句之一来都来了的兄弟句式——干都干了的文化熏陶:既然已经开始了,那就干脆给项目做个全套大保健!


image.png


不过此时我和在座的各位都一样,对打包优化这块着实没什么经验,可以说是毫无头绪。


浅浅百度了下webpack打包优化,有两个工具基本每篇文章都有提到:打包耗时分析speed-measure-webpack-plugin、打包体积分析webpack-bundle-analyzer(vue-cli内置)


目前的痛点是,那就先来个耗时分析试试水。


使用方法还是老样子,自己去查,别人都写的我就不再重复写了


效果如图:


2d1e9f581d75198215a76ae86e410db.png


9f5463a422434e783d851fe6cf13dbc.png


此时因为babeleslint还没有缓存,耗时多是意料之中的;其他的loader或多或少的三两组合,展示了一个module计数和耗时小计,我从中并没有办法获得什么有用的信息;并且多次构建对比发现loader组合的规律和耗时的排名也无迹可寻。在我的认知里:所有被命中的文件会按照loader配置的顺序依次处理,所以面对这样的结果我实在是无计可施。(有会看的朋友可以补充一下)


翻看speed-measure-webpack-plugin的文档,发现有可以打印耗时top N文件的配置项,但开启后再次构建得到的这些文件,同样令我摸不着头脑:一个寥寥数十行的SFC小组件,css-loader耗时竟然能用四到五秒!要知道里边只有一条scoped的样式规则。


无奈只好放弃,看了下项目用的是vue-cli@4.x创建的,对应的webpack@4.x,那就去webpack的文档里逛逛碰碰运气吧!


可惜,福无双至祸不单行。文档里翻了半天,耳熟能详、配置简单的路子,例如babel-loader、eslint-loader的编译缓存多线程打包chunk分割代码混淆压缩tree shaking这些,要么是之前已经被配置过了,要么是webpack内置了。而复杂、高级一些的优化方式,我的项目又用不到...


直到我看到了这里:


image.png

升级webpack简单(呸),npm upgrade webpack嘛,先来搞这个~~


回到项目的package.json里,咦,好奇怪,没有webpack,也没有vue-cli


vue-cli是装在全局的,而webpack是作为依赖的依赖安装的,没有体现在package.json中,所以直接npm upgrade webpack应该是不行的。vue-cli文档提供了一个升级的命令:vue upgrade


既然要升级,干脆全上新的!Node也给他干到20!(我也不知道我当时为什么要这么做,但这为后来的事情埋下了伏笔。。。)


vue-cli升级完,扫了一遍webpack升级指南,发现我项目里的配置文件也没什么好改的,Nice!


image.png


本地浅浅的run了一下server、run了一下build,发现也都OK!那就提交上去在Jekins上试试Node V14o不ok


emmmm...


报错倒是没报错,只是...


4f7cb2e83ffe8afb4db6cd7c5b02221.png

本地build的时候没注意,Jekins上跑才发现,怎么慢了这么多!说好的更新到最新版本均有助于提高性能呢?


再看看这构建物的体积


05d17813e68c984f311cff8868385fb.png


image.png


Hà的我赶紧又本地build了一次,还真让我发现了些东西:似乎build了两次


4e9531cc221e18e94592ac4df18ff6f.png


image.png


按理来说应该只有下边这个print,那上边的legacy bundle又是什么东西?百度上随便那么一搜,应该是不少人都被这么坑过,很容易搜到:这是一种兼容性的构建产物,主要是为了兼容一些很古老版本的浏览器/客户端。想控制也很容易,改package.json里的browserslist字段即可。


这就好办了,这项目是我们公司的内部项目,考虑兼容性?不存在的。


{
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}

配置了之后又试了下,基本恢复到了升级webpack之前的水平,但还是慢一点点...


构建速度的优化这块,实在是没头绪了,明明升级了webpack版本,构建速度却变慢了。


不过刚才提到的两个工具,构建耗时分析的用过了,还有个vue-cli内置的构建体积分析工具没体验;如果需要打包的东西变少了,那构建速度应该也能快一点吧!(吧?)


塑形瘦身


在正式瘦身前,有一个小插曲:


不知道在座的各位,项目里有没有这样的东西


console.log(123123)
// or
console.log('asdfasdf')

我是一个崇尚极简的人,我能接受的底线也就是


console.log('list data: ', data);

仅此而已


你要打印接口返回数据,Network里能看


你要打印函数中某个变量的值,可以打断点


我实在是想不出什么必须console.log的场景


如果你说为了方便线上调试


我能接受的最多也就是按规范打印有意义的log


更别提项目首屏就要翻好几页的无意义log,要知道,大量的console.log也是会影响首屏加载性能


在之前,我通过husky + lint-staged进行过限制,但还是有人以我这个有用这之前不是我写的等等诸多借口绕过了eslint检测,提交了无意义的log。所以这次我最终还是决定,你不仁就休怪我不义,TerserPlugin drop_console走起,本地开发你随便log,只要发到线上我就删掉。


{
plugins: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
pure_funcs: ["console.log", "console.info"],
},
},
})
]
}

毕竟删掉几句console.log,也算瘦身
















接着就webpack-bundle-analyzer走起,vue-cli内置的使用方式是


vue-cli-service build --report

打包后会在你输出的目录里边生成一个report.html,当时的截图找不到了,用语言描述一下就是:从node_moduels里打进去的依赖包,面积直接占了整个屏幕的大概三分之一。那个图网上很容易找到,内容就是打包产物按照体积和来源绘制成一个个的矩形在页面里。


这其实是好事,打进去的依赖包多,我们的可操作空间就大,先拿Vue开个刀。


// vue.config.js
module.exports = {
// ...
configureWebpack: {
externals: {
vue: "Vue",
},
},
// ...
}

也不要忘了把package.json里的vue依赖删除掉、在/public的模版HTML中,通过<script>引入CDN文件。


再打个包看看效果:


ac297e6a4499324cbbf2090d5c9b72d.png

可以看到vue确实咩有了


但在调试的过程中,发现第三方CDN不稳定,时而获取超时


a2e3ed8dfb40b5eb9d36693029e55e9.png

为了保险起见,只得把CDN文件copy到本地/public里来(我司没有自己的CDN或者依赖私仓,正在筹备中)


暂时没什么问题了,下一个就是我们的UI组件库ant-design-vue@1.7.8,按照相同的方式配置一下,不过这次运行后有报错了:


f4f2f6a9b9a3afaaa82b637617803dc.png

可以看到报错是和moment有关系,在antd的文档也找到了原因:如果使用已经构建好的文件,要自行引入moment



为什么antdv不做按需引入?原因有二:



  1. 项目的入口main.js中全量导入了antdv进行注册,页面中直接使用。如果要改成按需引入,要么每个页面里新增按需引入的语句,要么统计使用了哪些组件在main.js里改为按需引入(似乎有plugin解决这个问题,记不太清了)

  2. 按需的这个需,基本等于全量了。。。粗略的扫了一下文档,除了像CommentMentions这种带有互动性质的组件,其他的基本都用上了,所以改按需好像意义也不大



moment的时候国际化有一个小问题,CDN网站一般会提供以下几种文件:



  1. 无国际化的moment主体文件

  2. 带全部语言包的moment主体文件

  3. 单个语言包文件(无功能)


如果没有国际化的需求,那是万万没有必要引入全部语言包的moment。但moment默认是英语,至少需要引入一个中文语言包。碰巧antdv也需要做国际化处理,是相同的问题。


momentantdv的国际化方式很相似:


<a-config-provider :locale="antdLocale" />

moment.locale(momentLocale);

data() {
return {
antdLocale,
momentLocale
}
}

我们只需要知道这个locale运行时的值,把它提取出来就行了。打印后发现其实就是个很简单的key-value对象(不是JSON),在node_modules中的源码里找到 它们复制出来在/public下新建zhCN文件:


window.momentLocale = xxx /* 复制出来的对象 */
window.antdLocale = xxx /* 复制出来的对象 */

image.png


image.png


使用时:


<a-config-provider :locale="antdLocale" />

moment.locale(momentLocale);

data() {
return {
antdLocale: window.antdLocale,
momentLocale: window.momentLocale
}
}

以后如果有别的依赖也有类似的国际化需求,继续向zhCN.js里添加就行。只需要新增一个http请求,就解决了所有依赖的国际化问题。


剔除了antdvmoment之后的report.html


55b3f51ed28b6eb71e2939a26d8e471.png


惊喜的发现,antdvicons也被一起干掉了。


少了这么几个大家伙,此时必须要Jekins上build一波看看效果!


还记得之前把Node给升到20了吗


于是就...报错了...Node版本太低...


image.png


本地切回NodeV14,发现连server也起不来了。。


摸黑前行


预警:这将是一段枯燥且艰难的黑暗时光


搞过的都知道,处理Node版本兼容问题时,如果是需要升级还好;如果是要降级,Node内置的各种包会出现稀奇古怪的报错,而且这些报错还难以trace...



由于这趴的问题实在过于稀奇,甚至在google上都搜不到有用的信息,所以基本都没有截图,但我会尽可能的描述出我对问题的看法。看文字也许你觉得云淡风轻解决起来很轻松,但实际上花费了我接近一整天的时间以及一撮撮掉落的头发...



1. npm run server出现大量的.vue单文件报错


具体的报错信息记不太清,但报错顺序与路由表注册的顺序相符(和动态路由懒加载是两码事,路由懒加载是在运行时访问到页面才会加载对应的chunk,但编译打包时,只要是代码中webpacktrace到的文件,都会被处理)。目测是所有的.vue都有报错,那问题就应该不是出在代码上,而是整体配置上。


翻看vue-loader文档时看到了这个


image.png

升级vue-cli时确实也升级了vue-loader,按照指引配置了下,resolve


2. jsx语法报错


这个问题就有点奇怪了,在升级前,是没有给webpack做过什么支持jsx语法的配置的。升级后,却都报错了。


翻阅了一些资料和支持jsx的解决方案,大部分都是说把SFC<script>加上lang="jsx",里边的内容全部当作jsx解析。这种方式对eslintbabel的配置改动比较大,曾数次尝试无法成功,最后都把所有配置还原重新开始。


image.png


后来灵光一闪,不如直接用刚更新的vue-cli创建一个新项目,看是否支持,如果可行的话,直接把各个配置文件照搬即可。


4799c859068eaebc4f6f4a8db07ac02.png

9221f6f9f1e017649f47b29be1ab01d.png


结果还真可以。babel.config.jsvue.config.js以及package.jsoneslintConfig字段,先全部按照官方脚手架的配置改掉,成功启动之后再挨个把我们自定义的配置添加回去。这过程当中没有出现什么问题,且按下不表,resolve


3. 启动之后,页面白屏报错:(0 , vue__WEBPACK_IMPORTED_MODULE_0__.resolveComponent) is not a function


其中resolveComponent也有可能是其他一些Vue3暴露出来的Api,通过打断点观察,推测是Vue内部在初始化的时候出了问题。


不确定是哪里出了问题,但在把之前删除的Vue依赖安装回来(只是开发环境会用到,打包不会打进去)以及把添加的VueLoaderPlugin去掉以后,resolve


迎接黎明


以上这些问题解决以后,已经可以正常启动、打包项目了。但刚才的bundle analyzer进行到一半还没结束,图中的第三方依赖库应该还有一些可以剔除掉的,比如隐藏在一个业务代码chunk里的echarts


7ae1cd4a1c4f0715606a5074554c2a7.png


检索了代码后,发现有按需引入的:


import {xxx, xxx} from "echarts"

也有全量引入的:


import * as echarts from 'echarts';

在分析代码后整理了所使用到的echarts Api和组件,把全量引入改为按需引入,重新打包后发现包体积没有变。我好奇难道echarts只要有一个地方使用了按需引入,其他地方也能自动分析把全量引入改为按需引入?遂把按需引入的也反向改为全量引入重新打包,结果:


5010aca8d7ff78da92b22705416a4c1.png


第二次的改动体积变化了,那就只能说明....


问了写那段代码的同事,果然,在需求迭代的过程当中技术方案变更了,所以那个文件废弃掉没有用了,改了个寂寞...


此时还剩下jquerylodash计划剔除掉,其他的依赖包有一部分已经是比较规范的按需引入,剔除掉改为cdn引入带来的收益不大,当然主要的原因还是因为


jquery:这个npm包有点意思


5794b48e2187021280c39b9a6122da5.png


打进来的是非压缩版本,因为package.json中设置的main就确实是这个,但dist包中明明提供了压缩后的版本。两个版本的体积差距在三倍多,不知包作者的意图是什么


1ed025730a7a1ba28ef75a190cb696e.png


298e66d7e62306a1dc18b97e6b10ac5.png


但最后还是把jquery这个依赖彻底放弃了:整个项目中只有一个远古时期添加的图片预览组件依赖了它,而我们现在开发了样式、功能更为强大的新组件,所以把所有使用到这个组件的地方都改为使用新组件,然后顺带把jquery uninstall了。


lodash:官网本身提供了可按需引入的版本lodash-es,但项目中太多地方都是全量引入的方式在使用


import * as _ from "lodash"

暂且先改成CDN的方式全量引入


至此,bundle analyzer的分析图变成了这样:


8920151ddcf9709147f3a9ab8e094ec.png


三方依赖的chunk已经比包含了echarts的那个业务代码chunk体积还要小。瘦身瘦到这里感觉差不多了,那些更小的依赖包本身体积不大,换成一个http请求也未必是一件划算的事。


然后就还是回到webpack的配置上来,前边一直在琢磨怎么添加配置去做优化,但vue-cli本身已经封装了一套久经考验的配置,不如从这个配置着手,看能否针对我们项目的实际情况做一些修改


获取配置命令(融合了自定义的配置)


vue inspect --mode=production > file-name.js

mode不传的话默认是development。下载下来打开,1400多行猛的一看似乎有点唬人,但实际上有1000行左右都是对样式文件的loader配置。


image.png


粗略的看下vue-cli@5.0.8中有哪些值得注意的配置



  • 解析文件的优先级


// 导入模块时如果不提供文件后缀,同名文件 后缀名的优先级
extensions: [".mjs", ".js", ".jsx", ".vue", ".json", ".wasm"]


  • Hash


optimization: {
realContentHash: false, // 使用非严格的hash计算,减少耗时
}


  • 代码压缩:css使用的是CssMinimizerPluginjs使用的是TerserPlugin


minimizer: [
// 已经内置了js压缩工具terser
new TerserPlugin({
terserOptions: {
compress: {
arrows: false,
collapse_vars: false,
comparisons: false,
computed_props: false,
hoist_funs: false,
hoist_props: false,
hoist_vars: false,
inline: false,
loops: false,
negate_iife: false,
properties: false,
reduce_funcs: false,
reduce_vars: false,
switches: false,
toplevel: false,
typeofs: false,
booleans: true,
if_return: true,
sequences: true,
unused: true,
conditionals: true,
dead_code: true,
evaluate: true,
},
mangle: {
safari10: true, // 代码混淆时兼容使用`let`关键字声明的循环迭代器变量可能会出现无法重复声明let变量的错误。
},
},
parallel: true, // 多进程打包
extractComments: false, // 不将注释单独提取到一个文件中
}),
new CssMinimizerPlugin({
parallel: true,
minimizerOptions: {
preset: [
"default",
{
mergeLonghand: false,
cssDeclarationSorter: false,
},
],
},
}),
]


  • Loader

    • 大量的篇幅编排不同样式文件相关的Loader,分别有csspostcssscsssasslessstylus,按照css moduels in SFC -> SFC style -> normal css modules -> normal css的顺序依次处理。

    • 对于脚本文件,已经开启了多线程转译以及babel缓存功能




{
test: /\.m?jsx?$/,
exclude: [
function () {
/* omitted long function */
},
],
use: [
{
loader:
"path-to-your-project/node_modules/thread-loader/dist/cjs.js",
},
{
loader:
"path-to-your-project/node_modules/babel-loader/lib/index.js",
options: {
cacheCompression: false,
cacheDirectory:
"path-to-your-project/node_modules/.cache/babel-loader",
cacheIdentifier: "1d489a9c",
},
},
],
}


  • Plugin

    • VueLoaderPlugin:已经内置了

    • DefinePlugin:注入编译时的全局配置

    • CaseSensitivePathsPlugin:路径的大小写严格匹配

    • FriendlyErrorsWebpackPlugin:优化报错信息

    • MiniCssExtractPlugin

    • HtmlWebpackPlugin

    • CopyPlugin:配置了info.minimized = true,copy的同时也会压缩

    • ESLintWebpackPlugin:同样开启了缓存




得,不仅没找到有啥可优化的地方,甚至还污染了人自带的配置:


已经内置了TerserPlugin,前边为了打包时去除consoleplugin里边又配置了一次,通过speed-measure-webpack-plugin分析时发现似乎是走了两遍TerserPlugin


只好通过webpack-chain去注入一下,顺便把项目中其他修改webpack配置的地方也改为注入的形式。(使用ConfigureWebpack去改,无法改到已有的TerserPlugin配置):


chainWebpack: (config) => {
config.when(process.env.NODE_ENV === "production", (config) => {
config.devtool(false);
config.optimization.minimizer("terser").tap((args) => {
const compress = args[0].terserOptions.compress;
args[0].terserOptions.compress = {
...compress,
drop_console: true,
pure_funcs: ["console.log", "console.info"],
};
return args;
});
});
config
.externals({
vue: "Vue",
moment: "moment",
"moment/locale/zh-cn": "moment.local",
"ant-design-vue": "antd",
lodash: "_",
})
.resolve.alias
.set("@", path.join(__dirname, "src"))
.set("@worker", path.resolve(__dirname, "public/worker.js"))
.end();
config.plugin("speed-measure").use(SpeedMeasurePlugin);
}

image.png


如果使用ConfigureWebpack


configureWebpack: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
pure_funcs: ["console.log", "console.info"],
},
},
}),
],
},

集成的配置最下方会出现一个新的minimizer数组,不是我们想要的效果


image.png




截止到目前,构建速度变成这样(果然还是没有变更快)


image.png


从项目剔出去的第三方依赖,体积是这么多


928af467649ccc1467ac6476c0d230e.png


不过虽然没打进chunk里去,但还是作为静态依赖在构建产物中,下一步就是搭一个公司内部的简易缓存服务器(有关缓存的内容可以看我上一篇文章)。届时这部分体积才算真正从项目里移除了,不过此时我们还是可以把它视作优化的成果,由于没有对别的类型的资源做什么优化处理,也压根就没什么别的资源,所以只看打包后的js体积的话:


image.png


优化前








image.png


优化后


数据也基本对的上,所以综合来看:



  • 平均构建速度(average full time):从5分50秒减少到54秒,优化率84.48%1 - 54秒 / 3分50秒

  • 脚本构建体积(script size):从5.5M减少到3.6M,优化率35.55%1 - 3.6M / 5.5M






先这样吧,至少下次被问到webpack,多少有点自己的东西能讲,近期可能也不会再更新文章了,原因嘛,你们懂的


image.png


欢迎真诚交流,但如果你来抬杠?阿,对对对~ 你说的都对~


作者:Elecat
来源:juejin.cn/post/7389044903940603945
收起阅读 »

无插件实现一个好看的甘特图

web
效果 预览地址 code.juejin.cn/pen/7272286… 前言 刚好看到这么一个东西,甘特图,然后又发现好像echarts 里面没有这个图形渲染,也去找了一下插件,都不符合我的要求,然后自己就想着看能不能实现一个,然后说干就干,过程还挺复杂的,...
继续阅读 »

效果


预览地址 code.juejin.cn/pen/7272286…


QQ录屏20230828153807 00_00_00-00_00_30.gif


前言


刚好看到这么一个东西,甘特图,然后又发现好像echarts 里面没有这个图形渲染,也去找了一下插件,都不符合我的要求,然后自己就想着看能不能实现一个,然后说干就干,过程还挺复杂的,不过一想清楚了,也就还行。


逻辑


刚开始看着这个图,想到肯定是用表格来实现的,后面开始渲染的时候,发现是我想简单了,这表格是渲染不出来的,也或者是我技术还没够,反正我找了很多相关的插件,都不是用表格去实现的,后面就改变了一些思路,用div去渲染,然后去定位,这么一想,发现事情就简单了许多。


为什么不用表格实现


每点击一个视图切换,表头就需要重新渲染,表头有索引,名称,负责人,如果是表格实现的话,就是如下代码,一看就知道了id=“tableYear”的不好渲染,因为上面有可能是年,也有可能是月,所以肯定是有循环的,但如果一循环,它们没有共同的父容器,怎么渲染呢,也想过表格里面套表格,但那样,样式又实现不了我要的效果。


 <table>
<thead>
<tr>
<th rowspan="2">id</th>
<th rowspan="2">任务名称</th>
<th rowspan="2">负责人</th>
<th colspan="4" id="tableYear">2023-8</th>
</tr>
<tr id="tableDay">
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
</tr>
</thead>
</table>

第一个难点


日期渲染,因为我这表头是动态的,所以要复杂一些,有天数,月数,季度和年度显示的


image.png
点击日视图,就是按天去显示任务,月视图就是按月去显示任务,季视图和年视图同理,
比如说日视图,
先获取当前日期,年月日,然后是想渲染前后几年的


var currentDate=new Date;//当前日期
var currentYear = currentDate.getFullYear();//当前年份

var yearRange = 1; // 前后1年
var startDate = currentYear - yearRange;//前1年
var endDate = currentYear + yearRange;//后1年
var today = currentDate.getDate(); // 获取今天是几号
var currentYear = currentDate.getFullYear();//年
var currentMonth = currentDate.getMonth();//月
var displayedYears = {}; // 用于记录已显示的年份

开始渲染第一排是年月,一年的月份是固定的,所以都是可以写死的,这里有要注意一下的是,就是年月的宽度,因为要根据当月有多少天去计算宽度,所以我要知道,这一年这一月是多少天然后乘以40 下面是相关代码


for (var year = startDate; year <= endDate; year++) {
for (var month = 0; month < 12; month++) {
var lastDay = new Date(year, month + 1, 0).getDate();
var monthElement = $("<p>" + (month + 1) + "月: </p>"); // 创建表示月份的 <p> 元素

for (var day = 1; day <= lastDay; day++) {
dateRange.push(new Date(year, month, day));
}
// 在 .tableYear 中添加年份和月份信息
var yearMonthStr = year + "-" + (month + 1 < 10 ? "0" : "") + (month + 1);
var width = (lastDay * 40)-1 + "px"; // 计算宽度
$(".tableYear").append($("<p class='Gantt-table_header' style='width: " + width + "'>" + yearMonthStr + "</p>"));
}
}

渲染完成,年月以后就是日,我这里也做了一些小的显示,比如说周末深颜色表示,今天也深颜色表示,并且视图要显示在当前,而不是在2022-1-1号这里。


天数渲染


for (var i = 0; i < dateRange.length; i++) {
var currentDate = dateRange[i];
var dayNumber = currentDate.getDay(); // 获取星期几 (0 = 星期日, 1 = 星期一, ...)
var isWeekend = dayNumber === 0 || dayNumber === 6;
var dayText = currentDate.getDate();//获取日
var dayYear = currentDate.getFullYear(); // 获取年份
var dayMonth = currentDate.getMonth(); // 获取月份(注意:月份是从 0 到 11,0 表示一月,11 表示十二月)
var tableCell = $("<p>" + dayText + "</p>");
if (isWeekend) {
tableCell = $("<p class='Gantt-table_weekend'>" + dayText + "</p>");
}
//获取当前时间的年月日,与循环出来的年月日进行循环匹配,
if (dayText === today && dayYear === currentYear && dayMonth === currentMonth ) {
tableCell.addClass("today");
}
$(".tableDay").append(tableCell);
}

视口显示代码


 // 将视口滚动到今天所在的位置
var todayElement = $(".today");
if (todayElement.length > 0) {
var viewportHeight = window.innerHeight || document.documentElement.clientHeight;
var elementHeight = todayElement[0].clientHeight;

var offset = (viewportHeight - elementHeight) / 2;
//平滑滚动参数 smooth auto不滚动
todayElement[0].scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
}

第二个难点


image.png
如果不用表格,怎么去实现这个网格呢,刚开始是想着去渲染,表格对应有多少天,就渲染有多少个p标签,但一想这怎么行,如果任务很多,一条任务就要渲染上千个p标签,太浪费资源了,任务一多,那不直接挂壁。
后面在css里面找到了解决办法


background: repeating-linear-gradient(to right, rgb(221, 221, 221), rgb(221, 221, 221
1px, transparent 1px, transparent 40px) 0% 0% / 40px 100%;

ChatGPT是这样解释的


image.png
然后完美解决问题,不用渲染这么多,一个任务一个div就可以了。


第三个难点


甘特图的核心,那个柱状图的东西。


image.png


柱状图渲染,比如说我提了一个任务,是2023年8月20号开始的,然后到2023年8月25号要完成,那这样就只有五天时间,那渲染肯定是从2023年8月20号开始的,然后到8月25号结束。


我的思路是这样的,显示的宽度是五天,然后一天的宽度是40px,那么这个任务的总宽度就是200px,然后定位,获取到这个任务开始的时候,我这里显示是前后一年的也就是2022-1-1号开始的,然后相减,知道中间相差了多少天,然后再乘以40,得到left的距离,就实现了这个效果。后面的描述也是类似的效果,这样甘特图差不多就完成了。下面是日期相减代码


 function getOffsetDays(startDate, endDate){
var startDateArr = startDate.split("-");
var checkStartDate = new Date();
checkStartDate.setFullYear(startDateArr[0], startDateArr[1], startDateArr[2]);
var endDateArr = endDate.split("-");
var checkEndDate = new Date();
checkEndDate.setFullYear(endDateArr[0], endDateArr[1], endDateArr[2]);
var days = (checkEndDate.getTime() - checkStartDate.getTime())/ 3600000 / 24;
if(startDateArr[0]!=endDateArr[0]){
flag = true;
}
return days;
}

结语


虽然三言两语就讲解完了,但其中还是有很多逻辑的问题,我只是讲解了一下我的一个实现的思路,居体代码请看这


以上就是本篇文章的全部内容,希望对大家的学习有所帮助,以上就是关于无插件实现一个好看的甘特图的详细介绍,如果有什么问题,也希望各位大佬们能提出宝贵的意见。当然也有很多需要优化的地方,我这只是给了一个思路,你们可以去实现很多的功能。


作者:流口水的兔子
来源:juejin.cn/post/7272174836336132132
收起阅读 »

一次低端机 WebView 白屏的兼容之路

web
问题 项目:Vite4 + Vue3,APP WebView 项目 页面在 OPPO A5 手机上打不开,页面空白。 最开始是客户端在看,然后发现一个警告,大概也因为没看出什么问题,给到 Web 前端。 相关背景 为了方便描述过程的行为,先做一些相关背景的介绍...
继续阅读 »

问题


项目:Vite4 + Vue3,APP WebView 项目


页面在 OPPO A5 手机上打不开,页面空白。


最开始是客户端在看,然后发现一个警告,大概也因为没看出什么问题,给到 Web 前端。


相关背景


为了方便描述过程的行为,先做一些相关背景的介绍。知道这些背景才能更好的了解问题的复杂。这些在解决问题的过程中始终是干扰因素,在反复调试试错的过程中才梳理总结出来,这里把它们列出来。


使用测试 App,其中有两个入口,一个是本地调试,这个地址是写在 App 里的,也就是要修改这个地址需要客户端重新出包;一个是项目的测试地址,这个地址测试可以进行配置。



修改客户端,重新出包,是很麻烦的,所以尽量避免。


项目配置了 HTTPS 支持,所以开发地址是 https 开头。但是也能启动 http 的地址。


关于项目支持 HTTPS,可以参考之前写的这篇:juejin.cn/post/732783…


之所以要支持 HTTPS 是因为 iOS WebView 只支持 HTTPS 的地址。


而安卓 App WebView 却需要 HTTP 打开,原因是安卓 WebView 反馈不支持本地地址 HTTPS 的方式。但是在本机上用 MuMu 模拟器打开 App,是能打开本地 HTTPS 地址的,之前也尝试过给安卓手机安装根证书,但是还是不行,得到以上反馈。


所以我本地开发安卓在电脑 MuMu 上调试,iOS 可以用手机调试,如果要安卓真机本地调试,需要去掉本地 HTTPS 的支持,使用 HTTP 的地址(自然,iOS 本地调试就不能同时进行了)


快速尝试


拿到问题之后,快速进行问题验证,在 OPPO A5 上,进入 APP 中,打开本地调试,用 HTTP 的方式。发现确实白屏,查看了客户端相关的日志,发现一个警告:



[INFO:CONSOLE(9)] "The key "viewport-fit" is not recognized and ignored.", source: xxx



于是修改 viewport-fit,发现并没有区别,这只是一个提示,应该没有影响。


于是采用最简代码法,排除法,用最简单的页面进行测试,看是否能正常打开,确定 WebView 没有问题。直到确定 script type="module" 引入的 main.ts 的代码没有起作用。


于是,基本上确定 Vite 的开发模式在 OPPO A5 WebView 中有问题。那不支持 ESM,就是兼容性问题?


快速查看解决方案,引入官方插件:@vitejs/plugin-legacy。但是怎么测试呢?要验证兼容性是否生效,只能验证打包构建后的代码,而不是通过本地调试进行测试。那只能发布到测试了,但这样岂不是要改一点就要发布一次,这是没办法进行的。但是第一次,还是发布一下看有没有生效。


不出意料,没那么容易解决!测试地址依然白屏。


如何调试


确定如何方便的调试是解决问题的必要条件。


几天后又开始看这个问题。


浏览器是否能打开页面?


首先在 App 中进行调试是比较麻烦的,需求启动 App,那么能否在浏览器中进行测试呢?很遗憾,期间用手机系统浏览器打开测试地址是正常的,后面打开本地地址也是正常的。所以浏览器和 App WebView 是有区别的。


启动本地服务查看构建后的页面


兼容插件只是解决打包后的构建产物,想要看打包后的效果,于是我想到将打包后的文件起一个 Web 服务,这样就可以打开打包后的页面 index.html,而且手机访问同一网络,扫码就可以打开这个页面。



  • 找了 Chrome 插件 Web Server for Chrome,发现已经不能用了

  • 找了 VS code 插件 Live Server,服务启了,但是有个报错。

  • 换用 http-server,启动服务 xxx:8080。正常打开页面,手机也能访问。


那么考虑我们的实际问题,如何在手机调试呢?将本地调试改成本地起的服务 xxx:8080,看 WebView 能否打开,这样每次修改、打包,生成新的打包后文件,刷新 WebView 就可以了。


但是前面说了,找 APP 出包很麻烦,改一个地址要出个包,费时。还有其他的办法吗?如果把本地启的服务端口改成 5173 不就不用改 App 了吗,可以直接用本地调试来进行测试,突然又想到本地调试的地址是 HTTPS,可是启动的本地服务好像没法改成 HTTPS。


通过测试地址增加本地调试入口


又想到 App 中的本地调试入口本应该做成一个公共页面,里面放上很多可能的入口。这样只需要 APP 改一次,之后想要什么入口,可以自己添加。改这个调试入口还是需要 App 改动,还是麻烦。我可以在项目中增加一个页面 debug.html(因为项目是多页面应用),这样我增加页面\增加调试入口,发布一下测试就生效了,在测试入口就能看到,这样更快。于是做了一个公共页面。


Vite preview


而且突然想到根本不需要自己起一个服务,Vite 项目,Vite preview 就是把打包后的页面启动服务。地址是:xxx:4173。


修改测试地址为本地预览


然而,OPPO A5 WebView 本来就是打不开我们的系统,那么 WebView 打不开测试地址自然也就没法打开我的本地预览了。但是,测试地址是可以配置的,所以为了快速调试,让测试配置了我的本地预览地址 xxx:4173。


这样,终于在 APP WebView 中打开了我本地预览的页面。


如何查看 App WebView 的日志


手机连接电脑,adb 日志:


image1-2.png


看起来这几个报错是正常的,报错信息也说了:



vite: loading legacy chunks, syntax error above and the same error below should be ignored



但是页面没有加载,不知道 WebView 打开页面和页面加载之间发生了什么。


Vite 兼容插件的原理


这期间,反复详细理解原理,是否是插件的使用不对。


用一句话说就是,Vite 兼容插件让构建打包的产物多了传统版本的 chunk 和对应 ES 语言特性的 polyfill,来支持传统浏览器的运行。兼容版的 chunk 只会在传统浏览器中运行。它是如何做到的呢?




  • 通过 script type="module" 中的代码,判断当前浏览器是否是现代浏览器。如果是,设置全局变量:window.__vite_is_modern_browser = true。判断的依据是:



    • import.meta.url;

    • import("_").catch(() => 1);

    • async function* g() { }



  • 通过 script type="module",如果是现代浏览器,直接退出;如果不是,加载兼容文件:

  • 通过 script type="nomodule",加载兼容 polyfill 文件;

  • 通过 script type="nomodule",加载兼容入口文件;


传统浏览器不执行 type="module" 的代码,执行 type="nomodule" 的代码。


现代浏览器执行 type="module" 的代码,不执行 type="nomodule" 的代码。


为什么需要 type="module" 的代码?这里是针对浏览器支持 ESM 却不支持以上 3 个语法的情况,仍然使用兼容模式。


详细可以看参考文章,以及查看打包构建产物。


除了知乎那篇文章,我几乎翻遍了搜到的 vite 兼容 空白 白屏 相关的文章,参考相关的配置。这个插件就是很常规的使用,几乎没有看到有任何特殊的配置或处理。就是生成了兼容的代码,低版本浏览器就能使用而已,似乎没人碰到过我的问题。


尝试解决


前面说了,用手机系统浏览器打开页面,竟然正常。怀疑是不是 WebView 的问题。


WebView 的内核版本


借了几个低端机型,几个安卓 5.x 6.x 的系统,结果手机浏览器都能正常打开。


打印 console.log(navigator.appVersion),WebView 中:


5.0 (Linux; Android 8.1.0; PBAT00 Build/OPM1.171019.026; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36 uniweb/ma75 uniweb-apk-version/0.6.0 uniweb-script-version/0.6.0 uniweb-channel/netease Unisdk/2.1 NetType/wifi os/android27 ngwebview/4.1 package_name/com.netease.sky udid/944046b939d510b1 webview_orbit/1.2(1)


而手机浏览器版本为 Chrome 90,其他手机有 Chrome70。总之,OPPO A5 WebView 的内核 Chrome 版本较低。


Vite 文档对于构建生产版本浏览器兼容性的介绍:


用于生产环境的构建包会假设目标浏览器支持现代 JavaScript 语法。默认情况下,Vite 的目标是能够 支持原生 ESM script 标签支持原生 ESM 动态导入import.meta 的浏览器


原生 ESM script 标签的支持:



原生 ESM 动态导入的支持:



import.meta 的支持:



所以,原来 Chrome 62 支持 ESM,但是不支持其他 2 个。通过日志,也可以知道不支持兼容插件尝试的 3 个语法,因为打印了那句警告,来自 module 的代码位置,window.__vite_is_modern_browser 不为 true。



从 Android 4.4 开始,系统 WebView 使用 Chrome 内核。


手机系统浏览器内核和系统 WebView 不一样,手机系统的 WebView 也可能不是安卓默认的。


兼容生效了吗?


但还是不知道低版本的浏览器兼容性是否生效,当前我们只能确定 Chrome 62 的 WebView 中兼容有问题,那是否在浏览器中就正常呢?或者更低的版本兼容是否生效?(毕竟不支持 ESM 的浏览器版本的代码执行又不一样)


target 配置不对?


target 配置的是目标浏览器,针对这些浏览器生成对应的兼容代码,期间我一直调整 target 的配置,使用 .broswerlistrc 文件配置,target 直接配置,参考不同的配置方案,确保包含了 Chrome 62。


又是如何调试?


想要下载安卓 Chrome 62 进行测试,但是搜了一圈也没找到。


后来想到不一定要手机浏览器进行测试,Chrome 也行。这里面有点思维上的转换,之前我测试只能通过 WebView 进行调试,因为浏览器上没有问题。现在确定了是浏览器 Chrome 版本的问题,那么我们还是可以通过 PC 浏览器进行测试。


于是范围更大一些,找到了 Chrome 的所有历史版本,不得不说,Chrome 提供的下载真是太有用了,对于测试兼容性非常有帮助! 而且我所担心的覆盖现有浏览器版本的问题完全不存在,下载之后直接运行。


安装 Chrome 62,打开页面,果然空白。终于在浏览器复现,确定就是兼容的问题,而不是 WebView 的问题。安装安卓版本的 Chrome 62,也同样复现。


下载 win 的 Chrome 62,虽然在 refs 里找到同版本的记录:xxx。但是没找到同版本的下载,不过也都是 62,应该没问题。下打开页面,打开控制台,和在 adb 中看到的报错一样,只是这里是红色的:



安装更低版本的 Chrome,同样复现,说明不支持 ESM 的兼容也出现问题。同时可以看到那句提示没有了:



过程当然也没那么顺利,下载 Chrome 的过程中,Chrome 62 直接可以运行,下载 Chrome 59 却没法打开。于是又下载了 Chrome 55,mini exe 文件,可以直接打开。


报错到底要不要处理?


通过 adb日志可以看到报错,也可以从打包后的代码看到:对于语法报错是可以忽略的,因为那是预期中的行为。可是之后的代码为什么没执行了呢?


回到现在的问题,这个报错不是不需要处理吗?但是加载了兼容的 js,页面却没有渲染元素。此时隐隐觉得报错可能还是要处理,至少可能最后一个报错有点问题?


但是这个报错实在难以查看,之前我把它当作和前两个报错一样的来源。现在只剩这个报错了,问题是这是打包压缩后的代码,完全不知道真正的问题是什么。


通过请教网友,做了一些尝试:


通过对插件配置:renderModernChunks: false,只生成兼容代码,依然报错。


通过修改 Vite 配置:build.minify: false 不压缩代码,尝试查看报错位置。新的报错:



升级 Vite。新的报错:



所以每次的报错都不一样,越来越奇怪。不过看起来似乎是同一个原因导致的。


在构建源码中调试


通过在构建后的源码中打印,其中 excute 函数中,有两个参数 exports module,但是在其中使用 module.meta 报错,说明其他文件在使用这个方法是并没有传参。看起来像是模块规范的问题(commonJS 和 ES Module)。


ChatGPT


在这期间,也在 ChatGPT 搜素方法:




就尝试了一下 format: 'es',顺便看到有个配置 compact: true,好像也是压缩,就顺手改成 false,这样全部不要压缩,方便看报错。


结果竟然 OK 了,页面打开,没有报错!


是这个配置生效的吗?通过排除,发现竟然是 compact 的原因。这个配置不是 Vite 本身的,是 Vite 使用的 rollup 的配置:




果然是插件冲突的结果。


再搜素 execute,已经没有带参数了:



再次感叹 Webpack 配置工程师



build.sourcemap


后来想到开启 sourcemap 来定位报错的原始文件位置,未开启:




开启 sourcemap:




如果在打包过程中对代码进行了混淆或压缩,可能会导致 Source Map 无法准确映射到原始代码位置。


这就完了?


中午去吃个饭,下午回来,本以为打包发布验证一下就完了,结果测试地址能打开页面了,却和我在电脑浏览器上看到的一样,没有正常加载页面。晴天霹雳!


image21-2.png


说明一下:我们这个项目是 App Hybrid 应用,Web 前端和服务端的通信通过客户端中转,所以在非客户端环境是拿不到服务端的数据的,联调测试都是在 App 中进行的。


但为什么前面一直用浏览器测试呢?虽然数据获取不到,但是如果页面加载出来了,说明打包代码是没问题,所以在前面页面显示了背景图等元素,id="app"中有了内容以后,就是兼容版本的 js 正常执行了。但是再说一次,真实的环境是 WebView,最终的结果还是要看 WebView。


目前在 App 中还是没有显示完整的网页加载过程,又陷入了困境。页面已经没有了报错(除了 Vite 那个可以被忽略的),难道生成的兼容 js 有问题? 如果是具体的兼容代码有问题,那整个项目的代码又如何排查呢?


虽然没有头绪,但是隐隐觉得应该再坚持一下。继续分析,如果已经显示了页面,兼容的 js 已经执行了,那么数据接口请求了吗?


于是联系服务端,查看服务器日志,确定页面加载没有请求接口,说明请求接口的代码没执行。我用高版本 Chrome 查看 Preview(之前没做这一步),果然除了看到页面元素,还进行了请求尝试。于是又运用排除法,一步步打印首页加载的过程。


在这之前,又是如何调试的问题,我们把测试入口改为我本地的 preview 地址。


加入打印日志,本地 build,Preview,打印了日志信息,果然接口请求的部分未执行。排查发现是之前调起 WebView 导航的代码出问题,可能是 jsBridge 还未加载。


但是为什么其他设备没有出现这个问题呢,可能是设备性能较差。于是把相关代码放到 jsBridge 加载完成后执行,修复了这样一个隐藏的 bug。


但是为什么不报错呢??这真的是很不好的体验,之前 Vue router 不报错的问题也类似。


总结


同样,我们再回头看那个最初的报错:



vite: loading legacy chunks, syntax error above and the same error below should be ignored



上面的报错、项目相同的报错可以被忽略。很容易让人忽略了报错,明确提到了下面的报错,但是 Vite 打包中下面没有出现同样的报错了(我目前的打包和其他文章中提到的不一样,比如知乎文章提到的代码);而且相同的报错到底是什么相同,同样是语法报错而已啊。


这句提示值得商榷。


function __vite_legacy_guard() {
import.meta.url;
import("_").catch(() => 1);
async function* g() {};
};

主要是因为出问题的恰恰就是中间的版本,Chrome 62,Vite 让它报错又能正常运行。遇到这种情况少之又少,从权衡上来说,好像也没有问题


本质上,只是在解决一个打包后的文件报错的问题,问题是一开始并没有定位到这个问题,其次是打包后的报错仍然难以定位具体错误位置。然后这其中还涉及到项目自身环境的各种干扰。


几点感悟:



  • 坚持不懈,这是解决问题的唯一原因。

  • 总结熟练调试很重要,要快速找到方便调试的方法。

  • 没有报错是开发的一大痛点。

  • 针对当前的问题更深入的分析原因,更广泛的尝试。

  • 多用 ChatGPT,ChatGPT 的强大在于它没有弱点,没有缺项。


说明


通过这个案例,希望能给大家一点解决问题的启发。是遇到类似的问题时:



  • 了解相关的问题

  • 熟悉相关的概念

  • 学习解决问题的方法

  • 学习调试的方法

  • 坚持的重要性


参考


【原理揭秘】Vite 是怎么兼容老旧浏览器的?你以为仅仅依靠 Babel?


juejin.cn/post/723953…


Chromium History Versions Download ↓


作者:choreau
来源:juejin.cn/post/7386493910820667418
收起阅读 »

谈谈前端如何防止数据泄漏

web
最近突然发现了一个好玩的事情,部分网站进去的时候几乎都是死的,那种死是区别于我们常见的网站的死:不能选中文字不能复制粘贴文字不能鼠标右键显示选项不能打开控制台……各种奇葩的操作应接不暇,像极了我最初接触的某库。shigen的好奇心直接拉满,好家伙,这是咋做的呀...
继续阅读 »

最近突然发现了一个好玩的事情,部分网站进去的时候几乎都是死的,那种死是区别于我们常见的网站的死:

  • 不能选中文字
  • 不能复制粘贴文字
  • 不能鼠标右键显示选项
  • 不能打开控制台
  • ……

各种奇葩的操作应接不暇,像极了我最初接触的某库。shigen的好奇心直接拉满,好家伙,这是咋做的呀。一顿操作之后,发现这种是为了防止网站的数据泄露(高大上)。在我看来,不是为了装X就是为了割韭菜。

咱废话也不多说,就手动来一个,部分代码参考文章:如何防止网站信息泄露(复制/水印/控制台)

shigen实现的效果是这样的:

将进酒页面

用魔法生成了一个页面,展示的是李白的《将进酒》。我需要的功能有尽可能的全面,禁止复制、选择、调试……

找了很多的方式,最后能自豪的展示出来的功能有:

  • 禁止选择
  • 禁止鼠标右键
  • 禁止复制粘贴
  • 禁止调试资源(刷新页面的方式)
  • 常见的页面水印

那其实也没有特别的技术含量,我就在这里展示了,希望能作为工具类供大家使用。

页面部分

html5+css,没啥好讲的。

 html>
 <html lang="zh-CN">
 
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <style>
         body {
             font-family: "Microsoft YaHei", sans-serif;
             line-height: 1.6;
             padding: 20px;
             text-align: center;
             background-color: #f8f8f8;
        }
 
         .poem-container {
             max-width: 600px;
             margin: 0 auto;
             background-color: #fff;
             padding: 20px;
             border-radius: 8px;
             box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
        }
 
         h1 {
             font-size: 1.5em;
             margin-bottom: 20px;
        }
 
         p {
             text-indent: 2em;
             font-size: 1.2em;
        }
     style>
     <title>李白《将进酒》title>
 head>
 
 <body>
     <div class="poem-container">
         <h1>将进酒h1>
         <p>君不见,黄河之水天上来,奔流到海不复回。p>
         <p>君不见,高堂明镜悲白发,朝如青丝暮成雪。p>
         <p>人生得意须尽欢,莫使金樽空对月。p>
         <p>天生我材必有用,千金散尽还复来。p>
         <p>烹羊宰牛且为乐,会须一饮三百杯。p>
         <p>岑夫子,丹丘生,将进酒,杯莫停。p>
         <p>与君歌一曲,请君为我倾耳听。p>
         <p>钟鼓馔玉不足贵,但愿长醉不复醒。p>
         <p>古来圣贤皆寂寞,惟有饮者留其名。p>
         <p>陈王昔时宴平乐,斗酒十千恣欢谑。p>
         <p>主人何为言少钱,径须沽取对君酌。p>
         <p>五花马,千金裘,呼儿将出换美酒,与尔同销万古愁。p>
     div>
  body>

js部分

禁止选中

 // 防止用户选中
 function disableSelect() {
     // 方式:给body设置样式
     document.body.style.userSelect = 'none';
 
     // 禁用input的ctrl + a
     document.keyDown = function(event) {
         const { ctrlKey, metaKey, keyCode } = event;
         if ((ctrlKey || metaKey) && keyCode === 65) {
             return false;
        }
    }
 };

禁止复制、粘贴、剪切

 document.addEventListener('copy', function(e) {
     e.preventDefault();
 });
 document.addEventListener('cut', function(e) {
     e.preventDefault();
 });
 document.addEventListener('paste', function(e) {
     e.preventDefault();
 });

禁止鼠标右键

 // 防止右键
 window.oncontextmenu = function() {
     event.preventDefault()
     return false
 }

禁止调试资源

这个我会重点分析。

 let threshold = 160 // 打开控制台的宽或高阈值  
 window.setInterval(function() {
     if (window.outerWidth - window.innerWidth > threshold ||
         window.outerHeight - window.innerHeight > threshold) {
         // 如果打开控制台,则刷新页面  
         window.location.reload()
    }
 }, 1000)

这个代码的意思很好理解,当我们F12的时候,页面的宽度肯定会变小的,我们这个时候和屏幕的宽度比较,大于我们设置的阈值,我们就算用户在调试页面了。这也是我目前找到的比较好的方式了。但是,但是,认真思考一下以下问题需要你考虑吗?

  • 页面频繁加载,流量的损失大吗
  • 页面刷新,后端接口频繁调用,接口压力、接口幂等性

所以,我觉得这种方式不优雅,极度的不优雅,但是有没有别的好的解决办法。

加水印

 // 生成水印
 function generateWatermark(keyword = 'shigen-demo') {
     // 创建Canvas元素  
     const canvas = document.createElement('canvas');
     const context = canvas.getContext('2d');
 
     // 设置Canvas尺寸和字体样式  
     canvas.width = 100;
     canvas.height = 100;
     context.font = '10px Arial';
     context.fillStyle = 'rgba(0,0,0,0.1)';
 
     // 绘制文字到Canvas上  
     context.fillText(keyword, 10, 50);
 
     // 生成水印图像的URL  
     const watermarkUrl = canvas.toDataURL();
 
     // 在页面上显示水印图像(或进行其他操作)  
     const divDom = document.createElement('div');
     divDom.style.cssText = `
         position: fixed;
         z-index: 99999;
         top: -10000px;
         bottom: -10000px;
         left: -10000px;
         right: -10000px;
         transform: rotate(-45deg);
         pointer-events: none;
         background-image: url(${watermarkUrl});
     `
;
     document.body.appendChild(divDom);
 }

代码不需要理解,部分的参数去调整一下,就可以拿来就用了。

我一想,我最初接触到这种页面水印的时候,是在很老的OA办公系统,到后来用到了某书,它的app页面充满了水印,包括浏览器端的页面。

所以,我也实现了这个。but,but,有一种技术叫做OCR,大白话讲就是文字识别。我把图片截个图,让某信、某书识别以下,速度和效果那叫一个nice,当然也可能把水印也识别出来了。聪敏的开发者会把水印的颜色和文字的颜色设置成一种,这个时候需要准确的文字那可得下一番功夫了。换句话说,不是定制化的OCR,准确的识别出信息,真的够呛。

还有的很多页面实现了js的数据加密、接口数据加密。但是道高一尺,魔高一丈,各种都是在一种相互进步的。就看实际的业务场景和系统的设计了。


作者:shigen01
来源:juejin.cn/post/7300102080903675915
收起阅读 »

没用的东西,你连个内存泄漏都排查不出来!!

web
背景 (书接上回) ui妹子的无理要求,我通通满足了。但是不出意外的话,意外就出来了。 此功能在上线之后,我们的业务在客户app内使用刷脸的时候会因为内存过高导致app将webview杀死。 然后我们leader爆了,让我排查问题。可是可是,我哪里会排查内存...
继续阅读 »

背景 (书接上回)



  • ui妹子的无理要求,我通通满足了。但是不出意外的话,意外就出来了。

  • 此功能在上线之后,我们的业务在客户app内使用刷脸的时候会因为内存过高导致app将webview杀死。

  • 然后我们leader爆了,让我排查问题。可是可是,我哪里会排查内存泄漏呀。

  • 我:我不会。你自己不会上吗?你tm天天端个茶,抽个烟,翘个二郎腿,色眯眯的看着ui妹妹。

  • 领导:污蔑,你纯粹就是污蔑。我tm现在就可以让你滚蛋,你信吗?

  • 我:我怕你个鸟哦,我还不知道你啥水平,你tm能写出来个防抖节流,我就给你磕头。

  • 领导:hi~ui妹妹,今天过的好吗,来,哥哥这里有茶喝。(此时ui妹妹路过)。你赶快给我干活,以后ui妹妹留给你。

  • 艹!你早这么说不就好了。





开始学习


Chrome devTools查看内存情况




  • 打开Chrome的无痕模式,这样做的目的是为了屏蔽掉Chrome插件对我们之后测试内存占用情况的影响

  • 打开开发者工具,找到Performance这一栏,可以看到其内部带着一些功能按钮,例如:开始录制按钮;刷新页面按钮;清空记录按钮;记录并可视化js内存、节点、事件监听器按钮;触发垃圾回收机制按钮等




简单录制一下百度页面,看看我们能获得什么,如下动图所示:




从上图中我们可以看到,在页面从零到加载完成这个过程中JS Heap(js堆内存)、documents(文档)、Nodes(DOM节点)、Listeners(监听器)、GPU memoryGPU内存)的最低值、最高值以及随时间的走势曲线,这也是我们主要关注的点



看看开发者工具中的Memory一栏,其主要是用于记录页面堆内存的具体情况以及js堆内存随加载时间线动态的分配情况



堆快照就像照相机一样,能记录你当前页面的堆内存情况,每快照一次就会产生一条快照记录




如上图所示,刚开始执行了一次快照,记录了当时堆内存空间占用为33.7MB,然后我们点击了页面中某些按钮,又执行一次快照,记录了当时堆内存空间占用为32.5MB。并且点击对应的快照记录,能看到当时所有内存中的变量情况(结构、占总占用内存的百分比...)





在开始记录后,我们可以看到图中右上角有起伏的蓝色与灰色的柱形图,其中蓝色表示当前时间线下占用着的内存;灰色表示之前占用的内存空间已被清除释放



在得知有内存泄漏的情况存在时,我们可以改用Memory来更明确得确认问题和定位问题


首先可以用Allocation instrumentation on timeline来确认问题,如下图所示:



内存泄漏的场景



  • 闭包使用不当引起内存泄漏

  • 全局变量

  • 分离的DOM节点

  • 控制台的打印

  • 遗忘的定时器


1. 闭包使用不当引起内存泄漏


使用PerformanceMemory来查看一下闭包导致的内存泄漏问题


<button onclick="myClick()">执行fn1函数button>
<script>
function fn1 () {
let a = new Array(10000) // 这里设置了一个很大的数组对象

let b = 3

function fn2() {
let c = [1, 2, 3]
}

fn2()

return a
}

let res = []

function myClick() {
res.
push(fn1())
}
script>


在退出fn1函数执行上下文后,该上下文中的变量a本应被当作垃圾数据给回收掉,但因fn1函数最终将变量a返回并赋值给全局变量res,其产生了对变量a的引用,所以变量a被标记为活动变量并一直占用着相应的内存,假设变量res后续用不到,这就算是一种闭包使用不当的例子



设置了一个按钮,每次执行就会将fn1函数的返回值添加到全局数组变量res中,是为了能在performacne的曲线图中看出效果,如图所示:




  • 在每次录制开始时手动触发一次垃圾回收机制,这是为了确认一个初始的堆内存基准线,便于后面的对比,然后我们点击了几次按钮,即往全局数组变量res中添加了几个比较大的数组对象,最后再触发一次垃圾回收,发现录制结果的JS Heap曲线刚开始成阶梯式上升的,最后的曲线的高度比基准线要高,说明可能是存在内存泄漏的问题

  • 在得知有内存泄漏的情况存在时,我们可以改用Memory来更明确得确认问题和定位问题

  • 首先可以用Allocation instrumentation on timeline来确认问题,如下图所示:




  • 在我们每次点击按钮后,动态内存分配情况图上都会出现一个蓝色的柱形,并且在我们触发垃圾回收后,蓝色柱形都没变成灰色柱形,即之前分配的内存并未被清除

  • 所以此时我们就可以更明确得确认内存泄漏的问题是存在的了,接下来就精准定位问题,可以利用Heap snapshot来定位问题,如图所示:




  • 第一次先点击快照记录初始的内存情况,然后我们多次点击按钮后再次点击快照,记录此时的内存情况,发现从原来的1.1M内存空间变成了1.4M内存空间,然后我们选中第二条快照记录,可以看到右上角有个All objects的字段,其表示展示的是当前选中的快照记录所有对象的分配情况,而我们想要知道的是第二条快照与第一条快照的区别在哪,所以选择Object allocated between Snapshot1 and Snapshot2即展示第一条快照和第二条快照存在差异的内存对象分配情况,此时可以看到Array的百分比很高,初步可以判断是该变量存在问题,点击查看详情后就能查看到该变量对应的具体数据了


以上就是一个判断闭包带来内存泄漏问题并简单定位的方法了


2. 全局变量


全局的变量一般是不会被垃圾回收掉的当然这并不是说变量都不能存在全局,只是有时候会因为疏忽而导致某些变量流失到全局,例如未声明变量,却直接对某变量进行赋值,就会导致该变量在全局创建,如下所示:


function fn1() {
// 此处变量name未被声明
name = new Array(99999999)
}

fn1()


  • 此时这种情况就会在全局自动创建一个变量name,并将一个很大的数组赋值给name,又因为是全局变量,所以该内存空间就一直不会被释放

  • 解决办法的话,自己平时要多加注意,不要在变量未声明前赋值,或者也可以开启严格模式,这样就会在不知情犯错时,收到报错警告,例如


function fn1() {
'use strict';
name = new Array(99999999)
}

fn1()

3. 分离的DOM节点


假设你手动移除了某个dom节点,本应释放该dom节点所占用的内存,但却因为疏忽导致某处代码仍对该被移除节点有引用,最终导致该节点所占内存无法被释放,例如这种情况


<div id="root">
<div class="child">我是子元素div>
<button>移除button>
div>
<script>
let btn = document.querySelector('button')
let child = document.querySelector('.child')
let root = document.querySelector('#root')

btn.
addEventListener('click', function() {
root.
removeChild(child)
})
script>


该代码所做的操作就是点击按钮后移除.child的节点,虽然点击后,该节点确实从dom被移除了,但全局变量child仍对该节点有引用,所以导致该节点的内存一直无法被释放,可以尝试用Memory的快照功能来检测一下,如图所示





同样的先记录一下初始状态的快照,然后点击移除按钮后,再点击一次快照,此时内存大小我们看不出什么变化,因为移除的节点占用的内存实在太小了可以忽略不计,但我们可以点击第二条快照记录,在筛选框里输入detached,于是就会展示所有脱离了却又未被清除的节点对象



解决办法如下图所示:


<div id="root">
<div class="child">我是子元素div>
<button>移除button>
div>
<script>
let btn = document.querySelector('button')

btn.
addEventListener('click', function() {
let child = document.querySelector('.child')
let root = document.querySelector('#root')

root.
removeChild(child)
})

script>


改动很简单,就是将对.child节点的引用移动到了click事件的回调函数中,那么当移除节点并退出回调函数的执行上文后就会自动清除对该节点的引用,那么自然就不会存在内存泄漏的情况了,我们来验证一下,如下图所示:




结果很明显,这样处理过后就不存在内存泄漏的情况了


4. 控制台的打印


<button>按钮button>
<script>
document.querySelector('button').addEventListener('click', function() {
let obj = new Array(1000000)

console.log(obj);
})
script>

我们在按钮的点击回调事件中创建了一个很大的数组对象并打印,用performance来验证一下




开始录制,先触发一次垃圾回收清除初始的内存,然后点击三次按钮,即执行了三次点击事件,最后再触发一次垃圾回收。查看录制结果发现JS Heap曲线成阶梯上升,并且最终保持的高度比初始基准线高很多,这说明每次执行点击事件创建的很大的数组对象obj都因为console.log被浏览器保存了下来并且无法被回收



接下来注释掉console.log,再来看一下结果:


<button>按钮button>
<script>
document.querySelector('button').addEventListener('click', function() {
let obj = new Array(1000000)

// console.log(obj);
})
script>


可以看到没有打印以后,每次创建的obj都立马被销毁了,并且最终触发垃圾回收机制后跟初始的基准线同样高,说明已经不存在内存泄漏的现象了


其实同理 console.log也可以用Memory来进一步验证


未注释 console.log



注释掉了console.log




最后简单总结一下:在开发环境下,可以使用控制台打印便于调试,但是在生产环境下,尽可能得不要在控制台打印数据。所以我们经常会在代码中看到类似如下的操作:



// 如果在开发环境下,打印变量obj
if(isDev) {
console.log(obj)
}


这样就避免了生产环境下无用的变量打印占用一定的内存空间,同样的除了console.log之外,console.errorconsole.infoconsole.dir等等都不要在生产环境下使用



5. 遗忘的定时器



定时器也是平时很多人会忽略的一个问题,比如定义了定时器后就再也不去考虑清除定时器了,这样其实也会造成一定的内存泄漏。来看一个代码示例:



<button>开启定时器button>
<script>

function fn1() {
let largeObj = new Array(100000)

setInterval(() => {
let myObj = largeObj
},
1000)
}

document.querySelector('button').addEventListener('click', function() {
fn1()
})
script>

这段代码是在点击按钮后执行fn1函数,fn1函数内创建了一个很大的数组对象largeObj,同时创建了一个setInterval定时器,定时器的回调函数只是简单的引用了一下变量largeObj,我们来看看其整体的内存分配情况吧:



按道理来说点击按钮执行fn1函数后会退出该函数的执行上下文,紧跟着函数体内的局部变量应该被清除,但图中performance的录制结果显示似乎是存在内存泄漏问题的,即最终曲线高度比基准线高度要高,那么再用Memory来确认一次:




  • 在我们点击按钮后,从动态内存分配的图上看到出现一个蓝色柱形,说明浏览器为变量largeObj分配了一段内存,但是之后这段内存并没有被释放掉,说明的确存在内存泄漏的问题,原因其实就是因为setInterval的回调函数内对变量largeObj有一个引用关系,而定时器一直未被清除,所以变量largeObj的内存也自然不会被释放

  • 那么我们如何来解决这个问题呢,假设我们只需要让定时器执行三次就可以了,那么我们可以改动一下代码:


<button>开启定时器button>
<script>
function fn1() {
let largeObj = new Array(100000)
let index = 0

let timer = setInterval(() => {
if(index === 3) clearInterval(timer);
let myObj = largeObj
index ++
},
1000)
}

document.querySelector('button').addEventListener('click', function() {
fn1()
})
script>

现在我们再通过performancememory来看看还不会存在内存泄漏的问题



  • performance




这次的录制结果就能看出,最后的曲线高度和初始基准线的高度一样,说明并没有内存泄漏的情况




  • memory



这里做一个解释,图中刚开始出现的蓝色柱形是因为我在录制后刷新了页面,可以忽略;然后我们点击了按钮,看到又出现了一个蓝色柱形,此时就是为fn1函数中的变量largeObj分配了内存,3s后该内存又被释放了,即变成了灰色柱形。所以我们可以得出结论,这段代码不存在内存泄漏的问题



简单总结一下: 大家在平时用到了定时器,如果在用不到定时器后一定要清除掉,否则就会出现本例中的情况。除了setTimeoutsetInterval,其实浏览器还提供了一个API也可能就存在这样的问题,那就是requestAnimationFrame




  • 好了好了,学完了,ui妹妹我来了






  • ui妹妹:去你m的,滚远点





好了兄弟们,内存泄漏学会了吗?


作者:顾昂_
来源:juejin.cn/post/7309040097936474175
收起阅读 »

一种适合H5屏幕适配方案

web
一、动态rem适配方案:适合H5项目的适配方案 1. @media媒体查询适配 首先,我们需要设置一个根元素的基准值,这个基准值通常根据视口宽度进行计算。可以在项目的 CSS 文件中,通过媒体查询动态调整根元素的 font-size。 html { fon...
继续阅读 »

一、动态rem适配方案:适合H5项目的适配方案


1. @media媒体查询适配


首先,我们需要设置一个根元素的基准值,这个基准值通常根据视口宽度进行计算。可以在项目的 CSS 文件中,通过媒体查询动态调整根元素的 font-size


html {
font-size: 16px; /* 默认基准值 */
}
...
@media (min-width: 1024px) {
html {
font-size: 14px; /* 适配较大屏幕 */
}
}
@media (min-width: 1440px) {
html {
font-size: 16px; /* 适配超大屏幕 */
}
}

2. PostCSS 插件(自动转换)实现 px2rem


手动转换 pxrem 可能很繁琐,因此可以使用 PostCSS 插件 postcss-pxtorem 来自动完成这一转换。


2.1 安装 postcss-pxtorem


首先,在项目中安装 postcss-pxtorem 插件:


npm install postcss-pxtorem --save-dev

2.2 配置 PostCSS


然后,在项目根目录创建或编辑 postcss.config.js 文件,添加 postcss-pxtorem 插件配置:


/* postcss.config.cjs  */
module.exports = {
plugins: {
'postcss-pxtorem': {
rootValue: 16, // 基准值,对应于根元素的 font-size
unitPrecision: 5, // 保留小数点位数
propList: ['*', '!min-width', '!max-width'], // 排除 min-width 和 max-width 属性
selectorBlackList: [], // 忽略的选择器
replace: true, // 替换而不是添加备用属性
mediaQuery: false, // 允许在媒体查询中转换 px
minPixelValue: 0 // 最小的转换数值
}
}
};
/* vite */
export default defineConfig({
css: {
postcss: './postcss.config.cjs',
}
})

3. 在 CSS/SCSS 中使用 px


在编写样式时,依然可以使用 px 进行布局:


.container {
width: 320px;
padding: 16px;
}

.header {
height: 64px;
margin-bottom: 24px;
}

4. 构建项目


通过构建工具(如 webpack/vite )运行项目时,PostCSS 插件会自动将 px 转换为 rem


image-20240613170746376

5. 可以不用@media媒体查询,动态动态调整font-size


为了实现更动态的适配,可以通过 JavaScript 动态设置根元素的 font-size


/**utils/setRootFontSize**/
function setRootFontSize(): void {
const docEl = document.documentElement;
const clientWidth = docEl.clientWidth;
if (!clientWidth) return;
const baseFontSize = 16; // 基准字体大小
const designWidth = 1920; // 设计稿宽度
docEl.style.fontSize = (baseFontSize * (clientWidth / designWidth)) + 'px';
}
export default setRootFontSize;

/**utils/setRootFontSize**/
/**APP**/
import setRootFontSize from '../utils/setRootFontSize';
import { useEffect } from 'react';

export default function App() {
useEffect(() => {
// 设置根元素的字体大小
setRootFontSize();
// 窗口大小改变时重新设置
window.addEventListener('resize', setRootFontSize);
// 清除事件监听器
return () => {
window.removeEventListener('resize', setRootFontSize);
};
}, []);

return (
<>
<div>
<MyRoutes />
</div>
</>

)
}
/**APP**/

这样,无论视口宽度如何变化,页面元素都会根据基准值动态调整大小,确保良好的适配效果。
通过上述步骤,可以实现布局使用 px,并动态转换为 rem 的适配方案。这个方案不仅使得样式编写更加简洁,还提高了适配的灵活性。


注:如果你使用了 setRootFontSize 动态调整根元素的 font-size,就不再需要使用 @media 查询来调整根元素的字体大小了。这是因为 setRootFontSize 函数已经根据视口宽度动态调整了 font-size,从而实现了自适应。



  1. 动态调整根元素 font-size 的优势

    • 更加灵活:可以实现更加平滑的响应式调整,而不是依赖固定的断点。

    • 统一管理:所有的样式都依赖根元素的 font-size,维护起来更加简单。



  2. @media 媒体查询的优势

    • 尽管不再需要用 @media 查询来调整根元素的 font-size,但你可能仍然需要使用 @media 查询来处理其他的响应式设计需求,比如调整布局、隐藏或显示元素等。




这种方式简化了响应式设计,使得样式统一管理更加简单,同时保留了灵活性和适应性。


6. 效果对比(非H5界面)


图一为界面px 适配,效果为图片,文字等大小固定不变。


图二为动态rem适配:整体随界面扩大而扩大,能够保持相对比例。


Screen-2024-06-13-155704-ezgif.com-video-to-gif-converter


t11b673bcd6119f4e6a5e9509cf


7. Tips



  • 动态rem此方案比较适合H5屏幕适配

  • 注意: PostCSS 转换rem应排除 min-widthmax-widthmin-heightmax-height ,以免影响整体界面


二、其他适配


1. 弹性盒模型(Flexbox)


Flexbox 是一种布局模型,能够轻松地实现响应式布局。它允许元素根据容器的大小自动调整位置和大小。


.container {
display: flex;
flex-wrap: wrap;
}

.item {
flex: 1 1 100%; /* 默认情况下每个元素占满一行 */
}

@media (min-width: 600px) {
.item {
flex: 1 1 50%; /* 在较宽的屏幕上,每个元素占半行 */
}
}

@media (min-width: 1024px) {
.item {
flex: 1 1 33.33%; /* 在更宽的屏幕上,每个元素占三分之一行 */
}
}

2. 栅格系统(Grid System)


栅格系统是一种常见的响应式布局方案,广泛应用于各种框架(如 Bootstrap)。通过定义行和列,可以轻松地创建复杂的布局。


.container {
display: grid;
grid-template-columns: 1fr; /* 默认情况下每行一个列 */
gap: 10px;
}

@media (min-width: 600px) {
.container {
grid-template-columns: 1fr 1fr; /* 在较宽的屏幕上,每行两个列 */
}
}

@media (min-width: 1024px) {
.container {
grid-template-columns: 1fr 1fr 1fr; /* 在更宽的屏幕上,每行三个列 */
}
}

3. 百分比和视口单位


使用百分比(%)、视口宽度(vw)、视口高度(vh)等单位,可以根据视口尺寸调整元素大小。


  /* 示例:百分比和视口单位 */
.container {
width: 100%;
height: 50vh; /* 高度为视口高度的一半 */
}

.element {
width: 50%; /* 宽度为容器的一半 */
height: 10vw; /* 高度为视口宽度的 10% */
}

4. 响应式图片


根据设备分辨率和尺寸加载不同版本的图片,以提高性能和视觉效果。可以使用 srcset 和 sizes 属性。


  <!-- 示例:响应式图片 -->
<img
src="small.jpg"
srcset="medium.jpg 600w, large.jpg 1024w"
sizes="(max-width: 600px) 100vw, (max-width: 1024px) 50vw, 33.33vw"
alt="Responsive Image">


5. CSS Custom Properties(CSS变量)


使用 CSS 变量可以更灵活地定义和调整样式,同时通过 JavaScript 动态改变变量值实现响应式设计。


:root {
--main-padding: 20px;
}

.container {
padding: var(--main-padding);
}

@media (min-width: 600px) {
:root {
--main-padding: 40px;
}
}

作者:奇舞精选
来源:juejin.cn/post/7384265691162886178
收起阅读 »

利用高德地图API实现实时天气

web
前言 闲来无事,利用摸鱼时间实现实时天气的小功能 目录 登录 高德开放平台控制台,如果没有开发者账号,请 注册开发者。 创建 key,进入应用管理,创建新应用,新应用中添加 key,服务平台选择 Web端(JS API)。 获取 key 和密钥 获取当前...
继续阅读 »

前言



闲来无事,利用摸鱼时间实现实时天气的小功能



目录



  1. 登录 高德开放平台控制台,如果没有开发者账号,请 注册开发者

  2. 创建 key,进入应用管理,创建新应用,新应用中添加 key,服务平台选择 Web端(JS API)。

  3. 获取 key 和密钥

  4. 获取当前城市定位

  5. 通过定位获取城市名称、区域编码,查询目标城市/区域的实时天气状况


效果图


这里样式我就不做处理了,地图可以不用做展示,只需要拿到获取到天气的结果,结合自己的样式展示就可以了,未来天气可以结合echarts进行展示,页面效果更佳


image.png


实现



  1. 登录高德开放平台控制台
    image.png

  2. 创建 key



这里应用名称可以随便取(个人建议功能名称或者项目称)



image.png


image.png


3.获取 key 和密钥


image.png


4.获取当前城市定位



首先,先安装依赖



npm install @amap/amap-jsapi-loader --save


或者


pnpm add @amap/amap-jsapi-loader --save



页面使用时引入即可 import AMapLoader from "@amap/amap-jsapi-loader"




/**在index.html引入密钥,不添加会导致某些API调用不成功*/

<script type="text/javascript">window._AMapSecurityConfig =
{securityJsCode: "安全密钥"}</script>

  /** 1.  调用AMapLoader.load方法,通过传入一个对象作为参数来指定加载地图时的配置信息。
* - key: 申请好的Web端开发者Key,是必填项,用于授权您的应用程序使用高德地图API。
* - version: 指定要加载的JSAPI版本,不指定时默认为1.4.15。
* - plugins: 需要使用的插件列表,如比例尺、缩放控件等。
*/

function initMap() {
AMapLoader.load({
key: "申请好的Web端开发者Key", // 申请好的Web端开发者Key,首次调用 load 时必填
version: "2.0", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
plugins: [
"AMap.ToolBar",
"AMap.Scale",
"AMap.HawkEye",
"AMap.MapType",
"AMap.Geolocation",
"AMap.Geocoder",
"AMap.Weather",
"AMap.CitySearch",
"AMap.InfoWindow",
"AMap.Marker",
"AMap.Pixel",
], // 需要使用的的插件列表,如比例尺'AMap.Scale'等
})
.then((AMap) => {
map.value = new AMap.Map("container", {
//设置地图容器id
resizeEnable: true,
viewMode: "3D", //是否为3D地图模式
zoom: 10, //初始化地图级别
center: locationArr.value, //初始化地图中心点位置
});

getGeolocation(AMap);
getCitySearch(AMap, map.value);
})
.catch((e) => {
console.log(e);
});
}

// 浏览器定位
const getGeolocation = (AMap: any) => {
const geolocation = new AMap.Geolocation({
enableHighAccuracy: true, //是否使用高精度定位,默认:true
timeout: 1000, //超过10秒后停止定位,默认:5s
position: "RB", //定位按钮的停靠位置
offset: [10, 20], //定位按钮与设置的停靠位置的偏移量,默认:[10, 20]
zoomToAccuracy: true, //定位成功后是否自动调整地图视野到定位点
});
map.value.addControl(geolocation);
geolocation.getCurrentPosition(function (status: string, result: any) {
if (status == "complete") {
onComplete(result);
} else {
onError(result);
}
});
};

// IP定位获取当前城市信息
const getCitySearch = (AMap: any, map: any) => {
const citySearch = new AMap.CitySearch();
citySearch.getLocalCity(function (
status: string,
result: {
city: string;
info: string;
}
) {
if (status === "complete" && result.info === "OK") {
console.log(
"🚀 ~ file: map-container.vue:88 ~ getCitySearch ~ result:",
result
);
// 查询成功,result即为当前所在城市信息
getWeather(AMap, map, result.city);
}
});
};


onMounted(() => {
initMap();
});

5.通过定位获取城市名称、区域编码,查询目标城市/区域的实时天气状况


const getWeather = (AMap: any, map: any, city: string) => {
const weather = new AMap.Weather();
weather.getLive(
city,
function (
err: any,
data: {
city: string;
weather: string;
temperature: string;
windDirection: string;
windPower: string;
humidity: string;
reportTime: string;
}
) {
if (!err) {
const str = [];
str.push("<h4 >实时天气" + "</h4><hr>");
str.push("<p>城市/区:" + data.city + "</p>");
str.push("<p>天气:" + data.weather + "</p>");
str.push("<p>温度:" + data.temperature + "℃</p>");
str.push("<p>风向:" + data.windDirection + "</p>");
str.push("<p>风力:" + data.windPower + " 级</p>");
str.push("<p>空气湿度:" + data.humidity + "</p>");
str.push("<p>发布时间:" + data.reportTime + "</p>");
const marker = new AMap.Marker({
map: map,
position: map.getCenter(),
});
const infoWin = new AMap.InfoWindow({
content:
'<div class="info" style="position:inherit;margin-bottom:0;background:#ffffff90;padding:10px">' +
str.join("") +
'</div><div class="sharp"></div>',
isCustom: true,
offset: new AMap.Pixel(0, -37),
});
infoWin.open(map, marker.getPosition());
marker.on("mouseover", function () {
infoWin.open(map, marker.getPosition());
});
}
}
);
}

完整代码


 <template>
<div id="container"></div>
</template>

<script setup lang="ts">
import AMapLoader from "@amap/amap-jsapi-loader";
import { ref, onMounted, watch, reactive } from "vue";
const props = defineProps({
search: {
type: String,
default: "杭州市",
},
});

const isFalse = ref(false);

const map = ref<any>(null);
let locationArr = ref<any>();
watch(
() => props.search,
(newValue) => {
console.log("search", newValue);
initMap();
}
);
function initMap() {
AMapLoader.load({
key: "申请好的Web端开发者Key", // 申请好的Web端开发者Key,首次调用 load 时必填
version: "2.0", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
plugins: [
"AMap.ToolBar",
"AMap.Scale",
"AMap.HawkEye",
"AMap.MapType",
"AMap.Geolocation",
"AMap.Geocoder",
"AMap.Weather",
"AMap.CitySearch",
"AMap.InfoWindow",
"AMap.Marker",
"AMap.Pixel",
], // 需要使用的的插件列表,如比例尺'AMap.Scale'等
})
.then((AMap) => {
map.value = new AMap.Map("container", {
//设置地图容器id
resizeEnable: true,
viewMode: "3D", //是否为3D地图模式
zoom: 10, //初始化地图级别
center: locationArr.value, //初始化地图中心点位置
});

getGeolocation(AMap);
getCitySearch(AMap, map.value);
})
.catch((e) => {
console.log(e);
});
}

// 浏览器定位
const getGeolocation = (AMap: any) => {
const geolocation = new AMap.Geolocation({
enableHighAccuracy: true, //是否使用高精度定位,默认:true
timeout: 1000, //超过10秒后停止定位,默认:5s
position: "RB", //定位按钮的停靠位置
offset: [10, 20], //定位按钮与设置的停靠位置的偏移量,默认:[10, 20]
zoomToAccuracy: true, //定位成功后是否自动调整地图视野到定位点
});
map.value.addControl(geolocation);
geolocation.getCurrentPosition(function (status: string, result: any) {
if (status == "complete") {
onComplete(result);
} else {
onError(result);
}
});
};

// IP定位获取当前城市信息
const getCitySearch = (AMap: any, map: any) => {
const citySearch = new AMap.CitySearch();
citySearch.getLocalCity(function (
status: string,
result: {
city: string;
info: string;
}
) {
if (status === "complete" && result.info === "OK") {
console.log(
"🚀 ~ file: map-container.vue:88 ~ getCitySearch ~ result:",
result
);
// 查询成功,result即为当前所在城市信息
getWeather(AMap, map, result.city);
}
});
};

// 天气
const getWeather = (AMap: any, map: any, city: string) => {
const weather = new AMap.Weather();
weather.getLive(
city,
function (
err: any,
data: {
city: string;
weather: string;
temperature: string;
windDirection: string;
windPower: string;
humidity: string;
reportTime: string;
}
) {
console.log("🚀 ~ file: map-container.vue:96 ~ .then ~ data:", data);
if (!err) {
const str = [];
str.push("<h4 >实时天气" + "</h4><hr>");
str.push("<p>城市/区:" + data.city + "</p>");
str.push("<p>天气:" + data.weather + "</p>");
str.push("<p>温度:" + data.temperature + "℃</p>");
str.push("<p>风向:" + data.windDirection + "</p>");
str.push("<p>风力:" + data.windPower + " 级</p>");
str.push("<p>空气湿度:" + data.humidity + "</p>");
str.push("<p>发布时间:" + data.reportTime + "</p>");
const marker = new AMap.Marker({
map: map,
position: map.getCenter(),
});
const infoWin = new AMap.InfoWindow({
content:
'<div class="info" style="position:inherit;margin-bottom:0;background:#ffffff90;padding:10px">' +
str.join("") +
'</div><div class="sharp"></div>',
isCustom: true,
offset: new AMap.Pixel(0, -37),
});
infoWin.open(map, marker.getPosition());
marker.on("mouseover", function () {
infoWin.open(map, marker.getPosition());
});
}
}
);

// 未来4天天气预报
weather.getForecast(
city,
function (err: any, data: { forecasts: string | any[] }) {
console.log(
"🚀 ~ file: map-container.vue:186 ~ getWeather ~ data:",
data
);

if (err) {
return;
}
var strs = [];
for (var i = 0, dayWeather; i < data.forecasts.length; i++) {
dayWeather = data.forecasts[i];
strs.push(
`<p>${dayWeather.date}&nbsp&nbsp${dayWeather.dayWeather}&nbsp&nbsp${dayWeather.nightTemp}~${dayWeather.dayTemp}℃</p><br />`
);
}
}
);
};

function onComplete(data: any) {
console.log("🚀 ~ file: map-container.vue:107 ~ onComplete ~ data:", data);
const lngLat = [data.position.lng, data.position.lat];
locationArr.value = lngLat;
}

function onError(data: any) {
console.log("🚀 ~ file: map-container.vue:113 ~ onError ~ data:", data);
// 定位出错
}

onMounted(() => {
initMap();
});
</script>

<style scoped lang="less">
#container {
padding: 0px;
margin: 0px;
width: 100%;
height: 100%;
}
</style>


作者:快乐是Happy
来源:juejin.cn/post/7316746866040619035
收起阅读 »

离职前同事将下载大文件功能封装成了npm包,赚了145块钱

web
这几天有个同事离职了,本来那是last day,还有半个小时,但他还在那里勤勤恳恳的写着代码。我很好奇,就问:老张,你咋还不做好准备,都要撤了,还奋笔疾书呐。他说:等会儿和你说。 等了半个小时,他说:走,一起下班。我跟你说个好东西。 我说:好的。 老张一边走...
继续阅读 »

这几天有个同事离职了,本来那是last day,还有半个小时,但他还在那里勤勤恳恳的写着代码。我很好奇,就问:老张,你咋还不做好准备,都要撤了,还奋笔疾书呐。他说:等会儿和你说。



等了半个小时,他说:走,一起下班。我跟你说个好东西。


我说:好的。


老张一边走一边跟我说:公司的下载大文件代码不好。


我说哪里不好了,不是都用了很久了。


他说,那些代码,每次项目需要的时候,还得拷过来拷过去的,有时候拷着拷着就拷丢了,还得去网上现找代码,很不好。


我问:那然后呢?


他说:我这两天把这段代码封装了一下,封装成了npm包。以后,大家就直接调用就可以了,不用重复造轮子,或者担心轮子走丢了。我说那太好了。


他说:我把这个npm包给你,以后你就说自己写的,下个季度晋升的时候,你就说,为公司解决了代码冗余,重复造轮子的问题,而且让下载大文件功能更加便捷,节省开发时间,提升了开发效率。


我说:那怎么好啊,得请你吃个饭啊,你都要走了。不过,你先跟我说说,怎么用这个npm包啊。



下载大文件版


比如我们现有的成形的项目,大家使用axios或者fetch,一定在项目里已经封装好了请求,所以直接调用服务端给的请求地址,获取到blob数据流信息就可以了。但是拿到blob数据流以后,这段代码得四处拷贝,重复造轮子,很不好。所以可以这样使用,高效、便捷。


下载js-tool-big-box工具包


执行安装命令



npm install js-tool-big-box



项目中引入ajaxBox对象,下载文件的公共方法,downFile 在这个对象下面。


import { ajaxBox } from 'js-tool-big-box';

调用实现下载


比如你在项目中已经封装好了axios或者fetch的实现,那么只需要正常发送请求,然后调用方法即可,使用非常方便。


fetch('https://test.aaa.com/getPDF').then(res => res.blob()).then((blob) => {
ajaxBox.downFile(blob, '优乐的美.pdf');
});

在这个方法中,你只要将接口返回的信息流转为blob流,然后传入 downFile 方法中,然后再传入一个参数做为下载后的文件名即可。


fetch请求 + 下载实现版本


我又问他,的确是很多项目里,请求都已经封装好了。但我之前做过一个项目,功能很简单,大部分都是展示类的。但产品在一个详情页,让我加下载功能,我的请求并没有做封装。


然后呢,服务端告诉我,这个下载文件的接口,还需要传入参数params,需要传入headers,你这个方法就不适用了吧?


他想了一下,说。也是可以的,你听我说啊。


定义请求参数们


const url = 'https://test.aaaa.com/getPDF';
const headers = {
'Content-Type':'application/x-www-form-urlencoded;charset=UTF-8',
'CCC-DDD': 'js-tool-big-box-demo-header'
}
const params = {
name: '经海路大白狗',
startDate: '2024-03-05',
endDate: '2024-04-05',
}

你看这些参数了吗?url就是下载文件需要的那个接口,如果是get请求呢,你就按照get形式把参数拼接上去,如果是post形式呢,你就需要后面的这个params变量做为入参数据。如果服务端需要headers呢,你就再将headers定义好,准备往过传。


调用实现


ajaxBox.downFileFetch(url, '相的约奶的茶.mp4', 'get', headers, dataParams);

你看到这个 downFileFetch 方法了吧,他也在 ajaxBox 对象下面。



第一个参数呢,表示服务端接口,如果是get请求呢,就把参数拼接上去;


第二个参数呢,表示下载后文件名,比如 down.pdf 这样;


第三个参数呢,默认是get请求,如果不想写get呢,你就写个null,但是你得写进去,如果服务端要求是个post请求呢,你就写post;
第四个参数呢,就是headers啦,服务端需要你就传过去,不需要你就写个null;
第五个参数呢,如果是psot请求,你就传入json对象过去,如果没有参数,你就不写也行,写个null也行。



我说:你这个工具库真是棒,js-tool-big-box,就是前端JS的一个大盒子啊。他说:是的,里面还有很多特别实用的方法,用了这个工具库后,前端项目可以少些很多公共方法,少引很多第三方库,很不错的。我也要离职了,你在公司就说这是你开发的。


我说:那我得请你吃饭啊。于是,我去买了一瓶茅台王子酒,花了260元,定了两份炒饼,花了30元。


等吃完,我说,你这个工具库可以啊,直接从我这里挣了290元。


他说:看你说的,酒你喝了一半,炒饼你吃了一份。我这顶多也就是145元啊。


看完不过瘾?这里有更全的js-tool-big-box使用指南哦,掘金链接直达(只会Vue的我,一入职就让用React,用了这个工具库,我依然高效 - 掘金 (juejin.cn))




最后告诉你个消息:
js-tool-big-box的npm地址是(js-tool-big-box 的npm地址)


js-tool-big-box的git仓库地址(js-tool-big-box的代码仓库地址)


作者:经海路大白狗
来源:juejin.cn/post/7379524605104848946
收起阅读 »

掘金滑块验证码安全升级,继续破解

web
去年发过一篇文章,《使用前端技术破解掘金滑块验证码》,我很佩服掘金官方的气度,不但允许我发布这篇文章,还同步发到了官方公众号。最近发现掘金的滑块验证码升级了,也许是我那篇文章起到了一些作用,逼迫官方加强了安全性,这是一个非常好的现象。不过,这并不是终点,我们还...
继续阅读 »

去年发过一篇文章,《使用前端技术破解掘金滑块验证码》,我很佩服掘金官方的气度,不但允许我发布这篇文章,还同步发到了官方公众号。最近发现掘金的滑块验证码升级了,也许是我那篇文章起到了一些作用,逼迫官方加强了安全性,这是一个非常好的现象。

不过,这并不是终点,我们还是可以继续破解。验证码的安全性是在用户体验和安全性之间的一个平衡,如果安全性太高,用户体验就会变差,如果用户体验太好,安全性就会变差。掘金的滑块验证码是一个很好的例子,它的安全性和用户体验之间的平衡做得非常好,并且我们破解的难度体验也非常好。 😄

本次升级的内容

掘金的滑块验证码升级了,主要有以下几个方面的改进:

  1. 首先验证码不再是掘金自己的验证码了,而是使用了字节的校验服务,可以看到弹窗是一个 iframe,并且域名是 bytedance.com

我们都知道掘金被字节收购了,可以猜测验证码的升级是字节跳动的团队做的。

  1. 验证码的图形不再是拼图,而是随机的不同形状,比如爱心、六角星、圆环、月亮、盾牌等。
  2. 增加了干扰缺口,主要是大小或旋转这种操作。

下面看一下改版后的滑块验证码:

我在文章的评论区看到了一些关于这次升级或相关的讨论:

本文将继续破解这次升级后的滑块验证码,看看这次升级对破解的难度有多大影响,如果你还没有了解过如何破解滑块验证码,请先看我之前的文章。

iframe

这次升级,整个滑块都掉用的是外部链接,使用 iframe 呈现,那么在 puppeteer 中如何处理呢?

await page.waitForSelector('iframe');
const elementHandle = await page.$('iframe');
const frame = await elementHandle.contentFrame();

实际上,我们只需要等待 iframe 加载完成,然后获取 iframe 的内容即可。

Frame 对象和 Page 对象有很多相似的方法,比如 frame.$frame.evaluate 等,我们可以直接使用这些方法来操作 iframe 中的元素。

验证码的识别

上一篇文章采用比较简单的判断方式,当时缺口处有明显的白边,所以只需要找到这个白边即可。

但是本次升级后,缺口不再是白边,而是阴影的效果,并且缺口的形状也不再是拼图,大概率都是曲线的边,所以再判断缺口的方式就不再适用了。

现在我们可以采用一种新的方式,通过对比滑块图片和缺口区域的像素值相似程度来判断缺口位置。

首先还是二值化处理,将图片转换为黑白两色:

可以看到左侧缺口和右侧缺口非常相似,只是做了一点旋转作为干扰。

再看一下,iframe 中还有一个很重要的东西,就是校验的图片:

 

它是一个 png 图片,所以我们可以把它也转换成二值化,简单的方式就是将透明色转换为白色,非透明色转换为黑色,如果想提高识别精度,可以与背景图一样,通过灰度、二值化的转换方式。

// 获取缺口图像
const captchaVerifyImage = document.querySelector(
'#captcha-verify_img_slide',
) as HTMLImageElement;
// 创建一个画布,将 image 转换成canvas
const captchaCanvas = document.createElement('canvas');
captchaCanvas.width = captchaVerifyImage.width;
captchaCanvas.height = captchaVerifyImage.height;
const captchaCtx = captchaCanvas.getContext('2d');
captchaCtx.drawImage(
captchaVerifyImage,
0,
0,
captchaVerifyImage.width,
captchaVerifyImage.height,
);
const captchaImageData = captchaCtx.getImageData(
0,
0,
captchaVerifyImage.width,
captchaVerifyImage.height,
);
// 将像素数据转换为二维数组,同样处理灰度、二值化,将像素点转换为0(黑色)或1(白色)
const captchaData: number[][] = [];
for (let h = 0; h < captchaVerifyImage.height; h++) {
captchaData.push([]);
for (let w = 0; w < captchaVerifyImage.width; w++) {
const index = (h * captchaVerifyImage.width + w) * 4;
const r = captchaImageData.data[index] * 0.2126;
const g = captchaImageData.data[index + 1] * 0.7152;
const b = captchaImageData.data[index + 2] * 0.0722;
if (r + g + b > 30) {
captchaData[h].push(0);
} else {
captchaData[h].push(1);
}
}
}

为了对比图形的相似度,二值化后的数据我们页采用二维数组的方式存储,这样可以方便的对比两个图形的相似度。

如果想观测二值化后的真是效果,可以把二位数组转换为颜色,并覆盖到原图上:

// 通过 captchaData 0 黑色  1 白色 的值,绘制到 canvas 上,查看效果
for (let h = 0; h < captchaVerifyImage.height; h++) {
for (let w = 0; w < captchaVerifyImage.width; w++) {
captchaCtx.fillStyle =
captchaData[h][w] == 1 ? 'rgba(0,0,0,0)' : 'black';
captchaCtx.fillRect(w, h, 1, 1);
}
}
captchaVerifyImage.src = captchaCanvas.toDataURL();

数据拿到后,我们可以开始对比两个图形的相似度,这里就采用非常简单的对比方式,从左向右,逐个像素点对比,横向每个图形的像素一致的点数量纪录下来,然后取最大值,这个最大值就是缺口的位置。

这里我们先优化一下要对比的数据,我们只需要对比缺口的顶部到底部这段的数据,截取这一段,可以减少对比的性能消耗。

// 获取captchaVerifyImage 相对于 .verify-image 的偏移量
const captchaVerifyImageBox = captchaVerifyImage.getBoundingClientRect();
const captchaVerifyImageTop = captchaVerifyImageBox.top;
// 获取缺口图像的位置
const imageBox = image.getBoundingClientRect();
const imageTop = imageBox.top;
// 计算缺口图像的位置,top 向上取整,bottom 向下取整
const top = Math.floor(captchaVerifyImageTop - imageTop);
// data 截取从 top 列到 top + image.height 列的数据
const sliceData = data.slice(top, top + image.height);

然后循环对比两个图形的像素点,计算相似度:

// 循环对比 captchaData 和 sliceData,从左到右,每次增加一列,返回校验相同的数量
const equalPoints = [];
// 从左到右,每次增加一列
for (let leftIndex = 0; leftIndex < sliceData[0].length; leftIndex++) {
let equalPoint = 0;
// 新数组 sliceData 截取 leftIndex - leftIndex + captchaVerifyImage.width 列的数据
const compareSliceData = sliceData.map((item) =>
item.slice(leftIndex, leftIndex + captchaVerifyImage.width),
);
// 循环判断 captchaData 和 compareSliceData 相同值的数量
for (let h = 0; h < captchaData.length; h++) {
for (let w = 0; w < captchaData[h].length; w++) {
if (captchaData[h][w] === compareSliceData[h][w]) {
equalPoint++;
}
}
}
equalPoints.push(equalPoint);
}
// 找到最大的相同数量,大概率为缺口位置
return equalPoints.indexOf(Math.max(...equalPoints));

对比时像素较多,不容易直接看到效果,这里写一个简单的二位数组对比,方便各位理解:

[
[0, 1, 0],
[1, 0, 1],
[0, 1, 0],
]
[
[0, 0, 0, 1, 0, 0],
[0, 0, 1, 0, 1, 0],
[0, 0, 0, 1, 0, 0],
]

循环对比,那么第3列开始,匹配的数量可以达到9,所以返回 3,这样就是滑块要移动的位置。

干扰缺口其实对我们这个识别方式没什么影响,最多可能会增加一些失败的概率,我个人测试了一下,识别成功率有 95% 左右。

总结

这次升级后,掘金的滑块验证码的安全性有了一定的提升,还是可以继续破解的,只是难度有所增加。最后再奉劝大家不要滥用这个技能,这只是为了学习和研究,不要用于非法用途。如果各位蹲局子,可不关我事啊。 🤔️


作者:codexu
来源:juejin.cn/post/7376276140595888137
收起阅读 »

队友升职,被迫解锁 Jenkins(所以,前端需要学习Jenkins吗?🤔)

web
入坑 Jenkins 作为一个前端,想必大家都会有这个想法:“Jenkins 会用就行了,有啥好学的”。 我一直都是这么想的,不就会点个开始构建就行了嘛! 可是碰巧我们之前负责 Jenkins 的前端同事升了职,碰巧这个项目组就剩了两个人,碰巧我比较闲,于是这...
继续阅读 »

入坑 Jenkins


作为一个前端,想必大家都会有这个想法:“Jenkins 会用就行了,有啥好学的”。


我一直都是这么想的,不就会点个开始构建就行了嘛!


可是碰巧我们之前负责 Jenkins 的前端同事升了职,碰巧这个项目组就剩了两个人,碰巧我比较闲,于是这个“活”就落在我的头上了。


yali.jpeg


压力一下就上来了,一点不懂 Jenkins 可咋整?


然而现实是没有一点儿压力。


刚开始的时候挺轻松,也就是要发版的流程到我这了,我直接在对应项目上点击开始构建,so easy!可是某一天,突然遇到一个 bug:我们每次 web 端项目发完后,桌面端的 hybrid 包需要我手动改 OSS 上配置文件的版本号,正巧那天忘记更新版本号了,导致桌面端应用本地的 hybrid 没有更新。。。


领导:你要不就别手动更新了,弄成自动化的

我:😨 啊!什么,我我我不会,是不可能的


小弟我之前没有接触过 Jenkins,看着那一堆配置着实有点费脑,于是就只能边百度学习边输出,从 Jenkins 安装开始到配置不同类型的构建流程,踩过不少坑,最后形成这篇文章。如果有能帮到大家的点,我就很开心了,毕竟我也是刚接触的!


说说我经历过的前端部署流程


按照我的经历,我把前端部署流程分为了以下几个阶段:即原始时代 -> 脚本化时代 -> CI/CD 时代。


jenkins-history.png


原始时代


最开始的公司运维是一个小老头,他只负责管理服务器资源,不管各种项目打包之类的。我们就只能自己打包,再手动把构建的文件丢到服务器上。


整体流程就是:本地合并代码 --> 本地打包 --> 上传服务器;


上传服务器可以分为这几个小步骤:打开 xshell --> 连接服务器 --> 进入 tomcat 目录 --> 通过 ftp 上传本地文件。


可能全套下来需要 5 分钟左右。


脚本化时代


为了简化,我写了一个 node 脚本,通过ssh2-sftp-client上传服务器这一步骤脚本化:


const chalk = require('chalk')
const path = require('path')
const fs = require('fs')
const Client = require('ssh2-sftp-client')
const sftp = new Client()
const envConfig = require('./env.config')

const defalutConfig = {
port: '22',
username: 'root',
password: '123',
localStatic: './dist.tar.gz',
}

const config = {
...defalutConfig,
host: envConfig.host,
remoteStatic: envConfig.remoteStatic,
}

const error = chalk.bold.red
const success = chalk.bold.green
function upload(config, options) {
if (!fs.existsSync('./dist') && !fs.existsSync(options.localStatic)) {
return
}
// 标志上传dist目录
let isDist = false
sftp
.connect(config)
.then(() => {
// 判断gz文件存在时 上传gz 不存在时上传dist
if (fs.existsSync(options.localStatic)) {
return sftp.put(options.localStatic, options.remoteStatic)
} else if (fs.existsSync('./dist')) {
isDist = true
return sftp.uploadDir('./dist', options.remoteStatic.slice(0, -12))
}
})
.then(() => {
sftp.end()
if (!isDist) {
const { Client } = require('ssh2')
const conn = new Client()
conn
.on('ready', () => {
// 远程解压
const remoteModule = options.remoteStatic.replace('dist.tar.gz', '')
conn.exec(
`cd ${remoteModule};tar xvf dist.tar.gz`,
(err, stream) => {
if (err) throw err
stream
.on('close', (code) => {
code === 0
conn.end()
// 解压完成 删除本地文件
fs.unlink(options.localStatic, (err) => {
if (err) throw err
})
})
.on('data', (data) => {})
}
)
})
.connect(config)
}
})
.catch((err) => {
sftp.end()
})
}

// 上传文件
upload(config, {
localStatic: path.resolve(__dirname, config.localStatic), // 本地文件夹路径
remoteStatic: config.remoteStatic, // 服务器文件夹路径器
})

upload-dist.png


最后只要通过执行yarn deploy即可实现打包并上传,用了一段时间,队友也都觉得挺好用的,毕竟少了很多手动操作,效率大大提升。


CI/CD 时代


不过用了没多久后,来了个新的运维小年轻,一上来就整了个 Jenkins ,取代了我们手动打包的过程,只要我们点击部署就可以了,当时就感觉 Jenkins 挺方便的,但又觉得和前端没多大关系,也就没学习。


不过也挺 Jenkins 的,为啥呢?



当时和测试说的最多的就是“我在我这试试.....我这没问题啊,你刷新一下”,趁这个时候,赶紧打包重新部署下。有了 Jenkins 后,打包都有记录了,测试一看就知道我在哄她了 🙄



Jenkins 解决了什么问题


我觉得在了解一个新事物前,应该先了解下它的出现解决了什么问题。


以我的亲身经历来看,Jenkins 的出现使得 拉取代码 -> 打包 -> 部署 -> 完成后工作(通知、归档、上传CDN等)这一繁琐的流程不需要人为再去干预,一键触发 🛫。


jenkins-vs-old.png


只需要点击开始构建即可,如何你觉得还得每次打开 jenkins 页面去点击构建,可以通过设置代码提交到 master 或合并代码时触发构建,这样就不用每次手动去点击构建了,省时更省力 🚴🏻‍♂️。


Jenkins 部署


Jenkins 中文帮助文档


Jenkins 提供了多种安装方式,我的服务器是 Centos,按照官方教程进行部署即可。


官方提供两种方式进行安装:


方式一:


sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo
sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key

yum install jenkins

方式二:


直接下载 rpm 包进行安装,地址:mirrors.jenkins-ci.org/redhat/


wget https://pkg.jenkins.io/redhat/jenkins-2.449-1.1.noarch.rpm
rpm -ivh jenkins-2.449-1.1.noarch.rpm

安装过程


我是使用方式二进行安装的,来看下具体过程。


首先需要安装 jdk17 以上的版本



  1. 下载对应的 jdk


    wget https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.tar.gz


  2. 解压并放到合适位置


    tar xf jdk-17_linux-x64_bin.tar.gz
    mv jdk-17.0.8/ /usr/lib/jvm


  3. 配置 Java 环境变量


    vim /etc/profile
    export JAVA_HOME=/usr/lib/jvm/jdk-17.0.8
    export CLASSPATH=$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATH
    export PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH


  4. 验证


    java -version

    jenkins-java-version.png



接着安装 Jenkins,需要注意:Jenkins 一定要安装最新版本,因为插件要求最新版本,最新的 2.449。



  1. 下载 rpm 包


    cd /usr/local/jenkins
    wget https://mirrors.jenkins-ci.org/redhat/jenkins-2.449-1.1.noarch.rpm


  2. 安装 Jenkins


    rpm -ivh jenkins-2.449-1.1.noarch.rpm


  3. 启动 Jenkins


    systemctl start jenkins



jenkins-install-error.png


你以为就这么简单?肯定会报错的,通过百度报错信息,报错原因是:Java 环境不对,百度到的解决方法:


修改/etc/init.d/jenkins文件,添加 JDK,但是目录下并没有这个文件,继续百度得知:


使用 systemctl 启动 jenkins 时,不会使用 etc/init.d/jenkins 配置文件,而是使用 /usr/lib/systemd/system/jenkins.service文件


于是修改:


vim /usr/lib/systemd/system/jenkins.service

jenkins-service-java.png


搜索 Java,找到上面这一行,打开注释,修改为对应的 JDK 位置:


Environment="JAVA_HOME=/usr/lib/jvm/jdk-17.0.10"

重新启动 Jenkins:


systemctl restart jenkins

查看启动状态,出现如下则说明 Jenkins 启动完成:


jenkins-install-success.png
接着在浏览器通过 ip:8090 访问,出现如下页面,说明安装成功。


jenkins-install-success-ip.png


此时需要填写管理员密码,通过 cat /var/lib/jenkins/secrets/initialAdminPassword 即可获取。


Jenkins 配置


出现上述界面,填写密码成功后等待数秒,即可出现如下界面:


jenkins-install-plugins.png


选择 安装推荐的插件


jenkins-install-plugins-wait.png


这个过程稍微有点慢,可以整理整理文档,等待安装完成。


安装完成后,会出现此页面,需要创建一个管理员用户。


jenkins-install-ok.png


点击开始使用 Jenkins,即可进入 Jenkins 首页。


jenkins-home.png


至此,Jenkins 安装完成 🎉🎉🎉。


安装过程遇到的问题



  1. 没有经验第一次安装,参考网上文档推荐的是 JDK8,结果安装的 Jenkins 至少需要 JDK 11,导致安装失败;

  2. 第二次安装,按照网上的文档安装,不是最新版本,导致部分插件安装失败;


    release

    版本


  3. 配置修改问题



    • Jenkins 默认的配置文件位于 /usr/lib/systemd/system/jenkins.service

    • 默认目录安装在 /var/lib/jenkins/

    • 默认工作空间在 /var/lib/jenkins/workspace



  4. 修改端口号为 8090


    vim /usr/lib/systemd/system/jenkins.service

    修改 Environment="JENKINS_PORT=8090",修改完后执行:


    systemctl daemon-reload
    systemctl restart jenkins



如何卸载 Jenkins


安装过程遇到了不少坑,基本都是卸载了重新安装,于是就总结了以下卸载的命令。


# 查找是否存在 Jenkins 安装包
rpm -ql jenkins
# 卸载 Jenkins
rpm -e jenkins
# 再次查看 此时会提示:未安装软件包 jenkins
rpm -ql jenkins
# 删除所有 Jenkins 相关目录
find / -iname jenkins | xargs -n 1000 rm -rf

Jenkins 版本更新


Jenkins 发布版本很频繁,基本为一周一次,参考 Jenkins 更新


项目创建


点击 + 新建Item,输入名称,选择类型:


jenkins-create-project.png


有多种类型可供选择,这里我们主要讲这两种:Freestyle project 和 Pipeline。


Freestyle project


jenkins-create-freestyle.jpeg


选择这种类型后,就可以通过各种 web 表单(基础信息、源码、构建步骤等),配置完整的构建步骤,对于新手来说,易上手且容易理解,如果第一次接触,创建项目就选择 Freestyle project 即可。


总共有以下几个环节需要配置:



  • General

  • 源码管理

  • 构建触发器

  • 构建环境

  • Build Steps

  • 构建后操作


此时我们点击 OK,创建完如下所示都是空白的,也可以通过创建时的复制选项,复制之前项目的配置:


jenkins-create-configure.png


接着就如同填写表单信息,一步步完成构建工作。


General


项目基本信息也就是对所打包项目的描述信息:


jenkins-configure-general.png


比如描述这里,可以写项目名称、描述、输出环境等等。


Discard old builds 丢弃旧的构建

可以理解为清初构建历史,Jenkins 每打包一次就会产生一个构建历史记录,在构建历史中可以看到从第一次到最新的构建信息,这会导致磁盘空间消耗。


点击配置名称或勾选,会自动展开配置项。这里我们可以设置保持构建的最大个数5,则当前项目的构建历史记录只会保留最新的 5 个,并自动删除掉最老的构建。


jenkins-configure-discard.png


这个可以按照自己的需求来设置,比如保留 7 天的构建记录或保留最多 100 个构建记录。


Jenkins 的大多数配置都有 高级 选项,在高级选项中可以做更详细的配置。


This project is parameterized

可以理解为此构建后续过程可能用到的参数,可以是手动输入或选项等,如:git 分支、构建环境、特定的配置等等。通过这种方式,项目可以更加灵活和可配置,以适应不同的构建需求和环境。


默认有 8 种参数类型:



  1. Boolean Parameter: checkbox 选择,如果需要设置 true/false 值时,可以添加此参数类型

  2. Choice Parameter:选择,多个选项

  3. Credentials Parameter:账号证书等参数

  4. File Parameter:文件上传

  5. Multi-line String parameters:多行文本参数

  6. Password Parameter:密码参数

  7. Run Parameter:用于选择执行的 job

  8. String Parameter:单行文本参数


Git Parameter 需要在 系统管理 -> 插件管理 搜索 Git Parameter 插件进行安装,安装完成后重启才会有这个参数。


通过 添加参数 来设置后续会用到的参数,比如设置名称为 delopyTagGit Parameter 参数来指定要构建的分支,设置名称为 DEPLOYPATHChoice Parameter 参数来指定部署环境等等。


jenkins-configure-parameter.png


源码管理


Repositories

一般公司项目都是从 gitlab 上拉代码,首先设置 Repository URL,填写 git 仓库地址,比如:https://gitlab.com/xxx/xxx.git


填写完后会报错如下:


jenkins-configure-git-error.png


可以通过添加 Credentials 凭证解决,在 Jenkins 中,Git 的 Credentials 是用于访问 Git 仓库的认证信息,这些凭据可以是用户名和密码、SSH 密钥或其他认证机制,以确保 Jenkins 能够安全的与 Git 仓库进行交互,即构建过程中自动拉取代码、执行构建任务等


方式一:在当前页面填写帐号、密码

选择添加 -> Jenkins -> 填写 git 用户名、密码等信息生成一个新的 Credentials,然后重新选择我们刚刚添加的 Credentials,报错信息自动消失


jenkins-configure-git.png


这样添加会有一个问题,就是如果有多个项目时,每次都需要手动填写 Git 账户和密码信息。


方式二:Jenkins 全局凭证设置

Global Credentials 中设置全局的凭证。


jenkins-configure-git-credentials.png


然后在项目中配置时可以直接选择我们刚刚添加的 Credentials,报错信息自动消失。


Branches to build

这里构建的分支,可以设置为我们上面设置的 delopyTag 参数,即用户自己选择的分支进行构建。


构建触发器


特定情况下出发构建,如定时触发、代码提交或合并时触发、其他任务完成时触发等。


如果没有特殊的要求时,这一步完全可以不用设置,在需要构建时我们只需要手动点击开始构建即可。


构建环境


构建环境是在构建开始之前的准备工作,如清除上次构建、指定构建工具、设置 JDK 、Node 版本、生成版本号等。


Provide Node & npm bin/folder to PATH

默认是没有这一项的,但前端部署需要 Node 环境支持,所以需要在 系统管理 -> 插件管理 搜索 nodejs 插件进行安装,安装完成后重启才会展示这项配置。


但此时还是不能选择的,需要在 系统管理 -> 全局工具配置 中先安装 NodeJs,根据不同环境配置,可同时安装多个 NodeJs 版本


jenkins-configure-nodeJs.png


之后在 Provide Node 处才有可供选择的 Node 环境。


jenkins-configure-provide-node.png


Create a formatted version number

这个就是我用来解决了一开始问题的配置项,也就是把每次打包的结果上传到 OSS 服务器上时生成一个新的版本号,在 Electron 项目中通过对比版本号,自动更新对应的 hybrid 包,领导都爱上我了 😜。


首先需要安装插件 Version Number Plugin,在 系统管理 -> 插件管理 中搜索安装,然后重启 Jenkins 即可


jenkins-configure-version.png



  1. Environment Variable Name


    类似于第一步的构建参数,可以在其他地方使用。


  2. Version Number Format String


    用于设置版本号的格式,如1.x.x,Jenkins 提供了许多内置的环境变量:



    • BUILD_DAY:生成的日期

    • BUILD_WEEK:生成年份中的一周

    • BUILD_MONTH:生成的月份

    • BUILD_YEAR:生成的年份

    • BUILDS_TAY:在此日历日期完成的生成数

    • BUILDS_THIS_WEEK:此日历周内完成的生成数

    • BUILDS_THIS_MONTH:此日历月内完成的生成数

    • BUILDS_THIS_YEAR:此日历年中完成的生成数

    • BUILDS_ALL_TIME:自项目开始以来完成的生成数



  3. 勾选 Build Display Name Use the formatted version number for build display name 后


    此时每次构建后就会生成一个个版本号:


    jenkins-configure-version-result.png


  4. 把这个参数传递到后续的 OSS 上传的 Shell 脚本中即可。


如果想要重置版本号,只要设置Number of builds since the start of the project为 0 即可,此时就会从 1.7.0 重新开始。


Build Steps


这是最为重要的环节,主要用于定义整个构建过程的具体任务和操作,包括执行脚本、编译代码、打包应用等。


我们可以通过 Shell 脚本来完成前端项目常见的操作:安装依赖、打包、压缩、上传到 OSS 等。


点击 增加构建步骤 -> Execute shell,在上方输入 shell 脚本,常见的如下:


#环境变量
echo $PATH
#node版本号
node -v
#npm版本号
npm -v


#进入jenkins workspace的项目目录
echo ${WORKSPACE}
cd ${WORKSPACE}

#下载依赖包
yarn
#开始打包
yarn run build

#进入到打包目录
cd dist
#删除上次打包生成的压缩文件
rm -rf *.tar.gz

#上传oss,如果没有需要可删除此段代码
ossurl="xxx"
curl "xxx" > RELEASES.json
node deploy-oss.cjs -- accessKeyId=$OSS_KEY accessKeySecret=$OSS_SECRET zipDir=tmp.zip ossUrl=xxx/v${BUILD_VERSION}.zip
node deploy-oss.cjs -- accessKeyId=$OSS_KEY accessKeySecret=$OSS_SECRET zipDir=RELEASES.json ossUrl=xxx/RELEASES.json

#把生成的项目打包成压缩包方便传输到远程服务器
tar -zcvf `date +%Y%m%d%H%M%S`.tar.gz *
#回到上层工作目录
cd ../

构建后操作


通过上面的构建步骤,我们已经完成了项目的打包,此时我们需要执行一些后续操作,如部署应用、发送通知、触发其他 Job等操作。


Send build artifacts over SSH


通过 Send build artifacts over SSH,我们可以将构建好的产物(一般是压缩后的文件)通过 ssh 发送到指定的服务器上用于部署,比如 Jenkins 服务器是 10.10,需要将压缩文件发送到 10.11 服务器进行部署,需要以下步骤:



  1. 安装插件


    系统管理 -> 插件管理 中搜索插件 Publish over SSH 安装,用于处理文件上传工作;


  2. 配置服务器信息


    系统管理 -> System 中搜索 Publish over SSH 进行配置。


    jenkins-publish-over-SSH.png


    需要填写用户名、密码、服务器地址等信息,完成后点击 Test Configuration,如果配置正确,会显示 Success,否则会出现报错信息。


    这里有两种方式连接远程服务器,第一种是密码方式,输入服务器账户密码等信息即可;


    第二种是秘钥方式,在服务器生成密钥文件,并且将私钥全部拷贝,记住是全部,要携带起止标志-----BEGIN RSA PRIVATE KEY-----或-----END RSA PRIVATE KEY----,粘贴在 高级 -> key 即可。


    此处的 Remote Directory 是远程服务器接收 Jenkins 打包产物的目录,必须在对应的服务器手动创建目录,如 /home/jenkins


  3. 项目配置


    选择需要上传的服务器,接着设置需要传输的文件,执行脚本,移动文件到对应的目录。


    jenkins-configure-ssh.png



Transfer Set 参数配置


  • Source files:需要传输的文件,也就是通过上一步 Build Steps 后生成的压缩文件,这个路径是相对于“工作空间”的路径,即只需要输入 dist/*.tar.gz 即可

  • Remove prefix:删除传输文件指定的前缀,如 Source files 设置为dist/*.tar.gz ,此时设置 Remove prefix/dist,移除前缀,只传输 *.tar.gz 文件;如果不设置酒会传输 dist/*.tar.gz 包含了 dist 整个目录,并且会自动在上传后的服务器中创建 /dist 这个路径。如果只需要传输压缩包,则移除前缀即可

  • Remote directory:文件传输到远程服务器上的具体目录,会与 Publish over SSH 插件系统配置中的 Remote directory 进行拼接,如我们之前设置的目录是 /home/jenkins,此处在写入 qmp_pc_ddm,那么最终上传的路径为 /home/jenkins/qmp_pc_ddm,与之前不同的是,如果此路径不存在时会自动创建,这样设置后,Jenkins 服务器构建后的产物会通过 ssh 上传到此目录,供下一步使用。

  • Exec command


    文件传输完成后执行自定义 Shell 脚本,比如移动文件到指定目录、解压文件、启动服务等。


    #!/bin/bash

    #进入远程服务器的目录
    project_dir=/usr/local/nginx/qmp_pc_ddm/${DEPLOYPATH}
    cd $project_dir

    #移动压缩包
    sudo mv /home/jenkins/qmp_pc_ddm/*.tar.gz .

    #找到新的压缩包
    new_dist=`ls -ltr *.tar.gz | awk '{print $NF}' |tail -1`
    echo $new_dist

    #解压缩
    sudo tar -zxvf $new_dist

    #删除压缩包
    sudo rm *.tar.gz

    这一步可以使用之前定义的参数,如 ${DEPLOYPATH},以及 Jenkins 提供的变量:如 ${WORKSPACE} 来引用 Jenkins 的工作空间路径等。



Build other projects


添加 Build other projects,在项目构建成功后,触发相关联的应用开始打包。


jenkins-configure-other.png
另外还可以配置企业微信通知、生成构建报告等工作。


此时,所有的配置都设置完成,我们点击保存配置,返回到构建页。


构建


jenkins-start-build.png


点击 Build with parameters 选择对应的分支和部署环境,点击开始构建


在控制台输出中,可以看到打包的详细过程,


可以看到我们在Build Steps中执行的 Shell 脚本的输出如下:


jenkins-result-build.png


以及我们通过 Publish Over SSH 插件将构建产物传输的指定服务器的输出:


jenkins-result-ssh.png


最终需要部署的服务器就有了以下文件:


jenkins-remote-directory.png


Pipeline


对于简单的构建需求或新手用户来说,我们可以直接选择 FreeStyle project。而对于复杂的构建流程或需要更高灵活性和扩展性的场景来说,Pipeline 则更具优势。


通过 新建任务 -> 流水线 创建一个流水线项目。


jenkins-pipeline-white.png


开始配置前请先阅读下流水线章节。


生成方式


首先,Jenkins 流水线是一套插件,在最开始的插件推荐安装时会自动安装,如果选择自定义安装时,需要手动安装这一套插件。


Jenkins 流水线的定义有两种方式:Pipeline scriptPipeline script from SCM


jenkins-pipeline-type.png


Pipeline script


Pipeline script 是直接在 Jenkins 页面的配置中写脚本,可直接定义和执行,比较直观。


jenkins-pipeline-page.png


Pipeline script from SCM


Pipeline script from SCM 是将脚本文件和项目代码放在一起,即 Jenkinsfile,也可自定义名称。


jenkins-pipeline-code.png


当 Jenkins 执行构建任务时,会从 git 中拉取该仓库到本地,然后读取 Jenkinsfile 的内容执行相应步骤,通常认为在 Jenkinsfile 中定义并检查源代码控制是最佳实践


当选择 Pipeline script from SCM 后,需要设置 SCM 为 git,告诉 Jenkins 从指定的 Git 仓库中拉取包含 Pipeline 脚本的文件。


jenkins-pipeline-code-scm.png


如果没有对应的文件时,任务会失败并发出报错信息。


jenkins-pipeline-code-error.png


重要概念


了解完上面的基础配置,我们先找一段示例代码,粘贴在项目的配置中:


pipeline {
agent any
stages {
stage('Build') {
steps {
echo 'Build'
}
}
stage('Test') {
steps {
echo 'Test'
}
}
stage('Deploy') {
steps {
echo 'Deploy'
}
}
}
}

看下它的输出结果:


jenkins-pipeline-result.png


接着看一下上面语法中几个重要的概念。


流水线 pipline


定义了整个项目的构建过程, 包括:构建、测试和交付应用程序的阶段。


流水线顶层必须是一个 block,pipeline{},作为整个流水线的根节点,如下:


pipeline {
/* insert Declarative Pipeline here */
}

节点 agent


agent 用来指定在哪个代理节点上执行构建,即执行流水线,可以设置为 any,表示 Jenkins 可以在任何可用的代理节点上执行构建任务。


但一般在实际项目中,为了满足更复杂的构建需求,提高构建效率和资源利用率,以及确保构建环境的一致性,会根据项目的具体需求和资源情况,设置不同的代理节点来执行流水线。


如:


pipeline {
agent {
node {
label 'slave_2_34'
}
}
...
}

可以通过 系统管理 -> 节点列表 增加节点,可以看到默认有一个 master 节点,主要负责协调和管理整个 Jenkins 系统的运行,包括任务的调度、代理节点的管理、插件的安装和配置等。


jenkins-agent-master.png


阶段 stage


定义流水线的执行过程,如:Build、Test 和 Deploy,可以在可视化的查看目前的状态/进展。


注意:参数可以传入任何内容。不一定非得 BuildTest,也可以传入 打包测试,与红框内的几个阶段名对应。


jenkins-pipeline-console.png


步骤 steps


执行某阶段具体的步骤。


语法


了解上述概念后,我们仅仅只能看懂一个 Pipeline script 脚本,但距离真正的动手写还有点距离,此时就需要来了解下流水线语法


我将上面通过 Freestyle project 的脚本翻译成 Pipeline script 的语法:


pipeline {
agent any
triggers {
gitlab(triggerOnPush: true, triggerOnMergeRequest: true, branchFilterType: 'All')
}
parameters {
gitParameter branchFilter: 'origin/(.*)', defaultValue: 'master', name: 'delopyTag', type: 'PT_BRANCH'
}
stages {
stage('拉取代码') {
steps {
git branch: "${params.delopyTag}", credentialsId: 'xxx', url: 'https://xxx/fe/qmp_doc_hy.git'
}
}
stage('安装依赖') {
steps {
nodejs('node-v16.20.2') {
sh '''
#!/bin/bash
source /etc/profile
echo "下载安装包"
yarn config set registry https://registry.npmmirror.com
yarn
'''
}
sleep 5
}
}
stage('编译') {
steps {
sh '''
#!/bin/bash
source /etc/profile
yarn run build
sleep 5
if [ -d dist ];then
cd dist
rm -rf *.tar.gz

tar -zcvf `date +%Y%m%d%H%M%S`.tar.gz *
fi
'''
sleep 5
}
}
stage('解压') {
steps {
echo '解压'
sshPublisher(
publishers: [
sshPublisherDesc(
configName: 'server(101.201.181.27)',,
transfers: [
sshTransfer(
cleanRemote: false,
excludes: '',
execCommand: '''#!/bin/bash
#进入远程服务器的目录
project_dir=/usr/local/nginx/qmp_pc_ddm_${DEPLOYPATH}/${DEPLOYPATH}
if [ ${DEPLOYPATH} == "ddm" ]; then
project_dir=/usr/local/nginx/qmp_pc_ddm/dist
fi
cd $project_dir

sudo mv /home/jenkins/qmp_pc_ddm/*.tar.gz .

#找到新的压缩包
new_dist=`ls -ltr *.tar.gz | awk \'{print $NF}\' |tail -1`

#解压缩
sudo tar -zxvf $new_dist

#删除压缩包
sudo rm *.tar.gz

#发布完成
echo "环境发布完成"
''',
execTimeout: 120000,
flatten: false,
makeEmptyDirs: false,
noDefaultExcludes: false,
patternSeparator: '[, ]+',
remoteDirectory: 'qmp_pc_ddm',
remoteDirectorySDF: false,
removePrefix: 'dist/',
sourceFiles: 'dist/*.tar.gz'
)
],
usePromotionTimestamp: false,
useWorkspaceInPromotion: false,
verbose: false
)
]
)
}
}
}
post {
success {
echo 'success.'
deleteDir()
}
}
}

接下来,我们一起来解读下这个文件。


首先,所有的指令都是包裹在 pipeline{} 块中,


agent


enkins 可以在任何可用的代理节点上执行构建任务。


environment


用于定义环境变量,它们会保存为 Groovy 变量和 Shell 环境变量:定义流水线中的所有步骤可用的环境变量 temPath,在后续可通过 $tmpPath 来使用;


环境变量可以在全局定义,也可在 stage 里单独定义,全局定义的在整个生命周期里可以使用,在 stage 里定义的环境变量只能在当前步骤使用。


Jenkins 有一些内置变量也可以通过 env 获取(env 也可以读取用户自己定义的环境变量)。


steps {
echo "Running ${env.BUILD_ID} on ${env.JENKINS_URL}"
}

这些变量都是 String 类型,常见的内置变量有:



  • BUILD_NUMBER:Jenkins 构建序号;

  • BUILD_TAG:比如 jenkins-JOBNAME{JOB_NAME}-{BUILD_NUMBER};

  • BUILD_URL:Jenkins 某次构建的链接;

  • NODE_NAME:当前构建使用的机器


parameters


定义流水线中可以接收的参数,如上面脚本中的 gitParameter,只有安装了 Git Parameters 插件后才能使用,name 设置为delopyTag,在后续可通过 ${params.delopyTag} 来使用;


还有以下参数类型可供添加:


parameters {
booleanParam(name: 'isOSS', defaultValue: true, description: '是否上传OSS')
choice(name: 'select', choices: ['A', 'B', 'C'], description: '选择')
string(name: 'temp', defaultValue: '/temp', description: '默认路径')
text(name: 'showText', defaultValue: 'Hello\nWorld', description: '')
password(name: 'Password', defaultValue: '123', description: '')
}

triggers


定义了流水线被重新触发的自动化方法,上面的配置是:当 Git 仓库有新的 push 操作时触发构建


stages 阶段



  • 阶段一:拉取代码


    git:拉取代码,参数 branch 为分支名,我们使用上面定义的 ${params.delopyTag}credentialsId 以及 url,如果不知道怎么填,可以在 流水线语法 -> 片段生成器 中填写对应信息后,自动生成,如下:


    jenkins-stage-git.png


    再复制到此处即可。


  • 阶段二:安装依赖


    steps 中,sh 是 Jenkins pipeline 的语法,通过它来执行 shell 脚本。


    #!/bin/bash表示使用 bash 脚本;
    source /etc/profile 用于将指定文件中的环境变量和函数导入当前 shell。


    执行 yarn 安装依赖。


  • 阶段三:编译


    执行 yarn build 打包,


    if [ -d dist ]; 是 shell 脚本中的语法,用于测试 dist 目录是否存在,通过脚本将打包产物打成一个压缩包。


  • 阶段四:解压


    将上步骤生成的压缩包,通过 Publish over SSH 发送到指定服务器的指定位置,执行 Shell 命令解压。


    不会写 Publish over SSH 怎么办?同样,可以在 流水线语法 -> 片段生成器 中填写对应信息后,自动生成,如下:


    jenkins-generate-publish.png



post


当流水线的完成状态为 success,输出 success。


deleteDir() 函数用于删除当前工作目录中的所有文件和子目录。这通常用于清理工作区,确保在下一次构建之前工作区是干净的,以避免由于残留文件或目录引起的潜在问题。


构建看看效果


可以直接通过 Console Output 查看控制台输出,当然在流水线项目中自然要通过流水线去查看了。


jenkins-pipeline-result-in.png



  1. 效果一


    jenkins-pipeline-result1.png


    Pipeline Overview 中记录了每个步骤的执行情况、开始时间和耗时等信息,但是没有详细信息,详细信息就要在 Pipeline Console 中进行查看。


  2. 效果二


    安装插件 Blue Ocean,相当于同时结合了 Pipeline Overview 和 Pipeline Console,可以同时看到每个步骤的执行情况等基本信息,以及构建过程中的详细信息。


    jenkins-pipeline-result2.png


    通过 Blue Ocean 也可以直接创建流水线,选择代码仓库,然后填写对应的字段,即可快速创建流水线项目,如创建 gitlab 仓库:


    jenkins-blue-create.png


    或者直接连接 github 仓库,需要 token,直接点击红框去创建即可:


    jenkins-blue-create1.png



通过项目中的 Jenkinsfile 构建


再把对应的 Pipeline script 代码复制到对应代码仓库的 Jenkinsfile 文件,设置为 Pipeline script from SCM,填写 git 信息。


jenkins-pipeline-config-scm.png


正常情况下,Jenkins 会自动检测代码仓库的 Jenkinsfile 文件,如果选择的文件没有 Jenkinsfile 文件时就会报错,如下:


jenkins-pipeline-scm-error.png


正常按照流水线的执行流程,打开 Blue Ocean,查看构建结果,如下:


jenkins-pipeline-scm-result.png


片段生成器


如果你觉得上述代码手写麻烦,刚开始时又不会写,那么就可以使用片段代码生成器来帮助我们生成流水线语法。


进入任务构建页面,点击 流水线语法 进入:


配置构建过程遇到的问题



  1. Jenkins 工作空间权限问题


    jenkins-pipeline-error.png


    修复:


    chown -R jenkins:jenkins /var/lib/jenkins/workspace


  2. Git Parameters 不显示问题


    当配置完 Git Parameters 第一次点击构建时,会报如下错误,找了很久也没有找到解决方法,于是就先使用 master 分支构建了一次,构建完成之后再次点击构建这里就正常显示了,猜测是没构建前没有 git 仓库的信息,构建完一次后就有了构建信息,于是就正常显示了。


    jenkins-pipeline-error1.png



总结


本文对 Jenkins 的基本教程就到此为止了,主要讲了 Jenkins 的安装部署,FreeStyle project 和 Pipeline 的使用,以及插件安装、配置等。如果想要学,跟着我这个教程实操一遍,Jenkins 就基本掌握了,基本工作中遇到的问题都能解决,剩下的就只能在实际工作中慢慢摸索了。


再说回最初的话题,前端需不需要学习 Jenkins。我认为接触新的东西,然后学习并掌握,拓宽了技术面,虽然是一种压力,也是得到了成长的机会,在这个前端技术日新月异的时代,前端们不仅要熟练掌握前端技术,还需要具备一定的后端知识和自动化构建能力,才能不那么容易被大环境淘汰。


以上就是本文的全部内容,希望这篇文章对你有所帮助,欢迎点赞和收藏 🙏,如果发现有什么错误或者更好的解决方案及建议,欢迎随时联系。


作者:翔子丶
来源:juejin.cn/post/7349561234931515433
收起阅读 »

Nest:常用 15 个装饰器知多少?

web
nest 很多功能基于装饰器实现,我们有必要好好了解下有哪些装饰器:创建 nest 项目: nest new all-decorator -p npm @Module({}) 这是一个类装饰器,用于定义一个模块。模块是 Nest.js 中组织代码的单元,可以...
继续阅读 »

nest 很多功能基于装饰器实现,我们有必要好好了解下有哪些装饰器:
创建 nest 项目:


nest new all-decorator -p npm

@Module({})


这是一个类装饰器,用于定义一个模块。
模块是 Nest.js 中组织代码的单元,可以包含控制器、提供者等:
image.png


@Controller() 和 @Injectable()


这两个装饰器也是类装饰器,前者控制器负责处理传入的请求和返回响应,后者定义一个服务提供者,可以被注入到控制器或其他服务中。
通过 @Controller@Injectable 分别声明 controller 和 provider:
image.png


@Optional、@Inject


创建可选对象(无依赖注入),可以用 @Optional 声明一下,这样没有对应的 provider 也能正常创建这个对象。
image.png
注入依赖也可以用 @Inject 装饰器。


@Catch


filter 是处理抛出的未捕获异常,通过 @Catch 来指定处理的异常:
image.png


@UseXxx、@Query、@Param


使用 @UseFilters 应用 filter 到 handler 上:
image.png
image.png
除了 filter 之外,interceptor、guard、pipe 也是这样用:
image.png


@Body


如果是 post、put、patch** **请求,可以通过 @Body 取到 body 部分:
image.png
我们一般用 dto 定义的 class 来接收验证请求体里的参数。


@Put、@Delete、@Patch、@Options、@Head


@Put、@Delete、@Patch、@Options、@Head 装饰器分别接受 put、delete、patch、options、head 请求:
image.png


@SetMetadata


通过 @SetMetadata 指定 metadata,作用于 handler 或 class
image.png
然后在 guard 或者 interceptor 里取出来:
image.png


@Headers


可以通过 @Headers 装饰器取某个请求头或者全部请求头:
image.png


@Ip


通过 @Ip 拿到请求的 ip,通过 @Session 拿到 session 对象:
image.png


@HostParam


@HostParam 用于取域名部分的参数。
下面 host 需要满足 xxx.0.0.1 到这个 controller,host 里的参数就可以通过 @HostParam 取出来:
image.png


@Req、@Request、@Res、@Response


前面取的这些都是 request 里的属性,当然也可以直接注入 request 对象:
image.png
@Req 或者 @Request 装饰器,这俩是同一个东西。


使用 @Res 或 @Response 注入 response 对象,但是注入 response 对象之后,服务器会一直没有响应。
因为这时候 Nest 就不会把 handler 返回值作为响应内容了。我们可以自己返回响应:
image.png
Nest 这么设计是为了避免相互冲突。
如果你不会自己返回响应,可以设置 passthrough 为 true 告诉 Nest:
image.png


@Next


除了注入 @Res 不会返回响应外,注入 @Next 也不会。
当你有两个 handler 来处理同一个路由的时候,可以在第一个 handler 里注入 next,调用它来把请求转发到第二个 handler。
image.png


@HttpCode


handler 默认返回的是 200 的状态码,你可以通过 @HttpCode 修改它:
image.png


@Header


当然,你也可以修改 response header,通过 @Header 装饰器:
image.png


作者:云牧
来源:juejin.cn/post/7340554546253611023
收起阅读 »

还在使用 iconfont,上传图标审核好慢,不如自己做一个

web
之前使用 iconfont 是非常方便的,上传之后立马生效,项目里面直接引用即可,但是现在因为政策的收紧,每次上传图标都要等待十几分钟二十分钟的审核时间,这怎么能忍,有这个时间我都能写一个页面了好吧。 忍受不了就自己做,说干就干,于是我写了一个 svg 转图标...
继续阅读 »

之前使用 iconfont 是非常方便的,上传之后立马生效,项目里面直接引用即可,但是现在因为政策的收紧,每次上传图标都要等待十几分钟二十分钟的审核时间,这怎么能忍,有这个时间我都能写一个页面了好吧。


忍受不了就自己做,说干就干,于是我写了一个 svg 转图标字体的脚手架,所有的内容都自己维护,不再受制于人,感觉就是爽。


svg2font: 一个高效的 SVG 图标字体生成工具


github.com/tenadolante…


在现代 Web 开发中,使用图标是一种常见的做法。图标不仅能美化界面,还能提高可用性和可访问性。传统上,我们使用图片文件(如 PNG、JPG 等)来显示图标,但这种方式存在一些缺陷,例如图片文件较大、不能任意缩放、无法通过 CSS 设置颜色等。相比之下,使用字体图标具有许多优势,如文件体积小、可无限缩放、可通过 CSS 设置颜色和阴影等。


svg2font 就是一个用于将 SVG 图标转换为字体图标的工具,它可以帮助我们轻松地在项目中集成和使用字体图标。本文将详细介绍 svg2font 的使用方法、应用场景和注意事项。


安装


svg2font 是一个基于 Node.js 的命令行工具,因此需要先安装 Node.js 环境。安装完成后,可以使用 npm 或 yarn 在项目中安装 svg2font:


# 使用npm
npm install @tenado/svg2font -D

# 使用yarn
yarn add @tenado/svg2font -D

初始化配置


安装完成后,需要初始化 svg2font 的配置文件。在项目根目录执行以下命令:


npx svg2font init

该命令会在项目根目录下生成一个 svg2font.config.js 文件,内容如下:


module.exports = {
inputPath: "src/assets/svgs", // SVG图标文件夹路径
outputPath: "src/assets/font", // 生成字体文件的输出路径
fontFamily: "tenadoIcon", // 字体名称
fontPrefix: "", // 字体前缀
};

你可以根据实际需求修改这些配置项。


生成字体图标


配置完成后,就可以执行以下命令生成字体图标了:


npx svg2font sync

该命令会读取 inputPath 指定的 SVG 图标文件夹,将其中的 SVG 文件转换为字体文件(包括.eot、.ttf、.woff、.woff2 等格式),并输出到 outputPath 指定的路径下。同时,它还会生成一个 config.json 文件,记录了每个图标的 Unicode 编码和 CSS 类名。


在项目中使用字体图标


生成字体文件后,需要在项目中引入相应的 CSS 文件,才能正常使用字体图标。svg2font 会自动生成一个 index.min.css 文件,包含了所有字体图标的 CSS 定义。你可以在项目的入口文件(如 main.js)中导入该 CSS 文件:


import "./src/assets/font/index.min.css";

之后,你就可以在 HTML 中使用字体图标了。例如,如果你有一个名为 ticon-color-pick 的图标,可以这样使用:


<span class="ticon-color-pick"></span>

查看图标列表


如果你想查看当前项目包含的所有图标,可以执行以下命令:


npx svg2font example

该命令会根据 config.json 文件生成一个静态 HTML 页面,列出了所有图标及其对应的 CSS 类名和 Unicode 编码。它还会启动一个本地服务器,方便你在浏览器中预览这个页面。


注意事项


使用 svg2font 时,需要注意以下几点:


1.SVG 文件命名: 确保 SVG 文件名不包含特殊字符或空格,否则可能会导致生成字体时出错。


2.SVG 文件优化: 在将 SVG 文件转换为字体之前,建议先对 SVG 文件进行优化,以减小文件大小。你可以使用工具如 SVGO 或 SVG Optimizer 来优化 SVG 文件。


3.字体支持:不同浏览器和操作系统对字体格式的支持程度不同。为了最大程度地兼容各种环境,svg2font 会生成多种字体格式(.eot、.ttf、.woff、.woff2 等)。


4.字体缓存: 浏览器会缓存字体文件,因此在更新字体图标时,需要确保浏览器加载了最新的字体文件。你可以在 CSS 文件中为字体文件添加版本号或时间戳,以强制浏览器重新加载字体文件。


总结


svg2font 是一个功能强大且易于使用的 SVG 图标字体生成工具。它可以帮助你轻松地将 SVG 图标转换为字体格式,并在 Web 应用程序、跨平台应用程序或图标库中使用这些字体图标。通过使用 svg2font,你可以提高页面性能、确保图标显示一致性,并享受字体图标带来的诸多优势。


无论你是 Web 开发人员、移动应用程序开发人员,还是 UI 设计师,svg2font 都值得一试。它简单易用,且具有丰富的功能和配置选项,可以满足不同项目的需求。快来试试 svg2font,让你的项目与众不同吧!


作者:是阿派啊
来源:juejin.cn/post/7384808085348483087
收起阅读 »

时隔5年重拾前端开发,却倒在了环境搭建上

web
背景 去年不是降本增“笑”,“裁员”广进来着吗,公司有个项目因此停止了,最近又说这个项目还是很有必要的,就又重新启动这个项目了,然后让我这个“大聪明”把环境重新跑起来。让我无奈的是,原项目的团队成员都已经被增“笑”了,只留下了一堆不知从哪开始着手的文档。 后端...
继续阅读 »

背景


去年不是降本增“笑”,“裁员”广进来着吗,公司有个项目因此停止了,最近又说这个项目还是很有必要的,就又重新启动这个项目了,然后让我这个“大聪明”把环境重新跑起来。让我无奈的是,原项目的团队成员都已经被增“笑”了,只留下了一堆不知从哪开始着手的文档。


后端还好,前端我心里就犯嘀咕了,毕竟已经5年没有关注过前端了,上次写前端代码用的还是一个基于Angular构建的移动框架inoic,不知道大家用过没有。


好在这个项目前端也用的Angular框架,本以为整个过程会很顺利,然而,结果总是事与愿违。果不其然,在搭建前端开发环境时就给我上了一课,整个过程让我抓耳挠腮,遂特此记录。


环境搭建心路历程


跟着文档操作


前端文档中对环境搭建有进行说明,一共有4个步骤,大概是这样的:



  1. 确认node环境,需要某个及以上版本。

  2. 安装@angular/cli。

  3. 安装依赖。

  4. 启动项目。


看到这里,我第一反应是“啊?现在前端这么麻烦的吗?”,我记得以前在浏览器直接打开页面就可以访问了。咱也不懂,跟着说明操作就行。



  1. 我本地不知道啥时候装了nodejs,执行node -v后输出v18.13.0,符合要求。ok

  2. @angular/cli这是啥,咋也不懂,执行安装命令就行,输出看上去是没有问题。ok

  3. 安装依赖我理解跟Maven的依赖管理一样,先不管,执行。ok

  4. 到这一步,我觉得应该可以顺利启动,看一看这个项目的庐山真面目了,结果执行 npm start 后报下面这个错。


出现问题一:nodeJS版本过高


Error: error:0308010C:digital envelope routines::unsupported
......
......

{
'opensslErrorStack': [ 'error:03000086:digital envelope routines::initialization error' ],
'library': 'digital envelope routines',
'reason': 'unsupported',
'code': 'ERR_OSSL_EVP_UNSUPPORTED'
}
......
......

百度一看,原因是node 17版本之后,OpenSSL3.0对算法和密钥大小增加了严格的限制。


解决呗,降版本呗,node官网 下载了v14.12.0。


出现问题二:nodeJS版本低于Angular CLI版本


降版本之后重新运行npm start,您猜猜怎么着


在这里插入图片描述


Node.js version v14.12.0 detected.
The Angular CLI requires a minimum Node.js version of v18.13.

Please update your Node.js version or visit https://nodejs.org/ for additional instructions.

很明显,新老版本冲突了,又是版本问题,又是一顿百度之后,发现知乎上的一个帖子跟我这问题现象是一样的:“node是最新版,npm启动项目使用的不是最新版的node,请问这个怎么解决?


跟着下面的评论又安装了nvm(Node Version Manager),最后一顿操作后,莫名其妙的启动了。


事后才反应过来,这个问题的根本原因是:Angular CLI是在node版本为18.3时安装的,版本更新到14.12.0后需要删除依赖重新安装。


但是我不确定的是对应的npm版本会不会一同更新,有知道的小伙伴评论区交流一下。\color{blue}{但是我不确定的是对应的npm版本会不会一同更新,有知道的小伙伴评论区交流一下。}


不过nvm确实好用,至少不用担心node和npm版本问题,比如下面的命令:


[xxx % ] nvm use --delete-prefix v18.13.0
Now using node v18.13.0 (npm v8.19.3)

学到的第一个知识:nvm


这里记录下nvm安装过程



  1. clone this repo in the root of your user profile


  2. cd ~/.nvm and check out the latest version with git checkout v0.39.7

  3. activate nvm by sourcing it from your shell: . ./nvm.sh


配置环境变量


export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion

引发的思考


技术发展日新月异


早在几年前,程序员是要前后端一起开发的,不分什么前后端,我从最开始的HTML、JavaScript开始用到AngularJS这些框架,印象最深刻的是还要解决兼容IE浏览器。没想到现在的前端也会有版本管理、组件化等等,可见技术更新迭代速度之快。


前端的重要性


当初在选择后端的时候认为前端技术无非就那些,没有什么挑战。事实上,前后端没有分离之前,市场上的应用页面也是极其简洁的,前后端一起兼顾是没有精力写出那么好看的界面和交互的。所以“前端已死”的观点我是不认可的。


降本增“笑”被迫全栈


前几天参加了开发者社区的线下聚会,聊了一下行情。有小伙伴吐槽,因为在降本增“笑”的原因,现在他们被公司要求要写前端,被迫向全栈发展,竟意外发现开发效率极其高。还有小伙伴说“前端被裁的剩下几个人,一个前端对接十个后端。”。是呀,在降本增“笑”之后,老板恨不得让一个人干十个人的活。


与时俱进


不论是几年前的前后端分离还是降本增“笑”带来的被迫全栈,还是最近“前端已死”的观点,一切都是行业发展所需要的。我们需要做到的是:不断学习和更新自己的知识和技能,以适应行业的发展和变化。


作者:王二蛋呀
来源:juejin.cn/post/7327599804325052431
收起阅读 »

cesium 鼠标动态绘制墙及墙动效

web
实现在cesium中基于鼠标动态绘制墙功能 1. 基本架构设计 绘制墙的交互与绘制线的交互几乎一模一样,只是一些生成wall实体的计算方法不一样,所以可以看这篇文章 cesium 鼠标动态绘制线及线动效 juejin.cn/post/728826… 了解相关...
继续阅读 »

实现在cesium中基于鼠标动态绘制墙功能



1. 基本架构设计


绘制墙的交互与绘制线的交互几乎一模一样,只是一些生成wall实体的计算方法不一样,所以可以看这篇文章 cesium 鼠标动态绘制线及线动效 juejin.cn/post/728826… 了解相关的架构设计


2. 关键代码实现


2.1 绘制线交互相关事件


事件绑定相关与动态绘制线一样,这里不再重复代码


绘制形状代码有区别:
为了实现墙贴地,要实时计算minimumHeights,maximumHeights的值,min中算出地形高度,max中再地形高度的基础上再加上墙的高度


  /**
* 绘制形状,用于内部临时画墙
* @param positionData 位置数据
* @param config 墙的配置项
* @returns
*/

private drawShape(positionData: Cartesian3[], config?: WallConfig) {
const wallConfig = config || new WallConfig();
const material = this.createMaterial(wallConfig);
// @ts-ignore
const pArray = positionData._callback();

const shape = this.app.viewerCesium.entities.add({
wall: {
positions: positionData,
material: material,
maximumHeights: new CallbackProperty(() => {
let heights: number[] = [];
for (let i = 0; i < pArray.length; i++) {
const cartographic = Cartographic.fromCartesian(pArray[i]);
const height = cartographic.height;
heights.push(height);
}
const data = Array.from(heights, (x) => x + wallConfig.height);
return data;
}, false),
minimumHeights: new CallbackProperty(() => {
let heights: number[] = [];
for (let i = 0; i < pArray.length; i++) {
const cartographic = Cartographic.fromCartesian(pArray[i]);
const height = cartographic.height;
heights.push(height);
}
const data = Array.from(heights);
return data;
}, false)
}
});
return shape;
}

2.2 创建材质相关


  /**
* 创建材质
* @param config 墙的配置项
* @returns
*/

private createMaterial(config: WallConfig) {
let material = new ColorMaterialProperty(Color.fromCssColorString(config.style.color));
if (config.style.particle.used) {
material = new WallFlowMaterialProperty({
image: config.style.particle.image,
forward: config.style.particle.forward ? 1.0 : -1.0,
horizontal: config.style.particle.horizontal,
speed: config.style.particle.speed,
repeat: new Cartesian2(config.style.particle.repeat, 1.0)
});
}

return material;
}

创建WallFlowMaterialProperty.js(具体为何如此请看这篇文章,cesium自定义材质 juejin.cn/post/728795…


import { Color, defaultValue, defined, Property, createPropertyDescriptor, Material, Event, Cartesian2 } from 'cesium';

const defaultColor = Color.TRANSPARENT;
import defaultImage from '../../../assets/images/effect/line-color-yellow.png';
const defaultForward = 1;
const defaultHorizontal = false;
const defaultSpeed = 1;
const defaultRepeat = new Cartesian2(1.0, 1.0);

class WallFlowMaterialProperty {
constructor(options) {
options = defaultValue(options, defaultValue.EMPTY_OBJECT);

this._definitionChanged = new Event();
// 定义材质变量
this._color = undefined;
this._colorSubscription = undefined;
this._image = undefined;
this._imageSubscription = undefined;
this._forward = undefined;
this._forwardSubscription = undefined;
this._horizontal = undefined;
this._horizontalSubscription = undefined;
this._speed = undefined;
this._speedSubscription = undefined;
this._repeat = undefined;
this._repeatSubscription = undefined;
// 变量初始化
this.color = options.color || defaultColor; //颜色
this.image = options.image || defaultImage; //材质图片
this.forward = options.forward || defaultForward;
this.horizontal = options.horizontal || defaultHorizontal;
this.speed = options.speed || defaultSpeed;
this.repeat = options.repeat || defaultRepeat;
}

// 材质类型
getType() {
return 'WallFlow';
}

// 这个方法在每次渲染时被调用,result的参数会传入glsl中。
getValue(time, result) {
if (!defined(result)) {
result = {};
}

result.color = Property.getValueOrClonedDefault(this._color, time, defaultColor, result.color);
result.image = Property.getValueOrClonedDefault(this._image, time, defaultImage, result.image);
result.forward = Property.getValueOrClonedDefault(this._forward, time, defaultForward, result.forward);
result.horizontal = Property.getValueOrClonedDefault(this._horizontal, time, defaultHorizontal, result.horizontal);
result.speed = Property.getValueOrClonedDefault(this._speed, time, defaultSpeed, result.speed);
result.repeat = Property.getValueOrClonedDefault(this._repeat, time, defaultRepeat, result.repeat);

return result;
}

equals(other) {
return (
this === other ||
(other instanceof WallFlowMaterialProperty &&
Property.equals(this._color, other._color) &&
Property.equals(this._image, other._image) &&
Property.equals(this._forward, other._forward) &&
Property.equals(this._horizontal, other._horizontal) &&
Property.equals(this._speed, other._speed) &&
Property.equals(this._repeat, other._repeat))
);
}
}

Object.defineProperties(WallFlowMaterialProperty.prototype, {
isConstant: {
get: function get() {
return (
Property.isConstant(this._color) &&
Property.isConstant(this._image) &&
Property.isConstant(this._forward) &&
Property.isConstant(this._horizontal) &&
Property.isConstant(this._speed) &&
Property.isConstant(this._repeat)
);
}
},

definitionChanged: {
get: function get() {
return this._definitionChanged;
}
},

color: createPropertyDescriptor('color'),
image: createPropertyDescriptor('image'),
forward: createPropertyDescriptor('forward'),
horizontal: createPropertyDescriptor('horizontal'),
speed: createPropertyDescriptor('speed'),
repeat: createPropertyDescriptor('repeat')
});

Material.WallFlowType = 'WallFlow';
Material._materialCache.addMaterial(Material.WallFlowType, {
fabric: {
type: Material.WallFlowType,
uniforms: {
// uniforms参数跟我们上面定义的参数以及getValue方法中返回的result对应,这里值是默认值
color: defaultColor,
image: defaultImage,
forward: defaultForward,
horizontal: defaultHorizontal,
speed: defaultSpeed,
repeat: defaultRepeat
},
// source编写glsl,可以使用uniforms参数,值来自getValue方法的result
source: `czm_material czm_getMaterial(czm_materialInput materialInput)
{
czm_material material = czm_getDefaultMaterial(materialInput);

vec2 st = materialInput.st;
vec4 fragColor;
if (horizontal) {
fragColor = texture(image, fract(vec2(st.s - speed*czm_frameNumber*0.005*forward, st.t)*repeat));
} else {
fragColor = texture(image, fract(vec2(st.t - speed*czm_frameNumber*0.005*forward, st.t)*repeat));
}

material.emission = fragColor.rgb;
material.alpha = fragColor.a;

return material;
}`

},
translucent: true
});

export { WallFlowMaterialProperty };


2.3 添加wall实体


  /**
* 根据已知数据添加一个墙
* @param config 墙的配置项
*/

add(config: WallConfig) {
const configCopy = cloneDeep(config);

const positions = configCopy.positions;

const material = this.createMaterial(configCopy);

let distance = new DistanceDisplayCondition();
if (configCopy.distanceDisplayCondition) {
distance = new DistanceDisplayCondition(
configCopy.distanceDisplayCondition.near,
configCopy.distanceDisplayCondition.far
);
}

let heights: number[] = [];
for (let i = 0; i < positions.length; i++) {
const cartographic = Cartographic.fromCartesian(positions[i]);
const height = cartographic.height;
heights.push(height);
}

this.app.viewerCesium.entities.add({
id: 'wallEntity_' + configCopy.id,
wall: {
positions: positions,
maximumHeights: Array.from(heights, (x) => x + configCopy.height),
minimumHeights: Array.from(heights),
material: material,
distanceDisplayCondition: distance
}
});

this._wallConfigList.set('wallEntity_' + configCopy.id, config);
}

3. 业务端调用


调用方式与动态绘制线一样,是同一种架构设计,这里不再重复代码


4. 效果


wall动画.webp


wall动画1.webp


wall动画2.webp


wall动画3.webp


作者:山河木马
来源:juejin.cn/post/7288606110335565883
收起阅读 »

前端如何生成临时链接?

web
前言 前端基于文件上传需要有生成临时可访问链接的能力,我们可以通过URL.createObjectURL和FileReader.readAsDataURAPI来实现。 URL.createObjectURL() URL.createObjectURL() 静态...
继续阅读 »



前言


前端基于文件上传需要有生成临时可访问链接的能力,我们可以通过URL.createObjectURLFileReader.readAsDataURAPI来实现。


URL.createObjectURL()


URL.createObjectURL() 静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的URL。这个 URL 的生命周期和创建它的窗口中的 document 绑定。这个新的URL 对象表示指定的 File 对象或 Blob 对象。


1. 语法


let objectURL = URL.createObjectURL(object);

2. 参数


用于创建 URL 的 File 对象、Blob 对象或者 MediaSource 对象。


3. 返回值


一个DOMString包含了一个对象URL,该URL可用于指定源 object的内容。


4. 示例


"file" id="file">

document.querySelector('#file').onchange = function (e) {
console.log(e.target.files[0])
console.log(URL.createObjectURL(e.target.files[0]))
}

0f40e1fff9674142889f8bacc6d455b9.png


将上方console控制台打印的blob文件资源地址粘贴到浏览器中


blob:http://localhost:8080/1ece2bb1-b426-4261-89e8-c3bec43a4020

5cc4d088c5c941b7950f6f930cb9a1bc.png


URL.revokeObjectURL()


在每次调用 createObjectURL() 方法时,都会创建一个新的 URL 对象,即使你已经用相同的对象作为参数创建过。当不再需要这些 URL 对象时,每个对象必须通过调用 URL.revokeObjectURL() 方法来释放。


浏览器在 document 卸载的时候,会自动释放它们,但是为了获得最佳性能和内存使用状况,你应该在安全的时机主动释放掉它们。


1. 语法


window.URL.revokeObjectURL(objectURL);

2. 参数 objectURL


一个 DOMString,表示通过调用 URL.createObjectURL() 方法返回的 URL 对象。


3. 返回值


undefined


4. 示例


"file" id="file">
<img id="img1" style="width: 200px;height: auto" />
<img id="img2" style="width: 200px;height: auto" />

document.querySelector('#file').onchange = function (e) {
const file = e.target.files[0]

const URL1 = URL.createObjectURL(file)
console.log(URL1)
document.querySelector('#img1').src = URL1
URL.revokeObjectURL(URL1)

const URL2 = URL.createObjectURL(file)
console.log(URL2)
document.querySelector('#img2').src = URL2
}

ecba01284f034c42a2bf4200054b0e9f.png


与FileReader.readAsDataURL(file)区别


1. 主要区别



  • 通过FileReader.readAsDataURL(file)可以获取一段data:base64的字符串

  • 通过URL.createObjectURL(blob)可以获取当前文件的一个内存URL


2. 执行时机



  • createObjectURL是同步执行(立即的)

  • FileReader.readAsDataURL是异步执行(过一段时间)


3. 内存使用



  • createObjectURL返回一段带hashurl,并且一直存储在内存中,直到document触发了unload事件(例如:document close)或者执行revokeObjectURL来释放。

  • FileReader.readAsDataURL则返回包含很多字符的base64,并会比blob url消耗更多内存,但是在不用的时候会自动从内存中清除(通过垃圾回收机制)


4. 优劣对比



  • 使用createObjectURL可以节省性能并更快速,只不过需要在不使用的情况下手动释放内存

  • 如果不在意设备性能问题,并想获取图片的base64,则推荐使用FileReader.readAsDataURL




作者:sorryhc
来源:juejin.cn/post/7333236033038778409
收起阅读 »

Vue3 实现最近很火的酷炫功能:卡片悬浮发光

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ 有趣的动画效果 前几天在网上看到了一个很有趣的动画效果,如下,光会跟随鼠标在卡片上进行移动,并且卡片会有视差的效果 那么在 Vue3 中应该如何去实现这个效果呢...
继续阅读 »

前言


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


有趣的动画效果


前几天在网上看到了一个很有趣的动画效果,如下,光会跟随鼠标在卡片上进行移动,并且卡片会有视差的效果


那么在 Vue3 中应该如何去实现这个效果呢?



基本实现思路


其实实现思路很简单,无非就是分几步:



  • 首先,卡片是相对定位,光是绝对定位

  • 监听卡片的鼠标移入事件mouseenter,当鼠标进入时显示光

  • 监听卡片的鼠标移动事件mouseover,鼠标移动时修改光的left、top,让光跟随鼠标移动

  • 监听卡片的鼠标移出事件mouseleave,鼠标移出时,隐藏光


我们先在 Index.vue 中准备一个卡片页面,光的CSS效果可以使用filter: blur() 来实现



可以看到现在的效果是这样



实现光源跟随鼠标


在实现之前我们需要注意几点:



  • 1、鼠标移入时需要设置卡片 overflow: hidden,否则光会溢出,而鼠标移出时记得还原

  • 2、获取鼠标坐标时需要用clientX/Y而不是pageX/Y,因为前者会把页面滚动距离也算进去,比较严谨


刚刚说到实现思路时我们说到了mouseenter、mousemove、mouseleave,其实mouseenter、mouseleave 这二者的逻辑比较简单,重点是 mouseover 这个监听函数


而在 mouseover 这个函数中,最重要的逻辑就是:光怎么跟随鼠标移动呢?


或者也可以这么说:怎么计算光相对于卡片盒子的 left 和 top


对此我专门画了一张图,相信大家一看就懂怎么算了




  • left = clientX - x - width/2

  • height = clientY - y - height/2


知道了怎么计算,那么逻辑的实现也很明了了~封装一个use-light-card.ts



接着在页面中去使用



这样就能实现基本的效果啦~



卡片视差效果


卡片的视差效果需要用到样式中 transform 样式,主要是配置四个东西:



  • perspective:定义元素在 3D 变换时的透视效果

  • rotateX:X 轴旋转角度

  • rotateY:Y 轴旋转角度

  • scale3d:X/Y/Z 轴上的缩放比例



现在就有了卡片视差的效果啦~



给所有卡片添加光源


上面只是给一个卡片增加光源,接下来可以给每一个卡片都增加光源啦!!!




让光源变成可配置


上面的代码,总感觉这个 hooks 耦合度太高不太通用,所以我们可以让光源变成可配置化,这样每个卡片就可以展示不同大小、颜色的光源了~像下面一样



既然是配置化,那我们希望是这么去使用 hooks 的,我们并不需要自己在页面中去写光源的dom节点,也不需要自己去写光源的样式,而是通过配置传入 hooks 中



所以 hooks 内部要自己通过操作 DOM 的方式,去添加、删除光源,可以使用createElement、appendChild、removeChild 去做这些事~



完整源码


<!-- Index.vue -->

<template>
<div class="container">
<!-- 方块盒子 -->
<div class="item" ref="cardRef1"></div>
<!-- 方块盒子 -->
<div class="item" ref="cardRef2"></div>
<!-- 方块盒子 -->
<div class="item" ref="cardRef3"></div>
</div>
</template>

<script setup lang="ts">
import { useLightCard } from './use-light-card';

const { cardRef: cardRef1 } = useLightCard();
const { cardRef: cardRef2 } = useLightCard({
light: {
color: '#ffffff',
width: 100,
},
});
const { cardRef: cardRef3 } = useLightCard({
light: {
color: 'yellow',
},
});
</script>

<style scoped lang="less">
.container {
background: black;
width: 100%;
height: 100%;
padding: 200px;
display: flex;
justify-content: space-between;

.item {
position: relative;
width: 125px;
height: 125px;
background: #1c1c1f;
border: 1px solid rgba(255, 255, 255, 0.1);
}
}
</style>


// use-light-card.ts

import { onMounted, onUnmounted, ref } from 'vue';

interface IOptions {
light?: {
width?: number; // 宽
height?: number; // 高
color?: string; // 颜色
blur?: number; // filter: blur()
};
}

export const useLightCard = (option: IOptions = {}) => {
// 获取卡片的dom节点
const cardRef = ref<HTMLDivElement | null>(null);
let cardOverflow = '';
// 光的dom节点
const lightRef = ref<HTMLDivElement>(document.createElement('div'));
// 设置光源的样式

const setLightStyle = () => {
const { width = 60, height = 60, color = '#ff4132', blur = 40 } = option.light ?? {};
const lightDom = lightRef.value;
lightDom.style.position = 'absolute';
lightDom.style.width = `${width}px`;
lightDom.style.height = `${height}px`;
lightDom.style.background = color;
lightDom.style.filter = `blur(${blur}px)`;
};

// 设置卡片的 overflow 为 hidden
const setCardOverflowHidden = () => {
const cardDom = cardRef.value;
if (cardDom) {
cardOverflow = cardDom.style.overflow;
cardDom.style.overflow = 'hidden';
}
};
// 还原卡片的 overflow
const restoreCardOverflow = () => {
const cardDom = cardRef.value;
if (cardDom) {
cardDom.style.overflow = cardOverflow;
}
};

// 往卡片添加光源
const addLight = () => {
const cardDom = cardRef.value;
if (cardDom) {
cardDom.appendChild(lightRef.value);
}
};
// 删除光源
const removeLight = () => {
const cardDom = cardRef.value;
if (cardDom) {
cardDom.removeChild(lightRef.value);
}
};

// 监听卡片的鼠标移入
const onMouseEnter = () => {
// 添加光源
addLight();
setCardOverflowHidden();
};

// use-light-card.ts

// 监听卡片的鼠标移动
const onMouseMove = (e: MouseEvent) => {
// 获取鼠标的坐标
const { clientX, clientY } = e;
// 让光跟随鼠标
const cardDom = cardRef.value;
const lightDom = lightRef.value;
if (cardDom) {
// 获取卡片相对于窗口的x和y坐标
const { x, y } = cardDom.getBoundingClientRect();
// 获取光的宽高
const { width, height } = lightDom.getBoundingClientRect();
lightDom.style.left = `${clientX - x - width / 2}px`;
lightDom.style.top = `${clientY - y - height / 2}px`;

// 设置动画效果
const maxXRotation = 10; // X 轴旋转角度
const maxYRotation = 10; // Y 轴旋转角度

const rangeX = 200 / 2; // X 轴旋转的范围
const rangeY = 200 / 2; // Y 轴旋转的范围

const rotateX = ((clientX - x - rangeY) / rangeY) * maxXRotation; // 根据鼠标在 Y 轴上的位置计算绕 X 轴的旋转角度
const rotateY = -1 * ((clientY - y - rangeX) / rangeX) * maxYRotation; // 根据鼠标在 X 轴上的位置计算绕 Y 轴的旋转角度

cardDom.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`; //设置 3D 透视
}
};
// 监听卡片鼠标移出
const onMouseLeave = () => {
// 鼠标离开移出光源
removeLight();
restoreCardOverflow();
};

onMounted(() => {
// 设置光源样式
setLightStyle();
// 绑定事件
cardRef.value?.addEventListener('mouseenter', onMouseEnter);
cardRef.value?.addEventListener('mousemove', onMouseMove);
cardRef.value?.addEventListener('mouseleave', onMouseLeave);
});

onUnmounted(() => {
// 解绑事件
cardRef.value?.removeEventListener('mouseenter', onMouseEnter);
cardRef.value?.removeEventListener('mousemove', onMouseMove);
cardRef.value?.removeEventListener('mouseleave', onMouseLeave);
});

return {
cardRef,
};
};


结语 & 加学习群 & 摸鱼群


我是林三心



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

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

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

  • 一个逗比的B站up主;

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

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

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

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


如果你想一起学习前端,一起摸鱼,一起研究简历优化,一起研究面试进步,一起交流历史音乐篮球rap,可以来俺的摸鱼学习群哈哈,点这个,有7000多名前端小伙伴在等着一起学习哦 --> 摸鱼沸点


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

扫码出入库与web worker

web
我为什么会用到这个呢,那还得从最近项目的一个扫码出入库的需求说起,之前客户的扫码出入库都是c端的,在效率方面没有明显的问题,但是后面这个项目的升级,就把c端的扫码部分摞到了B端了 大体需求是这样,客户用无线扫码枪扫回运单上的条码,然后扫码枪使用HID键盘模式(...
继续阅读 »

我为什么会用到这个呢,那还得从最近项目的一个扫码出入库的需求说起,之前客户的扫码出入库都是c端的,在效率方面没有明显的问题,但是后面这个项目的升级,就把c端的扫码部分摞到了B端了


大体需求是这样,客户用无线扫码枪扫回运单上的条码,然后扫码枪使用HID键盘模式(扫码枪相当于一个键盘),在一个一直聚焦的输入框输入扫到的条码,然后我这边监听到条码调接口录入库,成功后再语音播报扫码结果,同时刷新结果,刷新统计信息。


听上去很简单是不是,想象是美好的,可现实就残酷了,在初始版的时候,功能是做出来了,本地出入库都没问题,但是发到生产就悲催了,乱七八糟的问题


比如



  • 1.扫码枪精度的问题,扫码识别率低下,扫10次才能正确识别1次

  • 2.扫出来的码,断码,原本以为扫码枪扫一次就等同于我复制一个条码进输入框,可结果是扫码枪一次扫入,输入框接受的条码就像一个字符串流一样,一个字符一个字符进入的,这就导致中间间隔稍微长一点,就被错误的识别为另外一个条码(扫码是多个码连续扫入的)

  • 3.语音播报延迟,经常会有语音播放不出或者播放一半,这个..


这个就很让人无语,明明本地啥问题也没有


第一个问题,扫码枪精度,确实是有,因为我做的时候拿的扫码枪是一个有线的扫码枪,那识别率才叫一个高,准确率差不多95%,几乎没遇到解码啥的问题,可换成无线的扫码枪就傻眼,第一个问题就很烦,想到几千个客户没办法统一更换扫码枪,于是就想想优化一下条码编码呢,
我这边条码是用的jsBarcode组件,默认的编码类型CODE128,嗯~~问题会不会出在条码规范上呢


我去查了一下,条码的编码规范大致有以下几种


条码类型类别描述常见应用编码长度
UPC-A1D通用产品代码,常见于零售业零售商品12位数字
UPC-E1DUPC-A的压缩版本小型零售商品6位数字
EAN-131D欧洲商品编号,国际通用图书、零售商品13位数字
EAN-81DEAN-13的压缩版本小型商品8位数字
Code 391D可变长度,包含字母、数字和特殊字符工业、政府可变长度
Code 1281D高密度条码,表示所有128个ASCII字符物流、运输可变长度
Interleaved 2 of 5 (ITF)1D数字条码,每两个数字组成一对交错编码分销、仓储偶数位数字
QR Code2D可存储大量数据,包括文字、数字、二进制数据和汉字支付、信息分享、广告可变长度
Data Matrix2D高密度编码,适用于小型物品标识电子元器件、医疗设备可变长度
PDF4172D可编码大量数据身-份-正件、运输标签可变长度
Aztec Code2D高容错性,适用于票务和登机牌票务、登机牌可变长度

我这里着重说说CODE39和CODE128;我发现CODE39生成的条码比CODE128生成的长很多,我这把无线扫码枪扫很久都扫不出来,识别超慢,这个很奇怪,之前客户C端系统找技术查了一下,编码规范是CODE39,我就懵逼了,都是CODE39,为啥我们生成的码就识别这么慢,捣鼓了很久也没个结果,如果有哪位知道的可以给我说一下,就索性放弃这种编码模式,改用CODE128吧,查了一下,这是一种效率更高的编码方式,CODE39条码较长的主要原因在于它的编码效率较低,每个字符占用的空间较大,而CODE128通过更加紧凑和高效的编码方式,能够在同样的内容下生成更短的条码,于是撺掇同事把所有的条码都用CODE128生成,至此,扫码枪识别效率低的问题算事过去了


然后就是第二点,扫出来的码,断码问题,这个也因为换了短码好那么一点,可扫出来也经常有解码内容变长,的问题,暂时还在想办法优化


最后就是语音播报延迟,卡壳,甚至没有语音的情况,这个问题比较恼火,我这边组件是使用的开源库howler.js,这个库的优点就是兼容性好,可以播放包括mp3, opus, ogg, wav, aac, m4a, m4b, mp4, webm, 等多种格式,而且还支持分轨sprite播放,这个是我的最初的代码


import config from "./config";
import "./lib/howler.min";
const ENV = import.meta.env;

class VoiceReport {
public list = [];
constructor() {
this.initVoice();
}
// 目录放在@/assets/voice/ 下面
public voiceList: any = import.meta.globEager("@/assets/voice/*.mp3");
public voiceNameList = Object.keys(this.voiceList);
// 初始化语音播报器列表
initVoice = () => {
config.forEach((v) => {
const item = {
name: `Ref${v.codeType}${v.codeKey}`,
code: v.codeKey,
codeName: v.codeName,
voice: "",
};
const voiceIndex = this.voiceNameList.findIndex((voice) =>
String(voice).includes(v.codeKey)
);
if (voiceIndex > -1) {
item.path = this.voiceNameList[voiceIndex];
item.voice = this.voiceList[this.voiceNameList[voiceIndex]];
}
this.list.push(item);
});
};
// 播放
play = (code: string) => {
const Stream = this.list.find((v) => v.code == code);
let StreamVoide = null;
if (ENV?.DEV) {
StreamVoide = Stream?.path;
} else {
StreamVoide = Stream?.voice?.default;
}
// 提供的条码不在列表中
if (!StreamVoide) return;
try {
const sound = new Howl({
src: [StreamVoide],
volume: 1.0,
html5: true,
onplayerror: (e) => {
console.log("error", e);
},
});
sound.play();
} catch (e) {
console.log(e);
}
};
}

export default VoiceReport;


这个倒是能放,可能不能优化呢


我首先想到的是就从播放器本身优化呢,我想着会不会是加载的延迟或者加载文件过多,想着将所有的文件进行合并,再生成sprite信息,弄是弄了,可是不论如何就是load报错,我再把这个多个mp3合并成一个文件@/assets/voice/fullStack.mp3,进行生成sprite,来加载,加载是加载上来了,可同样遇到播放错误,播放的track根本不是我期望的那个


这个是错误代码:



import config from "./config";
import "./lib/howler.min";
import fullVoice from "@/assets/voice/fullStack.mp3";
const ENV = import.meta.env;

class player {
public list: any = [];
public player: any = {};
constructor() {
this.initVoice();
}
// 目录放在@/assets/voice/ 下面
public voiceList: any = import.meta.globEager("@/assets/voice/*.mp3");
public fullVoice: any = fullVoice;
public voiceNameList = Object.keys(this.voiceList);
public sprite: any = {};
public streamVoide: any = [];
// 时间戳转换为秒
timeStringToSeconds = (timeStr: string) => {
const parts = timeStr.split(":");
const hours = parseInt(parts[0]);
const minutes = parseInt(parts[1]);
const seconds = parseInt(parts[2]);

return hours * 3600 + minutes * 60 + seconds;
}
// 初始化语音播报器列表
initVoice = () => {
config.forEach((v, index) => {
const item = {
name: `Ref${v.codeType}${v.codeKey}`,
code: v.codeKey,
codeName: v.codeName,
voice: {},
path: "",
duration: this.timeStringToSeconds(v.duration ?? 0) * 1000,
durationStart: 0,
durationEnd: 0,
};
item.durationStart = !index ? 0 : this.list[index - 1].durationEnd;
item.durationEnd = item.durationStart + item.duration;
this.sprite[v.codeKey] = [item.durationStart, item.durationEnd];
const voiceIndex = this.voiceNameList.findIndex((voice) =>
String(voice).includes(v.codeKey)
);
if (voiceIndex > -1) {
item.path = this.voiceNameList[voiceIndex];
item.voice = this.voiceList[this.voiceNameList[voiceIndex]];
}
this.list.push(item);
/* eslint-disable */
// @ts-ignore
this.streamVoide.push(ENV?.DEV ? item.path : item.voice?.default);
});
/* eslint-disable */
// @ts-ignore
this.player = new Howl({
src: this.streamVoide,
volume: 1.0,
html5: true,
sprite: this.sprite,
onplayerror: (e: any) => {
console.log("play error", e);
},
onload: (e: any) => {
console.log("error", e);
}
});
window.player = this.player;
console.log(this.sprite, this.player, fullVoice)
};
// 播放
play = (code: string) => {
try {
this.player.play(code);
} catch (e) {
console.log(e);
}
};
}

export default player;

到现在还在持续找解决方案中,
最后,不得不把希望寄托在异步任务请求导致阻塞主线程这个猜想上,因为每完成一次扫码,会发起三个请求



  • 入库请求

  • 刷新结果列表请求

  • 刷新统计请求


这么多请求一起,接口稍微一慢就有可能导致播放卡顿的问题
这个在我经过一段时间的搜索之后发现,发现webworker可以处理这个问题


web worker

根据MDN的说法

Web Workers 是 Web 内容在后台线程中运行脚本的一种简单方法。工作线程可以在不干扰用户界面的情况下执行任务。此外,他们还可以使用 fetch() 或 XMLHttpRequest API 发出网络请求。创建后,工作人员可以通过将消息发布到该代码指定的事件处理程序来向创建它的 JavaScript 代码发送消息(反之亦然)。


既然是独立于主线程之外的一个,那就不可避免的会遇到身份验证和通信的问题,对于发起的请求没有携带身份信息,这个好办,就自己在封装一个axios方法fetch,将身份信息传过去ok,这里主要贴一下worker的内容,也很简单


import type { WorkerMessageDataType } from "../types/types";
import fetch from "@/utils/fetch";
import { throttle } from "lodash";
let Ajax: any = null;

// 从主线程接受数据
self.onmessage = function (e: WorkerMessageDataType) {
console.log("Worker: 收到请求", e);
const type = e.data?.type || "";
const data = e.data?.data || {};
// 一定要初始化
if (type == "init") {
const headers: any = e.data?.headers;
Ajax = fetch(headers);
}
// 请求刷新统计数据
if (type == "refreshScanCountData") refreshScanCountData();
// 请求刷新列表扫码结果
if (type == "refreshDataList") refreshDataList();
// 请求入库
if (type == "checkAddIntoStock") checkAddIntoStock(data);
};

// 向主线程发送数据
const sedData = (type: string, data: object) => {
const param = {
type,
data: data || {},
};
self.postMessage(param);
};

// 刷新统计数据,查询统计信息api
const refreshScanCountData = throttle(() => {
Ajax({
method: "post",
url: `/api/CountStatistics`,
data: {},
}).then((res: any) => {
sedData("refreshScanCountData", res);
});
}, 500);

// 刷新扫码结果数据
const refreshDataList = throttle(() => {
Ajax({
method: "post",
url: `/api/scanToStorage/page`,
data: {},
}).then((res: any) => {
sedData("refreshDataList", res);
});
}, 500);

// 请求入库
const checkAddIntoStock = (data: { barcode: string; [x: string]: any }) => {
Ajax({
method: "post",
url: `/api/scanToStorage`,
data,
})
.then((res: any) => {
// 刷新统计数据
refreshScanCountData();
// 刷新列表
refreshDataList();
sedData("checkAddIntoStock", {
barcode: data.barcode,
...res,
status: true,
});
})
.catch(() => {
sedData("checkAddIntoStock", {
barcode: data.barcode,
status: false,
});
});
};

在主线程页面写一个方法,初始化一下这个worker


// 加载worker
const initWorker = () => {
const headers = {
Authorization: "bearer " + sessionStorage.getItem("token"),
token: sessionStorage.getItem("token"),
currRoleId: sessionStorage.getItem("roleId"),
};
// 初始化,加入身份信息
WebWorker.postMessage({ type: "init", headers });
// 从worker接受消息
WebWorker.onmessage = (e) => {
console.log("Main script: Received result", e.data);
const type = e.data?.type || "";
const data = e.data?.data || {};

// 异步更新统计信息
if (type == "refreshScanCountData") {
ScanCountData.value = data;
}
// 刷新表格数据
if (type == "refreshDataList") {
dataTable.value.updateData(data);
}
};
};


这样就可以了,即便是这样,依然还有好多问题没解决,这个是我的第一篇文章,难免有错误疏漏,这个需求并没结束,我还会持续跟进更新的


作者:kiohang
来源:juejin.cn/post/7380342160581492747
收起阅读 »

用空闲时间做了一个小程序-二维码生成器

web
一直在摸鱼中赚钱的大家好呀~ 先向各位鱼友们汇报一下情况,目前小程序已经有900+的鱼友注册使用过。虽然每天都有新的鱼友注册,但是鱼友增长的还很缓慢。自从国庆前的文字转语音的工具上线到现在已经将近有1个月没有更新小程序了。但是今天终终终终终于又有个小工具上线了...
继续阅读 »

一直在摸鱼中赚钱的大家好呀~


先向各位鱼友们汇报一下情况,目前小程序已经有900+的鱼友注册使用过。虽然每天都有新的鱼友注册,但是鱼友增长的还很缓慢。自从国庆前的文字转语音的工具上线到现在已经将近有1个月没有更新小程序了。但是今天终终终终终于又有个小工具上线了,希望这个小工具可以帮助到更多的鱼友们(没错就是你们)


这次更新的工具是一个二维码生成器,虽然很多小程序存在这个工具,但是本人也是想尝试一下实现这个工具。老规矩,先来看下知名UI设计师设计的页面。







同样在工具tab页中增加了二维码生成器模块。从UI图中可以看出第一个表单页面不是很难,就是一个文本框、两个颜色选择、一个图片上传。这个页面我在开发中也是很快就完成了,没有什么技术含量。


当我做到颜色选择弹窗的时候是想从网上找一个现成的插件。但是找了半天没有找到合适的,只能自己手动开发一个。既然要做颜色选择器的功能就要先了解一下颜色的两种格式 (我这边的实现就这两种格式)


颜色的HEX格式

颜色的HEX格式是#+六位数字/字母,其中六位数字/字母是一种十六进制的表达方式。这六位分别两个一组,从左到右分别表示绿00表示最小,十进制是0FF表示最大,十进制是255。通俗点讲,某个颜色的数值越大,包含这个颜色就越多。如:#000000-黑色、#FFFFFF-白色、#FF0000-红色、#00FF00-绿色、#0000FF-蓝色。


颜色的RGB格式

颜色的RGB格式是rgb(0-255,0-255,0-255), 其中0-255就是HEX格式的十进制表达方式。这三个数值从左到右分别表示绿0表示最小;255表示最大。通俗点讲,某个颜色的数值越大,包含这个颜色就越多。如:rgb(0,0,0)-黑色、rgb(255,255,255)-白色、rgb(255,0,0)-红色、rgb(0,255,0)-绿色、rgb(0,0,255)-蓝色。


有了上面的概念,我的思路也就出来了。让用户分别选择这三种颜色的数值,然后通过用户选择的三种颜色的数值转成目标颜色,就可以完成颜色选择的功能。思路出来了之后就告知了UI,然后按照我的思路将效果图出了出来 (没错,就是先实现后出图)。实现中主要使用了vant-ui组件库的popupslider两个组件 (聪明人都喜欢用现成的)。贴一下部分实现代码:


show="{{ show }}" 
title="展示弹出层"
position="bottom"
bind:close="cancelHandle"
custom-style="background-color: #F3F3F9;border-radius: 40rpx 40rpx 0rpx 0rpx;"
root-portal>
class="color-popup">
class="popup-header flex flex_j_c--space-between flex_a_i--center">
class="flex-item_f-1">
class="title flex-item_f-1">{{ title }}
class="flex-item_f-1 flex flex_j_c--flex-end">
name="cross" size="32rpx" bind:tap="cancelHandle" />


class="color-picker" wx:for="{{ pickers }}" wx:key="index" wx:if="{{ index !== 3 }}">
class="color-picker-label">{{ item.label }}
class="flex flex_a_i--center">
class="slider-wrap flex-item_f-1 {{ item.field }}">
value="{{ item.value }}" min="{{ 0 }}" max="{{ 255 }}" data-index="{{ index }}" bind:change="changeHandle" bind:drag="changeHandle" custom-class="slider" bar-height="60rpx" active-color="transparent" use-button-slot>
class="slider-button" slot="button">


class="slider-value">{{ item.value }}


class="color-preview-box flex flex_a_i--center">
class="preview-box-wrap">
class="preview-box" style="background-color: {{ rgbaStyle }};">
class="preview-label">颜色预览

class="presets-box-wrap flex-item_f-1 flex flex_j_c--space-between">
class="presets-box flex flex_j_c--center flex_a_i--center {{ rgbaStyle === item.rgbaStyle ? 'active' : '' }}" wx:for="{{ presets }}" wx:key="index" style="background-color: {{ item.rgbaStyle }};" data-row="{{ item }}" bind:tap="chooseHandle">
class="active-box">



class="confirm-wrap flex">
class="hex-box flex flex_a_i--center flex_j_c--space-between">
#
{{ hex }}

class="confirm-button-box flex-item_f-1">
type="primary" custom-class="confirm-button" bind:click="confirmHandle" round>确定





import { rgb2Hex } from '../../utils/util'

const presets = [
[0, 0, 0, 255], [102, 102, 102, 255],
[0, 95, 244, 255], [100, 196, 102, 255],
[247, 206, 70, 255], [235, 77, 61, 255],
]

Component({
options: {
addGlobalClass: true
},
properties: {
show: {
type: Boolean,
value: false
},
title: {
type: String,
value: ''
},
value: {
type: Array,
value: [0, 0, 0, 255],
observer: function(val) {
const { pickers } = this.data
if(val.length) {
this.setData({
pickers: pickers.map((item, index) => {
return {...item, value: val[index]}
}),
})
this.setColor(val)
} else {
this.setData({
pickers: pickers.map((item, index) => {
return {...item, value: index === 3 ? 255 : 0}
}),
})
const rgba = [0, 0, 0, 255]
this.setColor(rgba)
}
}
}
},
data: {
pickers: [
{ field: 'r', label: '红色', value: 0 },
{ field: 'g', label: '绿色', value: 0 },
{ field: 'b', label: '蓝色', value: 0 },
{ field: 'a', label: '透明度', value: 255 },
],
rgba: [],
hex: '',
rgbaStyle: '',
presets: [
...presets.map(rgba => {
return {
rgba,
rgbaStyle: `rgba(${ rgba.join(',') })`
}
})
]
},
methods: {
changeHandle(e) {
const { detail, currentTarget: { dataset: { index } } } = e
const key = `pickers[${ index }].value`
this.setData({
[key]: typeof detail === 'object' ? detail.value : detail
})
const rgba = this.data.pickers.map(item => item.value)
this.setColor(rgba)
},
chooseHandle(e) {
const { rgba } = e.currentTarget.dataset.row
this.setData({
pickers: this.data.pickers.map((item, index) => {
return {...item, value: rgba[index]}
}),
})
this.setColor(rgba)
},
// 设置颜色
setColor(rgba) {
const hex = rgb2Hex(...rgba)
const rgbaStyle = `rgba(${ rgba.join(',') })`
this.setData({ rgba, hex: hex.replace('#', ''), rgbaStyle })
},
confirmHandle(e) {
this.triggerEvent('confirm', { rgba: this.data.rgba, rgbaStyle: this.data.rgbaStyle })
},
cancelHandle() {
this.triggerEvent('cancel')
},
}
})

到此颜色选择器的组件已经实现了,还剩下一个预览下载的页面。我这边的实现并不是直接页面跳转,因为这边预览之后返回是希望还保留预览之前的数据的。如果直接离开当前页面并清除了数据,不符合用户预期的。所以使用了一个假页。微信小程序提供了一个 page-container 的页面容器,效果类似于 popup 弹出层,页面内存在该容器时,当用户进行返回操作,关闭该容器不关闭页面。


如果二维码中含中文的静态码使用微信扫描后是无法正常展示内容的(后期安排上二维码解析的功能)


感谢大家观看我今日的水文,文笔实在是不行,欢迎鱼友们给小程序提提意见,或者有什么有趣的想法也可以与楼主提一提。最后希望大家到我的小程序来多坐坐。





作者:拖孩
来源:juejin.cn/post/7384350475736989731
收起阅读 »

dockerhub国内镜像站集体下线?别慌,教你丝滑拉取镜像~

web
前言想必大家都听说了,国内镜像站几乎都用不了,对于开发者来说,无疑是个不好的消息。在docker pull时直接超时失败,拉取不下来镜像。那么有没有什么办法解决呢?有!还不止一种。通过docker配置文件配置可用的国内镜像源设置代理自建镜像仓库方法1已经不太好...
继续阅读 »

前言

想必大家都听说了,国内镜像站几乎都用不了,对于开发者来说,无疑是个不好的消息。在docker pull时直接超时失败,拉取不下来镜像。那么有没有什么办法解决呢?有!还不止一种。

  1. 通过docker配置文件配置可用的国内镜像源
  2. 设置代理
  3. 自建镜像仓库

方法1已经不太好使了,能找到可用的不多,有的还存在没有最新的镜像问题。

方法2可行,不过得要有科学上网的工具,再会一点配置代理的知识,操作起来稍稍复杂。

本文主要介绍第三种方法,上手快,简单,关键还0成本!

准备工作

  1. 登录阿里云,找到容器镜像服务,创建一个个人版实例。(第一次使用的话,会让设置访问密码。记住,后面会用)
  2. 找到仓库管理-命名空间,新建一个命名空间且设置为公开

微信截图_20240626174632.png 3.不要创建镜像仓库,回到访问凭证

可以看到,如下2个信息,一个是你的阿里云用户名,一个是你的仓库地址(后面有用)

sudo docker login --username=阿里云用户名 registry.cn-beijing.aliyuncs.com

github配置

  1. fork项目,地址: docker_image_pusher

(感谢tech-shrimp提供的工具)

  1. 在fork后的项目中通过Settings-Secret and variables-Actions-New Repository secret路径,配置4个环境变量
  • ALIYUN_NAME_SPACE-命名空间
  • ALIYUN_REGISTRY_USER-阿里云用户名
  • ALIYUN_REGISTRY_PASSWORD-访问密码
  • ALIYUN_REGISTRY-仓库地址

企业微信截图_20240626203514.png

3.配置要拉取的镜像 打开项目images.txt,每一行配置一个镜像,格式:name:tag 比如

企业微信截图_20240626213138.png

提交修改的文件,则会自动在Actions中创建一个workflow。等待片刻即可(1分钟左右)

企业微信截图_20240626212730.png

5.回到阿里云容器镜像服务控制台-镜像仓库

企业微信截图_20240626213555.png

可以看到镜像已成功拉取并同步到你自己的仓库中。

测试效果

我自己操作了下把nginx的镜像给拉了过来,找台服务器测试一下速度

演示.gif 哈哈!这速度杠杠的吧! 用这个方式的好处是,借助github的action机制,直接从dockerhub上拉取任何你想要的镜像,也不用担心国内镜像站版本更新不及时的问题。再从自建的仓库中pull下来就可以啦! 如果有小伙伴没捣鼓成功的,可以留言给我。


作者:临时工
来源:juejin.cn/post/7384623060199473171
收起阅读 »

微信小程序全新渲染引擎Skyline(入门篇)

web
前言 最近看小程序文档的时候发现了 swiper 组件新增了 Skyline 特有的属性,直接使用竟然没有效果。 不信邪的我打算来研究研究究竟什么是 Skyline!经过一系列文档阅读与实践,长时间闭门造车的我打开了新世界的大门,我惊讶的发现 Skyline...
继续阅读 »

前言


最近看小程序文档的时候发现了 swiper 组件新增了 Skyline 特有的属性,直接使用竟然没有效果。



不信邪的我打算来研究研究究竟什么是 Skyline!经过一系列文档阅读与实践,长时间闭门造车的我打开了新世界的大门,我惊讶的发现 Skyline 引擎很可能是微信小程序未来发展的重点方向,有着更类似原生的交互体验,新增的特性让人连连称叹,特以此文来总结性地介绍一下 Skyline。


双线程模型


了解 Skyline 之前,我们有必要重新复习一下什么是小程序的双线程模型。


如官方文档所言,小程序的运行环境分成渲染层和逻辑层,其中 WXML 模板和 WXSS 样式工作在渲染层,JS 脚本工作在逻辑层。小程序的渲染层和逻辑层分别由2个线程管理:



  • 渲染层的界面使用了WebView 进行渲染,一个小程序存在多个界面,所以渲染层存在多个WebView线程;

  • 逻辑层采用JsCore线程运行JS脚本。


这两个线程的通信会经由微信客户端(原生) 做中转,逻辑层发送网络请求也经由微信客户端 (原生) 转发,有了微信小程序客户端 (原生) 作为媒介系统,使得我们开发者能够专注于数据与逻辑。


如上所述,小程序的通信模型如下图所示。



什么是 Skyline 引擎


前文提到,基于 WebView 和原生控件混合渲染的方式,小程序优化扩展了 Web 的基础能力,所以小程序相对于普通的Web页面有着更为良好的性能与体验。


由于 Web 在移动端的表现与原生应用仍有一定差距,亦或许是 Web 的优化遇到了瓶颈,为了进一步优化小程序性能,微信在 WebView 渲染之外新增了一个渲染引擎,也就是我们本文的重磅主角: Skyline,它使用更精简高效的渲染管线,并带来诸多增强特性,让 Skyline 拥有更接近原生渲染的性能体验。


Skyline 引擎 vs Webview 引擎


我们知道:WebView 的 JS 逻辑、DOM 树创建、CSS 解析、样式计算、Layout、Paint (Composite) 都发生在同一线程,在 WebView 上执行过多的 JS 逻辑可能阻塞渲染,导致界面卡顿,大致流程如下图所示。



但是,在 Skyline 环境下改变了这个情况,它创建了一条渲染线程来负责计算图层布局,图层的绘制以及整合图层页面等渲染任务,并在 AppService 中划出一个独立的上下文,来运行之前 WebView 承担的 JS 逻辑、DOM 树创建等逻辑。



据官方统计数据表明,Skyline 与 WebView 性能相比,具有如下优势:


Skyline 的首屏时间比 WebView 快约 66%



单个页面 Skyline 的占用比 WebView 减少约 35%


单个页面 Skyline 的占用比 WebView 减少 35%,两个页面 Skyline 的内存占用比 WebView 减少 50%,随着打开的页面变多,内存差距越明显。



Skyline 引擎的优点



  • 界面更不容易被逻辑阻塞,进一步减少卡顿

  • 无需为每个页面新建一个 JS 引擎实例(WebView),减少了内存、时间开销

  • 框架可以在页面之间共享更多的资源,进一步减少运行时内存、时间开销

  • 框架的代码之间无需再通过 JSBridge 进行数据交换,减少了大量通信时间开销

  • 保持和原有架构的兼容性,基于 WebView 环境的小程序代码基本上无需任何改动即可直接在新的架构下运行


更多Skyline的特性更新请详见Skyline 渲染引擎 / 概览 / 特性 | 微信开放文档


Skyline 引擎的缺点



  • WXS效率可能有所下降 (WXS 由于被移到 AppService 中,虽然逻辑本身无需改动,但询问页面信息等接口会变为异步,效率也可能有所下降)


但是,也不必过多的担心,微信推出了新的 Worklet 机制,它比原有的 WXS 更靠近渲染流程,用以高性能地构建各种复杂的动画效果。


Skyline 引擎的使用


前文提到,我想使用 swiper 组件新增的 Skyline 特有属性无果,是因为我没有完成 Skyline的配置。如果想要使用 Skyline引擎,我们可以按页面级别来选择性的配置是走 Skyline 引擎或是 Webview 引擎来渲染。


// page.json
{
"renderer": "skyline"
}

// page.json
{
"renderer": "webview"
}

配置完成之后,我们就可以愉快的使用 Skyline 专有的新特性了。


Skyline 引擎的兼容性


我们可能会担心开启了 Skyline 的渲染模式会不会带来兼容性问题。官方表示:



所以我们完全可以放下对兼容性的顾虑,拥抱新的 Skyline 引擎,让大部分的用户优先体验到新一代微信小程序的渲染技术,做第一批吃螃蟹的人!对于我们开发者而言,有必要深入了解一下Skyline引擎的更新带来了哪些开发层面的变化与创新,毕竟,吃螃蟹的人会越来越多嘛。


后记


感谢您的阅读,本文仅为微信小程序 Skyline 引擎的入门介绍篇,后续会持续更新有关 Skyline 引擎相关实际操作及使用的文章,如有兴趣,欢迎持续关注。


作者:阿李贝斯
来源:juejin.cn/post/7298927261210361882
收起阅读 »

2024年令人眼前一亮的Web框架

web
本文翻译自 dev.to/wasp/web-fr… 感谢您的阅读! 介绍 2024年正向我们走来,我们怀着满腔热情为新的一年制定计划,探索未来一年可以学习或实现的目标。此时此刻,正是探寻来年值得学习的框架、理解其功能和特色的最佳时刻。我们以2023年JS 新...
继续阅读 »

本文翻译自 dev.to/wasp/web-fr…

感谢您的阅读!



介绍


2024年正向我们走来,我们怀着满腔热情为新的一年制定计划,探索未来一年可以学习或实现的目标。此时此刻,正是探寻来年值得学习的框架、理解其功能和特色的最佳时刻。我们以2023年JS 新星名单为指引,力求保持客观公正的态度。对于每一个特色框架,我们都将突出其最大的优势,使您能够全面理解它们的优点,从而选择适合自己的框架进行尝试!


HTMX - 返璞归真🚲


htmx-演示


为谁而设:



  • 你希望减少JavaScript的编写量

  • 你希望代码更简单,以超媒体为中心


HTMX在2023年迅速走红,过去一年间在GitHub上赢得了大量星标。HTMX并非普通的JS框架。如果你使用HTMX,你将大部分时间都花在超媒体的世界中,以与我们通常对现代Web开发的JS密集型视角完全不同的视角看待Web开发。HTMX利用HATEOAS(Hypermedia作为应用程序状态的引擎)的概念,使开发人员能够直接从HTML访问浏览器功能,而不是使用Javascript。


此外,它还证明了通过发布令人惊叹的表情符号并以口碑作为主要营销手段,你可以获得人气和认可。不仅如此,你还可能成为HTMX的CEO!它吸引了许多开发人员尝试这种构建网站的方法,并重新思考他们当前的实践。所有这些都使2024年对于这个库的未来发展充满了激动人心的可能性。


Wasp - 全栈,开箱即用🚀


开放SaaS


为谁而设:



  • 你希望快速构建全栈应用

  • 你希望在一个出色的一体化解决方案中继续使用React和Node.js,而无需手动挑选堆栈的每一部分

  • 你希望获得一个为React和Node.js预配置的免费SaaS模板—— Open SaaS


对于希望简单轻松地全面控制其堆栈的工具的用户,无需再寻找!Wasp是一个有主见的全栈框架,利用其编译器以快速简便的方式为你的应用创建数据库、后端和前端。它使用React、Node.js和Prisma,这些都是全栈Web开发人员正在使用的一些最著名的工具。


Wasp的核心是main.wasp文件,它作为你大部分需求的一站式服务。在其中,你可以定义:



  • 全栈身份验证

  • 数据库架构

  • 异步作业,无需额外的基础设施

  • 简单且灵活的部署

  • 全栈类型安全

  • 发送电子邮件(Sendgrid、MailGun、SMTP服务器等)

  • 等等……


最酷的事情是?经过编译器步骤后,你的Wasp应用程序的输出是一个标准的React + Vite前端、Node.js后端和PostgreSQL数据库。从那里,你可以使用单个命令轻松将一切部署到Fly.io等平台。


尽管有些人可能会认为Wasp的有主见立场是负面的,但它却是Wasp众多全栈功能的驱动力。使用Wasp,单个开发人员或小型团队启动全栈项目变得更加容易,尤其是如果你使用预制的模板或OpenSaaS作为你的SaaS起点。由于项目的核心是定义明确的,因此开始一个项目并可能在几天内创建自己的全栈SaaS变得非常容易!


此外,还有一点很酷的是,大多数Web开发人员对大多数现有技术的预先存在的知识仍然在这里适用,因为Wasp使用的技术已经成熟。


Solid.js - 一流的reactivity库 ↔️


扎实的例子


适合人群:



  • 如果你希望代码具有高响应性

  • 现有的React开发人员,希望尝试一种对他们来说学习曲线较低的高性能工具


Solid.js是一个性能很高的Web框架,与React有一些相似之处。例如,两者都使用JSX,采用基于函数的组件方法,但Solid.js不使用虚拟DOM,而是将你的代码转换为纯JavaScript。然而,Solid.js因其利用信号、备忘录和效果实现细粒度响应性的方法而更加出名。信号是Solid.js中最简单、最知名的基本元素。它们包含值及其获取和设置函数,使框架能够观察并在DOM中的确切位置按需更新更改,这与React重新渲染整个组件的方式不同。


Solid.js不仅使用JSX,还对其进行了增强。它提供了一些很酷的新功能,例如Show组件,它可以启用JSX元素的条件渲染,以及For组件,它使在JSX中更轻松地遍历集合变得更容易。另一个重要的是,它还有一个名为Solid Start的元框架(目前处于测试版),它使用户能够根据自己的喜好,使用基于文件的路由、操作、API路由和中间件等功能,以不同的方式渲染应用程序。


Astro - 静态网站之王👑


天文示例


适合人群:



  • 如果您需要一款优秀的博客、CMS重型网站工具

  • 需要一个能够集成其他库和框架的框架


如果您在2023年构建了一个内容驱动的网站,那么很有可能您选择了Astro作为首选框架来实现这一目标!Astro是另一个使用不同架构概念来脱颖而出的框架。对于Astro来说,这是岛屿架构。在Astro的上下文中,岛屿是页面上的任何交互式UI组件,与静态内容的大海形成鲜明对比。由于这些岛屿彼此独立运行,因此页面可以有任意数量的岛屿,但它们也可以共享状态并相互通信,这非常有用。


关于Astro的另一个有趣的事情是,他们的方法使用户能够使用不同的前端框架,如React、Vue、Solid来构建他们的网站。因此,开发人员可以轻松地在其当前知识的基础上构建网站,并利用可以集成到Astro网站中的现有组件。


Svelte - 简单而有效🎯


精简演示


适合人群:



  • 您希望学习一个简单易上手的框架

  • 追求简洁且代码执行速度快的开发体验


Svelte是另一个尝试通过尽可能直接和初学者友好的方式来简化和加速Web开发的框架。它是一个很容易学习的框架,因为要使一个属性具有响应性,您只需声明它并在HTML模板中使用它。 每当在JavaScript中程序化地更新值时(例如,通过触发onClick事件按钮),它将在UI上反映出来,反之亦然。


Svelte的下一步将是引入runes。runes将是Svelte处理响应性的方式,使处理大型应用程序变得更加容易。类似于Solid.js的信号,符文通过使用类似函数的语句提供了一种直接访问应用程序响应性状态的方式。与Svelte当前的工作方式相比,它们将允许用户精确定义整个脚本中哪些部分是响应性的,从而使组件更加高效。类似于Solid和Solid Start,Svelte也有其自己的框架,称为SvelteKit。SvelteKit为用户提供了一种快速启动其由Vite驱动的Svelte应用程序的方式。它提供了路由器、构建优化、不同的渲染和预渲染方式、图像优化等功能。


Qwik - 非常快🚤


qwik演示


适合人群:



  • 如果您想要一个高性能的Web应用

  • 现有的React开发人员,希望尝试一种高性能且学习曲线平缓的框架


最后一个但同样重要的框架是Qwik。Qwik是另一个利用JSX和函数组件的框架,类似于Solid.js,为基于React的开发人员提供了一个熟悉的环境,以便尽快上手。正如其名字所表达的,Qwik的主要目标是实现您应用程序的最高性能和最快执行速度。


Qwik通过利用可恢复性(resumability)的概念来实现其速度。简而言之,可恢复性基于在服务器上暂停执行并在客户端上恢复执行而无需重新播放和下载全部应用程序逻辑的想法。这是通过延迟JavaScript代码的执行和下载来实现的,除非有必要处理用户交互,这是一件非常棒的事情。它使整体速度提高,并将带宽降低到绝对最小值,从而实现近乎瞬间的加载。


结论


在我们所提及的所有框架和库中,最大的共同点是它们的熟悉度。每个框架和库都试图以构建在当前知识基础上的方式吸引潜在的新开发者,而不是做一些全新的事情,这是一个非常棒的理念。


当然,还有许多我们未在整篇文章中提及但值得一提的库和框架。例如,Angular 除了新的标志和文档外,还包括信号和新的控制流。还有 Remix,它增加了对 Vite、React Server Components 和新的 Remix SPA 模式的支持。最后,我们不能忘记 Next.js,它在过去几年中已成为 React 开发者的默认选择,为新的 React 功能铺平了道路。


作者:腾讯TNTWeb前端团队
来源:juejin.cn/post/7339830464000213027
收起阅读 »

Node.js 正在衰退吗?通过一些关键指标告诉你事实如何!

web
关于 “Node.js 凉了吗?” 类似话题大家平常在某乎上也有看到过。 近日 Node.js 官方 Twitter 上转载了一则帖子,看来国外也有此讨论。Node.js TSC 成员 & fastifyjs 首席维护者 @Matteo Collin...
继续阅读 »

关于 “Node.js 凉了吗?” 类似话题大家平常在某乎上也有看到过。



近日 Node.js 官方 Twitter 上转载了一则帖子,看来国外也有此讨论。Node.js TSC 成员 & fastifyjs 首席维护者 @Matteo Collina 对此进行了回复,表示关于 Node.js 衰退的传言被大大夸大了。Node.js 不仅不会消失,而且正在积极进化以满足现代 Web 开发的需求



以下内容翻译自 @Matteo Collina 的博文


在过去的 15 年里,Node.js 一直是 Web 开发的基石。自 2009 年发布以来,它从一个简单的小众技术,发展到如今支持超过 630 万个网站、无数的 API,并被财富 500 强中的 98% 所使用。


作为一个强大的开源运行时环境,Node.js 非常适合数字化转型的挑战。基于熟悉的 JavaScript 基础,Node.js 拥有轻量且事件驱动的架构,这使其非常适合构建可扩展的实时应用程序,能够处理大量并发请求——这是当今 API 驱动世界的关键需求。


结合其活跃且不断增长的开源社区以及 OpenJS 基金会的强力支持,Node.js 已成为当代 Web 开发的支柱。


但最近,有关 Node.js 衰落的传言开始流传。这些说法有多少可信度呢?


在这篇博客中,我们将深入探讨一些关键指标,这些指标描绘了一个繁荣的 Node.js 生态系统,并展现了其光明的未来。我们还将看看已经发布并即将在 Node.js 上推出的主要功能。


技术是永无止境的循环


有些人可能认为新技术不可避免地会使旧技术过时。但事实上,进步往往是建立在现有基础之上的。以 COBOL 为例,这种编程语言创建于 1959 年,今天仍在积极使用。虽然它可能不是前沿 Web 开发的首选,但 COBOL 在银行、金融和政府机构的核心业务系统维护中仍然至关重要。根据最新的 Tiobe 指数,COBOL 正在上升,其受欢迎程度在 Ruby 和 Rust 之间。其持久的相关性突显了一个关键点:技术进步并不总是意味着抛弃过去。


COBOL 正在崛起(来源: tiobe.com/tiobe-index)


让我们考虑另一个 Web 开发领域的老将:jQuery。这款 JavaScript 库比 Node.js 早三年发布,拥有令人印象深刻的使用统计数据——超过 95% 的 JavaScript 网站和 77% 的所有网站都在使用它。jQuery 的持久受欢迎程度表明,技术的年龄并不一定决定其相关性。就像 jQuery 一样,Node.js 尽管更年轻,但也有潜力保持其作为 Web 开发人员宝贵工具的地位。


94.4% 支持 JS 的网站都使用了 jQuery -(来源: w3techs.com/technologies/overview/javascrip..)


Node.js 目前的势头


根据 StackOverflow 的调查,Node.js 是最受欢迎的技术。这种成功依赖于 Node.js 和 npm 注册表的强大组合。这个创新的二人组解决了大规模软件复用的挑战,这是以前无法实现的。


来源:StackOverflow


因此,预先编写的代码模块的使用激增,巩固了 Node.js 作为开发强国的地位。



Readable-stream 的下载量从 2022 年的略高于 30 亿增长到 2023 年的接近 70 亿,意味着使用量在三年内翻了一番。


Node.js 的总下载量:Node.js 每月有高达 1.3 亿的下载量。


然而,理解这一数字包含什么很重要。这些下载量中的很大一部分实际上是头文件。在 npm i 命令期间,这些头文件是临时下载的,用于编译二进制插件。编译完成后,插件会存储在系统上供以后使用。


来源:nodedownloads.nodeland.dev


按操作系统划分的下载量中,Linux 位居榜首。这是有道理的,因为 Linux 通常是持续集成(CI)的首选——软件在开发过程中经过的自动化测试过程。虽然 Linux 主导 CI,但开源项目(OSS)通常在 Windows 上进行额外测试以确保万无一失。


这种高下载量的趋势转化为实际使用。在 2021 年,Node.js 二进制文件的下载量为 3000 万到 2024 年这一数字跃升至 5000 万。在 2023 年,Docker Hub 上的 Node.js 镜像获得了超过 8 亿次下载,提供了 Node.js 在生产环境中使用情况的宝贵洞察。


保持应用程序安全:更新你的 Node.js 版本


许多开发人员和团队无意中让他们的应用程序面临风险,因为他们没有更新 Node.js。以下是保持最新版本的重要性。


Node.js 提供了长期支持(LTS)计划,以确保关键应用程序的稳定性和安全性。然而,版本最终会到达其生命周期的终点,这意味着它们不再接收安全补丁。使用这些过时版本构建的应用程序将面临攻击风险。


例如,Node.js 版本 14 和 16 现在已经被弃用。尽管如此,这些版本每月仍有数百万次下载 —— Node 16 在 2 月份被下载了 2500 万次,而 Node 14 则约为 1000 万次*。令人震惊的是,一些开发人员甚至在使用更旧的版本,如 Node 10 和 12。


LTS 计划


好消息是:更新 Node.js 很容易。推荐的方法是每隔两个 LTS 版本进行升级。例如,如果你当前使用的是 Node.js 16(已不再支持),你应该迁移到最新的 LTS 版本,即目前的 Node.js 20。不要让过时的软件使你的应用程序暴露于安全威胁中。


Node.js 努力确保你的安全


Node.js 非常重视安全性。安全提交会由 Node 技术指导委员会(TSC)进行彻底评估,以确定其有效性。该团队努力确保快速响应时间,目标是在提交报告后 5 天内做出初步响应,通常在 24 小时内实现。


初次响应平均时间


安全修复每季度批量发布。去年,TSC 总共收到了 80 个提交。


Node.js 安全提交


没有 Open Source Security Foundation(OpenSSF)的支持,这种对安全性的承诺是不可能实现的。通过 OpenSSF 领导的 Alpha-Omega 项目,由微软、谷歌和亚马逊资助,Node.js 获得了专门用于提高其安全态势的拨款。该项目于 2022 年启动,旨在通过促进更快的漏洞识别和解决,使关键的开源项目更加安全。这一合作以及 Node.js 对安全工作的专门资金,展示了其保护用户安全的强烈承诺。


安全工作总资金


近年来发布的主要功能


让我们来看看过去几年引入的一些功能。


ESM


Node.js 已经采用了 ECMAScript 模块(ESM)。ESM 提供了一种现代的代码结构方式,使其更清晰和易于维护。


ESM 的一个关键优势是能够在 import 语句中显式声明依赖项。这改善了代码的可读性,并帮助你跟踪项目的依赖关系。因此,ESM 正迅速成为新 Node.js 项目的首选模块格式。


以下是如何在 Node 中使用 ESM 模块的演示:


// addTwo.mjs
function addTwo(num) {
return num + 2;
}

export { addTwo };

// app.mjs
import { addTwo } from './addTwo.mjs';

// 打印:6
console.log(addTwo(4));

线程


Node 还推出了工作线程,允许用户将复杂的计算任务卸载到独立的线程。这释放了主线程来处理用户请求,从而带来更流畅和响应更快的用户体验。


const {
Worker,
isMainThread,
setEnvironmentData,
getEnvironmentData,
} = require('node:worker_threads');

if (isMainThread) {
setEnvironmentData('Hello', 'World!');
const worker = new Worker(__filename);
} else {
console.log(getEnvironmentData('Hello')); // 打印“World!”。
}

Fetch


Node.js 现在内置了 Fetch API 的实现,这是一种现代且符合规范的方式来通过网络获取资源。这意味着你可以编写更清晰和一致的代码,而不必依赖外部库。


Node.js 还引入了几个与 Fetch 一起的新功能,以增强 Web 平台的兼容性。这些功能包括:



  • Web Streams:高效处理大数据流,而不会使应用程序不堪重负。

  • FormData:轻松构建和发送表单数据用于 Web 请求。

  • StructuredClone():创建复杂数据结构的深拷贝。

  • textEncoder() 和 textDecoder():无缝处理文本编码和解码任务。

  • Blob:表示各种用途的原始二进制数据。


结合 Fetch,这些新增功能使你能够在 Node.js 环境中完全构建现代 Web 应用程序。


const res = await fetch('https://example.com');
const json = await res.json();
console.log(json);

Promises


Node.js 提供了内置的 Promise 功能,提供了一种更清晰和结构化的方式来处理异步任务的结果(成功或失败)。


与回调地狱相比,使用 Promises 可以编写更自然、更易于理解的代码。


以下是使用 fs/promises 模块中的 readFile 方法的实际示例,展示了 Promises 如何简化异步文件读取:


import { readFile } from 'node:fs/promises';

try {
const filePath = new URL('./package.json', import.meta.url);
const contents = await readFile(filePath, { encoding: 'utf8' });
console.log(contents);
} catch (err) {
console.error(err.message);
}

Node 独有的核心模块


Node.js 引入了核心模块和用户引入模块的明确区分,使用 "node:" 前缀来标识核心模块


这个前缀像是一个标签,立即将模块标识为 Node.js 的核心构建块。这种区分有几个好处:



  • 减少混淆:不再将核心模块误认为是用户创建的模块。

  • 简化选择:使用 "node:" 前缀轻松选择所需的特定核心模块。


这种变化还防止用户使用可能与未来核心模块冲突的名称注册到 npm 注册表中,如下所示:


import test from 'node:test';
import assert from 'node:assert';

Watch


在引入此功能之前,nodemon 是文件更改监视中最流行的包。


现在,--watch 标志提供了:



  • 自动文件监视:它监视您导入的文件,准备在发生任何更改时立即采取行动。

  • 即时重启:每当修改监视的文件时,Node.js 自动重启,确保您的应用程序反映最新更新。

  • 测试协同作用:--watch 标志与测试运行器友好地协作,在文件更改后自动重新运行测试。这使得开发工作流程变得流畅,提供持续反馈。

  • 为了更精细的控制,--watch-path 标志允许您指定要监视的确切文件。


AsyncLocalStorage


AsyncLocalStorage 允许在 Web 请求或任何其他异步持续时间内存储数据。它类似于其他语言中的线程本地存储。


AsyncLocalStorage 增强了开发人员创建像 React 服务器组件这样的功能,并作为 Next.js 请求存储的基础。这些组件简化了 React 应用程序的服务器端渲染,最终提高了开发者体验。


import http from 'node:http';
import { AsyncLocalStorage } from 'node:async_hooks';

const asyncLocalStorage = new AsyncLocalStorage();

function logWithId(msg) {
const id = asyncLocalStorage.getStore();
console.log(`${id !== undefined ? id : '-'}:`, msg);
}

let idSeq = 0;
http.createServer((req, res) => {
asyncLocalStorage.run(idSeq++, () => {
logWithId('start');
// Imagine any chain of async operations here
setImmediate(() => {
logWithId('finish');
res.end();
});
});
}).listen(8080);

http.get('http://localhost:8080');
http.get('http://localhost:8080');
// 输出:
// 0: start
// 1: start
// 0: finish
// 1: finish

WebCrypto


这个标准化的 API 在 Node.js 环境中直接提供了强大的加密工具集。


使用 WebCrypto,您可以利用以下功能:



  • 密钥生成:创建强大的加密密钥以保护您的数据。

  • 加密和解密:对敏感信息进行加密,以安全存储和传输,并在需要时解密。

  • 数字签名:签署数据以确保真实性并防止篡改。

  • 哈希:生成数据的唯一指纹以进行验证和完整性检查。


通过将 WebCrypto 集成到您的 Node.js 应用程序中,您可以显著增强其安全性,并保护用户数据。


const { subtle } = require('node:crypto').webcrypto;

(async function () {
const key = await subtle.generateKey({
name: 'HMAC',
hash: 'SHA-256',
length: 256
}, true, ['sign', 'verify']);

const enc = new TextEncoder();
const message = enc.encode('I love cupcakes');

const digest = await subtle.sign({
name: 'HMAC'
}, key, message);
})();

实用工具


Node 开始提供了许多实用工具。其核心团队认为用户不应该安装新模块来执行基本实用程序。其中一些实用程序包括以下内容。


Utils.ParseArgs()


Node.js 提供了一个名为 Utils.ParseArgs() 的内置实用程序(或来自 node 模块的 parseArgs 函数),简化了解析应用程序中的命令行参数的任务。这消除了对外部模块的需求,使您的代码库更精简。


那么,Utils.ParseArgs() 如何帮助?它接受传递给您的 Node.js 脚本的命令行参数,并将它们转换为更可用的格式,通常是一个对象。这个对象使得在代码中访问和利用这些参数变得容易。


import { parseArgs } from 'node:util';

const args = ['-f', '--bar', 'b'];
const options = {
foo: {
type: 'boolean',
short: 'f',
},
bar: {
type: 'string',
},
};

const {
values,
positionals,
} = parseArgs({ args, options });

console.log(values, positionals);
// 输出:[Object: null prototype] { foo: true, bar: 'b' } []

单一可执行应用程序


单个可执行应用程序使得通过 Node 分发应用程序成为可能。这在构建和分发 CLI 到用户时非常强大。


这个功能将应用程序代码注入到 Node 二进制文件中。可以分发二进制文件而不必安装 Node/npm。目前仅支持单个 CommonJS 文件。


为了简化创建单个可执行文件,Node.js 提供了一个由 Postman Labs 开发的辅助模块 postject。


权限系统


Node.js 进程对系统资源的访问以及可以执行的操作可以通过权限来管理。还可以通过权限管理其他模块可以访问的模块。


process.permission.has('fs.write');
// true
process.permission.deny('fs.write', '/home/user');

process.permission.has('fs.write');
// true
process.permission.has('fs.write', '/home/user');
// false

测试运行器


它使用 node:test、--test 标志和 npm test。它支持子测试、skip/only 和生命周期钩子。它还支持函数和计时器模拟;模块模拟即将推出。


它还通过 --experimental-test-coverage 提供代码覆盖率和通过 -test-reporter 和 -test-reporter-destination 提供报告器。基于 TTY,默认为 spec、TAP 或 stdout。


import test from 'node:test';
import test from 'test';

test('synchronous passing test', (t) => {
// This test passes because it does not throw an exception.
assert.strictEqual(1, 1);
});

test('synchronous failing test', (t) => {
// This test fails because it throws an exception.
assert.strictEqual(1, 2);
});

test('asynchronous passing test', async (t) => {
// This test passes because the Promise returned by the async
// function is settled and not rejected.
assert.strictEqual(1, 1);
});

test('asynchronous failing test', async (t) => {
// This test fails because the Promise returned by the async
// function is rejected.
assert.strictEqual(1, 2);
});

test('failing test using Promises', (t) => {
// Promises can be used directly as well.
return new Promise((resolve, reject) => {
setImmediate(() => {
reject(new Error('this will cause the test to fail'));
});
});
});

test('callback passing test', (t, done) => {
// done() is the callback function. When the setImmediate() runs, it invokes
// done() with no arguments.
setImmediate(done);
});

test('callback failing test', (t, done) => {
// When the setImmediate() runs, done() is invoked with an Error object and
// the test fails.
setImmediate(() => {
done(new Error('callback failure'));
});
});

require(esm)


一个新的标志已经发布,允许开发者同步地引入 ESM 模块。


'use strict';

const { answer } = require('./esm.mjs');
console.log(answer);


另外,一个新的标志 --experimental-detect-module 允许 Node.js 检测模块是 commonJS 还是 esm。这个新标志简化了在 JavaScript 中编写 Bash 脚本。


WebSocket


WebSocket 是 Node.js 最受欢迎的功能请求之一。这个功能也是符合规范的。



为 Node.js 做贡献


作为一种开源技术,Node.js 主要由志愿者和协作者维护。由于 Node.js 的受欢迎程度不断提高,维护工作也越来越具有挑战性,需要更多的帮助。


Node.js 核心协作者维护 nodejs/node GitHub 仓库。Node.js 核心协作者的 GitHub 团队是 @nodejs/collaborators。协作者具有:



  • 对 nodejs/node 仓库的提交访问权限

  • 对 Node.js 持续集成(CI)作业的访问权限


无论是协作者还是非协作者都可以对 Node.js 源代码提出修改建议。提出修改建议的机制是 GitHub 拉取请求(pull request)。协作者审查并合并(land)拉取请求。


在拉取请求能够合并之前,必须得到两个协作者的批准。(如果拉取请求已经开放超过 7 天,一个协作者的批准就足够了。)批准拉取请求表示协作者对变更负责。批准必须来自不是变更作者的协作者。


如果协作者反对提出的变更,则该变更不能合并。例外情况是,如果 TSC 投票批准变更,尽管存在反对意见。通常,不需要涉及 TSC。


通常,讨论或进一步的更改会导致协作者取消他们的反对。


从根本上说,如果您想对 Node.js 的未来有发言权,请开始贡献!



总结


关于 Node.js 衰退的传言被大大夸大了。深入研究这些指标后,可以清楚地看到:Node.js 不仅不会消失,而且正在积极进化以满足现代 Web 开发的需求


凭借庞大的用户基础、繁荣的开源社区和不断创新的功能,Node.js 仍然是一个强大而多功能的平台。最近增加的 ESM、工作线程、Fetch API 和内置模块表明了它在技术前沿保持领先的承诺。


此外,Node.js 通过专门的团队和严格的流程优先考虑安全性。它的开放协作模式欢迎像您这样的开发人员的贡献,确保平台的光明未来。


因此,无论您是经验丰富的开发人员还是刚刚起步,Node.js 都为构建可扩展和高效的 Web 应用程序提供了一个有力的选择。丰富的资源、活跃的社区和对持续改进的承诺使其成为您下一个项目的坚实基础


参考:



作者:五月君
来源:juejin.cn/post/7379667550505304075
收起阅读 »

uni-app利用renderjs实现截取视频第一帧画面作为封面图

web
需求背景 如下图,使用 uni-app 做 app 时,要上传图片和视频,这里选择图片和视频分别使用的 uni.chooseImage 和 uni.chooseVideo,上传使用的 uni.uploadFile,问题就是这些 API 还有上传成功后服务器返回...
继续阅读 »



需求背景


如下图,使用 uni-app 做 app 时,要上传图片和视频,这里选择图片和视频分别使用的 uni.chooseImageuni.chooseVideo,上传使用的 uni.uploadFile,问题就是这些 API 还有上传成功后服务器返回内容中都没有提供视频封面图,于是只能使用一个固定的图片来充当视频封面,但是这样用户体验很不好


image.png


解决思路


在获取到视频链接后,如果我们可以让视频在后台自动播放,出现第一帧画面后再将它给停掉,在这个过程中利用 canvas 截取到视频播放的第一帧画面保存起来,那不就可以作为视频封面了吗?没那么容易,平时在 H5 环境中,到目前为止就行了,但问题是,现在我这里是 App,然后 uni-app 自带的 video 组件没法截取画面,而 App 环境又没法用 H5 环境的 video 标签,它甚至没有 document 对象, 技术框架上不兼容, 那怎么办?


这时候就需要用到 renderjs 了,毕竟它的核心作用之一就是 “在视图层操作dom,运行 for webjs库”。


那思路就有了,在 renderjs 模块中监听原始模块中的文件列表,当更改时(新增、删除),在 renderjs 中动态创建 video 元素,让它自动静音播放,使用 canvas 截取第一帧画面后销毁 video 元素并将图片传递给原始模块,原始模块将其设置为对应视频的封面


代码逻辑


<template>
<view :prop="canvasList" :change:prop="canvas.getVideoCanvas">
<view v-for="(item,index) in fileList" :key="index">
<image v-if="item.type===0" :src="item.url" @click="previewImage(item.url)">image>
<view v-else @click="previewVideoSrc = item.url">

<image mode="widthFix" :src="item.cover">image>

<u-icon class="play-icon" name="play-right-fill" size="30" color="#fff">u-icon>
view>
view>
<view class="preview-full" v-if="previewVideoSrc!=''">
<video :autoplay="true" :src="previewVideoSrc" :show-fullscreen-btn="false">
<cover-view class="preview-full-close" @click="previewVideoSrc=''"> ×
cover-view>
video>
view>
view>
template>

<script>
import { deepClone } from '@/utils'
// 原始模块
export default {
data() {
return {
previewVideoSrc: '', // 预览视频url
fileList: [
{ url: '', type: 0 },
{ url: '', type: 1 },
{ url: '', type: 1 },
] // 真正用来展示和传递的文件列表,type: 0代表图片,1代表视频
}
},
computed: {
// 用于 renderjs 模块监听,不用 fileList 是因为后续还有更改它(为其内部元素添加 cover )属性
// 监听 fileList 然后又更改它会导致循环递归,这里使用 deepClone 也是为了让 canvasList 不与
// fileList 产生关联
canvasList() {
return deepClone(this.fileList)
}
},
methods: {
// 预览图片
previewImage(url) {
uni.previewImage({
urls: [url]
});
},
// 生成视频封面
getVideoPoster({ index, cover }) {
this.$set(this.fileList[index], 'cover', cover)
},
}
}
script>
<script module="canvas" lang="renderjs">
// renderjs 模块
export default {
methods: {
getVideoCanvas(nV, oV, ownerInstance) {
if(oV !== undefined && Array.isArray(nV) && nV.length > 0) {
nV.forEach((item, index) => {
// 如果是视频
if(item.type == 1) {
// 防止一次性执行过多逻辑导致卡顿
setTimeout(() => {
// 创建video标签
let video = document.createElement("video")
// 设置为自动播放和静音
video.setAttribute('autoplay', 'autoplay')
video.setAttribute('muted', 'muted')
// 设置播放源
video.innerHTML = ''
// 创建 canvas 元素和 2d 画布
let canvas = document.createElement('canvas')
let ctx = canvas.getContext('2d')
// 监听 video 的 canplay 事件
video.addEventListener('canplay', function () {
// 设置宽高
let anw = document.createAttribute("width");
anw.nodeValue = 80;
let anh = document.createAttribute("height");
anh.nodeValue = 80;
canvas.setAttributeNode(anw);
canvas.setAttributeNode(anh);
// 画布渲染
ctx.drawImage(video, 0, 0, 80, 80);
// 生成 base64 图片
let base64 = canvas.toDataURL('image/png')
// 暂停并销毁 video 元素
video.pause()
video.remove();
// 传递数据给逻辑层
ownerInstance.callMethod('getVideoPoster', {
index,
cover: base64
})
}, false)
}, index * 120)
}
})
}
}
}
}
script>

成果展示


image.png


还有另一个地方,之前就是这样的,都是用的默认图片当作封面:


image.png


经过处理后就是这样啦:


image.png


7.gif


作者:鹏北海
来源:juejin.cn/post/7322762833690066981
收起阅读 »

微信小程序区分环境开发 and 合理绕过官方上线审核

web
前言:首先说明一点,虽然绕过官方审核,是不推荐的行为,但是实际的项目开发中,难免会有一些需求或功能在发布上线时,会被官方拒绝。例如类目不对的情况,由于企业性质或其他原因,无法申请相关类目要求的资质;或者申请资质办理难度大、所需时间漫长,无法在上线节点前申请完成...
继续阅读 »

前言:

首先说明一点,虽然绕过官方审核,是不推荐的行为,但是实际的项目开发中,难免会有一些需求或功能在发布上线时,会被官方拒绝。

例如类目不对的情况,由于企业性质或其他原因,无法申请相关类目要求的资质;或者申请资质办理难度大、所需时间漫长,无法在上线节点前申请完成,但是实际业务中确实有此需求。

这就需要在上线时先合理绕过官方审核,以期能顺利发布成功,不耽误业务使用。

一、背景和问题描述

很多开发者在开发项目的时候发现,上线微信小程序最难的不是开发阶段,而是微信审核机制。因为微信为了自身平台规避法律风险,开发的很多功能需要提供相关的正件或者资质,就像前面所说,相关的资质办理难度大,或者一般的公司根本办不下来。那么绕过审核就是一个很重要的上线技巧。
我们之前开发的一个微信小程序,涉及一些视频,发布审核时,被官方认定需要补充“教育服务-在线视频课程类目”。如下图所示:

image.png

但是我们项目中的视频内容是关于“用车知识的介绍和使用须知”,并不属于教育类视频或直播课程,而且我们也拥有“教育服务 > 在线教育”的服务类目,可能跟“在线视频课程”类目不一样。

image.png

可是实际业务中确实需要此功能,那么该如何顺利上线呢?

二、解决思路

因为需要此功能,那么:

  1. 体验版环境下必须能正常展示,才能让测试同事正常测试。
  2. 在提交审核时,即在开发版环境下,此模块需要隐藏,才能绕过官方审核,使审核通过。
  3. 在发布审核成功后,即在正式版环境下,此模块需正常展示,可供用户使用。

三、解决方案

我这边实现了两种解决方法,供大家参考:

方案一

核心: 使用 wx.getAccountInfoSync()

功能描述: 获取当前账号信息。线上小程序版本号仅支持在正式版小程序中获取,开发版和体验版中无法获取。

可参考微信小程序官方文档: 获取当前账号信息:Object wx.getAccountInfoSync()

image.png

具体使用方法如下:

  1. 在小程序项目的app.js文件中的onLaunch中获取小程序账号信息:

image.png

onLaunch: function () {
//启动时动态获取小程序的 appid
const accountinfo = wx.getAccountInfoSync()

wx.setStorageSync('miniProgram', accountinfo.miniProgram)
},
  1. 然后在需要做判断的模块的页面获取miniProgram,我这边是在展示视频模块入口页面获取:
  • js文件中获取账号信息的值:

image.png

data: {
miniProgram: wx.getStorageSync('miniProgram'),
},
  • html文件中进行判断:

image.png

注:我是使用miniProgram.version的值进行判断的。
因为此值是线上小程序版本号,只有在线上环境中才会有值,所以只会在线上环境中展示,提交审核的开发环境中看不到此模块。
而在体验版环境下,我不会加wx:if="{{miniProgram.version}}"这个代码,只有在提交审核时加上。缺点就是需要改动代码,但是能完美避开审核,使审核顺利通过。

方案二

核心: 使用小程序视频插件。
优点: 完美继承完美继承小程序原生的所有特性和事件。不用改代码。

后期我们开发了一个小程序的视频插件,在展示视频的页面中,使用视频插件代替。这样也能完美通过审核。

image.png

这个小程序视频插件作用是,专门为没有视频播放资质的小程序提供视频播放功能,解决视频播放资质问题。

思路来源于官方解答:

image.png

涉小程序插件功能介绍: developers.weixin.qq.com/miniprogram…

涉小程序类目资质、适用范围参考:developers.weixin.qq.com/miniprogram…

以上,希望对大家有帮助!


作者:小蹦跶儿
来源:juejin.cn/post/7340154170234552370
收起阅读 »

微信小程序:uniapp解决上传小程序体积过大的问题

web
概述 在昨天的工作中遇到了一个微信小程序上传代码过大的情况,在这里总结一下具体的解决步骤,首先介绍一下,技术栈是使用uniapp框架+HBuilderX的开发环境。 错误提示 真机调试,提示包提交过大,不能正常生成二维码,后续上传代码更是不可能了,减少包中的...
继续阅读 »

概述


在昨天的工作中遇到了一个微信小程序上传代码过大的情况,在这里总结一下具体的解决步骤,首先介绍一下,技术栈是使用uniapp框架+HBuilderX的开发环境。


错误提示


图4.png


真机调试,提示包提交过大,不能正常生成二维码,后续上传代码更是不可能了,减少包中的体积顺着这条思路去解决问题。


1.静态图片资源改变成网络请求的方式


问题5.png


我们使用的初衷是,把图片加载在static本地,缓存在本地,以便提升更快的响应速度,第一步剥离大的图片更换成网络请求,顺着编辑器提示去处理。


2.对小程序进行分包


小程序主包最大可以加载到1.5M,加载所有的依赖和插件不能大于2M,小程序中有个解决办法是对小程序进行分包处理,使每个包保持在2M的大小,主包和分包之间直接进行跳转,分包和分包不能跳转。


"optimization" : {
"subPackages" : true
},

进行了拆包还是没有解决问题,分包的作用主要运行的是代码,也就是说代码要尽量的小,多了需要进行分解。


3.压缩vendor.js


昨天真正的定位问题是vendor.js 1.88M ,小程序开发代码工具-详情-代码依赖分析中查看,解决vendor.js才是根本的解决之道。


使用HBuilderX打包上传来解决问题,HBuilderX -> 发行 -> 小程序(微信),操作的过程失败了一次,是因为需要注意的是需要绑定开发者后台的地方,开发管理->开发设置->小程序代码上传下载小程序代码上传密钥和绑定IP白名单,这个需要管理员同意。


问题6.png


最后包的体积从12.88M压缩到了4.16M,问题得以解决。


作者:stark张宇
来源:juejin.cn/post/7282363816020508733
收起阅读 »

uni-app app端 人脸识别

web
在听到人脸识别,连忙去看看,去阿里 腾讯 看他们的人脸识别方法,官方sdk什么的。 到后来,需求确定了,拍照(照片)上传,后台去识别是不是本人,这一瞬间从天堂到地狱,放着官方那么好的方法。 用照片,还的自己去写,去实现。 下面为大家提供一个 uni-app 自...
继续阅读 »

在听到人脸识别,连忙去看看,去阿里 腾讯 看他们的人脸识别方法,官方sdk什么的。


到后来,需求确定了,拍照(照片)上传,后台去识别是不是本人,这一瞬间从天堂到地狱,放着官方那么好的方法。


用照片,还的自己去写,去实现。


下面为大家提供一个 uni-app 自动拍照 上传照片 后端做匹配处理。


参考插件市场的 ext.dcloud.net.cn/plugin?id=4…


在使用前 先去manifest.json 选择APP模块配置, 勾选直播推流



直接采用nvue开发,直接使用live-pusher组件进行直播推流,如果是vue开发,则需要使用h5+的plus.video.LivePusher对象来获取


nuve js注意事项


注意nuve 页面 main.js 的封装函数 。无法直接调用(小程序其他的端没有测试)


在APP端 this.api报错,显示是undefined,难道nvue页面,要重新引入api文件


在APP端,main.js中挂载Vuex在nvue页面无法使用this.$store.state.xxx


简单粗暴点直接用uni.getStorageSync 重新获取一遍


//获取用户数据 userInfo在Data里定义


this``.userInfo = uni.getStorageSync(``'userInfo'``)


nuve css注意事项


单位只支持px


其他的em,rem,pt,%,upx 都不支持


需要重新引入外部css


不支持使用 import 的方式引入外部 css


<``style src="@/common/test.css"></``style``>


 默认flex布


display: flex; //不需要写
//直接用下面的标签
flex-direction: column;
align-items: center;
justify-content: space-between;

页面样式


<view class="live-camera" :style="{ width: windowWidth, height: windowHeight }">
<view class="title">
{{second}}秒之后开始识别
</view>
<view class="preview" :style="{ width: windowWidth, height: windowHeight-80 }">
<live-pusher id="livePusher" ref="livePusher" class="livePusher" mode="FHD" beauty="1" whiteness="0"
aspect="2:3" min-bitrate="1000" audio-quality="16KHz" :auto-focus="true" :muted="true"
:enable-camera="true" :enable-mic="false" :zoom="false" @statechange="statechange"
:style="{ width: cameraWidth, height: cameraHeight }">
</live-pusher>

<!--提示语-->
<cover-view class="remind">
<text class="remind-text" style="">{{ message }}</text>
</cover-view>

<!--辅助线-->
<cover-view class="outline-box" :style="{ width: windowWidth, height: windowHeight-80 }">
<cover-image class="outline-img" src="../../static/idphotoskin.png"></cover-image>
</cover-view>
</view>
</view>

JS部分


<script>
import operate from '../../common/operate.js'
import api from '../../common/api.js'
export default {
data() {
return {
//提示
message: '',
//相机画面宽度
cameraWidth: '',
//相机画面宽度
cameraHeight: '',
//屏幕可用宽度
windowWidth: '',
//屏幕可用高度
windowHeight: '',
//流视频对象
livePusher: null,
//照片
snapshotsrc: null,
//倒计时
second: 0,
ifPhoto: false,
// 用户信息
userInfo: []
};
},
onLoad() {
//获取屏幕高度
this.initCamera();
//获取用户数据
this.userInfo = uni.getStorageSync('userInfo')
setTimeout(() => {
//倒计时
this.getCount()
}, 500)
},
onReady() {
// console.log('初始化 直播组件');
this.livePusher = uni.createLivePusherContext('livePusher', this);
},
onShow() {
//开启预览并设置摄像头
/*
* 2023年12月28日
* 在最新的APP上面这个周期 比onReady 直播初始要早执行
* 故而第二次进入页面 相机启动失败
* 把该方法 移步到 onReady 即可
*/


this.startPreview();

},
methods: {
//获取屏幕高度
initCamera() {
let that = this
uni.getSystemInfo({
success: function(res) {
that.windowWidth = res.windowWidth;
that.windowHeight = res.windowHeight;
that.cameraWidth = res.windowWidth;
that.cameraHeight = res.windowWidth * 1.5;
}
});
},
//启动相机
startPreview() {
this.livePusher.startPreview({
success(res) {
console.log('启动相机', res)
}
});
},
//停止相机
stopPreview() {
let that = this
this.livePusher.stopPreview({
success(res) {
console.log('停止相机', res)
}
});
},
//摄像头 状态
statechange(e) {
console.log('摄像头', e);
if (this.ifPhoto == true) {
//拍照
this.snapshot()
}
},
//抓拍
snapshot() {
let that = this
this.livePusher.snapshot({
success(res) {
that.snapshotsrc = res.message.tempImagePath;
that.uploadingImg(res.message.tempImagePath)
}
});
},
// 倒计时
getCount() {
this.second = 5
let timer = setInterval(() => {
this.second--;
if (this.second < 1) {
clearInterval(timer);
this.second = 0
this.ifPhoto = true
this.statechange()
}
}, 1000)
},
// 图片上传
uploadingImg(e) {
let url = e
// console.log(url);
let that = this
uni.uploadFile({
url: operate.api + 'api/common/upload',
filePath: url,
name: 'file',
formData: {
token: that.userInfo.token
},
success(res) {
// console.log(res);
let list = JSON.parse(res.data)
// console.log(list);
that.request(list.data.fullurl)
}
})
},
//验证请求
request(url) {
let data = {
token: this.userInfo.token,
photo: url
}
api.renzheng(data).then((res) => {
// console.log(res);
operate.toast({
title: res.data.msg
})
if (res.data.code == 1) {
setTimeout(() => {
operate.redirectTo('/pages/details/details')
}, 500)
}
if (res.data.code == 0) {
setTimeout(() => {
this.anew(res.data.msg)
}, 500)
}
})
},
// 认证失败,重新认证
anew(msg) {
let that = this
uni.showModal({
content: msg,
confirmText: '重新审核',
success(res) {
if (res.confirm) {
// console.log('用户点击确定');
that.getCount()
} else if (res.cancel) {
// console.log('用户点击取消');
uni.navigateBack({
delta: 1
})
}
}
})
},
}
};
</script>

css 样式


<style lang="scss">
// 标题
.title {
font-size: 35rpx;
align-items: center;
justify-content: center;
}

.live-camera {
.preview {
justify-content: center;
align-items: center;

.outline-box {
position: absolute;
top: 0;
left: 0;
bottom: 0;
z-index: 99;
align-items: center;
justify-content: center;

.outline-img {
width: 750rpx;
height: 1125rpx;
}
}

.remind {
position: absolute;
top: 880rpx;
width: 750rpx;
z-index: 100;
align-items: center;
justify-content: center;

.remind-text {
color: #dddddd;
font-weight: bold;
}
}
}
}
</style>


作者:虚乄
来源:juejin.cn/post/7273126566459719741
收起阅读 »

Jquery4.0发布!下载量依旧是 Vue 的两倍!

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ 背景 其实在去年,Jquery 就宣布了要发布 4 版本 可以看到,Jquery 在五天前发布了 4 版本 Jquery4.0 更新了啥? 接下来说一下到...
继续阅读 »

前言


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


背景


其实在去年,Jquery 就宣布了要发布 4 版本



可以看到,Jquery 在五天前发布了 4 版本




Jquery4.0 更新了啥?


接下来说一下到底更新了啥?


弃用了 1x 和 2x 版本,废弃一些方法


这意味着不再去兼容低版本了,未来 Jquery 将着力于发展新的版本,弃用了一些方法



  • jQuery.cssNumber

  • jQuery.cssProps

  • jQuery.isArray

  • jQuery.parseJSON

  • jQuery.nodeName

  • jQuery.isFunction

  • jQuery.isWindow

  • jQuery.camelCase

  • jQuery.type

  • jQuery.now

  • jQuery.isNumeric

  • jQuery.trim

  • jQuery.fx.interval


Typescript 重构


看过 Jquery 源码的都知道,以前 Jquery 是用 JavaScript 写的,现在新版本是采用 Typescript 重构的,提高整体代码的可维护性


对新特性的支持


jQuery 4.0 将添加对新的 JavaScript 特性的支持,包括:



  • async/await

  • Promise

  • Optional Chaining

  • Nullish Coalescing


优化性能



  • 优化 DOM 操作

  • 改进事件处理

  • 优化 Ajax 请求

  • 增强兼容性


增强兼容性



  • 支持 Internet Explorer 11 和更高版本

  • 支持 Edge 浏览器

  • 支持 Safari 浏览器


FormData 支持


jQuery.ajax 添加了对二进制数据的支持,包括 FormData。


此外,jQuery 4.0 还删除了自动 JSONP 升级、将 jQuery source 迁移至 ES 模块;以及添加了对 Trusted Types 的支持,确保以 TrustedHTML 封装的 HTML 能以不违反 require-trusted-types-for 内容安全策略指令的方式用作 jQuery 操作方法的输入。


由于删除了 Deferreds 和 Callbacks(现在压缩后不到 20k 字节),jQuery 4.0.0 的 slim build 变得更加小巧。


还有人用 Jquery 吗?


随着现在前端发展的迅速,越来越多人投入了 React、Vue 的怀抱,这意味着越来越少人用 Jquery 了,而且用 Jquery 的基本都是老项目,老项目都是求稳的,所以也不会去升级 Jquery


所以我不太看好 Jquery 后续的发展趋势,虽然曾经它真的帮助了我们很多


虽然如此,现阶段 NPM 上,Jquery 的下载量依旧是 Vue 的两倍



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

适配最新微信小程序隐私协议开发指南,兼容uniapp版本

web
前一阵微信小程序官方发布了一个用户隐私保护指引填写说明,说是为了规范开发者的用户个人信息处理行为,保障用户合法权益,小程序、插件中涉及处理用户个人信息的开发者,均需补充相应用户隐私保护指引。 估计是有啥政策强制要求,微信自己也还没想好要咋实现就匆匆发布了第一版...
继续阅读 »

前一阵微信小程序官方发布了一个用户隐私保护指引填写说明,说是为了规范开发者的用户个人信息处理行为,保障用户合法权益,小程序、插件中涉及处理用户个人信息的开发者,均需补充相应用户隐私保护指引。


估计是有啥政策强制要求,微信自己也还没想好要咋实现就匆匆发布了第一版,然后不出意外各种问题,终于在2023年8月22发布了可以正常接入调试的版本。


逛开发者社区很多人在吐槽这个东西,按照现在的实现方式微信完全可以自己在它的框架层实现,非得让开发者多此一举搞个弹窗再去调它的接口通知它。


吐槽归吐槽,代码还是要改的不是,毕竟不改9月15号之后相关功能就直接挂了!时间紧任务重下面直说干货。


准备工作



  • 小程序后台设置用户隐私保护指引,需要等待审核通过:设置-基本设置-服务内容声明-用户隐私保护指引

  • 小程序的基础库版本从 2.32.3 开始支持,所以要选这之后的版本

  • 在 app.json 中加上这个设置 " usePrivacyCheck" : true,在2023年9月15号之前需要自己手动加这个设置,15号之后平台就强制了


具体步骤可以参考官方给的开发文档,里面也有官方提供的 demo 文件。



原生小程序适配代码


直接参考的官方给的 demo3 和 demo4 综合修改出的版本,通过组件的方式引用,所有相关处理逻辑全部放到了 privacy 组件内部,其他涉及到隐私接口的页面只需在 wxml 里引用一下就行了,其他任何操作都不需要,组件内部已经全部处理了。


网上有其他人分享的,要在页面 onLoad、onShow 里获取是否有授权这些,用下面的代码这些都不需要,只要页面里需要隐私授权,引入 privacy 组件后,用户触发了隐私接口时会自动弹出此隐私授权弹窗。



长话短说这一步你总共只需做2个步骤:



  1. 新建一个 privacy 组件:privacy.wxml、privacy.wxss、privacy.js、privacy.json,完整代码在下方

  2. 在涉及隐私接口的页面引入 privacy 组件,如果使用的页面比较多,可以直接在 app.json 文件里通过 usingComponents 全局引入


privacy.wxml


<view wx:if="{{innerShow}}" class="privacy">
<view class="privacy-mask" />
<view class="privacy-dialog-wrap">
<view class="privacy-dialog">
<view class="privacy-dialog-header">用户隐私保护提示</view>
<view class="privacy-dialog-content">感谢您使用本小程序,在使用前您应当阅读井同意<text class="privacy-link" bindtap="openPrivacyContract">《用户隐私保护指引》</text>,当点击同意并继续时,即表示您已理解并同意该条款内容,该条款将对您产生法律约束力;如您不同意,将无法继续使用小程序相关功能。</view>
<view class="privacy-dialog-footer">
<button
id="btn-disagree"
type="default"
class="btn btn-disagree"
bindtap="handleDisagree"
>不同意</button>
<button
id="agree-btn"
type="default"
open-type="agreePrivacyAuthorization"
class="btn btn-agree"
bindagreeprivacyauthorization="handleAgree"
>同意并继续</button>
</view>
</view>
</view>
</view>

privacy.wxss


.privacy-mask {
position: fixed;
z-index: 5000;
top: 0;
right: 0;
left: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.2);
}

.privacy-dialog-wrap {
position: fixed;
z-index: 5000;
top: 16px;
bottom: 16px;
left: 80rpx;
right: 80rpx;
display: flex;
align-items: center;
justify-content: center;
}

.privacy-dialog {
background-color: #fff;
border-radius: 32rpx;
}

.privacy-dialog-header {
padding: 60rpx 40rpx 30rpx;
font-weight: 700;
font-size: 36rpx;
text-align: center;
}

.privacy-dialog-content {
font-size: 30rpx;
color: #555;
line-height: 2;
text-align: left;
padding: 0 40rpx;
}

.privacy-dialog-content .privacy-link {
color: #2f80ed;
}

.privacy-dialog-footer {
display: flex;
padding: 20rpx 40rpx 60rpx;
}

.privacy-dialog-footer .btn {
color: #FFF;
font-size: 30rpx;
font-weight: 500;
line-height: 100rpx;
text-align: center;
height: 100rpx;
border-radius: 20rpx;
border: none;
background: #07c160;
flex: 1;
margin-left: 30rpx;
justify-content: center;
}

.privacy-dialog-footer .btn::after {
border: none;
}

.privacy-dialog-footer .btn-disagree {
color: #07c160;
background: #f2f2f2;
margin-left: 0;
}

privacy.js


let privacyHandler
let privacyResolves = new Set()
let closeOtherPagePopUpHooks = new Set()

if (wx.onNeedPrivacyAuthorization) {
wx.onNeedPrivacyAuthorization(resolve => {
if (typeof privacyHandler === 'function') {
privacyHandler(resolve)
}
})
}

const closeOtherPagePopUp = (closePopUp) => {
closeOtherPagePopUpHooks.forEach(hook => {
if (closePopUp !== hook) {
hook()
}
})
}

Component({
data: {
innerShow: false,
},
lifetimes: {
attached: function() {
const closePopUp = () => {
this.disPopUp()
}
privacyHandler = resolve => {
privacyResolves.add(resolve)
this.popUp()
// 额外逻辑:当前页面的隐私弹窗弹起的时候,关掉其他页面的隐私弹窗
closeOtherPagePopUp(closePopUp)
}

closeOtherPagePopUpHooks.add(closePopUp)

this.closePopUp = closePopUp
},
detached: function() {
closeOtherPagePopUpHooks.delete(this.closePopUp)
}
},
pageLifetimes: {
show: function() {
this.curPageShow()
}
},
methods: {
handleAgree(e) {
this.disPopUp()
privacyResolves.forEach(resolve => {
resolve({
event: 'agree',
buttonId: 'agree-btn'
})
})
privacyResolves.clear()
},
handleDisagree(e) {
this.disPopUp()
privacyResolves.forEach(resolve => {
resolve({
event: 'disagree',
})
})
privacyResolves.clear()
},
popUp() {
if (this.data.innerShow === false) {
this.setData({
innerShow: true
})
}
},
disPopUp() {
if (this.data.innerShow === true) {
this.setData({
innerShow: false
})
}
},
openPrivacyContract() {
wx.openPrivacyContract({
success: res => {
console.log('openPrivacyContract success')
},
fail: res => {
console.error('openPrivacyContract fail', res)
}
})
},
curPageShow() {
if (this.closePopUp) {
privacyHandler = resolve => {
privacyResolves.add(resolve)
this.popUp()
// 额外逻辑:当前页面的隐私弹窗弹起的时候,关掉其他页面的隐私弹窗
closeOtherPagePopUp(this.closePopUp)
}
}
}
}
})

privacy.json


{
"component": true,
"usingComponents": {}
}

uniapp版本


uniapp 版本也可以直接用上面的代码,新建的 privacy 组件放到微信小程序对应的 wxcompnents 目录下,这个目录下是可以直接放微信小程序原生的组件代码的,因为目前只有微信小程序有这个东西,后期还可能随时会更改,所以没必要再额外去封装成 vue 组件了。


页面引用组件的时候直接用条件编译去引用:


{
// #ifdef MP-WEIXIN
"usingComponents": {
"privacy": "/wxcomponents/privacy/privacy"
}
// #endif
}

在 vue 页面里使用组件也要用条件编译:


<template>
<view>
<!-- #ifdef MP-WEIXIN -->
<privacy />
<!-- #endif -->
</view>
</template>

注意uniapp官方目前还没有来适配微信这,目前开发调试 usePrivacyCheck 这个设置放到 page.json 文件里无效的,要放到 manifest.json 文件的 mp-weixin 下面:


{
"name" : "uni-plus",
"appid" : "__UNI__3C6F1BF",
"mp-weixin" : {
"appid" : "wx123456789",
"__usePrivacyCheck__" : true
}
}

作者:cafehaus
来源:juejin.cn/post/7272276908381175819
收起阅读 »

在微信小程序里使用rpx,被坑了😕 | 不同设备表现不同

web
小小需求 实现一个 Tooltip,就是那种很简单有一个按钮,按一下就会出现或消失气泡的组件,就是下面这个效果。 放个按钮、加个点击事件、做好气泡的定位好像就搞定了。然鹅,事情没有发展的这么顺利。 开发 组件结构 按照上面说的过程,放好按钮,添加好点击事...
继续阅读 »

小小需求


实现一个 Tooltip,就是那种很简单有一个按钮,按一下就会出现或消失气泡的组件,就是下面这个效果。


gif


放个按钮、加个点击事件、做好气泡的定位好像就搞定了。然鹅,事情没有发展的这么顺利。


开发


组件结构


按照上面说的过程,放好按钮,添加好点击事件。比较复杂的地方就是处理气泡的定位,气泡需要进行绝对定位,让它脱离文档流,不能在隐藏或偏移的时候还占个坑(需要实现那种浮动的效果)。还有气泡有个小三角,这个三角也是需要额外处理定位的。于是设计了组件的结构如下:
old.png
Tooltip 包着 Button 显示在界面上,设置定位属性,让它可以成为子元素定位的基准元素。然后创建 Prompt,它会相对于 Tooltip 进行定位,Prompt 中的小三角形则相对于 Prompt 进行定位。定位的具体数值则根据元素的尺寸和想放置的位置进行定位。在这个例子中就是实现气泡在按钮下方居中显示, Prompt 偏移数值计算如下:

水平偏移需要考虑 Tooltip 和 Prompt 的宽度,偏移的距离就是两者宽度之差的一半。


move


left=width(Tooltip)/2width(Prompt)/2left = width(Tooltip) / 2 - width(Prompt) / 2

垂直偏移则要考虑 Tooltip、Prompt 和 小三角的高度,计算方法类似。(偷懒不做动图了)

top=height(Tooltip)+hegiht(::before_border)+height(gap)top = height(Tooltip) + hegiht(::before\_border) + height(gap)

小三角相对于气泡的偏移也是类似的计算方法,总之能够根据元素的尺寸让偏移刚好能够居中。


代码


代码如下:(wxml 和 wxss 没有高亮,用 html 和 css 格式代替了)


<view class="tooltip" >
<button size="mini" bindtap="handleTooltipShow">TOOLTIP</button>
<view wx:if="{{showTooltip}}" class="prompt-container">
<view class="prompt-title">这是弹窗标题</view>
<view class="prompt-content">这是弹窗内容,啊吧啊吧吧</view>
</view>
</view>

.tooltip{
display: flex;
align-items: center;
position:relative;
width:350rpx;
height: 64rpx;
}
.prompt-container{
background-color: rgba(0,0,0,0.7);
border-radius: 16rpx;
color: #fff;
width: 200rpx;

position:absolute;
padding: 20rpx;
top: 80rpx;
left: 55rpx;
}
.prompt-container::before{
position:absolute;
content:'';
width: 0rpx;
height: 0rpx;
border: 14rpx solid transparent;
border-bottom-color:rgba(0,0,0,0.7);
top: -28rpx;
left: 103rpx;
}
.prompt-title{
font-size: 26rpx;
font-weight: bold;
}
.prompt-content{
font-size: 24rpx;
}

踩坑


美滋滋呀,这就做完了,在 iPhone 12 mini 模拟器上看起来丝毫没有问题,整个气泡内容看起来是那么完美~


12mini


换个最豪(ang)华(gui)的机型(iPhone 14 Pro Max)看看,大事不妙,怎么小三角和气泡之间出现了一条缝,再看看代码按按计算器,算的尺寸没有任何问题呀!但是展示就是变成了这样:


14pm


爬坑


打开调试器一顿猛调试,发现了一些不对劲,下面慢慢说。


关于 rpx


微信小程序提供了个特殊的单位 rpx,代码中也都是使用这个单位进行开发,据说是能够方便开发者在不同尺寸设备上实现自适应布局。
放一张官方文档截图:
wxwd.png
它的意思就是,它把所有的屏幕宽度都设置为 750rpx,不管这个设备真实的宽度有几个设备独立像素(就是宽度有多少 px)。开发者只需要使用 rpx 为单位,小程序会帮你把 rpx 转成 px,听起来是不是很方便很友好~(但并不是🌚)


试试这个公式是不是真的


根据图中提供的转换方式 1rpx=(screenWidth/750)×px1rpx = (screenWidth / 750) \times px
用上面那个例子中的 Tooltip 组件来进行验证,手动算一下在设备上得到的 px 值是不是真的能用上面的公式计算出来。


设备屏幕尺寸 / 750组件理论尺寸真实尺寸
iPhone 12 mini375 / 750 = 0.5Tooltip175 × 32175 × 32
iPhone 14 Pro Max428 / 750 = 0.57Tooltip199.5 × 36.48199× 36

看起来真实的计算会直接省略小数点后的值,直接进行取整。这可能是导致我们的预期和真实展示有偏差的原因。再看看其他组件的尺寸计算:


设备屏幕尺寸 / 750组件理论尺寸真实尺寸
iPhone 12 mini375 / 750 = 0.5Prompt120120
::before14 × 1414 × 14
iPhone 14 Pro Max428 / 750 = 0.57Prompt136.8136
::before15.96 × 15.9614 × 14

因为高度是根据文字自适应的,所以这里没有计算 Prompt 的高度。但依然可以从表中看出,rpx 到 px 的转换并不是简单的直接取整,不然 iPhone 14 Pro Max 中的 ::before 应该尺寸为 15 × 15。至于到底是所有 rpx 到 px 转换都有隐藏的规则,还是伪元素的尺寸转换和其他元素不统一,还是 border 尺寸计算比较特殊,我们也无从得知,官方也没有相关说明。


小三角相对于气泡的偏移


其实在这个例子中,我们最关心的就是这个小三角相对于气泡的偏移是不是符合预期,整体气泡居中与否那么小的差别我们几乎看不出来,但是这个小三角偏离气泡这段距离,搁谁都无法接受。
那着重看下这个小三角的偏移我们是怎么做的,小三角的尺寸完全是由边构成的,完整的矩形尺寸是 28rpx×28rpx28rpx × 28rpx,我们向上的偏移需要设置成整个矩形的尺寸,也就是 top:28rpxtop: -28rpx,这样才能让下半部分的小三角完全展示出来。理论上来说,尺寸和偏移都设置 rpx 为单位,如果使用统一的转换规则,那肯定也是没问题的,既然出现了问题肯定是两者的计算不是那么的统一。我们看到实际的结果,尺寸计算是不符合我们的预期的,那么就猜测偏移可能是按照公式计算的。可以来验证一下,计算得到的值 1515 和真实值 1414 相差 1px1px,我打算放个高度为 1px1px 的长条在这个缝隙里,看看是不是刚好塞进去。
test.png

竟然真的刚好塞进去了,这说明我的猜测应该没有错,偏移的计算在我们预期中,小三角向上移动了 15px。但不能进行更多的验证了,再猜我就要把这个规律猜出来了(手动狗头🤪)。
总之就是,盒子尺寸的计算和偏移距离的计算用的不是一个规律,这就是坑之所在。


解决方案


针对当前需求


我们可以避开这个坑,让小三角相对于气泡不要产生偏移,而是能死死的贴在气泡上。想要达到这样的效果,我们需要修改一下布局结构,改成下面这个样子:


new.png
让气泡整体和小三角形成兄弟关系,那他俩就不会分离了,然后整体的偏移让他们的父节点 Prompt 来决定。


代码如下:


<view class="tooltip" >
<button size="mini" bindtap="handleTooltipShow">TOOLTIP</button>
<view wx:if="{{showTooltip}}" class="prompt-container">
<view class="prompt-triangle"></view>
<view class="prompt-text-container">
<view class="prompt-title">这是弹窗标题</view>
<view class="prompt-content">这是弹窗内容,啊吧啊吧吧</view>
</view>
</view>
</view>

.tooltip{
display: flex;
align-items: center;
position:relative;
width:350rpx;
height: 64rpx;
}
.prompt-container{
position:absolute;
top: 80rpx;
left: 55rpx;
}
.prompt-triangle{
position:relative;
content:'';
width: 0rpx;
height: 0rpx;
border: 14rpx solid transparent;
border-bottom-color:rgba(0,0,0,0.7);
left: 103rpx;
}
.prompt-text-container{
background-color: rgba(0,0,0,0.7);
border-radius: 16rpx;
color: #fff;
width: 200rpx;
padding: 20rpx;
}
.prompt-title{
font-size: 26rpx;
font-weight: bold;
}
.prompt-content{
font-size: 24rpx;
}


通用方法


除了上面避坑的方法,还有一个方法就是进行一个填坑。自定义实现一个 rpx2px 方法,动态的根据设备来进行 px 值的计算,再通过内联样式传递给元素。


function rpx2px(rpx){
return ( wx.getSystemInfoSync().windowWidth / 750) * rpx
}
Page({
data: {
top: rpx2px(100), // 类似这样定义一个状态,通过内联样式传入
},
})

<view class="tooltip" >
<button size="mini" bindtap="handleTooltipShow">TOOLTIP</button>
<view wx:if="{{showTooltip}}" class="prompt-container">
<view class="prompt-triangle" style="top:{{top}}px"></view>
<!-- 注意上面👆这里添加了 style 内联样式 -->
<view class="prompt-text-container">
<view class="prompt-title">这是弹窗标题</view>
<view class="prompt-content">这是弹窗内容,啊吧啊吧吧</view>
</view>
</view>
</view>

这样子不管是什么尺寸什么偏移都按照统一的规则进行换算,妈妈再也不同担心我被坑啦~~~


总结


虽然解法看起来如此简单,但是爬坑的过程真是无比艰难,各种猜测和假设,虽然一些得到了验证,但最终也是无法猜透小程序的心~~ 只能自己避坑和填坑,按需选择吧。


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

写html页面没意思,来挑战chrome插件开发

web
谷歌浏览器插件开发是指开发可以在谷歌浏览器中运行的扩展程序,可以为用户提供额外的功能和定制化的体验。谷歌浏览器插件通常由HTML、CSS和JavaScript组成,非常利于前端开发者。 开发者可以利用这些技术在浏览器中添加新的功能、修改现有功能或者与网页进行交...
继续阅读 »

谷歌浏览器插件开发是指开发可以在谷歌浏览器中运行的扩展程序,可以为用户提供额外的功能和定制化的体验。谷歌浏览器插件通常由HTML、CSS和JavaScript组成,非常利于前端开发者。
开发者可以利用这些技术在浏览器中添加新的功能、修改现有功能或者与网页进行交互。


要开发谷歌浏览器插件,开发者通常需要创建一个包含*清单文件(manifest.json)、背景脚本(background script)、内容脚本(content script)*等文件的项目结构。清单文件是插件的配置文件,包含插件的名称、版本、描述、权限以及其他相关信息。背景脚本用于处理插件的后台逻辑,而内容脚本则用于在网页中执行JavaScript代码。


谷歌浏览器插件可以实现各种功能,例如添加新的工具栏按钮、修改网页内容、捕获用户输入、与后台服务器进行通信等。开发者可以通过谷歌浏览器插件API来访问浏览器的各种功能和数据,实现各种定制化的需求。
插件开发涉及的要点:


image.png


基础配置


开发谷歌浏览器插件,最重要的文件 manifest.json


{
"name": "Getting Started Example", // 插件名称
"description": "Build an Extension!", // 插件描述
"version": "1.0", // 版本
"manifest_version": 3, // 指定插件版本,这个很重要,指定什么版本就用什么样的api,不能用错了
"background": {
"service_worker": "background.js" // 指定background脚本的路径
},
"action": {
"default_popup": "popup.html", // 指定popup的路径
"default_icon": { // 指定popup的图标,不同尺寸
"16": "/images/icon16.png",
"32": "/images/icon32.png",
"48": "/images/icon48.png",
"128": "/images/icon128.png"
}
},
"icons": { // 指定插件的图标,不同尺寸
"16": "/images/icon16.png",
"32": "/images/icon32.png",
"48": "/images/icon48.png",
"128": "/images/icon128.png"
},
"permissions": [],// 指定应该在脚本中注入那些变量方法,后文再详细说
"options_page": "options.html",
"content_scripts": [ // 指定content脚本配置
{
"js": [ "content.js"], // content脚本路径
"css":[ "content.css" ],// content的css
"matches": ["<all_urls>"] // 对匹配到的tab起作用。all_urls就是全部都起作用
}
]
}


  • name: 插件名称


manifest_version:对应chrome API插件版本,浏览器插件采用的版本,目前共2种版本,是2和最新版3



  • version: 本插件的版本,和发布相关

  • action:点击图标时,设置一些交互

    • default_icon:展示图标

      • 16、32、48、128



    • default_popup:popup.html,一个弹窗页面

    • default_title:显示的标题



  • permissions:拥有的权限

    • tabs:监听浏览器tab切换事件



  • options_ui

  • background:

    • service_worker:设置打开独立页面




官方实例


官方教程


打开pop弹窗页面


设置action的default_popup属性


{
"name": "Hello world",
"description": "show 'hello world'!",
"version": "1.0",
"manifest_version": 3,
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "/images/icon16.png",
"32": "/images/icon32.png",
"48": "/images/icon48.png",
"128": "/images/icon128.png"
}
},
"permissions":["tabs", "storage", "activeTab", "idle"],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"js": [ "content.js"],
"css":[ "content.css" ],
"matches": ["<all_urls>"]
}
]
}

创建popup.html


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>显示出hello world</title>
<link rel="stylesheet" type="text/css" href="popup.css">
</head>

<body>
<h1>显示出hello world</h1>
<button id="clickBtn">点击按钮</button>
<script src="popup.js"></script>
</body>
</html>

文件可以通过链接引入css、js。


body {
width: 600px;
height: 300px;
}
h1 {
background-color: antiquewhite;
font-weight: 100;
}


console.log(document.getElementById('clickBtn'));
document.getElementById('clickBtn').addEventListener('click', function () {
console.log('clicked');
});

点击插件图标


点击图标可以看到如下的popup的页面。


image.png


调试popup.js的方法



  • 通过弹窗,在弹窗内部点击右键,选择审查内容
    image.png

  • 通过插件图标,进行点击鼠标右键,选择审查弹出内容
    image.png


通过background打开独立页面


基于backgroundservice_workerAPI可以打开一个独立后台运行脚本。此脚本会随着插件安装,初始化执行一次,然后一直在后台运行。可以用来存储浏览器的全局状态数据。
background脚本是长时间运行在后台,随着浏览器打开就运行,直到浏览器关闭而结束运行。通常把需要一直运行的、启动就运行的、全局公用的数据放到background脚本。


chrome.action.onClicked.addListener(function () {
chrome.tabs.create({
url: chrome.runtime.getURL('newPage.html')
});
});


为了打开独立页面,需要修改manifest.json


{
"name": "newPage",
"description": "Demonstrates the chrome.tabs API and the chrome.windows API by providing a user interface to manage tabs and windows.",
"version": "0.1",
"permissions": ["tabs"],
"background": {
"service_worker": "service-worker.js"
},
"action": {
"default_title": "Show tab inspector"
},
"manifest_version": 3
}

为了实现打开独立页面,在manifest.json中就不能在配置 action:default_popup
newPage.js文件中可以使用*chrome.tabs*和chrome.windowsAPI;
可以使用 chrome.runtime.getUrl 跳转一个页面。


chrome.runtime.onInstalled.addListener(async () => {
chrome.tabs.create(
{
url: chrome.runtime.getURL('newPage.html'),
}
);
});

content内容脚本


content-scripts(内容脚本)是在网页上下文中运行的文件。通过使用标准的文档对象模型(DOM),它能够读取浏览器访问的网页的详细信息,可以对打开的页面进行更改,还可以将DOM信息传递给其父级插件。内容脚本相对于background还是有一些访问API上的限制,它可以直接访问以下chrome的API



  • i18n

  • storage

  • runtime:

    • connect

    • getManifest

    • getURL

    • id

    • onConnect

    • onMessage

    • sendMessage




content.js运行于一个独立、隔离的环境,它不会和主页面的脚本或者其他插件的内容脚本发生冲突
有2种方式添加content脚本


在配置中设置


"content_scripts": [
{
"js": [ "content.js"],
"css":[ "content.css" ],
"matches": ["<all_urls>"]
}
]

content_scripts属性除了配置js,还可以设置css样式,来实现修改页面的样式。
matches表示需要匹配的页面;
除了这3个属性,还有



  • run_at: 脚本运行时刻,有以下3个选项

    • document_idle,默认;浏览器会选择一个合适的时间注入,并是在dom完成加载

    • document_start;css加载完成,dom和脚本加载之前注入。

    • document_end:dom加载完成之后



  • exclude_matches:排除匹配到的url地址。作用和matches相反。


动态配置注入


在特定时刻才进行注入,比如点击了某个按钮,或者指定的时刻
需要在popup.jsbackground.js中执行注入的代码。


chrome.tabs.executeScript(tabs[0].id, {
code: 'document.body.style.backgroundColor = "red";',
});

也可以将整个content.js进行注入


chrome.tabs.executeScript(tabs[0].id, {
file: "content.js",
});

利用content制作一个弹窗工具


某天不小心让你的女神生气了,为了能够道歉争取到原谅,你是否可以写一个道歉信贴到每一个页面上,当女神打开网站,看到每个页面都会有道歉内容。


image.png


道歉信内容自己写哈,这个具体看你的诚意。
下面设置2个按钮,原谅和不原谅。 点击原谅,就可以关闭弹窗。 点击不原谅,这个弹窗调整css布局位置继续显示。(有点像恶意贴片广告了)


下面设置content.js的内容


let newDiv = document.createElement('div');
newDiv.innerHTML = `<div id="wrapper">
<h3>小仙女~消消气</h3>
<div><button id="cancel">已消气</button>
<button id="reject">不原谅</button></div>
</div>`
;
newDiv.id = 'newDiv';
document.body.appendChild(newDiv);
const cancelBtn = document.querySelector('#cancel');
const rejectBtn = document.querySelector('#reject');
cancelBtn.onclick = function() {
document.body.removeChild(newDiv);
chrome.storage.sync.set({ state: 'cancel' }, (data) => {
});
}
rejectBtn.onclick = function() {
newDiv.style.bottom = Math.random() * 200 + 10 + "px";
newDiv.style.right = Math.random() * 800 + 10 + "px";
}
// chrome.storage.sync.get({ state: '' }, (data) => {
// if (data.state === 'cancel') {
// document.body.removeChild(newDiv);
// }
// });

content.css布局样式


#newDiv {
font-size: 36px;
color: burlywood;
position: fixed;
bottom: 20px;
right: 0;
width: 300px;
height: 200px;
background-color: rgb(237, 229, 216);
text-align: center;
z-index: 9999;
}

打开option页面


options页,就是插件的设置页面,有2个入口



  • 1:点击插件详情,找到扩展程序选项入口


image.png



  • 2插件图标,点击右键,选择 ‘选项’ 菜单


image.png


可以看到设置的option.html页面


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>插件的option配置</title>
</head>
<body>
<h3>插件的option配置</h3>
</body>
</html>

此页面也可以进行js、css的引入。


替换浏览器默认页面


override功能,是可以替换掉浏览器默认功能的页面,可以替换newtab、history、bookmark三个功能,将新开页面、历史记录页面、书签页面设置为自定义的内容。
修改manifest.json配置


{
"chrome_url_overrides": {
"newtab": "newtab.html",
"history": "history.html",
"bookmarks": "bookmarks.html"
}
}

创建一个newtab的html页面


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>new tab</h1>
</body>
</html>

插件更新后,点开新的tab,就会出现我们自定义的页面。第一次的情况会让用户进行选择,是进行更换还是保留原来的配置。


image.png
很多插件都是使用newtab进行自定义打开的tab页,比如掘金的浏览器插件,打开新页面就是掘金网站插件


页面之间进行数据通信


image.png
如需将单条消息发送到扩展程序的其他部分并选择性地接收响应,请调用 runtime.sendMessage()tabs.sendMessage()。通过这些方法,您可以从内容脚本向扩展程序发送一次性 JSON 可序列化消息,或者从扩展程序向内容脚本发送。如需处理响应,请使用返回的 promise。
来源地址:developer.chrome.com/docs/extens…


content中脚本发送消息


chrome.runtime.sendMessage只能放在content的脚本中。


(async () => {
const response = await chrome.runtime.sendMessage({greeting: "hello"});
// do something with response here, not outside the function
console.log(response);
})();

其他页面发送消息


其他页面需向内容脚本发送请求,请指定请求应用于哪个标签页,如下所示。此示例适用于 Service Worker、弹出式窗口和作为标签页打开的 chrome-extension:// 页面


(async () => {
const [tab] = await chrome.tabs.query({active: true, lastFocusedWindow: true});
const response = await chrome.tabs.sendMessage(tab.id, {greeting: "hello"});
// do something with response here, not outside the function
console.log(response);
})();

接收消息使用onMessage


在扩展程序和内容脚本中使用相同的代码


chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
console.log(sender.tab ?
"from a content script:" + sender.tab.url :
"from the extension");
if (request.greeting === "hello")
sendResponse({farewell: "goodbye"});
}
);

添加右键菜单


创建菜单


首先在manifest.json的权限中添加配置


{
"permissions": ["contextMenus"]
}


background.js中添加创建菜单的代码


let menu1 = chrome.contextMenus.create({
type: 'radio', // 可以是 【normal、checkbox、radio】,默认是normal
title: 'click me',
id: "myMenu1Id",
contexts:['image'] // 只有是图片时,菜显示
}, function(){

})

let menu2 = chrome.contextMenus.create({
type: 'normal', // 可以是 【normal、checkbox、radio】,默认是normal
title: 'click me222',
id: "myMenu222Id",
contexts:['all'] //所有类型都显示
}, function(){

})

let menu3 = chrome.contextMenus.create({
id: 'baidusearch1',
title: '使用百度搜索:%s',
contexts: ['selection'], //选择页面上的文字
});

// 删除一个菜单
chrome.contextMenus.remove('myMenu222Id'); // 被删除菜单的id menuItemId
// 删除所有菜单
chrome.contextMenus.removeAll();

// 绑定菜单点击事件
chrome.contextMenus.onClicked.addListener(function(info, tab){
if(info.menuItemId == 'myMenu222Id'){
console.log('xxx')
}
})

以下是其他可以使用的api


// 删除某一个菜单项
chrome.contextMenus.remove(menuItemId);
// 删除所有自定义右键菜单
chrome.contextMenus.removeAll();
// 更新某一个菜单项
chrome.contextMenus.update(menuItemId, updateProperties);
// 监听菜单项点击事件, 这里使用的是 onClicked
chrome.contextMenus.onClicked.addListener(function(info, tab)) {
//...
});

绑定点击事件,发送接口请求


首先需要在manifest.jsonhosts_permissions中添加配置


{
"host_permissions": ["http://*/*", "https://*/*"]
}

创建node服务器,返回json数据


// server.mjs
const { createServer } = require('node:http');
const url = require('url');

const server = createServer((req, res) => {
var pathname = url.parse(req.url).pathname;

if (pathname.includes('api')) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.write(
JSON.stringify({
name: 'John Doe',
age: 30,
})
);
res.end();
} else {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World!\n' + pathname);
}
});

server.listen(8080, '127.0.0.1', () => {
console.log('Listening on 127.0.0.1:8080');
});

编辑background.js文件


// 插件右键快捷键
// 点击右键进行选择
chrome.contextMenus.onClicked.addListener(function (info, tab) {
if (info.menuItemId === 'group1') {
console.log('分组文字1', info);
}
if (info.menuItemId === 'group2') {
console.log('分组文字2');
}
// 点击获取到数据
if (info.menuItemId === 'fetch') {
console.log('fetch 获取数据');
const res = fetch('http://localhost:8080/api', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
}).then((res) => {
console.log(res, '获取到http://localhost:8080/api接口数据');
chrome.storage.sync.set({ color: 'red' }, function (err, data) {
console.log('store success!');
});
});
}
// 创建百度搜索,并跳转到搜索结果页
if (info.menuItemId === 'baidusearch1') {
// console.log(info, tab, "baidusearch1")
// 创建一个新的tab页面
chrome.tabs.create({
url:
'https://www.baidu.com/s?ie=utf-8&wd=' + encodeURI(info.selectionText),
});
}
});

// 创建右键快捷键
chrome.runtime.onInstalled.addListener(function () {
// Create one test item for each context type.
let contexts = [
'page',
'selection',
'link',
'editable',
'image',
'video',
'audio',
];
// for (let i = 0; i < contexts.length; i++) {
// let context = contexts[i];
// let title = "Test '" + context + "' menu item";
// chrome.contextMenus.create({
// title: title,
// contexts: [context],
// id: context,
// });
// }

// Create a parent item and two children.
let parent = chrome.contextMenus.create({
title: '操作数据分组',
id: 'parent',
});
chrome.contextMenus.create({
title: '分组1',
parentId: parent,
id: 'group1',
});
chrome.contextMenus.create({
title: '分组2',
parentId: parent,
id: 'group2',
});
chrome.contextMenus.create({
title: '获取远程数据',
parentId: parent,
id: 'fetch',
});

// Create a radio item.
chrome.contextMenus.create({
title: '创建单选按钮1',
type: 'radio',
id: 'radio1',
});
chrome.contextMenus.create({
title: '创建单选按钮2',
type: 'radio',
id: 'radio2',
});

// Create a checkbox item.
chrome.contextMenus.create({
title: '可以多选的复选框1',
type: 'checkbox',
id: 'checkbox',
});
chrome.contextMenus.create({
title: '可以多选的复选框2',
type: 'checkbox',
id: 'checkbox2',
});

// 在title属性中有一个%s的标识符,当contexts为selection,使用%s来表示选中的文字
chrome.contextMenus.create({
id: 'baidusearch1',
title: '使用百度搜索:%s',
contexts: ['selection'],
});

// Intentionally create an invalid item, to show off error checking in the
// create callback.
chrome.contextMenus.create(
{ title: 'Oops', parentId: 999, id: 'errorItem' },
function () {
if (chrome.runtime.lastError) {
console.log('Got expected error: ' + chrome.runtime.lastError.message);
}
}
);
});

点击鼠标右键,效果如下


image.png


image.png


如果在页面选择几个文字,那么就显示出百度搜索快捷键,


image.png


缓存,数据存储


首先在manifest.json的权限中添加storage配置


{
"permissions": ["storage"]
}

chrome.storage.sync.set({color: 'red'}, function(){
console.log('background js storage set data ok!')
})

然后就可以在content.js或popup.js中获取到数据


// 这里的参数是,获取不到数据时的默认参数
chrome.storage.sync.get({color: 'yellow'}, function(){
console.log('background js storage set data ok!')
})

tabs创建页签


首先在manifest.json的权限中添加tabs配置


{
"permissions": ["tabs"]
}

添加tabs的相关操作


chrome.tabs.query({}, function(tabs){
console.log(tabs)
})
function getCurrentTab(){
let [tab] = chrome.tabs.query({active: true, lastFocusedWindow: true});
return tab;
}

notifications消息通知


Chrome提供chrome.notifications的API来推送桌面通知;首先在manifest.json中配置权限


{
"permissions": [
"notifications"
],
}

然后在background.js脚本中进行创建


// background.js
chrome.notifications.create(null, {
type: "basic",
iconUrl: "drink.png",
title: "喝水小助手",
message: "看到此消息的人可以和我一起来喝一杯水",
});


devtools开发扩展工具


在manifest中配置一个devtools.html


{
"devtools_page": "devtools.html",
}

devtools.html中只引用了devtools.js,如果写了其他内容也不会展示


<!DOCTYPE html>
<html lang="en">
<head> </head>
<body>
<script type="text/javascript" src="./devtools.js"></script>
</body>
</html>

创建devtools.js文件


// devtools.js
// 创建扩展面板
chrome.devtools.panels.create(
// 扩展面板显示名称
"DevPanel",
// 扩展面板icon,并不展示
"panel.png",
// 扩展面板页面
"Panel.html",
function (panel) {
console.log("自定义面板创建成功!");
}
);

// 创建自定义侧边栏
chrome.devtools.panels.elements.createSidebarPane(
"Sidebar",
function (sidebar) {
sidebar.setPage("sidebar.html");
}
);

然后在创建自定的Panel.html和sidebar.html页面。


相关代码下载


作者:北鸟南游
来源:juejin.cn/post/7350571075548397618
收起阅读 »

到底怎样配色才能降低图表的可读性?

web
点赞 + 关注 + 收藏 = 学会了 本文简介 在数据可视化的世界里,图表是我们最常用的语言。但你是否曾被一张图表的配色误导? 配色方案的选择往往被看作是一种艺术,但其实它更是一门科学。 文章将带你一探究竟,哪些配色选择实际上会削弱图表的表达力,甚至误导读者。...
继续阅读 »

点赞 + 关注 + 收藏 = 学会了


本文简介


在数据可视化的世界里,图表是我们最常用的语言。但你是否曾被一张图表的配色误导?


配色方案的选择往往被看作是一种艺术,但其实它更是一门科学。


文章将带你一探究竟,哪些配色选择实际上会削弱图表的表达力,甚至误导读者。


过于丰富的颜色


我管理着10家酒店。以下是这10家酒店在2023年里的收入数据。


1月2月3月4月5月6月7月8月9月10月11月12月
酒店A134501360013200135001370013350136501340013800132001360013700
酒店B680071007300690072007400700073007500760071007200
酒店C152001490015100148001500014700152001490015100148001500014700
酒店D830085008100840086008200850083008600840085008600
酒店E118001200012200121001190012100122001200011800121001190012000
酒店F790081007700830078008400800082008100830084008200
酒店G146501440014700145001480014400146001470014500144001460014800
酒店H550057005800560059005750580059505600590057005800
酒店I143001400014200141001430014200140001410014300142001410014300
酒店J960094009800950097009600990094009800950097009600

我想按月对比酒店G酒店I的收入,并且能直观的知道这两家酒店在所有酒店中的收入属于什么水平。


如果按下图这样展示,对吗?


01.png


粗略一看,这图的数据还挺丰富的,色彩也挺吸引眼球。但你花了多久才找到酒店G酒店I


我们使用 Echarts 等图表库时,通常都会在页面中展示图例。如果想看酒店G酒店I的数据,那我们把其他酒店的数据隐藏掉就行了。


02.png


这样确实能很直观的看到酒店G酒店I的收入趋势和对比。


但把其他酒店的数据隐藏了,又观察不到这两家酒店在所有酒店中的收入水平。


更好的做法是将其他酒店的颜色设置为灰色。


03.png


灰色是一个不起眼的颜色,非常适合用来展示“背景信息”,它不像其他颜色那样吸引眼球。


在上面这个例子中,灰色的主要作用是描述“大环境”,用来凸显想要强调的信息。


但在实际项目中,如果页面的背景色不是白色,又想做到上面这个例子的效果,那可以在页面背景色的基础上往“白色”或者“黑色”方向调色。


04.png


比如,圆点是页面的背景色,红框部分就是可以选择的“背景信息”的颜色。


现在回过头来看看为什么会出现色彩丰富的图表。


我猜有两种可能。


一是项目需求,比如做To G的大屏项目,通常需要炫酷的特效和丰富的色彩去吸引甲方眼球。


二是设计工具或者前端的图表库默认提供了丰富的颜色,开发者只管把数据丢给图表库使用默认的配色去渲染。


配色始终不如一


同一个数据,在不同页中使用了不同的配色方案。用户会觉得你的产品很不专业,也很难培养用户习惯和对品牌的认知。


举个例子,在下方这个图中,顶部的柱状图和下方3个折线图的配色完全不一样。


05.png


反传统的配色


我们的产品支持微信和支付宝这两个支付方式,我们都知道支付宝的主色是蓝色,微信的主色是绿色。


在统计支付来源的数据时,如果出现反传统的配色就会影响用户对数据的理解。


06.png


再错得离谱点的话,可能会将支付宝和微信的主色对掉。


07.png




IMG_0393.GIF


点赞 + 关注 + 收藏 = 学会了


作者:德育处主任
来源:juejin.cn/post/7383268946819792911
收起阅读 »

解决小程序web-view两个恶心问题

web
1.web-view覆盖层问题 问题由来 web-view 是一个 web 浏览器组件,可以用来承载网页的容器,会自动铺满整个页面。 所以这得多恶心。。。不仅铺满,还覆盖了普通的标签,调z-index都无解。 解决办法 web-view内部使用cover-...
继续阅读 »

1.web-view覆盖层问题


问题由来


web-view 是一个 web 浏览器组件,可以用来承载网页的容器,会自动铺满整个页面。



所以这得多恶心。。。不仅铺满,还覆盖了普通的标签,调z-index都无解。



解决办法


web-view内部使用cover-view,调整cover-view的样式即可覆盖在web-view上。


cover-view


覆盖在原生组件上的文本视图。


app-vue和小程序框架,渲染引擎是webview的。但为了优化体验,部分组件如map、video、textarea、canvas通过原生控件实现,原生组件层级高于前端组件(类似flash层级高于div)。为了能正常覆盖原生组件,设计了cover-view。


支持的平台:


AppH5微信小程序支付宝小程序百度小程序

具体实现


<template>
<view>
<web-view :src="viewUrl" v-if="viewUrl" >
<cover-view class="close-view" @click="closeView()">
<cover-image class="close-icon" src="../../static/design/close-icon.png"></cover-image>
</cover-view>
</web-view>
</view>

</template>

.close-view{
position: fixed;
z-index: 99999;
top: 30rpx;
left: 45vw;

.close-icon{
width: 100rpx;
height: 80rpx;
}
}

代码说明:这里的案例是一个关闭按钮图标悬浮在webview上,点击图标可以关闭当前预览的webview。


注意


仅仅真机上才生效,开发者工具上是看不到效果的,如果要调整覆盖层的样式,可以先把web-view标签注释了,写完样式没问题再释放web-view标签。


2.web-view导航栏返回


问题由来



  • 小程序端 web-view 组件一定有原生导航栏,下面一定是全屏的 web-view 组件,navigationStyle: custom 对 web-view 组件无效。


场景


用户在嵌套的webview里填写表单,不小心按到导航栏的返回了,就全没了。


解决办法


使用page-container容器,点击到返回的时候,给个提示。


page-container


页面容器。


小程序如果在页面内进行复杂的界面设计(如在页面内弹出半屏的弹窗、在页面内加载一个全屏的子页面等),用户进行返回操作会直接离开当前页面,不符合用户预期,预期应为关闭当前弹出的组件。 为此提供“假页”容器组件,效果类似于 popup 弹出层,页面内存在该容器时,当用户进行返回操作,关闭该容器不关闭页面。返回操作包括三种情形,右滑手势、安卓物理返回键和调用 navigateBack 接口。


具体实现


<template>
<view>
<web-view :src="viewUrl" v-if="viewUrl" >
<cover-view class="close-view" @click="closeView()">
<cover-image class="close-icon" src="../../static/design/close-icon.png"></cover-image>
</cover-view>
</web-view>
<!--这里这里,就这一句-->
<page-container :show="isShow" :overlay="false" @beforeleave="beforeleave"></page-container>
</view>

</template>

export default {
data() {
return {
isShow: true
}
},
methods: {
beforeleave(){
this.isShow = false
uni.showToast({
title: '别点这里',
icon: 'none',
duration: 3000
})
}
}
}


结语


算是小完美的解决了吧,这里记录一下,看看就行,勿喷。


作者:世界哪有真情
来源:juejin.cn/post/7379960023407198220
收起阅读 »

React Native新架构:恐怖的性能提升

web
自2018年以来,React Native团队一直在重构其核心架构,以便开发者能够创建更高质量更好性能的体验。最近在 React Native 的官网看到他们在安利他们的新的架构,本文将我所了解到的一些皮毛带给大家。以浅薄的见解来揭示其所带来的显著的性能改进,...
继续阅读 »

新架构


自2018年以来,React Native团队一直在重构其核心架构,以便开发者能够创建更高质量更好性能的体验。最近在 React Native 的官网看到他们在安利他们的新的架构,本文将我所了解到的一些皮毛带给大家。以浅薄的见解来揭示其所带来的显著的性能改进,并探讨为何以及如何过渡到这一新架构。


为什么需要新的架构?


多年来,使用React Native构建应用遇到了一些不可避免的限制。比如:React Native的布局和动画效果可能不如原生应用流畅,JavaScript和原生代码之间的通信效率低下,序列化和反序列化开销大,以及无法利用新的React特性等。这些限制在现有架构下无法解决,因此新的架构应运而生。新的架构提升了React Native在数个方面的能力,使得一些之前无法实现的特性和优化成为可能。


同步布局和效果


对比下老的架构(左边)和新的架构(右边)的效果:


React


构建自适应的UI体验通常需要测量视图的大小和位置并进行调整。在现有架构中,使用onLayout事件获取布局信息可能导致用户看到中间状态或视觉跳跃。而在新架构下,useLayoutEffect可以同步获取布局信息并更新,让这些中间状态彻底消失。可以明显看到不会存在跟不上的情况。


function ViewWithTooltip() {
const targetRef = React.useRef(null);
const [targetRect, setTargetRect] = React.useState(null);

useLayoutEffect(() => {
targetRef.current?.measureInWindow((x, y, width, height) => {
setTargetRect({ x, y, width, height });
});
}, [setTargetRect]);

return (
<>
<View ref={targetRef}>
<Text>一些内容,显示一个悬浮提示Text>

View>
<Tooltip targetRect={targetRect} />

);
}

支持并发渲染和新特性


可以看到新架构支持了并发渲染的效果对比,左边是老架构,右边是新架构:


并发渲染特性


新架构支持React 18及之后版本的并发渲染和新特性,例如Suspense数据获取和Transitions。这使得web和原生React开发之间的代码库和概念更加一致。同时,自动批处理减少了重绘的次数,提升了UI的流畅性。


function TileSlider({ value, onValueChange }) {
const [isPending, startTransition] = useTransition();

return (
<>
<View>
<Text>渲染 {value} 瓷砖Text>

<ActivityIndicator animating={isPending} />
View>
<Slider
value={value}
minimumValue={1}
maximumValue={1000}
step={1}
onValueChange={newValue =>
{
startTransition(() => {
onValueChange(newValue);
});
}}
/>

);
}

快速的JavaScript/Native接口


新架构移除了JavaScript和原生代码之间的异步桥接,取而代之的是JavaScript接口(JSI)。JSI允许JavaScript直接持有C++对象的引用,从而大大提高了调用效率。这使得像VisionCamera这样处理实时帧的库能够高效运行,消除大量序列化的开销。


JSI


VisionCamera 的地址是:github.com/mrousavy/re…


目前多达6K+的star,这个在 React Native 上的份量还是响当当的,可以看到它明显是用上了 JSI 了,向先驱们致敬。
VisionCamera


启用新架构的期望


尽管新架构提供了显著的改进,启用新架构并不一定会立即提升应用的性能。你的代码可能需要重构以利用新的功能,如同步布局效果或并发特性。或许,我认为,React Native 可能会同步出一些工具来帮助我们更好的迁移。比如配套的 eslint 插件,提示更优的建议写法等等。


现在是否应该使用新架构?


目前新架构仍被视为实验性,在2024年末发布的React Native版本中将成为默认设置。对于大多数生产环境应用,建议等待正式发布。库维护者则可以尝试启用并确认其用例被覆盖。另外看到react-native-vision-camera 这个库的 issue 下面反馈,JSI 目前还是存在一些坑需要爬的,所以要尝鲜的话,还是要有心理准备。
还是有坑在


通过详细介绍新架构的一系列优势和实际应用,我们可以看到React Native的未来发展前景。尽早了解和适应这些变化,一旦新架构正式发布,我们就能更好地利用React Native的潜力,为用户提供更好的体验。更好的产品体验,意味着产品的竞争力也会更强。


作者:brzhang
来源:juejin.cn/post/7377277576651898899
收起阅读 »

Jenkins 自动化部署微信小程序

web
近期一直参与微信小程序的开发工作,这段时间让我受益匪浅。在整个过程中,学到了很多关于小程序开发的知识和技能,比如如何优化小程序的性能、如何设计更好的用户界面、如何提高小程序的安全性,以及在小程序展示统计图表,层级渲染问题等等。同时,我也深刻认识到了小程序开发中...
继续阅读 »

近期一直参与微信小程序的开发工作,这段时间让我受益匪浅。在整个过程中,学到了很多关于小程序开发的知识和技能,比如如何优化小程序的性能、如何设计更好的用户界面、如何提高小程序的安全性,以及在小程序展示统计图表,层级渲染问题等等。同时,我也深刻认识到了小程序开发中的一些痛点,比如提测和修改bug需要被测试催着在 测试、uat、生产 环境中频繁发版,很是难受,于是想把这些繁琐的步骤交给机器处理,最终确定技术方案,利用Jenkinsuniapp() 还有 官方打包部署预览脚手架(miniprogram-ci) 配置了一套自动化部署的流程


准备


安装jenkins


服务器(本文 服务器系统是Ubuntu 22.04) 安装好jenkins,具体的步骤可以参考这篇文章


jenkins 自动化部署前端项目


配置项目


开发项目git仓库,项目搭建 具体请查看这篇


用Vue打造微信小程序,让你的开发效率翻倍!


打包部署预览原理和脚本编写请移步这篇文章


命令行秒传:一键上传微信小程序和生成二维码预览


上传脚本沿用了这篇文章的中脚本:命令行秒传:一键上传微信小程序和生成二维码预览,只需要略微改动, 改动支持了 设置版本号和备注,且先生成预览二维码和上传到微信小程序后台平台体验版


下面的代码中 appid 和 私钥(小程序后台的私钥 具体配置获取方法请参考上面文章链接)的路径 请自行更改


// 小程序发版
const ci = require('miniprogram-ci');
const path = require('path');
const argv = require('yargs').argv;

const appid = '*******'
let versions = '1.0.1'
let descs = '备注'
let projectPath = './dist/build/mp-weixin'
return (async () => {
if (argv.version) {
versions = argv.version
}
if (argv.descs) {
versions = argv.version
}
// 注意: new ci.Project 调用时,请确保项目代码已经是完整的,避免编译过程出现找不到文件的报错。
const project = new ci.Project({
appid: appid,
type: 'miniProgram',
projectPath: path.join(__dirname, projectPath), // 项目路径
privateKeyPath: path.join(__dirname, './private.*******.key'), // 私钥的路径
ignores: ['node_modules/**/*'],
})
// 生成二维码
const previewResult = await ci.preview({
project,
desc: 'hello', // 此备注将显示在“小程序助手”开发版列表中
setting: {
es6: true,
},
qrcodeFormat: 'image',
qrcodeOutputDest: path.join(__dirname, '/qrcode/destination.jpg'),
onProgressUpdate: console.log,
pagePath: 'pages/index/index', // 预览页面
searchQuery: 'a=1&b=2', // 预览参数 [注意!]这里的`&`字符在命令行中应写成转义字符`\&`
})
console.log('previewResult', previewResult)
console.log('等待5秒后 开始上传')
// 开始上传
let s = 5
let timer = setInterval(async () => {
--s
console.log(`${s}秒`)
if (s == 0) {
clearInterval(timer);
timer = undefined
const uploadResult = await ci.upload({
project,
version: versions,
desc: descs,
setting: {
es6: false, // es6 转 es5
disableUseStrict: true,
autoPrefixWXSS: true, // 上传时样式自动补全
minifyJS: true,
minifyWXML: true,
minifyWXSS: true,
}
})
console.log('uploadResult', uploadResult)
}
}, 1000);
})()

服务器配置 ssh配置


root 用户登录服务器 执行以下命令 切换为jenkins用户


sudo su jenkins

执行生成sshkey命令


 ssh-keygen -t rsa -C "你的邮箱"
// 然后一路回车

image.png


输出ssh私钥 和 公钥 保存备用


 cat ~/.ssh/id_rsa
cat ~/.ssh/id_rsa.pub

image.png


jenkins配置


全局工具配置


配置Git installations


image.png
配置NodeJS版本


image.png


安装jenkins插件


Git Parameter: git分支参数插件


description setter 根据构建日志文件的正则表达式设置每个构建的描述


Version Number 修改版本号


在 Manage Jenkins->插件管理中 搜索 Git Parameter 并且安装重启生效


image.png


image.png


3.jpg


image.png


image.png


配置 ssh


jenkins 全局安全配置


系统管理->全局安全配置->Git Host Key Verification Configuration,选则Manually provided keys


Approved Host Keys中填写上方 服务器的jenkins用户生成的私钥内容


image.png


image.png


git仓库配置 ssh


如果你的项目是私人隐藏的,则需要在项目 配置 SSH 公钥(从上文服务器jenkins用户生成公钥获取内容)


image.png


修改标记格式器


这一步是为了 在构建记录中输出二维码和备注准备


在全局安全配置中 找到标记格式器,改为Safe HTML 保存


image.png


创建任务


首页新建任务


构建一个多配置项目
1.jpg
填写描述、选择github项目写入地址


设置参数


勾选参数化构建过程,添加git参数,输入名称、描述、默认分支
参数类型选择 分支


image.png
继续新增 字符参数 version和remark(这里名字可以自定义,随便起,与shell 脚本中变量名称相匹配就好 )


image.png


配置源码管理


源码管理选择Git,填写 Repository URL,Branches to build 指定分支 ${branch}


image.png


构建环境


勾选 Create a formatted version number


依次填写


Environment Variable Name:BUILD_VERSION


Version Number Format String: ${branch}


Project Start Date: 2023-06-30(项目开始日期)


image.png


Provide Node & npm bin/ folder to PATH 选择 18.16.1


image.png


Build Steps shell脚本


点击 增加构建步骤,选择 执行 shell 输入以下命令(可根据自己的实际情况进行改写)


下方的图片(destination.jpg)存放目录,记得配置文件访问服务,或者自行 编写上传图片逻辑,保证能访问到图片即可;



cd /var/lib/jenkins/workspace/wechart;
npm config set registry https://registry.npmmirror.com/;
npm install -g yarn;
yarn config set registry https://registry.npmmirror.com/;
yarn;
npm run build:mp-weixin;
node upload.js --version=$version --remark=$remark;
mv qrcode/destination.jpg /var/www/html;
chmod -R 777 /var/www/html/destination.jpg;
echo DESC_INFO:http://服务器域名/destination.jpg,$remark;
exit 0;

继续新增构建步骤 选择 Set build description


Regular expression 填写 DESC_INFO:(.*),(.*)


Description 填写 <img src="\1" height:"200" width="200" /><div style="color:blue;">\2</div>


image.png


构建后操作


选择Git Publisher


勾选Push Only If Build Succeeds


点击 Add Tag


Tag to push: wechart-$BUILD_NUMBER


勾选 Create new tag
image.png


点击保存


打包运行构建


选择 Build with Parameters,设置分支(这里会默认显示 git仓库的所有分支)


image.png


打包构建完成后,选择 wechart 点击 配置default


image.png


image.png


image.png


作者:iwhao
来源:juejin.cn/post/7250374485567750203
收起阅读 »

深入理解前端缓存

web
前端缓存是所有前端程序员在成长历程中必须要面临的问题,它会让我们的项目得到非常大的优化提升,同样也会带来一些其它方面的困扰。大部分前端程序员也了解一些缓存相关的知识,比如:强缓存、协商缓存、cookie等,但是我相信大部分的前端程序员不了解它们的缓存机制。接下...
继续阅读 »

前端缓存是所有前端程序员在成长历程中必须要面临的问题,它会让我们的项目得到非常大的优化提升,同样也会带来一些其它方面的困扰。大部分前端程序员也了解一些缓存相关的知识,比如:强缓存协商缓存cookie等,但是我相信大部分的前端程序员不了解它们的缓存机制。接下来我将带你们深入理解缓存的机制以及缓存时间的判断公式,如何合理的使用缓存机制来更好的提升优化。我将会把前端缓存分成HTTP缓存和浏览器缓存两个部分来和大家一起聊聊。


HTTP 缓存


HTTP是一种超文本传输协议,它通常运行在TCP之上,从浏览器Network中可以看到,它分为Respnse Headers(响应头)Request Headers(请求头)两部分组成。


image.png


接下来介绍一下与缓存相关的头部字段:


image.png


expires


我们先来看一下MDN对于expires的介绍



响应标头包含响应应被视为过期的日期/时间。


备注:  如果响应中有指令为 max-age 或 s-maxage 的 Cache-Control 标头,则 Expires 标头会被忽略。



Expires: Wed, 24 Apr 2024 14:27:26 GMT

Cache-Control


Cache-ControlHTTP/1.1中定义的缓存字段,它可以由多种组合使用,分开列如:max-age、s-maxage、public/private、no-cache/no-store等


Cache-Control: max-age=3600, s-maxage=3600, public

max-age是相对当前时间,单位是秒,当设置max-age时则expires就会失效,max-age的优先级更高。


而 s-maxage 与 max-age 不同之处在于,其只适用于公共缓存服务器,比如资源从源服务器发出后又被中间的代理服务器接收并缓存。


public是指该资源可以被任何节点缓存,而private只能提供给客户端缓存。当设置了private之后,s-maxage则会无效。


使用no-store表示不进行资源缓存。使用no-cache表示告知(代理)服务器不直接使用缓存,要求向源服务器发起请求,而当在响应首部中被返回时,表示客户端可以缓存资源,但每次使用缓存资源前都必须先向服务器确认其有效性,这对每次访问都需要确认身份的应用来说很有用。


当然,我们也可以在代码里加入 meta 标签的方式来修改资源的请求首部:


<meta http-equiv="Cache-Control" content="no-cache" />

示例


这里我起了一个nestjs的服务,该getdata接口缓存10s的时间,Ï代码如下:


  @Get('/getdata')
getData(@Response() res: Res) {
return res.set({ 'Expires': new Date(Date.now() + 10).toUTCString() }).json({
list: new Array(1000000).fill(1).map((item, index) => ({ index, item: 'index' + index }))
});Ï
}

第一次请求,花费了334ms的时间。


image.png


第二次请求花费了163ms的时间,走的是磁盘缓存,快了近50%的速度


image.png


接下来我们来验证使用Cache-Control是否可以覆盖Exprie,我们将getdata接口修改如下,Cache-Control设置了1s。Ï我们刷新页面可以看到getdata接口并没有缓存,每次都会想服务器发送请求。


  @Get('/getdata')
getData(@Response() res: Res) {
return res.set({ 'Expires': new Date(Date.now() + 10).toUTCString(), 'Cache-Control': 1 }).json({
list: new Array(1000000).fill(1).map((item, index) => ({ index, item: 'index' + index }))
});
}

仔细的同学应该会发现一个问题,清除缓存后的第一次请求和第二次请求Size的大小不一样,这是为什么呢?


打开f12右键刷新按钮,点击清空缓存并硬性重新加载。


image.png


我们开启Big request rows更方便查看Size的大小,开启时Size显示两行,第一行就是请求内容的大小,第二行则是实际的大小。


image.png


刷新一下,可以看到Size变成了283B大小了。


image.png


带着这个问题我们来深入研究一下浏览器的压缩。HTTP2和HTTP3的压缩算法是大致相同,我们就拿HTTP2的压缩算法(HPACK)来了解一下。


HTTP2 HPACK压缩算法


HPACK压缩算法大致分为:静态Huffman(哈夫曼)压缩和动态Huffman哈夫曼压缩,所谓静态压缩是指根据HTTP提供的静态字典表来查找对应的请求头字段从而存储对应的index值,可以极大的减少内催空间。


动态压缩它是在同一个会话级的,第一个请求的响应里包含了一个比如 {list: [1, 2, 3]},那么就会把它存进表里面,后续的其它请求的响应,就可以只返回这个 header 在动态表里的索引,实现压缩的目的


需要详细了解哈夫曼算法原理的可以去这个博客看一看。


Last-Modified 与 If-Modified-Since


Last-Modified代表资源的最后修改时间,其属于响应首部字段。当浏览器第一次接收到服务器返回资源的 Last-Modified 值后,其会把这个值存储起来,并下次访问该资源时通过携带If-Modified-Since请求首部发送给服务器验证该资源是否过期。


yaml
复制代码
Last-Modified: Fri , 14 May 2021 17:23:13 GMT
If-Modified-Since: Fri , 14 May 2021 17:23:13 GMT

如果在If-Modified-Since字段指定的时间之后资源都没有发生更新,那么服务器会返回状态码 304 Not Modified 的响应。


Etag 与 If--Match


Etag代码该资源的唯一标识,它会根据资源的变化而变化着,同样浏览器第一次收到服务器返回的Etag值后,会把它存储起来,并下次访问该资源通过携带If--Match请求首部发送给服务器验证资源是否过期


Etag: "29322-09SpAhH3nXWd8KIVqB10hSSz66" 
If--Match: "29322-09SpAhH3nXWd8KIVqB10hSSz66"

如果两者不相同则代表服务器资源已经更新,服务器会返回该资源最新的Etag值。


强缓存


强缓存的具体流程如下:


image.png


上面我们介绍了expires设置的是绝对的时间,它会根据客户端的时间来判断,所以会造成expires不准确,如果我有一个资源缓存到到期时间是2024年4月31日我将客户端时间修改成过期的时间,则在一次访问该资产会重新请求服务器获取最新的数据。


max-age则是相对的时间,它的值是以秒为单位的时间,但是max-age也会不准确。


那么到底浏览器是怎么判断该资源的缓存是否有效的呢?这里就来介绍一下资源新鲜度的公式。


我们来用生活中的食品新鲜度来举例:


食品是否新鲜 = (生产日期 + 保质期) > 当前日期

那么缓存是否新鲜也可以借助这个公式来判断


缓存是否新鲜 = (创建时间 + expire || max-age) > 缓存使用期

这里的创建时间可以理解为服务器返回资源的时间,它和expires一样是一个绝对时间。


缓存使用期 = 响应使用期 + 传输延迟时间 + 停留缓存时间

响应使用期


响应使用期有两种获取方式:



  • max(0, responseTime - dateTime)

  • age


responseTime: 是指客户端收到响应的时间

dateTime: 是指服务器创建资源的时间

age:是响应头部的字段,通常是秒为单位


传输延迟时间


传输延迟的时间 = 客户端收到响应的时间 - 请求时间

停留时间


停留时间 = 当前客户端时间 - 客户端收到响应的时间

所以max-age也会失效的问题就是它也使用到了客户端的时间


协商缓存


协商缓存的具体流程如下:


image.png


从上文可以知道,协商缓存就是通过EtagLast-Modified这两个字段来判断。那么这个Etag的标识是如何生成的呢?


我们可以看node中etag第三方库。


该库会通过isState方法来判断文件的类型,如果是文件形式的话就会使用第一种方法:通过文件内容和修改时间来生成Etag


image.png


第二种方法:通过文件内容和hash值和内容长度来生成Etag


image.png


浏览器缓存


我们访问掘金的网站,查看Network可以看到有Size列有些没有大小的,而是disk cachememory cache这样的标识。


image.png


memory cache翻译就是内存缓存,顾名思义,它是存储在内存中的,优点就是速度非常快,可以看到Time列是0ms,缺点就是当网页关闭则缓存也就清空了,而且内存大小是非常有限的,如果要存储大量的资源的话还是使用磁盘缓存。


disk cache翻译就是磁盘缓存,它是存储在计算机磁盘中的一种缓存,它的优缺点和memory cache相反,它的读取是需要时间的,可以看到上方的图片Time列用了1ms的时间。


缓存获取顺序



  1. 浏览器会先查找内存缓存,如果存在则直接获取内存缓存中的资源

  2. 内存缓存没有,就回去磁盘缓存中查找,如果存在就返回磁盘缓存中的资源

  3. 磁盘缓存没有,那么就会进行网络请求,获取最新的资源然后存入到内存缓存或磁盘缓存


缓存存储优先级


浏览器是如何判断该资源要存储在内存缓存还是磁盘缓存的呢?


打开掘金网站可以看到,发现除了base64图片会从内存中获取,其它大部分资源会从磁盘中获取。


image.png


js文件是一个需要注意的地方,可以看到下面的有些js文件会被磁盘缓存有些则会被内存缓存,这是为什么呢?


image.png


Initiator列表示资源加载的位置,我们点击从内存获取资源的该列发现资源在HTML渲染阶段就被加载了,列入一下代码


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>DocumentÏ</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
</head>
<body>
<div id="root">这样加载的js资源大概率会存储到内存中</div>
</body>
</html>

而被内存抛弃的可以发现就是异步资源,这些资源不会被缓存到内存中。


上图我们可以看到有一个Initiator列的值是(index):50但是它还是被内存缓存了,我们可以点击进去看到他的代码如下:


image.png


这个js文件还是通过动态创建script标签来动态引入的。


Preload 与 Prefetch


PreloadPrefetch也会影响浏览器缓存的资源加载。


Preload称为预加载,用在link标签中,是指哪些资源需要页面加载完成后立刻需要的,浏览器会在渲染机制介入前加载这些资源。


<link rel="preload" href="//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/0358ea0.js" as="script">

当使用preload预加载资源时,这些资源一直会从磁盘缓存中读取。


prefetch表示预提取,告诉浏览器下一个页面可能会用到该资源,浏览器会利用空闲时间进行下载并存储到缓存中。


<link rel="prefretch" href="//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/0358ea0.js"Ï>

使用 prefetch 加载的资源,刷新页面时大概率会从磁盘缓存中读取,如果跳转到使用它的页面,则直接会从磁盘中加载该资源。


作者:sorryhc
来源:juejin.cn/post/7382891974942179354
收起阅读 »

Strapi让纯前端成为全栈不再是口号!🚀🚀🚀

web
序言很早以前就知道strap的存在,一直没有机会使用到。很早以前就想找一个类似strapi的框架,来帮我快速搭建后台服务。如果你只懂前端、那么它将非常适合你用来快速构建自己的api服务,以此实现全栈项目开发。strapi是什么?Strapi在国内鲜为人知,但它...
继续阅读 »

序言

很早以前就知道strap的存在,一直没有机会使用到。

很早以前就想找一个类似strapi的框架,来帮我快速搭建后台服务。

如果你只懂前端、那么它将非常适合你用来快速构建自己的api服务,以此实现全栈项目开发。

image.png

strapi是什么?

Strapi在国内鲜为人知,但它在国外的使用情况真的很Nice!

image.png 其仓库也是一直在维护、更新的。

Strapi 是一个开源的 Headless CMS(无头内容管理系统)。它允许开发者通过自定义的方式快速构建、管理和分发内容。Strapi 提供了一个强大的后端 API,支持 RESTful  GraphQL 两种方式,使得开发者可以方便地将内容分发到任何设备或服务,无论是网站、移动应用还是 IoT 设备。

Strapi 的主要特点包括:

  • 灵活性和可扩展性:通过自定义模型、API、插件等,Strapi 提供了极高的灵活性,可以满足各种业务需求。
  • 易于使用的 API:Strapi 提供了一个简洁、直观的 API,使得开发者可以轻松地与数据库进行交互。
  • 内容管理界面:Strapi 提供了一个易于使用的管理界面,使得用户可以轻松地创建、编辑和发布内容。
  • 多语言支持:Strapi 支持多种语言,包括中文、英语、法语、德语等。
  • 可扩展性:Strapi 具有高度的可扩展性,可以通过插件和自定义模块、插件来扩展其功能。
  • 社区支持:Strapi 拥有一个活跃的社区,提供了大量的文档、示例和插件,使得开发人员可以轻松地解决问题和扩展功能。

主要适用场景:

  • 多平台内容分发( 将内容分发到不同web、h5等不同平台 
  • 定制化 CMS 需求( 通过插件等扩展性高度定制 
  • 快速开发api(API管理界面能够大大加快开发速度,尤其是MVP(最小可行产品)阶段 

strapi实战

光看官网界面原官网地址),还是相当漂亮的🙈:

安装Strapi

超级简单,执行下面的命令后、坐等服务启动

(安装完后,自动执行了strapi start,其mysql、语言切换、权限配置等都内置到了@strapi包中)

yarn create strapi-app my-strapi --quickstart

浏览器访问:http://localhost:1337/admin/

第一步就完成了、是不是so easy😍,不过默认是英文的,虽然英语还凑合,但使用起来还是多有不便。strapi原本就支持国际化,我们来切换成中文再继续操作。

语言切换

  1. 设置国际化

  1. 个人设置中配置语言即可:

如果看不到"中文(简体)"选项,就在项目根目录下执行build并重启:npm run build && npm start,再刷新页面应该就能看到了。注意npm start默认是生产环境的启动(只能使用表,无法创建表)、开发环境启动用"npm run develop"

strapi的基础使用

在第一步完成的时候,其实数据库就已经搭建好了,我们只管建表、增加curd的接口即可

1. 建表

设置字段、可以选择需要的类型:

在保存左边的按钮可以继续添加字段

blog字段、建模完成后,进入内容管理器给表插入数据

2. curd

上面只是可视化的查看、插入数据,怎样才能变成api来进行curd了。

  • 设置API令牌,跟进提示操作

  • 权限说明

find GET请求 /api/blogs 查找所有数据

findone GET请求 /api/blogs/:id 查找单条数据

create POST请求 /api/blogs 创建数据

update PUT请求 /api/blogs/:id 更新数据

delete DELETE请求 /api/blogs/:id 删除数据

  • postman调试

先给blog公共权限,以便调试:

  1. 查找所有数据(find)

  1. 查找单条数据(findone)

  1. 更新修改数据(update)

  1. 删除数据(delete),返回被删除的数据

再次查看:

好了,恭喜你。 到这一步,你已经掌握了strapi curd的使用方法。


strapi数据可视化、Navicat辅助数据处理

Strapi 支持多种数据库,包括 MySQL、PostgreSQL、MongoDB 和 SQLite,并且具有高度的可扩展性和自定义性,可以满足不同项目的需求。(默认使用的是SQLite数据库)

我们也可以借助Navicat等第三个工具来实现可视化数据操作:

其用户名、密码默认都是strapi

strapi数据迁移

SQLite数据库

如果你只是需要将SQLite数据库从一个环境迁移到另一个环境(比如从一个服务器迁移到另一个服务器),操作相对简单:

  1. 备份SQLite数据库文件:找到你的SQLite数据库文件(默认位置是项目根目录下的 .tmp/data.db)并将其复制到安全的位置。
  2. 迁移文件:将备份的数据库文件移动到新环境的相同位置。
  3. 更新配置(如有必要) :如果新环境中数据库文件的位置有变化,确保更新Strapi的数据库配置文件(./config/database.js)以反映新的文件路径。

SQLite到其他数据库系统

如果你需要将SQLite数据库迁移到其他类型的数据库系统,比如PostgreSQL或MySQL,流程会更复杂一些:

  1. 导出SQLite数据:首先,你需要导出SQLite数据库中的数据。这可以通过多种工具完成,例如使用sqlite3命令行工具或一个图形界面工具(如DB Browser for SQLite)来导出数据为SQL文件。
  2. 准备目标数据库:在目标数据库系统中创建一个新的数据库,为Strapi项目准备使用。
  3. 修改Strapi的数据库配置:根据目标数据库类型,修改Strapi的数据库配置文件(./config/database.js)。你需要根据目标数据库系统的要求配置连接参数。
  4. 导入数据到目标数据库:使用目标数据库系统的工具导入之前导出的数据。不同数据库系统的导入工具和命令会有所不同。例如,对于PostgreSQL,你可能会使用psql工具,对于MySQL,则可能使用mysql命令行工具。
  5. 处理数据类型和结构差异:不同的数据库系统在数据类型和结构上可能会有所差异。在导入过程中,你可能需要手动调整SQL文件或在导入后调整数据库结构,尤其是对于关系和外键约束。
  6. 测试:迁移完成后,彻底测试你的Strapi项目,确保数据正确无误,所有功能正常工作。

注意事项

  • 数据兼容性:在不同数据库系统之间迁移时,可能会遇到数据类型不兼容的问题,需要仔细处理。
  • 性能调优:迁移到新的数据库系统后,可能需要根据新的数据库特性进行调优以确保性能。
  • 备份:在进行任何迁移操作之前,总是确保已经备份了所有数据和配置。

具体步骤可能会因你的具体需求和所使用的数据库系统而异。根据你的目标数据库系统,可能有特定的迁移工具和服务可以帮助简化迁移过程。

小结

好了,梳理一下。现在我要建一套博客系统的API该怎么做了?

  1. 安装启动(已安装可忽略)yarn create strapi-app my-strapi --quickstart
  2. 在后台建表建模、设置字段
  3. 设置表的API调用权限
  4. 在需要用到的地方使用即可
    是不是超级简单了!

总结

上面我们了解了strapi的后台使用、curd操作、数据迁移等。相信大家都能快速掌握使用。我们无需基于ORM框架去搭建数据模型,也无需使用python、nestjs等后台框架去创建后台服务了。 这势必能大大提升我们的开发效率。

后续会再继续讲解strapi富文本插件的使用,有了富文本的加持、我们都能省去搭建管理后台了,如果用来做博客、高度定制化的文档系统将是非常不错的选择。


作者:tager
来源:juejin.cn/post/7340152660224819212

收起阅读 »

未登录也能知道你是谁?浏览器指纹了解一下!

web
引言 大多数人都遇到过这种场景,我在某个网站上浏览过的信息,但我并未登录,可是到了另一个网站发现被推送了类似的广告,这是为什么呢? 本文将介绍一种浏览器指纹的概念,以及如何利用它来判断浏览者身份。 浏览器指纹 浏览器指纹是指通过浏览器的特征来唯一标识用户身份的...
继续阅读 »

引言


大多数人都遇到过这种场景,我在某个网站上浏览过的信息,但我并未登录,可是到了另一个网站发现被推送了类似的广告,这是为什么呢?


本文将介绍一种浏览器指纹的概念,以及如何利用它来判断浏览者身份。


浏览器指纹


浏览器指纹是指通过浏览器的特征来唯一标识用户身份的一种技术。


它通过记录用户浏览器的一些基本信息,包括操作系统、浏览器类型、浏览器版本、屏幕分辨率、字体、颜色深度、插件、时间戳等,通过这些信息,可以唯一标识用户身份。


应用场景


其实浏览器指纹这类的技术已经被运用的很广泛了,通常都是用在一些网站用途上,比如:



  • 资讯等网站:精准推送一些你感兴趣的资讯给你看

  • 购物网站: 精确推送一些你近期浏览量比较多的商品展示给你看

  • 广告投放: 有一些网站是会有根据你的喜好,去投放不同的广告给你看的,大家在一些网站上经常会看到广告投放吧?

  • 网站防刷: 有了浏览器指纹,就可以防止一些恶意用户的恶意刷浏览量,因为后端可以通过浏览器指纹认得这些恶意用户,所以可以防止这些用户的恶意行为

  • 网站统计: 通过浏览器指纹,网站可以统计用户的访问信息,比如用户的地理位置、访问时间、访问频率等,从而更好的为用户提供服务


如何获取浏览器指纹


指纹算法有很多,这里介绍一个网站 https://browserleaks.com/ 上面介绍了很多种指纹,可以根据自己的需要选择。



这里我们看一看canvas,可以看到光靠一个canvas的信息区分,就可以做到15万用户只有7个是重复的,如果结合其他信息,那么就可以做到更精准的识别。


canvas指纹


canvas指纹的原理就是通过 canvas 生成一张图片,然后将图片的像素点信息记录下来,作为指纹信息。


不同的浏览器、操作系统、cpu、显卡等等,画出来的 canvas 是不一样的,甚至可能是唯一的。


具体步骤如下:



  1. 用canvas 绘制一个图像,在画布上渲染图像的方式可能因web浏览器、操作系统、图形卡和其他因素而异,从而生成可用于创建指纹的唯一图像。在画布上呈现文本的方式也可能因不同web浏览器和操作系统使用的字体渲染设置和抗锯齿算法而异。




  1. 要从画布生成签名,我们需要通过调用toDataURL() 函数从应用程序的内存中提取像素。此函数返回表示二进制图像文件的 base64 编码字符串。然后,我们可以计算该字符串的MD5哈希来获得画布指纹。或者,我们可以从IDAT块中提取 CRC校验和IDAT块 位于每个 PNG 文件末尾的16到12个字节处,并将其用作画布指纹。


我们来看看结果,可以知道,无论是否在无痕模式下,都可以生成相同的 canvas 指纹。
在这里插入图片描述


换台设备试试



其他浏览器指纹


除了canvas,还有很多其他的浏览器指纹,比如:


WebGL 指纹


WebGL(Web图形库)是一个 JavaScript API,可在任何兼容的 Web 浏览器中渲染高性能的交互式 3D2D 图形,而无需使用插件。


WebGL 通过引入一个与 OpenGL ES 2.0 非常一致的 API 来做到这一点,该 API 可以在 HTML5 元素中使用。


这种一致性使 API 可以利用用户设备提供的硬件图形加速。


网站可以利用 WebGL 来识别设备指纹,一般可以用两种方式来做到指纹生产:


WebGL 报告——完整的 WebGL 浏览器报告表是可获取、可被检测的。在一些情况下,它会被转换成为哈希值以便更快地进行分析。


WebGL 图像 ——渲染和转换为哈希值的隐藏 3D 图像。由于最终结果取决于进行计算的硬件设备,因此此方法会为设备及其驱动程序的不同组合生成唯一值。这种方式为不同的设备组合和驱动程序生成了唯一值。


可以通过 Browserleaks test 检测网站来查看网站可以通过该 API 获取哪些信息。


产生 WebGL 指纹原理是首先需要用着色器(shaders)绘制一个梯度对象,并将这个图片转换为Base64 字符串。


然后枚举 WebGL 所有的拓展和功能,并将他们添加到 Base64 字符串上,从而产生一个巨大的字符串,这个字符串在每台设备上可能是非常独特的。


例如 fingerprint2js 库的 WebGL 指纹生产方式:


HTTP标头


每当浏览器向服务器发送请求时,它会附带一个HTTP标头,其中包含了诸如浏览器类型、操作系统、语言偏好等信息。


这些信息可以帮助网站优化用户体验,但同时也能用来识别和追踪用户。


屏幕分辨率


屏幕分辨率指的是浏览器窗口的大小和设备屏幕的能力,这个参数因用户设备的不同而有所差异,为浏览器指纹提供了又一个独特的数据点。


时区


用户设备的本地时间和日期设置可以透露其地理位置信息,这对于需要提供地区特定内容的服务来说是很有价值的。


浏览器插件


用户安装的插件列表是非常独特的,可以帮助形成识别个体的浏览器指纹。


音频和视频指纹


通过分析浏览器处理音频和视频的方式,网站可以获取关于用户设备音频和视频硬件的信息,这也可以用来构建用户的浏览器指纹。


webgl指纹案例


那么如何防止浏览器指纹呢?


先讲结论,成本比较高,一般人不会使用。


现在开始实践,根据上述的原理,我们知道了如何生成一个浏览器指纹,我们只需要它在获取toDataURL时,修改其中的内容,那么结果就回产生差异,从而无法通过浏览器指纹进行识别。


那么,我们如何修改toDataURL的内容呢?


我们不知道它会在哪里调用,所以我们只能通过修改它的原型链来修改。


又或者使用专门的指纹浏览,该浏览器可以随意切换js版本等信息来造成无序的随机值。


修改 toDataURL


第三方指纹库


FingerprintJS



FingerprintJS是一个源代码可用的客户端浏览器指纹库,用于查询浏览器属性并从中计算散列访问者标识符。


cookie和本地存储不同,指纹在匿名/私人模式下保持不变,即使浏览器数据被清除。


ClientJS Library



ClientJS 是另一个常用的JavaScript库,它通过检测浏览器的多个属性来生成指纹。


该库提供了易于使用的接口,适用于多种浏览器指纹应用场景。




作者:我码玄黄
来源:juejin.cn/post/7382344353069088803
收起阅读 »