注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

HTML表单标签详解:如何用HTML标签打造互动网页?

在互联网的世界中,表单是用户与网站进行互动的重要桥梁。无论是注册新账号、提交反馈、还是在线购物,表单都扮演着至关重要的角色。在网页中,我们需要跟用户进行交互,收集用户资料,此时就需要用到表单标签。HTML提供了一系列的表单标签,使得开发者能够轻松地创建出功能丰...
继续阅读 »

在互联网的世界中,表单是用户与网站进行互动的重要桥梁。无论是注册新账号、提交反馈、还是在线购物,表单都扮演着至关重要的角色。在网页中,我们需要跟用户进行交互,收集用户资料,此时就需要用到表单标签。

HTML提供了一系列的表单标签,使得开发者能够轻松地创建出功能丰富的表单。今天我们就来深入探讨这些标签,了解它们的作用以及如何使用它们来构建一个有效的用户界面。

一、表单的组成

在HTML中,一个完整的表单通常由表单域、表单控件(表单元素)和提示信息三个部分构成。

表单域

  • 表单域是一个包含表单元素的区域
  • 在HTML标签中,<form>标签用于定义表单域,以实现用户信息的收集和传递
  • <form>会把它范围内的表单元素信息提交给服务器

表单控件

这些是用户与表单交云的各种元素,如<input>(用于创建不同类型的输入字段)、<textarea>(用于多行文本输入)、<button>(用于提交表单或执行其他操作)、<select><option>(用于创建下拉列表)等。

提示信息

这些信息通常通过<label>标签提供,它为表单控件提供了描述性文本,有助于提高可访问性。<label>标签通常与<input>标签一起使用,并且可以通过for属性与<input>标签的id属性关联起来。

这三个部分共同构成了一个完整的HTML表单,使得用户可以输入数据,并通过点击提交按钮将这些数据发送到Web服务器进行处理。

二、表单元素

在表单域中可以定义各种表单元素,这些表单元素就是允许用户在表单中输入或者选择的内容控件。下面就来介绍HTML中常用的表单元素。

1、<form>标签:基础容器

作用:定义一个表单区域,用户可以在其中输入数据进行提交。

<form action="submit.php" method="post">

其中action属性指定了数据提交到的服务器端脚本地址,method属性定义了数据提交的方式(通常为GET或POST)。

2、<input>标签:数据输入

<input>标签是一个单标签,用于收集用户信息。允许用户输入文本、数字、密码等。

<input type="text" name="username" placeholder="请输入用户名">

type属性决定了输入类型,name属性定义了数据的键名,placeholder属性提供了输入框内的提示文本。

<input>标签的属性

Description

下面举个例子来说明:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <form>
               用户名:<input type="text" value="请输入用户名"><br> 

               密码:<input type="password"><br>

      性别:男<input type="radio" name="sex" checked="checked"><input type="radio" name="sex"><br>

               爱好:吃饭<input type="checkbox"> 睡觉<input type="checkbox"> 打豆豆<input type="checkbox"><br>

                <input type="submit" value="免费注册">
                <input type="reset" value="重新填写">
                <input type="button" value="获取短信验证码"><br>
                上传头像:<input type="file">
    </form>
</body>
</html>

Description

3、<label>标签:关联说明

它与输入字段如文本框、单选按钮、复选框等关联起来,以改善网页的可用性和可访问性。<label>标签有两种常见的用法:

1)包裹方式:

在这种用法中,<label>标签直接包裹住关联的表单元素。例如:

<label>用户名:<input type="text" name="username"></label>

这样做的好处是用户点击标签文本时,关联的输入字段会自动获取焦点,从而提供更好的用户体验。

2)使用for属性关联:

在这种用法中,<label>标签通过for属性与目标表单元素建立关联,for属性的值应与目标元素的id属性相匹配。例如:

<label for="username">用户名:</label><input type="text" id="username" name="username">

这样做的优势是单击标签时,相关的表单元素会自动选中(获取焦点),从而提高可用性和可访问性。

4、<select>和<option>标签:下拉选择

在页面中,如果有多个选项让用户选择,并且想要节约页面空间时,我们可以使用标签控件定义下拉列表。

注意点:

  • <select>中至少包含一对<option>
  • 在<option>中定义selected=“selected”时,当前项即为默认选中项
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <form>
        籍贯:
        <select>
            <option>山东</option>
            <option>北京</option>
            <option>西安</option>
            <option selected="selected">火星</option>
        </select>
    </form>
</body>
</html>

Description

5、<textarea>标签:多行文本输入

当用户输入内容较多的情况下,我们可以用表单元素标签替代文本框标签。

  • 允许用户输入多行文本。
<textarea name="message" rows="5" cols="30">默认文本</textarea>

rows和cols属性分别定义了文本区域的行数和列数。

Description

6、<button>标签:按钮控件

创建一个可点击的按钮,通常用于提交或重置表单。它允许用户放置文本或其他内联元素(如<i><b><strong><br><img>等),这使得它比普通的具有更丰富的内容和更强的功能。

<button type="submit">提交</button>

type属性为submit时表示这是一个提交按钮。

7、<fieldset>和<legend>标签:分组和标题

通常用于在HTML表单中对相关元素进行分组,并提供一个标题来描述这个组的内容。

<fieldset>标签: 该标签用于在表单中创建一组相关的表单控件。它可以将表单元素逻辑分组,并且通常在视觉上通过围绕这些元素绘制一个边框来区分不同的组。这种分组有助于提高表单的可读性和易用性。

<legend>标签: 它总是与<fieldset>标签一起使用。<legend>标签定义了<fieldset>元素的标题,这个标题通常会出现在浏览器渲染的字段集的边框上方。<legend>标签使得用户更容易理解每个分组的目的和内容。

代码示例:

<form>
  <fieldset>
    <legend>个人信息</legend>
    <label for="name">姓名:</label>
    <input type="text" id="name" name="name"><br><br>
    <label for="email">邮箱:</label>
    <input type="email" id="email" name="email"><br><br>
  </fieldset>
  <fieldset>
    <legend>兴趣爱好</legend>
    <input type="checkbox" id="hobby1" name="hobby1" value="music">
    <label for="hobby1">音乐</label><br>
    <input type="checkbox" id="hobby2" name="hobby2" value="sports">
    <label for="hobby2">运动</label><br>
    <input type="checkbox" id="hobby3" name="hobby3" value="reading">
    <label for="hobby3">阅读</label><br>
  </fieldset>  <input type="submit" value="提交">
</form>

在这个示例中,我们使用了两个<fieldset>元素来组织表单的不同部分。第一个<fieldset>包含姓名和邮箱字段,而第二个<fieldset>包含三个复选框,用于选择用户的兴趣爱好。每个<fieldset>都有一个<legend>元素,用于提供标题。这样,用户在填写表单时可以更清晰地了解每个部分的内容。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!

8、<datalist>标签:预定义选项列表

<datalist>标签是HTML5中引入的一个新元素,它允许开发者为输入字段提供预定义的选项列表。当用户在输入字段中输入时,浏览器会显示一个下拉菜单,其中包含与用户输入匹配的预定义选项。

使用<datalist>标签可以提供更好的用户体验,因为它可以帮助用户选择正确的选项,而不必手动输入整个选项。此外,<datalist>还可以与<input>元素的list属性结合使用,以将预定义的选项列表与特定的输入字段关联起来。

下面是一个使用<datalist>标签的代码示例:

<form>
  <label for="color">选择你喜欢的颜色:</label>
  <input type="text" id="color" name="color" list="colorOptions">
  <datalist id="colorOptions">
    <option value="红色">
    <option value="蓝色">
    <option value="绿色">
    <option value="黄色">
    <option value="紫色">
  </datalist>
  <input type="submit" value="提交">
</form>

9、<output>标签:计算结果输出

<output>标签是HTML5中引入的一个新元素,它用于显示计算结果或输出。该标签通常与JavaScript代码结合使用,通过将计算结果赋值给<output>元素的value属性来显示结果。

<output>标签可以用于各种类型的计算和输出,例如数学运算、字符串处理、数组操作等。它可以与<input>元素一起使用,以实时更新计算结果。

下面是一个使用<output>标签的示例:

<form>
  <label for="num1">数字1:</label>
  <input type="number" id="num1" name="num1" oninput="calculate()"><br><br>
  <label for="num2">数字2:</label>
  <input type="number" id="num2" name="num2" oninput="calculate()"><br><br>
  <label for="result">结果:</label>
  <output id="result"></output>
</form>

<script>
function calculate() {
  var num1 = parseInt(document.getElementById("num1").value);
  var num2 = parseInt(document.getElementById("num2").value);
  var result = num1 + num2;  document.getElementById("result").value = result;
}
</script>

10、<progress>标签:任务进度展示

<progress>标签是HTML5中用于表示任务完成进度的一个新元素。它通过value属性和max属性来表示进度,其中value表示当前完成的值,而max定义任务的总量或最大值。

示例:

<!DOCTYPE html>
<html>
<head>
  <title>Progress Example</title>
</head>
<body>
  <h1>File Download</h1>
  <progress id="fileDownload" value="0" max="100"></progress>
  <br>
  <button onclick="startDownload()">Start Download</button>

  <script>
    function startDownload() {
      var progress = document.getElementById("fileDownload");
      for (var i = 0; i <= 100; i++) {
        setTimeout(function() {
          progress.value = i;
        }, i * 10);
      }
    }
  </script>
</body>
</html>

Description

在上面的示例中,我们创建了一个名为"fileDownload"的<progress>元素,并设置了初始值为0,最大值为100。我们还添加了一个按钮,当用户点击该按钮时,会触发名为"startDownload"的JavaScript函数。这个函数模拟了一个文件下载过程,通过循环逐步增加<progress>元素的value属性值,从而显示下载进度。

11、<meter>标签:度量衡指示器

<meter>标签在HTML中用于表示度量衡指示器,它定义了一个已知范围内的标量测量值或分数值,通常用于显示磁盘使用情况、查询结果的相关性等。例如:

<p>CPU 使用率: <meter value="0.6" min="0" max="1"></meter> 60%</p>
<p>内存使用率: <meter value="0.4" min="0" max="1"></meter> 40%</p>

在这个示例中,我们使用了两个<meter>标签来分别显示CPU和内存的使用率。value属性表示当前的测量值,min和max属性分别定义了测量范围的最小值和最大值。通过这些属性,<meter>标签能够清晰地显示出资源的使用情况。

需要注意的是,<meter>标签不应该用来表示进度条,对于进度条的表示,应该使用<progress>标签。

12、<details><summary>标签:详细信息展示

<details><summary>标签是HTML5中新增的两个元素,用于创建可折叠的详细信息区域。

<details>标签定义了一个可以展开或折叠的容器,其中包含一些额外的信息。它通常与<summary>标签一起使用,<summary>标签定义了<details>元素的标题,当用户点击该标题时,<details>元素的内容会展开或折叠。

示例:

<details>
  <summary>点击查看详细信息</summary>
  <p>这里是一些额外的信息,用户可以点击标题来展开或折叠这些信息。</p>
</details>

在这个示例中,我们使用了<details>标签来创建一个可折叠的容器,并在其中添加了一个<summary>标签作为标题。当用户点击这个标题时,容器的内容会展开或折叠。

总结:

HTML表单标签是构建动态网页的基石,它们使得用户能够与网站进行有效的交互。通过合理地使用这些标签,开发者可以创建出既美观又功能强大的表单,从而提升用户体验和网站的可用性。所以说,掌握这些标签的使用,对于前端开发者来说是至关重要的。

收起阅读 »

如何从Button.vue到Button.js

web
Vue的插件系统提供了一种灵活的方式来扩展Vue。Element UI作为一个基于Vue的UI组件库,其使用方式遵循Vue的插件安装模式,允许通过Vue.use()方法全局安装或按需加载组件。本文以Button组件为例,深入探讨Vue.use()方法的工作原理...
继续阅读 »

Vue的插件系统提供了一种灵活的方式来扩展Vue。Element UI作为一个基于Vue的UI组件库,其使用方式遵循Vue的插件安装模式,允许通过Vue.use()方法全局安装或按需加载组件。本文以Button组件为例,深入探讨Vue.use()方法的工作原理,以及如何借助这一机制实现Element UI组件的动态加载。


1. Vue.use()的工作原理


Vue.use(plugin)方法用于安装Vue插件。其基本工作原理如下:



  1. 参数检查Vue.use()首先检查传入的plugin是否为一个对象或函数,因为一个Vue插件可以是一个带有install方法的对象,或直接是一个函数。

  2. 安装插件:如果插件是一个对象,Vue会调用该对象的install方法,传入Vue构造函数作为参数。如果插件直接是一个函数,Vue则直接调用此函数,同样传入Vue构造函数。

  3. 避免重复安装:Vue内部维护了一个已安装插件的列表,如果一个插件已经安装过,Vue.use()会直接返回,避免重复安装。


Vue.use方法是Vue.js框架中用于安装Vue插件的一个全局方法。它提供了一种机制,允许开发者扩展Vue的功能,包括添加全局方法和实例方法、注册全局组件、通过全局混入来添加全局功能等。接下来,我们深入探讨Vue.use的工作原理。


1.1 详细步骤


Vue.use(plugin, ...options)方法接受一个插件对象或函数作为参数,并可选地接受一些额外的参数。Vue.use的基本工作流程如下:



  1. 检查插件是否已安装:Vue内部维护了一个已安装插件的列表。如果传入的插件已经在这个列表中,Vue.use将不会重复安装该插件,直接返回。

  2. 执行插件的安装方法



    • 如果插件是一个对象,Vue将调用该对象的install方法。

    • 如果插件本身是一个函数,Vue将直接调用这个函数。


    在上述两种情况中,Vue构造函数本身和Vue.use接收的任何额外参数都将传递给install方法或插件函数。



1.2 插件的install方法


插件的install方法是实现Vue插件功能的关键。这个方法接受Vue构造函数作为第一个参数,后续参数为Vue.use提供的额外参数。在install方法内部,插件开发者可以执行如下操作:



  • 注册全局组件:使用Vue.component注册全局组件,使其在任何新创建的Vue根实例的模板中可用。

  • 添加全局方法或属性:通过直接在VueVue.prototype上添加方法或属性,为Vue添加全局方法或实例方法。

  • 添加全局混入:使用Vue.mixin添加全局混入,影响每一个之后创建的Vue实例。

  • 添加Vue实例方法:通过在Vue.prototype上添加方法,使所有Vue实例都能使用这些方法。


1.3 示例代码


考虑一个简单的插件,它添加了一个全局方法和一个全局组件:


const MyPlugin = {
install(Vue, options) {
// 添加一个全局方法
Vue.myGlobalMethod = function() {
// 逻辑...
}

// 添加一个全局组件
Vue.component('my-component', {
// 组件选项...
});
}
};

// 使用Vue.use安装插件
Vue.use(MyPlugin);

Vue.use(MyPlugin)被调用时,Vue会执行MyPlugininstall方法,传入Vue构造函数作为参数。MyPlugin利用这个机会向Vue添加一个全局方法和一个全局组件。


1.4 小结


Vue.use方法是Vue插件系统的核心,它为Vue应用提供了极大的灵活性和扩展性。通过Vue.use,开发者可以轻松地将外部库集成到Vue应用中,无论是UI组件库、工具函数集合,还是提供全局功能的插件。理解Vue.use的工作原理对于有效地利用Vue生态系统中的资源以及开发自定义Vue插件都至关重要。


2. Element UI的动态加载


Element UI允许用户通过全局方式安装整个UI库,也支持按需加载单个组件以减少应用的最终打包体积。按需加载的实现,本质上是利用了Vue的插件安装机制。


以按需加载Button组件为例,步骤如下:



  1. 安装babel插件:首先需要安装babel-plugin-component,这个插件可以帮助我们在编译过程中自动将按需加载的组件代码转换为完整的导入语句。

  2. 配置.babelrc或babel.config.js:在.babelrcbabel.config.js配置文件中配置babel-plugin-component,指定需要按需加载的Element UI组件。


{
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}


  1. 在Vue项目中按需加载:在Vue文件中,可以直接导入Element UI的Button组件,并使用Vue.use()进行安装。


import Vue from 'vue';
import { Button } from 'element-ui';

Vue.use(Button);

上述代码背后的实现逻辑如下:



  • babel-plugin-component插件处理这段导入语句时,它会将按需加载的Button组件转换为完整的导入语句,并且确保相关的样式文件也被导入。

  • Button组件对象包含一个install方法。这个方法的作用是将Button组件注册到全局,使其在Vue应用中的任何位置都可使用。

  • Vue.use(Button)调用时,Vue内部会执行Buttoninstall方法,将Button组件注册到Vue中。


在Vue中,如果一个组件(如Element UI的Button组件)需要通过Vue.use()方法进行按需加载,这个组件应该提供一个install方法。这个install方法是Vue插件安装的核心,它定义了当使用Vue.use()安装插件时Vue应该如何注册这个组件。接下来,我们来探讨一个具有install方法的Button组件应该是什么样的。


3. 仿Button组件


一个设计得当的Button组件,用于按需加载时,大致应该遵循以下结构:


// Button.vue
<template>
<button class="el-button">...</button>
</template>

<script>
export default {
name: 'ElButton',
// 组件的其他选项...
};
</script>


为了使上述Button组件可以通过Vue.use(Button)方式安装,我们需要在组件外层包裹一个对象或函数,该对象或函数包含一个install方法。这个方法负责将Button组件注册为Vue的全局组件:


// index.js 或 Button.js
import Button from './Button.vue';

Button.install = function(Vue) {
Vue.component(Button.name, Button);
};

export default Button;

这里,Button.install方法接收一个Vue构造函数作为参数,并使用Vue.component方法将Button组件注册为全局组件。Button.name用作全局注册的组件名(在这个例子中是ElButton),确保了组件可以在任何Vue实例的模板中通过标签<el-button>来使用。


使用场景


当开发者在其Vue应用中想要按需加载Button组件时,可以这样实现加载:


import Vue from 'vue';
import Button from 'path-to-button/index.js'; // 或者直接指向包含`install`方法的文件

Vue.use(Button);

通过这种方式,Button组件就被注册为了全局组件,可以在任何组件的模板中直接使用,而无需在每个组件中单独导入和注册。


小结


拥有install方法的Button组件使得它可以作为一个Vue插件来按需加载。这种模式不仅优化了项目的打包体积(通过减少未使用组件的引入),还提供了更高的使用灵活性。开发者可以根据需要,选择性地加载Element UI库中的组件,而无需加载整个UI库。这种按需加载的机制,结合Vue的插件安装系统,极大地增强了Vue应用的性能和可维护性。


结尾


通过上述分析,我们可以看到,Vue.use()方法为Vue插件和组件的安装提供了一种标准化的方式。Element UI通过结合Vue的插件系统和Babel插件,实现了组件的按需加载,既方便了开发者使用,又优化了应用的打包体积。


作者:慕仲卿
来源:juejin.cn/post/7346134710132129830
收起阅读 »

FastAPI流式接口开发演示

FastAPI流式接口开发 在现代的Web开发中,实时数据传输变得越来越重要。FastAPI作为一个高性能的Web框架,提供了一种简单的方式来创建流式接口。本文将向你展示如何使用FastAPI和ReactiveX库来开发一个流式数据接口。 什么是流式接口? 流...
继续阅读 »

FastAPI流式接口开发


在现代的Web开发中,实时数据传输变得越来越重要。FastAPI作为一个高性能的Web框架,提供了一种简单的方式来创建流式接口。本文将向你展示如何使用FastAPI和ReactiveX库来开发一个流式数据接口。


什么是流式接口?


流式接口允许服务器向客户端推送实时数据。这种接口特别适用于聊天应用、股票行情更新、实时通知等场景。与传统的HTTP请求-响应模式不同,流式接口可以在一个连接上发送多个响应。


FastAPI与ReactiveX


FastAPI是一个现代、快速(高性能)的Web框架,用于构建API。ReactiveX(Rx)是一个基于观察者模式的库,用于处理异步事件和数据流。结合FastAPI和ReactiveX,我们可以创建强大的实时数据流接口。


示例代码解析


以下是使用FastAPI和ReactiveX创建流式接口的示例代码:


from fastapi import FastAPI
from starlette.responses import StreamingResponse
from rx.subjects import BehaviorSubject
from reactivex.scheduler.eventloop import AsyncIOScheduler
import asyncio

app = FastAPI()

# 创建一个 Behavior Subject 实例
global_behavior_subject = BehaviorSubject("Initial State")
loop = asyncio.get_event_loop()
scheduler = AsyncIOScheduler(loop=loop)

# 异步函数,用于订阅 Behavior Subject 并输出新的状态
async def subscribe_to_behavior_subject():
async for state in global_behavior_subject.pipe(scheduler=scheduler):
yield state

# 更新 Behavior Subject 的状态
async def update_behavior_subject(new_state):
global_behavior_subject.on_next(new_state)

@app.get("/stream")
async def stream_data():
async def event_stream():
while True:
state = global_behavior_subject.value
yield f"data: {state}\n\n"
await asyncio.sleep(1) # 每秒发送一次数据

return StreamingResponse(event_stream(), media_type="text/event-stream")


@app.get("/update/{new_state}")
async def update_data(new_state: str):
await update_behavior_subject(new_state)
return {"message": "State updated successfully"}


if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8077)

创建Behavior Subject


首先,我们创建了一个BehaviorSubject实例,它是一个RxPython中的Subject,用于存储当前状态,并允许订阅者接收状态更新。


global_behavior_subject = BehaviorSubject("Initial State")

异步订阅函数


接下来,我们定义了一个异步函数subscribe_to_behavior_subject,它使用scheduler来异步订阅BehaviorSubject的状态更新。


async def subscribe_to_behavior_subject():
async for state in global_behavior_subject.pipe(scheduler=scheduler):
yield state

更新状态函数


update_behavior_subject函数用于更新BehaviorSubject的状态,并通过异步方式执行。


async def update_behavior_subject(new_state):
global_behavior_subject.on_next(new_state)

流式数据接口


stream_data函数定义了一个无限循环的事件流,每秒从BehaviorSubject获取当前状态,并将其作为响应发送给客户端。


@app.get("/stream")
async def stream_data():
async def event_stream():
while True:
state = global_behavior_subject.value
yield f"data: {state}\n\n"
await asyncio.sleep(1) # 每秒发送一次数据

return StreamingResponse(event_stream(), media_type="text/event-stream")

更新状态接口


update_data函数提供了一个GET接口,允许客户端通过URL参数更新BehaviorSubject的状态。


@app.get("/update/{new_state}")
async def update_data(new_state: str):
await update_behavior_subject(new_state)
return {"message": "State updated successfully"}

启动服务器


最后,我们使用Uvicorn来运行FastAPI应用。


if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8077)

结论


通过本文的示例,我们学习了如何使用FastAPI和ReactiveX来创建一个流式接口。这种接口可以用于多种实时数据传输场景,提高了Web应用的交互性和用户体验。FastAPI的简洁性和ReactiveX的响应式编程模型使得这一过程变得简单而高效。


作者:大橙子打游戏
来源:juejin.cn/post/7345121873598955571
收起阅读 »

电话背调,我给他打了8分

前段时间招聘的一位开发,待了两三周,拿到了京东的offer,离职了。在离职的后一天,接到了他新公司的背调电话,几乎每项都给他打了8分。这个分数打的有点虚,单纯只是为了不影响他下家的入职。 离职之前,收到他在飞书上查看电话号码的消息,大概也猜到是在填写背调人信息...
继续阅读 »

前段时间招聘的一位开发,待了两三周,拿到了京东的offer,离职了。在离职的后一天,接到了他新公司的背调电话,几乎每项都给他打了8分。这个分数打的有点虚,单纯只是为了不影响他下家的入职。


离职之前,收到他在飞书上查看电话号码的消息,大概也猜到是在填写背调人信息,但自始至终,他也没打一声招呼,让给个好评。


离职最后一天,办完手续,没跟任何人打一个招呼,不知什么时候就消失了。


当初他刚入职一周时,其实大家都已经看出他在沟通上有很大问题,还想着如何对他有针对性的安排工作和调整,发挥他的长处,避免他的短处。但没想到这么快就离职了。在他提离职时,虽没过多挽留,但给了一些过来人的建议,很明显也听不进去。


站在旁观者的角度来看,他的职业生涯或即将面临到的事几乎能看得清清楚楚,但他有自己的坚持,别人是没办法的。


就着这事,聊聊最近对职场上关于沟通的一些思考:


第一,忌固执己见


职场中最怕遇到的一种人就是固执己见的人。大多数聪明人,在遇到固执己见的人时,基本上都会在三言两语之后停止与其争辩。因为,人一旦在自己的思维层次形成思维闭环,是很难被说服的。


而对于固执己见的人,失去的是新的思维、新的思想、纠错学习的机会,甚至是贵人的相助。试想一下,本来别人好像给你提建议,指出一条更好的路,结果换来的是争辩,是抬杠,聪明人都会敬而远之,然后默默地在旁边看着你掉坑里。


真正牛的人,基本上都是兼听则明,在获得各类信息、建议之后,综合分析,为己所用。


第二,不必说服,尊重就好


站在另外一个方面,如果一件事与己无关,别人有不同的意见,或者这事本身就是别人负责,那么尊重就好,不必强行说服对方,不必表现自己。


曾看到两个都很有想法的人,为一件事争论好几天,谁也无法说服谁。一方想用权力压另一方,另一方也不care,把简单的事情激化,急赤白脸的。


其实争论的核心只是展现形式不同而已,最终只是在争情绪、争控制感、争存在感而已,大可不必。


对于成年人,想说服谁都非常难的。而工作中的事,本身就没有对错,只有优劣,大多数时候试一下就知道了。


有句话说的非常好,“成年人的世界只做筛选,不做教育”。如果说还能做点什么,那就是潜移默化的影响别人而已。


第三,不懂的领域多听少说


如果自己对一个领域不懂,最好少发表意见,多虚心学习、请教即可。任正非辞退写《万言书》的员工的底层逻辑就是这个,不懂,不了解情况,还草率提建议,只是哗众取宠、浪费别人时间。


如果你不懂一个领域,没有丰富的背景知识和基础理论支撑,在与别人沟通的过程中,强行提建议,不仅露怯,还会惹人烦。即便是懂,也需要先听听别人的看法和视角解读。


站在另一个角度,如果一个不懂的人来挑战你的权威,质疑你的决定,笑一笑就好,不必与其争辩。


郭德纲的一段相声说的好:如果你跟火箭专家说,发射火箭得先抱一捆柴,然后用打火机把柴点着,发射火箭。如果火箭专家看你一眼,就算他输。


第四,没事多夸夸别人


在新公司,学到的最牛的一招就是夸人。之前大略知道夸人的效果,但没有太多的去实践。而在新公司,团队中的几个大佬,身体力行的在夸人。


当你完成一件事时,夸“XXX,真牛逼!”,当你解决一个问题时,夸“还得是XXX,不亏是这块的专家”。总之,每当别人有好的表现时,总是伴随着夸赞和正面响应。于是整个团队的氛围就非常好。


这事本身也不需要花费什么成本,就是随口一句话的事,而效果却非常棒。与懂得“人捧人,互相成就彼此,和气生财”的人相处,是一种非常愉悦的体验。


前两天看到一条视频,一位六七岁的小姑娘指派正在玩游戏的父亲去做饭,父亲答应了。她妈妈问:你是怎么做到的?她说:夸他呀。


看看,这么小的小孩儿都深谙的人性,我们很多成人却不懂,或不愿。曾经以为开玩笑很好,现在发现“夸”才是利器,同时一定不要开贬低性的玩笑。


其实,职场中还有很多基本的沟通规则,比如:分清无效沟通并且及时终止谈话、适当示弱、认真倾听,积极反馈、少用反问等等。


当你留意和思考这些成型的规则时,你会发现它们都是基于社会学和心理学的外在呈现。很有意思,也很有用。


作者:程序新视界
来源:juejin.cn/post/7265978883123298363
收起阅读 »

git 如何撤回已push的代码

在日常的开发中,我们经常使用Git来进行版本控制。有时候,我们可能会不小心将错误的代码 Push 到远程仓库,或者想要在本地回退到之前的某个版本重新开发。 或者像我一样,写了一些感觉以后很有用的优化方案push到线上,又接到了一个新的需求。但是呢,项目比较重要...
继续阅读 »



在日常的开发中,我们经常使用Git来进行版本控制。有时候,我们可能会不小心将错误的代码 Push 到远程仓库,或者想要在本地回退到之前的某个版本重新开发。


或者像我一样,写了一些感觉以后很有用的优化方案push到线上,又接到了一个新的需求。但是呢,项目比较重要,没有经过测试的方案不能轻易上线,为了承接需求只能先把push上去的优化方案先下掉。


现在我的分支是这样的,我想要在本地和远程仓库中都恢复到help文档提交的部分。


image.png

1.基础的手动操作(比较笨,不推荐)



这样的操作非常不推荐,但是如果你不了解git,确实是我们最容易理解的方式。



如果你的错误代码不是很多,那么你其实可以通过与你想要恢复到的commit进行对比,然后手动删除错误代码,然后删除不同的代码。


image.png

按住 ctrl 选择想要对比的两个commit,然后选择 Compare Versions 就能通过对比删除掉你想要删除的代码。



这个方案在代码很简单时时非常有效的,甚至还能通过删除后最新commit和想要退回的commit在Compare一下保障代码一致。


但是这个方法对于代码比较复杂的情况来说就不太好处理了,如果涉及到繁杂的配置文件,那更是让人头疼。只能通过反复的Compare Version来进行对比。


这样的手动操作显然显得有些笨拙了,对此git有一套较为优雅的操作流程,同样能解决这个问题。


2. git Revert Commit(推荐)


image.png

同样的,我第三次提交了错误代码,并且已经push到远程分支。想要撤回这部分代码,只需要右键点击错误提交记录


image.png

git自动产生一个Revert记录,然后我们会看到git自动将我第三次错误提交代码回退了,这个其实就相当于git帮我们手动回退了代码。


image.png

后续,只需要我们将本次改动push到远程,即可完成一次这次回退操作,


image.png

revert相当于自动帮我们进行版本回退操作,并且留下改动记录,非常安全。这也是评论区各位大佬非常推荐的。



但是revert还是存在一点不足,即一次仅能回退一次push。如果我们有几十次甚至上百次的记录,一次次的单击回退不仅费时费力而且还留下了每次的回退记录,我个人觉得revert在这种情况下又不太优雅。


3. 增加新分支(推荐撤回较多情况下使用)


如果真的需要回退到上百次提交之前的版本,我的建议是直接新建个分支。


在想要回到的版本处的提交记录右键,点击new branch


image.png
image.png
image.png

新建分支的操作仅仅增加了一个分支,既能保留原来的版本,又能安全回退到想要回退的版本,同时不会产生太多的回退记录。


但是此操作仍然建议慎用,因为这个操作执行多了,分支管理就又成了一大难题。



4. Reset Current Branch 到你想要恢复的commit记录(不太安全,慎用)


image.png


这个时候会跳出四个选项供你选择,我这里是选择hard


其他选项的含义仅供参考,因为我也没有一一尝试过。




  1. Soft:你之前写的不会改变,你之前暂存过的文件还在暂存。

  2. Mixed:你之前写的不会改变,你之前暂存过的文件不会暂存。

  3. Hard:文件恢复到所选提交状态,任何更改都会丢失。
    你已经提交了,然后你又在本地更改了,如果你选hard,那么提交的内容和你提交后又本地修改未提交的内容都会丢失。

  4. keep:任何本地更改都将丢失,文件将恢复到所选提交的状态,但本地更改将保持不变。
    你已经提交了,然后你又在本地更改了,如果你选keep,那么提交的内容会丢失,你提交后又本地修改未提交的内容不会丢失。



image.png

image.png


image.png


然后,之前错误提交的commit就在本地给干掉了。但是远程仓库中的提交还是原来的样子,你要把目前状态同步到远程仓库。也就是需要把那几个commit删除的操作push过去。


打开push界面,虽然没有commit需要提交,需要点击Force Push,强推过去。
image.png


需要注意的是对于一些被保护的分支,这个操作是不能进行的。需要自行查看配置,我这里因为不是master分支,所以没有保护。


image.png

可以看到,远程仓库中最新的commit只有我们的help文档。在其上的三个提交都没了。


image.png

注意:以上使用的是2023版IDEA,如果有出入的话可以考虑搜索使用git命令。


作者:DaveCui
来源:juejin.cn/post/7307066452290043958
收起阅读 »

faceApi-人脸识别和人脸检测

web
需求:浏览器通过模型检测前方是否有人(距离和正脸),检测到之后拍照随机保存一帧 实现步骤: 获取浏览器的摄像头权限 创建video标签并通过video标签展示摄像头影像 创建canvas标签并通过canvas标签绘制摄像头影像并展示 将canvas的当前帧转...
继续阅读 »

需求:浏览器通过模型检测前方是否有人(距离和正脸),检测到之后拍照随机保存一帧


实现步骤:



  1. 获取浏览器的摄像头权限

  2. 创建video标签并通过video标签展示摄像头影像

  3. 创建canvas标签并通过canvas标签绘制摄像头影像并展示

  4. 将canvas的当前帧转成图片展示保存


pnpm install @vladmandic/face-api 下载依赖


pnpm install @vladmandic/face-api


下载model模型


将下载的model模型放到项目的public文件中 如下图


image.png


创建video和canvas标签


      <video ref="videoRef" style="display: none"></video>
<template v-if="!picture || picture == ''">
<canvas ref="canvasRef" width="400" height="400"></canvas>
</template>
<template v-else>
<img ref="image" :src="picture" alt="" />
</template>
</div>

  width: 400px;
height: 400px;
border-radius: 50%;
overflow: hidden;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}

.video_box {
position: fixed;
width: 400px;
height: 400px;
border-radius: 50%;
overflow: hidden;
}

@keyframes moveToTopLeft {
0% {
right: 50%;
top: 50%;
transform: translate(-50%, -50%);
}

100% {
right: -68px;
top: -68px;
transform: scale(0.5);
}
}

.video_box {
animation: moveToTopLeft 2s ease forwards;
}


介绍分析

video 类选择器 让视频流居中

picture变量 判断是否转成照片

video_box视频流的某一帧转成照片后 动态移动到屏幕右上角



主要逻辑代码 主要逻辑代码 主要逻辑代码!!!


import * as faceApi from '@vladmandic/face-api'

const videoRef = ref()
const options = ref(null)
const canvasRef = ref(null)
let timeout = null
// 初始化人脸识别
const init = async () => {
await faceApi.nets.ssdMobilenetv1.loadFromUri("/models") //人脸检测
// await faceApi.nets.tinyFaceDetector.loadFromUri("/models") //人脸检测 人和摄像头距离打开
await faceApi.nets.faceLandmark68Net.loadFromUri("/models") //特征检测 人和摄像头距离必须打开
// await faceApi.nets.faceRecognitionNet.loadFromUri("/models") //识别人脸
// await faceApi.nets.faceExpressionNet.loadFromUri("/models") //识别表情,开心,沮丧,普通
// await faceApi.loadFaceLandmarkModel("/models");

options.value = new faceApi.SsdMobilenetv1Options({
minConfidence: 0.5, // 0.1 ~ 0.9
});
await cameraOptions()
}

// 打开摄像头
const cameraOptions = async() => {
let constraints = {
video: true
}
// 如果不是通过loacalhost或者通过https访问会将报错捕获并提示
try {
if (navigator.mediaDevices) {
navigator.mediaDevices.getUserMedia(constraints).then((MediaStream) => {
// 返回参数
videoRef.value.srcObject = MediaStream;
videoRef.value.play();
recognizeFace()
}).catch((error) => {
console.log(error);
});
} else {
console.log('浏览器不支持开启摄像头,请更换浏览器')
}

} catch (err) {
console.log('非https访问')
}
}

// 检测人脸
const recognizeFace = async () => {
if (videoRef.value.paused) return clearTimeout(timeout);
canvasRef.value.getContext('2d', { willReadFrequently: true }).drawImage(videoRef.value, 0, 0, 400, 400);
// 直接检测人脸 灵敏较高
// const results = await new faceApi.DetectAllFacesTask(canvasRef.value, options.value).withFaceLandmarks();
// if (results.length > 0) {
// photoShoot()
// }
// 计算人与摄像头距离和是否正脸
const results = await new faceApi.detectSingleFace(canvasRef.value, options.value).withFaceLandmarks()
if (results) {
// 计算距离
const { positions } = results.landmarks;
const leftPoint = positions[0];
const rightPoint = positions[16];
// length 可以代替距离的判断 距离越近 length值越大
const length = Math.sqrt(
Math.pow(leftPoint.x - rightPoint.x, 2) +
Math.pow(leftPoint.y - rightPoint.y, 2),
);
// 计算是否正脸
const { roll, pitch, yaw } = results.angle
//roll水平角度 pitch上下角度 yaw 扭头角度
console.log(roll, pitch, yaw, length)
if (roll >= -10 && roll <= 10 && pitch >= -10 && pitch <= 10 && yaw>= -20 && yaw <= 20 && length >= 90 && length <= 110) {

photoShoot()

}

}


timeout = setTimeout(() => {
return recognizeFace()
}, 0)
}
const picture = ref(null)
const photoShoot = () => {
// 拿到图片的base64
let canvas = canvasRef.value.toDataURL("image/png");
// 停止摄像头成像
videoRef.value.srcObject.getTracks()[0].stop()
videoRef.value.pause()
if(canvas) {
// 拍照将base64转为file流文件
let blob = dataURLtoBlob(canvas);
let file = blobToFile(blob, "imgName");
// 将blob图片转化路径图片
picture.value = window.URL.createObjectURL(file)

} else {
console.log('canvas生成失败')
}
}
/**
* 将图片转为blob格式
* dataurl 拿到的base64的数据
*/
const dataURLtoBlob = (dataurl) => {
let arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while(n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], {
type: mime
});
}
/**
* 生成文件信息
* theBlob 文件
* fileName 文件名字
*/
const blobToFile = (theBlob, fileName) => {
theBlob.lastModifiedDate = new Date().toLocaleDateString();
theBlob.name = fileName;
return theBlob;
}

// 判断是否在区间
const isInRange = (number, start, end) => {
return number >= start && number <= end
}
export { init, videoRef, canvasRef, timeout, picture }

作者:发量浓郁的程序猿
来源:juejin.cn/post/7346121373113647167
收起阅读 »

神奇!一个命令切换测试和线上环境?

大家好,我是喜欢折腾,热爱分享的“一只韩非子”。 关注微信公众号:会编程的韩非子 添加微信号:Hanfz0712 免费加入问答群/知识交流群,一起交流技术难题与未来,让我们Geek起来! 今天跟大家分享一个小Tips,让大家能够更快的切换测试和线上环境。 1...
继续阅读 »

大家好,我是喜欢折腾,热爱分享的“一只韩非子”。

关注微信公众号:会编程的韩非子

添加微信号:Hanfz0712

免费加入问答群/知识交流群,一起交流技术难题与未来,让我们Geek起来!



今天跟大家分享一个小Tips,让大家能够更快的切换测试和线上环境。


1.前因


不知道大家会不会在开发中经常遇到需要切换测试环境和线上环境。比如本地开发完成后需要部署到测试环境查看,然后就需要在我们的主机上配置测试环境的DNS,从而使得相同的域名能够从线上指向到测试环境。


image.png


image.png
我们发现,需要点啊点啊点,真的太头痛了。估计配置玩这个,代码已经忘记写到哪一行了。


下载.jpeg


所以我们有没有更简单的方式来配置DNS呢,经过小韩一顿小脑瓜的思考,想起来那我们能不能够通过命令行代码的方式来解决这个问题呢。哎,你别说还真可以。

一般系统级的配置除了可视化操作外,会有对应的命令行代码(划重点喽)


2.上命令


又是熟悉的一顿Goole后,终于让我找到了,出来吧,My Code!


# 配置DNS
# Mac
networksetup -setdnsservers Wi-Fi x.x.x.x

# Windows
# WiFi
netsh interface ip set dns name="Wi-Fi" static x.x.x.x
# 网线 具体网线连接的名称例:本地连接、以太网...
netsh interface ip set dns name="具体网线连接的名称" static x.x.x.x

哎没错,就是上面这几个。但是!但是!但是!这他喵的也真的太长了!!!我还不如点啊点啊点!好好好把我骗进来杀是吧。


下载 (1).jpeg


别急别急,知道好兄弟记不住,所以我还有一招。那就是别名

友情提示:记忆力非常不错手速又特别快的好兄弟,可以点击左侧页面第一个大拇指和第三个小星星,然后退出群聊了。


3.什么是别名?


那么什么是别名呢?,让我们Google一波,找到你了。


image.png


啊?不对不对,搜索的姿势不对,让我们换个姿势。


image.png
这下姿势就对了,我们得到了我们的答案,原来别名就是用一个简单的命令替代完整的命令,好兄弟们有福了。


4.我要用别名!


别名需要存放在我们的配置文件中,文件的地址是:

Mac:~/.zshrc~/.bashrc。可以通过命令echo $SHELL查看默认使用的是zsh还是bash,来选择对应的配置文件。

Windows:查看文末


在Mac下我们别名的语法为:
alias 别名名称='具体的命令'

名称选择一个自己喜欢的即可,但是注意不要与已经注册的别名重复了,我们可以输入alias命令查看已经注册的别名。

所以我们最终的配置为:


# 别名配置
# 配置测试环境DNS
alias dtest='networksetup -setdnsservers Wi-Fi x.x.x.x'
# 清除测试环境DNS
alias dclear='networksetup -setdnsservers Wi-Fi empty'

然后我们输入dtest就可以进入测试环境,输入dclear就可以回到线上环境了,你也可以继续配置自己的预发环境等等。这简直太妙了。


634da736e9f24u06.gif







最后的最后,留给好兄弟们一个小作业

检索一下Windows下如何配置别名:别名的家在哪里,语法是什么


images.jpeg


作者:一只韩非子
来源:juejin.cn/post/7347161048572968975
收起阅读 »

Prisma,我觉得也不是那么的香...

web
我们项目主要使用的是 mongodb ,目前使用的 ORM 框架是 TypeORM。但是 TypeORM 的更新速度和对于 MongoDB 的支持实在感觉有点难受,于是之前抽空尝试了一下火热的 Prisma,写了一套 CURD 之后感觉有些五味杂陈... Pr...
继续阅读 »

我们项目主要使用的是 mongodb ,目前使用的 ORM 框架是 TypeORM。但是 TypeORM 的更新速度和对于 MongoDB 的支持实在感觉有点难受,于是之前抽空尝试了一下火热的 Prisma,写了一套 CURD 之后感觉有些五味杂陈...


Prisma 诸如类型安全,Prisma Studio 等等新功能可以阅读社区中的文章了解,本文还是想写出一些略微不便之处。



Prisma 与 TypeORM 的对比可以参考:




仓库 issues 回应速度较慢


将这个问题写在第一是因为下面列出的所有问题我都在 issue 中查找到了社区反馈,但是始终没有官方的解决方案。issue open 时间极其长,feedback 不断叠加。


image.png


image.png


image.png


Mongodb 需要使用副本集模式


http://www.prisma.io/docs/orm/ov…



对于 Mongodb sharding 模式我没有进行实际测试,仅依靠 issue 描述进行判断,可能至当前版本时已经支持。


github.com/prisma/pris…
github.com/prisma/pris…



Prisma 如果不将 MongoDB 配置为副本集模式,在运行 prisma gengrate 时会提示


Transactions are not supported by this deployment

我在开发过程中通常是通过 docker 创建一个独立的 mongodb,在 docker mongodb 中创建副本集的命令是


docker run --name mongodb -d -p 27017:27017 mongo:latest mongod --replSet rs0
docker exec -d mongodb mongosh --eval \
"rs.initiate({_id: 'rs', members: [{_id: 0, host: 'localhost:27017'}]})"

在生产环境中,配置副本集链接字符串有两种方法:



  1. 将副本集中的所有节点都写入链接字符串中


mongodb://username:password@host1,host2,host3/mydatabase?replicaSet=myReplicaSetName

将所有节点都写入会造成链接字符串极其长,可维护性变差



  1. 使用 mongodb+srv 方式进行链接


mongodb+srv://username:password@your-cluster-hostname/databaseName?replicaSet=yourReplicaSetName

使用 DNS SRV 记录可能需要自行维护记录解析的变更,并且在记录变更后由于 DNS 解析缓存的原因也可能造成无法及时切换。


schema.prisma 文件无法拆分,所有的模型 All in one


github.com/prisma/pris…


Prisma 需要在 schema.prisma 中声明 client-provier 和 datasouce 以及各种模型,Prisma 只会读取一个单一的 xx.prisma 文件。假如项目中存在多个数据表,可以预想到文件的行数会变得极其长,在一个极其长的文件中上下滑动寻找表结构并修改的体验我觉得并不是很好。


反观 TypeORM 为单独定义 Entity 并手动导入使用,倒也就可以单独对某个表进行修改,并且在 git diff 时也很好观察。


社区也有了一些解决方法,有自行实现了 import 关键字的,也有通过编辑器插件实现注释大纲的。我最后选择了 import 方案。



  1. 编辑器插件实现注释大纲


github.com/prisma/pris…


github.com/akirarika/c…


好处是侵入性小,只需要安装一个独立的 vscode 插件即可,不影响官方的编辑器插件


image.png



  1. prisma-import


github.com/ajmnz/prism…


可以将每个 model 都拆分到不同的 .prisma 文件中,通过 import 关键字导入。目录结构可以如下所示:


prisma
├── schema.prisma
└── src
├── index.prisma
└── model
├── comment.model.prisma
├── post.model.prisma
└── user.model.prisma

schema.prismaprisma-import 最终会覆写的文件,不应在此文件中手动书写任何配置。


index.prisma 文件中包含了 client-provider、datasouce 等全局配置,以及每个 model 的 import


generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}

import { User } from "model/user.model"
import { Post } from "model/post.model"
import { Comment } from "model/comment.model"

*.model.prisma 文件中按照官方语法定义 model 即可,如果有需要进行关联的表也可以通过 import 导入。


import { Post } from "./post.model"

model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
email String @unique
name String
address Address?
posts Post[]
}

type Address {
street String
city String
state String
zip Int
}

最终通过 cli 命令进行覆写即可


prisma-import --force --schemas prisma/src/index.prisma --schemas prisma/src/model/*

覆写后的 schema.prisma 文件如下:


//
// Autogenerated by `prisma-import`
// Any modifications will be overwritten on subsequent runs.
//

//
// index.prisma
//

generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}

//
// comment.model.prisma
//

model Comment {
id String @id @default(auto()) @map("_id") @db.ObjectId
comment String
post Post @relation(fields: [postId], references: [id])
postId String @db.ObjectId
}

但这与官方的语法相悖,使用官方的编辑器插件高亮会各种飘红。prisma-import 作者只开发了 vscode 的编辑器插件,Webstorm 的体验感相对较差。并且需要禁用官方的编辑器插件以避免冲突,如果之后官方支持了新的语法或关键字可能插件不能及时支持。



  • WebStorm


image.png



  • VSCode


image.png


findAndCount 没有官方的支持,需要安装插件


github.com/prisma/pris…


可以通过安装插件或手动运行 count 查询实现相同的效果。



我翻了一下 typeorm 的源码,发现他们也是同时运行 find & count 实现的 findAndCount




  • 使用插件


github.com/deptyped/pr…


插件可以支持普通的 offset & limit 分页和游标分页。


import { pagination } from 'prisma-extension-pagination';

new PrismaClient({
datasourceUrl: env.DATABASE_URL,
log: [{ level: 'query', emit: 'event' }],
}).$extends(pagination())

paginate().withPages() 中可以解构出 meta


const [users, meta] = await prisma.user.paginate().withPages({
page,
limit: pageSize,
includePageCount: true,
});

return ({
pagination: {
page: meta.currentPage,
pageSize: ctx.request.query.pageSize,
total: meta.totalCount,
pageCount: meta.pageCount,
}
})

最后叠个甲


本文主要记录对于 Prisma 的一些不便之处以及对应的解决方法,其中有些问题只通过阅读 issue,没有实际搭建环境测试,可能存在错误。如果有更好的解决方法或文中存在错误,敬请指正,谢谢(鞠躬)


作者:北野桜桃
来源:juejin.cn/post/7345478992055173183
收起阅读 »

领导:我有个需求,你把我们项目的技术栈升级一下

web
故事的开始 在一个风和日丽的下午,我正喝着女神请的9.9咖啡,逛着掘金摸着🐟,一切的一切都是这么的美好。 霎那间,只见耳边响起了声音,”系统觉醒中,请。。“,啊?我都外挂到账了? 呸,是”帅哥,领导叫你去开会“。 ”哦“,某位帅哥站了起来,撇了撇帅气的刘海,走...
继续阅读 »

故事的开始


在一个风和日丽的下午,我正喝着女神请的9.9咖啡,逛着掘金摸着🐟,一切的一切都是这么的美好。


霎那间,只见耳边响起了声音,”系统觉醒中,请。。“,啊?我都外挂到账了?


呸,是”帅哥,领导叫你去开会“。


”哦“,某位帅哥站了起来,撇了撇帅气的刘海,走向了办公室。


胖虎00002-我真的很不错.gif


会议室情节


”咦,不是开会吗,怎么就只有领导一个人“。


昏暗的灯光,发着亮光的屏幕,在幽闭的空间里气氛显得有那么一丝的~~~暧昧,不对,是紧张。


”帅哥你来了,那我直接说事情吧“,领导说到。


突然我察觉到那么一丝不安,但是现在走好像来不及了,房门紧闭,领导又有三头六臂,凭着我这副一米八五,吴彦祖的颜值的身躯根本就逃不了。


”是这样的,上面有个新需求,我看完之后发现我们目前的项目技术包袱有点重,做起来比较麻烦,看看你做个项目技术栈升级提高的方案吧“。


听到这里我松了一口气,还好只是升级提高的方案,不是把屁股抬高的方案。


哆啦A梦00009-噢那你要试试看么.png


进入正题


分析了公司项目当前的技术栈,确实存在比较杂乱的情况,一堆的技术包袱,惨不忍睹,但还是能跑的,人和项目都能跑。



技术栈:vue2全家桶 + vuetify + 某位前辈自己开发的组件库 + 某位前辈搞的半成品bff



我用了一分钟的时间做足了思想功课,这次的升级决定采用增量升级的方案,为什么用增量升级,因为线上的项目,需求还在开发,为了稳定和尽量少的投入人力,所以采取增量升级。



这里会有很多小伙伴问,什么是增量升级啊,我会说,自己百度



又经过了一分钟的思想斗争,决定使用微前端的方案进行技术升级,框架选择阿里的qiankun



这里又会有小伙伴说,为什么用qiankun啊,我会说,下面会说



胖虎00004我大雄今天就是要刁难你胖虎.gif


微前端---qiankun


为什么使用微前端,最主要是考虑到他与技术栈无关,能够忽略掉一些历史的包袱。


为什么要用qiankun,最主要还是考虑到稳定问题,qiankun目前的社区比较大,方案也多,出了问题能找到方案,本人之前也有使用过的经验,所以这次就决定是它。



技术为业务服务,在面对技术选型的时候要考虑到现实的问题,不能一味的什么都用新的,稳是第一位



那么应该如何在老项目中使用微前端去升级,我给出了我的方案步骤



  1. 对老项目进行改造(路由,登录,菜单)

  2. 编写子应用的开发模板

  3. 逐个逐个模块进行重构


下面会和大家分析一下三个步骤的内容


66b832f0c3f84bc99896d7f5c4367021_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.webp


老项目的改造(下面大多数为代码)


第一步,我们要将老项目改造成适合被子应用随便进入的公交车。🚗


先对老项目进行分析,老项目是一个后台项目,大多数的后台项目布局都是上中左布局,头部导航栏,左边菜单栏,中间内容。那么我们只需要当用户选择的菜单属于微前端模块时,将中间内容变成微前端容器就好了。


image.png


那我们现在制作一个layout,里面的UI库我懒得换了,讲究看吧🌈


// BasicLayout.vue
<a-layout>
<a-layout-sider collapsible>
//菜单
</a-layout-sider>

<a-layout>
<a-layout-header>
//头部
</a-layout-header>
<a-layout-content>
//内容
<router-view/>
<slot></slot>
</a-layout-content>
</a-layout>

</a-layout>

然后对App.vue进行一定的修改


// App.vue
<a-config-provider locale="zh-cn">
<component v-if="layout" :is="layout">
<!-- 微前端子应用的容器,插槽紧跟着view-router -->
<div id="SubappViewportWrapper"></div>
</component>

// 初始化子应用时传入容器,这个容器不能后续修改,目前方案是将下面的容器动态append到SubappViewportWrapper
<div id="SubappViewport"></div>
</a-config-provider>

import { BasicLayout, UserLayout } from '@/layouts'
import { MICRO_APPS } from './qiankun'
import { start } from 'qiankun';
export default defineComponent({
components: {
BasicLayout,
UserLayout
},
data () {
return {
locale: zhCN,
layout: ''
}
},
methods: {
isMicroAppUrl (url) {
let result = false;
MICRO_APPS.forEach(mUrl => {
if (url.includes(mUrl)) {
result = true;
}
});
return result;
},
checkMicroApp (val) {
if (isMicroAppUrl(val.fullPath)) {
// 展示微前端容器
console.log('是微前端应用....');
document.body.classList.toggle(cName, false);
console.log(document.body.classList);
} else {
// 隐藏微前端容器
console.log('不是微前端应用');
document.body.classList.toggle(cName, true);
}
const oldLayout = this.layout;
this.layout = val.meta.layout || 'BasicLayout';
if (oldLayout !== this.layout) {
const cNode = document.getElementById('SubappViewport');
this.$nextTick(function () {
const pNode = document.getElementById('SubappViewportWrapper');
if (pNode && cNode) {
pNode.appendChild(cNode);
}
});
}
}
},
watch: {
$route (val) {
this.checkMicroApp(val);
}
},
mounted () {
start()
}
})
</script>

<style lang="less">
</style>



修改目的,判断路由中是否为微前端模块,如果是的话,就插入微前端模块容器。


然后新建一个qiankun.js文件


// qiankun.js
import { registerMicroApps, initGlobalState } from 'qiankun';
export const MICRO_APPS = ['test-app']; // 子应用列表
const MICRO_APPS_DOMAIN = '//localhost:8081'; // 子应用入口域名
const MICRO_APP_ROUTE_BASE = '/test-app'; // 子应用匹配规则

const qiankun = {
install (app) {
// 加载子应用提示
const loader = loading => console.log(`加载子应用中:${loading}`);
const registerMicroAppList = [];
MICRO_APPS.forEach(item => {
registerMicroAppList.push({
name: item,
entry: `${MICRO_APPS_DOMAIN}`,
container: '#SubappViewport',
loader,
activeRule: `${MICRO_APP_ROUTE_BASE}`
});
});
// 注册微前端应用
registerMicroApps(registerMicroAppList);

// 定义全局状态
const { onGlobalStateChange, setGlobalState } = initGlobalState({
token: '', // token
});
// 监听全局变化
onGlobalStateChange((value, prev) => {
console.log(['onGlobalStateChange - master'], value);
});
}
};
export default qiankun;

这个文件我们引入了qiankun并对使用它的API进行子应用的注册,之后直接在main.js注册,


// main.js
...
import qiankun from './qiankun'
Vue.use(qiankun)
...

然后我们只需要对路由做一点点的修改就可以用了


新建RouteView.vue页面,用于接受微前端模块的内容


//RouteView.vue
<template>
<router-view v-slot="{ Component }">
<component :is="Component" />
</router-view>

</template>

修改路由配置


//router.js
const routes = [
{
path: '/home',
name: 'Home',
meta: {},
component: () => import('@/views/Home.vue')
},
// 当是属于微前端模块的路由, 使用RouteView组件
{
path: '/test-app',
name: 'test-app',
meta: {},
component: () => import('@/RouteView.vue')
}
]

最后就新增一个名为test-app的子应用就可以了。


关于子应用的内容,这次先不说了,码字码累了,下次再说吧。


哆啦A梦00007-嘛你慢慢享受吧.png


下集预告(子应用模板编写)


你们肯定会说,不要脸,还搞下集预告。


哼,我只能说,今天周五,准备下班,不码了。🌈


胖虎00014-怎么了-我胖虎说的有毛病吗.gif


作者:小酒星小杜
来源:juejin.cn/post/7307469610423664655
收起阅读 »

H5、小程序中四个反向圆角的图片如何实现

web
H5、小程序中四个反向圆角的图片如何实现 最近我逛热风小程序(一个卖鞋的小程序)时,发现了一个奇特的图片样式。图片的四个圆角是反向的,和常规图片不一样。 思索一番后,我发现想实现这个效果,需要的 CSS 知识还挺多,于是整理这篇文章。 下面我会先介绍如何实现...
继续阅读 »

H5、小程序中四个反向圆角的图片如何实现


最近我逛热风小程序(一个卖鞋的小程序)时,发现了一个奇特的图片样式。图片的四个圆角是反向的,和常规图片不一样。


hotwind.jpg


思索一番后,我发现想实现这个效果,需要的 CSS 知识还挺多,于是整理这篇文章。


下面我会先介绍如何实现四个反向圆角的矩形,再介绍如何把特殊矩形作为遮罩、得到和热风小程序的图片效果。接着,我会给出完整的代码。最后,我会给做一个简单的总结。


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


如何实现四个反向圆角的矩形


不难想到,四个反向圆角的矩形,就是四个边角都被圆遮挡的矩形。


rect-and-circle.png


径向渐变


知道圆遮挡矩形的原理后,我们最容易想到的办法是 —— 先用 50% 的 border-radius 得到圆,再改变圆形的定位去遮挡矩形。


不过这种方法需要多个元素,并不优雅,我将介绍另一种更巧妙的办法。


我们需要先了解一个 CSS 函数 —— radial-gradientradial-gradient 中文名称是径向渐变,它可以指定渐变的中心、形状、颜色和结束位置。语法如下:


/*
* 形状 at 位置
* 渐变颜色 渐变位置
* ...
* 渐变颜色 渐变位置
*/

background-image:
radial-gradient(
circle at center,
transparent 0,
transparent 20px,
#ddd 20px
);

利用径向渐变在矩形中心画圆


光看语法比较抽象,我们用代码写一个实际的例子。如图所示,我们要在矩形中心画圆:


rect0.png


下面是关键代码:


background: radial-gradient(
circle at center,
transparent 0,
transparent 20px,
#ddd 20px
);

其中:



  • radial-gradient 函数用于创建渐变效果。

  • circle at center 指定了圆形渐变,并且渐变的中心在矩形的中心。

  • transparent 0 指定了第一个渐变颜色为透明,位置是从中心开始。

  • transparent 20px 指定了第二个渐变颜色也为透明,位置距离中心 20px。

  • #ddd 20px 指定了第三个渐变颜色为淡灰色,位置距离中心 20px,第三个渐变颜色之后的颜色都是淡灰色。


通过 radial-gradient,我们成功让矩形中心、半径为 20px 的圆变透明,超过半径 20px 的地方颜色都变为灰色,这样看起来就是矩形中心有一个圆。


矩形左上角画圆


不难想到,只要我们把圆的中心从矩形中心移动到矩形的左上角,就可以让圆挡住矩形左上角,得到一个反向圆角。


rect1.png


关键代码如下,我们可以把 circle at center 改写为 circle at left top


background: radial-gradient(
circle at left top,
transparent 0,
transparent 20px,
#ddd 20px
);

矩形四个角画圆


我们已经知道 radial-gradient 如何实现 1 个反向圆角,接下来再看如何实现 4 个反向圆角。继续之前的思路,我们很容易想到给 background 设置多个反向渐变。


多个渐变之间可以用逗号分隔、且它们会按照声明的顺序依次堆叠。于是我们会写出如下关键代码:


background: radial-gradient(
circle at left top,
transparent 0,
transparent 20px,
#ddd 20px
),
/* ... */
radial-gradient(
circle at right bottom,
transparent 0,
transparent 20px,
#ddd 20px
);

遗憾的是,上述代码运行后我们看不到四个反向圆角,而是看到一个矩形。


rect.png


这是因为四个矩形互相堆叠,反而把反向圆角给遮住了。


overlay.png


遮挡怎么解决呢?我们可以分四步来解决。


设置背景宽度和高度


我们先单独看一个径向渐变。


第一步是设置 background-size: 50% 50%;,它设置了背景图像的大小是容器宽度和高度的 50%。代码运行后效果如下:


background-size.png


可以看到左上角有反向圆角的矩形重复出现了四次。


设置不允许重复


第二步是设置 background-repeat: no-repeat;。我们需要去除第一步中出现的重复。代码运行后效果如下:


no-repeat.png


给每个径向渐变设置位置


第三步是设置不允许重复时,应该保留的背景图像位置。


第一步中左上角是反向圆角的矩形出现了四次,第二步不允许重复时默认保留了左上角的矩形。事实上我们可以选择保留四个矩形中的任何一个,比如我们可以选择保留右下角的矩形。


background: 
radial-gradient(
circle at left top,
transparent 0,
transparent 20px,
#ddd 20px
)
right bottom;

代码运行后效果如下:


no-repeat-right-bottom.png


组合前三个步骤


第四步是组合前三个步骤的语法。


看完第三步后,一个很自然的想法,就是用四个渐变分别形成四个特殊矩形,然后把四个特殊矩形分别放在左上、右上、左下和右下角,最后得到有四个反向圆角的矩形。


关键代码如下,为了区分四个径向渐变,我给左上、右上、左下、右下分别设置了红、绿、蓝、黄四种颜色:


background: radial-gradient(
circle at left top,
transparent 0,
transparent 20px,
red 20px
)
left top,
radial-gradient(
circle at right top,
transparent 0,
transparent 20px,
green 20px
)
right top,
radial-gradient(
circle at left bottom,
transparent 0,
transparent 20px,
blue 20px
)
left bottom,
radial-gradient(
circle at right bottom,
transparent 0,
transparent 20px,
yellow 20px
)
right bottom;
background-repeat: no-repeat;
background-size: 50% 50%;

代码运行效果如下:


rect2.png


不难想到,只要把红、绿、蓝、黄都换为灰色,就可以得到一个全灰、有四个反向圆角的矩形。


rect2-gray.png


把背景改写为遮罩


知道四个反向圆角的矩形如何实现后,我们可以:



  • background-size 改写为 mask-size

  • background-repeat 改写为 mask-repeat;

  • background 改写为 mask


这样就可以得到四个反向圆角的矩形遮罩。


mask: radial-gradient(
circle at left top,
transparent 0,
transparent 20px,
#ddd 20px
)
left top,
radial-gradient(
circle at right top,
transparent 0,
transparent 20px,
red 20px
)
right top,
radial-gradient(
circle at left bottom,
transparent 0,
transparent 20px,
red 20px
)
left bottom,
radial-gradient(
circle at right bottom,
transparent 0,
transparent 20px,
red 20px
)
right bottom;
mask-size: 50% 50%;
mask-repeat: no-repeat;

我们可以把四个反向圆角的矩形覆盖在一张背景图片上,就得到了和热风小程序一样的效果:


rect3.png


需要注意的是,部分手机浏览器不支持 mask 语法,所以我们有必要再设置一份 -webkit-mask-webkit-mask-size-webkit-mask-repeat


代码示例



总结


本文我们介绍了如何实现四个反向圆角的图片。


我们可以利用径向渐变,实现四个反向圆角的矩形。然后我们把这个矩形作为遮罩,覆盖在背景图片上,这样就实现了四个反向圆角的图片。




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

回顾我这三年,都是泡沫

昨天,一个在掘金认识的小伙伴,进入了美团专门做 IDE 的基建组,心底真是替他高兴,这本来就是他应得的。 刚认识的时候还是一个工作一年的小毛孩,整天逮着我问各种问题,模板引擎、Babel、Electron、Jest、Rollup… 虽然没见过面,不知道他长什么...
继续阅读 »

朋友圈


昨天,一个在掘金认识的小伙伴,进入了美团专门做 IDE 的基建组,心底真是替他高兴,这本来就是他应得的。


刚认识的时候还是一个工作一年的小毛孩,整天逮着我问各种问题,模板引擎、Babel、Electron、Jest、Rollup…


虽然没见过面,不知道他长什么样,在我脑海里,他就是两样放着光,对技术充满好奇心、自我驱动力很强小伙子。


我就知道他能成,因为我多少也是这样子的,尽管我现在有些倦怠。


后来,随着工作越来越忙,博客也停更了,我们便很少联系了。


不过,后面我招人,尤其是校招生或者初级开发,我都是按照他这个范本来的。我也时常跟别人提起,我认识北京这样一个小伙子。


也有可能我们这边庙太小了,这样的小伙伴屈指可数。


平台和好奇心一样重要


大部分人智商条件不会有太多的差距,尤其是程序员这个群体,而好奇心可以让你比别人多迈出一步,经过长时间的积累就会拉开很大的差距。


而平台可以让你保持专注,与优秀的人共事,获得更多专业的经验和知识、财富,建立自己的竞争壁垒。








回到正题。


我觉得是时候阶段性地总结和回望回顾我过去这三年,却发现大部分都是泡沫。跨端、业务、质量管理、低代码、领域驱动设计... 本文话题可能会比较杂




2020 年七月,口罩第二年。我选择了跳槽,加入了一家创业公司




跨端开发的泡沫


2020 年,微信小程序已经成为国内重要的流量入口,事实也证明,我们过去几年交付的 C 端项目几乎上都是小程序。更严谨的说,应该是微信小程序,尽管很多巨头都推出了自己的小程序平台,基本上都是陪跑的。




Taro 2.x


进来后接手的第一个项目是原生小程序迁移到 Taro。


那时候,我们的愿景是“一码多端”,期望一套程序能够跑在微信小程序、支付宝小程序等小程序平台、H5、甚至是原生 App。


那时候 Taro 还是 2.x 版本,即通过语法静态编译成各端小程序的源码。


我们迁移花了不少的时间,尽管 Taro 官方提供了自动转换的工具,但是输出的结果是不可靠的,我们仍需要进行全量的回归测试,工作量非常大。 期间我也写了一个自动化代码迁移 CLI 来处理和 Lint 各种自动迁移后的不规范代码。




重构迁移只是前戏。难的让开发者写好 Taro,更难的是写出跨端的 Taro 代码。




我总结过,为什么 Taro(2.x) 这么难用:



  • 很多初级开发者不熟悉 React。在此之前技术栈基本是 Vue

  • 熟悉 React 的却不熟悉 Taro 的各种约束。

  • 即使 Taro 宣称一码多端,你还是需要了解对应平台/端的知识。 即使是小程序端,不同平台的小程序能力和行为都有较大的区别。而 Taro 本身在跨端上并没有提供较好的约束,本身 Bug 也比较多。

  • 如果你有跨端需求,你需要熟知各端的短板,以进行权衡和取舍。强调多端的一致和统一会增加很多复杂度, 对代码的健壮性也是一个比较大的考验。

  • 我们还背着历史包袱。臃肿、不规范、难以维护、全靠猜的代码。




在跨端上,外行人眼里‘一码多端’就是写好一端,其他端不用改就可以直接运行起来,那有那么简单的事情?


每个端都有自己的长板和短板:


短板效应


我们从拆分两个维度来看各端的能力:


维度




放在一个基线上看:


对比


跨端代码写不好,我们不能把锅扔给框架,它仅仅提供了一种通用的解决方案,很多事情还是得我们自己去做。




实际上要开发跨平台的程序,最好的开发路径就是对齐最短的板,这样迁移到其他端就会从而很多,当然代价就是开发者负担会很重:


路径


为了让开发者更好的掌握 Taro, 我编写了详细的 Wiki, 阐述了 React 的各种 trickTaro 如何阉割了 ReactTaro 的原理、开发调试、跨端开发应该遵循的各种规范






Taro 3.0


我们的 Taro 项目在 2020 年底正式在生产使用,而 Taro 3.0 在 2020 年 / 7 月就正式发布了,在次年 5 月,我们决定进行升级。


技术的发展就是这么快,不到 5 个月时间,Taro 2.x 就成为了技术债。


Taro 2.x 官方基本停止了新功能的更新、bug 也不修了,最后我们不得不 Fork Taro 仓库,发布在私有 npm 镜像库中。




Taro 2.x 就是带着镣铐跳舞,实在是太痛苦,我写了一篇文档来历数了它的各种‘罪行’:



  • 2.x 太多条条框框,学习成本高

  • 这是一个假的 React

  • 编译慢

  • 调试也太反人类







Taro 3.x 使用的是动态化的架构,有很多优势:


3.x 架构 和数据流


3.x 架构 和数据流



  • 动态化的架构。给未来远程动态渲染、低代码渲染、使用不同的前端框架(支持 Vue 开发)带来了可能

  • 不同端视图渲染方式差异更小,更通用,跨端兼容性更好。

  • 2.x 有非常多的条条框框,需要遵循非常多的规范才能写出兼容多端的代码。3.x 使用标准 React 进行开发,有更好的开发体验、更低的学习成本、更灵活的代码组织。

  • 可以复用 Web 开发生态。




使用类似架构的还有 Remax、Alita、Kbone, 我之前写过一篇文章实现的细节 自己写个 React 渲染器: 以 Remax 为例(用 React 写小程序)




而 Taro 不过是新增了一个中间层:BOM/DOM,这使得 Taro 不再直接耦合 React, 可以使用任意一种视图框架开发,可以使用 Vue、preact、甚至是 jQuery, 让 Web 生态的复用成为可能。




升级 3.x 我同样通过编写自动化升级脚本的形式来进行,这里记录了整个迁移的过程。








重构了再重构


我在 2B or not 2B: 多业态下的前端大泥球 讲述过我们面临的困境。


21 年底,随着后端开启全面的 DDD 重构(推翻现有的业务,重新梳理,在 DDD 的指导下重新设计和开发),我们也对 C 端进行了大规模的重构,企图摆脱历史债务,提高后续项目的交付效率




C 端架构


上图是重构后的结果,具体过程限于篇幅就不展开了:





  • 基础库:我们将所有业务无关的代码重新进行了设计和包装。

    • 组件库:符合 UI 规范的组件库,我们在这里也进行了一些平台差异的抹平

    • api: Taro API 的二次封装,抹平一些平台差异

    • utils: 工具函数库

    • rich-html、echart:富文本、图表封装

    • router:路由导航库,类型安全、支持路由拦截、支持命名导航、简化导航方法…




  • 模块化:我们升级到 Taro 3.x 之后,代码的组织不再受限于分包和小程序的约束。我们将本来单体的小程序进行了模块的拆分,即 monorepo 化。按照业务的边界和职责拆分各种 SDK

  • 方案:一些长期积累开发痛点解决方案,比如解决分包问题的静态资源提取方案、解决页面分享的跳板页方案。

  • 规范和指导实现。指导如何开发 SDK、编写跨平台/易扩展的应用等等




巨头逐鹿的小程序平台,基本上是微信小程序一家独大


跨端框架,淘汰下来,站稳脚跟的也只有 taro 和 uniapp


时至今日,我们吹嘘许久的“一码多端”实际上并没有实现;








大而全 2B 业务的泡沫


其实比一码多端更离谱的事情是“一码多业态”。


所谓一码多业态指的是一套代码适配多个行业,我在 2B or not 2B: 多业态下的前端大泥球 中已经进行了深入的探讨。


这是我过去三年经历的最大的泡沫,又称屎山历险记。不要过度追求复用,永远不要企图做一个大而全的 2B 产品






低代码的泡沫


2021 年,低代码正火,受到的资本市场的热捧。


广义的低代码就是一个大箩筐,什么都可以往里装,比如商城装修、海报绘制、智能表格、AI 生成代码、可视化搭建、审核流程编排…


很多人都在蹭热点,只要能粘上一点边的,都会包装自己是低代码,包括我们。在对外宣称我们有低代码的时候,我们并没有实际的产品。现在 AI 热潮类似,多少声称自己有大模型的企业是在裸泳呢?




我们是 2B 赛道,前期项目交付是靠人去堆的,效率低、成本高,软件的复利几乎不存在。


低代码之风吹起,我们也期望它能破解我们面临的外包难题(我们自己都在质疑这种软件交付方式和外包到底有什么区别)。


也有可能是为了追逐资本热潮,我们也规划做自己的 PaaS、aPaaS、iPaaS… 各种 “aaS”(不是 ass)。


但是我们都没做成,规划和折腾了几个月,后面不了了之,请来的大神也送回去了。




在我看来,我们那时候可能是钱多的慌。但并没有做低代码的相关条件,缺少必要的技术积累和资源。就算缩小范围,做垂直领域的低代码,我们对领域的认知和积累还是非常匮乏。




在这期间, 我做了很多调研,也单枪匹马撸了个 “前端可视化搭建平台”:


低代码平台


由于各种原因, 这个项目停止了开发。如今社区上也有若干个优秀的开源替代物,比如阿里的低代码引擎、网易云的 Tango、华为云的 TinyEngine。如果当年坚持开发下去,说不定今天也小有成就了。




不管经过这次的折腾,我越坚信,低代码目前还不具备取代专业编程的能力。我在《前端如何破解 CRUD 的循环》也阐述过相关的观点。


大型项目的规模之大、复杂度之深、迭代的周期之长,使用低代码无疑是搬石头砸自己的脚。简单预想一下后期的重构和升级就知道了。




低代码的位置


低代码是无代码和专业编码之间的中间形态,但这个中间点并不好把握。比如,如果倾向专业编码,抽象级别很低,虽然变得更加灵活,但是却丧失了易用性,最终还是会变成专业开发者的玩具。


找对场景,它就是一把利器。不要期望它能 100% 覆盖专业编码,降低预期,覆盖 10%?20%?再到 30%? 已经是一个不错的成就。


低代码真正可以提效不仅在于它的形式(可视化),更在于它的生态。以前端界面搭建为例,背后开箱即用的组件、素材、模板、应用,才是它的快捷之道。


在我看来,低代码实际上并不是一个新技术,近年来火爆,更像是为了迎合资本的炒作而稍微具象化的概念。


而今天,真正的’降本增效‘的大刀砍下来,又有多少’降本增效‘的低代码活下来了呢?








质量管理的泡沫


2021 年四月,我开始优化前端开发质量管理,设计的开发流程如下:


流程


开发环境:



  • 即时反馈:通过 IDE 或者构建程序即时对问题进行反馈。

  • 入库前检查:这里可以对变动的源代码进行统一格式化,代码规范检查、单元测试。如果检查失败则无法提交。


集成环境:



  • 服务端检查:聪明的开发者可能绕过开发环境本地检查,在集成环境我们可以利用 Gerrit + Jenkins 来执行检查。如果验证失败,该提交会被拒绝入库。

  • CodeReview:CodeReview 是最后一道防线,主要用于验证机器无法检验的设计问题。

  • 自动化部署:只有服务端检查和 CodeReview 都通过才能提交到仓库

    • 测试环境:即时部署,关闭安全检查、开启调试方便诊断问题

    • 生产环境:授权部署




生产环境:


前端应用在客户端中运行,我们通常需要通过各种手段来监控和上报应用的状态,以便更快地定位和解决客户问题。






原则一:我认为“自动化才是秩序”:


文档通常都会被束之高阁,因此单靠文档很难形成约束力。尤其在迭代频繁、人员构造不稳定的情况。规范自动化、配合有效的管理才是行之有效的解决办法。



  • 规范自动化。能够交给机器去执行的,都应该交给机器去处理, 最大程度降低开发者的心智负担、犯错率。可以分为以下几个方面:

    • 语言层面:类型检查,比如 Typescript。严格的 Typescript 可以让开发者少犯很多错误。智能提示对开发效率也有很大提升。

    • 风格层面:统一的代码格式化风格。例如 Prettier

    • 规范层面:一些代码规范、最佳实践、反模式。可以遵循社区的流行规范, 例如 JavaScript Standard

    • 架构层面:项目的组织、设计、关联、流程。可以通过脚手架、规范文档、自定义 ESLint 规则。



  • 管理和文化: 机器还是有局限性,更深层次的检查还是需要人工进行。比如单元测试、CodeReview。这往往需要管理来驱动、团队文化来支撑。这是我们后面需要走的路。






原则二:不要造轮子


我们不打算造轮子,建立自己的代码规范。社区上有很多流行的方案,它们是集体智慧的结晶,也最能体现行业的最佳实践:


社区规范


没必要自己去定义规则,因为最终它都会被废弃,我们根本没有那么多精力去维护。






实现


企业通知 Code Review


企业通知 Code Review






我们这套代码质量管理体系,主要基于以下技术来实现:



  • Jenkins: 运行代码检查、构建、通知等任务

  • Gerrit:以 Commit 为粒度的 CodeReview 工具

  • wkfe-standard: 我们自己实现渐进式代码检查 CLI






如果你想了解这方面的细节,可以查看以下文档:





我推崇的自动化就是秩序目的就是让机器来取代人对代码进行检查。然而它只是仅仅保证底线。


人工 CodeReview 的重要性不能被忽略,毕竟很多事情机器是做不了的。


为了推行 CodeReview,我们曾自上而下推行了 CCC(简洁代码认证) 运动,开发者可以提交代码让专家团队来 Code Review,一共三轮,全部通过可以获得证书,该证书可以成为绩效和晋升的加分项;除此之外还有代码规范考试…


然而,这场运动仅仅持续了几个月,随着公司组织架构的优化、这些事情就不再被重视。


不管是多么完善的规范、工作流,人才是最重要的一环,到最后其实是人的管理






DDD / 中台的泡沫


近年来,后端微服务、中台化等概念火热,DDD 也随之而起。


DDD 搜索趋势


上图的 DDD Google 趋势图,一定程度可以反映国内 DDD 热度的现实情况:



  • 在 14 年左右,微服务的概念开始被各方关注,我们可以看到这年 DDD 的搜索热度有明显的上升趋势

  • 2015 年,马某带领阿里巴巴集团的高管,去芬兰的赫尔辛基对一家名叫 supercell 的游戏公司进行商务拜访,中台之风随着而起,接下来的一两年里,DDD 的搜索热度达到了顶峰。

  • 2021 ~ 2022 年,口罩期间,很多公司业务几乎停摆,这是一个’内修‘的好时机。很多公司在这个阶段进行了业务的 DDD 重构,比较典型的代表是去哪儿业务瘦身 42%+效率提升 50% :去哪儿网业务重构 DDD 落地实践)。




上文提到,我们在 2021 年底也进行了一次轰轰烈烈的 DDD 重构战役,完全推翻现有的项目,重新梳理业务、重新设计、重新编码。


重构需要投入了大量的资源,基本公司 1 / 3 的研发资源都在里面了,这还不包括前期 DDD 的各种预研和培训成本。


在现在看来,这些举措都是非常激进的。而价值呢?现在还不’好说‘(很难量化)






DDD 落地难


其实既然开始了 DDD 重构, 就说明我们已经知道 ’怎么做 DDD‘ 了,在重构之前,我们已经有了接近一年的各种学习和铺垫,且在部分中台项目进行了实践。


但我至今还是觉得 DDD 很难落地,且不说它有较高的学习成本,就算是已落地的项目我们都很难保证它的连续性(坚持并贯彻初衷、规范、流程),烂尾的概率比较高。


为了降低开发者对 DDD 的上手门槛,我们也进行了一些探索。






低代码 + DDD?


可视化领域建模


可视化领域建模


2022 下半年,我们开始了 ’DDD 可视化建模‘ 的探索之路,如上图所示。


这个平台的核心理念和方法论来源于我们过去几年对 DDD 的实践经验,涵盖了需求资料的管理、产品愿景的说明、统一语言、业务流程图、领域模型/查询模型/领域服务的绘制(基于 CQRS),数据建模(ER)、对象结构映射(Mapper)等多种功能,覆盖了 DDD 的整个研发流程。


同时它也是一个知识管理平台,我们希望在这里聚合业务开发所需要的各种知识,包括原始需求资料、统一语言、领域知识、领域建模的结果。让项目的二开、新团队成员可以更快地入手。


最终,建模的结果通过“代码生成器”生成代码,真正实现领域驱动设计,而设计驱动编码。


很快我们会完全开源这套工具,可以关注我的后续文章。






DDD 泡沫


即使我们有’低代码‘工具 + 代码自动生成的加持,实现了领域驱动设计、设计驱动编码,结果依旧是虎头蛇尾,阻止不了 DDD 泡沫的破裂。




我也思考了很多原因,为什么我们没有’成功‘?





  • DDD 难?学习曲线高

  • 参与的人数少,DDD 受限在后端开发圈子里面,其他角色很少参与进来,违背了 DDD 的初衷

  • 重术而轻道。DDD 涵括了战略设计和战术设计,如果战略设计是’道‘、战术设计就是’术‘,大部分开发者仅仅着眼于术,具体来说他们更关注编码,思维并没有转变,传统数据建模思维根深蒂固

  • 中台的倒台,热潮的退去


扩展阅读:







一些零碎的事


过去三年还做了不少事情,限于篇幅,就不展开了:







过去三年经历时间轴:



  • 2020 年 7 月,换了公司,开始接手真正迁移中的 Taro 项目

  • 2020 年 10 月,Taro 2.x 小程序正式上线

  • 2020 年 10 月 ~ 11 月 优化代码质量管理体系,引入开发规范、Gerrit Code Review 流程

  • 2020 年 12 月 ~ 2021 年 4 月,业务开发

  • 2021 年 1 月 博客停更

  • 2021 年 5 月 Taro 3.x 升级

  • 2021 年 7 月 ~ 10 月 前端低代码平台开发

  • 2021 年 11 月 ~ 2022 年 5 月, DDD 大规模重构,C 端项目重构、国际化改造

  • 2022 年 6 月 ~ 2022 年 11 月,B 端技术升级,涉及容器化改造、微前端升级、组件库开发等

  • 2022 年 12 月~ 2023 年 4 月,可视化 DDD 开发平台开发

  • 2023 年 5 月 ~ 至今。业务开发,重新开始博客更新








总结


贝尔实验室


我们都有美好的愿望


重构了又重构,技术的债务还是高城不下


推翻了再推翻,我们竟然是为了‘复用’?


降本增效的大刀砍来


泡沫破碎,回归到了现实


潮水退去,剩下一些裸泳的人


我又走到了人生的十字路口,继续苟着,还是换个方向?


作者:荒山
来源:juejin.cn/post/7289718324857880633
收起阅读 »

你的年终奖怎么算个税?你的算法对吗?

又到了一年一度的报税阶段。相信现在很多人都是把年终奖分出来单独计税的,但是最近在计算年终奖交税的时候,突然觉得有些怪怪的,网上一查,不止我一个人发现有问题,这里记录一下,也听听大伙有什么看法。 前置工作,我们先来看看个税是怎么计算的。 单独计算 应纳税额 = ...
继续阅读 »

又到了一年一度的报税阶段。相信现在很多人都是把年终奖分出来单独计税的,但是最近在计算年终奖交税的时候,突然觉得有些怪怪的,网上一查,不止我一个人发现有问题,这里记录一下,也听听大伙有什么看法。


前置工作,我们先来看看个税是怎么计算的。


单独计算


应纳税额 = 年终奖金额 × 适用税率 - 速算扣除数



出处:财政部税务总局公告2023年第30号,自己百度



假设我拿了 40000 年终奖,而且设置单独计算,按照 2023 年税法规定,年终奖适用的七档分阶段税率以及速算扣除数如下(税表三):



全年一次性奖金单独计税的优惠政策曾在2021年12月延续2年,原本将于2023年年底到期。23年发布的《关于延续实施全年一次性奖金个人所得税政策的公告》将该政策再延4年



image.png



来源:http://www.gerensuodeshui.cn/index_gsslb…



适用税率使用的是年终奖总额除以12,分摊到12个月里的均值:40000 / 12 = 3333.33..., 查表后显示我在第 2 档,速算扣除数是 210,税率 10%.


按照计算公式,我的应交税额是:40000 * 10% - 210 = 3790.


合并计算


综合所得应纳税额 = {累计综合所得收入(含全年一次性奖金)-累计减除费用-累计专项扣除-累计专项附加扣除-累计依法确定的其他扣除-捐赠} × 适用税率-速算扣除数


顾名思义,就是将年终奖并入工资计算,这就适合税率表一了:


image.png


比如你每月工资 10000,允许扣除的三险一金每月为 2000 元,专项附加每月扣除 2000 元。并入以后全年收入所得:10000 * 12 + 40000 = 160000,查表可得在第三档。税率 20%:(160000 - 2000×12 - 2000×12)* 20% - 16920 = 5480.


交的税明显多多了,你会想,只有傻子才会合并计算啊!


单独 or 合并 ?


但是也有例外,比如你一个工资 5000,年终奖 500000,允许扣除的三险一金每月为 2000 元,专项附加每月扣除2000元,没有其他扣除项目。则单独计税时,年终奖缴纳:500000 * 30% - 4410 = 145590,若是合并计算(全年 56w 收入,在表一的第五档)需缴纳:560000 - 2000 × 12 - 2000 × 12)× 30% - 52920 = 100680,这时候合并计算反而划算了。


大多数情况,当你工资比较多,年终奖相对较少时选择单独计算会更好;当你的年终奖在全年收入中占大头时,合并计算更有利。


临界点问题


你以为这就完了?问题才刚开始,看一下知乎有人的帖子:为什么年终奖个税会存在BUG?


讲的是在现行的税法和相关文件中,年终奖(全年一次性奖金)的个人所得税存在人为设计的「临界点问题」。这个问题新华网也证实了:新华网关于税费计算,即:高收入者的税后收入反而会比低收入者的税后收入还要低的问题


我们以单独计算为例,假设我年终奖拿了 36000,那么交税:36000 * 3% - 0 = 1080,那么我到手 34920. 假设老板发了善心,给我长了一块的工资,现在是 36001,那么查表可得,在表三中我跨档了,要交税:36001 * 10% - 210 = 3390.1, 此时我到手:32610.9,反而到手还低了!?


网上说是计算公式”弄错“了速算扣除数。速算扣除数并非税法规定,而是根据税法推算出来的一个方便计算的系数。至于速算扣除数怎么来的,可以看这篇知乎文章,写的很详细。我这里总结一下,就是速算扣除数是反推计算的:



  • 税率3%对应速算扣除数为0

  • 税率10%对应速算扣除数为3000*(10%-3%)+0=210

  • 税率20%对应速算扣除数为12000*(20%-10%)+210=1410

  • 税率35%对应速算扣除数为25000*(25%-20%)+555=2660

  • ...


按我们这个例子:


36,000元,除以12得3000.0000,对应税率3%,速算扣除数0,


税金: 1080 元;


36,001元,除以12得3000.0833,对应税率10%,速算扣除数210,


税金: 1590.1;


据那篇文章所说,问题出现在速算扣除数上,这里的速算扣除数应该用年的,而公式只用了月的


如果你还是不清楚,我们不用速算扣除数计算,我们按照税法规定的:



  • 不超过1500元的 3

  • 超过1500元至4500元的部分 10

  • 超过4500元至9000元的部分 20

  • 超过9000元至35000元的部分 25

  • ...


按照税法规定,36,000 年终奖,月平均是 3000,按照他“修订”的公式,应该缴纳:(3000 * 3% + 0 * 10%) * 12 = 1080,36,001 年终奖,月平均 3000.0833, 按照他“修订”的公式,应该缴纳:(3000 * 3% + 0.0833 * 10%) * 12 = 1080.1,等价于现行算法 (3000.0833 * 10% - 210) * 12 相对合理?


文章指出现行算法是这样的: 36001 * 10% - 210 = 3390.1。只减了一个210,就是说每个月都多算了 7%。


总结


按照推论,现有的速算扣除数 * 12 才是真正的速算扣除数。所以计算公式应该是:


应纳税额 = (年终奖金额 / 12 × 适用税率 - 速算扣除数) * 12


按照这个公式,上面36,000 年终奖,月平均是 3000,应该缴纳 (36000 / 12 * 3% - 0) * 12 = 1080,36,000 年终奖,月平均是 3000,应该缴纳 (36001 / 12 * 10% - 210) * 12 = 1080.1



依据: 财政部税务总局公告2023年第30号





所以,写到这里,我也糊涂了,年终奖交税到底要怎么算?谁能告诉我😓


作者:小肚肚肚肚肚哦
来源:juejin.cn/post/7346720393414492199
收起阅读 »

H5、小程序 Tab 如何滚动居中

web
H5、小程序 Tab 如何滚动居中 Tab 在 PC 端、移动端应用都上很常见,不过 Tab 在移动端 比 PC 端更复杂。为什么呢?移动端设备屏幕较窄,一般仅能展示 4 ~ 7 个 Item。考虑到用户体验,UI 往往要求程序员实现一个功能——点击 Item...
继续阅读 »

H5、小程序 Tab 如何滚动居中


Tab 在 PC 端、移动端应用都上很常见,不过 Tab 在移动端 比 PC 端更复杂。为什么呢?移动端设备屏幕较窄,一般仅能展示 4 ~ 7 个 Item。考虑到用户体验,UI 往往要求程序员实现一个功能——点击 Item 后,Item 滚动到屏幕中央,拼多多的 Tab 就实现了这个功能。


pdd.gif


如果你也想实现这个功能,看了这篇文章,你一定会有所收获。我会先说明 Tab 滚动的本质,分析出滚动距离的计算公式,接着给出伪代码,最后再给出 Vue、React 和微信小程序的示例代码。


Tab 滚动的本质


Tab 滚动,本质是包裹着 Item 的容器在滚动。


如下图,竖着的虚线长方形代表手机屏幕,横着的长方形代表 Tab 的容器,标着数字的小正方形代表一个个 Tab Item。


左半部分中,Tab 容器紧贴手机屏幕左侧。右半部分中,Item 4 位于屏幕中央,两部分表示 Item 4 从屏幕右边滚动到屏幕中央。


scroll-left.png


不难看出,Item 4 滚动居中,其实就是容器向左移动 distance。此时容器滚动条到容器左边缘的距离也是 distance。


换句话说,让容器向左移动 distance,Item 4 就能居中。 因此只要我们能找出计算 distance 的公式,就能控制某个 Item 居中。


计算 distance 的公式


该如何计算 distance 呢?我们看下方这张更细致的示意图。


屏幕中央有一条线,它把 Item 4 分成了左右等宽的两部分,也把手机屏幕分成了左右等宽的两部分。你可以把 Item 4 一半的宽度记为 halfItemWidth,把手机屏幕一半的宽度记为 halfScreenWidth。再把 Item 4 左侧到容器左侧的距离记为 itemOffsetLeft


calculate-scroll-left.png


不难看出,这四个值满足如下等式:


distance + halfScreenWidth = itemOffsetLeft + halfItemWidth

简单推导一下,就得到了计算 distance 的公式。


distance = itemOffsetLeft + halfItemWidth - halfScreenWidth

公式的伪代码实现


现在开始解释公式的代码实现。


先看下 itemOffsetLefthalfItemWidthhalfScreenWidth 如何获取。



  • itemOffsetLeft 是 Item 元素到容器左侧的距离,你可以用 HTMLElement.offsetLeft 作它的值。

  • halfItemWidth 是 Item 元素一半的宽度。HTMLElement.offsetWidth 是元素的整体宽度,你可以用 offsetWidth / 2 作它的值,也可以先用 Element.getBoundingClientRect() 获取一个 itemRect 对象,再用 itemRect.width / 2 作它的值。

  • halfScreenWidth 是手机屏幕一半的宽度。 window.innerWidth 是手机屏幕的整体宽度,你可以用 innerWidth / 2 作它的值。


再看下如何把 distance 设置到容器上。


在 HTML 中,我们可以使用 Element.scrollLeft 来读取和设置元素滚动条到元素左边的位置。因此,你只需要容器的 scrollLeft 赋值为 distance,就可以实现 Item 元素滚动居中。


现在给出点击 tab 的函数的伪代码:


const onClick = () => {
const itemOffsetLeft = item.offsetLeft;
const halfItemWidth = item.offsetWidth / 2;
const halfScreenWidth = window.innerWidth / 2;
tabContainer.scrollLeft = itemOffsetLeft + halfItemWidth - halfScreenWidth
}

代码示例


Vue


Tab 滚动居中 | Vue


React


Tab 滚动居中 | React


微信小程序


Tab 滚动居中 | 微信小程序


小程序的 API 和浏览器的 API 有差异。



  • itemOffsetLeft ,你需要从点击事件的 event.currentTarget 中获取。

  • halfItemWidth,你需要先用 wx.createSelectorQuery() 选取到 Item 后,从 exec() 的执行结果中获取到 Item 整体宽度,然后再除以 2。

  • halfScreenWidth,你需要先用 wx.getSystemInfoSync() 获取屏幕整体宽度,然后再除以 2。


至于把 distance 设置到容器上,微信小程序 scroll-view 组件中,有 scroll-left 这个属性,你可以把 distance 赋值给 scroll-left




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

身在职场,必须面对的两点焦虑

今天听到了堂弟考编上岸的消息,挺为他感到高兴的,因为准备了很久,也失败了多次,最终取得了胜利的果实,真的可以用苦尽甘来来形容。 对于小县城家境一般的孩子,如果不去职场历练,那么唯一的路就是就是考编,但是这条路是充满艰辛的。 去年和一30几岁的同事聊天,他说很羡...
继续阅读 »

今天听到了堂弟考编上岸的消息,挺为他感到高兴的,因为准备了很久,也失败了多次,最终取得了胜利的果实,真的可以用苦尽甘来来形容。


对于小县城家境一般的孩子,如果不去职场历练,那么唯一的路就是就是考编,但是这条路是充满艰辛的。


去年和一30几岁的同事聊天,他说很羡慕那些考上编制的,即使编制内的工作强度和职场上的工作强度不相上下,但是也是让人望尘莫及。


我思考了下,在职场中为什么感觉累?其实就是两点。



1.对未来的不确定而感动担忧。




2.对当下的不稳定性而焦虑。



对未来的不确定而感动担忧


对于大多数人而言,在职场中就是充当一颗螺丝钉,没有啥核心竞争力,工作所获得的报酬也不可观,面对职场上越来越高的要求,而自己的能力却在原地踏步,坑位越来越少,但是新人却不断在涌入。


这时候就会产生对未来的担忧,这种担忧是对未来是否能在职场里混下去,如果混不下去,那自己又能做什么?


如果现在自己尚未成家立业,那么这种担忧可能不会很强烈,但是如果已经成家,并且身上的担子也不轻,那么这种担忧就会愈加强烈。


这个担忧里面还可以剖解出两个点,职业是否有第二曲线接受现实的能力


第一,如果自己已经想好,并且规划好了路线,也有收获了,那么其实是不会有太大的担忧,充其量就是离开平台,换到一个自己有利用价值得平台,或者自己干,这就是职业得第二曲线。


比如年龄大了写不了代码了,那么是否具备做销售的能力,或者是否具有跨行业去做和现在收入不相上下的能力。


第二,是否具有接受现实的能力,比如离开当前的行业,是否愿意跨行去做其它行业并且收入会下降很多,这也和个人消费情况有关。


比如之前的一个同事,因为快40岁了,在职场上已经没有竞争力了,于是他就跨行业去做建材销售,收入肯定比之前的行业低了不少,但是我和他聊的时候,他说其实自己早就意识到这个问题,所以今天走到这一步,其实是在自己的预料里,所以是能坦然接受,不会有很大的心理负担。


那么这种情况其实也不会过于焦虑,就像我们读书的时候,有些同学每天都在玩,你问他如果考不上好的大学怎么办,他会说:我已经准备好去读专科了。那么最后去读专科已经在他的意料中,所以当下自然不会焦虑。


但是有的人又不想学,但是又想考一本,那么当下自然就会焦虑,因为自己希望变好,但是又不去行动。


对当下的不稳定性而焦虑


对于当下的不稳定在我们这个时代其实更加明显,因为到处都在裁员,找工作的人大把。


如果自己所在的公司效益不好,一段时间就会裁员一波人,而自己对于目前的市场又不太了解,那么就会焦虑。


因为今天自己没有拿大礼包,不代表明天不拿,明天不拿,不代表后天不拿,只要效益一日不变好,那么就会提心吊胆的。


所以这就是为什么那些多人击破头脑去考公务员的原因,其实就是不敢面对不稳定性,不敢面对市场的跌宕起伏,并且也意识到自己能力很平庸,没有啥竞争力。


因为就目前来说,考上编制,即使工资不如自己之前,但是他是确定性的,只要你不犯大的错误,那么呆一辈子其实是可能的。


只要你没有太大的支出,并且没有太大的消费欲望,那么其实是可以安安稳稳过日子的,这就是体制的魔力。




以上两点,基本就能概括职场中感到累的原因。


但是我们多数人是考不上编制的,你可以去报一个名试试,相信你会怀疑人生。


那么面对职场的焦虑,这是就需要去做一些实质上的行动和心态上的调整。


实质上的行动就是深耕领域,提升竞争力,或者发展第二曲线,俗话说得好,闲时养兵,战时亮剑,有些现在看似没有用的东西,实际上会在某个时间点发挥作用。


前段时间和几个朋友聊的时候,一朋友说自己还在医院上班的时候,虽然只是个小护士,但是医院里面的机器总是会坏,每次坏的时候,都要等好几天别人才来修,并且也要花费几万块钱,于是在别人修的时候自己就偷学,后面机器坏了,自己就悄悄修好,自己也不说,后面领导发现是他修的,于是对他更加器重。


那么这样的人,即使不做这一行,到了别的行业,他也能从中找到自己的竞争力,也会发光发亮,事实也是如此,他现在转行了,估计也是做得不错。


心态上的调整就是避免去想很多还没有发生的东西,即使知道这一天早晚要来,心态上也要若无其事,因为总是去想那么暂时还没有发生的事情一点卵用都没有,只会把心态搞崩。


其实如果我们仔细观察,我们会发现那些厉害的人,都比较乐观。


因为只有拥有乐观的心态,自己做事情的时候效率才会更高,才不会去想这想那,专注才是进步最好的基石,而专注的前提是乐观。



今天的分享就到这里,感谢你的观看。



作者:苏格拉的底牌
来源:juejin.cn/post/7345071716680679424
收起阅读 »

写个 Mixin 来自动维护 loading 状态吧

web
Vue 中处理页面数据有两种交互方式: 骨架屏:加载时提供骨架屏,加载失败展示错误页面和重试按钮,需要维护加载状态数据,适用于注重用户体验的精细页面 消息弹窗:加载过程中展示 loading 遮罩,失败时弹出错误消息提示,不需要维护加载状态数据,适用于后台管...
继续阅读 »

Vue 中处理页面数据有两种交互方式:



  • 骨架屏:加载时提供骨架屏,加载失败展示错误页面和重试按钮,需要维护加载状态数据,适用于注重用户体验的精细页面

  • 消息弹窗:加载过程中展示 loading 遮罩,失败时弹出错误消息提示,不需要维护加载状态数据,适用于后台管理系统等不太看重用户体验的页面,或者提交数据的场景


本文适用于骨架屏类的页面数据加载场景。


痛点描述


我们日常加载页面数据时,可能需要维护 loading 状态,就像这样:


<template>
<el-table v-loading="loading" :data="tableData"></el-table>
</template>
<script>
export default {
data() {
return {
tableData: [],
loading: false,
}
},
methods: {
async getTableData() {
this.loading = true
try {
this.tableData = await this.$http.get("/user/list");
} finally {
this.loading = false;
}
},
},
}
</script>

其实加载函数本来可以只有一行代码,但为了维护 loading 状态,让我们的加载函数变得复杂。如果还要维护成功和失败状态的话,加载函数还会变得更加复杂。


export default {
data() {
return {
tableData: [],
loading: false,
success: false,
error: false,
errmsg: "",
}
},
methods: {
async getTableData() {
this.loading = true;
this.success = false;
this.error = false;
try {
this.user = await this.$http.get("/user/list");
this.success = true;
} catch (err) {
this.error = true;
this.errmsg = err.message;
} finally {
this.loading = false;
}
},
},
}

如果页面有多个数据要加载,比如表单页面中有多个下拉列表数据,那么这些状态属性会变得特别多,代码量会激增。


export default {
data() {
return {
yearList: [],
yearListLoading: false,
yearListLoaded: false,
yearListError: false,
yearListErrmsg: "",
deptList: [],
deptListLoading: false,
deptListLoaded: false,
deptListError: false,
deptListErrmsg: "",
tableData: [],
tableDataLoading: false,
tableDataLoaded: false,
tableDataError: false,
tableDataErrmsg: ""
}
}
}

其实我们可以根据加载函数的状态来自动维护这些状态数据,这次我们要实现的目标就是自动维护这些状态数据,并将它们放到对应函数的属性上。看看这样改进后的代码:


<template>
<div v-if="tableData.success">
<!-- 显示页面内容 -->
</div>
<div v-else-if="tableData.loading">
<!-- 显示骨架屏 -->
</div>
<div v-else-if="tableData.error">
<!-- 显示失败提示 -->
</div>
</template>
<script>
export default {
data() {
return {
tableData: []
}
},
methods: {
async getTableData() {
this.tableData = await this.$http.get("/user/list");
},
}
}
</script>

加载函数变得非常纯净,data 中也不需要定义一大堆状态数据,非常舒适。


Mixin 设计


基本用法


我们需要指定一下 methods 中的哪些方法是用来加载数据的,我们只需要对这些加载数据的方法添加状态属性。根据我之前在文章《我可能发现了Vue Mixin的正确用法——动态Mixin》中的看法,可以使用函数形式的 mixin 来指定。


export default {
mixins: [asyncStatus('getTableData')],
methods: {
async getTableData() {},
},
}

指定多个方法


也可以用数组指定多个方法名。


export default {
mixins: [asyncStatus([
'getDeptList',
'getYearList',
'getTableData'
])],
}

自动扫描所有方法


如果不传参数,则通过遍历的方式,给所有组件实例方法加上状态属性。


export default {
mixins: [asyncStatus()]
}

全局注入


虽然给所有的组件实例方法加上状态属性是没必要的,但也不影响。而且这有个好处,就是可以注册全局 mixin。


Vue.mixin(asyncStatus())

默认注入的属性


我们默认注入的状态字段有4个:



  • loading 是否正在加载中

  • success 是否加载成功

  • error 是否加载失败

  • exception 加载失败时抛出的错误对象


指定注入的属性名


当然,为了避免命名冲突,可以传入第二个参数,来指定添加的状态属性名。


export default {
mixins: [asyncStatus('getTableData', {
// 注入的加载状态属性名是 isLoading
loading: 'isLoading',
// 注入的错误状态属性名是 hasError
error: 'hasError',
// 错误对象的属性名是 errorObj
exception: 'errorObj',
// 不注入 success 属性
success: false,
})]
}

随意传入参数


由于第一个参数和第二个参数的形式没有重叠,所以省略第一个参数也是可行的。


export default {
mixins: [asyncStatus({
loading: 'isLoading',
error: 'hasError',
exception: 'errorObj',
success: false,
})]
}

总结


总结一下,我们需要使用函数形式来实现 mixin,函数接收两个参数,并且两个参数都是可选的。


/**
* @/mixins/async-status.mixin.js
* 维护异步方法的执行状态,为组件中的指定方法添加如下属性:
* - loading {boolean} 是否正在执行
* - success {boolean} 是否执行成功
* - error {boolean} 是否执行失败
* - exception 方法执行失败时抛出的异常
* @param {string|string[]} [methods] 方法名,可指定多个
* @param {Alias} [alias] 为注入的属性指定属性名,或将某个属性设置成false跳过注入
*
* @typedef Alias
* @type {object}
* @prop {boolean|string} [loading=true]
* @prop {boolean|string} [success=true]
* @prop {boolean|string} [error=true]
* @prop {boolean|string} [exception=true]
*/

export function asyncStatus(methods, alias) {}

函数返回真正的 mixin 对象,为组件中的异步方法维护并注入状态属性。


Mixin 实现


注入属性的时机


实现这个 mixin 是有一定难度的,首先要找准注入属性的时机。我们希望尽可能早往方法上注入属性,至少在执行 render 函数之前,以便在加载状态变化时可以重现渲染,但又需要在组件方法初始化之后。


所以,你需要熟悉 Vue 的组件渲染流程。在 Vue2 的源码中有这样一段组件初始化代码:


Vue.prototype._init = function (options?: Object) {
// ...
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, "beforeCreate");
initInjections(vm); // resolve injections before data/props
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, "created");
// ...
};

而其中的 initState 方法的源码如下:


export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}

所以总结一下 Vue 组件的初始化流程:



  • 执行 beforeCreate

  • 挂载 props

  • 挂载 methods

  • 执行并挂载 data

  • 挂载 computed

  • 监听 watch

  • 执行 created


我们必须在 methods 初始化之后开始注入属性,否则方法还没挂载到组件实例上。可以选择的是 data 或者 created。为了尽早注入,我们应该选择在 data 中注入。


// @/mixins/async-status.mixin.js
export function asyncStatus(methods, alias) {
return {
data() {
// 在这里为方法注入状态属性
return {}
}
}
}

处理参数


由于参数的形式比较自由,我们需要处理并统一一下参数形式。我们把 methods 处理成数组形式,并取出 alias 中指定注入的状态属性名。


// @/mixins/async-status.mixin.js
export function asyncStatus(methods, alias = {}) {
// 只传入 alias 的情况
if (typeof methods === 'object' && !Array.isArray(methods)) {
alias = methods
}
// 将 methods 规范化成数组形式
if (typeof methods === 'string') {
methods = [methods]
}
if (!Array.isArray(methods)) {
// TODO: 这里应该换成遍历出的所有方法名
methods = []
}
// 获取注入的状态属性名
const getKey = (name) =>
typeof alias[name] === 'string' || alias[name] === false
? alias[name]
: name
const loadingKey = getKey('loading')
const successKey = getKey('success')
const errordKey = getKey('error')
const exceptionKey = getKey('exception')
}

遍历组件方法


没有传入 methods 的时候,需要遍历组件上定义的所有方法。办法是遍历 this.$options.methods 上的所有属性名,这样遍历出的结果会包含从 mixins 中引入的方法。


// @/mixins/async-status.mixin.js
export function asyncStatus(methods, alias = {}) {
return {
data() {
if (!Array.isArray(methods)) {
// 遍历出的所有方法名,注意这段代码需要在 data 中执行
methods = Object.keys(this.$options.methods)
}
return {}
}
}
}

维护加载状态


需要注意的是,只有响应式对象上的属性才会被监听,也就是说,只有响应式对象上的属性值变化才能引起组件的重新渲染。所以我们必须创建一个响应式对象,把加载状态维护进去。这可以通过 Vue.observable() 这个API来创建。


// @/mixins/async-status.mixin.js
export function asyncStatus(methods, alias = {}) {
return {
data() {
for (const method of methods) {
if (typeof this[method] === 'function') {
// 存储状态值
const status = Vue.observable({})
loadingKey && Vue.set(status, loadingKey, false)
successKey && Vue.set(status, successKey, false)
errorKey && Vue.set(status, errorKey, false)
exceptionKey && Vue.set(status, exceptionKey, false)
// 设置状态值
const setStatus = (key, value) => key && (status[key] = value)
}
}
return {}
}
}
}

我们把加载状态维护到 status 中。


// @/mixins/async-status.mixin.js
export function asyncStatus(methods, alias = {}) {
return {
data() {
for (const method of methods) {
if (typeof this[method] === 'function') {
const fn = this[method]
// 用于识别是否最后一次调用
let loadId = 0
// 替换原始方法
this[method] = (...args) => {
// 生成本次调用方法的标识
const currentId = ++loadId
setStatus(loadingKey, true)
setStatus(successKey, false)
setStatus(errorKey, false)
setStatus(exceptionKey, null)
try {
// 这里调用原始方法,this 为组件实例
const result = fn.call(this, ...args)
// 兼容同步和异步方法
if (result instanceof Promise) {
return result
.then((res) => {
// 最后一次加载完成时才更新状态
if (loadId === currentId) {
setStatus(loadingKey, false)
setStatus(successKey, true)
}
return res
})
.catch((err) => {
// 最后一次加载完成时才更新状态
if (loadId === currentId) {
setStatus(loadingKey, false)
setStatus(errorKey, true)
setStatus(exceptionKey, err)
}
throw err
})
}
setStatus(loadingKey, false)
setStatus(successKey, true)
return result
} catch (err) {
setStatus(loadingKey, false)
setStatus(errorKey, true)
setStatus(exceptionKey, err)
throw err
}
}
}
}
return {}
}
}
}

注入状态属性


其实需要注入的属性都在 status 中,可以把它们作为访问器属性添加到对应的方法上。


// @/mixins/async-status.mixin.js
export function asyncStatus(methods, alias = {}) {
return {
data() {
for (const method of methods) {
if (typeof this[method] === 'function') {
// 存储状态值
const status = Vue.observable({})
// 替换原始方法
this[method] = (...args) => {}
// 注入状态值到方法中
Object.keys(status).forEach((key) => {
Object.defineProperty(this[method], key, {
get() {
return status[key]
}
})
})
Object.setPrototypeOf(this[method], fn)
}
}
return {}
}
}
}

完整代码


最后整合一下完整的代码。


import Vue from 'vue'

/**
* @/mixins/async-status.mixin.js
* 维护异步方法的执行状态,为组件中的指定方法添加如下属性:
* - loading {boolean} 是否正在执行
* - success {boolean} 是否执行成功
* - error {boolean} 是否执行失败
* - exception 方法执行失败时抛出的异常
* @param {string|string[]|Alias} [methods] 方法名,可指定多个
* @param {Alias} [alias] 为注入的属性指定属性名,或将某个属性设置成false跳过注入
*
* @typedef Alias
* @type {object}
* @prop {boolean|string} [loading=true] 加载状态的属性名
* @prop {boolean|string} [success=true] 加载成功状态的属性名
* @prop {boolean|string} [error=true] 加载失败状态的属性名
* @prop {boolean|string} [exception=true] 加载失败时存储错误对象的属性名
*
* @example
* <template>
* <el-table v-loading="getTableData.loading" />
* </template>
* <script>
* export default {
* mixins: [
* asyncMethodStatus('goFetchData')
* ],
* methods: {
* async getTableData() {
* this.tableData = await this.$http.get('/user/list');
* }
* }
* }
* </script>
*/

export default function asyncMethodStatus(methods, alias = {}) {
// 规范化参数
if (typeof methods === 'object' && !Array.isArray(methods)) {
alias = methods
}
if (typeof methods === 'string') {
methods = [methods]
}
const getKey = (name) =>
typeof alias[name] === 'string' || alias[name] === false
? alias[name]
: name
const loadingKey = getKey('loading')
const successKey = getKey('success')
const errorKey = getKey('error')
const exceptionKey = getKey('exception')
return {
data() {
if (!Array.isArray(methods)) {
// 默认为所有方法注入属性
methods = Object.keys(this.$options.methods)
}
for (const method of methods) {
if (typeof this[method] === 'function') {
const fn = this[method]
let loadId = 0
const status = Vue.observable({})
loadingKey && Vue.set(status, loadingKey, false)
successKey && Vue.set(status, successKey, false)
errorKey && Vue.set(status, errorKey, false)
exceptionKey && Vue.set(status, exceptionKey, false)
const setStatus = (key, value) => key && (status[key] = value)
this[method] = (...args) => {
const currentId = ++loadId
setStatus(loadingKey, true)
setStatus(successKey, false)
setStatus(errorKey, false)
setStatus(exceptionKey, null)
try {
const result = fn.call(this, ...args)
if (result instanceof Promise) {
return result
.then((res) => {
if (loadId === currentId) {
setStatus(loadingKey, false)
setStatus(successKey, true)
}
return res
})
.catch((err) => {
if (loadId === currentId) {
setStatus(loadingKey, false)
setStatus(errorKey, true)
setStatus(exceptionKey, err)
}
throw err
})
}
setStatus(loadingKey, false)
setStatus(successKey, true)
return result
} catch (err) {
setStatus(loadingKey, false)
setStatus(errorKey, true)
setStatus(exceptionKey, err)
throw err
}
}
Object.keys(status).forEach((key) => {
Object.defineProperty(this[method], key, {
get() {
return status[key]
}
})
})
Object.setPrototypeOf(this[method], fn)
}
}
return {}
}
}
}

作者:cxy930123
来源:juejin.cn/post/7249724085147254845
收起阅读 »

移动端vh适配短屏幕手机,兼容一屏显示问题

web
rem适配 在日常的移动端开发中,设计稿一般为750物理像素,而我平时开发的时候,习惯以屏幕宽度为375,高度为724为标准( iPhone X 在微信内置浏览器的宽高) ,如下图所示: 该页面是使用 rem 进行适配,此时该图片宽度为 533px,正常我们...
继续阅读 »

rem适配


在日常的移动端开发中,设计稿一般为750物理像素,而我平时开发的时候,习惯以屏幕宽度为375,高度为724为标准( iPhone X 在微信内置浏览器的宽高) ,如下图所示:


1710350110240.png


该页面是使用 rem 进行适配,此时该图片宽度为 533px,正常我们需要设置其宽度为5.33rem ,当屏幕高度为724时,可以正常一屏显示完全。


 <body>
   <div class="content">其他内容</div>
   <img class="pic" src="./images/1.png" />
 </body>

 * {
   margin: 0;
   padding: 0;
 }
 body {
   background-color: skyblue;
 }
 .content {
   margin-top: 8rem
 }
 .pic {
  width: 5.33rem;
 }

短屏手机显示


而当我模拟短屏幕手机进行预览时,设置屏幕高度为667,此时屏幕宽度没有变化,那么根元素 htmlfont-size 也不会发生变化,那么造成的结果就是短屏幕手机上会出现滚动条,无法一屏显示。


1710350616617.png


但是需求是要求内容一屏能显示完全,此时 rem 适配已经没法做到了,在屏幕宽度不变,但是高度变化的情况下,这该怎么进行适配呢?


没错,这里我想到的是 vh 单位,不使用百分比是因为百分比适配是根据父级的宽高进行计算,而 vh 是根据整个屏幕的高度进行计算。


修改 css 如下所示:


 .content {
   margin-top: 55.249vh;
   /* margin-top: 8rem; */
 }
 .pic {
   /* width: 5.33rem; */
   width: auto;
   height: 28.66vh;
   max-height: 4.15rem;
 }

vh高度适配


利用 vh 对高度进行适配,但是这个 55.249vh28.66vh 是如何这算出来的呢?


首先我是基于 375*724 进行布局,在724的高度下,图片宽度 5.33rem,高度没设置,那就是使用了图片533px 时的高度,为 415px


1710351128534.png


724的高度下,图片高度使用了 415px,那么在屏幕上显示的应该是207.5px,那如果使用 415px 进行vh 换算,应该是 415 / (724x2) x 100,得出的结果约为28.66,这个就是对应的 vh 高度。


那么 55.249vh 同理,原来设置的 8rem,也就是相当于 800px,经过换算后得出结果。


而对图片设置 max-height 是为了不让图片一直随着高度变大得拉伸,以免造成图片变形。


此时在短屏幕手机上显示的效果如下图所示,当然 font-size 我这里没处理,有时候 font-size 也可以使用 vh 适配。


1710351669836.png


作者:一如彷徨
来源:juejin.cn/post/7345729950458724389
收起阅读 »

为什么可以通过process.env.NODE_ENV来区分环境

web
0.背景 通常我们在开发中需要区分当前代码的运行环境是dev、test、prod环境,以便我们进行相对应的项目配置,比如是否开启sourceMap,api地址切换等。而我们区分环境一般都是通过process.env.NODE_ENV,那么为什么process....
继续阅读 »

0.背景


通常我们在开发中需要区分当前代码的运行环境是dev、test、prod环境,以便我们进行相对应的项目配置,比如是否开启sourceMap,api地址切换等。而我们区分环境一般都是通过process.env.NODE_ENV,那么为什么process.env.NODE_ENV可以区分环境呢?是我们给他配置的,还是他可以自动识别呢?


1.什么是process.env.NODE_ENV


process.env属性返回一个包含用户环境信息的对象。


在node环境中,当我们打印process.env时,发现它并没有NODE_ENV这一个属性。实际上,process.env.NODE_ENV是在package.json的scripts命令中注入的,也就是NODE_ENV并不是node自带的,而是由用户定义的,至于为什么叫NODE_ENV,应该是约定成俗的吧。


2.通过package.json来设置node环境中的环境变量


如下为在package.json文件的script命令中设置一个变量NODE_ENV


{
"scripts": {
"dev": "NODE_ENV=development webpack --config webpack.dev.config.js"
}
}

执行对应的webpack.config.js文件


// webpack.config.js
console.log("【process.env】", process.env.AAA);

但是在index.jsx中也就是浏览器环境下的文件中打印process.env就会报错,如下:
image.png
可以看到NODE_ENV被赋值为development,当执行npm run dev时,我们就可以在 webpack.dev.config.js脚本中以及它所引入的脚本中访问到process.env.NODE_ENV,而无法在其它脚本中访问。原因就是前文提到的peocess.env是Node环境的属性,浏览器环境中index.js文件不能够获取到。


3.使用webpack.DefinePlugin插件在业务代码中注入环境变量


这个时候我们就存在一个解决方法,通过webpack中的DefinePlugin来设置一个全局变量,这样所有的打包的js文件都可以访问到这个全局变量了。


const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"development"'
})
]
}



使用DefinePlugin注意点
webpack.definePlugins本质上是打包过程中的字符串替换,比如我们刚才定义的__WEBPACK__ENV:JSON.stringify('packages')
在打包过程中,如果我们代码中使用到了__WEPBACK__ENVwebpack会将它的值替换成为对应definePlugins中定义的值,本质上就是匹配字符串替换,并不是传统意义上的环境变量process注入。

如下图所示:
image.png
由上图可知:仔细对比这两段代码第一个问题的答案其实已经很明了了,针对definePlugin这个插件我们使用它定义key:value全局变量时,他会将value进行会直接替换文本。所以我们通常使用JSON.stringify('pacakges')或者"'packages'"



作者:会飞的特洛伊
来源:juejin.cn/post/7345760019319390248
收起阅读 »

HTML常用基础标签:图片与超链接标签全解!

HTML图片标签和超链接标签是网页开发中常用的两种标签,它们分别用于在网页中插入图片和创建超链接。我们每天都在互联网世界中与各种形式的信息打交道。你是否好奇过,当你点击一篇文章中的图片或链接时,是什么神奇的力量让你瞬间跳转到另一个页面?今天,就让我们一起揭开H...
继续阅读 »

HTML图片标签和超链接标签是网页开发中常用的两种标签,它们分别用于在网页中插入图片和创建超链接。

我们每天都在互联网世界中与各种形式的信息打交道。你是否好奇过,当你点击一篇文章中的图片或链接时,是什么神奇的力量让你瞬间跳转到另一个页面?

今天,就让我们一起揭开HTML图片标签和超链接标签的神秘面纱。

一、HTML图片标签

HTML图片标签是一种特殊的标记,它可以让网页显示图像。通过使用图片标签,我们可以在网页上展示各种图片,从而让网页更加生动有趣。

Description

1、语法结构

HTML图片标签的语法结构非常简单,只需要使用标签,并在其中添加src属性,指定图片的路径即可。例如:

<img src="image.jpg" alt="描述图片的文字">

2、图片格式

HTML支持多种图片格式,包括JPEG、PNG、GIF等。不同的图片格式具有不同的特点,可以根据需要选择合适的格式。

3、图片属性

除了src属性外,HTML图片标签还有其他一些常用的属性,如:

  • alt属性用于描述图片的内容,当图片无法显示时,会显示该属性的值;
  • width和height属性用于设置图片的宽度和高度;
  • title属性用于设置鼠标悬停在图片上时显示的提示信息。

4、网络图片的插入

当需要插入网络上的图片时,可以将图片的URL地址作为src属性的值。例如:

<img src="https://www.example.com/images/pic.jpg" alt="示例图片">

5、本地图片的插入

当需要插入本地图片时,可以将图片的相对路径或绝对路径作为src属性的值。

6、相对路径与绝对路径

在这里再给大家介绍两个概念,相对路径与绝对路径,搞懂它们,我们在插入本地图片时也能得心应手。

Description

相对路径:
相对于当前HTML文件所在目录的路径,包含Web的相对路径(HTML中的相对目录)。例如,如果图片文件位于与HTML文件相同的目录中,可以直接使用文件名作为路径:

<img src="pic.jpg" alt="本地图片">

绝对路径:
图片文件在计算机上的完整路径(URL和物理路径)。例如:

<img src="C:/Users/username/Pictures/pic.jpg" alt="本地图片">

二、HTML超链接标签

超链接标签是HTML中另一个重要的元素,它可以实现网页之间的跳转。通过使用超链接标签,我们可以将文本、图片等内容设置为可点击的链接,方便用户在不同页面之间自由切换。

Description

1、语法结构

超链接标签使用<a>标签表示,需要在href属性中指定链接的目标地址。

<a href="目标地址" title="标题">文本内容</a>

例如:

<a href="https://www.ydcode.cn/">点击访问示例网站</a>

示例:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>示例网站</title>
</head>
<body>
    <h1>欢迎来到云端源想!</h1>
    <p>这是一个简单的HTML页面,用于展示一个网站的结构和内容。</p>
    <a href="https://www.ydcode.cn/">点击访问示例网站</a>
</body>
</html>

Description

2、链接目标

超链接可以链接到不同的目标,包括其他网页、电子邮件地址、文件下载等。通过设置href属性的值,可以实现不同的链接目标。

3、链接属性

超链接标签还有一些其他常用的属性,如:

  • target属性用于设置链接打开的方式,可以选择在新窗口或当前窗口打开链接;
  • title属性用于设置鼠标悬停在链接上时显示的提示信息;
  • rel属性用于设置链接的关系,例如设置nofollow值可以告诉搜索引擎不要跟踪该链接。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!

4、锚点链接标签

锚点标签用于在网页中创建一个可以点击的锚点,以便用户可以通过点击锚点跳转到页面中的其他部分。如下图中电子书的章节切换。

Description

锚点标签的语法为:

<a name="锚点名称"></a>

例如,可以在页面中的一个段落前添加一个锚点:

<a name="section1"></a>
<p>这是一个段落。</p>

然后,可以在页面的其他位置创建一个指向该锚点的超链接:

<a href="#section1">跳转到第一节</a>

当用户点击“跳转到第一节”链接时,页面将滚动到名为“section1”的锚点所在的位置。

示例:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>示例网站</title>
</head>
<body>
    <h1>欢迎来到云端源想!</h1>
    <p><a href="#section1">跳转到第一节</a></p>
    <p>这是一个段落。</p>
    <p>这是另一个段落。</p>
    <p>这是第三个段落。</p>
    <a name="section1"></a>
    <p>这是第一节的内容。</p>
</body>
</html>

Description

三、总结

HTML图片标签和超链接标签是构建网页的两个重要元素,它们不仅丰富了网页的内容,还为网页添加了动态和互动性。

通过学习和掌握这两个标签的使用方法,我们可以创建更加丰富和互动的网页,为用户提供更好的浏览体验。无论是展示精美的图片,还是实现页面之间的跳转,HTML图片标签和超链接标签都能帮助我们实现更多的创意和功能。

让我们一起探索HTML的奇妙世界,创造出更加精彩的网页吧!

收起阅读 »

UNIAPP开发电视app教程

目前开发安卓TV的方法相对开说是十分的少的,使用uniapp开发相对来说几乎是没有的,因此,写下这篇文章供大家参考。 开发难点 如何方便的开发调试 如何使需要被聚焦的元素获取聚焦状态 如何使被聚焦的元素滚动到视图中心位置 如何在切换路由时,缓存聚焦的状态 如...
继续阅读 »

目前开发安卓TV的方法相对开说是十分的少的,使用uniapp开发相对来说几乎是没有的,因此,写下这篇文章供大家参考。


开发难点



  1. 如何方便的开发调试

  2. 如何使需要被聚焦的元素获取聚焦状态

  3. 如何使被聚焦的元素滚动到视图中心位置

  4. 如何在切换路由时,缓存聚焦的状态

  5. 如何启用wgt和apk两种方式的升级


一、如何方便的开发调试


之前我在论坛看到人家说,没办法呀,电脑搬到电视,然后调试。


其实大可不必,安装android studio里边创建一个模拟器就可以了。


注意:最好安装和电视系统相同的版本号,我这里是长虹电视,安卓9所以使用安卓9的sdk


二、如何使需要被聚焦的元素获取聚焦状态


uniapp的本质上是webview, 因此我们可以在它的元素上添加tabIndex, 就可以获取焦点了。


  <view class="card" tabindex="0">
<image :src="`${VITE_URL}${props.image}`" fade-show lazy-load mode="aspectFill"></image>
<view class="bottom">
<text class="name">{{ props.name }}</text> <text class="remark">{{ props.remark }}</text>
<div class="footer">
<view class="tags">
<text class="tag" v-for="tag in tags" :key="tag">{{ tag }}</text>
</view>
<text class="price">&yen; {{ props.price }}</text>
</div>
</view>
</view>


.card {
border-radius: 1.25vw;
overflow: hidden;
}
.card:focus {
box-shadow: 0 0 0 0.3vw #fff, 0 0 1vw 0.3vw #333;
outline: none;
transform: scale(1.03);
transition: box-shadow 0.3s ease, transform 0.3s ease;
}


三、如何使被聚焦的元素滚动到视图中心位置


使用renderjs进行实现如下


<script  module="homePage" lang="renderjs">
export default {
mounted() {
let isScrolling = false; // 添加一个标志位,表示是否正在滚动
document.body.addEventListener('focusin', e => {
if (!isScrolling) {
// 检查是否正在滚动
isScrolling = true; // 设置滚动标志为true
requestAnimationFrame(() => {
// @ts-ignore
e.target.scrollIntoView({
behavior: 'smooth', // @ts-ignore
block: e.target.dataset.index ? 'end' : 'center'
});
isScrolling = false; // 在滚动完成后设置滚动标志为false
});
}
});
}
};
</script>

就可以使被聚焦元素滚动到视图中心,requestAnimationFrame的作用是缓存


四、如何在切换路由时,缓存聚焦的状态


通过设置tabindex属性为0和1,会有不同的效果:



  1. tabindex="0":将元素设为可聚焦,并按照其在文档中的位置来确定焦点顺序。当使用Tab键进行键盘导航时,tabindex="0"的元素会按照它们在源代码中的顺序获取焦点。这可以用于将某些非交互性元素(如
    等)设为可聚焦元素,使其能够被键盘导航。

  2. tabindex="1":将元素设为可聚焦,并将其置于默认的焦点顺序之前。当使用Tab键进行键盘导航时,tabindex="1"的元素会在默认的焦点顺序之前获取焦点。这通常用于重置焦点顺序,或者将某些特定的元素(如重要的输入字段或操作按钮)置于首位。


需要注意的是,如果给多个元素都设置了tabindex属性,那么它们的焦点顺序将取决于它们的tabindex值,数值越小的元素将优先获取焦点。如果多个元素具有相同的tabindex值,则它们将按照它们在文档中的位置来确定焦点顺序。同时,负数的tabindex值也是有效的,它们将优先于零和正数值获取焦点。


我们要安装缓存插件,如pinia或vuex,需要缓存的页面单独配置


import { defineStore } from 'pinia';
export const useGlobalStore = defineStore('global', {
state: () => ({ home_active_tag: 'active0', hot_active_tag: 'hot0', dish_active_tag: 'dish0' })
});


更新一下业务代码


组件区域
<view class="card" :tabindex="home_active_tag === 'packagecard' + props.id ? 1 : 0">
<image :src="`${VITE_URL}${props.image}`" fade-show lazy-load mode="aspectFill"></image>
<view class="bottom">
<text class="name">{{ props.name }}</text> <text class="remark">{{ props.remark }}</text>
<div class="footer">
<view class="tags">
<text class="tag" v-for="tag in tags" :key="tag">{{ tag }}</text>
</view>
<text class="price">&yen; {{ props.price }}</text>
</div>
</view>

</view>

const { home_active_tag } = storeToRefs(useGlobalStore());

页面区域

<view class="content">
<FoodCard
v-for="_package in list.dishes"
@click="goShopByFood(_package)"
:id="_package.id"
:name="_package.name"
:image="_package.image"
:tags="_package.tags"
:price="_package.price"
:shop_name="_package.shop_name"
:shop_id="_package.shop_id"
:key="_package.id"
></FoodCard>
<image
class="card"
@click="goMore"
:tabindex="home_active_tag === 'more' ? 1 : 0"
style="width: 29.375vw; height: 25.9375vw"
src="/static/home/more.png"
mode="aspectFill"
/>
</view>

const goShopByFood = async (row: Record<string, any>) => {
useGlobalStore().home_active_tag = 'foodcard' + row.id;
uni.navigateTo({
url: `/pages/shop/index?shop_id=${row.shop_id}`,
animationDuration: 500,
animationType: 'zoom-fade-out'
});
};


如果,要设置启动默认焦点 id和index可默认设置,推荐启动第一个焦点组用index,它可以确定


  <view class="active">
<image
v-for="(active, i) in list.active"
:key="active.id"
@click="goActive(active, i)"
:tabindex="home_active_tag === 'active' + i ? 1 : 0"
:src="`${VITE_URL}${active.image}`"
data-index="0"
fade-show
lazy-load
mode="aspectFill"
class="card"
></image>
</view>

import { defineStore } from 'pinia';
export const useGlobalStore = defineStore('global', {
state: () => ({
home_active_tag: 'active0', //默认选择
hot_active_tag: 'hot0',
dish_active_tag: 'dish0'
})
});


对于多层级的,要注意销毁,在前往之前设置默认焦点


const goHot = (index: number) => {
useGlobalStore().home_active_tag = 'hotcard' + index;
useGlobalStore().hot_active_tag = 'hot0';
uni.navigateTo({ url: `/pages/hot/index?index=${index}`, animationDuration: 500, animationType: 'zoom-fade-out' });
};


五、如何启用wgt和apk两种方式的升级


pages.json


{
"path": "components/update/index",
"style": {
"disableScroll": true,
"backgroundColor": "#0068d0",
"app-plus": {
"backgroundColorTop": "transparent",
"background": "transparent",
"titleNView": false,
"scrollIndicator": false,
"popGesture": "none",
"animationType": "fade-in",
"animationDuration": 200
}
}
}


组件


<template>
<view class="update">
<view class="content">
<view class="content-top">
<text class="content-top-text">发现版本</text>
<image class="content-top" style="top: 0" width="100%" height="100%" src="@/static/bg_top.png"> </image>
</view>
<text class="message"> {{ message }} </text>
<view class="progress-box">
<progress
class="progress"
border-radius="35"
:percent="progress.progress"
activeColor="#3DA7FF"
show-info
stroke-width="10"
/>

<view class="progress-text">
<text>安装包正在下载,请稍后,系统会自动重启</text>
<text>{{ progress.totalBytesWritten }}MB/{{ progress.totalBytesExpectedToWrite }}MB</text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { onLoad } from '@dcloudio/uni-app';
import { reactive, ref } from 'vue';
const message = ref('');
const progress = reactive({ progress: 0, totalBytesExpectedToWrite: '0', totalBytesWritten: '0' });
onLoad((query: any) => {
message.value = query.content;
const downloadTask = uni.downloadFile({
url: `${import.meta.env.VITE_URL}/${query.url}`,
success(downloadResult) {
plus.runtime.install(
downloadResult.tempFilePath,
{ force: false },
() => {
plus.runtime.restart();
},
e => {}
);
}
});
downloadTask.onProgressUpdate(res => {
progress.progress = res.progress;
progress.totalBytesExpectedToWrite = (res.totalBytesExpectedToWrite / Math.pow(1024, 2)).toFixed(2);
progress.totalBytesWritten = (res.totalBytesWritten / Math.pow(1024, 2)).toFixed(2);
});
});
</script>
<style lang="less">
page {
background: transparent;
.update {
/* #ifndef APP-NVUE */
display: flex; /* #endif */
justify-content: center;
align-items: center;
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.65);
.content {
position: relative;
top: 0;
width: 50vw;
height: 50vh;
background-color: #fff;
box-sizing: border-box;
padding: 0 50rpx;
font-family: Source Han Sans CN;
border-radius: 2vw;
.content-top {
position: absolute;
top: -5vw;
left: 0;
image {
width: 50vw;
height: 30vh;
}
.content-top-text {
width: 50vw;
top: 6.6vw;
left: 3vw;
font-size: 3.8vw;
font-weight: bold;
color: #f8f8fa;
position: absolute;
z-index: 1;
}
}
}
.message {
position: absolute;
top: 15vw;
font-size: 2.5vw;
}
.progress-box {
position: absolute;
width: 45vw;
top: 20vw;
.progress {
width: 90%;
border-radius: 35px;
}
.progress-text {
margin-top: 1vw;
font-size: 1.5vw;
}
}
}
}
</style>


App.vue


import { onLaunch } from '@dcloudio/uni-app';
import { useRequest } from './hooks/useRequest';
import dayjs from 'dayjs'; onLaunch(() => {
// #ifdef APP-PLUS
plus.runtime.getProperty('', async app => { const res: any = await useRequest('GET', '/api/tv/app'); if (res.code === 2000 && res.row.version > (app.version as string)) { uni.navigateTo({ url: `/components/update/index?url=${res.row.url}&type=${res.row.type}&content=${res.row.content}`, fail: err => { console.error('更新弹框跳转失败', err); } }); } });
// #endif
});

如果要获取启动参数


plus.android.importClass('android.content.Intent');
const MainActivity = plus.android.runtimeMainActivity();
const Intent = MainActivity.getIntent();
const roomCode = Intent.getStringExtra('roomCode');
if (roomCode) {
uni.setStorageSync('roomCode', roomCode);
} else if (!uni.getStorageSync('roomCode') && !roomCode) {
uni.setStorageSync('roomCode', '8888');
}

作者:Rjl_CLI
来源:juejin.cn/post/7272348543625445437
收起阅读 »

有效封装WebSocket,让你的代码更简洁!

web
前言 在现代 Web 应用中,实时通信已经成为越来越重要的一部分。而 WebSocket 技术的出现,使得实时通信变得更加高效和便捷。 WebSocket 协议是一种基于 TCP 协议的双向通信协议,它能够在客户端和服务器之间建立起持久性的连接,从而实现实时通...
继续阅读 »

前言


在现代 Web 应用中,实时通信已经成为越来越重要的一部分。而 WebSocket 技术的出现,使得实时通信变得更加高效和便捷。


WebSocket 协议是一种基于 TCP 协议的双向通信协议,它能够在客户端和服务器之间建立起持久性的连接,从而实现实时通信。


在前端开发中,为了更好地利用 WebSocket 技术,我们通常会对其进行封装,以便于全局调用并根据自己的业务做不同的预处理。


本文将介绍如何有效封装一个 WebSocket 供全局使用,并根据自己的业务做不同的预处理,实现更方便的调用,减少重复代码。


具体实现


我们将基于 Web API 提供的 WebSocket 类,封装一个 Socket 类,该类将提供以下功能:



  1. 建立 WebSocket 连接,并支持发送 query 参数。

  2. 发送、接收消息,支持对 WebSocket 的事件进行监听。

  3. 断开 WebSocket 连接。

  4. 支持心跳检测。

  5. 可以根据业务需要,对发送和接收的消息进行预处理。


下面是实现代码:


// socket.js
import modal from '@/plugins/modal'
const baseURL = import.meta.env.VITE_APP_BASE_WS;
const EventTypes = ['open', 'close', 'message', 'error', 'reconnect'];
const DEFAULT_CHECK_TIME = 55 * 1000; // 心跳检测的默认时间
const DEFAULT_CHECK_COUNT = 3; // 心跳检测默认失败重连次数
const DEFAULT_CHECK_DATA = { Type: 1, Parameters: ['alive'] }; // 心跳检测的默认参数 - 跟后端协商的
const CLOSE_ABNORMAL = 1006; // WebSocket非正常关闭code码

class EventMap {
deps = new Map();
depend(eventType, callback) {
this.deps.set(eventType, callback);
}
notify(eventType, event) {
if (this.deps.has(eventType)) {
this.deps.get(eventType)(event);
}
}
}

class Socket extends WebSocket {
heartCheckData = DEFAULT_CHECK_DATA;
heartCheckTimeout = DEFAULT_CHECK_TIME;
heartCheckInterval = null;
heartCheckCount = DEFAULT_CHECK_COUNT
constructor(options, dep, reconnectCount = 0) {
let _baseURL = baseURL
const { url, protocols, query = {}, greet = null, customBase = null } = options;
const _queryParams = Object.keys(query).reduce((str, key) => {
if (typeof query[key] !== 'object' && typeof query[key] !== 'function') {
return str += str.length > 0 ? `&${key}=${query[key]}` : `${key}=${query[key]}`;
} else {
return str;
}
}, '');
if (customBase) {
_baseURL = customBase
}
super(`${_baseURL}${url}?${_queryParams}`, protocols);
this._currentOptions = options;
this._dep = dep;
this._reconnectCount = reconnectCount;
greet && Object.assign(this, {
heartCheckData: greet
})
this.initSocket();
}

// 初始化WebSocket
initSocket() {
// 监听webSocket的事件
this.onopen = function (e) {
this._dep.notify('open', e);
this.heartCheckStart();
}
this.onclose = function (e) {
this._dep.notify('close', e);
// 如果WebSocket是非正常关闭 则进行重连
if (e.code === CLOSE_ABNORMAL) {
if (this._reconnectCount < this.heartCheckCount) {
this._reconnectCount++;
const _socket = new Socket(this._currentOptions, this._dep, this._reconnectCount);
this._dep.notify('reconnect', _socket);
} else {
return modal.msgError('WebSocket重连失败, 请联系技术客服!');
}
}
}
this.onerror = function (e) {
this._dep.notify('error', e);
}
this.onmessage = function (e) {
// 如果后端返回的是二进制数据
if (e.data instanceof Blob) {
const reader = new FileReader()
reader.readAsArrayBuffer(e.data)
reader.onload = (ev) => {
if (ev.target.readyState === FileReader.DONE) {
this._dep.notify('message', ev.target?.result);
}
}
} else {
// 处理普通数据
try {
const _parseData = JSON.parse(e.data);
this._dep.notify('message', _parseData);
} catch (error) {
console.log(error)
}
}
}

}

// 订阅事件
subscribe(eventType, callback) {
if (typeof callback !== 'function') throw new Error('The second param is must be a function');
if (!EventTypes.includes(eventType)) throw new Error('The first param is not supported');
this._dep.depend(eventType, callback);
}

// 发送消息
sendMessage(data, options = {}) {
const { transformJSON = true } = options;
let result = data;
if (transformJSON) {
result = JSON.stringify(data);
}
this.send(result);
}

// 关闭WebSocket
closeSocket(code, reason) {
this.close(code, reason);
}

// 开始心跳检测
heartCheckStart() {
this.heartCheckInterval = setInterval(() => {
if (this.readyState === this.OPEN) {
let transformJSON = typeof this.heartCheckData === 'object'
this.sendMessage(this.heartCheckData, { transformJSON });
} else {
this.clearHeartCheck();
}
}, this.heartCheckTimeout)
}

// 清除心跳检测
clearHeartCheck() {
clearInterval(this.heartCheckInterval);
}

// 重置心跳检测
resetHeartCheck() {
clearInterval(this.heartCheckInterval);
this.heartCheckStart();
}
}
// 默认的配置项
const defaultOptions = {
url: '',
protocols: '',
query: {},
}

export const useSocket = (options = defaultOptions) => {
if (!window.WebSocket) return modal.msgWarning('您的浏览器不支持WebSocket, 请更换浏览器!');
const dep = new EventMap();
const reconnectCount = 0;
return new Socket(options, dep, reconnectCount);
}

接下来我们从实际使用的角度解释一下上面的代码,首先我们暴露了一个 useSocket 函数,该函数接收一个 options 配置项参数,支持的参数有:



  • url:要连接的 WebSocket URL;

  • protocols:一个协议字符串或者一个包含协议字符串的数组;

  • query:可以通过 URL 传递给后端的查询参数;

  • greet:心跳检测的打招呼信息;

  • customBase:自定义的 baseURL ,否则默认使用环境变量中定义的 env.VITE_APP_BASE_WS


在调用该函数后,我们首先会判断当前用户的浏览器是否支持 WebSocket,如果不支持给予用户提示。


然后我们实例化了一个 EventMap 类的实例对象 dep,你可以把它当作是一个依赖收集桶,当用户订阅了某个 WebSocket 事件时,我们将收集这个事件对应的回调作为依赖,在事件触发时,再通知该依赖,然后调用该事件对应的回调函数。


接下来我们定义了一个初始的重连次数记录值 reconnectCount 为 0,每当这个 WebSocket 重连时,该值会自增。


之后我们实例化了自己封装的 Socket 类,并传入了我们上面的三个参数。
Socket 类的构造函数 constructor 中,我们先取出配置项,把 query 内的参数拼接在 URL 上,然后使用 super 调用父类的构造函数进行建立 WebSocket 连接。


之后我们缓存了当前 Socket 实例化时的参数,再调用 initSocket() 方法去进行 WebSocket 事件的监听:



  • onopen:触发 depopen 对应的回调函数并且打开心跳检测;

  • onclose:触发 depclose 对应的回调函数并且对关闭的 code 码进行判断,如果是非正常关闭连接,将会进行重连,如果重连次数达到阈值,则通知给用户;

  • onerror:触发 deperror 对应的回调函数;

  • onmessage:接收到服务端返回的数据,可以先根据自身业务做一些预处理,比如我就根据不同的数据类型进行了数据解析的预处理,之后再触发 depmessage 对应的回调函数并传入处理过后的数据。


我们也暴露了一些成员方法以供实例对象使用:



  • subscribe:订阅 WebSocket 事件,传入事件类型并须是 EventTypes 内的类型之一,第二个参数则是回调函数;

  • sendMessage:同样的,我们在给服务端发送数据之前也可以根据自身业务做一些预处理,比如我将需要转成 JSON 的数据,在这里统一转换后再发送给服务端;

  • closeSocket:关闭 WebSocket 连接;

  • heartCheckStart:开始心跳检测,会创建一个定时器,在一定时间之后(默认是55s)给服务端发送信息确认连接是否正常;

  • clearHeartCheck:清除心跳检测定时器(如果当前 WebSocket 连接已经关闭,则自动清除);

  • resetHeartCheck:重置心跳检测定时器。


如何使用


让我们看下如何使用这个封装好的 useSocket 函数,以在 Vue3中使用为例:


// xx.jsx or xx.vue
import { useSocket } from './socket.js'
const socket = ref(null) // WebSocket实例
const initWebSocket = () => {
const options = {
url: '/<your url>',
query: {
// something params
},
}
socket.value = useSocket(options)
socket.value.subscribe('open', () => {
console.log('WebSocket连接成功!')
const greet = 'hello'
// 发送打招呼消息
socket.value.sendMessage(greet)
})
socket.value.subscribe('close', reason => {
console.log('WebSocket连接关闭!', reason)
})
socket.value.subscribe('message', result => {
console.log('WebSocket接收到消息:', result)
})
socket.value.subscribe('error', err => {
console.log('WebSocket捕获错误:', err)
})
socket.value.subscribe('reconnect', _socket => {
console.log('WebSocket断开重连:', _socket)
socket.value = _socket
})
}
initWebSocket()

最后,如果想 debug 我们的心跳检测是否有效,可以使用下面这段代码:


// 测试心跳检测重连 手动模拟断开的情况
if (this._reconnectCount > 0) return;
const tempTimer = setInterval(() => {
this.close();
if (this._reconnectCount < 3) {
console.log('重连');
this._reconnectCount++;
const _socket = new Socket(this._currentOptions, this._dep, this._reconnectCount);
this._dep.notify('reconnect', _socket);
} else {
return clearInterval(tempTimer);
}
}, 3 * 1000)

initSocket() 方法中的 this.onopen 事件的回调函数内的最后添加上面这段代码即可。


总结


至此,我们实现了一个 WebSocket 类的封装,提供了连接、断开、消息发送、接收和心跳检测等功能,并可以根据业务需要对消息进行预处理。同时,我们还介绍了如何使用封装好的 useSocket 函数。


WebSocket 封装的好处在于可以让我们在全局范围内方便地使用 WebSocket,提高代码的可读性和可维护性,降低代码的复杂度和重复性。在实际开发过程中,我们可以结合自己的业务需求,对封装的 WebSocket 类进行扩展和优化,以达到更好的效果。


尽管我在文中尽可能地详细介绍了每一个步骤和细节,但是难免会存在一些错误和不足之处。如果您在使用本文中介绍的方法时发现了任何错误或者有更好的方法,非常欢迎您指正并提出建议,以便我能够不断改进和提升文章的质量。


我是荼锦,一个兴趣使然的开发者。非常感谢您阅读本文,希望本文对您有所帮助!


作者:荼锦
来源:juejin.cn/post/7231481633671757861
收起阅读 »

吾辈楷模!国人开源的Redis客户端被Redis官方收购了!

不久前开源圈子里的一则消息在网上引起了一阵关注和讨论。 一个由国人开发者所打造的开源项目被 Redis 公司官方给收购了,作者自己也发了动态,表示感谢项目9年以来的陪伴,同时也希望她未来一切都好。 这个开源项目的名字叫做:ioredis,相信不少小伙伴也用过...
继续阅读 »

不久前开源圈子里的一则消息在网上引起了一阵关注和讨论。


一个由国人开发者所打造的开源项目被 Redis 公司官方给收购了,作者自己也发了动态,表示感谢项目9年以来的陪伴,同时也希望她未来一切都好。



这个开源项目的名字叫做:ioredis,相信不少小伙伴也用过。



目前在GitHub上我们可以看到,ioredis项目的开源地址已经被迁移至 Redis 官方旗下了。



iosredis是国人开发者所打造的一个Redis客户端,基于TypeScript所编写,以健壮性、高性能以及功能强大为特色,并且被很多大公司所使用。



截止到目前,该项目在GitHub上已累计获得超过 13000 个 Star标星和 1000+ Fork。


作者自己曾表示,自己创建这个开源项目的初衷也很简单,那就是当年在这方面并没有找到一个令自己满意的开源库,于是决定自己动手来打造一个,于是就利用闲暇时间,自己从零开发并开源了 ioredis 。


直到2022 年 8 月 30 日,历时整整7年,ioredis 成为了 Node.js 最流行的 Redis 客户端。



而直到如今,这个项目从个人的 side project 到被开源公司官方收购,作者9 年的坚持属实令人佩服,吾辈楷模啊!


而拜访了这位开发者的GitHub后我们会发现,作者非常热衷于创造工具,除了刚被收购的名作ioredis之外,主页还有非常多的开源项目,并且关注量都不低。



而且从作者发的一些动态来看,这也是一个热爱生活的有趣灵魂。



有一说一,个人开源作者真的挺不容易的,像上面这样的个人开源项目被官方收购的毕竟是个例,其实好多个人的开源项目到后期由于各种主客观原因,渐渐都停止更新和维护了。


大家都知道,伴随着这两年互联网行业的寒意,软件产业里的不少环节也受到了波动。行业不景气,连开源项目的主动维护也变得越来越少了。


毕竟连企业也要降本增效,而开源往往并不能带来快速直接的实际效益。付出了如果没有回报,便会很难坚持下去。


而对于一名学习者而言,参与开源项目的意义是不言而喻的,之前咱们这里也曾多次提及。


参与开源项目除了可以提升自身技术能力,收获项目开发经验之外,还可以让自己保持与开源社区其他优秀开发者之间的联系与沟通,并建立自己的技术影响力,另外参与优秀开源项目的经历也会成为自己求职简历上的一大亮点。


所以如果精力允许,利用业余时间来参与或维护一些开源项目,这对技术开发者来说,也是一段难得的经历!


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

一个高并发项目到落地的心酸路

前言 最近闲来没事,一直在掘金上摸鱼,看了不少高并发相关的文章,突然有感而发,想到了几年前做的一个项目,也多少和高并发有点关系。 这里我一边回忆落地细节一边和大家分享下,兴许能给大家带来点灵感。 正文 需求及背景 先来介绍下需求,首先项目是一个志愿填报系统,既...
继续阅读 »

前言


最近闲来没事,一直在掘金上摸鱼,看了不少高并发相关的文章,突然有感而发,想到了几年前做的一个项目,也多少和高并发有点关系。

这里我一边回忆落地细节一边和大家分享下,兴许能给大家带来点灵感。


正文


需求及背景


先来介绍下需求,首先项目是一个志愿填报系统,既然会扯上高并发,相信大家也能猜到大致是什么的志愿填报。

核心功能是两块,一是给考试填报志愿,二是给老师维护考生数据。

本来这个项目不是我们负责,奈何去年公司负责这个项目的组遭到了甲方严重的投诉,说很多考生用起来卡顿,甚至把没填上志愿的责任归到系统上。

甲方明确要求,如果这年再出现这种情况,公司在该省的所有项目将面临被替换的风险。

讨论来讨论去,最后公司将任务落到我们头上时,已经是几个月后的事了,到临危受命阶段,剩下不到半年时间。

虽然直属领导让我们不要有心理负担,做好了表扬,做不好锅也不是我们的,但明显感觉到得到他的压力,毕竟一个不小心就能上新闻。


分析


既然开始做了,再说那些有的没的就没用了,直接开始分析需求。

首先,业务逻辑并不算复杂,难点是在并发和数据准确性上。与客户沟通后,大致了解了并发要求后,于是梳理了下。



  1. 考生端登录接口、考生志愿信息查询接口需要4W QPS

  2. 考生保存志愿接口,需要2W TPS

  3. 报考信息查询4W QPS

  4. 老师端需要4k QPS

  5. 导入等接口没限制,可以异步处理,只要保证将全部信息更新一遍在20分钟以内即可,同时故障恢复的时间必须在20分钟以内(硬性要求)

  6. 考生端数据要求绝对精准,不能出现遗漏、错误等和考生操作不一致的数据

  7. 数据脱敏,防伪

  8. 资源是有限的,提供几台物理机

    大的要求就这么多,主要是在有限资源下需要达到如此高的并发确实需要思考思考,一般的crud根本达不到要求。


方案研讨


接下来我会从当时我们切入问题的点开始,从前期设计到项目落地整个过程的问题及思考,一步步去展示这个项目如何实现的

首先,我们没有去设计表,没有去设计接口,而是先去测试。测试什么?测试我们需要用到或可能用到的中间件是否满足需求


MySQL


首先是MySQL,单节点MySQL测试它的读和取性能,新建一张user表。

向里面并发插入数据和查询数据,得到的TPS大概在5k,QPS大概在1.2W。

查询的时候是带id查询,索引列的查询不及id查询,差距大概在1k。

insert和update存在细微并发差距,但基本可以忽略,影响更新性能目前最大的问题是索引。

如果表中带索引,将降低1k-1.5k的TPS。

目前结论是,mysql不能达到要求,能不能考虑其他架构,比如mysql主从复制,写和读分开。

测试后,还是放弃,主从复制结构会影响更新,大概下降几百,而且单写的TPS也不能达到要求。

至此结论是,mysql直接上的方案肯定是不可行的


Redis


既然MySQL直接查询和写入不满足要求,自然而然想到加入redis缓存。于是开始测试缓存,也从单节点redis开始测试。

get指令QPS达到了惊人的10w,set指令TPS也有8W,意料之中也惊喜了下,仿佛看到了曙光。

但是,redis容易丢失数据,需要考虑高可用方案


实现方案


既然redis满足要求,那么数据全从redis取,持久化仍然交给mysql,写库的时候先发消息,再异步写入数据库。

最后大体就是redis + rocketMQ + mysql的方案。看上去似乎挺简单,当时我们也这样以为 ,但是实际情况却是,我们过于天真了。

这里主要以最重要也是要求最高的保存志愿信息接口开始攻略


故障恢复

第一个想到的是,这些个节点挂了怎么办?

mysql挂了比较简单,他自己的机制就决定了他即使挂掉,重启后仍能恢复数据,这个可以不考虑。

rocketMQ一般情况下挂掉了可能会丢失数据,经过测试发现,在高并发下,确实存在丢消息的现象。

原因是它为了更加高效,默认采用的是异步落盘的模式,这里为了保证消息的绝对不丢失,修改成同步落盘模式。

然后是最关键的redis,不管哪种模式,redis在高并发下挂掉,都会存在丢失数据的风险。

数据丢失对于这个项目格外致命,优先级甚至高于并发的要求。

于是,问题难点来到了如何保证redis数据正确,讨论过后,决定开启redis事务。

保存接口的流程就变成了以下步骤:

1.redis 开启事务,更新redis数据

2.rocketMQ同步落盘

3.redis 提交事务

4.mysql异步入库

我们来看下这个接口可能存在的问题。

第一步,如果redis开始事务或更新redis数据失败,页面报错,对于数据正确性没有影响

第二步,如果rocketMQ落盘报错,那么就会有两种情况。

情况一,落盘失败,消息发送失败,好像没什么影响,直接报错就可。

情况二,如果发送消息成功,但提示发送失败(无论什么原因),这时候将导致mysql和redis数据的最终不一致。

如何处理?怎么知道是redis的有问题还是mysql的有问题?出现这种情况时,如果考生不继续操作,那么这条错误的数据必定无法被更新正确。

考虑到这个问题,我们决定引入一个时间戳字段,同时启动一个定时任务,比较mysql和redis不一致的情况,并自主修复数据。

首先,redis中记录时间戳,同时在消息中也带上这个时间戳并在入库时记录到表中。

然后,定时任务30分钟执行一次,比较redis中的时间戳是否小于mysql,如果小于,便更新redis中数据。如果大于,则不做处理。

同时,这里再做一层优化,凌晨的时候执行一个定时任务,比较redis中时间戳大于mysql中的时间戳,连续两天这条数据都存在且没有更新操作,将提示给我们手动运维。

然后是第三步,消息提交成功但是redis事务提交失败,和第二步处理结果一致,将被第二个定时任务处理。

这样看下来,即使redis崩掉,也不会丢失数据。


第一轮压测


接口实现后,当时怀着期待,信息满满的去做了压测,结果也是当头棒喝。

首先,数据准确性确实没有问题,不管突然kill掉哪个环节,都能保证数据最终一致性。

但是,TPS却只有4k不到的样子,难道是节点少了?

于是多加了几个节点,但是仍然没有什么起色。问题还是想简单了。


重新分析


经过这次压测,之后一个关键的问题被提了出来,影响接口TPS的到底是什么???

一番讨论过后,第一个结论是:一个接口的响应时间,取决于它最慢的响应时间累加,我们需要知道,这个接口到底慢在哪一步或哪几步?

于是用arthas看了看到底慢在哪里?

结果却是,最慢的竟然是redis修改数据这一步!这和测试的时候完全不一样。于是针对这一步,我们又继续深入探讨。

结论是:

redis本身是一个很优秀的中间件,并发也确实可以,选型时的测试没有问题。

问题出在IO上,我们是将考生的信息用json字符串存储到redis中的(为什么不保存成其他数据结构,因为我们提前测试过几种可用的数据结构,发现redis保存json字符串这种性能是最高的),

而考生数据虽然单条大小不算大,但是在高并发下的上行带宽却是被打满的。

于是针对这种情况,我们在保存到redis前,用gzip压缩字符串后保存到redis中。

为什么使用gzip压缩方式,因为我们的志愿信息是一个数组,很多重复的数据其实都是字段名称,gzip和其他几个压缩算法比较后,综合考虑到压缩率和性能,在当时选择了这种压缩算法。

针对超过限制的字符串,我们同时会将其拆封成多个(实际没有超过三个的)key存储。


继续压测


又一轮压测下来,效果很不错,TPS从4k来到了8k。不错不错,但是远远不够啊,目标2W,还没到它的一半。

节点不够?加了几个节点,有效果,但不多,最终过不了1W。

继续深入分析,它慢在哪?最后发现卡在了rocketMQ同步落盘上。

同步落盘效率太低?于是压测一波发现,确实如此。

因为同步落盘无论怎么走,都会卡在rocketMQ写磁盘的地方,而且因为前面已经对字符串压缩,也没有带宽问题。

问题到这突然停滞,不知道怎么处理rocketMQ这个点。

同时,另一个同事在测试查询接口时也带来了噩耗,查询接口在1W2左右的地方就上不去了,原因还是卡在带宽上,即使压缩了字符串,带宽仍被打满。

怎么办?考虑许久,最后决定采用较常规的处理方式,那就是数据分区,既然单个rocketMQ服务性能不达标,那么就水平扩展,多增加几个rocketMQ。

不同考生访问的MQ不一样,同时redis也可以数据分区,幸运的是正好redis有哈希槽的架构支持这种方式。

而剩下的问题就是如何解决考生分区的方式,开始考虑的是根据id进行求余的分区,但后来发现这种分区方式数据分布及其不均匀。

后来稍作改变,根据正件号后几位取余分区,数据分布才较为均匀。有了大体解决思路,一顿操作后继续开始压测。


一点小意外


压测之后,结果再次不如人意,TPS和QPS双双不增反降,继续通过arthas排查。

最后发现,redis哈希槽访问时会在主节点先计算key的槽位,而后再将请求转到对应的节点上访问,这个计算过程竟然让性能下降了20%-30%。

于是重新修改代码,在java内存中先计算出哈希槽位,再直接访问对应槽位的redis。如此重新压测,QPS达到了惊人的2W,TPS也有1W2左右。

不错不错,但是也只到了2W,在想上去,又有了瓶颈。

不过这次有了不少经验,马上便发现了问题所在,问题来到了nginx,仍然是一样的问题,带宽!

既然知道原因,解决起来也比较方便,我们将唯一有大带宽的物理机上放上两个节点nginx,通过vip代理出去,访问时会根据考生分区信息访问不同的地址。


压测


已经记不清第几轮压测了,不过这次的结果还算满意,主要查询接口QPS已经来到了惊人的4W,甚至个别接口来到6W甚至更高。

胜利已经在眼前,唯一的问题是,TPS上去不了,最高1W4就跑不动了。

什么原因呢?查了每台redis主要性能指标,发现并没有达到redis的性能瓶颈(上行带宽在65%,cpu使用率也只有50%左右)。

MQ呢?MQ也是一样的情况,那出问题的大概率就是java服务了。分析一波后发现,cpu基本跑到了100%,原来每个节点的最大链接数基本占满,但带宽竟然还有剩余。

静下心来继续深入探讨,连接数为什么会满了?原因是当时使用的SpringBoot的内置容器tomcat,无论如何配置,最大连接数最大同时也就支持1k多点。

那么很简单的公式就能出来,如果一次请求的响应时间在100ms,那么1000 * 1000 / 100 = 10000。

也就是说单节点最大支持的并发也就1W,而现在我们保存的接口响应时间却有300ms,那么最大并发也就是3k多,目前4个分区,看来1W4这个TPS也好像找到了出处了。

接下来就是优化接口响应时间的环节,基本是一步一步走,把能优化的都优化了一遍,最后总算把响应时间控制在了100ms以内。

那么照理来说,现在的TPS应该会来到惊人的4W才对。


再再次压测


怀着忐忑又激动的心情,再一次进入压测环节,于是,TPS竟然来到了惊人的2W5。

当时真心激动了一把,但是冷静之后却也奇怪,按正常逻辑,这里的TPS应该能达到3W6才对。

为了找到哪里还有未发现的坑(怕上线后来惊喜),我们又进一步做了分析,最后在日志上找到了些许端倪。

个别请求在链接redis时报了链接超时,存在0.01%的接口响应时间高于平均值。

于是我们将目光投向了redis连接数上,继续一轮监控,最终在业务实现上找到了答案。

一次保存志愿的接口需要执行5次redis操作,分别是获取锁、获取考生信息、获取志愿信息、修改志愿信息、删除锁,同时还有redis的事务。

而与之相比,查询接口只处理了两次操作,所以对于一次保存志愿的操作来看,单节点的redis最多支持6k多的并发。

为了验证这个观点,我们尝试将redis事务和加锁操作去掉,做对照组压测,发现并发确实如预期的一样有所提升(其实还担心一点,就是抢锁超时)。


准备收工


至此,好像项目的高并发需求都已完成,其他的就是完善完善细节即可。

于是又又又一次迎来了压测,这一次不负众望,重要的两个接口均达到了预期。

这之后便开始真正进入业务实现环节,待整个功能完成,在历时一个半月带两周的加班后,终于迎来了提测。


提测后的问题


功能提测后,第一个问题又又又出现在了redis,当高并发下突然kill掉redis其中一个节点。

因为用的是哈希槽的方式,如果挂掉一个节点,在恢复时重新算槽将非常麻烦且效率很低,如果不恢复,那么将严重影响并发。

于是经过讨论之后,决定将redis也进行手动分区,分区逻辑与MQ的一致。

但是如此做,对管理端就带来了一定影响,因为管理端是列表查询,所以管理端获取数据需要从多个节点的redis中同时获取。

于是管理端单独写了一套获取数据分区的调度逻辑。

第二个问题是管理端接口的性能问题,虽然管理端的要求没考生端高,但扛不住他是分页啊,一次查10个,而且还需要拼接各种数据。

不过有了前面的经验,很快就知道问题出在了哪里,关键还是redis的连接数上,为了降低链接数,这里采用了pipeline拼接多个指令。


上线


一切准备就绪后,就准备开始上线。说一下应用布置情况,8+4+1+2个节点的java服务,其中8个节点考生端,4个管理端,1个定时任务,2个消费者服务。

3个ng,4个考生端,1个管理端。

4个RocketMQ。

4个redis。

2个mysql服务,一主一从,一个定时任务服务。

1个ES服务。

最后顺利上线,虽然发生了个别线上问题,但总体有惊无险,

而真是反馈的并发数也远没有到达我们的系统极限,开始准备的水平扩展方案也没有用上,无数次预演过各个节点的宕机和增加分区,一般在10分钟内恢复系统,不过好在没有排上用场。


最后


整个项目做下来感觉越来越偏离面试中的高并发模式,说实在的也是无赖之举,

偏离的主要原因我认为是项目对数据准确性的要求更高,同时需要完成高并发的要求。

但是经过这个项目的洗礼,在其中也收获颇丰,懂得了去监控服务性能指标,然后也加深了中间件和各种技术的理解。

做完之后虽然累,但也很开心,毕竟在有限的资源下去分析性能瓶颈并完成项目要求后,还是挺有成就感的。

再说点题外话,虽然项目成功挽回了公司在该省的形象,也受到了总公司和领导表扬,但最后也就这样了,

实质性的东西一点没有,这也是我离开这家公司的主要原由。不过事后回想,这段经历确实让人难忘,也给我后来的工作带来了很大的帮助。

从以前的crud,变得能去解决接口性能问题。这之前一遇上,可能两眼茫然或是碰运气,现在慢慢的会根据蛛丝马迹去探究优化方案。

不知道我在这个项目的经历是否能引起大家共鸣?希望这篇文章能对你有所帮助。


作者:青鸟218
来源:juejin.cn/post/7346021356679675967
收起阅读 »

在 vite 工程化中手动分包

web
项目搭建 我们使用 vite 搭建一个 vue3 工程,执行命令: pnpm create vite vue3-demo --template vue-ts 安装 lodash 依赖包,下载依赖: pnpm add lodash pnpm inst...
继续阅读 »

项目搭建



  1. 我们使用 vite 搭建一个 vue3 工程,执行命令:


pnpm create vite vue3-demo --template vue-ts


  1. 安装 lodash 依赖包,下载依赖:


pnpm add lodash

pnpm install


  1. 完成后的工程目录结构是这样的:
    gv21sv3nq3yfm3tchedeawaz6134froy.png


业务场景


我们先首次构建打包,然后修改一下代码再打包,对比一下前后打包差异:


tin4v56sv7mrp3mu0o7d4lvfkq70jcnw.png


可以看到,代码改动后,index-[hash].js 的文件指纹发生了变化,这意味着每次打包后,用户就要重新下载新的 js,而这个文件里面包含了这些东西:vuelodash业务代码,其中像 vuelodash 这些依赖包是固定不变的,有变动的只是我们的业务代码,基于这个点我们就可以在其基础上打包优化。


打包优化


我们需要在打包上优化两个点:



  1. 把第三方依赖库单独打包成一个 js 文件

  2. 把我们的业务代码单独打包成一个 js 文件


这块需要我们对 vite 工程化知识有一定的了解,它使用 Rollup 打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源,可以通过配置 build.rollupOptions.output.manualChunks 来自定义 chunk 分割策略。


更改 vite 配置



  1. 打开 vite.config.ts,加入配置项:


import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor':['vue', 'lodash'], // 这里可以自己自定义打包名字和需要打包的第三方库
}
}
}
}
})


  1. 执行打包命令,我们可以看到打包文件中多了一个 verdor-[hash].js 的文件,这个就是刚才配置分包的文件:
    ihwenj2upzzid1fbpq4hw6kg16u3xf0e.png

  2. 这样的好处就是,将来如果我们的业务代码有改动,打包的第三方库的文件指纹就不会变,用户就会直接读取浏览器缓存,这是一种比较优的解决办法:
    2wd353hjb7zmgfuouf6yu80agg24npdq.png

  3. 但这样需要我们每次都手动填写第三方库,那也显得太呆了,我们可以把 manualChunks 配置成一个函数,每次去加载这个模块的时候,它就会运行这个函数,打印看下输出什么:
    0ihpx9l1t7qski90cxjlbeoibctusvan.png

  4. 我们会发现依赖包都是在 node_modules 目录下,接下来我们就修改一下配置:


 import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return 'vendor'
}
}
}
}
}
})


  1. 我们再看下打包结果:
    8fekhcavsaerrhe1zjbe9j1xyxvi30jb.png


总结


分包(Code Splitting)是一种将应用程序代码拆分为不同的块或包的技术,从而在需要时按需加载这些包。这种技术带来了许多优势,特别是在构建大型单页应用(Single Page Application,SPA)时。



  • 减小初始加载时间: 将应用程序分成多个小块,可以减少初始加载时需要下载的数据量。这意味着用户可以更快地看到初始内容,提高了用户体验。

  • 优化资源利用: 分包可以根据用户的操作行为和需要进行按需加载。这样,在用户访问特定页面或功能时,只会加载与之相关的代码,而不会加载整个应用程序的所有代码,从而减少了不必要的资源消耗。

  • 并行加载: 分包允许浏览器并行加载多个包。这可以加速页面加载,因为浏览器可以同时请求多个资源,而不是等待一个大文件下载完成。

  • 缓存优化: 分包使得缓存管理更加灵活。如果应用程序的一部分发生变化,只需重新加载受影响的分包,而不必重新加载整个应用程序。

  • 减少内存占用: 当用户访问某个页面时,只有该页面所需的代码被加载和执行。这有助于减少浏览器内存的使用,尤其是在应用程序变得复杂时。

  • 按需更新: 当应用程序的某个部分需要更新时,只需要重新发布相应的分包,而不必重新发布整个应用程序。这可以减少发布和部署的复杂性。

  • 代码复用和维护: 分包可以按功能或模块来划分,从而鼓励代码的模块化和复用。这样,不同页面之间可以共享相同的代码块,减少了重复编写和维护代码的工作量。


作者:白雾茫茫丶
来源:juejin.cn/post/7346031272919072779
收起阅读 »

H5推送,为什么都用WebSocket?

web
       大家好,我是石头~        最近大盘在3000点附近磨蹭,我也随大众去网上浏览了下感兴趣的几只股票,看下行情怎样。        看了一会,还是垃圾行情,不咋地,不过看着页面上的那些实时刷新分时图和五档行情,倒是想起公司以前就因为这个实时数...
继续阅读 »

       大家好,我是石头~


       最近大盘在3000点附近磨蹭,我也随大众去网上浏览了下感兴趣的几只股票,看下行情怎样。
       看了一会,还是垃圾行情,不咋地,不过看着页面上的那些实时刷新分时图和五档行情,倒是想起公司以前就因为这个实时数据刷新的问题,差点引起一次生产事故。


HTTP轮询差点导致生产事故


       那是一个给用户展示实时数据的需求,产品的要求是用户数据发生变动,需要在30秒内给客户展示出来。


       当时由于数据展示的页面入口较深,负责的后端开发就让H5通过轮询调用的方式来实现数据刷新。


       然而,由于客户端开发的失误,将此页面在APP打开时就进行了初始化,导致数据请求量暴涨,服务端压力大增,差点就把服务端打爆了。


fa7049166c79454eb87f3890d1aa6f4b.webp


H5推送,应该用什么?


       既然用HTTP做实时数据刷新有风险,那么,应该用什么方式来实现?


       一般要实现服务端推送,都需要用到长连接,而能够做到长连接的只有WebSocket、UDP和TCP,而且,WebSocket是在TCP之上构建的一种高级应用层协议。大家觉得我们应该用哪一种?


       其实,大家只要网上查一下,基本都会被推荐使用WebSocket,那么,为什么要用WebSocket?


u=2157318451,827303453&fm=253&fmt=auto&app=138&f=JPEG.webp


为什么要用WebSocket?


       这个我们可以从以下几个方面来看:



  • 易用性与兼容性:WebSocket兼容现代浏览器(HTML5标准),可以直接在H5页面中使用JavaScript API与后端进行交互,无需复杂的轮询机制,而且支持全双工通信。而TCP层级的通信通常不适合直接在纯浏览器环境中使用,因为浏览器API主要面向HTTP(S)协议栈,若要用TCP,往往需要借助Socket.IO、Flash Socket或其他插件,或者在服务器端代理并通过WebSocket、Comet等方式间接与客户端通信。

  • 开发复杂度与维护成本:WebSocket已经封装好了一套完整的握手、心跳、断线重连机制,对于开发者而言,使用WebSocket API相对简单。而TCP 开发则需要处理更多的底层细节,包括但不限于连接管理、错误处理、协议设计等,这对于前端开发人员来说门槛较高。

  • 资源消耗与性能:WebSocket 在建立连接之后可以保持持久连接,减少了每次请求都要建立连接和断开连接带来的资源消耗,提升了性能。而虽然TCP连接也可以维持长久,但如果是自定义TCP协议,由于没有WebSocket的标准化复用和优化机制,可能在大规模并发场景下,资源管理和性能控制更为复杂。

  • 移动设备支持:WebSocket在移动端浏览器上的支持同样广泛,对于跨平台的H5应用兼容性较好。若采用原生TCP,移动设备上的兼容性和开发难度会进一步加大。


websocket01.jpg


结论


       综上所述,H5实时数据推送建议使用WebSocket,但是在使用WebSocket的时候,大家对其安全机制要多关注,避免出现安全漏洞。


作者:石头聊技术
来源:juejin.cn/post/7345404998164955147
收起阅读 »

面试官:前端请求如何避免明文传输?谁沉默了,原来是我

web
如果你也在准备春招,欢迎加微信shunwuyu。这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。 前言 连夜肝文,面试以来最尴尬的一次,事情是这样的,最近有开始面稍微有难度一点岗位,本文的主题出自北京某一小厂的正式...
继续阅读 »

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



前言


连夜肝文,面试以来最尴尬的一次,事情是这样的,最近有开始面稍微有难度一点岗位,本文的主题出自北京某一小厂的正式岗面试题,薪资水平大概开在10k-12k。之前一直是投的比较小的公司比较简单的实习岗位,这个是无意间投出去的一个,由于是 0 年经验小白*1,结果没想到简历过筛,硬着头皮上了。


结果很惨,40分钟的面试有 80% 不会回答,像大文件上传、缓存优化、滑动 text-area标签用什么属性(话说为什么有这么冷的题)等等,有一个算一个,都没答出来。


2.jpg


重点来了,在两个面试官问到前端请求如何避免明文传输的时候,在我绞尽脑汁思考五秒之后,现场气氛非常凝重,这道题也成为了这次面试的最后一题。


在此提醒各位小伙伴,如果你的简历或者自我介绍中有提到网络请求,一定要注意了解一下有关数据加密处理,出现频率巨高!!!


最后,下午四点面试,六点hr就通知了我面试结果,凉凉


微信图片_20240224002007.jpg


如何避免前端请求明文传输


要在前端发送请求时做到不明文,有以下几种方法:



  1. HTTPS 加密传输: 使用 HTTPS 协议发送请求,所有的数据都会在传输过程中进行加密,从而保护数据不以明文形式传输。这样即使数据被截获,黑客也无法直接获取到数据的内容。

  2. 数据加密处理: 在前端对敏感数据进行加密处理,然后再发送请求。可以使用一些加密算法,如 AES、RSA 等,将敏感数据进行加密后再发送到服务器。这样即使数据在传输过程中被截获,也无法直接获取其内容。

  3. 请求签名验证: 在发送请求之前,前端对请求参数进行签名处理,并将签名结果和请求一起发送到服务器。服务器端根据事先约定的签名算法和密钥对请求参数进行验证,确保请求的完整性和可靠性。

  4. Token 验证: 在用户登录时,后端会生成一个 Token 并返回给前端,前端在发送请求时需要将 Token 添加到请求头或请求参数中。后端在接收到请求后,验证 Token 的有效性,以确保请求的合法性。

  5. 请求头加密处理: 在发送请求时,可以将请求头中的一些关键信息进行加密处理,然后再发送到服务器。服务器端需要在接收到请求后对请求头进行解密,以获取其中的信息。


HTTPS 加密传输


HTTPS(HyperText Transfer Protocol Secure)是HTTP协议的安全版本,它通过在HTTP和TCP之间添加一层TLS/SSL加密层来实现加密通信。


HTTPS加密传输的具体细节:



  1. TLS/SSL握手过程: 客户端与服务器建立HTTPS连接时,首先进行TLS/SSL握手。在握手过程中,客户端和服务器会交换加密算法和密钥信息,以协商出双方都支持的加密算法和密钥,从而确保通信的安全性。

  2. 密钥交换: 在握手过程中,客户端会向服务器发送一个随机数,服务器使用该随机数以及自己的私钥生成一个对称密钥(即会话密钥)。该对称密钥用于加密和解密后续的通信数据。

  3. 证书验证: 在握手过程中,服务器会向客户端发送自己的数字证书。客户端会验证服务器的数字证书是否有效,包括检查证书的颁发机构、有效期等信息,以确认与服务器建立连接的真实性。

  4. 加密通信: 客户端和服务器在握手成功后,就会使用协商好的加密算法和密钥进行通信。客户端和服务器之间传输的所有数据都会被加密,包括HTTP请求和响应内容、URL、请求头等信息。

  5. 完整性保护: 在通信过程中,TLS/SSL还会使用消息认证码(MAC)来保护通信的完整性,防止数据在传输过程中被篡改。MAC是通过将通信内容和密钥进行哈希计算得到的,用于验证数据的完整性。


通过以上步骤,HTTPS这种加密通信方式在保护用户隐私、防止数据被窃取或篡改方面起到了重要作用。


数据加密处理


数据加密处理是指在前端对敏感数据进行加密处理,以确保数据在传输过程中的安全性。


数据加密处理的一般步骤和具体方法:



  1. 选择加密算法: 首先需要选择合适的加密算法,常见的包括对称加密算法(如AES)和非对称加密算法(如RSA)。对称加密算法使用相同的密钥进行加密和解密,而非对称加密算法使用公钥和私钥进行加密和解密。

  2. 生成密钥: 对于对称加密算法,需要生成一个密钥,用于加密和解密数据。对于非对称加密算法,需要生成一对公钥和私钥,公钥用于加密数据,私钥用于解密数据。

  3. 加密数据: 在前端,使用选择好的加密算法和密钥对敏感数据进行加密处理。例如,对用户的密码、个人信息等敏感数据进行加密处理,确保在数据传输过程中不被窃取或篡改。

  4. 传输加密数据: 加密后的数据可以作为请求的参数发送到服务器。在发送请求时,可以将加密后的数据作为请求体或请求参数发送到服务器,确保数据在传输过程中的安全性。

  5. 解密数据(可选): 在服务器端接收到加密数据后,如果需要对数据进行解密处理,则需要使用相同的加密算法和密钥对数据进行解密操作。这样可以得到原始的明文数据,进一步进行业务处理。


总的来说,数据加密处理通过选择合适的加密算法、安全地管理密钥,以及正确地使用加密技术,可以有效地保护用户数据的安全性和隐私性。


请求签名验证


请求签名验证是一种验证请求完整性和身份验证的方法,通常用于确保请求在传输过程中没有被篡改,并且请求来自于合法的发送方。


请求签名验证的一般步骤:



  1. 签名生成: 发送请求的客户端在发送请求之前,会根据事先约定好的签名算法(如HMAC、RSA等)以及密钥对请求参数进行签名处理。签名处理的结果会作为请求的一部分发送到服务器。

  2. 请求发送: 客户端发送带有签名的请求到服务器。签名可以作为请求头、请求参数或请求体的一部分发送到服务器。

  3. 验证签名: 服务器接收到请求后,会根据事先约定好的签名算法以及密钥对请求参数进行签名验证。服务器会重新计算请求参数的签名,然后将计算得到的签名和请求中的签名进行比较。

  4. 比较签名: 服务器会将计算得到的签名和请求中的签名进行比较。如果两者一致,则说明请求参数没有被篡改,且请求来自于合法的发送方;否则,说明请求可能被篡改或来自于非法发送方,服务器可以拒绝该请求或采取其他适当的处理措施。

  5. 响应处理(可选): 如果请求签名验证通过,服务器会处理请求,并生成相应的响应返回给客户端。如果请求签名验证不通过,服务器可以返回相应的错误信息或拒绝请求。


通过请求签名验证,可以确保请求在传输过程中的完整性和可靠性,防止数据被篡改或伪造请求。这种方法经常用于对 API 请求进行验证,保护 API 服务的安全和稳定。


Token 验证


Token 验证是一种常见的用户身份验证方式,通常用于保护 Web 应用程序的 API 端点免受未经授权的访问。


Token验证的一般步骤:



  1. 用户登录: 用户使用用户名和密码登录到Web应用程序。一旦成功验证用户的凭据,服务器会生成一个Token并将其返回给客户端。

  2. Token生成: 服务器生成一个Token,通常包括一些信息,如用户ID、角色、过期时间等,然后将Token发送给客户端(通常是作为响应的一部分)。

  3. Token发送: 客户端在每次向服务器发送请求时,需要将Token作为请求的一部分发送到服务器。这通常是通过HTTP请求头的Authorization字段来发送Token,格式可能类似于Bearer Token。

  4. Token验证: 服务器在接收到请求时,会检查请求中的Token。验证过程包括检查Token的签名是否有效、Token是否过期以及用户是否有权限执行请求的操作。

  5. 响应处理: 如果Token验证成功,服务器会处理请求并返回相应的数据给客户端。如果Token验证失败,服务器通常会返回401 Unauthorized或其他类似的错误代码,并要求客户端提供有效的Token。

  6. Token刷新(可选): 如果Token具有过期时间,客户端可能需要定期刷新Token以保持登录状态。客户端可以通过向服务器发送刷新Token的请求来获取新的Token。


在Token验证过程中,服务器可以有效地识别和验证用户身份,以确保API端点仅允许授权用户访问,并保护敏感数据不被未经授权的访问。


请求头加密处理


请求头加密处理是指在前端将请求头中的一些关键信息进行加密处理,然后再发送请求到服务器。


请求头加密处理的一般步骤:



  1. 选择加密算法: 首先需要选择适合的加密算法,常见的包括对称加密算法(如AES)和非对称加密算法(如RSA)。根据安全需求和性能考虑选择合适的加密算法。

  2. 生成密钥: 对于对称加密算法,需要生成一个密钥,用于加密和解密请求头中的信息。对于非对称加密算法,需要生成一对公钥和私钥,公钥用于加密数据,私钥用于解密数据。

  3. 加密请求头: 在前端,使用选择好的加密算法和密钥对请求头中的关键信息进行加密处理。可以是请求中的某些特定参数、身份验证信息等。确保加密后的请求头信息无法直接被识别和篡改。

  4. 发送加密请求: 加密处理后的请求头信息作为请求的一部分发送到服务器。可以是作为请求头的一部分,也可以是作为请求体中的一部分发送到服务器。

  5. 解密处理(可选): 在服务器端接收到加密请求头信息后,如果需要对请求头进行解密处理,则需要使用相同的加密算法和密钥对数据进行解密操作。这样可以得到原始的请求头信息,服务器可以进一步处理请求。


请求头加密处理这种方法可以有效地防止请求头中的敏感信息被窃取或篡改,并提高了数据传输的安全性。


请求头加密处理和数据加密处理的区别


请求头加密处理和数据加密处理在概念和步骤上非常相似,都是为了保护数据在传输过程中的安全性。


要区别在于加密的对象和处理方式:



  1. 加密对象:



    • 请求头加密处理: 主要是对请求头中的一些关键信息进行加密处理,例如身份验证信息、授权信息等。请求头中的这些信息通常是用来授权访问或识别用户身份的关键数据。

    • 数据加密处理: 主要是对请求体中的数据或响应体中的数据进行加密处理,例如用户提交的表单数据、API请求中的参数数据等。这些数据通常是需要保护隐私的用户输入数据或敏感业务数据。



  2. 处理方式:



    • 请求头加密处理: 一般来说,请求头中的关键信息通常较少,并且不像请求体中的数据那样多样化。因此,请求头加密处理可以更加灵活,可以选择性地对请求头中的特定信息进行加密处理,以提高安全性。

    • 数据加密处理: 数据加密处理通常是对请求体中的整体数据进行加密处理,以保护整体数据的安全性。例如,对表单数据进行加密处理,或对API请求参数进行加密处理,确保数据在传输过程中不被窃取或篡改。




结论:
请求头加密处理和数据加密处理都是为了保护数据在传输过程中的安全性,但针对的对象和处理方式有所不同。


请求头加密处理主要针对请求头中的关键信息进行加密,而数据加密处理主要针对请求体中的数据进行加密。


作者:知了知了__
来源:juejin.cn/post/7338702103882399744
收起阅读 »

保守点,90%的程序员不适合做独立开发

近两年互联网行业不景气,很多程序员都在寻找新出路。很自然的,独立开发成为一个充满吸引力的选择 —— 背靠自己的开发技能,不用看老板脸色,靠产品养活自己,想想就很美好。 但恕我直言,保守点说,90%的程序员不适合做独立开发。 这篇文章全是大实话,虽然会打破一些人...
继续阅读 »

近两年互联网行业不景气,很多程序员都在寻找新出路。很自然的,独立开发成为一个充满吸引力的选择 —— 背靠自己的开发技能,不用看老板脸色,靠产品养活自己,想想就很美好。


但恕我直言,保守点说,90%的程序员不适合做独立开发。


这篇文章全是大实话,虽然会打破一些人的幻想,但也提供解决方案,希望对迷茫的同学有些帮助。


独立开发赚钱么?


如果你满足如下画像:



  • 程序员工作多年,编程水平不错

  • 收入完全来源于工资

  • 日常学习的目的是提升技术


那对你来说,独立开发是不赚钱的。不赚钱并不是说做这事儿一分钱赚不到,满足以上画像的大部分独立开发者在持续经营半年到一年产品后,还是能稳定获得几刀~几十刀收益的。只是相比于付出的心血来说,这点收益实在是低。


以至于出海独立开发圈儿在谈收益时的语境都不是我开发了1年,现在每月能赚50刀,而是我开发了1年,现在拥有了等效于3w刀年化2%的货基(3w * 2% / 12 = 50)


这么一换算,欣慰了许多。


为什么不赚钱?因为独立开发的重点并不在于开发,叫独立产品会更准确些。


对于一款形成稳定变现闭环的产品,有3个最重要的环节:



  • 流量获取

  • 运营转化

  • 产品交付


程序员只是产品交付环节下的一个工种,与你同处产品交付环节的工种还包括产品经理、QA、项目经理、运维......


独立开发的本质就是你一个人抗下上述所有工种。


话又说回来,如果你即会编程又会流量获取,会运营转化,这样的复合人才在公司根本不用担心被裁,也没必要做独立开发。


所以,对于满足以上画像的同学,我劝你不要把独立开发当作失业后的救命稻草。


认识真实的商业世界


虽然我不建议你all in独立开发,但我建议有空闲时间的同学都去尝试下独立开发。


尝试的目的并不是赚钱,而是更具象的感知流量获取 -> 运营转化 -> 产品交付的路径。


大部分互联网产品往简单了说,都是表格 + 表单的形式,比如推特就是2个大表单(推荐流、关注流)以及描述用户之间关系的表格。


既然如此,当我们有了独立开发的想法时,首先考虑的应该是 —— 我的产品能不能用表格 + 表单 + 高效沟通实现,比如腾讯/飞书文档 + 微信群交流


像多抓鱼(做二手书业务)早期验证需求时,就是几个用户群 + 保存二手书信息的excel表组成。


如果你发现需求靠微信群交流就能解决,付款靠微信转账就能解决,那还有必要写代码开发项目,对接微信支付API么?


当聊到微信交流时,其实就触碰到另一个工种的工作范围了 —— 私域运营。在私域运营看来,通过微信(或其他社交软件)成交是再正常不过的商业模式,但很多程序员是不知道的。


这就是为什么我不建议你把独立开发当作被裁后的救命稻草,但建议有空闲时间的同学都去尝试下独立开发 —— 涉猎其他工种的工作范围,认识真实的商业世界。


当达到这一步后,我们再考虑下一步 —— 发掘你的长处。


发掘你的长处


当我们认识到一款完整的产品有3个最重要的环节:



  • 流量获取

  • 运营转化

  • 产品交付


就应该明白 —— 如果我们想显著提高独立开发的成功率,最好的方式是找到自己最擅长的环节,再和擅长其他环节的人合作。


这里很多程序员有个误区,会认为程序员擅长的肯定就是产品交付下的开发。


实际上,就我交流过的,或者亲自带出来的跑通变现闭环的程序员中,很多人有编程之外的天赋,只是他们没有意识到罢了。


举几个非常厉害的能力(或者说天赋):



  1. 向上突破的能力


有一类同学敢于把自己放到当前可能还不胜任的位置,然后通过不断学习让自己完成挑战。举几个例子:



  • 在不懂地推的时候,参与到校园外卖团队做地推,学习市场和推广的知识

  • 在只看了一本HTML书的情况下,敢直接接下学校建设国际会议网站的任务

  • 在不懂做运营的时候,有老板找他当公司运营负责人,他也接下来,并也做得很好


这类同学很容易跑出有自己特色的非标服务,再包装成产品售卖。



  1. 源源不断的心力支持


有位同学看短视频趋势不错,正好大学也玩过一段时间单反,就买了一套专业的影视设备,准备一边学做饭一边拍短视频,想做一名美食博主。


每天下班拍视频、剪辑加后期的,每个视频都需要花 10+ 个小时。熬了半年多,数据一直不行,就放弃了。


虽然他失败了,但很少有人能在没有正反馈的事上坚持半年,这种源源不断的心力支持其实是一种天赋。


靠这个天赋,只要踩到合适的赛道,成功是迟早的事儿。



  1. 链接人的能力


有些同学特别喜欢在群里唠嗑,与大佬聊天也不犯怵。这就是链接人的天赋


在如今的时代,有价值的信息通常是在小圈子中传播,再慢慢破圈到大众视野中。这类同学靠链接人的天赋,可以:



  1. 从小圈子获得有价值的信息,做信息差生意

  2. 做中间人整合资源


假设你探寻一圈后发现 —— 自己最拿得出手的就是编程能力,那你的当务之急不是发掘需求


以咱们普通程序员的产品sense,也就能想出笔记应用Todo List应用这类点子了......


你需要做的,是多认识其他圈子的人,向他们展示你的编程能力,寻找潜在的需求方


以我在运营的Symbol社区举例,这是个帮程序员发展第二曲线的社群。


之前社群有个痛点:每天社群会产生大量有价值的碎片知识,但这些知识分散在大量聊天消息中,爬楼看消息很辛苦。


基于这个痛点出发,我作为产品经理和群里两位小伙伴合作开发了识别、总结、打标签、分发有价值聊天记录的社群机器人



作为回报,这两位小伙伴将获得付费社群的收入分成。


总结


对于满足如下画像的程序员:



  • 程序员工作多年,编程水平不错

  • 收入完全来源于工资

  • 日常学习的目的是提升技术


不要把独立开发当作被裁后的救命稻草,而应该将其作为认识真实商业世界分工的途径,以及发掘自身优势的手段。


拍脑袋想没有用,只有真正在事儿上修,才能知道自己喜欢什么、擅长什么。


当认清自身优势后,与有其他优势的个体合作,一起构建有稳定收益闭环的产品。




作者:魔术师卡颂
来源:juejin.cn/post/7345756317557047306
收起阅读 »

快速从0-1完成聊天室开发——环信ChatroomUIKit功能详解

聊天室是当下泛娱乐社交应用中最经典的玩法,通过调用环信的 IM SDK 接口,可以快速创建聊天室。如果想根据自己业务需求对聊天室应用的 UI界面、弹幕消息、礼物打赏系统等进行自定义设计,最高效的方式则是使用环信的 ChatroomUIKit 。文档地址:htt...
继续阅读 »

聊天室是当下泛娱乐社交应用中最经典的玩法,通过调用环信的 IM SDK 接口,可以快速创建聊天室。如果想根据自己业务需求对聊天室应用的 UI界面、弹幕消息、礼物打赏系统等进行自定义设计,最高效的方式则是使用环信的 ChatroomUIKit 。

文档地址:https://doc.easemob.com/uikit/chatroomuikit/ios/roomuikit_overview.html


环信 ChatroomUIKit v1.0.0,提供了 UIKit 的各种组件,可根据实际业务需求快速上线聊天室功能。通过该 UIKit,聊天室中的用户可实时交互,发送普通弹幕消息,并支持打赏消息和全局广播等功能。

环信ChatroomUIKit 亮点功能

1、模块化、组件化、API驱动三层架构—开发更灵活、自由

环信 ChatroomUIKit 采用模块化、组件化和 API 驱动的三层架构,提供高完成度和高自定义能力的聊天室解决方案。

  • 模块化:聊天室的整个页面被划分为不同的模块,例如弹幕区、输入区、成员列表和礼物列表等。我们可以自由重写每个模块,而不影响其他模块的可用性。
  • 组件化:每个模块都由多个组件组成。可以灵活定制每个组件,以满足个性化需求。
  • API 驱动:环信 ChatroomUIKit 提供了一套稳定可靠的 API,可用于自定义聊天室的各种属性。

2、全平台覆盖 跨平台开发更简易

在移动应用开发领域,React Native 和 Flutter 是两个备受欢迎的跨平台 UI 框架,业内很多 IM UIKit 目前还不支持上述两个跨平台框架,而环信 ChatroomUIKit 实现了突破!


在覆盖 iOS、Android、Web 三大原生平台的同时,环信ChatroomUIKit 还支持 Flutter 和 React Native 跨平台框架,助力开发者快速实现跨平台开发。

3、UI界面、功能组件可自定义、快速上线

环信 ChatroomUIKit 采用最新的 UI 框架和开发语言,可以快速上手。同时,该 UIKit 对标国际主流的社交应用,提供开箱即用的社交组件,一方面,可以快速将其集成到自己的应用程序中,另一方面还支持对功能组件进行自定义,灵活定制符合自身需求的聊天室应用。

4、界面明暗主题快速切换

明暗主题界面是目前极为主流的 App 界面设置,环信 ChatroomUIKit 默认风格为明亮模式,支持明暗主题一键切换,我们可以简单快速地对界面中所有元素的明亮/暗黑风格进行设置,产出舒适的视觉体验。

  • 浅色主题

  • 深色主题

5、自定义弹幕消息

弹幕消息是聊天室最为核心的功能。环信 ChatroomUIKit 支持业内主流的消息样式,包括发送时间显示、用户身份标识、用户头像、昵称等元素,提供极为灵活的弹幕消息自定义能力。


还可以根据业务需要,通过开关控件控制所有元素的显示或隐藏,如,是否隐藏对话框中的用户头像和昵称等。同时,也可以对消息气泡、颜色、字体等属性进行灵活快速的调整。

6、完整的打赏模块

对于主播和直播平台而言,打赏是主要收入来源之一,是直播/社交类 App 的重要功能。环信 ChatroomUIKit 支持完整的打赏流程,包含礼物赠送和打赏消息两部分。支持自定义礼物的样式、名称和金额等属性,也能够拓展礼物类型,如普通观众和会员观众礼物。同时,还可以选择特殊的打赏消息样式,突出展示打赏行为。


7、丰富的消息管理功能 支持全局广播、消息翻译

环信 ChatroomUIKit 具备丰富的消息管理功能,例如,支持用户向 App 内所有聊天室在线观众发送消息、通知以及大额打赏等重要信息;支持聊天室内的消息撤回和消息举报功能,所有用户只能撤回自己发送的消息。


同时,为满足出海用户的业务需要,环信 ChatroomUIKit 还支持消息的翻译功能,用户可以将聊天室中的单条消息从一种语言转换成另一种语言。

除了以上功能,环信 ChatroomUIKit 还支持成员管理等更多功能,进一步了解咨询或体验Demo可参考 ChatroomUIKit文档中心

相关文档:

收起阅读 »

HTML常用布局标签:提升网页颜值!不可不知的HTML布局技巧全解析!

在HTML的世界里,一切都是由容器和内容构成的。容器,就如同一个个盒子,用来装载各种元素;而内容,则是这些盒子里的珍宝。理解了这一点,我们就迈出了探索HTML布局的第一步。在HTML中,布局标签主要用于控制页面的结构和样式。本文将介绍一些常用的布局标签及其使用...
继续阅读 »

在HTML的世界里,一切都是由容器和内容构成的。容器,就如同一个个盒子,用来装载各种元素;而内容,则是这些盒子里的珍宝。理解了这一点,我们就迈出了探索HTML布局的第一步。

在HTML中,布局标签主要用于控制页面的结构和样式。本文将介绍一些常用的布局标签及其使用方法,并通过代码示例进行演示。

一、理解布局的重要性

布局在我们前端开发中担任什么样的角色呢?想象一下,你面前有一堆散乱的积木,无序地堆放在那里。

Description

而你的任务,就是将这些积木按照图纸拼装成一个精美的模型。HTML布局标签的作用就像那张图纸,它指导浏览器如何正确、有序地显示内容和元素,确保网页的结构和外观既美观又实用。

下面我们就来看看在HTML中常用的基础布局标签有哪些,如何使用这些布局标签完成我们的开发目标。

二、常用的布局标签

1、div标签

div标签是一个块级元素,它独占一行,用于对页面进行区域划分。它可以包含其他HTML元素,如文本、图片、链接等。通过CSS样式可以设置div的布局和样式。

示例代码:

<!DOCTYPE html>
<html>
<head>
<style>
  .box {
    width: 200px;
    height: 200px;
    background-color: red;
  }
</style>
</head>
<body>

<div>这是一个div元素

</div>

</body>
</html>

运行结果:

Description

2、span标签

span标签是一个内联元素,它不独占一行,用于对文本进行区域划分。它主要用于对文本进行样式设置,如字体、颜色等。与div类似,span也可以包含其他HTML元素。
示例代码:

<!DOCTYPE html>
<html>
<head>
<style>
  .text {
    color: blue;
    font-size: 20px;
  }
</style>
</head>
<body>

<p>这是一个<span>span元素</span></p>

</body>
</html>

运行结果:

Description

3、table标签

table标签用于创建表格,它包含多个tr(行)元素,每个tr元素包含多个td(单元格)或th(表头单元格)元素。

<table> 定义一个表格,<tr> 定义表格中的行,而 <td> 则定义单元格。通过这三个标签,我们可以创建出整齐划一的数据表,让信息的展示更加直观明了。

需要注意的是:

  • <table></table>标记着表格的开始和结束。
  • <tr></tr>标记着行的开始和结束,几组表示该表格有几行。
  • <td></td>标记着单元格的开始和结束,表示这一行中有几列。

示例代码:

<!DOCTYPE html>
<html>
<head>
<style>
  table, th, td {
    border: 1px solid black;
  }
</style>
</head>
<body>
<table>
  <tr>
    <th>姓名</th>
    <th>年龄</th>
  </tr>
  <tr>
    <td>张三</td>
    <td>25</td>
  </tr>
  <tr>
    <td>李四</td>
    <td>30</td>
  </tr>
</table>
</body>
</html>

运行结果:

Description

4、form标签

<form>标签的主要作用是定义一个用于用户输入的HTML表单。这个表单可以包含各种输入元素,如文本字段、复选框、单选按钮、提交按钮等。

<form>元素可以包含以下一个或多个表单元素:<input><textarea><button><select><option><optgroup><fieldset><label><output>等。

示例代码:

<!DOCTYPE html>
<html>
<head>
<style>
  form {
    display: flex;
    flex-direction: column;
  }
</style>
</head>
<body>

<form>
  <label for="username">用户名:</label>
  <input type="text" id="username" name="username">
  <br>
  <label for="password">密码:</label>
  <input type="password" id="password" name="password">
  <br>
  <input type="submit" value="提交">
</form>

</body>
</html>

运行结果:
Description

5、列表标签

1)无序列表

  • 指没有顺序的列表项目
  • 始于<ul>标签,每个列表项始于<li>
  • type属性有三个选项:disc实心圆、circle空心圆、square小方块。 默认属性是disc实心圆。

示例代码:

<!DOCTYPE html>
<htmml>
<head>
<meta charst = "UTF-8">
<title>html--无序列表</title>
</head>
<body>
<ul>
<li>默认的无序列表</li>
<li>默认的无序列表</li>
<li>默认的无序列表</li>
</ul>
<ul>
<li type = "circle">添加circle属性</li>
<li type = "circle">添加circle属性</li>
<li type = "circle">添加circle属性</li>
</ul>
<ul>
<li type = "square">添加square属性</li>
<li type = "square">添加square属性</li>
<li type = "squaare">添加square属性</li>
</ul>
</body>
</html>

运行结果:
Description
也可以使用CSS list-style-type属性定义html无序列表样式。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!

2)有序列表

  • 指按照字母或数字等顺序排列的列表项目。
  • 其结果是带有前后顺序之分的编号,如果插入和删除一个列表项,编号会自动调整。
  • 始于<ol>标签,每个列表项始于<li>

示例代码:

<ol>
<li>默认的有序列表</li>
<li>默认的有序列表</li>
<li>默认的有序列表</li>
</ol>
<ol type = "a" start = "2">
<li>第1项</li>
<li>第2项</li>
<li>第3项</li>
<li value ="20">第四项</li>
</ol>
<ol type = "Ⅰ" start = "2">
<li>第1项</li>
<li>第2项</li>
<li>第3项</li>
</ol>

运行结果:
Description
同样也可以使用CSS list-style-type属性定义html有序列表样式。

3)自定义列表

  • 自定义列表不仅仅是一列项目,而是项目及其注释的组合。
  • <dl>标签开始。每个自定义列表项以<dt>开始。每个自定义列表项的定义以<dd>开始。
  • 用于对术语或名词进行解释和描述,自定义列表的列表项前没有任何项目符号。
    基本语法:
<dl>
<dt>名词1</dt>
<dd>名词1解释1</dd>
<dd>名词1解释2</dd>

<dt>名词2</dt>
<dd>名词2解释1</dd>
<dd>名词2解释2</dd>
</dl>

<dl>即“definition list(定义列表)”,
<dt>即“definition term(定义名词)”,
<dd>即“definition description(定义描述)”。

示例代码:

<dl>
<dt>计算机</dt>
<dd>用来计算的仪器</dd>

<dt>显示器</dt>
<dd>以视觉方式显示信息的装置</dd>
</dl>

运行结果:
Description
以上就是HTML中常用的布局标签及其使用方法。在实际开发中,还可以结合CSS和JavaScript来实现更复杂的布局和交互效果。

掌握了这些HTML常用布局标签,你已经拥有了构建网页的基础工具。记住,好的布局不仅需要技术,更需要创意和对细节的关注。现在,打开你的代码编辑器,开始你的布局设计之旅吧!

收起阅读 »

关于浏览器调试的30个奇淫技巧

web
这篇文章为大家介绍一下浏览器的调试技巧,可帮助你充分利用浏览器的调试器。 console.log 这个是日常开发中最常用的了如果不知道这个就不是程序员了,这里不多描述了 console.count 计算代码执行的次数他会自动累加 // 例如我代码执行了三次 c...
继续阅读 »

这篇文章为大家介绍一下浏览器的调试技巧,可帮助你充分利用浏览器的调试器。


console.log


这个是日常开发中最常用的了如果不知道这个就不是程序员了,这里不多描述了


console.count


计算代码执行的次数他会自动累加


// 例如我代码执行了三次
console.count() // 1 2 3
console.count('执行:') // 执行:1 执行:2 执行:3

console.table


例如,每当你的应用程序在调试器中暂停时,要将 localStorage 的数据转储出来,你可以创建一个 console.table(localStorage) 观察器:


image.png


关于 console.table 的就暂时介绍到这里,下边有一篇详细介绍 console 其他用法的有兴趣的童靴可以去看看。


# 如何优雅的在控制台中使用 console.log


DOM 元素变化后执行表达式


要在 DOM 变化后执行表达式,需要设置一个 DOM 变化断点(在元素检查器中):


image.png


然后添加你的表达式,例如记录 DOM 的快照:(window.doms = window.doms || []).push(document.documentElement.outerHTML)。现在,每当 DOM 子树发生修改时,调试器将暂停执行,并且新的 DOM 快照将位于 window.doms 数组的末尾。(无法创建不暂停执行的 DOM 变化断点。)


跟踪调用堆栈


假设你有一个显示加载动画的函数和一个隐藏加载动画的函数,但是在你的代码中,你调用了 show 方法却没有对应的 hide 调用。你如何找到未配对的 show 调用的来源?在 show 方法中使用条件断点中的 console.trace,运行你的代码,找到 show 方法的最后一个堆栈跟踪,并点击调用者来查看代码:


log1.gif


改变程序行为


通过在具有副作用的表达式上使用,我们可以在浏览器中实时改变程序行为。


例如,你可以覆盖 getPerson 函数的参数 id。由于 id=1 会被评估为 true,这个条件断点会暂停调试器。为了防止这种情况发生,可以在表达式末尾添加 false:


log2.gif


性能分析


你不应该在性能分析中混入诸如条件断点评估时间之类的内容,但如果你想快速而精确地测量某个操作运行所需的时间,你可以在条件断点中使用控制台计时 API。在你想要开始计时的地方设置一个带有条件 console.time('label') 的断点,在结束点设置一个带有条件 console.timeEnd('label') 的断点。每当你要测量的操作运行时,浏览器就会在控制台记录它花费了多长时间:


log3.gif


根据参数个数进行断点


只有当当前函数被调用时带有 3 个参数时才暂停:arguments.callee.length === 3


当你有一个具有可选参数的重载函数时,这个技巧非常有用:


log4.gif


当前函数调用参数数量错误时


只有当当前函数被调用时参数数量不匹配时才暂停:(arguments.callee.length) !== arguments.length在查找函数调用站点中的错误时很有用:


log5.gif


跳过页面加载统计使用时间


直到页面加载后的 5 秒后才暂停:performance.now() > 5000


当你想设置一个断点,但只有在初始页面加载后才对暂停执行感兴趣时,这个技巧很有用。


跳过 N 秒


如果断点在接下来的 5 秒内触发,则不要暂停执行,但在之后的任何时候都要暂停:


window.baseline = window.baseline || Date.now(), (Date.now() - window.baseline) > 5000


随时可以从控制台重置计数器:window.baseline = Date.now()


使用 CSS


基于计算后的 CSS 值暂停执行,例如,仅当文档主体具有红色背景时才暂停执行:window.getComputedStyle(document.body).backgroundColor === "rgb(255,0,0)"


仅偶数次调用


仅在执行该行的每一次的偶数次调用时暂停:window.counter = (window.counter || 0) + 1, window.counter % 2 === 0


抽样断点


只在该行执行的随机样本上断点,例如,仅在执行该行的每十次中断点一次:Math.random() < 0.1


Never Pause Here


当你右键点击边栏并选择“Never Pause Here”时,Chrome 创建一个条件断点,该断点的条件为 false,永远不会满足。这样做就意味着调试器永远不会在这一行上暂停。


image.png


image.png


当你想要豁免某行不受 XHR 断点的影响,忽略正在抛出的异常等情况时,这个功能非常有用。


自动实例 ID


通过在构造函数中设置条件断点来为每个类的实例自动分配唯一 ID:(window.instances = window.instances || []).push(this)


然后通过以下方式检索唯一 ID:window.instances.indexOf(instance)(例如,在类方法中,可以使用 window.instances.indexOf(this))。


Programmatically Toggle


使用全局布尔值来控制一个或多个条件断点的开关。


image.png


然后通过编程方式切换布尔值,例如:



  • 在控制台手动切换布尔值


window.enableBreakpoints = true;


  • 从控制台上的计时器切换全局布尔值:


setTimeout(() => (window.enableBreakpoints = true), 5000);


  • 在其他断点处切换全局布尔值:


image.png


monitor() class Calls


你可以使用 Chromemonitor 命令行方法来轻松跟踪对类方法的所有调用。例如,给定一个 Dog 类:


class Dog {  
bark(count) {
/* ... */
}
}

如果我们想知道对 的所有实例进行的所有调用Dog,请将其粘贴到命令行中:


var p = Dog.prototype;  
Object.getOwnPropertyNames(p).forEach((k) => monitor(p[k]));

你将在控制台中得到输出:



function bark called with arguments: 2



如果您想暂停任何方法调用的执行(而不是仅仅记录到控制台),您可以使用debug而不是monitor


特定实例


如果不知道该类但有一个实例:


var p = instance.constructor.prototype;  
Object.getOwnPropertyNames(p).forEach((k) => monitor(p[k]));

当想编写一个对任何类的任何实例执行此操作的函数(而不仅仅是Dog)时很有用


调用和调试函数


在控制台中调用要调试的函数之前,请调用debugger. 例如给出:


function fn() {  
/* ... */
}

从控制台发现:



debugger; fn(1);



然后“Step int0 next function call”来调试fn.


当不想手动查找定义fn并添加断点或者fn动态绑定到函数并且不知道源代码在哪里时很有用。


Chrome 中,还可以选择debug(fn)在命令行上调用,调试器将fn在每次调用时暂停内部执行。


URL 更改时暂停执行


在单页应用程序修改 URL 之前暂停执行(即发生某些路由事件):


const dbg = () => {  
debugger;
};

history.pushState = dbg;
history.replaceState = dbg;
window.onhashchange = dbg;
window.onpopstate = dbg;

创建一个暂停执行而不中断导航的版本dbg是留给读者的练习。


另请注意,当代码直接调用时,这不会处理window.location.replace/assign,因为页面将在分配后立即卸载,因此无需调试。如果您仍然想查看这些重定向的来源(并调试重定向时的状态),在 Chrome 中您可以使用debug相关方法:


debug(window.location.replace);  
debug(window.location.assign);

读取属性


如果有一个对象并且想知道何时读取该对象的属性,请使用对象 getter 进行调用debugger。例如,转换{configOption: true}{get configOption() { debugger; return true; }}(在原始源代码中或使用条件断点)。


将一些配置选项传递给某些东西并且想了解它们如何使用时,这很有用。


copy()


可以使用控制台 API 将感兴趣的信息从浏览器直接复制到剪贴板,而无需进行任何字符串截断copy()。您可能想要复制一些有趣的内容:



  • 当前 DOM 的快照:copy(document.documentElement.outerHTML)

  • 有关资源的元数据(例如图像):copy(performance.getEntriesByType("resource"))

  • 一个大的 JSON blob,格式为:copy(JSON.parse(blob))

  • 本地存储的转储:copy(localStorage)


在禁用 JS 的情况下检查 DOM


DOM 检查器中按 ctrl+\ (Chrome/Windows) 可随时暂停 JS 执行。这允许检查 DOM 的快照,而不必担心 JS 改变 DOM 或事件(例如鼠标悬停)导致 DOM 从下方发生变化。


检查难以捉摸的元素


假设您想要检查仅有条件出现的 DOM 元素。检查所述元素需要将鼠标移动到它,但是当你尝试这样做时,它就会消失:


log6.gif
要检查该元素,您可以将其粘贴到控制台中setTimeout(function() { debugger; }, 5000);:这给了你 5 秒的时间来触发 UI,然后一旦 5 秒计时器到了,JS 执行就会暂停,并且没有任何东西会让你的元素消失。您可以自由地将鼠标移动到开发工具而不会丢失该元素:


log7.gif


当 JS 执行暂停时,可以检查元素、编辑其 CSS、在 JS 控制台中执行命令等。


在检查依赖于特定光标位置、焦点等的 DOM 时很有用。


记录DOM


要获取当前状态下 DOM 的副本:


copy(document.documentElement.outerHTML);

每秒记录一次 DOM 快照:


doms = [];

setInterval(() => {

const domStr = document.documentElement.outerHTML;

doms.push(domStr);

}, 1000);

或者将其转储到控制台:


setInterval(() => {

const domStr = document.documentElement.outerHTML;

console.log("snapshotting DOM: ", domStr);

}, 1000);

监控元素


(function () {  
let last = document.activeElement;
setInterval(() => {
if (document.activeElement !== last) {
last = document.activeElement;
console.log("Focus changed to: ", last);
}
}, 100);
})();

log8.gif


寻找元素


const isBold = (e) => {
let w = window.getComputedStyle(e).fontWeight;
return w === "bold" || w === "700";
};
Array.from(document.querySelectorAll("*")).filter(isBold);

或者只是当前在检查器中选择的元素的后代:


Array.from($0.querySelectorAll("*")).filter(isBold);

获取事件监听器


在 Chrome 中,可以检查当前所选元素的事件侦听器:getEventListeners($0),例如:


image.png


监听元素事件


调试所选元素的所有事件:monitorEvents($0)


调试所选元素的特定事件:monitorEvents($0, ["control", "key"])


log9.gif



点赞收藏支持、手留余香、与有荣焉,动动你发财的小手哟,感谢各位大佬能留下您的足迹。



11.png


往期热门精彩推荐



# 2024最新程序员接活儿搞钱平台盘点


解锁 JSON.stringify() 5 个鲜为人知的功能


解锁 JSON.stringify() 7 个鲜为人知的坑


如何去实现浏览器多窗口互动



面试相关热门推荐



前端万字面经——基础篇


前端万字面积——进阶篇


简述 pt、rpx、px、em、rem、%、vh、vw的区别



实战开发相关推荐



前端常用的几种加密方法


探索Web Worker在Web开发中的应用


不懂 seo 优化?一篇文章帮你了解如何去做 seo 优化


【实战篇】微信小程序开发指南和优化实践


前端性能优化实战


聊聊让人头疼的正则表达式


获取文件blob流地址实现下载功能


Vue 虚拟 DOM 搞不懂?这篇文章帮你彻底搞定虚拟 DOM



移动端相关推荐



移动端横竖屏适配与刘海适配


移动端常见问题汇总


聊一聊移动端适配



Git 相关推荐



通俗易懂的 Git 入门


git 实现自动推送



更多精彩详见:个人主页


作者:StriveToY
来源:juejin.cn/post/7345297230201716776
收起阅读 »

从发送短信验证码来研究几种常用的防刷策略

大家好,我是小趴菜,最近在做项目的时候有个发送短信验证码的需求,这个需求在大部分的项目中相信都会使用到,而发送短信验证码是需要收费的,所以我们要保证我们的接口不能被恶意刷, 1:前端控制 前端控制是指在用户点击发送验证码之后,在一分钟之内这个按钮就置灰,让用户...
继续阅读 »

大家好,我是小趴菜,最近在做项目的时候有个发送短信验证码的需求,这个需求在大部分的项目中相信都会使用到,而发送短信验证码是需要收费的,所以我们要保证我们的接口不能被恶意刷,


1:前端控制


前端控制是指在用户点击发送验证码之后,在一分钟之内这个按钮就置灰,让用户无法再次发起,这种方式有什么优点和缺点呢?


优点



  • 1: 实现简单,直接让前端进行控制


缺点



  • 1:安全性不够,别人完全可以绕过前端的控制,直接发起调用,这种方式只能作为防刷的第一道屏障


2:redis + 过期时间


在用户发送验证码之后,将用户的手机号作为redis的KEY,value可以设置为任意值,并且将该KEY的过期时间设置为1分钟,实现流程如下:



  • 1:用户客户端发起发送验证码

  • 2:后端收到请求以后,将该用户的手机号作为KEY,VALUE设置为任意值,并且是过期时间为1分钟

  • 3:当用户下次发起发送验证码请求,后端可以根据用户手机号作为KEY,从Redis中获取,如果这个KEY不存在,说明已经过去1分钟了,可以再次发送验证码

  • 4:如果这个KEY存在,说明这个用户在一分钟内这个用户已经发送过了,就提示用户一分钟后再试


那么这种方式又有什么优点和缺点呢???


优点



  • 1:实现简单

  • 2:由后端控制,安全性比前端控制高


缺点



  • 1:首先需要依赖Redis

  • 2:一分钟后这个KEY真的能被准时删除吗????


针对第2点我们深入分析下,正常来说,一个Redis的KEY,设置了1分钟过期时间,那么在1分钟后这个KEY就会被删除,所以这种redis+过期时间在正常情况下是可以满足防刷的,但是Reids真的能帮我们准时的删除这个KEY吗?


在此我们不得不了解下Redis的删除策略了,redis有三种删除策略



  • 1:定时删除:会给这个KEY设置一个定时器,在这个KEY的过期时间到了,就会由定时器来删除这个KEY,优点是可以快速释放掉内存,缺点就是会占用CPU,如果在某个点有大量的KEY到了过期时间,那么此时系统CPU就会被沾满

  • 2:惰性删除:当这个KEY过期了,但是不会自动释放掉内存,而是当下次有客户端来访问这个KEY的时候才会被删除,这样就会存在一些无用的KEY占用着内存

  • 3:定期删除:redis会每隔一段时间,随机抽取一批的KEY,然后把其中过期的KEY删除


如果reids设置的删除策略是定期删除,那么你这个KEY即使到了过期时间也不会被删除,所以你还是可以在Redis中获取到,这个时候客户端明明已经过了一分钟了,但是你还是能拿到这个KEY,所以这时候又会被限制发送验证码了,这明显不符合业务需求了


所以一般会采用惰性删除+定期删除的方式来实现,这样,即使定期删除没有删除掉这个KEY,但是在访问的时候,会通过惰性删除来删除掉这个KEY,所以这时候客户端就访问不到这个KEY,就可以实现一分钟内再次发送验证码的请求了


但是如果你的Redis是做了读写分离的,也就是写操作是写主,查询是从,那么这时候会有什么问题呢?


我们在设置Redis的过期时间有四种命令



  • 1:expire:从当前时间算起,过了设置的时间以后就过期

  • 2:pexpire:同expire,只是过期时间的单位不一样

  • 3:expireAt:设置未来的某个时间,当系统时间到了这个点之后就过期

  • 4:pexpireAt:同expireAt,只是过期时间单位不一样


如果我们使用的是expire命令来设置时间,redis主从同步是异步的,那么在这期间一定会有时间差,当主同步到从的时候,可能已经过去十几秒都有可能,那么这时候从redis收到这个KEY以后,是从当前时间开始算起,然后过去指定的时间以后才会过期,所以这时候主redis这个KEY过期了,但是从redis这个KEY可能还有十几秒以后才会过期


这时候你查的是从Redis,所以还是可以查到这个KEY的,这时候客户端其实已经过去一分钟了,但是由于你能从Redis查到这个KEY,所以客户端还是不能发送验证码


这时候我们可以使用expireAt命令来设置,只要系统到了这个时间点,这个KEY就会被删除,但是前提是要保证主从Redis系统的时间一致,如果你从库的时间比主库晚了几分钟,那么从库这个KEY存活的时间就会比主Redis存活的时间更长,那么这样也会有问题


redis + 特殊VALUE + 过期时间


这种的业务流程如下



  • 1:用户客户端发起发送验证码

  • 2:后端收到请求以后,将该用户的手机号作为KEY,VALUE设置为当前时间戳(重点)

  • 3:当用户下次发起发送验证码请求,后端可以根据用户手机号作为KEY,从Redis中获取,如果这个KEY不存在,可以再次发送验证码

  • 4:如果这个KEY存在,获取到这个KEY的VALUE,然后判断当前时间戳跟这个KEY的时间戳是否超过1分钟了,如果超过了就可以再次发送,如果没有就不能发送了


这种方式与其它几种方式的优点在哪呢?


无论你这个KEY有没有准时被删除,删除了说明可以发送,即使因为某些原因没有被删除,那么我们也可以通过设置的VALUE的值跟当前时间戳做一个比较。所以即使出现了上面 redis + 过期时间会出现的问题,那么我们也可以做好相应的判断,如果你过去一分钟还能拿到这个KEY,并且比较时间戳也已经超过一分钟了,那么我们可以重新给这个KEY设置VALUE,并且值为当前时间戳,就不会出现以上的几种问题了。


结尾


题外话,其实KEY即使时间到期了,但是我们还是能查到这个KEY,除了之前说的几个点,还有几种情况也会出现,Redis删除KEY是需要占用CPU的,如果此时你系统的CPU已经被其它进程占满了,那么这时候Redis就无法删除这个KEY了


作者:我是小趴菜
来源:juejin.cn/post/7341300805281087514
收起阅读 »

get请求参数放在body中?

web
1、背景 与后端对接口时,看到有一个get请求的接口,它的参数是放在body中的 ******get请求参数可以放在body中?? 随即问了后端,后端大哥说在postman上是可以的,还给我看了截图 可我传参怎么也调不通! 下面就来探究到底是怎么回事 2、...
继续阅读 »

1、背景


与后端对接口时,看到有一个get请求的接口,它的参数是放在body中的



******get请求参数可以放在body中??


随即问了后端,后端大哥说在postman上是可以的,还给我看了截图



可我传参怎么也调不通!


下面就来探究到底是怎么回事


2、能否发送带有body参数的get请求


项目中使用axios来进行http请求,使用get请求传参的基本姿势:


// 参数拼接在url上
axios.get(url, {
params: {}
})

如果想要将参数放在body中,应该怎么做呢?


查看axios的文档并没有看到对应说明,去github上翻看下axios源码看看


lib/core/Axios.js文件中



可以看到像deletegetheadoptions方法,它们只接收两个参数,不过在config中有一个data



熟悉的post请求,它接收的第二个参数data就是放在body的,然后一起作为给this.request作为参数


所以看样子get请求应该可以在第二个参数添加data属性,它会等同于post请求的data参数


顺着源码,再看看lib/adapters/xhr.js,上面的this.request最终会调用这个文件封装的XMLHttpRequest


export default isXHRAdapterSupported && function (config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
let requestData = config.data

// 将config.params拼接在url上
request.open(config.method.toUpperCase(),
buildURL(fullPath, config.params, config.paramsSerializer), true);

// 省略若干代码
...

// Send the request
request.send(requestData || null);
});
}

最终会将data数据发送出去


所以只要我们传递了data数据,其实axios会将其放在body发送出去的


2.1 实战


本地起一个koa服务,弄一个简单的接口,看看后端能否接收到get请求的body参数


router.get('/api/json', async (ctx, next) => {
console.log('get请求获取body: ', ctx.request.body)

ctx.body = ctx.request.body
})

router.post('/api/json', async (ctx, next) => {
console.log('post请求获取body: ', ctx.request.body)

ctx.body = ctx.request.body
})

为了更好地比较,分别弄了一个getpost接口


前端调用接口:


const res = await axios.get('/api/json', {
data: {
id: 1,
type: 'GET'
}
})


const res = await axios.post('/api/json', {
data: {
id: 2,
type: 'POST'
}
})
console.log('res--> ', res)

axiossend处打一个断点



可以看到数据已经被放到body中了


后端已经接收到请求了,但是get请求无法获取到body



结论:



  • 前端可以发送带body参数的get请求,但是后端接收不到

  • 这就是接口一直调不通的原因


3、这是为何呢?


我们查看WHATGW标准,在XMLHttpRequest中有这么一个说明:



大概意思:如果请求方法是GETHEAD ,那么body会被忽略的


所以我们虽然传递了,但是会被浏览器给忽略掉


这也是为什么使用postman可以正常请求,但是前端调不通的原因了


因为postman并没有遵循WHATWG的标准,body参数没有被忽略



3.1 fetch是否可以?


fetch.spec.whatwg.org/#request-cl…


答案:也不可以,fetch会直接报错



总结



  1. 结论:浏览器并不支持get请求将参数放在body

  2. XMLHTTPRequest会忽略body参数,而fetch则会直接报错


作者:蝼蚁之行
来源:juejin.cn/post/7283367128195055651
收起阅读 »

震惊:苹果手机电池栏“黑白无常”

iOS
前言: 当程序员👨🏻‍💻遇到难以解决的bug时,大家都会说同样的口头禅:真是见了鬼了(建国后不可以) 现象: 手机电池栏左黑右白,如下图    👈🏻左边的时间是黑色的字体,右边的信号和电池是白色的字体👉🏻,这种感觉就像电池栏在呼喊: 我与你之...
继续阅读 »

前言:



当程序员👨🏻‍💻遇到难以解决的bug时,大家都会说同样的口头禅:真是见了鬼了(建国后不可以)



现象:



手机电池栏左黑右白,如下图













👈🏻左边的时间是黑色的字体,右边的信号和电池是白色的字体👉🏻,这种感觉就像电池栏在呼喊:


我与你之间虽只差一个灵动岛的距离,却已是黑白相隔


心路历程:


初步断定应该是UIStatusBarStyle的设置问题,查看App的infoplist文件发现确实有 View controller-based status bar appearance = YES的相关设置,有特殊需要的界面就需要自己手动处理一下


- (UIStatusBarStyle)preferredStatusBarStyle {
if (@avaliable(iOS 13.0,*)) {
return XXXX;
} else {
return XXXXX;
}
return XXXXXXX;
}

但是本着谁污染谁治理的原则,我没有特殊的场景我不处理,别的地方设置了也不应该影响我吧。再退一步来说,就算影响了,也不应该给我显示成这种左黑右白的鬼样子吧。不过产品说这个功能很高级,可以保留。玩笑归玩笑,问题还是得解决。


解决方案:


最先想到的肯定是给出问题的界面实现一下 preferredStatusBarStyle,效果确实不错,解决了,如图:












先解决了问题上线再说,就像罗永浩说的:












但是这该死的求知欲天天折磨着我,直到今天在搞包体积的时候,脚本检测到这个大的背景图,发现是从左往右渐变加深的,难道和图片有关系?本着试一试的原则,把图片删除的同时并且把preferredStatusBarStyle的代码注释掉,竟然好了,不可思议:












找设计师要了不带渐变的图片,又尝试了一把












对比俩种情况不难发现:


•无背景图,系统的导航栏显示的是黑色


•有背景图,系统的导航栏显示的是白色


💡💡 是不是UIKit对导航栏背景图做了监听?目的是为了让用户可以清晰的看到电池栏的信息?


带着这个猜测,去看了下去年的WWDC,果然找到了答案:



在iOS17中,default样式会根据内容的深浅调整status bar的颜色。



由于没有手动处理preferredStatusBarStyle,而背景图又是从左到右渐变加深,所以电池栏显示成了左黑右白。


后语:


由此可见:


1、遇到难以解决的问题,把锅甩给系统bug是多么的机智🐶;


2、建国后还真的是:





吴京达咩是什么梗-抖音





参考链接:


developer.apple.com/videos/play…


作者:京东云开发者
来源:juejin.cn/post/7344710026853007394
收起阅读 »

js精度丢失的问题,重新封装toFixed()

web
js精度丢失的问题,重新封装toFixed() 最近项目中遇到一个问题,那就是用tofixed()保留位小数的时候出现问题;比如2.55.tofixed(1)的结果是2.5。在网上搜了以什么是什么toFixed用的是银行算法,大致了解了一下银行家算法,意思就是...
继续阅读 »

js精度丢失的问题,重新封装toFixed()


最近项目中遇到一个问题,那就是用tofixed()保留位小数的时候出现问题;比如2.55.tofixed(1)的结果是2.5。在网上搜了以什么是什么toFixed用的是银行算法,大致了解了一下银行家算法,意思就是四舍五入的话,如的情况有五种,舍的情况只有四种,所以5看情况是舍还是入。


然而事实并不是什么银行家算法,而是计算的二进制有关,计算机在存储数据是以二进制的形式存储的整数存储倒是没有问题的,但是小数就容易出问题了,就比如0.1的二进制是0.0001100110011001100110011001100110011001100110011001101...无限循环的但是计算保存的时候肯定是有长度限制的,到你使用的时候计算会做一个近似处理


如下图:


企业微信截图_20230824140406.png
那我再看一下2.55的是啥样子的吧


企业微信截图_20230824140555.png
现在是不是很容易理解了为什么2.55保留一位小数是2.5而不是2.6了吧


同时计算机在保留二进制的时候也会存在进位和舍去的
所以这也是解释了一下为什么0.1+0.2不等于0.3,因为0.1和0.2本来存储的就不是精确的数字,加在一起就把误差放大了,计算就不知道你是不是想要的结果是0.3了,但是同样的是不精确是数字加一起确实正确的就比如0.2和0.3


见下图:


企业微信截图_20230824141334.png
这是为什么呢,因为计算存储的时候有进有啥,一个进一个舍两个相加就抵消了。


知道原因了该怎么解决呢?


那就是不用数字,而是用字符串。如果你涉及到一些精确计算的话可以用到一些比较成熟的库比如math.js或者# decimal.js。我今天就是对toFixed()重新封装一下,具体思路就是字符串加整数之间的运算,因为整数存储是精确(不要扛啊 不要超出最大安全整数)



export function toFixed(num, fixed = 2) {//fixed是小数保留的位数
let numSplit = num.toString().split('.');
if (numSplit.length == 1 || !numSplit[1][fixed] || numSplit[1][fixed] <= 4) {
return num.toFixed(fixed);
}
function toFixed(num, fixed = 2) {
let numSplit = num.toString().split(".");
if (
numSplit.length == 1 ||
!numSplit[1][fixed] ||
numSplit[1][fixed] <= 4
) {
return num.toFixed(fixed);
}
numSplit[1] = (+numSplit[1].substring(0, fixed) + 1 + "").padStart( fixed,0);
if (numSplit[1].length > fixed) {
numSplit[0] = +numSplit[0] + 1;
numSplit[1] = numSplit[1].substring(1, fixed + 1);
}
return numSplit.join(".");
}
if (numSplit[1].length > fixed) {
numSplit[0] = +numSplit[0] + 1;
numSplit[1] = numSplit[1].substring(1, fixed + 1);
}
return numSplit.join('.');
}

文章样式简陋,但是干货满满。说的不对的,希望大家指正。


作者:Pangchengqiu12
来源:juejin.cn/post/7270544537671598114
收起阅读 »

从MariaDB衰败思考国产数据库品牌力建设

前文 10年前我非常好奇这个长得和MySQL一模一样的东西的发展,直到今天有了答案,MariaDB股价大跌,陷入财务危机,没有营收,砍产品,裁员,更换管理层,借债等等。 MariaDB曾经有一个InnoDB加强版的说法,但是从开发者的视角,没有必要关心里面是什...
继续阅读 »

前文


10年前我非常好奇这个长得和MySQL一模一样的东西的发展,直到今天有了答案,MariaDB股价大跌,陷入财务危机,没有营收,砍产品,裁员,更换管理层,借债等等。


MariaDB曾经有一个InnoDB加强版的说法,但是从开发者的视角,没有必要关心里面是什么。 即使page head加强、page tail优化,page directory综合做了完善的更改开发者只关心它在应用业务性能上有没有体现,使用是不是与MySQL一模一样。


从WEB应用体验,MariaDB与MySQL做比较,MariaDB的使用与MySQL的外模式和内模式一样。外模式指MySQL常用的SQL语句【等值查询、范围查询、聚合查询、窗口查询、两表关联、多表关联】,内模式指MySQL内部的元数据信息【数据字典、动态视图、静态视图】。


几乎可以把MariaDB当成套壳的MySQL,以此为镜, 300多个国产数据库可以感受到自由市场的竞争残酷。截至2月份, dbegines排行13名的MariaDB处于分崩瓦解的边缘。它给国产数据库的启迪是什么?我们怎么去发展建设国产数据库品牌。


image.png


千年前,明太祖朱元璋征求学士朱升对他平定天下战略方针的意见,朱升说:高筑墙,广积粮,缓称王。国产数据库可以遵从此道 ,高筑墙建立自己的品牌壁垒,广积粮营造自己的商业生态 。


从数据库的本质为应用服务、目标人群是商业实体,使用者多是开发者、DBA,我想到的三点有助于数据库品牌的建设。


协助建设应用


从目标利用的角度,天下应用分为以下三种系统建设,数据库作为系统中的核心服务,始终是为应用服务,应用是最终的变现。


信息系统建设方案: 传统的应用建设,主要是将现象生活反映到电子流,往往是单条业务流程整合,包括企业流程制度、企业控制管理、员工权限授权访问,常说的烟囱系统建设以及企业信息系统以及ERP、CRM、OA、交易系统、分析系统都属于这个范畴。此处数据架构较简单。


大数据系统建设方案: 此不仅仅包括我们常说的大数据建设,该应用建设表现往往高并发、高吞吐、低延迟、快响应,有时候需要整合较多的数据源,将集成较多的数据集,主要与业务系统联通或者其它设备的数据汲取过来,通过清洗、整合、编排后,输出一个错落有致、规范得体的数据指标,再反哺业务系统。用户跟踪、业务监控管理、实时需求预测都属于大数据系统的建设方案范围,主要它是能整合不同的数据,此处数据架构较复杂。


智能系统建设方案: 该系统建设属于高端信息应用范畴,需要智能算法以及更有效率的计算框架,包括音、视频、 边缘计算 、AI、 大模型AIGC等等,同时也包括基本的信息系统建设方案大数据系统建设方案,智能系统建设是应用优化的永无止境的追求。主要表现是提供更加友好验证手段,以及更加便利的识别方法提供相应的服务,一般智能系统会搭承其它技术手段完成客户端需求的闭环。


目前信创改造以及国家信息发展项目,大部分以信息系统建设方案大数据系统建设方案居多,所有的国内厂商创收盈利来自此处。因为有逐利的目的性,厂商有目标奔向这些项目。


但是对一些开源信息系统的适配,没有厂商愿意接入,因为没有任何利润可言。例如wordpress目前只支持MySQL,从技术上的改造,这个是简单的,但是没有人愿意做这样的事。大部分国内数据库厂商已经完成国内头部ERP厂商的适配兼容,数据库兼容wordpress是轻而易举的事情。


数据库厂商可以试着从基于利润创收的目的性适配应用,切换到主动适配发展应用创新的模式


信息系统建设方案来看,目前国内大多的开源系统底层使用MySQL,这个在侧面反映在中国MySQL为什么比postgreSQL火的原因,postgreSQL在世界很流行,但是在中国普及不如MySQL。


大数据系统建设方案较复杂,单一的数据库无法解决所有的问题,但是可以作为方案其中的一个实力支撑点,目前许多厂商都与上下游产品已经完成适配兼容, 但是没有脍炙人口的成熟的技术栈自成一套的解决方案。


例如某管理系统平台的技术栈如下, Hadoop +Hive +Kafka +Zookeeper + HBase +Sqoop +Flume + 国产厂商产品,国产厂商产品的位置也许随便用国际主流数据库产品就可以替代。为什么还用不熟悉的国产厂商产品?


这里需要宣传,需要推广,需要实践使用,需要生根发芽,了解国际的现状,认清与国际的差距,他们能做的,我们也能做。


这是文化使用习惯的问题,同样是碳酸水,可口可乐和百事可乐已经是有口皆碑,国内的炭酸水的味道和质量不比可乐差,为什么销量比不过这两家。其中一个原因,可乐不仅在饮料行业生根发展,而且在食谱行业也有建树。例 如可乐鸡翅,可乐不仅可以用于消暑解渴,而且可以用于烹饪食物。


严格科学上来说,可乐是越喝越渴的,根本不能够解渴,但是可乐的口感还不错,价格还可以,人们就建立喝可乐可以解渴的信仰。这信仰一时三刻撼动不了。国产可乐要摇动 可口可乐和百事可乐的位置,长期宣传推广是必不可少,多搞点可乐鸡翅、可乐鸭掌是根本,可乐应用于烹饪,可乐应用于清洁用途,可乐应用于除味。


智能系统建设方案这里比较特别,传统数据库与智能应用的依赖相对弱,一些数据库厂为了加强核心能力,已经推出向量型数据库。标准的现代化的数据库的基本能力,无论是结构化数据、半结构化数据、全结构化数据都可以在库中进行处理。


事实上智能系统建设方案更偏重应用端的处理能力,识别特征、应用模型、计算方式非常重要,这些都不是数据库能够兜底的,真正的智能系统建设方案可以不靠虑数据库产品选型,但是数据持久化落地,必定要选择其一款数据库。


智能系统建设方案应用场景比 信息系统建设方案大数据系统建设方案更加变幻莫测。


信息系统建设方案建设重点,数据库作为数字基座提供能力如下,包括系统稳定、高并发访问性能、交易查询请求功能、各种业务函数、友好的使用方式 , 满足项目级、部门级应用发展的需求。


大数据系统建设方案方面,数据库原有的基础能力新增,包括分布式处理能力、多模型构建能力、事务交易同体能力、内外数据集成能力 、动态伸缩能力 ,满足跨部门的集团级应用发展的需求。


信息系统建设方案大数据系统建设方案的对比区别,一个是简单,一个复杂,而智能系统建设方案则是强调应用端的建设,服务端获取特征参数,在后端联调大量的模型,连接不同的数据库、知识库、文件系统做特征识别匹配,返回给客户。


那么智能系统建设方案 普及的应该如何建设?数据库厂商大力投入,带头组织活动,鼓励应用创新,推动金融智能应用、电信智能应用、制造业智能应用的发展,有助于提高厂商的品牌力,暴光厂商在智能系统建设方案 方面的实践经验,增强厂商的威望值。


笔者看来,信创及国产化大多属于信息系统建设方案和大数据系统建设方案智能系统建设方案则是未来社会发展的应用硬性需求,厂商可以大力投入。


建设产品的长宽高壁垒


产品品牌力犹如一个盒子,由长宽高组成, 长度不够、宽度不及、高度不深都会造成品牌力的短板,无法把客户的信心牢牢装在里面。


下面列出产品长度、产品宽度、产品深度相关的指标来量化一个国产数据库的综合实力,努力往这三方面发展应该是没有错的。知己知彼,百战百胜,产品长宽高是知己,知道自己已经做的事和即将要做的事。


产品长度重点考察应用案例



  • 确定行业头部企业覆盖范围【金融行业、通信行业、制造业】

  • 确定行业相关企业覆盖面积【企业个数】

  • 确定行业主流应用场景【生产、经营、交易、办公】

  • 确定应用场景影响范围【核心业务、辅助业务 、边缘业务】

  • 确定产品基于生产环境运行的稳定性【精确到年】

  • 确定产品具备满足未来业务几年的性能【数据量增长】

  • 确定产品利于业务展开运行的功能【产品功能与业务需求匹配度】


产品宽度重点考察生态适配



  • 软件上游 CPU、芯片、操作系统、文件系统

  • 软件下游 BI软件、数据集成软件、ORM框架、数据可视化

  • 与Oracle、Postgresql、MySQL的适配兼容

  • 基于产品基础上的二次github技术开源

  • 同类软件的适配兼容调度集成

  • 行业标准接口JDBC\ODBC的适配兼容

  • 建立合作伙伴战略关系

  • 打造用户的社区【成熟度评估】

  • 高校教育培养使用人群


产品深度重点考察产品是否与时俱进



  • 行业主流技术【向量化计算、向量化数据类型、边缘计算】

  • 社会趋势【区块链】

  • serverless平台

  • 云计算系列

  • 内置支持人工智能、机器学习、数据分析、业务函数

  • 高校实验室底层研究


构建购买者和使用者的体验闭环


知己知彼,百战百胜,构建体验是知彼,知道对方对产品的感受以及使用者的反馈。


数据库作为一个商品,处处展现着生产者、购买者、使用者的关系。 生产者【厂商 】根据市场上的需求,丈量自家的研发实力开发了这么一个商品,当务之急就是要找到客户【购买者】,客户选择商品的背后有1000个原因。成交之后,使用者进场。购买者与使用者之间是甲方与乙方的关系,乙方根据产品的使用说明,使用相关解决解业务中的一系列问题。


厂商做的事情,包括引起购买者的兴趣,千方百计使用各种营销方式或者推广手段使购买者甘心付费成单,并且使用使用者的产品使用,包括质量反馈和功能缺点


购买者体验


商品列入购买者的采购清单范围 ,可能原因是是产品的【协助建设应用】和【长宽高壁垒】做了大量铺垫,举例某产品应用于人工手势智能系统中,XX商品充当核心服务存储,XX商品应用于同行业的用户画像分析业务场景,或者XX商品联同BI软件已经在驾驶舱系统运行中。


在商品已经得到购买者的侧目,下一步是实践得到客户的肯定。下面的POC以及持久的技术支持将证明产品具备满足客户业务需求的功能和性能。


厂商对客户的进一步输出,可以量化为以下三个指标。依次为服务响应时间、竞品对应亮点、应用迁移经验


服务响应时间,包括解决具体问题花的时间,到达现场花的时间,响应客户问题花的时间,原则上客户提出的问题 和疑问都要第一时间解答, 为此企业会有相应的知识库储备 ,针对问题可以马上解答。


竞品对应亮点, 建立同类相关产品对标,并突出本产品优势、亮点,数据库产品大同小异,全部产品都有数据存储、管理、使用的功能,但是单机产品与分布式无法100%进行匹配度。可以参考**【协助建设应用】和【长宽高壁垒】。**


应用迁移经验,客户关心第数据底座搬迁后的稳定性,基于数据库的应用迁移工作包括 具体数据迁移、数据结构迁移、SQL相关迁移,保障迁移后是无缝迁移状态。


具体数据迁移主要包括数据一致性、数据数量是否同、数据内容是否同等、数据是否转换等等。


数据结构迁移主要包括表结构、数据类型、数据长度、存储过程、函数、同义词等等,基于新底座转换后运行逻辑流程是否与原来相同 。


SQL相关迁移主要指SQL语句以及SQL相关的SHELLl调用、ETL语句,ORM框架内置SQL,基于新底座转换后运行逻辑流程是否与原来相同。


使用者反馈


在客户正式成为购买者后,使用者成为高级产品反馈者,厂商收集使用者和项目的相关信息,同时也是在进行**【协助建设应用】【产品长宽高壁垒】**。


厂商对客户的进一步输出,可以量化为以下三个指标,包括项目业务背景,技术套件、文件使用、产品使用状况


业务背景,包括项目驱动因素,领导愿景、发展目标、立项初衷等等,根据这些已知信息,掌握项目的痛点和痒点,评估项目预期可以做到的效果。


技术套件,记录包括项目中原来的和准备用上的技术套件,包括前端、UI、ORM框架构、中间件、数据库等相关解决方案一切用上的技术工具。


文档使用,建立用户对文档使用过程中的体验,根据厂商的提供的资料,是否足够工程师独力完成任务,不需要与厂商 互动。


产品使用状况,构建用户与研发互动和反馈的平台系统和制度流程, 包括API使用、DEMO操作、无法解决的问题。


总结


总的来说,品牌力不可能一蹴而就。围绕三点协助建设应用、建设长宽高壁垒、构建购买者和使用者的体验闭环进行数据库品牌力建设,笔者也是泛泛而谈展开铺叙,细节方面可以有更多补充。


最后概括以协助建设应用、建设长宽高壁垒为基本点,以此为线,赋能项目成单,增进客户的信心,交付项目使用。同时从购买者体验、使用者反馈搜集信息和数据,促进产品良性发展以及项目拿下订单,周而复始,不断循环。


作者:大数据模型
来源:juejin.cn/post/7343557616332767266
收起阅读 »

AI编程已有公司纳入绩效,你的AI编程工具是什么?

自从ChatGPT带动全球AI热潮,AI席卷着各行各业。编程界也不例外,最出名的摸过OpenAI与GitHub联合开发的Github Copilot。Github Copilot带动了一大堆AI编程工具的出现。后来Github Copilot付费了,再加上网络...
继续阅读 »

自从ChatGPT带动全球AI热潮,AI席卷着各行各业。编程界也不例外,最出名的摸过OpenAI与GitHub联合开发的Github Copilot。Github Copilot带动了一大堆AI编程工具的出现。后来Github Copilot付费了,再加上网络方面的问题,在国内使用Github Copilot还是有一定门槛的。那么在国内有没有适合国内程序员使用的类Github Copilot产品呢?下面分享一下个人安装使用的一些AI编程插件:



p.s.以上的下载量与评分均只是plugins.jetbrains的marketplace数据,仅供参考。


基本AI编程工具的功能都差不多:



  • 代码补全:根据当前代码上下文自动补全代码。

  • 根据注释生成代码:根据注释描述生成相应的代码。

  • 方法和函数生成:根据方法名或函数名自动生成该方法或函数的代码。

  • 生成测试代码:生成测试代码。

  • ....


具体选择可以自己体验一下,国内用户可能试用下codegeex、aixcoder。


1、Github Copilot



GitHub Copilot是一款由GitHub和OpenAI共同开发的基于云的人工智能工具,它可以帮助开发者自动生成代码、分析代码、调试代码以及进行安全检测等。Copilot是基于OpenAI的GPT-4模型开发的,可以通过文本问答的方式与开发者进行交互。



  • 效果



生成代码文档



当然大家可能比较关心的是如果不花钱,免费使用Github Copilot。除了官方试用,另外在国内自然是有大神


去破解它。如果你破解过idea的话,再关注下那个大神的博客基本就可能以找到这个了。



国内到国外访问Copilot Chat,会时不时的会抽风。




  • 评分(3.0)




  • 下载量(7,476,947)



2、Codeium



Codeium是一款免费的智能编程助手,由美国一家公司开发。它提供了超过40种编程语言的代码完成工具。这个工具支持几乎所有主流的编程语言和IDE,特别适合个人用户使用。Codeium的主要功能包括自动完成代码,从样板文件到单元测试节省时间,以及使用自然语言问题搜索存储库。此外,它还提供了一个Playground体验,可以直接在浏览器上试用Codeium的功能。



  • 界面截图


安装后需要登录,不过国内打不了。我登录后使用的效果很一般,估计网络太差。。。




  • 评分(4.7)




  • 下载量(328,213)




  • 小结





    • 国内不怎么好用,登录不了




3、CodeGeeX(开源)



CodeGeeX是一个强大的基于LLM的智能编程助手。提供代码生成/补全、注释生成、代码翻译、AI聊天等功能,帮助开发人员大幅提升工作效率。CodeGeeX是由清华大学知识工程实验室研发。它支持20多种编程语言,并适配多种主流IDE。




  • 效果




  • 评分(3.6)




  • 下载量(258,540)



4、Amazon CodeWhisperer


亚马逊CodeWhisperer是AWS提供的一种AI驱动的工具,它帮助开发者更高效地编写代码。它提供实时代码建议,支持自然语言到bash的翻译,并能进行安全扫描。此外,它还包含了Amazon Q,这是一个交互式AI助手,能在IDE中提供专家指导和代码解释。CodeWhisperer设计用于多种编程语言和集成开发环境(IDE),并且可以定制以更好地理解您的内部库和API。


由于idea版本的问题没有安装上



不过下载量还是比较高,但评分就相当的低



  • 下载量(6,213,740)




  • 评分



5、Tabnine


Tabnine是一款AI编程助手,它通过实时代码补全、聊天和代码生成功能,帮助开发者提高编码效率。


Tabnine是由以色列的开发者创建的,它是一款基于人工智能的编程助手,旨在提高代码编写的效率和质量。


自2018年推出以来,Tabnine已经成为软件开发领域生成式AI技术的先驱,并且在全球范围内拥有超过一百万的月活跃用户。Tabnine支持所有流行的编程语言和IDE,能够根据您的代码和模式提供个性化的建议。它还提供私有部署选项,确保代码的安全性和合规性。



  • 效果



免费版只提供自动补全代码,别的功能需要付费



Tabnine Pro提供全线和全功能代码完成,这是免费版本中不包括的



  • 评分(3.9)




  • 下载量(3,528,356)



6、aiXcoder


aiXcoder是一款基于人工智能的软件开发助手,它通过深度学习技术提供代码自动生成、代码自动补全、代码智能搜索等功能,旨在提升开发者的开发效率和代码质量。



  • 效果




  • 评分(4.1)




  • 下载量(455,879)



7、ChatGTP-EasyCode


EasyCode是一个基于IntelliJ IDEA开发的代码生成插件,它通过自定义模板来快速生成Java、HTML、JS、XML等代码。这个工具特别适合Java程序员进行CRUD操作,因为它可以自动搭建MVC三层架构,从而减少重复性工作,提高开发效率。EasyCode支持与数据库相关的代码生成,并允许用户根据个人需求定制模板。



  • 效果


没有自己补全,但可以使用右键把机器人对话窗口调出来




  • 评分(4.0)




  • 下载量(727,619)



8、TONGYI Lingma(通义灵码)



通义灵码是阿里云推出的一款智能编码助手,它基于通义大模型,提供了一系列编程辅助功能,包括代码智能生成、研发智能问答、代码注释生成等。它能够根据当前代码文件及跨文件的上下文,为开发者生成行级或函数级的代码、单元测试和代码注释,从而提高编码效率和质量。通义灵码还支持自然语言生成代码,即开发者可以用自然语言描述想要的功能,通义灵码会根据描述生成相应的代码和注释。



  • 效果



右键效果




  • 评分(3.3)




  • 下载量(961,347)



作者:栈江湖
来源:juejin.cn/post/7344625552667476002
收起阅读 »

小程序手势冲突做不了?不存在的!

web
原生的应用经常会有页面嵌套列表,滚动列表能够改变列表大小,然后还能支持列表内下拉刷新等功能。看了很多的小程序好像都没有这个功能,难道这个算是原生独享的吗,难道是由于手势冲突无法实现吗,冷静的思考了一下,又看了看小程序的手势文档(文档地址),感觉我又行了。 实现...
继续阅读 »

原生的应用经常会有页面嵌套列表,滚动列表能够改变列表大小,然后还能支持列表内下拉刷新等功能。看了很多的小程序好像都没有这个功能,难道这个算是原生独享的吗,难道是由于手势冲突无法实现吗,冷静的思考了一下,又看了看小程序的手势文档(文档地址),感觉我又行了。


实现效果如下:


a.gif


页面区域及支持手势



  • 红色的是列表未展开时内容展示,无手势支持

  • 绿色部分是控制部分,支持上拉下拉手势,对应展开列表及收起列表

  • 蓝色列表部分,支持上拉下拉手势,对应展开列表,上拉下拉刷新等功能

  • 浅蓝色部分是展开列表后的小界面内容展示,无手势支持


原理实现


主要是根据事件系统的事件来自行处理页面应当如何响应,原理其实同原生的差不多。
主要涉及 touchstart、touchmove、touchend、touchcancel 四个


另外的scrollview的手势要借助于 scroll-y、refresher-enable 属性来实现。


之后便是稀疏平常的数学加减法计算题环节。根据不同的内容点击计算页面应当如何绘制显示。具体的还是看代码吧,解释起来又要吧啦吧啦了。



Talk is cheap, show me the code



代码部分


wxml


<!--index.wxml-->
<view>
<view class="header" style="opacity: {{headerOpacity}};height:{{headerHeight}}px;"></view>
<view
class="toolbar"
data-type="toolbar"
style="bottom: {{scrollHeight}}px;height:{{toolbarHeight}}px;"
catch:touchstart="handleToolbarTouchStart"
catch:touchmove="handleToolbarTouchMove"
catch:touchend="handleToolbarTouchEnd"
catch:touchcancel="handleToolbarTouchEnd">
</view>
<scroll-view
class="scrollarea"
type="list"
scroll-y="{{scrollAble}}"
refresher-enabled="{{scrollAble}}"
style="height: {{scrollHeight}}px;"
bind:touchstart="handleToolbarTouchStart"
bind:touchmove="handleToolbarTouchMove"
bind:touchend="handleToolbarTouchEnd"
bind:touchcancel="handleToolbarTouchEnd"
bindrefresherrefresh="handleRefesh"
refresher-triggered="{{refreshing}}"
>

<view class="item" wx:for="{{[1,2,3,4,5,6,7,8,9,0,1,1,1,1,1,1,1]}}">

</view>
</scroll-view>

<view
class="mini-header"
style="height:{{miniHeaderHeight}}px;"
wx:if="{{showMiniHeader}}">


</view>
</view>


ts


// index.ts
// 获取应用实例
const app = getApp<IAppOption>()

Component({
data: {
headerOpacity: 1,
scrollHeight: 500,
windowHeight: 1000,
isLayouting: false,
showMiniHeader: false,
scrollAble: false,
refreshing: false,
toolbarHeight: 100,
headerHeight: 400,
miniHeaderHeight: 200,
animationInterval: 20,
scrollviewStartY: 0,
},
methods: {
onLoad() {
let info = wx.getSystemInfoSync()
this.data.windowHeight = info.windowHeight
this.setData({
scrollHeight: info.windowHeight - this.data.headerHeight - this.data.toolbarHeight
})
},
handleToolbarTouchStart(event) {
this.data.isLayouting = true
let type = event.currentTarget.dataset.type
if (type == 'toolbar') {

} else {
this.data.scrollviewStartY = event.touches[0].clientY
}
},
handleToolbarTouchEnd(event) {
this.data.isLayouting = false

let top = this.data.windowHeight - this.data.scrollHeight - this.data.miniHeaderHeight - this.data.toolbarHeight
if (top > (this.data.headerHeight - this.data.miniHeaderHeight) / 2) {
this.tween(this.data.windowHeight - this.data.scrollHeight, this.data.headerHeight + this.data.toolbarHeight, 200)
} else {
this.tween(this.data.windowHeight - this.data.scrollHeight, this.data.miniHeaderHeight + this.data.toolbarHeight, 200)
}
},
handleToolbarTouchMove(event) {
if (this.data.isLayouting) {
let type = event.currentTarget.dataset.type
if (type=='toolbar') {
this.updateLayout(event.touches[0].clientY + this.data.toolbarHeight / 2)
} else {
if (this.data.scrollAble) {
return
} else {
this.updateScrollViewLayout(event.touches[0].clientY)
}
}
}
},
handleRefesh() {
let that = this
setTimeout(() => {
that.setData({
refreshing: false
})
}, 3000);
},
updateLayout(top: number) {
if (top < this.data.miniHeaderHeight + this.data.toolbarHeight) {
top = this.data.miniHeaderHeight + this.data.toolbarHeight
} else if (top > this.data.headerHeight + this.data.toolbarHeight) {
top = this.data.headerHeight + this.data.toolbarHeight
}
let opacity = (top - (this.data.miniHeaderHeight + this.data.toolbarHeight)) / (this.data.miniHeaderHeight + this.data.toolbarHeight)
let isReachTop = opacity == 0 ? true : false
this.setData({
scrollHeight: this.data.windowHeight - top,
headerOpacity: opacity,
showMiniHeader: isReachTop,
scrollAble: isReachTop
})
},
updateScrollViewLayout(offsetY: number) {
let delta = offsetY - this.data.scrollviewStartY
if (delta > 0) {
return
}
delta = -delta
if (delta > this.data.headerHeight - this.data.miniHeaderHeight) {
delta = this.data.headerHeight - this.data.miniHeaderHeight
}

let opacity = 1 - (delta) / (this.data.headerHeight - this.data.miniHeaderHeight)
let isReachTop = opacity == 0 ? true : false
this.setData({
scrollHeight: this.data.windowHeight - this.data.headerHeight - this.data.toolbarHeight + delta,
headerOpacity: opacity,
showMiniHeader: isReachTop,
scrollAble: isReachTop
})
},
tween(from: number, to: number, duration: number) {
let interval = this.data.animationInterval
let count = duration / interval
let delta = (to-from) / count
this.tweenUpdate(count, delta, from)
},
tweenUpdate(count: number, delta: number, from: number) {
let interval = this.data.animationInterval
let that = this
setTimeout(() => {
that.updateLayout(from + delta)
if (count >= 0) {
that.tweenUpdate(count-1, delta, from + delta)
}
}, interval);
}
},
})


less


/**index.less**/
.header {
height: 400px;
background-color: red;
}
.scrollarea {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background-color: blue;
}
.toolbar {
height: 100px;
position: fixed;
left: 0;
right: 0;
background-color: green;
}
.mini-header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 200px;
background-color: cyan;
}
.item {
width: 670rpx;
height: 200rpx;
background-color: yellow;
margin: 40rpx;
}

作者:xyccstudio
来源:juejin.cn/post/7341007339216732172
收起阅读 »

有了这篇文章,妈妈再也不担心我不会处理树形结构了!

web
本篇文章你将学习到。 什么是树形结构 一维树形结构 与 多维树形结构 的相互转化。 findTreeData,filterTreeData ,mapTreeData 等函数方法 帮助我们更简单的处理多维树形结构 基础介绍 有很多小白开发可能不知道什么树形结...
继续阅读 »

本篇文章你将学习到。



  1. 什么是树形结构

  2. 一维树形结构 与 多维树形结构 的相互转化。

  3. findTreeDatafilterTreeDatamapTreeData 等函数方法 帮助我们更简单的处理多维树形结构


基础介绍


有很多小白开发可能不知道什么树形结构。这里先简单介绍一下。直接上代码一看就懂


一维树形结构


[
  { id: 1, name: `Node 1`, pId: 0 },
  { id: 2, name: `Node 1.1`, pId: 1 },
  { id: 4, name: `Node 1.1.1`, pId: 2 },
  { id: 5, name: `Node 1.1.2`, pId: 2 },
  { id: 3, name: `Node 1.2`, pId: 1 },
  { id: 6, name: `Node 1.2.1`, pId: 3 },
  { id: 7, name: `Node 1.2.2`, pId: 3 },
  { id: 8, name: `Node 2`, pId: 0 },
  { id: 9, name: `Node 2.1`, pId: 8 },
  { id: 10, name: `Node 2.2`, pId: 8 },
]

多维树形结构


[
  {
     id: 1,
     name: `Node 1`,
     children: [
      {
         id: 2,
         name: `Node 1.1`,
         children: [
          { id: 4, name: `Node 1.1.1`, children: [] },
          { id: 5, name: `Node 1.1.2`, children: [] },
        ],
      },
      {
         id: 3,
         name: `Node 1.2`,
         children: [
          { id: 6, name: `Node 1.2.1`, children: [] },
          { id: 7, name: `Node 1.2.2`, children: [] },
        ],
      },
    ],
  },
  {
     id: 8,
     name: `Node 2`,
     children: [
      { id: 9, name: `Node 2.1`, children: [] },
      { id: 10, name: `Node 2.2`, children: [] },
    ],
  },
]

咋一看一维树形结构可能会有点蒙,但是看一下多维树形结构想必各位小伙伴就一目了然了吧。这时候再回头去看一维树形结构想必就很清晰了。一维树形结构就是用pId做关联 来将多维树形结构给平铺了开来。


多维树形结构也是我们前端在渲染页面时经常用到的一种数据结构。但是后台一般给我们的是一维树形结构,而且一维树形结构 也非常有助于我们对数据进行增删改查。所以我们就要掌握一维树形结构多维树形结构的相互转化。


前置规划


再我们进入一段功能开发之前,我们肯定是要设计规划一下,我们的功能架构。


配置项的添加


动态参数名


让我们看一下上面那个数组 很明显有三个属性 是至关重要的。id pIdchildren。可以说没有这些属性就不是树形结构了。但是后台给你的树形结构相关参数不叫这个名字怎么办?所以我们后续的函数方法就要添加一些配置项来动态的配置的属性名。例如这样


type TreeDataConfig = {
 /** 唯一标识 默认是id */
 key?: string
 /** 与父节点关联的唯一标识 默认是pId */
 parentKey?: string
 /** 查询子集的属性名 默认是children */
 childrenName?: string
 isTileArray?: boolean
 isSetPrivateKey?: boolean
}
const flattenTreeData = (treeData:any,config?: TreeDataConfig): T[] => {
   //Do something...
}

keyparentKeychildrenName解决了我们上述的问题。想必你也发现了 除了这些 还有一些其他的配置项。


其他配置项


isTileArray:这个是设置后续的一些操作方法返回值是否为一维树形结构


isSetPrivateKey:这个就是我们下面要说的内容了,是否在节点中添加私有属性。


私有属性的添加


这里先插播一条小知识。可能有的小伙伴会在别人的代码中看到这样一种命名方式 _变量名下划线加变量名,这样就代表了这是一个私有变量。那什么是私有变量呢?请看代码


const name = '张三'
const fun = (_name) =>{
   console.log(_name)
}
fun(name)

上述代码中函数的参数名我们就把他用_name 用来表示。_name就表示了 这个name属性是fun函数的私有变量。用于与外侧的name进行区分。下面我们要添加的私有属性亦是同理 用于与treeNode节点的其他属性进行区分


请继续观察上面的两个树形结构数组。我们会发现多维树形结构的节点中并没有pId属性。这对我们的一些业务场景来说是很麻烦的。因此我们就内置了一个函数 来专门添加这些有可能非常有用的属性。 来更好的描述 我们当前节点在这个树形结构中的位置。


/**
* 添加私有属性。
* _pId     父级id
* _pathArr 记录了从一级到当前节点的id集合。
* _pathArr 的length可以记录当前是多少层
* @param treeNode
* @param parentTreeNode
* @param treeDataConfig
*/
const setPrivateKey = (treeNode,parentTreeNode, config) => {
 const { key = `id` } = config || {}
 item._pId = parentInfo?.[key]
 item._pathArr = parentInfo?._pathArr ? [...parentInfo._pathArr, item[key]] : [item[key]]
}

一维树形结构 与 多维树形结构 的相互转化


一维树形结构转多维树形结构


/**
* 一维树形结构转多维树形结构
* @param tileArray 一维树形结构数组
* @param config 配置项(key,childrenName,parentKey,isSetPrivateKey)
* @returns 返回多维树形结构数组
*/
const getTreeData = (tileArray = [], config) => {
 const {
   key = `id`,
   childrenName = `children`,
   parentKey = `pId`,
   isSetPrivateKey = false,
} = config || {}
 const fun = (parentTreeNode) => {
   const parentId = parentTreeNode[key]
   const childrenNodeList = []
   copyTileArray = copyTileArray.filter(item => {
     if (item[parentKey] === parentId) {
       childrenNodeList.push({ ...item })
       return false
    }
     else {
       return true
    }
  })
   parentTreeNode[childrenName] = childrenNodeList
   childrenNodeList.forEach(item => {
     isSetPrivateKey && setPrivateKey(item, parentTreeNode, config)
     fun(item)
  })
}
 const rootNodeList = tileArray.filter(item => !tileArray.some(i => i[key] === item[parentKey]))
 const resultArr = []
 let copyTileArray = [...tileArray]
 rootNodeList.forEach(item => {
   const index = copyTileArray.findIndex(i => i[key] === item[key])
   if (index > -1) {
     copyTileArray.splice(index, 1)
     const obj = { ...item }
     resultArr.push(obj)
     isSetPrivateKey && setPrivateKey(obj, undefined, config)
     fun(obj)
  }
})
 return resultArr
};

多维树形结构转一维树形结构


/**
* 多维树形结构转一维树形结构
* @param treeData 树形结构数组
* @param config 配置项(key,childrenName,isSetPrivateKey)
* @returns 返回一维树形结构数组
*/

const flattenTreeData = (treeData = [], config) => {
 const { childrenName = `children`, isSetPrivateKey = false } = config || {};
 const result = [];

 /**
  * 递归地遍历树形结构,并将每个节点推入结果数组中
  * @param _treeData 树形结构数组
  * @param parentTreeNode 当前树节点的父节点
  */

 const fun = (_treeData, parentTreeNode) => {
   _treeData.forEach((treeNode) => {
     // 如果需要,为每个树节点设置私有键
     isSetPrivateKey && setPrivateKey(treeNode, parentTreeNode, config);
     // 将当前树节点推入结果数组中
     result.push(treeNode);
     // 递归地遍历当前树节点的子节点(如果有的话)
     if (treeNode[childrenName]) {
       fun(treeNode[childrenName], treeNode);
    }
  });
};

 // 从树形结构的根节点开始递归遍历
 fun(treeData);

 return result;
};

处理多维树形结构的函数方法


在开始的基础介绍中我们有提到过一维树形结构 有助于我们对数据进行增删改查。因为一维的树形结构可以很容易的使用的我们数组内置的一些 find filter map 等方法。这几个方法不知道小伙伴赶紧去补一补这些知识吧 看完了再回到这里。传送门


下面我们会介绍 findTreeDatafilterTreeDatamapTreeData 这三个方法。使用方式基本和find filter map原始数组方法一样。也有些许不一样的地方:



  1. 因为我们不是直接把方法绑定在原型上面的 所以不能直接 arr.findTreeData 这样使用。需要findTreeData (arr) 把多维树形结构数组当参数传进来。

  2. callBack函数参数返回有些许不同 。前两个参数一样 第三个参数为父级的详情 没有父级的返回undefined。

  3. filterTreeDatamapTreeData方法我们可以通过配置项中的isTileArray属性来设置返回的是一维树形结构还是多维树形结构


findTreeData


/**
* 筛选多维树形结构 返回查询到的第一个结果
* @param treeData 树形结构数组
* @param callBack 写入搜索条件 用法和数组的find方法一致(前两个参数一样 第三个参数为父级的详情 没有父级的返回undefined)
* @param config 配置项(key,childrenName,isSetPrivateKey)
* @returns 返回查询到的第一个结果
*/

const findTreeData = (treeData = [], callBack, config, parentTreeNode) => {
 // 定义配置项中的 childrenName 和 isSetPrivateKey 变量, 如果没有传入 config 则默认值为 {}
 const { childrenName = `children`, isSetPrivateKey = false } = config || {};

 // 遍历树形数据
 for (const treeNode of treeData) {
   // 当 isSetPrivateKey 为真时,为每个节点设置私有变量
   if (isSetPrivateKey) {
     setPrivateKey(treeNode, parentTreeNode, config);
  }
   // 如果 callBack 返回真, 则直接返回当前节点
   if (callBack?.(treeNode, treeData.indexOf(treeNode), parentTreeNode)) {
     return treeNode;
  }
   // 如果有子节点, 则递归调用 findTreeData 函数, 直到找到第一个匹配节点
   if (treeNode[childrenName]) {
     const dataInfo = findTreeData(treeNode[childrenName], callBack, config, treeNode);
     if (dataInfo) {
       return dataInfo;
    }
  }
}
};

filterTreeData


/**
* 筛选多维树形结构 返回查询到的结果数组
* @param treeData 树形结构数组
* @param callBack 写入搜索条件 用法和数组的filter方法一致(前两个参数一样 第三个参数为父级的详情 没有父级的返回undefined)
* @param config 配置项(key,childrenName,isTileArray,isSetPrivateKey)
* @returns 返回查询到的结果数组
*/

const filterTreeData = (treeData = [], callBack, config) => {
 const { childrenName = `children`, isTileArray = false, isSetPrivateKey = false } = config || {}; // 解构配置项
 const resultTileArr = []; // 用于存储查询到的结果数组
 const fun = (_treeData, parentTreeNode) => {
   return _treeData.filter((treeNode, index) => {
       if (isSetPrivateKey) {
         setPrivateKey(treeNode, parentTreeNode, config); // 为每个节点设置私有键名
      }
       const bool = callBack?.(treeNode, index, parentTreeNode)
       if (treeNode[childrenName]) { // 如果该节点存在子节点
         treeNode[childrenName] = fun(treeNode[childrenName], treeNode); // 递归调用自身,将子节点返回的新数组赋值给该节点
      }
       if (bool) { // 如果传入了搜索条件回调函数,并且该节点通过搜索条件
         resultTileArr.push(treeNode); // 将该节点添加至结果数组
         return true; // 返回true
      } else { // 否则,如果该节点存在子节点
         return treeNode[childrenName] && treeNode[childrenName].length; // 判断子节点是否存在
      }
    });
};
 const resultArr = fun(treeData); // 调用函数,返回查询到的结果数组或整个树形结构数组
 return isTileArray ? resultTileArr : resultArr; // 根据配置项返回结果数组或整个树形结构数组
};

mapTreeData


/**
* 处理多维树形结构数组的每个元素,并返回处理后的数组
* @param treeData 树形结构数组
* @param callBack 写入搜索条件 用法和数组的map方法一致(前两个参数一样 第三个参数为旧的父级详情 第四个是新的父级详情)
* @param config 配置项(key,childrenName,isTileArray,isSetPrivateKey)
* @returns 返回查询到的结果数组
*/

const mapTreeData = (treeData = [], callBack, config) => {
 const { childrenName = `children`, isTileArray = false, isSetPrivateKey = false } = config || {}
 const resultTileArr = []
 const fun = (_treeData, oldParentTreeNode, newParentTreeNode) => {
   return _treeData.map((treeNode, index) => {
     isSetPrivateKey && setPrivateKey(treeNode, oldParentTreeNode, config)
     const callBackInfo = callBack?.(treeNode, index, oldParentTreeNode, newParentTreeNode)
     if (isTileArray) {
       resultTileArr.push(callBackInfo)
    }
     const mappedTreeNode = {
       ...treeNode,
       ...callBackInfo,
    }
     if (treeNode?.[childrenName]) {
       mappedTreeNode[childrenName] = fun(treeNode[childrenName], treeNode, mappedTreeNode)
    }
     return mappedTreeNode
  })
}
 const resultArr = fun(treeData)
 return isTileArray ? resultTileArr : resultArr
};

ts版本代码


/**
* 操控树形结构公共函数方法
* findTreeData     筛选多维树形结构 返回查询到的第一个结果
* filterTreeData   筛选多维树形结构 返回查询到的结果数组
* mapTreeData     处理多维树形结构数组的每个元素,并返回处理后的数组
* getTreeData     一维树形结构转多维树形结构
* flattenTreeData 多维树形结构转一维树形结构
*/


/** 配置项 */
type TreeDataConfig = {
 /** 唯一标识 默认是id */
 key?: string
 /** 与父节点关联的唯一标识 默认是pId */
 parentKey?: string
 /** 查询子集的属性名 默认是children */
 childrenName?: string
 /** 返回值是否为一维树形结构 默认是false*/
 isTileArray?: boolean
 /** 是否添加私有变量 默认是false */
 isSetPrivateKey?: boolean
}

type TreeNode = {
 _pId?: string | number
 _pathArr?: Array<string | number>
}

/**
* 新增业务参数。
* _pId     父级id
* _pathArr 记录了从一级到当前节点的id集合。
* _pathArr 的length可以记录当前是多少层
* @param treeNode
* @param parentTreeNode
* @param treeDataConfig
*/

const setPrivateKey = <T extends { [x: string]: any }>(
 treeNode: T & TreeNode,
 parentTreeNode: (T & TreeNode) | undefined,
 config?: TreeDataConfig
) => {
 const { key = `id` } = config || {}
 treeNode._pId = parentTreeNode?.[key]
 treeNode._pathArr = parentTreeNode?._pathArr
   ? [...parentTreeNode._pathArr, treeNode[key]]
  : [treeNode[key]]
}

type FindTreeData = <T extends { [x: string]: any }>(
 treeData?: readonly T[],
 callBack?: (treeNode: T, index: number, parentTreeNode?: T) => boolean,
 config?: TreeDataConfig,
 parentTreeNode?: T
) => (T & TreeNode) | undefined
/**
* 筛选多维树形结构 返回查询到的第一个结果
* @param treeData 树形结构数组
* @param callBack 写入搜索条件 用法和数组的find方法一致(前两个参数一样 第三个参数为父级的详情 没有父级的返回undefined)
* @param config 配置项(key,childrenName,isSetPrivateKey)
* @returns 返回查询到的第一个结果
*/

export const findTreeData: FindTreeData = (treeData = [], callBack, config, parentTreeNode) => {
 const { childrenName = `children`, isSetPrivateKey = false } = config || {}
 for (const treeNode of treeData) {
   isSetPrivateKey && setPrivateKey(treeNode, parentTreeNode, config)
   if (callBack?.(treeNode, treeData.indexOf(treeNode), parentTreeNode)) {
     return treeNode
  }
   if (treeNode[childrenName]) {
     const dataInfo = findTreeData(treeNode[childrenName], callBack, config, treeNode)
     if (dataInfo) {
       return dataInfo
    }
  }
}
}

/**
* 筛选多维树形结构 返回查询到的结果数组
* @param treeData 树形结构数组
* @param callBack 写入搜索条件 用法和数组的filter方法一致(前两个参数一样 第三个参数为父级的详情 没有父级的返回undefined)
* @param config 配置项(key,childrenName,isTileArray,isSetPrivateKey)
* @returns 返回查询到的结果数组
*/

export const filterTreeData = <T extends { [x: string]: any }>(
 treeData: readonly T[] = [],
 callBack?: (treeNode: T, index: number, parentTreeNode?: T) => boolean,
 config?: TreeDataConfig
): (T & TreeNode)[] => {
 const { childrenName = `children`, isTileArray = false, isSetPrivateKey = false } = config || {}
 const resultTileArr: T[] = []
 const fun = (_treeData: readonly T[], parentTreeNode?: T): T[] => {
   return _treeData.filter((treeNode, index) => {
     isSetPrivateKey && setPrivateKey(treeNode, parentTreeNode, config)
     const bool = callBack?.(treeNode, index, parentTreeNode)
     if (treeNode[childrenName]) {
      ;(treeNode[childrenName] as T[]) = fun(treeNode[childrenName], treeNode)
    }
     if (bool) {
       resultTileArr.push(treeNode)
       return true
    } else {
       return treeNode[childrenName] && treeNode[childrenName].length
    }
  })
}
 const resultArr = fun(treeData)
 return isTileArray ? resultTileArr : resultArr
}

/**
* 处理多维树形结构数组的每个元素,并返回处理后的数组
* @param treeData 树形结构数组
* @param callBack 写入搜索条件 用法和数组的map方法一致(前两个参数一样 第三个参数为父级的详情 没有父级的返回undefined)
* @param config 配置项(key,childrenName,isTileArray,isSetPrivateKey)
* @returns 返回查询到的结果数组
*/

export const mapTreeData = <T extends { [x: string]: any }>(
 treeData: readonly T[] = [],
 callBack?: (
   treeNode: T,
   index: number,
   oldParentTreeNode?: T,
   newParentTreeNode?: T
) => { [x: string]: any } | any,
 config?: TreeDataConfig
): Array<T & TreeNode & { [x: string]: any }> => {
 const { childrenName = `children`, isTileArray = false, isSetPrivateKey = false } = config || {}
 const resultTileArr: Array<T & { [x: string]: any }> = []
 const fun = (_treeData: readonly T[], oldParentTreeNode?: T, newParentTreeNode?: T) => {
   return _treeData.map((treeNode, index) => {
     isSetPrivateKey && setPrivateKey(treeNode, oldParentTreeNode, config)
     const callBackInfo = callBack?.(treeNode, index, oldParentTreeNode, newParentTreeNode)
     if (isTileArray) {
       resultTileArr.push(callBackInfo)
       return
    }
     const mappedTreeNode = {
       ...treeNode,
       ...callBackInfo,
    }
     if (treeNode?.[childrenName]) {
       mappedTreeNode[childrenName] = fun(treeNode[childrenName], treeNode, mappedTreeNode)
    }
     return mappedTreeNode
  })
}
 const resultArr = fun(treeData)
 return isTileArray ? resultTileArr : resultArr
}

/**
* 一维树形结构转多维树形结构
* @param tileArray 一维树形结构数组
* @param config 配置项(key,childrenName,parentKey,isSetPrivateKey)
* @returns 返回多维树形结构数组
*/

export const getTreeData = <T extends { [x: string]: any }>(
 tileArray: readonly T[] = [],
 config?: TreeDataConfig
): (T & TreeNode)[] => {
 const {
   key = `id`,
   childrenName = `children`,
   parentKey = `pId`,
   isSetPrivateKey = false,
} = config || {}
 const fun = (parentTreeNode: { [x: string]: any }) => {
   const parentId = parentTreeNode[key]
   const childrenNodeList: T[] = []
   copyTileArray = copyTileArray.filter(item => {
     if (item[parentKey] === parentId) {
       childrenNodeList.push({ ...item })
       return false
    } else {
       return true
    }
  })
   parentTreeNode[childrenName] = childrenNodeList
   childrenNodeList.forEach(item => {
     isSetPrivateKey && setPrivateKey(item, parentTreeNode, config)
     fun(item)
  })
}
 const rootNodeList = tileArray.filter(item => !tileArray.some(i => i[key] === item[parentKey]))
 const resultArr: (T & TreeNode)[] = []
 let copyTileArray = [...tileArray]
 rootNodeList.forEach(item => {
   const index = copyTileArray.findIndex(i => i[key] === item[key])
   if (index > -1) {
     copyTileArray.splice(index, 1)
     const obj = { ...item }
     resultArr.push(obj)
     isSetPrivateKey && setPrivateKey(obj, undefined, config)
     fun(obj)
  }
})
 return resultArr
}

/**
* 多维树形结构转一维树形结构
* @param treeData 树形结构数组
* @param config 配置项(key,childrenName,isSetPrivateKey)
* @returns 返回一维树形结构数组
*/

export const flattenTreeData = <T extends { [x: string]: any }>(
 treeData: readonly T[] = [],
 config?: TreeDataConfig
): (T & TreeNode)[] => {
 const { childrenName = `children`, isSetPrivateKey = false } = config || {}
 const result: T[] = []
 const fun = (_treeData: readonly T[], parentTreeNode?: T) => {
   _treeData.forEach(treeNode => {
     isSetPrivateKey && setPrivateKey(treeNode, parentTreeNode, config)
     result.push(treeNode)
     if (treeNode[childrenName]) {
       fun(treeNode[childrenName], treeNode)
    }
  })
}
 fun(treeData)
 return result
}


作者:热心市民王某
来源:juejin.cn/post/7213642622074765369
收起阅读 »

瀑布流最佳实现方案

web
传统实现方式 当前文章的gif文件较大,加载的时长可能较久 这里我拿小红书的首页作为分析演示 可以看到他们的实现方式是传统做法,把每个元素通过获取尺寸,然后算出left、top的排版位置,最后在每个元素上设置偏移值,思路没什么好说的,就是算元素坐标。那么...
继续阅读 »

传统实现方式



当前文章的gif文件较大,加载的时长可能较久



这里我拿小红书的首页作为分析演示


xhs2.gif


可以看到他们的实现方式是传统做法,把每个元素通过获取尺寸,然后算出lefttop的排版位置,最后在每个元素上设置偏移值,思路没什么好说的,就是算元素坐标。那么这种做法有什么缺点?请看下面这张图的操作


xhs.gif



  1. 容器尺寸每发生一次变化,容器内部所有节点都需要更新一次样式设置,当页面元素过多时,窗口的尺寸变动卡到不得了;

  2. 实现起来过于复杂,需要对每个元素获取尺寸然后进行计算,不利于后面修改布局样式;

  3. 每一次的容器尺寸发生变动,图片元素都会闪烁一下(电脑好的可能不会);


最佳实现方式



吐槽:早在2019年我就将下面的这种实现方式应用在小程序项目上了,但是目前还没见到有人会用这种方式去实现,为什么会没有人想到呢?表示不理解。



代码仓库


预览地址


先看一下效果


show.gif


在上面的预览地址中,打开控制台查看节点元素,可以看到是没有任何的js控制样式操作,而是全部交给css的自适应来渲染,我在代码层中只需要把数据排列好就行。


实现思路


这里我将把容器里面分为4列,如下图


微信截图_20240312210833.png


然后再在每列的数组里面按顺序添加数据即可,这样去布局的好处既方便、兼容性好、浏览器渲染性能开销最低化,而且还不会破坏文档流,将操作做到极致简单。剩下的只需要怎样去处理每一列的数组即可。


处理数组逻辑


由于是要做成动态列,所以不能固定4个数组列表,那就做成动态对容器输出N列,最后再对每一列添加数据即可。这里我用ResizeObserver去代替window.onresize,理由是在实际应用中,容器会受到其他布局而影响,而非窗口变动,所以前者更精确一些,不过思路做法都是一样的。



  • 设置一个变量column,代表显示页面有多少列;

  • 声明一个变量cacheList,用来缓存接口请求回来的数据,也就是总数据;

  • 然后监听容器的宽度去设置column的数量值;

  • 最后用computed根据column的值生成一个二维数组进行页面渲染即可;


import { ref, reactive, computed, onMounted, onUnmounted } from "vue";

/** 每一个节点item的数据结构 */
interface ItemInfo {
id: number
title: string
text: string
/** 图片路径 */
photo: string
}

type ItemList = Array<ItemInfo>;

const page = reactive({
/** 页面中出现多少列数据 */
column: 4,
update: 0,
});

const pageList = computed(function() {
const result = new Array(page.column).fill(0).map((_, index) => ({ id: index, list: [] as ItemList }));
let columnIndex = 0;
page.update; // TODO: 这里放一个引用值,用于手动更新;
for (let i = 0; i < cacheList.length; i++) {
const item = cacheList[i];
result[columnIndex].list.push(item);
columnIndex++;
if (columnIndex >= page.column) {
columnIndex = 0;
}
}
console.log("重新计算列表 !!----------!!");
return result;
});

let cacheList: ItemList = [];

async function getData() {
page.loading = true;
const res = await getList(20); // 接口请求方法
page.loading = false;
if (res.code === 1) {
cacheList = cacheList.concat(res.data);
// TODO: 手动更新,这里不把`cacheList`放进`page`里面是因为响应数据列表过多会影响性能
page.update++;
}
}

let observer: ResizeObserver;

onMounted(function() {
getData()
observer = new ResizeObserver(function(entries) {
const rect = entries[0].contentRect;
if (rect.width > 1200) {
page.column = 4;
} else if (rect.width > 900) {
page.column = 3;
} else if (rect.width > 600) {
page.column = 2;
}
});
observer.observe(document.querySelector(".water-list")!);
});

onUnmounted(function() {
observer.disconnect();
})


这里有个细节,我把page.update丢进computed中作为手动触发更新的开关而不是把cacheList声明响应式的原因是因为页面只需要用到一个响应数组,如果把cacheList也设置为响应式,那就导致了数组过长时,响应式过多的性能开销,所以这里用一个引用值作为手动触发更新依赖的方式会更加好。


这样一个基本的瀑布流就完成了。


基础版预览


更完美的处理


细心的同学这时已经发现问题了,就是当某一列的图片高度都很长时,会产生较大的空隙,因为是没有任何的高度计算处理而是按照数组顺序的逐个添加导致,像下面这样。


微信截图_20240312213804.png


所以这里就还需要优化一下逻辑



  • 在获取数据时,把每一个图片的高度记录下来并写入到总列表中

  • 在组装数据时,先拿到高度最低的一列,然后将数据加入到这一列中



/**
* 加载所有图片并设置对应的宽高
* @param list
*/

async function setImageSize(list: ItemList): Promise<ItemList> {
const total = list.length;
let count = 0;
return new Promise(function(resolve) {
function loadImage(item: ItemInfo) {
const img = new Image();
img.src = item.photo;
function complete<T extends { width: number, height: number }>(target: T) {
count++;
item.width = img.width;
item.height = img.height;
if (count >= total) {
resolve(list);
}
}
img.onload = () => complete(img);
img.onerror = function() {
item.photo = defaultPic.data;
complete(defaultPic);
};
}
for (let i = 0; i < total; i++) {
loadImage(list[i]);
}
});
}

async function getData() {
page.loading = true;
const res = await getList(20);
// page.loading = false;
if (res.code === 1) {
const list = await setImageSize(res.data);
page.loading = false;
cacheList = cacheList.concat(list);
// TODO: 手动更新,这里不把`cacheList`放进`page`里面是因为响应数据列表过多会影响性能
page.update++;
}
}

const pageList = computed(function() {
const result = new Array(page.column).fill(0).map((_, index) => ({ id: index, list: [] as ItemList, height: 0 }));
/** 设置列的索引 */
let columnIndex = 0;
// TODO: 这里放一个引用值,用于手动更新;
page.update;
// 开始组装数据
for (let i = 0; i < cacheList.length; i++) {
const item = cacheList[i];
if (columnIndex < 0) {
// 从这里开始,将以最低高度列的数组进行添加数据,这样就不会出现某一列高度与其他差距较大的情况
result.sort((a, b) => a.height - b.height);
// console.log("数据添加前 >>", item.id, result.map(ele => ({ index: ele.id, height: ele.height })));
result[0].list.push(item);
result[0].height += item.height!;
// console.log("数据添加后 >>", item.id, result.map(ele => ({ index: ele.id, height: ele.height })));
// console.log("--------------------");
} else {
result[columnIndex].list.push(item);
result[columnIndex].height += item.height!;
columnIndex++;
if (columnIndex >= page.column) {
columnIndex = -1;
}
}
}
console.log("重新计算列表 !!----------!!");
// 最后排一下原来的顺序再返回即可
result.sort((a, b) => a.id - b.id);
// console.log("处理过的数据列表 >>", result);
return result;
});


这样就达到完美的效果了,但是每次获取数据的时候却要等一会,因为要把获取回来的图片全部加载完才进行数据显示,所以没有基础版的无脑组装数据然后渲染快。除非然让后端返回数据的时候也带上图片的宽高(不现实),只能在上传图片的操作中携带上。


作者:黄景圣
来源:juejin.cn/post/7345379926147252236
收起阅读 »

程序员的这10个坏习惯,你中了几个?超过一半要小心了

前言 一些持续关注过我的朋友大部分都来源于我的一些资源分享和一篇万字泣血斩副业的劝诫文,但今年年后开始我有将近4个月没有再更新过。 有加过我好友的朋友私聊我问过,有些回复了有些没回复。 想通过这篇文章顺便说明一下个人的情况,主要是给大家的一些中肯的建...
继续阅读 »

前言



一些持续关注过我的朋友大部分都来源于我的一些资源分享和一篇万字泣血斩副业的劝诫文,但今年年后开始我有将近4个月没有再更新过。




有加过我好友的朋友私聊我问过,有些回复了有些没回复。




想通过这篇文章顺便说明一下个人的情况,主要是给大家的一些中肯的建议。



我的身体



今年年前公司福利发放的每人一次免费体检,我查出了高密度脂蛋白偏低,因为其他项大体正常,当时也没有太在意。




但过完年后的第一个月,我有一次下午上班忽然眩晕,然后犯恶心,浑身发软冒冷汗,持续了好一阵才消停。




当时我第一感觉就是颈椎出问题了?毕竟这是程序员常见的职业病。




然后在妻子陪伴下去医院的神经内科检查了,结果一切正常。




然后又去拍了片子看颈椎什么问题,显示第三节和第四节有轻微的增生,医生说其实没什么,不少从事电脑工作的人都有,不算是颈椎有大问题。




我人傻了,那我这症状是什么意思。




医生又建议我去查下血,查完后诊断出是血脂偏高,医生说要赶紧开始调理身体了,否则会引发更多如冠心病、动脉粥样硬化、心脑血管疾病等等。




我听的心惊胆战,没想到我才34岁就会得上老年病。




接下来我开始调理自己的作息和生活,放弃一些不该强求的,也包括工作之余更新博客,分享代码样例等等。




4个月的时间,我在没有刻意减肥的情况下体重从原先152减到了140,整个人也清爽了许多,精力恢复了不少。




所以最近又开始主动更新了,本来是总结了程序员的10个工作中的不良习惯。




但想到自己的情况,决定缩减成5个,另外5个改为程序员生活中的不良习惯,希望能对大家有警示的作用。



不良习惯


1、工作


1)、拖延症


不到最后一天交差,我没有压力,绝不提前完成任务,从上学时完成作业就是这样,现在上班了,还是这样,我就是我,改不了了。



2)、忽视代码可读性


别跟我谈代码注释,多写一个字我认你做die,别跟我谈命名规范,就用汉语拼音,怎样?其他人读不懂,关我什么事?



3)、忽视测试


我写一个单元测试就给我以后涨100退休金,那我就写,否则免谈。接口有问题你前端跟我说就行了发什么脾气,前后端联调不就这样吗?!



4)、孤立自己


团队合作不存在的,我就是不合群的那个,那年我双手插兜,全公司没有一个对手。



5)、盲目追求技术新潮


晚上下班了,吃完饭打开了某某网,看着课程列表中十几个没学完的课程陷入了沉默,但是首页又出现了一门新课,看起来好流行好厉害耶,嗯,先买下来,徐徐图之。



2、生活


1)、缺乏锻炼和运动


工作了一天,还加班,好累,但还是得锻炼,先吃完饭吧,嗯,看看综艺节目吧,嗯,再看看动漫吧,嗯,还得学习一下新技术吧,嗯,是手是得洗澡了,嗯,还要洗衣服,咦,好像忘记了什么重要的事情?算了,躺床上看看《我家娘子不对劲》慢慢入睡。



2)、加班依赖症


看看头条,翻翻掘金,瞅瞅星球,点点订阅号,好了,开始工作吧,好累,喝口水,上个厕所,去外面走走,回来了继续,好像十一点半了,快中午了,待会儿吃什么呢?


午睡醒了,继续干吧,看看头条,翻翻掘金,瞅瞅星球,点点订阅号,好了,开始工作吧,好累,喝口水,上个厕所,去外面走走,回来了继续,好像5点半了,快下班了,任务没完成。


算了,加加班,争取8点之前搞定。


呼~搞定了,走人,咦,10点了。



3)、忽视饮食健康


早上外卖,中午外卖,晚上外卖,哇好丰富耶,美团在手,简直就是舌尖上的中国,晚上再来个韩式炸鸡?嗯,来个韩式甜辣酱+奶香芝士酱,今晚战个痛快!



4)、缺乏社交活动


好烦啊,又要参加公司聚会,聚什么餐,还不是高级外卖,说不定帮厨今天被大厨叼了心情不好吐了不少唾沫在里面,还用上完厕所摸了那里没洗的手索性搅了一遍,最后在角落里默默看着你们吃。


吃完饭还要去KTV?继续喝,喝不死你们,另外你们唱得很好听吗?还不是看谁嗷的厉害!


谁都别跟我说话,尤其是领导,离我越远越好,唉,好想回去,这个点B站该更新了吧,真想早点看up主们嘲讽EDG。



5)、没有女朋友


张三:我不是不想谈恋爱,是没人看得上我啊,我也不好意思硬追,我也要点脸啊,现在的女孩都肿么了?一点暗示都不给了?成天猜猜猜,我猜你MLGB的。


李四:家里又打电话了,问在外面有女朋友了没,我好烦啊,我怎么有啊,我SpringCloudAlibaba都没学会,我怎么有?现在刚毕业的都会k8s了,我不学习了?不学习怎么跳槽,不跳槽工资怎么翻倍,不翻倍怎么买房,不买房怎么找媳妇?


王五:亲朋好友介绍好多个了,都能凑两桌麻将了,我还是没谈好,眼看着要30了,我能咋整啊,我瞅她啊。破罐破摔吧,大不了一个人过呗,多攒点钱以后养老,年轻玩个痛快,老了早点死也不亏,又不用买房买车结婚受气还得养娃,多好啊,以后两脚一蹬我还管谁是谁?



总结



5个工作坏习惯,5个生活坏习惯,送给我亲爱的程序员们,如果你占了一半,真得注意点了,别给自己找借口,你不会对不起别人,只是对不起自己。





喜欢的小伙伴们,麻烦点个赞,点个关注,也可以收藏下,以后没事儿翻出来看看哈。


作者:程序员济癫
来源:juejin.cn/post/7269375465319415867
收起阅读 »

如何移植 JsBridge 到鸿蒙

相信大多数小伙伴的项目都已经有了线上稳定运行的 JsBridge 方案,那么对于鸿蒙来说,最好的方案肯定是不需要前端同学的改动,就可以直接运行,这个兼容任务就得我们自己来做了。 关于 JsBridge 的通信原理,现在主流的技术方案有 拦截 URL 和 对象注...
继续阅读 »

相信大多数小伙伴的项目都已经有了线上稳定运行的 JsBridge 方案,那么对于鸿蒙来说,最好的方案肯定是不需要前端同学的改动,就可以直接运行,这个兼容任务就得我们自己来做了。


关于 JsBridge 的通信原理,现在主流的技术方案有 拦截 URL对象注入 两种,我们分别看一下如何在鸿蒙上实现。


拦截 URL


在安卓上,拦截 URL 这个技术方案的代表作一定是 github.com/lzyzsd/JsBr… ,相信有不少小伙伴都使用了这个开源库。


我这里就以该开源库为例,介绍一下如何在鸿蒙上无缝迁移。


首先,在页面加载完成后注入通信需要的 JS 代码。在 Android 中,是 WebViewClient.onPageFinished(),在鸿蒙中对应 Web组件的 onPageEnd()方法。


Web({ src: this.url, controller: this.controller })
.onPageEnd(() => {
this.onPageEnd()
BridgeUtil.webViewLoadLocalJs(getContext(), this.controller, BridgeUtil.toLoadJs)
})

鸿蒙中本地资源文件放在 resouce/rawfile 目录下,通过以下代码读取:


rawFile2Str(context: Context, file: string): string {
try {
let data = context.resourceManager.getRawFileContentSync(file)
let decoder = util.TextDecoder.create("utf-8")
let str = decoder.decodeWithStream(data, { stream: false })
return str
} catch (e) {
return ""
}
}

读取到的 JS 代码,通过系统能力动态执行。在 Android 中,通过 WebView.loadUrl() 或者 WebView.evaluateJavaScript() 来实现。在鸿蒙中,对应的是 WebviewController.runJavaScriptExt()


webViewLoadLocalJs(context: Context, controller: WebviewController, path: string) {
let jsContent = BridgeUtil.rawFile2Str(context, path)
controller.runJavaScriptExt(BridgeUtil.JAVASCRIPT_STR + jsContent, (err, result) => {
...
})
}

JS 代码注入完成后,就是核心的拦截 URL 了。在 Android 中,通过 WebViewClient.shouldOverrideUrlLoading() 实现,看一下具体的代码:


    @Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
try {
url = URLDecoder.decode(url, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}

if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) {
webView.handlerReturnData(url);
return true;
} else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { //
webView.flushMessageQueue();
return true;
} else {
return super.shouldOverrideUrlLoading(view, url);
}
}

拦截到所有的 URL,判断是否是 H5 通过 iFrame.src 发送的指定特征的 URL,来完成通信流程。


在鸿蒙中,对应的是 Web 组件的 onInterceptRequest()方法。


Web({ src: this.url, controller: this.controller })
.onInterceptRequest((event) => {
if (event) {
let url = event.request.getRequestUrl()
if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) {
this.ytoJsBridge.handlerReturnData(url)
} else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) {
this.ytoJsBridge.flushMessageQueue()
} else {
return null
}
}
return null
})

核心逻辑就这样,剩下的工作量就是苦逼的翻译代码。好在代码量并不大,大概五六个文件。


移植过程中,也踩了一些坑,印象最深的是 ArkTs 中关于接口的写法。


export interface CallBackFunction {
onCallBack(data: string): void
}

这是在 Java/Kotlin 中很常见的一种写法,顺手在 ArkTs 也这么写,但是在使用过程中尝试去写实现的时候就犯了难。如果直接按照传统的前端写法:


let responseFunction: CallBackFunction
if (callBackId != undefined)
{
responseFunction = {
onCallBack: (data: string): void => {
...
}
}
}

你会得到一个 lint 错误 Object literal must correspond to some explicitly declared class or interface (arkts-no-untyped-obj-literals) 。


你可以使用箭头函数来解决这个问题。


export interface CallBackFunction {
onCallBack: (data: string) => void
// onCallBack(data: string): void
}

这也是 ArkTs 目前比较割裂的地方,基于 TS,但是禁用了很多特性。


设想一下如果可以继续兼容 Java/Kotlin,那么这篇文章都不会存在了,压根不存在迁移成本,海量移动端类库无缝衔接......


对象注入


对象注入在 Android WebView 中的实现是 WebView.addJavascriptInterface(Object object, String name) 方法 。


addJavascriptInterface(JsBridge(this@MainActivity, webView), "JsBridge")

class JsBridge(private val activity: Activity, private val webView: WebView) {

@JavascriptInterface
fun webCallNative(message: String) {
Log.e("JsBridge", "webCallNative: ${Thread.currentThread().name}")
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
}
}

在鸿蒙中,可以通过 Web 组件的 javaScriptProxy() 方法,或者 WebviewController.registerJavaScriptProxy() 方法。


      Web({ src: this.url, controller: this.controller })
.javaScriptAccess(true)
.javaScriptProxy({
object: this.testObj,
name: "objName",
methodList: ["test", "toString"],
controller: this.controller,
})

这种方式只支持注入一个对象,如果需要注入多个对象,要用 WebviewController.registerJavaScriptProxy()


this.controller.registerJavaScriptProxy(this.testObjtest, "objName", ["test", "toString", "testNumber", "testBool"]);
this.controller.registerJavaScriptProxy(this.webTestObj, "objTestName", ["webTest", "webString"]);

这个方法的调用时机需要注意,必须发生在 controller 和 Web 组件绑定之后,建议放在 Web.onPageEnd()。注册之后需要调用 WebviewController.refresh() 才会生效。


总结


一入鸿蒙深似海,波涛汹涌无尽头。

云涛翻滚遮日月,雾霭弥漫掩星楼。

仙禽异兽齐飞舞,灵草神木共清幽。

鸿蒙奥秘难穷尽,探寻真道意未休。


Write by 文心一言,如有雷同,请...


作者:MobileDeveloper
来源:juejin.cn/post/7345071687309180962
收起阅读 »

违反这些设计原则,系统就等着“腐烂”

分享是最有效的学习方式。 老猫的设计模式专栏已经偷偷发车了。不甘愿做crud boy?看了好几遍的设计模式还记不住?那就不要刻意记了,跟上老猫的步伐,在一个个有趣的职场故事中领悟设计模式的精髓吧。还等什么?赶紧上车吧 故事 这段时间以来,小猫按照之前的系统梳...
继续阅读 »

分享是最有效的学习方式。



老猫的设计模式专栏已经偷偷发车了。不甘愿做crud boy?看了好几遍的设计模式还记不住?那就不要刻意记了,跟上老猫的步伐,在一个个有趣的职场故事中领悟设计模式的精髓吧。还等什么?赶紧上车吧


故事


这段时间以来,小猫按照之前的系统梳理方案【系统梳理大法&代码梳理大法】一直在整理着文档。


系统中涉及的业务以及模型也基本了然于胸,但是这代码写的真的是...


小猫也终于知道了为什么每天都有客诉,为什么每天都要去调用curl语句去订正生产的数据,为什么每天都在Hotfix...


整理了一下,大概出于这些原因,业务流程复杂暂且不议,光从技术角度来看,整个代码体系臃肿不堪,出问题之后定位困难,后面接手的几任开发为了解决问题都是“曲线救国”,不从正面去解决问题,为了解决一时的客诉问题而去解决问题,于是定义了各种新的修复流程去解决问题,这么一来,软件系统“无序”总量一直在增加,整个系统体系其实在初版之后就已经在“腐烂”了,如此?且抛开运维稳定性不谈,就系统本身稳定性而言,能好?


整个系统,除了堆业务还是堆业务,但凡有点软件设计原则,系统也不会写成这样了。


关于设计原则


大家在产品提出需求之后,一般都会去设计数据模型,还有系统流程。但是各位有没有深度去设计一下代码的实现呢?还是说上手就直接照着流程图开始撸业务了?估计有很多的小伙伴由于各种原因不会去考虑代码设计,其实老猫很多时候也一样。主要原因比如:项目催的紧,哪有时间考虑那么多,功能先做出来,剩下的等到后面慢慢优化。然而随着时间的推移,我们会发现我们一直很忙,说好的把以前的代码重构好一点,哪有时间!于是,就这样“技术债”越来越多,就像滚雪球一样,整个系统逐渐“腐烂”到了根。最终坑的可能是自己,也有可能是“下一个他”。


虽然在日常开发的时候项目进度比较紧张,我们很多时候也不去深度设计代码实现,但是我们在写代码的时候保证心中有一杆秤其实还是必要的。


那咱们就结合各种案来聊聊“这杆秤”————软件设计原则。


design_rule.png


下面我们通过各种小例子来协助大家理解软件设计原则,案例是老猫构想的,有的时候不要太过较真,主要目的是讲清楚原则。另外后文中也会有相关的类图表示实体之间的关系,如果大家对类图不太熟悉的,也可以看一下这里【类图传送门


开闭原则


开闭原则,英文(Open-Closed Principle,简称:OCP)。只要指一个软件实体(例如,类,模块和函数),应该对扩展开放,对修改关闭。其重点强调的是抽象构建框架,实现扩展细节,从而提升软件系统的可复用性以及可维护性。


概念是抽象,但是案例是具体的,所以咱们直接看案例,通过案例去理解可能更容易。


由于小猫最近在维护商城类业务,所以咱们就从商品折价售卖这个案例出发。业务是这样的,商城需要对商品进行做打折活动,目前针对不同品类的商品可能打折的力度不一样,例如生活用品和汽车用品的打折情况不同。
创建一个基础商品接口:


public interface IProduct {
String getSpuCode(); //获取商品编号
String getSpuName(); //获取商品名称
BigDecimal getPrice(); //获取商品价格
}

基础商品实现该接口,于是我们就有了如下代码:


/**
*
@Author: 公众号:程序员老猫
*
@Date: 2024/2/7 23:39
*/

public class Product implements IProduct {
private String spuCode;
private String spuName;
private BigDecimal price;
private Integer categoryTag;

public Product(String spuCode, String spuName, BigDecimal price, Integer categoryTag) {
this.spuCode = spuCode;
this.spuName = spuName;
this.price = price;
this.categoryTag = categoryTag;
}

public Integer getCategoryTag() {
return categoryTag;
}

@Override
public String getSpuCode() {
return spuCode;
}

@Override
public String getSpuName() {
return spuName;
}

@Override
public BigDecimal getPrice() {
return price;
}
}

按照上面的业务,现在搞活动,咱们需要针对不同品类的商品进行促销活动,例如生活用品需要进行折扣。当然我们有两种方式实现这个功能,如果咱们不改变原有代码,咱们可以如下实现。


public class DailyDiscountProduct extends Product {
private static final BigDecimal daily_discount_factor = new BigDecimal(0.95);
private static final Integer DAILY_PRODUCT = 1;

public DailyDiscountProduct(String spuCode, String spuName, BigDecimal price) {
super(spuCode, spuName, price, DAILY_PRODUCT);
}

public BigDecimal getOriginPrice() {
return super.getPrice();
}

@Override
public BigDecimal getPrice() {
return super.getPrice().multiply(daily_discount_factor);
}
}

上面我们看到直接打折的日常用品的商品继承了标准商品,并且对其进行了价格重写,这样就完成了生活用品的打折。当然这种打折系数的话我们一般可以配置到数据库中。


对汽车用品的打折其实也是一样的实现。继承之后重写价格即可。咱们并不需要去基础商品Product中根据不同的品类去更改商品的价格。


错误案例


如果我们一味地在原始类别上去做逻辑应该就是如下这样:



public class Product implements IProduct {
private static final Integer DAILY_PRODUCT = 1;
private static final BigDecimal daily_discount_factor = new BigDecimal(0.95);
private String spuCode;
private String spuName;
private BigDecimal price;
private Integer categoryTag;
....
@Override
public BigDecimal getPrice() {
if(categotyTag.equals(DAILY_PRODUCT)){
return price.multiply(daily_discount_factor);
}
return price;
}
}

后续随着业务的演化,后面如果提出对商品名称也要定制,那么咱们可能还是会动当前的代码,我们一直在改当前类,代码越堆越多,越来越臃肿,这种实现方式就破坏了开闭原则。


咱们看一下开闭原则的类图。如下:


kb_01.png


依赖倒置原则


依赖倒置原则,英文名(Dependence Inversion Principle,简称DIP),指的是高层模块不应该依赖低层模块,二者都应该依赖其抽象。通过依赖倒置,可以减少类和类之间的耦合性,从而提高系统的稳定性。这里主要强调的是,咱们写代码要面向接口编程,不要面向实现去编程。


定义看起来不够具体,咱们来看一下下面这样一个业务。针对不同的大客户,我们定制了很多商城,有些商城是专门售卖电器的,有些商城是专门售卖生活用品的。有个大客,由于对方是电器供应商,所以他们想售卖自己的电器设备,于是,我们就有了下面的业务。


//定义了一个电器设备商城,并且支持特有的电器设备下单流程
public class ElectricalShop {
public String doOrder(){
return "电器商城下单";
}
}
//用户进行下单购买电器设备
public class Consumer extends ElectricalShop {
public void shopping() {
super.doOrder();
}
}

我们看到,当客户可选择的只有一种商城的时候,这种实现方式确实好像没有什么问题,但是现在需求变了,马上要过年了,大客户不想仅仅给他们的客户提供电器设备,他们还想卖海鲜产品,这样,以前的这种下单模式好像会有点问题,因为以前我们直接继承了ElectricalShop,这样写的话,业务可拓展性就太差了,所以我们就需要抽象出一个接口,然后客户在下单的时候可以选择不同的商城进行下单。于是改造之后,咱们就有了如下代码:


//抽象出一个更高维度的商城接口
public interface Shop {
String doOrder();
}
//电器商城实现该接口实现自有下单流程
public class ElectricalShop implements Shop {
public String doOrder(){
return "电器商城下单";
}
}
//海鲜商城实现该接口实现自有下单流程
public class SeaFoodShop implements Shop{
@Override
public String doOrder() {
return "售卖一些海鲜产品";
}
}
//消费者注入不同的商城商品信息
public class Consumer {
private Shop shop;
public Consumer(Shop shop) {
this.shop = shop;
}
public String shopping() {
return shop.doOrder();
}
}
//消费者在不同商城随意切换下单测试
public class ConsumerTest {
public static void main(String[] args) {
//电器商城下单
Consumer consumer = new Consumer(new ElectricalShop());
System.out.println(consumer.shopping());
//海鲜商城下单
Consumer consumer2 = new Consumer(new SeaFoodShop());
System.out.println(consumer2.shopping());
}
}


上面这样改造之后,原本继承详细商城实现的Consumer类,现在直接将更高维度的商城接口注入到了类中,这样相信后面再多几个新的商城的下单流程都可以很方便地就完成拓展。


这其实也就是依赖倒置原则带来的好处,咱们最终来看一下类图。


DIP.png


单一职责原则


单一职责原则,英文名(SimpleResponsibility Pinciple,简称SRP)指的是不要存在多余一个导致类变更的原因。这句话看起来还是比较抽象的,老猫个人的理解是单一职责原则重点是区分业务边界,做到合理地划分业务,根据产品的需求不断去重新规划设计当前的类信息。关于单一职责老猫其实之前已经和大家分享过了,在此不多赘述,大家可以进入这个传送门【单一职责原则


接口隔离原则


接口隔离原则(Interface Segregation Principle,简称ISP)指的是指尽量提供专门的接口,而非使用一个混合的复杂接口对外提供服务。


聊到接口隔离原则,其实这种原则和单一职责原则有点类似,但是又不同:



  1. 联系:接口隔离原则和单一职责原则都是为了提高代码的可维护性和可拓展性以及可重用性,其核心的思想都是“高内聚低耦合”。

  2. 区别:针对性不同,接口隔离原则针对的是接口,而单一职责原则针对的是类。


下面,咱们用一个业务例子来说明一下吧。
我们用简单的动物行为这样一个例子来说明一下,动物从大的方面有能飞的,能吃,能跑,有的也会游泳等等。如果我们定义一个比较大的接口就是这样的。


public interface IAnimal {
void eat();
void fly();
void swim();
void run();
...
}

我们用猫咪实现了该方法,于是就有了。


public class Cat implements IAnimal{
@Override
public void eat() {
System.out.println("老猫喜欢吃小鱼干");
}
@Override
public void fly() {
}
@Override
public void swim() {
}
@Override
public void run() {
System.out.println("老猫还喜欢奔跑");
}
}

我们很容易就能发现,如果老猫不是“超人猫”的话,老猫就没办法飞翔以及游泳,所以当前的类就有两个空着的方法。
同样的如果有一只百灵鸟,那么实现Animal接口之后,百灵鸟的游泳方法也是空着的。那么这种实现我们发现只会让代码变得很臃肿,所以,我们发现IAnimal这个接口的定义太大了,我们需要根据不同的行为进行二次拆分。
拆分之后的结果如下:


//所有的动物都会吃东西
public interface IAnimal {
void eat();
}
//专注飞翔的接口
public interface IFlyAnimal {
void fly();
}
//专注游泳的接口
public interface ISwimAnimal {
void swim();
}

那如果现在有一只鸭子和百灵鸟,咱们分别去实现的时候如下:


public class Duck implements IAnimal,ISwimAnimal{
@Override
public void eat() {
System.out.println("鸭子吃食");
}

@Override
public void swim() {
System.out.println("鸭子在河里游泳");
}
}

public class Lark implements IAnimal,IFlyAnimal{
@Override
public void eat() {
System.out.println("百灵鸟吃食");
}

@Override
public void fly() {
System.out.println("百灵鸟会飞");
}
}

我们可以看到,这样在我们具体的实现类中就不会存在空方法的情况,代码随着业务的发展也不会变得过于臃肿。
咱们看一下最终的类图。


ISP.png


迪米特原则


迪米特原则(Law of Demeter,简称 LoD),指的是一个对象应该对其他对象保持最少的了解,如果上面这个原则名称不容易记,其实这种设计原则还有两外一个名称,叫做最少知道原则(Least Knowledge Principle,简称LKP)。其实主要强调的也是降低类和类之间的耦合度,白话“不要和陌生人说话”,或者也可以理解成“让专业的人去做专业的事情”,出现在成员变量,方法输入、输出参数中的类都可以称为成员朋友类,而出现在方法体内部的类不属于朋友类。


通过具体场景的例子来看一下。
由于小猫接手了商城类的业务,目前他对业务的实现细节应该是最清楚的,所以领导在向老板汇报相关SKU销售情况的时候总是会找到小猫去统计各个品类的sku的销售额以及销售量。于是就有了领导下命令,小猫去做统计的业务流程。


//sku商品
public class Sku {
private BigDecimal price;
public BigDecimal getPrice() {
return price;
}

public void setPrice(BigDecimal price) {
this.price = price;
}
}

//小猫统计总sku数量以及总销售金额
public class Kitty {
public void doSkuCheck(List skuList) {
BigDecimal totalSaleAmount =
skuList.stream().map(sku -> sku.getPrice()).reduce(BigDecimal::add).get();
System.out.println("总sku数量:" + skuList.size() + "sku总销售金额:" + totalSaleAmount);
}
}

//领导让小猫去统计各个品类的商品
public class Leader {
public void checkSku(Kitty kitty) {
//模拟领导指定的各个品类
List difCategorySkuList = new ArrayList<>();
kitty.doSkuCheck(difCategorySkuList);
}
}

//测试类
public class LodTest {
public static void main(String[] args) {
Leader leader = new Leader();
Kitty kitty = new Kitty();
leader.checkSku(kitty);
}
}


从上面的例子来看,领导其实并没有参与统计的任何事情,他只是指定了品类让小猫去统计。从而降低了类和类之间的耦合。即“让专门的人做专门的事”


我们看一下最终的类图。


LOD.png


里氏替换原则


里氏替换原则(Liskov Substitution Principle,英文简称:LSP),它由芭芭拉·利斯科夫(Barbara Liskov)在1988年提出。里氏替换原则的含义是:如果一个程序中所有使用基类的地方都可以用其子类来替换,而程序的行为没有发生变化,那么这个子类就遵守了里氏替换原则。换句话说,一个子类应该可以完全替代它的父类,并且保持程序的正确性和一致性。


上述的定义还是比较抽象的,老猫试着重新理解一下,



  1. 子类可以实现父类的抽象方法,但是不能覆盖父类的抽象方法。

  2. 子类可以增加自己特有的方法。

  3. 当子类的方法重载父类的方法的时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更加宽松。

  4. 当子类的方法实现父类的方法时,方法的后置条件比父类更严格或者和父类一样。


里氏替换原则准确来说是上述提到的开闭原则的实现方式,但是它克服了继承中重写父类造成的可复用性变差的缺点。它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。


下面咱们用里式替换原则比较经典的例子来说明“鸵鸟不是鸟”。我们看一下咱们印象中的鸟类:


class Bird {
double flySpeed;

//设置飞行速度
public void setSpeed(double speed) {
flySpeed = speed;
}

//计算飞行所需要的时间
public double getFlyTime(double distance) {
return (distance / flySpeed);
}
}
//燕子
public class Swallow extends Bird{
}
//由于鸵鸟不能飞,所以我们将鸵鸟的速度设置为0
public class Ostrich extends Bird {
public void setSpeed(double speed) {
flySpeed = 0;
}
}

光看这个实现的时候好像没有问题,但是我们调用其方法计算其指定距离飞行时间的时候,那么这个时候就有问题了,如下:


public class TestMain {
public static void main(String[] args) {
double distance = 120;
Ostrich ostrich = new Ostrich();
System.out.println(ostrich.getFlyTime(distance));

Swallow swallow = new Swallow();
swallow.setSpeed(30);
System.out.println(swallow.getFlyTime(distance));
}
}

结果输出:


Infinity
4.0

显然鸵鸟出问题了,



  1. 鸵鸟重写了鸟类的 setSpeed(double speed) 方法,这违背了里氏替换原则。

  2. 燕子和鸵鸟都是鸟类,但是父类抽取的共性有问题,鸵鸟的飞行不是正常鸟类的功能,需要特殊处理,应该抽取更加共性的功能。


于是我们进行对其进行优化,咱们取消鸵鸟原来的继承关系,定义鸟和鸵鸟的更一般的父类,如动物类,它们都有奔跑的能力。鸵鸟的飞行速度虽然为 0,但奔跑速度不为 0,可以计算出其奔跑指定距离所要花费的时间。优化之后代码如下:


//抽象出更高层次的动物类,定义内部的奔跑行为
public class Animal {
double runSpeed;

//设置奔跑速度
public void setSpeed(double speed) {
runSpeed = speed;
}
//计算奔跑所需要的时间
public double getRunTime(double distance) {
return (distance / runSpeed);
}
}
//定义飞行的鸟类
public class Bird extends Animal {
double flySpeed;
//设置飞行速度
public void setSpeed(double speed) {
flySpeed = speed;
}
//计算飞行所需要的时间
public double getFlyTime(double distance) {
return (distance / flySpeed);
}
}
//此时鸵鸟直接继承动物接口
public class Ostrich extends Animal {
}
//燕子继承普通的鸟类接口
public class Swallow extends Bird {
}

简单测试一下:


public class TestMain {
public static void main(String[] args) {
double distance = 120;
Ostrich ostrich = new Ostrich();
ostrich.setSpeed(40);
System.out.println(ostrich.getRunTime(distance));

Swallow swallow = new Swallow();
swallow.setSpeed(30);
System.out.println(swallow.getFlyTime(distance));
}
}

结果输出:


3.0
4.0

优化之后,优点:



  1. 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;

  2. 提高代码的重用性;

  3. 提高代码的可扩展性;

  4. 提高产品或项目的开放性;


缺点:



  1. 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;

  2. 降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;

  3. 增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果————大段的代码需要重构。


最终我们看一下类图:


LSP.png


老猫觉得里氏替换原则是最难把握好的,所以到后续咱们再进行深入涉及模式回归的时候再做深入探究。


合成复用原则


合成复用原则(Composite/Aggregate Reuse Principle,英文简称CARP)是指咱们尽量要使用对象组合而不是继承关系达到软件复用的目的。这样的话系统就可以变得更加灵活,同时也降低了类和类之间的耦合度。


看个例子,当我们刚学java的时候都是从jdbc开始学起来的。所以对于DBConnection我们并不陌生。那当我们实现基本产品Dao层的时候,我们就有了如下写法:


public class DBConnection {
public String getConnection(){
return "获取数据库链接";
}
}
//基础产品dao层
public class ProductDao {
private DBConnection dbConnection;

public ProductDao(DBConnection dbConnection) {
this.dbConnection = dbConnection;
}

public void saveProduct(){
String conn = dbConnection.getConnection();
System.out.println("使用"+conn+"新增商品");
}
}

上述就是最简单的合成服用原则应用场景。但是这里有个问题,DBConnection目前只支持mysql一种连接DB的方式,显然不合理,有很多企业其实还需要支持Oracle数据库链接,所以为了符合之前说到的开闭原则,我们让DBConnection交给子类去实现。于是我们可以将其定义成抽象方法。


public abstract class DBConnection {
public abstract String getConnection();
}
//mysql链接
public class MySqlConnection extends DBConnection{
@Override
public String getConnection() {
return "获取mysql链接";
}
}
//oracle链接
public class OracleConnection extends DBConnection{
@Override
public String getConnection() {
return "获取Oracle链接方式";
}
}

最终的实现方式我们一起看一下类图。


CARP.png


总结


之前看过一个故事,一栋楼的破败往往从一扇破窗户开始,慢慢腐朽。其实代码的腐烂其实也是一样,往往是一段拓展性极差的代码开始。所以这要求我们研发人员还是得心中有杆“设计原则”的秤,咱们可能不会去做刻意的代码设计,但是相信有这么一杆原则的秤,代码也不致于会写得太烂。


当然我们也不要刻意去追求设计原则,要权衡具体的场景做出合理的取舍。
设计原则是设计模式的基础,相信大家在了解完设计原则之后对后续的设计模式会有更加深刻的理解。


作者:程序员老猫
来源:juejin.cn/post/7332858431572049947
收起阅读 »

如何找到方向感走出前端职业的迷茫区

web
引言 最近有几天没写技术文章了,因为最近我也遇到了前端职业的迷茫,于是我静下来,回想了下这几年来在工作上处理问题的方式,整理了下思路 ,写了这一片文章。 关于对前端职业的迷茫,如何摆脱或者说衰减,我觉得最重要的是得找到一个自己愿意持续学习、有领域知识积累的细...
继续阅读 »

引言


image.png
最近有几天没写技术文章了,因为最近我也遇到了前端职业的迷茫,于是我静下来,回想了下这几年来在工作上处理问题的方式,整理了下思路 ,写了这一片文章。


关于对前端职业的迷茫,如何摆脱或者说衰减,我觉得最重要的是得找到一个自己愿意持续学习、有领域知识积累的细分方向。工作了3-5年的同学应该需要回答这样一个问题,自己的技术领域是什么?前端工程化、nodejs、数据可视化、互动、搭建、多媒体?如果确定了自己的技术领域,前端的迷茫感和方向感应该会衰弱很多。关于技术领域的学习可以参照 前端开发如何给自己定位?初级?中级?高级!这篇,来确定自己的技术领域。


前端职业是最容易接触到业务,对于业务的要求,都有很大的业务压力,但公司对我们的要求是除了业务还要体现技术价值,这就需要我们做事情之前有充分的思考。在评估一个项目的时候,要想清楚3个问题:业务的目标是什么、技术团队的策略是什么,我们作为前端在里面的价值是什么。如果3个问题都想明白了,前后的衔接也对了,这事情才靠谱。


我们将从业务目标、技术团队策略和前端在其中的价值等方面进行分析。和大家一起逐渐走出迷茫区。


业务目标


image.png
前端开发的最终目标是为用户提供良好的使用体验,并支持实现业务目标。然而,在不同的项目和公司中,业务目标可能存在差异。有些项目注重界面的美观和交互性,有些项目追求高性能和响应速度。因此,作为前端开发者,我们需要了解业务的具体需求,并确保我们的工作能够满足这些目标。


举例来说,假设我们正在开发一个电商网站,该网站的业务目标是提高用户购买商品的转化率。作为前端开发者,我们可以通过改善页面加载速度、优化用户界面和提高网站的易用性来实现这一目标。



  1. 改善页面加载速度: 使用懒加载(lazy loading)来延迟加载图片和其他页面元素,而不是一次性加载所有内容。


htmlCopy Code
<img src="placeholder.jpg" data-src="image.jpg" class="lazyload">

javascriptCopy Code
document.addEventListener("DOMContentLoaded", function() {
var lazyloadImages = document.querySelectorAll(".lazyload");

function lazyLoad() {
lazyloadImages.forEach(function(img) {
if (img.getBoundingClientRect().top <= window.innerHeight && img.getBoundingClientRect().bottom >= 0 && getComputedStyle(img).display !== "none") {
img.src = img.dataset.src;
img.classList.remove("lazyload");
}
});
}

lazyLoad();

window.addEventListener("scroll", lazyLoad);
window.addEventListener("resize", lazyLoad);
});


  1. 优化用户界面: 使用响应式设计确保网站在不同设备上都有良好的显示效果。


htmlCopy Code
<meta name="viewport" content="width=device-width, initial-scale=1.0">

cssCopy Code
@media (max-width: 768px) {
/* 适应小屏幕设备的样式 */
}

@media (min-width: 769px) and (max-width: 1200px) {
/* 适应中等屏幕设备的样式 */
}

@media (min-width: 1201px) {
/* 适应大屏幕设备的样式 */
}


  1. 提高网站易用性: 添加搜索功能和筛选功能,使用户能够快速找到他们想要购买的商品。


htmlCopy Code
<form>
<input type="text" name="search" placeholder="搜索商品">
<button type="submit">搜索</button>
</form>

<select name="filter">
<option value="">全部</option>
<option value="category1">分类1</option>
<option value="category2">分类2</option>
<option value="category3">分类3</option>
</select>

javascriptCopy Code
document.querySelector("form").addEventListener("submit", function(e) {
e.preventDefault();
var searchQuery = document.querySelector("input[name='search']").value;
// 处理搜索逻辑
});

document.querySelector("select[name='filter']").addEventListener("change", function() {
var filterValue = this.value;
// 根据筛选条件进行处理
});

协助技术团队制定策略


image.png
为了应对前端开发中的挑战,协助技术团队需要制定相应的策略。这些策略可以包括技术选型、代码规范、测试流程等方面。通过制定清晰的策略,团队成员可以更好地协作,并在面对困难时有一个明确的方向。


举例来说,我们的团队决定采用React作为主要的前端框架,因为它提供了组件化开发和虚拟DOM的优势,能够提高页面性能和开发效率。同时,我们制定了一套严格的代码规范,包括命名规范、文件组织方式等,以确保代码的可读性和可维护性。



  1. 组件化开发: 创建可重用的组件来构建用户界面,使代码更模块化、可复用和易于维护。


jsxCopy Code
// ProductItem.js
import React from "react";

function ProductItem({ name, price, imageUrl }) {
return (
<div className="product-item">
<img src={imageUrl} alt={name} />
<div className="product-details">
<h3>{name}</h3>
<p>{price}</p>
</div>
</div>

);
}

export default ProductItem;


  1. 虚拟DOM优势: 通过使用React的虚拟DOM机制,只进行必要的DOM更新,提高页面性能。


jsxCopy Code
// ProductList.js
import React, { useState } from "react";
import ProductItem from "./ProductItem";

function ProductList({ products }) {
const [selectedProductId, setSelectedProductId] = useState(null);

function handleItemClick(productId) {
setSelectedProductId(productId);
}

return (
<div className="product-list">
{products.map((product) => (
<ProductItem
key={product.id}
name={product.name}
price={product.price}
imageUrl={product.imageUrl}
onClick={() =>
handleItemClick(product.id)}
isSelected={selectedProductId === product.id}
/>
))}
</div>

);
}

export default ProductList;


  1. 代码规范示例: 制定一套严格的代码规范,包括命名规范、文件组织方式等。


命名规范示例:



文件组织方式示例:


Copy Code
src/
components/
ProductList.js
ProductItem.js
utils/
api.js
styles/
product.css
App.js
index.js

前端的价值


image.png
作为前端开发者,在业务中发挥着重要的作用,并能为团队和产品创造价值。前端的价值主要体现在以下几个方面:


1. 用户体验


前端开发直接影响用户体验,良好的界面设计和交互能够提高用户满意度并增加用户的黏性。通过技术的提升,我们可以实现更流畅的页面过渡效果、更友好的交互反馈等,从而提高用户对产品的喜爱度。


例如,在电商网站的商品详情页面中,我们可以通过使用React和动画库来实现图片的缩放效果和购物车图标的动态变化,以吸引用户的注意并提升用户体验。


jsxCopy Code
import React from 'react';
import { Motion, spring } from 'react-motion';

class ProductDetail extends React.Component {
constructor(props) {
super(props);
this.state = {
isImageZoomed: false,
isAddedToCart: false,
};
}

handleImageClick = () => {
this.setState({ isImageZoomed: !this.state.isImageZoomed });
};

handleAddToCart = () => {
this.setState({ isAddedToCart: true });
// 添加到购物车的逻辑
};

render() {
const { isImageZoomed, isAddedToCart } = this.state;

return (
<div>
<img
src={product.image}
alt={product.name}
onClick={this.handleImageClick}
style={{
transform: `scale(${isImageZoomed ? 2 : 1})`,
transition: 'transform 0.3s',
}}
/>

<button
onClick={this.handleAddToCart}
disabled={isAddedToCart}
className={isAddedToCart ? 'disabled' : ''}
>

{isAddedToCart ? '已添加到购物车' : '添加到购物车'}
</button>
</div>

);
}
}

export default ProductDetail;

2. 跨平台兼容性


在不同的浏览器和设备上,页面的呈现效果可能会有所差异。作为前端开发者,我们需要解决不同平台和浏览器的兼容性问题,确保页面在所有环境下都能正常运行。


通过了解各种前端技术和标准,我们可以使用一些兼容性较好的解决方案,如使用flexbox布局代替传统的浮动布局,使用媒体查询来适配不同的屏幕尺寸等。



  1. 使用Flexbox布局代替传统的浮动布局: Flexbox是一种弹性布局模型,能够更轻松地实现自适应布局和等高列布局。


cssCopy Code
.container {
display: flex;
flex-direction: row;
justify-content: space-between;
}

.item {
flex: 1;
}


  1. 使用媒体查询适配不同的屏幕尺寸: 媒体查询允许根据不同的屏幕尺寸应用不同的CSS样式。


cssCopy Code
@media (max-width: 767px) {
/* 小屏幕设备 */
}

@media (min-width: 768px) and (max-width: 1023px) {
/* 中等屏幕设备 */
}

@media (min-width: 1024px) {
/* 大屏幕设备 */
}


  1. 使用Viewport单位设置响应式元素: Viewport单位允许根据设备的视口尺寸设置元素的宽度和高度。


cssCopy Code
.container {
width: 100vw; /* 100% 视口宽度 */
height: 100vh; /* 100% 视口高度 */
}

.element {
width: 50vw; /* 50% 视口宽度 */
}


  1. 使用Polyfill填补兼容性差异: 对于一些不兼容的浏览器,可以使用Polyfill来实现缺失的功能,以确保页面在各种环境下都能正常工作。


htmlCopy Code
<script src="polyfill.js"></script>

3. 性能优化


用户对网页加载速度的要求越来越高,前端开发者需要关注页面性能并进行优化。这包括减少HTTP请求、压缩和合并资源、使用缓存机制等。


举例来说,我们可以通过使用Webpack等构建工具来将多个JavaScript文件打包成一个文件,并进行代码压缩,从而减少页面的加载时间。


结论


image.png
作为前端开发者,我们经常面临各种挑战,如业务目标的实现、技术团队策略的制定等。通过不断学习和提升,我们可以解决前端开发中的各种困难,并为业务目标做出贡献。同时,我们的工作还能够直接影响用户体验,提高产品的竞争。


作者:已注销
来源:juejin.cn/post/7262133010912100411
收起阅读 »

关于padStart和他的兄弟padEnd

web
遇到一个需求,后端返回最多六位的数字,然后显示到页面上。显示大概要这种效果。 这虽然也不是很难,最开始我是这样的 //html <div class="itemStyle" v-if="item in numList">{{item}}</...
继续阅读 »

遇到一个需求,后端返回最多六位的数字,然后显示到页面上。显示大概要这种效果。
image.png
这虽然也不是很难,最开始我是这样的


//html
<div class="itemStyle" v-if="item in numList">{{item}}</div>
//script
let numList;
const setNumberBlock = ()=>{
const bit = 4
const num = '123'//后端返回的数据,这里写死了。
const zorestr = '0'.repeat(bit-num.length)//repeat方法可以重复生成字符串
numList = (zorestr +num).split('')
//然后遍历numList
//大概就这么个意思
}

但是今天我发现了一个方法,他的名字叫padStart,他还有个兄弟叫padEnd;



padStart()padEnd() 是 JavaScript 字符串方法,用于在字符串的开始位置(padStart())或结束位置(padEnd())填充指定的字符,直到字符串达到指定的长度。



这两个方法的语法相似,都接受两个参数:



  • targetLength:表示字符串的目标长度,如果字符串的长度小于目标长度,则会在开始或结束位置填充指定的字符,直到字符串的长度达到目标长度。

  • padString:表示用于填充字符串的字符,它是一个可选参数。如果未提供 padString,则默认使用空格填充。


以下是两个方法的使用示例:


const str = '123';

const paddedStart = str.padStart(5, '0');
console.log(paddedStart); // 输出:00123

const paddedEnd = str.padEnd(5, '0');
console.log(paddedEnd); // 输出:12300

在这个示例中,padStart() 方法将在字符串的开始位置填充 0,直到字符串的长度达到 5,所以结果是 '00123'。而 padEnd() 方法将在字符串的结束位置填充 0,所以结果是 '12300'


这两个方法通常用于格式化数字,确保数字在特定长度内,并且可以按照需要在前面或后面填充零或其他字符。


然后这个需求就可以简化为这样


//html
<div class="itemStyle" v-if="item in numList">{{item}}</div>
//script
let numList;
const setNumberBlock = ()=>{
const num = '123'//后端返回的数据,这里写死了,需要时字符串哦。
numList = num.padStart(4,'0').split('')
//输出[0,1,2,3]
}


神奇小方法



有什么不对和更好的方法可以留言哦


作者:乐观的用户
来源:juejin.cn/post/7345107078904922164
收起阅读 »

别再这么写POST请求了~

       大家好,我是石头~        今天在进行组内code review,发现有一位同学在使用POST方式进行接口调用传参的时候,并不是按照HTTP规范,将参数写入到请求体中进行传输,而是拼接到接口URL上面。        那么,POST请求,是...
继续阅读 »

       大家好,我是石头~


       今天在进行组内code review,发现有一位同学在使用POST方式进行接口调用传参的时候,并不是按照HTTP规范,将参数写入到请求体中进行传输,而是拼接到接口URL上面。


       那么,POST请求,是否适宜将参数拼接到URL中呢?


图片


POST请求与参数传递的标准机制


       在讨论这个问题之前,我们先了解一下POST请求参数传递的正确方式是怎样的?


       按照HTTP协议规定,POST请求主要服务于向服务器提交数据的操作,这类数据通常包含表单内容、文件上传等。标准实践中,这些数据应封装于请求体(Request Body)内,而非附加在URL上。这是出于POST请求对数据容量和安全性的考量,URL因其长度限制和透明性特点并不适合作为大型或敏感数据的载体。


图片


URL参数拼接的风险


       从上所述,URL参数拼接并不是POST请求参数传递的正确方式,但是既然这样做也是可以正常进行请求的,对方服务端也能正常获取到参数,那么,URL参数拼接又有什么风险?



  • URL长度限制:URL长度并非无限制,大多数浏览器和服务器都有最大长度限制,一般在2000字符左右,若参数过多或过大,可能导致URL截断,进而使服务端无法完整接收到所有参数

  • 安全性隐患:将参数拼接到URL中,可能导致敏感信息泄露,如密码、密钥等。此外,URL中的参数容易被浏览器历史记录、缓存、代理服务器等记录,增加了信息泄露的风险

  • 不符合HTTP规范:POST请求通常将数据放在请求体中,而非URL中,违反这一规范可能导致与某些服务器或中间件的兼容性问题。


图片


POST传参正确写法


       以下是一个使用Java的HttpURLConnection发送Post请求并将数据放在请求体中的示例:


import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;

public class PostRequestExample {

public static void sendPostRequest(String requestUrl, String postData) throws Exception {
URL url = new URL(requestUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); // 设置请求头,表明请求体的内容类型

connection.setDoOutput(true); // 表示要向服务器写入数据
try (OutputStream os = connection.getOutputStream()) {
byte[] input = postData.getBytes("UTF-8"); // 将参数转换为字节数组,此处假设postData是已编码好的参数字符串
os.write(input, 0, input.length); // 将参数写入请求体
}

int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
// 处理响应...
} else {
// 错误处理...
}
}

public static void main(String[] args) throws Exception {
String requestUrl = "http://example.com/api/endpoint";
String postData = "param1=value1&param2=value2"; // 参数以键值对的形式编码
sendPostRequest(requestUrl, postData);
}
}

作者:石头聊技术
来源:juejin.cn/post/7341952374368108583
收起阅读 »

面试官问,如何在十亿级别用户中检查用户名是否存在?

前言 不知道大家有没有留意过,在使用一些app注册的时候,提示你用户名已经被占用了,需要更换一个,这是如何实现的呢?你可能想这不是很简单吗,去数据库里查一下有没有不就行了吗,那么假如用户数量很多,达到数亿级别呢,这又该如何是好? 数据库方案 第一种方案就是查...
继续阅读 »

前言


不知道大家有没有留意过,在使用一些app注册的时候,提示你用户名已经被占用了,需要更换一个,这是如何实现的呢?你可能想这不是很简单吗,去数据库里查一下有没有不就行了吗,那么假如用户数量很多,达到数亿级别呢,这又该如何是好?


数据库方案



第一种方案就是查数据库的方案,大家都能够想到,代码如下:


public class UsernameUniquenessChecker {
private static final String DB_URL = "jdbc:mysql://localhost:3306/your_database";
private static final String DB_USER = "your_username";
private static final String DB_PASSWORD = "your_password";

public static boolean isUsernameUnique(String username) {
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD)) {
String sql = "SELECT COUNT(*) FROM users WHERE username = ?";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, username);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
int count = rs.getInt(1);
return count == 0; // If count is 0, username is unique
}
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return false; // In case of an error, consider the username as non-unique
}

public static void main(String[] args) {
String desiredUsername = "new_user";
boolean isUnique = isUsernameUnique(desiredUsername);
if (isUnique) {
System.out.println("Username '" + desiredUsername + "' is unique. Proceed with registration.");
} else {
System.out.println("Username '" + desiredUsername + "' is already in use. Choose a different one.");
}
}
}

这种方法会带来如下问题:



  1. 性能问题,延迟高 如果数据量很大,查询速度慢。另外,数据库查询涉及应用程序服务器和数据库服务器之间的网络通信。建立连接、发送查询和接收响应所需的时间也会导致延迟。

  2. 数据库负载过高。频繁执行 SELECT 查询来检查用户名唯一性,每个查询需要数据库资源,包括CPU和I/O。



  1. 可扩展性差。数据库对并发连接和资源有限制。如果注册率继续增长,数据库服务器可能难以处理数量增加的传入请求。垂直扩展数据库(向单个服务器添加更多资源)可能成本高昂并且可能有限制。


缓存方案


为了解决数据库调用用户名唯一性检查的性能问题,引入了高效的Redis缓存。



public class UsernameCache {

private static final String REDIS_HOST = "localhost";
private static final int REDIS_PORT = 6379;
private static final int CACHE_EXPIRATION_SECONDS = 3600;

private static JedisPool jedisPool;

// Initialize the Redis connection pool
static {
JedisPoolConfig poolConfig = new JedisPoolConfig();
jedisPool = new JedisPool(poolConfig, REDIS_HOST, REDIS_PORT);
}

// Method to check if a username is unique using the Redis cache
public static boolean isUsernameUnique(String username) {
try (Jedis jedis = jedisPool.getResource()) {
// Check if the username exists in the Redis cache
if (jedis.sismember("usernames", username)) {
return false; // Username is not unique
}
} catch (Exception e) {
e.printStackTrace();
// Handle exceptions or fallback to database query if Redis is unavailable
}
return true; // Username is unique (not found in cache)
}

// Method to add a username to the Redis cache
public static void addToCache(String username) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.sadd("usernames", username); // Add the username to the cache set
jedis.expire("usernames", CACHE_EXPIRATION_SECONDS); // Set expiration time for the cache
} catch (Exception e) {
e.printStackTrace();
// Handle exceptions if Redis cache update fails
}
}

// Cleanup and close the Redis connection pool
public static void close() {
jedisPool.close();
}
}

这个方案最大的问题就是内存占用过大,假如每个用户名需要大约 20 字节的内存。你想要存储10亿个用户名的话,就需要20G的内存。


总内存 = 每条记录的内存使用量 * 记录数 = 20 字节/记录 * 1,000,000,000 条记录 = 20,000,000,000 字节 = 20,000,000 KB = 20,000 MB = 20 GB


布隆过滤器方案


直接缓存判断内存占用过大,有没有什么更好的办法呢?布隆过滤器就是很好的一个选择。


那究竟什么布隆过滤器呢?


布隆过滤器Bloom Filter)是一种数据结构,用于快速检查一个元素是否存在于一个大型数据集中,通常用于在某些情况下快速过滤掉不可能存在的元素,以减少后续更昂贵的查询操作。布隆过滤器的主要优点是它可以提供快速的查找和插入操作,并且在内存占用方面非常高效。


具体的实现原理和数据结构如下图所示:



布隆过滤器的核心思想是使用一个位数组(bit array)和一组哈希函数。



  • 位数组(Bit Array) :布隆过滤器使用一个包含大量位的数组,通常初始化为全0。每个位可以存储两个值,通常是0或1。这些位被用来表示元素的存在或可能的存在。

  • 哈希函数(Hash Functions) :布隆过滤器使用多个哈希函数,每个哈希函数可以将输入元素映射到位数组的一个或多个位置。这些哈希函数必须是独立且具有均匀分布特性。


那么具体是怎么做的呢?



  • 添加元素:如上图所示,当将字符串“xuyang”,“alvin”插入布隆过滤器时,通过多个哈希函数将元素映射到位数组的多个位置,然后将这些位置的位设置为1。

  • 查询元素:当要检查一个元素是否存在于布隆过滤器中时,通过相同的哈希函数将元素映射到位数组的相应位置,然后检查这些位置的位是否都为1。如果有任何一个位为0,那么可以确定元素不存在于数据集中。但如果所有位都是1,元素可能存在于数据集中,但也可能是误判。


本身redis支持布隆过滤器的数据结构,我们用代码简单实现了解一下:


import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class BloomFilterExample {
public static void main(String[] args) {
JedisPoolConfig poolConfig = new JedisPoolConfig();
JedisPool jedisPool = new JedisPool(poolConfig, "localhost", 6379);

try (Jedis jedis = jedisPool.getResource()) {
// 创建一个名为 "usernameFilter" 的布隆过滤器,需要指定预计的元素数量和期望的误差率
jedis.bfCreate("usernameFilter", 10000000, 0.01);

// 将用户名添加到布隆过滤器
jedis.bfAdd("usernameFilter", "alvin");

// 检查用户名是否已经存在
boolean exists = jedis.bfExists("usernameFilter", "alvin");
System.out.println("Username exists: " + exists);
}
}
}

在上述示例中,我们首先创建一个名为 "usernameFilter" 的布隆过滤器,然后使用 bfAdd 将用户名添加到布隆过滤器中。最后,使用 bfExists 检查用户名是否已经存在。


优点:



  • 节约内存空间,相比使用哈希表等数据结构,布隆过滤器通常需要更少的内存空间,因为它不存储实际元素,而只存储元素的哈希值。如果以 0.001 误差率存储 10 亿条记录,只需要 1.67 GB 内存,对比原来的20G,大大的减少了。

  • 高效的查找, 布隆过滤器可以在常数时间内(O(1))快速查找一个元素是否存在于集合中,无需遍历整个集合。


缺点:



  • 误判率存在:布隆过滤器在判断元素是否存在时,有一定的误判率。这意味着在某些情况下,它可能会错误地报告元素存在,但不会错误地报告元素不存在。

  • 不能删除元素:布隆过滤器通常不支持从集合中删除元素,因为删除一个元素会影响其他元素的哈希值,增加了误判率。


总结


Redis 布隆过滤器的方案为大数据量下唯一性验证提供了一种基于内存的高效解决方案,它需要在内存消耗和错误率之间取得一个平衡点。当然布隆过滤器还有更多应用场景,比如防止缓存穿透、防止恶意访问等。


作者:JAVA旭阳
来源:juejin.cn/post/7293786247655129129
收起阅读 »

编写LLVM Pass

iOS
在上一篇的基础上,编写一个简单的LLVM Pass。在llvm-project-17.0.6.src/llvm/include/llvm/Transforms/SweetWound/目录下,新建ModuleTest.h文件,并写入如下代码:// ModuleT...
继续阅读 »

上一篇的基础上,编写一个简单的LLVM Pass。

  1. llvm-project-17.0.6.src/llvm/lib/Transforms/目录下,新建一个文件夹SweetWound


  1. 在在llvm-project-17.0.6.src/llvm/include/llvm/Transforms/目录下,新建一个文件夹SweetWound


  1. Transforms目录下的CMakeLists.txt文件末尾,增加如下代码:

...
add_subdirectory(SweetWound)


  1. llvm-project-17.0.6.src/llvm/include/llvm/Transforms/SweetWound/目录下,新建ModuleTest.h文件,并写入如下代码:

// ModuleTest.h
#ifndef _LLVM_TRANSFORMS_SWEETWOUND_H_
#define _LLVM_TRANSFORMS_SWEETWOUND_H_
#include "llvm/Pass.h"
#include "llvm/IR/PassManager.h"
#include "llvm/IR/Module.h"

namespace llvm {
class ModuleTestPass : public PassInfoMixin {
public:
bool flag;
ModuleTestPass(bool flag) {
this->flag = flag;
}
PreservedAnalyses run(Module &M, ModuleAnalysisManager &AM);
static bool isRequired() {
return true;
}
};
}

#endif


  1. llvm-project-17.0.6.src/llvm/lib/Transforms/SweetWound/目录下,创建ModuleTest.cpp文件,并写入如下代码:

// ModuleTest.cpp
#include "llvm/Transforms/SweetWound/ModuleTest.h"

using namespace llvm;

PreservedAnalyses ModuleTestPass::run(Module &M, ModuleAnalysisManager &AM) {
if (this->flag == true) {
outs() << "[SW]:" << M.getName() << "\n";
return PreservedAnalyses::none();
}
return PreservedAnalyses::all();
}


  1. llvm-project-17.0.6.src/llvm/lib/Transforms/SweetWound/目录下,创建CMakeLists.txt文件,并写入如下代码:

add_llvm_component_library(LLVMSweetWound
ModuleTest.cpp

LINK_COMPONENTS
Analysis
Core
Support
TransformUtils
)


  1. 修改llvm-project-17.0.6.src/llvm/lib/Passes/PassBuilder.cpp文件:

......
#include
//======================导入头文件======================//
#include "llvm/Transforms/SweetWound/ModuleTest.h"
......
// ======================增加编译参数 begin ======================//
static cl::opt s_sw_test("test", cl::init(false), cl::desc("test module pass."));
// ======================增加编译参数 end ========================//

PassBuilder::PassBuilder(TargetMachine *TM, PipelineTuningOptions PTO,
std::optional PGOOpt,
PassInstrumentationCallbacks *PIC)
: TM(TM), PTO(PTO), PGOOpt(PGOOpt), PIC(PIC) {
......
// 注册Pass
this->registerPipelineStartEPCallback(
[](llvm::ModulePassManager &MPM, llvm::OptimizationLevel Level) {
MPM.addPass(ModuleTestPass(s_sw_test));
}
);
}
  1. 重新执行编译脚本,成功后,替换LLVM17.0.6.xctoolchain文件。

  2. 在Xcode的Build Settings-->Other C Flags中,设置编译参数:-mllvm -test:


  1. Command + B编译(或Command + R运行):


可以看到每个编译单元都有对应的输出,即代表编写的LLVM Pass加载成功!!!
收起阅读 »

编译llvm源码

iOS
前往LLVM官网,下载LLVM17.0.6版本的源码:下载源码后,解压到任意目录:在llvm-project-17.0.6.src同级目录下,编写编译脚本build.sh:#!/bin/shpwd_path=`pwd`build_llvm=${pwd_path...
继续阅读 »
  1. 前往LLVM官网,下载LLVM17.0.6版本的源码

  1. 下载源码后,解压到任意目录:


  1. llvm-project-17.0.6.src同级目录下,编写编译脚本build.sh:

#!/bin/sh
pwd_path=`pwd`
build_llvm=${pwd_path}/build-llvm #编译目录
installprefix=${pwd_path}/install #install目录
llvm_project=${pwd_path}/llvm-project-17.0.6.src/llvm #项目目录

mkdir -p $build_llvm
mkdir -p $installprefix

cmake -G Ninja -S ${llvm_project} -B $build_llvm \
-DLLVM_ENABLE_PROJECTS="clang" \
-DLLVM_CREATE_XCODE_TOOLCHAIN=ON \
-DLLVM_INSTALL_UTILS=ON \
-DCMAKE_INSTALL_PREFIX=$installprefix \
-DCMAKE_BUILD_TYPE=Release

ninja -C $build_llvm install-xcode-toolchain


  1. 执行编译脚本:

$ chmod +x ./build.sh
$ ./build.sh

编译过程需要大约20分钟左右。

  1. 编译完成之后,即可在当前目录下的install目录下看到编译产物:



  1. LLVM17.0.6.xctoolchain文件复制到~/Library/Developer/Toolchains/目录下:


  1. 点击菜单栏Xcode——>Toolchains,选择org.llvm.17.0.6:



  1. 在Xcode的Build Settings中,关闭Enable Index-While-Building Functionality


  1. Command+B编译(或Command + R 运行):


收起阅读 »

接口防止重复调用方案

web
大家好,今天我向大家介绍对于接口防重复提交的一些方案,请笑纳! 重复调用同个接口导致的问题 表单提交,输入框失焦、按钮点击、值变更提交等容易遇到重复请求的问题,即一次请求还没有执行完毕,用户又点击了一次,这样重复请求可能会造成后台数据异常。又比如在查询数据的...
继续阅读 »

大家好,今天我向大家介绍对于接口防重复提交的一些方案,请笑纳!


重复调用同个接口导致的问题



  • 表单提交,输入框失焦、按钮点击、值变更提交等容易遇到重复请求的问题,即一次请求还没有执行完毕,用户又点击了一次,这样重复请求可能会造成后台数据异常。又比如在查询数据的时候点击了一次查询,还在处理数据的时候,用户又点击了一次查询。第一次查询执行完毕页面已经有数据展示出来了,用户可能正在看呢,此时第二次查询也处理完返回到前台把页面刷新了,就会造成很不好的体验。


解决方案



  • 1、利用防抖避免重复调用接口

  • 2、采用禁用按钮的方式,loading、置灰等

  • 3、利用axios的cancelToken、AbortController方法取消重复请求

  • 4、利用promise的三个状态改造方法3


方法1:利用防抖



效果:当用户连续点击多次同一个按钮,最后一次点击之后,过小段时间后才发起一次请求

原理:每次调用方法后都产生一个定时器,定时器结束以后再发请求,如果重复调用方法,就取消当前的定时器,创建新的定时器,等结束后再发请求,可以用第三方封装的工具函数例如lodash的debounce方法来简化防抖的代码



<div id="app">    
<button @click="onClick">请求</button>
</div>

methods: {
// 调用lodash的防抖方法debounce,实现连续点击按钮多次,0.3秒后调用1次接口
onClick: _.debounce(async function(){
let res = await sendPost({username:'zs', age: 20})
console.log('请求的结果', res.data)
}, 300),
},
// 自定义指令防抖,在directive文件中自定义v-debounce指令
<el-button v-debounce:500="buttonDebounce">按钮</el-button>


  • 优缺点:


      防抖可以有效减少请求的频率,防止接口重复调用,但是如果接口响应比较慢,
    响应时间超过防抖的时间阈值,再次点击也会出现重复请求 需要在触发事件加上防抖处理,不够通用



方法2:采用禁用按钮的方式



禁用按钮:在发送请求之前,禁用按钮(利用loading或者disabled属性),直到请求完成后再启用它。这可以防止用户在请求进行中多次点击按钮



<div id="app">    
<button @click="sendRequest" :loading="loading">请求</button>
</div>

methods: {
async sendRequest() {
this.loading = true; // 禁用按钮
try { // 发送请求
await yourApiRequestFunction(); // 请求成功后,启用按钮
} catch (error) { // 处理错误情况 }
finally {
this.loading = false; // 请求完成后,启用按钮
}
},
}


  • 优缺点:


      最有效避免请求还在pending状态时,再次触发事件发起请求  
    不够通用,需要在按钮、tab、输入框等触发事件的地方都加上



方法3:利用axios取消接口的api



axios 内部提供的 CancelToken 来取消请求(AxiosV0.22.0版本中把CancelToken打上 👎deprecated 的标记,意味废弃。与此同时,推荐 AbortController 来取而代之)

通过axios请求拦截器,在每次请求前把请求信息和请求的取消方法放到一个map对象当中,并且判断map对象当中是否已经存在该请求信息的请求,如果存在取消上次请求



const pendingRequest = new Map();

function generateReqKey(config) {
const { method, url, params, data } = config;
return [method, url, Qs.stringify(params), Qs.stringify(data)].join("&");
}

function addPendingRequest(config) {
const requestKey = generateReqKey(config);
config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
if (!pendingRequest.has(requestKey)) {
pendingRequest.set(requestKey, cancel);
}
});
}

function removePendingRequest(config) {
const requestKey = generateReqKey(config);
if (pendingRequest.has(requestKey)) {
const cancelToken = pendingRequest.get(requestKey);
cancelToken(requestKey);
pendingRequest.delete(requestKey);
}
}

// axios拦截器代码
axios.interceptors.request.use(
function (config) {
removePendingRequest(config); // 检查是否存在重复请求,若存在则取消已发的请求
addPendingRequest(config); // 把当前请求信息添加到pendingRequest对象中
return config;
},
(error) => {
return Promise.reject(error);`
}
);
axios.interceptors.response.use(
(response) => {
removePendingRequest(response.config); // 从pendingRequest对象中移除请求
return response;
},
(error) => {
removePendingRequest(error.config || {}); // 从pendingRequest对象中移除请求
if (axios.isCancel(error)) {
console.log("已取消的重复请求:" + error.message);
} else {
// 添加异常处理
}
return Promise.reject(error);
}
);

image.png



  • 优缺点:


      可以防止前端重复响应相同数据导致体验不好的问题  
    但是这个取消请求只是前端取消了响应,取消时请求已经发出去了,后端还是会一一收到所有的请求,该查库的查库,该创建的创建,针对这种情形,服务端的对应接口需要进行幂等控制



方法4:利用promise的pending、resolve、reject状态



此方法其实是对cancelToken方案的改造,cancelToken是在请求还在pending状态时,判断接口是否重复,重复则取消请求,但是无法保证服务端是否接收到了请求,我们只要改造这点,在发送请求前判断是否有重复调用,如果用重复调用接口,利用promise的reject拦截请求,在请求resolve或reject状态后清除用来判断是否是重复请求的key



// axios全局拦截文件
import axios from '@/router/interceptors
import Qs from '
qs'

const cancelMap = new Map()

// 生成key用来判断是否是同个请求
function generateReqKey(config = {}) {
const { method = '
get', url, params, data } = config
const _params = typeof params === '
string' ? Qs.stringify(JSON.parse(params)) : Qs.stringify(params)
const _data = typeof data === '
string' ? Qs.stringify(JSON.parse(data)) : Qs.stringify(data)`
const str = [method, url, _params, _data].join('
&')
return str
}

function checkoutRequest(config) {
const requestKey = generateReqKey(config)
// 如果设置允许多次重复请求,直接返回成功,让网络请求继续流通下去
if (!cancelMap.has(requestKey) || config._allowRepeatRequest) {
cancelMap.set(requestKey, 0)
return new Promise((resolve, reject) => {
axios(config).then(res => {
resolve(res)
}).catch(err => {
reject(err)
})
})
} else {
// 如果存在重复请求
return new Promise((resolve, reject) => {
reject(new Error())
})
}
}

// 移除已响应的请求,移除的时间可设置响应后延迟移除,此时间内可以继续阻止重复请求
export async function removeRequest(config = {}) {
const time = config._debounceTime || 0
const requestKey = generateReqKey(config)
if (cancelMap.has(requestKey)) {
// 延迟清空,防止快速响应时多次重复调用
setTimeout(() => {
cancelMap.delete(requestKey)
}, time)
}
}

export default checkoutRequest


// @/router/interceptors 拦截器代码
axios.interceptors.request.use(
function (config) {
return config;
},
(error) => {
removeRequest(error.config) // 从cancelMap中移除key
return Promise.reject(error)
}
);
axios.interceptors.response.use(
(response) => {
removeRequest(response.config) // 从cancelMap中移除key
return response;
},
(error) => {
removeRequest(error.config || {}) // 从cancelMap中移除key
return Promise.reject(error);
}
);

// 接口可以配置_allowRepeatRequest开启允许重复请求
return request({
url: '
xxxxxxx',
method: '
post',
data: data,
loading: false,
_allowRepeatRequest: true
})


  • 优缺点:


      此方法效果跟禁用按钮的效果一致,但是可以全局修改,方案比较通用



其他



或者也可以在请求时加个全局loading,但是感觉都不如上一种好



ps



针对可能上传文件使用formData的情况,需要在重复请求那再判断一下



以上是为大家介的四种方法,有更好的建议请评论区留言。


image.png


作者:写代码真是太难了
来源:juejin.cn/post/7344536653464191013
收起阅读 »

前端打包版本号自增

web
1.新建sysInfo.json文件 { "version": "20240307@1.0.1" } 2.新建addVersion.js文件,打包时执行,将版本号写入sysInfo.json文件 //npm run build打包前执行此段代码 let f...
继续阅读 »

1.新建sysInfo.json文件


{
"version": "20240307@1.0.1"
}

2.新建addVersion.js文件,打包时执行,将版本号写入sysInfo.json文件


//npm run build打包前执行此段代码
let fs = require('fs')

//返回package的json数据
function getPackageJson() {
let data = fs.readFileSync('./src/assets/json/sysInfo.json') //fs读取文件
return JSON.parse(data) //转换为json对象
}

let packageData = getPackageJson() //获取package的json
let arr = packageData.version.split('@') //切割后的版本号数组
let date = new Date()
const year = date.getFullYear()
let month = date.getMonth() + 1
let day = date.getDate()
month = month > 9 ? month : '0' + month
day = day < 10 ? '0' + day : day
let today = `${year}${month}${day}`
let verarr = arr[1].split('.')
verarr[2] = parseInt(verarr[2]) + 1
packageData.version = today + '@' + verarr.join('.') //转换为以"."分割的字符串
//用packageData覆盖package.json内容
fs.writeFile('./src/assets/json/sysInfo.json', JSON.stringify(packageData, null, '\t'), err => {
console.log(err)
})


3.package.json中配置


  "scripts": {
"dev": "vite",
"serve": "vite",
"build": "node ./src/addVersion.js && vite build",
....

4.使用


import sysInfo from '@/assets/json/sysInfo.json'

作者:点赞侠01
来源:juejin.cn/post/7343811223207624745
收起阅读 »

面试常问:为什么 Vite 速度比 Webpack 快?

web
 前言 最近作者在学习 webpack 相关的知识,之前一直对这个问题不是特别了解,甚至讲不出个123....,这个问题在面试中也是常见的,作者在学习的过程当中总结了以下几点,在这里分享给大家看一下,当然最重要的是要理解,这样回答的时候就不用死记硬背了。 原因...
继续阅读 »

 前言


最近作者在学习 webpack 相关的知识,之前一直对这个问题不是特别了解,甚至讲不出个123....,这个问题在面试中也是常见的,作者在学习的过程当中总结了以下几点,在这里分享给大家看一下,当然最重要的是要理解,这样回答的时候就不用死记硬背了。


原因


1、开发模式的差异


在开发环境中,Webpack 是先打包再启动开发服务器,而 Vite 则是直接启动,然后再按需编译依赖文件。(大家可以启动项目后检查源码 Sources 那里看到)


这意味着,当使用 Webpack 时,所有的模块都需要在开发前进行打包,这会增加启动时间和构建时间。


Vite 则采用了不同的策略,它会在请求模块时再进行实时编译,这种按需动态编译的模式极大地缩短了编译时间,特别是在大型项目中,文件数量众多,Vite 的优势更为明显。


Webpack启动



Vite启动



2、对ES Modules的支持


现代浏览器本身就支持 ES Modules,会主动发起请求去获取所需文件。Vite充分利用了这一点,将开发环境下的模块文件直接作为浏览器要执行的文件,而不是像 Webpack 那样先打包,再交给浏览器执行。这种方式减少了中间环节,提高了效率。


什么是ES Modules?


通过使用 exportimport 语句,ES Modules 允许在浏览器端导入和导出模块。


当使用 ES Modules 进行开发时,开发者实际上是在构建一个依赖关系图,不同依赖项之间通过导入语句进行关联。


主流浏览器(除IE外)均支持ES Modules,并且可以通过在 script 标签中设置 type="module"来加载模块。默认情况下,模块会延迟加载,执行时机在文档解析之后,触发DOMContentLoaded事件前。



3、底层语言的差异


Webpack 是基于 Node.js 构建的,而 Vite 则是基于 esbuild 进行预构建依赖。esbuild 是采用 Go 语言编写的,Go 语言是纳秒级别的,而 Node.js 是毫秒级别的。因此,Vite 在打包速度上相比Webpack 有 10-100 倍的提升。


什么是预构建依赖?


预构建依赖通常指的是在项目启动或构建之前,对项目中所需的依赖项进行预先的处理或构建。这样做的好处在于,当项目实际运行时,可以直接使用这些已经预构建好的依赖,而无需再进行实时的编译或构建,从而提高了应用程序的运行速度和效率。


4、热更新的处理


在 Webpack 中,当一个模块或其依赖的模块内容改变时,需要重新编译这些模块。


而在 Vite 中,当某个模块内容改变时,只需要让浏览器重新请求该模块即可,这大大减少了热更新的时间。


总结


总的来说,Vite 之所以比 Webpack 快,主要是因为它采用了不同的开发模式充分利用了现代浏览器的 ES Modules 支持使用了更高效的底层语言并优化了热更新的处理。这些特点使得 Vite在大型项目中具有显著的优势,能够快速启动和构建,提高开发效率。



作者:JacksonChen
来源:juejin.cn/post/7344916114204049445
收起阅读 »