注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

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

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
收起阅读 »

h5 如何跳转微信小程序(uni-app)?

web
前几天组员遇到h5跳转微信小程序的功能,由于时间关系,我这边接手实现这个功能。 遇到问题点 微信内部跳转怎么实现? 外部浏览器或者app打开链接如何跳转? 微信内部跳转怎么实现 使用 wx-open-launch-weapp 这边标签进行跳转,使用这个标签...
继续阅读 »

前几天组员遇到h5跳转微信小程序的功能,由于时间关系,我这边接手实现这个功能。


遇到问题点



  • 微信内部跳转怎么实现?

  • 外部浏览器或者app打开链接如何跳转?


微信内部跳转怎么实现

使用 wx-open-launch-weapp 这边标签进行跳转,使用这个标签跳转小程序,需要满足3个条件



  • 首先 wx.config 授权,公众号设置 > 功能设置 > js安全域名设置

  • 域名设置,静态资源托管

  • 小程序需要关联这个公众号 设置 > 基本设置 > 相关公众号


h5上代码源码



<html>
<head>
<title>打开小程序</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
<script>
window.onerror = e => {
console.error(e)
alert('发生错误' + e)
}
</script>
<!-- 引入jQuery -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
<!-- weui 样式 -->
<link rel="stylesheet" href="https://res.wx.qq.com/open/libs/weui/2.4.1/weui.min.css"></link>
<!-- 调试用的移动端 console -->
<script src="https://cdn.jsdelivr.net/npm/eruda"></script>
<script>eruda.init();</script>
<!-- 公众号 JSSDK -->
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<!-- 云开发 Web SDK -->
<script src="https://res.wx.qq.com/open/js/cloudbase/1.1.0/cloud.js"></script>
<script>
const baseUrl ="/pages/biddingCenter/index"
function getQueryVariable(variable)
{
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if(pair[0] == variable){return pair[1];}
}
return(false);
}

function docReady(fn) {
if (document.readyState === 'complete' || document.readyState === 'interactive') {
fn()
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
var version = 'trial'
var fetchData = new Promise((resolve, reject) => {
// 获取签名,timestamp、nonceStr、signature
$.ajax({
url: "xxxxxxx",
dataType: "json",
method: 'get',
data: { xxx },

success: function (res) {
console.log("WeChatConfig", res);
if (res.data) {
var data = res.data; // 根据实际情况返还的数据进行赋值
console.log(999, data)
resolve(data);
}
},
error: function (error) {
console.error('error-->', error)
return reject(error)
}
})
});



docReady(async function() {
var ua = navigator.userAgent.toLowerCase()
var isWXWork = ua.match(/wxwork/i) == 'wxwork'
var isWeixin = !isWXWork && ua.match(/MicroMessenger/i) == 'micromessenger'
var isMobile = false
var isDesktop = false
if (navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|IEMobile)/i)) {
isMobile = true
} else {
isDesktop = true
}
console.warn('ua', ua)
console.warn(ua.match(/MicroMessenger/i) == 'micromessenger')
var m = ua.match(/MicroMessenger/i)
console.warn(m && m[0] === 'micromessenger')

if (isWeixin) {
var containerEl = document.getElementById('wechat-web-container')
containerEl.classList.remove('hidden')
containerEl.classList.add('full', 'wechat-web-container')

var launchBtn = document.getElementById('launch-btn')
launchBtn.addEventListener('ready', function (e) {

launchBtn.setAttribute('env-version',version)
launchBtn.setAttribute('path', baseUrl)

console.log('开放标签 ready')
})
launchBtn.addEventListener('launch', function (e) {
console.log('开放标签 success')
})
launchBtn.addEventListener('error', function (e) {
console.log('开放标签 fail', e.detail)
})

await fetchData.then(res=> {
wx.config({
// debug: true, // 调试时可开启
appId: res.appId,
timestamp: res.timestamp, // 必填,填任意数字即可
nonceStr: res.nonceStr, // 必填,填任意非空字符串即可
signature: res.signature, // 必填,填任意非空字符串即可
jsApiList: ['chooseImage'], // 安卓上必填一个,随机即可
openTagList:['wx-open-launch-weapp'], // 填入打开小程序的开放标签名
})
})

} else if (isDesktop) {
// 在 pc 上则给提示引导到手机端打开
var containerEl = document.getElementById('desktop-web-container')
containerEl.classList.remove('hidden')
containerEl.classList.add('full', 'desktop-web-container')
} else {

var containerEl = document.getElementById('public-web-container')
containerEl.classList.remove('hidden')
containerEl.classList.add('full', 'public-web-container')
var c = new cloud.Cloud({
// 必填,表示是未登录模式
identityless: true,
// 资源方 AppID
resourceAppid: 'wxaabe7e5d652fc1d8',
// 资源方环境 ID
resourceEnv: 'cloud1-7gnlxfema33074ec',
})
await c.init()
console.log(c)
window.c = c

var buttonEl = document.getElementById('public-web-jump-button')
var buttonLoadingEl = document.getElementById('public-web-jump-button-loading')
try {
await openWeapp(() => {
console.log('dui')
buttonEl.classList.remove('weui-btn_loading')
buttonLoadingEl.classList.add('hidden')
})
} catch (e) {
console.log(e,'cuo')
buttonEl.classList.remove('weui-btn_loading')
buttonLoadingEl.classList.add('hidden')
throw e
}
}
})

async function openWeapp(onBeforeJump) {
var c = window.c
const res = await c.callFunction({
name: 'public',
data: {
action: 'getUrlScheme',

},
})

if (onBeforeJump) {
onBeforeJump()
}
location.href = res.result.openlink
}
</script>
<style>
.hidden {
display: none;
}

.full {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}

.public-web-container {
display: flex;
flex-direction: column;
align-items: center;
}

.public-web-container p {
position: absolute;
top: 40%;
}

.public-web-container a {
position: absolute;
bottom: 40%;
}

.wechat-web-container {
display: flex;
flex-direction: column;
align-items: center;
}

.wechat-web-container p {
position: absolute;
top: 40%;
}

.wechat-web-container wx-open-launch-weapp {
position: absolute;
bottom: 40%;
left: 0;
right: 0;
display: flex;
flex-direction: column;
align-items: center;
}

.desktop-web-container {
display: flex;
flex-direction: column;
align-items: center;
}

.desktop-web-container p {
position: absolute;
top: 40%;
}
</style>
</head>
<body>
<div class="page full">
<div id="public-web-container" class="hidden">
<p class="">正在打开 “ 小程序”...</p>
<a id="public-web-jump-button" href="javascript:" class="weui-btn weui-btn_primary weui-btn_loading" onclick="openWeapp()">
<span id="public-web-jump-button-loading" class="weui-primary-loading weui-primary-loading_transparent"><i class="weui-primary-loading__dot"></i></span>
小程序
</a>
</div>
<div id="wechat-web-container" class="hidden">
<p class="">点击以下按钮打开 “小程序”</p>
<!-- 跳转小程序的开放标签。文档 https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_Open_Tag.html -->
<wx-open-launch-weapp id="launch-btn" username="gh_36a8dca79910" path="/pages/biddingCenter/index" env-version="trial">
<template>
<button style="width: 200px; height: 45px; text-align: center; font-size: 17px; display: block; margin: 0 auto; padding: 8px 24px; border: none; border-radius: 4px; background-color: #07c160; color:#fff;">打开小程序</button>
</template>
</wx-open-launch-weapp>
</div>
<div id="desktop-web-container" class="hidden">
<p class="">请在手机打开网页链接</p>
</div>
</div>
</body>
</html>

以上第二点后面会讲到
以上功能满足基本可以在微信内部打开小程序了,但是实际上,我们url可能是其他app,短信,或者浏览器里面打开的


外部app或者短信跳转怎么实现

首先我这边是uni-app实现的



  • 第一步在项目根目录项目新建functions这个文件夹,然后到


image.png
点击下载放入刚刚的functions下面,然后再 manifest.json 下面


 "mp-weixin" : {
"appid" : "xxxxx",
"cloudfunctionRoot": "./functions/", // 这一行就是标记云函数目录的字段
"setting" : {
"urlCheck" : false
},
"usingComponents" : true,
"permission" : {}
}

这个时候编译项目在编译之后的小程序项目是没有functions 这个文件夹的,我们这个时候需要在vue.config.js


const path = require('path')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
configureWebpack: {
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: path.join(__dirname, 'functions'),
to: path.join(__dirname, 'unpackage/dist', process.env.NODE_ENV === 'production' ? 'build' : 'dev', process.env.UNI_PLATFORM, 'functions')
}
]
})
]
}
}

在终端上面npm install 才行,需要node环境
设置以上代码重新启动编译,在微信工具里面就会出现


image.png


image.png


右击public上传并部署(云端安装依赖),成功之后打开微信开发工具云开发


image.png


image.png


会出现刚刚的云函数文件
然后再次点击云函数权限


image.png
点击所有用户访问,保存


image.png


然后再点击右上角设置>权限设置> 未登录用户访问云资源权限设置打开


image.png


以上搞定也只是函数环境搞定,然后还要配置刚刚的函数方法api


image.png


只有配置这个才能在index方法里面调用


// 云函数入口文件
const cloud = require('wx-server-sdk')

cloud.init()

