注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

EdgeUtils:安卓沉浸式方案(edge to edge)封装

EdgeUtils     项目地址:github.com/JailedBird/… 1、 接入方式 EdgeUtils是基于androidx.core,对edge to edge沉浸式方案封装 📦 接入方式: 添加jitpack仓库 maven { ur...
继续阅读 »

EdgeUtils


GitHub stars GitHub forks GitHub issues 


项目地址:github.com/JailedBird/…


1、 接入方式


EdgeUtils是基于androidx.core,对edge to edge沉浸式方案封装 📦


接入方式:



  • 添加jitpack仓库


maven { url 'https://jitpack.io' }


  • 添加依赖


implementation 'com.github.JailedBird:EdgeUtils:1.0.0'

2、 使用方式


2-1、 布局拓展全屏


Activity中使用API edgeToEdge() 将开发者实现的布局拓展到整个屏幕, 同时为避免冲突, 将状态栏和到导航栏背景色设备为透明;


1669552233097-eacf0003-1ede-4035-a24e-ace16bfbe400.gif


注意:edgeToEdge() 的参数withScrim表示是否启用系统默认的反差色保护, 不是很熟悉的情况下直接使用默认true即可;


2-2、 系统栏状态控制


布局拓展之后, 开发者布局内容会显示在状态栏和导航栏区域, 造成布局和系统栏字体重叠(时间、电量……);


此时为确保系统栏字体可见,应该设置其字体; 设置规则:白色(浅色)背景设置黑色字体(edgeSetSystemBarLight(true)),黑色(深色)背景设置白色字体(注:系统栏字体只有黑色和白色)(edgeSetSystemBarLight(false));


如果未作夜间模式适配, 默认使用 edgeSetSystemBarLight(true)浅色模式即可!


综合1、2我们的基类可以写成如下的形式:


abstract class BasePosActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
      if (usingEdgeToEdgeTheme()) {
              defaultEdgeToEdge()
      } else {
          customThemeSetting()
      }
      super.onCreate(savedInstanceState)
  }
}

protected open fun defaultEdgeToEdge() {
    edgeToEdge(false)
    edgeSetSystemBarLight(true)
}

2-3、 解决视觉冲突


2-3-1、状态栏适配


步骤一布局拓展全屏会导致视觉上的冲突, 下面是几种常见的思路:请灵活使用



  • 布局中添加View(id="@+id/edge")使用heightToTopSystemWindowInsets API动态监听并修改View的高度为状态栏的高度


    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
          android:orientation="vertical">

          <View
              android:id="@+id/edge"
              android:layout_width="match_parent"
              android:layout_height="0dp" />
          xxx
      </LinearLayout>
    binding.edge.heightToTopSystemWindowInsets()


  • 直接获取状态栏的高度,API为:edgeStatusBarHeight; 和1不同的是,1中View的height会随状态栏高度变化而变化,2不会; 此外获取状态栏高度需要在View Attached之后才可以(否则高度为0),因此使用suspend函数等待Attached后才返回状态栏,确保在始终能获取到正确的状态栏高度!


    lifecycleScope.launch {
      val height = edgeStatusBarHeight()
      xxx
    }


  • 针对有Toolbar的布局, 可直接为Toolbar加padding(or margin), 让padding的高度为状态栏高度!如果无效, 一般都与Toolbar的高度测量有关, 可以直接在Toolbar外层包上FrameLayout,为FrameLayout加padding, 详情阅读下文了解原理,从而灵活选择;


    fun View.paddingTopSystemWindowInsets() =
      applySystemWindowInsetsPadding(applyTop = true)

    fun View.marginTopSystemWindowInsets() =
      applySystemWindowInsetsMargin(applyTop = true)



2-3-2、 导航栏适配


导航栏的适配原理和状态栏适配是非常相似的, 需要注意的是 导航栏存在三种模式:



  • 全面屏模式

  • 虚拟导航栏

  • 虚拟导航条


API已经针对导航栏高度、导航栏高度margin和padding适配做好了封装,使用者无需关心;


fun View.paddingBottomSystemWindowInsets() =
  applySystemWindowInsetsPadding(applyBottom = true)
   
fun View.marginBottomSystemWindowInsets() =
  applySystemWindowInsetsMargin(applyBottom = true)

适配思路是一致的,不再赘述;


2-4、 解决手势冲突


手势冲突和视觉冲突产生的原理是相同的,不过是前者无形而后者有形;系统级别热区(如侧滑返回)优先级是要高于View的侧滑的, 因此有时候需要避开(情况很少)


EdgeUtils主要工作只是做了视觉冲突的解决和一些API封装;使用者可以基于封装的API拓展,替换掉WindowInsetCompat.Type为你需要的类型;


fun View.applySystemWindowInsetsPadding(
  applyLeft: Boolean = false,
  applyTop: Boolean = false,
  applyRight: Boolean = false,
  applyBottom: Boolean = false,
)
{
  doOnApplyWindowInsets { view, insets, padding, _ ->
  // val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
  // 替换为Type.SYSTEM_GESTURES即可,其他类似
      val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemGestures())
      val left = if (applyLeft) systemBars.left else 0
      val top = if (applyTop) systemBars.top else 0
      val right = if (applyRight) systemBars.right else 0
      val bottom = if (applyBottom) systemBars.bottom else 0

      view.setPadding(
          padding.left + left,
          padding.top + top,
          padding.right + right,
          padding.bottom + bottom
      )
  }
}

3、 Edge教程


3-1 何为edge to edge?


如何彻底理解Edge to edge的思想呢?


或许你需要官方文章 , 也可以看的我写的翻译文章doc1😘


3-2 底层是如何实现的?


了解Edge to edge原理后,你或许会好奇他是怎么实现的?


或许你需要Flywith24大佬的文章 , 也可看缩略文章doc2😘


3-3 其他杂项记录


请看doc3 , 东西多但比较杂没整理😘


3-4 如何快速上手?


EdgeUtils此框架基于androidx.core, 对WindowInsets等常见API进行封装,提供了稳定的API和细节处理;封装的API函数名称通俗易懂,理解起来很容易, 难点是需要结合 [Edge-to-edge](#Edge to edge) 的原理去进行灵活适配各种界面


项目中存在三个demo对于各种常见的场景进行了处理和演示



  • navigation-sample 基于Navigation的官方demo, 此demo展示了Navigation框架下这种单Activity多Fragment的沉浸式缺陷

  • navigation-edge-sample 使用此框架优化navigation-sample, 使其达到沉浸式的效果

  • immersion-sample 基于开源项目immersionbar中的demo进行EdgeUtils的替换处理, 完成大部分功能的替换 (注:已替换的会标记[展示OK],部分未实现)


4、 注意事项


4-1、 Toolbar通过paddingTop适配statusbar失效的问题


很多时候, 状态栏的颜色和ToolBar的颜色是一致的, 这种情况下我们可以想到为ToolBar加 paddingTop = status_bar_height但是注意如果你的Toolbar高度为固定、或者测量的时候没处理好padding,那么他就可能失真;


快速判断技巧:xml布局预览中(假设状态栏高度预估为40dp),使用tools:padding = 40dp, 通过预览查看这40dp的padding是否对预览变成预期之外的变形,如果OK那么直接使用paddingTopSystemWindowInsets为ToolBar大多是没问题的


可以看下下面的2个例子:



  • paddingTop = 0时候, 如下的代码:


<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/teal_200"
android:paddingTop="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:title="高度测试" />


  • UI预览可以看到是这个样子的:


image-20221124102655144



  • paddingTop = 20时候, 如下的代码:


<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/teal_200"
android:paddingTop="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:title="高度测试" />


  • 可以看到, Toolbar的总高度是不变的,内容高度下移20dp,这显然是不合理的;实际运行时动态为ToolBar添加statusbar的paddingTop肯定也会导致这样的问题


image-20221124103232396


解决方案:


1、 使用FrameLayout等常见ViewGr0up包住ToolBar,将paddingTop高度设置到FrameLayout中, 将颜色teal_200设置到FrameLayout


<FrameLayout
android:id="@+id/layout_tool"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="20dp"
android:background="@color/teal_200">

<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/teal_200"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:title="高度测试" />

</FrameLayout>

如下:


image-20221124103542651


2、 在ToolBar外层直接封装FrameLayout(LinearLayout等也可, 下文统一用FrameLayout替代);


我相信大家一般都不会直接使用原生的Toolbar, 每个公司或多或少的都封装了一些自定义ToolBar;按照上述1的思路, 我们不难发现:



  • 如果自定义ToolBar继承自FrameLayout(或者说Toolbar最外层被FrameLayout包住), 直接将paddingTop加到自定义ToolBar即可;

  • 当然有些做的好的公司可能会直接通过继承ViewGr0up(如原生ToolBar), 这个时候可能就只能用方案1了;


当然上述几点都是具体问题具体分析, 大家可以在预览界面临时加paddingTop,看看实际是什么样的, 便于大家尽早发现问题;可以参考下BottomNavigationView的源码, 它间接继承自FrameLayout, 内部对paddingBottom自动适配了navigation_bar_height;


这个思路和ImmersionBar的 状态栏与布局顶部重叠解决方案 类似,不同的是,ImmersionBar使用的是固定的高度,而方案1是动态监听状态栏的高度并设置FrameLayout的paddingTop;


注:上述的paddingTop = 20dp, 只是方便预览添加的, 运行时请通过API动态设置paddingTop = statusBar


3、 添加空白View,通过代码设置View高度为导航栏、状态栏高度时,存在坑;约束布局中0dp有特殊含义,可能导致UI变形,需要注意哈!特别是处理导航栏的时候,全屏时导航栏高度为0,就会导致View高度为0,如果有组件依赖他,可能会出现奇怪问题,因此最好现在布局预览中排查下


4-2、 Bug&兼容性(框架已修复)


直接使用Edge to edge(参照google官方文档)存在一个大坑:调用hide隐藏状态栏后会导致状态栏变黑, 并且内容区域无法铺满


详细描述看这里:point_right: WindowInsetsControllerCompat.hide makes status bar background undrawable


private fun setWindowEdgeToEdge(window: Window) {
WindowCompat.setDecorFitsSystemWindows(window, false)
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
}

WindowCompat.getInsetsController(this, this.decorView)?.let {
it.systemBarsBehavior = behavior
it.hide(WindowInsetsCompat.Type.statusBars())
}

具体表现下图这个样子:


image-20221125143449641


解决方案如下 :point_down: How to remove top status bar black background


object EdgeUtils {
/** To fix hide status bar black background please using this post
* youtube: https://www.youtube.com/watch?v=yukwno2GBoI
* stackoverflow: https://stackoverflow.com/a/72773422/15859474
* */

private fun Activity.edgeToEdge() {
requestWindowFeature(Window.FEATURE_NO_TITLE)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.attributes.layoutInDisplayCutoutMode = WindowManager
.LayoutParams
.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
setWindowEdgeToEdge(this.window)
}

private fun setWindowEdgeToEdge(window: Window) {
WindowCompat.setDecorFitsSystemWindows(window, false)
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
}

4-3、 如何去掉scrim?


在导航栏设置为全透明时, 部分机型就会出现scrim半透明遮罩,考虑到样式有点丑陋, 直接将其修改为#01000000, 这样看起来也是完全透明的, 但是系统判定其alpha不为0, 不会主动添加scrim的; 【具体请看官方文档】


private fun setWindowEdgeToEdge(window: Window) {
WindowCompat.setDecorFitsSystemWindows(window, false)
/** using not transparent avoid scrim*/
Color.parseColor("#01000000").let { color ->
window.statusBarColor = color
window.navigationBarColor = color
}
}

4-4 、 禁止View的多次监听


一个View只能绑定一次ApplyWindowInset的监听,多次绑定可能会导致之前的失效或者出现奇怪问题!!!



5、 参考资料



作者:JailedBird
来源:juejin.cn/post/7313742254144307236
收起阅读 »

你还在使用websocket实现实时消息推送吗?

web
前言 在日常的开发中,我们经常能碰见服务端需要主动推送给客户端数据的业务场景,比如数据大屏的实时数据,比如消息中心的未读消息,比如聊天功能等等。 本文主要介绍SSE的使用场景和如何使用SSE。 服务端向客户端推送数据的实现方案有哪几种? 我们常规实现这些需求...
继续阅读 »

前言


在日常的开发中,我们经常能碰见服务端需要主动推送给客户端数据的业务场景,比如数据大屏的实时数据,比如消息中心的未读消息,比如聊天功能等等。


本文主要介绍SSE的使用场景和如何使用SSE。


image.png


服务端向客户端推送数据的实现方案有哪几种?


我们常规实现这些需求的方案有以下三种



  1. 轮询

  2. websocket

  3. SSE


轮询简介


在很久很久以前,前端一般使用轮询来进行服务端向客户端进行消息的伪推送,为什么说轮询是伪推送?因为轮询本质上还是通过客户端向服务端发起一个单项传输的请求,服务端对这个请求做出响应而已。通过不断的请求来实现服务端向客户端推送数据的错觉。并不是服务端主动向客户端推送数据。显然,轮询一定是上述三个方法里最下策的决定。


轮询的缺点:



  1. 首先轮询需要不断的发起请求,每一个请求都需要经过http建立连接的流程(比如三次握手,四次挥手),是没有必要的消耗。

  2. 客户端需要从页面被打开的那一刻开始就一直处理请求。虽然每次轮询的消耗不大,但是一直处理请求对于客户端来说一定是不友好的。

  3. 浏览器请求并发是有限制的。比如Chrome 最大并发请求数目为 6,这个限制还有一个前提是针对同一域名的,超过这一限制的后续请求将会被阻塞。而轮询意味着会有一个请求长时间的占用并发名额

  4. 而如果轮询时间较长,可能又没有办法非常及时的获取数据


websocket简介


websocket是一个双向通讯的协议,他的优点是,可以同时支持客户端和服务端彼此相互进行通讯。功能上很强大。


缺点也很明显,websocket是一个新的协议,ws/wss。也就是说,支持http协议的浏览器不一定支持ws协议。


相较于SSE来说,websocket因为功能更强大。结构更复杂。所以相对比较


websocket对于各大浏览器的兼容性↓
image.png


SSE简介


sse是一个单向通讯的协议也是一个长链接,它只能支持服务端主动向客户端推送数据,但是无法让客户端向服务端推送消息。


长链接是一种HTTP/1.1的持久连接技术,它允许客户端和服务器在一次TCP连接上进行多个HTTP请求和响应,而不必为每个请求/响应建立和断开一个新的连接。长连接有助于减少服务器的负载和提高性能。

SSE的优点是,它是一个轻量级的协议,相对于websockte来说,他的复杂度就没有那么高,相对于客户端的消耗也比较少。而且SSE使用的是http协议(websocket使用的是ws协议),也就是现有的服务端都支持SSE,无需像websocket一样需要服务端提供额外的支持。


注意:IE大魔王不支持SSE


SSE对于各大浏览器的兼容性↓
image.png


注意哦,上图是SSE对于浏览器的兼容不是对于服务端的兼容。


websocket和SSE有什么区别?


轮询


对于当前计算机的发展来说,几乎很少出现同时不支持websocket和sse的情况,所以轮询是在极端情况下浏览器实在是不支持websocket和see的下策。


Websocket和SSE


我们一般的服务端和客户端的通讯基本上使用这两个方案。首先声明:这两个方案没有绝对的好坏,只有在不同的业务场景下更好的选择。


SSE的官方对于SSE和Websocket的评价是



  1. WebSocket是全双工通道,可以双向通信,功能更强;SSE是单向通道,只能服务器向浏览器端发送。

  2. WebSocket是一个新的协议,需要服务器端支持;SSE则是部署在HTTP协议之上的,现有的服务器软件都支持。

  3. SSE是一个轻量级协议,相对简单;WebSocket是一种较重的协议,相对复杂。

  4. SSE默认支持断线重连,WebSocket则需要额外部署。

  5. SSE支持自定义发送的数据类型。


Websocket和SSE分别适用于什么业务场景?


对于SSE来说,它的优点就是轻,而且对于服务端的支持度要更好。换言之,可以使用SSE完成的功能需求,没有必要使用更重更复杂的websocket。


比如:数据大屏的实时数据,消息中心的消息推送等一系列只需要服务端单方面推送而不需要客户端同时进行反馈的需求,SSE就是不二之选。


对于Websocket来说,他的优点就是可以同时支持客户端和服务端的双向通讯。所适用的业务场景:最典型的就是聊天功能。这种服务端需要主动向客户端推送信息,并且客户端也有向服务端推送消息的需求时,Websocket就是更好的选择。


SSE有哪些主要的API?


建立一个SSE链接 :var source = new EventSource(url);

SSE连接状态


source.readyState



  • 0,相当于常量EventSource.CONNECTING,表示连接还未建立,或者连接断线。

  • 1,相当于常量EventSource.OPEN,表示连接已经建立,可以接受数据。

  • 2,相当于常量EventSource.CLOSED,表示连接已断,且不会重连。


SSE相关事件



  • open事件(连接一旦建立,就会触发open事件,可以定义相应的回调函数)

  • message事件(收到数据就会触发message事件)

  • error事件(如果发生通信错误(比如连接中断),就会触发error事件)


数据格式


Content-Type: text/event-stream //文本返回格式
Cache-Control: no-cache //不要缓存
Connection: keep-alive //长链接标识

image.png


SSE:相关文档,文档入口文档入口文档入口文档入口


显然,如果直接看api介绍不论是看这里还是看官网,大部分同学都是比较懵圈的状态,那么我们写个demo来看一下?


image.png


demo请看下方


我更建议您先把Demo跑起来,然后在看看上面这个w3cschool的SSE文档。两个配合一起看,会更方便理解些。


image.png


如何实操一个SSE链接?Demo↓


这里Demo前端使用的就是最基本的html静态页面连接,没有使用任何框架。
后端选用语言是node,框架是Express。


理论上,把这两段端代码复制过去跑起来就直接可以用了。



  1. 第一步,建立一个index.html文件,然后复制前端代码Demo到index.html文件中,打开文件

  2. 第二步,进入一个新的文件夹,建立一个index.js文件,然后将后端Demo代码复制进去,然后在该文件夹下执行


npm init          //初始化npm       
npm i express //下载node express框架
node index //启动服务

image.png


在这一层文件夹下执行命令。


完成以上操作就可以把项目跑起来了


前端代码Demo


<!DOCTYPE html>
<html lang="en">
<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>
<ul id="ul">

</ul>
</body>
<script>

//生成li元素
function createLi(data){
let li = document.createElement("li");
li.innerHTML = String(data.message);
return li;
}

//判断当前浏览器是否支持SSE
let source = ''
if (!!window.EventSource) {
source = new EventSource('http://localhost:8088/sse/');
}else{
throw new Error("当前浏览器不支持SSE")
}

//对于建立链接的监听
source.onopen = function(event) {
console.log(source.readyState);
console.log("长连接打开");
};

//对服务端消息的监听
source.onmessage = function(event) {
console.log(JSON.parse(event.data));
console.log("收到长连接信息");
let li = createLi(JSON.parse(event.data));
document.getElementById("ul").appendChild(li)
};

//对断开链接的监听
source.onerror = function(event) {
console.log(source.readyState);
console.log("长连接中断");
};

</script>
</html>

后端代码Demo(node的express)


const express = require('express'); //引用框架
const app = express(); //创建服务
const port = 8088; //项目启动端口

//设置跨域访问
app.all("*", function(req, res, next) {
//设置允许跨域的域名,*代表允许任意域名跨域
res.header("Access-Control-Allow-Origin", '*');
//允许的header类型
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
//跨域允许的请求方式
res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
// 可以带cookies
res.header("Access-Control-Allow-Credentials", true);
if (req.method == 'OPTIONS') {
res.sendStatus(200);
} else {
next();
}
})

app.get("/sse",(req,res) => {
res.set({
'Content-Type': 'text/event-stream', //设定数据类型
'Cache-Control': 'no-cache',// 长链接拒绝缓存
'Connection': 'keep-alive' //设置长链接
});

console.log("进入到长连接了")
//持续返回数据
setInterval(() => {
console.log("正在持续返回数据中ing")
const data = {
message: `Current time is ${new Date().toLocaleTimeString()}`
};
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 1000);
})

//创建项目
app.listen(port, () => {
console.log(`项目启动成功-http://localhost:${port}`)
})

效果


动画3.gif


总结



  1. SSE比websocket更轻

  2. SSE是基于http/https协议的

  3. websocket是一个新的协议,ws/wss协议

  4. 如果只需要服务端向客户端推送消息,推荐使用SSE

  5. 如果需要服务端和客户端双向推送,请选择websocket

  6. 不论是SSE还是websocket,对于浏览器的兼容性都不错

  7. 轮询是下策,很占用客户端资源,不建议使用。(不过偷懒的时候他确实方便)

  8. IE不支持SSE

  9. 小白同学demo如果跑不明白可以私信我


对了,小程序不支持SSE哦


image.png


最后


如果文章对您有帮助的话。


image.png


作者:工边页字
来源:juejin.cn/post/7325730345840066612
收起阅读 »

vscode+vite+ts助你高效开发uni-app项目

前言 最近在基于uni-app开发小程序,由于公司使用的是 HBuilder创建的项目,每次都需要打开HBuilderX当运行工具,开发体验真是难受至极。打算使用vscode + vite + ts创建一套模版,脱离 HBuilder 为什么不喜欢HBuild...
继续阅读 »

前言


最近在基于uni-app开发小程序,由于公司使用的是 HBuilder创建的项目,每次都需要打开HBuilderX当运行工具,开发体验真是难受至极。打算使用vscode + vite + ts创建一套模版,脱离 HBuilder


为什么不喜欢HBuilderX呢?



  1. 超级难用的git管理全局搜索,谁用谁知道

  2. 界面风格,代码样式,格式化,插件生态相比vscode都太差了

  3. 习惯了vscode开发


Snipaste_2023-09-05_21-53-02.png



点击查看 github



cli创建uni-app 项目


1、 创建 Vue3/Vite 工程


# npx degit https://github.com/dcloudio/uni-preset-vue.git#分支名称 自定义项目名称

# 创建以 javascript 开发的工程
npx degit dcloudio/uni-preset-vue#vite uni-starter

# 创建以 typescript 开发的工程
npx degit dcloudio/uni-preset-vue#vite-ts uni-starter



  • degit 可以帮助你从任意 git 仓库中克隆纯净的项目,忽略整个仓库的 git 历史记录。

  • 可以使用 npm install -g degit 命令全局安装



2、进入工程目录


cd uni-starter

3、更新 uni-app依赖版本


npx @dcloudio/uvm@latest

4、安装依赖


推荐一个好用的包管理器 antfu/ni


ni 或 pnpm install 或 bun install

5、运行


# 运行到 h5   
npm run dev:h5
# 运行到 app
npm run dev:app
# 运行到 微信小程序
npm run dev:mp-weixin

6、打包


# 打包到 h5   
npm run build:h5
# 打包到 app
npm run build:app
# 打包到 微信小程序
npm run build:mp-weixin

dcloudio 官方更多模版地址


自动引入



使用了自动引入就无需写下面的 import {xx} from @dcloudio/uni-app/vue。


如果不喜欢此方式可忽略



每个页面使用vue api或者uniapp api都需要引入,个人感觉有些麻烦


import { shallowRef,computed,watch } from 'vue';
import { onLoad,onShow } from "@dcloudio/uni-app";

1、 下载自动引入插件 pnpm add unplugin-auto-import -D


2、vite.config.ts 配置如下:


import { defineConfig } from "vite";
import uni from "@dcloudio/vite-plugin-uni";
// 引入自动导入插件
import AutoImport from 'unplugin-auto-import/vite'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [
uni(),
// 配置自动导入 vue相关函数, uni-app相关函数。ref, reactive,onLoad等
AutoImport({
imports: ['vue','uni-app'],
dts: './typings/auto-imports.d.ts',
}),
],
});

3、tsconfig.json include新增如下类型文件配置


"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
// unplugin-auto-import/vite自动引入的类型声明文件
"typings/**/*.d.ts",
"typings/**/*.ts"
]


注意: Option 'importsNotUsedAsValues' is deprecated and will stop functioning in TypeScript 5.5. Specify compilerOption '"ignoreDeprecations": "5.0"' to silence this error. Use 'verbatimModuleSyntax' instead


翻译一下: 选项“importsNotUsedAsValues”已弃用,并将停止在TypeScript 5.5中运行。指定compilerOption“”ignoreDeprecations“:”5.0“”以消除此错误。 请改用“verbatimModuleSyntax”。


如果出现此警告⚠️可添加如下配置



Snipaste_2023-08-22_23-20-42.png


eslint自动格式化



为了使用方便,这里直接使用 antfu大佬的插件了,有需要的配置自行再添加到rules里面。


注意: 这个插件可能更适合web端,antfu基本是不写小程序的,如果有特殊需要或者想更适合小程序版本格式化可以自行配置或者网上找一些格式化方案,这类文章还是比较多的。



使用 eslint + @antfu/eslint-config点击查看使用


1、 安装插件


pnpm add -D eslint @antfu/eslint-config

2、新建.eslintrc.cjs


module.exports = {
root: true,
env: {
browser: true,
node: true,
es6: true,
},
// https://github.com/antfu/eslint-config
extends: '@antfu',
rules: {
// your custom rules...
'vue/html-self-closing': ['error', {
html: { normal: 'never', void: 'always' },
}],
'no-console': 'off', // 禁用对 console 的报错检查
// "@typescript-eslint/quotes": ["error", "double"], // 强制使用双引号
'@typescript-eslint/semi': ['error', 'always'], // 强制使用行位分号
},
};


3、新建.vscode/settings.json


{
// 禁用 prettier,使用 eslint 的代码格式化
"prettier.enable": false,
// 保存时自动格式化
"editor.formatOnSave": false,
// 保存时自动修复
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.organizeImports": false
},
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml"
]
}

4、此时打开App.vue 查看已经检查出规范了,只要保存就会自动格式化


eslint-format.gif


5、提交代码时自动对暂存区代码进行格式化操作


pnpm add -D lint-staged simple-git-hooks

// package.json
"scripts": {
+ "prepare": "pnpx simple-git-hooks",
}
+"simple-git-hooks": {
+ "pre-commit": "pnpm lint-staged"
+},
+"lint-staged": {
+ "*": "eslint --fix"
+}


"prepare": "pnpx simple-git-hooks": 在执行npm install命令之后执行的脚本,用于初始化simple-git-hooks配置



editorConfig 规范



项目根目录添加.editorConfig文件,统一不同编辑器的编码风格和规范。


vscode需要安装插件EditorConfig for VS Code获取支持



# @see: http://editorconfig.org

root = true

[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符编码为 utf-8
indent_style = space # 缩进风格为 空格(tab | space)
indent_size = 2 # 缩进大小为 2
end_of_line = lf # 换行符为 lf (crlf | lf | cr)
insert_final_newline = true # 在文件末尾插入一个新行
trim_trailing_whitespace = true # 去除行尾空格

[*.md] # 表示所有 .md 文件适用
insert_final_newline = false # 在文件末尾不插入一个新行
trim_trailing_whitespace = false # 不去除行尾空格


安装组件库


成套的全端兼容ui库包括:



  • uni-ui:官方组件库,兼容性好、组件封装性好、功能强大,而且还有大佬编写的ts类型。目前正在使用的组件库

  • uview-plus:uview-plus3.0是基于uView2.x修改的vue3版本。

  • uViewUI:组件丰富、文档清晰,支持nvue

  • colorUI css库:颜值很高,css库而非组件

  • 图鸟UI:高颜值UI库

  • 图鸟UI vue3版:高颜值UI库,vue3+ts版组件,值得尝试

  • first UI:分开源版和商业版,虽然组件很全、功能强大,但是大多数组件都是需要购买的商业版才能用


1、安装组件


pnpm add @dcloudio/uni-ui -S
pnpm add sass -D

2、配置easycom自动引入组件


// pages.json
{
"easycom": {
"autoscan": true,
"custom": {
// uni-ui 规则如下配置
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
},
// 其他内容
pages:[
// ...
]
}

3、安装uni-uits类型库


pnpm add -D @uni-helper/uni-ui-types

具体使用方法请查看:uni-ui-types


后续


模版更多内置功能(如网络请求、登录、授权、上传、下载、分享)等更新中...


参考链接:



作者:xkfe
来源:juejin.cn/post/7270830083740450816
收起阅读 »

实现一个鼠标框选的功能,要怎么实现和设计 api?

web
前言 前两年在一家做电商的公司做了一个需求:鼠标框选商品卡片,开始拖拽的时候合成一个然后改变位置,页面上有几千个所以还要结合虚拟列表。当时不知道怎么做,就在 github 上到处找现成的库,最后找到了 react-selectable-fast,并结合 rea...
继续阅读 »

285330798-9d463acf-c56b-48d8-b7d5-2dc02b4257e0.gif


前言


前两年在一家做电商的公司做了一个需求:鼠标框选商品卡片,开始拖拽的时候合成一个然后改变位置,页面上有几千个所以还要结合虚拟列表。当时不知道怎么做,就在 github 上到处找现成的库,最后找到了 react-selectable-fast,并结合 react-virtualizedreact-sortable-hoc 完成了需求。虽然该库已经很久不维护了,但大致上能满足我的需求了,尽管它是以 dom 的方式,很不 react,但秉承着能用就行的原则。意料之中,开发过程中遇到了 bug,最后只能 fork 一份修改源码后自己发了个 npm 包来使用。


项目介绍


前几个月在空闲时间突然来了兴致,自己找点事做,就想自己开发一个框选的库吧,万一也有人有这个需求不知道怎么办呢?写完后发到了 antd 的社区共建群里,有的人觉得不错也 star 了。先献上项目地址 react-selectable-box,文档完整,使用 dumi 编写。api 友好,支持自定义一些功能。


api 设计


一个组件在设计时,首先思考的应该是 api 如何去设计,最好符合大家平常的习惯,并具有一定的自定义和拓展能力。再加上了解 react-selectable-fast 这个库的缺点和痛点,对我的设计就更加有帮助了。大家在看下面的文章之前也可以思考一下,如果是你,你会怎么设计?这里只选取几个 api 来进行介绍。


主组件 Selectable


选中的值


defaultValuevalue,类型为 any[],每个方块一般都有一个唯一 id 来标识,2024-1-31 更新后 后支持任意类型,因为考虑到很多情况你可能需要一个对象或数组来标识,文章后面提供了 compareFn 来自定义比较值相等。


禁用


disabled,大部分有值的组件应该都会有此属性,能直接禁用框选功能。


模式


mode,类型为 "add" | "remove" | "reverse"。模式,表明当前框选是增加、减少还是取反。这个 api 感觉是设计的最好的,用户会框选来选择目标,肯定也会需要删除已经框选的目标,可能是按住 shift 来删除等等之类的操作。用户可以自己编写自定义逻辑来修改 mode 的值来控制不同的行为,反观 react-selectable-fast,则是提供了 deselectOnEscallowAltClickallowCtrlClickallowMetaClickallowShiftClick 等多个 api。


开始框选的条件


selectStartRange,类型 "all" | "inside" | "outside",鼠标从方块内部还是外部可以开始框选,或都可以。


可以进行框选的容器


dragContainer,类型 () => HTMLElement,例如你只希望某个卡片内才可以进行框选,不希望整个页面都可以进行框选,这个 api 就会起到作用了。


滚动的容器


scrollContainer,类型 () => HTMLElement,如果你的这些方块是在某个容器中并且可滚动,就需要传入这个属性,就可以在滚动的容器中进行框选操作了。


框选矩形的 style 与 className


boxStyleboxClassName,使用者可以自定义颜色等一些样式。


自定义 value 比较函数


compareFn,类型 (a: any, b: any) => boolean,默认使用 === 进行比较(因为 value 支持任意类型,比如你使用了对象或数组类型,所以你可能需要自定义比较)


框选开始事件


onStart,框选开始时,使用者可能需要做一些事情。


框选结束事件


onEnd,类型为 (selectingValue: (string | number)[], { added: (string | number)[], removed: (string | number)[] }) => voidselectingValue 为本次框选的值,added 为本次增加的值,removed 为本次删除的值。例如你想在每次框选后覆盖之前的操作,直接设置 selectingValue 成 value 即可。如果你想每次框选都是累加,加上 added 的值即可,这里就不再说明了。


方块可选 - useSelectable


怎么让方块可以被选择呢?并且一一绑定上对应的值?react-selectable-fast 则是提供 clickableClassName api,传入可以被选择的目标的 class,这种方式太不 react 了。此时我的脑海里想到了 dnd-kit,我认为是 react 最好用的拖拽库,它是怎么让每个方块可以被拖拽的呢?优秀的东西应该借鉴,于是就有了 useSelectable


const { 
setNodeRef, // 设置可框选元素
isSelected, // 是否已经选中
isAdding, // 当前是否正在添加
isRemoving, // 当前是否正在删除
isSelecting, // 当前是否被框选
isDragging // 是否正在进行框选操作
} = useSelectable({
value, // 每个元素的唯一值,支持任意类型
disabled, // 这个元素是否禁用
rule, // "collision" | "inclusion" | Function,碰撞规则,碰到就算选中还是全部包裹住才算选中,也可以自定义
});

如何使用?


const Item = ({ value }: { value: number }) => {
const { setNodeRef, isSelected, isAdding } = useSelectable({
value,
});

return (
<div
ref={setNodeRef}
style={{
width: 50,
height: 50,
borderRadius: 4,
border: isAdding ? '1px solid #1677ff' : undefined,
background: isSelected ? '#1677ff' : '#ccc',
}}
/>

);
};

实现


这里只简单讲一下思路,有兴趣的同学可以直接前往源码进行阅读。


主组件 Selectable 相当于一个 context,一些状态在这里进行保存,并掌管每个 useSelectable,将其需要的值通过 context 传递过去。


在设置的可被框选的容器内监听鼠标 mousedown 事件,记录其坐标,根据 mousemove 画出框选矩形,再根据 setNodeRef 收集的元素和框选矩形根据碰撞检测函数计算出是否被框选了,并将值更新到 Selectable 中去,最后在 mouseup 时触发 onEnd,将值处理完之后并丢出去。


演示


这里演示一下文章开头所说的框选拖拽功能,配合 dnd-kit 实现,代码在文档的 example 中。
录屏2024-01-23 19.27.43.gif


遇到的坑


这里分享一下遇到的坑的其中之一:框选的过程中会选中文字,很影响体验,怎么让这些文字不能被框选呢?


方案1: 用 user-select: none 来控制文本不可被选中,但是这是在用户侧来做,比较麻烦。并且发现在 chrome 下设置此属性后,拖拽框选到浏览器边缘或容器边缘后不会自动滚动,其它浏览器则正常


方案2: 在 mousedown 时设置 e.preventDefault(),这样选中时文字就不会被选中,但是拖拽框选到浏览器边缘或容器边缘后不会自动滚动,只能自己实现了滚动逻辑。后面又发现在移动端的 touchstart 设置时,会发现页面上的点击事件都失效了,查资料发现没法解决,只能另辟蹊径。


方案3: 在 mousemovetouchmove 时设置 e.preventDefault() 也是可以的,但也需要自己实现滚动逻辑。


最终也是采取了方案3。


后续目标


目前只能进行矩形的碰撞检测,不支持圆形(2024.1.26 更新支持自定义已经可以实现)及一些不规则图形(2024.1.26 更新提供自定义碰撞检测(dom 下太难,canvas 比较好做碰撞检测),剩下的就是使用者的事了!)。这是一个难点,如果有想法的可以在评论区提出或者 pr 也可。


2024-1-24 更新


添加 cancel 方法,试一试。可以调用 ref.current?.cancel() 方法取消操作。这样可以自定义按下某个键来取消当前操作。有想需不需要添加一个属性传入 keyCode 数组内置取消,但是感觉会使 api 太多而臃肿,也欢迎留下你的想法。


2024-1-26 更新一


添加 items api 以优化虚拟滚动滚动时框选范围增加或减小时,已经卸载的 Item 的判断框选。(可选)试一试


优化前:滚动到下面时,加大框选面积,上面已经被卸载的不会被选中


录屏2024-01-26 16.50.31.gif


优化后:滚动到下面时,加大框选面积,上面已经被卸载的会被选中


录屏2024-01-26 16.53.36.gif


2024-1-26 更新二


支持自定义碰撞规则检测,试一试自定义圆形碰撞检测
录屏2024-01-26 17.41.37.gif


2024-1-31 更新


value 支持任意类型 any,不再只是 string | number 类型,因为很多情况需要是一个对象或数组来当唯一标识,并提供了 compareFn 来支持自定义值的比较,默认使用 ===,如果你的 value 是对象或数组,需要此属性来比较值。


总结


开发一个较为复杂的组件,可以提交自己的 api 设计能力和解决问题的能力,可以将平常所学习、所了解、所使用的东西取其精华运用起来。最后希望这个组件能帮助到有需要的人,欢迎大家提出建议!有 issues 才能维护下去!如果觉得不错,帮忙点个 star 吧,地址 react-selectable-box


作者:马格纳斯
来源:juejin.cn/post/7326979670485123110
收起阅读 »

Kotlin开发者尝试Flutter——错怪了Dart这门语言

前言 我曾经是Java安卓开发者,进入大学后了解并且转向了Kotlin开发安卓,时至今日已经有了一年时间,Kotlin带给我的体验实在是太惊艳了,我深信这就是我最喜欢的语言了。 抱着这种看法,我发现了Flutter+Dart这种抽象的组合,大量的嵌套好像让我在...
继续阅读 »

你的段落文字.png


前言


我曾经是Java安卓开发者,进入大学后了解并且转向了Kotlin开发安卓,时至今日已经有了一年时间,Kotlin带给我的体验实在是太惊艳了,我深信这就是我最喜欢的语言了。


抱着这种看法,我发现了Flutter+Dart这种抽象的组合,大量的嵌套好像让我在写ifelse,这导致了我迟迟没有接触Flutter跨平台框架,当然还有一些其他原因。


其实在之前Flutter的跨平台能力已经惊艳到我了,这次寒假正好有机会让我学习它。


当我试着用它完成业务时,我发现好像也不是那么不可接受,我甚至还有那么点快感,如果你写过安卓Compose那么你会更加觉得如此,因为在UI和业务的关系上它真的太容易绑定了,我不再考虑像XML监听数据变化,只可惜Dart语法仍然在一些地方让我感觉到不太好用,还是得Kotlin来,等等,那我不就是想要Compose吗?


哈哈,不要着急,为什么这个项目是Flutter而不是KMP随后我们再说。


其实我本身没有很严重的技术癖,面对新的事物和技术,一旦有合适的机会我都是愿意试一试,比起框架的选择,我更加享受开发过程,思想转换为代码的那一刻至少我是享受的。


这次选择Flutter开发不意味着我会一直选择和追捧它,更不会放弃安卓原生和KMP的学习,因此也希望阅读这篇文章读者意识到这点,我作为原生开发者学习Flutter是希望扩展技能而不是代替原生,Flutter本身也不是这么想的,它更像是给大家了一个更低的开发门槛,让更多其他领域的创作者完成他们作品的一种媒介。



如果你希望快速了解Kotlin开发者使用Dart的开发体验,那么直接跳过下面两部分,直接阅读#错怪的Dart。



动机


我觉得主要动机由两部分组成吧,一部分是跨平台开发本身是我感兴趣的方向之一,另一边是未来工作可能需要吧,现在来看国内跨平台趋势还是比较明显的。


不过我更希望这次项目是体验移动跨平台开发,而不是真正的深入学习移动跨平台开发。为此,我希望可以找到学习成本和项目质量相平衡的开发方式,很遗憾我没有那么多的精力做到既要还要,这是我必须面临的选择。


面对众多跨平台框架下我还是选择了Flutter,这主要与它的跨桌面端和生态完善有关,毫无疑问,Flutter有许多的成品组件,这让我可以快速轻松的上手跨平台开发


为什么是Flutter


这个项目的主要功能就是播放器,只不过这个播放器比较特殊,后续文章我们会揭晓它。


单就网络音频播放器开发任务而言,假设使用KMP可能没有现成封装好的库来给我用,可能许多开发者考虑没有就造一个,很遗憾,我不太具备这样的能力,我们需要同时对接多个平台的媒体播放,无论开发周期,单就这样的任务对我已经是很难了。


好吧,我想必须承认我很菜,但是事实如此,因此我选择了更加成熟的Flutter,避免我写不出来,哈哈哈哈。


不过我们今天先不谈Flutter,我们看看Dart。


错怪的Dart


对Dart的刻板印象是从我第一次见到Flutter的语法时形成的,第一次见到Dart时我还没有接触Kotlin。


看着有点像Java,还有好多_的名字是什么鬼东西、怎么要写这么多return、为什么有个?、总之就是反人类啊!!!


当我真正尝试去编写Flutter程序时,我发现,嗯,错怪Dart了,特别是因为我了解Kotlin后,Kotlin和Dart也有几分相似之处,这体现在一些语法特性上。


空安全


可空类型在Kotlin上可以说相当不错,在Dart上也可以体验到它,虽然它是类型前置,但是写法倒是一样的在类型后加上"?"即可。


class AudioMediaItem {
String title;
String description;
AudioMediaType type;
String? mediaUrl;
String? bvId;
//省略其他代码.....
}

当我们试图使用AudioMediaItem的对象时,我们就可以像Kotlin那样做,注意mediaUrl现在是可空的。


audioMediaItem?.mediaUrl,如果我们认为这个属性一定有值,那么就可以使用audioMediaItem!.mediaUrl,需要注意的是,dart中是"!"而不是"!!"


如果你希望使用Kotlin的Elvis操作符 ?: ,那么你可以这么做


audioMediaItem?.mediaUrl ?? "default";

对应Kotlin的


audioMediaItem?.mediaUrl ?: "default"

在这方面,dart和Kotlin是非常相似的,因此,你可以非常平滑的迁移这部分的开发体验和理解。


延迟初始化


在Kotlin中,我们可以使用lateinit var定义一个非空延迟初始化的变量,通俗的讲就是定义一个非空类型,但是不给初始值。dart也有对应从关键字,那就是late了。


late String name;

相当于Kotlin的


lateinit var String name

我们知道延迟初始化意味着这个值必定有值,只是我们希望这个值在代码运行过程中产生并且初始化,初始化后再使用该值,否则就会空指针了。


如果你已经熟悉了Kotlin的lateinit,那这里也可以平滑迁移了。


但是在Android Studio 2023.1.1我发现个有意思的事情。


late String? name;

ide没有提示这是错误的,我没试着运行,但是我觉得这应该是不合理的。


扩展函数


扩展函数在Kotlin当中可以说相当重要,许多内置函数都是这个特性所带来的。


在Kotlin中,我们通过 被扩展的类名.扩展函数名(){} 这样的写法就实现了一个扩展函数。


fun String.toColorInt(): Int = Color.parseColor(this)

Dart中也存在扩展函数的语法糖!


extension StringExtension on String {
/// 将字符串的首字母大写
String capitalize() {
if (isEmpty) {
return this;
}
return '${this[0].toUpperCase()}${substring(1)}';
}
}

其中,StringExtension只是这个扩展的名字,相当于一个标志,可以随便起,on String则代表扩展String类,那么capitalize 自然就是扩展的方法名了。


将Kotlin的内置函数带入


Kotlin的内置函数实在是太棒了,下面以also和let为例子,模仿了Kotlin的扩展函数,只可惜Dart的lambda不太能像Kotlin那样,还是有一些割裂。


extension AlsoExtension<T> on T {
T also(void Function(T) block) {
block(this);
return this;
}
}

extension LetExtension<T> on T {
R let<R>(R Function(T) block) {
return block(this);
}
}

//用法
String demo = "xada".let((it) => "${it}xadadawdwad");


emm不过因为没办法直接传this,在变量很长或者类型可空时还有点用。


顶层函数


Kotlin中,我们有时候需要在全局使用一些函数,但是不希望写在类里,而是随时随地直接可以调用或者拿到。


注意这些代码不在类里


val json = Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
}

var retrofit = Retrofit.Builder()
.baseUrl("https://api.juejin.cn/")
.addConverterFactory(json.asConverterFactory(MediaType.parse("application/json;charset=utf-8")!!))
.build()


在某个类需要我们就直接写retrofit.xxxx() 就可以了,我们不需要再单独从类中找。


Dart也有这样的功能


final _cookieJar = CookieJar();

final Dio dioClient = Dio(BaseOptions(
baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 5),
contentType: Headers.jsonContentType,
persistentConnection: true,
))
..transformer = BackgroundTransformer()
..let((it) {
if (!kIsWeb) {
it.interceptors.add(CookieManager(_cookieJar));
return it;
} else {
return it;
}
});


上面的例子只是写了变量,写函数也是一样的,都可以直接在全局任何的位置调用。


高阶函数


在Kotlin中,高阶函数是特殊的一种函数,这种函数接受了另一个函数作为参数。


我们以Kotlin的forEach函数为例子:



public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}

// 用法
fun main() {
val demoList = listOf("da", "da", "da")
demoList.forEach {
println(it)
}
}

forEach本身扩展了Iterable,但是它的参数非常特殊,我们看看action参数的类型:


(T) -> Unit,这是Kotlin匿名函数的写法,意味着这个函数有一个参数,类型为T泛型,这个参数也没有起名字,所以就只有类型T在。


这种情况,在Java中这种实现一般是接口类,我们需要实例化这个匿名类,假设这个接口只有一个方法,那么就可以转换为lambda的写法。


在Kotlin里我们可以直接写为lambda的形式,要方便很多,由于只有一个参数,那么kotlin默认就叫it了。


OK回顾完Kotlin,我们看看Dart:


void forEach(void action(E element)) {
for (E element in this) action(element);
}

//用法
List<String> demoList = ["da","da","da"];

demoList.forEach((element) {
print(element);
});

其实差别不大,只是我们需要写void当作这个参数的类型,内部写法没有太大差异。


不过,Dart的lambda更加贴近JS,写法基本上是一模一样。


相信如果你已经掌握了Kotlin的高阶函数,那么在Dart尝试也是不错的。


运算符重载


Kotlin当中有个不太常用的东西,叫运算符重载,它在Dart中也有。


public operator fun <T> Collection<T>.plus(elements: Iterable<T>): List<T> {
if (elements is Collection) {
val result = ArrayList<T>(this.size + elements.size)
result.addAll(this)
result.addAll(elements)
return result
} else {
val result = ArrayList<T>(this)
result.addAll(elements)
return result
}
}

//用法
val demoList = listOf("da", "da", "da") + listOf<String>("add")

可以看到kotlin通过operator关键字配合扩展函数实现了这个功能,dart也可以模仿这种手段:


// 模仿
extension ListPlusOperatorExtension<T> on List<T> {
List<T> operator +(List<T> elements) {
List<T> result = this;
addAll(elements);
return result;
}
}

// 用法

List<String> demo1 = ["da","da"];

List<String> demo2 = ["da","d1a"] + demo1;

不过这里的加减乘除就是operator + 了。


总结


可以看得出,Dart也有部分我们在Kotlin中喜欢的特性,如果你已经掌握了Kotlin的基本语法,那么相信Dart对你来说也不是太大问题,你可以平滑的迁移一些在Kotlin中的知识到Dart上去。


起初我是很坑距使用Flutter的,现在看见Dart的特性,我似乎又接受了一些,好吧,对于Flutter开发、布局约束和其他感受我在下一篇文章再分享给大家吧。


最后感谢大家看到这里,还有什么好玩的特性欢迎在下面留言,文章内容有错误请指出。


作者:萌新杰少
来源:juejin.cn/post/7329874214378078245
收起阅读 »

uniapp小程序包过大的问题

uniapp小程序包过大的问题 前言 微信小程序为了优化用户体验,将小程序首次加载的数据限制在了2M以内(推荐1.5M),剩下的数据采取分包(懒加载)的方式进行引用。 一 开启分包subPackages 在manifest.json文件中添加"optimiza...
继续阅读 »

uniapp小程序包过大的问题


前言


微信小程序为了优化用户体验,将小程序首次加载的数据限制在了2M以内(推荐1.5M),剩下的数据采取分包(懒加载)的方式进行引用。


一 开启分包subPackages


manifest.json文件中添加"optimization" : {"subPackages" : true}来开启分包。


1680316261345.png

然后可以在pages.json中添加subPackages来进行分包页面的配置。


当然,uniapp还贴心的为我们提供了便捷的创建方式:


1680316550191.png

二 静态资源优化


小程序中尽量少使用大背景图片,这样会占据大量包资源。微信小程序推荐使用网络图片资源来减少主包资源。因为某种原因,我把图片放进了主包里,但是要进行图片压缩。这里推荐一个图片压缩网站tintpng


image.png

可以看到图片被压缩了百分之62,并且可以批量处理,就很方便。


三 去除冗余代码


这里你以为我会说提升代码质量巴拉巴拉,其实不然。接下来要说的,才是我要写这篇文章的真正原因!!!


如果你使用uniapp开发微信小程序,并直接在微信开发小程序工具中上传,你会发现你的包会离奇的大


image.png

在代码依赖分析中我们可以发现,一个叫common的文件竟有1.3M多,而这个并非是我自己的文件。


image.png

后来发现这应该是uniapp开发时的编译文件,删掉就可以了。


还有一个方法,在uniapp运行到小程序中,时勾选运行时是否开启代码压缩,此时再看代码其实也可以符合要求了:


image.png

四 通过uniapp上传发布


uniapp也提供了通过cli来进行发布小程序的能力:


image.png

这里需要准备的是appId和微信小程序上传接口的key,并且要配置你上传时所在的网络IP,具体方法


结语


OK,当你看到这里,那么恭喜你,又多活了三分钟~respect!!!


作者:FineYoung
来源:juejin.cn/post/7216845797143969850
收起阅读 »

近年来项目研发之怪现状

简述 近年来,机缘巧合之下接触了不少toG类项目。项目上颇多事情,令人疑惑频频。然而屡次沟通,却都不了了之,长此以往,心力愈发交瘁,终究心灰意冷,再无劝谏之心。 令人困惑的项目经理 孟子说天时不如地利,地利不如人和。而项目上遇到的很多事情,天时、地利终为...
继续阅读 »

简述



近年来,机缘巧合之下接触了不少toG类项目。项目上颇多事情,令人疑惑频频。然而屡次沟通,却都不了了之,长此以往,心力愈发交瘁,终究心灰意冷,再无劝谏之心。



令人困惑的项目经理



孟子说天时不如地利,地利不如人和。而项目上遇到的很多事情,天时、地利终为少数,多数在人和。



立项开工,项目经理自然是项目上的第一把手。既为第一把手,自要有调兵遣将,排兵布阵的能耐。


当然用我们业内的话来说,可分为下面几类:


第一等的自然是懂业务又懂技术,这样的项目经理可运筹帷幄之中,决胜千里之外,当然这般的项目经理可遇而不可求。


这第二等的懂业务不懂技术,或者懂技术不懂业务,这样的项目经理,辅以数名参将,只要不瞎指挥,也可稳扎稳打,有功无过。


第三等的项目经理,业务与技术皆是不懂,如这般的项目经理,若尽职尽责,配先锋、军师、参将、辎重,最好再辅之以亲信,也可功成身退。若其是领导亲信,那更可说是有惊无险了。


而这第四等的,业务与技术不懂也就罢了,既无调兵遣将之才,又无同甘共苦之心,更是贻误战机,上下推诿。若其独断专横,那便是孔明在世也捧不起来。



有这般一个项目,公司未设需求经理,常以项目经理沟通需求。工期八月,立项后,多次催促,却不与甲方沟通,以至硬生生拖了两月之后才去。然而不通业务,不明技术。甲方被生耗两个月才沟通需求,这样的情况下,如何能顺利进行,以至于项目返工现象,比比皆是。多次提及需求管理,亦是左耳进右耳出。类类数落问题,甲方、研发、产品都有问题,独独他自身若皎皎之明月,灿灿之莲花。然而纵是项目成员承星履草,夜以继日,交付一版之后。举目皆是项目经理之间的恭维之词。



我有很多朋友是优秀的项目经理。言必行,行必果。沟通起来非常愉悦。偶尔遇到一个这样的人,确实让我大开眼界。


其实我也想过,这并非是项目经理职位的问题,实在是个别人自身的问题,这样的人,在任何岗位都是令人恼火的。


技术人员的无力感


我们互联网从业者经常听到一个词,技术债。技术债是令人难受的,尤其是在做项目的时候。做产品,我们可以制定完善的迭代周期,而项目,当需求都无法把控的时候,那么就意味着一切都是可变的。


糟糕的事情是,我遇到了这样的项目。前期无法明确的需求,项目期间,子虚乌有的需求管理,项目中不断的需求变更,deadline的不断临近,最终造就了代码的无法维护。


我从未想过,在同一个紧迫的时间阶段,让研发进行需求研发、bug修复、代码解耦,仿佛每一件事情都很重要。当然,我更建议提桶跑路,这样的项目管理,完全是忽视客观现实的主观意识。


前端规范难落地


公司是有前端规范的,然而前端规范的落地却很糟糕。如果使用TS,那么对于诸多时间紧,任务重,且只有一名前端开发人员的项目来说,显得太过冗余了。所以依旧使用js,那么代码中单个性化不会少见。使用esLint怎么样呢?这当然很棒,直到你发现大部分成员直接将esLint的检查注释了。或许还可以依靠团队内不断的宣讲与code Review,这是个好主意,然而你会发现,公司的code Review也是那么形式化的过程。


或许对一些企业来说,代码的规范性不重要,所谓的技术类的东西都显得没那么重要。只有政府将钱塞到它的口袋里这件事,很重要。


崩盘的时间管理


那么,因为各方面的原因,项目不可避免的走向了失控。时间管理的崩溃,项目自然开始了不断的延期。在私下里,一些擅长酒桌文化的甲方与项目经理,开始了酒桌上的攀谈,推杯换盏之间,开始了走形式的探讨。灯红酒绿之间,公司又开始了例行的恭维。


当然,我依旧无法理解,即使管理的如此糟糕,只要在酒桌上称兄道弟,那便什么问题都没有了?若是如此,项目经理面试的第一道题,一定是酒量如何了。


作者:卷不动咯
来源:juejin.cn/post/7263372536791433275
收起阅读 »

502故障,你是怎么解决的?

在现代网络应用的开发和维护中,502 Bad Gateway错误是一个常见而令人头疼的问题。这种错误通常意味着代理服务器或网关在尝试访问上游服务器时未能获取有效的响应。本文将深入探讨502故障的原因、可能的解决方案,并提供基于实际案例的客观凭证。 1. 原因深...
继续阅读 »

在现代网络应用的开发和维护中,502 Bad Gateway错误是一个常见而令人头疼的问题。这种错误通常意味着代理服务器或网关在尝试访问上游服务器时未能获取有效的响应。本文将深入探讨502故障的原因、可能的解决方案,并提供基于实际案例的客观凭证。


1. 原因深入解析


a. 上游服务器问题


502错误的最常见原因之一是上游服务器出现问题。这可能包括服务器崩溃、过载、应用程序错误或者数据库连接故障。具体而言,通过观察服务器的系统日志、应用程序日志以及数据库连接状态,可以深入分析问题的根本原因。


b. 网络问题


网络中断、代理服务器配置错误或者防火墙问题都可能导致502错误。使用网络诊断工具,如traceroute或ping,可以检查服务器之间的连接是否畅通。同时,审查代理服务器和防火墙的配置,确保网络通信正常。


c. 超时问题


502错误还可能是由于上游服务器响应时间超过了网关或代理服务器的超时设置而引起的。深入了解请求的性能特征和服务器响应时间,调整超时设置可以是一项有效的解决方案。


2. 解决方案的客观凭证


a. 上游服务器状态监控


使用监控工具,例如Prometheus、New Relic或Datadog,对上游服务器进行状态监控。通过设置警报规则,可以及时发现服务器性能下降或者异常情况。


b. 网络连接分析


借助Wireshark等网络分析工具,捕获和分析服务器之间的网络通信数据包。这有助于定位网络中断、数据包丢失或防火墙阻塞等问题。


c. 超时设置调整


通过监控工具收集请求的响应时间数据,识别潜在的性能瓶颈。根据实际情况,逐步调整代理服务器的超时设置,以确保其适应上游服务器的响应时间。


3. 实例代码分析


循环引用问题


gc_enabled 是否开启gc
gc_active 垃圾回收算法是否运行
gc_full 垃圾缓冲区是否满了,在debug模式下有用
buf 垃圾缓冲区,php7默认大小为10000个节点位置,第0个位置保留,既不会使用
roots: 指向缓冲区中最新加入的可能是垃圾的元素
unused 指向缓冲区中没有使用的位置,在没有启动垃圾回收算法前,指向空
first_unused 指向缓冲区第一个为未使用的位置。新的元素插入缓冲区后,指向会向后移动一位
last_unused 指向缓冲区最后一个位置
to_free 带释放的列表
next_to_free 下一个待释放的列表
gc_runs 记录gc算法运行的次数,当缓冲区满了,才会运行gc算法
collected 记录gc算法回收的垃圾数

Nginx配置


location / {
proxy_pass http://backend_server;

proxy_connect_timeout 5s;
proxy_read_timeout 30s;
proxy_send_timeout 12s;

# 其他代理配置项...
}

上述Nginx配置中,通过设置proxy_connect_timeoutproxy_read_timeoutproxy_send_timeout,可以调整代理服务器的超时设置,从而适应上游服务器的响应时间。


PHP代码


try {
// 执行与上游服务器交互的操作
// ...

// 如果一切正常,输出响应
echo "Success!";
} catch (Exception $e) {
// 捕获异常并处理
header("HTTP/1.1 502 Bad Gateway");
echo "502 Bad Gateway: " . $e->getMessage();
}

在PHP代码中,通过捕获异常并返回502错误响应,实现了对异常情况的处理,提高了系统的健壮性。


4. 结语


502 Bad Gateway错误是一个综合性的问题,需要从多个角度进行深入分析。通过监控、网络分析和超时设置调整等手段,可以提高对502故障的解决效率。在实际应用中,结合客观的凭证和系统实时监控,开发者和运维人员能够更加迅速、准确地定位问题,确保网络应用的稳定性和可用性。通过以上深度透析和实际案例的代码分析,我们希望读者能够更好地理解502错误,并在面对此类问题时能够快速而有效地解决。


作者:Student_Li
来源:juejin.cn/post/7328766815101108243
收起阅读 »

01CSS 实现多行文本“展开收起”

web
最近在开发移动端的评论内容功能时,我遇到了一个需求,需要实现一个展开收起效果。主要目的是让用户能够方便地查看和隐藏评论内容,现在想将我的成果分享给大家 完成效果: 实现思路: 1.准备一个文本的外部容器( content),并将最大高度设置为65px(根据...
继续阅读 »

最近在开发移动端的评论内容功能时,我遇到了一个需求,需要实现一个展开收起效果。主要目的是让用户能够方便地查看和隐藏评论内容,现在想将我的成果分享给大家



完成效果:


展开收起.gif


实现思路:


1.准备一个文本的外部容器( content),并将最大高度设置为65px(根据实际需求而定),超出内容设置不可见


image.png


2.文本容器的高度(text-content)不做样式设置,这个容器是为了获取内容实际高度


image.png


3.通过 js 获取文本容器的高度(text-content),判断文本高度是否超过外部容器(content)的最大高度,控制展开收起按钮是否显示


4.点击按钮时根据条件设置容器(content)的最大高度,css 对通过 transition 对 max-height 设置过渡效果


完整示例代码如下


HTML



<div class="container">
<div class="content">
<div class="text-content">
1月30日上午10时,中国贸促会将召开1月例行新闻发布会,介绍第二届链博会筹备进展情况;
2025大阪世博会中国馆筹备进展;2023年全国贸促系统商事认证数据;2023年贸法通运行情况;
2023年11月全球经贸摩擦指数;2023年12月全球知识产权保护指数月度观察报告;助力培育外贸新动能有关工作考虑等。
</div>
</div>
<button class="btn">展开</button>
</div>


CSS



.container {
width: 260px;
padding: 20px;
border: 1px solid #ccc;
margin: 50px auto;
}

.content {
max-height: 65px;
overflow: hidden;
transition: max-height 0.5s;
}


.btn {
display: flex;
width: 40px;
color: cornflowerblue;
outline: none;
border: none;
background-color: transparent;
}



JS


    const maxHeight=65
const btn = document.querySelector('.btn')
const content = document.querySelector('.content')
const textContent=document.querySelector('.text-content')
const textHeight=textContent.getBoundingClientRect().height // 文本高度
const contentHeight=content.getBoundingClientRect().height // 容器高度
let flag = false
if (textHeight < maxHeight) {
btn.style.display = 'none'
}
btn.addEventListener('click', () => {
if (!flag) {
content.style.maxHeight=textHeight+'px'
btn.innerHTML = '收起'
flag = true
} else {
content.style.maxHeight=contentHeight+'px'
btn.innerHTML = '展开'
flag = false
}
})



实现一个功能的方式往往有多种,你们是怎么解决的呢?


作者:前端小山
来源:juejin.cn/post/7329694104118919195
收起阅读 »

苹果 visionOS for web

苹果的 Vision Pro 已经发布了,虽然没有拿到手,但我还是对它的操作界面充满了好奇。 我看到很多小伙伴写了 Windows for Web,Mac OS for Web,所以我也想来实现一下 Vision Pro 的系统主页。 一开始,我以为这不会太难...
继续阅读 »

苹果的 Vision Pro 已经发布了,虽然没有拿到手,但我还是对它的操作界面充满了好奇。


我看到很多小伙伴写了 Windows for Web,Mac OS for Web,所以我也想来实现一下 Vision Pro 的系统主页。


一开始,我以为这不会太难,当头一棒的就是苹果祖传优势: 动画。


CPT2401291503-845x461.gif


这动画,这模糊,还是从中心点开始逐渐向外层扩散,应该根据人眼的视觉特征进行设计的。


问题是,该如何实现呢?


模糊我知道怎么实现,


filter: blur(15px);

从中心点开始逐渐向外层扩散的效果,我直接来个
transition-delay: 0.1s;


一通操作之下,也实现就似是而非的效果。而且边缘处app图标的缓缓落下的效果也不好。


CPT2401291508-1281x733.gif


然后就是光影效果的实现,因为它的很美,让人很难忽略。


在 Vision Pro 系统演示中可以看出,为了模拟菜单栏使用了磨砂玻璃材质,而为了营造真实感,会模拟光照射到玻璃上而形成的光线边框。


我不知道这是不是菲涅尔效应,但问题是,这又该如何在前端实现呢?


我想到了 CSS Houdini,可以利用 Houdini 开放的底层能力 paint 函数来实现一个菜单栏效果。


if ('paintWorklet' in CSS) {
CSS.paintWorklet.addModule('data:text/javascript,' + encodeURIComponent(`

class FresnelAppRectPainter {
static get inputProperties() { return ['--light-angle']; }

paint(ctx, size, properties) {
const borderRadius = 30;
const fresnelColor = 'rgba(255, 255, 255, .9)';
const lightAngle = parseFloat(properties.get('--light-angle')[0]) || 0;

// 绘制圆角矩形
ctx.beginPath();
ctx.moveTo(borderRadius, 0);
ctx.lineTo(size.width - borderRadius, 0);
ctx.arcTo(size.width, 0, size.width, borderRadius, borderRadius);
ctx.lineTo(size.width, size.height - borderRadius);
ctx.arcTo(size.width, size.height, size.width - borderRadius, size.height, borderRadius);
ctx.lineTo(borderRadius, size.height);
ctx.arcTo(0, size.height, 0, size.height - borderRadius, borderRadius);
ctx.lineTo(0, borderRadius);
ctx.arcTo(0, 0, borderRadius, 0, borderRadius);
ctx.closePath();
ctx.fillStyle = 'rgba(163, 163, 163)';
ctx.fill();

// 模拟光照效果
const gradient = create360Gradient(ctx, size, lightAngle)
ctx.fillStyle = gradient;
ctx.fill();

// 添加菲涅尔效果
const borderGradient = ctx.createLinearGradient(0, 0, size.width, size.height);
borderGradient.addColorStop(0, fresnelColor);
borderGradient.addColorStop(0.2, 'rgba(255,255,255, 0.7)');
borderGradient.addColorStop(1, fresnelColor);

ctx.strokeStyle = borderGradient;
ctx.lineWidth = 1.5;
ctx.stroke();
}
}

registerPaint('fresnelAppRect', FresnelAppRectPainter);
`));
}

结果效果还可以,我甚至可以接收一个光的入射角度,来实时绘制光影效果。


 function create360Gradient(ctx, size, angle) {
// 将角度转换为弧度
const radians = angle * Math.PI / 180;

// 计算渐变的起点和终点
const x1 = size.width / 2 + size.width / 2 * Math.cos(radians);
const y1 = size.height / 2 + size.height / 2 * Math.sin(radians);
const x2 = size.width / 2 - size.width / 2 * Math.cos(radians);
const y2 = size.height / 2 - size.height / 2 * Math.sin(radians);

// 创建线性渐变
const gradient = ctx.createLinearGradient(x1, y1, x2, y2);
gradient.addColorStop(0, 'rgba(255, 255, 255, 0.2)');
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');

return gradient;
}

CPT2401291454-249x209.gif


演示效果图


哦对了,还有一个弹层底边角的缩放效果,我目前还没想到什么好办法来实现,年底还得抓紧搬砖,只能先搁置了,如果小伙伴们有好办法,欢迎告知或者讨论。


1706511484530.png


最终效果图


这里是 Demo 地址


本来是冲着纯粹娱乐(蹭流量)来写的,但写着写着就发现好像没那么简单,三个晚上过去,也只写了个首页,不得不感慨苹果真的太细了呀。


以上。


作者:于益
来源:juejin.cn/post/7329280514627600425
收起阅读 »

uniapp系列-改变底部安全区-顶部的手机信号、时间、电池栏颜色样式

uniapp 的默认安全区域的颜色是白色,如果我们做了沉浸式页面,背景色也是白色的话,就会看不到电池栏,等的颜色,如何修改呢? 首先来说底部安全区域 下图是底部安全区原始状态,感觉和整个页面格格不入 修改代码配置safearea manifest.json...
继续阅读 »

uniapp 的默认安全区域的颜色是白色,如果我们做了沉浸式页面,背景色也是白色的话,就会看不到电池栏,等的颜色,如何修改呢?


首先来说底部安全区域


下图是底部安全区原始状态,感觉和整个页面格格不入



修改代码配置safearea



  • manifest.json(下面代码仅支持ios)


// 在app-plus下配置:
"safearea": { //安全区域配置,仅iOS平台生效
"background": "#F5F6F9", //安全区域外的背景颜色,默认值为"#FFFFFF"
"bottom": { // 底部安全区域配置
"offset": "none|auto" // 底部安全区域偏移,"none"表示不空出安全区域,"auto"自动计算空出安全区域,默认值为"none"
}
},


  • 页面里写(下面代码支持android)


写法一:
// #ifdef APP-PLUS
var Color = plus.android.importClass("android.graphics.Color");
plus.android.importClass("android.view.Window");
var mainActivity = plus.android.runtimeMainActivity();
var window_android = mainActivity.getWindow();
window_android.setNavigationBarColor(Color.parseColor("#eb8c76"));
// #endif
写法二:
// #ifdef APP-PLUS
let color, ac, c2int, win;
color = plus.android.newObject("android.graphics.Color")
ac = plus.android.runtimeMainActivity();
c2int = plus.android.invoke(color, "parseColor", "#000000")
win = plus.android.invoke(ac, "getWindow");
plus.android.invoke(win, "setNavigationBarColor", c2int)
// #endif



底部区域颜色已配置成功(下图仅供参考,随便选的颜色,有点丑哈哈)



接下来讲一下顶部电池栏的配置


配置顶部导航栏颜色


方案一:仅适用于原生导航配置,非自定义导航



在page.json修改需要配置的页面的navigationBarTextStyle属性



"pages": [ 
{
"path": "pages/index/index",
"style": {
// "navigationStyle": "custom"
"navigationBarTitleText": "我是原生title",
"navigationBarTextStyle": "white" ,// 仅支持 black/white
"navigationBarBackgroundColor": "#aaaaff"
}
}
],


方案二:通用,也适用于自定义导航



在页面中使用nativejs的api,native是uni内置的sdk,不需要手动引入,直接用就可以,但是需要注意调用时机和条件使用,参考下面的注意事项哦



onReady(){
plus.navigator.setStatusBarStyle("dark"); //只支持dark和light
}



注意事项



注意函数的调用时机,如果是自定义导航栏,方法只写在onReady的话,切换路由再回来以后,你的配置会失效,所以要注意调用时机



uniapp中 onReady, onLoad, onShow区别



  • onReady 页面初次渲染完成了,但是渲染完成了,你才发送请求获取数据,显得有些慢

  • onLoad 只加载一次,监听页面加载,其参数为上个页面传递的数据,参数类型为Object

  • onShow 监听页面显示。页面每次出现都触发,包括从下级页面点返回露出当前页面


目前我是这样配置(举个栗子:配置顶部导航栏背景颜色为黑色)


import { onLoad, onShow, onReady} from '@dcloudio/uni-app';
onReady(() =>
/* #ifdef APP-PLUS */ 
plus.navigator.setStatusBarStyle('dark'); 
/* #endif */
});

onShow(() =>
/* #ifdef APP-PLUS */ 
plus.navigator.setStatusBarStyle('dark'); 
/* #endif */
});

今天就写到这里啦~



  • 小伙伴们,( ̄ω ̄( ̄ω ̄〃 ( ̄ω ̄〃)ゝ我们明天再见啦~~

  • 大家要天天开心哦



欢迎大家指出文章需要改正之处~

学无止境,合作共赢



在这里插入图片描述


欢迎路过的小哥哥小姐姐们提出更好的意见哇~~


作者:tangdou369098655
来源:juejin.cn/post/7206628135005143099
收起阅读 »

字节开源安卓开发利器-CodeLocator

CodeLocator登场 CodeLocator 是字节跳动开源的一个包含 Android SDK 与 Android Studio 插件的 Android 工具集。个人使用之后感觉是安卓开发人员的利器,推荐给大家。(mac、windows都可以用) Cod...
继续阅读 »

CodeLocator登场


CodeLocator 是字节跳动开源的一个包含 Android SDK 与 Android Studio 插件的 Android 工具集。个人使用之后感觉是安卓开发人员的利器,推荐给大家。(mac、windows都可以用)


CodeLocator的丰富功能可以让安卓应用人员受益,下面这个GIF展示了一些CodeLocator的功能。


CodeLocator转存失败,建议直接上传图片文件

快速上手



  1. 在Android Studio中安装CodeLocator插件(点此下载最新版插件)

  2. 工程中集成CodeLocator


// 集成基础能力, 只需要添加一行依赖即可
dependencies {
// 依赖androidx, 已升级AndroidX的项目集成下面的依赖
implementation "com.bytedance.tools.codelocator:codelocator-core:2.0.3"
// 未升级AndroidX的项目集成下面的依赖 support版本不再维护 请升级androidx
implementation "com.bytedance.tools.codelocator:codelocator-core-support:2.0.0"
}


  • 目前官网描述的代码跳转的能力,需要集成Lancet,但是Lancet的引入有关于Gradle 版本AGP 版本的要求



集成Lancet 插件和依赖的项目,关于Gradle 版本AGP 版本不能适配超过7.2,不建议高版本去适配,已经帮大家踩了很多坑了😢




还有一坑就是,CodeLocatorcompose支持不是友好🐶



当工程的依赖和Android Studio的插件都到位之后,便可以启动开发app,然后使用抓取功能和调试开发。


使用功能和场景


这里我讲述下自己在使用CodeLocator的一些场景。


UI相关功能


UI界面功能



当抓取了app当前的界面之后,直接可以在界面上点击,然后查看一些组件尺寸和间距的情况。这里在界面上有几种点击模式:



  • 直接单击: 会按照可点击属性查找View, 上层可点击View会覆盖底部View。

  • control(Alt) + 单击: 会去查看view的深度,z轴的情况。

  • Shift + 单击: 多选View, 同时可对比最后选中的两个View的间距,大家在安卓XML开发的时候,在真机测试下,这里的间距和尺寸观察就十分有用了。


实时修改ui


在界面上,点击view组件之后,可以直接右键选择修改属性,当然这里选中view之后右键还有很多好用的功能。



直接修改view组件的属性: 字符内容、字体大小、颜色、可见性、内外边距等等




CodeLocator还有复制窗口功能,复制窗口之后还有diff模式,比对ui的差别。



追溯抓取历史


CodeLocator抓取历史最多可以有三十条,其中每一条数据都带有时间和缩略图浏览。你可以在显示历史抓取功能里选择之前抓取的界面,然后对比属性。这里还可以直接保存抓取数据,文件会以projectName_XXXX_XXXX.codeLocator保存,之后想要使用便可以加载。


跳转界面对应的activity和fragment


CodeLocator可以在界面上,根据你抓取的界面和view组件,来判断它是在哪个activity、fragment和对应的XML组件名,并且直接选择跳转。


一些项目上,想快速知道这个页面到底归属哪个activity、fragment或者XML组件的时候,这个功能的优越性就体现出来了。



快速启动charles


一键启动charles,并且在Android Studio随开随关,不需要你去手机上专门开启和关闭代理



  • 开启




  • 关闭



上图工具箱中的集成功能也很丰富,也是在Android Studio随开随关。


工具箱


值得一提的是工具箱中的集成功能也很丰富,也是随开随关。




集成lanct有的功能


如果CodeLocator集成了lancet相关依赖和插件之后,可以有更强大的代码跳转能力:



  • 跳转findViewById

  • 跳转clickListener

  • 跳转touchListener

  • 跳转XML

  • 跳转viewHolder

  • 跳转startActivity

  • 跳转相应的dialog、toast


作者:weiran1999
来源:juejin.cn/post/7280787122012405794
收起阅读 »

又要用Compose来做Loading了,不过这次是带小火苗的

本篇文章已同步更新至个人公众号:Coffeeee 今年第一篇Compose动效开发,继续回归老本行,来一起做个Loading,老实说Loading动效个人已经做麻了,去年做了十几个,这次主要是想实现一个带火苗的Loading,因为之前看到过有位博主用Thre...
继续阅读 »

本篇文章已同步更新至个人公众号:Coffeeee



今年第一篇Compose动效开发,继续回归老本行,来一起做个Loading,老实说Loading动效个人已经做麻了,去年做了十几个,这次主要是想实现一个带火苗的Loading,因为之前看到过有位博主用Threejs实现过一个火焰的效果,然后又是职业病啊,想试试看用Compose实现一个火焰效果到底难不难


源码地址


扩散效果


第一步,先别去想啥Loading,先想想火是啥样子的,颜色以红黄为主,也有蓝色的,绿色的,然后从火源开始逐渐向外燃烧扩散,那么这里首先就要想办法把扩散的效果做出来,先上基础代码


image.png

先创建出代表画布的宽高widthheightCanvas创建出来之后会得到宽高的具体值,然后宽高的一半就是画布的中心坐标centerxcentery,radius是整个Loading的半径,接着我们先随意在中心位置画一个实心圆


image.png
image.png

现在如果想要让这个实心圆动起来的话,通常会使用animateFloatAsState这个api,比如这里想要改变它的横坐标,可以这么写


image.png
0109aa1.gif

也可以使用循环动画让圆点在那一直动


image.png
0109aa2.gif

但是以上两种方式如果是作用在有限数量的视图上,是没啥问题的,但是像我们要做的这个扩散效果,有大量元素的,并且每个元素动画的轨迹方向都不一样,那么就不能使用上面这种动画api了,性能问题先不说,写起来也是个麻烦,所以得想个其他办法,那么既然不能用动画api来改变元素的位置,我们就手动改嘛,先来定义个model,代表每个元素,这个model有以下几个属性


image.png

其中



  • startX:代表元素初始位置的x坐标

  • startY:代表元素初始位置的y坐标

  • endx:代表元素移动结束后的x坐标

  • endy:代表元素移动结束后的y坐标

  • angle:代表元素移动的方向,也就是角度

  • dis:代表元素每次移动的距离

  • size:代表元素的大小,如果是圆就当作半径,如果是方块就当作宽高

  • color:代表元素的颜色


然后给Particle里面添加一些更新位置的代码,第一处在初始化函数中,目的是当Particle刚创建出来时候,根据startXstartY来计算出第一次位移的终点endxendy


image.png

pointXpointY分别是通过起点,角度,半径来计算终点坐标的函数,代码如下


image.png

除了刚才在初始化函数中加的代码之外,还要增加一个update函数,每次调用这个函数的时候,都会重新把上一次的终点作为起点,重新计算新的终点坐标,这样才能做到让元素移动的效果


image.png

这样我们Particle的基础功能就开发完成了,接下来就要去创建我们需要扩散的元素,由于数量较多,我们得循环创建这些元素才行,首先创建的事情我们放在副作用函数LaunchedEffect中进行


image.png

其中ANGLES表示0到360的一个范围,调用random()函数来随机取出一个值当作元素移动的方向角度,上述代码中还缺点东西,首先需要有一个数组来保存创建好之后的Particle,我们这里新建一个数组,将创建好之后的Particle添加到数组中


image.png

其次这个LaunchedEffect函数体由于keytrue,所以无论重组几次都只会执行一次,那么我们的元素只会创建一次,而我们想要的效果是每过10毫秒都创建个元素,所以得把key值改成一个会改变的值,只有key改变了才会触发LaunchedEffect再执行一遍内部的代码,那么这个key我们就改成particleList这个数组的大小,每创建一个新元素,particleList的大小都会改变,改变之后下一次又会重新再去创建新元素,代码修改为


image.png

现在每过10毫秒,我们就会多一个Particle元素,但是现在只是创建了元素,元素还没动起来,要让它们动起来的话这个时候就要用到之前我们创建的update函数了,我们在重组的过程中遍历particleList中的元素,每个元素都执行一遍update,这样元素就动起来了


image.png

整个扩散效果到这里就算完工了,来看看效果咋样


0109aa3.gif

定制扩散的样式


扩散的效果做出来了,但是可以看到现在是无限往四周扩散的,咱要做的火苗可不能无限扩散,那不得发大火了吗,所以得让我们这些元素扩散到一定范围之后“看不见”,在Canvas中让一个元素看不见除了不去绘制之外,就是让它的透明度为0,那么在Particle中再新增一个属性alpha来表示元素的透明度


image.png

默认值为1,然后在update函数中,每次都减去一点透明值,直到透明值变为0,那么该元素就看不见了


image.png

CanvasdrawCircle函数中也添加alpha属性


image.png
0109aa4.gif

现在这个扩散的范围看起来又太小了,不过没事,可以通过设置dis属性来增加整个扩散的区域


image.png

还可以给每个元素设置不同的大小和颜色来改变整个效果的外观,先创建个半径的范围


image.png

再创建个颜色的集合


image.png

然后在创建Particle的时候,随机从半径范围与颜色集合中取出一个值作为Particlesizecolor


image.png

再来看下现在的效果


0109aa5.gif

制作loading效果


到这里为止我们的扩散的起始为止都是一个固定的点,现在要让这个固定的点变成可以变化的,绕着圆周转圈,那么首先就要获得圆周上的角度,这里使用循环动画创建一个0到360度循环改变的值当成角度


image.png

获得角度之后,使用pointXpointY函数来计算出这个角度在圆周上的x坐标tapx与y坐标tapy,将创建元素用到的centerxcentery替换成tapx,tapy


image.png

现在扩散效果就绕着画布中心转圈了


0109aa6.gif

看起来有点别扭啊,首先这个转圈一顿一顿的,然后尾巴貌似分叉的太开了,不过没事,这些都可以优化,分叉的太开主要是我们扩散的角度是0到360度,将这个范围变小一点就好了


image.png

动画一顿一顿的是因为我们的动画设置的是两秒,它只有到了两秒以后才会进行下一次动画,但是变化的角度不到两秒的时候就已经到达360度了,所以才会在360度的位置停滞了一段时间,解决办法就是将动画规格从补间动画改成关键帧动画,将到达360度的那一帧设置在2000毫秒的位置上


image.png
0109aa7.gif

转圈不顿了,但是现在离火苗的效果还是有点出入的,我们这个loading的头部位置相当于火苗的燃烧源头,而燃烧源相对来讲都是比较大的,然后逐渐朝着燃烧的方向变小,所以还得继续优化下,现在元素的半径还太小,得变大


image.png

其次在update函数中,也对半径size做递减处理,直到半径变为0


image.png

再来看下效果


0109aa8.gif

还差最后一步,将整个画布设置下模糊效果,设置一下blur函数,内部参数越大,模糊的效果越严重,调了一下后7.dp比较合适


image.png

加了模糊效果后的效果如下


0109aa9.gif

一团小火苗就做出来了,感觉效果比较空,我们可以再加一个火苗,现在圆周上只有一个定点在转,我们再加一个,颜色设置成偏蓝,刚好一个火焰一个冰焰


image.png
image.png

最终效果如下


0109aa10.gif

总结


到这里一个火焰Loading的动效就完成了,还是很容易的其实,里面最主要的就是通过那几个参数来控制好元素扩散的效果,甚至我们可以尝试着去更改一些参数或者实现方式,来做一些其他不一样的动效,这些大家如果有兴趣的可以自己去试试看。


作者:Coffeeee
来源:juejin.cn/post/7329433979806810146
收起阅读 »

浏览器关闭实现登出(后端清token)

web
实现浏览器关闭后,后端记录用户登出日志的最佳方式是通过前端发送请求来通知后端进行记录。以下是一种常见的实现方式,重点就是如何区分用户行为是页面刷新还是关闭浏览器。 // 写在APP.vue mounted() { window.addEventLi...
继续阅读 »

实现浏览器关闭后,后端记录用户登出日志的最佳方式是通过前端发送请求来通知后端进行记录。以下是一种常见的实现方式,重点就是如何区分用户行为是页面刷新还是关闭浏览器。


// 写在APP.vue
mounted() {
window.addEventListener("beforeunload", () => this.beforeunloadHandler())
window.addEventListener("unload", () => this.unloadHandler())
},

destroyed() {
window.removeEventListener("beforeunload", () => this.beforeunloadHandler())
window.removeEventListener("unload", () => this.unloadHandler())
clearInterval(this.timer)
},

methods:{
beforeunloadHandler() {
this.beforeUnloadTime = new Date().getTime()
},
unloadHandler() {
this.gapTime = new Date().getTime() - this.beforeUnloadTime
if (this.gapTime <= 5) { //判断是窗口关闭还是刷新,小于5代表关闭,否则就是刷新。
// 这里是关闭浏览器
logout()
}
},
}


但是经测试,发现上面这种浏览器关闭事件并不是一种可靠的方式来捕捉用户的登出操作,后端并非百分百接收到logout请求,经查资料得知,在unload阶段发送的异步请求是不可靠的,有可能被cancel。后面又尝试了fetch,设置了keepalive(即使浏览器关闭,请求照样发送), 但是又发现gapTime<=5的判断条件也存在兼容性问题,不同浏览器的时间差存在差异。此外还存在一些特殊情况:用户可能直接关闭浏览器窗口、断开网络连接或发生其他异常情况,导致浏览器关闭事件无法被触发,因此pass掉上述方案。


后面也尝试了心跳机制(websocket),也存在局限性,pass。


最后想到了一种最简单,最朴实的方式:
开启定时器每秒往localStorage写入当前时间lastRecordTime(new Date().getTime()), 在请求拦截器中给每个接口请求头带上两个时间,最后一次写入时间lastRecordTime和当前时间nowTime, 后端只要把两个时间相减, 超过5s(自定义)就算登出,清掉redis里相应的token。


// 写在APP.vue
created (){
// 每秒写入一次时间
this.timer = setInterval(() => {
// 这个判断代表登录成功后才开始写入时间
if(localStorage.getItem('token')) {
localStorage.setItem('lastRecordTime', new Date().getTime())
}
}, 1000)
}

另外需要注意, 在登录成功的地方要立即写入一次时间, 不然有BUG。


  // 写在请求拦截器
const headers = config.headers;
/** 用于判断用户是否关闭过浏览器,如果关闭则跳转至登录页面,以及及时清理redis中的token */
if (localStorage.getItem('lastRecordTime')) {
headers.lastRecordTime = localStorage.getItem('lastRecordTime');
}
headers.nowTime = new Date().getTime();

总结一下,目前没发现哪种方式可以提供一种可靠的通信方式去通知后端清除token, 通过两个时间差的方式相对靠谱。


作者:起床搬砖啦
来源:juejin.cn/post/7328221562817478665
收起阅读 »

转转流量录制与回放的原理及实践

1 需求背景 随着转转业务规模和复杂度不断提高,业务服务增加,服务之间依赖关系也逐渐复杂。在开发和测试中遇到如下问题: 参数构造:转转接口测试平台可以很方便调用SCF(转转RPC框架),但是接口的参数很多是复杂model,手动构造参数成本不小,希望能够录制稳...
继续阅读 »

1 需求背景


随着转转业务规模和复杂度不断提高,业务服务增加,服务之间依赖关系也逐渐复杂。在开发和测试中遇到如下问题:



  • 参数构造:转转接口测试平台可以很方便调用SCF(转转RPC框架),但是接口的参数很多是复杂model,手动构造参数成本不小,希望能够录制稳定测试环境流量,从流量中抽取接口参数,方便使用者选择参数进行接口测试。

  • 压测流量构造:转转是二手电商平台,有许多促销活动的压测需求,人工构造压测流量既不能模拟真实访问,又成本高昂。所以有录制线上流量的需求,然后压测平台通过策略二次加工形成压测case。

  • 自动化回归测试:业务迭代速度很快,每次迭代会不会影响原有逻辑?希望有一个平台能够提供筛选保存case,自动化回归,case通过率报告通知等能力。


这些问题每个互联网公司都会遇到,如何才能优雅解决这些问题呢?首先定义一下优雅:不增加业务成本,业务基本无感,对业务性能影响要足够小。阿里开源的jvm-sandbox-repeater(简称Repeater)正是为解决这些问题而生,能够做到业务无感,但是性能问题需要特别定制处理。本文重点介绍:



  • Repeater流量录制和回放业务无感实现原理(第2、3章节)

  • 线上服务流量录制时,如何减少对正常业务的性能影响(第4章节)


希望能够揭秘Repeater如何做到业务无感的流量录制和回放,进而让使用流量录制的同学对Repeater内部做了哪些工作以及对性能有哪些影响做到心中有数,最后介绍在流量录制时,为了保证对线上服务的性能影响相对可控,我们做了哪些工作,让大家会用敢用。


2 流量录制和回放概念


2.1 流量录制


对于Java调用,一次流量录制包括一次入口调用(entranceInvocation)(eg:HTTP/Dubbo/Java)和若干次子调用(subInvocations)。流量的录制过程就是把入口调用和子调用绑定成一次完整的记录。


    /**
* 获取商品价格,先从redis中获取,如果redis中没有,再用rpc调用获取,
*
@param productId
*
@return
*/

public Integer getProductPrice(Long productId){ //入口调用

//1.redis获取价格
Integer price = redis.get(productId); //redis远程子调用
if(Objects.isNull(price)){
//2.远程调用获取价格
price = daoRpc.getProductCount(productId); //rpc远程子调用
redis.set(productId, price); //redis远程子调用
}
//3.价格策略处理
price = process(price); //本地子调用
return price;

}

private Integer process(Long price){
//价格策略远程调用
return logicRpc.process(productId); //rpc远程子调用
}

getProductPrice流量录制图解


以获取产品价格方法为例,流量录制的内容简单来说是入口调用(getProductPrice)的入参和返回值,远程子调用(redis.get,daoRpc.getProductCount,redis.set,logicRpc.process)的入参和返回值,用于流量回放。注意并不会录制本地子调用(process)。


下图是转转流量回放平台录制好的单个流量的线上效果,帮助理解流量录制概念。
流量录制


2.2 流量回放


流量回放,获取录制流量的入口调用入参,再次发起调用,并且对于子调用,直接使用流量录制时记录的入参和返回值,根据入参(简单来说)匹配子调用后,直接返回录制的数据。这样就还原了流量录制时的环境,如果回放后返回值和录制时返回值不一致,那么本条回放case标记为失败。
还以getProductPrice为例,假设录制时入口调用参数productId=1,返回值为1;redis.get子调用参数productId=1,返回值为1。那么回放时,redis.get不会访问redis,而是直接返回1。假设该函数有逻辑更新,回放返回值是2,与录制时返回值1不相等,那么次此流量回放标记为失败。


下图是转转流量回放平台的流量回放的线上效果,帮助理解流量回放概念
流量回放


明白流量录制和回放概念后,下面看看业务无感实现流量录制和回放的实现原理。


3 Repeater实现原理


Repeater架构图



  • Repeater Console模块

    • 流量录制和回放的配置管理

    • 心跳管理

    • 录制和回放调用入口



  • Repeater agent plugin模块:Repeater核心功能是流量录制回放,其实现核心是agent插件,开源框架已经实现redis、mybatis、http、okhttp、dubbo、mq、guava cache等插件。由于录制和回放逻辑是以字节码增强的方式在程序运行时织入,所以无需业务编码。换句话说,agent技术是业务无感的关键。


下面我们就进入无感的关键环节,介绍Repeater如何织入流量录制和回放逻辑代码,以及梳理流量录制和回放的核心代码。


3.1 流量录制和回放逻辑如何织入


用一句话来说,Repeater本身并没有实现代码织入功能,它依赖另一个阿里开源项目JVM-Sandbox。详细来讲,Repeater的核心逻辑录制协议基于JVM-Sandbox的BEFORERETRUNTHROW事件机制进行录制流程控制。本质上来说,JVM-Sandbox实现了java agent级别的spring aop功能,是一个通用增强框架。JVM-Sandbox的基于字节码增强的事件机制原理见下图:JVM-Sandbox事件机制


上图以add方法为例,揭示JVM-Sandbox增强前后的代码变化,方便大家理解。下面的代码是对图中增强代码相关重点的注释


public int add(int a, int b) {
try {
Object[] params = new Object[]{a, b};
//BEFORE事件
Spy.Ret retOnBefore = Spy.onBefore(10001,
"com.taobao.test.Test", "add", this, params);
//BEFORE结果可以直接返回结果或者抛出异常,是实现mock(阻断真实远程调用)的关键
if (retOnBefore.state == I_RETURN) return (int) retOnBefore.object;
if (retOnBefore.state == I_THROWS) throws(Throwable) retOnBefore.object;
a = params[0];
b = params[1];
int r = a + b;
//RETRUN事件
Spy.Ret retOnReturn = Spy.onReturn(10001, r);
if (retOnReturn.state == I RETURN)return (int) retOnReturn.object;
if (retOnReturn.state == I_THROWS) throws(Throwable) retOnReturn.object;
return r;
} catch (Throwable cause) {
//THROW事件
Spy.Ret retOnIhrows = Spy.onThrows(10001, cause);
if (retOnThrows.state == I RETURN)return (int) retOnThrows.object;
if (retOnThrows.state == I THROWS) throws(Throwable) retOnThrows.object;
throws cause;
}
}

由上可知,Repeater是利用jvm agent字节码增强技术为目标方法织入BEFORERETRUNTHROW逻辑。


3.2 流量录制和回放的核心代码


既然Repeater利用JVM-Sandbox aop框架编写流量录制和回放逻辑,那么让我们看看它的核心代码doBefore。先来一张流程图。


录制和回放插件逻辑图解


再重点介绍doBeforedoMock的核心代码,它们是实现录制和回放的关键,注意阅读注释。为了方便理解,我对开源代码做了大量删减,只保留核心逻辑。


    /**
* 处理before事件
* 流量录制时记录函数元信息和参数,缓存录制数据
* 流量回放时,调用回放逻辑,直接返回录制时的数据,后面会对processor.doMock进行展开讲解
*
@param event before事件
*/

protected void doBefore(BeforeEvent event) throws ProcessControlException {
// 回放流量;如果是入口则放弃;子调用则进行mock
if (RepeatCache.isRepeatFlow(Tracer.getTraceId())) {
processor.doMock(event, entrance, invokeType);
return;
}
//非回放流量,进行流量录制,主要元信息、参数、返回值
Invocation invocation = initInvocation(event);
//记录是否为入口流量
invocation.setEntrance(entrance);
//记录参数
invocation.setRequest(processor.assembleRequest(event));
//记录返回值
invocation.setResponse(processor.assembleResponse(event));

}

@Override
public void doMock(BeforeEvent event, Boolean entrance, InvokeType type) throws ProcessControlException {

try {

//通过录制数据构建mock请求
final MockRequest request = MockRequest.builder().build();
//执行mock动作
final MockResponse mr = StrategyProvider.instance().provide(context.getMeta().getStrategyType()).execute(request);
//根据mock结果,阻断真实远程调用
switch (mr.action) {
case SKIP_IMMEDIATELY:
break;
case THROWS_IMMEDIATELY:
//直接抛出异常,映射到JVM-Sandbox的事件机制原理的add函数
//也就是代码走到if (retOnBefore.state == I_THROWS) throws(Throwable) retOnBefore.object;
//而不再执行后面的代码(JVM-Sandbox框架机制,调用如下代码会触发阻断真实调用)
ProcessControlException.throwThrowsImmediately(mr.throwable);
break;
case RETURN_IMMEDIATELY:
//直接返回录制结果,映射到JVM-Sandbox的事件机制原理的add函数,同理,也不再执行后面的代码(阻断真实调用)
ProcessControlException.throwReturnImmediately(assembleMockResponse(event, mr.invocation));
break;
default:
ProcessControlException.throwThrowsImmediately(new RepeatException("invalid action"));
break;
}
} catch (ProcessControlException pce) {
throw pce;
} catch (Throwable throwable) {
ProcessControlException.throwThrowsImmediately(new RepeatException("unexpected code snippet here.", throwable));
}
}

通过上面的2、3章节介绍了Repeater流量录制和回放业务无感的实现原理,下面说一下应用过程中需要哪些改造点。


4 Repeater落地实践


4.1 改造点



  • Rpeater开源管理后台仅仅是个Demo,需要重新设计和实现。

  • SCF(转转RPC框架)插件扩展,支持SCF应用的流量录制和回放。

  • DB由MySQL改造为ES,Repeater原生使用MySQL作为流量录制和回放的数据库,仅用于Demo演示,性能和容量无法满足实际需求。

  • Docker环境下频繁更换ip时不中断录制。

  • 回放结果Diff支持字段过滤。

  • 大批量回放。

  • 线上环境录制。


4.2 线上环境录制


流量录制很大一部分应用场景在线下,但是线上也有录制场景。从流量录制的原理可知,由于要记录入口调用和各种远程子调用,开启流量录制后,对于该请求占用内存资源会大大增加,并且会增加耗cpu的序列化操作(用于上报流量录制结果)。既然流量录制是一个天然的耗内存和性能操作,对于线上服务的录制除了保持敬畏之心之外,还有设计一种机制减少录制时对线上服务的性能影响。下面开始介绍如果做到录制时减少对线上服务性能的影响。


线上录制减少性能影响的方案:



  • 从流程上,线上录制需要申请。

  • 从技术上,与发布系统联动,为录制服务增加专门的节点进行录制,并且设置权重为正常节点的1/10,正常节点不会挂载流量录制代码。

  • 从回滚上,如果线上录制节点遇到问题,可以从发布系统直接删除录制节点。


    线上录制效果



5 总结


本文旨在介绍Repeater流量录制和回放的实现原理,以及在落地过程中改造点,希望达到让大家懂原理、会使用、敢使用的目的。


作者:转转技术团队
来源:juejin.cn/post/7327538517528068106
收起阅读 »

Java 世界的法外狂徒:反射

概述 反射(Reflection)机制是指在运行时动态地获取类的信息以及操作类的成员(字段、方法、构造函数等)的能力。通过反射,我们可以在编译时期未知具体类型的情况下,通过运行时的动态查找和调用。 虽然 Java 是静态的编译型语言,但是反射特性的加入,提供...
继续阅读 »

Reflection Title


概述


反射(Reflection)机制是指在运行时动态地获取类的信息以及操作类的成员(字段、方法、构造函数等)的能力。通过反射,我们可以在编译时期未知具体类型的情况下,通过运行时的动态查找和调用。 虽然 Java 是静态的编译型语言,但是反射特性的加入,提供一种直接操作对象外的另一种方式,让 Java 具备的一些灵活性和动态性,我们可以通过本篇文章来详细了解它


为什么需要反射 ?


Java 需要用到反射的主要原因包括以下几点:



  1. 运行时动态加载,创建类:Java中的类是在编译时加载的,但有时希望在运行时根据某些条件来动态加载和创建所需要类。反射就提供这种能力,这样的能力让程序可以更加的灵活,动态

  2. 动态的方法调用:根据反射获取的类和对象,动态调用类中的方法,这对于一些类增强框架(例如 Spring 的 AOP),还有安全框架(方法调用前进行权限验证),还有在业务代码中注入一些通用的业务逻辑(例如一些日志,等,动态调用的能力都非常有用

  3. 获取类的信息:通过反射,可以获取类的各种信息,如类名、父类、接口、字段、方法等。这使得我们可以在运行时检查类的属性和方法,并根据需要进行操作


一段示例代码


以下是一个简单的代码示例,展示基本的反射操作:


import java.lang.reflect.Method;

public class ReflectionExample {
public static void main(String[] args) {
// 假设在运行时需要调用某个类的方法,但该类在编译时未知
String className = "com.example.MyClass";

try {
// 使用反射动态加载类
Class<?> clazz = Class.forName(className);

// 使用反射获取指定方法
Method method = clazz.getMethod("myMethod");

// 使用反射创建对象
Object obj = clazz.newInstance();

// 使用反射调用方法
method.invoke(obj);

} catch (ClassNotFoundException e) {
System.out.println("类未找到:" + className);
} catch (NoSuchMethodException e) {
System.out.println("方法未找到");
} catch (IllegalAccessException | InstantiationException e) {
System.out.println("无法实例化对象");
} catch (Exception e) {
System.out.println("其他异常:" + e.getMessage());
}
}
}

在这个示例中,我们假设在编译时并不知道具体的类名和方法名,但在运行时需要根据动态情况来加载类、创建对象并调用方法。使用反射机制,我们可以通过字符串形式传递类名,使用 Class.forName() 动态加载类。然后,通过 getMethod() 方法获取指定的方法对象,使用 newInstance() 创建类的实例,最后通过 invoke() 方法调用方法。


使用场景


技术再好,如果无法落地,那么始终都是空中楼阁,在日常开发中,我们常常可以在以下的场景中看到反射的应用:



  1. 框架和库:许多框架和库使用反射来实现插件化架构或扩展机制。例如,Java 的 Spring 框架使用反射来实现依赖注入(Dependency Injection)和 AOP(Aspect-Oriented Programming)等功能。

  2. ORM(对象关系映射):ORM 框架用于将对象模型和关系数据库之间进行映射。通过反射,ORM 框架可以在运行时动态地读取对象的属性和注解信息,从而生成相应的 SQL 语句并执行数据库操作。

  3. 动态代理:动态代理是一种常见的设计模式,通过反射可以实现动态代理。动态代理允许在运行时创建代理对象,并拦截对原始对象方法的调用。这在实现日志记录、性能统计、事务管理等方面非常有用

  4. 反射调试工具:在开发和调试过程中,有时需要查看对象的结构和属性,或者动态调用对象的方法来进行测试。反射提供了一种方便的方式来检查和操作对象的内部信息,例如使用getDeclaredFields()获取对象的所有字段,或使用getMethod()获取对象的方法

  5. 单元测试:在单元测试中,有时需要模拟或替换某些对象的行为,以便进行有效的测试。通过反射,可以在运行时创建对象的模拟实例,并在测试中替换原始对象,以便控制和验证测试的行为


Class 对象


Class 对象是反射的第一步,我们先从 Class 对象聊起,因为在反射中,只要你想在运行时使用类型信息,就必须先得到那个 Class 对象的引用,他是反射的核心,它代表了Java类的元数据信息,包含了类的结构、属性、方法和其他相关信息。通过Class对象,我们可以获取和操作类的成员,实现动态加载和操作类的能力。


常见的获取 Class 对象的方式几种:


// 使用类名获取
Class<?> clazz = Class.forName("com.example.MyClass");

// 使用类字面常量获取
Class<?> clazz = MyClass.class;

// 使用对象的 getClass() 方法获取
MyClass obj = new MyClass();
Class<?> clazz = obj.getClass();


需要注意的是,如果 Class.forName() 找不到要加载的类,它就会抛出异常 ClassNotFoundException



正如上面所说,获取 Class 对象是第一步,一旦获取了Class对象,我们可以使用它来执行各种反射操作,例如获取类的属性、方法、构造函数等。示例:


String className = clazz.getName(); // 获取类的全限定名
int modifiers = clazz.getModifiers(); // 获取类的修饰符,如 public、abstract 等
Class<?> superClass = clazz.getSuperclass(); // 获取类的直接父类
Class<?> superClass = clazz.getSuperclass(); // 获取类的直接父类
Class<?>[] interfaces = clazz.getInterfaces(); // 获取类实现的接口数组
Constructor<?>[] constructors = clazz.getConstructors(); // 获取类的公共构造函数数组
Method[] methods = clazz.getMethods(); // 获取类的公共方法数组
Field[] fields = clazz.getFields(); // 获取类的公共字段数组
Object obj = clazz.newInstance(); // 创建类的实例,相当于调用无参构造函数

上述示例仅展示了Class对象的一小部分使用方法,还有许多其他方法可用于获取和操作类的各个方面。通过Class对象,我们可以在运行时动态地获取和操作类的信息,实现反射的强大功能。


类型检查


在反射的代码中,经常会对类型进行检查和判断,从而对进行对应的逻辑操作,下面介绍几种 Java 中对类型检查的方法


instanceof 关键字


instanceof 是 Java 中的一个运算符,用于判断一个对象是否属于某个特定类或其子类的实例。它返回一个布尔值,如果对象是指定类的实例或其子类的实例,则返回true,否则返回false。下面来看看它的使用示例


1:避免类型转换错误


在进行强制类型转换之前,使用 instanceof 可以检查对象的实际类型,以避免类型转换错误或 ClassCastException 异常的发生:


if (obj instanceof MyClass) {
MyClass myObj = (MyClass) obj;
// 执行针对 MyClass 类型的操作
}

2:多态性判断


使用 instanceof 可以判断对象的具体类型,以便根据不同类型执行不同的逻辑。例如:


if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.bark();
} else if (animal instanceof Cat) {
Cat cat = (Cat) animal;
cat.meow();
}

3:接口实现判断


在使用接口时,可以使用 instanceof 判断对象是否实现了某个接口,以便根据接口进行不同的处理


if (obj instanceof MyInterface) {
MyInterface myObj = (MyInterface) obj;
myObj.doSomething();
}

4:继承关系判断


instanceof 可以用于判断对象是否是某个类的子类的实例。这在处理继承关系时非常有用,可以根据对象的具体类型执行相应的操作


if (obj instanceof MyBaseClass) {
MyBaseClass myObj = (MyBaseClass) obj;
// 执行 MyBaseClass 类型的操作
}

instanceof 看似可以做很多事情,但是在使用时也有很多限制,例如:



  1. 无法和基本类型进行匹配:instanceof 运算符只能用于引用类型,无法用于原始类型

  2. 不能和 Class 对象类型匹配:只可以将它与命名类型进行比较

  3. 无法判断泛型类型参数:由于Java的泛型在运行时会进行类型擦除,instanceof 无法直接判断对象是否是某个泛型类型的实例



instanceof 看似方便,但过度使用它可能表明设计上的缺陷,可能违反了良好的面向对象原则。应尽量使用多态性和接口来实现对象行为的差异,而不是过度依赖类型检查。



isInstance() 函数


java.lang.Class 类也提供 isInstance() 类型检查方法,用于判断一个对象是否是指定类或其子类的实例。更适合在反射的场景下使用,代码示例:


Class<?> clazz = MyClass.class;
boolean result = clazz.isInstance(obj);

如上所述,相比 instanceof 关键字,isInstance() 提供更灵活的类型检查,它们的区别如下:



  1. isInstance() 方法的参数是一个对象,而 instanceof 关键字的操作数是一个引用类型。因此,使用 isInstance() 方法时,可以动态地确定对象的类型,而 instanceof 关键字需要在编译时指定类型。

  2. isInstance()方法可以应用于任何Class对象。它是一个通用的类型检查方法。而instanceof关键字只能应用于引用类型,用于检查对象是否是某个类或其子类的实例。

  3. isInstance()方法是在运行时进行类型检查,它的结果取决于实际对象的类型。而instanceof关键字在编译时进行类型检查,结果取决于代码中指定的类型。

  4. 由于Java的泛型在运行时会进行类型擦除,instanceof无法直接检查泛型类型参数。而isInstance()方法可以使用通配符类型(<?>)进行泛型类型参数的检查。


总体而言,isInstance()方法是一个动态的、通用的类型检查方法,可以在运行时根据实际对象的类型来判断对象是否属于某个类或其子类的实例。与之相比,instanceof关键字是在编译时进行的类型检查,用于检查对象是否是指定类型或其子类的实例。它们在表达方式、使用范围和检查方式等方面有所差异。在具体的使用场景中,可以根据需要选择合适的方式进行类型检查。


代理


代理模式


代理模式是一种结构型设计模式,其目的是通过引入一个代理对象,控制对原始对象的访问。代理对象充当了原始对象的中间人,可以在不改变原始对象的情况下,对其进行额外的控制和扩展。这是一个简单的代理模式示例:


// 定义抽象对象接口
interface Image {
void display();
}

// 定义原始对象
class RealImage implements Image {
private String fileName;

public RealImage(String fileName) {
this.fileName = fileName;
loadFromDisk();
}

private void loadFromDisk() {
System.out.println("Loading image:" + fileName);
}

@Override
public void display() {
System.out.println("Displaying image:" + fileName);
}
}

// 定义代理对象
class ImageProxy implements Image {
private String filename;
private RealImage realImage;

public ImageProxy(String filename) {
this.filename = filename;
}

@Override
public void display() {
if (realImage == null) {
realImage = new RealImage(filename);
}
realImage.display();
}
}

public class ProxyPatternExample {
public static void main(String[] args) {
// 使用代理对象访问实际对象
Image image = new ImageProxy("test_10mb.jpg");
// 第一次访问,加载实际对象
image.display();
// 第二次访问,直接使用已加载的实际对象
image.display();
}
}

输出结果:


Loading image:test_10mb.jpg
Displaying image:test_10mb.jpg
Displaying image:test_10mb.jpg

在上述代码中,我们定义了一个抽象对象接口 Image,并有两个实现类:RealImage 代表实际的图片对象,ImageProxy 代表图片的代理对象。在代理对象中,通过控制实际对象的加载和访问,实现了延迟加载和额外操作的功能。客户端代码通过代理对象来访问图片,实现了对实际对象的间接访问。


动态代理


Java的动态代理是一种在运行时动态生成代理类和代理对象的机制,它可以在不事先定义代理类的情况下,根据接口或父类来动态创建代理对象。动态代理使用Java的反射机制来实现,通过动态生成的代理类,可以在方法调用前后插入额外的逻辑。


以下是使用动态代理改写上述代码的示例:


import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 定义抽象对象接口
interface Image {
void display();
}

// 定义原始对象
class RealImage implements Image {
private String filename;

public RealImage(String filename) {
this.filename = filename;
loadFromDisk();
}

private void loadFromDisk() {
System.out.println("Loading image: " + filename);
}

public void display() {
System.out.println("Displaying image: " + filename);
}
}

// 实现 InvocationHandler 接口的代理处理类
class ImageProxyHandler implements InvocationHandler {

private Object realObject;

public ImageProxyHandler(Object realObject) {
this.realObject = realObject;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
if (method.getName().equals("display")) {
System.out.println("Proxy: before display");
result = method.invoke(realObject, args);
System.out.println("Proxy: after display");
}
return result;
}
}

public class DynamicProxyExample {

public static void main(String[] args) {
// 创建原始对象
Image realImage = new RealImage("image.jpg");
// 创建动态代理对象
Image proxyImage = (Image) Proxy.newProxyInstance(Image.class.getClassLoader(), new Class[]{Image.class}, new ImageProxyHandler(realImage));
// 使用代理对象访问实际对象
proxyImage.display();
}
}

在上述代码中,我们使用 java.lang.reflect.Proxy 类创建动态代理对象。我们定义了一个 ImageProxyHandler 类,实现了 java.lang.reflect.InvocationHandler 接口,用于处理代理对象的方法调用。在 invoke() 方法中,我们可以在调用实际对象的方法之前和之后执行一些额外的逻辑。


输出结果:


Loading image: image.jpg
Proxy: before display
Displaying image: image.jpg
Proxy: after display

在客户端代码中,我们首先创建了实际对象 RealImage,然后通过 Proxy.newProxyInstance() 方法创建了动态代理对象 proxyImage,并指定了代理对象的处理类为 ImageProxyHandler。最后,我们使用代理对象来访问实际对象的 display() 方法。


通过动态代理,我们可以更加灵活地对实际对象的方法进行控制和扩展,而无需显式地创建代理类。动态代理在实际开发中常用于 AOP(面向切面编程)等场景,可以在方法调用前后添加额外的逻辑,如日志记录、事务管理等。


违反访问权限


在 Java 中,通过反射机制可以突破对私有成员的访问限制。以下是一个示例代码,展示了如何使用反射来访问和修改私有字段:


import java.lang.reflect.Field;

class MyClass {
private String privateField = "Private Field Value";
}

public class ReflectionExample {

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
MyClass myObj = new MyClass();
// 获取私有字段对象
Field privateField = MyClass.class.getDeclaredField("privateField");

// 取消对私有字段的访问限制
privateField.setAccessible(true);

// 获取私有字段的值
String fieldValue = (String) privateField.get(myObj);
System.out.println("Original value of privateField: " + fieldValue);

// 修改私有字段的值
privateField.set(myObj, "New Field Value");

// 再次获取私有字段的值
fieldValue = (String) privateField.get(myObj);
System.out.println("Modified value of privateField: " + fieldValue);
}
}

在上述代码中,我们定义了一个 MyClass 类,其中包含一个私有字段 privateField。在 ReflectionExample 类的 main 方法中,我们使用反射获取了 privateField 字段,并通过 setAccessible(true) 方法取消了对私有字段的访问限制。然后,我们使用 get() 方法获取私有字段的值并输出,接着使用 set() 方法修改私有字段的值。最后,再次获取私有字段的值并输出,验证字段值的修改。


输出结果:


Original value of privateField: Private Field Value
Modified value of privateField: New Field Value

除了字段,通过反射还可以实现以下违反访问权限的操作:



  • 调用私有方法

  • 实例化非公开的构造函数

  • 访问和修改静态字段和方法

  • 绕过访问修饰符检查


虽然反射机制可以突破私有成员的访问限制,但应该慎重使用。私有成员通常被设计为内部实现细节,并且具有一定的安全性和封装性。过度依赖反射访问私有成员可能会破坏代码的可读性、稳定性和安全性。因此,在使用反射突破私有成员限制时,请确保了解代码的设计意图和潜在风险,并谨慎操作。


总结


反射技术自 JDK 1.1 版本引入以来,一直被广泛使用。它为开发人员提供了一种在运行时动态获取类的信息、调用类的方法、访问和修改类的字段等能力。在过去的应用开发中,反射常被用于框架、工具和库的开发,以及动态加载类、实现注解处理、实现代理模式等场景。反射技术为Java的灵活性、可扩展性和动态性增添了强大的工具。


当下,反射技术仍然发挥着重要的作用。它被广泛应用于诸多领域,如框架、ORM(对象关系映射)、AOP(面向切面编程)、依赖注入、单元测试等。反射技术为这些领域提供了灵活性和可扩展性,使得开发人员能够在运行时动态地获取和操作类的信息,以实现更加灵活和可定制的功能。同时,许多流行的开源框架和库,如 Spring、Hibernate、JUnit 等,也广泛使用了反射技术。


反射技术可能继续发展和演进。随着 Java 平台的不断发展和语言特性的增强,反射技术可能会在性能优化,安全性,模块化等方面进一步完善和改进反射的应用。然而,需要注意的是,反射技术应该谨慎使用。由于反射涉及动态生成代码、绕过访问限制等操作,如果使用不当,可能导致代码的可读性和性能下降,甚至引入安全漏洞。因此,开发人员在使用反射时应该充分理解其工作原理和潜在的风险,并且遵循最佳实践。


作者:小二十七
来源:juejin.cn/post/7235513984556220476
收起阅读 »

🌟前端使用Lottie实现炫酷的开关效果🌟

web
前言 在平时的开发过程中,前端或多或少都会遇到实现动画效果的场景。手写动画是一件相当麻烦的事情,调来调去不仅费时费力,可能还会被产品/UI吐槽:这动画效果也不难呀,为什么就不能实现呢?/为什么就没有还原成我想要的样子呢。 比如说产品让我们实现这样的一个开关动...
继续阅读 »

前言


在平时的开发过程中,前端或多或少都会遇到实现动画效果的场景。手写动画是一件相当麻烦的事情,调来调去不仅费时费力,可能还会被产品/UI吐槽:这动画效果也不难呀,为什么就不能实现呢?/为什么就没有还原成我想要的样子呢。


image.png


比如说产品让我们实现这样的一个开关动效


Kapture 2024-01-20 at 21.53.34.gif


今天我们就用动画的实现方式——Lottie,来百分百还原设计师的动画效果,并且可以大大提高我们的工作效率(摸鱼时间)。


image.png


Lottie简介


首先我们先来看一下,平时我们实现动画都有哪些方式,它们分别有什么优缺点:


动画类型优点缺点
CSS 动画使用简便,通过@keyframestransition创建动画;浏览器原生支持,性能较好控制有限,不适用于复杂动画;复杂动画可能需要大量 CSS 代码,冗长
JavaScript 动画提供更高程度的控制和灵活性;适用于复杂和精细动画效果引入库增加页面负担,可能需要学习曲线;使用不当容器对页面性能造成影响,产生卡顿
GIF 动画制作和使用简单,无需额外代码;几乎所有浏览器原生支持有限颜色深度,不适用于所有场景;清晰度与文件尺寸成正比,无法适应所有分辨率
Lottie支持矢量动画,保持清晰度和流畅性 ;跨平台使用,适用于 iOS、Android 和 Web在一些较旧或性能较低的设备上,播放较大的 Lottie 动画可能会导致性能问题;对设计师要求较高

Lottie是由Airbnb开发的一个开源库,用于在移动端和Web上呈现矢量动画。它基于JSON格式的Bodymovin文件,可以将由设计师在AE中创建的动画导出为可在Lottie库中播放的文件。


相对于手写CSS/JS动画而言,它可以大大减少前端开发的工作量,相对于GIF文件来说,它可以在一个合理的文件体积内保证动画的清晰度以及流畅程度。下面我们就介绍一下如何播放一个Lottie动画,并实现一个炫酷的开关效果。


Hello Lottie


假设我们现在已经有一个Lottiejson文件,那么现在安装一些依赖


npm i react-lottie prop-types

安装完之后我们就可以这样子来播放一个Lottie动画:


import animationData from "../../assets/switch-lottie.json";

const LottieSwitch = () => {
const playing = useRef(false);
const options = {
loop: true,
autoplay: true,
animationData: animationData,
rendererSettings: {
preserveAspectRatio: "xMidYMid slice",
},
};
return (
<Lottie
options={options}
height={20}
width={40}
/>

);
};


Kapture 2024-01-20 at 21.41.20.gif


来解释一下上面的options参数里面各个字段是什么意思:



  • loop:是否循环播放

  • autoplay:是否自动播放

  • animationDataLottie动画json资源

  • rendererSettings.preserveAspectRatio:指定如何在给定容器中渲染Lottie动画

    • xMidYMid: 表示在水平和垂直方向上都在中心对齐

    • 表示保持纵横比,但可能会裁剪超出容器的部分




正/反向播放


正常的把Lottie动画播放出来之后,我们就可以开始实现一个开关的功能。其实就是点击的时候更换Lottie的播放方向,这里对应的是direction字段,direction1时正向播放,direction-1时反向播放。


我们就要实现下面的功能:



  • 点击时切换方向

  • 播放过程中加锁,禁止切换方向

  • 监听播放结束事件,解锁

  • loop改为falseautoplay改为false


实现代码如下:


const LottieSwitch = () => {
const [direction, setDirection] = useState(null);
const playing = useRef(false);
const options = {
loop: false,
autoplay: false,
animationData: animationData,
rendererSettings: {
preserveAspectRatio: "xMidYMid slice",
},
};

const handleClick = () => {
if (playing.current) {
return;
}
playing.current = true;
setDirection((prevState) => (prevState === 1 ? -1 : 1));
};
return (
<div style={{ padding: 40 }}>
<div onClick={handleClick} className={styles.lottieWrapper}>
<Lottie
direction={direction}
options={options}
speed={2}
height={20}
width={40}
eventListeners={[
{
eventName: "complete",
callback: () =>
{
playing.current = false;
},
},
]}
/>
</div>
</div>

);
};

这样我们就是实现了一个开关的效果


Kapture 2024-01-20 at 21.53.34.gif


持续时长


Lottiejson中,有几个关键的字段跟动画的播放时长有关系:



  • fr:帧率,每一秒的帧数

  • ip:开始帧

  • op:结束帧


假如说有下面的一个描述:


{
"fr": 30,
"ip": 0,
"op": 60,
}

则表示帧率是30帧,从第0帧开始,60帧结束,那这个动画的持续时长是 (op-ip)/fr,为2s。那如果我们希望整个动画的播放时长是500ms,则只需要把Lottie的倍速调整为4。对应的就是speed字段:


<Lottie
direction={direction}
options={options}
speed={4}
height={20}
width={40}
eventListeners={[
{
eventName: "complete",
callback: () => {
playing.current = false;
},
},
]}
/>

Kapture 2024-01-20 at 22.06.53.gif


修改Lottie


Lottie json中,描述整个动画的过程以及效果其实对应的就是某个值。在实现的过程中,其实开发是可以去修改这些值的。比如说我们可以修改上面开关的边框颜色以及小球的颜色。


首先在页面中找到小球对应的颜色是rgb(99, 102, 241)


image.png


Lottie JSON文件中,颜色信息通常出现在表示图层样式的字段中。常见的字段是 "c"(color)
"c" 字段表示颜色,通常以RGBA格式(红绿蓝透明度)存储。例如:


"c": {"a":0,"k":[1,0,0,1]}

这表示红色,RGBA值为 [1, 0, 0, 1]


rgb(99, 102, 241)转成上面的写法那就是"c": {"a":0,"k":[99/255,102/255,241/255,1]}。以99/255为例,结果是0.38823529411764707,那么就拿这个结果去json文件中找到对应的节点。


image.png


对应有2个结果,就是小球的颜色以及边框的颜色。当我们找到这个值的时候,如果我们想修改这个值,就必须知道这个值的路径,在一个Lottie中,想肉眼找到这个值的路径是一件很难的事情。所以我们写一个辅助函数:


const updateJsonValue = (json, targetValue, newValue) => {
const find = (json, targetValue, currentPath = []) => {
for (const key in json) {
if (json[key] === targetValue) {
return [...currentPath, key];
} else if (typeof json[key] === "object" && json[key] !== null) {
const path = find(json[key], targetValue, [...currentPath, key]);
if (path) {
return path;
}
}
}
};
const res = JSON.parse(JSON.stringify(json));
const path = find(res, targetValue);
let current = res;

for (let i = 0; i < path.length - 1; i++) {
const key = path[i];
current = current[key];
}

const lastKey = path[path.length - 1];
current[lastKey] = newValue;

return json;
};

上面的辅助函数就帮助我们找到这个值的路径,并修改目标值。比如说我们想把目前的颜色改成绿色(rgb(25, 195, 125)),就可以找到对应的路径,并修改。别忘了替换的时候把rgb对应的值除以255


let newAnimationData = updateJsonValue(animationData, 0.388235300779, 0.09803921568627451)
newAnimationData = updateJsonValue(newAnimationData, 0.388235300779, 0.09803921568627451)
newAnimationData = updateJsonValue(newAnimationData, 0.40000000596, 0.7647058823529411)
newAnimationData = updateJsonValue(newAnimationData, 0.40000000596, 0.7647058823529411)
newAnimationData = updateJsonValue(newAnimationData, 0.945098042488, 0.49019607843137253)
newAnimationData = updateJsonValue(newAnimationData, 0.945098042488, 0.49019607843137253)

image.png


掌握了这种方式之后,我们就能修改Lottie里面的大部分内容,包括文案、资源图片、颜色等等。


最后


以上就是一些Lottie的使用以及修改的介绍,下次再遇到比较麻烦的动画需求。就可以跟产品说:可以做,让UI给我导出一个Lottie


image.png


如果你有一些别的想法,欢迎评论区交流~如果你觉得有意思的话,点点关注点点赞吧~


作者:可乐鸡翅kele
来源:juejin.cn/post/7325717778597773348
收起阅读 »

从‘相信前端能做一切’到‘连这个都做不了么’

web
帮助阅读 此篇文章主要是为了实现仪表盘功能,前后过了4种方案,每篇方案从逻辑、代码、效果、问题四个方面出发。最后个人总结。同时非常非常希望有大佬能够提供一个方案,个人实在想不到实现方案了 需求 h5页面中,做一个环形仪表盘(如下图),需要一个从0%到实际百分比...
继续阅读 »

4711705568245_.pic.jpg


帮助阅读


此篇文章主要是为了实现仪表盘功能,前后过了4种方案,每篇方案从逻辑、代码、效果、问题四个方面出发。最后个人总结。同时非常非常希望有大佬能够提供一个方案,个人实在想不到实现方案了


需求


h5页面中,做一个环形仪表盘(如下图),需要一个从0%到实际百分比的增长过渡动效
未命名.png


前提


使用前端原生Html、css、js语言实现, 不打算借助第三方插件。


最初Scheme


将UI图片作为背景,上面放一个白色div作为遮罩,再利用css3将白色div旋转,从而达到过渡效果。


代码如下:


<style>
.light-strip {
width: 500px;
height:500px;
border: 1px solid #efefef;
background-image: url('Frame 29@3x.png');
float: right;
background-size: 100% 100%;
}
.light-strip-top {
margin-top: 0%;
width: 500px;
height: 250px;
background: #fff;
transform-origin: bottom center;
/* transform: rotate 5s ; */
rotate: 0deg;
transition: all 2s ease-in-out;
}
</style>
<body onload="load()">
<div class="light-strip">
<div class="light-strip-top">

</div>
</div>
</body>
<script>
function load() {
setTimeout(() => {
document.querySelectorAll('.light-strip-top')[0].setAttribute('style', "rotate: 180deg")
}, 1000)
}
</script>

效果如下:


屏幕录制2024-01-29 13.50.58.gif


出现问题:


由于仪表盘整体大于180度,所以白色div,在最开始遮挡全部仪表盘,那么旋转一定角度后一定会覆盖UI图。


进化Scheme


根据上面出现的问题,想到与UI沟通将仪表盘改成180度效果(解决不了问题,就把问题解决掉),该方案由于改变了原型之后会导致UI过于丑,就没有进行深度测试。


超进化Scheme


根据上面两个方案的结合,想到将方案1中的白色div换成一张指针图片,利用css3旋转追针,达到过渡效果,但此方案也是改了UI效果。


代码如下:


	<style>
.light-strip {
width: 500px;
height:500px;
border: 1px solid #efefef;
background-image: url('Frame 29@3x.png');
/* background-color: #fff; */
float: right;
background-size: 100% 100%;
}
.light-strip-top {
margin-top: 50%;
width: 49%;
height: 4px;
background: red;
transform-origin: center right;
/* transform: rotate 5s ; */
rotate: -35deg;
transition: all 2s ease-in-out;
}

</style>
<body onload="load()">
<div class="light-strip">
<div class="light-strip-top">

</div>
</div>
</body>
<script>
function load() {
setTimeout(() => {
document.querySelectorAll('.light-strip-top')[0].setAttribute('style', "rotate: 90deg")
}, 1000)
}
</script>

效果如下:


屏幕录制2024-01-29 15.44.31.gif


Now:


此时大脑宕机了,在我的前端知识基础上,想不到能够完美实现UI效果的方案了。于是和同事探讨了一下,了解到element-plus中的进度条有类似的效果,于是打算看一下源码,了解到它是svg实现的。发现新大陆又开始尝试svg实现。


究极进化Scheme


利用svg,做一个带白色的背景圆环A,再做一个带有渐变背景色的进度圆环B, 利用进度圆环的偏移值、显示长度、断口长度配合css3过渡实现过渡效果。


代码如下:


 <style>
body {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
background-color: #f5f5f5;
}

.dashboard {
position: relative;
width: 200px;
height: 200px;
background-size: 100% 100%;
}

.circle-background {
fill: none; /* 不填充 */
stroke: #fff; /* 圆环的颜色 */
stroke-width: 10; /* 圆环的宽度 */
stroke-dasharray: 200, 52; /* 圆环断开部分的长度,总长度为周长 */
stroke-dashoffset: 163;
stroke-linecap: round;
border-radius: 10;
transition: all 1s; /* 过渡效果时间 */
}

.circle-progress {
fill: none; /* 不填充 */
stroke: url(#gradient); /* 圆环的颜色 */
stroke-width: 10; /* 圆环的宽度 */
stroke-dasharray: 252, 0; /* 圆环断开部分的长度,总长度为周长 */
stroke-dashoffset: 163;
stroke-linecap: round; /* 圆滑断点 */
transition: all 1s; /* 过渡效果时间 */
}

.percentage {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24px;
color: #3498db;
}
</style>
</head>
<body>

<svg class="dashboard" viewBox="0 0 100 100">
<!-- 定义渐变色 -->
<defs>
<linearGradient id="gradient" gradientUnits="userSpaceOnUse" x1="50" y1="0" x2="50" y2="100%">
<stop offset="0%" style="stop-color: rgba(111, 232, 191, 1)" />
<stop offset="33%" style="stop-color: rgba(255, 175, 19, 1)" />
<stop offset="70%" style="stop-color: rgba(222, 19, 80, 1)" />
<stop offset="100%" style="stop-color: rgba(133, 14, 205, 1)" />
</linearGradient>
</defs>

<!-- 背景圆环 -->
<circle class="circle-background" cx="50" cy="50" r="40"></circle>

<!-- 进度圆环 -->
<circle class="circle-progress" cx="50" cy="50" r="40"></circle>

</svg>

<!-- 进度百分比显示 -->
<div class="percentage" id="percentage">0%</div>

<script>
function setProgress(percentage) {
const circleProgress = document.querySelector('.circle-progress');
const circleBackground = document.querySelector('.circle-background');
const percentageText = document.getElementById('percentage');

const circumference = 2 * Math.PI * 40; // 圆的周长
const circumNewLength = (percentage / 100) * (circumference - 52);
const dashOffset = 163 - circumNewLength;


// 设置进度圆环的样式
circleBackground.style.strokeDashoffset = dashOffset;
circleBackground.style.strokeDasharray = `${200 - circumNewLength}, ${ 52 + circumNewLength }`
circleProgress.style.strokeDasharray = `${circumNewLength}, ${ circumference - circumNewLength }`

// 更新百分比文本
percentageText.textContent = `${percentage}%`;
}

// 设置初始进度为0%
setProgress(0);

// 模拟过渡效果,从0%到50%
setTimeout(() => {
setProgress(50);
}, 1000); // 过渡时间为1秒,你可以根据需要调整这个值
</script>


效果如下:


屏幕录制2024-01-29 15.46.35.gif


问题:


基本实现,但是还有一个问题是,渐变色是两点之间的线性渐变,无法做到圆环的顺时针渐变。


总结



  • 单纯前端不是万能的😂😂😂😂

  • 个人认为这个需求还是能够实现的

  • 希望有da lao能出个方案

  • 加油,继续搞


作者:Otway
来源:juejin.cn/post/7329310941106356275
收起阅读 »

伪指纹浏览器开发的那些事

web
什么是伪指纹浏览器开发 就是通过开源的chromium浏览器进行二次简单的封装不涉及到重新编译chromium,配合puppeteer进行轻微的指纹修改开发 一、如何操作 本次操作客户端以前端擅长的electron来举例子,至于electron是什么,打开文心...
继续阅读 »

什么是伪指纹浏览器开发


就是通过开源的chromium浏览器进行二次简单的封装不涉及到重新编译chromium,配合puppeteer进行轻微的指纹修改开发


一、如何操作


本次操作客户端以前端擅长的electron来举例子,至于electron是什么,打开文心一言看看...


第一步下载chromium到本地客户端


登录官网,看到如下界面


image.png


可以发现箭头处指定是浏览器对应的版本buildId和系统,这里可以直接手动点击下载到本地,也可以通过@puppeteer/browsers这个库使用js代码去下载。这里说说如何使用它下载


const { app } = require('electron')
const browserApi = require('@puppeteer/browsers')
const axios = require('axios')

// browser缓存路径,避免和electron一起打包占用安装包体积和打包时间
const cacheDir = `${app.getPath('cache')}/myBrowser`

browserApi.install({
cacheDir, // 自己想要下载的路径,用来给puppeteer去调用
browser: browserApi.Browser.CHROMIUM,
// buildId: '1247373',
// baseUrl: 'https://commondatastorage.googleapis.com/chromium-browser-snapshots'
})

耐心的小伙伴肯定发现了这里buildId版本号和baseUrl下载url我打了注释,是因为@puppeteer/browsers默认下载的chromium版本比较旧,那么我们怎么获取这个最新版本buildId和baseUrl呢,还是官网那个界面打开控制台,可以看到如下请求链接


image.png
然后看到请求结果
image.png
这就是最新的buildId了,然后封装成函数调用


// 获取最新的chromium构建ID
function getLastBuildId(platform) {
return axios
.get(
`https://download-chromium.appspot.com/rev/${browserApi.BrowserPlatform.MAC}?type=snapshots`
)
.then((res) => res.data.content)
}

baseUrl可以在界面点击下载时候,看到控制台有一个请求,那就是baseUrl了


image.png
下载好后,可以去我们定义的下载保存地址,通过终端去打开就可以看到了


二、第二步启动chromium


使用puppeteer-core这个库,启动我们下好的chromium


const puppeteer = require('puppeteer-core')
const browserApi = require('@puppeteer/browsers')

// browser缓存路径
const cacheDir = `${app.getPath('cache')}/myBrowser`

// 获取安装的浏览器路径
function getBrowserPath() {
return browserApi
.getInstalledBrowsers({ cacheDir })
.then((list) => list[0]?.executablePath)
}

// 浏览器生成
const createBrowser = async (proxyServer, userAgent) => {
const browser = await puppeteer.launch({
args: [
`--proxy-server=${proxyServer}`,
`--user-agent="${userAgent}"`,
'--no-first-run',
'--no-zygote',
'--disable-extensions',
'--disable-infobars',
'--disable-automation',
'--no-default-browser-check',
'--disable-device-orientation',
'--disable-metrics-reporting',
'--disable-logging'
],
headless: false,
defaultViewport: null,
ignoreHTTPSErrors: true,
ignoreDefaultArgs: [
'--enable-infobars',
'--enable-features=NetworkService,NetworkServiceInProcess',
'--enable-automation',
'about:blank'
],
executablePath: await getBrowserPath()
})

return browser
}

通过puppeteer.launch启动一个浏览器,至于启动参数这里我只说指纹相关的两个参数--proxy-server--user-agent,其他AI一下。


--proxy-server代理服务,浏览器访问的出口IP,即你用自己启动的浏览器访问google时候,那边服务端获取的ip就是你的代理ip,测试时候可以自己在另外一台机器上装个Squid测试。--user-agent即浏览器的window.navigator.userAgent,简单指纹一般都是依赖于它生成


三、开发过程中用到的功能点


看完puppeteer官网,我们知道操作chromium依赖于一套协议chromedevtools.github.io/devtools-pr…


3.1 更换dock图标


比如多开浏览器,我如何更换chromium的桌面dock图标,去标识这是我启动的第几个浏览器。我们可以使用Browser.setDockTile去操作浏览器更换dock图标


const pages = await browser.pages()
const page = pages[0]
const session = await pages[0].target().createCDPSession()
await session.send('Browser.setDockTile', {
image: new Buffer.from(fs.readFileSync(file)).toString('base64')
})

效果如下:


image.png


更多的协议操作需要自己摸索了,提示下,AI搜索chrome cdp协议


3.2 增加默认书签


这里我没找到协议,直接通过类似爬虫的方式,先进入标签管理页面,直接操作js新增,也算是一个技巧性的骚操作


await page.goto('chrome://bookmarks/') // 进入标签管理页面
await page.evaluate(async () => {
// 类似在控制台直接操作一样,下面的代码控制台一样可以达到效果
const defaultBookmarks = [
{
title: "文心一言",
url: "https://yiyan.baidu.com/",
},
{
title: "掘金",
url: "https://juejin.cn/",
},
];

defaultBookmarks.forEach((item) => {
chrome.bookmarks.create({
parentId: "1",
...item,
});
});
});
await page.goto('自己的本来要跳的首页')

3.3 如何使用已经打开的浏览器


const browserWSEndpoint = browser.wsEndpoint() // 获取本次打开的浏览器链接,留作下一次使用
// 保存下来, 比如直接存在一个变量map中,给它定义一个唯一的browserId,下一次好直接获取
browserMap.set(browserId, browserWSEndpoint)

...
// 再次打开新页面,要用到上一次打开的浏览器
const browser = puppeteer.connect({
...launchOptions, // 和自己首次打开浏览器的配置一样
browserWSEndpoint: browserMap.set(browserId)
})

这样就可以使用之前打开的浏览器打开网页了


3.4 如何把浏览器的信息显示在网页上


比如代理、userAgent、地区、浏览器名称等信息,先写个页面,然后轮询从localStorage直到获取信息为止。


// 浏览器代理信息页
await page.goto('浏览器信息页')
// 设置localStorage
await page.evaluate(
(values) => {
window.localStorage.setItem('browserInfo', values)
},
JSON.stringify(browserData)
)

page在打开页面后,并不会在页面中马上能获取到这里注入的browserInfo,可以通过轮询方式去扫描localStorage中是否存在我们注入的变量,这里举个react中的例子,在页面ready后去轮询处理


useEffect(() => {
let loopId = null
const clearLoop = () => {
loopId && clearTimeout(loopId)
}

// 轮询直到获取browserInfo
const loop = () => {
loopId = setTimeout(() => {
const localData = window.localStorage.getItem('browserInfo')
if (localData) {
Promise.resolve()
.then(() => {
setInfo(JSON.parse(localData))
})
.catch(() => {
message.error('获取浏览器信息失败')
})
} else {
loop()
}
}, 1500)
}

loop()

return () => {
clearLoop()
}
})

3.5 校验代理


一般的代理服务为了不让别人也能用都会加上账密校验,所以我们还需要在启动后,调用方法去校验


// 校验proxy
if (proxyData.proxyServer) {
await page.authenticate({
username: proxyData.proxyUser,
password: proxyData.proxyPwd
})
}

四、遇到了哪一些问题


4.1 mac下关闭浏览器关不掉


当我们点击左上角关闭浏览器按钮或者是关闭所有页面时候,底部的dock中依旧存在着,我们不希望像mac其他软件一样保留在dock中,不然下一次打开浏览器时候,会出现相同标识的浏览器,可以这么解决


// 每次页面关闭时候,查看浏览器是不是还有页面了,没有就关闭
browser.on('targetdestroyed', async () => {
const pages = await browser.pages()
if (!pages.length) {
await browser.close()
}
})

4.2 当我们之间关闭电脑屏幕时候,比如盖上电脑,再次打开时候,关闭不了浏览器


打上log,可以发现熄屏时候,会触发puppeteer定义的browser的disconnected事件,但是再次打开电脑时候浏览器是可以正常使用的,也就是说,puppeteer和我们打开的chromium断连了,所以我们需要在disconnected事件里再此尝试链接下chromium,如果不行才认为是浏览器被关闭了


browser.on('disconnected', () => {
const cacheData = browserMap.get(browserId)
puppeteer
.connect({
...launchOptions,
browserWSEndpoint: cacheData.browserWSEndpoint
})
.then((newBrowser) => {
browser = newBrowser
log.info(
'browser disconnected but browser is exist',
)
initEvent()
})
.catch((err) => {
log.info(
'browser disconnected success',
)
})
})

结语


puppeteer很强大,chromium也强大,就是那个官网文档啊,写的真是让人...,所以多问问AI吧


作者:柠檬阳光
来源:juejin.cn/post/7327642905245433891
收起阅读 »

防御性编程失败,我开始优化我写的多重 if-else 代码

前言 最近防御性编程比较火,不信邪的我在开发中我进行了尝试,然后我写下了如下的代码: public static void main(String[] args) { // do something if ("满足条...
继续阅读 »

前言



  • 最近防御性编程比较火,不信邪的我在开发中我进行了尝试,然后我写下了如下的代码:


    public static void main(String[] args) {
// do something
if ("满足条件A") {
// 查询权限
if ("是否具备权限A" && "是否具备权限B") {
// 查询配置
if ("配置是否开启"){
// do something
}
}
}
// do something
}


  • 不出意外我被逮捕了,组内另外一位同事对我的代码进行了 CodeReview,我的防御性编程编程没有幸运逃脱,被标记上了“多重 if-else ”需要进行优化,至此我的第一次防御性编程失败,开始了优化多重 if-else 之路,下面是我总结出的常用几种优化方式。


版本



  • Java8


几种常用的优化方式


提前使用 return 返回去除不必要的 else



  • 如果我们的代码块中需要使用 return 返回,我们应该尽可能早的使用 return 返回而不是使用 else

  • 优化前


    private static boolean extracted(boolean condition) {
if (condition) {
// do something
return false;
}else {
// do something
return true;
}
}


  • 优化后


    private static boolean extracted(boolean condition) {
if (condition) {
// do something
return false;
}

// do something
return true;
}

使用三目运算符



  • 一些简单的逻辑我们可以使用三目运算符替代 if-else ,这样可以让我们的代码更加简洁

  • 优化前


        int num = 0;
if (condition) {
num = 1;
} else {
num = 2;
}


  • 优化后


int num = condition ? 1 : 2;

使用枚举



  • 在某一些场景我们也可以使用枚举来优化多重 if-else 代码,使我们的代码更加简洁、具备更多的可读性和可维护性。

  • 优化前


        String OrderStatusDes;
if (orderStatus == 0) {
OrderStatusDes = "订单未支付";
} else if (orderStatus == 1) {
OrderStatusDes = "订单已支付";
} else if (orderStatus == 2) {
OrderStatusDes = "已发货";
} else {
throw new Exception("Invalid order status");
}


  • 优化后


public enum OrderStatusEnum {
UN_PAID(0, "订单未支付"),
PAIDED(1, "订单已支付"),
SENDED(2, "已发货"),
;

private final int code;
private final String desc;

public int getCode() {
return code;
}

public String getDesc() {
return desc;
}

OrderStatusEnum(int index, String desc) {
this.code = index;
this.desc = desc;
}

public static OrderStatusEnum getOrderStatusEnum(int orderStatusCode) {
for (OrderStatusEnum statusEnum : OrderStatusEnum.values()) {
if (statusEnum.getCode() == orderStatusCode) {
return statusEnum;
}
}
return null;
}
}


// 当然你需要根据业务场景对异常值做出合适的处理
OrderStatusEnum.getOrderStatusEnum(2)

抽取条件判断作为单独的方法



  • 当我们某个逻辑条件判断比较复杂时,可以考虑将判断条件抽离为单独的方法,这样可以使我们主流程逻辑更加清晰

  • 优化前


        // do something
if ("满足条件A" && "满足条件B") {
// 查询权限
if ("是否具备权限A" && "是否具备权限B") {
// do something
}
}
// do something


  • 优化后


    public static void main(String[] args) {
// do something
if (hasSomePermission()) {
// do something
}
// do something
}

private static boolean hasSomePermission() {
if (!"满足条件A" || !"满足条件B") {
return false;
}
// 查询权限
return "是否具备权限A" && "是否具备权限B";
}

有时候 switch 比 if-else 更加合适



  • 当条件为清晰的变量和枚举、或者单值匹配时,switch 比 if-else 更加合适,可以我们带好更好的可读性以及更好的性能 O(1)

  • 优化前


if (day == Day.MONDAY) {
// 处理星期一的逻辑
} else if (day == Day.TUESDAY) {
// 处理星期二的逻辑
} else if (day == Day.WEDNESDAY) {
// 处理星期三的逻辑
} else if (day == Day.THURSDAY) {
// 处理星期四的逻辑
} else if (day == Day.FRIDAY) {
// 处理星期五的逻辑
} else if (day == Day.SATURDAY) {
// 处理星期六的逻辑
} else if (day == Day.SUNDAY) {
// 处理星期日的逻辑
} else {
// 处理其他情况
}


  • 优化后


// 使用 switch 处理枚举类型
switch (day) {
case MONDAY:
// 处理星期一的逻辑
break;
case TUESDAY:
// 处理星期二的逻辑
break;
// ...
default:
// 处理其他情况
break;
}

策略模式 + 简单工厂模式



  • 前面我们介绍一些常规、比较简单的优化方法,但是在一些更加复杂的场景(比如多渠道对接、多方案实现等)我们可以结合一些场景的设计模式来实现让我们的代码更加优雅和可维护性,比如策略模式 + 简单工厂模式。

  • 优化前


    public static void main(String[] args) {
// 比如我们商场有多个通知渠道
// 我们需要根据不同的条件使用不同的通知渠道
if ("满足条件A") {
// 构建渠道A
// 通知
} else if ("满足条件B") {
// 构建渠道B
// 通知
} else {
// 构建渠道C
// 通知
}
}
// 上面的代码不仅维护起来麻烦同时可读性也比较差,我们可以使用策略模式 + 简单工厂模式


  • 优化后


import java.util.HashMap;
import java.util.Map;

// 定义通知渠道接口
interface NotificationChannel {
void notifyUser(String message);
}

// 实现具体的通知渠道A
class ChannelA implements NotificationChannel {
@Override
public void notifyUser(String message) {
System.out.println("通过渠道A发送通知:" + message);
}
}

// 实现具体的通知渠道B
class ChannelB implements NotificationChannel {
@Override
public void notifyUser(String message) {
System.out.println("通过渠道B发送通知:" + message);
}
}

// 实现具体的通知渠道C
class ChannelC implements NotificationChannel {
@Override
public void notifyUser(String message) {
System.out.println("通过渠道C发送通知:" + message);
}
}

// 通知渠道工厂
class NotificationChannelFactory {
private static final Mapextends NotificationChannel>> channelMap = new HashMap<>();

static {
channelMap.put("A", ChannelA.class);
channelMap.put("B", ChannelB.class);
channelMap.put("C", ChannelC.class);
}

public static NotificationChannel createChannel(String channelType) {
try {
Classextends NotificationChannel> channelClass = channelMap.get(channelType);
if (channelClass == null) {
throw new IllegalArgumentException("不支持的通知渠道类型");
}
return channelClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("无法创建通知渠道", e);
}
}
}

// 客户端代码
public class NotificationClient {
public static void main(String[] args) {
// 根据条件选择通知渠道类型
String channelType = "A";
// 使用简单工厂创建通知渠道
NotificationChannel channel = NotificationChannelFactory.createChannel(channelType);

// 执行通知
channel.notifyUser("这是一条通知消息");
}
}


  • 有时候我们还可以借助 Spring IOC 能力的自动实现策略类的导入,然后使用 getBean() 方法获取对应的策略类实例,可以根据我们的实际情况灵活选择。


如何优化开头的代码



  • 好了现在回到开头,如果是你会进行怎么优化,下面是我交出的答卷,大家也可以在评论区发表自己的看法,欢迎一起交流:


   public static void main(String[] args) {
// do something
if (isMeetCondition()) {
// 查询配置
// 此处查询配置的值需要在具体的任务中使用,所有并没抽离
if ("配置是否开启") {
// do something
}
}
// do something
}

/**
* 判断是否满足执行条件
*/

private static boolean isMeetCondition() {
if (!"满足条件A") {
return false;
}
// 查询权限
return "是否具备权限A" && "是否具备权限B";
}



作者:Lorin洛林
来源:juejin.cn/post/7325353198591672359
收起阅读 »

Flutter 首个真正可商用的 JSBridge 框架(完全兼容的 DSBridge for Flutter)

DSBridge for Flutter 在 Android 和 iOS 平台上做过 Hybrid 开发的同学基本都会知道 DSBridge,该框架目前最受欢迎的 JSBridge 框架之一,为了在 Flutter 侧实现原生 Hybrid 的能力,于是我们将...
继续阅读 »

DSBridge for Flutter


在 Android 和 iOS 平台上做过 Hybrid 开发的同学基本都会知道 DSBridge,该框架目前最受欢迎的 JSBridge 框架之一,为了在 Flutter 侧实现原生 Hybrid 的能力,于是我们将其适配到了Flutter 平台。


dsbridge.png



三端易用的现代跨平台 JavaScript bridge,通过它你可以在 JavaScript 和 Flutter 之间同步或异步的调用彼此的函数.



概述


DSBridge for Flutter 完全兼容 Android 和 iOS DSBridge 的 dsbridge.js。不像其他类似的框架无法实现JavaScript 调用 Dart 并同步返回结果,本框架完整支持同步调用和异步调用。dsbridge_flutter 是首个完整实现了 DSBridge 在原 Android 和 iOS 上的所有功能,因此可以实现将原来通过原生实现的 Webview 业务完全迁移到 Flutter 实现,即一套代码实现APP与H5的Hybrid开发。在现有使用了 dsbridge.js 的 Web 项目中无须修改任何代码即可使用 DSBridge for Flutter。


本框架目前支持Android 和 iOS 平台,即将支持纯鸿蒙平台(OpenHarmony & HarmonyOS Next),敬请期待!


DSBridge for Flutter 基于 Flutter官方的 webview_flutter


目前已发布到官方pub.dev:dsbridge_flutter


特性



  1. Android、iOS、JavaScript 三端易用,轻量且强大、安全且健壮。

  2. 同时支持同步调用和异步调用

  3. 支持以类的方式集中统一管理API

  4. 支持API命名空间

  5. 支持调试模式

  6. 支持 API 存在性检测

  7. 支持进度回调:一次调用,多次返回

  8. 支持 JavaScript 关闭页面事件回调

  9. 支持 JavaScript 模态对话框


安装



  1. 添加依赖


    dependencies:
    ...
    dsbridge_flutter: x.y.z



示例


请参考工程目录下的 example 包。运行 example 工程并查看示例交互。


如果要在你自己的项目中使用 dsBridge :


使用



  1. 新建一个Dart类,实现API


    import 'package:dsbridge_flutter/dsbridge_flutter.dart';

    class JsApi extends JavaScriptNamespaceInterface {
    @override
    void register() {
    registerFunction(testSyn);
    registerFunction(testAsyn);
    }

    /// for synchronous invocation
    String testSyn(dynamic msg) {
    return "$msg[syn call]";
    }

    /// for asynchronous invocation
    void testAsyn(dynamic msg, CompletionHandler handler) {
    handler.complete("$msg [ asyn call]");
    }
    }

    所有Dart APIs必须在register函数中使用registerFunction来注册。


  2. 添加API类实例到DWebViewController


    import 'package:dsbridge_flutter/dsbridge_flutter.dart';
    ...
    late final DWebViewController _controller;
    ...
    _controller.addJavaScriptObject(JsApi(), null);


  3. 在 JavaScript 中调用 Dart API ,并注册一个 JavaScript API 供原生调用.



    • 初始化 dsBridge


      //cdn
      //<script src="https://unpkg.com/dsbridge@3.1.3/dist/dsbridge.js"> </script>
      //npm
      //npm install dsbridge@3.1.3
      var dsBridge=require("dsbridge")


    • 调用 Dart API;以及注册一个 JavaScript API 供 Dart 调用.



      //同步调用
      var str=dsBridge.call("testSyn","testSyn");

      //异步调用
      dsBridge.call("testAsyn","testAsyn", function (v) {
      alert(v);
      })

      //注册 JavaScript API
      dsBridge.register('addValue',function(l,r){
      return l+r;
      })




  4. 在 Dart 中调用 JavaScript API


    import 'package:dsbridge_flutter/dsbridge_flutter.dart';
    ...
    late final DWebViewController _controller;
    ...
    _controller.callHandler('addValue', args: [3, 4],
    handler: (retValue) {
    print(retValue.toString());
    });



Dart API 签名


为了兼容Android&iOS,我们约定Dart API 签名,注意,如果API签名不合法,则不会被调用!签名如下:



  1. 同步API.


    any handler(dynamic msg)


    参数必须是 dynamic 类型,并且必须申明(如果不需要参数,申明后不适用即可)。返回值类型没有限制,可以是任意类型。


  2. 异步 API.


    void handler(dynamic arg, CompletionHandler handler)



命名空间


命名空间可以帮助你更好的管理API,这在API数量多的时候非常实用,比如在混合应用中。DSBridge支持你通过命名空间将API分类管理,并且命名空间支持多级的,不同级之间只需用'.' 分隔即可。


调试模式


在调试模式时,发生一些错误时,将会以弹窗形式提示,并且Dart API如果触发异常将不会被自动捕获,因为在调试阶段应该将问题暴露出来。


进度回调


通常情况下,调用一个方法结束后会返回一个结果,是一一对应的。但是有时会遇到一次调用需要多次返回的场景,比如在 JavaScript 中调用端上的一个下载文件功能,端上在下载过程中会多次通知 JavaScript 进度, 然后 JavaScript 将进度信息展示在h5页面上,这是一个典型的一次调用,多次返回的场景,如果使用其它 JavaScript bridge, 你将会发现要实现这个功能会比较麻烦,而 DSBridge 本身支持进度回调,你可以非常简单方便的实现一次调用需要多次返回的场景,下面我们实现一个倒计时的例子:


In Dart


void callProgress(dynamic args, CompletionHandler handler) {
var i = 10;
final timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (i == 0) {
timer.cancel();
handler.complete(0);
} else {
handler.setProgressData(i--);
}
});
}

In JavaScript


dsBridge.call("callProgress", function (value) {
document.getElementById("progress").innerText = value
})

完整的示例代码请参考example工程。


Javascript 对话框


DSBridge 已经实现了 JavaScript 的对话框函数(alert/confirm/prompt),如果你想自定义它们,通过DWebViewController设置相关回调函数即可。DSBridge实现的对话框默认设置是模态的,这会挂起UI线程。


API 列表


Dart API


在 Dart 中我们把实现了供 JavaScript 调用的 API 类的实例称为 Dart API object.


DWebViewController.addJavaScriptObject(JavaScriptNamespaceInterface? object, String? namespace)

Dart API object到DWebViewController,并为它指定一个命名空间。然后,在 JavaScript 中就可以通过bridge.call("namespace.api",...)来调用Dart API object中的原生API了。


如果命名空间是空(null或空字符串), 那么这个添加的Dart API object就没有命名空间。在 JavaScript 通过 bridge.call("api",...)调用。


示例:


In Dart


class JsEchoApi extends JavaScriptNamespaceInterface {
@override
void register() {
registerFunction(syn);
registerFunction(asyn);
}

dynamic syn(dynamic args) {
return args;
}

void asyn(dynamic args, CompletionHandler handler) {
handler.complete(args);
}
}
//namespace is "echo"
controller.addJavaScriptObject(JsEchoApi(), 'echo');

In JavaScript


// call echo.syn
var ret=dsBridge.call("echo.syn",{msg:" I am echoSyn call", tag:1})
alert(JSON.stringify(ret))
// call echo.asyn
dsBridge.call("echo.asyn",{msg:" I am echoAsyn call",tag:2},function (ret) {
alert(JSON.stringify(ret));
})

DWebViewController.removeJavaScriptObject(String namespace)

通过命名空间名称移除相应的Dart API object。


DWebViewController.callHandler(String method, {List? args, OnReturnValue? handler})

调用 JavaScript API。handlerName 为 JavaScript API 的名称,可以包含命名空间;参数以数组传递,args数组中的元素依次对应 JavaScript API的形参; handler 用于接收 JavaScript API 的返回值,注意:handler将在Dart主isolate中被执行


示例:


_controller.callHandler('append', args: ["I", "love", "you"],
handler: (retValue) {
print(retValue.toString());
});
/// call with namespace 'syn', More details to see the Demo project
_controller.callHandler('syn.getInfo', handler: (retValue) {
print(retValue.toString());
});

DWebViewController.javaScriptCloseWindowListener

当 JavaScript 中调用window.close时,DWebViewController 会触发此监听器,你可以自定义回调进行处理。


Example:


controller.javaScriptCloseWindowListener = () {
print('window.close called');
};

DWebViewController.hasJavaScriptMethod(String handlerName, OnReturnValue existCallback)

检测是否存在指定的 JavaScript API,handlerName可以包含命名空间.


示例:


_controller.hasJavaScriptMethod('addValue', (retValue) {
print(retValue.toString());
});

DWebViewController.dispose()

释放资源。在当前页面处于dispose状态时,你应该显式调用它。


JavaScript API


dsBridge

"dsBridge" 在初始化之后可用 .


dsBridge.call(method,[arg,callback])

同步或异步的调用Dart API。


method: Dart API 名称, 可以包含命名空间。


arg:传递给Dart API 的参数。只能传一个,如果需要多个参数时,可以合并成一个json对象参数。


callback(String returnValue): 处理Dart API的返回结果. 可选参数,只有异步调用时才需要提供.


dsBridge.register(methodName|namespace,function|synApiObject)

dsBridge.registerAsyn(methodName|namespace,function|asynApiObject)

注册同步/异步的 JavaScript API. 这两个方法都有两种调用形式:



  1. 注册一个普通的方法,如:


    In JavaScript


    dsBridge.register('addValue',function(l,r){
    return l+r;
    })
    dsBridge.registerAsyn('append',function(arg1,arg2,arg3,responseCallback){
    responseCallback(arg1+" "+arg2+" "+arg3);
    })

    In Dart


    _controller.callHandler('addValue', args: [3, 4],
    handler: (retValue) {
    print(retValue.toString());
    });

    _controller.callHandler('append', args: ["I", "love", "you"],
    handler: (retValue) {
    print(retValue.toString());
    });


  2. 注册一个对象,指定一个命名空间:


    In JavaScript


    //namespace test for synchronous calls
    dsBridge.register("test",{
    tag:"test",
    test1:function(){
    return this.tag+"1"
    },
    test2:function(){
    return this.tag+"2"
    }
    })

    //namespace test1 for asynchronous calls
    dsBridge.registerAsyn("test1",{
    tag:"test1",
    test1:function(responseCallback){
    return responseCallback(this.tag+"1")
    },
    test2:function(responseCallback){
    return responseCallback(this.tag+"2")
    }
    })


    因为 JavaScript 并不支持函数重载,所以不能在同一个 JavaScript 对象中定义同名的同步函数和异步函数



    In Dart


    _controller.callHandler('test.test1',
    handler: (retValue) {
    print(retValue.toString());
    });

    _controller.callHandler('test1.test1',
    handler: (retValue) {
    print(retValue.toString());
    });



dsBridge.hasNativeMethod(handlerName,[type])

检测Dart中是否存在名为handlerName的API, handlerName 可以包含命名空间.


type: 可选参数,["all"|"syn"|"asyn" ], 默认是 "all".


//检测是否存在一个名为'testAsyn'的API(无论同步还是异步)
dsBridge.hasNativeMethod('testAsyn')
//检测test命名空间下是否存在一个’testAsyn’的API
dsBridge.hasNativeMethod('test.testAsyn')
// 检测是否存在一个名为"testSyn"的异步API
dsBridge.hasNativeMethod('testSyn','asyn') //false

最后


如果你喜欢DSBridge for Flutter,欢迎点点star和like,以便更多的人知道它, 谢谢 !


作者:gtbluesky
来源:juejin.cn/post/7328753414724681728
收起阅读 »

【干货】一文掌握JavaScript检查对象空值的N种技巧!

在开发 JavaScript 应用程序时,经常需要检查对象是否为空。这是因为在处理和操作对象数据时,我们需要确保对象包含有效的值或属性。以下是一些常见情况,我们需要检查 JavaScript 对象是否为空:防止空引用错误:当我们尝试访问或使用一个空对象时,可能...
继续阅读 »

在开发 JavaScript 应用程序时,经常需要检查对象是否为空。这是因为在处理和操作对象数据时,我们需要确保对象包含有效的值或属性。以下是一些常见情况,我们需要检查 JavaScript 对象是否为空:

  1. 防止空引用错误:当我们尝试访问或使用一个空对象时,可能会导致空引用错误(如 TypeError: Cannot read property ‘x’ of null)。通过检查对象是否为空,我们可以避免这些错误的发生,并采取相应的处理措施。
  2. 数据验证和表单提交:在表单提交之前,通常需要验证用户输入的数据是否有效。如果对象为空,表示用户未提供必要的数据或未填写表单字段,我们可以显示错误消息或阻止表单提交。
  3. 条件逻辑和流程控制:根据对象是否为空,可以根据不同的条件逻辑执行不同的操作或采取不同的分支。例如,如果对象为空,可以执行备用的默认操作或返回默认值。
  4. 数据处理和转换:在处理对象数据之前,可能需要对其进行处理或转换。如果对象为空,可以提前终止或跳过数据处理逻辑,以避免不必要的计算或错误发生。
  5. 用户界面交互和显示:在用户界面中,可能需要根据对象的存在与否来显示或隐藏特定的界面元素、更改样式或呈现不同的内容。

通过检查 JavaScript 对象是否为空,可以增加应用程序的健壮性、提升用户体验,并避免潜在的错误和异常情况。因此,检查对象是否为空是编写高质量代码的重要部分。

在本文中,我们将讨论如何检查对象是否为空,其中包括 JavaScript 中检查对象是否为空的不同方法以及如何检查对象是否为空、未定义或为 null。

使用Object.keys()

使用Object.keys()方法可以检查对象是否为空。Object.keys(obj)返回一个包含给定对象所有可枚举属性的数组。
利用这个特性,我们可以通过检查返回的数组长度来确定对象是否为空。如果数组长度为0,则表示对象没有任何属性,即为空。
以下是一个示例代码:

javascriptCopy Codefunction isObjectEmpty(obj) {
return Object.keys(obj).length === 0;
}

// 测试对象是否为空
const obj1 = {};
console.log(isObjectEmpty(obj1)); // true

const obj2 = { name: "John", age: 25 };
console.log(isObjectEmpty(obj2)); // false

在上面的示例中,我们定义了一个isObjectEmpty()函数,它接受一个对象作为参数。函数内部使用Object.keys(obj)获取对象的所有可枚举属性,并检查返回的数组长度是否为0。根据返回结果,判断对象是否为空。

使用Object.values()

使用Object.values()方法来检查对象是否为空,Object.values(obj)方法返回一个包含给定对象所有可枚举属性值的数组。如果返回的数组长度为0,则表示对象没有任何属性值,即为空。

以下是使用Object.values()方法来检查对象是否为空的示例代码:

function isObjectEmpty(obj) {
return Object.values(obj).length === 0;
}

// 测试对象是否为空
const obj1 = {};
console.log(isObjectEmpty(obj1)); // true

const obj2 = { name: "John", age: 25 };
console.log(isObjectEmpty(obj2)); // false

在上面的示例中,我们定义了一个isObjectEmpty()函数,它接受一个对象作为参数。函数内部使用Object.values(obj)获取对象的所有可枚举属性值,并检查返回的数组长度是否为0。根据返回结果,判断对象是否为空。
请注意,Object.values()方法是ES2017(ES8)引入的新方法,因此在一些旧版本的JavaScript引擎中可能不被支持。在使用之前,请确保你的环境支持该方法或使用适当的polyfill来提供支持。

使用 for…in 循环

使用 for…in 循环方法是通过遍历对象的属性来判断对象是否为空。以下是一个示例代码:

javascriptCopy Codefunction isObjectEmpty(obj) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
return false; // 只要有一个属性存在,就返回false表示不为空
}
}
return true; // 如果遍历完所有属性后仍然没有返回false,表示对象为空
}

// 测试对象是否为空
const obj1 = {};
console.log(isObjectEmpty(obj1)); // true

const obj2 = { name: "John", age: 25 };
console.log(isObjectEmpty(obj2)); // false

在上面的示例中,isObjectEmpty()函数接受一个对象作为参数。函数内部使用 for…in 循环遍历对象的属性,如果发现任何属性,则返回false表示对象不为空;如果循环结束后仍然没有返回false,则表示对象为空,并返回true。
虽然使用 for…in 循环可以达到同样的目的,但相比起使用 Object.keys() 或 Object.values() 方法,它的实现稍显繁琐。因此,通常情况下,推荐使用 Object.keys() 或 Object.values() 方法来检查对象是否为空,因为它们提供了更简洁和直观的方式。

使用 Object.entries()

Object.entries()方法返回一个给定对象自身可枚举属性的键值对数组。如果返回的数组长度为0,则表示对象没有任何属性,即为空。
以下是使用Object.entries()方法来检查对象是否为空的示例代码:

function isObjectEmpty(obj) {
return Object.entries(obj).length === 0;
}

// 测试对象是否为空
const obj1 = {};
console.log(isObjectEmpty(obj1)); // true

const obj2 = { name: "John", age: 25 };
console.log(isObjectEmpty(obj2)); // false

在上面的示例中,isObjectEmpty()函数接受一个对象作为参数。函数内部使用Object.entries(obj)获取对象的键值对数组,并检查返回的数组长度是否为0。如果数组长度为0,则表示对象没有任何属性,即为空。
请注意,Object.entries()方法是ES2017(ES8)引入的新方法,因此在一些旧版本的JavaScript引擎中可能不被支持。在使用之前,请确保你的环境支持该方法或使用适当的polyfill来提供支持。

使用 JSON.stringify()

使用 JSON.stringify() 方法来检查对象是否为空的方法是将对象转换为 JSON 字符串,然后检查字符串的长度是否为 2。当对象为空时,转换后的字符串为 “{}”,长度为 2。如果对象不为空,则转换后的字符串长度会大于 2。
以下是使用 JSON.stringify() 方法来检查对象是否为空的示例代码:

function isObjectEmpty(obj) {
return JSON.stringify(obj) === "{}";
}

// 测试对象是否为空
const obj1 = {};
console.log(isObjectEmpty(obj1)); // true

const obj2 = { name: "John", age: 25 };
console.log(isObjectEmpty(obj2)); // false

在上述示例中,isObjectEmpty() 函数接受一个对象作为参数。函数内部使用 JSON.stringify(obj) 将对象转换为 JSON 字符串,然后将转换后的字符串与 “{}” 进行比较。如果相等,则表示对象为空。
需要注意的是,这种方式只适用于纯粹的对象,并且不包含任何非原始类型属性(如函数、undefined 等)。如果对象中包含了非原始类型的属性,那么转换后的 JSON 字符串可能不为空,即使对象实际上是空的。

E6使用Object.getOwnPropertyNames()

在ES6中,你可以使用Object.getOwnPropertyNames()方法来检查对象是否为空,但需要注意的是,该方法返回一个数组,其包含给定对象中所有自有属性(包括不可枚举属性,但不包括使用 symbol 值作为名称的属性)。
以下是使用Object.getOwnPropertyNames()方法来检查对象是否为空的示例代码:

function isObjectEmpty(obj) {
return Object.getOwnPropertyNames(obj).length === 0;
}

// 测试对象是否为空
const obj1 = {};
console.log(isObjectEmpty(obj1)); // true

const obj2 = { name: "John", age: 25 };
console.log(isObjectEmpty(obj2)); // false

在上面的示例中,isObjectEmpty()函数接受一个对象作为参数。函数内部使用Object.getOwnPropertyNames(obj)获取对象的所有属性名,并检查返回的数组长度是否为0。如果数组长度为0,则表示对象没有任何属性,即为空。
请注意,Object.getOwnPropertyNames()方法返回的数组只包含对象自身的属性,不包括继承的属性。如果你需要检查继承的属性,请使用for…in循环或其他方法。同样,Object.getOwnPropertyNames()方法在ES5中引入,因此在一些旧版本的JavaScript引擎中可能不被支持。

ES6使用Object.getOwnPropertySymbols()方法

在ES6中,可以使用Object.getOwnPropertySymbols()方法来检查对象是否为空。该方法返回一个数组,其中包含了给定对象自身的所有符号属性。
以下是使用Object.getOwnPropertySymbols()方法来检查对象是否为空的示例代码:

function isObjectEmpty(obj) {
const symbols = Object.getOwnPropertySymbols(obj);
const hasSymbols = symbols.length > 0;
return !hasSymbols;
}

// 测试对象是否为空
const obj1 = {};
console.log(isObjectEmpty(obj1)); // true

const symbol = Symbol("key");
const obj2 = { [symbol]: "value" };
console.log(isObjectEmpty(obj2)); // false

在上面的示例中,isObjectEmpty()函数接受一个对象作为参数。函数内部使用Object.getOwnPropertySymbols(obj)获取对象的所有符号属性,并将它们存储在symbols数组中。然后,通过检查symbols数组的长度是否大于0来判断对象是否具有符号属性。如果symbols数组的长度为0,则表示对象没有任何符号属性,即为空。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试【云端源想】!课程视频、知识库、微实战、云实验室、一对一咨询……你想要的全部学习资源这里都有,重点是现在还是免费的!点这里即可免费学习!

注意,Object.getOwnPropertySymbols()方法只返回对象自身的符号属性,不包括其他类型的属性,例如字符串属性。如果你想同时检查对象的字符串属性和符号属性,可以结合使用Object.getOwnPropertyNames()和Object.getOwnPropertySymbols()方法。

ES6使用Reflect.ownKeys()

在ES6中,你可以使用Reflect.ownKeys()方法来检查对象是否为空。Reflect.ownKeys()返回一个包含了指定对象自身所有属性(包括字符串键和符号键)的数组。
以下是使用Reflect.ownKeys()方法来检查对象是否为空的示例代码:

function isObjectEmpty(obj) {
return Reflect.ownKeys(obj).length === 0;
}

// 测试对象是否为空
const obj1 = {};
console.log(isObjectEmpty(obj1)); // true

const symbol = Symbol("key");
const obj2 = { [symbol]: "value" };
console.log(isObjectEmpty(obj2)); // false

在上面的示例中,isObjectEmpty()函数接受一个对象作为参数。函数内部使用Reflect.ownKeys(obj)获取对象的所有自身属性名(包括字符串键和符号键),并检查返回的数组长度是否为0。如果数组长度为0,则表示对象没有任何属性,即为空。
Reflect.ownKeys()方法提供了一种统一的方式来获取对象的所有键,包括字符串键和符号键。因此,使用Reflect.ownKeys()方法可以更全面地检查对象是否为空。

使用lodash库的isEmpty()函数

如果您使用了lodash库,可以使用其提供的isEmpty()函数来直接判断对象是否为空。
以下是使用 Lodash 的 isEmpty() 函数进行对象空检查的示例代码:

// 导入Lodash库
const _ = require('lodash');

// 检查对象是否为空
const obj1 = {};
console.log(_.isEmpty(obj1)); // true

const obj2 = { name: "John", age: 25 };
console.log(_.isEmpty(obj2)); // false

在上述示例中,_.isEmpty() 函数接受一个对象作为参数,并返回一个布尔值表示对象是否为空。如果对象为空,则返回 true;否则返回 false。
使用 Lodash 的 isEmpty() 函数可以更方便地进行对象空检查,同时处理了各种情况,包括非原始类型的属性、数组、字符串等。

使用jQuery库的$.isEmptyObject()函数

要使用 jQuery 库中的 $.isEmptyObject() 函数来检查 JavaScript 对象是否为空,首先确保已经安装了 jQuery 库,并将其导入到你的项目中。
以下是使用 jQuery 的 $.isEmptyObject() 函数进行对象空检查的示例代码:

// 导入jQuery库
const $ = require('jquery');

// 检查对象是否为空
const obj1 = {};
console.log($.isEmptyObject(obj1)); // true

const obj2 = { name: "John", age: 25 };
console.log($.isEmptyObject(obj2)); // false

在上述示例中,$.isEmptyObject() 函数接受一个对象作为参数,并返回一个布尔值表示对象是否为空。如果对象为空,则返回 true;否则返回 false。
使用 jQuery 的 $.isEmptyObject() 函数可以更方便地进行对象空检查,同时处理了各种情况,包括非原始类型的属性、数组、字符串等。

检查对象是否为空、未定义或为 null

要同时检查对象是否为空、未定义或为 null,你可以使用以下函数来进行判断:

function isObjectEmptyOrNull(obj) {
return obj === undefined || obj === null || Object.getOwnPropertyNames(obj).length === 0;
}

在上述代码中,isObjectEmptyOrNull函数接收一个对象作为参数。它首先检查对象是否为 undefined 或者 null,如果是,则直接返回 true 表示对象为空或者未定义。如果对象不是 undefined 或者 null,则使用 Object.getOwnPropertyNames() 方法获取对象的所有自身属性名,然后判断属性名数组的长度是否为 0。如果属性名数组长度为 0,则表示对象没有任何属性,即为空。
下面是一个示例用法:

const obj1 = {};
console.log(isObjectEmptyOrNull(obj1)); // true

const obj2 = null;
console.log(isObjectEmptyOrNull(obj2)); // true

const obj3 = { name: "John", age: 25 };
console.log(isObjectEmptyOrNull(obj3)); // false

const obj4 = undefined;
console.log(isObjectEmptyOrNull(obj4)); // true

总结和比较

在本文中,我们介绍了多种方法来检查 JavaScript 对象是否为空。下面是这些方法的优缺点总结:

  • 使用 Object.keys() 方法

优点:简单易用,不需要依赖第三方库。
缺点:无法处理非原始类型的属性,如函数、undefined 等。

  • Object.values()

优点:能够将对象的属性值组成一个数组,可以通过判断该数组的长度来判断对象是否为空。
缺点:无法直接判断对象是否为空,只提供了属性值的数组。

  • 使用 for…in 循环遍历对象

优点:可以处理非原始类型的属性。
缺点:代码较为冗长,需要手动判断每个属性是否为对象自身属性。

  • 使用 JSON.stringify() 方法

优点:可以处理非原始类型的属性,并且转换后的字符串长度为 2 表示对象为空。
缺点:当对象包含循环引用时,将抛出异常。

  • Object.entries()

优点:能够将对象的键值对组成一个数组,可以通过判断该数组的长度来判断对象是否为空。
缺点:同样无法直接判断对象是否为空,只提供了键值对数组。

  • Object.getOwnPropertyNames()

优点:能够返回对象自身的所有属性名组成的数组,包括可枚举和不可枚举的属性,可以通过判断该数组的长度来判断对象是否为空。
缺点:同样无法直接判断对象是否为空,只提供了属性名数组。

  • Object.getOwnPropertySymbols()

优点:能够返回对象自身的所有 Symbol 类型的属性组成的数组,可以通过判断该数组的长度来判断对象是否为空。
缺点:仅针对 Symbol 类型的属性,无法判断其他类型的属性是否为空。

  • Reflect.ownKeys()

优点:能够返回对象自身的所有属性(包括字符串键和 Symbol 键)组成的数组,包括可枚举和不可枚举的属性,可以通过判断该数组的长度来判断对象是否为空。
缺点:同样无法直接判断对象是否为空,只提供了所有键的数组。

  • 使用 Lodash 库的 isEmpty() 函数

优点:可以处理各种情况,包括非原始类型的属性、数组、字符串等。
缺点:需要依赖第三方库。

  • 使用 jQuery 库的 $.isEmptyObject() 函数

优点:可以处理各种情况,包括非原始类型的属性、数组、字符串等。
缺点:需要依赖第三方库。

总体来说, 这些方法都提供了一种间接判断对象是否为空的方式,即通过获取对象的属性、属性值或键值对的数组,并判断该数组的长度。然而,它们并不能直接告诉我们对象是否为空,因为它们只提供了属性、属性值或键值对的信息。因此,在使用这些方法判断对象是否为空时,需要结合其他判断条件来综合考虑。

收起阅读 »

JS逐页转pdf文件为图片格式

web
背景 年前的时候,开发一个电子杂志项目,功能需求是通过上传pdf文件,将其转为图片格式,所以杂志的内容其实就是一张张图片 不过当时技术要求用后端实现,所以使用的是PHP实现该功能。项目完成后,寻思着在前端是否也能实现pdf转图片的功能。一番研究后,果真可行。以...
继续阅读 »

背景


年前的时候,开发一个电子杂志项目,功能需求是通过上传pdf文件,将其转为图片格式,所以杂志的内容其实就是一张张图片


不过当时技术要求用后端实现,所以使用的是PHP实现该功能。项目完成后,寻思着在前端是否也能实现pdf转图片的功能。一番研究后,果真可行。以下就分享如何通过前端js将pdf文件转为图片格式,并且支持翻页预览、以及图片打包下载


效果预览


图片

所需工具



  1. pdf.js(负责API解析,可将pdf文件渲染成canvas实现预览)

  2. pdf.worker.js(负责核心解析)

  3. jszip.js(将图片打包成生成.zip文件)

  4. Filesaver.js(保存下载zip文件)


工具下载


一、pdf.js及pdf.worker.js下载地址:


mozilla.github.io/pdf.js/gett…


1.选择稳定版下载


图片


2.解压后将bulid中的pdf.js及pdf.worker.js拷贝到项目中


图片


二、jszip.js及Filesaver.js下载地址:

stuk.github.io/jszip/


1.点击download.JSZip


图片


2.解压后将dist文件夹下的jszip.js文件以及vendor文件夹下的FileSaver.js文件拷贝到项目中


图片


至此,所需工具已齐全。以下直接附上项目完整代码(代码可直接复制使用,查看效果。 对应的文件需自行下载引入)


源代码: 嫌麻烦的小伙伴可以直接在公众号后回复: pdf转图片


代码实现


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>PDF文件转图片</title>
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
<script type="text/javascript" src="js/pdf.js"></script>
<script type="text/javascript" src="js/pdf.worker.js"></script>
<script type="text/javascript" src="js/jszip.js"></script>
<script type="text/javascript" src="js/FileSaver.js"></script>
<style type="text/css">

button {
width: 120px;
height: 30px;
background: none;
border: 1px solid #b1afaf;
border-radius: 5px;
font-size: 12px;
font-weight: 1000;
color: #384240;
cursor: pointer;
outline: none;
margin: 0 0.5%
}

button:hover {
background: #ccc;
}

#container {
width: 600px;
height: 780px;
margin-top: 1%;
border-radius: 2px;
border: 2px solid #a29b9b;
}

.pdfInfos {
margin: 0 2%;
}
</style>
</head>

<body>

<div style="margin-top:1%">
<button id="prevpage">上一页</button>
<button id="nextpage">下一页</button>
<button id="exportImg">导出图片</button>
<button onclick="choosePdf()">选择一个pdf文件</button>
<input style="display:none" id='chooseFile' type='file' accept="application/pdf">
</div>

<div style="margin-top:1%">
<span class="pdfInfos">页码:<span id="currentPages"></span><span id="totalPages"></span></span>
<span class="pdfInfos">文件名:<span id="fileName"></span></span>
<span class="pdfInfos">文件大小:<span id="fileSize"></span></span>
</div>

<div style="position: relative;">
<div id="container"></div>
<img id="imgloading" style="position: absolute;top: 20%;left: 2%;display:none" src="loading.gif">
</div>

</body>


<script>

var currentPages,totalPages //声明一个当前页码及总页数变量
var scale = 2; //设置缩放比例,越大生成图片越清晰

$('#chooseFile').change(function() {
var pdfFilePath = $('#chooseFile').val();
if(pdfFilePath) {

$("#imgloading").css('display','block');
$("#container").empty(); //清空上一PDF文件展示图

currentPages=1; //重置当前页数
totalPages=0; //重置总页数

var filesdata = $('#chooseFile')[0].files; //jquery获取到文件 返回属性的值
var fileSize = filesdata[0].size; //文件大小
var mb;

if(fileSize) {
mb = fileSize / 1048576;
if(mb > 10) {
alert("文件大小不能>10M");
return;
}
}

$("#fileName").text(filesdata[0].name);
$("#fileSize").text(mb.toFixed(2) + "Mb");

var reader = new FileReader();
reader.readAsDataURL(filesdata[0]); //将文件读取为 DataURL
reader.onload = function(e) { //文件读取成功完成时触发

pdfjsLib.getDocument(this.result).then(function(pdf) { //调用pdf.js获取文件
if(pdf) {
totalPages = pdf.numPages; //获取pdf文件总页数
$("#currentPages").text("1/");
$("#totalPages").text(totalPages);

//遍历动态创建canvas
for(var i = 1; i <= totalPages; i++) {
var canvas = document.createElement('canvas');
canvas.id = "pageNum" + i;
$("#container").append(canvas);
var context = canvas.getContext('2d');
renderImg(pdf,i,context);
}

}
});

};
}
});

//渲染生成图片
function renderImg(pdfFile,pageNumber,canvasContext) {
pdfFile.getPage(pageNumber).then(function(page) { //逐页解析PDF
var viewport = page.getViewport(scale); // 页面缩放比例
var newcanvas = canvasContext.canvas;

//设置canvas真实宽高
newcanvas.width = viewport.width;
newcanvas.height = viewport.height;

//设置canvas在浏览中宽高
newcanvas.style.width = "100%";
newcanvas.style.height = "100%";

//默认显示第一页,其他页隐藏
if (pageNumber!=1) {
newcanvas.style.display = "none";
}

var renderContext = {
canvasContext: canvasContext,
viewport: viewport
};

page.render(renderContext); //渲染生成
});

$("#imgloading").css('display','none');

return;
};

//上一页
$("#prevpage").click(function(){

if (!currentPages||currentPages <= 1) {
return;
}

nowpage=currentPages;
currentPages--;

$("#currentPages").text(currentPages+"/");

var prevcanvas = document.getElementById("pageNum"+currentPages);
var currentcanvas = document.getElementById("pageNum"+nowpage);
currentcanvas.style.display = "none";
prevcanvas.style.display = "block";

})

//下一页
$("#nextpage").click(function(){

if (!currentPages||currentPages>=totalPages) {
return;
}

nowpage=currentPages;
currentPages++;

$("#currentPages").text(currentPages+"/");

var nextcanvas = document.getElementById("pageNum"+currentPages);
var currentcanvas = document.getElementById("pageNum"+nowpage);
currentcanvas.style.display = "none";
nextcanvas.style.display = "block";

})

//导出图片
$("#exportImg").click(function() {

if (!$('#chooseFile').val()) {
alert('请先上传pdf文件')
return false;
}

$("#imgloading").css('display','block');

var zip = new JSZip(); //创建一个JSZip实例
var images = zip.folder("images"); //创建一个文件夹用来存放图片

//遍历canvas,将其生成图片放进文件夹images中
$("canvas").each(function(index, ele) {
var canvas = document.getElementById("pageNum" + (index + 1));

//将图片放进文件夹images中
//参数1为图片名称,参数2为图片数据(格式为base64,需去除base64前缀 data:image/png;base64)
images.file("" + (index + 1) + ".png", splitBase64(canvas.toDataURL("image/png", 1.0)), {
base64: true
});

})

//打包下载
zip.generateAsync({
type: "blob"
}).then(function(content) {
saveAs(content, "picture.zip"); //saveAs依赖的js文件是FileSaver.js
$("#imgloading").css('display','none');
});

});

//截取base64前缀
function splitBase64(dataurl) {
var arr = dataurl.split(',')
return arr[1]
}

function choosePdf(){
$("#chooseFile").click()
}
</script>
</html>

项目实现原理分析



  1. 首先利用pdf.js将上传的pdf文件转化成canvas

  2. 然后使用jszip.js将canvas打包图片生成.zip文件

  3. 最后使用Filesaver.js将zip文件保存下载


项目注意要点



  1. 由于pdf文件是通过上传的,因此需要通过js的FileReader()对象将其读取为DataURL,pdf.js文件才可读取渲染

  2. JSZip对象的.file()函数中第二个参数传入的是base64格式图片,但是要去掉base64前缀标识


作者:程序员Winn
来源:juejin.cn/post/7238442926334918711
收起阅读 »

火烧眉毛,我是如何在周六删了公司的数据库

这本是一个安静的星期六。 我收到了支持团队的一条消息,说我们一个客户遇到了问题。我认为这个问题很重要,值得开始调试。15 分钟后,我明白了问题所在 - 在数据库中有一些损坏的订单需要删除。 听起来小菜一碟。 事故还原 如果你不给创业公司打工,请不要嘲笑我 😅 ...
继续阅读 »


这本是一个安静的星期六。


我收到了支持团队的一条消息,说我们一个客户遇到了问题。我认为这个问题很重要,值得开始调试。15 分钟后,我明白了问题所在 - 在数据库中有一些损坏的订单需要删除。


听起来小菜一碟。


事故还原


如果你不给创业公司打工,请不要嘲笑我 😅


有几百个订单需要删除,所以我决定不手动操作,而是编写一个简单的 SQL 查询语句(警告 🚩)


实际上比这复杂一些,但这里简化一下:


UPDATE orders
SET is_deleted = true

WHERE id in (1, 2, 3)

你大概已经猜到这场灾难的规模了...


我按下了 CTRL + Enter 并运行了命令。当它花费超过一秒钟时,我明白发生了什么。我的客户端 DBeaver 看到空的第三行,并忽略了第四行。


是的,我删除了数据库中所有的订单 😢


我整个人都不好了。


恢复


深吸一口气后,我知道我必须快速行动起来。不能犯更多错误浪费时间了。


恢复工作做得很好。



  1. 停止系统 - 约 5 分钟

  2. 创建变更前数据库(幸运的是我们有 PITR)的克隆 - 约 20 分钟

  3. 在等待期间给我的老板打电话 😨

  4. 根据克隆更新生产数据库的信息* - 约 15 分钟

  5. 启动系统 - 约 5 分钟


*我决定不还原整个数据库,因为无法停止所有系统,因为我们有多个独立的系统。我不想在恢复过程中丢失所做的更改。我们用 GCP 提供的托管 PostgreSQL,所以我从更新之前创建了一个新的克隆。然后,我只导出了克隆中的 idis_deleted 列,并将结果导入到生产数据库中。之后,就是简单的 update + select 语句。


所以显然本可以很容易避免这 45 分钟的停机时间...


发生了什么?


这可能听起来像是一个你永远不会犯的愚蠢错误(甚至在大公司中,根本不能犯)。确实。问题不在于错误的 SQL 语句。**一个小小的人为失误从来都不是真正的问题。**我运行那个命令只是整个失败链条的终点。



  1. 为什么要在周末处理生产环境?在这种情况下,事情并没有那么紧急。没有人要求我立即修复它。我本可以等到星期一再处理。

  2. 谁会在生产数据库上更改而不先在 QA 环境上运行一下呢?

  3. 为什么我手动编辑了数据库而不是通过调用 API?

  4. 如果没有 API,为什么我没打电话给队友,在如此敏感的操作上进行双重检查?

  5. **最糟糕的是,为什么我没使用事务?**其实只要用了 Begin,万一出错时使用 Rollback 就可以了。


错误一层层叠加,其中任何一个被避免了 - 整件事就不会发生。大多数问题答案都很简单:我太自信了。
不过还好通过有章法的恢复程序,阻止了连锁反应。想象一下如果无法将数据库恢复到正确状态会发生什么灾难……


这与切尔诺贝利有什么关系?


几个月前,我阅读了「切尔诺贝利:一部悲剧史」。那里发生的一系列错误使我想起了那个被诅咒的周末(并不是要低估或与切尔诺贝利灾难相比较)。



  1. RBMK 反应堆存在根本技术问题。

  2. 这个问题没有得到恰当传达。之前有涉及该问题的事件,但切尔诺贝利团队对此并不熟悉。

  3. 在安全检查期间,团队没有按程序操作。

  4. 爆炸后,苏联政府试图掩盖事实,从而大大加剧了损害程度。


谁应该负责?


反应堆设计师?其他电厂团队未能传达他们遇到的问题?切尔诺贝利团队?苏联政府?


所有人都有责任。灾难从来不是由单一错误引起的,而是由一连串错误造成的。我们的工作就是尽早打断这条链条,并做到最好。


后续


我对周一与老板的谈话本没有什么期待。


但他让我惊讶:「确保不再发生这种情况。但是我更喜欢这样 - 你犯了错误是因为你专注并且喜欢快速行动。做得越多,砸得越多。」


那正是我需要听到的。如果以过于「亲切」的方式说:没关系,别担心,谢谢你修复它!我反而会感觉虚伪。另一方面,我已经感觉很糟糕了,所以没有必要进一步吐槽我。


从那时起:



  • 我们减少了对数据库直接访问的需求,并创建相关的 API。

  • 我总是先在 QA 上运行查询(显而易见吧?没有比灾难更能教训人了)。

  • 我与产品经理商量,了解真正紧急和可以等待的事项。

  • 任何对生产环境进行更删改操作都需要两个人来完成。这实际上防止了其他错误!

  • 我开始使用事务处理机制。


可以应用在你的团队中的经验教训


事发后,我和团队详细分享了过程,没有隐瞒任何事情,也没有淡化我的过错。
在责备他人和不追究责任之间有一个微妙的平衡。当你犯错误时,这是一个传递正确信息的好机会。


如果你道歉 1000 次,他们会认为你期望当事情发生在他们身上时,他们也需要给出同样的回应。


如果你一笑了之,并忽视其影响,他们会认为这是可以接受的。


如果你承担责任、学习并改进自己 - 他们也会以同样的方式行事。


file


总结一下



  • 鼓励行动派,关心客户,并解决问题。这就是初创企业成功的方式。

  • 当犯错时,要追究责任。一起理解如何避免这种情况发生。

  • 没必要落井下石。有些人需要更多的责任感,而有些人则需要更多的鼓励。我倾向于以鼓励为主。


顺便说一句,如果团队采用了 Bytebase 的话,这个事故是大概率可以被避免的,因为 Bytebase 有好几道防线:



  1. 用户不能随意通过使用 DBeaver 这样的本地客户端直连数据库,而必须通过 Bytebase 提交变更工单。

  2. 变更工单的 SQL 会经过自动审查,如果影响范围有异常,会有提示。

  3. 变更工单只有通过人工审核后才能发布。

作者:Bytebase
来源:juejin.cn/post/7322156771614507059
收起阅读 »

JSON.parse记录一次线上bug排查

web
最近项目中有一个匪夷所思的问题,业务在使用的时候,偶发性的会白屏,经常下班的时候骚扰我们,开发苦不堪言,经过长达一周的排查,仍然没有查到bug的存在,最终尝试通过添加埋点日志,记录关键信息。 现状 首先讲述一下现状,首先业务进入后,页面可以认为有两个按钮 跳...
继续阅读 »

最近项目中有一个匪夷所思的问题,业务在使用的时候,偶发性的会白屏,经常下班的时候骚扰我们,开发苦不堪言,经过长达一周的排查,仍然没有查到bug的存在,最终尝试通过添加埋点日志,记录关键信息。


现状


首先讲述一下现状,首先业务进入后,页面可以认为有两个按钮



  • 跳转共享链接

  • 打开表单弹窗按钮,点击后展示表单。


image-20240124132744181


操作顺序是,页面加载后,先点击跳转共享链接,看完链接后再返回点击表单弹窗。



里面有两个重要的时间节点,一个是跳转链接之前,一个是返回到当前页面。




  • 跳转链接之前



    • 需要存储接口数据,接口数据包含了表单的数据



  • 返回当前页面



    • 请求接口数据



      • 本地缓存无,直接使用接口数据

      • 本地缓存有,缓存和接口数据合并,接口数据优先






image-20240124132901079


返回页面的时候,点击表单弹窗


正常上来说弹窗能够正常显示,但是线上环境再点击 展示弹窗的按钮导致白屏了。整个流程如下


image-20240124133213958


初步判断是整合缓存和接口数据问题,于是需要给页面添加两个埋点



  • 页面报错异常时上报

  • 点击打开表单的时,上报缓存数据和聚合之后的数据。



    • 为什么不上报接口数据呢?因为当时修复bug比较紧急,观察代码发现接口直接返回的数据没有在公共变量中存储,如果需要存储改动较大,还有就是接口数据也可以从后端日志去排查




页面报错异常上报


异常上报的方法有很多,通常使用一个gif图片,地址为get的请求地址+上报信息,具体的可以自行百度,此处简单叙述下


使用图片是因为加载资源里面img优先级比较低,不会阻塞其他资源,而且图片请求不会跨域,用gif是因为对比图片类型他是比较小的


//utils/utils.js
/**
* 异常上报方法
* 希望抽离出来同步异常类和异步异常类
*/

function uploadError() {
 //上报处理参数
 const upload = errObj =>{
   const logUrl = 'https://xxx.xxx.com/log.gif'; // 上报接口
   //将obj拼接成url
   const queryStr = Object.entries(errObj)
      .map(([key, value]) => `${key}=${value}`)
      .join('&');
     const oImg = new Image();
     oImg.src = logUrl + '?' + encodeURIComponent(queryStr);
}
 //同步方法
 function handleError(e) {
   try {
     let baseInfo = localStorage.getItem('base_info'); // 域账户
     let masterName = baseInfo ? JSON.parse(baseInfo)?.master_name : ''; // 域账户
     let errObj = {
       masterName: masterName,//域账户
       url: window.location.href,//报错的路由,利于排查
       reason: JSON.stringify({
         message: e?.error?.message, //报错信息
         stack: e?.error?.stack,//调用栈
      }),
       message: e?.message, //报错信息
    };
     upload(errObj)
     console.log('error', errObj);
  } catch (err) {
     console.log('error', err);
  }
}
 window.addEventListener('error', handleError);//调用监听
}

//app.js
//异常上报方法 开发环境禁止上报
if(!['dev'].includes(process.env.BUILD_ENV)){
 uploadError()
}

点击弹窗的异常上报


//打开弹窗的操作  
const open = () => {
   setShow(!show);//控制表单的展示隐藏
   if(!show){
     const logUrl = 'https://xxx.xxx.com/log.gif'; // 上报接口
     const oImg = new Image();
     let initFormVal = localStorage.getItem('initFormVal' + query?.id);
     oImg.src = logUrl + '?' + encodeURIComponent(`initFormVal=${initFormVal}&integratedData=${JSON.stringify(integratedData)}`);
  }
};
//initFormVal为缓存中的数据 integratedData为整合后的数据

发现问题原因


通过添加以上异常上报,业务员进行操作时,又出现了白屏,此时根据业务员token与上报关键字与时间查到了相关日志,其中日志中记录的是


https://xxx.xxx.com/log.gif?initFormVal=&integratedData=null

integratedData是后端接口数据和缓存的融合呀!通过查日志发现当时后端确确实实返回正常的响应了,不可能为null,同时还有一个疑问浮出水面,为什么initFormVal没有值,而不是null


正常来说如果initFormVal从json中取值时,取不到应该默认就是null,此处为'',只说明一个问题,缓存的时候给他赋值了


那么问题大致可以定位到以下两个操作节点



  • 缓存时

  • 返回页面后,缓存和接口数据融合时


//缓存时操作  
const getFormValues = () => {
   let formVal = childRef?.current?.getFormVal() || '';
localStorage.setItem('initFormVal' + query.id, JSON.stringify(formVal));
};

缓存时,如果子节点获取不到,那么childRef?.current?.getFormVal()就为undefind,又由于使用了或运算符,那么此时存储的是'',那么取这个暂时看也没问题呀,然后也写入了缓存



更严格来讲,应该先判断formVal是否存在然后再去缓存,没有就不缓存。



再看一下返回页面,数据融合的代码


const getDataFn = url => {
   dispatch({
     type: url,
     payload: { id: query.id },
     callback: res => {
       if (res.ret === 1) {
         let initFormVal = localStorage.getItem('initFormVal' + query?.id);
         console.log('initFormVal', JSON.parse(initFormVal));
         let cacheFormVal = {};
         
         if (initFormVal) {
           //initFormVal赋值给cacheFormVal,此处省略
        }
         setPricingInfo({
           ...cacheFormVal,
           ...res.data
        });
      }  

发现有一个console.log(),JSON.parse('')会是什么?报错,果然,查异常上报日志的时候,也查到这个错误,真是一失足成千古恨,当时只是为了方便查看,打印了一下缓存数据,没想到是这个地方出现的问题 Uncaught SyntaxError: Unexpected end of JSON input


image-20240124142222982


JSON.parse


那问题来了 json.parse什么情况会报错呢?通过查阅MDN


image-20240124143007732


那么,什么是规范的JSON格式呢?我们此处再去查阅MDN


此处只列出了json的结构 很显然,传入null 是合法的,但是传入空字符是不合法的,


JSON = null
   or true or false
   or JSONNumber
   or JSONString
   or JSONObject
   or JSONArray

吐槽


可能有人要吐槽,直接写JSON存储的时候格式不对不就行了吗?干什么这那么多,又是异常上报,又是贴代码?又是贴MDN的。


我在这里回答一下之所以这么写一是为了记录出错的时候出现的问题,方便下次出现类似问题能够即时复盘。


二是希望贴出自己的排错方式,新手若有不明白的可以模仿这个方式得到一些启发和思考,高手也可指出我的问题,共同成长


同样我也希望大家遇到问题的时候要记得查文档,查文档再查文档,自己遇到的问题,先文档,是不是自己理解错了,如果还不行就去stackoverflow,如果再不济就去github issue看看是否有相同的问题是不是作者的bug,如果都没有,那么好了,这个问题几乎解决不了了,此时有两个选择,要么产品接受,要么 那我走???


作者:傲娇的腾小三
来源:juejin.cn/post/7327227246618476583
收起阅读 »

Android:布局动画和共享动画的结合效果

大家好,我是时曾相识2022。不喜欢唱跳,但对杰伦的Rap却情有独钟。 今天给大家带来能够提升用户体验感的交互动画,使用起来非常简单,体验效果非常赞。其中仅使用到布局动画和共享动画。废话不多说,直接上效果图: 怎么样,效果看起来还不错吧。这其实都是官方提供...
继续阅读 »

大家好,我是时曾相识2022。不喜欢唱跳,但对杰伦的Rap却情有独钟。



今天给大家带来能够提升用户体验感的交互动画,使用起来非常简单,体验效果非常赞。其中仅使用到布局动画和共享动画。废话不多说,直接上效果图:


Screenrecorder-2023-09-12-12-00-04-706.gif


怎么样,效果看起来还不错吧。这其实都是官方提供的效果,接下来让我给大家简单分享下整套效果实现的过程和其中遇到的一些问题。


首先是布局动画,何为布局动画呢?


布局动画的作用于ViewGr0up,执行动画效果的是内部的子View。布局动画在Android中可以通过LayoutAnimationLayoutTransition来实现。咱们这里直接使用LayoutAnimation方式。在项目目录res下新建anim文件夹,并在其中新建layout_slid_from_right.xml文件和slide_from_right.xml两个文件:


//Gr0upView中设置动画文件
android:layoutAnimation="@anim/layout_slid_from_right"

//layout_slid_from_right.xml文件
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
android:animation="@anim/slide_from_right"
android:animationOrder="normal"
android:delay="15%"/>

//slide_from_right.xml文件
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="600">
<translate
android:fromYDelta="100%p"
android:interpolator="@android:anim/decelerate_interpolator"
android:toYDelta="0" />

<alpha
android:fromAlpha="0.5"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:toAlpha="1" />

<scale
android:fromXScale="20%"
android:fromYScale="20%"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="100%"
android:toYScale="100%" />

<rotate
android:fromDegrees="-5"
android:interpolator="@android:anim/accelerate_interpolator"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="0" />
</set>

其中set标签下可包含多个动画,运行时动画就是同时进行的。具体实现步骤可以参考我之前的文章:Android:LayoutAnimal的神奇效果



  • translate :平移动画

  • alpha:渐变动画

  • scale:缩放动画

  • rotate:旋转动画


接下来是共享动画,其实就是两个页面都包含了同一个元素,进行的一种转场动画。这是Android5.0以后Google推出Material Design设计风格中包含的功能。


如何使用呢?



  • 第一个ActivityXML文件中咱们将ImageView作为共享元素


<ImageView
android:id="@+id/iv"
android:layout_width="match_parent"
android:layout_height="250dp"
app:riv_corner_radius="10dp" />


  • 第二个ActivityXML文件中需要添加一个transitionName属性,在跳转页面的时候也要用到它。


<ImageView
android:id="@+id/iv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:transitionName="share"/>


  • 跳转页面时使用ActivityOptionsCompat设置共享信息并传输给下个页面:


val optionsCompat = ActivityOptionsCompat.makeSceneTransitionAnimation(this, iv, "share")//iv是当前点击的图片  share字符串是第二个activity布局中设置的**transitionName**属性
startActivity(Intent(this, MainActivity10::class.java).apply {
putExtra("data", url) //这里仍然可以正常传值
}, optionsCompat.toBundle()) //注意这里是转化为了bundle


  • 当然关闭页面的时候不再使用finish() 方法而是使用如下方式:


ActivityCompat.finishAfterTransition(this)

到此运行程序,就能达到和上面一样的动画效果。


遇到的坑:



  • 设置布局动画的时候,一定要记得在set标签内添加duration属性并赋值,否则不会有动画效果

  • 布局动画作用于所有的Gr0upView

  • 转场动画在选用共享属性的时候最好选用原生View。笔者之前尝试过一些第三方的ImageView,在跳到目标页的时候即便XML中将图片宽高设置为了match_parent,结果却只展示了图片本身的宽高。很有可能是自定义过程中计算和官方有冲突。

  • 官方的转场动画从5.0开始支持


好了,以上便是布局动画和共享动画的结合效果的全部内容。大家可以根据自己的需求和喜好实现更多酷炫的效果,希望这篇内容能给大家带来收获!


作者:似曾相识2022
来源:juejin.cn/post/7276750877251649592
收起阅读 »

相见恨晚的前端开发利器-PageSpy

web
今天介绍一个非常有用的前端开发工具。 做前端开发的你,一定有过以下经历: 我这里是好的啊,你截个图给我看看 不会吧,你打开f12,控制台截个图给我看看 录个屏给我看看你是怎么操作的 ... 还有,我们在开发h5的时候,一般为了调试方便,可能会在开发环境和测...
继续阅读 »

今天介绍一个非常有用的前端开发工具。


做前端开发的你,一定有过以下经历:



  1. 我这里是好的啊,你截个图给我看看

  2. 不会吧,你打开f12,控制台截个图给我看看

  3. 录个屏给我看看你是怎么操作的

  4. ...


还有,我们在开发h5的时候,一般为了调试方便,可能会在开发环境和测试环境实例化一个vConsole,遇到问题看一下大概就能定位到错误。


可是如果测试小姐姐在远程呢?如果是线上环境呢?如果有这么一个工具,能让我坐在工位上就能调(窥)试(探)用户的操作,那岂不是美滋滋。


你可能会说,这不就是埋点吗,先别急,今天介绍的这个工具和埋点有着本质区别。


不啰嗦了,有请主角**「PageSpy」**登场。


PageSpy是什么?




PageSpy[1] 是由货拉拉大前端开源的一款用于调试 H5 、或者远程 Web 项目的工具。是一个强大的开源前端远程调试平台,它可以显著提高我们在面对前端问题时的效率。




有什么作用?



  • 一眼查看客户端信息 能识别客户端运行环境,支持Linux/Mac/Window/IOS/Android

  • 实时查看输出 可以实时输出客户端的Element,Console,Network,Storage

  • 网络请求监控 可以捕获和显示页面的网络请求

  • 远程控制台 支持远程调试客户机上的js代码


如何使用?


查看官方文档[2]



  1. 安装npm包


yarn global add @huolala-tech/page-spy-api

# 如果你使用 npm

npm install -@huolala-tech/page-spy-api


  1. 启动服务


直接在命令行执行page-spy-api,部署完成后浏览器访问:6752,页面顶部会出现接入SDK菜单,点击菜单查看如何在业务项目中配置并集成。图片命令行执行后出现这个界面表示服务启动成功了,然后访问我自己的ip+端口,再点击顶部接入SDK图片去创建一个测试项目,建一个最简单的index.html,按照文档接入SDK,然后在浏览器访问这个页面图片图片左下角出现Pagepy的logo说明引入成功了。


此时点击顶部菜单房间列表图片点击调试,就可以看到这个项目的一些实时调试信息,但是还没加什么代码。图片现在改一下我们的代码,加一些输出信息。图片Console控制台的信息图片直接输出用户端代码变量的实时的值图片加个定时器试试,也是实时输出的图片图片再来看看Storage信息图片图片Element信息图片调个接口试试图片图片图片


好了,今天的介绍就到这里,这么牛叉的工具,是不是有种相见恨晚的感觉,感兴趣的小伙伴快去试试吧!


Reference


[1] PageSpy:huolalatech.github.io/page-spy-we…


[2] 官方文档:github.com/HuolalaTech…


作者:丝绒拿铁有点甜
来源:juejin.cn/post/7327691403844665380
收起阅读 »

年底了,出了P0级故障,人肉运维不可靠

翻车现场 5年前,大概是2018年12月份的一个晚上,我接到数据组同事的消息,要求将A用户的磁盘快照共享给B用户。我对这个线上运维工作早已轻车熟路,登录线上服务器仅用了2分钟就完成了。 我继续忙着其他事情,3分钟后,我正要打开新的控制台页面,猛然发现控制台上的...
继续阅读 »

翻车现场


5年前,大概是2018年12月份的一个晚上,我接到数据组同事的消息,要求将A用户的磁盘快照共享给B用户。我对这个线上运维工作早已轻车熟路,登录线上服务器仅用了2分钟就完成了。


我继续忙着其他事情,3分钟后,我正要打开新的控制台页面,猛然发现控制台上的“ public = true”。我惊慌地查看磁盘快照状态,发现磁盘快照已经共享给了所有用户。任何用户都可以在自己的快照列表中看到这个快照,并用快照创建新的磁盘,这意味着这些快照数据已经泄露了。这可是公司重要客户的磁盘数据啊!!!!


我心里明白,对于云计算行业,数据安全问题比线上bug还要严重!


我立刻就慌了,心脏砰砰的跳,手也开始颤抖。我心里很忐忑,一开始试图偷偷回滚,纠结之后,最终选择告诉了组长。


我倒吸一口气,一边进行回滚,一边试图平静的说,“我把刚才的快照共享给了所有租户”。瞬间,组长瞪大眼睛了看着我,“回滚了吗,赶紧回滚……”。 我颤抖地编辑SQL,手都麻木了,心脏还在怦怦跳个不停,开始担心这件事的后果。


领导边看我回滚,边小声对我说,“赶紧回滚,下次小心点”,看的出来,组长不想声张,他想先看看影响。


”嗯,好“,我努力嗯了一声,组长没大声骂我,我很感动。本以为回滚了,就没事了。



(后来这家小公司黄了,这是被我干黄的第二家公司,你们干黄了几家?)



然而,这远远没有结束。


原本宁静的办公室突然变得热闹起来,周围的同事们纷纷接到了报警通知。他们“兴高采烈”地讨论着报警的原因,我的注意力也被吸引了过去,听起来似乎与我有关,但我却没有心情去理会他们。


最终,快照被共享 5 分钟后,回滚完成,我长舒一口气,心想幸好我多看了一眼控制台,否则不知道被泄露多久。


与此同时,邻居组的成员钱哥找到了我,问道:“刚才快照计费数据暴涨了,你们这边有做过什么操作吗?”


随后,邻居组的组长王哥也过来了,询问情况如何。


我的组长苦笑着告诉他们:“刚才一个磁盘快照错误地被共享给了所有租户,不过现在已经回滚了。”


邻居组的王哥听后惊愕地说道:“卧槽,谁干的?”他的脸上露出了一丝微笑,似乎是看热闹的微笑。


我实在不知道该怎么说了,苦着脸问他们:“计费数据能回滚吗?”


邻居组的王哥没有回答我的问题,看了我一眼,说:“我叫上老板,先找个会议室讨论一下吧。”


万幸的是这 5分钟里没有用户使用此快照创建磁盘,这意味快照数据没有发生实质性泄露。


至暗时刻


接下来的两天里,我只做了两件事,参加复盘会议和去会议室的路上。这两天是我人生中最难忘的时刻,我尴尬得连脚丫子都能拧成麻花。


我真希望能立刻辞职离开这个地方。”别再鞭尸了,老子不干了,行不行。md,不就是共享个快照嘛!“ 我的心理状态从忐忑变得暴躁~



(每次造成线上故障,我都有类似的想法,我不想干了,不就是个bug吗,不干了,还不行吗?你们有类似想法吗?)



后来我开始后悔 ,为什么不早点下班,九点多还帮同事进行高危的线上操作,我图个啥


对,我图个啥。我脑子被驴踢了,才提出这个人肉运维方案,一周运维十几次,自己坑自己……


背景


2个月前,组长接到一个大客户需求,要求在两个租户之间共享云磁盘数据,当时提出很多个方案,其中包括分布式存储系统提供工具共享两个云磁盘数据等非常复杂的方案。 我当时听到这个需求,就立马想到, 我们的云管理系统可以实现两个租户的资源共享啊,通过给云磁盘打快照、共享快照等,就实现两个云磁盘的数据共享。


当时我非常得意,虽然我对存储并不是很了解,但是我相信我的方案比存储团队的底层方案更加简单且可行性更高。经过与客户的沟通,确定了这个方案能够满足他们的诉求,于是我们定下了这个方案。


由于大客户要的比较急,我改了代码就急匆匆上线,这个需求甚至没有产品参与,当客户需要共享数据时,需要我构造请求参数,在线上服务器上命令行执行共享操作。第一版方案在线上验证非常顺利,客户对这样快速的交付速度非常满意


因为我们使用了开源的框架,资源共享能力是现成的,所以改动起来很快。只不过有一个核弹级feature,我忽略了它的风险。


public = true时,资源将共享给全部用户。“只要不设置这个参数就不会有什么问题。” 这是我的想法,我没有考虑误操作的可能,更没有想到自己会犯下这个错误。


本以为只是低频的一次性操作,没想到后来客户经常性使用。我不得不一次次在线上执行高危操作,刚开始我非常小心谨慎,仔细的检查每个参数,反复确认后才执行命令。


然而,后来我感到这个工作太过枯燥乏味,于是开始集中处理,一次性执行一批操作。随着时间的推移,我越来越熟悉这件事。这种运维操作我两分钟就能完成……之所以这么快,是因为我不再仔细检查参数,只是机械地构造参数,随手执行。正是我松懈的态度导致闯下了大祸,在那个日常性加班的晚上。


后来我开始反思,从需求提出到故障发生前,我有哪些做的不对的地方。我认为有如下问题。



  1. 技术方案不能仅限于提供基本的资源共享能力,还要提供可视页面,提供产品化能力。

  2. 高危接口,一定要严格隔离成 单独的接口,不能和其他接口混合在一起,即使功能类似

  3. 线上重要操作要提供审核能力!或者有double check 的机制!


深刻的反思


任何工作都是有风险的,尤其是程序员无时无刻都在担心发生线上问题,如果不学会保护自己,那么多干一件事就多增加很多风险,增加背锅的风险。


拿我来说,本来这个需求不需要我参与,我提出了一个更简单的方案,高效的响应了大客户需求,是给自己长脸的事情。然而,我犯了一个巨大的错误,之前所做的努力都付之一炬。大领导根本不知道我提出的方案更简洁高效,他只认为我办事不可靠。在复盘会议上,我给大领导留下了非常糟糕的印象。


话说回来,在这个事情上如何保护自己呢?



  1. 技术方案一定要避免人肉运维,对于高危运维操作要求产品提供可视化页面运维。一定要尽全力争取,虽然很多时候,因为排期不足,前端资源不足等原因无法做到。

  2. 如果没有运维页面,等基础能力上线后,继续寻求组长帮助,协调产品提供操作页面,避免一直依赖自己人肉运维去执行高危操作。

  3. 在还没有产品化之前,要求客户或上游同事将所有的需求整理到文档上,使用文档进行沟通交流,记录自己的工作量,留存一份自己的”苦劳“。

  4. 在低频操作,变为高频操作时,不应该压迫自己更加“高效运维”,而是将压力和风险再次传达给产品和组长,让他们意识到我的人肉运维存在极大危险,需要要尽快提供产品化能力。让他们明白:“如果不尽快排期,他们也会承担风险!”

  5. 任何时候,对于线上高危操作,一定要小心谨慎。万万不可麻痹大意!


总之,千万不要独自承担所有的压力和风险。在工作中,我们可以付出辛勤努力,承受一定的风险,但是必须得到相应的回报。



风浪越大,鱼越贵。但是如果大风大浪,鱼还是很便宜,就不要出海了!风险收益要对等



就这个事情来说,每天我都要执行高风险的运维操作,是一种辛苦而不太受重视的工作。尽管如此,我却必须承担着巨大的风险,并自愿地让自己不断追求更高效的人工运维方式。然而结果却让人啼笑皆非,我终究翻车了。实在是可笑。



挣着卖白菜的钱,操着卖白粉的心,这是我的真实写照。



吾日三省吾身、这事能不能不干、这事能不能明天干、这事能不能推给别人干。


程序员不善于沟通,往往通过加班、忍一忍等方式默默地承担了很多苦活、脏活、累活。但是我们要明白,苦活可以,脏活等高风险的活 千万不要自己扛。


你干好十件事不一定传到大领导耳朵里,但是你出了一次线上大事故,他肯定第一时间知道。


好事不出门,坏事传千里。


我们一定要对 高危的人工运维,勇敢说不!


作者:五阳神功
来源:juejin.cn/post/7285673629526753316
收起阅读 »

幻兽帕鲁Palworld服务端最佳一键搭建教程

幻兽帕鲁Palworld最近彻底火了,忍不住也自己去搭建了一下,找了很多教程,发现还是目前找的这个最方便,结合腾讯云服务器新人优惠套餐,这里给出我搭建的详细步骤,包你轻松搞定。 此方案适合新用户,毕竟老用户购买服务器价格还是很贵的,自己如果已经是老用户了记得用...
继续阅读 »

幻兽帕鲁.jpg


幻兽帕鲁Palworld最近彻底火了,忍不住也自己去搭建了一下,找了很多教程,发现还是目前找的这个最方便,结合腾讯云服务器新人优惠套餐,这里给出我搭建的详细步骤,包你轻松搞定。


此方案适合新用户,毕竟老用户购买服务器价格还是很贵的,自己如果已经是老用户了记得用身边的人帮你买一个即可。


服务器选择


目前发现各大厂家都推出了自家的新人首单优惠,官方入场,最为致命!太便宜了,这里推荐三家主流的



image.png


腾讯云的点击进来后,可以看到很明显的一栏关于帕鲁游戏的,点击后面的前往部署就可以进入优惠的服务器了


,推荐新人使用66元这一档,我个人也是买了这档来测试。


image.png



阿里云也推出了幻兽帕鲁专属云服务器,还是针对新用户的,如果你进来看到的价格也是入下图这样,那推荐入手


image.png



华为云也推出新用户一个月的优惠价,一个比一个卷


image.png


教程推荐


我这次操作的教程脚本是参考github.com/2lifetop/Pa… 这个项目
之所以用这个教程因为足够简单,也有界面可视化来配置私服的参数,


image.png


搭建步骤详细说明


这里我用的是腾讯云服务器,所以流程介绍腾讯云上面的搭建方式,如果你买的是其他家的也类似,核心步骤都是以下2点:



  • 一键安装脚本

  • 服务端配置(可选)

  • 端口8211开放


服务器购买


因为脚本推荐的是用 Debian 12,所以我购买腾讯云服务器的时候,直接选择了 Debian12带Docker的版本。


image.png


购买后就可以进入服务器的界面了,如果找不到,可以搜索轻量应用服务器


image.png


image.png


这里你可以用第三方ssh登录或者直接直接网页登录都行。我推荐用第三方登录,我用的是FinalShell这个软件,我第一步是进入修改密码。


image.png


然后就用FinalShell登录上了,稳的一批。


一键安装脚本


以root用户登陆到服务器然后运行以下命令即可。该脚本目前只在Debian12系统上验证过。如果遇上非网络问题则请自行更换系统或者寻求其他解决方案。


非root用户请先运行 sudo su命令。


1.  wget -O PalServerInstall.sh https://www.xuehaiwu.com/wp-content/uploads/shell/Pal/PalServerInstall.sh --no-check-certificate && chmod +x PalServerInstall.sh && ./PalServerInstall.sh

出现下面这个画面了,选择1安装即可


image.png


正常等待几分钟就可以安装好了, 不过我自己安装的时候出现过问题,提示安装失败,然后我就执行11删除,然后重新执行脚本安装就成功了。


服务端配置(可选)


因为搭建的是私服嘛,所以为了体验更加,这个脚本提供了在线参数修改,步骤也很简单
先打开 http://www.xuehaiwu.com/Pal/
把你想调整的参数自行设置


image.png


其中比较重要的配置有



  • 服务器名称

  • 服务器上允许的最大玩家数(上限为 32)

  • 用于授予管理员访问权限的密码

  • 普通玩家加入所需的密码


如果要使用管理员命令需要加上管理员密码,普通玩家加入密码暂时不推荐设置,因为可能会造成玩家进不来。


服务器配置生成也挺麻烦的,所以我简单的做了个生成网页。要修改哪个直接在网页上修改就行。配备了中文介绍。


都设置好了就可以点击下面的【生成配置文件】,然后复制下生成的wget这一行命令。


image.png


然后切回到SSH那边,黏贴执行即可,这样就会生成一个叫 PalWorldSettings.ini配置文件,这个时候就重新执行下脚本命令 ./PalServerInstall.sh ,调出命令窗口,选择4 就行,这样就会覆盖配置了。


修改之后不是立即生效的,要重启帕鲁的服务端才能生效,对应数字8


端口8211开放


到此还差最后一步,就是要开放8211端口,我们进入到腾讯云网页端,点击进入详情


image.png


切换到防火墙,配置两条,TCP、UDP端口8211开放即可。


image.png


到此就算搞定了服务端的搭建了,这时候复制下公网IP,一会要用到


登录游戏


游戏也是需要大家自己购买的,打开游戏后,会看到一个【加入多人游戏(专用服务器)】选项,点击这个


8b463bab9f2b026c77afaf711f79448.png


进来后看到底部这里了没,把你服务器公网的ip去替换下 :8211前面的ip数字即可
比如我的ip是:106.54.6.86,那我输入的就是 106.54.6.86:8211


image.png


总结


ok,到此就是我搭建幻兽帕鲁Palworld服务端的全部流程,这游戏还是挺有意思的,缺点是缝合怪,优点是缝的还不错,我昨天自己搭建完玩了2个小时,大部分在搭建我的房子,盖着停不下来哈哈,感觉可以盖个10层楼。


499f598cf68efdf9486e23424e65f44.png


别人盖的比我好看多了。


image.png


这游戏其实火起来还有一个梗:帕鲁大陆最不缺的就是帕鲁,你不干有的是帕鲁干。
图片


我体验了一下也发现很真实,在游戏里面和帕鲁交朋友哈哈哈,其实是在压榨它们,让它们帮我们干活,累倒了就换一个,帕鲁多的是不缺你一个。现实中我们不也是帕鲁吗,所以大家突然找到了共鸣。


各位上班的时候就是帕鲁,下班了在游戏里面压榨帕鲁。


作者:嘟嘟MD
来源:juejin.cn/post/7328621062727122944
收起阅读 »

大厂真实 Git 开发工作流程

记得之前也写过一篇类似的文章,来到我东后感觉不够详尽,很多流程还是太局限了。大厂的开发流程还是比较规范的。如果你来不及学习长篇大论的 git 文档,这篇文章够你入职时用一段时间了。 一、开发分支模型分类 目前所在部门使用是主要是四种:dev(开发)、test(...
继续阅读 »

记得之前也写过一篇类似的文章,来到我东后感觉不够详尽,很多流程还是太局限了。大厂的开发流程还是比较规范的。如果你来不及学习长篇大论的 git 文档,这篇文章够你入职时用一段时间了。


一、开发分支模型分类


目前所在部门使用是主要是四种:dev(开发)、test(测试)、uat(预发)、release(生产)



小公司可能就一个 dev、一个 master 就搞定了,测试都是开发人员自己来🤣。



二、开发主体流程



  1. 需求评审

  2. 开发排期

  3. 编码开发

  4. 冒烟测试(自检验)

  5. 冒烟通过,提交测试,合并代码到测试分支,部署测试环境

  6. 测试环境测试,开发修 bug

  7. 测试完成,提交预发,合并代码到预发分支,部署预发环境

  8. 预发环境测试,开发修 bug(修完的 bug 要重新走测试再走预发,这个下面会解释)

  9. 测试完成,产品验收

  10. 验收完成,提交生产,合并代码到生产分支,部署生产环境

  11. 生产运营(客户)验收

  12. 验收完成,结项


三、具体操作


1. 拉取代码


一般都会在本地默认创建一个 master 分支


git clone https://code.xxx.com/xxx/xxx.git

2. 初次开发需求前,要先拉取生产/预发分支,然后基于这个分支之上,创建自己的特性分支进行开发


git fetch origin release:release

git checkout release

git checkout -b feat-0131-jie

此时,在你本地已经有了一个 release 分支对应着远程仓库的 release 分支,还有一个内容基于 release 分支的特性分支,之后便可以在这个特性分支上进行需求开发了。


注意1:分支名称是有规范和含义的,不能乱取。

推荐格式:分支责任-需求日期/需求号-开发人姓名,一般按部门规范来,常见的有以下几种。


  - feat:新功能

- fix:修补bug

- doc:文档

- refactor:重构(即不是新增功能,也不是修改bug的代码变动)

- test:测试

- chore:构建过程或辅助工具的变动

注意2:为啥拉取的是生产/预发分支

之所以要拉取 release/uat 分支而不是拉取 dev/test,是因为后者可能包含着一些其他成员还未上线或者可能有 bug 的需求代码,这些代码没有通过验证,如果被你给拉取了,然后又基于此进行新的需求开发,那当你需求开发完成,而其他成员的需求还没上线,你将会把这些未验证的代码一起发送到 uat/release 上,导致一系列问题。


3. 需求开发完成,提交&合并代码


首先先在本地把新的改动提交,提交描述的格式可以参考着分支名的格式



  • 如果是新需求的提交,可以写成 "feat: 需求0131-新增账期"

  • 如果是 bug 修复,可以写成 "fix: 禅道3387-重复请求"


git add .

git commit -m "提交描述"

此时,本地当前分支已经记录了你的提交记录,接下来进行代码合并了


在代码合并之前,我们先要梳理一下我们应该如何对分支进行管理(非常重要!)


  1. 首先,我们需要认知到的是,每一个分支应该只对应一个功能,例如当我们开发需求 01 时,那么就创建一个 feat-01-jie 分支进行开发;开发需求 02 时,就另外创建一个 feat-02-jie 分支进行开发;修改生产环境的某个 bug 时,就创建 fix-jie-3378 进行开发,等等。


    这样做的目的是,能够把不同的功能/需求/修改分离开来。想象一下这样一个场景,如果有某些紧急的需求是需要提前上线的,而此时你的分支里既包含了这些紧急的需求,又包含了其他未开发好的需求,那么这两种需求就不能拆开来分别进行提测和上线了。


  2. 其次,在合并代码时,我们要将四种分支模型(dev、test、uat、release)作为参照物,而不是把关注点放在自己的分支上。比如我们要在 dev 上调试,那就需要把自己的分支合并到 dev 分支上;如果我们需要提测,则把自己的分支合并到 test 分支上,以此类推。


    即,我们要关注到,这四个环境的分支上,会有什么内容,会新增什么内容。切记不能反过来将这四个分支合并到自己的代码上!! 如果其他成员将自己的代码也提交到 dev 分支上,但是这个代码是没有通过验证的,此时你将 dev 往自己的分支上合,那之后的提测、上预发、生产则很大概率会出问题。所以一定要保持自己的分支是干净的!



接下来介绍合并代码的方式:


第一种:线上合并,也是推荐的规范操作

git push origin feat-0131-jie

先接着上面的提交步骤,将自己的分支推送到远程仓库。


然后在线上代码仓库中,申请将自己的分支合并到 xx 分支(具体是哪个分支就根据你当前的开发进度来,如 test),然后在线上解决冲突。如果有权限就自己通过了,如果没有就得找 mt 啥的


第二种,本地合并(前提你要有对应环境分支 push 的权限)

## 先切换到你要提交的环境分支上,如果本地还没有就先拉取下来
git fetch origin test:test

git checkout test

#
# 然后将自己的分支合并到环境分支上(在编辑器解决冲突)
git merge feat-0131-jie

#
# 最后将环境分支推送到远程仓库
git push origin test

## 先切换到你要提交的环境分支上,如果本地已有该分支,则需要先拉取最新代码
git checkout test

git pull origin test

#
# 然后将自己的分支合并到环境分支上(在编辑器解决冲突)
git merge feat-0131-jie

#
# 最后将环境分支推送到远程仓库
git push origin test

两种方式有何区别?为什么推荐第一种?

这是因为在团队协作开发的过程中,将合并操作限制在线上环境有以下几个好处:



  1. 避免本地合并冲突:如果多个开发人员同时在本地进行合并操作,并且对同一段代码进行了修改,可能会导致冲突。将合并操作集中在线上环境可以减少此类冲突的发生,因为不同开发人员的修改会先在线上进行合并,然后再通过更新拉取到本地。

  2. 更好的代码审查:将合并操作放在线上环境可以方便其他开发人员进行代码审查。其他人员可以在线上查看合并请求的代码变动、注释和讨论,并提供反馈和建议。这样可以确保代码的质量和可维护性。

  3. 提高可追溯性和可回滚性:将合并操作记录在线上可以更容易地进行版本控制和管理。如果出现问题或需要回滚到之前的版本,可以更轻松地找到相关的合并记录并进行处理。


当然,并非所有情况都适用于第一种方式。在某些特定情况下,例如个人项目或小团队内部开发,允许本地合并也是可以的。但在大多数团队协作的场景中,将合并操作集中在线上环境具有更多优势。


4. 验收完成,删除分支


当我们这一版的需求完成后,本地肯定已经留有很多分支了,这些分支对于之后的开发已经意义不大了,留下来只会看着一团糟。


git branch -d <分支名>

#
# 如果要强制删除分支(即使分支上有未合并的修改)
git branch -D <分支名>

四、一些小问题


1. 前面提到,预发环境修完的 bug 要重新走测试再走预发,为什么呢?


预生产环境是介于测试和生产环境之间的一个环境,它的目的是模拟生产环境并进行更真实的测试。
它是一个重要的测试环境,需要保持稳定和可靠。通过对修复的bug再次提交到测试环境测试,可以确保预生产环境中的软件版本是经过验证的,并且没有明显的问题。


当然,也不是非要这么做不可,紧急情况下,也可以选择直接发到预生产重新测试,只要你保证你的代码 99% 没问题了。


2. 代码合并错误,并且已经推送到远程分支,如何解决?


假设是在本地合并,本来要把特性分支合并到 uat 分支,结果不小心合到了 release 分支(绝对不是我自己的案例,绝对不是。。。虽然好在最后同事本地有我提交前的版本,事情就简单很多了)


首先切换到特性分支合并到的错误分支,比如是 release


git checkout release

然后查看最近的合并信息


git log --merges

撤销合并


git revert -m 1 <merge commit ID>


  • 这里的 merge commit ID 就是上一步查询出来的 ID 或者 ID 的前几个字符


最后,撤销远程仓库的推送


git push -f origin release


  • 这个命令会强制推送本地撤销合并后的 release 分支到远程仓库,覆盖掉远程仓库上的内容。(即,得通过一个新的提交来“撤销”上一次的提交,本质上是覆盖)


3. 当前分支有未提交的修改,但是暂时不想提交,想要切换到另一个分支该怎么做?


例如:你正在开发 B 需求,突然产品说 A 需求有点问题,让你赶紧改改,但是当前 B 需求还没开发完成,你又不想留下过多无用的提交记录,此时就可以按照下面这样做:


首先,可以将当前修改暂存起来,以便之后恢复


git stash

然后切换到目标分支,例如需求 A 所在分支


git checkout feat-a-jie

修改完 A 需求后,需要先切换回之前的分支,例如需求 B 所在分支


git checkout feat-b-jie

如果你不确定之前所在的分支名,可以使用以下命令列出暂存的修改以及它们所属的分支:


git stash list

最后从暂存中恢复之前的修改


git stash pop

此时你的工作区就恢复如初了!




喜欢本文的话,可以点赞收藏呀~😘


如果有疑问,欢迎评论区留言探讨~🤔


作者:JIE
来源:juejin.cn/post/7327863960008392738
收起阅读 »

《卖炭翁》致敬河北程序员,初读已解诗中意,再读却是诗中人!

起初他们追杀共产主义者的时候, 我没有说话 ——因为我不是共产主义者; 接着他们追杀犹太人的时候, 我没有说话 ——因为我不是犹太人; 后来他们追杀工会成员的时候, 我没有说话 ——因为我不是工会成员; 此后他们追杀天主教徒的时候, 我没有说话 ——因为我是新...
继续阅读 »

起初他们追杀共产主义者的时候,


我没有说话


——因为我不是共产主义者;


接着他们追杀犹太人的时候,


我没有说话


——因为我不是犹太人;


后来他们追杀工会成员的时候,


我没有说话


——因为我不是工会成员;


此后他们追杀天主教徒的时候,


我没有说话


——因为我是新教教徒;


最后他们奔我而来,


那时已经没有人能为我说话了。



这一首著名的《我没有说话》是德国神学家马丁・尼莫拉牧师的忏悔诗,尽管他写的是自己,但这首诗却振聋发聩,发人深省,其描述忽视与表面上自己无关的团体所造成的结果。该诗后来常被引用,作为对事不关己高高挂起的人的呼吁。


这首诗被镌刻在美国马萨诸塞州波士顿的新英格兰犹太人大屠杀纪念碑石碑上。


马丁・尼莫拉曾经生活在一个黑暗无光的时代,遭受过极权统治的迫害,这一经历对他来说,有着切肤之痛。


因为自己的惨痛经历,尼莫拉牧师认识到:在这个世界上,人与人的命运往往是休戚与共的,不坚持真理,不伸张正义,不维护公平,在邪恶面前只顾及自身的利益,对他人被冤屈被欺凌被迫害漠然置之,最终受到惩罚的是我们自己。


最近的事情大家也都晓得了,这件事让我们禁不住想起初中课本里的一篇课文:唐代大诗人白居易所创作的《卖炭翁》:



卖炭翁,伐薪烧炭南山中。

满面尘灰烟火色,两鬓苍苍十指黑。

卖炭得钱何所营?身上衣裳口中食。

可怜身上衣正单,心忧炭贱愿天寒。

夜来城外一尺雪,晓驾炭车辗冰辙。

牛困人饥日已高,市南门外泥中歇。

翩翩两骑来是谁?黄衣使者白衫儿。

手把文书口称敕,回车叱牛牵向北。

一车炭,千余斤,宫使驱将惜不得。

半匹红纱一丈绫,系向牛头充炭直。




白居易在《新乐府》中每首诗的题目下面都有一个序,说明这首诗的主题。


《卖炭翁》的序是“苦宫市也”,就是要反映宫市给人民造成的痛苦。唐代皇宫里需要物品,就派人去市场上拿,随便给点钱,实际上是公开掠夺。


唐德宗时用太监专门负责掠夺老百姓。白居易写作《新乐府》是在宫市为害最深的时候,他对宫市有十分的了解,对太监极度的痛恨,对人民又有深切的同情,所以才能写出这首感人至深的《卖炭翁》。


这首诗的意义,远不止于对宫市的揭露。诗人在卖炭翁这个典型形象上,概括了唐代劳动人民的辛酸和悲苦,在卖炭这一件小事上反映出了当时社会的黑暗和不平。读着这首诗,读者所看到的决不仅仅是卖炭翁一个人,透过他,还能看到有许许多多种田的、打渔的、织布以及编程的人出现在眼前。


他们虽然不是“两鬓苍苍十指黑”,但也各自带着劳苦生活的标记;他们虽然不会因为卖炭而受到损害,但也各自在田租或赋税的重压下流着辛酸和仇恨的泪水。《卖炭翁》这首诗不但在当时有积极意义,即使对于今天的读者也有一定的教育作用。


正道是:



初读已解诗中意,再读却是诗中人!



作者:刘悦的技术博客
来源:juejin.cn/post/7284468618019143695
收起阅读 »

支付系统的心脏:简洁而精妙的状态机设计与核心代码实现

本篇主要讲清楚什么是状态机,简洁的状态机对支付系统的重要性,状态机设计常见误区,以及如何设计出简洁而精妙的状态机,核心的状态机代码实现等。 我前段时间面试一个工作过4年的同学竟然没有听过状态机。假如你没有听过状态机,或者你听过但没有写过,或者你是使用if el...
继续阅读 »



本篇主要讲清楚什么是状态机,简洁的状态机对支付系统的重要性,状态机设计常见误区,以及如何设计出简洁而精妙的状态机,核心的状态机代码实现等。


我前段时间面试一个工作过4年的同学竟然没有听过状态机。假如你没有听过状态机,或者你听过但没有写过,或者你是使用if else 或switch case来写状态机的代码实现,建议花点时间看看,一定会有不一样的收获。


1. 前言


在线支付系统作为当今数字经济的基石,每年支撑几十万亿的交易规模,其稳定性至关重要。在这背后,是一种被誉为支付系统“心脏”的技术——状态机。本文将一步步介绍状态机的概念、其在支付系统中的重要性、设计原则、常见误区、最佳实践,以及一个实际的Java代码实现。


2. 什么是状态机


状态机,也称为有限状态机(FSM, Finite State Machine),是一种行为模型,由一组定义良好的状态、状态之间的转换规则和一个初始状态组成。它根据当前的状态和输入的事件,从一个状态转移到另一个状态。


下图就是在《支付交易的三重奏:收单、结算与拒付在支付系统中的协奏曲》中提到的交易单的状态机。



从图中可以看到,一共4个状态,每个状态之间的转换由指定的事件触发。


3. 状态机对支付系统的重要性


想像一下,如果没有状态机,支付系统如何知道你的订单已经支付成功了呢?如果你的订单已经被一个线程更新为“成功”,另一个线程又更新成“失败”,你会不会跳起来?


在支付系统中,状态机管理着每笔交易的生命周期,从初始化到完成或失败。它确保交易在正确的时间点,以正确的顺序流转到正确的状态。这不仅提高了交易处理的效率和一致性,还增强了系统的鲁棒性,使其能够有效处理异常和错误,确保支付流程的顺畅。


4. 状态机设计基本原则


无论是设计支付类的系统,还是电商类的系统,在设计状态机时,都建议遵循以下原则:


明确性: 状态和转换必须清晰定义,避免含糊不清的状态。


完备性: 为所有可能的事件-状态组合定义转换逻辑。


可预测性: 系统应根据当前状态和给定事件可预测地响应。


最小化: 状态数应保持最小,避免不必要的复杂性。


5. 状态机常见设计误区


工作多年,见过很多设计得不好的状态机,导致运维特别麻烦,还容易出故障,总结出来一共有这么几条:


过度设计: 引入不必要的状态和复杂性,使系统难以理解和维护。


不完备的处理: 未能处理所有可能的状态转换,导致系统行为不确定。


硬编码逻辑: 过多的硬编码转换逻辑,使系统不具备灵活性和可扩展性。


举一个例子感受一下。下面是亲眼见过的一个交易单的状态机设计,而且一眼看过去,好像除了复杂一点,整体还是合理的,比如初始化,受理成功就到ACCEPT,然后到PAYING,如果直接成功就到PAIED,退款成功就到REFUND。



我说说这个状态机有几个不合理的地方:



  1. 过于复杂。一些不必要的状态可以去掉,比如ACCEPT没有存在的必要。

  2. 职责不明确。支付单就只管支付,到PAIED就支付成功,就是终态不再改变。REFUND应该由退款单来负责处理,否则部分退款怎么办。


我们需要的改造方案:



  1. 精简掉不必要的状态,比如ACCEPT。

  2. 把一些退款、请款等单据单独抽出去,这样状态机虽然多了,但是架构更加清晰合理。


主单:



普通支付单:



预授权单:



请款单:



退款单:



6. 状态机设计的最佳实践


在代码实现层面,需要做到以下几点:


分离状态和处理逻辑:使用状态模式,将每个状态的行为封装在各自的类中。


使用事件驱动模型:通过事件来触发状态转换,而不是直接调用状态方法。


确保可追踪性:状态转换应该能被记录和追踪,以便于故障排查和审计。


具体的实现参考第7部分的“JAVA版本状态机核心代码实现”。


7. 常见代码实现误区


经常看到工作几年的同学实现状态机时,仍然使用if else或switch case来写。这是不对的,会让实现变得复杂,且容易出现问题。


甚至直接在订单的领域模型里面使用String来定义,而不是把状态模式封装单独的类。


还有就是直接调用领域模型更新状态,而不是通过事件来驱动。


错误的代码示例:


if (status.equals("PAYING") {
status = "SUCCESS";
} else if (...) {
...
}

或者:


class OrderDomainService {
public void notify(PaymentNotifyMessage message) {
PaymentModel paymentModel = loadPaymentModel(message.getPaymentId());
// 直接设置状态
paymentModel.setStatus(PaymentStatus.valueOf(message.status);
// 其它业务处理
... ...
}
}

或者:


public void transition(Event event) {
switch (currentState) {
case INIT:
if (event == Event.PAYING) {
currentState = State.PAYING;
} else if (event == Event.SUCESS) {
currentState = State.SUCESS;
} else if (event == Event.FAIL) {
currentState = State.FAIL;
}
break;
// Add other case statements for different states and events
}
}

8. JAVA版本状态机核心代码实现


使用Java实现一个简单的状态机,我们将采用枚举来定义状态和事件,以及一个状态机类来管理状态转换。


定义状态基类


/**
* 状态基类
*/

public interface BaseStatus {
}

定义事件基类


/**
* 事件基类
*/

public interface BaseEvent {
}

定义“状态-事件对”,指定的状态只能接受指定的事件


/**
* 状态事件对,指定的状态只能接受指定的事件
*/

public class StatusEventPairextends BaseStatus, E extends BaseEvent> {
/**
* 指定的状态
*/

private final S status;
/**
* 可接受的事件
*/

private final E event;

public StatusEventPair(S status, E event) {
this.status = status;
this.event = event;
}

@Override
public boolean equals(Object obj) {
if (obj instanceof StatusEventPair) {
StatusEventPair other = (StatusEventPair)obj;
return this.status.equals(other.status) && this.event.equals(other.event);
}
return false;
}

@Override
public int hashCode() {
// 这里使用的是google的guava包。com.google.common.base.Objects
return Objects.hashCode(status, event);
}
}

定义状态机


/**
* 状态机
*/

public class StateMachineextends BaseStatus, E extends BaseEvent> {
private final Map, S> statusEventMap = new HashMap<>();

/**
* 只接受指定的当前状态下,指定的事件触发,可以到达的指定目标状态
*/

public void accept(S sourceStatus, E event, S targetStatus) {
statusEventMap.put(new StatusEventPair<>(sourceStatus, event), targetStatus);
}

/**
* 通过源状态和事件,获取目标状态
*/

public S getTargetStatus(S sourceStatus, E event) {
return statusEventMap.get(new StatusEventPair<>(sourceStatus, event));
}
}

定义支付的状态机。注:支付、退款等不同的业务状态机是独立的


/**
* 支付状态机
*/

public enum PaymentStatus implements BaseStatus {

INIT("INIT", "初始化"),
PAYING("PAYING", "支付中"),
PAID("PAID", "支付成功"),
FAILED("FAILED", "支付失败"),
;

// 支付状态机内容
private static final StateMachine STATE_MACHINE = new StateMachine<>();
static {
// 初始状态
STATE_MACHINE.accept(null, PaymentEvent.PAY_CREATE, INIT);
// 支付中
STATE_MACHINE.accept(INIT, PaymentEvent.PAY_PROCESS, PAYING);
// 支付成功
STATE_MACHINE.accept(PAYING, PaymentEvent.PAY_SUCCESS, PAID);
// 支付失败
STATE_MACHINE.accept(PAYING, PaymentEvent.PAY_FAIL, FAILED);
}

// 状态
private final String status;
// 描述
private final String description;

PaymentStatus(String status, String description) {
this.status = status;
this.description = description;
}

/**
* 通过源状态和事件类型获取目标状态
*/

public static PaymentStatus getTargetStatus(PaymentStatus sourceStatus, PaymentEvent event) {
return STATE_MACHINE.getTargetStatus(sourceStatus, event);
}
}

定义支付事件。注:支付、退款等不同业务的事件是不一样的


/**
* 支付事件
*/

public enum PaymentEvent implements BaseEvent {
// 支付创建
PAY_CREATE("PAY_CREATE", "支付创建"),
// 支付中
PAY_PROCESS("PAY_PROCESS", "支付中"),
// 支付成功
PAY_SUCCESS("PAY_SUCCESS", "支付成功"),
// 支付失败
PAY_FAIL("PAY_FAIL", "支付失败");

/**
* 事件
*/

private String event;
/**
* 事件描述
*/

private String description;

PaymentEvent(String event, String description) {
this.event = event;
this.description = description;
}
}

在支付单模型中声明状态和根据事件推进状态的方法:


/**
* 支付单模型
*/

public class PaymentModel {
/**
* 其它所有字段省略
*/


// 上次状态
private PaymentStatus lastStatus;
// 当前状态
private PaymentStatus currentStatus;


/**
* 根据事件推进状态
*/

public void transferStatusByEvent(PaymentEvent event) {
// 根据当前状态和事件,去获取目标状态
PaymentStatus targetStatus = PaymentStatus.getTargetStatus(currentStatus, event);
// 如果目标状态不为空,说明是可以推进的
if (targetStatus != null) {
lastStatus = currentStatus;
currentStatus = targetStatus;
} else {
// 目标状态为空,说明是非法推进,进入异常处理,这里只是抛出去,由调用者去具体处理
throw new StateMachineException(currentStatus, event, "状态转换失败");
}
}
}

代码注释已经写得很清楚,其中StateMachineException是自定义,不想定义的话,直接使用RuntimeException也是可以的。


在支付业务代码中的使用:只需要paymentModel.transferStatusByEvent(PaymentEvent.valueOf(message.getEvent()))


/**
* 支付领域域服务
*/

public class PaymentDomainServiceImpl implements PaymentDomainService {

/**
* 支付结果通知
*/

public void notify(PaymentNotifyMessage message) {
PaymentModel paymentModel = loadPaymentModel(message.getPaymentId());
try {

// 状态推进
paymentModel.transferStatusByEvent(PaymentEvent.valueOf(message.getEvent()));
savePaymentModel(paymentModel);
// 其它业务处理
... ...
} catch (StateMachineException e) {
// 异常处理
... ...
} catch (Exception e) {
// 异常处理
... ...
}
}
}

上面的代码只需要加完善异常处理,优化一下注释,就可以直接用起来。


好处:



  1. 定义了明确的状态、事件。

  2. 状态机的推进,只能通过“当前状态、事件、目标状态”来推进,不能通过if else 或case switch来直接写。比如:STATE_MACHINE.accept(INIT, PaymentEvent.PAY_PROCESS, PAYING);

  3. 避免终态变更。比如线上碰到if else写状态机,渠道异步通知比同步返回还快,异步通知回来把订单更新为“PAIED”,然后同步返回的代码把单据重新推进到PAYING。


9. 并发更新问题


留言中“月朦胧”同学提到:“状态机领域模型同时被两个线程操作怎么避免状态幂等问题?”


这是一个好问题。在分布式场景下,这种情况太过于常见。同一机器有可能多个线程处理同一笔业务,不同机器也可能处理同一笔业务。


业内通常的做法是设计良好的状态机 + 数据库锁 + 数据版本号解决。



简要说明:



  1. 状态机一定要设计好,只有特定的原始状态 + 特定的事件才可以推进到指定的状态。比如 INIT + 支付成功才能推进到sucess。

  2. 更新数据库之前,先使用select for update进行锁行记录,同时在更新时判断版本号是否是之前取出来的版本号,更新成功就结束,更新失败就组成消息发到消息队列,后面再消费。

  3. 通过补偿机制兜底,比如查询补单。

  4. 通过上述三个步骤,正常情况下,最终的数据状态一定是正确的。除非是某个系统有异常,比如外部渠道开始返回支付成功,然后又返回支付失败,说明依赖的外部系统已经异常,这样只能进人工差错处理流程。


10. 结束语


状态机在支付系统中扮演着不可或缺的角色。一个专业、精妙的状态机设计能够确保支付流程的稳定性和安全性。本文提供的设计原则、常见误区警示和最佳实践,旨在帮助开发者构建出更加健壮和高效的支付系统。而随附的Java代码则为实现这一关键组件提供了一个清晰、灵活的起点。希望这些内容能够对你有用。



作者:隐墨星辰
来源:juejin.cn/post/7321569896453521419
收起阅读 »

转全栈之路,会比想象中的艰难

背景 我于22年校招入职字节安全方向大前端部门,支持公司安全Tob产品的前端开发工作。今年8月,因为组织架构调整,很多同事都直接划入了业务部门,我也和另一名北京的同事互换了业务,划入业务部门。 在新部门工作2-3个月,因为种种原因,工作体验上的差别大到像是换了...
继续阅读 »

背景


我于22年校招入职字节安全方向大前端部门,支持公司安全Tob产品的前端开发工作。今年8月,因为组织架构调整,很多同事都直接划入了业务部门,我也和另一名北京的同事互换了业务,划入业务部门。


在新部门工作2-3个月,因为种种原因,工作体验上的差别大到像是换了一家公司,也很想记录一下到底有什么不同。


大前端部门业务部门
组织人数近30人,纯前端方向近40人,分为不同方向,前端背景1人
工作模式由于同事都在天南海北,需要通过视频会议进行沟通纯下线沟通,所有同事都base深圳
沟通效率较低,每次沟通都需要调试设备,共享屏幕等,并且见不到面很多信息会失真高,直接面谈,肢体语言这些信息不会丢失
工作节奏有排期压力,有承诺客户交付时间。如果排期不合理会很疲惫。没有排期压力,前端工作量相比之前轻松
设计资源有专门的UED团队出图,前端不需要思考如何进行交互,这部分工作由设计师承担无设计资源,交互的好坏完全取决于研发的审美水平与自我要求
前端技术建设每个季度会有横向建设,有组件库共建等机会,前端技术相对先进部门内部无前端建设,依赖公司基建与之前经验
同事组成深圳base全员年轻化,校招生为主,因为年龄相同且技术方向相同,天然就有很多话题资深员工多,校招生占比很低,且划分不同方向,一般自己方向的人自己内部沟通较多
和+1的关系base不同,沟通频率很低。因为主要是做业务方的需求,沟通内容主要在支持工作的进展上。base相同,沟通频率比以前高5-10倍,除同步开发进展,还会针对产品迭代方向,用户体验等问题进行沟通
技术成长受限于部门性质以及绩效评价体系,员工需要在前端技术领域保持专业且高效,但工作一定年限后有挑战性的业务需求不足,容易遇到职业发展瓶颈。因为前端人数多,所以存在横向建设的空间,可以共建组件库等基建,非常自然的会接触这些需求。角色划分不明确,前后端可以相互支援彼此,大家摘掉前后端的标签,回归通用开发的角色。技术成长依赖自驱力与公司技术水平。研发人少,没有内部的横向建设机会。

纠结


为什么要转全栈?究竟有什么收益?我会在心里时不时问自己这个问题。任何一门技能,从入门到精通,都需要很多时间的学习与实践,在初期都会经历一段相当痛苦的时光。除了学习不轻松,能否创造出更大的价值也是一个问号。


但这次转全栈,有天时地利人和的作用,总结下来就是:



  1. Leader支持:和Leader沟通过,Leader觉得在我们团队多做多学对个人,对团队都有益处,欢迎我大胆尝试

  2. 后端同学支持:我们团队的细分项目多,后端工作饱和,可以分一个相对独立的活给我

  3. 全栈化背景:原先的大前端部门已经有部分前端转为全栈开发职能,部门层面鼓励全栈化发展

  4. 需求清晰:有些开发之所以忙碌是因为开会和对齐耗时太多。但是我目前拿到的prd都非常清晰,拿到就能直接开发,对齐扯皮的时间几乎不计,我只需要完成开发工作即可。这节约了我大量时间成本。想到之前经常是一天开个1-2小时会,搞得很疲惫。

  5. 工作熟练:从实习开始算起,我已经有2年多的开发经验,可以在预期时间内完成需求开发和bugfix,因此安全的预留时间精力转全栈。


其实不仅仅是我,和很多做前端的同事、朋友也都聊过,其实内心各有各的纠结。基本上大家的内心想法就是想在有限的条件下学习后端,并在有限的条件下承担一部分后端开发。


想学后端的原因:



  1. 纯属好奇心,想研究一下后端技术栈

  2. 前端作为最终的执行角色,话语权低

  3. 业务参与度低,可以半游离业务的存在,较边缘化。未来如果希望成长为管理,难以做业务管理,只能做技术管理,想象空间天花板就是成为管理一批前端的技术管理。

  4. 工作遇到天花板,想多了解一下其他的内容


想在有限条件下学习后端的原因:



  1. 工作比较忙碌,没那么多时间学习

  2. 学习一门技能要算ROI,学太多了如果既不能升职也不能加薪就没有意义

  3. 不确定市场对于全栈人才的反应,不想all in


想承担一部分后端开发的原因:



  1. 学习任何一门技能只有理论没有实践遗忘速度最快,马上就会回归到学习之前

  2. 掌握后端技能但没有企业级实战经验,说服力很弱


不想学习后端的原因:



  1. 国内市场上的全栈岗位数量稀少,如果后端岗位有10个,前端岗位有3个,那么可能就只有1个全栈岗位

  2. 普通前后端开发薪酬基本上没有区别,未来谁更好晋升在当前的经济背景也难说

  3. 大概率前端依然是自己的职业发展主线,学多一门技能可能会分摊本可以提升前端能力的时间精力

  4. 做舒适圈里面的事情很舒服,谁知道多做会不会有好处


我就是在这种纠结中一路学过来,从8月开始,痛苦且挣扎,不过到目前为止还可以接受。学到现在甚至已经有点麻木。但我也确实不知道继续在前端领域还能专精什么技能,现有的业务没有那么大的挑战性让我快速成长,所以想跳脱出来看看更大的世界。


学习路线


曲线学习


如果说做前端开发我是如鱼得水,那做后端开发就是经常呛到水。


记得我刚开始做前端实习的时候,真心感到前端知识好像黑洞,永远也学不完。由此非常佩服之前的同事,怎么把这些代码写出来的,有些代码后面一看写的还不错,甚至可能会感觉脊背发凉,是自己太弱还是自己太强?


在实习的时候,我的学习曲线可以说是一个向外扩散的圆。比如我第一次接触webpack的时候,根本不了解这是什么工具,之前一直在用jQuery写项目,所有的js都是明文写好,然后通过script引入到html中。所以一开始我会去查这个webpack到底是什么内容,但脑海中对他的印象还是非常模糊。接着我又因为webpack了解到了babel,css-loader这些概念,又去学习。又发现这需要利用到node,又去学习了《深入浅出node.js》。再后来又了解到了sourcemap等概念。直到正式加入字节半年后,我自己配了一次webpack,并且阅读了他的源码。进行了debug,进行了一次webpack插件开发的分享,才有信心说自己是真的弄明白了。不过这个弄明白,也仅限于排查bug,配项目,进行plugin和loader的开发,如果遇到更难的领域,那又将解锁一块黑洞。


怎么学


学习后端,要学的内容也一点都不少,作为新人会遇到非常多的问题。



  1. 怎么学 - 是死皮赖脸的逮住后端同学使劲问,还是多自己研究研究?遇到所有同事都不会的问题怎么处理?

  2. 学到什么程度 - 究竟要学到怎样的程度才能进入项目开发,而不犯下一些非常愚蠢的问题呢?

  3. 学习顺序 - 最简单的办法就是去看项目,看到不懂的分析一下这是什么模块的,看看要不要系统性的去了解。


我比较喜欢一开始就系统性的学,学完后再查缺补漏,再开启第二轮学习。


比如Go,官网就有很详细的文档,但未必适合新人去学。我跟着官网学了一阵子之后跑b站找视频学习了。然后又Google了一些资料,大致讲了一下反射、切片的原理,以及一些错误用法。学习Go大概用了2-3周。刚学完直接去看项目还是会觉得非常不适应,需要不断的让自己去阅读项目代码,找到Go的那种感觉。


然后需要学习很多公司内部的基建



  • 微服务架构 - 公司内部所有服务都是微服务架构,需要了解服务发现、服务治理、观测、鉴权这些基本概念以及大致的原理。为了在本地开发环境使用微服务,还需要在本地安装doas,用来获取psm的token。

  • RDS - 公司内的项目分为了各种环境,非常复杂。可以自己先创建一个MySQL服务自测,看看公司的云平台提供了哪些能力。

  • Redis - 大致了解即可,简单用不难

  • RPC - 微服务通过RPC传递,RPC协议通过IDL来定义接口传输格式,像字节会在api管理平台做封装。你定义好的IDL可以直接生成一个gopkg上传到内部镜像,然后其他用户直接go get这个库就能调用你的服务。但如果你是node服务,就可以在本地通过字节云基建的工具库自动生成代码。

  • Gorm - 所有的MySQL最终如果通过go程序调用,都需要经过gorm的封装,来避免一些安全问题。同时也可以规避一些低级错误。还需要了解gen怎么使用,将MySQL库的定义自动生成为orm代码。


还要好好学习一下MySQL的用法,这边花了一周看完了《MySQL必知必会》,然后去leetcode刷题。国庆节刷了大概80道MySQL的题目,很爽。从简单的查询,到连接、子查询、分组、聚合,再到比较复杂的窗口函数、CTE全刷了个遍,刷上瘾了。


接着就可以去看项目代码了,这一部分还是蛮折腾的,对于新人来说。本身阅读别人的代码,对于很多开发者来说就是一件痛苦的事情,何况是去阅读自己不熟悉的语言的别人的代码。


我最近接手的一个半废弃项目,就很离谱。开发者有的已经离职了,提交记录是三四年前的。PRD也找不全,到今天权限还没拿齐,明天再找人问问。这边可能是真的上下文就是要丢失的,没法找了。只能自己创建一个新的文档,把相关重点补充一下。


明天找一下这个项目的用户,演示一下怎么使用,然后根据对用法的理解进行开发……


收获


新鲜感


一直写前端真的有点腻,虽然现在技术还在迭代,但也万变不离其宗。而且真的是有点过分内卷了,像一个打包工具从webpack -> esbuild -> vite -> turbopack -> rspack。不可否认的是这些开发者的努力,为前端生态的繁荣做出了贡献。但对于很多业务来说,其实并没有太大的性能问题,对于这部分项目来说升级的收益很小。比如云服务的控制台,基本都是微前端架构,每个前端项目都非常小,就算用webpack热更新也不会慢。而且webpack使用下来是最稳定的,我现在的项目用的是vite,会存在样式引入顺序的问题,导致开发环境和生产环境的页面区别。


后端技术栈不管好还是不好,反正对我来说是很新鲜的。虽然我之前Python、Go也都用过,也用Python写出了完整的项目,但论企业级开发这算第一次~各方面都更正规


Go写起来很舒服,虽然写同样的需求代码量比TypeScript多一堆……习惯之后还是可以感受到Go的简单与安心。Go打包就没那么多事,你本地怎么跑服务器那边就怎么跑,不像前端可能碰到一堆兼容性问题。


真的有学到


我前几个月买了掘金大佬神说要有光的小课《Nest 通关秘籍》,据我了解我的几个同事也买了。不过我没坚持下来,因为工作上实在是没有使用到Nest的机会。我无法接受学了两三个月却无法在工作里做出产出的感觉。


但这一次学了可以立马去用,可以在工作中得到检验,可以接受用户的检验。我就会得到价值感与成就感。


而且字节的Go基建在我认知里很牛叉,一家以Go为主的大厂,养得起很多做基建的人。比如张金柱Gorm的作者,竟然就在字节,我前几天知道感觉牛人竟然……


Go的学习资料也非常多,还有很多实战的,真的像突然打开了新世界的大门~


与业务更近,以及更平和的心态


如果我没有学后端,会在“前端已死”的氛围里胡思乱想,忽略了前端的业务价值,前端依旧是很重要的岗位。让后端来写前端不是不行,但只有分工才能达到最高的效率。对于一个正常的业务团队来说,也完全没必要让后端去硬写前端,好几个后端配一个前端,也不是什么事。


就我目前的工作经验来看,后端可以和业务的使用者更近的对接。我们这里的后端开发会和非常多用户对接需求,了解他们的真实使用场景,思考他们背后的需求,可能还能弥补一下产品思考上的不周。和用户对齐数据传递、转换、存储、查询、以及需要不需要定时任务等等,这些后端会去负责。


而前端负责最终的交互,基本可以不用碰到使用者,基本上只需要根据后端给的接口文档,调用接口把内容渲染在表格上即可。碰到用户提反馈一般在于,加载慢(往往是数据请求很慢,但是用户会觉得是前端的问题)、交互不满意(交互美不美真的是一个很难量化的问题,按理说这属于UI的绩效)、数据请求失败(前后端接口对齐虽然体验越来越好,但是开发阶段经常改动还是免不了,最后导致前后端没有同步)。


之前开周会的时候,我基本上说不上什么话。一个是刚转岗,确实不熟。另一个是前端半游离于业务的状态,单纯的把接口内容渲染出来也很难有什么思考,导致开会比较尴尬。基本是后端在谈解决了什么oncall,解决了什么技术问题,有什么业务建设的思考等等。


这次看了别人代码之后非常期盼未来能独立owner一个方向,享受闭环一个小功能的乐趣。


职业安全感


我学的这项技能能够立马投入到工作中进行自我检验,因此我相信自己学的是“有效技能”。我理解的无效技能指学了用不上,然后忘了,花了很多时间精力最后不升职不加薪。之前看李运华大佬的网课《大厂晋升指南》里面有提到,有人花了半年把编译原理这个看似非常重要的计算机基础课学的很扎实,但因为业务不需要,不产生业务价值,也不可能获得提拔的机会。


其实内部全栈化我的理解,还有一个原因,那就是灵活调度。现在这个背景下,老板更希望用有限的人力去做更多事情。有些业务前端过剩了,但是缺后端,这个时候如果直接去招后端,一方面增加成本,再就是没有解决剩的前端,反之也是。在盘点hc的时候就容易出现调整。


多学一些有效技能,提高解决问题的深度、广度,让自己更值钱。我想不管是什么职能,最终都要回归到为业务服务的目标上。


End


写到这里,我依旧在转全栈的路上,只是想给自己一个阶段性的答案。


脱离舒适圈,进入拉伸区,需要付出,需要勇气,也需要把握机遇。给自己多一种可能,去做,去挑战自己不会的。我相信他山之石可以攻玉,越往深处走,就越能触类旁通。


作者:程序员Alvin
来源:juejin.cn/post/7287426666417700919
收起阅读 »

加密的手机号,如何模糊查询?

前言 前几天,知识星球中有位小伙伴,问了我一个问题:加密的手机号如何模糊查询? 我们都知道,在做系统设计时,考虑到系统的安全性,需要对用户的一些个人隐私信息,比如:登录密码、身-份-证号、银彳亍卡号、手机号等,做加密处理,防止用户的个人信息被泄露。 很早之前,...
继续阅读 »

前言


前几天,知识星球中有位小伙伴,问了我一个问题:加密的手机号如何模糊查询?


我们都知道,在做系统设计时,考虑到系统的安全性,需要对用户的一些个人隐私信息,比如:登录密码、身-份-证号、银彳亍卡号、手机号等,做加密处理,防止用户的个人信息被泄露。


很早之前,CSDN遭遇了SQL注入,导致了600多万条明文保存的用户信息被泄。


因此,我们在做系统设计的时候,要考虑要把用户的隐私信息加密保存。


常见的对称加密算法有 AES、SM4、ChaCha20、3DES、DES、Blowfish、IDEA、RC5、RC6、Camellia等。


目前国际主流的对称加密算法是AES,国内主推的则是SM4


无论是用哪种算法,加密前的字符串,和加密后的字符串,差别还是比较大的。


比如加密前的字符串:苏三说技术,使用密钥:123,生成加密后的字符串为:U2FsdGVkX1+q7g9npbydGL1HXzaZZ6uYYtXyug83jHA=


如何对加密后的字符串做模糊查询呢?


比如:假设查询苏三关键字,加密后的字符串是:U2FsdGVkX19eCv+xt2WkQb5auYo0ckyw


上面生成的两个加密字符串差异看起来比较大,根本没办法直接通过SQL语句中的like关键字模糊查询。


那我们该怎么实现加密的手机号的模糊查询功能呢?


1 一次加载到内存


实现这个功能,我们第一个想到的办法可能是:把个人隐私数据一次性加载到内存中缓存起来,然后在内存中先解密,然后在代码中实现模糊搜索的功能。


图片这样做的好处是:实现起来比较简单,成本非常低。


但带来的问题是:如果个人隐私数据非常多的话,应用服务器的内存不一定够用,可能会出现OOM问题。


还有另外一个问题是:数据一致性问题。


如果用户修改了手机号,数据库更新成功了,需要同步更新内存中的缓存,否则用户查询的结果可能会跟实际情况不一致。


比如:数据库更新成功了,内存中的缓存更新失败了。


或者你的应用,部署了多个服务器节点,有一部分内存缓存更新成功了,另外一部分刚好在重启,导致更新失败了。


该方案不仅可能会导致应用服务器出现OOM问题,也可能会导致系统的复杂度提升许多,总体来说,有点得不偿失。


2 使用数据库函数


既然数据库中保存的是加密后的字符串,还有一种方案是使用数据库的函数解密。


我们可以使用MySQL的DES_ENCRYPT函数加密,使用DES_DECRYPT函数解密:


SELECT 
DES_DECRYPT('U2FsdGVkX1+q7g9npbydGL1HXzaZZ6uYYtXyug83jHA=''123')


应用系统重所有的用户隐私信息的加解密都在MySQL层实现,不存在加解密不一致的情况。


该方案中保存数据时,只对单个用户的数据进行操作,数据量比较小,性能还好。


但模糊查询数据时,每一次都需要通过DES_DECRYPT函数,把数据库中用户某个隐私信息字段的所有数据都解密了,然后再通过解密后的数据,做模糊查询。


如果该字段的数据量非常大,这样每次查询的性能会非常差。


3 分段保存


我们可以将一个完整的字符串,拆分成多个小的字符串。


以手机号为例:18200256007,按每3位为一组,进行拆分,拆分后的字符串为:182,820,200,002,025,256,560,600,007,这9组数据。


然后建一张表:


CREATE TABLE `encrypt_value_mapping` (
  `id` bigint NOT NULL COMMENT '系统编号',
  `ref_id` bigint NOT NULL COMMENT '关联系统编号',
  `encrypt_value` varchar(255NOT NULL COMMENT '加密后的字符串'
ENGINE=InnoDB  CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='分段加密映射表'

这张表有三个字段:



  • id:系统编号。

  • ref_id:主业务表的系统编号,比如用户表的系统编号。

  • encrypt_value:拆分后的加密字符串。


用户在写入手机号的时候,同步把拆分之后的手机号分组数据,也一起写入,可以保证在同一个事务当中,保证数据的一致性。


如果要模糊查询手机号,可以直接通过encrypt_value_mapping的encrypt_value模糊查询出用户表的ref_id,再通过ref_id查询用户信息。


具体sql如下:


select s2.id,s2.name,s2.phone 
from encrypt_value_mapping s1
inner join `user` s2 on s1.ref_id=s2.id
where s1.encrypt_value = 'U2FsdGVkX19Se8cEpSLVGTkLw/yiNhcB'
limit 0,20;

这样就能轻松的通过模糊查询,搜索出我们想要的手机号了。


注意这里的encrypt_value用的等于号,由于是等值查询,效率比较高。


注意:这里通过sql语句查询出来的手机号是加密的,在接口返回给前端之前,需要在代码中统一做解密处理。


为了安全性,还可以将加密后的明文密码,用*号增加一些干扰项,防止手机号被泄露,最后展示给用户的内容,可以显示成这样的:182***07


4 其他的模糊查询


如果除了用户手机号,还有其他的用户隐私字段需要模糊查询的场景,该怎么办?


我们可以将encrypt_value_mapping表扩展一下,增加一个type字段。


该字段表示数据的类型,比如:1.手机号 2.身-份-证 3.银彳亍卡号等。


这样如果有身-份-证和银彳亍卡号模块查询的业务场景,我们可以通过type字段做区分,也可以使用这套方案,将数据写入到encrypt_value_mapping表,最后根据不同的type查询出不同的分组数据。


如果业务表中的数据量少,这套方案是可以满足需求的。


但如果业务表中的数据量很大,一个手机号就需要保存9条数据,一个身-份-证或者银彳亍卡号也需要保存很多条数据,这样会导致encrypt_value_mapping表的数据急剧增加,可能会导致这张表非常大。


最后的后果是非常影响查询性能。


那么,这种情况该怎么办呢?
最近我建了新的技术交流群,打算将它打造成高质量的活跃群,欢迎小伙伴们加入。


我以往的技术群里技术氛围非常不错,大佬很多。


image.png


加微信:su_san_java,备注:加群,即可加入该群。


5 增加模糊查询字段


如果数据量多的情况下,将所有用户隐私信息字段,分组之后,都集中到一张表中,确实非常影响查询的性能。


那么,该如何优化呢?


答:我们可以增加模糊查询字段。


还是以手机模糊查询为例。


我们可以在用户表中,在手机号旁边,增加一个encrypt_phone字段。


CREATE TABLE `user` (
  `id` int NOT NULL,
  `code` varchar(20)  NOT NULL,
  `age` int NOT NULL DEFAULT '0',
  `name` varchar(30NOT NULL,
  `height` int NOT NULL DEFAULT '0',
  `address` varchar(30)  DEFAULT NULL,
  `phone` varchar(11DEFAULT NULL,
  `encrypt_phone` varchar(255)  DEFAULT NULL,
  PRIMARY KEY (`id`)
ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='用户表'

然后我们在保存数据的时候,将分组之后的数据拼接起来。


还是以手机号为例:


18200256007,按每3位为一组,进行拆分,拆分后的字符串为:182,820,200,002,025,256,560,600,007,这9组数据。


分组之后,加密之后,用逗号分割之后拼接成这样的数据:,U2FsdGVkX19Se8cEpSLVGTkLw/yiNhcB,U2FsdGVkX1+qysCDyVMm/aYXMRpCEmBD,U2FsdGVkX19oXuv8m4ZAjz+AGhfXlsQk,U2FsdGVkX19VFs60R26BLFzv5nDZX40U,U2FsdGVkX19XPO0by9pVw4GKnGI3Z5Zs,U2FsdGVkX1/FIIaYpHlIlrngIYEnuwlM,U2FsdGVkX19s6WTtqngdAM9sgo5xKvld,U2FsdGVkX19PmLyjtuOpsMYKe2pmf+XW,U2FsdGVkX1+cJ/qussMgdPQq3WGdp16Q。


以后可以直接通过sql模糊查询字段encrypt_phone了:


select id,name,phone
from user where encrypt_phone like '%U2FsdGVkX19Se8cEpSLVGTkLw/yiNhcB%'
limit 0,20;

注意这里的encrypt_value用的like


这里为什么要用逗号分割呢?


答:是为了防止直接字符串拼接,在极端情况下,两个分组的数据,原本都不满足模糊搜索条件,但拼接在一起,却有一部分满足条件的情况发生。


当然你也可以根据实际情况,将逗号改成其他的特殊字符。


此外,其他的用户隐私字段,如果要实现模糊查询功能,也可以使用类似的方案。


最后说一句,虽说本文介绍了多种加密手机号实现模糊查询功能的方案,但我们要根据实际业务场景来选择,没有最好的方案,只有最合适的。


作者:苏三说技术
来源:juejin.cn/post/7288963208408563773
收起阅读 »

为什么说程序员到国企就废了?

事实上,单纯技术上来说,确实程序员进国企就废了。虽然我也是在央企,但是这确实是一件很无奈也很真实的事情,广大想来国企的小伙伴们也做好心理准备。 倒不是因为什么技术栈老旧这种广泛传播老掉牙的原因,而是一种工作气氛、工作压力、项目规模和大家吐槽最多的”工作压力“方...
继续阅读 »

事实上,单纯技术上来说,确实程序员进国企就废了。虽然我也是在央企,但是这确实是一件很无奈也很真实的事情,广大想来国企的小伙伴们也做好心理准备。


倒不是因为什么技术栈老旧这种广泛传播老掉牙的原因,而是一种工作气氛、工作压力、项目规模和大家吐槽最多的”工作压力“方面的原因。


首先,我本身互联网和央企都呆过,对于两者的工作环境和工作模式等是比较有发言权的。


由于互联网大多是to C项目,面向的是广大用户,是非常有竞争压力的,因为市场上同类产品的数目很多,如果你的代码出现了bug,那么影响到的可就是成千上万的普通用户。


就像微博崩溃了,在整个中国至少几亿人知道,那么你产生一个bug的成本是极大的,因此整体上来说互联网对代码质量的要求就非常高。


相对应来说的就是程序员个人的压力会极大,你必须确保你写的代码的严谨性,不能出现哪怕任何一点儿线上问题,否则等待你的可能就是辞退和担责。


同样也是上面的原因,由于你写的每一行代码都是需要经历成千上万人的使用来检验的,你和你的同时在写每一行代码的时候考虑的场景和风险就会更加全面,而不是简单的curd来完成业务即可。


并且,软件的复杂程度也是与软件的用户使用数量成正比的,使用的用户量越大,你的软件复杂性越高,你所需要解决的问题就越多,技术上涉及到的深度也更深,所以你在互联网企业中编程能力提升得会更快。


但但但 。。。是,并不是呆在互联网的所有程序员能力上都会有提升的,前面我说的这些都是互联网中的那群写核心代码的程序员,在别人框架上修修改改干些搬砖的活儿的人不在其中。


因为,大多数的大厂都有着自己的技术建设团队,会设计一堆自己内部用的工具,哪怕这种工具市面上已经有了也要自己造。长期在这种环境下,干使用别人框架的活儿,但是不继续学习承担更重要工作的人也是挺惨的,而且大概率会被大厂所淘汰。


但是国企的逻辑跟上面互联网的底层逻辑完全不同,互联网的项目大多数要拿到市场上去经历残酷的厮杀,只有做得最好的产品才能够最终活下来,获得垄断地位。


而国企的软件项目一般是一些集团的内部项目,或者有一些作为乙方为其它公司开发的项目也是常年合作下来的项目。


就如同中石油、中国五矿这样的集团内部的智慧化建设项目,本质上是不存在竞争的,因为即使我的软件开发部分可以外包出去或者找外包人员来做,但是这个软件必须得是你们集团牵头来做的。


包括版权和数据什么的最终必须属于你们的集团,而且大领导也热衷于将企业整体的智慧化建设作为工作成果向上汇报 。


所以这也就决定了,国企很多的软件是不太会面对竞争问题的,比如你很难想象让一个私企来做油田的智慧化管理软件。


因此,目前来说石油大体上还是垄断的,油田建设也只有中石油、中石化集团的相关公司可以进行建设,配套的软件你让私企自己来做个产品也是空中楼阁,做出来的东西也不一定能用。


但是,正是由于软件项目没有生存压力,并且国企的正式员工只要不犯下原则性问题,如违法犯罪,泄露公司机密、造成巨大的生产事故之类的重大问题,一般也是不会被轻易开除的,但是也可能存在不续约的情况发生。


因此,呆在国企的程序员就像温室里面的花朵一样,没有动力去优化问题,去采用新的技术,因为你代码写得再好也并不会给你更多的晋升机会,所以长期下去国企程序员的技术上确实就是惨不忍睹了。


其实,我刚从互联网大厂那边跳过来的时候,心理还极度不平衡,倒不是因为国企工资给得有多么低,而是我一直耿耿于怀的是我呆的那个大厂给的钱真的太少了。


但是,当我今年和一些工作好多年的成都普通程序员交流之后,我才发现了我作为校招生进入大厂之后整个人的狂妄与无知。


前一段时间,很多人看了我分享的去5A级景区写代码的blog之后,很多人都找到我问我们公司是否还有HC,是否还能够内推,其中一个哥们令我印象深刻。


他是西南某一本毕业的本科生,从学校毕业现在已经3~4年,也是Java后台开发,一直也是在一些中小型企业里面打转转。


和他聊的时候,才发现毕业短短3~4年他已经换过4~5份工作了,而且工资才渐渐的从当初刚毕业的几千块涨到现在的1w左右。


所以,怎么说呢,对于广大普通程序员来说,近几年的主题是活下来,如果这个时候能够有个地方能让你待到春暖花开的时候,也未尝不是件好事情。


至于技术,废了就废了吧,本质上来说job和career是两回事儿,job也就是“just over break”,每一个打工人,无论在国企还是私企都必须开启自己的”career“,而不是一直打工下去,这就是我的人生信条。


作者:浣熊say
来源:juejin.cn/post/7327724945761452042
收起阅读 »

uniapp云开发--微信登录

web
前言 我们主要使用uniapp + uniCloud 实现小程序登录,并将用户数据存入云数据库。 小程序 wx.getUserProfile 调整,接口将被收回 详情,所以需要用户自己填写资料。 注:填写个人资料是一个组件,覆盖在登录之上而已,还是在同一个页面...
继续阅读 »

前言


我们主要使用uniapp + uniCloud 实现小程序登录,并将用户数据存入云数据库。


小程序 wx.getUserProfile 调整,接口将被收回 详情,所以需要用户自己填写资料。


注:填写个人资料是一个组件,覆盖在登录之上而已,还是在同一个页面



uniCloud


创建 uniapp + uniCloud 项目,创建云数据库 数据表 uniCloud传送门


开始


创建项目


39d23acf47b440e2880f5ccadc1417f9~tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.webp


关联云服务空间




创建云数据库 数据表


不使用模版,输入名称直接创建即可。



编辑表结构,想了解更多可以去看云数据库 DB Schema 数据结构文档 传送门


{
"bsonType": "object",
"required": [],
"permission": {
"read": true,
"create": true,
"update": true,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"nickName": {
"bsonType": "string",
"label": "昵称",
"description": "用户昵称,登录获取的"
},
"avatarUrl": {
"bsonType": "string",
"label": "头像",
"description": "用户头像图片的 URL,登录获取的"
},
"gender": {
"bsonType": "number",
"label": "性别",
"description": "用户性别,1: 男;2: 女"
},
"personalize": {
"bsonType": "string",
"label": "个性签名",
"description": "个性签名,编辑资料获取"
},
"background": {
"bsonType": "object",
"label": "个人中心背景图",
"description": "个人中心背景图,编辑资料获取"
},
"mp_wx_openid": {
"bsonType": "string",
"description": "微信小程序平台openid"
},
"register_date": {
"bsonType": "timestamp",
"description": "注册时间",
"forceDefaultValue": {
"$env": "now"
}
}
}
}

创建云函数




云函数代码


云函数 将 uni.login 取得的 code 获取到用户 session, 并对 数据库进行 增加、修改、查询 操作,第一次注册必须用户主动填写用户资料。


对云数据库的相关操作 传送门


'use strict';

//小程序的AppID 和 AppSecret
const mp_wx_data = {AppID: '************', AppSecret: '***********************'}

//event为客户端上传的参数
exports.main = async (event, context) => {

//使用云数据库
const db = uniCloud.database();
// 获取 `users` 集合的引用
const pro_user = db.collection('users');
// 通过 action 判断请求对象

let result = {};
switch (event.action) {
// 通过 code 获取用户 session
case 'code2Session':
const res_session = await uniCloud.httpclient.request('https://api.weixin.qq.com/sns/jscode2session', {
method: 'GET', data: {
appid: mp_wx_data.AppID,
secret: mp_wx_data.AppSecret,
js_code: event.js_code,
grant_type: 'authorization_code'
}, dataType: 'json'
}
)
const success = res_session.status === 200 && res_session.data && res_session.data.openid
if (!success) {
return {
status: -2, msg: '从微信获取登录信息失败'
}
}

//从数据库查找是否已注册过
const res_user = await pro_user.where({
mp_wx_openid: res_session.data.openid
}).get()
// 没有用户信息,进入注册
if (res_user.data && res_user.data.length === 0) {
//event.user_info 用户信息
if (event.user_info) {
//有信息则进入注册,向数据库写入数据
const register = await uniCloud.callFunction({
name: 'user',
data: {
action: 'register',
open_id: res_session.data.openid,
user_info: event.user_info
}
}).then(res => {
result = res
})
} else {
//没有信息返回{register: true}
result = {
result: {
result: {register: true}
}
}
}
} else {
result = {
result: {
result: res_user.data[0]
}
}
}
break;
//注册 向数据库写入数据
case 'register':
const res_reg = await pro_user.add({
nickName: event.user_info.nickName,
avatarUrl: event.user_info.avatarUrl,
gender: event.user_info.gender,
mp_wx_openid: event.open_id,
register_date: new Date().getTime()
})
if (res_reg.id) {
const res_reg_val = await uniCloud.callFunction({
name: 'user', data: {
action: 'getUser', open_id: event.open_id
}
}).then(res => {
result = res
})
} else {
result = {
status: -1, msg: '微信登录'
}
}
break;
case 'update':
if (event._id && event.info) {
const res_update = await pro_user.doc(event._id).update(event.info)
if (res_update.updated >= 0) {
result = {status: 200, msg: '修改成功'}
} else {
result = {status: -1, msg: '修改失败'}
}
} else {
result = {status: -1, msg: '修改失败'}
}
break;
case 'getUser':
const res_val = await pro_user.where({
mp_wx_openid: event.open_id
}).get()
return res_val.data[0]
break;
}
return result;
};

微信登录操作


如上面所说,用户需手动上传资料,对于用户头像我们需要上传至云储存。


上传用户头像


上传图片函数参数为微信本地图片路径,我们对路径用/进行分割,取最后的图片名称进行上传


/**
* 上传图片至云存储
*/

export async function uploadImage(url) {
const fileName = url.split('/')
return new Promise(resolve => {
uniCloud.uploadFile({
filePath: url,
cloudPath: fileName[fileName.length - 1],
success(res) {
resolve(res)
},
fail() {
uni.showToast({
title: '图片上传失败!',
icon: 'none'
})
resolve(false)
}
})
})
}

登录函数


如果用户第一次上传资料,我们需要先上传头像并取得图片链接,再将用户资料写入数据库。


async wxLogin() {
if (this.userInfo && this.userInfo.avatarUrl) {
uni.showLoading({
title: '正在上传图片...',
mask: true
});
//上传头像至云储存并返回图片链接
const imageUrl = await uploadImage(this.userInfo.avatarUrl)
if (!imageUrl) {
return
}
this.userInfo = {...this.userInfo, avatarUrl: imageUrl.fileID}
}
uni.showLoading({
title: '登陆中...',
mask: true
});
const _this = this
uni.login({
provider: 'weixin',
success: (res) => {
if (res.code) {
//取得code并调用云函数
uniCloud.callFunction({
name: 'user',
data: {
action: 'code2Session',
js_code: res.code,
user_info: _this.userInfo
},
success: (res) => {
//如register为true,用户未填写资料
if (res.result.result.result.register) {
//_this.showUserInfo 显示填写资料组件
_this.showUserInfo = true
uni.hideLoading();
return
}
if (res.result.result.result._id) {
const data = {
_id: res.result.result.result._id,
mp_wx_openid: res.result.result.result.mp_wx_openid,
register_date: res.result.result.result.register_date
}
this.loginSuccess(data)
}
},
fail: () => {
this.loginFail()
}
})
}
}
})
},

登录成功与失败


在用户登录成功后将数据存入 Storage 中,添加登录过期时间,我这里设置的是七天的登录有效期。


loginSuccess(data) {
updateTokenStorage(data)
updateIsLoginStorage(true)
uni.showToast({
title: '登陆成功!',
icon: 'none'
});
uni.navigateBack()
},

将用户数据存入 Storage,并设置过期时间 expiresTime


export function updateTokenStorage(data = null) {
if (data) {
const expiresTime = new Date().getTime() + 7 * 24 * 60 * 60 * 1000
data = {...data, expiresTime: expiresTime}
}
uni.setStorageSync('user', data)
}

isLogin 用于判断是否是否登录


export function updateIsLoginStorage(data = null) {
uni.setStorageSync('isLogin', data)
}

登录失败


loginFail() {
updateTokenStorage()
updateIsLoginStorage()
uni.showToast({
title: '登陆失败!',
icon: 'none'
});
}

判断是否登录


除了判断 isLogin 还要判断 expiresTime 是否登录过期


//判断是否登陆
export function isLogin() {
try {
const user = uni.getStorageSync('user')
const isLogin = uni.getStorageSync('isLogin')
const nowTime = new Date().getTime()
return !!(isLogin && user && user._id && user.expiresTime > nowTime);
} catch (error) {

}
}

最后


至此就实现了微信登录并将用户信息存入数据库中,我们也可以通过云函数获取用户数据,做出用户个人主页。



以上是我做个人小程序时用的登录流程,整个小程序项目已上传至 GitHub。


GitHub地址


小程序码



作者:Biao
来源:juejin.cn/post/7264592481592705076
收起阅读 »

真的不考虑下grid布局?有时候真的很方便!

web
前言 flex布局大家应该已经运用的炉火纯青了,相信在日常开发中大家和我一样不管遇到什么都是flex一把搜哈。直到我遇到grid,才发现有些场景下,不是说flex实现不了而是使用grid能够更加轻松的完成任务。下面拿几个场景和大家分享一下。 宫格类的布局 比如...
继续阅读 »

前言


flex布局大家应该已经运用的炉火纯青了,相信在日常开发中大家和我一样不管遇到什么都是flex一把搜哈。直到我遇到grid,才发现有些场景下,不是说flex实现不了而是使用grid能够更加轻松的完成任务。下面拿几个场景和大家分享一下。


宫格类的布局


比如我要实现一个布局,最外层元素的宽度是1000px,高度自适应。子元素宽度为300px,一行展示3个,从左到右排列。其中最左边与最右边的元素需要紧挨父元素的左右边框。如下图所示:



使用flex实现


这个页面布局在日常开发中非常常见,考虑下使用flex布局如何实现,横向排列元素,固定宽度300,wrap设置换行显示,设置双端对齐。看起来很简单,来实现一下。


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

<head>
<style>
.box{
width: 1000px;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 10px;
}
.item{
background: pink;
width: 300px;
height: 150px;
}
</style>
</head>

<body>
<div class="box">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item">5</div>
<div class="item">6</div>
<div class="item">7</div>
<div class="item">8</div>
</div>
</body>

</html>


实现之后发现了问题,由于我们设置了双端对齐导致,当最后一行的个数不足三个时,页面展示的效果和我们预期的效果有出入。使用flex实现这个效果就要对这个问题进行额外的处理。


处理的方式有很多种,最常见的处理方式是在元素后面添加空元素,使其成为3的倍数即可。其实这里添加空元素的个数没有限制,因为空元素不会展示到页面上,即使添加100个空元素用户也是感知不到的。个人觉得这并不是一个好办法,在实际处理的时候可能还会遇到别的问题。个人觉得还是把flex下的子元素设置成百分比好一点。


使用grid实现


面对这种布局使用grid是非常方便的,设置3列,每列300px,剩下的元素让它自己往下排即可。几行代码轻松实现该效果,不需要flex那样额外的处理。


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

<head>
<style>
.box {
display: grid;
grid-template-columns: repeat(3, 300px);
justify-content: space-between;
gap: 10px;
width: 1000px;
}

.item {
background: pink;
height: 100px;
}
</style>
</head>

<body>
<div class="box">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item">5</div>
<div class="item">6</div>
<div class="item">7</div>
<div class="item">8</div>

</div>
</body>

</html>


实现后台管理布局



这种后台管理的布局,使用flex实现当然也没有问题。首先需要纵向排列红色的两个div,然后再横向的排列蓝色的两个div,最后再纵向的排列绿色的两个div实现布局。达到效果是没有问题的,但是实现起来较为繁琐,而且需要很多额外的标签嵌套。



由于grid是二维的,所以它不需要额外的标签嵌套。html里面结构清晰,如果需要改变页面结构,只需要改变container的样式就可以了,不需要对html进行修改。


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

<head>
<style>
.container {
display: grid;
grid-template-columns: 250px 1fr;
grid-template-rows: 100px 1fr 100px;
grid-template-areas:
'header header'
'aside main'
'aside footer';
height: 100vh;
}

.header {
grid-area: header;
background: #b3c0d1;
}

.aside {
grid-area: aside;
background: #d3dce6;
}

.main {
grid-area: main;
background: #e9eef3;
}

.footer {
grid-area: footer;
background: #b3c0d1;
}
</style>
</head>

<body>
<div class="container">
<div class="header">Header</div>
<div class="aside">Aside</div>
<div class="main">Main</div>
<div class="footer">Footer</div>
</div>
</body>

</html>

实现响应式布局


借助grid的auto-fillminmax函数可以实现类似响应式布局的效果,可以应用在后台管理的表单布局等场景。



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

<head>
<style>
.box {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
justify-content: space-between;
gap: 10px;
}

.item {
background: pink;
height: 100px;
}
</style>
</head>

<body>
<div class="box">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item">5</div>
<div class="item">6</div>
<div class="item">7</div>
<div class="item">8</div>
</div>
</body>

</html>

兼容性对比


flex的兼容性


image.png


grid的兼容性


image.png


可以看到grid在兼容性上还是不如flex,grid虽然强大,但是在使用前还是需要先考虑一下项目的用户群体。


结尾


除了上述场景外肯定还有许多场景适合使用grid来完成。gridflex都是强大的布局方式,它们并没有明显的优劣之分。关键在于掌握这两种方法,并在开发中根据实际情况选择最合适的方案。


希望大家能有所收获!


作者:欲买炸鸡同载可乐
来源:juejin.cn/post/7326816030042669110
收起阅读 »

一些不被人熟知,但又很好用的HTML属性

web
HTML(超文本标记语言)具有多种属性,可用于增强我们的网页的结构和功能。 下面我就给大家介绍一下,一些很好用的HTML属性,但是不被人熟知的HTML属性 contenteditable: 这个属性使我们的元素变的可编辑。用户可以直接在我们的浏览器中修改元素的...
继续阅读 »

HTML(超文本标记语言)具有多种属性,可用于增强我们的网页的结构和功能。
下面我就给大家介绍一下,一些很好用的HTML属性,但是不被人熟知的HTML属性


contenteditable:


这个属性使我们的元素变的可编辑。用户可以直接在我们的浏览器中修改元素的内容。


<div contenteditable="true">
这段内容可以被编辑。
</div>

使用场景:
-可以用来创建富文本编辑器,使用户能够在网页中创建、编辑和格式化文本,


spellcheck:


该属性用于启用或禁用元素的拼写检查功能。(如果用户输入的单词拼写有误,浏览器通常会标记出来并提供纠正建议)


<textarea spellcheck="true">
这个文本区域启用了拼写检查。
</textarea>

image.png


使用场景:



  • 可以在文章创作者的富文本编辑器中使用,辅助文章创作


代码演示:


draggable:


该属性使元素可拖动。通常与 JavaScript 结合使用,实现拖放功能。


<img src="image.jpg" draggable="true" alt="可拖动的图片">

使用场景:



  • 在电子商务网站中,用户可以拖动产品图像到购物车区域,以便快速添加商品到购物清单。

  • 在可视化数据分析工具中,用户可以通过拖拽图表或数据元素来定制自己的数据可视化图形。

  • 可以创建一个可拖放的低代码平台


代码演示:


sandbox:


与 元素一起使用,sandbox 属性限制了嵌入内容的行为,如阻止执行脚本或提交表单。

<iframe src="sandboxed-page.html" sandbox="allow-same-origin allow-scripts"></iframe>

使用场景:



  • 可以在电子邮件客户端中,通过使用 sandbox 属性限制电子邮件中嵌入内容的行为,以确保安全性并防止恶意代码执行。

  • 可以在需要嵌入第三方内容(如广告、外部应用程序等)但又需要限制其行为的情况下使用。这可以防止嵌入的内容执行恶意脚本或访问敏感信息。


download:


该属性与 <a>(锚点)元素一起使用,指定用户单击链接时应下载的目标。


<a href="document.pdf" download="my-document">下载 PDF</a>

使用场景:



  • 可用于提供下载链接,例如下载文档、图像或其他文件。这使得用户可以通过单击链接直接下载相关内容而无需离开页面。


hidden:


该属性用于隐藏页面上的元素。这是最初隐藏内容的简单方法,可以通过 CSS 或 JavaScript 在后来显示。


<p hidden>这个段落最初是隐藏的。</p>

使用场景:



  • 在网页中使用弹出式模态框或折叠式面板,可以利用 hidden 属性来最初隐藏它们,并在用户点击或触发特定事件时展现。

  • 在网页表单验证中,可以将错误消息初始隐藏,只有当用户提交表单出现错误时才显示出来。


defer:



<script defer src="myscript.js"></script>

使用场景:



  • 在网页底部延迟加载 JavaScript 脚本,以确保页面内容首先加载,提升页面加载速度和性能。

  • 对于异步加载的脚本(async),适用于对网页影响较小的辅助性 JavaScript 脚本,例如网页分析工具或跟踪代码。


async:


类似于 defer,async 属性与

<script async src="myscript.js"></script>

使用场景:



  • 在网页底部延迟加载 JavaScript 脚本,以确保页面内容首先加载,提升页面加载速度和性能。

  • 对于异步加载的脚本(async),适用于对网页影响较小的辅助性 JavaScript 脚本,例如网页分析工具或跟踪代码。


Accept 属性:


你可以将 accept 属性与 元素(仅适用于文件类型)一起使用,以指定服务器可以接受的文件类型。


<input type="file" accept=".jpg, .jpeg, .png">

使用场景:



  • 在上传图片的社交媒体平台中,限制用户只能上传特定格式(如 JPG、PNG)的图片文件,确保图片质量和页面加载速度。

  • 在在线应用程序中,限制用户只能上传特定类型的文件,例如在云存储服务中只允许上传文档文件。


Translate:


该属性用于指定在页面本地化时,元素的内容是否应该被翻译。


<p translate="no">这段内容不应被翻译。</p>

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

Celeris Web,一套女生都觉得好看的Vue3模板

web
Vue3+Unocss+NaiveUI+Monorepo搭建一套女生觉得好看的前端模板 一年前,我刚刚从后端转入前端的大门,兴奋又迷茫。身边的女性朋友们总是找我帮忙写小工具,但每次都被吐槽UI太丑了。于是,我想,能不能搞点不一样的? 嗯,女生总是很喜欢漂亮的东...
继续阅读 »

Vue3+Unocss+NaiveUI+Monorepo搭建一套女生觉得好看的前端模板


一年前,我刚刚从后端转入前端的大门,兴奋又迷茫。身边的女性朋友们总是找我帮忙写小工具,但每次都被吐槽UI太丑了。于是,我想,能不能搞点不一样的?


嗯,女生总是很喜欢漂亮的东西,对吧?于是我决定写一款前端开发模板,让开发出来的工具她们用起来不仅方便,还得有点美美哒。Vue 3、Unocss、NaiveUI、Monorepo,这些都是我的秘密武器。我取名它为Celeris Web


这个开发框架采用了最新的技术,包括Vue 3、Vite和 TypeScript。而且,这个项目的设计初衷就用了monorepo的方法使得依赖管理和多个项目的协作变得轻松。这可是一套为开发人员提供了构建现代Web应用程序的全面解决方案哦。


不管你是老手还是新手,Celeris Web都能给你提供一个简化的前端开发流程,利用最新的工具和技术。是不是觉得很吸引人?


Snipaste_2024-01-16_14-27-03.png


Celeris Web的特点



  • ⚡ 闪电般快速:使用Vue 3,Vite和pnpm构建 🔥

  • 💪 强类型:使用TypeScript 💻

  • 📂 单库存储:易于管理依赖项和协作多个项目 🤝

  • 🔥 最新语法:使用新的< script setup >语法 🆕

  • 📦 自动导入组件:自动导入组件 🚚

  • 📥 自动导入API:使用unplugin-auto-import直接导入Composition API和其他API 📨

  • 💡 官方路由器:使用Vue Router v4 🛣️

  • 🎉 加载反馈:使用NProgress提供页面加载进度反馈 🔄

  • 🍍 状态管理:使用Pinia进行状态管理 🗃️

  • 📜 中文字体预设:包含中文字体预设 🇨🇳

  • 🌍 国际化就绪:具备使用本地化的国际化功能 🌎

  • ☁️ Netlify准备就绪:在Netlify上零配置部署 ☁️


有了Celeris Web,你的前端开发之路将更加轻松愉快!🚀


中英文双语注释


在Celeris Web的设计中,我们注重代码的可读性和学习性,为此,我们为每个函数都配备了中英文双语注释,以确保无论您的母语是中文还是英文,都能轻松理解和学习代码。


为什么选择中英文双语注释?



  1. 全球协作: 在多语言团队中,中英文双语注释能够促进更好的沟通和协作,确保团队成员都能准确理解代码的功能和实现。

  2. 学习便捷: 对于新手来说,中英文双语注释提供了更友好的学习环境,帮助他们更快速地掌握代码的逻辑和结构。

  3. 开发者友好: 我们致力于构建一个开发者友好的开发环境,中英文双语注释是我们为实现这一目标而采取的一项关键措施。

  4. 示例:


    /**
    * 打开一个新的浏览器窗口
    * Open a new browser window
    *
    * @param {string} url - 要在新窗口中打开的 URL
    * The URL to open in the new window
    *
    * @param {object} options - 打开窗口的选项
    * Options for opening the window
    * @param {string} options.target - 新窗口的名称或特殊选项,默认为 "_blank"
    * @param {string} options.features - 新窗口的特性(大小,位置等),默认为 "noopener=yes,noreferrer=yes"
    */

    export function openWindow(url: string, { target = "_blank", features = "noopener=yes,noreferrer=yes" }: {
    target?: "_blank" | "_self" | "_parent" | "_top"; // 新窗口的名称或特殊选项,默认为 "_blank"
    features?: string; // 新窗口的特性(大小,位置等),默认为 "noopener=yes,noreferrer=yes"
    } = {}
    ) {
    window.open(url, target, features);
    }

    通过这样的中英文双语注释,我们希望为开发者提供更愉悦、更高效的编码体验,让Celeris Web成为一个真正容易上手和深入学习的前端模板。



Monorepo 设计的好处


1. 依赖管理更轻松: Monorepo 将所有项目的依赖项集中管理,避免了不同项目之间版本冲突的问题,使得整体的依赖管理更加清晰和简便。


2. 代码共享与重用: 不同项目之间可以方便地共享和重用代码,减少重复开发的工作量。这对于保持代码一致性和提高开发效率非常有利。


3. 统一的构建和部署: Monorepo 可以通过统一的构建和部署流程,简化整个开发过程,减少了配置和管理的复杂性,提高了开发团队的协作效率。


4. 统一的版本控制: 所有项目都在同一个版本控制仓库中,使得版本管理更加一致和可控。这有助于团队协同开发时更好地追踪和处理版本问题。 Monorepo设计让Celeris Web不仅是一款后台管理系统模板,同时也是一个快速开发C端产品的前端Web模板。有了Celeris Web,前端开发之路将更加轻松愉快!🚀


设计理念:突破Admin管理的局限性,关注C端用户体验


在市面上,大多数前端模板都着眼于满足B端用户的需求,为企业管理系统(Admin)提供了强大的功能和灵活的界面。然而,很少有模板将C端产品的特点纳入设计考虑,这正是我们Celeris Web的创新之处。


突破Admin管理的局限性:


传统的Admin管理系统更注重数据展示和业务管理,但C端产品更加侧重用户体验和视觉吸引力。我们深知C端用户对于界面美观、交互流畅的要求,因此Celeris Web不仅提供了强大的后台管理功能,更注重让前端界面在用户层面上达到更高水平。


关注C端用户体验:


Celeris Web不仅仅是一个后台管理系统的模板,更是一个注重C端用户体验的前端Web模板。我们致力于打破传统Admin系统的束缚,通过引入崭新的设计理念,使得C端产品在前端呈现上具备更为出色的用户体验。


特色亮点:



  • 时尚美观的UI设计: 我们注重界面的美感,采用现代化设计语言,使得Celeris Web的UI不仅仅是功能的堆砌,更是一种视觉盛宴,让C端用户爱不释手。

  • 用户友好的交互体验: 考虑到C端用户的习惯和需求,Celeris Web注重交互体验的设计,通过流畅的动画效果和直观的操作,使用户感受到前所未有的愉悦和便捷。

  • 个性化定制的主题支持: 我们理解C端产品的多样性,因此提供了丰富的主题定制选项,让每个C端项目都能拥有独一无二的外观,更好地满足产品个性化的需求。


通过这一独特的设计理念,Celeris Web致力于在前端开发领域探索全新的可能性,为C端产品注入更多活力和创意。我们相信,这样的创新将带来更广泛的用户认可和更高的产品价值。在Celeris Web的世界里,前端不再局限于Admin系统,而是融入了更多关于用户体验的精彩元素。


后期发展路线:瞄准AIGC,引领互联网产品变革


随着人工智能与图形计算(AIGC)技术的崛起,我们决定将Celeris Web的发展方向更加专注于推动AIGC相关产品的研发和落地。这一战略决策旨在顺应互联网产品的变革浪潮,为未来的科技创新开辟全新的可能性。


AIGC技术引领变革:


AIGC的兴起标志着互联网产业迎来了一场技术变革,为产品带来更加智能、交互性更强的体验。Celeris Web将积极响应这一变革,致力于为开发者提供更优秀的工具,助力他们在AIGC领域创造更具前瞻性的产品。


模板的研发重心:


在后期的发展中,Celeris Web将更加重视AIGC相关产品的研发需求。我们将推出更多针对人工智能的功能模块,使开发者能够更便捷、高效地构建出色的AIGC应用。


专注产品落地:


除了技术研发,我们将加强对AIGC产品落地的支持。通过提供详实的文档、示例和定制化服务,Celeris Web旨在帮助开发者更好地将AIGC技术融入他们的实际项目中,实现技术创新与商业应用的有机结合。


开放合作生态:


为了推动AIGC技术的更广泛应用,Celeris Web将积极构建开放合作生态。与行业内优秀的AIGC技术提供商、开发者社区保持密切合作,共同推动AIGC技术的发展,携手打造更加繁荣的互联网产品生态圈。


Celeris Web未来的发展将以AIGC为核心,我们期待在这个快速发展的技术领域中,与开发者们一同探索、创新,共同引领互联网产品的未来。通过持续的努力和创新,Celeris Web将成为AIGC领域的引领者,助力开发者创造更加智能、引人入胜的互联网产品。


源码


kirklin/celeris-web (github.com)


作者:KirkLin
来源:juejin.cn/post/7324334380373688371
收起阅读 »

4天卖600万份的爆款游戏《幻兽帕鲁》,真的是AI缝合怪吗

AI一天,人间一年。我是卷福同学,一个在福报厂修过福报的程序员。现在专注AI领域整活分享 大家好啊,最近2天,Steam上出了一款非常火的游戏《幻兽帕鲁》。到今天1月24日,才过去4天。它就卖出超过600万份,最高时180万人同时在线,直接登上Steam的热...
继续阅读 »

AI一天,人间一年。我是卷福同学,一个在福报厂修过福报的程序员。现在专注AI领域整活分享



大家好啊,最近2天,Steam上出了一款非常火的游戏《幻兽帕鲁》。到今天1月24日,才过去4天。它就卖出超过600万份,最高时180万人同时在线,直接登上Steam的热销游戏和最热游戏榜首。


1.png


这个成绩,放在Steam游戏史上甚至赶超了前段时间特火的《完蛋!我被美女包围了!》


同时,在玩家近7万的评测里,高达93%的评价都是好评


2.png


到底是一款什么样的游戏能拿到这样的成绩呢?


游戏内容


0.png


游戏世界是类似《塞尔达传说 旷野之息》那样的开发探索世界,玩家可以在庞大的世界里收集各种各样的幻兽,而幻兽借鉴了任天堂《宝可梦》这个大IP中的神奇生物系统,通过AI缝合而成的。


玩家们可以在游戏里找到各种熟悉的宝可梦的影子。


4.png


5.png


在游戏里,玩家可以进行开放世界探索、宝可梦式抓幻兽、第三人称射击战斗、生存建造房屋、养成宠物等各种玩法,不同的玩家都能在里面找到属于自己的乐趣。


6.png


7.png


现在已有小学生玩家体会到了在游戏当老板,压榨帕鲁的乐趣,还总结出一套帕鲁圣经:



缝合怪,但是全缝了


让人难以想象的是,这样一款现象级的爆款游戏在项目开始时只有10人,由小作坊Pocketpair开发。而这10个人也不是专业开发游戏的,而是Pocketpair的社长在网上发掘的野生的零经验的爱好者。


相关信息可以在社长Takuro Mizobe的推特上找到,置顶是1月16日社长写给玩家们的公开信。从信中,可以找到游戏开发设计过程中的很多细节


1.游戏里的枪械动作是社长在网上找的一个爱好者做的


9.png


(PS:没事咱也上传自己做手工的视频,说不定哪天就被伯乐挖掘了)


2.帕鲁的美术师在最初在推特上应聘时被拒绝,而且出图速度惊人


美术师是个应届生,曾应聘过上百家公司,都被拒绝了。社长表示,她是一个罕见的人才,出图速度是其他原画师的四五倍(注意这句),也因为有了这位美术师的加入,现在的游戏里才有了100种帕鲁。


10.png


11.png


另外给大家说一下熟知的《怪物猎人世界》游戏里的怪物类型也才50种。


如此惊人的出图速度,以及反馈修改,一分钟内就能修改完成,很难让人不怀疑其中有AI的参与。


社长Takuro Mizobe自身就是生成式AI的拥护者,早期在推特上就有分享过用AI制作游戏的动态。几乎可以实锤AI缝合怪的传闻了!


如何玩《幻兽帕鲁》


游戏在Steam上上架的,需要先安装Steam,然后游戏售价现在有优惠,是168港币,折合人民币为152元钱。冲着这几天爆火的程度,还是值得入手玩一玩的。


12.png


但是因为游戏实在太火爆了,官方服务器已经支撑不了这么多玩家了。好在官方提供了自建游戏服务器的方法,也就是你可以在云服务器上,甚至自己电脑上搭建幻兽帕鲁的服务器,然后游戏客户端登录就行。


甚至可以搭建个局域网服务器,约着几个好朋友一起在游戏世界里探索。


13.png


小卷已经给大家整理了云服务器部署幻兽帕鲁服务端的教程。在我的公众号内发关键词幻兽帕鲁领取


作者:卷福同学
来源:juejin.cn/post/7327538517528772618
收起阅读 »

揭秘 "mitt" 源码:为什么作者钟情于 `map` 而放弃 `forEach`

web
故事是这样的,半年前我提交了一个 Pull Request(PR),想要将作者在代码中使用的 map 改成 forEach, 而作者的回应却是:map() is used because it is 3 bytes smaller when gzipped. ...
继续阅读 »

故事是这样的,半年前我提交了一个 Pull Request(PR),想要将作者在代码中使用的 map 改成 forEach


而作者的回应却是:map() is used because it is 3 bytes smaller when gzipped. (使用 map 是因为在 Gzip 压缩时可以减小 3 字节的体积。)


咦?为什么会这样呢?


"mitt" 简介


首先,让我们认识一下 "mitt",它是一只小巧灵活的事件发射器(event emitter)库,体积仅有 200 字节,但功能强大。这个小家伙在项目中充当了事件的传播者,有点像是一个小型的邮差,把消息传递给需要它的地方。


developit/mitt: 🥊 Tiny 200 byte functional event emitter / pubsub. (github.com)


作者的选择:map vs forEach


在源码中,我们发现作者选择使用了 Array.prototype.map(),这是一个处理数组每个元素并返回新数组的函数。然而,有趣的地方在于,作者并没有在 map 中返回任何值。这和我对 map 的期望有些出入,因为我们习惯于用它生成一个新的数组。


代码的细微变化


曾经,代码片段是这样的,作者想要用 map 来执行一些操作,但却不生成新数组。


if (handlers) {
(handlers as EventHandlerList<Events[keyof Events]>)
.slice()
.map((handler) => {
handler(evt!);
});
}

我希望修改成这样:


if (handlers) {
(handlers as EventHandlerList<Events[keyof Events]>)
.slice()
.forEach((handler) => {
handler(evt!);
});
}

所以我很快就交了个PR:将map改成了forEach,经过了几个月的等待,PR被拒了,作者的回应是:map() is used because it is 3 bytes smaller when gzipped.(使用 map 是因为在 Gzip 压缩时可以减小 3 字节的体积。)


code.png


pr.png


小技巧背后的逻辑


虽然 map 通常用于生成新数组,但作者在这里使用它更像是在借助压缩的优势,让代码更轻量。


大小对比


通过实验验证,使用 map 的打包大小确实稍微小一些:



  • 使用 map 时,打包大小为:


  - 190 B: mitt.js.gz
- 162 B: mitt.js.br
- 189 B: mitt.mjs.gz
- 160 B: mitt.mjs.br
- 268 B: mitt.umd.js.gz
- 228 B: mitt.umd.js.br


  • 而使用 forEach 后,打包大小为:


  - 192 B: mitt.js.gz
- 164 B: mitt.js.br
- 191 B: mitt.mjs.gz
- 162 B: mitt.mjs.br
- 270 B: mitt.umd.js.gz
- 230 B: mitt.umd.js.br

进一步实验


为了深入了解选择的影响,我又进行了一个实验。有趣的是,当我将代码中的一处使用 map 改为 forEach,而另一处保持不变时,结果居然是打包体积更大了。


experiment_results.png


总结


这个故事让我不仅仅关注于代码表面,还开始注重微小选择可能带来的影响。学到了很多平时容易忽略的点,"mitt" 作者的选择展现了在开发中面对权衡时的智慧,通过选择不同的API,以轻松的方式达到减小代码体积的目标。在编写代码时,无处不充满着权衡的乐趣。


如果你对这个故事有更多的想法或者其他技术话题感兴趣,随时和我分享哦!


作者:KirkLin
来源:juejin.cn/post/7327424955037564965
收起阅读 »

使用pixi.js开发一个智慧路口(车辆轨迹追踪)项目

web
项目效果 项目功能: 位置更新、航向角计算。 debug模式。 位置角度线性补帧。 变道、转弯、碰撞检测。 mock轨迹数据 图片效果: 视频效果: 项目启动 项目地址 github:(github.com/huoguozhang…) 线上:todo...
继续阅读 »

项目效果


项目功能:



  • 位置更新、航向角计算。

  • debug模式。

  • 位置角度线性补帧。

  • 变道、转弯、碰撞检测。

  • mock轨迹数据


图片效果:


result.gif


视频效果:



项目启动


项目地址



(如果觉得项目对你有帮助的话, 可以给我一个star 和 赞,❤️)


启动demo项目



  1. cd car-tracking-2d/demos/react-demo

  2. yarn

  3. yarn start


界面使用


debug 模式


浏览器url ?后面(search部分)加入参数debug=1


例如:http://localhost:3000/home?tunnelNo=tunnel1&debug=1


将会展示调试信息:


image.png


如图:车旁边的白色文字信息为debug模式才会展示的内容(由上到下为:里程、车id、车道id、[x,y]、旋转角度)


实现:


技术栈:


ts+pixi.js+任意前端框架


(前端框架使用vuereact或者其他框架都可以。只需要在mounted阶段,实例化我们暴露出来class即可。然后在destroyed或者unmounted阶段destory示例即可,后面会提到。)


pixi.js


官网介绍:



Create beautiful digital content with the fastest, most flexible 2D WebGL renderer.



pixi.js是一个2D的WebGL的渲染库。但是没有three.js知名度高。一个原因是,我们2D的需求技术路线很多,可以是dom、svg、canvas draw api等,包括本项目也可以使用其他技术方案实现,希望通过本文,大家在实现这种频繁更新元素位置的功能,可以考虑一下pixi.js。



API快速讲解

这里只讲我们项目使用到的


Application

import * as PIXI from 'pixi.js';

const app = new PIXI.Application({
view: canvasDom // canvas dom 对象
});


Container

容器,功能为一个组。
当我们设置容器的scale(缩放)、rotation(旋转)、x、y(位置)时。里面的元素都会收到影响。


(ps:app.stage也是一个Container


每个 Container可以通过addChild(增加子节点)、removeChild(删除子节点),也可以设置子元素的zIndex(和css的功能一致)。子原始的scale(缩放)、rotation(旋转)、x、y(位置)是相对于Container的。


Sprite

精灵,渲染图片对象。


carObj = Sprite.from('/car.svg')

Sprite.from(url),url相同的话,只会加载一次图片。纹理对象也只会创建一次。


anchor属性其他对象也有,设置定位点,类似于csstransform-origin


执行下面代码
carObj.anchor.set(0.5, 0.5)


如果x = 10 y =10,carObj的中心点的坐标就是(10,10),旋转原点也是(10,10),缩放也是如此。


Graphics

绘制几何图形,圆弧,曲线、直线等都可以。也支持fill和stroke,canvas draw api支持的,Graphics都支持。


Text

文本,比较简单。字体、颜色、大小,都支持。



  • 值得注意的是文本内容含有换行符时(\n \r),文本会换行。

  • pixi提供测量文本的width height的方法非常好用。


Tick

this.app.ticker.add(() => {})

类似于requestAnimationFrame


具体实现


分三步,vue/react都一样:


1 获取canvas dom通过ref的方式。


2 创建我们封装Stage Road


3 组件销毁时,执行 stage.destroy(注意stage是我封装的,不是pixi的。使用方不需要使用pixi.js的api)


线性插帧

当有一个对象由坐标 点a(0,0)变换到点b(1000,1000),1秒内完成。
中间的变化值为:
dx =1000 dy=1000
记录每帧的时间差t(当前帧距离第0帧的,单位毫秒)


所以第n帧位置信息为(0+dx / 1000 * t, 0+ dy /1000 *t)


角度变换也是这个道理。


位置坐标获取

如果直线长度为1000px,对应的实际里程为100米。


当跑了50米,当前就是直线的中点坐标。
弯道呢,通过弧度可以推算出坐标。
可以把 Road.ts line 70的注释取消。


 // 方便开发观察 绘制车道线 ---begin----
// this.mount(lane.centerLine.paint())

航向角

直线简单,通过Math.atan2可以求出来。
弯道需要通过解析几何,计算出圆弧切线,然后推测出航向角。


转弯

mark.png
可以查看我们标注的一些点


以1到7的弯道举例,相当于是从新创建一次车道,车道的点是车道1和车道7的组合。
我们通过 circle属性配置,在创建Road


{
uid: '1-2',
x: 1072,
y: 1605,
circle: {
// 编号形式 车道序号-第几个点

linkId: '7-3'
}
},

这条信息表示:车道1的第2个点(uid),有圆弧链接到车道7的第3个点(circle.linkId)


碰撞检测

我们这个项目的特点是,前端展示,实际后端返回什么数据,我们就展示什么数据。(一般不需要前端处理)。
这里我们mock的数据就简单处理一下。判断是否存在相交的线段(当前对象的位置和将要到达的点),如果线段相交,车辆暂停移动。


作者:火锅小王子
来源:juejin.cn/post/7327467832866095130
收起阅读 »

微信小程序开发大坑盘点

web
微信小程序开发大坑盘点 起因 前几天心血来潮,想给学校设计个一站式校园小程序,可以查询成绩,考试信息,课表之类的(本来想法里是还想包括一些社交功能的,但这个因为资质问题暂且搁置了)。其实很久以前就有大概了解过微信小程序的一些概念,那个时候试图用 uni-app...
继续阅读 »

微信小程序开发大坑盘点


起因


前几天心血来潮,想给学校设计个一站式校园小程序,可以查询成绩,考试信息,课表之类的(本来想法里是还想包括一些社交功能的,但这个因为资质问题暂且搁置了)。其实很久以前就有大概了解过微信小程序的一些概念,那个时候试图用 uni-app 做,但是这玩意太难用所以不了了之了。


于是这次打算正经的用微信自己的那套东西做,结果不出意外的是入了深坑......


大坑


微信小程序云函数外部调用异常


微信小程序提供 wx.request 发起 HTTP 请求,由于微信不是浏览器,没有跨域限制,这方便了很多事,但是由于 wx.request 函数只能对 HTTPS 协议的地址发起请求,而我们学校的教务系统又是清一色的 HTTP,因此我需要一个可以用来帮助我发起 HTTP 请求的转发接口。


对于这种简单需求,云函数显然是最好的解决方案,进而我发现微信小程序自带云函数的支持,于是便兴冲冲地写了一段 NodeJS 代码,放上去跑。


结果我发现不知道为什么,请求其他网站都没问题,唯独请求我们教务系统就会原地超时。经过了几个小时的调试,最后以失败告终,转而改用腾讯云的云函数。


代码也十分简单:


const url = require('url')

const express = require('express');
const app = express()
const port = 9000

const rp = require('request-promise')

app.use(express.json());

app.post('/', async (req, res) => {
const jar = rp.jar()

try {
const response = await rp({
...req.body,
resolveWithFullResponse: true,
simple: false,
jar: jar
})
res.json(response)
} catch (e) {
res.json(e)
console.error(e)
}
})

app.listen(port, () => {
console.log("Successfully loaded")
})

其中额外引入了 request-promise 库(express 是默认引入的,腾讯云函数这里做的不错,对 npm 支持很好)。


然后做了一个模仿 wx.request 调用风格的 request 函数,这样我就可以在 wx.request 和我自己的 request 函数中无缝切换(更进阶的是,我自己写的这个还额外支持了以 Promise 风格调用。


export async function request(data) {
try {
const res = await rp({
...data,
uri: data.url,
headers: data.header,
})
let result = {
...res,
data: res.body,
header: res.headers
}
if (result.statusCode != 200) {
throw {
err_msg: "内部错误"
}
}
if (data.dataType === 'json') {
result.body = JSON.parse(result.body)
}
data.success && data.success(result);
data.complete && data.complete({})
return result;
} catch (e) {
data.fail && data.fail(e)
data.complete && data.complete({})
throw e;
}
}

function rp(data) {
return new Promise((resolve, reject) => {
wx.request({
method: 'POST',
url: 'https://service-abcdefg-123456789.gz.apigw.tencentcs.com/release/',
data: data,
success: (res) => {
resolve(res.data)
},
fail: (err) => {
reject(err)
}
})
})
}

ES6 module 和变量作用域支持差


不知道为什么,微信小程序完全不支持 ES6 module,即使它是支持 ES6 语法的。也就是说,你只能使用这种传统的 CommonJS 方式引入:


const module = require('module.js')

而不是 ES6 的 import 语法:


import module from 'module.js'

最离谱的是,微信小程序这个基于 VSCode 的编译器会给你 warn 这段代码,告知你可以转换为使用 import 导入。



于是这又引出了另外一个奇怪的问题:当你在一个界面的逻辑层文件上声明变量时,IDE 会认为这个变量是一个全局变量,因此在其他界面声明同名变量会得到一个 error,即使不会导致任何编译错误。


这导致了,现在我的模块引入必须用一种很奇怪的写法...


const sessionModule = require('../../utils/session');
const tgcModule = require('../../utils/tgc')
const cryptoModule = require('../../miniprogram_npm/crypto-js/index.js')

奇葩的 NPM 支持


在以前,微信小程序是不支持包管理器的,这也就意味着,你得手动把那些库的 JS 复制到你的项目目录里再引用,非常麻烦。但是现在好了,微信可以自动帮你做这件事了。


没错,是自动帮你复制,而不是做了包管理器支持。


怎么说呢...你需要先在你的项目源代码目录中 init 一个 package.jsonadd 你需要的包然后 install,接下来点击 IDE 顶栏的 Tools - Build npm 选项,Weixin Devtools 就会帮你生成一个 miniprogram_npm文件夹,将每个项目各自 combine 到一个 index.js 然后塞到各自名字的文件夹里,然后,你就能通过上面那种方式手动引入使用了。


很奇葩但是... 勉强能用(而且不限制使用的包管理器,比如我用的就是 yarn)。


避免使用双向绑定


微信小程序的 WXML 存在一个有限的双向绑定支持,也是类似 Vue 的那种语法糖:


<input model:value="{{value}}" />

但是这个双向绑定不知道为什么,在某些情况下会认为你没有设置一个 bindinput 事件(但实际上应该是由双向绑定自动设置的),于是不断地在后台刷警告,因此还不如手动实现来的省心。


有限的标准组件支持


如果你觉得微信小程序的开发和前端开发差不多,那就大错特错了。因为微信小程序默认情况下根本不支持任何 HTML 元素,而是套了一层他们自己的元素,比如 view 实际上是 classblock 则和 Vue 的 template 差不多(微信小程序也有 template 元素,只不过那个是给组件用的),不分 h1, h2, span, strong,只有 text 元素等。当然好在 CSS 还是那套,基本都能用。


但是... 微信小程序提供的元素依然太少了,根本没办法满足实际开发需要(比如根本没有表格元素)。于是微信小程序提供了一个 rich-text 元素,可用于渲染 HTML 元素。


但是这个 rich-text 就显得十分鸡肋,他不是通过 slot 传入 HTML 元素,而是通过 string 或者 object。这凭空增加了开发难度,导致我不得不这么写:


<rich-text nodes="{{nodes}}"></rich-text>

this.setData({
nodes: licenses.map(it => {
return `
<div style="margin: 20px 10px;"><strong>${it.projectName}</strong>
is licensed under the <code>${it.licenseName}</code>:</div>
<pre style="overflow: auto; background-color:#F5F6FA;"><code>${it.fullLicense}</code></pre>
${it.sourceRepo?`<div style="margin: 20px 10px;"><span style="color:gray; font-size: 12px;">The source code can be found at: ${it.sourceRepo}</span></div>`:""}
<br/><br/>
`

}).join("")
})

甚至这么写:



完美的回答了知乎有人“为什么不用 JSON 表达页面而是用类似 XML 一样的 HTML”的问题。


最后


虽然吐槽了这么多,但是微信小程序还是有一些不错的点的。除了上面说的宽松的跨域策略以外,微信小程序的 TypeScript 支持很完善,IDE 工具链做的也不错(除了他那个特别容易崩溃的 Simulator),加之微信开放社区的活跃度也不低(问问题一天内就有人回复),也算是能用了。


作者:HikariLan贺兰星辰
来源:juejin.cn/post/7228563544022761509
收起阅读 »

很多人是无知的,但是他们总是觉得自己是对的!

昨天在朋友圈看到了我少年时期的初恋在朋友圈晒娃,她的娃拿了省里文艺比赛的冠军,站在C位拿着话筒演讲。 同时也看到了另外的朋友晒娃,只是二者形成了很鲜明的对比。 我在动车上陷入了沉思,然后和好朋友聊了几句,他说:我们那个时代的成长和教育方式在这个时代已经不可取了...
继续阅读 »

图片


昨天在朋友圈看到了我少年时期的初恋在朋友圈晒娃,她的娃拿了省里文艺比赛的冠军,站在C位拿着话筒演讲。


同时也看到了另外的朋友晒娃,只是二者形成了很鲜明的对比。


我在动车上陷入了沉思,然后和好朋友聊了几句,他说:我们那个时代的成长和教育方式在这个时代已经不可取了!


在我小时候,农活干得很熟练,挖地,跳水,放牛,割草,整天脏兮兮的,没条件学习艺术,没条件去旅游,眼睛看到的永远是门前那一望无尽的大山。


虽然三年级就到了镇里读书,五年级就到了城里读书,但是自卑伴随了很久。不过好在父母基本上一直都在身边,虽然物质条件不充足,但是精神上并不是那么贫乏。


但是我想说的是,对于我这种农村出来的95后,这种事情发生在那个年代是正常的,是可以理解的。但是发生在今天,那就是有问题的,因为那时候大多父母的文化水平都比较低,农村人的土地思维还根深蒂固。 只要孩子能平平安安成长,至于将来有没有出息,就看命吧。


但是今天不一样了,即使是大山里面的,那也看过了外面的世界,文化水平也提高了许多,但是为什么依然还会出现很多年轻人即使物质生活多么贫乏,依然还是选择要孩子,而孩子无论从教育还是各方面都非常落后。


在我小时候那个年代,早上打着电筒去上学的事情是有,但是到了今天,我亲眼看见了七八岁的小孩子早晨六七点在乡村充满泥泞的路上打着电筒,穿着陈旧的衣服,冷得打哆嗦,走几公里的路去上学。


而在他们没见过,没听过,没想过的大中小城市里面,小孩子早上起来吃了营养餐,父母或者爷爷奶奶再送去上学,每天学习各种技能,才艺,人一说都特别有自信。


你也别说这说那,比如:干嘛要送孩子啊,你看人家日本,孩子从来都是自己去上学,从小就锻炼了独立的意识,而国内则当成老祖宗一样,上下学都接送。


但是人家和你一样吗,人家那是选择这样,而我们大部分人是只能这样,这个问题下面我们会谈。


所以我们发现一个问题,农村出来的孩子在这个时代大多混得都比较差,还比较自卑,只有极少的能稍微改命,但一定是经过脱胎换骨换来的。


我们经常在网上看到一些视频,父母在外务工,一个小孩子就在开始干家务,做饭,还要照顾比自己小的弟弟妹妹。


然后下面的人就说:这孩子以后一定能成大器,一定有一番作为。


有些人也深深认同,甚至搬出一些名人的故事来:比如董卿很小的时候父亲就让她承包家务,每天还要去酒店打扫很多房间的卫生,最后人家不也成就一番事业了。


我想说:简直荒诞得不行,就算这种事情是真实存在过,那我们也别用来乱套在所有孩子身上。


现在很多人就喜欢说,孩子要穷养,这样对他以后才好,但是穷养并不是你想的那样的,你也别乱套在孩子身上。


首先要区分真穷养还是假穷养


董卿的父母都是复旦大学毕业的高材生,这种家庭放在今天都是炸裂的存在,更何况是八九十年代,所以文化水平和经济水平都是很强的。


那么人家穷养的目的是啥,无非就是锻炼孩子的心智,让她以后的路走得更远。


所以人家是有选择性的去穷养,今天我可以让你去打扫酒店,明天就可以带你去看艺术表演,学习钢琴。


但是一般甚至过得艰难的家庭,穷养不是选择,而是没有办法,所以只能穷养,你今天干家务,明天也只能干家务,看艺术,学钢琴和你一点关系都没有,甚至你一辈子都不可能接触到。


而且孩子以后走的路大概率也是十分艰难,都是为了一次三餐,混得基本上也不会好,这是必然的。


但是奇怪的是,很多人为了所谓的人生任务,传宗接代,根本不会去思考这些问题,甚至还有一些年轻人还抱有“儿孙自有儿孙福”的落后思想,还将自己的养老任务寄托在孩子身上。


有时候真的无法想象,二十一世纪了,还抱有这种思想,实在是可恶,可悲!


我先表达自己的观点和立场:如果你没有一定的经济支撑和教育能力,而是想要孩子自己靠自己,那么就是不负责任!


那么回到文章开头,一个孩子在省里的台上演讲,一个孩子在泥泞中穿梭。


是想说什么呢?


其实无非就是想表达良好的教育和物质生活的重要性。


我亲自见过一些尖酸刻薄的人,从来不会反思自己,看到别人的孩子特别优秀,他们会说:有啥了不起的,我孩子也不差,虽然在农村玩泥巴,但是他健康啊,他快乐啊,你孩子虽然成绩优异,能歌善舞,但是你看他压力多大,没有童年。


然后转头望向旁边的几个孩子,对他们说:以后老妈就靠你们了,你们以后出来打工,一人给我买一个金戒指和金项链,直接把老妈的脖子都压弯。


上面的事情是我亲眼目睹的,他们没耐心教孩子做作业,而是直接手机上搜出来抄上去。


然后孩子的考试成绩差了,很多人就开始怪孩子了,大声呵斥:你是怎么学的,你怎么一点出息都没有,你看看人家为啥能考第一,你为啥只能考这么点分?


而这样的例子少吗?我想说,一点也不少,特别是在落后的农村和小县城,很普遍。


因为他们的目的就是怕以后自己老了没人养啊,死了没人送终啊,然后又一直给孩子灌输这种思想,最终造成了恶性循环。


而孩子从小就没有得到良好的教育和生活体验,进入社会会恐惧,也没多少竞争力。


你以为那些初入职场就特别优秀的人是进入职场才优秀的吗?


不,人家在读书的时候就已经开始崭露头角了。


国内外你只要能数得出来的优秀企业家,作家,艺术家等等,要么从小家境就不错,即使不是大富大贵,但是也是小康以上,要么文化,教育气氛特别浓厚,要么二者都兼顾,基本很难找出一个没有具备这二者的条件人。


就拿几个熟悉的人来说,人家余华当年能在家全职写作,罗永浩能在家看两年闲书,马老师能复读。


那已经是八九十年代的事情了。


试问,就算现在有多少家庭能扛得住?


并不是说经济条件和教育条件一定要多么优越才能养孩子,而是最起码要有基本的保障吧,能做到负责二字吧。


你总不可能让他以后再把你的老路走一遍吧,这和害人没有任何区别。


还有很多父母总是逼着自己的孩子结婚生子,哪怕孩子现在都自身难保。


他们会说:生了以后放孩子在农村,我们给你带啊,你们再去外面打工,几年后孩子长大了就能自己读书,就能自己做饭了。


嗯。。。。。。。。。。。。。


很离谱。


但是依旧有很多按照旨意去做了,不为别的,就是为了所谓的责任。


然后就开始赌下一代会有出息,好给自己打个漂亮的翻身仗,自己的晚年就能安稳度过了。


二三十几岁,就已经开始去担心65岁以后的日子了,把话说难听一点,如果当下都不能好好去生活,还指望65岁以后能够生活好?


反正我不信,这逻辑本来就行不通。


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

面试理想汽车,给我整懵了。。。

理想汽车 今天看到一个帖子,挺有意思的。 先别急着骂草台班子。 像理想汽车这种情况,其实还挺常见的。 就是:面试官说出一个错误的结论,我们该咋办? 比较好的做法还是先沟通确认清楚,看看大家是否针对的为同一场景,对某些名词的认识是否统一,其实就是对错误结论的再...
继续阅读 »

理想汽车


今天看到一个帖子,挺有意思的。



先别急着骂草台班子。


像理想汽车这种情况,其实还挺常见的。


就是:面试官说出一个错误的结论,我们该咋办?


比较好的做法还是先沟通确认清楚,看看大家是否针对的为同一场景,对某些名词的认识是否统一,其实就是对错误结论的再次确认。


如果确定清楚是面试官的错误,仅做一次不直白的提醒后,看对方是否会陷入不确定,然后进入下一个问题,如果是的话,那就接着往下走。


如果对方还是揪着那个错误结论不放,不断追问。


此时千万不要只拿你认为正确的结论出来和对方辩论。


因为他只有一个结论,你也只有一个结论的话,场面就成了没有理据的争论,谁也说服不了谁。


我们可以从两个方向进行解释:



  • 用逻辑进行正向推导,证明你的结论的正确性

  • 用类似反证法的手段进行解释,试图从他的错误结论出发,往回推,直到推出一个对方能理解的,与常识相违背的基本知识


那么对应今天这个例子,关于「后序遍历」的属于一个定义类的认识。


我们可以用正向推导的方法,试图纠正对方。


可以从另外两种遍历方式进行入手,帮助对方理解。


比如你说:


"您看,前序遍历是「中/根 - 左 - 右」,中序遍历是「左 - 中/根 - 右」"


"所以它这个「X序遍历」的命名规则,主要是看对于一棵子树来说,根节点被何时访问。"


"所以我理解的后序遍历应该是「左 - 右 - 中/根」。"


"这几个遍历确实容易混,所以我都是这样的记忆理解的。"


大家需要搞清楚,这一段的主要目的,不是真的为了教面试官知识,因此适当舍弃一点点的严谨性,提高易懂性,十分重要。


因为我们的主要目的是:想通过有理据的解释,让他不要再在这个问题下纠缠下去


如果是单纯想争对错,就不会有前面的「先进行友好提示,对方如果进行下一问,就接着往下」的前置处理环节。


搞清楚这一段表达的实际目的之后,你大概知道用什么口吻进行解释了,包括上述的最后一句,给对方台阶下,我觉得也是必要的。


对方是错了,但是你没必要给别人落一个「得理不饶人」的印象。


还是谦逊一些,面试场上争对错,赢没赢都是候选人输。


可能会有一些刚毕业的同学,心高气傲,觉得连二叉树这么简单的问题都搞岔的面试官,不值得被尊重。


你要知道,Homebrew 作者去面谷歌的时候,也不会翻转二叉树呢。


难道你要说这世上只有那些知识面是你知识面超集的人,才值得被尊重吗?


显然不是的,大家还是要学会带着同理心的去看待世界。


...


看了一眼,底下评论点赞最高的那位:



什么高情商说法,还得是网友。


所以面试官说的后序遍历是「右 - 左 - 中」?interesting。


...


回归主线。


也别二叉树后续遍历了,直接来个 nn 叉树的后序遍历。


题目描述


平台:LeetCode


题号:590


给定一个 nn 叉树的根节点 rootroot ,返回 其节点值的后序遍历


nn 叉树在输入中按层序遍历进行序列化表示,每组子节点由空值 null 分隔(请参见示例)。


示例 1:


输入:root = [1,null,3,2,4,null,5,6]

输出:[5,6,3,2,4,1]

示例 2:


输入:root = [1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]

输出:[2,6,14,11,7,3,12,8,4,13,9,10,5,1]

提示:



  • 节点总数在范围 [0,104][0, 10^4]

  • 0<=Node.val<=1040 <= Node.val <= 10^4

  • nn 叉树的高度小于或等于 10001000


进阶:递归法很简单,你可以使用迭代法完成此题吗?


递归


常规做法,不再赘述。


Java 代码:


class Solution {
List ans = new ArrayList<>();
public List postorder(Node root) {
dfs(root);
return ans;
}
void dfs(Node root) {
if (root == null) return;
for (Node node : root.children) dfs(node);
ans.add(root.val);
}
}

C++ 代码:


class Solution {
public:
vector<int> postorder(Node* root) {
vector<int> ans;
dfs(root, ans);
return ans;
}
void dfs(Node* root, vector<int>& ans) {
if (!root) return;
for (Node* child : root->children) dfs(child, ans);
ans.push_back(root->val);
}
};

Python 代码:


class Solution:
def postorder(self, root: 'Node') -> List[int]:
def dfs(root, ans):
if not root: return
for child in root.children:
dfs(child, ans)
ans.append(root.val)
ans = []
dfs(root, ans)
return ans

TypeScript 代码:


function postorder(root: Node | null): number[] {
const dfs = function(root: Node | null, ans: number[]): void {
if (!root) return ;
for (const child of root.children) dfs(child, ans);
ans.push(root.val);
};
const ans: number[] = [];
dfs(root, ans);
return ans;
};


  • 时间复杂度:O(n)O(n)

  • 空间复杂度:忽略递归带来的额外空间开销,复杂度为 O(1)O(1)


非递归


针对本题,使用「栈」模拟递归过程。


迭代过程中记录 (cnt = 当前节点遍历过的子节点数量, node = 当前节点) 二元组,每次取出栈顶元素,如果当前节点已经遍历完所有的子节点(当前遍历过的子节点数量为 cnt=子节点数量cnt = 子节点数量),则将当前节点的值加入答案。


否则更新当前元素遍历过的子节点数量,并重新入队,即将 (cnt+1,node)(cnt + 1, node) 入队,以及将下一子节点 (0,node.children[cnt])(0, node.children[cnt]) 进行首次入队。


Java 代码:


class Solution {
public List postorder(Node root) {
List ans = new ArrayList<>();
Deque d = new ArrayDeque<>();
d.addLast(new Object[]{0, root});
while (!d.isEmpty()) {
Object[] poll = d.pollLast();
Integer cnt = (Integer)poll[0]; Node t = (Node)poll[1];
if (t == null) continue;
if (cnt == t.children.size()) ans.add(t.val);
if (cnt < t.children.size()) {
d.addLast(new Object[]{cnt + 1, t});
d.addLast(new Object[]{0, t.children.get(cnt)});
}
}
return ans;
}
}

C++ 代码:


class Solution {
public:
vector<int> postorder(Node* root) {
vector<int> ans;
stackint, Node*>> st;
st.push({0, root});
while (!st.empty()) {
auto [cnt, t] = st.top();
st.pop();
if (!t) continue;
if (cnt == t->children.size()) ans.push_back(t->val);
if (cnt < t->children.size()) {
st.push({cnt + 1, t});
st.push({0, t->children[cnt]});
}
}
return ans;
}
};

Python 代码:


class Solution:
def postorder(self, root: 'Node') -> List[int]:
ans = []
stack = [(0, root)]
while stack:
cnt, t = stack.pop()
if not t: continue
if cnt == len(t.children):
ans.append(t.val)
if cnt < len(t.children):
stack.append((cnt + 1, t))
stack.append((0, t.children[cnt]))
return ans

TypeScript 代码:


function postorder(root: Node | null): number[] {
const ans = [], stack = [];
stack.push([0, root]);
while (stack.length > 0) {
const [cnt, t] = stack.pop()!;
if (!t) continue;
if (cnt === t.children.length) ans.push(t.val);
if (cnt < t.children.length) {
stack.push([cnt + 1, t]);
stack.push([0, t.children[cnt]]);
}
}
return ans;
};


  • 时间复杂度:O(n)O(n)

  • 空间复杂度:O(n)O(n)


通用「非递归」


另外一种「递归」转「迭代」的做法,是直接模拟系统执行「递归」的过程,这是一种更为通用的做法。


由于现代编译器已经做了很多关于递归的优化,现在这种技巧已经无须掌握。


在迭代过程中记录当前栈帧位置状态 loc,在每个状态流转节点做相应操作。


Java 代码:


class Solution {
public List postorder(Node root) {
List ans = new ArrayList<>();
Deque d = new ArrayDeque<>();
d.addLast(new Object[]{0, root});
while (!d.isEmpty()) {
Object[] poll = d.pollLast();
Integer loc = (Integer)poll[0]; Node t = (Node)poll[1];
if (t == null) continue;
if (loc == 0) {
d.addLast(new Object[]{1, t});
int n = t.children.size();
for (int i = n - 1; i >= 0; i--) d.addLast(new Object[]{0, t.children.get(i)});
} else if (loc == 1) {
ans.add(t.val);
}
}
return ans;
}
}

C++ 代码:


class Solution {
public:
vector<int> postorder(Node* root) {
vector<int> ans;
stackint, Node*>> st;
st.push({0, root});
while (!st.empty()) {
int loc = st.top().first;
Node* t = st.top().second;
st.pop();
if (!t) continue;
if (loc == 0) {
st.push({1, t});
for (int i = t->children.size() - 1; i >= 0; i--) {
st.push({0, t->children[i]});
}
} else if (loc == 1) {
ans.push_back(t->val);
}
}
return ans;
}
};

Python 代码:


class Solution:
def postorder(self, root: 'Node') -> List[int]:
ans = []
stack = [(0, root)]
while stack:
loc, t = stack.pop()
if not t: continue
if loc == 0:
stack.append((1, t))
for child in reversed(t.children):
stack.append((0, child))
elif loc == 1:
ans.append(t.val)
return ans

TypeScript 代码:


function postorder(root: Node | null): number[] {
const ans: number[] = [];
const stack: [number, Node | null][] = [[0, root]];
while (stack.length > 0) {
const [loc, t] = stack.pop()!;
if (!t) continue;
if (loc === 0) {
stack.push([1, t]);
for (let i = t.children.length - 1; i >= 0; i--) {
stack.push([0, t.children[i]]);
}
} else if (loc === 1) {
ans.push(t.val);
}
}
return ans;
};


  • 时间复杂度:O(n)O(n)

  • 空间复杂度:O(n)O(n)

作者:宫水三叶的刷题日记
来源:juejin.cn/post/7327188195770351635
收起阅读 »

程序员为什么不能一次把功能写好,是因为他不想吗

引言 交流一下为什么他做的功能这么多Bug 大家好,最近看到一个有趣的问题: 程序员为什么要不能一次性写好,需要一直改Bug? 在我看来,程序员也是人,并非机器。 拿这个问题去质问程序员,答案无非那么几个。 1.需求的理解 有时候,在项目一开始,需求可能并...
继续阅读 »

为什么他做的功能那么多Bug


引言


交流一下为什么他做的功能这么多Bug


大家好,最近看到一个有趣的问题



程序员为什么要不能一次性写好,需要一直改Bug?



在我看来,程序员也是人,并非机器。


拿这个问题去质问程序员,答案无非那么几个


1.需求的理解


有时候,在项目一开始,需求可能并没有被完全理解清楚。


随着项目的推进,更多的细节可能浮现,需要对代码进行调整以适应新的或更清晰的需求。


首先需求的传递,通常有以下几种



  • 口头传递:程序员可能无意间听到策划的一句话,就认定为需求就是这样。

  • 需求会议:这是笔者认为比较正式的,相关人员一起,进行需求的分析和探讨。

  • 临时加的:前面提需求的时候遗漏的,后面补的。

  • 非工作日加的:在非工作日休息时,收到经理或者老板的电话需求。


这里面都涉及人与人之间交流和理解。它是极其容易受到人的状态和情绪影响的。


可能因为程序员在理解需求时较真策划无意或者有意的一句话


也可能因为程序员在会议过程中打瞌睡或者不以为然


甚至在程序员情绪不满的状态下接到了需求。


2.功能的复杂性


许多功能都涉及复杂的业务逻辑、数据处理和用户交互


理解整个功能如何运作的过程中,程序员可能会对功能的梳理不够清晰,导致一开始的实现可能考虑得不够完善


相信大家都清楚,无论是大功能还是小功能,都会有Bug


但是在相对复杂的功能下,Bug会更加容易出现甚至更多。


笔者认为这和人生的选择有点相似,越是关键的选择,越难选择


3.新的内容


项目迭代过程中,可能需要引入新的功能,他可能与项目框架或者方向完全不同。


这必然会导致程序的稳定性受到影响。


越是底层的内容,在修改时引发的内容变化就越容易,影响的面更广


这里面可能新的内容旧项目完全不搭,强行要引入这样的内容,在设计层面就不对。


也可能是因为程序员考虑不当,没有更加全面的考虑到策划或者经理的变化


4.时间的压力


项目通常有时间限制,导致程序员可能不得不在有限的时间内完成任务。


这可能导致在一开始时忽略一些潜在的问题,需要在后期修复


迫于时间的压力,程序员往往会不断地跳过遇到的问题,往更容易完成的方向去执行


那么这些卡点会被放到功能的最后处理,这和我们以前考试是相类似的。


老师教导我们,在考试遇到困难的问题时,先跳过,等到试卷做完一遍之后回来再看难题。


但往往问题也会出现在这些跳过的内容,要么难题还是难题,做不出来。要么就是给到这些难题的时间已经不多了。


5.功能的耦合


在团队协作的环境中,不同部分的代码可能同时被多个程序员修改,可能导致冲突和Bug


此外,不同模块之间的复杂交互可能在测试之前难以被完全预测


这种问题通常表现为,A程序员修改的项目的A功能,但是出乎意料的的是B程序员B功能出了问题。


这里面就涉及框架和项目的耦合情况,越是耦合严重的代码(通常被称为"屎山"),你的修改越是不能一干二净出乎意料地影响了其他功能。


6.硬件和环境变化


程序可能在不同的硬件和环境中运行,这可能导致一些未考虑到的问题。


为了适应不同的环境,可能需要进行一些修复和调整


大家知道用户的使用环境可能千奇百怪


首先设备环境就分为好几种,原生的Android,iOS,网页的H5,还有PC小程序


其次不同的网络环境,2g,3g,4g,5g和wifi


程序员在开发时以最好的网络最好的机器,去到用户的千元机,万元机和老人机的时候表现都不尽相同。


怎么解决


一把需求给你,你就那么多问题,都是不能解决的吗?


笔者认为事实并不如此,人是会进步的,通过不断的总结和优化,能逐步减少Bug的产生,但是不能杜绝



  • 需求理解:程序员与策划/经理的关系要融洽,工作时沟通和交流不要存在个人情绪和意见。认真对待每次需求会议。

  • 功能的复杂性:程序员与策划/经理要一同考虑功能的复杂性,策划与经理不能一味地提需求而不考虑复杂性,程序员不能一味地实现功能不考虑功能的变化。

  • 新的内容:程序员要仔细评估新内容对旧项目的冲击,策划/经理要认真考虑,这个功能是不是真的合适项目。

  • 时间的压力:更合理地评估功能的完成时间,拒绝不合理的降本增效。

  • 功能的耦合:不断提升代码能力,学习更加优秀的写法,应对不同需求的变化。

  • 硬件和环境变化:加强不同环境的测试,这里面要考虑的是不同环境测试的便捷性,不断优化测试环境,不要让测试困难导致了Bug的产生。


结语


不管是程序员还是策划还是经理,沟通是减少问题的关键,而不是质问。


在哪里可以看到如此清晰的思路,快跟上我的节奏!关注我,和我一起了解游戏行业最新动态,学习游戏开发技巧。


作者:亿元程序员
来源:juejin.cn/post/7320906381795672116
收起阅读 »

Linux新手村必备!这些常用操作命令你掌握了吗?

在计算机的世界里,Linux操作系统以其强大的功能和灵活性受到了广大程序员和IT爱好者的喜爱。然而,对于初学者来说,Linux的操作命令可能会显得有些复杂和难以理解。今天,我们就来一起探索一些Linux常用操作命令,让你的计算机操作更加流畅。一、目录操作首先带...
继续阅读 »

在计算机的世界里,Linux操作系统以其强大的功能和灵活性受到了广大程序员和IT爱好者的喜爱。然而,对于初学者来说,Linux的操作命令可能会显得有些复杂和难以理解。

今天,我们就来一起探索一些Linux常用操作命令,让你的计算机操作更加流畅。

一、目录操作

首先带大家了解一下Linux 系统目录:

├── bin -> usr/bin # 用于存放二进制命令
├── boot # 内核及引导系统程序所在的目录
├── dev # 所有设备文件的目录(如磁盘、光驱等)
├── etc # 配置文件默认路径、服务启动命令存放目录
├── home # 用户家目录,root用户为/root
├── lib -> usr/lib # 32位库文件存放目录
├── lib64 -> usr/lib64 # 64位库文件存放目录
├── media # 媒体文件存放目录
├── mnt # 临时挂载设备目录
├── opt # 自定义软件安装存放目录
├── proc # 进程及内核信息存放目录
├── root # Root用户家目录
├── run # 系统运行时产生临时文件,存放目录
├── sbin -> usr/sbin # 系统管理命令存放目录
├── srv # 服务启动之后需要访问的数据目录
├── sys # 系统使用目录
├── tmp # 临时文件目录
├── usr # 系统命令和帮助文件目录
└── var # 存放内容易变的文件的目录

下面我们来看目录操作命令有哪些

pwd    查看当前工作目录
clear 清除屏幕
cd ~ 当前用户目录
cd / 根目录
cd - 上一次访问的目录
cd .. 上一级目录

查看目录内信息

ll    查看当前目录下内容(LL的小写)

创建目录

  • mkdir aaa 在当前目录下创建aaa目录,相对路径;
  • mkdir ./bbb 在当前目录下创建bbb目录,相对路径;
  • mkdir /ccc 在根目录下创建ccc目录,绝对路径;

递归创建目录(会创建里面没有的目录文件夹)

mkdir -p temp/nginx

搜索命令

  • find / -name ‘b’ 查询根目录下(包括子目录),名以b的目录和文件;
  • find / -name ‘b*’ 查询根目录下(包括子目录),名以b开头的目录和文件;
  • find . -name ‘b’ 查询当前目录下(包括子目录),名以b的目录和文件;

重命名

mv 原先目录 文件的名称   mv tomcat001 tomcat

剪切命令(有目录剪切到制定目录下,没有的话剪切为指定目录)

mv /aaa /bbb      将根目录下的aaa目录,移动到bbb目录下(假如没有bbb目录,则重命名为bbb);
mv bbbb usr/bbb 将当前目录下的bbbb目录,移动到usr目录下,并且修改名称为bbb;
mv bbb usr/aaa 将当前目录下的bbbb目录,移动到usr目录下,并且修改名称为aaa;

复制目录

cp -r /aaa /bbb:将/目录下的aaa目录复制到/bbb目录下,在/bbb目录下的名称为aaa
cp -r /aaa /bbb/aaa:将/目录下的aa目录复制到/bbb目录下,且修改名为aaa;

强制式删除指定目录

rm -rf /bbb:强制删除/目录下的bbb目录。如果bbb目录中还有子目录,也会被强制删除,不会提示;

删除目录

  • rm -r /bbb:普通删除。会询问你是否删除每一个文件
  • rmdir test01:目录的删除

查看树状目录结构

tree test01/

批量操作

需要采用{}进行参数的传入了。

mkdir {dirA,dirB}  # 批量创建测试目录
touch dirA/{A1,A2,A3} # dirA创建三个文件dirA/A1,dirA/A2,dirA/A3

二、文件操作

删除

rm -r a.java  删除当前目录下的a.java文件(每次会询问是否删除y:同意)

强制删除

  • rm -rf a.java 强制删除当前目录下的a.java文件
  • rm -rf ./a* 强制删除当前目录下以a开头的所有文件;
  • rm -rf ./* 强制删除当前目录下所有文件(慎用);

创建文件

touch testFile

递归删除.pyc格式的文件

find . -name '*.pyc' -exec rm -rf {} \;

打印当前文件夹下指定大小的文件

find . -name "*" -size 145800c -print

递归删除指定大小的文件(145800)

find . -name "*" -size 145800c -exec rm -rf {} \;

递归删除指定大小的文件,并打印出来

find . -name "*" -size 145800c -print -exec rm -rf {} \;
  • “.” 表示从当前目录开始递归查找
  • “ -name ‘*.exe’ "根据名称来查找,要查找所有以.exe结尾的文件夹或者文件
  • " -type f "查找的类型为文件
  • “-print” 输出查找的文件目录名
  • -size 145800c 指定文件的大小
  • -exec rm -rf {} ; 递归删除(前面查询出来的结果)

split拆分文件

split命令:可以将一个大文件分割成很多个小文件,有时需要将文件分割成更小的片段,比如为提高可读性,生成日志等。

  1. b:值为每一输出档案的大小,单位为 byte。
  2. -C:每一输出档中,单行的最大 byte 数。
  3. -d:使用数字作为后缀。
  4. -l:值为每一输出档的行数大小。
  5. -a:指定后缀长度(默认为2)。

使用split命令将上面创建的date.file文件分割成大小为10KB的小文件:

[root@localhost split]# split -b 10k date.file
[root@localhost split]# ls
date.file xaa xab xac xad xae xaf xag xah xai xaj

文件被分割成多个带有字母的后缀文件,如果想用数字后缀可使用-d参数,同时可以使用-a length来指定后缀的长度:

[root@localhost split]# split -b 10k date.file -d -a 3
[root@localhost split]# ls
date.file x000 x001 x002 x003 x004 x005 x006 x007 x008 x009

为分割后的文件指定文件名的前缀:

[root@localhost split]# split -b 10k date.file -d -a 3 split_file
[root@localhost split]# ls
date.file split_file000 split_file001 split_file002 split_file003 split_file004 split_file005 split_file006 split_file007 split_file008 split_file009

使用-l选项根据文件的行数来分割文件,例如把文件分割成每个包含10行的小文件:

split -l 10 date.file

三、文件内容操作

修改文件内容

  • vim a.java:进入一般模式
  • i(按键):进入插入模式(编辑模式)
  • ESC(按键):退出
  • :wq:保存退出(shift+:调起输入框)
  • :q!:不保存退出(shift+:调起输入框)(内容有更改)(强制退出,不保留更改内容)
  • :q:不保存退出(shift+:调起输入框)(没有内容更改)
    文件内容的查看
cat a.java   查看a.java文件的最后一页内容;
more a.java从 第一页开始查看a.java文件内容,按回车键一行一行进行查看,按空格键一页一页进行查看,q退出;
less a.java 从第一页开始查看a.java文件内容,按回车键一行一行的看,按空格键一页一页的看,支持使用PageDown和PageUp翻页,q退出。

总结下more和less的区别

  • less可以按键盘上下方向键显示上下内容,more不能通过上下方向键控制显示。
  • less不必读整个文件,加载速度会比more更快。
  • less退出后shell不会留下刚显示的内容,而more退出后会在shell上留下刚显示的内容。

实时查看文件后几行(实时查看日志)

tail -f a.java   查看a.java文件的后10行内容;

前后几行查看

  • head a.java:查看a.java文件的前10行内容;
  • tail -f a.java:查看a.java文件的后10行内容;
  • head -n 7 a.java:查看a.java文件的前7行内容;
  • tail -n 7 a.java:查看a.java文件的后7行内容;

文件内部搜索指定的内容

  • grep under 123.txt:在123.txt文件中搜索under字符串,大小写敏感,显示行;
  • grep -n under 123.txt:在123.txt文件中搜索under字符串,大小写敏感,显示行及行号;
  • grep -v under 123.txt:在123.txt文件中搜索under字符串,大小写敏感,显示没搜索到的行;
  • grep -i under 123.txt:在123.txt文件中搜索under字符串,大小写敏感,显示行;
  • grep -ni under 123.txt:在123.txt文件中搜索under字符串,大小写敏感,显示行及行号;

终止当前操作

Ctrl+c和Ctrl+z都是中断命令,但是作用却不一样。

Ctrl+Z就扮演了类似的角色,将任务中断,但是任务并没有结束,在进程中只是维持挂起的状态,用户可以使用fg/bg操作前台或后台的任务,fg命令重新启动前台被中断的任务,bg命令把被中断的任务放在后台执行。

Ctrl+C也扮演类似的角色,强制终端程序的执行。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试【云端源想】!课程视频、知识库、微实战、云实验室、一对一咨询……你想要的全部学习资源这里都有,重点是现在还是免费的!点这里即可免费学习!

重定向功能

可以使用 > 或 < 将命令的输出的命令重定向到test.txt文件中(没有则创建一个)

echo 'Hello World' > /root/test.txt

1、grep(检索文件内容)

grep [options] pattern file
  • 全称:Global Regular Expression Print。
  • 作用:查找文件里符合条件的字符串。
// 从test开头文件中,查找含有start的行
grep "start" test*
// 查看包含https的行,并展示前1行(-A),后1行(-B)
grep -A 1 -B 1 "https" wget-log

2、awk(数据统计)

awk [options] 'cmd' file
  • 一次读取一行文本,按输入分隔符进行切片,切成多个组成部分。
  • 将切片直接保存在内建的变量中,$1,$2…($0表示行的全部)。
  • 支持对单个切片的判断,支持循环判断,默认分隔符为空格。
  • -F 指定分隔符(默认为空格)
    1)将email.out进行切分,打印出第1/3列内容
awk '{print $1,$3}' email.out

2)将email.out进行切分,当第1列为tcp,第2列为1的列,全部打印

awk '$1=="tcp" && $2==1{print $0}' email.out

3)在上面的基础上将表头进行打印(NR表头)

awk '($1=="tcp" && $2==1)|| NR==1 {print $0}' email.out

4) 以,为分隔符,切分数据,并打印第二列的内容

awk -F "," '{print $2}' test.txt

5)将日志中第1/3列进行打印,并对第1列的数据进行分类统计

awk '{print $1,$3}' email.out | awk '{count[$1]++} END {for(i in count) print i "\t" count[i]}'

6)根据逗号,切分数据,并将第一列存在文件test01.txt中

awk -F "," '{print $1 >> "test01.txt"}

3、sed(替换文件内容)

  • sed [option] ‘sed commond’ filename
  • 全名Stream Editor,流编辑器
  • 适合用于对文本行内容进行处理
  • sed commond为正则表达式
  • sed commond中为三个/,分别为源内容,替换后的内容

sed替换标记

g # 表示行内全面替换。
p # 表示打印行。
w # 表示把行写入一个文件。
x # 表示互换模板块中的文本和缓冲区中的文本。
y # 表示把一个字符翻译为另外的字符(但是不用于正则表达式)
\1 # 子串匹配标记
& # 已匹配字符串标记

1)替换解析

sed -i 's/^Str/String/' replace.java

Description

2)将末尾的.替换为;(转义.)

sed -i 's/\.$/\;/'

3)全文将Jack替换为me(g是全部替换,不加只替换首个)

sed -i 's/Jack/me/g/ replace.java

4)删除replace.java中的空格(d是删除)

sed -i '/^ *$/d' replace.java

5)删除包含Interger的行(d是删除)

sed -i '/Interger/d' replace.java

6)多命令一起执行

grep 'input' 123.txt | sed 's/\"//g; s/,/\n/g'

7)替换后将数据保存在文中

grep  123.txt | sed -n 's/\"//gw test01.txt'

4、管道操作符|

可将指令连接起来,前一个指令的输出作为后一个指令的输入

find ~ |grep "test"
find ~ //查找当前用户所有文件
grep "test" //从文件中

使用管道注意的要点

  • 只处理前一个命令正确输出,不处理错误输出。
  • 右边命令必须能够接收标准输入流,否则传递过程中数据会被抛弃
  • sed,awk,grep,cut,head,top,less,more,c,join,sort,split等

1)从email.log文件中查询包含error的行

grep 'error' email.log

2)获取到error的行,并取[]含有数字的

grep 'error' email.log | grep -o '\[0-9\]'

3)并过滤掉含有当前进程

ps -ef|grep tomcat |grep -v

4)替换后将数据保存在文中

grep  123.txt | sed -n 's/\"//gw test01.txt'

5)将文件123.txt,按,切分,去除",按:切分后,将第一列存到文件test01.txt中

grep 'input' 123.txt | awk -F ',' '{print $2}' | sed 's/\"//g; s/,/\n/g' | awk -F ":" 

5、cut(数据裁剪)

  • 从文件的每一行剪切字节、字符和字段并将这些字节、字符和字段输出。
  • 也可采用管道输入。

Description
文件截取

[root@VM-0-9-centos shell]# cut -d ":" -f 1 cut.txt

管道截取

[root@VM-0-9-centos shell]# echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin

# 按:分割。截取第3列
[root@VM-0-9-centos shell]# echo $PATH | cut -d ":" -f 3
/usr/sbin

# 按:分割。截取第3列之后数据
[root@VM-0-9-centos shell]# echo $PATH | cut -d ":" -f 3-
/usr/sbin:/usr/bin:/root/bin
[root@VM-0-9-centos shell]#

四、系统日志位置

  • cat /etc/redhat-release:查看操作系统版本
  • /var/log/message:系统启动后的信息和错误日志,是Red Hat Linux中最常用的日志之一
  • /var/log/message:系统启动后的信息和错误日志,是Red Hat Linux中最常用的日志之一
  • /var/log/secure:与安全相关的日志信息
  • /var/log/maillog:与邮件相关的日志信息
  • /var/log/cron:与定时任务相关的日志信息
  • /var/log/spooler:与UUCP和news设备相关的日志信息
  • /var/log/boot.log:守护进程启动和停止相关的日志消息

查看某文件下的用户操作日志
到达操作的目录下,执行下面的程序:

cat .bash_history

五、创建与删除软连接

1、创建软连接

ln -s /usr/local/app /data

注意:创建软连接时,data目录后不加 / (加上后是查找其下一级目录);
Description

2、删除软连接

rm -rf /data

注意:取消软连接最后没有/,rm -rf 软连接。加上/是删除文件夹;
Description

六、压缩和解压缩

tar
Description
压缩(-c)

tar -cvf start.tar a.java b.java  //将当前目录下a.java、b.java打包
tar -cvf start.tar ./* //将当前目录下的所欲文件打包压缩成haha.tar文件

tar -zcvf start.tar.gz a.java b.java //将当前目录下a.java、b.java打包
tar -zcvf start.tar.gz ./* //将当前目录下的所欲文件打包压缩成start.tar.gz文件

解压缩(-x)

tar -xvf start.tar      //解压start.tar压缩包,到当前文件夹下;
tar -xvf start.tar -C usr/local //(C为大写,中间无空格)
//解压start.tar压缩包,到/usr/local目录下;
tar -zxvf start.tar.gz //解压start.tar.gz压缩包,到当前文件夹下;
tar -zxvf start.tar.gz -C usr/local //(C为大写,中间无空格)
//解压start.tar.gz压缩包,到/usr/local目录下;

解压缩tar.xz文件

tar xf node-v12.18.1-linux-x64.tar.xz

unzip/zip

压缩(zip)

zip lib.zip tomcat.jar       //将单个文件压缩(lib.zip)
zip -r lib.zip lib/ //将目录进行压缩(lib.zip)
zip -r lib.zip tomcat-embed.jar xml-aps.jar //将多个文件压缩为zip文件(lib.zip)

解压缩(unzip)

unzip file1.zip          //解压一个zip格式压缩包
unzip -d /usr/app/com.lydms.english.zip //将`english.zip`包,解压到指定目录下`/usr/app/`

七、Linux下文件的详细信息

R:Read  w:write  x: execute执行
-rw-r--r-- 1 root root 34942 Jan 19 2018 bootstrap.jar
  • 前三位代表当前用户对文件权限:可以读/可以写/不能执行
  • 中间三位代表当前组的其他用户对当前文件的操作权限:可以读/不能写/不能执行
  • 后三位其他用户对当前文件权限:可以读/不能写/不能执行图片

Description

更改文件的权限

chmod u+x web.xml (---x------)  为文件拥有者(user)添加执行权限;
chmod g+x web.xml (------x---) 为文件拥有者所在组(group)添加执行权限;
chmod 111 web.xml (---x--x--x) 为所有用户分类,添加可执行权限;
chmod 222 web.xml (--w--w--w-) 为所有用户分类,添加可写入权限;
chmod 444 web.xml (-r--r--r--) 为所有用户分类,添加可读取权限;

八、Linux终端命令格式

command [-options] [parameter]

说明:

  • command :命令名,相应功能的英文单词或单词的缩写
  • [-options] :选项,可用来对命令进行控制,也可以省略
  • parameter :传给命令的参数,可以是0个、1个或者多个

查阅命令帮助信息

-help: 显示 command 命令的帮助信息;
-man: 查阅 command 命令的使用手册,man 是 manual 的缩写,是 Linux 提供的一个手册,包含了绝大部分的命令、函数的详细使用。

使用 man 时的操作键

Description

以上就是一些Linux常用操作命令的介绍,希望对你有所帮助。

虽然这些只是Linux命令的冰山一角,但它们足以让你自如地运用Linux操作系统,记住,每一个命令都有其独特的用途和魅力。掌握了这些命令,你就能更加自如地在Linux世界中遨游。愿你在探索的道路上,发现更多的惊喜和乐趣!

收起阅读 »

北京职场50万定律:在北京不论你在任何单位工作,只要年收入大于50w,基本上都要牺牲个人生活

大家有没有注意到北京职场里的一个不成文的规则?就是,不管你在哪个单位,干什么活,只要年薪过了50万,基本上个人生活就得打个折扣。无论是教书、看病,还是在金融和互联网领域打拼,或是在央企、民企工作,年入五十万似乎成了个隐形的分水岭。当然,销售岗位除外,那里可能情...
继续阅读 »
大家有没有注意到北京职场里的一个不成文的规则?

就是,不管你在哪个单位,干什么活,只要年薪过了50万,基本上个人生活就得打个折扣。

无论是教书、看病,还是在金融和互联网领域打拼,或是在央企、民企工作,年入五十万似乎成了个隐形的分水岭。当然,销售岗位除外,那里可能情况会有点不同。

但大体上,挣得多,似乎就得在个人时间上付出更多。

北京这地儿,竞争激烈,生活成本高。这就导致了“高薪等于高投入”的默认规则。

想挣大钱,自然得付出相应的努力和时间。这里的“牺牲”,就不仅仅是晚上加个班、周末去办公室那么简单,更多的是一种持续性的、深入骨髓的工作状态。

再看看我们周围,无论是医生还是教师,这些本来应该是相对稳定的职业,现在也变得跟时代的步伐紧密相连。

医生要不断学习新技术,教师要跟上教育的最新趋势。在金融或互联网行业就更不用说了,几乎每时每刻都在发生变化,稍有不慎,就可能被淘汰。

这种压力下,不仅仅是时间的牺牲,还有心理上的压力和身体上的消耗。

那些年薪过50万的人,大多数都不是刚入门的新手,而是那些担任一定职位、肩负一定责任的中高层管理者。

他们不仅要管理好自己的工作,还要带领团队达成目标。这里面的付出,远远超过了普通员工。

作为领导者,他们需要有自我牺牲的精神,不仅要把工作做好,还要让团队成员感到鼓舞和尊重。

但这就带来了一个问题,工作和生活的平衡怎么办?在这样的工作强度下,家庭、朋友、爱好,甚至是基本的休息和锻炼时间,都可能被挤压。有的人为了工作,可能连基本的身体健康都顾不上。

长此以往,无论是身体还是心理上都可能出现问题。

这种“牺牲个人生活”的现象,在体制内也同样存在。想象一下,一个普通的公务员或企业职员,如果只是每天按时上下班,不加班不出差,他的年薪可能也就在25万到35万之间。

但如果想要年薪超过50万,那就必须得承担更多的工作,比如疯狂地做业务,或者成为领导,这几乎意味着要把全部的精力和时间都投入到工作中去,个人生活自然会受到很大影响。

这里的“50万定律”并不是一个精确的数字,可能在40万到60万之间都有类似的现象。

有些行业里,三十岁之前如果年薪没达到这个水平,可能就被认为是没什么前途;而有些行业则突然间成为热门,员工的收入在短时间内暴涨,这都是市场变化的常态

但不可否认的是,这种现象背后反映出的是一个更深层次的社会和文化问题。

在北京这样一个高度竞争的环境下,很多人为了职业成功,不得不放弃其他很多东西,比如时间、家庭甚至是自己的价值观和人格。

说实话,这个“50万定律”真是让人又爱又恨。我们都知道,钱虽然不是万能的,但没钱是万万不能的。

在北京这样的大城市里,不拼一拼,可能连基本的生活水平都难以保障。所以,能拿到高薪的人,确实值得尊敬。

他们的努力和付出是显而易见的。

但话说回来,这种高强度的工作压力真的值得吗?工作再好,钱再多,如果没有时间和精力去享受生活,那这一切又有什么意义呢?

有时候,我真的在想,我们这些在职场打拼的人,是不是都陷入了一个误区:认为只有工作成功了,人生才算成功。这种想法真的对吗?

我觉得,工作是为了更好的生活,而不是生活只为了工作。我们要追求的,应该是一种平衡。不是说不努力工作,而是在努力工作的同时,也要关注自己的身心健康,家庭和人际关系。

毕竟,当我们老了回头看这一生的时候,可能不会因为多挣了几个钱而感到自豪,反而会因为错过了孩子的成长、家人的陪伴而感到遗憾。


作者:升职笔谈
来源:mp.weixin.qq.com/s/Ku-qjNYERd2sqWuNA7IwCw

收起阅读 »

互联网大厂,开始对领导层动刀了

最近,我周围有挺多互联网大厂leader级别的同事或朋友,被“降本增效”了。 其中最有意思的是,我的前同事老Z今年刚刚晋升了一级,在这个级别上还没待热乎了,然后就下来了。 有句话是这么说的:“世界上最残忍的事,莫过于让其拥有一切,然后再剥夺其所有。” 有次我跟...
继续阅读 »

最近,我周围有挺多互联网大厂leader级别的同事或朋友,被“降本增效”了。


其中最有意思的是,我的前同事老Z今年刚刚晋升了一级,在这个级别上还没待热乎了,然后就下来了。


有句话是这么说的:“世界上最残忍的事,莫过于让其拥有一切,然后再剥夺其所有。”


有次我跟老Z吃饭,他苦笑着跟我说:“妈的,如果不晋升,没准还能待下去呢,晋升之后反而目标变大了。”


我问他:“那你最近看新机会的结果怎么样,有没有拿到比较满意的offer呢?”


他说:“面试机会倒是不少,大厂已经面了五六个,但最后都无疾而终了。”


接下来,他又把话题聊了回来,说:“你说,如果公司对我不满意,为什么还给我晋升呢,但如果公司对我满意,又为什么还要裁我呢?”


我给他举了一个这样的例子:“就算大款给小三买奢侈品,让她住豪宅,但并不代表不会甩了她啊,对吧。”


他听了哈哈大笑,似乎释怀了。


接下来,我盘点一下,具备什么特征的管理层最容易被“降本增效”,以及在未来的日子里,我们应该如何应对这种不确定性。


“降本增效”画像


跟大家聊下,哪类用户画像的领导层最容易被“降本增效”,请大家对号入座,别心存侥幸。


(1)非嫡系


不管到哪天,大厂也都是个江湖,是江湖就有人情世故。


如果你不是老板的嫡系,那公司裁员指标下来了,你不背锅谁背锅,你不下地狱谁下地狱。


你可能会说:“我的能力比老板的嫡系强啊,公司这种操作,不成了劣币驱逐良币了吗?”


其实,这个时候对于公司来说,无论是劣币还是良币,都不如人民币来得实在。


人员冗余对于公司来讲就是负担,这个时候谁还跟你讲任人唯亲还是任人唯贤啊。


(2)老员工


可能有人会这么认为,老员工不但忠诚,而且N+1赔的钱也多,为什么会优先裁掉老员工呢。


我认为,一个员工年复一年、日复一日地待在熟悉的工作环境,就犹如温水煮青蛙一样,很容易停留在舒适区,有的甚至混成了老油子。


而老板最希望看到的是,人才要像水一样流动起来,企业要像大自然一样吐故纳新,这样才会一直保持朝气和活力。


总之,老板并不认为员工和公司一起慢慢变老,是一件最浪漫的事。


(3)高职级


对于公司来讲,职级越高的员工,薪资成本也就越高,如果能够创造价值,那自不必多说,否则的话,呵呵呵。。。


现在越来越多的公司,在制定裁员目标的时候,已经不是要裁掉百分之多少的人了,而是裁员后把人均薪资降到多少。


嗯,这就是传说中的“降均薪”,目标用户是谁,不多说也知道了吧?


(4)高龄


35+,40+,嗯,你懂的。


老夫少妻难和谐,大龄下属跟小领导不和谐的几率也很大,一个觉得年轻人不要抬气盛,另外一个觉得不气盛就不是年轻人。


不确定性——在职


恭喜你,幸存者,老天确实待你不薄,在应对不确定性这件事情上,给了你一段时间来缓冲。


如果你已经35+了,那接下来你需要把在职的每一天,都当成是最后一天来度过,然后疯狂地给自己找后路,找副业。


一定要给你自己压力,给自己紧迫感。


因为说不定哪天,曾经对你笑圃如花的HR,会忽然把你叫到一个偏僻的会议室里,面无表情地递给你一式两份的离职协议书,让你签字。


在你心乱如麻地拿起签字笔之际,她没准还得最后PUA你几句:“这次公司不是裁员,而是优化。你要反思自己过去的贡献,认识到自己的不足,这样才能持续发展。


当然,你有大厂员工的光环加持,到市场上还是非常抢手的,你要以人才输出的高度来看这次优化,为社会做贡献。”


至于找后路和副业的方式,现在网上有很多类似的星球,付费和免费的都有,加一个进去,先好好看看,主要是先把思路和视野打开。


当然,如果你周围要是有一个副业做得比较好的同事,并且他愿意言传身教你,那就更好了。


然后,找一个自己适合的方向和领域,动手去做,一定动手去做,先迈出第一步,可以给自己定一个小目标,在未来几个月内,从副业中赚到第一次钱。


从0到1最难,再接下来,应该就顺了。


不确定性——不在职


如果35+的你刚刚下来,而且手头还算殷实的话,我先劝你第一件事:放弃重返职场。


原因很简单,如果一个方向,随着你经验的积累和年龄的增长,不仅不会带来复利,而是路会越走越窄,那你坚持的意义是什么?难道仅仅是凑合活着吗?


第二件事,慢下来,别立马急急忙忙地找出路,更不要一下子拿出很多本金砸在一个项目上。据说,有的项目是专门盯着大厂员工的遣散费来割韭菜的。


有人会说,在职的人你劝要有紧迫感,离职的人你又劝慢下来,这不是“劝风尘从良,逼良家为娼”吗?


其实不是的,只是无论是在职还是离职,我们都需要在某件事情的推进上,保持一个适合且持久的节奏,不要止步不前,也不要急于求成,用力过猛。


第三件事,就是舍得把面子喂狗,不要觉得做这个不体面,做那个有辱斯文,只要在合理合法的情况下,能赚到钱才是最光荣的。


接下来,盘点周围可用资源,调研有哪些领域和方向适合你,并愿意投入下半生的精力all in去做。


这个过程可能会很痛苦,尤其对于一些悲观者来说,一上来会有一种“世界那么大,竟然再也找不到一个我能谋生的手段”的感觉,咬牙挺过去就好了。


这里说一句,人只要自己不主动崩,还是远比想象中耐操很多的。


结语


好像也没什么好说的,大家各自安好,且行且珍惜吧。


作者:托尼学长
来源:juejin.cn/post/7317859658285318170
收起阅读 »

一行代码快速实现全局模糊

web
github 仓库:github.com/astak16/blu…npm 仓库:http://www.npmjs.com/package/blu…页面在展示时,某些敏感的数据不想展示,可以使用该插件,对敏感数据进行模糊处理敏感数据过滤通常是由后端去做的,有时候...
继续阅读 »

github 仓库:github.com/astak16/blu…

npm 仓库:http://www.npmjs.com/package/blu…

页面在展示时,某些敏感的数据不想展示,可以使用该插件,对敏感数据进行模糊处理

敏感数据过滤通常是由后端去做的,有时候在某些场合不想展示某些数据,这时让后端去改代码,再重新部署,这样的成本太高,所以前端也需要有这样的能力,可以在前端对敏感数据进行模糊处理,这样就不需要后端参与了

前端过滤文本,通常有两种方法:

  1. 拦截响应,对文本进行过滤后在渲染在页面上
  2. 渲染在页面上后,对文本进行过滤

对于方案一,需要遍历所有的响应对象的所有属性,而且只能替换或者删除文本,无法实现个性化的配置

所以我选择了方案二,当页面渲染结束后,遍历所有的 dom,对文本进行过滤,这样可以实现个性化的配置

遍历 dom,你能想到最直接的方法是:

  1. document.querySelectorAll("*")
  2. document.body.getElementsByTagName("*")

然后在遍历找到的所有 dom,对文本进行过滤,这样效率会很差

所以我选择了 TreeWalker 遍历 dom,效率会高很多

技术说明

treeWalker 是通过 document.createTreeWalker() 创建的,传入 NodeFilter.SHOW_TEXT 参数,就可以拿到页面中所有的文本节点

然后用正则匹配文本,如果匹配到了,就对文本进行模糊处理:

const wrapTextWithTag = (node: Node) => {
const oldText = node?.textContent;
if (!oldText) return;
const regexNumber = /[-+]?\d{1,3}(?:,\d{3})*(?:\.\d+)?/;
const regexWord = new RegExp(`(${this.words.join("|")})`);
const mergedRegex = new RegExp(
`(${regexNumber.source}|${regexWord.source})`,
"g"
);

if (mergedRegex.test(oldText)) {
const rep = oldText.replace(mergedRegex, "$1");
const arr = rep.split(/<\/?span>/);
let span;
node.textContent = "";
for (let i = 0; i < arr.length; i++) {
const newText = arr[i];
const isContainsWord = this.isContainsWord(newText);
const isContainsNumber = this.isContainsNumber(newText);
if (!this.isVoid(newText) && (isContainsWord || isContainsNumber)) {
span = this.createElementAndSetBlurred(newText);
} else {
span = document.createTextNode(newText);
}
node.parentElement?.insertBefore(span, node);
}
}
};

如果到此结束的话,只能处理静态的文本,如果是异步数据,就无法处理了,所以还需要监听 dom 的变化,对新增的 dom 进行模糊处理

监听 dom 可以使用 MutationObserver,将 subtree 设置为 true,就可以监听到所有的 dom 变化

const observer = new MutationObserver((mutationList) => {
mutationList.map((mutation) => {
const node = mutation.addedNodes[0];
if (!node) return;
const isStyle = this.hasStyle(node);
if (node.nodeType === 1 && !isStyle && !this.isVoid(node.textContent)) {
this.blurryWordsWithTag(node);
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
this.observer = observer;

这里需要说明一点:默认不开启异步数据模糊处理,因为这样会影响性能,如果需要开启,设置 autoBlur 为 true

推荐使用方式:页面设置个按钮,点击按钮开启模糊,这样可以避免一些性能问题

使用

  1. 安装
npm i blurryjs
# 或
yarn add blurryjs
# 或
pnpm i blurryjs
  1. 引入
import Blur from "blurryjs";
  1. 快速使用
const blur = new Blur(); // 默认全局数字模糊

参数说明

5 个参数

  1. blur?: number:模糊程度,单位:px,默认 2px
  2. blurryWords?: string[]:需要模糊的词,默认数字模糊
  3. operator?: string:默认模糊,可以是其他符号,比如 * 等
  4. autoBlur?: boolean:是否自动模糊,默认 false,只处理静态文本,如果需要处理异步数据,需要设置为 true
  5. excludes?: string[]:排除不需要模糊的 dom,类名,比如 ["ant-input"]
const blur = new Blur({
blur: 2,
blurryWords: ["周杰伦", "陈奕迅"],
operator: "*",
autoBlur: false,
excludes: ["ant-input"],
});

3 个方法:

  1. enableBlur(options):开启模糊,options 参数为:blurblurryWordsoperatorautoBlur

    blur.enableBlur({
    blur: 2,
    blurryWords: ["1", "2", "3"],
    operator: "*",
    autoBlur: false,
    });
  2. disableBlur:关闭模糊

    blur.disableBlur();
  3. destroy:销毁

    blur.destroy();

demo

git clone https://github.com/astak16/blurryjs.git

cd blurryjs

pnpm i

pnpm dev

效果

Kapture 2023-06-30 at 14.48.53.gif


作者:uccs
来源:juejin.cn/post/7250311237107122232github 仓库:github.com/astak16/blu…

npm 仓库:http://www.npmjs.com/package/blu…

页面在展示时,某些敏感的数据不想展示,可以使用该插件,对敏感数据进行模糊处理

敏感数据过滤通常是由后端去做的,有时候在某些场合不想展示某些数据,这时让后端去改代码,再重新部署,这样的成本太高,所以前端也需要有这样的能力,可以在前端对敏感数据进行模糊处理,这样就不需要后端参与了

前端过滤文本,通常有两种方法:

  1. 拦截响应,对文本进行过滤后在渲染在页面上
  2. 渲染在页面上后,对文本进行过滤

对于方案一,需要遍历所有的响应对象的所有属性,而且只能替换或者删除文本,无法实现个性化的配置

所以我选择了方案二,当页面渲染结束后,遍历所有的 dom,对文本进行过滤,这样可以实现个性化的配置

遍历 dom,你能想到最直接的方法是:

  1. document.querySelectorAll("*")
  2. document.body.getElementsByTagName("*")

然后在遍历找到的所有 dom,对文本进行过滤,这样效率会很差

所以我选择了 TreeWalker 遍历 dom,效率会高很多

技术说明

treeWalker 是通过 document.createTreeWalker() 创建的,传入 NodeFilter.SHOW_TEXT 参数,就可以拿到页面中所有的文本节点

然后用正则匹配文本,如果匹配到了,就对文本进行模糊处理:

const wrapTextWithTag = (node: Node) => {
const oldText = node?.textContent;
if (!oldText) return;
const regexNumber = /[-+]?\d{1,3}(?:,\d{3})*(?:\.\d+)?/;
const regexWord = new RegExp(`(${this.words.join("|")})`);
const mergedRegex = new RegExp(
`(${regexNumber.source}|${regexWord.source})`,
"g"
);

if (mergedRegex.test(oldText)) {
const rep = oldText.replace(mergedRegex, "$1");
const arr = rep.split(/<\/?span>/);
let span;
node.textContent = "";
for (let i = 0; i < arr.length; i++) {
const newText = arr[i];
const isContainsWord = this.isContainsWord(newText);
const isContainsNumber = this.isContainsNumber(newText);
if (!this.isVoid(newText) && (isContainsWord || isContainsNumber)) {
span = this.createElementAndSetBlurred(newText);
} else {
span = document.createTextNode(newText);
}
node.parentElement?.insertBefore(span, node);
}
}
};

如果到此结束的话,只能处理静态的文本,如果是异步数据,就无法处理了,所以还需要监听 dom 的变化,对新增的 dom 进行模糊处理

监听 dom 可以使用 MutationObserver,将 subtree 设置为 true,就可以监听到所有的 dom 变化

const observer = new MutationObserver((mutationList) => {
mutationList.map((mutation) => {
const node = mutation.addedNodes[0];
if (!node) return;
const isStyle = this.hasStyle(node);
if (node.nodeType === 1 && !isStyle && !this.isVoid(node.textContent)) {
this.blurryWordsWithTag(node);
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
this.observer = observer;

这里需要说明一点:默认不开启异步数据模糊处理,因为这样会影响性能,如果需要开启,设置 autoBlur 为 true

推荐使用方式:页面设置个按钮,点击按钮开启模糊,这样可以避免一些性能问题

使用

  1. 安装
npm i blurryjs
# 或
yarn add blurryjs
# 或
pnpm i blurryjs
  1. 引入
import Blur from "blurryjs";
  1. 快速使用
const blur = new Blur(); // 默认全局数字模糊

参数说明

5 个参数

  1. blur?: number:模糊程度,单位:px,默认 2px
  2. blurryWords?: string[]:需要模糊的词,默认数字模糊
  3. operator?: string:默认模糊,可以是其他符号,比如 * 等
  4. autoBlur?: boolean:是否自动模糊,默认 false,只处理静态文本,如果需要处理异步数据,需要设置为 true
  5. excludes?: string[]:排除不需要模糊的 dom,类名,比如 ["ant-input"]
const blur = new Blur({
blur: 2,
blurryWords: ["周杰伦", "陈奕迅"],
operator: "*",
autoBlur: false,
excludes: ["ant-input"],
});

3 个方法:

  1. enableBlur(options):开启模糊,options 参数为:blurblurryWordsoperatorautoBlur

    blur.enableBlur({
    blur: 2,
    blurryWords: ["1", "2", "3"],
    operator: "*",
    autoBlur: false,
    });
  2. disableBlur:关闭模糊

    blur.disableBlur();
  3. destroy:销毁

    blur.destroy();

demo

git clone https://github.com/astak16/blurryjs.git

cd blurryjs

pnpm i

pnpm dev

效果

Kapture 2023-06-30 at 14.48.53.gif


作者:uccs
来源:juejin.cn/post/7250311237107122232
收起阅读 »