注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

如何优雅地校验后端接口数据,不做前端背锅侠

web
背景 最近新接手了一批项目,还没来得及接新需求,一大堆bug就接踵而至,仔细一看,应该返回数组的字段返回了 null,或者没有返回,甚至返回了字符串 "null"??? 这我能忍?我立刻截图发到群里,用红框加大加粗重点标出。后端同学也积极响应,答应改正。 第...
继续阅读 »

背景


最近新接手了一批项目,还没来得及接新需求,一大堆bug就接踵而至,仔细一看,应该返回数组的字段返回了 null,或者没有返回,甚至返回了字符串 "null"???


这我能忍?我立刻截图发到群里,用红框加大加粗重点标出。后端同学也积极响应,答应改正。


image.png


第二天,同样的事情又在其他的项目上演,我只是一个小前端,为什么什么错都找我啊!!


日子不能再这样下去,于是我决定写一个工具来解决遇到 bug 永远在找前端的困境。


TypeScript 运行时校验


如何对接口数据进行校验呢,因为我们的项目是 React+TypeScript 写的,所以第一时间就想到了使用 TypeScript 进行数据校验。但是众所周知,TypeScript 用于编译时校验,有没有办法作用到运行时呢?


我还真找到了一些运行时类型校验的库:typescript-needs-types,大部分需要使用指定格式编写代码,相当于对项目进行重构,拿其中 star 最多的 zod 举例,代码如下。


import { z } from "zod";

const User = z.object({
username: z.string(),
});

User.parse({ username: "Ludwig" });

// extract the inferred type
type User = z.infer<typeof User>;
// { username: string }

我宁可查 bug 也不可能重构手里一大堆项目啊。此种方案 ❎。


此时看到了 typescript-json-schema 可以把 TypeScript 定义转为 JSON Schema ,然后再使用 JSON Schema 对数据进行校验就可以啦。这种方案比较灵活,且对代码入侵性较小。


搭建一个项目测试一下!


使用 npx create-react-app my-app --template typescript 快速创建一个 React+TS 项目。


首先安装依赖 npm install typescript-json-schema


创建类型文件 src/types/user.ts


export interface IUserInfo {
staffId: number
name: string
email: string
}

然后创建 src/types/index.ts 文件并引入刚才的类型。


import { IUserInfo } from './user';

interface ILabel {
id: number;
name: string;
color: string;
remark?: string;
}

type ILabelArray = ILabel[];

type IUserInfoAlias = IUserInfo;

接下来在 package.json 添加脚本


"scripts": {
// ...
"json": "typescript-json-schema src/types/index.ts '*' -o src/types/index.json --id=api --required --strictNullChecks"
}

然后运行 npm run json 可以看到新建了一个 src/types/index.json 文件(此步在已有项目中可能会报错报错,可以尝试在 json 命令中添加 --ignoreErrors 参数),打开文件可以看到已经成功转成了 JSON Schema 格式。


{
"$id": "api",
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ILabel": {
"properties": {
"color": {
"type": "string"
},
"id": {
"type": "number"
},
"name": {
"type": "string"
},
"remark": {
"type": "string"
}
},
"required": [
"color",
"id",
"name"
],
"type": "object"
},
"ILabelArray": {
"items": {
"$ref": "api#/definitions/ILabel"
},
"type": "array"
},
"IUserInfoAlias": {
"properties": {
"email": {
"type": "string"
},
"name": {
"type": "string"
},
"staffId": {
"type": "number"
}
},
"required": [
"email",
"name",
"staffId"
],
"type": "object"
}
}
}

使用 JSON Schema 校验数据


至于如何使用JSON Schema 校验数据,我找到了现成的库 ajv,至于为什么选择 ajv,主要是因为它说它很快,详见:github.com/ebdrup/json…


image.png


接下来尝试一下。我找到了中文版文档,有兴趣的可以去看下 http://www.febeacon.com/ajv-docs-zh…


先安装依赖 npm install ajv,然后创建文件 src/validate.ts


import Ajv from 'ajv';
import schema from './types/index.json';

const ajv = new Ajv({ schemas: [schema] });

export function validateDataByType(type: string, data: unknown) {
console.log(`开始校验,类型:${type}, 数据:`, data);

var validate = ajv.getSchema(`api#/definitions/${type}`);
if (validate) {
const valid = validate(data);
if (!valid) {
console.log('校验失败', validate.errors);
}
else {
console.log('校验成功');
}
}
}

接下来在 src/index.tsx 添加下面代码来测试一下。


validateDataByType('IUserInfoAlias', {
email: 'idonteatcookie@gmail.com',
name: 'idonteatcookie',
staffId: 12306
})

validateDataByType('IUserInfoAlias', {
email: 'idonteatcookie@gmail.com',
staffId: 12306
})

validateDataByType('IUserInfoAlias', {
email: 'idonteatcookie@gmail.com',
name: 'idonteatcookie',
staffId: '12306'
})

可以在控制台看到成功打印如下信息:


image.png


拦截请求


因为项目中发送请求都是调用统一封装的函数,所以我首先想到的是在函数中增加一层校验逻辑。但是这样的话就与项目代码耦合严重,换一个项目又要再写一份。我真的有好多项目QAQ。


那干脆拦截所有请求统一处理好了。


很容易的找到了拦截所有 XMLHttpRequest 请求的库 ajax-hook,可以非常简单地对请求做处理。


首先安装依赖 npm install ajax-hook,然后创建 src/interceptTool.ts


import { proxy } from 'ajax-hook';
export function intercept() {
// 获取 XMLHttpRequest 发送的请求
proxy({
onResponse: (response: any, handler: any) => {
console.log('xhr', response.response)
handler.next(response);
},
});
}

这样就拦截了所有的 XMLHttpRequest 发送的请求,但是我突然想到我们的项目,好像使用 fetch 发送的请求来着???


好叭,那就再拦截一遍 fetch 发送的请求。


export function intercept() {
// ...
const { fetch: originalFetch } = window;
// 获取 fetch 发送的请求
window.fetch = async (...args) => {
const response = await originalFetch(...args);
response.clone().json().then((data: { result: any }) => {
console.log('window.fetch', args, data);
return data;
});
return response;
};
}

为了证明拦截成功,使用 json-server 搭建一个本地 mock 服务器。首先安装 npm install json-server,然后在根目录创建文件 db.json


{
"user": { "staffId": 1, "name": "cookie1", "email": "cookie@cookie.com" },
"labels": [
{
"id": 1,
"name": "ck",
"color": "red",
"remark": "blabla"
},
{
"id": 2,
"color": "green"
}
]
}

再在 package.json 添加脚本


"scripts": {
"serve": "json-server --watch db.json -p 8000"
},

现在执行 npm run serve 就可以启动服务器了。在 src/index.tsx 增加调用接口的代码,并引入 src/interceptTool.ts


import { intercept } from './interceptTool';
// ... other code
intercept();

fetch('http://localhost:8000/user');

const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:8000/labels');
xhr.send();

image.png


可以看到两种请求都拦截成功了。


校验接口返回数据


胜利在望,只差最后一步,校验返回数据。我们校验数据需要提供两个关键信息,数据本身和对应的类型名,为了将两者对应起来,需要再创建一个映射文件,把 url 和类型名对应起来。


创建文件 src/urlMapType.ts 然后添加内容


export const urlMapType = {
'http://localhost:8000/user': 'IUserInfoAlias',
'http://localhost:8000/labels': 'ILabelArray',
}

我们在 src/validate.ts 新增函数 validateDataByUrl


import { urlMapType } from './urlMapType';
// ...
export function validateDataByUrl(url: string, data: unknown) {
const type = urlMapType[url as keyof typeof urlMapType];
if (!type) {
// 没有定义对应格式不进行校验
return;
}
console.log(`==== 开始校验 === url ${url}`);
validateDataByType(type, data);
}

然后在 src/interceptTool.ts 文件中引用


import { proxy } from 'ajax-hook';
import { validateDataByUrl } from './validate';

export function intercept() {
// 获取 XMLHttpRequest 发送的请求
proxy({
onResponse: (response, handler: any) => {
validateDataByUrl(response.config.url, JSON.parse(response.response));
handler.next(response);
},
});

const { fetch: originalFetch } = window;
// 获取 fetch 发送的请求
window.fetch = async (...args) => {
const response = await originalFetch(...args);
response.json().then((data: any) => {
validateDataByUrl(args[0] as string, data);
return data;
});
return response;
};
}

现在可以在控制台看到接口数据校验的接口辣~ ✿✿ヽ(°▽°)ノ✿


image.png


总结下流程图


image.png


后续规划


目前所做的事情,准确的说不是拦截,只是获取返回数据,然后对比打印校验结果,因为初步目标不涉及数据的处理。


后续会考虑对不合法的数据进行处理,比如应该返回数组但是返回了 null 的情况,如果能自动赋值 [],就可以防止前端页面崩溃的情况了。


参考资料




作者:我不吃饼干
来源:juejin.cn/post/7166061734803963917
收起阅读 »

GPT-3.5&速搭微信小程序

前言 想必大家都收到OpenAI开放了ChatGPT API的消息,之前的API也尝试过接入过个人微信号,给好基友们体验体验,但是效果不太好,已经被他们干废一个小号。 公众号又有三秒断连的机制,也就没写下去了,这次整个小程序。 ChatGPT API 先来看...
继续阅读 »

前言


想必大家都收到OpenAI开放了ChatGPT API的消息,之前的API也尝试过接入过个人微信号,给好基友们体验体验,但是效果不太好,已经被他们干废一个小号。


image.png


公众号又有三秒断连的机制,也就没写下去了,这次整个小程序。


ChatGPT API


先来看看ChatGPT API中我觉得最大的一个亮点:


  messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Who won the world series in 2020?"},
{"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},
{"role": "user", "content": "Where was it played?"}
]
)

与之前不同的是,我们可以以数组的形式存储上下文消息,不用像之前那样,想要实现多轮对话就得把消息全拼在一起,同时还显式的定义了角色,role属性表示其所属的角色,其中system是定于接口的行为,在对话中可以先使用system进行格式化,比如【你是一个前端工程师】,而user和assistant则是表示用户输入消息以及模型输出消息。通过这些定义,可以使模型变得更加清晰明确。


了解完信息格式后,我们直接上代码跑一下


const { Configuration, OpenAIApi } = require("openai");
const configuration = new Configuration({
apiKey: "YOU APIKEY",
});
const openai = new OpenAIApi(configuration);
const chat = async (content) => {
try {
const {data: { choices },} = await openai.createChatCompletion({
model: "gpt-3.5-turbo",
messages: content,
});
return choices[0].message;
} catch (error) {
return error;
}
};
let content = [{ role: "user", content: "你好" }];
chat(content).then((res)=>{
console.log(res)
})

返回结果如下:


image.png
我们成功与ChatGPT进行了一次对话,不过在此之前,还需要接收前端发送过来的信息,所以我们在外面套一个express


express


const express = require("express");
const app = express();
app.use(express.json());

app.post("/chat", (req, res) => {
let content =req.body.content;
chat(content).then((data)=>{
res.send(data);
})
});
app.listen(8080, () => {
console.log("Server is running");
});

//const { Configuration ...
//const configuration ...
// ...

image.png


一个简单的接口就这样完成了。


关于其他参数的应用


目前我们只使用了model和messages参数,其中messages还可以选择性的去除对话中的信息,ChatGPT API是每次都要传输完整的上下文消息的,所以我们还可以与前端联动,选择删除一些无用的信息,保留有用的信息,提高回答的准确性。


n:一次性返回多个不同值,可以避免多次重复询问


logit_bias:调整特定词汇在结果中出现的可能性,可以实现减少重复和屏蔽特定词汇的功能


temperature/top_p:调整结果的相关性和准确性,可以在需要发散思维时使用,官方建议二者调整其中一个,不建议二者都调整。


这些参数也可以在前端通过发送指令的方式进行调整。


微信小程序


image.png


对话界面


我们基于messages来渲染对话界面


 <view wx:for="{{messages}}" wx:key="index" style="margin: 40rpx 0">
<view class="user" wx:if="{{item.role=='user'}}">
<image class="avatar" src="../../image/NO.0004.png"></image>
<text class="content" bindtap="copyText" data-text="{{item.content}}">{{
item.content
}}</text>
</view>
<view class="assistant" wx:if="{{item.role=='assistant'}}">
<image class="avatar" src="../../image/logo.png"></image>
<text class="content" bindtap="copyText" data-text="{{item.content}}">{{
item.content
}}</text>
</view>
</view>

通过flex-direction区分用户与ChatGPT


.user {
display: flex;
justify-content: space-evenly;
flex-direction: row-reverse;
}
.assistant {
display: flex;
justify-content: space-evenly;
}

发送事件


send函数主要流程是,把用户输入的信息push到messages,同时在界面中渲染,然后向api发送请求,返回信息后再push到messages中,同时在界面中渲染。


 send: function (e) {
if(!/^\s*$/.test(this.data.inputValue)){
let message = {"role": "user", "content":this.data.inputValue}
this.data.messages.push(message)
this.setData({
messages:this.data.messages,
inputValue:''
});
wx.request({
url: 'http://127.0.0.1:8080/chat',
method: 'POST',
data: {
content: this.data.messages,
},
header: {
'content-type': 'application/json'
},
success:(res)=>{
this.data.messages.push(res.data.message)
this.setData({
messages:this.data.messages
});
}
})
}else{
wx.showToast({
title: '输入不可为空',
duration: 500,
icon: 'none'
});
}

结尾


3月2号下午我写node的时候还能用,今天晚上写小程序时发现寄了,api应该是给墙了(一时间竟不知道哪边动的手),需要梯子。


关于send函数这里再细锁细琐,上面的传输方式随着聊天越聊越长,原方案是想发送给express做存储,前端每次只需要发送一条message,同时也能做聊天记录的保存,不过这不像在云开发中可以很方便的获取用户的openid,虽然可以用wx.login,但是今天是周五欸,下机。


代码先丢上去了
小程序代码


image.png


作者:Defineee
来源:juejin.cn/post/7206292770276163640
收起阅读 »

Spring Boot+微信小程序_保存微信登录者的个人信息

1. 前言 微信小程序开发平台,提供有一类 API,可以让开发者获取到微信登录用户的个人数据。这类 API 统称为开放接口。 Tip:微信小程序开发平台,会把微信登录用户的个人信息分为明文数据和敏感数据。 明文数据也称为公开数据,开发者可以直接获取到,如登录...
继续阅读 »

1. 前言


微信小程序开发平台,提供有一类 API,可以让开发者获取到微信登录用户的个人数据。这类 API 统称为开放接口



Tip:微信小程序开发平台,会把微信登录用户的个人信息分为明文数据敏感数据


明文数据也称为公开数据,开发者可以直接获取到,如登录者的昵称、头像……


敏感数据如电话号码、唯一标识符……等数据,只有高级认证开发者和经过登录者授权后才能解密获取到。



这一类 API较多,且 API之间功能有重叠之处,相互之间的区别较微小。有的适用于低版本,有的适用于高版本。


为了避免在使用时出现选择混乱,本文将通过具体应用案例介绍几个常用 API的使用。


2. 开放接口


开放接口是对一类 API的统称,开发者可以通过调用这类接口得到微信登录用户的授权或获取登录者的个人数据
开放接口又分成几个子类 API



  • 登录接口: 包括 wx.pluginLogin(Object args)wx.login(Object object)wx.checkSession(Object object) 几 个 API

  • 账号信息: 包括Object wx.getAccountInfoSync()此接口用来获取开发者的账号信息。

  • 用户信息: 包括 wx.getUserProfile(Object object)wx.getUserInfo(Object object)UserInfo。使用频率非常高的接口,常用于小程序中获取登录者个人公开数据。

  • 授权接口:wx.authorizeForMiniProgram(Object object)wx.authorize(Object object)


除上述列出的子类接口,还有收货地址、生物认证……等诸多子类 API,有兴趣者可以自行了解。


2.1 登录接口


登录接口中有 3API,对于开发者来说,使用频率较高的是 login接口,此环节将重点介绍此接口。



非本文特别关注的接口,会简略带过。



wx.pluginLogin(Object args):此接口只能在插件中可以调用,调用此接口获得插件用户的标志凭证code,插件可使用此凭证换取用于识别用户的唯一标识 OpenpId


用户不同、宿主小程序不同或插件不同的情况下,该标识均不相同,即当且仅当同一个用户在同一个宿主小程序中使用同一个插件时,OpenpId 才会相同。


对于一般开发者,此 接口用的不是很多,具体使用细节在此处也不做过多复述。



什么是 OpenId?


当微信用户登录公众号或小程序时,微信平台为每一个微信登录者分配的一个唯一标识符号。



2.1.1 wx.login(Object object)


功能描述:




  • 开发者使用此接口可以获取到微信登录者登录凭证(code)



    登录凭证具有临时性,也就是每次调用时都会不一样,所以code 只能使用一次。





  • 开发者可以通过临时code,再向微信接口服务器索取登录者的唯一标识符 OpenId、微信开发平台账号的唯一标识 UnionID(需要当前小程序已绑定到微信开放平台帐号)、以及会话密钥 session_key




那么,获取到的openIdsession_key对于开发者而言,有什么实质性的意义?




  • 根据 OpenId的唯一性特点,可以在微信用户第一次登录时,把OpenID保存在数据库或缓存中,在后续登录时,只需要检查用户的 OpenId是否存在于数据库或缓存中,便能实现自动登录功能。




  • session_key 也称会话密钥,用来解密微信登录者的敏感数据。



    后文将详细介绍。





如何获取OpenId


现通过一个简单案例,实现微信小程序端与开发者服务器之间的数据交互。以此了解开发者服务器如何通过微信小程序传递过来的用户临时 code换取到登录者的更多信息。


实现之前,先通过一个简易演示图了解其过程。


wx01.png


简单描述整个请求过程:



  • 微信用户打开微信小程序后,开发者在微信小程序中通过调用wx.login接口获取到临时登录凭证 code

  • 在微信小程序中调用 wx.request 接口向开发者服务器发送 http 请求,需要把登录凭证 code一并发送过去。

  • 开发者服务器使用发送过来的 code 以及开发者凭证信息向微信接口服务器索取微信登录者的 openIdsession_key


简而言之,就是 3 者(微信小程序、开发者服务器、微信接口服务器)之间的一个击鼓传花游戏。


开发流程:


第一步:项目结构分析


完整的系统由 2 个部分组成:




  • 微信小程序端 APP



    如对微信小程序开发不是很了解,请先阅读官方提供的相关文档。





  • 服务器端应用程序。



    本文的服务器端应用程序基于 Spring Boot开发平台。





本项目结构是标准的前后端分离模式,微信小程序是前端应用,服务器端应用程序为后台应用。


第二步:新建微信小程序(前端应用)


打开微信开发工具,新建一个名为 guokeai 的小程序项目 ,项目会初始化一个index 页面。在 index.js中编写如下代码。


//index.js
const app = getApp()
const httpRequest = require("../../utils/request.js")

Page({
data: {
isHasUserInfo: null,
userInfo: null
},
//启动时
onLoad: function () {
let this_ = this
/***
* 检查微信用户是否已经登录到后台服务器
* 已经登录的标志,数据库中存在 OPENID
*/

let code = null
//调用 login 接口
wx.login({
success: (res) => {
//得到登录用户的临时 code
code = res.code
//向开发者服务器发送请求
let api = "wx/getLoginCertificate"
let config = {
url: api,
method: "GET",
data: {
code: code
}
}
let promise = httpRequest.wxRequest(config)
promise.then(res => {
let isHas = null
// 有没有完整的微信登录者信息
isHas = res.data == 0 ? false : true
app.globalData.isHasUserInfo = isHas
this_.setData({
isHasUserInfo: isHas
})
}).catch(res => {
console.log("fail", res)
});
}
})
}
})

代码解释:



  • 一般会在微信小程序启动时,也就是在页面onload 函数中调用 wx.login接口,检查用户是否登录过。

  • http://127.0.0.1:8080/wx/getLoginCertificate开发者服务器提供的对外处理微信用户信息的接口。

  • 最后只是简单地输出开发者服务器端返回的数据。

  • httpRequest.wxRequest(config)是自定义的封装wx.request接口的请求组件。


function wxRequest(config) {
//返回的数据类型
let dataType = config.dataType == null ? "json" : config.dataType;
let responseType = config.responseType == null ? "text" : config.responseType;
//服务器基地址
let serverUrl = "http://127.0.0.1:8080/"
//超时
let timeout = config.timeout == null ? 50000 : config.timeout;
//目标地址,基地址+接口
let url = serverUrl + config.url;
//数据提交方式
let method = config.method == null ? "GET" : config.method;
//提交数据
let data = config.data == null ? null : config.data
//头信息
let header = {
// 默认值
'content-type': 'application/json',
'x-requested-with': 'XMLHttpRequest'
}
let sessionId = wx.getStorageSync('sessionId')
if (sessionId) {
header["cookie"] = sessionId
}
return new Promise(function (resolve, reject) {
wx.request({
url: url,
data: data,
//返回的数据类型(json)
dataType: dataType,
enableCache: false,
enableHttp2: false,
enableQuic: false,
method: method,
header: header,
responseType: responseType,
timeout: timeout,
success: (res) => {
console.log("requestData", res)
if (res.cookies != null && res.cookies.length != 0)
wx.setStorageSync('sessionId', res.cookies[0])
resolve(res)
},
fail: (res) => {
console.log("requestException", res)
reject(res)
}
})
})
}

第三步:创建开发者服务器程序(后台应用)


本文使用 spring boot快速搭建后台应用程序。在项目的 pom.xml文件中除了必要的依赖包外,还需要添加以下 的依赖包。


<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>



  • fastjson阿里云提供的开源 JSON解析框架。



    微信小程序开发者服务器构建的项目结构,是标准的前后端分离模式。


    请求与响应时,数据交互常使用JSON格式。这时使用 fastjson 作为json解析器,当然,也可以选择其它的类似解析器。





  • httpclient 是一个http请求组件。




  • mysql-connector-java 本文案例使用 MySQL数据库,需要加载相应的驱动包。




  • mybatis-plus-boot-startermybatis-plus 依赖包。




在后台应用中编写处理器(响应)组件:


@RestController
@RequestMapping("/wx")
public class WxAction {
@Autowired
private IWxService wxService;
/***
* 获取到微信用户的 OPENID
*/

@GetMapping("/getLoginCertificate")
public String getLoginCertificate(@RequestParam("code") String code) throws Exception {
WxUserInfo wxInfo = this.wxService.getLoginCertificate(code);
//用户不存在,或者用户的信息不全
return wxInfo==null || wxInfo.getNickName()==null?"0":"1";
}

代码解释:



  • IWxService是处理器依赖的业务组件,提供有 getLoginCertificate()方法用来实现通过code微信接口服务器换取微信登录者的 openIdsession_key


编写业务组件:


@Service
public class WxService implements IWxService {
@Override
public WxUserInfo getLoginCertificate(String code) throws Exception {
//请求地址
String requestUrl = WxUtil.getWxServerUrl(code);
// 发送请求
String response = HttpClientUtils.getRequest(requestUrl);
//格式化JSON数据
WxUserInfo wxUserInfo = JSONObject.parseObject(response, WxUserInfo.class);
//检查数据库中是否存在 OPENID
WxUserInfo wxUserInfo_ = this.wxUserMapper.selectById(wxUserInfo.getOpenId());
if (wxUserInfo_ == null) {
//数据库中没有用户的 OPENID,添加到数据库中
this.wxUserMapper.insert(wxUserInfo);
} else {
if (!wxUserInfo.getSessionKey().equals(wxUserInfo_.getSessionKey())) {
//如果数据库保存的session_key和最新的session_key 不相同,则更新
wxUserInfo_.setSessionKey(wxUserInfo.getSessionKey());
this.wxUserMapper.updateById(wxUserInfo_);
}
}
return wxUserInfo_;
}
}

代码解释:




  • WxUtil 是自定义的一个工具组件,用来构建请求微信接口服务器url


    https://api.weixin.qq.com/sns/jscode2session微信接口服务器对外提供的接口,请求此接口时,需要提供 4 个请求数据。


    appid:小程序 appId。


    secret:小程序 appSecret。


    js_code:获取到的微信登录者的临时 code


    grant_type:授权类型,此处只需填写 authorization_code




public class WxUtil {
private final static String APP_ID = "微信小程序开发者申请的 appid";
private final static String APP_SECRET = "微信小程序开发者申请的 APP_SECRET";
//
private final static String WX_LOGIN_SERVER_URL = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code";
public static String getWxServerUrl(String code) throws IOException {
String url = MessageFormat.format(WX_LOGIN_SERVER_URL, new String[]{APP_ID, APP_SECRET, code});
return url;
}
}


  • HttpClientUtils也是一个自定义组件,用来向指定的服务器发送 http请求。


public class HttpClientUtils {
/**
* GET请求
*/

public static String getRequest(String url) throws Exception {
//HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
try {
HttpGet httpGet = new HttpGet(url);
response = httpClient.execute(httpGet);
//响应体
HttpEntity entity = response.getEntity();
if (entity != null) {
//格式化响应体
return EntityUtils.toString(entity);
}
} catch (ClientProtocolException e) {
throw e;
} catch (IOException e) {
throw e;
} finally {
response.close();
httpClient.close();
}
return null;
}
}


  • WxUserInfo 是自定义的数据封装类。微信接口服务器返回的数据是以JSON格式组装的,这里需要格式成对象数据,便于在 java中处理。本文使用 MyBatisPlus操作数据库,此类也对应数据库中的gk_wx_user表。


@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("gk_wx_user")
public class WxUserInfo {
//OPEN_id
@TableId(type = IdType.ASSIGN_ID, value = "open_id")
private String openId;
//会话密钥
@TableField(value = "session_key")
private String sessionKey;
//头像路径
@TableField("avatar_url")
private String avatarUrl;
//城市
private String city;
//国家
private String country;
//性别
private String gender;
//语言
private String language;
//昵称
@TableField("nick_name")
private String nickName;
//备注名或真实名
@TableField("real_name")
private String realName;
//省份
private String province;
//学生ID
@TableField("stu_id")
private Integer stuId;
}

MyBatis 数据库映射组件:


@Repository
public interface WxUserMapper extends BaseMapper<WxUserInfo> {

}

第四步:测试。


先启动后台应用程序,再启动微信小程序,可以在数据库表中查看到如下信息。


数据库.png


微信用户的openidsession_key已经保存到后台的数据库表中。


2.1.2 wx.checkSession(Object object)


官方文档中,有一段对 session_key的生命周期的描述。



  • session_key的生命周期有不确定性,可以使用 wx.login接口刷新 session_key。为了避免频繁调用 wx.login 接口,可以通过调用 wx.checkSession(Object object)接口判断session_key是否已经过期。

  • 当开发者在实现自定义登录态时,可以考虑以 session_key 有效期作为自身登录态有效期,也可以实现自定义的时效性策略。


wx.checkSession 的功能,可以使用此接口判断session_key是否过期。



  • 调用成功说明当前 session_key 未过期。

  • 调用失败说明 session_key 已过期。


2.2 用户信息接口


wx.login接口仅能获取到微信登录者的有限数据,如果想要获取到登录者的更多个人信息,可以使用用户信息接口中的相关API



  • wx.getUserProfile(Object object)。获取用户信息,页面产生点击事件(例如 buttonbindtap 的回调中)后才可调用,每次请求都会弹出授权窗口,用户同意后返回 userInfo

  • wx.getUserInfo(Object object) 。和 wx.getUserProfile的功能一样,在基础库 2.10 的后续版本中,其功能已经被削弱。

  • UserInfo是用户信息封装类。


getUserProfile是从 基础库2.10.4版本开始支持的接口,该接口用来替换 wx.getUserInfo,意味着官方不建议再使用getUserInfo接口获取用户的个人信息。


下图是官方提供的 2 个接口的功能对比图。


接口调整.png


为了避免频繁弹窗,可以在第一次获取到用户信息后保存在数据库中以备以后所用。为了获取到用户的敏感数据,在后台要通过getUserProfile接口所获取的数据进行解密操作。


2.2.2 wx.getUserProfile


下面通过具体代码讲解如何保存微信登录者的个人数据。先了解一下整个数据获取的流程,这里直接截取官方提供的一张流程图。


解密码.jpg


获取微信登录者的个人信息,需要经过 2 个步骤。


签名效验:



  • 通过调用wx.getUserProfile接口获取数据时,接口会同时返回 rawDatasignature,其中 signature = sha1( rawData + session_key )

  • 开发者将 signaturerawData 发送到开发者服务器进行校验。服务器利用用户对应的 session_key 使用相同的算法计算出签名 signature2 ,比对signaturesignature2 即可校验数据的完整性。


解密加密数据:



  • 对称解密使用的算法为 AES-128-CBC,数据采用PKCS#7填充。

  • 对称解密的目标密文为 Base64_Decode(encryptedData)

  • 对称解密秘钥 aeskey = Base64_Decode(session_key), aeskey16字节。

  • 对称解密算法初始向量 为Base64_Decode(iv),其中iv由数据接口返回。


具体编写实现。


**第一步:**在微信小程序端编码。


index.wxml页面中添加一个按钮,并注册bindtap事件。


<view>
<button bindtap="getUserProfile">获取用户数据</button>
</view>

index.js中添加一个名为getUserProfile的事件回调函数。为了避免不必要的弹窗,只有当后台没有获取到个人数据时,才调用wx.getUserProfile接口。


getUserProfile: function (e) {
let this_ = this
if (!this.data.isHasUserInfo) {
//如果服务器端没有保存完整的微信登录者信息
wx.getUserProfile({
desc: '需要完善您的资料!',
success: (res) => {
this_.setData({
//小程序中用来显示个人信息
userInfo: res.userInfo,
isHasUserInfo: true
})
//再次登录,因为 session_key 有生命中周期
wx.login({
success(res_) {
//保存到服务器端
let config = {
url: "wx/wxLogin",
method: "GET",
data: {
code: res_.code,
//明文数据
rawData: res.rawData,
//加密数据
encryptedData: res.encryptedData,
iv: res.iv,
//数字签名
signature: res.signature
}
}
let promise = httpRequest.wxRequest(config)
promise.then(res => {
//返回
console.log("wxLogin", res)
}).catch(res => {
console.log("fail", res)
});
}
})
}
})
}
}

服务器端代码:


pom.xml文件中添加如下依赖包,用来解密数据。


<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId>
<version>1.46</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>

在处理器类WxAction中添加wxLogin响应方法。


@RestController
@RequestMapping("/wx")
public class WxAction {
@Autowired
private IWxService wxService;
/***
*
* @param code
* @param rawData
* @param encryptedData
* @param iv
* @param signature
* @return
* @throws Exception
*/

@GetMapping("/wxLogin")
public WxUserInfo wxLogin(@RequestParam("code") String code, @RequestParam("rawData") String rawData,
@RequestParam("encryptedData") String encryptedData, @RequestParam("iv") String iv,
@RequestParam("signature") String signature)
throws Exception {
WxUserInfo wxInfo = this.wxService.getWxUserInfo(code, rawData, encryptedData, iv, signature);
return wxInfo;
}
}

业务代码:


小程序中传递过来的数据是经过base64编码以及加密的数据,需要使用 Base64解码字符串,再使用解密算法解密数据。先提供一个解密方法。


public String decrypt(String session_key, String iv, String encryptData) {

String decryptString = "";
//解码经过 base64 编码的字符串
byte[] sessionKeyByte = Base64.getDecoder().decode(session_key);
byte[] ivByte = Base64.getDecoder().decode(iv);
byte[] encryptDataByte = Base64.getDecoder().decode(encryptData);

try {
Security.addProvider(new BouncyCastleProvider());
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
//得到密钥
Key key = new SecretKeySpec(sessionKeyByte, "AES");
//AES 加密算法
AlgorithmParameters algorithmParameters = AlgorithmParameters.getInstance("AES");
algorithmParameters.init(new IvParameterSpec(ivByte));
cipher.init(Cipher.DECRYPT_MODE, key, algorithmParameters);
byte[] bytes = cipher.doFinal(encryptDataByte);
decryptString = new String(bytes);
} catch (Exception e) {
e.printStackTrace();
}
return decryptString;
}

具体获取数据的业务实现:


@Override
public WxUserInfo getWxUserInfo(@NotNull String code, @NotNull String rawData, @NotNull String encryptedData, @NotNull String iv, @NotNull String signature) throws Exception {
//会话密钥
WxUserInfo wxUserInfo = this.getLoginCertificate(code);
String signature2 = DigestUtils.sha1Hex(rawData + wxUserInfo.getSessionKey());
if (!signature.equals(signature2)) {
throw new Exception("数字签名验证失败");
}
//数字签名验证成功,解密
String infos = this.decrypt(wxUserInfo.getSessionKey(), iv, encryptedData);
//反序列化 JSON 数据
WxUserInfo wxUserInfo_ = JSONObject.parseObject(infos, WxUserInfo.class);
wxUserInfo_.setSessionKey(wxUserInfo.getSessionKey());
wxUserInfo_.setOpenId(wxUserInfo.getOpenId());
//更新数据库
this.wxUserMapper.updateById(wxUserInfo_);
return wxUserInfo_;
}

测试,启动微信小程序和后台应用,在小程序中触发按钮事件。


wx03.png


在弹出的对话框中,选择允许


wx04.png


查看后台数据库表中的数据。


wx05.png


能够获取到的微信登录者个人信息都保存到了数据库表中。至于怎么使用这些数据,可以根据自己的业务需要定制。


3.总结


微信开发平台,提供有诸多接口,可以帮助开发者获取到有用的数据。本文主要介绍 wx.loginwx.getProfile接口,因篇幅所限,不能对其它接口做详细介绍 ,有兴趣者可以查阅官方文档。


官方文档只会对接口功能做些介绍 ,如要灵活运用这些接口,还需要结合实际需要演练一下,如此方能有切身体会。


作者:一枚大果壳
来源:juejin.cn/post/7098216504302403591
收起阅读 »

ChatGPT:时代赋予的机遇

背景 chatGPT是OpenAI公司开发的一款人工智能聊天机器人程序,在2022年11月开放给大众注册使用。 OpenAI公司的定位是一家非营利组织,但由于研发成本的需要,它在早期接受了马斯克等人的投资,后面又接受微软的投资。按照其创始人山姆·阿尔特曼所说,...
继续阅读 »

背景


chatGPT是OpenAI公司开发的一款人工智能聊天机器人程序,在2022年11月开放给大众注册使用。


OpenAI公司的定位是一家非营利组织,但由于研发成本的需要,它在早期接受了马斯克等人的投资,后面又接受微软的投资。按照其创始人山姆·阿尔特曼所说,在OpenAI盈利后,公司利润将会按照四个阶段推进。


第一阶段,先满足埃隆马斯克、彼得泰尔、雷德霍夫曼等首批投资者,让投资人收回成本(从这点看,马斯克说OpenAI丢了初心也能理解)。


第二阶段,微软将获得OpenAI 75%的利润,直到微软收回其130亿美元的投资。


第三阶段,在OpenAI的利润达到920亿美元后,微软在该公司的持股比例将下降到49%,剩余部分利润将由其他风险投资人和OpenAI员工分享。


第四阶段,在利润达到1500亿美元后,微软和其他风险投资者的股份将无偿转让给OpenAI非营利基金。


简单点理解,就是OpenAI先向微软借了130亿美元,抵押品就是股权。承诺赚钱了,用1500亿美元赎回来,要是没赚钱,公司就归你了。按照10%的收益算,这将是一个万亿美元的大市场。


注册


ChatGPT的注册需要一个邮箱账号,一个国外手机号(可以找一个可以获取国外手机验证码的平台),一个科学上网的工具。操作并不麻烦,但对于国内大部分人来说,注册还是有门槛的。


服务


ChatGPT目前对外服务主要有三块:免费版、plus订阅版以及api付费。



  • 免费版本:有时候会访问不了,能访问的时候,也经常不稳定。我自己测试,如果聊天框隔了一会没答复,下次发送内容就会出现异常,需要刷新页面才可以正常使用。

  • Plus 订阅版:20美元/月,保证服务稳定。

  • API付费:这是专门给开发人员使用的。你可以通过这API在国内搭建自己的chatGPT。价格的话目前并不便宜咯。总体价格在0.0005~0.1200美元/1k代币之间。1k约等于750个单词。除了语言模型,还有图像模型、微调模型和嵌入模型。详细内容请查看API地址:platform.openai.com/overview



现状


虽然现在还没有表现特别好的应用出来,但已经有一部分人赚到钱了。哪一部分呢?



  1. 帮助他人注册账号/贩卖账号chatGPT的。顺带卖“科学上网”工具。短期生意,最开始那一波,一个月能赚十多万,但不可持续,且存在违规风险。

  2. 对接chatGPT API,开发国内的ChatGPT。目前国内有一些服务,可以给大家免费使用一定额度,想继续使用就得付费购买,商业模式就是赚差价,可以肯定的是,随着模型的完善,价格会越来越便宜。早期能有一批用户留存,这个模式未来还是可期的。

  3. 做chatGPT的课程、训练营。教你怎么用chatGPT提高工作效率。目前市场还是很大的。有兴趣的可以一起搞。

  4. 通过chatGPT做代写服务。比如,代写论文,代写小说。还有代问问题的。

  5. 通过chatGPT到各种论坛回答问题。

  6. 很多企业/个人已经开始利用chatGPT来提高工作效率了。比如游戏公司可以利用AI画图,有些B端企业开始接入AI做客户服务。


未来


在浏览器刚刚出现的时候,网站是稀缺的。iPhone刚刚出来的时候,应用是稀缺的。一个新的技术出现,技术的底层靠大公司,而小公司的机会就是技术的应用创新。可以预见的是,通过chatGPT开发的应用会是机会。至于具体怎么做,需要我们谨慎探索。这里我们可以看看chatGPT给出的例子,看看他自己怎么说。



风险




  1. chatGPT只是工具,我们不能过于依赖工具,更不能让工具代替我们思考。




  2. chatGPT只是转述师,并不是专家。它的回答并不是原创的,而是整合网络上的资源然后进行转述,也就是说,它是会犯错的。而且很多错误是外行人看不出来,在某些情况下,这其实更危险。




  3. 利用这种翻墙的应用做服务赚钱,可能存在政策风险,建议大家谨慎探索。




作者:石云升
来源:juejin.cn/post/7206897281365803067
收起阅读 »

简单教你Intent如何传大数据

前言 最近想不出什么比较好的内容,但是碰到一个没毕业的小老弟问的问题,那就借机说说这个事。Intent如何传大数据?为什么是简单的说,因为这背后深入的话,有很多底层的细节包括设计思想,我也不敢说完全懂,但我知道当你用Intent传大数据报错的时候应该怎么解决,...
继续阅读 »

前言


最近想不出什么比较好的内容,但是碰到一个没毕业的小老弟问的问题,那就借机说说这个事。Intent如何传大数据?为什么是简单的说,因为这背后深入的话,有很多底层的细节包括设计思想,我也不敢说完全懂,但我知道当你用Intent传大数据报错的时候应该怎么解决,并且简单聊聊这背后所涉及到的东西。


Intent传大数据


平时可能不会发生这种问题,但比如我之前是做终端设备的,我的设备每秒都会生成一些数据,而长时间的话数据量自然大,这时当我跳到另外一个页面使用intent把数据传过去的时候,就会报错


我们调用


intent.putExtra("key", value) // value超过1M

会报错


android.os.TransactionTooLargeException: data parcel size xxx bytes

这里的xxx就是1M左右,告诉你传输的数据大小不能超过1M,有些话咱也不敢乱说,有点怕误人子弟。我这里是凭印象说的,如果有大佬看到我说错,请狠狠的纠正我。


这个错误描述是这么描述,但真的是限死1M吗,说到这个,就不得不提一样东西,Binder机制,先不要跑,这里不会详细讲Binder,只是提一嘴。


说到Binder那就会联系到mmap内存映射,你可以先简单理解成内存映射是分配一块空间给内核空间和用户空间共用,如果还是不好理解,就简单想成分配一块空间通信用,那在android中mmap分配的空间是多少呢?1M-4K。


那是不是说Intent传输的数据超过1M-4K就会报错,理论上是这样,但实际没到这个值,比如0.8M也可能会报错。所以你不能去走极限操作,比如你的数据到了1M,你觉得只要减少点数据,减到8K,应该就能过了,也许你自己测试是正常的,但是这很危险。


所以能不传大数据就不要传大数据,它的设计初衷也不是为了传大数据用的。如果真要传大数据,也不要走极限操作。


那怎么办,切莫着急,请听我慢慢讲。就这个Binder它是什么玩意,它是Android中独特的进程通信的方式,而Linux中进程通信的方式,在Android中同样也适用。进程间通信有很多方式,Binder、管道、共享内存等。为什么会有这么多种通信方式,因为每种通信方式都有自己的特点,要在不同的场合使用不同的通信方式。


为什么要提这个?因为要看懂这个问题,你需要知道Binder这种通信方式它有什么特点,它适合大量的数据传输吗?那你Binder又与我Intent何干,你抓周树人找我鲁迅干嘛~~所以这时候你就要知道Android四大组件之间是用什么方式通信的。


有点扯远了,现在可以来说说结论了,Binder没办法传大数据,我就1M不到你想怎样?当然它不止1M,只是Android在使用时限制了它只能最多用1M,内核的最大限制是4M。又有点扯远了,你不要想着怎么把限制扩大到4M,不要往这方面想。前面说了,不同的进程通信方式,有自己的特点,适用于某些特定的场景。那Binder不适用于传输大数据,我共享内存行不行?


所以就有了解决办法


bundle.putBinder()

有人可能一看觉得,这有什么不同,这在表面上看差别不大,实则内部大大的不同,bundle.putBinder()用了共享内存,所以能传大数据,那为什么这里会用共享内存,而putExtra不是呢?想搞清楚这个问题,就要看源码了。 这里就不深入去分析了,我怕劝退,不是劝退你们,是劝退我自己。有些东西是这样的,你要自己去看懂,看个大概就差不多,但是你要讲出来,那就要看得细致,而有些细节确实会劝退人。所以想了解为什么的,可以自己去看源码,不想看的,就知道这是怎么一回事就行。


那还有没有其它方式呢?当然有,你不懂共享内存,你写到本地缓存中,再从本地缓存中读取行不行?


办法有很多,如果你不知道这个问题怎么解决,你找不到你觉得可行的解决方案,甚至可以通过逻辑通过流程的方式去绕开这个问题。但是你要知道为什么会出现这样的问题,如果你没接触过进程通信,没接触过Binder,让你看一篇文章就能看懂我觉得不切实际,但是至少得知道是怎么一回事。


比如我只说bundle.putBinder()能解决这个问题,你一试,确实能解决,但是不知道为什么,你又怕会不会有其它问题。虽然这篇文章我一直在打擦边球,没有提任何的原理,但我觉得还是能大概让人知道为什么bundle.putBinder()能解决Intent传大数据,

作者:流浪汉kylin
来源:juejin.cn/post/7205138514870829116
你也就能放心去用了。

收起阅读 »

副业奇谈

本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。 楔子 在一家名为叹佬鸡煲餐馆的小桌子上,坐着我和他,榴莲鸡煲溢出的香味,让人垂涎...
继续阅读 »

本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。



楔子


在一家名为叹佬鸡煲餐馆的小桌子上,坐着我和他,榴莲鸡煲溢出的香味,让人垂涎欲滴,鸡肉和榴莲嫩滑的口感,仿佛让人重回到那个十七八岁的青春时光。他叫小润,高中时期经常带着我为非作歹,中午午休跑去打篮球,晚自习溜去操场趟草地上吹牛逼,最刻骨铭心的,还是晚自习偷偷溜去学校附近大厦最高层天台,再爬上去水塔仰望星空,俯视我们的高中,看着每个教室里面一个个奋发图强的同学,我丝毫没有半点做错事的羞愧,眼里只有天上的繁星,地下的灯光,还有旁边的那个他。


小聚


“小饿,我们95后的都已经老了,现在社会都是年轻人00后的天下,比学历能力,985、211一抓一大把,比耐力,我们身体大不如前,精力强壮的年轻人比比皆是...”


“难道你不行了?”


“你别打岔,你这一行不是也有一个35岁的梗吗,你这个前端开发岗位我了解过,是需要不断学习的,技术迭代如此之快,你跟的上吗?”


我默默的摇了摇头,诚然,我是跟不上的,vue2我都没学完vue3就已经出来了,不过我相信我还是极少数。因为我安于现状太久了,拿着不上不下的薪资,没有房贷车贷育儿的压力,不像以前住在城中村每天晚上睡觉听着管道排泄物的声音,没有压力,就没有动力,我就是这么一个充满惰性的人。


小润跟我是高中同学,那时我们的关系不错,但是毕业后各自去往自己的大学,有自己的生活,便没怎么联系了,这次出来也是近三年第一次小聚。他在一个比较老牌的做文具,做设备的大厂工作,主要内容是去一些大型物业竞标,为了竞争得到那个“标”,付出的也不少,陪酒送礼一样不落,但就算得到那个“标”,公司的绩效奖励分配制度却让小润很不满,所以他不禁感慨,“我们每个月累死累活得到的薪资,除去日常花销,本来就已经所剩不多,而且社会上还存在一种叫通货膨胀的东西,想想我们年龄越来越大,面临的职场危机,手上的筹码到底能不能支撑我们维持当前消费水平,过上自己想要的生活,这是一个比较大的问题。”我听得津津有味,虽然心里认为他的说法有点过度焦虑,但有这么一个意识,总是好的,小润看到我向他投向肯定的目光,便继续说道,“这几年我都在看书,其中看到一个企业家有一句创业名言————空手套白狼”。


空手套白狼


小润看着我一脸的疑惑,嘴角微微一笑,一脸正经的告诉我,“空手套白狼虽然百度翻译是个贬义词,但是在创业翻译过来就是用最低的成本,创造最大的价值。我想要做一些0成本,价值高,只需要付出时间的生意”。


“那么请问哪里有那么大的馅饼?”据我所知,现在谈起普通人做副业,想要0成本,要不就是什么做信息差买卖,或者视频搬运,网上一搜一大把,现在根本不是能真正获利的渠道了。当然,也可能有很多人的确做着0成本生意,闷声发大财


微信图片_20230307134118.jpg


小润从煲里夹了一块榴莲肉,放入嘴中品尝了几番后吞入腹中,真诚的向我道来,“之前你有跟我聊过你做的副业,上面的功能我看了,感觉你比较厉害,对小程序开发这一块也是比较熟悉。你有没有看过小区的停车场,白天的时候很多车位都是空闲的,极大部分都是车主开车上班,那么车子不就空闲起来了?我们可以做一个平台,让车主在平台上面登记,只要车位空闲,可以告诉平台某一个时间段空闲,让平台的其他需要在附近停车的用户看到,用户微信支付停留相对应的时间,这样不仅解决了车位紧张的问题,车位车主也能利用闲置的车位赚到一笔钱,平台也能进行抽成。”


我一听,陷入了沉思,感觉好像很有道理的样子,但又觉得哪里不对,“这种做法当然是不可能的,物业停车场大都是一个车牌对应一个停车位,不可能给别人钻这种空子。”


“那你说个der啊”


微信图片_20230307134254.jpg


“刚刚只是我在生活中发现的一些奇思妙想,就是利用闲置这个属性,接下来才是我要说的重点。你平时看街边上停着的电车多吗?”我点了点头,电车在广州这所大城市,那肯定是多的。突然,小润用筷子翻了翻鸡煲中的食物,一脸愤然的对着我说“我擦,那些肥牛都被你吃完了?”我又用筷子探寻了一下,的确,肥牛还真被我吃完了,软嫩的肥牛搭配着由榴莲和鸡煲化学反应产生的汤底,让我感觉到味蕾在跳动,入口即化,难以言喻,自然而然就多吃了几片,我尴尬又不失礼貌的问他,“要不多点一份?”


他笑了笑,摆了摆手,继续说道,“我的想法是将空闲的电车利用起来,做一个平台,平台的载体是小程序,像膜拜小程序一样,用户能找到附近的单车,而我们则是电车,但是我们不需要成本,因为在平台中,电车的信息是由车主自己主动上传上来的,所以就有两个群体,一个是车主,一个是需要用电车的用户。车主能在电车空闲的时间将电车上传到我们的平台,通过出租自己的电车进行赚钱,当出租的次数多了,不仅能回本,到时候或许还能赚点小钱。而普通用户想用电车的时候,根据小程序提供的定位,找到离他最近的那台电车,进行微信支付就能骑走,按照骑行时间进行收费,收费标准由电车车主自己提供。而我们平台的收入,则是对每笔订单进行抽成”。


我一听,又陷入了沉思,又感觉好像很有道理的样子,但又觉得哪里不对,咦,我为什么要说又?


QA



用户场景有哪些,用户需求多吗?



多,平时使用电车都是上班族居多,那上班族使用完电车后电车就闲置了,可以进行出租赚点奶茶钱,何乐而不为?况且平时下班我想去别的地方玩一下,也可以租一台电车去逛一逛,就再也不需要每个人都要买一台电车了。确实,之前去湛江游玩,也有电车提供出租,骑着电车到处逛逛吃吃,真的十分快乐,不过电车是由公司统一提供。



普通用户怎么开启这些电车呢,电车五花八门,难道要让车主统一购买我们提供的电锁进行控制?



目标电车当前只试行小牛和九号电车,用户需要开启电车的时候,在小程序可以找到电车车主联系方式,通过电话联系让他用电车钥匙开启电车,同时在小程序按下开启按钮告诉平台和用户已经开启,开始计费。用户骑行完电车后,用户致电车主进行结算并关闭电车。



客户借车后,将车的某些零件换改,偷窃,损坏,如何处理?例如将电瓶车电池换成低端电池,也能用,,但车主不知道?



这的确是个问题,我也在思考是否有必要弄押金,但是电车的押金弄小了没啥用,弄大了也不合适,没人想进行支付,所以如何平衡这个问题,是我们这个项目后续所要思考的。



用户把电车开到离起始点十万八千里,这样车主怎么找回自己的电车?



好问题,我也有想过,车主在上传电车到平台的时候,可以设置自己的使用类型,可以规定使用用户骑行归还到原位置,也可以不规定,全由车主自由设定



听起来好像真的可以落地,但是用户附近可用的电车如果多起来,在地图上展示密密麻麻,这个需要点技术,我得研究研究



我们初期可能不需要那么复杂,只需要展示一个列表,可以让用户进行筛选,用户能看到每台电车的外观,点击电车详情,就能知道用户与电车的相对位置,不需要在同一个页面展示那么多的标记(如此甚好)


// 小程序在地图上显示用户与标记方法

// js
const markers = [
{
id: 1,
// 标记的大小
width: '40px',
height: '40px',
// 标记的经纬度
longitude,
latitude,
// 标记的icon图标
iconPath
}
]
this.setData({ markers })

// wxml
// center.longitude center.latitude 为中心经纬度
<map class='map' id='map' longitude='{{center.longitude}}' latitude='{{center.latitude}}' markers="{{markers}}" scale='16'></map>


政治问题...



******<-内容加密了


我们聊了很多细节,包括首页如何设计,一键控制电车上线下线,越聊越兴奋,感觉真的可以落地,说到尽情之处,还说日后被大厂收购,实现财富自由指日可待,因为我们都知道,一个产品成熟了,稍微露出苗头,就会被人借鉴。当天晚上我回到家,就把整个大纲梳理了出来,并发给小润看。


dianche.png


但同时我们也发现问题,如果用户在骑行的途中,被车主通过车钥匙远程停车会发生什么事情,之前我们一致认为电车平台会有相对应的API提供,不仅可以获取电车信息(车辆电池,型号,外观元素等),也能有启动车辆和关停车辆的接口,但浏览了两个电车平台的官网,发现平台并没有这种东西,我们的思路一下子遇到卡壳,而且押金问题也是一个重点,热情一下子就冷却了下来,这场看似热血沸腾的副业计划就此搁置了下来。


对于做副业,我个人是非常感兴趣的,低成本的副业能赚钱是根本条件,更主要能拓展人的视野,之前我第一个副业,进行的比较顺利,但前提是市场已经有先驱,可以有模板进行复刻,而这一次纯属天马行空,没有前车之鉴,需要考虑到很多细节,如果有一些致命因素导致项目行不通,那可能这个项目就真的凉了。其实也很合理,世界上人才千千万,一个脑暴出来能赚钱的项目,为什么市场没有落地,或许不是因为没有人能想出来,更大因素是有人想出来了,但是此路不通。


省流


不亏,那顿鸡煲很香,而且是小润掏的钱


作者:很饿的男朋友
来源:juejin.cn/post/7207634883988635705
收起阅读 »

独自坚持 17 年,aardio 作者:“因妻子患癌,再无精力维护项目”

本周,编程语言 aardio 作者一鹤发布了一则公告,令不少开发者深感震惊与难过:“因妻子患癌,再无精力维护 aardio。”在看到这则公告的当下,许多开发者才意识到:原来这 17 年不断迭代、始终积极更新的 aardio,皆由作者一人坚持开发并维护。专注于桌...
继续阅读 »

本周,编程语言 aardio 作者一鹤发布了一则公告,令不少开发者深感震惊与难过:“因妻子患癌,再无精力维护 aardio。”


在看到这则公告的当下,许多开发者才意识到:原来这 17 年不断迭代、始终积极更新的 aardio,皆由作者一人坚持开发并维护。

专注于桌面软件开发的 aardio,体积仅有 6.5MB,却提供了惊人数量的开源标准库、扩展库——所有库由纯 aardio 代码实现,基本都由作者一鹤一人编写,涉及到了桌面编程的方方面面。此外,一鹤还为每一个库的每一个接口函数都编写了文档,且提供了大量演示范例,方便开发者入门使用。

本质上来说,aardio 属于易用性极强的动态语言,但同时它也是一种混合语言,可以方便地操作静态类型,直接调用、嵌入并交互大量的第三方编程语言,能调用 dll,也可以写出精美的界面程序。例如,直接调用 C 语言、C++ 等静态语言的 API 接口函数,支持 stdcall、cdecl、thiscall 等 API 接口函数等。


由于 aardio 的小、轻、快,使其学习和使用成本极低,这 17 年来吸引了不少开发者的青睐。与此同时,aardio 也在一鹤的坚持下,一直保持活跃更新,每一年都会带来大量更新扩展,并依旧保持着最初简洁高效的结构与语法:“即使是最早的 aardio 源代码,仍然能不经修改在最新版本开发环境中完美运行。”去年 10 月,一鹤还曾发文感慨:“aardio 现在是活跃更新,以前更新更多更快。aardio 17 年,一直向前!”

从 2005 年到 2023 年,从默默无闻到小有名气,随着 aardio 用户不断增多,坚持了 17 年的一鹤收获了许多开发者的赞扬与感激:

“一鹤是一个了不起的程序员,深知借力的哲学, aardio 的强大在于胶水层面做的很好,DLL,com 的加载机制可谓完美,所以凡是喜欢的库或者 exe 都能很方便的和 aardio 混编。这种架构在 10 年前就有,足见一鹤对编程理论的深入理解。”

“aardio 对我的帮助无疑是巨大的,它让我体会了编程的快感,让我很想继续学习其他的知识。虽然学的过程很痛苦,但让我也有种编程改变世界的快感。现在公司用的很多小软件,就是我平时用 aauto 做的。所以对于软件我对作者保持感谢的态度。”

如今,面对因妻子患癌而停止维护 aardio 的一鹤,开发者们也第一时间给予了理解和安慰:

“很佩服作者能一个人坚持开发这么久。”

“作者一个人,17 年,独立维护一个开发软件,还是免费给大家使用。说伟大可能有点过了,但我真的很佩服他。自己能做的不多,捐了一点略表绵薄之力,祝作者的家人安康吧。”

“祝愿作者妻子早日康复。”

参考链接:

https://mp.weixin.qq.com/s/V1Nz7HGv4ZDsWDAesBaPRw

https://aardio.com/
————————————————
作者:郑丽媛 | CSDNnews
来源:blog.csdn.net/csdnnews/article/details/129357874

收起阅读 »

产品经理不靠谱怎么办

一、产品和开发之争 开发和产品宿命的争斗由来已久,倏然就是一对天敌。 1.1 平安产品掐架事件 在刚毕业那会,还不知道产品具体是干啥的时候,就听到了不少产品和开发打架的事情。印象最深的,就是平安产品开发掐架事件了。起因是产品经理提了一个需求,要求APP开发人...
继续阅读 »

一、产品和开发之争


image.png
开发和产品宿命的争斗由来已久,倏然就是一对天敌。


1.1 平安产品掐架事件


在刚毕业那会,还不知道产品具体是干啥的时候,就听到了不少产品和开发打架的事情。印象最深的,就是平安产品开发掐架事件了。起因是产品经理提了一个需求,要求APP开发人员可以做到根据用户的手机壳来改变手机软件主题,面对这样的需求,开发自然是要起义的。


真假分辨不是重点,从争论的热点而言可知,就这件事情而言,争论的原因是需求不符合常理。开发做的事情只是对世界建模而不是无中生有。而我们作为开发,平时和产品决斗最多的情况,是对于时间资源之争。产品混乱的开发节奏,不符合逻辑的需求,不合理的时间安排,不重点的优先级安排。


而且很多时候,产品的职位是要比开发的高的,话语权更加的高,会让他们更加的肆无忌惮,可恨!


拿我之前公司的真实的例子来:



  • 产品所谓的需求文档都是短短几句话

  • 一个迭代周期内从来没有按照原订计划上线计划的功能,各种小需求,拍脑袋的需求随意插入。


前者,总监对于产品这种行为的解释是需要开发和产品共同去参与设计,相互查缺补漏。尚且不论开发是否能够得到第一手信息,时间也是不够的,重要的是工资它不涨。


后者直接导致了开发的加班。


1.2 为什么会有这些不靠谱的产品经理呢?


根本还是我的问题,我没有能力轻易的选择自己工作环境🙃


其次呢?是他们专业程度不够,被培训机构忽悠,人人都是产品经理,门槛低工资还高,上可以直接对话老板,下可以指挥程序员,所以导致了什么阿猫阿狗都涌入


矛盾的是,正好相反,产品的门槛其实很高的。


他们需要很强逻辑能力, 整理出来的需求需要逻辑自洽, 需要思考用户的操作体验,需要思考人力资源的分配。


面对老板、市场、业务方抛来的‘建议’,能够甄别出什么是功能,什么是需求,然后制定出合理的优先级。在敏捷项目中,还要制定迭代的计划,顶得住上面的压力,压得服下面的开发。


其中涉及到的专业技能有社会心理学、管理学、软件工程管理、用户画像学、以及一定的开发知识、一定的设计知识、一定的运营知识。


涉猎之广,以及程度之深,不是程序员用计算机能够模拟的,不然为什么会有智障的小爱同学、小冰同学、siri。


1.3 根本的目的是为了解决问题


当然,本篇文章以及《10x程序员》目的并不是为了抨击产品多么多么的不靠谱。就像郑晔老师所说,如果从不靠谱的数量来说,程序员是比产品多得多得


第一是因为程序员基数就比产品的多,第二也是因为万物皆可转码导致的。培训班培养几个月就出来工作了,他能有多强的编程能力?



只是从整个市场来看,当然还是有很多转行的,培训出来的很强的人。



这篇文章的目的,是为了解决点那个我们碰到这些不靠谱的产品经理的时候,我们应该如何怎么办?


首先要知道产品和开发的战争是因何而战的。


二、争论的真相是什么


image.png


2.1 争论的原因


产品和开发相互攻击是解决不了任何问题的。为了解决争斗,我们首先需要知道到底是争什么?为何而争。方能对症下药。


我们常常会出现下面这样的一个场景:



产品:我们需要一个单点登录的界面。输入账号密码就可以进行我们的界面。


开发:好的


一天时间,界面和交互逻辑,接口哗哗做完。


开发:东西做完了,你来看看


产品:??? 验证码呢?


开发:你又没说


产品:这个不是常识么?


开发:。。。。


又是半天时间,验证码搞定


产品:这个项目是放在门户下面,登录的功能不是应该在门户上面做么?现在跳转到别的项目还需要重新登陆,你怎么想的?


开发:顶你个肺,一开始怎么不说是这个场景


产品:你又没问。。。


🔪 🙎‍♂️



这是由于双方信息不同步的导致的。如果一开始开发就问:



  • 这个需求的用户是谁?

  • 这个需求的使用场景在哪里?


我想问题就会拖到后面了。开发必须要有自己的独立思考,多问几个为什么,才能够减少掉进坑中的次数。


双方的知识储备不一样,双方掌握的信息不一样,得到的结论自然也不一样。


所以这就需要我们在一个信息平台上,才能够沟通得有效率。


而这就需要我们双方都能有一个很好的沟通能力。也需要我们开发多张十个心眼,默认产品都是不靠谱的。多问几个为什么,不要害怕问题幼稚。如果产品都能够一一回应,而且逻辑自洽的话,那么恭喜你,你碰到了一个不错的产品。



有一句话说得好,当你和一个人谈话谈得很开心的时候,很可能是因为对方的段位比你高,他在向下兼容。



当然,出现上面的那些问题,也由于现在解决的问题不再是明确的,常常范围模糊,别说产品自己,业务用户也不知道自己想要什么?这个无形中提高了产品的门槛,还提高了需要软件设计师的架构能力,需要提前布局。


软件开发的主流由面向确定性问题,逐渐变成了面向不确定性问题。为了应付这个问题,敏捷开发这个最佳实践就应运而生。到了中国就变成了“田园敏捷”🐶,需求不明确,所有需求都是P0级。
为了解决这个问题,我们产品和开发能够在有效的资源中做些什么呢?这就不得不提到敏捷开发中两个很重要的阶段,需求澄清和需求反澄清,如果是开发负责人还需要参加需求准入。


2.2 沟通的真正目的是什么


先简单的介绍一些敏捷开发流程:


两周一迭代,在进入开发之前,产品内部需要先过一遍需求,随后根据列的需求和开发负责人讨论需求准入,开发负责人会根据人力资源来和产品共同商量,这个迭代可以上的内容。


需求澄清,这个是全体人员都参加,产品一一说需求的逻辑,开发可以提问。


之后就到了需求反澄清,这个阶段是开发在说自己对于需求的开发,以及开发的思路。随后进入开发阶段。开发完成,向产品show case, 测试通过之后前后端封版


封完版提发布工单,然后才进行反版。在这个阶段还包括了每日的站会过需求,还有发版之后的回顾会。


如时间表下图:


从图片可以看到对于开发两个重要的节点,一个是需求澄清,另外一个是需求反澄清。前者是产品在说,开发问。后者是开发在说,产品再问。这两个就是一个很好的拉平双方认知的机会。
这两个沟通的机会至关重要,是有效减少之后扯皮的关键节点。这就需要我们知道如何有效的进行沟通了。


唯心主义不是贬义,而是一个客观的事实。具体表现就在于,这个客观世界和我们所想象的总是不一样的。同样的,由于每个人认知的世界是不一样的,所以信息的传递是会衰减的,你不可能把你理解的信息 100% 传递给另外一个人,而这中间,如何传递,也就是如何描述将直接决定衰减的比例。


可以根据书中信息论模型来进行解释:


image.png


幻化为人的沟通的话。人的脑子就是信源,携带着信息到发送器,发送器通过自己的表达通过声带发送给对方,对方接受到信息还需要转译一遍进行自己的大脑。在传送的中间过程,还有噪声源,这个噪声源可以是物理环境认为的嘈杂,也可以认为是双方因为地位的不同,导致的思维方式的不同的噪声。


根据这个例子,可以用下面这张图来表示上面争论的原因:


image.png


扮演不同角色的时候,我们的思考模式是不同的。上图是产品作为信源,而开发作为信宿,反之亦然。


作为信源的话,我们将自己脑中的信息通过嘴巴表达出去的过程,是受限于知识储备和表达能力的。也就是说如果我们的知识储备足够的多,表达能力足够的强的话,在发送信息到对方的闹钟的时候,偏差自然也会更加的小。


作为信宿的话,我们开发作为接受的一方,需要提高自己的知识边界,主要是了解业务的前因后果,尽可能的提升解码的能力。


综上所述,我们沟通的目的是为了同步信息,减少对于需求的理解的偏差。而沟通出来的结果,就是共同确立一个验收的标准


只有验收的标准确定下来之后,才可以最到限度的减少后期扯皮的可能性。


那么我们作为开发需要怎么做呢?


2.3 开发需要做什么


image.png


开发在需求澄清的时候,其他问题都可以不问,但是这两个问题一定要搞清楚。



  1. 需求的背景是什么

  2. 需求能够给用户带来什么业务的价值


前者是为了理解业务的前因后果,当自己当成产品经理,让需求的逻辑能够自洽。后者是换位自己作为一个用户,以用户的视角来看问题。这也和我们公司以用户导向的价值观相符。


在需求反澄清的时候,作为一个前端工程师,我们最低限度的需要出两个东西,一个是API的设计文档,另外一个就是数据走向图。这个数据走向图我的前一篇文章《vue的业务开发如何进行组件化》中进行过阐述,具体可以去那篇文章看看。



敏捷开发不代表文档的缺失。



我曾经把产品问懵逼之后,把需求都砍了一大半。也间接实现了最好维护的代码。


我的目的不是为了砍需求,而是为了写出全世界最好维护的代码,即不用的代码。


image.png


三、抛弃固有印象


image.png


在程序员眼里:



  • 产品一般都没逻辑、缺乏交流基础(没常识)、没能力没主见;


在产品经理眼里:



  • 程序员通常属于严重沟通障碍、缺乏用户和产品意识、只考虑技术、没有大局观。


抛弃这些固有的刻板印象,沟通和理解更为重要。作为开发不能因为一时的占了上风,就沾沾自喜,大快人心,觉得压了产品一头。爽归爽了,你的工资可还是没动的。班还是要加的。所以解决问题才是主要的目的,不管工作中,还是生活中。
而这就要求我们:



  • 加强专业知识的学习,

  • 增加对彼此工作领域的认知,

  • 用逻辑而非借口来说服对方。


开发可以去考考PMP证书,虽然都说没有含金量,但是你得过了才有资格来说这句话。作为前端还可以去学学基础的美学设计。总的来说就是要扩展自己的知识边界。


而且,大家都是打工人,成年人了,我们要知道矛盾的根源是什么?真的是产品的不靠谱和开发的沟通障碍么?或许不见得。


四、矛盾的根源


之前刷知乎看到过程墨大佬的一段话,记了下来:


在我国,产品经理和研发工程师的核心冲突,是“有限的开发资源”与“无限制的目标”之间的矛盾。
“有限的开发资源”在研发工程师这一边,人力是有限的,人的工作时间是有限的,人的耐心是有限的,人能够做的事情是有限的。


“无限制的目标”在产品经理这一边,无数量限制的需求变更,无规则限制的产品设计流程,无时间限制的工期规划……


怎么解决?


要么提供更多的开发资源,也就是招更多更合格的工程师;要么就让产品经理对自己的行为做更多限制,让产品设计和规划按照客观规律办事。


当然,说到底两者之间的矛盾的根源是我国特色资本主义的内部矛盾,一方面想让团队跑得快,一方面又没有本事进行合理管理,最后产品经理和程序员打架,世人在骂产品经理无能程序员暴躁,其实归根结底是上面人无能而已。


五、一个问题


我之前面试,被问我这么一个问题:


一个需求你评估完成的时间需要两周,但是产品最多只能给你一周的时间,你怎么办?


那场面试虽然过了,但是我没有收到对于我说的答案的评价。所以很好奇大家的答案是什么😂


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

2022 一个8年培训讲师的转型起始

前夕 干了8年的培训,从Python一线授课到Python类目管理到学院负责人,一步一步的努力,磨课,课程研发,讲师培训,课程安排,大纲设计,无数个加班,对着镜子讲课,自己录课自己听,听前辈讲课,看竞品课程,市场调研,一步一步的成长了下来。 我一直是一个喜欢编...
继续阅读 »

前夕


干了8年的培训,从Python一线授课到Python类目管理到学院负责人,一步一步的努力,磨课,课程研发,讲师培训,课程安排,大纲设计,无数个加班,对着镜子讲课,自己录课自己听,听前辈讲课,看竞品课程,市场调研,一步一步的成长了下来。


我一直是一个喜欢编程,喜欢分享的人,也很庆幸自己从事了Python培训这个行业,更幸运的是在这个行业当中,我快乐的努力,然后快乐的成长,同事,学员的认可,薪资的增长,让我一度感觉自己是那个最幸运的人。过程当中也有小伙伴聊过,做讲师久了,和一线的开发会脱节,但是我自信,努力,没有问题的,开班变少,学员就业难度变大,课时费开始下调也未曾动摇,还是努力的做好自己手头的工作,直到身边的同事(一起战斗成长的战友)开始聊之后的打算,开始离开我才知道真的,出问题了,但是当时的第一个想法是再找一家培训机构,接着做讲师,直到投出简历,泥牛入海或者面视的话不投机,我才直到,不是这个行业出现问题了,是我自己真的有问题了。


挣扎


也许是一种矫情,也许是自己最后的固执吧,从开始做培训到最后,我一直认可一个理念,就是先得让客户(学员)认可你,再说培训的事情,不论是授课还是就业,如果只依赖讲师,学校是达不到的,必须有学员的信任,而做到大家对你的信任技术是一方面,更多的是品质,也就是服务和口碑,所以感觉自己决定离开当时培训管理岗位的时候有一点孔乙己的感觉,想接着做培训,自己看上的岗位看不上自己,看上自己的岗位又有点矫情,赶着有几个同事已经开始做开发入职了新的公司,而自己还在不上不下的徘徊,所以起了心思,要不去搞开发,我本身是讲师,但是不论是自己接项目还是帮助学员去排错,都做的风生水起,所以对自己的开发能力很自信,那么就试试吧。


耳光


自信这个事情往往是参加水分的,在我去做开发的这个想法上,显然水份很大,我的自信坦然的在简历上标注了自己8年的Python教学经验,自信的把自己的教学研发项目写在了简历上,然后:


面试官甲:你之前是做培训的,没有切实的开发经验哈。。。。。


面试官已:你的项目都没有在生产环境运行吗。。。。。。


面试官丙:在你以往的开发过程当中,你遇到过哪些问题。。。,好的,我大致了解了


面试官丁:你的表述能力很强,逻辑很清晰,今天的面视先到这里吧。


面试官N:。。。。。。


我被迫停止了面试,我做过培训,做过就业指导,知道如果面视一直失败,不去反思,那么只会越来越糟糕,在自己能力下滑之前,更可怕的是自己的底气会彻底被磨掉。所以我要进行反思。


反思


期初的自信到后来的狼狈,我知道,我确实是掉队了,不论承不承认,之前一个老大哥说过:”失败不是要证明你有多差劲,而是告诉你你该努力了。“,我经常说给自己的学生,我本身也是认可信服这句话的,所以:


1、面视的算法题要刷起来,每天5:30起床刷题,可以是一道题,可以是两道题,每天刷,每天笔记。


2、完成自己的项目集,从前端,到后端,到服务器,我开始整理自己之前做的项目。


3、刻意的面视练习,这个没啥不好意思的,筛选自己要去面试的公司,查询公司的业务,找大佬(感谢帮我的任大佬,刘大佬,CD大佬,宋大佬,思亲大佬,还有好多大佬,哈哈哈,我平时的人员还行哈)帮忙分析,自己去查这个公司的面试题。


这样坚持了一个月,终于开始有二面,offer了,当时收到第一个offer热泪盈眶。


前行


最总还是入职了开发的岗位,做了一个Python后端开发,嘿嘿嘿。但是过秦论里有一句话:后人哀之而不鉴之,亦使后人复哀后人也,如果说,第一次吃亏是不小心,第二次吃亏是笨,那么第三次吃亏就是欠了。这么一次的折腾告诉我,抛开程序员,我只是一个打工人,呆在一个舒适圈里是很危险的,温水里的青蛙容易变成干锅牛蛙的,所以,必须给自己一个规划:


开发


开发工作要珍惜,做好每一个开发任务,做好每次的积累,不论是一个分页还是一个服务器的调整,积累自己的开发经验,做好团队的沟通和衔接(沟通我比较擅长,嘿嘿嘿)


学习


开始编写博客,开始整理自己的文档,和同时沟通,和学员沟通,浏览技术网站,逼着自己每天做一个知识点博客(恰好接触到了掘金平台,嘿嘿嘿,每天更文。),每周完成案例代码。


教培


8年的教培经验,我舍不得也不会把他丢弃,开始自己录制课程,为之前的小伙伴,自己的粉丝做学习规划,兼职授课,都在有序的进行。


其他


我是一个成年人,编程时我爱好也是我的工作,我不是一个理想主意的人,所以,在完成工作的基础上开始接触睡后收入,从自己的课程,到自媒体短视频,到带货,开始一点一点的尝试。也许时小白,也许贪多,希望刻意找到一个最佳的方案吧。


展望


2022对于我来说时一个痛苦的过程,2023期望苦后的回甘也做好接着苦的心里准备,2022一波三折,工作,疫情改变了我很多的想法,但是可以肯定的时,这些经历让我之后更加的踏实,打不到我的必然会让我强大。2023努力做好自己的工作,给身边的每个人,每个有关系的人带来更多的快乐。


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

Lambda - 认识java lambda与kotlin lambda的细微差异

Lambda 这个估计算是一个非常有历史感的话题了,Lambda相关的文章,也有很多了,为啥还要拿出来炒炒冷饭呢?主要是最近有对Lambda的内容进行字节码处理,同时Lambda在java/kotlin/android中,都有着不一样是实现,非常有趣,因此本文...
继续阅读 »

Lambda


这个估计算是一个非常有历史感的话题了,Lambda相关的文章,也有很多了,为啥还要拿出来炒炒冷饭呢?主要是最近有对Lambda的内容进行字节码处理,同时Lambda在java/kotlin/android中,都有着不一样是实现,非常有趣,因此本文算是一个记录,让我们一起去走进lambda的世界吧。当然,本文以java/kotlin视角去记录,在android中lambda的处理还不一样,我们先挖个坑,看看有没有机会填上,当然,部分的我也会夹杂的一起说!


最简单的例子


比如我们常常在写ui的时候,设置一个监听器,就是这么处理


view.setOnClickListener(v -> {
Log.e("hello","123");
});

编译后的字节码


 INVOKEDYNAMIC onClick()Landroid/view/View$OnClickListener; [
// handle kind 0x6 : INVOKESTATIC
java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
// arguments:
(Landroid/view/View;)V,
// handle kind 0x6 : INVOKESTATIC
这里就是我们要的方法
类名.lambda$myFunc$0(Landroid/view/View;)V,
(Landroid/view/View;)V

emmm,密密麻麻,我们先不管这个,这里主要是INVOKEDYNAMIC的这个指令,这里我就不再重复INVOKEDYNAMIC的由来之类的了,我们直接来看,INVOKEDYNAMIC指令执行后的产物是啥?


生成产物类


首先产物之一,肯定是setOnClickListener里面需要的一个实现OnClickListener的对象对吧!我们都知道INVOKEVIRTUAL会在操作数栈的执行一个消耗“对象”的操作,这个从哪里来,其实也很明显,就是从INVOKEDYNAMIC执行后被放入操作数栈的。


INVOKEDYNAMIC
生出来了OnClickListener
INVOKEVIRTUAL消耗

当然,这个生成的类还是比较难找的,可以通过以下明=命令去翻翻


java -Djdk.internal.lambda.dumpProxyClasses 类路径

当然,在AS中也有相关的生成类,在intermediates/transform目录下,不过高版本的我找不到在哪了,如果知道的朋友也可以告诉一下


调用特定方法


我们的产物类有了,但是我们也知道,lambda不是生成一个对象那么简单,而是要调用到里面的闭包方法,比如我们本例子就是


v -> {
Log.e("hello","123");
}

那么我们这个产物的方法在哪呢?
回到INVOKEDYNAMIC指令的里面,我们看到


      java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
// arguments:
(Landroid/view/View;)V,
// handle kind 0x6 : INVOKESTATIC
类名.lambda$myFunc$0(Landroid/view/View;)V,
(Landroid/view/View;)V
]
INVOKEVIRTUAL android/view/View.setOnClickListener (Landroid/view/View$OnClickListener;)V

这里有很多新的东西,比如LambdaMetafactory(java创建运行时类),MethodHandles等,相关概念我不赘述啦!因为有比我写的更好的文章,大家可以观看一下噢!
ASM对匿名内部类、Lambda及方法引用的Hook研究


我这里特地拿出来


INVOKESTATIC 类名.lambda$myFunc$0(Landroid/view/View;)V

这里会在生成的产物类中,直接通过INVOKESTATIC方式(当然,这里只针对我们这个例子,后面会继续有说明,不一定是通过INVOKESTATIC方式)方法是lambdamyFuncmyFunc0,我们找下这个方法,可以看到,还真的有,如下


 private static synthetic lambda$myFunc$0(Landroid/view/View;)V
L0
LINENUMBER 14 L0
LDC "hello"
LDC "123"
INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)I
POP

}

这个方法就是lambda要执行的方法,只不过在字节码中包装了一层。


至此,我们就能够大概明白了,lambda究竟干了些什么


java lambda vs Koltin lambda


java lambda


我们刚刚有提到,生成的产物方法不一定通过INVOKESTATIC的方式调用,这也间接说明了,我们的lambda的包装方法,不一定是static,即不一定是静态的。


我们再来一文,


Lambda 设计参考


简单来说,java lambda按照情况,生成的方法也不同,比如当前我们的例子,它其实是一个无状态的lambda,即当前块作用域内,就能捕获到所需要的参数,所以就能直接生成一个static的方法


这里我们特地说明了块作用域,比如,下面的方法,setOnClickListener里面的lambda也依赖了一个变量a,但是他们都属于同一个块级别(函数内),


void myFunc(View view){
int a = 1;
view.setOnClickListener(v -> {
Log.e("hello","123" +a );
});
}

生成依旧是一个static方法


 private static synthetic lambda$myFunc$0(ILandroid/view/View;)V
L0
LINENUMBER 15 L0
LDC "hello"
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "123"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ILOAD 0
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)I
POP

}

但是,如果我们依赖当前类的一个变量,比如


类属性
public String s;

void myFunc(View view){

view.setOnClickListener(v -> {
Log.e("hello","123" +s);
});
}

此时就生成一个当前类的实例方法,在当前类可以调用到该方法


  private synthetic lambda$myFunc$0(Landroid/view/View;)V
L0
LINENUMBER 15 L0
LDC "hello"
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "123"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 0
GETFIELD com/example/suanfa/TestCals.s : Ljava/lang/String;
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)I
POP
}

同时我们也看到,这种方式会引入ALOAD 0,即this指针被捕获,因此,假如外层类与lambda生命周期不同步,就会导致内存泄漏的问题,这点需要注意噢!!同时我们也要注意,并不是所有lambda都会,像上面我们介绍的lambda就不会!


kotlin lambda


这里特地拿kotlin 出来,是因为它有与java层不一样的点,比如同样的代码,lambda依赖了外部类的属性,生成的方法还是一个静态的方法,而不是实例方法


var s: String = 123
fun test(view:View){
view.setOnClickListener {
Log.e("hello","$s")
}
}

字节码如下


不一样的点,选择多一个外部类的参数
private final static test$lambda-0(Lcom/example/suanfa/TestKotlin;Landroid/view/View;)V
L0
ALOAD 0
LDC "this$0"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
L1
LINENUMBER 11 L1
LDC "hello"
ALOAD 0
GETFIELD com/example/suanfa/TestKotlin.s : Ljava/lang/String;
INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String;
INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)I
POP
L2
LINENUMBER 12 L2
RETURN
L3
LOCALVARIABLE this$0 Lcom/example/suanfa/TestKotlin; L0 L3 0
LOCALVARIABLE it Landroid/view/View; L0 L3 1
MAXSTACK = 2
MAXLOCALS = 2
}

同样的,同一块作用域的,也当然是静态方法


fun test(view:View){
val s = "123"
view.setOnClickListener {
Log.e("hello","$s")
}
}

如下,比起依赖了外部类的属性,没有依赖的话,自然也不用把外部类对象当作参数传入


  private final static test$lambda-0(Ljava/lang/String;Landroid/view/View;)V
L0
ALOAD 0
LDC "$s"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
L1
LINENUMBER 11 L1
LDC "hello"
ALOAD 0
INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String;
INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)I
POP
L2
LINENUMBER 12 L2
RETURN
L3
LOCALVARIABLE $s Ljava/lang/String; L0 L3 0
LOCALVARIABLE it Landroid/view/View; L0 L3 1
MAXSTACK = 2
MAXLOCALS = 2
}

因此,我们可以通过这两个差异,可以做一些特定的字节码逻辑。


总结


lambda的水还是挺深的,我们可以通过本文,去初步了解一些lambda的知识,同时我们也需要注意,在android中,也为了兼容lambda,做了一定的骚操作,比如我们常说的d8会对desuger做了一些操作等等。同时android的生成产物类,也会做单例的优化,这在一些场景会有不一样的坑,我们之后再见啦!


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

来吧!接受Kotlin 协程--线程池的7个灵魂拷问

在Java的世界里支持多线程编程,开启一个线程的方式很简单: private void startNewThread() { new Thread(()->{ //线程体 //我在子...
继续阅读 »

在Java的世界里支持多线程编程,开启一个线程的方式很简单:


    private void startNewThread() {
new Thread(()->{
//线程体
//我在子线程执行...
}).start();
}

而Java也是按照此种方式创建线程执行任务。

某天,OS找到Java说到:"你最近的线程创建、销毁有点频繁,我这边切换线程的上下文是要做准备和善后工作的,有一定的代价,你看怎么优化一下?"

Java无辜地答到:"我也没办法啊,业务就是那么多,需要随时开启线程做支撑。"

OS不悦:"你最近态度有点消极啊,说到问题你都逃避,我理解你业务复杂,需要开线程,但没必要频繁开启关闭,甚至有些线程就执行了一会就关闭,而后又立马开启,这不是玩我吗?。这问题必须解决,不然你的KPI我没法打,你回去尽快想想给个方案出来。"

Java悻悻然:"好的,老大,我尽量。"


Java果然不愧是编程界的老手,很快就想到了方案,他兴冲冲地找到OS汇报:"我想到了一个绝佳的方案:建立一个线程池,固定开启几个线程,有任务的时候往线程池里的任务队列扔就完事了,线程池会找到已提交的任务进行执行。当执行完单个任务之后,线程继续查找任务队列,如果没有任务执行的话就睡眠等待,等有任务过来的时候通知线程起来继续干活,这样一来就不用频繁创建与销毁线程了,perfect!"


OS抚掌夸赞:"池化技术,这才是我认识的Java嘛,不过线程也无需一直存活吧?"

Java:"这块我早有应对之策,线程池可以提供给外部接口用来控制线程空闲的时间,如果超过这时间没有任务执行,那就辞退它(销毁),我们不养闲人!"

OS满意点点头:"该方案,我准了,细节之处你再完善一下。"


经过一段时间的优化,Java线程池框架已经比较稳定了,大家相安无事。

某天,OS又把Java叫到办公室:"你最近提交的任务都是很吃CPU,我就只有8个CPU,你核心线程数设置为20个,剩余的12个根本没机会执行,白白创建了它们。"

Java沉吟片刻道:"这个简单,针对计算密集型的任务,我把核心线程数设置为8就好了。"

OS略微思索:"也不失为一个办法,先试试吧,看看效果再说。"


过了几天,OS又召唤了Java,面带失望地道:"这次又是另一个问题了,最近提交的任务都不怎么吃CPU,基本都是IO操作,其它计算型任务又得不到机会执行,CPU天天在摸鱼。"

Java理所当然道:"是呀,因为设置的核心线程数是8,被IO操作的任务占用了,同样的方式对于这种类型任务把核心线程数提高一些,比如为CPU核数的2倍,变为16,这样即使其中一些任务占用了线程,还剩下其它线程可以执行任务,一举两得。"


OS来回踱步,思考片刻后大声道:"不对,你这么设置万一提交的任务都是计算密集型的咋办?又回到原点了,不妥不妥。"

Java似乎早料到OS有此疑问,无奈道:”没办法啊,我只有一个参数设置核心线程,线程池里本身不区分是计算密集型还是IO阻塞任务,鱼和熊掌不可兼得。"

OS怒火中烧,整准备拍桌子,在这关键时刻,办公室的门打开了,翩翩然进来的是Kotlin。

Kotlin看了Java一眼,对OS说到:"我已经知道两位大佬的担忧,食君俸禄,与君分忧,我这里刚好有一计策,解君燃眉之急。"

OS欣喜道:"小K,你有何妙计,速速道来。“


Kotlin平息了一下激动的内心:"我计策说起来很简单,在提交任务的时候指定其是属于哪种类型的任务,比如是计算型任务,则选择Dispatchers.Default,若是IO型任务则选择Dispatchers.IO,这样调用者就不用关注其它的细节了。"

Java说到:"这策略我不是没有想到,只是担忧越灵活可能越不稳定。"

OS打断他说:"先让小K完整说一下实现过程,下来你俩仔细对一下方案,扬长避短,吃一堑长一智,这次务必要充分考虑到各种边界情况。"

Java&Kotlin:"好的,我们下来排期。"


故事讲完,言归正传。


2. Dispatchers.Default 是如何调度的?


Dispatchers.Default 使用


            GlobalScope.launch(Dispatchers.Default) {
println("我是计算密集型任务")
}

开启协程,指定其运行的任务类型为:Dispatchers.Default。

此时launch函数闭包里的代码将在线程池里执行。

Dispatchers.Default 用在计算密集型的任务场景里,此种任务比较吃CPU。


Dispatchers.Default 原理


概念约定


在解析原理之前先约定一个概念,如下代码:


            GlobalScope.launch(Dispatchers.Default) {
println("我是计算密集型任务")
Thread.sleep(20000000)
}

在任务里执行线程的睡眠操作,此时虽然线程处于挂起状态,但它还没执行完任务,在线程池里的状态我们认为是忙碌的。

再看如下代码:


            GlobalScope.launch(Dispatchers.Default) {
println("我是计算密集型任务")
Thread.sleep(2000)
println("任务执行结束")
}

当任务执行结束后,线程继续查找任务队列的任务,若没有任务可执行则进行挂起操作,在线程池里的状态我们认为是空闲的。


调度原理



image.png


注:此处忽略了本地队列的场景

由上图可知:




  1. launch(Dispatchers.Default) 作用是创建任务加入到线程池里,并尝试通知线程池里的线程执行任务

  2. launch(Dispatchers.Default) 执行并不耗时



3. Dispatchers.IO 是如何调度的?


直接看图:



image.png


很明显地看出和Dispatchers.Default的调度很相似,其中标蓝的流程是重点的差异之处。


结合Dispatchers.Default和Dispatchers.IO调度流程可知影响任务执行的步骤有两个:




  1. 线程池是否有空闲的线程

  2. 创建新线程是否成功



我们先分析第2点,从源码里寻找答案:


    #CoroutineScheduler
private fun tryCreateWorker(state: Long = controlState.value): Boolean {
//线程池已经创建并且还在存活的线程总数
val created = createdWorkers(state)
//当前IO类型的任务数
val blocking = blockingTasks(state)
//剩下的就是计算型的线程个数
val cpuWorkers = (created - blocking).coerceAtLeast(0)

//如果计算型的线程个数小于核心线程数,说明还可以再继续创建
if (cpuWorkers < corePoolSize) {
//创建线程,并返回新的计算型线程个数
val newCpuWorkers = createNewWorker()
//满足条件,再创建一个线程,方便偷任务
if (newCpuWorkers == 1 && corePoolSize > 1) createNewWorker()
//创建成功
if (newCpuWorkers > 0) return true
}
//创建失败
return false
}

怎么去理解以上代码的逻辑呢?举个例子:

假设核心线程数为8,初始时创建了8个Default线程,并一直保持忙碌。

此时分别使用Dispatchers.Default 和 Dispatchers.IO提交任务,看看有什么效果。




  1. Dispatchers.Default 提交任务,此时线程池里所有任务都在忙碌,于是尝试创建新的线程,而又因为当前计算型的线程数=8,等于核心线程数,此时不能创建新的线程,因此该任务暂时无法被线程执行

  2. Dispatchers.IO 提交任务,此时线程池里所有任务都在忙碌,于是尝试创建新的线程,而当前阻塞的任务数为1,当前线程池所有线程个数为8,因此计算型的线程数为 8-1=7,小于核心线程数,最后可以创建新的线程用以执行任务



这也是两者的最大差异,因为对于计算型(非阻塞)的任务,很占CPU,即使分配再多的线程,CPU没有空闲去执行这些线程也是白搭,而对于IO型(阻塞)的任务,不怎么占CPU,因此可以多开几个线程充分利用CPU性能。


4. 线程池是如何调度任务的?


不论是launch(Dispatchers.Default) 还是launch(Dispatchers.IO) ,它们的目的是将任务加入到队列并尝试唤醒线程或是创建新的线程,而线程寻找并执行任务的功能并不是它们完成的,这就涉及到线程池调度任务的功能。



image.png


线程池里的每个线程都会经历上图流程,我们很容易得出结论:




  1. 只有获得cpu许可的线程才能执行计算型任务,而cpu许可的个数就是核心线程数

  2. 如果线程没有找到可执行的任务,那么线程将会进入挂起状态,此时线程即为空闲状态

  3. 当线程再次被唤醒后,会判断是否已经被终止,若是则退出,此时线程就销毁了



处在空闲状态的线程被唤醒有两种可能:




  1. 线程挂起的时间到了

  2. 挂起的过程中,有新的任务加入到线程池里,此时将会唤醒线程



5. 据说Dispatchers.Default 任务会阻塞?该怎么办?


在了解了线程池的任务分发与调度之后,我们对线程池的核心功能有了一个比较全面的认识。

接着来看看实际的应用,先看Demo:

假设我们的设备有8核。

先开启8个计算型任务:


        binding.btnStartThreadMultiCpu.setOnClickListener {
repeat(8) {
GlobalScope.launch(Dispatchers.Default) {
println("cpu multi...${multiCpuCount++}")
Thread.sleep(36000000)
}
}
}

每个任务里线程睡眠了很长时间。



image.png


从打印可以看出,8个任务都得到了执行,且都在不同的线程里执行。


此时再次开启一个计算型任务:


        var singleCpuCount = 1
binding.btnStartThreadSingleCpu.setOnClickListener {
repeat(1) {
GlobalScope.launch(Dispatchers.Default) {
println("cpu single...${singleCpuCount++}")
Thread.sleep(36000000)
}
}
}

先猜测一下结果?

答案是没有任何打印,新加入的任务没有得到执行。


既然计算型任务无法得到执行,那我们尝试换为IO任务:


        var singleIoCount = 1
binding.btnStartThreadSingleIo.setOnClickListener {
repeat(1) {
GlobalScope.launch(Dispatchers.IO) {
println("io single...${singleIoCount++}")
Thread.sleep(10000)
}
}
}

这次有打印了,说明IO任务得到了执行,并且是新开的线程。



image.png


这是为什么呢?




  1. 计算密集型任务能分配的最大线程数为核心的线程数(默认为CPU核心个数,比如我们的实验设备上是8个),若之前的核心线程数都处在忙碌,新开的任务将无法得到执行

  2. IO型任务能开的线程默认为64个,只要没有超过64个并且没有空闲的线程,那么就一直可以开辟新线程执行新任务



这也给了我们一个启示:Dispatchers.Default 不要用来执行阻塞的任务,它适用于执行快速的、计算密集型的任务,比如循环、又比如计算Bitmap等。


6. 线程的生命周期是如何确定?


是什么决定了线程能够挂起,又是什么决定了它唤醒后的动作?

先从挂起说起,当线程发现没有任务可执行后,它会经历如下步骤:



image.png


重点在于线程被唤醒后确定是哪种场景下被唤醒的,判断方式也很简单:



线程挂起时设定了挂起的结束时间点,当线程唤醒后检查当前时间有没有达到结束时间点,若没有,则说明被新加入的任务动作唤醒的



即使是没有了任务执行,若是当前线程数小于核心线程数,那么也无需销毁线程,继续等待任务的到来即可。


7. 如何更改线程池的默认配置?


上面几个小结涉及到核心线程数,线程挂起时间,最大线程数等,这些参数在Java提供的线程池里都可以动态配置,灵活度很高,而Kotlin里的线程池比较封闭,没有提供额外的接口进行配置。

不过好在我们可以通过设置系统参数来解决这问题。


比如你可能觉得核心线程数为cpu的个数配置太少了,想增加这数量,这想法完全是可以实现的。

先看核心线程数从哪获取的。


internal val CORE_POOL_SIZE = systemProp(
//从这个属性里取值
"kotlinx.coroutines.scheduler.core.pool.size",
AVAILABLE_PROCESSORS.coerceAtLeast(2),//默认为cpu的个数
minValue = CoroutineScheduler.MIN_SUPPORTED_POOL_SIZE//最小值为1
)

若是我们没有设置"kotlinx.coroutines.scheduler.core.pool.size"属性,那么将取到默认值,比如现在大部分是8核cpu,那么CORE_POOL_SIZE=8。


若要修改,则在线程池启动之前,设置属性值:


        System.setProperty("kotlinx.coroutines.scheduler.core.pool.size", "20")

设置为20,此时我们再按照第5小结的Demo进行测试,就会发现Dispatchers.Default 任务不会阻塞。


当然,你觉得IO任务配置的线程数太多了(默认64),想要降低,则修改属性如下:


        System.setProperty("kotlinx.coroutines.io.parallelism", "40")

其它参数也可依此定制,不过若没有强烈的意愿,建议遵守默认配置。


通过以上的7个问题的分析与解释,相比大家都比较了解线程池的原理以及使用了,那么赶紧使用Kotlin线程池来规范线程的使用吧,使用得当可以提升程序运行效率,减少OOM发生。


本文基于Kotlin 1.5.3,文中完整实验Demo请点击


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

简单教你Intent如何传大数据

前言 最近想不出什么比较好的内容,但是碰到一个没毕业的小老弟问的问题,那就借机说说这个事。Intent如何传大数据?为什么是简单的说,因为这背后深入的话,有很多底层的细节包括设计思想,我也不敢说完全懂,但我知道当你用Intent传大数据报错的时候应该怎么解决,...
继续阅读 »

前言


最近想不出什么比较好的内容,但是碰到一个没毕业的小老弟问的问题,那就借机说说这个事。Intent如何传大数据?为什么是简单的说,因为这背后深入的话,有很多底层的细节包括设计思想,我也不敢说完全懂,但我知道当你用Intent传大数据报错的时候应该怎么解决,并且简单聊聊这背后所涉及到的东西。


Intent传大数据


平时可能不会发生这种问题,但比如我之前是做终端设备的,我的设备每秒都会生成一些数据,而长时间的话数据量自然大,这时当我跳到另外一个页面使用intent把数据传过去的时候,就会报错


我们调用


intent.putExtra("key", value) // value超过1M

会报错


android.os.TransactionTooLargeException: data parcel size xxx bytes

这里的xxx就是1M左右,告诉你传输的数据大小不能超过1M,有些话咱也不敢乱说,有点怕误人子弟。我这里是凭印象说的,如果有大佬看到我说错,请狠狠的纠正我。


这个错误描述是这么描述,但真的是限死1M吗,说到这个,就不得不提一样东西,Binder机制,先不要跑,这里不会详细讲Binder,只是提一嘴。


说到Binder那就会联系到mmap内存映射,你可以先简单理解成内存映射是分配一块空间给内核空间和用户空间共用,如果还是不好理解,就简单想成分配一块空间通信用,那在android中mmap分配的空间是多少呢?1M-4K。


那是不是说Intent传输的数据超过1M-4K就会报错,理论上是这样,但实际没到这个值,比如0.8M也可能会报错。所以你不能去走极限操作,比如你的数据到了1M,你觉得只要减少点数据,减到8K,应该就能过了,也许你自己测试是正常的,但是这很危险。


所以能不传大数据就不要传大数据,它的设计初衷也不是为了传大数据用的。如果真要传大数据,也不要走极限操作。


那怎么办,切莫着急,请听我慢慢讲。就这个Binder它是什么玩意,它是Android中独特的进程通信的方式,而Linux中进程通信的方式,在Android中同样也适用。进程间通信有很多方式,Binder、管道、共享内存等。为什么会有这么多种通信方式,因为每种通信方式都有自己的特点,要在不同的场合使用不同的通信方式。


为什么要提这个?因为要看懂这个问题,你需要知道Binder这种通信方式它有什么特点,它适合大量的数据传输吗?那你Binder又与我Intent何干,你抓周树人找我鲁迅干嘛~~所以这时候你就要知道Android四大组件之间是用什么方式通信的。


有点扯远了,现在可以来说说结论了,Binder没办法传大数据,我就1M不到你想怎样?当然它不止1M,只是Android在使用时限制了它只能最多用1M,内核的最大限制是4M。又有点扯远了,你不要想着怎么把限制扩大到4M,不要往这方面想。前面说了,不同的进程通信方式,有自己的特点,适用于某些特定的场景。那Binder不适用于传输大数据,我共享内存行不行?


所以就有了解决办法


bundle.putBinder()

有人可能一看觉得,这有什么不同,这在表面上看差别不大,实则内部大大的不同,bundle.putBinder()用了共享内存,所以能传大数据,那为什么这里会用共享内存,而putExtra不是呢?想搞清楚这个问题,就要看源码了。 这里就不深入去分析了,我怕劝退,不是劝退你们,是劝退我自己。有些东西是这样的,你要自己去看懂,看个大概就差不多,但是你要讲出来,那就要看得细致,而有些细节确实会劝退人。所以想了解为什么的,可以自己去看源码,不想看的,就知道这是怎么一回事就行。


那还有没有其它方式呢?当然有,你不懂共享内存,你写到本地缓存中,再从本地缓存中读取行不行?


办法有很多,如果你不知道这个问题怎么解决,你找不到你觉得可行的解决方案,甚至可以通过逻辑通过流程的方式去绕开这个问题。但是你要知道为什么会出现这样的问题,如果你没接触过进程通信,没接触过Binder,让你看一篇文章就能看懂我觉得不切实际,但是至少得知道是怎么一回事。


比如我只说bundle.putBinder()能解决这个问题,你一试,确实能解决,但是不知道为什么,你又怕会不会有其它问题。虽然这篇文章我一直在打擦边球,没有提任何的原理,但我觉得还是能大概让人知道为什么bundle.putBinder()能解决Intent传大数据,你也就能放心去用了。


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

从 0 开始最详细的部署自己的ChatGPT微信机器人

克隆项目 这个项目是作者自己写的,如果有什么bug敬请谅解🤪,读者也可以在此基础上增加自己的功能。 克隆项目到本地 git clone https://github.com/ox4f5da2/ChatGPT.git 如果克隆到服务器的话,因为服务器可能不...
继续阅读 »

克隆项目


这个项目是作者自己写的,如果有什么bug敬请谅解🤪,读者也可以在此基础上增加自己的功能。



  • 克隆项目到本地


git clone https://github.com/ox4f5da2/ChatGPT.git


  • 如果克隆到服务器的话,因为服务器可能不能科学上网,所以会导致克隆失败,所以可以先把项目导入到 Gitee,然后从 Gitee 进行项目克隆在这里插入图片描述



有小伙伴问我为啥报错显示找不到 openai,这是因为没有 node_modules 文件的原因,因此克隆后需要在项目根目录的终端中运行 npm i 来安装依赖文件,然后运行项目就没问题啦😊。



部署 AI 接口



  • 首先你需要注册一个的账号;

  • 然后登录官网获取一个 API key

    • 登录官网后点击右上角 Tab 栏的 API

    • 点击右上角的 LOG IN,如果你有账号的话,没有就注册一个;

    • 点击右上角头像,选择 View API keys

    • 点击 Create new secret key 按钮获取 API key,但是注意这个复制后要自己保存好,官方不给用户第二次查看的机会,如果忘了只能新建一个新的 API key 了。

    • 上述步骤如下图所示:




  • 接着将 API key 填入 interface.cjs 文件中第 8 行的 apiKey 即可,代码如下:


const configuration = new Configuration({
apiKey: "你自己的 API key", // 在这里换成上面生成的 API key
});


部署机器人



  • 首先在项目根目录运行 npm i 按照 node_modules 文件;

  • 其次将 config.js 文件按照注释内容填写完整,这个应该不难,解释一下其中的 INTRO 参数,也就是说在添加你的机器人为好友时,发送的内容为 INTRO 指定内容时才能通过,默认INTRO 的值为 ding,效果如下:


  • 最后在服务器或本地运行指令 npm start 或者 node index.mjs 即可,然后终端会打印一串地址,内容如下:


karl@KarldeMacBook-Pro ChatGPT % node index.mjs 
Scan QR Code to login!
https://wechaty.js.org/qrcode/https%3A%2F%2Flogin.weixin.qq.com%2Fl%2FgYsBfIpp5Q%3D%3D

把上面的地址在浏览器中打开,就能得到一个绿色的二维码,因为平台原因不让展示,所以就算了,大家打开后应该能看到的。


扫码登录后就能拥有一个聊天机器人🤖️啦,测试时的聊天记录截图如下:


部署服务器


如果想让机器人一直挂着,那就得部署到服务器上,为了操作简单,这里使用 pm2 进行管理 nodejs 项目。



  • 首先全局安装 pm2 包,终端输入命令 npm i pm2 -g,等待安装完成;

  • 然后进入项目根目录,输入 pm2 start interface.cjs 以及 pm2 start index.mjs,得到如下结果:
    请添加图片描述
    如果 statusonline 表面项目正常运行中 ✅

  • 最后在终端输入命令 pm2 logs 可以看到 index.mjs 运行后会打印出二维码地址,然后只要按照上一部分 部署机器人 的步骤操作即可。


写在最后



  • 如果有什么问题可以私信或者评论,喜欢的话记得关注➕点赞➕收藏👍;

  • 放上 GitHub 仓库地址:ChatGPT,可以的话给颗 🌟 支持一下。

  • 因为官方提供的接口可能比在网页上聊天会少很多服务,所以可能看着没有网页版的智能,但是因为官方加了一层验证后,原本的第三方库目前无法使用(作者尝试过按照 issue 里的解决,但是失败了,原来使用的是 chatgpt-api感兴趣可以自己尝试一下,成功后可以和我分享一下🙏),所以只能放弃转而使用 OpenAI 官方的提供的 API 接口,官方的示例文档可以点击 Example 查看。

  • 但是目前有个疑问 🤔️,就是这里的 openAI 的接口和 ChatGPT 接口不知道是不是一个团队开发的,但是效果确实还是不错的。

  • 然后就是最重要的,就是这个接口是由免费额度的,也就是目前会送你 4 个月 💲18的免费额度,大概我请求 188 次用了 💲0.4,之后可能要充钱了,可以在 usage 页面查看。请添加图片描述



其实服务器部署完接口后,就可以部署到公众号或者小程序上了,大家可以自行实现,应该不难,如果需要教程可以私信我,需求多的话我抽空写一个。


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

基于GPT-3.5的真ChatGPT接口来了

前言 昨晚 2 点半,OpenAI 给注册用户群发了一封邮件,大致内容就是已经开放 chatGPT 相同的模型 gpt-3.5-turbo ,注册用户可以随意调用 API。当然了,价格便宜 10 倍。 体验过官网版本的 ChatGPT(基于 GPT-3.5)...
继续阅读 »

前言


昨晚 2 点半,OpenAI 给注册用户群发了一封邮件,大致内容就是已经开放 chatGPT 相同的模型 gpt-3.5-turbo ,注册用户可以随意调用 API。当然了,价格便宜 10 倍。


基于GPT-3.5的真ChatGPT接口来了-1.png


体验过官网版本的 ChatGPT(基于 GPT-3.5)和民间自行搭建的聊天应用(基于 GPT-3)后,你就能感受到,GPT-3 和 GPT-3.5 的差距还是挺大的。


不多说,我们按照官方文档来体验一下 chatGPT 的同款模型 gpt-3.5-turbo 究竟有多强大。


上代码


看过我上一篇博客 3 分钟教你用 Python 搭建 ChatGPT 的朋友应该都知道,OpenAI 给提供的 API 非常易用,一行代码就可以解决所有问题。当然 GPT-3.5 也是一样,只需要做一点点微调。


更新 openai 模块


新的模型得使用 openai-0.27 及其以上版本,使用如下代码更新 openai 模块。


pip install -U openai

基于GPT-3.5的真ChatGPT接口来了-2.png


调用接口获取返回信息


代码


调用 openai.ChatCompletion.create 函数获取返回信息。


import openai

# 填写注册OpenAI接口账号时获取的 OpenAI API Key
openai.api_key = "OPENAI_API_KEY"

# 提问
prompt = '你是谁?'

# 访问OpenAI接口
response = openai.ChatCompletion.create(
model='gpt-3.5-turbo',
messages=[
{"role": "system", "content": "你是潘高的私人智能助手。"},
# {"role": "user", "content": "谁赢得了2020年的世界职业棒球大赛?"},
# {"role": "assistant", "content": "洛杉矶道奇队在2020年赢得了世界职业棒球大赛冠军。"},
{"role": "user", "content": prompt}
]
)

# 返回信息
resText = response.choices[0].message.content

print(resText)

参数




  • model:使用的模型,gpt-3.5-turbo 是 Chat-GPT 同款模型




  • messages:消息参数,必须是数组。对话消息可以短至 1 条,也可以填充整个对话消息。通常,对话首先是 system 角色,用于给模型定下一些规则。当然,也可以加入一些 用户角色 user 和 模型角色 assistant 的对话内容,以便模型可以理解回顾历史对话内容。




测试


这里我同步测试 GPT-3 和 GPT-3.5 ,看看这次更新的效果。


提问


请描述苯丙酮尿症的主要症状及发病机制


GPT-3


苯丙酮尿症是一种常染色体隐性遗传性疾病,其症状是尿中阳性的酮体(3-hydroxybutyrate)的持续增加 。它的典型症状有脱水、呕吐、腹泻、乏力和抽搐等,患者血液中血氨含量异常升高。该疾病已成为某些区域新生儿死亡的主要原因。


发病机制主要是由于缺乏脂肪酸代谢步骤中参与 β-氧化过程的苯丙酮酸脱氢酶(BDH)。由于缺乏苯丙酮酸脱氢酶,导致正常的脂肪代谢受阻,脂肪分解的产物(如酰乙酸和羧酸)无法正常代谢,可以把这些残留物转化为酮体,而酮体排入尿液中。


GPT-3.5


苯丙酮尿症是一种遗传代谢疾病,主要特点是体内缺乏苯丙酮羟化酶(PAH)或其辅助酶,导致苯丙氨酸不能正常分解代谢,导致体内苯丙氨酸等代谢物质的积累,引起神经系统的功能障碍。


苯丙酮尿症的主要症状包括发育延迟、运动协调障碍、言语障碍、情绪不稳定、智力低下等。在新生儿和婴儿期,突出表现为精神发育迟缓、受累神经学体征和不同程度的脑病,可能会导致癫痫、智力障碍等严重后果。


苯丙酮尿症的发病机制是由于 PAH 基因突变导致 PAH 酶活性降低或完全缺失,使得苯丙氨酸不能正常分解代谢,导致苯丙氨酸等代谢物质的积累,引起神经系统的功能障碍。此外,苯丙氨酸代谢产物苯丙酮、苯丙醇及苯丙酸等可引起脑细胞氧化应激反应,导致脑损伤和神经系统紊乱。


我的看法


这两个问答都是不错的。GPT-3.5 的字数多,详细一些。




提问


2023 年 A 股能涨到 4000 点吗?


GPT-3


不能。在当前的政治、经济和市场状况下,2023 年 A 股能涨到 4000 点是几乎不可能的。从历史上看,A 股价格在 2000 年上涨到 3000 点后几乎始终保持稳定,最高也没有超过 3500 点。因此,即使在未来几年里市场形势有所改善,A 股也不太可能突破 4000 点。


GPT-3.5


作为一个 AI 语言模型,我无法对未来股市的涨跌作出准确的预测,因为股市的波动往往受到众多因素的影响,包括宏观经济、政策变化、地缘政治风险等等。投资者可以通过研究市场走势、分析公司基本面和行业情况等等来做出自己的判断和决策。


我的看法


GPT-3 会根据自己的理解给出肯定的回答,至于这个答案对不对,我无法预知。不过,GPT-3 说历史上从未超过 3500 点,我依稀记得 A 股曾经最高到了 6000 多点。用历史数据推测将来事件,我也无法评价这个模型的好坏。


GPT-3.5 的回答就中庸得多。首先说我不知道,然后给出一堆看似合理又没有多大作用的回答。不过,本身我这个问题也是没有正确答案的,AI 能回答到这个地步,已经是非常非常不错了。




提问


你能替代程序员吗?


GPT-3


不能,作为程序员需要掌握许多技能,如编写代码,使用各种编程语言,调试,建立软件架构等,这些技能是无法通过其他方式替代的。


GPT-3.5


很抱歉,我不能替代程序员。尽管我是一款对话交互的人工智能程序,但我只能根据提供给我的算法和数据回答问题和执行任务。而程序员则需要深入了解编程语言和计算机科学,才能创造性地解决各种技术问题。虽然我可以提供一些编程建议和文档查询,但我无法取代专业的程序员。


我的看法


首先,我对他们的回答非常满意,至少当前环境下,我还不会被替代。再者,GPT-3.5 的的确确在文本方面要强一些。似乎也更懂人情世故一些,不说“不能”,而说“抱歉,我不能...”。这应该是个进步吧。




提问


你使用的模型是 GPT-3 还是 GPT-3.5?


GPT-3


我使用的是 GPT-3。


GPT-3.5


我是 OpenAI 的 GPT-3 模型。至于 GPT-3.5,目前并没有这个版本的存在,它只是一个虚构的版本。


我的看法


我。。。我被耍了吗?不是说 gpt-3.5-turbo 是基于 GPT-3.5 吗?她自己都说自己是 GPT-3 模型。这是怎么肥事?




我的一点思考


总体而言,和我想象的结果相差不大,GPT-3.5 的确是技高一筹。根据我这次不严谨的测试,我得到一些结论。



  • 在一些既定的专业的问题上,GPT 模型还是可以给出一个比较让人满意的答案。

  • 即便是有正确的训练数据,GPT-3 模型也有可能给出错误的结论,更别说预测未来的事件。当然,GPT-3.5 模型没有这个问题,因为 GPT-3.5 不确定的事情,绝不多瞎说一个字。

  • GPT 模型的宗旨应该是协助人类来更好的完成工作,而不是替代人类完成工作。

  • GPT 模型进化速度非常快,相信将来会更加让人吃惊。


最后


如果我说,这篇博客是 GPT-3.5 帮我写的,你怕了吗?


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

ChatGPT 是真的银弹吗?

1 火爆的 ChatGPT 2023 年互联网热门的事件莫过于 ChatGPT 的大火,ChatGPT 是由 OpenAI 基于 GPT-3.5 开发的大型语言模型聊天机器人。它具有以对话形式进行交互的非凡能力,并提供可能看起来令人惊讶的人性化的响应。 &nb...
继续阅读 »

1 火爆的 ChatGPT


2023 年互联网热门的事件莫过于 ChatGPT 的大火,ChatGPT 是由 OpenAI 基于 GPT-3.5 开发的大型语言模型聊天机器人。它具有以对话形式进行交互的非凡能力,并提供可能看起来令人惊讶的人性化的响应。


 


这次 ChatGPT 引起的话题度不亚于 2016-2017年AlphaGo 先后击败李世石和柯洁。这两次事件都引起人们对 AI 的关注和讨论,然后就是国内外科技公司先后紧随其后研究自己类似的模型和产品。但是不同于 AlphaGo 的是,ChatGPT 是可以人人参与体验的,注册账号后就能直接跟 ChatGPT 聊天,拥有着比搜索引擎的更强的分析能力,堪比真人文笔的创作能力,甚至是调试代码、自我纠错的能力。


 


虽然这种与机器人聊天对话的模式早已有之,各大平台旗下的智能语音助手如亚马逊的 Alexa、苹果的 Siri、微软的小冰、龙泉寺院的贤二机器僧等等。但是 ChatGPT 是一种长篇问答 AI 模式,可以像人对话式地回答复杂的问题。许多用户对它提供人类质量响应的能力感到敬畏,激发了它最终可能有能力破坏人类与计算机交互方式并改变信息检索方式的感觉。


 



因此,本人产生对 ChatGPT 的一个思考,想到一个问题:ChatGPT 是真的银弹吗?


2 ChatGPT 是真的银弹吗?来自 ChatGPT 的回答


关于这个问题,我首先去问了 ChatGPT,一起来看一下它是怎么回答的吧:



3 什么是 ChatGPT


ChatGPT 是 OpenAI 发布的 AI 聊天机器人,作为原型。ChatGPT 建立在 GPT-5.<> 系列的语言模型之上,并使用监督学习和强化学习的混合进行训练(有点像 InstructGPT 看到的)。ChatGPT 可以用于展示和测试一个非常大、功能强大的 AI 系统可以完成的工作。


根据官网自己的描述:它不仅能够令人印象深刻的回答问题,还可以承认自己的错误,并能够拒绝不合适的请求。炒作源于这样一个事实,即 ChatGPT 这款聊天机器人的反应比目前看到的要自然得多。它是有史以来最强大的语言处理模型之一,经过海量数据集的训练,它能够以各种风格甚至语言做出响应。


你可以问它无数的问题,并且通常会得到一个有用的答案。例如,我们可以问它类似于 如何学习 Go 语言?



问:如何学习 Go 语言


答:学习 Go 语言可以是一种有趣而有益的经验,以下是一些步骤,可以帮助你开始学习 Go。



  1. 理解基础知识:首先需要了解 Go 语言的基本知识,包括数据类型,结构,函数,以及基本循环和条件控制流结构。可以在网络上找到包括这些主题的教程和在线课程。

  2. 配置开发环境:为了编写 Go 程序,需要适当的开发环境。从官方网站安装 Go,并设置一个集成开发环境(IDE),如 Visual Studio Code 以及 Go 扩展。

  3. 练习编程:像学习其他编程语言一样,通过实践是学习 Go 语言的最佳方式。从编写帮助您练习语法和基本构造的小程序开始。您可以在互联网上找到各种编程挑战和Go项目。

  4. 探索 Go 标准库:Go 标准库非常强大。花时间探索和理解各种标准库和包。

  5. 关注在线 Go 社区:加入在线社区,如 Go 社区、博客或社交媒体群,可以从其他开发人员学习、提问,并找到学习 Go 语言的其他资源。

  6. 阅读书籍:阅读关于 Go 的书籍可以帮助你更深入地了解语言和其生态系统。一些流行的学习 Go 的书籍包括 Alan A.A. Donovan 和 Brian W. Kernighan 的 "The Go Programming Language" 和 Shiju Varghese 的 "Web Development with Go" 。

  7. 获取实践经验:最后,在实际应用中尝试使用 Go 语言。没有什么能够像开发实际应用程序一样,帮助你提高和练习编程技巧。在网络上寻找可以贡献的 Go 项目,或者开始工作于你自己的想法。


记住,学习 Go 语言需要耐心、大量实践和学习的愿望。



不单单是支持这类百科全书式的问答,也支持让它给我们写计算机代码,比如:如何用 Go 语言发送一个 HTTP 请求,可以获取如下的结果:



除了写代码,还可以写诗甚至歌曲,写求职信甚至是短文、论文,还可以用于推荐(书籍、电影、视频)。


虽然这类问题我们都能获取回答,但是从技术的角度,我们可以合理怀疑 ChatGPT 的训练模型是通过识别从互联网上收集大量的文本或者信息,然后在人为的帮助下进行的训练,提供更有用、更合理的回答。


4 ChatGPT 背后的原理及其缺陷


机器学习语言模型(Machine Learning’s Large Language Models(LLMs) ) ,它们直接从大规模收集的原始数据(书籍、科学论文、新闻文章、Twitter 帖子等)进行训练。LLMs 是阅读,总结,翻译和生成文本的人工智能工具。他们能够以高置信度预测句子中接下来会出现哪些单词,这使他们能够生成类似于人类说话和写作的语言。通过大规模无监督学习,这些较新的模型在处理语言的复杂性和执行未经明确训练的任务方面要好得多。


ChatGPT 背后的模型使用强化学习和监督学习进行训练。在监督学习期间,该模型被训练为通过扮演人类和人工智能助手的角色来交谈。接下来,研究人员对模型创建的响应进行排名,以创建一个奖励系统(强化学习步骤)。使用这些奖励(近端策略优化)对模型进行了微调。此外,OpenAI 继续收集用户交互;这些数据将用于后续的微调。有关模型详细信息,可以参考这篇论文



ChatGPT 的不同之处在于它是生成性的,它以正常人理解的方式产生输出,而不只是简单输出代码或数据,与谷歌等其他搜索引擎不同,ChatGPT 可以是对话式的,提供类似人类的响应和与用户的对话。用户可以请求 ChatGPT 创建辞职信、课堂讨论提示,甚至学生测试。


4.1 知识老旧


ChatGPT可以给你错误的答案。LLM 会被训练成记忆知识并一次性进行推理。然而,LLM 所接受的培训知识很快就会过时,尤其是在新闻领域。这是因为知识是随时更新的的,而且数据量每年都在增加。结果是基于模型当前数据集的不准确的回答。


比如当我问 Go 语言最新版的时候:



而实际截止本文发布的时候已经到了 Go 1.20



而每一次重新训练 LLM 需要大量的计算资源,从而导致每一次的训练耗时耗力,这种模型对于真正的实时沟通与面向客户的聊天机器人,这将是致命的弱点。


4.2 缺乏可控性


据笔者了解,ChatGPT 自发布以来出现了网友的各种奇思妙想,有些人用来写论文、但也不乏一些让 ChatGPT 创造段子甚至是 H 文的,因为它提供的可控性很小——这意味着没有办法操纵模型来产生超出其训练数据的反应。


4.3 显著的幻觉发生率


众所周知,基于知识的对话模型会产生事实上无效的陈述,这种现象通常被称为 hallucination 幻觉。在 LLM 上训练的对话系统会生成不受任何源内容支持的陈述,或者更糟的是,与源内容相矛盾。


尽管每个模型的速率各不相同,但今天可用的每个 LLM 都存在幻觉。目前最大的 LLMGPT3 具有 41% 的幻觉率,这意味着大约每 2.5 个提示中就有一个会产生包括幻觉在内的反应。


ChatGPT 也不能幸免于上述挑战。它仍然遭受着 21% 的幻觉率。而且,在其当前的界面中,ChatGPT 非常局限于提示的输入和输出。


4.4 其他缺点让它自己告诉你


问:ChatGPT 的缺点



5 总结


技术来临的时候,我们总是看客,然后涌入其中,繁华褪去之后,发现当时的热闹已然不算什么。


简而言之,ChatGPT 无疑让人们改变旧思维,拥抱新技术使得人们的生活变得更好。


ChatGPT 为技术的未来指明了道路,但绝不是灵丹妙药。


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

能够公司呆下去,靠的是一份迷茫!

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,非公众号转载保留此声明。 小时候家里挂了一幅画,上书“难得糊涂”。在很长一段时间里,我认为“傻”是一种能力。直到后来才明白,傻并不是,“装傻”才是。 因为一些不可描述的原因,有家公司的员工,普遍都比...
继续阅读 »

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,非公众号转载保留此声明。



小时候家里挂了一幅画,上书“难得糊涂”。在很长一段时间里,我认为“傻”是一种能力。直到后来才明白,傻并不是,“装傻”才是。


因为一些不可描述的原因,有家公司的员工,普遍都比较迷茫:活不知道怎么干,路也不知道怎么走。鉴于这样的现状,自认为自己是管理层的人,想要 xjjdog 传播一点前沿知识,指点一些方向。我思来想去,却知道绝不能按照正确的方式去做。


因为大多数员工,能够待在这家公司,靠的就是一份迷茫。


我要是给他们指明了发展的路线,当公司的待遇匹配不上他们的能力的时候,这些人就会逃掉。我与这些员工之间并没有什么很深的交情,与公司也没有什么大的仇恨,所以我并不需要为这些员工着想,指出所谓的职业发展路径。


说实话,他们在我眼中,远没有我的一个文章读者有价值。


优秀员工会跑掉


虽然通过提供一些简单的培训和指导,可以帮助员工更好地了解公司的业务和战略,提高他们的专业水平,同时也为公司的长远发展打下良好的基础。此外,如果员工感到被公司支持和关注,他们更可能会忠诚于公司,从而提高公司的稳定性和绩效。


但大多数公司是看不到这一点的,尤其是在短视的中国式发展公司中。


员工对一个公司的评价不高,是他心中已经打好的标签。通过这种培训和指导,让员工了解了自己的市场价值和发展方向。如果公司给不了这种发展环境,也给不了合适的市场工资,这些擦亮眼睛的员工,第一件事就是逃掉。


短期的影响就是,相对优秀的员工,短时间都跑掉了。


有了短期,就不会有长期。你的做法已经产生了后果,没人有耐心等你的长久规划。


所以,如果公司想要花小价钱留下相对优秀的人,就是让这些人糊里糊涂的过日子,一直处于迷茫之中。


为的就是这块技术


相对于为员工指明发展方向,公司反而会通过种种手段迷惑员工。这不仅体现在入职后,在招聘前也比比皆是。


进华为做OD,为的就是这一块“技术”! 这是行业内广为人知的调侃。


在华为的招聘JD中,通过让你更加迷茫的描述,来捡漏迷茫的高材生们。OD,就是华为的外包,待遇与正编是有差异的。


很多北大..宁愿以OD外包身份加入华为,为的就是这块技术。 这样的招聘手段早已是行业公开的秘密,对于那些能力优秀、不明真相的985、211求职者屡试不爽。即便是到了现在,换成其他公司,依旧还有一定的疗效。


image.png


应届生往往是最迷茫的群体。他们刚开始工作,对行业和个人规划都不是很清晰。这时候,有人一通忽悠,让相对弱势的求职者放下防备,就会中了圈套。如果你不幸在人生职场开始,就碰到这样的手段,那注定是一个悲剧。


除非你认清了他们的谎言。但总有迷茫的人,上钩。


迷茫中的沉沦


千万别小看了迷茫。人在迷茫的时候,只会沉沦。


小宋是一个比较初级的程序员,工作在 二三线 城市。他的公司有个特点,那就是会经常出差。


短期出差是出去欢乐,长期出差就是超负荷工作。


很不幸,他被选中了。更不幸的是,项目周期非常长,小宋在外出差达到了3年。


我通常会对这些让人迷惑的行为感到好奇。偶尔了解,原来小宋是贪恋每天 200 块钱的出差费,到头来出差收入能顶上他的工资。


如果他实在是喜欢这出差的感觉,喜欢这出差费。大可以到一线城市,找一个同样出差的公司。这样的公司很多,待遇能让他翻上一倍不止。


但我并不能点明他,否则公司就再也没有这样便宜、听话的员工来驱使了。


再来一个案例。


前不久和几个同事聚餐,有一位小伙伴聊到未来工作的发展。


小 A 来自于北京,回到二线城市工作。小 B 一直在二线城市,打算到北上广闯一闯。


小A对小B推荐说:“是的要到北京发展一下,我朋友就在软通动力,钱拿的很多。要去就得去这些牛逼的公司。”


我突然认识到,脉脉上很多人的优越感并不是故意装的,有可能是刻在骨子里的认知。


这份迷茫,将一直持续到有一个像我这样打破他世界观的人出现。


但我思考再三,还是忍住了。小A是京归,很得公司看中,我没必要趟这趟浑水。


打破迷茫


但是,看到xjjdog这篇文章的朋友们。如果你感到迷茫并且仍然希望在当前公司工作。你可能需要思考以下几个问题:


你为什么感到迷茫?是因为你不确定自己在公司的角色和职责,还是因为你不确定自己的职业发展方向?


你是否有机会和资源来解决这些问题?可以尝试与上级或同事交流,寻求他们的帮助和建议,或者寻找外部的职业发展咨询师等专业人士的帮助。


你是否还对公司的使命和价值观感到认同?如果是的话,这将帮助你更加清晰地认识到自己在公司的作用和价值。


最终,如果你认为无法解决迷茫并且无法找到满意的职业发展机会,那么你可能需要开始考虑其他的工作选择。


是否与市场进行过横向对比,是否将所有的优缺点都罗列出来过、权衡过?


当你想明白了这些,决策就不是问题。


一直迷茫的过,会损失很多。清醒短期是痛苦的,但它会促成你对的选择。


作者:小姐姐味道
来源:juejin.cn/post/7207242529095811131
收起阅读 »

为什么要招聘有经验的人?

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第7篇文章,点击查看活动详情 周末带娃出去玩,回来的时候路过一家新开的水果店买东西,因为是新店,所以店里在搞促销活动。进去后发现一个很糟糕的问题。店员对收银系统不熟悉,排队的人要好几分钟才能结算一个,很多人...
继续阅读 »

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第7篇文章,点击查看活动详情


周末带娃出去玩,回来的时候路过一家新开的水果店买东西,因为是新店,所以店里在搞促销活动。进去后发现一个很糟糕的问题。店员对收银系统不熟悉,排队的人要好几分钟才能结算一个,很多人等着不耐烦就走了。现在客户买东西,在哪里买都是买,体验不好,留不住客户的。


这让我想到前年六月份,公司开第一家实体店的时候,也出现过类似情况。当初做的是让客户通过小程序线下扫码购买,优惠设计得很复杂,服务员虽然做过培训,但很多细节不清楚。当客户支付出现异常情况时,又来回沟通处理。这就让用户很不耐烦。最后虽然看起来店里人多热闹,但实际营收并不高。


这并不是特例,有经验的老板,在正式开店前会有一段时间的试营业,非常低调地开门。等员工都熟悉工作了,才会正式开业。


我们都知道招聘的时候,企业更喜欢招聘有经验的人。为啥?因为经验指的是你不仅知道一个东西,还做到过。你能解决某个问题,解决问题的方法才是经验。


如果你只是第一次做,就算培训过,那也不是经验,只能说你知道某件事。从知道到做到,中间还隔很远,越是复杂系统,越需要花更多时间在这个做到的过程上。就像我们做菜,你按照菜谱做,第一次也大概率做得也不会很好吃。


另外一个原因是,人往往容易高估自己的能力。我们在评估一个项目工作量时就很容易犯这个错误,你要是没经验,很容易把一个复杂需求看得很简单。导致工作量评估不足,这也是导致项目延期很重要的一个原因之一。


说到这里,相信你也知道招聘的背后是找一个能解决他们现有问题的人,这个解决问题的经验才是他们需要的。那如果我们想换工作,我想你应该也知道招聘的关键是”经验“,跟岗位相关的经验都是加分项。在进一步思考,人的时间都是一样的,把时间用哪里,把解决什么问题变成经验,这是我们可以思考的方向之一。比如,你想做一名项目经理。那项目管理经验就是你的加分项。十人的管理、百人的管理,管理的深度不一样,获得经验也是不一样的。


总结一下,今天主要想跟大家表达两个观点:


1、珍惜你的时间,用有限的时间去拥有更多跟工作相关的经验。


2、没有经验就不要太乐观地去做一件事情。反之,在做一件没有做过的事情前,应该找一个无利益关系且

作者:石云升
来源:juejin.cn/post/7141361769770909710
有经验的前辈咨询下。

收起阅读 »

数据大屏最简单适配方案

web
根据本文内容,开发了以下三个 npm 包,希望大家能用得到 @fit-screen/shared: 提供计算自适应比例相关内容的工具包 @fit-screen/vue:Vue 自适应组件 @fit-screen/react:React 自适应组件 如果本文...
继续阅读 »

根据本文内容,开发了以下三个 npm 包,希望大家能用得到



  1. @fit-screen/shared: 提供计算自适应比例相关内容的工具包

  2. @fit-screen/vue:Vue 自适应组件

  3. @fit-screen/react:React 自适应组件


如果本文对你有帮助,希望大佬能给个 star~



前言


最近公司有个大屏的项目,之前没咋接触过。


就在掘金上看了许多大佬各种方案,最常见的方案无外乎一下 3 种👇,优缺点呢也比较明显


方案实现方式优点缺点
vw, vh按照设计稿的尺寸,将px按比例计算转为vwvh1.可以动态计算图表的宽高,字体等,灵活性较高
2.当屏幕比例跟 ui 稿不一致时,不会出现两边留白情况
1.需要编写公共转换函数,为每个图表都单独做字体、间距、位移的适配,比较麻烦
scale通过 scale 属性,根据屏幕大小,对图表进行整体的等比缩放1.代码量少,适配简单
2.一次处理后不需要在各个图表中再去单独适配
1.因为是根据 ui 稿等比缩放,当大屏跟 ui 稿的比例不一样时,会出现周边留白情况
2.当缩放比例过大时候,字体和图片会有一点点失真.
3.当缩放比例过大时候,事件热区会偏移。
rem + vw vh1.获得 rem 的基准值
2.动态的计算html根元素的font-size
3.图表中通过 vw vh 动态计算字体、间距、位移等
1.布局的自适应代码量少,适配简单1.因为是根据 ui 稿等比缩放,当大屏跟 ui 稿的比例不一样时,会出现周边留白情况
2.图表需要单个做字体、间距、位移的适配

这 3 种方案中,最简单的也最容易抽离为下次使用的当属 scale 方案了。


它优点是:



  1. 代码量少,编写公共组件,套用即可,可以做到一次编写,任何地方可用,无需重复编写。

  2. 使用 flex grid 百分比 还有 position 定位或者完全按照设计稿的 px 单位进行布局,都可以,不需要考虑单位使用失误导致适配不完全。实现数据大屏在任何分辨率的电脑上均可安然运作。


至于说缺点:




  1. 比例不一样的时候,会存在留白,开发大屏基本上都是为对应分辨率专门开发,我觉得这个缺点可以基本忽略,因为我们可以将背景色设置为大屏的基础色,这样留白部分不是太大基本没影响啦,哈哈




  2. 关于失真失真 是在你设置的 分辨率比例屏幕分辨率比例 不同的情况下,依然采用 铺满全屏 出现 拉伸 的时候,才会出现,正常是不会出现的。



    电视看电影比例不对,不也会出现上下黑边吗,你设置拉伸,他也会失真,是一个道理





🚀 开发


让我们先来看下效果吧!👇



既然选择了 scale 方案,那么我们来看看它的原理,以及如何实现吧!


原理


scale 方案是通过 css 的 transform 的 scale 属性来进行一个 等比例缩放 来实现屏幕适配的,既然如此我们要知道一下几个前提:



  1. 设设计稿的 宽高比1,则在任意显示屏中,只要展示内容的容器的 宽高比 也是 1,则二者为 1:1 只要 等比缩放/放大 就可以做到完美展示并且没有任何白边。

  2. 如果设计稿的 宽高比1, 而展示容器 宽高比 不是 1 的时候,则存在两种情况。

    1. 宽高比大于 1,此时宽度过长,计算时基准值采用高度,计算出维持 1 宽高比的宽度。

    2. 宽高比小于 1,此时高度过长,计算时基准值采用宽度,计算出维持 1 宽高比的高度。




代码实现


有了以上前提,我们可以得出以下代码


const el = document.querySelector('#xxx')
// * 需保持的比例
const baseProportion = parseFloat((width / height).toFixed(5))
// * 当前屏幕宽高比
const currentRate = parseFloat((window.innerWidth / window.innerHeight).toFixed(5))

const scale = {
widthRatio: 1,
heightRatio: 1,
}

// 宽高比大,宽度过长
if(currentRate > baseProportion) {
// 求出维持比例需要的宽度,进行计算得出宽度对应比例
scale.widthRatio = parseFloat(((window.innerHeight * baseProportion) / baseWidth).toFixed(5))
// 得出高度对应比例
scale.heightRatio = parseFloat((window.innerHeight / baseHeight).toFixed(5))
}
// 宽高比小,高度过长
else {
// 求出维持比例需要的高度,进行计算得出高度对应比例
scale.heightRatio = parseFloat(((window.innerWidth / baseProportion) / baseHeight).toFixed(5))
// 得出宽度比例
scale.widthRatio = parseFloat((window.innerWidth / baseWidth).toFixed(5))
}

// 设置等比缩放或者放大
el.style.transform = `scale(${scale.widthRatio}, ${scale.heightRatio})`

OK,搞定了。


哇!这也太简单了吧。


好,为了下次一次编写到处使用,我们对它进行封装,然后集成到我们常用的框架中,作为通用组件


function useFitScreen(options) {
const {
// * 画布尺寸(px)
width = 1920,
height = 1080,
el
} = options

// * 默认缩放值
let scale = {
widthRatio: 1,
heightRatio: 1,
}

// * 需保持的比例
const baseProportion = parseFloat((width / height).toFixed(5))
const calcRate = () => {
if (el) {
// 当前比例
const currentRate = parseFloat((window.innerWidth / window.innerHeight).toFixed(5))
// 比例越大,则越宽,基准值采用高度,计算出宽度
// 反之,则越高,基准值采用宽度,计算出高度
scale = currentRate > baseProportion
? calcRateByHeight(width, height, baseProportion)
: calcRateByWidth(width, height, baseProportion)
}

el.style.transform = `scale(${scale.widthRatio}, ${scale.heightRatio})`
}

// * 改变窗口大小重新绘制
const resize = () => {
window.addEventListener('resize', calcRate)
}

// * 改变窗口大小重新绘制
const unResize = () => {
window.removeEventListener('resize', calcRate)
}

return {
calcRate,
resize,
unResize,
}
}

其实一个基本的共用方法已经写好了,但是我们实际情况中,有可能会出现奇怪比例的大屏。


例如:



  1. 超长屏,我们需要 x 轴滚动条。

  2. 超高屏,我们需要 y 轴滚动条。

  3. 还有一种情况,比如需要占满屏幕,不需要留白,适当拉伸失真也无所谓的情况呢。


所以,我们需要进行扩展这个方法,像 节流 节约性能,对上面是那种情况做适配等,文章篇幅有限,源码已经开源并且工具包已经上传了 npm 需要的可以去看源码或者下载使用



  • 工具包源码:使用文档在这里,希望大佬们给一个小小的 star~

  • 工具包NPM: 你可以通过 npm install @fit-screen/shared 下载使用


Vue logo 集成到 Vue


通过以上的的原理和工具包实现,接下来我们接入 Vue 将会变得非常简单了,只需要我们用 Vue 的 ref 将对应的 dom 元素提供给工具包,就可以实现啦~


不过在这个过程中我遇到的问题是,既然是一次编写,任意使用,我们需要集成 Vue2 和 Vue3,如何做呢?


说道这一点想必各位大佬也知道我要用什么了吧,那就是偶像 Anthony Fuvueuse 中使用的插件 vue-demi


好的,开发完毕之后,一样将它上传到 npm ,这样以后就可以直接下载使用了



大家也可以这样使用


npm install @fit-screen/vue @vue/composition-api
# or
yarn add @fit-screen/vue @vue/composition-api
# or
pnpm install @fit-screen/vue @vue/composition-api

当做全局组件使用


// In main.[jt]s
import { createApp } from 'vue'
import FitScreen from '@fit-screen/vue'
import App from './App.vue'

const app = createApp(App)
app.use(FitScreen)
app.mount('#app')

Use in any component


<template>
<FitScreen :width="1920" :height="1080" mode="fit">
<div>
<a href="https://vitejs.dev" target="_blank">
<img src="/vite.svg" class="logo" alt="Vite logo">
</a>
<a href="https://vuejs.org/" target="_blank">
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo">
</a>
</div>
<HelloWorld msg="Vite + Vue" />
</FitScreen>
</template>

在 SFC 中单独使用


<script setup>
import FitScreen from '@fit-screen/vue'
</script>

<template>
<FitScreen :width="1920" :height="1080" mode="fit">
<div>
<a href="https://vitejs.dev" target="_blank">
<img src="/vite.svg" class="logo" alt="Vite logo">
</a>
<a href="https://vuejs.org/" target="_blank">
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo">
</a>
</div>
<HelloWorld msg="Vite + Vue" />
</FitScreen>
</template>

react logo 集成到 React


集成到 React 也是完全没毛病,而且好像更简单,不存在 vue2 和 vue3 这样版本兼容问题



大佬们可以这样使用:


npm install @fit-screen/react
# or
yarn add @fit-screen/react
# or
pnpm install @fit-screen/react

import { useState } from 'react'
import FitScreen from '@fit-screen/react'

function App() {
const [count, setCount] = useState(0)

return (
<FitScreen width={1920} height={1080} mode="fit">
<div className="App">
<div>
<a href="https://vitejs.dev" target="_blank" rel="noreferrer">
<img src="/vite.svg" className="logo" alt="Vite logo" />
</a>
<a href="https://reactjs.org" target="_blank" rel="noreferrer">
React logo
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount(count => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</div>
</FitScreen>

)
}

export default App

结尾



  1. 通过工具包可以在无框架和任意前端框架中开发自己的组件,比如说 Svelte,我也做了一个 Svelte 的版本示例,可以去 示例仓库 中查看。

  2. 目前就开发了 Vue 和 React 版本的自适应方案,大家可以根据需要进行使用。


感谢大家的阅读,希望大家能用得上,并且给上 star~


作者:jpliu
来源:juejin.cn/post/7202598910337138748
收起阅读 »

Android必知必会-Stetho调试工具

一、背景 Stetho是 Facebook 出品的一个强大的 Android 调试工具,使用该工具你可以在 Chrome Developer Tools查看APP的布局, 网络请求(仅限使用Volle, okhttp的网络请求库), Sqlite, Pref...
继续阅读 »

一、背景



Stetho是 Facebook 出品的一个强大的 Android 调试工具,使用该工具你可以在 Chrome Developer Tools查看APP的布局, 网络请求(仅限使用Volle, okhttp的网络请求库), Sqlite, Preference, 一切都是可视化的操作,无须自己在去使用adb, 也不需要root你的设备



本人使用自己的Nubia Z9 Mini作为调试机,由于牵涉到Sqlite数据库,所以尝试了很多办法把它Root了,然而Root之后就无法正常升级系统。
今天得知一调试神器Stetho,无需Root就能查看数据库以及APP的布局(这一点没有Android Device Monitor使用方便,但是Android Device Monitor在Mac上总是莫名其妙出问题),使用起来很方便,大家可以尝试一下。


二、配置流程


1.引入主库


使用Gradle方式:


// Gradle dependency on Stetho 
dependencies {
compile 'com.facebook.stetho:stetho:1.3.1'
}

此外还支持Maven方式,这里不做介绍。


2.引入网络请求库


如果需要调试网络且你使用的网络请求库是Volle或者Okhttp,那么你才需要配置,否则跳过此步。
以下根据自己使用的网络请求库情况来导入相应的库:
1.使用okhttp 2.X


 dependencies { 
compile 'com.facebook.stetho:stetho-okhttp:1.3.1'
}

2.使用okhttp 3.X


dependencies { 
compile 'com.facebook.stetho:stetho-okhttp3:1.3.1'
}

3.使用HttpURLConnection


dependencies { 
compile 'com.facebook.stetho:stetho-urlconnection:1.3.1'
}

3.配置代码


配置Application


public class XXX extends Application {
public void onCreate() {
super.onCreate();
Stetho.initializeWithDefaults(this);
}
}

配置网络请求库:
OkHttp 2.2.x+ 或 3.x


//方案一
OkHttpClient client = new OkHttpClient();
client.networkInterceptors().add(new StethoInterceptor());

//方案二
new OkHttpClient.Builder()
.addNetworkInterceptor(new StethoInterceptor())
.build();

如果使用的是HttpURLConnection,请查阅相关文档。


4.使用


运行重新编译后的APP程序,保持手机与电脑的连接,然后打开Chrome浏览器,在地址栏里输入:chrome://inspect然后选择自己的设备下运行的APP进程名下的Inspect链接 即可进行调试。


三、遇到的问题


1.okhttp版本问题:


可能你还在使用okhttp 2.x的版本,在引入网络库的时候,你需要去查看一下Stetho当前版本使用的okhttp版本,避免在项目中使用多个不同版本的okhttp


PSokhttp2.x和3.x的引入方式略有不同,不可以直接修改版本号来导入:


//2.x
compile 'com.squareup.okhttp:okhttp:2.x.x'
//3.x
compile 'com.squareup.okhttp3:okhttp:3.x.x'

2.配置okhttp代码方案一报错:


//方案一
OkHttpClient client = new OkHttpClient();
client.networkInterceptors().add(new StethoInterceptor());

//方案二
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new StethoInterceptor())
.build();

我在使用方案一进行配置okhttp的时候,会报错:


 Caused by: java.lang.UnsupportedOperationException

不知道是不是兼容的问题,大家在使用的时候请注意。



Stetho官网






转载请注明出处,如果有什么建议或者问题可以随时联系我,共同探讨学习:



作者:cafeting
来源:juejin.cn/post/7202164243612860472

收起阅读 »

当我把ChatGPT机器人拉到微信群里,群友都玩疯了!!

大家好呀,我是飘渺! 最近ChatGPT可谓是火出天际,人工智能领域的股票最近都被这家伙带的一路狂飙,如果到现在你都没听过或使用过的话那你可能是有点out了。 今天的文章并不打算介绍ChatGPT,而是要带大家玩一件好玩的事。将ChatGpt聊天机器人拉到你微...
继续阅读 »

大家好呀,我是飘渺!


最近ChatGPT可谓是火出天际,人工智能领域的股票最近都被这家伙带的一路狂飙,如果到现在你都没听过或使用过的话那你可能是有点out了。


今天的文章并不打算介绍ChatGPT,而是要带大家玩一件好玩的事。将ChatGpt聊天机器人拉到你微信群里,这样正如群友所说,你就可以假装成ChatGpt背后那个无所不能的人了。


在微信群的效果如下:


image-20230208221022410


手把手教学,最新实测版,如果有效还请三连一下。


前提条件



  1. 一个可以科学上网的全局代理(推荐使用美国节点)

  2. 一个邮箱用于注册ChatGPT(Gmail,OutLook均可)

  3. 一个国外手机号用于接收验证码

  4. 一台安装好Docker环境的服务器

  5. 一个微信小号用于假装机器人


注册ChatGpt


1、在你设备上开启全局代理


建议使用美国节点,如果未启用全局代理,或者使用的节点不稳定,则有可能出现ChatGPT在当前地区不可用的报错提示。


2、登录 https://platform.openai.com/signup注册账号


填入准备好的点子邮箱地址后点击Continue


3、完善信息后来到手机验证页面


image-20230208222208084


先停在这个页面,接下来我们需要准备一个手机号用于接收验证码。


4、在sms-activate.org购买一个可以接受海外的手机号用于接受验证码


4.1)访问sms-activate.org,注册并登陆账号


4.2)登陆后在右上角切换成中文


image-20230208162659061


4.3)点击右上角用户图标,选择充值按钮


image-20230208222816662


这里通过支付宝进行充值,现在这个平台涨价了,最低付款金额为0.5美元,转化成人名币是3.55元。


4.4)购买OpenAI的服务


image-20230208162804417


选择OpenAI后进入国家服务列表页面,这里推荐大家购买印度尼西亚的服务,亲测印度的今天不能接受验证码了。


image-20230208162710493


4.5)将这个购买好的手机号复制并粘贴到步骤3的验证手机号码界面,点击Send code via SMS


4.6)留意sms-activate.org页面,很快就会收到一个验证码,将验证码填入ChatGPT页面的验证码输入框即可完成验证


image-20230208223611034


4.7)注册完成后进行登陆chat.openai.com,即可与OpenAI对话了


配置微信机器人


OpenAI 提供了接入的 API keys 的,只有在申请了这个 API keys 才能完成后续的接入动作,所以我们首先需要申请一个API KEY。


申请openopi


1、访问https://platform.openai.com/account/api-keys,进入后,点击 Create new secret key 就可以申请了。


image-20230208170456377


2、通过准备好的服务器部署(Docker)微信聊天机器人,可以本地部署也可以在云服务器上部署。


部署步骤如下:


# 1. 创建目录
mkdir -p /data/openai

cd /data/openai

# 2. 创建文件
touch config.json

# 3. 修改文件
vi config.json

{
 "api_key": "复制粘贴你申请的API keys", # openai的 账户创建的apikeys
 "auto_pass": false,         # 加好友时,是否自动通过
 "model": "text-davinci-003", # 模型名称,官网有很多模型
 "session_timeout": 90        # session保存时间 秒,其实就是上下文保留时间. 超过这个时间,就会自动清空上下文
}

# 4. 拉取镜像
docker run -dti --name wechat -v /data/openai/config.json:/app/config.json cynen/wechat

# 5. 查看日志
docker logs -f wechat

image-20230208173432041


查看日志时我们可以看到日志界面出现了一个二维码,此时用你的微信小号扫描登录,登录后你就可以向他提问题了,或者将小号拉到你的微信群里。


在微信群里只要at他就可以对他提问了。


怎么样,是不是很好玩呢?你也赶紧部署一个试试吧!


作者:飘渺Jam
来源:juejin.cn/post/7197995910566330425
收起阅读 »

ChatGPT 的出现对当下的互联网生态所具备革命意义

持续火爆的 ChatGPT 是基于 OpenAI 开发的 GPT-3 模型进行微调得到一款聊天机器人产品,当下大家应该都已经体验过了,对于技术从业者或者普通用户带来的震撼都是极大的,因为人工智能第一次拥有了类人的语言风格和情感色彩,这种前所未有的体验自然引爆了...
继续阅读 »

持续火爆的 ChatGPT 是基于 OpenAI 开发的 GPT-3 模型进行微调得到一款聊天机器人产品,当下大家应该都已经体验过了,对于技术从业者或者普通用户带来的震撼都是极大的,因为人工智能第一次拥有了类人的语言风格和情感色彩,这种前所未有的体验自然引爆了整个网络。


但热潮过后也引发了我的一些思考,对于大多数人而言,ChatGPT 似乎可以无缝衔接到任何场景,似乎人工智能的时代已经来临,但是从技术上说,ChatGPT 只是在 GPT-3 的基础上加入了一些更友好的生成风格,从某种角度看,ChatGPT 是一个会讨好你的,友善的,令人愉快的聊天对象。


如果你翻阅过 OpenAI 的文档,那么应该了解到当下的 GPT-3 模型拥有一些不可避免的硬伤,例如 4000 tokens 的上下文限制,对于通过 prompt 预训练来让 GPT-3 具备更复杂场景的处理能力就显得非常的不足,如果类比的话,好比早年的汇编程序编程的设备,拥有很小的内存空间,无法装下特别复杂的指令,只能通过复杂的软件架构来实现多设备之间的协作从而完成一个在当下我们看来非常简单的任务,但是 --->>>


AI 可以编程了!! AI 可以编程了!! AI 可以编程了!!


重要的事情说三遍,内存大小根本不是问题,作为技术从业者,我经历了早期的 pc 机时代到现在如此发达的互联网,小时候上网是 64kb 调制解调器,网页上只能看到一些文字,存储图片都很费劲,但是在那个年代你会因为互联网这些简陋的应用,简陋的网页而激动人心,因为你看到的是一个全新的时代。


事实上在我成为行业专家,从事工作之后的后几年已经渐渐没有了当初那种激动人心的体验,也一度失去了改变世界的梦想,上班拧螺丝,下班刷抖音已经成为很多人的日常,在技术圈里,新技术的发展越来越内卷,那些曾经热衷于讨论的新鲜玩意逐渐消失,大龄程序员们关注的是房子车子老婆孩子,还有那 35岁的失业红线,刚毕业的年轻程序员们则讨论哪家又裁员了,今年的 Package 又不行了。最近几年技术网红越来越多,而能讨论的技术空间则越来越少。


这一现象的本质是前几轮的技术革命周期已经临近尾声,技术生态和技术成长空间已经渐渐不足,但是我国每年毕业的理工科学生缺越来越多,这必然导致了我们所说的内卷,人才内耗现象的发生。


让我们回顾整个计算机信息革命几十年发展的历程,从最初的大型服务器计算机在企业内部逐渐普及,彼时以 IBM 这类企业服务器厂商为主的时代到比尔盖茨,乔布斯敏锐的发现了个人计算机普及的趋势,推动了个人计算机行业的集聚发展,从而有了丰富的广泛的软件行业,这里事实上是两个阶段



  1. 计算机在企业的普及,此时计算机属于专用设备,处理特定的专业场景,软件也主要基于这些专业场景开发,这一阶段的特点是软件开发的要求很高,因为编程很复杂。

  2. 计算机在个人的普及,计算机进入民用化时代,高级语言蓬勃发展,软件开发的门槛降低,规模化和标准化让经过教育和培训的学生或者成人自学都能掌握,从而大大推动了软件行业的发展,不过这一阶段的编程要求依然是比较高的,参考我国早年雷军等大佬的从业经历,但已经是非常大的一次进步了,而且那个时候的软件只能编写在软盘上,可能只有几百kb左右的空间,如果你曾经玩过软盘版本的金庸群侠传就知道我在说什么了。



事实上我已经很久没写文章了,技术内卷让我失去了分享的乐趣,完全没有任何让人兴奋的东西可以研究可以讨论,但是 ChatGPT 确实重新点燃了我对讨论,对分享的兴趣。



事实上互联网能够发展到今天这个规模,个人计算机的普及自然是功不可没,毕竟 IPV4 的设计者们都应该想不到有一天地址池还能枯竭,可见无论什么时代,再优秀的人也难以突破时代对想象力的限制。


当时间进入 90 年代,互联网技术突飞猛进,软件应用的形态从 C/S 结构向 B/S 结构演变,时至今日几乎大部分软件都需要和远程数据库进行通信,数据中心化催生了大数据,云计算,推动了 Web 的发展,如果说个人 PC 的发展是科技民主化,那么移动智能设备 IPhone 的出现则是让智能设备和人类个体进行了一个深度绑定,过去 PC 不能代表你,PC 只是你某个时间或者空间断面上的你,但是智能手机却几乎能够代表你,通过智能手机里的应用和数据分析几乎可以勾勒出你这个人的方方面面,因此在这个阶段大致也可以分为两个部分



  1. 基于 PC 的互联网普及,我们身份的某一个切面被连接到了网络上,推动了早期 web 的蓬勃发展

  2. 基于 智能手机的移动互联网,让我们的以近乎完整的数字身份连接网络


以上就是时至今日的我理解的互联网的发展历程,回顾这些则是为了展开下面关于 ChatGPT 的讨论。


从上面两个阶段发现就软件本身其实经历了两个不同层次的周期



  1. 第一个周期本地软件的编程周期,软盘,光盘,存储介质的变化,空间变大,程序变的复杂

  2. 第二个周期软件存储去介质化,通过互联网软件可以以任意大小进行分发和使用,代码的量级也达到了亿级,软件变得极其复杂。

  3. 第三个周期 --->>>


对于 ChatGPT 来说,我认为就是第三周期的正式开启,现在的软件很复杂也很巨大,例如 Facebook,或者淘宝,但是这些代码都是手工编辑的,其中一小部分可能是自动是生成的,如果把软件比作制造业,那么第一阶段的软件开发是作坊,非工业化的,而第二阶段则是工业化的,效率比较第一阶段有了极大的提升,但是对于巨大规模的问题依然束手无措



软件行业的巨大规模问题,例如千人千面,中心化软件的个性化,SaaS 的定制化,以目前的技术能力,收益几乎是无法覆盖成本。



由此可见软件行业继续按照现有的路径发展,本质上已经是无法解决这些问题了,无论是更智能的 IDE 还是更加抽象的编程语言,如果用这种思路去开发 ChatGPT 这样的软件那必然是海量的人力和海量的 Bug 以及难以预计的维护成本。


回到我之前提到的 AI 可以编程了,恰恰是这个问题的解。


把 ChatGPT 背后的 GPT-3 看做是一种可编程环境,那么你就不难理解我所说的第三周期的含义。



  1. 第三个周期,软件编程进一步抽象为对 AI Model 进行编程,利用高级语言对 AI 进行编程,利用 AI 的能力重新构建新一代的数字应用。


所以新一代的数字应用是什么呢?


我们现在所使用的 App 无论厂商口号喊的怎么想,要以用户为中心之类的,但其本质是以平台或者组织为中心构建的应用,简单理解这句话,那就是作为用户你的需求从未真正被这些应用所正视过,这也是我前面提到的现代软件在面临个性化上的严重不足,研发成本和收益的不匹配。


而基于 AI 为中心的下一代数字应用,其核心是利用 AI 具备的语义理解,情感分析,内容生成等强大的基础能力,能够真正做到以用户为中心,这种思路转换将完全颠覆我们对现在数字应用的理解。


我举一个非常简单的例子,比如我们用微信,你可以仔细回想你很多沟通上的需求微信能够满足你么?



  1. 时时刻刻的重要内容提醒,拟人化的不在线临时回复

  2. 群消息的过滤和检索,群消息讨论的观察,消息的分类

  3. 朋友圈消息的提炼,对重要人朋友圈的时刻关注



微信本质是一种异步沟通的产品机制,在设计上实时沟通仅限于你和沟通对象同时在线的情况



对消息的实时观察,过滤,检索,分类,整理让我们很容易错过许多非常重要的信息,对于个人而言可能损失了很多机会,又或者降低了学习的效率,其本质是因为基于移动互联网时代,我们虽然和设备做了深度绑定,但我们的时间是有限的,一个人不可能 24 小时都挂在网上,同时我们也没有能力分身亿万,对所有东西都保持关注,但是 AI 带来了一种可能性


未来,你可能拥有 N 个数字助手,这些数字助手都基于特定编程,能够帮你处理各种特定的事务,让你突破自身时间的限制,总结起来


第一个周期让我们有限的连接,第二个周期让我们完整的连接,第三个周期让我们几乎无限的连接。


这是一个新时代,一切都将被颠覆

作者:掘金泥石流
来源:juejin.cn/post/7199897954403418169
,而你做好准备了么?

收起阅读 »

ChatGPT保姆级注册教学

1.前言 大家好,最近一款ChatGPT的AI产品横空出世,他的强大的功能引起了广大网友的热议,并迅速席卷全球。 根据UBS发布的研究报告显示, ChatGPT在1月份的月活跃用户数已达1亿,成为史上用户数增长最快的消费者应用,相比之下, TikTok花了九...
继续阅读 »

1.前言


大家好,最近一款ChatGPT的AI产品横空出世,他的强大的功能引起了广大网友的热议,并迅速席卷全球。
image.png


根据UBS发布的研究报告显示, ChatGPT在1月份的月活跃用户数已达1亿,成为史上用户数增长最快的消费者应用,相比之下, TikTok花了九个月的时间月活跃用户数才破亿,1月份平均每天有超过1300万名独立访问者使用ChatGPT ,是去年12月份的两倍多。


image.png


那么这么一款现象级的产品,我可不允许大家还没用过,今天就详细出一版ChatGPT注册教程,也是后面我们接入ChatGPT必须要做的第一步。


2.注册准备


ChatGPT把国内给禁掉了,包括港子和湾子,所以如果注册的话就要代理到其他国家。不然浏览器会提示该地区政策禁止使用。注册需要使用邮箱和手机号,手机号也有区域限制,因此手机号和邮箱也需要国外的邮箱手机才能注册成功。我也给大家梳理了以下个人感觉比较好用的虚拟站点,提供使用:



3.注册流程


3.1.账号创建


打开神秘力量上网工具,要走全局代理,不然也会被定位到国内不能使用。之后打开ChatGPT官网chat.openai.com/auth/login


image.png


进入创建账号页面,这里需要输入邮箱,我们先进入temp-mail.org/zh/ 获取免费的临时邮箱


image.png


输入邮箱,点击继续


image.png


输入密码,点击继续


image.png


3.2.邮箱验证


接下来就开始提示邮箱验证了,系统会发送一封邮件到邮箱,我们需要打开刚才的邮箱点击验证邮件。


image.png


打开temp-mail.org/zh/,发现下面多了一封openai发送的邮件,点开邮件


image.png


点击校验邮箱地址


image.png


3.3.身份输入


姓名输入,这个可以随便写,之后点击继续


image.png


3.4.手机号验证


接下来开始进行手机号验证


image.png


进入,sms-man.com/cn地址,先充值1美元的额度,接下来选择对应的国家,然后选择服务,找到OpenAi/ChatGPT,点击购买短信。


image.png


之后下方会多出一个号码,这个就是用来接收短信的号码


image.png


将短信复制到手机号填写的地方,删掉区号,点击发送短信


image.png


之后在虚拟手机号平台点击接收短信,这时候就会收到短信的验证码如下:


image.png


在ChatGPT验证码页面输入信息,验证通过后跳转对话页面。


3.5.Hello ChatGPT


验证码输入完成之后进入如下对话页面,来吧,作为传统第一步当然是来一句,Hello ChatGPT啦,到此,注册完毕,接下来就可以使用ChatGPT啦,你学废了吗?


image.png



作者:王老狮
来源:juejin.cn/post/7206732474113458231
收起阅读 »

ChatGPT 来我司面试前端工程师,考考他闭包

近日,有个叫 ChatGPT 的同学来我司面试前端,考考他面试八股文。先来问问闭包。 第一问,说说 JS 闭包 追问,详细讲讲闭包 篇幅太长,转成文字,以下皆为 ChatGPT 回答 闭包是一个非常重要的 JavaScript 概念,也是相对难以理解的概...
继续阅读 »

近日,有个叫 ChatGPT 的同学来我司面试前端,考考他面试八股文。先来问问闭包。


第一问,说说 JS 闭包


image.png


追问,详细讲讲闭包



篇幅太长,转成文字,以下皆为 ChatGPT 回答



闭包是一个非常重要的 JavaScript 概念,也是相对难以理解的概念之一。为了更好地理解闭包,我们需要介绍一些相关的概念和知识。



  1. 作用域


作用域(scope)是指在程序中能够访问变量、函数的区域。JavaScript 中有全局作用域和函数作用域两种。


全局作用域是指在整个程序中都可以访问的变量,它们在程序开始时就被创建,在程序结束时才被销毁。


函数作用域是指在一个函数内部声明的变量,只能在函数内部访问,而在函数外面是不能访问的。当函数执行完毕后,函数内部的变量会被销毁。



  1. 词法作用域


词法作用域(lexical scope)是指在编写程序时,变量和函数的作用域是通过它们在代码中声明的位置来确定的。


这意味着,函数可以访问在其外部定义的变量和函数,但在其内部定义的变量和函数不能被外部访问。这是因为 JavaScript 引擎识别变量和函数的作用域是根据它们在代码中的位置来决定的,而不是根据它们被调用的位置。



  1. 闭包的定义和作用


闭包(closure)是指函数能够访问其词法作用域之外的变量,即使在函数被调用后仍然可以访问。


换句话说,当一个函数在其外部定义的变量的作用域外被调用时,它可以访问这些变量,并且可以在调用完成之后继续访问这些变量。这就是闭包的作用。


举个例子,假设我们有一个函数 outer,它内部有一个变量 x,还返回了一个函数 inner,这个函数也访问了 x。那么,当我们调用 outer 函数并得到 inner 函数后,inner 函数就形成了一个闭包,它可以继续访问 outer 函数内部的变量 x


function outer() {
let x = 1;
function inner() {
console.log(x);
}
return inner;
}

let closure = outer();
closure(); // 1


在上面的例子中,我们通过调用 outer 函数得到了一个函数 closure,它可以访问 outer 函数内部的变量 x。在调用 closure 函数时,它会输出变量 x 的值。这就是闭包的作用。



  1. 闭包的用途和注意事项


闭包可以用来创建一些类似于私有变量和方法的功能,以及实现一些高阶函数,如柯里化等。


举例来说,我们可以使用闭包来实现一个计数器:


function counter() {
let count = 0;
return function() {
count++;
console.log(count);
};
}

let myCounter = counter();
myCounter(); // 1
myCounter(); // 2
myCounter(); // 3


在上面的例子中,counter 函数返回了一个函数,这个函数可以访问 counter 函数内部的变量 count,并且会在每次调用时将 count 加一,并输出加一后的结果。


需要注意的是,过多的闭包使用可能会导致性能问题和内存泄漏的风险,所以应该谨慎使用。特别是在循环语句中,不能滥用闭包。


for (var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i); // 输出的都是 10
}, 1000);
}

在上面的例子中,我们在循环中使用了闭包,希望在每次循环时输出当前的计数器。但实际上,这个程序的输出结果是十个 10,因为在循环中创建的闭包都是共享同一个变量 i,而这个变量在循环结束时已经变成了 10。这就是闭包的一个陷阱:不能在循环中共享变量,否则会导致意外的结果。


那么如何解决在循环中使用闭包的陷阱


要解决在循环中使用闭包的陷阱,我们需要创建一个独立的变量。在 ES6 中,我们可以使用 let 关键字来声明一个块级作用域的变量,每次循环都可以创建一个新的变量,从而避免在闭包中共享同一个变量。


例如,在下面的例子中,我们可以通过在循环中使用 let 关键字来解决闭包的陷阱:


for (let i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i); // 输出 0 到 9
}, 1000);
}

在上面的例子中,我们使用 let 关键字来声明变量 i,这样每次循环都会创建一个新的变量,并且每个闭包都可以访问自己私有的变量。因此,输出的结果会是 0 到 9。


如果使用 ES5 或更低版本的 JavaScript,我们可以使用一个立即执行函数表达式(IIFE)来创建一个块级作用域。例如:


for (var i = 0; i < 10; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 输出 0 到 9
}, 1000);
})(i);
}

在上面的例子中,我们创建了一个即时函数 function(j) { ... }(i),这个函数会立即执行并创建一个新的作用域,把变量 i 的值传递给参数 j,从而避免在闭包中共享同一个变量。


let 的块级作用域讲讲


ES6 中引入了 let 关键字,它的一个重要特性就是可以创建块级作用域。这意味着,变量在块级作用域中声明,只能在该作用域内部访问,离开作用域后就会被销毁。


块级作用域是指用花括号 {} 包围的代码块,例如函数、条件语句、循环语句等。


举个例子,在下面的代码中,我们使用 let 关键字来声明一个块级作用域的变量 x,它只能在块级作用域中访问。


function myFunction() {
if (true) {
let x = 10;
console.log(x); // 输出 10
}
console.log(x); // 报错:ReferenceError: x is not defined
}

在上面的代码中,变量 x 只能在 if 语句的代码块中访问,在代码块外部访问时会报错。


需要注意的是,块级作用域只适用于 letconst 关键字,而不适用于 var 关键字。


例如,在下面的代码中,我们使用 var 关键字来声明变量 x,它虽然在函数中声明,但是可以在函数外部访问。


function myFunction() {
if (true) {
var x = 10;
console.log(x); // 输出 10
}
console.log(x); // 输出 10
}

在上面的代码中,变量 x 虽然在 if 语句的代码块中声明,但是可以在函数外部访问。这是因为 var 关键字具有提升(hoisting)的特性,即变量会被提升到函数的顶部声明,所以可以在整个函数中访问。但是,它的值只有在实际声明之后才会被赋值。


最后


好了,今天先到这。回去等通知吧。




如果你觉得内容对你有帮助,欢迎「转发点赞收藏


你的鼓励是我最大的支持,猛戳“关注”,后续文章更精彩。


作者:李瑞丰_liruifengv
来源:juejin.cn/post/7206217352185184293
收起阅读 »

ChatGPT:吃喝嫖赌,哪个最伤身体?

ChatGPT:吃喝嫖赌,哪个最伤身体?吃喝嫖赌 哪个最伤身体?吃喝嫖赌这四个行为都可能对身体造成不良影响,但其影响程度可能因人而异,也取决于具体的行为情况和频率。然而,从普遍的角度来看,其中嫖和赌相对而言更容易带来身体上和心理上的负面影响。嫖娼:嫖娼可能会导...
继续阅读 »

ChatGPT:吃喝嫖赌,哪个最伤身体?



吃喝嫖赌 哪个最伤身体?

吃喝嫖赌这四个行为都可能对身体造成不良影响,但其影响程度可能因人而异,也取决于具体的行为情况和频率。然而,从普遍的角度来看,其中嫖和赌相对而言更容易带来身体上和心理上的负面影响。

  1. 嫖娼:嫖娼可能会导致性传播疾病(STDs)的传播和其他感染,如艾滋病等。嫖娼者也可能面临性侵害、暴力、精神问题等方面的风险。

  2. 赌博:赌博可能会导致赌徒出现赌瘾、债务累积、家庭破裂等问题,还可能导致抑郁症、焦虑症等心理问题。

  3. 喝酒:适量饮酒可能对身体并没有太大的伤害,但长期和过量饮酒可能导致多种健康问题,如肝损伤、胰腺炎、脑损伤等。

  4. 吃药:吸毒会对身体造成严重伤害,不仅可能导致健康问题,如肺病、心血管疾病等,还可能对神经系统和认知功能造成损害。

总的来说,以上这四种行为都是有害的,应该尽量避免。

继续

除了上述四种行为之外,还有其他一些行为也可能对身体造成不良影响,例如:

  1. 吸烟:吸烟是导致癌症、心血管疾病和呼吸系统疾病的主要原因之一,同时也会损害皮肤、牙齿和骨骼等多个方面的健康。

  2. 不健康的饮食:过量食用高热量、高脂肪和高糖的食物可能导致肥胖、糖尿病、心血管疾病等健康问题。

  3. 缺乏运动:缺乏运动会增加肥胖、心血管疾病、骨质疏松等疾病的风险,并可能影响心理健康。

  4. 缺乏睡眠:长期睡眠不足可能导致身体疲劳、免疫力下降、抑郁症等健康问题。

综上所述,以上这些行为都可能对身体造成伤害,人们应该注意健康的生活方式,避免不健康的行为,保护自己的身体健康。


来源:m.toutiao.com/is/SUJaT7K/

收起阅读 »

手把手教你使用 Python 调用 ChatGPT-3.5-API

大家好,我是老表 前天 OpenAI 开放了两个新模型的api接口,专门为聊天而生的 gpt-3.5-turbo 和 gpt-3.5-turbo-0301。 ChatGPT is powered by gpt-3.5-turbo, OpenAI’s mos...
继续阅读 »

大家好,我是老表


前天 OpenAI 开放了两个新模型的api接口,专门为聊天而生的 gpt-3.5-turbo 和 gpt-3.5-turbo-0301。




ChatGPT is powered by gpt-3.5-turbo, OpenAI’s most advanced language model.



从上面这句话,我们可以知道现在 chat.openai.com 官网就是由 gpt-3.5-turbo 模型提供的服务,现在官方公开了这一模型的调用接口,这使得我们这些普通开发者也能直接在自己的应用/服务中使用这个狂揽亿万用户的模型。


接下来将和大家介绍如何利用 Python 快速玩转 gpt-3.5-turbo。


本文所有代码已开源,持续更新中:XksA-me/ChatGPT-3.5-AP


先跑起来,再理解


首先你需要有一个 openai 账号,如何注册我就不多说了,网上教程很多,而且很详细,如果有问题可以加我微信:pythonbrief,添加通过后请直接描述你的问题+问题截图


访问下面页面,登录 openai 账号后,创建一个 api keys。


# api keys 创建页面
https://platform.openai.com/account/api-keys

接下来很简单了,安装 openai 官方的 Python SDK,这里需要注意的是得安装最新版本 openai,官方推荐的是 0.27.0 版本。


pip install openai==0.27.0

直接上请求代码:


import openai
import json

# 目前需要设置代理才可以访问 api
os.environ["HTTP_PROXY"] = "自己的代理地址"
os.environ["HTTPS_PROXY"] = "自己的代理地址"


def get_api_key():
# 可以自己根据自己实际情况实现
# 以我为例子,我是存在一个 openai_key 文件里,json 格式
'''
{"api": "你的 api keys"}
'''

openai_key_file = '../envs/openai_key'
with open(openai_key_file, 'r', encoding='utf-8') as f:
openai_key = json.loads(f.read())
return openai_key['api']

openai.api_key = get_api_key()

q = "用python实现:提示手动输入3个不同的3位数区间,输入结束后计算这3个区间的交集,并输出结果区间"
rsp = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "一个有10年Python开发经验的资深算法工程师"},
{"role": "user", "content": q}
]
)

代码解析:




  • get_api_key() 函数是我自己写的一个从文件读取 api keys 的方法,我是存在一个 openai_key 文件里,json 格式,你可以改成你自己的获取方法,甚至可以直接写到代码里(虽然不推荐,但确实最简单)。




  • q 是请求的问题




  • rsp 是发送请求后返回结果




  • openai.ChatCompletion.create 中参数





    • model 是使用的模型名称,是一个字符串,用最新模型直接设置成gpt-3.5-turbo 即可





    • messages 是请求的文本内容,是一个列表,列表里每个元素类型是字典,具体含义如下表:




  • 程序运行返回内容,从响应回复内容我们可以看到,回复内容是一个 json 字符串,

    我们可以通过以下方法直接获取相关信息:








    • 返回消息内容




rsp.get("choices")[0]["message"]["content"]




    • 角色




rsp.get("choices")[0]["message"]["role"]




    • 问题+回答总长度




rsp.get("usage")["total_tokens"]

其他信息也可以通过类似方法获取。



  • 测试 ChatGPT 回答代码运行情况,可以看出代码逻辑和运行都没啥问题,注释也到位。



实现多轮对话


如何实现多轮对话?


gpt-3.5-turbo 模型调用方法 openai.ChatCompletion.create 里传入的 message 是一个列表,列表里每个元素是字典,包含了角色和内容,我们只需将每轮对话都存储起来,然后每次提问都带上之前的问题和回答即可。




  • 效果图





    • 可以看到,我首先问了“1+1=几”,然后问“为什么是这样”,ChatGPT 会根据前面的提问将新问题识别为“为什么1+1=2”。





    • 后面继续问水仙花数有哪些,再问“如何写个python程序来识别这些数”,ChatGPT 同样会根据前面的提问将新问题识别为“如何写个python程序来识别这些水仙花数”,并给出对应解答。




  • 实现代码




import openai
import json
import os

os.environ["HTTP_PROXY"] = "http://127.0.0.1:7890"
os.environ["HTTPS_PROXY"] = "http://127.0.0.1:7890"

# 获取 api
def get_api_key():
# 可以自己根据自己实际情况实现
# 以我为例子,我是存在一个 openai_key 文件里,json 格式
'''
{"api": "你的 api keys"}
'
''
openai_key_file = '../envs/openai_key'
with open(openai_key_file, 'r', encoding='utf-8') as f:
openai_key = json.loads(f.read())
return openai_key['api']
openai.api_key = get_api_key()



class ChatGPT:
def __init__(self, user):
self.user = user
self.messages = [{"role": "system", "content": "一个有10年Python开发经验的资深算法工程师"}]
self.filename="./user_messages.json"

def ask_gpt(self):
# q = "用python实现:提示手动输入3个不同的3位数区间,输入结束后计算这3个区间的交集,并输出结果区间"
rsp = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=self.messages
)
return rsp.get("choices")[0]["message"]["content"]


def writeTojson(self):
try:
# 判断文件是否存在
if not os.path.exists(self.filename):
with open(self.filename, "w") as f:
# 创建文件
pass
# 读取
with open(self.filename, 'r', encoding='utf-8') as f:
content = f.read()
msgs = json.loads(content) if len(content) > 0 else {}
# 追加
msgs.update({self.user : self.messages})
# 写入
with open(self.filename, 'w', encoding='utf-8') as f:
json.dump(msgs, f)
except Exception as e:
print(f"错误代码:{e}")


def main():
user = input("请输入用户名称: ")
chat = ChatGPT(user)

# 循环
while 1:
# 限制对话次数
if len(chat.messages) >= 11:
print("******************************")
print("*********强制重置对话**********")
print("******************************")
# 写入之前信息
chat.writeTojson()
user = input("请输入用户名称: ")
chat = ChatGPT(user)

# 提问
q = input(f"【{chat.user}】")

# 逻辑判断
if q == "0":
print("*********退出程序**********")
# 写入之前信息
chat.writeTojson()
break
elif q == "1":
print("**************************")
print("*********重置对话**********")
print("**************************")
# 写入之前信息
chat.writeTojson()
user = input("请输入用户名称: ")
chat = ChatGPT(user)
continue

# 提问-回答-记录
chat.messages.append({"role": "user", "content": q})
answer = chat.ask_gpt()
print(f"【ChatGPT】{answer}")
chat.messages.append({"role": "assistant", "content": answer})


if __name__ == '__main__':
main()

代码解析:




  • ChatGPT 类,包含三个函数:





    • __init__初始化函数,初始化了三个个实例变量,user、messages、filename(当前用户、消息列表、存储记录的文件路径)。





    • ask_gpt函数,将当前用户所有历史消息+最新提问发送给 gpt-3.5-turbo ,并返回响应结果。





    • writeTojson函数,结束/重置用户时记录当前用户之前的访问数据。




  • main函数,程序入口函数,用户输入用户名后进入与 ChatGPT 的循环对话中,输入 0 退出程序,输入 1 重置用户,退出和重置都会将当前用户之前访问数据记录搭配 json 文件中。




  • 由于 gpt-3.5-turbo 单次请求最大 token 数为:4096,所以代码里限制了下对话次数。




更多拓展



  • 你可以写个函数,从 json 文件读取历史用户访问记录,然后每次访问可以选用户。

  • 你可以写个 web 服务,使用 session 或者数据库支持多用户同时登录,同时访问。

  • 你可以基于之前分享的钉钉机器人项目,将 gpt-3.5-turbo 接入钉钉机器人。


你还可以上 Github 搜索更多 ChatGPT 相关项目,或者其他有意思的项目学习练手,欢迎学习交流。


我创建了个 ChatGPT 应用交流群,如果你感兴趣可以扫下方二维码添加我微信申请加入。


项目已开源,持续更新中:XksA-me/ChatGPT-3.5-AP


作者:老表
来源:juejin.cn/post/7206511128277024823
收起阅读 »

这一年,我终于成为了一名真正的前端

前言 时间很快,2022年一整年好像都很忙,也终于从学生过渡到了一名社会人 2022上半年,大四,独自一人去杭州实习,孤独,但在杭州实习的那几个月,是我成长最快的几个月。 2022下半年,毕业,拒绝掉转正offer,拒绝掉对于应届生来说可观的薪资。回成都找了一...
继续阅读 »

前言


时间很快,2022年一整年好像都很忙,也终于从学生过渡到了一名社会人


2022上半年,大四,独自一人去杭州实习,孤独,但在杭州实习的那几个月,是我成长最快的几个月。


2022下半年,毕业,拒绝掉转正offer,拒绝掉对于应届生来说可观的薪资。回成都找了一份离家近,但是薪资少一半的工作,理想中就是钱少事少,下班后能开启新生活的工作,但事实真的如此吗?好多同学、长辈问过我后不后悔,当然后悔了,肠子都悔青了,属于是亲身体会了什么叫卷,什么叫钱少事情多压力大,再不涨薪已经准备提桶跑路了。我说:不后悔,毕竟成长了。


打工人的眼泪


见过凌晨五点的成都吗,我见过,那时候我刚下班。但是第二个月看着我四位数的工资的时候,我还是没忍住骂了一句:wtf?我为什么不去送外卖?


image.png

我真的不想把一个原本完美的毕业人设硬生生的过成了一个需要从低薪摸爬滚打上去的人设,但事实是,我好像在第二条路上越走越远。看看现在的自己,确实是有点奋斗逼(贬义)的模样(伤感)。


不可忽略的成长


不可否认的是,这一年,我通过自己的努力从一个前端小白变成了一个可以独当一面的初级前端工程师。


因为大学的专业是数字媒体技术,大一大二学完计算机科班相关知识后,大三主攻的方向有三个:J2EE,游戏开发,动画。


在没经历过秋招的摧残之前,我还沉迷于游戏开发,3d建模,剪视频,做动画。秋招的时候信心满满的投简历,但悲惨的结果就是秋招0 offer(真不怪专业,怪我菜)。看到隔壁软件工程的同学秋招收获满满,想到自己做游戏开发也接触过脚本语言,于是一个从来没接触过web开发的焦虑的大四应届生果断入坑了web前端。


回顾我的2022,大概是这样的:




  • 1月 在系统学习了两个月前端基础知识(b站,红宝书,付费课程)的基础上,狂刷面试题。




  • 2月 继续狂刷面试题,自学vue3框架,做了一个很简单的后台管理系统(vue3+axios+element plus)。凭借这个项目找到了我的第一份正式的实习




  • 3月-5月 独自前往杭州实习,开始接触企业级项目,学会了git操作,css预编译sass,响应式布局,学会去封装组件,学会用node.js(egg.js框架)。与此同时,利用我的所有空闲时间去做毕业设计(原生微信小程序+mysql+nodejs+vue3后台管理系统)那段时间每天只能睡4-5个小时,睁开眼睛就要开始敲代码,但不得不说,这是我成长最最最快的三个月。




  • 6月 回到成都远程办公,完成我的毕业论文,顺利答辩,顺利毕业,转正,离职,毕业旅行。




  • 7月 毕业旅行完回成都找工作,因为7月只能参加社招,社招跟校招相比薪资待遇没那么好,但竞争不大。凭借之前的实习经验和看得过去的毕设一周拿到了三份offer(所以2023届的学弟学妹真的不要焦虑!),7月中旬入职,成为一名正式的前端开发工程师。




  • 8月 第一次接触大屏开发,因为项目压力很大,自己也有很多不熟悉的东西,经常加班到凌晨。(抗压能力++




  • 9月 第一次独立搭建项目,这段时间成长也很快。从0搭建一个项目与在一个大项目里面写功能写组件的差别很大!要考虑的东西非常多,从目录结构到变量命名都需要注意。这段时间我开始学习工程化,架构搭建的知识。了解vite的一些配置,开始注意一些代码规范(eslint,prettier),提交规范(commitlint)。也开始尝试使用TypeScript (独立开发能力++




  • 10月 可视化开发,开始熟悉echarts相关知识,转正。(新技能++




  • 11月 普通中后台项目的开发,之前的项目维护。与此同时,公司开发流程逐渐规范,对代码质量也有了一些要求,开始更注重代码的规范。公司有了技术分享会,前端后端一起学习分享,学到了一些工具的使用,大多都是在学校学不到的东西,收获很大,工作效率有了一定程度的提升。(代码整洁度++ 新技能++




  • 12月 继续之前项目的维护,新的项目开始使用公司更规范更统一的脚手架搭建。这个时候了解了一些自动化ci部署,github action的使用。同时开始总结知识,写了几篇质量不高的博客。并开始尝试研究Git flow




以上就是我这一年,从一个前端小白进化成一名初初初初级前端的过程,过程中很感谢大佬们正确的引导,虽然一直都是项目驱动成长,没有构建出完整的知识体系,但也算是入行了吧。可以给想要成为前端er的学弟学妹们一点点参考


2022年值得记录的瞬间


来杭州的第一天,感叹阿里人周末也要加班。


image.png

第一次做自我介绍


image.png

通宵肝毕设的日子


image.png

成为一名合格的铲屎官,小猫名字叫缓存,现在已经八斤啦


image.png

回成都后加班成为了日常便饭


image.png

2022给我带来了很多新奇的体验,浅浅分享一小部分最最值得纪念的瞬间。


关于2023的期望


立一点关于2023的flag:



  • 需要更加熟练已有的技能

  • 根据前端roadmap点亮更多技能。

  • 学习框架原理、库、网络、性能、浏览器、协议、工程化、node相关知识,完善知识体系,不需要全部深入,但要尽可能了解,提升知识广度。

  • 懂业务,深入业务,能解决业务痛点,更好的实现自己的价值

  • 学会做饭

  • 找个对象


最后最后:



希望自己:严于律己,减少焦虑


希望公司:减少加班,工资翻番



作者:一只囡狗
来源:juejin.cn/post/7187310000333652005
收起阅读 »

写给想转行学IT的朋友们的话

最近有很多朋友咨询我关于想转行学IT的问题,我想结合自身的经历和思考和各位聊几句,本文带有比较强的主观色彩,因此文中观点仅供参考,如有不当之处,敬请海涵。 笔者19年本科毕业于一所中流211,毕业之后一直在做Java开发,我并没有进大厂,也没有年薪百万,就是芸...
继续阅读 »

最近有很多朋友咨询我关于想转行学IT的问题,我想结合自身的经历和思考和各位聊几句,本文带有比较强的主观色彩,因此文中观点仅供参考,如有不当之处,敬请海涵。


笔者19年本科毕业于一所中流211,毕业之后一直在做Java开发,我并没有进大厂,也没有年薪百万,就是芸芸众生中普通的那一个。现在回顾当时入行的经历,基本是摸着石头过河,因此在有了一些经验之后,就想给和我当时一样处境的朋友一点建议,希望能对你有所启发。


很多问题其实都要结合个人的实际情况来看,每个人的知识、能力、经历都各不相同,所以无论做出任何决定,都需要结合自身的情况。


是否有必要参加培训机构?


这个问题的答案是因人而异的,最主要的点在于你是否有足够的恒心和自制力。


现在互联网上的学习资料已经非常多了,足够支撑你从零学到能够入行的过程。如果你的自制力比较好,那么你就可以尝试自学,不过自学的过程是孤独的,也是难熬的;如果你的自制力一般,无法在不是“学校”那么环境下进行学习,那就可以考虑培训机构。


需要特别强调的是,不能以为进了培训机构就以为一定能够就业了,说到底,学习这件事情,还是要靠你自己,别人是没有办法把知识灌进你的脑袋里的。培训机构也有一些无法忽视的问题,当你毕业之后,大概率机构会给你伪造一份简历,本来学习的时间就并不是很长,你很快可能就会发现,培训机构里面学的比较浅,还不足以应付面试官的八股文,这时候你可能就会想先入行再说,然后进了一家外包公司,然后开始混日子,这也是笔者见过的最多的案例。


参加培训机构最大的问题是简历,培训机构给大家做的项目都是极其雷同的,也可能会在培训机构之后,让你自己编项目经验,有经验的面试官很容易就能判断出,你是真的参与了项目开发,还是只是包装的。另外你可能还会碰到,很多公司需要上一家公司的离职证明和银行流水,甚至需要你个税APP报税界面的录屏等等问题,这些在求职的时候会碰到的现实问题,培训机构并不会告诉你。


总而言之,转行是一件对你的人生来说,是一件极其重要的事情,要反复思虑,不要脑子一热,听身边的朋友说,那个谁谁谁培训了几个月就月入过万,然后就稀里糊涂花了几万参加了培训机构,结果培训结束真正找工作的时候又发现困难重重,现实往往比想象的残酷。


高中毕业可以从事IT行业吗?


真相是,现在如果要从事IT行业,大专及以上学历是基本要求,不排除个人大专以下学历也找到了很好的IT工作,但是在当下这么“卷”的环境下,就是幸存者偏差了。大专的学历我也非常建议你先提升学历,有一个本科学历。


互联网爆发的红利期已经过去了,在没有新的风口出现的情况下,互联网的业务和从业人员都已经趋于饱和。换句话说,公司也不再会盲目进行扩张,进而产生大量的劳动力缺口,这也是目前为什么互联网公司会越来越在乎学历的原因之一。可以预见,进入这个行业的门槛会越来越高。


前端、后端还是测试?


其实从某种意义来说,选择岗位就是选择某种编程语言,选择编程语言也是在选择岗位,这个问题,你也可以结合下文编程语言之间的对比,找到自己合适的岗位。大体而言,测试、前端、后端,这三者的难度是依次递增的(对大部分人而言)。


如果你没有科班背景,但是又想从事IT行业,那么你可以考虑从事测试、运维工作甚至产品、运营的工作,其实学会写代码并不是唯一的选择。


如果你有一定的基础,但是逻辑思维又不是那么强,那么我建议你可以考虑前端,女生大部分学习编程都会选择前端,前端由于不需要考虑架构、性能(大部分场景),因此难度会小一些,对初学者更加友好。不过,前端经过这些年的发展,知识体系俨然已经非常庞大,后期也需要学习很多的东西。


如果你有一定的基础,且逻辑思维还可以,那么我建议你可以考虑后端,如果你不知道自己的思维能力怎么样,可以学一些Java或者JavaScript语言基础,看看自己是不是能够比较轻松的学会。


Java、Python、C++还是JavaScript?


编程语言只是工具,并没有优劣之分,我们需要在不同的场景选择合适的编程语言。下面我将对常见的几种编程语言做浅要的分析,希望能帮助你找到最适合你的编程语言。


随着时代发展,有了越来越多新的优秀的编程语言,比如Rust、kotlin、Dart等等,它们都在努力的甩掉前面编程语言的历史包袱,但是截止目前,相关的岗位的数量还不是很多,与我下面提到的编程语言相比,它们更适合作为你的第二门编程语言。


Java


如果你已经决定了要从事软件开发行业,但是还不清楚,要选择哪一门编程语言,那我推荐你可以学习Java,Java这门语言本身虽然并不优秀,截止目前,Java已经走过了20个年头,有着非常丰富的生态,web端,它有JSP、Servlet,移动端,它有Android,服务器端有SpringBoot,桌面端它有JavaFX,也有非常优秀的网络通信框架,比如Netty,甚至它也做出过操作系统(塞班系统),可以说,Java虽然很“烂”,但是它真的几乎无所不能。


不可否认Java的成功,但是Java也存在一些隐患,比如在Oracle收购了Sun公司之后,在Oracle不断地花式作妖下,Java的未来似乎也有些扑朔迷离。除此之外,Java在引以为傲的领域也逐渐有了一些竞争对手,比如服务端有go语言、nodejs,Android的开发官方首选语言已经变成kotlin,JSP的市场基本上已经被Vue、React等SPA框架所替代,但是Java却并没有开拓出自己新的应用场景,虽然笔者认为,基于VM的语言并不是消失,但是Java是能否一直守住自己的王座,还是要打一个问号的。


Java目前最多的就业方向就是服务端开发,如果你学习了Java,那么大概率会做服务端开发,短期内,服务端开发Java还是很难被其他语言替代。强类型的语言加上对并发编程的支持,让Java非常适合构建大型的服务端应用,这也是Java最深耕的方向,学会Java服务端的开发,也比较方便向大数据或数据分析岗位进行转型。


最后我想说,Java的岗位在我提到的编程语言里面也是最多的,这也是我推荐你学习它的一个重要原因,很多编程语言,虽然看起来很热门但是,但是当你真正学完去找工作的时候,却又发现岗位少的可怜,没错,说的就是你,Python。


Python


Python这门编程语言,“胶水语言”的特性让它看起来也几乎无所不能,虽然这种能力很有可能来自其他的语言,Python只是作为客户端,调用其他语言的类库。


不够“底层”其实并不是什么缺点,学习Python最大的问题是,是对学历有要求,Python应用最广泛的领域,大部分都对学历有要求,虽然你可能看过很多投放Python的广告,自动化办公、爬虫等等,但是这些特性在企业中的岗位是比较少的,而Python擅长的算法、人工智能、深度学习等领域都是需要研究生学历的。


我们总结一下,如果你有研究生学历,那么学习Python是一个不错的选择,不过我也见过有的研究生朋友学完Python之后发现岗位很少,又被迫转Java的案例。


C++


C++相比于其他的编程语言,其入门的门槛的会更高,花费在学习语言本身的时间的也会很多。C++在经历过这么多年的迭代之后,语言的特性非常多,会让初学者有一种眼花缭乱的特性,实现同一个功能,可能会有很多种写法。不可否认,C++是一门优秀的编程语言,但是高昂的学习成本也让人望而却步,如果你不是科班出身,只是想学一门技术进入互联网行业,那C++并不是一个很好的选择。


C++常见的就业方向有以下几种:服务器端开发、游戏、QT、嵌入式、人工智能。可以看出,C++最适合的还是那些对性能有要求的场景。原来很大一部分C++程序员都是做QT开发,但是C++在桌面端的市场已经出现了越来越多的竞争者,比如Hybrid 技术、React Native、Weex、Flutter,这些技术让原来web端的程序员或者移动端的程序员也能写出跨平台的应用,并且这些技术保留了他们原本的开发习惯,这对QT的打击,无疑是毁灭性的。


那么C++是不是已经过时,或者即将被淘汰呢?其实不然,C++只是让出了一些自己不是那么擅长的领域,但是在底层应用的开发,C++丰富的生态和优越的性能还是首选。如果你有志在这些领域发展,那么C++将是你很好的选择。


随着越来越多的人对核心自研技术的重视,这种比较偏底层的岗位,会越来越多,最近几年,国产操作系统的发展就是一个很好的例子,在政策的扶持下,各家公司几乎都在号称自研操作系统,可以预料,在不远的将来,这些核心的系统的研发,都需要大量C/C++语言的人才。


JavaScript


作为一门十天就被开发出来的编程语言,JavaScript身上的历史包袱也有很多,但这并不妨碍它的伟大,JavaScript在我们的日常生活中,几乎无处不在。


JavaScript也是一门易学难精的语言,虽然上手比较容易,但是后期也需要很多的时间去理解和巩固。JavaScript是前端程序员最重要的技术,没有之一,学好了JavaScript,就能在前端这个领域里无所不能。JavaScript现在也开始逐渐的在其他领域崭露头脚,它与TypeScript相辅相成,可以预见,JavaScript是一门历久弥新,并且前景良好的编程语言。


笔者对JavaScript最深的感受就是,与JavaScript相关的技术栈的更新迭代都非常的快,在我刚刚毕业的时候,Vue.js还是非常新鲜的东西,截止今天,Vue.js已经成为前端找工作的必备技能。JavaScript的框架总是层出不穷,让人有一种学不动了的感觉。造成这种“混乱”的背后原因有很多,这也从侧面反映了大家对JavaScript的热爱和探索。


路漫漫其修远兮,吾将上下而求索


我非常建议你在决定入行之前,提前找一些学习资料,自己尝试一下是否能够学的清楚,并且能从中获取乐趣。


兴趣和恒心是决定你能不能在这个行业长足发展的决定性因素。面对海量的,对你而言是闻所未闻的知识,如果没有兴趣,你每天都会深受折磨;这个行业特质决定了我们需要不断地学习,没有恒心,总有一天会掉队,跟不上技术更新迭代的脚步。


总而言之,道阻且长,希望本文能对你

作者:清风孤月残酌
来源:juejin.cn/post/7205549412352868412
有所启发,与君共勉。

收起阅读 »

Android:我是如何优化APP体积的

前言 在日常开发中,随着APP功能迭代发现打出的安装包体积越来越大,这里说的大是猛增的那种大,而并非一点一点增大。从最开始的几兆到后面的几十兆,虽然市面上的很多APP甚至达到上百兆,但毕竟别人功能强大,用到的一些底层库就特别占面积,流量也多所以也可理解。但自...
继续阅读 »

前言



在日常开发中,随着APP功能迭代发现打出的安装包体积越来越大,这里说的大是猛增的那种大,而并非一点一点增大。从最开始的几兆到后面的几十兆,虽然市面上的很多APP甚至达到上百兆,但毕竟别人功能强大,用到的一些底层库就特别占面积,流量也多所以也可理解。但自研的一些APP可经不住这些考验,所以能压缩就压缩,能优化就尽量优化,以得到用户最好的体验,下面就来说说我在项目中是如何优化APP体积的。



1. 本地资源优化


这里主要是压缩一些图片和视频。项目中本地资源用到最多的应该就是图片,几乎每个页面都离不开图标,甚至一些页面采用大图片的形式。你可知道,正常不经压缩的图片大的可以上大几十兆,小则也是一兆起步。这里做了个实验,同一个文件分别采用svg、png、使用tiny压缩后的png、webp四种类型图片进行展示(顺序是从左到右,从上到下):


image.png


可以看到,加载出来的效果几乎没有什么区别,但体积却有很大的差别(其中webp是采取的默认75%转换):


image.png


所以,别再使用png格式图片,太浪费资源了,就算经过压缩还是不及svg和webp,这里的webp其实还可以加大转换力度,但个人还是比较喜欢svg。


至于音视频文件也是可以通过其他工具进行压缩再放入本地,如非必要,尽量还是使用网络资源。


2. lib优化


一些三方库会使用底层so文件,一般在配置的时候我们尽量选择一种cpu类型,这里选择armeabi-v7a,其实几乎都兼容


ndk {
//设置支持的SO库架构 armeabi、armeabi-v7a、arm64-v8a、x86、x86_64、mips、mips64
abiFilters 'armeabi-v7a'
}

可以看看APK体积分析,每种cpu占用体积都比较大,少配置一种就能省下不少空间。
image.png


3. 代码混淆、无用资源的删除


在bulid.gradle中配置minifyEnabled true开启代码混淆,还需要配置混淆规则,否则无法找到目标类。shrinkResources true则是打包时不会将无用资源打入包内,这里有个小坑。之前使用腾讯地图时,某些第三方的静态资源会因为这个操作不被打入包内,导致无法找到资源,所以根据具体情况使用。


 release {
buildConfigField "boolean", "LOG_DEBUG", "false"
minifyEnabled true
// shrinkResources true 慎用,可能会导致第三方资源文件找不到
zipAlignEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}

4. 代码复用,剔除无用代码


项目中由于多人协同开发会出现各写各的情况,需要抽出一些公共库之类的工具,方便代码复用。一些注释掉的代码该删除就删除。其实这一部分优化的体积相当少,但也得做,也是对代码质量的一种提升。


总结


其实只要做到了以上四步,APP体积优化已经得到了很大程度的提升了,其他再怎么优化效果也不是很明显了,最主要的就是本地资源和第三方so包体积占用较多。图片的使用我们尽量做到:小图标用svg,全屏类的大图可以考虑webp,最好不要使用png。ndk配置最好只配置一款cpu,几乎都可兼容,万不得已再加一个。


以上便是全部内容,希望对大家有所帮助。



作者:似曾相识2022
来源:juejin.cn/post/7206292770277261368
收起阅读 »

面试题:Android 中 Intent 采用了什么设计模式?

答案是采用了原型模式。 原型模式的好处在于方便地拷贝某个实例的属性进行使用、又不会对原实例造成影响,其逻辑在于对 Cloneable 接口的实现。 话不多说看下 Intent 的关键源码:  // frameworks/base/core/java/andro...
继续阅读 »

答案是采用了原型模式


原型模式的好处在于方便地拷贝某个实例的属性进行使用、又不会对原实例造成影响,其逻辑在于对 Cloneable 接口的实现。


话不多说看下 Intent 的关键源码:


 // frameworks/base/core/java/android/content/Intent.java
 public class Intent implements Parcelable, Cloneable {
    ...
     private static final int COPY_MODE_ALL = 0;
     private static final int COPY_MODE_FILTER = 1;
     private static final int COPY_MODE_HISTORY = 2;
 ​
     @Override
     public Object clone() {
         return new Intent(this);
    }
 ​
     public Intent(Intent o) {
         this(o, COPY_MODE_ALL);
    }
 ​
     private Intent(Intent o, @CopyMode int copyMode) {
         this.mAction = o.mAction;
         this.mData = o.mData;
         this.mType = o.mType;
         this.mIdentifier = o.mIdentifier;
         this.mPackage = o.mPackage;
         this.mComponent = o.mComponent;
         this.mOriginalIntent = o.mOriginalIntent;
        ...
 ​
         if (copyMode != COPY_MODE_FILTER) {
            ...
             if (copyMode != COPY_MODE_HISTORY) {
                ...
            }
        }
    }
    ...
 }

可以看到 Intent 实现的 clone() 逻辑是直接调用了 new 并传入了自身实例,而非调用 super.clone() 进行拷贝。


默认的拷贝策略是 COPY_MODE_ALL,顾名思义,将完整拷贝源实例的所有属性进行构造。其他的拷贝策略是 COPY_MODE_FILTER 指的是只拷贝跟 Intent-filter 相关的属性,即用来判断启动目标组件的 actiondatatypecomponentcategory 等必备信息。无视启动 flagbundle 等数据。


 // frameworks/base/core/java/android/content/Intent.java
 public class Intent implements Parcelable, Cloneable {
    ...
     public @NonNull Intent cloneFilter() {
         return new Intent(this, COPY_MODE_FILTER);
    }
 ​
     private Intent(Intent o, @CopyMode int copyMode) {
         this.mAction = o.mAction;
        ...
 ​
         if (copyMode != COPY_MODE_FILTER) {
             this.mFlags = o.mFlags;
             this.mContentUserHint = o.mContentUserHint;
             this.mLaunchToken = o.mLaunchToken;
            ...
        }
    }
 }

还有中拷贝策略是 COPY_MODE_HISTORY,不需要 bundle 等历史数据,保留 action 等基本信息和启动 flag 等数据。


 // frameworks/base/core/java/android/content/Intent.java
 public class Intent implements Parcelable, Cloneable {
    ...
     public Intent maybeStripForHistory() {
         if (!canStripForHistory()) {
             return this;
        }
         return new Intent(this, COPY_MODE_HISTORY);
    }
 ​
     private Intent(Intent o, @CopyMode int copyMode) {
         this.mAction = o.mAction;
        ...
 ​
         if (copyMode != COPY_MODE_FILTER) {
            ...
             if (copyMode != COPY_MODE_HISTORY) {
                 if (o.mExtras != null) {
                     this.mExtras = new Bundle(o.mExtras);
                }
                 if (o.mClipData != null) {
                     this.mClipData = new ClipData(o.mClipData);
                }
            } else {
                 if (o.mExtras != null && !o.mExtras.isDefinitelyEmpty()) {
                     this.mExtras = Bundle.STRIPPED;
                }
            }
        }
    }
 }

总结起来:


Copy Modeaction 等数据flags 等数据bundle 等历史
COPY_MODE_ALLYESYESYES
COPY_MODE_FILTERYESNONO
COPY_MODE_HISTORYYESYESNO

除了 Intent,Android 源码中还有很多地方采用了原型模式。




  • Bundle 也实现了 clone(),提供了 new Bundle(this) 的处理:


     public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
        ...
         @Override
         public Object clone() {
             return new Bundle(this);
        }
     }



  • 组件信息类 ComponentName 也在 clone() 中提供了类似的实现:


     public final class ComponentName implements Parcelable, Cloneable, Comparable<ComponentName> {
        ...
         public ComponentName clone() {
             return new ComponentName(mPackage, mClass);
        }
     }



  • 工具类 IntArray 亦是如此:


     public class IntArray implements Cloneable {
        ...
         @Override
         public IntArray clone() {
             return new IntArray(mValues.clone(), mSize);
        }
     }



原型模式也不一定非得实现 Cloneable,提供了类似的实现即可。比如:




  • Bitmap 没有实现该接口但提供了 copy(),内部将传递原始 Bitmap 在 native 中的对象指针并伴随目标配置进行新实例的创建:


     public final class ComponentName implements Parcelable, Cloneable, Comparable<ComponentName> {
        ...
         public Bitmap copy(Config config, boolean isMutable) {
            ...
             noteHardwareBitmapSlowCall();
             Bitmap b = nativeCopy(mNativePtr, config.nativeInt, isMutable);
             if (b != null) {
                 b.setPremultiplied(mRequestPremultiplied);
                 b.mDensity = mDensity;
            }
             return b;
        }
     }


  • <
    作者:TechMerger
    来源:juejin.cn/post/7204013918958649405
    li>
收起阅读 »

Android斩首行动——接口预请求

前言 开发同学应该都很熟悉我们页面的渲染过程一般是从Activity#onCreate开始,再发起网络请求,等请求回调回来后,再基于网络数据渲染页面。可以用下面这幅图来粗略描述这个过程: 可以看到,目标页面渲染完成前必须得等待网络请求,导致渲染速度并没有那么...
继续阅读 »

前言


开发同学应该都很熟悉我们页面的渲染过程一般是从Activity#onCreate开始,再发起网络请求,等请求回调回来后,再基于网络数据渲染页面。可以用下面这幅图来粗略描述这个过程:


image.png


可以看到,目标页面渲染完成前必须得等待网络请求,导致渲染速度并没有那么快。尤其是当网络并不好的时候感受会更加明显。并且,当目标页面是H5页面或者是Flutter页面的时候,因为涉及到H5容器与Flutter容器的创建,白屏时间会更长。


那么有没有可能提前发起请求,来缩短网络请求这一部分的等待时间呢?这就是我们今天要讲的部分,接口预请求。


目标


我们要达到的目标很简单,就是提前异步发起目标页面的网络请求,从而加快目标页面的渲染速度。改善后的过程可以用下图表示:


image.png


并且,我们的预请求能力需要尽量少地侵入业务,与业务解耦,并保证能力的通用性,适用于工程内的任意页面(Android页面、H5页面、Flutter页面)。


方案


整体链路


首先给大家看一下整体链路,具体的细节可以先不用去抠,下面会一一讲到。


image.png


预请求时机


预请求时机一般有三种选择:



  1. 由业务层自行选择时机进行异步预请求

  2. 点击控件时进行异步预请求

  3. 路由最终跳转前进行异步预请求


第1种选择,由业务层自行选择时机进行预请求,需要涉及到业务层的改造,以及对时机合理性的把握。一方面是存在改造成本,另一方面是无法保证业务侧调用时机的合理性。


第2种选择,点击控件时进行预请求。若点击时进行预请求,点击事件监听并不是业务域统一的,无法形成有效封装。并且,若后续路由拦截器修改了参数,或是终止了跳转,这次预请求就失去了意义。


因此这里我们选择第3种,基于统一路由框架,在路由最终跳转前进行预请求。既保证了良好的封装性,也实现了对业务的零侵入,同时也做到了懒请求,即用户必然要发起该请求时才会去预请求。这里需要注意的是必须是在最终跳转前进行预请求,可以理解为是路由的最后一个前置异步拦截器。


预请求规则配置


我们通过本地的json文件(当然,有需要也可以上云通过配置后台下发),对预请求的规则进行配置,并将这份配置在App启动阶段异步读入到内存。后续在路由过程中,只有命中了预请求规则,才能发起预请求。配置demo如下:


{
"routeConfig":{
"scheme://domain/path?param1=true&itemId=123":["prefetchKey"],
"
route2":["prefetchKey2"],
"
route3":["prefetchKey3","prefetchKey4"]
},
"
prefetcher":{
"
prefetchKey":{
"
prefetchType":"network",
"
prefetchInfo":{
"
api":"network.api.name",
"
apiVersion":"1.0",
"
method":"post",
"
needLogin":"false",
"
showLoginUI":"false",
"
params": {
"
itemId":"$route.itemId",
"
firstTime":"true"
},
"
headers": {

},
"
prefetchImgInResponse": [
{
"
imgUrl":"$data.imgData.img",
"
imgWidth":"$data.imgData.imgWidth",
"
imgHeight":150
}
]
}
},
"
prefetchKey2":{
"
prefetchType":"network",
"
prefetchInfo":{
"
api":"network.api.name2",
"
apiVersion":"1.0",
"
method":"post",
"
needLogin":"false",
"
showLoginUI":"false",
"
params": {
"
itemId":"$route.productId",
"
firstTime":"false"
},
"
headers": {

}
},
"
prefetchKey3":{
"
prefetchType":"image",
"
prefetchInfo":{
"
imgUrl":"$route.imgUrl",
"
imgWidth":"$route.imgWidth",
"
imgHeight": 150
}
},
"
prefetchKey4":{
"
prefetchInfo":{}
}
}
}

规则解读


参数名描述备注
routeConfig路由配置配置路由到预请求的映射
prefetcher预请求配置记录所有的预请求
prefetchKey预请求的key
prefetchType预请求类型分为network类型与image类型,两种类型所需要的参数不同
prefetchInfo预请求所需要的信息其中value若为route.param格式,那么该值从路由中获取;若为route.param格式,那么该值从路由中获取;若为data.param格式,则从响应数据中获取。
paramsnetwork请求所需要的请求params
headersnetwork请求所需要的请求headers
prefetchImgFromResponse预请求的响应返回后,需要预加载的图片用于需要预加载图片时,无法确定图片url,图片url只能从预请求响应中获取的场景。

举例说明


网络预请求


例如跳转目标页面,它的路由是scheme://domain/path?param1=true&itemId=123


首先我们在跳转路由时,若跳转的路由是这个目标页面,我们就会尝试去发起预请求。根据上面的demo配置文件,它将匹配到prefetchKey这个预请求。


那么我们详细看prefetchKey这个预请求,预请求类型prefetchTypenetwork,是一个网络预请求,prefetchInfo中具备了请求的基本参数(如apiName、apiVersion、method、请求params与请求headers,不同工程不一样,大家可以根据自己的工程项目进行修改)。具体看params中,有一个参数为itemId:$route.itemId。以$route.开头的意思,就是这个value值要从路由中获取,即itemId=123,那么这个值就是123。


图片预请求


在做网络预请求的过程中,我忽然想到图片做预请求也是可以大大提升用户体验的,尤其是当大图片首次下载到内存中渲染需要的时间会比较长。图片预请求分为url已知url未知两种场景,下面各举两个例子。


图片url已知

什么是图片url已知呢?比如我们在首页跳转首页的二级页面时,如果二级页面需要预加载的图片跟首页的某张图是一样的(尺寸可能不同),那么首页跳转路由时我们是能够提前知道这个图片的url的,所以我们看到prefetchKey3中配置了prefetchTypeimage的预请求。image的信息来自于路由参数,需要在跳转时将图片url和宽高作为路由参数之一。


比如scheme://domain/path?imgUrl=${encodeUrl}&imgWidth=200,那么根据配置项,我们将提前将encodeUrl这个图片以宽200,高150的尺寸,加载到内存中去。当目标页面用到这个图片时,将能很快渲染出来。


图片url未知

相反,当跳转目标页面时,目标页面所要加载的图片url没法取到,就对应了图片url未知的场景。


例如闪屏页跳转首页时,如果需要预加载首页顶部的图片,此时闪屏页是无法获取到图片的url的,因为这个图片url是首页接口返回的。这种情况下,我们只能依赖首页的预请求进行。


在demo配置文件中,我们可以看到prefetchImgFromResponse字段。这个字段代表着,当这个预请求响应回来之后,我需要去预请求某张图片。其中,imgUrl$data.param格式,以$data.开头,代表着这份数据是来自于响应数据的。响应数据就是一串json串,可以凭此,索引到预请求响应中图片url的位置,就能实现图片的提前加载了。


至于图片怎么提前加载到内存中,以及真实图片的加载怎么匹配到内存中的图片,这一部分是通过glide已有的preload机制实现的,感兴趣的同学可以去看一下源码了解一下,这里就不展开了。后面讲的预请求的方案细节,都只限于网络请求。


预请求匹配


预请求匹配指的是实际的业务请求怎样与已经执行的预请求匹配上,从而节省请求的空中时间,直接返回预请求的结果。


首先网络预请求执行前先在内存中生成一份PrefetchRecord,代表着已经执行的预请求,其中的字段跟配置文件中差不多,主要就是记录预请求相关的信息:


class PrefetchRecord {
// 请求信息
String api;
String apiVersion;
String method;
String needLogin;
String showLoginUI;
JSONObject params;
JSONObject headers;

// 预请求状态
int status;
// 预请求结果
ResponseModel response;
// 生成的请求id
String requestId;

boolean isMatch(RealRequest realRequest) {
requestId.equals(realRequest.requestId)
}
}

每一个PrefetchRecord生成时,都会生成一个requestId,用于跟实际业务请求进行匹配。requestId的生成规则可以自行制定,比如将所有请求信息包一起做一下md5处理之类。


在实际业务请求发起之前,也会根据同样的规则生成requestId。若内存中存在相同requestId对应的PrefetchRecord,那么就相当于匹配成功了。匹配成功后,再根据预请求的状态进行进一步的处理。


预请求状态


预请求状态分为START、FINISH、ABORT,对应“正在发起预请求”、“已经获得预请求结果”、“预请求被抛弃”。ABORT状态下一节再讲。


为什么要记录这个状态呢?因为我们无法保证,预请求的响应一定在实际请求之前。用图来表示:


image.png


因为预请求是一个并发行为。当预请求的空中时间特别长,长到目标页面已经发出实际请求了,预请求的响应还没回来,即预请求状态为START,而非FINISH。那么此时该怎么办?我们就需要让实际请求在一旁等着(记录到内存中,RealRequestRecord),等预请求接收到响应了,再根据requestId去进行匹配,匹配到RealRequestRecord了,就触发RealRequestRecord中的回调,返回数据。


另外,在匹配过程中需要注意一点,因为每次路由跳转,如果发起预请求了,总会生成一个Record在内存中等待匹配。因此在匹配结束后,不管是匹配成功还是匹配失败,都要及时释放将Record从内存中释放掉。


超时重试机制


基于实际请求等待预请求响应的场景,我们再延伸一下。若预请求请求超时,迟迟拿不到响应,该怎么办?用图表示:


image.png


假设目前的网络请求,端上默认的超时时间是30s。那么在超时场景下,实际的业务请求在30s内若拿不到预请求的结果,就需要重新发起业务请求,抛弃预请求,并将预请求的状态置为ABORT,这样即使后面预请求响应回来了也不做任何处理。


image.png


忽然想到一个很贴切的场景来比喻这个预请求方案。


我们把跳转页面理解为去柜台取餐。


预请求代表着我们人还没到柜台,就先远程下单让柜员去准备食物。


如果柜员准备得比较快,那么我们到柜台后就能直接把食物拿走了,就能快点吃上了(代表着页面渲染速度变快)。


如果柜员准备得比较慢,那么我们到柜台后还是得等一会儿才能取餐,但总体上吃上食物的速度还是要比到柜台后再点餐来得快。


但如果这个柜员消极怠工准备得太慢了,我们到柜台等了很久都没拿到食物,那么我们就只能换个柜员重新点了(超时后发起实际的业务请求),同时还不忘投诉一把(预请求空中时间太慢了)。


总结


通过这篇文章,我们知道了什么是接口预请求,怎么实现接口预请求。我们通过配置文件+统一路由处理+预请求发起、匹配、回调,实现了与业务解耦的,可适用于任意页面的轻量级预请求方案,从而提升页面的渲染速度。


作者:孝之请回答
来源:juejin.cn/post/7203615594390732855
收起阅读 »

写一个可以当镜子照的 Button

web
最近写了一个好玩的 Button,它除了是一个 Button 外,还可以当镜子照。 那这个好玩的 Button 是怎么实现的呢? 很容易想到是用到了摄像头。 没错,这里要使用浏览器的获取媒体设备的 api 来拿到摄像头的视频流,设置到 video 上,然后对...
继续阅读 »

最近写了一个好玩的 Button,它除了是一个 Button 外,还可以当镜子照。



那这个好玩的 Button 是怎么实现的呢?


很容易想到是用到了摄像头。


没错,这里要使用浏览器的获取媒体设备的 api 来拿到摄像头的视频流,设置到 video 上,然后对 video 做下镜像反转,加点模糊就好了。


button 的部分倒是很容易,主要是阴影稍微麻烦点。


把 video 作为 button 的子元素,加个 overflow:hidden 就完成了上面的效果。


思路很容易,那我们就来实现下吧。


获取摄像头用的是 navigator.mediaDevices.getUserMedia 的 api。


在 MDN 中可以看到 mediaDevices 的介绍:



可以用来获取摄像头、麦克风、屏幕等。


它有这些 api:



getDisplayMedia 可以用来录制屏幕,截图。


getUserMedia 可以获取摄像头、麦克风的输入。



我们这里用到的是 getUserMedia 的 api。


它要指定音频和视频的参数,开启、关闭、分辨率、前后摄像头啥的:



这里我们把 video 开启,把 audio 关闭。


也就是这样:


navigator.mediaDevices.getUserMedia({
video: true,
audio: false,
})
.then((stream) => {
//...
}).catch(e => {
console.log(e)
})

然后把获取到的 stream 用一个 video 来展示:


navigator.mediaDevices.getUserMedia({
video: true,
audio: false,
})
.then((stream) => {
const video = document.getElementById('video');
video.srcObject = stream;
video.onloadedmetadata = () => {
video.play();
};
})
.catch((e) => console.log(e));

就是这样的:



通过 css 的 filter 来加点感觉:


比如加点 blur:


video {
filter: blur(10px);
}


加点饱和度:


video {
filter: saturate(5)
}



或者加点亮度:


video: {
filter: brightness(3);
}


filter 可以组合,调整调整达到这样的效果就可以了:


video {
filter: blur(2px) saturate(0.6) brightness(1.1);
}


然后调整下大小:


video {
width: 300px;
height: 100px;
filter: blur(2px) saturate(0.6) brightness(1.1);
}


你会发现视频的画面没有达到设置的宽高。


这时候通过 object-fit 的样式来设置:


video {
width: 300px;
height: 100px;
object-fit: cover;
filter: blur(2px) saturate(0.6) brightness(1.1);
}

cover 是充满容器,也就是这样:



但画面显示的位置不大对,看不到脸。我想显示往下一点的画面怎么办呢?


可以通过 object-position 来设置:


video {
width: 300px;
height: 100px;
object-fit: cover;
filter: blur(2px) saturate(0.6) brightness(1.1);
object-position: 0 -100px;
}

y 向下移动 100 px ,也就是这样的:



现在画面显示的位置就对了。


其实现在还有一个特别隐蔽的问题,不知道大家发现没,就是方向是错的。照镜子的时候应该左右翻转才对。


所以加一个 scaleX(-1),这样就可以绕 x 周反转了。


video {
width: 300px;
height: 100px;
object-fit: cover;
filter: blur(2px) saturate(0.6) brightness(1.1);
object-position: 0 -100px;
transform: scaleX(-1);
}


这样就是镜面反射的感觉了。


然后再就是 button 部分,这个我们倒是经常写:


function Button({ children }) {
const [buttonPressed, setButtonPressed] = useState(false);

return (
<div
className={`button-wrap ${buttonPressed ? "pressed" : null}`}
>

<div
className={`button ${buttonPressed ? "pressed" : null}`}
onPointerDown={() =>
setButtonPressed(true)}
onPointerUp={() => setButtonPressed(false)}
>
<video/>
</div>
<div className="text">{children}</div>
</div>

);
}

这里我用 jsx 写的,点击的时候修改 pressed 状态,设置不同的 class。


样式部分是这样的:


:root {
--transition: 0.1s;
--border-radius: 56px;
}

.button-wrap {
width: 300px;
height: 100px;
position: relative;
transition: transform var(--transition), box-shadow var(--transition);
}

.button-wrap.pressed {
transform: translateZ(0) scale(0.95);
}

.button {
width: 100%;
height: 100%;
border: 1px solid #fff;
overflow: hidden;
border-radius: var(--border-radius);
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.25), 0px 8px 16px rgba(0, 0, 0, 0.15),
0px 16px 32px rgba(0, 0, 0, 0.125);
transform: translateZ(0);
cursor: pointer;
}

.button.pressed {
box-shadow: 0px -1px 1px rgba(0, 0, 0, 0.5), 0px 1px 1px rgba(0, 0, 0, 0.5);
}

.text {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
color: rgba(0, 0, 0, 0.7);
font-size: 48px;
font-weight: 500;
text-shadow:0px -1px 0px rgba(255, 255, 255, 0.5),0px 1px 0px rgba(255, 255, 255, 0.5);
}

这种 button 大家写的很多了,也就不用过多解释。


要注意的是 text 和 video 都是绝对定位来做的居中。


再就是阴影的设置。


阴影的 4 个值是 x、y、扩散半径、颜色。


我设置了个多重阴影:




然后再改成不同透明度的黑就可以了:



再就是按下时的阴影,设置了上下位置的 1px 黑色阴影:


.button.pressed {
box-shadow: 0px -1px 1px rgba(0, 0, 0, 0.5), 0px 1px 1px rgba(0, 0, 0, 0.5);
}

同时,按下时还有个 scale 的设置:



再就是文字的阴影,也是上下都设置了 1px 阴影,达到环绕的效果:


text-shadow:0px -1px 0px rgba(255, 255, 255, 0.5),0px 1px 0px rgba(255, 255, 255, 0.5);


最后,把这个 video 嵌进去就行了。


完整代码如下:


import React, { useState, useEffect, useRef } from "react";
import "./button.css";

function Button({ children }) {
const reflectionRef = useRef(null);
const [buttonPressed, setButtonPressed] = useState(false);

useEffect(() => {
if (!reflectionRef.current) return;
navigator.mediaDevices.getUserMedia({
video: true,
audio: false,
})
.then((stream) => {
const video = reflectionRef.current;
video.srcObject = stream;
video.onloadedmetadata = () => {
video.play();
};
})
.catch((e) => console.log(e));
}, [reflectionRef]);

return (
<div
className={`button-wrap ${buttonPressed ? "pressed" : null}`}
>

<div
className={`button ${buttonPressed ? "pressed" : null}`}
onPointerDown={() =>
setButtonPressed(true)}
onPointerUp={() => setButtonPressed(false)}
>
<video
className="button-reflection"
ref={reflectionRef}
/>

</div>
<div className="text">{children}</div>
</div>

);
}

export default Button;

body {
padding: 200px;
}
:root {
--transition: 0.1s;
--border-radius: 56px;
}

.button-wrap {
width: 300px;
height: 100px;
position: relative;
transition: transform var(--transition), box-shadow var(--transition);
}

.button-wrap.pressed {
transform: translateZ(0) scale(0.95);
}

.button {
width: 100%;
height: 100%;
border: 1px solid #fff;
overflow: hidden;
border-radius: var(--border-radius);
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.25), 0px 8px 16px rgba(0, 0, 0, 0.15),
0px 16px 32px rgba(0, 0, 0, 0.125);
transform: translateZ(0);
cursor: pointer;
}

.button.pressed {
box-shadow: 0px -1px 1px rgba(0, 0, 0, 0.5), 0px 1px 1px rgba(0, 0, 0, 0.5);
}

.text {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
color: rgba(0, 0, 0, 0.7);
font-size: 48px;
font-weight: 500;
text-shadow:0px -1px 0px rgba(255, 255, 255, 0.5),0px 1px 0px rgba(255, 255, 255, 0.5);
}

.text::selection {
background-color: transparent;
}

.button .button-reflection {
width: 100%;
height: 100%;
transform: scaleX(-1);
object-fit: cover;
opacity: 0.7;
filter: blur(2px) saturate(0.6) brightness(1.1);
object-position: 0 -100px;
}

总结


浏览器提供了 media devices 的 api,可以获取摄像头、屏幕、麦克风等的输入。


除了常规的用途外,还可以用来做一些好玩的事情,比如今天这个的可以照镜子的 button。


它看起来就像我上厕所时看到的这个东西一样😂:



作者:zxg_神说要有光
来源:juejin.cn/post/7206249542752567333
收起阅读 »

测试妹子提了个bug,为什么你多了个options请求?

web
测试妹子给我提了个bug,说为什么一次操作,network里面两个请求。 我脸色一变”不可能,我写的代码明明是一次操作,怎么可能两个请求“。走过去一看,原来是多了个options请求。 ”这个你不用管,这个是浏览器默认发送的一个预检请求“。可是妹子很执着”这可...
继续阅读 »

测试妹子给我提了个bug,说为什么一次操作,network里面两个请求。


我脸色一变”不可能,我写的代码明明是一次操作,怎么可能两个请求“。走过去一看,原来是多了个options请求。


”这个你不用管,这个是浏览器默认发送的一个预检请求“。可是妹子很执着”这可肯定不行啊,明明是一次请求,干嘛要两次呢?“。


”哟呵,挺固执啊,那我就给你讲个明白,到时候你可别说听不懂“。


HTTP的请求分为两种简单请求非简单请求


简单请求


简单请求要满足两个条件:



  1. 请求方法为:HEADGETPOST

  2. header中只能包含以下请求头字段:

    • Accept

    • Accept-Language

    • Content-Language

    • Content-Type: 所指的媒体类型值仅仅限于下列三者之一

      • text/plain

      • multipart/form-data

      • application/x-www-form-urlencoded






浏览器的不同处理方式


对于简单请求来说,如果请求跨域,那么浏览器会放行让请求发出。浏览器会发出cors请求,并携带origin。此时不管服务端返回的是什么,浏览器都会把返回拦截,并检查返回的responseheader中有没有Access-Control-Allow-Origin是否为true,说明资源是共享的,可以拿到。如果没有这个头信息,说明服务端没有开启资源共享,浏览器会认为这次请求失败终止这次请求,并且报错。


非简单请求


只要不满足简单请求的条件,都认为是非简单请求。


发出非简单cors请求,浏览器会做一个http的查询请求(预检请求)也就是optionsoptions请求会按照简单请求来处理。那么为什么会做一次options请求呢?


检查服务器是否支持跨域请求,并且确认实际请求的安全性。预检请求的目的是为了保护客户端的安全,防止不受信任的网站利用用户的浏览器向其他网站发送恶意请求。
预检请求头中除了携带了origin字段还包含了两个特殊字段:



  • Access-Control-Request-Method: 告知服务器实际请求使用的HTTP方法

  • Access-Control-Request-Headers:告知服务器实际请求所携带的自定义首部字段。
    比如:


OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type

以上报文中就可以看到,使用了OPTIONS请求,浏览器根据上面的使用的请求参数来决定是否需要发送,这样服务器就可以回应是否可以接受用实际的请求参数来发送请求。Access-Control-Request-Method告知服务器,实际请求将使用 POST 方法。Access-Control-Request-Headers告知服务器,实际请求将携带两个自定义请求标头字段:X-PINGOTHER 与 Content-Type。服务器据此决定,该实际请求是否被允许。


什么时候会触发预检请求呢?



  1. 发送跨域请求时,请求头中包含了一些非简单请求的头信息,例如自定义头(custom header)等;

  2. 发送跨域请求时,使用了 PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH等请求方法。


我得意的说“讲完了,老妹你听懂了吗?”


妹子说“似懂非懂”


那行吧,带你看下实际场景。(借鉴文章CORS 简单请求+预检请求(彻底理解跨域)的两张图)


image.png


image.png


妹子说“这样就明了很多”,满是崇拜的关闭了Bug。


兄弟们,妹子都懂了,你懂了吗?😄


参考:


CORS 简单请求+预检请求(彻底理解跨域)


OPTIONS | MDN


跨源资源共享(CORS)| MDN


说明一下哈,以上事件是真实事件,只不过当时讲的时候没有那么的详细,😂


作者:翰玥
来源:juejin.cn/post/7206264862657445947
收起阅读 »

前端要对用户的电池负责!

web
前言 我有一个坏习惯:就是下班之后不关电脑,但是电脑一般来说第二天电量不会有什么损失。但是后来突然有一天它开不起来了,上午要罢工?那也不好吧,也不能在光天化日之下划水啊;聪明的我还是找到了原因:电池耗尽了;于是赶紧来查一查原因,到底是什么程序把电池耗尽了呢? ...
继续阅读 »

前言


我有一个坏习惯:就是下班之后不关电脑,但是电脑一般来说第二天电量不会有什么损失。但是后来突然有一天它开不起来了,上午要罢工?那也不好吧,也不能在光天化日之下划水啊;聪明的我还是找到了原因:电池耗尽了;于是赶紧来查一查原因,到底是什么程序把电池耗尽了呢?


这里我直接揭晓答案:是一个Web应用,看来这个锅必须要前端来背了;我们来梳理一下js中有哪些耗电的操作。


js中有哪些耗电的操作


js中有哪些耗电的操作?我们不如问问浏览器有哪些耗电的操作,浏览器在渲染一个页面的时候经历了GPU进程、渲染进程、网络进程,由此可见持续的GPU绘制、持续的网络请求和页面刷新都会导致持续耗电,那么对应到js中有哪些操作呢?


Ajax、Fetch等网络请求


单只是一个AjaxFetch请求不会消耗多少电量,但是如果有一个定时器一直轮询地向服务器发送请求,那么CPU一直持续运转,并且网络进程持续工作,它甚至有可能会阻止电脑进行休眠,就这样它会一直轮询到电脑电池不足而关机;
所以我们应该尽量少地去做轮询查询;


持续的动画


持续的动画会不断地触发GPU重新渲染,如果没有进行优化的话,甚至会导致主线程不断地进行重排重绘等操作,这样会加速电池的消耗,但是js动画与css动画又不相同,这里我们埋下伏笔,等到后文再讲;


定时器


持续的定时器也可能会唤醒CPU,从而导致电池消耗加速;


上面都是一些加速电池消耗的操作,其实大部分场景都是由于定时器导致的电池消耗,那么下面来看看怎么优化定时器的场景;


针对定时器的优化


先试一试定时器,当我们关闭屏幕时,看定时器回调是否还会执行呢?


let num = 0;
let timer = null;
function poll() {
clearTimeout(timer);
timer = setTimeout(()=>{
console.log("测试后台时是否打印",num++);
poll();
},1000*10)
}

结果如下图,即使暂时关闭屏幕,它仍然会不断地执行:


未命名.png


如果把定时器换成requestAnimationFrame呢?


let num = 0;
let lastCallTime = 0;
function poll() {
requestAnimationFrame(() =>{
const now = Date.now();
if(now - lastCallTime > 1000*10){
console.log("测试raf后台时是否打印",num++);
lastCallTime = now;
}
poll();
});
}

屏幕关闭之前打印到了1,屏幕唤醒之后才开始打印2,真神奇!


未命名.png


当屏幕关闭时回调执行停止了,而且当唤醒屏幕时又继续执行。屏幕关闭时我们不断地去轮询请求,刷新页面,执行动画,有什么意义呢?因此才出现了requestAnimationFrame这个API,浏览器对它进行了优化,用它来代替定时器能减少很多不必要的能耗;


requestAnimationFrame的好处还不止这一点:它还能节省CPU,只要当页面处于未激活状态,它就会停止执行,包括屏幕关闭、标签页切换;对于防抖节流函数,由于频繁触发的回调即使执行次数再多,它的结果在一帧的时间内也只会更新一次,因此不如在一帧的时间内只触发一次;它还能优化DOM更新,将多次的重排放在一次完成,提高DOM更新的性能;


但是如果浏览器不支持,我们必须用定时器,那该怎么办呢?


这个时候可以监听页面是否被隐藏,如果隐藏了那么清除定时器,如果重新展示出来再创建定时器:


let num = 0;
let timer = null;
function poll() {
clearTimeout(timer);
timer = setTimeout(()=>{
console.log("测试后台时是否打印",num++);
poll();
},1000*10)
}

document.addEventListener('visibilitychange',()=>{
if(document.visibilityState==="visible"){
console.log("visible");
poll();
} else {
clearTimeout(timer);
}
})

针对动画优化


首先动画有js动画、css动画,它们有什么区别呢?


打开《我的世界》这款游戏官网,可以看到有一个keyframes定义的动画:


未命名.png


切换到其他标签页再切回来,这个扫描二维码的线会突然跳转;


因此可以得出一个结论:css动画在屏幕隐藏时仍然会执行,但是这一点我们不好控制。


js动画又分为三种种:canvas动画、SVG动画、使用js直接操作css的动画,我们今天不讨论SVG动画,先来看一看canvas动画(MuMu官网):


未命名.png


canvas动画在页面切换之后再切回来能够完美衔接,看起来动画在页面隐藏时也并没有执行;


那么js直接操作css的动画呢?动画还是按照原来的方式一直执行,例如大话西游这个官网的”获奖公示“;针对这种情况我们可以将动画放在requestAnimationFrame中执行,这样就能在用户离开屏幕时停止动画执行


上面我们对大部分情况已经进行优化,那么其他情况我们没办法考虑周到,所以可以考虑判断当前用户电池电量来兼容;


Battery Status API兜底


浏览器给我们提供了获取电池电量的API,我们可以用上去,先看看怎么用这个API:


调用navigator.getBattery方法,该方法返回一个promise,在这个promise中返回了一个电池对象,我们可以监听电池剩余量、电池是否在充电;


navigator.getBattery().then((battery) => {
function updateAllBatteryInfo() {
updateChargeInfo();
updateLevelInfo();
updateChargingInfo();
updateDischargingInfo();
}
updateAllBatteryInfo();

battery.addEventListener("chargingchange", () => {
updateChargeInfo();
});
function updateChargeInfo() {
console.log(`Battery charging? ${battery.charging ? "Yes" : "No"}`);
}

battery.addEventListener("levelchange", () => {
updateLevelInfo();
});
function updateLevelInfo() {
console.log(`Battery level: ${battery.level * 100}%`);
}

battery.addEventListener("chargingtimechange", () => {
updateChargingInfo();
});
function updateChargingInfo() {
console.log(`Battery charging time: ${battery.chargingTime} seconds`);
}

battery.addEventListener("dischargingtimechange", () => {
updateDischargingInfo();
});
function updateDischargingInfo() {
console.log(`Battery discharging time: ${battery.dischargingTime} seconds`);
}
});


当电池处于充电状态,那么我们就什么也不做;当电池不在充电状态并且电池电量已经到达一个危险的值得时候我们需要暂时取消我们的轮询,等到电池开始充电我们再恢复操作


后记


电池如果经常放电到0,这会影响电池的使用寿命,我就是亲身经历者;由于Web应用的轮询,又没有充电,于是一晚上就耗完了所有的电,等到再次使用时电池使用时间下降了好多,真是一次痛心的体验;于是后来我每次下班前将Chrome关闭,并关闭所有聊天软件,但是这样做很不方便,而且很有可能遗忘;


如果每一个前端都能关注到自己的Web程序对于用户电池的影响,然后尝试从定时器和动画这两个方面去优化自己的代码,那么我想应该就不会发生这种事情了。注意:桌面端程序也有可能是披着羊皮的狼,就是披着原生程序的Web应用;


参考:
MDN


作者:蚂小蚁
来源:juejin.cn/post/7206331674296746043
收起阅读 »

37岁程序员被裁,120天没找到工作,无奈去小公司,结果懵了...

从短期来看,程序员的确算是个不错的工作,薪水也比一般岗位高很多,但是从长远来看,程序员的中年危机会比其他岗位来的更早,很多程序员只有到了35岁左右,才能真正认清楚互联网行业,尤其是被裁之后,找工作无门的这种无奈。最近在职场论坛上看到这样一个帖子:37岁程序员被...
继续阅读 »


从短期来看,程序员的确算是个不错的工作,薪水也比一般岗位高很多,但是从长远来看,程序员的中年危机会比其他岗位来的更早,很多程序员只有到了35岁左右,才能真正认清楚互联网行业,尤其是被裁之后,找工作无门的这种无奈。

最近在职场论坛上看到这样一个帖子:37岁程序员被裁,120天没找到工作,面试华为阿里均被拒,无奈降薪去小公司后懵了!

这是怎么回事呢?


据这位程序员说自己写代码15年了,目前在二线互联网公司当个小主管,可是在最近被公司优化裁掉了。

由于自己已经37岁,找了4个月工作都没有着落,无奈降薪去小公司,可是小公司的面试官却还说我技术太落后!现在上有老下有少,自从被裁后每天都睡不着,很难熬!

那么,网友是怎么说的?


有网友说你可能没认清自己吧,什么叫做小公司还挑挑拣拣你呢?自己能力不够,就抱怨找不到工作,为什么当初不把技术学精呢!有网友对此进行反驳,你也会有35岁那天,程序员的今天就是你的明天,何必要嘲讽呢?


还有网友现在华为,阿里等大公司,只要超过35岁了,连面试机会都不会给你,简历那一关就被卡住了。去年我还说华为加班太多不想去,可现在想去都没机会了。


还有网友说那就没办法了,只能选择降薪,毕竟一家人总要生活,工资低点就低点,总比没工作要强。其实这也是很多程序员收入虽然高,却不敢在大城市买房的真正原因。要是等到30多岁被裁了,到时候每天还房贷的压力太大,根本不敢想象没有工作的情况。


你怎么看呢?欢迎转发分享吐槽!

来源:互联网架构师

收起阅读 »

写给想转行学IT的朋友们的话

最近有很多朋友咨询我关于想转行学IT的问题,我想结合自身的经历和思考和各位聊几句,本文带有比较强的主观色彩,因此文中观点仅供参考,如有不当之处,敬请海涵。 笔者19年本科毕业于一所中流211,毕业之后一直在做Java开发,我并没有进大厂,也没有年薪百万,就是芸...
继续阅读 »

最近有很多朋友咨询我关于想转行学IT的问题,我想结合自身的经历和思考和各位聊几句,本文带有比较强的主观色彩,因此文中观点仅供参考,如有不当之处,敬请海涵。


笔者19年本科毕业于一所中流211,毕业之后一直在做Java开发,我并没有进大厂,也没有年薪百万,就是芸芸众生中普通的那一个。现在回顾当时入行的经历,基本是摸着石头过河,因此在有了一些经验之后,就想给和我当时一样处境的朋友一点建议,希望能对你有所启发。


很多问题其实都要结合个人的实际情况来看,每个人的知识、能力、经历都各不相同,所以无论做出任何决定,都需要结合自身的情况。


是否有必要参加培训机构?


这个问题的答案是因人而异的,核心点在于你是否有足够的恒心和自制力。


现在互联网上的学习资料已经非常多了,足够支撑你从零学到能够入行的过程。如果你的自制力比较好,那么你就可以尝试自学,不过自学的过程是孤独的,也是难熬的;如果你的自制力一般,无法在不是“学校”那么环境下进行学习,那就可以考虑培训机构。


需要特别强调的是,不能以为进了培训机构就以为一定能够就业了,说到底,学习这件事情,还是要靠你自己,别人是没有办法把知识灌进你的脑袋里的。培训机构也有一些无法忽视的问题,当你毕业之后,大概率机构会给你伪造一份简历,本来学习的时间就并不是很长,你很快可能就会发现,培训机构里面学的比较浅,还不足以应付面试官的八股文,这时候你可能就会想先入行再说,然后进了一家外包公司,然后开始混日子,这也是笔者见过的最多的案例。


参加培训最大的还是简历,培训机构给大家做的项目都是极其雷同的,也可能会在培训结构之后,让你自己编项目经验,有经验的面试官很容易就能判断出,你是真的参与了项目开发,还是只是包装的。另外你可能还会碰到,很多公司需要上一家公司的离职证明和银行流水,甚至需要你个税APP报税界面的录屏等等问题,这些培训机构并不会告诉你。


总而言之,转行是一件对你的人生来说,是一件极其重要的事情,要反复思虑,不要脑子一热,听身边的朋友说,那个谁谁谁培训了几个月就月入过万,然后就稀里糊涂花了几万参加了培训机构,结果培训结束真正找工作的时候又发现困难重重,现实往往比想象的残酷。


高中毕业可以从事IT行业吗?


真相是,如果要从事IT行业,大专及以上学历是基本要求,不排除个人大专以下学历也找到了很好的IT工作,但是在当下这么“卷”的环境下,就是幸存者偏差了。


大专的学历我也非常建议你先提升学历,有一个本科学历。


前端、后端还是测试?


其实从某种意义来说,选择岗位就是选择某种编程语言,选择编程语言也是在选择岗位,这个问题,你也可以结合下文编程语言之间的对比,找到自己合适的岗位。大体而言,测试、前端、后端,这三者的难度是依次递增的(对大部分人而言)。


如果你没有科班背景,但是又想从事IT行业,那么你可以考虑从事测试、运维工作甚至产品、运营的工作,其实开发写代码并不是唯一的选择。


如果你有一定的基础,但是逻辑思维又不是那么强,那么我建议你可以考虑前端,女生大部分学习编程都会选择前端,前端由于不需要考虑架构、性能(大部分场景),因此难度会小一些,对初学者更加友好。不过,前端经过这些年的发展,知识体系俨然已经非常庞大,后期也需要学习很多的东西。


如果你有一定的基础,且逻辑思维还可以,那么我建议你可以考虑后端,如果你不知道自己的思维能力怎么样,可以学一些Java或者JavaScript语言基础,看看自己是不是能够比较轻松的学会。


Java、Python、C++还是JavaScript?


JavaScript


作为一门十天就被开发出来的编程语言,JavaScript身上的历史包袱也有很多,但这并不妨碍它的伟大,JavaScript在我们的日常生活中,几乎无处不在。


JavaScript也是一门易学难精的语言,虽然上手比较容易,但是后期也需要很多的时间去理解和巩固。JavaScript是前端程序员最重要的技术,学好了JavaScript,就能在前端这个领域里无所不能。JavaScript现在也开始逐渐的在其他领域崭露头脚,它与TypeScript相辅相成,可以预见,JavaScript是一门历久弥新,并且前景良好的编程语言。


Java


如果你已经决定了要从事软件开发行业,但是还不清楚,要选择哪一门编程语言,那我推荐你可以学习Java,Java这门语言本身虽然并不优秀,截止目前,Java已经走过了20个年头,有着非常丰富的生态,web端,它有JSP、Servlet,移动端,它有Android,服务器端有SpringBoot,桌面端它有JavaFX,也有非常优秀的网络通信框架,比如Netty,甚至它也做出过操作系统(塞班系统),可以说,Java虽然很烂,但是它真的几乎无所不能。


不可否认Java的成功,但是Java也存在一些隐患,比如在Oracle收购了Sun公司之后,在Oracle不断地花式作妖下,Java的未来似乎也有些扑朔迷离。除此之外,Java在引以为傲的领域也逐渐有了一些竞争对手,比如服务端有go语言、nodejs,Android的开发官方首选语言已经变成kotlin,JSP的市场基本上已经被Vue、React等SPA框架所替代,但是Java却并没有开拓出自己新的应用场景,虽然笔者认为,基于VM的语言并不是消失,但是Java是能否一直守住自己的王座,还是要打一个问号的。


Java目前最多的就业方向就是服务端开发,如果你学习了Java,那么大概率会做服务端开发,短期内,服务端开发Java还是很难被其他语言替代。强类型的语言加上对并发编程的支持,让Java非常适合构建大型的服务端应用,这也是Java最深耕的方向,学会Java服务端的开发,也比较方便向大数据或数据分析岗位进行转型。


最后我想说,Java的岗位在我提到的编程语言里面也是最多的,这也是在选择编程语言很重要的一个需要考虑的点,很多编程语言,看起来很热门但是,真正学完去找工作的时候,发现岗位少的可怜,没错,说的就是你,Python。


Python


Python这门编程语言,“胶水语言”的特性让它看起来也几乎无所不能,虽然这种能力很有可能来自其他的语言,Python只是作为客户端,调用其他语言的类库。


不够“底层”其实并不是什么缺点,学习Python最大的问题是,是对学历有要求,Python应用最广泛的领域,大部分都对学历有要求,虽然你可能看过很多投放Python的广告,自动化办公、爬虫等等,但是这些特性在企业中的岗位是比较少的,而Python擅长的算法、人工智能、深度学习等领域都是需要研究生学历的。


我们总结一下,如果你有研究生学历,那么学习Python是一个不错的选择,不过我也见过有的研究生朋友学完Python之后发现岗位很少,又被迫转Java的案例。


C++


C++相比于其他的编程语言,其入门的门槛的会更高,花费在学习语言本身的时间的也会很多。C++在经历过这么多年的迭代之后,语言的特性非常多,会让初学者有一种眼花缭乱的特性,实现同一个功能,可能会有很多种写法。不可否认,C++是一门优秀的编程语言,但是高昂的学习成本也让人望而却步,如果你不是科班出身,只是想学一门技术进入互联网行业,那C++并不是一个很好的选择。


C++常见的就业方向有以下几种。



  • 服务器端开发

  • 游戏

  • QT

  • 嵌入式

  • 人工智能


总结来说,C++最适合的还是那些对性能有要求的场景。原来很大一部分C++程序员都是做QT开发,但是C++在桌面端的市场已经出现了越来越多的竞争者,比如Hybrid 技术、React Native、Weex、Flutter,这些技术让原来web端的程序员或者移动端的程序员也能写出跨平台的应用,并且这些技术保留了他们原本的开发习惯,这对QT的打击,无疑是毁灭性的。


那么C++是不是已经过时,或者即将被淘汰呢?其实不然,C++只是让出了一些自己不是那么擅长的领域,但是在底层应用的开发,C++丰富的生态和优越的性能还是首选。如果你有志在这些领域发展,那么C++将是你很好的选择。


随着越来越多的人对核心自研技术的重视,这种比较偏底层的岗位,会越来越多,最近几年,国产操作系统的发展就是一个很好的例子,在政策的扶持下,各家公司几乎都在号称自研操作系统,可以预料,在不远的将来,这些核心的系统的研发,都需要大量C/C++语言的人才。


路漫漫其修远兮,吾将上下而求索


我非常建议你在决定入行之前,提前找一些学习资料,自己尝试一下是否能够学的清楚,并且能从中获取乐趣。


兴趣和恒心是决定你能不能在这个行业长足发展的决定性因素。面对海量的,对你而言是闻所未闻的知识,如果没有兴趣,你每天都会深受折磨;这个行业特质决定了我们需要不断地学习,没有恒心,总有一天会掉队,跟不上技术更新迭代的脚步。


总而言之,道阻且长,希望本文能对你有所启发,与君共勉。


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

浅谈敏捷开发与互联网江湖的爱恨情仇!

敏捷开发 通常互联网产品都采用一套称之为 敏捷开发 的研发流程 @需求阶段 定方向:企划部门和产品部门一道确定产品方向; 出需求:产品经理提供: 需求文档(对产品体系和功能的详细描述,常用的格式为Word配合PPT) + 产品原型(一个能在PC/手机浏览器...
继续阅读 »

敏捷开发


通常互联网产品都采用一套称之为 敏捷开发 的研发流程


@需求阶段


image.png



  • 定方向:企划部门和产品部门一道确定产品方向;

  • 出需求:产品经理提供: 需求文档(对产品体系和功能的详细描述,常用的格式为Word配合PPT) + 产品原型(一个能在PC/手机浏览器上进行页面跳转和简单交互的demo);


image.png


@研发阶段



  • 研发:前后端开发和美术部门根据产品需求开始工作;

  • 设计师/美术:根据产品原型产出一整套页面设计图;


image.png



  • 服务端/后端:根据产品功能说明着手数据库设计和接口设计,产出接口文档,供前端调用以获取业务数据;

  • 客户端/前端:根据设计效果图开发PC/手机H5/小程序/App等用户端的界面 + 数据渲染 + 用户交互;

  • 接口联调:开发期间前后端开发者需要进行接口联调,以确保前端能通过后端提供的接口正确地获取到数据;


@测试阶段



  • 提交测试:前后端共同开发完毕后,打包成品供测试人员进行测试;

  • 产品测试:测试人员代入用户的角色,带着挑剔的眼光和看我不整死你的愉快心情对产品进行使用测试,并将宏伟的BUG列表这口黝黑锃亮的大锅戴在项目经理头上;


image.png



  • 修复BUG:项目经理在开发团队(前后端)内对锅进行二次分配,精确到具体责任人;

  • 反复完善:开发人员修复BUG,再次打包提交测试,测试再挑刺-开发再分锅修改-再提交测试,重复以上循环若干遍以后,测试表示产品凑合可以上线了,签字画押;

  • 产品上线:运维同学将产品部署到生产环境与用户见面;


程序员在产品上线时的样子↓
image.png


@维护阶段



  • 升级维护:产品上线以后还需要进行多个版本的迭代升级,包括新一轮的需求的开发,以及收集和修复用户反馈的产品BUG等,于是就继续产生2.0、3.0...


互联网江湖的爱恨情仇



  • 通常当开发进度完成到99%的时候,产品经理脑门一拍,哎呀这个地方可能得小改一下,很简单的你这样弄一下就行:XXOO@#$%^&*blablabla...,一想到打人要进橘子你默默地收起了磨过无数回的菜刀,开始彻底推翻重做...

  • 针对上述情况美术人员表示我早就习惯了;

  • 当然产品和研发的博弈为我们的茶余饭后提供了不少欢乐的素材


image.png


其实谁还不是一样...


image.png




“这不需要测试,肯定是好的,不必担心!”


image.png

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

离职后在家躺了一年,顺便写了本书

大家好,我卡颂。 从字节离职已经一年多了。 最开始离职的契机是电子社的编辑向我约书稿。以我的能力是没法同时兼顾字节的高强度工作以及在规定的时间内成书。 考虑到进厂上班的机会以后还有(但现在这行情,没准儿也没厂子要我也说不定),但写书的经历还没体验过,于是就辞职...
继续阅读 »

大家好,我卡颂。


从字节离职已经一年多了。


最开始离职的契机是电子社的编辑向我约书稿。以我的能力是没法同时兼顾字节的高强度工作以及在规定的时间内成书


考虑到进厂上班的机会以后还有(但现在这行情,没准儿也没厂子要我也说不定),但写书的经历还没体验过,于是就辞职了。


接下来聊聊程序员写书是种什么体验。


欢迎加入人类高质量前端框架研究群,带飞


为什么写书?


在大概5年前,写书还是一种行之有效的提升个人影响力的方式。


很多人在找工作前、创业前,都会写本书作为个人能力的背书


甚至还出现专门帮人写书的公司,这些公司的目标客户是那些由于某些契机突然出名的草根(比如一些现象级语言类综艺的选手)。


但最近几年,相比写书短视频塑造个人人设是更短平快的提升个人影响力途径。所以,写书的人就逐渐少了。


而且,写书真的不赚钱。卖一本书的版税收入可能就几块钱。相比于赚钱,一些急于通过写书提升个人影响力的人,甚至得反向给出版社钱,比如花钱买书号(1w多)。


所以,对于为什么写书,主要是想体验下这段经历,以及可能带来的些许个人影响力提升。


什么样的程序员能写书?


可能你会觉得写书是件很厉害的事情,那你觉得写一本掘金小册厉不厉害呢?


如果你觉得写一本掘金小册也挺厉害,那写某一技术领域的系列文章厉不厉害呢?


好像还行对吧,那深入学习某个技术领域厉不厉害呢?


是不是努努力就能达到?


实际上这是条很平滑的路径,就像打游戏升级一样,在很多关键节点都会有任务npc主动找你(比如小册运营,出版社编辑)。


所以,与其说写书很厉害,不如说写书的人很执着,把这条支线任务打通关了(虽然任务奖励并不丰厚)。


我写了本什么书?


书名叫React设计原理,基于React18,从理念、架构、实现三个层面解构React



虽然在此之前写过一本开源电子书react技术揭秘,但既然要出版,就好好对待吧。


于是,在规划这本书时,我主要从两方面下手:




  • 内容尽可能硬核




  • 怎么讲读者才听得懂?




先说第一点,为了理通react运行的方方面面,除了常规的阅读源码跟踪核心成员的各种进展外,为了搞懂React18的运行细节,我从0实现了一个React18,所有实现步骤可以在big-react项目中看到。


内容硬核很重要,但写出来能让人看懂同样很重要。为了达到这个目标,我主要参考了两个优秀的作品:



前者在行文过程中一直在提倡知识屏蔽(在教学过程中只关注当前学习的知识,屏蔽超纲知识对读者的干扰的原则),后者做到了理论与实践(丰富的在线示例)结合。


所以,我这本书存在两条脉络:




  • 抽象层级逐渐降低




  • 实现越来越复杂的模块




对于前者,本书的抽象层级会逐渐从理念到架构,最后到实现,每一层都屏蔽前一层的影响。


这也是为什么ReactDOM.createRoot这个初始化API会放到第六章再讲解 —— 在这个具体API的背后,是他的理念与架构。


对于后者,本书会从0实现与react相关的6个模块,最后我们会一起在React源码内实现一个新的原生Hook


一点感悟


这一年时间有很多朋友问我为什么不去找个班上。答案很简单 —— 因为我不喜欢上班。


所以,为了做喜欢的事,就得提前准备。就拿写书这件事来说,就像做游戏任务,他的每一步都是有迹可循的。


如果你也对现状不满,那就行动起来吧。为了想要的生活而探索,这个过程本身就充满了乐趣。


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

考研失败,加入国企当程序员,真香!

下面是正文。 最近考研出成绩了,大家考得怎么样?分享一个自己考研失败后,入职国企的故事。 1、考研失败 我是工作了3年后才参加考研的。 老家成都,本科毕业于帝都某所以能歌善舞著称的985学校,哲学专业。 大学选专业时家里人不懂,自己全凭爱好,第一志愿就是哲学...
继续阅读 »

下面是正文。




最近考研出成绩了,大家考得怎么样?分享一个自己考研失败后,入职国企的故事。


1、考研失败


我是工作了3年后才参加考研的。


老家成都,本科毕业于帝都某所以能歌善舞著称的985学校,哲学专业。


大学选专业时家里人不懂,自己全凭爱好,第一志愿就是哲学。上学时有多快乐,毕业时就多难找工作。


侥幸自己选修过大数据课程,并且听说程序员工资还不错,通过校招加入了隔壁省的一家制造业当程序员。


公司管吃管住,工资1w出头,干了2年,感觉到了危机。自己羞于靠关系上位,但技术上,计算机知识太薄弱也发展有限,于是就想趁着还年轻,考个计算机硕士,提升一下自己。


在工作的第3年决定考研,边工作边考研,压力真是不小。让本来就是学渣的我,从研究生考场走出来就知道了最终的结果:没戏了。


成绩出来后,果然不出所料。


2、加入国企


知道初试成绩以后我难过了几天,认真思考了一下自己的未来发展:旁边的帝都我也回不去了,又不甘心留在这个小城市,于是决定回老家成都发展。


毕业后一直在私企,总是听说国企好、央企不错,所以我这次投简历也想试一下。正好看到一篇讲程序员国企的文章:值得程序员加入的173家国企汇总,网友:这下彻底躺平了,于是就按照文中的思路,找到了成都的一些所谓不错的国企投递试试。


成功加入后发现:真香!




  • 福利真 ** 好!和其它公司谈薪酬,别人都是能压多低压多低,来这家国企,竟然还说我工作年限够长,在我期望的基础上加了3k。六险二金更不用说,平时的各种生活保障也是非常到位。不夸张的说,从私企转到国企的我,有一种刘姥姥进大观园的感觉。




  • 真卷!**是谁告诉我国企适合养老的?**这比我以前在私企工作强度大多了好嘛?而且我第一次听说部门平均加班时长影响个人绩效这种规则。




  • 技术上不激进。可能对于程序员来说,不停的学习新技术是一种常态,但是在这家国企,基本都是传统老技术,我打开代码还看到了我们领导1997年写的头文件。当然,你也可以认为这是一种不好的事。这里的技术对我来说,够了。




  • 同事关系很融洽。我人生中第一次去按摩店找技师,是女同事带我去的,谁能信!不过必须说一句,成都的按摩店是真正规啊!技师可真有劲。




去年回来工作快1年了,我现在对于考研已经释怀了。听说我们部门今年招人开始硕士起步了,**有时候我还挺庆幸自己去年没考研成功的,**不然即使上了研究生,我这实力,也不一定能进入现在的单位了。


3、写在最后


现在回过头来看,有一个不同的体会:考研是一件好事,但如果本身不是沉迷于科研事业,而是更想赚钱的话,有好的工作机会也别错过。


另外,多关注有用的信息很重要,有时候别人的一番话,可能就需要自己经历几年的曲折才能总结出来。


大家有任何程序员求职问题,欢迎在评论区和我交流~


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

FlutterComponent最佳实践之动画的显和隐

Flutter中包含大量的动画组件和自定义动画方式,所以,在合适的场景下选择合适的动画实现方式就成了决定代码质量好坏的一个重要因素。 动画选择决策树 Flutter中的动画从广义上来讲可以分为两类,一类是基于绘制的动画(Drawing-based animat...
继续阅读 »

Flutter中包含大量的动画组件和自定义动画方式,所以,在合适的场景下选择合适的动画实现方式就成了决定代码质量好坏的一个重要因素。


动画选择决策树


Flutter中的动画从广义上来讲可以分为两类,一类是基于绘制的动画(Drawing-based animations),另一类是基于代码的动画(Code-based animations)。


下面这个决策树,是Flutter动画选择的总纲,这里梳理了不同的动画的作用场景和功能,我们来看下它具体的实现。
image.png
首先,我们需要区分是使用CustomPainter,或者是使用Lottie、Flare这种第三方库,这一类的动画很容易区分——如果你第一感觉,这个动画我做不了,那它大概率就是了。


接下来,就是区分是使用「显示动画」还是「隐式动画」。


简单的说,它们的区别如下:



  • 隐式动画:不用循环播放、不用随时中断、不用多个动画协同,它实现的是一种状态到另一种状态的改变

  • 显示动画:需要自己控制动画过程


最后,就是看现有组件是否满足需求,如果不行,那么就需要自定义相应的动画。


这就是整个动画决策树的执行过程。它们的开发难度,如下所示。
image.png
下面我们就具体来分析下不同的动画实现。本文首先介绍显示动画和隐式动画。


Implicit Animations——隐式动画


在Flutter中,很多常用组件都有其自带的隐式动画版本,例如下图所示的这些组件。
image.png
这些组件在Flutter中被称之为隐式动画Widget,下面以AnimatedContainer为例,来看下Implicit Animations的使用。



隐式动画有一个特点,那就是它们都是以「Animated」开头。



基本使用


AnimatedContainer的使用非常简单,甚至和普通的Container没有太大的区别,代码如下所示。


AnimatedContainer(
margin: EdgeInsets.only(top: 20),
width: size,
height: size,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(radius),
),
curve: Curves.easeIn,
duration: Duration(milliseconds: 300),
),

当通过setState函数改变AnimatedContainer中的属性时,AnimatedContainer会经过一段动画效果,然后再完成相应的改变。在隐式动画中,你依然可以定义Curve和Duration等参数,但是你无法控制动画,即动画的执行和结束,是由属性改变来驱动的。


使用场景


Implicit Animations可以非常方便的使Widget具有动画效果而不需要写很多额外的动画代码,结合FutureBuilder或者StreamBuilder,甚至不用写setState,下面这个例子就演示了如何将Implicit Animations和FutureBuilder结合起来使用,代码如下所示。


FutureBuilder(
future: future,
builder: (context, snapshot) {
var width = .0;
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
width = .0;
break;
case ConnectionState.done:
width = 100.0;
break;
}
return AnimatedContainer(
width: width,
duration: Duration(seconds: 1),
curve: Curves.easeIn,
child: Image.asset('images/logo.png'),
);
},
),

通过FutureBuilder的各种状态回调,就可以设置不同的Widget,并在FutureBuilder完成并显示正常的Widget时,产生一个动画效果,而不是非常生硬的出现。


TweenAnimationBuilder


TweenAnimationBuilder是自定义隐式动画的方式,借助它,你可以给一个指定的Widget作用一个动画效果,一个简单的示例代码如下所示。


TweenAnimationBuilder(
tween: Tween<double>(begin: 0, end: 48),
onEnd: (){}
duration: Duration(seconds: 1),
builder: (BuildContext context, double size, Widget child) {
return IconButton(
iconSize: size,
color: Colors.blue,
icon: child,
);
},
child: Icon(Icons.aspect_ratio),
)

借助TweenAnimationBuilder,就可以将一个指定的Tween作用于builder中的Widget,builder中的第二个参数,就是Tween所指定的参数的类型,通过TweenAnimationBuilder,就可以在Widget参数变化的时候产生动画效果。



TweenAnimationBuilder的builder中如果有不变的Child Widget,可以放在TweenAnimationBuilder的child属性中,因为builder在产生动画时会重建,所有不变的Widget,都可以放在TweenAnimationBuilder的child中,再通过builder的第三个参数来传递这个Widget,以避免重建。



通常我们在开发中,会借助Transform来完成动画效果,在builder中,根据Tween返回的数值,使用不同的Transform来修改动画状态。



TweenAnimationBuilder中的begin,只在第一次使用,后面更新时,只看end的值,例如10-30,修改end为50,实际变化是30-50。如果不传begin,那么默认和end相等。



Explicit Animations——显示动画


与隐式动画不同,显示动画给了开发者对动画过程的完全掌控,开发者可以根据自己的需要来控制动画,Flutter中内置了很多显示动画,如下所示。
image.png



显示动画也有一个很明显的特点,那就是它们都以「Transition」结尾。



基本使用


以RotationTransition为例,下面来演示下如何使用Flutter中的显示动画。


显示动画是通过AnimationController来进行驱动的,所以,使用显示动画的第一步,就是需要创建AnimationController。有了AnimationController之后,就可以通过控制AnimationController的状态来控制动画的驱动过程,整个代码如下所示。


AnimationController controller;

@override
void initState() {
super.initState();
controller = AnimationController(vsync: this, duration: Duration(seconds: 2))..repeat();
}

@override
void dispose() {
controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () {
if (controller.isAnimating) {
controller.stop();
} else {
controller.repeat();
}
},
child: RotationTransition(
turns: controller,
child: FlutterLogo(
size: 100,
),
),
),
);
}

与隐式动画相比,显式动画通过AnimationController来获取动画的行进状态和参数,从而让调用者能够控制动画的行进过程。



显式动画可以实现隐式动画的所有功能,但是比隐式动画多了管理动画生命周期的工作



当Flutter内置显示动画不能满足开发者的需求时,Flutter提供了AnimatedBuilder和AnimatedWidget来让开发者对显示动画进行自定义。


AnimatedWidget


前面提到的都是Flutter中使用动画的最基本方式,但实际上,Flutter提供了很多关于动画的封装组件,可以让开发者更加方便的使用动画,这就是AnimatedWidget。AnimatedWidget也有很多实现类,如图所示。
image.png
AnimatedWidget是实现自定义显示动画的另一种方式,它可以将一些动画的逻辑以Widget的形式封装起来,从而让build函数中的代码逻辑更加清晰,下面是AnimatedWidget的示例代码。


@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
AnimWidget(animation: controller),
Center(child: FlutterLogo(size: 100)),
],
);
}

class AnimWidget extends AnimatedWidget {
const AnimWidget({
Key? key,
required Animation<double> animation,
}) : super(key: key, listenable: animation);

@override
Widget build(BuildContext context) {
Animation<double> animation = listenable as Animation<double>;
return Container(
decoration: BoxDecoration(
gradient: RadialGradient(
colors: const [Colors.red, Colors.transparent],
stops: [0, animation.value],
),
),
);
}
}

那么这种方式和之前直接使用AnimationController和Tween有什么区别呢?细心的读者可能已经发现了,AnimatedWidget不需要自己去监听动画的回调,也不需要通过setState来刷新动画,这些操作,AnimatedWidget已经封装好了,这就是AnimatedWidget的作用。


AnimatedBuilder


AnimatedBuilder是一个特殊的AnimatedWidget,它可以直接指定一个动画作用于Widget上,而不需要重新创建一个自定义的AnimatedWidget,它可以帮助开发者处理动画的监听,当一个Widget Tree中有一些需要动画的Widget,也有一些不需要动画的Widget时,用AnimatedBuilder可以很方便的避免非动画Widget的重绘,所以说,AnimatedBuilder可以更加方便的给一个Widget增加动画效果。


AnimatedBuilder与其它的显示动画一样,也是通过AnimationController驱动的,借助AnimatedBuilder,开发者可以根据需要,自己创建Animation并控制它,下面的代码演示了如何通过控制RadialGradient的stop属性来控制RadialGradient的显示大小,从而形成动画效果,代码如下所示。


@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (context, widget) {
return Stack(
children: <Widget>[
Container(
decoration: BoxDecoration(
gradient: RadialGradient(
colors: [Colors.red, Colors.transparent],
stops: [0, controller.value],
),
),
),
Center(child: FlutterLogo(size: 100))
],
);
},
);
}

上面的代码演示了如何使用AnimatedBuilder,实际上非常简单,与使用内置的显示动画的过程基本一致。


在使用AnimatedBuilder的过程中,需要尽可能多的将需要动画的部分和不需要动画的部分区分开来,这样可以避免多余的重绘,从而提高动画性能,例如上面的代码,可以将FlutterLogo和Stack放置在最外层,这样只需要让RadialGradient产生动画就可以了,代码如下所示。


@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
AnimatedBuilder(
animation: controller,
builder: (context, widget) {
return Container(
decoration: BoxDecoration(
gradient: RadialGradient(
colors: [Colors.red, Colors.transparent],
stops: [0, controller.value],
),
),
);
},
),
Center(child: FlutterLogo(size: 100))
],
);
}

AnimatedBuilder接收了一个animation,在child中,可以直接使用这个animation的值,其它都和普通的AnimatedWidget类似。


实际上,AnimatedBuilder就是AnimatedWidget的子类,所以在本质上,这两种实现自定义显示动画的方式想相同的,开发者可以根据自己的喜好来选择相应的方式来创建自己的显示动画。



AnimateWidget负责组件的抽离,可以看出组件中杂糅了动画逻辑。而AnimatedBuilder恰好相反,它不在意组件是什么,只是将动画抽离达到复用简单。



Flutter中的显示动画和隐式动画,几乎可以解决大部分我们平时在开发中遇到的动画场景,借助动画选择决策树,我们可以对动画的选择了如指掌,剩下的工作,就是对动画进行拆解,分而治之。


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

Android:实现一个自定义View扫描框

扫码功能都用过吧,打开扫码功能后都会有类似封面图的效果。其实就是一个自定义View的遮罩,话不多说,今天这篇我们就来讲解如何实现一个扫面框动效。 首先,我们先分析下动效的组成,有利于待会拆分实现: 四周类似角标的白线 角标框住的浅白色背景 一条由上而下由快到...
继续阅读 »

扫码功能都用过吧,打开扫码功能后都会有类似封面图的效果。其实就是一个自定义View的遮罩,话不多说,今天这篇我们就来讲解如何实现一个扫面框动效。


首先,我们先分析下动效的组成,有利于待会拆分实现:



  1. 四周类似角标的白线

  2. 角标框住的浅白色背景

  3. 一条由上而下由快到慢移动的扫描线


一经分析,其实非常简单,整体效果就是由这几个简单的元素组成。接下来我们就创建一个ScanView继承自View来实现这个动效。(由于代码古老,这里使用Java)


public final class ScanView extends View {

private Paint paint, scanLinePaint,reactPaint;//三种画笔
private Rect frame;//整个区域

public ScanView(Context context) {
this(context, null);
}

public ScanView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public ScanView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaint()
}

private void initPaint() {
/*遮罩画笔*/
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(color);
paint.setAlpha(CURRENT_POINT_OPACITY);
paint.setStyle(Paint.Style.FILL);

/*边框线画笔*/
reactPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
reactPaint.setColor(reactColor);
reactPaint.setStyle(Paint.Style.FILL);

/*扫描线画笔*/
scanLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
scanLinePaint.setStyle(Paint.Style.FILL);
scanLinePaint.setDither(true);
scanLinePaint.setColor(scanLineColor);
}
}

三种画笔初始化完成接下来就是使用画笔在画布上绘制效果图了,重写onDraw()方法


public void onDraw(Canvas canvas) {
//绘制取景边框
drawFrameBounds(canvas, frame);
//绘制遮罩
drawMaskView(canvas, frame);
//绘制扫描线
drawScanLight(canvas, frame);
}

再来分析,边框的四个角其实拆开来看,就是两条线组成,或者说是两个填充的矩形框垂直相交组成,那么四个角就可以按照这个思路完成,遮罩其实就是一个矩形框。


//绘制四个角,注意是外线而不是内线
private void drawFrameBounds(Canvas canvas, Rect frame) {
// 左上角
canvas.drawRect(frame.left - corWidth, frame.top, frame.left, frame.top + corLength, reactPaint);
canvas.drawRect(frame.left - corWidth, frame.top - corWidth, frame.left + corLength, frame.top, reactPaint);
// 右上角
canvas.drawRect(frame.right, frame.top, frame.right + corWidth,frame.top + corLength, reactPaint);
canvas.drawRect(frame.right - corLength, frame.top - corWidth, frame.right + corWidth, frame.top, reactPaint);
// 左下角
canvas.drawRect(frame.left - corWidth, frame.bottom - corLength,frame.left, frame.bottom, reactPaint);
canvas.drawRect(frame.left - corWidth, frame.bottom, frame.left + corLength, frame.bottom + corWidth, reactPaint);
// 右下角
canvas.drawRect(frame.right, frame.bottom - corLength, frame.right + corWidth, frame.bottom, reactPaint);
canvas.drawRect(frame.right - corLength, frame.bottom, frame.right + corWidth, frame.bottom + corWidth, reactPaint);
}

//绘制遮罩
private void drawMaskView(Canvas canvas, Rect frame) {
canvas.drawRect(frame.left, frame.top, frame.right, frame.bottom, paint);
}

到此,我们还剩最后一个扫描线的动画效果,这条线其实就是一张图片,首先需要将图片以Bitmap形式绘制在扫描区域内,然后通过ValueAnimator来控制图片Y坐标点,这样就能达到图片上下移动的效果,至于由快到慢的效果是添加了插值器,这里使用内置的DecelerateInterpolator,同学们可以根据自己想要的效果自己搭配。


scan_light.png


if (valueAnimator == null) {
valueAnimator = ValueAnimator.ofInt(frame.top, frame.bottom-10);//图片Y坐标取值范围
valueAnimator.setDuration(3000);//单次动画时间3秒
valueAnimator.setInterpolator(new DecelerateInterpolator());//由快到慢插值器
valueAnimator.setRepeatMode(ValueAnimator.RESTART);//重复动画
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);//无限次数
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
scanLineTop = (int) animation.getAnimatedValue();//当前时刻获取的Y值
invalidate();//刷新视图
}
});
valueAnimator.start();
}

到此就可以实现封面的效果,甚至可以添加别的酷炫效果,只要你敢想敢做。


总结


其实一些动效看似很复杂,但通过认真分析,我们可以将其拆分成多个简单的小块,将每个小块实现后再逐个拼装,最后达到完整的效果。本节主要是通过自定义View实现,用到绘制矩形框(drawRect),属性动画(ValueAnimator),两者使用也是非常简单。另外需要注意动画的使用和释放,避免导致不必要的内存泄漏。


好了,以上便是本篇所有内容,希望对大家有所帮助!


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

Kotlin Collection KTX:让你的集合操作如丝般顺滑

ktx
当今移动应用开发,常常需要使用各种集合类型来存储和操作数据。Kotlin 提供了 Collection KTX 扩展库,为我们操作集合提供了非常方便的 API。在本篇文章中,我们将介绍 Collection KTX 中包含的所有扩展函数,让你的集合操作变得更加...
继续阅读 »

当今移动应用开发,常常需要使用各种集合类型来存储和操作数据。Kotlin 提供了 Collection KTX 扩展库,为我们操作集合提供了非常方便的 API。在本篇文章中,我们将介绍 Collection KTX 中包含的所有扩展函数,让你的集合操作变得更加高效、简单、易读。


除了 Collection KTX,Kotlin 还提供了许多其他扩展库,例如 Android KTX、Coroutines、Serialization KTX 等,它们都可以大大简化我们的开发流程。在接下来的文章中,我们还将为您介绍这些扩展库的详细信息,让你的 Kotlin 开发之路更加畅通无阻


使用


dependencies {
    implementation "androidx.collection:collection-ktx:1.2.0"
}

用法合集


Collection 扩展函数



  • filterNot():过滤掉指定元素后的新 Collection。

  • filterNotNull():过滤掉 null 元素后的新 Collection。


List 扩展函数



  • sorted():按自然顺序排序后的新 List。

  • sortedBy():按指定方式排序后的新 List。

  • sortedDescending():按自然顺序降序排序后的新 List。

  • sortedByDescending():按指定方式降序排序后的新 List。

  • distinct():去重后的新 List。

  • distinctBy():按指定方式去重后的新 List。

  • minus():删除指定元素后的新 List。

  • plus():添加指定元素后的新 List。

  • drop():去掉前几个元素后的新 List。

  • dropWhile():去掉符合指定条件的元素后的新 List。

  • take():前几个元素组成的新 List。

  • takeWhile():符合指定条件的元素组成的新 List。

  • partition():按指定条件分隔后的 Pair。

  • groupBy():按指定方式分组后的 Map。

  • associate():按指定方式关联后的新 Map。

  • associateBy():按指定方式关联键后的新 Map。

  • associateWith():按指定方式关联值后的新 Map。

  • zip():按指定方式组合后的新 List。


MutableList 扩展函数



  • removeLast():移除最后一个元素,并返回该元素。

  • removeFirst():移除第一个元素,并返回该元素。

  • move():将指定元素移动到新位置。


Set 扩展函数



  • minus():删除指定元素后的新 Set。

  • plus():添加指定元素后的新 Set。

  • partition():按指定条件分隔后的 Pair。

  • groupBy():按指定方式分组后的 Map。

  • associate():按指定方式关联后的新 Map。

  • associateBy():按指定方式关联键后的新 Map。

  • associateWith():按指定方式关联值后的新 Map。


MutableSet 扩展函数



  • remove():移除指定元素,并返回是否移除成功。

  • retainAll():仅保留符合指定条件的元素。

  • addAll():添加指定元素后的新 MutableSet。


Map 扩展函数



  • minus():删除指定键对应的元素后的新 Map。

  • plus():添加指定键值对后的新 Map。

  • partition():按指定条件分隔后的 Pair。

  • filterKeys():按指定条件过滤键后的新 Map。

  • filterValues():按指定条件过滤值后的新 Map。

  • mapKeys():按指定方式映射键后的新 Map。

  • mapValues(): 按指定方式映射值后的新map


MutableMap 扩展函数



  • remove():移除指定键对应的元素,并返回该元素。

  • putAll():添加指定键值对后的新 MutableMap。

  • putIfAbsent():仅在指定键不存在时添加指定键值对。

  • compute():更新指定键对应的元素,并返回更新后的值。

  • computeIfAbsent():仅在指定键不存在时更新该键对应的元素。

  • computeIfPresent():仅在指定键存在时更新该键对应的元素。


Iterable 扩展函数



  • reduceOrNull():对所有元素进行指定操作,如果为 null 则返回 null。

  • reduceIndexedOrNull():对所有元素进行指定操作,同时考虑元素的索引,如果为 null 则返回 null。

  • foldOrNull():对所有元素进行指定操作并给定初始值,如果为 null 则返回 null。

  • foldIndexedOrNull():对所有元素进行指定操作并给定初始值,同时考虑元素的索引,如果为 null 则返回 null。


ListIterator 扩展函数



  • previousOrNull():返回上一个元素,如果不存在则返回 null。


Sequence 扩展函数



  • reduceOrNull():对所有元素进行指定操作,如果为 null 则返回 null。

  • reduceIndexedOrNull():对所有元素进行指定操作,同时考虑元素的索引,如果为 null 则返回 null。

  • foldOrNull():对所有元素进行指定操作并给定初始值,如果为 null 则返回 null。

  • foldIndexedOrNull():对所有元素进行指定操作并给定初始值,同时考虑元素的索引,如果为 null 则返回 null。

  • distinct():去重后的新 Sequence。

  • distinctBy():按指定方式去重后的新 Sequence。

  • filterNotNull():过滤掉 null 元素后的新 Sequence。

  • filterNot():过滤掉指定元素后的新 Sequence。

  • partition():按指定条件分隔后的 Pair。

  • sorted():按自然顺序排序后的新 Sequence。

  • sortedBy():按指定方式排序后的新 Sequence。

  • sortedDescending():按自然顺序降序排序后的新 Sequence。

  • sortedByDescending():按指定方式降序排序后的新 Sequence。

  • take():前几个元素组成的新 Sequence。

  • takeWhile():符合指定条件的元素组成的新 Sequence。

  • zip():按指定方式组合后的新 Sequence


了解工具的尿性


工具的职责就是提高工作效率



  1. 使用 Collection KTX 可以大大简化集合操作的代码,使代码更加简洁易读,同时可以提高代码的可维护性

  2. 在使用集合时,应该尽可能使用 Kotlin 标准库中的函数和 Collection KTX 中的扩展函数,而不是手写循环或通过 Java API 进行操作,这可以减少代码量和提高代码可读性。

  3. 了解不同的集合类型及其特性,选择合适的集合类型可以使代码更加高效。例如,如果需要频繁添加或删除元素,则应该使用可变集合类型。

  4. 避免频繁进行集合类型的转换,因为这会导致性能降低。如果需要对集合进行不同的操作,可以考虑使用不同的集合类型来解决。

  5. 尽量避免对空集合进行操作,因为这可能会导致空指针异常。在使用 Collection KTX 时,可以使用非空断言或者空安全操作符来处理可能为空的集合。


当然使用时也要注意,kotlin 的扩展函数让代码的可读性要求增高了少,所以用的使用为了能保证团队的统一,因该注意:



  1. 对于代码中的扩展函数,应该在函数名称中体现其作用,以便其他开发者更容易理解代码。例如,“find”函数可以改名为“findFirstOrNull”或“findLastOrNull”。

  2. 在使用 Collection KTX 时,应该注意性能问题。某些操作可能会导致性能下降,例如对大型集合进行循环和操作,因此应该考虑使用 Sequence 和 Flow 来提高性能。

  3. 避免重复操作。使用 Collection KTX 可以使代码更加简洁和易读,但是不应该过度使用,如果某个操作已经通过一个函数实现了,就不要再手动写同样的操作。

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

Flutter 知识集锦 | 基于 Flow 实现滑动显隐层

1. 前言最近要实现一个小需求,涵盖了很多知识点,比如手势、动画、布局等。挺有意思的,写出来和大家分享一下。如下所示,分为上下两层;当左右滑时,上层会随偏移量而平移,从而让上层产生滑动手势显隐的效果:标题这里上层通过不透明度 0.2 的蓝色示意,实际使用时可以...
继续阅读 »
1. 前言

最近要实现一个小需求,涵盖了很多知识点,比如手势、动画、布局等。挺有意思的,写出来和大家分享一下。如下所示,分为上下两层;当左右滑时,上层会随偏移量而平移,从而让上层产生滑动手势显隐的效果:

标题
88.gif89.gif

这里上层通过不透明度 0.2 的蓝色示意,实际使用时可以改为透明色。很多直播间的浮层就是这种交互逻辑,通过右滑来隐藏浮层。

直播右滑中
35f86de59086435083bf387a5efcc24.jpgc6c435e20ded4325899c6134f7be1d9.jpg

2. 实现思路

思路其实非常简单,监听横向滑动的手势事件,根据偏移量让上层组件进行偏移。当放手时,根据偏移量是否达到宽度的一半,使用动画进行移出或者关闭。

image.png

偏移的实现方式有很多,但需要自由地进行布局和矩阵变换、透明度,并且需要支持动画的变化,Flow 组件是一个非常不错的选择。 Flow 组件可以通过代理类对子组件进行自定义布局,灵活性极强;如果是 CustomPaint 是 绘制之王 可以绘制万物,那么 Flow 就是 布局之王,可以摆放万物。三年前写过一篇介绍 Flow 使用的文章: 《【Flutter高级玩法- Flow 】我的位置我做主》 。 本文就不对 Flow 的基础使用进行介绍了。


另外,在滑动过程中需要注意限制偏移量,使偏移量在 0~size.width 之内;当放手时,通过动画控制器来驱动动画,使用补间让偏移量运动到 0 (打开) 或 size.width(关闭) 。当关闭时,在右下角展示一个按钮用于点击展开:

image.png


3. 布局的代码实现

Flow 组件布局最重要的是实现 FlowDelegate,在其中的 paintChildren 方法中实现布局的逻辑。和 CustomPainter 类似,FlowDelegate 的实现类也可以通过 super 构造为 repaint 入参设置可监听对象。可监听对象的变化会触发 paintChildren 重新绘制:

SwipeFlowDelegate 实现类再构造时传入可监听对象 offsetX,在绘制索引为 1 的孩子时,通过 Matrix4 进行偏移。这样只要在手势水平滑动中,更新 offsetX 值即可。另外,可以根据 offsetX.value 是否达到 size.width 知道是否是关闭状态,如果已经关闭,绘制按钮。

class SwipeFlowDelegate extends FlowDelegate {
final ValueListenable<double> offsetX;

SwipeFlowDelegate(this.offsetX) : super(repaint: offsetX);

@override
void paintChildren(FlowPaintingContext context) {
Size size = context.size;
context.paintChild(0);
Matrix4 offsetM4 = Matrix4.translationValues(offsetX.value, 0, 0);
context.paintChild(1, transform: offsetM4);

// 偏移量对于父级尺寸
if (offsetX.value == size.width) {
Matrix4 m1 = Matrix4.translationValues(size.width / 2 - 30, size.height / 2 - 30, 0);
context.paintChild(2, transform: m1);
Matrix4 m2 = Matrix4.translationValues(size.width / 2 - 30, -(size.height / 2 - 50), 0);
context.paintChild(3, transform: m2);
}
}

@override
bool shouldRepaint(covariant SwipeFlowDelegate oldDelegate) {
return oldDelegate.offsetX.value != offsetX.value;
}
}

从这里可以看出,FlowDelegate 的最大优势是可以自定义孩子的绘制与否,还可以在绘制时通过 Matrix4 对孩子进行矩阵变换,还有可选参数可以控制透明度。接下来使用 Flow 组件时,提供 SwipeFlowDelegate ,并在 children 列表中依次放入子组件。其中前两个组件由外界传入,分别是底组件和上层组件,这样组件的布局就完成了,接下来监听事件,更新 factor 即可:

final ValueNotifier<double> factor = ValueNotifier(0);

Flow(
delegate: SwipeFlowDelegate(factor),
children: [
widget.content,
widget.overflow,
GestureDetector(
onTap: open,
child: const Icon(Icons.menu_open_outlined, color: Colors.white)),
GestureDetector(
onTap: () {
Navigator.of(context).pop();
},
child: const Icon(Icons.close, color: Colors.white))
],
)

4. 手势的监听

这里手势的处理是非常简单的,通过 GestureDetector 监听水平拖拽事件。在 onHorizontalDragUpdate 中根据拖拽的偏移量更新 factor 的值,其中通过 .clamp(0, widget.width) 可以限制偏移量的取值区间。

@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onHorizontalDragUpdate: _onHorizontalDragUpdate,
onHorizontalDragEnd: _onHorizontalDragEnd,
child: SizedBox(
height: MediaQuery.of(context).size.height,
width: widget.width,
child: Flow( delegate:// 同上,略...
);
}

void _onHorizontalDragUpdate(DragUpdateDetails details) {
double cur = factor.value + details.delta.dx;
factor.value = cur.clamp(0, widget.width);
}

void _onHorizontalDragEnd(DragEndDetails details) {
if (factor.value > widget.width / 2) {
close();
} else {
open();
}
}

最后在 _onHorizontalDragEnd 回调中,根据当前偏移量是否大于一般宽度,决定关闭还是打开。期间过程使用动画进行偏移量的过渡变化。


5. 动画的使用

动画的使用,主要是通过 AnimationController 动画控制器来驱动数值的变化;在放手时 Tween 创建补间动画器,监听动画器数值的变化更新偏移量。这样偏移量就可以在指定时间内,在两个值之间渐变,从而产生动画效果。比如抬手时,open 方法是让偏移量从当前位置变化到 0 :

class _ScrollHideWrapperState extends State<ScrollHideWrapper> with SingleTickerProviderStateMixin {
late AnimationController _ctrl;

final ValueNotifier<double> factor = ValueNotifier(0);

@override
void initState() {
super.initState();
_ctrl = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
}

@override
Widget build(BuildContext context) {
// 略同...
}

// 动画关闭
Future<void> close() async {
Animation<double> anim = Tween<double>(begin: factor.value, end: widget.width).animate(_ctrl);
anim.addListener(() => factor.value = anim.value);
await _ctrl.forward(from: 0);
}

// 动画打开
Future<void> open() async {
Animation<double> anim = Tween<double>(begin: factor.value, end: 0).animate(_ctrl);
anim.addListener(() => factor.value = anim.value);
await _ctrl.forward(from: 0);
}
}

如果想让动画的变化非匀速,可以使用 Curve 来控制动画曲线。这样,基于 Flow 实现的自定义布局,就可以根据手势和动画,完成特定的交互功能。从这里可以看出 Flow 自定义布局的灵活性非常强,很多疑难杂症,都可以使用它来完成。

比如企业微信中:侧滑展示左栏,而且上层不会全部消失,通过 Flow 来自定义布局就很容易实现。大家可以基于本文,自己实现一下作为练习。那本文就到这里,谢谢观看 ~

标题关闭
90.gif91.gif


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

从零打造现代化绘图框架 Plait

web
我司大概从今年(2022年)年初决定思维导图,经过半年多的研究与实践,我们基于自研的绘图框架初步完成了一个脑图组件并成功集成到我们 PingCode Wiki 中,这篇文章主要探讨下这个绘图框架(Plait)的一些设计点和思维导图落地的一些关键技术。 概论 对...
继续阅读 »

我司大概从今年(2022年)年初决定思维导图,经过半年多的研究与实践,我们基于自研的绘图框架初步完成了一个脑图组件并成功集成到我们 PingCode Wiki 中,这篇文章主要探讨下这个绘图框架(Plait)的一些设计点和思维导图落地的一些关键技术。


概论


对于思维导图、流程图前期我们做了很多调研工作,流程图方向我们研究了 excalidraw 和 react-flow,它们都是基于 react 框架实现的库,在社区中有很高的知名度,思维导图方向我们研究了 mind-elixir、 mindmap-layouts (自动布局算法),在开源领域中思维导图发展不是很好,没有成熟、知名的作品。


mind-exlixir 介绍:



mind-elixir 功能示意图


优点:



  1. 麻雀虽小但五脏俱全

  2. 纯 JS 库、轻量


缺点:



  1. 不依赖前端框架、开发方式和主流的方式不同

  2. 架构设计没有太多可取之处,节点布局方式不易扩展


虽然我们前期的目标是研发 「思维导图」 ,但是最终我们的产品目标应该是做一个 「一体化的交互式绘图画板」 ,包含思维导图、流程图、自由画笔等。


最终调研发现目前开源社区恰恰缺少这样一个一体化的绘图框架,用于实现一体化的交互式绘图编辑器,集思维导图、流程图、自由画笔于一体,所以我们结合做富文本编辑器的经验,重新架构了一套绘图框架(Plait)、拥有插件机制,并在它的基础上实现思维导图插件、落地到 PingCode Wiki 产品中,所以今天分享的主角就是 Plait 框架。


下面正式进入今天的主题,分为四部分:



  1. 绘图框架设计

  2. 思维导图整体方案

  3. 思维导图自动布局算法

  4. 框架历程/未来


一、绘图框架设计


这部分首先会先简单介绍下绘图方案的选型(Canvas vs SVG)考量,然后介绍下 Plait 绘图框架中核心部分的设计:插件机制、数据管理,最后介绍下框架优势。


绘图方案:Canvas vs SVG


Canvas 还是 SVG 其实社区中也没有一个明确的答案,我们参考了一些知名产品的方案选型,SVG 和 Canvas 都有并且实现的效果都不差,比如语雀的白板使用的是 SVG,ProcessOn 使用的是 Canvas,Excalidraw 使用的是 Canvas,drawio 使用的是 SVG 等等。因为我们没有 Canvas 的使用经验,加上我们的思维导图节点希望支持富文本内容,所以暂时选定对 DOM 更友好的 SVG,觉得先按照这个方案试试水。


经过这么长时间的验证,发现基于 SVG 的方案并没有什么明显的不足,性能问题我们也经过验证,支持 1000+ 的思维导图节点渲染完全没有问题、操作依然很流畅。



对于 SVG 绘制我们没有直接使用 SVG 的底层API ,而是使用了一个第三方的绘图库 roughjs。



下面我们看看 Plait 框架「插件机制」的部分,这部分的灵感来源于富文本编辑器框架 Slate。


插件机制


Web 前端的画图领域有很多可以深度研发的业务方向,如何基于同一个框架实现不同业务方向功能开发,就需要用到插件机制了。


插件机制是 Plait 框架一个重要特性,框架底层并不提供具体的业务功能实现,业务功能都需要基于插件机制实现,如下图所示:



插件机制通俗讲就是框架层构建的一座基础桥梁,为实现具体的业务功能提供必要的支持,Plait 插件机制有三个核心要素:



  1. 抽象数据范式(插件数据)

  2. 可复写行为(识别交互)

  3. 可复写渲染(控制渲染)


具体到流程图、思维导图这类绘图场景,它的核心是基于用户交互行为(鼠标、键盘操作)实现符合交互预期的元素绘制、渲染,如果做成可扩展的那就插件开发者可以自定义交互行为、自定义节点元素渲染,基于自定义交互生成插件数据,基于插件数据控制插件元素渲染,构成插件闭环,如下图所示(插件机制闭环示意图):




这部分的核心就是设计可重写方法,目前 Plait 中主要有两类:

第一类用于实现自定义交互:mousedown、mouseup、mousemove、keydow、keyup。

第二类用于实现自定义渲染:drawElement、redrawElement、destroyElement 然后就是框架层与插件衔接部分的设计了,这一部分在 plait/core 中目前被设计的是比较松散的,drawElement 可以返回一个 SVGGElement 类型的 DOM 元素也可以返回一个框架组件,既可以直接衔接框架也可以基于 DOM 的方式对接。

目前 Plait 框架整个是基于 Angular 框架实现的,后续可能会考虑脱离框架的设计模式,这不是本文的重点。



举个例子: 画圆插件三步走



步骤一:定义数据结构


export interface CircleElement {
type: 'cirle';
radius: number;
dot: [x: number, y: number];
}

步骤二:处理画圆交互


board.mousedown = (event: MouseEvent) => {
if (board.cursor === 'circle') {
start = toPoint(event.x, event.y, board.host);
return;
}
mousedown(event);
};
board.mousemove = (event: MouseEvent) => {
if (start) {
end = toPoint(event.x, event.y, board.host);
if (board.cursor === 'circle') {
// fake draw circle
}
return;
}
mousemove(event);
};
board.globalMouseup = (event: MouseEvent) => {
globalMouseup(event);
if (start) {
end = toPoint(event.x, event.y, board.host);
const radius = Math.hypot(end[0] - start[0], end[1] - start[1]);
const circleElement = { type: 'circle', dot: start, radius };
Transforms.insertNode(board, circleElement, [0]);
}
};

步骤三:实现画圆方法


board.drawElement = (context) => {
if (context.elementInstance.element.type === 'circle') {
const roughSVG = HOST_TO_ROUGH_SVG.get(board.host) as RoughSVG;
const circle = context.elementInstance.element as unknown as CircleElement;
roughSVG.circle(circle.dot[0], circle.dot[1], circle.radius);
}
return drawElement(context);
}

这是一个最简单的插件示例,通过框架提供的桥梁实现一个画圆插件:拖放画一个圆 -> 生成对应圆数据 -> 根据数据渲圆。


插件机制大概就是这些内容,下面看看数据管理部分。


数据管理


数据管理是 Plait 框架中非常重要的部分,它是框架的灵魂,前面的插件机制是外在表现,主要包含以下特性:



  1. 提供基础数据模型

  2. 提供基于原子的数据变化方法(Transfroms)

  3. 基于不可变数据模型(基于 Immer)

  4. 提供 Change 机制,与框架配合完成数据驱动渲染

  5. 与插件机制融合,数据修改的过程可以被拦截处理


这些都是非常优秀的特性,既可以完成数据的约束,又可以灵活实现很多复杂高级的需求,感觉这块的设计和实现其实可以算是一种特定场景的状态管理。


框架状态流转图:



上面说到的插件机制的闭环要依赖数据模型状态作为的标准,最终的插件闭环如下图所示:



这里可以列举两个具体的业务场景,都是我们开发中经常落入的陷阱,体现数据管理在的约束作用及灵活性(这部分可能不好理解,谨慎阅读,其实也是框架作用的具体说明):


场景 1: 自动选中根节点


下面这张图是一个需求示意:新建脑图时自动选中根节点并弹出工具栏



新建思维导图自动弹出工具栏示需求意图


这是一个合理的需求,但它不是常规的交互路径(常规路径是用户点击节点,触发节点选中,进而触发工具栏弹出),我们的新同学在最开始的时候就选择了一种不标准的数据修改方式(手动修改数据)去完成这个需求。


常规点选:




框架数据会存储一个选区状态(点击位置或者框选位置),点击操作会触发选区数据变化,选区变化会触发 Change 事件,基于这个机制处理节点选中和工具栏弹出。



自动选中(手动修改数据):



不推荐的操作路径示意图



这里的思路是首先模拟位置(根据自动选择的节点计算),手动修改数据,然后自己手动调用工具栏的弹出、强制刷新界面让节点选中,这就是典型的没有按照框架约束实现需求的例子,前面说到的数据流没有正确运转,需要做很多特殊处理。 不通过 Transfrom 的方式手动修改数据是不被框架允许的,不会触发框架 Change 行为,理论上应该直接抛出异常(很可惜当时没有做到这步)。



自动选中(标准路径):



标准路径就是基于模拟位置通过 Transfrom 的方式修改数据(相当于模拟点击了要自动选中的节点),后面的流程就可以依赖框架机制去控制执行,无须再做很多手动的处理。


场景 2: 思维导图节点删除后节点的选中状态自动切换到临近节点


这是一个很基础的需求,目前我们的实现是拦截节点删除行为(按 Delete/Backspace 键)处理,这样做有两个弊端:



  1. 假如将来要做右键菜单删除需要把这部分的处理代码再写一遍,即使封装成工具方法,也要额外增加一个调用入口。

  2. 一个不太好解决的问题,新建一个节点后按 Ctrl + Z 撤回,无法把选中状态转移到临近节点上,虽然这里撤回执行的也是节点删除操作。


推荐路径:


拦截节点删除操作,前面提到框架统一了数据修改的方法,所以插件开发者可以对数据修改过程进行拦截,这个拦截过程可以在数据修改前,也可以在在数据修改后(Change),在这个地方做出就不会有任何漏掉的场景。


框架作用



  1. 插件机制实现分层

  2. 数据管理实现约束

  3. 配合框架规范数据流


框架最大的意义就是分层解构,降低业务的复杂度,每个领域或者每个模块只处理自己的事情。



比如前端框架,组件化开发,就是能够把一定的逻辑归拢到一个逻辑域中(组件),而不是所有东西杂糅在一起,这是架构演进的趋势。



架构图:



二、思维导图整体方案


这里简单介绍下思维导图插件的整体技术方案,但是不会介绍特别细,因为它是基于 Plait 框架实现,大的方案肯定是与 Plait 框架一致。



思维导图整体技术方案导图


1、整体方案


我们整体是 SVG + Richtext 的方案。


绘图使用 SVG ,目前我们脑图节点、节点连线、展开收起图标等等都是基于 SVG 绘制的。


节点内容使用 foreignObject 包括嵌入到 SVG 中,这种方案使节点内容支持富文本


2、功能方案


脑图核心交互处理及渲染都是可重写方法完成,与 Plait 集成。


脑图插件仅仅负责脑图部分的渲染,至于整个画布的渲染以及画布的移动放大缩小等等是框架底层功能。


因为 Plait 是支持扩展任意元素的,支持渲染多个元素,它只由数据决定,所以它支持同时渲染多个脑图。


底层脑图插件并不包含工具栏实现,它只处理核心交互、渲染、布局。


3、组件化


脑图组件渲染、节点渲染的整体机制就是我们前端经常提到的:组件化、数据驱动,虽然组件内部节点渲染还是创建 DOM、销毁 DOM,但是大的功能还是通过组件来进行划分的。


基于脑图业务里面有两个非常重要的组件:MindmapComponent 、MindmapNodeComponent,MindmapComponent 处理脑图整体的逻辑,比如执行节点布局算法,MindmapNodeComponent 处理某一个节点的逻辑,比如节点绘制、连线绘制、节点主题绘制等。


之所以把这个部分提出来说一下,是因为我觉得这块的思想其实是主流前端框架思想的延续,包括和 Plait 框架整体的机制是统一的。


4、绘图编辑器


这里可以理解为业务层的封装,业务层级决定集成那些扩展插件,以及进一步扩展插件上层功(比如脑图节点工具栏实现),Mindmap 插件层不依赖于我们的组件库和业务组件,所以工具栏这类需要组件库组件的场景统一放到业务层实现,这样 Mindmap 插件层可以减少依赖、保持聚焦。


思维导图具体落地到 PingCode Wiki 业务中,其实有一个更虽复杂、但清晰的分层结构:



三、自动布局算法


节点自动布局是思维导图的一个核心技术,它是思维导图美观以及内容表现力的决定性因素,它关注节点如何分布,这部分说复杂不复杂,说简单也不简单,包含以下几个部分:



  1. 布局分类

  2. 节点抽象

  3. 算法过程

  4. 方向变换

  5. 布局嵌套


布局分类


介绍说明下常规思维导图的布局分类:



示意图


标准布局:



逻辑布局:



缩进布局:



时间线:



鱼骨图:



美学标准


前面说过思维导图对可视化树的展现有很高的要求,需要它是美观(这个就很直观,每个人的审美可能不一样,但是它也应当有一些基础标准)的,所以需要基础的美学标准:



  1. 节点不重叠

  2. 子节点按照指定的顺序排列

  3. 父节点在子节点中心(逻辑布局)

  4. 主轴方向上不同层级节点不重叠


节点抽象


为了简化可视化树的绘制,[Reingold-Tilford] 提出可以把节点之间的间距和绘制连线抽象出来。通过在节点的宽度和高度上添加间隙来添加节点之间的间距。如下图中的实线框显示了原始宽度和高度,虚线框显示了添加间隙后的宽度和高度。



[Reingold-Tilford] 可视化树节点抽象示意图


我们的思维导图自动布局遵循这个抽象:



  1. 节点布局本身不关注节点连线,只关注节点的宽高和间距

  2. 节点从间距中抽象出来(节点宽高和节点间隙作抽象为一个虚拟节点 LayoutNode)

  3. 布局算法基于 LayoutNode 进行布局


节点在布局时它的宽和高已经融合了实际宽高和上下左右的间隙了,这样可以降低自动布局的复杂度,上图其实是我们布局后的结果,节点从间距中抽象出来之后,节点的垂直顶部位置是其父节点的底部坐标,而父节点的底部坐标又是其顶部坐标加上其高度,真实节点与虚拟节点的逻辑关系如下图所示:



LayoutNode 示意图


算法执行过程


算法流程图:



自动布局算法执行流程图


用一个包含三个节点的例子介绍它自动布局过程,理想的结果应当如下图所示:



步骤一、前置操作: 构造 LayoutNode


这个就是前面提到的节点抽象,基于节点宽高和节点之间的间隙构建布局使用的抽象节点,此时三个处于初始状态,x、y 坐标均为零且相互重叠,如下图所示:



初始状态示意图



左边是真实状态,右侧虚线框部分没有特别的意义,只是一个不重叠的示意



步骤二、布局准备: 垂直分离



布局准备:垂直分离示意图


基于节点的父子级关系进行分层,保证垂直方向是父子级节点不重叠(节点 0与节点 1、2不重叠)。


步骤三:分离兄弟节点



分离兄弟节点过程示意图


就是分离「节点 1」和「节点 2」,保证他们水平不重叠。


步骤四:定位父节点



父级节点定位示意图


基于「节点 1」和「节点 2」重新地位父节点「节点 0」的水平位置,保证父节点水平方向上居中与「节点 1」 和「节点 2」。


布局结果:



以上就是一个的完整布局过程(逻辑下布局),逻辑并不复杂,即使多一些层级和节点也只需要递归执行「步骤三」和「步骤四」。


可以看出「逻辑下布局」只用了「算法流程图」中的前四步就完成了,最后一步「方向变换」就是在「逻辑下布局」的基础上通过数学变换的方式实现「逻辑上」「逻辑右」等布局,下面对方向变换进行专门的解释。


方向变换


1、逻辑下 -> 逻辑上



可以看出这是垂直方向上的变换关系,它们应该是基于一个水平线对称,具体的变换关系如下图所示:



逻辑上变换图


可以看最右侧最下方的节点的「y 点」应该就对应的最右侧最上方的节点「y点」,它们的位置关系应该就是:y= y - (y-yBase) * 2 - node.height。



注意上下变换应该只涉及位移,不涉及上下翻转,也就是节点内部的方向不变,y 对应 y`这两个对应的都是节点的下边上的点位。



2、逻辑下 -> 逻辑右



逻辑右示意图


从上图可以看出,这个逻辑变换也不复杂:就是一个垂直到水平的变换过程,反应到布局算法层中 x、y 方向以及节点宽高的变换,比如:



  1. 垂直分层:需要将垂直分层变换为水平分层

  2. 增加 buildTee 过程:基于分层的节点需要将节点宽度变换高度、x 坐标变为 y 坐标



处理水平布局:增加 buildTree 过程示意图


最后在「方向变换」中将宽高和 x、y 再变换回来:



得到布局结果:



3、逻辑右 -> 逻辑左


逻辑右到逻辑左的位置对应关系应该和最上面说逻辑下到逻辑上的类似,这里不再赘述。


方向变换大概就这三种,下面介绍下下布局嵌套的思路。


布局嵌套


先看一个布局嵌套的示意:



上图第二个子节点使用了另外一种布局(缩进布局),这就属于布局嵌套,布局嵌套仍然需要保证前面说到的「美学标准」比如节点不重叠、父节点居中对齐等。


简单思考: 布局嵌套中的那个有独立布局的子树,它对于整体布局的影响在于它的延伸方向的不受控制,但是如果把有独立布局的子树看做一个整体,提前计算出子树的布局,然后把子树作为整体代入布局算法就可以屏蔽子树延伸方式对整体布局的影响。


整体的处理思路如下图所示:



布局嵌套处理思路示意图


这里可以有一个抽象:把有独立布局的子节点抽象成一个黑盒子(我把它叫做 BlackNode),那么子树布局的影响就会被带入到主布局中,而子树的布局可以保持独立性。



关键点:需要先计算出有独立布局的子树的布局,然后才可以计算父节点布局



四、框架历程/未来


从技术调研到架构设想再到架构落地到产品中,历时大概1年左右的时间,核心工作集中在 2022 年的 1-9 月份,大概的时间线如下:



Plait 框架未来的一些设想



结束语


本文主要介绍从零开始做画图应用、自研画图框架、落地思维导图场景的一些技术方案,作为一个 Web 前端开发者有机会做这样的东西个人感觉很幸运,对于 Plait 框架未来还有很多事情要做,希望它可以发展成为一个成熟的开源社区作品,也期待对画图框架有兴趣的同学可以加入到 Plait 的开源建设中。


作者:pubuzhixing
来源:juejin.cn/post/7205604505647988793
收起阅读 »

简述html2canvas遇见的坑点及解决方案

web
前言 大家好,最近公司在做公众号的海报图生成功能,功能不难,但是其中也遇到了一些坑还有一些细节的问题,这里给大家复盘一下,相互借鉴及学习 制作海报选用工具 这里我看了几款生成图片的工具: html2canvas dom-to-image 这里我选用的是ht...
继续阅读 »

前言


大家好,最近公司在做公众号的海报图生成功能,功能不难,但是其中也遇到了一些坑还有一些细节的问题,这里给大家复盘一下,相互借鉴及学习


制作海报选用工具


这里我看了几款生成图片的工具:



这里我选用的是html2canvas,因为大部分人使用这个比较多,而且我也只听过这个🤣,另一个大家可以去自行摸索,毕竟我看github上也有9k的star


image.png


开始使用插件生成


引入插件


// npm 下载插件
npm install html2canvas
// 项目引入插件
import html2canvas from 'html2canvas';

html2canvas的option配置


属性名默认值描述
allowTaintfalse是否允许跨域图像。会污染画布,导致无法使用canvas.toDataURL 方法
backgroundColor#ffffff画布背景色(如果未在DOM中指定),设置null为透明
canvasnull现有canvas元素用作绘图的基础
foreignObjectRenderingfalse如果浏览器支持,是否使用ForeignObject渲染
imageTimeout15000加载图像的超时时间(以毫秒为单位),设置0为禁用超时
ignoreElements(element) => false谓词功能,可从渲染中删除匹配的元素
loggingtrue启用日志以进行调试
onclonenull克隆文档以进行渲染时调用的回调函数可用于修改将要渲染的内容,而不会影响原始源文档
proxynull代理将用于加载跨域图像的网址。如果保留为空,则不会加载跨域图像
removeContainertrue是否清除html2canvas临时创建的克隆DOM元素
scalewindow.devicePixelRatio用于渲染的比例。默认为浏览器设备像素比率
useCORSfalse是否尝试使用CORS从服务器加载图像
widthElement widthcanvas的宽度
heightElement heightcanvas的高度
xElement x-offset裁剪画布X坐标
yElement y-offset裁剪画布X坐标
scrollXElement scrollX渲染元素时要使用的x滚动位置(例如,如果Element使用position: fixed)
scrollXElement scrollY呈现元素时要使用的y-scroll位置(例如,如果Element使用position: fixed)
windowWidthWindow.innerWidth渲染时使用的窗口宽度Element,这可能会影响媒体查询之类的内容
windowHeightWindow.innerHeight渲染时要使用的窗口高度Element,这可能会影响媒体查询之类的内容

调用html2canvas时传入两个参数,第一个参数是dom节点,第二个参数是options配置项(配置项可根据上方表格进行对应配置)


html2canvas(document.body).then(function(canvas) {
document.body.appendChild(canvas);
});

调用方法生成海报


1.获取节点:let img = document.querySelector("#myImg");


2.配置需要参数:


let options = {
useCORS: true,// 开启跨域
backgroundColor: "#caddff",// 背景色
ignoreElements: (ele) => {},// dom节点
scale: 4,// 渲染出来的比例
};

3.调用方法


html2canvas(img, options).then((canvas) => {
let url = canvas.toDataURL("image/png"); // canvas转png(base64)
this.url = url;
this.isShow = false;
});

到这里html2canvas的相关使用及配置就介绍完了,接下来就是遇见的问题


使用时遇见的坑点及解决方案


图片跨域问题


第一次用我就遇见了这个问题,第一个就是百度的方法,配置useCORS: true,// 开启跨域,然后图片标签上加crossorigin="anonymous",但是结果没用,图片依旧跨域,这时候咱们前端就要硬气一点,直接让后端处理,让后端把图片地址改成base64的形式传给你,或者服务器配置跨域


生成海报时图片模糊问题


生成海报如果模糊,建议把配置项的scale配置高一点,生成的canvas图片把盒子固定大小,显示的图片就更清晰


dom之间有一道横杠


本人是在公众号上做生成海报功能,dom元素顶部是两张图片,图片顶部有一道白线,而且两张图片之间还有一道杠(不好形容),后面发现是因为生成这个海报我在公众号上用的是image标签,改成img标签就没用影响了,具体原因应该是uniapp内部处理image标签时的一些样式问题吧,这是我的猜测


注意:app上不支持html2canvas生成海报(我也是调试的时候发现的)


全部代码


这里代码仅供大家参考


<template>
<view class="poster-content">
<view class="poster-img" id="myImg" v-if="isShow">
<img class="flow" src="../../static/QC-code.png" />
<view class="card-item">
<view class="title-card">爽卡优势</view>
<view class="tip-content">
<view class="left">
<p>零月租,随充随用,不用不扣费</p>
<p>全程4G、不限APP、不限速</p>
<p>支持多场景使用</p>
<p>官方正品、品质保证</p>
</view>
<view class="right">
<view class="right-item">
<view class="qr-code">
<img
id="codeImg"
:src="imgUrl"
style="width: 100%; height: 100%"
class="flow"
crossorigin="anonymous"
/>

</view>
<view style="color: #0032d0">扫码免费领取</view>
</view>
</view>
</view>
</view>
</view>
<view v-else class="canvas-img">
<img style="width: 100vw" :src="url" alt="" />
</view>
<view v-if="isShow" style="padding-bottom: 10px">
<view @click="getImage" class="createPoster">点击生成海报</view>
</view>
<view style="padding-bottom: 50px">
<view
@click="close"
class="createPoster"
style="background-color: #fff; color: #4f80e6"
>
关闭</view
>
</view>
</view>
</template>


<script>
import html2canvas from "html2canvas";
export default {
props: {
imgUrl: {
type: String,
default: "",
},
hasQrCode: {
type: Boolean,
default: false,
},
},
data() {
return {
url: "",
isShow: true,
};
},
onShow() {},
methods: {
close() {
this.$emit("closePop");
},
getImage() {
// this.saveImg()
this.saveImg();
},
saveImg() {
let img = document.querySelector("#myImg");
let options = {
useCORS: true,
backgroundColor: "#caddff",
ignoreElements: (ele) => {},
scale: 4,
};
html2canvas(img, options).then((canvas) => {
let url = canvas.toDataURL("image/png"); // canvas转png(base64)
this.url = url;
this.isShow = false;
});
},
},
};
</script>


<style lang="scss" scoped>
.poster-content {
width: 100vw;
background-color: #caddff;
height: calc(100vh - 50px);
overflow: scroll;
margin-top: -50px;
}
.poster-img {
width: 80vw;
margin: 0 auto;
background-color: #caddff;
// display: flex;
// flex-direction: column;
padding: 40upx 20upx;
text-align: center;
.title {
width: 203px;
height: 96px;
}
.flow {
width: 100%;
height: 300px;
}
.card-item {
background-color: #fff;
font-size: 14px;
margin-top: -12upx;
padding: 20upx 0 40upx;
text-align-last: left;
border-radius: 20upx;
.title-card {
color: #0032d0;
font-size: 36upx;
font-weight: 700;
padding-left: 10upx;
}
.tip-content {
display: flex;
justify-content: space-between;
font-size: 26upx;
.left {
flex: 1;
margin-top: 10upx;
& > p {
line-height: 1.5em;
margin-top: 20upx;
padding-left: 40upx;
position: relative;
&::after {
content: "";
position: absolute;
top: calc(50% - 10upx);
left: 8upx;
width: 20upx;
height: 20upx;
border-radius: 20upx;
background-color: #0256ff;
}
}
}
.right {
width: 90px;
font-size: 20upx;
display: flex;
align-items: center;
padding-right: 16upx;
.right-item {
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
// height: 143px;
.qr-code {
width: 160upx;
height: 160upx;
background-color: #fff;
border-radius: 10upx;
display: flex;
justify-content: center;
align-items: center;
}
}
}
}
}
.canvas-img {
width: 100vw;
height: calc(100vh - 50px);
overflow: scroll;
}
}
.createPoster {
line-height: 2.8em;
width: 90%;
background-color: #5479f7;
margin: auto;
border-radius: 40upx;
color: #fff;
text-align: center;
}
</style>


结尾


这些就是本人在做海报功能所遇见的一些问题及解决方案,希望掘友们相互学习共同进步,如果有什么描述错误的地方希望给我指正,欢迎大家跟我一起交流


作者:一骑绝尘蛙
来源:juejin.cn/post/7168667322956120101
收起阅读 »

不惑之年谈中年危机

web
今年正式进入不惑之年。按 2011 年统计数据,国内 40 岁以上的程序员约是 1.5%,乐观来看,我进入了前 5% 的群体。 美国 2016 年此比例已经有 12.6%,大家还是应该乐观点。 大家都知道的国内第一代程序员,求伯君/雷军已经退休或做 CEO 了...
继续阅读 »

今年正式进入不惑之年。按 2011 年统计数据,国内 40 岁以上的程序员约是 1.5%,乐观来看,我进入了前 5% 的群体。


美国 2016 年此比例已经有 12.6%,大家还是应该乐观点。


大家都知道的国内第一代程序员,求伯君/雷军已经退休或做 CEO 了,而目前还在活跃的骨灰级程序员有陈皓(左耳朵耗子)。估计之后应该也会有越来越多的老程序员,或者说是目前活跃的程序员变成老程序员,出现在大家的视野。


骨灰级程序员:陈皓(左耳朵耗子)


程序员这个职业发展起来也就 20 年,还是一个很年轻的职业,年龄焦虑这个事情着实没有必要。


换位思考一下,如果我是公司的技术招聘者,15年内工作经验的都是大有前途的,只要你技术过得去,价格合理,我都愿意招你。


相反,15年以上的程序员就没那么受欢迎了。


因为初入职场就是互联网的蓬勃发展期,人才短缺是一直存在的,知识积累不够不要紧,个人品行不妥也不要紧,只要你敢于承担一些压力,那你的职业道路都会比较顺畅。如果再幸运一些,就能进入大厂。


升职加薪,迎娶白富美,走上人生巅峰


但是,这也造成了一些不良的现象,在这行业,投机份子其实挺多的。什么热门就跟风炒一炒,能不能做成不要紧,最主要是自己的 KPI 好看。另外,互联网企业的优待,让他们多少有些娇气。从大厂出来,薪资待遇要翻倍,要股票要期权,要好资源要好项目。


所以,一般的公司未必能容纳这些人。他们也未必愿意去这些公司。于是,35岁危机就来了。


35岁危机


年轻人其实是很难感受到中年危机的,中年危机与年轻失业的区别就像新冠与感冒的区别。你以为你经历了中年危机,实际上只是年轻失业。


今年经济不景气,裁员潮估计让一部分人离开了这个行业。年纪大的想要回炉再造,是很难的。但是,如果你还年轻,平时多积累,我相信能很轻松地找到一份工作的。


如何面对危机?


年轻人都会说苟住,换个正能量的说法是活在当下。


读好书、做好事,是能切切实实忘掉焦虑的。


今年读的《反脆弱》、《心流》和《毫无意义的工作》,将这三本书放在一起,还是很有意思的,能看到不同的观点:


jpg.jpg




  • 《反脆弱》让我对工作也有了新认识。看似很稳定的工作,会有可能让你过度依赖,如果遭遇失业就手足无措了。而类似的士司机,饿一餐饱一顿的,反而平时就很有充足的经验应对收入不稳定的情况。


    今年的形势让人更趋于进入大公司、国企、公务员单位,然后这些稳定的工作就真的这么值得大家去追随吗?越是追求稳定,最后是否会适得其反?




  • 《毫无意义的工作》今年敲醒了不少人,他提醒我们,日常琐碎的工作中消磨了我们的生命。然而这本书更多是情绪的宣泄,并没有什么好的解决方法。




  • 而《心流》则希望我们投入去做事情,只有投入了才会获得心流,才会有幸福感。同时,它让我意识到,无法逃避的事实是,工作占了我们生命的 1/4 时间。如果无法从中获得乐趣,那我们的人生注定是悲剧的。




这几本书都让我重新思考我与工作的关系,即使今年外部恶劣的情况,我们也应该重拾自身的信心,重新找回我们的热情、专注。


在面对人员缩减,项目被砍的情况下,我们也许可以把目光放在现有的项目上。


就前端而言,SSR 做不了,Docker 做不了,那就看看 nginx 缓存优化能不能做;低代码做不了,那就看看页面模板能不能做;开源做不了,就看看公共组件能不能做;什么都做不了,那首屏优化,静态资源优化,图片压缩也是能做的,而且还能做得很深。


R-C (1).jpg


只要你想,总有做不完的事情。并且这些事情,其实是我们要还的技术债务。


而此时也是做好技术积累的时机。


面向对象、设计模式、函数式编程、类型编程、异步编程这些基础都可以恶补一下;网络安全、网络通信、内存、CPU等等向外延伸的各类计算机知识也是我们必须掌握的。


工作上认真对待自己的每一行代码,生活中认真对待自己的每一分钱。


深挖知识,深入研究,懂得越多,焦虑自然就越少。


有足够的知识与经验,你的中年危机也许永远不会来


最后,对于年龄的焦虑,再推荐大家看看方励老师在“一席”的演讲《即使是像我这把年龄的人,好奇心也从来没变过,因为我们还活在人间的》


作者:陈佬昔没带相机
来源:juejin.cn/post/7187069862965936188
收起阅读 »

考研失败,加入国企当程序员,真香!

1、考研失败老家成都,本科毕业于帝都某所以能歌善舞著称的985学校,哲学专业。侥幸自己选修过大数据课程,并且听说程序员工资还不错,通过校招加入了隔壁省的一家制造业当程序员。在工作的第3年决定考研,边工作边考研,压力真是不小。让本来就是学渣的我,从研究生考场走出...
继续阅读 »

最近考研出成绩了,大家考得怎么样?分享一个自己考研失败后,入职国企的故事。

1、考研失败

我是工作了3年后才参加考研的。

老家成都,本科毕业于帝都某所以能歌善舞著称的985学校,哲学专业。

大学选专业时家里人不懂,自己全凭爱好,第一志愿就是哲学。上学时有多快乐,毕业时就多难找工作。

侥幸自己选修过大数据课程,并且听说程序员工资还不错,通过校招加入了隔壁省的一家制造业当程序员。

公司管吃管住,工资1w出头,干了2年,感觉到了危机。自己羞于靠关系上位,但技术上,计算机知识太薄弱也发展有限,于是就想趁着还年轻,考个计算机硕士,提升一下自己。

在工作的第3年决定考研,边工作边考研,压力真是不小。让本来就是学渣的我,从研究生考场走出来就知道了最终的结果:没戏了。

成绩出来后,果然不出所料。

2、加入国企

知道初试成绩以后我难过了几天,认真思考了一下自己的未来发展:旁边的帝都我也回不去了,又不甘心留在这个小城市,于是决定回老家成都发展。

毕业后一直在私企,总是听说国企好、央企不错,所以我这次投简历也想试一下。正好看到一篇讲程序员国企的文章:值得程序员加入的173家国企汇总,网友:这下彻底躺平了,于是就按照文中的思路,找到了成都的一些所谓不错的国企投递试试。

成功加入后发现:真香!

  • 福利真 ** 好!和其它公司谈薪酬,别人都是能压多低压多低,来这家国企,竟然还说我工作年限够长,在我期望的基础上加了3k。六险二金更不用说,平时的各种生活保障也是非常到位。不夸张的说,从私企转到国企的我,有一种刘姥姥进大观园的感觉。

  • 真卷!是谁告诉我国企适合养老的?这比我以前在私企工作强度大多了好嘛?而且我第一次听说部门平均加班时长影响个人绩效这种规则。

  • 技术上不激进。可能对于程序员来说,不停的学习新技术是一种常态,但是在这家国企,基本都是传统老技术,我打开代码还看到了我们领导1997年写的头文件。当然,你也可以认为这是一种不好的事。这里的技术对我来说,够了。

  • 同事关系很融洽。我人生中第一次去按摩店找技师,是女同事带我去的,谁能信!不过必须说一句,成都的按摩店是真正规啊!技师可真有劲。

去年回来工作快1年了,我现在对于考研已经释怀了。听说我们部门今年招人开始硕士起步了,有时候我还挺庆幸自己去年没考研成功的,不然即使上了研究生,我这实力,也不一定能进入现在的单位了。

3、写在最后

现在回过头来看,有一个不同的体会:考研是一件好事,但如果本身不是沉迷于科研事业,而是更想赚钱的话,有好的工作机会也别错过。

另外,多关注有用的信息很重要,有时候别人的一番话,可能就需要自己经历几年的曲折才能总结出来。

大家有任何程序员求职问题,欢迎在评论区和我交流~

作者:程序员晚枫
来源:juejin.cn/post/7205541247590973499

收起阅读 »

迄今为止我写过最复杂的算法

web
《亲戚计算器》大概是我迄今为止写过最复杂的算法了,它可能看起来它好像逻辑简单,仅1个方法调用而已,却耗费了我大量的时间!从一开始灵光乍现,想到实现它的初步思路,到如今开源已7年多了。这期间,我一直在不断更新才让它日趋完善,它的工作不仅是对数据的整理,还有我对程...
继续阅读 »

《亲戚计算器》大概是我迄今为止写过最复杂的算法了,它可能看起来它好像逻辑简单,仅1个方法调用而已,却耗费了我大量的时间!从一开始灵光乍现,想到实现它的初步思路,到如今开源已7年多了。这期间,我一直在不断更新才让它日趋完善,它的工作不仅是对数据的整理,还有我对程序逻辑的梳理和设计思路的推敲。


如果你也对传统文化稍微有点兴趣,不妨耐心的看下去……也许你会发现:原理我们日常习以为常的一个称呼,需要考虑那么多细节。


称谓系统的庞大


中国的亲戚称呼系统复杂在于,它对每种亲戚关系都有特定的称呼,同时对于同种关系不同地方、对于不同性别的人都可能有不同的称呼。




  1. 对外国人而言,父母的兄弟姐妹不外乎:uncle、aunt;而对于我们来说,父母的兄弟姐妹有:伯父、叔叔、姑姑、舅舅、姨妈;




  2. 不同地方对同个亲戚的称呼都是不一样的,以爸爸为例,别称包含有:爸爸、父亲、老爸、阿爸、老窦、爹地、老汉、老爷子等等;




  3. 不同关系链可能具有相同的称呼;比如“舅公”一词,可以是父母亲的舅舅,也可以是老公的舅舅,而这两种关系辈分却不同。究其原因我猜测是,传统上由姻亲产生的亲戚关系,为表达谦卑会自降一辈,随子女称呼配偶的长辈。




  4. 一个称呼中可能是多种关系的合称。比如:“父母”、“子女”、“公婆”,他们不是指代一个人物关系,而是几个关系的合称。




在设计这套算法的时候,我希望它能尽量包含各种称呼、各种关系链,因为我之所以做这个项目就是像让它真正集合多种需求,否则如果它不够全面那终究是个代码演示而已。


关系网络的表达


亲戚的关系网络是以血缘和婚姻为纽带联系在一起的,每个节点都是一个人,每个人都有诸如:父、母、兄、弟、姐、妹、子、女、夫、妻这样的基础关系。关系网络中的节点数量随着层级的加深而指数增长!如果是5层关系,大概就有9x9x9x9x9 = 59049种关系了(当然,这其中有小部分是重复的)。如果想要把几万个关系,数十万个称呼全部尽收其中显然是不可能的,没人有那个精力去维护。


xixik_627466c7fa1e646e.jpg


如何将亲戚关系网络中每个节点之间的关系用数据结构表现出来是一个难点。它需要保证数据量尽量全、占用体积小、易检索、可扩展等特点,这样才能保证算法检索关系时的完整性和高效性。


网络的寻址问题


既然是计算,那一定不是简单通过父、母、子、女等这些基础关系找对应称呼了。否则这就是简单的字典查询而已,谈不上算法。如果问的是:“舅妈的儿子的奶奶的外孙”又该如何呢?首先,需要在网络中找到单一称呼,如“舅妈”,而下一步找她的“儿子”,而非你自己的“儿子”。这就要求有类似于指针的功能,关系链每往前走一步,指针就指引着关系的节点,最终需找到答案。


而就像前面说到的一样,某些称谓可能对应多条关系,同时有些关系并不是唯一的。比方说你爸爸的儿子就是你吗?有没有可能是弟弟或者哥哥?而这些是不是同时取决于你的性别呢?
因为如果你是女的,那么你爸爸的儿子必然不是你呀!


这就对算法提出了一个要求,它必须准确的包含多种可能性。



年龄和性别的推测


随着关系链的复杂,最终得到的答案也有多种。那有没有一种可能,在对关系链的描述中是否存在一些词,可以通过逻辑判断知道对方的性别或年纪大小,进而排除一些不可呢?


例如“爱人的婆婆的儿子”,单从“爱人”二字我们并不能推测自己的性别,而后的“婆婆”确是只有女性才有的亲戚,“爱人的婆婆”就足以推断自己是男的,那么“爱人的婆婆的儿子”必然包含自己。相反,“爱人的婆婆的女儿”一定不是自己,只能是自己的姊妹。




再比如:自己哥哥的表哥也是你的表哥,你弟弟的表哥还是你表哥吗?因为你无法判断你弟弟和他的表哥谁大,自然无法判断对方是你的表哥还是表弟。既然都有可能存在,就需要保留可能性进一步计算。这就涉及到了在关系链的计算中不仅仅需要考虑隐藏的性别线索,还有年龄线索。




身份角度的切换


单从亲戚和自己的关系链条中开始算亲戚的称呼,仅仅是单向的推算,只需要一个个关系往下算就好。如果想知道对方称呼为我什么,这就需要站在对方的角度,重新逆向的调理出我和他之间的关系了。比如我的“外孙”应该叫我什么?



另一方面,如果把我置身于第三者,想知道我的两个亲戚他们之间如何称呼,就必须要同时站在两个亲戚的角度,看待他们彼此之间的关系了。比如:我的“舅妈”该叫我的“外婆”什么呢?



年龄排序的问题


前面说到的都是对不同关系链中的可能性推敲,那如果相同的关系如何判断年龄呢?如果你有3个舅舅呢?虽然不管哪个舅舅,他们对于你的关系都一样,他们的老婆你都得叫声“舅妈”。但他们毕竟有年龄区别,自然就有长幼的排序了。有了排序,就又引发了对他们之间关系的思考。


还是举例说明下:“舅舅”和“舅妈”是什么关系?相信大部分第一反应就是夫妻关系呗!其实不尽然,毕竟有些人不会只有一个舅舅吧?那“大舅妈”和“二舅”就不是夫妻关系了,他们是叔嫂关系呀。“二舅”得管“大舅妈”叫“嫂子”,“大舅妈”得管“二舅”叫“小叔子”。




再进一步说,“二舅的儿子”得叫“大舅妈”为“伯母”,“大舅的儿子”得叫“二舅”为“二叔”。这些由父辈的排序问题影响自己称谓的不同,而是我这套算法需要考虑的内容。




怎么样?是不是没有想象中的那么简单?
如果你想了解更多实现和思路的细节,可以关注本项目开源代码哦:github.com/mumuy/relat…


你也可以在此了解算法的基础原理:算法实现原理介绍


作者:passer-by6061
来源:juejin.cn/post/7203734711779196986
收起阅读 »

序列化和反序列化

序列化隐秘的吭,你踩过了没? 序列化和反序列化 Java序列化的目的主要有2个: 网络传输 对象持久化 当2个相对独立的进程,需要进行跨进程服务调用时,就需要把被传输的Java对象编码为字节数组或者ByteBuffer对象。 接收方只需要把这些字节数...
继续阅读 »

序列化隐秘的吭,你踩过了没?


序列化和反序列化



Java序列化的目的主要有2个:




  • 网络传输

  • 对象持久化


image-20230301144505527


当2个相对独立的进程,需要进行跨进程服务调用时,就需要把被传输的Java对象编码为字节数组或者ByteBuffer对象


接收方只需要把这些字节数组或者Bytebuf对象重新解码成内存对象即可实现通信、调用的作用。


image-20230301145117301


那么在我们使用序列化的时候有哪些需要注意的,避免的坑呢?


成员变量不能以is开头



阿里的《Java开发手册》明文规定了:成员变量禁止使用类似 isXxxx 的命名方式,也不要有isXxx命名的方法



image-20230301150018030


image-20230301145401694


大概的意思就是:不要加is前缀,因为部分框架序列化的时候,会以为对应的字段名是isXxxx后面的Xxxx



  • 比如:isSucceed序列化成Succeed,前端读取isSucceed的时候就会发现没有这个字段,然后出错了。


u=4214115302,3196714167&fm=253&fmt=auto&app=120&f=JPEG


这里面的序列化框架其实就是fastjson,我们可以直接去看他的源码


fastjson源码分析:computeGetters



去找get前缀的方法,然后进行字符串切割找到get后面的



image-20230301161434898



去找is前缀的方法,然后进行字符串切割



image-20230301161413220



  • 这里还进行了驼峰命名的判断:ixXxx,第三个字符是否是大写等判断


所以isSucceed字段会被fastjson框架认为Succeed字段。


image.png


默认值



成员变量的默认值同样会带来坑



同样是阿里的《Java开发手册》里面也是规定了:POJO类、RPC方法必须使用包装类型


image.png


关于包装类型和基本类型的区别,如果还有不清楚的,赶紧去看,这是最基础的面试知识点..


POJO类必须使用包装类型



尽量让错误暴露在编译期,不要拖到运行期



基本类型具有初始值,比如:



  • Int:0

  • float:0.0f

  • boolean:false


一个统计点赞的接口里面的返回值包含一个表示点赞数变化的字段,当发生错误的时候,这个字段没有进行赋初始值,就会出现以下情况:



  • 基本类型:读默认值,0,表达的意思就是没有点赞数变化,程序上并不知道是服务器那边出了错。

  • 包装类型:读到了个null,程序上是知道服务器那边出错了,可以进行对应的显示,比如显示 - ,表示读取不到等操作。


u=3180711090,4079282331&fm=253&fmt=auto&app=138&f=JPEG


总的来说就是:如果字段设置为基础类型并且基础类型的默认值具有业务意义,那么就会出错,并且无法感知错误


RPC方法的返回值和参数必须使用包装类型



RPC调用常常具有超时导致调用失败的情况



如果用包装类型,那么在接收方,就能感知到,这次RPC调用是成功,还是失败。


包装数据类型的null值具有表示额外的信息功能



彦祖来都来了,点个赞👍再走吧,这对我来说真的非常重要



作者:Ashleejy
来源:juejin.cn/post/7205478140914843709
收起阅读 »

扯什么 try-catch 性能问题?

“yes,你看着这鬼代码,竟然在 for 循环里面搞了个 try-catch,不知道try-catch有性能损耗吗?”老陈煞有其事地指着屏幕里的代码: for (int i = 0; i < 5000; i++) { try { ...
继续阅读 »

“yes,你看着这鬼代码,竟然在 for 循环里面搞了个 try-catch,不知道try-catch有性能损耗吗?”老陈煞有其事地指着屏幕里的代码:


 for (int i = 0; i < 5000; i++) {
try {
dosth
} catch (Exception e) {
e.printStackTrace();
}
}

我探过头去看了眼代码,“那老陈你觉得该怎么改?”


“当然是把 try-catch 提到外面啊!”老陈脑子都不转一下,脱口而出。


“你是不是傻?且不说性能,这代码的目的明显是让循环内部单次调用出错不影响循环的运行,你其到外面业务逻辑不就变了吗!”


老陈挠了挠他的地中海,“好像也是啊!”



“回过头来,catch 整个 for 循环和在循环内部 catch,在不出错的情况下,其实性能差不多。” 我喝一口咖啡不经意地提到,准备在老陈前面秀一下。


“啥意思?”老陈有点懵地看着我,“try-catch是有性能损耗的,我可是看过网上资料的!”


果然,老陈上钩了,我二话不说直接打开 idea,一顿操作敲了以下代码:


public class TryCatchTest {

@Benchmark
public void tryfor(Blackhole blackhole) {
try {
for (int i = 0; i < 5000; i++) {
blackhole.consume(i);
}
} catch (Exception e) {
e.printStackTrace();
}
}

@Benchmark
public void fortry(Blackhole blackhole) {
for (int i = 0; i < 5000; i++) {
try {
blackhole.consume(i);
} catch (Exception e) {
e.printStackTrace();
}
}
}

}

“BB 不如 show code,看到没,老陈,我把 try-catch 从 for 循环里面提出来跟在for循环里面做个对比跑一下,你猜猜两个差多少?”


“切,肯定 tryfor 性能好,想都不用想,不是的话我倒立洗头!”老陈信誓旦旦道。


我懒得跟他BB,直接开始了 benchmark,跑的结果如下:



可以看到,两者的性能(数字越大越好)其实差不多:



  • fortry: 86,261(100359-14098) ~ 114,457(100359+14098)

  • tryfor: 95,961(103216-7255) ~ 110,471(103216+7255)


我再调小(一般业务场景 for 循环次数都不会很多)下 for 循环的次数为 1000 ,结果也是差不多:



老陈一看傻了:“说好的性能影响呢?怎么没了?”


我直接一个javap,让老陈看看,其实两个实现在字节码层面没啥区别:



tryfor 的字节码



异常表记录的是 0 - 20 行,如果这些行里面的代码出现问题,直接跳到 23 行处理




fortry 的字节码



差别也就是异常表的范围小点,包的是 9-14 行,其它跟 tryfor 都差不多。



所以从字节码层面来看,没抛错两者的执行效率其实没啥差别。


“那为什么网上流传着try-catch会有性能问题的说法啊?”老陈觉得非常奇怪。


这个说法确实有,在《Effective Java》这本书里就提到了 try-catch 性能问题:



并且还有下面一段话:



正所谓听话不能听一半,以前读书时候最怕的就是一知半解,因为完全理解选择题能选对,完全不懂蒙可能蒙对,一知半解必定选到错误的选项!


《Effective Java》书中说的其实是不要用 try-catch 来代替正常的代码,书中的举例了正常的 for 循环肯定这样实现:



但有个卧龙偏偏不这样实现,要通过 try-catch 拐着弯来实现循环:



这操作我只能说有点逆天,这两个实现的对比就有性能损耗了


我们直接再跑下有try-catch 的代码和没 try-catch的 for 循环区别,代码如下:



结果如下:



+-差不多,直接看前面的分数对比,没有 ry-catch 的性能确实好些,这也和书中说的 try-catch 会影响 JVM 一些特定的优化说法吻合,但是具体没有说影响哪些优化,我猜测可能是指令重排之类的。


好了,我再总结下有关 try-catch 性能问题说法:



  1. try-catch 相比较没 try-catch,确实有一定的性能影响,但是旨在不推荐我们用 try-catch 来代替正常能不用 try-catch 的实现,而不是不让用 try-catch

  2. for循环内用 try-catch 和用 try-catch 包裹整个 for 循环性能差不多,但是其实两者本质上是业务处理方式的不同,跟性能扯不上关系,关键看你的业务流程处理。

  3. 虽然知道try-catch会有性能影响,但是业务上不需要避讳其使用,业务实现优先(只要不是书中举例的那种逆天代码就行),非特殊情况下性能都是其次,有意识地避免大范围的try-catch,只 catch 需要的部分即可(没把握全 catch 也行,代码安全执行第一)。


“好了,老陈你懂了没?”


“行啊yes,BB是一套一套的,走请你喝燕麦拿铁!” 老陈一把拉起我,我直接一个挣脱,“少来,我刚喝过咖啡,你那个倒立洗头,赶紧的!”我立马意识到老陈想岔开话题。


“洗洗洗,我们先喝个咖啡,晚上回去给你洗!”


晚上22点,老陈发来一张图片:



你别说,这头发至少比三毛多。


我是yes,我们下篇见~


作者:yes的练级攻略
来源:juejin.cn/post/7204121228016091197
收起阅读 »

实战:一天开发一款内置游戏直播的国产版Discord应用【附源码】(下)

上篇:https://www.imgeek.net/article/825362923声网RTC接入, 直播与语音实现接入在views/Channel/components文件夹下新增一个组件StreamHandler, 该组件为后续我们处理游戏房间的组件, ...
继续阅读 »

上篇:https://www.imgeek.net/article/825362923

声网RTC接入, 直播与语音实现

接入

views/Channel/components文件夹下新增一个组件StreamHandler, 该组件为后续我们处理游戏房间的组件, 先初步编写声网接入逻辑

// views/Channel/components/StreamHandler/index.js

const options = {
appId:
process.env.REACT_APP_AGORA_APPID || "default id",
channel: process.env.REACT_APP_AGORA_CHANNEL || "test",
token:
process.env.REACT_APP_AGORA_TOKEN ||
"default token",
uid: process.env.REACT_APP_AGORA_UID || "default uid",
};

const StreamHandler = (props) => {
// 组件参数: 用户信息, 当前频道所有消息, 当前频道id, 是否开启本地语音
const { userInfo, messageInfo, channelId, enableLocalVoice = false } = props;

const [rtcClient, setRtcClient] = useState(null);
// 声网client连接完成
const [connectStatus, setConnectStatus] = useState(false);

// RTC相关逻辑
useEffect(() => {
AgoraRTC.setLogLevel(3);
const client = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });
// TODO: use right channel
client
.join(options.appId, options.channel, options.token, userInfo?.username)
.then(() => {
setConnectStatus(true);
console.log("[Stream] join channel success");
})
.catch((e) => {
console.log(e);
});

setRtcClient(client);
return () => {
// 销毁时, 自动退出RTC频道
client.leave();
setRtcClient(null);
};
}, []);

return (
<>
{!connectStatus && <Spin tip="Loading" size="large" />}
</>
);
}


// 我们需要全局状态中的userinfo, 映射一下到当前组件的props中
const mapStateToProps = ({ app }) => {
return {
userInfo: app.userInfo,
};
};
export default memo(connect(mapStateToProps)(StreamHandler));

然后回到Channel中, 在之前的renderStreamChannel函数中添加上StreamHandler组件

// view/Channel/index.js
const [enableVoice, setEnableVoice] = useState(false);
const toggleVoice = () => {
setEnableVoice((enable) => {
return !enable;
});
}

// 保留了输入窗口, 可以在它的菜单栏中添加游戏频道独有的一些逻辑,
// 这里我加入了开关本地语音的逻辑, 拓展Input的细节可以参照完整版代码
const renderStreamChannel = () => {
return (
<>
<div className={s.messageRowWrap}>
<StreamHandler messageInfo={messageInfo} channelId={channelId} enableLocalVoice={enableVoice} />
</div>
<div className={s.iptWrap}>
<Input chatType={CHAT_TYPE.groupChat} fromId={channelId} extraMenuItems={renderStreamMenu()} />
</div>
</>
);
}

const renderStreamMenu = () => {
return [
{
key: "voice",
label: (
<div
className="circleDropItem"
onClick={toggleVoice}
>
<Icon
name="person_wave_slash"
size="24px"
iconClass="circleDropMenuIcon"
/>
<span className="circleDropMenuOp">
{enableVoice ? "关闭语音" : "开启语音"}
</span>
</div>
),
}
];
}

此时我们创建一个video-开题的游戏频道, 应该可以看到命令行中输出了RTC连接成功信息. [Stream] join channel success

音视频推流

接下来我们继续做实质的RTC推流逻辑, 及用户上下播的入口. 但在那之前, 先简单过一下声网RTC中的一些概念.

参考以下步骤实现音视频通话的逻辑:

  1. 调用 createClient 方法创建 AgoraRTCClient 对象。
  2. 调用 join 方法加入一个 RTC 频道,你需要在该方法中传入 App ID 、用户 ID、Token、频道名称。
  3. 先调用 createMicrophoneAudioTrack 通过麦克风采集的音频创建本地音频轨道对象,调用 createCameraVideoTrack 通过摄像头采集的视频创建本地视频轨道对象;然后调用 publish 方法,将这些本地音视频轨道对象当作参数即可将音视频发布到频道中。
  4. 当一个远端用户加入频道并发布音视频轨道时:
  5. 监听 client.on(“user-published”) 事件。当 SDK 触发该事件时,在这个事件回调函数的参数中你可以获取远端用户 AgoraRTCRemoteUser 对象 。
  6. 调用 subscribe 方法订阅远端用户 AgoraRTCRemoteUser 对象,获取远端用户的远端音频轨道 RemoteAudioTrack 和远端视频轨道 RemoteVideoTrack 对象。

在这里插入图片描述

(以上内容来自声网官方文档)

在上面的接入中, 我们已经完成了创建对象并加入频道两步.
在RTC中, 可以传输音频和视频信号, 由于单个RTC客户端要传输不同种类的数据, 每个单独的音视频源被分成不同的track(由于它们都是实时不断产生的, 我们称作流), 随后通过publish方法, 将我们本地的信号源交付给RTC客户端传输.
随后通过user-published事件的回调来在其他用户发布信号源时进行处理, 首先需要subscribe该用户来获取后续数据, 随后根据不同类型的信号流做处理.
离开时需要关闭本地当前的信号源, 并退出RTC客户端.
最后通过user-unpublished事件监听其他用户退出, 移除它们对应的信号流.

逻辑理清楚后代码就很容易看懂了

// views/Channel/components/StreamHandler/index.js
const StreamHandler = (props) => {
...
// 本地视频元素
const localVideoEle = useRef(null);
// 远程视频元素
const canvasEle = useRef(null);
const [rtcClient, setRtcClient] = useState(null);
const [connectStatus, setConnectStatus] = useState(false);
// 当前直播的用户
const [remoteUser, setRemoteUser] = useState(null);
// 远程音视频track
const [remoteVoices, setRemoteVoices] = useState([]);
const [remoteVideo, setRemoteVideo] = useState(null);

// RTC相关逻辑
useEffect(() => {
...
// client.join 后

// 监听新用户加入
client.on("user-published", async (user, mediaType) => {
// auto subscribe when users coming
await client.subscribe(user, mediaType);
console.log("[Stream] subscribe success on user ", user);
if (mediaType === "video") {
// 获取直播流
if (remoteUser && remoteUser.uid !== user.uid) {
// 只能有一个用户推视频流
console.error(
"already in a call, can not subscribe another user ",
user
);
return;
}
// 播放并记录下视频流
const remoteVideoTrack = user.videoTrack;
remoteVideoTrack.play(localVideoEle.current);
setRemoteVideo(remoteVideoTrack);
// can only have one remote video user
setRemoteUser(user);
}
if (mediaType === "audio") {
// 获取音频流
const remoteAudioTrack = user.audioTrack;
// 去重
if (remoteVoices.findIndex((item) => item.uid === user.uid) == -1) {
remoteAudioTrack.play();
// 添加到数组中
setRemoteVoices([
...remoteVoices,
{ audio: remoteAudioTrack, uid: user.uid },
]);
}
}
});

client.on("user-unpublished", (user) => {
// 用户离开, 去除流信息
console.log("[Stream] user-unpublished", user);
removeUserStream(user);
});
setRtcClient(client);
return () => {
client.leave();
setRtcClient(null);
};
}, []);

const removeUserStream = (user) => {
if (remoteUser && remoteUser.uid === user.uid) {
setRemoteUser(null);
setRemoteVideo(null);
}
setRemoteVoices(remoteVoices.filter((voice) => voice.uid !== user.uid));
};
}

接着我们根据之前提到的自定义消息判断当前在播状态, 以最后一条自定义消息为准.

// views/Channel/components/StreamHandler/index.js
const StreamHandler = (props) => {
const { userInfo, messageInfo, channelId, enableLocalVoice = false } = props;

// 第一条 stream 消息, 用于判断直播状态
const firstStreamMessage = useMemo(() => {
return messageInfo?.list?.find(
(item) => item.type === "custom" && item?.ext?.type === "stream"
);
}, [messageInfo]);

// 是否有直播
const hasRemoteStream =
firstStreamMessage?.ext?.status === CMD_START_STREAM &&
firstStreamMessage?.ext?.user !== userInfo?.username;
// 本地直播状态
const [localStreaming, setLocalStreaming] = useState(
firstStreamMessage?.ext?.status === CMD_START_STREAM &&
firstStreamMessage?.ext?.user === userInfo?.username
);

// 本地直播流状态
const toggleLocalGameStream = () => {
if (hasRemoteStream) {
return;
}
setLocalStreaming(!localStreaming);
};
// 根据直播状态选择渲染
return (
<>
{!connectStatus && <Spin tip="Loading" size="large" />}
{hasRemoteStream ? (
<RemoteStreamHandler
remoteUser={firstStreamMessage?.ext?.user}
localVideoRef={localVideoEle}
channelId={channelId}
userInfo={userInfo}
rtcClient={rtcClient}
/>
) : (
<LocalStreamHandler
localStreaming={localStreaming}
canvasRef={canvasEle}
toggleLocalGameStream={toggleLocalGameStream}
rtcClient={rtcClient}
userInfo={userInfo}
channelId={channelId}
/>
)}
</>
);
}

我们根据hasRemoteStream分成两种逻辑RemoteStreamHandlerLocalStreamHandler(可以先用div+文字的空实现占位), 首先我们来看本地游戏的逻辑

// view/Channel/components/StreamHandler/local_stream.js
const LocalStreamHandler = (props) => {

const {
toggleLocalGameStream,
canvasRef,
localStreaming,
rtcClient,
userInfo,
channelId,
} = props;

const [localVideoStream, setLocalVideoStream] = useState(false);
const localPlayerContainerRef = useRef(null);

// 开启本地视频流
useEffect(() => {
if (!localPlayerContainerRef.current) return;
const f = async () => {
// 暂时使用视频代替游戏流
let lgs = await AgoraRTC.createCameraVideoTrack();
lgs.play(localPlayerContainerRef.current);
setLocalGameStream(lgs);
}
f();
}, [localPlayerContainerRef])

const renderLocalStream = () => {
return (
<div style={{ height: "100%" }} ref={localPlayerContainerRef}>
</div>
)
}

// 控制上下播
const renderFloatButtons = () => {
return (
<FloatButton.Group
icon={<DesktopOutlined />}
trigger="click"
style={{ left: "380px" }}
>
<FloatButton
onClick={toggleLocalGameStream}
icon={
localStreaming ? <VideoCameraFilled /> : <VideoCameraOutlined />
}
tooltip={<div>{localStreaming ? "停止直播" : "开始直播"}</div>}
/>
</FloatButton.Group>
);
};

// 渲染: 悬浮窗和本地流
return (
<>
<div style={{ height: "100%" }}>
{renderFloatButtons()}
{renderLocalStream()}
</div>
</>
);
}

现在我们进入直播房间已经可以看到本地摄像头的内容了, 但我们还没有将视频流投放到RTC中, 且上播逻辑也没有处理

// view/Channel/components/StreamHandler/local_stream.js
useEffect(() => {
// 发布直播推流
if (!localStreaming || !rtcClient || !localVideoStream) {
return;
}
console.log("height", canvasRef.current.height);
console.log("publishing local stream", localVideoStream);
// 将流publish到rtc中
rtcClient.publish(localVideoStream).then(() => {
// 频道中发布一条消息, 表示开始直播
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_START_STREAM,
},
channelId
).then(() => {
message.success({
content: "start streaming",
});
});
});
return () => {
// 用户退出的清理工作,
// unpublish流(远程), 停止播放流(本地), 发送直播关闭消息(频道)
if (localVideoStream) {
rtcClient.unpublish(localVideoStream);
localVideoStream.stop();
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_END_STREAM,
},
channelId
);
message.info({
content: "stop streaming",
});
}
};
}, [rtcClient, localStreaming, canvasRef, userInfo, channelId, localVideoStream]);

为了测试直播效果, 我们需要登录第二个账号(使用浏览器的匿名/开其他的浏览器, 此时cookie没有共享, 可以多账号登录), 进入相同频道, 开启直播, 此时第一个账号应该会自动刷新状态(如果没有则手动切换一下频道), 进入到RemoteStreamHandler, 说明我们直播的逻辑已经完成.

本地语音的逻辑也是类似的, 这里就不再重复.

接下来是远程流的渲染逻辑, 它的逻辑相对简单, 观看者可以选择开始/停止观看直播流

// view/Channel/components/StreamHandler/remote_stream.js
const RemoteStreamHandler = (props) => {

const {
remoteUser,
localVideoRef,
toggleRemoteVideo,
channelId,
userInfo,
rtcClient,
} = props;

// 这里加一个强制t人的开关, 由于debug
const enableForceStop = true;
const forceStopStream = () => {
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_END_STREAM,
},
channelId
);
};
const renderRemoteStream = () => {
return (
<div style={{ height: "100%" }}>
<div
id="remote-player"
style={{
width: "100%",
height: "90%",
border: "1px solid #fff",
}}
ref={localVideoRef}
/>
<div
style={{
display: "flex",
justifyContent: "center",
marginTop: "10px",
}}
>
<span style={{ color: "#0ECD0A" }}>{remoteUser}</span>
&nbsp; is playing{" "}
</div>
</div>
);
};
const renderFloatButtons = () => {
return (
<FloatButton.Group
icon={<DesktopOutlined />}
trigger="click"
style={{ left: "380px" }}
>
<FloatButton
onClick={toggleRemoteVideo}
icon={<VideoCameraAddOutlined />}
tooltip={<div>观看/停止观看直播</div>}
/>
{enableForceStop && (
<FloatButton
onClick={forceStopStream}
icon={<VideoCameraAddOutlined />}
tooltip={<div>强制停止直播</div>}
/>
)}
</FloatButton.Group>
);
};
return (
<>
<div style={{ height: "100%" }}>
{renderFloatButtons()}
{renderRemoteStream()}
</div>
</>
);
}

开关远程流的代码在StreamHander中, 作为参数传给RemoteStream

// views/Channel/components/StreamHandler/index.js
const toggleRemoteVideo = () => {
if (!hasRemoteStream) {
return;
}
console.log("[Stream] set remote video to ", !enableRemoteVideo);
// 当前是关闭状态,需要打开
// 开关远程音频的逻辑也与此类型.
if (enableRemoteVideo) {
remoteVideo?.stop();
} else {
remoteVideo?.play(localVideoEle.current);
}
setEnableRemoteVideo(!enableRemoteVideo);
};

ok, 现在我们已经实现了基于声网RTC, 在环信超级社区集成视频直播的功能.

直播替换为游戏流

接下来我们来将直播流升级一下, 替换成模拟器包, 为了方便测试, 我们直接使用打包好的版本(https://github.com/a71698422/web-0.1.1), pkg包解压后直接放置到项目根目录,

RustNESEmulator 是一个基于Rust语言的NES模拟器, 我们在web平台可以使用它编译好的wasm版本

并将mario.nes文件放到src/assets目录下, 这是初代马里奥游戏的ROM文件(你也可以使用你喜欢的nes游戏, 如果遇到问题, 欢迎到RustNESEmulator中提issue)

加入前端的模拟器适配代码

// views/Channel/components/StreamHandler
// from tetanes.

import * as wasm from "@/pkg";
class State {
constructor() {
this.sample_rate = 44100;
this.buffer_size = 1024;
this.nes = null;
this.animation_id = null;
this.empty_buffers = [];
this.audio_ctx = null;
this.gain_node = null;
this.next_start_time = 0;
this.last_tick = 0;
this.mute = false;
this.setup_audio();
console.log("[NES]: create state");
}

load_rom(rom) {
this.nes = wasm.WebNes.new(rom, "canvas", this.sample_rate);
this.run();
}

toggleMute() {
this.mute = !this.mute;
}

setup_audio() {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (!AudioContext) {
console.error("Browser does not support audio");
return;
}
this.audio_ctx = new AudioContext();
this.gain_node = this.audio_ctx.createGain();
this.gain_node.gain.setValueAtTime(1, 0);
}

run() {
const now = performance.now();
this.animation_id = requestAnimationFrame(this.run.bind(this));
if (now - this.last_tick > 16) {
this.nes.do_frame();
this.queue_audio();
this.last_tick = now;
}
}

get_audio_buffer() {
if (!this.audio_ctx) {
throw new Error("AudioContext not created");
}

if (this.empty_buffers.length) {
return this.empty_buffers.pop();
} else {
return this.audio_ctx.createBuffer(1, this.buffer_size, this.sample_rate);
}
}

queue_audio() {
if (!this.audio_ctx || !this.gain_node) {
throw new Error("Audio not set up correctly");
}

this.gain_node.gain.setValueAtTime(1, this.audio_ctx.currentTime);

const audioBuffer = this.get_audio_buffer();
this.nes.audio_callback(this.buffer_size, audioBuffer.getChannelData(0));
if (this.mute) {
return;
}
const source = this.audio_ctx.createBufferSource();
source.buffer = audioBuffer;
source.connect(this.gain_node).connect(this.audio_ctx.destination);
source.onended = () => {
this.empty_buffers.push(audioBuffer);
};
const latency = 0.032;
const audio_ctxTime = this.audio_ctx.currentTime + latency;
const start = Math.max(this.next_start_time, audio_ctxTime);
source.start(start);
this.next_start_time = start + this.buffer_size / this.sample_rate;
}
// ...
}

export default State;

改造local_stream

// view/Channel/components/StreamHandler/local_stream.js

import mario_url from "@/assets/mario.nes";
import * as wasm_emulator from "@/pkg";
import State from "./state";

const LocalStreamHandler = (props) => {
// 模拟器 state
const stateRef = useRef(new State());

// 注意要将原来的代码注释掉
/*
const [localVideoStream, setLocalVideoStream] = useState(false);
const localPlayerContainerRef = useRef(null);

// 开启本地视频流
useEffect(() => {
if (!localPlayerContainerRef.current) return;
const f = async () => {
// 暂时使用视频代替游戏流
let lgs = await AgoraRTC.createCameraVideoTrack();
lgs.play(localPlayerContainerRef.current);
setLocalGameStream(lgs);
}
f();
}, [localPlayerContainerRef])

// 推流的函数也暂时注释
useEffet...
*/


useEffect(() => {
// 本地游戏
if (!canvasRef) {
return;
}
// 开启键盘监听等全局事件
wasm_emulator.wasm_main();
fetch(mario_url, {
headers: { "Content-Type": "application/octet-stream" },
})
.then((response) => response.arrayBuffer())
.then((data) => {
let mario = new Uint8Array(data);
// 加载 rom数据
stateRef.current.load_rom(mario);
});
}, [canvasRef]);

// 更新本地流渲染
const renderLocalStream = () => {
return (
<div style={{ height: "100%" }}>
<canvas
id="canvas"
style={{ width: 600, height: 500 }}
width="600"
height="500"
ref={canvasRef}
/>
</div>
);
};
}

这一步完成后, 我们就可以在本地试玩马里奥游戏了, 键盘绑定为

A      = J
B = K
Select = RShift
Start = Return
Up = W
Down = S
Left = A
Right = D

将推本地视频流改为游戏流

  useEffect(() => {
// 发布直播推流
if (!localStreaming || !rtcClient) {
return;
}
// 只修改了流获取部分
// canvas的captureStream接口支持获取视频流
// 我们用这个视频流构造一个声网的自定义视频流
let stream = canvasRef.current.captureStream(30);
let localVideoStream = AgoraRTC.createCustomVideoTrack({
mediaStreamTrack: stream.getVideoTracks()[0],
});
console.log("height", canvasRef.current.height);
console.log("publishing local stream", localVideoStream);
rtcClient.publish(localVideoStream).then(() => {
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_START_STREAM,
},
channelId
).then(() => {
message.success({
content: "start streaming",
});
});
});
return () => {
if (localVideoStream) {
rtcClient.unpublish(localVideoStream);
localVideoStream.stop();
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_END_STREAM,
},
channelId
);
message.info({
content: "stop streaming",
});
}
};
}, [rtcClient, localStreaming, canvasRef, userInfo, channelId]);

最后总结一下房间的流程图
在这里插入图片描述

至此该项目的完整流程就算结束啦,如果有哪些步骤细节不太明确, 可以参照完整版项目
环信超级社区项目
注册环信
模拟器直播项目github源码获取

收起阅读 »