// 云函数入口函数
exports.main = async (event, context) => {
const wxContext = cloud.getWXContext()

switch (event.action) {
case 'getUrlScheme': {
return getUrlScheme(event)
}
}

return 'action not found'
}

async function getUrlScheme(event) {

return cloud.openapi.urlscheme.generate({
jumpWxa: {
path:'/pages/index/index', // <!-- replace -->
env_version:'trial',
},
// 如果想不过期则置为 false,并可以存到数据库
isExpire: false,
// 一分钟有效期
expireTime: parseInt(Date.now() / 1000 + 60),
})
}


代码讲到这边会发现之前h5里面调用的 c.callFunction 方法其实调用的就是exports.main 这个导出得方法
然后还需要把h5代码放入


image.png


image.png
以上就是整个h5跳转微信小程序的过程,需要注意的点事在h5里面 方法返回 res.result.openlink 这个参数需要跳转的url在生产上线过,要不然报错 openapi.urlscheme.generate pagepath 获取不到


作者:成书平
来源:juejin.cn/post/7264772939773526075
收起阅读 »

【小程序分包】小程序包大于2M,来这教你分包啊

web
前言🍊缘由该大的不大,小程序包超出2M,无法上传发布前段时间项目迭代时,因版本大升级,导致uniapp打包后小程序后,包体积大于2M。虽然将图片等静态资源压缩,体积大的资源放置cdn,在不懈的努力下,治标不治本,包体积还是不听话的长到2M以上。憋的实在没办法,...
继续阅读 »

前言

🍊缘由

该大的不大,小程序包超出2M,无法上传发布

前段时间项目迭代时,因版本大升级,导致uniapp打包后小程序后,包体积大于2M。虽然将图片等静态资源压缩,体积大的资源放置cdn,在不懈的努力下,治标不治本,包体积还是不听话的长到2M以上。憋的实在没办法,遂将小程序分包,彻底解除封印,特来跟大家分享下如何将小程序分包,减小主包大小


🎯主要目标

实现2大重点

  1. 如何进行小程序分包
  2. 如个根据分包调整配置文件

🍈猜你想问

如何与狗哥联系进行探讨

关注公众号【JavaDog程序狗】

公众号回复【入群】或者【加入】,便可成为【程序员学习交流摸鱼群】的一员,问题随便问,牛逼随便吹。

此群优势:

  1. 技术交流随时沟通
  2. 任何私活资源免费分享
  3. 实时科技动态抢先知晓
  4. CSDN资源免费下载
  5. 本人一切源码均群内开源,可免费使用

2.踩踩狗哥博客

javadog.net

大家可以在里面留言,随意发挥,有问必答


🍯猜你喜欢

文章推荐

正文

🍵三个问题

  1. 为什么小程序会有2M的限制?
  1. 用户体验:小程序要求在用户进入小程序前能够快速加载,以提供良好的用户体验。限制小程序的体积可以确保小程序能够在较短的时间内下载和启动,避免用户长时间的等待。
  2. 网络条件:考虑到不同地区和网络条件的差异,限制小程序的体积可以确保在低速网络环境下也能够较快地加载和打开小程序,提供更广泛的用户覆盖。
  3. 设备存储:一些用户使用的设备可能存储空间有限,限制小程序的体积可以确保小程序可以在这些设备上正常安装和运行。
  1. 如何解决包过大问题?
  1. 优化代码,删除掉不用的代码
  2. 图片压缩或者上传服务器
  3. 分包加载
  1. 什么是分包加载?

小程序一般都是由某几个功能组成,通常这几个功能之间是独立的,但会依赖一些公共的逻辑,且这些功能一般会对应某几个独立的页面。那么小程序代码的打包,可以按照功能的划分,拆分成几个分包,当需要用到某个功能时,才加载这个功能对应的分包。


🧀实操分包步骤

1.查看项目结构

通过上方三个问题,我们开始具体分包流程,首先看一下分包前项目结构pages.json配置文件

pages.json
{
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "uni-app"
}
},
{
"path": "pages/card/index",
"style": {
"navigationBarTitleText": "uni-app"
}
},
{
"path": "pages/device/index",
"style": {
"navigationBarTitleText": "uni-app"
}
},
{
"path": "pages/order/index",
"style": {
"navigationBarTitleText": "uni-app"
}
},
{
"path": "pages/product/index",
"style": {
"navigationBarTitleText": "uni-app"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {}
}

2.分析主包大小

微信开发者工具中,查看【详情】进行分析,此处本地代码只有一个主包大小399.8KB

3.参考文档

本文以uniapp为实操介绍案例

小程序官方文档:

developers.weixin.qq.com/miniprogram…

uniapp 分包文档:

uniapp.dcloud.net.cn/collocation…

4. 结构调整

将咱们项目结构按照如下图所示进行拆分

新建subPages_A 和 subPages_B,将pages下不同页面移入进新增的两个包,此处subPages_A的名字只做示例,实际要按照标准命名!

比较下之前项目结构,此处项目会报错,不用担心,稍后修改pages.json


5. 修改pages.json

根据上一步拆分的包路径,进行配置文件的调整,此处注意"subPackages" 要和 "pages" 同级

{
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "uni-app"
}
}
],
"subPackages": [{
"root": "pages/subPages_A",
"pages": [{
"path": "card/index",
"style": {
"navigationBarTitleText": "card"
}
},
{
"path": "device/index",
"style": {
"navigationBarTitleText": "device"
}
}
]
}, {
"root": "pages/subPages_B",
"pages": [{
"path": "order/index",
"style": {
"navigationBarTitleText": "order"
}
},
{
"path": "product/index",
"style": {
"navigationBarTitleText": "product"
}
}
]
}],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {}
}

这里的意思是将主包拆成subPages_A和subPages_B两个子包,对比下之前的配置


6. 启动测试

启动后查看微信开发者工具,查看【详情】可看到主包大小降为326.0kb,并且下方还有subPages_A和subPages_B两个子包

比较之前包大小,分包成功!


7. 特别注意

🎯 如果设计代码中路径问题,需要调成最新包结构路径。例如

拆包前跳转到对应设备页面

uni.navigateTo({
url:'/pages/device/index'
})

拆包后跳转到对应设备页面

uni.navigateTo({
url:'/pages/subPages_A/device/index'
})

切记如果拆包后所有路径问题需要统一修改,否则则会报错!!!


总结

本文通过实际demo进行uniapp小程序拆包,通过分析项目主包大小,查看官方文档,按照功能划分进行子包拆分,如果还有博友存在疑问或者不理解可以在上方与本狗联系,或者查看本狗发布在上方的代码,希望可以帮到大家。



作者:JavaDog程序狗
来源:juejin.cn/post/7270445774324367394

收起阅读 »

产品经理:优惠金额只入不舍,支付金额只舍不入...

web
前言 当前做的项目是一个售卖会员的平台。其中涉及到优惠券、支付金额等。 优惠券分为:折扣券(n折)、抵扣券(减x元) 需求 优惠金额、支付金额都需要保留两位小数。 优惠金额只入不舍,比如18.811元,显示为:18.82元。这样看起来,优惠的相对多一些 支付金...
继续阅读 »

前言


当前做的项目是一个售卖会员的平台。其中涉及到优惠券、支付金额等。

优惠券分为:折扣券(n折)、抵扣券(减x元)


需求


优惠金额、支付金额都需要保留两位小数。

优惠金额只入不舍,比如18.811元,显示为:18.82元。这样看起来,优惠的相对多一些

支付金额只舍不入,比如18.888元,显示为:18.88元。

从产品角度来讲,这个设计相当人性化。


实现


/**
* 金额计算
* @param {number} a sku原始价格
* @param {number} b 优惠券金额/折扣
* @param {string} mathFunc 四舍五入:round/ceil/floor
* @param {string} type 计算方式默认减法
* @param {digits} type 精度,默认两位小数
* */

export function numbleCalc(a, b,mathFunc='round', type = '-',digits=2) {

var getDecimalLen = num =>{
return num.toString().split('.')[1] ? num.toString().split('.')[1].length : 0;
}
//将小数按照一定的倍数转换成整数数
var floatToInt = (num,numlen,maxlen)=>{
var numInt = num.toString().replace('.', '');
if(numlen==maxlen)return +numInt;
return numInt * (10**(maxlen-numlen))
}


var c;
//获取2个数字中,最长的小数位数
var aLen = getDecimalLen(a);
var bLen = getDecimalLen(b);
var decimalLen = aLen>bLen?aLen:bLen;
var mul = decimalLen>0?(10 ** decimalLen):1;

//转换成整数
var aInteger = floatToInt(a,aLen,decimalLen)
var bInteger = floatToInt(b,bLen,decimalLen)


if(type=='-'){
c = (aInteger - bInteger)/mul;
}else if(type=='*'){
c = aInteger * bInteger/mul/mul;
}

c = digits==0?c : Math.round(c * (10 ** digits));

if(mathFunc=='floor'){
c= Math.floor(c);
}else if(mathFunc=='ceil'){
c= Math.ceil(c);
}else {
c= Math.round(c);
}
return digits==0?c : c/(10**digits);
}



整体思路:获取两个数字之间最大的小数位,先取整再计算。
不直接进行计算,是因为存在0.1+0.2!=0.3的情况,具体原因可以看下文章下方的参考链接,写的很详细。




  • Math.ceil()  总是向上舍入,并返回大于等于给定数字的最小整数。

  • Math.floor()  函数总是返回小于等于一个给定数字的最大整数。

  • Math.round() 四舍五入


