这一年我优化了一个46万行的超级系统
背景
我曾带领团队治理了一个超级工程,是我毕业以来治理的最庞大、最复杂的工程系统,涉及到开发的方方面面。下面我给大家列几个数字,大家感受一下:
指标 数据 菜单数量 250+ 代码行数 46 万 路由数量 300+ 业务组件、util 600+ 构建时间 6min 关联业务 报表、CRM、订单、车辆、配置、财务...
这是上一任留给我的烫手山芋,而且从需求迭代频次来看,这个系统占据了这个业务部门50%的需求,也就是说,之前的伙计把几乎所有的业务功能都做进了一个系统中,像屎山一般堆积,构建一次的时间长达6-9min,作为一个合格的前端,简直怒火冲天。
我曾带领团队治理了一个超级工程,是我毕业以来治理的最庞大、最复杂的工程系统,涉及到开发的方方面面。下面我给大家列几个数字,大家感受一下:
指标 | 数据 |
---|---|
菜单数量 | 250+ |
代码行数 | 46 万 |
路由数量 | 300+ |
业务组件、util | 600+ |
构建时间 | 6min |
关联业务 | 报表、CRM、订单、车辆、配置、财务... |
这是上一任留给我的烫手山芋,而且从需求迭代频次来看,这个系统占据了这个业务部门50%的需求,也就是说,之前的伙计把几乎所有的业务功能都做进了一个系统中,像屎山一般堆积,构建一次的时间长达6-9min,作为一个合格的前端,简直怒火冲天。
问题
面对这样的超级应用,想要解决,必须先把问题整理出来,才好对症下药。
- 构建时间过长,影响开发体验。
- 系统单一,代码量庞大,未做拆分,对于后期维护难度很大,且存在增量上线风险(改一个字,也要整个系统打包上线)。
- 代码业务组件太少,复用率低,需要整理业务代码,封装高效业务组件库。
- 工具函数、请求Axios、目录规范、菜单权限、公共能力等未做统一封装和梳理。
- 存在大量重复的代码、大量重复的组件(有很多都是拷贝了一份,改个名字)。
- 页面加载极其缓慢,无论是首屏还是菜单加载,很明显存在严重的性能问题。
- 工程中随意引入各种插件,比如:big.js、xlsx.js、echarts、velocity-animate、lodash、file-saver等。
- 代码中存在很多mixin写法,导致调试难度很大。
以上是针对46万行的应用做出的问题分析报告,找出这些问题以后,我们就能开启优化之路。
面对这样的超级应用,想要解决,必须先把问题整理出来,才好对症下药。
- 构建时间过长,影响开发体验。
- 系统单一,代码量庞大,未做拆分,对于后期维护难度很大,且存在增量上线风险(改一个字,也要整个系统打包上线)。
- 代码业务组件太少,复用率低,需要整理业务代码,封装高效业务组件库。
- 工具函数、请求Axios、目录规范、菜单权限、公共能力等未做统一封装和梳理。
- 存在大量重复的代码、大量重复的组件(有很多都是拷贝了一份,改个名字)。
- 页面加载极其缓慢,无论是首屏还是菜单加载,很明显存在严重的性能问题。
- 工程中随意引入各种插件,比如:big.js、xlsx.js、echarts、velocity-animate、lodash、file-saver等。
- 代码中存在很多mixin写法,导致调试难度很大。
以上是针对46万行的应用做出的问题分析报告,找出这些问题以后,我们就能开启优化之路。
目标
我觉得不管哪一行,进入这个社会最重要的能力是生存能力,面对生存最重要的技能是解决问题的能力。想要把问题解决好,就要有章法可循,比如:找到问题要害、制定目标、提供解决方案、复盘总结。
我觉得不管哪一行,进入这个社会最重要的能力是生存能力,面对生存最重要的技能是解决问题的能力。想要把问题解决好,就要有章法可循,比如:找到问题要害、制定目标、提供解决方案、复盘总结。
方案
- 250+菜单归类整理、废弃菜单下线
- 搭建业务组件库
- 搭建工具函数库
- 基础框架优化
- 基于microApp做微服务拆分、引入webpack5的module-federation机制
- 引入rocket-render插件,对局部页面、基础功能做重构,后续逐步替换。
- 性能优化
- 250+菜单归类整理、废弃菜单下线
- 搭建业务组件库
- 搭建工具函数库
- 基础框架优化
- 基于microApp做微服务拆分、引入webpack5的module-federation机制
- 引入rocket-render插件,对局部页面、基础功能做重构,后续逐步替换。
- 性能优化
菜单整理
300 多个菜单,业务和产研人员可能已经更换过好几次,通过跟产品的沟通,可以得知,受业务调整影响,很多功能已经废弃,系统未及时下线导致日积月累。然而由于对业务不熟,产研很难判断哪些菜单需要下线,我们也不能随意凭感觉下线某一个菜单或功能,必须采取谨慎的态度,有法可依,因此我们采用如下措施:
菜单汇总
产品牵头,汇总系统所有菜单,按环境进行逐个确认。前端会根据确认结果依次删除路由配置、菜单、关联组件、调用 API 等相关代码。
- 同业务方确认后,直接下线。
- 线上访问异常菜单,进行标注。
- 数据异常菜单,进行标注。
- 对于无法确认的,通过监控查看页面访问量,通过1-3个月的观察,最终决定是否下线。
通过1个月的菜单整理,下线了大概50+的菜单、删除了近100+页面,以及不计其数的业务组件代码,整体代码量减少近8万,同时,我们将250多个菜单进行归类,拆分了6个大类,供后续微服务做准备。
300 多个菜单,业务和产研人员可能已经更换过好几次,通过跟产品的沟通,可以得知,受业务调整影响,很多功能已经废弃,系统未及时下线导致日积月累。然而由于对业务不熟,产研很难判断哪些菜单需要下线,我们也不能随意凭感觉下线某一个菜单或功能,必须采取谨慎的态度,有法可依,因此我们采用如下措施:
菜单汇总
产品牵头,汇总系统所有菜单,按环境进行逐个确认。前端会根据确认结果依次删除路由配置、菜单、关联组件、调用 API 等相关代码。
- 同业务方确认后,直接下线。
- 线上访问异常菜单,进行标注。
- 数据异常菜单,进行标注。
- 对于无法确认的,通过监控查看页面访问量,通过1-3个月的观察,最终决定是否下线。
通过1个月的菜单整理,下线了大概50+的菜单、删除了近100+页面,以及不计其数的业务组件代码,整体代码量减少近8万,同时,我们将250多个菜单进行归类,拆分了6个大类,供后续微服务做准备。
框架优化
一个稳定的系统,必然有一个稳定的框架做支撑。我们在梳理的过程中发现系统极其脆弱,无 ESLint 语法检查规范、没有 Prettier 格式化、Hard Code、随处可见的自定义组件、到处报错的控制台...
- 引入
ESLint
做语法检查. - 引入
Pettier
做代码格式化。 - 引入
Husky
lint-staged
规范代码提交。 - 对于硬编码部分跟产品沟通做成可视化配置或者json配置形式。
- Axios的二次封装:常规请求、全局Loading、文件下载、登录失效拦截、报错拦截提示、状态码统一适配。
- 插件删减:
big.js
剔除改为手动计算、lodash
替换为 lodash-es
、删除动画插件、文件导出统一由后端返回二进制,前端通过Axios封装下载函数进行文件下载等等。 - 配置文件封装:多环境配置、常量定义。
- 菜单权限封装。
- 按钮权限指令封装:
v-has="'create'"
- router路由提取优化,针对路由守卫处理一些特殊跳转问题。
- 接入前端监控平台,对于资源请求失败、接口请求超时、接口请求异常等做异常监控。
- 对于
eslint
和prettier
大家自行参考其他文章,此处不再赘述。 - 对于Axios二次封装,大家可能会比较奇怪,为什么要封装文件下载?因为文件导出本身也是一个请求,只是跟常规get/post有差异,所以我们为了方便调用,把它放在一个文件里面定义,比如:
export default {
get(url, params = {}, options = { showLoading: true, showError: true }) {
return instance.get(url, { params, ...options })
},
post(url, params = {}, options = { showLoading: true, showError: true }) {
return instance.post(url, params, options)
},
download(url, data, fileName = 'fileName.xlsx') {
instance({
url,
data,
method: 'post',
responseType: 'blob'
}).then(response => {
const blob = new Blob([response.data], {
type: response.data.type
})
const name = (response.headers['file-name'] as string) || fileName
const link = document.createElement('a')
link.download = decodeURIComponent(name)
link.href = URL.createObjectURL(blob)
document.body.append(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(link.href)
})
}
}
我们在api.js
中调用的时候,就会变的很简单,如:api.get('/user/list')
、api.download('/export',{ id: 123 })
。当然可能每个人有自己的开发习惯,这些都是我个人的经验,也不一定适合大家。对于上面的参数有一个options
变量,用来做参数扩展的,比如展示loading
和error
,为了做全局Loading
和全局错误提示用的。
- 如果你担心页面并发请求,导致重复
loading
问题,可以通过计数的方式来控制,避免多次重复Loading
。 - 插件删除部分大家可能有争议,这个纯属于个人行为,因为我们不是金融行业,不需要高精度,如果出现计算我们直接四舍五入即可,而
lodash-es
主要为了做tree-shaking
,还有很多插件根据自身情况考虑要不要引入。 - 指令封装,对于按钮权限非常管用,举个例子:
一个稳定的系统,必然有一个稳定的框架做支撑。我们在梳理的过程中发现系统极其脆弱,无 ESLint 语法检查规范、没有 Prettier 格式化、Hard Code、随处可见的自定义组件、到处报错的控制台...
- 引入
ESLint
做语法检查. - 引入
Pettier
做代码格式化。 - 引入
Husky
lint-staged
规范代码提交。 - 对于硬编码部分跟产品沟通做成可视化配置或者json配置形式。
- Axios的二次封装:常规请求、全局Loading、文件下载、登录失效拦截、报错拦截提示、状态码统一适配。
- 插件删减:
big.js
剔除改为手动计算、lodash
替换为lodash-es
、删除动画插件、文件导出统一由后端返回二进制,前端通过Axios封装下载函数进行文件下载等等。 - 配置文件封装:多环境配置、常量定义。
- 菜单权限封装。
- 按钮权限指令封装:
v-has="'create'"
- router路由提取优化,针对路由守卫处理一些特殊跳转问题。
- 接入前端监控平台,对于资源请求失败、接口请求超时、接口请求异常等做异常监控。
- 对于
eslint
和prettier
大家自行参考其他文章,此处不再赘述。 - 对于Axios二次封装,大家可能会比较奇怪,为什么要封装文件下载?因为文件导出本身也是一个请求,只是跟常规get/post有差异,所以我们为了方便调用,把它放在一个文件里面定义,比如:
export default {
get(url, params = {}, options = { showLoading: true, showError: true }) {
return instance.get(url, { params, ...options })
},
post(url, params = {}, options = { showLoading: true, showError: true }) {
return instance.post(url, params, options)
},
download(url, data, fileName = 'fileName.xlsx') {
instance({
url,
data,
method: 'post',
responseType: 'blob'
}).then(response => {
const blob = new Blob([response.data], {
type: response.data.type
})
const name = (response.headers['file-name'] as string) || fileName
const link = document.createElement('a')
link.download = decodeURIComponent(name)
link.href = URL.createObjectURL(blob)
document.body.append(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(link.href)
})
}
}
我们在api.js
中调用的时候,就会变的很简单,如:api.get('/user/list')
、api.download('/export',{ id: 123 })
。当然可能每个人有自己的开发习惯,这些都是我个人的经验,也不一定适合大家。对于上面的参数有一个options
变量,用来做参数扩展的,比如展示loading
和error
,为了做全局Loading
和全局错误提示用的。
- 如果你担心页面并发请求,导致重复
loading
问题,可以通过计数的方式来控制,避免多次重复Loading
。 - 插件删除部分大家可能有争议,这个纯属于个人行为,因为我们不是金融行业,不需要高精度,如果出现计算我们直接四舍五入即可,而
lodash-es
主要为了做tree-shaking
,还有很多插件根据自身情况考虑要不要引入。 - 指令封装,对于按钮权限非常管用,举个例子:
封装指令
import { getPageButtons } from '@hll/css-oms-utils'
// 权限指令
Vue.directive('has', {
inserted: (el, binding) => {
let pageButtons = getPageButtons().map;
if (!pageButtons[binding.value]) {
el.parentNode && el.parentNode.removeChild(el)
}
}
})
权限判断
// 1. 通过v-if判断
<el-button type="primary" v-if="pageButtons.create">创建el-button>
// 2. 通过v-has指令判断
<el-button type="primary" v-has="'create'">创建el-button>
getPageButtons 其实是为了兼容历史代码而封装的函数。
整个系统权限极其混乱,代码替换,耗时近 2 周,覆盖近 500 个文件才完成。
- 状态码适配
这个我觉得有必要提一下,我相信很多前端可能都遇到这个问题,一个系统对应n个后台服务,不同的后台服务有不同的格式,比如:A系统返回code=0
,B系统返回result=0
,C系统返回res=0
,那前端就要做不同的适配,其实也有不同的方法可以做:
- 让后端接入网关,统一在网关做适配。
- 前端在拦截器中开发
adapter
函数,针对响应码做适配。 - 针对分页、返回码、错误信息等都可以在适配函数里面做几种适配,或者提供单一函数在不同的页面单独调用,类似于
request
的to
模块。
业务组件库建设
这一部分工作非常重要,不管你在哪个公司,做任何行业,我都建议封装业务组件库,当然封装有三种形式:
- 基于公司自建的
npm
平台开发业务组件库,通过npm
方式引入。 - 对于小体量项目,直接把业务组件库放在
components
中进行维护,但是无法跨项目使用。 - 基于
webpack5
的module federation
能力开发公共组件,跨项目提供服务。
MF文档参考:http://www.webpackjs.com/concepts/mo… 我觉得在某些场景下,这个能力非常好用,它不需要像npm
一样,发布以后还需要给项目做升级,只要你改了组件源码,发布以后,其他引用的项目就会立即生效。想要快速了解这个能力,我建议大家先看一下插件文档,然后了解两个概念exposes
和remotes
,如果还是看不懂,就去找个掘进入门文章再结合本地实践一次基本就算入门了。
我是基于上面三种方式来搭建系统的业务组件库,对于通用部分全部提取封装,基于rollup
和vite
搭建一套npm
包,最终发布到公司私有npm
平台。对于一些频繁改动,链路较长部分通过module federation
进行封装和暴露。
梳理业务后,其实封装了很多业务组件,上面只是给大家提供了思路,没有一一列举具体组件,下面给大家简单列一个大纲图:
业务组件库建设对于这种大体量的超级系统收益非常大,而且是长期可持续的。
微服务搭建
前面第一部分梳理完菜单以后,其实已经将250+的菜单进行了归类,归类的目的其实就是为了服务拆分。拆分的好处非常明显:
- 服务解耦,便于维护。
- 局部需求可单独上线,不需要整包上传,减小线上风险。
- 缩小每个服务模块的构建时间,提升开发体验。
本次基于pnpm + microApp + module federation
来实现的微服务拆分,为什么?
- 微服务拆分以后,创建了 7 个仓库,非常不利于代码维护。
pnpm
天然具备monorepo
能力,可以把多个仓库合并为一个仓库,而且可以继续按7个项目的开发构建方式去工作。 - 微服务使用的是京东的
microApp
框架,集成非常简单,上手成本很低,文档友好,我觉得比阿里的乾坤简单太多了(个人主观看法)。 - 对于难于抽取的组件,直接通过
module federation
对外暴露组件服务。
上面在搭建业务组件库的时候,其实遇到了一个非常棘手的问题,有些组件跨了很多项目,链路很长,在抽取的过程中难度非常大,大家看一个图:
服务之间耦合太严重,从而导致组件抽取难度很大,那如何解决? 答案就是module federation
,抽取不了,就不抽取了,直接通过exposes
对外暴露组件服务,在其它子服务中调用即可。
下面给大家举一个接入microApp
的例子:
基座服务(主应用)
import microApp from '@micro-zoe/micro-app';
microApp.start()
添加组件容器(主应用)
<template>
<micro-app name="vms" :url="url" baseroute="/" @datachange='handleDataChange'>micro-app>
template>
<script>
export default {
name: 'ChildApp',
data() {
return {
activeMenu: '',
url: 'http://xxxx',
};
},
methods:{
handleDataChange({ detail: { data } }) {
// todo
},
}
};
script>
分配菜单(主应用)
{
path: '/child',
name: 'child',
component: () => import('./../layout/microLayout.vue'),
meta: {
appName: 'vms',
title: '子服务A'
}
}
就这样,一个主服务就搭建好了,等子服务上线以后,点击/child
菜单,就能打开对应的子服务了。 当然因为我们是一个超级应用,所以我们在做的过程中其实遇到了很多问题,比如:跨域问题、变量问题、包共享问题、本地开发调试问题、远程加载问题等等,不过大家可以参考官方文档,都是可以解决的。
Rocket-render接入
这是我个人开源的一套基于Vue2
的渲染引擎,通过json
定义就可以快速搭建各种简单或复杂的表单、表格场景,跟formly
这一类非常相似。
- 插件文档:rocket-render
- 开发文档:rocket-doc
给大家举一个简单的例子:
- 安装插件
yarn add rocket-render -S
- 组件注册
// 导入包
import RocketRender from 'rocket-render';
// 导入样式
import 'rocket-render/lib/rocket-render.css';
// 默认安装
Vue.use(RocketRender);
// 或者使用自定义属性,以下的值都是系统默认值,如果你的业务不同,可以配置下面的全局变量进行修改。
Vue.use(RocketRender, {
size: 'small',
empty: '-',
inline: 'flex',
toolbar: true,
align: 'center',
stripe: true,
border: true,
pager: true,
pageSize: 20,
emptyText: '暂无数据',
});
插件支持自定义属性,建议默认即可,我们已经给大家调好,如果确实想改,就通过自定义属性的方式进行修改。
- 页面应用
search-form 自带背景色和内填充,如果你有特殊需要,可以添加 class 进行覆盖,另外建议给 model 添加 sync 修饰符。
<template>
<search-form
:json="form"
:model.sync="queryForm"
@handleQuery="getTableList"
/>
template>
<script>
export default {
data() {
return {
queryForm: {},
form: [
{
type: 'text',
model: 'user_name',
label: '用户',
placeholder: '请输入用户名称',
},
],
};
},
};
script>
我们针对需要自定义部分提供了slot插槽,所以不用担心用了以后,对于复杂需求支持不了,导致返工的现象,完全不存在,除了json
以外,我们还内置了非常多的开发技巧,保证你爱不释手,继续看几个例子:
- 日期范围组件,通过
export
直接暴露两个字段。
{
type: 'daterange',
model: 'login_time',
label: '日期范围',
// 对于日期范围控件来说,一般接口需要拆分为两个字段,通过export可以很方便的实现字段拆分
export: ['startTime', 'endTime'],
// 日期转换为时间戳单位
valueFormat: 'timestamp', // 支持:yyyy-MM-dd HH:mm:ss
defaultTime: ['00:00:00', '23:59:59'], //可以设置默认时间,有时候非常有用。后端查询的时候,必须从0点开始才能查到数据。
}
前端是一个组件对应一个字段,但是接口往往需要让你传开始和结束日期,通过export可以直接拆分,太好用了。
- 下拉组件支持一步请求
{
type: 'select',
model: 'userStatus',
label: '用户状态',
multiple: true, // 支持多选
filterable: true, //支持输入过滤
clearable: true,
// 如果下拉框的值是动态的,可以使用fetchOptions获取,必须返回promise
fetchOptions: async () => {
return [
{ name: '全部', id: 0 },
{ name: '已注销', id: 1 },
{ name: '老用户', id: 2 },
{ name: '新用户', id: 3 },
];
},
// 字段映射,用来处理接口返回字段,避免前端去循环处理一次。
field: {
label: 'name',
value: 'id',
},
options: [],
// 如果想要修改其它表单值,可以通过change事件来处理
change: this.getSelectList,
}
通过fetchOptions可以直接在里面调用接口来返回下拉的值。如果是静态的,直接定义options即可。 如果接口返回的类别字段不是label和value结构,可以通过field来做映射。我觉得用的太爽了。
还有很多,我就不举例子了,大家去看文档就好了,文档写的非常清楚。
性能优化
前面几部分做完以后,我们的代码总量基本已经降到了 30 万行左右了,而且整个构建时长从9min降到了 30s ,有了业务组件库的加持,简直不要太爽,不过你看到的这些,看起来容易,其实我们花费了近1年的时间才完成。 剩下就是提升系统性能了:
- 资源全部上
cdn
,不仅上cdn
,还要再阿里云针对图片开启webp
(需要做兼容处理),cdn
记得添加Cache-Control
缓存。 - 服务器全部支持
gzip
压缩。 - 添加
external
配置,我在npm
开发了一个vite-plugin-external-new
插件,可以帮你解决。
- 这个大家一定要做,因为可以有效降低vender体积,你通过LightHouse做性能分析,就能知道原因,对于体积较大的js会给你提示的。
- 通过
external
,我们可以直接让vue
、vue-router
、vuex
、element-ui
等等全部通过defer
加载。
- 建议在根html
中加一个
Loading
标签
<div id="app">
<div class="loading">加载中...div>
div>
这样做的好处是,如果vue.js
还没有加载完成之前,可以让页面先loading
,等new Vue({ el: '#app' })
执行以后,才会覆盖#app
里面的 内容,这样可以提升FCP
指标。 5. 对于比较大的插件,建议按需
export const loadScript = (src: string) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.type = 'text/javascript';
script.defer = true;
script.onload = resolve;
script.onerror = reject;
script.src = src;
document.head.append(script);
});
};
某一个页面如果使用较大的插件,比如:xlsx、html2Canvas等,可以单独在某一个页面内做按需加载。
- 有些页面也可以针对
vue
组件或者大图片做按需加载。 - 其它性能优化,我觉得可做可不做,针对LightHouse分析报告针对性下手即可,可能很多文章将性能优化会非常细致,比如chunk分包等等,我倒觉得不是很重要。 只要把上面的做完,理论上前端的性能已经会有巨大的提升了。
结果指标
指标 | 优化前 | 优化后 |
---|---|---|
构建时长 | 6-9min | 30-45s |
代码行数 | 46万 | 30 万 |
服务 | 1个 | 7个 |
业务组件库 | 乱七八糟 | 基于rollup开发构建 |
基础框架 | 乱七八糟 | 高逼格 |
性能评分 | 30分 | 92分 |
团队成员 | 9个 | 4个 |
以上就是我面对一个 46 万行代码的超级系统,耗时大概一年的时间所做的一些成果,很多技术细节也只是泛泛而谈,但是我希望给大家提供的是工作方法、解决思路等偏软实力方面,技术功底和技术视野还是要靠自己一步一个脚印。
这一年我过的很充实,面对突如其来的问题,我心情很复杂,这一年,我感觉也老了很多。最后,感谢你耐心的倾听,我是河畔一角,一个普通的前端工程师。
来源:juejin.cn/post/7394095950383710247
在自己没有价值前,就不要谈什么人情世故了!
Hello,大家好,我是 Sunday。
昨天有位同学跟我吐槽,大致的意思是:“感觉自己不会搞 人情世故,导致在职场中很吃亏。领导也不罩着自己!”
在我的印象中,好像很多程序员都是 “不懂人情世故的典型”,至少很多程序员都认为自己是 “不懂人情世故的”。
但是,人情世故是什么?它真的有用吗?你跟领导关系好,他就会罩着你,帮你背锅吗?
恐怕是:想多了!!
一个真实的故事
给大家讲一个之前我经历过的真实故事,里面涉及到两个人物,我们用:领导 和 员工A 代替。
员工A是一个很懂 “人情世故” 的人,主要体现在两个方面:
- 酒桌文化:不像我这种压根就不能喝酒的人。员工A的酒量很好,并且各种喝酒的说法了熟于心(可以把领导说的很舒服的那种)
- 开会文化:各种反应都在领导的 “兴奋点” 上。我不知道怎么进行形容,类似于罗老师的这张图片,大家自己体会
其他方面的事情(私下的吃饭、逢年过节送礼),这些我就不清楚了,所以这里就不乱说了。
在我的眼里看来,这应该就是 熟通人情世故 的了。不知道,大家认为是不是。
不过,结果呢?
当公司决定裁员时,员工A 是 最早一批 出现在裁员名单中的。
领导会帮他争取留下来的机会吗?并不会
当你只能为对方带来 “情绪价值” 时,对方并不会把你当成心腹,更多的只是类似“马屁精”的存在。而这样的情绪价值,并没有太大的意义。更不要指望 领导会为了你做一些影响他自己利益,或者为他自己带来风险的事情了。
在自己没有价值前,就不要谈什么人情世故了!
国人在很多时候都会探讨 “人情世故” 的重要性。因为我生在 山东,对此更是感触颇深。(山东是受 儒家思想 熏陶最为严重的地方)。甚至,在之前,我也一度认为 “人情世故” 是非常重要的一件事情。
但是,当我工作之后进入企业以来。我越来越发现,在企业之中,所谓的 “人情世故” 并没有太大的意义。
人都是非常现实的,别人对你的看法,完全取决于你能为对方带来什么价值。
而这个价值通常需要体现在 金钱上 或者 事业上!
当你无法在这两个方面为对方提供价值时,那么你做的所谓的 “人情世故” 也会被对方认为是“马屁精”的嫌疑。
所以,与其把精力放到所谓的“人情世故”中,甚至为此而感到苦恼(就像开头所提到的同学一样),是 大可不必 的!
在你无法为对方带来价值之前,先努力提升自己的的能力,当你可以和对方处于一个平等的位置进行交流时,再去谈所谓的人情世故,也不迟!
来源:juejin.cn/post/7393713240995676175
去寺庙做义工,有益身心健康
《乔布斯传》中写到:乔布斯把对事物专注的能力和对简洁的热爱归功于他的禅修。他说:“禅修磨炼了他对直觉的欣赏能力,教他如何过滤掉任何能分散时间和精力的其它不必要的事情,在他的身上培养出了专注基于至简主义的审美观。”
如何在当今物欲横流的浮躁社会里不沦陷其中?如何在每天奔波忙碌之后却不内心疲惫、焦虑?如何在巨大的工作与生活压力下保持一颗平和的心?如何在经历感情、友情和亲情的起起落落后看破放下?如何改变透支健康和生命的人生模式?
程序员无疑是一个高压的职业,尤其是在头部公司工作的程序员们,工作压力更是大。并且在互联网行业,禅修并不是一件新鲜事。我们不一定要正儿八经地参加禅修活动,只是去寺庙走一走,呼吸一下新鲜空气,给寺庙干点活,对身心健康的帮助也会很大。
我与寺庙
我最早接触寺庙是在2011年上军校的时候,我的一个老师,作为大校,经常在课上分享他周末在南京附近寺庙的奇闻轶事,也会分享他自己的一些人生体验和感悟,勾起了我对寺庙生活的向往。
2013年,作为现役军人,我跑到了江西庐山的东林寺做了一个礼拜的义工,在那里,每天早上四点起床早上寺庙早课,负责三餐的行堂,也作为机动义工,干一些杂活,比如卸菜、组装床等,晚上有时也可以听寺庙的传统文化课。
2013年底,我申请退出现役,于是14年春就可以休假了,根据流程不定期去各部门办理手续即可,期间一个周末,我弟带我去凤凰岭玩,偶遇一个皈依法会,为了能看到传说中的北大数学天才,我填了一个义工表,参加了皈依仪式。
因为没有考虑政府安排的工作,所以打算考个研,期间不时会去凤凰岭的寺庙参加活动。考完研后,到18年春季,周末节假日,基本都会去这个寺庙做义工,累计得有200天以上。
期间,作为骨干义工,参与了该寺庙组织的第二至第四届的IT禅修营,负责行堂、住宿和辅导员等相关的工作。
很多人都听过这样一件往事:2010年,张小龙(微信之父)偶然入住一个寺院,当时正是微信研发的关键时刻,因为几个技术难题,张小龙连续几天彻夜难眠,终于一气之下把资料撕得粉碎。
没想到负责打扫卫生的僧人看到后,竟然帮他把资料重新粘贴了起来,还顺手写下了几条建议。张小龙非常惊讶,打听过后才知道这位扫地僧出家前曾混迹IT界,是个著名的极客。
经扫地僧点化,张小龙回到广州苦攻一年后,微信终于大成。这件事传的很广也很玄乎,可信度不会太高,不过故事中张小龙入住的寺院,就是我常去的寺庙。
至于在故事中懂得IT的扫地僧,在这里遇到其实不是什么奇怪的事,你还有可能遇到第47届国际数学奥赛金牌得主贤宇法师,他来自北大数学系;或者是禅兴法师,他是清华大学流体力学博士;又或者贤启法师,他是清华大学核能和热能物理博士。
“扫地只不过是我的表面工作,我真正的职业是一位研究僧。” 《少林足球》这句台词的背后,隐藏着关于这个寺庙“高知僧团”的一个段子。
因为各种不可描述的原因,18年9月之后,我就很少去这个寺庙了,但我知道我依然很向往寺庙的生活。于是22年春,我下定决心离开北京去深圳,其中就有考虑到深圳后,可以去弘法寺或弘源寺做义工。
去了一次弘法寺,感觉那边人太多,后面去了一次弘源寺后,感觉这里比较适合我,人少很安静,不堵车的话,开车只需20分钟到家。
目前,只要我有时间,我都会去弘源寺干一天临时义工,或者住上几天。
何为禅?
禅,是心智的高度成熟状态。直至印度词汇传入,汉语音译为“禅那”,后世简称为“禅”,汉译意思有“静虑”、“思维修”等。
禅修的方法就是禅法,禅法是心法,并不固着于某种具体形式,也不限于宗教派别。从泛义来说,任何一种方法,如果能够让你的心灵成熟,了解生命的本质,让心获得更高层次的证悟,并从而获得生命究竟意义的了悟。这样的方法就是禅修!
从狭义来说,在绵延传承数千年的漫长时空里,形成各种系统的修行方法,存在于各种教派中。现存主要有传承并盛行于南传佛教国家的原始佛教禅法与传承并盛行于中国汉传佛教的祖师禅。
如来禅是佛陀的原始教法,注重基础练习,强调修行止观。祖师禅是中国禅宗祖师的教法,强调悟性、觉性,推崇顿悟,以参话头为代表,以开悟、明心见性为目的。
我们普遍缺乏自我觉察,甚至误解了包括自由在内的生命状态真义。禅修中,会进入深刻自我觉察中,有机会与自己整合,从而开启真我。
近年来,禅修在西方非常流行,像美国的学校、医疗机构和高科技公司都广泛地在进行打坐、禅修。美国有些科学家曾做过一个实验,实验对象是长期禅修的修行人。在实验室中,实验者一边用脑电波图测量脑波的变化,一边用功能性核磁共振测量脑部活动的位置。
最后得出结论:通过禅修,不但能够短期改变脑部的活动,而且非常有可能促成脑部永久的变化。这就是说:通过禅定,可以有效断除人的焦虑、哀伤等很多负面情绪,创造出心灵的幸福感,甚至可以重塑大脑结构。
禅修能够修复心智、疗愈抑郁、提升智慧,让我们重获身心的全面健康!禅修让人的内心变得安静。在禅修时,人能放松下来,专注于呼吸,使内心归于平静,身体和心灵才有了真正的对话与接触。
“禅修是未来科技世界中的生存必需品”时代杂志曾在封面报道中这样写道。在硅谷,禅修被认为是新的咖啡因,一种能释放能量与创造力的全新“燃料”。
禅修也帮助过谷歌、Facebook、Twitter高管们走出困惑,国内比较知名则有搜狐的张朝阳和阿里的马云,还有微信之父张小龙的传说。
对于他们来说,商海的起伏伴随着心海的沉浮,庞大的财富、名声与地位带来的更多的不是快乐,但是禅修,却在一定程度上给他们指点迷津,带领他们脱离现代社会的痛苦、让内心更加平静!
乔布斯的禅修故事
乔布斯和禅修,一直有着很深的渊源。乔布斯是当世最伟大的企业家之一,同时也是一名虔诚的禅宗教徒。他少有慧根,17岁那年,他远赴印度寻找圣人寻求精神启蒙,18岁那年,他开始追随日本禅师乙川弘文学习曹洞宗的禅法。
年轻的时候,乔布斯去印度,在印度体验,呆了七个月。乔布斯在印度干了些什么,我们不得而知。不过据我推测,也就是四处逛一逛,看一看,可能会去一些寺庙,拜访一些僧人。
我从来不认为,他遇到了什么高人,或者在印度的小村庄一待,精神就受到了莫大的洗礼。变化永远都是从内在发生的,外在的不过是缘分,是过客,负责提供一个合适的环境,或者提供一些必要的刺激。
但我们知道,从此以后,乔布斯的人生,就开始变得不一样了。乔布斯的人生追求是“改变世界”,当年他劝说百事可乐总裁,来担任苹果CEO的时候所说的话:“你是愿意一辈子卖糖水,还是跟我一起改变这个世界?”激励了无数心怀梦想的朋友。
早在1973年乔布斯已经对禅有较深的领悟了。他这样说:“我对那些超越物质的形而上的学说极感兴趣,也开始注意到比知觉及意识更高的层次——直觉和顿悟。”
他还说:“因为时间有限,不要带着面具为别人而活,不要让别人的意见左右自己内心的想法,最重要的是要勇敢地忠于自己内心的直觉。”
乔布斯说:“你不能预先把点点滴滴串在一起;唯有未来回顾时,你才会明白那些点点滴滴是如何串在一起的。所以你得相信,你现在所体会的东西,将来多少会连接在一块。你得信任某个东西,直觉也好,命运也好,生命也好,或者业力。这种作法从来没让我失望,也让我的人生整个不同起来。”
他大学时学的书法,被他用来设计能够印刷出漂亮字体的计算机,尽管他在大学选修书法课时,完全不知道学这玩意能有什么用。他被自己创立的苹果公司开除,于是转行去做动画,结果在做动画的时候,遇到了自己未来的妻子。
人呐实在不知道,自己可不可以预料。你说我一个被自己创立的公司开除的失业狗,怎么就在第二份工作里遇到了一生的挚爱呢?若干年后,他回顾起自己的人生,他把这些点点滴滴串了起来,他发现,他所经历过的每一件事,都有着特殊的意义。
所以,无论面对怎样的困境,我们都不必悲观绝望,因为在剧本结束之前,你永远不知道,自己现在面对的这件事,到底是坏事还是好事。
所谓创新就是无中生有,包括思想、产品、艺术等,重大的创新我们称为颠覆。通过那则著名的广告《think different》他告诉世人:“因为只有那些疯狂到以為自己能够改变世界的人,才能真正地改变世界。”乔布斯确实改变了世界,而且不是一次,至少五次颠覆了这个世界:
- 通过苹果电脑Apple-I,开启了个人电脑时代;
- 通过皮克斯电脑动画公司,颠覆了整个动漫产业;
- 通过iPod,颠覆了整个音乐产业;
- 通过iPhone,颠覆了整个通讯产业;
- 通过iPad,重新定义并颠覆了平板PC行业。
程序员与禅修
编程是一门需要高度专注和创造力的艺术,它要求程序员们在面对复杂问题和压力时,能够保持内心的安宁和平静。在这个快节奏、竞争激烈的行业中,如何修炼内心的禅意境界,成为程序员们更好地发挥潜力的关键。
在编程的世界里,专注是至关重要的品质。通过培养内在专注力,程序员能够集中精力去解决问题,避免被外界的干扰所困扰。以下是几种培养内在专注的方法:
- 冥想和呼吸练习: 通过冥想和深呼吸来调整身心状态,让自己平静下来。坚持每天进行一段时间的冥想练习,可以提高专注力和注意力的稳定性。
- 时间管理: 制定合理的工作计划和时间表,将任务分解为小的可管理的部分,避免心理上的压力。通过专注于每个小任务,逐步完成整个项目。
- 限制干扰: 将手机静音、关闭社交媒体和聊天工具等干扰源,创造一个安静的工作环境。使用专注工作法(如番茄钟),集中精力在一项任务上,直到完成。
编程过程中会遇到各种问题和挑战,有时甚至会感到沮丧和失望。然而,保持平和的心态是非常重要的,它可以帮助程序员更好地应对压力和困难。以下是一些培养平和心态的技巧:
- 接受不完美性: 程序永远不会是完美的,因为它们总是在不断发展和改进中。接受这一事实,并学会从错误中汲取教训。不要过于苛求自己,给自己一些宽容和理解。
- 积极思考: 关注积极的方面,让自己的思维更加积极向上。遇到问题时,寻找解决方案而非抱怨。积极的心态能够帮助你更好地应对挑战和困难。
- 放松和休息: 给自己合理的休息时间,让大脑得到充分的放松和恢复。休息和娱乐能够帮助你调整心态,保持平和的状态。
编程往往是一个团队合作的过程,与他人合作的能力对于一个程序员来说至关重要。以下是一些建立团队合作意识和促进内心安宁的方法:
- 沟通与分享: 与团队成员保持良好的沟通,分享想法和问题。倾听他人的观点和建议,尊重不同的意见。积极参与和贡献团队,建立合作关系。
- 友善和尊重: 培养友好、尊重和包容的态度。尊重他人的工作和努力,给予鼓励和支持。与团队成员建立良好的关系,创造和谐的工作环境。
- 共享成功: 当团队取得成功时,与他人一起分享喜悦和成就感。相信团队的力量,相信集体的智慧和努力。
修炼内心安宁需要时间和长期的自我管理。通过培养专注力、平和心态、创造力和团队合作意识,程序员们可以在面对复杂的编程任务和挑战时保持内心的安宁和平静。
禅修有许多不同的境界,其中最典型的可能包括:
- 懵懂:刚开始禅修时,可能会觉得茫然和困惑,不知该如何开始。
- 困扰:在进行深度内省和冥想时,可能会遇到很多烦恼和难题,需耐心思考和解决。
- 安和:通过不断地练习和开放自己的心灵,可能会进入一种更加平和和沉静的状态。
- 祥和:当一些心理障碍得到解决,你会感受到一种更深层的平静和和谐。
- 转化:通过不断的冥想与内省,你可以向内看到自己的内心,获得对自己和世界的新的认识和多样的观察角度。
- 整体意识:通过冥想,您将能够超越个人的视野和言语本身,深入探究宇宙的内心,领悟更加深入和广泛的境界和意识。
程序员写代码的境界:
- 懵懂:刚熟悉编程语言,不知做什么。
- 困扰:可以实现需求,但仍然会被需求所困,需要耐心思考和解决。
- 安和:通过不断练习已经可以轻易实现需求,更加平和沉静。
- 祥和:全栈。
- 转化:做自己的产品。
- 整体意识:有自己的公司。
一个创业设想
打开小红书,与“疗愈”相关的笔记高达236万篇,禅修、瑜伽、颂钵等新兴疗愈方法层出不穷,无论是性价比还是高消费,总有一种疗愈方法适合你。
比起去网红景点打卡拍照卷构图卷妆造,越来越多的年轻人正在借助上香、拜神、颂钵、冥想等更为“佛系”的方式去追寻内心的宁静。放空大脑,呼吸之间天地的能量被尽数吸收体内,一切紧张、焦虑都被稀释,现实的残酷和精神的困顿,都在此间找到了出口。
在过去,简单的瑜伽和冥想就能达到这种目的,但伴随着疗愈文化的兴起与壮大,不断在传统方式之上叠加buff,才是新兴疗愈的终极奥义。
从目标人群来看,不同的禅修对应不同的人群。比如临平青龙寺即将在8月开启的禅修,就分为了企业禅修、教育禅修、功能禅修、共修禅、突破禅、网络共修等多种形式。但从禅修内容来看,各个寺庙的安排不尽相同,但基本上跳脱不出早晚功课、上殿过堂、出坡劳作、诵经礼忏、佛学讲座等环节。
艺术疗愈,是截然不同于起参禅悟道这种更亲近自然,还原本真的另一疗愈流派。具体可以细分为戏剧疗愈、绘画疗愈、音乐疗愈等多种形式。当理论逐渐趋向现代化,投入在此间的花费,也成正比增长。
绘画疗愈 ,顾名思义就是通过绘画的方式来表达自己内心的情绪。画幅的大小、用笔的轻重、空间的配置、色彩的使用,都在某种程度上反映着创作者潜意识的情感与冲突。
在绘画过程中,绘画者也同样会获得纾解和满足。也有一些课程会在绘画创作之外,添加绘画作品鉴赏的内容,通过一幅画去窥视作者的内心,寻求心灵上的共鸣,也是舒缓压力的一种渠道。
疗愈市场之所以能够发展,还是因为有越来越多人的负面情绪需要治愈。不论是工作压力还是亲密关系所带来的情绪内耗,总要有一个释放的出口。
当前,我正在尝试依托自营绘馆老师提供优质课件,打造艺培课件分享的平台或社区,做平台前期研发投入比较大,当前融资也比较困难,同时自己也需要疗愈。
所以,最近也在调研市场,评估是否可以依托自营的门店,组织绘画手工+寺庙行禅+技术专题分享的IT艺术禅修营活动,两天含住宿1999元,包括半天寺庙义工体验、半天禅修、半天绘画手工课和半天的技术专题分享。
不知道,这样的活动,大家会考虑参加吗?
总结
出家人抛弃尘世各种欲望出家修行值得尊重,但却不是修行的唯一方法,佛经里著名的维摩洁居士就是在家修行,也取得了非凡成就,六祖惠能就非常鼓励大家在世间修行,他说:“佛法在世间,不离世间觉,离世觅菩提,恰如求兔角”。
普通人的修行是在红尘欲望中的修行,和出家人截然不同,但无分高下,同样可以证悟,工作就是他们最好的修练道场。禅学的理论学习并不困难,但这只是万里长征的第一步,最重要的是,我们要在日常实践中证悟。
简单可能比复杂更难做到:你必须努力理清思路,从而使其变得简单。但最终这是值得的,因为一旦你做到了,便可以创造奇迹。”乔布斯所说的这种专注和简单是直接相关的,如果太复杂,心即散乱,就很难保持专注,只有简单,才能做到专注,只有专注,才能极致。
来源:juejin.cn/post/7292781589477687350
程序员的副业发展
前言
之前总有小伙伴问我,现在没有工作,或者想在空闲时间做一些程序员兼职,怎么做,做什么,能赚点外快
因为我之前发别的文章的时候有捎带着说过一嘴我做一些副业,这里就说一下我是怎么做的,都做了什么
希望能对你有些帮助~
正文
学生单
学生单是我接过最多的,已经写了100多份毕设,上百份大作业了,这里给大家介绍一下
像python
这种的数据处理的大作业也很多,但是我个人不太会,所以没结过,我只说我做过的
我大致做过几种单子,最多的是学生的单子,分为大作业单子
和毕设单子
大作业单一般指一个小作业,比如:
- 几个web界面(大多是html、css、js)
- 一个全栈的小demo,大多是
jsp+SSM
或者vue+springboot
,之所以不算是毕设是因为,页面的数目不多,数据库表少,而且后端也很简单
我不知道掘金这里能不能说价格,以防万一我就不说大致价格了,大家想参考价格可以去tb
或者咸鱼
之类的打听就行
然后最多的就是毕设单子,一般就是一个全栈的项目
- 最多的是
vue+springboot
的项目,需求量特别大,这里说一下,之前基本都是vue2的项目,现在很多学校要求vue3了,但是大部分商家vue3的模板很少,所以tb上接vue3的项目要么少,要么很贵,所以我觉得能接vue3和springboot项目的可以打一定的价格战,vue2的市面上价格差不多,模板差不多,不好竞争的 - 少数
vue+node
的全栈项目,一般是express
或者koa
,价格和springboot差不多,但是需求量特别少 uni+vue+springboot
的项目,其实和vue+springboot
项目差不多,因为单纯的vue+springboot项目太多了,所以现在很多人要求做个uni手机端,需求量适中.net项目
,信管专业的学生用.net的很多,需求量不少,有会的可以考虑一下
这是我接过的比较多的项目,数据库我没有单说,基本上都是MySQL,然后会要求几张表,以及主从表有几对,这就看客户具体要求了
需要注意的点:大部分你得给客户配环境,跑程序,还是就是毕设一般是要求论文的,有论文的会比单纯程序赚的多,但是一定要注意对方是否要求查重,如果要求查重,一般是不建议接的,一般都是要求维普和知网查重,会要了你的老命。还有需要注意的是,学生单子一般是需要答辩的,你可以选择是否包答辩,当然可以调整价格,但是你一旦包答辩,你的微信在答辩期间就不会停了。你永远不知道他们会有怎样的问题
商业单
商业单有大有小,小的跟毕设差不多,大的需要签合同
我接的单子大致就一种,小程序+后台管理+后端
,也就是一个大型的全栈项目,要比学生单复杂,而且你还要打包、部署、上线,售后
,有一个周期性,时间也比较长
为了防止大家不信,稍微放几个聊天记录,是这两个月来找的,也没有给自己打广告,大家都是开发者,开发个小程序有什么打广告,可吹的(真的是被杠怕了)
技术栈有两种情况:自己定
,客户定
UI也有两种情况:有设计图的
、无设计图的(也就是自己设计)
基本上也就是两种客户:懂技术的客户,不懂技术的客户
指定技术栈的我就不说了,对于不指定技术栈的我大致分为两种
小程序端:uni/小程序原生、后台:vue、后端:云开发
小程序端:uni/小程序原生、后台:vue、后端:springboot
这取决于预算,预算高的就用springboot、预算不高的就云开发一把嗦,需要说的是并不是说云开发差,其实现在云开发已经满足绝大部分的需求,很完善了,而springboot则是应用广泛,客户后期找别人接手更方便
对于没有UI
设计图的,我会选择去各种设计网站去找一些灵感
当项目达到一定金额,会签署合同,预付定金,这是对双方的一种保障
其实在整个项目中比较费劲的是沟通,不是单独说与客户的沟通,更多的是三方沟通,作为上线的程序,需要一些资料手续,这样就需要三方沟通,同时还有一定的周期,可能会被催
讲解单
当然,有的时候人家是有程序的,可能是别人代写的,可能是从开源扒下来的,这个时候客户有程序,但是看不懂,他们可能需要答辩,所以会花钱找人给他们梳理一下,讲一讲, 这种情况比较简单,因为不需要你去写代码,但是需要你能看懂别人的代码
这种情况不在少数,尤其是在小红书这种单子特别多,来钱快,我一般是按照小时收费
知识付费这东西很有意思,有时候你回答别人的一些问题,对方也会象征性地给你个几十的红包
接单渠道
我觉得相对于什么单,大家更在意的是怎么接单,很多人都接不到单,这才是最难受的
其实对此我个人并没有太好的建议的方法,我认为最重要的,还是你的交际能力
,你在现实中不善于交际,网络上也不善于交际,那就很难了
因为我之前是在学校,在校期间干过一些兼职,所以认识的同学比较多,同时自身能力还可以,所以会有很多人来找,然后做完之后,熟人之间会慢慢介绍,人就越来越多,所以我不太担心能否接单这件事,反而是单太多,自己甚至成立一个小型工作室去接单
如果你是学生的话,一定要在学校积累客户,这样会越来越多,哪怕是现在我还看到学校的各种群天天有毕业很多年以及社会人士来打广告呢,你为什么就不可以呢
当然但是很多人现在已经不是学生了,也不知道怎么接触学生,那么我给大家推荐另外的道路
闲鱼
接单小红书
接单
大部分学生找的写手都会比较贵,这种情况下,很多学生都会选择去上面的两个平台去货比三家,那么你的机会就来了
有人说不行啊,这种平台发接单帖子就被删了,那么你就想,为什么那么多人没被删,我也没被删,为什么你被删除了
其次是我最不推荐的一种接单方式:tb写手
为什么不推荐呢,其实就是tb去接单,然后会在tb写手群外包给写手,也就是tb在赚你的差价
这种感觉很难受,而且赚的不多,但是如果你找不到别的渠道,也可以尝试一下
最后
我只是分享一下自己接单的方式,但是说实在的,接一个毕设单或者是商业单其实挺累的,不是说技术层面的,更多的是心累,大家自行体会吧,而且现在商场内卷严重,甚至有人200、300就一个小程序。。。
所以大家要想,走什么渠道,拿什么竞争
另外,像什么猪八戒这种的外包项目的网站,我只是见过,但是没实际用过,接过,所以不好评价
希望大家赚钱顺利,私单是一种赚钱的方式,但是是不稳定的,一定还是要以自己本身的工作为主,自行判断~
来源:juejin.cn/post/7297124052174848036
小程序和h5有什么差别
差别
微信小程序和H5应用在实现原理上的差异主要体现在架构、渲染方式、数据通信、运行环境和API接口等方面。以下是详细的对比:
1. 架构和运行环境
微信小程序:
架构:微信小程序主要分为逻辑层(JavaScript)和视图层(WXML、WXSS)。逻辑层运行在小程序的JSCore中,而视图层运行在WebView(它是基于浏览器内核重构的内置解析器,它并不是一个完整的浏览器,官方文档中重点强调了脚本内无法使用浏览器中常用的
window
对象和document
对象,就是没有DOM
和BOM
的相关的API
,这一条就干掉了JQ
和一些依赖于BOM
和DOM
的NPM包)中,两者通过平台提供的桥接机制进行通信。运行环境:逻辑层在微信提供的JS引擎中运行,视图层在微信内置的WebView中渲染。
H5 应用:
架构:H5应用是一个整体。HTML、CSS和JavaScript共同构成了一个Web页面。
运行环境:H5应用在浏览器中运行,所有代码都在浏览器的环境中解析和执行。
2. 渲染方式
微信小程序:
微信小程序采用双线程模型,将逻辑层和视图层分离,分别运行在不同的线程中(两者通过平台提供的桥接机制进行通信):
逻辑层:运行在小程序的JSCore环境中,负责处理业务逻辑、数据计算和API调用。
视图层:运行在WebView中,负责渲染用户界面和处理用户交互。( 性能提升:由于小程序的渲染过程并不依赖于JS,因此即使JS线程发生阻塞,页面的渲染也不会受到影响。这种机制有利于提高渲染效率,减少卡顿,提升用户体验。)
通信桥接机制
逻辑层和视图层之间不能直接访问和操作对方的数据和界面,因此需要通过微信小程序框架提供的桥接机制来进行通信。这种通信机制通常包括以下几个方面:
1. 数据绑定和响应式更新(逻辑层--->视图层)
逻辑层通过数据绑定的方式将数据传递给视图层,视图层根据数据变化自动更新界面。数据绑定的过程如下:
设置数据:逻辑层通过
Page
或Component
实例的setData
方法,将数据传递给视图层。更新视图:视图层接收到数据变化的消息后,根据新的数据重新渲染界面。
2. 事件处理(视图层--->逻辑层)
视图层中的用户交互(如点击、输入等)会触发事件,这些事件通过桥接机制传递给逻辑层进行处理。事件处理的过程如下:
事件绑定:在视图层(WXML)中定义事件处理函数。
事件触发:用户在界面上进行交互时,触发相应的事件。
事件传递:视图层将事件信息通过桥接机制传递给逻辑层。
事件处理:逻辑层的事件处理函数接收到事件信息,执行相应的业务逻辑。
3. 消息传递
逻辑层和视图层之间的通信实际是通过消息传递的方式实现的。微信小程序框架负责在两个层之间传递消息,包括:
逻辑层到视图层的消息:如数据更新、视图更新等。
视图层到逻辑层的消息:如用户交互事件、视图状态变化等
通信桥接机制具体实现
依赖于微信小程序框架内部的设计和优化,开发者无需直接接触底层的通信细节。以下是桥接机制的一些关键点:
消息队列:逻辑层和视图层之间维护一个消息队列,用于存储待传递的消息。
消息格式:消息以JSON格式进行编码,包含消息类型、数据内容等信息。
消息处理:逻辑层和视图层各自维护一个消息处理器,负责接收、解析和处理消息。
异步通信:消息传递通常是异步进行的,以确保界面和逻辑的流畅性和响应性
H5 应用:
H5应用的逻辑层和视图层通常是在同一线程(主线程)中运行,直接通过JavaScript代码操作DOM来更新界面。主要的通信方式包括:
直接DOM操作:通过JavaScript直接操作DOM元素,更新界面。
事件监听和处理:通过JavaScript监听DOM事件(如点击、输入等)并处理。
数据绑定:使用现代前端框架(如Vue.js、React.js)的数据绑定和响应式机制,实现视图的自动更新。
3. 数据通信
微信小程序:
通信机制:逻辑层和视图层之间的通信通过小程序框架提供的机制来实现,通常是通过事件和数据绑定。
后台通信:可以通过小程序提供的API与服务器通信,例如wx.request等。
H5 应用:
通信机制:页面内的通信可以通过DOM事件、JavaScript函数调用等方式实现。
后台通信:可以使用标准的AJAX请求、Fetch API、WebSocket等方式与服务器通信。
4. 运行机制
微信小程序
启动
如果用户已经打开过某小程序,在一定时间内再次打开该小程序,此时无需重新启动,只需将后台态的小程序切换到前台,整个过程就是所谓的
热启动
如果用户首次打开或小程序被微信主动销毁后再次打开的情况,此时小程序需要重新加载启动,就是
冷启动
销毁
当小程序进入后台一定时间,或系统资源占用过高,或者是你手动销毁,才算真正的销毁
h5:解析HTML CSS形成DOM树和CSSOM树,两者结合形成renderTree,js运行,当然中间存在一系列的阻塞问题,还有同源策略等等
5. 系统权限方面(特定功能)
微信小程序依托于微信平台,能够利用微信提供的特有功能和API,实现许多H5应用无法直接实现或不易实现的功能,如微信支付、微信登录、硬件接口(如摄像头、麦克风、蓝牙、NFC等)、微信特有功能等。
6.更新机制
h5更新后访问地址即可
微信小程序需要审核
开发者在发布新版本之后,无法立刻影响到所有现网用户,要在发布之后 24 小时之内才下发新版本信息到用户
小程序每次
冷启动
时,都会检查有无更新版本,如果发现有新版本,会异步下载新版本代码包,并同时用客户端本地包进行启动,所以新版本的小程序需要等下一次冷启动
才会应用上,当然微信也有wx.getUpdateManager
可以做检查更新
7. 开发工具和调试
微信小程序:
开发工具:微信提供了专门的开发者工具,集成了调试、预览、上传等功能,方便开发者进行开发和测试。
调试:可以使用微信开发者工具进行实时调试,并提供丰富的日志和调试信息。
H5 应用:
开发工具:可以使用任何Web开发工具和IDE(如VS Code、WebStorm等),以及浏览器的开发者工具进行调试。
调试:依赖浏览器的开发者工具(如Chrome DevTools),可以进行断点调试、查看网络请求、分析性能等。
总结来说,微信小程序和H5应用在实现原理上的差异主要是由于它们的架构设计、运行环境和生态系统的不同。小程序依托于微信平台,提供了许多平台专属的优化和功能,而H5应用则更加开放和灵活,依赖于浏览器的标准和特性。
小程序为什么使用双层架构
微信小程序采用双线程架构的原因主要是为了优化性能和用户体验。双线程架构将逻辑层和视图层分离,使得业务逻辑处理和视图渲染在不同的线程中进行,从而提高了小程序的运行效率和响应速度。以下是采用双线程架构的具体原因和优势:
提高性能:
将逻辑处理和页面渲染分离到不同的线程中,可以避免互相干扰,提高整体性能。例如,在复杂的业务逻辑计算过程中,视图层仍然可以保持流畅的界面更新和响应。
逻辑层和视图层通过消息机制进行异步通信,可以避免阻塞和卡顿。这样即使逻辑层的操作较为耗时,也不会影响界面的即时响应。
安全性: 视图层无法直接操作逻辑层的数据和代码,这样可以避免一些潜在的安全风险和漏洞。
XSS
由于逻辑层和视图层分离,视图层不能直接执行逻辑层的JavaScript代码。这种隔离使得即使视图层(WXML)中存在注入的恶意代码,也不能直接影响逻辑层的数据和操作。
逻辑层和视图层之间的通信通过统一的API进行,传递的数据会经过平台的安全检查和过滤,进一步减少了XSS攻击的风险。
CSRF
小程序通过平台的统一API进行请求,这些请求包含了平台自动添加的安全令牌(如
session_key
等),确保请求的合法性。由于逻辑层和视图层的分离,用户在视图层进行操作时,逻辑层的业务逻辑和数据处理经过平台的校验,减少了CSRF攻击的风险。
DOM篡改:视图层的DOM结构由WXML和WXSS定义,不能直接通过逻辑层的JavaScript代码进行操作,这种隔离减少了DOM篡改的可能性。
安全权限管理:小程序的API权限由平台统一管理和控制,开发者需要申请和用户授权后才能使用特定的API。
用户体验: 微信小程序在启动时可以并行加载逻辑层和视图层资源,减少初始加载时间,提升启动速度。同时,微信平台会对小程序进行预加载和缓存优化,进一步提升加载性能。
rpx
微信的自适应单位,可以根据屏幕宽度进行自适应。
在微信小程序中,1 rpx 表示屏幕宽度的 1/750,因此 rpx
和 px
的换算关系是动态的,基于设备的实际屏幕宽度。
作者:let_code
来源:juejin.cn/post/7389168680747614245
展开收起的箭头动画应该怎么做?
背景
我们在日常开发中,一定经常遇到折叠面板的开发,为了美观,我们经常会添加展开收起按钮,并且带有箭头旋转动画。
比如下面的几种情况
- 文字点击变化,且有箭头旋转动画
- 只有箭头动画
这几种情况的核心其实就是:点击箭头开始旋转,再点击箭头恢复初始位置。
如何实现
思路分析
要实现展开和收起箭头的旋转动画,我们可以使用 CSS 和 JavaScript。我们在点击按钮时,通过添加和移除 CSS 类,实现箭头的旋转动画。并且添加transition属性实现过渡效果。
代码实现
我们以第一种动画效果为例,先写基础代码
<template>
<div @click="open = !open">
<span>{{ open ? '收起' : '展开' }}</span>
<span>▼</span>
</div>
</template>
<script>
const open = ref(false)
</script>
现在我们点击按钮,只有文字会变化,箭头不会旋转
我们给按钮加一个动态类
<template>
<div @click="open = !open">
<span>{{ open ? '收起' : '展开' }}</span>
<span :class="{ rotate: open }">▼</span>
</div>
</template>
<script>
const open = ref(false)
</script>
<style scoped>
.rotate {
transform: rotate(180deg);
transition: transform 0.3s linear;
}
</style>
可以看到,展开的时候有动画,但是收起的时候是没有过渡效果的。
我们只需要加一个transition属性即可
<template>
<div @click="open = !open">
<span>{{ open ? '收起' : '展开' }}</span>
<span :class="{ rotate: open }" class="arrow">▼</span>
</div>
</template>
<script>
const open = ref(false)
</script>
<style scoped>
.arrow {
transition: transform 0.3s linear;
}
.rotate {
transform: rotate(180deg);
transition: transform 0.3s linear;
}
</style>
现在样式就ok了
html版本
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Arrow Rotation</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<button id="toggleButton">
<span id="arrow" class="arrow">▼</span>
</button>
</div>
<script src="script.js"></script>
</body>
</html>
css
/* styles.css */
.container {
text-align: center;
margin-top: 50px;
}
#toggleButton {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
outline: none;
}
.arrow {
display: inline-block;
transition: transform 0.3s ease;
}
.arrow.rotate {
transform: rotate(180deg);
}
js
// script.js
document.getElementById('toggleButton').addEventListener('click', function() {
const arrow = document.getElementById('arrow');
arrow.classList.toggle('rotate');
});
这种方式可以实现箭头在点击时的旋转动画效果。在实际项目中使用,我们也可以根据具体需求调整样式和逻辑。
来源:juejin.cn/post/7385132403025149989
如果iconfont停止服务了,我们怎么办
前言
个人一直都比较喜欢阿里大佬提供的一些组件服务什么的,这里就只说图标管理吧,最开始的时候我是使用的icomoon.io 后面发现http://www.iconfont.cn/ 在以前开发是时候我们为了图片的请求少点,提升网站的性能会把这些小图标做到一张图上面专业叫法是雪碧图,后来随着前端发展有了字体图标,不啰嗦了吧,回到正题,由于上次icofont官网挂了之后,导致我的项目无法进行正常迭代,我就决定要自己弄个简单的图标管理。
需求
一般使用需要的是把设计做好的图标或者其他地方下载的svg图标上传到服务器然后可以查看,并且打包成一个js文件加载到项目中。
准备
都说天下文章一大抄,我也是去抄iconfont,我把iconfont的使用demo打开研究了一下。
我这里只说Symbol,通过分析看到就是我们需要在项目中引入./iconfont.js
iconfont把我们上传的图标进行了删减优化,最外层就一个svg标签里面使用了一个symbol标签来包裹之前的图标内容,每一个symbol的id都对应之前的svg图标名称。
iconfont也没有开源,具体怎么做的咱也不知道,只能知道目前这些信息了,然后就按照自己理解开始开发实现了
前端开发
通过上面我们可以知道,现在我们前端需要的是把设计稿的svg图变成symbol里面的内容,打开svg图 可以简单测试下,直接把内容复制然后粘贴带了iconfont.js中,然后运行demo图标正常显示就说明是ok的,不然就是这样做是不行的。
通过图片我们可以看到,现在需要的就是把svg里面的path改成用symblo来包裹,在开发中svg图我们是通过文档流上传上来的,这个时候我们就需要对文档流进行操作,先把文档流变成字符串,然后js操作字符串拼接达到目的。
- 使用到FileReader和readAsText获取到字符串
const fileParse = (file) => {
return new Promise((resolve) => {
const fileReader = new FileReader();
fileReader.readAsText(file, "UTF-8");
fileReader.onload = (e) => {
resolve({ content: e.target?.result, name: file.name })
}
})
}
- 字符操作拼接我使用的是cheerio
const handleUploadSvg = ($, result) => {
let index = result.indexOf('
通过上面的操作我们可以得到一个新的svg源码,图标显示还是flow
然后我把这个字符串传送给后端就行了
后端开发
后端要做的就是把我上传上来的svg字符串拼接到一起,然后以iconfont.js一样通过一个get接口返回给我就可以了, 通过浏览器可以访问到就说明ok了
其他两种需要用到些第三方库当时也有顺便研究看到了,如果有需要我再写一篇。
文章中会有些不怎么清晰或者有问题的地方还望各位大佬见谅,刚刚开始决定写一些技术相关文章,还很生疏
来源:juejin.cn/post/7340197367515578378
iOS 开发们,是时候干掉 Charles 了
这里每天分享一个 iOS 的新知识,快来关注我吧
前言
一说到 mac 上的抓包工具,大家自然而然的会想到 Charles,作为老牌抓包工具,它功能很全面,也很强大。但是随着系统的不断更新迭代,Charles 的一些缺点也慢慢表露出来,比如:
- 卡顿,特别在一些低端 Mac 机型上比较卡,体验就很差
- 吃内存,时间久了总是得重启一下,不然内存吃的太多
- 页面老旧,感觉像是旧时代的产品
今天来介绍一个我觉得比较好用的抓包工具,Proxyman
Proxyman 配置
安装就不说了,大家可以自行去官网下载安装。
Proxyman 提供了一个免费版本,其中包含所有基本功能,平时使用应该是够了,如果重度使用,也可以考虑购买高级版本。
这是他的主页面,看起来是不是挺干净的:
安装好了之后都需要配置代理和 https 证书,这点 Proxyman 做的非常好,首先点击顶部导航上的证书,可以看到所有安装证书的选项:
教程是全中文的,而且设置步骤非常详细,比如 iOS 设置指南:
Proxyman 针对 iOS 开发还提供了一种无配置的方案,可以直接通过 Pod 或者 SPM 添加 atlantis-proxyman
框架,这样可以在不进行任何配置的情况下进行代理监听:
除了监控手机的流量,也可以很方便地添加 iOS 模拟器的监控,只需要选择顶部菜单 -> 证书 -> 在 iOS 上安装证书 -> 模拟器:
按照以上步骤操作即可。
使用
配置完成之后就可以在 Proxyman 主页面上看到接口请求了,接下来介绍一些常用的功能。
本地 Mock 数据
本地 Mock 数据是很常见的需求,你只需要选中某个接口后,鼠标右键,选择工具 -> 本地映射:
然后在弹出的新页面中编辑相应即可,非常方便:
断点
断点工具可以让我们动态编辑请求或响应的内容。
它本地映射在同一个菜单栏里,鼠标右键,选择工具 -> 断点,然后进行对应的设置即可。
创建断点后,Proxyman 将在收到我们想要拦截的请求或响应后立即打开一个新的编辑窗口。然后我们根据需要修改数据,最后再继续即可。
导出请求和响应数据
有时候我们需要把有问题的接口保存下载给其他服务端的同学查看。选中具体的请求,点击鼠标右键,选择导出,然后再选择你要导出的格式:
不过这里导出的 Proxyman 日志需要使用 Proxyman 才能打开,也就是说,需要想查看这条请求的人的电脑上也安装 Proxyman,如果他没有安装,也可以选择拷贝 cURL。
模拟弱网
好的产品一定能够在弱网下正常使用,所以弱网测试也成为了日常开发必要的步骤,点击顶部菜单栏,选择工具 -> 网络状况,可以打开一个新页面,然后点击左下角为一个新的域名添加网络状况,这里可以根据你的需求选择不同的网络状况:
总结
从流畅度、功能引导等方面,我感觉 Proxyman 是比 Charles 好用的,除了以上介绍到的功能,还有很多更强大更全面的功能。例如远程映射、保存会话、GraphQL 调试、黑名单白名单、Protobuf、自定义脚本等等,大家可以自己试试看。
这里每天分享一个 iOS 的新知识,快来关注我吧
本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!
来源:juejin.cn/post/7355845238906175551
Flutter 为什么没有一款好用的UI框架?
哈喽,我是老刘
前两天,系统给我推送了一个问题。
我理解提问者真正想问的是:有没有一个不用学习那么多UI组件和渲染知识,可以简单快速搭建UI的东西。
Flutter 包括原生开发,为什么需要考虑那么多细节,不能做的简单一些?
首先,我们需要明白Flutter的定位。
Flutter不是一个简单的甜品,而是一个能支撑大型系统开发的工程级框架。
这种定位和原生框架的定位是相当的。
因此,它要求整个框架有足够的灵活性,能适用于尽可能多的场景。
那么,如何提供足够的灵活性呢?
答案是让整个框架尽可能多的细节是可控的。
这就需要把整个框架的功能拆分的更细,提供的配置项足够多。
然而,这样的缺点就是开发起来会比较麻烦,需要控制很多细节。
因此,我们可以看到Flutter的组件拆分的很细,甚至有类似Padding这样专门负责缩进的组件,而且每个组件都有很多的配置参数。
Flutter配合Material组件库本身本就非常优秀的UI框架
虽然Flutter的灵活性带来了开发上的复杂性,但Flutter配合Material组件库本身就是一个非常优秀的UI框架。
Material组件库提供了丰富的预设组件,这些组件遵循Material Design指南,可以帮助开发者快速搭建出既美观又符合设计规范的UI界面。
使用Material组件库,开发者可以不必从头开始设计每一个UI元素,而是可以直接使用现成的组件,如按钮、对话框、卡片等,这些组件都有良好的交互和动画效果。
此外,Material组件库还提供了主题支持,开发者可以通过简单的配置,快速应用统一的风格到整个应用中。
因此,虽然Flutter的灵活性可能让初学者感到有些复杂,但配合Material组件库,Flutter实际上提供了一个非常高效和优秀的UI开发体验。
大型项目的正确打开方式
即便是Material组件库,它的设计是需要考虑应对各种不同类型app开发的,但是针对一个具体的项目,我们大多数时候不需要这样高的灵活性。
所以,这种情况下直接用Flutter提供的组件效率会比较低。
解放方法就是针对特定的项目做组件封装。
以我目前维护的项目为例,我们项目中所有的对话框都是相同的偏绿色调,圆角半径20,按钮大小固定,标题、详情的字体、字号也固定。
简单来说,就是所有的UI细节都是固定的,只是不同的dialog需要填充的文字不同。
这时候,我们就会定义一个自己的Dialog组件,只需要使用者传入标题和内容,以及设置按钮的回调即可。
UI的其他地方也是如此,比如页面框架、在多个页面都能用到的用户卡片、商品卡片等等。
当你的整个App大部分都是基于这些自定义组件进行搭积木式的开发,那开发效率是不是比找一些通用的UI框架更高呢?
总结
总而言之,Flutter因为它的工程级框架定位需要提供高度的灵活性,而这往往会导致开发细节的复杂性。
但是,通过针对具体项目的组件封装,我们可以大大提高开发效率,同时保持UI的一致性和项目的特定需求。
所以,与其寻找一个通用的UI框架,不如根据项目的具体需求进行自定义组件的开发。
如果看到这里的同学有学习Flutter的兴趣,欢迎联系老刘,我们互相学习。
点击免费领老刘整理的《Flutter开发手册》,覆盖90%应用开发场景。
可以作为Flutter学习的知识地图。
覆盖90%开发场景的《Flutter开发手册》
来源:juejin.cn/post/7387001928209170447
劝互联网的牛马请善待自己
掘友们,大家好,我是一名95后全栈程序媛,一直以来在努力追求WLB,28岁前完成了畅游中国,既努力生活也认真工作。很多人可能还不知道WLB这个词,WLB就是work life balance
,一开始我看到这个词都是从猎头那里传过来的,岗位招聘一般都是:xxx神仙外企WLB,一周只上3天班,每周两天可以居家办公,每年年假几十天,要求英语口语流利,有x年工作经验,base范围在xx个w....
众所周知,外企的年包肯定不如那些一线爆肝厂,当然工作时间跟收入都是成正比的,965的外企跟11116的互联网的年包肯定是不一样的,付出的工作时间都不一样。假如拼夕夕给你年薪百万,神仙外企给你年薪50+w,你会怎么选?
最近出来几条消息刺痛了牛马的心情!这个世界变幻莫测~
四十多岁的程序员在公司工作11年被裁员
徐峥出新电影了《逆行人生》讲述的就是一个四十多岁的程序员被裁员后找不到工作只能去送外卖的心酸故事,当现实照进电影,卑微的打工人在时代的潮流下只是一个渺小颗粒的缩影。
得物“35岁被暴力裁员”、“80余万元期权直接打水漂”。
一年前,面临裁员的得物员工徐凯多次与公司沟通取得期权再离职未果后,他到上海市仲裁委员会处申请恢复与得物的劳动关系,后被予以支持。7月,因不服上海市仲裁委员会裁定的结果,得物继续上诉,再度将前员工诉于法庭之上。
去哪儿宣布每周可居家办公两天
这则消息意味着互联网公司开启新的里程碑,向神仙外企的福利看齐了,对于老弱病残的打工人简直不要太友好了。
这种待遇,以前在互联网几乎不存在的。一周连休四天的日子,体验过就不再想去卷996的牛马岗了。
不管我们有多努力,我们都只是老板眼里赚钱的工具人
在互联网,35岁已经是一道坎,人在互联网漂,哪有不挨刀,不管有多努力,到了大龄的年纪,工资比年轻人高产出比年轻人低的时候,面临着公司随时都可能会说:分手吧,没有分手费,你自己知难而退吧,大家好聚好散!像极了一个渣男遇到了更年轻漂亮的白富美抛弃糟糠之妻,完了还pua你说都是你的错我才选择了别人。同样,公司会pua说,都是你的没能力,我才选择了别的员工,渣男有道德的谴责?公司有吗?公司跟你只有劳动关系,只要合法,随时跟你说你被毕业了,给个n+1的分手费都要被牛马说这渣渣企真良心!
对于老板来说,赚钱的时候大家都是兄弟,不赚钱了不认兄弟,说好聚好散!你把公司当家,公司把你当牛马。这点,我们真的要向00后学习,提前认清职场,打工就是为了赚钱,为了更好的生活,并不是为了努力加班毁了我们的生活,那我们辛辛苦苦打工有什么意义呢?
是否真的对自己的选择满意
知乎上有一个热度很高的话题:阿里p7和副处级哪个更厉害?
总说纷云,有人选择p7:
有人选择为人民服务;
也有人两者都想要:
在稳定和高薪面前,大家都想要稳定高薪的工作,最后变成稳定焦虑的牛马,这就好像围城,体制内的羡慕体制外的高薪,高薪的牛马羡慕体制内的稳定。即使义无反顾选择了卷互联网,几年挣够了人家一辈子的钱,但是买了二居想换三居,买了三居想换别墅,收入的增长带来消费的提升,物欲的无限放大,依然很多年入百万的人并不觉得真正的快乐而满足现状!即使选择了稳定的体制内,工作体面生活稳定,但是在权力面前,一直追名逐利,在很多诱惑下,最后的选择身不由己!
所以,欲望面前,你有好好认真的生活吗?认真对待自己的身体健康吗?是为了碎银几两熬夜加班把身体搞垮还是为了三餐有汤就行选择WLB呢?希望每一个焦虑的互联网牛马都好好善待自己,平衡好自己身体的健康和对金钱物欲的追逐。
我最羡慕内心富裕,内核稳定的人,这种人一般要比同龄人状态更年轻。不容易被外界所干扰,明确知道自己该要什么,不该要什么,选择适合自己的生活,幸福满意度极高。
来源:juejin.cn/post/7390457313163067431
买房后,害怕失业,更不敢裸辞,心情不好就提前还房贷,缓解焦虑
自从买房后,心态有很大变化。虽然住自己的房子,心情和体验都很好,但是一把掏空钱包,很焦虑。买房后现金流一直吃紧,再加上每年16万的房贷,我很焦虑会失业。之前我喜欢裸辞,现在不敢想裸辞这个话题。尤其是在行业下行期,找工作很艰难,背着房贷裸辞,简直是头孢就酒,嫌命太久。
焦虑的根源是背负房贷,金额巨大,而且担心40岁以后失业,还不上房贷。
一次偶然的沟通
"你的带款利率调整了吗",同事问我。
同事比我早两年在北京买房,他在顺义买的,我在昌平买的,我俩一直有沟通房贷的问题。但我没听说利率有调整,银行好像也没通知我,于是我问道:”我不知道啊,你调整了?调到多少了?“。
”调的挺多的,已经降到了 4.3%“。同事兴高采烈的回复我。
”这么牛逼,之前我记得一直是4.85%,我去看看我的利率“,我听说房贷利率下降那么多,很是兴奋。
然而我的房贷利率没有调整,我尝试给银行打电话,沟通的过程很坎坷。工商银行客服说了很多,大概意思是:利率会自动调整,无需申请,但是要等到利率调整日才会调整”。我开始很不理解,很生气,利率都调整了,别人也都调整了,凭什么不给我调整呢?
我想到同事有尝试提前还贷,生气的时候,我就萌发了提前还贷的想法。
开始尝试提前还贷,真香
我在22年初带款买房,其中商业带款 174 万,带款25年,等额本息,每个月要还 1 万的房贷。公积金带款每个月大概需要还 2500。每个月一万二的房贷还是很有压力的,尤其是刚买房的这一两年,兜里比脸都干净,没存款,不敢失业,更不敢裸辞。
即便兜里存款不多,也要提前还贷,因为实在太香了。
我在工行App上,申请 提前还贷,选择缩短 18个月的房贷,只需要 6万2,而我每个月房贷才1万,相当于是用 6 万 顶 18 万的房贷。还有比这更划算的事情吗?
预约提前还款后,银行会安排一个时间,在这个时间前,把钱存进去。到时候银行就会扣除,如果扣除金额不足,那么提前还款计划则自动终止,需要重新预约!
工行的预约还款时间大概是1个月以后,我是10-15号申请提前还款,银行给的预约日期是 11-14号,大概是1个月。
提前还款,比理财强多了
这次还贷以后,我又申请了提前还款, 提前还 24 期,只需要 9 万,也就是 9 万顶 24 万;提前还 60 期,只需要 24 万,相当于 24 万顶 60 万。
还有比提前还贷收益更高,风险更低的理财方式吗?没有! 除了存款外,任何理财都是有风险的。债券和基金收益和风险挂钩,想找到收益5%的债券基金,要承担亏本风险。你惦记人家的利息,人家惦记你的本金!
股票的风险更不必说,我买白酒股票已经被套的死死,只能躺平装死。(劝大家不要入 A 股)
提前还贷划算吗?
我目前的带款利息是 4.85%,而存到银行的利息不会超过 3% ,很多货币基金只有 2%了。两者利息差高达 3%,肯定是提前还带款更加合适。
要明白,一年两年短期的利息差还好,但是房贷可是高达 25 年。25年 170 万带款 3% 的利息差,这个金额太大了。提前还了,省下来的钱还是很多的。例如刚才截图里展示的 提前还 24 万顶了 60 万的房贷。
网上很多砖家说,“要考虑通货膨胀因素,4.85% 的带款利率和实际通货膨胀比起来不高,提前还款不划算。”
砖家说话都是昧良心的。提前还带款是否划算,只需要和存款利率比就行了,不需要和通货膨胀比。因为把钱存在银行也会因为通货膨胀贬值。只有把钱 全都消费,全部花光才不会受通货膨胀的困扰,建议砖家,多消费,把家底败光,这样最划算!
砖家们一定是害怕太多人提前还贷,影响了银行的放贷生意。今年上半年,提前还贷已经成潮流,有些银行坐不住,甚至关闭了提前还贷的入口…… 所以要抓紧,没准哪天就提高了还贷门槛,或者直接禁止。
程序员群体收入高,手里闲钱多,可以考虑提前还带款,比存银行划算多了,别再给银行打工了!
来源:juejin.cn/post/7301530293378727971
刚入职因为粗心大意,把事情办砸了,十分后悔
刚入职,就踩大坑,相信有很多朋友有我类似的经历。
5年前,我入职一家在线教育公司,新的公司福利非常好,各种零食随便吃,据说还能正点下班,一切都超出我的期望,“可算让我找着神仙公司了”,我的心里一阵窃喜。
在熟悉环境之后,我趁着上厕所的时候,顺便去旁边的零食摊挑了点零食。接下来的一天里,我专注地配置开发环境、阅读新人文档,当然我也不忘兼顾手边的零食。
初出茅庐,功败垂成
"好景不长",第三天上午,刚到公司,屁股还没坐热。新组长立刻给我安排了任务。他决定让我将配置端的课程搜索,从使用现有的Lucene搜索切换到ElasticSearch搜索。这个任务并不算复杂,然而我却办砸了。
先说为什么不复杂?
- ElasticSearch的搜索功能 基于Lucene工具库实现的,两者在搜索请求构造方式上几乎一致,在客户端使用上差异很小。
- 切换方案无需顾虑太多稳定性问题。由于是配置端课程搜索,并非是用户端搜索,所以平稳切换的压力较小、性能压力也比较小。
总的来说,领导认为这个事情并不紧急,重要性也不算高,而且业务逻辑相对简单,难度能够把握,因此安排我去探索一下。可是,我却犯了两个错误,把入职的第一件事办砸了。现在回过头来看,十分遗憾!
难以解决的bug让我陷入困境
将搜索方式从Lucene切换为ElasticSearch后,如何评估切换后搜索结果的准确度呢?
除了通过不断地回归测试,还有一个更好的方案。
我的方案是,在调用搜索时同时并发调用Lucene搜索和ElasticSearch搜索。在汇总搜索结果时,比对两者的搜索结果是否完全一致。如果在切换搜索引擎的过程中,两个方案的搜索结果不一致,就打印异常搜索条件和搜索结果,并进行人工排查原因。
在实际切换过程中,我经常遇到搜索数据不一致的情况,这让我感到十分苦恼。我花了一周的时间编写代码,然后又用了两周多的时间来排查问题,这超出了预估的时间。在这个过程中,我感到非常焦虑和沮丧。作为一个新来的员工,我希望能够表现出色,给领导留下好印象。然而事与愿违,难以解决的bug让我陷入困境。
经过无数次的怀疑和尝试,我终于找到了问题的根源。原来,我忘记了添加排序方式。
因为存在很多课程数据,所以配置端搜索需要分页搜索。在之前的Lucene搜索方式中,我们使用课程Id来进行排序。然而在切换到新的ElasticSearch方案中时,我忘记了添加排序方式。这个错误的后果是,虽然整体上结果是一致的,但由于新方案没有排序方式,每一页的搜索结果是随机的,无法预测,所以与原方案的结果不一致。
新方案加上课程Id排序方式以后,搜索结果和原方案一致。
为此,我总结了分页查询的设计要点!希望大家不要重复踩坑!# 四选一,如何选择适合你的分页方案?
千万不要粗心大意
实际上,在解决以上分页搜索没有添加排序方式的问题之后,还存在着许多小问题。而这些小问题都反映了我的另一个不足:粗心大意。
正是这些小问题,导致线上环境总会出现个别搜索结果不一致的情况,导致这项工作被拖延很久。
课程模型是在线教育公司非常核心的数据模型,业务逻辑非常复杂,当然字段也非常多。在我入职时,该模型已经有120 个字段,并且有近 50 个字段可以进行检索。
在切换搜索方式时,我需要重新定义各种DO、DTO、Request等类型,还需新增多个类,并重新定义这些字段。在这个过程中,我必须确保不遗漏任何字段,也不能多加字段。当字段数量在20个以内时,这项工作出错的可能性非常低。然而,班课模型却有多达120个字段,因此出错的风险极大。当时我需要大量搬运这些字段,然而我只把这项工作看作是枯燥乏味的任务,未能深刻意识到出错的可能性极大,所以工作起来散漫随意,没有特别仔细校验重构前后代码的准确性。
墨菲定律:一件事可能出错时就一定会出错
墨菲定律是一种普遍被接受的观念,指出如果某件事情可能出错,那么它将以最不利的方式出错。这个定律起源于美国航天局的项目工程师爱德华·墨菲,在1950年代发现了这一规律。
墨菲定律还强调了人类的倾向,即将事情弄糟或让事情朝着最坏的方向发展。它提醒人们在计划和决策时要考虑可能出错的因素,并准备应对不利的情况。
墨菲定律实在是太准了,当你感觉某个事情可能会出错的时候,那它真的就会出错。而且我犯错不止一次,因为有120个字段,很多字段的命名非常相似,最终我遗漏了2个字段,拼写错误了一个字段,总共有三个字段出了问题。
不巧的是,这三个字段也参与检索。当用户在课程搜索页面选择这三个字段来进行检索时,因为字段的拼写错误和遗漏,这三个字段没有被包含在检索条件中,导致搜索结果出错……
导致这个问题的原因有很多,其中包括字段数量太多,我的工作不够细致,做事粗心大意,而且没有进行充分的测试……
为什么没有测试
小公司的测试人员相对较少,尤其是在面对课程管理后台的技术重构需求时,更加无法获取所需的测试资源!
组长对我说:“ 要人没有,要测试更没有!”
事情办砸了,十分遗憾
首先,从各个方面来看,切换搜索引擎这件事的复杂度和难度是可控的,而且目标也非常明确。作为入职后第一项任务,我应该准确快速地完成它,以留下一个良好印象。当然,领导也期望我能够做到这一点,然而事实与期望相去甚远。
虽然在线上环境没有出现问题,但在上线后,问题排查的时间却远远超出了预期,让领导对结果不太满意。
总的来说,从这件事中,我获得的最重要教训就是:对于可能出错的事情,要保持警惕。时刻用墨菲定律提醒自己,要仔细关注那些可能发生小概率错误的细节问题。
对于一些具有挑战性的工作,我们通常都非常重视,且在工作中也非常认真谨慎,往往不会出错。
然而,像大量搬运代码、搬运大量字段等这类乏味又枯燥的工作确实容易使人麻痹大意,因此我们必须提高警惕。要么我们远离这些乏味的工作,要么就要认真仔细地对待它们。
否则,如果对这些乏味工作粗心大意,墨菲定律一定会找上你,让你在线上翻车!
来源:juejin.cn/post/7295576148364787751
记一种不错的缓存设计思路
之前与同事讨论接口性能问题时听他介绍了一种缓存设计思路,觉得不错,做个记录供以后参考。
场景
假设有个以下格式的接口:
GET /api?keys={key1,key2,key3,...}&types={1,2,3,...}
其中 keys 是业务主键列表,types 是想要取到的信息的类型。
请求该接口需要返回业务主键列表对应的业务对象列表,对象里需要包含指定类型的信息。
业务主键可能的取值较多,千万量级,type 取值范围为 1-10,可以任意组合,每种 type 对应到数据库是 1-N 张表,示意:
现在设想这个接口遇到了性能瓶颈,打算添加 Redis 缓存来改善响应速度,应该如何设计?
设计思路
方案一:
最简单粗暴的方法是直接使用请求的所有参数作为缓存 key,请求的返回内容为 value。
方案二:
如果稍做一下思考,可能就会想到文首我提到的觉得不错的思路了:
- 使用
业务主键:表名
作为缓存 key,表名里对应的该业务主键的记录作为 value; - 查询时,先根据查询参数 keys,以及 types 对应的表,得到所有
key1:tb_1_1
、key1:tb_1_2
这样的组合,使用 Redis 的 mget 命令,批量取到所有缓存中存在的信息,剩下没有命中的,批量到数据库里查询到结果,并放入缓存; - 在某个表的数据有更新时,只需刷新
涉及业务主键:该表名
的缓存,或令其失效即可。
小结
在以上两种方案之间做评估和选择,考虑几个方面:
- 缓存命中率;
- 缓存数量、占用空间大小;
- 刷新缓存是否方便;
稍作思考和计算,就会发现此场景下方案二的优势。
另外,就是需要根据实际业务场景,如业务对象复杂度、读写次数比等,来评估合适的缓存数据的粒度和层次,是对应到某一级组合后的业务对象(缓存值对应存储 + 部分逻辑),还是最基本的数据库表/字段(存储的归存储,逻辑的归逻辑)。
来源:juejin.cn/post/7271597656118394899
有哪些事情,是当了程序员之后才知道的?
1、平庸的程序员占比很大。 还没参加工作时,觉得程序员是个改变世界的高科技职业,后来才发现,其实这个群体里有很多CRUD Boy和SQL Boy,Ctrl C+Ctrl V是我们使用最多的电脑操作,没有之一。
而且,大多数同事上班摸鱼偷懒,遇到问题躲着走,下班也从来不主动学习充电。每当我问他们,他们都说这样混着也挺好,不想太累。
2、数量堆死质量。 如果你觉得没有写代码的天赋,那么请你先写10万行代码再说。
如果你在刷leetcode的时候非常痛苦,甚至有时候看答案都看不懂。那你就先把代码背下来,然后一遍一遍默写。每当你默写五遍以上,就开始慢慢理解了,刷十遍以上,再遇到类似的题,就有条件发射,能举一反三了。
这种方法运用到看底层源码,看一些晦涩难懂的技术类书籍上,也同样适用。
后来,我在网上看了硅谷王川的一段话:所有的我们以为的质量问题,大多本质是数量问题。数量是最重要的质量。
而欧成效则说得更加直接:数量堆死质量!
3、尽量选择研发出身的老板的公司。 他们会知道程序员不是故意写bug的,也没有任何系统能做到100%的可用性。
而销售出身的老板,却永远把自家公司的程序员看做产出并不令人满意的高成本项。而且还时不时地要求程序员跟销售一样喊几句令人其鸡皮疙瘩的鸡血口号。
4、大厂和小厂的程序员,技术上差距并不大。 他们的差距也许是在学历上,也许是在人脉上,也许是在沟通和向上管理上。
5、对测试同学客气一点, 他们是你写的代码的最后一道防线。再有就是,如果线上出了故障或者严重bug,很多产研以外的人都关注是哪个程序员造成了事故,而不是哪个测试同学没测出来。
6、产品经理是SB,甲方是SB的N次方。 最令人蛋疼的是,任何一家公司都是这样,所以你根本避无可避,只能长期共存。
7、程序员涨薪,最好的方式是跳槽, 而不是兢兢业业地加班工作。如果就靠公司每年涨的那些钱,估计得用7,8年才能实现薪资翻番。但如果靠跳槽,估计3年就能实现薪资翻番。
8、能不去外包公司就尽量不去,那种寄人篱下的无归属感才最让人心累。你会发现,公司的正式员工吃饭和娱乐都是不愿意带你玩儿的,平时跟你说话的表情也是鼻孔朝天。
9、面试造火箭,工作拧螺丝是正常的。 你要做的就是提升造火箭吹牛逼的能力,毕竟这才是你定级谈薪的资本。不要抱怨,要适应。
10、35岁的危机真的存在。 那些认为技术牛逼就可以平稳度过中年危机的人,很多都SB了。人老不以筋骨和技术为能,顺势而为,尽早找后路才是王道。
11、尽量去工程师占比超过30%的公司,因为它的期权可能在未来十年内变得很有价值。因为工程师占比越高,边际成本就越低。
12、离开公司这个平台,也许你什么都不是。 很多大厂的高P前辈,甚至是总监、 VP,也可能在某一个时间点,突然被淘汰!我身边就有一个BAT的总监,真的就突然被优化了,真的就找不到哪怕一半的薪资了。突然之间!
拔剑四顾心茫然.... 所以,永远要分清楚哪些是平台资源,哪些是你的能力。时刻对自身能力保持清醒且准确的认知,千万不要陷入盲目自负的境地。实在太过乐观的大厂朋友,可以周期性出来面试,哪怕不跳槽,认知自己的真实价值。
13、技术面试官问期望薪资,记得往低了说。 因为他们往往并不负责最终的定薪,但如果你的期望薪资高于他,会让他产生强烈的不平衡,从而把你Pass掉。
14、身体才是一切的本钱。 前些天左耳朵耗子前辈的忽然离世,再次验证了这一点,如果身体健康是0,那么其他的所有一切都是0。
15、脱发和格子衫的说法,并不普遍。 我认识的程序员里,80%是不穿格子衫的,而且35岁+的程序员,80%也是不脱发的。
但是有一种东西是很普遍的,那就是装着电脑的双肩包。
16、PPT架构师、周报合并师、无损复读师真的存在,而且越是在大厂,这种人就会越多。
PPT架构师在PPT中讲的架构各种高端大气上档次,其实就是大家很常用的部署流程;周报合并师每周的任务就是每周将团队中每个人的周报进行汇总,再报告给上级;无损复读师要求可能会高一些,对老板提出的问题或者质疑,要原原本本的向下传达给项目组对应的同学,不能有一丝偏差。
或许他们最开始不是这样的,但是慢慢地,他们活成了最舒服的,也是曾经最讨厌的样子。
17、大多数程序员是不会修电脑的。 很多行业以外的人,他们会觉得很多事情程序员都可以做,从盗QQ,Photoshop,硬盘文件恢复,到装系统,处理系统故障和软件问题,安装各种盗版软件,各种手机的越狱Root装盗版应用。
并且,另外这些事情往往不涉及实物,给人的感觉是只是在键盘上打打字,又不需要买新硬件之类的,所以往往会被认为是举手之劳,理应帮忙。
18、杀死程序员不用枪,改三次需求就可以了。 很多程序员并不反感别人说他无趣,也不反感别人说他们的穿着土鳖,也不反感别人说他们长相平庸。
也就是说,除了反复改需求,别的他们都能忍受。
先说这么多吧,总结得也算是比较全了,后续有新的,我再补充。
来源:juejin.cn/post/7292960995437166601
uniapp下各端调用三方地图导航
技术栈
- 开发框架: uniapp
- vue 版本: 2.x
- 开发框架: uniapp
- vue 版本: 2.x
需求
使用uniapp
在app端(Android,IOS)
中显示宿主机已有的三方导航应用,由用户自主选择使用哪家地图软件进行导航,选择后,自动将目标地址设为终点在导航页面。 使用uniapp
在微信小程序
中调用微信内置地图导航。
使用uniapp
在app端(Android,IOS)
中显示宿主机已有的三方导航应用,由用户自主选择使用哪家地图软件进行导航,选择后,自动将目标地址设为终点在导航页面。 使用uniapp
在微信小程序
中调用微信内置地图导航。
实现
微信小程序调用微信内置地图导航
使用uni.openLocation()
方法可直接调用,微信比较简单
传值字段
名称 说明 是否必传 latitude 纬度,范围为-90~90,负数表示南纬,使用 gcj02 国测局坐标系 是 longitude 经度,范围为-180~180,负数表示西经,使用 gcj02 国测局坐标系 是 name 位置名称 非必传,但不传不显示目标地址名称 address 地址的详细说明 非必传,但不传不显示目标地址名称详情
具体代码
经纬度需转为float数据类型
uni.openLocation({
latitude: parseFloat('地址纬度'),
longitude: parseFloat('地址经度'),
name: ‘地址名称,
address: '地址详情',
success: function (res) {
console.log('打开系统位置地图成功')
},
fail: function (error) {
console.log(error)
}
})
使用uni.openLocation()
方法可直接调用,微信比较简单
传值字段
名称 | 说明 | 是否必传 |
---|---|---|
latitude | 纬度,范围为-90~90,负数表示南纬,使用 gcj02 国测局坐标系 | 是 |
longitude | 经度,范围为-180~180,负数表示西经,使用 gcj02 国测局坐标系 | 是 |
name | 位置名称 | 非必传,但不传不显示目标地址名称 |
address | 地址的详细说明 | 非必传,但不传不显示目标地址名称详情 |
具体代码
经纬度需转为float数据类型
uni.openLocation({
latitude: parseFloat('地址纬度'),
longitude: parseFloat('地址经度'),
name: ‘地址名称,
address: '地址详情',
success: function (res) {
console.log('打开系统位置地图成功')
},
fail: function (error) {
console.log(error)
}
})
app端调用宿主机三方地图导航
步骤:
- 获取宿主机已安装的三方地图应用并显示,没有安装提示宿主机。
- 根据宿主机选择的三方地图,打开对应的三方地图进行导航。
使用plus
调用原生API知识点:
- 获取宿主机系统环境
uniapp文档:uniapp.dcloud.net.cn/api/system/…
使用uniapp
的uni.getSystemInfoSync().platform
方法获取宿主机系统环境,结果为android
、ios
。
- 获取宿主机是否安装某个应用
步骤:
- 获取宿主机已安装的三方地图应用并显示,没有安装提示宿主机。
- 根据宿主机选择的三方地图,打开对应的三方地图进行导航。
使用plus
调用原生API知识点:
- 获取宿主机系统环境
uniapp文档:uniapp.dcloud.net.cn/api/system/…
使用uniapp
的uni.getSystemInfoSync().platform
方法获取宿主机系统环境,结果为android
、ios
。
- 获取宿主机是否安装某个应用
使用H5产业联盟中的 plus.runtime.isApplicationExist
来判断宿主机是否安装指定应用,已安装返回True
,
Android平台需要通过设置appInf的pname属性(包名)进行查询。 iOS平台需要通过设置appInf的action属性(Scheme)进行查询,在iOS9以后需要添加白名单才可查询,在manifest.json文件plus->distribute->apple->urlschemewhitelist节点下添加(如urlschemewhitelist:["weixin"])。
调用示例
// Android
plus.runtime.isApplicationExist({pname: 'com.autonavi.minimap'})
// iOS
plus.runtime.isApplicationExist({action: 'iosamap://'})
- 调用系统级选择菜单显示已安装地图列表
调用示例
plus.nativeUI.actionSheet({ //选择菜单
title: "选择地图应用",
cancel: "取消",
buttons: [
{title: '1'},
{title: '2'}
]
}, function (e) {
console.log("您点击的是第几个:"+e.index)
})
- 打开三方某个应用
调用示例
// Android
plus.runtime.openURL('三方应用地址', function(res){
// todo...
}, 'com.xxx.xxxapp');
// ios
plus.runtime.openURL('三方应用地址', function(res){
// todo...
});
具体代码:
<template>
<view @click.stop="handleNavigation">导航view>
template>
<script>
...
data() {
return {
// 目标纬度
latitude: '',
// 目标经度
longitude: '',
// 目标地址名称
name: '',
// 目标地址详细信息
address: '',
// 我自己的位置经纬度(百度地图需要传入自己的经纬度进行导航)
selfLocation: {
latitude: '',
longitude: ''
}
}
},
methods: {
handleNavigation() {
const _this = this
if (!this.latitude || !this.longitude || !this.name) return
// 微信
// #ifdef MP-WEIXIN
let _obj = {
latitude: parseFloat(this.latitude),
longitude: parseFloat(this.longitude),
name: this.name,
}
if (this.address) {
_obj['address'] = this.address
}
uni.openLocation({
..._obj,
success: function (res) {
console.log('打开系统位置地图成功')
},
fail: function (error) {
console.log(error)
}
})
// #endif
// #ifdef APP-PLUS
// 判断系统安装的地图应用有哪些, 并生成菜单按钮
let _mapName = [
{title: '高德地图', name: 'amap', androidName: 'com.autonavi.minimap', iosName: 'iosamap://'},
{title: '百度地图', name: 'baidumap', androidName: 'com.baidu.BaiduMap', iosName: 'baidumap://'},
{title: '腾讯地图', name: 'qqmap', androidName: 'com.tencent.map', iosName: 'qqmap://'},
]
// 根据真机有的地图软件 生成的 操作菜单
let buttons = []
let platform = uni.getSystemInfoSync().platform
platform === 'android' && _mapName.forEach(item => {
if (plus.runtime.isApplicationExist({pname: item.androidName})) {
buttons.push(item)
}
})
platform === 'ios' && _mapName.forEach(item => {
console.log(item.iosName)
if (plus.runtime.isApplicationExist({action: item.iosName})) {
buttons.push(item)
}
})
if (buttons.length) {
plus.nativeUI.actionSheet({ //选择菜单
title: "选择地图应用",
cancel: "取消",
buttons: buttons
}, function (e) {
let _map = buttons[e.index - 1]
_this.openURL(_map, platform)
})
} else {
uni.showToast({
title: '请安装地图软件',
icon: 'none'
})
return
}
// #endif
},
// 打开第三方程序实际应用
openURL(map, platform) {
let _defaultUrl = {
android: {
"amap": `amapuri://route/plan/?sid=&did=&dlat=${this.latitude}&dlon=${this.longitude}&dname=${this.name}&dev=0&t=0`,
'qqmap': `qqmap://map/routeplan?type=drive&to=${this.name}&tocoord=${this.latitude},${this.longitude}&referer=fuxishan_uni_client`,
'baidumap': `baidumap://map/direction?origin=${this.selfLocation.latitude},${this.selfLocation.longitude}&destination=name:${this.name}|latlng:${this.latitude},${this.longitude}&coord_type=wgs84&mode=driving&src=andr.baidu.openAPIdemo"`
},
ios: {
"amap": `iosamap://path?sourceApplication=fuxishan_uni_client&dlat=${this.latitude}&dlon=${this.longitude}&dname=${this.name}&dev=0&t=0`,
'qqmap': `qqmap://map/routeplan?type=drive&to=${this.name}&tocoord=${this.latitude},${this.longitude}&referer=fuxishan_uni_client`,
'baidumap': `baidumap://map/direction?origin=${this.selfLocation.latitude},${this.selfLocation.longitude}&destination=name:${this.name}|latlng:${this.latitude},${this.longitude}&mode=driving&src=ios.baidu.openAPIdemo`
}
}
let newurl = encodeURI(_defaultUrl[platform][map.name]);
console.log(newurl)
plus.runtime.openURL( newurl, function(res){
console.log(res)
uni.showModal({
content: res.message
})
}, map.androidName ? map.androidName : '');
}
}
script>
<template>
<view @click.stop="handleNavigation">导航view>
template>
<script>
...
data() {
return {
// 目标纬度
latitude: '',
// 目标经度
longitude: '',
// 目标地址名称
name: '',
// 目标地址详细信息
address: '',
// 我自己的位置经纬度(百度地图需要传入自己的经纬度进行导航)
selfLocation: {
latitude: '',
longitude: ''
}
}
},
methods: {
handleNavigation() {
const _this = this
if (!this.latitude || !this.longitude || !this.name) return
// 微信
// #ifdef MP-WEIXIN
let _obj = {
latitude: parseFloat(this.latitude),
longitude: parseFloat(this.longitude),
name: this.name,
}
if (this.address) {
_obj['address'] = this.address
}
uni.openLocation({
..._obj,
success: function (res) {
console.log('打开系统位置地图成功')
},
fail: function (error) {
console.log(error)
}
})
// #endif
// #ifdef APP-PLUS
// 判断系统安装的地图应用有哪些, 并生成菜单按钮
let _mapName = [
{title: '高德地图', name: 'amap', androidName: 'com.autonavi.minimap', iosName: 'iosamap://'},
{title: '百度地图', name: 'baidumap', androidName: 'com.baidu.BaiduMap', iosName: 'baidumap://'},
{title: '腾讯地图', name: 'qqmap', androidName: 'com.tencent.map', iosName: 'qqmap://'},
]
// 根据真机有的地图软件 生成的 操作菜单
let buttons = []
let platform = uni.getSystemInfoSync().platform
platform === 'android' && _mapName.forEach(item => {
if (plus.runtime.isApplicationExist({pname: item.androidName})) {
buttons.push(item)
}
})
platform === 'ios' && _mapName.forEach(item => {
console.log(item.iosName)
if (plus.runtime.isApplicationExist({action: item.iosName})) {
buttons.push(item)
}
})
if (buttons.length) {
plus.nativeUI.actionSheet({ //选择菜单
title: "选择地图应用",
cancel: "取消",
buttons: buttons
}, function (e) {
let _map = buttons[e.index - 1]
_this.openURL(_map, platform)
})
} else {
uni.showToast({
title: '请安装地图软件',
icon: 'none'
})
return
}
// #endif
},
// 打开第三方程序实际应用
openURL(map, platform) {
let _defaultUrl = {
android: {
"amap": `amapuri://route/plan/?sid=&did=&dlat=${this.latitude}&dlon=${this.longitude}&dname=${this.name}&dev=0&t=0`,
'qqmap': `qqmap://map/routeplan?type=drive&to=${this.name}&tocoord=${this.latitude},${this.longitude}&referer=fuxishan_uni_client`,
'baidumap': `baidumap://map/direction?origin=${this.selfLocation.latitude},${this.selfLocation.longitude}&destination=name:${this.name}|latlng:${this.latitude},${this.longitude}&coord_type=wgs84&mode=driving&src=andr.baidu.openAPIdemo"`
},
ios: {
"amap": `iosamap://path?sourceApplication=fuxishan_uni_client&dlat=${this.latitude}&dlon=${this.longitude}&dname=${this.name}&dev=0&t=0`,
'qqmap': `qqmap://map/routeplan?type=drive&to=${this.name}&tocoord=${this.latitude},${this.longitude}&referer=fuxishan_uni_client`,
'baidumap': `baidumap://map/direction?origin=${this.selfLocation.latitude},${this.selfLocation.longitude}&destination=name:${this.name}|latlng:${this.latitude},${this.longitude}&mode=driving&src=ios.baidu.openAPIdemo`
}
}
let newurl = encodeURI(_defaultUrl[platform][map.name]);
console.log(newurl)
plus.runtime.openURL( newurl, function(res){
console.log(res)
uni.showModal({
content: res.message
})
}, map.androidName ? map.androidName : '');
}
}
script>
最终效果图
- 微信
- 微信
- app端
最后
参考链接: H5产业联盟:http://www.html5plus.org/doc/h5p.htm… uniapp: uniapp.dcloud.net.cn/api/ 百度、高德、腾讯地图,三方APP调用其的文档。
本文初发于:blog.zhanghaoran.ren/article/htm…
来源:juejin.cn/post/7262941534528700453
这可能是开源界最好用的行为验证码工具
- 💂 个人网站: IT知识小屋
- 🤟 版权: 本文由【IT学习日记】原创、需要转载请联系博主
- 💬 如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连)和订阅专栏哦
写在前面
大家好,这里是IT学习日记。今日推荐项目:tianai-captcha行为验证码工具。
1000+优质开源项目推荐进度:6/1000。如需更多类型优质项目推荐,请在文章后留言。
工具简介
tianai-captcha行为验证码工具:分为 Go 和 Java 两个版本。支持多种验证方式,包括随机验证、曲线匹配、滑块验证、增强版滑块验证、旋转验证、滑动还原、角度验证、刮刮乐、文字点选、图标点选及语序点选等。
该系统能够快速集成到个人项目或系统中,显著提高开发效率。
功能展示
- 随机型验证码
- 曲线匹配验证码
- 滑动验证增强版验证码
- 滑块验证码
- 旋转验证码
- 滑动还原验证码
- 角度验验证码
- 刮刮乐验验证码
- 文字点选验证码
- 图标验证码
架构设计
tianai-captcha 验证码整体分为 生成器(ImageCaptchaGenerator)、校验器(ImageCaptchaValidator)、资源管理器(ImageCaptchaResourceManager) 其中生成器、校验器、资源管理器等都是基于接口模式实现可插拔的,可以替换为自定义实现,灵活度高
- 生成器 (ImageCaptchaGenerator)
主要负责生成行为验证码所需的图片。 - 校验器 (ImageCaptchaValidator)
主要负责校验用户滑动的行为轨迹是否合规。 - 资源管理器 (ImageCaptchaResourceManager)
主要负责读取验证码背景图片和模板图片等。
- 资源存储 (ResourceStore)
负责存储背景图和模板图。 - 资源提供者 (ResourceProvider)
负责将资源存储器中对应的资源转换为文件流。一般资源存储器中存储的是图片的 URL 地址或 ID,资源提供者则负责将 URL 或其他 ID 转换为真正的图片文件。
- 资源存储 (ResourceStore)
- 图片转换器 (ImageTransform)
主要负责将图片文件流转换成字符串类型,可以是 Base64 格式、URL 或其他加密格式,默认实现为 Base64 格式。
工具集成
引入依赖
<!-- maven 导入 -->
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha</artifactId>
<version>1.4.1</version>
</dependency>
- 使用 ImageCaptchaGenerator生成器生成验证码
public class Test {
public static void main(String[] args) throws InterruptedException {
ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager();
ImageTransform imageTransform = new Base64ImageTransform();
ImageCaptchaGenerator imageCaptchaGenerator = new MultiImageCaptchaGenerator(imageCaptchaResourceManager,imageTransform).init(true);
/*
生成滑块验证码图片, 可选项
SLIDER (滑块验证码)
ROTATE (旋转验证码)
CONCAT (滑动还原验证码)
WORD_IMAGE_CLICK (文字点选验证码)
更多验证码支持 详见 cloud.tianai.captcha.common.constant.CaptchaTypeConstant
*/
ImageCaptchaInfo imageCaptchaInfo = imageCaptchaGenerator.generateCaptchaImage(CaptchaTypeConstant.SLIDER);
System.out.println(imageCaptchaInfo);
// 负责计算一些数据存到缓存中,用于校验使用
// ImageCaptchaValidator负责校验用户滑动滑块是否正确和生成滑块的一些校验数据; 比如滑块到凹槽的百分比值
ImageCaptchaValidator imageCaptchaValidator = new BasicCaptchaTrackValidator();
// 这个map数据应该存到缓存中,校验的时候需要用到该数据
Map<String, Object> map = imageCaptchaValidator.generateImageCaptchaValidData(imageCaptchaInfo);
}
}
- 使用ImageCaptchaValidator校验器 验证
public class Test2 {
public static void main(String[] args) {
BasicCaptchaTrackValidator sliderCaptchaValidator = new BasicCaptchaTrackValidator();
ImageCaptchaTrack imageCaptchaTrack = null;
Map<String, Object> map = null;
Float percentage = null;
// 用户传来的行为轨迹和进行校验
// - imageCaptchaTrack为前端传来的滑动轨迹数据
// - map 为生成验证码时缓存的map数据
boolean check = sliderCaptchaValidator.valid(imageCaptchaTrack, map).isSuccess();
// // 如果只想校验用户是否滑到指定凹槽即可,也可以使用
// // - 参数1 用户传来的百分比数据
// // - 参数2 生成滑块是真实的百分比数据
check = sliderCaptchaValidator.checkPercentage(0.2f, percentage);
}
}
工具获取
如果这篇文章对您有帮助,请**“彦祖们”**一定帮我点个 “关注” 和 “点赞”,这对我非常重要。我将会继续推荐更多优质项目和新闻。
来源:juejin.cn/post/7391351326153965568
这样做产品,死是早晚的事!
昨天和在北京的朋友聊天,他了解到之前我做过餐饮的SAAS系统,于是问我这一块是否还能分到一杯羹!
说实话,我觉得没机会,特别是对于一家小公司来说,基本上没机会,甚至连入场券都拿不到!
这不禁让我想起几年前认识的一个小公司,给他们兼职开发的两款SAAS产品,一款是连锁酒店系统,一款则是餐饮系统。
他们的酒店系统,现在在我看来依然是很牛逼的,我也去看过一些市面上的解决方案,但是依然没有他们的牛逼。
不过残酷的是,最近半年来,他们好像一套也没有卖出去,如果我没猜错的话,这几年下来,他们应该没有卖出多少套。
其实几年前我和他们协同开发,听了他们的一些想法,我就预见他们很难打出去。
因为我发现他去做了一些看似很完美,但是不是必须的功能,而且还花了大量时间去做,当时我觉得这完全就是鸡肋,现在看来是鸡骨头。
说白了,就是定位不明确,想做一个大而全方案,但是这对于一个小公司初创团队来说,这是很致命的,特别是资金不充足的情况下去干这事!
下面从几个方面去看问题。
1.定位不明确
理想一定是会被现实啪啪打脸的,当想去做一个产品的时候,不要觉得自己做得很全很大就能赢得市场,这简直是痴人说梦。
特别是在行业竞争如此之大的情况下,大公司早都入局了,人家的解决方案比你强大,价格比你便宜,售后比你全,你拿什么去拼?
当时我问他,为啥要做餐饮解决方案,你觉得你从技术,价格,服务这些方面,你有哪里比得上客如云,微盟,美团这些巨头,他说别管那么多,东西做出来自然有办法!
现在里面过去了,基本上没有任何推进。
这肯定是定位出问题了啊,不要觉得你手上有产品就能赚钱,如果是这样,那还需要销售干嘛。
对于小公司来说,大家都是技术出身,没有营销经验,就算做出产品来,也只能摆着看,如果要请销售团队,公司又支撑不起,显然矛盾了!
所以就尽量别去做这类似的产品,应该去做一些能解决别人痛点的小而美的解决方案。
就像微信公众号刚兴起的那几年,因为公众号自带的编辑器很难用,有一个人就做了一个小编辑器出来,赚得盆满钵满。
看似冷门,但是垂直!
2.陷入大而全的误区
接着上面的说。
后面有人看到看到了这个红利,就进军去做,他们希望做出更强大,功能更全的编辑器,结果花了大量时间去做,最后产品出来了,但是市场已经被别人抢了先机,最终不得不死。
这就是迷恋大而全的后果!
其实开源就是一个很好避免大而全的方案。
在开源领域,先做出一个小而美的产品,把影响力传播开,然后根据用户的需求不断迭代,这时候不是人去驱动产品了,而是需求去驱动产品。
这样做出来的产品不仅能避免出现很多无用的功能,还能节约很多的成本!
一定要让用户的需求来驱动产品的发展,而不是靠自己的臆想去决定做什么产品!
老罗当年在做锤子科技的时候,我觉得他就陷入了想去做一个大而全的产品,还陷入自己以为的漩涡,所以耗费了很多资金去研发TNT,所以导致失败。
如果那时候致力于去做好坚果系列,那么结局可能大不一样!
3.没有尝到甜头,你怎敢去做!
在我们贵州本土,有一个技术大佬,他一开始做了一个门户系统的解决方案,后续就有人来找他,说要购买他的系统,他从里面尝到了甜头!
于是就在这个领域持续深耕,最终形成了一套强大的解决方案。现在他的解决方案已经遍布全国。
他们公司基本上就是靠门户系统的解决方案来维持的。
所以,做一个产品,只有自己尝到甜头了,再去深耕,形成一套解决方案,那么成功率就会变得越高。
特别对于小公司来说,这是很重要的!
4.总结
做产品一定要忌讳大而全,也不要陷入只要我做出来了,无论如何都能分一杯羹,这是不现实的。
市场上到处是饿狼潜伏,你不过是一只小羊羔,怎么生存?
用最少的成本开发出一个小而美的解决方案,然后拿出去碰一碰,闻到味道了,再不断进击,这样成功率就高一点,即使失败了代价也不高。
今天的分享就到这里!
来源:juejin.cn/post/7313887095415324672
小小扫码枪bug引发的思考
最近新公司发生了一件bug引发思考的事
产品需求
大致如上图,一个输入框,我们制作了自定义数字键盘,input框可以回显键盘的输入,并且,可以支持扫码枪输入回显
bug描述
在win 系统没有问题,但在安卓系统:
- 每次自定义键盘输入时,还会吊起系统软键盘,且通过系统软键盘输入,input是无法回显的!
- 不支持 扫码枪输入了
最讨厌研究 系统兼容性问题了,但问题出了,就得研究
我们先看一下,自定义数字键盘是怎么实现的?
在了解自定义键盘之前,我先问问大家,键盘输入会触发哪些事件?
对,就是这三个 keydown,keypress, keyup
如何控制Input框只回显数字呢?答案就是在keyDown事件里,通过捕获 event.key来获取用户按下的物理按键的值,非数字的值直接return就能做到了
那么言归正传,自定义键盘怎么实现呢?
其实到这边我们不难想到一个解决方案的思路就是,当按下自定义键盘时,我们模拟一个 keydown事件,并向获得焦点的input 派发这个keydown事件,那么就能模拟键盘输入了
上代码:
const input = document.activeElement
const event = document.createEvent('UIEvents')
event.initUIEvent('keydown', true, true, window, 0)
event.key = key
event.keyCode = -1
input.dispatchEvent(event)
扫码枪又是个啥?
就是这个东东:
去过超市的都看过吧
用扫码枪或者其他设备扫描图形码(条形码或其他码)后将其为文本输入,
input需要识别到扫码枪输入结束,并回显input区,
其实扫码枪输入和用户键盘输入一样都可以触发keydown事件,派发给聚焦的input
那么问题来了?
怎样识别 扫码枪输入结束呢?
答案是onEnter事件
我们再来看看 安卓端出现的bug
1,为啥每次我们在自定义键盘上输入,会同时弹出系统软键盘呢??
问了下安卓侧RD,原来只要input获得焦点,系统键盘就会弹出
但是不聚焦,自定义键盘/扫码枪也没办法回显了呀?
难道真的无解了吗?这时候第n个知识点来了!用readOnly!
readonly,对,就是它,
什么?readonly不是只读吗?有了它,相当于 用户无法输入,因此无法触发系统键盘,这个可以理解,但是,加上它之后,还有焦点吗?
这里有个问题要问大家,你知道readonly和disabled的区别吗?
答案就是在交互上,readonly 仍是可以聚焦的!disabled 就不能了
并且readOnly 是禁止用户输入,所以在允许聚焦的同时,又阻止了软键盘的弹出,这时我不禁感叹: 完美!
2,安卓为啥不支持扫码枪扫码了?
我们通过调试发现,在安卓上,keyDown事件 捕获到的event.key 是 Unidentified, 被我们判定为非数字,直接return了
那解法呢?我们神奇的发现,当我们解了bug1,加上readonly后,bug2也好了!
至于为啥它也好了,具体原因我还不清楚,以下是我的猜测:
前文我们提到,只要input聚焦,软键盘就会弹出,而扫码枪其实也可以看成一个特殊的键盘,可能两个键盘冲突导致 event.key 无法识别,加上readonly禁掉 软键盘后,冲突解除,自然event.key 也可以正常识别了
清楚原因的同学可以留言给我哈!我好想知道!!
反思来了
这件问题的最终解决方案只有一行代码,一个单词: readOnly
简单到令人发指,而且这个问题是一个刚来两天的新同学搞定的
我在想这一连串的故事,太神奇了
为啥这个困扰前辈同学包括我很久的问题,一个萌新一下子就解决了呢?虽然我也是萌新
readOnly可以 解决禁止软键盘弹出,网上的答案是有的,但是我pass了这��方案,
为什么呢?
- input相关基础差,我错误的认为readOnly是只读嘛,肯定会不带焦点啊,虽然禁用了软键盘,但是 扫码枪输入也不能回显了啊
- 当我看到 event.key 是 Unidentified 时,研究重点跑偏了
- 我觉得这可能某种程度上是一种 beginer’s luck, 因为当时新同学的任务是研究如何禁用软键盘,并没有提到其他扫码枪问题,可能这种心无旁骛反而成了事
- 工作中,尤其遇到一些诡异的兼容性问题,真的需要多尝试,不要被自己的想当然绑手绑脚
- 对于兼容性问题,因为要不断尝试,最好找到一种简单方便的调试方法,会大大加快调研进度
最后还是感谢一切的发生,收获了知识,也让我有冲动分享给大家我的一点小思考,感恩感恩!
来源:juejin.cn/post/7388459061758017571
软件工程师,为什么不喜欢关电脑
💡 如果想阅读最新的文章,或者有技术问题需要交流和沟通,可搜索并关注微信公众号“希望睿智”。
概述
你是否注意到,软件工程师们似乎从不关电脑,也不喜欢关电脑?别以为他们是电脑“上瘾”,或是沉迷于电脑,这一现象背后蕴含着多种实际原因。
1、代码保存与恢复。
在编写代码过程中,遇到问题时可能会暂时离开去查阅资料或者休息,而不想打断当前的思路和工作进度。如果电脑不关机,他们可以迅速回到上次中断的地方,继续解决问题,避免了重新加载项目和找回思考线索的过程。
2、远程访问与协作。
很多软件工程师采用分布式团队协作模式,需要通过SSH等远程访问手段进行代码部署、调试或监控线上服务。下班后保持电脑开机,有利于他们在家或其他地点远程处理紧急任务。
3、持续集成/持续部署。
对于实施CI/CD流程的项目,电脑上的开发环境可能作为构建服务器的一部分,用于自动编译、测试和部署代码。在这种情况下,电脑全天候运行是必需的。
4、虚拟机与容器运行。
软件工程师使用的电脑上可能运行着虚拟机或容器,用于支持多套开发环境或者运行测试实例。这些虚拟资源,通常要求宿主机保持运行状态。
5、挂起与休眠模式。
虽然没有完全关机,但许多软件工程师会选择将电脑设置为休眠或挂起模式,这样既能节省能源,又能在短时间内快速恢复到工作状态。
实际上,以上5点归根到底,都是为了保持一个持续开发环境。那么,何为持续开发环境?
持续开发环境
持续开发环境是指软件工程师为了进行软件开发而搭建的、包含所有必要工具和服务的一套完整生态系统。它涵盖了集成开发环境(IDE)、版本控制系统(比如:Git)、本地服务器、数据库服务、构建工具以及各种编程框架和库等元素。这个环境是软件工程师日常工作的核心载体,也是他们实现高效编程、调试和测试的基础。
首先,持续开发环境通过自动化流程,极大地减少了开发过程中的人工干预。每当软件工程师提交代码到版本控制系统时,持续开发环境会自动触发构建、测试和部署流程。这意味着:软件工程师无需手动编译代码、运行测试用例或手动部署应用程序。这些繁琐的任务由持续开发环境自动完成,从而释放了软件工程师的时间和精力,让他们更专注于编写高质量的代码。
其次,持续开发环境有助于及时发现和修复问题。在持续集成的过程中,每次代码提交都会触发一次完整的构建和测试流程。这意味着:任何潜在的错误或问题都会在早期阶段被及时发现。此外,持续开发环境通常与持续监控和警报系统相结合,当出现问题时,系统会立即向团队成员发送警报,从而确保问题能够得到及时解决。
此外,持续开发环境还促进了团队协作和沟通。通过版本控制系统和自动化测试工具,团队成员可以轻松地查看彼此的代码、理解彼此的工作进度,并在出现问题时及时沟通。这种透明的工作方式有助于建立信任、减少误解,从而提高团队的整体效能。
最后,持续开发环境为创新提供了有力的支持。在快速迭代和不断试错的过程中,软件工程师可以迅速验证他们的想法和假设。如果某个功能或改进在实际应用中效果不佳,他们可以迅速调整方向,尝试新的方法。这种灵活性和敏捷性使得软件工程师能够不断尝试新的技术和方法,从而推动软件行业的创新和发展。
在这个日益复杂和快速变化的数字世界中,持续开发环境已经成为软件工程师们不可或缺的利器。但持续开发环境的搭建和启动可能耗时较长,因此为了保持工作连续性,软件工程师往往倾向于让电脑保持开机状态,以便随时可以继续编程或调试。
案例一
假设小张是一位正在开发一款大型Web应用的后端软件工程师,他的工作台的配置如下。
操作系统:Windows 10。
集成开发环境:IntelliJ IDEA,用于编写Java代码。
版本控制系统:Git,用于代码版本管理及团队协作。
本地服务器:Apache Tomcat,用于运行和测试Java Web应用。
数据库服务:MySQL,存储应用程序的数据。
构建工具:Maven,负责项目的自动化构建与依赖管理。
虚拟机环境:Docker容器,模拟生产环境以进行更真实的测试。
在每天的工作中,小张需要不断地编译代码、调试程序、提交更新到Git仓库,并在本地Tomcat服务器上验证功能是否正常。同时,他还可能需要在Docker容器内模拟不同的操作系统环境,以对软件进行兼容性测试。
如果小张下班时关闭了电脑,第二天重新启动所有服务和工具将会耗费至少半小时以上的时间。而在这段时间里,他无法立即开始编程或解决问题,影响了工作效率。
此外,小张所在的团队采用了CI/CD流程,利用Jenkins等工具自动执行代码编译、单元测试以及部署至测试服务器的任务。这就要求他的电脑作为Jenkins客户端始终在线,以便触发并完成这些自动化任务。
因此,为了确保高效流畅的开发流程,减少不必要的环境配置时间,及时响应线上问题以及支持远程协同,小张和其他许多软件工程师都会选择让自己的电脑始终保持开机状态,维持一个稳定的持续开发环境。
案例二
假设小李是一名全栈开发者,他正在参与一个大型的微服务项目,他的开发环境配置如下。
操作系统:Ubuntu 20.04 LTS。
集成开发环境:Visual Studio Code,用于编写前后端代码。
版本控制系统:Git,协同团队进行代码管理。
本地开发工具链:Node.js、NPM/Yarn用于前端开发,Python及pip用于后端开发,同时使用Kubernetes集群模拟生产环境部署。
数据库与缓存服务:MySQL作为主数据库,Redis作为缓存服务。
消息队列服务:RabbitMQ用于微服务间的异步通信。
CI/CD工具:GitHub Actions和Docker Compose结合,实现自动化构建、测试和部署。
在项目开发过程中,小李需要频繁地编译、打包、运行并测试各个微服务。一旦他关闭电脑,第二天重新启动所有服务将耗费大量时间。比如:搭建完整的Kubernetes集群可能需要数分钟到数十分钟不等,而每次重启服务都可能导致微服务间的依赖关系错乱,影响开发进度。
此外,由于团队采用了敏捷开发模式,每天都有多次代码提交和合并。为了能及时响应代码变动,小李设置了自己的电脑作为GitHub Actions的一部分,当有新的Pull Request时,可以立即触发自动化构建和测试流程,确保新代码的质量。
更进一步,在下班后或周末期间,如果线上服务出现紧急问题,小李可以通过SSH远程登录自己始终保持在线的电脑,快速定位问题所在,并在本地环境中复现和修复,然后推送到测试或生产环境,大大提高了响应速度和解决问题的效率。
综上所述,对于像小李这样的全栈开发者而言,维持一个持续稳定的开发环境是其高效工作的重要保障,也是应对复杂软件工程挑战的关键策略之一。
案例三
假设小王是一名独立游戏开发者,他正在使用Unity引擎制作一款3D角色扮演游戏,他的开发环境配置如下。
操作系统:macOS Big Sur。
集成开发环境:Unity Editor,集成了脚本编写、场景设计、动画编辑等多种功能。
版本控制系统:Perforce,用于大型项目文件的版本管理和团队协作。
资产构建工具:TexturePacker用于图片资源打包,FMOD Studio用于音频处理和混音。
本地测试环境:在电脑上运行Unity的内置播放器进行实时预览和调试。
云服务与部署平台:阿里云服务器作为远程测试和分发平台。
在游戏开发过程中,小王需要频繁地编辑代码、调整场景布局、优化美术资源并即时查看效果。由于Unity项目的加载和编译过程可能较长,尤其在处理大量纹理和模型时,如果每次关闭电脑后都要重新启动项目,无疑会大大降低工作效率。
此外,小王经常需要利用晚上或周末时间对游戏进行迭代更新,并将新版本上传到云端服务器进行远程测试。为了能在任何时刻快速响应工作需求,他的电脑始终保持开机状态,并且已连接至Perforce服务器,确保能及时获取最新的代码变更,同时也能立即上传自己的工作成果以供团队其他成员审阅和测试。
因此,对于小王这样的游戏开发者来说,保持持续开发环境不仅能有效提高日常工作效率,还能确保在非工作时段可以灵活应对突发任务,从而更好地满足项目进度要求。
总结
持续开发环境为程序员提供了一个高效、稳定且富有创新的工作环境。它通过自动化流程、及时发现问题、促进团队协作和支持创新,为软件开发带来了巨大的变革。
保持持续开发环境对于软件开发者而言至关重要,它能够显著提高工作效率,并确保项目开发的连贯性。通过维持开发环境始终在线,我们可以在任何时间方便地进行代码编辑、资源优化、实时预览和调试,并能灵活应对团队协作需求,实现快速迭代更新,从而满足项目进度要求。
来源:juejin.cn/post/7376837003520245772
种种迹象表明:前端岗位即将消失
最近,腾讯混元大模型的HR约我面试,为了确定是否真招人,我打开了腾讯内推的小程序,确实有这个岗位,但整个深圳也只有这一个。
于是,我突然意识到:在大模型时代,前端工程师这个岗位应该会是最先消失的岗位。
AI程序员的诞生
24年年初,英伟达CEO黄仁勋表示,自己相信就在不久的将来,人类再也不需要学习如何编码了,孩子们应该停止编程课。
然后24年3月,一家叫Cognition美国初创公司,发布了首个AI软件工程师Devin。它掌握全栈技能,云端部署、底层代码、改bug、训练和微调AI模型都不在话下。
只需一句指令,Devin就可端到端处理整个开发项目,这再度引发“码农是否将被淘汰”的大讨论。在SWE-bench上,它的表现远远超过Claude 2、Llama、GPT-4等选手,取得了13.86%的惊人成绩!
也就是说,它已经能通过AI公司的面试了。
接着4月,阿里发布消息称,其迎来了首位 AI 程序员——通义灵码。并在阿里云上海AI峰会上,阿里云宣布推出首个AI程序员,具备架构师、开发工程师、测试工程师等多种岗位的技能,能一站式自主完成任务分解、代码编写、测试、问题修复、代码提交整个过程,最快分钟级即可完成应用开发,大幅提升研发效率。
此次发布的AI程序员,是基于通义大模型构建的多智能体,每个智能体分别负责具体的软件开发任务并互相协作,可端到端实现一个产品功能的研发,这极大地简化了软件开发的流程。
由此带来的影响
一方面, AI技术的迅速发展和普及势必给程序员的工作带来冲击:传统的编码方式将显著改变,水平一般的程序员被取代的趋势或不可避免。
另一方面,尽管AI可以辅助程序员快速生成代码、提高开发效率,但并不能完全取代程序员的角色,尤其是技术理解深厚、能力强大的高水平程序员。
对于未来的程序员而言,掌握AI技术并应用于自己的工作流程中,与AI协同工作从而提高自己的工作效率和编码质量,是与时俱进、适应市场的必然需求。
由此,未来一名好的程序员不应仅仅是一名技术人员,还需要具备广泛的知识和技能。他们是整个人、机、环境系统框架中的创造者,要持续创新、创造价值。
具体而言,为了编写高质量代码,他们可能要精通多种编程语言;为了能按需选用合适的技术方案,他们要能迅速适应新的技术和工具。
为了面对复杂问题时能抓住原因并及时分析解决,他们必须保持与团队及客户的高效沟通协作,并不断积累知识、经验,同步跟进行业技术前沿,针对具体问题设计出创新的解决方案,保障程序的稳定性和可靠性。
所以,去年我在 从美团的开发通道合并谈谈开发的职业规划 就提出:LLM在软件工程的采用,将在众多工程领域产生突破,甚至于颠覆,由此也敦促我们必须认真审视专业能力的变迁和专业角色的定义。
为何最先消失的是前端岗
在我去年写前端学哪些技能饭碗越铁收入还高时,我还没有前端岗位可能即将消失的观点,但过去半年和很多猎头聊了一下前端岗的机会,以及看了很多后端培训课程中都包含前端的知识技能。
再结合22年我在美团内部,给几百个后端同学培训如何快速上手前端开发,我觉得前端这个岗位很有可能以后在招聘中就看不到这一细分岗位了。
其实15年前,全球应该都没有前端工程师这个岗位,当时的多数前端工作都比较简单,一部分是后端自己做,一个部分则是设计出生的切图仔完成~
后来随着移动互联网的兴起,前端开发语言发布了全新的规范ES6,整个前端开发生态逐步繁荣了起来,因为发展很快,网页的多端兼容和多版本工作比较繁杂,所以前端工作才由一个全新的岗位为负责。
原本很多前端同学在整个系统开发中就处于辅助角色,经常是多个团队的后端争抢一个专业的前端工程师,但如今,随着前端技术已经非常成熟和完善和大模型技术的加持,后端完成前端工作越来越容易。
所以,各公司自然就会减少很多前端岗位的招聘,只有少量技术比较新或业务比较复杂的项目才需要少量专职的前端工程师。
从各公司合并开发通道来看,消失的不仅是前端,还有后端和系统开发,对外招聘岗位都是软件工程师,工作内容根据需要动态调整。
总结
知识本身并不是力量,能有效将知识应用于实践才是真正的力量。同样,大量的编程知识可能是有价值的,但若不会运用、不知变通,无法解决实际问题,它就很难产生任何实质性影响。
能够有效使用程序,意味着智能体正具备将知识与学习应用转化的能力。这就需要程序员具备一些编程规则之外的能力,如分析、判断、解决问题的能力等。
程序员之所以能够不被取代,底气正在于其能将所学与实际情况相结合,并作出正确决策,而不是像AI程序员那样的编程工具,为了编程而编程。
未来,AI负责基础重复性劳动、人类程序员负责顶层设计的模式已经初露端倪,而认为人类程序员将被AI取代、沦为提要求的“边缘人”,为时尚早。
来源:juejin.cn/post/7392852233999892495
谁说forEach不支持异步代码,只是你拿不到异步结果而已
在前面探讨 forEach 中异步请求后端接口时,很多人都知道 forEach 中 async/await 实际是无效的,很多文章也说:forEach 不支持异步,forEach 只能同步运行代码,forEach 会忽略 await 直接进行下一次循环...
当时我的理解也是这样的,后面一细想好像不对,直接上我前面一篇文章用到的示例代码:
async function getData() {
const list = await $getListData()
// 遍历请求
list.forEach(async (item) => {
const res = await $getExtraInfo({
id: item.id
})
item.extraInfo = res.extraInfo
})
// 打印下最终处理过的额外数据
console.log(list)
}
上面 $getListData、$getExtraInfo 都是 promise 异步方法,按照上面说的 forEach 会直接忽略掉 await,那么循环体内部拿到的 res 就应该是 undefined,后面的 res.extraInfo 应该报错才对,但是实际上代码并没有报错,说明 await 是有效的,内部的异步代码也是可以正常运行的,所以 forEach 肯定是支持异步代码的。
手写版 forEach
先从自己实现的简版 forEach 看起:
Array.prototype.customForEach = function (callback) {
for (let i = 0; i < this.length; i++) {
callback(this[i], i, this)
}
}
里面会为数组的每个元素执行一下回调函数,实际拿几组数组测试和正宗的 forEach 方法效果也一样。可能很多人还是会有疑问你自己实现这到底靠不靠谱,不瞒你说我也有这样的疑问。
MDN 上关于 forEach 的说明
先去 MDN 上搜一下 forEach,里面的大部分内容只是使用层面的文档,不过里面有提到:“forEach() 期望的是一个同步函数,它不会等待 Promise 兑现。在使用 Promise(或异步函数)作为 forEach 回调时,请确保你意识到这一点可能带来的影响”。
ECMAScript 中 forEach 规范
继续去往 javascript 底层探究,我们都知道执行 js 代码是需要依靠 js 引擎,去将我们写的代码解释翻译成计算机能理解的机器码才能执行的,所有 js 引擎都需要参照 ECMAScript 规范来具体实现,所以这里我们先去看下 ECMAScript 上关于 forEach 的标准规范:
添加图片注释,不超过 140 字(可选)
谷歌 V8 的 forEach 实现
常见的 js 引擎有:谷歌的 V8、火狐 FireFox 的 SpiderMonkey、苹果 Safari 的 JavaScriptCore、微软 Edge 的 ChakraCore...后台都很硬,这里我们就选其中最厉害的谷歌浏览器和 nodejs 依赖的 V8 引擎,V8 中对于 forEach 实现的主要源码:
transitioning macro FastArrayForEach(implicit context: Context)(
o: JSReceiver, len: Number, callbackfn: Callable, thisArg: JSAny): JSAny
labels Bailout(Smi) {
let k: Smi = 0;
const smiLen = Cast<Smi>(len) otherwise goto Bailout(k);
const fastO = Cast<FastJSArray>(o) otherwise goto Bailout(k);
let fastOW = NewFastJSArrayWitness(fastO);
// Build a fast loop over the smi array.
for (; k < smiLen; k++) {
fastOW.Recheck() otherwise goto Bailout(k);
// Ensure that we haven't walked beyond a possibly updated length.
if (k >= fastOW.Get().length) goto Bailout(k);
const value: JSAny = fastOW.LoadElementNoHole(k)
otherwise continue;
Call(context, callbackfn, thisArg, value, k, fastOW.Get());
}
return Undefined;
}
源码是 .tq 文件,这是 V8 团队开发的一个叫 Torque 的语言,语法类似 TypeScript,所以对于前端程序员上面的代码大概也能看懂,想要了解详细的 Torque 语法,可以直接去 V8 的官网上查看。
从上面的源码可以看到 forEach 实际还是依赖的 for 循环,没有返回值所以最后 return 的一个 Undefined。看完源码是不是发现咱上面的手写版也大差不差,只不过 V8 里实现了更多细节的处理。
结论:forEach 支持异步代码
最后的结论就是:forEach 其实是支持异步的,循环时并不是会直接忽略掉 await,但是因为 forEach 没有返回值,所以我们在外部没有办法拿到每次回调执行过后的异步 promise,也就没有办法在后续的代码中去处理或者获取异步结果了,改造一下最初的示例代码:
async function getData() {
const list = await $getListData()
// 遍历请求
list.forEach(async (item) => {
const res = await $getExtraInfo({
id: item.id
})
item.extraInfo = res.extraInfo
})
// 打印下最终处理过的额外数据
console.log(list)
setTimeout(() => {
console.log(list)
}, 1000 * 10)
}
你会发现 10 秒后定时器中是可以按照预期打印出我们想要的结果的,所以异步代码是生效了的,只不过在同步代码中我们没有办法获取到循环体内部的异步状态。
如果还是不能理解,我们对比下 map 方法,map 和 forEach 很类似,但是 map 是有返回值的,每次遍历结束之后我们是可以直接 return 一个值,后续我们就可以接收到这个返回值。这也是为什么很多文章中改写 forEach 异步操作时,使用 map 然后借助 Promise.all 来等待所有异步操作完成后,再进行下面的逻辑来实现同步的效果。
参考文档
- MDN forEach 文档:developer.mozilla.org/zh-CN/docs/…
- ECMAScript 中 forEach 规范:tc39.es/ecma262/#se…
- 谷歌 V8 中 forEach 源码:chromium.googlesource.com/v8/v8.git/+…
- 谷歌 V8 中 map 源码:chromium.googlesource.com/v8/v8.git/+…
- 谷歌 V8 官网:v8.dev
- 谷歌 V8 源码:github.com/v8/v8
来源:juejin.cn/post/7389912354749087755
js如何实现当文本内容过长时,中间显示省略号...,两端正常展示
前一阵做需求时,有个小功能实现起来废了点脑细胞,觉得可以记录一下。
产品的具体诉求是:用户点击按钮进入详情页面,详情页内的卡片标题内容过长时,标题的前后两端正常展示,中间用省略号...表示,并且鼠标悬浮后,展示全部内容。
关于鼠标悬浮展示全部内容的代码就不放在这里了,本文主要写关于实现中间省略号...的代码。
实现思路
- 获取标题盒子的真实宽度, 我这里用的是clientWidth;
- 获取文本内容所占的实际宽度;
- 根据文字的大小计算出每个文字所占的宽度;
- 判断文本内容的实际宽度是否超出了标题盒子的宽度;
- 通过文字所占的宽度累加之和与标题盒子的宽度做对比,计算出要截取位置的索引;
- 同理,文本尾部的内容需要翻转一下,然后计算索引,截取完之后再翻转回来;
代码
html代码
<div class="title" id="test">近日,银行纷纷下调大额存单利率,但银行定期存款仍被疯抢。银行理财经理表示:有意向购买定期存款要尽快,不确定利率是否会再降。</div>
css代码: 设置文本不换行,同时设置overflow:hidden
让文本溢出盒子隐藏
.title {
width: 640px;
height: 40px;
line-height: 40px;
font-size: 14px;
color: #00b388;
border: 1px solid #ddd;
overflow: hidden;
/* text-overflow: ellipsis; */
white-space: nowrap;
/* box-sizing: border-box; */
padding: 0 10px;
}
javascript代码:
获取标题盒子的宽度时要注意,如果在css样式代码中设置了padding, 就需要获取标题盒子的左右padding值。 通过getComputedStyle
属性获取到所有的css样式属性对应的值, 由于获取的padding值都是带具体像素单位的,比如: px
,可以用parseInt特殊处理一下。
获取盒子的宽度的代码,我当时开发时是用canvas计算的,但计算的效果不太理想,后来逛社区,发现了嘉琪coder
大佬分享的文章,我这里就直接把代码搬过来用吧, 想了解的掘友可以直接滑到文章末尾查看。
判断文本内容是否超出标题盒子
// 标题盒子dom
const dom = document.getElementById('test');
// 获取dom元素的padding值
function getPadding(el) {
const domCss = window.getComputedStyle(el, null);
const pl = Number.parseInt(domCss.paddingLeft, 10) || 0;
const pr = Number.parseInt(domCss.paddingRight, 10) || 0;
console.log('padding-left:', pl, 'padding-right:', pr);
return {
left: pl,
right: pr
}
}
// 检测dom元素的宽度,
function checkLength(dom) {
// 创建一个 Range 对象
const range = document.createRange();
// 设置选中文本的起始和结束位置
range.setStart(dom, 0),
range.setEnd(dom, dom.childNodes.length);
// 获取元素在文档中的位置和大小信息,这里直接获取的元素的宽度
let rangeWidth = range.getBoundingClientRect().width;
// 获取的宽度一般都会有多位小数点,判断如果小于0.001的就直接舍掉
const offsetWidth = rangeWidth - Math.floor(rangeWidth);
if (offsetWidth < 0.001) {
rangeWidth = Math.floor(rangeWidth);
}
// 获取元素padding值
const { left, right } = getPadding(dom);
const paddingWidth = left + right;
// status:文本内容是否超出标题盒子;
// width: 标题盒子真实能够容纳文本内容的宽度
return {
status: paddingWidth + rangeWidth > dom.clientWidth,
width: dom.clientWidth - paddingWidth
};
}
通过charCodeAt返回指定位置的字符的Unicode
编码, 返回的值对应ASCII码表对应的值,0-127包含了常用的英文、数字、符号等,这些都是占一个字节长度的字符,而大于127的为占两个字节长度的字符。
截取和计算文本长度
// 计算文本长度,当长度之和大于等于dom元素的宽度后,返回当前文字所在的索引,截取时会用到。
function calcTextLength(text, width) {
let realLength = 0;
let index = 0;
for (let i = 0; i < text.length; i++) {
charCode = text.charCodeAt(i);
if (charCode >= 0 && charCode <= 128) {
realLength += 1;
} else {
realLength += 2 * 14; // 14是字体大小
}
// 判断长度,为true时终止循环,记录索引并返回
if (realLength >= width) {
index = i;
break;
}
}
return index;
}
// 设置文本内容
function setTextContent(text) {
const { status, width } = checkLength(dom);
let str = '';
if (status) {
// 翻转文本
let reverseStr = text.split('').reverse().join('');
// 计算左右两边文本要截取的字符索引
const leftTextIndex = calcTextLength(text, width);
const rightTextIndex = calcTextLength(reverseStr, width);
// 将右侧字符先截取,后翻转
reverseStr = reverseStr.substring(0, rightTextIndex);
reverseStr = reverseStr.split('').reverse().join('');
// 字符拼接
str = `${text.substring(0, leftTextIndex)}...${reverseStr}`;
} else {
str = text;
}
dom.innerHTML = str;
}
最终实现的效果如下:
上面就是此功能的所有代码了,如果想要在本地试验的话,可以在本地新建一个html文件,复制上面代码就可以了。
下面记录下从社区内学到的相关知识:
- js判断文字被溢出隐藏的几种方法;
- JS获取字符串长度的几种常用方法,汉字算两个字节;
1、 js判断文字被溢出隐藏的几种方法
1. Element-plus这个UI框架中的表格组件实现的方案。
通过document.createRange
和document.getBoundingClientRect()
这两个方法实现的。也就是我上面代码中实现的checkLength
方法。
2. 创建一个隐藏的div模拟实际宽度
通过创建一个不会在页面显示出来的dom元素,然后把文本内容设置进去,真实的文本长度与标题盒子比较宽度,判断是否被溢出隐藏了。
function getDomDivWidth(dom) {
const elementWidth = dom.clientWidth;
const tempElement = document.createElement('div');
const style = window.getComputedStyle(dom, null)
const { left, right } = getPadding(dom); // 这里我写的有点重复了,可以优化
tempElement.style.cssText = `
position: absolute;
top: -9999px;
left: -9999px;
white-space: nowrap;
padding-left:${style.paddingLeft};
padding-right:${style.paddingRight};
font-size: ${style.fontSize};
font-family: ${style.fontFamily};
font-weight: ${style.fontWeight};
letter-spacing: ${style.letterSpacing};
`;
tempElement.textContent = dom.textContent;
document.body.appendChild(tempElement);
const obj = {
status: tempElement.clientWidth + right + left > elementWidth,
width: elementWidth - left - right
}
document.body.removeChild(tempElement);
return obj;
}
3. 创建一个block元素来包裹inline元素
这种方法是在UI框架acro design vue
中实现的。外层套一个块级(block)元素,内部是一个行内(inline)元素。给外层元素设置溢出隐藏的样式属性,不对内层元素做处理,这样内层元素的宽度是不变的。因此,通过获取内层元素的宽度和外层元素的宽度作比较,就可以判断出文本是否被溢出隐藏了。
// html代码
<div class="title" id="test">
<span class="content">近日,银行纷纷下调大额存单利率,但银行定期存款仍被疯抢。银行理财经理表示:有意向购买定期存款要尽快,不确定利率是否会再降。</span>
</div>
// 创建一个block元素来包裹inline元素
const content = document.querySelector('.content');
function getBlockDomWidth(dom) {
const { left, right } = getPadding(dom);
console.log(dom.clientWidth, content.clientWidth)
const obj = {
status: dom.clientWidth < content.clientWidth + left + right,
width: dom.clientWidth - left - right
}
return obj;
}
4. 使用canvas中的measureText方法和TextMetrics对象来获取元素的宽度
通过Canvas 2D渲染上下文(context)可以调用measureText方法,此方法会返回TextMetrics对象,该对象的width
属性值就是字符占据的宽度,由此也能获取到文本的真实宽度,此方法有弊端,比如说兼容性,精确度等等。
// 获取文本长度
function getTextWidth(text, font = 14) {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d")
context.font = font
const metrics = context.measureText(text);
return metrics.width
}
5. 使用css实现
这种方式来自评论区的掘友@S_mosar
提供的思路。
先来看下效果:
代码如下:
css部分
.con {
font-size: 14px;
color: #666;
width: 600px;
margin: 50px auto;
border-radius: 8px;
padding: 15px;
overflow: hidden;
resize: horizontal;
box-shadow: 20px 20px 60px #bebebe, -20px -20px 60px #ffffff;
}
.wrap {
position: relative;
line-height: 2;
height: 2em;
padding: 0 10px;
overflow: hidden;
background: #fff;
margin: 5px 0;
}
.wrap:nth-child(odd) {
background: #f5f5f5;
}
.title {
display: block;
position: relative;
background: inherit;
text-align: justify;
height: 2em;
overflow: hidden;
top: -4em;
}
.txt {
display: block;
max-height: 4em;
}
.title::before{
content: attr(title);
width: 50%;
float: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
direction: rtl;
}
html部分
<ul class="con">
<li class="wrap">
<span class="txt">CSS 实现优惠券的技巧 - 2021-03-26</span>
<span class="title" title="CSS 实现优惠券的技巧 - 2021-03-26">CSS 实现优惠券的技巧 - 2021-03-26</span>
</li>
<li class="wrap">
<span class="txt">CSS 测试标题,这是一个稍微有点长的标题,超出一行以后才会有title提示,标题是 实现优惠券的技巧 - 2021-03-26</span>
<span class="title" title="CSS 测试标题,这是一个稍微有点长的标题,超出一行以后才会有title提示,标题是 实现优惠券的技巧 - 2021-03-26">CSS
测试标题,这是一个稍微有点长的标题,超出一行以后才会有title提示,标题是 实现优惠券的技巧 - 2021-03-26</span>
</li>
<li class="wrap">
<span class="txt">CSS 拖拽?</span>
<span class="title" title="CSS 拖拽?">CSS 拖拽?</span>
</li>
<li class="wrap">
<span class="txt">CSS 文本超出自动显示title</span>
<span class="title" title="CSS 文本超出自动显示title">CSS 文本超出自动显示title</span>
</li>
</ul>
思路解析:
- 文字内容的父级标签li设置
line-height: 2;
、overflow: hidden;
、height: 2em;
,因此 li 标签的高度是当前元素字体大小的2倍,行高也是当前字体大小的2倍,同时内容若溢出则隐藏。 - li 标签内部有两个 span 标签,二者的作用分别是:类名为
.txt
的标签用来展示不需要省略号时的文本,类名为.title
用来展示需要省略号时的文本,具体是如何实现的请看第五步。 - 给
.title
设置伪类before
,将伪类宽度设置为50%,搭配浮动float: right;
,使得伪类文本内容靠右,这样设置后,.title
和伪类就会各占父级宽度的一半了。 .title
标签设置text-align: justify;
,用来将文本内容和伪类的内容两端对齐。- 给伪类
before
设置文字对齐方式direction: rtl;
,将伪类内的文本从右向左流动,即right to left
,再设置溢出省略的css样式就可以了。 .title
标签设置了top: -4em
,.txt
标签设置max-height: 4em;
这样保证.title
永远都在.txt
上面,当内容足够长,.txt
文本内容会换行,导致高度从默认2em变为4em,而.title
位置是-4em
,此时正好将.txt
覆盖掉,此时显示的就是.title
标签的内容了。
知识点:text-align: justify;
- 文本的两端(左边和右边)都会与容器的边缘对齐。
- 为了实现这种对齐,浏览器会在单词之间添加额外的空间。这通常意味着某些单词之间的间距会比其他单词之间的间距稍大一些。
- 如果最后一行只有一个单词或少数几个单词,那么这些单词通常不会展开以填充整行,而是保持左对齐。
需要注意的是,
text-align: justify;
主要用于多行文本。对于单行文本,这个值的效果与text-align: left;
相同,因为单行文本无法两端对齐。
2、JS获取字符串长度的几种常用方法
1. 通过charCodeAt判断字符编码
通过charCodeAt获取指定位置字符的Unicode
编码,返回的值对应ASCII码表对应的值,0-127包含了常用的英文、数字、符号等,这些都是占一个字节长度的字符,而大于127的为占两个字节长度的字符。
function calcTextLength(text) {
let realLength = 0;
for (let i = 0; i < text.length; i++) {
charCode = text.charCodeAt(i);
if (charCode >= 0 && charCode <= 128) {
realLength += 1;
} else {
realLength += 2;
}
}
return realLength;
}
2. 采取将双字节字符替换成"aa"的做法,取长度
function getTextWidth(text) {
return text.replace(/[^\x00-\xff]/g,"aa").length;
};
参考文章
4. canvas绘制字体偏上不居中问题、文字垂直居中后偏上问题、measureText方法和TextMetrics对象
来源:juejin.cn/post/7329967013923962895
一文让你彻底悟透柯里化
什么是柯里化?
在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术
前端为什么需要使用柯里化?
前端使用柯里化的用途主要就是简化代码结构,提高系统的维护性,一个方法只有一个参数,强制了功能的单一性,很自然就能做到功能内聚,降低耦合
一句话就是:降低代码重复率,提高代码适应性
普通函数
实现一个普通的累加函数,调用时需要传入三个参数,如果少 传则输出NaN,多传则后面的参数都无效
function add(a,b,c){
return a + b + c
}
add(1,2,3) //6
普通柯里化函数
实现一个普通的柯里化函数(含有柯里化性质),通过调用传参的方式将函数传入,并传入参数进行运算,返回一个新的函数的参数进行累计(取决于传入函数时传入参数的个数以及执行函数的传入参数进行累计)
function add(a, b, c) {
return a + b + c;
}
function fixedCurryAdd(fn) {
const arg = [].slice.call(arguments, 1);
return function () {
const newArg = arg.concat([].slice.call(arguments, 0));
return fn.apply(this, newArg);
};
}
const curryAdd = new fixedCurryAdd(add, 1);
console.log(curryAdd(2,11)); //14
柯里化函数
通过上面的含有柯里化性质的函数可以看出 ,要实现柯里化函数可以有多种传参方式,例如:
newAdd(1,2,3,4)
newAdd(1)(2,3,4)
newAdd(1)(2,3)(4)
newAdd(1)(2)(3)(4)
含有多种传参方式 ,无论哪种方式,最后都会把所需参数传入,但是柯里化函数只是期望你执行这次函数传入所需参数个数,并不强求你传入所需参数(4个,可传1个后续补上即可,最后一次凑齐4个即可)
function add(a, b, c) {
return a + b + c;
}
function CurryAdd(fn){
let arg = [].slice.call(arguments,1);
return function(){
let newArg = arg.concat([].slice.call(arguments,0));
return fn.apply(this,newArg);
}
}
function Curry(fn,length){
let len = length|| fn.length; //获取传入函数所需参数的个数
return function(){
if(arguments.length <len){
const callback = [fn].concat([].slice.call(arguments,0));
return Curry(CurryAdd.apply(this,callback),len-arguments.length);
}else{
return fn.apply(this,arguments);
}
}
}
let adds=new Curry(add)
let a = adds(1)(2)
console.log(a(1)); //4
以上完善柯里化函数的整个书写。下面来捋一下这个书写过程的思路
- 首先柯里化函数期待传入一个函数,并且返回一个函数(add)
- 通过fn.length获取当前以传入的参数个数
- 在返回函数中判断当前参数是否已传入完毕
- 如果已传入fn.length个 则直接调用传入函数
- 如果未传入fn.length个 则通过callback将fn放到第一位在进行合并arguments作为下一次进入此函数的参数,通过CurryAdd 函数对参数再进行一遍"过滤",通过递归调用自己来判断参数是否已经达到fn.length个从而实现柯里化
柯里化应用
说了这么多,那么柯里化到底能做哪些应用呢?
在前端页面中,向后端进行数据请求时,大部分都用到ajax进行请求
function ajax(method,url,data){
...ajax请求体(不作书写)
}
ajax("post","/api/getNameList",params)
ajax("post","/api/getAgeList",params)
ajax("post","/api/getSexList",params)
如果有这么多请求且每次都需要写请求方式("post"),页面多了请求多了自然成为冗余代码,那么优化一下
const newAjax = Curry(ajax);
const postAjax = newAjax("post")
...
如果url还有类似的那么就可以重复以上的代码,这样能减少相同代码重复出现
来源:juejin.cn/post/7389049604632166427
背调,程序员入职的紧箍咒
首先说下,目前,我的表哥自己开一家小的背调公司,所以我在跟他的平时交流中,了解到了背调这个行业的一些信息。
今天跟大家分享出来,给大家在求职路上避避坑。
上周的某天,以前的阿里同事小李跟我说,历经两个月的面试,终于拿到了开水团的offer。我心里由衷地替他高兴,赶紧恭喜了他,现在这年头,大厂的offer没这么好拿的。
又过了两周,小张沮丧地跟我说,这家公司是先发offer后背调,结果背调之后,offer GG了,公司HR没有告知他具体原因,只是委婉地说有缘自会再相见。(手动狗头)
我听了,惋惜之余有些惊讶,问了他具体情况。
原来,小李并没有在学历上作假,也没有做合并或隐藏工作经历的事。
他犯错的点是,由于在上家公司,他跟他老板的关系不好,所以他在背调让填写上级领导信息的时候,只写了上级领导的名字,电话留的是他一个同事的。
我听后惋惜地一拍脑门儿,说:“你这么做之前,怎么也不问我一下啊?第三方背调公司进行手机号和姓名核实,都是走系统的,秒出结果。而且,这种手机号的机主姓名造假,背调结果是亮红灯的,必挂。”
小李听后,也是悔得肠子都青了,没办法,只能重新来过了。
我以前招人的时候,遇到过一次这样的情况,当时有个候选人面试通过,发起背调流程。一周后,公司HR给了我一份该候选人背调结果的pdf,上面写着:
“候选人背调信息上提供,原公司上级为郭xx,但经查手机号主为王xx,且候选人原公司并无此人。”
背调结果,红灯,不通过。
基本面
学历信息肯定不能造假,这个大家应该都清楚,学信网不是吃素的,秒出结果。
最近两份工作的入离职时间不要出问题,这个但凡是第三方背调,近两份工作是必查项,而且无论是明察还是暗访,都会查得非常仔细,很难钻空子的。
再有就是刚才说的,手机号和人名要对上,而且这个人确实是在这家公司任职的。
大家耳熟能详的大厂最好查,背调公司都有人才数据库的,而且圈子里的人也好找。再有就是,随便找个内部员工,大厂的组织结构在内部通讯软件里都能看到的。
小厂难度大一些,如果人才数据库没有的话,背调员会从网上找公司电话,然后打给前台,让前台帮忙找人。但有的前台听了会直接挂断电话。
薪资方面,不要瞒报,一般背调公司会让你打印最近半年或一年的流水,以及纳税信息。
直接上级
这应该也是大家最关心的问题之一。
马云曾经说过:离职无非两种原因,钱没给够,心委屈了。而心委屈了,绝大多数都跟自己的直接上级有关。
如果在背调的时候,担心由于自己跟直接上级关系不好,从而导致背调结果不利的话,可以尝试以下三种方式。
第一,如果你在公司里历经了好几任领导的话,可以留关系最好的那任领导的联系方式,这个是在规则允许范围内的。
第二,如果你的直接上级只是一个小组长,而你跟大领导(类似于部门负责人)关系还可以的话,可以跟大领导沟通一下,然后背调留他的信息。像这个,一般背调公司不会深究的。
就像我的那个表哥,背调公司的老板所说的:“如果一个腾讯员工,马化腾都出来给他做背调了,那我们还能说什么呢?”
第三,如果前两点走不通的话,还可以坦诚地跟HR沟通一次,说明跟上级之间确实存在一些问题,原因是什么什么。
比如:我朋友遇到了这种情况,公司由于经营不善而裁员,老板竟然无耻地威胁我朋友,如果要N+1赔偿的话,背调就不会配合。
如果你确实不是责任方的话,一般HR也能理解。毕竟都是打工人,何苦相互为难呢。
你还可以这么加上一句:“我之前工作过的公司,您背调哪家都可以,我的口碑都很好的,唯独这家有些问题。”
btw:还有一些朋友,背调的时候留平级同事的真实电话和姓名,用来冒充领导,这个是有风险的。但是遇到背调不仔细的公司,也能通过。通过概率的话,一半一半吧。
就像我那个朋友所说:“现在人力成本不便宜,如果公司想盈利的话,我的背调员一天得完成5个背调,平均不到两个小时一个。你总不能希望他们个个都是名侦探柯南吧。”
信用与诉讼
一般来讲,背调的标准套餐还包括如下内容:金融违规、商业利益冲突、个人信用风险和有限民事诉讼。其中后两个大家尽量规避。
个人信用风险包括:网贷/逾期风险、反欺诈名单和欠税报告。
网贷这块,当时我有一个同事,2021年的时候,拿了4个offer,结果不明不白地都挂在了背调上,弄得他很懵逼。
当他问这三家公司HR原因的时候,HR都告诉他不便透露。
最后,他动用身边人脉,才联系上一家公司的HR出来吃饭,HR跟他说:“以后网贷不要逾期,尤其是不同来源的网贷多次逾期。”
同事听了,这才恍然大悟。
欠税这个,就更别说了,呵呵,大家都懂,千万别心存侥幸。
再说说劳动仲裁和民事诉讼。
现在有些朋友确实法律意识比较强,受到不公正待遇了,第一想法就是“我要去仲裁”,仲裁不满意了,就去打官司。
首先我要说的是,劳动仲裁是查不到的,所以尽量在这一步谈拢解决。
但民事诉讼在网上都是公开的,而且第三方背调公司也是走系统的,一查一个准儿。如果非必要的话,尽量不要跟公司闹到这一步。
如果真遇到垃圾公司或公司里的垃圾人,第一个想法应该是远离,不要让他们往你身上倒垃圾。
尤其是你主动跟公司打官司这种,索要个加班费、年终奖什么的,难免会让新公司产生顾虑,会不会我offer的这名候选人,以后也会有对簿公堂的一天。
结语
现在这大市场行情,求职不易,遇到入职前背调更是如履薄冰,希望大家都能妥善处理好,一定要避免节外生枝的情况发生,不要在距离成功一米的距离倒下。
最后,祝大家工作顺利,纵情向前,人人都能拿到自己满意的offer,开开心心地入职。
来源:juejin.cn/post/7295160228879204378
Python: 深入了解调试利器 Pdb
Python是一种广泛使用的编程语言,以其简洁和可读性著称。在开发和调试过程中,遇到错误和问题是不可避免的。Python为此提供了一个强大的调试工具——Pdb(Python Debugger)。Pdb是Python标准库中自带的调试器,可以帮助开发者跟踪代码执行、查看变量值、设置断点等功能。本文将详细介绍Pdb的使用方法,并结合实例展示其强大的调试能力。
1. Pdb简介
Pdb是Python内置的调试器,支持命令行操作,可以在Python解释器中直接调用。Pdb提供了一系列命令来控制程序的执行,查看和修改变量值,甚至可以在运行时修改代码逻辑。
2. 如何启动Pdb
在Python代码中启动Pdb有多种方式,以下是几种常见的方法:
2.1 在代码中插入断点
在代码中插入import pdb; pdb.set_trace()
可以在运行到该行时启动Pdb:
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)
import pdb; pdb.set_trace()
print(factorial(5))
2.2 通过命令行启动
可以通过命令行启动Python脚本,并在需要调试的地方使用pdb
模块:
python -m pdb myscript.py
3. Pdb的基本命令
Pdb提供了许多命令来控制调试过程,以下是一些常用命令:
b
(break
): 设置断点c
(continue
): 继续执行程序直到下一个断点s
(step
): 进入函数内部逐行执行n
(next
): 执行下一行,不进入函数内部p
(print
): 打印变量的值q
(quit
): 退出调试器
4. 实战示例
让我们通过一个具体的例子来演示Pdb的使用。假设我们有一个简单的Python脚本,用于计算列表中元素的平均值:
def average(numbers):
total = sum(numbers)
count = len(numbers)
return total / count
numbers = [1, 2, 3, 4, 5]
print(average(numbers))
4.1 设置断点并启动调试
我们希望在计算平均值之前检查total
和count
的值:
import pdb; pdb.set_trace()
def average(numbers):
total = sum(numbers)
count = len(numbers)
return total / count
numbers = [1, 2, 3, 4, 5]
print(average(numbers))
运行上述代码,当程序执行到pdb.set_trace()
时,将进入调试模式:
PS C:\src\uml\2024\07> python -m pdb myscript.py
> c:\src\uml\2024\07\myscript.py(1)<module>()
-> import pdb; pdb.set_trace()
(Pdb) n
> c:\src\uml\2024\07\myscript.py(3)<module>()
-> def average(numbers):
(Pdb) m
*** NameError: name 'm' is not defined
(Pdb) n
> c:\src\uml\2024\07\myscript.py(8)<module>()
-> numbers = [1, 2, 3, 4, 5]
(Pdb) n
> c:\src\uml\2024\07\myscript.py(9)<module>()
-> print(average(numbers))
(Pdb) n
3.0
--Return--
> c:\src\uml\2024\07\myscript.py(9)<module>()->
-> print(average(numbers))
4.2 查看变量值
在调试模式下,可以使用p
命令查看变量值:
(Pdb) p numbers
[1, 2, 3, 4, 5]
(Pdb)
通过这种方式,可以一步步检查变量的值和程序的执行流程。
5. 高级功能
除了基本命令,Pdb还提供了许多高级功能,如条件断点、调用栈查看等。
5.1 查看调用栈
使用where
命令可以查看当前的调用栈:
(Pdb) where
<frozen runpy>(198)_run_module_as_main()
<frozen runpy>(88)_run_code()
c:\users\heish\miniconda3\lib\pdb.py(1952)<module>()->
-> pdb.main()
c:\users\heish\miniconda3\lib\pdb.py(1925)main()
-> pdb._run(target)
c:\users\heish\miniconda3\lib\pdb.py(1719)_run()
-> self.run(target.code)
c:\users\heish\miniconda3\lib\bdb.py(600)run()
-> exec(cmd, globals, locals)
<string>(1)<module>()->
> c:\src\uml\2024\07\myscript.py(9)<module>()->
-> print(average(numbers))
6. 总结
Pdb是Python提供的一个功能强大的调试工具,掌握它可以大大提高代码调试的效率。在开发过程中,遇到问题时不妨多利用Pdb进行调试,找出问题的根源。通过本文的介绍,希望大家能够更好地理解和使用Pdb,为Python编程之路增添一份助力。
来源:juejin.cn/post/7392439754678321192
安卓开发转鸿蒙开发到底有多简单?
前言
相信各位搞安卓的同学多多少少都了解过鸿蒙了,有些一知半解而有些已经开始学习起来。那这个鸿蒙到底好不好搞?要不要搞?
安卓反正目前工作感觉不好找,即便是上海这样的大城市也难搞,人员挺饱和的。最近临近年关裁员的也很多。想想还是搞鸿蒙吧现在刚刚要起步说不定有机会!
首先可以肯定的一点,对于做安卓的来说鸿蒙很好搞,究竟有多好搞我来给大家说说。最近开始学鸿蒙,对其开发过程有了一定了解。刚好可以进行一些对比。
好不好搞?
开发环境
要我说,好搞的很。首先开发环境一样,不是说长得像,而是就一模一样。
你看这个DevEco-Studio和Android Studio什么关系,就是双胞胎。同样基于Intellj IDEA开发, 刚装上的时候我都惊呆了,熟悉的感觉油然而生。
再来仔细看看:
- 项目文件管理栏,同样可以切换Project和Packages视图
- 底部工具栏,文件管理,日志输出,终端,Profiler等
- SDK Manager, 和安卓一样也内建了SDK管理器,可以下载管理不同版本的SDK
- 模拟器管理器
可以看出鸿蒙开发的IDE是功能完备并且安卓开发人员可以无学习成本进行转换。
开发工具
安卓开发中需要安装Java语言支持,由于开发过程需要进行调试,adb也是必不可少的。
在鸿蒙中,安装EcoDev-Studio后,可以在IDE中选择安装Node.js即可。由于鸿蒙开发使用的语言是基于TS改进增强而来,也就是熟悉JS语言就可以上手。而会JAVA的话很容易可以上手JS
- 语言支持
- 鸿蒙上的类似adb的工具名叫hdc
hdc(HarmonyOS Device Connector)是HarmonyOS为开发人员提供的用于调试的命令行工具,通过该工具可以在windows/linux/mac系统上与真实设备或者模拟器进行交互。
- hdc list targets
- hdc file send local remote
- hdc install package File
这里列举的几个命令是不是很熟悉?一看名字就知道和安卓中的adb是对应关系。不需要去记忆,在需要使用到的时候去官网查一下就行: hdc使用指导
配置文件
安卓中最主要的配置文件是AndroidManifest.xml。 其中定义了版本号,申明了页面路径,注册了广播和服务。并且申明了App使用的权限。
而鸿蒙中也对应有配置文件,但与安卓稍有不同的是鸿蒙分为多个文件。
- build-profile.json5
Sdk Version配置在这里, 代码的模块区分也在这里
{
"app": {
"signingConfigs": [],
"compileSdkVersion": 9,
"compatibleSdkVersion": 9,
"products": [
{
"name": "default",
"signingConfig": "default",
}
],
"buildModeSet": [
{
"name": "debug",
},
{
"name": "release"
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
}
]
}
- app.json5
包名,VersionCode,VersionName等信息
{
"app": {
"bundleName": "com.example.firstDemo",
"vendor": "example",
"versionCode": 1000000,
"versionName": "1.0.0",
"icon": "$media:app_icon",
"label": "$string:app_name"
}
}
- module.json5
模块的详细配置,页面名和模块使用到的权限在这里申明
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone",
"tablet"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ts",
"description": "$string:EntryAbility_desc",
"icon": "$media:icon",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.home"
]
}
]
}
],
"requestPermissions":[
{
"name" : "ohos.permission.APPROXIMATELY_LOCATION",
"reason": "$string:reason",
"usedScene": {
"abilities": [
"FormAbility"
],
"when":"inuse"
}
}
]
}
}
官方指导
安卓开发的各种技术文档在网上可以很方便的搜索到,各种demo也有基数庞大的安卓开发者在技术网站上分享。虽然鸿蒙目前处于刚起步的阶段,但是官方的技术文档目前也已经非常完善,并且可以感受到鸿蒙的官方维护团队肯定在高强度加班中,他们的文档更新的太快了。经常能看到文档的编辑日期在迅速迭代。
从日期可以看到非常新。而且文档都是中文的,学习和查找起来都特别方便。
并且不仅仅是api文档,鸿蒙官方还提供了各种用以学习的demo, 甚至还有官方的视频教程和开发论坛。
遇到问题有各种方法可以解决,查文档,看视频课程,抄官方demo, 论坛发帖提问,简直是保姆级的官方支持!
其他
- 鸿蒙的UI开发模式是一种响应式开发,与安卓的compose UI很像。组件的名字可能不同,但是概念上是一致的,并且鸿蒙的原生组件种类丰富也比较全。熟悉以后使用起来很方便。
build() {
Column() {
Text(this.accessText)
.fontSize(20)
.fontWeight(FontWeight.Bold)
if (!this.hasAccess) {
Button('点击申请').margin({top: 12})
.onClick(() => {
this.reqPermissionsFromUser(this.permissions);
})
} else {
Text('设备模糊位置信息:' + '\n' + this.locationText)
.fontSize(20)
.margin({top: 12})
.width('100%')
}
}
.height('100%')
.width('100%')
.padding(12)
}
- 对应安卓的权限管理
鸿蒙有ATM,ATM (AccessTokenManager) 是HarmonyOS上基于AccessToken构建的统一的应用权限管理能力。
- 对应安卓的SharedPreferences能力,鸿蒙有首选项能力。
这里就不一一列举了
我们只需要知道在安卓上有的概念,就可以在鸿蒙官方文档中去找一下对应的文档。
原理都是相通的。所以有过安卓开发经验的同学相对于前端FE来说有对客户端开发理解的优势。
要不要搞?
先看看目前的情况, 各家大厂正在积极布局鸿蒙客户端开发。
虽说移动端操作系统领域对安卓和iOS进行挑战的先例也有且还没有成功的先例。但是当前从国内互联网厂商的支持态度,从国际形势的情况,从华为对鸿蒙生态的投入来看。 我觉得很有搞头!
明年鸿蒙即将剔除对安卓的支持,届时头部互联网公司的大流量App也将完成鸿蒙原生纯血版的开发。
更有消息称鸿蒙PC版本也在路上了,了解信创的朋友应该能感受到这将意味着国产移动端和PC端操作系统会占有更大比例的市场。不仅仅是企业的市场行为,也是国产操作系统快速提升市占率的大好时机。
话说回来,作为安卓开发者,学习鸿蒙的成本并不高!
而对我们来说这是个机遇,毕竟技多不压身,企业在选取人才的时候往往也会偏好掌握更多技术的候选人。
如果鸿蒙起飞,你要不要考虑乘上这股东风呢?
我是张保罗,一个老安卓。最近在学鸿蒙
来源:juejin.cn/post/7308001278420320275
只会Vue的我,一入职就让用React,用了这个工具库,我依然高效
由于公司最近项目周期紧张,还有一个项目因为人手不够排不开,时间非常紧张,所以决定招一个人来。这不,经过一段时间紧张的招聘,终于招到了一个前端妹子。妹子也坦白过,自己干了3年,都是使用的Vue开发,自己挺高效的。但如果入职想用React的话,会稍微费点劲儿。我说,没事,来就是了,我们都可以教你的。
但入职后发现,这个妹子人家一点也不拖拉,干活很高效。单独分给她的项目,她比我们几个干的还快,每天下班准时就走了,任务按时完成。终于到了分享会了,组长让妹子准备准备,分享一下高效开发的秘诀。
1 初始化React项目
没想到妹子做事还挺认真,分享并没有准备个PPT什么的,而是直接拿着电脑,要给我们手动演示她的高效秘诀。而且是从初始化React项目开发的,这让我们很欣慰。
首先是初始化React项目的命令,这个相信大家都很熟悉了:
第一步:启动终端
第二步:npm install -g create-react-app
第三步:create-react-app js-tool-big-box-website
(注意:js-tool-big-box-website是我们要创建的那个项目名称)
第四步:cd js-tool-big-box-website
(注意:将目录切换到js-tool-big-box-website项目下)
第五步:npm start
然后启动成功后,可以看到这样的界面:
2 开始分享秘诀
妹子说,自己不管使用Vue,还是React,高效开发的秘诀就是 js-tool-big-box 这个前端JS库
首先需要安装一下: npm install js-tool-big-box
2.1 注册 - 邮箱和手机号验证
注册的时候,需要验证邮箱或者手机号,妹子问我们,大家平时怎么验证?我们说:不是有公共的正则验证呢,就是验证一下手机号和邮箱的格式呗,你应该在utils里加了公共方法了吧?或者是加到了表单验证里?
妹子摇摇头,说,用了js-tool-big-box工具库后,会省事很多,可以这样:
import logo from './logo.svg';
import './App.css';
import { matchBox } from 'js-tool-big-box';
function App() {
const email1 = '232322@qq.com';
const email2 = '232322qq.ff';
const emailResult1 = matchBox.email(email1);
const emailResult2 = matchBox.email(email2);
console.log('emailResult1验证结果:', emailResult1); // true
console.log('emailResult2验证结果:', emailResult2); // false
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
js-tool-big-box,使React开发更加高效
</header>
</div>
);
}
export default App;
2.2 验证密码强度值
验证密码强度值的时候呢,妹子问我们,大家平时怎么验证?我们说:不就是写个公共方法,判断必须大于几位,里面是否包含数字,字母,大写字母,特殊符号这样子吗?
妹子摇摇头,说,不是,我们可以这样来验证:
const pwd1 = '12345';
const pwd1Strength = matchBox.checkPasswordStrength(pwd1);
console.log('12345的密码强度值为:', pwd1Strength); // 0
const pwd2 = '123456';
const pwd2Strength = matchBox.checkPasswordStrength(pwd2);
console.log('123456的密码强度值为:', pwd2Strength); // 1
const pwd3 = '123456qwe';
const pwd3Strength = matchBox.checkPasswordStrength(pwd3);
console.log('123456qwe的密码强度值为:', pwd3Strength); // 2
const pwd4 = '123456qweABC';
const pwd4Strength = matchBox.checkPasswordStrength(pwd4);
console.log('123456qweABC的密码强度值为:', pwd4Strength); // 3
const pwd5 = '123@456qwe=ABC';
const pwd5Strength = matchBox.checkPasswordStrength(pwd5);
console.log('123@456qwe=ABC的密码强度值为:', pwd5Strength); // 4
2.3 登录后存localStorage
登录后,需要将一些用户名存到localStorage里,妹子问,我们平时怎么存?我们说:就是直接拿到服务端数据后,存呗。妹子问:你们加过期时间不?我们说:有时候需要加。写个公共方法,传入key值,传入value值,传个过期时间,大家不都是这样?
妹子摇摇头,说,不是,我们可以这样来存:
import { storeBox } from 'js-tool-big-box';
storeBox.setLocalstorage('today', '星期一', 1000*6);
2.4 需要判断是否手机端浏览器
我们市场需要判断浏览器是否是手机端H5浏览器的时候,大家都怎么做?我们说:就是用一些内核判断一下呗,写好方法,然后在展示之处判断一下,展示哪些组件?不是这样子吗?
妹子又问:我这个需求,老板比较重视微信内置的浏览器,这样大家写的方法是不是就比较多了?我们说,那再写方法,针对微信内置浏览器的内核做一下判断呗。
妹子摇摇头,说,那样得写多少方法啊,可以用这个方法,很全面的:
如果你单纯的只是想判断一下是否是手机端浏览器,可以这样:
import { browserBox } from 'js-tool-big-box';
const checkBrowser = browserBox.isMobileBrowser();
console.log('当前是手机端浏览器吗?', checkBrowser);
如果你需要更详细的,根据内核做一些判断,可以这样:
const info = browserBox.getBrowserInfo();
console.log('=-=-=', info);
这个getBrowserInfo方法,可以获取更详细的ua,浏览器名字,以及浏览器版本号
2.5 日期转换
妹子问,大家日常日期转换怎么做?如果服务端给的是一个时间戳的话?我们说:不就是引入一个js库,然后就开始使用呗?
妹子问:这次产品的要求是,年月日中间不是横岗,也不是冒号,竟然要求我显示这个符号 “~” ,也不是咋想的?然后我们问:你是不是获取了年月日,然后把年月日中间拼接上了这个符号呢?
妹子摇摇头,说,你可以这样:
import { timeBox } from 'js-tool-big-box';
const dateTime2 = timeBox.getFullDateTime(1719220131000, 'YYYY-MM-DD', '~');
console.log(dateTime2); // 2024~06~24
2.6 获取数据的详细类型
妹子问,大家日常获取数据的类型怎么获取?我们说,typeof呀,instanceof呀,或者是
Object.prototype.toString.call 一下呗,
妹子摇摇头,说,你可以这样:
import { dataBox } from 'js-tool-big-box';
const numValue = 42;
console.log('42的具体数据类型:', dataBox.getDataType(numValue)); // [object Number]
const strValue = 'hello';
console.log('hello的具体数据类型:', dataBox.getDataType(strValue)); // [object String]
const booleanValue = true;
console.log('true的具体数据类型:', dataBox.getDataType(booleanValue)); // [object Boolean]
const undefinedValue = undefined;
console.log('undefined的具体数据类型:', dataBox.getDataType(undefinedValue)); // [object Undefined]
const nullValue = null;
console.log('null的具体数据类型:', dataBox.getDataType(nullValue)); // [object Null]
const objValue = {};
console.log('{}的具体数据类型:', dataBox.getDataType(objValue)); // [object Object]
const arrayValue = [];
console.log('[]的具体数据类型:', dataBox.getDataType(arrayValue)); // [object Array]
const functionValue = function(){};
console.log('function的具体数据类型:', dataBox.getDataType(functionValue)); // [object Function]
const dateValue = new Date();
console.log('date的具体数据类型:', dataBox.getDataType(dateValue)); // [object Date]
const regExpValue = /regex/;
console.log('regex的具体数据类型:', dataBox.getDataType(regExpValue)); // [object RegExp]
2.8 更多
估计妹子也是摇头摇的有点累了,后来演示的就快起来了,我后来也没听得太仔细,大概有,
比如我们做懒加载的时候,判断某个元素是否在可视范围内;
比如判断浏览器向上滚动还是向下滚动,距离底部和顶部的距离;
比如某个页面,需要根据列表下载一个excel文件啦;
比如生成一个UUID啦;
比如后面还有将小写数字转为大写中文啦,等等等等
3 最后
分享完了第二天,妹子就没来,我们还准备请教她具体js-tool-big-box的使用心得呢。据说是第一天分享的时候,摇头摇得把脖子扭到了,希望妹子能早日康复,早点来上班。
最后告诉你个消息:
js-tool-big-box的npm地址是(js-tool-big-box 的npm地址)
js-tool-big-box的git仓库地址(js-tool-big-box的代码仓库地址)
来源:juejin.cn/post/7383650248265465867
绑定大量的的v-model,导致页面卡顿的解决方案
绑定大量的的v-model,导致页面卡顿的解决方案
设计图如下:
页面布局看着很简单使用element组件,那就完蛋了,因为是大量的数据双向绑定,所以使用组件,延迟非常高,高到什么程度,请求100条数据到渲染到页面上,要10-12s,特别是下拉选择的时候,延迟都在2-3s,人麻了老铁!!!
卡顿的原因很长一段时间都是在绑定v-model,为什么绑定v-model会很卡呢,请求到的每一条数据有14个数据需要绑定v-model,每次一请求就是100个打底,那就是1400个数据需要绑定v-model;而且组件本身也有延迟,所以这个方案不能采用,那怎么做呢?
我尝试采用原生去写,写着写着,哎解决了!!!惊呆了
做完后100条数据页面渲染不超过2s,毕竟还是需要绑定v-model,能在2s内,我还是能接受的吧;选择和输入延迟基本没有
下面就来展示一下我的代码,写的不好看着玩儿就好了:
请求到的数据:
methods这两个事件做的什么事儿呢,就是手动将数据绑定到数据上去也就是row上如图:
当然还有很多解决方案
来源:juejin.cn/post/7392248233222881316
那些年,我在职场中做过的蠢事
大家好,我是程序员马晓博,目前从事前端行业已有5年有余,而近期由于被裁员有一段时间,也开始回顾自己的过往,发现自己以前在职场中,做过不少傻事,这里就写篇文章来记录下。曾经的犯傻已不可避免,索性公之于众,坦然面对。
ps: 像一直戴工牌,故意露个工牌带子在外面就不说了,谁还没年轻过呢?还真谁也别笑话谁哈哈。
自诩正义强出头
这是我在腾讯实习的时候遇到的,故事很简单,有个同事加了一个全局错误捕获的逻辑,导致原本有报错但是能够正常运行的程序,出现了线上 bug。
此时团队间要追究责任,认为是加了全局捕获错误的同事的责任。从现在的视角看,加了全局错误捕获同事自然是有问题的,但是当时的我,非常正义的认为,写 bug 的人才应该承担责任,而为了让代码更健壮写了全局错误捕获的同事是没有错的。
现在回想起当时自己义正严辞的发言,真是太年轻啦。
更何况这事我连当事人都算不上,只能安慰自己:谁还没年轻过呢?
平易近领导
这个事,也是发生在腾讯实习期间。
在来实习之前的我,深受互联网扁平化管理,工位不做区别,这些非常先进的思想影响,到了腾讯之后,听领导说,自己平时喜欢游泳,我一想我也喜欢啊,就直接跑领导工位上问,你平时在哪里游泳?
吓的我的直属上级,直接跑出来拉住我,我给你推荐,我带你游泳。
还有一次是,午休一起打王者荣耀,人多了把我空出来了,我就指导领导玩亚瑟,上来就说你这出装太肉了,没一点伤害,要怎么怎么玩。
我现在还能想起来他当时的眼神,你,是在教我做事?
对于领导,毫无距离感。这应该是很多年轻人都会有的心态,还会认为这就是年轻人的本色,是互联网的特色,而且会对奉承领导的人嗤之以鼻。
当然,互联网公司文化本身也都提倡这种,没有上下级,大家都能上。也就是倡导所谓扁平化管理,工位也是领导普通员工没区别。
曾经的我认为这一切都是问题不大的,就是扁平化,互联网就是不一样,但是透过一些其他行业的人,我虽不介意工位情况,但是难免对其底层所宣扬的扁平化产生一定的怀疑。
ps: 这里放一个其他行业的人的对互联网工位的一些看法,我妹妹(中国移动打过一段时间工)来参观了我的工位之后说了一句话:这是工位?牛马间吧这是,我不以为然。直到我看到了她的工位,差别还真不是一般的大,好歹有隔间。这里应该放一张图,但是我没有,大家自行脑补吧哈哈。
自诩性格真诚直率
可谓是初生牛犊不怕虎吧,看到认为拉垮的代码,就会找同事当面聊,应该怎么怎么样,不应该怎么怎么写。
但是实际上,代码写成你认为的不合理的样子,往往是很多因素导致的,或工期,或对方当时也是初学者,或团队风格,或当时环境,或仅仅是对方对新方案的尝试。
上下文不了解,就开始吐槽。但是实际上人家的代码线上运行毕竟没有问题,没有故障的代码,本身就是一份合格的代码以及对方能力的认证。
而我的当面不友好交流,真是一点礼貌没有。还美名其曰,性格比较直率真诚。
好在当时的同事比较友好,并未计较,还在我后来选房子的时候还提供了很大的帮助,可叹没有仔细聆听对方的教诲终究还是没有十全十美。
夜郎自大而不自知
这是在我工作两年左右时候产生的一种感觉,觉得自己完成业务没有任何压力,而且还承担了一些比较重要的工作,从而有一种觉得自己很行的错觉。
但是当时面试很快就泼了一盆冷水,一般来讲,这个阶段做业务的同学,应对业务开发其实基本都没有什么问题。
但是国内对程序员的面试根本不限于业务,深挖一些知识点,理解其原理才是及格线。
而当时的我就是一直停留在使用阶段,用好本身没有问题,但是奈何不足以应对面试。
当然,心态还是最重要的。半瓶水晃荡而不自知才是最可怕的。
开弓来个回头箭
这个事,说实话有点羞于启齿。
是我工作大概第四个年头发生的,那时我在网易工作有一年多一些。
由于自己做了一个还算比较有技术难度的项目,想要寻求晋升,结果当时的晋升期答辩都结束了自己还不知晓。
心里有闷气,就开始面试找工作,也顺利拿到了几个涨幅非常不错的 offer。
开始跟上级提离职,哈哈,对方聊了下,也答应了。
结果我自己晚上就是睡不着,始终觉得自己这个时候走,是逃避,是逃兵。而且这个时候走,之前的积累就全部白费,新公司还得从零做起。
网上都说开弓没有回头箭。但是我就还是厚着脸皮来个回头箭。
不得不说,这个决定并不算蠢事,我在整件事里最蠢的是没有想好就和上级提了离职,虽然拿了 offer,但是没有想清楚就离职,是非常不成熟的表现。
好在我的上级,也主打一个真诚,也明确说明,想清楚了就行。
接下来一年的合作非常愉快,既有可视化埋点平台这样的业务技术都有挑战的项目,也有团队状态管理方案的产出,顺利在第二年迎来了自己的晋升。
这一次,愚蠢更多是在于自己没有想清楚就开弓,而真诚待人在我看来是双向必杀技,但真诚也为我后来吃亏埋下了种子。
整体而言,在网易的几年,领导,同事,大家都比较真诚,不屑于暗地里去做一些掉份的事情,也让我在职场上,形成了真诚而缺少防范的一个问题,这在我的下一步职业生涯中,给我带来了比较大的打击和跟头。
和同事交往讲真诚
这是我在离开网易后,选择的一家规模比较小的公司。
这时候,我工作已经整整 5年了,但是我过往的经历终究让我缺少了一些对同事的防范,大公司还好,大家相互之间,利益冲突不大,更多的是合作关系,同时由于大家或多或少都有自己的一点点的"骄傲",所以其实并没有遇到一些因为利益冲突而导致的暗箭。
而过往的经历也在告诉我,真诚,并不会带来什么问题。
真诚无错,但是说者无意听者有心。
到新公司之后,也到了该带人的职级,此时,我还是主打真诚,很快就和团队融为一片。
几个关系近的同事和下属,知道我家里买了几套房,知道我平时看的书,知道我平时都在干啥,知道我对生活和工作的态度,知道我在工作上的安排。
这些事情,平时没有什么问题,但是当和有心的同事出现利益冲突的时候,这些事情就成为一把利剑,间接导致我失去了这份工作。
而这些利剑,是我亲手递给了对方。
对职场恶意的容忍
如果说真诚是给别人递了一把利剑,那么自己的容忍和锋芒的隐藏,是我自己收起了盾牌。
我在周围的同事身上,总能看到自己的影子,所以对于他们的恶意,往往有一定程度的容忍,我觉得,年轻人嘛,有点锋芒,很正常。
比如,当他们吹嘘自己写了一篇文章,获得了几个赞的时候,我往往是进行倾听并表示赞赏,虽然几个赞的文章其实真的很简单。又或者公开场合提出质疑,虽然我会讲道理,理可以辩明,但是对于这其中的恶意,我一般会选择包容。
但是就是这一步,自身锋芒的隐藏,在对方眼里却是得寸进尺的机会。
个人觉得,作为级别比对方高的,还是需要适时的漏出自身的锋芒,而不是仅仅倾听加赞赏,同时由于私下交往的密集,更导致对方的肆无忌惮。
从而亲手递给对方利剑,又自己收起盾牌。
只能说,在这条路上,我还是太稚嫩。
最后
以上,就是我个人认为在职场中,做过的一些蠢事。虽然已经工作了五年之久,但是这条路上,还是觉得太过稚嫩,谨以此文,纪念哪些蠢事!
ps: 不知道看完这篇的你,有没有回忆起一些类似的事情呢?欢迎交流哈。
// 还是那句话,都年轻过,谁也别笑话谁~
来源:juejin.cn/post/7357994849386102836
从20k到50k再到2k,聊聊我在互联网干前端的这几年
大家好,我是程序员晓博,目前从事前端行业已经有将近 6年。这六年,从最初的互联网鼎盛时期,到今年是未来十年内最好的一年,再到疫情时期的回暖,再到如今年年都喊寒冬的寒冬。从最初的 20k,到最近的一份 50k 的工作,再到如今的 "政府补贴" 2k,可谓是感悟颇多。
刚好最近 gap 一段时间,有所空闲,就整理下这几年的经历以及我所看到的行业的兴衰。
学生见闻
我是 2011 年读的大学,当时是电子科学与技术这个专业,并非计算机科班出身,更偏向于硬件编程,单片机,嵌入式,FPGA 这些会更多一些。
所以当时对于互联网行业的前端后端,并没有特别明确的概念,也对于 c++, java 这些语言的地位和适用性其实也没有明确的认知。
记得是 2012, 2013 年的时候,学校里经常有 java 培训班的宣传,说实话,那会还看不上 java,虽然自己也不会,但是学校里教的都是 c, c++, java 那会在我看来,更多的是一个 c++ 简化后的语言,所以对于我这个非科班的并没有提起兴趣。
现在回想起来,那时可真是入行的好时期,当然也是风云变换的几年。那会学校流传着一个段子: 你只要会安装 eclipse,就能找到一份美团的工作。而之后的一年,你得开发过自己的 app,才能找到安卓开发工作。
不过当时更多的观念告诉我至少得读个研究生出来,所以我选择了读研而非直接工作。可以说是错过了互联网飞速发展的黄金时期,直接毕业就来到了今年是未来十年里最好的一年。
也就是在研究生阶段,我才慢慢了解到,外界的互联网大厂,其实已经分化出了移动端,前端,后端这样的岗位,当时的前端圈最为活跃,而移动端,后端,似乎都已经定型。而前端圈的新框架此起彼伏,从 react, vue, webpack,还有很多已经消失在历史中的框架。
在当时的就业情况下,前端的工资似乎是最高的,在加上当时的前端圈确实很活跃,而学习起来也比较简单。作为非科班的学生,自学前端上手最快,所以我选择了前端作为自己的就业方向。专业对口的硬件开发就不说了,工资实在是大相径庭。
但是话说回来,硬件开发如今的热门程度,并不亚于当时的软件开发。硬件开发培训,挑战 30w 年薪这样的培训班,在 2022 年左右也出现了,一如当时 2015 年左右 java 开发包就业那样的火热。
不过这个专业,其实还给我带来了一份现在看起来可以称为副业的东西:代写课程设计和毕业设计,因为是比互联网前后端更细分的赛道,所以竞争并不激烈,我还是接到了不少的单子,但是由于自己本身也是学生,所以定价很低,基本按照 100/h 的费用在收,也有做代码复用的整合,但是在硬件这一块,它的售后并不像软件这样,往往需要花费时间帮用户在板子上走通,这一部分是比较花费时间的。也在研究生阶段尝试过转项目的方式来获取收益,但是由于定价过低以及单子并不多的问题,而没有继续。不过如今想来,借助 chatgpt 等 AI 工具,定价确实还能更低(尤其是包含论文的单子)。
第一份实习
出于提升自身竞争力的考虑,我在研究生阶段就开始了边自学,边找实习。好在自学的时间比较早,准备的也比较充分,顺利拿到了腾讯等几家公司的实习。
当时虽说还处于互联网发展的时期,但是竞争其实就已经比较激烈了,没有实习进大厂基本就是 hard 模式,我也是面试了 n 家公司,才拿到的 offer。
不过腾讯这个部门虽然在面试的时候,会问一些比较现代技术的问题,但是实际进去后,是写的 php 和 jquery。我的收获其实并不多,但是简历上好看一点,后来也顺利拿到了转正的 offer。
当时给的薪资应该是 16k 左右,还会加上城市的补贴大概 2k。不过最终因为房价的原因,并没有考虑留在深圳。
ps:我在实习的时候专门考察了深圳腾讯总部附近的房价,好像是 6w,确认过眼神,是掏光 6 个钱包也买不起的房子。不过据说一度涨到了 10w,现在没有再关注了,可能有所下跌吧。
说起来当时还有一个事让我印象比较深刻,也因此对阿里有了一些抵触。就是 2017:众多应届生被阿里毁了 offer。而对于这种事情,阿里给的解释是:拥抱变化。
可能马爸爸从那时就嗅到了危机,但这却是我第一次听说毁应届生 offer,非常败好感。ps: 现在这种毁应届生 offer 的事是非常常见啦。
似乎那时警钟已经敲响,但是我并没有未雨绸缪。
第一份工作
我是 2018 年毕业的,那会北京,上海,都有落户的限制,甚至还有一些积分制等似乎不欢迎应届生去上班的感觉。
那会刚毕业,可谓是心比天高,落个户都这么麻烦,我还不想去呢!还不如人深圳的口号,来了就是深圳人。而当时阿里的总部,就在杭州,而且杭州只要是大学生,立马就能落户,立马能摇号买房。而当时房价也比较亲民 (确认过眼神,是掏光钱包可以买得起的价格)。
所以基本上只找杭州的工作。最终入职了当时比较热门的 p2p 领域的独角兽,51 信用卡。
当时的 51信用卡,可以说是 p2p 领域的一只牛逼独角兽,甚至这家公司的缩写就是 51NB。
不过以我当时的认知,入职 51信用卡,纯粹是因为 20k 的薪资,以及全额报销来回路费。要知道当时 BAT 虽然有报销,但是实际上都有各种限制和上限。
ps: 以我当时的认知,几乎没有任何犹豫,我就关闭了我的副业通道,因为我觉得,精进前端技术,带来的收益更大,毕竟一个月 20k 的收入,更别提还有 4个月加的年终收入了。而这份副业,一方面对主业没有提升,同时还要消耗比较大的精力(主要集中在给学生讲解代码以及售后上),收入也就几千块而且时间比较集中,很难兼顾。
现在回过头来看,真的是误打误撞赶上了 p2p 行业的末班车。起始薪资确实不错,但是很快就来到了国家严控 p2p 行业的开端。
最终,入职当年,就遇到了一波一波的裁员,从开始的 n + 3,到 n + 2 再到 n + 1,可谓是一波一波的裁员。也包括了应届生。一如现在,应届生也还是裁员重灾区。
也因为 51 当时是杭州互联网第一波开启的裁员,还裁了应届生。口碑急转直下,但是很快就迎来了反转,隔壁滴滴,微店等也迅速开启裁员模式,仅仅只有 n + 1。
ps: 在 51 的第一年,是有年会的。第二年,年会倒是有,但是主题就是一句话,今年将是接下来十年内,最好的一年。也是这一年 2019,p2p 彻底宣告结束,51 也出现了警车上门的事件。最终借贷业务转型为依赖于银行的借贷业务。也结束了接近 10% 的储蓄利率时代。
短暂的阿里之旅
2020 年年初,p2p 行业宣告结束叠加疫情之初,悲观情绪四处蔓延。我在 51 的旅程也渐渐走到了尾声。
当时面试了字节和阿里。彼时的字节跳动,在杭州名气和规模还没有如今这么大。权衡之下选择了名气更盛,当时口碑更好的阿里。但是我对于字节的判断,实在是偏差的离谱,看着如今蒸蒸日上的字节,真是后悔莫及。
但是进了阿里,说实话是真有些不适应。
一方面生活上,不提供纸巾,让我颇为诧异,而时不时的团队聚餐竟然是 AA 也让我非常不适应。
当然,对方看我也很奇怪,说了一句话,感觉你是外企来的 (ps: 如今的 51信用卡还是有点小而美的感觉,各项业务依托也还在继续,也有露营等新业务的开拓,老板自由后也还在继续折腾着)。
因为疫情的原因,我并没有经历百阿培训。但是有一本小册子,写着价值观。让我印象深刻的是,"此时此刻,非我莫属" 和 "不难,要你做什么"。
这两句话听起来都没有什么问题,鼓励人奋进并没有任何问题,但是以前的奋斗,伴随着可能的巨大的回报,而我当时的付出与回报,显然已经是大打折扣了。
那时还没有 pua 的说法,但是确实有一些话让我觉得不舒服,比如目标要跳起来才是 3.5,蹦起来才是 3.75,以及业务好和你一点关系都没有,你把业务做好了,也只能给你 3.25。必须得出一些技术项目,才能拿到好绩效。
而那种没有人会点明但是大家都在执行的道理:和我 kpi 有关的就是天,无关的就是已读不回,而已读不回,就是拒绝。更是让当时还非常稚嫩的我想要逃之夭夭。
这些也不能说错,但是这确实和我在 5信用卡当时围着业务转的风格大相径庭。
说回技术,阿里整个集团的基建可以说非常好,反而我所在的这个小前端团队的基建,赶不上 51前端团队的基建。
不论是脚手架,发布系统(比较让我震惊的是我当时团队的发布是自己丢文件到服务器上,测试正式环境的区分还是靠手动维护的一份文件),开发流程,完全赶不上 51当时的丝滑程度。
可以说是对压力的逃避,也可以说是对这种环境的不适应,也可以说是对涨幅的不满,我很快就开启了下一段旅程。
现在回过头来看,当时离职,还是冲动占了大部分。一方面,随着后来业务接触的多了,能够理解当时那个小团队的基建差的原因:主要是业务形态,当时的小团队是 toB 的,要维护的仅仅是一个项目,自然在发布流程上的投入不会太多,而且收益也是远远不及 51 这种移动端几十上百个工程的发布工作来的实在。
而另一方面,所谓的深夜开会,不明说但是心里都清楚的加班氛围,以及唯 kpi 导向的风气,其实也不过是一种生存规则而已。强行说服自己接受也很容易。毕竟人生如戏,适应规则,利用规则,掌握规则,但凡能够想通这一点,当时坚持下来也非常容易。
长达三年的网易之旅
当处于阿里的水深火热之中时,一个在周末就完成了全部流程的网易团队,向我抛出了橄榄枝。
经过短暂的调整,我也就入职了这个团队,不曾想一待就是三年。此时的薪资来到了 30k 左右,但是由于当时这个团队独特的奖金制,月收入会比 base 高出不少。
团队的业务主要是直播,所以 toB 和 toC 的业务都有。基建上也比较完善,发布系统,组件库,脚手架,微前端,等等,相对更为繁荣。
这个团队并没有明确的技术项目的考核,还是以业务为主,大多数人,完成业务开发目标,就能够顺利拿到 3.5 的绩效,同时由于当时直播行业的繁荣,基本都会有一笔不菲的奖金。而技术项目属于锦上添花,确确实实能在最终的绩效上有所体现但是并不多。
但是恰恰是在这样的环境下,组内的同学在相对宽松的氛围下,更热衷于鼓捣技术项目,反而平时对技术的研究及讨论会更多一些。
这三年,也算是见证了业务的兴衰,从开始的营收暴涨开始出海,到最终营收暴跌收缩,到裁员。也不过短短三年。
现在回头来看,在这三年里,对于业务的了解更多的还是停留在表层,虽然当时觉得自己理解业务方的需求了,但是其实内部的很多玩法还是远非仅仅理解需求就能接触到的,什么大 R 运营,"军火商" 等等秀场直播的黑话,我是没有学到一点。
由于组内业务还算比较综合,c端页面的开发,b端后台都有所接触。同时业务之余还会有很多时间去做一些技术项目,比如我负责的 CloudIDE, WebIDE, 可视化埋点项目, 基于 zustand 的状态管理库, 均是这一时期的产物。
整体来讲,这三年不论是工作节奏,还是技术产出,都还算可以。
但是如今回过头来看,似乎这三年,对外界的关注,基本上有了一定的钝感,不像之前,对互联网的各个信息都会去了解看一下。反而这几年,说内敛沉稳也好,说闭门造车也好,说停留在自己的舒适圈内也好,除了技术层面的精进,对于整个行业的发展,都太过闭塞,仿佛只是重复一种舒适的生活过了三年:每天和老婆一起轮流开车上下班,顺便再健个身,住着自己的房子,还着公积金就能覆盖还有结余的带款。
如今回想起来,也正是这三年的经历,让我在技术上有所精进,但是对互联网行业的关注,反而有所下降。同时由于同事间的关系比较简单,也让我在人际交往上变得更加朴素真诚。
半年的小公司之旅
怎么说呢,好像人总是在不稳定的时候追求稳定,在稳定的时候追求不稳定。
所以在结束了网易的三年相对稳定的工作之后,我内心反而变得很躁动,想要去小公司,谋一番事业。
出来看机会之后,才发现外界的环境其实并没有平时了解的那么糟糕,确实不像之前机会那么多,但是确实也还有一些岗位。
在这之中,我选择了在发展业务第二曲线同时又有第一业务支持的说稳定又不稳定的公司 ---- 爱普拉维。
这家公司业务主要集中在海外,所以整体业务情况也还是非常客观。给出的薪资也比较客观,我的薪资也在这一时期,达到了 50k 左右。
不过入职之初,就经历了一些人事变动,如今想来,可以说是警醒,但是我应该是选择性的进行了忽视。心思沉浸在技术和一点点的管理上。
这个团队前端同学并不多,但是业务上除了常规的 h5 和少量的后台项目之外,还会存在一些 chrome 扩展逆向,爬虫项目的存在,而我被招进来的主要任务,也就是 chrome 扩展的逆向和爬虫项目。
在这一期间,我一度沉浸在了技术上的钻研中,从 webpack 的解码逆向,到 puppeteer 爬虫的实现,从 项目秒开的优化,到 svelte 的重构,都是对我之前技术经验的一个补充。也顺利在技术角度上在公司站稳了脚跟。度过了这个公司网上传言的不好过的试用期。
不过终归还是在人际交往上有所欠缺,叠加上公司的业务方向调整,导致了最终今年 1月份的离职。而这,也为我的职场画下了短暂的暂停键。
离职快小半年了
不知不觉离离职已经快小半年了,也顺利领到了失业金,也就是题目中提到的 2k。
这段时间从刚开始的玩乐,到中途的读书写文章,再到一些副业(对于无业人员来讲应该是主业)的探索。焦虑在所难免,未来也还比较迷茫,而其他主业的探索,说实话也没探索出来什么结果。
反倒是这段读书的时间给了我一些收获,一方面是 《穷爸爸富爸爸》中对于资产负债表的解释,我自己也做了一份,还参加了财富流沙盘游戏,对自己的财务状况有了更好的认知。另一方面便是 《认知觉醒》中关于焦虑的说法,一定程度上命中了当下的自己很多。
最后,就用《认知觉醒》中关于焦虑的根源来结束这篇文章吧:想同时做很多事,又想立即看到效果。自己的欲望大于能力,又极度缺乏耐心。人的天性就是避难驱易和急于求成。
ps: 避难驱易,这几个字实在太戳我了,也正是因为避难驱易,所以其实很多之前就想写的文章都拖拖拖,直到认识到是内心的避难驱易之后才开始控制自己开始输出,而也正是输出才让我注意到了自己之前没有注意到的点,才有了这篇文章以及 那些年,我在职场中做过的蠢事。
最后的最后,愿我们都有美好的未来!
来源:juejin.cn/post/7366567675315126281
uni-app 集成推送
研究了几天,终于是打通了uni-app的推送,本文主要针对的是App端的推送开发过程,分为在线推送和离线推送。我们使用uni-app官方推荐的uni-push2.0。官方文档
准备工作:开通uni-push功能
- 勾选uniPush2.0
- 点击"配置"
- 填写表单
关联服务空间说明:
uni-push2.0需要开发者开通uniCloud。不管您的业务服务器是否使用uniCloud,但实现推送,就要使用uniCloud服务器。
- 如果您的后台业务使用uniCloud开发,那理解比较简单。
- 如果您的后台业务没有使用uniCloud,那么也需要在uni-app项目中创建uniCloud环境。在uniCloud中写推送逻辑,暴露一个接口,再由业务后端调用这个推送接口。
在线推送
以上操作配置好了以后,回到HBuilderX。
因为上面修改了manifest.json配置,一定要重新进行一次云打包(打自定义调试基座和打正式包都可以)后才会生效。
客户端代码
我这边后端使用的是传统服务器,未使用云开发。要实现推送,首先需要拿到一个客户端的唯一标识,使用uni.getPushClientId API
链接地址
onLaunch() {
uni.getPushClientId({
success: (res) => {
let push_clientid = res.cid
console.log('客户端推送标识:', push_clientid)
// 保存在全局,可以在进入app登录账号后调用一次接口将设备id传给后端
this.$options.globalData.pushClientId = push_clientid
// 一进来就掉一次接口把push_clientid传给后端
this.$setPushClientId(push_clientid).then(res => {
console.log('[ set pushClientId res ] >', res)
})
},
fail(err) {
console.log(err)
}
})
}
客户端监听推送消息
监听推送消息的代码,需要在收到推送消息之前被执行。所以应当写在应用一启动就会触发的应用生命周期
onLaunch
中。
//文件路径:项目根目录/App.vue
export default {
onLaunch: function() {
console.log('App Launch')
uni.onPushMessage((res) => {
console.log("收到推送消息:",res) //监听推送消息
})
},
onShow: function() {
console.log('App Show')
},
onHide: function() {
console.log('App Hide')
}
}
服务端代码
- 鼠标右击项目根目录,依次执行
- 然后右击uniCloud目录,选择刚开始创建的云服务空间
- 在cloudfunctions目录右击,新建云函数/云对象,命名为uni-push,会创建一个uni-push目录
- 右击uni-push目录,点击 管理公共模块或扩展库依赖,选择uni-cloud-push
- 右击database目录,新建DB Schema,创建这三张表:
opendb-tempdata
,opendb-device
,uni-id-device
,也就是json文件,直接输入并选择相应的模板。
- 修改index.js
'use strict';
const uniPush = uniCloud.getPushManager({appId:"__UNI__XXXX"}) //注意这里需要传入你的应用appId
exports.main = async (event, context) => {
console.log('event ===> ', event)
console.log('context ===> ', context)
// 所有要传的参数,都在业务服务器调用此接口时传入
const data = JSON.parse(event.body || '{}')
console.log('params ===> ', data)
return await uniPush.sendMessage(data)
};
- package.json
{
"name": "uni-push",
"dependencies": {},
"main": "index.js",
"extensions": {
"uni-cloud-push": {}
}
}
- 右击uni-push目录,点击上传部署
- 云函数url化
登录云函数控制台,进入云函数详情
8. postman测试一下接口
没问题的话,客户端将会打印“console.log("收到推送消息:", xxx)”,这一步最好是使用真机,运行到App基座,使用自定义调试基座运行,会在HBuilderX控制台打印。
离线推送
APP离线时,客户端收到通知会自动在通知栏创建消息,实现离线推送需要配置厂商参数。
苹果需要专用的推送证书,创建证书参考链接
安卓需要在各厂商开发者后台获取参数,参考链接
参数配置好了以后,再次在postman测试
注意 安卓需要退出app后,在任务管理器彻底清除进程,才会走离线推送
解决离线推送没有声音
这个是因为各安卓厂商为了避免开发者滥用推送进行的限制,因此需要设置离线推送渠道,查看文档
调接口时需要传一个channel参数
实现离线推送自定义铃声
这个功能只有华为和小米支持
也需要设置channel参数,并使用原生插件,插件地址
注意 使用了原生插件,一定要重新进行一次云打包
- 华为,申请了自分类权益即可
- 小米,在申请渠道时,选择系统铃声,url为
android.resource://安卓包名/raw/铃声文件名(不要带后缀)
来源:juejin.cn/post/7267417057451573304
无框架,跨框架!时隔两年,哈啰Quark Design迎来重大特性升级!
引言
历经1年多迭代,Quarkd 2.0 版本正式发布,这是自 Quarkd 开源以来第二个重大版本。本次升级主要实现了组件外部可以穿透影子Dom,修改组件内部元素的任何样式。
- (迁移后)最新官网:quark-ecosystem.github.io/quarkd-docs
- Github 地址:github.com/hellof2e/qu…
Quark Design 介绍
Quark(夸克) Design 是由哈啰平台 UED 和增长&电商前端团队联合打造的一套面向移动端的跨框架 UI 组件库。与业界第三方组件库不一样,Quark Design 底层基于 Web Components 实现,它能做到一套代码,同时运行在各类前端框架/无框架中。
前端各类框架技术发展多年,很多公司存量前端项目中必定存在各类技术栈。为了解决各类不同技术栈下UI交互统一,我们开发了这套UI组件库。
之前技术瓶颈
熟悉 quarkd 的开发者都知道其底层基因是 Web Components,从而实现了跨技术栈使用。但Web Components 中的 shadow dom 特性决定了其“孤岛”的特性,组件内部是个独立于外部的小世界,外部无法修改组件内部样式,若要修改内部样式,我们在 quarkd 1.x 版本中采用了 CSS 变量的方式来支援这种做法。
但这种做法依旧局限性非常大,你只能修改预设css变量的指定样式,比如你要修改 Dialog 内容中的字体大小/颜色:
// 使用组件
<quark-dialog class=“dialog” content="生命远不止连轴转和忙到极限,人类的体验远比这辽阔、丰富得多。"></quark-dialog>
// 内部css源码
:host .quark-dialog-content {
font-size: var(--dialog-content-font-size, 14px);
color: var(--dialog-content-color, "#5A6066");
// ... 其它样式
}
这时候,你需要在组件外部书写:
.dialog {
--dialog-content-font-size: 36px;
--dialog-content-color: red;
}
这种做法会带来一些问题,比如当源码中没有指定的css变量,就意味着你无法通过css变量从外面渗透进入组件内部去修改,比如 dialog conent 内的 font-style
。
升级后
得益于 ::part
CSS 伪元素的特性, 我们将 Quarkd 主要 dom 节点进行改造,升级后,你可以通过如下方式来自定义任何组件样式。
custom-element::part(foo) {
/* 样式作用于 `foo` 部分 */
}
::part
可以用来表示在阴影树中任何匹配 part
属性的元素。
该特性已兼容主流浏览器,详情见:mozilla.org # ::part()
用法示例:
// 使用组件
<quark-dialog class=“dialog” content="生命远不止连轴转和忙到极限,人类的体验远比这辽阔、丰富得多。"></quark-dialog>
.dialog::part(body) {
font-size: 24px;
color: #666;
}
.dialog::part(footer) {
font-size: 14px;
color: #333;
}
其它DEMO地址:stackblitz.com/edit/quarkd…
关于升级
Quarkd 2.x 向下兼容所有 1.x 功能及特性,之前的css变量也被保留,所以使用者可以从1.x直接升级到2.x!
One more thing
假如你也想利用 quarkd 底层能力构建属于自己的跨技术栈组件,欢迎使用:
github.com/hellof2e/qu…
最后
感谢在Quarkd迭代期间作出贡献的朋友们,感谢所有使用quarkd的开发者!
来源:juejin.cn/post/7391753478123864091
zero-privacy——uniapp小程序隐私协议弹窗组件
一. 引言
为规范开发者的用户个人信息处理行为,保障用户的合法权益,自2023年9月15日起,对于涉及处理用户个人信息的小程序开发者,微信要求,仅当开发者主动向平台同步用户已阅读并同意了小程序的隐私保护指引等信息处理规则后,方可调用微信提供的隐私接口。
公告地址:关于小程序隐私保护指引设置的公告
developers.weixin.qq.com/miniprogram…
接下来我们将打造一个保姆级的隐私协议弹窗组件
二. 开发调试基础
划重点,看文档,别说为什么没有效果,没有弹窗
1. 更新用户隐私保护指引
小程序管理员或开发者可以根据具体小程序涉及到的隐私相关接口来更新微信小程序后台的用户隐私保护指引,更新并审核通过后就可以进行相关的开发调试工作。仅有在指引中声明所处理的用户信息,才可以调用平台提供的对应接口或组件。若未声明,对应接口或组件将直接禁用。
- ���知道怎么填写隐私协议,看看文档:用户隐私保护指引设置developers.weixin.qq.com/miniprogram…
- 哪些api需要用户点击同意隐私协议才可以使用的看这里:小程序用户隐私保护指引内容介绍developers.weixin.qq.com/miniprogram…
审核时间有人说十几分钟,我自己的给大家参考一下。
审核通过!审核通过!审核通过后才可以开发调试。
2.配置调试字段 "__usePrivacyCheck__": true
- 在 2023 年 9 月 15 号之前,在 app.json 中配置
"__usePrivacyCheck__": true
后,会启用隐私相关功能,如果不配置或者配置为 false 则不会启用。 - 在 2023 年 9 月 15 号之后,不论 app.json 中是否有配置 usePrivacyCheck,隐私相关功能都会启用。
- 所以在基于uni-app开发时,我们在 2023 年 9 月 15 号之前进行相关开发调试则需要在manifest.json文件mp-weixin中添加
"__usePrivacyCheck__": true
- manifest.json文件源码视图
"mp-weixin" : {
"__usePrivacyCheck__": true
},
3. 配置微信开发工具基础库
将调试基础库改为3.0.0以上。具体路径为:
微信开发者工具->详情->本地设置->调试基础库
以上配置完成后,即可看看效果,我在小程序后台设置了剪切板的隐私接口,果然,已经提示没有隐私授权不能使用了。
三. zero-privacy组件介绍
组件下载地址:ext.dcloud.net.cn/plugin?name…
组件的功能和特点
- 支持 居中弹出,底部弹出
- 不依赖第三方弹窗组件,内置轻量动画效果
- 支持自定义触发条件
- 支持自定义主题色
- 组件中最重要的4个api(只需用到前3个):
- wx.getPrivacySetting 查询隐私授权情况 官方链接
- wx.onNeedPrivacyAuthorization 监听隐私接口需要用户授权事件。 官方链接
- wx.openPrivacyContract 跳转至隐私协议页面 官方链接
- wx.requirePrivacyAuthorize 模拟隐私接口调用,并触发隐私弹窗逻辑 官方链接
四. zero-privacy组件使用方法
在uniapp插件市场直接下载导入 uni_modules
后使用即可
- 最直接看到弹窗效果的测试方法
<template>
<view class="container">
<zero-privacy :onNeed='false'></zero-privacy>
</view>
</template>
注意以上是测试方案,不建议实际开发中按上面的方法使用,推荐以下两种方法
- 在小程序首页等tabbar页面直接处理隐私弹窗逻辑
<template>
<view class="container">
<zero-privacy :onNeed='false' :hideTabBar='true'></zero-privacy>
</view>
</template>
- 在页面点击某些需要用到隐私协议后处理隐私弹窗逻辑
<template>
<view class="container">
<view class="btn" @click="handleCopy">
复制
</view>
<zero-privacy></zero-privacy>
</view>
</template>
- 自定义内容使用
<template>
<view class="container">
<zero-privacy title="测试自定义标题" predesc="协议前内容" privacy-contract-name-custom="<自定义名称及括号>" subdesc="协议后内容协议后内容协议后内容. 主动换行"></zero-privacy>
</view>
</template>
五. zero-privacy组件参数说明
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
position | String | center | 可选 bottom ,从底部弹出 |
color | String | #0396FF | 主颜色: 协议名和同意按钮的背景色 |
bgcolor | String | #ffffff | 弹窗背景色 |
onNeed | Boolean | true | 使用到隐私相关api时触发弹窗,设置为false时初始化弹窗将判断是否需要隐私授权,需要则直接弹出 |
hideTabBar | Boolean | false | 是否需要隐藏tabbar,在首页等tabbar页面使用改弹窗时建议改为true |
title | String | #ffffff | 用户隐私保护提示 |
predesc | String | 使用前请仔细阅读 Ï | 协议名称前的内容 |
subdesc | String | 当您点击同意后,即表示您已理解并同意该条款内容,该条款将对您产生法律约束力。如您拒绝,将无法使用该服务。 | 协议名称后的内容 |
privacyContractNameCustom | String | '' | 自定义协议名称,不传则由小程序自动获取 |
predesc
和 subdesc
的自定义内容,需要主动换行时在内容中添加实体字符
即可
六. zero-privacy组件运行效果
来源:juejin.cn/post/7273803674790150183
java就能写爬虫还要python干嘛?
爬虫学得好,牢饭吃得饱!!!切记!!!
相信大家多少都会接触过爬虫相关的需求吧,爬虫在绝大多数场景下,能够帮助客户自动的完成部分工作,极大的减少人工操作。目前更多的实现方案可能都是以python为实现基础,但是作为java程序员,咱们需要知道的是,以java 的方式,仍然可以很方便、快捷的实现爬虫。下面将会给大家介绍两种以java为基础的爬虫方案,同时提供案例供大家参考。
一、两种方案
传统的java实现爬虫方案,都是通过jsoup的方式,本文将采用一款封装好的框架【webmagic】进行实现。同时针对一些特殊的爬虫需求,将会采用【selenium-java】的进行实现,下面针对两种实现方案进行简单介绍和演示配置方式。
1.1 webmagic
官方文档:webmagic.io/
1.1.1 简介
使用webmagic开发爬虫,能够非常快速的实现简单且逻辑清晰的爬虫程序。
四大组件
- Downloader:下载页面
- PageProcessor:解析页面
- Scheduler:负责管理待抓取的URL,以及一些去重的工作。通常不需要自己定制。
- Pipeline:获取页面解析结果,数持久化。
Spider
- 启动爬虫,整合四大组件
1.1.2 整合springboot
webmagic分为核心包和扩展包两个部分,所以我们需要引入如下两个依赖:
<properties>
<webmagic.version>0.7.5</webmagic.version>
</properties>
<!--WebMagic-->
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-core</artifactId>
<version>${webmagic.version}</version>
</dependency>
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-extension</artifactId>
<version>${webmagic.version}</version>
</dependency>
到此为止,我们就成功的将webmagic引入进来了,具体使用,将在后面的案例中详细介绍。
1.2 selenium-java
1.2.1 简介
selenium是一款浏览器自动化工具,它能够模拟用户操作浏览器的交互。但前提是,我们需要在使用他的机器(windows/linux等)安装上它需要的配置。相比于webmigc的安装,它要繁琐的多了,但使用它的原因,就是为了解决一些webmagic做不到的事情。
支持多种语言:java、python、ruby、javascript等。其使用代码非常简单,以java为例如下:
package dev.selenium.hello;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
public class HelloSelenium {
public static void main(String[] args) {
WebDriver driver = new ChromeDriver();
driver.get("https://selenium.dev");
driver.quit();
}
}
1.2.2 安装
无论是在windows还是linux上使用selenium,都需要两个必要的组件:
- 浏览器(chrome)
- 浏览器驱动 (chromeDriver)
需要注意的是,要确保上述两者的版本保持一致。
下载地址
chromeDriver:chromedriver.storage.googleapis.com/index.html
windows
windows的安装相对简单一些,将chromeDriver.exe下载至电脑,chrome浏览器直接官网下载相应安装包即可。严格保证两者版本一致,否则会报错。
在后面的演示程序当中,只需要通过代码指定chromeDriver的路径即可。
linux
linux安装才是我们真正的使用场景,java程序通常是要部署在linux环境的。所以我们需要linux的环境下安装chrome和chromeDriver才能实现想要的功能。
首先要做的是判断我们的linux环境属于哪种系统,是ubuntu
、centos
还是其他的种类,相应的shell脚本都是不同的。
我们采用云原生的环境,所有的服务均以容器的方式部署,所以要在每一个服务端容器内部安装chrome和chromeDiver。我们使用的是Alpine Linux
,一个轻量级linux发行版,非常适合用来做Docker镜像。
我们可以通过apk --help
去查看相应的命令,我直接给出安装命令:
# Install Chrome for Selenium
RUN apk add gconf
RUN apk add chromium
RUN apk add chromium-chromedriver
上面的内容,可以放在DockerFile文件中,在部署的时候,会直接将相应组件安装在容器当中。
需要注意的是,在Alpine Linux中自带的浏览器是chromium
和chromium-chromedriver
,且版本相应较低,但是足够我们的需求所使用了。
/ # apk search chromium
chromium-68.0.3440.75-r0
chromium-chromedriver-68.0.3440.75-r0
1.2.3 整合springboot
我们只需要在爬虫模块引入依赖就好了:
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
</dependency>
二、三个案例
下面通过三个简单的案例,给大家实际展示使用效果。
2.1 爬取省份街道
使用webmagic进行省份到街道的数据爬取。注意,本文只提供思路,不提供具体爬取网站信息,请同学们自己根据使用选择。
接下来搭建webmagic的架子,其中有几个关键点:
- 创建页面解析类,实现PageProcessor。
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.processor.PageProcessor;
/**
* 页面解析
*
* @author wjbgn
* @date 2023/8/15 17:25
**/
public class TestPageProcessor implements PageProcessor {
@Override
public void process(Page page) {
}
@Override
public Site getSite() {
return site;
}
/**
* 初始化Site配置
*/
private Site site = Site.me()
// 重试次数
.setRetryTimes(3)
//编码
.setCharset(StandardCharsets.UTF_8.name())
// 超时时间
.setTimeOut(10000)
// 休眠时间
.setSleepTime(1000);
}
- 实现PageProcessor后,要重写其方法process(Page page),此方法是我们实现爬取的核心(页面解析)。通常省市区代码分为6级,所以常见的网站均是按照层级区分,我们是从省份开始爬取,即从第三层开始爬取。
- 初始化变量
@Override
public void process(Page page) {
// 市级别
Integer type = 3;
// 初始化结果明细
RegionCodeDTO regionCodeDTO = new RegionCodeDTO();
// 带有父子关系的结果集合
List<Map<String, Object>> list = new ArrayList();
// 页面所有元素集合
List<String> all = new ArrayList<>();
// 页面中子页面的链接地址
List<String> urlList = new ArrayList<>();
}
- 根据不同级别,获取相应页面不同的元素
if (CollectionUtil.isEmpty(all)) {
// 爬取所有的市,编号,名称
all = page.getHtml().css("table.citytable").css("tr").css("a", "text").all();
// 爬取所有的城市下级地址
urlList = page.getHtml().css("table.citytable").css("tr").css("a", "href").all()
.stream().distinct().collect(Collectors.toList());
if (CollectionUtil.isEmpty(all)) {
// 区县级别
type = 4;
all = page.getHtml().css("table.countytable").css("tr.countytr").css("td", "text").all();
// 获取区
all.addAll(page.getHtml().css("table.countytable").css("tr.countytr").css("a", "text").all());
urlList = page.getHtml().css("table.countytable").css("tr").css("a", "href").all()
.stream().distinct().collect(Collectors.toList());
if (CollectionUtil.isEmpty(all)) {
// 街道级别
type = 5;
all = page.getHtml().css("table.towntable").css("tr").css("a", "text").all();
urlList = page.getHtml().css("table.towntable").css("tr").css("a", "href").all()
.stream().distinct().collect(Collectors.toList());
if (CollectionUtil.isEmpty(all)) {
// 村,委员会
type = 6;
List<String> village = new ArrayList<>();
all = page.getHtml().css("table").css("tr.villagetr").css("td", "text").all();
for (int i = 0; i < all.size(); i++) {
if (i % 3 != 1) {
village.add(all.get(i));
}
}
all = village;
}
}
}
}
- 定义一个实体类RegionCodeDTO,用来存放临时获取的code,url以及父子关系等内容:
public class RegionCodeDTO {
private String code;
private String parentCode;
private String name;
private Integer type;
private String url;
private List<RegionCodeDTO> regionCodeDTOS;
}
- 接下来对页面获取的内容(code、name、type)进行组装和临时存储,添加到children中:
// 初始化子集
List<RegionCodeDTO> children = new ArrayList<>();
// 初始化临时节点数据
RegionCodeDTO region = new RegionCodeDTO();
// 解析页面结果集all当中的数据,组装到region 和 children当中
for (int i = 0; i < all.size(); i++) {
if (i % 2 == 0) {
region.setCode(all.get(i));
} else {
region.setName(all.get(i));
}
if (StringUtils.isNotEmpty(region.getCode()) && StringUtils.isNotEmpty(region.getName())) {
region.setType(type);
// 添加子集到集合当中
children.add(region);
// 重新初始化
region = new RegionCodeDTO();
}
}
- 组装页面链接,并将页面链接组装到children当中。
// 循环遍历页面元素获取的子页面链接
for (int i = 0; i < urlList.size(); i++) {
String url = null;
if (StringUtils.isEmpty(urlList.get(0))) {
continue;
}
// 拼接链接,页面的子链接是相对路径,需要手动拼接
if (urlList.get(i).contains(provinceEnum.getCode() + "/")) {
url = provinceEnum.getUrlPrefixNoCode();
} else {
url = provinceEnum.getUrlPrefix();
}
// 将链接放到临时数据子集对象中
if (urlList.get(i).substring(urlList.get(i).lastIndexOf("/") + 1, urlList.get(i).indexOf(".html")).length() == 9) {
children.get(i).setUrl(url + page.getUrl().toString().substring(page.getUrl().toString().indexOf(provinceEnum.getCode() + "/") + 3
, page.getUrl().toString().lastIndexOf("/")) + "/" + urlList.get(i));
} else {
children.get(i).setUrl(url + urlList.get(i));
}
}
- 将children添加到结果对象当中
// 将子集放到集合当中
regionCodeDTO.setRegionCodeDTOS(children);
- 在下面的代码当中将进行两件事儿:
- 处理下一页,通过page的addTargetRequests方法,可以进行下一页的跳转,此方法参数可以是listString和String,即支持多个页面跳转和单个页面的跳转。
- 将数据传递到Pipeline,用于数据的存储,Pipeline的实现将在后面具体说明。
// 定义下一页集合
List<String> nextPage = new ArrayList<>();
// 遍历上面的结果子集内容
regionCodeDTO.getRegionCodeDTOS().forEach(regionCodeDTO1 -> {
// 组装下一页集合
nextPage.add(regionCodeDTO1.getUrl());
// 定义并组装结果数据
Map<String, Object> map = new HashMap<>();
map.put("regionCode", regionCodeDTO1.getCode());
map.put("regionName", regionCodeDTO1.getName());
map.put("regionType", regionCodeDTO1.getType());
map.put("regionFullName", regionCodeDTO1.getName());
map.put("regionLevel", regionCodeDTO1.getType());
list.add(map);
// 推送数据到pipeline
page.putField("list", list);
});
// 添加下一页集合到page
page.addTargetRequests(nextPage);
- 当本次process方法执行完后,将会根据传递过来的链接地址,再次执行process方法,根据前面定义的读取页面元素流程的代码,将不符合type=3的内容,所以将会进入到下一级4的爬取过程,5、6级别原理相同。
- 创建Pipeline,用于编写数据持久化过程。经过上面的逻辑,已经将所需内容全部获取到,接下来将通过pipline进行数据存储。首先定义pipeline,并实现其process方法,获取结果内容,具体存储数据的代码就不展示了,需要注意的是,此处pipeline没有通过spring容器托管,需要调用业务service需要使用SpringUtils进行获取:
public class RegionDataPipeline implements Pipeline{
@Override
public void process(ResultItems resultItems, Task task) {
// 获取service
IXXXXXXXXXService service = SpringUtils.getBean(IXXXXXXXXXService.class);
// 获取内容
List<Map<String, String>> list = (List<Map<String, String>>) resultItems.getAll().get("list");
// 解析数据,转换为对应实体类
// service.saveBatch
}
- 启动爬虫
//启动爬虫
Spider.create(new RegionCodePageProcessor(provinceEnum))
.addUrl(provinceEnum.getUrl())
.addPipeline(new RegionDataPipeline())
//此处不能小于2
.thread(2).start()
2.2 爬取网站静态图片
爬取图片是最常见的需求,我们通常爬取的网站都是静态的网站,即爬取的内容都在网页上面渲染完成的,我们可以直接通过获取页面元素进行抓取。
可以参考下面的文章,直接拉取网站上的图片:juejin.cn/post/705138…
针对获取到的图片网络地址,直接使用如下方式进行下载即可:
url = new URL(imageUrl);
//打开连接
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
//设置请求方式为"GET"
conn.setRequestMethod("GET");
//超时响应时间为10秒
conn.setConnectTimeout(10 * 1000);
//通过输入流获取图片数据
InputStream is = conn.getInputStream();
2.3 爬取网站动态图片
在2.2中我们可以很快地爬取到对应的图片,但是在另外两种场景下,我们获取图片将会不适用上面的方式:
- 需要拼图,且多层的gis相关图片,此种图片将会在后期进行复杂的图片处理(按位置拼接瓦片,多层png图层叠加),才能获取到我们想要的效果。
- 动态js加载的图片,直接无法通过css、xpath获取。
所以在这种情况下我们可以使用开篇介绍的selenium-java来解决,本文使用的仅仅是截图的功能,来达到我们需要的效果。具体街区全屏代码如下所示:
public File getItems() {
// 获取当前操作系统
String os = System.getProperty("os.name");
String path;
if (os.toLowerCase().startsWith("win")) {
//windows系统
path = "driver/chromedriver.exe";
} else {
//linux系统
path = "/usr/bin/chromedriver";
}
WebDriver driver = null;
// 通过判断 title 内容等待搜索页面加载完毕,间隔秒
try {
System.setProperty("webdriver.chrome.driver", path);
ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.addArguments("--headless");
chromeOptions.addArguments("--no-sandbox");
chromeOptions.addArguments("--disable-gpu");
chromeOptions.addArguments("--window-size=940,820");
driver = new ChromeDriver(chromeOptions);
// 截图网站地址
driver.get(UsaRiverConstant.OBSERVATION_POINT_URL);
// 休眠用于网站加载
Thread.sleep(15000);
// 截取全屏
File screenshotAs = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
return screenshotAs;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
driver.quit();
}
}
如上所示,我们获取的是整个页面的图片,还需要对截取的图片进行相应的剪裁,保留我们需要的区域,如下所示:
public static void cutImg(InputStream inputStream, int x, int y, int width, int height, OutputStream outputStream) {//图片路径,截取位置坐标,输出新突破路径
InputStream fis = inputStream;
try {
BufferedImage image = ImageIO.read(fis);
//切割图片
BufferedImage subImage = image.getSubimage(x, y, width, height);
Graphics2D graphics2D = subImage.createGraphics();
graphics2D.drawImage(subImage, 0, 0, null);
graphics2D.dispose();
//输出图片
ImageIO.write(subImage, "png", outputStream);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
三、小结
通过如上两个组件的简单介绍,足够应付在java领域的大多数爬取场景。从页面数据、到静态网站图片,在到动态网站的图片截取。本文以提供思路为主,原理请参考相应的官方文档。
爬虫学得好,牢饭吃得饱!!!切记!!!
来源:juejin.cn/post/7267532912617177129
领导让前端实习生在网页上添加一个长时间不操作锁定电脑的功能
前情提要
大约一个月前,公司的医疗管理系统终于完工上线。后面一个周一,领导叫大家开会,说后面没有项目进来了,用不了这么多开发人员,原地宣布裁员。再后一周后,花 2000 招了个实习生,工作内容为系统维护。
工作内容
领导:由于我们工作内容很简单,事情轻松,基本就在页面上加加按钮就行,所以工资相对较少一些,是否接受?
实习生小李:能开实习证明吗?
领导:能的。
实习生小李:好的,谢谢老板。
领导:什么时候能入职?
实习生小李:现在。
工作来源
医疗系统是一个比较数据敏感的系统,现在医院那边需要添加一个十分钟时间没有在系统进行操作,则锁定电脑的功能,使用者再次使用时,必须输入密码。客户那边在系统对接群里发出需求时,并没有人回复(PS:人都裁完了),然后老板回复到:好的。
工作安排
领导:小李,我们有个医疗系统,需要添加锁屏功能,你处理一下,两天时间应该没问题吧?
实习生小李:(思索片刻)好的,有代码吗
(4小时之后)
领导:有的,我找下
(第二天10点)
实习生小李:王总,代码找到了没有
(第二天12点)
领导:没代码改不了吗?
实习生小李:(瑟瑟发抖)我试试
(第二天14点)
实习生小李:王总,是那种长时间不操作就锁定系统的功能吗
领导:是的
实习生小李:多久不操作才锁
领导:十分钟,锁了需要输入密码才能使用
实习生小李:但是我们医疗系统没有密码功能
领导:客户电脑有密码啊
实习生小李:是锁电脑系统吗
领导:对
实习生小李:(若有所思)我试试
实现过程
实习生小李:魔镜魔镜,我们有个医疗系统,需要做一个十分钟不操作电脑,就锁定用户电脑系统的功能,在没有源代码的情况下如何实现?
魔镜:好的,在没有源代码的情况下为医疗系统添加十分钟不操作电脑就锁定用户电脑系统的功能,可以使用 sys-shim 实现。
第一步,创建一个目录例如 medical-system
,目录里有以下两个文件:
package.json
文件用来配置 sys-shimpreload.js
用来向医疗系统添加功能
第二步
在 package.json 中编写内容如下
{
"browserArguments": "--disable-web-security --allow-running-insecure-content ",
"form": {
"right": "1300",
"bottom": "800"
},
"page": "https://www.baidu.com/"
}
- browserArguments 用来指定浏览器参数,这里配置为允许跨域以方便注入代码
- form 用来控制窗口如何显示,这里表示窗口大小
- page 表示医疗系统的页面
在 preload.js 中编写内容如下
new Promise(async function () {
window.main = await new window.Sys({ log: true })
// 设置倒计时时间,为了测试方便,这里改为 30 秒
const TIMEOUT = 0.5 * 60 * 1000;
// 声明一个变量来存储 setTimeout 的引用
let timeoutId = null;
// 定义一个函数来重置倒计时并在2分钟后打印日志
function startInactivityCheck() {
// 清除之前的倒计时(如果有的话)
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
// 设置一个新的倒计时
timeoutId = setTimeout(function() {
// 锁定系统
window.main.native.sys.lock()
}, TIMEOUT);
}
// 为 body 元素添加点击事件监听器
document.body.addEventListener('click', function() {
console.log("检测到点击事件,重新开始计时。");
// 重置倒计时
startInactivityCheck();
});
// 初始化倒计时
startInactivityCheck();
})
sys.lock()
方法用于锁定操作系统。
第三步,生成应用程序
npx sys-shim pack --input medical-system
运行该命令后,会在当前目录生成一个名为 medical-system.exe 的可执行文件。它封装了医疗系统这个 web 程序,并在里面添加了锁屏功能。
pack
指定表示打包应用--input
参数表示要打包的目录
--input 参数也可以是线上的网页,比如:
npx sys-shim pack --input https://www.baidu.com/
即可获取一个可以调用操作系统 api 的 web 应用。
交付反馈
用户:以前我们还需要进入浏览器输入网址才能进入系统,现在直接在桌面上就能进入,并且还有安全锁屏功能,非常好!
领导:小李干得不错,但没有在规定的时间内完成,但由于客户反馈不错,就不扣你的考核分了。
实习生小李:(不得其解)谢谢老板。
后记
不知不觉,又到了周五,这是公司技术分享会的时候。当前公司技术人员只有实习生小李,由小李负责技术分享。
宽旷的会议室里,秘书、领导、小李三人面面相觑,小李强忍住尴尬,开始了自己的第一次技术分享:
实习生小李:感谢领导给我的工作机会,在这份工作里,我发现了 sys-shim 这个工具,它可以方便的在已有的 web 页面中添加系统 api,获取调用操作系统层面功能的能力,比如关机、锁屏。
领导:(好奇)那他可以读取电脑上的文件吗?
实习生小李:可以的,它可以直接读取电脑上的文件,例如电脑里面的文档、照片、视频等。
突然领导脸色一黑,看了一眼秘书,并关闭了正在访问的医疗系统,然后在技术分享考核表上写下潦潦草草的几个字:考核分-5
。
续集:托领导大福!前端实习生用 vue 随手写了个系统修复工具,日赚 300
提示
大家可以直接运行这个命令生成 app 体验:
npx sys-shim pack --input https://www.baidu.com/
生成后的 app 可以右键解压,看到内部结构。如果遇到问题,可以在这里提交,方便追溯,我会及时解答的。
参考
来源:juejin.cn/post/7373831659470880806
领导被我的花式console.log吸引了!直接写入公司公共库!
文章的效果,大家可以直接只用云vscode实验一下:juejin.cn/post/738875…
背景简介
这几天代码评审,领导无意中看到了我本地代码的控制台,被我花里胡哨的console打印
内容吸引了!
老板看见后,说我这东西有意思,花里胡哨的,他喜欢!
但是随即又问我,这么花里胡哨的东西,上生产会影响性能吧?我自信的说:不会,代码内有判断的,只有开发环境会打印
!
老板很满意,于是让我给其他前端同事分享一下,讲解下实现思路!最终,这个方法还被写入公司的公用utils库里,供大家使用!
console简介
console 是一个用于调试和记录信息的内置对象, 提供了多种方法,可以帮助开发者输出各种信息,进行调试和分析。
console.log()
用于输出一般信息,大家应该在熟悉不过了。
console.info() :
输出信息,与 console.log 类似,但在某些浏览器中可能有不同的样式。
console.warn() :
输出警告信息,通常会以黄色背景或带有警告图标的样式显示。
console.error() :
输出错误信息,通常会以红色背景或带有错误图标的样式显示。
console.table() :
以表格形式输出数据,适用于数组和对象。
例如:
const users = [
{ name: '石小石', age: 18 },
{ name: '刘亦菲', age: 18 }
];
console.table(users);
通过上述介绍,我们可以看出,原生的文本信息、警告信息、错误信息、数组信息打印出来的效果都很普通,辨识度不高!现在我们通过console.log来实现一些花里花哨的样式!
技术方案
console.log()
console.log() 可以接受任何类型的参数,包括字符串、数字、布尔值、对象、数组、函数等。最厉害的是,它支持占位符!
常用的占位符:
- %s - 字符串
- %d or %i - 整数
- %f - 浮点数
- %o - 对象
- %c - CSS 样式
格式化字符串
console.log() 支持类似于 C 语言 printf 函数的格式化字符串。我们可以使用占位符来插入变量值。
const name = 'Alice';
const age = 30;
console.log('Name: %s, Age: %d', name, age); // Name: Alice, Age: 30
添加样式
可以使用 %c 占位符添加 CSS 样式,使输出内容更加美观。
console.log('%c This is a styled message', 'color: red; font-size: 20px;');
自定义样式的实现,其实主要是靠%c 占位符添加 CSS 样式实现的!
实现美化的信息打印
基础信息打印
我们创建一个prettyLog方法,用于逻辑编写
// 美化打印实现方法
const prettyLog = () => {
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
console.log(
`%c ${title} %c ${text} %c`,
`background:${color};border:1px solid ${color}; padding: 1px; border-radius: 2px 0 0 2px; color: #fff;`,
`border:1px solid ${color}; padding: 1px; border-radius: 0 2px 2px 0; color: ${color};`,
'background:transparent'
);
};
// 基础信息打印
const info = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Info' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#909399');
};
return {
info
};
};
上述代码定义了一个 prettyLog 函数,用于美化打印信息到控制台。通过自定义样式,输出信息以更易读和美观的格式呈现。
我们使用一下看看效果
// 创建打印对象
const log = prettyLog();
// 不带标题
log.info('这是基础信息!');
//带标题
log.info('注意看', '这是个男人叫小帅!');
info 方法用于输出信息级别的日志。它接受两个参数:textOrTitle 和 content。如果只提供一个参数,则视为内容并设置默认标题为 Info;如果提供两个参数,则第一个参数为标题,第二个参数为内容。最后调用 prettyPrint 方法进行输出。
错误信息打印
const prettyLog = () => {
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
// ...
};
const info = (textOrTitle: string, content = '') => {
// ...
};
const error = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Error' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#F56C6C');
};
// retu;
return {
info,
error,
};
};
// 创建打印对象
const log = prettyLog();
log.error('奥德彪', '出来的时候穷 生活总是让我穷 所以现在还是穷。');
log.error('前方的路看似很危险,实际一点也不安全。');
成功信息与警告信息打印
// 美化打印实现方法
const prettyLog = () => {
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
console.log(
`%c ${title} %c ${text} %c`,
`background:${color};border:1px solid ${color}; padding: 1px; border-radius: 2px 0 0 2px; color: #fff;`,
`border:1px solid ${color}; padding: 1px; border-radius: 0 2px 2px 0; color: ${color};`,
'background:transparent'
);
};
const info = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Info' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#909399');
};
const error = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Error' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#F56C6C');
};
const warning = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Warning' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#E6A23C');
};
const success = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Success ' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#67C23A');
};
// retu;
return {
info,
error,
warning,
success
};
};
// 创建打印对象
const log = prettyLog();
log.warning('奥德彪', '我并非无路可走 我还有死路一条! ');
log.success('奥德彪', '钱没了可以再赚,良心没了便可以赚的更多。 ');
实现图片打印
// 美化打印实现方法
const prettyLog = () => {
// ....
const picture = (url: string, scale = 1) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
const c = document.createElement('canvas');
const ctx = c.getContext('2d');
if (ctx) {
c.width = img.width;
c.height = img.height;
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, c.width, c.height);
ctx.drawImage(img, 0, 0);
const dataUri = c.toDataURL('image/png');
console.log(
`%c sup?`,
`font-size: 1px;
padding: ${Math.floor((img.height * scale) / 2)}px ${Math.floor((img.width * scale) / 2)}px;
background-image: url(${dataUri});
background-repeat: no-repeat;
background-size: ${img.width * scale}px ${img.height * scale}px;
color: transparent;
`
);
}
};
img.src = url;
};
return {
info,
error,
warning,
success,
picture
};
}
// 创建打印对象
const log = prettyLog();
log.picture('https://nimg.ws.126.net/?url=http%3A%2F%2Fdingyue.ws.126.net%2F2024%2F0514%2Fd0ea93ebj00sdgx56001xd200u000gtg00hz00a2.jpg&thumbnail=660x2147483647&quality=80&type=jpg');
上述代码参考了其他文章:Just a moment...
url可以传支持 base64,如果是url链接,图片链接则必须开启了跨域访问才能打印
实现美化的数组打印
打印对象或者数组,其实用原生的console.table比较好
const data = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 },
{ id: 3, name: 'Charlie', age: 35 }
];
console.table(data);
当然,我们也可以伪实现
const table = () => {
const data = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 },
{ id: 3, name: 'Charlie', age: 35 }
];
console.log(
'%c id%c name%c age',
'color: white; background-color: black; padding: 2px 10px;',
'color: white; background-color: black; padding: 2px 10px;',
'color: white; background-color: black; padding: 2px 10px;'
);
data.forEach((row: any) => {
console.log(
`%c ${row.id} %c ${row.name} %c ${row.age} `,
'color: black; background-color: lightgray; padding: 2px 10px;',
'color: black; background-color: lightgray; padding: 2px 10px;',
'color: black; background-color: lightgray; padding: 2px 10px;'
);
});
};
但是,我们无法控制表格的宽度,因此,这个方法不太好用,不如原生。
仅在开发环境使用
// 美化打印实现方法
const prettyLog = () => {
//判断是否生产环境
const isProduction = import.meta.env.MODE === 'production';
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
if (isProduction) return;
// ...
};
// ...
const picture = (url: string, scale = 1) => {
if (isProduction) return;
// ...
};
// retu;
return {
info,
error,
warning,
success,
picture,
table
};
};
我们可以通过import.meta.env.MODE 判断当前环境是否为生产环境,在生产环境,我们可以禁用信息打印!
完整代码
// 美化打印实现方法
const prettyLog = () => {
const isProduction = import.meta.env.MODE === 'production';
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
if (isProduction) return;
console.log(
`%c ${title} %c ${text} %c`,
`background:${color};border:1px solid ${color}; padding: 1px; border-radius: 2px 0 0 2px; color: #fff;`,
`border:1px solid ${color}; padding: 1px; border-radius: 0 2px 2px 0; color: ${color};`,
'background:transparent'
);
};
const info = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Info' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#909399');
};
const error = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Error' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#F56C6C');
};
const warning = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Warning' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#E6A23C');
};
const success = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Success ' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#67C23A');
};
const table = () => {
const data = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 },
{ id: 3, name: 'Charlie', age: 35 }
];
console.log(
'%c id%c name%c age',
'color: white; background-color: black; padding: 2px 10px;',
'color: white; background-color: black; padding: 2px 10px;',
'color: white; background-color: black; padding: 2px 10px;'
);
data.forEach((row: any) => {
console.log(
`%c ${row.id} %c ${row.name} %c ${row.age} `,
'color: black; background-color: lightgray; padding: 2px 10px;',
'color: black; background-color: lightgray; padding: 2px 10px;',
'color: black; background-color: lightgray; padding: 2px 10px;'
);
});
};
const picture = (url: string, scale = 1) => {
if (isProduction) return;
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
const c = document.createElement('canvas');
const ctx = c.getContext('2d');
if (ctx) {
c.width = img.width;
c.height = img.height;
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, c.width, c.height);
ctx.drawImage(img, 0, 0);
const dataUri = c.toDataURL('image/png');
console.log(
`%c sup?`,
`font-size: 1px;
padding: ${Math.floor((img.height * scale) / 2)}px ${Math.floor((img.width * scale) / 2)}px;
background-image: url(${dataUri});
background-repeat: no-repeat;
background-size: ${img.width * scale}px ${img.height * scale}px;
color: transparent;
`
);
}
};
img.src = url;
};
// retu;
return {
info,
error,
warning,
success,
picture,
table
};
};
// 创建打印对象
const log = prettyLog();
来源:juejin.cn/post/7371716384847364147
听说去哪儿混合办公了? 聊聊程序员如何找到远程工作和好处
哈喽大家好,这两天看到去哪儿开始混合办公了,作为远程工作的支持者我表示很开心,终于有大厂全面开始支持远程员工,去哪儿的邮件截图是这么说的
去年开始现在一些团队做小范围的尝试,好评如潮,有的同学利用通勤的时间减肥,有的同学可以回家陪家人,家人对工作的支持度变高,而且工作效率一点都没下降,所以去哪儿在7月开始,每周三周五可以居家办公,无需申请
远程的好处非常明显,尤其是在一线城市,通勤的一个半小时就省下了,生活幸福度直线提高,你出去旅游周四就可以出发,如果五天都可以居家就是完全远程,你可以回老家省会还有北京一线的收入,老板节省下了组办公室的费用
因为远程无论是对员工满意度,还是老板的成本控制都很友好,混合或者远程办公在海外已经比较流行了,基本海外所有的招聘网站都有是否远程的选项,你可以过滤只看支持远程的,你搜工作就有一个选项是坐班,混合还是远程的,不像国内boss直聘,默认都是要通勤的,哪怕跟我说支持远程,也是只是面试可以远程
比如英国这边混合办公室基本操作,好一些的就会全员远程,比如这边大厂,Meta是每周去两天还是三天,我给忘了,我觉得国内以后支持远程的会越来越多,越来越多的小老板会抛弃自己奴隶主的思想,必须得盯着你干活,反而会考虑更真实的办公室成本
对远程最大的批评就是会降低工作效率,所谓的见面沟通效率才是最高的,确实有一些场景面对面效率最高,但是扪心自问,你现在开会的效率真的高吗,大公司动辄就一小时的会议,而且你首先就在通勤上浪费了一小时,你一天的效率可能很高吗 ,腾讯会议的AI总结功能比以前人工写的会议纪要不知道好多少倍
而一个人能有更多的时间照顾家庭后,闲暇时间才会产生创意,尝试新的工具和沟通方式等等,可能会让效率有非常大的提高
远程我就可以工作的同事带家人出游,远程久了你换工作就会只考虑远程的,再也不想挤地铁了,虽然现在通勤还是主流,尤其是还有马斯克这种非常反对远程的人,但是我最近聊的一些创业公司和小公司,基本都支持混合办公的
说了这么多远程办公的好处,喜欢工作和生活平衡的你可能已经蠢蠢欲动了,那远 程工作需要哪些能力呢,以及如何找到一个远程工作呢
其实远程工作有很多最佳实践,比较典型的有37signals这家公司,这家公司坚持小而美,写出来ruby on rails这种框架,有一套书叫重来,有三本,第二本就叫remote,比较系统的介绍远程工作文化的方方面面
包括远程工作的好处,可以逃离大城市的房价,更好的work life balance,更自在的生活,还反驳了一些对远程文化的批评,比如觉得坐在一起才能效率高,家里干扰大等等
更重要的介绍了如何更好的远程协作,这是需要学习的技能,比如如何可视化你的工作进度,高效的沟通,怎么管理远程员工的效率,还有远程人如何更好的生活,非常推荐
还有就是有一个公司叫gitlab,这个更厉害,这是一家美国上市公司,应该是第一个招股书里没有办公地址的,从老板到实习生,全员都散落在世界各地办公,关于大型公司如何实践远程文化,他们有一个专门的文档,质量非常高,主要是关于如何管理远程团队,还有很重要的远程开会技巧
地址和上面重来的电子书,评论区好像没法发链接,要不加我吧,我研究下怎么发给大家
那最后如何找到一个远程工作呢,其实之前我也分享过,这里简单总结下
首先程序员是非常适合远程的,所有的代码任务都可以在线完成,通过git管理代码,腾讯会议或者zoom开会,飞书钉钉slack等工作聊天等等
就像前面所述国内远程机会比较少,而且很多国内的老板哪怕远程也依然是监控心态,比如要求你开摄像头或者响应速度也挺难受,所以我觉得比如你想明年找个远程,那现在就优先学英语,程序员怎么学英语大家感兴趣以后可以专门聊,大概方法就是不要学英语,而是用英语学习编程就可以了
希望大家都能尝试和探索混合或者远程工作的新体验,能够拥有一个更加自在的职场,在努力工作的同时,可以有时间陪家人探索世界
来源:juejin.cn/post/7392116075674927131
同为情怀程序员,给博客园提供几个救园思路
博客园是老牌的技术社区,近来因为各种原因,导致社区运营岌岌可危。
笔者也是一个情怀型程序员,但是在这几年的创业生涯中,也摸爬滚打,养成了一些商业思路。
这个世界,就是一个肉弱强食,尔虞我诈的,不要做一个纯粹的情怀主义者,也不要做一个老实人。
那么下面呢,基于笔者的认知和思路,给博客园提供一些救园思路。
降本增效
个人认为,这是非常核心,非常重要的问题。
收入骤减,那么我们的开销成本,也要跟着降低,才能维持更久的运转。
比如关停不必要的网站功能,减少服务器的运行开支。
作为一个技术社区,主要的金钱投入,除了人员开支,办公场地之外,应该就是服务器的运行成本了吧。
那么相应的,人员开支,办公费用,场地,该减少的减少,该换场地的就换场地。
这是降低成本,控制支出最直观的方式了。
那么接下来呢,就是如何提高收入了。
外包接单
这是博客园正在做的,当时我看到官方提成只拿5%(原文我没找到了,如有错漏,请指正),这个提成,太低了。
我自己也做接单撮合,运营2-3个月,撮合成功30-40单,平台提成是15%。
那么这个提成,市场行情是多少呢?20%-30%。
5%,说实话,太少了。
接单撮合,有一个问题非常重要:接单,客户,技术,三方都要能赚到钱
。
你拿5%,唉,还是程序员思维,还是在做情怀。
不要这样,商业就是商业,程序员作为最终实施的一方,确实出力最多,最苦,最累。
平台少拿点,给技术多一点,非常好,但是,我的个人建议是不能低于15%,为什么呢?
如果有额外的介绍人,可以给介绍人5%或一半(7.5%),这样才有余地。
如果平台赚不到钱,那么如何运营好接单撮合这个项目呢?
广告接入
这个问题,在博客园7月15日的文章中已经说明了。
我的感觉是,令人肃然起敬,但是违背社会规律。
可以这么说,为了博客园的存亡问题,至少90%以上的用户,是不反感广告的。
但是,目前来看,似乎只有网站的运营者反对广告。。。
只要不做成某C开头的社区,看个电视剧,广告比剧集还长。
所以,我突然发现,这一整个事件的根本原因是什么?居然是人的执念。。可怕。
凭博客园的流量,接入广告,分分钟救园,大家又能愉快地玩耍。
不要考验人性
笔者曾经说过一句话,在网上广为流传。
博客园是一个程序员社区。
通过卖社区周边产品,还是通过情怀变现,下策,下策啊!
既然要商业化,那就不要过多考虑情怀,要从商业的角度去思考问题。
所谓的商业,并不是变成一个尔虞我诈的商人,有太多的既盈利,又给用户带来有价值,有意义的参考,大家可以想想有哪些。
而且,不管是会员,还是周边,这根本不是一个“我尽心尽力服务多年,当我落难的时候,大家挺身而出
”的问题。
大家受益于博客园,博客园也因为所有用户而收益,这是一个相互作用。
绝大多数用户,仅仅只是看客,真正写文章,在博客园出人头地的,少之又少。
社会和技术的发展,日新月异,技术内容,也远不及当年那么必需。
周边也不是必需,会员也是一次性的,根本不具备持续性。
救了这次,下次呢?所以还得从长计议。
产品推广分成
现在,有各种“严选”,“甄选”,“优品”。
博客园也可以做严选,甄选产品。
通过合作的模式,降低产品价格,获得产品提成,也不失为一个好地方方式。
比如,某某软件,某某产品,以低于官网价格的拿货价,平台推广卖给光大社区用户。
这样,用户购买价格更低,产品方销量更好,博客园平台也赚到了钱,也是一个三方共赢的方案。
比卖会员,卖周边好。
这种方式,选品品类更多,更接近用户刚需。
甚至可以发起投票,让用户选择拼团购买什么产品,软件,工具。
既可以积累社区凝聚力,也能让用户买到真正的,需要的东西,平台也赚到了钱。
舆论维护
要倾听用户的声音,目前技术圈,对博客园的这段时间运营情况,可以说非常不满。
鱼皮的这篇文章我觉得就说得不错,这是具备真正生意头脑的创业者思路。
如果一意孤行,还是程序员的传统思路,最终很可能连口碑也坏了,三思。
网站出售
最后呢,当然,是最坏的情况,万不得已,也可以卖掉博客园,获得一笔不菲的收入。
当然,这是下下之策。
希望博客园可以挺过这次难关,不论如何,这么有情怀的社区,真的不多见了。
如果它真的消失了,那将是技术圈的重大损失。
来源:juejin.cn/post/7392071328520994816
fabric.js实战
一、业务需求
- 给定一个图片作为参考
- 可配置画笔颜色、粗细
- 可通过手势生成实际轨迹
- 可通过手势来圈选轨迹
- 可撤销、删除轨迹
- 可操作轨迹
- 可缩放、拖动
- 背景网格,且背景网格可缩放和拖动
- 参考图片可缩放、偏移
- 手势处理系统,单指绘制、双指缩放、三指拖动
- 需要一个禁止绘制区域,方便用户在平板上有手掌的支撑区域
二、技术选型
因为涉及到轨迹
、操作轨迹
两点,
svg无法满足大量且复杂的轨迹,canvas没法操作轨迹,所以从 fabricjs/konva 中选型,
因 fabricjs 使用人数更多,所以采取了其作为技术选型。
三、fabric 原理
- 通过其内置的几何对象来创建图形
- 维护一个对象树
- 将对象树通过 canvas-api 绘制在实际的 canvas 上
因此,fabric 能做非常多的优化手段
- 已渲染的节点可通过
子canvas
做缓存 - 对比新旧对象树能做差值更新
- 虚拟画布,类似于虚拟滚动,只渲染可视化的区域
四、模块拆分
- header:与业务相关
- toolBar:工具栏
- sidebar:与业务相关
- canvas:绘制画板
五、架构设计
六、问题收集
6.1 性能问题
- 圈选是实时的,即判断一个多边形是否相交于或包含于一条复杂轨迹,因为使用了射线法,当遇到大量轨迹的时候,可能会卡顿。目前做了多重优化手段,比如函数节流、先稀疏复杂轨迹的点、然后判断图形的占位区域是否相交、然后判断图形的线段之间是否相交、然后判断是否包含关系。
- 轨迹的实时生成,在一长串touchmove事件中,使用一个初始化的polyline,后续更改其点集,这样只需要实例化一个对象,性能高。
- touchmove回调里执行复杂的逻辑,这会阻塞touchmove的触发频率,我们将touchmove里的回调通过settimeout放到异步队列中,这样就剥离了touchmove
事件层
和 回调函数处理层
,这样touchmove的触发频率就不会被影响
6.2 禁止绘制区域
该需求无法实现,因为当我们手掌放在平板上时,会触发系统级别的误触识别算法,阻止所有的触摸事件,所以我们没办法在页面上实现该功能。
touch事件
一个屏幕上可以有多个touch触摸点,这些触摸点绑定的target是可以多个的
touchEvent对象有如下重要属性
- targetTouches:只在当前target(比如某div)上触发的touch触摸点
- touches:在屏幕上触发的所有touch触目点
特别注意点:
- 没有类似鼠标的mouseout事件,所以你一开始是点在div上的,然后移出div的范围后,依旧触发touchmove事件
平板调试手段-chrome浏览器
- 平板安装chrome移动版
- 电脑安装chrome + chrome插件:inspect devices
- 平板开启开发者选项,然后允许usb调试
- usb链接平板和chrome
- 平板和电脑都打开chrome
- 电脑启动插件,然后就能控制平板的chrome,并且对该chrome访问的网页进行调试
- 很好用哇
来源:juejin.cn/post/7278931998650744869
实现小红书响应式瀑布流
前言
瀑布流布局,不管是在pc端还是手机端都很常见,但是我们通常都是列固定。今天来实现一下小红书的响应式瀑布流。后面有仓库地址。
正文
还是先来看看效果
原理:
对每一个item都使用绝对定位,left和top都是0,最后根据容器大小、item的height通过计算来确定item的transform值
接下来从易到难来解析一下实现
初始化数据
列表怎么可以没有数据,先来初始化一下数据
确定列数及列大小
由于是响应式,我们要去监听列表容器的大小变化并记录容器宽度,这样才能做出相应的处理
根据监听得到的容器大小信息,我们可以确定每行个数
和每一个item的宽度
确定列表中item位置
确定item的位置,那么我们只需要确定transform
值就可以了,这也是整个实现的核心。我们还需要解决几个问题
- 对还不知道item的高度,怎么确定
- 我们希望把新的item放置在最低高度的旧item下方,这样全部渲染完每一列的高度才不会相差很多。
item放置的原理图,放置在当前最低高度的下面
更新item高度
当我们第一次运行的时候,每一个item的高度一定都是随机生成的,现在我们要确定item的实际高度。在这里我们还可优化一下,使用懒加载和底部加载,提升性能
。这两个在这里就不讲了,不懂的可以去搜一下。
下面代码一共两个作用
- 记录容器滚动值,传递给每一个
item
,用于判断是否加载图片。 - 判断是否请求添加数据
根据滚动值判断是否加载图片,加载图片后触发父亲更新高度函数
父亲接受到新的高度并更新高度,然后去重新计算transform
值和item高度
完整代码
结语
感兴趣的可以去试试
来源:juejin.cn/post/7270160291411886132
明明 3 行代码即可轻松实现,Promise 为何非要加塞新方法?
给前端以福利,给编程以复利。大家好,我是大家的林语冰。
00. 观前须知
地球人都知道,JS 中的异步编程是 单线程 的,和其他多线程语言的三观一龙一猪。因此,虽然其他语言的异步模式彼此互通有无,但对 JS 并不友好,比如 Actor 模型等。
这并不是说 JS 被异步社区孤立了,只是因为 JS 天生和多线程八字不合。你知道的,要求 JS 使用多线程,就像要求香菜恐惧症患者吃香菜一样离谱。本质上而言,这是刻在 JS 单线程 DNA 里的先天基因,直接决定了 JS 的“异步性状”。有趣的是,如今 JS 也变异出若干多线程的使用场景,只是比较非主流。
ES6 之后,JS 的异步编程主要基于 Promise
设计,比如人气爆棚的 fetch
API 等。因此,最新的 ES2024 功能里,又双叒叕往 Promise
加塞了新型静态方法 Promise.withResolvers()
,也就见怪不怪了。
问题在于,我发现这个新方法居然只要 3 行代码就能实现!奥卡姆剃刀原则告诉我们, 若无必要,勿增实体。那么这个鸡肋的新方法是否违背了奥卡姆剃刀原则呢?我决定先质疑、再质疑。
当然,作为应试教育的漏网之鱼,我很擅长批判性思考,不会被第一印象 PUA。经过三天三夜的刻意练习,机智如我发现新方法果然深藏不露。所以,本期我们就一起来深度学习 Promise
新方法的技术细节。
01. 静态工厂方法
Promise.withResolvers()
源自 tc39/proposal-promise-with-resolvers
提案,是 Promise
类新增的一个 静态工厂方法。
静态的意思是,该方法通过 Promise
类调用,而不是通过实例对象调用。工厂的意思是,我们可以使用该方法生成一个 Promise
实例,而无须求助于传统的构造函数 + new
实例化。
可以看到,这类似于 Promise.resolve()
等语法糖。区别在于,传统构造函数实例化的对象状态可能不太直观,而这里的 promise
显然处于待定状态,此外还“买一送二”,额外附赠一对用于改变 promise
状态的“变态函数” —— resolve()
和 reject()
。
ES2024 之后,该方法可以作为一道简单的异步笔试题 —— 请你在一杯泡面的时间里,实现一下 Promise.withResolvers()
。
如果你是我的粉丝,根本不慌,因为新方法的基本原理并不复杂,参考我下面的实现,简单给面试官表演一下就欧了。
可以看到,这个静态工厂方法的实现难点在于,如何巧妙地将变态函数暴露到外部作用域,其实核心逻辑压缩后有且仅有 3 行代码。
这就引发了本文开头的质疑:新方法是否多此一举?难道负责 JS 标准化的 tc39 委员会也有绩效考核,还是确实存在某些不为人知的极端情况?
02. 技术细节
通过对新方法进行苏格拉底式的“灵魂拷问”和三天三夜的深度学习,我可以很有把握地说,没人比我更懂它。
首先,与传统的构造函数实例化不同,新方法支持无参构造,我们不需要在调用时传递任何参数。
可以看到,构造函数实例化要求传递一个执行器回调,偷懒不传则直接报错,无法顺利实例化。
其次,变态函数的设计更加自由。
可以看到,传统的构造函数中,变态函数能且仅能作为局部变量使用,无法在构造函数外部调用。而新方法同时返回实例及其变态函数,这意味着实例和变态函数处于同一级别的作用域。
那么,这个设计上的小细节有何黑科技呢?
假设我们想要一个 Promise
实例,但尚未知晓异步任务的所有细节,我们期望先将变态函数抽离出来,再根据业务逻辑灵活调用,请问阁下如何应对?
ES2024 之前,我们可以通过 作用域提升 来“曲线救国”,举个栗子:
可以看到,这种方案的优势在于,诉诸作用域提升,我们不必把所有猫猫放在一个薛定谔的容器里,在构造函数中封装一大坨“代码屎山”;其次,变态函数不被限制在构造函数内部,随时随地任你调用。
该方案的缺陷则在于,某些社区规范鼓励“const
优先”的代码风格,即 const
声明优先,再按需修改为 let
声明。
这里的变态函数被迫使用 let
声明,这意味着存在被愣头青意外重写的隐患,但为了缓存赋值,我们一开始就不能使用 const
声明。从防御式编程的角度,这可能不太鲁棒。
因此,Promise.withResolvers()
应运而生,该静态工厂方法允许我们:
- 无参构造
const
优先- 自由变态
03. 设计动机
在某些需要封装 Promise
风格的场景中,新方法还能减少回调函数的嵌套,我把这种代码风格上的优化称为“去回调化”。
举个栗子,我们可以把 Node 中回调风格的 API 转换为 Promise
风格,以 fs
模块为例:
可以看到,由于使用了传统的构造函数实例化,在封装 readFile()
的时候,我们被迫将其嵌套在构造函数内部。
现在,我们可以使用新方法来“去回调化”。
可以看到,传统构造函数嵌套的一层回调函数就无了,整体实现更加扁平,减肥成功!
粉丝请注意,很多 Node API 现在也内置了 Promise
版本,现实开发中不需要我们手动封装,开箱即用就欧了。但是这种封装技巧是通用的。
举个栗子,瞄一眼 MDN 电子书搬运过来的一个更复杂的用例,将 Node 可读流转换为异步可迭代对象。
可以看到,井然有序的代码中透露着一丝无法形容的优雅。我脑补了一下如何使用传统构造函数来实现上述功能,现在还没缓过来......
04. 高潮总结
从历史来看,Promise.withResolvers()
并非首创,bluebird 的 Promise.defer()
或 jQuery 的 $.defer()
等库就提供了同款功能,ES2024 只是换了个名字“新瓶装旧酒”,将其标准化为内置功能。
但是,Promise.withResolvers()
的标准化势在必行,比如 Vite 源码中就自己手动封装了同款功能。
无独有偶,Axios、Vue、TS、React 等也都在源码内部“反复造轮子”,像这种回头率超高的代码片段我们称之为 boilerplate code(样板代码)。
重复乃编程之大忌,既然大家都要写,不如大家都别写,让 JS 自己写,牺牲小我,成全大家。编程里的 DRY 原则就是让我们不要重复,因为很多 bug 就是重复导致的,而且不好统一管理和维护,《ES6 标准入门教程》科普的 魔术字符串 就是其中一种反模式。
兼容性方面,我也做过临床测试了,主流浏览器广泛支持。
总之,Promise.withResolvers()
通过将样板代码标准化,达到了消除重复的目的,原生实现除了性能更好,是一个性价比较高的静态工厂方法。
参考文献
- GitHub:github.com/tc39/propos…
- MDN:developer.mozilla.org/en-US/docs/…
- bluebird:bluebirdjs.com/docs/deprec…
粉丝互动
本期话题是:你觉得新方法好评几颗星,为什么?你可以在本文下方自由言论,文明科普。
欢迎持续关注“前端俱乐部”,给前端以福利,给编程以复利。
坚持阅读的小伙伴可以给自己点赞!谢谢大家的点赞,掰掰~
来源:juejin.cn/post/7391745629876469760
显示器分辨率的小知识
数字化时代,显示器是我们日常生活和工作中不可或缺的一部分。
无论是在电脑、手机、平板还是电视上,我们都依赖显示器来呈现图像和文字。然而,对于许多人来说,显示器分辨率这一概念可能并不十分清晰。
分辨率是影响显示器性能和视觉效果的关键因素之一。它决定了图像的清晰度、细节和整体观感。
因此,了解显示器分辨率的知识对于我们选择和使用显示设备至关重要。
本文将向大家介绍关于显示器分辨率的一些小知识,希望能够帮助大家更好地选择合适的显示器,并提升在使用显示设备时的体验。
1. 常用的分辨率
代号 | 分辨率 | 备注 |
---|---|---|
720p | 1280 x 720 | 也被称为 HD,高清 |
1080p | 1920 x 1080 | 也被称为 FULL HD,全高清 |
1440p | 2560 x 1440 | 也被称为 QHD,Quad HD |
2160p | 3840 x 2160 | 也被称为 4K |
4320p | 7680 x 4320 | 也被称为 8K |
2. 一些术语
关于显示器,最常见的三个术语就是:
2.1. 刷新率
刷新率(Refresh rate
)是指屏幕硬件每秒刷新以显示图像的速率,通常以赫兹(Hz
)为单位。
刷新率是指显示器的能力,简单理解就是每秒屏幕能切换多少个图像。
刷新率越高的显示器,显示的视频越流畅。
不过,由于人眼有视觉暂留的能力,一般60Hz左右的液晶屏已经很流畅了。
2.2. 帧速率
帧速率(Frame rate
)是指视频或游戏每秒传输的图像帧数,通常以FPS
(每秒帧数)为单位。
帧速率一般取决于视频或者游戏本身,与显示器关系不大。
帧速率越高,视频和游戏的清晰度和流畅度越高,当然,占用的硬盘空间也越大,对显卡要求也越高。
在实际使用中,如果帧速率高于刷新率,可能会出现屏幕撕裂等现象,因为显示器无法完全跟上图像的更新速度。
而如果刷新率高于帧速率的话,对显示影响不大,但是对显示器来说,有点大材小用。
因此,刷新率和帧速率的匹配和协调对于获得最佳的视觉体验,以及购买显示器时考虑性价比至关重要。
2.3. 纵横比
纵横比(Aspect ratio
)概念比较简单,是指水平像素数与垂直像素数的比率。
对于视频和游戏,一般可以调节输出的纵横比;对于显示器,也可以通过调节像素来显示不同的纵横比。
视频或游戏与显示器的纵横比匹配的时候,显示效果最佳,图像不会变形。
这也是为什么很多视频在手机上竖屏看的时候,只会集中在中间显示,上下很多部分都是黑屏,
就是因为视频的纵横比在竖屏上的纵横比不匹配,只能缩小在中间那部分显示。
换成横屏观看,视频才能完全展开。
3. 容易混淆的概念
关于纵横比和刷新,有2个概念可能我们平时容易混淆。
3.1. 4:3 和 16:9
这两种纵横比常常被误会成差不多,甚至是一样的,但是细算起来,它们的差距还挺大。
对于4:3 的纵横比意味着图像中每 4 个宽度单位就有 3 个高度单位,
最终显示出来,屏幕宽度比长度增加了 33%。
而16:9 的纵横比意味着图像中每 16 个宽度单位就有 9 个高度单位,
最终显示出来,屏幕宽度比长度增加了 78%。
3.2. 1080i 和 1080p
i 代表隔行扫描,通过照亮屏幕上的奇数像素和偶数像素,然后将它们的结果拼接在一起以获得最终图像,容易闪烁。
p 代表逐行扫描,对图像以逐行平滑的方式拼接,有效防止屏幕闪烁。
1080p比1080i的显示效果更加的清晰和细腻,因为1080p
是后来改进的技术,现在的显示器用 i 的方式已经不多了。
来源:juejin.cn/post/7302268383315148827
关于我在HarmonyOS中越陷越深这件事...
前言
上次发文已是2023年,在上一篇 前端的春天!拥抱HarmonyOS4.0🤗 - 掘金 (juejin.cn)一文中我介绍了一些鸿蒙OS知识,此文一出大家的看法也层出不穷,笔者持开放的态度对待大家对于新生态的看法。在2024年的今天,我想来说说这几个月我有哪些思考和行动。
在短短几个月的时间里,HarmonyOS已经来到了Next版本,迎来属于鸿蒙的春天。俗话说光说不练假把式,实践是试金石,我深知在做开发的这一行只有不断试错,反复的验证,才能创造新的轮子,创造力一个人无法被替代的根本。
我写这篇文章的目的不在于极力推荐大家去学习这项技术,更多的是以一个求学者的角度去阐述自己对新技术学习的心路历程。
为什么学习鸿蒙?
迷茫
笔者是25届的学生,对于学生来说最多的是时间和学习热情,自己也曾经经历过一段时间的专业方向选择困惑期,或许当人越迷茫的时候越容易听信别人的话吧,好与坏是相对的,分人也分时间,在合适的时间选择做了合适的事情这就没有什么问题了,至少学习鸿蒙这件事情对我来说,无论将来何时都会让我记忆犹新。
渴望
在学校里老师会告诉你成熟的解决方案,会告诉你应该这样做,不应该那样做,你仿佛一个机器人,进行一些机械系的学习,时间太急,急到我们只能应付相对的课程考试与学习,内容太多,多到我们最后仅靠老师给出的精简知识点去实际开发项目。这显然不是我想要的学习方式和结果...
动力源泉
有人说:这不就是Vue、React、Flutter、就是个缝合怪....
面对互联网高速发展的今天,各家博采众长,相互吸收优秀的开发思想已不是一件新鲜的事情了。
我自己学习的方向是大前端,加上之前开发的项目都是web的与小程序相关的,自己一直想尝试结合之前开发的项目开发一个基于HarmonyOS的App,听到“一次开发多端部署”这句话让我眼前一亮(很可惜这里的多端部署在4.0的开放版本是不支持的)。在接触鸿蒙的第一天,犹如我第一次接触前端开发,那种所见即所得的开发体验让我从内心里竟有了一丝“自信”,但也恰恰是这种“自信”也逐渐将我推入了深渊
坎坷与前行
在我真正尝试开发一个鸿蒙App的时候是在2023年底,我希望通过我所学的东西去做一个完整的东西并参加 2024年的计算机设计大赛。
在十二月份的那几周,我不断的使用Figma进行原型的绘制,与指导老师探讨功能、确定交互逻辑,期间我也参考了大量的App类设计准则,最后发现鸿蒙的ArkUI是具有工业审美的(至少是符合我的想法),这使得我不必耗费太多的精力在从0到1的去做一些组件,仅需适配设计规范上所涉及到的即可,将更多的精力放在逻辑的完整性。
在开发过程中遇到了各种形形色色的问题,例如:http请求封装upload组件无法拿到回调、地图功能无法使用的解决方案、websocket连接不上、创建时间、地理位置编码......
所幸所有问题都有解决办法,只是过程真的很痛苦,反复尝试、不断验证,我很喜欢在夜晚写代码,天空越黑星星越亮,当空气都变得安静时,我的内心反而会激发一种向上的力量来支撑我,可能是因为自己太想进步了吧(hhhh),在开发的App的日子里,每天都很崩溃,但是我的老师、朋友也都在鼓励我,我又不太想都付出这么多了又轻易放弃......
在2024年的4月,我去看了武汉的樱花,距离比截止还有七天不到,因为我实在撑不住了,在这个时间点,与其逼自己一把,不如放自己一马,于是和朋友相约武汉一起赏樱......
在五月,我得知自己的鸿蒙原生应用拿到了省一等奖,内心是非常激动的,但同时有一些失落的是,我无缘继续参与今年七月的国赛,因为赛制名额原因,我无法被上推。
比赛结束后,我开始准备投递简历实习,但最终都石沉大海,行业现状让我十分焦虑,我时常觉得自己能力不足......
星河璀璨,紧接着HarmonyOS Next 正式面世,我内心不断在问自己,难道我就所有的努力都要止步于此了吗?
....
学习现状
六月我的一位学习伙伴邀请我和他们一起开发研究院的一款基于ArkUI-X的软件,这将对我来说是一个非常宝贵的机会,几个月的时间,兜兜转转回到了我梦开始的地方......
Harmony Next 正式beta发布已过去半个多月了,这期间我了解到了很多之前没有学习到的新东西,鸿蒙提供了3w+的api,这些api有什么用?我打个比方,你要做满汉全席首先得要食材,其次需要烹饪技巧。而鸿蒙他会为你提供所需的所有食材,但是,你要做松鼠桂鱼还是佛跳墙,完全取决于你自己!至于烹饪技巧,鸿蒙开设了相关的做菜视频,你可以从中学习。
笔者也看到了许多鸿蒙原生开发者,一起交流关于鸿蒙的技术问题,在这里我附上一个宝藏鸿蒙优秀案例仓库
HarmonyOS NEXT应用开发案例集
感悟
真正的强大不是对抗,而是允许和接受,接纳挫折,接纳无常,接纳情绪,接纳不同,当你允许一切发生之后,就会不再那么尖锐,会渐渐变得柔和。
Per aspera ad astra. 没有人能熄灭满天星光,鸿蒙让我见证了从星光微微到星河璀璨,它教会我的不是一项技术,更多的是教会我如何去解决问题,去思考问题,当问题没有解决方案的时候,是否自己能够结合现有资源去提出自己的想法,并不断进行验证与总结。
路上会有风
会有浪漫
会有悲伤
会有孤独
也会有无尽的星辰与希望
来源:juejin.cn/post/7390956576180109312
原来Optional用起来这么清爽!
前言
大家好,我是捡田螺的小男孩。
最近在项目中,看到一段很优雅的代码,用Optional 来判空的。我贴出来给大家看看:
//遍历打印 userInfoList
for (UserInfo userInfo : Optional.ofNullable(userInfoList)
.orElse(new ArrayList<>())) {
//print userInfo
}
这段代码因为Optional的存在,优雅了很多,因为userInfoList
可能为null,我们通常的做法,是先判断不为空,再遍历:
if (!CollectionUtils.isEmpty(userInfoList)) {
for (UserInfo userInfo:userInfoList) {
//print userInfo
}
}
显然,Optional让我们的判空更加优雅啦、
- 关注公众号:捡田螺的小男孩(很多后端干货文章)
1. 没有Optional,传统的判空?
如果只有上面这一个例子的话,大家会不会觉得有点意犹未尽呀。那行,田螺哥再来一个。
假设有一个订单信息类,它有个地址属性。
要获取订单地址的城市,会有这样的代码:
String city = orderInfo.getAddress().getCity();
这块代码会有啥问题呢?是的,可能报空指针问题!为了解决空指针问题,一般我们可以这样处理:
if (orderInfo != null) {
Address address = orderInfo.getAddress();
if (address != null) {
String city = address.getCity();
}
}
这种写法显然有点丑陋。为了更加优雅一点,我们可以使用Optional
String city = Optional.ofNullable(orderInfo)
.map(Order::getAddress)
.map(Address::getCity)
.orElseThrow(() ->
new IllegalStateException("OrderInfo or Address is null"));
这样是不是优雅一点,好了这例子也介绍完了。你们知道,田螺哥很细的。当然,是指写文章很细哈
有些伙伴,可能第一眼看那个Optional
优化后的代码有点生疏。因此,接下来,给介绍Optional
相关API
。
2. Optional API简介
2.1 ofNullable(T value)、empty()、of(T value)
因为我们上面的例子,使用到了 Optional.ofNullable(T value)
,第一个函数就讲它啦。源码如下:
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}
如果value
为null,就返回 empty()
,否则返回 of(value)
函数。接下来,我们看Optional的empty()
和 of(value)
函数
public final class Optional<T> {
private static final Optional<?> EMPTY = new Optional<>();
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
显然, empty()
函数的作用就是返回EMPTY
对象。
而of(value)
函数会返回Optional的构造函数
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}
对于 Optional的构造函数:
private Optional(T value) {
this.value = Objects.requireNonNull(value);
}
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
- 当value值为空时,会报
NullPointerException
。 - 当value值不为空时,能正常构造
Optional
对象。
2.2 orElseThrow(Supplier<? extends X> exceptionSupplier)、orElse(T other) 、orElseGet(Supplier<? extends T> other)
上面的例子,我们用到了orElseThrow
.orElseThrow(() -> new IllegalStateException("OrderInfo or Address is null"));
那我们先来介绍一下它吧:
public final class Optional<T> {
private final T value;
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}
很简单就是,如果value
不为null
,就返回value
,否则,抛出函数式exceptionSupplier
的异常。
一般情况,跟orElseThrow
函数功能相似的还有orElse(T other)
和 orElseGet(Supplier<? extends T> other)
public T orElse(T other) {
return value != null ? value : other;
}
对于orElse
,如果value
不为null
,就返回value
,否则返回 other
。
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}
对于orElseGet
,如果value
不为null
,就返回value
,否则返回执行函数式other
后的结果。
2.3 map 和 flatMap
我们上面的例子,使用到了map(Function<? super T, ? extends U> mapper)
Optional.ofNullable(orderInfo)
.map(Order::getAddress)
.map(Address::getCity)
我们先来介绍一下它的:
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Optional.ofNullable(mapper.apply(value));
}
}
public boolean isPresent() {
return value != null;
}
其实这段源码很简答,先是做个空值检查,接着就是value的存在性检查,最后就是应用函数并返回新的
Optional```
跟.map
相似的,还有个flatMap
,如下:
public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Objects.requireNonNull(mapper.apply(value));
}
}
可以发现,它两差别并不是很大,主要就是体现在入参所接受类型不一样。
2.4 isPresent 和ifPresent
我们在使用Optional的过程中呢,有些时候,会使用到isPresent
和ifPresent
,他们有点像,一个就是判断value值是否为空,另外一个就是判断value值是否为空,再去做一些操作。比如:
public void ifPresent(Consumer<? super T> consumer) {
if (value != null)
consumer.accept(value);
}
即判断value值是否为空,然后做一下函数式的操作。
举个例子,这段代码:
if(userInfo!=null){
doBiz(userInfo);
}
用了isPresent
可以优化为:
Optional.ofNullable(userInfo)
.ifPresent(u->{
doBiz(u);
});
优雅永不过时,嘻嘻~
来源:juejin.cn/post/7391065678546386953
我毕业俩月,就被安排设计了公司第一个负载均衡方案,真头大
前言
Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。
今天我想和大家聊一段我自己毕业初期的一段经历,当领导给你安排了无从下手的任务,难以解决的技术问题时,该怎么办?
本文会从我的一段经历出发,分析在初入职场、刚刚成为一个程序的时候,遇到自己无解决的问题,和自己的一些解决问题的思考。
如果你工作不满三年,一定要往下看,相信一定会对你有帮助。但如果你已经工作3年以上,那么你应该不会遇到下面的问题啦,但也欢迎你往下看,也许会有不同的收获。
先讲故事
每个人都有自己的职场新人期,这个阶段你会敏感、迷茫。
记得在毕业的2个月后,我的组长给我布置了一个任务,实现服务的热部署。
热部署是什么?
热部署是一种技术,它允许在应用程序运行时不中断地更新或替换软件组件,如代码或配置。这种技术主要应用于Web应用程序和分布式系统中,旨在减少停机时间,提高开发效率,并增强应用程序的可用性。
那时候我们的服务器是单实例,每次升级,一定要先停掉服务,替换war包,然后重启tomcat。想升级,必须要在半夜客户下班的时候升级。
作为一个职场小白,技术小白,第一次接到这种任务的时候,我整个人是一脸懵逼的。
那时候报错的堆栈信息我都看不懂,还得请教组里的同事,CRUD还没整明白呢。就要去解决这个架构问题,可想而知我那时候有多崩溃。
其实选择让我一个毕业2个月的应届生来做,能发现两个基本的背景
- 公司没有标准化的解决方案,没有人知道你的解决方案是否对错。
- 包括我的组长、组内老同事在内,也没有人了解如何在生产环境实现热部署,没人可以提供帮助。
因此在这件事的起步阶段,非常的困难。最重要的是,我还不会调研行业的标准方案是什么,刚毕业的我只能埋头苦干,不断试错。
持续的没有进展,也让我内心非常焦虑。多次周会上被问及这件事情,也完全不知道如何汇报进展,因为我自己也不知道,到底什么时候能解决。
记得那时候上厕所、接水的时候,我都是低头不语,装作看不见,生怕他问我一句:“那件事情的进度怎么样了?”
现在想起自己那时候的无助和不安,我都会微微一笑。
很庆幸是在计算机行业,即使是用百度,也能够搜到大把的信息,那段时间也了解到了像Nginx、Redis这样的中间件,最终也是用Nginx,实现了最基本的目标,可以在保证客户使用的情况下,正常的升级我们的系统了。
是的,毕业后的第一家公司的负载均衡和不停机服务发布,是由一个小小毕业生来做的,前期内心有多煎熬,上线的那一刻就有多自豪。
故事讲完了,我相信很多职场人,都会面临这样的场景吧,或许是一个功能不知道怎么去实现,又或者一个方案不知道如何去设计,不敢面对,不敢说出来。
为什么会这样
在职场中,我们大概可以用四个阶段概括
- 新人期
- 发展期
- 成熟期
- 衰退期
上面我自己的一小段经历,遇到了新人期中最容易面临的问题,技能上的迷茫,和职场上的迷茫。
技能上的迷茫
我们可能发现自己有好多东西不会,什么都要学,却不知道自己该如何入手。
在大厂里,周围人看起来都是大牛,可以看着他们侃侃而谈,虽然自己只能茫然四顾,但起码有着标杆和榜样。
而在普通传统行业,身边连个会的人都没有,自己的摸索更像是盲人摸象。
职场上的迷茫
不知道事情该办成什么样,领导会不会不满意,对我的考核会不会有影响。
又或者主动说了有困难,领导会不会觉得我能力不足?
面对多重困难,我该如何汇报自己的进度?没有进度怎么办。
思考、建立正确的认知
界定问题,比问题本身要重要
技术日新月异,新技术是学不完的,解决问题的方式更是多种多样的。所有成熟的方案,都不是一蹴而就的,而是通过发现问题、解决问题一步步优化完善来的。
所以我们有一个清晰的认知,我们很难得到最优的解决方案,而是把注意力放在问题本身,看看我们到底要解决什么问题。
例如我一开始不知道热部署的含义,但我知道的是,公司想解决服务的不停机升级问题。那么我最终引入了Nginx,通过反向代理,部署多个服务就好了。提供的解决方案,完成了预期,我认为对于一个新人来讲就是合格的。
PS:事实上我在使用了Nginx完成了负载均衡后,我的组长曾和我说,你这个方案感觉不太行,不是领导想要的效果。但点到为止了,也没有什么指导,或者改进措施,最终也用这个方案在生产去部署。
注重当下
职场规则是明确的,可以在员工手册、职级要求中看到。但职场中的潜规则是不明确的,你无法摸清领导最真实的想法。
为什么这么说呢?
- 领导可能就是想历练你,给你有挑战的事情,想看看你做到什么程度,你的能力边界在哪里
- 领导对你的预期就是能够解决,也不需要他的指导
不同的想法,对你的要求截然不同。
历练你的时候,即使事情没有结果,领导也不会多说什么,或许这就是一个当下受限于公司运维、资源而难以做到的事情。
但领导相信你能够解决问题的时候,你必须要尽量完成,不然确实会影响到对你的一些看法。
对于职场新手期,你短时间内是无法建立一个良好的向上沟通渠道的(如何建立向上沟通渠道,我们后面再说)。所以对你而言,与其揣摩想法, 不如界定好问题,然后注重当下,解决好遇到的每一个困难。
两个方法
目标拆分
有一个关于程序员的经典段子:这个工作已经做完了80%,剩下的**20%**还要用和前面的一样时间。
遇到无从下手,不知如何去解决的问题,是怎么给出一个可执行的分解。
重点来了,拆分的,一定要可执行。每个人对于可执行的区分,是有很大不同的。不同的地方在于可执行的定义,你是否能清楚地知道这个问题该如何解决。
比如文章开头的故事,如果时间回到那一刻,我会这么列出计划
- 了解什么是热部署,方案调研
- 了解Nginx是什么,学会使用基本的命令
- 多个服务之间如何同步信息,比如上一秒在A服务,下一秒在B服务
- 进行验证,线上部署
学会借力
手里有把锤子,看见什么都是钉子。
我们更倾向于用我们手里的某种工具,去解决所有问题。
在遇到问题时,我们的大脑会根据以往经验做出预判,从而形成思维定势,使得我们倾向于使用熟悉的工具或方法来解决问题。然而,并不是所有问题都是钉子,一旦碰到超出我们经验边界的事情,我们可能会束手无策。
还是分享一段经历:
在字节的时候,我遇到过一个问题场景,就是如何保证集群服务器的本地缓存,保持一致。
直白来讲,我要开一个500人的会议,我准备了500份资料,那么在资料可能会修改的情况下,如何保证大家手里资料,都是最新的?
加载、更新,我能想到的就是用消息队列广播,系统收到消息的时候,每一台服务器都走一遍加载逻辑。
但有一个问题我解决不了,几百台服务器同时请求,如何解决突发的压力问题呢?如果500个人同时去找会议组织人打印,排队不说了,打印机得忙冒烟。
我给身边的一个技术大拿说了我的问题,他说,你可以用公司的一个中间件啊,数据只需要一次加载,然后服务器去下载就好了。是哈,用一个超级打印机打印出500份,分发下去就好了,不用每个人亲自来取呀。
就是几句点拨,同步给我了这一个信息,我了解了一下这个中间件,完美解决了我的问题。
因此,学会借力。找到公司大佬,找到网上的大佬,买杯咖啡、发个红包,直接了当的说出你的问题,咨询解决方案,从更高层次,降维打击你的问题。
当然,一定要在自己思考完、没有结果的情况下,再去请教,能够自己研究明白的,比如使用相关的,不要去麻烦别人。
说在最后
文章到这里就要结束啦,很感谢你能看到最后。
职场初期遇到无从下手的任务时,我们应该建立正确的认知,界定问题,并注重当下。
也分享给你了两个行之有效的方法,目标拆分和学会借力,当然,这一切都离不开你的行动和坚持,这不是方法,而是一个技术人最基本的素质,所以就不多说啦。
不知道你在职场中初期遇到无从下手的问题时,你是怎么处理的呢,欢迎你在评论区和我分享,也希望你点赞、评论、收藏,让我知道对你有所收获,这对我来说很重要。也欢迎你加我的wx:Ldhrlhy10,让我做你的垫脚石,帮你解决你遇到的问题呀,欢迎一起交流~
来源:juejin.cn/post/7362905254725648438
失业的七个月,失去了很多很多,一个普通的不能再普通的人的年中总结
开篇
这不是一篇技术的文章。
第一阶段 裸辞后的两个半月
介绍下自己的情况,坐标上海,双非院校前端打工人,目前是有三年的工作经验,在23年的年底裸辞了,有一个女朋友,本来异地,她在22年10月来到了上海,选择相信我。
在刚刚裸辞的时候,我信心满满的在各个平台投递着简历,给自己做了一个规划:“先投外包的,投小公司来练手,最后一鸣惊人进入大厂,走向人生巅峰!”当时已经可以在各大论坛上看到,前端已死啊之类的标题,但从实际感受来说,好像并没有那么夸张,程序员还是比较高薪的职业。已读不回是挺常见的,但实际面试还是比较多的,并不像大家在网上说的感觉前端开发都要找不到工作了。在这三个月里,我BOSS上沟通了400个人左右,面试的有11家,其中有5个外包。这样的情况在现在的我看来真有点属于暴殄天物了,只沟通了400多个公司就约了11家面试,后来的我才知道这就是我找工作最顺利的阶段,也是我最浪费机会的阶段。由于没有一个正确的认知,以及一个具体的规划和行动,我面试没有尽力去准备,再加上父亲住院,经常往医院跑,一来一回基本一天就没了,11家面试只有一家进入了二轮复面,甚至没有一家进入到谈薪阶段。可当时的我始终是抱着一个比较乐观的态度,还会天天和我的女朋友吐槽一些公司,从来没有认真的找过自己的问题,天真的认为只要我好好准备一下,就一定可以拿下offer,还和她保证我过年前一定能找到工作!她也十分信任我,可后续的发展就愈发的不可控了。
第二阶段 字节面试一轮结束 失业的第四个月
因为之前的面试都没有一个很好的反馈,渐渐的我有点开始着急,也已经渐渐处于摆烂的状态投递简历,就是海投,不看公司介绍,只要是个公司我就打个招呼,有回复我就发个简历,以至于在投递简历的过程中,都不知道何时自己投了字节的一个岗位,然后还约到了面试,我一时间来了信心,约了两周后的字节一轮面试。这两周我没有打开招聘软件,处于一个孤注一掷的状态,实际上就是抱着侥幸心理想碰碰运气,这期间我看了一周的技术课程,浅浅的背了一些八股文,但并没有对之前所有面试的经历进行一个总结,找找自己的问题。之后时间很快就到了面试的当天,这天我突然感觉自己差的东西有很多,但还是硬着头皮去面了,结果可想而知,一轮没有通过。我当时非常沮丧,给女朋友打了一个电话说自己没有通过,她还是非常鼓励我,不给我压力。她工作也很忙,基本就是早上8点到晚上11点的上班强度,但还是一直很有耐心的和我说没事,没有工作也没事,等你想找工作的时候再找也来得及。实际这个时候我就应该醒悟了,自己并不是什么技术大牛,只是一个顶着一个前端工程师光环的cv工程师,一个凭借一些错误的自我认知,就觉得自己未来一定会更好的愚蠢、不可理喻的愚蠢的人,况且我已经不算年轻,是一个快要奔三的大龄青年了,还抱着这种天真,不成熟的想法,但这也是后话了,当时的我还是挫败了几天后继续着躺平的生活,还大言不惭的和我的女朋友说着我三四月份一定可以找到一份合适的工作,做出承诺,但却没有匹配承诺的态度和行为,这时候的女朋友已经对我有了一些意见,但她没有明确的和我说,怕给我压力,只是感觉她的工作状态逐渐不对了,疲惫和内耗占据了她大部分空闲时间,她第一次跟我说了,要不试试别的岗位吧,前端的工作这么不好找,不如换个方向,但这句话我没有放在心上。因为报了健身班,我的健身教练已经开始为我着急了,他说要不来干健身教练吧,我带你入门,我也只当是玩笑话了。
第三阶段 裸辞后的六月半
不出意外的,在金三银四过去之后,我仍然没有找到工作,面试机会也几乎没有了,一个月可能有一两个外包的面试,但自己因为不想考虑外包,在某一家外包面完挫败感的驱使下,屏蔽了BOSS上大多数的外包公司,自此我的BOSS上再也没有一点水花。后来在我认为是缘分的加持下,我的父母和她的父母在上海碰到了,然后顺利的吃了饭,见了面,我爸妈给了我的女朋友见面礼一万零一的红包,寓意万里挑一。我的父母是很普通的农民,一辈子都跟种地打交道,不善言辞,一整个饭局,基本就是我的哥哥和她的爸妈和她在聊天,我在饭桌上也一言不发,可能是怕她的父母问及我工作的问题,也可能是因为哥哥是个很优秀的人,他对于我的期望很高,每次他在场的时候我都会避免开口,以免被抓到会被教育的点。总之饭局也很快就结束了,她的父母对我也没有什么过分的要求,也没有着急让我找工作,说着安慰我的话,现在市场环境不好,找不到工作很正常,利用这段时间学习,以后还有很多机会,你还年轻呢,不要害怕。现在的我几乎是流着眼泪打出当时阿姨跟我说的话,她们一家都是很讲道理,也会为别人考虑的人,但我还是辜负了他们的信任,让他们失望了。这期间面试机会少之又少,我这时候已经渐渐由摆烂,变得慌张的不知所措了,现在发生的情况我之前完全没有考虑到,随着存款一点一点的被社保、房租、吃喝消耗,女朋友的状态也愈发变得消极,我内心陷入了很痛苦的境地。但这并没有让我去总结面试的问题,只是一味的投着简历,看着八股文,等待面试。在六月初,女朋友第二次和我商量,如果前端的工作不好找就换个别的干干吧,这时我已经有点想逃避社会了,想要避免外出,除了每天去健身房,剩下时间几乎都呆在小小的出租屋房间。但我还是答应了她,在6月15号之后我就找其他岗位的工作。接下来几天,有了两个面试,还有原来公司的人找我想让我帮忙做个项目,因为甲方报价还没有,所以这个事情就相当于没有后续,但实际我还是心存幻想,想走更平坦的路。接着时间很快就到了6月13号,她问我计划还实行吗,我说当然实行啊,不过想等一下这个项目的事情,她第一次发泄了自己的情绪,说自己要等到什么时候啊!6月14号,她说她好累,不想等了,想分手。晚上沟通中我没有任何的话可以讲出口,一直沉默,气氛一直很凝重,6月15日,我整理了一些我的问题,想和她聊聊,她上班回来之后很平静的和我说了分手,说了本身对我的期望也没有那么高,说只需要有一个工作,或者我努力的心就可以。我说了我的很多问题,保证我自己会改,但为时已晚,因为她已经给我了足够的时间,但我没有珍惜。她把礼金退给了我,我搬了出去,故事在这就画了句号。
现在
痛苦总是后知后觉,并且悄悄击碎你的防线。父亲身体愈发不好,在知道我分手之后,去医院检查身体的时候晕倒了,进入急诊的那一刻,看着急诊中的病人,看着意识模糊的父亲,我的世界一下就崩塌了。好在父亲没有大碍,但精神状态不是很好。我没有一个最坏的预期,并且时刻都在回避问题,不敢直面自己,承认自己的弱小以及愚蠢,还渐渐看不到身边人做出的牺牲和努力。我浑浑噩噩的过着这几天,这几天是我第一次正面的和哥哥进行了我认为的平等的沟通,我第一次很认真的听他讲话,不再排斥,也第一次觉得他说的其实都对,他的话第一次在我眼里不是说教,而是关心和爱护。我发现了自己的很多问题,他还教会了我一个人生态度:凡事抱最大的希望,尽最大的努力,做最坏的打算。我的世界开始慢慢重建起来,我发现了自己之前的工作内容很简单,基于vue2、antdv、Echarts的后台管理系统,图表,小程序。我发现我没有规划,总是走一步看一步,并且还带着莫名的自信。我发现我辜负了很多人的期待,让自己也很失望。我发现了很多很多,我不知道这次裸辞还会持续的给我带来什么影响,但生活总是要继续,一手好牌最终打的稀烂,但已经没有时间让我继续消沉下去,2024年有可能是我这一生都不会忘记的。
结语
这七个月,让我失去了很多很多,失去了工作,失去了信任,失去了挚爱,失去了···这篇文章主要目的不是为了卖惨,或者贩卖焦虑,只是一个普通的不能再普通的人的一段时间的总结,是对可能已经逝去爱情的怀念。未来还会有很多很多意外的事情发生,我需要做的是积极的去面对,能做的是适应环境,找自己的不足,及时补救,不要一切都来不及的时候才后悔,才反思。我想要和林克一样,忍受孤独,努力变强,去找到她。
来源:juejin.cn/post/7382892371224461352
教你做事,uniapp ios App 打包全流程
背景
使用uniapp 开发App端,开发完成后,ios端我们需要上架到App Store,在此之前,我们需要将App先进行打包。
在HubilderX中,打包ios App我们需要四个东西,分别是:
- Bundle ID
- 证书私钥密码
- 证书私钥文件
- 证书profile文件
下面,我将一步步讲解,如何获取以上文件。
加入苹果开发者
- 使用iPhone或iPad 在App Store 下载 Apple Developer
- 进入App
- 点击底部【账户】
- 点击立即注册
- 填写资料(填写的信息要与你的苹果账号对应,因为这个App需要双重认证)
- 填完信息和资料后点击订阅
- 付费(需要给你的手机添加付款方式)
- 付费成功
- 成功加入苹果开发者计划
生成p12证书和证书私钥密码
步骤:CSR文件 ➡️ cer文件 ➡️ p12文件
- 进入Apple Developer官网,登录成功后,点击顶部导航栏的【账户】,在【账户】页面点击【证书】
- 进入到【Certificates, Identifiers & Profiles】页面,点击+号,开始注册证书
- 选择【iOS Distribution (App Store and Ad Hoc)】再点击【Continue】
- 上传证书签名(CSR文件) 下面会教大家如何生成CSR文件:
- 打开Mac上的【钥匙串访问】App
- 依次选择App顶上菜单栏的【钥匙串访问】➡️【证书助理】➡️【从证书颁发机构请求证书…】
- 打开弹窗,填写两个邮件、常用名称,选择存储到磁盘,点击【继续】
- 存储到桌面,得到【CSR文件】
- 回到网页,选择并上传刚刚生成的【CSR文件】,点击【Continue】
- 到这里【cer文件】就生成好了,点击【Download】下载到桌面
- 得到【cer文件】
接下来我们要根据这个【cer文件】导出生成为【p12文件】
- 双击打开【cer文件】,Mac会自动打开【钥匙串访问】,选中左侧登录 ➡️ 我的证书 ➡️ 证书文件,找到这个【cer证书】
- 此时证书是未受信任,双击该证书,在弹窗中展开【信任】,选择【始终信任】,然后关闭输入密码保存,证书就改成受信任了
- 右键选中该证书,在菜单中选择【导出】
- 输入密码,即【证书私钥密码】(该密码就是HbuilderX发行打包App时,填写的【证书私钥密码】),之后再输入电脑密码
- 最终得到【p12证书】
生成Bundle ID
- 回到页面(Certificates, Identifiers & Profiles),选择【Identifiers】,点击+号
- 选择【App IDs】,点击【Continue】
- 选择【App】,点击【Continue】
- 填写描述和Bundle ID,ID格式如:com.domainname.appname
- 下面的功能如果有需要的话,需要勾选上
- 比如你的App需要Apple登录的话,则需要勾选【Sign In with Apple】
- 设置完成后,点击右上角的【Continue】,【Bundle ID】就生成好了
生成profile文件
- 回到页面(Certificates, Identifiers & Profiles),选择【Profiles】,点击+号
- 选择【App Store】,点击【Continue】
- 选择上一步生成的【身份标识】,点击【Continue】
- 选择第一步生成的【Certificates证书】,点击【Continue】
- 设置【配置文件名称】,点击【Generate】生成
- 点击【Download】下载【profile文件】
- 得到【profile文件】
到这里,【Bundle ID】、【p12文件】【证书私钥密码】、【profile文件】就生成好了,可以去HbuilderX打包ios App了
HbuilderX 打包ios App
- 填入配置和文件
- 点击【打包】,即可生成App
到这一步,iOS App就生成好了。
来源:juejin.cn/post/7264939254290579495
程序员的这10个坏习惯,你中了几个?超过一半要小心了
前言
一些持续关注过我的朋友大部分都来源于我的一些资源分享和一篇万字泣血斩副业的劝诫文,但今年年后开始我有将近4个月没有再更新过。
有加过我好友的朋友私聊我问过,有些回复了有些没回复。
想通过这篇文章顺便说明一下个人的情况,主要是给大家的一些中肯的建议。
我的身体
今年年前公司福利发放的每人一次免费体检,我查出了高密度脂蛋白偏低,因为其他项大体正常,当时也没有太在意。
但过完年后的第一个月,我有一次下午上班忽然眩晕,然后犯恶心,浑身发软冒冷汗,持续了好一阵才消停。
当时我第一感觉就是颈椎出问题了?毕竟这是程序员常见的职业病。
然后在妻子陪伴下去医院的神经内科检查了,结果一切正常。
然后又去拍了片子看颈椎什么问题,显示第三节和第四节有轻微的增生,医生说其实没什么,不少从事电脑工作的人都有,不算是颈椎有大问题。
我人傻了,那我这症状是什么意思。
医生又建议我去查下血,查完后诊断出是血脂偏高,医生说要赶紧开始调理身体了,否则会引发更多如冠心病、动脉粥样硬化、心脑血管疾病等等。
我听的心惊胆战,没想到我才34岁就会得上老年病。
接下来我开始调理自己的作息和生活,放弃一些不该强求的,也包括工作之余更新博客,分享代码样例等等。
4个月的时间,我在没有刻意减肥的情况下体重从原先152减到了140,整个人也清爽了许多,精力恢复了不少。
所以最近又开始主动更新了,本来是总结了程序员的10个工作中的不良习惯。
但想到自己的情况,决定缩减成5个,另外5个改为程序员生活中的不良习惯,希望能对大家有警示的作用。
不良习惯
1、工作
1)、拖延症
不到最后一天交差,我没有压力,绝不提前完成任务,从上学时完成作业就是这样,现在上班了,还是这样,我就是我,改不了了。
2)、忽视代码可读性
别跟我谈代码注释,多写一个字我认你做die,别跟我谈命名规范,就用汉语拼音,怎样?其他人读不懂,关我什么事?
3)、忽视测试
我写一个单元测试就给我以后涨100退休金,那我就写,否则免谈。接口有问题你前端跟我说就行了发什么脾气,前后端联调不就这样吗?!
4)、孤立自己
团队合作不存在的,我就是不合群的那个,那年我双手插兜,全公司没有一个对手。
5)、盲目追求技术新潮
晚上下班了,吃完饭打开了某某网,看着课程列表中十几个没学完的课程陷入了沉默,但是首页又出现了一门新课,看起来好流行好厉害耶,嗯,先买下来,徐徐图之。
2、生活
1)、缺乏锻炼和运动
工作了一天,还加班,好累,但还是得锻炼,先吃完饭吧,嗯,看看综艺节目吧,嗯,再看看动漫吧,嗯,还得学习一下新技术吧,嗯,是手是得洗澡了,嗯,还要洗衣服,咦,好像忘记了什么重要的事情?算了,躺床上看看《我家娘子不对劲》慢慢入睡。
2)、加班依赖症
看看头条,翻翻掘金,瞅瞅星球,点点订阅号,好了,开始工作吧,好累,喝口水,上个厕所,去外面走走,回来了继续,好像十一点半了,快中午了,待会儿吃什么呢?
午睡醒了,继续干吧,看看头条,翻翻掘金,瞅瞅星球,点点订阅号,好了,开始工作吧,好累,喝口水,上个厕所,去外面走走,回来了继续,好像5点半了,快下班了,任务没完成。
算了,加加班,争取8点之前搞定。
呼~搞定了,走人,咦,10点了。
3)、忽视饮食健康
早上外卖,中午外卖,晚上外卖,哇好丰富耶,美团在手,简直就是舌尖上的中国,晚上再来个韩式炸鸡?嗯,来个韩式甜辣酱+奶香芝士酱,今晚战个痛快!
4)、缺乏社交活动
好烦啊,又要参加公司聚会,聚什么餐,还不是高级外卖,说不定帮厨今天被大厨叼了心情不好吐了不少唾沫在里面,还用上完厕所摸了那里没洗的手索性搅了一遍,最后在角落里默默看着你们吃。
吃完饭还要去KTV?继续喝,喝不死你们,另外你们唱得很好听吗?还不是看谁嗷的厉害!
谁都别跟我说话,尤其是领导,离我越远越好,唉,好想回去,这个点B站该更新了吧,真想早点看up主们嘲讽EDG。
5)、没有女朋友
张三:我不是不想谈恋爱,是没人看得上我啊,我也不好意思硬追,我也要点脸啊,现在的女孩都肿么了?一点暗示都不给了?成天猜猜猜,我猜你MLGB的。
李四:家里又打电话了,问在外面有女朋友了没,我好烦啊,我怎么有啊,我SpringCloudAlibaba都没学会,我怎么有?现在刚毕业的都会k8s了,我不学习了?不学习怎么跳槽,不跳槽工资怎么翻倍,不翻倍怎么买房,不买房怎么找媳妇?
王五:亲朋好友介绍好多个了,都能凑两桌麻将了,我还是没谈好,眼看着要30了,我能咋整啊,我瞅她啊。破罐破摔吧,大不了一个人过呗,多攒点钱以后养老,年轻玩个痛快,老了早点死也不亏,又不用买房买车结婚受气还得养娃,多好啊,以后两脚一蹬我还管谁是谁?
总结
5个工作坏习惯,5个生活坏习惯,送给我亲爱的程序员们,如果你占了一半,真得注意点了,别给自己找借口,你不会对不起别人,只是对不起自己。
喜欢的小伙伴们,麻烦点个赞,点个关注,也可以收藏下,以后没事儿翻出来看看哈。
来源:juejin.cn/post/7269375465319415867
你的 Flutter 项目异常太多是因为代码没有这样写
以前在团队里 Review 过无数代码,也算是阅码无数的人了,解决过的线上异常也数不胜数,故对如何写出健状性的代码有一些微小的见解。刚好最近闲来得空(其实是拖延症晚期)把之前躺在备忘录里的一些小记整理了一下,希望能对你有一些启发。
Uri 对象的使用
在 Dart 语言中 Uri 类用于表示 URIs(网络地址、文件地址或者路由),它内部能自动处理地址的模式与的百分号编解码。在实际开发过程我们经常直接使用字符串进行拼接 URIs,然而这种方式会给地址的高级处理带来不便甚至隐式异常。
/// 假设当前代码为一个内部系统,[outsideInput] 变量是外部系统传入内部的字符串变量
/// 你无法限定 [outsideInput] 的内容,如果变量包含非法字符(如中文),整个地址非法
final someAddress = 'https://www.special.com/?a=${outsideInput}';
/// 为了保持 URI 的完整性你可能会这样做
final someAddress = 'https://www.special.com/?a=${Uri.decodeFull(outsideInput)}';
/// 如有多个外部输入变量你又需要这样做
final someAddress = 'https://www.special.com/?a=${Uri.encodeFull(outsideInput)}&b=${Uri.encodeFull(outsideInput1)}&c=${Uri.encodeFull(outsideInput2)}';
直接使用字符串来拼接 URI 地址会带来非常多的限制,你需要关注地址中拼接的每个部分的合法性,并且在处理复杂逻辑时需要更冗长为的处理。
/// 如果我们需要在另一个系统中对 [someAddress] 地址的参数按条件进行添加
if (conditionA) {
someAddress = 'https://www.special.com/?a=${Uri.encodeFull(outsideInput)}';
} else if (conditionB) {
someAddress = 'https://www.special.com/?a=${Uri.encodeFull(outsideInput)}&b=${otherVariable}';
} else {
someAddress = 'https://www.special.com';
}
如果使用 Uri 可以简化绝大多数对 URIs 的处理,同时限定类型对外部有更明确的确定性,因此针对 URIs 需要做如下约定:
在任何系统中都不应直接拼接 URIs 字符串,应当构造 URI 对象作为参数或返回值。
/// 生成 Uri 对象
final someAddress = Uri(path: 'some/sys/path/loc', queryParameters: {
'a': '${outsideInput}', // 非法参数将自动百分号编码
'b': '${outsideInput1}', // 不用对每个参数单独进行编码
if (conditionA) 'c': '${outsideInput2}', // 条件参数更为简洁
});
类型转换
Dart 中可以使用 is
进行类型判断,as
进行类型转换。 同时,使用 is
进行类型判断成功后会进行隐性的类型转换。示例如下:
class Animal {
void eat(String food) {
print('eat $food');
}
}
class Bird extends Animal {
void fly() {
print('flying');
}
}
void main() {
Object animal = Bird();
if (animal is Bird) {
animal.fly(); // 隐式类型转换
}
(animal as Animal).eat('meat'); // 强制类型转换一旦失败就会抛异常
}
由于隐式的类型转换存在,is
可以充当 as
的功能,同时 as
进行类型失败会抛出异常。
所以日常开发中建议使用 is
而不是 as
来进行类型转换。 is
运算符允许更安全地进行类型检查,如果转换失败,也不会抛出异常。
void main() {
dynamic animal = Bird();
if (animal is Bird) {
animal.fly();
} else {
print('转换失败');
}
}
List 使用
collection package 的使用
List 作为 Dart 中的基础对象使用广范,由于其本身的特殊性,如使用不当极易导致异常,从而影响业务逻辑。典型示例如下:
List<int> list = [];
// 当 List 为空时访问其 first 会抛异常
list.first
// 同理访问 last 也会抛异常
list.last
// 查找对象时没有提供 orElse 也会抛异常
list.firstWhere((t) => t > 0);
// List 对象其它会抛异常的访问还有
list.single
list.lastWhere((t) => t > 0)
list.singleWhere((t) => t > 0)
所以如果没有前置判断条件,所有对 List 的访问均需替换为 collection
里对应的方法。
import 'package:collection/collection.dart';
List<int> list = [];
list.firstOrNull;
list.lastOrNull;
list.firstWhereOrNull((t) => t > 0);
list.singleOrNull;
list.lastWhereOrNull((t) => t > 0);
list.singleWhereOrNull((t) => t > 0);
取元素越界
在 Dart 开发时,碰到数组越界或者访问数组中不存在的元素情况时,会导致运行时错误,如:
List<int> numbers = [0, 1, 2];
print(numbers[3]); // RangeError (index): Index out of range: index should be less than 3: 3
你可以使用使用 try-catch
来捕获异常,但由于数组取值这样的基础操作往往遍布在项目的各个角落,try-catch
在这样的情况下使用起来会比较繁琐,而且并不是所有的取值都会导致异常,所以往往越界的问题只有真正出现了才发现。
好在,我们可以封装一个 extension
来简化数组越界的问题:
extension SafeGetList<T> on List<T> {
T? tryGet(int index) =>
index < 0 || index >= this.length ? null : this[index];
}
使用时:
final list = <int>[];
final single = list.tryGet(0) ?? 0;
由于 tryGet
返回值类型为可空(T?
) ,外部接收时需要进行空判断或者赋默认值,这相当于强迫开发者去思考值不存在的情况,如此减少了异常发生的可能,同时在业务上也更加严谨。
当然还有另一种方案,可以继承一个 ListMixin
的自定义类:SafeList
,其代码如下:
class SafeList<T> extends ListMixin<T> {
final List<T?> _rawList;
final T defaultValue;
final T absentValue;
SafeList({
required this.defaultValue,
required this.absentValue,
List<T>? initList,
}) : _rawList = List.from(initList ?? []);
@override
T operator [](int index) => index < _rawList.length ? _rawList[index] ?? defaultValue : absentValue;
@override
void operator []=(int index, T value) {
if (_rawList.length == index) {
_rawList.add(value);
} else {
_rawList[index] = value;
}
}
@override
int get length => _rawList.length;
@override
T get first => _rawList.isNotEmpty ? _rawList.first ?? defaultValue : absentValue;
@override
T get last => _rawList.isNotEmpty ? _rawList.last ?? defaultValue : absentValue;
@override
set length(int newValue) {
_rawList.length = newValue;
}
}
使用:
final list = SafeList(defaultValue: 0, absentValue: 100, initList: [1,2,3]);
print(list[0]); // 正常输出: 1
print(list[3]); // 越界,输出缺省值: 100
list.length = 101;
print(list[100]); // 改变数组长度了,输出默认值: 0
以上两种方案均可以解决越界的问题,第一个方案更简洁,第二个方案略复杂且侵略性也更强但好处是可以统一默认值、缺省值,具体使用哪种取决于你的场景。
ChangeNotifier 使用
ChangeNotifier 的属性访问或方法调用
ChangeNotifier
及其子类在 dispose
之后将不可使用,dispose
后访问其属性(hasListener
)或方法(notifyListeners
)时均不合法,在 Debug 模式下会触发断言异常;
// ChangeNotifier 源码
bool get hasListeners {
// 访问属性时会进行断言检查
assert(ChangeNotifier.debugAssertNotDisposed(this));
return _count > 0;
}
void dispose() {
assert(ChangeNotifier.debugAssertNotDisposed(this));
assert(() {
// dispose 后会设置此标志位
_debugDisposed = true;
return true;
}());
_listeners = _emptyListeners;
_count = 0;
}
static bool debugAssertNotDisposed(ChangeNotifier notifier) {
assert(() {
if (notifier._debugDisposed) { // 断言检查是否 dispose
throw FlutterError(
'A ${notifier.runtimeType} was used after being disposed.\n'
'Once you have called dispose() on a ${notifier.runtimeType}, it '
'can no longer be used.',
);
}
return true;
}());
return true;
}
在 dispose
后访问属性或调用方法通常出现在异步调用的场景下,由其是在网络请求之后刷新界面。典型场景如下:
class PageNotifier extends ChangeNotifier {
dynamic pageData;
Future<voud> beginRefresh() async {
final response = await API.getPageContent();
if (!response.success) return;
pageData = response.data;
// 接口返回之后此实例可能被 dispose,从而导致异常
notifyListeners();
}
}
为使代码逻辑更加严谨,增强整个代码的健状性:
ChangeNotifier
在有异步的场景情况下,所有对 ChangeNotifier
属性及方法的访问都需要进行是否 dispose
的判断。
你可能会想到加一个 hasListeners
判断:
class PageNotifier extends ChangeNotifier {
dynamic pageData;
Future<voud> beginRefresh() async {
final response = await API.getPageContent();
if (!response.success) return;
pageData = response.data;
// Debug 模式下 hasListeners 依然可能会抛异常
if (hasListeners) notifyListeners();
}
}
如上所述 hasListeners
内部仍然会进行是否 dispose
的断言判断,所以 hasListeners
仍然不安全。
因此正确的做法是:
// 统一定义如下 mixin
mixin Disposed on ChangeNotifier {
bool _disposed = false;
bool get hasListeners {
if (_disposed) return false;
return super.hasListeners;
}
@override
void notifyListeners() {
if (_disposed) return;
super.notifyListeners();
}
@override
void dispose() {
_disposed = true;
super.dispose();
}
}
// 在必要的 ChangeNotifier 子类混入 Disposed
class PageNotifier extends ChangeNotifier with Disposed {
Future<voud> beginRefresh() async {
final response = await API.getPageContent();
if (!response.success) return;
pageData = response.data;
// 异步调用不会异常
notifyListeners();
}
}
ChangeNotifier 禁止实例复用
ChangeNotifier
在各种状态管理模式中一般都用于承载业务逻辑,初入 Flutter 的开发者会受原生开发的思维模式影响可能会将 ChangeNotifier
实例进行跨组件复用。典型的使用场景是购物车,购物车有加/减商品、数量管理、折扣管理、优惠计算等复杂逻辑,将 ChangeNotifier
单个实例复用甚至单例化能提高编码效率。
但单个 ChangeNotifier
实例在多个独立的组件或页面中使用会造成潜在的问题:复用的实例一旦在某个组件中被意外 dispose
之后就无法使用,从而影响其它组件展示逻辑并且这种影响是全局的。
@override
void initState() {
super.initState();
// 添加监听
ShoppingCart.instance.addListener(_update);
}
@override
void dispose() {
// 正确移除监听
ShoppingCart.instance.removeListener(_update);
// 如果哪个实习生不小心在组件中这样移除监听,将产生致命影响
// ShoppingCart.instance.dispose();
super.dispose();
}
因此在 Flutter 开发中应禁止 ChangeNotifier
实例对外跨组件直接复用,如需跨组件复用应借助provider
、get_it
等框架将 ChangeNotifer
子类实例对象置于顶层;
void main() {
runApp(
MultiProvider(
providers: [
Provider<Something>.value(ShoppingCart.instance),
],
child: const MyApp(),
)
);
}
如果你非得要 「单例化」 自定义 ChangeNotifier
子类实例,记得一定要重新 dispose
函数。
Controller 使用
在 Flutter 中大多数 Controller
都直接或间接继承自 ChangeNotifier
。为使代码逻辑更加严谨,增强整个代码的健状性,建议:
所有 Controller
需要显式调用 dispose
方法,所有自定义 Controller
需要重写或者添加 dispose
方法。
// ScrollController 源码
class ScrollController extends ChangeNotifier {
//...
}
// 自定义 Controller 需要添加 dispose 方法
class MyScrollController {
ScrollController scroll = ScrollController();
// 添加 dispose 方法
void dispose() {
scroll.dispose();
}
}
ChangeNotifierProvider 使用
ChangeNotifierProvider
有两个构造方法:
ChangeNotifierProvider.value({value:})
ChangeNotifierProvider({builder:})
使用 value
构造方法时需要注意:value
传入的是一个已构造好的 ChangeNotifier
子类实例,此实例不由 Provider
内构建,Provider
不负责此实例的 dispose
。
虽然这个差异在 Provider 文档中有重点说明,但仍然有不少开发人员在写代码的过程中混用,故在此再次强调
因此开发人员在使用 ChangeNotifierProvider.value
时为使代码逻辑更加严谨,增强整个代码的健状性,培养良好的开发习惯开发人员需践行以下规范:
使用 ChangeNotifierProvider.value
构造方法时传入的实例一定是一个已构建好的实例,你有义务自行处理此实例的 dispose
。使用 ChangeNotifierProvider(builder:)
构造方法时你不应该传入一个已构建好的实例,这会导致生命周期混乱,从而导致异常。
你需要这样做
MyChangeNotifier variable;
void initState() {
super.initState();
variable = MyChangeNotifier(); // 提前构建实例
}
void build(BuildContext context) {
return ChangeNotifierProvider.value(
value: variable, // 已构建好的实例
child: ...
);
}
void dispose() {
super.dispose();
variable.dispose(); // 主动 dispose
}
你不能这样做
MyChangeNotifier variable;
void initState() {
super.initState();
variable = MyChangeNotifier();
}
void build(BuildContext context) {
// create 对象的生命周期只存在于 Provider 树下,此处应不直接使用此实例
return ChangeNotifierProvider(
create: (_) => variable,
child: ...
);
}
避免资源释放遗忘
在 Flutter 中有很多需要主动进行资源释放的类型,包含但不限于:Timer
、StreamSubscription
、ScrollController
、TextEditingController
等,另外很多第三方库存在需要进行资源释放的类型。
如此多的资源释放类型管理起来是非常麻烦的,一旦忘记某个类型的释放很会造成整个页面的内存泄漏。而资源的创建一般都位于 initState
内,资源释放都位于 dispose
内。
为了减小忘记资源释放的可能性,dispose
应为 State
内的第一个函数并尽可能的将 initsate
紧跟在 dispose
后
这样在代码 Review 时可以从视觉上一眼看出来资源释放是否被遗忘。
Bad
final _controller = TextEditingController();
late Timer _timer;
void initState() {
super.initState();
_timer = Timer(...);
}
Widget build(BuildContext context) {
return SizedBox(
child: // 假设此处为简单的登录界面,也将是一串很长的构建代码
);
}
void didChangeDependencies() {
super.didChangeDependencies();
// 又是若干行
}
// dispose 函数在 State 末尾,与 initState 大概率会超过一屏的距离
// 致使 dispose 需要释放的资源与创建的资源脱节
// 无法直观看出是否漏写释放函数
void dispose() {
_timer.cancell();
super.dispose();
}
Good
final _controller = TextEditingController();
late Timer _timer;
// 属性后第一个函数应为 dispose
void dispose() {
_controller.dispose();
_timer.cancell();
super.dispose();
}
// 中间不要插入其它函数,紧跟着写 initState
void initState() {
super.initState();
_timer = Timer(...);
}
上面推荐的写法也可以用在自定义的 ChangeNotifer
子类中,将 dispose
函数紧在构造函数后,有利于释放遗漏检查。
由于创建资源与释放资源在不同的函数内,因此存在一种情况:为了释放资源不得不在 State
内加一个变量以便于在 dipose
函数中引用并释放,即便此资源仅在局部使用。
典型场景如下:
late CancelToken _token;
Future<void> _refreshPage() async {
// _token 只在页面刷新的函数中使用,却不得不加一个变量来引用它
_token = CancelToken();
Dio dio = Dio();
Response response = await dio.get(url, cancelToken: _token);
int code = response.statusCode;
// ...
}
void dispose() {
super.dispose();
_token.cancel();
}
这样的场景在一个页面内可能有多处,相同的处理方式使用起来就略显麻烦了,也容易导致遗忘。因此推荐如下写法:
// 创建下面的 Mixin
mixin AutomaticDisposeMixin<T extends StatefulWidget> on State<T> {
Set<VoidCallback> _disposeSet = Set<VoidCallback>();
void autoDispose(VoidCallback callabck) {
_disposeSet.add(callabck);
}
void dispose() {
_disposeSet.forEach((f) => f());
_disposeSet.removeAll();
super.dispose();
}
}
class _PageState extends State<Page> with AutomaticDisposeMixin {
Future<void> _refreshPage() async {
final token = CancelToken();
// 添加到自动释放队列
autoDispose(() => token.cancel());
Dio dio = Dio();
Response response = await dio.get(url, cancelToken: token);
int code = response.statusCode;
// ...
}
}
当然也这种用法不限于局部变量,同样也可以在 initState
内进行资源声明的同时进行资源释放,这种写法相对来讲更加直观,更不易遗漏资源释放。
final _controller = TextEditingController();
void initState() {
super.initState();
_timer = Timer(...);
autoDispose(() => _timer.cancel());
autoDispose(() => _controller.dispose());
}
StatefulWidget 使用
State 中存在异步刷新
在开发过程中简单的页面或组件通常直接使用 StatefulWidget
进行构建,并在 State
中实现状态逻辑。因此 State
不可避免可能会存在异步刷新的场景。但异步结束时当前 Widget 可能已经从当前渲染树移除,直接刷新当前 Widget 可能导致异常。典型示例如下:
class SomPageState extends State<SomePageWidget> {
PageData _data;
Future<void> _refreshPage() async {
// 异步可能是延时、接口、文件读取、平台状态获取等
final response = await API.getPageDetaile();
if (!response.success) return;
// 直接界面刷新页面可能会导致异常,当前 Widget 可能已从渲染树移除
setState((){
_data = response.data;
});
}
}
为使代码逻辑更加严谨,增强整个代码的健壮性,培养良好的开发习惯,建议:
在 State
里异步刷新 UI 时需要进行 mounted
判断,确认当前 Widget
在渲染树中时才需要进行界面刷新否则应忽略。
Future<void> _refreshPage() async {
// 异步可能是接口、文件读取、状态获取等
final response = await API.getPageDetaile();
if (!response.success) return;
// 当前 Widget 存在于渲染树中才刷新
if (!mounted) return;
setState((){
_data = response.data;
});
}
上面的 mounted
判断可能会存在于所有 State
中又或者一个 State
里有多个异步 setState
调用,每个调用都去判断过于繁锁,因此更推荐如下写法:
// 统一定义如下 mixin
mixin Stateable<T extends StatefulWidget> on State<T> {
@override
void setState(VoidCallback fn) {
if (!mounted) return;
super.setState(fn);
}
}
// 在存在异步刷新的 State 中 with 如上 mixin
class SomPageState extends State<SomePageWidget> with Stateable {
//...
}
来源:juejin.cn/post/7375882178012577802
为什么常说,完成比完美更重要
前言
Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。
最近学习了12个生财思维,受益匪浅,但是纸上得来终觉浅,绝知此事要躬行,没有亲身实践,怎么能更好的理解呢?
单纯的学习,尤其是思维工具类的学习,只看但不实践,是不会有太好的效果的。
课程中的案例虽然真实,但是每个人的眼界、能力不同,所以案例对自己只能开开眼,但自己对于思维模式的理解却不会有太多的帮助。
为了更好的理解每一个生财思维,我决定根据每一个生财思维去复盘过去十年间遇到的机遇,看看自己错过了什么,有抓住了什么,然后把学习过程中的思考重新整理出来。
今天想和大家分享的是迭代思维,希望对你有所帮助。
迭代思维
什么是迭代思维
迭代是什么意思,一种重复反馈过程的活动,每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值。
迭代对于一个程序员来讲并不陌生,甚至很多公司把版本发布,都成为“迭代”。
如何真正的用好迭代思维?
主要有三步
- 确定目标,比如在软件开发中,我们首先要知道,我们目标是什么,是收入、用户数,还是流量。
- 找到迭代方法,这个依然很常见,利用上一篇文章讲的对标思维,参考领域成功的高手,看看他们用了什么工具、方法。
- 持续改进,每次哪怕只改进一小点,让每一步走的更踏实。
先完成,再完美
一周上线的新系统
软件开发是离不开迭代的过程的,就算是你设计了完美的框架,也会在各类需求的轰炸中不得不进行迭代满足用户需求。
在上家公司,有一次领导安排一个新业务启动,于是要单独启动一个项目。
我不知道大家平常对于一个新项目的开发需要多久,那时候我的能力较弱,开发语言也不熟悉,以往的经验是,从技术设计、框架搭建、代码开发、测试这一套流程下来,我觉着这个项目起码也要1个月左右才能完成吧。
但是经过领导们评估,最终决定这个系统的开发时间是5天,留两天时间自测,说实话我是持着怀疑的态度,硬着头皮接了下来。
最终项目用了一周多的时间就上线了,虽然时长报警,缺乏监控,代码性能不够高,但快速验证了业务的可行性。
我承认刚上线的系统并不是一个完美的系统,甚至有问题时发下连日志都没打印。
但是,发现问题排查困难,所以先不停的完善日志打印。
接着觉着发现问题太慢了,补充了监控和报警,异常情况第一时间就能感知。
性能不够,响应时间长,花了3天时间优化性能瓶颈点。
业务不断扩充,代码扩展性不好,优化了1周的时间,进行了一部分重构。
就是随着一段段时间的投入,一次次的发版上线,最终服务趋于稳定,也成了业务的一部分收入来源。
不断练习的写作
如果还能想到一个例子,那这件事就是写作。
去年年底决定开始写作时,发布的那篇文章,写完就直接发布了,很明显,数据非常不好。
内容很少分段,也没有配图,更不用说加黑、二级标题这些了。
先从内容开始,对比很多流量好的文章,都有一个共性,就是没有一大段话,而是都进行了分段。为了让大家阅读更加轻松,把一大段话,拆成几个小段,看来更加清爽。
添加了分段之后,又发现如果正片内容都是文字,一样让大家阅读压力很大,所以在其中搭配上配图,效果会更好。
后面,改进了文字排版,开始带有一级标题、二级标题。
再后来,学习如何取标题、如何选题,还建立了自己的写作模版,开头和结尾,还添加上了引导关注的文案。
就用这种方式,在掘金和公众号上,也写出了一些数据比较好的内容,掘金的创作登记,从lv3,也升级到了lv5。
而迭代思维,也会在写作上,持续应用下去。
不去开始,必定失败
迭代思维,主要适用于哪类场景呢?
当你有一件事情、一个项目时,因为内心没有完美的方案,迟迟没有行动时,就需要用到迭代思维了。
记得我发布的第一篇文章,可以是2022年了,然而下一次再次发布,已经隔了1年多的时间。
相隔时间这么久的原因,就是因为我在那个时候,我发现我写出的文章和别人差距太大了,别人的文章洋洋洒洒一两千字,标题吸引人,然而我自己的文章即没有深度,又没有自己的感触。
我在和一篇优秀的文章做比较时,我完全不知道应该如何做,才能写的像别人一样好。
于是,遇到问题不做了,睡大觉。这一睡,就白白荒废了1年的时间。
迭代思维,首先要避免完美思维,先开始,然后最重要的是小步快跑。
记得在学习写作的时候,听到老师的一句话,让我记忆犹新,也再次分享给大家。
想都是问题,做才有答案。
说在最后
好了,文章到这里就要结束了,总结一下。
迭代思维从概念上看其实不难,只需要三步即可,确定目标,找到迭代方法,持续改进。
来源:juejin.cn/post/7390134326871080972