注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Flutter-数字切换动画

效果 需求 数字切换时新数字从上往下进入,上个数字从上往下出 新数字进入时下落到位置并带有回弹效果 上个数字及新输入切换时带有透明度和缩放动画 实现 主要采用Animat...
继续阅读 »

效果





需求





  • 数字切换时新数字从上往下进入,上个数字从上往下出



  • 新数字进入时下落到位置并带有回弹效果



  • 上个数字及新输入切换时带有透明度和缩放动画


实现


主要采用AnimatedSwitcher实现需求,代码比较简单,直接撸


import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_xy/widgets/xy_app_bar.dart';

class NumAnimPage extends StatefulWidget {
  const NumAnimPage({super.key});

  @override
  State<NumAnimPage> createState() => _NumAnimPageState();
}

class _NumAnimPageState extends State<NumAnimPage> {
  int _currentNum = 0;

  // 数字文本随机颜色
  Color get _numColor {
    Random random = Random();
    int red = random.nextInt(256);
    int green = random.nextInt(256);
    int blue = random.nextInt(256);
    return Color.fromARGB(255, red, green, blue);
  }

  // 数字累加
  void _addNumber() {
    setState(() {
      _currentNum++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: XYAppBar(
        title: "数字动画",
      ),
      body: Center(
        child: _bodyWidget(),
      ),
    );
  }

  Widget _bodyWidget() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        AnimatedSwitcher(
          duration: const Duration(milliseconds: 500),
          transitionBuilder: (Widget child, Animation<double> animation) {
            Offset startOffset = animation.status == AnimationStatus.completed
                ? const Offset(0.0, 1.0)
                : const Offset(0.0, -1.0);
            Offset endOffset = const Offset(0.0, 0.0);
            return SlideTransition(
              position: Tween(begin: startOffset, end: endOffset).animate(
                CurvedAnimation(parent: animation, curve: Curves.bounceOut),
              ),
              child: FadeTransition(
                opacity: Tween(begin: 0.0, end: 1.0).animate(
                  CurvedAnimation(parent: animation, curve: Curves.linear),
                ),
                child: ScaleTransition(
                  scale: Tween(begin: 0.5, end: 1.0).animate(
                    CurvedAnimation(parent: animation, curve: Curves.linear),
                  ),
                  child: child,
                ),
              ),
            );
          },
          child: Text(
            '$_currentNum',
            key: ValueKey<int>(_currentNum),
            style: TextStyle(fontSize: 100, color: _numColor),
          ),
        ),
        const SizedBox(height: 80),
        ElevatedButton(
          onPressed: _addNumber,
          child: const Text(
            '数字动画',
            style: TextStyle(fontSize: 25, color: Colors.white),
          ),
        ),
      ],
    );
  }
}


具体见github:https://github.com/yixiaolunhui/flutter_xy


作者:移动小样
来源:mdnice.com/writing/9645b22a9a54493f9f2e3f74e60d17c7
收起阅读 »

第三方认证中心跳转

一、业务需求 由第三方认证中心将 token 放在 header 中跳转系统,前端获取到第三方系统携带 header 中的 token。 二、 业务流程 模拟第三方应用 CUSTOM-USERTOKEN 是第三方的 tok...
继续阅读 »

一、业务需求


由第三方认证中心将 token 放在 header 中跳转系统,前端获取到第三方系统携带 header 中的 token。


二、 业务流程





模拟第三方应用





  • CUSTOM-USERTOKEN 是第三方的 token



  • proxy_pass 是我们的前端地址


  server {
listen 12345;
server_name localhost;

location / {
proxy_set_header Host $host:$server_port;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header CUSTOM-USERTOKEN 'MY-TOKEN'
proxy_pass http://127.0.0.1;
}
}

前端静态代理





  • backend 是后端服务地址



  • 80 是前端代理端口


  server {
listen 80;
server_name localhost;

location / {
root /vuepress/docs;
index index.html;
try_files $uri $uri/ /index.html;
}
error_page 405 =200 $uri;
}

三、处理方式


由于放在 header 中的内容,前端只有从 XHR 请求中才能拿到,所以直接打开页面时,肯定是无法拿到 header 中的 token 的,又因为这个 token 只有从第三方系统中跳转才能携带,所以也无法通过请求当前页面去获取 header 中的内容。


一、通过后端重定向


在 nginx 代理中,第三方请求从原本跳转访问前端的地址==改为==后端地址, 因为后端是可以从请求总直接拿到 header,所以这时由后端去处理 token ,在重定向到前端。





  • 后端可以设置 cookie,前端从 cookie 中获取



  • 后端可以拼接 URL, 前端从 url 中获取



  • 后端可以通过缓存 cookie, 重定向到前端后发请求获取 token


模拟第三方应用





  • 第三方应用由跳转前端改为跳转后端接口


  server {
listen 12345;
server_name localhost;

location / {
proxy_set_header Host $host:$server_port;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header CUSTOM-USERTOKEN 'MY-TOKEN'
proxy_pass http://backend/token;
}
}

前端静态代理





  • 前端代理不需要做任何处理


  server {
listen 80;
server_name localhost;

location / {
root /vuepress/docs;
index index.html;
try_files $uri $uri/ /index.html;
}
error_page 405 =200 $uri;
}

二、通过 nginx 重定向 URL


在 nginx 代理中,新增一个 /token 的代理地址,用于转发地址,第三方请求从原本跳转访问前端的地址,改为 /token 代理地址 因为 nginx 中是可以获取 header 中的内容的,所以这时由 /token 处理拼接好 url ,在重定向到前端。





模拟第三方应用



  server {
listen 12345;
server_name localhost;

location / {
proxy_set_header Host $host:$server_port;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header CUSTOM-USERTOKEN 'MY-TOKEN'
proxy_pass http://127.0.0.1/token;
}
}

前端静态代理





  • 新增 /token 代理,进行拼接 URL 后跳转


  server {
listen 80;
server_name localhost;

location / {
root /vuepress/docs;
index index.html;
try_files $uri $uri/ /index.html;
}
location /token {
# 将 $http_custom_usertoken 拼接在 URL 中,同时重定向到前端
# 前端通过 location.search 处理 token
rewrite (.+) http://127.0.0.1?token=$http_custom_usertoken;
}
error_page 405 =200 $uri;
}

三、通过 nginx 设置 Cookie


由于通过响应头中设置 Set-Cookie 可以直接存储到浏览器中,所以我们也可以通过直接设置 cookie 的方式处理。





模拟第三方应用





  • 此时第三方应用直接访问前端即可


  server {
listen 12345;
server_name localhost;

location / {
proxy_set_header Host $host:$server_port;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header CUSTOM-USERTOKEN 'MY-TOKEN'
proxy_pass http://127.0.0.1;
}
}

前端静态代理





  • token 设置在 cookie


  server {
listen 80;
server_name localhost;

location / {
add_header Set-Cookie "token=$http_custom_usertoken;HttpOnly;Secure";
root /vuepress/docs;
index index.html;
try_files $uri $uri/ /index.html;
}
error_page 405 =200 $uri;
}

四、nginx 代理转发设置 Cookie


方法 三、通过 nginx 设置 Cookie 中,存在一个问题,由于此时在前端静态代理上添加 cookie,这就会导致所有静态资源都会携带 cookie, 这就会造成 cookie 中因为 path 不同而重复添加, 所以我们还可以通过造一层代理的方式处理这个问题





模拟第三方应用





  • 代理地址再次修改为 token


  server {
listen 12345;
server_name localhost;

location / {
proxy_set_header Host $host:$server_port;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header CUSTOM-USERTOKEN 'MY-TOKEN'
proxy_pass http://127.0.0.1/token;
}
}

前端静态代理





  • token 设置在 /token 代理地址的 cookie



  • /token 重定向到前端地址


  server {
listen 80;
server_name localhost;

location / {
root /vuepress/docs;
index index.html;
try_files $uri $uri/ /index.html;
}

location /token {
add_header Set-Cookie "token=$http_custom_usertoken;HttpOnly;Secure";
rewrite (.+) http://127.0.0.1;
}
error_page 405 =200 $uri;
}

作者:子洋
来源:mdnice.com/writing/d92f346cc96a43b49fc36c9894add729
收起阅读 »

用Vue.js构建一个Web3应用像,像开发 Web2 一样熟悉

web
作为一名涉足去中心化网络的前端 JavaScript 开发人员,您可能遇到过许多 Web3 开发解决方案。但是,这些解决方案通常侧重于钱包集成和交易执行,这就造成了学习曲线,偏离了熟悉的 Web2 开发体验。 但不用担心!有一种解决方案可以无缝衔接 Web2...
继续阅读 »

作为一名涉足去中心化网络的前端 JavaScript 开发人员,您可能遇到过许多 Web3 开发解决方案。但是,这些解决方案通常侧重于钱包集成和交易执行,这就造成了学习曲线,偏离了熟悉的 Web2 开发体验。


但不用担心!有一种解决方案可以无缝衔接 Web2 和 Web3,它就是 Juno



网址:https://juno.build/



在本篇博文中,我们将探讨如何利用 Vue 和 Juno 的强大功能来开发去中心化应用程序(dApps)。加入我们的旅程,揭开 Juno 的神秘面纱,让您轻松创建非凡的去中心化体验!





导言


在我之前的博文中,我讨论了 React[1]Angular[2] 这两个流行的 JavaScript 前端框架的类似解决方案。如果这两个框架中的任何一个是您的首选,我建议您浏览这些具体的文章,以获得量身定制的见解。


Juno如何工作


如果你还不了解 Juno,它是一个功能强大的开源区块链即服务平台,旨在让去中心化应用程序开发变得更加容易。可以把它想象成一个无服务器平台,类似于谷歌Firebase或AWS Amplify等流行服务,但增加了区块链技术的优势。Juno 完全在区块链上运行您的应用程序,确保完全去中心化和安全的基础设施。


通过利用Internet Computer[3]区块链网络和基础设施,Juno 为您创建的每个应用程序引入了一个名为 “Satellites” 的独特概念。这些 Satellites 作为强大的智能合约,封装了您的整个应用程序,包括 JavaScript、HTML 和图像文件等网络资产,以及简单的数据库、文件存储和身份验证机制。通过 Juno,您可以完全控制应用程序的功能和数据。


构建一个 Dapp


让我们开始构建我们的第一个去中心化应用程序,简称“dapp”。在这个例子中,我们将创建一个笔记应用程序,允许用户存储和检索数据条目,以及上传文件。


本教程和代码示例使用了 Vue Composition API。


初始化


在将 Juno 集成到应用程序之前,需要创建一个 satellite。该过程在文档[4]中有详细的解释。


此外,还需要安装SDK。


npm i @junobuild/core

完成这两个步骤后,您可以在 Vue 应用程序的根目录(例如 App.vue)中使用 satellite ID 初始化 Juno。这将配置库与您的智能合约进行通信。


<script setup lang="ts">
import { onMounted } from 'vue'
import { initJuno } from '@junobuild/core'

onMounted(
  async () =>
    await initJuno({
      satelliteId'pycrs-xiaaa-aaaal-ab6la-cai'
    })
)
</script>

<template>
<h1>Hello World</h1>
</template>

配置完成!现在,您的应用程序已经可以用于 Web3 了!😎


身份验证


为了确保用户身份的安全性和匿名性,需要对用户进行登录和注销。要做到这一点,可以将相关函数绑定到应用程序中任何位置的 call-to-action 按钮。


<script setup lang="ts">
import { signIn, signOut} from '@junobuild/core'
</script>

<button @click="signIn">Sign-in</button>
<button @click="signOut">Sign-out</button>

为了与其他服务建立无缝集成,库和 satellite 组件在用户成功登录后自动在您的智能合约中生成新条目。此功能使库能够在任何数据交换期间有效地验证权限。


为了监视并深入了解该条目,从而访问有关用户状态的信息,Juno提供了一个名为authSubscribe() 的可观察函数。您可以根据需要灵活地多次使用此函数。然而,你也可以创建一个在整个应用中有效传播用户信息的 store。


import { ref, type Ref } from 'vue'
import { defineStore } from 'pinia'
import { authSubscribe, type User } from '@junobuild/core'

export const useAuthStore = defineStore('auth', () => {
  const user: Ref<User | null | undefined> = ref(undefined)

  const unsubscribe = authSubscribe((u) => (user.value = u))

  return { user, unsubscribe }
})

这样,在应用程序的顶层订阅它就变得非常方便。


<script setup lang="ts">
import { useAuthStore } from '../stores/auth.store'
import { storeToRefs } from 'pinia'

const store = useAuthStore()
const { user } = storeToRefs(store)
</script>

<template>
  <template v-if="user !== undefined && user !== null">
    <slot /
>
  </template>

  <template v-else>
    <p>Not signed in.</
p>
  </template>
</
template>

存储文档


Juno提供了一个名为“Datastore”的功能,旨在将数据直接存储在区块链上。Datastore 由一组集合组成,其中每个集合保存文档,这些文档由您选择的键唯一标识。


在本教程中,我们的目标是存储笔记,因此必须按照文档中提供的说明创建一个集合。为集合选择合适的名称,例如“notes”。


一旦设置好应用程序并创建了必要的集合,就可以利用库的 setDoc 函数将数据持久化到区块链上。此功能使您能够安全且不变地存储笔记。


import { setDoc } from "@junobuild/core";

// TypeScript example from the documentation
await setDoc<Example>({
  collection"my_collection_key",
  doc: {
    key"my_document_key",
    data: myExample,
  },
});

由于集合中的文档是通过唯一的密钥来标识的,因此我们使用 nanoid[5] 来创建密钥--这是一种用于 JavaScript 的微型字符串 ID 生成器。


<script lang="ts" setup>
import { ref } from 'vue'
import { setDoc } from '@junobuild/core'
import { nanoid } from 'nanoid'

const inputText = ref('')

const add = async () => {
  const key = nanoid()

  await setDoc({
    collection'notes',
    doc: {
      key,
      data: {
        text: inputText.value
      }
    }
  })
}
</script>

<template>
  <textarea rows="5" placeholder="Your diary entry" 
            v-model="inputText">
</textarea>

  <button @click="add">Add</button>
</template>


请注意,为简单起见,本教程提供的代码片段不包括适当的错误处理,也不包括复杂的表单处理。



检索文档列表


为了检索存储在区块链上的文档集合,我们可以使用库的 listDocs 函数。这个多功能函数允许加入各种参数,以方便数据过滤、排序或分页。


出于本教程的目的,我们将保持示例的简约性。我们的目标是在挂载组件时简单地列出所有用户数据。


<script lang="ts" setup>
import { listDocs } from '@junobuild/core'
import { onMounted, ref } from 'vue'

const items = ref([])

const list = async () => {
  const { items: data } = await listDocs({
    collection'notes'
  })

  items.value = data
}

onMounted(async () => await list())
</script>

<template>
  <p v-for="(item, index) in items">
    <span>
      {{ index + 1 }}
    </span>
    <span>{{ item.data.text }}</span>
  </p>
</template>

文件上传


在去中心化网络上存储数据是一项复杂的任务。然而,Juno 的设计旨在为需要轻松存储和检索用户生成内容(如照片或文件)的应用程序开发人员简化这一过程。


在处理文档时,第一步是按照文档[6]中提供的说明创建一个集合。在本教程中,我们将重点实施图片上传,因此该集合可以恰当地命名为 “images”。


为确保存储数据的唯一性和正确识别,每个文件都有唯一的文件名和路径。这一点非常重要,因为数据是在网络上提供的,每条数据都应该有一个独特的 URL。


要实现这一点,我们可以使用用户唯一ID的文本表示形式和每个上传文件的时间戳的组合来创建一个键。通过访问我们之前在存储中声明的属性,我们可以检索相应的用户键。


<script lang="ts" setup>
import { ref } from 'vue'
import { useAuthStore } from '@/stores/auth.store'
import { storeToRefs } from 'pinia'
import { uploadFile } from '@junobuild/core'

const file = ref(undefined)

const store = useAuthStore()
const { user } = storeToRefs(store)

const setFile = (f) => (file.value = f)

const upload = async () => {
  // Demo purpose therefore edge case not properly handled
  if ([nullundefined].includes(user.value)) {
    return
  }

  const filename = `${user.value.key}-${file.value.name}`

  const { downloadUrl } = await uploadFile({
    collection'images',
    data: file.value,
    filename
  })

  console.log('Uploaded', downloadUrl)
}
</script>

<template>
  <input type="file" @change="(event) => setFile(event.target.files?.[0])" />

  <button @click="upload">Upload</button>
</template>

一旦一个资源被上传,一个 downloadUrl 返回,它提供了一个直接的 HTTPS 链接,可以在web上访问上传的资源。


列出资源


为了检索存储在区块链上的资产集合,我们可以利用库提供的 listAssets 函数。这个函数在参数方面提供了灵活性,允许我们根据需要对文件进行过滤、排序或分页。


与前面的文档示例类似,我们将保持这个示例的简约性。


<script lang="ts" setup>
import { listAssets } from '@junobuild/core'
import { onMounted, ref } from 'vue'

const assets = ref([])

const list = async () => {
  const { assets: images } = await listAssets({
    collection'images'
  })

  assets.value = images
}

onMounted(async () => await list())
</script>

<template>
  <img loading="lazy" :src="asset.downloadUrl" v-for="asset in assets" />
</template>

部署 🚀


在开发和构建应用程序之后,下一步是将其部署到区块链上。为此,您需要在终端中执行以下命令来安装 Juno 命令行接口(CLI):


npm i -g @junobuild/cli

安装过程完成后,您可以按照文档[7]中的说明并从终端登录来访问您的 satellite。这将使你的机器能够控制你的 satellite。


juno login

最后,您可以使用以下命令部署项目:


juno deploy

恭喜你!您的 Vue dapp 现在已经上线,并完全由区块链提供支持。🎉


资源





原文:https://betterprogramming.pub/build-a-web3-app-with-vuejs-db1503ca20d2


是哒是哒说


参考资料


[1]

React: https://betterprogramming.pub/build-a-web3-app-with-react-js-6353825baf9a

[2]

Angular: https://levelup.gitconnected.com/develop-an-angular-app-on-blockchain-9cde44ae00b7

[3]

Internet Computer: https://internetcomputer.org/

[4]

文档: https://juno.build/docs/add-juno-to-an-app/create-a-satellite

[5]

nanoid: https://github.com/ai/nanoid

[6]

文档: https://juno.build/docs/build/storage#collections-and-rules

[7]

文档: https://juno.build/docs/miscellaneous/cli#login

[8]

https://juno.build/docs/intro: https://juno.build/docs/intro

[9]

GitHub 代码库: https://github.com/buildwithjuno/examples/tree/main/vue/diary



作者:程序员张张
来源:mdnice.com/writing/26615feb73924bb4821f543e0f041fa4
收起阅读 »

前端开发如何给自己定位?初级?中级?高级!

web
引言 在快速发展的互联网时代,前端开发一直处于高速增长的趋势中。作为构建用户界面和实现交互功能的关键角色,前端开发人员需要不断提升自己的技能和能力,以适应变化的行业需求。本文将为前端开发人员提供一个能力定位指南,帮助他们了解自己在前端领域的定位,内容参考阿里前...
继续阅读 »

引言


在快速发展的互联网时代,前端开发一直处于高速增长的趋势中。作为构建用户界面和实现交互功能的关键角色,前端开发人员需要不断提升自己的技能和能力,以适应变化的行业需求。本文将为前端开发人员提供一个能力定位指南,帮助他们了解自己在前端领域的定位,内容参考阿里前端面试指南,P6/P6+/P7的能力标准。


目录



0.掌握图形学,webgl或熟练使用threejs框架,熟练canvas相关渲染及动画操作的优先。

1.熟练掌握JavaScript。

2.熟悉常用工程化工具,掌握模块化思想和技术实现方案。

3.熟练掌握React前端框架,了解技术底层。同时了解vue以及angular等其他框架者优先。

4.熟练掌握react生态常用工具,redux/react-router等。

5.熟悉各种Web前端技术,包括HTML/XML/CSS等,有基于Ajax的前端应用开发经验。

6.有良好的编码习惯,对前端技术有持续的热情,个性乐观开朗,逻辑性强,善于和各种背景的人合作。

7.具有TS/移动设备上前端开发/NodeJS/服务端开发等经验者优先。



0.掌握图形学,webgl或熟练使用threejs框架,熟练canvas相关渲染及动画操作的优先。


初级:



  • 学习过图形学相关知识,知道矩阵等数学原理在动画中的作用,知道三维场景需要的最基础的构成,能用threejs搭3d场景,知道webgl和threejs的关系。

  • 知道canvas是干嘛的,聊到旋转能说出canvas的api。

  • 知道css动画,css动画属性知道关键字和用法(换句话说,电话面试会当场出题要求口喷css动画,至少能说对大概,而不是回答百度一下就会用)。

  • 知道js动画,能说出1~2个社区js动画库,知道js动画和css动画优缺点以及适用场景。

  • 知道raf和其他达到60fps的方法。


中级:



  • 如果没有threejs,你也能基于webgl自己封装一个简单的threejs出来。

  • 聊到原理能说出四元数,聊到鼠标操作能提到节流,聊到性能能提到restore,聊到帧说出raf和timeout的区别,以及各自在优化时候的作用。

  • 知道怎样在移动端处理加载问题,渲染性能问题。

  • 知道如何结合native能力优化性能。

  • 知道如何排查性能问题。对chrome动画、3d、传感器调试十分了解。


高级:



  • 搭建过整套资源加载优化方案,能说明白整体方案的各个细节,包括前端、客户端、服务端分别需要实现哪些功能点、依赖哪些基础能力,以及如何配合。

  • 设计并实现过前端动画引擎,能说明白一个复杂互动项目的技术架构,知道需要哪些核心模块,以及这些模块间如何配合。

  • 有自己实现的动画相关技术方案产出,这套技术方案必须是解决明确的业务或技术难点问题的。为了业务快速落地而封装一个库,不算这里的技术方案。如果有类似社区方案,必须能从原理上说明白和竞品的差异,各自优劣,以及技术选型的原因。


1.熟练掌握JavaScript。


初级:



  • JavaScript各种概念都得了解,《JavaScript语言精粹》这本书的目录都得有概念,并且这些核心点都能脱口而出是什么。这里列举一些做参考:

  • 知道组合寄生继承,知道class继承。

  • 知道怎么创建类function + class。

  • 知道闭包在实际场景中怎么用,常见的坑。

  • 知道模块是什么,怎么用。

  • 知道event loop是什么,能举例说明event loop怎么影响平时的编码。

  • 掌握基础数据结构,比如堆、栈、树,并了解这些数据结构计算机基础中的作用。

  • 知道ES6数组相关方法,比如forEach,map,reduce。


中级:



  • 知道class继承与组合寄生继承的差别,并能举例说明。

  • 知道event loop原理,知道宏微任务,并且能从个人理解层面说出为什么要区分。知道node和浏览器在实现loop时候的差别。

  • 能将继承、作用域、闭包、模块这些概念融汇贯通,并且结合实际例子说明这几个概念怎样结合在一起。

  • 能脱口而出2种以上设计模式的核心思想,并结合js语言特性举例或口喷基础实现。

  • 掌握一些基础算法核心思想或简单算法问题,比如排序,大数相加。


2.熟悉常用工程化工具,掌握模块化思想和技术实现方案。


初级:



  • 知道webpack,rollup以及他们适用的场景。

  • 知道webpack v4和v3的区别。

  • 脱口而出webpack基础配置。

  • 知道webpack打包结果的代码结构和执行流程,知道index.js,runtime.js是干嘛的。

  • 知道amd,cmd,commonjs,es module分别是什么。

  • 知道所有模块化标准定义一个模块怎么写。给出2个文件,能口喷一段代码完成模块打包和执行的核心逻辑。


中级:



  • 知道webpack打包链路,知道plugin生命周期,知道怎么写一个plugin和loader。

  • 知道常见loader做了什么事情,能几句话说明白,比如babel-loader,vue-loader。

  • 能结合性能优化聊webpack配置怎么做,能清楚说明白核心要点有哪些,并说明解决什么问题,需要哪些外部依赖,比如cdn,接入层等。

  • 了解异步模块加载的实现原理,能口喷代码实现核心逻辑。


高级:



  • 能设计出或具体说明白团队研发基础设施。具体包括但不限于:

  • 项目脚手架搭建,及如何以工具形态共享。

  • 团队eslint规范如何设计,及如何统一更新。

  • 工具化打包发布流程,包括本地调试、云构建、线上发布体系、一键部署能力。同时,方案不仅限于前端工程部分,包含相关服务端基础设施,比如cdn服务搭建,接入层缓存方案设计,域名管控等。

  • 客户端缓存及预加载方案。


3.熟练掌握React前端框架,了解技术底层。同时了解vue以及angular等其他框架者优先。


初级:



  • 知道react常见优化方案,脱口而出常用生命周期,知道他们是干什么的。

  • 知道react大致实现思路,能对比react和js控制原生dom的差异,能口喷一个简化版的react。

  • 知道diff算法大致实现思路。

  • 对state和props有自己的使用心得,结合受控组件、hoc等特性描述,需要说明各种方案的适用场景。

  • 以上几点react替换为vue或angular同样适用。


中级:



  • 能说明白为什么要实现fiber,以及可能带来的坑。

  • 能说明白为什么要实现hook。

  • 能说明白为什么要用immutable,以及用或者不用的考虑。

  • 知道react不常用的特性,比如context,portal。

  • 能用自己的理解说明白react like框架的本质,能说明白如何让这些框架共存。


高级:



  • 能设计出框架无关的技术架构。包括但不限于:

  • 说明如何解决可能存在的冲突问题,需要结合实际案例。

  • 能说明架构分层逻辑、各层的核心模块,以及核心模块要解决的问题。能结合实际场景例举一些坑或者优雅的处理方案则更佳。


4.熟练掌握react生态常用工具,redux/react-router等。


初级:



  • 知道react-router,redux,redux-thunk,react-redux,immutable,antd或同级别社区组件库。

  • 知道vue和angular对应全家桶分别有哪些。

  • 知道浏览器react相关插件有什么,怎么用。

  • 知道react-router v3/v4的差异。

  • 知道antd组件化设计思路。

  • 知道thunk干嘛用的,怎么实现的。


中级:



  • 看过全家桶源码,不要求每行都看,但是知道核心实现原理和底层依赖。能口喷几行关键代码把对应类库实现即达标。

  • 能从数据驱动角度透彻的说明白redux,能够口喷原生js和redux结合要怎么做。

  • 能结合redux,vuex,mobx等数据流谈谈自己对vue和react的异同。


高级:



  • 有基于全家桶构建复杂应用的经验,比如最近很火的微前端和这些类库结合的时候要注意什么,会有什么坑,怎么解决


5.熟悉各种Web前端技术,包括HTML/XML/CSS等,有基于Ajax的前端应用开发经验。


初级:



  • HTML方面包括但不限于:语义化标签,history api,storage,ajax2.0等。

  • CSS方面包括但不限于:文档流,重绘重排,flex,BFC,IFC,before/after,动画,keyframe,画三角,优先级矩阵等。

  • 知道axios或同级别网络请求库,知道axios的核心功能。

  • 能口喷xhr用法,知道网络请求相关技术和技术底层,包括但不限于:content-type,不同type的作用;restful设计理念;cors处理方案,以及浏览器和服务端执行流程;口喷文件上传实现;

  • 知道如何完成登陆模块,包括但不限于:登陆表单如何实现;cookie登录态维护方案;token base登录态方案;session概念;


中级:



  • HTML方面能够结合各个浏览器api描述常用类库的实现。

  • css方面能够结合各个概念,说明白网上那些hack方案或优化方案的原理。

  • 能说明白接口请求的前后端整体架构和流程,包括:业务代码,浏览器原理,http协议,服务端接入层,rpc服务调用,负载均衡。

  • 知道websocket用法,包括但不限于:鉴权,房间分配,心跳机制,重连方案等。

  • 知道pc端与移动端登录态维护方案,知道token base登录态实现细节,知道服务端session控制实现,关键字:refresh token。

  • 知道oauth2.0轻量与完整实现原理。

  • 知道移动端api请求与socket如何通过native发送,知道如何与native进行数据交互,知道ios与安卓jsbridge实现原理。


高级:



  • 知道移动端webview和基础能力,包括但不限于:iOS端uiwebview与wkwebview差异;webview资源加载优化方案;webview池管理方案;native路由等。

  • 登陆抽象层,能够给出完整的前后端对用户体系的整体技术架构设计,满足多业务形态用户体系统一。考虑跨域名、多组织架构、跨端、用户态开放等场景。

  • mock方案,能够设计出满足各种场景需要的mock数据方案,同时能说出对前后端分离的理解。考虑mock方案的通用性、场景覆盖度,以及代码或工程侵入程度。

  • 埋点方案,能够说明白前端埋点方案技术底层实现,以及技术选型原理。能够设计出基于埋点的数据采集和分析方案,关键字包括:分桶策略,采样率,时序性,数据仓库,数据清洗等。


6.有良好的编码习惯,对前端技术有持续的热情,个性乐观开朗,逻辑性强,善于和各种背景的人合作。


初级:



  • 知道eslint,以及如何与工程配合使用。

  • 了解近3年前端较重要的更新事件。

  • 面试过程中遇到答不出来的问题,能从逻辑分析上给出大致的思考路径。

  • 知道几个热门的国内外前端技术网站,同时能例举几个面试过程中的核心点是从哪里看到的。


高级:



  • 在团队内推行eslint,并给出工程化解决方案。

  • 面试过程思路清晰,面试官给出关键字,能够快速反应出相关的技术要点,但是也要避免滔滔不绝,说一堆无关紧要的东西。举例来说,当时勾股老师面试我的时候,问了我一个左图右文的布局做法,我的回答是:我自己总结过7种方案,其中比较好用的是基于BFC的,float的以及flex的三种。之后把关键css口喷了一下,然后css就面完了。


7.具有TS/移动设备上前端开发/NodeJS/服务端开发等经验者优先。



  • 根据了解的深度分初/中/高级。

  • 知道TS是什么,为什么要用TS,有TS工程化实践经验。

  • 知道移动端前端常见问题,包括但不限于:rem + 1px方案;预加载;jsbridge原理等。

  • 能说出大概的服务端技术,包括但不限于:docker;k8s;rpc原理;中后台架构分层;缓存处理;分布式;响应式编程等。


5. 结论与进一步学习


本文为前端开发人员提供了一个能力定位指南,帮助他们了解自己在前端领域的定位,并提供了具体的代码示例来巩固学习成果。通过不断学习和实践,前端开发人员可以逐步提升自己的能力,从初级到中级再到高级。但请注意,在实际工作中,不同公司和项目对于各个级别的要求可能会有所不同。


为了进一步提高自己的水平,前端开发人员可以考虑以下学习路径和资源:



  • 阅读官方文档和教程,如MDN、React官方文档等;

  • 参与开源项目,并与其他开发人员进行交流和合作;

  • 关注前端开发的博客和社区,如Medium、Stack Overflow等;

  • 参加在线或线下的前端开发培训课程;

  • 阅读经典的前端开发书籍,如《JavaScript高级程序设计》、《CSS权威指南》等。


通过持续学习和实践,相信每个前端开发人员都可以不断成长,并在前端领域中取得更好的成就。祝愿大家在前端开

作者:Jony_men
来源:juejin.cn/post/7259961208794628151
发的道路上越走越远!

收起阅读 »

该写好代码吗?我也迷茫了

我在抖音上看到当当网创始人李国庆发了一条视频,感觉他说的挺有意思。 他说,作为企业中层管理者,该不该有自己的山头,用于自我保护,这让人很迷茫。 其实我也迷茫过。我猜测,我们迷茫的可能是同一件事。 程序员内部,曾经流传着这样几句圣经: 代码写的好,写得快,会像...
继续阅读 »

2023-04-21_172756.png


我在抖音上看到当当网创始人李国庆发了一条视频,感觉他说的挺有意思。


他说,作为企业中层管理者,该不该有自己的山头,用于自我保护,这让人很迷茫。


其实我也迷茫过。我猜测,我们迷茫的可能是同一件事。


程序员内部,曾经流传着这样几句圣经:



代码写的好,写得快,会像个闲人。代码有注释,逻辑清晰,任何人都能轻松取代你。


代码写的烂,只有自己能看懂,一次次救火,你反而会成为团队不可缺少的人才。



那么,问题来了:到底该把事情干好呢,还是不要干好呢?


这是一个问题吗?当然是往多快好省了做呀!


我以前的想法就是这样。


我做底层员工时,代码写的清晰简洁,高效严谨。有时候我会因为计算循环次数而费心设计。如果循环层数太多,我会先把关键数据放到Map里,后续可以直接取用。我也会关注代码的可读性,尽量少套几层循环,命名兼顾字符长度和表意指向,如果代码太多就抽离成一个方法函数,并且要在代码里注释清楚。而这些操作,在形成习惯之后,是不会影响开发效率的。反而在某些情况下,还会提高效率。因为不管逻辑多复杂,不管过去多久,一看就能懂,很容易排查问题和他人接手。


我做中层管理时,除了培养团队内每个员工都能做到上述标准外,让我投入很大精力的事情就是“去我化”。也就是通过手段、流程、文化做到团队自治。我在团队时,大家能很高效地完成工作。当我短时间内离开时,大家依然能依靠惯性维持高效的状态。


我的想法很单纯:不管我是一线开发,还是中层管理,我修炼的都是自己。当你具备一定的职场能力时,你就是值钱的。作为员工你能把手头的活干得又快又好,作为管理你能把团队管理得积极健康。这就是亮点。不要在意别人的看法,你只需要修炼自己。当你具备了这个能力,这里不适合你,好多地方都会求此类贤人若渴。


其实,后面慢慢发现,这种想法可能还值得商榷。


因为创业和打工的区别还是挺大的。


创业是给自己干,想干好是肯定的,谁都不愿意面对一团糟。


我看明朝的历史,建文帝朱允炆刚登基时,就想削弱其他藩王的势力,加强自己的权力。当建文帝打算办燕王朱棣时,朱棣就起兵造反,自己做了皇帝。我感觉朱棣其实是自卫。后来,感情朱棣当上皇帝的第一件事,也是继续削弱藩王的势力。其实,大家都一样。


打工就不一样了,干得好不好,不是你说了算,是你的上级领导说了算,周围同事说了算,规章制度说了算。


因此,扁鹊三兄弟的现象就出现了。


扁鹊大哥,医术最高,能预防病人生病。扁鹊二哥,医术很高,能消灭病症在萌芽阶段。到扁鹊这里,只能到人快死了,开刀扎针,救人于生死之间。但是,世人都称扁鹊为神医。


如果你的领导是一个技术型的,同时他还能对你的工作质量做一些审查,那么他对你的评价,可能还具有些客观性。


但是,如果你碰到的是一个行政型的领导,他不是很懂医术,那他就只能像看医生治病一样,觉得救治好了快要死的人,才是高人。而对于预防重症这类防患于未然的事情,他会觉得你在愚弄他。


事实上,不少中小企业的领导,多是行政型领导。他们常常以忠心于老板而被提拔。


因此,为了自己获得一些利益。有些人常常是先把大病整出来,然后再治好,以此来体现自己的价值。


老维修工建设管道,水管里流的是燃气,燃气管的作用是排废水。出了问题,来一批一批的新师傅,都解决不了,越弄越乱。结果老维修工一出手,就把问题解决了。老板觉得,哎呀,看,还得是我的老师傅管用。


相反,如果你把事情打理的井井有条,没有一丝风浪,就像扁鹊大哥一样,我都不得病,还养你干啥,随便换谁都可以。这样的话,往往你的结局就多是被领导忽视。


我有好几个大领导都在大会上说过:你请假一周,你的部门连给你打一个电话的都没有,这说明你平时疏于管理,对于团队没有一丝作用!


从业这么多年,见过各种现实,很讽刺,就像是笑话。有一个同事,给程序加了一个30秒后延时执行。后来,领导让他优化速度,他分4次,将30秒调到5秒。最后领导大喜,速度提高6倍,他被授予“超级工匠”的荣誉称号。


一个是领导的评价。还有一个是同事的评价。


我有一次,在自己的项目组里搞了个考核。考核的核心就是,干好了可以奖,干差了便会罚。我觉得这样挺好,避免伤了好人心,杜绝隧了闲人的意。结果因为其他项目组没有搞,所以我成了众矢之的。他为什么要搞?人家项目组都没有,就他多事,咱们不在他这里干了!


我想,国内的管理可能不是一种客观的结果制。而是另一种客观的”平衡制“。


就像是古代的科举,按照才学,按照成绩来说,状元每届多是江南的。但是,皇帝需要平衡,山西好久没有出个状元了,点一个吧。河南今年学子闹事,罢考,为了稳一稳人心,给一个吧。江南都这么多了,少一个没什么关系的。


他更多是要让各方都满意。一个”平衡“贯穿了整个古今现代的价值观。


有人遵循自己的内心做事,也有人遵循别人的内心做事。不管遵循哪一方,坚持就好,不要去轻易比较,各自有各自的付出和收获。


当路上都在逆行时,你会发

作者:TF男孩
来源:juejin.cn/post/7224764099187966010
现,其实是你在逆行。

收起阅读 »

给同学解决问题有感——天下前端是一家!

web
   在毕设如火如荼进行的过程中,大家设计xxx系统时都会有各种各样的界面,这不就到了本菜鸟的领域!hhh,小时候的画家梦也算实现了一半,只不过画笔变成了code~    最近,给两位同学解决了前端方面的问题,但都不是我学的javascript语言,摸索着平时...
继续阅读 »

   在毕设如火如荼进行的过程中,大家设计xxx系统时都会有各种各样的界面,这不就到了本菜鸟的领域!hhh,小时候的画家梦也算实现了一半,只不过画笔变成了code~


   最近,给两位同学解决了前端方面的问题,但都不是我学的javascript语言,摸索着平时学到的前端思想,还是成功的解决了这些问题,有感而发,记录下来~



  •    第一位出场的是正在自学python的学习委员,也是一位准研究生。他遇到的问题是,在a项目里定义了一个复杂界面,在b项目里定义了一个简单页面。他找到我的时候说,启动了b项目,但打开的却是a项目定义的页面。报错如下:


d60d9f2414f5a2867091a5a14db6e54.png
    看了他的页面,这路由和我学的不长得一毛一样!


8bb4fc7bab1580d1baa6692d0f2b801.png
  打开他的浏览器页面,看看页面的网络请求,404,我第一反应会不会是他路由的问题,导致找不到这个页面,显示了之前项目的界面。。。


dc111956dfdb5bc954fd74c3bfdea11.png
  但我转念一想,404 应该不会显示另一个项目的界面呀,除非请求的是之前项目的服务器。再注意到warning中的,use a server instead,这不就是换一个服务器,那一定是之前的端口被占用了,所以相当于请求之前的服务器。于是,搬出来我只会一个cd的小黑窗:


1684658842987.png
   解决占用之后,重启项目,完整的展示了新项目中的页面~
这个问题准确的说属于计网,但前后端的思想还是在里面。果然还是基础的东西~



  • 第二位出场的是一位在做安卓应用(毕设项目)的女同学,躺在床上收到她的连环问,导航栏隐藏?我直接惊坐起:


1684659076469.png
   仔细听了她的描述之后,在有了导航栏之后,页面某些按钮的位置发生了偏差,如下图:


4637c8740c325359481bc6fb139af42.png
  原本卡其色圆圈应该和下面蓝色圆圈重合,通关之后,显示下面的颜色。
因为下面一层的按钮是嵌套的背景图片里,所以不知道固定位置,不能用绝对定位控制两个按钮的位置完全重合(她是用可视化拖动的方法做的页面)。
让导航栏透明肯定是有办法的,但我想如果只透明 但仍然占据文档流,那还是没用呀!


1684659743090.png
  找到网友的方法,尝试之后,模拟机显示确实ok,隐藏了导航栏,位置也消除了偏差,在鼠标接触到导航栏位置时,导航栏显现,很人性化!但她在手机上通过apk安装包查看,还是有一定的偏差,我想是因为屏幕尺寸的问题吗?看了他的代码用的都是相对单位dp,应该可以自适应的呀,这个就不懂了,毕竟适配所有机型的问题,我在实习的时候也很头疼!




PS:
在此应该鸣谢一下我的老师和队友,坚定让我自己做了前后端分离的一个项目,自己建数据库,自己写接口,对前后端请求的
作者:MindedMier
来源:juejin.cn/post/7235458133505491005
发送接收还是有更细致的了解!
收起阅读 »

Kotlin 密封接口sealed interface

什么是密封接口?密封接口(sealed interface)是kotlin 1.5引入的一个新特性,它可以让我们定义一个限制性的类层次结构,也就是说,我们可以在编译时就知道一个密封接口有哪些可能的子类型。这样,我们就可以更好地控制继承关系,避免出现意外的子类型...
继续阅读 »

什么是密封接口?

密封接口(sealed interface)是kotlin 1.5引入的一个新特性,它可以让我们定义一个限制性的类层次结构,也就是说,我们可以在编译时就知道一个密封接口有哪些可能的子类型。这样,我们就可以更好地控制继承关系,避免出现意外的子类型。

密封接口与密封类(sealed class)类似,都可以用来表示一组有限的可能性。但是,密封类只能有一个实例,而密封接口的子类型可以有多个实例。此外,密封类只能被类继承,而密封接口可以被类和枚举类(enum class)实现。

要声明一个密封接口,我们需要在interface关键字前加上sealed修饰符:

sealed interface Error // 密封接口

一个密封接口可以有抽象或默认实现的方法,也可以有属性:

sealed interface Shape { // 密封接口
val area: Double // 属性
fun draw() // 抽象方法
fun printArea() { // 默认实现方法
println("The area is $area")
}
}

密封接口的优点

使用密封接口有以下几个优点:

  • 类型安全:由于密封接口的子类型是固定的,我们可以在编译时就检查是否覆盖了所有可能的情况。这样,我们就不会遗漏某些分支或者处理错误的类型。
  • 可读性:使用密封接口可以让我们清楚地看到一个类型有哪些变种。这样,我们就可以更容易地理解和维护代码。
  • 灵活性:使用密封接口可以让我们定义更多样化的子类型。我们可以使用数据类(data class),对象(object),普通类(class),或者另一个密封类(sealed class)作为子类型。我们还可以在不同的文件或模块中定义子类型。
  • 表达力:使用密封接口可以让我们利用多态(polymorphism)和继承(inheritance)来实现更复杂和优雅的设计模式。

密封接口在设计模式中的应用

设计模式是一些经过验证和总结的解决特定问题的代码结构和技巧。使用设计模式可以让我们编写出更高效,更可复用,更易扩展的代码。

下面,我们将介绍几种常见的设计模式,并展示如何使用密封接口来实现它们。

策略模式

策略模式(Strategy Pattern)是一种行为型设计模式,它可以让我们在运行时根据不同的情况选择不同的算法或策略。这样,我们就可以将算法的定义和使用分离,提高代码的灵活性和可维护性。

要实现策略模式,我们可以使用密封接口来定义一个策略的抽象,然后使用不同的子类型来实现具体的策略。例如,我们可以定义一个排序策略的密封接口,然后使用不同的排序算法作为子类型:

sealed interface SortStrategy { // 密封接口
fun sort(list: List<Int>): List<Int> // 抽象方法
}

object BubbleSort : SortStrategy { // 对象
override fun sort(list: List<Int>): List<Int> {
// 实现冒泡排序
}
}

object QuickSort : SortStrategy { // 对象
override fun sort(list: List<Int>): List<Int> {
// 实现快速排序
}
}

object MergeSort : SortStrategy { // 对象
override fun sort(list: List<Int>): List<Int> {
// 实现归并排序
}
}

然后,我们可以定义一个上下文类(Context Class),它可以持有一个策略的引用,并根据需要切换不同的策略:

class Sorter(var strategy: SortStrategy) { // 上下文类
fun sort(list: List<Int>): List<Int> {
return strategy.sort(list) // 调用策略的方法
}
}

最后,我们可以在客户端代码中使用上下文类来执行不同的策略:

fun main() {
val list = listOf(5, 3, 7, 1, 9)
val sorter = Sorter(BubbleSort) // 创建上下文类,并指定初始策略
println(sorter.sort(list)) // 使用冒泡排序
sorter.strategy = QuickSort // 切换策略
println(sorter.sort(list)) // 使用快速排序
sorter.strategy = MergeSort // 切换策略
println(sorter.sort(list)) // 使用归并排序
}

使用密封接口实现策略模式的优点是:

  • 我们可以在编译时就知道有哪些可用的策略,避免出现无效或未知的策略。
  • 我们可以使用数据类,对象,普通类或密封类作为子类型,根据不同的策略需要定义不同的属性和方法。
  • 我们可以在不同的文件或模块中定义子类型,提高代码的模块化和可读性。

如果使用java实现策略模式,我们可能需要定义一个接口来表示策略,然后使用不同的类来实现接口:

interface SortStrategy { // 接口
List<Integer> sort(List<Integer> list); // 抽象方法
}

class BubbleSort implements SortStrategy { // 类
@Override
public List<Integer> sort(List<Integer> list) {
// 实现冒泡排序
}
}

class QuickSort implements SortStrategy { // 类
@Override
public List<Integer> sort(List<Integer> list) {
// 实现快速排序
}
}

class MergeSort implements SortStrategy { // 类
@Override
public List<Integer> sort(List<Integer> list) {
// 实现归并排序
}
}

然后,我们也需要定义一个上下文类来持有和切换策略:

class Sorter { // 上下文类
private SortStrategy strategy; // 策略引用

public Sorter(SortStrategy strategy) { // 构造函数
this.strategy = strategy;
}

public void setStrategy(SortStrategy strategy) { // 设置策略方法
this.strategy = strategy;
}

public List<Integer> sort(List<Integer> list) {
return strategy.sort(list); // 调用策略的方法
}
}

最后,我们也可以在客户端代码中使用上下文类来执行不同的策略:

public static void main(String[] args) {
List<Integer> list = Arrays.asList(5, 3, 7, 1, 9);
Sorter sorter = new Sorter(new BubbleSort()); // 创建上下文类,并指定初始策略
System.out.println(sorter.sort(list)); // 使用冒泡排序
sorter.setStrategy(new QuickSort()); // 切换策略
System.out.println(sorter.sort(list)); // 使用快速排序
sorter.setStrategy(new MergeSort()); // 切换策略
System.out.println(sorter.sort(list)); // 使用归并排序
}

使用java实现策略模式的缺点是:

  • 我们不能在编译时就知道有哪些可用的策略,因为任何类都可以实现接口。
  • 我们只能使用类作为子类型,不能使用数据类或对象。
  • 我们必须在同一个包中定义子类型,不能在不同的文件或模块中。

访问者模式

访问者模式(Visitor Pattern)是一种行为型设计模式,它可以让我们在不修改原有类结构的情况下,为类添加新的操作或功能。这样,我们就可以将数据结构和操作分离,提高代码的扩展性和复用性。

要实现访问者模式,我们可以使用密封接口来定义一个元素(Element)的抽象,然后使用不同的子类型来实现具体的元素。例如,我们可以定义一个表达式(Expression)的密封接口,然后使用不同的子类型来表示不同的表达式:

sealed interface Expression { // 密封接口
fun accept(visitor: Visitor): Any // 抽象方法,接受访问者
}

data class Number(val value: Int) : Expression { // 数据类
override fun accept(visitor: Visitor): Any {
return visitor.visitNumber(this) // 调用访问者的方法
}
}

data class Sum(val left: Expression, val right: Expression) : Expression { // 数据类
override fun accept(visitor: Visitor): Any {
return visitor.visitSum(this) // 调用访问者的方法
}
}

data class Product(val left: Expression, val right: Expression) : Expression { // 数据类
override fun accept(visitor: Visitor): Any {
return visitor.visitProduct(this) // 调用访问者的方法
}
}

然后,我们可以定义一个访问者(Visitor)的接口,它可以为每种元素提供一个访问方法:

interface Visitor { // 访问者接口
fun visitNumber(number: Number): Any // 访问数字表达式
fun visitSum(sum: Sum): Any // 访问加法表达式
fun visitProduct(product: Product): Any // 访问乘法表达式
}

最后,我们可以定义不同的访问者实现类,它们可以为元素提供不同的操作或功能。例如,我们可以定义一个求值(Evaluate)访问者,它可以计算表达式的值:

class Evaluate : Visitor { // 求值访问者
override fun visitNumber(number: Number): Any {
return number.value // 返回数字本身
}

override fun visitSum(sum: Sum): Any {
return (sum.left.accept(this) as Int) + (sum.right.accept(this) as Int) // 返回左右子表达式之和
}

override fun visitProduct(product: Product): Any {
return (product.left.accept(this) as Int) * (product.right.accept(this) as Int) // 返回左右子表达式之积
}
}

我们还可以定义一个打印(Print)访问者,它可以打印表达式的字符串表示:

class Print : Visitor { // 打印访问者
override fun visitNumber(number: Number): Any {
return number.value.toString() // 返回数字的字符串
}

override fun visitSum(sum: Sum): Any {
return "(${sum.left.accept(this)}) + (${sum.right.accept(this)})" // 返回加法的字符串
}

override fun visitProduct(product: Product): Any {
return "(${product.left.accept(this)}) * (${product.right.accept(this)})" // 返回乘法的字符串
}
}

使用密封接口实现访问者模式的优点是:

  • 我们可以在编译时就知道有哪些可用的元素,避免出现无效或未知的元素。
  • 我们可以使用数据类,对象,普通类或密封类作为子类型,根据不同的元素需要定义不同的属性和方法。
  • 我们可以在不同的文件或模块中定义子类型,提高代码的模块化和可读性。
  • 我们可以在不修改元素类的情况下,为它们添加新的访问者和操作。

如果使用java实现访问者模式,我们可能需要定义一个抽象类来表示元素,然后使用不同的子类来继承元素:

abstract class Expression { // 抽象类
public abstract Object accept(Visitor visitor); // 抽象方法,接受访问者
}

class Number extends Expression { // 子类
private int value; // 属性

public Number(int value) { // 构造函数
this.value = value;
}

public int getValue() { // 获取属性值方法
return value;
}

@Override
public Object accept(Visitor visitor) {
return visitor.visitNumber(this); // 调用访问者的方法
}
}

class Sum extends Expression { // 子类
private Expression left; // 属性
private Expression right; // 属性

public Sum(Expression left, Expression right) { // 构造函数
this.left = left;
this.right = right;
}

public Expression getLeft() { // 获取属性值方法
return left;
}

public Expression getRight() { // 获取属性值方法
return right;
}

@Override
public Object accept(Visitor visitor) {
return visitor.visitSum(this); // 调用访问者的方法
}
}

class Product extends Expression { // 子类
private Expression left; // 属性
private Expression right; // 属性

public Product(Expression left, Expression right) { // 构造函数
this.left = left;
this.right = right;
}

public Expression getLeft() { // 获取属性值方法
return left;
}

public Expression getRight() { // 获取属性值方法
return right;
}

@Override
public Object accept(Visitor visitor) {
return visitor.visitProduct(this); // 调用访问者的方法
}
}

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

Java Map 所有的值转为String类型

可以使用 Java 8 中的 Map.replaceAll() 方法将所有的值转为 String 类型:Map<String, Object> map = new HashMap<>(); // 添加一些键值对 ma...
继续阅读 »

可以使用 Java 8 中的 Map.replaceAll() 方法将所有的值转为 String 类型:

Map<String, Object> map = new HashMap<>();
// 添加一些键值对
map.put("key1", 123);
map.put("key2", true);
map.put("key3", new Date());

// 将所有的值转为 String 类型
map.replaceAll((k, v) -> String.valueOf(v));

上面的代码会将 map 中所有的值都转为 String 类型。


HashMap 是 Java 中使用最广泛的集合类之一,它是一种非常快速的键值对存储方式,可以用于存储和访问大量的数据。下面介绍一些 HashMap 的常用方法:

  1. put(key, value) :向 HashMap 中添加一个键值对。
HashMap<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
  1. get(key) :根据键取出对应的值。
Integer value = map.get("apple");
  1. containsKey(key) :判断 HashMap 中是否包含指定键。
if (map.containsKey("apple")) {
// ...
}
  1. containsValue(value) :判断 HashMap 中是否包含指定值。
if (map.containsValue(1)) {
// ...
}
  1. remove(key) :根据键删除 HashMap 中的一个键值对。
map.remove("apple");
  1. keySet() :返回 HashMap 中所有键的集合。
Set<String> keys = map.keySet();
  1. values() :返回 HashMap 中所有值的集合。
Collection<Integer> values = map.values();
  1. entrySet() :返回 HashMap 中所有键值对的集合。
Set<Map.Entry<String, Integer>> entries = map.entrySet();

以上是常用的 HashMap 方法,还有其他一些方法可以查阅相关文档获得更多信息。


HashMap 的存储原理主要是基于 Hash 算法和数组实现的。 在 HashMap 中,每个键值对对应一个数组中的一个元素,这个元素叫做“桶(bucket)”或“槽(slot)”。

数组的索引值就是通过 Hash 算法计算出来的,每个桶中存放的是一个链表,存储了 key-value 对。如果不同的键值对计算出来的索引值相同,则这些键值对会被放到同一个桶中,以链表的形式存储在该桶中,这就是 HashMap 的解决冲突的方法。

HashMap 的存储过程如下:

  1. 当使用 put 方法将一个键值对添加到 HashMap 中时,首先会根据键的 hashCode 值计算出数组索引位置。具体方法是,将 hashCode 值进行一些运算,得到一个数组索引值。这个索引值是键值对在数组中的位置。
  2. 如果数组中该位置为空,那么就可以直接将键值对存储在该位置,完成添加操作。
  3. 如果该位置已经有了键值对,那么就需要通过比较键的 equals 方法,来判断是更新该键值对的值,还是添加一个新的键值对。
  4. 如果表示键值对的链表长度较长,就会影响到 HashMap 的性能,因为在查找时可能需要遍历整个链表。

为此,Java 8 引入了“红黑树”(Red-Black Tree) 的数据结构,可以将链表转换为树,以提高性能。 需要注意的是,HashMap 是非线程安全的,如果在多线程环境下使用,可能会发生一些异常情况。如果需要在多线程环境中使用 HashMap,可以使用 ConcurrentHashMap 或 Collections.synchronizedMap 方法来实现线程安全。


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

用户被盗号?你肯定缺少这些设计

前言在之前的文章【你的登录接口真的安全吗?】中,我们在用户登录安全方面做了很多设计,就是保护用户的账号安全,但是!!我相信做过用户体系的开发或产品都知道,用户的密码泄漏是一个不可避免的事件,总会有用户因为各种奇奇怪怪的原因而导致账号被盗,进而导致用户信息泄漏、...
继续阅读 »

前言

在之前的文章【你的登录接口真的安全吗?】中,我们在用户登录安全方面做了很多设计,就是保护用户的账号安全,但是!!我相信做过用户体系的开发或产品都知道,用户的密码泄漏是一个不可避免的事件,总会有用户因为各种奇奇怪怪的原因而导致账号被盗,进而导致用户信息泄漏、虚拟数据丢失、经济损失等各种后果。那针对这种场景,我们可以通过什么手段来尽量预防呢?

几种实现方式来判断用户登录环境

一般这种情况,业界最简单的处理方式就是识别用户登录环境是是否正常:比如是否是常登录IP、或者是常登录设备等,如果不是,那么则进行限制、二次验证、用户告警等操作。

异地登录

首先是用户异地登录,一般场景下,用户的使用环境大部分时间都是不怎么变化的,比如公司、家里、宿舍或学校等。那么我们可以基于用户的使用IP,来做风险管理。

伪代码实现

   def login(username, password, ip):
# 登录失败,简化其它流程
if not do_login(username, password):
return Result(100, '登录失败')

# 检查用户登录环境异常
if !check_login_env(username, ip):
# 发送短信或邮件给用户,告知用户账号在非常用地登录
send_notice(username, ip)
# 前端收到这个状态码后跳转到二次验证页面
return Result(101, '非常登录地登录')

# 登录成功,异步记录当前ip
async_log_ip(username, ip)
return Result(0, '登录成功')

流程很简单,用户登录成功后,进行一次环境校验,判断用户当前登录ip是否为常登录地,如果不是,那么先给用户发送邮件或短信通知,然后返回对应状态码给前端,跳转到二次校验的页面。

二次校验可以通过APP扫码、手机验证码等方式登录。

*上面的代码中还缺少很重要的一步操作,怎么判断用户的IP是否是常登录IP?我们可以基于IP来实现,但是我们现在家用网络的IP基本上都不是固定IP,所以实际场景下可能更多的是使用地区来判断,比如城市。

伪代码实现

   def check_login_env(username, ip):
cur_city = get_city_from_ip(ip)
# 此城市是否在近半年的常登录城市中
city = get_last_half_year_cities(username)
return cur_city == city


def log_env(username, ip):
city = get_city_from_ip(ip)
# 记录当前登录城市
insert_login_ip(username, city, ip, datetime.now())
# 统计常登录城市
# 查询的近半年登录次数最多的城市
city = select_max_login_city_last_half_year()
# 设置常登录城市
set_last_half_year_cities(username, city)

上面只是简单的实现,整个判断过程比较粗糙,准确性也不够高。实际产生中,我们可以根据ip位置,结合算法来计算常登录地;或者结合下面的其它方式共同判断。

非常用设备登录

除了通过ip来判断用户使用环境外,我们一般还会结合用户设备来判断,特别是移动端应用,用户设备大部分时候是固定不变的。
设备信息一般可以使用设备指纹的方式,通过采集设备的各种信息,生成一个唯一标识,用于标识此设备。如果设备指纹不存在,那么证明用户在新设备上登录,则进行二次验证。

伪代码实现

   def login(username, password, ip, device_info):
# 登录失败,简化其它流程
if not do_login(username, password):
return Result(100, '登录失败')

# 检查用户登录环境异常
if !check_login_env(username, ip):
# 发送短信或邮件给用户,告知用户账号在非常用地登录
send_notice(username, ip)
# 前端收到这个状态码后跳转到二次验证页面
return Result(101, '非常登录地登录')

if !check_device_env(username, device_info):
send_notice(username, device_info)
# 前端收到这个状态码后跳转到二次验证页面
return Result(102, '正在使用新设备登录')

# 登录成功,异步记录当前ip
async_log_ip(username, ip)
# 记录设备信息
async_log_device(username, device_info)
return Result(0, '登录成功')

用户设备信息采集需要征得用户同意,那万一无法采集信息怎么办? 我们也可以想办法在用户第一次登录时,在用户设备中生成记录一个唯一ID并存储在设备中,用于标记这个设备。

异常IP登录

这个和异地登录不同的是,我们可以维护一个IP黑名单,只要是用户登录的IP在黑名单内,则一定要求用户做二次验证。

黑名单的来源主要是通过购买、自行采集的方式获取到的黑产IP

用户风控

上面的各种方式,都不是孤立的,更多的是结合起来,包括其它更多的判断逻辑,来最终决定用户的登录环境是否存在风险,而这部分功能,我们一般会把它抽离出来,单独做为一个风控服务实现。 我们输入用户登录相关的数据,比如用户ID、登录IP、设备信息等,风控服务结合历史数据、用户行为数据等,通过大数据分析以及我们配置的风控规则,最终输出给我们一个风险级别,然后再根据风险级别决定是否需要做后续的措施。

总结

今天主要讲了几种预防用户账号被盗的手段以及简单的实现,相信大家在使用各种产品的时候也有碰到过对应的功能,我们在做用户体系设计的时候,前期可能用户量比较少,但是也可以尽量的考虑安全相关的设计,体量小有小的做法,大有大的做法,但是做了总比没做好。希望读完这篇文章大家有所收获~


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

服务器被爬虫恶意攻击怎么办?

在有预算的情况可以采购第三方服务防火墙,没钱就使用开源的WAF进行防护。WAF防火墙的基本防护原理WAF(Web 应用防火墙)可以使用多种技术来防止恶意爬虫攻击,例如:黑名单:WAF 可以使用黑名单技术来过滤恶意爬虫的请求。黑名单中包含一些已知的爬虫用户代理(...
继续阅读 »

在有预算的情况可以采购第三方服务防火墙,没钱就使用开源的WAF进行防护。

WAF防火墙的基本防护原理

WAF(Web 应用防火墙)可以使用多种技术来防止恶意爬虫攻击,例如:

  1. 黑名单:WAF 可以使用黑名单技术来过滤恶意爬虫的请求。黑名单中包含一些已知的爬虫用户代理(User-Agent),WAF 可以检查每个请求的用户代理,并拒绝那些与黑名单匹配的请求。

  2. 限制访问频率:WAF 可以使用限制访问频率的技术来防止恶意爬虫攻击。例如,可以设置每个 IP 地址在一定时间内只能访问网站的某个页面一定次数。如果超过了访问次数限制,则 WAF 会拒绝该 IP 地址的请求。

  3. JavaScript 检测:WAF 可以使用 JavaScript 检测技术来检测爬虫。例如,可以在页面中嵌入一些 JavaScript 代码,这些代码会检测浏览器的一些属性(如是否支持 JavaScript、是否支持 Cookie 等),如果检测到浏览器属性与正常用户不同,则 WAF 可以认为该请求来自恶意爬虫,从而拒绝该请求。

  4. 隐藏字段:WAF 可以在页面中添加一些隐藏的字段,这些字段只有正常用户才会填写,而恶意爬虫往往无法正确填写这些字段。例如,可以在登录表单中添加一个隐藏字段(如 CSRF Token),如果该字段的值不正确,则 WAF 可以认为该请求来自恶意爬虫,从而拒绝该请求。

  5. 图片验证码:WAF 可以使用图片验证码技术来防止恶意爬虫攻击。例如,可以在某些敏感操作(如注册、登录、发表评论等)前,要求用户输入验证码。如果 WAF 发现多次输入错误验证码的请求,则可以认为该请求来自恶意爬虫,从而拒绝该请求。

使用注意事项

关于 WAF 的具体使用方法,常见的开源 WAF 包括 ModSecurity、Naxsi、WebKnight 等。这些 WAF 都可以通过配置文件来设置规则,过滤恶意请求。一般来说,使用 WAF 的步骤如下:

  1. 安装 WAF:根据 WAF 的安装说明,安装 WAF 并将其集成到 Web 服务器中。

  2. 配置规则:编辑 WAF 的配置文件,设置需要过滤的请求规则,例如黑名单、访问频率限制等。

  3. 测试 WAF:启动 Web 服务器,并针对一些已知的恶意请求进行测试,验证 WAF 是否能够正确过滤这些请求。

  4. 持续维护:WAF 的规则需要根据实际情况不断更新和维护,以保证其能够有效地防止恶意攻击。

开源WAF的优缺点

ModSecurity、Naxsi、WebKnight 都是常见的开源 WAF,它们各有优缺点。

  1. ModSecurity

优点:

  • 可以通过自定义规则来检测和防止各种攻击,包括 SQL 注入、XSS 攻击、命令注入、文件包含等。
  • 支持正则表达式,可以灵活地匹配和过滤请求。
  • 支持 HTTP/2 和 WebSocket 协议。
  • 有一个活跃的社区,提供了丰富的文档和示例代码。
  • 可以与 Apache、Nginx、IIS 等常见的 Web 服务器集成。

缺点:

  • 学习曲线较陡峭,需要一定的安全知识和经验。
  • 配置复杂,需要仔细调整规则以避免误报和漏报。
  • 对于高并发的 Web 应用,可能会对性能产生一定的影响。
  1. Naxsi

优点:

  • 专门针对 Web 应用安全的防火墙,易于使用和配置。
  • 通过学习模式(Learning Mode)和白名单模式(Whitelist Mode)来防止误报。
  • 支持自定义规则,可以根据实际需求进行扩展。
  • 对于高并发的 Web 应用,性能表现较好。

缺点:

  • 仅支持 Nginx Web 服务器。
  • 防护能力相对较弱,只能检测和防止一些常见的攻击,如 SQL 注入、XSS 攻击等。
  • 社区活跃度不高,文档相对较少。
  1. WebKnight

优点:

  • 支持多种 Web 服务器,包括 IIS、Apache、Tomcat 等。
  • 可以通过自定义规则来检测和防止各种攻击,包括 SQL 注入、XSS 攻击、命令注入等。
  • 支持正则表达式,可以灵活地匹配和过滤请求。
  • 有一个活跃的社区,提供了较为详细的文档和示例代码。

缺点:

  • 学习曲线较陡峭,需要一定的安全知识和经验。
  • 配置较为复杂,需要仔细调整规则以避免误报和漏报。
  • 对于高并发的 Web 应用,可能会对性能产生一定的影响。

总的来说,选择哪种 WAF 主要取决于实际需求和应用场景。如果需要防范多种攻击,并且具备一定的安全知识和经验,可以选择 ModSecurity;如果需要一个易于使用和配置的 WAF,并且仅需要防范一些常见的攻击,可以选择 Naxsi;如果需要一个支持多种 Web 服务器的 WAF,并且对性能要求较高,可以选择 WebKnight。

需要注意的是,WAF 并不能完全防止恶意爬虫攻击,因为恶意攻击者可以使用各种技术来规避 WAF 的过滤。因此,在使用 WAF 的同时,还需要采取其他措施来增强网站的安全性,例如使用 SSL/TLS 加密技术、限制敏感操作的访问、使用验证码等。


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

做个清醒的程序员之努力工作为哪般

阅读时长约10分钟,共计2268个字如果要问自己这样一个问题:“我们工作的意义到底是什么?”会得到怎样的答案?是为了安身立命?是为了满足别人的期待?是为了得到社会的认同?抑或是索性认为工作是无意义的?如果我说:工作的意义在于自我实现,你会同意吗?你会觉得这样的...
继续阅读 »

阅读时长约10分钟,共计2268个字

如果要问自己这样一个问题:“我们工作的意义到底是什么?”会得到怎样的答案?

是为了安身立命?是为了满足别人的期待?是为了得到社会的认同?抑或是索性认为工作是无意义的?

如果我说:工作的意义在于自我实现,你会同意吗?你会觉得这样的观点很片面吗?你会觉得这很理想化吗?

其实,依我的工作经验来看,上面列举出的常见答案其实都有道理。人在不同的阶段,不同状态,工作的意义便会发生变化。

坦率地讲,我从开始工作之后,就下定决心不再啃老。简单地说,就是不再向父母伸手要钱。于是,如何保障自己的温饱就是我工作的最首要和最基本目的。也就是说,我刚开始的时候,工作就是为了挣钱。

刚起步的时候我一个月有多少工资呢?很少,2500块。即便是毕业后转正,拿到手也只有三四千块。

但是,这些钱已经可以很好地满足我的温饱需要。即使我出去租房,不在家吃饭,这些钱其实也是够的,只不过很可能剩不下分文。很感谢我的父母,在我事业刚刚开始的时候,照顾了我的起居生活。

一开始,我眼中工作的意义就是为了能养活我自己,就是为了那三两碎银。所以,为了养活自己而选择工作,挣钱,然后达到目的,我至今也不觉得有什么丢人的。

后来呢?因为我一直在家吃饭,午饭的话公司也有食堂,所以基本没什么开销。唯一生活上的花销就是衣服和鞋,可偏偏我穿衣穿鞋算是比较省的,一件衣服基本上少说穿个四五年,鞋的话就更久了,我现在还经常穿七八年前买回来的经典款。我认为穿衣方面,买经典款总是不会错,而且很难因为流行趋势而过时。

话说回来,随着我的小金库慢慢积累变多,我就不愁“安身立命”的目标了。因为我是家族后代中唯一的男性,所以心中总会有一种使命感,虽然没有人给我这方面的压力。我感受到的最大的责任感其实是想让家人生活得更美好的目标,虽然我父母在这方面依然没有表现出很大的期待。

于是凭着这个我自认为的“责任感”,一直努力工作了很多年。其实我的想法很简单,就是想让爱自己和自己爱的人过得好一点。我觉得凭本事挣更多的钱,然后达到这个目标,更是无可厚非的事情,也没什么错。

后来呢?我其实很早就有写博客的习惯,随着读者对我的文章产生认同,更重要的是有出版社编辑的认同,我就产生了要获得社会认同感的目标。虽说是“社会认同感”,其实它所包括的内容很广泛。比如读者的、家人的、老同学的、(前)同事的等等。这种“社会认同感”还会顺便带来他人的尊重甚至是敬重。至少我这次的工作和上次的工作,在面试的时候基本上技术方面是被认可的,免去了“八股文”的考验。不过老实讲,如果考验我面试“八股文”,大概率我还真得吃败仗。

再到现在,金钱对我的诱惑依然存在,但已经大幅降低了。更多的是考虑如何实现自己的价值,真正地释放自己的潜力,对这个社会,对这个世界发挥光和热。也就是我在一开始说的“工作的意义在于自我实现”。

好了,这就是我的故事。一开始为了满足温饱,我去工作,去挣钱;后来,为了得到别人的认可,获得社会认同感而努力工作,顺便把钱给挣了,引一句读者的评论:“挣钱是重要的事情中最不重要的”;再到现在,自我实现对我是最重要的。

所以,在不同阶段,不同状态,对工作意义产生不同的观点,我觉得都是正常的,也都是正确的。

但是,你知道吗?在我刚刚工作的时候,我就立下目标,要在这个世界上留下点什么,留下自己活过的印记,不想虚度此生。但当时无论如何也想不到,自己会成为作者,通过文字和图片把枯燥的编程知识教授给需要的人。

不知道你有没有听说过“马斯洛需求层次”,莫名其妙地,从一开始我就攀登这个需求金字塔,直到现在,已过去十余年。

有读者说我是“长期主义者”,以现在的认知,我愿意做一个“长期主义者”。但当初的我,哪懂什么“长期主义”。我更偏向于用“轴”、“固执”或是“不见棺材不落泪,不撞南墙不死心”这类的修饰词来形容自己。所幸的是,在我“固执”的一路上,受到了很多人的帮助与支持,还有上天的眷顾。“运气”、“机遇”占了很大的比重。

回到最初的问题:“我们工作的意义到底是什么”?如果我们对一个疲于奔命的人说:“你要实现自我”。很遗憾,“实现自我”在这个人眼中,也许就只是保障温饱,他甚至会转过头来和别人说我们是神经病。因为阶段不同,状态不同,追求的东西自然也会不同。也许这很无情,很冷血。但它很真实,也很正常。

但即便我们处在最开始的温饱阶段,着眼于生计,但目光一定要看到那个“未来”,坚定不移地坚持走“长期主义”的道路。

另一方面,在学校里,往往努力就会有结果,这是具有极大的确定性的。踏入社会之后,这种确定性消失了,我们往往努力了,却并没有得到想要的结果。很多人在鼓吹“选择大于努力”,选择固然重要。选错了,越努力,走得越偏。但这种话听多了,就要当心它是否成为了不努力的借口。努力是为了在机会到来的时候,我们有能力抓住他。这就好像不慎跌落坑里的人,有人扔绳子救他,他得努力抓住绳子,才有被救的可能。如果一味求救,却不选一种力所能及的方法去做,无论有多少次生还的机会,也会错过。

所以,任何时候也不要轻视努力与坚持的重要性。这看上去很笨很傻,但它却是每一个平凡人实现“自我价值”的可行之路。就像歌中唱的那样:“老天爱笨小孩”。在这个充满不确定性的时代,更应如此。不确定性就像基因突变,可能会变糟,也可能会变好。当有好事到来时,便是好运来临之际。和“选择大于努力”相比,我更倾向于相信“机会总是留给有准备的人”。这句话出自法国著名的微生物学家、化学家路易斯·巴斯德,告诫人们:机遇往往不易察觉,可遇不可求,容易稍纵即逝。作为普通人,若要抓住机遇,就要把功夫用在平时,甚至是有点傻的、偏执的努力。


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

思考:如何做一名合格的面试官?

背景关于招聘,在最近一段时间有幸又参与到了面试工作。这次面试也包含一些相对高级一点的岗位,如何招到合适的人成为了最近一段时间一直在思考的一个点。整体上感觉自己还是没有章法,没有清晰的思路(做不到因人而异),这种情况对自己对他人都是不负责任的。因此,简单做一些总...
继续阅读 »

背景

关于招聘,在最近一段时间有幸又参与到了面试工作。这次面试也包含一些相对高级一点的岗位,如何招到合适的人成为了最近一段时间一直在思考的一个点。

整体上感觉自己还是没有章法,没有清晰的思路(做不到因人而异),这种情况对自己对他人都是不负责任的。

因此,简单做一些总结思考,边面边想边改进吧。

招聘者的目标

首先,作为招聘者,都希望能找到一些厉害的人,成本不应该是他要考虑的问题。但现实总是相反。

面试官:这是我这次招聘的需求,要这个...... 那个...... 总之,能力越强越好。

公司:这次招聘成本范围已发给你了,注意查收。

面试官:......

所以,在成本有限的情况下,面试官要做的就是找到那些会发光的人。对面试官来说,招到一个即战力(不亏),招到一个高潜力(赚翻了)。

因此,招聘者的目标都是希望能够招到 能力 > 成本 的人。

梳理招聘需求

招聘的需求是需要面试官提前梳理好的,面试官作为团队的组建者,一定要提前规划好招聘需求。比如:

  1. 我要找技术强的。(我不懂的他来)
  2. 我要找态度好的。(我说啥都听)
  3. 我要找有责任心的。(不用我说就把活干得很漂亮)
  4. 我要找学习能力强的。(自我提升,啥需求都能接的住)
  5. 最好还能带团队。(这样我就轻松了)
  6. 最后一定要稳定。(这样我就一直轻松了)

哈哈,先开个玩笑。虽然我真的想......

现实就像上边提到的,招聘者希望的求职者模样。虽然知道不可能,但还是忍不住想要(我控制不住我几己呀!),所以在有限的面试时间内,问了很多方方面面的问题......

面试结束后:我问了那么多才答上来那么几个问题,不行,下一个......

面了几天后:人怎么这么难招呢?

所以真正的招聘需求应该是下边这样的:

  1. 我要招一个领导者还是执行者:这个一定要想清楚,两者考察维度完全不一样。
  2. 我要技术强的:想好哪方面技术强,不要妄图面面俱到。
  3. 我要找有责任心的,学习能力强的,稳定的:想好怎么提问,如何判断。

如果能够做到上边的三点,相信招进来的人应该都是OK得。PS:先做到不亏。

领导者Or执行者

为什么把这个作为第一点,上边也提到过,两者考察维度完全不一样。一场面试,时间就那么点,所以要有针对性。

先说领导者

如果招聘领导者。试想一下领导者有哪些特点,什么样的人你愿意让他成为领导者。

  1. 业务理解程度深,不仅限于产品规划的业务需求,还要有自己的理解和看法。
  2. 技术能力强,通常一个技术方案便能提现出来,方案好不好,考虑全面不全面。
  3. 抗压能力强,能够承担工作压力,这里不是指加班(当然加班也算),更多的是来自于技术,业务的困难和挑战。

以上三点并不代表全部,仅做参考。那么如何在面试中确认呢?

  1. 业务理解程度主要通过追问细节的方式来确认。在你不了解的情况下,依然能够给你讲明白,这个业务是做什么的,关键核心点是什么,核心点有什么难度和挑战,最后是怎么解决的,解决的是否完美,不完美的原因。如果能够回答的不错那就基本合格了。最后可以再多问一下: 有没有哪些产品提出的需求,你认为不合理或者不适合当前产品现状的?这个问题只要回答的有一定高度,那就完美了。

    ps: 如果面试者认为没有什么难度和挑战,只能证明他自己没有深度参与或主导该业务。再简单的系统,也不可能一点问题都没有,如果真的没有,那么完全没有必要安排团队去专门负责。没有简单的系统,只有简单的思考。

    举个栗子,用户管理(用户CRUD)系统我们一听可能都觉得很简单,早期,用户注册要填一堆的东西,现在都是各种登录渠道,非常的方便。站在现在的角度,对于早期的用户管理来说,如何提升用户注册效率,增加用户量就是一个有难度有挑战的事情。

  2. 技术能力,我简单分为有效技术能力和无效技术能力。无效技术能力代指无用且无聊的八股,当然也不是所有的八股都无用。有效技术能力我理解就是解决问题的能力,而解决问题不在于使用的技术手段与否高明,是否先进,只要贴合业务场景,我都会认为有技术能力。反而那些八股回答的头头是道,解决实际项目问题无一用到的会严重减分。

  3. 抗压能力,项目经验是能反应出来一些信息的:有难度,有挑战的事情你不会交给一个不合适的人来做的,所以如果简历的项目经验中有类似的经验那么就证明别人已经帮你筛选过了。PS:别忘了鉴别一下。

再说执行者

还是试想一下,好的执行者有哪些特质:

  1. 注重细节,考虑问题全面。
  2. 责任心强,不会随便应付了事。
  3. 技术OK,至少基础没有问题。

同样,上述三点仅做参考。

  1. 注重细节,直接体现其实跟方案的完善程度有关,所以问问技术方案的异常情况是如何考虑的。另外,直接体现其实就是BUG比较少,当然这个一般人肯定不会说自己BUG多,所以可以问问,对于如何减少BUG量,有没有心得。(这个问题目前来看基本没啥用,哈哈)

  2. 责任心强。责任心如何体现的我也说不太清楚,思考以后认为加班算一方面,有加薪、晋升算一方面,面试中很难直接体现,只能凭感觉。和第一条注重细节一样,我面试全凭聊完之后的直觉,这种大家都会有,而且一般来说准确率也不错。

    PS: 其实在面试沟通过程中,一问一答,很多事情靠直觉、面向也能猜个七七八八,玄学的东西这里不多说。说一点有科学依据的,人的性格简单分为外向、内向,两者都有其各自的特质,通常来说,内向者的特质更多时候适合于执行者。感兴趣的可以去了解一下两种性格特质,有益于团队管理。

  3. 技术OK。这个不做多说了,一定要多问实际使用的,不用的就不要问了,可能用的适当问一下,实际使用的也可以拔高一下往深了问问。比如:mysql都在用,mysql的八股可以多问几个。JVM这种开发基本不用的,简单问一下得了(我一般是不问的)。


这一篇先到这里把,关于技术强、责任心其实也简单提了一下。关于这两点,后续结合实际情况再更新吧。


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

项目提交按钮没防抖,差点影响了验收

前言一个运行了多年的ToB的项目,由于数据量越来越大,业务越来越复杂,也一直在迭代,今年的阶段性交付那几天,公司 最大的客户 现场那边人员提出,某某某单据页面速度太慢了,点击会出现没反应的情况,然后就多点了几次,结果后面发现有的数据重复提交...
继续阅读 »

前言

一个运行了多年的ToB的项目,由于数据量越来越大,业务越来越复杂,也一直在迭代,今年的阶段性交付那几天,公司 最大的客户 现场那边人员提出,某某某单据页面速度太慢了,点击会出现没反应的情况,然后就多点了几次,结果后面发现有的数据重复提交了,由于数据错误个别单据流程给弄不正常了,一些报表的数据统计也不对了,客户相关人员很不满意,马上该交付了,出这问题可还了得,项目款不按时给了,这责任谁都担不起🤣

领导紧急组织相关技术人员开会分析原因

初步分析原因

发生这个情况前端选手应该会很清楚这是怎么回事,明显是项目里的按钮没加防抖导致的,按钮点击触发接口,接口响应慢,用户多点了几次,可能查询接口还没什么问题,如果业务复杂的地方,部分按钮的操作涉及到一些数据计算和后端多次交互更新数据的情况,就会出现错误。

看下项目情况

用到的框架和技术

项目使用 angular8 ts devextreme 组合。对!这就是之前文章提到的那个屎山项目(试用期改祖传屎山是一种怎么样的体验

项目规模

业务单据页面大约几百个,项目里面的按钮几千个,项目里面的按钮由于场景复杂,分别用了如下几种写法:

  • dx-button
  • div
  • dx-icon
  • input type=button
  • svg

由于面临交付,领导希望越快越好,最好一两天之内解决问题

还好我们领导没有说这问题当天就要解决 😁

解决方案

1. 添加防抖函数

按钮点击添加防抖函数,设置合理的时间

function debounce(func, wait) {
let timeout;
return function () {
if(timeout) clearTimeout(timeout);
timeout = setTimeout(func, wait)
}
}

优点

封装一个公共函数,往每个按钮的点击事件里加就行了

缺点

这种情况有个问题就是在业务复杂的场景下,时间设置会比较棘手,如果时间设置短了,接口请求慢,用户多次点击还会出现问题,如果时间设置长了,体验变差了

2. 设置按钮禁用

设置按钮的 disabled 相关属性,按钮点击后设置禁用效果,业务代码执行结束后取消禁用

this.disabled = true
this.disabled = false

优点

原生按钮和使用的UI库的按钮设置简单

缺点

diviconsvg 这种自定义的按钮的需要单独处理效果,比较麻烦

3. 请求拦截器中添加loading

在请求拦截器中根据请求类型显示 loading,请求结束后隐藏

优点

直接在一个地方设置就行了,不用去业务代码里一个个加

缺点

由于我们的技术栈使用的 angular8 内置的请求,无法实现类似 axios 拦截器那种效果,还有就是项目中的接口涉及多个部门的接口,不同部门的规范命名不一样,没有统一的标准,在实际的业务场景中,一个按钮的行为可能触发了多个请求,因此这个方案不适合当前的项目

4. 添加 loading 组件(项目中使用此方案)

新增一个 loading 组件,绑定到全局变量中,按钮点击触发显示 loading,业务执行结束后隐藏。

loading 组件核心代码

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
  providedIn: 'root'
})
export class LoadingService {
  private isLoading$ = new BehaviorSubject<boolean>(false);
  private message$ = new BehaviorSubject<string>('正在加载中...');
  constructor() {}
  show(): void {
    this.isLoading$.next(true);
  }
  hide(): void {
    this.isLoading$.next(false);
  }
}

主要是 show() 和 hide() 函数,将 loading 组件绑定到 app.components.ts 中,绑定组件到window 对象上,

window['loading'] = this.loadingService

在按钮点击时触发 show() 函数,业务代码执行结束后触发 hide() 函数

window['loading'].show();
window['loading'].hide();

优点

这种方式很好的解决了问题,由于 loading 有遮罩层还避免了用户点击某提交按钮后,接口响应慢,这时候去点击了别的操作按钮的情况。

缺点

需要在业务单据的按钮提交的地方一个个加

问题来了,一两天解决所有问题了吗?

这么大的项目一两天不管哪种方案,把所有按钮都处理好是不现实的,经过分析讨论,最终选择了折中处理,先把客户提出来的几个业务单据页面,以及相关的业务单据页面添加上提交 loading 处理,然后再一边改 bug 一边完善剩余的地方,优先保证客户正常使用


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

Android View绘制原理 - RenderNode

这一篇文章我们继续分析另外一个重要的类RenderNode, 这个在前面绘制流程里有也有提到,这里我将更加深入的介绍这个类1 简介RenderNode是一个绘制节点,一个大的界面是由很多小的绘制单元组成,这个正如View的层级结构,整个界面由很多控件组成,这样...
继续阅读 »

这一篇文章我们继续分析另外一个重要的类RenderNode, 这个在前面绘制流程里有也有提到,这里我将更加深入的介绍这个类

1 简介

RenderNode是一个绘制节点,一个大的界面是由很多小的绘制单元组成,这个正如View的层级结构,整个界面由很多控件组成,这样带来的好处就是需要整体绘制界面的时候,只有那些变化的单元重新绘制,然后在重新组装界面即可。这让我联想到了活字印刷术,当我们要印刷一页内容的时候,如果将所有的字都刻在一块板上,当要修改的时候,就需要整体重新来刻,效率很低成本很高,但是如果是将每一个字作为一个组件,页面只是这些字拼接出来的,修改或者重用的话就相对容易很多,RenderNode就相当于是一个个的字。

尽管在应用层我们很少使用这个类,但是实际上的每个View都持有 一个RenderNode,我们可以这样去理解,View作为一个组件,会由很多业务,比如事件,布局,测量和绘制等,而绘制业务正是委托给RenderNode去完成,绘制需要Canvas也是由这个RenderNode提供的。RenderNode除了为View提供绘制能力外,还为其他可绘制的API提供绘制能力,最常见的就是Drawable,我们也可以封装自己的绘制组件,基于RenderNode的绘制是利用了硬件加速的绘制。

在应用层,View会形成树型的层级结构,因此RenderNode也会相应的构造一个出绘制节点的树形结构。但是RenderNode的树形结构和View的树形结构可能是不一样的,因为一个View可能会对应着几个RenderNode,比如View的背景也会转换成一个RenderNode,因此一个View节点可能会产生多个RenderNode对象,通常一个View的背景和View的的Children是平级的。

2 属性

2.1 Java层

RenderNode 的功能主要是在C层实现的。在java层,它持有一个mCurrentRecordingCanvas,表示当前正在使用的那个Canvas
frameworks/base/graphics/java/android/graphics/RenderNode.java

private RecordingCanvas mCurrentRecordingCanvas;

这个Canvas的类型是RecordingCanvas, 它由RenderNode的beginRecording方法创建的

public @NonNull RecordingCanvas beginRecording(int width, int height) {
if (mCurrentRecordingCanvas != null) {
throw new IllegalStateException(
"Recording currently in progress - missing #endRecording() call?");
}
mCurrentRecordingCanvas = RecordingCanvas.obtain(this, width, height);
return mCurrentRecordingCanvas;
}

这里可以看到beginRecording方法不能连续调用,需要在调用endRecording之后才能再次调用。这个canvas是通过RecordingCanvas获得的一个canvas,obtain方法往往代表是从缓存池中获取的,这里我们不深入介绍,我们知道这个Canvas 是再从这里获得的,它的类型是RecordingCanvas. 它是Canvas的子类。

RenderNode 也由很多其他的属性,但是在C层定义的,所以我们继续分析一下在C层的RenderNode

2.2 C层

在JNI 和C层这里,主要有这个几个文件
frameworks/base/libs/hwui/jni/android_graphics_RenderNode.cpp

frameworks/base/libs/hwui/RenderNode.h
frameworks/base/libs/hwui/RenderNode.cpp

以及专门用于存储属性的RenderProperties类

frameworks/base/libs/hwui/RenderProperties.h
frameworks/base/libs/hwui/RenderProperties.cpp

2.2.1 mStagingProperties

mStagingProperties记录的是修改过的属性,在没有提交前,所有的修改都临时存在mStagingProperties。

RenderProperties mStagingProperties;

对属性的修改,是通过一个宏定义来实现的,来分析一个简单属性的top的修改流程

frameworks/base/graphics/java/android/graphics/RenderNode.java

public boolean setTop(int top) {
return nSetTop(mNativeRenderNode, top);
}

frameworks/base/libs/hwui/jni/android_graphics_RenderNode.cpp

static jboolean android_view_RenderNode_setTop(CRITICAL_JNI_PARAMS_COMMA jlong renderNodePtr, int top) {
return SET_AND_DIRTY(setTop, top, RenderNode::Y);
}


通过SET_AND_DIRTY这个宏定义来调用mutateStagingProperties上的方法

#define SET_AND_DIRTY(prop, val, dirtyFlag) \
(reinterpret_cast<RenderNode*>(renderNodePtr)->mutateStagingProperties().prop(val) \
? (reinterpret_cast<RenderNode*>(renderNodePtr)->setPropertyFieldsDirty(dirtyFlag), true) \
: false)

扩展开就相当于是

reinterpret_cast<RenderNode*>(renderNodePtr)->mutateStagingProperties().setTop(top) 
? (reinterpret_cast<RenderNode*>(renderNodePtr)->setPropertyFieldsDirty(dirtyFlag), true)
: false

renderNode->mutateStagingProperties() 返回的就是 mStagingProperties
frameworks/base/libs/hwui/RenderNode.h

RenderProperties& mutateStagingProperties() { return mStagingProperties; }

因此,会执行RenderProperties的setTop方法。如果setTop返回true,则会调用setPropertyFieldsDirty,记录发生变化的属性,这里传入的是RenderNode::Y这个枚举值,定义如下:

 enum DirtyPropertyMask {
GENERIC = 1 << 1,
TRANSLATION_X = 1 << 2,
TRANSLATION_Y = 1 << 3,
TRANSLATION_Z = 1 << 4,
SCALE_X = 1 << 5,
SCALE_Y = 1 << 6,
ROTATION = 1 << 7,
ROTATION_X = 1 << 8,
ROTATION_Y = 1 << 9,
X = 1 << 10,
Y = 1 << 11,
Z = 1 << 12,
ALPHA = 1 << 13,
DISPLAY_LIST = 1 << 14,
};

frameworks/base/libs/hwui/RenderProperties.h

bool setTop(int top) {
if (RP_SET(mPrimitiveFields.mTop, top)) {
mPrimitiveFields.mHeight = mPrimitiveFields.mBottom - mPrimitiveFields.mTop;
if (!mPrimitiveFields.mPivotExplicitlySet) {
mPrimitiveFields.mMatrixOrPivotDirty = true;
}
return true;
}
return false;
}

RP_SET是一个宏定义

#define RP_SET(a, b, ...) ((a) != (b) ? ((a) = (b), ##__VA_ARGS__, true) : false)

也就是如果mPrimitiveFields.mTop与top不相同,则将top赋值给mPrimitiveFields.mTop, 并且返回true,否则直接返回false。
如果top变化了,同步修改高度。
这就是一个简单属性的修改流程。 那么RenderNode有那些属性呢?来看一看RenderProperties的定义

 struct PrimitiveFields {
int mLeft = 0, mTop = 0, mRight = 0, mBottom = 0;
int mWidth = 0, mHeight = 0;
int mClippingFlags = CLIP_TO_BOUNDS;
SkColor mSpotShadowColor = SK_ColorBLACK;
SkColor mAmbientShadowColor = SK_ColorBLACK;
float mAlpha = 1;
float mTranslationX = 0, mTranslationY = 0, mTranslationZ = 0;
float mElevation = 0;
float mRotation = 0, mRotationX = 0, mRotationY = 0;
float mScaleX = 1, mScaleY = 1;
float mPivotX = 0, mPivotY = 0;
bool mHasOverlappingRendering = false;
bool mPivotExplicitlySet = false;
bool mMatrixOrPivotDirty = false;
bool mProjectBackwards = false;
bool mProjectionReceiver = false;
bool mAllowForceDark = true;
bool mClipMayBeComplex = false;
Rect mClipBounds;
Outline mOutline;
RevealClip mRevealClip;
} mPrimitiveFields;

我们可以看到这里的属性和我们在JAVA层View的几何属性是非常相似的,基本上View的几何属性都会类似setTop的方式反映到RenderProperties。大部分的简单属性比如top,bottom,translate,rotate,elevation,scale,pivot这些就不介绍了,我们分析一下两个比较特殊的属性mProjectBackwards 和 mProjectionReceiver。这两个属性会更改RenderNode绘制顺序。设置成mProjectionReceiver的RenderNode会成为一个锚点,被标记成mProjectBackwards的RenderNode不会被绘制在它的父节点,而是绘制到它最近的父节点中的标记成mProjectionReceiver的子节点中。例如P节点包含一个子节点C,以及P的背景PB,C包含一个背景CB. 一般的顺序应该是CB绘制到C中,然后C和PB绘制到P中。 但是如果PB被设置成mProjectionReceiver ,且CB被标记成mProjectBackwards,绘制的顺序将变成,C绘制到P中,CB绘制到PB 中,然后PB绘制到P中。也就是说将CB投影到PB中去。这种做法将使得CB的变化不会导致C重新绘制,从而提升效率,比如作为背景动画的RenderNode,它不会导致View自身的RenderNode的重新绘制。

2.2.1 mProperties

mStagingProperties暂存的修改将会与mProperties同步,从而正式成为影响绘制的参数。同步的方法很简单,直接赋值,在绘制帧之前会完成这些参数的同步。

void RenderNode::syncProperties() {
mProperties = mStagingProperties;
}

个人感觉好像第一同步之后,mProperties就和mStagingProperties指向同一个对象,只有似乎以后没有同步的必要了。

3 总结

RenderNode主要保存了一系列的属性,大部分的View属性都会反映到RenderNode,RenderNode使用RenderProperties来保存这些属性,在绘制帧的时候,这些属性会影响最终的绘制。RenderNode也会形成一颗树形的层级结构,但是它与View的层级结构并不是一一对应的,在同一级中的RenderNode不仅包含View的兄弟节点的RenderNode,也包含父View的背景等可绘制内容。除了属性之外,RenderNode的另外一个重要属性是DisplayList,它存放的是这个RenderNode的绘制指令,这个将在下一篇中继续分析。

以上内容是对RenderNode的分析,基于个人的理解,如有疏漏和错误, 👀关注公众号:Android老皮!!!欢迎大家来找我探讨交流👀


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

前端跨域的几种方式

前端跨域的几种方式一、 什么是跨域跨域(Cross-Origin)是指在浏览器你执行脚本时,通过XMLHttpRequest、Fetch等方式请求不同源(协议、域名、端口)的资源。同源策略是浏览器的一种安全机制,它限制了网页中的脚本只能与同源(相同协议、域名、...
继续阅读 »

前端跨域的几种方式

一、 什么是跨域

跨域(Cross-Origin)是指在浏览器你执行脚本时,通过XMLHttpRequest、Fetch等方式请求不同源(协议、域名、端口)的资源。同源策略是浏览器的一种安全机制,它限制了网页中的脚本只能与同源(相同协议、域名、端口)的资源进行交互,防止恶意网站获取用户的敏感信息或进行攻击。

在同源策略下。浏览器允许脚本访问同源的资源,但不允许访问不同域的资源。跨域请求会触发浏览器的安全机制,导致请求被拒绝。例如,如果网页在域名A下加载了一个脚本,而在这个脚本尝试访问域名B下的资源,浏览器阻止这个跨域请求。

对于前端开发来说,跨域请求是一个常见的问题,因为现代应用通常需要不同的服务器或域名上获取数据。为了实现跨域访问,开发者可以采用常用的一些常见的方式,如 JSONP、CORS、代理服务器或 WebSocket等。这些允许前端页面与其他源的服务器进行安全的通信。

二、 前端跨域的几种方式

1、JSONP

JSONP(JSON with Padding)是一种利用<script>标签跨域获取数据的方法,可以绕过浏览器的同源策略限制。

JSONP的原理如下:

  • 通过请求参数作为回调函数的参数传递给服务器,服务器在响应中返回这个回调函数的调用,前端页面通过动态插入<script>标签来加载数据。
  • 由于<script>标签不受同源策略的限制,因此可以跨域加载并执行返回的脚本。

以下是JSONP的使用示例:

<script>
function callback(data) {
// 处理数据
}
</script>

<script src="http://example.com/api?callback=callback"></script>

上面的示例中,我们定义了一个名为callback的函数,在之后的脚本中使用这个函数来处理返回的数据。通过将callback函数的名称作为请求参数传递给服务器(例如: example.com/api?callbac… ),服务器在返回的响应中将调用该函数并传递数据。前端页面通过动态插入<script>标签来加载这个跨域的脚本,并在脚本执行时用callback函数来处理数据。

JSONP的应用场景是在需要获取跨域数据时,由于同源策略的限制我们无法直接使用XMLHttpRequest 或 Fetch方法时。比如说:我们需要从另一个域名的API获取数据,而该API支持JSONP,我们可以使用JSONP来实现跨域获取数据并在前端页面中进行处理。

JSONP需要服务器直接返回可执行的脚本代码。此外,JSONP只支持GET请求,不支持POST请求等其他类型的脚本。

2、CORS

CORS(跨域资源共享)是一种通过在服务器配置响应头来实现跨域请求的机制。它允许在浏览器中进行安全的跨域通信,突破同源策略的限制。

CORS的原理如下:

  • 前端页面发送跨域请求给服务器。
  • 服务器在响应头中添加Access-Control-Allow-Origin字段,指定允许跨域的源。例如,可以将其设置为Access-Control-Allow-Origin: http://example.com
  • 浏览器收到带有这个响应头的请求后,会判断该请求是否在允许的跨域列表中,如果是则将响应返回给前端页面,否则会被浏览器拦截。
  • 前端页面收到响应后,跨域像处理同源请求一样处理响应数据。

以下是CORS的使用示例:

//服务器端响应头配置
Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type


//前端页面请求
fetch('http://example.com/api',{
method: 'GET',
mode: 'cors'
})
.then(response => response.json())
.then(data => {
//处理数据
});

在上面的示例中,服务器在响应头中添加了Access-Control-Allow-Origin字段,指定允许跨域请求源为http://example.com。前端页面在发送具有mode: 'cors'的跨域请求时,浏览器会允许请求通过,并将响应返回给前端页面,使得前端跨域像处理同源请求一样处理跨域请求的响应数据。

CORS的运用非常广泛,特别是在现代的Web应用中。通过使用CORS,前端跨域与其他域的服务器进行安全的跨域通信,实现数据的获取与交互。开发者跨域在服务器端配置CORS响应头,来实现不同应用之间的跨域请求,提供更好的用户体验以及功能拓展。

3、前端代理服务器

前端代理服务器作为中间层。通过其中转,跨域绕过浏览器的同源限制,实现跨域请求。这种方法的优点是简单、灵活、适用于各种场景。

前端代理服务器的原理如下:

  • 前端代理服务器位于浏览器和后端服务器之间,充当转发请求和响应的角色。
  • 当浏览器发起跨域请求时,请求会先发送到前端代理服务器。
  • 前端代理服务器收到请求后,根据根据配置的规则判断是否属于跨域请求。
  • 如果属于跨域请求,前端代理服务器将发送新的请求到后端服务器,获取数据。
  • 前端代理服务器收到后端服务器的响应后,将响应内容返回给浏览器。

以下是如何使用Node.js创建一个前端代理服务器:

const http = require('http');
const request = require('request');

const proxy = http.createServer((req,res) => {
//处理跨域请求
res.setHeader('Access-Control-Allow-Origin','*');
res.setHeader('Access-Control-Allow-Methods','GET,POST,PUT,DELETE');

//转发请求到后端服务器
const url = 'http://example.com' + req.url;
req.pipe(request(url)).pipe(res);
});

const port = 8080;
proxy.listen(port, () => {
console.log('Proxy server is running on port ${port}');
});

使用前端代理服务器的好处是可以方便地在开发环境中进行前后端分离,同时避免一些跨域请求带来地麻烦。但在生产环境中,建议采用更成熟地反向代理服务器,如Nginx来处理跨域请求。

4、WebSocket

前端WebSocket实现跨域的原理是基于浏览器的同源策略的限制,通过WebSocket协议进行通信。由于WebSocket是基于TCP协议的全双工通信协议,WebSocket对象不受同源策略的约束,因此跨域实现跨域通信。

WebSocket通信的实现原理:

  • 在服务器端配置允许跨域请求:服务器端需要设置响应头,允许特定的域名访问该服务器资源。可以通过Access-Control-Allow-Origin进行跨域资源共享。
  • 在前端使用WebSocket对象与服务器建立连接:在前端代码中,可以使用WebSocket对象建立与目标服务器的连接。使用WebSocket构造函数提供服务器的URL,例如:let socket = new WebSocket('ws://example.com/socket').
  • 进行WebSocket通信:一旦与服务器建立了WebSocket连接,前端就可以通过WebSocket对象发送和接收数据。可以使用WebSocket对象的send()方法发送数据,使用onmessage时间监听接收到的信息,使用onopen事件监听连接建立成功,使用onclose事件建立连接关闭。

WebSocketHTML5的新技术,不是所有的浏览器都支持。在使用WebSocket实现跨域通信时,需要检查浏览器的兼容性并提供备选方案,确保在不支持WebSocket的情况下仍能正常工作。


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

前端开发如何给自己定位?初级?中级?高级!

引言在快速发展的互联网时代,前端开发一直处于高速增长的趋势中。作为构建用户界面和实现交互功能的关键角色,前端开发人员需要不断提升自己的技能和能力,以适应变化的行业需求。本文将为前端开发人员提供一个能力定位指南,帮助他们了解自己在前端领域的定位,内容参考阿里前端...
继续阅读 »

引言

在快速发展的互联网时代,前端开发一直处于高速增长的趋势中。作为构建用户界面和实现交互功能的关键角色,前端开发人员需要不断提升自己的技能和能力,以适应变化的行业需求。本文将为前端开发人员提供一个能力定位指南,帮助他们了解自己在前端领域的定位,内容参考阿里前端面试指南,P6/P6+/P7的能力标准。

目录

0.掌握图形学,webgl或熟练使用threejs框架,熟练canvas相关渲染及动画操作的优先。
1.熟练掌握JavaScript。
2.熟悉常用工程化工具,掌握模块化思想和技术实现方案。
3.熟练掌握React前端框架,了解技术底层。同时了解vue以及angular等其他框架者优先。
4.熟练掌握react生态常用工具,redux/react-router等。
5.熟悉各种Web前端技术,包括HTML/XML/CSS等,有基于Ajax的前端应用开发经验。
6.有良好的编码习惯,对前端技术有持续的热情,个性乐观开朗,逻辑性强,善于和各种背景的人合作。
7.具有TS/移动设备上前端开发/NodeJS/服务端开发等经验者优先。

0.掌握图形学,webgl或熟练使用threejs框架,熟练canvas相关渲染及动画操作的优先。

初级:

  • 学习过图形学相关知识,知道矩阵等数学原理在动画中的作用,知道三维场景需要的最基础的构成,能用threejs搭3d场景,知道webgl和threejs的关系。
  • 知道canvas是干嘛的,聊到旋转能说出canvas的api。
  • 知道css动画,css动画属性知道关键字和用法(换句话说,电话面试会当场出题要求口喷css动画,至少能说对大概,而不是回答百度一下就会用)。
  • 知道js动画,能说出1~2个社区js动画库,知道js动画和css动画优缺点以及适用场景。
  • 知道raf和其他达到60fps的方法。

中级:

  • 如果没有threejs,你也能基于webgl自己封装一个简单的threejs出来。
  • 聊到原理能说出四元数,聊到鼠标操作能提到节流,聊到性能能提到restore,聊到帧说出raf和timeout的区别,以及各自在优化时候的作用。
  • 知道怎样在移动端处理加载问题,渲染性能问题。
  • 知道如何结合native能力优化性能。
  • 知道如何排查性能问题。对chrome动画、3d、传感器调试十分了解。

高级:

  • 搭建过整套资源加载优化方案,能说明白整体方案的各个细节,包括前端、客户端、服务端分别需要实现哪些功能点、依赖哪些基础能力,以及如何配合。
  • 设计并实现过前端动画引擎,能说明白一个复杂互动项目的技术架构,知道需要哪些核心模块,以及这些模块间如何配合。
  • 有自己实现的动画相关技术方案产出,这套技术方案必须是解决明确的业务或技术难点问题的。为了业务快速落地而封装一个库,不算这里的技术方案。如果有类似社区方案,必须能从原理上说明白和竞品的差异,各自优劣,以及技术选型的原因。

1.熟练掌握JavaScript。

初级:

  • JavaScript各种概念都得了解,《JavaScript语言精粹》这本书的目录都得有概念,并且这些核心点都能脱口而出是什么。这里列举一些做参考:
  • 知道组合寄生继承,知道class继承。
  • 知道怎么创建类function + class。
  • 知道闭包在实际场景中怎么用,常见的坑。
  • 知道模块是什么,怎么用。
  • 知道event loop是什么,能举例说明event loop怎么影响平时的编码。
  • 掌握基础数据结构,比如堆、栈、树,并了解这些数据结构计算机基础中的作用。
  • 知道ES6数组相关方法,比如forEach,map,reduce。

中级:

  • 知道class继承与组合寄生继承的差别,并能举例说明。
  • 知道event loop原理,知道宏微任务,并且能从个人理解层面说出为什么要区分。知道node和浏览器在实现loop时候的差别。
  • 能将继承、作用域、闭包、模块这些概念融汇贯通,并且结合实际例子说明这几个概念怎样结合在一起。
  • 能脱口而出2种以上设计模式的核心思想,并结合js语言特性举例或口喷基础实现。
  • 掌握一些基础算法核心思想或简单算法问题,比如排序,大数相加。

2.熟悉常用工程化工具,掌握模块化思想和技术实现方案。

初级:

  • 知道webpack,rollup以及他们适用的场景。
  • 知道webpack v4和v3的区别。
  • 脱口而出webpack基础配置。
  • 知道webpack打包结果的代码结构和执行流程,知道index.js,runtime.js是干嘛的。
  • 知道amd,cmd,commonjs,es module分别是什么。
  • 知道所有模块化标准定义一个模块怎么写。给出2个文件,能口喷一段代码完成模块打包和执行的核心逻辑。

中级:

  • 知道webpack打包链路,知道plugin生命周期,知道怎么写一个plugin和loader。
  • 知道常见loader做了什么事情,能几句话说明白,比如babel-loader,vue-loader。
  • 能结合性能优化聊webpack配置怎么做,能清楚说明白核心要点有哪些,并说明解决什么问题,需要哪些外部依赖,比如cdn,接入层等。
  • 了解异步模块加载的实现原理,能口喷代码实现核心逻辑。

高级:

  • 能设计出或具体说明白团队研发基础设施。具体包括但不限于:
  • 项目脚手架搭建,及如何以工具形态共享。
  • 团队eslint规范如何设计,及如何统一更新。
  • 工具化打包发布流程,包括本地调试、云构建、线上发布体系、一键部署能力。同时,方案不仅限于前端工程部分,包含相关服务端基础设施,比如cdn服务搭建,接入层缓存方案设计,域名管控等。
  • 客户端缓存及预加载方案。

3.熟练掌握React前端框架,了解技术底层。同时了解vue以及angular等其他框架者优先。

初级:

  • 知道react常见优化方案,脱口而出常用生命周期,知道他们是干什么的。
  • 知道react大致实现思路,能对比react和js控制原生dom的差异,能口喷一个简化版的react。
  • 知道diff算法大致实现思路。
  • 对state和props有自己的使用心得,结合受控组件、hoc等特性描述,需要说明各种方案的适用场景。
  • 以上几点react替换为vue或angular同样适用。

中级:

  • 能说明白为什么要实现fiber,以及可能带来的坑。
  • 能说明白为什么要实现hook。
  • 能说明白为什么要用immutable,以及用或者不用的考虑。
  • 知道react不常用的特性,比如context,portal。
  • 能用自己的理解说明白react like框架的本质,能说明白如何让这些框架共存。

高级:

  • 能设计出框架无关的技术架构。包括但不限于:
  • 说明如何解决可能存在的冲突问题,需要结合实际案例。
  • 能说明架构分层逻辑、各层的核心模块,以及核心模块要解决的问题。能结合实际场景例举一些坑或者优雅的处理方案则更佳。

4.熟练掌握react生态常用工具,redux/react-router等。

初级:

  • 知道react-router,redux,redux-thunk,react-redux,immutable,antd或同级别社区组件库。
  • 知道vue和angular对应全家桶分别有哪些。
  • 知道浏览器react相关插件有什么,怎么用。
  • 知道react-router v3/v4的差异。
  • 知道antd组件化设计思路。
  • 知道thunk干嘛用的,怎么实现的。

中级:

  • 看过全家桶源码,不要求每行都看,但是知道核心实现原理和底层依赖。能口喷几行关键代码把对应类库实现即达标。
  • 能从数据驱动角度透彻的说明白redux,能够口喷原生js和redux结合要怎么做。
  • 能结合redux,vuex,mobx等数据流谈谈自己对vue和react的异同。

高级:

  • 有基于全家桶构建复杂应用的经验,比如最近很火的微前端和这些类库结合的时候要注意什么,会有什么坑,怎么解决

5.熟悉各种Web前端技术,包括HTML/XML/CSS等,有基于Ajax的前端应用开发经验。

初级:

  • HTML方面包括但不限于:语义化标签,history api,storage,ajax2.0等。
  • CSS方面包括但不限于:文档流,重绘重排,flex,BFC,IFC,before/after,动画,keyframe,画三角,优先级矩阵等。
  • 知道axios或同级别网络请求库,知道axios的核心功能。
  • 能口喷xhr用法,知道网络请求相关技术和技术底层,包括但不限于:content-type,不同type的作用;restful设计理念;cors处理方案,以及浏览器和服务端执行流程;口喷文件上传实现;
  • 知道如何完成登陆模块,包括但不限于:登陆表单如何实现;cookie登录态维护方案;token base登录态方案;session概念;

中级:

  • HTML方面能够结合各个浏览器api描述常用类库的实现。
  • css方面能够结合各个概念,说明白网上那些hack方案或优化方案的原理。
  • 能说明白接口请求的前后端整体架构和流程,包括:业务代码,浏览器原理,http协议,服务端接入层,rpc服务调用,负载均衡。
  • 知道websocket用法,包括但不限于:鉴权,房间分配,心跳机制,重连方案等。
  • 知道pc端与移动端登录态维护方案,知道token base登录态实现细节,知道服务端session控制实现,关键字:refresh token。
  • 知道oauth2.0轻量与完整实现原理。
  • 知道移动端api请求与socket如何通过native发送,知道如何与native进行数据交互,知道ios与安卓jsbridge实现原理。

高级:

  • 知道移动端webview和基础能力,包括但不限于:iOS端uiwebview与wkwebview差异;webview资源加载优化方案;webview池管理方案;native路由等。
  • 登陆抽象层,能够给出完整的前后端对用户体系的整体技术架构设计,满足多业务形态用户体系统一。考虑跨域名、多组织架构、跨端、用户态开放等场景。
  • mock方案,能够设计出满足各种场景需要的mock数据方案,同时能说出对前后端分离的理解。考虑mock方案的通用性、场景覆盖度,以及代码或工程侵入程度。
  • 埋点方案,能够说明白前端埋点方案技术底层实现,以及技术选型原理。能够设计出基于埋点的数据采集和分析方案,关键字包括:分桶策略,采样率,时序性,数据仓库,数据清洗等。

6.有良好的编码习惯,对前端技术有持续的热情,个性乐观开朗,逻辑性强,善于和各种背景的人合作。

初级:

  • 知道eslint,以及如何与工程配合使用。
  • 了解近3年前端较重要的更新事件。
  • 面试过程中遇到答不出来的问题,能从逻辑分析上给出大致的思考路径。
  • 知道几个热门的国内外前端技术网站,同时能例举几个面试过程中的核心点是从哪里看到的。

高级:

  • 在团队内推行eslint,并给出工程化解决方案。
  • 面试过程思路清晰,面试官给出关键字,能够快速反应出相关的技术要点,但是也要避免滔滔不绝,说一堆无关紧要的东西。举例来说,当时勾股老师面试我的时候,问了我一个左图右文的布局做法,我的回答是:我自己总结过7种方案,其中比较好用的是基于BFC的,float的以及flex的三种。之后把关键css口喷了一下,然后css就面完了。

7.具有TS/移动设备上前端开发/NodeJS/服务端开发等经验者优先。

  • 根据了解的深度分初/中/高级。
  • 知道TS是什么,为什么要用TS,有TS工程化实践经验。
  • 知道移动端前端常见问题,包括但不限于:rem + 1px方案;预加载;jsbridge原理等。
  • 能说出大概的服务端技术,包括但不限于:docker;k8s;rpc原理;中后台架构分层;缓存处理;分布式;响应式编程等。

5. 结论与进一步学习

本文为前端开发人员提供了一个能力定位指南,帮助他们了解自己在前端领域的定位,并提供了具体的代码示例来巩固学习成果。通过不断学习和实践,前端开发人员可以逐步提升自己的能力,从初级到中级再到高级。但请注意,在实际工作中,不同公司和项目对于各个级别的要求可能会有所不同。

为了进一步提高自己的水平,前端开发人员可以考虑以下学习路径和资源:

  • 阅读官方文档和教程,如MDN、React官方文档等;
  • 参与开源项目,并与其他开发人员进行交流和合作;
  • 关注前端开发的博客和社区,如Medium、Stack Overflow等;
  • 参加在线或线下的前端开发培训课程;
  • 阅读经典的前端开发书籍,如《JavaScript高级程序设计》、《CSS权威指南》等。

通过持续学习和实践,相信每个前端开发人员都可以不断成长,并在前端领域中取得更好的成就。祝愿大家在前端开发的道路上越走越远!


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

程序员接外包的三个原则以及有意思的讨论

文章来源网络 原则一:乙方来做决策 最终拍板人是谁?是甲方,如果你非要抢板子,那你以后就没有甲方了 但是,如果甲方也觉得“我花钱了,当然要听我的(那些只对上级负责又不能拍板的底层打工人,总是这样认为)”,那这种甲方的项目你就不要接 因为在这种甲方眼里,你只是...
继续阅读 »

文章来源网络


原则一:乙方来做决策



  • 最终拍板人是谁?是甲方,如果你非要抢板子,那你以后就没有甲方了

  • 但是,如果甲方也觉得“我花钱了,当然要听我的(那些只对上级负责又不能拍板的底层打工人,总是这样认为)”,那这种甲方的项目你就不要接

  • 因为在这种甲方眼里,你只是“施工方”,他们即不需要你的经验价值,更不会为你的经验买单。所以这种甲方你会做得很累,当他们觉得“你的工作强度不足以匹配付给你的费用时(他们总这样觉得)”,他们就会不停地向你提出新的开发需求

  • 所以,你要尽量找那种尊重你经验价值,总是向你请教,请你帮他做决策的甲方


原则二:为甲方护航



  • 甲方未来会遇到什么问题,你们双方其实都不知道,所以你需要一边开发一边解决甲方实际遇到的问题。

  • 因此,不要为了完成合同上的工作内容而工作,要为甲方遇到的实际问题,为了甲方的利益而开发,要提前做好变通的准备。

  • 永远不要觉得你把合同上的功能列表做完,你就能休息了。你要解决的真正问题是,为甲方护航,直至甲方可以自己航行。


原则三:不做没有用户的项目



  • 如果甲方的项目没有太多用户使用,这种项目就不要接。

  • 除了代码的累计经验,还有一种经验也很重要,那就是“了解用户的市场经验”

  • 只有真正面对有实际用户的项目,你才能有“解决市场提出的问题”的经验,而不是停留在“解决甲方提出的问题”

  • 拥有市场经验,你就会有更高的附加价值,再配上尊重你经验价值的甲方,你就会有更高的收入

  • 永远记住:真正愿意在你身上花钱的甲方,他的目的一定是为了让你帮他赚钱!


以上只是我根据自己经验的一家之言,可能对我有用,不一定对别人也有用。肯定还有很多有价值的原则,希望大家根据自己的经验一起来分享。


下面是一些有意思的讨论


原则 2 、3 都是虚的,就不讨论了。

只说原则一:


一般而言,甲方跟你对接的,一定不是老板。

所以他的核心目的一定是项目实施成功。

但项目是不是真的能给企业带来效益,其实是优先级特别低的一个选项。


拿日常生活举个例子。夫妻、情侣之间,你媳妇儿托你办个事,比如让你买个西瓜。

你明知道冬天的西瓜又贵又不好吃,你会怎么办?


A ,买西瓜,回去一边吃西瓜一起骂水果摊老板没良心。

B ,给你媳妇儿上农业课。然后媳妇儿让你跪搓衣板。

C ,水果摊老板训你一顿,以冬天吃白菜豆腐好为由,非卖你一颗大白菜。你家都不敢回。


这里面,你就是那个甲方对接人。你怎么选?


所以乙方一定不能做决策。乙方做决策的结果,就是甲方对接人被利益集团踹开或者得罪甲方对接人,最终导致项目失败




我也来说三个原则

1.要签合同,合同越细越好;

2.要给订金,订金越多越好;

3.尾款不结不给全部源码。




原则一:外包大部分就是苦力活,核心有价值的部分有自己公司的人干轮不到外包,你不干有的是人干,不会有人尊重你经验价值,甲方说怎么干就怎么干,写到合同里,按合同来,没甲方懂自家业务,别替甲方做决策,万一瞎建议导致项目出现大问题,黄了,外包钱都拿不回来


原则二:给多少钱办多少事,如果甲方给钱痛快,事少,可以看自己良心对甲方多上点心,否则别给自己加戏,不然很可能把自己感动了,甲方却想着好不容易碰上这么个人,白嫖


原则三:不做没有用户的项目,太片面,不是所有外包项目都是面对海量用户,但是做所有外包项目都是为了赚钱,假如有个富二代两三万找你做个毕设,简单钱多不用后续维护,这种接不接?假如某工厂几十万定制内部系统,可能只有几个人用,这种接不接


总之外包就是赚个辛苦钱,别指望这个来提升自己技术和自我价值,外包行业水太深,你这几个原则都太理想化




某富豪要盖一栋私人别墅,招建筑工人,现在缺一名搅拌水泥的工人,

找到了张三,张三说我去过很多工地,啥活儿都干过,经验极其丰富,我可以指导一切事物,我再给你兼职当个总设计师吧,一切事物听我的决策没错。

我每天做完我的本职工作搅拌水泥砂浆,我还能熬夜给建筑设计布局,风水,房间规划,材料采购等等,我啥都会,直接干到建筑完工

富豪很感兴趣,说那你来吧,我盖的是自己住的别墅,张三一听连连摆手:你是盖私人别墅啊?不行不行,我不去了,我以前盖的都是高楼大厦,住户多对我技术水平有严峻的考验,做成了对我有很大提高,私人别墅才几个人用,对我职业生涯一点帮助都没




永远不要接外包

这才是正确的答案

做私活的时间

不如自己休息休息,陪陪家人




屁事真多,有钱就行了,管他项目有没有人,人家产品低能你还得兜底,接外包考虑的是能不能满足需求。啥条件啊还能挑三拣四,给多少钱干多少活。 招投标接的 30 万以上的项目才有可能考虑你说的这些东西。





呵呵 我的意见是:



  1. 给钱就做(前提是合规,不是合理),先给定金,拿到定金开工。

  2. 遇到扯皮,就停止开发。

  3. 要有空闲的时间,偶尔做做(上面说的对:永远不要做外包)。


展开来说,做外包的长期收益很低。就当临时玩一下,所以给钱就做,不管你的需求合理不合理,比如甲方想给智障人士开发一款数独小游戏,好,给钱,签合同,支付定金,开工。


开工了3天,甲方突然说,那个我想加个魔方游戏。不好意思,不行,立即停止开发,开始和甲方掰扯,如果掰扯不明白,就终止合同,如果掰扯明白就继续。


不说了,我要和甲甲甲甲甲方掰扯去了。

作者:Data_Adventure
来源:juejin.cn/post/7256590619412676663

收起阅读 »

这一篇浏览器事件循环,可能会颠覆部分人的对宏任务和微任务的理解🤪🤪🤪

web
在这两天里看到一篇文章,发现好像很多人都把事件循环给搞混了,到底是宏任务先执行还是微任务先执行。在写这篇文章之前,我也随机挑选了几位幸运观众来问这个问题,好像大多都是说微任务先执行。 那么从这篇文章里,我们就来探讨一下到底是哪个先执行。 什么是进程 进程是计算...
继续阅读 »

在这两天里看到一篇文章,发现好像很多人都把事件循环给搞混了,到底是宏任务先执行还是微任务先执行。在写这篇文章之前,我也随机挑选了几位幸运观众来问这个问题,好像大多都是说微任务先执行。


那么从这篇文章里,我们就来探讨一下到底是哪个先执行。


什么是进程


进程是计算机系统中正在运行的程序的实例。它是操作系统对一个正在运行的程序的抽象表示,负责管理程序的执行和资源分配。


通常它包括堆栈,例如临时数据,如函数参数、返回地址和局部变量和数据段,其中数据段包括全局变量。


进程还可能包括堆,这是在进程运行时动态分配的内存。在 JavaScript 中,堆和栈的内存分配是通过不同方式进行的:




  1. 堆内存分配:



    • JavaScript 中的对象、数组和函数等复杂数据类型都存储在堆内存中;

    • 使用 new 关键字或对象字面量语法创建对象时,会在堆内存中动态分配相应的内存空间;

    • 堆内存的释放由垃圾回收机制自动处理,当一个对象不再被引用时,垃圾回收机制会自动回收其占用的堆内存,释放资源;




  2. 栈内存分配:



    • JavaScript 中的基本数据类型,如数字、布尔值和字符串以及函数的局部变量保存在栈内存中;

    • 栈内存的分配是静态的,编译器在编译阶段就确定了变量的内存空间大小;

    • 当函数被调用时,会在栈内存中创建一个称为栈帧 stack frame 的数据结构,用于存储函数的参数、局部变量、返回地址等信息;

    • 当函数执行完毕或从函数中返回时,对应的栈帧会被销毁,栈内存中的数据也随之释放;




在操作系统中,每个进程都有自己的地址空间、状态和控制信息。进程可以独立运行,与其他进程离开来,互不干扰。它们可以同时进行,并通过进程间通信机制进行交互。


进程在执行时会改变状态。进程状态,部分取决于进程的当前活动。每个进程可能处于以下状态:



  • 新的: 进程正在创建;

  • 运行: 指令正在运行;

  • 等待: 进程等待发生某个时间,如 I/O 完成或收到信号;

  • 就绪: 进程等待分配处理器;

  • 终止: 进程已经完成执行;


下图完整地显示了一个状态图:
20230725131814


什么是线程


线程是进程中的一个执行路径,是进程的组成部分。在同一个进程中的多个线程共享进程的资源,如内存空间和文件句柄等。不同线程之间可以并发执行,各自堵路地完成特定的任务。


进程和线程之间的关系如下:



  • 一个进程可以创建多个线程,这些线程共享同一个地址空间和资源,能够并发地执行任务;

  • 线程是在进程内部创建和销毁的,它们与进程共享进程的上下文,包括打开的文件、全局变量和堆内存等;

  • 每个进程至少包含一个主线程,主线程用于执行进程的主要业务逻辑。其他线程可以作为辅助线程来完成特定的任务;


主线程是一个程序中的特殊线程,它是程序的入口点和主要执行线程。


在许多编程语言和操作系统中,主线程是程序启动后自动创建的线程,负责执行程序的主要业务逻辑。主线程会按照顺序执行代码,从程序的入口点开始,直到程序结束或主线程显式终止。


你可以理解成一个篮球场,整个篮球场就是一个进程,就是一块能供线程使用的内存。


而线程就是篮球场上的每一个队员,每个人都有不同的职责。


Chrome: 多进程架构浏览器


许多网站包含活动内容,如 JavaScriptFlushHTML5 等,以便提供丰富的、动态的 Web 浏览体验。遗憾的是,这些 Web 应用程序也可能包含软件缺陷,从而导致响应迟滞,有的甚至网络浏览器崩溃。如果一个 Web 浏览器只对一个网站进行浏览,那么这不是一个大问题。


但是,现代 Web 浏览器提供标签是浏览,它运行 Web 浏览器的一个实例,同时打开多个网站,而每个标签代表一个网站。要在不同网站之间切换,用户只需点击响应的标签。这种安排如下图所示:


20230725154126


这种方法的一个问题是: 如果任何标签的 Web 应用程序崩溃,那么整个进程,包括所有其他标签所显示的网站也会崩溃。


GoogleChrome Web 浏览器通过多进程架构的设计解决这以问题。Chrome 具有多种不同类型的进程,这里我们主要讲讲其中三个:



  • 浏览器进程: 主要负责界面显示、用户交互、子进程管理,同时提供存储等功能;

  • 网络进程: 网络进程负责处理浏览器内发起的所有网络请求,例如加载网页、资源文件,如图片、CSS JavaScriptXMLHttpRequest 和 Fetch API 等请求;

  • 渲染进程: 主要负责渲染网页的逻辑。主要处理 HTML、Javascript、图像等等。一般情况下,对应于新标签的每个网站都会创建一个新的渲染进程。因此,可能会有多个渲染进程同时活跃;


如下图所示:
20230725155622


例如我只打开了两个标签也,浏览器就会开辟两个两个不同的进程,两者之间相互独立,一个崩掉了不会另外一个。



这个目前是这样子的,但是后续可能会改,根据 chrome 文档说明,后期可能会修改成每个站点开启一个进程,例如,你访问 b 站,而再 b 站里面的所有页面都不会开启新的渲染进程了,详情请看 chrome 官方文档



渲染主线程是如何工作的


渲染主线程是浏览器中最繁忙的线程,需要它处理的任务包括但不限于:



  • 解析 HTML;

  • 解析 CSS;

  • 计算样式;

  • 布局;

  • 处理图层;

  • 每秒把⻚面画 60 次;

  • 执行全局 JavaScript 代码;

  • 执行事件处理函数;

  • 执行计时器的回调函数;


等等事情,当浏览器的网络线程收到 HTML 文档后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列,在事件循环的作用下,渲染主线程取出消息队列中的渲染任务,并开启渲染流程。整个过程分为多个阶段,分别是: 解析 HTML、样式计算、布局、分层、绘制、分块、光栅化、画,每个阶段都有明确的输入输出,上一个阶段的输出会成为下一个阶段的输入。


首先解析的是 HTML 文档,这里我们忽略 CSS 的文件,如果主线程解析到 script 位置,会停止解析 HTML,转而等待 JavaScript 文件下载好,主线程将 JavaScript 代码解析执行完成后,才能继续解析 HTML。这是因为 JavaScript 代码的执行过程中可能会修改当前的 DOM 树,所以 DOM 树的生成必须暂停。这就是 JavaScript 会阻塞 HTML 解析的根本原因。
20230726082307



前面的这句话中有两个关键字很关键字,画好重点,它们分别是 下载解析



要处理这么多的事情,浏览器给渲染进程采用了多线程,它主要包含了以下线程:



  • 主线程: 主线程负责解析 HTMLCSSJavaScript,构建 DOM 树、CSSOM 树和渲染树,并进行页面布局和绘制。它还处理用户交互,执行 JavaScript 代码以及其他页面渲染相关的任务;

  • 合成线程: 合成线程负责将渲染树转换为图层,并执行图层的合成操作;

  • 网络线程: 网络线程负责处理网络请求和数据传输。当浏览器需要加载网页、图片或其他资源时,网络线程负责发送请求并接收响应数据;

  • 定时器线程: 定时器线程负责管理定时器事件,包括 setTimeoutsetInterval 等。它用于在指定的时间间隔内触发预定的任务;

  • 事件处理线程: 当用户进行交互操作,如点击按钮、滚动页面、输入文本等,需要触发相应的事件处理函数;


由于主线程和合成线程是并行执行的,这就可能导致这两个线程之间存在数据交互的问题。例如,当主线程和合成线程都需要访问相同的共享资源时,就需要进行同步,以避免竞态条件等问题。


这里就设计到消息队列的作用: 主线程和合成线程之间通过消息队列进行通信。主线程将渲染任务和图层数据等信息封装成消息,并将消息放入消息队列中。合成线程从消息队列中获取消息,并执行相应的图层合成操作。


浏览器中出现消息队列是为了处理异步任务和事件。在浏览器当中,有许多人任务是在后台执行或者将来某个事件触发时才执行的,例如:



  • 异步操作: 比如通过 AJAX 请求从服务器获取数据、读取本地文件等,这些操作需要等待网络请求或者文件读取完成后再处理响应的数据;

  • 定时器: 通过 setTimeoutsetInterval 设置的定时器任务,需要在指定的时间间隔后执行;

  • 事件处理: 当用户进行交互操作,如点击按钮、滚动页面、输入文本等,需要触发相应的事件处理函数;


20230726091257


如上图所示,当渲染主线程正在执行一个 JavaScript 函数,执行到一半的时候用户点击了按钮或者碰到了一个定时器,也就是 setTimeout。因为在我们的渲染进程里面是有定时器线程的,定时器线程监听到有这个定时器操作。那么该线程会将 setTimeout 里面的事件处理函数 (setTimeout 的第一个回调函数) 作为一个任务拿去排队。


因为消息队列采用的是队列的数据结构,当渲染主线程将所有任务情况之后,然后从消息队列中拿去最旧的那个任务,假设消息队列之前没有任务的情况下,就拿出 setTimeout 这个事件处理函数。如果在该函数当中又遇到了类似的事件处理函数或者定时器,按照前面的步骤。依此循环,直到所有任务执行完成。


整个过程,就被称之为事件循环。


什么是异步


JavaScript 是一门单线程的编程语言.意味着在一个特定的时间点,只能有一个代码块在执行。当执行一个同步任务时,如果任务需要很长时间才能完成,如网络请求、文件读取等,整个程序会被阻塞,导致用户界面无响应,甚至造成卡顿的问题。这种情况在 Web 应用中尤其常见,因为 JavaScript 经常与网络请求、DOM 操作等耗时任务打交道。


常见的异步操作包括:



  • 网络请求: 发送 HTTP 请求并等待服务器响应时,通常使用异步方式,以允许程序继续执行其他操作;

  • 定时器: 设置定时器,在一段时间后执行某个任务,也是异步操作的一种;

  • 事件处理: 为 DOM 元素注册事件监听器是一种常见的异步任务。当特定事件触发时,相应的事件处理函数将被异步调用,例如 addEventListener


渲染主线程负责处理网页的构建、布局、绘制和用户交互等任务,而异步编程使得我们可以在主线程执行同步代码的同时,处理耗时的异步操作,例如网络请求、文件读写等,以提高程序的性能和用户体验。在 JavaScript 中,通过事件循环机制,异步编程实现了一种非阻塞的执行方式,使得浏览器能够高效地处理各种任务,同时保持用户界面的响应性。


例如你要执行一个 setTimeout:


setTimeout(() => {
console.log(111);
}, 3000);

console.log(222);

在这段代码当中,如果让渲染主线程去等待这个定时器任务执行完再去执行下一个任务,就会导致主线程长期处于阻塞的状态,从而导致浏览器页面长期见不到效果,可能要砸电脑了。


20230726095635



整个流程你可以理解为这样,但也可能不完全正确。



等到整个计时结束,再执行 console.log(222) 的代码,这种模式就叫作同步。整个时候消息队列还有很多任务在等待,可能还存在一些渲染页面的任务,有可能直接导致整个页面卡死。


所以为了这个问题,浏览器采用了异步的方式来解决这个问题,因为渲染主线程承担着及其重要的工作,无论如何都不能阻塞。


20230726100527


当计时开始之后,我就不管你了,就例如你是服务,餐厅里面来了客人,客人点完了菜,你把菜单交给后厨,你就可以先不管了,继续服务下一个客人,也就是从消息队列中拿下一个任务。当计时结束之后,会把该回调函数放入到消息队列末尾.等到剩下的任务完成之后,你就可以给客人端菜了。


任务优先级


任务没有优先级,在消息队列中先进先出,但消息队列是有优先级的。


事件循环在过去的说法中,任务分为两个队列,一个是宏任务,一个是微任务。但是现在已经没有了宏任务的说法了。



因为我在年初的时候就写过相关事件循环的文章,且有在 mdn 上搜索过宏任务的相关概念,但现在在 mdn 已经完全搜索不到了。



根据 W3C 的最新解释:



  • 每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列。在一次事件循环当中,浏览器可以根据实际情况从不同的队列中取出任务执行;

  • 浏览器必须准备好一个微任务队列,微队列中的任务优先所有其他任务执行;


相关 W3C 连接


Chrome 的实现中,至少包含了下面的队列:



  • 延时队列: 用于存放计时器到达后的回调任务,优先级 ;

  • 交互队列: 用于存放用户操作后产生的事件任务,优先级 ;

  • 微队列: 用户存放需要最快执行的任务,优先级最高;


虽然浏览器最新规范是这样,但是你用之前的宏任务和微任务去答题也完全没有问题的,但是输出的顺序是完全没有变的,况且这篇文章主要内容也不是讲这个,那么在之后的代码中我们就继续以宏任务和微任务的来讲。


宏任务和微任务 重点来啦!!!


宏任务是一组异步任务,这些任务通常由浏览器的事件触发器发起,并在主线程中按照顺序执行。常见的宏任务包括:



  • setTimeoutsetInterval;

  • I/O 操作,例如读取文件、网络请求;

  • DOM 事件,例如点击事件、输入事件;

  • requestAnimationFrame;

  • script 标签;


微任务是一个细微的异步任务,它的执行时机在宏任务之后、渲染之前。微任务通常在一个宏任务执行完毕后立即执行,而不需要等待其他宏任务。这使得微任务的执行优先级比宏任务高。常见的微任务包括:



  • Promiseresolvereject 回调;

  • async/await 中的异步函数;

  • MutationObserver;


很重要的一点来了,为什么说 script 标签是宏任务呢?


如果忘记了,你再看看我们前面中说到的 下载解析 两个关键字。


script 标签包含的 JavaScript 代码在浏览器中执行时,被认为是宏任务。这是因为执行 script 标签内的代码需要进行一系列的操作,包括解析、编译和执行。主要有以下几个理由:



  • 解析和编译: 当浏览器遇到 script 标签时,它会停止当前的文档解析过程,并开始解析 script 内的 JavaScript 代码。解析器将逐行读取代码,并将其转换为可执行的内部表示形式。这个解析和编译的过程是一个比较耗时的操作,需要占用大量的 CPU 资源;

  • 阻塞页面渲染: 由于脚本的执行通常会修改当前页面的结构和样式,浏览器必须等待脚本执行完毕后再进行页面的渲染。也就是说,当浏览器执行 script 标签时,它会阻塞页面的渲染,直到脚本执行完毕才会继续渲染;

  • 可能引起网络请求:在 script 标签中,可以使用外部的 JavaScript 文件引用,例如 <script src="example.js"></script>。当浏览器遇到这样的情况时,它会发起一个网络请求去下载该文件,并等待文件下载完成后再执行。网络请求通常是一个比较耗时的操作,因此将其作为宏任务可以确保脚本的执行按照正确的顺序进行;


总结起来,script 标签被认为是宏任务是因为它需要解析、编译和执行 JavaScript 代码,并且会阻塞页面的渲染。此外,如果使用了外部 JavaScript 文件,还可能引起网络请求,进一步增加了执行时间。这些特性使得 script 标签的执行与其他微任务(如 Promise)不同,被归类为宏任务。


如果你依然觉得理由不够充分的话,请看以下代码:


<!-- 脚本 1 -->
<script>
// 同步
console.log("start1");
// 异步宏
setTimeout(() => console.log("timer1"), 0);
new Promise((resolve, reject) => {
// 同步
console.log("p1");
resolve();
}).then(() => {
// 异步微
console.log("then1");
});
// 同步
console.log("end1");
</script>

<!-- 脚本 2 -->
<script>
// 同步
console.log("start2");
// 异步宏
setTimeout(() => console.log("timer2"), 0);
new Promise((resolve, reject) => {
// 同步
console.log("p2");
resolve();
}).then(() => {
// 异步微
console.log("then2");
});
// 同步
console.log("end2");
</script>

该代码的输出结果如下所示:
20230726105526


如果 script 标签不是宏任务,普通任务的话,是不是应该先执行 start2end2 再执行 then1


所以根据此结论,整个浏览器循环应该是 先执行 宏任务 -> 同步代码 -> 微任务,直到当前宏任务中的微任务清理完毕,继续执行下一个宏任务,以此类推。


最后我再抛出一个问题,没有你这个 script 这个宏任务的出现,你哪来的微任务?


一个事件循环过程模型如下,当调用栈为空时,执行以下步骤:



  1. 选择任务队列中最旧的任务(队列是一个先进先出的队列,最旧的那个就是最先进的,这里是 任务 A);

  2. 如果任务 A为空(意味着任务队列为空),跳转到第6步;

  3. 将当前运行的任务设置为任务 A;

  4. 运行任务 A,意味着运行回调函数;

  5. 运行结束,将当前的任务设置为空,删除任务 A;

  6. 执行微任务队列:

    1. 选择微任务队列中最早的任务 X;

    2. 如果任务 X,代表这微任务为空,跳转到步骤6;

    3. 将当前运行的任务设置为任务 X,并运行该任务;

    4. 运行结束,将当前正在运行的任务设置为空,删除任务 X;

    5. 选择微任务队列中下一个最旧的任务,可以理解为第n+1个入队的,跳转到步骤2;

    6. 完成微任务队列;



  7. 跳转到第1步;


这个事件循环过程模型如下图所示:


image.png


值得注意的是,当一个任务在宏任务队列中正在运行时,可能会注册新事件,因此可能会创建新任务,下面是两个新创建的任务:



  • Promise.then(...) 是一个回调任务:当 promisefulfilled/rejected:任务将被推入当前轮事件循环中的微任务队列;当promisepending:任务将在下一轮事件循环中被推入微任务队列(可能是下一轮);


案例


接下来我们通过一些案例来加深对事件循环的理解。


案例一


setTimeout(() => {
console.log("time1");

new Promise((resolve) => {
resolve();
}).then(() => {
new Promise((resolve) => {
resolve();
}).then(() => {
console.log("then4");
});

console.log("then2");
});
});

new Promise((resolve) => {
console.log("p1");
resolve();
}).then(() => {
console.log("then1");
});

最后的输出结果为 p1 then1 time1 then2 then4,下面就来分析一下这个结果的由来:



  1. 代码首先遇到settimeout,是一个宏任务,里面的代码不会被执行;

  2. 接着代码往下执行,遇到 new Promise(...)中的回调函数是一个同步任务,直接执行;

  3. 直接输出 "p1",调用 resolve(),Promise 的状态变为 fuifilled,当 promise 状态变为 fulfilled/rejected时,任务将被推入当前轮事件循环中的微任务队列,所以后面的 then(...) 会被加入到微任务队列里面;

  4. 主线程中的同步代码执行完,从微任务中取出最旧的那个任务,也就是 then(...),输出 then1,此时微任务队列为空;

  5. 继续执行宏任务,也就是这个 settimeout,代码从上往下执行,首先输出 time1;

  6. 在下面的代码中又遇到了 new Promise(...),并且调用了 resolve(),then(...)被加入到微任务队列中,此时的同步任务已经执行完毕,直接执行这个 then(...);

  7. 又是遇到 new Promise(....),又是调用的 resolve(),所以 then() 方法会被添加到微任务队列中,代码往下执行,输出 "then2",此时微任务then(...)中的代码全部执行完毕;

  8. 此时同步任务执行完毕,继续执行微任务中的 then(...),输出 "then4";

  9. 所有代码运行完毕,程序结束;


案例二


<script>
console.log(1);

setTimeout(() => {
console.log(5);
});

new Promise((resolve) => {
resolve();
}).then(() => {
console.log(3);
});
console.log(2);
</script>

<script>
console.log(4);
</script>

这段代码的最后的输出结果是: 1 2 3 4 5,具体代码执行过程有以下步骤:
首先提醒一点,script 标签本身是一个宏任务,当页面出现多个 script 标签的时候,浏览器会把script 标签作为宏任务来解析。当前实例中两个 script 标签,它们会一次加入到宏任务队列中。



  1. console.log(...) 是同步代码,1首先会被输出,代码往下执行;

  2. 遇到 settimeout(),会被加入到宏任务队列中;

  3. then(...) 会被加入到微任务队列中,代码继续往下执行;

  4. console.log(...) 为同步认为输出 2;

  5. 此时同步任务执行完毕,转而执行微任务 then(...),输出 3;

  6. 当前宏任务执行完毕,此时同步任务和微任务都为空,取出最旧的宏任务,也就是第二个 script 标签;

  7. 输出 4,此时同步代码和微任务队列都为空,继续执行下一个宏任务,也就是 settimeout;

  8. 输出 5;


案例三


async function foo() {
console.log("start");
await bar();
console.log("end");
}

async function bar() {
console.log("bar");
}

console.log(1);

setTimeout(() => {
console.log("time");
});

foo();

new Promise((resolve) => {
console.log("p1");
resolve();
}).then(() => {
console.log("p2");
});

console.log(2);

这段代码的最后的输出结果是: 1 start bar p1 2 end p2 time,下面就来分析一下这段代码的执行过程:



  1. 前面两个是函数定义,不执行,遇到 console.log(),输出 1;

  2. 代码继续往下执行,遇到 settimeout(),代码加入到宏任务队列之中,代码往下执行;

  3. 调用 foo,输出 start;

  4. await 等待 bar() 调用的返回结果;

  5. 执行 bar() 函数,输出 bar;

  6. await 相当于 Promise.then(...),代码被加入到微任务队列中,所以 end 还不执行;

  7. 代码往下执行,遇到 new Promise(...),p1 直接输出,then() 又继续被加入到微任务队列中;

  8. 代码继续往下执行,遇到 console.log(2),输出 2;

  9. 此时主线程代码快为空,执行微任务队列中最旧的那个任务,继续执行 await 后续代码,输出 end;

  10. 执行 then() ,输出 p2;

  11. 最后执行 settimeout,输出 time;


案例四


Promise.resolve()
.then(() => {
console.log(0);
return Promise.resolve(4);
})
.then((res) => {
console.log(res);
});

Promise.resolve()
.then(() => {
console.log(1);
})
.then(() => {
console.log(2);
})
.then(() => {
console.log(3);
})
.then(() => {
console.log(5);
})
.then(() => {
console.log(6);
});

这个案例中,因为每一个 then() 都是一个微任务,所以首先执行的是0,代码继续往下执行,输出同级的 then(),也就是输出 1


如果 Promise 内返回的对象具有可调用的 then() 方法,则会在微任务队列中再插入一个任务,这就慢了一拍,如果这个 then() 方法是来源于 Promise 的,则因为是异步又慢了一拍,所以一共是慢了拍,所以 Promise.resolve(4) 的结果等到 23 输出完成,console.log(res) 的结果才会被输出;


所以该案例的最终结果输出的是 0 1 2 3 4 5 6


参考资料



总结


整个浏览器循环应该是 先执行 宏任务 -> 同步代码 -> 微任务,直到当前宏任务中的微任务清理完毕,继续执行下一个宏任务,以此类推。


最后分享两个我的两个开源项目,它们分别是:



这两个项目都会一直维护的,如果你也喜欢,欢迎 star 🥰🥰🥰


作者:Moment
来源:juejin.cn/post/7259927532249710653
收起阅读 »

今日算法09-青蛙跳台阶问题

web
一、题目描述 题目链接:leetcode.cn/problems/qi… 难易程度:简单 一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。 答案需要取模 1e9+7(1000000007),如计算初始结果为...
继续阅读 »

一、题目描述



题目链接:leetcode.cn/problems/qi…


难易程度:简单



一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。


答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。


示例1
输入:n = 2
输出:2

示例2
输入:n = 7
输出:21

示例3
输入:n = 0
输出:1




二、解题思路


动态规划


当 n 为 1 时,只有一种覆盖方法:





当 n = 2 时,有两种跳法:





跳 n 阶台阶,可以先跳 1 阶台阶,再跳 n-1 阶台阶;或者先跳 2 阶台阶,再跳 n-2 阶台阶。而 n-1 和 n-2 阶台阶的跳法可以看成子问题,该问题的递推公式为:





也就变成了斐波那契数列问题,参考:今日算法07-斐波那契数列


复杂度分析


时间复杂度 O(N) :计算 f(n) 需循环 n 次,每轮循环内计算操作使用 O(1) 。


空间复杂度 O(1) : 几个标志变量使用常数大小的额外空间。


三、代码实现


public int JumpFloor(int n) {
   if (n <= 2)
       return n;
   int pre2 = 1, pre1 = 2;
   int result = 0;
   for (int i = 2; i < n; i++) {
       result = pre2 + pre1;
       pre2 = pre1;
       pre1 = result;
  }
   return result;
}


推荐阅读



封面




今日算法系列,题解更新地址:studeyang.tech/2023/0725.h…



作者:杨同学technotes
来源:juejin.cn/post/7259543257708658747
收起阅读 »

媒体查询,响应式设计?帮帮我!

web
什么是媒体查询?媒体查询是一种 CSS 语言特性,它允许作者根据设备或窗口的特性有条件地应用 CSS 规则来查看应用程序。最常见的情况是根据视口宽度应用 CSS 规则,这样 CSS 作者就能根据窗口或设备的大小创建相应的组件和布局。但这也可能延伸到用户是否偏好...
继续阅读 »

什么是媒体查询?
媒体查询是一种 CSS 语言特性,它允许作者根据设备或窗口的特性有条件地应用 CSS 规则来查看应用程序。最常见的情况是根据视口宽度应用 CSS 规则,这样 CSS 作者就能根据窗口或设备的大小创建相应的组件和布局。但这也可能延伸到用户是否偏好浅色或深色模式,甚至是用户的可访问性偏好,以及更多属性。



什么是响应式设计?


随着各种设备类型和屏幕尺寸的增多,网络应用程序为用户提供更加量身定制的可视化展示,并针对用户首选交互方式的屏幕尺寸进行优化,已变得越来越重要。


响应式设计可以通过多种技术组合来实现,包括有条件地应用 CSS 规则的媒体查询、容器查询,以及根据它们所包含的内容(例如 flexbox 或 grid)选择灵活的布局。在本文中,我们将重点关注媒体查询和响应式布局,但随着浏览器支持程度的增加,容器查询也需要记住。在撰写本文时,它们还没有准备好进入普及阶段,但可以用于渐进式增强


什么是移动优先设计?


移动优先设计是在设计和构建响应式 web 应用时可以采用的原则。理想情况下,这种方法应该在流程的所有阶段--从开始到结束--都作为指导原则。对于设计来说,这意味着原型或 UI 设计的第一次迭代应该专注于移动的体验,然后再转向更宽的视口尺寸。


虽然你可以从另一个方向(宽优先)来处理 Web 应用程序,但随着屏幕空间的增加,在视觉上重新组织组件要比试图将组件塞进更小的屏幕空间容易得多。


类似的规则也适用于开发过程。一般来说,您应该为基本情况(最窄的屏幕)编写标记和样式,并在必要时逐步为更宽的屏幕应用条件样式。


虽然你可以从另一个方向来处理这个问题,或者使用窄优先和宽优先的混合方法,但这会使你的样式更难以理解,并增加了其他人在审查或维护时的精神负担。当然,也有一些例外情况,编写少量的宽优先规则会更简单,所以请谨慎行事。


CSS 像素对比设备像素


当苹果在2011年推出 iPhone 4时,它是第一款采用高密度显示屏的主流智能手机。早期的 iPhone 的显示分辨率为320x480px,当 iPhone 推出所谓的 “视网膜显示屏” 时 -- 在相同的物理显示宽度下,分辨率提高了一倍,达到640x960px -- 这带来了挑战。不希望用户面临他们不断问自己的情况,“这是什么,蚂蚁的网站?”,一个巧妙的解决方案被设计了出来,iPhone 4将遵循 CSS 规则,就好像它仍然是一个320 x480 px 的设备,并简单地以两倍的密度渲染。这使得现有的网站可以按预期运行,而不需要任何代码更改 - 当为 Web 引入新技术时,您会发现这是一个常见的主题。


由此,创建了术语 CSS 像素和设备像素。


W3C CSS 规范将设备像素定义为:



设备像素是设备输出上能够显示其全部颜色范围的最小面积单位。



CSS 像素(也称为逻辑像素或参考像素)由 W3C CSS 规范定义为:



参考像素是设备像素密度为96 dpi 并且与读取器的距离为一臂长的设备上的一个像素的视角。因此,对于28英寸的标称臂长,视角约为0.0213度。因此,对于在臂长处的阅读,lpx 对应于约0.26mm(1/96英寸)。



参考像素的96 DPI 规则并不总是严格遵守,并且可以根据设备类型和典型的观看距离而变化。
设备像素比率(或 dppx)是每个 CSS 像素使用多少设备像素的一维因子。设备像素比通常是整数(例如,整数)。1、2、3),因为这使得缩放更简单,但并不总是(例如,1.25、1.5等)。


如何使我的网站响应?


默认情况下,移动的浏览器将假定网站的设计不适合这种设备的较窄视口。为了向后兼容,这些浏览器可能会呈现一个网站,就好像屏幕更大,然后缩小到适合更小的屏幕。这不是一个理想的体验,用户经常需要缩放和平移页面,但允许网站的功能主要是因为它最初创建的。


要告诉浏览器某个站点正在为所有视口大小提供优化的体验,您可以在文档中包含以下 Meta 标记:


<meta name="viewport" content="width=device-width, initial-scale=1" />

非矩形显示器


如今,一些设备具有圆角或显示遮挡(诸如显示凹口、相机孔穿孔或软件覆盖),这意味着整个矩形对于用于内容来说是不“安全”的,因为它可能被部分地或完全地遮挡。


默认情况下,此类设备上的浏览器将在“安全区”内接矩形和与文档背景匹配的垂直或水平条内显示内容。


有一些方法可以允许内容扩展到这个区域,避免黑边邮筒的丑陋,但这是一个更高级的功能,不是必需的。


要退出默认的黑边和邮筒,并声明您的应用程序可以适当地处理屏幕的安全和不安全区域,您可以包含以下 Meta 标记:


<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />

文本大小


移动的浏览器还可能人为地增大字体大小,以使文本更具可读性。如果您的网站已经提供了适当大小的文本,您可以包含以下 CSS 来禁用此行为:


html {
-moz-text-size-adjust: none;
-webkit-text-size-adjust: none;
text-size-adjust: none;
}

虽然在无障碍标准中没有规定文本的最小大小,但在大多数情况下 16px 是一个很好的最低限度。


对于输入字段,如果字体大小小于 16px,浏览器可能会在聚焦时放大。有一些方法可以禁用这种行为,例如在 Meta viewport 中设置 maximum-scale=1.0,但强烈建议不要这样做,因为这可能会干扰依赖缩放的用户。更好的解决方案是确保 font-size 大小至少为 16px


什么是断点?


样式中的断点是指条件样式规则停止或开始应用于页面以响应视口大小的点。最常见的是指 min-widthmax-width,但也可以应用于 height


在媒体查询中,这些断点(768px479px)将像这样使用:


@media (min-width: 768px) {
// 宽度 > 768px的条件样式
}

@media (max-width: 479px) {
// 宽度 <= 479px的条件样式
}

当遵循移动优先设计原则时,大多数时候应该使用媒体查询。


还需要注意的是,min-*max-* 查询适用于包含范围,因此在定义断点两侧的样式时,不应使用相同的像素值。


同样重要的是要注意,当放大页面时,以 CSS 像素为单位的视口大小可能会沿着明显的设备像素比率而变化。这可能会导致视口实际上表现得好像其长度是小数值的情况。


@media (max-width: 479px) {
// 宽度 < 479px的条件样式
}

@media (min-width: 480px) {
// 宽度 >= 480px的条件样式
}

在上面的示例中,如果视口(作为缩放的结果)报告为 479.5px,则两个条件规则块都不适用。相反,例如额外的小数值 0.98px 通常应用于查询 max-width


为什么要这么说? 0.02px 是早期版本的 Safari 支持的 CSS 像素的最小分割。参见 WebKit bug #178261


CSS 在 Media Queries Level 4规范中引入了范围查询的概念,其中 <><=, 和 >= 可用于表达性更强的条件,包括包含和排除范围。在撰写本文时,所有主流浏览器都支持这些功能,然而,在 iOS 等平台上的支持还不够。


@media (width < 480px) {
// 宽度 < 480px的条件样式
}

@media (width >= 480px) {
// 宽度 >= 480px的条件样式
}

我应该选择哪些断点?


这是一个经常被问到的问题,但坦率地说,这并不重要,只要一个 Web 应用程序在你选择的断点之间的所有屏幕尺寸上都能正常工作。你也不想选择太多或太少。


iPhone 在2007年首次推出时,屏幕分辨率为320x480px。按照惯例,所有智能手机的视口宽度至少为320 CSS 像素。当建立一个响应式网站时,你至少应该满足这个宽度的设备。


最近,网络变得更容易被一类设备访问,这些设备适合更经典的外形,称为功能手机,以及可穿戴技术。这些设备通常具有小于320px 的视口宽度。


一些设备,如 Apple Watch,将表现得好像它们有一个320px 的视口,以允许与未专门针对极小视口进行优化的网站兼容。如果要声明您确实处理 Apple watch 的较窄视口,请在文档中包含以下 Meta 标记:


<meta name="disable-adaptations" content="watch" />

如果您使用的是设计系统或组件库(例如 Material UI、Bootstrap 等)它为您提供了自己的默认断点,您可能会发现坚持使用这些断点是有益的。


如果你选择自己的断点,有一些历史上相关的断点可以作为灵感:



  • 320px - 智能手机视口的最小宽度

  • 480px - 智能手机和平板电脑之间的近似边界

  • 768px - 最初的 iPad 分辨率为768 x1024 px

  • 1024px - 同上

  • 1280px - 16:9 720p(HD)显示器的标准宽度


通常,给断点命名是个好主意。但是,不要试图称它们为“移动”,“平板电脑”和“桌面”之类的名称。虽然在平板电脑的早期,移动,平板电脑和桌面之间的界限更加清晰,但现在有如此广泛的设备和视口尺寸,以至于这些设备之间的界限变得模糊。如今,我们有屏幕尺寸比一些平板电脑更大的可折叠手机,以及让台式机和笔记本电脑屏幕相形见绌的平板电脑屏幕。


将特定范围称为“平板电脑”或“台式机”可能会让您陷入为单一类型的设备(例如平板电脑)进行构建和设计的陷阱。假设“移动的”或“平板”视口将总是使用触摸屏)。相反,您应该专注于构建在各种设备上工作的体验。


响应式布局技术


有两种 CSS 布局算法特别适合响应式设计:



  • Flexbox

  • Grid


FLEXBOX


Flexbox 是一种 CSS 布局算法,它允许我们指定子元素在页面上的排列方式。此控件应用于特定方向(称为 flex 轴)。


虽然 flexbox 可以用于呈现多行(带换行),但一行中的内容元素不会改变其他行中元素的排列方式。这意味着除非明确设置 flex 项的宽度,否则它们的排列方式可能不一致。如果需要,CSS Grid 可能更合适。


Flex Wrap


使用 Flexbox 时可以不使用媒体查询,而是依靠 flex-wrap 属性,使内容可以根据内容大小多次跨轴。设置 flex-wrap: wrap 将意味着内容在下方( flex-direction: row)或右侧( flex-direction: column)换行。您还可以设置 flex-wrap: wrap-reverse 使内容在上方或左侧换行。


Flex Direction


通常情况下,对于水平空间有限的窄视口,设计可能要求垂直排列内容,但对于屏幕空间较大的宽视口,则可能改为水平排列内容。


.className {
display: flex;
flex-direction: column;
}

@media (min-width: 768px) {
.className {
flex-direction: row;
}
}

长期以来,媒体查询需要在顶层定义,但当相关规则没有在大型样式表中共存时,这会增加维护负担。在撰写本文时,浏览器尚未广泛支持这种做法,但许多工具和预处理器都允许这样做。


.className {
display: flex;
flex-direction: column;

@media (min-width: 768px) {
flex-direction: row;
}
}

GRID


Grid 是一种 CSS 布局算法,它允许我们指定子元素在页面上的排列方式。Grid 允许开发人员指定元素在行和列之间的排列方式。


就可实现的布局类型而言,它与 flexbox 有重叠之处,但也有显著区别。使用 Grid 布局时,网格项会根据横轴和纵轴上的网格轨道进行约束和对齐。


以下是与响应式设计搭配使用的常见布局技术的几个示例。


Columns


设计师通常会使用 12 栏网格(或窄视口的 4 栏网格)。您可以使用 grid-template-columns 在 CSS 中复制这种模式。结合断点,您就可以轻松分配类,使元素跨越特定的列数。


Google的Una Kravets在One Line Layouts开发站点上分享了一些交互式示例。


RAM(重复、自动、最小最大)


另一种网格布局技术通常称为RAM(重复,自动,最小最大)。我鼓励你去看看One Line Layouts开发站点上的交互式示例


当你事先不知道网格需要多少列,而是希望在一些预设的范围内让内容的大小来决定列数时,RAM 是最有用的。auto-fitauto-fill 的工作方式类似,但当项目数量少于填充一行的数量时会发生什么情况除外。


// 网格项目将始终至少150像素宽,
// 并将伸展以填满所有可用空间
.auto-fit {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}

// 网格项目将始终至少150像素宽,
// 并且会伸展直到有足够的空间
// (如果有)添加匹配的空网格轨道
.auto-fill {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}

Grid Template Areas


网格模板区域是用于响应式布局的最强大工具之一,可让您以直观的方式将元素排列到网格上。


举个例子,我们可能会有一个包含页眉、主要部分、侧边栏和页脚的布局,它们都在狭窄的视口上垂直排列。不过,将 grid-template-areas 区域与 grid-template-columnsgrid-template-rows 结合使用,我们可以用相同的标记将这些元素重新排列成网格模式。


代码示例:


.layout {
display: grid;
grid-template-areas:
"header"
"main"
"sidebar"
"footer";
grid-template-rows: auto 1fr auto auto;
}

@media (min-width: 768px) {
.layout {
grid-template-areas:
"header header"
"main sidebar"
"footer footer";
grid-template-columns: 1fr 200px;
grid-template-rows: auto 1fr auto;
}
}

.header {
grid-area: header;
}

.main {
grid-area: main;
}

.sidebar {
grid-area: sidebar;
}

.footer {
grid-area: footer;
}

<div class="layout">
<header class="header">Headerheader>
<main class="main">Mainmain>
<aside class="sidebar">Sidebaraside>
<footer class="footer">Footerfooter>
div>

响应图像


在高密度显示屏上,根据 CSS 像素而不是设备像素来调整图片大小,可能会导致图片质量低于用户的预期,尤其是在显示清晰的文本或矢量资源时,会显得格外刺眼。因此,为用户提供更高密度的图片是有意义的。


如果可能,请使用基于矢量的图像 (SVG)。矢量不是指定像素的光栅,而是描述在屏幕上绘制某些内容的过程,这一过程可以放大/缩小到任何屏幕尺寸,但始终保持清晰。矢量图像通常适用于简单的插图、图标或徽标。它们不适用于照片。



请注意,SVG 可以嵌入光栅图像。如果是这种情况,又无法获得真正的矢量图像,最好直接使用光栅图像。这是因为光栅图像在 SVG 中使用 base64 编码,与普通二进制文件相比,文件大小会增大。


对于光栅图像,有几种为高密度显示指定多个图像源的方法,允许浏览器选择最适合特定设备的图像源。


对于静态大小的图像,你可以使用 x 描述符(指定最佳设备像素比)指定。例如,如果您有一个图标或徽标,显示宽度为44px,你可以创建该图像的多个不同版本,并指定如下内容:


<img
srcset="
/path/
to/img-44w.png 1x,
/path/
to/img-66w.png 1.5x,
/path/
to/img-88w.png 2x,
/path/
to/img-132w.png 3x
"

/>


重要的是,这些 x 描述符只是一种提示,设备仍可能出于各种原因(如用户选择了节省带宽的规定)选择较低分辨率的版本。


在 CSS 中使用 image-set()(注意浏览器的支持并不完善)对background-image也可以采用类似的技术:


.selector {
height: 44px;
width: 44px;
/* 对于不支持 image-set() 的浏览器使用 2x 回退 */
background-image: url(/path/to/img-88w.png);
/* Safari 只支持 -webkit-image-set() */
background-image: -webkit-image-set(
url(/path/to/img-44w.png) 1x,
url(/path/to/img-66w.png) 1.5x,
url(/path/to/img-88w.png) 2x,
url(/path/to/img-132w.png) 3x
);
/* 标准语法 */
background-image: image-set(
url(/path/to/img-44w.png) 1x,
url(/path/to/img-66w.png) 1.5x,
url(/path/to/img-88w.png) 2x,
url(/path/to/img-132w.png) 3x
);
}

对于随着页面大小调整而改变大小的图像,可以组合使用 srcsetsizes 属性。例如。


<img
srcset="
/path/to/img-320w.jpg 320w,
/path/to/img-480w.jpg 480w,
/path/to/img-640w.jpg 640w,
/path/to/img-960w.jpg 960w,
/path/to/img-1280w.jpg 1280w
"

sizes="(min-width: 768px) 480px, 100vw"
/>


在上面的例子中,我们在 srcset 中以不同实际宽度(320px、480px、640px、960px、1280px)的多个不同图像渲染。在 sizes 属性中,我们告诉浏览器这些图像将默认以视口宽度的 100% 显示,然后对于768px 和更宽的视口,图像将以固定的480 CSS 像素宽度显示。然后,浏览器将根据设备像素比为设备选择最佳图像渲染(尽管这只是一个提示,浏览器可以选择使用更高或更低的分辨率选项)。


使用 WebP 和 AVIF 等现代图像格式压缩技术,当图像以 2 倍密度显示时,文件大小通常只比 1 倍版本略有增加。而且,当设备像素比大于 2 时,收益会越来越小。因此,您可以只包括优化的2x 图像。Google Chrome 团队的开发者倡导者 Jake Archibald 写了一篇博客文章讨论了这一问题,并强调了一个事实:你的大多数用户可能都在使用高密度显示器浏览网页。

作者:chansee97
来源:juejin.cn/post/7259605860603576375

收起阅读 »

微服务的各种边界在架构演进中的作用

演进式架构 在微服务设计和实施的过程中,很多人认为:“将单体拆分成多少个微服务,是微服务的设计重点。”可事实真的是这样吗?其实并非如此! Martin Fowler 在提出微服务时,他提到了微服务的一个重要特征——演进式架构。那什么是演进式架构呢?演进式...
继续阅读 »

演进式架构


在微服务设计和实施的过程中,很多人认为:“将单体拆分成多少个微服务,是微服务的设计重点。”可事实真的是这样吗?其实并非如此!


Martin Fowler 在提出微服务时,他提到了微服务的一个重要特征——演进式架构。那什么是演进式架构呢?演进式架构就是以支持增量的、非破坏的变更作为第一原则,同时支持在应用程序结构层面的多维度变化。


那如何判断微服务设计是否合理呢?其实很简单,只需要看它是否满足这样的情形就可以了:随着业务的发展或需求的变更,在不断重新拆分或者组合成新的微服务的过程中,不会大幅增加软件开发和维护的成本,并且这个架构演进的过程是非常轻松、简单的。


这也是微服务设计的重点,就是看微服务设计是否能够支持架构长期、轻松的演进。


那用DDD方法设计的微服务,不仅可以通过限界上下文和聚合实现微服务内外的解耦,同时也可以很容易地实现业务功能积木式模块的重组和更新,从而实现架构演进。


微服务还是小单体?


有些项目团队在将集中式单体应用拆分为微服务时,首先进行的往往不是建立领域模型,而只是按照业务功能将原来单体应用的一个软件包拆分成多个所谓的“微服务”软件包,而这些“微服务”内的代码仍然是集中式三层架构的模式,“微服务”内的代码高度耦合,逻辑边界不清晰,这里我们暂且称它为“小单体微服务”。


下面这张图也很好地展示了这个过程。





而随着新需求的提出和业务的发展,这些小单体微服务会慢慢膨胀起来。当有一天你发现这些膨胀了的微服务,有一部分业务功能需要拆分出去,或者部分功能需要与其它微服务进行重组时,你会发现原来这些看似清晰的微服务,不知不觉已经摇身一变,变成了臃肿油腻的大单体了,而这个大单体内的代码依然是高度耦合且边界不清的。


“辛辛苦苦好多年,一夜回到解放前啊!”这个时候你就需要一遍又一遍地重复着从大单体向单体微服务重构的过程。想想,这个代价是不是有点高了呢?


其实这个问题已经很明显了,那就是边界。


这种单体式微服务只定义了一个维度的边界,也就是微服务之间的物理边界,本质上还是单体架构模式。微服务设计时要考虑的不仅仅只有这一个边界,别忘了还要定义好微服务内的逻辑边界和代码边界,这样才能得到你想要的结果。


那现在你知道了,我们一定要避免将微服务设计为小单体微服务,那具体该如何避免呢?清晰的边界人人想要,可该如何保证呢?DDD已然给出了答案。


微服务边界的作用


你应该还记得DDD设计方法里的限界上下文和聚合吧?它们就是用来定义领域模型和微服务边界的。


我们再来回顾一下DDD的设计过程。


在事件风暴中,我们会梳理出业务过程中的用户操作、事件以及外部依赖关系等,根据这些要素梳理出实体等领域对象。根据实体对象之间的业务关联性,将业务紧密相关的多个实体进行组合形成聚合,聚合之间是第一层边界。根据业务及语义边界等因素将一个或者多个聚合划定在一个限界上下文内,形成领域模型,限界上下文之间的边界是第二层边界。


为了方便理解,我们将这些边界分为: 逻辑边界、物理边界和代码边界


逻辑边界 主要定义同一业务领域或应用内紧密关联的对象所组成的不同聚类的组合之间的边界。事件风暴对不同实体对象进行关联和聚类分析后,会产生多个聚合和限界上下文,它们一起组成这个领域的领域模型。微服务内聚合之间的边界就是逻辑边界。一般来说微服务会有一个以上的聚合,在开发过程中不同聚合的代码隔离在不同的聚合代码目录中。


逻辑边界在微服务设计和架构演进中具有非常重要的意义!


微服务的架构演进并不是随心所欲的,需要遵循一定的规则,这个规则就是逻辑边界。微服务架构演进时,在业务端以聚合为单位进行业务能力的重组,在微服务端以聚合的代码目录为单位进行微服务代码的重组。由于按照DDD方法设计的微服务逻辑边界清晰,业务高内聚,聚合之间代码松耦合,因此在领域模型和微服务代码重构时,我们就不需要花费太多的时间和精力了。


现在我们来看一个微服务实例,在下面这张图中,我们可以看到微服务里包含了两个聚合的业务逻辑,两个聚合分别内聚了各自不同的业务能力,聚合内的代码分别归到了不同的聚合目录下。


那随着业务的快速发展,如果某一个微服务遇到了高性能挑战,需要将部分业务能力独立出去,我们就可以以聚合为单位,将聚合代码拆分独立为一个新的微服务,这样就可以很容易地实现微服务的拆分。





另外,我们也可以对多个微服务内有相似功能的聚合进行功能和代码重组,组合为新的聚合和微服务,独立为通用微服务。现在你是不是有点做中台的感觉呢?


物理边界 主要从部署和运行的视角来定义微服务之间的边界。不同微服务部署位置和运行环境是相互物理隔离的,分别运行在不同的进程中。这种边界就是微服务之间的物理边界。


代码边界 主要用于微服务内的不同职能代码之间的隔离。微服务开发过程中会根据代码模型建立相应的代码目录,实现不同功能代码的隔离。由于领域模型与代码模型的映射关系,代码边界直接体现出业务边界。代码边界可以控制代码重组的影响范围,避免业务和服务之间的相互影响。微服务如果需要进行功能重组,只需要以聚合代码为单位进行重组就可以了。


正确理解微服务的边界


从上述内容中,我们知道了,按照DDD设计出来的逻辑边界和代码边界,让微服务架构演进变得不那么费劲了。


微服务的拆分可以参考领域模型,也可以参考聚合,因为聚合是可以拆分为微服务的最小单位的。但实施过程是否一定要做到逻辑边界与物理边界一致性呢?也就是说聚合是否也一定要设计成微服务呢?答案是不一定的,这里就涉及到微服务过度拆分的问题了。


微服务的过度拆分会使软件维护成本上升,比如:集成成本、发布成本、运维成本以及监控和定位问题的成本等。在项目建设初期,如果你不具备较强的微服务管理能力,那就不宜将微服务拆分过细。当我们具备一定的能力以后,且微服务内部的逻辑和代码边界也很清晰,你就可以随时根据需要,拆分出新的微服务,实现微服务的架构演进了。


当然,还要记住一点,微服务内聚合之间的服务调用和数据依赖需要符合高内聚松耦合的设计原则和开发规范,否则你也不能很快完成微服务的架构演进。


总结


我们主要讨论了微服务架构设计中的各种边界在架构演进中的作用。


逻辑边界: 微服务内聚合之间的边界是逻辑边界。它是一个虚拟的边界,强调业务的内聚,可根据需要变成物理边界,也就是说聚合也可以独立为微服务。


物理边界: 微服务之间的边界是物理边界。它强调微服务部署和运行的隔离,关注微服务的服务调用、容错和运行等。


代码边界: 不同层或者聚合之间代码目录的边界是代码边界。它强调的是代码之间的隔离,方便架构演进时代码的重组。


通过以上边界,我们可以让业务能力高内聚、代码松耦合,且清晰的边界,可以快速实现微服务代码的拆分和组合,轻松实现微服务架构演进。但有一点一定要格外注意,边界清晰的微服务,不是大单体向小单体的演进。


作者:架构狂人
来源:mdnice.com/writing/2e64f8fdf9cb4213894a57d4e7a8a904
收起阅读 »

分布式架构关键设计10问

一、选择什么样的分布式数据库? 分布式架构下的数据应用场景远比集中式架构复杂,会产生很多数据相关的问题。谈到数据,首先就是要选择合适的分布式数据库。 分布式数据库大多采用数据多副本的方式,实现数据访问的高性能、多活和容灾。目前主要有三种不同的分布式数据库...
继续阅读 »

一、选择什么样的分布式数据库?


分布式架构下的数据应用场景远比集中式架构复杂,会产生很多数据相关的问题。谈到数据,首先就是要选择合适的分布式数据库。


分布式数据库大多采用数据多副本的方式,实现数据访问的高性能、多活和容灾。目前主要有三种不同的分布式数据库解决方案。它们的主要差异是数据多副本的处理方式和数据库中间件。


1. 一体化分布式数据库方案


它支持数据多副本、高可用。多采用Paxos协议,一次写入多数据副本,多数副本写入成功即算成功。代表产品是OceanBase和高斯数据库。


2. 集中式数据库+数据库中间件方案


它是集中式数据库与数据库中间件结合的方案,通过数据库中间件实现数据路由和全局数据管理。数据库中间件和数据库独立部署,采用数据库自身的同步机制实现主副本数据的一致性。集中式数据库主要有MySQL和PostgreSQL数据库,基于这两种数据库衍生出了很多的解决方案,比如开源数据库中间件MyCat+MySQL方案,TBase(基于PostgreSQL,但做了比较大的封装和改动)等方案。


3. 集中式数据库+分库类库方案


它是一种轻量级的数据库中间件方案,分库类库实际上是一个基础JAR包,与应用软件部署在一起,实现数据路由和数据归集。它适合比较简单的读写交易场景,在强一致性和聚合分析查询方面相对较弱。典型分库基础组件有ShardingSphere。


小结: 这三种方案实施成本不一样,业务支持能力差异也比较大。一体化分布式数据库主要由互联网大厂开发,具有超强的数据处理能力,大多需要云计算底座,实施成本和技术能力要求比较高。集中式数据库+数据库中间件方案,实施成本和技术能力要求适中,可满足中大型企业业务要求。第三种分库类库的方案可处理简单的业务场景,成本和技能要求相对较低。在选择数据库的时候,我们要考虑自身能力、成本以及业务需要,从而选择合适的方案。


二、如何设计数据库分库主键?


选择了分布式数据库,第二步就要考虑数据分库,这时分库主键的设计就很关键了。


与客户接触的关键业务,我建议你以客户ID作为分库主键。这样可以确保同一个客户的数据分布在同一个数据单元内,避免出现跨数据单元的频繁数据访问。跨数据中心的频繁服务调用或跨数据单元的查询,会对系统性能造成致命的影响。


将客户的所有数据放在同一个数据单元,对客户来说也更容易提供客户一致性服务。而对企业来说,“以客户为中心”的业务能力,首先就要做到数据上的“以客户为中心”。


当然,你也可以根据业务需要用其它的业务属性作为分库主键,比如机构、用户等。


三、数据库的数据同步和复制


在微服务架构中,数据被进一步分割。为了实现数据的整合,数据库之间批量数据同步与复制是必不可少的。数据同步与复制主要用于数据库之间的数据同步,实现业务数据迁移、数据备份、不同渠道核心业务数据向数据平台或数据中台的数据复制、以及不同主题数据的整合等。


传统的数据传输方式有ETL工具和定时提数程序,但数据在时效性方面存在短板。分布式架构一般采用基于数据库逻辑日志增量数据捕获(CDC)技术,它可以实现准实时的数据复制和传输,实现数据处理与应用逻辑解耦,使用起来更加简单便捷。


现在主流的PostgreSQL和MySQL数据库外围,有很多数据库日志捕获技术组件。CDC也可以用在领域事件驱动设计中,作为领域事件增量数据的获取技术。


四、跨库关联查询如何处理?


跨库关联查询是分布式数据库的一个短板,会影响查询性能。在领域建模时,很多实体会分散到不同的微服务中,但很多时候会因为业务需求,它们之间需要关联查询。


关联查询的业务场景包括两类:第一类是基于某一维度或某一主题域的数据查询,比如基于客户全业务视图的数据查询,这种查询会跨多个业务线的微服务;第二类是表与表之间的关联查询,比如机构表与业务表的联表查询,但机构表和业务表分散在不同的微服务。


如何解决这两类关联查询呢?


对于第一类场景,由于数据分散在不同微服务里,我们无法跨多个微服务来统计这些数据。你可以建立面向主题的分布式数据库,它的数据来源于不同业务的微服务。采用数据库日志捕获技术,从各业务端微服务将数据准实时汇集到主题数据库。在数据汇集时,提前做好数据关联(如将多表数据合并为一个宽表)或者建立数据模型。面向主题数据库建设查询微服务。这样一次查询你就可以获取客户所有维度的业务数据了。你还可以根据主题或场景设计合适的分库主键,提高查询效率。


对于第二类场景,对于不在同一个数据库的表与表之间的关联查询场景,你可以采用小表广播,在业务库中增加一张冗余的代码副表。当主表数据发生变化时,你可以通过消息发布和订阅的领域事件驱动模式,异步刷新所有副表数据。这样既可以解决表与表的关联查询,还可以提高数据的查询效率。


五、如何处理高频热点数据?


对于高频热点数据,比如商品、机构等代码类数据,它们同时面向多个应用,要有很高的并发响应能力。它们会给数据库带来巨大的访问压力,影响系统的性能。


常见的做法是将这些高频热点数据,从数据库加载到如Redis等缓存中,通过缓存提供数据访问服务。这样既可以降低数据库的压力,还可以提高数据的访问性能。


另外,对需要模糊查询的高频数据,你也可以选用ElasticSearch等搜索引擎。


缓存就像调味料一样,投入小、见效快,用户体验提升快。


六、前后序业务数据的处理


在微服务设计时你会经常发现,某些数据需要关联前序微服务的数据。比如:在保险业务中,投保微服务生成投保单后,保单会关联前序投保单数据等。在电商业务中,货物运输单会关联前序订单数据。由于关联的数据分散在业务的前序微服务中,你无法通过不同微服务的数据库来给它们建立数据关联。


如何解决这种前后序的实体关联呢?


一般来说,前后序的数据都跟领域事件有关。你可以通过领域事件处理机制,按需将前序数据通过领域事件实体,传输并冗余到当前的微服务数据库中。


你可以将前序数据设计为实体或者值对象,并被当前实体引用。在设计时你需要关注以下内容:如果前序数据在当前微服务只可整体修改,并且不会对它做查询和统计分析,你可以将它设计为值对象;当前序数据是多条,并且需要做查询和统计分析,你可以将它设计为实体。


这样,你可以在货物运输微服务,一次获取前序订单的清单数据和货物运输单数据,将所有数据一次反馈给前端应用,降低跨微服务的调用。如果前序数据被设计为实体,你还可以将前序数据作为查询条件,在本地微服务完成多维度的综合数据查询。只有必要时才从前序微服务,获取前序实体的明细数据。这样,既可以保证数据的完整性,还可以降低微服务的依赖,减少跨微服务调用,提升系统性能。


七、数据中台与企业级数据集成


分布式微服务架构虽然提升了应用弹性和高可用能力,但原来集中的数据会随着微服务拆分而形成很多数据孤岛,增加数据集成和企业级数据使用的难度。你可以通过数据中台来实现数据融合,解决分布式架构下的数据应用和集成问题。


你可以分三步来建设数据中台。


第一,按照统一数据标准,完成不同微服务和渠道业务数据的汇集和存储,解决数据孤岛和初级数据共享的问题。


第二,建立主题数据模型,按照不同主题和场景对数据进行加工处理,建立面向不同主题的数据视图,比如客户统一视图、代理人视图和渠道视图等。


第三,建立业务需求驱动的数据体系,支持业务和商业模式创新。


数据中台不仅限于分析场景,也适用于交易型场景。你可以建立在数据仓库和数据平台上,将数据平台化之后提供给前台业务使用,为交易场景提供支持。


八、BFF与企业级业务编排和协同


企业级业务流程往往是多个微服务一起协作完成的,每个单一职责的微服务就像积木块,它们只完成自己特定的功能。那如何组织这些微服务,完成企业级业务编排和协同呢?


你可以在微服务和前端应用之间,增加一层BFF微服务(Backend for Frontends)。 BFF主要职责是处理微服务之间的服务组合和编排,微服务内的应用服务也是处理服务的组合和编排,那这二者有什么差异呢?


BFF位于中台微服务之上,主要职责是微服务之间的服务协调; 应用服务主要处理微服务内的服务组合和编排。 在设计时我们应尽可能地将可复用的服务能力往下层沉淀,在实现能力复用的同时,还可以避免跨中心的服务调用。


BFF像齿轮一样,来适配前端应用与微服务之间的步调。它通过Façade服务适配不同的前端,通过服务组合和编排,组织和协调微服务。BFF微服务可根据需求和流程变化,与前端应用版本协同发布,避免中台微服务为适配前端需求的变化,而频繁地修改和发布版本,从而保证微服务核心领域逻辑的稳定。


如果你的BFF做得足够强大,它就是一个集成了不同中台微服务能力、面向多渠道应用的业务能力平台。


九、分布式事务还是事件驱动机制?


分布式架构下,原来单体的内部调用,会变成分布式调用。如果一个操作涉及多个微服务的数据修改,就会产生数据一致性的问题。数据一致性有强一致性和最终一致性两种,它们实现方案不一样,实施代价也不一样。


对于实时性要求高的强一致性业务场景,你可以采用分布式事务,但分布式事务有性能代价,在设计时我们需平衡考虑业务拆分、数据一致性、性能和实现的复杂度,尽量避免分布式事务的产生。


领域事件驱动的异步方式是分布式架构常用的设计方法,它可以解决非实时场景的数据最终一致性问题。基于消息中间件的领域事件发布和订阅,可以很好地解耦微服务。通过削峰填谷,可以减轻数据库实时访问压力,提高业务吞吐量和处理能力。你还可以通过事件驱动实现读写分离,提高数据库访问性能。对最终一致性的场景,我建议你采用领域事件驱动的设计方法。


十、多中心多活的设计


分布式架构的高可用主要通过多活设计来实现,多中心多活是一个非常复杂的工程,下面我主要列出以下几个关键的设计。


1.选择合适的分布式数据库。数据库应该支持多数据中心部署,满足数据多副本以及数据底层复制和同步技术要求,以及数据恢复的时效性要求。


2.单元化架构设计。将若干个应用组成的业务单元作为部署的基本单位,实现同城和异地多活部署,以及跨中心弹性扩容。各单元业务功能自包含,所有业务流程都可在本单元完成;任意单元的数据在多个数据中心有副本,不会因故障而造成数据丢失;任何单元故障不影响其它同类单元的正常运行。单元化设计时我们要尽量避免跨数据中心和单元的调用。


3.访问路由。访问路由包括接入层、应用层和数据层的路由,确保前端访问能够按照路由准确到达数据中心和业务单元,准确写入或获取业务数据所在的数据库。


4.全局配置数据管理。实现各数据中心全局配置数据的统一管理,每个数据中心全局配置数据实时同步,保证数据的一致性。


总结


企业级分布式架构的实施是一个非常复杂的系统工程,涉及到非常多的技术体系和方法。今天我列的10个关键的设计领域,每个领域其实都非常复杂,需要很多的投入和研究。在实施的时候,你和你的公司要结合自身情况来选择合适的技术组件和实施方案。


作者:架构狂人
来源:mdnice.com/writing/efcac6bf632b4172903c8a14c2e1f0f4
收起阅读 »

纯C文件推理Llama 2

web
这段项目可以让你通过PyTorch从头开始训练Llama 2 LLM架构模型,然后将权重保存到一个原始二进制文件中,再将其加载到一个仅有500行的简单C文件(run.c)中,该文件推断模型,目前仅支持fp32。在作者的云Linux开发平台上,一个维度为288的...
继续阅读 »

这段项目可以让你通过PyTorch从头开始训练Llama 2 LLM架构模型,然后将权重保存到一个原始二进制文件中,再将其加载到一个仅有500行的简单C文件(run.c)中,该文件推断模型,目前仅支持fp32。在作者的云Linux开发平台上,一个维度为288的6层6头模型(约15M个参数)推断速度约为每秒100个令牌;在M1 MacBook Air上推断速度也差不多。作者有些惊喜地发现,采用这种简单方法,可以以高度交互的速度运行相当大的模型(几千万个参数)。




参考文献:
https://github.com/karpathy/llama2.c

作者:阿升
来源:mdnice.com/writing/6f98f171b14e4050bf627afe59ccb82a

收起阅读 »

也许跟大家不太一样,我是这么用TypeScript来写前端的

web
一、当前一些写前端的骚操作 先罗列一下见到过的一些写法吧:) 1. interface(或Type)一把梭 掘金上很多文章,一提到 TypeScript,那不得先用 interface 或者 type 来声明个数据结构吗?像这样: type User = { ...
继续阅读 »

一、当前一些写前端的骚操作


先罗列一下见到过的一些写法吧:)


1. interface(或Type)一把梭


掘金上很多文章,一提到 TypeScript,那不得先用 interface 或者 type 来声明个数据结构吗?像这样:


type User = {
nickname: string
avatar?: string
age: number
}

interface User {
nickname: string
avatar?: string
age: number
}

然后其他方法限制下入参类型,搞定,我掌握了 TypeScript 了,工资不得给我涨3000???



这里说明一下, 我司 不允许 直接使用 interface type 来定义非装饰器参数和配置性参数之外其他 任何数据类型



2. 类型体操整花活


要么把属性整成只读了,要么猪狗类型联合了,要么猪尾巴搞丢了,要么牛真的会吹牛逼了。


类型体操确实玩出了很多花活。 昨天说过了:TypeScript最好玩的就是类型体操, 也恰好是最不应该出现的东西


3. hook 的无限神话


不知道什么时候开始,hook 越来越流行。 听说不会写 hook 的前端程序员,已经算不上高阶程序员了, 不 use 点啥都展示不出牛逼的水平。


4. axios 拦截大法好


随便搜索一下 axios 的文章, 没有 拦截器 这个关键词的文章都算不上 axios 的高端用法了。


二、 我们一些不太一样的前端骚操作


昨天的文章有提到一些关于在前端使用 装饰器 来实现一些基于配置的需求实现, 今天其实想重点聊一聊如何在前端优雅的面向对象。


写过 JavaSpringBootJPA 等代码的后端程序员应该非常熟悉的一些概念:



  • 抽象: 万物都可抽象成相关的类和对象

  • 面向对象: 继承、封装、多态等特性的面向对象设计思维

  • 切面: 没有什么是切一刀解决不了的,如果一刀不行, 那就多来几刀。

  • 注解: 没有什么常量是不能使用注解来配置的, 也没有什么注解是切面想切还能躲得掉的

  • 反射: 没有什么是暴力拿取会失败的, 即使失败也没有异常是丢不出来的

  • 实体: 没有什么是不能抽象到实体上的, 万物皆唯一。

  • 很多: 还有很多,以上描述比较主观和随意。


于是我们开始把后端思维往前端来一个个的转移:)


1. 抽象和面向对象


与后端的交互数据对象、 请求的API接口都给抽象到具体的类上去,于是有了:



  • Service API请求类


abstract class AbstractService{
// 实现一个抽象属性 让子类们实现
abstract baseUrl!: string

// 再实现一些通用的 如增删改查之类的网络请求
// save()

// getDetail()

// deleteById()

// select()

// page()

// disabled()

// ......
}


  • Entity 数据实体基类


abstract class AbstractBaseEntityextends AbstractService> {
abstract service!: AbstractService

// 任何数据都是唯一的 ID
id!: number

// 再来实现一些数据实体的更新和删除方法
save(){
await service.save(this.toJson())
Notify.success("新增成功")
}

delete(){
service.deleteById(this.id)
Notify.success("删除成功")
}

async validate(scene: EntityScene):Promise<void>{
return new Promise((resolve,reject)=>{
// 多场景的话 可以Switch
if(...){
Notify.error("XXX校验失败")
reject();
}
resove();
})
}
// ......
}



  • 子类的实现:)


class UserEntity extends AbstractUserEntity<UserService>{
service = new UserService()

nickname!: string
age!: number
avatar?: string

// 用户是否成年人
isAdult(): boolean{
return this.age >= 18
}

async validate(scene: EntityScene): Promise<void> {
return new Promise((resove,reject)=>{
if(!this.isAdult()){
Notify.error("用户未成年, 请确认年龄")
reject();
}
await super.validate(scene)
})
}

}


  • View 视图调用


<template>
<el-input v-model="user.nickname"/>
<el-button @click="onUserSave()">创建用户el-button>
template>
<script setup lang="ts">
const user = ref(new UserEntity())
async function onUserSave(){
await user.validate(EntityScene.SAVE);
await user.save()
}
script>

2. 装饰器/切面/反射


装饰器部分的话,昨天的文章有提到一些了,今天主要所说反射和切面部分。


TypeScript 中, 其实装饰器本身就可以理解为一个切面了, 这里与 Java 中还是有很多不同的, 但概念和思维上是基本一致的。


反射 ReflectTypeScript 中比较坑的一个存在, 目前主要是依赖 reflect-metadata 这个第三方库来实现, 将一些元数据存储到 metadata 中, 在需要使用的时候通过反射的方式来获取。 可以参考这篇文章:TypeScript 中的元数据以及 reflect-metadata 实现原理分析


在实际使用中, 我们早前用的是 class-transformer 这个库, 之前我对这个库的评价应该是非常高的: “如果没有 class-transformer 这个库, TypeScript 狗都不写。”


确实很棒的一个库,但是在后来,我们写了个通用的内部框架, 为了适配 微信小程序端 以及 uniapp 端, 再加上有一些特殊的业务功能以及 class-transfromer 的写法和命名方式我个人不太喜欢的种种原因, 我们放弃了这个库, 但我们仿照了它的思想重新实现了一个内部使用的库,做了一些功能的阉割和新特性的添加。


核心功能的一些说明




  • 通过反射进行数据转换



    如将后端API返回的数据按照前端的数据结构强制进行转换, 当后端数据返回乱七八糟的时候,保证前端数据在使用中不会出现任何问题, 如下 demo



    class UserEntity {
    @Type(String) phone!: string;
    @Type(RoleEntity) roleInfo!: RoleEntity:
    @Type(DeptEntity) @List @Default([]) deptInfoList!: DeptEntity[]
    @Type(Boolean) @Default(false) isDisabled!: boolean
    }



  • 通过反射进行配置的存储和读取



    这个在昨天的文章中有讲到一部分, 比如配置表单、表格、搜索框、权限 等





3. 再次强调面向对象


为了整个前端项目的工程化、结构化、高度抽象化,这里不得不再次强调面向对象的设计:)




  • 这是个拼爹的社会



    一些通用的功能,一旦有复用的可能, 都可以考虑和尝试让其父类进行实现, 如需要子类传入一些特性参数时, 可以使用抽象方法或抽象属性(这可是Java中没有的)来传入父类实现过程中需要的特性参数。





  • 合理的抽象分层



    将一些特性按照不同的抽象概念进行组合与抽离,实现每个类的功能都是尽可能不耦合,实现类的单一职责。如存在多继承, 在考虑实现类的实现成本前提下,可考虑抽象到接口 interface 中。





  • 还有很多,有空再一一列举




4. 严格但又有趣的 tsdoc


我们先来看一些注释的截图吧:)








一些详细的注释、弃用的方法、选填的参数、传入参数后可能影响或依赖的其他参数,在注释里写好玩的 emoji或者图片,甚至是 直接在注释里写调用 demo, 让调用方可以很轻松愉快的对接调用, 玩归玩, 确实对整体项目的质量有很大的帮助。


三、 写在最后


中午跟同事吃饭聊了聊现在国内大前端的一个状态, 当时聊到一个关键词 舒适区, 还有前端整个技术栈过于灵活的一些优缺点, 几个大老爷们都发出了一些感慨, 如果前端能够更标准化一些, 像 Java 一样, 说不定前端还能上升几个高度。


我们基于今天文章里的一些设计写了一些DEMO,但目前不太方便直接开源,如果有兴趣,可以私聊我获取代码链接。


是的, 我还是那个 Java 仔, 是, 也不仅仅是。

作者:Hamm
来源:juejin.cn/post/7259562014417813564

收起阅读 »

请自信的大声告诉面试官forEach跳不出循环

web
如果面试官,或者有人问你foreach怎么跳出循环,请你大声的告诉ta,跳不出!!!!!!!!!! foreach 跳不出循环 为什么呢? 先看看foreach大体实现。 Array.prototype.customForEach = function (fn...
继续阅读 »

如果面试官,或者有人问你foreach怎么跳出循环,请你大声的告诉ta,跳不出!!!!!!!!!!


foreach 跳不出循环


为什么呢?


先看看foreach大体实现。


Array.prototype.customForEach = function (fn) {
for (let i = 0; i < this.length; i++) {
fn.call(this, this[i], i, this)
}
}

list.customForEach((item, i, list) => {
console.log(item, i, list)
})

let list = [1,2,3,4,5]

list.forEach((item,index,list)=>{
console.log(item,index,list)
})

list.customForEach((item,index,list)=>{
console.log(item,index,list)
})




两个输出的结果是一样的没啥问题,这就是foreach的大体实现,既然都知道了它的实现,那么对它为什么跳不出循环♻️应该都知道了,再不清楚的话,再看一下下面的例子。



function demo(){
return 'demo'
}

function demo2(){
demo()
return 'demo2'
}

demo()


在demo2函数里面调用demo函数,demo函数的return能阻止demo2函数下面的执行吗?很明显不行啊,demo函数里的return跟demo2函数一点关系都没有。现在你再回头看看foreach的实现,就明白它跳不出循环一清二楚了。


有点同学说不是可以通过抛出错误跳出循环吗?是的。看看下面例子。



let list = [1,2,3,4,5]

try {
list.forEach((item, index, list) => {
if (index === 2) {
throw new Error('demo')
}
console.log(item)
})
} catch (e) {
// console.log(e)
}




结果是我们想要,但是你看代码,哪个正常人会这样写代码?是非foreach不用吗?还是其他的循环关键字不配呢。


end


有反驳在评论区,show me your code !!!!!!!!!


作者:啥也不懂的前端
来源:juejin.cn/post/7259595485090906149
收起阅读 »

这次被 foreach 坑惨了,再也不敢乱用了...

近日,项目中有一个耗时较长的Job存在CPU占用过高的问题,经排查发现,主要时间消耗在往MyBatis中批量插入数据。mapper configuration是用foreach循环做的,差不多是这样。(由于项目保密,以下代码均为自己手写的demo代码)<...
继续阅读 »

近日,项目中有一个耗时较长的Job存在CPU占用过高的问题,经排查发现,主要时间消耗在往MyBatis中批量插入数据。mapper configuration是用foreach循环做的,差不多是这样。(由于项目保密,以下代码均为自己手写的demo代码)

<insert id="batchInsert" parameterType="java.util.List">  
insert into USER (id, name) values
<foreach collection="list" item="model" index="index" separator=",">
(#{model.id}, #{model.name})
</foreach>
</insert>

这个方法提升批量插入速度的原理是,将传统的:

INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");  
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");

转化为:

INSERT INTO `table1` (`field1`, `field2`)   
VALUES ("data1", "data2"),
("data1", "data2"),
("data1", "data2"),
("data1", "data2"),
("data1", "data2");

在MySql Docs中也提到过这个trick,如果要优化插入速度时,可以将许多小型操作组合到一个大型操作中。理想情况下,这样可以在单个连接中一次性发送许多新行的数据,并将所有索引更新和一致性检查延迟到最后才进行。

乍看上去这个foreach没有问题,但是经过项目实践发现,当表的列数较多(20+),以及一次性插入的行数较多(5000+)时,整个插入的耗时十分漫长,达到了14分钟,这是不能忍的。在资料中也提到了一句话:

Of course don't combine ALL of them, if the amount is HUGE. Say you have 1000 rows you need to insert, then don't do it one at a time. You shouldn't equally try to have all 1000 rows in a single query. Instead break it into smaller sizes.

它强调,当插入数量很多时,不能一次性全放在一条语句里。可是为什么不能放在同一条语句里呢?这条语句为什么会耗时这么久呢?我查阅了资料发现:

Insert inside Mybatis foreach is not batch, this is a single (could become giant) SQL statement and that brings drawbacks:

some database such as Oracle here does not support.

in relevant cases: there will be a large number of records to insert and the database configured limit (by default around 2000 parameters per statement) will be hit, and eventually possibly DB stack error if the statement itself become too large.

Iteration over the collection must not be done in the mybatis XML. Just execute a simple Insertstatement in a Java Foreach loop. The most important thing is the session Executor type.

SqlSession session = sessionFactory.openSession(ExecutorType.BATCH);
for (Model model : list) {
session.insert("insertStatement", model);
}
session.flushStatements();

Unlike default ExecutorType.SIMPLE, the statement will be prepared once and executed for each record to insert.

从资料中可知,默认执行器类型为Simple,会为每个语句创建一个新的预处理语句,也就是创建一个PreparedStatement对象。

在我们的项目中,会不停地使用批量插入这个方法,而因为MyBatis对于含有的语句,无法采用缓存,那么在每次调用方法时,都会重新解析sql语句。

Internally, it still generates the same single insert statement with many placeholders as the JDBC code above.

MyBatis has an ability to cache PreparedStatement, but this statement cannot be cached because it containselement and the statement varies depending on the parameters. As a result, MyBatis has to 1) evaluate the foreach part and 2) parse the statement string to build parameter mapping [1] on every execution of this statement. And these steps are relatively costly process when the statement string is big and contains many placeholders.

[1] simply put, it is a mapping between placeholders and the parameters.

从上述资料可知,耗时就耗在,由于我foreach后有5000+个values,所以这个PreparedStatement特别长,包含了很多占位符,对于占位符和参数的映射尤其耗时。并且,查阅相关资料可知,values的增长与所需的解析时间,是呈指数型增长的。

图片

所以,如果非要使用 foreach 的方式来进行批量插入的话,可以考虑减少一条 insert 语句中 values 的个数,最好能达到上面曲线的最底部的值,使速度最快。一般按经验来说,一次性插20~50行数量是比较合适的,时间消耗也能接受。

重点来了。上面讲的是,如果非要用的方式来插入,可以提升性能的方式。而实际上,MyBatis文档中写批量插入的时候,是推荐使用另外一种方法。(可以看
http://www.mybatis.org/mybatis-dyn… Insert Support 标题里的内容)

SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);  
try {
SimpleTableMapper mapper = session.getMapper(SimpleTableMapper.class);
List<SimpleTableRecord> records = getRecordsToInsert(); // not shown

BatchInsert<SimpleTableRecord> batchInsert = insert(records)
.into(simpleTable)
.map(id).toProperty("id")
.map(firstName).toProperty("firstName")
.map(lastName).toProperty("lastName")
.map(birthDate).toProperty("birthDate")
.map(employed).toProperty("employed")
.map(occupation).toProperty("occupation")
.build()
.render(RenderingStrategy.MYBATIS3);

batchInsert.insertStatements().stream().forEach(mapper::insert);

session.commit();
} finally {
session.close();
}

即基本思想是将 MyBatis session 的 executor type 设为 Batch ,然后多次执行插入语句。就类似于JDBC的下面语句一样。

Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydb?useUnicode=true&characterEncoding=UTF-8&useServerPrepStmts=false&rewriteBatchedStatements=true","root","root");  
connection.setAutoCommit(false);
PreparedStatement ps = connection.prepareStatement(
"insert into tb_user (name) values(?)");
for (int i = 0; i < stuNum; i++) {
ps.setString(1,name);
ps.addBatch();
}
ps.executeBatch();
connection.commit();
connection.close();

经过试验,使用了 ExecutorType.BATCH 的插入方式,性能显著提升,不到 2s 便能全部插入完成。

总结一下

如果MyBatis需要进行批量插入,推荐使用 ExecutorType.BATCH 的插入方式,如果非要使用  的插入的话,需要将每次插入的记录控制在 20~50 左右。


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

项目开发过程中,成员提离职,怎么办?

之前写过一篇《如何应对核心员工提离职》反响特别好,今天做个延展篇,在项目过程中,员工突然提离职,我们有什么办法让项目按时按质的上线。项目做多了,总会碰到这种情况。这里给大家介绍一个解决项目问题的分析方法:从问题本身、环境、问题的主体三个方面去思考解决方案。通常...
继续阅读 »

之前写过一篇《如何应对核心员工提离职》反响特别好,今天做个延展篇,在项目过程中,员工突然提离职,我们有什么办法让项目按时按质的上线。

项目做多了,总会碰到这种情况。这里给大家介绍一个解决项目问题的分析方法:从问题本身、环境、问题的主体三个方面去思考解决方案。

通常情况下,一个员工向上级提出离职,那意味着他已经下决心走了,你留得住人,留不住心。而且这段时间,最好别派太多活,他只想早点交接完早点离开。

我们试着从环境、问题本身、问题主体三个方面来思考解决方案。

环境

  • 从问题发生的环境看,如果我们有一个好的氛围,好的企业文化。员工会不会突然突出离职?或者哪怕提出离职,会不会给我们更多一点时间,在离职期间仍然把事情做好?如果答案是肯定的,那么管理者可以尝试从问题发生的上游解决问题。
  • 提前安排更多的资源来做项目,预防资源不足的情况发生。比如整体预留了20%的开发时间做缓冲,或者整体安排的工作量比规划的多20%。

问题本身

从问题本身思考,员工离职导致的问题是资源不够用。

  • 新增资源,能不能快速找到替代离职员工的人?或者我们能不能使用外包方式完成需求?跟团队商量增加一些工作时间或提高工作效率?
  • 减少需求,少做一些不是很重要的需求,把离职员工的需求分给其他人。

这2个解决方案其实都有一个前提,那就是离职人员的代码是遵循编码规范的,这样接手的人才看得懂。否则,需要增加的资源会比原来规划的多很多。这种问题不能靠员工自觉,而应该要有一套制度来规范编码。

问题的主体

我们不一定能解决问题,但可以解决让问题发生的人。这样问题就不存在了。比如,既然问题出现在张三面前,那就想办法搞定张三,让他愿意按计划把项目完成。如果公司里没人能搞定这个事,这里还有另一个思路,就是想想谁能解决这个问题,找那个能解决问题的人。

从环境、问题本身、问题的主体三个维度来分析,我们得到了好几个解决方案。我们接着分析哪种方案更靠谱。

解决方案分析

方案一,从环境角度分析,让问题不发生。这种成本是最小的。但如果问题已经发生,那这个方案就没用了。

方案二,在项目规划的时候,提前安排更多资源。这招好是好,但前提是你公司有那么多资源。大部分公司都是资源不足。

方案三,新增资源,这个招人不会那么快,就算招进来了,一时半会还发挥不出多大的价值。请外包的话,其实跟招人一样,一时半会还发挥不出多大的价值,成本还更高,也不适合。至于跟团队成员商量提高工作效率或者大家加个班赶上进度,这也是一个解决方案。不过前提是团队还有精力承担这些工作。

方案四,减少需求。这个成本最小,对大部分公司其实也适用。关键是需求管理要做好,对需求的优先级有共识。

方案五,解决让问题发生的人。这个如果不是有大的积怨,也是一个比较好的方案。对整个项目来说,成本也不会很大,项目时间和质量都有保证。

项目管理里有一个生命周期概念,越是在早期发生问题,成本越小。越到后期成本越大。所以,如果让我选,我会选择方案一。但如果已经发生,那只能在四和五里选一个。

实战经验

离职是一场危机管理

让问题不发生,那么解决之道就是不让员工离职。尤其是不让核心骨干员工提离职。离职就是一场危机管理。

这里的本质的是人才是资产,我们在市场上看到很多案例,很多企业的倒闭并不是因为经营问题,而是管理层的大批量流失,资本市场也不看好管理层流失的企业。了解这点,你就能理解为什么人才是资产了。所以对企业来说,核心员工离职不亚于一场危机。

下面分享一个危机管理矩阵,这样有助于我们对危机进行分类。

横轴是一件事情发生之后,危害性有多大,我们分为大、中、小。纵轴就是这件事发生的概率,也可以分为大、中、小。然后就形成了九种不同的类型。

我自己的理解是,有精力的话,上图红色区域是需要重点关注的。如果精力有限,就关注最右边那三种离职后,危害性特别大的员工(不管概率发生的大小)。要知道给企业造成大影响的往往是那些发生概率小的,因为概率大的,你肯定有预防动作,而那些你认为不会离职的员工,突然一天找到你提离职,你连什么准备都没,这种伤害是最大的。

理论上所有岗位都应该准备好”接班人“计划,但实际上很多公司没办法做到。在一些小公司是一个萝卜一个坑,这个岗位人员离职,还得现招。这不合理,但这就是现状。

公司如何管理危机?

好,回到公司身上,公司如何管理危机?

第一,稳住关键性员工,让员工利益和公司利益进行深入绑定。

那些创造利润最大的前10~20%的员工,就应该获得50%甚至更高的收益。当然除了金钱上的激励外,还要有精神上的激励,给他目标,让他有成就感等等。

第二,有意识地培养关键岗位的接班人或者助理。

比如通过激励鼓励他们带新人、轮岗等等

第三,人员的危机管理是动态变化的,要时不时地明确团队各成员的位置。

比如大公司每年都会做人才盘点。

第四,当危机真的出现后,要有应对方案。

也就是把危机控制在可承受的范围内。比如,项目管理中的planB方案,真遇到资源不够,时间不够的情况下,我们能不能放弃一些不重要的需求?亦或者能不能先用相对简单但可用的方案?

离职管理的核心是:降低离职发生的概率和降低离职造成危害的大小。

离职沟通

如果事情已经发生了,管理者应该先通过离职沟通,释放自己的善意。我会按照如下情况跟离职员工沟通

第一,先做离职沟通,了解对方为什么离职?还有没有留下来的可能,作为管理者有什么能帮他做的?

第二,确定走的话,确认下对方期望的离职时间,然后根据公司情况,协商一个双方都能接受的离职时间点。不要因为没有交接人,就不给明确时间。

第三,征求对方意见,是否需要公布离职。然后一起商量这段时间的工作安排。比如,你会坦诚告知会减少工作量,但哪些工作是需要他继续支持的。希望他能一如既往地高效完成工作。

第四,如果还没有交接人到岗,最好在一周内安排人员到岗,可以考虑内部换岗,内招、猎聘等手段尽快让人员到岗。

第五,如果已经到离职时间,但还没有交接人,作为公司管理者,你就是最好的交接人。在正式交接工作之前,要理清楚需要哪些相关的资料,做好文档分类。如果实在对离职员工的工作不了解,可以让离职人员写一封日常工作的总结。

如果做完这些,离职员工还是消极怠工。作为管理者能做得就比较有限,可以尝试以下几个方法

1、再进行一次沟通。表明现在公司的情况,希望他给予支持。

2、看看自己能给予对方哪些帮助,先把这些落实好。比如写推荐信。另外有些公司入职的时候会做背景调查,这也是你能够帮助到他的。

3、如果你有权利,可以跟离职员工商量是否可以以兼职的方式来完成后续工作。这种方式对大家都好,他可以早点离职,你也不用担心因为时间仓促招错人。

如果做完以上这些还不行,那么就考虑减少一些需求,用更简单的方案先用着,后期做迭代。至于说让团队加班加点赶进度,这个要根据项目实际情况来定。

总结:今天给大家分享了一个简单分析问题的方法。然后重点聊了一下项目成员突然要离职,项目负责人有哪些应对方案。如果你看完有收获,欢迎留言讨论。


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

程序员提高效率的办法

最重要的-利用好工具 🔧工欲善其事必先利其器,利用好的知识外脑来帮助自己,学会使用AI大模型 比如Chagpt等;http://www.dooocs.com/chatgpt/REA…1. 早上不要开会 📅每个人一天是 24 小时,时间是均等的,但是时...
继续阅读 »

最重要的-利用好工具 🔧

工欲善其事必先利其器,利用好的知识外脑来帮助自己,学会使用AI大模型 比如Chagpt等;http://www.dooocs.com/chatgpt/REA…

1. 早上不要开会 📅

每个人一天是 24 小时,时间是均等的,但是时间的价值却不是均等的,早上 1 小时的价值是晚上的 4 倍。为什么这么说?

因为早晨是大脑的黄金时间,经过一晚上的睡眠,大脑经过整理、记录、休息,此时的状态是最饱满的,适合专注度高的工作,比如编程、学习外语等,如果把时间浪费在开会、刷手机等低专注度的事情上,那么就会白白浪费早上的价值。

2. 不要使用番茄钟 🍅

有时候在专心编程的时候,会产生“心流”,心流是一种高度专注的状态,当我们专注的状态被打破的时候,需要 15 分钟的时候才能重新进入状态。

有很多人推荐番茄钟工作法,设定 25 分钟倒计时,强制休息 5 分钟,之后再进入下一个番茄钟。本人在使用实际使用这种方法的时候,经常遇到的问题就是刚刚进入“心流”的专注状态,但番茄钟却响了,打破了专注,再次进入这种专注状态需要花费 15 分钟的时间。

好的替换方法是使用秒表,它跟番茄钟一样,把时间可视化,但却是正向计时,不会打破我们的“心流”,当我们编程专注度下降的时候中去查看秒表,确定自己的休息时间。

3. 休息时间不要玩手机 📱

大脑处理视觉信息需要动用 90% 的机能,并且闪烁的屏幕也会让大脑兴奋,这就是为什么明明休息了,但是重新回到工作的时候却还是感觉很疲惫的原因。

那么对于休息时间内,我们应该阻断视觉信息的输入,推荐:

  • 闭目养神 😪
  • 听音乐 🎶
  • 在办公室走动走动 🏃‍♂️
  • 和同事聊会天 💑
  • 扭扭脖子活动活动 💁‍♂️
  • 冥想 or 正念 🧘

4. 不要在工位上吃午饭 🥣

大脑经过一早上的编程劳累运转之后,此时的专注度已经下降 40%~50%,这个时候我们需要去重启我们的专注度,一个好的方法是外出就餐,外出就餐的好处有:

  • 促进血清素分泌:我们体内有一种叫做血清素的神经递质,它控制着我们的睡眠和清醒,外出就餐可以恢复我们的血清素,让我们整个人神经气爽:

    • 日光浴:外出的时候晒太阳可以促进血清素的分泌
    • 有节奏的运动:走路是一种有节奏的运动,同样可以促进血清素分泌
  • 激发场所神经元活性:场所神经元是掌控场所、空间的神经细胞,它存在于海马体中,外出就餐时场所的变化可以激发场所神经元的活性,进而促进海马体活跃,提高我们的记忆力

  • 激活乙酰胆碱:如果外出就餐去到新的餐馆、街道,尝试新的事物的话,可以激活我们体内的乙酰胆碱,它对于我们的“创作”和“灵感”起到非常大的作用。

5. 睡午觉 😴

现在科学已经研究表现,睡午觉是非常重要的一件事情,它可以:

  • 恢复我们的身体状态:26 分钟的午睡,可以让下午的工作效率提升 34%,专注力提升 54%。
  • 延长寿命:中午不睡午觉的人比中午睡午觉的人更容易扑街
  • 预防疾病:降低老年痴呆、癌症、心血管疾病、肥胖症、糖尿病、抑郁症等

睡午觉好处多多,但也要适当,15 分钟到 30 分钟的睡眠最佳,超过的话反而有害。

6. 下午上班前运动一下 🚴

下午 2 点到 4 点是人清醒度最低的时候,10 分钟的运动可以让我们的身体重新清醒,提高专注度,程序员的工作岗位和场所如果有限,推荐:

  • 1️⃣ 深蹲
  • 2️⃣ 俯卧撑
  • 3️⃣ 胯下击掌
  • 4️⃣ 爬楼梯(不要下楼梯,下楼梯比较伤膝盖,可以向上爬到顶楼,再坐电梯下来)

7. 2 分钟解决和 30 秒决断 🖖

⚒️ 2 分钟解决是指遇到在 2 分钟内可以完成的事情,我们趁热打铁把它完成。这是一个解决拖延的小技巧,作为一个程序员,经常会遇到各种各样的突发问题,对于一些问题,我们没办法很好的决策要不要立即完成,2 分钟解决就是一个很好的辅助决策的办法。

💣 30 秒决断是指对于日常的事情,我们只需要用 30 秒去做决策就好了,这源于一个“快棋理论”,研究人员让一个著名棋手去观察一盘棋局,然后分别给他 30 秒和 1 小时去决定下一步,最后发现 30 秒和 1 小时做出的决定中,有 90% 都是一致的。

8. 不要加班,充足睡眠 💤

作为程序员,我们可能经常加班到 9 点,到了宿舍就 10 点半,洗漱上床就 12 点了,再玩会儿手机就可以到凌晨 2 、3 点。

压缩睡眠时间,大脑就得不到有效的休息,第二天的专注度就会降低,工作效率也会降低,这就是一个恶性循环。

想想我们在白天工作的时候,其实有很多时间都是被无效浪费的,如果我们给自己强制设定下班时间,创新、改变工作方式,高效率、高质量、高密度的完成工作,那是否就可以减少加班,让我们有更多的自由时间去学习新的知识技术,进而又提高我们的工作效率,形成一个正向循环。

9. 睡前 2 小时 🛌

  1. 睡前两小时不能做的事情:

    • 🍲 吃东西:空腹的时候会促进生长激素,生长激素可以提高血糖,消除疲劳,但如果吃东西把血糖提高了,这时候生长激素就停止分泌了
    • 🥃 喝酒
    • ⛹️ 剧烈运动
    • 💦 洗澡水过高
    • 🎮 视觉娱乐(打游戏,看电影等)
    • 📺 闪亮的东西(看手机,看电脑,看电视)
    • 💡 在灯光过于明亮的地方
  2. 适合做的事情

    • 📖 读书
    • 🎶 听音乐
    • 🎨 非视觉娱乐
    • 🧘‍♂️ 使身体放松的轻微运动

10. 周末不用刻意补觉 🚫

很多人以周为单位进行休息,周一到周五压缩睡眠,周末再补觉,周六日一觉睡到下午 12 点,但这与工作日的睡眠节奏相冲突,造成的后果就是星期一的早上起床感的特别的厌倦、焦躁。

其实周末并不需要补觉,人体有一个以天为单位的生物钟,打破当前的生物钟周期,就会影响到下一个生物钟周期,要调节回来也需要花费一定时间。

我们应该要以天为单位进行休息,早睡早起,保持每天的专注度。

参考

以上大部分来源于书籍 《为什么精英都是时间控》,作者桦泽紫苑;


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

如何诊断Java 应用线程泄漏

大家经常听到内存泄漏, 那么线程泄漏是指什么呢?线程泄漏是指 JVM 里面的线程越来越多, 而这些新创建的线程在初期被使用之后, 再也不被使用了, 然而也没有被销毁. 通常是由于错误的代码导致的这类问题.一般通过监控 Java 应用的线程数量的相关指标, 都能...
继续阅读 »

大家经常听到内存泄漏, 那么线程泄漏是指什么呢?

线程泄漏是指 JVM 里面的线程越来越多, 而这些新创建的线程在初期被使用之后, 再也不被使用了, 然而也没有被销毁. 通常是由于错误的代码导致的这类问题.

一般通过监控 Java 应用的线程数量的相关指标, 都能发现这种问题. 如果没有很好的对这些指标的监控措施, 或者没有设置报警信息, 可能要到等到线程耗尽操作系统内存导致OOM才能暴露出来.

最常见的例子

在生产环境中, 见过很多次类似下面例子:

public void handleRequest(List<String> requestPayload) {
if (requestPayload.size() > 0) {
ExecutorService executor = Executors.newFixedThreadPool(2);

for (String str : requestPayload) {
final String s = str;
executor.submit(new Runnable() {
@Override
public void run() {
// print 模拟做很多事情
System.out.println(s);
}
});
}
}
// do some other things
}

这段代码在处理一个业务请求, 业务请求中包含很多小的任务, 于是想到使用线程池去处理每个小任务, 于是创建了一个 ExecutorService, 接着去处理小任务去了.

错误及改正

看到这段代码, 大家会觉的不可能啊, 怎么会有人这么使用线程池呢? 线程池不是这么用的啊? 一脸问号. 可是现实情况是: 总有新手写出这样的代码.

有的新手被指出这个问题之后, 就去查文档, 发现 ExecutorService 有 shutdown() 和 shutdownNow() 方法啊, 于是就在 for 循环后边加了 executor.shutdown(). 当然, 这会解决线程泄漏的问题. 但却不是线程池正确的用法, 因为这样虽然避免了线程泄漏, 却还是每次都要创建线程池, 创建新线程, 并没有提升性能.

正确的使用方法是做一个全局的线程池, 而不是一个局部变量的线程池, 然后在应用退出前通过 hook 的方式 shutdown 线程池.

然而, 我们是在知道这段代码位置的前提下, 很快就修好了. 如果你有一个复杂的 Java 应用, 它的线程不断的增加, 我们怎么才能找到导致线程泄漏的代码块呢?

情景再现

通常情况下, 我们会有每个应用的线程数量的指标, 如果某个应用的线程数量启动后, 不管分配的 CPU 个数, 一直保持上升趋势, 那么就危险了. 这个时候, 我们就会去查看线程的 Thread dump, 去查看到底哪些线程在持续的增加, 为什么这些线程会不断创建, 创建新线程的代码在哪?

找到出问题的代码

在 Thread dump 里面, 都有线程创建的顺序, 还有线程的名字. 如果新创建的线程都有一个自己定义的名字, 那么就很容易的找到创建的地方了, 我们可以根据这些名字去查找出问题的代码.

根据线程名去搜代码

比如下面创建的线程的方式, 就给了每个线程统一的名字:

Thread t = new Thread(new Runnable() {
@Override
public void run() {
}
}, "ProcessingTaskThread");
t.setDaemon(true);
t.start();

如果这些线程启动之前不设置名字, 系统都会分配一个统一的名字, 比如thread-npool-m-thread-n, 这个时候通过名字就很难去找到出错的代码.

根据线程处理的业务逻辑去查代码

大多数时候, 这些线程在 Thread dump 里都表现为没有任何事情可做, 但有些时候, 你可以能发现这些新创建的线程还在处理某些业务逻辑, 这时候, 根据这些业务逻辑的代码向上查找创建线程的代码, 也不失为一种策略.

比如下面的线程栈里可以看出这个线程池在处理我们的业务逻辑代码 AsyncPropertyChangeSupport.run, 然后根据这个关键信息, 我们就可以查找出到底那个地方创建了这个线程:

"pool-2-thread-4" #159 prio=5 os_prio=0 cpu=7.99ms elapsed=354359.32s tid=0x00007f559c6c9000 nid=0x6eb in Object.wait()  [0x00007f55a010a000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(java.base@11.0.18/Native Method)
- waiting on <0x00000007c5320a88> (a java.lang.ProcessImpl)
at java.lang.Object.wait(java.base@11.0.18/Object.java:328)
... 省略 ...
at com.tianxiaohui.JvmConfigBean.propertyChange(JvmConfigBean.java:180)
at com.tianxiaohui.AsyncPropertyChangeSupport.run(AsyncPropertyChangeSupport.java:346)
at java.util.concurrent.Executors$RunnableAdapter.call(java.base@11.0.18/Executors.java:515)
at java.util.concurrent.FutureTask.run(java.base@11.0.18/FutureTask.java:264)
at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@11.0.18/ThreadPoolExecutor.java:1128)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@11.0.18/ThreadPoolExecutor.java:628)
at java.lang.Thread.run(java.base@11.0.18/Thread.java:829)

使用 btrace 查找创建线程的代码

在上面2种比较容易的方法已经失效的时候, 还有一种一定能查找到问题代码的方式, 就是使用 btrace 注入拦截代码: 拦截创建新线程的地方, 然后打印当时的线程栈.

我们稍微改下官方的拦截启动新线程的例子, 加入打印当前栈信息:

import org.openjdk.btrace.core.annotations.BTrace;
import org.openjdk.btrace.core.annotations.OnMethod;
import org.openjdk.btrace.core.annotations.Self;

import static org.openjdk.btrace.core.BTraceUtils.*;

@BTrace
public class ThreadStart {
@OnMethod(
clazz = "java.lang.Thread",
method = "start"
)
public static void onnewThread(@Self Thread t) {
D.probe("jthreadstart", Threads.name(t));
println("starting " + Threads.name(t));
println(jstackStr());
}
}

然后执行 btrace 注入, 一旦有新线程被创建, 我们就能找到创建新线程的代码, 当然, 我们可能拦截到不是我们想要的线程创建栈, 所以要区分, 哪些才是我们希望找到的, 有时候, 上面的代码中可以加一个判断, 比如线程名字是不是符合我们要找的模式.

$ ./bin/btrace 1036 ThreadStart.java
Attaching BTrace to PID: 1036
starting HandshakeCompletedNotify-Thread
java.base/java.lang.Thread.start(Thread.java)
java.base/sun.security.ssl.TransportContext.finishHandshake(TransportContext.java:632)
java.base/sun.security.ssl.Finished$T12FinishedConsumer.onConsumeFinished(Finished.java:558)
java.base/sun.security.ssl.Finished$T12FinishedConsumer.consume(Finished.java:525)
java.base/sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:392)

上面的代码, 就抓住了一个新创建的线程的地方, 只不过这个可能不是我们想要的.

除了线程会泄漏之外, 线程组(ThreadGroup) 也有可能泄漏, 导致内存被用光, 感兴趣的可以查看生产环境出现的一个真实的问题: 为啥 java.lang.ThreadGroup 把内存干爆了

总结

针对线程泄漏的问题, 诊断的过程还算简单, 基本过程如下:

  1. 先确定是哪些线程在持续不断的增加;
  2. 然后再找出创建这些线程的错误代码;
    1. 根据线程名字去搜错误代码位置;
    2. 根据线程处理的业务逻辑代码去查找错误代码位置;
    3. 使用 btrace 拦截创建新线程的代码位置

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

从零开始搭建个人网站

伴随着颈椎疼痛的困扰,此时的我不敢轻易扭动脖子,宛如一只梗着脖子的傻猫。好在个人博客网站基本搭建完毕,尽管下一个任务紧迫,但它没有任何盈利点。现在,只需简单总结一下,就能稍微松口气了。个人博客网站功能设计炫酷展示:独特页面展示博客列表,包括标题和发布日期,可通...
继续阅读 »

伴随着颈椎疼痛的困扰,此时的我不敢轻易扭动脖子,宛如一只梗着脖子的傻猫。好在个人博客网站基本搭建完毕,尽管下一个任务紧迫,但它没有任何盈利点。现在,只需简单总结一下,就能稍微松口气了。

个人博客网站功能设计

炫酷展示:独特页面展示博客列表,包括标题和发布日期,可通过点击链接阅读完整内容。

分类与标签:文章分类和标签,助力读者按主题或关键词快速筛选和浏览感兴趣的内容。

神奇搜索:强大而快捷的模糊搜索功能,让读者迅速找到心仪文章,关键词轻松搜。

探秘联系:Github的联系方式隐藏在神秘角落,前往解锁更多关于开发者的信息。

响应式布局:兼容各设备,个人主页灵活展示在桌面、平板和手机,畅享极致用户体验。

数据剖析:揭示访问统计与分析报告,文章点击、来源追踪,读者行为一览无余。数据助力个人优化内容、领略读者趣味。

主题风格:提供浅色模式和暗黑模式,页面可一键换装或跟随系统。

为什么是Next.js和vercel?

首先是这样最简单,从基友的劳动成果中扒拉了整个技术架构。在此基础上,我又做了一些页面的功能的扩展,比如全文模糊搜索、继续阅读、文章目录等。【叉腰】

服务器渲染(SSR)可以给博客带来超棒的性能和超棒的SEO优化。它会让页面加载速度变得超快,还能让搜索引擎更好地抓取和索引博客内容。(但现在还不能搜到我的文章ε=(´ο`*))))

Vercel作为部署平台。它简单易用,超快速而且靠谱。最重要的是,它能跟Next.js完美结合,让我轻轻松松地将我的应用程序部署到全球分布式网络上。它还提供自动化的CI/CD流程,让我可以专注于撰写博客内容,而不用花费太多时间和精力在繁琐的部署和服务器配置上。

赞美基友!

UI设计的灵感来源

页面布局

首页的布局模仿nextjs框架的样本页,它本身拥有响应式布局。

首页的内容并不需要太多,所以保留了样本页的单屏设计,页面不需要滚动就能展示完整的内容。页面分为三个模块:顶部为面包屑按钮区,中间区域面积最大,突出博主猫奴本色,底部则包含了三个菜单,分别作为专栏文章和其他项目的链接入口。

对于专栏文章列表页的布局,首先需要考虑两个主要模块:专栏标题和描述,文章列表的标题和发布时间。在PC端,按2:3比例左右分栏,移动端按2:8比例上下分栏。顶部和首页类似有漂亮的手绘按钮点缀页面,还有一只正在认真工作的猫猫作为背景,增添趣味。

文章内容页的布局设计采用主流的设计,基本上在每个教程文档都能看见这样的布局方式。布局是类似的,但怎样才能做得好看点呢?如何用颜色+透明度、字号+行高的奇妙组合创造美的魔术?对于个人网站,这很大程度依赖于个人的审美喜好,以及最重要的、很容易被个人开发者忽略的一点:考虑读者的阅读体验。

关于颜色

浅色模式的颜色来源于我的=月白色的瓷碗,和瓷碗里的黄米汤圆。降低一点饱和度,调成目前主流的莫兰迪色系,给人一种温柔的感觉。我喜欢性格温和的人,希望自己也变得温柔一些。

深色模式中,很多网站都呈现出超棒的视觉效果,我个人最喜欢tailwind的配色。这种模式下的背景傻猫露出了其暗黑的一面,似乎正在谋划消灭人类,重建一个有吃不完的小鱼干和虾虾的新世界。

图标和图片

小图标采用阿里iconfont的手绘风格系列,给页面增添了一抹生动与活泼。

背景图由网图和两个手绘Icon图标巧妙拼合而成,似乎在讲述一个工作猫的故事。在图中,你可以看到一只憨憨的小猫专注地盯着电脑屏幕,右爪按住鼠标,而鼠标线的末端却连接着一个毛线团,给人一种不太聪明的印象。

问题和解决方案

  1. 浅色和深色模式转换的解决方案,哪个最方便快捷?

项目使用的组件库mantine提供了MantineProvider组件,可用于更改主题。

import { MantineProvider } from '@mantine/core';

function Demo() {
return (
<MantineProvider theme={{ fontFamily: 'Open Sans' }} withGlobalStyles>
<App />
</MantineProvider>
);
}

theme属性可用来传递其它任意风格属性。withGlobalStyles 这个属性可以增加几个样式,其中一个就是深色模式的颜色和浅色模式的颜色。

但我并没有使用这个方案,原因有三:

杀鸡用牛刀既视感,theme提供了很多选项默认值,但除了颜色,其它用不上。
主题色色彩不够多,如果想要更多颜色,需要修改它的css变量。而tailwind的颜色选择范围更多,添加透明度也很方便。
相比于在js中修改样式,我更喜欢在css中完成同样的功能。

比如,使用tailwind,仅需在某个className加上前缀 dark:,比如dark:bg-slate-900, 表示bg-slate-900在深色模式下生效。具体如何使用可以参考tailwind官网,这里不做赘述。

  1. 老大难useEffect。

当需要使用useEffect监听事件时,有时候只想在组件 mounted 前执行某些操作,而不希望随着状态的变化而不停地更新。对于那个状态,我会创建一个ref值,就像是一块“不会变形的金属”。这样,我可以在useEffect中使用这个ref值作为判断依据,而不会受到状态的干扰。

然而,还有一种情况无法绕开。由Context上下文提供的value值 和 updateValue方法,updateValue需要在某个子组件的useEffect中监听事件而触发。

  1. Tailwind SVG 样式未生效

tailwind支持处理SVG图标的样式,比如你可以这样写一个svg的颜色:

<svg class="fill-blue-500 ..."> <!-- ... --> </svg>

<svg class="stroke-blue-500 ..."> <!-- ... --> </svg>

可是,对于我下载的那些手绘图标,上述方法却不起作用,包括stroke属性也无效。我猜测可能与图标的path路径有关。脑海中浮现出一个解决方案:使用全局provider来传递主题色作为参数,并将其赋值给fill属性。但是,我实在太懒了,不想采用那么繁琐的方式,也不想依赖市面上已有的状态管理方案。

fill属性接受的值是颜色,同样可以使用currentColor,它会继承最近祖先元素的颜色,类似于inherit的效果。当fill属性不存在或为空字符串时,默认会被填充为黑色。然而,这些手绘图标似乎并不理会tailwind针对svg的className属性,反而继承了更上一级的颜色。既然如此,那解决方案就是在引用这些图标的组件中指定颜色。

  1. sticky不粘了。 通常,我们知道,英文文档的中文翻译总有一些令人困惑的小问题。但这一次,是mdn的中文文档略胜一筹。例如,在stick相关文档中:

This value always creates a new stacking context. Note that a sticky element "sticks" to its nearest ancestor that has a "scrolling mechanism" (created when overflow is hiddenscrollauto, or overlay), even if that ancestor isn't the nearest actually scrolling ancestor.

注意,一个 sticky 元素会“固定”在离它最近的一个拥有“滚动机制”的祖先上(当该祖先的 overflow 是 hiddenscrollauto 或 overlay 时),即便这个祖先不是最近的真实可滚动祖先。

中文文档明确多了这一句话:

这有效地抑制了任何“sticky”行为(详情见 Github issue on W3C CSSWG)。

如果不看这句话,中英文文档都会让人以为是要在overflow有这些值的时候才生效。然而事实却完全相反。在我去掉藏在 body 中的overflow-x:hidden后, sticky终于能按预期触发了。

  1. 模糊查询算法和匹配文本标记

前文提到,贴心而又温柔的笔者为网站提供了搜索功能。搜索功能的界面和常见开发文档的Spotlight看起来基本上一样,不同的是细节的模糊查询功能。

一个具备模糊查询的Spotlight看起来像这样:

(好吧,截图的时候又发现自己漏了个细节,搜索文章内容节选的前后省略号。烦)

关于模糊查询算法,可以看这里探秘Fuse.js:模糊查询算法的学习笔记,这里不做赘述。

而匹配文本颜色标记,这个功能需要自己来实现,大致需求是,找出最佳匹配的文章内容节选展示在Spotlight的搜索结果列表里。解析md文档,去掉那些不想要的md文档标记符号,再循环找出最佳匹配区间作为节选内容,节选内容不超过30个字。

以上。


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

什么是设计思维?

你有没有想过事情是否可以采取不同的方式, 但又不确定如何做?那么,不要爱上解决方案,而是爱上问题!这是一篇深入探讨设计思维如何做到这一点的文章。在快节奏的行动和反应的世界中,高质量的结果是我们所有人的目标。但是你怎样才能做得更好呢?无论是产品、服务还是流程,我...
继续阅读 »

你有没有想过事情是否可以采取不同的方式, 但又不确定如何做?那么,不要爱上解决方案,而是爱上问题!这是一篇深入探讨设计思维如何做到这一点的文章。

在快节奏的行动和反应的世界中,高质量的结果是我们所有人的目标。但是你怎样才能做得更好呢?无论是产品、服务还是流程,我们的目标都是让事情变得更好。它让我们想知道我们将如何设计一个漫长而公平的世界以达到一定程度的卓越。

当我们谈论让“事情变得更好”时,我们不能否认看板系统在丰田实施 TQM(全面质量管理)中的作用。如果TQM为制造业做到了这一点,那么设计思维就有可能为创新带来以人为本的解决方案。

设计思维采用以人为本的方法,以便在我们跳入所有可能的解决方案之前了解如何处理问题。我们可以看到它在各个地方的使用。嗡嗡声无处不在,从社会部门到政策制定,从医疗保健到商业。那么什么是设计思维呢?

设计思维的意义 

设计思维促进以创造性的方式解决复杂问题,这种方式优先考虑人类的需求,并着重于寻找技术上可行的创造性解决方案。

虽然很难用几个词来定义“设计思维”,但我宁愿将设计思维视为一种哲学或一种思维方式,以解决难以用传统和标准方法解决的复杂问题解决问题的实践。设计思维采取的途径是提供以下解决方案:可行的、可行的和可取的。

一般来说,问题的解决方案有时会被传统的解决方法所忽视,而有些方法是高度理性和分析性的,而另一些则是情绪化的。设计思维可能只是增加人类问题的理性、情感和功能需求的第三种方式。设计思维不仅限于建筑产品;任何促进创新的新举措都可以利用解决问题的设计思维原则。

起源 

虽然没有确凿的证据表明设计思维的起源,但设计思维作为一种思维方式可以追溯到John E. Arnold,他是斯坦福大学“创意工程”研究的先驱,机械工程教授。他是最早撰写有关设计思维的少数人之一,并将设计思维的种子视为一种运动。他的讲座激发了更多的想象力和创新性。他的问题解决理论通过将问题的个人、科学和实践方面联系起来,着重于人类需求。

他强调了像艺术家一样处理问题并将人类作为想要构建的解决方案的基石的重要性。“创意问题”没有一个正确答案。

“工程师可以承担艺术家的某些方面,并通过美化或改善产品或机器的外观,或者通过对市场以及人们想要或不想要的东西的种类具有更敏锐的敏感性来尝试改善或增加产品或机器的适销性'想要。— 约翰·E·阿诺德*

设计思维过程 

在对设计思维进行情境化和应用的努力中,它通常被认为是一个过程,该过程可以指导价值观来控制如何处理问题。这个过程可能不一定是线性的或顺序的,而是一个对特定问题或用例有意义的循环。

设计思维过程

EMPATHIZE - 移情 

为人类设计可能很棘手。有时,需求或愿望未被发现,可能无法完全反映真正的问题。传统的市场研究过程是根据事实进行的,而设计思维方法是通过同理心来解决问题。同理心试图理解潜在需求并转化环境的当前现实。这有助于解决方案设计人员了解可以解释问题的人员、他们的行为和上下文,从而构建更好的解决方案。

因此,从问题中获得灵感的第一步是了解设计的对象以及他们寻求解决方案的动机。这对于企业了解可用的机会空间尤其有用。就像苹果所做的那样。虽然 MP3 播放器已经成为一种东西,但iPod 改变了人们消费音乐的方式。当索尼凭借随身听和 CD 统治消费电子市场时,苹果公司凭借 iPod彻底改变了音乐世界。Apple 对人们随身携带盒式磁带的问题深表同情,而 iPod 改变了游戏规则!

为了识别和理解问题的脉搏并结合这一步的背景,收集信息是关键。这是与问题空间中的人交谈以了解他们关心什么以及他们目前如何处理问题的地方。用户访谈和他们的反馈可以帮助了解他们的情况。

DEFINE - 定义 

这是形成问题的最重要步骤之一。

在设计思维中,构思不当的问题陈述为构建“问题”而非“问题”的解决方案铺平了道路。

例如,与某人谈论一个问题。记录下观察结果,并以合理的解决方案来解决该问题。用户对解决方案可能会解决该问题感到兴奋。但这里真正发生的是你和那个人一直在讨论他们许多其他问题中的一个问题。因此,他们是否决定采用你的解决方案取决于此人对你承诺解决的问题的重视程度。

在这里,定义问题的脉搏变得非常重要。 通常,事后看来,焦点会落在你试图解决的问题上,而不是这个人可能实际遇到的许多其他问题。

因此,定义问题对引导如何着手构建以人为本的解决方案大有帮助。

IDEATE - 构思 

在此阶段,观察结果会找到归宿,加以综合以创造改变的机会。集思广益来定义和重新定义潜在的解决方案,以创建解决问题的竞争想法。理想情况下,这一步可以帮助找到问题的核心。

如前所述,这不会是一个线性过程,可能经常会发现自己在共同努力挑战想法或问题本身时会回到之前的步骤。因此,可以挑选出好的想法来实施。

PROTOTYPE - 原型 

下一阶段是原型,可以通过创建最终解决方案的模型来验证想法。解决方案采用有形的形式来展示原型实施阶段的证据。它还展示了在构思阶段未说明的想法的限制和局限性。

Uber 是成功构建出色原型的经典公司之一。Uber 在最初发布时专注于解决“找出租车”的核心问题。该产品的第一个测试版是一个非常简约的应用程序,所有订单都是手动管理的,CEO 可以联系司机预订行程。而且它没有付费功能。目标是测试和验证叫车问题,这是该应用程序的核心优势。最终,当他们了解了目标市场和痛点后,他们开始改进其他功能。

TEST - 测试 

然后将最终原型与目标群体进行测试,并反复进行以适应学习方式。验证在这里采用最终形式,并且可能再次要求重新访问之前的一些步骤以大规模实施该计划。

行动中的设计思维 - 案例研究 

  1. 社会部门的设计思维 

疟疾是非洲最令人不安的问题之一,也是5 岁以下儿童死亡的前 5 大原因之一。这是一个本质上非常复杂的社会问题,分发蚊帐的设计思维方法有助于有效地对抗这种疾病。世界卫生组织报告称,埃塞俄比亚和卢旺达等国家的死亡率下降了 50%-60% ,加纳下降了 34%。

结果发现,蚊帐的设计对加纳的一些人没有吸引力。一组研究人员确定了一个潜在的解决方案来解决让人们使用网络的问题。他们提出了一种以人为本的蚊帐设计,为长期解决这一社会问题铺平了道路。

以人为本的蚊帐设计方法。( 大预览 

  1. 爱彼迎 

设计思维在改造一家几近失败的公司——Airbnb 方面也发挥了巨大的作用。他们的业务正在瘫痪,当他们遇到问题时,他们发现广告中的图片效果不佳。展出的照片是用劣质手机拍摄的。当看到网站上的照片时,想租房的人觉得他们没有看到他们实际支付的是什么。创始人一意识到这一点,就租了一台相机,为客户的财产拍下好照片。创始人之一杰比亚 (Gebbia)继续解释设计学校的经历如何帮助他们重塑自我,更好地为客户服务。

  1. 网飞 

另一个家喻户晓的品牌 Netflix 已经取得了长足的进步,设计思维在他们做出的决策中发挥了重要作用。Netflix 推出了直接送货上门的 DVD 租赁服务,而其他竞争对手则让人们开车穿过商店挑选电影。

后来当有线电视开始提供点播电影时,Netflix 了解了客户的痛点,开始按月提供在线流媒体服务,而无需为每张 DVD 付费。他们建立了一个订阅模式的在线目录,让客户在家中舒适地观看他们最喜欢的电影的便利性使他们感到高兴。

当选择要观看的内容比实际观看要花费更长的时间时,Netflix 想出了“预览”来帮助人们选择要观看的内容。听起来很简单,但 Netflix推荐系统背后的想法有助于减少人们花在决定观看内容上的时间。虽然变化是不可避免的,但 Netflix 不断通过设计思维方法重塑自己,以发现以最终用户为中心的创新解决方案。

从租用 DVD 到在线流媒体,Netflix 一直致力于了解最终用户,以以人为本的方式设计解决方案。

不仅如此。清单不胜枚举。

最后的想法 

设计思维一直在不断发展。人们一直在为使其在各个领域的使用情境化更有用做出贡献。根据问题的大小和复杂性,应用设计思维框架并着手创建以人为本的解决方案具有不同的形式。

考虑到它的灵活性,设计思维可以帮助熟悉并适应歧义。该方法可以在各种规模上动态地进行游戏,使其成为一项有价值的追求。

从想要解决的任何问题开始尝试,并在下面的评论中告诉我它是如何工作的。如果你愿意,我很乐意倾听并提供帮助。

相关资源 


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

技术人创业是怎么被自己短板KO的

这几天搜索我的聊天记录,无意中看到了一个两年前的聊天消息。那是和我讨论出海业务的独立开发网友,后来又去创业的业界精英。顺手一搜,当初他在我的一个开发者群也是高度活跃的人群之一,还经常私下给我提建议,人能力很强,做出海矩阵方向的项目做的很拿手。但是在后来忽然就没...
继续阅读 »

这几天搜索我的聊天记录,无意中看到了一个两年前的聊天消息。那是和我讨论出海业务的独立开发网友,后来又去创业的业界精英。顺手一搜,当初他在我的一个开发者群也是高度活跃的人群之一,还经常私下给我提建议,人能力很强,做出海矩阵方向的项目做的很拿手。但是在后来忽然就没消息了,虽然人还留在群里。

我好奇点开了他的朋友圈,才知道他已经不做独立开发了,而且也(暂时)不在 IT 圈里玩了,去帮亲戚家的服装批发业务打打下手,说是下手,应该也是二当家级别了,钱不少,也相对安稳。朋友圈的画风以前是IT行业动态,出海资讯现在是销售文案和二维码。

和他私下聊了几句,他跟我说他现在过的也还好,人生路还长着呢,谈起了自己在现在这行做事情的经历,碎碎念说了不少有趣的事情,最后还和我感慨说:“转行后感觉脑子灵活了很多”,我说那你写程序的时候脑子不灵活吗,他发了个尴尬而不失礼貌的表情,“我以前技术搞多了,有时候死脑筋。”

这种话我没少听过,但是从一个认识(虽然是网友)而且大跨度转行的朋友这里说出来,就显得特别有说服力。尤其了解了他的经历后,想写篇文章唠叨下关于程序员短板的问题,还有这种短板不去补强,会怎么一步步让路越走越窄的。

现在离职(或者被离职)的程序员越来越多了,程序员群体,尤其是客户端程序员这个群体,只要能力过得去,都有全栈化和业务全面化的潜力。尤其是客户端程序员,就算是在公司上班时,业余时间写写个人项目,发到网上,每个月赚个四到五位数的副业收入也是可以的。

再加上在公司里遇到的各种各样的窝囊事,受了无数次“煞笔领导”的窝囊气,这会让一些程序员产生一种想法,我要不是业余时间不够,不然全职做个项目不就起飞了?

知道缺陷在哪儿,才能扬长避短,所以我想复盘一下,程序员创业,在主观问题上存在哪些短板。(因为说的是总体情况,也请别对号入座)

第一,认死理。

和代码,协议,文档打交道多了,不管自己情愿不情愿,人多多少少就有很强的“契约概念”,代码的世界条理清晰,因果分明,1就是1,0就是0,在这样的世界里呆多了,你要说思维方式不被改变,那是不可能的 --- 而且总的来说,这种塑造其实是好事情。要不然也不会有那么多家长想孩子从小学编程了。(当然了,家长只是想孩子学编程,不是做程序员。)

常年埋头程序的结果,很容易让技术人对于社会上很多问题的复杂性本质认识不到位,恐惧,轻视,或者视而不见,总之,喜欢用自己常年打磨的逻辑能力做一个推理,然后下一个简单的结论。用毛爷爷的话说,是犯了形而上的毛病。

例如,在处理iOS产品上架合规性一类问题时,这种毛病暴露的就特别明显。

比如说相信一个功能别的产品也是这么做的,也能通过审核,那自己照着做也能通过。但是他忽略了这种判断背后的条件是,你的账号和别的账号在苹果眼里分量也许不同的,而苹果是不会把这件事写在文档上的。

如果只是说一说不要紧,最怕的是“倔”,要不怎么说是“认死理”呢。

第二,喜欢拿技术套市场。

这个怎么理解呢,就是有追求的技术人喜欢研究一些很强的技术,但是研究出来后怎么用,也就是落实到具体的应用场景,就很缺点想象力了。

举个身边有意思的例子,有个技术朋友花了三年时间业余时间断断续续的写,用 OpenGL 写了一套动画效果很棒的 UI 引擎,可以套一个 View 进去后定制各种酷炫的动画效果。做出来后也不知道用来干嘛好,后来认识了一个创业老板,老板一看你这个效果真不错啊,你这引擎多少钱我买了,朋友也没什么概念,说那要不五万卖你。老板直接钱就打过去了。后来老板拿给手下的程序员维护,用这套东西做了好几个“小而美”定位的效率工具,简单配置下就有酷炫的按钮动画效果,配合高级的视觉设计逼格拉满,收入怎么样我没问,但是苹果在好几个国家都上过推荐。

可能有人要说,那这个程序员哥哥没有UI帮忙啊,对,是这个理,但是最根本的问题是,做小而美工具这条路线,他想都没想到,连意识都意识不到的赚钱机会,怎么可能把握呢?有没有UI帮忙那是实现层的门槛而已。

第三,不擅长合作。

为什么很多创业赚到小钱(马化腾,李彦宏这些赚大钱就不说了,对我们大部分人没有参考价值)而且稳定活下来的都是跑商务,做营销出身的老板。

他们会搞钱。

他们会搞钱,是因为他们会搞定人,投资人,合伙人,还有各种七七八八的资源渠道。

大部分人,在创业路上直接卡死在这条路线上了。

投资人需要跑,合作渠道需要拉,包括当地的税务减免优惠,创业公司激励奖金,都需要和各种人打交道才能拿下来。

那我出海总行了吧,出海就不用那么麻烦了吧。不好意思,出海的合作优势也是领先的,找海外的自媒体渠道合作,给产品提曝光。坚持给苹果写推荐信,让自家产品多上推荐。你要擅长做这些,就不说比同行强一大截,起码做出好产品后创业活下来的希望要高出不少,还有很多信息差方法论,需要进圈子才知道。

--- 

我说的这些,不是贬损也不是中伤,说白了,任何职业都有自己的短板,也就是我们说的职业病,本来也不是什么大不了的事情。只是我们在大公司拧螺丝的时候,被保护的太好了。

只是创业会让一个人的短处不断放大,那是因为你必须为自己的选择负责了,没人帮你擦屁股了背锅了。所以短板才显得那么刺眼。

最后说一下,不是说有短板就会失败,谁没点短处呢。写出来只是让自己和朋友有更好的自我认知,明白自己的长处在哪,短处在哪。

最后补一个,左耳朵耗子的事情告诉我们,程序员真的要保养身子,拼到最后其实还是拼身体,活下来才有输出。


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

工作 7 年的老程序员,现在怎么样了

犹记得高中班主任说:“大家努力考上大学,大学没人管,到时候随便玩”。我估计很多老师都这么说过。我考上大学(2010年)之前也是这么过的。第一年哥哥给买了个一台华硕笔记本电脑。那个年代买华硕的应该不少,我周边就好几个。有了电脑之后,室友就拉着我一起 cs,四个人...
继续阅读 »

犹记得高中班主任说:“大家努力考上大学,大学没人管,到时候随便玩”。我估计很多老师都这么说过。

我考上大学(2010年)之前也是这么过的。第一年哥哥给买了个一台华硕笔记本电脑。那个年代买华硕的应该不少,我周边就好几个。有了电脑之后,室友就拉着我一起 cs,四个人组队玩,那会觉得很嗨,上头。

后来看室友在玩魔兽世界,那会不知道是什么游戏,就感觉很好玩,再后来就入坑了。还记得刚开始玩,完全不会,玩个防骑,但是打副本排DPS,结果还被人教育,教育之后还不听(因为别的职业不会玩),就经常被 T 出组。之后,上课天天看游戏攻略和玩法,或者干脆看小说。前两年就这么过去了

1 跟风考研

大三开始,觉得这么混下去不行了。在豆瓣上找了一些书,平时不上课的时候找个自习室学习。那会家里打电话说有哪个亲戚家的孩子考研了,那是我第一次知道“考研”这个词。那会在上宏微观经济学的课,刚好在豆瓣上看到一本手《牛奶面包经济学》,就在自习室里看。刚好有个同院系的同学在里面准备考研,在找小伙伴一起战斗(毕竟,考研是一场长跑,没有同行者,会很艰难)。我一合计,就加入了他们的小团队。从此成为“中国合伙人”(刚好四个人)中的一员。

我那会也不知道毕业了之后能去哪些公司,能找哪些岗位,对于社会完全不了解,对于考研也是完全不了解。小团队中的三个人都是考金融学,我在网上查,知道了学硕和专硕的区别,也知道专硕学费贵。我家里没钱,大学时期的生活费都是自己去沃尔玛、麦当劳、发传单挣得,大学四年,我在沃尔玛工作超过了 2 年、麦当劳半年,食堂倒盘子半年,中途还去发过传单,暑假还去实习。没钱,他们考金融学专硕,那我就靠经济学学硕吧,学硕学费便宜。

从此开始了考研之路。

2 三次考研

大三的时候,报名不是那么严格,混进去报了名,那会还没开始看书,算是体验了一把考研流程;

还记得那次政治考了 48 分,基本都过了很多学校的单科线,那会就感觉政治最好考(最后发现,还是太年轻)。

大四毕业那年,把所有考研科目的参数书都过了 2 遍,最后上考场,最后成绩也就刚过国家线。

毕业了,也不知道干啥,就听小伙伴的准备再考一次,之前和小伙伴一起来了北京,租了个阳台,又开始准备考研。结果依然是刚过国家线。这一年也多亏了一起来北京的几个同学资助我,否则可能都抗不过考试就饿死街头了。

总结这几次考研经历,失败的最大原因是,我根本不知道考研是为了什么。只是不知道如果工作的话,找什么工作。刚好别人提供了这样一个逃避工作的路,我麻木的跟着走而已。这也是为什么后面两次准备的过程中,一有空就看小说的原因。

但是,现在来看,我会感谢那会没有考上,不然就错过了现在喜欢的技术工作。因为如果真的考上了经济学研究生,我毕业之后也不知道能干啥,而且金融行业的工作我也不喜欢,性格上也不合适,几个小伙伴都是考的金融,去的券商,还是比较了解的。

3 入坑 JAVA 培训

考完之后,大概估了分,知道自己大概率上不了就开始找工作了。那会在前程无忧上各种投简历。开始看到一个做外汇的小公司,因为我在本科在一个工作室做过外汇交易相关的工作,还用程序写了一段量化交易的小程序。

所以去培训了几天,跟我哥借了几千块钱,注册了一个账号,开始买卖外汇。同时在网上找其他工作。

后面看介绍去西二旗的一家公司面试,说我的技术不行,他们提供 Java 培训(以前的套路),没钱可以贷款。

我自己也清楚本科一行 Java 代码没写过,直接工作也找不到工作。就贷款培训了,那会还提供住宿,跟学校宿舍似的,上下铺。

4 三年新手&非全研究生

培训四个月之后,开始找工作。那会 Java 还没这么卷,而且自己还有个 211 学历,一般公司的面试还是不少的。但是因为培训的时候学习不够刻苦(也是没有基础)。最后进了一个小公司,面试要 8000,最后给了 7000。这也是我给自己的最底线工资,少于这个工资就离开北京了,这一年是 2015 年。

这家公司是给政府单位做内部系统的,包括中石油、气象局等。我被分配其中一个组做气象相关系统。第二年末的时候,组内的活对我来说已经没什么难度了,就偷偷在外面找工作,H3C 面试前 3 面都通过了,结果最后大领导面气场不符,没通过。最后被另外一家公司的面试官劝退了。然后公司团建的时候,大领导也极力挽留我,最后没走成。

这次经历的经验教训有 2 个,第 1 个是没有拿到 offer 之前,尽量不要被领导知道。第 2 个是,只要领导知道你要离职,就一定要离职。这次就是年终团建的时候,被领导留下来了。但是第二年以各种理由不给工资。

之前自己就一直在想出路,但是小公司,技术成长有限,看书也对工作没有太大作用,没有太大成长。之后了解到研究生改革,有高中同学考了人大非全。自己也就开始准备非全的考试。最后拿到录取通知书,就开始准备离职了。PS:考研准备

在这家公司马上满 3 年重新签合同的时候,偷偷面试了几家,拿到了 2 个还不错的 offer。第二天就跟直属领导提离职了。这次不管直属领导以及大领导如何劝说,还是果断离职了。

这个公司有两个收获。一个是,了解了一般 Java Web 项目的全流程,掌握了基本开发技能,了解了一些大数据开发技术,如Hadoop套件。另外一个是,通过准备考研的过程,也整理出了一套开发过程中应该先思而后行。只有先整理出

5 五年开发经历

第二家公司是一家央企控股上市公司,市场规模中等。主要给政府提供集成项目。到这家公司第二年就开始带小团队做项目,但是工资很低,可能跟公司性质有关。还好公司有宿舍,有食堂。能省下一些钱。

到这家公司的时候,非全刚好开始上课,还好我们 5 点半就下班,所以我天天卡点下班,大领导天天给开发经理说让我加班。但是第一学期要上课,领导对我不爽,也只能这样了。

后来公司来了一个奇葩的产品经理,但是大领导很挺他,大领导下面有 60 号人,研发、产品、测试都有。需求天天改,还不写在文档上。研发都开发完了,后面发现有问题,要改回去,产品还问,谁让这么改的。

是否按照文档开发,也是大领导说的算,最后你按照文档开发也不对,因为他们更新不及时;不按照文档开发也不对,写了你不用。

最后,研发和产品出差,只能同时去一波人,要是同时去用户现场,会打架。最后没干出成绩,产品和大领导一起被干走了。

后面我们整体调整了部门,部门领导是研发出身。干了几个月之后,领导也比较认可我的能力,让我带团队做一个中型项目,下面大概有 10 号人,包括前后端和算法。也被提升为开发经理。

最后因为工资、工作距离(老婆怀孕,离家太远)以及工作内容等原因,跳槽到了下一家互联网公司。

6 入行互联网

凭借着 5 年的工作经历,还算可以的技术广度(毕竟之前啥都干),985 学校的非全研究生学历,以及还过得去的技术能力。找到了一家知名度还可以的互联网公司做商城开发。

这个部门是公司新成立的部门,领导是有好几家一线互联网经验的老程序员,技术过硬,管理能力强,会做人。组内成员都年轻有干劲。本打算在公司大干一场,涨涨技术深度(之前都是传统企业,技术深度不够,但是广度可以)。

结果因为政策调整,整个部门被裁,只剩下直属领导以及领导的领导。这一年是 2020 年。这个时候,我在这个公司还不到 1 年。

7 再前行

拿着上家公司的大礼包,马上开始改简历,投简历,面试,毕竟还有房贷要还(找了个好老婆,她们家出了大头,付了首付),马上还有娃要养,一天也不敢歇息。

经过一个半月的面试,虽然挂的多,通过的少。最终还是拿了 3 个不错的offer,一个滴滴(滴滴面经)、一个XXX网络(最终入职,薪资跟滴滴基本差不多,技术在市场上认可度也还不错。)以及一个建信金科的offer。

因为大厂部门也和上家公司一样,是新组建的部门,心有余悸。然后也还年轻,不想去银行躺平,也怕银行也不靠谱,毕竟现在都是银行科技公司,干几年被裁,更没有出路。最终入职XXX网络。

8 寒冬

入职XXX网络之后,开始接入公司的各种技术组件,以及看到比较成熟的需求提出、评估、开发、测试、发布规范。也看到公司各个业务中心、支撑中心的访问量,感叹还是大公司好,流程规范,流量大且有挑战性。

正要开心的进入节奏,还没转正呢(3 个月转正),组内一个刚转正的同事被裁,瞬间慌得一批。

刚半年呢,听说组内又有 4 个裁员指标,已经开始准备简历了。幸运的是,这次逃过一劫。

现在已经 1 年多了,在这样一个裁员消息满天飞的年代,还有一份不错的工作,很幸运、也很忐忑,也在慢慢寻找自己未来的路,共勉~

9 总结

整体来看,我对自己的现状还算满意,从一个高中每个月 300 块钱生活费家里都拿不出来;高考志愿填报,填学校看心情想去哪,填专业看专业名字的的村里娃,走到现在在北京有个不错的工作,组建了幸福的家庭,买了个不大不小的房子的城里娃。不管怎么样,也算给自己立足打下了基础,留在了这个有更多机会的城市;也给后代一个更高的起点。

但是,我也知道,现在的状态并不稳固,互联网工作随时可能会丢,家庭成员的一场大病可能就会导致整个家庭回到解放前。

所以,主业上,我的规划就是,尽力提升自己的技术能力和管理能力,争取能在中型公司当上管理层,延迟自己的下岗年龄;副业上,提升自己的写作能力,尝试各种不同的主题,尝试给各个自媒体投稿,增加副业收入。

希望自己永远少年,不要下岗~


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

Android-Widget重装上阵

如果要在Android系统中找一个一直存在,但一直被人忽略,而且有十分好用的功能,那么Widget,一定算一个。这个从Android 1.x就已经存在的功能,经历了近10年的迭代,在遭到无数无视和白眼之后,又重新回到了大家的视线之内,当然,也有可能是App内部...
继续阅读 »

如果要在Android系统中找一个一直存在,但一直被人忽略,而且有十分好用的功能,那么Widget,一定算一个。这个从Android 1.x就已经存在的功能,经历了近10年的迭代,在遭到无数无视和白眼之后,又重新回到了大家的视线之内,当然,也有可能是App内部已经没东西好卷了,所以大家又把目光放到了App之外,但不管怎样,Widget在Android 12之后,都开始焕发一新,官网镇楼,让我们重新来了解下这个最熟悉的陌生人。

developer.android.com/develop/ui/…

Widget使用的是RemoteView,这与Notification的使用如出一辙,RemoteView是继承自Parcelable的组件,可以跨进程使用。在Widget中,通过AppWidgetProvider来管理Widget的行为,通过RemoteView来对Widget进行布局,通过AppWidgetManager来对Widget进行刷新。基本的使用方式,我们可以通过一套模板代码来实现,在Android Studio中,直接New Widget即可。这样Android Studio就可以自动为你生成一个Widget的模板代码,详细代码我们就不贴了,我们来分析下代码的组成。

首先,每个Widget都包含一个AppWidgetProvider。这是Widget的逻辑管理类,它继承自BroadcastReceiver,然后,我们需要在清单中注册这个Receiver,并在meta-data中指定它的配置文件,它的配置文件是一个xml,这里描述的是添加Widget时展示的一些信息。

从这些地方来看,其实Widget的使用还是比较简单的,所以本文也不准备来讲解这些基础知识,下面我们针对开发中会遇到的一些实际需求来进行分析。

appwidget-provider配置文件

这个xml文件虽然简单,但还是有些有意思的东西的。

尺寸

在这里我们可以为Widget配置尺寸信息,通过maxResizeWidth、maxResizeHeight和minWidth、minHeight,我们可以大致将Widget的尺寸控制在MxN的格子内,这也是Widget在桌面上的展示方式,它并不是通过指定的宽高来展示的,而是桌面所占据的格子数。

官方设计文档中,对格子数和尺寸的转换标准,有一个表格,如下所示。

我们在设计的时候,也应该尽量遵循这个尺寸约束,避免在桌面上展示异常。在Android12之后,描述文件中,还增加了targetCellWidth和targetCellHeight两个参数,他们可以直接指定Widget所占据的格子数,这样更加方便,但由于它仅支持Android12+,所以,通常这些属性会一起设置。

有意思的是这个尺寸标准并不适用于所有的设备,因为ROM的碎片化问题,各个厂商的桌面都不一样,所以。。。只能参考参考。

updatePeriodMillis

这个参数用于指定Widget的被动刷新频率,它由系统控制,所以具有很强的不定性,而且它也不能随意设置,官网上对这个属性的限制如下所示。

updatePeriodMillis只支持设置30分钟以上的间隔,即1800000milliseconds,这也是为了保证后台能耗,即使你设置了小于30分钟的updatePeriodMillis,它也不会生效。

对于Widget来说,updatePeriodMillis控制的是系统被动刷新Widget的频率,如果当前App是活着的,那么随时可以通过广播来修改Widget。

而且这个值很有可能因为不同ROM而不同,所以,这是一个不怎么稳定的刷新机制。

其它

除了上面我们提到的一些属性,还有一些需要留意的。

  • resizeMode:拉伸的方向,可以设置为horizontal|vertical,表示两边都可以拉伸。
  • widgetCategory:对于现在的App来说,只能设置为home_screen了,5.0之前可以设置为锁屏,现在基本已经不用了。
  • widgetFeatures:这是Android12之后新加的属性,设置为reconfigurable之后,就可以直接调整Widget的尺寸,而不用像之前那样先删除旧的Widget再添加新的Widget了。

配置表

这个配置文件的主要作用,就是在添加Widget时,展示一个简要的描述信息,所以,一个App中是可以存在多个描述xml文件的,而且有几个描述文件,添加时,就会展示几个Widget的缩略图,通常我们会创建几个不同尺寸的Widget,例如2x2、4x2、4x1等,并创建多个xml面试文件,从而让用户可以选择添加哪一个Widget。

不过在Android12之后,设置一个Widget,通过拉动来改变尺寸,就可以动态改变Widget的不同展示效果了,但这仅限于Android12+,所以需要权衡使用利弊。

configure

通过configure属性可以配置添加Widget时的Configure Activity,这个在创建默认的Widget项目时就已经可以选择创建了,所以不多讲了,实际上就是一个简单的Activity,你可以配置一些参数,写入SP,然后在Widget中进行读取,从而实现自定义配置。

应用内唤起Widget的添加页面

大部分时候,我们都是通过在桌面上长按的方式来添加Widget,但是在Android API 26之后,系统提供了一直新的方式来在应用内唤起——requestPinAppWidget。

文档如下。

developer.android.com/reference/a…

代码如下所示。

fun requestToPinWidget(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val appWidgetManager: AppWidgetManager? = getSystemService(context, AppWidgetManager::class.java)
appWidgetManager?.let {
val myProvider = ComponentName(context, NewAppWidget::class.java)
if (appWidgetManager.isRequestPinAppWidgetSupported) {
val pinnedWidgetCallbackIntent = Intent(context, MainGroupActivity::class.java)
val successCallback: PendingIntent = PendingIntent.getBroadcast(context, 0,
pinnedWidgetCallbackIntent, PendingIntent.FLAG_UPDATE_CURRENT)
appWidgetManager.requestPinAppWidget(myProvider, null, successCallback)
}
}
}
}

通过这种方式,就可以直接唤起Widget的添加入口,从而避免用户手动在桌面中进行添加。

应用内主动更新Widget

前面我们提到了,当App活着的时候,可以主动来更新Widget,而且有两种方式可以实现,一种是通过广播ACTION_APPWIDGET_UPDATE,触发Widget的update回调,从而进行更新,代码如下所示。

val manager = AppWidgetManager.getInstance(this)
val ids = manager.getAppWidgetIds(ComponentName(this, XXXWidget::class.java))
val updateIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
sendBroadcast(updateIntent)

这种方式的本质就是发送更新的广播,除此之外,还可以使用AppWidgetManager来直接对Widget进行更新,代码如下。

val remoteViews = RemoteViews(context.packageName, R.layout.xxx)
val appWidgetManager = AppWidgetManager.getInstance(context)
val componentName = ComponentName(context, XXXWidgetProvider::class.java)
appWidgetManager.updateAppWidget(componentName, remoteViews)

这种方式就是通过AppWidgetManager来对指定的Widget进行修改,使用新的RemoteViews来更新当前Widget。

这两种方式一种是主动替换,一种是被动刷新,具体的使用场景可以根据业务的不同来使用不同的方式。

应用外被动更新Widget

产品现在重新开始重视Widget的一个重要原因,实际上就是App内部卷不动了,Widget可以在不打开App的情况下,对App进行引流,所以,应用外的Widget更新,就是一个很重要的组成部分,Widget需要展示用户感兴趣的内容,才能触发用户的点击。

前面我们提到了通过设置updatePeriodMillis来进行Widget的更新,但是这种方式存在一些使用限制,如果你需要完全自主的控制Widget的刷新,那么可以使用AlarmManager或者WorkManager,类似的代码如下所示。

private fun scheduleUpdates(context: Context) {
val activeWidgetIds = getActiveWidgetIds(context)
if (activeWidgetIds.isNotEmpty()) {
val nextUpdate = ZonedDateTime.now() + WIDGET_UPDATE_INTERVAL
val pendingIntent = getUpdatePendingIntent(context)
context.alarmManager.set(
AlarmManager.RTC_WAKEUP,
nextUpdate.toInstant().toEpochMilli(), // alarm time in millis since 1970-01-01 UTC
pendingIntent
)
}
}

当然,这种方式也同样会受到ROM的限制,所以说,不管是WorkManager还是AlarmManager,或者是updatePeriodMillis,都不是稳定可靠的,随它去吧,强扭的瓜不甜。

一般来说,使用updatePeriodMillis就够了,Widget的目的是为了引流,对内容的实时性其实并不是要求的那么严格,updatePeriodMillis在大部分场景下,都是够用的。

多布局动态适配

由于在Android12之后,用户可以在单个Widget上进行修改,从而修改Widget当前的配置,所以,用户在拖动修改Widget的尺寸时,就需要动态去调整Widget的布局,以自动适应不同的尺寸。我们可以通过下面的方式,来进行修改。

internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, widgetData: AppWidgetData) {
val views41 = RemoteViews(context.packageName, R.layout.new_app_widget41).also { updateView(it, context, appWidgetId, widgetData) }
val views42 = RemoteViews(context.packageName, R.layout.new_app_widget42).also { updateView(it, context, appWidgetId, widgetData) }
val views21 = RemoteViews(context.packageName, R.layout.new_app_widget21).also { updateView(it, context, appWidgetId, widgetData) }
val viewMapping: Map<SizeF, RemoteViews> = mapOf(
SizeF(180f, 110f) to views21,
SizeF(270f, 110f) to views41,
SizeF(270f, 280f) to views42
)
appWidgetManager.updateAppWidget(appWidgetId, RemoteViews(viewMapping))
}

private fun updateView(remoteViews: RemoteViews, context: Context, appWidgetId: Int, widgetData: AppWidgetData) {
remoteViews.setTextViewText(R.id.xxx, widgetData.xxx)
}

它的核心就是RemoteViews(viewMapping),通过这个就可以动态适配当前用户选择的尺寸。

那么如果是Android12之前呢?

我们需要重写onAppWidgetOptionsChanged回调来获取当前Widget的宽高,从而修改不同的布局,模板代码如下所示。

override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, newOptions: Bundle) {
   super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
   val options = appWidgetManager.getAppWidgetOptions(appWidgetId)

   val minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
   val minHeight = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT)

   val rows: Int = getWidgetCellsM(minHeight)
   val columns: Int = getWidgetCellsN(minWidth)
   updateAppWidget(context, appWidgetManager, appWidgetId, rows, columns)
}

fun getWidgetCellsN(size: Int): Int {
var n = 2
while (73 * n - 16 < size) {
++n
}
return n - 1
}

fun getWidgetCellsM(size: Int): Int {
var m = 2
while (118 * m - 16 < size) {
++m
}
return m - 1
}

其中的计算公式,n x m:(73n-16)x(118m-16)就是文档中提到的算法。

但是这种方案有一个致命的问题,那就是不同的ROM的计算方式完全不一样,有可能在Vivo上一个格子的高度只有80,但是在Pixel中,一个格子就是100,所以,在不同的设备上显示的n x m不一样,也是很正常的事。

也正是因为这样的问题,如果不是只在Android 12+的设备上使用,那么通常都是固定好Widget的大小,避免使用动态布局,这也是没办法的权衡之举。

RemoteViews行为

RemoteViews不像普通的View,所以我们不能像写普通布局的方式一样来操纵View,但RemoteViews提供了一些set方法来帮助我们对RemoteViews中的View进行修改,例如下面的代码。

remoteViews.setTextViewText(R.id.title, widgetData.xxx)

再比如点击后刷新Widget,实际上就是创建一个PendingIntent。

val intentUpdate = Intent(context, XXXAppWidget::class.java).also {
it.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
it.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(appWidgetId))
}
val pendingUpdate = PendingIntent.getBroadcast(
context, appWidgetId, intentUpdate,
PendingIntent.FLAG_UPDATE_CURRENT)
views.setOnClickPendingIntent(R.id.btn, pendingUpdate)

原理

RemoteViews通常用在通知和Widget中,分别通过NotificationManager和AppWidgetManager来进行管理,它们则是通过Binder来和SystemServer进程中的NotificationManagerService以及AppWidgetService进行通信,所以,RemoteViews实际上是运行在SystemServer中的,我们在修改RemoteViews时,就需要进行跨进程通信了,而RemoteViews封装了一系列跨进程通信的方法,简化了我们的调用,这也是为什么RemoteViews不支持全部的View方法的原因,RemoteViews抽象了一系列的set方法,并将它们抽象为统一的Action接口,这样就可以提供跨进程通信的效率,同时精简核心的功能。

如何进行后台请求

Widget在后台进行更新时,通常会请求网络,然后根据返回数据来修改Widget的数据展示。

AppWidgetProvider本质是广播,所以它拥有和广播一致的生命周期,ROM通常会定制广播的生命周期时间,例如设置为5s、7s,如果超过这个时间,那么就会产生ANR或者其它异常。

所以,我们一般不会把网络请求直接写在AppWidgetProvider中,一个比较好的方式,就是通过Service来进行更新。

首先我们创建一个Service,用来进行后台请求。

class AppWidgetRequestService : Service() {

override fun onBind(intent: Intent): IBinder? {
return null
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val appWidgetManager = AppWidgetManager.getInstance(this)
val allWidgetIds = intent?.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS)
if (allWidgetIds != null) {
for (appWidgetId in allWidgetIds) {
BackgroundRequest.getWidgetData {
NewAppWidget.updateAppWidget(this, appWidgetManager, appWidgetId, AppWidgetData(book1Cover = it))
}
}
}
return super.onStartCommand(intent, flags, startId)
}
}

在onStartCommand中,我们创建一个协程,来进行真正的网络请求。

object BackgroundRequest : CoroutineScope by MainScope() {
fun getWidgetData(onSuccess: (result: String) -> Unit) {
launch(Dispatchers.IO) {
val response = RetrofitClient.getXXXApi().getXXXX()
if (response.isSuccess) {
onSuccess(response.data.toString())
}
}
}
}

所以,在AppWidgetProvider的update里面,就需要进行下修改,将原有逻辑改为对Service的启动。

class NewAppWidget : AppWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
val intent = Intent(context.applicationContext, AppWidgetRequestService::class.java)
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
}

动画?

有必要这么卷吗,Widget里面还要加动画。由于RemoteViews里面不能实现正常的View动画,所以,Widget里面的动画基本都是通过类似「帧动画」的方式来实现的,即将动画抽成一帧一帧的图,然后通过Animator来进行切换,从而实现动画效果,群友给出了一篇比较好的实践,大家可以参考参考,我就不卷了。

juejin.cn/post/704862…

Widget的使用场景主要还是以实用功能为主,只有让用户觉得有用,才能锦上添花给App带来更多的活跃,否则只能是鸡肋。


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

请自信的大声告诉面试官forEach跳不出循环

如果面试官,或者有人问你foreach怎么跳出循环,请你大声的告诉ta,跳不出!!!!!!!!!!foreach 跳不出循环为什么呢?先看看foreach大体实现。Array.prototype.customForEach = function (fn) { ...
继续阅读 »

如果面试官,或者有人问你foreach怎么跳出循环,请你大声的告诉ta,跳不出!!!!!!!!!!

foreach 跳不出循环

为什么呢?

先看看foreach大体实现。

Array.prototype.customForEach = function (fn) {
for (let i = 0; i < this.length; i++) {
fn.call(this, this[i], i, this)
}
}

list.customForEach((item, i, list) => {
console.log(item, i, list)
})

let list = [1,2,3,4,5]

list.forEach((item,index,list)=>{
console.log(item,index,list)
})

list.customForEach((item,index,list)=>{
console.log(item,index,list)
})

两个输出的结果是一样的没啥问题,这就是foreach的大体实现,既然都知道了它的实现,那么对它为什么跳不出循环♻️应该都知道了,再不清楚的话,再看一下下面的例子。


function demo(){
return 'demo'
}

function demo2(){
demo()
return 'demo2'
}

demo()

在demo2函数里面调用demo函数,demo函数的return能阻止demo2函数下面的执行吗?很明显不行啊,demo函数里的return跟demo2函数一点关系都没有。现在你再回头看看foreach的实现,就明白它跳不出循环一清二楚了。

有点同学说不是可以通过抛出错误跳出循环吗?是的。看看下面例子。


let list = [1,2,3,4,5]

try {
list.forEach((item, index, list) => {
if (index === 2) {
throw new Error('demo')
}
console.log(item)
})
} catch (e) {
// console.log(e)
}

结果是我们想要,但是你看代码,哪个正常人会这样写代码?是非foreach不用吗?还是其他的循环关键字不配呢。

end

有反驳在评论区,show me your code !!!!!!!!!


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

环信EaseCallKit 使用指南及AppServer相关配置

简介:环信EaseCallKit 是结合声网音视频结合开发的音视频 UI 库,实现了一对一语音和视频通话以及多人音视频通话的功能。基于 EaseCallKit 可以快速实现通用音视频功能。环信官网EaseCallKit 使用文档: http://do...
继续阅读 »

简介:

环信EaseCallKit 是结合声网音视频结合开发的音视频 UI 库,实现了一对一语音和视频通话以及多人音视频通话的功能。基于 EaseCallKit 可以快速实现通用音视频功能。

在官网使用指南里面介绍了EaseCallKit 快速集成。如果项目中只使用了音视频,根据EaseCallKit 步骤集成即可。
下面介绍的是如何在环信官网Demo 中修改配置,跑Demo 音视频 和AppServer

前提条件:

需要分别创建 环信应用及 声网应用
环信Demo中音视频的介绍,以V4.0.3 Demo 为例
快速跑通环信IM Android Demo 参考之前文章(见链接:https://www.imgeek.net/article/825363562

环信IM Demo体验:https://www.easemob.com/download/demo

Android 项目快速集成音视频:

远程依赖不可以对代码进行修改,集成前可以先体验Demo是否可以满足业务需求,如果需要对页面进行修改,可以把ease-call-kit 以module 的形式导进项目中,对其进行修改
远程依赖:implementation 'io.hyphenate:ease-call-kit:4.0.3'
本地依赖源码链接:https://github.com/easemob/easecallkitui-android

本地Module的导入教程

1、从链接中下载源码并解压,下面是解压完的目录,需要导入的是红圈中的文件:

2、直接拖进项目里面

也可以通过Android Studio 导进去

3、在 build.gradle (App级别下)添加 api project(':ease-call-kit')

4、在settings.gradle下添加api project(':ease-call-kit')

添加完以后编译项目即可

Android 项目中的相关配置:

关于EaseCallKit 初始化在DemoHelper类下,见下图。

注:AgoraAppId这里是从声网平台注册的,在项目中需要切换,不可以直接上线使用


在Demo中 DemoHelper 类下,可以看到两个请求接口如下图
第一个接口:获取声网token ,这里用于音视频通话
第二个接口:根channelName和声网uId获取频道内所有人的UserId

注:这里的两个接口对应的appServer中的接口,a1.easemob.com 是环信域名,搭建appServer 以后需要将这里换成自己的域名


在环信Demo中,DemoHelper类中可以看到Demo是通过AppServer获取到token,这里获取到的token用于音视频

注:如果在测试中遇到音视频无法正常拨打接通,可以使用断点判断这里请求是否成功,环信Demo 中默认的token 请求接口不可以直接用于项目正常上线



代码简介:

获取到token和uid以后通过回调的形式传到音视频页面



下面是加入音视频的api 调用

注:在测试或者集成中如果遇到问题,可以按上面的步骤断点查看,是否有报错

Demo 中音视频点击代码如下

Java服务端项目中的配置:

appserver中需要修改的配置:
orgName:环信appkey 中#前面的部分
appName:环信appkey中#后面的部分
agoraAppId 和agoraCert 从声网console 下获取

修改完配置正常运行项目
Uid是服务端生成的随机数

扩展阅读:
手把手教你从零开始集成声网音视频功能(iOS版):https://www.imgeek.net/article/825364398

收起阅读 »

北宋 赵普:成事法则

前言 老规矩,唠唠嗑~ 1、历史才是真正值得我们去学习、去研究的 我们处于21世纪,相对于比以前更加先进,经济也更加发达了,但是你也会发现信息碎片化了,然后我们每天接收各种各样的海量的数据,真正对我们成长没有一丁点的好处。 我觉得原因有几个:第一还是信息碎片...
继续阅读 »

8e0dc884457ed5fb795606f4a251441e.jpg


前言




老规矩,唠唠嗑~


1、历史才是真正值得我们去学习、去研究的


我们处于21世纪,相对于比以前更加先进,经济也更加发达了,但是你也会发现信息碎片化了,然后我们每天接收各种各样的海量的数据,真正对我们成长没有一丁点的好处。


我觉得原因有几个:第一还是信息碎片化,东一块西一块,你无法吃透某个领域,形成自己的知识体系;第二你接收到的信息大部分是别人想让你知道的,你被人当“枪”使了,第三个知识是别人嚼过一次的半成品,我们习惯性的搬运,没有经过自己的思考,那么这些信息也是没有多大价值的。


比如抖音有个“不提倡苦难让我们成长的一种说法”,我的思考是这样的:苦难是环境+个人因果里面一种情况,就像一瓶水,里面有水、塑料瓶、包装构造成的一种东西。我们学过王阳明在龙场悟道,曾国藩在经历贬官、人生挫折之后,换了个人。里面最重要一点,是人在挫折面前存活了下来,然后是做事方式、思考方式变了,这才是最为关键的。


如果你没有变化,苦难还是那个苦难,不提倡感恩苦难还是那么一句话,没有一点营养。经过我们这番思考之后,如何改变做事方式,如何改变思考方式才是我们要提高的,才是有营养的东西!



我们虽然处于一个现代化的时代,但是做事方式、思考方式甚至智慧,不一定超越古人。



所以冯唐老师:“历史是一块磨刀石,在不同人眼中,增长智慧”


2、成长需要老师


在我人生成长路上,我都会选某些优秀的人作为榜样去学习,这使得我成长飞快。最重要的原因是,人容易以自己常用的方法来处理事情,那么就会固守踏步不前,正是因为有这些优秀的老师,我们才能一直前进学习。


我有这么一个亲戚,我老叔,他儿子都很厉害,厅级,我老叔为人处事比较强,家教里面有这么一段话:就是跟别人有矛盾不要到处讲,甚至反其道帮助别人。当然啦,这个一般人达不到的境界,我们来看看历史上韩信,当他贫穷的时候受到了胯下之辱,从别人胯下爬过去,当他功成名就的时候没有去记仇,所以有这等气概之人一般都会有所作为。


当然我们今天想聊的另一个角色:北宋 赵普,就不是这样有气概的一个人,我们不能以一个很高的标准去要求其他人这样做,气概会分为好几等,曾国藩讲看一个人未来的成就,就看他有多少气概。


我们今天这位是北宋 赵普,我是在《百家讲坛》王立群老师讲述北宋历史中认识的,三次为相,最吸引我的点是做事犀利,眼光同样犀利,这正是我们成事所追求的。


北宋-赵普 生平




他辅佐了两代皇帝,三次拜相,跟历史上很著名的事件都有关联,陈桥兵变,黄袍加身,将赵匡胤扶上了皇帝,然后在治理方针上也有独到的见解,先治理南方再北方,整顿内政,做事相当犀利,当然缺点也是有的,这里我们略过,因为我们主要学习里面的成事法则。


有人讲他没有什么业绩,或者重大的贡献,我们来看看他做了哪些事情


他所做突出事件


1、陈桥兵变,黄袍加身 关键参与者


赵普不仅是宰相,还是谋士。960年,在赵光义和赵普等人的策划下,太祖在陈桥驿发动兵变,随即成功登基,赵普因拥立之功升任右谏议大夫。


我们跟现实对比,就像你能否帮你老板提升一个level,甚至好几个level,做的东西能够提升整个团队影响力,产出可以影响其他团队的价值的事情,这个非常有难度的。


2、治理方针


赵匡胤经常登门找赵普,有一次问他你的治理想法是什么,他说先南方后北方,南方相对比较肥沃,资源比较多,其次北方有契丹,让另一个敌对势力去抵挡,这样不会消耗精力,可以看出他在平时是有研究的,不会在这种突发的检查中慌乱。


杯酒释兵权,也是在他的劝说下,赵匡胤实施了,对于稳定的局势是有帮助的。


3、二次拜相给出有价值的东西


太宗赵光义即位的时候,位置不太符合常规,赵普给出当年的“金匮之盟”,让太宗的位置合理了,然后让他儿子继位也搞定了,赵普给了很有价值的东西。


4、治理内政


打掉了“五人帮”,还有另一个“大师集团”,这是非常厉害的两件事,比如说让你打掉你们公司某些团体哈哈哈,可想而知很难的,第一个没有背景,第二个很容易翻车,特别是“大师集团”,那是太宗赵光义专门去保的,最后还是被赵普办成铁案,翻不了盘,在我眼中看到了做事的犀利。


5、为老板解围


前几任老板打契丹,都无功而返,很多人站出来指责,最后是赵普承担了所有,然后在晚年也极力推荐人才,标准是人靠谱,不耍聪明。


成事法则




1、有眼界、有经验


我们做一件事的时候,需要去研究,别人有没有比较好的方案,而不是瞎折腾,比如apm方案、全链路灰度,其实业界早有成熟的方案让我们来借鉴,这样我们再结合自己团队的特点特殊化,让整个技术方案更加稳健。


其次是不会慌乱,如果你对问题平时有研究,就不会自乱阵脚,看到一个段子:泰森面对重拳也会晕过去,正是因为实力够强,练习够多,才能轻松应对。


2、干对事、干好事


赵普虽然说没有做出什么杰出贡献,所以他每次对这个岗位都很有动力,给他一个空间他能干出很大的事业出来,但是你仔细去看他干过的事情,非常细致,也很有价值。


比如说我们日常工作中也有一些重要的工作,也有一些修修补补的工作,当然那些重要的工作价值会更大,不一定是你能拿到的,你懂的,我们尽可能眼光要犀利点,比如说将一个功能做成一个系统,这样它的价值是辐射性向其他项目、其他团队产出价值。


做事计划周密,让我想起一个朋友跟我讲他们架构师推ddd,最后没推成被辞退。当我们在做计划前,需要调研,明白ddd解决的问题是什么,团队推不下去的原因可能是什么,团队的特点是怎样的,我们的计划应该怎么调整,这是非常关键,最后跟主要干系人沟通,让他们参与进来,而不是自己yy一套不适合方案然后直接推,你都没有沟通过别人的意见,还有没有获得别人的支持,所以在推的过程会很多问题。


说直白点,ddd是为了解决系统庞大之后错综复杂的关系,提高扩展能力,同时你也要意识到业务团队的特点,业绩为大,时间紧迫,不可能全套搬迁到应用到,只能针对性的改造!


3、职场角色扮演


小兵角色,就是博主这个角色啦,哈哈。我认为很重要的一点:主动性,只有你主动参与事件的一员,你才能改变点什么,做出点什么贡献,其次整个团队、项目也是在这种主动性基础推进更快,更稳健。因为规则只能保证基本的流程,而且它也需要优化的,也没有创造性的能力。第二个:有眼力,能够发现有价值的闪光点,第三个:做事计划周密,第四个;行动力,跟第一个有所重叠。


领导的角色,因为我这方面经验比较少,所以只能大概聊聊。第一个有主见,有原则,你对某些问题有研究,有自己的判断,赵普正是因为这个被百官推崇,不会说你老板一会改一个需求,你的团队跟着变,这样做不好事情的,团队劳累,所以需要一个比较强势的领导。第二个,有背景、有支持度,在公司做事还是在其他地方做事都一样,需要点人脉、资源,不然你的方案比较难推进的,没有主要干系人的支持,你怎么获得大多数人的支持呢对吧。第三个:同样是有眼光,这个不分等级,就跟我们高考一样,只不过题型变了,解题思路不变的,什么事情价值更大一些。


最后引用孙子一段话“知己知彼,百战不殆”,这句话非常简单,但是你真正去做会发现很多的细节,所以我们上面总结很多的要点,关键还是在事上去练我们这种成事的技能。


作者:大鸡腿同学
来源:juejin.cn/post/7259372449833254969
收起阅读 »

10年程序员,想对新人说什么?

前言 最近知乎上,有一位大佬邀请我回答下面这个问题,看到这个问题我百感交集,感触颇多。在我是新人时,如果有前辈能够指导方向一下,分享一些踩坑经历,或许会让我少走很多弯路,节省更多的学习的成本。 这篇文章根据我多年的工作经验,给新人总结了25条建议,希望对你会有...
继续阅读 »

前言


最近知乎上,有一位大佬邀请我回答下面这个问题,看到这个问题我百感交集,感触颇多。图片在我是新人时,如果有前辈能够指导方向一下,分享一些踩坑经历,或许会让我少走很多弯路,节省更多的学习的成本。


这篇文章根据我多年的工作经验,给新人总结了25条建议,希望对你会有所帮助。


1.写好注释


很多小伙伴不愿意给代码写注释,主要有以下两个原因:



  1. 开发时间太短了,没时间写注释。

  2. 《重构》那本书说代码即注释。


我在开发的前面几年也不喜欢写注释,觉得这是一件很酷的事情。


但后来发现,有些两年之前的代码,业务逻辑都忘了,有些代码自己都看不懂。特别是有部分非常复杂的逻辑和算法,需要重新花很多时间才能看明白,可以说自己把自己坑了。


没有注释的代码,不便于维护。


因此强烈建议大家给代码写注释。


但注释也不是越多越好,注释多了增加了代码的复杂度,增加了维护成本,给自己增加工作量。


我们要写好注释,但不能太啰嗦,要给关键或者核心的代码增加注释。我们可以写某个方法是做什么的,主要步骤是什么,给算法写个demo示例等。


这样以后过了很长时间,再去看这段代码的时候,也会比较容易上手。


2.多写单元测试


我看过身边很多大佬写代码有个好习惯,比如新写了某个Util工具类,他们会同时在test目录下,给该工具类编写一些单元测试代码。


很多小伙伴觉得写单元测试是浪费时间,没有这个必要。


假如你想重构某个工具类,但由于这个工具类有很多逻辑,要把这些逻辑重新测试一遍,要花费不少时间。


于是,你产生了放弃重构的想法。


但如果你之前给该工具类编写了完整的单元测试,重构完成之后,重新执行一下之前的单元测试,就知道重构的结果是否满足预期,这样能够减少很多的测试时间。


多写单元测试对开发来说,是一个非常好的习惯,有助于提升代码质量。


即使因为当初开发时间比较紧,没时间写单元测试,也建议在后面空闲的时间内,把单元测试补上。


3.主动重构自己的烂代码


好的代码不是一下子就能写成的,需要不断地重构,修复发现的bug。


不知道你有没有这种体会,看自己1年之前写的代码,简直不忍直视。


这说明你对业务或者技术的理解,比之前更深入了,认知水平有一定的提升。


如果有机会,建议你主动重构一下自己的烂代码。把重复的代码,抽取成公共方法。有些参数名称,或者方法名称当时没有取好的,可以及时修改一下。对于逻辑不清晰的代码,重新梳理一下业务逻辑。看看代码中能不能引入一些设计模式,让代码变得更优雅等等。


通过代码重构的过程,以自我为驱动,能够不断提升我们编写代码的水平。


4.代码review很重要


有些公司在系统上线之前,会组织一次代码评审,一起review一下这个迭代要上线的一些代码。


通过相互的代码review,可以发现一些代码的漏洞,不好的写法,发现自己写代码的坏毛病,让自己能够快速提升。


当然如果你们公司没有建立代码的相互review机制,也没关系。


可以后面可以多自己review自己的代码。


5.多用explain查看执行计划


我们在写完查询SQL语句之后,有个好习惯是用explain关键字查看一下该SQL语句有没有走索引


对于数据量比较大的表,走了索引和没有走索引,SQL语句的执行时间可能会相差上百倍。


我之前亲身经历过这种差距。


因此建议大家多用explain查看SQL语句的执行计划。


关于explain关键字的用法,如果你想进一步了解,可以看看我的另外一篇文章《explain | 索引优化的这把绝世好剑,你真的会用吗?》,里面有详细的介绍。


6.上线前整理checklist


在系统上线之前,一定要整理上线的清单,即我们说的:checklist


系统上线有可能是一件很复杂的事情,涉及的东西可能会比较多。


假如服务A依赖服务B,服务B又依赖服务C。这样的话,服务发版的顺序是:CBA,如果顺序不对,可能会出现问题。


有时候新功能上线时,需要提前执行sql脚本初始化数据,否则新功能有问题。


要先配置定时任务。


上线之前,要在apollo中增加一些配置。


上线完成之后,需要增加相应的菜单,给指定用户或者角色分配权限。


等等。


系统上线,整个过程中,可能会涉及多方面的事情,我们需要将这些事情记录到checklist当中,避免踩坑。


7.写好接口文档


接口文档对接口提供者,和接口调用者来说,都非常重要。


如果你没有接口文档,别人咋知道你接口的地址是什么,接口参数是什么,请求方式时什么,接口多个参数分别代码什么含义,返回值有哪些字段等等。


他们不知道,必定会多次问你,无形当中,增加了很多沟通的成本。


如果你的接口文档写的不好,写得别人看不懂,接口文档有很多错误,比如:输入参数的枚举值,跟实际情况不一样。


这样不光把自己坑了,也会把别人坑惨。


因此,写接口文档一定要写好,尽量不要马马虎虎应付差事。


如果对写接口文档比较感兴趣,可以看看我的另一篇文章《瞧瞧别人家的API接口,那叫一个优雅》,里面有详细的介绍。


8.接口要提前评估请求量


我们在设计接口的时候,要跟业务方或者产品经理确认一下请求量。


假如你的接口只能承受100qps,但实际上产生了1000qps。


这样你的接口,很有可能会承受不住这么大的压力,而直接挂掉。


我们需要对接口做压力测试,预估接口的请求量,需要部署多少个服务器节点。


压力测试的话,可以用jmeter、loadRunner等工具。


此外,还需要对接口做限流,防止别人恶意调用你的接口,导致服务器压力过大。


限流的话,可以基于用户id、ip地址、接口地址等多个维度同时做限制。


可以在nginx层,或者网关层做限流。


9.接口要做幂等性设计


我们在设计接口时,一定要考虑并发调用的情况。


比如:用户在前端页面,非常快的点击了两次保存按钮,这样就会在极短的时间内调用你两次接口。


如果不做幂等性设计,在数据库中可能会产生两条重复的数据。


还有一种情况时,业务方调用你这边的接口,该接口发生了超时,它有自动重试机制,也可能会让你这边产生重复的数据。


因此,在做接口设计时,要做幂等设计。


当然幂等设计的方案有很多,感兴趣的小伙伴可以看看我的另一篇文章《高并发下如何保证接口的幂等性?》。


如果接口并发量不太大,推荐大家使用在表中加唯一索引的方案,更加简单。


10.接口参数有调整一定要慎重


有时候我们提供的接口,需要调整参数。


比如:新增加了一个参数,或者参数类型从int改成String,或者参数名称有status改成auditStatus,参数由单个id改成批量的idList等等。


建议涉及到接口参数修改一定要慎重。


修改接口参数之前,一定要先评估调用端和影响范围,不要自己偷偷修改。如果出问题了,调用方后面肯定要骂娘。


我们在做接口参数调整时,要做一些兼容性的考虑。


其实删除参数和修改参数名称是一个问题,都会导致那个参数接收不到数据。


因此,尽量避免删除参数和修改参数名。


对于修改参数名称的情况,我们可以增加一个新参数,来接收数据,老的数据还是保留,代码中做兼容处理。


11.调用第三方接口要加失败重试


我们在调用第三方接口时,由于存在远程调用,可能会出现接口超时的问题。


如果接口超时了,你不知道是执行成功,还是执行失败了。


这时你可以增加自动重试机制


接口超时会抛一个connection_timeout或者read_timeout的异常,你可以捕获这个异常,用一个while循环自动重试3次。


这样就能尽可能减少调用第三方接口失败的情况。


当然调用第三方接口还有很多其他的坑,感兴趣的小伙伴可以看看我的另一篇文章《我调用第三方接口遇到的13大坑》,里面有详细的介绍。


12.处理线上数据前,要先备份数据


有时候,线上数据出现了问题,我们需要修复数据,但涉及的数据有点多。


这时建议在处理线上数据前,一定要先备份数据


备份数据非常简单,可以执行以下sql:


create table order_2022121819 like `order`;
insert into order_2022121819 select * from `order`;

数据备份之后,万一后面哪天数据处理错了,我们可以直接从备份表中还原数据,防止悲剧的产生。


13.不要轻易删除线上字段


不要轻易删除线上字段,至少我们公司是这样规定的。


如果你删除了某个线上字段,但是该字段引用的代码没有删除干净,可能会导致代码出现异常。


假设开发人员已经把程序改成不使用删除字段了,接下来如何部署呢?


如果先把程序部署好了,还没来得及删除数据库相关表字段。


当有insert请求时,由于数据库中该字段是必填的,会报必填字段不能为空的异常。


如果先把数据库中相关表字段删了,程序还没来得及发。这时所有涉及该删除字段的增删改查,都会报字段不存在的异常。


所以,线上环境字段不要轻易删除。


14.要合理设置字段类型和长度


我们在设计表的时候,要给相关字段设置合理的字段类型和长度。


如果字段类型和长度不够,有些数据可能会保存失败。


如果字段类型和长度太大了,又会浪费存储空间。


我们在工作中,要根据实际情况而定。


以下原则可以参考一下:



  • 尽可能选择占用存储空间小的字段类型,在满足正常业务需求的情况下,从小到大,往上选。

  • 如果字符串长度固定,或者差别不大,可以选择char类型。如果字符串长度差别较大,可以选择varchar类型。

  • 是否字段,可以选择bit类型。

  • 枚举字段,可以选择tinyint类型。

  • 主键字段,可以选择bigint类型。

  • 金额字段,可以选择decimal类型。

  • 时间字段,可以选择timestamp或datetime类型。


15.避免一次性查询太多数据


我们在设计接口,或者调用别人接口的时候,都要避免一次性查询太多数据。


一次性查询太多的数据,可能会导致查询耗时很长,更加严重的情况会导致系统出现OOM的问题。


我们之前调用第三方,查询一天的指标数据,该接口经常出现超时问题。


在做excel导出时,如果一次性查询出所有的数据,导出到excel文件中,可能会导致系统出现OOM问题。


因此我们的接口要做分页设计


如果是调用第三方的接口批量查询接口,尽量分批调用,不要一次性根据id集合查询所有数据。


如果调用第三方批量查询接口,对性能有一定的要求,我们可以分批之后,用多线程调用接口,最后汇总返回数据。


16.多线程不一定比单线程快


很多小伙伴有一个误解,认为使用了多线程一定比使用单线程快。


其实要看使用场景。


如果你的业务逻辑是一个耗时的操作,比如:远程调用接口,或者磁盘IO操作,这种使用多线程比单线程要快一些。


但如果你的业务逻辑非常简单,在一个循环中打印数据,这时候,使用单线程可能会更快一些。


因为使用多线程,会引入额外的消耗,比如:创建新线程的耗时,抢占CPU资源时线程上下文需要不断切换,这个切换过程是有一定的时间损耗的。


因此,多线程不一定比单线程快。我们要根据实际业务场景,决定是使用单线程,还是使用多线程。


17.注意事务问题


很多时候,我们的代码为了保证数据库多张表保存数据的完整性和一致性,需要使用@Transactional注解的声明式事务,或者使用TransactionTemplate的编程式事务。


加入事务之后,如果A,B,C三张表同时保存数据,要么一起成功,要么一起失败。


不会出现数据保存一半的情况,比如:表A保存成功了,但表B和C保存失败了。


这种情况数据会直接回滚,A,B,C三张表的数据都会同时保存失败。


如果使用@Transactional注解的声明式事务,可能会出现事务失效的问题,感兴趣的小伙伴可以看看我的另一篇文章《聊聊spring事务失效的12种场景,太坑了》。


建议优先使用TransactionTemplate的编程式事务的方式创建事务。


此外,引入事务还会带来大事务问题,可能会导致接口超时,或者出现数据库死锁的问题。


因此,我们需要优化代码,尽量避免大事务的问题,因为它有许多危害。关于大事务问题,感兴趣的小伙伴,可以看看我的另一篇文章《让人头痛的大事务问题到底要如何解决?》,里面有详情介绍。


18.小数容易丢失精度


不知道你在使用小数时,有没有踩过坑,一些运算导致小数丢失了精度。


如果你在项目中使用了float或者double类型的数据,用他们参与计算,极可能会出现精度丢失问题。


使用Double时可能会有这种场景:


double amount1 = 0.02;
double amount2 = 0.03;
System.out.println(amount2 - amount1);

正常情况下预计amount2 - amount1应该等于0.01


但是执行结果,却为:


0.009999999999999998

实际结果小于预计结果。


Double类型的两个参数相减会转换成二进制,因为Double有效位数为16位这就会出现存储小数位数不够的情况,这种情况下就会出现误差。


因此,在做小数运算时,更推荐大家使用BigDecimal,避免精度的丢失。


但如果在使用BigDecimal时,使用不当,也会丢失精度。



BigDecimal amount1 = new BigDecimal(0.02);
BigDecimal amount2 = new BigDecimal(0.03);
System.out.println(amount2.subtract(amount1));

这个例子中定义了两个BigDecimal类型参数,使用构造函数初始化数据,然后打印两个参数相减后的值。


结果:


0.0099999999999999984734433411404097569175064563751220703125

使用BigDecimal的构造函数创建BigDecimal,也会导致精度丢失。


如果如何避免精度丢失呢?


BigDecimal amount1 = BigDecimal.valueOf(0.02);
BigDecimal amount2 = BigDecimal.valueOf(0.03);
System.out.println(amount2.subtract(amount1));

使用BigDecimal.valueOf方法初始化BigDecimal类型参数,能保证精度不丢失。


19.优先使用批量操作


有些小伙伴可能写过这样的代码,在一个for循环中,一个个调用远程接口,或者执行数据库的update操作。


其实,这样是比较消耗性能的。


我们尽可能将在一个循环中多次的单个操作,改成一次的批量操作,这样会将代码的性能提升不少。


例如:


for(User user : userList) {
   userMapper.update(user);
}

改成:


userMapper.updateForBatch(userList);

20.synchronized其实用的不多


我们在面试中当中,经常会被面试官问到synchronized加锁的考题。


说实话,synchronized的锁升级过程,还是有点复杂的。


但在实际工作中,使用synchronized加锁的机会不多。


synchronized更适合于单机环境,可以保证一个服务器节点上,多个线程访问公共资源时,只有一个线程能够拿到那把锁,其他的线程都需要等待。


但实际上我们的系统,大部分是处于分布式环境当中的。


为了保证服务的稳定性,我们一般会把系统部署到两个以上的服务器节点上。


后面哪一天有个服务器节点挂了,系统也能在另外一个服务器节点上正常运行。


当然也能会出现,一个服务器节点扛不住用户请求压力,也挂掉的情况。


这种情况,应该提前部署3个服务节点。


此外,即使只有一个服务器节点,但如果你有api和job两个服务,都会修改某张表的数据。


这时使用synchronized加锁也会有问题。


因此,在工作中更多的是使用分布式锁


目前比较主流的分布式锁有:



  1. 数据库悲观锁。

  2. 基于时间戳或者版本号的乐观锁。

  3. 使用redis的分布式锁。

  4. 使用zookeeper的分布式锁。


其实这些方案都有一些使用场景。


目前使用更多的是redis分布式锁。


当然使用redis分布式锁也很容易踩坑,感兴趣的小伙伴可以看看我的另一篇文章《聊聊redis分布式锁的8大坑》,里面有详细介绍。


21.异步思想很重要


不知道你有没有做过接口的性能优化,其中有一个非常重要的优化手段是:异步


如果我们的某个保存数据的API接口中的业务逻辑非常复杂,经常出现超时问题。


现在让你优化该怎么优化呢?


先从索引,sql语句优化。


这些优化之后,效果不太明显。


这时该怎么办呢?


这就可以使用异步思想来优化了。


如果该接口的实时性要求不高,我们可以用一张表保存用户数据,然后使用job或者mq,这种异步的方式,读取该表的数据,做业务逻辑处理。


如果该接口对实效性要求有点高,我们可以梳理一下接口的业务逻辑,看看哪些是核心逻辑,哪些是非核心逻辑。


对于核心逻辑,可以在接口中同步执行。


对于非核心逻辑,可以使用job或者mq这种异步的方式处理。


22.Git提交代码要有好习惯


有些小伙伴,不太习惯在Git上提交代码。


非常勤劳的使用idea,写了一天的代码,最后下班前,准备提交代码的时候,电脑突然死机了。


会让你欲哭无泪。


用Git提交代码有个好习惯是:多次提交。


避免一次性提交太多代码的情况。


这样可以减少代码丢失的风险。


更重要的是,如果多个人协同开发,别人能够尽早获取你最新的代码,可以尽可能减少代码的冲突。


假如你开发一天的代码准备去提交的时候,发现你的部分代码,别人也改过了,产生了大量的冲突。


解决冲突这个过程是很痛苦的。


如果你能够多次提交代码,可能会及时获取别人最新的代码,减少代码冲突的发生。因为每次push代码之前,Git会先检查一下,代码有没有更新,如果有更新,需要你先pull一下最新的代码。


此外,使用Git提交代码的时候,一定要写好注释,提交的代码实现了什么功能,或者修复了什么bug。


如果有条件的话,每次提交时在注释中可以带上jira任务的id,这样后面方便统计工作量。


23.善用开源的工具类


我们一定要多熟悉一下开源的工具类,真的可以帮我们提升开发效率,避免在工作中重复造轮子。


目前业界使用比较多的工具包有:apache的common,google的guava和国内几个大佬些hutool。


比如将一个大集合的数据,按每500条数据,分成多个小集合。


这个需求如果要你自己实现,需要巴拉巴拉写一堆代码。


但如果使用google的guava包,可以非常轻松的使用:


List<Integer> list = Lists.newArrayList(12345);
List<List<Integer>> partitionList = Lists.partition(list, 2);
System.out.println(partitionList);

如果你对更多的第三方工具类比较感兴趣,可以看看我的另一篇文章《吐血推荐17个提升开发效率的“轮子”》。


24.培养写技术博客的好习惯


我们在学习新知识点的时候,学完了之后,非常容易忘记。


往往学到后面,把前面的忘记了。


回头温习前面的,又把后面的忘记了。


因此,建议大家培养做笔记的习惯。


我们可以通过写技术博客的方式,来记笔记,不仅可以给学到的知识点加深印象,还能锻炼自己的表达能力。


此外,工作中遇到的一些问题,以及解决方案,都可以沉淀到技术博客中。


一方面是为了避免下次犯相同的错误。


另一方面也可以帮助别人少走弯路。


而且,在面试中如果你的简历中写了技术博客地址,是有一定的加分的。


因此建议大家培养些技术博客的习惯。


25.多阅读优秀源码


建议大家利用空闲时间,多阅读JDK、Spring、Mybatis的源码。


通过阅读源码,可以真正的了解某个技术的底层原理是什么,这些开源项目有哪些好的设计思想,有哪些巧妙的编码技巧,使用了哪些优秀的设计模式,可能会出现什么问题等等。


当然阅读源码是一个很枯燥的过程。


有时候我们会发现,有些源码代码量很多,继承关系很复杂,使用了很多设计模式,一眼根本看不明白。


对于这类不太容易读懂的源码,我们不要一口吃一个胖子。


要先找一个切入点,不断深入,由点及面的阅读。


我们可以通过debug的方式阅读源码。


在阅读的过程中,可以通过idea工具,自动生成类的继承关系,辅助我们更好的理解代码逻辑。


我们可以一边读源码,一边画流程图,可以更好的加深印象。


当然还有很多建议,由于篇幅有限,后面有机会再跟大家分享。


当然还有很多建议,由于篇幅有限,后面有机会再跟大家分享。


最后欢迎大家加入苏三的知识星球【Java突击队】,一起学习。


星球中有很多独家的干货内容,比如:Java后端学习路线,分享实战项目,源码分析,百万级系统设计,系统上线的一些坑,MQ专题,真实面试题,每天都会回答大家提出的问题。


星球目前开通了6个优质专栏:技术选型、系统设计、Spring源码解读、痛点问题、高频面试题 和 性能优化。


作者:苏三说技术
来源:juejin.cn/post/7259341632700235832
收起阅读 »

有些程序员表面老实,背地里不知道玩得有多花

作者:CODING来源:juejin.cn/post/7259258539164205115


















作者:CODING

来源:juejin.cn/post/7259258539164205115

该怎么放弃你,我的内卷

各位,两个月没写文章了,这两个月发生了很多事,也让我产生了很多不一样的感悟。从上次发完《阅阿里大裁员有感》,我的手机里就推了越来越多的“裁员”、“经济下行”、“焦虑”等信息。我这边现在这家公司,虽然不裁员,但是执行了“SABC”绩效分布考核,什么内容大家应该也...
继续阅读 »

各位,两个月没写文章了,这两个月发生了很多事,也让我产生了很多不一样的感悟。从上次发完《阅阿里大裁员有感》,我的手机里就推了越来越多的“裁员”、“经济下行”、“焦虑”等信息。我这边现在这家公司,虽然不裁员,但是执行了“SABC”绩效分布考核,什么内容大家应该也都清楚,最后给我打了个 B-。呵呵,扣工资 20%,变成所谓的绩效工资,下次考核看情况发放。


很多兄弟看到这可能会替我打抱不平,狗资本家,快去发起劳动仲裁。可是他们马上又下上另一剂猛药,那就是不停的 PUA 你,告诉你现在有家庭,要多努力,要一心扑在工作上,放心,下次一定给你打回来。


我承认,他们这些话术我都看过,基本相当于明牌。可依然我还是被影响到了,情绪十分低落,也没心思去劳动局跟他们 PK。对未来的预期瞬间变得很悲观,人要是一悲观了,真的干什么也提不起兴趣。我一门心思都扑在以后能干什么上,其它的啥也不想管,疯狂的在国内外门户上刷信息,希望能找到一条“赚钱之路”。我研究了 Web3、AI 绘画、搞自媒体、网赚攻略(什么视频搬运、抄书、小说转漫画等等),基本上信息流推给我的,我都研究了一遍。这些玩意越研究越让人焦虑,因为那些标题都起的特别的有煽动性,动不动就日入几万,而我发的那些,浏览量都破不了百。于是我就想研究更多的路子去赚钱,老实说,东南亚那边的情况,我也了解过一些。


后面我对家人的态度也越来越坏,经常不耐烦,看着我小孩我经常叹气,我想这他妈可能就是中年危机提前爆发了,总之那段时间人会越来越焦虑。


后来还是我一兄弟,邀请我一家人去平潭自驾游,我们其实也没玩几天,属于特种兵式旅游,两天两晚(晚上熬夜开车去)。回来之后心情就好多了,也没那么焦虑了。其实本来也没什么,君子不立于危墙之下,这里不行那就走。找不到就先干自己的项目(我有开源项目)。我其实对干这行还是蛮有兴趣的,应该持续坚持的干下去,半途而废干别的是下策。


回想下我那时候焦虑的经历,我以前根本看都不看那种赚钱文章的,因为我知道这些大部分是在卖课,可为什么那时候我着了魔一样呢?其实很大部分与网络有关系,你着急干什么,你就愿意看点什么,你看点什么,网络就给你推什么。这种消极循环人一旦深陷其中,光凭自己是很难走出来的。其实这种时候应该主动去接收一些积极乐观的情绪,有助于自己调整心态,网络给不了你,只有身边人能给你。


更深一步的想,所谓内卷是不是也是通过网络在传播着,深刻的影响到每一个人。所谓的“智能推荐算法”,真的智能吗?大家想看的一定就是适合每个人的吗?你不停的点击去看的信息,真的能帮助到你吗?网络是我们的工具,还是我们是网络的工具?


我想我们真的应该停下来,想想我们到底在多大程度上需要抖音、需要 BiliBili、需要知乎,也许它们真的没这么重要。


人生在世,我们到底应该追逐什么?或者说,追逐什么其实不重要,重要的是我们去追逐的过程。在这个过程中,没有内卷,没有与别人的竞争,只有对自我的审视和成长。


换句话说,我有多久没有好好了解自己了,那些独属于自己的东西,永远不会背叛的资源。我们常说的:能力、人脉、技术、视野。其实除此之外还有很多很多,我刷视频看到的有趣视频点的赞,我 Chrome 里收藏的网页,我百度网盘里躺着的分享资料等等等等,还有最重要的一项,就是我的身体和组成我身体的每一个部分:大脑、心脏、肺...... 有多久没有关注和了解它们了?在这个内卷的时代,每个人都在比拼都在竞争,都怕落于人后,都想快点挣更多的钱,这些,时常让我们忽视了对我们最重要的东西。


有趣的是,每个平台都在疯狂的更新自己的算法,期望能更精准的描述一个人,给人打上各种各样的标签。但在这场竞赛中,没有平台能竞争的过你自己,在这个世界上,只有自己更了解自己。所以我真的感觉它们在做无用功,浪费资源,最好的平台,不是给打各种标签,而是引导每个人发现自己的标签是什么。


这里我想分享给各位几个我思考的点,以供探讨。


原则一:相比与到处去找信息差,更重要的是建立自己的“资源池”


我那时候不停的刷信息,不停的找信息,本质上,我是在幻想着找到一个信息差,从而获利。这也是网上铺天盖地的文章所推崇的,所谓在风口上猪都能飞。但它们总是在掩盖一个逻辑错误,那就是找到信息差和获利之间的因果关系。实际上,找到信息差只是获利的条件之一,你有多大的能力利用这个信息差,这个信息差的时效性,方方面面的因素都会互相交织和影响。


更进一步的想,信息差就像风一样,它存在于冷热空气的交换之时,它存在于各行各业、每时每刻。让我们去追逐风,这现实吗?


我们更应该静下来,好好数数自己手头的东西,整理自己的大脑。找到自己“资源池”有哪些资源,哪些可以为我们所用,哪些可以继续扩充。思路可以打开一点,任何在当前时刻属于你的东西,都是你“资源池”的一部分。


原则二:出卖自己时间和体力的不做


这个不做,不是指不去做,而是指不长期的做。一般入门一个行业或者技术,肯定要付出时间和体力的。但你要说十年如一日的付出相同的东西,那所谓“35 岁”危机就只能找到你了。这点其实各行各业都一样,只是互联网行业处在发声的前沿罢了。


包括所谓网赚、搬运都是一个道理,毫无技术含量的事做几年就好。要时常审视自己现在在干什么,手头有哪些资源,未来的目标是什么。这跟程序运行是一个道理,运行了一段时间,停下来让自己 GC 一下。不然很容易 StackOverflow。


原则三:自己抓住的资源,千万不要轻易放手


如果不经常审视自己的“资源池”,给所有资源估估价值,就很容易被人带坑里。


原先我就做过一个项目,这是个跨部门项目,我那个领导一直告诉我说这个项目没前途、没卵用,绩效也给我打的不好,问我还要不要继续做。我说那就算了吧,做的我都不想做了。


我一放弃,马上就有新人接手,连交接也不用做,代码直接拿走,吃相可见一斑。


也就是从这里我才理解到,我其实没有了解自己,没了解过我手里的项目,被人潜移默化的影响了。影响一个人的思想真的不难,不停的重复就好了。所以还是那句话,多把自己手里的“资源池”拿出来晒一晒,整理一下。


其实 996 也是一样,拿出了你最重要的资源---身体,到底换来了什么,值得好好评估一下。


原则四:做自己喜欢的赛道,更要积累自己的资源


这几个月的经历给我的最大感觉是,这世界上真的有太多太多的行业,也有很多人赚到了钱(至少网络上宣传他们赚了钱)。网络能让这些信息病毒式的传播,导致很多人错觉的以为自己照着做也能挣到钱。但他们忽视的是,网络能把世界各地的人汇聚起来,让信息流通。其实也提供了一个更大的平台,在这个平台里,只有更卷的人才能挣到钱。


有时候真的应该抛开网络。比如,你会写代码,这是你“资源池”里的一项技能,你把这个技能公开到网络售卖。只有两种情况,要么你非常的卷,打拼出一番事业;要么你根本竞争不过别人,这是普遍情况,这世界那么大,比你优秀的人有太多太多了。


但是抛开网络,回到你身边的小小社交圈子,你的技能可能就没那么普遍了。可能你会说,那我做程序员,我身边朋友认识的大部分也是做程序员啊。那么可以这么想,假如你会做菜,你身边的程序员朋友都会做菜吗?假如你会画画,你身边的程序员朋友都会画画吗?人和人总有差异点,你觉得找不到优势,那是因为你尚未建立自己的“资源库”。


先认识自己,再让身边的人认识自己,当他们会给你打标签时,他们就成了你“资源库”中的一员,这就是人脉。这才是是独属于你自己的标签,而不是抖音、B 站为你打的冷冰冷的标签。


总结


以上我感悟的四个原则,我称之为“资源池思维”,一个比较程序员化的名词。


这篇文章发完后,我后续可能就继续更新一下具体的技术文章了,继续深耕技术。


最后,推荐看到最后的各位看一部冷门电影:《神迹》,讲述的是医生维维安托马斯的故事。看完可以来一起交流交流感悟。



本文仅发于掘金平台,禁止未经作者同意转载、复制。


作者:FengY_HYY
来源:juejin.cn/post/7259210874447151163

收起阅读 »

独立开发前100天真正重要的事

我从4月开始离职开始全职做独立开发,算是真正踏进入了这条河流。在过去半年多了我也观察了很多独立开发者。自己目前算是过了新手村(有正常的开发节奏,有3万用户)。看到很多刚起步的独立开发者还是有很多疑问,所以分享一下我在独立开发最初期的一些经验。因为我也不是很成功...
继续阅读 »

我从4月开始离职开始全职做独立开发,算是真正踏进入了这条河流。在过去半年多了我也观察了很多独立开发者。自己目前算是过了新手村(有正常的开发节奏,有3万用户)。看到很多刚起步的独立开发者还是有很多疑问,所以分享一下我在独立开发最初期的一些经验。因为我也不是很成功(没有走的很远),所以只能分享独立开发前100天的经验。


先说一下我认为独立开发起步阶段面临的主要困难:


第一:没有公司的孤独感。如果是一个人全职开发就更寂寞了。即使有一两个合作伙伴,但是大概率也是异地,因此也算是网友性质的社交。人说到底是群居的,所以需要找到一种社交平衡。我想可能这也是很多独立开发白天要在外面地方待着的原因,也许一个人一直在家待着有点闷。


第二:无法建立产品的健康开发节奏。以前在公司的时候自己是流程里的一环,只关心自己分工的完成情况。做了独立开发以后,所有事情都需要自己决策。太多自由的结果就是没了方向。什么都想做,好像什么都可以做,又感觉什么都不好做。


第三:没有收入。产品从开始建造到有足够健康收入中间有一段过程。这还有一个前提要是一个真正有用户价值的产品。如果你起步的时候自己没有一个优势产品方向,又没有个人社区号召力,就算你的产品是好的,也需要一段时间(可长可短)才能获得有效收入。一上线就火的概率太小了。高强度投入一件事情,如果长期没有收入,家人会有很多质疑,可能最后自己也很怀疑自己。


我把这三点结合起来,编一个故事大家可能比较有画面了:



一个人做独立开发已经半年多了,产品设计都是自己做,也没有什么人可以讨论,不知道下一步该做什么。目前每天只有零星的新增。做了这么久,总共只有两三千的收入。看来做产品还得会营销**,打算最近开始学习一下运营**。最近也打算做一下AI产品,感觉这个赛道很火。老婆说如果不行就早点回去上班好了,总不能一直这样。家人们,你们说我应该坚持吗。




家人们会说你的产品很棒,会说你做的比他们强,会说下次一定,会说你未来会成功。但是家人们不会为你掏一分钱。



也许我们不知道如何成功,但是我们可以知道什么是失败。你知道的失败方式越多,你成功的概率就越大。总的来说,产品的成功就两个要点:有用户价值,能赚钱。注意,这两点是或的关系,不是且的关系。一个产品可以能赚钱,但是没有用。一个产品也可以有用,但是不赚钱。失败就是你做的产品:既没用,又不赚钱


基于前面提到的三个困难,我得出的前100天最重要的事是:找到一个可行的产品迭代方向。和团队经过磨合,互相能有有效、信任的协作。找到一百个种子用户。你越早解决这三个困难,你越快走上轨道


确认产品方向


如果你真的做过产品,你就知道最终正确的产品路径不是通过脑中的某刻灵光乍现得到的。所以不是那种大脑飞速运算解题的方式。这里有两件事情需要确认:大的产品方向,产品的路径。


比如阿里巴巴,马云不是一开始就做的淘宝网。他只是觉得互联网普及以后,电子商务会有需求。最开始做的是黄页,并不是淘宝网。但是他没有在第一个项目失败以后,去做门户网站。产品路径的例子是特斯拉。特斯拉很早就确定了先出高性能的跑车,高性能轿车(model s),有了前面的技术积累以后,最后通过推出平价的轿车赢得市场(model 3)。特斯拉在 model 3 大规模量产前都是亏损的。


所以最重要的是确认产品方向。这个方向要结合自身的情况进行设定,就是我在前面帖子里提到的要是你想做的,能做的。也许想达到的产品方向有很多工作量,这个时候就要有同步的产品路径。比如小米手机的创业,他们一开始就想造手机。但是直接启动手机的制造市场、技术都有很大的困难。于是他们先通过做 MIUI 入局。


这里面首先要有个大的方向判断,对于独立开发来说,我觉得张宁在《创作者》里提到的两个维度的方向挺有意思:大众、小众;高频、低频。这里面两两结合各有什么特点我这里就不展开了,大家可以自行体会。


但是可以明确的是,独立开发者做不了又大众又高频的应用。大众又高频,就不可能小而美。大众又高频,最后赢家除了产品能力,要有运营优势,要有资源优势。独立开发者通常没有运营优势和资源优势。另外一点,如果是小众低频,就一定要高忠诚,高付费转化。可以往大众低频或者小众高频的方向多想想


产品方向选择还有一个建议就是要有秘密。成功的业务后面一定有秘密。秘密也回答了一个问题:如果这个需求真的存在,为什么用户选择了你的产品。


最初级的秘密就是信息差,你知道别人不知道,所以你可以,更早做,可以更低的成本,更高效,有更高的获客率。


更高级的秘密就是大家都能看到,但是大家知道了,但是大家不信(脑中想到了拼多多的砍一刀)。


最高级的秘密就是所有人都知道,但是他们做不到。


总结起来,你应该找到一个你有优势的细分方向。信息优势,洞察优势也是优势。


没有一发即中的银弹,最平凡的方式想很多方向,用最低成本进行最快速的验证。在反馈中渐渐明晰产品路径。如果你三个月不管反馈闷头做,只做出了一个产品方向。你失败的概率是很大的。所以我看到很多产品1.0 的时候就做会员,做社区,做跨平台我是很不理解的。其实这些功能在早期性价比很低。


我的方式是脑海中有10个想法,挑出3个想法做初步设计,选出一个或者两个想法做产品验证。可能是原型,数据是模拟的,没有设计,如果产品真的解决了痛点的话,用户会愿意用,然后他会给你反馈他想要更好的体验,他愿意付钱得到这些改进。这里的效率优势是,你能在更短的时间验证产品方向是不是对的。总比走了3个月才发现是一条死胡同要好。


开发者很容易因为想到一个想法很兴奋,觉得这个很有用,就闷头做了一个月。有可能的问题是,这个想法虽然是个痛点,但是这个痛点频次很低,场景很少,所以虽然有用,但是没人会愿意买单。所以尽量跳出自己的思维,从用户的角度来进行验证是很必要的。


团队协作


独立开发的开发方式和传统公司不同。需要建立一个全新的工作流程。在初期大家都是空白,所以需要通过产品迭代中,形成高效的开发默契。大家松散做东西,工作习惯,工作职责都需要有共识才行。


比如我合作的设计师早期喜欢一次做一大板块的整体设计,大概一周的工作量。初期我觉得我们对产品有激情,大家都应该有自由的发挥空间。但是做了一周的设计图和产品脑海中的产品行进方向不一致怎么办。在工作时间上,我合作的设计师因为目前还是兼职,他只能在下班后设计。然而我全职只在6点前工作。这又是一个要协调的地方。


如果你是一个产品,需要协调研发和设计,三个人协调就又更复杂了。要找到一个大家都舒服,高效的协作方式。


100个种子用户


独立开发最核心的一环就是找到一个健康的商业模式。产品方向和团队协作的目标都是为了未来可以达成一个健康的商业模式。我觉得太多独立开发者上来就把目标(野心)定的太高。一口吃不成胖子。独立开发早期的商业目标只有一个:尽快达成团队最低维持标准。一鸟在手胜过二鸟在林。不要在团队只有几个人的时候用几十个人的方式管理。


初期就要估算出产品(团队)能够持续运转的最低收入。这个成本越低,团队就越容易跑起来。当收入足够覆盖团队的成本后,你的心态就会得到极大的自由,可以尝试很多奇奇怪怪有趣的想法。所以早期不要想有多高的天花板,如何建立壁垒,就关心如何达成产品的及格生命线。谁会想做一个注定失败的产品呢。


早期在没有运营优势的情况下,最重要的指标就是用户满意度了。用户满意度,就暗示了这个产品有没有解决切实的用户问题,用户愿不愿意为你宣传。其实很多人都搞错了重点,在产品没有让100个种子用户满意前,新增的流量是没有意义的。因为再多的用户都会流失。竹篮打水一场空。如果你把产品的用户目标定在100个种子用户,你也就没了运营压力,可以关注在如何打造正确的产品上。在产品基本盘没有问题后,再思考后面的才有意义。


总结


总结起来三点就是:做什么(产品方向),怎么做(团队协作),为谁做(验证用户)。以上就是我全职独立开发3个多月以来肤浅的经验分享,希望对你有帮助。



PS:目前我的 App 是:打工人小组件(只在 AppStore),有兴趣的欢迎下载体验。


作者:没故事的卓同学
来源:juejin.cn/post/7259210748801663031

收起阅读 »

你的代码不堪一击!太烂了!

前言 小王,你的页面白屏了,赶快修复一下。小王排查后发现是服务端传回来的数据格式不对导致,无数据时传回来不是 [] 而是 null, 从而导致 forEach 方法报错导致白屏,于是告诉测试,这是服务端的错误导致,要让服务端来修改,结果测试来了一句:“服务端返...
继续阅读 »

前言


小王,你的页面白屏了,赶快修复一下。小王排查后发现是服务端传回来的数据格式不对导致,无数据时传回来不是 [] 而是 null, 从而导致 forEach 方法报错导致白屏,于是告诉测试,这是服务端的错误导致,要让服务端来修改,结果测试来了一句:“服务端返回数据格式错误也不能白屏!!” “好吧,千错万错都是前端的错。” 小王抱怨着把白屏修复了。


刚过不久,老李喊道:“小王,你的组件又渲染不出来了。” 小王不耐烦地过来去看了一下,“你这个属性data 格式不对,要数组,你传个对象干嘛呢。”老李反驳: “ 就算 data 格式传错,也不应该整个组件渲染不出来,至少展示暂无数据吧!” “行,你说什么就是什么吧。” 小王又抱怨着把问题修复了。


类似场景,小王时不时都要经历一次,久而久之,大家都觉得小王的技术太菜了。小王听到后,倍感委屈:“这都是别人的错误,反倒成为我的错了!”


等到小王离职后,我去看了一下他的代码,的确够烂的,不堪一击!太烂了!下面来吐槽一下。


一、变量解构一解就报错


优化前


const App = (props) => {
const { data } = props;
const { name, age } = data
}

如果你觉得以上代码没问题,我只能说你对你变量的解构赋值掌握的不扎实。



解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于 undefinednull 无法转为对象,所以对它们进行解构赋值时都会报错。



所以当 dataundefinednull 时候,上述代码就会报错。


优化后


const App = (props) => {
const { data } = props || {};
const { name, age } = data || {};
}

二、不靠谱的默认值


估计有些同学,看到上小节的代码,感觉还可以再优化一下。


再优化一下


const App = (props = {}) => {
const { data = {} } = props;
const { name, age } = data ;
}

我看了摇摇头,只能说你对ES6默认值的掌握不扎实。



ES6 内部使用严格相等运算符(===)判断一个变量是否有值。所以,如果一个对象的属性值不严格等于 undefined ,默认值是不会生效的。



所以当 props.datanull,那么 const { name, age } = null 就会报错!


三、数组的方法只能用真数组调用


优化前:


const App = (props) => {
const { data } = props || {};
const nameList = (data || []).map(item => item.name);
}

那么问题来了,当 data123 , data || [] 的结果是 123123 作为一个 number 是没有 map 方法的,就会报错。


数组的方法只能用真数组调用,哪怕是类数组也不行。如何判断 data 是真数组,Array.isArray 是最靠谱的。


优化后:


const App = (props) => {
const { data } = props || {};
let nameList = [];
if (Array.isArray(data)) {
nameList = data.map(item => item.name);
}
}

四、数组中每项不一定都是对象


优化前:


const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => `我的名字是${item.name},今年${item.age}岁了`);
}
}

一旦 data 数组中某项值是 undefinednull,那么 item.name 必定报错,可能又白屏了。


优化后:


const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => `我的名字是${item?.name},今年${item?.age}岁了`);
}
}

? 可选链操作符,虽然好用,但也不能滥用。item?.name 会被编译成 item === null || item === void 0 ? void 0 : item.name,滥用会导致编辑后的代码大小增大。


二次优化后:


const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
}

五、对象的方法谁能调用


优化前:


const App = (props) => {
const { data } = props || {};
const nameList = Object.keys(data || {});
}

只要变量能被转成对象,就可以使用对象的方法,但是 undefinednull 无法转换成对象。对其使用对象方法时就会报错。


优化后:


const _toString = Object.prototype.toString;
const isPlainObject = (obj) => {
return _toString.call(obj) === '[object Object]';
}
const App = (props) => {
const { data } = props || {};
const nameList = [];
if (isPlainObject(data)) {
nameList = Object.keys(data);
}
}

六、async/await 错误捕获


优化前:


import React, { useState } from 'react';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const res = await queryData();
setLoading(false);
}
}

如果 queryData() 执行报错,那是不是页面一直在转圈圈。


优化后:


import React, { useState } from 'react';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
try {
const res = await queryData();
setLoading(false);
} catch (error) {
setLoading(false);
}
}
}

如果使用 trycatch 来捕获 await 的错误感觉不太优雅,可以使用 await-to-js 来优雅地捕获。


二次优化后:


import React, { useState } from 'react';
import to from 'await-to-js';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const [err, res] = await to(queryData());
setLoading(false);
}
}

七、不是什么都能用来JSON.parse


优化前:


const App = (props) => {
const { data } = props || {};
const dataObj = JSON.parse(data);
}

JSON.parse() 方法将一个有效的 JSON 字符串转换为 JavaScript 对象。这里没必要去判断一个字符串是否为有效的 JSON 字符串。只要利用 trycatch 来捕获错误即可。


优化后:


const App = (props) => {
const { data } = props || {};
let dataObj = {};
try {
dataObj = JSON.parse(data);
} catch (error) {
console.error('data不是一个有效的JSON字符串')
}
}

八、被修改的引用类型数据


优化前:


const App = (props) => {
const { data } = props || {};
if (Array.isArray(data)) {
data.forEach(item => {
if (item) item.age = 12;
})
}
}

如果谁用 App 这个函数后,他会搞不懂为啥 dataage 的值为啥一直为 12,在他的代码中找不到任何修改 dataage 值的地方。只因为 data 是引用类型数据。在公共函数中为了防止处理引用类型数据时不小心修改了数据,建议先使用 lodash.clonedeep 克隆一下。


优化后:


import cloneDeep from 'lodash.clonedeep';

const App = (props) => {
const { data } = props || {};
const dataCopy = cloneDeep(data);
if (Array.isArray(dataCopy)) {
dataCopy.forEach(item => {
if (item) item.age = 12;
})
}
}

九、并发异步执行赋值操作


优化前:


const App = (props) => {
const { data } = props || {};
let urlList = [];
if (Array.isArray(data)) {
data.forEach(item => {
const { id = '' } = item || {};
getUrl(id).then(res => {
if (res) urlList.push(res);
});
});
console.log(urlList);
}
}

上述代码中 console.log(urlList) 是无法打印出 urlList 的最终结果。因为 getUrl 是异步函数,执行完才给 urlList 添加一个值,而 data.forEach 循环是同步执行的,当 data.forEach 执行完成后,getUrl 可能还没执行完成,从而会导致 console.log(urlList) 打印出来的 urlList 不是最终结果。


所以我们要使用队列形式让异步函数并发执行,再用 Promise.all 监听所有异步函数执行完毕后,再打印 urlList 的值。


优化后:


const App = async (props) => {
const { data } = props || {};
let urlList = [];
if (Array.isArray(data)) {
const jobs = data.map(async item => {
const { id = '' } = item || {};
const res = await getUrl(id);
if (res) urlList.push(res);
return res;
});
await Promise.all(jobs);
console.log(urlList);
}
}

十、过度防御


优化前:


const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
const info = infoList?.join(',');
}

infoList 后面为什么要跟 ?,数组的 map 方法返回的一定是个数组。


优化后:


const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
const info = infoList.join(',');
}

后续


以上对小王代码的吐槽,最后我只想说一下,以上的错误都是一些 JS 基础知识,跟任何框架没有任何关系。如果你工作了几年还是犯这些错误

作者:红尘炼心
来源:juejin.cn/post/7259007674520158268
,真的可以考虑转行。

收起阅读 »

假如互联网人都很懂冒犯

大家好,我是老三,最近沉迷于听脱口秀,并且疯狂安利同事。 脱口秀演员常常说的一句话是:“脱口秀是冒犯的艺术”。最近我发现,同事们好像有点不一样了。 阳光灿烂的早上,趿拉着我的宝马拖鞋,跨上包浆的小黄车,屁股感受着阳光积累的炙热,往公司飞驰而去。 一步跨进电梯...
继续阅读 »

大家好,我是老三,最近沉迷于听脱口秀,并且疯狂安利同事。


脱口秀演员常常说的一句话是:“脱口秀是冒犯的艺术”。最近我发现,同事们好像有点不一样了。




阳光灿烂的早上,趿拉着我的宝马拖鞋,跨上包浆的小黄车,屁股感受着阳光积累的炙热,往公司飞驰而去。


一步跨进电梯间,我擦汗的动作凝固住了,挂上了矜持的微笑:“老板,早上好。”


老板:“早,你还在呢?又来带薪划水了?”


我:“嗨,我这再努力,最后不也就让你给我们多换几个嫂子嘛。”


老板:“没有哈哈,我开玩笑。”


我:“我也是,哈哈哈。”


今天的电梯似乎比往常慢了很多。


我:“老板最近在忙什么?”


老板:“昨天参加了一个峰会,马xx知道吧?他就坐我前边。”


我:“卧槽,真能装。没有,哈哈。”


老板:“哈哈哈”。


电梯到了,我俩都步履匆匆地进了公司。


小组内每天早上都有一个晨会,汇报工作进度和计划。


开了一会,转着椅子,划着朋友圈的我停了下来——到我了。


我:“昨天主要……今天计划……”


Leader:“你这不能说没有一点产出,也可以说一点产出都没有。其实,我对你是有一些失望的,原本今年绩效考评给你一个……”


我:“影响你合周报了是吗?不是哈哈。”


Leader、小组同事:“哈哈哈“。


Leader:“好了,我们这次顺便来对齐一下双月OKR,你们OKR都写的太保守了,一看就是能完成的,往大里吹啊。开玩笑哈哈。”。


我:”我以前就耕一亩田,现在把整个河北平原都给犁了。不是,哈哈。”


同事:“我要带公司打上月球,把你踢下来,我来当话事人。唉,哈哈”


Leader、同事、我:“哈哈哈“。


晨会开完,开始工作,产品经理拉我和和前端对需求。


产品经理:“你们程序员懂Java语言、Python语言、Go语言,就是不懂汉语言,真不想跟你们对需求。开个玩笑,哈哈。”


我:“没啥,你吹牛皮像狼,催进度像狗,做需求像羊,就这需求文档,还没擦屁股纸字多,没啥好对的。不是哈哈。”


产品经理、前端、我:“哈哈哈”。


产品经理:“那我们就对到这了,你们接着聊技术实现。”


前端:“没啥好聊的,后端大哥看着写吧,反正你们那破接口,套的比裹脚布还厚,没事还老出BUG。没有哈哈。”


我:“还不是为了兼容你们,一点动脑子的逻辑都不写,天天切图当然不出错。不是哈哈。”


前端、我:“哈哈哈”。


经过一番拉扯之后,我终于开始写代码了。


看到一段代码,我皱起了眉头,同事写的,我顺手写下了这样一段注释:


/**
* 写这段代码的人,建议在脑袋开个口,把水倒掉。不是哈哈,开个玩笑。
**/


代码写完了,准备上线,找同事给我Review,同事看了一会,给出了评论。



又在背着我们偷偷写烂代码了,建议改行。没有哈哈。



同事、我:“哈哈哈”。


终于下班了,路过门口,HR小姐姐还在加班。


我:“小姐姐怎么还没下班?别装了,老板都走了。开玩笑哈哈。”


HR小姐姐:“这不是看看怎么优化你们嘛,任务比较重。不是,哈哈。”


HR小姐姐、我:“哈哈哈”。


我感觉到一种不一样的氛围在公司慢慢弥散开来,我不知道怎么形容,但我想到了一句话——


“既分高下,也决生死”。




写这篇的时候,想到两年前,有个叫码农小说家的作者横空出世,写了一些生动活泼、灵气十足的段子,我也跟风写了两篇,这就是“荒腔走板”系列的来源。


后来,他结婚了。


看(抄)不到的我只能自己想,想破头也写不不来像样的段子,这个系列就不了了之,今天又偶尔来了灵感,写下一篇,也顺带缅怀

作者:三分恶
来源:juejin.cn/post/7259036373579350077
一下光哥带来的快乐。

收起阅读 »

应届毕业生关于五险一金你知道多少?很多人找工作都吃了亏

“明明说好月薪1w,结果最后到手才7k” 最近,不少上了工作岗位的小伙伴跟作者吐槽 为什么面试的时候说好的薪资跟与实际到手的差别这么大呢? 所以五险一金究竟有什么作用? 薪资都是如何被它扣掉的? 很多同学在面试或者刚刚入职时 怕HR觉得自己问题多 就不敢多问...
继续阅读 »

“明明说好月薪1w,结果最后到手才7k”


最近,不少上了工作岗位的小伙伴跟作者吐槽


为什么面试的时候说好的薪资跟与实际到手的差别这么大呢?


所以五险一金究竟有什么作用?


薪资都是如何被它扣掉的?


img


很多同学在面试或者刚刚入职时


怕HR觉得自己问题多


就不敢多问什么


但是,这和你的薪资福利息息相关


如果没问清,你一年可能要少拿上万元!


跟着作者一起来看看五险一金的具体细则吧。


1. 什么是五险一金?


五险是指养老保险、医疗保险、失业保险、工伤保险和生育保险,这五险可以统称为社会保险,也就是我们常说的“社保”。而“一金”则是住房公积金。


在这五险中,个人需要承担养老保险、医疗保险和失业保险这三项的缴费,而单位则需要为员工交齐五险的全部费用。


对于应届生来说,五险一金的重要性可能并不清楚,但它与日后的购房、生育、医疗和退休等方面息息相关。


img


举个例子,如果你打算在上海或北京买房,那你一定要连续缴纳5年社保才可以。


2. 五险一金的具体内容:


养老保险:


养老保险是为了解决劳动者在达到国家规定的解除劳动义务的劳动年龄界限,或因年老丧失劳动能力退出劳动岗位后的基本生活而建立的一种社会保险制度。个人缴纳养老保险累计满15年后才能领取退休金。养老保险的缴纳标准各地有所不同,以2018年北京市为例,单位缴费比例为19%,个人缴费比例为8%。也就是说,如果你的月入为10000元,你每个月需要交800元的养老保险金。


医疗保险:


医疗保险是为了补偿劳动者因疾病风险造成的经济损失而建立的一项社会保险制度。个人需要缴纳医疗保险费用,以获取医疗方面的一定报销和救助。各地医疗保险的缴纳标准也有所不同,以2018年北京市为例,单位缴费比例为10%,个人缴费比例为2%。也就是说,如果你在北京月入10000元,你每个月需要交203元的医疗保险金。


失业保险:


失业保险是为了对因失业而暂时中断生活来源的劳动者提供物质帮助,以保障其基本生活。单位和个人共同按照缴费基数进行缴纳。以2018年北京市为例,单位缴费比例为1%,个人缴费比例为0.2%。失业保险的领取条件包括按规定参加失业保险,所在单位和本人已按照规定履行缴费义务满1年,非因本人意愿中断就业等情况。


工伤保险:


工伤保险是为在工作期间或上下班途中因意外受伤的劳动者提供报销的一项社会保险制度。工伤保险费由公司全额缴纳。工伤保险的认定比较复杂,一旦发生意外,建议第一时间报警或联系公司存留证据,同时需要在一个月内办理工伤鉴定。


生育保险:


生育保险是针对怀孕生育的劳动者,缴纳一定时间后可以享受产假,并在产假期间领取生育津贴。单位全额缴纳生育保险费用。产假期间的生育津贴额度为本人或妻子生育当月本单位人均缴费工资除以30(天)再乘以产假/陪产假天数。


注意:男性职工也是有生育保险的哦~如果你的妻子没有工作,是可以使用你的生育保险的;如果你的工作性质决定了你不能休14天的陪产假(陪产假天数各地有差异),你在正常拿工资的同时是可以申请生育津贴的。


所以,不要再说,我是男生,为什么还要缴生育保险。


产假期间的生育津贴额度:


本人或妻子生育当月本单位人平缴费工资÷30(天)×产假/陪产假天数。


值得注意的是,大家通常理解的产假是有工资的,这个工资其实是生育津贴,是你在休完产假后国家支付给你的,并不是单位支付给。根据法律规定,单位也没有必要支付给你。个别福利较好的单位,才会同时支付生育津贴+产假工资。


【缴纳标准】


按照保险基数进行缴纳,由公司全额缴纳。以2018年北京市为例,缴纳比例为0.8%。


关于“五险一金”的缴纳费用,一张图更清楚


以2018年北京市为例,如果月薪一万,那么个人所要缴纳的五险一金为2223元。具体计算方法如下图所示,


img


住房公积金:


住房公积金是一项强制性的储蓄制度,员工每月交纳公积金,单位也会给员工交纳相同金额的公积金,存入员工的公积金账户。这笔钱可以用于购房、还房贷、自己盖房或租房等住房相关支出。不同地区的公积金缴纳比例和政策也不尽相同。


关于公积金,你需要注意以下问题:


不买房子,住房公积金可以取出来吗?


公积金大家最关心的,就是能不能不买房子可以把这笔钱取出来?基本上现在都是可以取出来的,主要看当地的满足条件。


公积金存的越多购房贷款就越多吗?


是的。连续缴满半年就可以公积金贷款,能贷款多少也跟你的公积金余额和缴存比例有很大的关系。但是无论你薪资多高,公积金的缴存比例不得超过 12%。


试用期不缴五险一金?


《中华人民共和国劳动合同法》、《住房公积金管理条例》清楚表明,确定劳动关系后,用人单位就要为职工缴纳社保和公积金。


企业不缴五险一金或者强迫你签订不缴社保的协议都是违反劳动法的!


如果你遇到这样的公司,可以考虑拿起法律武器保护自己的权益,收集好相关证据(合同、工资条、考勤记录等)去当地的人力资源和社会保障局申请劳动仲裁


五险一金中断怎么办?


如果你找到了新工作:那么养老保险、医疗保险、失业保险、工伤保险、生育保险五险都可以转移到新公司。


如果你辞职了,没有新的接收公司,也都可以找代缴公司给你代缴上述保险。


但是注意!医疗保险要及时补缴,因为**一断医保就会停,当连续中断时间超过三个月,你的连续缴费年限就会清零!**会影响到以后生大病的门诊报销比例及各项额度。


住房公积金中断后可以补缴,但自己补缴不了。只能在找到新公司后,填写好补缴材料给公司的人事部门,再让公司的人去办理补缴手续。


那么你能贷多少?


第一次办理。如果夫妻二人共同贷款,贷款最高限额 70 万,如果只有单人贷款,最高限额 45 万。


第二次办理。如果夫妻二人共同贷款,贷款最高限额 50 万,如果只有单人贷款,最高限额 30 万


应届生找工作的例子:


假设小明是一名应届大学毕业生,他找到了一家公司,并在面试时商定了月薪1万。然而,当他拿到第一个月的工资时,发现实际到手只有7000元。为什么会出现这种情况呢?


经过了解,小明发现差额是因为公司依法为他缴纳了五险一金,而这部分费用被从他的薪资中扣除了。具体来说,他需要缴纳养老保险、医疗保险和失业保险,而单位为他缴纳了五险的全部费用。


养老保险缴纳比例为8%,医疗保险缴纳比例为2%,失业保险缴纳比例为0.2%,这些费用按照小明的月薪进行扣除,导致他实际到手的薪资较少。


尽管初时看起来差额较大,但五险一金对员工未来的福利和保障有着重要作用。养老保险可以为他的退休生活提供保障,医疗保险可以在生病时获得一定程度的报销和救助,失业保险可以在意外失业时提供一定的经济支持。


因此,尽管五险一金会让员工的实际到手薪资较少,但从长远来看,这些社会保险和公积金将为员工的未来提供重要的帮助和保障。所以,在面试或入职时,了解清楚五险一金的具体细则是非常重要的,而不仅仅关注月薪本身。


总之,五险一金是员工福利的重要组成部分,也是雇主合法义务,而对于应届生来说,了解这些内容对于未来的职业生涯和生活规划至关重要。在求职过程中,应该了解和询问相关的薪资和福利细则,确保自己的权益得到保障。



本文由博客一文多发平台 OpenWrite 发布!


作者:不败顽童
来源:juejin.cn/post/7258207459357933605

收起阅读 »

new 一个对象时,js 做了什么?

js
前言在 JavaScript 中, 通过 new 操作符可以创建一个实例对象,而这个实例对象继承了原对象的属性和方法。因此,new 存在的意义在于它实现了 JavaScript 中的继承,而不仅仅是实例化了一个对象。new 的作用我们先通过例子来了解 ...
继续阅读 »

前言

在 JavaScript 中, 通过 new 操作符可以创建一个实例对象,而这个实例对象继承了原对象的属性和方法。因此,new 存在的意义在于它实现了 JavaScript 中的继承,而不仅仅是实例化了一个对象。

new 的作用

我们先通过例子来了解 new 的作用,示例如下:

function Person(name) {
this.name = name
}
Person.prototype.sayName = function () {
console.log(this.name)
}
const t = new Person('小明')
console.log(t.name) // 小明
t.sayName() // 小明

从上面的例子中我们可以得出以下结论:

  • new 通过构造函数 Person 创建出来的实例对象可以访问到构造函数中的属性。

  • new 通过构造函数 Person 创建出来的实例可以访问到构造函数原型链中的属性,也就是说通过 new 操作符,实例与构造函数通过原型链连接了起来。

构造函数 Person 并没有显式 return 任何值(默认返回 undefined),如果我们让它返回值会发生什么事情呢?

function Person(name) {
this.name = name
return 1
}
const t = new Person('小明')
console.log(t.name) // 小明

在上述例子中的构造函数中返回了 1,但是这个返回值并没有任何的用处,得到的结果还是和之前的例子完全一样。我们又可以得出一个结论:

构造函数如果返回原始值,那么这个返回值毫无意义。

我们再来试试返回对象会发生什么:

function Person(name) {
this.name = name
return {age: 23}
}
const t = new Person('小明')
console.log(t) // { age: 23 }
console.log(t.name) // undefined

通过上面这个例子我们可以发现,当返回值为对象时,这个返回值就会被正常的返回出去。我们再次得出了一个结论:

构造函数如果返回值为对象,那么这个返回值会被正常使用。

总结:这两个例子告诉我们,构造函数尽量不要返回值。因为返回原始值不会生效,返回对象会导致 new 操作符没有作用。

实现 new

首先我们要清楚,在使用 new 操作符时,js 做了哪些事情:

  1. js 在内部创建了一个对象
  2. 这个对象可以访问到构造函数原型上的属性,所以需要将对象与构造函数连接起来
  3. 构造函数内部的this被赋值为这个新对象(即this指向新对象)
  4. 返回原始值需要忽略,返回对象需要正常处理

知道了步骤后,我们就可以着手来实现 new 的功能了:

function _new(fn, ...args) {
const newObj = Object.create(fn.prototype);
const value = fn.apply(newObj, args);
return value instanceof Object ? value : newObj;
}

测试示例如下:

function Person(name) {
this.name = name;
}
Person.prototype.sayName = function () {
console.log(this.name);
};

const t = _new(Person, "小明");
console.log(t.name); // 小明
t.sayName(); // 小明

以上就是关于 JavaScript 中 new 操作符的作用,以及如何来实现一个 new 操作符。


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

手写一个Promise

Promise背景JavaScript这种单线程事件循环模型,异步行为是为了优化因计算量大而时间长的操作。在JavaScript中我们可以见到很多异步行为,比如计时器、ui渲染、请求数据等等。Promise的主要功能,是为异步代码提供了清晰的抽象,支持优雅地定...
继续阅读 »

Promise

背景

JavaScript这种单线程事件循环模型,异步行为是为了优化因计算量大而时间长的操作。在JavaScript中我们可以见到很多异步行为,比如计时器、ui渲染、请求数据等等。

Promise的主要功能,是为异步代码提供了清晰的抽象,支持优雅地定义和组织异步逻辑。可以用Promise表示异步执行的代码块,也可以用Promise表示异步计算的值。

Promise现在主流的翻译为“期约”,在英文里,promise还有承诺的意思,既然是承诺,那就是一种约定,这恰好就符合异步情境的需求:异步的代码不在当前的代码块中调用,而是由外部调用。既然如此,为了获取到异步代码执行的状态,或是为了拿到执行结果,就需要制定一定的规范去获取和维护,Promise A+就是对此指定的规范,Promise类型就是对Promise A+规范的实现。

过去在JavaScript中处理异步,通常会使用一层层的回调嵌套,没有一个规范、清晰的处理逻辑,造成的结果就是阅读困难、调试困难,可维护性差。

Promise A+规范设计的一套逻辑,Promise提供统一的API,可以使我们更有条理的去处理异步操作。

首先,将某个异步任务相关的代码包裹在一个代码块里,也就是Promise执行器函数的函数体中;比如下面的代码:

let p1 = new Promise((resolve, reject) => { // 执行器函数
   const add = (a, b) => a + b;
   resolve(add(1, 2));
   console.log(add(3, 4));
});

同时,针对这段异步代码的执行状态和执行结果,Promise实例内部会进行维护;

此外,Promise类型内部维护一个resolve和reject函数,用于维护状态的更新,以及调用处理程序将异步执行结果传递给用户进行后续处理,这些处理程序由用户自己定义。

Promise类型实现了Thenable接口,用户可以通过Promise的实例方法then来新增处理程序

当用Promise指代异步执行的代码块时,他涉及异步代码执行的三种状态:进行中等待结果的pending、成功执行fulfilled(一般也用resolved)、执行失败或出现异常rejected。当一个Promise实例被初始化时,其对应的异步代码块就进入进行中的状态,也就是说pending是初始状态。

当代码块执行完毕或者出现异常,将得到最终的一个确定状态,resolved或者rejected,和执行结果,并且不能被再次更新。

Promise的基本使用

let p = new Promise((resolve, reject) => {
   const add = (a, b) => a + b;
   resolve(add(1, 2));
   console.log(add(3, 4));
});
p.then(res => {
   console.log(res);
});

简易版Promise

针对Promise的基本使用,可以实现一个简易版的Promise

首先是状态常量的维护,以便于开发和后期维护:

const PENDING = 'pending';
const RESOLVED = 'resolved';
const REJECTED = 'rejected';

然后定义我们自己的MyPromise,维护Promise实例对象的属性

function MyPromise(fn) {
const that = this;
   that.state = PENDING;
   that.value = null;
   that.resolvedCallbacks = [];
   that.rejectedCallbacks = [];
}
  • 首先是state,表示异步代码块执行的状态,初始状态为pending
  • value变量用于维护异步代码执行的结果
  • resolvedCallbacks用于维护部分的处理程序,处理成功执行的结果
  • rejectedCallbacks用于维护另一部分的处理程序,处理的是执行失败的结果

内部使用常量that是因为,代码可能会异步执行,这用于获取正确的this。

接下来定义resolve和reject函数,添加在MyPromise函数体内部

function resolve(value) {
   if (that.state === PENDING) {
       that.state = RESOLVED;
       that.value = value;
       that.resolvedCallbacks.forEach(cb => cb(value));
  }
}

function reject(reason) {
   if (that.state === PENDING) {
       that.state = REJECTED;
       that.value = reason;
       that.rejectedCallbacks.forEach(cb => cb(reason));
  }
}
  • 首先这两个函数都得判断当前状态是否为pending,因为状态落定后不允许再次修改
  • 如果判断为pending,就更新为对应状态,并且将异步执行结果维护到Promise实例的value属性上
  • 最后遍历处理程序,并传入异步结果挨个执行

当然传递给Promise的执行器函数fn也得执行

try {
   fn(resolve, reject);
} catch (e) {
   reject(e);
}

执行器函数接收两个函数类型的参数,实际传入的就是前面定义的resolve和reject。另外,执行函数的过程中可能会抛出异常,需要捕获并执行reject函数。

最后实现较为复杂的then函数

MyPromise.prototype.then = function (onResolved, onRejected) {
   const that = this;
   onResolved = typeof onResolved === 'function' ? onResolved: v => v;
   onRejected = typeof onRejected === 'function'
       ? onRejected
      : r => {
           throw r;
      };
   if (that.state === PENDING) {
       that.resolvedCallbacks.push(onResolved);
       that.rejectedCallbacks.push(onRejected);
  }
   if (that.state === RESOLVED) {
       onResolved(that.value);
  }
   if (that.state === REJECTED) {
       onRejected(that.value);
  }
}
  • 首先判断两个参数是否为函数类型,因为这两个参数是可选参数。
  • 当参数不是函数类型时,就创建一个函数赋值给对应的参数,实现透传
  • 然后是状态的判断,当Promise的状态是等待结果pending时,就会将处理程序维护到Promise实例内部的处理程序的数组中,resolvedCallbacks和rejectedCallbacks,如果不是pending,就去执行对应状态的处理程序。

至此就实现了一个简易版本的MyPromise,可以进行测试:

let p = new MyPromise((resolve, reject) => {
   const add = (a, b) => a + b;
   resolve(add(1, 2));
   console.log(add(3, 4));
});
p.then(res => {
   console.log(res);
});

进阶版Promise

根据promise的使用经验,我们知道promise解析异步结果是一个微任务,并且promise的原型方法then会返回一个promise类型的值,这些简易版中都没有实现,为了使我们的MyPromise更符合Promise A+的规范,我们需要对简易版进行改造。

首先是resolvereject函数,这两个函数中的代码会被推入微任务的队列中等待执行

  function resolve(value) {
       if (value instanceof MyPromise) {
           return value.then(resolve, reject);
      }

       // 调用queueMicrotask,将代码插入微任务的队列
       queueMicrotask(() => {
           if (that.state === PENDING) {
               that.state = RESOLVED;
               that.value = value;
               that.resolvedCallbacks.forEach(cb => cb(value));
          }
      })
  }

   function reject(reason) {
       queueMicrotask(() => {
           if (that.state === PENDING) {
               that.state = REJECTED;
               that.value = reason;
               that.rejectedCallbacks.forEach(cb => cb(reason));
          }
      });
  }
  • 对于resolve函数,我们首先需要判断传入的值是否为Promise类型,如果是,则要得到x最终的异步执行结果再继续执行resolve和reject
  • 此处使用queueMicrotask方法将代码推入微任务队列

接下来继续改造then函数中的代码

  • 首先新增一个变量promise2用于返回,因为每个then函数都需要返回一个新的Promise对象,该变量就用于保存新的返回对象

    let promise2; // then方法必须返回一个promise
  • 然后先改造pending状态的逻辑

    if (that.state === PENDING) {
       return promise2 = new MyPromise((resolve, reject) => {
           that.resolvedCallbacks.push(() => {
               try {
                   const x = onResolved(that.value); // 执行原promise的成功处理程序,如果未定义就透传
                   // 如果正常得到一个解决值x,即onResolved的返回值,就解决新的promise2,即调用resolutionProcedure函数,这是对[[Resolve]](promise, x)的实现
                   // 将新创建的promise2,处理程序返回结果x,以及与promise2关联的resolve和reject函数作为参数传递给 这个函数
                   resolutionProcedure(promise2, x, resolve, reject);
              } catch(r) { // 如果onResolved程序执行过程中抛出异常,promise2就被标记为失败,执行reject
                   reject(r);
              }
          });
           that.rejectedCallbacks.push(() => {
               try {
                   const x = onRejected(that.value); // 执行原promise的失败处理程序,如果未定义就抛出异常
                   resolutionProcedure(promise2, x, resolve, reject); // 解决新的promise2
              } catch(r) {
                   reject(r);
              }
          });
      })
    }

    整体来看下:

    • 首先创建新的Promise实例,传入执行器函数
    • 大致逻辑还是和之前一样,往回调数组中push处理程序,只是除了onResolved函数之外,还做了一些额外操作
    • 首先在onResolved和onRejected函数调用的时候包裹了一层try/catch用于处理异常,如果出现异常,promise2就被标记为失败,执行其关联的reject函数
    • 如果onResolved和onRejected正常执行,就调用resolutionProcedure函数去解决promise2
  • 继续改造resolved状态的逻辑

    if (that.state === RESOLVED) {
       return promise2 = new MyPromise((resolve, reject) => {
           queueMicrotask(() => {
               try {
                   const x = onResolved(that.value);
                   resolutionProcedure(promise2, x, resolve, reject);
              } catch (r) {
                   reject(r);
              }
          });
      })
    }
    • 这段代码和pending的逻辑基本一致,不同之处在于,这里直接将处理程序插入微任务队列,而不是push进回调数组
    • rejected状态的逻辑基本也类似

最后就是实现上述代码中所调用的resolutionProcedure函数,用于解决promise2

function resolutionProcedure(promise2, x, resolve, reject) {}
  • 首先规范规定了x不能与promise2相等,否则会发生循环引用的问题

    if (promise2 === x) { // 如果x和promise2相等,以 TypeError 为拒因 拒绝执行 promise2
       return reject(new TypeError('Error'));
    }
  • 接着判断x的类型是否为promise

    if (x instanceof MyPromise) { // 如果x为Promise类型,则使 promise2 接受 x 的状态
       x.then(function (value) {
           // 等到x状态落定后,再去解决promise2,也就是递归调用resolutionProcedure这个函数
           resolutionProcedure(promise2, value, resolve, reject);
      }, reject/*如果x落定为拒绝状态,就用同样的拒因拒绝promise2*/);
    }
  • 处理x的类型不是promise的情况

    首先创建一个变量called用于标识是否调用过函数

    let called = false;
    if (x !== null && (typeof x === 'object' || typeof x === 'function')) { // 如果x为对象或函数类型
       try {
           let then = x.then; // 取出x上的then属性
           if (typeof then === 'function') { // 判断then的类型是否为函数,进行调用
            // 根据规范可知,在then调用时,要将this指向x,所以这里使用call对then函数进行调用
               // then接收两个函数类型的参数,第一个参数叫做resolvePromise,第二个参数叫做rejectPromise
               // 如果resolvePromise被执行,则去解决promise2,如果rejectPromise被调用,则promise2被认为失败,会调用其关联的reject函数
               then.call(
                   x, // 将this指向x
                   y => { // 第一个参数叫做resolvePromise
                       if (called) return;
                       called = true;
                       resolutionProcedure(promise2, y, resolve, reject);
                  },
                   r => { // 第二个参数叫做rejectPromise
                       if (called) return;
                       called = true;
                       reject(r);
                  }
              )
          } else { // 如果then不是函数,就将x传递给resolve,执行promise2的resolve函数
               resolve(x);
          }
      } catch (e) { // 如果上述代码抛出异常,则认为promise2失败,执行其关联的reject函数
           if (called) return;
           called = true;
           reject(e);
      }
    } else { // 如果x不是对象或函数,就将x传递给promise2关联的resolve并执行
       resolve(x);
    }

至此resolutionProcedure函数就完成了,最终会执行promise2关联的resolve或者reject函数。之所以说关联,是因为这两个函数中有对实例的引用。

到这为止,进阶版的promise就基本完成了,可以来试用一下:

let p = new MyPromise((resolve, reject) => {
   const add = (a, b) => a + b;
   resolve(add(1, 2));
   console.log(add(3, 4));
});
p.then(res => {
   console.log(res);
   return res;
}).then(res => {
   console.log(res);
});
p.then(res => {
   return {
       name: 'x',
       then: function (resolvePromise, rejectPromise) {
           resolvePromise(this.name + res);
      }
  }
}).then(res => {
   console.log(res);
})

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

前端同事最讨厌的后端行为,看看你中了没有

前端吐槽:后端从不自测接口,等到前后端联调时,这个接口获取不到,那个接口提交不了,把前端当成自己的接口测试员,耽误前端的开发进度。听到这个吐槽,仿佛看到曾经羞愧的自己。这个毛病以前我也有啊,有些接口,尤其是大表单提交接口,表单特别大,字段很多,有时候就偷懒了,...
继续阅读 »

前端吐槽:后端从不自测接口,等到前后端联调时,这个接口获取不到,那个接口提交不了,把前端当成自己的接口测试员,耽误前端的开发进度。

听到这个吐槽,仿佛看到曾经羞愧的自己。这个毛病以前我也有啊,有些接口,尤其是大表单提交接口,表单特别大,字段很多,有时候就偷懒了,直接编译过了,就发到测试环境了。前端同时联调的时候一调接口,异常了。

好在后来改了,毕竟让人发现自己接口写的有问题,也是一件丢脸的事儿。

但是我还真见过后端的同学,写完接口一个都不测,直接发测试环境的。

我就碰到过厉害的,编译都不过,就直接提代码。以前,有个新来的同事,分了任务就默默的干着,啥也不问,然后他做的功能测试就各种发现问题。说过之后,就改一下,但是基本上还是不测试,本想再给他机会的,所以后来他每次提代码,我都review一下。直到有一天,我发现忍不了了,他把一段全局配置给注释了,然后把代码提了,我过去问他是不是本地调试,忘了取消注释了。他的回答直接让我震惊了,他说:不是的,是因为不注释那段代码,我本地跑步起来,所以肯定是那段代码有问题,所以就注释了。

然后,当晚,他就离职了。

解决方式

对于这种大表单类似的问题,应该怎么处理呢?

好像没有别的方法,只能克服自己的懒惰,为自己写的代码负责。就想着,万一接口有问题,别人可能会怀疑你水平不行,你水平不行,就是你不行啊,程序员怎么能不行呢。

你可以找那么在线 Java Bean转 JSON的功能,直接帮你生成请求参数,或者现在更可以借助 ChatGPT ,帮你生成请求参数,而且生成的参数可能比你自己瞎填的看上去更合理。

或者,如果是小团队,不拘一格的话,可以让前端的同事把代码提了,你本地跑着自测一下,让前端同事先做别的功能,穿插进行也可以。

前端吐槽:后端修改了字段或返回结构不通知前端

这个就有点不讲武德了。

正常情况下,返回结构和字段都是事先约定好的,一般都是先写接口,做一些 Mock 数据,然后再实现真实的逻辑。

除了约定好返回字段和结构外,还包括接口地址、请求方法、头信息等等,而且一个项目都会有项目接口规范,同一类接口的返回字段可能有很多相同的部分。

后端如果改接口,必须要及时通知前端,这其实应该是正常的开发流程。后端改了接口,不告诉前端,到时候测试出问题了,一般都会先找前端,这不相当于让前端背锅了吗,确实不地道啊。

后端的同学们,谨记啊。

前端吐槽:为了获取一个信息,要先调用好几个接口,可能参数还是相同的

假设在一个详情页面,以前端的角度就是,我获取详情信息,就调用详情接口好了,为什么调用详情接口之前,要调用3、4个其他的接口,你详情里需要啥参数,我直接给你传过去不就好了吗。

在后端看来可能是这样的,我这几个接口之前就写好了,前端拿过去就能用,只不过就是多调几次罢了,没什么大不了的吧。

有些时候,可能确实是必须这么做的,比如页面内容太多,有的部分查询逻辑复杂,比较耗时,这时候需要异步加载,这样搞确实比较好。

但是更多时候其实就是后端犯懒了,不想再写个新接口。除了涉及到性能的问题,大多数逻辑都应该在后端处理,能用一个接口处理完,就不应该让前端多调用第二个接口。

有前端的朋友曾经问过我,他说,他们现在做的系统中有些接口是根据用户身份来展示数据的,但是前端调用登录接口登录系统后,在调用其他接口的时候,除了在 Header 中加入 token 外,还有传很多关于用户信息的很多参数,这样做是不是不合理的。

这肯定不合理,token 本来就是根据用户身份产生的,后端拿到 token 就能获取用户信息,这是常识问题,让前端在接口中再传一遍,既不合理也不安全。

类似的问题还有,比如后端接口返回一堆数据,然后有的部分有用、有的部分没有,有的部分还涉及到逻辑,不借助文档根本就看不明白怎么用,这其实并不合理。

接口应该尽量只包含有用的部分,并且尽可能结构清晰,配合简单的字段说明就能让人明白是怎么回事,是最好的效果。

如果前后端都感觉形势不对了,后端一个接口处理性能跟不上了,前端处理又太麻烦了。这时候就要向上看了,产品设计上可能需要改一改了。

后端的同学可以学一点前端,前端的同学也可以学一点后端,当你都懂一些的时候,就能两方面考虑了,这样做出来的东西可能会更好用一点。总之,前后端相互理解,毕竟都是为了生活嘛。


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

你的代码不堪一击!太烂了!

前言小王,你的页面白屏了,赶快修复一下。小王排查后发现是服务端传回来的数据格式不对导致,无数据时传回来不是 [] 而是 null, 从而导致 forEach 方法报错导致白屏,于是告诉测试,这是服务端的错误导致...
继续阅读 »

前言

小王,你的页面白屏了,赶快修复一下。小王排查后发现是服务端传回来的数据格式不对导致,无数据时传回来不是 [] 而是 null, 从而导致 forEach 方法报错导致白屏,于是告诉测试,这是服务端的错误导致,要让服务端来修改,结果测试来了一句:“服务端返回数据格式错误也不能白屏!!” “好吧,千错万错都是前端的错。” 小王抱怨着把白屏修复了。

刚过不久,老李喊道:“小王,你的组件又渲染不出来了。” 小王不耐烦地过来去看了一下,“你这个属性data 格式不对,要数组,你传个对象干嘛呢。”老李反驳: “ 就算 data 格式传错,也不应该整个组件渲染不出来,至少展示暂无数据吧!” “行,你说什么就是什么吧。” 小王又抱怨着把问题修复了。

类似场景,小王时不时都要经历一次,久而久之,大家都觉得小王的技术太菜了。小王听到后,倍感委屈:“这都是别人的错误,反倒成为我的错了!”

等到小王离职后,我去看了一下他的代码,的确够烂的,不堪一击!太烂了!下面来吐槽一下。

一、变量解构一解就报错

优化前

const App = (props) => {
const { data } = props;
const { name, age } = data
}

如果你觉得以上代码没问题,我只能说你对你变量的解构赋值掌握的不扎实。

解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于 undefined 、null无法转为对象,所以对它们进行解构赋值时都会报错。

所以当 data 为 undefined 、null 时候,上述代码就会报错。

优化后

const App = (props) => {
const { data } = props || {};
const { name, age } = data || {};
}

二、不靠谱的默认值

估计有些同学,看到上小节的代码,感觉还可以再优化一下。

再优化一下

const App = (props = {}) => {
const { data = {} } = props;
const { name, age } = data ;
}

我看了摇摇头,只能说你对ES6默认值的掌握不扎实。

ES6 内部使用严格相等运算符(===)判断一个变量是否有值。所以,如果一个对象的属性值不严格等于 undefined ,默认值是不会生效的。

所以当 props.data 为 null,那么 const { name, age } = null 就会报错!

三、数组的方法只能用真数组调用

优化前:

const App = (props) => {
const { data } = props || {};
const nameList = (data || []).map(item => item.name);
}

那么问题来了,当 data 为 123 , data || [] 的结果是 123123 作为一个 number 是没有 map 方法的,就会报错。

数组的方法只能用真数组调用,哪怕是类数组也不行。如何判断 data 是真数组,Array.isArray 是最靠谱的。

优化后:

const App = (props) => {
const { data } = props || {};
let nameList = [];
if (Array.isArray(data)) {
nameList = data.map(item => item.name);
}
}

四、数组中每项不一定都是对象

优化前:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => `我的名字是${item.name},今年${item.age}岁了`);
}
}

一旦 data 数组中某项值是 undefined 或 null,那么 item.name 必定报错,可能又白屏了。

优化后:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => `我的名字是${item?.name},今年${item?.age}岁了`);
}
}

? 可选链操作符,虽然好用,但也不能滥用。item?.name 会被编译成 item === null || item === void 0 ? void 0 : item.name,滥用会导致编辑后的代码大小增大。

二次优化后:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
}

五、对象的方法谁能调用

优化前:

const App = (props) => {
const { data } = props || {};
const nameList = Object.keys(data || {});
}

只要变量能被转成对象,就可以使用对象的方法,但是 undefined 和 null 无法转换成对象。对其使用对象方法时就会报错。

优化后:

const _toString = Object.prototype.toString;
const isPlainObject = (obj) => {
return _toString.call(obj) === '[object Object]';
}
const App = (props) => {
const { data } = props || {};
const nameList = [];
if (isPlainObject(data)) {
nameList = Object.keys(data);
}
}

六、async/await 错误捕获

优化前:

import React, { useState } from 'react';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const res = await queryData();
setLoading(false);
}
}

如果 queryData() 执行报错,那是不是页面一直在转圈圈。

优化后:

import React, { useState } from 'react';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
try {
const res = await queryData();
setLoading(false);
} catch (error) {
setLoading(false);
}
}
}

如果使用 trycatch 来捕获 await 的错误感觉不太优雅,可以使用 await-to-js 来优雅地捕获。

二次优化后:

import React, { useState } from 'react';
import to from 'await-to-js';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const [err, res] = await to(queryData());
setLoading(false);
}
}

七、不是什么都能用来JSON.parse

优化前:

const App = (props) => {
const { data } = props || {};
const dataObj = JSON.parse(data);
}

JSON.parse() 方法将一个有效的 JSON 字符串转换为 JavaScript 对象。这里没必要去判断一个字符串是否为有效的 JSON 字符串。只要利用 trycatch 来捕获错误即可。

优化后:

const App = (props) => {
const { data } = props || {};
let dataObj = {};
try {
dataObj = JSON.parse(data);
} catch (error) {
console.error('data不是一个有效的JSON字符串')
}
}

八、被修改的引用类型数据

优化前:

const App = (props) => {
const { data } = props || {};
if (Array.isArray(data)) {
data.forEach(item => {
if (item) item.age = 12;
})
}
}

如果谁用 App 这个函数后,他会搞不懂为啥 data 中 age 的值为啥一直为 12,在他的代码中找不到任何修改 data中 age 值的地方。只因为 data 是引用类型数据。在公共函数中为了防止处理引用类型数据时不小心修改了数据,建议先使用 lodash.clonedeep 克隆一下。

优化后:

import cloneDeep from 'lodash.clonedeep';

const App = (props) => {
const { data } = props || {};
const dataCopy = cloneDeep(data);
if (Array.isArray(dataCopy)) {
dataCopy.forEach(item => {
if (item) item.age = 12;
})
}
}

九、并发异步执行赋值操作

优化前:

const App = (props) => {
const { data } = props || {};
let urlList = [];
if (Array.isArray(data)) {
data.forEach(item => {
const { id = '' } = item || {};
getUrl(id).then(res => {
if (res) urlList.push(res);
});
});
console.log(urlList);
}
}

上述代码中 console.log(urlList) 是无法打印出 urlList 的最终结果。因为 getUrl 是异步函数,执行完才给 urlList 添加一个值,而 data.forEach 循环是同步执行的,当 data.forEach 执行完成后,getUrl 可能还没执行完成,从而会导致 console.log(urlList) 打印出来的 urlList 不是最终结果。

所以我们要使用队列形式让异步函数并发执行,再用 Promise.all 监听所有异步函数执行完毕后,再打印 urlList 的值。

优化后:

const App = async (props) => {
const { data } = props || {};
let urlList = [];
if (Array.isArray(data)) {
const jobs = data.map(async item => {
const { id = '' } = item || {};
const res = await getUrl(id);
if (res) urlList.push(res);
return res;
});
await Promise.all(jobs);
console.log(urlList);
}
}

十、过度防御

优化前:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
const info = infoList?.join(',');
}

infoList 后面为什么要跟 ?,数组的 map 方法返回的一定是个数组。

优化后:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
const info = infoList.join(',');
}

后续

以上对小王代码的吐槽,最后我只想说一下,以上的错误都是一些 JS 基础知识,跟任何框架没有任何关系。如果你工作了几年还是犯这些错误,真的可以考虑转行。


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

多个AAR打包成一个AAR

AAR
1. 背景介绍公司日常开发基于自建的Maven服务器,不对外开放,公司内开发的SDK都传到私服,经过这么多年的迭代已经有上百个包,前段时间有其他公司需要依赖内部某个SDK,而这个SDK有依赖了公司好多SDK,但是公司内网权限无法对外开放,所以无法使用Maven...
继续阅读 »

1. 背景介绍

公司日常开发基于自建的Maven服务器,不对外开放,公司内开发的SDK都传到私服,经过这么多年的迭代已经有上百个包,前段时间有其他公司需要依赖内部某个SDK,而这个SDK有依赖了公司好多SDK,但是公司内网权限无法对外开放,所以无法使用Maven方式对外提供依赖,如果基于AAR方式,对外提供十几个AAR不仅不友好,而且内部也不好维护迭代。

2. 解决思路及办法

市面上有一套开源的合并AAR的方案,合并AAR主要的步骤:

  • AndroidManifest合并
  • Classes合并
  • Jar合并
  • Res合并
  • Assets合并
  • Jni合并
  • R.txt合并
  • R.class合并
  • DataBinding合并
  • Proguard合并
  • Kotlin module合并

这些都有对应Gradle task,具体方案可以看对应源码:adwiv/android-fat-aar目前已不再维护,gradle不支持高版本,kezong/fat-aar-android虽然也不在维护,但是已经适配了AGP 3.0 - 7.1.0,Gradle 4.9 - 7.3。

3. 遇到问题

3.1 资源冲突

如果library和module中含有同名的资源(比如 string/app_name),编译将会报duplication resources的相关错误,有两种方法可以解决这个问题:

  • 将library以及module中的资源都加一个前缀来避免资源冲突(不是所有历史版本的SDK都遵循这个规范);
  • gradle.properties中添加android.disableResourceValidation=true可以忽略资源冲突的编译错误,程序会采用第一个找到的同名资源作为实际资源(资源覆盖可能会导致某些错误)

3.2 动态库冲突

在application中动态库冲突可以使用pickFirst指定第一个,但是这个无法适用于library中。

关于packagingOptions常见的设置项有exclude、pickFirst、doNotStrip、merge。

1. exclude,过滤掉某些文件或者目录不添加到APK中,作用于APK,不能过滤aar和jar中的内容。

比如:

packagingOptions {
exclude 'META-INF/**'
exclude 'lib/arm64-v8a/libopus.so'
}

2. pickFirst,匹配到多个相同文件,只提取第一个。只作用于APK,不能过滤aar和jar中的文件。

比如:

 packagingOptions {
pickFirst "lib/armeabi-v7a/libopus.so"
pickFirst "lib/armeabi-v7a/libopus.so"
}

3. doNotStrip,可以设置某些动态库不被优化压缩。

比如:

 packagingOptions{
doNotStrip "*/armeabi/*.so"
doNotStrip "*/armeabi-v7a/*.so"
}

4. merge,将匹配的文件都添加到APK中,和pickFirst有些相反,会合并所有文件。

比如:

packagingOptions {
merge '**/LICENSE.txt'
merge '**/NOTICE.txt'
}

最后针对包含冲突动态库的SDK,单独对外依赖,在application中pickfirst,暂时没有特别好的方法。

3.3 外部依赖库

SDK中有些依赖的是外部公共仓库,比如OKHTTP等,如果都合并到同一的AAR,会导致外部依赖不够灵活,我们的思路是合并的时候不合并外部SDK,只打包公司内部SDK,并打印外部依赖的SDK,提供给外部手动依赖:

  1. 先定义内部SDK规则方法:
static boolean isInnerDep(RenderableDependency dep) {
return (dep.name.contains("com.xxx")
|| dep.name.contains("com.xxxxx")
|| dep.name.contains("com.xxxxxxx")
|| dep.name.contains("com.xxxxxxxx"))
}
  1. 定义三个集合:
//所有的内部库依赖
Map<String, String> allInnerDeps = new HashMap<>()
//所有的非内部依赖:公共平台库
Map<String, String> allCommonDeps = new HashMap<>()
//库的类型,jar 或者 aar,依赖方式不同
Map<String, String> depType = new HashMap<>()
  1. 分析依赖,放到不同集合打印、合并:

void collectDependencies(Map<String, String> commonDependencies, Map<String, String> innerDependencies, RenderableDependency result) {
String depName = result.name.substring(0, result.name.lastIndexOf(":"))
// println "denName = " + depName
String version = result.name.substring(result.name.lastIndexOf(":") + 1, result.name.length())

if (result.getChildren() != null && result.getChildren().size() > 0) {
if (isInnerDep(result) && !isExcludeDep(result)) {
tryToAdd(innerDependencies, depName, version)
result.getChildren().each {
res ->
collectDependencies(commonDependencies, innerDependencies, res)
}
} else {
tryToAdd(commonDependencies, depName, version)
}
} else {
if (isInnerDep(result) && !isExcludeDep(result)) {
tryToAdd(innerDependencies, depName, version)
} else {
tryToAdd(commonDependencies, depName, version)
}
}
}

configurations.findAll { conf ->
return conf.name == "implementation" || conf.name == "api"
}.each {
conf ->
// println "--------------"+conf.name
def copyConf = conf.copy()
copyConf.setCanBeResolved(true)
copyConf.each {
file ->
String s = file.name.substring(0, file.name.lastIndexOf("."))
String key
if (s.contains("-SNAPSHOT")) {
String t = (s.substring(0, s.lastIndexOf("-SNAPSHOT")))
key = t.substring(0, t.lastIndexOf("-"))
} else {
key = s.substring(0, s.lastIndexOf("-"))
}
String value = file.name.substring(file.name.lastIndexOf("."), file.name.length())
depType.put(key, value)
}
ResolutionResult result = copyConf.getIncoming().getResolutionResult()
RenderableDependency depRoot = new RenderableModuleResult(result.getRoot())
depRoot.getChildren().each {
d ->
collectDependencies(allCommonDeps, allInnerDeps, d)
}

}
println("==================内部依赖====================")

allInnerDeps.each {
dep ->
println dep.key + ":" + dep.value

dependencies {
String key = dep.key.substring(dep.key.lastIndexOf(":") + 1, dep.key.length())
String type = depType.get(key)
if (type == ".aar") {
embed(dep.key + ":" + dep.value + "@aar")
} else {
embed(dep.key + ":" + dep.value)
}
}
}

println "=====================正确使用 sdk,需要添加如下依赖========================"
allCommonDeps.each {
dep ->
println "api " + """ + dep.key + ":" + dep.value + """
}

3.4 对外提供多个业务SDK

我们提供一个同一AAR后,另一个业务也要对外提供SDK,这样有公共依赖的就会有冲突问题,如果都合并成一个,某一方改动,势必会引起另一方回归测试,最后抽取公共的sdk合并成一个aar,各自业务合并各自的AAR。

4. 参考资料

使用fat-aar编译打包多个aar库 - 简书

fat-aar实践及原理分享 - 简书

github.com/kezong/fat-…

GitHub - adwiv/android-fat-aar: Gradle script that allows you to merge and embed dependencies in generted aar file

5. 总结

本文介绍了Android对外输出AAR和不依赖maven,通过合并多个AAR的方式减少依赖方成本,并介绍了实际使用过程中遇到的问题和解决方案。


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

那些隐藏在项目中的kotlin小知识,在座各位...

写kotlin越来越久,很多代码虽然能看懂,并且能去改,但是不知道他用了啥,里面的原理是什么,举个例子?大家一起学习一下吧内联函数顾名思义,但是在项目中我遇到得很少,他比较适用于一些包装方法的写法,比如下面这个inline fun measureTimeMil...
继续阅读 »

写kotlin越来越久,很多代码虽然能看懂,并且能去改,但是不知道他用了啥,里面的原理是什么,举个例子?大家一起学习一下吧

内联函数

顾名思义,但是在项目中我遇到得很少,他比较适用于一些包装方法的写法,比如下面这个

inline fun measureTimeMillis(block: () -> Unit): Long {
val startTime = System.currentTimeMillis()
block()
return System.currentTimeMillis() - startTime
}

val time = measureTimeMillis {
// code to be measured here
}
println("Time taken: $time ms")

这样的函数,以后如果想要测一段代码的运行时间,只需要将measureTimeMillis包着他就行

类型别名

一个很神奇的东西,允许为现有类型定义新名称

data class Person(val name: String, val age: Int)
typealias People = List<Person>

val people: People = listOf(
Person("Alice", 25),
Person("Bob", 30),
Person("Charlie", 35)
)

fun findOlderThan(people: People, age: Int): People {
return people.filter { it.age > age }
}

fun main() {
val olderPeople = findOlderThan(people, 30)
println(olderPeople)
}

其中People就是一个别名,如果使用typealias替代直接定义list,项目中就会少很多后缀为list的列表,少了类似于personlist这种变量,在搜索,全局替换,修改时也会更加直观看到person和people的区分场景

typealias可以被大量使用在list, map乃至于函数中,因为这些命名可能会比较长,替换后可以提高可读性

高阶函数

一个一开始很难理解,理解后又真香的函数,我愿称理解的那一刻为程序员进阶闪耀时,当一个老程序员回首往事时,他不会因为虚度年华而悔恨,但是一定会因为不懂高阶函数而羞耻

尤其是在项目中发现这种函数,又看不懂时,是万万不敢问同事的,所以,请现在就了解清楚吧

fun calculate(x: Int, y: Int, operation: (Int, Int) -> Int): Int {
return operation(x, y)
}

fun main() {
val sum = calculate(10, 5) { x, y -> x + y }
println("Sum is $sum")

val difference = calculate(10, 5) { x, y -> x - y }
println("Difference is $difference")
}

可以看到,calculate其实并没有做什么,只是执行了传入进来的operation,这,就是高阶函数,所谓领导也是如此,优秀的下属,往往将方案随着问题传入进来,领导只要批示一下执行operation即可

配合上lambda原则,最后一个参数可以提出到括号外面,也就是讲operation提出到外面的{}中,交给调用方自己执行,就形成了这样的写法

    val sum = calculate(10, 5) { x, y -> x + y }

理解这一点后,一下子就清晰了很多,calculate看起来什么都没做,他却成为了世界上功能最强大,最灵活,bug最少的计算两个数运算结果的函数

深入

了解上面分析,已经足够我们在kotlin项目中进阶了,现在,我们来看下高阶函数反编译后的java代码

public final class TestKt {
public static final int calculate(int x, int y, @NotNull Function2 operation) {
Intrinsics.checkNotNullParameter(operation, "operation");
return ((Number)operation.invoke(x, y)).intValue();
}

public static final void main() {
int sum = calculate(10, 5, (Function2)null.INSTANCE);
String var1 = "Sum is " + sum;
System.out.println(var1);
int difference = calculate(10, 5, (Function2)null.INSTANCE);
String var2 = "Difference is " + difference;
System.out.println(var2);
}

// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}

虽然java的实现太不优雅,但是我们可以看出,高阶函数,本质上传入的函数是一个名为Function2的对象,

public interface Function2<in P1, in P2, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2): R
}

他是kotlin包自带的函数,看起来可以用来在反编译中替换匿名lambda表达式,将其逻辑移动到自身的invoke中,然后生成一个Function2对象,这样实现kotlin反编译为java时的lambda替换

这也是高阶函数得以实现的根本原因


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