【重点】小数位取整:我之前的写法原来是错误的


image.png
我一直以来也是这种形式,预想的是直接乘100变成整数,但是出现了以下情况


19.9 * 100 = 1989.9999999999998
5.02 * 100 = 501.99999999999994

可以看到,出现了意料之外的结果!!

最后采用的方案是:将小数转成字符串,再将小数点替换成空格


//将小数按照一定的倍数转换成整数数
var floatToInt = (num,numlen,maxlen)=>{
var numInt = num.toString().replace('.', '');
if(numlen==maxlen)return +numInt;
return numInt * (10**(maxlen-numlen))
}

总结


省流:将小数点替换成空格,变成整数,再进行相应计算。

封装的这个函数,只考虑了当前业务场景,未兼容一些边界值情况。



  • 大金额计算问题

  • 计算方式:加法、除法未做处理


参考


# 前端金额运算精度丢失问题及解决方案


作者:前端大明
来源:juejin.cn/post/7341210909069770792
收起阅读 »

如何使用Electron集成环信UIKIT

写在前面环信单群聊 UIKit 是基于环信即时通讯云 IM SDK 开发的一款即时通讯 React UI 组件库,本篇文章介绍如何在Electron中如何集成UIKit,采用框架Electron-vite-react--- 准备工作1.已经在环信即时...
继续阅读 »

写在前面
环信单群聊 UIKit 是基于环信即时通讯云 IM SDK 开发的一款即时通讯 React UI 组件库,本篇文章介绍如何在Electron中如何集成UIKit,采用框架Electron-vite-react


---
 准备工作
1.已经在环信即时通讯云控制台创建了有效的环信即时通讯 IM 开发者账号,并获取了App Key

2.了解并可以创建Electron-vite-react项目

3.了解UIkit各功能以及api调用


---

开始集成
第一步:创建一个Electron项目进度10%
Electron-vite官网有详细的教程,此处不做过多赘述,仅以当前示例项目为参考集成,更多详情指路官网

yarn create @quick-start/electron electronReact --template react

第二步:安装依赖进度15%

yarn install

第三步:启动项目进度20%

yarn run dev

到这一步我们可以得到下图


你的目录结构如下图


第四步:安装UIKit进度50%

下载easemob-chat-uikit
使用 npm 安装 easemob-chat-uikit 包

npm install easemob-chat-uikit --save

使用 yarn 安装 easemob-chat-uikit 包

yarn add easemob-chat-uikit

第五步:引入UIKit组件进度80%

1、删除App.tsx自带的内容,在App.tsx中引入UIKit组件

import {
Provider as UIKitProvider,
Chat,
ConversationList,
useClient
} from 'easemob-chat-uikit'
import 'easemob-chat-uikit/style.css'
import { useEffect } from 'react'
import './App.css'
const ChatApp = () => {
const client = useClient()
useEffect(() => {
client &&
client
.open({
user: 'userId',
pwd: 'pwd'
})
.then((res) => {
console.log('get token success', res)
})
}, [client])
return (
<div className="app_container">
<div className="conversation_container">
<ConversationList />
</div>
<div className="chat_container">
<Chat />
</div>
</div>
)
}
function App(): JSX.Element {
return (
<UIKitProvider
initConfig={{
appKey: 'your app key'
}}
>
<ChatApp />
</UIKitProvider>
)
}

export default App


2、将src/renderer/src/assets/main.css中的css样式全部替换如下
body {
display: flex;
flex-direction: column;
font-family:
Roboto,
-apple-system,
BlinkMacSystemFont,
'Helvetica Neue',
'Segoe UI',
'Oxygen',
'Ubuntu',
'Cantarell',
'Open Sans',
sans-serif;
color: #86a5b1;
background-color: #2f3241;
}

* {
padding: 0;
margin: 0;
}

ul {
list-style: none;
}

code {
font-weight: 600;
padding: 3px 5px;
border-radius: 2px;
background-color: #26282e;
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Menlo,
Consolas,
Liberation Mono,
monospace;
font-size: 85%;
}

a {
color: #9feaf9;
font-weight: 600;
cursor: pointer;
text-decoration: none;
outline: none;
}

a:hover {
border-bottom: 1px solid;
}

.container {
flex: 1;
display: flex;
flex-direction: column;
max-width: 840px;
margin: 0 auto;
padding: 15px 30px 0 30px;
}

.versions {
margin: 0 auto;
float: none;
clear: both;
overflow: hidden;
font-family: 'Menlo', 'Lucida Console', monospace;
color: #c2f5ff;
line-height: 1;
transition: all 0.3s;
}

.versions li {
display: block;
float: left;
border-right: 1px solid rgba(194, 245, 255, 0.4);
padding: 0 20px;
font-size: 13px;
opacity: 0.8;
}

.versions li:last-child {
border: none;
}

.hero-logo {
margin-top: -0.4rem;
transition: all 0.3s;
}

@media (max-width: 840px) {
.versions {
display: none;
}

.hero-logo {
margin-top: -1.5rem;
}
}

.hero-text {
font-weight: 400;
color: #c2f5ff;
text-align: center;
margin-top: -0.5rem;
margin-bottom: 10px;
}

@media (max-width: 660px) {
.hero-logo {
display: none;
}

.hero-text {
margin-top: 20px;
}
}

.hero-tagline {
text-align: center;
margin-bottom: 14px;
}

.links {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24px;
font-size: 18px;
font-weight: 500;
}

.links a {
font-weight: 500;
}

.links .link-item {
padding: 0 4px;
}

.features {
display: flex;
flex-wrap: wrap;
margin: -6px;
}

.features .feature-item {
width: 33.33%;
box-sizing: border-box;
padding: 6px;
}

.features article {
background-color: rgba(194, 245, 255, 0.1);
border-radius: 8px;
box-sizing: border-box;
padding: 12px;
height: 100%;
}

.features span {
color: #d4e8ef;
word-break: break-all;
}

