注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

一个古诗文起名工具

web
大家好,我是 Java陈序员,我们常常会为了给孩子取名而烦恼,取名不仅要好听而且要规避大众化。其实,我们中华文化博大精深,可以借鉴先辈文人们留下的经典诗词中的文字来起名。今天,给大家介绍一个古诗文起名的工具。 这个工具支持从《诗经》、《楚辞》、《唐诗》、《宋词...
继续阅读 »

大家好,我是 Java陈序员,我们常常会为了给孩子取名而烦恼,取名不仅要好听而且要规避大众化。其实,我们中华文化博大精深,可以借鉴先辈文人们留下的经典诗词中的文字来起名。今天,给大家介绍一个古诗文起名的工具。


这个工具支持从《诗经》、《楚辞》、《唐诗》、《宋词》、《乐府诗集》、《古诗三百首》、《著名辞赋》等经典中来生成不同的名字。


Img


我们可以根据自己的姓氏来生成名字,例如《陈》姓:
Img


一次性可以生成六个姓名,并有对应的诗句来源说明,是不是很nice呢!


再比如,《李》姓:
Img


当然了,这个项目没有任何人工智能, 没有判断名字价值的目标函数,所以都是随机生成的。因此可以孕育出一些惊艳、惊鸿一瞥的名字,反之也会生成智障、搞笑的名字,大家可自行甄别。


大家如果对于这个项目感兴趣的话,也可自行下载代码到本地运行:


# 克隆代码
git clone https://github.com/holynova/gushi_namer.git

# 安装依赖
npm install

# 本地调试
npm start

# 编译
npm run build

或者直接使用线上地址:


http://xiaosang.net/gushi_namer/

线上地址也是完美支持移动端的。


Img


大家快把这个地址收藏到收藏夹吃灰吧,以免需要的时候找不到!


最后


推荐的开源项目已经收录到 GitHub 项目,欢迎 Star


https://github.com/chenyl8848/great-open-source-project


大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!



作者:Java陈序员
来源:juejin.cn/post/7282692430100201535
收起阅读 »

百分百空手接大锅

web
背景 愉快的双休周末刚过完,早上来忽然被运营通知线上业务挂了,用户无法下单。卧槽,赶紧进入debug模式,一查原来是服务端返回的数据有问题,赶紧问了服务端,大佬回复说是业务部门配置套餐错误。好在主责不在我们,不过赶紧写了复盘文档,主动找自己的责任,扛起这口大锅...
继续阅读 »

背景


愉快的双休周末刚过完,早上来忽然被运营通知线上业务挂了,用户无法下单。卧槽,赶紧进入debug模式,一查原来是服务端返回的数据有问题,赶紧问了服务端,大佬回复说是业务部门配置套餐错误。好在主责不在我们,不过赶紧写了复盘文档,主动找自己的责任,扛起这口大锅,都怪我们前端,没有做好前端监控,导致线上问题持续两天才发现。原本以为运营会把推辞一下说不,锅是她们的,可惜人家不太懂人情世故,这锅就扣在了技术部头上。虽然但是,我还是静下心来把前端异常监控搞了出来,下次一定不要主动接锅,希望看到本文的朋友们也不要随便心软接锅^_^


监控


因为之前基于sentry做了埋点处理,基础已经打好,支持全自动埋点、手动埋点和数据上报。相关的原理可以参考之前的一篇文章如何从0-1构建数据平台(2)- 前端埋点。本次监控的数据上报也基于sentry.js。那么如何设计整个流程呢。具体步骤如下:




  1. 监控数据分类




  2. 监控数据定义




  3. 监控数据收集




  4. 监控数据上报




  5. 监控数据输出




  6. 监控数据预警




数据分类


我们主要是前端的数据错误,一般的异常大类分为逻辑异常和代码异常。基于我们的项目,由于涉及营收,我们就将逻辑错误专注于支付异常,其他的代码导致的错误分为一大类。然后再将两大异常进行细分,如下:




  1. 支付异常


    1.1 支付成功


    1.2 支付失败




  2. 代码异常


    2.1 bindexception


     2.1.1  js_error

    2.1.2 img_error

    2.1.3 audio_error

    2.1.4 script_error

    2.1.5 video_error



  3. unhandleRejection


    3.1 promise_unhandledrejection_error


    3.2 ajax_error




  4. vueException




  5. peformanceInfo




数据定义


基于sentry的上报数据,一般都包括事件与属性。在此我们定义支付异常事件为“page_h5_pay_monitor”,定义代码异常事件为“page_monitor”。然后支付异常的属性大概为:



pay_time,

pay_orderid,

pay_result,

pay_amount,

pay_type,

pay_use_coupon,

pay_use_coupon_id,

pay_use_coupon_name,

pay_use_discount_amount,

pay_fail_reason,

pay_platment


代码异常不同的错误类型可能属性会有所区别:



// js_error

monitor_type,

monitor_message,

monitor_lineno,

monitor_colno,

monitor_error,

monitor_stack,

monitor_url

// src_error

monitor_type,

monitor_target_src,

monitor_url

// promise_error

monitor_type,

monitor_message,

monitor_stack,

monitor_url

// ajax_error

monitor_type,

monitor_ajax_method,

monitor_ajax_data,

monitor_ajax_params,

monitor_ajax_url,

monitor_ajax_headers,

monitor_url,

monitor_message,

monitor_ajax_code

// vue_error

monitor_type,

monitor_message,

monitor_stack,

monitor_hook,

monitor_url

// peformanceInfo 为数据添加 loading_time 属性,该属性通过entryTypes获取

try {

const observer = new PerformanceObserver((list) => {

for (const entry of list.getEntries()) {

if (entry.entryType === 'paint') {

sa.store.set('loading_time', entry.startTime)

}
}

})

observer.observe({ entryTypes: ['paint'] })

} catch (err) {

console.log(err)

}


数据收集


数据收集通过事件绑定进行收集,具体绑定如下:


import {

BindErrorReporter,

VueErrorReporter,

UnhandledRejectionReporter

} from './report'

const Vue = require('vue')


// binderror绑定

const MonitorBinderror = () => {

window.addEventListener(

'error',

function(error) {

BindErrorReporter(error)

},true )

}

// unhandleRejection绑定 这里由于使用了axios,因此ajax_error也属于promise_error

const MonitorUnhandledRejection = () => {

window.addEventListener('unhandledrejection', function(error) {

if (error && error.reason) {

const { message, code, stack, isAxios, config } = error.reason

if (isAxios && config) {

// console.log(config)

const { data, params, headers, url, method } = config

UnhandledRejectionReporter({

isAjax: true,

data: JSON.stringify(data),

params: JSON.stringify(params),

headers: JSON.stringify(headers),

url,

method,

message: message || error.message,

code

})

} else {

UnhandledRejectionReporter({

isAjax: false,

message,

stack

})

}

}

})

}

// vueException绑定

const MonitorVueError = () => {

Vue.config.errorHandler = function(error, vm, info) {

const { message, stack } = error

VueErrorReporter({

message,

stack,

vuehook: info

})

}

}

// 输出绑定方法

export const MonitorException = () => {

try {

MonitorBinderror()

MonitorUnhandledRejection()

MonitorVueError()

} catch (error) {

console.log('monitor exception init error', error)

}

}


数据上报


数据上报都是基于sentry进行上报,具体如下:



/*

* 异常监控库 基于sentry jssdk

* 监控类别:

* 1、window onerror 监控未定义属性使用 js资源加载失败问题

* 2、window addListener error 监控未定义属性使用 图片资源加载失败问题

* 3、unhandledrejection 监听promise对象未catch的错误

* 4、vue.errorHandler 监听vue脚本错误

* 5、自定义错误 包括接口错误 或其他diy错误

* 上报事件: page_monitor

*/


// 错误类别常量

const ERROR_TYPE = {

JS_ERROR: 'js_error',

IMG_ERROR: 'img_error',

AUDIO_ERROR: 'audio_error',

SCRIPT_ERROR: 'script_error',

VIDEO_ERROR: 'video_error',

VUE_ERROR: 'vue_error',

PROMISE_ERROR: 'promise_unhandledrejection_error',

AJAX_ERROR: 'ajax_error'

}

const MONITOR_NAME = 'page_monitor'

const PAY_MONITOR_NAME = 'page_h5_pay_monitor'

const MEMBER_PAY_MONITOR_NAME = 'page_member_pay_monitor'

export const BindErrorReporter = function(error) {

if (error) {

if (error.error) {

const { colno, lineno } = error

const { message, stack } = error.error

// 过滤

// 客户端会有调用calljs的场景 可能有一些未知的calljs

if (message && message.toLowerCase().indexOf('calljs') !== -1) {

return

}

sa.track(MONITOR_NAME, {

//属性

})

} else if (error.target) {

const type = error.target.nodeName.toLowerCase()

const monitorType = type + '_error'

const src = error.target.src

sa.track(MONITOR_NAME, {

//属性

})

}

}

}

export const UnhandledRejectionReporter = function({

isAjax = false,

method,

data,

params,

url,

headers,

message,

stack,

code

}
) {

if (!isAjax) {

// 过滤一些特殊的场景

// 1、自动播放触发问题

if (message && message.toLowerCase().indexOf('user gesture') !== -1) {

return

}

sa.track(MONITOR_NAME, {

//属性

})

} else {

sa.track(MONITOR_NAME, {

//属性

})

}

}

export const VueErrorReporter = function({ message, stack, vuehook }) {

sa.track(MONITOR_NAME, {

//属性

})

}

export const H5PayErrorReport = ({

isSuccess = true,

amount = 0,

type = -1,

couponId = -1,

couponName = '',

discountAmount = 0,

reason = '',

orderid = 0,

}
) => {

// 事件名:page_member_pay_monitor

sa.track(PAY_MONITOR_NAME, {

//属性

})

}


以上,通过sentry的sa.track进行上报,具体不作展开


输出与预警


数据被上报到大数据平台,被存储到hdfs中,然后我们直接做定时任务读取hdfs进行一定的过滤通过钉钉webhook输出到钉钉群,另外如果有需要做数据备份可以通过hdfs到数据仓库再到kylin进行存储。


总结


数据监控对于大的,特别是涉及营收的平台是必要的,我们在设计项目的时候一定要考虑到,最好能说服服务端,让他们服务端也提供相应的代码监控。ngnix层或者云端最好也来一层。严重的异常可以直接给你打电话,目前云平台都有相应支持。这样有异常及时发现,锅嘛,接到手里就可以精准扔出去了。


作者:CodePlayer
来源:juejin.cn/post/7244363578429030459
收起阅读 »

跨浏览器兼容性指南:解决常见的前端兼容性问题

跨浏览器兼容性是前端开发中至关重要的概念。由于不同浏览器(如Chrome、Firefox、Safari等)在实现Web标准方面存在差异,网页在不同浏览器上可能会呈现不一致的结果。因此,确保网页在各种浏览器上都能正确显示和运行,是提供良好用户体验、扩大受众范围以...
继续阅读 »

跨浏览器兼容性是前端开发中至关重要的概念。由于不同浏览器(如Chrome、Firefox、Safari等)在实现Web标准方面存在差异,网页在不同浏览器上可能会呈现不一致的结果。因此,确保网页在各种浏览器上都能正确显示和运行,是提供良好用户体验、扩大受众范围以及增强网站可访问性的关键。



兼容性测试工具和方法


自动化测试工具的使用 自动化测试工具能够帮助开发者更快速、高效地进行浏览器兼容性测试,以下是一些常用的自动化测试工具:


  1. Selenium:Selenium是一个流行的自动化测试框架,用于模拟用户在不同浏览器上的交互。它支持多种编程语言,并提供了丰富的API和工具,使开发者可以编写功能测试、回归测试和跨浏览器兼容性测试。
  2. TestCafe:TestCafe是一款基于JavaScript的自动化测试工具,用于跨浏览器测试。它不需要额外的插件或驱动程序,能够在真实的浏览器中运行测试,并支持多个浏览器和平台。
  3. Cypress:Cypress是另一个流行的自动化测试工具,专注于现代Web应用的端到端测试。它提供了简单易用的API,允许开发者在多个浏览器中运行测试,并具有强大的调试和交互功能。
  4. BrowserStack:BrowserStack是一个云端跨浏览器测试平台,提供了大量真实浏览器和移动设备进行测试。它允许开发者在不同浏览器上同时运行测试,以检测网页在不同环境中的兼容性问题。

手动测试方法和技巧 除了自动化测试工具,手动测试也是重要的一部分,特别是需要验证用户体验和视觉方面的兼容性。以下是几种常用的手动测试方法和技巧:


  1. 多浏览器测试:在不同浏览器(如Chrome、Firefox、Safari)上手动打开网页,并检查布局、样式和功能是否正常。特别关注元素的位置、尺寸、颜色和字体等。
  2. 响应式测试:使用浏览器的开发者工具或专门的响应式测试工具(如Responsive Design Mode)来模拟不同设备的屏幕尺寸和方向,确保网页在不同设备上呈现良好。
  3. 用户交互测试:模拟用户操作,例如点击按钮、填写表单、滚动页面和使用键盘导航,以确保网页在各种用户交互场景下都能正常运行。
  4. 边界条件测试:测试极端情况下的表现,例如超长文本、超大图片、无网络连接等。确保网页在异常情况下具备良好的鲁棒性和用户友好性。

设备和浏览器的兼容性测试 为了确保网页在不同设备和浏览器上的兼容性,以下是一些建议的测试方法:

  1. 设备兼容性测试:

    • 使用真实设备:将网页加载到不同类型的设备上进行测试,例如桌面电脑、笔记本电脑、平板电脑和智能手机等。
    • 使用模拟器和仿真器:利用模拟器或仿真器来模拟不同设备的环境,并进行测试。常用的模拟器包括Android Studio自带的模拟器和Xcode中的iOS模拟器。
  2. 浏览器兼容性测试:

    • 考虑常见浏览器:测试网页在主流浏览器(如Chrome、Firefox、Safari、Edge)的最新版本上的兼容性。
    • 旧版本支持:如果目标受众使用旧版浏览器,需要确保网页在这些浏览器上也能正常运行。可以使用Can I Use(caniuse.com)等工具来查找特定功能在不同浏览器上的兼容性。
  3. 定期更新测试设备和浏览器:随着时间的推移,新的设备和浏览器版本会发布,因此建议定期更新测试设备和浏览器,以保持兼容性测试的准确性。


常见的前端兼容性问题


我在下面列举了一些常见的兼容性问题,以及解决办法。

  • 浏览器兼容性问题:

    • 不同浏览器对CSS样式的解析差异:使用CSS预处理器(如Less、Sass)可以减少浏览器间的差异,并使用reset.css或normalize.css来重置默认样式。
    • JavaScript API的差异:使用polyfill或Shim库(如Babel、ES5-Shim)来填补不同浏览器之间JavaScript API的差异。
    1. 响应式布局兼容性问题:

      • 媒体查询失效:确保正确使用CSS媒体查询,并对不支持媒体查询的旧版浏览器提供备用样式。
      • 页面在不同设备上的布局错乱:使用弹性布局(Flexbox)、网格布局(Grid)和CSS框架(如Bootstrap)可以有效解决布局问题。
    2. 图片兼容性问题:

      • 不支持的图片格式:使用WebP、JPEG XR等现代图片格式,同时提供备用格式(如JPEG、PNG)以供不支持的浏览器使用。
      • Retina屏幕显示问题:使用高分辨率(@2x、@3x)图片,并通过CSS的background-size属性或HTML的srcset属性适应不同屏幕密度。
    3. 字体兼容性问题:

      • 不支持的字体格式:使用Web字体(如Google Fonts、Adobe Fonts)或@font-face规则,并提供备用字体格式以适应不同浏览器。
      • 字体加载延迟:使用字体加载器(如Typekit、Font Face Observer)来优化字体加载,确保页面内容在字体加载完成前有一致的显示。
    4. JavaScript兼容性问题:

      • 不支持的ES6+特性:使用Babel等工具将新版本的JavaScript代码转换为旧版本的代码,以兼容不支持最新特性的浏览器。
      • 缺乏对旧版浏览器的支持:根据目标用户群体使用的浏览器版本,选择合适的JavaScript库或Polyfill进行填充和修复。
    5. 表单兼容性问题:

      • 不同浏览器对表单元素样式的差异:使用CSS样式重置或规范化库来保证表单元素在各个浏览器上显示一致。
      • HTML5表单元素的不完全支持:使用JavaScript库(如Modernizr)来检测并补充HTML5表单元素的功能支持。
    6. Ajax和跨域请求问题:

      • 浏览器安全策略导致的Ajax跨域问题:通过设置CORS(跨域资源共享)或JSONP(仅适用于GET请求)来解决跨域请求问题。
      • IE浏览器对XMLHttpRequest的限制:使用自动检测并替代方案(如jQuery的AJAX方法),或考虑使用现代的XMLHttpRequest Level 2 API(如fetch)。

    CSS常见的兼容性问题


    CSS兼容性问题是在不同浏览器中,对CSS样式的解析和渲染会存在一些差异。以下是一些常见的CSS兼容性问题以及对应的解决方案:




    1. 盒模型:



      • 问题:不同浏览器对盒模型的解析方式存在差异,导致元素的宽度和高度计算结果不一致。

      • 解决方案:使用CSS盒模型进行标准化,通过设置box-sizing: border-box;来确保元素的宽度和高度包括边框和内边距。




    2. 浮动和清除浮动:



      • 问题:浮动元素可能导致父元素的塌陷问题(高度塌陷)以及与其他元素的重叠问题。

      • 解决方案:可以使用清除浮动的技巧,如在容器元素末尾添加一个空的<div style="clear: both;"></div>元素来清除浮动,或者使用clearfix类来清除浮动(如.clearfix:after { content: ""; display: table; clear: both; })。




    3. 绝对定位和相对定位:



      • 问题:绝对定位和相对定位的元素在不同浏览器中的表现可能存在差异,特别是在z轴上的堆叠顺序。

      • 解决方案:明确设置定位元素的position属性(position: relative;position: absolute;),并使用z-index属性来控制元素的堆叠顺序。




    4. 样式重置与规范化:



      • 问题:不同浏览器对默认样式的定义存在差异,导致页面在不同浏览器中显示效果不一致。

      • 解决方案:引入样式重置或规范化的CSS文件,如Eric Meyer's Reset CSS 或 Normalize.css。这些文件通过将默认样式置为一致的基准值,使页面在各个浏览器上的显示效果更加一致。




    5. 不同浏览器对CSS盒模型的解析差异:



      • 解决方案:使用box-sizing: border-box;样式来确保元素的宽度和高度包括内边距和边框。




    6. CSS选择器差异:



      • 解决方案:避免使用过于复杂的选择器,尽量使用普通的类名、ID或标签名进行选择。如果需要兼容旧版浏览器,请使用Polyfill或Shim库。




    7. 浮动元素引起的布局问题:



      • 解决方案:使用清除浮动(clear float)技术,例如在容器的末尾添加一个具有clear: both;样式的空元素或使用CSS伪类选择器(如:after)清除浮动。




    8. CSS3特性的兼容性问题:



      • 解决方案:使用CSS前缀来适应不同浏览器支持的CSS3属性和特效。例如,-webkit-适用于Chrome和Safari,-moz-适用于Firefox。




    除了以上问题,还可能存在字体、渐变、动画、弹性盒子布局等方面的兼容性问题。在实际开发中,可以使用CSS预处理器(如Less、Sass)来减少浏览器间的差异,并借助Autoprefixer等工具自动添加浏览器前缀,以确保在各种浏览器下的一致性。


    JavaScript常见的兼容性问题


    以下是几个常见的 JavaScript 兼容性问题及其解决方案:

  • 不支持ES6+语法和新的API:(上面有提到)

    • 问题:旧版本的浏览器可能不支持ES6+语法(如箭头函数、let和const等)和新的JavaScript API。
    • 解决方案:使用Babel等工具将ES6+代码转换为ES5语法,以便在旧版本浏览器中运行,并使用polyfill或shim库来提供缺失的JavaScript API支持。
    1. 缺乏对新JavaScript特性的支持:

      • 问题:某些浏览器可能不支持最新的JavaScript特性、方法或属性。
      • 解决方案:在编写代码时,可以检查特定的JavaScript特性是否受支持,然后使用适当的替代方法或实现回退方案。可以使用Can I use (caniuse.com) 等网站来查看浏览器对特定功能的支持情况。
    2. 事件处理程序兼容性问题:

      • 问题:不同浏览器对事件处理程序的绑定、参数传递和事件对象的访问方式存在差异。
      • 解决方案:使用跨浏览器的事件绑定方法(例如addEventListener),正确处理事件对象,并避免依赖事件对象的特定属性或方法。
    3. XMLHttpRequest兼容性问题:

      • 问题:旧版本的IE浏览器(< IE7)使用ActiveX对象而不是XMLHttpRequest。
      • 解决方案:检查浏览器是否支持原生的XMLHttpRequest对象,如果不支持,则使用ActiveX对象作为替代方案。
    4. JSON解析兼容性问题:

      • 问题:旧版本的浏览器可能不支持JSON.parse()JSON.stringify()方法。
      • 解决方案:使用json2.js等JSON解析库来提供对这些方法的支持,或者在必要时手动实现JSON的解析和序列化功能。
    5. DOM操作兼容性问题:

      • 问题:不同浏览器对DOM操作方法(如getElementByIdquerySelector等)的实现方式存在差异。
      • 解决方案:使用跨浏览器的DOM操作库(如jQuery、prototype.js)或使用feature detection技术来检测浏览器对特定DOM方法的支持,并根据情况使用不同的解决方案。
    6. 跨域请求限制:

      • 问题:浏览器的同源策略限制了通过JavaScript进行的跨域请求。
      • 解决方案:使用JSONP、CORS(跨源资源共享)、服务器代理或 WebSocket等技术来绕过跨域请求限制。

    总结


    跨浏览器兼容性是网站和应用程序开发中至关重要的一环。由于不同浏览器对CSS和JavaScript的解析和渲染存在差异,如果不考虑兼容性问题,可能会导致页面在不同浏览器上显示不正确、功能不正常甚至完全无法使用的情况。这将严重影响用户体验,并可能导致流失用户和损害品牌声誉。


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

    组件阅后即焚?挂载即卸载!看完你就理解了

    web
    前言 上家公司有个需求是批量导出学生的二维码,我一想这简单啊,不就是先批量获取学生数据,然后根据QRcode生成二维码,然后在用html2canvas导出成图片嘛。 由于公司工具库有现成的生成压缩包方法,我只需要获得对应的图片blob就可以了,非常的easy啊...
    继续阅读 »

    前言


    上家公司有个需求是批量导出学生的二维码,我一想这简单啊,不就是先批量获取学生数据,然后根据QRcode生成二维码,然后在用html2canvas导出成图片嘛。
    由于公司工具库有现成的生成压缩包方法,我只需要获得对应的图片blob就可以了,非常的easy啊。


    开始动手


    思路没啥问题,但第一步就犯了难,用过react框架或者其他MVVM框架的都知道,这种类型的框架都是数据驱动视图,也就是说一般情况下,必须先获得数据,然后根据数据才能得到视图。


    但是问题是,html2canvas也是必须需要获取真实dom的快照然后转换成canvas对象。


    听着好像不冲突,诶,我先获取数据,然后渲染出视图,在依次通过html2canvas来生成图片不就完事了嘛!但是想归想,却不能这么做。


    原因主要有两个,一个原因呢是交互逻辑上就行不太通,也不友好。你不能“啪”点一下导出按钮,然后获取数据之后再去等所有数据渲染出对应组件之后,再去延迟处理导出逻辑。(耗时太长)


    另一个原因呢,主要是跟html2canvas这个工具库有关系了,它的原理简单来说呢,就是复制你期望获取截图的那个dom的渲染树,然后根据这个渲染树在当前页面生成一个你看不见的canvas dom对象来。那么问题来了,因为是批量下载,所以肯定会有大量的数据,那么如果不做处理,就会有大量的canvas对象存在当前页面。


    canvas标签是会占用内存的,那么当同时存在过多的canvas时,就会出现一个问题,页面卡顿甚至崩溃。所以,这是第二个原因。


    那么这篇文章主要是解决第一个原因所带来的问题的。


    编程!启动!


    第一步


    那么先简单的随便生成一个组件好了,因为是公司源码嘛,大家懂的都懂。


    interface IProps {
    qrCode: string
    studentName: string
    className: string
    }

    const SaveQRCode = (props: IProps) => {
    const divRef = React.useRef<HTMLDivElement>(null)
    // 具体怎么渲染看你们需求了
    return (
    <div ref={divRef}>XXXXXX</div>
    )
    }

    看到代码,用过html2canvas的小伙伴应该知道ref是干嘛用的了,html2canvas()这个方法的参数是HTMLElement,传统一点的办法呢,可以通过document.getXXXXX这个方法来获取真实的dom元素。那么Ref就是替代前者的,它可以直接通过react框架获取真实的dom元素。


    第二步


    那么最简单的组件我们已经写好了,接下来就是如何动态的挂载这个组件,并且在挂载完之后就立刻卸载它。


    那么先来理一下思路:
    1、动态地挂载这个组件,且不能被用户肉眼观察到
    2、挂载动作执行完立刻执行html2canvas获取canvas对象
    3、通过canvas对象转换成blob对象并返回,或者直接通过回调函数返回canvas对象
    4、组件卸载,清空dom


    那么根据上面几点,可以得出:从外部获取的肯定是有组件这个东西,而挂载的位置则有要求,但并不一定需要从外部获取。


    为了不被样式影响,我们直接在body标签下,再挂载一个div标签,来进行组件的动态渲染和卸载,同时也避免了影响之前dom树的结构。


    思路就说到这了,接下来直接抛出代码:


    const AsyncMountComponent = (
    getElement: (onUnmount: () => void) => ReactNode,
    container: HTMLElement,
    ) => {
    const root = createRoot(container)
    const element = getElement(() => {
    root.unmount()
    container.remove()
    })
    root.render(<Suspense fallback={null}>{element}</Suspense>)
    }

    这里我因为想做的更加通用一点,所以把根节点让外部进行处理,如果希望更加业务一点,比如当前这个场景必然不会让用户可见,可以直接改成


    const AsyncMountComponent = (getElement: (onUnmount: () => void) => ReactNode) => {
    const div = document.createElement('div')
    div.style.position = 'absolute'
    div.style.left = '2000px'
    document.body.appendChild(div)
    const root = createRoot(div)
    const element = getElement(() => {
    root.unmount()
    container.remove()
    })
    root.render(<Suspense fallback={null}>{element}</Suspense>)
    }

    这里的隐藏方式看个人喜好,无所谓。但有一点要注意的是,一定要可见,不然的话html2canvas生成不了图片,这里是最简单粗暴的方式,直接偏移left


    第三步


    那么地基打好了,我们该怎么用这两个东西呢


    interface IProps {
    qrCode: string
    studentName: string
    className: string
    // 这里自然就是获取blob和canvas对象的地方了
    onConfirm?: (data: { canvas: HTMLCanvasElement, blob: Blob }) => void
    // 这里是卸载的地方,由外部决定何时卸载节点,更加自由
    onUnmount?: () => void
    }

    const SaveQRCode = (props: IProps) => {
    const divRef = React.useRef<HTMLDivElement>(null)
    useEffect(() => {
    if (divRef.current && props.onConfirm) {
    html2canvas(divRef.current).then((canvas) => {
    canvas.toBlob((blob) => {
    props.onConfirm!({canvas, blob: blob!})
    props.onUnmount!()
    })
    })
    }
    }, [])
    // 具体怎么渲染看你们需求了
    return (
    <div ref={divRef}>XXXXXX</div>
    )
    }

    首先我们对组件进行修改,因为我的方案是第一种,没有太业务向,所以说一些业务逻辑必然是要到组件层面去处理的,所以添加两个参数,一个获取blobcanvas对象,另一个用来卸载节点。


    至于useEffect就很容易理解了,挂载后用html2canvas处理组件顶层div获取截图,然后返回数据,并卸载节点。


    组件改造完毕了,那我们接下来把这两个组合一下


    const getQRCodeBlobCanvas = async (props: IProps): Promise<{
    canvas: HTMLCanvasElement, blob: Blob
    }> => {
    return new Promise((resolve) => {
    const div = document.createElement('div')
    div.style.position = 'absolute'
    div.style.left = '2000px'
    document.body.appendChild(div)
    asyncMountComponent(
    (dispose) => (<SaveQRCode {...props} onConfirm={resolve} onUnmount={dispose}/>),
    div
    )
    })
    }

    那么一个简单的动态阅后即焚组件就完成了,且可以直接通过方法的形式使用,完美适配批量导出功能,当然也包括单个导出,至于批量导出的细节我就不写了,非常的简单。


    升级V2


    我只提供了最通用一种方式来做这么个阅后即焚组件,之后我闲着无聊,又把它做了一次业务向升级,获得了V2版本


    这个版本呢,你只需要传入一个组件进去,且不用关心何时卸载,它是最真实的阅后即焚。至于数据,会通过Promise的方式返回给用户。


    const Wrapper = ({callback, children}: {  
    callback: (data: { blob: Blob,canvas: HTMLCanvasElement }) => void,
    children: ReactNode
    }
    ) => {
    const divRef = useRef<HTMLDivElement>(null)
    useEffect(() => {
    if (divRef.current) {
    html2canvas(divRef.current).then((canvas) => {
    canvas.toBlob((blob) => {
    callback({canvas, blob: blob!})
    })
    })
    }
    }, [])
    return <div ref={divRef}>
    {children}
    </div>

    }

    const getComponentSnapshotBlobCanvas = (getElement: () => ReactNode): Promise<{canvas:HTMLCanvasElement, blob: Blob}> => {
    return new Promise((resolve) => {
    const div = document.createElement('div')
    div.style.position = 'absolute'
    div.style.left = '2000px'
    document.body.appendChild(div)
    const root = createRoot(div)
    root.render((
    <Wrapper
    callback={(values) =>
    {
    root.unmount()
    div.remove()
    resolve(values)
    }}
    >
    {getElement()}
    </Wrapper>

    ))
    })
    }

    其实也没啥特别的,无非就是把业务层公共的东西封装进了方法里,思路还是上面那个思路。


    那么这篇博客就到这里了,感谢阅读!


    作者:寒拾Ciao
    来源:juejin.cn/post/7278512641781334051
    收起阅读 »

    求你了,别再说不会JSONP了

    JSONP是一种很远古用来解决跨域问题的技术,当然现在实际工作当中很少用到该技术了,但是很多同学在找工作面试过程中还是经常被问到,本文将带您深入了解JSONP的工作原理、使用场景及安全注意事项,让您轻松掌握JSONP。 JSONP是什么? JSONP,全称JS...
    继续阅读 »

    JSONP是一种很远古用来解决跨域问题的技术,当然现在实际工作当中很少用到该技术了,但是很多同学在找工作面试过程中还是经常被问到,本文将带您深入了解JSONP的工作原理、使用场景及安全注意事项,让您轻松掌握JSONP。


    JSONP是什么?


    JSONP,全称JSON with Padding,是一项用于在不同域之间进行数据交互的技术。这项技术的核心思想是通过在页面上动态创建<script>标签,从另一个域加载包含JSON数据的外部脚本文件,然后将数据包裹在一个函数调用中返回给客户端。JSONP不仅简单而且强大,尤其在处理跨域数据请求时表现出色。


    JSONP的工作原理


    JSONP的工作流程如下:


    • 客户端请求数据:首先,客户端会创建一个<script>标签,向包含JSON数据的远程服务器发出请求。这个请求通常包括一个名为callback的参数,用来指定在数据加载完毕后应该调用的JavaScript函数的名称。
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JSONP Example</title>
    </head>
    <body>
    <h1>JSONP Example</h1>
    <div id="result"></div>

    <script>
    // 定义JSONP回调函数
    function callback(data) {
    const resultDiv = document.getElementById('result');
    resultDiv.innerHTML = `Name: ${data.name}, Age: ${data.age}`;
    }

    // 创建JSONP请求
    const script = document.createElement('script');
    script.src = 'http://localhost:3000/data?callback=callback';
    document.body.appendChild(script);
    </script>
    </body>
    </html>

    • 服务器响应:服务器收到请求后,将JSON数据包装在指定的回调函数中,并将其返回给客户端。响应的内容类似于:
    const Koa = require('koa');
    const Router = require('koa-router');

    const app = new Koa();
    const router = new Router();

    // 定义一个简单的JSON数据
    const jsonData = {
    name: 'John',
    age: 30,
    };

    // 添加路由处理JSONP请求
    router.get('/data', (ctx) => {
    const callback = ctx.query.callback;
    if (callback) {
    ctx.body = `${callback}(${JSON.stringify(jsonData)})`;
    } else {
    ctx.body = jsonData;
    }
    });

    // 将路由注册到Koa应用程序
    app.use(router.routes()).use(router.allowedMethods());

    // 启动Koa应用程序
    const port = 3000;
    app.listen(port, () => {
    console.log(`Server is running on port ${port}`);
    });


    • 客户端处理数据:在客户端的页面中,我们必须事先定义好名为callback的函数,以便在响应被加载和执行时被调用。这个函数会接收JSON数据,供我们在页面中使用。

    JSONP使用场景


    跨域请求:JSONP主要用于解决跨域请求问题,尤其适用于无法通过CORS或代理等方式实现跨域的情况。
    数据共享:在多个域名之间共享数据,可以利用JSONP实现跨域数据共享。
    第三方数据获取:当需要从第三方网站获取数据时,可以使用JSONP技术。


    使用JSONP注意事项


    JSONP的简单性和广泛的浏览器支持使其成为跨域数据交互的强大工具。然而,我们也必须谨慎使用它,因为它存在一些安全考虑,我们分析下它的优缺点:


    优点


    • 简单易用:JSONP非常容易实现和使用,无需复杂的配置。
    • 跨浏览器支持:几乎所有现代浏览器都支持JSONP。
    • 绕过同源策略:JSONP帮助我们绕过了同源策略的限制,轻松获取跨域数据。

    安全考虑


    • XSS风险:JSONP未经过滤的数据可能会引起XSS攻击,因此需要对返回的数据进行过滤和验证。
    • CSRF攻击:使用JSONP时要注意防范CSRF攻击,可以通过添加随机数等方式增强安全性。
    • 仅支持GET请求:JSONP只支持GET请求,不适用于POST等其他HTTP方法。
    • 难以处理HTTP错误:JSONP难以有效处理HTTP错误,在请求失败时的异常处理比较困难。

    随着技术的发展,JSONP已不再是首选跨域解决方案,但了解它的工作原理仍然有助于我们更深入地理解跨域数据交互的基本原理。在实际项目中,根据具体需求和安全考虑,建议优先选择CORS或代理服务器方式处理跨域问题。


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

    为什么日本的网站看起来如此不同

    快来免费体验ChatGpt plus版本的,我们出的钱 体验地址:chat.waixingyun.cn 可以加入网站底部技术群,一起找bug,另外新版作图神器,ChatGPT4 已上线 cube.waixingyun.cn/home 该篇文章讨论了日本网站外观...
    继续阅读 »

    快来免费体验ChatGpt plus版本的,我们出的钱 体验地址:chat.waixingyun.cn 可以加入网站底部技术群,一起找bug,另外新版作图神器,ChatGPT4 已上线 cube.waixingyun.cn/home


    该篇文章讨论了日本网站外观与设计的独特之处。作者指出日本网站设计与西方设计存在明显差异。文章首先强调了日本网站的视觉风格,包括丰富的色彩、可爱的角色和复杂的排版。作者解释了这种风格背后的文化和历史因素,包括日本的印刷传统和动漫文化。


    文章还讨论了日本网站的信息密集型布局,这种布局适应了日本语言的特点,使得页面能够容纳大量文字和图像。此外,文章提到了日本网站的功能丰富性,如弹出式窗口和互动元素,以及这些元素在用户体验方面的作用。


    作者强调了日本网站在技术和创新方面的进步,尽管在过去存在技术限制。最后,文章提出了一些关于如何将日本网站设计的元素应用到其他文化中的建议。


    下面是正文~~~


    多年来,我朋友与日本的网站有过许多接触——无论是研究签证要求、计划旅行,还是简单地在线订购东西。而我花了很长时间才适应这些网站上的大段文字、大量使用鲜艳颜色和10多种不同字体的设计,这些网站就像是直接冲着你扔过来的。




    虽然有许多网站都采用了更简约、易于导航的设计,适应了西方网站的用户,但是值得探究的是为什么这种更复杂的风格在日本仍然盛行。


    只是为了明确起见,这些不是过去的遗迹,而是维护的网站,许多情况下,它们最后一次更新是在2023年。




    我们可以从几个角度来分析这种设计方法:


    • 字体和前端网站开发限制
    • 技术发展与停滞
    • 机构数字素养(或其缺乏)
    • 文化影响

    与大多数话题一样,很可能没有一个正确的答案,而是这个网站设计是随着时间的推移而相互作用的各种因素的结果。


    字体和前端网站开发限制


    对于会一些基本排版知识、掌握适当软件并有一些空闲时间的人来说,为罗马化语言创造新字体可能是一项有趣的挑战。然而,对于日语来说,这是一个完全不同层次的努力。


    要从头开始创建英文字体,需要大约230个字形——字形是给定字母的单个表示(A a a算作3个字形)——或者如果想覆盖所有基于拉丁字母表的语言,则需要840个字形。对于日语而言,由于其三种不同的书写系统和无数的汉字,需要7,000至16,000个字形甚至更多。因此,在日语中创建新字体需要有组织的团队合作和比其拉丁字母表的同行们更多的时间。



    这并不令人意外,因此中文和(汉字)韩文字体也面临着类似的工作量,这导致这些语言通常被称为CJK字体所覆盖。



    由于越来越少的设计师面对这个特殊的挑战,建立网站时可供选择的字体也越来越少。再加上缺乏大写字母和使用日文字体会导致加载时间较长,因为需要引用更大的库,这就不得不采用其他方式来创建视觉层次。


    以美国和日本版的星巴克主页为例:


    美国的:




    日本的




    就这样,我们就可以解释为什么许多日本网站倾向于用文字较多的图片来表示内容类别了。有时,你甚至会看到每个磁贴都使用自己定制的字体,尤其是在限时优惠的情况下。




    技术发展/停滞与机构数字素养


    如果你对日本感兴趣,你可能对现代与过时技术之间的鲜明对比有所了解。在许多地方,先进的技术与完全过时的技术并存。作为世界机器人领导者之一的国家,在台场人工岛上放置了一座真人大小的高达雕像,却仍然依赖软盘和传真机,面对2022年Windows资源管理器关闭时感到恐慌。




    在德国,前总理安格拉·默克尔在2013年称互联网为“未知领域”后,遭到全国范围的嘲笑。然而,这在2018年被前网络安全部长樱田义孝轻易地超越,他声称自己从未使用过电脑,并且在议会被问及USB驱动器的概念时,他被引述为“困惑不解”(来源)。


    对于那些尚未有机会窥探幕后幻象的人来说,这可能听起来很奇怪,但日本在技术素养方面严重落后于更新计划。因此,可以推断这些问题也在阻碍日本网站设计的发展。而具体来说,日本的网页设计正面临着这一挑战——只需在谷歌或Pinterest上搜索日本海报设计,就能看到一个非常不同和现代化的平面设计水平。




    文化影响


    在分析任何设计选择时,不应低估文化习俗、倾向、偏见和偏好的影响。然而,“这是文化”的说法可能过于简单化,并被用作为各种差异辩解的借口。而且,摆脱自己的观点偏见是困难的,甚至可能无法完全实现。


    因此,从我们的角度来看,看这个网站很容易..




    感觉不知所措,认为设计糟糕,然后就此打住。因为谁会使用这个混乱不堪的网站呢?


    这就是因为无知而导致有趣的见解被忽视的地方。现在,我没有资格告诉你日本文化如何影响了这种设计。然而,我很幸运能够从与日本本土人士的交谈中获得启发,以及在日本工作和生活的经验。


    与这个分析相关的一次对话实际上不是关于网站,而是关于YouTube的缩略图 - 有时候它们也同样令人不知所措。




    对于习惯了许多西方频道所采用的极简和时尚设计——只有一个标题、重复的色彩搭配和有限的字体——上面的缩略图确实有些难以接受。然而,当我询问一个日本本土人士为什么许多极受欢迎频道的缩略图都是这样设计时,他对这种设计被视为令人困惑的想法感到惊讶。他认为日本的设计方法使视频看起来更加引人入胜,提供了一些信息碎片,从而使我们更容易做出是否有趣的明智决策。相比之下,我给他看的英文视频缩略图在他看来非常模糊和无聊。


    也许正是这种寻求信息的态度导致了我们的观念如此不同。在日本,对风险的回避、反复核对和对迅速做出决策的犹豫明显高于西方国家。这与更加集体主义的社会心态紧密相连——例如,在将文件发送给商业伙伴之前进行两次(或三次)检查可能需要更长时间,但错误的风险显著降低,从而避免了任何参与者丢面子的情况发生。


    尽管有人认为这只适用于足够高的赌注,而迷惑外国游客似乎不符合条件——搜索一下“Engrish”这个词,然后感谢我吧。


    回到网站设计,这种文化角度有助于解释为什么在线购物、新闻和政府网站在外部观察者看来常常是“最糟糕的罪犯”。毕竟,这些正是需要大量细节直接对应于做出良好购买决策、高效地保持最新信息或确保你拥有某个特定程序的所有必要信息的情况。


    有趣的是,关于美国人和中国/日本人如何感知信息,也有相当多的研究。几项研究的结果似乎表明,例如,日本人更加整体地感知信息,而美国人倾向于选择一个焦点来引导他们的注意力(来源)。这可能给我们提供了另一个线索,解释为什么即使在日语能力较高的情况下,西方人对这类网站也感到困难。


    后但并非最不重要的是,必须说的是,网站并不是在一个在线真空中存在。而且,各种媒体,从小册子或杂志到地铁广告,也使用了尽可能多地压缩信息的布局,人们可能已经习惯了这种无处不在的方式,以至于没有人曾经想过质疑它。


    长话短说,这并不是为了找到标题问题的绝对答案,也不是为了加强日本人独特性的观点,就像日本人论一样。相反,尤其是在看到了几次关注一个解释为“真正答案”的讨论之后,我想展示科技、历史和文化影响的广度,这些最终塑造了这种差异。


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

    uCharts 小程序地图下钻功能

    web
    uCharts 小程序地图下钻功能 最近在研究小程序图表,其中提到了一个地图下钻的功能,感觉挺有意思的,分享一下共同学习! 项目简介 这个Uni-App项目旨在提供一个可交互的地图,允许用户在中国地图的不同层级之间自由切换。用户可以从国家地图开始,然后深入到各...
    继续阅读 »

    uCharts 小程序地图下钻功能


    最近在研究小程序图表,其中提到了一个地图下钻的功能,感觉挺有意思的,分享一下共同学习!


    项目简介


    这个Uni-App项目旨在提供一个可交互的地图,允许用户在中国地图的不同层级之间自由切换。用户可以从国家地图开始,然后深入到各省份地图,最终进入城市地图,点击不同区域/县级市查看详细信息。


    下面是最终效果图👇👇


    1695175245503.png


    文档地址



    准备工作


    在开始之前,请确保你已经安装了Vue.js和Uni-App


    并且准备好了模拟地图数据。这些数据将用于绘制地图。


    地图数据遵循geoJson地图数据交换格式。如果你不熟悉geoJson,可以参考这里


    绘制中国地图


    // 首先引入我们的mock数据
    import mockData from '../../mock/index'

    // onLoad中调用 drawChina 方法来绘制中国地图
    drawChina() {
    uni.setNavigationBarTitle({
    title: '中国地图'
    });
    setTimeout(() => {
    let series = mockData.china.features;
    // 这里循环一下series,把需要的数据增加到serie的属性中,fillOpacity是根据数据来显示的颜色层级透明度
    for (var i = 0; i < series.length; i++) {
    // 这里模拟了随机数据,实际开发中请根据实际情况修改
    series[i].value = Math.floor(Math.random() * 1000)
    series[i].fillOpacity = series[i].value / 1000
    series[i].color = "#0D9FD8"
    }
    // 这里把series赋值给chartData,这样就可以在页面中渲染出来了
    this.chartData = {
    series: series
    };
    }, 100);
    }

    uCharts组件使用


    插件导入后在uni_modules中,命名规则符合easyCom,可以直接在页面中使用


    <qiun-data-charts
    type="map"
    canvas2d=""
    :chartData="chartData"
    :opts="opts"
    :inScrollView="true"
    :pageScrollTop="pageScrollTop"
    tooltipFormat="mapFormat"
    @getIndex="getIndex"
    @complete="complete"
    />

    注释说明:



    • chartData 包含地图数据

    • opts 是我们在 data 中定义的配置项

    • tooltipFormat 类型为字符串,需要指定为 config-ucharts.jsconfig-echarts.js 中 formatter 下的属性值,
      这里我们使用了 mapFormat,可以在 config-ucharts.js 中查看

    • 在页面中必须传入 pageScrollTop,并将 inScrollView 设置为 true,否则可能导致某些地图事件无法触发


    事件说明:



    • @complete 事件是地图绘制完成后触发的事件,我们可以在这个事件中获取地图的实例,
      然后可以调用地图的方法进行进一步操作。

    • @getIndex 事件是地图点击事件,我们可以获取到点击的地图信息,
      根据这个信息来判断是否需要进行下钻操作,如果需要下钻,可以替换 chartData 并重新绘制地图。


    下钻操作


      // 点击地图获取点击的索引
    getIndex(e) {
    console.log('点击地图', e);
    if (e.currentIndex > -1) {
    switch (this.layout) {
    case 'china':
    this.layout = 'province';
    break;
    case 'province':
    this.layout = 'city';
    break;
    case 'city':
    this.layout = 'area';
    break;
    default:
    uni.showModal({
    title: '提示',
    content: '当前已经是最后一级地图,点击空白回到中国地图',
    success: () => {

    }
    });
    break;
    }

    this.drawNext(e.currentIndex);
    } else {
    this.layout = 'china';
    this.drawChina();
    }
    }

    以上代码中,我们通过 currentIndex 来判断当前点击的是哪个地图,然后根据 layout 的值来判断是否需要进行下钻操作。
    如果需要下钻,我们就调用 drawNext 方法来绘制下一级地图。


    这个demo中,我们只模拟了中国地图、省级地图、市级地图和区县级地图,如果在开发中我们需要根据adcode请求后端接口来获取地图数据


    具体代码:git仓库地址


    作者:养乐多多多
    来源:juejin.cn/post/7278945628905226275
    收起阅读 »

    某律师执业诚信信息公示平台字体加密解决思路

    web
    本文章只做技术探讨,请勿用于非法用途。 目标网站 为持续加深对律法的学习, 我们需要再来收集一些数据。 本文来解决 credit.acla.org.cn/ 这个网站的字体加密问题。 网站分析 网站反爬 这个网站的各种反爬措施还挺多的, 接口加密啊, 验证码啊...
    继续阅读 »

    本文章只做技术探讨,请勿用于非法用途。



    目标网站


    为持续加深对律法的学习, 我们需要再来收集一些数据。


    本文来解决 credit.acla.org.cn/ 这个网站的字体加密问题。


    网站分析


    网站反爬


    这个网站的各种反爬措施还挺多的, 接口加密啊, 验证码啊(每个页面都有), 无限 debugger 啊, 什么的, 还是挺烦的, 如果不要求效率的话可以考虑用 selenium 来过掉, 这里重点来解决一下字体加密的问题。


    加密分析


    首先来看下字体加密什么样子。


    image.png


    如图, 为律所详情页的截图, 可以看到啊, 这个 标签下的字体为加密字体, 这个网站他大多数数据信息都会像这样来做一个加密。


    开整


    首先来说下解决的方法。




    1. 找到字体文件。




    2. 确定文件字体与网站字体的映射关系。




    3. 替换网站字体。




    字体文件获取


    image.png


    刷新页面, 勾选字体栏即可看到返回的页面, 直接下载下来即可。



    有些网站可能会返回多个字体文件来迷惑你, 这时候可以全局搜索 ttf 等字体文件的关键词, 来读相关代码来找到前端页面解密时用的具体是字体文件。



    字体文件解析


    字体文件处理可以用 TTFont 工具, 我们先将文件解析成可读的 xml 格式来看下这到底是什么个东西。


    image.png


    下载字体文件保存为本地 font.ttf, 然后解析为 font.xml。


    image.png


    可以看到文件里是一些映射关系, 和一些字形的信息, 如果是简单的数字加密或是很少的字体加密的话, 这一步直接拿到映射关系就可以用了, 但是这个网站他每次的字体文件都不一样, 所以这种简单的映射关系不可用。


    image.png


    在字体编辑软件里也可以看到是对哪些字体进行了修改加密, windows 可以用 font creator , mac 上我用的是 FontForge 来解析的。


    映射关系获取


    上一步我们拿到了字形信息, 这里来生成提供一个通用的方法来做映射关系。



    font.ttf 文件中通过 unicode 码来标记对应的字体的字形信息, 我们也可以用同样的方式, 获取加密字体对应的原字体的字形信息(固定不变的), 以此为 key 来设计映射关系。



    image.png


    这里定义了一个全局变量 font_map 来存储映射关系, 通过 PIL 的 ImageFont 对象来将字体的字形信息复现出来, 然后通过 ocr 技术得到字的原型, 完成解密。将解密过的字存入 font_map, 随着收录的字越来越多, 解析效率会越来越高。


    加密字体替换


    image.png


    这里没什么难度, 做一个简单的替换就好。


    结语


    这个也是我首次接触这种麻烦些的字体加密, 就想写出来权当分享, 思路也是借鉴于之前看到的一个帖子(找不见了。。)。 文中有些东西需要自己去调试后可能会理解更深些, 因为写的过程中被其他事情打断了几次, 之前整理的思路乱掉了, 写的可能不太顺畅, 大家哪里不懂的话可以留言讨论吧, 或者有什么更好的思路也欢迎来交流。


    作者:Glommer
    来源:juejin.cn/post/7272399042091909131
    收起阅读 »

    各位微信小程序的开发者们,你们还好吗?

    web
    前言 最近微信小程序隐私指引这波骚操作实在是太好玩了,剧情跌宕起伏,让人不由得直呼周星驰在《唐伯虎点秋香》里的那句名言“人生大起大落得太快,实在是太刺激了”。 我自己因为有几个小程序需要做适配,所以全程体验了整个剧情,到今天感觉这过程实在是有点离谱,所以决定...
    继续阅读 »

    前言


    最近微信小程序隐私指引这波骚操作实在是太好玩了,剧情跌宕起伏,让人不由得直呼周星驰在《唐伯虎点秋香》里的那句名言“人生大起大落得太快,实在是太刺激了”。


    3583c74475ac4036932cf1421e1abafd.jpeg


    我自己因为有几个小程序需要做适配,所以全程体验了整个剧情,到今天感觉这过程实在是有点离谱,所以决定记录一下。以飨读者。


    起因


    让我们把时钟回拨到一个月前的8月14日,微信默默的发了一个足以影响所有小程序的公告 《关于小程序隐私保护指引设置的公告》。大概意思就是对于某些隐私接口,如相册,地理位置啥的,要给用户弹个窗,需要用户点同意以后才能调用。公告内容很多,总结起来有两个重点:



    1. 这个弹窗要你们自己做哟。

    2. 9月15号生效呦,在此之前要是没做弹窗,你的小程序就死定了呦。


    然后小伙伴们不敢怠慢,纷纷开始研究怎么个改法,社区论坛里乱成了一锅粥。


    有看文档看不懂的:


    Screen Shot 2023-09-15 at 12.46.41 AM.png


    有没看到公告早上上班突然发现接口都出错一脸懵的:


    6a5f760e492e69d02606d9339c33ea8a.jpeg


    Screen Shot 2023-09-15 at 12.56.09 AM.png


    还有先知先觉的:


    Screen Shot 2023-09-15 at 1.05.15 AM.png


    骂街的我就不贴了,以防不能过审。


    发展


    由于推出的过于仓促,阻碍了开发调试,引起开发者不满,官方很快又默默回滚了此次更新。并且鉴于大家都看不懂文档,也不知道怎么改代码。在小伙伴们的强烈呼吁下


    Screen Shot 2023-09-15 at 1.17.53 AM.png
    官方答应尽快推出demo。


    终于在8天之后的8月22日,距离9月15日大限还有24天的时候,大家翘首以盼的官方demo终于出现了。而且不是1个,是3个(后来又多了1个,一共4个)。这下一脸懵变成三(四)脸懵了。


    3d769c68d1e39eb2cfc670d8f47af595.jpeg


    你让我用哪个???


    demo都有了,那就开干吧,还有二十几天,来得及。于是天真的小伙伴们开始高ma高ma兴lie兴lie的写bug。这次改动不仅仅涉及代码层面,还需要后台配置,前台配置,更新缓存,uni/taro框架,基础库版本,第三方开发等等各种问题,大家也是磕磕绊绊的慢慢的都摸清楚了该怎么做。


    由于此次改动涉及到所有的小程序,影响面太大,随着9月15号大限的临近,更多的问题渐渐浮出水面。


    首先就是那些维护着几十上百个小程序的小朋友,改动看似不多,但每一个都要走开发测试发版流程,而且必须在这短短的不到一个月的时间内完成,听起来就头大有木有?改不完,根本改不完,加班加点不睡觉也改不完。


    然后就是那些在线上跑了好几年没更新的小程序,有的是源代码都找不到了,有的原来的开发者早就提桶跑路了,直接欲哭无泪。


    还有后知后觉的,剩下几天就到大限的时候来社区里问这个隐私协议是个啥?这么重要的事情平台为什么没有早点通知我???


    最后,就是审核变的奇慢无比,原来几个小时就有结果,而现在有小程序审核了好几天了还是没有反应。这其实也是可以预见的,这种影响所有小程序的改动必然会导致版本发布量大增,微信这是人为的自己给自己制造了个审核DDOS,但后果都是开发者承担。就问你急不急吧


    Screen Shot 2023-09-15 at 2.20.05 AM.png
    就剩几天的时候,有小伙伴被逼的没办法了,不得不使用了一年一次平时打死都不敢用的加急审核。


    当时间来到今天,也就是9.15大限前最后一天的时候,上了车的暗自庆幸,没上车的放弃治疗,尘埃即将落定,大家各安天命的时候,意想不到的转折又来了。。。


    高潮


    9月14号晚上20点38分,也就是距离9月15号0点还有3个多小时的时候,微信官方又发布了一条公告:


    Screen Shot 2023-09-15 at 2.49.07 AM.png
    总结下来就两点:



    1. 原来说大限是还剩3个小时的9月15号,新的大限推迟到10月17号。

    2. 代码不改也没关系,平台自己会弹窗。


    啊???????????????


    啥???????????????


    就剩3个小时了你给我来个180度大转弯???????????


    不瞒你们说,我晚上看到这个公告的开始的表情是:


    96282a4cc8aa3522a78448bd660be56a.jpeg
    然后是


    210235eea23d7fcc4867ae9bbccb3df9.jpeg


    这下原本上了车的直接被180度逮虾户给甩出去了,合着我吭哧吭哧费劲巴拉的折腾了一个月眼看就剩3个小时了你告诉我都是白干???你平台为什么不一开始就把方案定成你们自己处理,非要折磨我们开发者???我加班都是白加了?我的用掉的加急审核次数能不能退给我??我怎么觉得有一种被人耍了的感觉?


    离最后期限3个小时啊,不是3周,也不是3天,是3个小时啊。你们官方人员业余都是玩赛车的还是兼职开渣土车的啊,平时上下班开车是不是都是漂移过弯啊。


    未完待续


    这事就这么完了吗? no no no,我觉得还没完,接下来这几天社区里估计又要炸锅了,大家喜欢看热闹的没事可以去围观一下。这不又延期了一个月吗?再出什么幺蛾子我是一点都不会感到意外了。


    最后,我觉得这件事印证了我之前听过的一句话,“这个世界就是个巨大的草台班子”。


    最后的最后,拿社区里最经典的一张动图镇楼


    0-2.gif


    作者:ad6623
    来源:juejin.cn/post/7278517841884266536
    收起阅读 »

    环信web、uniapp、微信小程序sdk报错详解---登录篇

    项目场景:记录对接环信sdk时遇到的一系列问题,总结一下避免大家再次踩坑。这里主要针对于web、uniapp、微信小程序在对接环信sdk时遇到的问题。主要针对报错400、404、401、40 (一)登录用户报400原因分析:从console控制台输出...
    继续阅读 »

    项目场景:
    记录对接环信sdk时遇到的一系列问题,总结一下避免大家再次踩坑。这里主要针对于web、uniapp、微信小程序在对接环信sdk时遇到的问题。主要针对报错400、404、401、40

     (一)
    登录用户报400



    原因分析:
    从console控制台输出及network请求返回入手分析
    可以看到报错描述invalid password,密码无效,这个时候就需要去排查一下该用户密码填写是否正确


    排查思路:
    因为环信不保存用户的密码,可以在console后台或者调用修改密码的restapi来修改一下密码再重新登录(修改密码目前只有这两种方式)



    (二)
    登录用户报404




    原因分析:
    从console控制台输出及network请求返回入手分析
    可以看到报错描述user not found,这个时候就需要去排查一下该用户是否存在于该项目使用的appkey下了

    排查思路:
    可以看一下console后台拥有这个用户的appkey和自己项目初始化时用的是否是同一个,若在console后台并没有查到该用户,就要注意这个用户是否真的没有注册




    (三)
    登录用户报40、401




    原因分析:
    报错40或者401一般都是token的问题,需要排查一下token是否还在有效期,token是否是当前用户的用户token
    40的报错还有一种情况,用户名密码登录需要排查用户名及密码传参是否都是string类型


    注:此处需要注意用户token和apptoken两种概念
    用户token指的是该用户的token,一般只用于该用户在客户端使用环信 token 登录和鉴权
    app token指的是管理员权限 token,发送 HTTP 请求时需要携带 app token
    token较为私密,一般不要暴露出去

    排查思路:
    排查用户名及密码传参是否都是string类型,这个可以直接将option传参打印出来取一下数据类型看看是否是string
    关于token排查,现在没有合适的办法直接查询token是否还在有效期或者是不是当前用户的token,只能通过api调用看是否报错401,可以在console后台直接获取新的用户token来测试一下


    是不是当前用户的token也可以找环信的技术支持帮忙查,但在不在有效期他们也查不了


    话外
    有人遇到为什么已经open成功了但是还会报错

    这里要注意open只能证明获取到了token,证明不了已经建立了websocket连接,只有触发onOpened或者onConnected回调 只有onOpened或者onConnected回调触发,才算真正与环信建立连接。所以也不能在open返回的success或者.then中做任何逻辑处理,此外还要注意监听回调一定要放在调用api之前,在调用任何一个api时都要保证监听挂载完毕,包括open


    如何判断自己是否在登录状态

    可以用以下三种方法中的一种判断当前用户是否在登录状态~
    1、WebIM.conn方法下有一个logOut字段,该字段为true时表明未登录状态,该字段为false时表明登录;
    2、WebIM.conn.isOpened () 方法有三个状态,undefined为未登录状态,true为已登录状态,false为未登录状态,可以根据这三个状态去判断是否登录;
    3、通过onOpened 这个回调来判断,只要执行了就说明登录成功了,输出的话,输出的是undefined
    三者选其一判断登录状态

    收起阅读 »

    中秋节,我只想回家😭

    web
    前言 中秋节马上就要到啦,中秋节作为刻入我们DNA里面的团圆佳节,我想大家关心的肯定是团圆了吧 对于普通人回家跟家人团圆可能也就是一张车票而已 但是今年~我就想知道28 29号的票都被谁买去了,我现在连30号的都没买到!!! 这让我怎么团圆啊!!! 忆中秋 还...
    继续阅读 »

    前言


    中秋节马上就要到啦,中秋节作为刻入我们DNA里面的团圆佳节,我想大家关心的肯定是团圆了吧


    对于普通人回家跟家人团圆可能也就是一张车票而已


    但是今年~我就想知道28 29号的票都被谁买去了,我现在连30号的都没买到!!!


    这让我怎么团圆啊!!!


    忆中秋


    还记得小时候过中秋,就跟过年一样。记忆中的中秋,各家各户外出的人都会从天南地北赶回来,一家人团聚在一起;记忆中的月亮,又圆又亮,坐在门前的小院子里,一起吃着月饼赏着月,已经很开心了。


    小时候
    听着嫦娥的故事
    心里却惦记着月饼

    长大了
    手里捧着月饼
    心里却想着嫦娥

    中秋节到了
    愿你重拾童年的快乐
    点缀幸福的生活

    程序员怎么过中秋呢?


    当然是以代码来庆祝一下中秋啦,正好还可以参加下中秋创意大赛!但是很难有一些新奇的创意了,那就做一个猜灯谜的小游戏供大家消遣吧哈哈哈。话不多说,开始今天的主题,制作灯谜小游戏,码上掘金会有源码哦,很基础的~


    1、游戏简介



    • 玩家在提交答案后,游戏将根据玩家的回答情况给予相应的提示信息。如果答案正确,将显示回答正确的提示,并增加相应的得分;如果答案错误,将显示回答错误的提示,并扣除相应的分数。同时,游戏会记录玩家的最高分数,以便玩家挑战自己的最好成绩。

    • 玩家可以选择继续猜下一道题目,直到回答完所有题目或不再继续。游戏结束后,将显示玩家的得分和最高分数,并提供重新开始和退出游戏的选项。


    2、游戏规则



    游戏包括多道灯谜题目,每个题目都有一个对应的答案。


    玩家需要在输入框中输入自己的答案,并点击提交按钮进行确认。


    如果答案正确,将显示相应的提示信息,表示回答正确;如果答案错误,将显示错误提示信息并扣除相应分数。


    游戏根据玩家的回答情况给予评分,并记录最高分数。


    玩家可以选择重置游戏重新开始,或者退出游戏。



    3、游戏设计



    • 定义题目和答案数组,每个元素包含一个题目和对应的答案。

    • 初始化游戏数据,包括当前题目索引、得分和最高分数。

    • 显示当前题目,将题目显示在页面上供用户查看。

    • 用户输入答案后,点击提交按钮。

    • 检查用户答案是否正确,如果正确则增加得分,显示回答正确的提示;如果错误则显示回答错误的提示。

    • 更新最高分数,如果当前得分超过最高分数,则更新最高分数。

    • 显示当前得分和最高分数。

    • 清空输入框,准备接受下一题答案。

    • 判断是否回答完所有题目,若回答完所有题目则显示游戏结束的提示信息,并禁用提交按钮;若未完成则显示下一题。

    • 提供重新开始游戏的功能,重置游戏数据并重新显示第一题。

    • 提供退出游戏的功能,显示退出游戏的提示信息。


    4、功能实现


    题目和答案的存储



    题目和答案的存储可以使用数组来实现,每个元素表示一道题目和对应的答案。例如:



    // 定义题目和答案
    const questions = [

    { question: '中秋佳节结良缘 (打一城市名)', answer: '重庆' },
    { question: '中秋鼓励消费 (打一成语)', answer: '月下花前' },
    { question: '中秋遥知兄弟赏光处 (打一唐诗目)', answer: '望月怀远' },
    { question: '木兰迷恋中秋夜 (打一成语)', answer: '花好月圆' },
    { question: '中秋渡蜜月 (打一成语)', answer: '喜出望外' }
    ];

    每个元素都是一个对象,包含两个属性:question表示题目,answer表示答案。可以根据实际需要修改题目和答案的内容和数量。


    5、游戏展示


    话不多说直接上效果 !


    作者:优秀稳妥的Zn
    来源:juejin.cn/post/7280747221510733878
    收起阅读 »

    前端监控究竟有多重要?

    web
    为什么要有前端监控? 一个很现实的原因是bug是不可能被全部测试出来的,由于成本和上线档期的考虑,测试无法做到“面面俱到”,即使时间充裕也总会有这样或那样的bug埋藏在某个角落。 所以一个可靠的前端监控系统可以帮助我们化被动为主动,不再被动的等待客服来找,而是...
    继续阅读 »

    为什么要有前端监控?


    一个很现实的原因是bug是不可能被全部测试出来的,由于成本和上线档期的考虑,测试无法做到“面面俱到”,即使时间充裕也总会有这样或那样的bug埋藏在某个角落。


    所以一个可靠的前端监控系统可以帮助我们化被动为主动,不再被动的等待客服来找,而是在问题出现时开发人员可以第一时间知道并解决。并且我们还可以通过监控系统获取用户行为以及跟踪产品在用户端的使用情况,并以监控数据为基础,指明产品优化的方向。


    常见的前端监控


    前端监控系统大体可以分为四部分



    • 异常监控

    • 用户数据监控

    • 性能监控

    • 异常报警


    用户数据监控



    数据监控,就是监听用户的行为,可以帮助我们评估和改进用户在使用网站时的体验:




    • PV:PV(page view):即用户访问特定页面的次数,也可以说是页面的浏览量或点击量,

    • UV:访问网站的不同个体或设备数量,而不是页面访问次数

    • 新独立访客:当日的独立访客中,历史上首次访问网站的访客为新独立访客。

    • 跳出次数:跳出指仅浏览了1个页面就离开网站的访问(会话)行为。跳出次数越多则访客对网站兴趣越低或站内入口质量越差。

    • 来访次数:由该来源进入网站的访问(会话)次数。

    • 用户在每一个页面的停留时间

    • 用户通过什么入口来访问该网页

    • 用户在相应的页面中触发的行为

    • 网站的转化率

    • 导航路径分析


    统计这些数据是有意义的,我们可以清晰展示前端性能的表现,并依据这些监控结果来进一步优化前端性能。例如,我们可以改善动画效果以在低版本浏览器上兼容,或者采取措施加快首屏加载时间等。这些优化措施不仅可以提高转化率,因为快速加载的网站通常具有更高的转化率,还可以确保我们的网站在多种设备和浏览器上都表现一致,以满足不同用户的需求。最终达到,改善用户体验,提供更快的页面加载时间和更高的性能,增强用户满意度,降低跳出率的目的。


    性能监控



    性能监控是一种用于追踪和评估网站和性能的方法。它专注于用户在浏览器中与网站互时的性能体验




    • 首次绘制(FP): 全称 First Paint,标记浏览器渲染任何在视觉上不同于导航前屏幕内容之内容的时间点

    • 首次内容绘制(FCP):全称 First Contentful Paint,标记的是浏览器渲染来自 DOM 第一位内容的时间点,该内容可能是文本、图像、SVG 甚至 <canvas> 元素。

    • 首次有效绘制(FMP):全称 First Meaningful Paint,标记的是页面主要内容绘制的时间点,例如视频应用的视频组件、天气应用的天气信息、新闻应用中的新闻条目。

    • 最大内容绘制(LCP):全称 Largest Contentful Paint,标记在可视区“内容”最大的可见元素开始绘制在屏幕上的时间点。

    • 白屏时间

    • http 等请求的响应时间

    • 静态资源整体下载时间

    • 页面渲染时间

    • 页面交互动画完成时间


    异常监控



    由于产品的前端代码在客户端的执行过程中也会发生异常,因此需要引入异常监控。及时的上报异常情况,这样可以避免线上故障的发生。虽然大部分异常可以通过 try catch 的方式捕获,但是比如内存泄漏以及其他偶现的异常难以捕获。



    常见的需要监控的异常包括:



    • Javascript 的异常监控:捕获并报告JavaScript代码中的错误,如未定义的变量、空指针引用、语法错误等

    • 数据请求异常监控:监控Ajax请求和其他网络请求,以便识别网络问题、服务器错误和超时等。

    • 资源加载错误:捕获CSS、JavaScript、图像和其他资源加载失败的情况,以减少页面加载问题。

    • 跨域问题:识别跨域请求导致的问题,如CORS(跨源资源共享)错误。

    • 用户界面问题:监控用户界面交互时的错误,如用户界面组件的不正常行为或交互问题


    通过捕获和报告异常,开发团队可以快速响应问题,提供更好的用户体验,减少客户端问题对业务的不利影响


    异常报警



    前端异常报警是指在网站中检测和捕获异常、错误以及问题,并通过各种通知方式通知开发人员或团队,以便他们能够快速诊断、分析和解决问题。



    常见的异常报警方式




    • 邮件通知:通过邮件将异常信息发送给相关人员,通常用于低优先级的问题。




    • 短信或电话通知:通过短信或电话自动通知相关人员,通常用于紧急问题或需要立即处理的问题。




    • 即时消息:使用即时通讯工具如企业微信 飞书或钉钉发送异常通知,以便团队及时协作。




    • 日志和事件记录:将异常信息记录到中央日志,或者监控中台系统,以供后续分析和审计。




    报警级别和策略:


    异常报警通常有不同的级别和策略,根据问题的紧急性和重要性来确定通知的方式和频率。例如,可以定义以下报警级别:




    • 紧急报警:用于严重的问题,需要立即处理,通常通过短信或电话通知。




    • 警告报警:用于中等级别的问题,需要在短时间内处理,可以通过即时消息或邮件通知。




    • 信息报警:用于一般信息和低优先级问题,通过邮件或即时消息通知。




    • 静默报警:用于临时性问题或不需要立即处理的问题,可以记录到日志而不发送通知。




    异常报警是确保系统稳定性和可用性的重要机制。它能够帮助组织及时发现和解决问题,减少停机时间,提高系统的可靠性和性能,从而支持业务运营。异常报警有助于快速识别和响应问题,减少停机时间,提高系统的可用性和性能


    介绍完了前端监控的四大部分,现在就来聊聊前端监控常见的几种监控方式。


    SDK设计(埋点方案)


    前端埋点是一种用于收集和监控网站数据的常见方法


    image.png


    手动埋点:


    手动埋点也称为代码埋点,是通过手动在代码中插入埋点代码(SDK 的函数)的方式来实现数据收集。像腾讯分析(Tencent Analytics)、百度统计(Baidu Tongji)、诸葛IO(ZhugeIO)等第三方数据统计服务商大都采用这种方案,这种方法的优点是:



    • 灵活:开发人员可以根据需要自定义属性和事件,以捕获特定的用户行为和数据。

    • 精确:可以精确控制埋点位置,以确保收集到关键数据。


    然而,手动埋点的缺点包括:



    • 工作量大:需要在代码中多次插入埋点代码,工程量较大。

    • 沟通成本高:需要开发、产品和运营之间的频繁沟通,容易导致误差和延迟。

    • 更新迭代成本高:每次有埋点更新或漏埋点都需要重新发布应用程序,成本较高。


    可视化埋点:


    可视化埋点通过提供可视化界面,允许用户在不编写代码的情况下进行添加埋点。这种方法的优点是:



    • 简单方便:非技术人员也可以使用可视化工具添加埋点,减少了对技术团队的依赖。

    • 实时更新:可以实时更新埋点配置,无需重新上传网站。


    然而,可视化埋点的缺点包括:



    • 可定制性受限:可视化工具通常只支持有限的埋点事件和属性,无法满足所有需求。

    • 对控件有限制:可视化埋点通常只适用于特定的UI控件和事件类型。


    无埋点:


    无埋点是一种自动收集所有用户行为和事件的方法,然后通过后端过滤和分析以提取有用的数据。这种方法的优点是:



    • 全自动:无需手动埋点,数据自动收集,降低了工程量,而且不会出现漏埋和误埋等现象。

    • 全面性:捕获了所有用户行为,提供了完整的数据集。


    然而,无埋点的缺点包括:



    • 数据量大:数据量庞大,需要后端过滤和处理,可能增加服务器性能压力。

    • 数据处理复杂:需要处理大量原始数据,提取有用的信息可能需要复杂的算法和逻辑。


    作者:zayyo
    来源:juejin.cn/post/7280430881964638262
    收起阅读 »

    闲来无事,拜拜电子财神

    web
    最近在刷抖音的时候,经常能刷到类似下面这种手机桌面,通过手机小组件功能,搭了一个电子供台。。。    由于最近闲来无事儿,就在想可不可以制作一个类似的网页,功能点有以下这些: 1.类似手机小组件一样的布局 2.点击木鱼一次,可以显示功德加一并且带音效 3.随着...
    继续阅读 »

    最近在刷抖音的时候,经常能刷到类似下面这种手机桌面,通过手机小组件功能,搭了一个电子供台。。。


      


    由于最近闲来无事儿,就在想可不可以制作一个类似的网页,功能点有以下这些:


    1.类似手机小组件一样的布局


    2.点击木鱼一次,可以显示功德加一并且带音效


    3.随着功德点击,香炉上方会有烟雾飘散的效果


    4.统计不同省份的功德数据


    5.心愿墙功能,


    于是说干就干,就开始了开发工作;


    经过了 2 个下午的忙碌,完成了前三个功能,有了大概的雏形,就是下面这个样子



    开发的过程中也遇到了一些问题


    1.在手机上连续点击木鱼时,会导致网页放大


    在网上找了一些解决办法,设置 meta 属性


    无效,在 ios 的浏览器上没有效果


    这个方法类似于写个节流函数,不过这样做就没有连续敲击木鱼的快感了,所以也不行。


    最后让我找到了一个插件 fastClick.js,完美解决了问题。只要正常引入,然后加入以下代码即可。


    if ("addEventListener" in document) {            document.addEventListener(                "DOMContentLoaded",                function () {                    FastClick.attach(document.body);                },                false            );        }

    2.播放木鱼音效延迟问题


    通过document.createElement('audio')方式创建 audio 组件,代码如下


    var audio = document.createElement('audio') //生成一个audio元素
    audio.controls = true //这样控件才能显示出来
    audio.src = 'xxxxx' //音乐的路径
    document.body.appendChild(audio) //把它添加到页面中
    audio.play()

    声音是能播放出来了,但是延迟很高,点一下木鱼,过几秒钟后才有音效,所以这个方式 pass 了。还有说可以通过AudioContext API 来播放音效,但是看了一下,感觉写起来有些复杂,也 pass 掉了,最后也是找到了一款合适的插件解决了这个问题。



    使用方式也是异常简单


    var sound = new Howl({
    src: ['sound.mp3']
    });

    sound.play();

    由于有个功能是敲击木鱼后,页面香炉的位置会生成烟雾,自己不太会写,于是又找到了可以一个模拟烟雾的插件,可以在页面任意位置生成烟雾动画


    使用时先创建一个 canvas 标签


    <canvas id="smoke"></canvas>

    然后初始化


    let canvas = document.getElementById("smoke");let ctx = canvas.getContext("2d");canvas.width = window.innerWidth;canvas.height = window.innerHeight;party = SmokeMachine(ctx, [230, 230, 230]); // 数组里是颜色 rgb 值

    点击木鱼一次,创建一次播放动画


    party.start();party.addSmoke(    window.innerWidth / 2,    //烟雾生成的位置,x    window.innerHeight * 0.4, //烟雾生成的位置,y    10 //烟雾大小);

    至此烟雾效果就完美实现了。


    体验url:财神爷.我爱你


    没错,是纯中文域名,中国的神仙就要用中文域名。


    未完待续......


    作者:yibeicha
    来源:juejin.cn/post/7280435142245285946
    收起阅读 »

    前端又出新框架了,你还学得动吗?

    web
    最近前端又出来一个新框架/库,名为nue.js。一周前的9.13号提交了第一个commit,到今天已超过2000个star。 翻译一下: Nue 是一个强大的 React、Vue、Next.js、Vite 和 Astro 替代品。它可能会改变您的web开发...
    继续阅读 »

    最近前端又出来一个新框架/库,名为nue.js。一周前的9.13号提交了第一个commit,到今天已超过2000个star。


    官网首页截图


    翻译一下:



    Nue 是一个强大的 React、Vue、Next.js、Vite 和 Astro 替代品。它可能会改变您的web开发方式。



    What is Nue JS?


    Nue JS 是一个非常小的(压缩后 2.3kb)JavaScript 库,用于构建 Web 界面。 它是即将推出的 Nue 生态系统的核心。 它就像 Vue.js、React.js 或 Svelte,但没有hooks, effects, props, portals, watchers, provides, injects, suspension 这些抽象概念。了解 HTML、CSS 和 JavaScript 的基础知识,就可以开始了。


    用更少的代码构建用户界面


    它表示,Nue 最大的好处是你需要更少的代码来完成同样的事情:


    同样一个listBox组件,react需要2537行,vue需要1913行,svelte需要1286行,Nue只需要208行,比react小10倍。





    仅仅是HTML


    Nue 使用基于 HTML 的模板语法:


    <div @name="media-object" class="{ type }">
    <img src="{ img }">
    <aside>
    <h3>{ title }</h3>
    <p :if="desc">{ desc }</p>
    <slot/>
    </aside>
    </div>

    React 和 JSX 声称是“Just JavaScript”,但 Nue 可以被认为是“Just HTML”


    按比例构建


    Nue 具有出色扩展性的三个原因:



    1. 关注点分离,易于理解的代码比“意大利面条代码”更容易扩展

    2. 极简主义,一百行代码比一千行代码更容易扩展

    3. 人才分离,当 UX 开发人员专注于前端,而 JS/TS 开发人员专注于前端后端时,团队技能就会达到最佳平衡:



    解耦样式


    Nue不提倡使用 Scoped CSS、样式属性、Tailwind 或其他 CSS-in-JS 体操:



    1. 更多可重用代码:当样式未硬编码到组件时,同一组件可能会根据页面或上下文而看起来有所不同。

    2. 没有意大利面条式代码:纯 HTML 或纯 CSS 比混合意大利面条式代码更容易阅读

    3. 更快的页面加载:通过解耦样式,可以更轻松地从辅助 CSS 中提取主 CSS,并将 HTML 页面保持在关键的14kb 限制以下。


    反应式和同构


    Nue拥有丰富的组件模型,它允许您使用不同类型的组件创建各种应用程序:



    1. 服务器组件在服务器上呈现。它们可以帮助您构建以内容为中心的网站,无需 JavaScript 即可加载速度更快,并且可以被搜索引擎抓取。

    2. 反应式组件在客户端上呈现。它们帮助您构建动态岛或单页应用程序。

    3. 混合组件部分在服务器端呈现,部分在客户端呈现。这些组件可帮助您构建响应式、SEO 友好的组件,例如视频标签或图片库。

    4. 通用组件在服务器端和客户端上使用相同的方式。


    UI库文件


    Nue允许您在单个文件上定义多个组件。这是将相关组件组合在一起并简化依赖关系管理的好方法。


    <!-- shared variables and methods -->
    <script>
    import { someMethod } from './util.js'
    </script>

    <!-- first component -->
    <article @name="todo">
    ...
    </article>

    <!-- second component -->
    <div @name="todo-item">
    ...
    </div>

    <!-- third component -->
    <time @name="cute-date">
    ...
    </time>

    使用库文件,您的文件系统层次结构看起来更干净,并且您需要更少的样板代码将连接的部分连接在一起。他们帮助为其他人打包库。


    更简单的工具


    Nue JS带有一个简单的render服务器端渲染功能和一个compile为浏览器生成组件的功能。不需要 WebpackVite 等复杂的捆绑程序来控制您的开发环境。只需将 Nue 导入到项目中即可。


    如果应用程序因大量依赖项而变得更加复杂,可以在业务模型上使用打包器。Bunesbuild是很棒的高性能选择。


    用例


    Nue JS是一款多功能工具,支持服务器端和客户端渲染,可帮助您构建以内容为中心的网站和反应式单页应用程序。



    1. UI 库开发:为反应式前端或服务器生成的内容创建可重用组件。

    2. 渐进式增强:Nue JS 是一个完美的微型库,可通过动态组件或“岛”增强以内容为中心的网站

    3. 静态网站生成器:只需将其导入您的项目即可准备渲染。不需要捆绑器。

    4. 单页应用程序:与即将推出的Nue MVC项目一起构建更简单、更具可扩展性的应用程序。

    5. Template Nue:是一个用于生成网站和 HTML 电子邮件的通用工具。


    本文参考资料



    作者:xintianyou
    来源:juejin.cn/post/7280747833371705405
    收起阅读 »

    为什么5.225.toFixed(2)!=5.23,令人摸不着头脑的银行家舍入法

    web
    前言 很多时候,我们在程序中计算数字,得到的结果也许并不和我们想象得一样,在我们大多数人的认知里几乎都是四舍五入法,但是程序中所呈现的好像并不是我们想要的结果。 今天就谈谈程序中的那匪夷所思得银行家舍入法(也会涉及到数字精度问题) 什么是银行家舍入法 银行家舍...
    继续阅读 »

    前言


    很多时候,我们在程序中计算数字,得到的结果也许并不和我们想象得一样,在我们大多数人的认知里几乎都是四舍五入法,但是程序中所呈现的好像并不是我们想要的结果。

    今天就谈谈程序中的那匪夷所思得银行家舍入法(也会涉及到数字精度问题)


    什么是银行家舍入法


    银行家舍入法,也称为四舍六入五留双或四舍六入五成双,是一种在计算机科学和金融领域广泛使用的舍入方法。


    具体操作步骤如下:



    1. 如果被修约的数字小于5,则直接舍去;

    2. 如果被修约的数字大于5,则进行进位;

    3. 如果被修约的数字等于5,则需要查看5前面的数字。如果5前面的数字是奇数,则进位;如果5前面的数字是偶数,则舍去5,即修约后末尾数字都成为偶数。特别需要注意的是,如果5的后面还有不为0的任何数,则无论5的前面是奇数还是偶数,均应进位。


    以上可以看出银行家舍入法得规则,当为5时,并不是所有得都会向前进一位,所以就可以知道5.225.toFixed(2)为什么不等于5.23了


    举例


    在浏览器的控制台中,我们可以试着打印一下


    image.png
    这个时候我们可以看到,哎,好像是符合我们的所认知得四舍五入法了,但是紧接着


    image.png
    这里看出,怎么又变成这样的了,这还是银行家舍入法呀,为了更严谨再试一下5前面为奇数时得结果


    image.png
    这里结果又变了,反而是整数大于等于4得正常了,但是小于4得又有些失常了,反而整数为1得总是按照咱们预想的结果在进行,这种结果让我大脑一片混乱,所以这到底是什么原因,导致结果不像是银行家舍入法,也不像是四舍五入法


    在我掉了一花西币的头发后,终于想通了,是程序中的精度问题,我们所写的数字并不是表面那么纯粹,再次打印一下看看


    image.png
    现在可以清楚看出,我们所写的简单的数字后面并不见简单,之所以1.235和1.225使用toFiexd的时候都准确的四舍五入了,都是因为他的后面是多出来了0.0000000000几的数字,然而2.235就没有那么幸运了,所以2.235的0.005就被舍弃了!


    解决方法


    先说一种可行但不完全可行的解决方法,就是使用Math.round()
    首先这个方法确实是js中提供的真正含义上的四舍五入的方法。


    image.png
    哎,这么一看,确实可行,既然简单的可以,那我们就试着进行复杂运算一下,再保留一下两位小数试试看


    image.png
    呕吼,错了,按我们正常来算应该是9.77,但却得到了9.76。

    要知道程序中存在着精度问题,再我们算来这个式子的结果应该是9.765,但是在程序看来


    image.png
    可以说是无限趋近于9.765但还没有达到,然后就在Math.round这个方法中给舍弃掉了,这个方法似乎不完全可行


    那么另外一招就是可行但有隐式风险的方式,就是在我们所算出来的结果后面添加0.0000000001,这样再让我们看一下结果


    image.png
    这样可以看出,无论使用哪种方法,都能达到我们所需的结果了,即使使用toFixed有了银行家舍入法的规则,依旧可以按我们所想的一样进行四舍五入,因为当我们加了0.000000001后,即使最后一位等于5了,5后面还有数字,它就会向前进一位,那如果说加了这0.000000001正好等于5然后又触发了银行家舍入法的规则,那只能说算你倒霉,这就是我说为什么会有隐式风险,有风险但很小。


    当然还有一个方法就是自己写一个方法来解决这个问题


    //有的时候也许传的参数就是计算过后的,无线趋近于5的数,可以根据需求来判断是否传入第二个参数
    Number.prototype.myToFixed = function (n, d) {
    //进来之后转为字符串 字符串不存在精度问题
    const str = this.toString();
    const dotIndex = str.indexOf(".");
    //如果没有小数点传进来的就是整数,直接使用toFixed传出去
    if (dotIndex === -1) {
    return this.toFixed(n);
    }
    //当为小数的时候
    const intStr = str.substring(0, dotIndex);
    const decStr = str.substring(dotIndex + 1, str.length).split("");
    //当大于5时,就进一
    if (decStr[n] >= 5) {
    decStr[n - 1] = Number(decStr[n - 1]) + 1;
    const dec = decStr.slice(0, n).join("");
    return `${intStr}.${dec}`;
    } else {
    //否则小于五时 先判断是否有第二个参数
    if (d) {
    //如果有就截取到第二个参数的位置
    const newDec = decStr.splice(n, n + d);
    let nineSum = 0;
    //遍历循环有多少个9
    for (let index = 0; index < newDec.length; index++) {
    if (index != 0 && newDec[index] == 9) {
    nineSum++;
    }
    }
    //判断四舍五入后面的位置 是否为四 并且是否除了4之后全是9 或者 9的位数大于第二个传的参数
    if (newDec[0] == 4 && (nineSum >= newDec.length - 2 || nineSum >= d)) {
    //条件成立 就按5进一
    decStr[n - 1] = Number(decStr[n - 1]) + 1;
    const dec = decStr.slice(0, n).join("");
    return `${intStr}.${dec}`;
    } else {
    //不成立则舍一
    const dec = decStr.slice(0, n).join("");
    return `${intStr}.${dec}`;
    }
    } else {
    //没有第二个参数,小于五直接舍一
    const dec = decStr.slice(0, n).join("");
    return `${intStr}.${dec}`;
    }
    }
    };

    我们再进行测试一下


    image.png


    image.png
    这样就是我们想要的结果了


    总结


    在程序中,银行家舍入法和数字的精度问题很多时候都会遇见,不论前端还是后端,然而处理这些数据也是比较头疼的事,我所讲的这些也许不能满足所有情况,但大多数情况都是可以处理的。


    如果是相对于银行里这种对数字比较敏感的环境,这些参数的处理还需要更加谨慎的处理


    写的如有问题,欢迎提出建议


    作者:iceCode
    来源:juejin.cn/post/7280430881952759862
    收起阅读 »

    看完这位小哥的GitHub,我沉默了

    就在昨天,一个名为win12的开源项目一度冲上了GitHub的Trending热榜。 而且最近项目的Star量也在飙升,目前已经获得了2.2k+的Star标星。 出于好奇,点进去看了看。好家伙,项目README里写道这是一个14岁的初中生所打造的开源项目。...
    继续阅读 »

    就在昨天,一个名为win12的开源项目一度冲上了GitHub的Trending热榜。



    而且最近项目的Star量也在飙升,目前已经获得了2.2k+的Star标星。



    出于好奇,点进去看了看。好家伙,项目README里写道这是一个14岁的初中生所打造的开源项目。


    即:在网页端实现了Windows 12的UI界面和交互效果。


    这里也放几张图片感受一下。

    登录页面

    开始菜单

    资源管理器

    设置

    终端命令行

    AI Copilot

    其他应用



    这个项目的灵感来源于作者之前看到 Windows 12 概念版后深受启发,于是决定做一个Windows12网页版(就像之前的 Windows 11 网页版一样),可以让用户在网络上预先体验 Windows 12。


    可以看到,这个项目是一个前端开源项目,而且由标准前端技术(HTML,JS,CSS)来实现,下载代码,无需安装,打开desktop.html即可。



    项目包含:

    • 精美的UI设计
    • 流畅丰富的动画
    • 各种高级的功能(相较于网页版)

    不仅如此,作者团队对于该项目的后续发展还做了不少规划和畅想。


    • 项目规划


    • 项目畅想


    刚上面也说了,项目README里写道该项目的作者是一位14岁的初中生,网名叫星源,曾获得CSP普及组一等奖和蓝桥杯国赛三等奖。


    作者出生于2009年,在成都上的小学和初中,目前刚上初三。


    这样来看的话,虽说作者年龄很小,不过接触计算机和编程应该非常早,而且对计算机领域的知识和技术应该有着非常浓厚的兴趣。


    从作者的个人主页里能看到,技术栈这块涉猎得还挺广泛。



    作者自己表示如今上初三了,对于win12这个项目也不会做什么功能的更新了,后续的维护更新将交给其他贡献者成员。



    文章的结尾也附上Windows 12网页版体验地址:tjy-gitnub.github.io/win12/desktop.html,感兴趣的同学可以自行体验。


    聊到这里不得不说,人与人之间的差距确实挺大的。就像这位小哥,才14岁就已经精通前端技术了。


    而14岁的我,当年在干嘛呢?


    我想了又想。。


    额,我好像在网吧里玩红警。。(手动doge)


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

    硬盘坏了,一气之下用 js 写了个恢复程序

    web
    硬盘坏了,一气之下写了个恢复程序 师傅拯救无望 硬盘已经寄过去超过一周了,一问竟然是还没开始弄??? 再过一周,上来就问我分几个区?我要恢复哪些数据?我要恢复的数据在哪个位置? 那好吧,既然给了钱师傅也都放弃了,我也没什么好寄托希望的了。况且经过这三个...
    继续阅读 »

    硬盘坏了,一气之下写了个恢复程序


    师傅拯救无望


    硬盘已经寄过去超过一周了,一问竟然是还没开始弄???


    2023-03-24-14-15-16.png


    再过一周,上来就问我分几个区?我要恢复哪些数据?我要恢复的数据在哪个位置?


    2023-03-24-14-18-50.png


    2023-03-24-14-19-30.png


    2023-03-24-14-20-05.png


    那好吧,既然给了钱师傅也都放弃了,我也没什么好寄托希望的了。况且经过这三个星期的缓解,心情已经平复了很多,就像时光,回不来了就是回不来了。


    自救之路


    在把硬盘寄过去的时间里,等待师傅的修复结果的时间里,我并没有闲着(在摸鱼)。


    经过调研,数据恢复方法通常有:



    • 硬件损坏,对坏的盘进行修复

    • 误删或逻辑错误等,文件扫描修复

    • git 重置恢复


    很明显,这些都不适用于我现在的场景。因为师傅能不能修好是未知的,我只是数据盘没了,系统盘还在。由于 vscode 的数据目录空间占比较小,就没有搬迁到数据盘里,这刚好可以为恢复代码提供了可能。


    这是因为新版 vscode 有一个时间线功能,这个时间线数据是默认存储在用户目录下的。


    我从 C:/Users/love/AppData/Roaming/Code/User/History 目录中确实找到了很多名为 entries.json 的文件,结构如下:


    {
    // 配置版本
    "version": 1,
    // 原来文件所在位置
    "resource": "file:///d%3A/git2/cloudcmd/.madrun.mjs",
    // 文件历史
    "entries": [
    {
    // 历史文件存储的名称
    "id": "YFRn.mjs",
    "source": "工作区编辑",
    // 修改的时间
    "timestamp": 1656583915880
    },
    {
    "id": "Vfen.mjs",
    "timestamp": 1656585664751
    },
    ]
    }

    通过上面的文件大概可以看到,每一个时间点的文件都保存在另一个随机命名的文件里。而网上的方法基本都是自己一个个手动到目录里去根据最新的 id 去找对应的文件内容,然后创建文件并把内容复制出来。


    这个过程恢复一两个文件还好,但我这可是要恢复整个 git 工作区,大概有几十个项目上千个文件。


    这时候当然是在网上找找有没有什么 vscode 数据恢复 相关的工具,很遗憾找了大半天都没有找到。


    气死我了,一气之下就自己写个!


    恢复程序开发步骤


    毕竟只要数据在磁盘上,无非就是一个文件读取操作的问题,还要拿在这水文章,见谅见谅。


    首先考虑需求:



    • 我要实现一个自动扫描 vscode 数据目录

    • 然后以原始的目录结构还原出来,不需要我自己去创建文件夹和文件

    • 如果还原的文件最新的那份不是我想要的,我还能根据时间线进行对比和选择

    • 扫描出来有N个项目时,我可以指定只还原某此项目

    • 我可以搜索文件、目录名或文件内容进行还原

    • 为了方便,我还要一个看起来不太丑的操作界面


    大概就上面这些吧。


    然后考虑实现:


    我要实现一个自动扫描 vscode 数据目录


    要的就是我自己连数据目录和恢复地址也不需要填写,就能自动恢复的那种。那么就让程序来自动查找数据目录。经过调研,各版本的 vscode 的数据目录一般保存在这些地方:


    参考: stackoverflow.com/a/72610691


      - win -- C:\Users\Mark\AppData\Roaming\Code\User\History
    - win -- C:\Users\Mark\AppData\Roaming\Code - Insiders\User\History
    - /home/USER/.config/VSCodium/User/History/
    - C:\Users\USER\AppData\Roaming\VSCodium\User\History

    大概有上面这些路径,当然不排除使用者故意把默认位置修改掉这种边缘情况,或者使用者就只想扫描某个数据目录的情况,所以我也要支持手动输入目录:


      let { historyPath, toDir } = req.body
    const homeDir = os.userInfo().homedir
    const pathList = [
    historyPath,
    `${homeDir}/AppData/Roaming/Code/User/History/`,
    `${homeDir}/AppData/Roaming/Code - Insiders/User/History/`,
    `${homeDir}/AppData/Roaming/VSCodium/User/History`,
    `${homeDir}/.config/VSCodium/User/History/`,
    ]
    historyPath = (() => {
    return pathList.find((path) => path && fs.existsSync(path))
    })()
    toDir = toDir || normalize(`${process.cwd()}/re-store/`)

    然后以原始的目录结构还原出来……


    这就需要解析扫描到的时间线文件 entries.json 了。我们先把解析结果放到一个 list 中,以下是一个完整的解析方法。


    然后再把列表转换为树型,与硬盘上的状态对应起来,这样便于调试数据和可视化。


    function scan({ historyPath, toDir } = {}) {
    const gitRoot = `${historyPath}/**/entries.json`

    fs.existsSync(toDir) === false && fs.mkdirSync(toDir, { recursive: true })
    const globbyList = globby.sync([gitRoot], {})

    let fileList = globbyList.map((file) => {
    const data = require(file)
    const dir = path.parse(file).dir
    // entries.json 地址
    data.from = file
    data.fromDir = dir
    // 原文件地址
    data.resource = decodeURIComponent(data.resource).replace(
    /.*?\/\/\/(.*$)/,
    `$1`
    )
    // 原文件存储目录
    data.resourceDir = path.parse(data.resource).dir
    // 恢复后的完整地址
    data.rresource = `${toDir}/${data.resource.replace(/:\//g, `/`)}`
    // 恢复后的目录
    data.rresourceDir = `${toDir}/${path
    .parse(data.resource)
    .dir.replace(/:\//g, `/`)}
    `

    const newItem = [...data.entries].pop()
    // 创建文件所在目录
    fs.mkdirSync(data.rresourceDir, { recursive: true })
    const binary = fs.readFileSync(`${dir}/${newItem.id}`, {
    encoding: `binary`,
    })
    fs.writeFileSync(data.rresource, binary, { encoding: `binary` })
    return data
    })

    const tree = pathToTree(fileList, { key: `resource` })
    return tree
    }

    为了方便,我还要一个看起来不太丑的操作界面


    我们要把文件树的形式展示出来,还要方便切换。后面决定使用 macos 的文件管理器风格,大概如下。


    image.png


    如果还原的文件最新的那份不是我想要的,我还能根据时间线进行对比和选择


    理论上这里应该要做一个像 vscode 对比文件那样,有代码高亮功能,并且把有差异的字符高亮出来。


    实际上,这个需求得加钱。


    2023-03-24-15-09-25.png


    由于界面是在浏览器里的,需要自动打开,浏览器与系统交互需要一个接口,所以我们使用 opener 来自动打开浏览器。


    使用 get-port 来自动生成接口服务的端口,避免使用时出现占用。


      const opener = require(`opener`)
    const { portNumbers, default: getPort } = await import(`get-port`)
    const port = await getPort({ port: portNumbers(3000, 3100) })
    const server = express()
    server.listen(port, `0.0.0.0`, () => {
    const link = `http://127.0.0.1:${port}`
    opener(link)
    })

    封装成工具,我为人人


    理论上我根本不需要什么 UI 界面,也不需要配置,因为我的文件都恢复出来了我还花时间去搞毛线?


    实际上,万一别人也有这个恢复文件的需要呢?那么他只要运行下面这条命令代码就能立刻恢复到当前目录啦!


    npx vscode-file-recovery

    这就是恢复后的文件在硬盘里的样子啦:


    2023-03-24-15-22-23.png


    所有代码位于:



    建议收藏,以备不时之需。/手动狗头


    作者:程序媛李李李李李蕾
    来源:juejin.cn/post/7213994684262826040
    收起阅读 »

    跨域漏洞,我把前端线上搞崩溃了

    web
    最近迁移前端资源到云厂商时,我遇到了一个奇怪的问题 —— 线上环境某个 CSS 资源跨域了,后果直接导致项目崩溃不可用。大领导直接找到了我的工位,激动的心,颤抖的手,问题原因就是找不到!!! 很奇怪,我已经提前设置了资源允许跨域访问,提测环境、测试环境也已经...
    继续阅读 »

    最近迁移前端资源到云厂商时,我遇到了一个奇怪的问题 —— 线上环境某个 CSS 资源跨域了,后果直接导致项目崩溃不可用。大领导直接找到了我的工位,激动的心,颤抖的手,问题原因就是找不到!!!


    WX20230807-141353@2x.png


    很奇怪,我已经提前设置了资源允许跨域访问,提测环境、测试环境也已经正常运行很久了,理论上不应该出现跨域问题。而且更奇怪的是,这个问题只出现在某个 CSS 文件上。


    建议大家在阅读本文时结合目录一起查看。本文详细介绍了从跨域问题发现到跨域问题解决的整个过程,文章还简要提到了前端资源链路。结合上下文来看,对处理前端跨域问题具有一定的参考价值。希望对大家有所帮助。


    什么是跨域问题?


    跨域及其触发条件


    跨域是指在 web 开发中,一个网页的源(origin)与另一个网页的源不同,即它们的协议、域名或端口至少有一个不同。跨域问题是由于浏览器的同源策略而产生的,它限制了一个网页中加载的资源只能来自同一个源,以防止恶意网站在未经允许的情况下访问其他网站的数据。


    以下情况会触发跨域问题:



    1. 不同域名:当页面的域名与请求的资源的域名不一致时,会触发跨域问题,如从 example.com 页面请求资源来自 api.example.net

    2. 不同协议:如果页面使用了 https 协议加载,但试图请求非 https 资源,也会触发跨域问题。

    3. 不同端口:如果页面加载的是 example.com:3000,但试图请求资源来自 example.com:4000,同样会触发跨域问题。

    4. 不同子域名:即使是不同的子域名也会被认为是不同的源。例如,subdomain1.example.comsubdomain2.example.com 是不同的源。


    image.png


    跨域问题会影响到浏览器执行以下操作:



    • JavaScript的XMLHttpRequest或Fetch API请求其他源的资源。

    • 通过<img><link><script>等标签加载其他源的资源。

    • 使用CORS(跨源资源共享)机制实现跨域数据传输。


    解决跨域的方法


    解决跨域问题的方法有多种,具体的选择取决于你的应用场景。以下是一些常见的跨域解决方法:



    1. 跨域资源共享(CORS) :CORS是一种标准机制,通过在服务器端设置响应头来允许或拒绝跨域请求。这是解决跨域问题的最常见方法。

      • 在服务器端设置响应头中的Access-Control-Allow-Origin字段来指定允许访问的域名或使用通配符*表示允许所有域名访问。

      • 其他相关的CORS头,如Access-Control-Allow-MethodsAccess-Control-Allow-Headers,用于控制允许的HTTP方法和请求头。



    2. JSONP(JSON with Padding): 通过动态创建 <script> 标签来实现跨域请求的技术。服务器端返回的数据被包装在一个函数调用中,该函数名由客户端指定。虽然 JSONP 简单易用,但只支持GET请求,由于安全性较差(容易受到跨站脚本攻击),存在安全风险。
      // 客户端代码
      function handleResponse(data) {
      console.log('Received data:', data);
      }

      const script = document.createElement('script');
      script.src = 'https://example.com/api/data?callback=handleResponse';
      document.head.appendChild(script);


    3. 代理服务器:设置一个位于同一域的代理服务器,将跨域请求代理到目标服务器,并将响应返回给客户端。这个方法需要服务器端的额外配置。

    4. 跨文档消息传递: 使用window.postMessage()方法,可以在不同窗口或iframe之间进行跨域通信。

    5. WebSocket: WebSocket是一种双向通信协议,不受同源策略的限制。通过WebSocket,客户端和服务器可以建立持久连接进行跨域通信。

    6. Nginx反向代理: 使用 Nginx 或其他反向代理服务器可以将跨域请求转发到目标服务器,同时解决跨域问题。这种方法适用于前端无法直接控制目标服务器的情况。


    每种方法都有其适用的场景和安全考虑,具体的选择取决于项目的需求和架构。


    背景与跨域设置


    image.png


    项目背景介绍


    最近我负责了一个前端迁移第三方云(阿里云)的工作,由于这是一个多项目组合成的微前端项目,我考虑在前端迁移中,尽可能统一各个应用项目流程、规范和技术。一是形成统一的规范和方式,二是团队项目各负责人按照方案迁移各自项目时避免因各自不一致导致出现问题。


    而在这其中就存在着资源存储和加载不一致的情况,我遇到了三种不同的方法:




    1. 直接使用云存储提供的资源地址


      这是一种常见方式,但也伴随着一些潜在问题。首先,访问云资源可能会有一定的延迟,尤其对于大型文件或数据集。其次,公共云资源地址可能存在安全性和隐私风险,特别是涉及敏感信息时。此外,直接使用OSS资源地址可能导致资源管理分散,难以跟踪和监控资源的使用情况,也可能限制了一些高级功能的实现,如CDN缓存控制、分布式访问控制以及资源日志记录。


      https://company.oss-cn-beijing-internal.company.com/productdata/gateway/prod/assets/index-a12abcbf.css



    2. 使用配置了云存储的CDN域名地址


      这种方式是我比较推荐的方式,然而团队在配置这块链路上存在一些潜在问题。首先 CDN 请求打到了团队内部的一个老服务器,目前老服务器的稳定性不如预期,稳定性方面较差,出现过故障,影响用户体验。前端迁移到第三方云的主要目的之一就是解耦该服务,提供更稳定的前端资源环境。此外,该服务器与其他服务器存在依赖关系,增加了项目的复杂性和不稳定性,解耦是必要的。并且使用这个 CDN 的项目很多,随着时间推移,项目的增加可能会使得该资源地址的维护变得相当复杂。


      https://static.company.com/productdata/gateway/prod/assets/index-a12abcbf.css



    3. 直接加载服务器内部的前端资源


      直接加载服务器内部的前端资源是通过网站域名的代理访问服务器内部资源的一种方法。有几个项目使用这个种方式,这种方式具备简单、快捷等优势。然而,这种方式可能引入网络延迟和性能影响,因为资源请求需要经过团队内部的服务器。同时,存在单点故障的风险,如果内部服务器发生故障或不可用,将导致网站前端资源无法加载,可能对用户造成不便。它也依赖于团队内部的网络环境,需要保持网络的稳定性和可靠性以确保资源能够顺利加载。




    为了统一这三种方式并规避潜在问题,我想到了一个综合性的前端资源请求链路方案。通过将 OSS 存储桶、CDN 和网关服务器相互结合,以提升资源分发速度和安全性,同时减轻 OSS 服务器的负载。此外,我还将所有资源引用集中到一个配置文件中,位于网关服务器,以便轻松进行维护和跟踪。(这里只是简要介绍,我将在后续文章分享详细细节


    然而,在初步方案制定后,也需要考虑如何处理在同源策略下可能出现的跨域问题。


    image.png


    前端静态资源跨域设置


    我在OSS存储桶的跨域设置中配置了允许跨域头,使得网页可以通过浏览器访问OSS资源,免受同源策略的限制。


    image.png


    为什么我会选择在 OSS 存储桶配置呢?


    主要因为这个存储桶非常整洁,只有两个项目在使用,已经提前简单配置了跨域处理。而且这两个项目后续会按照前端迁移方案进行统一迁移处理,因此我认为直接在 OSS 存储桶配置跨域会更为简洁和可维护,我还和 SRE 老师调整了一下配置(然而,没想到恰恰因为我的这个行为,导致后面出现了跨域问题)。


    此外,为了确保安全性,我采取了以下措施:



    • 将项目单独、分批迁移到阿里云 OSS。

    • 在网关服务器中使用nginx进行项目正则匹配,每次迁移就开放一个项目。
      location ~ ^/(gateway|message)/prod/

    • 项目在提测、测试环境都各自运行一段时间(有的甚至在1~2个月)。

    • 在未迁移到正式环境前,各项目按照各自排期计划进行过多次发版。


    这些措施是为了确保有问题,可以在提测环境、测试环境中暴露出来。然而,在迁移第3个项目到正式服环境时,出现了问题。。。


    奇怪的CSS资源跨域问题


    为什么只有某个CSS文件受影响?


    跨域问题通常由浏览器的同源策略引起,该策略限制了来自不同源的资源之间的交互。


    如果资源有跨域问题,不应该只有某个CSS文件出现跨域问题呀?


    3p55k2cus.png


    分析后,我发现浏览器中 CSS 资源的返回头中缺少 CORS 头信息,截图如下:


    image.png


    正常情况下,应该是下图这样:


    image.png


    这时候我在想不应该呀,我已经在源站 OSS 存储桶配置了允许跨域头,这里的返回头中应用是要携带的,而且别的文件(如html、js)返回头中都是携带了允许跨域,但是为什么只有这个 CSS 资源的就没有呢?





    需要注意的是,通常情况下,HTML 文件本身不受同源策略的限制,因此可以从不同源加载 CSS 文件。但如果 CSS 文件中包含引用其他跨域资源(如字体、图片等),那么同源策略仍然会生效,需要特别注意处理这些跨域资源的加载。


    问题的深层原因分析


    image.png


    排除了自身导致的问题


    面对这样一个看似简单的跨域问题,我做了一系列的排查和解决过程。首先,我排除了浏览器缓存、资源代码方面以及浏览器本身的问题,并同 SRE 老师否定了前端资源链路(如OSS、CDN)配置错误的可能性。随后,我们还仔细查看了网关日志,但未能发现问题。


    一直没找到导致跨域问题出现的原因,我们也想到了直接在网关服务器或 CDN 中强制加入允许跨域头。然而我们一讨论,发现不行,因为 OSS 中已经配置了跨域,强制加入允许跨域头,会出现双重跨域问题;如果移除 OSS 中跨域头也不行,因为已经有两个项目已经直接引用阿里云地址,移除后那两个项目也会出现跨域问题。





    寻求阿里云 CDN 运维工程师的帮助


    结合我们自己的分析,我们认为是前端资源请求链路的哪个环节出现了问题,但是迟迟找不到原因,于是我们咨询了阿里云 CDN 运维工程师,因为阿里云 CDN 的日志隔天才出来,所以借此希望通过阿里云 CDN 运维老师能够查看下当天的 CDN 日志,从而找到问题。查看日志后,阿里云 CDN 运维老师也只是给出了日志显示一切正常,但随后我们继续沟通。


    随后,给到了我们一个关键点:“OSS要能响应跨域头,请求头必须携带 Origin 请求头”。阿里云 CDN 运维老师也说传任何值都可以,但是我多次查看到浏览器请求已经携带了 Origin 请求头。如下图:


    image.png


    这就奇怪了!此时测试环境提测环境又无法复现 CORS 跨域问题,我们又不能直接在生产环境调试这个问题。


    借助工具复现问题


    于是我在思考是否能够在提测环境模拟出加载有问题资源的场景。我想到了可以通过拦截浏览器对提测环境的资源请求地址,并将其代理到具有问题的资源地址上来实现这个目的。为了实现这一方案,我使用了一个名为 GoRes 的谷歌浏览器插件。


    image(2).png


    成功复现,见下图:


    3p55k2cus.png


    随后,在多次代理调试中,我发现只有在正式服这个项目的资源地址中出现了这个问题。我和 SRE 老师一起再次确认了提测环境、测试环境和正式环境中各自网关服务器和 CDN 域名等的差异性,当然还是没发现问题!





    问题逐渐浮现出水面


    经过综合分析,我们怀疑 CDN 缓存可能是导致问题的原因。然而,我们无法直接查看缓存的资源,只能再次联系阿里云 CDN 的运维老师。经过多次沟通,我们得知如果客户端在第一次请求 CDN 时没有携带 Origin 请求头,CDN 就不会将 Origin 请求头传递到 OSS,OSS 因此不会响应跨域头,而后续 CDN 便会将没有跨域头的资源内容缓存下来。


    这时我才意识到,OSS 内部存在着对 Origin 辨别的跨域处理机制。而在此之前,上传代码资源到 OSS 后,由于是正式环境,为了安全起见测试资源是否上传成功,我直接在浏览器中访问了一个 CSS 文件地址(当时请求到了资源,我还信心满满,丝毫没有注意到还有这么一个坑),但这一步的操作却间接成为了导致跨域问题出现的导火索


    通常情况下,当网页加载跨域资源时,由于违反了同源策略,浏览器会自动添加源 Origin 到资源的请求头中。然而,由于我直接请求了 CSS 资源地址,未触发同源策略,浏览器也就没有自动添加 Origin 请求头,导致请求到的 OSS 资源中没有跨域头配置。这也就是为什么 CDN 缓存了没有跨域头的资源。


    在网页加载资源时,由于 CDN 缓存了没有跨域头的资源,无论你如何请求,只要 CDN 缓存一直存在,浏览器加载的就是没有跨域头的资源。 因此,这也导致了资源跨域问题的出现。



    本来是为了谨慎一点,提前验证资源是否已上传成功的操作,没想到却成为了跨域问题出现的导火索!!!



    image.png


    这个问题的教训很深刻,让我们意识到必须在向 OSS 请求资源时强制添加 Origin 请求头,以确保今后前端资源的稳定性。否则,这个问题将成为一个定时炸弹。我们决定在网关服务器上分工合作解决这个问题,以杜绝类似情况再次发生。这个经验教训也提醒了我和SRE老师要更加谨慎地处理类似操作,以避免潜在的问题。


    如何稳定解决跨域问题


    尽管我们已经找到了问题的根源,但是不排除是不是还有其他类似问题,为了保险起见,我决定还是缩小影响范围。在确保测试无问题后,逐步放开所有项目。


    SRE 老师负责处理向 OSS 传递Origin请求头的部分,而我负责处理 Nginx location 的正则匹配项目的部分。以下是我们的网关服务器配置:


    location ~ ^/(message|dygateway|logcenter)/tice/ { 
    set $cors_origin "";
    if ($http_origin = "" ) {
    set $cors_origin 'static.example.com';
    }
    if ($http_origin != "") {
    set $cors_origin $http_origin;
    }
    proxy_set_header Origin $cors_origin;
    }



    • location ~ ^/(message|dygateway)/tice/:这是一个正则匹配,能更容易地添加或移除项目。




    • proxy_set_header Origin $cors_origin;:如果请求中包含 Origin 头部,它会被直接传递给 OSS;如果没有,它会被设置为一个值后再传递给 OSS。




    配置完成后,直接在浏览器中请求下面这个资源地址,你会发现请求头并没有添加上去。这并不是配置出错,而是因为上面我们提到的CDN不仅缓存了资源,还缓存了请求头。


    https://static.company.com/productdata/gateway-admin/prod/assets/index-a12abcbf.css

    所以我们在这个资源的地址后面拼接了参数,相当于是请求新的 CDN 资源地址,此时可以发现跨域头已经添加上了。


    https://static.company.com/productdata/gateway-admin/prod/assets/index-a12abcbf.css?abc=111

    image.png


    接下来就是在真实项目中测试下,首先在 CDN 后台刷新了有问题的项目资源文件目录,清除掉有跨域问题 CDN 资源缓存后。然后重新刷新浏览器,此时这个文件就成功加上了跨域的请求头,页面访问也正常了。





    image.png


    image.png


    后面我又测试了多次,跨域问题彻底解决。为了避免以后出现类型的问题,所以我又整理了跨域资源共享(CORS)方案,希望对大家有用,请大家接着往下看。


    跨域资源共享方案


    image.png


    跨域资源共享方案是解决前端资源跨域问题的最常见方法,可维护性强,配置简单,可以说这是业界普遍处理前端资源跨域的方式。下面我们将深入探讨三种不同的 CORS 配置方案,并分析各自的优缺点。


    OSS存储桶配置跨域


    我们都知道 OSS(对象存储服务)是阿里云提供的海量、安全、低成本、高可靠的云存储服务。但其实 OSS 也能设置跨域处理,可以让客户端前端应用从其他域名请求资源。


    实施步骤:



    1. 登录阿里云控制台,找到对应的OSS存储桶。

    2. 进入存储桶的管理界面,选择“跨域设置”。

    3. 添加CORS规则,指定允许的来源、方法、头信息等。


    image.png


    优点:



    • 简单易用:配置简单,通过图形界面即可完成。

    • 安全性高:可以灵活控制允许访问的来源,减少安全风险。


    缺点:



    • 依赖云服务商:此方法只适用于使用阿里云OSS的情况,不适用于其他云服务商或自建服务器。


    注意:OSS存储桶配置完成跨域后,需要在请求 OSS 存储桶资源时,在请求头中配置 Origin。因为 OSS 内部的机制是 OSS 响应跨域头的前提是必须要携带源站Origin请求头。 建议大家强制配置必传 Origin 请求头,否则容易出现我这次的问题。使用OSS存储桶配置跨域制定方案时,可以参考我在上面的处理:“如何稳定解决跨域问题”。


    网关服务器配置跨域


    在网关服务器配置跨域,网关服务器通常配置了 Nginx 反向代理服务器。通过配置 Nginx location,可以实现对特定域名的允许跨域支持。


    实施步骤:



    1. 修改nginx配置文件(通常位于/etc/nginx/nginx.conf),添加CORS相关配置。

    2. 配置Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers等头信息。


    location / {
    add_header 'Access-Control-Allow-Origin' '*';
    add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE';
    add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
    add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
    }

    优点:



    • 灵活性高:可以自由配置适应特定需求。

    • 适用性广:适用于各种服务器环境,不依赖特定云服务商。


    缺点:



    • 配置复杂:需要熟悉nginx配置。


    CDN配置跨域


    CDN(内容分发网络)是一种通过将内容缓存到全球各地节点,加速用户访问速度的网络服务。但也能通过 CDN 配置 CORS,可以在边缘节点处实现跨域。


    实施步骤:



    1. 登录CDN服务提供商的控制台,找到相应CDN加速域名。

    2. 进入域名配置界面,找到CORS配置选项。

    3. 添加CORS规则,指定允许的来源、方法、头信息等。


    image.png


    优点:



    • 高性能:CDN 服务通常提供全球分发,可以加速跨域请求,提供更好的性能。

    • 规模化:适用于大规模的Web应用,可支持高并发的跨域请求。


    缺点:



    • 成本:使用 CDN 服务可能会产生额外的费用,特别是对于大量的数据传输。

    • 配置复杂性:相对于 OSS 或 Nginx,CDN 的配置可能会更为复杂,需要在控制台进行详细的设置。


    注意:腾讯云 CDN 中有专门针对跨域设置的勾选项,只需要选中保存就行。


    三种跨域处理方案各有优缺点,选择合适的方案取决于具体的业务需求和技术栈。我上面所说的也只供大家参考,毕竟 CDN、存储桶这种很大程度受限于云平台,这也是我把允许跨域配置在网关服务器的原因之一。可以综合考虑选择合适的方案或者结合多种方案来实现跨域资源共享。


    u=2094032080,194978745&fm=30&app=106&f=JPEG.jpeg


    结语


    前端资源加载问题往往受多种因素的影响,包括 CDN 配置、资源请求链路、云存储配置等。因此,需要全面分析并综合考虑可能出现问题的任何风险点。也要合理使用浏览器插件工具、网络抓包工具和服务器日志分析等工具,可以帮助我们更快速地诊断和解决问题。如果问题复杂或涉及云服务配置,与云厂商的支持团队联系可以提供专业的帮助。


    这是我关于资源跨域的一篇文章,里面关于定位问题和跨域方案希望对您有所帮助和参考。如果您需要进一步的协助或有任何问题,请随时提问!


    作者:Sailing
    来源:juejin.cn/post/7279429009796546623
    收起阅读 »

    别再用 float 布局了,flex 才是未来!

    web
    大家好,我是树哥! 前面一篇文章整体介绍了 CSS 的布局知识,其中说到 float 布局是 CSS 不断完善的副产物。而在 2023 年的今天,flex 这种布局方式才是未来!那么今天我们就来学习下 flex 弹性布局。 什么是 Flex 布局? 在经过了长...
    继续阅读 »

    大家好,我是树哥!


    前面一篇文章整体介绍了 CSS 的布局知识,其中说到 float 布局是 CSS 不断完善的副产物。而在 2023 年的今天,flex 这种布局方式才是未来!那么今天我们就来学习下 flex 弹性布局。


    什么是 Flex 布局?


    在经过了长达 10 年的发展之后,CSS3 才终于迎来了一个简单好用的布局属性 —— flex。Flex 布局又称弹性布局,它使用 flexbox 属性使得容器有了弹性,可以自动适配各种设备的不同宽度,而不必依赖于传统的块状布局和浮动定位。


    举个很简单地例子,如果我们想要实现一个很简单左侧定宽,右侧自适应的导航布局,如下图所示。


    -w1239


    在没有 flex 之前,我们的代码是这么写的。


    <div>
    <h1>4.1 两栏布局 - 左侧定宽、右侧自适应 - float</h1>
    <div class="container">
    <div class="left41"></div>
    <div class="right41"></div>
    </div>
    </div>

    /** 4.1 两栏布局 - 左侧定宽、右侧自适应 - float **/
    .left41 {
    float: left;
    width: 300px;
    height: 500px;
    background-color: pink;
    }
    .right41 {
    width: 100%;
    height: 500px;
    background-color: aquamarine;
    }

    这种方式不好的地方在于,我们还需要去理解 float 这个概念。一旦需要理解 float 这个概念,我们就会拖出一大堆概念,例如文档流、盒子模型、display 等属性(虽然这些东西确实应该学)。但对于 flex 来说,它就很简单,只需要设置一个伸缩系数即可,如下代码所示。


    <div>
    <h1>4.2 两栏布局 - 左侧定宽、右侧自适应 - flex</h1>
    <div class="container42">
    <div class="left42"></div>
    <div class="right42"></div>
    </div>
    </div>

    .container42 {
    display: flex;
    }
    .left42 {
    width: 300px;
    height: 500px;
    background-color: pink;
    }
    .right42 {
    flex: 1;
    width: 100%;
    height: 500px;
    background-color: aquamarine;
    }

    上面的代码里,我们只需要将父级容器设置为 flex 展示形式(display: flex),随后在需要自动伸缩的容器里设置属性即可。上面代码中的 flex: 1 表示其占据所有其他当行所剩的空间。通过这样的方式,我们非常方便地实现了弹性布局。


    当然,上面只是一个最简单的例子,甚至还不是很能体现出 flex 的价值。flex 除了在响应式布局方面非常方便之外,它在对齐等方面更加方便,能够极大地降低学习成本、提高工作效率。


    Flex 核心概念


    对于 Flex 布局来说,其有几个核心概念,分别是:主轴与交叉轴、起始线和终止线、Flex 容器与 Flex 容器项。


    主轴和交叉轴


    在 Flex 布局中有一个名为 flex-direction 的属性,可以取 4 个值,分别是:



    • row

    • row-reverse

    • column

    • column-reverse


    如果你选择了 row 或者 row-reverse,那么主轴(Main Axis)就是横向的 X 轴,交叉轴(Cross Axis)就是竖向的 Y 轴,如下图所示。


    主轴是横向的X轴,交叉轴是竖向的Y轴


    如果你选择了 column 或者 column-reverse,那么主轴(Main Axis)就变成是竖向的 Y 轴,交叉轴(Cross Axis)就是横向的 X 轴,如下图所示。


    主轴是竖向的Y轴,交叉轴是横向的X轴


    起始线和终止线


    过去,CSS 的书写模式主要被认为是水平的,从左到右的。但现代的布局方式涵盖了书写模式的范围,所以我们不再假设一行文字是从文档的左上角开始向右书写的。


    对于不同的语言来说,其书写方向不同,例如英文是从左到右,但阿拉伯文则是从右到左。那么对于这两种语言来说,其xx会有所不同 TODO。举个简单的例子,如果 flex-direction 是 row ,并且我是在书写英文。由于英文是从左到右书写的,那么主轴的起始线是左边,终止线是右边,如下图所示。


    -w557


    但如果我在书写阿拉伯文,由于阿拉伯文是从右到左的,那么主轴的起始线是右边,终止线是左边,如下图所示。


    -w541


    在 Flex 布局中,起始线和终止线决定了 Flex 容器中的 Flex 元素从哪个方向开始排列。 举个简单例子,如果我们通过 direction: ltr 设置了文字书写方向是从左到右,那么起始线就是左边,终止线就是右边。此时,如果我们设置的 flex-direction 值是 row,那么 Flex 元素将会从左到右开始排列。但如果我们设置的 flex-direction 值是 row-reverse,那么 Flex 元素将会从右到左开始排列。


    在上面的例子中,交叉轴的起始线是 flex 容器的顶部,终止线是底部,因为两种语言都是水平书写模式。但如果有一种语言,它的书写形式是从底部到顶部,那么当设置 flex-direction 为 column 或 column-reverse 时,也会有类似的变化。


    Flex 容器与 Flex 元素


    我们把一个容器的 display 属性值改为 flex 或者 inline-flex 之后,该容器就变成了 Flex 容器,而容器中的直系子元素就会变为 flex 元素。如下代码所示,parent 元素就是 Flex 容器,son 元素就是 Flex 元素。


    <style>
    #parent {
    display: flex;
    }
    </style>
    <div id="parent">
    <div id="son"></div>
    </div>

    Flex 核心属性


    对于 Flex 来说,它有非常多的用法,但核心属性却相对较少。这里我只简单介绍几个核心属性,如果你想了解更多 Flex 的属性,可以去 Mozilla 官网查询,这里给个传送门:flex 布局的基本概念 - CSS:层叠样式表 | MDN


    对于 Flex 布局来说,其核心属性有如下几个:



    1. flex-direction 主轴方向

    2. flex 伸缩系数及初始值

    3. justify-content 主轴方向对齐

    4. align-items 交叉轴方向对齐


    flex-direction 主轴方向


    如上文所介绍过的,flex-direction 定义了主轴的方向,可以取 4 个值,分别是:



    • row 默认值

    • row-reverse

    • column

    • column-reverse


    一旦主轴确定了,交叉轴也确定了。主轴和交叉轴与后续的对齐属性有关,因此弄懂它们非常重要!举个很简单的例子,如下的代码将展示下图的展示效果。


    .box {
    display: flex;
    flex-direction: row-reverse;
    }

    <div class="box">
    <div>One</div>
    <div>Two</div>
    <div>Three</div>
    </div>

    -w538


    如果你将 flex-direction 改成 column-reverse,那么将会变成如下的效果,如下图所示。


    -w541


    flex 伸缩系数及初始值


    前面说到 Flex 布局可以很方便地进行响应式布局,其实就是通过 flex 属性来实现的。flex 属性其实是 flex-grow、flex-shrink、flex-basis 这三个参数的缩写形式,如下代码所示。


    flex-grow: 1;
    flex-shrink: 1;
    flex-basis: 200px;
    /* 上面的设置等价于下面 flex 属性的设置 */
    flex: 1 1 200px;

    在考虑这几个属性的作用之前,需要先了解一下 可用空间 available space 这个概念。这几个 flex 属性的作用其实就是改变了 flex 容器中的可用空间的行为。


    假设在 1 个 500px 的容器中,我们有 3 个 100px 宽的元素,那么这 3 个元素需要占 300px 的宽,剩下 200px 的可用空间。在默认情况下,flexbox 的行为会把这 200px 的空间留在最后一个元素的后面。


    -w537


    如果期望这些元素能自动地扩展去填充满剩下的空间,那么我们需要去控制可用空间在这几个元素间如何分配,这就是元素上的那些 flex 属性要做的事。


    flex-basis


    flex-basis 属性用于设置 Flex 元素的大小,其默认值是 auto。此时浏览器会检查元素是否有确定的尺寸,如果有确定的尺寸则用该尺寸作为 Flex 元素的尺寸,否则就采用元素内容的尺寸。


    flex-grow


    flex-grow 若被赋值为一个正整数,flex 元素会以 flex-basis 为基础,沿主轴方向增长尺寸。这会使该元素延展,并占据此方向轴上的可用空间(available space)。如果有其他元素也被允许延展,那么他们会各自占据可用空间的一部分。


    举个例子,上面的例子中有 a、b、c 个 Flex 元素。如果我们给上例中的所有元素设定 flex-grow 值为 1,容器中的可用空间会被这些元素平分。它们会延展以填满容器主轴方向上的空间。


    但很多时候,我们可能都需要按照比例来划分剩余的空间。此时如果第一个元素 flex-grow 值为 2,其他元素值为 1,则第一个元素将占有 2/4(上例中,即为 200px 中的 100px), 另外两个元素各占有 1/4(各 50px)。


    flex-shrink


    flex-grow 属性是处理 flex 元素在主轴上增加空间的问题,相反 flex-shrink 属性是处理 flex 元素收缩的问题。如果我们的容器中没有足够排列 flex 元素的空间,那么可以把 flex 元素 flex-shrink 属性设置为正整数,以此来缩小它所占空间到 flex-basis 以下。


    与flex-grow属性一样,可以赋予不同的值来控制 flex 元素收缩的程度 —— 给flex-shrink属性赋予更大的数值可以比赋予小数值的同级元素收缩程度更大。


    justify-content 主轴方向对齐


    justify-content 属性用来使元素在主轴方向上对齐,它的初始值是 flex-start,即元素从容器的起始线排列。justify-content 属性有如下 5 个不同的值:



    • flex-start:从起始线开始排列,默认值。

    • flex-end::从终止线开始排列。

    • center:在中间排列。

    • space-around:每个元素左右空间相等。

    • space-between:把元素排列好之后,剩余的空间平均分配到元素之间。


    各个不同的对齐方式的效果如下图所示。


    flex-start:


    -w454


    flex-end:


    -w444


    center:


    -w449


    space-around:


    -w442


    space-between:


    -w453


    align-items 交叉轴方向对齐


    align-items 属性可以使元素在交叉轴方向对齐,它的初始值是 stretch,即拉伸到最高元素的高度。align-items 属性有如下 5 个不同的值:



    • stretch:拉伸到最高元素的高度,默认值。

    • flex-start:按 flex 容器起始位置对齐。

    • flex-end:按 flex 容器结束为止对齐。

    • center:居中对齐。

    • baseline:始终按文字基线对齐。


    各个不同的对齐方式的效果如下图所示。


    stretch:


    -w448


    flex-start:


    -w439


    flex-end:


    -w438


    center:


    -w444


    要注意的事,无论 align-items 还是 justify-content,它们都是以主轴或者交叉轴为参考的,而不是横向和竖向为参考的,明白这点很重要。


    Flex 默认属性


    由于所有 CSS 属性都会有一个初始值,所以当没有设置任何默认值时,flex 容器中的所有 flex 元素都会有下列行为:



    • 元素排列为一行 (flex-direction 属性的初始值是 row)。

    • 元素从主轴的起始线开始。

    • 元素不会在主维度方向拉伸,但是可以缩小。

    • 元素被拉伸来填充交叉轴大小。

    • flex-basis 属性为 auto。

    • flex-wrap 属性为 nowrap。


    弄清楚 Flex 元素的默认值有利于我们更好地进行布局排版。


    实战项目拆解


    看了那么多的 Flex 布局知识点,总感觉干巴巴的,是时候来看看别人在项目中是怎么用的了。


    -w1290


    上面是我在 CodePen 找到的一个案例,这样的一个布局就是用 Flex 布局来实现的。通过简单的分析,其实我们可以拆解出其 Flex 布局方法,大致如下图所示。


    -w1297


    首先整体分为两大部分,即导航栏和内容区域,这部分的主轴纵向排列的(flex-direction: column),如上图红框部分。随后在内容区域,又将其分成了左边的导航栏和右边的内容区域,此时这块内容是横向排列的(flex-direction: row),如下上图蓝框部分。


    剩下的内容布局也大致类似,其实就是无限套娃下去。由于偏于原因,这里就不继续深入拆解了,大致的布局思路已经说得很清楚了。


    有了 Flex 布局之后,貌似布局也变得非常简单了。但纸上得来终觉浅,还是得自己实际动手练练才知道容易不容易,不然就变成纸上谈兵了!


    总结


    看到这里,关于 Flex 布局的核心点就介绍得差不多了。掌握好这几个核心的知识点,开始去实践练习基本上没有什么太大的问题了。剩下的一些比较小众的属性,等用到的时候再去查查看就足够了。


    接下来更多的时间,就是找多几个实战案例实践,唯有实践才能巩固所学知识点。后面有机会,我将分享我在 Flex 布局方面的项目实践。


    如果这篇文章对你有帮助,记得一键三连支持我!


    参考资料



    作者:树哥聊编程
    来源:juejin.cn/post/7280054182996033548
    收起阅读 »

    看完这位小哥的GitHub,我沉默了

    web
    就在昨天,一个名为win12的开源项目一度冲上了GitHub的Trending热榜。 而且最近项目的Star量也在飙升,目前已经获得了2.2k+的Star标星。 出于好奇,点进去看了看。好家伙,项目README里写道这是一个14岁的初中生所打造的开源项目。...
    继续阅读 »

    就在昨天,一个名为win12的开源项目一度冲上了GitHub的Trending热榜。



    而且最近项目的Star量也在飙升,目前已经获得了2.2k+的Star标星。



    出于好奇,点进去看了看。好家伙,项目README里写道这是一个14岁的初中生所打造的开源项目。


    即:在网页端实现了Windows 12的UI界面和交互效果。


    这里也放几张图片感受一下。



    • 登录页面




    • 开始菜单




    • 资源管理器




    • 设置




    • 终端命令行




    • AI Copilot




    • 其他应用



    这个项目的灵感来源于作者之前看到 Windows 12 概念版后深受启发,于是决定做一个Windows12网页版(就像之前的 Windows 11 网页版一样),可以让用户在网络上预先体验 Windows 12。


    可以看到,这个项目是一个前端开源项目,而且由标准前端技术(HTML,JS,CSS)来实现,下载代码,无需安装,打开desktop.html即可。



    项目包含:



    • 精美的UI设计

    • 流畅丰富的动画

    • 各种高级的功能(相较于网页版)


    不仅如此,作者团队对于该项目的后续发展还做了不少规划和畅想。



    • 项目规划




    • 项目畅想



    刚上面也说了,项目README里写道该项目的作者是一位14岁的初中生,网名叫星源,曾获得CSP普及组一等奖和蓝桥杯国赛三等奖。


    作者出生于2009年,在成都上的小学和初中,目前刚上初三。


    这样来看的话,虽说作者年龄很小,不过接触计算机和编程应该非常早,而且对计算机领域的知识和技术应该有着非常浓厚的兴趣。


    从作者的个人主页里能看到,技术栈这块涉猎得还挺广泛。



    作者自己表示如今上初三了,对于win12这个项目也不会做什么功能的更新了,后续的维护更新将交给其他贡献者成员。



    文章的结尾也附上Windows 12网页版体验地址:tjy-gitnub.github.io/win12/desktop.html,感兴趣的同学可以自行体验。


    聊到这里不得不说,人与人之间的差距确实挺大的。就像这位小哥,才14岁就已经精通前端技术了。


    而14岁的我,当年在干嘛呢?


    我想了又想。。


    额,我好像在网吧里玩红警。。(手动doge)


    作者:CodeSheep
    来源:juejin.cn/post/7275978708644151354
    收起阅读 »

    蒙提霍尔问题

    最近看韩国电视剧【D.P:逃兵追缉令】里面提到一个有趣的数学概率游戏 -> 蒙提霍尔问题 意思是:参赛者会看见三扇门,其中一扇门的里面有一辆汽车,选中里面是汽车的那扇门,就可以赢得该辆汽车,另外两扇门里面则都是一只山羊,让你任意选择其中一个,然后打开其余...
    继续阅读 »



    最近看韩国电视剧【D.P逃兵追缉令】里面提到一个有趣的数学概率游戏 -> 蒙提霍尔问题


    意思是:参赛者会看见三扇门,其中一扇门的里面有一辆汽车,选中里面是汽车的那扇门,就可以赢得该辆汽车,另外两扇门里面则都是一只山羊,让你任意选择其中一个,然后打开其余两个门中的一个并且是山羊(去掉一个错误答案),这时,让你重新选择。那么你是会坚持原来的选择,还是换选另外一个未被打开过的门呢?


    大家可以想一想如果是自己,我们是会换还是不会换?


    好了,我当时看到后感觉很有意思,所以我简单写了一套代码,源码贴在下面,大家可以验证一下,先告诉大家,换赢得汽车的概率是2/3,不换赢得汽车的概率是1/3

    <header>
    <h1>请选择换不换?</h1><button class="refresh">刷新</button>
    </header>
    <section>
    <div class="box">
    <h2>1</h2>
    <canvas width="300" height="100"></canvas>
    <div class="prize">奖品</div>
    </div>
    <div class="box">
    <h2>2</h2>
    <canvas width="300" height="100"></canvas>
    <div class="prize">奖品</div>
    </div>
    <div class="box">
    <h2>3</h2>
    <canvas width="300" height="100"></canvas>
    <div class="prize">奖品</div>
    </div>
    </section>
    <span>请选择号码牌</span>
    <select name="" id="">
    <option value="1">1</option>
    <option value="2">2</option>
    <option value="3">3</option>
    </select>
    <button class="confirm">确认</button>
    <span class="confirm-text"></span>
    <span class="opater">
    <button class="change">换</button>
    <button class="no-change">不换</button>
    </span>
    <p>
    <strong>游戏规则:</strong>
    <span>
    上面有三个号码牌,其中一个号码牌的里面有汽车,选中里面是汽车的号码牌,
    你就可以赢得该辆汽车,另外两个号码牌里面则都是一只山羊,
    你任意选择其中一个,然后打开其余两个号码牌中的一个并且是山羊(去掉一个错误答案),
    这时,你有一个重新选择的机会,你选择换还是不换?
    </span>
    </p>
    .prize {
    width: 300px;
    height: 100px;
    background-color: pink;
    font-size: 36px;
    line-height: 100px;
    text-align: center;
    position: absolute;
    }

    canvas {
    position: absolute;
    z-index: 2;
    }

    section {
    display: flex;
    }

    .box {
    width: 300px;
    height: 200px;
    cursor: pointer;
    }

    .box+.box {
    margin-left: 8px;
    }

    header {
    display: flex;
    align-items: center;
    }

    header button {
    margin-left: 8px;
    height: 24px;
    }
    p {
    width: 400px;
    background-color: pink;
    }
    function shuffleArray(array) {
    for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [array[i], array[j]] = [array[j], array[i]];
    }
    return array;
    }
    function getRandomNumber() {
    return Math.random() > 0.5 ? 1 : 2;
    }
    let a1 = [0, 1, 2]
    let i1 = undefined
    let i2 = undefined
    let isChange = false
    const opater = document.querySelector('.opater')
    opater.style.display = 'none'
    // 随机一个奖品
    const prizes = document.querySelectorAll('.prize')
    let a0 = [0, 1, 2]
    a0 = shuffleArray(a0)
    a0.forEach((v,i) => {
    const innerText = !!v ? '山羊' : '汽车'
    prizes[i].innerText = innerText
    })

    const canvas = document.querySelectorAll('canvas')
    const confirmText = document.querySelector('.confirm-text')
    canvas.forEach(c => {
    // 使用canvas实现功能
    // 1. 使用canvas绘制一个灰色的矩形
    const ctx = c.getContext('2d')
    ctx.fillStyle = '#ccc'
    ctx.fillRect(0, 0, c.width, c.height)
    // 2. 刮奖逻辑
    // 鼠标按下且移动的时候,需要擦除canvas画布
    let done = false
    c.addEventListener('mousedown', function () {
    if (i1 === undefined) return alert('请先选择号码牌,并确认!')
    if (!isChange) return alert('请选择换不换!')
    done = true
    })
    c.addEventListener('mousemove', function (e) {
    if (done) {
    // offsetX 和 offsetY 可以获取到鼠标在元素中的偏移位置
    const x = e.offsetX - 5
    const y = e.offsetY - 5
    ctx.clearRect(x, y, 10, 10)
    }
    })
    c.addEventListener('mouseup', function () {
    done = false
    })
    })
    const confirm = document.querySelector('.confirm')
    const refresh = document.querySelector('.refresh')
    confirm.onclick = function () {
    let select = document.querySelector('select')
    const options = Array.from(select.children)
    confirmText.innerText = `您选择的号码牌是${select.value},请问现在换不换?`
    // 选择后,去掉一个错误答案
    // i1是下标
    i1 = select.value - 1
    // delValue是值
    let delValue = undefined
    // 通过下标找值
    if (a0[i1] === 0) {
    delValue = getRandomNumber()
    } else {
    delValue = a0[i1] === 1 ? 2 : 1
    }
    // 通过值找下标
    i2 = a0.indexOf(delValue)
    // 选择的是i1, 去掉的是
    const ctx = canvas[i2].getContext('2d')
    ctx.clearRect(0, 0, 300, 100)
    options.map(v => v.disabled = true)
    confirm.style.display = 'none'
    opater.style.display = 'inline-block'
    }
    const change = document.querySelector('.change')
    const noChange = document.querySelector('.no-change')
    change.onclick = function () {
    isChange = true
    const x = a1.filter(v => v !== i1 && v !== i2)
    confirmText.innerText = `您确认选择的号码牌是${x[0] + 1},请刮卡!`
    opater.style.display = 'none'
    }
    noChange.onclick = function () {
    isChange = true
    confirmText.innerText = `您确认选择的号码牌是${i1 + 1},请刮卡!`
    opater.style.display = 'none'
    }
    refresh.onclick = function () {
    window.location.reload()
    }

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

    改了 3 个字符,10 倍的沙箱性能提升?!!

    确实会慢,但不多 🤪 qiankun2 自发布以来,常被人诟病慢、有性能问题。虽在大部分场景下,这个问题表现的并不明显,不会对应用造成可感知的影响(2m JS 的解析约增加 200ms 耗时,单个函数调用增加耗时可忽略不计)。大部分情况下应用渲染慢,真的就是因...
    继续阅读 »

    确实会慢,但不多 🤪


    qiankun2 自发布以来,常被人诟病慢、有性能问题。虽在大部分场景下,这个问题表现的并不明显,不会对应用造成可感知的影响(2m JS 的解析约增加 200ms 耗时,单个函数调用增加耗时可忽略不计)。大部分情况下应用渲染慢,真的就是因为你的 JS 太大(一个不分片的超大的 bundle),接口响应太长,UI 不够有「弹性」导致的。
    但在面临一些 CPU 密集型的 UI 操作时,如图表、超量 DOM 变更(1000以上)等场景,确实存在明显的卡顿现象。所以我们也不好反驳什么,通常的解决方案就是推荐用户关闭沙箱来提升性能。


    去年底我们曾尝试过一波优化,虽然略有成效,但整体优化幅度不大,因为有一些必要访问耗时省不掉,最终以失败告终。


    重启优化之路 😤


    近期有社区用户又提到了这个问题,加之年初的时候「获取」到了一些灵感,中秋假期在家决定对这个问题重新做一次尝试。
    我们知道 qiankun 的沙箱核心思路其实是这样的:

    const windowProxy = new Proxy(window, traps);

    with(windowProxy) {
    // 应用代码,通过 with 确保所有的全局变量的操作实际都是在操作 qiankun 提供的代理对象
    ${appCode}
    }

    此前主要的性能问题出在应用的代码会频繁的访问沙箱,比如 Symbol.unscopables 在图表场景很容易就达到千万级次的访问。
    优化的思路也很简单,就是要减少全局变量在 proxy 里的 lookup 次数。比如可以先缓存起来,后续访问直接走作用域里的缓存即可:

    const windowProxy = new Proxy(window, traps);

    with(windowProxy) {
    + // 提前将一些全局变量通过 赋值/取值 从 proxy 里缓存下来
    + var undefined = windowProxy.undefined; var Array = windowProxy.Array; var Promise = windowProxy.Promise;
    // 应用代码,通过 with 确保所有的全局变量的操作实际都是在操作 qiankun 提供的代理对象
    ${appCode}
    }

    看上去很完美,不过手上没有 windows 设备没法验证(M1性能太强测不出来),于是先提了个 pr


    验证 👻


    假期结束来公司,借了台 windows 设备,验证了一下。
    糟了,没效果。优化前跟优化后的速度几乎没有变化。🥲


    想了下觉得不应该啊,理论上来讲多少得有点作用才是,百思不得其解。


    苦恼之际,突然好像想到了什么,于是做出了下面的修改:

    const windowProxy = new Proxy(window, traps);

    with(windowProxy) {
    + // 提前将一些全局变量通过 赋值/取值 从 proxy 里缓存下来
    - var undefined = windowProxy.undefined; var Array = windowProxy.Array; var Promise = windowProxy.Promise;
    + const undefined = windowProxy.undefined; const Array = windowProxy.Array; const Promise = windowProxy.Promise;
    // 应用代码,通过 with 确保所有的全局变量的操作实际都是在操作 qiankun 提供的代理对象
    ${appCode}
    }

    改动更简单,就是将 var 声明换成了 const,立马保存验证一把。


    直接起飞!


    场景 1:vue 技术栈下大 checkbox 列表变更





    在有沙箱的情况下,耗时基本与原生框架的一致了。


    场景 2:10000 个 dom 插入/变更


    在 vue 的 event handler 中做原生的 10000 次 的 for 循环,然后插入/更新 10000 个 dom,记录中间的耗时:

    <template>
    <div>
    <ul>
    <li v-for="item in aaa" :key="item">{{ item }}</li>
    </ul>
    <button @click="test">test</button>
    </div>
    </template>

    <script>
    import logo from "@/assets/logo.png";
    export default {
    data() {
    return {
    aaa: 1
    };
    },
    methods: {
    test() {
    console.time("run loop", 10000);

    for (let index = 2; index < 1 * 10000; index++) {
    this.aaa = index;
    }

    console.timeLog("run loop", 10000);

    this.$nextTick(() => {
    // 10000 个 dom 更新完毕后触发
    console.timeEnd("run loop", 10000);
    });
    }
    }
    };
    </script>
     

     

    可以看到,这个优化后的提升已经不止 10 倍了,都超过 50 倍了,跟原生的表现基本无异。


    如何做到的 🧙


    完成最后的性能飞跃,实际上我只改了 3 个字符,就是把 with 里的 var 换成了 const,这是为什么呢?
    其实我之前的这篇文章早就告诉了我答案:
    ES 拾遗之 with 声明与 var 变量赋值
    里面有一个重要的结论:
    image.png
    因为 windowProxy 里有所有的全局变量,那么我们之前使用 var 去尝试做作用域缓存的方案其实是无效的,声明的变量实际还是在全局的词法环境中的,也就避免不了作用域链的查找。而换成 const,就可以顺利的将变量写到 with 下的词法环境了。


    one more thing 😂


    至此,如果以后你的应用在微前端场景下表现的不尽如人意,请先考虑:


    1. 是否是应用的打包策略不合理,导致 bundle 过大 js 执行耗时过长
    2. 是否是前置依赖逻辑过多执行过慢(如接口响应),阻塞了页面渲染
    3. 是否是微应用的加载策略不合理,导致过晚的加载
    4. 没有加载过渡动画,只有硬生生的白屏

    别再试图甩锅给微前端了,瑞思拜🫡。


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

    比 React 快 30%?Gyron 是怎么做到的。

    距离第一个试用版已经过去半年之久,想想就发了这篇文章,该文章起这种标题完全是被迫。之前也相继发表了一些相关的文章但是阅读量和点赞量寥寥无几,这种标题我也是看了一些自媒体发表的文章后学习而来。我也在尝试编写一些高质量的文章,如果你看到这篇文章请嘴下留情。 响应式...
    继续阅读 »

    距离第一个试用版已经过去半年之久,想想就发了这篇文章,该文章起这种标题完全是被迫。之前也相继发表了一些相关的文章但是阅读量和点赞量寥寥无几,这种标题我也是看了一些自媒体发表的文章后学习而来。我也在尝试编写一些高质量的文章,如果你看到这篇文章请嘴下留情。


    响应式


    Gyron.js是一款零依赖的响应式框架,和社区中大多数响应式方案一样,Gyron.js也是利用Proxy提供的能力进行的依赖收集和更新,理论上来说只要支持Proxy能力的环境都支持这套响应式的逻辑,只是需要修改(接入)对应的渲染部分。这里也要感谢 Vue 实现的响应式依赖更新方案,我们基本上参考着这套方案实现了自己的 API。


    响应式核心部分就是 effect 结构,基本上所有提供出来的 api 都是在 effect 之上实现的,所以我们先来介绍 effect 这个数据结构。


    我们先来看一眼 effect 长什么样子

    export type Dep = Set<Effect>;

    export type EffectScheduler = (...args: any[]) => any;

    export type Noop = () => void;

    export interface Effect {
    // A 等于 self effect
    // B 等于 other effect
    // deps 是一个 Set 结构的 Effect ,用于存放 A 依赖的 B
    // 当A不再使用时需要清除所有依赖 A 的 B
    deps: Dep[];
    // 在边界情况下强制执行依赖的B
    allowEffect: boolean;
    // 在首次执行时收集依赖的函数
    scheduler: EffectScheduler | null;
    // 存放所有手动设置的依赖
    wrapper: Noop;
    // 当需要更新时执行的函数
    run: Noop;
    // 当不再使用时清除依赖的B
    stop: Noop;
    }

    effect 中有很多属性,我也在其中注释了每个属性的作用。每个属性都有自己的应用场景,比如 deps 就是用于卸载组件后清除所有组件依赖的数据,避免数据更新后组件被卸载,导致组件更新异常。其实响应式核心分为两个部分,第一个部分就是依赖收集,第二个部分就是响应更新,我们先来看看下面这张图。




    上面这张图就是说明两个不同的数据之间的依赖关系,第一块(左上角)表明 variable proxy 2 依赖 variable proxy 1,在 variable proxy 2 中访问 variable proxy 1 时,会触发 variable proxy 1 的自动收集任务,当 variable proxy 1 的值更新后会触发依赖 variable proxy 2 的任务,也就是 run 或者 scheduler。那么我们是通过什么将这两个变量关联在一起的呢?我们引入了一个WeakMap数据effectTracks,用变量作为一个 key 值,然后在变更 variable proxy 1 时从这个模块变量中找到依赖再做更新,也就是图中右边部分。至此,依赖收集和依赖更新都已经完成,接下来,如何对应到组件上面呢?


    上面我们介绍了两个响应式变量是如何完成这一整套响应式方案,那么我们把上述的变量变更为组件可不可以呢?答案是可以的。组件是什么?在 Gyron.js 中组件就是一个函数,一个被内装函数包装的函数。那么,组件在初次渲染时如何进行依赖收集呢?在讲解组件的依赖收集之前,我们先讲一讲另外一个模块变量activeEffect,这个变量主要用于组件初次渲染时保存组件的 effect 对象,然后在响应式数据 track 时,获取到组件的 effect 对象保存在上面讲的effectTracks模块变量中,在响应式数据发生变更后触发组件 effect 的 update 方法(也就是 run)来更新组件。这里值得一提的是,所有的更新我们全部都是异步,并且是可选支持中断继续模式的,这部分内容我们接下来再进行介绍。


    好了,响应式的核心内容其实并不多,其实如何实现只是冰山一角,最主要的是其中的想法可以应用在你的业务之中,尽量少写一些恶心同事的代码。


    任务调度


    上面我们讲解了Gyron.js是如何做到响应式更新的,接下来我们说一说多个组件同时更新应该怎么处理呢?如果组件更新阻止了用户操作应该怎么办呢?如何在组件中拿到更新后的DOM呢?这些问题在平时开发中相信大多数开发中都遇到过,其实这些问题可以在编写业务代码时去进行优化,但是不怎么优雅,也可能会导致代码不可读,比如


    获取更新后的 DOM

    // 获取更新后的 D<DOM>
    任务: [update component A, update component B, [update component C, [update component D]]]
    等待任务更新完成: A、B、C、D
    获取组件D更新后的DOM

    为了提高开发效率和用户体验,开发者可以合理选择使用哪种模式更新组件。具体有哪些模式可以选择呢?答案是两种,第一种就是默认模式,组件更新全部在异步队列中完成。第二种模式就是在异步队列中的任务会受到外部状态控制。接下来我们分开讲一讲这两种模式。


    第一种模式,我们可以使用Gyron.js暴露的FC方法定义组件,然后正常使用 JSX 去描述 UI,在组件更新时通过暴露的nextRender获取到更新后的 DOM。这是一个很常见的实现方式,这也和 Vue 的nextTick一样。我们重点讲讲另外一种更新模式。


    延迟更新:组件更新的前面几步都是一样,有一个异步队列,但是延迟更新模式中有一个priority属性,当组件 effect 拥有这种属性的时候会自动根据这批组件的更新时间,或者用户操作来中断队列中后续任务的更新,当浏览器告诉我们,现在有空闲了可以继续任务时再继续未更新的任务。其实这种模式还可以更进一步,设定一个冷却时间,在冷却时间内再次发现相同的任务直接抛弃上一次相同的任务(根据任务 ID 来区分),这样做可以减少浏览器开销,因为这些任务在下一个周期中肯定会被覆盖。我们有计划的去实现这个内容,但不是现在。


    第二种模式的实现完全得益于浏览器提供的 API,让这种模式实现变为可能,也让用户体验得到提升。


    其实第二种模式后面的理念可以用在一些大型的编辑场景和文档协作中以此来提升用户体验,这也是在研究 React 的调度任务之后得出的结论。


    所以,有人在反驳说看这些源码时没用时可以硬气的告诉他们,看完之后学到了什么。(不过不要盲目的去看,针对具体的问题去研究和借鉴)


    复合直观的


    如果你是一个 React 的用户,你会发现函数式组件的状态心智负担太高,不符合直觉,直接劝退新手。那么,什么是符合直观的代码?当我的组件依赖更新后组件的内容发生响应的更新即可,这也是响应式的核心。在Gyron.js中编写一个简单的响应式组件会如此简单。

    import { FC, useValue } from "gyron";

    interface HelloProps {
    initialCount: number;
    }

    const Hello = FC<HelloProps>(({ initialCount = 0 }) => {
    const count = useValue(initialCount);
    const onClick = () => count.value++;

    // Render JSX
    return <div onClick={onClick}>{count.value}</div>;
    });

    上面定义了一个 Hello 的组件,这个组件接受一个参数 initialCount,类型为数字,组件的功能也很简单,当用户点击这个数字然后自增一。而如果要用 React 去实现或者 Vue 去实现这样一个功能,我们应该怎么做呢?


    我们用 Vue 去实现一个一样的组件,代码如下(使用了 setup 语法)

    <script lang="ts" setup>
    import { ref } from "vue";

    const props = withDefaults(
    defineProps<{
    initialCount: number;
    }>(),
    {
    initialCount: 0,
    }
    );

    const count = ref(props.initialCount);
    function onClick() {
    count.value++;
    }
    </script>

    <template>
    <div @click="onClick">{{ count }}</div>
    </template>

    那么我们用 React 也去实现一个一样的组件,代码如下

    import { useState, useCallback } from "react";

    export const Hello = ({ initialCount = 0 }) => {
    const [count, setCount] = useState(initialCount);

    const onClick = useCallback(() => {
    setCount(count + 1);
    }, [count, setCount]);

    console.log("refresh"); // 每点击一次都会打印一次

    return <div onClick={onClick}>{count}</div>;
    };

    好了,上面是不同框架实现的 Hello 组件。这里并不是说其它框架不好,只是我认为在表达上有一些欠缺。Vue2 中需要理解 this,并且没办法让 this 稳定下来,因为它可以在任何地方修改然后还无法被追踪,在 Vue3 中需要理解 setup 和 template 之间的关系,然后实现类型推断需要了解 defineXXX 这种 API。在 React 中想要更新组件需要注意 React 更新机制,比如内部状态何时才是预期的值,在遇到复杂的组件时这往往比较考验开发者的编码水平。


    以上,Gyron.js是如何解决这些问题的呢?其实,这完全得益于 babel 的强大能力,让开发者不需要知道编译构建优化的知识也能介入其中,改变源码并能重新构建。如果想了解其中的用法可以去 babel 官网plugin 页面


    然后,Gyron.js是如何解决上面提到的问题?我们以上面编写的一个简单组件 Hello 为例,介绍其中到底发生了什么。
    首先,我们的组件用 FC 函数进行了一个包装,这里 FC 就好比一个标识符,在 AST 中属于 BinaryExpression 节点,然后函数体的返回值就是JSX.Element。我们有了这个规则,然后在 babel 中就可以根据这个规则定位到组件本身,再做修改。为了解决重复渲染的问题,我们需要把返回值做一些修改,把JSX.Element用一个函数进行包裹再进行返回。具体转换如下:

    const Hello = FC(({ numbers }) => {
    return <div>{numbers}</div>;
    });
    // ↓ ↓ ↓ ↓ ↓
    const Hello = FC(({ numbers }) => {
    return ({ numbers }) => <div>{numbers}</div>;
    });


    名词解释

    组件函数:我们熟知的 JSX 组件

    渲染函数:转换后的 JSX 函数,用于标记哪些部分是渲染部分,哪些是逻辑部分。类似于 Vue3 的 setup 和 render 的区别。



    这是一个最简单的转换,但是这又引入了另外几个问题。第一,在JSX.Element中的元素内容是组件的参数,但是在下次渲染时取到的是顶层函数中的numbers,为了解决这个问题,我们将顶层函数中的第一个参数作为渲染函数中的第一个参数,然后在渲染函数中访问到的状态就是最新状态。


    这其中还有一个问题,我在组件函数中访问 props 状态也无法保证是最新的,这时候就需要使用Gyron.js提供的onBeforeUpdate方法,这个方法会在组件更新之前调用,然后我们需要把组件函数中定义的 props 全部放进这个函数中,然后根据函数的 new props 去更新用户定义的 props。但是真实的使用场景比较复杂,比如可以这样定义({ a, ...b }) => {},将 props 的 a 单独拎出来,然后其余部分全部归纳到 b 中。


    举一个简单的例子:

    const Hello = FC(({ numbers }) => {
    function transform() {
    return numbers;
    }
    return <div>{transform()}</div>;
    });
    // ↓ ↓ ↓ ↓ ↓
    import { onBeforeUpdate as _onBeforeUpdate } from "gyron";
    const Hello = FC(({ numbers }) => {
    _onBeforeUpdate((_, props) => {
    var _props = props;
    numbers = _props.numbers;
    });
    function transform() {
    return numbers;
    }
    return <div>{transform()}</div>;
    });

    可以看到转换后的组件中多出了一个_onBeforeUpdate方法调用,其作用就是更新组件函数作用域中的 props。



    小结:为了让用户在开发中编写符合直观的代码,Gyron.js在背后做了很多事情。这其实也是 babel 在实际项目中的一种使用方法。



    极快的 hmr


    hmr(hot module replacement )就是模块的热更新,其实这部分功能都是编译工具提供,我们只需要按照他们提供的 API 然后更新我们的组件。

    if (module.hot) {
    module.hot.accept("./hello.jsx", function (Comp1) {
    rerender("HashId", Comp1);
    });
    }

    以上代码我们的插件会自动插入,无需手动引入。(目前还只接入 vite,后续有计划的支持 webpack 等有 hot 模块的工具)


    我们大致了解一下这其中发生了什么?首先,我们还是借助 babel 把每一个组件的名字和内容生成的 hash 值作为注释节点存放在模块中,然后通过编译工具获取到所有本模块的组件,然后通过注册 hot 事件重新渲染更新后的组件。


    好了,讲解了编译工具提供的功能,这里着重讲解一下Gyron.js是如何做到重新渲染的。首先,我们通过编译工具获取到了组件 Hash 和组件函数,然后通过rerender函数执行重新渲染。那么rerender所需要的数据又是从哪里来的呢?其实,在实例第一次初始化的时候这个数据全部都收集到一个Map<string, Set<Component>>数据结构中,然后再通过Component上的 update 方法执行组件的更新。


    SEO 友好


    其实这段内容和Gyron.js本身关系不太大,但是没有Gyron.js提供的能力也很难办到。Gyron.js提供了 SSR(Server Side Render)的渲染模式,也就是我们熟知的服务端渲染。其中大致的原理就是服务端将实例渲染成字符串之后返回给浏览器,然后再通过客户端的hydrate功能让“静态”文本变的可响应。


    以上是简单的用法,然后大致流程图如下所示:




    为了让组件变得更通用,我们在所有组件的 props 上注入了一个变量告诉开发者当前处于何种模式的渲染当中,在服务端渲染当中时不能使用客户端提供的 API,在客户端渲染的过程中不能使用服务端的 API。

    const App = ({ isSSR }) => {
    // ...
    if (!isSSR) {
    document.title = "欢迎";
    }
    };
    import { strict as assert } from "node:assert";
    const App = ({ isSSR }) => {
    // ...
    if (isSSR) {
    assert.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, "3"]], 4, 5]);
    }
    };

    这是服务端渲染的方式,还有一种介于服务端渲染和客户端渲染之间,就是完全输出静态资源然后就可以部署到任何机器或者在线平台服务商中,比如app.netlify.comgithub.com等。这里不再介绍 SSR 模式的使用方法,可以去gyron.cc/docs/ssr这里有更详细的介绍。


    所见即所得



    这里介绍的是官方文档中的在线编辑器,相比于接入其它平台,我们占用的资源更少,功能齐全。



    经过一段时间的折腾,终于弄出一个简单版的在线编辑器,支持实时预览、语法纠错、语法高亮、智能跳转等功能。


    语言的话目前支持 jsx、tsx、less,并且还支持加载在线资源,比如import { h } from 'https://cdn.jsdelivr.net/npm/gyron'。因为所有数据都不保存在远端,只保存在本地,所以没有使用 standalone 沙盒技术隔离运行环境,也没有防范 xss 攻击。在线编辑器的目标就是让用户可以在线使用,支持用户编辑源代码,支持本地模块导入,支持实时预览,支持多个编辑器运行互不干扰。


    目前这个编辑器支持本地编译和服务端编译,本地编译会让首屏加载变慢所以在线编辑器使用的服务端编译。现在,可以访问gyron.cc/explorer这个地址在线体验。


    如果使用本地编译,这里面最终的就是需要实现一套本地的虚拟文件系统,让打包工具能够正常访问到对应的本地资源。而在 esbuild 中实现一套虚拟文件系统其实很简单,只需要编写一个插件,然后用 resolve 和 load 两种勾子就可以将本地文件输出到 esbuild 中。

    const buildModuleRuntime = {
    name: "buildModuleRuntime",
    setup(build) {
    build.onResolve({ filter: /\.\// }, (args) => {
    return {
    path: args.path,
    namespace: "localModule",
    };
    });
    build.onLoad({ filter: /\.\//, namespace: "localModule" }, async (args) => {
    // 具体实现可以去github https://github.com/gyronorg/core/blob/main/packages/babel-plugin-jsx/src/browser.ts
    const source = findSourceCode(config.sources, args.path);

    if (source) {
    const filename = getFileName(args, source.loader);
    const result = await transformWithBabel(
    source.code,
    filename,
    main,
    true
    );
    return {
    contents: result.code,
    };
    }
    return {
    contents: "",
    loader: "text",
    warnings: [
    {
    pluginName: "buildModuleRuntime",
    text: `Module "${args.path}" is not defined in the local editor`,
    },
    ],
    };
    });
    },
    };

    然后会输出一个 module 文件,最终只需要将文件塞到 script 中让其运行。


    在页面中引用多个编辑器,需要注意的是在不用这个 module 文件后及时删除。可以使用命名空间给 module 加上一个标签,新增和删除都使用这个命名空间作为变量控制当前运行时的资源。


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

    停止编写 API 函数

    如果你正在开发一个前端程序,后端使用的 RESTFUL API,那你必须停止为每一个接口编写函数。 RESTFUL API 通常提供在不同实体上执行增删改查(CRUD)操作的一组接口。我们通常在我们的前端项目中为这些每一个接口提供一个函数,这些函数的功能非常的...
    继续阅读 »

    如果你正在开发一个前端程序,后端使用的 RESTFUL API,那你必须停止为每一个接口编写函数。


    RESTFUL API 通常提供在不同实体上执行增删改查(CRUD)操作的一组接口。我们通常在我们的前端项目中为这些每一个接口提供一个函数,这些函数的功能非常的相似,只是为了服务于不用的实体。举个例子,假设我们有这些函数。

    // api/users.js

    // 创建
    export function createUser(userFormValues) {
    return fetch('users', { method: 'POST', body: userFormValues });
    }

    // 查询
    export function getListOfUsers(keyword) {
    return fetch(`/users?keyword=${keyword}`);
    }

    export function getUser(id) {
    return fetch(`/users/${id}`);
    }

    // 更新
    export updateUser(id, userFormValues) {
    return fetch(`/users/${is}`, { method: 'PUT', body: userFormValues });
    }

    // 删除
    export function removeUser(id) {
    return fetch(`/users/${id}`, { method: 'DELETE' });
    }

    类似的功能可能存在于其他实体,例如:城市、产品、类别...但是我们可以用一个简单的函数调用来代替这些函数:

    // apis/users.js
    export const users = crudBuilder('/users');

    // apis/cities.js
    export const cities = crudBuilder('/regions/cities');


    然后像这样去使用:

    users.create(values);
    users.show(1);
    users.list('john');
    users.update(values);
    users.remove(1);

    你可能会问为什么?有一些很好的理由:


    • 减少了代码行数:你编写的代码,和当你离开公司时其他人维护的代码
    • 强制执行 API 函数的命名约定,这可以增加代码的可读性和可维护性。例如你已经见过的函数名称: getListOfUsersgetCitiesgetAllProductsproductIndexfetchCategories等, 他们都在做相同的事情,那就是“获取实体列表”。使用这种方法,你将始终拥有entityName.list()函数,并且团队中的每个人都知道这一点。

    所以,让我们创建crudBuilder()函数,然后再添加一些糖。


    一个非常简单的 CRUD 构造器


    对于上边的简单示例,crudBuilder()函数将非常简单:

    export function crudBuilder(baseRoute) {
    function list(keyword) {
    return fetch(`${baseRoute}?keyword=${keyword}`);
    }
    function show(id) {
    return fetch(`${baseRoute}/${id}`);
    }
    function create(formValues) {
    return fetch(baseRoute, { method: 'POST', body: formValues });
    }
    function update(id, formValues) {
    return fetch(`${baseRoute}/${id}`, { method: 'PUT', body: formValues });
    }
    function remove(id) {
    return fetch(`${baseRoute}/${id}`, { method: 'DELETE' });
    }

    return {
    list,
    show,
    create,
    update,
    remove
    };
    }

    假设约定 API 路径并且给相应实体提供一个路径前缀,他将返回该实体上调用 CRUD 操作所需的所有方法。


    但老实说,我们知道现实世界的应用程序并不会那么简单。在将这种方法应用于我们的项目时,有很多事情需要考虑:


    • 过滤:列表 API 通常会提供许多过滤器参数
    • 分页:列表 API 总是分页的
    • 转换:API 返回的值在实际使用之前可能需要进行一些转换
    • 准备:formValues对象在发送给 API 之前需要做一些准备工作
    • 自定义接口:更新特定项的接口不总是${baseRoute}/${id}

    因此,我们需要可以处理更多复杂场景的 CRUD 构造器。


    高级 CRUD 构造器


    让我们通过上述方法来构建一些日常中我们真正使用的东西。


    过滤


    首先,我们应该在 list输出函数中处理更加复杂的过滤。每个实体列表可能有不同的过滤器并且用户可能应用了其中的一些过滤器。因此,我们不能对应用过滤器的形状和值有任何假设,但是我们可以假设任何列表过滤都可以产生一个对象,该对象为不同的过滤器名称指定了一些值。例如,我们可以过滤一些用户:

    const filters = {
    keyword: 'john',
    createdAt: new Date('2020-02-10')
    };

    另一方面,我们不知道这些过滤器应该如何传递给 API,但是我们可以假设(跟 API 提供方进行约定)每一个过滤器在列表 API 中都有一个相应的参数,可以以'key=value'URL 查询参数的形式被传递。


    因此我们需要知道如何将应用的过滤器转换成相对应的 API 参数来创建我们的 list 函数。这可以通过将 transformFilters 参数传递给 crudBuilder() 来完成。举一个用户的例子:

    function transformUserFilters(filters) {
    const params = [];
    if (filters.keyword) {
    params.push(`keyword=${filters.keyword}`);
    }
    if (filters.createdAt) {
    params.push(`create_at=${dateUtility.format(filters.createdAt)}`);
    }

    return params;
    }

    现在我们可以使用这个参数来创建 list 函数了。

    export function crudBuilder(baseRoute, transformFilters) {
    function list(filters) {
    let params = transformFilters(filters)?.join('&');
    if (params) {
    params += '?';
    }

    return fetch(`${baseRoute}${params}`);
    }
    }

    转换和分页


    从 API 接收的数据可能需要进行一些转换才能在我们的应用程序中使用。例如,我们可能需要将 snake_case 转换成驼峰命名或将一些日期字符串转换成用户时区。


    此外,我们还需要处理分页。


    我们假设来自 API 的分页数据都按照如下格式(与 API 提供者约定):

    {
    data: [], // 实体对象列表
    pagination: {...} // 分页信息
    }

    因此,我们需要知道如何转换单个实体对象。然后我们可以遍历列表对象来转换他们。为此,我们需要一个 transformEntity 函数作为 crudBuilder 的参数。

    export function crudBuilder(baseRoute, transformFilters, transformEntity, ) {
    function list(filters) {
    const params = transformFilters(filters)?.join('&');
    return fetch(`${baseRoute}?${params}`)
    .then((res) => res.json())
    .then((res) => ({
    data: res.data.map((entity) => transformEntity(entity)),
    pagination: res.pagination
    }));
    }
    }

    list() 函数我们就完成了。


    准备


    对于 createupdate 函数,我们需要将 formValues 转换成 API 需要的格式。例如,假设我们在表单中有一个 City 的城市选择对象。但是 create API 只需要 city_id。因此,我们需要一个执行以下操作的函数:

    const prepareValue = formValue => ({city_id: formValues.city.id});

    这个函数会根据用例返回普通对象或者 FormData,并且可以将数据传递给 API:

    export function crudBuilder(baseRoute, transformFilters, transformEntity, prepareFormValues) {
    function create(formValues) {
    return fetch(baseRoute, {
    method: 'POST',
    body: prepareFormValues(formValues)
    });
    }
    }

    自定义接口


    在一些少数情况下,对实体执行某些操作的 API 接口不遵循相同的约定。例如,我们不能使用 /users/${id} 来编辑用户,而是使用 /edit-user/${id}。对于这些情况,我们应该指定一个自定义路径。


    在这里我们允许覆盖 crud builder 中使用的任何路径。注意,展示、更新、移除操作的路径可能取决于具体实体对象的信息,因此我们必须使用函数并传递实体对象来获取路径。


    我们需要在对象中获取这些自定义路径,如果没有指定,就退回到默认路径。像这样:

    const paths = {
    list: 'list-of-users',
    show: (userId) => `users/with/id/${userId}`,
    create: 'users/new',
    update: (user) => `users/update/${user.id}`,
    remove: (user) => `delete-user/${user.id}`
    };

    最终的 BRUD 构造器


    这是创建 CRUD 函数的最终代码。

    export function crudBuilder(baseRoute, transformFilters, transformEntity, prepareFormValues, paths) {
    function list (filters) {
    const path = paths.list || baseRoute;
    let params = transformFilters(filters)?.join('&');
    if (params) {
    params += '?';
    }

    return fetch(`${path}${params}`)
    .then((res) => res.json())
    .then(() => ({
    data: res.data.map(entity => transformEntity(entity)),
    pagination: res.pagination
    }));
    }
    function show(id) {
    const path = paths.show?.(id) || `${baseRoute}/${id}`;

    return fetch(path)
    .then((res) => res.json())
    .then((res => transformEntity(res)));
    }
    function create(formValues) {
    const path = paths.create || baseRoute;

    return fetch(path, { method: 'POST', body: prepareFormValues(formValues) });
    }
    function update(id, formValues) {
    const path = paths.update?.(id) || `${baseRoute}/${id}`;

    return fetch(path, { method: 'PUT', body: formValues });
    }
    function remove(id) {
    const path = paths.remove?.(id) || `${baseRoute}/${id}`;

    return fetch(path, { method: 'DELETE' });
    }
    return {
    list,
    show,
    create,
    update,
    remove
    }
    }


    Saeed Mosavat: Stop writing API functions


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

    实现滚动点赞墙

    web
    需要实现的效果如下: 需要将用户点赞的信息,一条一条的展示在页面顶部,这样的效果有多种实现方式,下面一一来了解一下吧~ 纯css实现 scss如下:(如果要将scss改为less,将$改为@就可以了) 当移动到第8行结束的时候,同屏出现的两行(第9行和第1...
    继续阅读 »

    需要实现的效果如下:



    需要将用户点赞的信息,一条一条的展示在页面顶部,这样的效果有多种实现方式,下面一一来了解一下吧~


    纯css实现



    scss如下:(如果要将scss改为less,将$改为@就可以了)


    当移动到第8行结束的时候,同屏出现的两行(第9行和第10行),就需要结束循环,重头开始了


    这是一个上移的动画,动画执行的时间就是8s


    itemShowTime+(itemShowTime + (oneCycleItemNum - oneScreenItemNum)(oneScreenItemNum) * (itemShowTime / $oneScreenItemNum)


    $itemHeight: 60px; // 单个item的高度

    $itemShowTime: 2s; // 单个item从完整出现到消失的时长
    $oneCycleItemNum: 8; // 单个循环上移的item条数
    $oneScreenItemNum: 2; // 同屏出现的item条数(不能大于 oneCycleItemNum)

    $oneCycleItemTime: calc($itemShowTime + ($oneCycleItemNum - $oneScreenItemNum) * ($itemShowTime / $oneScreenItemNum));

    @keyframes dynamics-rolling {
    from {
    transform: translateY(0);
    }
    to {
    transform: translateY(-$itemHeight * $oneCycleItemNum);
    }
    }
    .container {
    height: 600px;

    animation: dynamics-rolling $oneCycleItemTime linear infinite;
    .div {
    line-height: 60px;
    }
    }

    .visibleView {
    width: 100%;
    height: 120px;
    overflow: hidden;
    background-color: skyblue;

    }
    .box {
    width: 100%;
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    }

    简单的demo:



    import React, { useEffect, useRef, useState } from 'react'
    import styles from './index.module.scss'

    const dataSource = new Array(50).fill(0).map((_, index) => index + 1)

    export default function CycleScrollList() {
    const [data, setData] = useState(dataSource.slice(0, 10))

    return (
    <div className={styles.box}>
    <div className={styles.visibleView}>
    <div className={styles.container}>
    {
    data.map((item, index) => (
    <div key={ index } className={styles.div}>{ item }</div>
    ))
    }
    </div>
    </div>
    </div>

    )
    }

    setInterval监听


    css动画是定时的,所以可以定时更新列表内容,但是会有很明显的抖动,效果不太友好,应该是定时器的时间还不能太准




    import React, { useEffect, useRef, useState } from 'react'
    import styles from './index.module.scss'

    const dataSource = new Array(50).fill(0).map((_, index) => index + 1)


    export default function CycleScrollList() {
    const [data, setData] = useState(dataSource.slice(0, 10))
    const nextIndex = useRef(10) // 持续从 dataSource 拿数据的下一个 index

    useEffect(() => {
    const timer = setInterval(() => {
    replaceData()
    },4900)

    return () => {
    clearInterval(timer)
    }
    }, [])


    const replaceData = () => {
    let newData = []
    if (nextIndex.current-5 < 0) {
    newData = [...dataSource.slice(nextIndex.current-5) ,...dataSource.slice(0, nextIndex.current + 5)]
    } else {
    newData = [...dataSource.slice(nextIndex.current-5, nextIndex.current + 5)]
    }
    // 使用当前的后半份数据,再从 dataSource 中拿新数据
    console.log(newData)
    const nextIndexTemp = nextIndex.current + 5
    const diff = nextIndexTemp - dataSource.length
    if (diff < 0) {
    nextIndex.current = nextIndexTemp
    } else {
    // 一轮数据用完,从头继续
    nextIndex.current = diff
    }
    setData(newData)
    }

    return (
    <div className={styles.box}>
    <div className={styles.visibleView}>
    <div className={styles.container}>
    {
    data.map((item, index) => (
    <div key={ index } className={styles.div}>{ item }</div>
    ))
    }
    </div>
    </div>
    </div>

    )
    }

    IntersectionObserver监听


    监听第5个元素


    如果第五个元素可见了,意味着不可见时,需要更换数据了


    如果第五个元素不可见了,立刻替换数据


    替换的数据如下:



    使用IntersectionObserver监听元素,注意页面卸载时,需要去除绑定


    tsx如下:



    import React, { useEffect, useRef, useState } from 'react'
    import styles from './index.module.scss'

    const dataSource = new Array(50).fill(0).map((_, index) => index + 1)
    const ITEM_5_ID = 'item-5'

    export default function CycleScrollList() {
    const [data, setData] = useState(dataSource.slice(0, 10))

    const intersectionObserverRef = useRef<IntersectionObserver | null>()
    const item5Ref = useRef<HTMLDivElement | null>(null)

    const nextIndex = useRef(10) // 持续从 dataSource 拿数据的下一个 index
    const justVisible5 = useRef<boolean>(false) // 原来是否为可视

    useEffect(() => {
    intersectionObserverRef.current = new IntersectionObserver((entries) => {
    entries.forEach((item) => {
    if (item.target.id === ITEM_5_ID) {
    // 与视图相交(开始出现)
    if (item.isIntersecting) {
    justVisible5.current = true
    }
    // 从可视变为不可视
    else if (justVisible5.current) {
    replaceData()
    justVisible5.current = false
    }
    }
    })
    })
    startObserver()

    return () => {
    intersectionObserverRef.current?.disconnect()
    intersectionObserverRef.current = null
    }
    }, [])

    const startObserver = () => {
    if (item5Ref.current) {
    // 对第五个 item 进行监测
    intersectionObserverRef.current?.observe(item5Ref.current)
    }
    }

    const replaceData = () => {
    let newData = []
    if (nextIndex.current-5 < 0) {
    newData = [...dataSource.slice(nextIndex.current-5) ,...dataSource.slice(0, nextIndex.current + 5)]
    } else {
    newData = [...dataSource.slice(nextIndex.current-5, nextIndex.current + 5)]
    }
    // 使用当前的后半份数据,再从 dataSource 中拿新数据
    console.log(newData)
    const nextIndexTemp = nextIndex.current + 5
    const diff = nextIndexTemp - dataSource.length
    if (diff < 0) {
    nextIndex.current = nextIndexTemp
    } else {
    // 一轮数据用完,从头继续
    nextIndex.current = diff
    }
    setData(newData)
    }

    return (
    <div className={styles.box}>
    <div className={styles.visibleView}>
    <div className={styles.container}>
    {
    data.map((item, index) => (
    index === 4 ?
    <div id={ ITEM_5_ID } ref={ item5Ref } key={ index } className={styles.div}>{ item }</div>
    :
    <div key={ index } className={styles.div}>{ item }</div>
    ))
    }
    </div>
    </div>
    </div>

    )
    }

    scss样式


    $itemHeight: 60px; // 单个item的高度

    $itemShowTime: 3s; // 单个item从完整出现到消失的时长
    $oneCycleItemNum: 5; // 单个循环上移的item条数
    $oneScreenItemNum: 3; // 同屏出现的item条数(不能大于 oneCycleItemNum)

    $oneCycleItemTime: calc($itemShowTime + ($oneCycleItemNum - $oneScreenItemNum) * ($itemShowTime / $oneScreenItemNum));

    @keyframes dynamics-rolling {
    from {
    transform: translateY(0);
    }
    to {
    transform: translateY(-$itemHeight * $oneCycleItemNum);
    }
    }
    .container {
    height: 600px;

    animation: dynamics-rolling $oneCycleItemTime linear infinite;
    .div {
    line-height: 60px;
    }
    }

    .visibleView {
    width: 100%;
    height: 120px;
    overflow: hidden;
    background-color: skyblue;

    }
    .box {
    width: 100%;
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    }

    作者:0522Skylar
    来源:juejin.cn/post/7278244755825442853
    收起阅读 »

    用了策略模式之后,再也不用写那么多 if else 了,真香!

    web
    前言 从我个人理解来看,设计模式其实就藏在我们平时的代码中,只是有人把它们提、炼出来,赋予了一些专业的名词和定义,下面给大家介绍一个日常项目开发中非常实用的设计模式,也就是策略模式。 策略模式的定义 先来看下策略模式的定义:定义一系列的算法,把它们一个个封装起...
    继续阅读 »

    前言


    从我个人理解来看,设计模式其实就藏在我们平时的代码中,只是有人把它们提、炼出来,赋予了一些专业的名词和定义,下面给大家介绍一个日常项目开发中非常实用的设计模式,也就是策略模式


    策略模式的定义


    先来看下策略模式的定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。


    简单来说就是有多种选择,然后一般只会选择一种。从代码的角度来说就是,定义一系列的ifelseif,然后只会命中其中一个。


    举个例子


    话不多说,直接来看例子,比如我们需要计算员工工资,员工工资计算规则如下:



    • 高级工:时薪为25块/小时

    • 中级工:时薪为20块/小时

    • 初级工:时薪为15块/小时


    按每天10小时的工作时长来算。


    一、第一版实现:


    const calculateSalary = function (workerLevel, workHours = 10) {
    if (workerLevel === 'high') {
    return workHours * 25
    }
    if (workerLevel === 'middle') {
    return workHours * 20
    }
    if (workerLevel === 'low') {
    return workHours * 15
    }
    }
    console.log(calculateSalary('high')) // 250
    console.log(calculateSalary('middle')) // 200

    这段代码具有明显的缺点:



    • calculateSalary函数庞大,有许多的if else语句,这些语言需要覆盖所有的逻辑分支

    • calculateSalary函数缺乏弹性,如果新增一种员工等级higher,需要修改calculateSalary函数的内部实现,违反开放——封闭原则

    • 算法的复用性差


    二、第二版实现(函数组合):


    当然,我们可以使用函数组合的方式重构代码,把每一个if中的逻辑单独抽离成一个函数。


    const workerLevelHigh = function (workHours) {
    return workHours * 25
    }

    const workerLevelMiddle = function (workHours) {
    return workHours * 20
    }
    const workerLevelLow = function (workHours) {
    return workHours * 15
    }

    const calculateSalary = function (workerLevel, workHours = 10) {
    if (workerLevel === 'high') {
    return workerLevelHigh(workHours)
    }
    if (workerLevel === 'middle') {
    return workerLevelMiddle(workHours)
    }
    if (workerLevel === 'low') {
    return workerLevelLow(workHours)
    }
    }
    console.log(calculateSalary('high', 10)) // 250
    console.log(calculateSalary('middle', 10)) // 200

    这样会提高算法的复用性,但这种改善十分有限,calculateSalary函数依旧庞大和缺乏弹性。


    三、第三版实现(策略模式):


    我们可以把不变的部分和变化的部分拆分开来。



    • 不变的部分:算法的使用方式不变,都是根据某个算法取得计算后的工资数额;

    • 变化的部分:算法的实现。


    我们js的对象是key value的形式,这可以帮助我们天然的替换掉if else


    因此,我们可以定义对象的两部分:



    • 针对变化的部分,我们可以定义一个策略对象,它封装了具体的算法,负责具体的计算过程

    • 针对不变的部分,我们提供一个Context函数,它接受客户的请求,随后把请求委托给策略对象。


    const strategies = {
    "high": function (workHours) {
    return workHours * 25
    },
    "middle": function (workHours) {
    return workHours * 20
    },
    "low": function (workHours) {
    return workHours * 15
    },
    }

    const calculateSalary = function (workerLevel, workHours) {
    return strategies[workerLevel](workHours)
    }
    console.log(calculateSalary('high', 10)) // 250
    console.log(calculateSalary('middle', 10)) // 200

    策略模式的优缺点


    从我个人在实际项目中的使用来看,策略模式的优缺点如下:


    优点:



    • 代码复杂度降低:再也不用写那么多if else了。eslint其中有一项规则配置叫圈复杂度,其中一条分支也就是一个if会让圈复杂增加1,圈复杂度高的代码不易阅读和维护,用策略模式就能很好的解决这个问题;

    • 易于切换、理解和扩展:它将算法封装在独立的strategy中,比如你要在上面代码中加一个等级higherlower,直接更改策略对象strategies就行,十分方便。

    • 复用性高:策略模式中的算法可以复用在系统的其它地方,你只需要用将策略类strategies用export或者module.exports导出,就能在其他地方很方便的复用。


    缺点:



    • 增加使用者使用负担:因为大量运用策略模式会在实际项目中堆砌很多策略类或者策略对象,这样项目的新人如果不熟悉这些策略类和策略对象,会增加他们的使用成本和学习成本,前期来说会比看if else更加难懂。


    小结


    以上就是我个人对策略模式的解读和了解啦,实际上项目中用策略模式的场景还是挺多的,因为在写业务代码中,很容易写出大量的if else,这时候就可以封装为策略模式,方便项目维护和扩展,从我个人的使用体验来看,还是相当香的。


    大家喜欢在实际项目使用策略模式么,欢迎留言和讨论~


    作者:han_
    来源:juejin.cn/post/7279041076273610764
    收起阅读 »

    八百年不面试,一面试就面得一塌糊涂

    web
    前言 好久没面试了,最近看一些厂开始招人了,于是投了投,没想着过,主要是抱着学习的态度,看看自己哪里不足,没想到自己这么不足。。。 最近面了个试,某大厂,具体是哪个厂大家可以自行猜测,我看看有没有对的,哈哈哈哈 目前是三面,但是估计止步于三面了,然后我稍微整理...
    继续阅读 »

    前言


    好久没面试了,最近看一些厂开始招人了,于是投了投,没想着过,主要是抱着学习的态度,看看自己哪里不足,没想到自己这么不足。。。


    最近面了个试,某大厂,具体是哪个厂大家可以自行猜测,我看看有没有对的,哈哈哈哈


    目前是三面,但是估计止步于三面了,然后我稍微整理了一下面试题,但是这里只说出我的思考,而不说出答案,至于为啥不写答案,我只能说,我自己不会。。。有些内容我需要进行学习,然后系统地、简单易懂地分享给大家


    我说一下我自身的情况:主要技术栈是vue全家桶,算是能深挖的那种,其他的,react、webpack、vite、less、sass、tailwindcss、unocss、Nuxt、node等系列都会,但是说实话我是没法手写出来的,只停留在会用的程度,webgl、canvas等可视化方向还可以,毕竟我之前就是做这个的,算法还算可以,不说精通,但是一般题是可以做出来的,然后在基础方面,也就是js、css这块,我只能说了解吧,因为见过css大神coco这种的,就感觉自己css从熟悉变成了听说。


    反正大概的就那样吧,会的比较多,比较杂,但是很多都不精通(不去看源码这种),关键的来了,我对于浏览器这块比较薄弱,计网、操作系统对我来说像噩梦一样,我觉得是我经验少吧,没能在工作中接触到这几个层面,所以我就是真的能答出来,也就是硬背的,并且不能举一反三,这也导致了这次面试的惨败



    下面我说一下面试吧


    注意,公司的技术栈主要是React(umi)这块,vue很少很少,然后会用node写一些中间件,大部分都是大前端



    然后算法问题的话,也不在这里说,主要说一些口述的问题


    一面


    一面是对我来说最友好的一面了,基本上都是简单的一些基础问题


    面试官主要是react技术栈,然后我和他说了,我主要是vue的,vue的原理可以,但是问我react的太深的问题我是不太会的,首先是自我介绍,然后开始问问题



    • pinia和vuex的区别,其实他想问我Redux和Mobx和其他React状态管理的区别,但是奈何我就会这几个,所以他索性问了问了我pinia

    • css实现DOM节点的水平居中有几种方式:我记得我说了四种,flex,text-align,margin,position,应该还有,但是一瞬间的话,脑袋瓦特了

    • 实现一个左右布局,左侧200px,右侧自适应,css写有几种方式:我说了浮动、定位、弹性盒、网格这四种

    • 检测js数据类型,typeof和instanceof区别,instanceof原理:这里我直接手写了instanceof,这个很简单

    • 浏览器输入url,到看到页面会发生什么:我当时懵了,我看过n个面经都说过这个问题,经典八股,但是我就是没背,只能磕磕巴巴说了一些(我八股真的不行,而且我不背这玩意)

    • 用Java的时候,对登录请求进行拦截,怎么处理的:这个很简单哈,为啥问Java,这是因为我简历上有,我之前从事过全栈,然后他就问了一下

    • 函数式编程的副作用是什么

    • 工作的经历,项目问题(这个占据了大部分的时间),其中有个问题可以分享一下,因为我用了wangeditor,他问我wangeditor的内核是什么


    一面总体来说是很友好的,而且都答出来了,面试官很礼貌,面试感受非常好,第二天下午的时候通知二面


    二面


    噩梦的开始



    • 自我介绍

    • 公司项目问题(绝大部分时间)

    • vue、react数据绑定的区别

    • 我想存储一个客户端的数据,前端有哪些存储方式:后来就存储、内存的问题开始展开

    • pinia会进行数据的存储,它最终存在了哪里

    • js的内存是怎么进行管理的

    • 垃圾回收、内存泄漏,什么情况会导致内存泄漏

    • 闭包是什么,应用场景,怎么操作会产生内存泄漏

    • 你在工作时用的哪种协议

    • 除了http还有哪些通信协议(跟前端有关的)

    • websocket通信过程是怎么样的

    • 前端跨域相关问题

    • 代理相关问题

    • 服务和服务之间有没有跨域

    • 前端安全方面有哪些攻击方式

    • 该怎么处理呢

    • node有哪些框架可以处理脚本攻击(或者是库)


    有些问题记不清了,后面有一些网络的问题,但是忘了,前面其实还好,而且问题是一步一步衍生出来的,这感觉很好,但是到网络安全这里,我就有点不会了,当时就感觉完犊子了,再见


    然后过了三天,hr电话告诉我过了,约了三面,其实是比较吃惊的,我以为已经止步了


    三面


    最难受的一面



    • 自我介绍

    • 说说最近自己认为最好的项目,然后我说了一些,然后对方:就这?我一时语塞,开始紧张(项目占据了大多数时间)

    • 说说tcp三次挥手,为什么不能两次

    • tcp粘包,讲讲

    • 还有一些计网和操作系统的问题,这里是因为,我根本不会,所以压根没记住问题。。

    • 进程、线程区别,举个生动的例子

    • 讲讲多线程

    • 浏览器的核心线程和核心进程有哪些

    • MySQL的引擎

    • 现在有一个100tb的文件,让你一分钟之内把这个文件遍历出来,怎么做


    计网和操作系统一塌糊涂,现在面试还没有反馈,凉凉了,而且看面试官的态度也能看出来是很不满意的


    总结


    平均时长在45min左右


    几乎没问vue的任何问题,这是我最难受的,而且js、ts、css也几乎不问的,反正就是我上面的技术栈几乎一个没问,面试官主要就问你两处:你的工作经验(也就是你曾经的公司项目),以及计网和操作系统


    因为我有做一些开源的项目和个人的项目,但是他们更在乎你之前公司的项目是什么样的


    我自己的项目比较多,简历就有5.6页,但是没啥用,他们都没问


    我也发现自己计网、操作系统这里太薄弱了,有时间还是得系统学习一下的,自己的确在开发中没遇到过这些,欠加思考


    希望大家也能重视一下这里吧


    作者:Shaka
    来源:juejin.cn/post/7273682292538933306
    收起阅读 »

    帮你省时间,看看 bun v1.0 怎么用!

    web
    本文基于 Window Ubuntu WSL 环境测试,本文只选取重点,细节需查看文档 一、bun v1.0 做了什么? all in JavaScript/TypeScript app。看起真的很了不起! 作为JS/TS运行时 作为包管理工具和包运行...
    继续阅读 »

    本文基于 Window Ubuntu WSL 环境测试,本文只选取重点,细节需查看文档



    一、bun v1.0 做了什么?



    all in JavaScript/TypeScript app。看起真的很了不起!




    • 作为JS/TS运行时

    • 作为包管理工具和包运行器

    • 作为构建工具

    • 作为测试运行器

    • 对外提供 API


    资源



    二、安装 bun v1.0



    bun 目前不支持 window 环境,但是可以在 wsl 中使用。



    2.1) 各种安装方法



    • curl


    curl -fsSL https://bun.sh/install | bash # 检查:which bun


    • 使用 npm 安装


    npm install -g bun # 检查:which bun


    • 其他平台的安装方法



    brew tap oven-sh/bun # for macOS and Linux
    brew install bun # brew
    docker pull oven/bun # docker

    2.2) bun 提供的命令


    命令描述
    init初始化一个 bun 项目
    run运行一个文件或者脚本
    test运行测试
    xbun x 的别名,类似于 npx
    repl进入交互式环境
    create使用模板创建项目
    install安装依赖
    add添加依赖
    remove移除依赖
    update更新依赖
    link全局链接一个 npm 包
    unlink移除全局链接的 npm 包
    pm更多的包管理命令
    build打包 TypeScript/JavaScript 文件到单个文件
    update获取最新的 bun 版本

    三、作为 JS/TS 运行时


    bun index.js // 运行 js 文件
    bun run index.ts // 运行 ts 文件
    // 其他相关的 tsx/jsx/...

    如果直接运行 index.tsx 没有任何依赖会报错:


    const Ad = <div>ad</div>

    console.log(Ad)

    // bun index.tsx
    // 错误:Cannot find module "react/jsx-dev-runtime" from "/xxx/index.tsx"

    四、作为包管理工具和包运行器


    4.1)初始化一个项目


    bun init # 与 npm init -y 类似

    4.2)使用脚手架


    # 与 npx 类似, 以下可能常用的初始化项目的脚手架
    bun create react-app
    bun create remix
    bun create next-app
    bun create nuxt-app

    五、作为构建工具



    • 初始化一个简单的项目


    cd your_dir
    bun init # 默认
    bun add react react-dom # 添加依赖包
    touch index.tsx


    • 添加 TSX 文件内容


    import React from 'react'

    const App = () => {
    return <div>This is App</div>
    }


    • 打包 bun build


    bun build ./index.tsx --outfile=bundle.js


    提示:bundle.js 中打包了 react 相关的包。



    六、作为测试运行器


    测试与 Jest 非常相似, 以下是官方示例:


    import { expect, test } from "bun:test";

    test("2 + 2", () => {
    expect(2 + 2).toBe(4);
    });

    运行时测试:


    bun test

    速度很快,输出结果:


    bun test v1.0.0 (822a00c4)

    t.test.ts:
    ✓ 2 + 2 [1.03ms]

    1 pass
    0 fail
    1 expect() calls
    Ran 1 tests across 1 files. [92.00ms]

    七、对外提供 API


    项目描述
    HTTP 服务处理 HTTP 请求和响应
    WebSocket 套接字支持 WebSocket 连接
    Workers 工具在后台运行多线程任务
    Binary data处理二进制数据
    Streams流处理
    File I/O文件输入/输出操作
    import.meta访问模块元信息
    SQLite使用 SQLite 数据库
    FileSystemRouter文件系统路由器
    TCP socketsTCP 套接字通信
    Globals全局变量和对象
    Child processes创建子进程
    Transpiler转译器
    Hashing哈希函数和算法
    Console控制台输出
    FFI外部函数接口
    HTMLRewriterHTML 重写和转换
    Testing测试工具和框架
    Utils实用工具函数
    Node-APINode.js API 访问

    八、展望



    • windows 支持


    小结


    本文主要讲解了 bun v1.0 中所做的事情,包含极速的运行时、一体化的包管理工具、内置测试运行器、构建应用(打包)和对象提供各种类型的 API(兼容 Node API(非完全)),如此功能能完整的 bun 你想尝试一下吗?


    作者:进二开物
    来源:juejin.cn/post/7277399972916428835
    收起阅读 »

    为什么react中的hooks都要放在顶部?

    1. 使用场景: 公司开会的时候有师兄问到为什么hooks定义一般都写在顶部,对于这个问题我以前总结过,这次看了 react新文档后我给出更加详细的解释并给出具体代码来解释为什么要放在顶部。 2.官网解释: 1.官网截图镇楼: 2.那我写在条件语句中会怎样 ...
    继续阅读 »

    1. 使用场景:


    公司开会的时候有师兄问到为什么hooks定义一般都写在顶部,对于这个问题我以前总结过,这次看了
    react新文档后我给出更加详细的解释并给出具体代码来解释为什么要放在顶部。


    2.官网解释:


    1.官网截图镇楼:




    2.那我写在条件语句中会怎样


    我给出一段代码:其中const [message, setMessage] = useState('');写在了条件语句里面

    import { useState } from 'react';

    export default function FeedbackForm() {
    const [isSent, setIsSent] = useState(false);
    if (isSent) {
    return <h1>Thank you!</h1>;
    } else {
    // eslint-disable-next-line
    const [message, setMessage] = useState('');
    return (
    <form onSubmit={e => {
    e.preventDefault();
    alert(`Sending: "${message}"`);
    setIsSent(true);
    }}>
    <textarea
    placeholder="Message"
    value={message}
    onChange={e => setMessage(e.target.value)}
    />
    <br />
    <button type="submit">Send</button>
    </form>
    );
    }
    }

    效果图:这是一个收集用户反馈的小表单。当反馈被提交时



     它应该显示一条感谢信息,当我点击确定时出现一条错误。




    “渲染的 hooks 比预期的少”


    3.那我不写在顶部可能会怎样


    下方的const [message, setMessage] = useState('');并没有写在顶部

       import { useState } from 'react';

    export default function FeedbackForm() {
    const [isSent, setIsSent] = useState(false);
    if (isSent) {
    return <h1>Thank you!</h1>;
    }
    const [message, setMessage] = useState('');
    return (
    <form onSubmit={e => {
    e.preventDefault();
    alert(`Sending: "${message}"`);
    setIsSent(true);
    }}>
    <textarea
    placeholder="Message"
    value={message}
    onChange={e => setMessage(e.target.value)}
    />
    <br />
    <button type="submit">Send</button>
    </form>
    );
    }
    }

    效果图:



     点击确认后:
    同样出现这个错误:提前return导致后面一个hooks没有渲染。




    4.原因分析


    从源码的角度来说的话,React会在内部创建一个名为“Hooks”(中文为钩子)的数据结构来追踪每个组件的状态。


    在函数组件中调用Hook时,React会根据Hook的类型将其添加到当前组件的Hooks链表中。然后,React会将这些Hooks存储在Fiber节点的“memoizedState”字段中,以便在下一次渲染时使用。


    如果你在代码中多次调用同一个Hook,React会根据Hooks的顺序将其添加到当前组件的Hooks链表中。这样,React就可以确定哪个状态应该与哪个组件关联,并且能够正确地更新UI。


    以下是一个示例代码片段:

    import { useState, useEffect } from 'react';

    function useCustomHook() {

    const [count, setCount] = useState(0);

    useEffect(() => {
    document.title = `Count: ${count}`;
    }, [count]);
    return [count, setCount];
    }

    export default function MyComponent() {

    const [count, setCount] = useCustomHook();
    return (
    <div>
    <p>Count: {count}</p>
    <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
    );
    }

    在上面的代码中,useCustomHook是一个自定义Hook,它使用useStateuseEffectHook来管理状态。在MyComponent中,我们调用自定义Hook并使用返回值来渲染UI。


    由于useCustomHook只能在函数组件或其他自定义Hooks的最顶层调用,我们不能将它嵌套在条件语句、循环或其他函数内部。如果这样做,React将无法正确追踪状态并更新UI,可能导致不可预测的结果。如果我们条件渲染中使用可能导致没有引入useCustomHook(),从而导致错误。


    总结描述就是创建了一个链表,当在条件语句中使用hooks时可能会导致前后两次链表不同,从而导致错误,所以我们必须尽可能避免这种错误从而写在顶部。


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

    你可能并不需要useEffect

    相信大家在写react时都有这样的经历:在项目中使用了大量的useEffect,以至于让我们的代码变得混乱和难以维护。 难道说useEffect这个hook不好吗?并不是这样的,只是我们一直在滥用而已。 在这篇文章中,我将展示怎样使用其他方法来代替useEff...
    继续阅读 »

    相信大家在写react时都有这样的经历:在项目中使用了大量的useEffect,以至于让我们的代码变得混乱和难以维护。


    难道说useEffect这个hook不好吗?并不是这样的,只是我们一直在滥用而已。


    在这篇文章中,我将展示怎样使用其他方法来代替useEffect。


    什么是useEffect


    useEffect允许我们在函数组件中执行副作用。它可以模拟 componentDidMount、componentDidUpdate 和componentWillUnmount。我们可以用它来做很多事情。但是它也是一个非常危险的钩子,可能会导致很多bug。


    为什么useEffect是容易出现bug的


    来看一个定时器的例子:

    import React, { useEffect } from 'react'

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

    useEffect(() => {
    const timer = setInterval(() => {
    setCount((c) => c + 1)
    }, 1000)
    })

    return <div>{count}</div>
    }

    这是一个非常常见的例子,但是它是非常不好。因为如果组件由于某种原因重新渲染,就会重新设置定时器。该定时器将每秒调用两次,很容易导致内存泄漏。


    怎样修复它?


    useRef

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

    const Counter = () => {
    const [count, setCount] = useState(0)
    const timerRef = useRef()

    useEffect(() => {
    timerRef.current = setInterval(() => {
    setCount((c) => c + 1)
    }, 1000)
    return () => clearInterval(timerRef.current)
    }, [])

    return <div>{count}</div>
    }

    它不会在每次组件重新渲染时设置定时器。但是我们在项目中并不是这么简单的代码。而是各种状态,做各种事情。


    你以为你写的useEffect

    useEffect(() => {
    doSomething()

    return () => cleanup()
    }, [whenThisChanges])

    实际上是这样的

    useEffect(() => {
    if (foo && bar && (baz || quo)) {
    doSomething()
    } else {
    doSomethingElse()
    }
    // 遗忘清理函数。
    }, [foo, bar, baz, quo, ...])

    写了一堆的逻辑,这种代码非常混乱难以维护。


    useEffect 到底是用来干啥的


    useEffect是一种将React与一些外部系统(网络、订阅、DOM)同步的方法。如果你没有任何外部系统,只是试图用useEffect管理数据流,你就会遇到问题。



    有时我们并不需要useEffect


    1.我们不需要useEffect转化数据

    const Cart = () => {
    const [items, setItems] = useState([])
    const [total, setTotal] = useState(0)

    useEffect(() => {
    setTotal(items.reduce((total, item) => total + item.price, 0))
    }, [items])

    // ...
    }

    上面代码使用useEffect来进行数据的转化,效率很低。其实并不需要使用useEffect。当某些值可以从现有的props或state中计算出来时,不要把它放在状态中,在渲染期间计算它。

    const Cart = () => {
    const [items, setItems] = useState([])
    const [total, setTotal] = useState(0)

    const totalNum = items.reduce((total, item) => total + item.price, 0)

    // ...
    }

    如果计算逻辑比较复杂,可以使用useMemo:

    const Cart = () => {
    const [items, setItems] = useState([])
    const total = useMemo(() => {
    return items.reduce((total, item) => total + item.price, 0)
    }, [items])

    // ...
    }

    2.使用useSyncExternalStore代替useEffect


    useSyncExternalStore


    常见方式:

    const Store = () => {
    const [isConnected, setIsConnected] = useState(true)

    useEffect(() => {
    const sub = storeApi.subscribe(({ status }) => {
    setIsConnected(status === 'connected')
    })

    return () => {
    sub.unsubscribe()
    }
    }, [])

    // ...
    }

    更好的方式:

    const Store = () => {
    const isConnected = useSyncExternalStore(
    storeApi.subscribe,
    () => storeApi.getStatus() === 'connected',
    true
    )

    // ...
    }

    3.没必要使用useEffect与父组件通信

    const ChildProduct = ({ onOpen, onClose }) => {
    const [isOpen, setIsOpen] = useState(false)

    useEffect(() => {
    if (isOpen) {
    onOpen()
    } else {
    onClose()
    }
    }, [isOpen])

    return (
    <div>
    <button
    onClick={() => {
    setIsOpen(!isOpen)
    }}
    >
    Toggle quick view
    </button>
    </div>
    )
    }

    更好的方式,可以使用事件处理函数代替:

    const ChildProduct = ({ onOpen, onClose }) => {
    const [isOpen, setIsOpen] = useState(false)

    const handleToggle = () => {
    const nextIsOpen = !isOpen;
    setIsOpen(nextIsOpen)

    if (nextIsOpen) {
    onOpen()
    } else {
    onClose()
    }
    }

    return (
    <div>
    <button
    onClick={}
    >
    Toggle quick view
    </button>
    </div>
    )
    }

    4.没必要使用useEffect初始化应用程序

    const Store = () => {
    useEffect(() => {
    storeApi.authenticate()
    }, [])

    // ...
    }

    更好的方式:


    方式一:

    const Store = () => {
    const didAuthenticateRef = useRef()

    useEffect(() => {
    if (didAuthenticateRef.current) return

    storeApi.authenticate()

    didAuthenticateRef.current = true
    }, [])

    // ...
    }

    方式二:

    let didAuthenticate = false

    const Store = () => {
    useEffect(() => {
    if (didAuthenticate) return

    storeApi.authenticate()

    didAuthenticate = true
    }, [])

    // ...
    }

    方式三:

    if (typeof window !== 'undefined') {
    storeApi.authenticate()
    }

    const Store = () => {
    // ...
    }

    5.没必要在useEffect请求数据


    常见写法

    const Store = () => {
    const [items, setItems] = useState([])

    useEffect(() => {
    let isCanceled = false

    getItems().then((data) => {
    if (isCanceled) return

    setItems(data)
    })

    return () => {
    isCanceled = true
    }
    })

    // ...
    }

    更好的方式:


    没有必要使用useEffect,可以使用swr:

    import useSWR from 'swr'

    export default function Page() {
    const { data, error } = useSWR('/api/data', fetcher)

    if (error) return <div>failed to load</div>
    if (!data) return <div>loading...</div>

    return <div>hello {data}!</div>
    }

    使用react-query:

    import { getItems } from './storeApi'
    import { useQuery, useQueryClient } from 'react-query'

    const Store = () => {
    const queryClient = useQueryClient()

    return (
    <button
    onClick={() => {
    queryClient.prefetchQuery('items', getItems)
    }}
    >
    See items
    </button>
    )
    }

    const Items = () => {
    const { data, isLoading, isError } = useQuery('items', getItems)

    // ...
    }

    没有正式发布的react的 use函数

    function Note({ id }) {
    const note = use(fetchNote(id))

    return (
    <div>
    <h1>{note.title}</h1>
    <section>{note.body}</section>
    </div>
    )
    }

    reference


    http://www.youtube.com/watch?v=bGz…


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

    我的天!多个知名组件库都出现了类似的bug!

    前言 首先声明,没有标题党哈! 以下我知道的国内知名react组件库全部都有这个bug,你们现在都能去复现,一个提pr的好机会就让给你们了,哈哈!复现组件库: 阿里系:ant design, fusion design,字节系:arco design...
    继续阅读 »

    前言


    首先声明,没有标题党哈!


    以下我知道的国内知名react组件库全部都有这个bug,你们现在都能去复现,一个提pr的好机会就让给你们了,哈哈!复现组件库:



    本来字节还有一个semi design,结果我发现它没有Affix组件,也就是固钉组件,让他躲过一劫,他有这个组件我也觉得肯定会复现相同的bug。


    Affix组件是什么,以及bug复现


    Affix组件(固钉组件)能将页面元素钉在可视范围。如下图:




    这个button组件,会在距离顶部80px的时候会固定在屏幕上(position: fixed),如下图:




    如何复现bug


    你在这个button元素任意父元素上,加上以下任意style属性


    • will-change: transform;
    • will-change: filter;
    • will-change: perspective;
    • transform 不为none
    • perspective不为none
    • 非safari浏览器,filter属性不为none
    • 非safari浏览器,backdrop-filter属性不为none
    • 等等

    都可以让这个固定组件失效,就是原本是距离顶部80px固定。


    我的组件库没有这个bug,哈哈


    mx-design


    目前组件不是很多,还在努力迭代中,不知道凭借没有这个bug的小小优点,能不能从你手里取一个star,哈哈


    bug原因


    affix组件无非都是用了fixed布局,我是如何发现这个bug的呢,我的组件库动画系统用的framer-motion,我本来是想在react-router切换路由的时候整点动画的,动画效果就是给body元素加入例如transform的变化。


    然后我再看我的固钉组件怎么失效了。。。后来仔细一想,才发现想起来fixed布局的一个坑就是,大家都以为fixed布局相对的父元素是window窗口,其实是错误的!


    真正的规则如下(以下说的包含块就是fixed布局的定位父元素):

    1. 如果 position 属性是 absolute 或 fixed,包含块也可能是由满足以下条件的最近父级元素的内边距区的边缘组成的:

    • transform 或 perspective 的值不是 none
    • will-change 的值是 transform 或 perspective
    • filter 的值不是 none 或 will-change 的值是 filter(只在 Firefox 下生效)。
    • contain 的值是 paint(例如:contain: paint;
    • backdrop-filter 的值不是 none(例如:backdrop-filter: blur(10px);

    评论区有很多同学居然觉的这不是bug?


    其实这个问题本质是定位错误,在这些组件库里,同样使用到定位的有,例如Tooltip,Select,Popuver等等,明显这些组件跟写Affix组件的不是一个人,其他组件这个bug是没有的,只有Affix组件出现了,所以你说这是不是bug。


    还有,如果因为引用了Affix组件,这个固定元素的任一父元素都不能用以上的css属性,我作为使用者,我用了动画库,动画库使用transfrom做Gpu加速,你说不让我用了,因为引起Affix组件bug,我心里想凭啥啊,明明加两行代码就解决了。


    最后,只要做过定位组件的同学,其复杂度在前端算是比较高的了,这也是为什么有些组件库直接用第三方定位组件库(floating-ui,@popper-js),而不是自己去实现,因为自己实现很容易出bug,这也是例如以上组件库Tooltip为什么能适应很多边界case而不出bug。


    所以你想想,这仅仅是定位组件遇到的一个很小的问题,你这个都解决不了,什么都怪css,你觉得用户会这么想吗,一有css,你所有跟定位相关的组件全部都不能用了,你们还讲理不?


    总之一句话,你不能把定位组件的复杂度高怪用户没好好用,建议去看看floating-ui的源码,或者之前我写的@popper-js定位组件的简要逻辑梳理,你就会意识到定位组件不简单。边界case多如牛毛。


    解决方案


    • 首先是找出要固定元素的定位元素(定位元素的判断逻辑上面写了),然后如果定位元素是window,那么跟目前所有组件库的逻辑一样,所以没有bug,如果不是window,就要求出相对定位父元素距离可视窗口顶部的top的值
    • 然后在我们原本要定位的值,比如距离顶部80px的时候固定,此时80px再减去上面说的定位父元素距离可视窗口顶部的top的值,就没有bug了

    具体代码如下:


    • offsetParent固定元素的定位上下文,也就是相对定位的父元素
    • fixedTop是我们要触发固定的值,比如距离可视窗口顶部80px就固定
    affixDom.style.top = `${isHTMLElement(offsetParent) ? (fixedTop as number) - offsetParent.getBoundingClientRect().top : fixedTop}px`;

    如何找出offsetParent,也就是定位上下文

    export function getContainingBlock(element: Element) {
    let currentNode = element.parentElement;
    while (currentNode) {
    if (isContainingBlock(currentNode)) return currentNode;
    currentNode = currentNode.parentElement;
    }
    return null;
    }

    工具方法,isContainingBlock如下:

    import { isSafari } from './isSafari';

    export function isContainingBlock(element: Element): boolean {
    const safari = isSafari();
    const css = getComputedStyle(element);

    // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
    return (
    css.transform !== 'none' ||
    css.perspective !== 'none' ||
    (css.containerType ? css.containerType !== 'normal' : false) ||
    (!safari && (css.backdropFilter ? css.backdropFilter !== 'none' : false)) ||
    (!safari && (css.filter ? css.filter !== 'none' : false)) ||
    ['transform', 'perspective', 'filter'].some((value) => (css.willChange || '').includes(value)) ||
    ['paint', 'layout', 'strict', 'content'].some((value) => (css.contain || '').includes(value))
    );
    }


    本文完毕,求关注,求star!!!对于react组件库感兴趣的小伙伴,欢迎加群一起交流哦!


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

    彻底搞懂小程序登录流程-附小程序和服务端代码

    web
    编者按:本文作者奇舞团高级前端开发工程师冯通 用户登录是大部分完整 App 必备的流程 一个简单的用户系统需要关注至少这些层面 安全性(加密) 持久化登录态(类似cookie) 登录过期处理 确保用户唯一性, 避免出现多账号 授权 绑定用户昵称头像等信息 ...
    继续阅读 »

    编者按:本文作者奇舞团高级前端开发工程师冯通



    用户登录是大部分完整 App 必备的流程


    一个简单的用户系统需要关注至少这些层面



    • 安全性(加密)

    • 持久化登录态(类似cookie)

    • 登录过期处理

    • 确保用户唯一性, 避免出现多账号

    • 授权

    • 绑定用户昵称头像等信息

    • 绑定手机号(实名和密保方式)


    很多的业务需求都可以抽象成 Restful 接口配合 CRUD 操作


    但登录流程却是错综复杂, 各个平台有各自的流程, 反倒成了项目中费时间的部分, 比如小程序的登录流程



    对于一个从零开始的项目来说, 搞定登录流程, 就是一个好的开始, 一个好的开始, 就是成功的一半


    本文就以微信小程序这个平台, 讲述一个完整的自定义用户登录流程, 一起来啃这块难啃的骨头


    名词解释


    先给登录流程时序图中出现的名词简单做一个解释



    • code 临时登录凭证, 有效期五分钟, 通过 wx.login() 获取

    • session_key 会话密钥, 服务端通过 code2Session 获取

    • openId 用户在该小程序下的用户唯一标识, 永远不变, 服务端通过 code 获取

    • unionId 用户在同一个微信开放平台帐号(公众号, 小程序, 网站, 移动应用)下的唯一标识, 永远不变

    • appId 小程序唯一标识

    • appSecret 小程序的 app secret, 可以和 code, appId 一起换取 session_key


    其他名词



    • rawData 不包括敏感信息的原始数据字符串,用于计算签名

    • encryptedData 包含敏感信息的用户信息, 是加密的

    • signature 用于校验用户信息是否无篡改

    • iv 加密算法的初始向量



    哪些信息是敏感信息呢? 手机号, openId, unionId, 可以看出这些值都可以唯一定位一个用户, 而昵称, 头像这些不能定位用户的都不是敏感信息



    小程序登录相关函数



    • wx.login

    • wx.getUserInfo

    • wx.checkSession


    小程序的 promise


    我们发现小程序的异步接口都是 success 和 fail 的回调, 很容易写出回调地狱


    因此可以先简单实现一个 wx 异步函数转成 promise 的工具函数


    const promisify = original => {
    return function(opt) {
    return new Promise((resolve, reject) => {
    opt = Object.assign({
    success: resolve,
    fail: reject
    }, opt)
    original(opt)
    })
    }
    }

    这样我们就可以这样调用函数了


    promisify(wx.getStorage)({key: 'key'}).then(value => {
    // success
    }).catch(reason => {
    // fail
    })

    服务端实现


    本 demo 的服务端实现基于 express.js



    注意, 为了 demo 的简洁性, 服务端使用 js 变量来保存用户数据, 也就是说如果重启服务端, 用户数据就清空了




    如需持久化存储用户数据, 可以自行实现数据库相关逻辑



    // 存储所有用户信息
    const users = {
    // openId 作为索引
    openId: {
    // 数据结构如下
    openId: '', // 理论上不应该返回给前端
    sessionKey: '',
    nickName: '',
    avatarUrl: '',
    unionId: '',
    phoneNumber: ''
    }
    }

    app
    .use(bodyParser.json())
    .use(session({
    secret: 'alittlegirl',
    resave: false,
    saveUninitialized: true
    }))

    小程序登录


    我们先实现一个基本的 oauth 授权登录



    oauth 授权登录主要是 code 换取 openId 和 sessionKey 的过程



    前端小程序登录


    写在 app.js 中


    login () {
    console.log('登录')
    return util.promisify(wx.login)().then(({code}) => {
    console.log(`code: ${code}`)
    return http.post('/oauth/login', {
    code,
    type: 'wxapp'
    })
    })
    }

    服务端实现 oauth 授权


    服务端实现上述 /oauth/login 这个接口


    app
    .post('/oauth/login', (req, res) => {
    var params = req.body
    var {code, type} = params
    if (type === 'wxapp') {
    // code 换取 openId 和 sessionKey 的主要逻辑
    axios.get('https://api.weixin.qq.com/sns/jscode2session', {
    params: {
    appid: config.appId,
    secret: config.appSecret,
    js_code: code,
    grant_type: 'authorization_code'
    }
    }).then(({data}) => {
    var openId = data.openid
    var user = users[openId]
    if (!user) {
    user = {
    openId,
    sessionKey: data.session_key
    }
    users[openId] = user
    console.log('新用户', user)
    } else {
    console.log('老用户', user)
    }
    req.session.openId = user.openId
    req.user = user
    }).then(() => {
    res.send({
    code: 0
    })
    })
    } else {
    throw new Error('未知的授权类型')
    }
    })

    获取用户信息


    登录系统中都会有一个重要的功能: 获取用户信息, 我们称之为 getUserInfo


    如果已登录用户调用 getUserInfo 则返回用户信息, 比如昵称, 头像等, 如果未登录则返回"用户未登录"



    也就是说此接口还有判断用户是否登录的功效...



    小程序的用户信息一般存储在 app.globalData.userInfo 中(模板如此)


    我们在服务端加上前置中间件, 通过 session 来获取对应的用户信息, 并放在 req 对象中


    app
    .use((req, res, next) => {
    req.user = users[req.session.openId]
    next()
    })

    然后实现 /user/info 接口, 用来返回用户信息


    app
    .get('/user/info', (req, res) => {
    if (req.user) {
    return res.send({
    code: 0,
    data: req.user
    })
    }
    throw new Error('用户未登录')
    })

    小程序调用用户信息接口


    getUserInfo () {
    return http.get('/user/info').then(response => {
    let data = response.data
    if (data && typeof data === 'object') {
    // 获取用户信息成功则保存到全局
    this.globalData.userInfo = data
    return data
    }
    return Promise.reject(response)
    })
    }

    专为小程序发请求设计的库


    小程序代码通过 http.get, http.post 这样的 api 来发请求, 背后使用了一个请求库


    @chunpu/http 是一个专门为小程序设计的 http 请求库, 可以在小程序上像 axios 一样发请求, 支持拦截器等强大功能, 甚至比 axios 更顺手


    初始化方法如下


    import http from '@chunpu/http'

    http.init({
    baseURL: 'http://localhost:9999', // 定义 baseURL, 用于本地测试
    wx // 标记是微信小程序用
    })

    具体使用方法可参照文档 github.com/chunpu/http…


    自定义登录态持久化


    浏览器有 cookie, 然而小程序没有 cookie, 那怎么模仿出像网页这样的登录态呢?


    这里要用到小程序自己的持久化接口, 也就是 setStorage 和 getStorage


    为了方便各端共用接口, 或者直接复用 web 接口, 我们自行实现一个简单的读 cookie 和种 cookie 的逻辑


    先是要根依据返回的 http response headers 来种上 cookie, 此处我们用到了 @chunpu/http 中的 response 拦截器, 和 axios 用法一样


    http.interceptors.response.use(response => {
    // 种 cookie
    var {headers} = response
    var cookies = headers['set-cookie'] || ''
    cookies = cookies.split(/, */).reduce((prev, item) => {
    item = item.split(/; */)[0]
    var obj = http.qs.parse(item)
    return Object.assign(prev, obj)
    }, {})
    if (cookies) {
    return util.promisify(wx.getStorage)({
    key: 'cookie'
    }).catch(() => {}).then(res => {
    res = res || {}
    var allCookies = res.data || {}
    Object.assign(allCookies, cookies)
    return util.promisify(wx.setStorage)({
    key: 'cookie',
    data: allCookies
    })
    }).then(() => {
    return response
    })
    }
    return response
    })

    当然我们还需要在发请求的时候带上所有 cookie, 此处用的是 request 拦截器


    http.interceptors.request.use(config => {
    // 给请求带上 cookie
    return util.promisify(wx.getStorage)({
    key: 'cookie'
    }).catch(() => {}).then(res => {
    if (res && res.data) {
    Object.assign(config.headers, {
    Cookie: http.qs.stringify(res.data, ';', '=')
    })
    }
    return config
    })
    })

    登录态的有效期


    我们知道, 浏览器里面的登录态 cookie 是有失效时间的, 比如一天, 七天, 或者一个月


    也许有朋友会提出疑问, 直接用 storage 的话, 小程序的登录态有效期怎么办?


    问到点上了! 小程序已经帮我们实现好了 session 有效期的判断 wx.checkSession


    它比 cookie 更智能, 官方文档描述如下



    通过 wx.login 接口获得的用户登录态拥有一定的时效性。用户越久未使用小程序,用户登录态越有可能失效。反之如果用户一直在使用小程序,则用户登录态一直保持有效



    也就是说小程序还会帮我们自动 renew 咱们的登录态, 简直是人工智能 cookie, 点个赞👍


    那具体在前端怎么操作呢? 代码写在 app.js 中


    onLaunch: function () {
    util.promisify(wx.checkSession)().then(() => {
    console.log('session 生效')
    return this.getUserInfo()
    }).then(userInfo => {
    console.log('登录成功', userInfo)
    }).catch(err => {
    console.log('自动登录失败, 重新登录', err)
    return this.login()
    }).catch(err => {
    console.log('手动登录失败', err)
    })
    }

    要注意, 这里的 session 不仅是前端的登录态, 也是后端 session_key 的有效期, 前端登录态失效了, 那后端也失效了需要更新 session_key



    理论上小程序也可以自定义登录失效时间策略, 但这样的话我们需要考虑开发者自己的失效时间和小程序接口服务的失效时间, 还不如保持统一来的简单



    确保每个 Page 都能获取到 userInfo


    如果在新建小程序项目中选择 建立普通快速启动模板


    我们会得到一个可以直接运行的模板


    点开代码一看, 大部分代码都在处理 userInfo....



    注释里写着



    由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回




    所以此处加入 callback 以防止这种情况



    但这样的模板并不科学, 这样仅仅是考虑了首页需要用户信息的情况, 如果扫码进入的页面也需要用户信息呢? 还有直接进入跳转的未支付页活动页等...


    如果每个页面都这样判断一遍是否加载完用户信息, 代码显得过于冗余


    此时我们想到了 jQuery 的 ready 函数 $(function), 只要 document ready 了, 就可以直接执行函数里面的代码, 如果 document 还没 ready, 就等到 ready 后执行代码


    就这个思路了! 我们把小程序的 App 当成网页的 document


    我们的目标是可以这样在 Page 中不会出错的获取 userInfo


    Page({
    data: {
    userInfo: null
    },
    onLoad: function () {
    app.ready(() => {
    this.setData({
    userInfo: app.globalData.userInfo
    })
    })
    }
    })

    此处我们使用 min-ready 来实现此功能


    代码实现依然写在 app.js 中


    import Ready from 'min-ready'

    const ready = Ready()

    App({
    getUserInfo () {
    // 获取用户信息作为全局方法
    return http.get('/user/info').then(response => {
    let data = response.data
    if (data && typeof data === 'object') {
    this.globalData.userInfo = data
    // 获取 userInfo 成功的时机就是 app ready 的时机
    ready.open()
    return data
    }
    return Promise.reject(response)
    })
    },
    ready (func) {
    // 把函数放入队列中
    ready.queue(func)
    }
    })

    绑定用户信息和手机号


    仅仅获取用户的 openId 是远远不够的, openId 只能标记用户, 连用户的昵称和头像都拿不到


    如何获取这些用户信息然后存到后端数据库中呢?


    我们在服务端实现这两个接口, 绑定用户信息, 绑定用户手机号


    app
    .post('/user/bindinfo', (req, res) => {
    var user = req.user
    if (user) {
    var {encryptedData, iv} = req.body
    var pc = new WXBizDataCrypt(config.appId, user.sessionKey)
    var data = pc.decryptData(encryptedData, iv)
    Object.assign(user, data)
    return res.send({
    code: 0
    })
    }
    throw new Error('用户未登录')
    })

    .post('/user/bindphone', (req, res) => {
    var user = req.user
    if (user) {
    var {encryptedData, iv} = req.body
    var pc = new WXBizDataCrypt(config.appId, user.sessionKey)
    var data = pc.decryptData(encryptedData, iv)
    Object.assign(user, data)
    return res.send({
    code: 0
    })
    }
    throw new Error('用户未登录')
    })

    小程序个人中心 wxml 实现如下


    <view wx:if="userInfo" class="userinfo">
    <button
    wx:if="{{!userInfo.nickName}}"
    type="primary"
    open-type="getUserInfo"
    bindgetuserinfo="bindUserInfo">
    获取头像昵称 </button>
    <block wx:else>
    <image class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover"></image>
    <text class="userinfo-nickname">{{userInfo.nickName}}</text>
    </block>

    <button
    wx:if="{{!userInfo.phoneNumber}}"
    type="primary"
    style="margin-top: 20px;"
    open-type="getPhoneNumber"
    bindgetphonenumber="bindPhoneNumber">
    绑定手机号 </button>
    <text wx:else>{{userInfo.phoneNumber}}</text>
    </view>

    小程序中的 bindUserInfo 和 bindPhoneNumber 函数, 根据微信最新的策略, 这俩操作都需要用户点击按钮统一授权才能触发


    bindUserInfo (e) {
    var detail = e.detail
    if (detail.iv) {
    http.post('/user/bindinfo', {
    encryptedData: detail.encryptedData,
    iv: detail.iv,
    signature: detail.signature
    }).then(() => {
    return app.getUserInfo().then(userInfo => {
    this.setData({
    userInfo: userInfo
    })
    })
    })
    }
    },
    bindPhoneNumber (e) {
    var detail = e.detail
    if (detail.iv) {
    http.post('/user/bindphone', {
    encryptedData: detail.encryptedData,
    iv: detail.iv
    }).then(() => {
    return app.getUserInfo().then(userInfo => {
    this.setData({
    userInfo: userInfo
    })
    })
    })
    }
    }

    代码


    本文所提到的代码都可以在我的 github 上找到


    小程序代码在 wxapp-login-demo


    服务端 Node.js 代码在 wxapp-login-server


    关于奇舞周刊


    《奇舞周刊》是360公司专业前端团队「奇舞团」运营的前端技术社区。关注公众号后,直接发送链接到后台即可给我们投稿。



    作者:奇舞精选
    来源:juejin.cn/post/6844903702726180871
    收起阅读 »

    Bun 1.0 正式发布,爆火的前端运行时,速度遥遥领先!

    web
    9 月 8 日,前端运行时 Bun 1.0 正式发布,如今,Bun 已经稳定并且适用于生产环境。Bun 不仅是一个专注性能与开发者体验的全新 JavaScript 运行时,还是一个快速的、全能的工具包,可用于运行、构建、测试和调试JavaScript和Type...
    继续阅读 »

    9 月 8 日,前端运行时 Bun 1.0 正式发布,如今,Bun 已经稳定并且适用于生产环境。Bun 不仅是一个专注性能与开发者体验的全新 JavaScript 运行时,还是一个快速的、全能的工具包,可用于运行、构建、测试和调试JavaScript和TypeScript代码,无论是从单个文件还是完整的全栈应用。
    image.png
    2022年,Bun 发布,随即爆火,成为年度最火的前端项目:
    image.png
    Bun 的流行程度伴随着在去年夏天发布的第一个 Beta 版而爆炸性增长:仅一个月内,就在 GitHub 上获得了超过两万颗 Star。
    star-history-202397.png



    Bun 不仅仅是一个运行时。它也是:



    • 一个包管理器 (类似 Yarn、 NPM、 PNPM)

    • 一个构建工具 (类似 Webpack、 ESBuild、 Parcel)

    • 一个测试运行器

    • ...


    所以 Bun 可以通过读取 package.json 来安装依赖项。Bun 还可以运行脚本。不管它做什么都比其他工具更快。Bun 在 JavaScript 生态系统的许多方面都有新的尝试,其中的重点是性能。它优先支持标准的 Web API,如 Fetch。它也支持许多 Node.js APIs,使其能与大多数 NPM 包兼容。



    安装 Bun:


    // npm
    npm install -g bun

    // brew
    brew tap oven-sh/bun
    brew install bun

    // curl
    curl -fsSL https://bun.sh/install | bash

    // docker
    docker pull oven/bun
    docker run --rm --init --ulimit memlock=-1:-1 oven/bun

    更新 Bun:


    bun upgrade

    下面就来看看 Bun 是什么,1.0 版本带来了哪些更新!


    Bun:全能的工具包


    JavaScript 成熟、发展迅速,并且有着充满活力和激情的开发者社区。然而,自14年前Node.js发布以来,JavaScript 的工具链变得越来越庞大和复杂。这是因为在发展过程中,各种工具被逐渐添加进来,但没有一个统一的集中规划,导致工具链缺乏整体性和效率,变得运行缓慢和复杂。


    Bun 为什么会出现?


    Bun的目标很简单,就是要消除JavaScript工具链的缓慢和复杂性,但同时保留JavaScript本身的优点。Bun希望让开发者继续使用喜欢的库和框架,并且无需放弃已经熟悉的规范和约定。


    为了实现这个目标,可能需要放弃一些在使用Bun之后变得不再必要的工具:



    • Node.js:Bun 的一个可以直接替代的工具,因此不再需要以下工具:

      • node

      • npx:Bun 的 bunx 命令比 npx 快5倍。

      • nodemon:Bun 内置了监听模式,无需使用 nodemon

      • dotenvcross-env:Bun 默认支持读取.env文件的配置。



    • 转译器:Bun 可以运行.js.ts、``.cjs.mjs.jsx.tsx文件,因此不再需要以下工具:

      • tsc:仍然可以保留它用于类型检查!

      • babel.babelrc@babel/preset-*:不再需要使用 Babel 进行转译。

      • ts-nodets-node-esm:Bun 可以直接运行 TypeScript 文件。

      • tsx:Bun可以直接运行 TypeScript 的 JSX 文件。



    • 构建工具:Bun 具有一流的性能和与esbuild兼容的插件API,因此不再需要以下工具:

      • esbuild

      • webpack

      • parcel, .parcelrc

      • rollup, rollup.config.js



    • 包管理器:Bun 是一个与 npm 兼容的包管理器,可以使用熟悉的命令。它可以读取 package.json文件并将依赖写入node_modules目录,与其他包管理器的行为类似,因此可以替换以下工具:

      • npm, .npmrc, package-lock.json

      • yarn,yarn.lock

      • pnpm, pnpm.lock, pnpm-workspace.yaml

      • lern



    • 测试库:Bun是一个支持Jest的测试运行器,具有快照测试、模拟和代码覆盖率等功能,因此不再需要以下测试相关的工具:

      • jest, jest.config.js

      • ts-jest, @swc/jest, babel-jest

      • jest-extended

      • vitest, vitest.config.ts




    尽管这些工具都有自己的优点,但使用它们时往往需要将它们全部集成在一起,这会导致开发过程变得缓慢和复杂。而Bun通过成为一个单一的工具包,提供了最佳的开发者体验,从性能到API设计都力求做到最好。


    Bun:JavaScript 运行时


    Bun是一个快速的JavaScript运行时。旨在提供出色的性能和开发体验。它的设计旨在解决开发过程中的各种痛点,使开发者的工作更加轻松和愉快。


    与Node.js兼容


    Bun 是可以直接替代 Node.js 的。这意味着现有的 Node.js 应用和 npm 包可以在 Bun 中正常工作。Bun 内置了对 Node.js API 的支持,包括:



    • 内置模块,如fspathnet

    • 全局对象,如__dirnameprocess

    • Node.js 模块解析算法(例如node_modules


    尽管与 Node.js 完全兼容是不可能的,特别是一些依赖于v8版本的特性,但 Bun 几乎可以运行任何现有的 Node.js 应用。


    Bun经过了与最受欢迎的Node.js包的兼容性测试,支持与Express、Koa、Hapi等服务端框架以及其他流行的全栈框架的无缝集成。开发者可以放心地在Bun中使用这些库和框架,并享受到更好的开发体验。
    image.png
    使用Next.js、Remix、Nuxt、Astro、SvelteKit、Nest、SolidStart和Vite构建的全栈应用可以在Bun中运行。


    速度


    Bun的速度非常快,启动速度比 Node.js 快 4 倍。当运行TypeScript文件时,这种差异会更加明显,因为在Node.js中运行TypeScript文件需要先进行转译才能运行。
    image.png
    Bun在运行一个简单的"Hello World" TypeScript文件时,比在Node.js中使用esbuild运行速度快5倍。


    Bun使用的是Apple的WebKit引擎,而不是像Node.js和其他运行时一样使用Google的V8引擎。WebKit引擎是Safari浏览器的核心引擎,每天被数十亿的设备使用。它经过了长时间的实际应用和测试,具备快速和高效的特性。


    TypeScript 和 JSX 支持


    Bun内置了JavaScript转译器,因此可以运行JavaScript、TypeScript甚至JSX/TSX文件,无需任何依赖。


    // 运行 TS 文件
    bun index.ts

    // 运行 JSX/TSX 文件
    bun index.tsx

    ESM 和 CommonJS 兼容


    从CommonJS到ES模块的过渡一直是缓慢而充满挑战的。在引入ESM之后,Node.js花了5年时间才在没有--experimental-modules标志的情况下支持它。尽管如此,生态系统仍然充斥着CommonJS。


    Bun 同时支持这两种模块系统。无论是使用CommonJS的.js扩展名、.cjs扩展名,还是使用ES模块的.mjs扩展名,Bun都会进行正确的解析和执行,而无需额外的配置。


    甚至可以在同一个文件中同时使用importrequire()


    import lodash from "lodash";
    const _ = require("underscore");

    Web API


    Bun 内置支持浏览器中可用的Web标准API,如fetchRequestResponseWebSocketReadableStream等。


    const response = await fetch("https://example.com/");
    const text = await response.text();

    开发者不再需要安装像node-fetchws这样的包。Bun内置的 Web API 是使用原生代码实现的,比第三方替代方案更快速和可靠。


    热重载


    Bun提供了热重载功能,可以在开发过程中实现文件的自动重新加载。只需在运行Bun时加上--hot参数,当文件发生变化时,Bun 就会自动重新加载你的应用,从而提高开发效率。


    bun --hot server.ts

    与像nodemon这样完全重新启动整个进程的工具不同,Bun 在重新加载代码时不会终止旧进程。这意味着HTTP和WebSocket连接不会断开,并且状态不会丢失。
    hot (1).gif


    插件


    Bun 被设计为高度可定制的。
    可以定义插件来拦截导入操作并执行自定义的加载逻辑。插件可以添加对其他文件类型的支持,比如.yaml.png。插件API的设计灵感来自于esbuild,这意味着大多数esbuild插件在 sBun 中也可以正常工作。


    import { plugin } from "bun";

    plugin({
    name: "YAML",
    async setup(build) {
    const { load } = await import("js-yaml");
    const { readFileSync } = await import("fs");
    build.onLoad({ filter: /.(yaml|yml)$/ }, (args) => {
    const text = readFileSync(args.path, "utf8");
    const exports = load(text) as Record<string, any>;
    return { exports, loader: "object" };
    });
    },
    });

    Bun API


    Bun内部提供了针对开发者最常用需求的标准库API,并对其进行了高度优化。与Node.js的API不同,Node.js的API存在着向后兼容的考虑,而Bun的原生API则专注于提供更快速和更易于使用的功能。


    Bun.file()


    使用Bun.file()可以懒加载位于特定路径的文件。


    const file = Bun.file("package.json");
    const contents = await file.text();

    它返回一个扩展了 Web 标准FileBunFile对象。文件内容可以以多种格式进行懒加载。


    Bun.serve({
    port: 3000,
    fetch(request) {
    return new Response("Hello from Bun!");
    },
    });

    Bun每秒可以处理的请求比 Node.js 多 4 倍。


    也可以使用tls选项来配置TLS(传输层安全协议)。


    Bun.serve({
    port: 3000,
    fetch(request) {
    return new Response("Hello from Bun!");
    },
    tls: {
    key: Bun.file("/path/to/key.pem"),
    cert: Bun.file("/path/to/cert.pem"),
    }
    });

    Bun内置了对WebSocket的支持,只需要在websocket中定义一个事件处理程序来实现同时支持HTTP和WebSocket。而Node.js没有提供内置的WebSocket API,所以需要使用第三方依赖库(例如ws)来实现WebSocket的支持。因此,使用Bun可以更加方便和简单地实现WebSocket功能。


    Bun.serve({
    fetch() { ... },
    websocket: {
    open(ws) { ... },
    message(ws, data) { ... },
    close(ws, code, reason) { ... },
    },
    });

    Bun 每秒可以处理的消息比在 Node.js 上使用 ws 库多 5 倍。


    bun:sqlite


    Bun内置了对 SQLite 的支持。它提供了一个受到better-sqlite3启发的API,但是使用本地代码编写,以达到更快的执行速度。


    import { Database } from "bun:sqlite";

    const db = new Database(":memory:");
    const query = db.query("select 'Bun' as runtime;");
    query.get(); // => { runtime: "Bun" }

    在 Node.js 上,Bun 执行 SQLite 查询操作的速度比better-sqlite3快 4 倍。


    Bun.password


    Bun 还支持一些常见但复杂的API,不用自己去实现它们。


    例如,可以使用Bun.password来使用bcryptargon2算法进行密码哈希和验证,无需外部依赖。


    const password = "super-secure-pa$$word";
    const hash = await Bun.password.hash(password);
    // => $argon2id$v=19$m=65536,t=2,p=1$tFq+9AVr1bfPxQdh...

    const isMatch = await Bun.password.verify(password, hash);
    // => true

    Bun:包管理器


    Bun是一个包管理器。即使不使用Bun作为运行时环境,它内置的包管理器也可以加速开发流程。以前在安装依赖项时需要盯着npm的加载动画,现在可以通过Bun的包管理器更高效地进行依赖项的安装。


    Bun可能看起来像你熟悉的包管理器:


    bun install
    bun add <package> [--dev|--production|--peer]
    bun remove <package>
    bun update <package>

    安装速度


    Bun的安装速度比 npm、yarn 和 pnpm 快好几个数量级。它利用全局模块缓存来避免从npm注册表中重复下载,并使用每个操作系统上最快速的系统调用。
    image.png


    运行脚本


    很可能你已经有一段时间没有直接使用 Node 来运行脚本了。相反,通常使用包管理器(如npm、yarn等)与框架和命令行界面(CLI)进行交互,以构建应用。


    npm run dev

    你可以用bun run来替换npm run,每次运行命令都能节省 150 毫秒的时间。


    这些数字可能看起来很小,但在运行命令行界面(CLI)时,感知上的差异是巨大的。使用"npm run"会明显感到延迟:
    265893417-fbfb4172-5a91-4158-904f-55f2dbb0acde.gif而使用bun run则感觉几乎瞬间完成:
    image.png
    并不只是针对npm进行比较。实际上,bun run <command>的速度比yarn和pnpm中相应的命令更快。


    脚本运行平均时间
    npm run176ms
    yarn run131ms
    pnpm run259ms
    bun run7ms 🚀

    Bun:测试运行器


    如果你以前在 JavaScript 中写过测试,可能了解 Jest,它开创了“expect”风格的API。


    Bun有一个内置的测试模块bun:test,它与Jest完全兼容。


    import { test, expect } from "bun:test";

    test("2 + 2", () => {
    expect(2 + 2).toBe(4);
    });

    可以使用bun test命令来运行测试:


    bun test

    还将获得 Bun 运行时的所有优势,包括TypeScript和JSX支持。


    从Jest或Vite迁移很简单。@jest/globalsvitest的任何导入将在内部重新映射到bun:test,因此即使不进行任何代码更改,一切也将正常运行。


    import { test } from "@jest/globals";

    describe("test suite", () => {
    // ...
    });

    在与 zod 的测试套件进行基准测试中,Bun比Jest快13倍,比Vite快8倍。
    image.png
    Bun的匹配器由快速的原生代码实现,Bun中的expect().toEqual()比Jest快100倍,比Vite快10倍。


    可以使用bun test命令来加快 CI 构建速度,如果在Github Actions中,可以使用官方的oven-sh/setup-bun操作来设置Bun


    name: CI
    on: [push, pull_request]

    jobs:
    test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: oven-sh/setup-bun@v1
    - run: bun test

    Bun会自动为测试失败的部分添加注释,以便在持续集成(CI)日志中更容易理解。这样,当出现测试失败时,可以直接从日志中读取Bun提供的注释,而不需要深入分析代码和测试结果,从而更方便地检查问题所在。
    image.png


    Bun:构建工具


    Bun是一个JavaScript和TypeScript的构建工具和代码压缩工具,可用于将代码打包成适用于浏览器、Node.js和其他平台的形式。


    bun build ./index.tsx --outdir ./build

    Bun 受到了 esbuild 的启发,并提供了兼容的插件API。


    import mdx from "@mdx-js/esbuild";

    Bun.build({
    entrypoints: ["index.tsx"],
    outdir: "build",
    plugins: [mdx()],
    });

    Bun 的插件 API 是通用的,这意味着它适用于打包工具和运行时。所以前面提到的.yaml插件可以在这里使用,以支持在打包过程中导入.yaml文件。


    根据esbuild的基准测试,Bun比esbuild快1.75倍,比Parcel 2快150倍,比Rollup + Terser快180倍,比Webpack快220倍。
    image.png
    由于Bun的运行时和打包工具是集成在一起的,这意味着Bun可以做其他打包工具无法做到的事情。


    Bun引入了JavaScript宏机制,可以在打包时运行JavaScript函数。这些函数返回的值会直接内联到打包文件中。


    // release.ts
    export async function getRelease(): Promise<string> {
    const response = await fetch(
    "https://api.github.com/repos/oven-sh/bun/releases/latest"
    );
    const { tag_name } = await response.json();
    return tag_name;
    }

    // index.ts
    import { getRelease } from "./release.ts" with { type: "macro" };

    // release的值是在打包时进行评估的,并且内联到打包文件中,而不是在运行时执行。
    const release = await getRelease();

    bun build index.ts
    // index.ts
    var release = await "bun-v1.0.0";

    Bun:可以做更多事


    Bun 在 macOS 和 Linux 上提供了原生构建支持,但 Windows 一直是一个明显的缺失。以前,在 Windows 上运行 Bun 需要安装 Windows 子系统来运行Linux系统,但现在不再需要。


    Bun 首次发布了一个实验性的、专为Windows平台的本地版本的 Bun。这意味着Windows用户现在可以直接在其操作系统上使用 Bun,而无需额外的配置。
    image.png
    尽管Bun的macOS和Linux版本已经可以用于生产环境,但Windows版本目前仍然处于高度实验阶段。目前只支持JavaScript运行时,而包管理器、测试运行器和打包工具在稳定性更高之前都将被禁用。性能方面也还未进行优化。


    Bun:面向未来


    Bun 1.0 只是一个开始。Bun 团队正在开发一种全新的部署JavaScript和TypeScript到生产环境的方式,期待 Bun 未来更好的表现!


    作者:CUGGZ
    来源:juejin.cn/post/7277387014046335010
    收起阅读 »

    Htmx 意外走红,我们从 React“退回去”后:代码行数减少 67%,JS 依赖项从 255 下降到 9

    htmx 的走红 过去 Web 非常简单。URL 指向服务器,服务器将数据混合成 html,然后在浏览器上呈现该响应。围绕这种简单范式,诞生了各种 Javascript 框架,以前可能需要数月时间完成的一个应用程序基本功能,现在借助这些框架创建相对复杂的项目却...
    继续阅读 »

    htmx 的走红


    过去 Web 非常简单。URL 指向服务器,服务器将数据混合成 html,然后在浏览器上呈现该响应。围绕这种简单范式,诞生了各种 Javascript 框架,以前可能需要数月时间完成的一个应用程序基本功能,现在借助这些框架创建相对复杂的项目却只需要数小时,我们节省了很多时间,从而可以将更多精力花在业务逻辑和应用程序设计上。


    但随着 Web 不断地发展,Javascript 失控了。不知何故,我们决定向用户抛出大量 App,并在使用时发出不断增加的网络请求;不知何故,为了生成 html,我们必须使用 JSON,发出数十个网络请求,丢弃我们在这些请求中获得的大部分数据,用一个越来越不透明的 JavaScript 框架黑匣子将 JSON 转换为 html,然后将新的 html 修补到 DOM 中......


    难道大家快忘记了我们可以在服务器上渲染 html 吗?更快、更一致、更接近应用程序的实际状态,并且不会向用户设备发送任何不必要的数据?但是如果没有 Javascript,我们必须在每次操作时重新加载页面。



    现在,有一个新的库出现了,摒弃了定制化的方法,这就是 htmx。作为 Web 开发未来理念的一种实现,它的原理很简单:

    • 从任何用户事件发出 AJAX 请求。

    • 让服务器生成代表该请求的新应用程序状态的 html。

    • 在响应中发送该 html。

    • 将该元素推到它应该去的 DOM 中。


    htmx 出现在 2020 年,创建者Carson Gross 说 htmx 来源自他于 2013 年研究的一个项目intercooler.js。2020 年,他重写了不依赖 jQuery 的 intercooler.js,并将其重命名为 htmx。然后他惊讶的发现 Django 社区迅速并戏剧性地接受了它!



    图片来源:lp.jetbrains.com/django-deve…2021-486/


    Carson Gross认为 htmx 设法抓住了开发者对现有 Javascript 框架不满的浪潮,“这些框架非常复杂,并且经常将 Django 变成一个愚蠢的 JSON 生产者”,而 htmx 与开箱即用的 Django 配合得更好,因为它通过 html 与服务器交互,而 Django 非常擅长生成 html。


    对于 htmx 的迅速走红,Carson Gross 发出了一声感叹:这真是“十年窗下无人问,一举成名天下知(this is another example of a decade-long overnight success)”。


    htmx 的实际效果


    可以肯定的一点是 htmx 绝对能用,单从理论上讲,这个方法确实值得称道。但软件问题终究要归结于实践效果:效果好吗,能不能给前端开发带来改善?


    在 DjangoCon 2022 上,Contexte 的 David Guillot 演示了他们在真实 SaaS 产品上实现了从 React 到 htmx 的迁移,而且效果非常好,堪称“一切 htmx 演示之母”(视频地址:http://www.youtube.com/watch?v=3GO…)。


    Contexte 的项目开始于 2017 年,其后端相当复杂,前端 UI 也非常丰富,但团队非常小。所以他们在一开始的时候跟随潮流选择了 React 来“构建API绑定 SPA、实现客户端状态管理、前后端状态分离”等。但实际应用中,因为 API 设计不当,DOM 树太深,又需要加载很多信息,导致 UI“非常非常缓慢”。在敏捷开发的要求下,团队里唯一的 Javascript 专家对项目的复杂性表现得一无所措,因此他们决定试试 htmx。


    九大数据提升



    于是我们决定大胆尝试,花几个月时间用简单的 Django 模板和 htmx 替换掉了 SaaS 产品中已经使用两年的 React UI。这里我们分享了一些相关经验,公布各项具体指标,希望能帮同样关注 htmx 的朋友们找到说服 CTO 的理由!

    • 这项工作共耗费了约 2 个月时间(使用 21K 行代码库,主要是 JavaScript)

    • 不会降低应用程序的用户体验(UX)

    • 将代码库体积减小了 67%(由 21500 行削减至 7200 行)

    • 将 Python 代码量增加了 140%(由 500 行增加至 1200 行);这对更喜欢 Python 的开发者们应该是好事

    • 将 JS 总体依赖项减少了 96%(由 255 个减少至 9 个)


    • 将 Web 构建时间缩短了 88%(由 40 秒缩短至 5 秒)

    • 首次加载交互时间缩短了 50%至 60%(由 2 到 6 秒,缩短至 1 到 2 秒)

    • 使用 htmx 时可以配合更大的数据集,超越 React 的处理极限

    • Web 应用程序的内存使用量减少了 46%(由 75 MB 降低至 40 MB)



    这些数字令人颇为意外,也反映出 Contexte 应用程序高度契合超媒体的这一客观结果:这是一款以内容为中心的应用程序,用于显示大量文本和图像。很明显,其他 Web 应用程序在迁移之后恐怕很难有同样夸张的提升幅度。


    但一些开发者仍然相信,大部分应用程序在采用超媒体/htmx 方法之后,肯定也迎来显著的改善,至少在部分系统中大受裨益。


    开发团队组成


    可能很多朋友没有注意,移植本身对团队结构也有直接影响。在 Contexte 使用 React 的时候,后端与前端之间存在硬性割裂,其中两位开发者全职管理后端,一位开发者单纯管理前端,另有一名开发者负责“全栈”。(这里的「全栈」,代表这位开发者能够轻松接手前端和后端工作,因此能够在整个「栈」上独立开发功能。)



    而在移植至 htmx 之后,整个团队全都成了“全栈”开发人员。于是每位团队成员都更高效,能够贡献出更多价值。这也让开发变得更有乐趣,因为开发人员自己就能掌握完整功能。最后,转向 htmx 也让软件优化度上了一个台阶,现在开发人员可以在栈内的任意位置进行优化,无需与其他开发者提前协调。


    htmx 是传统思路的回归


    如今,单页应用(SPA)可谓风靡一时:配合 React、Redux 或 Angular 等库的 JS 或 TS 密集型前端,已经成为创建 Web 应用程序的主流方式。以一个需要转译成 JS 的 SPA 应用为例:



    但 htmx 风潮已经袭来,人们开始强调一种“傻瓜客户端”方法,即由服务器生成 html 本体并发送至客户端,意味着 UI 事件会被发送至服务器进行处理。



    用这个例子进行前后对比,我们就会看到前者涉及的活动部件更多。从客户端角度出发,后者其实回避了定制化客户端技术,采取更简单的方法将原本只作为数据引擎的服务器变成了视图引擎。


    后一种方法被称为 AJAX(异步 JavaScript 与 XML)。这种简单思路能够让 Web 应用程序获得更高的响应性体验,同时消除了糟糕的“回发”(postback,即网页完全刷新),由此回避了极其低效的“viewstate”等.NET 技术。


    htmx 在很多方面都体现出对 AJAX 思路的回归,最大的区别就是它仅仅作为新的声明性 html 属性出现,负责指示触发条件是什么、要发布到哪个端点等。


    另一个得到简化的元素是物理应用程序的结构与构建管道。因为不再涉及手工编写 JS,而且整个应用程序都基于服务器,因此不再对 JS 压缩器、捆绑器和转译器做(即时)要求。就连客户端项目也能解放出来,一切都由 Web 服务器项目负责完成,所有应用程序代码都在.NET 之上运行。从这个角度来看,这与高度依赖服务器的Blazor Server编程模型倒是颇有异曲同工之妙。


    技术和软件开发领域存在一种有趣的现象,就是同样的模式迭起兴衰、周而复始。随着 SPA 的兴起,人们一度以为 AJAX 已经过气了,但其基本思路如今正卷土重来。这其中当然会有不同的权衡,例如更高的服务器负载和网络流量(毕竟现在我们发送的是数据视图,而不只是数据),但能让开发者多个选择肯定不是坏事。


    虽然不敢确定这种趋势是否适用于包含丰富用户体验的高复杂度应用程序,但毫无疑问,相当一部分 Web 应用程序并不需要完整的 SPA 结构。对于这类用例,简单的 htmx 应用程序可能就是最好的解决方案。


    参考链接:


    news.ycombinator.com/item?id=332…


    htmx.org/essays/a-re…


    http://www.reddit.com/r/django/co…


    mekhami.github.io/2021/03/26/…


    http://www.compositional-it.com/news-blog/m…


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

    Taro开发小程序记录-海报生成

    Taro开发小程序记录-海报生成在公司开发的第一个项目是一个简单的Taro小程序项目,主要应用的则是微信小程序,由于是第一次使用Taro,所以在开发过程中遇到的一些问题和一些开发技巧,记录一下,方便以后进行查看,也与大家进行分享。 自定义海报 说到自定义海报...
    继续阅读 »

    Taro开发小程序记录-海报生成


    在公司开发的第一个项目是一个简单的Taro小程序项目,主要应用的则是微信小程序,由于是第一次使用Taro,所以在开发过程中遇到的一些问题和一些开发技巧,记录一下,方便以后进行查看,也与大家进行分享。



    自定义海报


    说到自定义海报可以说是很多小程序中都会进行开发的内容,比如需要进行二维码的保存,然后再对二维码进行一点文字的修饰,涉及到这方面的时候我们就需要使用canvas了。


    在实际开发的过程中,遇到了一些很坑的问题,当我们需要使用离屏canvas来进行绘制时,我们可能就会遇到问题(我自己就遇到了)。


    对于安卓端,我们可以正常的使用OffscreenCanvas来创建离屏canvas,然后绘制相关内容,最后在使用Taro.canvasToTempFilePath方法保存到临时文件下,Taro.canvasToTempFilePath方法会返回文件路径,我们就可以通过获取到的文件路径来进行下载。


    下面是安卓端的一个🌰,大家有需要也可以直接拿去使用


    • 需要使用到的方法
     /**
      * @description 获取二维码图像
      */
     export const qrCodeImage = async (qrCodeValue: string, size: number = 128) => {
         /* NOTE: 通过创建离屏canvas承载code */
         const context = createOffscreenCanvas('2d', size, size);
         QRCode.toCanvas(context, qrCodeValue, { width: size, height: size, margin: 1 });
         return (context as unknown as HTMLCanvasElement).toDataURL();
     };
     /**
      * @description 创建离屏canvas对象,width与height单位为px
      */
     export const createOffscreenCanvas = (type: '2d' | 'webgl', width: number = 100, height: number = 100) => {
         return Taro.createOffscreenCanvas({ type, width, height });
     };
     /**
      * @description 将传入的图片url转换成一个ImageElement对象
      */
     export const loadImageByUrlToCanvasImageData = async (url: string, width: number = 100, height: number = 100) => {
         const context = createOffscreenCanvas('2d', width, height);
         const imageElement = context.createImage();
         await new Promise(resolve => {
             imageElement.onload = resolve;
             imageElement.src = url;
        });
         return imageElement;
     };
     /**
      * @description 将canvas转成图片文件并保存在临时路径下
      */
     export const changeCanvasToImageFileAndSaveToTempFilePath = async (options: Taro.canvasToTempFilePath.Option) => {
         const successCallback = await Taro.canvasToTempFilePath(options);
         return successCallback.tempFilePath;
     };
     interface SettingOptions {
         title: string;
         titleInfo: {
             dx: number;
             dy: number;
             color?: string;
             font?: string;
        };
         imageUrl: string;
         imagePos: {
             dx: number;
             dy: number;
        };
         width: number;
         height: number;
     }
     /**
      * @description 获取二维码图像并设置标题
      */
     export const generateQrCodeWithTitle = async (option: SettingOptions): Promise<string> => {
         const {
             title,
             titleInfo,
             imageUrl,
             imagePos,
             width,
             height,
        } = option;
         const context = await createOffscreenCanvas('2d', width, height);
         const ctx = (context.getContext('2d') as CanvasRenderingContext2D);
         const imgElement: any = await loadImageByUrlToCanvasImageData(imageUrl, width, height);
         ctx.fillStyle = 'white';
         ctx.fillRect(0, 0, width, height);
         ctx.fillStyle = titleInfo.color || 'black';
         ctx.font = titleInfo.font || '';
         ctx.textAlign = 'center';
         ctx.fillText(title, titleInfo.dx || 0, titleInfo.dy || 0);
         ctx.drawImage(imgElement, imagePos.dx, imagePos.dy);
     
         const filePath = await changeCanvasToImageFileAndSaveToTempFilePath({
             canvas: (context as Canvas),
             width,
             height,
             fileType: 'png',
             destWidth: width,
             destHeight: height,
        });
         return filePath;
     };
     /**
      * @description 保存图片
      */
     export const saveImage = async (urls: string[], isLocal: boolean = true) => {
         let filePath = urls;
         if (!isLocal) {
             filePath = await netImageToLocal(urls);
        }
         await Promise.all(filePath.map(path => {
             return Taro.saveImageToPhotosAlbum({ filePath: path });
        }));
     
         return true;
     };
     /**
      * @description 加载在线图片,并返回临时图片文件地址
      */
     export const netImageToLocal = async (urls: string[]) => {
         const res = await Promise.all(urls.map((url:string) => {
             return Taro.downloadFile({ url });
        }));
     
         const result = res.map(data => {
             if (data.statusCode === 200) {
                 return data.tempFilePath;
            }
             throw new Error(data.errMsg);
        });
     
         return result;
     };
     /**
      * @description 判断用户是否授权保存图片
      */
     export const checkHasAuthorizedSaveImagePermissions = async () => {
         const setting = await Taro.getSetting();
         const { authSetting } = setting;
         return authSetting['scope.writePhotosAlbum'];
     };
     /**
      * @description 下载图片,需要区分是本地图片还是在线图片
      */
     export const downloadImage = async (urls: string[], isLocal: boolean = true) => {
         const hasSaveImagePermissions = await checkHasAuthorizedSaveImagePermissions();
         if (hasSaveImagePermissions === undefined) {
             // NOTE: 用户未授权情况下,进行用户授权,允许保存图片
             await Taro.authorize({ scope: 'scope.writePhotosAlbum' });
             return await saveImage(urls, isLocal);
        } else if (typeof hasSaveImagePermissions === 'boolean' && !hasSaveImagePermissions) {
             return new Promise((resolve, reject) => {
                 Taro.showModal({
                     title: '是否授权保存到相册',
                     content: '需要获取您的保存图片权限,请确认授权,否则图片将无法保存到相册',
                     success: (result) => {
                         if (result.confirm) {
                             Taro.openSetting({
                                 success: async (data) => {
                                     if (data.authSetting['scope.writePhotosAlbum']) {
                                         showLoadingModal('正在保存...');
                                         resolve(await saveImage(urls, isLocal));
                                    }
                                },
                            });
                        } else {
                             reject(new Error('未授予保存权限'));
                        }
                    },
                });
            });
        }
         await saveImage(urls, isLocal);
         return true;
     };

    • 生成海报(二维码+标题头)

     /**
      * @description 获取二维码图像并设置标题
      */
     export const generateQrCodeWithTitle = async (option: SettingOptions): Promise<string> => {
         const {
             title,
             titleInfo,
             imageUrl,
             imagePos,
             width,
             height,
        } = option;
         const context = await createOffscreenCanvas('2d', width, height);
         const ctx = (context.getContext('2d') as CanvasRenderingContext2D);
         const imgElement: any = await loadImageByUrlToCanvasImageData(imageUrl, width, height);
         ctx.fillStyle = 'white';
         ctx.fillRect(0, 0, width, height);
         ctx.fillStyle = titleInfo.color || 'black';
         ctx.font = titleInfo.font || '';
         ctx.textAlign = 'center';
         ctx.fillText(title, titleInfo.dx || 0, titleInfo.dy || 0);
         ctx.drawImage(imgElement, imagePos.dx, imagePos.dy);
     
         const filePath = await changeCanvasToImageFileAndSaveToTempFilePath({
             canvas: (context as Canvas),
             width,
             height,
             fileType: 'png',
             destWidth: width,
             destHeight: height,
        });
         return filePath;
     };

    • 具体使用

     export const saveQrCodeImageWithTitle = async () => {
         const url = await qrCodeImage(enterAiyongShopUrl(), 160);
         const imgUrl: string = await generateQrCodeWithTitle({
             title: 'adsionli菜鸡前端',
             titleInfo: {
                 dx: 95,
                 dy: 20,
                 font: '600 14px PingFang SC',
                 color: 'black',
            },
             imageUrl: url,
             imagePos: {
                 dx: 15,
                 dy: 34,
            },
             width: 190,
             height: 204,
        });
         await downloadImage([imgUrl]);
     }


    上面三块内容就可以组成我们的海报生成了,这里面的主要步骤不是很难,包括了几个方面:


    1. 用户授权鉴定,主要是是否允许保存,这里做了一点处理,就是可以在用户第一次授权不允许时,进行二次授权调起,这个可以看一下上面的downloadImage这个函数,以及用于判断用户是否授权的checkHasAuthorizedSaveImagePermissions这个函数
    2. 创建OffscreenCanvas并进行绘制,这里其实没有太多的难点,主要就是需要知道,如果我们使用image的内容的话,或者是一个图片的url时,我们需要先将其绘制到一个canvas上(这里可以获取imageElement对象,也可以直接使用canvas),这样方便我们后面进行drawImage时进行使用
    3. 图片保存,这里也有一个需要注意的点,如果图片(或二维码)是网络图片的话,我们需要处理以下,先将其转成本地图片,也就是通过netImageToLocal这个方法,然后再还给对应的将图片画在canvas上的方法。最后的保存很简单,我们可以直接使用Taro.canvasToTempFilePath这个方法转到临时地址,再通过downloadImage就可以搞定了。

    感觉好像很麻烦,其实就四步:图片加载转化—>canvas绘制—>用户鉴权—>图片保存。


    安卓端实现起来还是很简单的,但是这些方法对于ios端就出现了问题,如果按照上面的路线进行海报绘制保存的话,在ios端就会报一个错误(在本地开发的时候并不会抛出): canvasToTempFilePath:fail invalid viewId


    这一步错误就是发生在Taro.canvasToTempFilePath这里,保存到临时文件时会触发,然后这一切的原因就是使用了OffscreenCanvas离屏canvas造成的。


    所以为了能够兼容ios端的这个问题,有了以下的修改:


    首先需要在我们要下载海报的pages中,添加一个Canvas,帮助我们可以获取CanvasElement

     <Canvas
         type='2d'
         id='qrCodeOut'
         className='aiyong-shop__qrCode'
     />

    这里需要注意一下,我们需要添加一个type='2d'的属性,这是为了能够使用官方提供的获取Canvas2dContext的属性,这样就可以不使用createCanvasContext这个方法来获取了(毕竟已经被官方停止维护了)。


    然后我们就可以获取一下CanvasElement对象了

     /**
      * @description 获取canvas标签对象
      */
     export const getCanvasElement = (canvasId: string): Promise<Taro.NodesRef> => {
         return new Promise(resolve => {
             const canvasSelect: Taro.NodesRef = selectQuery().select(`#${canvasId}`);
             canvasSelect.node().exec((res: Taro.NodesRef) => {
                 resolve(res);
            });
        });
     };

    注:这里又有一个小坑,我们在获取CanvasElement之后,如果直接进行绘制的话,这里存在一个问题,就是这个CanvasElementwidth:300、height:150被限制死了,所以我们需要自己在拿到CanvasElement之后,在设置一下width、height

     const canvasNodeRef = await getCanvasElement(canvas);
     let context;
     if (canvasNodeRef && canvasNodeRef[0].node !== null) {
         context = canvasNodeRef[0].node;
        (context as Taro.Canvas).width = width;
        (context as Taro.Canvas).height = height;
     }

    好了,改造完成,这样就可以兼容ios端的内容了,实际我们只需要修改generateQrCodeWithTitle这个方法和page新增Canvas用于获取CanvasElement就可以了,其他可以不要动。修改后的generateQrCodeWithTitle方法如下:

     /**
      * @description 获取二维码图像并设置标题
      */
     export const generateQrCodeWithTitle = async (option: SettingOptions): Promise<string> => {
         const {
             title,
             titleInfo,
             imageUrl,
             imagePos,
             width,
             height,
             qrCodeSize,
             canvas,
        } = option;
         const canvasNodeRef = await getCanvasElement(canvas);
         let context;
         if (canvasNodeRef && canvasNodeRef[0].node !== null) {
             context = canvasNodeRef[0].node;
            (context as Taro.Canvas).width = width;
            (context as Taro.Canvas).height = height;
        }
         const ctx = (context.getContext('2d') as CanvasRenderingContext2D);
         const imgElement: Taro.Image = await loadImageByUrlToCanvasImageData(imageUrl, qrCodeSize.width, qrCodeSize.height);
         ctx.fillStyle = 'white';
         ctx.fillRect(0, 0, width, height);
         ctx.fillStyle = titleInfo.color || 'black';
         ctx.font = titleInfo.font || '';
         ctx.textAlign = 'center';
         ctx.fillText(title, titleInfo.dx || 0, titleInfo.dy || 0);
         ctx.drawImage((imgElement as HTMLImageElement), imagePos.dx, imagePos.dy, imgElement.width, qrCodeSize.height);
     
         const filePath = await changeCanvasToImageFileAndSaveToTempFilePath({
             canvas: (context as Canvas),
             width,
             height,
             fileType: 'png',
             destWidth: width,
             destHeight: height,
        });
         return filePath;
     };


    如果大家不想让海报被人看到,那可以设置一下css

     .qrCode {
         position: fixed;
         left: 100%;
     }

    这样就可以啦



    突然发现内容可能有点多了,所以打算分成两篇进行Taro使用过程中的总结,开发完之后进行总结,总是可以让自己回顾在开发过程中遇到的问题的进一步进行思考,这是一个很好的进步过程,加油加油!!!


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

    chrome 搞事,下个月全面删除 Event.path

    背景 前两天下午,测试同学反馈生产环境有个功能出现异常,然后给我发了张报错截图。  随后在测试同学的电脑复现并排查问题,通过异常断点,捕捉到异常信息如下:  可以看到异常的原因是 n.path 的值为 undefined,因此 n.path...
    继续阅读 »

    背景


    前两天下午,测试同学反馈生产环境有个功能出现异常,然后给我发了张报错截图。 



    随后在测试同学的电脑复现并排查问题,通过异常断点,捕捉到异常信息如下: 



    可以看到异常的原因是 n.path 的值为 undefined,因此 n.path.find 等价于 undefined.find,因此程序报错。其中 n 是一个 Event 实例;path 是事件冒泡经过的节点组成的数组。n.path 有值的情况如下: 


    Event.path 不是标准属性,常见于 chrome 浏览器,取不到值出现异常不足为奇,只要做个兼容就搞定了,当 Event.path 取不到值时就取 Event.composedPath()。那这是兼容性问题吗,事情好像没有这么简单。仔细对比上述两张截图可以发现,异常时 Event 实例甚至不存在 path 属性,这跟属性存在但值为空是两码事。


    进一步排查


    好好的 path 属性怎么就不翼而飞了,这是个神奇的问题。当我用自己电脑尝试复现问题时,发现功能正常且无法复现,事情变得更加神奇。两边浏览器都升到了最新版 chrome 108,区别是系统不同,一个是 windows 一个是 macOS。也就是 说同样的代码在同样的软件上跑出了不同结果,这说明可能不是代码或兼容性问题。


    为找出真正的原因,我做了几组对照实验,以排除代码、硬件、操作系统和浏览器的影响。情况如下 :


    分析这些结果,出现了更有意思的事:只有一种情况会出现异常,使用测试同学的电脑且浏览器是 chrome 108;当改变电脑、系统、浏览器、浏览器版本等因素时结果都是正常。 也就是说导致异常的因素居然不是单一的,而是多个因素组合(测试同学电脑+chrome+108 版本)产生的结果。



    chromium issue 的助攻


    从上面的结果看好像没办法再继续排查下去,不过从经验判断,多半是 chrome 又在搞事,这时候可以去 chromium issue 里找找蛛丝马迹,经过一番搜索找到了这条 issue: Issue 1277431: Remove event.path。 



    issue 标题很直白,Event.path 将被删除。 从 issue 内容可以看到,这次搞事是从 2021 年 12 月 7 日开始,起因是 chromium 开发团队认为 Event.path 属于非标准 API,会导致 Firefox 等其他浏览器的兼容性问题,于是他们决定将其删除。目前这个变更在 chrome 108 属于灰度阶段,在即将发布的 chrome 109 上会全面应用,webview 则是从 109 版本开始逐步禁用。


    变更详情和计划


    另外 issue 中提到这个变更会在 console 中进行告警。 



    console 中确实有这个告警,不过藏在 console 面板的右上角,不太容易发现,而且需要调用 Event.path 后才会显示。点进去之后会跳转到 Issues 面板并显示详细信息。
     



    从图中可以看到这个变更属于 Breaking Change,即破坏性变更。另外可以看到变更详情链接版本计划链接。打开变更详情链接可以看到详细的说明、目的、状态、开发阶段等信息。 



    打开版本计划链接可以看到,chrome 108 已经在 2022-11-29 正式发布(Stable Release Tue, Nov 29, 2022),chrome 109 将在 2023-01-10 正式发布(Stable Release Tue, Jan 10, 2023)。 



    验证


    由于英文水平有限,为了避免个人理解存在歧义,使用 chrome 的前瞻版本进行测试,以验证 chrome 108 之后的版本是否真的会应用这个变更。


    • 测试使用的系统为 macOS,浏览器版本包括:chrome-stable(108.0.5359.124)、chrome-beta(109.0.5414.36)、chrome-dev(110.0.5464.2)、chrome-canary(110.0.5477.0)。



    • 测试代码如下
    <!DOCTYPE html>
    <html lang="en">
    <head></head>
    <body>
    <script>
    function test() {
    console.log("event.path is:", window.event.path);
    }
    </script>
    <h1 onclick="test()">click me</h1>
    </body>
    </html>

    • 测试结果如下

    chrome-stable(108.0.5359.124)在 macOS 下 Event.path 有值,结合上文的对照实验中 windows10 下一个有值一个为空。说明 chrome 108 中该变更属于灰度阶段。
    image.png


    chrome-beta(109.0.5414.36)、chrome-dev(110.0.5464.2)、chrome-canary(110.0.5477.0)在 macOS 下 Event.path 都为空,说明 chrome 109 之后全面删除了 Event.path 属性。
    image.png


    解决方案


    先看影响范围,从项目维度来看,所有前端项目都可能受到影响;从代码维度来看,项目源码和第三方依赖都可能受影响。在 github 中搜索发现 swipperopenlayers 等第三方库中都有相关 issue。因此解决方案需要全面考虑:最好对所有项目都进行排查修复,另外不仅要排查源码,还要考虑第三方库。


    根据官方建议及综合考虑,推荐在前端项目中统一添加如下 polyfill 代码:

      Object.defineProperty(Event.prototype, "path", {
    get() {
    return this.composedPath();
    },
    });

    最后


    chrome 109 预计在 2023-01-10 正式发布,届时会全面禁用 Event.path,所有源码中使用该属性或第三方库使用该属性的前端项目都可能会出现异常,还有 20 几天时间,建议尽快排查修复。


    一些经验

  • 关注 devtools 中的 console、issue 等各种告警信息,有助于调试和排查问题、以及发现潜在的问题
    • 关注 chorme 迭代计划,有条件可以做前瞻性测试,预防未来可能发生的异常



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

    如何在上班时间利用终端控制台摸鱼🧐🧐🧐

    web
    作为一个资深的摸鱼小能手,班我们每天要上,终端也是我们也要每天要用到的,那么有什么办法可以在控制台终端中去摸鱼呢,那么在接下来的文章中我们就来看看它可以做到怎么样摸鱼。 简介 在我们开发的项目中,几乎有很多项目要都是使用 webpack 作为构建工具来进行开发...
    继续阅读 »

    作为一个资深的摸鱼小能手,班我们每天要上,终端也是我们也要每天要用到的,那么有什么办法可以在控制台终端中去摸鱼呢,那么在接下来的文章中我们就来看看它可以做到怎么样摸鱼。


    简介


    在我们开发的项目中,几乎有很多项目要都是使用 webpack 作为构建工具来进行开发的,在它进行构建的时候,会有一些信息会输出在控制台上面,如下图所示:


    20230910150719


    爱瞎折腾的朋友们可能就会想了,为什么 create-react-pp 也是用的 webpack 作为构建工具,为什么我的输出和它的输出是不一样的呢?


    20230910150945


    compiler


    通过查阅文档,我发现了问题所在,原来在 webpack 中它提供了一个 compiler 钩子,它用来监控文件系统的 监听(watching) 机制,并且在文件修改时重新编译。 当处于监听模式(watch mode)时, compiler 会触发诸如 watchRun, watchClose 和 invalid 等额外的事件。


    done 钩子就是当我们的代码被编译完成的时候被调用的。


    如何调用 done 钩子


    要想调用我们的 done 钩子,首先我们要引入 webpack 包,并把 webpack 配置传递给 webpack 函数,如下图所示:


    20230910151624


    接下来我们看看终端输出:


    20230910151749


    这些就是我们的一些 webpack 配置,在这个 compiler 对象上,它存在一个 hooks 对象,如下代码所示:


    compiler.hooks.done.tap("done", async (stats) => {
    console.log(11111111111111);
    });

    它会在代码编译完成阶段调用该回调函数:


    20230910152621


    咦,你会发现了,代码编译执行完成,我的终端上的输出会这么干净,是因为在输出控制台之前, 已经被我调用了一个函数清空了。


    通过这个函数,你可以情况控制台上的一些输出信息,如下代码所示:


    function clearConsole() {
    process.stdout.write(
    process.platform === "win32" ? "\x1B[2J\x1B[0f" : "\x1B[2J\x1B[3J\x1B[H"
    );
    }

    再调用以下,你会发现控制台上面很干净的,图下图所示:


    20230910153357


    要想这一些个性化的输出,我们直接在这个回调函数中打印输出就可以了,如果你要你输出的信息和项目中的信息有关,你可以利用 stats 这个参数:


    20230910160905


    大概就这样子,如果你想更好玩的话,你可以使用一些网络请求库,去获取一些网络资源:


    20230910161247


    去获取这些资源都是可以的呀。


    总结


    如果你的项目是使用的 webpack,并且要想在项目的开发中自定义,你可以通过 compiler.hooks 的方式去监听不同的钩子,然后通过不同的方式来实现不同的信息输出。


    源代码地址


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

    发送验证码后的节流倒计时丨刷新 & 重新进入页面,还原倒计时状态

    web
    前言   最近在做一个 H5 工具,需要手机号 + 验证码登录,很自然地,点击发送验证码后需要等待一段时间才能重新发送,用于请求节流,避免用户疯狂点击:     不过这里其实有个隐藏需求——如果仍然在冷却时间内,那么用户无论是刷新或是关闭页面,再次打开登...
    继续阅读 »

    前言


      最近在做一个 H5 工具,需要手机号 + 验证码登录,很自然地,点击发送验证码后需要等待一段时间才能重新发送,用于请求节流,避免用户疯狂点击:





     

      不过这里其实有个隐藏需求——如果仍然在冷却时间内,那么用户无论是刷新或是关闭页面,再次打开登录弹窗,需要直接展示正确的倒计时状态


    解决方案



    使用经典的 localStorage




    1. 发送验证码时,将发送时间 (lastSendingTime) 存入 localStorage,并开启 60 秒倒计时。

    2. 倒计时结束后,清除 localStorage 中的 lastSendingTime

    3. 重新进入页面时,若 localStorage 中存有 lastSendingTime,则说明仍处于冷却时间内,那么计算出剩余的倒计时 N,并开启 N 秒倒计时。


    Talk is cheap, show me the code!


      const [countdown, setCountdown] = useState(60) // 倒计时
    const [canSendCode, setCanSendCode] = useState(true) // 控制按钮文案的状态
    const [timer, setTimer] = useState() // 定时器 ID

    async function sendVerificationCode() {
    try {
    // network request...
    Toast.show({ content: '验证码发送成功' })
    startCountdown()
    setCanSendCode(false)
    } catch (error) {
    setCountdown(0)
    setCanSendCode(true)
    }
    }

    function startCountdown() {
    const nowTime = new Date().getTime()
    const lastSendingTime = localStorage.getItem('lastSendingTime')
    if (lastSendingTime) {
    // 若 localStorage 中存有 lastSendingTime,则说明仍处于冷却时间内,计算出剩余的 countdown
    const restCountdown = 60 - parseInt(((nowTime - lastSendingTime) / 1000), 10)
    setCountdown(restCountdown <= 0 ? 0 : restCountdown)
    } else {
    // 否则说明冷却时间已结束,则 countdown 为 60s,并将发送时间存入 localStorage
    setCountdown(60)
    localStorage.setItem('lastSendingTime', nowTime)
    }

    setTimer(
    setInterval(() => {
    setCountdown(old => old - 1)
    }, 1000),
    )
    }

    // 重新进入页面时,若 localStorage 中存有上次的发送时间,则说明还处于冷却时间内,则调用函数计算剩余倒计时;
    // 否则什么也不做
    useEffect(() => {
    const lastSendingTime = localStorage.getItem('lastSendingTime')
    if (lastSendingTime) {
    setCanSendCode(false)
    startCountdown()
    }

    return () => {
    clearInterval(timer)
    }
    }, [])


    // 监听倒计时,倒计时结束时:
    // * 清空 localStorage 中存储的上次发送时间
    // * 清除定时器
    // * 重置倒计时
    useEffect(() => {
    if (countdown <= 0) {
    setCanSendCode(true)
    localStorage.removeItem('lastSendingTime')
    clearInterval(timer)
    setCountdown(60)
    }
    }, [countdown])

    return (
    {canSendCode ? (
    <span onClick={sendVerificationCode}>
    获取验证码
    </span>

    ) : (
    <span>
    获取验证码({`${countdown}`})
    </span>

    )}
    )

    最终效果





    总结


      一开始感觉这是个很简单的小需求,可能 20min 就写完了,但实际花了两个多小时才把逻辑全部 cover 到,还是不能太自信啊~


    作者:Victor_Ye
    来源:juejin.cn/post/7277187894872014848
    收起阅读 »

    别再用 display: contents 了

    web
    文章讨论了在网站上使用"display: contents"属性可能导致的潜在问题。作者强调了这种做法可能破坏网页的语义结构,并可能对可访问性产生不利影响。文章还提到了一些潜在的解决方案,以帮助开发人员避免这些问题。 下面是正文~~ display: cont...
    继续阅读 »

    文章讨论了在网站上使用"display: contents"属性可能导致的潜在问题。作者强调了这种做法可能破坏网页的语义结构,并可能对可访问性产生不利影响。文章还提到了一些潜在的解决方案,以帮助开发人员避免这些问题。


    下面是正文~~


    display: contents 介绍


    CSS(层叠样式表)中的 display: contents 是一个相对较新的属性值,它对元素的布局和可视化有特殊的影响。当你对一个元素应用 display: contents,这个元素本身就像从DOM(文档对象模型)中消失了一样,而它的所有子元素则会升级到DOM结构中的下一个层级。换句话说,该元素的盒模型将被忽略,它的子元素会取而代之,就像直接插入到父元素中一样。


    假设我们有这样一个HTML结构:


    id="parent">
    id="child1">Child 1
    id="child2">Child 2

    正常情况下,#parent#child1#child2 的父元素,它们在DOM和布局中有一个明确的层级关系。


    现在,如果我们对 #parent 应用 display: contents


    #parent {
    display: contents;
    }

    在这种情况下,#parent 在页面布局中就像是“消失了”一样。它的所有子元素(这里是 #child1#child2)会直接升级到#parent所在的DOM层级。也就是说,在布局和渲染过程中,#child1#child2 将不再被视为 #parent 的子元素,而是像直接插入到 #parent 的父元素中一样。


    这样做的结果是,任何应用于 #parent 的布局和样式都不会影响到页面的渲染,但 #child1#child2 会像正常元素一样被渲染。


    主要用途:



    1. 语义改进:能够改进HTML结构,使其更符合语义,但不影响布局和样式。

    2. 布局优化:在某些复杂的布局场景中,它可以简化DOM结构,提高渲染性能。


    display: contents 和可访问性的长期问题


    从字面上看,这个CSS声明改变了其应用到的元素的显示属性。它使元素“消失”,将其子元素提升到DOM中的下一层级。


    这种声明在很多方面都可能是有用的。讽刺的是,其中一个用例就是改善你工作的底层语义。然而,这个声明一开始的效果有点过头了。


    CSS和可访问性


    不是每个人都意识到这一点,但某些CSS会影响辅助技术的工作方式。就像烧毁你的房子确实会成功地除去其中可能存在的蜘蛛一样,使用 display: contents 可能会完全消除某些元素被辅助技术识别的关键属性。


    简而言之,这会导致按钮不被声明为按钮,表格不被声明和导航为表格,列表也是如此,等等。


    换句话说:当人们说“HTML默认是可访问的”时,display: contents 彻底破坏了这个“默认”。这不好。


    可访问性从业者注意到了这个问题,并提出了完全合理的修复要求。特别值得一提的是Adrian Roselli的勤勉、有条理和实事求是的文档和报告工作。


    修复已经完成,浏览器也已经更新,我们得到了一个快乐的结局。对吗?并不是那么简单。


    回归问题


    在软件开发中,回归可能意味着几件事情。这个词通常用于负面语境,表达更新后的行为不小心恢复到以前,不太理想的工作方式。


    对于 display: contents,这意味着每个人的自动或近乎自动更新的浏览器抛弃了非常必要的错误修复,而没有任何警告或通知,就回到了破坏语义HTML与辅助技术交流的基础属性。


    这种类型的回归不是一个令人讨厌的 bug,而是破坏了 Web 可访问性的基础方面。


    Adrian注意到了这一点。如果你继续阅读我给你链接的部分,他继续注意到这一点。总之,我统计了关于 display: contents 的行为以不可访问的方式回归了16次的更新。


    看问题的角度


    制作浏览器是一件困难的事情。需要考虑很多、很多不同的事情,那还没考虑到软件的复杂性。


    可访问性并不是每个人的首要任务。我可以在这里稍微宽容一些,因为我主要是尝试用我拥有的东西工作,而不是我希望能有的东西。我习惯了应对由于这种优先级而产生的所有小问题、陷阱和杂项。


    然而,能够使用Web界面绝非小事。display: contents 的问题对使用它的界面的人们的生活质量有非常真实、非常可量化的影响。


    我还想让你考虑一下这种打地鼠游戏是如何影响可访问性从业者的。告诉某人他们不能使用一个闪亮的新玩具永远不会受到欢迎。然后告诉他们你可以,但后来又不能了,这会削弱信任和能力的认知。


    别用 display: contents


    现在,我不认为我们这个行业可以自信地使用 display: contents。过去的行为是未来行为的良好指标,而走向地狱的道路是由好意铺成的。


    我现在认为这个声明是不可预测的。常见的“只需用辅助技术测试其支持情况”的回应在这里也不适用——当前浏览器版本中该声明的期望行为并不能保证在该浏览器的未来版本中持续。


    这是一件罕见且令人不安的事情——整个现代Web都是建立在这样的假设之上,即这样的事情不会以这种方式停止工作。这不是互操作性问题,而是由于疏忽造成的伤害。


    display: contents 的回归给我们提供了一个小小的窗口,让我们看到浏览器制作的某些方面是如何(或不是如何)被优先考虑和测试的。


    人们可以发誓说像可访问性和包容性这样的事情是重要的,但当涉及到这个特定的CSS声明时,很明显大多数浏览器制造商是不可信的。


    这个声明在实践中的不稳定性代表了一种非常真实、非常严重的风险,即在你无法控制的情况下,可能会在你的网站或Web应用中引入关键的可访问性问题。


    作者:王大冶
    来源:juejin.cn/post/7275973778915573772
    收起阅读 »

    产品:请给我实现一个在web端截屏的功能!

    web
    一、故事的开始 最近产品又开始整活了,本来是毫无压力的一周,可以小摸一下鱼的,但是突然有一天跟我说要做一个在网页端截屏的功能。 作为一个工作多年的前端,早已学会了尽可能避开麻烦的需求,只做增删改查就行! 我立马开始了我的反驳,我的理由是市面上截屏的工具有很多的...
    继续阅读 »

    一、故事的开始


    最近产品又开始整活了,本来是毫无压力的一周,可以小摸一下鱼的,但是突然有一天跟我说要做一个在网页端截屏的功能。


    作为一个工作多年的前端,早已学会了尽可能避开麻烦的需求,只做增删改查就行!


    我立马开始了我的反驳,我的理由是市面上截屏的工具有很多的,微信截图、Snipaste都可以做到的,自己实现的话,一是比较麻烦,而是性能也不会很好,没有必要,把更多的时间放在核心业务更合理!


    结果产品跟我说因为公司内部有个可以用来解析图片,生成文本OCR的算法模型,web端需要支持截取网页中部分然后交给模型去训练,微信以及其他的截图工具虽然可以截图,但需要先保存到本地,再上传给模型才行。


    网页端支持截图后可以在在截屏的同时直接上传给模型,减少中间过程,提升业务效率。


    我一听这产品小嘴巴巴的说的还挺有道理,没有办法,只能接了这个需求,从此命运的齿轮开始转动,开始了我漫长而又曲折的思考。


    二、我的思考


    在实现任何需求的时候,我都会在自己的脑子中大概思考一下,评估一下它的难度如何。我发现web端常见的需求是在一张图片上截图,这个还是比较容易的,只需要准备一个canvas,然后利用canvas的方法 drawImage就可以截取这个图片的某个部分了。


    示例如下:


    <!DOCTYPE html>
    <html>
    <head>
    <title>截取图片部分示例</title>
    </head>
    <body>
    <canvas id="myCanvas" width="400" height="400"></canvas>
    <br>
    <button onclick="cropImage()">截取图片部分</button>
    <br>
    <img id="croppedImage" alt="截取的图片部分">
    <br>

    <script>
    function cropImage() {
    var canvas = document.getElementById('myCanvas');
    var ctx = canvas.getContext('2d');
    var image = new Image();

    image.onload = function () {
    // 在canvas上绘制整张图片
    ctx.drawImage(image, 0, 0, canvas.width, canvas.height);

    // 截取图片的一部分,这里示例截取左上角的100x100像素区域
    var startX = 0;
    var startY = 0;
    var width = 100;
    var height = 100;
    var croppedData = ctx.getImageData(startX, startY, width, height);

    // 创建一个新的canvas用于显示截取的部分
    var croppedCanvas = document.createElement('canvas');
    croppedCanvas.width = width;
    croppedCanvas.height = height;
    var croppedCtx = croppedCanvas.getContext('2d');
    croppedCtx.putImageData(croppedData, 0, 0);

    // 将截取的部分显示在页面上
    var croppedImage = document.getElementById('croppedImage');
    croppedImage.src = croppedCanvas.toDataURL();
    };

    // 设置要加载的图片
    image.src = 'your_image.jpg'; // 替换成你要截取的图片的路径
    }
    </script>
    </body>
    </html>

    一、获取像素的思路


    但是目前的这个需求远不止这样简单,因为它的对象是整个document,需要在整个document上截取一部分,我思考了一下,其实假设如果浏览器为我们提供了一个api,能够获取到某个位置的像素信息就好了,这样我将选定的某个区域的每个像素信息获取到,然后在一个像素一个像素绘制到canvas上就好了。


    我本以为我发现了一个很好的方法,可遗憾的是经过调研浏览器并没有为我们提供类似获取某个位置像素信息的API。


    唯一为我们提供获取像素信息的是canvas的这个API。


    <!DOCTYPE html>
    <html>
    <head>
    <title>获取特定像素信息示例</title>
    </head>
    <body>
    <canvas id="myCanvas" width="400" height="400"></canvas>
    <br>
    <button onclick="getPixelInfo()">获取特定像素信息</button>
    <br>
    <div id="pixelInfo"></div>

    <script>
    function getPixelInfo() {
    var canvas = document.getElementById('myCanvas');
    var ctx = canvas.getContext('2d');

    // 绘制一些内容到canvas
    ctx.fillStyle = 'red';
    ctx.fillRect(50, 50, 100, 100);

    // 获取特定位置的像素信息
    var x = 75; // 替换为你想要获取的像素的x坐标
    var y = 75; // 替换为你想要获取的像素的y坐标
    var pixelData = ctx.getImageData(x, y, 1, 1).data;

    // 提取像素的颜色信息
    var red = pixelData[0];
    var green = pixelData[1];
    var blue = pixelData[2];
    var alpha = pixelData[3];

    // 将信息显示在页面上
    var pixelInfo = document.getElementById('pixelInfo');
    pixelInfo.innerHTML = '在位置 (' + x + ', ' + y + ') 的像素信息:<br>';
    pixelInfo.innerHTML += '红色 (R): ' + red + '<br>';
    pixelInfo.innerHTML += '绿色 (G): ' + green + '<br>';
    pixelInfo.innerHTML += '蓝色 (B): ' + blue + '<br>';
    pixelInfo.innerHTML += 'Alpha (透明度): ' + alpha + '<br>';
    }
    </script>
    </body>
    </html>


    浏览器之所以没有为我们提供相应的API获取像素信息,停下来想想也是有道理的,甚至是必要的,因为假设浏览器为我们提供了这个API,那么恶意程序就可以通过这个API,不断的获取你的浏览器页面像素信息,然后全部绘制出来。一旦你的浏览器运行这个段恶意程序,那么你在浏览器干的什么,它会一览无余,相当于在网络的世界里裸奔,毫无隐私可言。


    二、把DOM图片化


    既然不能走捷径直接拿取像素信息,那就得老老实实的把document转换为图片,然后调用canvas的drawImage这个方法来截取图片了。


    在前端领域其实99%的业务场景早已被之前的大佬们都实现过了,相应的轮子也很多。我问了一下chatGPT,它立马给我推荐了大名鼎鼎的html2canvas,这个库能够很好的将任意的dom转化为canvas。这个是它的官网。


    我会心一笑,因为这不就直接能够实现需求了,很容易就可以写出下面的代码了:


    html2canvas(document.body).then(function(canvas) {
    // 将 Canvas 转换为图片数据URL
    var src = canvas.toDataURL("image/png");
    var image = new Image();
    image.src = src;
    image.onload = ()=>{
    const canvas = document.createElement("canvas")
    const ctx = canvas.getContext("2d");
    const width = 100;
    const height = 100;
    canvas.width = width;
    canvas.height = height;
    // 截取以(10,10)为顶点,长为100,宽为100的区域
    ctx.drawImage(image, 10, 10, width, height , 0 , 0 ,width , height);
    }
    });


    上面这段代码就可以实现截取document的特定的某个区域,需求已经实现了,但是我看了一下这个html2canvas库的资源发现并没有那么简单,有两个点并不满足我希望实现的点:


    1.大小


    当我们将html2canvas引入我们的项目的时候,即便压缩过后,它的资源也有近200kb:


    Screen Shot 2023-09-09 at 3.15.10 PM.png


    要知道整个react和react-dom的包压缩过后也才不到150kb,因此在项目只为了一个单一的功能引入一个复杂的资源可能并不划算,引入一个复杂度高的包一个是它会增加构建的时间,另一方面也会增加打包之后的体积。


    如果是普通的web工程可能情有可原,但是因为我会将这需求做到插件当中,插件和普通的web不一样的一点,就是web工程如果更新之后,客户端是自动更新的。但是插件如果更新了,需要客户端手动的下载插件包,然后再在浏览器安装,因此包的大小尽可能小才好,如果一个插件好几十MB的话,那客户端肯定烦死了。


    2.性能


    作为业内知名的html2canvas库,性能方面表现如何呢?


    我们可以看看它的原理,一个dom结构是如何变成一个canvas的呢!


    它的源码在这里:核心的实现是canvas-renderer.ts这个文件。


    当html2canvas拿到dom结构之后,首先为了避免副作用给原dom造成了影响,它会克隆一份全新的dom,然后遍历DOM的每一个节点,将其扁平化,这个过程中会收集每个节点的样式信息,尤其是在界面上的布局的几何信息,存入一个栈中。


    然后再遍历栈中的每一个节点进行绘制,根据之前收集的样式信息进行绘制,就这样一点点的绘制到提前准备的和传入dom同样大小的canvas当中,由于针对很多特殊的元素,都需要处理它的绘制逻辑,比如iframe、input、img、svg等等。所以整个代码就比较多,自然大小就比较大了。


    整个过程其实需要至少3次对整个dom树的遍历才可以绘制出来一个canvas的实例。


    这个就是这个绘制类的主要实现方法:


    Screen Shot 2023-09-09 at 4.08.30 PM.png


    可以看到,它需要考虑的因素确实特别多,类似写这个浏览器的绘制引擎一样,特别复杂。


    要想解决以上的大小的瓶颈。


    第一个方案就是可以将这个资源动态加载,但是一旦动态加载就不能够在离线的环境下使用,在产品层面是不能接受的,因为大家可以想一想如果微信截图的功能在没有网络的时候就使用不了,这个肯定不正常,一般具备工具属性的功能应该尽可能可以做到离线使用,这样才好。


    因此相关的代码资源不能够动态加载。


    二、dom-to-image


    正当我不知道如何解决的时候,我发现另外了一个库dom-to-image,我发现它打包后的大小只有10kb左右,这其实已经一个很可以接受的体积了。这个是它的github主页。好奇的我想知道它是怎么做到只有这么小的体积就能够实现和html2canvas几乎同样的功能的呢?于是我就研究了一下它的实现。


    dom-to-image的实现利用了一个非常灵活的特性--image可以渲染svg


    我们可以复习一下img标签的src可以接受什么样的类型:这里是mdn的说明文档


    可以接受的格式要求是:



    如果我们使用svg格式来渲染图片就可以是这样的方式:


    <!DOCTYPE html>
    <html>
    <head>
    <title>渲染SVG</title>
    </head>
    <body>
    <h1>SVG示例</h1>
    <img src="example.svg" alt="SVG示例">
    </body>
    </html>


    但是也可以是这样的方式:


    <!DOCTYPE html>
    <html>
    <head>
    <title>渲染SVG字符串</title>
    </head>
    <body>
    <div id="svg-container">
    <!-- 这里是将SVG内容渲染到<img>标签中 -->
    <img id="svg-image" src="data:image/svg+xml, <svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><circle cx='50' cy='50' r='40' stroke='black' stroke-width='2' fill='red' /></svg>" alt="SVG图像">
    </div>
    </body>
    </html>


    把svg的标签序列化之后直接放在src属性上,image也是可以成功解析的,只不过我们需要添加一个头部:data:image/svg+xml,


    令人兴奋的是,svg并不是只支持svg语法,也支持将其他的xml类型的语法比如html嵌入在其中。antv的x6组件中有非常多这样的应用例子,我给大家截图看一下:


    Screen Shot 2023-09-09 at 4.49.40 PM.png


    在svg中可以通过foreignObject这个标签来嵌套一些其他的xml语法,比如html等,有了这一特性,我们就可以把上面的例子改造一下:


    <!DOCTYPE html>
    <html>
    <head>
    <title>渲染SVG字符串</title>
    </head>
    <body>
    <div id="svg-container">
    <!-- 这里是将SVG内容渲染到<img>标签中 -->
    <img
    id="svg-image"
    src="data:image/svg+xml, <svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><circle cx='50' cy='50' r='40' stroke='black' stroke-width='2' fill='red' /><foreignObject>{ 中间可以放 dom序列化后的结果呀 }</foreignObject></svg>"
    alt="SVG图像"
    >

    </div>
    </body>
    </html>


    所以我们可以将dom序列化后的结构插到svg中,这不就天然的形成了一种dom->image的效果么?下面是演示的效果:


    <!DOCTYPE html>
    <html>
    <head>
    <title>渲染SVG字符串</title>
    </head>
    <body>
    <div id="render" style="width: 100px; height: 100px; background: red"></div>
    <br />
    <div id="svg-container">
    <!-- 这里是将SVG内容渲染到<img>标签中 -->
    <img id="svg-image" alt="SVG图像" />
    </div>

    <script>
    const perfix =
    "data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><foreignObject x='0' y='0' width='100%' height='100%'>";
    const surfix = "</foreignObject></svg>";

    const render = document.getElementById("render");

    render.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");

    const string = new XMLSerializer()
    .serializeToString(render)
    .replace(/#/g, "%23")
    .replace(/\n/g, "%0A");

    const image = document.getElementById("svg-image");

    const src = perfix + string + surfix;

    console.log(src);

    image.src = src;
    </script>
    </body>
    </html>


    Screen Shot 2023-09-09 at 5.18.12 PM.png


    如果你将这个字符串直接通过浏览器打开,也是可以的,说明浏览器可以直接识别这种形式的媒体资源正确解析对应的资源:


    data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><foreignObject x='0' y='0' width='100%' height='100%'><div id="render" style="width: 100px; height: 100px; background: red" xmlns="http://www.w3.org/1999/xhtml"></div></foreignObject></svg>

    实不相瞒这个就是dom-to-image的核心原理,性能肯定是不错的,因为它是调用浏览器底层的渲染器。


    通过这个dom-to-image我们可以很好的解决资源大小性能这两个瓶颈的点。


    三、优化


    这个库打包后的产物是umd规范的,并且是统一暴露出来的全局变量,因此不支持treeshaking。


    Screen Shot 2023-09-09 at 9.13.08 PM.png


    但是很多方法比如toJpeg、toBlob、等方法我们其实都用不到,所以打包了很多我们不需要的产物,于是其实我们可以把核心的实现自己写一遍,使用1-2kb的空间就可以做到这一点。


    经过以上的思考我们就可以基本上确定方案了:


    基于dom-to-image的原理,实现一个简易的my-dom-to-image,大约只需要100行代码左右就可以做到。


    然后将document.body转化为image,再从这个image中截取特定的部分。


    Screen Shot 2023-09-09 at 9.23.06 PM.png


    好了,以上就是我关于这个需求的一些思考,如果掘友也有一些其他非常有意思的需求,欢迎评论区讨论我们一起头脑风暴啊!!!


    四、最后的话


    以下是我的其他文章,欢迎掘友们阅读哦!


    保姆级讲解JS精度丢失问题(图文结合)


    shell、bash、zsh、powershell、gitbash、cmd这些到底都是啥?


    从0到1开发一个浏览器插件(通俗易懂)


    用零碎时间个人建站(200+赞)


    另外我有一个自己的网站,欢迎来看看 new-story.cn


    创作不易,如果您觉得文章有任何帮助到您的地方,或者触碰到了自己的知识盲区,请帮我点赞收藏一下,或者关注我,我会产出更多高质量文章,最后感谢您的阅读,祝愿大家越来越好。


    作者:Story
    来源:juejin.cn/post/7276694924137463842
    收起阅读 »

    谈谈干前端四年的几点感受

    19年毕业的我,最开始怀揣着无限憧憬进入这个行业 不知不觉,已经工作4年了,如果算上大四实习的时光,也接近5年了。 4年间换了两家公司。 对于工作,我也有过很多的困扰和迷茫,现在依旧在走一步看一步的状态。 或许一觉起来工作没了,都是概率事件。 为什么会有这篇文...
    继续阅读 »

    19年毕业的我,最开始怀揣着无限憧憬进入这个行业


    不知不觉,已经工作4年了,如果算上大四实习的时光,也接近5年了。


    4年间换了两家公司。


    对于工作,我也有过很多的困扰和迷茫,现在依旧在走一步看一步的状态。


    或许一觉起来工作没了,都是概率事件。


    为什么会有这篇文章?


    一是与行业内大佬山月交流了一次,解惑答疑,有所感悟,想记录下心中所想;


    二是呼应一年前的文章《谈谈干前端三年的几点感受》,对比看看自己想法变化。




    前端在一个公司定位


    一年前我说,前端在公司的定位是必要不重要,现在的想法依旧不变,只是对象变了。


    整个技术开发人员,在一个公司的定位都是处于必要但不重要的角色,可替代性非常高。


    可替代的属性越高,价值属性便越低。


    也许局部或者短期看,技术开发的薪资是高的,但这对于公司来说,是成本。


    如果公司要降本增效,最先压榨的也是这部分人员。


    有这样一种说法,“技术傍身,编程改变世界”等等,其实是有些误导人的。


    重要的从来都是想法,是渠道,不是技术。


    只要能想到,大概率都能实现,实现不了就加班想办法实现。


    能够提出想法的人,才处于一个公司重要的地位。


    就正如我看到的,一个公司,核心业务人员离职,公司上下极力挽留,一个开发离职,领导回复祝好。




    我们要学些什么


    学有价值的东西。


    何谓价值,价值就是经过时间考验,依旧不变的东西。


    我个人极其反对花太多精力深入研究各个技术的源码。


    技术说到底是工具,工具最重要的使用,不是本身,而且只要是工具,便都有替代品,


    有新的技术,又会有新的源码,学是学不完的。


    而计算机行业,不变的什么,有价值的是什么?


    是计算机网络,是计算机组成原理,是数据结构与网络,是操作系统,是信息安全,是项目管理,是软件测试。


    以上都是大学中计算机类的专业课程,这些年没有变过。


    具体一点,与前端不变的什么,有价值的是什么?


    是网络请求,是nginx,是性能优化,是前端工程化,是脚手架,是对UI的基本审美。


    当然了,如果做可视化,音视频,跨端方向等等也有属于自己的专业壁垒。


    以上我提到的都属于目前自己看到,前端通用知识。




    如何评价自己的薪资和技术水平


    其实,我们学到的99%的知识是无用的,或者学完不用就忘记了。


    我学习的目的很单纯,就是为了跳槽涨薪。


    让自己的实力能够匹配和市场对我的要求和我自己期望的薪资。


    那么,如何得知自己的薪资水平,是否符合年限,技术水平,是否符合市场要求?


    需要比较。


    我们常说,人要和自己比,不要和别人比,人别人,气死人。但其实这是句自我安慰的鸡汤,不是生活的真相。


    人得自知,不比较,如何自知呢?


    当然,这种事情很难和同事交流,也不建议问同行。


    问同事,同事水平参差不齐,给不了你准确的答案。


    问同行,同行也许自己也发展不顺,多半是同病相怜,或是能给你方向,但给不了方案。


    我的建议是做付费咨询,向行业内大佬求助。


    同行业过来人的经验,更靠谱一些,做不到感同身受,但能明白心中所想。


    我想看到这篇文章的各位,都关注过几位技术大佬,那就去主动搭讪,说明来意,付费咨询。


    “我的薪资目前和我的工作年限匹配吗?我的技术还应该补充哪部分?我应该如何学习某些知识,有没有推荐的学习路线和文章,等等之类的问题”


    而付费是获取能够心安理得的咨询,不要计较那一顿火锅的钱。


    但其实只要搭讪成功,大佬一般都不会收费。




    2023年了,还要不要往大厂努力?


    当然要啊,这个想法和一年前比,没有动摇。


    但是大厂今年都在降本增效,门槛更高了,面试更难了,工作更卷了。


    但这并不能成为放弃的借口。无论结果与否,人总得有个工作上的目标啊。


    正如我的前同事今年初送给我的一句话,


    “备考公务员或者向大厂努力,总得找个目标,找件事情去做吧。如果觉得大厂太卷,那就干一年就走,但这个经历会成为你永久的财富。”




    前端已死?


    今年上半年受chatGPT冲击,这个言论甚嚣尘上。


    我这里不讨论死不死的事情,只觉得这个问题很荒谬,多思考思考就会明白这句话,在创造概念,制造焦虑。


    仔细想想,这波言论最大的受益方还是 做职业教育的那帮人。




    拿多少钱干多少活还是干多少活拿多少钱?


    第一家公司一切都很好。


    我还是义无反顾的离开了,离开后公司发展得更好了。


    离职的直接原因就是,当时要前端使用uni-app做跨端应用,去替换客户端的工作。


    这项工作的直接影响就是,整个公司只前端部门加班,我疲惫不堪的同时,uni-app踩不完的坑,也身心俱疲。


    每当加班到很晚时,委屈总是涌上心头。


    受不了之时,就只剩一个走字。


    这时候,小兵心态就出现了,拿多少钱多少活,我就拿这点工资,整这么多活,我无法承担,只能摆烂了。


    当然,也有领导心态,你得先努力干,干出成果,我拿着成果才去争取涨薪。


    这中间就有一个认知偏差,双方因为角度不同,无法理解对方的心态和想法。


    领导觉得小兵不懂他的良苦用心,小兵觉得领导天天画饼。


    哪种做法是对的呢?


    得就事论事。


    如果这件事情,对你有成长,有帮助,比如做一些工程化,脚手架,性能优化的工作,肯定得先干出成果。


    如果这件事情,对自己是一种消耗,那还是持小兵心态吧。


    如何区分这件事情是对你的帮助还是对自己的消耗呢?


    其实自己最清楚。


    如果干这项工作时,总是充满期待,充满激情,加班也无怨无悔,那就是帮助。


    如果干这项工作时,总是身心俱疲,牢骚满腹,加班会委屈抱怨,那就是消耗。




    人都是不愿意被管理的


    这句话出自山月,我听后豁然开朗。


    今年听闻行业内很多公司严抓考勤,多了很多制度和会议,吐槽随处可见。


    新的领导,势必会带来新的管理制度,新的实施方案。


    人都是不愿意被管理的,所以会引起各种不适应,但是一般一个月后都会销声匿迹,因为已经适应了。


    无法评价这些变化的好与坏。


    身处其中的我们只有慢慢适应,打工到哪里都一样,只要被管理着,都需要面临不同的问题。




    最后


    其实,回顾毕业这些年,19年谣传资本寒冬,然后是防疫三年,到后来前端已死,到现在无法言状的行业颓势。


    正应了那句话,“今年是过去十年最差的一年,却可能是未来十年最好的一年。”


    然后呢,这句话想表达什么?仅仅是传播了一个情绪,放在近些年都受用。


    我想说,


    “大环境的整体劣势,不影响个人的局部优势,我们要想办法建立这种个人优势;”


    “种一棵树最好的时间是十年前,其次是现在。”


    作者:虎妞先生
    来源:juejin.cn/post/7258509816691834917
    收起阅读 »

    突然发现,前端好像没几个做到 CTO 的……

    web
    大家好,我 ssh,这几天,在推上看到了一个节奏,swyx 小哥发了一篇关于前端天花板的讨论,吸引了 150w 左右的阅读。主要是在讲前端天花板,前端人员被集中捆绑在低级别工程师这个行列中,通往 VP(技术副总裁) 或者 CTO 的大门是朝他们关上的。 而他...
    继续阅读 »

    大家好,我 ssh,这几天,在推上看到了一个节奏,swyx 小哥发了一篇关于前端天花板的讨论,吸引了 150w 左右的阅读。主要是在讲前端天花板,前端人员被集中捆绑在低级别工程师这个行列中,通往 VP(技术副总裁) 或者 CTO 的大门是朝他们关上的。


    微信图片_20230726161930.jpg


    而他发出这篇推文的起因,正是 swyx 正文里配的这篇文章截图:



    经过我查询,这是 honeycomb.io 的一篇博客 成为工程 VP 里的一段话。他们没有刻意贬低前端工程师,只是客观的描述了统计情况而已,这反而是更加令人悲观的。


    其实这个问题我也不止一次想过,尤其是有一些校招的同学特别喜欢思考这个问题,之前一次校招的宣讲会后答疑环节,也有不止一个同学过来问我这个问题。


    确实,仔细想想,国内的前端界比较出名的前端出身做到很高职位的,玉伯算是一个代表,后期他基本上已经成为一个产品设计方面的负责人了,脱离了单纯前端的范畴。主导设计了云凤蝶、语雀这些非常 nb 的产品。


    image.png


    image.png


    但是除了玉伯之外,让我们仔细想想,是不是大概率情况下,前端升到更高级别负责人的概率比后端要低很多呢?第一印象是如此,而且我以前在阿里没有隐藏职位的时候,在钉钉上直接搜索 title 来确认过这个问题。


    在阿里,资深前端专家则对应前端的 p9,资深技术专家对应后端的 p9,这两个职位的人数在我印象里是相差很悬殊的,很多倍的关系…… 而且我记得 p9 的前端非常稀少。这其实也侧面反应出大家的主观感受是确有其事的。


    写到这里,我深感焦虑,赶紧去问问万能的 AI:


    ai.sb


    卧槽,被辱骂了一通。拿出我大哥 Dan 也没用!


    回到正题,swyx 又提到,有人说只要成为全栈就好了。



    直接看看这张图:



    全栈并不是口头说说那么简单,有一个小型公司的 CTO 也现身说出了自己的看法:


    image.png


    后端普遍认为前端简单,在国外也一样



    前端成为产品总负责人,比成为技术 vp 的路径要概率更大一些,这也符合玉伯的发展路径:


    image.png


    关于这件事儿,Hacker News 也有一些讨论,不过质量比较差,走偏了:


    开始讨论后端的烂代码了


    讨论男女平等


    我的看法


    看完了几乎全部的讨论以后,我感觉国外的开发者对于前端天花板的看法和国内差不多,确实是认为有后端工程背景的人升为 VP/CTO 级别的概率比较高,而前端更倾向于在框架中日复一日的迷失。


    以我自己的职业经历来说,假设我在使用 React 技术栈,今天在用 redux,明天出了一个 redux-toolkit 来解决 redux 太烂的问题,你迁移过去了,学到了很多范式很充实。再过几个月,又来了个 recoil,又来个 jotai。好像在很忙碌的学习,但其实都没有脱离状态管理的范畴,就像是被困在小学里反复的读五年级,而后端的人可能去研究更广阔的东西了。比如:




    1. 稳定性:各种灾备方案,限流等操作。




    2. 高并发:延迟,tps。




    3. 一致性:数据正确性。




    而前端比较好的处境,就是在一家前端主导产品的公司(比如最近比较火的 AffiNE)参与核心功能的研发,那么可以接触到前端比较深入的一些技术,而且有一帮大牛同事可以陪你玩最新的技术栈。又或者是参与到大型公司的基础架构建设,我了解到的比如性能监控、低代码搭建、serveless 建设、自研 JS 引擎、自研 Rust 编译库,也可以获得比较深入的技术提升。


    不过,大部分人的整个职业生涯可能都在做一些 Vue 或者 React 的应用开发,后台管理系统、活动页等等。。。是不是就完了?人生没希望了?


    再问问 AI:



    我丢,这 AI 吃枪药了吧。


    不过他骂的也不无道理,安心做个平庸的前端又怎么样呢?比起很多职业来说,坐在电脑前敲敲你喜欢的代码,当个快乐的小前端,拿个 10-20k 的薪资,不够过日子的嘛?想想土木老哥在烈日下的样子?



    我对于平庸人生的看法,把注意力转移到自己的生活中,有一个可以坚持热爱的爱好(比如我自己就喜欢踢足球和健身)。做一个自信阳光的小骚年,不是也很不错吗?


    不要高杠杆买房,不要负债太多,保持一定的积蓄习惯,注意资产的合理配置。你肯定能比一般职业的人过得更好,欲望才是万恶之源。


    当然,这只是比较悲观的想法,如果你有一颗上进的心,拼到个资深工程师,有点管理能力的话,再争取个前端小 leader 当当,过上小资点的生活也没问题。


    我的意思是,人生短短几十年,职业不是生活的全部。假设你全心全意拼在工作上,到了 40 岁挣了一堆钱,落了一身的病。你觉得你真的快乐吗?如果钱是快乐的全部的话,李玟也不会得抑郁症,张朝阳也不会因为抑郁放弃公司管理跑去修行了。


    总结


    前端确实天花板比较低,不过那又咋样呢?最终能成为 VP 的人也没几个,如果你从小就就是天之骄子,目标是星辰大海,那你考上 985 的计算机系应该没什么问题,在校招的时候就果断选后端吧,确实有几率爬的更高点,但是付出相应的代价也是必要的(后端头发平均值明显低于前端)。


    屏幕截图 2023-07-29 052610.jpg


    否则,你就做个快乐的小前端,也比其他大多数职业过得舒服。


    作者:ssh_晨曦时梦见兮
    来源:juejin.cn/post/7261807670746513463
    收起阅读 »

    第一个可以在条件语句中使用的原生hook诞生了

    大家好,我卡颂。 在10月13日的first-class-support-for-promises RFC中,介绍了一种新的hook —— use。 use什么?就是use,这个hook就叫use。这也是第一个:可以在条件语句中书写的hook可以在其他hook...
    继续阅读 »

    大家好,我卡颂。


    在10月13日的first-class-support-for-promises RFC中,介绍了一种新的hook —— use


    use什么?就是use,这个hook就叫use。这也是第一个:

    • 可以在条件语句中书写的hook

    • 可以在其他hook回调中书写的hook


    本文来聊聊这个特殊的hook


    欢迎加入人类高质量前端框架研究群,带飞


    use是什么


    我们知道,async函数会配合await关键词使用,比如:

    async function load() {
    const {name} = await fetchName();
    return name;
    }

    类似的,在React组件中,可以配合use起到类似的效果,比如:

    function Cpn() {
    const {name} = use(fetchName());
    return <p>{name}</p>;
    }

    可以认为,use的作用类似于:

    • async await中的await

    • generator中的yield


    use作为读取异步数据的原语,可以配合Suspense实现数据请求、加载、返回的逻辑。


    举个例子,下述例子中,当fetchNote执行异步请求时,会由包裹NoteSuspense组件渲染加载中状态


    当请求成功时,会重新渲染,此时note数据会正常返回。


    当请求失败时,会由包裹NoteErrorBoundary组件处理失败逻辑。

    function Note({id}) {
    const note = use(fetchNote(id));
    return (
    <div>
    <h1>{note.title}</h1>
    <section>{note.body}</section>
    </div>
    );
    }

    其背后的实现原理并不复杂:

    1. Note组件首次renderfetchNote发起请求,会throw promise,打断render流程

    2. Suspense fallback作为渲染结果

    3. promise状态变化后重新触发渲染

    4. 根据note的返回值渲染


    实际上这套基于promise的打断、重新渲染流程当前已经存在了。use的存在就是为了替换上述流程。


    与当前React中已经存在的上述promise流程不同,use仅仅是个原语primitives),并不是完整的处理流程。


    比如,use并没有缓存promise的能力。


    举个例子,在下面代码中fetchTodo执行后会返回一个promiseuse会消费这个promise

    async function fetchTodo(id) {
    const data = await fetchDataFromCache(`/api/todos/${id}`);
    return {contents: data.contents};
    }

    function Todo({id, isSelected}) {
    const todo = use(fetchTodo(id));
    return (
    <div className={isSelected ? 'selected-todo' : 'normal-todo'}>
    {todo.contents}
    </div>
    );
    }

    Todo组件的id prop变化后,触发fetchTodo重新请求是符合逻辑的。


    但是当isSelected prop变化后,Todo组件也会重新renderfetchTodo执行后会返回一个新的promise


    返回新的promise不一定产生新的请求(取决于fetchTodo的实现),但一定会影响React接下来的运行流程(比如不能命中性能优化)。


    这时候,需要配合React提供的cache API(同样处于RFC)。


    下述代码中,如果id prop不变,fetchTodo始终返回同一个promise

    const fetchTodo = cache(async (id) => {
    const data = await fetchDataFromCache(`/api/todos/${id}`);
    return {contents: data.contents};
    });

    use的潜在作用


    当前,use的应用场景局限在包裹promise


    但是未来,use会作为客户端中处理异步数据的主要手段,比如:


    • 处理context

    use(Context)能达到与useContext(Context)一样的效果,区别在于前者可以在条件语句,以及其他hook回调内执行。


    • 处理state

    可以利用use实现新的原生状态管理方案:

    const currentState = use(store);
    const latestValue = use(observable);

    为什么不使用async await


    本文开篇提到,use原语类似async await中的await,那为什么不直接使用async await呢?类似下面这样:

    // Note 是 React 组件
    async function Note({id, isEditing}) {
    const note = await db.posts.get(id);
    return (
    <div>
    <h1>{note.title}</h1>
    <section>{note.body}</section>
    {isEditing ? <NoteEditor note={note} /> : null}
    </div>
    );
    }

    有两方面原因。


    一方面,async await的工作方式与React客户端处理异步时的逻辑不太一样。


    await的请求resolve后,调用栈是从await语句继续执行的(generatoryield也是这样)。


    而在React中,更新流程是从根组件开始的,所以当数据返回后,更新流程是从根组件从头开始的。


    改用async await的方式势必对当前React底层架构带来挑战。最起码,会对性能优化产生不小的影响。


    另一方面,async await这种方式接下来会在Server Component中实现,也就是异步的服务端组件。


    服务端组件与客户端组件都是React组件,但前者在服务端渲染(SSR),后者在客户端渲染(CSR),如果都用async await,不太容易从代码层面区分两者。


    总结


    use是一个读取异步数据的原语,他的出现是为了规范React在客户端处理异步数据的方式。


    既然是原语,那么他的功能就很底层,比如不包括请求的缓存功能(由cache处理)。


    之所以这么设计,是因为React团队并不希望开发者直接使用他们。这些原语的受众是React生态中的其他库。


    比如,类似SWRReact-Query这样的请求库,就可以结合use,再结合自己实现的请求缓存策略(而不是使用React提供的cache方法)


    各种状态管理库,也可以将use作为其底层状态单元的容器。


    值得吐槽的是,Hooks文档中hook的限制那一节恐怕得重写了。


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

    从尤雨溪这两天微博募捐,思考开源如何赚大钱

    这两天,尤大在他的微博表示,他打算开启国内开源捐赠计划,截止本文发帖为止,已经有 6k / 月的固定充电了。 这个数额目前还是比较小的,企业级别的 sponsor 应该还没有出现,光靠个人捐赠的话这点钱真的完全不够团队开销的。 正巧我看到了 Ink 作者的...
    继续阅读 »

    这两天,尤大在他的微博表示,他打算开启国内开源捐赠计划,截止本文发帖为止,已经有 6k / 月的固定充电了。


    这个数额目前还是比较小的,企业级别的 sponsor 应该还没有出现,光靠个人捐赠的话这点钱真的完全不够团队开销的。






    正巧我看到了 Ink 作者的一篇文章,讲述他在开源软件如何稳定搞钱这方面的思考,觉得他的很多观点非常犀利,值得各位前端开发者同学一起学习,毕竟大家未来可能有搞开源的一天。
    接下来是他的这篇 Generating income from open source 的内容:


    最近,Ink 的知名度越来越高,并且已经被一些知名公司使用了一段时间。然而,与大多数其他开源项目一样,Ink没有任何收入。


    我开始研究各种选项,以改变这种情况,并以某种方式开始收费,这样它就可以支持我以及 Ink 和相关项目(如 Ink UIPastel) 的进一步开发。


    本文是我在这个主题上所学到的内容的简要版本。


    不起作用的方法


    以下是我认为维护者无法从他们的项目中获得收入的原因。


    依靠个人捐赠


    能够有人愿意支持你是很好的,但是每月 5 美元的捐赠无法维持生活。这是社区对你工作的感激的一种方式,但不应被视为稳定的收入来源。


    除非你是社区中极少数非常受欢迎的开发者之一,否则接受事实,不会有足够多的人订阅每月捐赠。


    尽管如此,我认为个人捐赠并不是答案。


    期望公司捐赠


    你构建了很火的项目,并在生产环境中稳定运行,他们从中获益良多。当然,他们肯定知道要回馈一下,毕竟他们赚了那么多钱,是这样的吗?


    我们需要最终明白一些简单的道理,改变我们的预期。


    经营业务意味着最大化收入和最小化支出。企业不会为了只是为了对你好点,而增加一个长期开支。(万恶的资本家)


    企业习惯于以金钱交换价值。开源维护者需要考虑到这一点。你提供价值,他们从中受益并为此付费。


    确实有一些拥有强大开源文化的公司可以持续给他们依赖的项目提供重大的每月捐赠,但不幸的是,他们是个例。


    完全依赖捐赠或赞助


    下面这句话,是不是很耳熟?



    请赞助我吧,这样我就可以继续开发我的开源项目。



    我们整了一个漂亮的 GitHub 赞助页面,然后坐在那里等待有人注册。你能想象一个企业采用类似的策略吗?它会在一个月内破产倒闭。


    我们需要理解我们的项目对公司所提供的价值,并开始收费,就像我们经营一家企业,销售一种有用的产品。


    认为没有人愿意付费或者定价不够高


    在几家中小型初创公司工作过后,我现在明白几年前自己有多么愚蠢,以为每月 200 块的订阅费是天价,或者公司不愿意为工具付费。纯属扯犊子。


    公司为员工解决日常问题和开发产品支付数百万的钞票。如果你的项目解决了他们的问题,使他们的团队不必自己解决,他们会支付比你认为的价值高 10 倍、100 倍甚至 1000 倍的费用。而且,他们会很满意。


    公司已经为各种工具和费用支付数万元每月。无论你要求什么,实际上对他们来说都是九牛一毛。把你的产品价格翻倍吧,没毛病。


    害怕或者羞于索要信用卡信息


    我们不需要为我们的工作收费找理由。没有什么可羞耻的。


    你为解决一个问题而付出你的努力。有人为了这个问题请你付费解决,别多虑了。


    有效方法


    我们喜欢抱怨没人支付维护者的费用,但实际上有很多建立在开源基础上的成功企业。以下是它们持续收入的秘诀:


    商业许可证


    Dave DeSandro 的Metafizzy提供各种 JavaScript 库,其中包括 Isotope - 用于创建灵活网格布局的库。Isotope 是开源的,但根据你的使用方式有不同的许可证



    1. 开源许可证。


    这个许可证允许在个人或开源项目中免费使用 Isotope。



    1. 商业许可证。


    这个许可证允许你在几乎任何商业应用中使用 Isotope。实际上,任何希望使用它的公司很可能需要购买商业许可证。


    商业许可证的定价根据使用人数而不同:

    • 单个开发者的费用为 25 美元。
    • 8 名开发者团队的费用为 110 美元。
    • 无限数量的开发者的费用为 320 美元。

    请注意,这些不是订阅,而是一次性付款。


    商业许可证本身是一份 PDF 文件,支付后通过 Gumroad 发送给你。



    1. 商业 OEM 许可证。


    该许可证适用于先前的商业许可证未涵盖的其他用途,特别是 UI 构建器、SDK 或工具包。对于商业 OEM 许可证没有公开的定价,这意味着它比前几个等级要贵得多。这些用例可能意味着 Isotope 作为用户界面或产品提供中的关键组成部分,因此公司愿意支付高额费用。


    我喜欢这种方法的原因


    这看起来是对开源进行收费最简单的方式,因为 Metafizzy 为同一份代码提供了不同的许可证,许可证本身是一个 PDF 文件。没有专业版,没有许可证密钥,也没有其他需要维护的东西。个人开发者可以免费使用同样的工具,而公司则支付合理的价格。


    为更多功能收费


    Mike Perham 的Sidekiq是一个在 Ruby 应用程序中基于 Redis 的后台作业的著名的库。Sidekiq 提供了 3 种不同的计划:



    1. 开源版。


    Sidekiq 免费提供一个有限的开源版本。尽管它被称为“开源”,但 LGPL 许可证似乎允许你在商业应用中使用免费版本。


    开源计划不提供任何客户支持,有问题就去提 GitHub Issue 吧。



    1. 专业版。


    专业版每月收费 99 美元(或 995 美元/年),提供更多的功能。例如,批处理后台作业、通过更高级的 Redis API 提供的增强可靠性。专业版还包括通过电子邮件提供的客户支持。



    1. 企业版。


    企业版根据你运行的 Sidekiq 实例数量,以 229 美元/月或更高的价格提供全部功能。


    Sidekiq 的表现非常出色,根据 Mike 在 Hacker News 的最新评论,它现在每年创造 1000 万美元的收入。


    有趣的是,他还提到,你可以通过其他开源 Ruby gem 组装 Sidekiq 的大多数付费功能,但是设置和维护起来需要很多时间。最终,你可能会得到一个比经过多次测试的 Sidekiq 还要糟糕的系统,所以购买功能齐全的 Sidekiq 似乎是明智之举。



    Sidekiq 的大多数商业功能都可作为开源软件包获得,但是当你将 3-6 个这些功能集成在一起时,复杂性会悄然而至。自己构建往往会导致一个比我精心策划的成熟、经过良好调试的系统还要差的系统。



    一旦你注册了 Sidekiq,你将获得访问私有 Ruby gem 服务器的权限,可以从中下载并更新应用程序中的sidekiq gem。他自己构建了这个系统,并表示不用花太多时间维护它。


    我喜欢这种方法的原因


    Sidekiq 首先是一个很棒的开源项目。在 Ruby 社区中,当你需要后台队列时,它成为了一个明显的选择。这是 Sidekiq 唯一的营销渠道。


    然后,开发人员向他们的朋友和公司的管理人员推荐 Sidekiq。随着他们的应用程序扩大,客户有明显的动机支付 Sidekiq 以解锁更多功能。


    托管版本


    最近,越来越多的企业将其整个产品开源,并提供托管版本以获取收费。

    • Plausible Analytics - 一个注重隐私的 Google Analytics 替代方案。托管版本每月起价 9 美元。
    • PostHog - 产品分析、功能标志、A/B 测试等多个数据工具的组合。托管版本采用按用量计费,前 100 万个事件免费,之后每个事件收费 0.0003068 美元。
    • Metabase - 数据库仪表板。托管版本每月起价 85 美元。

    这些只是我能想到的例子,还有许多类似的例子。


    我喜欢这种方法的原因


    你可以构建一次应用程序,并将相同版本作为开源和托管付费产品提供。你可能会想:“为什么有人愿意为可免费获得的东西付费”。然而,Plausible Analytics 每年收入 100 万美元,所以肯定有很多人愿意支付小额的月费来享受他们的产品,而不用自己搞乱七八糟的服务器啥的。


    收费维护和高级材料


    Moritz Klack、Christopher Möller、John Robb 和 Hayleigh Thompson 的React Flow是一个用于交互式流程图的 React 库。这是一个可持续的开源项目,与我以前见过的任何项目都不同。React Flow 为公司提供了一个专业版订阅,其中提供以下功能:

    • 访问专业版高级用例示例。
    • 优先解决 GitHub 上的问题。
    • 每月最多 1 小时的电子邮件支持。
    • 最有趣的是,我引用一下,“保持库的运行和维护,采用 MIT 许可证”。

    在整个定价页面上,大部分文案都集中在最后一点上。React Flow 不是一个容易用其他东西替代的库,所以公司很可能有兴趣确保它得到良好的维护,并继续使用 MIT 许可。


    John 在他们的博客上写了一篇优秀的文章,名为“Dear Open Source: let’s do a better job of asking for money”,我建议你阅读一下。我对此非常着迷,所以给 John 发了一封邮件,提出了一些后续问题,他非常友善地回答了我关于这个话题的许多宝贵的知识。


    以下是我从我们的邮件往来中总结出的要点:

    • 包装很重要。公司内部持有信用卡的人希望看到他们一直在看到的“定价”页面。GitHub 赞助页面行不通。React Flow 最初有一个这样的页面,但几乎没有获得任何收入。当他们推出一个类似 SaaS 的产品网站,并提供几个定价层次时,情况改善了。
    • 让大家发现专业版计划。React Flow 组件显示一个指向他们网站的链接,并要求开发人员在订阅专业版计划后将其删除。即使在不这样做的情况下删除它仍然完全合法和可以接受,但它作为一个不会强迫的好方法,可以促使人们查看专业版计划。
    • 公司在有支持的情况下更有安全感。React Flow 每月提供最多 1 小时的电子邮件支持,所以我自然而然地问如果客户花费的时间超过 1 小时会发生什么。John 表示,即使如此,他们还是会继续通过电子邮件提供支持,最后一切都会平衡,因为有很多客户根本不联系他们。他还认为,电子邮件支持会给人一种保险的感觉,因此公司知道如果有需要,他们可以找到他们,即使他们从未这样做过。
    • 为人们提供可以立即购买和访问的东西。我想知道那些对专业版客户可用的高级示例有多重要,因为与其他好处相比,它们似乎只是一种美好的附加功能。令人惊讶的是,John 有不同的看法。他坚信,购买后立即提供一些有价值的东西可以将他们的专业版计划与咨询公司或服务区分开来。这还为客户提供了一个参考点,他们可以在项目中使用并学习。此外,这还有助于吸引那些对 React Flow 感兴趣的公司。

    我喜欢这种方法的原因


    React Flow 以其出色的开源库而闻名,但他们找到了一种明智的方式在商业上获得收入。他们在定价、包装和支持方面的决策都非常明智,并成功地转化了开源用户为付费客户。


    这是我了解到的一些有关将开源项目变为可持续收入的方法。希望这些例子能给你提供一些灵感和启示!


    支持包


    最后但同样重要的是,你可以围绕你的开源工作建立一家咨询公司,并向依赖于该工作的公司提供专业知识支持。

    • Babel 在他们的Open Collective页面上提供了每年 2.4 万美元的计划,其中公司每月可以获得 2 小时的电子邮件或视频支持。
    • curl 提供商业支持,甚至包括开发定制功能和代码审核以了解你如何使用 curl。
    • Filippo Valsorda向公司提供每年五位数的保留协议。Filippo 与工程师会面,了解他们的需求,并在开发他的开源软件时确保这些需求得到满足。Filippo 是一个密码学专家,所以公司可以签订更昂贵的合同,以获得他在与密码学相关的任何事物上的专业知识,而不仅仅是他自己的项目。

    我喜欢这种方法的原因


    为公司提供付费支持使你的项目保持完全开源的同时,比 Pro 订阅带来更多的收入。这个过程很难,但对于一个习惯于作为员工工作的人来说,很有吸引力。


    结论


    偶尔会在 Hacker News 上看到人们讨论开源模式的缺点,护者没有从受益于他们工作的公司那里获得任何收入。


    这不公平。他们能做些什么?可以有多种可行的选项可以生成可持续的收入,也有许多成功的例子说明人们今天正在这样做,并且已经持续了很久。这也可能适用于你,快去试试吧,否则你永远不会知道。


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

    图片转换成webp

    web
    webp的几个问题 1. 什么是webp? 最直接的就是一个图片的后缀是.webp而不是.png/.jpeg等,官方的说法就是由Google开发的一种用于图像压缩的现代格式,目的就是减小图片的大小从而提高网页加载速; 2. 是不是所有浏览器都支持webp图片?...
    继续阅读 »

    webp的几个问题


    1. 什么是webp?


    最直接的就是一个图片的后缀是.webp而不是.png/.jpeg等,官方的说法就是由Google开发的一种用于图像压缩的现代格式,目的就是减小图片的大小从而提高网页加载速;


    2. 是不是所有浏览器都支持webp图片?如何判断浏览器是否支持webp格式的图片


    不是所有的浏览器都支持 WebP 图片格式,但大多数主流的现代浏览器都已经支持了。以下是一些常见的浏览器对 WebP 格式的支持情况:



    • Google Chrome:支持 WebP 格式。

    • Mozilla Firefox:支持 WebP 格式。

    • Microsoft Edge:支持 WebP 格式。

    • Safari:从 Safari 14 开始,支持 WebP 格式
      要判断浏览器是否支持 WebP 格式的图片,可以使用 JavaScript 进行检测。以下是一种常用的方法:


    function isWebPSupported() {
    var elem = document.createElement('canvas');
    if (!!(elem.getContext && elem.getContext('2d'))) {
    // canvas 支持
    return elem.toDataURL('image/webp').indexOf('data:image/webp') === 0;
    }
    // canvas 不支持
    return false;
    }

    if (isWebPSupported()) {
    console.log('浏览器支持 WebP 格式');
    } else {
    console.log('浏览器不支持 WebP 格式');
    }


    上述代码通过创建一个 canvas 元素,并尝试将其转换为 WebP 格式的图片。如果浏览器支持 WebP 格式,则会返回一个以 "data:image/webp" 开头的数据 URL。


    通过这种方式,你可以在网页中使用 JavaScript 检测浏览器是否支持 WebP 格式,并根据需要提供适当的替代图片


    3. 图片转换成webp之后一定会比之前的图片更小吗?


    答案是否定的。一般来说,具有大量细节、颜色变化和复杂结构的图像可能会在转换为 WebP 格式后获得更好的压缩效果,反之有些转换后可能会比之前更大;所以最好是图片转换为 WebP 格式之前,建议进行测试和比较不同压缩参数和质量级别的结果,以找到最佳的压缩设置,对最终转换后变成更大的建议不做转换


    4. 如何将图片转换成webp



    • 图像编辑软件 如 Adobe Photoshop、GIMP 或在线工具,如 Google 的 WebP 编码器。这些工具可以让你将现有的图像转换为 WebP 格式,并选择压缩质量和压缩类型(有损或无损)

    • 插件转换webp插件文档链接接入


    image.png


    5. 项目中如何接入??


    思路:



    • 第一步肯定是转化将项目中的存储的图片文件通过插件转换出webp格式的图片

    • 判断网页运行的浏览器是否支持webp格式的图片,如果支持,将项目中所有使用png/jpeg的图片的全部替换成webp


    6. 转换出项目中图片的webp格式的图片


    const imagemin = require("imagemin");
    const imageminWebp = require("imagemin-webp");

    function transformToWebp(destination, filePaths) {
    await imagemin([filePath || `${destination}/*.{jpg,png}`], {
    destination: `${destination}/webp/`, // 转换出的webp图片放置在什么目录
    plugins: [imageminWebp({quality: 75})] // 使用imageminWebp转换转换质量级别设置多少
    })
    }

    具体到项目中,我们只希望转换我们当前正在开发的文件夹中的图片,而且已经转化的未作修改的就不要再重复转化; 如何知道哪些是新增的或者修改的呢? 想一想🤔️,是不是“git status”可以看到
    所以开始做如下调整


    // 获取git仓库中发生变更的文件列表
    function getGitStatusChangedImgFiles() {
    return String(execSync('git status -s'))
    .split('\n')
    .map(item => item.split(' ').pop()
    .filter(path => path.match(/\.(jpg)|(png)/))
    );
    };

    返回一个包含变更图片文件路径的数组['src/example/image/a.png','src/example/image/b.png', '……']


    const imgPaths = getGitStatusChangedImgFiles()
    async function transformAllChangedImgToWebp() {
    const resData = await promise.all(
    imgPaths.map(path => {
    const imgDir = path.replace(/([^\\/]+)\.([^\\/]+)/i, "") // src/banners/guardian_8/img/95_copy.png => src/banners/guardian_8/img/
    return transformToWebp(imgDir, path)
    })
    )
    const allDestinationPaths = resData.map((subArr) => subArr[0].destinationPath)
    // 如果这里我们想将生成的webp图片自动的add上去,那么就这样:
    execSync(`git add ${allDestinationPaths.join(" ")}`);
    }



    image.png


    什么时候转换成webp最好?


    我们在commit的时候进行转换图片,以及自动将转换的图片进行提交
    这样我们就可以运用git的钩子函数处理了;


    npm install husky --save-dev

    // .husky/pre-commit中
    #!/usr/bin/env sh
    . "$(dirname -- "$0")/_/husky.sh"

    current_branch=`git rev-parse --abbrev-ref HEAD`

    if [[ $current_branch === 'main']]; then
    # 生成 webp 图片
    npm run webp -- commit
    fi

    这样在我们commit时就会自动触发pre-commit钩子函数,在package.json中配置webp执行的脚步,执行上述transformAllChangedImgToWebp函数,然后在里面转换出webp图片并将新生成的webp自动git add上去,最后一并commit;


    知识点


    1. execSync是什么?


    execSync 是一个 Node.js 内置模块 child_process 中的方法,用于同步执行外部命令。在 Node.js 中,child_process 模块提供了一组用于创建子进程的函数,其中包括 execSync 方法。execSync 方法用于执行指定的命令,并等待命令执行完成后返回结果。


    const { execSync } = require('child_process'); const output = execSync(command, options);

    2. git status -s 会显示每个文件的状态信息



    • A:新增文件

    • M:修改文件

    • D:删除文件

    • R:文件名修改

    • C:文件的拷贝

    • U:未知状态


    image.png


    3. execSync('git status -s')返回值是什么?


    image.png


    通过String后就可以变成可见的字符串了,然后通过分割等就能拿到具体的修改的文件路径


    4. Husky是什么?


    Husky 是一个用于在 Git 提交过程中执行脚本的工具。它可以帮助开发人员在代码提交前或提交后执行一些自定义的脚本,例如代码格式化、代码质量检查、单元测试等。Husky 可以确保团队成员在提交代码之前遵循一致的规范和约定。


    Husky 的工作原理是通过在 Git 钩子(Git hooks)中注册脚本来实现的。Git 钩子是在特定的 Git 事件发生时执行的脚本,例如在提交代码前执行 pre-commit 钩子,或在提交代码后执行 post-commit 钩子。push代码前执行pre-push的钩子、编写提交信息时执行commit-msg的钩子可用于提交什么规范


    小结



    1. 通过execSync('git status -s')从中获取筛选当前新增/修改过的图片;

    2. 调用imagemin和imagemin-webp将图片转换出webp格式的图片

    3. husky的pre-commit中触发上述调用执行,并在里面顺道将新生成的webp一并add上去

    4. 至于后续生成的webp图片怎么使用,这将在下一篇文章中学习


    作者:东风t西瓜
    来源:juejin.cn/post/7260016275300155449
    收起阅读 »

    Token到底是什么?!

    web
    随着Web应用的发展,为了保证API通信的安全性,很多项目在进行设计时会采用JSON Web Token(JWT)的解决方案。 JWT是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为JSON对象。这种信息可以...
    继续阅读 »

    随着Web应用的发展,为了保证API通信的安全性,很多项目在进行设计时会采用JSON Web TokenJWT)的解决方案。


    JWT是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为JSON对象。这种信息可以被验证和信任,因为它是数字签名的。


    那么JWT中的Token到底是什么?接下来,我们将以登录功能为例进行Token的分析。


    登录流程


    很多小伙伴对登录的流程已经很熟悉了,我们来看一个最基本的后台系统的登录流程


    登录流程图.png


    流程图很清楚了,接下来我们使用 V2Koa 实现一个登录过程,来看看Token到底是什么


    Vue2 + Koa 实现登录


    前端代码


    1. 前端点击事件


    数据的校验就忽略掉,感兴趣的同学可自行书写或者找我要源码,直接看点击事件


    handleLogin() {
    this.$refs.loginForm.validate((valid) => {
    if (valid) {
    this.loading = true;
    // 这里使用了VueX
    this.$store
    .dispatch("user/login", this.loginForm)
    .then(() => {
    this.$router.push({ path: this.redirect || "/" });
    this.loading = false;
    })
    .catch(() => {
    this.loading = false;
    });
    } else {
    return false;
    }
    });
    }

    2. Vuex中的action


    校验通过后触发VueXUser模块的Login方法:


    async login(context, userInfo) {
    const users = {
    username: userInfo.mobile,
    password: userInfo.password
    }
    const token = await login(users)
    // 在这里大家可以对返回的数据进行更详细的逻辑处理
    context.commit('SET_TOKEN', token)
    setToken(token)
    }

    3. 封装的接口


    export function login(data) {
    return request({
    url: '/login',
    method: 'post',
    data
    })
    }

    以上三步,是我们从前端向后端发送了请求并携带着用户名和密码,接下来,我们来看看Koa中是如何处理前端的请求的


    Koa 处理请求


    首先介绍一下Koa



    Koa 基于Node.js平台,由 Express 幕后的原班人马打造,是一款新的服务端 web 框架



    Koa的使用极其简单,感兴趣的小伙伴可以参考官方文档尝试用一下


    Koa官网:koa.bootcss.com/index.html#…


    1. 技术说明


    在当前案例的koa中,使用到了jsonwebtoken的依赖包帮助我们去加密生成和解密Token


    2. 接口处理


    const { login } = require("../app/controller/user")
    const jwt = require("jsonwebtoken")
    const SECRET = 'test_';
    router.post('/login', async (ctx, next) => {
    const { username, password } = ctx.request.body
    // 这里是调用Controller中的login方法来跟数据库中的数据作对比,可忽略
    const userList = await login(username, password)

    if (!userList) {
    // 这里的errorModel是自己封装的处理错误的模块
    ctx.body = new errorModel('用户名或密码错误', '1001')
    return
    }

    // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓ ※ 重点看这里 ※ ↓↓↓↓↓↓↓↓↓↓↓↓↓↓
    const token = jwt.sign({ userList }, SECRET, { expiresIn: "1h" })

    ctx.body = {
    success: true,
    state: 200,
    message: 'login success',
    data: token
    };
    return;
    })

    关于 JWT


    上面的重点代码大家看到了,接下来具体给大家解释下JWT



    Jwt由三部分组成:headerpayloadsignature



    export interface Jwt {
    header: JwtHeader;
    payload: JwtPayload | string;
    signature: string;
    }

    header头部


    里面的包含的内容有很多,比如用于指定加密算法的alg、指定加密类型的typ,全部参数如下所示:


    export interface JwtHeader {
    alg: string | Algorithm;
    typ?: string | undefined;
    cty?: string | undefined;
    crit?: Array<string | Exclude<keyof JwtHeader, 'crit'>> | undefined;
    kid?: string | undefined;
    jku?: string | undefined;
    x5u?: string | string[] | undefined;
    'x5t#S256'?: string | undefined;
    x5t?: string | undefined;
    x5c?: string | string[] | undefined;
    }

    payload负载


    payload使我们存放信息的地方,里面包含了签发者过期时间签发时间等信息


    export interface JwtPayload {
    [key: string]: any;
    iss?: string | undefined;
    sub?: string | undefined;
    aud?: string | string[] | undefined;
    exp?: number | undefined;
    nbf?: number | undefined;
    iat?: number | undefined;
    jti?: string | undefined;
    }

    signature签名


    signature 需要使用编码后的 headerpayload以及我们提供的一个密钥(SECRET),然后使用 header 中指定的签名算法进行签名


    关于 jwt.sign()


    jwt.sign()方法,需要三个基本参数和一个可选参数:payloadsecretOrPrivateKeyoptions和一个callback


    export function sign(
    payload: string | Buffer | object,
    secretOrPrivateKey: Secret,
    options: SignOptions,
    callback: SignCallback,
    ): void;

    payload是我们需要加密的一些信息,这个参数对应上面koa代码中的{ userList },而userList则是我从数据库中查询得到的数据结果


    secretOrPrivateKey则是我们自己定义的秘钥,用来后续验证Token时所用


    options选项中有很多内容,例如加密算法algorithm、有效期expiresIn等等


    export interface SignOptions {
    /**
    * Signature algorithm. Could be one of these values :
    * - HS256: HMAC using SHA-256 hash algorithm (default)
    * - HS384: HMAC using SHA-384 hash algorithm
    * - HS512: HMAC using SHA-512 hash algorithm
    * - RS256: RSASSA using SHA-256 hash algorithm
    * - RS384: RSASSA using SHA-384 hash algorithm
    * - RS512: RSASSA using SHA-512 hash algorithm
    * - ES256: ECDSA using P-256 curve and SHA-256 hash algorithm
    * - ES384: ECDSA using P-384 curve and SHA-384 hash algorithm
    * - ES512: ECDSA using P-521 curve and SHA-512 hash algorithm
    * - none: No digital signature or MAC value included
    */

    algorithm?: Algorithm | undefined;
    keyid?: string | undefined;
    /** expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms.js). Eg: 60, "2 days", "10h", "7d" */
    expiresIn?: string | number | undefined;
    /** expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms.js). Eg: 60, "2 days", "10h", "7d" */
    notBefore?: string | number | undefined;
    audience?: string | string[] | undefined;
    subject?: string | undefined;
    issuer?: string | undefined;
    jwtid?: string | undefined;
    mutatePayload?: boolean | undefined;
    noTimestamp?: boolean | undefined;
    header?: JwtHeader | undefined;
    encoding?: string | undefined;
    allowInsecureKeySizes?: boolean | undefined;
    allowInvalidAsymmetricKeyTypes?: boolean | undefined;
    }

    callback则是一个回调函数,有两个参数,默认返回Token


    export type SignCallback = (
    error: Error | null,
    encoded: string | undefined,
    ) =>
    void;

    通过以上方法加密之后的结果就是一个Token


    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU3ZmVmMTY0ZTU0YWY2NGZmYzUzZGJkNSIsInhzcmYiOiI0ZWE1YzUwOGE2NTY2ZTc2MjQwNTQzZjhmZWIwNmZkNDU3Nzc3YmUzOTU0OWM0MDE2NDM2YWZkYTY1ZDIzMzBlIiwiaWF0IjoxNDc2NDI3OTMzfQ.PA3QjeyZSUh7H0GfE0vJaKW4LjKJuC3dVLQiY4hii8s


    总结


    在整个的Koa中,用到了jsonwebtoken这个依赖包,里面有sign()方法


    而我们前端所得到的数据通过sign()加密出来的包含自定义秘钥的一份用户信息而已


    至于用户信息中有什么内容,可以随便处理,比如用户的ID、用户名、昵称、头像等等


    那么这个Token后续有什么用呢?


    后续我们可以在前端的拦截器中配置这个Token,让每一次的请求都携带这个Token,因为Koa后续需要对每一次请求进行Token的验证


    比如登录成功后请求用户的信息,获取动态路由,再通过前端的router.addRoutes()将动态路由添加到路由对象中去即可


    作者:半截短袖
    来源:juejin.cn/post/7275211391102189628
    收起阅读 »

    移动端的「基金地图」是怎么做的?

    web
    🙋🏻‍♀️ 编者按:本文作者是蚂蚁集团前端工程师芒僧,今年 8 月份开始到 9 月底,「支付宝 - 基金」里面的指数专区进行了一波大改版升级,这次借助 F2 4.0 强大的移动端手势交互能力,我们尝试了一种多维度探索式选基决策工具,本文具体介绍了它是如何实现的...
    继续阅读 »

    🙋🏻‍♀️ 编者按:本文作者是蚂蚁集团前端工程师芒僧,今年 8 月份开始到 9 月底,「支付宝 - 基金」里面的指数专区进行了一波大改版升级,这次借助 F2 4.0 强大的移动端手势交互能力,我们尝试了一种多维度探索式选基决策工具,本文具体介绍了它是如何实现的。



    Kapture 2022-10-19 at 14.12.19.gif


    这次在 「支付宝 - 基金」里的【指数专区改版】需求,我们玩了一种很新的东西 🌝


    8月份开始到9月底,「支付宝 - 基金」里面的指数专区进行了一波大改版升级,这次借助 F2 4.0 强大的移动端手势交互能力,我们尝试了一种多维度探索式选基决策工具(如上动图所示)。


    简单来说,用户可以在一个散点图上根据 「收益」和「波动」 这两个维度全览对比整个市场里的指数基金,并选出适合自己的指数基金进行投资,这个功能我们愿称其为 「指数图谱」 🐶 。



    图谱是这个业务场景上的叫法,实际上图谱应该是关系图而非统计图.



    image.pngimage.pngimage.png


    功能已发布,页面访问路线如上


    先看看有哪些功能点



    1. 精细打磨的移动端手势交互,平移、缩放、横扫不在话下 :


    Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 19.00.49.gif Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 19.01.38.gif Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 19.43.17.gif


    依次为:缩放、平移、横扫



    1. 底部产品卡和图表的联动交互:


    Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 19.54.40.gif Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 19.54.40.gif


    依次为:点击图表上的气泡、滑动底部卡片



    1. 无惧数据点太多看不到细节,我们有自适应的气泡抽样展示和自动聚焦:


    Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 19.58.03.gif Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 20.03.46.gif


    依次为:抽样优化前、抽样优化后


    那么,怎么做的呢?


    最开始看到这个需求的时候,当时觉得可行性比较低。因为需求里面针对图谱的方案以及细节都特别模糊;不敢承诺各种功能和排期,所以先做了一轮比较完整的系分,增加一些说话的底气🫣


    📱 第一步:同类产品调研


    因为设计同学的灵感来自于 大众点评APP 上面的「美食地图」,所以第一步就是做了一次「同类产品调研」,仔细去看了一下 「美食地图」上究竟有哪些花样,有哪些体验优化的小细节,不看不知道,一看发现细节原来这么多啊 🤕:


    图表和卡片的交互联动点抽样展示列表视图和卡片视图可切换交互时卡片自动折叠散点懒加载上滑直接唤起详情页
    21.gif22.gif1658280205580-84108c85-793b-4318-89af-7504f3517613.gif24.gif25.gif1658280765505-5eb8bb05-30c5-45a0-bd81-9f494267b843.gif

    做完这一步之后,大概能够知道自己距离“成品”有多远的距离,方便自己评估工期;另外还可以在系分评审的时候把这些细节提出来,防止临近发布了突然发现某个交互逻辑有个致命的漏洞(别问我怎么知道的,要命的)。
    这波调研之后,最终我们在实现上致敬了「美食地图」50% 的体验细节优化 (狗头)。


    ⚙️ 第二步:功能点分析


    第二步就是从需求本身的角度做功能点的分析,这样可以方便我们拆分组件,为后续做分层设计打下基础,明白哪些是需要支持可扩展的。这一步大家都熟悉,就不赘述了:
    image.png


    📦 第三步:通用化设计


    有了功能点的分析之后,就可以进行通用化的设计了,这就来到了喜闻乐见的沉淀组件的设计环节 🌝


    我们希望这个功能不仅仅是纯业务代码**,期望下次能够复用大部分核心功能 **(理想很丰满),所以在系分的时候是往通用化的方向去设计的,这里主要做了三件事情:分层设计概念标准化核心流程定义



    1. 分层设计


    拆的逻辑是按最基础的 M(数据层) - C(控制层) - V(视图层) 拆分的。


    image.png


    有了分层设计和功能点分析之后,就可以知道哪些应该放到组件内,哪些接口应该被抽象成通用接口,哪些应该保留扩展性供使用者自己来定义,就可以画个表格了,一一决定哪些模块应该放到组件内:
    image.png



    1. 概念标准化


    下面来到造词环节,把一些常用的概念都定义成一个个名字,这样方便和后端、设计协同的时候效率更高,同时也方便自己定义清楚各个模型(类)。(这里其实取名越贴切越形象越好,有点考验语言能力了属实是)
    image.png



    1. 核心流程定义


    这一步是脑补环节,在脑子里跑一遍整体的流程,也是整个需求最核心的流程,比如这里会分成四种流程:初始化流程 、散点图交互流程、底部卡片交互流程、顶部tab交互流程


    进而可以将四种流程里面的各节点做一些归类,比如都会有图表渲染、数据补全、卡片渲染这些共同的节点,而这些节点就可以实现成具体模型里的具体方法。


    image.png


    🌝 第四步:难点分析


    根据上面拆分的各模块,列出哪些点是实现有困难的,耗时长的。这样就可以在评估工期的时候多 Battle 一下,还能砍砍需求,更可以让底层引擎/SDK来突破这些难点(比如找 F2 的核心开发者) :


    image.png
    image.png


    📃 最后一步:


    按照上述的设计进行代码编写。


    难点实现


    1. 移动端的图表手势交互体验优化


    开发之初,F2 只支持单轴(x或者y)的平移缩放,也不支持全方向交互;在 swipe 上的体验也不太好(阻尼感很强),所以在项目开发过程中, F2 完成了很多体验优化,打磨出很多细致入微的良好体验:



    • X轴、Y轴可同时开启平移、缩放

    • swiper 体验效果优化

    • 移出可视区之后的蒙层遮挡能力(view-clip)

    • zIndex 元素层叠渲染

    • 平移缩放性能优化


    2. 气泡抽样展示优化


    因为散点图上的点在初始化的缩放比例下分布非常密集,所以如果每个点上面都绘制一个气泡的话,就会显得密密麻麻的,根本无从下手(如下图1所示)。针对这样的问题,做了「气泡抽样展示」的优化。


    image.png


    实现方式上就是渲染前遍历所有的点,如果在这个点周围某个半径距离之内有其他点,那么就认为这个点是脏点(dirty point),最后筛选出所有“干净”的点进行气泡展示。


    如下图图1所示,灰色点(右上角)是干净点,而灰白色的点(偏中间的位置)因为其在圆圈半径范围之内有其他点存在,所以这个点是脏点。


    image.png



    多提一句,这样的过滤方式会使得密集区域的点都不会展示气泡,后续会进行优化。



    3. 获取到可视区内的所有点


    image.png
    由于做了气泡抽样展示,所以上图中的底部卡片只会展示用户可视区内散点图上有气泡的点(细心的盆友可以发现,散点图上有两种点,一种是带气泡的交互点,一种是不带气泡的缩略点)。那么就需要一个获取「可视区内所有的点」,实现思路如下:


    - 监听 PanEnd(平移结束)、PinchEnd(缩放结束), SwipeEnd(横扫结束)的事件
    - 获取到平移/缩放/横扫之后最新的 scales
    - 根据最新的 scales 里面的 x、y 的 range 过滤一遍图表原数据
    - 将脏点从上一步的结果过滤出去
    - 底部卡片根据上一步的结果进行渲染展示
    - 结束



    // 根据当前的缩放比例,拿到「可视区」范围内的数据
    function getRecordsByZoomScales(scales, data) {
    const { x: xScale, y: yScale } = scales;
    const { field: xField, min: xMin, max: xMax } = xScale;
    const { field: yField, min: yMin, max: yMax } = yScale;

    return data.filter((record) => {
    const isInView =
    record[xField] >= xMin &&
    record[xField] <= xMax &&
    record[yField] >= yMin &&
    record[yField] <= yMax;

    return isInView;
    });
    }


    // 使用时
    export default props => {
    // 图表原数据
    const { data } = props;

    function handlePanEnd (scales, data) {
    // 手动高亮下面这一行
    getRecordsByZoomScales(scales, data);
    }

    return (
    <ReactCanvas>
    <Chart>
    {/* ... */}
    <ScrollBar onPanEnd={handlePanEnd}/>
    </Chart>
    </ReactCanvas>

    )

    }

    4. 数据懒加载


    image.pngimage.png
    底部卡片的数量是由散点图上点的数量决定的,而每张卡上都有不少的数据量(基金产品信息、指数信息、标签信息),所以不能一次性就把所有点里关联的数据都查询出来(会导致接口返回数据过多)。


    这里采取的是懒加载的方式 ,每次只在交互后查询相邻 N+2/N-2 张的卡片数据,并且增加了一份内存缓存来存储已经查询过的卡片数据:


    image.png


    基本的流程图如下:


    - 触发散点图交互/滑动底部卡片
    - 读取缓存,过滤出没有缓存过的卡片
    - 发起数据调用,获取到卡片的数据
    - 写入缓存
    - 更新卡片数据,返回
    - 更新卡片视图,渲染完成

    实际线上效果


    项目上线之后,我们发现散点图区域的交互率(包含平移,缩放)非常高,可以看出用户对新类型的选基工具抱有新鲜感,也乐于去进行探索;也有部分用户能够通过工具完成决策或者进行产品之间的详细对比(即点击底部卡片上的详情按钮),起到了一个工具类产品的作用 🌝 。


    致谢


    感谢 AntV 以及 F2 对移动端图表交互能力的支持。


    作者:支付宝体验科技
    来源:juejin.cn/post/7176891015112949819
    收起阅读 »

    Vue3为什么推荐使用ref而不是reactive

    web
    为什么推荐使用ref而不是reactive reactive本身具有很大局限性导致使用过程需要额外注意,如果忽视这些问题将对开发造成不小的麻烦;ref更像是vue2时代option api的data的替代,可以存放任何数据类型,而reactive声明的数据类...
    继续阅读 »

    为什么推荐使用ref而不是reactive



    reactive本身具有很大局限性导致使用过程需要额外注意,如果忽视这些问题将对开发造成不小的麻烦;ref更像是vue2时代option apidata的替代,可以存放任何数据类型,而reactive声明的数据类型只能是对象;



    先抛出结论,再详细说原因:非必要不用reactive! (官方文档也有对应的推荐)


    官方原文:建议使用 ref() 作为声明响应式状态的主要 API。


    最懂Vue的人都这么说了:推荐ref!!!!!!


    image.png


    reactiveref 对比


    reactiveref
    ❌只支持对象和数组(引用数据类型)✅支持基本数据类型+引用数据类型
    ✅在 <script><template> 中无差别使用❌在 <script><template> 使用方式不同(script中要.value)
    ❌重新分配一个新对象会丢失响应性✅重新分配一个新对象不会失去响应
    能直接访问属性需要使用 .value 访问属性
    ❌将对象传入函数时,失去响应✅传入函数时,不会失去响应
    ❌解构时会丢失响应性,需使用toRefs❌解构对象时会丢失响应性,需使用toRefs


    • ref 用于将基本类型的数据(如字符串、数字,布尔值等)和引用数据类型(对象) 转换为响应式数据。使用 ref 定义的数据可以通过 .value 属性访问和修改。

    • reactive 用于将对象转换为响应式数据,包括复杂的嵌套对象和数组。使用 reactive 定义的数据可以直接访问和修改属性。


    原因1:reactive有限的值类型


    reactive只能声明引用数据类型(对象)


    let  obj = reactive({
      name: '小明',
      age : 18
    })

    ref既能声明基本数据类型,也能声明对象和数组;



    Vue 提供了一个 ref() 方法来允许我们创建可以使用任何值类型的响应式 ref



    //对象
    const state = ref({})
    //数组
    const state2 = ref([])

    原因2:reactive使用不当会失去响应:



    reactive一时爽,使用不恰当的时候失去响应泪两行,开开心心敲代码过程中,会感叹!!咦?怎么不行?为什么这么赋值失去响应了? 辣鸡reactive!!! 我要用 ref 👉👉yyds



    1. 给reactive赋一整个普通对象/reactive对象


    通常在页面数据回显时,需要将AJAX请求获取的对象直接赋值给响应式对象,如果操作不当就导致reactive声明的对象失去响应





    • 赋值一个普通对象


      let state = reactive({ count: 0 })
      //这个赋值将导致state失去响应
      state = {count: 1}



    • 赋值一个reactive对象



      如果给reactive的响应式对象赋值普通对象会失去响应,那么给它赋值一个reactive的响应式对象不就行了吗?下面试试看





    <template>
    {{state}}
    </template>    

    <stcirpt setup>
    const state = reactive({ count: 0 })
    //nextTick异步方法中修改state的值
    nextTick(() => {
    //并不会触发修改DOM ,说明失去响应了
    state = reactive({ count: 11 });
    });
    </stcirpt>

    nexTick中给state赋值一个reactive的响应式对象,但是DOM并没有更新!


    解决方法:



    1. 不要直接整个对象替换,对象属性一个个赋值


      let state = reactive({ count: 0 })
      //state={count:1}
      state.conut = 1



    2. 使用Object.assign


      let state = reactive({ count: 0 })
      // state = {count:1}   state失去响应
      state = Object.assign(state , {count:1})



    3. 使用ref定义对象



      非必要不用reactive



      let state = ref({ count: 0 })
      state.value={count:1}



    为什么同样是赋值对象ref不会失去响应而reactive会?

    ref 定义的数据(包括对象)时,返回的对象是一个包装过的简单值,而不是原始值的引用;



    就和对象深拷贝一样,是将对象属性值的赋值



    reactive定义数据(必须是对象),reactive返回的对象是对原始对象的引用,而不是简单值的包装。



    类似对象的浅拷贝,是保存对象的栈地址,无论值怎么变还是指向原来的对象的堆地址;


    reactive就算赋值一个新的对象,reactive还是指向原来对象堆地址



    2.将reactive对象的属性-赋值给变量(断开连接/深拷贝)


    这种类似深拷贝不共享同一内存地址了,只是字面量的赋值;对该变量赋值也不会影响原来对象的属性值



    let state = reactive({ count: 0 })
    //赋值
    // n 是一个局部变量,同 state.count
    // 失去响应性连接
    let n = state.count
    // 不影响原始的 state
    n++
    console.log(state.count) //0

    有人就说了,既然赋值对象的属性,那我赋值一整个对象不就是浅拷贝了吗?那不就是上面说的给响应式对象的字面量赋一整个普通对象/reactive对象这种情况吗?这种是会失去响应的


    3.直接reactive对象解构时


    • 直接解构会失去响应


    let state = reactive({ count: 0 })
    //普通解构count 和 state.count 失去了响应性连接
    let { count } = state
    count++ // state.count值依旧是0

    解决方案:



    • 使用toRefs解构不会失去响应



      使用toRefs解构后的属性是ref的响应式数据





    const state = reactive({ count: 0 })
    //使用toRefs解构,后的属性为ref的响应式变量
    let { count } = toRefs(state)
    count.value++ // state.count值改变为1

    建议: ref一把梭



    当使用reactive时,如果不了解reactive失去响应的情况,那么使用reactive会造成很多困扰!



    推荐使用ref总结原因如下:




    1. reactive有限的值类型:只能声明引用数据类型(对象/数组)




    2. reactive在一些情况下会失去响应,这个情况会导致数据回显失去响应(数据改了,dom没更新)


      给响应式对象的字面量赋一整个普通对象,将会导致reactive声明的响应式数据失去响应


      <template>
        {{state.a}}
        {{state.b}}
        {{state.c}}
      </template>

      <script>
      let state = reactive({ a:1,b:2,c:3 })
      onMounted(()=>{
          //通AJAX请求获取的数据,回显到reactive,如果处理不好将导致变量失去响应,
         //回显失败,给响应式数据赋值一个普通对象
         state = { a:11,b:22,c:333 }
        //回显成功,一个个属性赋值  
         state.a = 11
         state.b = 22
         state.c = 33
      })
      </script>

      上面这个例子如果是使用ref进行声明,直接赋值即可,不需要将属性拆分一个个赋值


      使用ref替代reactive:


      <template>
        {{state.a}}
        {{state.b}}
        {{state.c}}
      </template>

      <script>
      let state = ref({ a:1,b:2,c:3 })
      onMounted(()=>{
         //回显成功
         state.value = { a:11,b:22,c:333 }
      })
      </script>



    3. ref适用范围更大,声明的数据类型.基本数据类型和引用数据类型都行




    虽然使用ref声明的变量,在读取和修改时都需要加.value小尾巴,但是正因为是这个小尾巴,我们review代码的时候就很清楚知道这是一个ref声明的响应式数据;


    ref的.value小尾巴好麻烦!


    ref声明的响应式变量携带迷人的.value小尾巴,让我们一眼就能确定它是一个响应式变量!虽然使用ref声明的变量,在读取和修改时都需要加.value小尾巴,但是正因为是这个小尾巴,我们review代码的时候就很清楚知道这是一个ref声明的响应式数据;


    可能有些人不喜欢这个迷人小尾巴,如果我能自动补全阁下又如何应对?


    volar插件能自动补全.value (强烈推荐!!!!!!!)



    本人推荐ref一把梭,但是ref又得到处.value ,那就交给插件来完成吧!!!





    • valor 自动补全.value (不是默认开启,需要手动开启)




    • 不会有人不知道Vue3需要不能使用vetur要用valor替代吧?不会不会吧? (必备volar插件)




    volar设置自动填充value.gif
    可以看到当输入ref声明的响应式变量时,volar插件自动填充.value 那还有啥烦恼呢? 方便!


    本文会根据各位的提问和留言持续更新;


    @ 别骂了_我真的不懂vue 说(总结挺好的,因此摘抄了):



    reactive 重新赋值丢失响应是因为引用地址变了,被proxy代理的对象已经不是原来那个所以丢失响应了,其实ref也是一样的,当把.value那一层替换成另外一个有着.value的对象也会丢失响应 ref定义的属性等价于reactive({value:xxx})

    另外说使用Object.assign为什么可以更新模板

    Object.assign解释是这样的: 如果目标对象与源对象具有相同的键(属性名),则目标对象中的属性将被源对象中的属性覆盖,后面的源对象的属性将类似地覆盖前面的源对象的同名属性。

    那个解决方法里不用重新赋值,直接Object.assign(state,{count:1})即可,所以只要proxy代理的引用地址没变,就会一直存在响应性



    作者:我要充满正能量
    来源:juejin.cn/post/7270519061208154112
    收起阅读 »

    H5快速上手鸿蒙元服务(前端)

    web
    一、前言 鸿蒙元服务虽然与h5在很多地方虽然有相似之处,但还是有部分不同的地方,鸿蒙服务开发模式更接近与vue2版本,很多写法与其相似。该篇文章主要用于帮助有h5基础的伙伴能够快速上手鸿蒙元服务,并且对个人在开发过程中遇到的一些坑做个总结。 二、开发相关 项目...
    继续阅读 »

    一、前言


    鸿蒙元服务虽然与h5在很多地方虽然有相似之处,但还是有部分不同的地方,鸿蒙服务开发模式更接近与vue2版本,很多写法与其相似。该篇文章主要用于帮助有h5基础的伙伴能够快速上手鸿蒙元服务,并且对个人在开发过程中遇到的一些坑做个总结。


    二、开发相关


    项目目录


    a51bd7a8cf9c80848926b24be2b8a27.jpg


    cd3136d4b9ca22519a4934b79db8d4e.jpg
    前端部分主要看js目录下的文件目录即可,除default目录外,其他文件都是与服务卡片相关的。


    commom:存放公共配置文件方法等

    components:存放公共组件
    i18n:i18n相关

    media:存放静态文件,图片等

    pages:存放页面的目录,包括js,hml,css

    utils:存放工具方法,比如网络请求封装等

    app.js:全局文件,能够在这个文件中定义全局变量,拥有应用级的生命周期函数


    其他关键目录:


    supervisual:低代码相关

    config.json:项目配置相关,包括路由等


    config.json文件


    用于给整个项目进行一些关键配置


    定义路由


    image.png
    这种定义路由的方式,可能开发过微信小程序的伙伴会比较熟悉,在微信小程序中,一般第一个路径即是项目打开的页面,可惜在鸿蒙元服务中没有这个便捷的功能,designWidth用于定义页面以多宽的设计图来绘制,autoDesginWidth设为true,即是系统根据手机自动设置。


    config.json详细配置请看官方文档: developer.harmonyos.com/cn/docs/doc…


    HML


    HML是一套类HTML的标记语言,通过组件,事件构建出页面的内容。页面具备数据绑定、事件绑定、列表渲染、条件渲染和逻辑控制等高级能力,由鸿蒙内部实现。


    <!-- xxx.hml -->
    <div class="container">
    <text class="title">{{count}}</text>
    <div class="box">
    <input type="button" class="btn" value="increase" onclick="increase" />
    <input type="button" class="btn" value="decrease" @click="decrease" />
    <!-- 传递额外参数 -->
    <input type="button" class="btn" value="double" @click="multiply(2)" />
    <input type="button" class="btn" value="decuple" @click="multiply(10)" />
    <input type="button" class="btn" value="square" @click="multiply(count)" />
    </div>
    </div>

    // xxx.js
    export default {
    data: {
    count: 0
    },
    increase() {
    this.count++;
    },
    decrease() {
    this.count--;
    },
    multiply(multiplier) {
    this.count = multiplier * this.count;
    }
    };
    /* xxx.css */
    .container {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    left: 0px;
    top: 0px;
    width: 454px;
    height: 454px;
    }
    .title {
    font-size: 30px;
    text-align: center;
    width: 200px;
    height: 100px;
    }
    .box {
    width: 454px;
    height: 200px;
    justify-content: center;
    align-items: center;
    flex-wrap: wrap;
    }
    .btn {
    width: 200px;
    border-radius: 0;
    margin-top: 10px;
    margin-left: 10px;
    }

    看这段代码是不是就觉得很亲近了,在hml中通过“{{}}”的形式绑定数据,用@和on的方法来绑定事件,同时支持冒泡、捕获等方式。


    列表渲染for


    <!-- xxx.hml -->
    <div class="array-container" style="flex-direction: column;margin: 200px;">
    <!-- div列表渲染 -->
    <!-- 默认$item代表数组中的元素, $idx代表数组中的元素索引 -->
    <div for="{{array}}" tid="id" onclick="changeText">
    <text>{{$idx}}.{{$item.name}}</text>
    </div>
    <!-- 自定义元素变量名称 -->
    <div for="{{value in array}}" tid="id" onclick="changeText">
    <text>{{$idx}}.{{value.name}}</text>
    </div>
    <!-- 自定义元素变量、索引名称 -->
    <div for="{{(index, value) in array}}" tid="id" onclick="changeText">
    <text>{{index}}.{{value.name}}</text>
    </div>
    </div>


    tid等于vue中的key,id即为array每一项中的唯一属性,需要注意的是,与vue不同,在鸿蒙元服务中,tid是必须的,如果没有tid可能会引起运行异常的情况。


    条件渲染if和show


    <!-- xxx.hml -->
    //if
    <div class="container">
    <button class="btn" type="capsule" value="toggleShow" onclick="toggleShow"></button>
    <button class="btn" type="capsule" value="toggleDisplay" onclick="toggleDisplay"></button>
    <text if="{{visible}}"> Hello-world1 </text>
    <text elif="{{display}}"> Hello-world2 </text>
    <text else> Hello-World </text>
    </div>


    //show
    <!-- xxx.hml -->
    <div class="container">
    <button class="btn" type="capsule" value="toggle" onclick="toggle"></button>
    <text show="{{visible}}" > Hello World </text>
    </div>


    if和show相当于vue中的v-if和v-show,原理也一样。


    自定义组件使用(props和emit传值)


    <!-- template.hml -->
    <div class="item">
    <text>Name: {{name}}</text>
    <text>Age: {{age}}</text>
    <text class="text-style" onclick="childClicked" id="text" ref="animator">点击这里查看隐藏文本</text>
    </div>

    <!-- template.js -->
    export default {
    props:{
    name,
    age
    contentList
    }
    childClicked () {
    //获取标签对象
    //this.$element("text");
    //this.$element("text").currentOffset().y 获取属性;
    //通过ref的形式来获取
    //this.$refs.animator
    this.$emit('eventType1',{text:'123'});
    },
    };
    <!-- index.hml -->
    //注册
    <element name='comp' src='../../common/template.hml'></element>
    <div>
    //使用
    <comp name="Tony" age="18" content-list="contentList" @event-type1="textClicked"></comp>
    </div>

    <!-- template.js -->
    export default {
    textClicked (e) {
    //e.detail 拿到传过来的数据 e.detail.text
    },
    };


    注意:组件传递props和emit属性时,强制使用横杆连接的变量名进行传递,接收时,需要使用驼峰名进行接收,通过e.detail拿到emit传过来的参数,通过$element()方法或ref的形式来获取元素对象,其他用法基本和vue2相同。



    生命周期和插槽等用法参考官方文档developer.harmonyos.com/cn/docs/doc…


    通用事件


    developer.harmonyos.com/cn/docs/doc…


    内部系统组件


    image.png
    常用的组件包括:

    容器组件:dialog、div、滚动组件用于上拉加载(list、list-item、list-item-group)、popup、轮播组件(swiper)

    基础组件:image、text、span、input、label


    <div>
    <text>123</text>
    </div>


    注意:

    1.div组件内部不能够直接嵌入文字,需要通过text组件进行包裹

    2.list组件在相同方向的滚动不能嵌套使用,否则会造成滚动异常

    3.image标签有些图片格式不支持,需要转换为可支持的格式



    CSS


    华为鸿蒙元服务不支持less,sass等预编译语言,只支持css,相对于h5来说,还做了部分阉割,有些属性在h5能用,在鸿蒙元服务确用不了。


    元素标签默认样式


    需要注意的是,在元服务中,所有的div标签都是一个flex盒子,所以在我们使用div的时候,如果是纵向布局,那我们需要去手动改变flex-direction: column,更改主轴方向。


    //hml
    <div id="tabBarCon">
    <div id="tab1">
    </div>

    <div id="tab2" onclick="handleJumpToCart">
    </div>

    <div id="tab3" onclick="handleJumpToMine">
    </div>

    </div>
    //css
    .tabBarCon{
    flex-direction:column;
    }

    元素选择器


    image.png


    image.png
    只支持部分选择器和部分伪类选择器,像h5中的伪元素选择器都是不支持的,也不支持嵌套使用,由于不存在伪元素选择器,所以遇到有时候一些特殊场景时,我们只能在hml中去判断元素索引来添加动态样式。


    属性与h5中的差异


    属性鸿蒙元服务h5
    position只支持absolute、relative、fixed支持absolute、relative、fixed、sticky
    background渐变linear-gradient(134.27deg, #ff397e 0%, #ff074c 98%),渐变百分比不支持带小数点支持
    长度单位只支持px、百分比,不支持rem、em、vw、vhpx、百分比、rem、em、vw、vh
    多行文字省略text-overflow: ellipsis; max-lines: 1;(只能用于text组件)单行和多行使用属性不同

    JS


    特点:

    1.支持ES6

    2.用法和vue2相似


    // app.js
    export default {
    onCreate() {
    console.info('Application onCreate');
    },
    onDestroy() {
    console.info('Application onDestroy');
    },
    globalData: {
    appData: 'appData',
    appVersion: '2.0',
    },
    globalMethod() {
    console.info('This is a global method!');
    this.globalData.appVersion = '3.0';
    }
    };
    // index.js页面逻辑代码
    export default {
    data: {
    appData: 'localData',
    appVersion:'1.0',
    },
    onInit() {
    //获取全局属性
    this.appData = this.$app.$def.globalData.appData;
    this.appVersion = this.$app.$def.globalData.appVersion;
    },
    invokeGlobalMethod() {
    this.$app.$def.globalMethod();
    },
    getAppVersion() {
    this.appVersion = this.$app.$def.globalData.appVersion;
    }
    }

    data:定义变量

    onInit:生命周期函数

    getAppVersion:方法,不需要写在methods里面,直接与生命周期函数同级
    this.app.app.def:可以拿到全局对象,


    导入导出


    支持ESmodule


    //import
    import router from '@ohos.router';
    //export
    export const xxx=123;

    应用级生命周期


    image.png


    页面级生命周期


    image.png


    网络请求


    使用@ohos.net.http内置模块即可,下面是对网络请求做了一个简单封装,使用的时候直接导入,调用相应请求方法即可,可惜的鸿蒙元服务目前没法进行抓包,所以网络请求调试的时候只能通过打断点的形式进行调试。


    import http from '@ohos.net.http';


    import { invokeShowLogin } from '../common/invoke_user';

    export default {
    interceptors(response) {
    const result = JSON.parse(response.result || {});
    const {code,errno} = result
    if (errno === 1024 || code === 1005) {
    return invokeShowLogin();
    }

    return result;
    },

    get(url, data) {
    return http.createHttp().request(
    // 填写http请求的url地址,可以带参数也可以不带参数。URL地址需要开发者自定义。请求的参数可以在extraData中指定
    url,
    {
    method: http.RequestMethod.GET, // 可选,默认为http.RequestMethod.GET
    // 开发者根据自身业务需要添加header字段
    header: {
    'Content-Type': 'application/json'
    },
    // 当使用POST请求时此字段用于传递内容
    extraData: data,
    connectTimeout: 10*1000,
    readTimeout: 10*1000,
    }
    ).then(res=>{
    return this.interceptors(res);
    });
    },

    post(url, data) {
    return http.createHttp().request(
    // 填写http请求的url地址,可以带参数也可以不带参数。URL地址需要开发者自定义。请求的参数可以在extraData中指定
    url,
    {
    method: http.RequestMethod.POST, // 可选,默认为http.RequestMethod.GET
    // 开发者根据自身业务需要添加header字段
    header: {
    'Content-Type': ' application/x-www-form-urlencoded'
    },
    // 当使用POST请求时此字段用于传递内容
    extraData: data,
    connectTimeout: 10*1000, // 可选,默认为60s
    readTimeout: 10*1000, // 可选,默认为60s
    }
    ).then(res=>{
    return this.interceptors(res);
    });
    },

    postJson(url, data) {
    return http.createHttp().request(
    // 填写http请求的url地址,可以带参数也可以不带参数。URL地址需要开发者自定义。请求的参数可以在extraData中指定
    url,
    {
    method: http.RequestMethod.POST, // 可选,默认为http.RequestMethod.GET
    // 开发者根据自身业务需要添加header字段
    header: {
    'Content-Type': 'application/json'
    },
    // 当使用POST请求时此字段用于传递内容
    extraData: data,
    connectTimeout: 10*1000, // 可选,默认为60s
    readTimeout: 10*1000, // 可选,默认为60s
    }
    ).then(res=>{
    return this.interceptors(res);
    })
    }
    }

    数据存储


    只有本地持久化存储这种方式,关闭应用,数据不会丢失。


    storage.set({
    key: 'loginInfo',
    value: JSON.stringify({
    uid, skey
    }),
    });
    storage.get({
    key: 'userInfo',
    value: JSON.stringify(userInfo),
    });

    路由跳转


    <!-- index.hml -->
    <div class="container">
    <text class="title">This is the index page.</text>
    <button type="capsule" value="Go to the second page" class="button" onclick="launch"></button>
    </div>

    // index.js
    import router from '@ohos.router';
    export default {
    launch() {
    router.push ({
    url: 'pages/detail/detail',
    //携带的参数
    params:{a:123}
    });
    //router.back()
    //router.replace()
    },
    }.
    // detail.js
    import router from '@ohos.router';
    export default {
    data:{
    a:''
    }
    onInit(){
    //页面携带过来的参数可以直接使用
    //this.a
    }
    }

    官方文档链接



    1. config.json:developer.harmonyos.com/cn/docs/doc… &developer.harmonyos.com/cn/docs/doc…

    2. http请求:developer.harmonyos.com/cn/docs/doc…

    3. hml:developer.harmonyos.com/cn/docs/doc…

    4. css:developer.harmonyos.com/cn/docs/doc…

    5. js:developer.harmonyos.com/cn/docs/doc…

    6. 生命周期:developer.harmonyos.com/cn/docs/doc…

    7. 目录结构:developer.harmonyos.com/cn/docs/doc…


    作者:前端小萌新y
    来源:juejin.cn/post/7275945995609964563
    收起阅读 »

    我来聊聊面向模板的前端开发

    web
    在软件开发中,研发效率永远是开发人员不断追求的主题之一。于公司而言,在竞争激烈的互联网行业中,产出得快和慢也许就决定着公司的生死存亡;于个人而言,效率高了就可以少加班,多出时间去提升自己、发展爱好、陪伴家人,工作、生活两不误。 提升效率的途径,无外乎就是「方法...
    继续阅读 »

    在软件开发中,研发效率永远是开发人员不断追求的主题之一。于公司而言,在竞争激烈的互联网行业中,产出得快和慢也许就决定着公司的生死存亡;于个人而言,效率高了就可以少加班,多出时间去提升自己、发展爱好、陪伴家人,工作、生活两不误。


    提升效率的途径,无外乎就是「方法」和「工具」。以一个开发者的思维来想,就是将工作内容进行总结、归纳,从一组相似的工作内容中提炼共同点,抽象出解决这一类问题的方法,从而造出便于在今后的工作中更为快速解决这类问题的工具。这个「工具」可以是个函数、组件、中间件、插件,也可以是 IDE、其他开发工具的扩展,甚至是语言。


    面向组件


    在现代前端开发中,如果去问一个业务前端开发:「如何提升团队开发效率?」对方所回答的内容中,极有可能会出现「组件库」。没错,在前端工程化趋近完善的今天,在近几年 React、Vue 等组件化库/框架的影响下,面向组件开发的思维方式早已深入人心。


    组件库提效有限


    现在,组件库已经是一个前端团队的必备设施了,长远来看,团队一定且必须要有自己的组件库。开源的第三方组件库再好,对于一家企业的前端团队来说也只是短期用来充饥的,因为它们无法完全满足一家公司的业务场景,并且出于多终端支持的考虑,必定要进行二次开发或者自研。


    组件库有了,团队和公司中推广的效果也不错,绝大多数的人都在用。使用组件开发页面相对 jQuery 时代要每块功能区都得从

    等 HTML 标签码起来说确实提升了效率,然而有限;要搞出页面需要反复去引入组件,然后组合拼装出来,就像工厂流水线上的工人拼装零件,仍然要去做很多重复动作。


    只要觉得当前的开发方式重复的动作多了,就代表还能继续提效,得想个法子减少重复无意义动作。


    面向组件的开发方式,是现代前端页面开发提效的初级阶段,也是一个团队所要必经的阶段。


    更高层面的提效


    在之前写的文章中有段话——



    组件可以很简单,也可以很复杂。按照复杂程度从小到大排的话,可以分为几类:



    1. 基础组件;

    2. 复合组件;

    3. 页面;

    4. 应用。


    对,不用揉眼睛,你没有看错!


    站在更高的角度去看,「页面」和「应用」也是一种「组件」,只不过它们更为复杂。在这里我想要说的不是它们,而是「基础组件」和「复合组件」。



    文中提到了「页面」和「应用」也可以看作是种「组件」。虽然与当时的想法有些差异,但本文的内容就是要在那篇文章的基础上简单聊聊在「页面」层面的提效。


    一般来说,「页面」是用户所能看到的最大、最完整的界面,如果能在这个层面有个很好的抽象方案,在做业务开发时与单纯地面向组件开发相比,应该会有更大的提效效果。


    GUI 发展了几十年,人机交互的图形元素及布局方式已经相对固定,只要不是出现像 Google Glass 之类的革命性交互设备,就不会发生重大改变。在业务开发中界面形式更是千篇一律,尤其是 web 页面,尤其是中后台系统的 web 页面,一定可以通过什么方式来将这种「千篇一律」进行抽象。


    试着来回想下,自己所做过的中后台系统的绝大部分页面是不是我所描述的这样——


    页面整体是上下或左右布局。如果是上下布局的话,上面是页头,下面的左侧可能有带页面导航的侧边栏,或者没有侧边栏直接将页面导航全部集中在页头中,剩余区域是页面主体部分,承载着这个页面的主要数据和功能;如果是左右布局,左侧毋庸置疑就是有页面导航的侧边栏,页头跑到了右侧上面,其余是页面主体。


    中后台系统的主要功能就是 CRUD,即业务数据的增删改查,相对应的页面展现及交互形式就是列表页、表单页和详情页。列表页汇总了所有业务数据的简要信息,并提供了数据的增、删、改和更多信息查看的入口;表单页肩负着数据新增和修改的功能;详情页能够看到一条业务数据记录最完整的信息。


    每新增一个业务模块,就要又写一遍列表页、表单页和详情页……反复做这种事情有啥意思呢?既然这三种页面会反复出现,那干脆封装几个页面级别的组件好了,有新需求的时候就建几个页面入口文件,里面分别引入相应的页面组件,传入一些 props,完活儿!


    这种方式看起来不错,然而存在几个问题:



    • 没有描述出页面内容的结构,已封装好的页面组件对于使用者来说算是个黑盒子,页面内容是什么结构不去看源码不得而知;

    • 如果新需求中虽然需要列表页、表单页和详情页,但与已封装好的能够覆盖大部分场景的相关组件所支持的页面有些差异,扩展性是个问题;

    • 每来新需求就要新建页面入口文件然后在里面引入页面组件,还是会有很多无意义重复动作和重复代码,时间长了还是觉得烦。


    我需要一种既能看一眼就理解内容结构和关系,又具备较好扩展性,还能减少重复代码和无意义动作的方式——是的,兜了一个大圈子终于要进入正题了——面向模板开发。


    面向模板


    面向模板的前端开发有三大要素:模板;节点;部件。


    富有表达力的模板


    我所说的「模板」的主要作用是内容结构的描述以及页面的配置,观感上与 XHTML 相近。它主要具备以下几个特征:



    1. 字符全部小写,多单词用连接符「-」连接,无子孙的标签直接闭合;

    2. 包含极少的具备抽象语义的标签的标签集;

    3. 以特定标签的特定属性的形式支持有限的轻逻辑。


    为什么不选择用 JSON 或 JSX 来描述和配置页面?因为模板更符合直觉,更易读,并且中立。用模板的话,一眼就能几乎不用思考地看出都有啥,以及层级关系;如果是 JSON 或 JSX,还得在脑中进行转换,增加心智负担,并且拼写起来相对复杂。Vue 上手如此「简单」的原因之一,就是它「符合直觉」的设计。


    要使用模板去描述页面的话,就得自定义一套具有抽象语义的标签集。


    页面的整体布局可以用如下模板结构去描述:


    <layout>
    <header>
    <title>欧雷流title>
    <navs />
    header>
    <layout>
    <sidebar>
    <navs />
    sidebar>
    <content>...content>
    layout>
    <footer>...footer>
    layout>

    看起来是不是跟 HTML 标签很像?但它们并不是 HTML 标签,也不会进行渲染,只是用来描述页面的一段文本。


    整体布局可以描述了,但承载整个页面的主要数据和功能的主体部分该如何去描述呢?


    在上文中提到,我们习惯将中后台系统中与数据的增删改查相对应的页面称为「列表页」、「表单页」和「详情页」。虽然它们中都带有「页」,但真正有区别的只是整个页面中的一部分区域,通常是页面主体部分。它们可以被分别看成是一种视图形式,所以可以将称呼稍微改变一下——「列表视图」、「表单视图」和「详情视图」。一般情况下,表单视图和详情视图长得基本一样,就是一个能编辑一个不能,可以将它们合称为「表单/详情视图」。


    「视图」只描述了一个数据的集合该展示成啥样,并没有也没法去描述每个数据是什么以及长啥样,需要一个更小粒度的且能够去描述每个数据单元的概念——「字段」。这样一来,用来描述数据的概念和模板标签已经齐活儿了:


    <view>
    <field name="name" label="姓名" />
    <field name="gender" label="性别" />
    <field name="age" label="年龄" />
    <field name="birthday" label="生日" />
    view>

    虽然数据能够描述了,但还有些欠缺:表单/详情视图中想将字段分组展示没法描述;对数据的操作也没有描述。为了解决这两个问题,再引入「分组」和「动作」。这下,表单/详情视图的模板看起来会是这样:


    <view>
    <group title="基本信息">
    <field name="name" label="姓名" />
    <field name="gender" label="性别" />
    <field name="age" label="年龄" />
    <field name="birthday" label="生日" />
    group>
    <group title="宠物">
    <field name="dogs" label="🐶" />
    <field name="cats" label="🐱" />
    group>
    <action ref="submit" text="提交" />
    <action ref="reset" text="重置" />
    <action ref="cancel" text="取消" />
    view>

    模板很好地解决了内容结构描述和配置的问题,但如何去动态地调整结构和更改配置呢?在平常的业务页面开发时也许不会太凸显出问题,但碰到流程表单设计或页面可视化编辑这种灵活性很高的需求时,问题就会被暴露出来了。


    充满控制力的节点


    在这里,我要将定义好的标签集所拼成的模板解析成节点树,通过更改树的结构和节点的属性去影响页面最终的呈现效果。每个节点都会有节点的基本信息、对应标签的属性和一些节点操作方法:


    {
    name: "field",
    tag: "field",
    attrs: {
    name: "name",
    label: "姓名"
    },
    parent: {},
    children: [],
    remove: function() {},
    insert: function() {}
    }

    在页面模板化且节点化之后,理想情况下,页面长啥样已经不受如 React、Vue 等运行时技术栈的束缚,控制权完全在解析模板所生成的节点树上,要想改变页面的视觉效果时只需更改节点即可。


    极具表现力的部件


    页面内容的描述通过模板来表达了,页面内容的控制权集中到节点树中了,那么页面内容的呈现在这种体系下应该如何去做呢?负责这块的,就是接下来要说的面向模板开发的第三大要素——部件。


    「部件」这个词不新鲜,但在我所说的这个面向模板开发的体系中的含义,需要被重新定义一下:「部件」是一个可复用的,显示的信息排列可由用户改变的,可以进行交互的 GUI 元素。


    在这个面向模板开发的体系中,模板和节点树完全是中立的,即不受运行时的技术栈所影响;而部件是建立在运行时技术栈的基础之上,但不必限于同一个技术栈。也就是说,可以使用 React 组件,也可以用 Vue 组件。


    每个部件在使用前都需要注册,然后在模板中通过 widget 属性引用:


    <view widget="form">
    <group title="基本信息" widget="fieldset">
    <field name="name" label="姓名" widget="input" />
    <field name="gender" label="性别" widget="radio" />
    <field name="age" label="年龄" widget="number" />
    <field name="birthday" label="生日" widget="date-picker" />
    group>
    <group title="宠物" widget="fieldset">
    <field name="dogs" label="🐶" widget="select" />
    <field name="cats" label="🐱" widget="select" />
    group>
    <action ref="submit" text="提交" widget="button" />
    <action ref="reset" text="重置" widget="button" />
    <action ref="cancel" text="取消" widget="button" />
    view>

    这样,一个面向模板开发的普通表单页出来了!


    思想总结


    面向模板的开发方式很好,能够大幅度提高业务前端开发效率,一定程度上减少了业务系统的搭建速度;作为核心的模板和节点树是保持中立的,大大降低了运行时技术栈的迁移成本,且能够应对多端等场景。


    面向模板的开发方式初期投入成本很高,标签集、模板解析和部件注册与调用机制等的设计和实现需要较多时间,并且这仅仅是视图层,逻辑层也需要做出相应的变化,不能简单地用 props 和事件绑定进行处理了。


    这个体系建成之后,在业务开发上会很简单,但机制理解上会增加部分开发人员的心智负担。


    为了效率,一家公司里的业务前端开发到最后一定是面向模板,而非面向组件。


    作者:欧雷殿
    来源:juejin.cn/post/7274430147126493199
    收起阅读 »