.features .title {
font-size: 17px;
font-weight: 500;
color: #c2f5ff;
line-height: 22px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.features .detail {
font-size: 14px;
font-weight: 500;
line-height: 22px;
margin-top: 6px;
}

@media (max-width: 660px) {
.features .feature-item {
width: 50%;
}
}

@media (max-width: 480px) {
.links {
flex-direction: column;
line-height: 32px;
}

.links .link-dot {
display: none;
}

.features .feature-item {
width: 100%;
}
}


3、在src/renderer/src目录下添加App.css
.app_container {
width: calc(100%);
height: 100vh;
display: flex;
}
.conversation_container {
width: 30%;
}
.chat_container {
width: 70%;
}


到这一步你可以得到如下图



第六步:解决问题`进度99%`

在第五步执行完毕之后发现调试器有如下图报错


经查阅资料,发现是Electron内容安全策略在搞鬼,并提供了解决方案


接下来我们就需要在src/renderer/index.html中更改meta标签
同样out/renderer/index.html也需要更改meta标签

<meta
http-equiv="Content-Security-Policy"
content="font-src 'self' data:; img-src 'self' data:; default-src 'self'; connect-src * ws://* wss://*; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'"
/>


接下来保存代码并运行你将得到下图


第七步:发送消息进度100%
点击好友并发送一条消息,如下图

恭喜你集成完毕~
总结:
通过以上步骤,你已经成功在Electron中集成了环信单聊 UIKit 并实现了基本的即时通讯功能,接下来继续根据 UIKit 提供的组件和 API 文档进行进一步开发吧

收起阅读 »

写了一个字典hook,瞬间让组员开发效率提高20%!!!

web
1、引言 在项目的开发中,发现有很多地方(如:选择器、数据回显等)都需要用到字典数据,而且一个页面通常需要请求多个字典接口,如果每次打开同一个页面都需要重新去请求相同的数据,不但浪费网络资源、也给开发人员造成一定的工作负担。最近在用 taro + react ...
继续阅读 »

1、引言


在项目的开发中,发现有很多地方(如:选择器、数据回显等)都需要用到字典数据,而且一个页面通常需要请求多个字典接口,如果每次打开同一个页面都需要重新去请求相同的数据,不但浪费网络资源、也给开发人员造成一定的工作负担。最近在用 taro + react 开发一个小程序,所以就写一个字典 hook 方便大家开发。


2、实现过程


首先,字典接口返回的数据类型如下图所示:
image.png


其次,在没有实现字典 hook 之前,是这样使用 选择器 组件的:


  const [unitOptions, setUnitOptions] = useState([])

useEffect(() => {
dictAppGetOptionsList(['DEV_TYPE']).then((res: any) => {
let _data = res.rows.map(item => {
return {
label: item.fullName,
value: item.id
}
})
setUnitOptions(_data)
})
}, [])

const popup = (
<PickSelect
defaultValue=""
open={unitOpen}
options={unitOptions}
onCancel={() =>
setUnitOpen(false)}
onClose={() => setUnitOpen(false)}
/>

)

每次都需要在页面组件中请求到字典数据提供给 PickSelect 组件的 options 属性,如果有多个 PickSelect 组件,那就需要请求多次接口,非常麻烦!!!!!


既然字典接口返回的数据格式是一样的,那能不能写一个 hook 接收不同属性,返回不同字典数据呢,而且还能 缓存 请求过的字典数据?


当然是可以的!!!


预想一下如何使用这个字典 hook?


const { list } = useDictionary('DEV_TYPE')

const { label } = useDictionary('DEV_TYPE', 1)

const { label } = useDictionary('DEV_TYPE', 1, '、')

从上面代码中可以看到,第一个参数接收字典名称,第二个参数接收字典对应的值,第三个参数接收分隔符,而且后两个参数是可选的,因此根据上面的用法来写我们的字典 hook 代码。


interface dictOptionsProps {
label: string | number;
value: string | number | boolean | object;
disabled?: boolean;
}

interface DictResponse {
value: string;
list: dictOptionsProps[];
getDictValue: (value: string) => string
}

let timer = null;
const types: string[] = [];
const dict: Record<string, dictOptionsProps[]> = {}; // 字典缓存

// 因为接收不同参数,很适合用函数重载
function useDictionary(type: string): DictResponse;
function useDictionary(
type: string | dictOptionsProps[],
value: number | string | Array<number | string>,
separator?: string
): DictResponse;
function useDictionary(
type: string | dictOptionsProps[],
value?: number | string | Array<string | number>,
separator = ","
): DictResponse {
const [options, setOptions] = useState<dictOptionsProps[]>([]); // 字典数组
const [dictValue, setDictValue] = useState(""); // 字典对应值

const init = () => {
if (!dict[type] || !dict[type].length) {
dict[type] = [];

types.push(type);

// 当多次调用hook时,获取所有参数,合成数组,再去请求,这样可以避免多次调用接口。
timer && clearTimeout(timer);
timer = setTimeout(() => {
dictAppGetOptionsList(types.slice()).then((res) => {
for (const key in dictRes.data) {
const dictList = dictRes.data[key].map((v) => ({
label: v.description,
value: v.subtype,
}));
dict[type] = dictList
setOptions(dictList) // 注意这里会有bug,后面有说明的
}
});
}, 10);
} else {
typeof type === "string" ? setOptions(dict[type]) : setOptions(type);
}
};

// 获取字典对应值的中文名称
const getLabel = useCallback(
(value) => {
if (value === undefined || value === null || !options.length) return "";
const values = Array.isArray(value) ? value : [value];

const items = values.map((v) => {
if (typeof v === "number") v = v.toString();
return options.find((item) => item.value === v) || { label: value };
});
return items.map((v) => v.label).join(separator);
},
[options]
)

useEffect(() => init(), [])
useEffect(() => setDictValue(getLabel(value)), [options, value])

return { value: dictValue, list: options, getDictValue: getLabel };
}

初步的字典hook已经开发完成,在 Input 组件中添加 dict 属性,去试试看效果如何。


export interface IProps extends taroInputProps {
value?: any;
dict?: string; // 字典名称
}

const CnInput = ({
dict,
value,
...props
}: IProps
) => {
const { value: _value } = dict ? useDictionary(dict, value) : { value };

return <Input value={_value} {...props} />
}

添加完成,然后去调用 Input 组件


<CnInput
readonly
dict="DEV_ACCES_TYPE"
value={formData?.accesType}
/>

<CnInput
readonly
dict="DEV_SOURCE"
value={formData?.devSource}
/>


没想到,翻车了


会发现,在一个页面组件中,多次调用 Input 组件,只有最后一个 Input 组件才会回显数据


image.png


这个bug是怎么出现的呢?原来是 setTimeout 搞的鬼,在 useDictionary hook 中,当多次调用 useDictionary hook 的时候,为了能拿到全部的 type 值,请求一次接口拿到所有字典的数据,就把字典接口放在 setTimeout 里,弄成异步的逻辑。但是每次调用都会清除上一次的 setTimeout,只保存了最后一次调用 useDictionary 的 setTimeout ,所以就会出现上面的bug了。


既然知道问题所在,那就知道怎么去解决了。


解决方案: 因为只有调用 setOptions 才会引起页面刷新,为了不让 setTimeout 清除掉 setOptions,就把 setOptions 添加到一个更新队列中,等字典接口数据回来再去执行更新队列就可以了。



let timer = null;
const queue = []; // 更新队列
const types: string[] = [];
const dict: Record<string, dictOptionsProps[]> = {};

function useDictionary2(type: string): DictResponse;
function useDictionary2(
type: string | dictOptionsProps[],
value: number | string | Array<number | string>,
separator?: string
): DictResponse;
function useDictionary2(
type: string | dictOptionsProps[],
value?: number | string | Array<string | number>,
separator = ","
): DictResponse {
const [options, setOptions] = useState<dictOptionsProps[]>([]);
const [dictValue, setDictValue] = useState("");

const getLabel = useCallback(
(value) => {
if (value === undefined || value === null || !options.length) return "";
const values = Array.isArray(value) ? value : [value];

const items = values.map((v) => {
if (typeof v === "number") v = v.toString();
return options.find((item) => item.value === v) || { label: value };
});
return items.map((v) => v.label).join(separator);
},
[options]
);

const init = () => {
if (typeof type === "string") {
if (!dict[type] || !dict[type].length) {
dict[type] = [];

const item = {
key: type,
exeFunc: () => {
if (typeof type === "string") {
setOptions(dict[type]);
} else {
setOptions(type);
}
},
};
queue.push(item); // 把 setOptions 推到 更新队列(queue)中

types.push(type);

timer && clearTimeout(timer);
timer = setTimeout(async () => {
const params = types.slice();

types.length = 0;

try {
let dictRes = await dictAppGetOptionsList(params);
for (const key in dictRes.data) {
dict[key] = dictRes.data[key].map((v) => ({
label: v.description,
value: v.subtype,
}));
}

queue.forEach((item) => item.exeFunc()); // 接口回来了再执行更新队列
queue.length = 0; // 清空更新队列
} catch (error) {
queue.length = 0;
}
}, 10);
} else {
typeof type === "string" ? setOptions(dict[type]) : setOptions(type);
}
}
};

useEffect(() => init(), []);

useEffect(() => setDictValue(getLabel(value)), [options, value]);

return { value: dictValue, list: options, getDictValue: getLabel };
}

export default useDictionary;

修复完成,再去试试看~


image.png


不错不错,已经修复,嘿嘿~


这样就可以愉快的使用 字典 hook 啦,去改造一下 PickSelect 组件


export interface IProps extends PickerProps {
open: boolean;
dict?: string;
options?: dictOptionsProps[];
onClose: () => void;
}
const Base = ({
dict,
open = false,
options = [],
onClose = () => { },
...props
}: Partial<IProps>
) => {
// 如果不传 dict ,就拿 options
const { list: _options } = dict ? useDictionary(dict) : { list: options };

return <Picker.Column>
{_options.map((item) => {
return (
<Picker.Option
value={item.value}
key={item.value as string | number}
>

{item.label}
</Picker.Option>
);
})}
</Picker.Column>


在页面组件调用 PickSelect 组件


image.png


效果:


image.png


这样就只需要传入 dict 值,就可以轻轻松松获取到字典数据啦。不用再手动去调用字典接口啦,省下来的时间又可以愉快的摸鱼咯,哈哈哈


最近也在写 vue3 的项目,用 vue3 也实现一个吧。


// 定时器
let timer = 0
const timeout = 10
// 字典类型缓存
const types: string[] = []
// 响应式的字典对象
const dict: Record<string, Ref<CnPage.OptionProps[]>> = {}

// 请求字典选项
function useDictionary(type: string): Ref<CnPage.OptionProps[]>
// 解析字典选项,可以传入已有字典解析
function useDictionary(
type: string | CnPage.OptionProps[],
value: number | string | Array<number | string>,
separator?: string
): ComputedRef<string>
function useDictionary(
type: string | CnPage.OptionProps[],
value?: number | string | Array<number | string>,
separator = ','
): Ref<CnPage.OptionProps[]> | ComputedRef<string> {
// 请求接口,获取字典
if (typeof type === 'string') {
if (!dict[type]) {
dict[type] = ref<CnPage.OptionProps[]>([])

if (type === 'UNIT_LIST') {
// 单位列表调单独接口获取
getUnitListDict()
} else if (type === 'UNIT_TYPE') {
// 单位类型调单独接口获取
getUnitTypeDict()
} else {
types.push(type)
}
}

// 等一下,人齐了才发车
timer && clearTimeout(timer)
timer = setTimeout(() => {
if (types.length === 0) return
const newTypes = types.slice()
types.length = 0
getDictionary(newTypes).then((res) => {
for (const key in res.data) {
dict[key].value = res.data[key].map((v) => ({
label: v.description,
value: v.subtype
}))
}
})
}, timeout)
}

const options = typeof type === 'string' ? dict[type] : ref(type)
const label = computed(() => {
if (value === undefined || value === null) return ''
const values = Array.isArray(value) ? value : [value]
const items = values.map(
(value) => {
if (typeof value === 'number') value = value.toString()
return options.value.find((v) => v.value === value) || { label: value }
}
)
return items.map((v) => v.label).join(separator)
})

return value === undefined ? options : label
}

export default useDictionary

感觉 vue3 更简单啊!


到此结束!如果有错误,欢迎大佬指正~


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

为了解决小程序tabbar闪烁的问题,我将小程序重构成了 SPA

web
(日落西山,每次看到此景,我总是会想到明朝(明朝那些事儿第六部的标题,日落西山))  前言 几个月前,因工作需求,我开发了一个小程序,当时遇到了一个需求,是关于tabbar权限的问题。小程序的用户分两种,普通用户和vip用户,普通用户tabbar有两个,vip...
继续阅读 »

1.jpg


(日落西山,每次看到此景,我总是会想到明朝(明朝那些事儿第六部的标题,日落西山))


 前言


几个月前,因工作需求,我开发了一个小程序,当时遇到了一个需求,是关于tabbar权限的问题。小程序的用户分两种,普通用户和vip用户,普通用户tabbar有两个,vip用户小程序下面的tabbar有五个。  

因为涉及自定义tabbar的问题,所以官方自带的tabbar肯定就不能用了,我们需要自定义tabbar。官方也提供了自定义tabbar的功能。


官网自定义tabbar


官网地址:基础能力 / 自定义 tabBar (qq.com)


{
"tabBar": {
"custom": true,
"list": []
}
}

就是需要在 app.json 中的 tabBar 项指定 custom 字段,需要注意的是 list 字段也需要存在。


然后,在代码根目录下添加入口文件:


custom-tab-bar/index.js
custom-tab-bar/index.json
custom-tab-bar/index.wxml
custom-tab-bar/index.wxss

具体代码,大家可以参考官网案例。


需要注意的是每个tabbar页面 / 组件都需要在onshow / show 函数中执行以下函数,否则就会出现tabbar按钮切换两次,才会变成选中色的问题。


      if (typeof this.getTabBar === 'function' &&
this.getTabBar()) {
this.getTabBar().setData({
selected: 0 // 第n个tabbar页面就填 n-1
})
}

接下来就是我的思路


2.png


我在 custom-tab-bar/index.js 中定义了一个函数,这个函数去判断当前登录人是否为vip,如果是就替换掉tabbar 的数据。


那么之前每个页面的代码就要写成这样


      if (typeof this.getTabBar === 'function' &&
this.getTabBar()) {
this.getTabBar().change_tabbar_list()
this.getTabBar().setData({
selected: 0 // 第n个tabbar页面就填 n-1
})
}

ok,我们来看一下效果。注意看视频下方的tabbar,每个页面,第一次点击的时候,有明显的闪烁bug。(大家也可以参考一下市面上的小程序,小部分的小程序有这个闪烁问题,大部分的小程序没有这个闪烁的问题(如:携程小程序))



bug产生原因


那么我们就要去思考了,为什么人家的小程序没有这个bug呢?


想这个问题前,要先去想这个bug是怎么产生的,我猜测是每个tabbar页面都有个初始化的过程,第一次渲染页面的时候要去重新渲染tabbar,每个页面的tabbar都是从0开始渲染,然后会缓存到每个页面上,所以第二次点击就没有这个bug了。


解决tabbar闪烁问题


为了解决这个问题,我想到了SPA ,也就是只留一个页面,其他的tabbar页面都弄成组件。


效果展示



已经解决,tabbar闪烁的问题。


代码思路,通过wx:if 控制组件的显示隐藏。


3.png


4.png


源码地址:gitlab.com/wechat-mini…

https克隆地址:gitlab.com/wechat-mini…


写在最后


1、我也是在网上见过别人的一些评论,说如果将小程序重构成这种单页面,会有卡顿问题,我目前没有发现这个问题,可能是我做的小程序功能比较少。


2、至于生命周期,将页面切换成组件后,页面的那些生命周期也肯定都不能使用了,只能用组件的生命周期,我之前开发使用组件的生命周期实现业务逻辑也没什么问题。 触底加载这些也只能换成组件去实现了。


3、小程序最上面的标题,也可以使用以下代码来实现。就是在每个组件初始化的时候要去执行下列代码。


            wx.setNavigationBarTitle({
title: '',
});

作者:楚留香Ex
来源:juejin.cn/post/7317281367111827475
收起阅读 »

如何将微信小程序从WebView迁移到Skyline

web
什么是 Skyline 微信小程序新的渲染引擎,使用更加高效的渲染管线,提供更好的性能和全新的交互动画体验。 具体可以查阅官网介绍 将开发者工具切换成 Sykline 模式 调试基础库切到 2.30.4 或以上版本 确保右上角 > 详情 > 本地...
继续阅读 »

什么是 Skyline


微信小程序新的渲染引擎,使用更加高效的渲染管线,提供更好的性能和全新的交互动画体验。


具体可以查阅官网介绍


将开发者工具切换成 Sykline 模式



  1. 调试基础库切到 2.30.4 或以上版本

  2. 确保右上角 > 详情 > 本地设置里的 开启 Skyline 渲染调试、启用独立域进行调试 选项被勾选上

  3. 确保右上角 > 详情 > 本地设置里的 将 JS 编译成 ES5 选项被勾选上


使用 skylint 工具迁移



npx skylint


image.png


image.png


使用过程中可能会出现文件未找到错误,例如


image.png


原因就是使用绝对路径 <import src="/components/chooserList/index.wxml" />导入模块,而 skylint 无法找到该文件,需要修改为相对路径 <import src="../../components/chooserList/index.wxml" />导入模块


有几种提示不是很准确,可以评估下:



  1. @position-fixed 不支持 position: fixed:如果你根据不同 renderer 兼容,则会导致该提示一直存在

  2. @no-pseudo-element 不支持伪元素: 目前对已经支持的 ::before 和 ::after 也会进行提示


手动迁移


在 app.json 配置



{
"lazyCodeLoading": "requiredComponents", // 开启按需注入
"rendererOptions": {
"skyline": {
"defaultDisplayBlock": true // skyline 下节点默认为 flex 布局,可以在此切换为默认 block 布局
}
}
}


在 page.json 配置



{
"renderer": "skyline", // 声明为 skyline 渲染,对于已有的项目,建议渐进式迁移,对于新项目,直接全局打开,在 app.json 里进行配置
"componentFramework": "glass-easel", // 声明使用新版 glass-easel 组件框架
"disableScroll": true, // skyline 不支持页面全局滚动,为了使之与WebView保持兼容,在此禁止滚动
"navigationStyle": "custom" // skyline 不支持原生导航栏,为了使之与WebView保持兼容,并且自行实现自定义导航栏
}


skyline 不支持页面全局滚动,如果需要页面滚动,在需要滚动的区域使用 scroll-view 实现



<scroll-view type="list" scroll-y style="flex: 1; width: 100%; overflow: hidden;"></scroll-view>



page {
display: flex;
flex-direction: column;
height: 100vh;
}


skyline 渲染模式下 flex-direction 默认值是 column,为了使之与WebView保持兼容,需要在 flex 布局里将 flex-direction 默认值改为 row


在真机上调试 skyline 渲染模式


小程序菜单 > 开发调试 > Switch Render,会出现三个选项,说明如下:


Auto :跟随 AB 实验,即对齐小程序正式用户的表现


WebView :强制切为 WebView 渲染


Skyline :若当前页面已迁移到 Skyline,则强制切为 Skyline 渲染


image.png


常见问题


position: fixed 不支持


需要将



<view class="background"></view>



.background {

position: fixed;

}


修改为



<root-portal>

<view class="background"></view>

</root-portal>



.background {

position: absolute;

}


如果无法做到适配,则可以根据不同 renderer 兼容



<view class="position {{renderer}}"></view>



.position {
position: fixed;
}

.position.skyline {
position: absolute;
}



Page({
data: {
renderer: 'webview'
},

onLoad() {
this.setData({
renderer: this.renderer,
})
},
})


不支持 Canvas 旧版接口


Skyline 渲染模式下,旧版 Canvas 绘制图案无效(使用 wx.createCanvasContext 创建的上下文)


在真机中图片的 referer 丢失


测试结果如下:


使用 WebView 渲染 Image,请求的 header 是:



{
host: 'xxx',
connection: 'keep-alive',
accept: 'image/webp,image/avif,video/*;q=0.8,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5',
'user-agent': 'xxx',
'accept-language': 'zh-CN,zh-Hans;q=0.9',
referer: 'https://servicewechat.com/',
'accept-encoding': 'gzip, deflate'
}


使用 Skyline 渲染 Image,请求的 header 是:



{

'user-agent': 'Dart/2.16 (dart:io)',
'accept-encoding': 'gzip',
host: 'xxx'
}


官方 Demo


可以参考官方Demo学习使用 Skyline 的增强特性,比如 Worklet 动画、手势系统等,但在首次下载编译时,会遇到【交互动画】页面为空的问题,主要原因是该页面是由 TypeScript 写的,编译成 JavaScript 需要开启工具内置的 TypeScript编译插件,需要在project.config.json project.config.json 配置:



setting.useCompilerPlugins: ["typescript"]


参考



作者:不二先生不二
来源:juejin.cn/post/7262656196854644792
收起阅读 »

小程序实现无感登录+权限分配

web
djngo开发:小程序实现无感登录+权限分配 由来 最近开发一个预约系统,需要区分普通用户和工作人员。由于账号密码/短信验证过于繁琐,因而选择记录openid实现无感登录。(基本小程序都这样操作) 同时事先在数据库中录入客户手机号,即可在用户登录时根据有无手机...
继续阅读 »

djngo开发:小程序实现无感登录+权限分配


由来


最近开发一个预约系统,需要区分普通用户和工作人员。由于账号密码/短信验证过于繁琐,因而选择记录openid实现无感登录。(基本小程序都这样操作)


同时事先在数据库中录入客户手机号,即可在用户登录时根据有无手机号来区分普通用户和工作人员。这样在项目交付时,工作人员和普通用户一样可以直接登录无感登录小程序


1. 开发思路


微信的openid是一种唯一标识用户身份的字符串

用户登录小程序,通过手机号快速验证组件获取动态令牌code,后端向微信服务器发送get请求并带上code获取每个用户唯一的openid,然后记录到mysql中,并签发token。该openid就是登录小程序的唯一凭证。


2.简单实现



  • 获取openid,如果通过openid查不用户,就自动新建用户,并返回token。


#####LoginView###########

code = request.data.get("code")
appid = appid # 微信小程序的appid
appsecret = "xxxxxxxx" # 微信小程序的密钥,登录微信公众平台即可获取
# 获取openid和session_token
querystring = {"appid":appid,"js_code":code,"secret":appsecret,"grant_type":"authorization_code"}
jscode2session = requests.get('https://api.weixin.qq.com/sns/jscode2session',params=querystring)
if not jscode2session.json().get("errcode"):
data = jscode2session.json()
########拿到openid#########
openid=data.get("openid")
#######去数据库比对,如果通过openid查到用户并且未被禁用,就新建##########
try:
user = models.UserInfo.objects.get(openid)
if user.is_deleted: # 检查用户是否被禁用
return ErrorResponse(msg='用户已被禁用,无法登录',data=data,code=302)
except models.UserInfo.DoesNotExist:
models.UserInfo.objects.create()

3.更进一步:通过手机号来区别普通用户和工作人员


openid虽然做到的唯一性验证,但是当用户数量庞大时,该如何区分用户角色:



  • 一:手动在后台根据已有用户分配权限

  • 二:登录时根据某一标识区分角色


    方法一显然不靠谱,因为用户至少会超过1000人,方法二需要额外标识,显然手机号最合适。



3.1 前端获取手机号的动态令牌


小程序提供了手机号快速验证组件,方便我们获取手机号


bindgetphonenumber 事件回调中的动态令牌code传到开发者后台


  <view class="title">欢迎来到广盈预约</view>
<view class="card">
<view class="button">快捷登录</view>
<button
style="opacity: 0"
class="bottom-button"
open-type="getPhoneNumber|agreePrivacyAuthorization"
bindgetphonenumber="getrealtimephonenumber"
bindagreeprivacyauthorization="handleAgreePrivacyAuthorization"
>

同意隐私协议并授权手机号注册
</button>
</view>
</view>

Page({
getPhoneNumber (e) { console.log(e.detail.code) // 动态令牌 }
})

image.png



注意:如果你想获取用户手机号就必须添加用户授权《隐私保护协议》bindagreeprivacyauthorization,否则小程序无法上线



3.2 后端带着动态令牌去微信服务器获取手机号


简单来说就是用户登录时数据库中没有手机号对应的用户,后端就会自动建立一个账号,并分配权限为普通用户,然后直接登录。


这样的话,只需要第一次登录时获取手机号,以后登录就可以直接进入系统。


class RegisterView(APIView):  
authentication_classes = []
permission_classes = []
def getmobile(self,appid,code):
"""获取用户的手机号"""
try:
appsecret = "cxxxxxxxxx"
querystring = {"appid":appid,"secret":appsecret,"grant_type":"client_credential"}
response = requests.get('https://api.weixin.qq.com/cgi-bin/token',params=querystring)
access_token = response.json().get("access_token")
querystring = {"access_token":access_token}
headers = {"content-type": "application/json"}
payload = {"code":code}
mobile =requests.post(f"https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={access_token}",json=payload,headers=headers)
return mobile.json().get('phone_info').get('phoneNumber')
except Exception as e:
return


def post(self, request):
unionid =request.data.get("unionid")
nickname =request.data.get("nickname")
openid = request.data.get("openid")
code = request.data.get("code")
appid = request.data.get("appid")

mobile = self.getmobile(appid=appid,code=code)
if not mobile:
return ErrorResponse(msg="手机号获取失败")

defaults = {
"openid":openid,
"unionid":unionid,
"mobile":mobile,
"nickname":nickname,
}

"""这条语句将查找一个符合mobile=mobile条件的记录,如果找到就更新 defaults中的字段 ,否则就创建
注意: 查询的条件必须是唯一的,否则会造成多条数据返回而报错,这个逻辑同 get() 函数。
注意: 使用的字段,没有唯一的约束,并发的调用这个方法可能会导致多条相同的值插入。
"""

models.UserInfo.objects.update_or_create(mobile=mobile,defaults=defaults)
models.User_GZH.objects.get_or_create(unionid=unionid, defaults={'unionid':unionid})
return DetailResponse()

作者:大海前端
来源:juejin.cn/post/7293177336488804391
收起阅读 »

【已解决】uniapp小程序体积过大、隐私协议的问题

web
概述 在前几天的工作中又遇到了一个微信小程序上传代码过大的情况,在这里总结一下具体的解决步骤,说明我们需要进一步的优化它,技术栈是使用uniapp框架+HBuilderX的开发环境,微信小程序更新了隐私协议,Http返回信息{errMsg: "getUserP...
继续阅读 »

概述


在前几天的工作中又遇到了一个微信小程序上传代码过大的情况,在这里总结一下具体的解决步骤,说明我们需要进一步的优化它,技术栈是使用uniapp框架+HBuilderX的开发环境,微信小程序更新了隐私协议,Http返回信息{errMsg: "getUserProfile:fail can only be invoked by user TAP gesture."}


定位原因


程序出现问题,首先需要把原因定位归结在第一点,这是解决问题的关键,检查了一下Git仓库的修改情况,发现引入了一个7kb大小的防抖插件,其实7kb的插件不是根本问题,问题是之前的代码写的太不规范了。


压缩资源


尽量把static下面的图片都压缩一下,这里推荐一个好用的压缩网站,图片进行压缩:tinypng.com/


我没有压缩过Js文件,但会有一种方法压缩js文件,使js文件尽量的缩小来减少js文件建立的文件体积。


uniapp官方压缩建议:


小程序工具提示vendor.js过大,已经跳过es6向es5转换。这个转换问题本身不用理会,因为vendor.js已经是es5的了。


关于体积控制,参考如下:



  • 使用运行时代码压缩
    HBuilderX创建的项目勾选运行-->运行到小程序模拟器-->运行时是否压缩代码

  • cli创建的项目可以在package.json中添加参数--minimize,示例:"dev:mp-weixin": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch --minimize"


小程序分包处理



  • 在对应平台的配置下添加 "optimization":{"subPackages":true}开启分包优化

  • 目前只支持 mp-weixin、mp-qq、mp-baidu、mp-toutiao、mp-kuaishou的分包优化


分包优化具体逻辑:



  • 静态文件:分包下支持 static 等静态资源拷贝,即分包目录内放置的静态资源不会被打包到主包中,也不可在主包中使用

  • js文件:当某个 js 仅被一个分包引用时,该 js 会被打包到该分包内,否则仍打到主包(即被主包引用,或被超过 1 个分包引用)

  • 自定义组件:若某个自定义组件仅被一个分包引用时,且未放入到分包内,编译时会输出提示信息


分包内静态文件示例


"subPackages": [{
"root": "pages/sub",
"pages": [{
"path": "index/index"
}]
}]

网络请求


还有一个解决小程序体积过大的问题,把非必要的组件都使用Http Api接口的形式去进行交互,尽量去减少本地包中的体积,再根目录下/utils里有一个232kb的获取地址交互,可以替换成Http Api的形式来解决。


隐私协议


在开发微信小程序过程中遇到了{errMsg: "getUserProfile:fail can only be invoked by user TAP gesture."},出现这个信息的原因是微信平台更新了隐私协议,需要再后台备案更新一下,搜索了很多,都不准确,这个隐私协议没有什么特殊情况,2个小时就可以通过了。


设置路径1: 公众号平台->设置->服务内容声明,设置通过后显示的状态是已更新,状态之前的是审核中


111.png


设置路径2: 首页->管理->版本管理->提交审核 ,再这里面提审,隐私协议审核过了,就可以继续开发了。


作者:stark张宇
来源:juejin.cn/post/7296025902911897627
收起阅读 »

uniapp微信小程序授权后得到“微信用户”

web
背景 近日在开发微信小程序的时候,发现数据库多了很多用户名称是"微信用户"的账号信息。接口的响应信息如下。 (nickName=微信用户, avatarUrl=https://thirdwx.qlogo.cn/mmopen/vi_32/POgEwh4mIHO4...
继续阅读 »

背景


近日在开发微信小程序的时候,发现数据库多了很多用户名称是"微信用户"的账号信息。接口的响应信息如下。


(nickName=微信用户, avatarUrl=https://thirdwx.qlogo.cn/mmopen/vi_32/POgEwh4mIHO4nibH0KlMECNjjGxQUq24ZEaGT4poC6icRiccVGKSyXwibcPq4BWmiaIGuG1icwxaQX6grC9VemZoJ8rg/132, gender=0, country=, province=, city=, language=), code=0e1abNFa1dBwRG0lnoJa18qT0i2abNFk)

经过排查,发现官方是对微信授权的接口做出了调整。小程序用户头像昵称获取规则调整公告


WX20240206-112518@2x.png

根据上面标红的字体说明,官方的意图就是只提供openid和unionid, 不暴露用户头像昵称数据。

基于此才会在新版的接口中返回"微信用户"的信息。



  • 针对这个问题,官方提供的解决方案如下。


WX20240206-112912@2x.png
以上解决方案,表达的意思是新版用户授权的接口中, 官方只会给你提供unionid和openid.

至于用户的昵称和头像,开发者可以提供功能,以用户的意志去完成修改和更新。

tips: 建议授权接口生成用户名和昵称,采用系统默认的方式。


微信授权流程


152f3cb28a734e768381f986cec1dd26.png


uniapp代码实现


uni.login接口文档


WX20240207-221556@2x.png


后端代码


WX20240207-221641@2x.png


以上是uniapp和springboot部分代码截图展示,关注微信公众号:JeecgFlow,或微信扫描下面二维码.

回复"微信用户"可以获取完整代码。


异常分析


//如果你的接口出现如下信信息,该如何处理呢?
# {errMsg: “getUserProfile:fail api scope is not
declared in the privacy agreement“, errno: 112}

出现问题的原因: api 范围未在隐私协议中声明,建议大家更具公告,更新对应的隐私协议。

【设置-服务内容声明-用户隐私保护指引】,更新隐私协议,在第一条:开发者处理的信息中,点击【增加信息类型】,选择需要授权的信息,头像昵称我已经勾选了,所以列表中不显示了,根据需求选择和填写其他内容,最后确定并生成协议。等待隐私协议审核通过。


68b3f3f0c4ee419d9ca5dec8aa5b0a4c.png
建议按需添加,以防审核不通过。


为了分辨用户,开发者将在获取你的明示同意后,收集你的微信昵称、头像。
为了显示距离,开发者将在获取你的明示同意后,收集你的位置信息。
开发者收集你的地址,用于获取位置信息。
开发者收集你的发票信息,用于维护消费功能。
为了用户互动,开发者将在获取你的明示同意后,收集你的微信运动步数。
为了通过语音与其他用户交流互动,开发者将在获取你的明示同意后,访问你的麦克风。
开发者收集你选中的照片或视频信息,用于提前上传减少上传时间。
为了上传图片或者视频,开发者将在获取你的明示同意后,访问你的摄像头。
为了登录或者注册,开发者将在获取你的明示同意后,收集你的手机号。
开发者使用你的通讯录(仅写入)权限,用于方便用户联系信息。
开发者收集你的设备信息,用于保障你正常使用网络服务。
开发者收集你的身-份-证号码,用于实名认证后才能继续使用的相关网络服务。
开发者收集你的订单信息,用于方便获取订单信息。
开发者收集你的发布内容,用于用户互动。
开发者收集你的所关注账号,用于用户互动。
开发者收集你的操作日志,用于运营维护。
为了保存图片或者上传图片,开发者将在获取你的明示同意后,使用你的相册(仅写入)权限。
为了用户互动,开发者将在获取你的明示同意后,收集你的车牌号。
开发者访问你的蓝牙,用于设备连接。
开发者使用你的日历(仅写入)权限,用于用户日历日程提醒。
开发者收集你的邮箱,用于在必要时和用户联系。
开发者收集你选中的文件,用于提前上传减少上传时间。


当你选择所需的接口后,需要您填写使用说明。 可以参考上面的内容进行填写。

给大家看一下我申请的接口。折腾半天终于把授权登录给整好了。


WX20240208-100953@2x.png


做完上述隐私设置后,需要你重新发布自己的小程序。 并且设置成采集用户隐私。

审核通过后就可以啦。如下图, 请一定注意!!!


WX20240208-101216@2x.png


参考文档


头像昵称填写-微信官方文档

uniapp头像昵称填写

getUserProfile:fail api scope is not declared in the privacy agreement


作者:代码次位面
来源:juejin.cn/post/7332113324651610150
收起阅读 »

UniApp TabBar的巅峰之作:个性化导航的魅力

web
前言在当今数字化时代,用户界面(UI)设计扮演着至关重要的角色,它不仅仅是产品的外表,更是用户与产品互动的第一印象。在一个社交群里,我有幸结识了一位创业的大佬,陈总,他自研的产品UI设计堪称一流,尤其是引人注目的菜单栏设计,深深吸引了我的注意,我就想着将从零玩...
继续阅读 »

前言

在当今数字化时代,用户界面(UI)设计扮演着至关重要的角色,它不仅仅是产品的外表,更是用户与产品互动的第一印象。在一个社交群里,我有幸结识了一位创业的大佬,陈总,他自研的产品UI设计堪称一流,尤其是引人注目的菜单栏设计,深深吸引了我的注意,我就想着将从零玩转系列之微信支付也优化一下

⚠️注意 本次不是从零玩转系列需要有一定的编程能力的同学

二、介绍

UniApp的TabBar

如果应用是一个多 tab 应用,可以通过 tabBar 配置项指定一级导航栏,以及 tab 切换时显示的对应页。

在 pages.json 中提供 tabBar 配置,不仅仅是为了方便快速开发导航,更重要的是在App和小程序端提升性能。在这两个平台,底层原生引擎在启动时无需等待js引擎初始化,即可直接读取 pages.json 中配置的 tabBar 信息,渲染原生tab。

Tips

  • 当设置 position 为 top 时,将不会显示 icon
  • tabBar 中的 list 是一个数组,只能配置最少2个、最多5个 tab,tab 按数组的顺序排序。
  • tabbar 切换第一次加载时可能渲染不及时,可以在每个tabbar页面的onLoad生命周期里先弹出一个等待雪花(hello uni-app使用了此方式)
  • tabbar 的页面展现过一次后就保留在内存中,再次切换 tabbar 页面,只会触发每个页面的onShow,不会再触发onLoad。
  • 顶部的 tabbar 目前仅微信小程序上支持。需要用到顶部选项卡的话,建议不使用 tabbar 的顶部设置,而是自己做顶部选项卡

三、设计

原本的ui样式,真滴丑不好看......

我改造后的,我滴妈真漂亮pink 猛男粉

设计图如下,懂前端的大佬肯定觉得没什么,虽然但是.....我是后端

可以分析他一个大的div包裹并且设置了边框圆形,里面有多个item元素菜单也设置了边框样式,每个菜单上面点击的时候会有背景颜色,我滴妈很简单啊,这我们在 从零玩转系列之微信支付当中讲过呀 给一个 `class样式 如果当前是谁就给谁 通过 vue 的 动态样式 so easy to happy !

四、实现思路

  • 删除TabBar配置的菜单栏:首先,需要从原始TabBar配置中移除默认的菜单栏,这将为自定义TabBar腾出空间。
  • 自定义底部菜单栏:接下来,自定义创建一个底部菜单栏,他是一个组件页面每个页面都需要引入
  • 自定义样式:使用CSS或相关样式设置,将自定义菜单栏精确地定位到底部,确保它与屏幕底部对齐,以实现预期的效果。

五、删除TabBar配置

好的我们尝试来删除 TabBar 配置 重新编译

可以看到报错了,这个错误就是我们使用的是switchTab进行菜单跳转使用别的肯定可以.但是为什么要用switchTab呢?

需求: 和原先的菜单栏功能一样不能销毁其他的菜单页面

那么我们将配置重新填上,他就不会报错了

⚠️注意: 这里有个问题,我们做的是菜单栏在uniapp当中菜单栏跳转是不会销毁其他页面的他其实是根据 switchTab 来进行路由的跳转不回销毁其他TabBar页面

菜单栏跳转的我们是不能销毁的那么这个配置就必须存在了呀,存在就存在无所谓!

遇事不要慌打开文档看看

这个时候我看到了什么?  hide 隐藏啊给我猜到了.绝壁有!!!!

uni.hideTabBar(OBJECT)

好我们知道有这个懂就行,后面我们进行创建我们的 自定义菜单栏组件 tabbar.vue

六、自定义TabBar

创建组件 tabbar.vue 这里我们使用vue3组合式Api搭建页面

<template>

<view class="tab-bar">

<view v-for="(item,index) in tabBarList" :key="index"
:class="{'tab-bar-item': true,currentTar: selected == item.id}"
@click="switchTab(item, index)">

<view class="tab_text" :style="{color: selected == index ? selectedColor : color}">
<image class="tab_img" :src="selected == index ? item.selectedIconPath : item.iconPath">image>
<view>{{ item.text }}view>
view>
view>
view>
template>

代码详细介绍

  1. : 这是一个外部的 view 元素,它用来包裹整个选项卡栏。
  1. : 这是一个 Vue.js 的循环指令 v-for,它用来遍历一个名为 tabBarList 的数据数组,并为数组中的每个元素执行一次循环。在循环过程中,item 是数组中的当前元素,index 是当前元素的索引。v-for 指令还使用 :key="index" 来确保每个循环元素都有一个唯一的标识符。
  1. :tab-bar-item': true,currentTar: selected == item.id}": 这是一个动态的 class 绑定,它根据条件为当前循环的选项卡元素添加不同的 CSS 类。如果 selected 的值等于当前循环元素的 item.id,则添加 currentTar 类,否则添加 tab-bar-item 类。
  1. @click="switchTab(item, index)": 这是一个点击事件绑定,当用户点击选项卡时,会触发名为 switchTab 的方法,并将当前选项卡的 item 对象和索引 index 作为参数传递给该方法。
  1. : 这是一个包含文本内容的 view 元素,它用来显示选项卡的文本。它还具有一个动态的样式绑定,根据条件选择文本的颜色。如果 selected 的值等于当前循环元素的 index,则使用 selectedColor,否则使用 color
  1. : 这是一个 image 元素,它用来显示选项卡的图标。它的 src 属性也是根据条件动态绑定,根据 selected 的值来选择显示不同的图标路径。
  1. {{ item.text }}: 这是一个用来显示选项卡文本内容的 view 元素,它显示了当前选项卡的文本,文本内容来自于 item.text

编写函数

代码当中的 tabBarList 函数要和 pages.json -> tabbar 配置一样哦

<script setup>
import { defineProps, ref } from 'vue'

// 子组件传递参数
const props = defineProps({
selected: {
type: Number,
default: 0
}
})

// 为选中颜色
let color = ref('#000')
// 选中的颜色
let selectedColor = ref('#ffb2b2')
// 菜单栏集合 - 与 pages.json -> tabbar 配置一样
let tabBarList = ref([
{
"id": 0,
"pagePath": "/pages/index/index",
"iconPath": "../../static/icon/icon_2.png",
"selectedIconPath": "../../static/icon/icon_2.png",
"text": "购买课程"
},
{
"id": 1,
"pagePath": "/pages/order/order",
"iconPath": "../../static/icon/gm_1.png",
"selectedIconPath": "../../static/icon/gm_1.png",
"text": "我的订单"
},
{
"id": 2,
"pagePath": "/pages/about/about",
"iconPath": "../../static/icon/about_3.png",
"selectedIconPath": "../../static/icon/about_3.png",
"text": "关于"
}
])

// 跳转tabBar菜单栏
const switchTab = (item) => {
let url = item.pagePath;
uni.switchTab({
url
})
}

script>

自定义TabBar样式


<style lang="less" scoped>
// 外部装修
.tab-bar {
position: fixed;
bottom: 25rpx;
left: 15rpx;
right: 15rpx;
height: 100rpx;
background: white;
padding: 20rpx;
border-radius: 30rpx;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 4px 15px rgba(165, 168, 171, 0.83) !important;

// 当前点击的
.currentTar {
border-radius: 15rpx;
box-shadow: 0 0 15px #D7D7D7FF !important;
transition: all 0.5s ease-in-out;
}

// 给每个 item 设置样式
.tab-bar-item {
//flex: 0.5;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 150rpx;
padding: 15rpx;
background-color: transparent;
transition: all 0.5s ease-in-out;
margin: auto;

// 限制每个icon的大小
.tab_img {
width: 37rpx;
height: 41rpx;
}

// 限制文字大小
.tab_text {
font-size: 20rpx;
margin-top: 9rpx;
flex: 1;
}
}
}
style>

测试

我们自定义的效果出来了但是下面是什么鬼.....

可以看到我们下面也有一个菜单栏是 tabbar 配置产生出来的,我们前面不是说了隐藏吗?

修改函数新增隐藏tabbar代码

// 隐藏原生TabBar
uni.hideTabBar();

最后

本期结束咱们下次再见👋~

🌊 关注我不迷路,如果本篇文章对你有所帮助,或者你有什么疑问,欢迎在评论区留言,我一般看到都会回复的。大家点赞支持一下哟~ 💗


作者:杨不易呀
来源:juejin.cn/post/7330295657167290403

收起阅读 »

uni-app新建透明页面实现全局弹窗

web
需求背景 实现一个能遮住原生 tabbar 和 navbar 的全局操作框 原理 使用一个透明的页面来模拟弹窗,这个页面可以遮住原生 tabbar 和 navbar 页面配置 { "path" : "pages/shootAtWill/shootAtW...
继续阅读 »

需求背景


实现一个能遮住原生 tabbarnavbar 的全局操作框


原理


使用一个透明的页面来模拟弹窗,这个页面可以遮住原生 tabbarnavbar


页面配置


{
"path" : "pages/shootAtWill/shootAtWill",
"style" :
{
"navigationBarTitleText" : "随手拍",
"navigationStyle": "custom",
"backgroundColor": "transparent",
"app-plus": {
"animationType": "slide-in-bottom", // 我这边需求是从底部弹出
"background": "transparent",
"popGesture": "none",
"bounce": "none",
"titleNView": false,
"animationDuration": 150
}
}
}

页面样式


<style>
page {
/* 必须的样式,这是页面背景色为透明色 */
background: transparent;
}
</style>
<style lang="scss" scoped>
// 写你页面的其他样式
</style>

这样的话就新建成功了一个透明的页面,那么这个页面上的东西都可以遮挡住原生 tabbarnavbar


我还加了遮罩:


<template>
<view>
<view class="modal" style="z-index: -1"></view>

</view>
</template>

<style lang="scss" scoped>
.modal {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.5);
}
</style>


效果演示


在这里插入图片描述


作者:鹏北海
来源:juejin.cn/post/7317325043541639178
收起阅读 »

集帅(美)们,别再写 :key = "index" 啦!

web
浅聊一下 灵魂拷问:你有没有在v-for里使用过:key = "index",如果有,我希望你马上改正过来并且给我点个赞,如果没有,来都来了,顺手给我点个赞... 假如您也和我一样,在准备春招。欢迎加我微信shunwuyu,这里有几十位一心去大厂的友友可以相...
继续阅读 »

浅聊一下


灵魂拷问:你有没有在v-for里使用过:key = "index",如果有,我希望你马上改正过来并且给我点个赞,如果没有,来都来了,顺手给我点个赞...



假如您也和我一样,在准备春招。欢迎加我微信shunwuyu,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!



开始


在向掘友们解释为什么不能使用 :key = "index" 之前,我想我还得向你们铺垫一点东西


虚拟DOM


什么是虚拟DOM呢?虚拟DOM是一个对象,没想到吧...我们来看看Vue是如何将template模板里面的东西交给浏览器来渲染的


image.png


首先通过 compiler 将 template模板变成一个虚拟DOM,再将虚拟DOM转换成HTML,最后再交给浏览器V8引擎渲染,那么虚拟DOM是什么样的呢?


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>

<body>
<div id="app">
<ul id="item">
<li v-for="item in list" class="item">{{item}}</li>
</ul>
</div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const list = ref(['vue','js','html'])
return {
list
}
}
}).mount('#app')
</script>
</body>

</html>

在这里,template模板实际上是


 <ul>
<li v-for="item in list">{{item}}</li>
</ul>

通过v-for循环,渲染出来了3个li


<ul>
<li>vue<li>
<li>js<li>
<li>html<li>
</ul>

我们的compiler会将这个模板转化成虚拟DOM


let oldDom = {
tagName = 'ul',
props:{
//存放id 和 class 等
id:'item'
},
children[
{
tagName = 'li',
props:{
class:'item'
},
children:['vue']
},
{
tagName = 'li',
props:{
class:'item'
},
children:['js']
},
{
tagName = 'li',
props:{
class:'item'
},
children:['html']
},
]
}

diff算法


给前面的例子来点刺激的,加上一个按钮和反转函数,点击按钮,list反转


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>

<body>
<div id="app">
<ul>
<li v-for="item in list">{{item}}</li>
</ul>
<button @click="change">change</button>
</div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const list = ref(['唱','跳','rap','篮球'])
const change = ()=>{
list.value.reverse()
}
const add = ()=>{
list.value.unshift('6')
}
return {
list,
change,
}
}
}).mount('#app')
</script>
</body>

</html>

点击change按钮,此时我们的DOM更改vue又是如何来更新DOM的呢?


image.png


众所周知,回流和重绘会消耗极大的性能,而当DOM发生变更的时候会触发回流重绘(可以去看我的文章(从输入4399.com到页面渲染之间的回流和重绘),那么vue3就有一个diff算法,用来优化性能


image.png


当DOM更改,compiler会生成一个新的虚拟DOM,然后通过diff算法来生成一个补丁包,用来记录旧DOM和新DOM的差异,然后再拿到html里面进行修改,最后再交给浏览器V8进行渲染


简单介绍一下diff算法的比较规则



  1. 同层比较,是不是相同的结点,不相同直接废弃老DOM

  2. 是相同结点,比较结点上的属性,产生一个补丁包

  3. 继续比较下一层的子节点,采用双端对列的方式,尽量复用,产生一个补丁包

  4. 同上


image.png


别再写 :key = "index"


要说别写 :key = "index" ,我们得先明白key是用来干什么的...如果没有key,那么在diff算法对新旧虚拟DOM进行比较的时候就没法比较了,你看这里有两个一样的vue,当反转顺序以后diff算法不知道哪个vue该对应哪个vue了


image.png


如果我们用index来充当key的话来看,当我们在头部再插入一个结点的时候,后面的index其实是改变了的,导致diff算法在比较的时候认为他们与原虚拟DM都不相同,那么diff算法就等于没有用...


image.png


可以用随机数吗?


<li v-for="item in list" :key="Math.random()">

想出这种办法的,也是一个狠人...当然是不行的,因为在template模板更新时,会产生一个新的虚拟DOM,而这个虚拟DOM里面的key也是随机值,和原虚拟DOM里的key99.99999%是不一样的...


结尾


希望你以后再也不会写 :key = "index" 了



假如您也和我一样,在准备春招。欢迎加我微信shunwuyu,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!



作者:滚去睡觉
来源:juejin.cn/post/7337513012394115111
收起阅读 »