注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Flutter桌面端开发——复制和粘贴内容

复制和粘贴这个功能,一般系统都自带,简单的按几个键就能完成。但是有时候我们想要自己在应用中集成这个功能,或者想在用户复制文字后不使用粘贴操作,就让复制的内容直接出现在我们的应用中。想要实现该功能,就可以用我今天介绍的几个插件。 screen_capturer ...
继续阅读 »

复制和粘贴这个功能,一般系统都自带,简单的按几个键就能完成。但是有时候我们想要自己在应用中集成这个功能,或者想在用户复制文字后不使用粘贴操作,就让复制的内容直接出现在我们的应用中。想要实现该功能,就可以用我今天介绍的几个插件。


screen_capturer


这个方法是用来截取屏幕的。本来想写一期介绍截屏插件的,但是找了一圈只找到这个适用于桌面端,只写这一个插件篇幅又太短,所以直接加了进来。


安装🛠


点击screen_capturer获取最新版本。以下是在编写本文章时的最新版本:


screen_capturer: ^0.1.0

使用🥟


该插件的主体是 ScreenCapturer.instance ,其下有3个方法:isAccessAllowed 、requestAccess 和 capture。


isAccessAllowed 和 requestAccess仅在 macOS中适用,分别用来检测和申请截图的权限。


await ScreenCapturer.instance.requestAccess();  // 申请权限
bool _isAllowed = await ScreenCapturer.instance.isAccessAllowed (); // 检测是否拥有权限

我们截图的目的是把图片显示出来,所以在正式截图前先定义个图片路径的参数:


String _image;

接下来介绍截图的主要方法 capture,该方法可以传递2个参数:



  • String? imagePath:该属性为必填,传递一个图片保存的路径和名称

  • bool silent:设置是否开启截屏的提示音


String _imageName = '${DateTime.now().millisecondsSinceEpoch}.png';  // 设置图片名称
String _imagePath = 'C:\\Users\\ilgnefz\\Pictures\\$_imageName'; // 设置图片保存的路径
CapturedData? _capturedData = await ScreenCapturer.instance.capture(imagePath: _imagePath);
if (_capturedData != null) {
_image = _capturedData.imagePath;
setState(() {});
} else {
BotToast.showText(text: '截图被取消了');
}

1


通过运行可以发现,这里调用的其实是系统的截图功能,然后将截取的图片进行了保存。和windows自带的截图又有些差别,自带的只会将截图保存到剪切板中。


screen_text_extractor


安装🛠


点击screen_text_extractor获取最新版本。以下是在编写本文章时的最新版本:


screen_text_extractor: ^0.1.0

使用🥟


该插件的主体是 ScreenTextExtractor.instance ,其下有7个方法:



  1. isAccessAllowed:检测是否有进行相关操作的权限,仅macOS

  2. requestAccess:申请进行相关操作的权限,仅macOS

  3. extractFromClipboard:从剪切板提取内容

  4. extractFromScreenSelection:从选择的屏幕提取

  5. simulateCtrlCKeyPress:模拟 Ctrl + c ,返回一个布尔值


前面4个仅macOS使用的方法和screen_capturer一样,这里就不多赘述。后面3个方法将会返回一个 Future 的 ExtractedData 对象。


我们先定义一个String对象用来显示获取到的内容:


String _text = '获取的内容将会在这里🤪';

获取剪切板内容


ExtractedData data = await ScreenTextExtractor.instance.extractFromClipboard();
_text = data.text;
setState((){});

我们先在 windows 中按 windows键 + v 来调出剪切板,清空


无标题


使用该方法看一下:


1


我们将会得到一个空白的内容。为了更好的用户体验,我们可以添加个条件。


if (data.text!.isEmpty) {
BotToast.showText(text: '剪切板什么都没有🤨');
} else {
_text = data.text!;
setState(() {});
}

我们现在复制一段内容:


无标题


然后看看效果:


2


获取成功😀,但是剪切板除了能存储文本,还是能存储图片的。


1


但是ExtractedData只有个text属性,我们来看下会发生什么:


3


直接为空了😶


获取选区内容


ExtractedData data = await ScreenTextExtractor.instance.extractFromScreenSelection(
useAccessibilityAPIFirst: false, // 使用辅助功能API,仅macOS
);
if (data.text!.isEmpty) {
BotToast.showText(text: '剪切板什么都没有🤨');
} else {
_text = data.text!;
setState(() {});
}

👻该方法如果是在windows端,返回的就是extractFromClipboard()方法的结果,在macOS端和Linux端暂时无法演示😪


pasteboard


安装🛠


点击pasteboard获取最新版本。以下是在编写本文章时的最新版本:


pasteboard: ^0.0.2

使用🥟


该插件中的 Pasteboard 对象一共拥有4个方法:



  • image:复制图片

  • file:复制文件/文本

  • writeImage:粘贴图片

  • writeFile:粘贴文件/文本


复制粘贴文本


当然,第一步先定义一个用来存储结果的变量:


String _text = '还没粘贴任何内容';

定义一个文本控制器,用来获取输入的内容:


final TextEditingController _controller = TextEditingController();

接下来使用 pasteboard 来实现复制和粘贴的功能:




  • 复制文本


    void _copyText() async {
    if (_controller.text.isEmpty) {
    BotToast.showText(text: '啥都没输入,你要我复制什么🥴');
    } else {
    final lines = const LineSplitter().convert(_controller.text);
    await Pasteboard.writeFiles(lines);
    }
    }



  • 粘贴文本


    void _pastText() async {
    final results = await Pasteboard.files();
    if (results.isNotEmpty) {
    _text = result.toString();
    setState(() {});
    } else {
    BotToast.showText(text: '我什么都不能给你,因为我也咩有😭');
    }
    }



我们先来试一下,不用复制直接直接粘贴会发生什么。此时我的剪切板有一条内容:


无标题


来看看效果:


1


我们可以发现,它并不能读取我们剪切板的内容。试下复制再粘贴:


2


通过测试可以知道,最终的结果是一个数组。我们再来看看剪切板有没有记录:


无标题


这里其实用的是上面同一张图,因为没有变化所以就没再截图了。


通过以上内容,我们可以发现,pasteboard 的复制粘贴是和系统隔开的。


复制粘贴文件


其实代码可以不用修改,但是为了更好的显示,我们还是修改以下:


void _pastText() async {
final results = await Pasteboard.files();
if (results.isNotEmpty) {
_text = '';
for (final result in results) {
_text += '$result\n';
}
setState(() {});
} else {
BotToast.showText(text: '我什么都不能给你,因为我也咩有😭');
}
}

在这里,我使用了 url_launcher 插件,用来打开系统的文件浏览器。代码如下:


void _openExplorer() async {
const _filePath = r'C:\Users\ilgnefz\Pictures';
final Uri _uri = Uri.file(_filePath);
await launch(_uri.toString());
}

来看看效果:


image


图片本质上也是文件,可以直接使用上面的方法进行复制粘贴。所以关于图片的方法就不讲解了


(🤫ps: 其实是我使用官方例子的方法,用Base64图片进行测试,发现无法得到想要的结果。使用了官方的例子也是一样。复制图片的方法需要传递一个Uint8List参数,虽然可以使用其他方法转换,但是就变得麻烦了。以后我会出一篇关于用 CustomPaint 绘制图片的文章,里面会用到将图片转换成Uint8List对象的方法)。


clipboard


安装🛠


点击clipboard获取最新版本。以下是在编写本文章时的最新版本:


clipboard: ^0.1.3

使用🥟


该插件拥有4个方法:



  • controlC:模仿 cttr + c 键,复制

  • controlC:模仿 cttr + v 键,粘贴

  • copy:复制

  • paste:粘贴


先来看看前面两个方法:


void _useCtrC() async {
if (_controller.text.isEmpty) {
BotToast.showText(text: '啥都没输入,你要我复制什么🥴');
} else {
await FlutterClipboard.controlC(_controller.text);
}
}

void _useCtrV() async {
ClipboardData result = await FlutterClipboard.controlV();
_text = result.text.toString();
setState(() {});
}

使用 controlV 会返回一个 ClipboardData 对象。


4


后面两个方法和前面的唯一不同,就是返回的是一个 String 对象:


void _useCopy() async {
if (_controller.text.isEmpty) {
BotToast.showText(text: '啥都没输入,你要我复制什么🥴');
} else {
await FlutterClipboard.copy(_controller.text);
}
}

void _usePaste() async {
_text = await FlutterClipboard.paste();
setState(() {});
}

5


我们打开系统的剪切板可以发现,以上复制的内容都会被记录。我们试一下不按复制看能不能直接读取剪切板的信息进行粘贴:


6


试试 paste 方法:


7


🛫OK,以上就是这篇文章的全部内容,仅针对插件的当前版本,并不能保证适用于以后插件用法的更新迭代。


最后,感谢 leanflutterMixin Network 两个团队还有 samuelezedi 对以上插件的开发和维护😁。本应用代码已上传至 githubgitee,有需要的可以下载下来查看学习。


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

Flutter好用的轮子推荐03:一套精美实用的内置动画弹窗组件

前言 Flutter 是 Google 开源的应用开发框架,仅通过一套代码就能构建支持Android、iOS、Windows、Linux等多平台的应用。Flutter的性能非常高,拥有120fps的刷新率,也是目前非常流行的跨平台UI开发框架。 本专栏为大家收...
继续阅读 »

前言


Flutter 是 Google 开源的应用开发框架,仅通过一套代码就能构建支持Android、iOS、Windows、Linux等多平台的应用。Flutter的性能非常高,拥有120fps的刷新率,也是目前非常流行的跨平台UI开发框架。


本专栏为大家收集了Github上近70个优秀开源库,后续也将持续更新。希望可以帮助大家提升搬砖效率,同时祝愿Flutter的生态越来越完善🎉🎉。


正文


一、🚀 轮子介绍



  • 名称:awesome_dialog

  • 概述:一个简单易用的内置动画弹窗

  • 出版商:marcos930807@gmail.com

  • 仓库地址:awesomeDialogs

  • 推荐指数: ⭐️⭐️⭐️⭐️

  • 常用指数: ⭐️⭐️⭐️⭐️

  • 效果预览:


gif.gif


二、⚙️ 安装及使用


dependencies:
awesome_dialog: ^2.1.2

import 'package:awesome_dialog/awesome_dialog.dart';

三、🔧 常用属性






















































































































































































属性类型描述
dialogTypeDialogType设置弹窗类型
customHeaderWidget设置自定义标题(如果设置了,DiaologType将被忽略。)
widthdouble弹窗最大宽度
titleString弹窗标题
descString弹窗描述文本
bodyWidget弹窗主体,如果设置了此属性,标题和描述将被忽略。
contextBuildContext@required
btnOkTextString确认按钮的文本
btnOkIconIconData确认按钮的图标
btnOkOnPressFunction确认按钮事件
btnOkColorColor确认按钮颜色
btnOkWidget创建自定义按钮,以上确认按钮相关属性将被忽略
btnCancelTextString取消按钮的文本
btnCancelIconIconData取消按钮的图标
btnCancelOnPressFunction取消按钮事件
btnCancelColorColor取消按钮颜色
btnCancelWidget创建自定义按钮,以上取消按钮相关属性将被忽略
buttonsBorderRadiusBorderRadiusGeometry按钮圆角
dismissOnTouchOutsidebool点击外部消失
onDissmissCallbackFunction弹窗关闭回调
animTypeAnimType动画类型
aligmentAlignmentGeometry弹出方式
useRootNavigatorbool使用根导航控制器而不是当前根导航控制器,可处理跨界面关闭弹窗。
headerAnimationLoopbool标题动画是否循环播放
paddingEdgeInsetsGeometry弹窗内边距
autoHideDuration自动隐藏时间
keyboardAwarebool键盘弹出内容被遮挡时是否跟随移动
dismissOnBackKeyPressbool控制弹窗是否可以通过关闭按钮消失
buttonsBorderRadiusBorderRadiusGeometry按钮圆角
buttonsTextStyleTextStyle按钮文字风格
showCloseIconbool是否显示关闭按钮
closeIconWidget关闭按钮图标
dialogBackgroundColorColor弹窗背景色
borderSideBorderSide整个弹窗形状

四、🗂 示例


1.带有点击动画的按钮


animatedButton-2.gif


AnimatedButton(
color: Colors.cyan,
text: '这是一个带有点击动画的按钮',
pressEvent: () {},
)

2.固定宽度并带有确认 / 取消按钮的提示框


fixedWidthAndButtons.gif


AnimatedButton(
text: '固定宽度并带有确认 / 取消按钮的提示框',
pressEvent: () {
AwesomeDialog(
context: context,
dialogType: DialogType.INFO_REVERSED,
borderSide: const BorderSide(
color: Colors.green,
width: 2,
),
width: 380,
buttonsBorderRadius: const BorderRadius.all(
Radius.circular(2),
),
btnCancelText: '不予理会',
btnOkText: '冲啊!',
headerAnimationLoop: false,
animType: AnimType.BOTTOMSLIDE,
title: '提示',
desc: '一个1级bug向你发起挑衅,是否迎战?',
showCloseIcon: true,
btnCancelOnPress: () {},
btnOkOnPress: () {},
).show();
});

3.自定义按钮样式的问题对话框


questionDialogWithCustomButtons.gif


AnimatedButton(
color: Colors.orange[700],
text: '具有自定义按钮样式的问题对话框',
pressEvent: () {
AwesomeDialog(
context: context,
dialogType: DialogType.QUESTION,
headerAnimationLoop: false,
animType: AnimType.BOTTOMSLIDE,
title: '触发额外剧情',
desc: '发现一名晕倒在草丛的路人,你会?',
buttonsTextStyle: const TextStyle(color: Colors.black),
btnCancelText: '拿走他的钱袋',
btnOkText: '救助',
showCloseIcon: true,
btnCancelOnPress: () {},
btnOkOnPress: () {},
).show();
});

4.无按钮的信息提示框


noHeaderDialog.gif


AnimatedButton(
color: Colors.grey,
text: '无按钮的信息提示框',
pressEvent: () {
AwesomeDialog(
context: context,
headerAnimationLoop: true,
animType: AnimType.BOTTOMSLIDE,
title: '提示',
desc:
'你救下路人,意外发现他是一位精通Flutter的满级大佬,大佬为了向你表示感谢,赠送你了全套Flutter的学习资料...',
).show();
});

5.警示框


warningDialog.gif


AnimatedButton(
color: Colors.orange,
text: '警示框',
pressEvent: () {
AwesomeDialog(
context: context,
dialogType: DialogType.WARNING,
headerAnimationLoop: false,
animType: AnimType.TOPSLIDE,
showCloseIcon: true,
closeIcon: const Icon(Icons.close_fullscreen_outlined),
title: '警告',
desc: '意外发现bug的窝点,你准备?',
btnCancelOnPress: () {},
onDissmissCallback: (type) {
debugPrint('Dialog Dissmiss from callback $type');
},
btnCancelText: '暂且撤退',
btnOkText: '发起战斗',
btnOkOnPress: () {},
).show();
});

6.错误提示框


errorDialog.gif


AnimatedButton(
color: Colors.red,
text: '错误提示框',
pressEvent: () {
AwesomeDialog(
context: context,
dialogType: DialogType.ERROR,
animType: AnimType.RIGHSLIDE,
headerAnimationLoop: true,
title: '挑战失败',
desc: '你寡不敌众,败下阵来,(回到出生点后,拿出大佬赠送的全套学习资料,立志学成后报仇血恨... )',
btnOkOnPress: () {},
btnOkIcon: Icons.cancel,
btnOkColor: Colors.red,
).show();
});

7.成功提示框


successDialog.gif


AnimatedButton(
color: Colors.green,
text: '成功提示框',
pressEvent: () {
AwesomeDialog(
context: context,
animType: AnimType.LEFTSLIDE,
headerAnimationLoop: false,
dialogType: DialogType.SUCCES,
showCloseIcon: true,
title: '挑战成功',
desc: '经过三天三夜的苦战,你成功消灭了所有的bug',
btnOkOnPress: () {
debugPrint('OnClcik');
},
btnOkIcon: Icons.check_circle,
onDissmissCallback: (type) {
debugPrint('Dialog Dissmiss from callback $type');
},
).show();
});

8.不带顶部动画的弹窗


noHeaderDialog.gif


AnimatedButton(
color: Colors.cyan,
text: '不带顶部动画的弹窗',
pressEvent: () {
AwesomeDialog(
context: context,
headerAnimationLoop: false,
dialogType: DialogType.NO_HEADER,
title: 'No Header',
desc:'Dialog description here...',
btnOkOnPress: () {
debugPrint('OnClcik');
},
btnOkIcon: Icons.check_circle,
).show();
});

9.自定义内容弹窗


customBodyDialog.gif


AnimatedButton(
color: Colors.purple,
text: '自定义内容弹窗',
pressEvent: () {
AwesomeDialog(
context: context,
animType: AnimType.SCALE,
dialogType: DialogType.INFO,
body: const Center(
child: Text(
'If the body is specified, then title and description will be ignored, this allows to further customize the dialogue.',
style: TextStyle(fontStyle: FontStyle.italic),
),
),
title: 'This is Ignored',
desc: 'This is also Ignored',
).show();
});

10.自动隐藏弹窗


autoHideDialog.gif


AnimatedButton(
color: Colors.grey,
text: '自动隐藏弹窗',
pressEvent: () {
AwesomeDialog(
context: context,
dialogType: DialogType.INFO,
animType: AnimType.SCALE,
title: 'Auto Hide Dialog',
desc: 'AutoHide after 2 seconds',
autoHide: const Duration(seconds: 2),
).show();
});

11.测试弹窗


testingDialog.gif


AnimatedButton(
color: Colors.blue,
text: '测试弹窗',
pressEvent: () {
AwesomeDialog(
context: context,
keyboardAware: true,
dismissOnBackKeyPress: false,
dialogType: DialogType.WARNING,
animType: AnimType.BOTTOMSLIDE,
btnCancelText: "Cancel Order",
btnOkText: "Yes, I will pay",
title: 'Continue to pay?',
desc:'Please confirm that you will pay 3000 INR within 30 mins. Creating orders without paying will create penalty charges, and your account may be disabled.',
btnCancelOnPress: () {},
btnOkOnPress: () {},
).show();
});

12.文本输入弹窗


bodyWithInput.gif


AnimatedButton(
color: Colors.blueGrey,
text: '带有文本输入框的弹窗',
pressEvent: () {
late AwesomeDialog dialog;
dialog = AwesomeDialog(
context: context,
animType: AnimType.SCALE,
dialogType: DialogType.INFO,
keyboardAware: true,
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
Text('Form Data',
style: Theme.of(context).textTheme.headline6,),
const SizedBox(height: 10,),
Material(
elevation: 0,
color: Colors.blueGrey.withAlpha(40),
child: TextFormField(
autofocus: true,
minLines: 1,
decoration: const InputDecoration(
border: InputBorder.none,
labelText: 'Title',
prefixIcon: Icon(Icons.text_fields),
),
),
),
const SizedBox(height: 10,),
Material(
elevation: 0,
color: Colors.blueGrey.withAlpha(40),
child: TextFormField(
autofocus: true,
keyboardType: TextInputType.multiline,
minLines: 2,
maxLines: null,
decoration: const InputDecoration(
border: InputBorder.none,
labelText: 'Description',
prefixIcon: Icon(Icons.text_fields),
),
),
),
const SizedBox(height: 10,),
AnimatedButton(
isFixedHeight: false,
text: 'Close',
pressEvent: () {
dialog.dismiss();
},
)
],),
),
)..show();
});

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

从0-1背包问题看动态规划的几种方式

0-1 knapsack Problem Statement Given the weights and profits of ‘N’ items, we are asked to put these items in a knapsack that has ...
继续阅读 »

0-1 knapsack


Problem Statement


Given the weights and profits of ‘N’ items, we are asked to put these items in a knapsack that has a capacity ‘C’. The goal is to get the maximum profit from the items in the knapsack. Each item can only be selected once, as we don’t have multiple quantities of any item.


Items: { Apple, Orange, Banana, Melon }
Weights: { 2, 3, 1, 4 }
Profits: { 4, 5, 3, 7 }
Knapsack capacity: 5
Output: 10

暴力解法


每个 Item 都可以放或者不放,可以对每个元素一次进行递归


export function bruteforce(
profits: number[],
weights: number[],
capacity: number
) {
const recursive = (i: number, c: number, p: number): number => {
if (i >= profits.length) return p;

return Math.max(
c + weights[i] > capacity
? p
: recursive(i + 1, c + weights[i], p + profits[i]),
recursive(i + 1, c, p)
);
};

const ans = recursive(0, 0, 0);
return ans;
}

时间复杂度:O(2n)


空间复杂度:O(n)


Top-Down DP


稍微变一个 recursive 的逻辑,recursive 函数表示,把 0~i 范围内的 Item 装入 capacity 为 c 的背包中,能够获得的最大的 profit


image.png


可以看到有重复的分支,我们需要做的就是在这个基础上记忆


export function topDown(
profits: number[],
weights: number[],
capacity: number
) {
const dp: Record<string, number> = {};

const recursive = (i: number, c: number, p: number): number => {
if (i >= profits.length) return p;

const key = `${i}_${c}`;

if (dp[key]) {
return dp[key];
}

const ans = Math.max(
c - weights[i] >= 0
? recursive(i + 1, c - weights[i], p + profits[i])
: p,
recursive(i + 1, c, p)
);

return (dp[key] = ans);
};

const ans = recursive(0, capacity, 0);
return ans;
}

时间复杂度:O(n * c),因为记忆化后,最多有 n * c 个子问题


空间复杂度:O(n * c + n)


Bottom-Up DP


dp 记录对于前 i 个元素,当 capacity 为 c 时,能获得的最大 profit,那么


dp[i][c] = max (
// 不取当前元素
dp[i-1][c],
// 取当前元素
profits[i] + dp[i-1][c-weights[i]]
)

图解如下:


image.png


export function bottomUp(
profits: number[],
weights: number[],
capacity: number
) {
const N = profits.length;
const dp: number[][] = new Array(N)
.fill(0)
.map(() => new Array(capacity + 1).fill(0));

// 初始化
for (let i = 0; i < N; i++) dp[i][0] = 0;
for (let c = 0; c <= capacity; c++) {
if (weights[0] <= c) dp[0][c] = profits[0];
}

// dp
for (let i = 1; i < N; i++) {
for (let c = 1; c <= capacity; c++) {
dp[i][c] = Math.max(
dp[i - 1][c],
c < weights[i] ? 0 : profits[i] + dp[i - 1][c - weights[i]]
);
}
}

return dp[N - 1][capacity];
}

时间复杂度:O(n * c)


空间复杂度:O(n * c)


Bottom-Up DP 优化


在计算第 i 个元素的过程中,只需要用到前一次的 dp[c] and dp[c-weight[i]] ,所以,在空间复杂度上我们可以做优化,可以使用同一个数组进行前后两次的记忆


如果 capacity 从 c ~ 0 ,而不是 0 ~ c 循环,去修改 dp[i][c ~ capacity],可以确保 dp[i][0 ~ c-1] 的值是前一次的,但是,如果按照之前的反过来,计算 dp[i][c] 的时候,dp[i][0 ~ c-1] 已经变成第 i 次的值在存储,所以行不通


export function bottomUp2(
profits: number[],
weights: number[],
capacity: number
) {
const N = profits.length;
const dp: number[] = new Array(capacity + 1).fill(0);

// 初始化
for (let c = 0; c <= capacity; c++) {
if (weights[0] <= c) dp[c] = profits[0];
}

// dp
for (let i = 1; i < N; i++) {
for (let c = capacity; c >= 0; c--) {
dp[c] = Math.max(
dp[c],
c < weights[i] ? 0 : profits[i] + dp[c - weights[i]]
);
}
}

return dp[capacity];
}

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

七大跨域解决方法原理

前言 大家好,我是林三心。用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初衷。 咱们做前端的,平时跟后端对接接口那是必须的事情,但是可能很多同学忽略了一个对接过程中可能会发生的问题——跨域,那跨域到底是啥呢?为什么会跨域呢?又怎么才能解决呢...
继续阅读 »

前言


大家好,我是林三心。用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初衷。


咱们做前端的,平时跟后端对接接口那是必须的事情,但是可能很多同学忽略了一个对接过程中可能会发生的问题——跨域,那跨域到底是啥呢?为什么会跨域呢?又怎么才能解决呢?


截屏2021-10-01 上午7.16.06.png


为什么跨域?


image.png


为什么会出现跨域问题呢?那就不得不讲浏览器的同源策略了,它规定了协议号-域名-端口号这三者必须都相同才符合同源策略


截屏2021-10-01 上午8.50.11.png


如有有一个不相同,就会出现跨域问题,不符合同源策略导致的后果有



  • 1、LocalStorge、SessionStorge、Cookie等浏览器内存无法跨域访问

  • 2、DOM节点无法跨域操作

  • 3、Ajax请求无法跨域请求


注意点:一个IP是可以注册多个不同域名的,也就是多个域名可能指向同一个IP,即使是这样,他们也不符合同源策略


截屏2021-10-01 上午9.02.55.png


跨域的时机?


跨域发生在什么时候呢?我考过很多位同学,得到了两种答案



  • 1、请求一发出就被浏览器的跨域报错拦下来了(大多数人回答)

  • 2、请求发出去到后端,后端返回数据,在浏览器接收后端数据时被浏览器的跨域报错拦下来


那到底是哪种呢?我们可以验证下,咱们先npm i nodemon -g,然后创建一个index.js,然后nodemon index起一个node服务


// index.js  http://127.0.0.1:8000

const http = require('http');

const port = 8000;

http.createServer(function (req, res) {
const { query } = urllib.parse(req.url, true);
console.log(query.name)
console.log('到后端喽')
res.end(JSON.stringify('林三心'));
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

再创建一个index.html,用来写前端的请求代码,咱们就写一个简单的AJAX请求


// index.html  http://127.0.0.1:5500/index.html
<script>
//步骤一:创建异步对象
var ajax = new XMLHttpRequest();
//步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数
ajax.open('get', 'http://127.0.0.1:8000?name=前端过来的林三心');
//步骤三:发送请求
ajax.send();
//步骤四:注册事件 onreadystatechange 状态改变就会调用
ajax.onreadystatechange = function () {
if (ajax.readyState == 4 && ajax.status == 200) {
//步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
console.log(ajax.responseText);//输入相应的内容
}
}

</script>
复制代码

截屏2021-10-01 下午1.37.01.png


最终,前端确实是跨域报错了。但这不是结果,我们要想知道是哪一个答案,关键在于看后端的node服务那里有没有输出,就一目了然了。所以,答案2才是对的。


截屏2021-10-01 下午1.38.52.png


截屏2021-10-01 下午1.41.51.png


同域情况 && 跨域情况?


前面提到了同源策略,满足协议号-域名-端口号这三者都相同就是同域,反之就是跨域,会导致跨域报错,下面通过几个例子让大家巩固一下对同域和跨域的认识把!


截屏2021-10-01 上午9.24.38.png


解决跨域的方案


跨域其实是一个很久的问题了,对应的解决方案也有很多,一起接着往下读吧!!!


JSONP


前面咱们说了,因为浏览器同源策略的存在,导致存在跨域问题,那有没有不受跨域问题所束缚的东西呢?其实是有的,以下这三个标签加载资源路径是不受束缚的



  • 1、script标签:<script src="加载资源路径"></script>

  • 2、link标签:<link herf="加载资源路径"></link>

  • 3、img标签:<img src="加载资源路径"></img>


而JSONP就是利用了scriptsrc加载不受束缚,从而可以拥有从不同的域拿到数据的能力。但是JSONP需要前端后端配合,才能实现最终的跨域获取数据


JSONP通俗点说就是:利用script的src去发送请求,将一个方法名callback传给后端,后端拿到这个方法名,将所需数据,通过字符串拼接成新的字符串callback(所需数据),并发送到前端,前端接收到这个字符串之后,就会自动执行方法callback(所需数据)。老规矩,先上图,再上代码。


截屏2021-10-01 下午1.22.08.png


后端代码


// index.js  http://127.0.0.1:8000

const http = require('http');
const urllib = require('url');

const port = 8000;

http.createServer(function (req, res) {
const { query } = urllib.parse(req.url, true);
if (query && query.callback) {
const { name, age, callback } = query
const person = `${name}今年${age}岁啦!!!`
const str = `${callback}(${JSON.stringify(person)})` // 拼成callback(data)
res.end(str);
} else {
res.end(JSON.stringify('没东西啊你'));
}
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

前端代码


// index.html  http://127.0.0.1:5500/index.html

const jsonp = (url, params, cbName) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
window[cbName] = (data) => {
resolve(data)
document.body.removeChild(script)
}
params = { ...params, callback: cbName }
const arr = Object.keys(params).map(key => `${key}=${params[key]}`)
script.src = `${url}?${arr.join('&')}`
document.body.appendChild(script)
})
}

jsonp('http://127.0.0.1:8000', { name: '林三心', age: 23 }, 'callback').then(data => {
console.log(data) // 林三心今年23岁啦!!!
})
复制代码

截屏2021-10-01 下午1.47.29.png



JSONP的缺点就是,需要前后端配合,并且只支持get请求方法



WebSocket


WebSocket是什么东西?其实我也不怎么懂,但是我也不会像别人一样把MDN的资料直接复制过来,因为复制过来相信大家也是看不懂的。


我理解的WebSocket是一种协议(跟http同级,都是协议),并且他可以进行跨域通信,为什么他支持跨域通信呢?我这里找到一篇文章WebSocket凭啥可以跨域?,讲的挺好


截屏2021-10-01 下午10.02.39.png


后端代码


先安装npm i ws


// index.js  http://127.0.0.1:8000
const Websocket = require('ws');

const port = 8000;
const ws = new Websocket.Server({ port })
ws.on('connection', (obj) => {
obj.on('message', (data) => {
data = JSON.parse(data.toString())
const { name, age } = data
obj.send(`${name}今年${age}岁啦!!!`)
})
})
复制代码

前端代码


// index.html  http://127.0.0.1:5500/index.html


function myWebsocket(url, params) {
return new Promise((resolve, reject) => {
const socket = new WebSocket(url)
socket.onopen = () => {
socket.send(JSON.stringify(params))
}
socket.onmessage = (e) => {
resolve(e.data)
}
})
}
myWebsocket('ws://127.0.0.1:8000', { name: '林三心', age: 23 }).then(data => {
console.log(data) // 林三心今年23岁啦!!!
})
复制代码

结果如下


截屏2021-10-01 下午1.47.29.png


Cors


Cors,全称是Cross-Origin Resource Sharing,意思是跨域资源共享,Cors一般是由后端来开启的,一旦开启,前端就可以跨域访问后端。


为什么后端开启Cors,前端就能跨域请求后端呢?我的理解是:前端跨域访问到后端,后端开启Cors,发送Access-Control-Allow-Origin: 域名 字段到前端(其实不止一个),前端浏览器判断Access-Control-Allow-Origin的域名如果跟前端域名一样,浏览器就不会实行跨域拦截,从而解决跨域问题。


截屏2021-10-01 下午6.41.11.png


后端代码


// index.js  http://127.0.0.1:8000

const http = require('http');
const urllib = require('url');

const port = 8000;

http.createServer(function (req, res) {
// 开启Cors
res.writeHead(200, {
//设置允许跨域的域名,也可设置*允许所有域名
'Access-Control-Allow-Origin': 'http://127.0.0.1:5500',
//跨域允许的请求方法,也可设置*允许所有方法
"Access-Control-Allow-Methods": "DELETE,PUT,POST,GET,OPTIONS",
//允许的header类型
'Access-Control-Allow-Headers': 'Content-Type'
})
const { query: { name, age } } = urllib.parse(req.url, true);
res.end(`${name}今年${age}岁啦!!!`);
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

前端代码


// index.html  http://127.0.0.1:5500/index.html
//步骤一:创建异步对象
var ajax = new XMLHttpRequest();
//步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数
ajax.open('get', 'http://127.0.0.1:8000?name=林三心&age=23');
//步骤三:发送请求
ajax.send();
//步骤四:注册事件 onreadystatechange 状态改变就会调用
ajax.onreadystatechange = function () {
if (ajax.readyState == 4 && ajax.status == 200) {
//步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
console.log(ajax.responseText);//输入相应的内容
}
}
复制代码

结果如下


截屏2021-10-01 下午1.47.29.png


截屏2021-10-01 下午7.10.57.png


Node接口代理


还是回到同源策略,同源策略它只是浏览器的一个策略而已,它是限制不到后端的,也就是前端-后端会被同源策略限制,但是后端-后端则不会被限制,所以可以通过Node接口代理,先访问已设置Cors的后端1,再让后端1去访问后端2获取数据到后端1,后端1再把数据传到前端


截屏2021-10-01 下午8.46.28.png


后端2代码


// index.js  http://127.0.0.1:8000

const http = require('http');
const urllib = require('url');

const port = 8000;

http.createServer(function (req, res) {
console.log(888)
const { query: { name, age } } = urllib.parse(req.url, true);
res.end(`${name}今年${age}岁啦!!!`)
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

创建一个index2.js,并nodmeon index2.js


后端1代码


// index2.js  http://127.0.0.1:8888

const http = require('http');
const urllib = require('url');
const querystring = require('querystring');
const port = 8888;

http.createServer(function (req, res) {
// 开启Cors
res.writeHead(200, {
//设置允许跨域的域名,也可设置*允许所有域名
'Access-Control-Allow-Origin': 'http://127.0.0.1:5500',
//跨域允许的请求方法,也可设置*允许所有方法
"Access-Control-Allow-Methods": "DELETE,PUT,POST,GET,OPTIONS",
//允许的header类型
'Access-Control-Allow-Headers': 'Content-Type'
})
const { query } = urllib.parse(req.url, true);
const { methods = 'GET', headers } = req
const proxyReq = http.request({
host: '127.0.0.1',
port: '8000',
path: `/?${querystring.stringify(query)}`,
methods,
headers
}, proxyRes => {
proxyRes.on('data', chunk => {
console.log(chunk.toString())
res.end(chunk.toString())
})
}).end()
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

前端代码


// index.html  http://127.0.0.1:5500

//步骤一:创建异步对象
var ajax = new XMLHttpRequest();
//步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数,动态的传递参数starName到服务端
ajax.open('get', 'http://127.0.0.1:8888?name=林三心&age=23');
//步骤三:发送请求
ajax.send();
//步骤四:注册事件 onreadystatechange 状态改变就会调用
ajax.onreadystatechange = function () {
if (ajax.readyState == 4 && ajax.status == 200) {
//步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
console.log(ajax.responseText);//输入相应的内容
}
}
复制代码

结果如下


截屏2021-10-01 下午1.47.29.png


Nginx


其实NginxNode接口代理是一个道理,只不过Nginx就不需要我们自己去搭建一个中间服务


截屏2021-10-01 下午8.47.40.png


先下载nginx,然后将nginx目录下的nginx.conf修改如下:


    server{
listen 8888;
server_name 127.0.0.1;

location /{
proxy_pass 127.0.0.1:8000;
}
}
复制代码

最后通过命令行nginx -s reload启动nginx


后端代码


// index.js  http://127.0.0.1:8000

const http = require('http');
const urllib = require('url');

const port = 8000;

http.createServer(function (req, res) {
const { query: { name, age } } = urllib.parse(req.url, true);
res.end(`${name}今年${age}岁啦!!!`);
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

前端代码


// index.html  http://127.0.0.1:5500

//步骤一:创建异步对象
var ajax = new XMLHttpRequest();
//步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数,动态的传递参数starName到服务端
ajax.open('get', 'http://127.0.0.1:8888?name=林三心&age=23');
//步骤三:发送请求
ajax.send();
//步骤四:注册事件 onreadystatechange 状态改变就会调用
ajax.onreadystatechange = function () {
if (ajax.readyState == 4 && ajax.status == 200) {
//步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
console.log(ajax.responseText);//输入相应的内容
}
}
复制代码

结果如下


截屏2021-10-01 下午1.47.29.png


postMessage


场景:http://127.0.0.1:5500/index.html页面中使用了iframe标签内嵌了一个http://127.0.0.1:5555/index.html的页面


虽然这两个页面存在于一个页面中,但是需要iframe标签来嵌套才行,这两个页面之间是无法进行通信的,因为他们端口号不同,根据同源策略,他们之间存在跨域问题


那应该怎么办呢?使用postMessage可以使这两个页面进行通信


截屏2021-10-01 下午9.28.53.png


// http:127.0.0.1:5500/index.html

<body>
<iframe src="http://127.0.0.1:5555/index.html" id="frame"></iframe>
</body>
<script>
document.getElementById('frame').onload = function () {
this.contentWindow.postMessage({ name: '林三心', age: 23 }, 'http://127.0.0.1:5555')
window.onmessage = function (e) {
console.log(e.data) // 林三心今年23岁啦!!!
}
}
</script>
复制代码

// http://127.0.0.1:5555/index.html

<script>
window.onmessage = function (e) {
const { data: { name, age }, origin } = e
e.source.postMessage(`${name}今年${age}岁啦!!!`, origin)
}
</script>
复制代码

document.domain && iframe


场景:a.sanxin.com/index.htmlb.sanxin.com/index.html之间的通信


其实上面这两个正常情况下是无法通信的,因为他们的域名不相同,属于跨域通信


那怎么办呢?其实他们有一个共同点,那就是他们的二级域名都是sanxin.com,这使得他们可以通过document.domain && iframe的方式来通信


截屏2021-10-01 下午9.58.55.png


由于本菜鸟暂时没有服务器,所以暂时使用本地来模拟


// http://127.0.0.1:5500/index.html

<body>
<iframe src="http://127.0.0.1:5555/index.html" id="frame"></iframe>
</body>
<script>
document.domain = '127.0.0.1'
document.getElementById('frame').onload = function () {
console.log(this.contentWindow.data) // 林三心今年23岁啦!!!
}
</script>
复制代码

// http://127.0.0.1:5555/index.html

<script>
// window.name="林三心今年23岁啦!!!"
document.domain = '127.0.0.1'
var data = '林三心今年23岁啦!!!';
</script>

复制代码

结果如下


截屏2021-10-01 下午1.47.29.png


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

画一手好的架构图是码农进阶的开始

1.前言 你是否对大厂展示的五花八门,花花绿绿的架构设计图所深深吸引,当我们想用几张图来介绍下业务系统,是不是对着画布不知从何下手?作为技术扛把子的筒子们是不是需要一张图来描述系统,让系统各个参与方都能看的明白?如果有这样的困惑,本文将介绍一些画图的方...
继续阅读 »

1.前言

你是否对大厂展示的五花八门,花花绿绿的架构设计图所深深吸引,当我们想用几张图来介绍下业务系统,是不是对着画布不知从何下手?作为技术扛把子的筒子们是不是需要一张图来描述系统,让系统各个参与方都能看的明白?如果有这样的困惑,本文将介绍一些画图的方法论,让技术图纸更加清晰。

2. 架构的定义

  • 系统架构是概念的体现,是对物/信息的功能与形式元素之间的对应情况所做的分配,是对元素之间的关系以及元素同周边环境之间的关系所做的定义;

  • 架构就是对系统中的实体以及实体之间的关系所进行的抽象描述,是一系列的决策;

  • 架构是结构和愿景.

在TOGAF企业架构理论中, 架构是从公司战略层面,自顶向下的细化的一部分,从战略=> 业务架构=>应用/数据/技术架构,当然老板层关注的是战略与业务架构,我们搬砖的需要聚焦到应用/数据/技术架构这一层。


  • 业务架构: 由业务架构师负责,也可以称为业务领域专家、行业专家,业务架构属于顶层设计,其对业务的定义和划分会影响组织架构和技术架构;

  • 应用架构: 由应用架构师负责,需要根据业务场景需要,设计应用的层次结构,制定应用规范、定义接口和数据交互协议等。并尽量将应用的复杂度控制在一个可以接受的水平,从而在快速的支撑业务发展的同时,在保证系统的可用性和可维护性的同时,确保应用满足非功能属性的要求如性能、安全、稳定性等。

  • 技术架构: 描述了需要哪些服务;选择哪些技术组件来实现技术服务;技术服务以及组件之间的交互关系;

  • 数据架构: 描述了数据模型、分布、数据的流向、数据的生命周期、数据的管理等关系;

3.架构图的分类

系统架构图是为了抽象的表示软件系统的整体轮廓和各个组件之间的相互关系和约束边界,以及软件系统的物理部署和软件系统的演进方向的整体视图。好的架构图可以让干系人理解、遵循架构决策,就需要把架构信息传递出去。那么,画架构图是为了:解决沟通障碍/达成共识/减少歧义。比较流行的是4+1视图和C4视图。

3.1 4+1视图

3.1.1 场景视图

用于描述系统的参与者与功能用例间的关系,反映系统的最终需求和交互设计,通常由用例图表示;


3.1.2 逻辑视图

用于描述系统软件功能拆解后的组件关系,组件约束和边界,反映系统整体组成与系统如何构建的过程,通常由UML的组件图和类图来表示。


3.1.3 物理视图

用于描述系统软件到物理硬件的映射关系,反映出系统的组件是如何部署到一组可计算机器节点上,用于指导软件系统的部署实施过程。


3.1.4 处理流程视图

用于描述系统软件组件之间的通信时序,数据的输入输出,反映系统的功能流程与数据流程,通常由时序图和流程图表示。


3.1.5 开发视图

开发视图用于描述系统的模块划分和组成,以及细化到内部包的组成设计,服务于开发人员,反映系统开发实施过程。


5 种架构视图从不同角度表示一个软件系统的不同特征,组合到一起作为架构蓝图描述系统架构。

3.2 C4视图

下面的案例来自C4官网,然后加上了一些笔者的理解。


C4 模型使用容器(应用程序、数据存储、微服务等)、组件和代码来描述一个软件系统的静态结构。这几种图比较容易画,也给出了画图要点,但最关键的是,我们认为,它明确指出了每种图可能的受众以及意义。

3.2.1 语境图(System Context Diagram)

用于描述要我们要构建的系统是什么,用户是谁,需要如何融入已有的IT环境。这个图的受众可以是开发团队的内部人员、外部的技术或非技术人员。


3.2.2 容器图(Container Diagram)

容器图是把语境图里待建设的系统做了一个展开描述,主要受众是团队内部或外部的开发人员或运维人员,主要用来描述软件系统的整体形态,体现了高层次的技术决策与选型,系统中的职责是如何分布的,容器间是如何交互的。


3.2.3 组件图(Component Diagram)

组件图是把某个容器进行展开,描述其内部的模块,主要是给内部开发人员看的,怎么去做代码的组织和构建,描述了系统由哪些组件/服务组成,了组件之间的关系和依赖,为软件开发如何分解交付提供了框架。


4.怎么画好架构图

上面的分类是前人的经验总结,图也是从网上摘来的,那么这些图画的好不好呢?是不是我们要依葫芦画瓢去画这样一些图?先不去管这些图好不好,我们通过对这些图的分类以及作用,思考了一下,总结下来,我们认为,明确这两点之后,从受众角度来说,一个好的架构图是不需要解释的,它应该是自描述的,并且要具备一致性和足够的准确性,能够与代码相呼应。

4.1 视图的受众

在画出一个好的架构图之前, 首先应该要明确其受众,再想清楚要给他们传递什么信息 ,所以,不要为了画一个物理视图去画物理视图,为了画一个逻辑视图去画逻辑视图,而应该根据受众的不同,传递的信息的不同,用图准确地表达出来,最后的图可能就是在这样一些分类里。那么,画出的图好不好的一个直接标准就是:受众有没有准确接收到想传递的信息。

4.2 视图的元素区分

可以看到架构视图是由方框和线条等元素构成,要利用形状、颜色、线条变化等区分元素的含义,避免混淆。架构是一项复杂的工作,只使用单个图表来表示架构很容易造成莫名其妙的语义混乱。

让我们一起画出好的架构图!

参考资料


作者:代码的色彩
来源:https://juejin.cn/post/7062662600437268493

收起阅读 »

CSS性能优化的8个技巧

我们都知道对于网站来说,性能至关重要,CSS作为页面渲染和内容展现的重要环节,影响着用户对整个网站的第一体验。因此,与其相关的性能优化是不容忽视的。对于性能优化我们常常在项目完成时才去考虑,经常被推迟到项目的末期,甚至到暴露出严重的性能问题时才进行性能优化,相...
继续阅读 »

我们都知道对于网站来说,性能至关重要,CSS作为页面渲染和内容展现的重要环节,影响着用户对整个网站的第一体验。因此,与其相关的性能优化是不容忽视的。

对于性能优化我们常常在项目完成时才去考虑,经常被推迟到项目的末期,甚至到暴露出严重的性能问题时才进行性能优化,相信大多数人对此深有体会。

笔者认为,为了更多地避免这一情况,首先要重视起性能优化相关的工作,将其贯穿到整个产品设计与开发中。其次,就是了解性能相关的内容,在项目开发过程中,自然而然地进行性能优化。最后,也是最最重要的,那就是从现在开始实施优化。

推荐大家阅读下奇舞周刊之前推的《嗨,送你一张Web性能优化地图》1这篇文章,能够帮助大家对性能优化需要做的事以及需要考虑的问题形成一个整体的概念。

本文将会详细介绍CSS性能优化相关的技巧,笔者将它们分为实践型建议型两类,共8个小技巧。实践型技巧能够快速地应用在项目中,能够很好地提升性能,也是笔者经常使用的,建议大家尽快在项目中实践。建议型技巧中,有的可能对性能影响并不显著,有的平时大家也并不会那么用,所以笔者不会着重讲述,读者们可以根据自身情况了解一下即可。

在正式开始之前,需要大家对于浏览器的工作原理2有些一定的了解,需要的小伙伴可以先简单了解下。

下面我们开始介绍实践型的4个优化技巧,先从首屏关键CSS开始。

1. 内联首屏关键CSS(Critical CSS)

性能优化中有一个重要的指标——首次有效绘制(First Meaningful Paint,简称FMP)即指页面的首要内容(primary content)出现在屏幕上的时间。这一指标影响用户看到页面前所需等待的时间,而内联首屏关键CSS(即Critical CSS,可以称之为首屏关键CSS)能减少这一时间。

大家应该都习惯于通过link标签引用外部CSS文件。但需要知道的是,将CSS直接内联到HTML文档中能使CSS更快速地下载。而使用外部CSS文件时,需要在HTML文档下载完成后才知道所要引用的CSS文件,然后才下载它们。所以说,内联CSS能够使浏览器开始页面渲染的时间提前,因为在HTML下载完成之后就能渲染了。

既然内联CSS能够使页面渲染的开始时间提前,那么是否可以内联所有的CSS呢?答案显然是否定的,这种方式并不适用于内联较大的CSS文件。因为初始拥塞窗口3存在限制(TCP相关概念,通常是 14.6kB,压缩后大小),如果内联CSS后的文件超出了这一限制,系统就需要在服务器和浏览器之间进行更多次的往返,这样并不能提前页面渲染时间。因此,我们应当只将渲染首屏内容所需的关键CSS内联到HTML中

既然已经知道内联首屏关键CSS能够优化性能了,那下一步就是如何确定首屏关键CSS了。显然,我们不需要手动确定哪些内容是首屏关键CSS。Github上有一个项目Critical CSS4,可以将属于首屏的关键样式提取出来,大家可以看一下该项目,结合自己的构建工具进行使用。当然为了保证正确,大家最好再亲自确认下提取出的内容是否有缺失。

不过内联CSS有一个缺点,内联之后的CSS不会进行缓存,每次都会重新下载。不过如上所说,如果我们将内联后的文件大小控制在了14.6kb以内,这似乎并不是什么大问题。

如上,我们已经介绍了为什么要内联关键CSS以及如何内联,那么剩下的CSS我们怎么处理好呢?建议使用外部CSS引入剩余CSS,这样能够启用缓存,除此之外还可以异步加载它们。

2. 异步加载CSS

CSS会阻塞渲染,在CSS文件请求、下载、解析完成之前,浏览器将不会渲染任何已处理的内容。有时,这种阻塞是必须的,因为我们并不希望在所需的CSS加载之前,浏览器就开始渲染页面。那么将首屏关键CSS内联后,剩余的CSS内容的阻塞渲染就不是必需的了,可以使用外部CSS,并且异步加载。

那么如何实现CSS的异步加载呢?有以下四种方式可以实现浏览器异步加载CSS。

第一种方式是使用JavaScript动态创建样式表link元素,并插入到DOM中。

// 创建link标签
const myCSS = document.createElement( "link" );
myCSS.rel = "stylesheet";
myCSS.href = "mystyles.css";
// 插入到header的最后位置
document.head.insertBefore( myCSS, document.head.childNodes[ document.head.childNodes.length - 1 ].nextSibling );

第二种方式是将link元素的media属性设置为用户浏览器不匹配的媒体类型(或媒体查询),如media="print",甚至可以是完全不存在的类型media="noexist"。对浏览器来说,如果样式表不适用于当前媒体类型,其优先级会被放低,会在不阻塞页面渲染的情况下再进行下载。

当然,这么做只是为了实现CSS的异步加载,别忘了在文件加载完成之后,将media的值设为screenall,从而让浏览器开始解析CSS。

<link rel="stylesheet" href="mystyles.css" media="noexist" onl0ad="this.media='all'">

与第二种方式相似,我们还可以通过rel属性将link元素标记为alternate可选样式表,也能实现浏览器异步加载。同样别忘了加载完成之后,将rel改回去。

<link rel="alternate stylesheet" href="mystyles.css" onl0ad="this.rel='stylesheet'">

上述的三种方法都较为古老。现在,rel="preload"5这一Web标准指出了如何异步加载资源,包括CSS类资源。

<link rel="preload" href="mystyles.css" as="style" onl0ad="this.rel='stylesheet'">

注意,as是必须的。忽略as属性,或者错误的as属性会使preload等同于XHR请求,浏览器不知道加载的是什么内容,因此此类资源加载优先级会非常低。as的可选值可以参考上述标准文档。

看起来,rel="preload"的用法和上面两种没什么区别,都是通过更改某些属性,使得浏览器异步加载CSS文件但不解析,直到加载完成并将修改还原,然后开始解析。

但是它们之间其实有一个很重要的不同点,那就是使用preload,比使用不匹配的media方法能够更早地开始加载CSS。所以尽管这一标准的支持度还不完善,仍建议优先使用该方法。

该标准现在已经是候选标准,相信浏览器会逐渐支持该标准。在各浏览器的支持度如下图所示。


从上图可以看出这一方法在现在的浏览器中支持度不算乐观,不过我们可以通过loadCSS6进行polyfill,所以支持不支持,这都不是事儿。

3. 文件压缩

性能优化时有一个最容易想到,也最常使用的方法,那就是文件压缩,这一方案往往效果显著。

文件的大小会直接影响浏览器的加载速度,这一点在网络较差时表现地尤为明显。相信大家都早已习惯对CSS进行压缩,现在的构建工具,如webpack、gulp/grunt、rollup等也都支持CSS压缩功能。压缩后的文件能够明显减小,可以大大降低了浏览器的加载时间。

4. 去除无用CSS

虽然文件压缩能够降低文件大小。但CSS文件压缩通常只会去除无用的空格,这样就限制了CSS文件的压缩比例。那是否还有其他手段来精简CSS呢?答案显然是肯定的,如果压缩后的文件仍然超出了预期的大小,我们可以试着找到并删除代码中无用的CSS

一般情况下,会存在这两种无用的CSS代码:一种是不同元素或者其他情况下的重复代码,一种是整个页面内没有生效的CSS代码。对于前者,在编写的代码时候,我们应该尽可能地提取公共类,减少重复。对于后者,在不同开发者进行代码维护的过程中,总会产生不再使用的CSS的代码,当然一个人编写时也有可能出现这一问题。而这些无用的CSS代码不仅会增加浏览器的下载量,还会增加浏览器的解析时间,这对性能来说是很大的消耗。所以我们需要找到并去除这些无用代码。

当然,如果手动删除这些无用CSS是很低效的。我们可以借助Uncss7库来进行。Uncss可以用来移除样式表中的无用CSS,并且支持多文件和JavaScript注入的CSS。

前面已经说完了实践型的4个优化技巧,下面我们介绍下建议型的4个技巧

1. 有选择地使用选择器

大多数朋友应该都知道CSS选择器的匹配是从右向左进行的,这一策略导致了不同种类的选择器之间的性能也存在差异。相比于#markdown-content-h3,显然使用#markdown .content h3时,浏览器生成渲染树(render-tree)所要花费的时间更多。因为后者需要先找到DOM中的所有h3元素,再过滤掉祖先元素不是.content的,最后过滤掉.content的祖先不是#markdown的。试想,如果嵌套的层级更多,页面中的元素更多,那么匹配所要花费的时间代价自然更高。

不过现代浏览器在这一方面做了很多优化,不同选择器的性能差别并不明显,甚至可以说差别甚微。此外不同选择器在不同浏览器中的性能表现8也不完全统一,在编写CSS的时候无法兼顾每种浏览器。鉴于这两点原因,我们在使用选择器时,只需要记住以下几点,其他的可以全凭喜好。

  1. 保持简单,不要使用嵌套过多过于复杂的选择器。

  2. 通配符和属性选择器效率最低,需要匹配的元素最多,尽量避免使用。

  3. 不要使用类选择器和ID选择器修饰元素标签,如h3#markdown-content,这样多此一举,还会降低效率。

  4. 不要为了追求速度而放弃可读性与可维护性。

如果大家对于上面这几点还存在疑问,笔者建议大家选择以下几种CSS方法论之一(BEM9,OOCSS10,SUIT11,SMACSS12,ITCSS13,Enduring CSS14等)作为CSS编写规范。使用统一的方法论能够帮助大家形成统一的风格,减少命名冲突,也能避免上述的问题,总之好处多多,如果你还没有使用,就赶快用起来吧。

Tips:为什么CSS选择器是从右向左匹配的?

CSS中更多的选择器是不会匹配的,所以在考虑性能问题时,需要考虑的是如何在选择器不匹配时提升效率。从右向左匹配就是为了达成这一目的的,通过这一策略能够使得CSS选择器在不匹配的时候效率更高。这样想来,在匹配时多耗费一些性能也能够想的通了。

2. 减少使用昂贵的属性

在浏览器绘制屏幕时,所有需要浏览器进行操作或计算的属性相对而言都需要花费更大的代价。当页面发生重绘时,它们会降低浏览器的渲染性能。所以在编写CSS时,我们应该尽量减少使用昂贵属性,如box-shadow/border-radius/filter/透明度/:nth-child等。

当然,并不是让大家不要使用这些属性,因为这些应该都是我们经常使用的属性。之所以提这一点,是让大家对此有一个了解。当有两种方案可以选择的时候,可以优先选择没有昂贵属性或昂贵属性更少的方案,如果每次都这样的选择,网站的性能会在不知不觉中得到一定的提升。

3. 优化重排与重绘

在网站的使用过程中,某些操作会导致样式的改变,这时浏览器需要检测这些改变并重新渲染,其中有些操作所耗费的性能更多。我们都知道,当FPS为60时,用户使用网站时才会感到流畅。这也就是说,我们需要在16.67ms内完成每次渲染相关的所有操作,所以我们要尽量减少耗费更多的操作。

3.1 减少重排

重排会导致浏览器重新计算整个文档,重新构建渲染树,这一过程会降低浏览器的渲染速度。如下所示,有很多操作会触发重排,我们应该避免频繁触发这些操作。

  1. 改变font-sizefont-family

  2. 改变元素的内外边距

  3. 通过JS改变CSS类

  4. 通过JS获取DOM元素的位置相关属性(如width/height/left等)

  5. CSS伪类激活

  6. 滚动滚动条或者改变窗口大小

此外,我们还可以通过CSS Trigger15查询哪些属性会触发重排与重绘。

值得一提的是,某些CSS属性具有更好的重排性能。如使用Flex时,比使用inline-blockfloat时重排更快,所以在布局时可以优先考虑Flex

3.2 避免不必要的重绘

当元素的外观(如color,background,visibility等属性)发生改变时,会触发重绘。在网站的使用过程中,重绘是无法避免的。不过,浏览器对此做了优化,它会将多次的重排、重绘操作合并为一次执行。不过我们仍需要避免不必要的重绘,如页面滚动时触发的hover事件,可以在滚动的时候禁用hover事件,这样页面在滚动时会更加流畅。

此外,我们编写的CSS中动画相关的代码越来越多,我们已经习惯于使用动画来提升用户体验。我们在编写动画时,也应当参考上述内容,减少重绘重排的触发。除此之外我们还可以通过硬件加速16和will-change17来提升动画性能,本文不对此展开详细介绍,感兴趣的小伙伴可以点击链接进行查看。

最后需要注意的是,用户的设备可能并没有想象中的那么好,至少不会有我们的开发机器那么好。我们可以借助Chrome的开发者工具进行CPU降速,然后再进行相关的测试,降速方法如下图所示。


如果需要在移动端访问的,最好将速度限制更低,因为移动端的性能往往更差。

4. 不要使用@import

最后提一下,不要使用@import引入CSS,相信大家也很少使用。

不建议使用@import主要有以下两点原因。

首先,使用@import引入CSS会影响浏览器的并行下载。使用@import引用的CSS文件只有在引用它的那个css文件被下载、解析之后,浏览器才会知道还有另外一个css需要下载,这时才去下载,然后下载后开始解析、构建render tree等一系列操作。这就导致浏览器无法并行下载所需的样式文件。

其次,多个@import会导致下载顺序紊乱。在IE中,@import会引发资源文件的下载顺序被打乱,即排列在@import后面的js文件先于@import下载,并且打乱甚至破坏@import自身的并行下载

所以不要使用这一方法,使用link标签就行了。

总结

至此,我们介绍完了CSS性能优化的4个实践型技巧和4个建议型技巧,在了解这些技巧之后,CSS的性能优化从现在就可以开始了。不要犹豫了,尽快开始吧。

参考文章

  1. Efficiently Rendering CSS

  2. How to write CSS for a great performance web application

  3. CSS performance revisited: selectors, bloat and expensive styles

  4. Avoiding Unnecessary Paints

  5. Five CSS Performance Tools to Speed up Your Website

  6. How and Why You Should Inline Your Critical CSS

  7. Render blocking css

  8. Modern Asynchronous CSS Loading

  9. Preload

作者:奇舞精选 · 高峰
来源:https://juejin.cn/post/6844903649605320711

收起阅读 »

你要懂的单页面应用和多页面应用

单页面应用(SinglePage Web Application,SPA)只有一张Web页面的应用,是一种从Web服务器加载的富客户端,单页面跳转仅刷新局部资源 ,公共资源(js、css等)仅需加载一次,常用于PC端官网、购物等网站如图:单页面应用结构视图多页...
继续阅读 »

单页面应用(SinglePage Web Application,SPA)

只有一张Web页面的应用,是一种从Web服务器加载的富客户端,单页面跳转仅刷新局部资源 ,公共资源(js、css等)仅需加载一次,常用于PC端官网、购物等网站

如图:


单页面应用结构视图

多页面应用(MultiPage Application,MPA)

多页面跳转刷新所有资源,每个公共资源(js、css等)需选择性重新加载,常用于 app 或 客户端等

如图:


多页面应用结构视图

具体对比分析:

单页面应用(SinglePage Web Application,SPA)多页面应用(MultiPage Application,MPA)
组成一个外壳页面和多个页面片段组成多个完整页面构成
资源共用(css,js)共用,只需在外壳部分加载不共用,每个页面都需要加载
刷新方式页面局部刷新或更改整页刷新
url 模式a.com/#/pageone a.com/#/pagetwoa.com/pageone.html a.com/pagetwo.html
用户体验页面片段间的切换快,用户体验良好页面切换加载缓慢,流畅度不够,用户体验比较差
转场动画容易实现无法实现
数据传递容易依赖 url传参、或者cookie 、localStorage等
搜索引擎优化(SEO)需要单独方案、实现较为困难、不利于SEO检索 可利用服务器端渲染(SSR)优化实现方法简易
试用范围高要求的体验度、追求界面流畅的应用适用于追求高度支持搜索引擎的应用
开发成本较高,常需借助专业的框架较低 ,但页面重复代码多
维护成本相对容易相对复杂


作者:boxser
来源:https://juejin.cn/post/6844903512107663368

收起阅读 »

千万别小瞧九宫格 一道题就能让候选人原形毕露!

前言 据不完全统计(其实就统计了自己身边的朋友和同事),在刨除抖音或快手这一类短视频 APP 后,每天在手机上花费时间最长的就是刷微博和逛朋友圈。 在刷微博和逛朋友圈的时候经常会看到这种东西: 它有一个高大上的名字:九宫格。 顾名思义,九宫格通常为如图这种三...
继续阅读 »

前言


据不完全统计(其实就统计了自己身边的朋友和同事),在刨除抖音或快手这一类短视频 APP 后,每天在手机上花费时间最长的就是刷微博和逛朋友圈。


在刷微博和逛朋友圈的时候经常会看到这种东西:



它有一个高大上的名字:九宫格。
顾名思义,九宫格通常为如图这种三行三列的布局。


微信客户端就用到了这种布局方式:



大家最熟悉的朋友圈也采用了九宫格:



还有微博:



它在移动端的运用十分的广泛,而且不仅仅是在移动端的运用,它甚至还运用到了一些面试题中,因为九宫格可以很好的考察面试者的 CSS 功底。


边距九宫格


九宫格通常分为两种,一种是边距九宫格,另一种是边框九宫格。


边距九宫格就是朋友圈那种每张图都带有一定边距的那种:


这种其实反而更简单一些,因为不涉及到边框问题,像这种几行几列的布局用网格布局(grid)简直再合适不过了。


但考虑到大家普遍对网格不太熟悉,所以咱们用同样适合几行几列的表格布局来实现,为什么不用万能的弹性盒子(flex)来做呢?因为下面那道面试题就是用flex实现的,不想用两个一样的布局来实现,为了美观一点,这里使用了一个中文渐变色的库:chinese-gradient,来看代码:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- 在这里用link标签引入中文渐变色 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/chinese-gradient">
<style>
/* 清除默认样式 */
* { padding: 0; margin: 0; }

/* 全屏显示 */
html, body, ul { height: 100% }

/* 父元素 */
ul {
/* 给个合适的宽度 */
width: 100%;

/* 清除默认样式 */
list-style: none;

/* 令其用table方式去显示 */
display: table;

/* 设置间距 */
border-spacing: 3px
}

/* 子元素 */
li {
/* 令其用table-row方式去显示 */
display: table-row
}

/* 孙子元素 */
div {
/* 令其用table-cell方式去显示 */
display: table-cell;

/* 蓝色渐变 */
background: var(--湖蓝)
}
</style>
</head>
<body>
<ul>
<li>
<div></div>
<div></div>
<div></div>
</li>
<li>
<div></div>
<div></div>
<div></div>
</li>
<li>
<div></div>
<div></div>
<div></div>
</li>
</ul>
</body>
</html>
复制代码

运行结果:



可以看到在 DOM 结构上我们并没有用到 <table>、<tr>、<td> 这类传统表格元素,因为在这种情况下只是用到了表格的那种几行几列而已。但实际上九宫格并不是表格,所以为了符合 W3C 的语义化标准,我们采用了其他的 DOM 元素。



在有些适合使用表格布局但又不是表格的情况下,可以利用 display 属性来模仿表格的行为:




  • display: table;相当于把元素的行为变成<table></table>

  • display: inline-table;相当于把元素的行为变成行内元素版的<table></table>

  • display: table-header-group;相当于把元素的行为变成<thead></thead>

  • display: table-row-group;相当于把元素的行为变成<tbody></tbody>

  • display: table-footer-group;相当于把元素的行为变成<tfoot></tfoot>

  • display: table-row;相当于把元素的行为变成<tr></tr>

  • display: table-column-group;相当于把元素的行为变成<colgroup></colgroup>

  • display: table-column;相当于把元素的行为变成<col></col>

  • display: table-cell;相当于把元素的行为变成<td></td><th></th>

  • display: table-caption;相当于把元素的行为变成<caption></caption>


边框九宫格


可能大家看了前面的内容觉得:就这?这么简单还想让人原形毕露?


那咱们来看这么一道题:



要求如下:



  • 边框九宫格的每个格子中的数字都要居中

  • 鼠标经过时边框和数字都要变红

  • 点击九宫格会弹出对应的数字


看起来还是没什么大不了对不对?是不是觉得就是把九宫格加个边框就行了?如果你是这么想的话,那么你写出来的九宫格将会变成这样:



是不是跟想象中的好像不太一样?为什么会这样呢?




因为给每个盒子加入了边框以后,在有边距的情况下看起来都挺正常的,但要将他们合并在一起的话相邻的两个边框就会贴合在一起,肉眼看起来就是一个两倍粗的边框:



那么怎么解决这个问题呢?


解法1


不是相邻的两个边框合并在一起会变粗吗?那么最简单粗暴的办法就是让两个相邻的盒子的其中一个的相邻边不显示边框不就完了!就像这样:



这么做完全可以实现,绝对没毛病。但这种属于笨方法,如果给换成四宫格、六宫格、十二宫格,那么又要重新去想一下该怎么实现,而且写出来的代码也比较冗余,几乎每个盒子都要给它定义一个不同的样式。


如果去参加面试的时候这么实现出来,面试官也不会给你满分,甚至可能连个及格分都不会给。但毕竟算是实现出来了,总比那些没实现出来的强点,不会给零分的。


解法2


上面那种实现方式要给每一个盒子都写一套不同的样式,而且还不适合别的像六宫格、十二宫格这类,代码冗余、可复用性差。


那么怎么才能每个盒子只用到一个样式,并且同样还适用于别的宫格呢?来看看这个思路:



但是仔细一看经不起推敲啊:整个九宫格最右边和最下边的边框都没有了!其实只要咱们在父元素上再加上右侧和下侧的边框即可:



而且并不一定非得是这个方向的,别的方向也可以实现啊,比如酱婶儿的:



酱婶儿的:



还有酱婶儿的:



这种方式不管你是4、6、9还是12宫格,只需在子元素上加一个样式即可,然后再在父元素上加一个互补的边框样式。


解法3


上面那种解法其实已经可以了,但还不是最完美的,那么它都有哪些问题呢?




  • 首先,虽然换成别的宫格也可以复用,但都只适合"满"的情况。比如像朋友圈,最大就是九宫格对吧?但用户可以不是每次都发满九张照片,有可能发7张、有可能发五张,这样的话就会露馅(所以朋友圈采用的是边距九宫格而不是边框九宫格)。




  • 其次,它并不适合这道面试题,因为这道面试题的要求是在鼠标移入时边框变红,而上面那种解法会导致每个盒子的边框都不完整,所以当鼠标移入时效果会变成这样:





那么怎么样才能完美的解出这道题呢?首先每个盒子的边框不能再给它缺斤少两了,但那又会回到最初的那个问题上去:



有的面试题就是这样,在你苦思冥想的时候怎么也想不出来,但是稍微给点思路立马就能明白!


其实就是每个盒子都给它一个负边距,边距的距离恰巧就是边框的粗细,这样后面一个盒子就会"叠加"在前面那个盒子的边框上,我们来写一个粗点的半透明边框演示一下:



中间那些颜色变深了的就是叠在一起的边框,由于是半透明,所以叠在一起时颜色会变深。


不过一些比较细心的朋友可能会纳闷:既然所有盒子都用负边距向左上角移动了,岂不是九宫格不会处在原来的位置上了,没错是这样的!所以我们需要让最左边那一排和最上面那一排不要有负边距,这时候就要考察候选人的CSS水平了,看看他/她能不能够灵活运用伪类选择器:每一行的第一个,应该怎么写?



  • :nth-child(1), :nth-child(4), :nth-child(7)


这样也能实现,不过更好的方式是写成这样:



  • :nth-child(3n+1)


最上面那一排负边距可以不用管,因为如果页面上的九宫格往左边移动了,哪怕只有一两像素,也会导致和页面上的版面无法对齐,而往上移动个一两像素的话谁也看不出来。


但如果要写的话大多数人想的可能是这样:



  • :first-child, :nth-child(2), :nth-child(3)


而更好的方式是这样:



  • :nth-child(-n+3)


每个宫格内的数字要居中,这里推荐用grid,因为九宫格可以用flex去实现,但里面的内容还继续用它去实现的话就体现不出你技术的全面性了,而且在居中这一方面grid可以做到比flex代码更少,即使你对grid不感兴趣,那么只需记住这一固定用法即可:


父元素 {
display: grid;

/* 令其子元素居中 */
place-items: center;
}
复制代码

点击这里查看更多实现居中布局的方式


里面的内容解决了,外面的九宫格咱们来用万能的flex去实现,flex默认是一维布局,但如果仅支持一维的话就不会称之为万能的flex了,思路是这样的,假如每一个宫格宽高为100 x 100,九宫格加起来是300 x 300,每三个就让它换行,这样就可以考察到候选人对flex的灵活运用的程度了:


父元素 {
width: 300px;

/* 设置为flex布局 */
display: flex;

/* 设置换行 */
flex-flow: wrap;
}

子元素 {
width: 100px;
height: 100px;

border: 1px solid black;
}
复制代码

看起来没毛病对不对?实际上确是每行只有两个宫格就会换行,因为加了边框以后子元素的宽高就变成了102 x 102了,三个的话就已经超过了300,所以还没到三个就开始换行了,这时候就考察到候选人的盒模型了:


子元素 {
width: 100px;
height: 100px;

border: 1px solid black;

/* 设置盒模型 */
box-sizing: border-box;
}
复制代码

这样即使加了边框,宽高也还是100,刚好能满3个就换行,想象一下如果你是面试官,直接问盒模型是不是显得很low,但是就这一个小小的九宫格立马就能区分出这个候选人的水平如何。


再接下来就是鼠标移入时边框和里面的内容一起变红,这有啥难的,不就是:


:hover {
/* 红色字体 */
color: red;

/* 红色边框 */
border: 1px solid red;
}
复制代码

还是那句话,这样确实能实现,但如果在咱们写js的过程中像red这种多处地方使用的值是不是一般都会给它设置成变量啊?那么这里要写CSS变量?也可以,但有一个更好的变量叫做currentColor,这个属性可以把它理解成一个内置变量,就像js里的innerWidth(window.innerWidth)一样,不用定义自然就是一个变量。


CSS变量不同的是它取的是自身或父元素上的color值,而且它的兼容性还更好,可以一直兼容到IE9


如果你觉得纳闷:这单词这么长,还不如直接写个red多方便啊,那么请别忘了color是可以继承的!如果在一个外层元素中定义了一个颜色,里面的子元素都可以继承,用JS来控制的话只需要获取外层DOM元素然后修改它的color样式即可。


currentColor作为一个变量,可以用在 border、box-shadow、background、linear-gradient() 等一大堆的 CSS 属性上…甚至连svg中的 fill 和 stroke 都可以使用这个变量,它能做的事情很多,这里为了不跑题就先不展开讲,有兴趣的可以去搜一下。


:hover {
/* 红色字体 */
color: red;

/* 红色边框 */
border: 1px solid;
}
复制代码

修改后的代码如上,为什么没有currentColor?那是因为如果你不写的话,默认就是currentColor,这个关键字代表的就是你当前的color值。



大多数的候选人可能都不会写成这样,如果你作为面试官的话最好是适当的提示一下,看他能不能说出currentColor这个变量或者CSS变量



然后就是点击每个宫格弹出对应的数字,这个考察的是事件冒泡和事件代理:


父元素.addEventListener('click', e => alert(e.target.innerText))
复制代码

你可以观察一下候选人是把事件绑定在父元素上还是一个个的绑定在子元素上,这个问题按理说基本上都不会错。但如果发现候选人一个个把事件绑定在子元素上了,那就可以到此为止了,也不用浪费时间再去问别的问题了,可以十分装B的来一句:行,你的情况我已基本了解了,回去等通知吧!


接下来我们再来写一下完整一点的代码,以便引出下一个问题:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
/* 清除默认样式 */
* { padding: 0; margin: 0; }

/* 全屏显示 */
html, body { height: 100% }

body {
/* 网格布局 */
display: grid;

/* 子元素居中 */
place-items: center;
}

/* 父元素 */
ul {
width: 300px;

/* 清除默认样式 */
list-style: none;

/* 设置为flex布局 */
display: flex;

/* 设置换行 */
flex-flow: wrap;
}

/* 子元素 */
li {
/* 显示为网格布局 */
display: grid;

/* 子元素水平垂直居中 */
place-items: center;

/* 宽高都是100像素 */
width: 100px;
height: 100px;

/* 设置盒模型 */
box-sizing: border-box;

/* 设置1像素的边框 */
border: 1px solid black;

/* 负边距 */
margin: -1px 0 0 -1px;
}

/* 第1、4、7个子元素 */
li:nth-child(3n+1) {
/* 取消左负边距 */
margin-left: 0
}

/* 前三个子元素 */
li:nth-child(-n+3) {
/* 取消上负边距 */
margin-top: 0
}

/* 当鼠标经过时 */
li:hover {
/* 红色字体 */
color: red;

/* 红色边框 */
border: 1px solid;
}
</style>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
</ul>
<script>
// 选择ul元素
const ul = document.getElementsByTagName('ul')[0]

// 监听ul元素的点击事件
ul.addEventListener('click', e => alert(e.target.innerText))
</script>
</body>
</html>
复制代码

运行结果:



想知道为什么会这样吗?因为当前这个边框被后面的宫格压住了嘛!那么只需要当鼠标经过时不让后面的压住就好了(调高层级)。


说到调高层级,大家首先想到的可能就是z-index了,这个属性用的最多的地方可能就是绝对定位和固定定位了。但其实很少有人知道,z-index不是只能用在position: xxx的,万能的弹性盒子(display:flex)也是支持z-index的:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
/* 清除默认样式 */
* { padding: 0; margin: 0; }

/* 全屏显示 */
html, body { height: 100% }

body {
/* 网格布局 */
display: grid;

/* 子元素居中 */
place-items: center;
}

/* 父元素 */
ul {
width: 300px;

/* 清除默认样式 */
list-style: none;

/* 设置为flex布局 */
display: flex;

/* 设置换行 */
flex-flow: wrap;
}

/* 子元素 */
li {
/* 显示为网格布局 */
display: grid;

/* 子元素水平垂直居中 */
place-items: center;

/* 宽高都是100像素 */
width: 100px;
height: 100px;

/* 设置盒模型 */
box-sizing: border-box;

/* 设置1像素的边框 */
border: 1px solid black;

/* 负边距 */
margin: -1px 0 0 -1px;
}

/* 第1、4、7个子元素 */
li:nth-child(3n+1) {
/* 取消左负边距 */
margin-left: 0
}

/* 前三个子元素 */
li:nth-child(-n+3) {
/* 取消上负边距 */
margin-top: 0
}

/* 当鼠标经过时 */
li:hover {
/* 红色字体 */
color: red;

/* 红色边框 */
border: 1px solid;

/* 调高层级 */
z-index: 1;
}
</style>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
</ul>
<script>
// 选择ul元素
const ul = document.getElementsByTagName('ul')[0]

// 监听ul元素的点击事件
ul.addEventListener('click', e => alert(e.target.innerText))
</script>
</body>
</html>
复制代码

运行结果:



结语


没想到这么一个看似不起眼的九宫格一下子就能考察这么多内容吧!如果面试的时候直接问:



  • 你对 flex 了解的怎么样

  • 当元素的外边距为负值时会有什么样的行为

  • 请实现一下水平垂直居中

  • 了解过 grid 吗

  • 谈一下你对盒模型的理解

  • 说一下事件绑定和事件冒泡

  • CSS3的伪类选择器用的怎么样

  • 当页面元素重叠时如何控制哪个在上哪个在下

  • 在CSS中如何运用变量


直接这么问的话既浪费口舌,又显得很low,而且还不能筛选出真正能够灵活运用技术的候选人。


因为这些问题都不难,一般来说都能答出来,但具体能不能灵活运用就不一定了,而这一道九宫格,就像一面照妖镜一样,瞬间让人原形毕露!


如果你是候选人的话,那么一定要好好练习一下这道题。


如果是面试官的话,那么也推荐你用这道题来考察候选者的技术水平,如果能非常完美的做出来,那么基本上就不用再问其他的CSS题目了,日常开发所用到的样式基本难不倒他/她了,可以直接上JS面试题了。


但如果没做出来也不一定就代表这个人水平不行,可以试着提示一下候选者,然后再问一下其他的CSS题来确定一下此人的水平。


作者:手撕红黑树
来源:https://juejin.cn/post/6886770985060532231
收起阅读 »

仅靠H5标签就能实现收拉效果

前言 最近做项目时碰到这么一个需求: 这有点类似于手风琴效果,但不一样的是很多手风琴效果是同一时间内只能有一个展开,而这个是各个部分独立的,你展不展开完全不会影响我的展开与否。其实这种效果简直再普遍不过了,网上随便一搜就出来一大堆。但不一样的是,我在接到这个...
继续阅读 »

前言


最近做项目时碰到这么一个需求:



这有点类似于手风琴效果,但不一样的是很多手风琴效果是同一时间内只能有一个展开,而这个是各个部分独立的,你展不展开完全不会影响我的展开与否。其实这种效果简直再普遍不过了,网上随便一搜就出来一大堆。但不一样的是,我在接到这个需求的时候突然想起来很久以前看过张鑫旭大佬的一篇文章,模糊的记得那篇文章里说过有个什么很方便的 CSS 属性能够实现这一效果,不用像咱们平时实现的那些展开收起那样写很多的代码,于是就来到他的博客里面一顿搜,找了半天终于发现原来是我记错了,并不是什么 CSS3 属性,而是 HTML5 标签!


details


想要非常轻松的实现一个收拉效果,需要用到三个标签,分别是:<details><summary>以及随意


随意是什么意思?意思是什么标签都可以?


咱们先只写一个<details>标签来看看页面上会出现什么:


<details></details>
复制代码

运行结果:



可以看到非常有意思的一个现象:我们明明什么文字都没有写,但页面上却出现了详细信息这四个字,因为如果你在标签里没有写<summary>的话,浏览器会自动给你补上一个<summary>详细信息</summary>,那有人可能奇怪了,怎么补的是中文呢?那老外不写<summary>的话也会来一个<summary>详细信息</summary>?其实是这样:



现代浏览器经常偷偷获取用户隐私信息,包括但不仅限于用人工智能判断屏幕前的用户是中国人还是外国人,然后根据用户的母语来动态向<summary>标签里加入不同语言的'详细信息'这几个字。




开个玩笑,其实是根据你当前操作系统的语言来判断的,要是你把系统语言改成其它语言的话出现的就不再是'详细信息'这几个中文字符了。


那如果我们在<details>标签里写了<summary>呢?


<details>
<summary>公众号:</summary>
</details>
复制代码

运行结果:



可以看到<summary>里面的文字就会在三角箭头旁边的标题位置展示出来,可是我们展开三角箭头发现里面什么内容也没有,那么内容写在哪呢?


只需写在<summary>的后面就可以了,那是不是还要写个固定标签呢?比如什么<describe>之类的,其实在<summary>之后无论写什么标签都可以,当然必须得是合法的 HTML 标签啊,比如我们写个<h1>标签来试试看:


<details>
<summary>公众号:</summary>
<h1>前端学不动</h1>
</details>
复制代码

运行结果:



再换个别的标签试试:


<details>
<summary>公众号:</summary>
<button>前端学不动</button>
</details>
复制代码

运行结果:



看!我们仅用了三个标签就完成了一个最简单的收拉效果!以前在网上看到类似的效果要么就是 getElementById 获取到 DOM 元素,然后添加 onclick 事件控制下方元素的 style 属性,要么就是纯 CSS 实现,写几个单选按钮配合兄弟选择器来控制后方元素的显隐,抑或是 CSS 与 JS 相结合来实现的,但仅靠 HTML 标签来实现这一效果还是非常清新脱俗的!并且十分简洁、非常节约代码量、也更加直观易于理解。


深入测试


既然<summary>标签后面写什么都行,那么可不可以写很多个标签呢?我们来测试一下:


<details>
<summary>公众号:</summary>
<button>前端学不动</button>
<span>前端学不动</span>
<h1>前端学不动</h1>
<a href="#">前端学不动</a>
<strong>前端学不动</strong>
</details>
复制代码

运行结果:



那展开收起那部分的内容只能放在<summary>标签之后吗?如果放它前面呢:


<details>
<button>前端学不动</button>
<span>前端学不动</span>
<h1>前端学不动</h1>
<a href="#">前端学不动</a>
<strong>前端学不动</strong>
<summary>公众号:</summary>
</details>
复制代码

运行结果:



效果居然一模一样,看来展开收起的那部分应该是在<details>标签内部的除<summary>标签之外的所有内容。那如果写两个<summary>标签呢:


<details>
<button>前端学不动</button>
<span>前端学不动</span>
<h1>前端学不动</h1>
<a href="#">前端学不动</a>
<strong>前端学不动</strong>
<summary>公众号:</summary>
<summary>summary</summary>
</details>
复制代码

运行结果:



可以看到只有第一个出现的<summary>标签是真正的summary,后续出现的其他所有标签(包括其它的<summary>)都是展开收起的那部分。


既然所有标签都可以,那么也包括<details>咯?


<details>
<summary>project</summary>
<details>
<summary>html</summary>
index.html
</details>
<details>
<summary>css</summary>
reset.css
</details>
<details>
<summary>js</summary>
main.js
</details>
</details>
复制代码

运行结果:



这玩意有点意思,利用这种嵌套写法可以轻松实现编辑器左侧的那些文件区的效果。


加入样式


虽然可以很轻松、甚至在不用写 CSS 代码的情况下就实现展开收起效果,但毕竟不写 CSS 只是实现了个最基础的乞丐版效果,很多人都不想要点击的时候出现的那个轮廓:



在谷歌浏览器和 Safari 浏览器下都会出现这个轮廓,火狐就没有这玩意,咱们只需要给<summary>标签设置 outline 属性就可以了,一般如果你的项目引入了抹平浏览器样式间差异的 reset.css 文件的话,就不用写这个 CSS 了,为了方便同时观看 HTML、CSS 和 JS,我们来用 Vue 的格式来写代码:


<template>
<details>
<summary>project</summary>
<details>
<summary>html</summary>
index.html
</details>
<details>
<summary>css</summary>
reset.css
</details>
<details>
<summary>js</summary>
main.js
</details>
</details>
</template>

<style>
summary { outline: none }
</style>
复制代码

运行结果:



这样看起来就舒服多啦!但是还有个问题:那个三角箭头太傻大黑粗了,一般我们很少会用这样的箭头,而且我们也不一定非得让它在左边待着,那么怎么修改箭头的样式呢?


在谷歌浏览器以及 Safari 浏览器下我们需要用::-webkit-details-marker伪元素,在火狐浏览器下我们要用::-moz-list-bullet伪元素,比如我们想让它别那么傻大黑粗:


<template>
<details>
<summary>project</summary>
<details>
<summary>html</summary>
index.html
</details>
<details>
<summary>css</summary>
reset.css
</details>
<details>
<summary>js</summary>
main.js
</details>
</details>
</template>

<style>
summary { outline: none }

/* 谷歌、Safari */
::-webkit-details-marker {
transform: scale(.5);
color: gray
}

/* 火狐 */
::-moz-list-bullet { color: gray }
</style>
复制代码

运行结果:



是不是没那么傻大黑粗了,不过有时我们不想要这个三角形的箭头,想要的是自己自定义的箭头,那么我们就需要先把这个默认的三角给隐藏掉:


<template>
<details>
<summary>project</summary>
<details>
<summary>html</summary>
index.html
</details>
<details>
<summary>css</summary>
reset.css
</details>
<details>
<summary>js</summary>
main.js
</details>
</details>
</template>

<style>
summary { outline: none }

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }
</style>
复制代码

运行结果:



这回箭头没了,我们只需要在<summary>标签里写个箭头就好了,可以用::before::after伪元素,也可以直接在里面写个<img>标签,为了让大家能够直接复制代码到 Vue 环境里运行,在这里我们就不用图片了,直接手写<svg>


<template>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
project
</summary>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
html
</summary>
index.html
</details>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
css
</summary>
reset.css
</details>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
js
</summary>
main.js
</details>
</details>
</template>

<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
fill: none;
stroke: gray
}
</style>
复制代码

运行结果:



箭头是变成自定义的了,但是方向却不智能了,不能像原生箭头那样展开收起时会自动改变方向,但是<details>这个标签好就好在它在展开是会自动在标签里添加一个open属性:



我们可以利用它的这一特点,用属性选择器来让<svg>标签进行旋转:


<template>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
project
</summary>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
html
</summary>
index.html
</details>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
css
</summary>
reset.css
</details>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
js
</summary>
main.js
</details>
</details>
</template>

<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}

[open] > summary > svg { transform: none }
</style>
复制代码

运行结果:



用 JS 控制 open 属性


既然展开时会自动给<details>标签添加一个open属性,那如果我们用 JS 手动给<details>标签添加或删除open属性,<details>标签会随之展开收起吗?


比如我们用定时器,每隔1秒就自动展开一个,同时收起上一个已被展开过的标签:


<template>
<details v-for="({title, content}, index) of list" :key="title" :open="openIndex === index">
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
{{ content }}
</details>
</template>

<script>
import { defineComponent, ref, onBeforeUnmount } from 'vue'

export default defineComponent(() => {
const list = [{
title: 'html',
content: 'index.html'
}, {
title: 'css',
content: 'reset.css'
}, {
title: 'js',
content: 'main.js'
}]

const openIndex = ref(-1)

const interval = setInterval(() => openIndex.value === list.length
? openIndex.value = 0
: openIndex.value++
, 1000)

onBeforeUnmount(() => clearInterval(interval))

return { list, openIndex }
})
</script>

<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}

[open] > summary > svg { transform: none }
</style>
复制代码

运行结果:



既然能靠控制open属性来控制元素的展开收起,那么手风琴效果也很好实现了:只需要保证在当前列表中仅有一个<details>标签有open属性,点击别的标签时就去掉另一个标签的open属性即可:


<template>
<details
v-for="({title, content}, index) of list"
:key="title"
:open="openIndex === index"
@toggle="onChange($event, index)"
>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
{{ content }}
</details>
</template>

<script>
import { defineComponent, ref } from 'vue'

export default defineComponent(() => {
const list = [{
title: 'html',
content: 'index.html'
}, {
title: 'css',
content: 'reset.css'
}, {
title: 'js',
content: 'main.js'
}]

const openIndex = ref(-1)

const onChange = ({ target }, i) => target.open && (openIndex.value = i)

return { list, openIndex, onChange }
})
</script>

<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}

[open] > summary > svg { transform: none }
</style>
复制代码

运行结果:




⚠️需要注意的是,在<details>标签展开收起时会触发一个 toggle 事件,和 click、mousemove 等事件用法一致,也会接收一个 event 对象的参数,event.target 是当前触发事件的 DOM,也就是<details>,它会有一个.open属性,值为 true 或 false,代表是否展开收起。



加入动画


那么接下来离一个理想的手风琴效果只差最后一步了:过渡动画


但过渡动画这里有坑,我们先来分析一下思路:在平时就给<details>标签里的内容区(除第一个出现的

标签以外的内容)写上:max-height: 0;

然后在 open 时用属性选择器 [open] 配合后代选择器来给内容区加上 max-height: xxx; 的代码,这样平时在收起时高度就是0,等出现 open 属性时就会慢慢过渡到我们定义的最大高度:


<template>
<details
v-for="({title, content}, index) of list"
:key="title"
:open="openIndex === index"
@toggle="onChange($event, index)"
>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
<ul>
<li v-for="doc of content" :key="doc">{{ doc }}</li>
</ul>
</details>
</template>

<script>
import { defineComponent, ref } from 'vue'

export default defineComponent(() => {
const list = [{
title: 'html',
content: ['index.html', 'banner.html', 'login.html', '404.html']
}, {
title: 'css',
content: ['reset.css', 'header.css', 'banner.css', 'footer.css']
}, {
title: 'js',
content: ['index.js', 'main.js', 'javascript.js']
}]

const openIndex = ref(-1)

const onChange = ({ target }, i) => target.open && (openIndex.value = i)

return { list, openIndex, onChange }
})
</script>

<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}

details > ul {
max-height: 0;
margin: 0;
overflow: hidden;
}

[open] > summary > svg { transform: none }
[open] > ul { max-height: 120px }
</style>
复制代码

运行结果:



如果用谷歌浏览器打开的话居然看不到任何的过渡效果!但用火狐打开就有效果:



估计是浏览器的 bug,既然过渡动画(transition)在不同浏览器之间表现不一致,那关键帧动画(keyframes)呢?


<template>
<details
v-for="({title, content}, index) of list"
:key="title"
:open="openIndex === index"
@toggle="onChange($event, index)"
>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
<ul>
<li v-for="doc of content" :key="doc">{{ doc }}</li>
</ul>
</details>
</template>

<script>
import { defineComponent, ref } from 'vue'

export default defineComponent(() => {
const list = [{
title: 'html',
content: ['index.html', 'banner.html', 'login.html', '404.html']
}, {
title: 'css',
content: ['reset.css', 'header.css', 'banner.css', 'footer.css']
}, {
title: 'js',
content: ['index.js', 'main.js', 'javascript.js']
}]

const openIndex = ref(-1)

const onChange = ({ target }, i) => target.open && (openIndex.value = i)

return { list, openIndex, onChange }
})
</script>

<style lang="scss">
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}

details > ul {
max-height: 0;
margin: 0;
overflow: hidden;
}

[open] {
> summary > svg { transform: none }
> ul { animation: open .2s both }
}

@keyframes open {
to { max-height: 120px }
}
</style>
复制代码

运行结果:



可以看到关键帧动画在各大浏览器的行为都是一致的,推荐大家使用关键帧动画。


收起动画


上面那种效果已经完全足够满足我们的日常开发需求了,但它仍然有一个小小的遗憾,那就是:收起的时候没有任何的动画效果。



这是因为<details>的行为是靠着 open 属性控制内容显示或隐藏,你可以简单的把它的隐藏理解为display: block;display: none;,虽然这么说可能并不准确,但却非常有助于我们理解<details>的行为:在展开时display: block;突然显示,既然显示了就可以有时间展示我们的展开动画。但在收起时display: none;是突然消失,根本没时间展示我们的收起动画。



那么怎么才能解决这个问题呢?答案就是更改 DOM 结构,我们把原本放在<details>里面那部分需要展开收起的内容元素移到<details>标签的外面去,但一定要在它的后一位,这样就可以方便我们用兄弟选择器配合属性选择器来控制外部元素的显隐了,在<details>标签有 open 属性时我们就让它的后面一个元素用动画展开,没有 open 属性时我们就让后一个元素用动画收起:


<template>
<template v-for="({title, content}, index) of list" :key="title">
<details
:open="openIndex === index"
@toggle="onChange($event, index)"
>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
</details>
<ul>
<li v-for="doc of content" :key="doc">{{ doc }}</li>
</ul>
</template>
</template>

<script>
import { defineComponent, ref } from 'vue'

export default defineComponent(() => {
const list = [{
title: 'html',
content: ['index.html', 'banner.html', 'login.html', '404.html']
}, {
title: 'css',
content: ['reset.css', 'header.css', 'banner.css', 'footer.css']
}, {
title: 'js',
content: ['index.js', 'main.js', 'javascript.js']
}]

const openIndex = ref(-1)

const onChange = ({ target }, i) => target.open && (openIndex.value = i)

return { list, openIndex, onChange }
})
</script>

<style lang="scss">
summary {
position: relative;
padding-left: 20px;
outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}

ul {
max-height: 0;
margin: 0;
transition: max-height .2s;
overflow: hidden
}

[open] {
> summary > svg { transform: none }
+ ul { max-height: 120px }
}
</style>
复制代码

运行结果:



结语


如果你的项目不需要这些花里胡哨的动画效果,完全可以只靠 H5 标签去实现,根本不必再去关心展开收起的逻辑了,只需要写一些样式代码就可以了,比如写成暗黑模式:



你的 CSS 只需要专注于暗黑模式本身就够了,是不是很省心呢?


同时这个收拉效果也并不仅仅只适用于手风琴,很多地方都可以用到它,比如这种:


但唯一比较遗憾的事就是这个标签不支持 IE:



不过好在别的浏览器支持的都不错,如果你的项目不需要兼容 IE 的话就请尽情的享受<details>标签所带来的便利吧!


作者:手撕红黑树
来源:https://juejin.cn/post/6912374170743472135
收起阅读 »

后端一次给你10万条数据,如何优雅展示,面试官到底考察我什么

背景面试题:后台传给前端十万条数据,你作为前端如何渲染到页面上?回答者A:我有句话不知当讲不当讲,这什么鬼需求。回答者B:滚,后端,我不要这样的数据,你就不能分页给我吗。回答C:10万条数据这怎么展示,展示了也看不完啊。分析:面试官既然能这么问,我们从技术的角...
继续阅读 »

背景

面试题:后台传给前端十万条数据,你作为前端如何渲染到页面上?

回答者A:我有句话不知当讲不当讲,这什么鬼需求。

回答者B:滚,后端,我不要这样的数据,你就不能分页给我吗。

回答C:10万条数据这怎么展示,展示了也看不完啊。

分析:

面试官既然能这么问,我们从技术的角度出发,探索一下这道题,上手操作了一下:

function loadAll(response) {
  var html = "";
  for (var i = 0; i < 100000; i++) {
      html += "<li>title:" + '我正在测试'+[i] + "</li>";
  }
          $("#content").html(html);
}

在chorme浏览器下面 非常卡顿,刷新页面数据非常卡顿,渲染页面大概花掉10秒左右的时间,卡顿非常明显,性能瓶颈是在将html字符串插入到文档中这个过程上, 也就是性能瓶颈是在将html字符串插入到文档中这个过程上,也就是$("#content").html(html); 这句代码的执行, 毕竟有10万个li元素要被挺入到文档里面, 页面渲染速度缓慢也在情理之中。

解决方案

既然一次渲染10万条数据会造成页面加载速度缓慢,那么我们可以不要一次性渲染这么多数据,而是分批次渲染, 比如一次10000条,分10次来完成, 这样或许会对页面的渲染速度有提升。 然而,如果这13次操作在同一个代码执行流程中运行,那似乎不但无法解决糟糕的页面卡顿问题,反而会将代码复杂化。 类似的问题在其它语言最佳的解决方案是使用多线程,JavaScript虽然没有多线程,但是setTimeout和setInterval两个函数却能起到和多线程差不多的效果。 因此,要解决这个问题, 其中的setTimeout便可以大显身手。 setTimeout函数的功能可以看作是在指定时间之后启动一个新的线程来完成任务。

ajax 请求。。。。

function loadAll(response) {
  //将10万条数据分组, 每组500条,一共200组
  var groups = group(response);
  for (var i = 0; i < groups.length; i++) {
      //闭包, 保持i值的正确性
      window.setTimeout(function () {
          var group = groups[i];
          var index = i + 1;
          return function () {
              //分批渲染
              loadPart( group, index );
          }
      }(), 1);
  }
}

//数据分组函数(每组500条)
function group(data) {
  var result = [];
  var groupItem;
  for (var i = 0; i < data.length; i++) {
      if (i % 500 == 0) {
          groupItem != null && result.push(groupItem);
          groupItem = [];
      }
      groupItem.push(data[i]);
  }
  result.push(groupItem);
  return result;
}
var currIndex = 0;
//加载某一批数据的函数
function loadPart( group, index ) {
  var html = "";
  for (var i = 0; i < group.length; i++) {
      var item = group[i];
      html += "<li>title:" + item.title + index + " content:" + item.content + index + "</li>";
  }
  //保证顺序不错乱
  while (index - currIndex == 1) {
      $("#content").append(html);
      currIndex = index;
  }
}

思考:

面试官为啥会问这样的问题呢?现实中会有这样的需求吗? 我们从技术的角度思考,其实就是考察setTimetout的知识点。面试官就是换汤不换药。当然,其实这道题还有其他的解决方案,可以在评论区讨论学习。


作者:zz
来源:https://juejin.cn/post/6986237263164211207

收起阅读 »

前端人员不要只知道KFC,你应该了解 BFC、IFC、GFC 和 FFC

前言说起KFC,大家都知道是肯德基🍟,但面试官问你什么是BFC、IFC、GFC和FFC的时候,你是否能够像回答KFC是肯德基时的迅速,又或者说后面这些你根本就没听说过,作为一名前端开发工程师,以上这些FC(Forrmatting Context)你都得知道,而...
继续阅读 »

前言

说起KFC,大家都知道是肯德基🍟,但面试官问你什么是BFC、IFC、GFC和FFC的时候,你是否能够像回答KFC是肯德基时的迅速,又或者说后面这些你根本就没听说过,作为一名前端开发工程师,以上这些FC(Forrmatting Context)你都得知道,而且必须得做到像肯德基这样印象深刻。下面我将会带大家一起揭开这些FC的真面目,如果你已经了解的请奖励自己一顿肯德基~(注意文明用语,这里别用语气词😂)

FC的全称是:Formatting Contexts,译作格式化上下文,是W3C CSS2.1规范中的一个概念。它是页面中的一块渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及和其他元素的关系和相互作用。

CSS2.1中只有BFC和IFC,CSS3中才有GFC和FFC。

如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新的文章~

前置概念

在学习各种FC之前,我们先来了解几个基本概念:

Box(CSS布局基本单位)

简单来讲,我们看到的所有页面都是由一个个Box组合而成的,元素的类型和display属性决定了Box的类型。

  • block-level Box: 当元素的 CSS 属性 displayblock, list-itemtable 时,它是块级元素 block-level。块级元素(比如<p>)视觉上呈现为块,竖直排列。 每个块级元素至少生成一个块级盒(block-level Box)参与 BFC ,称为主要块级盒(principal block-level box)。一些元素,比如<li>,生成额外的盒来放置项目符号,不过多数元素只生成一个主要块级盒。

  • Inline-level Box: 当元素的 CSS 属性 display 的计算值为inline,inline-blockinline-table 时,称它为行内级元素。视觉上它将内容与其它行内级元素排列为多行。典型的如段落内容,有文本或图片,都是行内级元素。行内级元素生成行内级盒(inline-level boxes),参与行内格式化上下文 IFC 。

  • flex container: 当元素的 CSS 属性 display 的计算值为 flexinline-flex ,称它为弹性容器display:flex这个值会导致一个元素生成一个块级(block-level)弹性容器框。display:inline-flex这个值会导致一个元素生成一个行内级(inline-level)弹性容器框。

  • grid container:*当元素的 CSS 属性 display 的计算值为 gridinline-grid,称它为*栅格容器

块容器盒(block container box)

只包含其它块级盒,或生成一个行内格式化上下文(inline formatting context),只包含行内盒的叫做块容器盒子

也就是说,块容器盒要么只包含行内级盒,要么只包含块级盒。

块级盒(block-level Box)是描述元素跟它的父元素与兄弟元素之间的表现。

块容器盒(block container box)描述元素跟它的后代之间的影响。

块盒(BLock Boxes)

同时是块容器盒的块级盒称为块盒(block boxes)

行盒(Line boxes)

行盒由行内格式化上下文(inline formatting context)产生的盒,用于表示一行。在块盒里面,行盒从块盒一边排版到另一边。 当有浮动时, 行盒从左浮动的最右边排版到右浮动的最左边。

OK,了解完上面这些概念,我们再来看我们本篇文章的重点内容(终于要揭开各种FC的庐山真面目了,期待~)

BFC(Block Formatting Contexts)块级格式化上下文

什么是BFC?

BFC 全称:Block Formatting Context, 名为 块级格式化上下文

W3C官方解释为:BFC它决定了元素如何对其内容进行定位,以及与其它元素的关系和相互作用,当涉及到可视化布局时,Block Formatting Context提供了一个环境,HTML在这个环境中按照一定的规则进行布局。

如何触发BFC?

  • 根元素或其它包含它的元素

  • 浮动 float: left/right/inherit

  • 绝对定位元素 position: absolute/fixed

  • 行内块display: inline-block

  • 表格单元格 display: table-cell

  • 表格标题 display: table-caption

  • 溢出元素 overflow: hidden/scroll/auto/inherit

  • 弹性盒子 display: flex/inline-flex

BFC布局规则

  • 内部的Box会在垂直方向,一个接一个地放置。

  • Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生重叠。

  • 每个元素的margin box的左边, 与包含块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。

  • BFC的区域不会与float box重叠。

  • BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。

  • 计算BFC的高度时,浮动元素也参与计算

BFC应用场景

解决块级元素垂直方向margin重叠

我们来看下面这种情况:

<style>
 .box{
   width:180px;
   height:180px;
   background:rosybrown;
   color:#fff;
   margin: 60px auto;
}
</style>
<body>
   <div class="box">nanjiu</div>
   <div class="box">南玖</div>
</body>

按我们习惯性思维,上面这个box的margin-bottom60px,下面这个box的margin-top也是60px,那他们垂直的间距按道理来说应该是120px才对。(可事实并非如此,我们可以来具体看一下)

bfc1.png 从图中我们可以看到,两个box的垂直间距只有60px,并不是120px!

这种情况下的margin边距为两者的最大值,而不是两者相加,那么我们可以使用BFC来解决这种margin塌陷的问题。

<style>
 .box{
   width:180px;
   height:180px;
   background:rosybrown;
   color:#fff;
   margin: 60px auto;
}
 .outer_box{
   overflow: hidden;
}
</style>
<body>
   <div class="outer_box">
       <div class="box">nanjiu</div>
   </div>
   <div class="box">南玖</div>
</body>

bfc2.png 由上面可以看到,我们通过给第一个box外面再包裹一层容器,并触发它形成BFC,此时的两个box就不属于同一个BFC了,它们的布局互不干扰,所以这时候他们的垂直间距就是两者间距相加了。

解决高度塌陷问题

我们再来看这种情况,内部box使用float脱离了普通文档流,导致外层容器没办法撑起高度,使得背景颜色没有显示出来。

<style>
 .box{
   float:left;
   width:180px;
   height:180px;
   background:rosybrown;
   color:#fff;
   margin: 60px;
}
 .outer_box{
   background:lightblue;
}
</style>
<body>
   <div class="outer_box">
       <div class="box">nanjiu</div>
       <div class="box">南玖</div>
   </div>
</body>

bfc3.png 从这张图,我们可以看到此时的外层容器的高度为0,导致背景颜色没有渲染出来,这种情况我们同样可以使用BFC来解决,可以直接为外层容器触发BFC,我们来看看效果:

<style>
 .box{
   float:left;
   width:180px;
   height:180px;
   background:rosybrown;
   color:#fff;
   margin: 60px;
}
.outer_box{
 display:inline-block;
 background:lightblue;
}
</style>
<body>
   <div class="outer_box">
       <div class="box">nanjiu</div>
       <div class="box">南玖</div>
   </div>
</body>

bfc4.png

清除浮动

在早期前端页面大多喜欢用浮动来布局,但浮动元素脱离普通文档流,会覆盖旁边内容:

<style>
.aside {
 float: left;
 width:180px;
 height: 300px;
 background:lightpink;
}
 .container{
   width:500px;
   height:400px;
   background:mediumturquoise;
}
</style>
<body>
   <div class="outer_box">
       <div class="aside">nanjiu</div>
       <div class="container">南玖</div>
   </div>
</body>

bfc5.png 我们可以通过触发后面这个元素形成BFC,从而来清楚浮动元素对其布局造成的影响

<style>
.aside {
 float: left;
 width:180px;
 height: 300px;
 background:lightpink;
}
 .container{
   width:500px;
   height:400px;
   background:mediumturquoise;
   overflow: hidden;
}
</style>
<body>
   <div class="outer_box">
       <div class="aside">nanjiu</div>
       <div class="container">南玖</div>
   </div>
</body>

bfc6.png

IFC(Inline Formatting Contexts)行内级格式化上下文

什么是IFC?

IFC全称:Inline Formatting Context,名为行级格式化上下文

如何触发IFC?

  • 块级元素中仅包含内联级别元素

形成条件非常简单,需要注意的是当IFC中有块级元素插入时,会产生两个匿名块将父元素分割开来,产生两个IFC。

IFC布局规则

  • 在一个IFC内,子元素是水平方向横向排列的,并且垂直方向起点为元素顶部。

  • 子元素只会计算横向样式空间,【padding、border、margin】,垂直方向样式空间不会被计算,【padding、border、margin】。

  • 在垂直方向上,子元素会以不同形式来对齐(vertical-align)

  • 能把在一行上的框都完全包含进去的一个矩形区域,被称为该行的行框(line box)。行框的宽度是由包含块(containing box)和与其中的浮动来决定。

  • IFC中的line box一般左右边贴紧其包含块,但float元素会优先排列。

  • IFC中的line box高度由 CSS 行高计算规则来确定,同个IFC下的多个line box高度可能会不同。

  • inline boxes的总宽度少于包含它们的line box时,其水平渲染规则由 text-align 属性值来决定。

  • 当一个inline box超过父元素的宽度时,它会被分割成多个boxes,这些boxes分布在多个line box中。如果子元素未设置强制换行的情况下,inline box将不可被分割,将会溢出父元素。

IFC应用场景

元素水平居中

当一个块要在环境中水平居中时,设置其为inline-block则会在外层产生IFC,通过text-align则可以使其水平居中。

<style>
/* IFC */
 .text_container{
   width: 650px;
   border: 3px solid salmon;
   margin-top:60px;
   text-align: center;
}
 strong,span{
   /* border:1px solid cornflowerblue; */
   margin: 20px;
   background-color: cornflowerblue;
   color:#fff;
}
</style>
<body>
   <div class="text_container">
       <strong>众里寻他千百度,南玖需要你关注</strong>
       <span>蓦然回首,那人却在,南玖前端交流群</span>
   </div>
</body>

ifc1.png

多行文本水平垂直居中

创建一个IFC,然后设置其vertical-align:middle,其他行内元素则可以在此父元素下垂直居中。

<style>
.text_container{
 text-align: center;
 line-height: 300px;
 width: 100%;
 height: 300px;
 background-color: turquoise;
 font-size: 0;
}
 p{
   line-height: normal;
   display: inline-block;
   vertical-align: middle;
   background-color: coral;
   font-size: 18px;
   padding: 10px;
   width: 360px;
   color: #fff;
}
</style>
<body>
 <div class="text_container">
   <p>
    东风夜放花千树,更吹落,星如雨。宝马雕车香满路。凤箫声动,玉壶光转,一夜鱼龙舞。蛾儿雪柳黄金缕,笑语盈盈暗香去。
     <strong>众里寻他千百度,蓦然回首,那人却在,灯火阑珊处。</strong>
   </p>
 </div>
</body>

ifc2.png

GFC(Grid Formatting Contexts)栅格格式化上下文

什么是GFC?

GFC全称:Grids Formatting Contexts,名为网格格式上下文

简介: CSS3引入的一种新的布局模型——Grids网格布局,目前暂未推广使用,使用频率较低,简单了解即可。 Grid 布局与 Flex 布局有一定的相似性,都可以指定容器内部多个项目的位置。但是,它们也存在重大区别。 Flex 布局是轴线布局,只能指定"项目"针对轴线的位置,可以看作是一维布局。Grid 布局则是将容器划分成"行"和"列",产生单元格,然后指定"项目所在"的单元格,可以看作是二维布局。Grid 布局远比 Flex 布局强大。

如何触发GFC?

当为一个元素设置display值为grid或者inline-grid的时候,此元素将会获得一个独立的渲染区域。

GFC布局规则

通过在网格容器(grid container)上定义网格定义行(grid definition rows)网格定义列(grid definition columns)属性各在网格项目(grid item)上定义网格行(grid row)和网格列(grid columns)为每一个网格项目(grid item)定义位置和空间(具体可以在MDN上查看)

GFC应用场景

任意魔方布局

这个布局使用用GFC可以轻松实现自由拼接效果,换成其他方法,一般会使用相对/绝对定位,或者flex来实现自由拼接效果,复杂程度将会提升好几个等级。

<style>
.magic{
display: grid;
grid-gap: 2px;
width:300px;
height:300px;
}
.magic div{
border: 1px solid coral;
}
.m_1{
grid-column-start: 1;
grid-column-end: 3;
}
.m_3{
grid-column-start: 2;
grid-column-end: 4;
grid-row-start: 2;
grid-row-end: 3;
}
</style>
<body>
<div>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
<div>6</div>
<div>7</div>
</div>
</body>

gfc1.png

FFC(Flex Formatting Contexts)弹性格式化上下文

什么是FFC?

FFC全称:Flex Formatting Contexts,名为弹性格式上下文

简介: CSS3引入了一种新的布局模型——flex布局。 flex是flexible box的缩写,一般称之为弹性盒模型。和CSS3其他属性不一样,flexbox并不是一个属性,而是一个模块,包括多个CSS3属性。flex布局提供一种更加有效的方式来进行容器内的项目布局,以适应各种类型的显示设备和各种尺寸的屏幕,使用Flex box布局实际上就是声明创建了FFC(自适应格式上下文)

如何触发FFC?

display 的值为 flexinline-flex 时,将生成弹性容器(Flex Containers), 一个弹性容器为其内容建立了一个新的弹性格式化上下文环境(FFC)

FFC布局规则

  • 设置为 flex 的容器被渲染为一个块级元素

  • 设置为 inline-flex 的容器被渲染为一个行内元素

  • 弹性容器中的每一个子元素都是一个弹性项目。弹性项目可以是任意数量的。弹性容器外和弹性项目内的一切元素都不受影响。简单地说,Flexbox 定义了弹性容器内弹性项目该如何布局

⚠️注意:FFC布局中,float、clear、vertical-align属性不会生效。

Flex 布局是轴线布局,只能指定"项目"针对轴线的位置,可以看作是一维布局。Grid 布局则是将容器划分成"行"和"列",产生单元格,然后指定"项目所在"的单元格,可以看作是二维布局。Grid 布局远比 Flex 布局强大。

FFC应用场景

这里只介绍它对于其它布局所相对来说更方便的特点,其实flex布局现在是非常普遍的,很多前端人员都喜欢用flex来写页面布局,操作方便且灵活,兼容性好。

自动撑开剩余高度/宽度

看一个经典两栏布局:左边为侧边导航栏,右边为内容区域,用我们之前的常规布局,可能就需要使用到csscalc方法来动态计算剩余填充宽度了,但如果使用flex布局的话,只需要一个属性就能解决这个问题:

calc动态计算方法:

<style>
.outer_box {
width:100%;
}
.aside {
 float: left;
 width:180px;
 height: 300px;
 background:lightpink;
}
.container{
 width:calc(100% - 180px);
 height:400px;
 background:mediumturquoise;
 overflow: hidden;
}
</style>
<body>
<div class="outer_box">
       <div class="aside">nanjiu</div>
       <div class="container">南玖</div>
   </div>
</body>

ffc.gif 使用FFC:

<style>
.outer_box {
 display:flex;
width:100%;
}
.aside {
 float: left;
 width:180px;
 height: 300px;
 background:lightpink;
}
.container{
 flex: 1;
 height:400px;
 background:mediumturquoise;
 overflow: hidden;
}
</style>
<body>
<div class="outer_box">
       <div class="aside">nanjiu</div>
       <div class="container">南玖</div>
   </div>
</body>

ffc2.gif

总结

一般来说,FFC能做的事情,通过GFC都能搞定,反过来GFC能做的事通过FFC也能实现。 通常弹性布局使用FFC,二维网格布局使用GFC,所有的FFC与GFC也是一个BFC,在遵循自己的规范的情况下,向下兼容BFC规范。

现在所有的FC都介绍完了,了解清楚的去奖励自己一顿KFC吧😄~

推荐阅读

作者:南玖
来源:https://juejin.cn/post/7072174649735381029

收起阅读 »

经验复盘-力扣刷题给我带来了什么?

起因 虽然在去年跳槽前有刷过一段时间的力扣,但是一直都没有将自己的刷题的过程记录下来。直到2021年5月,掘金举办了一个Java 刷题打卡的活动,我才开始将自己的第一篇刷题文章发布在掘金👇 后面掘金又举办了2021年6月更文挑战活动,活动大奖要求有 30 篇...
继续阅读 »

起因


虽然在去年跳槽前有刷过一段时间的力扣,但是一直都没有将自己的刷题的过程记录下来。直到2021年5月,掘金举办了一个Java 刷题打卡的活动,我才开始将自己的第一篇刷题文章发布在掘金👇


image.png


后面掘金又举办了2021年6月更文挑战活动,活动大奖要求有 30 篇文章,最终这次活动中我输出了 23 篇力扣刷题的文章。


至今为止,我在掘金上已发表了 124 个力扣题解,有兴趣的话可以关注我的专栏:力扣刷题


image.png


我是怎么做的?


1. 代码存储


为了更方便的调试代码以及存储自己写的测试用例,所以我在Github上建了一个私人库用于存储代码。这样做的好处就是:随时随地获取你的历史代码、而且可以保留自己写的测试用例。我非常推荐你也使用仓库来存储自己的解题代码!


image.png


2.模仿和总结


刚开始刷题的时候我只会简单的暴力求解,所以在提交第一题的时候击败率连 10% 都不到,后来通过看官方题解发现了可以使用 HashMap 用空间换取时间(因 HashMap 中链表节点超过8时会转为红黑树)。


所以在刚开始很长一段时间我都是 题目看完 -> 思考15分钟,没有思路 -> 看官方题解,但经过了一段时间后我发现自己的提升很小。就像是一种脑子会了,手没会的感觉,下次碰到类似的题目还是不会做。


后来经过不断的摸索,我刷题的步骤大概是这样的:



  1. 看题目,列出题目中的重点

  2. 想清楚解题的关键和思路(碰到链表或二叉树相关的题目,我一般都会在草稿纸上画一下)

  3. 实在不会的可以看一下官方题解,但是不抄官方的代码

  4. 运行调试:写好代码后,将力扣的测试用例写在main方法中测试(像一些二叉树的题目就需要根据官方的二叉树数组自己转为 TreeNode 了)

  5. 提交代码,然后再在本地调试不通过的测试用例(一般来说需要调试二十分钟左右,不要着急!)

  6. 直到通过所有的测试用例


经过这段时间的刷题,我也有了一些小感悟:



  • 算法的本质就是利用空间换区时间

  • 有时候看到数据结构就能猜到用什么算法:例如碰到 二叉树 就会想到 递归、碰到 截取字符串 会想到 滑动窗口、碰到大问题可以分解的情况会想到 动态规划

  • 使用 HashMap 替代数组或链表,可有效降低时间复杂度

  • 遍历链表时一般都需要临时指针指向头结点

  • ......


我收获了什么?


虽然刷题碰到的一些算法在实际业务中无法很好落地,但是刷题能够对数据结构有更深刻的理解调试能力的提升(先将步骤分解,然后寻找非预期的输入或输出)、锻炼思维能力、以及对代码执行的效率更敏感了(减少时间复杂度)。


实际项目如何运用?



下面我举得这个例子就可以很好的体现:递归算法的运用和提高执行效率。



在常见的业务系统中,不可避免的会需要动态菜单和权限的功能。动态菜单本质上是一个 ,而某个菜单路由下面的权限就像是树中的 叶子节点。但是数据库并不能够存储树这样的结构,那怎么办呢?


常规的树形结构的路由菜单如下所示:


image.png


设计的数据库表如下所示:


parentId:表示父节点的Id  
menuType:当类型为 C 标识为路由下的权限,即二叉树中的叶子节点

image.png


实际业务需要将查找出来的列表转为树形结构返回给前端,这怎么实现呢?大致的思路如下所示:


    /**
* list转为树结构
*/
private List<MenuBO> list2Tree(List<MenuBO> list, Integer pId) {
List<MenuBO> tree = new ArrayList<>();
Iterator<MenuBO> it = list.iterator();
while (it.hasNext()) {
MenuBO m = it.next();
if (m.getParentId() == pId) {
tree.add(m);
// 已添加的元素删除掉
it.remove();
}
}
// 寻找子元素
tree.forEach(n -> n.setChildren(list2Tree(list, n.getId())));
return tree;
}

这段代码的注释比较清楚,就是找到原列表中所有 pid 相同的元素放入新列表,并将新列表设为孩子节点,直到所有节点都遍历完成。又因为一行记录只会成为树中的一个节点,故每个元素只需遍历一次。所以再将元素放入到新列表后,就在原列表中将此元素移除。算法复杂度从 O(N*logN) 变为了 0(N),大大提升了执行效率。


总结


去年的这个时候我是零基础开始刷题的,刚开始写一个中等难度的题目需要一两个小时,再加上写完题目后还要输出一篇文章,一般都要搞到凌晨左右。但现在我简单题已经可以重拳出击了,中等难度的题目解题的时间更少了,至于困难题还是需要看题解。


从我一年多刷题的经历来看,我总结出来两句话:



  1. 万事开头难

  2. 实践才是检验真理的唯一标准!


所以行动起来吧!就现在!



最后我想感谢一下稀土掘金!来这里不仅能刷沸点段子,还能写文章拿奖品。不得不说,掘金的活动实在是太多了(PS:奖品也很多)!


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

Flutter终极大杀器,一个它顶四个库!

每次新建Flutter项目,是我最痛苦的时间,每次去pub仓库找库,再复制粘贴到 pubspec.yaml ,再执行 flutter pub get 。这套操作往往需要重复十几次。毕竟Flutter大到路由,状态管理,小到工具类,国际化都需要库来支持,等找齐这...
继续阅读 »

每次新建Flutter项目,是我最痛苦的时间,每次去pub仓库找库,再复制粘贴到 pubspec.yaml ,再执行 flutter pub get 。这套操作往往需要重复十几次。毕竟Flutter大到路由,状态管理,小到工具类,国际化都需要库来支持,等找齐这些东西,终于可以准备开发的时候,半天已经过去了。


所幸,我在pub仓库发现了它,GetX,这玩意是真的牛皮,使用它大大小小开发了四五个项目后,确定了稳定性和性能后,决定进行分享一波。


本文只是简单分享,GetX没有官方文档,只有github的README,所以我结合自己的经验,整理了一份。


github


gitee


GetX为何物?




  • GetX 是 Flutter 上的一个轻量且强大的解决方案:高性能的状态管理、智能的依赖注入和便捷的路由管理。




  • GetX 有3个基本原则:



    • 性能: GetX 专注于性能和最小资源消耗。GetX 打包后的apk占用大小和运行时的内存占用与其他状态管理插件不相上下。如果你感兴趣,这里有一个性能测试

    • 效率: GetX 的语法非常简捷,并保持了极高的性能,能极大缩短你的开发时长。

    • 结构: GetX 可以将界面、逻辑、依赖和路由完全解耦,用起来更清爽,逻辑更清晰,代码更容易维护。




  • GetX 并不臃肿,却很轻量。如果你只使用状态管理,只有状态管理模块会被编译,其他没用到的东西都不会被编译到你的代码中。它拥有众多的功能,但这些功能都在独立的容器中,只有在使用后才会启动。




GetX能干什么?


GetX包含的功能:



  • 状态管理

  • 路由

  • 国际化

  • 工具类

  • IDE拓展

  • 工程化Cli

  • ......


GetX的优点?


对于一个菜鸟来说,它最大的优点当然是 简单易上手


举几个例子:


状态管理


创建一个 Controller 管理你的状态变量


class Controller extends GetxController{
var count = 0.obs;
increment() => count++;
}

然后直接使用


class Home extends StatelessWidget {

@override
Widget build(context) {

// 使用Get.put()实例化你的类,使其对当下的所有子路由可用。
final Controller c = Get.put(Controller());

return Scaffold(
// 使用Obx(()=>每当改变计数时,就更新Text()。
appBar: AppBar(title: Obx(() => Text("Clicks: ${c.count}"))),

// 用一个简单的Get.to()即可代替Navigator.push那8行,无需上下文!
body: Center(child: ElevatedButton(
child: Text("Go to Other"), onPressed: () => Get.to(Other()))),
floatingActionButton:
FloatingActionButton(child: Icon(Icons.add), onPressed: c.increment));
}
}

class Other extends StatelessWidget {
// 你可以让Get找到一个正在被其他页面使用的Controller,并将它返回给你。
final Controller c = Get.find();

@override
Widget build(context){
// 访问更新后的计数变量
return Scaffold(body: Center(child: Text("${c.count}")));
}
}

你只需要 put 一个 Controller 后,再将 widget 包裹在 Obx 中,这样就将 count 绑定在了你的 widget 中,只要 count 发生改变, widget 就很跟着更新。



注意,Controller是与Widget解耦的,只有进行了put才会进行绑定,所以是局部状态还是全局状态完全由你自己决定。



路由


GetX的路由最大的特点就是,不需要 context ,直接使用即可


导航到新页面


Get.to(NextScreen());

用别名导航到新页面。


Get.toNamed('/details');

要关闭snackbars, dialogs, bottomsheets或任何你通常会用Navigator.pop(context)关闭的东西。


Get.back();

进入下一个页面,但没有返回上一个页面的选项(用于闪屏页,登录页面等)。


Get.off(NextScreen());

进入下一个页面并取消之前的所有路由(在购物车、投票和测试中很有用)。


Get.offAll(NextScreen());

国际化


GetX的国际化尤其简单,我们只需要新建一个 Translations


import 'package:get/get.dart';

class Messages extends Translations {
@override
Map<String, Map<String, String>> get keys => {
'zh_CN': {
'hello': '你好 世界',
},
'de_DE': {
'hello': 'Hallo Welt',
}
};
}

并且将你的 MaterialApp 更改为 GetMaterialApp ,并绑定上刚刚创建的 Translations 类。



不用担心,GetMaterialApp支持所有MaterialApp的接口,它们是兼容的



return GetMaterialApp(
translations: Messages(), // 你的翻译
locale: Locale('zh', 'CN'), // 将会按照此处指定的语言翻译
fallbackLocale: Locale('en', 'US'), // 添加一个回调语言选项,以备上面指定的语言翻译不存在
);

然后直接使用


Text('title'.tr);

是的,你只需要在字符串后面加上 .tr 即可使用国际化功能


GetX Cli是何物?


GetX Cli是一个命令行脚本,类似vue cli,但更强大一些,它可以做到:



  • 创建项目

  • 项目工程化

  • 生成Model

  • 生成page

  • 生成view

  • 生成controller

  • 自定义controller模板

  • 生成翻译文件

  • ......


想要使用GetX Cli,你需要安装dart环境或者Flutter环境


然后直接安装即可使用


pub global activate get_cli 
# or
flutter pub global activate get_cli

创建项目


get create project:my_project

这个命令会调用 flutter create ,然后再执行 get init


项目工程化


get init

生成page


get create page:home

生成controller


get create controller:dialogcontroller on home

生成view


get create view:dialogview on home

生成model


get generate model on home with assets/models/user.json

更多详细命令去看文档


IDE拓展



结语


祝大家在编程路上飞黄腾达,越走越远。


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

Flutter桌面端开发——选择读取本地文件

file_selector 安装🛠 点击file_selector获取最新版本。以下是在编写本文章时的最新版本: file_selector: ^0.8.4+1 👻注意:在开发 macOS 端的程序时,还需要额外的操作,具体可以查看这里 了解🧩 在 file_...
继续阅读 »

file_selector


安装🛠


点击file_selector获取最新版本。以下是在编写本文章时的最新版本:


file_selector: ^0.8.4+1

👻注意:在开发 macOS 端的程序时,还需要额外的操作,具体可以查看这里


了解🧩


在 file_selector 插件中,我们需要了解对象 XTypeGroup 和方法 openFiles。


XTypeGroup的作用是使用给定的标签和文件扩展名创建一个新组,没有提供任何类型选项的组表示允许任何类型。


XTypeGroup 可以传入5个参数:



  • String? label:用来自己作区分

  • List<String>? extensions:过滤非描述的后缀文件,默认加载全部类型的文件

  • List<String>? mimeTypes:主要针对 Linux 系统下的文件类型

  • List<String>? macUTIs:主要针对 mac 系统下的文件类型

  • List<String>? webWildCards:主要针对网页开发时的类型


openFile 方法返回一个 XFile 对象,可以传入3个参数:



  • List acceptedTypeGroups = const []:接受的类型组,传入一个XTypeGroup列表

  • String? initialDirectory:初始化文件夹。打开文件对话框以加载文件并返回文件路径

  • String? confirmButtonText:弹窗“打开”按钮的显示文字


openFiles 方法返回 XFile 对象列表,传入的参数和 openFile 一样。


使用🥩


选择的文件我们最后需要读取出来,读取需要接受一个 String 类型的路径:


String path = '';

这里先以选择图片为例,我们需要先定义一个 XTypeGroup 对象:


final xType = XTypeGroup(label: '图片', extensions: ['jpg', 'png']);

选择单张图片


打开弹窗选取单个文件,使用 openFile 方法:


final XFile? file = await openFile(acceptedTypeGroups: [xType]);

将获取到的 XFile 对象的路径值传给 path。当然,并不是每次打开弹窗都会选择图片,所以需要判断一下:


if (file != null) {
path = file.path;
setState((){});
} else {
BotToast.showText(text: '你不选择图片打开干啥😤');
}

image


openFile 方法中还有两个属性,我们修改试一下:


final XFile? file = await openFile(
acceptedTypeGroups: [xType],
initialDirectory: r'C:\Users\ilgnefz\Pictures',
confirmButtonText: '嘿嘿嘿',
);

image


initialDirectory 属性貌似没用😕去看了官方的例子,也没用到过这个参数,以后就忽略它吧。


选择多张图片


选取多张图片,我们就需要定义一个路径的数组了:


final List<String> paths = []

XTypeGroup 对象和刚才的一样就行,重要的是使用 openFiles 方法:


final List<XFile> files = await openFiles(acceptedTypeGroups: [xType]);

将获取到的文件路径列表赋值给 paths:


if (file != null) {
paths.addAll(files.map((e) => e.path).toList());
setState((){});
} else {
BotToast.showText(text: '你不选择图片打开干啥😤');
}

好了,来看看效果如何


image


读取文本文件


读取文本文件,我们需要获取文件的名称和内容:


final String title = '';
final String content = '';

再更改一下 XTypeGroup 对象就行:


final XTypeGroup xType = XTypeGroup(label: '文本', extensions: ['txt']);
final XFile? file = await openFile(acceptedTypeGroups: [xType]);

将获取到的 XFile 对象的属性赋值给我们定义的对象:


if (file != null) {
title = file.name;
content = await file.readAsString();
setState((){});
} else {
BotToast.showText(text: '打开了个寂寞🙄');
}

image


存储文本文件


存储文本需要用到 XFile 对象中的 fromData 方法。让我们来看看这个方法中需要传入什么参数:



  • Uint8List bytes:存储的主要内容

  • String? mimeType:文件的 mine 类型

  • String? name:文件名?测试了毫无用处😑

  • int? length:不知道是什么的长度,反正无法截取内容😑

  • DateTime? lastModified:最后修改文件的时间

  • String? path:文件保存的路径?测试了毫无效果😑

  • CrossFileTestOverrides? overrides:覆盖CrossFile的某些方法用来测试


(以上几个参数要是有朋友测试出来了,可以告知一下😁)


在存储文本文件前,我们需要先知道应该存储在哪个文件夹:


final String? path = await getSavePath();

然后再把 XFile.fromData 需要的参数放进去:


if (path != null) {
// 将内容编码成utf8
final Uint8List fileData = const Utf8Encoder().convert(content);
const String fileMimeType = 'text/plain';
final XFile xFile = XFile.fromData(
fileData,
mimeType: fileMimeType,
name: title,
);
await xFile.saveTo(path);
} else {
BotToast.showText(text: '给你个眼神自己体会😑');
}

5


获取文件夹路径


读取文件夹路径需要使用 getDirectoryPath 方法:


final String? path = await getDirectoryPath();
if (path != null) {
title = '目录';
content = path;
setState((){});
}

6


file_picker


安装🛠


点击file_picker获取最新版本。以下是在编写本文章时的最新版本:


file_picker: ^4.5.1

使用🥩


先定义一个默认的路径:


String path = '';

选择单个文件


选择单个文件需要用到 pickFiles 方法,该方法可以传入10个参数:



  • String? dialogTitle:弹窗的标题

  • String? initialDirectory:初始化的文件夹

  • FileType type = FileType.any:文件的类型

  • List<String>? allowedExtensions:允许的文件后缀名称

  • dynamic Function(FilePickerStatus)? onFileLoading:监听文件选择的状态

  • bool allowCompression = true:是否允许压缩

  • bool allowMultiple = false:是否允许选择多个文件

  • bool withData = false:如果为true,选取的文件将在内存中立即以“Uint8List”的形式提供其字节数据,如果您选择它进行服务器上传或类似操作,这将很有用。但是,请记住,如果您允许多个选择或选择大文件,则在 IO(iOS 和 Android)上启用此功能可能会导致内存不足问题。请改用 [withReadStream]。在 web 上默认为 true,其他为 false

  • bool withReadStream = false:拾取的文件将以 [Stream<List>] 的形式提供其字节数据,这对于上传和处理大文件很有用

  • bool lockParentWindow = false:是否将子窗口(文件选择器窗口)一直停留在 Flutter 窗口的前面,直到它关闭(如模态窗口)。此参数仅适用于 Windows


FilePickerResult? result = await FilePicker.platform.pickFiles();
if (result != null) {
File file = File(result.files.single.path!);
path = file.path;
setState((){});
}

image


我们试着添加一些参数:


FilePickerResult? result = await FilePicker.platform.pickFiles(
dialogTitle: '我的地盘我做主',
initialDirectory: r'C:\Users\ilgnefz\Pictures\Saved Pictures',
type: FileType.image,
);

8


initialDirectory 又没起作用😑


选择多个文件


定义一个接受所有路径的数组:


final List<String> paths = [];

FilePickerResult? result = await FilePicker.platform.pickFiles(
allowMultiple: true,
);
if (result != null) {
paths = result.files.map((e) => e.path!).toList();
setState((){});
}

读取文件信息


通过以上的方法,我们会得到一个 PlatformFile 对象:


FilePickerResult? result = await FilePicker.platform.pickFiles();
PlatformFile file = result.files.single;

该对象有以下几个属性:



  • name:文件名称

  • size:文件大小,以字节为单位

  • bytes:此文件的字节数据。如果您想操作其数据或轻松上传到其他地方,则特别有用。 在常见问题解答中查看此处 一个关于如何使用它在网络上上传的示例。

  • extension:文件后缀

  • path:文件路径

  • identifier:原始文件的平台标识符,是指 Android 上的 Uri 和 iOS 上的 NSURL。其他为null

  • readStream:将文件内容转换成流读取


1


存储文件


存储文件需要使用 saveFile 方法,该方法有可以传入6个参数:



  • String? dialogTitle:同 pickFiles 方法

  • String? fileName:存储文件的名字

  • String? initialDirectory:同 pickFiles 方法

  • FileType type = FileType.any:同 pickFiles 方法

  • List ? allowedExtensions:同 pickFiles 方法

  • bool lockParentWindow = false:同 pickFiles 方法


String? outputFile = await FilePicker.platform.saveFile();

(⊙o⊙)…这个方法连保存内容的参数都没有,诶!就是玩😄。官方说这个方法没有实际意义。


获取文件夹路径


获取文件夹需要使用 getDirectoryPath 方法,可以传入3个参数:



  • String? dialogTitle:同 pickFiles 方法

  • bool lockParentWindow = false:同 pickFiles 方法

  • String? initialDirectory:同 pickFiles 方法


final String title = '';
final String content = '';

String? dir = await FilePicker.platform.getDirectoryPath();
if (path != null) {
title = '目录';
content = path;
setState((){});
}

9


🛫OK,以上就是这篇文章的全部内容,仅针对插件的当前版本,并不能保证适用于以后插件用法的更新迭代。


最后,感谢 flutter 团队和 miguelpruivo 对以上插件的开发和维护😁。本应用代码已上传至 githubgitee,有需要的可以下载下来查看学习。


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

MySQL模糊查询再也用不着 like+% 了

前言 我们都知道 InnoDB 在模糊查询数据时使用 "%xx" 会导致索引失效,但有时需求就是如此,类似这样的需求还有很多,例如,搜索引擎需要根基用户数据的关键字进行全文查找,电子商务网站需要根据用户的查询条件,在可能需要在商品的详细介绍中进行查找,这些都不...
继续阅读 »

前言


我们都知道 InnoDB 在模糊查询数据时使用 "%xx" 会导致索引失效,但有时需求就是如此,类似这样的需求还有很多,例如,搜索引擎需要根基用户数据的关键字进行全文查找,电子商务网站需要根据用户的查询条件,在可能需要在商品的详细介绍中进行查找,这些都不是B+树索引能很好完成的工作。


通过数值比较,范围过滤等就可以完成绝大多数我们需要的查询了。但是,如果希望通过关键字的匹配来进行查询过滤,那么就需要基于相似度的查询,而不是原来的精确数值比较,全文索引就是为这种场景设计的。


全文索引(Full-Text Search)是将存储于数据库中的整本书或整篇文章中的任意信息查找出来的技术。它可以根据需要获得全文中有关章、节、段、句、词等信息,也可以进行各种统计和分析。


在早期的 MySQL 中,InnoDB 并不支持全文检索技术,从 MySQL 5.6 开始,InnoDB 开始支持全文检索。


倒排索引


全文检索通常使用倒排索引(inverted index)来实现,倒排索引同 B+Tree 一样,也是一种索引结构。它在辅助表中存储了单词与单词自身在一个或多个文档中所在位置之间的映射,这通常利用关联数组实现,拥有两种表现形式:



  • inverted file index:{单词,单词所在文档的id}

  • full inverted index:{单词,(单词所在文档的id,再具体文档中的位置)}


MarkerHub


上图为 inverted file index 关联数组,可以看到其中单词"code"存在于文档1,4中,这样存储再进行全文查询就简单了,可以直接根据 Documents 得到包含查询关键字的文档;而 full inverted index 存储的是对,即(DocumentId,Position),因此其存储的倒排索引如下图,如关键字"code"存在于文档1的第6个单词和文档4的第8个单词。



相比之下,full inverted index 占用了更多的空间,但是能更好的定位数据,并扩充一些其他搜索特性。



MarkerHub


全文检索


创建全文索引


1、创建表时创建全文索引语法如下:

CREATE TABLE table_name ( id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY, author VARCHAR(200), 
title VARCHAR(200), content TEXT(500), FULLTEXT full_index_name (col_name) ) ENGINE=InnoDB;
复制代码

输入查询语句:


SELECT table_id, name, space from INFORMATION_SCHEMA.INNODB_TABLES
WHERE name LIKE 'test/%';
复制代码

MarkerHub


上述六个索引表构成倒排索引,称为辅助索引表。当传入的文档被标记化时,单个词与位置信息和关联的DOC_ID,根据单词的第一个字符的字符集排序权重,在六个索引表中对单词进行完全排序和分区。


2、在已创建的表上创建全文索引语法如下:

CREATE FULLTEXT INDEX full_index_name ON table_name(col_name);
复制代码

使用全文索引


MySQL 数据库支持全文检索的查询,全文索引只能在 InnoDB 或 MyISAM 的表上使用,并且只能用于创建 char,varchar,text 类型的列。


其语法如下:


MATCH(col1,col2,...) AGAINST(expr[search_modifier])
search_modifier:
{
    IN NATURAL LANGUAGE MODE
    | IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION
    | IN BOOLEAN MODE
    | WITH QUERY EXPANSION
}
复制代码

全文搜索使用 MATCH() AGAINST()语法进行,其中,MATCH()采用逗号分隔的列表,命名要搜索的列。AGAINST()接收一个要搜索的字符串,以及一个要执行的搜索类型的可选修饰符。全文检索分为三种类型:自然语言搜索、布尔搜索、查询扩展搜索,下面将对各种查询模式进行介绍。


Natural Language


自然语言搜索将搜索字符串解释为自然人类语言中的短语,MATCH()默认采用 Natural Language 模式,其表示查询带有指定关键字的文档。


接下来结合demo来更好的理解Natural Language


SELECT
    count(*) AS count 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( 'MySQL' );
复制代码

MarkerHub


上述语句,查询 title,body 列中包含 'MySQL' 关键字的行数量。上述语句还可以这样写:


SELECT
    count(IF(MATCH ( title, body ) 
    against ( 'MySQL' ), 1, NULL )) AS count 
FROM
    `fts_articles`;
复制代码

上述两种语句虽然得到的结果是一样的,但从内部运行来看,第二句SQL的执行速度更快些,因为第一句SQL(基于where索引查询的方式)还需要进行相关性的排序统计,而第二种方式是不需要的。


还可以通过SQL语句查询相关性:


SELECT
    *,
    MATCH ( title, body ) against ( 'MySQL' ) AS Relevance 
FROM
    fts_articles;
复制代码

MarkerHub


相关性的计算依据以下四个条件:



  • word 是否在文档中出现

  • word 在文档中出现的次数

  • word 在索引列中的数量

  • 多少个文档包含该 word


对于 InnoDB 存储引擎的全文检索,还需要考虑以下的因素:



  • 查询的 word 在 stopword 列中,忽略该字符串的查询

  • 查询的 word 的字符长度是否在区间 [ innodb_ft_min_token_size,innodb_ft_max_token_size] 内


如果词在 stopword 中,则不对该词进行查询,如对 'for' 这个词进行查询,结果如下所示:


SELECT
    *,
    MATCH ( title, body ) against ( 'for' ) AS Relevance 
FROM
    fts_articles;
复制代码

MarkerHub


可以看到,'for'虽然在文档 2,4中出现,但由于其是 stopword ,故其相关性为0


参数 innodb_ft_min_token_sizeinnodb_ft_max_token_size 控制 InnoDB 引擎查询字符的长度,当长度小于 innodb_ft_min_token_size 或者长度大于 innodb_ft_max_token_size 时,会忽略该词的搜索。在 InnoDB 引擎中,参数 innodb_ft_min_token_size 的默认值是3,innodb_ft_max_token_size的默认值是84


Boolean


布尔搜索使用特殊查询语言的规则来解释搜索字符串,该字符串包含要搜索的词,它还可以包含指定要求的运算符,例如匹配行中必须存在或不存在某个词,或者它的权重应高于或低于通常情况。


例如,下面的语句要求查询有字符串"Pease"但没有"hot"的文档,其中+和-分别表示单词必须存在,或者一定不存在。


select * from fts_test where MATCH(content) AGAINST('+Pease -hot' IN BOOLEAN MODE);
复制代码

Boolean 全文检索支持的类型包括:



  • +:表示该 word 必须存在

  • -:表示该 word 必须不存在

  • (no operator)表示该 word 是可选的,但是如果出现,其相关性会更高

  • @distance表示查询的多个单词之间的距离是否在 distance 之内,distance 的单位是字节,这种全文检索的查询也称为 Proximity Search,如 MATCH(context) AGAINST('"Pease hot"@30' IN BOOLEAN MODE)语句表示字符串 Pease 和 hot 之间的距离需在30字节内

  • >:表示出现该单词时增加相关性

  • <:表示出现该单词时降低相关性

  • ~:表示允许出现该单词,但出现时相关性为负

  • * :表示以该单词开头的单词,如 lik*,表示可以是 lik,like,likes

  • " :表示短语


下面是一些demo,看看 Boolean Mode 是如何使用的。


demo1:+ -


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( '+MySQL -YourSQL' IN BOOLEAN MODE );
复制代码

上述语句,查询的是包含 'MySQL' 但不包含 'YourSQL' 的信息


MarkerHub


demo2: no operator


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( 'MySQL IBM' IN BOOLEAN MODE );
复制代码

上述语句,查询的 'MySQL IBM' 没有 '+','-'的标识,代表 word 是可选的,如果出现,其相关性会更高


MarkerHub


demo3:@


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( '"DB2 IBM"@3' IN BOOLEAN MODE );
复制代码

上述语句,代表 "DB2" ,"IBM"两个词之间的距离在3字节之内


MarkerHub


demo4:> <


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( '+MySQL +(>database <DBMS)' IN BOOLEAN MODE );
复制代码

上述语句,查询同时包含 'MySQL','database','DBMS' 的行信息,但不包含'DBMS'的行的相关性高于包含'DBMS'的行。


MarkerHub


demo5: ~


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( 'MySQL ~database' IN BOOLEAN MODE );
复制代码

上述语句,查询包含 'MySQL' 的行,但如果该行同时包含 'database',则降低相关性。


MarkerHub


demo6:*


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( 'My*' IN BOOLEAN MODE );
复制代码

上述语句,查询关键字中包含'My'的行信息。


MarkerHub


demo7:"


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( '"MySQL Security"' IN BOOLEAN MODE );
复制代码

上述语句,查询包含确切短语 'MySQL Security' 的行信息。


MarkerHub


Query Expansion


查询扩展搜索是对自然语言搜索的修改,这种查询通常在查询的关键词太短,用户需要 implied knowledge(隐含知识)时进行,例如,对于单词 database 的查询,用户可能希望查询的不仅仅是包含 database 的文档,可能还指那些包含 MySQL、Oracle、RDBMS 的单词,而这时可以使用 Query Expansion 模式来开启全文检索的 implied knowledge通过在查询语句中添加 WITH QUERY EXPANSION / IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION 可以开启 blind query expansion(又称为 automatic relevance feedback),该查询分为两个阶段。



  • 第一阶段:根据搜索的单词进行全文索引查询

  • 第二阶段:根据第一阶段产生的分词再进行一次全文检索的查询


接着来看一个例子,看看 Query Expansion 是如何使用的。


-- 创建索引
create FULLTEXT INDEX title_body_index on fts_articles(title,body);
复制代码

-- 使用 Natural Language 模式查询
SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH(title,body) AGAINST('database');
复制代码

使用 Query Expansion 前查询结果如下:


MarkerHub


-- 当使用 Query Expansion 模式查询
SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH(title,body) AGAINST('database' WITH QUERY expansion);
复制代码

使用 Query Expansion 后查询结果如下:


MarkerHub


由于 Query Expansion 的全文检索可能带来许多非相关性的查询,因此在使用时,用户可能需要非常谨慎。


删除全文索引


1、直接删除全文索引语法如下:

DROP INDEX full_idx_name ON db_name.table_name;
复制代码

2、使用 alter table 删除全文索引语法如下:

ALTER TABLE db_name.table_name DROP INDEX full_idx_name;
复制代码


来源:juejin.cn/post/6989871497040887845





推荐3个原创springboot+Vue项目,有完整视频讲解与文档和源码:


【dailyhub】【实战】带你从0搭建一个Springboot+elasticsearch+canal的完整项目



【VueAdmin】手把手教你开发SpringBoot+Jwt+Vue的前后端分离后台管理系统



【VueBlog】基于SpringBoot+Vue开发的前后端分离博客项目完整教学



作者:MarkerHub
来源:https://juejin.cn/post/7072257652784365599
收起阅读 »

被尤雨溪推荐,这款开箱即用的Vue3组件库做对了什么

相信很多开发者都有过这样的想法:因为对某个技术栈或明星开源项目感兴趣,产生了开发拓展方向的新项目的想法与实践,同时也希冀于这个全新的开源项目也能如同别的优质开源项目一样受到关注,只是并非每个项目都能登上热门,获得高额 star 数。 不过,今天马建仓介绍的这...
继续阅读 »

相信很多开发者都有过这样的想法:因为对某个技术栈或明星开源项目感兴趣,产生了开发拓展方向的新项目的想法与实践,同时也希冀于这个全新的开源项目也能如同别的优质开源项目一样受到关注,只是并非每个项目都能登上热门,获得高额 star 数。



不过,今天马建仓介绍的这款开源项目的开发者,就曾在过去一年里实现了从零到一的华丽逆袭,让我们一起来瞧瞧这究竟是什么宝藏项目。


Varlet 是一个基于 Vue3 开发的 Material 风格移动端组件库,并在今年的 Vue JS Live 上被 Vue 的作者尤雨溪推荐。然而自这个项目诞生的时间不到一年。


从 Varlet 作者的某技术博客上得知,作者是一位专科毕业、在无锡工作的四川前端开发。去年,因所属单位打算开发某个与 Vue3 相关的组件库,机缘巧合下,作者自告奋勇包揽下这个活。然而,公司却因成本、投资回报等原因并不打算提供支持,随后作者搭档两位好友决心继续坚持下去。



这个组件库是基于 Material Design 的设计进行规范的,在此期间作者与合作的小伙伴们共同参考社区成品以及结合国内开发者感兴趣的 api 。对于为何选择 Material,作者在官方文档中这样描述:



在早期的移动端设备中,大色块以及强烈对比色,对显示设备要求很高,同时非线性动画和水波纹对 GPU 有一定要求。 导致 Material 风格并没有在移动端浏览器环境下有很好的体验,更多选择更扁平朴素的风格投入产品。 但随着现代设备和新的 js 框架运行时处理的效率的逐步提升,浏览器有了更多的空闲时间和能力去处理动画效果,Material Design 将会给应用带来更好的体验。



经历了多次的反复推敲之后,组件库隐约有了个雏形。打这时起, Varlet 也正式开源,并采用 MIT 开源许可证。



之后的日子里,Varlet 不仅获得阮一峰老师的推荐,同时也得到了国外开源技术社区的认可,其中 Vite 核心团队的 Antfu 大神也接受了这个组件库的 PR。不久前,在 Vue3 的 2021 年度总结分享会上,尤雨溪大神也推荐了 Varlet 。前段时间,在 Gitee 上开源的 varlet-ui 项目经过评估,也获得了Gitee的推荐,项目地址:gitee.com/varlet/varl…


那么 Varlet 究竟有着怎样的魅力,吸引着这么多大神与优质平台的推广呢?




从特性上看



  • 提供50个高质量通用组件

  • 组件十分轻量

  • 由国人开发,完善的中英文文档和后勤保障

  • 支持按需引入

  • 支持主题定制

  • 支持国际化

  • 支持 webstorm,vscode 组件属性高亮

  • 支持 SSR

  • 支持 Typescript

  • 确保90%以上单元测试覆盖率,提供稳定性保证

  • 支持暗黑模式


如何安装与部署


CDN


varlet.js 包含组件库的所有样式和逻辑, 因此只需引入即可。


<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/vue@next"></script>
<script src="https://cdn.jsdelivr.net/npm/@varlet/ui/umd/varlet.js"></script>
<script>
  const app = Vue.createApp({
    template: '<var-button>按钮</var-button>'
  })
  app.use(Varlet).mount('#app')
</script>
复制代码

Webpack/Vite


# 通过 npm、yarn 或 pnpm 安装

# npm
npm i @varlet/ui -S

# yarn
yarn add @varlet/ui

# pnpm
pnpm add @varlet/ui
复制代码

import App from './App.vue'
import Varlet from '@varlet/ui'
import { createApp } from 'vue'
import '@varlet/ui/es/style.js'

createApp(App).use(Varlet).mount('#app')
复制代码

如何引入?



手动引入


每一个组件都是一个 Vue 插件,并由组件逻辑和样式文件组成,如下方式进行手动引入使用。


import { createApp } from 'vue'
import { Button } from '@varlet/ui'
import '@varlet/ui/es/button/style/index.js'

createApp().use(Button)
复制代码

自动引入


所有在模板中的组件,都会被 unplugin-vue-components 插件自动扫描,插件会自动引入组件逻辑和样式文件并注册组件。


# 安装插件

# npm
npm i unplugin-vue-components -D

# yarn
yarn add unplugin-vue-components -D

# pnpm
pnpm add unplugin-vue-components -D
复制代码

Vue Cli


// vue.config.js
const Components = require('unplugin-vue-components/webpack')
const { VarletUIResolver } = require('unplugin-vue-components/resolvers')

module.exports = {
  configureWebpack: {
    plugins: [
      Components({
        resolvers: [VarletUIResolver()]
      })
    ]
  }
}
复制代码

Vite


// vite.config.js
import vue from '@vitejs/plugin-vue'
import components from 'unplugin-vue-components/vite'
import { VarletUIResolver } from 'unplugin-vue-components/resolvers'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    vue(),
    components({
      resolvers: [VarletUIResolver()]
    })
  ]
})
复制代码

注意


完成配置后如下使用即可


<template>
  <var-button>默认按钮</var-button>
</template>
复制代码

如何切换主题


该项目提供了暗黑模式的主题,暗黑模式的优势在于在弱光环境下具有更高的可读性。



<var-button block @click="toggleTheme">切换主题</var-button>
复制代码

import dark from '@varlet/ui/es/themes/dark'
import { StyleProvider } from '@varlet/ui'

export default {
  setup() {
    let currentTheme
    
    const toggleTheme = () => {
      currentTheme = currentTheme ? null : dark
      StyleProvider(currentTheme)
    }
    
    return { toggleTheme }
  }
}
复制代码

注入组件库推荐的文字颜色和背景颜色变量来控制整体颜色


body {
  transition: background-color .25s;
  color: var(--color-text);
  background-color: var(--color-body);
}
复制代码

样式展示




在线编辑地址


前往下列网址:varlet.gitee.io/varlet-ui/#…


点击界面右上方:


作者:Gitee
来源:https://juejin.cn/post/7075162881498562590
收起阅读 »

平时的工作如何体现一个人的技术深度?

今天在公司内网看到一个讨论帖,原文如下:平时的工作如何体现一个人的技术深度?平时工作中很多时候需求细而碎的,如何在工作中积累技术深度?又如何体现一个人的技术深度? 思考:做需求与做需求的差异 再回答问题之前,我想先抛开「技术深度」这次词,讲讲做需求这件事,说说...
继续阅读 »

今天在公司内网看到一个讨论帖,原文如下:

平时的工作如何体现一个人的技术深度?

平时工作中很多时候需求细而碎的,如何在工作中积累技术深度?又如何体现一个人的技术深度?

思考:做需求与做需求的差异


再回答问题之前,我想先抛开「技术深度」这次词,讲讲做需求这件事,说说我对做需求的理解。每一个程序员都是从刚毕业做需求开始,为什么有的人逐渐成为大牛,主导大型技术项目或走向团队管理岗位,而有的人一直还在做需求。我觉得这里面的差异在于,每一个对做需求这件事的理解有所不同。


这里面的差异在于,你是抱着一种什么样的心态去完成这个需求,是把这个需求做到极致,还是只是当做任务完成这个需求,达到产品想要的功能就行。这两个表面上看似差不多其实相差极大,差异在于,你有没有站在更高的角度,希望这件事做到完美。从需求角度有没有思考产品设计当中的缺陷,能不能反向为产品设计提供建议,从技术角度能不能做到高质量高兼容性无bug,以及下次再有类似的需求能不能快速高效率的迭代。


用一句话来描述就是,能不能跳出自己是一个程序员是一个被动执行人的角色,而是将自己当做产品当做技术负责人的心态去做这件事


业务需求该怎么做


知易行难,如果一开始做不到,那就先着眼小事,关注细节,从需求开始的需求评审,编写技术方案文档,设计文档,到开发的代码注释,结构设计,保证高质量,完善无漏洞的代码逻辑,再到异常埋点,指标监控,线上可用性运维等等,认真对待一个需求的每一个环节。


当你自认为已经做好整个流程的每一件小事之后,接下来可以 通过深入细节,思考整个流程是否存在问题。做需求过程中沟通协作的有没有问题,流程规范的有没有问题,机制环节哪方面问题,代码公共基础能力的是否有缺失,开发过程中你所遇到的问题是不是一个通用问题,能不能抽象出一个公共库解决大家的问题,能不能制定一个SOP的解决方案流程,亦或是提炼出一个最佳实践在组内外分享经验。


通过这些一件件小事来锻炼自己解决问题的能力,以及更深层级的发现问题的能力。再通过不断的发现问题,思考问题出现的原因,拿出解决方案,最终落地解决了自己或组内或协作方的问题,锻炼自己的综合能力逐步慢慢成长。


再说「技术深度」


说了这么多,你可能会说,这跟我问的技术深度有什么关系?我想说:抛开业务需求谈技术深度都是耍流氓


举一个例子,数据可视化方面3D three.js,视频直播方面的编解码压缩,客户端安全方面的攻防渗透,每一个都是有技术深度的事情,但问题是即使你掌握了这些领域拥有了非常高的技术深度之后呢,不能应用于业务需求,不能解决产品急迫要解决的问题,不能完成你老板的OKR,达成部门的战略目标,还是英雄无用武之地(当然你也可以选择一个可以用得上的团队,那是就是另外一回事了)。


由于这些单点的有技术深度的事情,不能为你带来直观和显而易见的 「回报」(也就是颜如玉 黄金屋与金榜题名),也就间接的打击了积极性(当然自己对某门技术感兴趣而钻研的不再本次讨论之列)。所以提升自己的技术深度,最好的方式还是在公司业务中,发现有深度的事,然后去在攻克这个问题的过程中,提升了自己的技术深度,即跟随公司业务的发展的同时自身也获得了成长,你用技术能力为公司解决了业务发展过程中的问题,自然也就从公司获得了该有的回报。这是一个ROI投入产出比最高的获得技术深度的方式。


获取做有深度事情的授权


当想明白获取技术深度的路径之后,接下来要解决的就是,如何让领导给你安排有技术深度的事情?


业务发展中有很多有技术深度有技术难度的事情,为什么领导要安排你来做这件事呢?你凭什么让领导觉得你 「有能力」「有意愿」 来完成这件事?能力与意愿是作为领导在分配工作当中的最重要的两个决策项(有机会的话我会再写一篇,从管理视角是如何做分工的)。既然你能提问如何积累技术深度,我相信你一定是有强烈意愿的,那么剩下的就是如何让领导认为你有完成这个有技术深度的事情的能力?关于这个问题,可以参考我之前写的一篇回答:如何管理leader对你的能力预期? 最简单来讲就是我在前面讲的,你能不能在开发需求中做到深度思考,追求极致,精益求精,有责任心,有主人翁意识与主R意识,在每件小事中能做到 「自闭环」,之后才会逐步让你承担更大范围更高挑战更大深度的事情,形成正向循环。


这也是我前面为什么要先重点强调做好每一件小事的重要性。


技术深度不是唯一标准


作为一个程序员,在职业生涯的初期,确实是以技术深度也就是技术能力作为最大的衡量标准。但随着职业生涯的发展,职级从L5到L8,站在从公司角度,对一个人的需求,也会从能完成一个业务需求,变成能带领一帮人完成一个更大的维度的需求,能不能为组织解决问题,为事业部达成战略目标,对人的要求的重心也会慢慢发生变化,这种变化可以参考公司的职级能力模型体系的雷达图。


所以一味的追求积累技术深度就跑偏了,但不是说技术深度不重要,技术能力是作为程序员的安身立命之本,但是在积累技术深度的同时,也需要学习锻炼技术深度以外的能力。具体到底是什么其他能力,这就够再展开好几篇文章的篇幅了,今天在这就不细说了,有机会可以谈谈我对这方面的理解。


最后


故不积跬步无以至千里,不积小流无以成江海。先从做好每一件小事开始,把每个业务需求做到120分,深度思考,发现问题,解决问题,逐步建立起靠谱有责任心技术牛的人设,逐步负责有技术难度的事情,跟随公司业务发展积累自己的业务领域经验与技术深度,从而获得双赢的回报。


这是我对如何积累技术深度这件事的理解,或许会有一些片面和偏激,毕竟不是谁都有一个能知人善任的好领导,不是谁都能遇到一个快速发展的业务,不是谁都能遇到有技术难度与技术挑战的场景,无论我怎么说,都会有幸存者偏差的存在。


努力与机遇并存,机遇可遇不可求,所以我们能做的事,就是学会正确做事的思路和方法,并为之坚持不懈的践行它。知易行难,学会方法容易,坚持践行难于上青天。自己该做的都做好了,机遇来了就可以抓住,即使抓不住,你也有了「选择的能力」,有了选择更好机遇更好公司的能力


以上均为个人主观且片面的看法,欢迎批评讨论~~。


作者:沧海月明FE
来源:https://juejin.cn/post/7073001183123603470 收起阅读 »

Flutter好用的轮子推荐02:拥有炫酷光影效果的拟态风格UI套件

前言 Flutter 是 Google 开源的应用开发框架,仅通过一套代码就能构建支持Android、iOS、Windows、Linux等多平台的应用。Flutter的性能非常高,拥有120fps的刷新率,也是目前非常流行的跨平台UI开发框架。 本专栏为大家收...
继续阅读 »

前言


Flutter 是 Google 开源的应用开发框架,仅通过一套代码就能构建支持Android、iOS、Windows、Linux等多平台的应用。Flutter的性能非常高,拥有120fps的刷新率,也是目前非常流行的跨平台UI开发框架。


本专栏为大家收集了Github上近70个优秀开源库,后续也将持续更新。希望可以帮助大家提升搬砖效率,同时祝愿Flutter的生态越来越完善🎉🎉。


正文


一、🚀 轮子介绍




  • 名称:flutter_neumorphic




  • 概述:易用的拟态风格UI套件,几乎可以在任何App中使用它。




  • 作者:idean Team




  • 仓库地址:Flutter-Neumorphic




  • 推荐指数: ⭐️⭐️⭐️⭐️⭐️




  • 常用指数: ⭐️⭐️⭐️⭐️⭐️




  • 效果预览:




flutter_logo_small.gif


二、⚙️ 安装及使用


dependencies:
flutter_neumorphic: ^3.0.3

import 'package:flutter_neumorphic/flutter_neumorphic.dart';

三、🔧 常用属性


1.基本



















































属性描述
LightSource特定于theme或小组件的光源,用于投射浅色/深色阴影
shape拟态容器中使用的曲线形状
Depth小组件与父组件的垂直距离
Intensity光的强度,它影响阴影的颜色
SurfaceIntensity组件表面的明暗效果
Color拟态组件的默认颜色
Accent拟态组件的选中颜色,例如复选框
Variant拟态组件的次要颜色
BoxShape拟态组件形状
Border边框

2.Shapes


image.png


四、🗂 内置组件介绍


tips:为了更直观的展示效果,本文案例已将组件和背景设置为同一色值的浅灰色。


1.Neumorphic


一个基本的拟态容器组件,可根据光源、高度(深度)添加浅色/深色渐变的容器。


container.gif


NeumorphicStyle(
depth: 3,
lightSource: LightSource.left,
color: Colors.grey[200],
),
child: const SizedBox(
width: 200,
height: 200,
),
)

2.NeumorphicButton


拟态按钮,默认按下有高度变化及震动反馈


button-2.gif


NeumorphicButton(
style: NeumorphicStyle(
boxShape: NeumorphicBoxShape.roundRect(
BorderRadius.circular(12),
),
color: Colors.grey[200],
shape: NeumorphicShape.flat,
),
child: Container(
color: Colors.grey[200],
width: 100,
height: 25,
child: const Center(
child: Text('Click me'),
),
),
onPressed: () {},
)

3.NeumorphicRadio


单选按钮


radio.gif


class NeumorphicButtonWidget extends StatefulWidget {
const NeumorphicButtonWidget({Key? key}) : super(key: key);
@override
State<NeumorphicButtonWidget> createState() => _NeumorphicButtonWidgetState();
}

class _NeumorphicButtonWidgetState extends State<NeumorphicButtonWidget> {
int _groupValue = 1;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
getChild('A', 1),
const SizedBox(width: 12),
getChild('B', 2),
const SizedBox(width: 12),
getChild('C', 3),
],
);
}
Widget getChild(String str, int value) {
return NeumorphicRadio(
child: Container(
color: Colors.grey[200],
height: 50,
width: 50,
child: Center(
child: Text(str))),
value: value,
groupValue: _groupValue,
onChanged: (value) {
setState(() {
_groupValue = value as int;
});
});
}}

4.NeumorphicCheckbox


多选按钮


checkbox.gif


class NeumorphicCheckboxWidget extends StatefulWidget {
const NeumorphicCheckboxWidget({Key? key}) : super(key: key);
@override
State<NeumorphicCheckboxWidget> createState() => _NeumorphicCheckboxWidgetState();
}

class _NeumorphicCheckboxWidgetState extends State<NeumorphicCheckboxWidget> {
bool check1 = false;
bool check2 = false;
bool check3 = false;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const SizedBox(width: 12),
NeumorphicCheckbox(
value: check1,
onChanged: (value) {
setState(() {
check1 = value;
});
},
),
const SizedBox(width: 12),
NeumorphicCheckbox(
value: check2,
onChanged: (value) {
setState(() {
check2 = value;
});
},
),
const SizedBox(width: 12),
NeumorphicCheckbox(
value: check3,
onChanged: (value) {
setState(() {
check3 = value;
});
},
),
],
);
}
}

5.NeumorphicText


拟态文字


text.png


NeumorphicText(
'Flutter',
textStyle: NeumorphicTextStyle(
fontSize: 80,
fontWeight: FontWeight.w900,
),
style: NeumorphicStyle(
depth: 3,
lightSource: LightSource.left,
color: Colors.grey[200],
),
)

6.NeumorphicIcon


拟态图标


icon.png


NeumorphicIcon(
Icons.public,
size: 180,
style: NeumorphicStyle(
depth: 3,
lightSource: LightSource.left,
color: Colors.grey[200],
),
);

7.material.TextField


拟态文本输入框


textfield.png


Neumorphic(
margin: const EdgeInsets.only(left: 8, right: 8, top: 2, bottom: 4),
style: NeumorphicStyle(
depth: NeumorphicTheme.embossDepth(context),
boxShape: const NeumorphicBoxShape.stadium(),
color: Colors.grey[200]),
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 18),
child: const TextField(
decoration: InputDecoration.collapsed(hintText: 'NeumorphicTextField'),
),
);

8.NeumorphicSwitch


拟态开关


switch.gif


class NeumorphicSwitchWidget extends StatefulWidget {
const NeumorphicSwitchWidget({Key? key}) : super(key: key);
@override
State<NeumorphicSwitchWidget> createState() => _NeumorphicSwitchWidgetState();
}
class _NeumorphicSwitchWidgetState extends State<NeumorphicSwitchWidget> {
bool isChecked = false;
bool isEnabled = true;
@override
Widget build(BuildContext context) {
return NeumorphicSwitch(
style: NeumorphicSwitchStyle(
trackDepth: 3,
activeThumbColor: Colors.grey[200], // 开启时按钮颜色
activeTrackColor: Colors.green, // 开启时背景颜色
inactiveThumbColor: Colors.green, // 关闭时按钮颜色
inactiveTrackColor: Colors.grey[200], // 关闭时背景颜色
),
isEnabled: isEnabled,
value: isChecked,
onChanged: (value) {
setState(() {
isChecked = value;
});
},
);
}}

9.NeumorphicToggle


拟态滑动选择器


toggle.gif


class NeumorphicToggleWidget extends StatefulWidget {
const NeumorphicToggleWidget({Key? key}) : super(key: key);
@override
State<NeumorphicToggleWidget> createState() => _NeumorphicToggleWidgetState();
}
class _NeumorphicToggleWidgetState extends State<NeumorphicToggleWidget> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return NeumorphicToggle(
height: 50,
style: NeumorphicToggleStyle(
backgroundColor: Colors.grey[200],
),
selectedIndex: _selectedIndex,
displayForegroundOnlyIfSelected: true,
children: [
ToggleElement(
background: const Center(
child: Text(
"This week",
style: TextStyle(fontWeight: FontWeight.w500),
)),
foreground: const Center(
child: Text(
"This week",
style: TextStyle(fontWeight: FontWeight.w700),
)),
),
ToggleElement(
background: const Center(
child: Text(
"This month",
style: TextStyle(fontWeight: FontWeight.w500),
)),
foreground: const Center(
child: Text(
"This month",
style: TextStyle(fontWeight: FontWeight.w700),
)),
),
ToggleElement(
background: const Center(
child: Text(
"This year",
style: TextStyle(fontWeight: FontWeight.w500),
)),
foreground: const Center(
child: Text(
"This year",
style: TextStyle(fontWeight: FontWeight.w700),
)),
)
],
thumb: Neumorphic(
style: NeumorphicStyle(
boxShape: NeumorphicBoxShape.roundRect(
const BorderRadius.all(Radius.circular(12))),
),
),
onChanged: (value) {
setState(() {
_selectedIndex = value;
});
},
);
}}

10.NeumorphicSlider


拟态滑动控制器


slider.gif


class NeumorphicSliderWidget extends StatefulWidget {
const NeumorphicSliderWidget({Key? key}) : super(key: key);
@override
State<NeumorphicSliderWidget> createState() => _NeumorphicSliderWidgetState();
}
class _NeumorphicSliderWidgetState extends State<NeumorphicSliderWidget> {
double num = 0;
@override
Widget build(BuildContext context) {
return NeumorphicSlider(
min: 8,
max: 75,
value: num,
onChanged: (value) {
setState(() {
num = value;
});
},
);
}
}

11.NeumorphicProgress


拟态百分比进度条


progress.gif


NeumorphicProgress(
height: 20,
percent: 0.5,
);

12.NeumorphicProgressIndeterminate


渐进式进度条


indeterminate.gif


NeumorphicProgressIndeterminate(
height: 10,
);

13.NeumorphicBackground


拟态背景,可以使用Radius裁剪屏幕


image.png


class NeumorphicPageView extends StatelessWidget {
const NeumorphicPageView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return NeumorphicBackground(
borderRadius: const BorderRadius.all(Radius.circular(130)),
child: Scaffold(
backgroundColor: Colors.grey[200],
));
}
}

14.NeumorphicApp


使用拟态设计的应用程序。可以处理主题、导航、本地化等


void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return NeumorphicApp(
debugShowCheckedModeBanner: false,
themeMode: ThemeMode.light,
title: 'Flutter Neumorphic',
home: FullSampleHomePage(),
);
}
}

15.NeumorphicAppBar


拟态导航条


app_bar.png


五、🏠 使用案例


image.png


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

Flutter桌面端开发——发送本地悬浮通知?

在我用的大部分桌面端中,发送本地悬浮通知的软件可以说是屈指可数。但是,这不妨碍到我们学习✊,奋斗说不定以后就能用到呢! 在选择该使用哪些插件开发桌面端的时候,由 lijy91 主导的 LeanFlutter 可以说是帮了很大的忙,有需要的可以自己去看看。 接下...
继续阅读 »

在我用的大部分桌面端中,发送本地悬浮通知的软件可以说是屈指可数。但是,这不妨碍到我们学习✊,奋斗说不定以后就能用到呢!


在选择该使用哪些插件开发桌面端的时候,由 lijy91 主导的 LeanFlutter 可以说是帮了很大的忙,有需要的可以自己去看看。


接下来要介绍的两个发送通知的插件,也是从 LeanFlutter 下的 awesome-flutter-desktop 仓库中找的。


local_notifier


安装🛠


点击local_notifier获取最新版本。以下是在编写本文章时的最新版本:


local_notifier: ^0.1.1

👻注意:在开发 Linux 端的程序时,还需要额外的操作,具体可以查看这里


使用🍖


要想实现发送通知的功能,需要用到一个实例化的 LocalNotifier 对象:


final localNotifier = LocalNotifier.instance;

如果你想全局使用,可以把该行代码放到一个自定义的全局类里。


要想编辑想发送的内容,需要用到 LocalNotification 类。实例化该类可以传入5个参数:



  • String? identifier:用来当做通用唯一识别码

  • required String title:发送通知的标题,一般是软件名称

  • String? subtitle:发送的通知内容的标题

  • String? body:发送的内容的主体

  • bool silent = false:在发送通知时是否静音


final notification = LocalNotification(
identifier: '12345',
title: '古诗鉴赏从',
subtitle: '桃夭 - 佚名〔先秦〕',
body: '桃之夭夭,灼灼其华。之子于归,宜其室家。\n桃之夭夭,有蕡其实。之子于归,宜其家室。\n桃之夭夭,其叶蓁蓁。之子于归,宜其家人。',
silent: false,
);
localNotifier.notify(notification);

现在我们就能愉快地发送一条自定义的通知了🎉


local-notifier


我们发现,其中其实只有title参数是必传的,我们就试一下只传入这个参数:


final notification = LocalNotification(
title: '古诗鉴赏从',
);
localNotifier.notify(notification);

local-notifier1


我们发现,只传 title 参数,它会自动将 title 的参数值赋值给 subtitle,而 body 参数会以“新通知”代替。
好了,以上就是 local_notifier 目前的全部功能,如果你只是简单的发送一条提示使用该插件完全够用。


win_toast


安装🛠


点击win_toast获取最新版本。以下是在编写本文章时的最新版本:


win_toast: ^0.0.2

使用🍖


在全局使用该插件,需要在app初始化时初始化。在某个页面使用,只要在页面初始化时初始化就行。


初始化时需要传递3个参数:



  • required String appName:程序名称

  • required String productName:产品名称

  • required String companyName:公司名称


await WinToast.instance().initialize(
appName: '第一个Desktop应用',
productName: '第一个Desktop应用',
companyName: 'Hiden Intelligence',
);

没写过原生,插件作者贴出Pick a unique AUMID that will identify your Win32 app告诉我们为什么要填这些内容👀,想了解的可以看一下。


要想发送一条通知,需要使用 showToast 方法,该方法有5个参数可以传:




  • required ToastType type:传入toast显示的类型,一共有8种:



    • imageAndText01至imageAndText04

    • text01至text04


    至于这些类型的异同,可以点这里👀




  • required String title:通知显示的标题




  • String subtitle = '':通知显示的主要内容




  • String imagePath = '':选择 imageAndText 类型时,要显示的图片




  • List actions = const []:显示通知中的按钮




使用text类型


先定义几个变量和常量:


Toast? toast;
final List<String> _title = 'Shining For One Thing(《一闪一闪亮星星》影视剧歌曲) - 赵贝尔';
final List<String> _subtitle = 'I fall in love\nI see your love\n遇见你才发现\n我在等你到来';
final List<String> _actione = ['上一首', '播放/暂停', '下一首'];

先来看看只传入文字的 text01 类型:


toast = await WinToast.instance().showToast(
type: ToastType.text01,
title: _title,
actions: _actione,
);

👻注意:当使用 ToastType.text01 或 ToastType.imageAndText01 时不能传入 subtitle 参数。


1


再来看看只传入文字的 text02 类型:


toast = await WinToast.instance().showToast(
type: ToastType.text02,
title: _title,
subtitle: _subtitle,
actions: _actione,
);

2


用了一下 ToastType.text03 和 ToastType.text04,发现显示的效果和 ToastType.text02 没有差别。大家可以自己试试。


使用imageAndText类型


修改一下常量的值(非必要):


Toast? toast;
final List<String> _title = '又下雨了,你的心情怎么样?';
final List<String> _subtitle = '偷偷告诉你,明天就天晴了😏\n好雨知时节,当春乃发生。随风潜入夜,润物细无声。野径云俱黑,江船火独明。晓看红湿处,花重锦官城。';
final List<String> _actione = ['不开森😭', '只想睡觉🥱', '非常高兴😃'];

还需要传入一张图片,目前无法得知应该传入图片的路径怎么填,所以先准备一张资源图片传入它的相对路径:


final String _imagePath = 'assets/images/pdx.jpg';

来看看 imageAndText01 类型:


toast = await WinToast.instance().showToast(
type: ToastType.imageAndText01,
title: _titles * 3,
imagePath: _imagePath,
actions: _action,
);

3


😲嗯?我们传入的图片怎么没显示?换个网络图片的链接试试:


final String _imagePath = 'https://gitee.com/ilgnefz/image-house/raw/master/images/pdx.jpg';

发现效果还是一样的。通过查看文档里的第一个链接中的例子,可以发现,这里需要传入图片的绝对路径。


那在 Flutter 中怎么获取文件的绝对路径呢🤔?当然,可以直接在 Android Studio 中选中图片点右键的 Copy Path,但是程序被打包安装后就不一定在这个位置了。学过Node.js,在里面获取文件的绝对路径要用到 Path 模块,那么 Flutter 是否也用同样的插件。打开 pub.dev 搜索,还真有。复制到 pubspec.yaml 进行安装,报错,告诉我们 Flutter Desktop 中已经集成了该插件,但是版本不一样。😀那不就好办了,第一步导入:


import 'package:path/path.dart' as path;

path 中没有 __dirname 方法,可以通过path. 查看提示,发现有一个 current的方法。虽然我们不知道这个方法是干什么的,但也不妨试试。修改 imagePath 为如下代码:


final String _imagePath = path.join(path.current, 'assets/images/pdx.jpg');

3


成功🎉🎉🎉🎉🎉


接下来使用 imageAndText02 类型来看看:


toast = await WinToast.instance().showToast(
type: ToastType.imageAndText02,
title: _titles,
subtitle: _subtitle,
imagePath: _imagePath,
actions: _action,
);

3


imageAndText03 和 imageAndText04 显示的效果也和 imageAndText02 无差别,这里就不放图了。


大家可能已经发现,通知中的3个按钮是由 actions 参数决定的,但是这个参数传入的是 String 类型,那我们要怎么才能获取到用户对这些按钮的点击事件呢?


前期我们定义了一个toast对象用来赋值,接下来就要用到这个参数:


if (toast != null) {
toast.eventStream.listen((event) {
if (event is ActivatedEvent) {
print(event);
}
});
}

在这里我们会获得一个 event 对象,通过打印会发现该对象下面只有一个属性actionIndex,返回的是 int? 类型。通过该属性,我们就可以获取到用户点击的是第几个按钮:


WinToast.instance().bringWindowToFront(); // 用户点击后关闭弹窗通知
BotToast.showText(text: '你当前的状态是${_action[event.actionIndex!]}');

4


知道了用户点击的是哪个按钮,接下来编写事件的代码就容易了。


🛫OK,以上就是这篇文章的全部内容,仅针对插件的当前版本,并不能保证适用于以后插件用法的更新迭代。本人只处于对代码的实践部分,如某些内容的概念或叫法出错还请指正🙏。


最后,感谢 lijy91boyan01 对以上插件的维护和开发😁。本程序相关代码已上传至 githubgitee,有需要的可以下载下来查看学习。


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

Android 实现卡片堆叠,钱包管理效果(带动画)

先上效果图 源码 github.com/woshiwzy/Ca… 实现原理: 1.继承LinearLayout 2.重写onLayout,onMeasure 方法 3.利用ValueAnimator 实施动画 4.在动画回调中requestLayout 实现...
继续阅读 »

先上效果图


result.gif


源码
github.com/woshiwzy/Ca…


实现原理:


1.继承LinearLayout

2.重写onLayout,onMeasure 方法

3.利用ValueAnimator 实施动画

4.在动画回调中requestLayout 实现动画效果


思路:


1.用Bounds 对象记录每一个CardView 对象的初始位置,当前位置,运动目标位置


2.点击时计算出对应的view以及可能会产生关联运动的View的运动的目标位置,从当前位置运动到目标位置,然后以这2个位置作为动画参数实施ValueAnimator动画,在动画回调中触发onLayout,达到动画的效果。


重写adView 方法,确保新添加的在这里确保所有的子view 都有一个初始化的bounds位置


   @Override
public void addView(View child, ViewGroup.LayoutParams params) {
super.addView(child, params);
Bounds bounds = getBunds(getChildCount());
}

确保每个子View的测量属性宽度填满父组件



boolean mesured = false;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mesured == true) {//只需要测量一次
return;
}
mesured = true;
int childCount = getChildCount();
int rootWidth = getWidth();
int rootHeight = getHeight();
if (childCount > 0) {
View child0 = getChildAt(0);
int modeWidth = MeasureSpec.getMode(child0.getMeasuredWidth());
int sizeWidth = MeasureSpec.getSize(child0.getMeasuredWidth());

int modeHeight = MeasureSpec.getMode(child0.getMeasuredHeight());
int sizeHeight = MeasureSpec.getSize(child0.getMeasuredHeight());

if (childCount > 0) {
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
childView.measure(MeasureSpec.makeMeasureSpec(sizeWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(sizeHeight, MeasureSpec.EXACTLY));
int top = (int) (i * (sizeHeight * carEvPercnet));
getBunds(i).setTop(top);
getBunds(i).setCurrentTop(top);
getBunds(i).setLastCurrentTop(top);
getBunds(i).setHeight(sizeHeight);
}

}

}
}

重写onLayout 方法是关键,是动画触发的主要目的,这里layout参数并不是写死的,而是计算出来的(通过ValueAnimator 计算出来的)


@Override
protected void onLayout(boolean changed, int sl, int st, int sr, int sb) {
int childCount = getChildCount();
if (childCount > 0) {
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
int mWidth = view.getMeasuredWidth();
int mw = MeasureSpec.getSize(mWidth);
int l = 0, r = l + mw;
view.layout(l, getBunds(i).getCurrentTop(), r, getBunds(i).getCurrentTop() + getBunds(i).getHeight());
}
}
}

源码


github:
github.com/woshiwzy/Ca…


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

求求你们了,对自己代码质量有点要求!

开篇 最近在合并几个项目的代码,把功能拼一拼简称项目拼多多。 但是几个系统都没有 eslint 之类的东西,我也不知道怎么想的居然想给 拼多多 加上代码检查。 我还真的干了,于是就有了下面这些奇奇怪怪甚至有些可爱的代码。算是给大家做个反面教材。 一些示例 ...
继续阅读 »

开篇



  • 最近在合并几个项目的代码,把功能拼一拼简称项目拼多多

  • 但是几个系统都没有 eslint 之类的东西,我也不知道怎么想的居然想给 拼多多 加上代码检查。

  • 我还真的干了,于是就有了下面这些奇奇怪怪甚至有些可爱的代码。算是给大家做个反面教材。


一些示例




  • ps 示例代码来源于网络社区。



循环不要声明无用的变量


image.png


不要在 template中写很长的判断、运算,因为有个东西叫做计算属性。


image.png


使用 getCurrentInstance 获取 proxy 时候,请仔细想想你真的需要吗? 最重要的不要声明了但不使用它!


image.png


不要声明未使用变量函数!



  • 当然可能有时候,业务变更忘记改了! 如果是这样,那应该安装 eslint 并增加代码提交检查!


image.png


请在data 中声明所有已知变量及其子属性


image.png


请不要太随意的对文件进行命名



  • 如果有疑问可以查看vue风格指南那里会有答案!


image.png


请不要写一些奇怪的逻辑,如果写了请写上注释,对于重复的东西,有必要进行提取,这会使代码更整洁。


image.png


如果你使用了 v-for 请记得加上 key 不然它就像没穿内裤一样会很难受!


image.png


一个组件是需要一个名字的,就像人一样!


image.png


image.png


不要混用 v-if、v-for,更不要像下图这样写!



  • 组件在使用 v-for 遍历时 需要使用 v-if 判断是否加载,可以使用计算属性先处理一遍再把数据用于v-for遍历。

  • 下边这种写法,我猜测可能是数据不存在则不展示,但是 v-for 没有数据本身就不会展示啊!


image.png


不要混合使用使用不同的操作符


image.png


它是想做什么呢?



  • obj[next.id] 存在不做操作, 不存在赋值为 true 且执行 cur.push(next)


image.png


写vue的强烈建议查看官网的风格指南 猛击查看


作者:唐诗
来源:https://juejin.cn/post/7073049322656366622
收起阅读 »

跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview

前言 跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制企...
继续阅读 »
前言

跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制
企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

Flutter作为基础的应用,如果要在flutter 中嵌入webview 去做Hybrid混合开发,咱们就必须要封装一套易用的webview,但网上关于flutter webview的文章极其的少。但的确也有做封装的文章,但是封装手法不够优雅,封装效果不够扩展。于是我打算把我的封装与大家分享,看我如何做到高扩展,高易用性。


目标:


Flutter 中嵌入 webview ,能够与 flutter 双向通信,并且易用。


搭建前夕准备


三方库:


webview_flutter flutter网页widget


开始搭建


一、基本回调


1.1 webview外部基本管理器


typedef void InnerWebPageCreatedCallback(InnerWebPageController controller);

涉及到的管理器源码之后介绍


1.2 显示处理回调


typedef WebPageCallBack = Function(String name,dynamic value);

由url拦截器,或者js返回数据后调用flutter页面代码,可以更新页面,或者状态变更。
使用如下:


webPageCallBack = (String name,dynamic value){
switch(name){
case LibWebPage.ACTION_SHOW_BAR:
setState(() {
widget.isShowToolBar = value;
});
break;
case LibWebPage.ACTION_SHOW_RIGHT:
setState(() {
widget.isShowRight = value;
});
break;
case LibWebPage.ACTION_BACK:
if(value){
Navigator.of(context).pop();
}else{
_goBack(context).then((value) => {
Navigator.of(context).pop()
});
}
break;
}

};

1.3 url拦截器处理


typedef WebPageUrlIntercept = bool Function(String url,InnerWebPageController? _controller);

一般用于处理 请求的url中的特殊文字处理


1.4 网页title 回调


typedef TitleCallBack = Function(String title);

网页加载完成后调用该回调展示当前html 的 title标签


二、构建web widget 控制管理器


class InnerWebPageController {
//webview原有管理器
WebViewController _controller;

InnerWebPageController(this._controller);
//执行网页js,在原有基础上封装,只需要发送jsname与参数
Future runJavascript(String funname,List? param,bool brackets) async{
String javaScriptString = getJavaScriptString(funname,param,brackets);
await _controller.runJavascript(javaScriptString);
}
//带返回值执行网页js,在原有基础上封装,只需要发送jsname与参数
Future runJavascriptReturningResult(String funname,List? param,bool brackets) async {
String javaScriptString = getJavaScriptString(funname,param,brackets);
_controller.runJavascript(javaScriptString);
return await _controller.runJavascriptReturningResult(javaScriptString);
}
//是否可以返回
Future canGoBack() {
return _controller.canGoBack();
}
//返回网页历史
Future goBack() {
return _controller.goBack();
}
//重新加载
Future reload() {
LibLoading.show();
return _controller.reload();
}
//获取js请求(工具)
String getJavaScriptString(String funname, List? param,bool brackets) {
var strb = StringBuffer(funname);
if(brackets){
strb.write("(");
}
if(param!=null&¶m.length>0){
for(int i=0;i ${strb.toString()}");
return strb.toString();
}

}


三、构建JavascriptChannels js注册抽象基础类


abstract class JavascriptChannels{

WebPageCallBack? webPageCallBack;
InnerWebPageController? controller;
JavascriptChannels();
//log日志
void logFunctionName(String functionName, String data) {
ULog.d("JS functionName -> $functionName JS params -> $data");
}

Set? baseJavascriptChannels(BuildContext context){
var javascriptChannels = {
_alertJavascriptChannel(context),
};
var other = otherJavascriptChannels(context);
if(other!=null){
javascriptChannels.addAll(other);
}
return javascriptChannels;
}

//lib库基本方法
JavascriptChannel _alertJavascriptChannel(BuildContext context) {
var jname = 'Toast';
return JavascriptChannel(
name: jname,
onMessageReceived: (JavascriptMessage message) {
logFunctionName(jname,message.message);
TipToast.instance.tip(message.message);
});
}
//实现类实现方法
Set? otherJavascriptChannels(BuildContext context);

}

三、构建UrlIntercept url拦截抽象基础类



abstract class UrlIntercept{
WebPageCallBack? webPageCallBack;

WebPageUrlIntercept _webPageUrlIntercept;

InnerWebPageController? controller;

UrlIntercept(this._webPageUrlIntercept);
//基本拦截
bool baseUrlIntercept(String url){
ULog.d('intercept: ${url}');
return _libUrlIntercept( url)||otherUrlIntercept( url);
}
//其他拦截
bool otherUrlIntercept(String url) {
return _webPageUrlIntercept.call(url,controller);
}
//lib 库默认拦截
bool _libUrlIntercept(String url) {
return _openPay(url);
}

// 跳转外部支付
bool _openPay(String url) {
if (url.startsWith('alipays:') || url.startsWith('weixin:')) {
canLaunch(url).then((value) => {
if(value){
launch(url)
}else{
TipToast.instance.tip('未安装支付软件')
}
});
return true;
}
return false;
}
}

四、webview widget实现




class InnerWebPage extends StatefulWidget{

String _url;
TitleCallBack? _titleCallBack;
JavascriptChannels? _javascriptChannels;
UrlIntercept? _urlIntercept;
InnerWebPageCreatedCallback? _onInnerWebPageCreated;
WebResourceErrorCallback? _onWebResourceError;
InnerWebPage(String url,{TitleCallBack? titleCallBack,JavascriptChannels? javascriptChannels,UrlIntercept? urlIntercept,InnerWebPageCreatedCallback? onInnerWebPageCreated,WebResourceErrorCallback? onWebResourceError}):_url = url,_titleCallBack = titleCallBack,_javascriptChannels = javascriptChannels,_urlIntercept = urlIntercept,_onInnerWebPageCreated = onInnerWebPageCreated,_onWebResourceError = onWebResourceError;

@
override
State
createState() => _InnerWebPageState();

}

class _InnerWebPageState extends State
{

late
WebViewController _controller;
InnerWebPageController? _innercontroller;

@
override
void initState() {
super.initState();
// Android端复制粘贴问题
if (Platform.isAndroid) {
if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();
}

}

@
override
Widget build(BuildContext context) {
return WebView(
onWebViewCreated: (controller){
ULog.i("WebView is create");
LibLoading.show();
_controller = controller;
_innercontroller =
InnerWebPageController(_controller);
widget._onInnerWebPageCreated?.call(_innercontroller!);
widget._javascriptChannels?.controller = _innercontroller;
widget._urlIntercept?.controller = _innercontroller;
//本地与线上文件展示
if(!TextUtil.isNetUrl(widget._url)){
_loadHtmlAssets(controller);
}
else{
controller.loadUrl(widget._url);
}
},
onPageFinished: (url) async{
//加载完成
LibLoading.dismiss();
ULog.d("${url} loading finish");
_controller.runJavascriptReturningResult(
"document.title").then((result){
widget._titleCallBack?.call(result);
});
},
onPageStarted: (
String url) {
ULog.d("${url} loading start");
},
onWebResourceError: (error){
//错误回调
LibLoading.dismiss();
ULog.d("loading error -> ${error.errorCode},${error.description},${error.domain},${error.errorType},${error.failingUrl}");
widget._onWebResourceError?.call(error);
},
navigationDelegate : (
NavigationRequest request){
//拦截处理
if (widget._urlIntercept?.baseUrlIntercept(request.url)??false) {
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
// initialUrl : TextUtil.isNetUrl(widget._url)?widget._url:Uri.dataFromString(widget._url, mimeType: 'text/html', encoding: Encoding.getByName('utf-8')).toString(),
// 是否支持js,默认是不支持的
javascriptMode:
JavascriptMode.unrestricted,
gestureNavigationEnabled:
true, //启用手势导航
//js 调用 flutter
javascriptChannels: widget._javascriptChannels?.baseJavascriptChannels(context),
);
}


//加载本地文件
_loadHtmlAssets(
WebViewController controller) async {
String htmlPath = await DefaultAssetBundle.of(context).loadString(widget._url);
controller.loadUrl(
Uri.dataFromString(htmlPath,mimeType: 'text/html', encoding: Encoding.getByName('utf-8'))
.
toString());
}


}


四、app中的实现与使用


4.1 JavascriptChannels 实现



class WisdomworkJavascriptChannels extends JavascriptChannels{
@override
Set? otherJavascriptChannels(BuildContext context) {
return {_appInfoJavascriptChannel(context),
_reportNameJavascriptChannel(context),
_saveImageJavascriptChannel(context),
};
}
//调用函数
JavascriptChannel _appInfoJavascriptChannel(BuildContext context) {
var jname = 'appInfo';
return JavascriptChannel(
name: jname,
onMessageReceived: (JavascriptMessage message) {
logFunctionName(jname,message.message);
Map user = convert.jsonDecode(message.message);
if(user.containsKey("showBar")){
webPageCallBack?.call(LibWebPage.ACTION_SHOW_BAR,user["showBar"]!);
// setState(() {
// isShowToolBar = user["showBar"]!;
// });
}

if(user.containsKey("shareFlag")){
webPageCallBack?.call(LibWebPage.ACTION_SHOW_RIGHT,user["shareFlag"]!);
// setState(() {
// hasShare = user["shareFlag"]!;
// });
}

// 数据传输
String callbackname = message.message; //实际应用中要通过map通过key获取
Map backParams = {
"userToken": UserStore().getUserToken()??"",
"userId": UserStore().getUserId()??"",
"userName": UserStore().getUserName()??"",
"titleHeight":MediaQuery.of(context).size.height * 0.07,
"statusHeight":MediaQueryData.fromWindow(window).padding.top,
"role": "teacher"
};

String jsondata= convert.json.encode(backParams);
controller?.runJavascript("callJS", [jsondata],true);
});
}



JavascriptChannel _reportNameJavascriptChannel(BuildContext context) {
var jname = 'getReportName';

return JavascriptChannel(
name: jname,
onMessageReceived: (JavascriptMessage message) {
logFunctionName(jname,message.message);

Map user = convert.jsonDecode(message.message);
if(user.containsKey("reportName")){
webPageCallBack?.call(WisdomworkLibWebPageCallback.REPORT_NAME,user['reportName']!);
}
});
}


JavascriptChannel _saveImageJavascriptChannel(BuildContext context) {
var jname = 'savePicture';
return JavascriptChannel(
name: jname,
onMessageReceived: (JavascriptMessage message) {
logFunctionName(jname,message.message);
Map user = convert.jsonDecode(message.message);
if(user.containsKey("url")){
var url = user['url']!;
if(url.isNotEmpty){
ULog.d("下载的地址:$url");
ImageTool.saveImageToPhoto(url);
}
}
});
}
}

4.2 UrlIntercept 实现



class WisdomworkUrlIntercept extends UrlIntercept{
WisdomworkUrlIntercept() : super((String url,InnerWebPageController? _controller) {
return false;
});

}

4.3 web通用页实现



import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_base_ui/flutter_base_ui.dart';
import 'package:flutter_base_ui/src/widget/appbar/default_app_bar.dart';
import 'package:flutter_base_ui/src/widget/web/inner_web_page.dart';
import 'package:flutter_base_ui/src/widget/web/url_intercept.dart';

import 'javascript_channels.dart';

abstract class LibWebPageCallBack{

void libWebPagerightBtn(String? key,dynamic value,InnerWebPageController _controller);
void libWebPageCallBack(String? key,dynamic value,InnerWebPageController _controller);

}

class LibWebPage extends StatefulWidget{

static const String TITLE = "title";
static const String URL = "url";
static const String RIGHT = "right";
static const String RIGHT_VALUE = "rightValue";
static const String RIGHT_KEY = "rightKey";
static const String BACKPAGE = "backpage";


static const String ACTION_SHOW_BAR = "actionShowBar";
static const String ACTION_BACK = "actionBack";
static const String ACTION_SHOW_RIGHT = "actionShowRight";

static LibWebPage start(Map argument,{JavascriptChannels? javascriptChannels,UrlIntercept? urlIntercept,LibWebPageCallBack? libWebPageCallBack,Widget? back}){
return LibWebPage(argument[URL]!,title: argument[TITLE],javascriptChannels: javascriptChannels,urlIntercept: urlIntercept,back:back,libWebPageCallBack: libWebPageCallBack
,backPage: argument[BACKPAGE],right: argument[RIGHT],rightValue: argument[RIGHT_VALUE],rightkey: argument[RIGHT_KEY],isShowRight: argument[ACTION_SHOW_RIGHT],isShowToolBar: argument[ACTION_SHOW_BAR],);
}

static Map getArgument(String url,{String? title,bool? backPage, Widget? right,bool? isShowToolBar,bool? isShowRight,String? rightkey,dynamic rightValue}){
return {
URL :url,
TITLE :title,
RIGHT :right,
RIGHT_VALUE :rightValue,
RIGHT_KEY :rightkey,
BACKPAGE :backPage,
ACTION_SHOW_BAR :isShowToolBar,
ACTION_SHOW_RIGHT :isShowRight,
};
}



String? title;
final String url;
JavascriptChannels? _javascriptChannels;
UrlIntercept? _urlIntercept;
LibWebPageCallBack? _libWebPageCallBack;
bool _backPage;
Widget? _right;
String? _rightkey;
dynamic _rightValue;
Widget? _back;
bool isShowToolBar;
bool isShowRight;
LibWebPage(String url,{String? title,bool? isShowToolBar,bool? isShowRight,JavascriptChannels? javascriptChannels,UrlIntercept? urlIntercept,Widget? back,bool? backPage, Widget? right,String? rightkey,dynamic rightValue,LibWebPageCallBack? libWebPageCallBack})
:this.url = url,_javascriptChannels = javascriptChannels,_urlIntercept = urlIntercept,this.title = title,this._back = back,this.isShowToolBar = isShowToolBar??true,this.isShowRight = isShowRight??true,
_backPage = backPage??false,_right = right,_rightkey = rightkey,_rightValue = rightValue,_libWebPageCallBack = libWebPageCallBack;

@override
State createState() => _LibWebPageState();

}

class _LibWebPageState extends State{

late InnerWebPageController _innerWebPageController;

String? urlTitle;
// EmptyStatusController? emptyStatusController;
var status = EmptyStatus.none;

WebPageCallBack? webPageCallBack;

@override
void initState() {
super.initState();
webPageCallBack = (String name,dynamic value){
widget._libWebPageCallBack?.libWebPageCallBack(name, value, _innerWebPageController);
switch(name){
case LibWebPage.ACTION_SHOW_BAR:
setState(() {
widget.isShowToolBar = value;
});
break;
case LibWebPage.ACTION_SHOW_RIGHT:
setState(() {
widget.isShowRight = value;
});
break;
case LibWebPage.ACTION_BACK:
if(value){
Navigator.of(context).pop();
}else{
_goBack(context).then((value) => {
Navigator.of(context).pop()
});
}
break;
}

};

}

@override
Widget build(BuildContext context) {
var title;
if(widget.title == null){
if(urlTitle!=null){
title = urlTitle;
}
}else{
title = widget.title;
}

return WillPopScope(child: Scaffold(


appBar: !widget.isShowToolBar? null
: DefalutBackAppBar(title??"",back : widget._back,showRight :widget.isShowRight,tap: () => _goBack(context),right: widget._right,rightcallback: (){
widget._libWebPageCallBack?.libWebPagerightBtn(widget._rightkey, widget._rightValue, _innerWebPageController);
},),
body: LibEmptyView(
layoutType: status,
refresh: () {

status = EmptyStatus.none;
_innerWebPageController.reload();

},

child: InnerWebPage(widget.url,titleCallBack: (title){
setState(() {
urlTitle = title;
});
},javascriptChannels: widget._javascriptChannels,urlIntercept: widget._urlIntercept,onInnerWebPageCreated: (innerWebPageController){
_innerWebPageController = innerWebPageController;
widget._javascriptChannels?.webPageCallBack = webPageCallBack;
widget._urlIntercept?.webPageCallBack = webPageCallBack;
},onWebResourceError: (error){
setState(() {
status = EmptyStatus.fail;
});
},),
),
),
onWillPop: () {
return _goBack(context);
});
}

Future _goBack(BuildContext context) async {
if(widget._backPage){
return true;
}
if (await _innerWebPageController.canGoBack()) {
_innerWebPageController.goBack();
return false;
}
return true;
}
}

4.3 外部页面WebPageCallBack 回调,处理js交互逻辑


例子(处理下载pdf,并分享)



class WisdomworkLibWebPageCallback extends LibWebPageCallBack{
static const String REPORT_DETAIL = "ReportDetail";
static const String REPORT_NAME = "reportName";

String? reportName;

@override
void libWebPageCallBack(String? key, dynamic value, InnerWebPageController _controller)
{
switch(key){
case REPORT_NAME:
reportName = value;
break;
}
}

@override
void libWebPagerightBtn(String? key, dynamic value, InnerWebPageController _controller)
{
switch(key){
case REPORT_DETAIL:
if(reportName?.isEmpty??true){
TipToast.instance.tip("网页加载完毕后再分享");
return;
}
LibLoading.show(status: "下载中");
ReportResponsitory.instance.createFileOfPdfUrl(value.toString(),reportName!).then((f) {
ULog.d(f);
String pdfpath = f.path;
List imagePaths = [];
imagePaths.add(pdfpath);
final box = LibRouteNavigatorObserver.instance.navigator!.context.findRenderObject() as RenderBox?;
LibLoading.dismiss();
Share.shareFiles(imagePaths,
mimeTypes: ["application/pdf"],
text: null,
subject: null,
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
});
break;
}
}

}

以上就是flutter 的Hybrid 混合开发封装


本人将js与拦截操作从原有的web组件中抽离出来,相当于业务抽离在外。与webview的耦合降低。


感谢大家阅读我的文章

收起阅读 »

Kotlin - 改良观察者模式

一、前言 观察者模式 作用:定义了一个一对多的依赖关系,让一个或多个观察者对象监听一个主题对象。这样一来,当被观察者状态发生改变时,需要通知相应的观察者,使这些观察者对象能够自动更新。 核心操作: 观察者(订阅者)添加或删除对 被观察者(主题)的状态监听...
继续阅读 »

一、前言



  • 观察者模式

    • 作用:定义了一个一对多的依赖关系,让一个或多个观察者对象监听一个主题对象。这样一来,当被观察者状态发生改变时,需要通知相应的观察者,使这些观察者对象能够自动更新。

    • 核心操作:

      • 观察者(订阅者)添加或删除对 被观察者(主题)的状态监听

      • 被观察者(主题)状态改变时,将事件通知给所有观察者,观察者执行响应逻辑






二、使用观察者模式



  • 例子:监听股票价格变动

  • 重点:使用 Java API 或 自定义实现 观察者模式


1、使用 Java API 实现观察者模式


Java 标准库中提供了通用观察者模式的 API,分别是:



  • java.util.Observable:被观察者(主题)

    • setChanged():标记状态更新

    • addObserver():添加观察者

    • deleteObserver():删除观察者

    • countObservers():获取观察者数量

    • notifyObservers():通知所有观察者

    • notifyObservers(Object arg):通知所有观察者(携带参数 arg)



  • java.util.Observer:观察者(订阅者)


利用 Java API,可以实现监听股票价格变动这个功能:


import java.util.Observable
import java.util.Observer

/**
* 被观察者(主题)
*
* @author GitLqr
*/
class StockSubject : Observable() {
fun changeStockPrice(price: Int) {
this.setChanged() // 标识状态更新
this.notifyObservers(price) // 通知所有观察者当前股票价格
}
}

/**
* 观察者(订阅者)
*
* @author GitLqr
*/
class StockDisplay(val name: String) : Observer {
override fun update(o: Observable?, price: Any?) {
println("$name receive stock price : $price") // 注意 price 的类型是 Any?
}
}

// 使用
val subject = StockSubject()
subject.addObserver(StockDisplay("observer 1"))
subject.addObserver(StockDisplay("observer 2"))
subject.changeStockPrice(200)

// 输出
// observer 2 receive stock price : 200
// observer 1 receive stock price : 200


注意:在主题中通过 notifyObservers() 方法通知订阅者之前,需要先调用 setChanged() 标识状态更新,才能正常通知给订阅者,这是使用 Java API 实现观察者模式时需要注意的一点。



Java 提供的 API 已经涵盖了观察者模式的完整实现,所以我们在使用的时候,只需要关注业务本身,而不用自己去做模式的具体实现,但是呢,Java 提供的 API 是一种通用实现,从上面的例子中可以注意到,StockDisplay.update(o: Observable?, price: Any?) 中的 price 参数类型是 Any? ,这就会有以下几个问题:



  • 参数判断:因为参数类型是 Any?,所以开发中不得不对 参数是否为空 以及 参数的实际类型 做判断。

  • 通知入口单一:实际业务需求会更加复杂,而 java.util.Observer 只有唯一一个通知入口 update(o: Observable?, arg: Any?),所以我们不得不在该方法中分离响应逻辑,比如股票价格升降,这会让代码显得臃肿。


2、自定义实现观察者模式


虽然 Java 提供了现成的观察者模式 API,但是实际开发中,我们通常还是会自定义实现观察者模式,以便更好的控制代码结构:


/**
* 回调接口(解耦业务通知入口)
*
* @author GitLqr
*/
interface StockUpdateListener {
fun onRise(price: Int)
fun onFall(price: Int)
}

/**
* 被观察者(主题)
*
* @author GitLqr
*/
class StockSubject {
val listeners = mutableSetOf<StockUpdateListener>()
var price: Int = 0

fun subscribe(observer: StockUpdateListener) {
listeners.add(observer)
}

fun unsubscribe(observer: StockUpdateListener) {
listeners.remove(observer)
}

fun changeStockPrice(price: Int) {
val isRise = price > this.price
listeners.forEach { if (isRise) it.onRise(price) else it.onFall(price) }
this.price = price
}
}

/**
* 观察者(订阅者)
*
* @author GitLqr
*/
class StockDisplay : StockUpdateListener {
override fun onRise(price: Int) {
println("The latest stock price has rise to $price")
}

override fun onFall(price: Int) {
println("The latest stock price has fell to $price")
}
}

// 使用
val subject = StockSubject()
subject.subscribe(StockDisplay())
subject.changeStockPrice(200) // The latest stock price has rise to 200

可见,自定义实现观察者模式,可以让代码结构变得更加简单直观。


三、改良观察者模式



  • 例子:监听股票价格变动

  • 重点:委托属性 Delegates.observable()


Kotlin 标准库引入了可被观察的委托属性,可通过 xxx by Delegates.observable() 的方式,用来监听 xxx 属性的改变,于是可以用来改良上面的自定义观察者模式:


import kotlin.properties.Delegates

/**
* 观察者模式改良:使用委托属性监听值变化后通知
*
* @author GitLqr
*/
class StockSubject {
val listeners = mutableSetOf<StockUpdateListener>()

var price: Int by Delegates.observable(0) { prop, old, new ->
val isRise = new > old
listeners.forEach { if (isRise) it.onRise(price) else it.onFall(price) }
}

fun subscribe(observer: StockUpdateListener) {
listeners.add(observer)
}

fun unsubscribe(observer: StockUpdateListener) {
listeners.remove(observer)
}

// fun changeStockPrice(price: Int) { ... }
}

// 使用
val subject = StockSubject()
subject.subscribe(StockDisplay())
subject.price = 250 // The latest stock price has rise to 200

使用 Delegates.observable() 之后,StockSubject 相比之前减少了一个 changeStockPrice() 方法。使用上,一旦对 price 属性赋值,就可以触发通知,显然,这对使用者更加友好了(直观,少记一个方法)。


四、补充


前面说到,Kotlin 标准库引入可被观察的委托属性,除了 Delegates.observable() 之外,还有
Delegates.vetoable() 也很实用,当我们不希望被监控的属性被随意修改时,就可以用它来否决属性赋值:


import kotlin.properties.Delegates

var value: Int by Delegates.vetoable(0) { prop, old, new ->
// 新值大于0时,才给属性赋值
new > 0
}

// 使用
value = 1
println(value) // 1
value = -1
println(value) // 1(没能赋值成功)

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

面试官:private修饰的方法可以通过反射访问,那么private的意义是什么?

在一个类中,为了不让外界访问到某些属性和方法,通常将其设置为private,用正常的方式(对象名.属性名,对象名.方法名)将无法访问此属性与方法,但有没有其他方法可以访问呢?答案是有的,这就是java反射带来的便利。利用反射访问类的私有属性及方法如下: /**...
继续阅读 »

在一个类中,为了不让外界访问到某些属性和方法,通常将其设置为private,用正常的方式(对象名.属性名,对象名.方法名)将无法访问此属性与方法,但有没有其他方法可以访问呢?答案是有的,这就是java反射带来的便利。利用反射访问类的私有属性及方法如下:


/**  * @Description: 反射  * @author: Mr_VanGogh  */
public class Reflect {
 
    private String name;
    private int age;
 
    private Reflect(int age) {
        this.age = age;
    }
 
    private void speak(String name) {
        System.out.println("My name is" + name);
    }
 
    public Reflect(String name) {
        this.name = name;
    }
}

首先,我们要了解三个反射包中的类:



  • Constructor:代表类的单个构造方法,通过Constructor我们可执行一个类的某个构造方法(有参或者无参)来创建对象时。

  • Method:代表类中的单个方法,可以用于执行类的某个普通方法,有参或无参,并可以接收返回值。

  • Field:代表类中的单个属性,用于set或get属性

  • AccessibleObject:以上三个类的父类,提供了构造方法,普通方法,和属性的访问控制的能力。


使用Class类中的方法可以获得该类中的所有Constructor对象,Method对象,和Field对象。但是任然无法访问私有化的构造方法,普通方法,和私有属性,此时我们可以使用他们继承父类(AccessibleObject)中的setAccessible()方法,来设置或取消访问检查,以达到访问私有对象的目的。


public static void main(String[] args)  throws Exception {
 
        Reflect reflect = new Reflect("a");
 
        Method[] methods = Reflect.class.getMethods();
        Field[] fields = Reflect.class.getDeclaredFields();
 
        for (int i = 0; i < fields.length; i ++) {
            fields[i].setAccessible(true);
            System.out.println(fields[i].getName());
        }
 
        for (int j = 0; j < methods.length; j ++) {
            methods[j].setAccessible(true);
            System.out.println(methods[j].getName());
 
            methods[j].invoke(reflect);
            System.out.println(methods[j].getName());
        }
    }

这样,我们就获得了私有属性的值


当然,凡事有利就有弊,然后我们再来说一下java反射的优缺点;


优点:



  • 能够运行时动态获取类的实例,大大提高了系统的灵活性和扩展性;

  • 与java动态编译相结合,可以实现无比强大的功能。


缺点:



  • 使用反射的性能较低;

  • 使用反射来说相对不安全;

  • 破坏了类的封装性,可以通过反射来获取这个类的属性,和私有方法。


Q:private修饰的方法可以通过反射访问,那么private的意义是什么?


A:


1、Java的private修饰符不是为了绝对安全设计的,而是对用户常规使用Java的一种约束。就好比饭店厨房门口挂着“闲人免进”的牌子,但是你还是能够通过其他方法进去。


2、从外部对对象进行常规调用时,能够看到清晰的类结构


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

快慢指针的妙用

快慢指针是双指针的一种典型用法,通常控制两个指针以不同的速度移动来解决问题。采用快慢指针解决问题往往都很巧妙。本文我们通过分析几个例子来学习快慢指针的用法,并分析其本质,最终达到方便记忆、灵活使用的目的。 接下来我们先看四个例子:判断链表是否有环、寻找链表中环...
继续阅读 »

快慢指针是双指针的一种典型用法,通常控制两个指针以不同的速度移动来解决问题。采用快慢指针解决问题往往都很巧妙。本文我们通过分析几个例子来学习快慢指针的用法,并分析其本质,最终达到方便记忆、灵活使用的目的。


接下来我们先看四个例子:判断链表是否有环、寻找链表中环的入口、寻找链表的中间结点、寻找链表的倒数第 n 个结点。


判断链表是否有环


题目(来源Leetcode)


“给你一个链表的头结点 head ,判断链表中是否有环。


如果链表中有某个结点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。


如果链表中存在环 ,则返回 true 。 否则,返回 false 。”



环形链表:leetcode-cn.com/problems/li…



分析


这道题有两种常见解法:1. 哈希表 2. 快慢指针。我们主要分析快慢指针的解法:


我们定义两个指针:一个移动的慢叫慢指针,一个移动的快叫快指针。慢指针在链表中一次移动一个结点,快指针在链表中一次移动两个结点,如果两个指针最终相遇了,说明有环;如果快指针顺利到达了链表的终点说明没有环。



相遇:快指针 = 慢指针;到达终点:快指针的下一个结点为Null。



要想理解这个解决办法需要先了解:Floyd 判圈算法


Floyd 判圈算法又称为龟兔赛跑算法(Tortoise and Hare Algorithm)。乌龟跑得慢、兔子跑得快。乌龟和兔子在赛跑,如果存在环的话,兔子必然会追上乌龟(乌龟被套圈了)。


我们把乌龟比作慢指针、兔子比作快指针同时在链表中移动。跑步和指针移动不太相同的是,跑步的路程是连续的,快指针一次移动两个结点是不连续的。又反过来想一下如果链表中存在环,那最小的环也需要两个结点,所以对于是否有环来说快指针一次移动两个结点也是不会错过任何一个环的。而且环的结点不管是奇数还是偶数个,快指针也最终会和慢指针在某个结点重合。


答案


基于上面的分析我们给出代码如下:


func hasCycle(head *ListNode) bool {
    if head == nil || head.Next == nil { // 如果链表有0个或者1个结点,则链表不存在环
        return false
    }
    slowPoint, fastPoint := head, head.Next // 定义快慢指针
    for fastPoint != slowPoint { // 如果快慢指针相等则结束循环,证明有环
        if fastPoint == nil || fastPoint.Next == nil { // 如果快指针到达终点或者终点前的倒数第一个结点,说明没有环
            return false
        }
        slowPoint = slowPoint.Next
        fastPoint = fastPoint.Next.Next
    }
    return true
}

寻找链表中环的入口


题目(来源Leetcode)


“给定一个链表的头结点 head ,返回链表开始入环的第一个结点。 如果链表无环,则返回 null。


如果链表中有某个结点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。”



环形链表 II:leetcode-cn.com/problems/li…



分析


这道题和上题一样,通常有哈希表、快慢指针两种解法。我们主要分析快慢指针的解法:


我们定义两个指针:一个移动的慢叫慢指针,一个移动的快叫快指针。慢指针在链表中一次移动一个结点,快指针在链表中一次移动两个结点,如果两个指针最终相遇了,说明有环;如果快指针顺利到达了链表的终点说明没有环。



当有环存在时,我们假设快慢指针在 D 点相遇。a 代表入环之前的长度,b 代表慢指针进入环后又走了b的长度,c 代表环余下的长度。指针的指向是圆的顺时针方向。



  1. 如果快指针和慢指针在 D 点相遇,此时快指针比慢指针多走了 n 圈,也就是 n*(b+c) 的长度。

  2. 此时快指针走过的距离是 a+n*(b+c)+b,慢指针走过的距离是 a+b。

  3. 因为快指针每次走两步,慢指针每次走一步,所以快指针走过的距离永远是慢指针的两倍,所以 a+n* (b+c)+b=2(a+b)*

  4. 上述公式可以推导出a = (n-1)*(b+c)+c,也就是a的长度是恰好是 n-1 圈环的长度加上从 D 点到相遇点的距离。


根据上面的推论,当快慢指针相遇之后,我们再申请一个指针从链表的头部开始,每次移动一个结点,同时慢指针一次移动一个结点,这两个指针最终的相遇点就是环的入口点。


答案


func detectCycle(head *ListNode) *ListNode {
    slowPoint, fastPoint := head, head
    for fastPoint != nil && fastPoint.Next != nil { // 如果快指针到达终点或者终点前的倒数第一个结点,说明没有环。
        slowPoint = slowPoint.Next
        fastPoint = fastPoint.Next.Next
        if fastPoint == slowPoint { // 如果快慢指针相等,说明有环,后面开始寻找环的入口。
            delectPoint := head
            for delectPoint != slowPoint { // 慢指针和从头开始的delectPoint指针相等,则说明当前结点就是环的入口。
                delectPoint = delectPoint.Next
                slowPoint = slowPoint.Next
            }
            return delectPoint
        }
    }
    return nil
}

寻找链表的中间结点


题目(来源Leetcode)


给定一个头结点为 head 的非空单链表,返回链表的中间结点。


如果有两个中间结点,则返回第二个中间结点。



链表的中间结点:leetcode-cn.com/problems/mi…



分析


这道题有多种解法,但是快慢指针的解法十分巧妙。我们定义两个指针:一个移动的慢叫慢指针,一个移动的快叫快指针。慢指针在链表中一次移动一个结点,快指针在链表中一次移动两个结点。因为快指针移动的距离始终是慢指针的两倍,所以当快指针移动到链表尾部时,慢指针刚好在链表中间位置。


答案


func middleNode(head *ListNode) *ListNode {
    slowPoint, fastPoint := head, head
    for fastPoint!= nil && fastPoint.Next != nil{
        slowPoint = slowPoint.Next
        fastPoint = fastPoint.Next.Next
    }
    return slowPoint
}

寻找链表的倒数第 N 个结点


题目


给你一个链表,找到链表的倒数第 n 个结点并返回。


分析


这道题有多种解法,但是快慢指针的解法十分巧妙。我们定义两个指针:两个指针每次移动一个结点。我们让其中一个指针先移动 n 个结点,先移动的这个指针我们叫它快指针,另外一个叫慢指针。然后两个指针同时移动。因为移动速度相同所以两个指针之间的距离始终是 n ,当快指针到达链表尾部时,慢指针刚好指向了链表的倒数第 n 个结点。



这里叫“快慢指针”有点牵强,感觉叫前后指针更合适,不过为了方便记忆就先归为快慢指针吧。



答案


func findNthFromEnd(head *ListNode, n int) *ListNode {
    fastPoint,slowPoint := node,node
    for i:=0; i<n+1; i++{
        fastPoint  = fastPoint.Next
    }
    for fastPoint != nil {
        fastPoint=  fastPoint.Next
        slowPoint = slowPoint.Next
    }
    return slowPoint
}

总结


本文我们介绍了快慢指针的常见用法:



  1. 利用 Floyd 判圈算法找环。

  2. 利用 Floyd 判圈算法找环的入口。

  3. 寻找链表的中间结点。

  4. 寻找链表的倒数第 n 个结点。


在 Floyd 判圈算法中,把判断环的问题抽象成两个指针围绕环运动最终会相遇的问题,通过环形跑道这个生动场景解决了问题;在寻找链表中间节点时,利用两个指针的速度差,使一个指针运动的距离始终是另外一个指针的一半,用指针的速度、路程解决了问题;在寻找链表倒数第 n 个节点时,让相同速度的两个指针始终保持 n 的相对距离,把链表问题抽象成了距离问题。


这几种方法都是把算法问题抽象成了两个指针的距离、速度问题,最终通过数学公式推导得出结论。推而广之,我们以后遇到链表的相关问题也可以采用类似的方式去抽象问题,推导解决办法。


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

MVVM 进阶版:MVI 架构了解一下~

MVI
前言 Android开发发展到今天已经相当成熟了,各种架构大家也都耳熟能详,如MVC,MVP,MVVM等,其中MVVM更是被官方推荐,成为Android开发中的显学。 不过软件开发中没有银弹,MVVM架构也不是尽善尽美的,在使用过程中也会有一些不太方便之处,而...
继续阅读 »

前言


Android开发发展到今天已经相当成熟了,各种架构大家也都耳熟能详,如MVC,MVP,MVVM等,其中MVVM更是被官方推荐,成为Android开发中的显学。

不过软件开发中没有银弹,MVVM架构也不是尽善尽美的,在使用过程中也会有一些不太方便之处,而MVI可以很好的解决一部分MVVM的痛点。

本文主要包括以下内容



  1. MVC,MVP,MVVM等经典架构介绍

  2. MVI架构到底是什么?

  3. MVI架构实战



需要重点指出的是,标题中说MVI架构是MVVM的进阶版是指MVIMVVM非常相似,并在其基础上做了一定的改良,并不是说MVI架构一定比MVVM适合你的项目

各位同学可以在分析比较各个架构后,选择合适项目场景的架构



经典架构介绍


MVC架构介绍


MVC是个古老的Android开发架构,随着MVPMVVM的流行已经逐渐退出历史舞台,我们在这里做一个简单的介绍,其架构图如下所示:



MVC架构主要分为以下几部分



  1. 视图层(View):对应于xml布局文件和java代码动态view部分

  2. 控制层(Controller):主要负责业务逻辑,在android中由Activity承担,同时因为XML视图功能太弱,所以Activity既要负责视图的显示又要加入控制逻辑,承担的功能过多。

  3. 模型层(Model):主要负责网络请求,数据库处理,I/O的操作,即页面的数据来源


由于androidxml布局的功能性太弱,Activity实际上负责了View层与Controller层两者的工作,所以在androidmvc更像是这种形式:



因此MVC架构在android平台上的主要存在以下问题:



  1. Activity同时负责ViewController层的工作,违背了单一职责原则

  2. Model层与View层存在耦合,存在互相依赖,违背了最小知识原则


MVP架构介绍


由于MVC架构在Android平台上的一些缺陷,MVP也就应运而生了,其架构图如下所示



MVP架构主要分为以下几个部分



  1. View层:对应于ActivityXML,只负责显示UI,只与Presenter层交互,与Model层没有耦合

  2. Presenter层: 主要负责处理业务逻辑,通过接口回调View

  3. Model层:主要负责网络请求,数据库处理等操作,这个没有什么变化


我们可以看到,MVP解决了MVC的两个问题,即Activity承担了两层职责与View层与Model层耦合的问题


MVP架构同样有自己的问题



  1. Presenter层通过接口与View通信,实际上持有了View的引用

  2. 但是随着业务逻辑的增加,一个页面可能会非常复杂,这样就会造成View的接口会很庞大。


MVVM架构介绍


MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。

唯一的区别是,它采用双向数据绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然

MVVM架构图如下所示:



可以看出MVVMMVP的主要区别在于,你不用去主动去刷新UI了,只要Model数据变了,会自动反映到UI上。换句话说,MVVM更像是自动化的MVP


MVVM的双向数据绑定主要通过DataBinding实现,不过相信有很多人跟我一样,是不喜欢用DataBinding的,这样架构就变成了下面这样



  1. View观察ViewModle的数据变化并自我更新,这其实是单一数据源而不是双向数据绑定,所以其实MVVM的这一大特性我其实并没有用到

  2. View通过调用ViewModel提供的方法来与ViewMdoel交互


小结



  1. MVC架构的主要问题在于Activity承担了ViewController两层的职责,同时View层与Model层存在耦合

  2. MVP引入Presenter层解决了MVC架构的两个问题,View只能与Presenter层交互,业务逻辑放在Presenter

  3. MVP的问题在于随着业务逻辑的增加,View的接口会很庞大,MVVM架构通过双向数据绑定可以解决这个问题

  4. MVVMMVP的主要区别在于,你不用去主动去刷新UI了,只要Model数据变了,会自动反映到UI上。换句话说,MVVM更像是自动化的MVP

  5. MVVM的双向数据绑定主要通过DataBinding实现,但有很多人(比如我)不喜欢用DataBinding,而是View通过LiveData等观察ViewModle的数据变化并自我更新,这其实是单一数据源而不是双向数据绑定


MVI架构到底是什么?


MVVM架构有什么不足?


要了解MVI架构,我们首先来了解下MVVM架构有什么不足

相信使用MVVM架构的同学都有如下经验,为了保证数据流的单向流动,LiveData向外暴露时需要转化成immutable的,这需要添加不少模板代码并且容易遗忘,如下所示


class TestViewModel : ViewModel() {
//为保证对外暴露的LiveData不可变,增加一个状态就要添加两个LiveData变量
private val _pageState: MutableLiveData<PageState> = MutableLiveData()
val pageState: LiveData<PageState> = _pageState
private val _state1: MutableLiveData<String> = MutableLiveData()
val state1: LiveData<String> = _state1
private val _state2: MutableLiveData<String> = MutableLiveData()
val state2: LiveData<String> = _state2
//...
}

如上所示,如果页面逻辑比较复杂,ViewModel中将会有许多全局变量的LiveData,并且每个LiveData都必须定义两遍,一个可变的,一个不可变的。这其实就是我通过MVVM架构写比较复杂页面时最难受的点。

其次就是View层通过调用ViewModel层的方法来交互的,View层与ViewModel的交互比较分散,不成体系


小结一下,在我的使用中,MVVM架构主要有以下不足



  1. 为保证对外暴露的LiveData是不可变的,需要添加不少模板代码并且容易遗忘

  2. View层与ViewModel层的交互比较分散零乱,不成体系


MVI架构是什么?


MVIMVVM 很相似,其借鉴了前端框架的思想,更加强调数据的单向流动和唯一数据源,架构图如下所示



其主要分为以下几部分



  1. Model: 与MVVM中的Model不同的是,MVIModel主要指UI状态(State)。例如页面加载状态、控件位置等都是一种UI状态

  2. View: 与其他MVX中的View一致,可能是一个Activity或者任意UI承载单元。MVI中的View通过订阅Model的变化实现界面刷新

  3. Intent: 此Intent不是ActivityIntent,用户的任何操作都被包装成Intent后发送给Model层进行数据请求


单向数据流


MVI强调数据的单向流动,主要分为以下几步:



  1. 用户操作以Intent的形式通知Model

  2. Model基于Intent更新State

  3. View接收到State变化刷新UI。


数据永远在一个环形结构中单向流动,不能反向流动:


上面简单的介绍了下MVI架构,下面我们一起来看下具体是怎么使用MVI架构的


MVI架构实战


总体架构图




我们使用ViewModel来承载MVIModel层,总体结构也与MVVM类似,主要区别在于ModelView层交互的部分



  1. Model层承载UI状态,并暴露出ViewStateView订阅,ViewState是个data class,包含所有页面状态

  2. View层通过Action更新ViewState,替代MVVM通过调用ViewModel方法交互的方式


MVI实例介绍


添加ViewStateViewEvent


ViewState承载页面的所有状态,ViewEvent则是一次性事件,如Toast等,如下所示


data class MainViewState(val fetchStatus: FetchStatus, val newsList: List<NewsItem>)  

sealed class MainViewEvent {
data class ShowSnackbar(val message: String) : MainViewEvent()
data class ShowToast(val message: String) : MainViewEvent()
}


  1. 我们这里ViewState只定义了两个,一个是请求状态,一个是页面数据

  2. ViewEvent也很简单,一个简单的密封类,显示ToastSnackbar


ViewState更新


class MainViewModel : ViewModel() {
private val _viewStates: MutableLiveData<MainViewState> = MutableLiveData()
val viewStates = _viewStates.asLiveData()
private val _viewEvents: SingleLiveEvent<MainViewEvent> = SingleLiveEvent()
val viewEvents = _viewEvents.asLiveData()

init {
emit(MainViewState(fetchStatus = FetchStatus.NotFetched, newsList = emptyList()))
}

private fun fabClicked() {
count++
emit(MainViewEvent.ShowToast(message = "Fab clicked count $count"))
}

private fun emit(state: MainViewState?) {
_viewStates.value = state
}

private fun emit(event: MainViewEvent?) {
_viewEvents.value = event
}
}

如上所示



  1. 我们只需定义ViewStateViewEvent两个State,后续增加状态时在data class中添加即可,不需要再写模板代码

  2. ViewEvents是一次性的,通过SingleLiveEvent实现,当然你也可以用Channel当来实现

  3. 当状态更新时,通过emit来更新状态


View监听ViewState


    private fun initViewModel() {
viewModel.viewStates.observe(this) {
renderViewState(it)
}
viewModel.viewEvents.observe(this) {
renderViewEvent(it)
}
}

如上所示,MVI 使用 ViewStateState 集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码。


View通过Action更新State


class MainActivity : AppCompatActivity() {
private fun initView() {
fabStar.setOnClickListener {
viewModel.dispatch(MainViewAction.FabClicked)
}
}
}
class MainViewModel : ViewModel() {
fun dispatch(action: MainViewAction) =
reduce(viewStates.value, action)

private fun reduce(state: MainViewState?, viewAction: MainViewAction) {
when (viewAction) {
is MainViewAction.NewsItemClicked -> newsItemClicked(viewAction.newsItem)
MainViewAction.FabClicked -> fabClicked()
MainViewAction.OnSwipeRefresh -> fetchNews(state)
MainViewAction.FetchNews -> fetchNews(state)
}
}
}

如上所示,View通过ActionViewModel交互,通过 Action 通信,有利于 ViewViewModel 之间的进一步解耦,同时所有调用以 Action 的形式汇总到一处,也有利于对行为的集中分析和监控


总结


本文主要介绍了MVC,MVP,MVVMMVI架构,目前MVVM是官方推荐的架构,但仍然有以下几个痛点



  1. MVVMMVP的主要区别在于双向数据绑定,但由于很多人(比如我)并不喜欢使用DataBindg,其实并没有使用MVVM双向绑定的特性,而是单一数据源

  2. 当页面复杂时,需要定义很多State,并且需要定义可变与不可变两种,状态会以双倍的速度膨胀,模板代码较多且容易遗忘

  3. ViewViewModel通过ViewModel暴露的方法交互,比较零乱难以维护


MVI可以比较好的解决以上痛点,它主要有以下优势



  1. 强调数据单向流动,很容易对状态变化进行跟踪和回溯

  2. 使用ViewStateState集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码

  3. ViewModel通过ViewStateAction通信,通过浏览ViewStateAciton 定义就可以理清 ViewModel 的职责,可以直接拿来作为接口文档使用。


当然MVI也有一些缺点,比如



  1. 所有的操作最终都会转换成State,所以当复杂页面的State容易膨胀

  2. state是不变的,因此每当state需要更新时都要创建新对象替代老对象,这会带来一定内存开销


软件开发中没有银弹,所有架构都不是完美的,有自己的适用场景,读者可根据自己的需求选择使用。

但通过以上的分析与介绍,我相信使用MVI架构代替没有使用DataBindingMVVM是一个比较好的选择~


更多


关于MVI架构更佳实践,支持局部刷新,可参见: MVI 架构更佳实践:支持 LiveData 属性监听

关于MVI架构封装,优雅实现网络请求,可参见: MVI 架构封装:快速优雅地实现网络请求


项目地址


本文所有代码可见:github.com/shenzhen201…


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

把 VS Code 带到安卓 - Code FA

注意,本篇讨论的是不基于pc的 这个是9月份初弄出来的,自己一直在使用,一直没来得及分享,前段时间在b站看到了一个差不多的方案。 背景 vs code 大部分是由 ts 编写,上层 UI 可以运行在各个系统的浏览器中,但 vs code 基于 electr...
继续阅读 »

注意,本篇讨论的是不基于pc的
这个是9月份初弄出来的,自己一直在使用,一直没来得及分享,前段时间在b站看到了一个差不多的方案。



camera.jpg


背景


vs code 大部分是由 ts 编写,上层 UI 可以运行在各个系统的浏览器中,但 vs code 基于 electron 框架,这个框架提供了对 node 的支持,一些浏览器内核中的 js 引擎没有的 api,例如 I/O,系统内核的一些交互等。
而 code-server 则是解决了脱离 electron 的问题。
目前安卓上有一个叫 aid learing 的软件,自带 VS Code ,看了一下原理差不多,并不是 linux 图形界面打开的 VS Code,也是打开的 webview 连接本地的服务,但这个玩意占磁盘内存太高,整个下载安装完就干掉6个g。


客户端框架


客户端是用 Flutter 进行的开发,而这个框架的选用并不是为了跨端,仅仅是为了快速尝试,还有基础能力的使用。


实现方法分析


code-server 在 github 发布的版本中是有 arm64 架构的,整个下载后,开终端解压执行就挂了,这个虽然是 arm64,并且带有一个 arm64 的 node,但是是为完整 linux 准备的。也就是说,node 中硬编码了 /usr /lib 等这些路径,并且附带的 node_modules 中也有大量的使用到 linux 特有节点的路径,这些安卓上都没有。
后来一想,termux 中自带的环境也是有 libllvm gcc nodejs 的,把整个 node_mudules 一删,再手动 install 一下,就行了。
所以整个流程大致分为两类。


初始尝试方案:非完整Linux



  1. 启动 termux 环境

  2. 安装 node,python,libllvm,clang

  3. 下载 code-server arm64,解压

  4. 处理兼容,删除 node_modules ,重新 yarn install

  5. 执行 bin/code-server 启动服务


经过一些测试发现,这种模式有一些问题。



  • 下载的依赖太多,由于源都在我的个人服务器,会下很久。

  • 编译太久,yarn install 的时候调用了 gcc 的编译,整个过程特别耗时。

  • 启动的 vs code 用不了搜索代码(正常情况能支持这个功能)

  • 磁盘占用太大,一阵操作下来,直接1.6g磁盘空间给干没了,主要是 npm install 拉了很多东西,还生成了一堆缓存,node_modules 嘛,比黑洞还重的东西。


不过按照以上的流程过一遍后,code-server 内的 node_modules 已经是安卓 arm64 可用的模块了,二次打包 code-server,流程就可以简化成如下



  1. 启动 termux 环境

  2. 安装 node

  3. 下载 code-server arm64,解压

  4. 执行 bin/code-server


但还是会存在编辑器无法搜索代码的 bug,node 虽然只有 20m ,但还是在个人服务器,下行带宽 5mb,大概 700kb/s ,emmm,要集成到 apk 内的话,得集成 deb ,调 dpkg 去安装,放弃。


最后使用方案:完整Linux



  1. 启动 termux 环境

  2. 下载并安装完整 Linux(30m)

  3. 下载 code-server arm64(自带node能用了)

  4. 执行 bin/code-server 启动服务


最终是选用了完整 Linux 的方式,除了安装需要的体积更小之外,还有完整源的支持,异常 bug 的避免等。
由于整个 VS Code 的启动需要的 130mb 的内存都是第一次打开需要的,所以将这些内存的占用放到服务器上,由 app 启动再下载的意义并不大,最后就全都作为资源文件集成到了 apk 内。


具体实现


启动 termux 环境


这个过程之前有现成的轮子了,只需要按照 termux-package 的编译脚本编译一个 bootstrap 集成到 apk,app 启动进行解压,然后根据符号链接格式进行恢复就行。
终端是 termare_view



bootstrap 是一个带有最小依赖的类 linux 环境,有bash,apt 等。



具体实现代码


function initApp(){
cd ${RuntimeEnvir.usrPath}/
echo 准备符号链接...
for line in `cat SYMLINKS.txt`
do
OLD_IFS="\$IFS"
IFS="←"
arr=(\$line)
IFS="\$OLD_IFS"
ln -s \${arr[0]} \${arr[3]}
done
rm -rf SYMLINKS.txt
TMPDIR=/data/data/com.nightmare.termare/files/usr/tmp
filename=bootstrap
rm -rf "\$TMPDIR/\$filename*"
rm -rf "\$TMPDIR/*"
chmod -R 0777 ${RuntimeEnvir.binPath}/*
chmod -R 0777 ${RuntimeEnvir.usrPath}/lib/* 2>/dev/null
chmod -R 0777 ${RuntimeEnvir.usrPath}/libexec/* 2>/dev/null
apt update
rm -rf $lockFile
export LD_PRELOAD=${RuntimeEnvir.usrPath}/lib/libtermux-exec.so
install_vs_code
start_vs_code
bash
}


RuntimeEnvir.usrPath 是 /data/data/$package/files/usr/bin



安装完整 Linux 和 code-server


这个我从好几个方案进行了筛选,起初用的 atlio 这个开源,整个开源依赖 python,并且有一个requirement.txt ,需要执行 python -r requirement.txt,依赖就是一大堆,后来换了 proot-distro,纯 shell,所以只需要直接集成到 apk 内就行。


1.安装 ubuntu


install_ubuntu(){
cd ~
colorEcho - 安装Ubuntu Linux
unzip proot-distro.zip >/dev/null
#cd ~/proot-distro
bash ./install.sh
apt-get install -y proot
proot-distro install ubuntu
echo '$source' > $ubuntuPath/etc/apt/sources.list
}

2.安装 code-server


install_vs_code(){
if [ ! -d "$ubuntuPath/home/code-server-$version-linux-arm64" ];then
cd $ubuntuPath/home
colorEcho - 解压 Vs Code Arm64
tar zxvf ~/code-server-$version-linux-arm64.tar.gz >/dev/null
cd code-server-$version-linux-arm64
fi
}

启动 code-server


直接用 proot-distro 启动就行,非常方便



--termux-home 参数:开启 app 沙盒的 home 挂载到 ubuntu 的 /root 下,这样 ubuntu 就能用 app 里面的文件夹了。



start_vs_code(){
install_vs_code
mkdir -p $ubuntuPath/root/.config/code-server 2>/dev/null
echo '
bind-addr: 0.0.0.0:8080
auth: none
password: none
cert: false
' > $ubuntuPath/root/.config/code-server/config.yaml
echo -e "\x1b[31m- 启动中..\x1b[0m"
proot-distro login ubuntu -- /home/code-server-$version-linux-arm64/bin/code-server
}

其实整个实现其实是没啥难度的,全都是一些 shell 脚本,也是得益于之前的 Termare 系列的支持,有兴趣的可以看下这个组织。
然后就是打开 webview 的过程了,如果觉得性能不好,你可以用局域网的电脑来进行连接。
看一下非首次的启动过程


WebView 实现方案


首先去 pub 看了一下 webview 的插件,官方目前正在维护的 webview 有这样的提示




  • Hybrid composition mode has a built-in keyboard support while Virtual displays mode has multiple keyboard issues

  • Hybrid composition mode requires Android SKD 19+ while Virtual displays mode requires Android SDK 20+

  • Hybrid composition mode has performence limitations when working on Android versions prior to Android 10 while Virtual displays is performant on all supported Android versions



也就是说开启 hybird 后,安卓10以下有性能限制,而使用虚拟显示器的话,键盘问题会很多。


实际尝试的时候,OTG 连接的键盘基本是没法用的。


再分析了下这个场景,最后还是用的原生 WebView,这里有些小坑。


必须启用项


        WebSettings mWebSettings = mWebView.getSettings();
//允许使用JS
mWebSettings.setJavaScriptEnabled(true);
mWebSettings.setJavaScriptCanOpenWindowsAutomatically(true);
mWebSettings.setUseWideViewPort(true);
mWebSettings.setAllowFileAccess(true);
// 下面这行不写不得行
mWebSettings.setDomStorageEnabled(true);
mWebSettings.setDatabaseEnabled(true);
mWebSettings.setAppCacheEnabled(true);
mWebSettings.setLoadWithOverviewMode(true);
mWebSettings.setDefaultTextEncodingName("utf-8");
mWebSettings.setLoadsImagesAutomatically(true);
mWebSettings.setSupportMultipleWindows(true);

路由重定向


有些场景 VS Code 会打开一个新的窗口,例如点击 file -> new window 的时候,不做处理,webview 会调起系统的浏览器。


        //系统默认会通过手机浏览器打开网页,为了能够直接通过WebView显示网页,必须设置
mWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
//使用WebView加载显示url
view.loadUrl(url);
//返回true
return true;
}
});

浏览器正常跳转


例如终端输出了 xxx.xxx,ctrl + 鼠标点击,预期是会打开浏览器的。



mWebView.setWebChromeClient(webChromeClient);
WebChromeClient webChromeClient = new WebChromeClient() {

@Override
public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
WebView childView = new WebView(context);//Parent WebView cannot host it's own popup window.
childView.setBackgroundColor(Color.GREEN);
childView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
return true;
}
});
WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
transport.setWebView(childView);//setWebView和getWebView两个方法
resultMsg.sendToTarget();
return true;
}
};

可行性探索


这个能干嘛?安卓屏幕那么小,电脑能本地用 VsCode 干嘛要连安卓的?



  • 有一个 vs code 加一个完整的 linux 环境,能 cover 住一些场景的开发了,安卓开发等除外。

  • 开发程序到 arm 板子的同学,PC 上还得弄一堆交叉编译工具链,并且每次编译调试过程也很繁琐,现在就能本地写本地编译。


正巧,买了一个平板,爱奇艺之余,也能作为程序员的一波生产力了。


screenshot.jpg


编译 C 语言


选了一个一直在学习的项目,scrcpy,一堆 c 源码,最后很顺利的编译下来了。
build_scrcpy.jpg


Web 开发


移动端的网页调试一直都是问题,作为野路子前端的我也很无奈,一般会加一些 vconsole 的组件来获取调试日志。



之前个人项目速享适配移动端 web 就是这么干的



现在,我们可以本地开发,本地调试,有 node 整个前端大部分项目都能拉下来了,真实的移动端物理环境。
试试
out.gif


写博客


本篇文章完全是在这个安卓版的 VS Code 中完成的,使用 hexo 本地调式


blog.jpg


写文档


docs.jpg


docs_code.jpg


写后台,接口测试


写一点简单的后台,如 python 的 fastapi,flask,并通过 rest client 进行接口测试


rest_client.jpg


最后


为了让其他的用户能直接使用到这个 app,我将其上架到了酷安。


看了下 vscodium 和 code-server 的开源协议都是 MIT,如果有侵权的地方辛苦评论区提醒一下鄙人。


Code FA 酷安下载地址


Code FA 个人服务器下载地址


个人软件快捷下载地址


开源地址


随便玩,有问题评论区留言,觉得不错的给个 star,文章不错的给个赞,🤔


其实还想尝试下 Flutter for web 的,折腾了半天还是失败了,能写代码,能有提示,编译会引发 dart runtime 的 crash。


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

北京市人社局发文:集中排查整治超时加班

据北京市海淀区人民政府网站消息,北京市人社局发布《关于进一步做好工时和休息休假权益维护工作的通知》,在3月15日至5月15日期间,在全市组织开展工时和休息休假权益维护集中排查整治,聚焦重点行业企业,集中排查整治超时加班问题,依法保障职工工时和休息休假权益。本次...
继续阅读 »

据北京市海淀区人民政府网站消息,北京市人社局发布《关于进一步做好工时和休息休假权益维护工作的通知》,在3月15日至5月15日期间,在全市组织开展工时和休息休假权益维护集中排查整治,聚焦重点行业企业,集中排查整治超时加班问题,依法保障职工工时和休息休假权益。

本次集中排查整治的检查对象主要是超时加班问题易发多发的重点行业、重点企业、重点园区,重点突出互联网(平台)企业及关联企业、研发岗位占比较高的技术密集型企业、劳动密集型加工制造业企业和服务业企业。


图片来源:北京市海淀区人民政府网站

北京市人社局指出,集中排查整治内容主要包括五个方面,包括用人单位制定工时、休息休假内部规章制度情况;用人单位实行特殊工时制度情况;用人单位安排加班情况以及依法与工会和劳动者协商情况;用人单位落实职工休息休假制度情况;用人单位支付加班费、未休年休假工资报酬情况等。

在排查整治行动中,还将通过新闻媒体、“北京人社”系列政务新媒体等平台发布工时和休息休假政策问答。全市各区还将结合实际,深入重点企业和工业(产业)园区,普及工时和休息休假法律规定,有针对性地开展政策解读,督促企业强化劳动法治观念,引导劳动者正确理解相关法律规定,形成和扩大社会共识。例如,在工时方面,国家实行劳动者每日工作时间不超过八小时,平均每周工作时间不超过四十小时的工时制度;用人单位应当保证劳动者每周至少休息一日。如果存在安排劳动者延长工作时间、休息日安排劳动者工作又不能安排补休、或者法定休假日安排劳动者工作的这些情形,用人单位应当按照相关标准,支付高于劳动者正常工作时间工资的工资报酬。

在休息休假方面,在机关、团体、企业、事业单位、民办非企业单位、有雇工的个体工商户等单位工作的职工,只要连续工作1年以上就可以享受带薪年休假。根据累计工作年限,职工可享受不同天数的年休假,且国家法定休假日、休息日不计入年休假的假期。对职工应休未休的年休假天数,单位应当按照职工日工资收入的300%支付年休假工资报酬。

在本次集中排查中,如果发现用人单位有相关违法行为,人力资源社会保障部门将依法予以行政处理处罚并督促限期整改,严防类似问题发生。

此外,北京市还将持续深化人力资源社会保障部门窗口行风作风建设,畅通12333热线咨询电话、现场举报投诉窗口、邮件、网络等举报投诉渠道,对涉及超时加班问题第一时间受理处置,对经调查确属劳动争议的工时和休息休假问题,做好法规政策解释,引导职工通过劳动争议仲裁或司法渠道解决诉求。

来源:中新经纬APP

收起阅读 »

今年太难了,互联网公司又大批裁员

说实话,今年的开局,依然艰难啊。2022 年,考研分数线涨了很多。学生党不好过。近日,阿里、腾讯裁员集体冲上微博热搜,「阿里裁员」、「腾讯裁员」与「裁员」话题热度升至19万。打工人们也不好过。据内部员工猜测,最高裁员量达30%,多个业务线已初步敲定裁员名单。到...
继续阅读 »

说实话,今年的开局,依然艰难啊。

2022 年,考研分数线涨了很多。

学生党不好过。

近日,阿里、腾讯裁员集体冲上微博热搜,「阿里裁员」、「腾讯裁员」与「裁员」话题热度升至19万。

打工人们也不好过。

pic_f4d4087f.png

据内部员工猜测,最高裁员量达30%,多个业务线已初步敲定裁员名单。

到目前为止,官方并未对此事作出回应。

转自:新智元 | 时光

参考:https://www.zhihu.com/question/518032671

好一个「倒春寒」!

春天来了,但对于风云变幻的互联网大厂而言,却毫无「年年岁岁花相似」的相约浪漫。

这不,全国应届大学生期盼已久的一年一度春招旺季,今年大厂却遇「寒冬」,不仅难以获得心仪offer,在职员工还面临「大裁员」。

pic_5bb65022.png

据传,腾讯 PCG(平台和内容事业群)将裁员 4000 人,CSIG(云与智慧产业事业群)将裁员 2000 人,仅这两个事业群的裁员,就已经达到腾讯总员工数量的 10% 左右。

pic_35948bfa.png

内部群聊截图在网上传出,消息显示,鹅厂这波裁员「零零碎碎我估计裁员10-15%左右」,「每个组抽人杀,或者整个组可能over」,「其实卷过这波又怎么样,我估计还有下一波」。

都说春天的气候如婴儿的脸,说变就变,而今春互联网大厂的春天,却难有「笑脸」,只有遭遇「倒春寒」的难堪。

据传,阿里旗下MMC事业部正计划裁员,多个业务线已经初步敲定裁员名单。

「阿里投资交流群」内部议论纷纷,「阿里裁员30%了」,「整个杭州扛不住」,「真正的断臂求生,看谁的现金熬得过今年」。

pic_544de02d.png

其实,阿里的这个MMC事业部才成立不满1年时间,2021年3月宣布成立,聚焦社区业务,整合了零售通和盒马集市。

在去年4月1日,阿里内网发布全员公开信《MMC,吹响集结号!》,将MMC定位为「服务每家店,只为每个家」。

公开信吹起战斗号角,「创造一个新的商业形态和生活方式是辛苦的,披星戴月将是日常,但青春滚烫,每个人不都在渴望能够全力以赴一次吗?」

pic_41039a30.png

文中提到「新的商业形态和生活方式」,正指向MMC的「近场电商」,MMC定位将已有的小商贩进行数字化改造,通过小店感知社区消费需求,再进行确定性的采购,将货物匹配到人。

与此同时,阿里巴巴下大决心实施这一雄心勃勃的计划,不仅通过招聘网罗人才,仅MMC事业部就发布了1500个岗位,还豪掷重金加码事业,仅2021年计划花200亿元打造社区团购。

相比于去年此时,不到1年时间,简直天壤之别,不禁慨然感叹「岁岁年年人不同」。

为什么裁员?

本来,互联网企业是「心有理想,春暖花开」,这些大厂赶上全球数字经济发展的大好时代,他们自身也成了引领发展的弄潮儿。

而今,面临发展困境,大厂纷纷裁员「减负」,尽管此举并不高明,但也迫不得已。

30年互联网发展浪潮,不仅开启了一代企业家的梦想,也造就了一批青年才俊的IT梦。

2022年《求是》发表习总书记文章《不断做强做优做大我国数字经济》,文章指出,当今时代,数字技术、数字经济是世界科技革命和产业变革的先机,是新一轮国际竞争重点领域,我们一定要抓住先机、抢占未来发展制高点。

那么,中国互联网企业为什么会步入裁员境地?

全球疫情蔓延加剧了各行业的困难,互联网也不例外。

经过30年的迅猛发展,互联网红利期难再超越。

阿里最新股票信息显示,即便是和去年10月并不算高的每股177美元相比,阿里的股票在不足半年时间就已经面临「拦腰斩」,只剩下不到87美元,足足打了个对折。

和将近246美元的高点相比,更是只有1/3多点。

pic_332bafac.png

而今日腾讯的股票价格不足45美元,且近期持续下降,其市值不足半月已跌破5000亿美元大关,只有4300亿美元。

pic_1309b01b.png

互联网行业整体竞争加剧,获取客户成本增加,核心收入开始下降,许多公司难以找到新的突破点,每次尝试的代价往往是裁员。

行业内卷加深,互联网竞争加剧

当越来越多的竞争者进入互联网赛道,互联网行业渗透率和普及率目前处于较高水平,出现僧多粥少,行业内卷加剧。

尤其是近些年传统行业利润率低,造成大量行业涌入互联网,其过度繁荣,也形成过度竞争。

网友怎么看

微博上的网友普遍认为认为,裁员是正常现象。

疫情时期,大厂的日子并不好过,大环境不景气,只能「舍小保大」。

@叶艾茂表示,前年扩张太快,去年开始收缩,互联网大厂陆续都在精简,这个是肉眼可见的。

pic_c96ceebd.png

@长安数码君表示,不光是腾讯和阿里,各行各业都受到了影响,老板们都在感叹「生意不好做」。

pic_865f3fc1.png

@锅盖头司令表示了质疑,这么大规模的裁员不可置信。

pic_fba24c07.png

@李桂江表示,「2022年还是求稳吧,别乱投资、别瞎扩张,有班的就先把班上好,手中多攒点现金。」

pic_5a445bd3.png

另外,也有网友表示,只要是人才,就不怕找不到工作。

不过也不乏对当下大环境的担忧:这次终于轮到大厂了。

时代的一粒灰,落在个人身上就是一座山。

絮叨

好多互联网公司的人,拿着的公司股票都大幅缩水。

今年互联网的校招 hc 也会减少不少,做好准备吧。

接下来,杠杆少加,保证个人手里的现金流,还是蛮重要的。

来源:https://mp.weixin.qq.com/s/OFdHiNXs3iGSLeShnvDc6Q

收起阅读 »

如何从一名“普通码农”成长为技术Leader?

有一个非常有趣的现象:据说大部分的技术管理者,在从程序员转为管理岗位的时候,都是在领导或公司的要求下,被动的推到管理岗位上的,并非是自己当初有强烈意愿、主动去选择管理岗。这种被动的比例还不低,竟然高达 80% 以上,这个现象从我自己身边的同事中也可以感受到。最...
继续阅读 »

有一个非常有趣的现象:据说大部分的技术管理者,在从程序员转为管理岗位的时候,都是在领导或公司的要求下,被动的推到管理岗位上的,并非是自己当初有强烈意愿、主动去选择管理岗。

pic_529f8a5e.png

这种被动的比例还不低,竟然高达 80% 以上,这个现象从我自己身边的同事中也可以感受到。

最近两年我接触到的四五位新晋的技术管理者,全是因为技术/项目做得好,被上级提拔到管理岗,几乎没有人是因为具备了管理技能后主动去选择,其实包括曾经的我也是这样走过来的。

这里,我们不讨论这种普遍现象是否合理,我们先来看看这种晋升方式会带来什么样的结果。

既然有这么多人是「被动」的成为技术管理者的,那可以想象,在这些人刚步入管理岗位的时候,对管理知识的了解会是多么的薄弱,对即将要开展的管理工作会多么的心虚和纠结。

甚至有些人,因为刚开始进行管理工作的不顺利,导致对自己能力的质疑,对技术管理岗位的排斥。

所以这也说明了很多程序员刚晋升为管理后,内心其实是痛并快乐着的。针对这个现象,应该怎么办呢?

这里,我就以「过来人」的工作经验,结合近期读到的「刘建国老师」的一些管理理念,计划从一名新晋的技术管理者角度出发,来聊一聊我们应该怎么走好初入管理岗的这段路,希望能给管理新人们一些启发。

pic_b73afcad.png

我适不适合去做一名技术管理者呢?

很多初入管理岗的同学,可能会有这样一些内心的纠结:

  • 「我没有做过管理,不知道自己能不能做得好?有点胆怯」
  • 「是公司领导安排我做技术管理的,我也不知道自己适不适合?更不知道对自己职业是好还是坏?有点焦虑」
  • 「晋升管理岗会给我带来工资福利和职位的提高,这是我很想要的。但我不知道管理这条路自己是否真的喜欢?有点迷茫」
  • ……

其实对于一名新晋管理者,或者想要步入管理岗的同学来说,有这些纠结和不安也是正常现象。

要解决这些问题,首先你得问问自己的内心:你为什么要去做一名技术管理者,你对管理工作所需的 投入要求/意愿 以及 带来的回报 都清楚了吗?

对管理工作的投入要求/意愿

认可管理工作的价值

我们都知道,在日常的管理中会有很多的「繁琐的」、「协调性」、「打杂的」的工作需要做。

例如:协调资源、跟进项目、管理进度、员工面谈、绩效考评、开会沟通、邮件汇报、研发流程、关注项目和人员问题等等。

这些工作在有的人看来就是打杂,觉得很没有价值,没有写牛逼的代码来得高大上。

而在有的人眼中却非常认可这些工作,觉得能给自己带来多方位的素质提升。那么,在你眼中,你是怎么看待这些工作的呢?

对管理工作发自内心的兴趣

很多管理工作并非一定要你到达管理岗位后才能做的。在你还是一名普通程序员的时候,在你还是团队技术骨干的时候,如果你真的对技术管理有兴趣,那么这些「管理」工作已经在你的日常工作中无形的开始了。

例如:关注项目整体进度、了解项目目标、推进项目流程、关心身边的同事成长、优化研发与协作方式等等。

那么,你是否发自内心的对这些无形中的「管理」工作感兴趣呢?

愿意去提升管理能力

一旦从纯粹的技术岗转到管理岗,你可能需要面临很多管理技巧上的挑战,甚至还有很多在思维和认知上的颠覆。

例如:首先,管理工作已经不再像敲代码一样非 0 即 1 了,管理工作中有很多中间态,不确定的因素,这些往往是对程序员之前习惯性思维的一个很大的冲击。

其次,之前敲代码是与计算机打交道,转为管理之后,会花更多的时间与人打交道,与上司、与平级、与下属、与跨部门协作等等。

另外,管理者会承担更多更大的责任,需带领团队穿山越岭实现公司的最终目标,这些压力也是作为程序员时候所没有的。

你愿意为此方向重构自己,提升自己的管理思维和能力吗?你做好这个准备了吗?

管理工作带来的回报

你拥有了一个团队

步入管理岗之后,你就不是一个人在战斗,你拥有了一个团队,基于团队,你可以做出更大的成就。

以前你的成绩可能就是技术做的好,代码写的好,而转入管理开始带团队之后,你可以和团队一起搞定更复杂的任务,做出更大的成绩。

能力、视野、影响力 都会得到显著提升

除了技术能力,你还获得了管理能力、领导力,你看待问题的视角不再是程序员思维了,会有更高的视野。由于团队间的协作,你还能获得更大的个人影响力。

物质的回报

这是非常现实的,看得见摸得着的回报。

好了,上面已经将一名技术管理者所需的要求和回报都简单捋了捋。作为程序员的你,可以对照一下,然后问问内心的自己是否真的合适。

如果你觉得没有问题,那咱们就继续来看看,一般有那些机会可以帮助我们成长为技术管理者。

pic_c6816426.png

有哪些机会能使我成为一名技术管理者?

首先,「管理比技术更需要机会」,我们做程序员的,都非常勤奋,挑灯熬夜的干活学习都是平常事,而且技术这东西也确实很公平,你不断的努力去研究去学习,迟早会提高一个层次,无非是不同人不同时间的问题。

但是做管理呢,并不是这样。要想成为一名技术管理者,勤奋必不可少,然而其中的机会也很重要。

在职场上,经常有遇到这样的现象:

  • 「你的能力非常不错了,可是团队中没有管理的空缺了」
  • 「你是团队中技术最好的一个,可是管理岗却安排给了别人」
  • ……

可以发现,这里面除了你个人的条件以外,外部的「机会」因素相当重要。

想成为技术管理者,那我们应该抓住那些潜在的机会呢?

  • 快速发展的公司最有机会,这类公司经常会建立新的项目新的团队,需要很多技术管理者。
  • 耐心积攒能力,掌握核心技术的人会更有机会,厚积薄发的道理人人都懂。
  • 手上负责的项目属于基础性、全局性、跨部门协作工作多的业务相对来说机会会多一些。
  • 在平时的工作中,经常得到上级认可、甚至上级能支持你转管理,这类人等待的就是一个契机。
  • 身边有管理能力较好的导师朋友来解惑帮助的人也会更容易把握机会。

最后就是,当你还不是管理岗,但你却已经在团队中做着技术管理者应该做的事情的时候,你最有机会。

在互联网公司中,很多管理岗的晋升不是给予的,更多是对既定事实的追认。

pic_4534cf70.png

技术和管理应该怎么去平衡?

从一名程序员晋级为技术管理者之后,很多人的内心多多少少都存在这样一些顾虑:

  • 「每天管理的工作越来越多,留给自己研究技术的时间却越来越少,时间一长,我会不会慢慢脱离技术了」
  • 「写代码的时间变少了,对很多技术细节也没有以前敏感了,感觉自己离技术老本行越来越远,内心越来越发虚」
  • 「脱离了一线编码,心里空落落的,很担心自己的职业发展」
  • ……

其实有这些顾虑也无妨,这也是大多数新晋技术管理者都会遇到的问题。但是,我们来想想,为什么这些问题在新管理者面前这么普遍呢?

主要原因还是因为新晋的技术管理者大多都是程序员出身,一直以来都是靠一线的编码技术能力去打江山混名声的。

突然之间转为管理了,既担心把「技术」丢了没了退路,又对「管理」应该要做哪些事情、如何把「管理」做好,如何重新依靠「管理」这项能力去打江山混江湖还不熟练。

正处于青黄不接的时期,自然而然就会觉得焦虑不安了。那这些顾虑有解吗?有的。

要明白「放弃编码,不代表放弃技术」

转做技术管理之后,我们只是减少了编码的时间,并不是放弃了技术,事实上,作为一名技术人,我们永远永远也不能放弃技术。

但也千万不要把「编码能力」与「技术能力」之间划上等号。技术能力是可以更多的关注应用,但并不一定需要时时关注实现细节。

就像部队打仗一样,作战指挥官需要了解陆军、空军、海军等不同军种的优劣势,需要了解军舰、坦克、导弹等不同作战武器的最佳特性,才能部署出最佳的作战方针,统筹全局打胜仗。

但是他并不需要了解军舰具体怎么开、坦克具体怎么驾驶。另外,当你还是一名程序员的时候,编码可能就是你的全部实现。

而当你成为一名技术管理者的时候,技术就应该是你的工具,你应该站在更高的视野去看待技术的价值,技术是为最终的目标而服务。

要保持对技术的评估能力

上面提到了「技术能力」并不等于「编码能力」,抛开一些非核心能力的话,可以简单点理解为「技术能力」=「编码能力」+「技术评估能力」。

当我们还是程序员的时候编码能力是我们最为注重的,但当我们转技术管理之后,技术评估能力就应该成为我们的重点,编码能力在精力有限的情况下是可以放弃的。

技术评估能力主要是指我们通过自己的技术认知,去评估一个项目/开发任务要不要做、值不值得做、做到什么程度,技术方案边界在哪儿、技术选型用什么、可用性/拓展性方案是什么等等,甚至是对团队人员技术水平的边界评估。

pic_9fc1859e.png

怎样才能保持技术评估能力,以及怎样能不断增长自己的技术评估水平呢?作为技术管理者而言,很明显,已经不能通过大量编码的方式去提高技术能力了。

只能依赖于:自己以往技术经验的积累、团队的技术分享、技术调研、与同行专家交流、培训学习等方式。这些方式有的时候会比编码的方式更快更有效率。

技术管理是多样性的,你总会找到一条你自己的路

我们要明白,技术管理并没有固定的模式,有的技术老大做着做着就往商业方向靠了,比如雷军这类。

有的技术老大无论做到多高的级别,带几百上千人的团队,却依旧非常关注技术日常。每个人的技术管理风格不同,但最后都会找到一条自己风格的管理之路。

即使最后你发现自己不喜欢做管理了,想转回做技术架构师或创业,你通过管理获得的这些经验能力和视野,对你的其他道路依旧会有莫大的帮助。

技术管理能力是每一个程序员都需要的技能

技术管理是一项能力,并不是一个职业。它是每一个技术同学在成长过程中,都应该去学习和具备的能力。

无论你以后是走管理道路,还是做职业经理人、技术专家、架构师、创业,你都需要具备技术管理者应具备的团队管理能力、技术视野、技术规划能力、项目管理能力、沟通协调能力。

因此,你还需要有顾虑吗?反正无论如何你都得会一点嘛。

以上,就是对新晋的技术管理者如何解决初入管理岗时纠结心路的学习与分享,希望能给新步入管理岗的同学们一些启发。

来源:51CTO技术栈  https://mp.weixin.qq.com/s/PU18nj59xUPO-GySR3--MQ

收起阅读 »

互联网行业的常用黑话,你知道几条?

身为一名新时代的互联网工作人员,怎么能对这个行业的黑话一无所知呢?下面我给大家整理了互联网行业的基本“黑话”,看看你知道几条。一、互联网人知名大厂别称1、熊厂、狼厂、蓝厂——百度。百度的Logo是一个蓝色的熊爪子,所以蓝厂和熊厂的名字是这样来的,但是相比来说,...
继续阅读 »

身为一名新时代的互联网工作人员,怎么能对这个行业的黑话一无所知呢?下面我给大家整理了互联网行业的基本“黑话”,看看你知道几条。

一、互联网人知名大厂别称

1、熊厂、狼厂、蓝厂——百度。百度的Logo是一个蓝色的熊爪子,所以蓝厂和熊厂的名字是这样来的,但是相比来说,百度更多的时候被叫做狼厂,起源于百度CEO李彦宏给百度员工的一封公开信:《鼓励狼性淘汰小资》,引起广泛的讨论,之后百度有员工就开始称百度为「狼厂」。

2、猫厂、东厂、西厂——阿里巴巴阿里巴巴是因为旗下天猫的Logo而演变来的,所以叫做猫厂。除此之外在浙江杭州有东西两处办公地点,被内部员工称为东厂和西厂。

3、鹅厂——腾讯。腾讯名字的原由就比较简单了,因为腾讯的Logo是一只企鹅,企鹅也是鹅。

4、渣浪——新浪。新浪人称渣浪人人都知道,当然这么渣的名字就不是自称得来的了,而是A站和B站网友对新浪视频的称号,起因是up主使用外链投稿曾多次被新浪审核但又无故删除,使得UP主们抓狂,从此就有了“战渣浪”的定义。

5、猪厂——网易。这个名字内部人基本不用,也是属于外面人叫得比较多一点,起源是网易CEO丁磊在之前养过一段时间的猪,所以就这样被传开了。

6、狐厂——搜狐搜狐的吉祥物是一只红色大尾巴的小狐狸,所以被叫做狐厂,不过对搜狐的人使用花名的时间并不多。

7、狗厂——京东。为了和天猫打一场硬架,2013年3月30日,京东高调地更换域名、logo及VI系统,随之,一只名为“Joy”的金属小狗也空降互联网,于是在电商领域,阿里巴巴和京东的交战也可以被称为“猫狗大战”。

8、绿厂、数字公司——奇虎360。由来:绿厂名字的带来是360的Logo颜色,不过OPPO也是绿色的Logo所以也被叫做绿厂,但是360更加出名的花名是数字公司,因为360嘛。

9、杂粮、粗粮、粮厂——小米。由来:杂粮名字的由来是360周鸿祎2012年和雷军在微博上打口水战而来,而后又被传播为粗粮和粮厂。

二、招聘黑话

1、能承受较大的工作压力——加班

2、抗压能力强——加班+替别人扛雷

3、工作有激情——自觉加班还要特美

4、有强烈责任心——没做完不准走

5、弹性工作制——加班不给加班费

6、弹性工作制,但不保证准时下班——做完了才准走

7、包三餐——早晚都得加班

8、双休——工作日加班

9、薪资+社保+带薪休假+职位晋升——是个正经公司都有,没什么拿得出手的福利

10、适应较快的工作节奏——加班把三天的工作两天做完

11、公司提供水果——貌似也就这一个福利

12、有强烈的上进心——干完工作就加班去干其他工作

13、喜欢有挑战性的工作——加班、前人留的坑不少

14、不提倡加班——你懂的(该加还得加,加班是因为你工作效率低,不是安排工作多)

15、不强制加班——你懂的 (不做完额外安排的工作你走一个试试!)

16、上不封顶——下不保底

17、偶尔会忙——以后忙了你别抱怨,提醒过了

18、团队氛围很好——大家经常一起加班,一起吃加班餐,聊聊工作,多happy

19、上升空间大——工资低,3000元涨个50%不也就4500吗?

20、领导安排的其它任务——我叫你干啥你就得干啥

21、妹子多——这个屌丝虽然脑子不太好使但便宜,看看这个理由能不能骗一蛤

22、有期权——没多余的现金发工资给你

23、有股权——工资微妙地低于你应得的数,反正我不信你能干满拿走

24、年底双薪——13 薪

25、13 薪起——别想了,就是13 薪

26、年底有奖金——年薪大于 12 薪小于 13 薪

27、我们 6 点准时下班 -——入职才知道最严重的早晨 6 点下班,回家吃早饭睡觉。

28、扁平化管理 -——领导和你坐一屋,盯着你干活

29、核心团队来自 BAT ——嗯,你不是BAT的,所以你不是核心

30、"我把你的简历整理一下"——对方说这个的话基本可以判断他是卖人头外派的了,把你的简历给他的甲方。

31、弹性工作时间——只弹下班(下班时间不固定),不弹上班

32、能独立完成任务 ——前端后端或产品测试推广运营全都你一个人干

33、领导好——看你顺眼就好,不顺眼就 XX ;你有生之年不可能有晋升的空间

34、XXX 比钱重要——钱达不到你的要求

35、公司会给你培训,但是工作之后你要交培训费——麻痹就是培训班

36、BAT 薪资——略微高于本地市场价

37、帮员工避税——按最低工资给你交社保公积金(五险一金)

38、我们是创业公司——有不少坑要填,另请做好加班拼命的准备

39、老板 /负责人不在,稍后会联系你的——面试不合格,不要抱期望了

40、期权激励拿到手软——希望能弥补你看到基本工资后的脚软

41、专注移动互联网,拥有几亿活跃用户——就注册了个微信公众号

42、有活力的技术团队——团队平均工作经验<1年

43、创始团队全部来自BAT——在淘宝干过客服、自己玩过微信公众号、还在百度实过习

44、千亿市场的探索者——目前尚没看清具体市场在哪

45、扁平化管理,高度自由——全公司人数,一只手就数的过来。

46、典型欧美创业工作环境——办公室现处于毛坯房状态

47、新技术+新方向+新团队——嗯,目前这三样都没有

48、直进核心团队——公司尚未设置非核心团队岗

49、全方位成长机会——你有很大机会成为外卖超人

50、有机会晋升技术合伙人 ——现在就缺一个程序员来码代码了!

51、提供各种福利——每样细说咱就伤感情了

52、福利完善,待遇从优——严格按照法定节假日上班和……上班。

53、有完善的员工期权激励措施——所以对工资要求别太高。

54、提供住宿、班车及两餐——每周提供数小时时间与家人团聚

55、底薪+岗位绩效+职称奖+管理绩效+提成+五险 ——和在一块您看看够不56、够付这个月房租?

57、加入我们,给你足够大的发展空间!——目前公司规模<10人

58、女性员工占一半以上——创业公司的员工性别结构为:男生、女汉子以及别人的女友。

59、深受资本追捧,行业方向被投资人看好——又有几个同行拿到融资了,而他们还在追着投资人跑

60、大牛云集——我司属牛的同事比较多

61、与互联网大咖面对面,有巨大成长空间——加了个混充有某某大佬的微信群,常年潜水从不说话,好友更不加。

62、公司计划短期内上市——您也知道计划一般赶不上变化

63、徘徊在牛A和牛C之间一群人!——永不止步,从来没在牛B上停驻过

三、 老板黑话

1、你来我办公室一下 = 老子又想到了绝妙的idea

2、得专注用户体验 = 界面画的好看点

3、产品气质不够年轻 = 饱和度通通调最高

4、产品气质不够成熟 = 界面通通做成黑的

5、产品不够大气 = 我也不知道哪不好反正就是不好

6、要精致的感觉 = 抄苹果

7、要利用用户的固有习惯 = 抄同行

8、要追求流行设计趋势 = 抄微信/淘宝/滴滴…

9、你说的这是另一个问题=我说的才是对的

10、你说的跟我是一个意思=我说的才是对的

11、我们讨论的是两件事情=我说的才是对的

12、我们是弹性工作制 = 加班不给钱

13、我们是扁平化管理 = 公司没几个人

14、我们会给你很多期权 = 不会给你很多工资

15、我们每天都有果盘! = 可能是公司唯一的福利了

16、当务之急是抢占市场 = 快狂发补贴

17、快速建立用户群体的壁垒 = 快拉他们进微信群

18、要让用户产生自发传播 = 快让他们转发朋友圈

19、为了健康发展我们要启动下轮融资 = 公司没钱了

20、我们辞退了一些跟不上公司发展的同事 = 公司没钱了

21、打补贴战其实不符合我们公司的理念 = 公司没钱了

四、 产品经理黑话

1、产品设计应该大道至简 = 复杂的我也不会

2、用户都应该用完即走 = 回不回来我不管

3、这是常规的典型的做法 = 我抄的微信/淘宝/滴滴…

4、需求要抓住人性 = 多放美女照片

5、得考虑用户的使用场景 = 加个夜间模式

6、商业模式要形成闭环 = 放东西线上卖

7、要搭建完善的用户运营体系 = 做个积分商城

8、要有社交元素促进活跃度 = 塞一个IM

9、没有用户是因为没有做好运营 = 不是我的锅

10、体验不好是因为技术实现问题 = 不是我的锅

11、界面难看是因为设计水平不行 = 反正都不是我的锅

12、这个我回去再确认一下 = 别说了,老子没想到

13、在吗?= 要改需求了

14、哥哥最近累吗要不要喝奶茶 = 要改需求了

15、那个…有句话…不知… = 要改需求了

16、下次肯定不改了 = 这次先改了再说

17、你的建议很好我们已经想到了 = 我擦说得真有道理赶紧提需求

18、你的建议我们会考虑的 = 这建议好像有点蠢

19、你的建议很有启发性 = 哈哈哈什么玩意儿

三、程序员的职场行话

1、那个bug没问题啊,你再试试——刚偷偷改完这个bug

2、下个版本再做吧——根本就不想做

3、正在改——忘了有这回事了

4、需求太不合理——这逻辑不好做

5、别人家的实现方式不一样——我不会做

6、产品逻辑不对——傻X,还不如我上

7、最近老加班——老板该加工资了

8、我回去评估一下技术难度——先拖两天

9、你这个需求不清晰——我不想做

10、你确定有这个需求吗?——做出来没人用老子跟你拼了

11、下次肯定不延期了——先应付了这次再说

12、你试过……——到底会不会用我的程序啊

13、我测试没问题啊!——到底会不会用我的程序啊

14、我的时间排满了——我不想做

15、我有优先级更高的任务——我不想做

16、我今晚有事——我今天不想加班

17、我在调试程序——我没时间理你

18、你怎么还在自学Python啊?——PHP才是最好的语言

19、你怎么还用 Word 啊?——Markdown 才是最好的写作工具

20、你怎么还在用 ThinkPad 啊?——Mac 才是最好的电脑

来源:https://zhuanlan.zhihu.com/p/70495913

收起阅读 »

最完整的Explain总结,SQL优化不再困难!

两个变种会在 explain 的基础上额外提供一些查询优化的信息。一般是使用了覆盖索引(索引包含了所有查询的字段)。对于innodb来说,如果是辅助索引性能会有不少提高mysql> explain select film_id from film_act...
继续阅读 »

在 select 语句之前增加 explain 关键字,MySQL 会在查询上设置一个标记,执行查询时,会返回执行计划的信息,而不是执行这条SQL(如果 from 中包含子查询,仍会执行该子查询,将结果放入临时表中)

CREATE TABLE `film` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


CREATE TABLE `actor` (
`id` int(11) NOT NULL,
`name` varchar(45) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


CREATE TABLE `film_actor` (
`id` int(11) NOT NULL,
`film_id` int(11) NOT NULL,
`actor_id` int(11) NOT NULL,
`remark` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_film_actor_id` (`film_id`,`actor_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

两个变种

explain extended

会在 explain 的基础上额外提供一些查询优化的信息。

紧随其后通过 show warnings 命令可以 得到优化后的查询语句,从而看出优化器优化了什么。

额外还有 filtered 列,是一个半分比的值,rows * filtered/100 可以估算出将要和 explain 中前一个表进行连接的行数(前一个表指 explain 中的id值比当前表id值小的表)

mysql> explain extended select * from film where id = 1;


mysql> show warnings;


explain partitions

相比 explain 多了个 partitions 字段,如果查询是基于分区表的话,会显示查询将访问的分区。

id列

id列的编号是 select 的序列号,有几个 select 就有几个id,并且id的顺序是按 select 出现的顺序增长的。


一般是使用了覆盖索引(索引包含了所有查询的字段)。对于innodb来说,如果是辅助索引性能会有不少提高

mysql> explain select film_id from film_actor where film_id = 1;

Using where

查询的列未被索引覆盖,where筛选条件非索引的前导列

mysql> explain select * from actor where name = 'a';

Using where Using index

查询的列被索引覆盖,并且where筛选条件是索引列之一但是不是索引的前导列,意味着无法直接通过索引查找来查询到符合条件的数据

mysql> explain select film_id from film_actor where actor_id = 1;

NULL

查询的列未被索引覆盖,并且where筛选条件是索引的前导列,意味着用到了索引,但是部分字段未被索引覆盖,必须通过“回表”来实现,不是纯粹地用到了索引,也不是完全没用到索引

mysql>explain select * from film_actor where film_id = 1;

Using index condition

与Using where类似,查询的列不完全被索引覆盖,where条件中是一个前导列的范围;

mysql> explain select * from film_actor where film_id > 1;

Using temporary

mysql需要创建一张临时表来处理查询。出现这种情况一般是要进行优化的,首先是想到用索引来优化。

  1. actor.name没有索引,此时创建了张临时表来distinct

mysql> explain select distinct name from actor;
  1. film.name建立了idx_name索引,此时查询时extra是using index,没有用临时表

mysql> explain select distinct name from film;

Using filesort

mysql 会对结果使用一个外部索引排序,而不是按索引次序从表里读取行。

此时mysql会根据联接类型浏览所有符合条件的记录,并保存排序关键字和行指针,然后排序关键字并按顺序检索行信息。

这种情况下一般也是要考虑使用索引来优化的。

  1. actor.name未创建索引,会浏览actor整个表,保存排序关键字name和对应的id,然后排序name并检索行记录。

mysql> explain select * from actor order by name;
  1. film.name建立了idx_name索引,此时查询时extra是using index

mysql> explain select * from film order by name;

作者:程序员段飞
来源:https://juejin.cn/post/7074030240904773645

收起阅读 »

Flutter真香,我用它写了个桌面版JSON解析工具

Flutter支持稳定的桌面设备开发已经一段时间了,不得不说,Flutter多平台支持的特性真的很香。我本人并没有任何桌面开发的经验,但仍然使用Flutter开发出了一个桌面版小程序,功能很简单,就是对输入的json做格式化处理和转模型。话不多说,先来看看实际...
继续阅读 »

Flutter支持稳定的桌面设备开发已经一段时间了,不得不说,Flutter多平台支持的特性真的很香。我本人并没有任何桌面开发的经验,但仍然使用Flutter开发出了一个桌面版小程序,功能很简单,就是对输入的json做格式化处理和转模型。

话不多说,先来看看实际效果。项目源码地址


开发环境如下:

Flutter: 2.8.1

Dart: 2.15.1

IDE: VSCode

JSON作为我们日常开发工作中经常要打交道的一种数据格式,它共有6种数据类型:null, num, string, object, array, bool。我们势必对它又爱又恨。爱他因为他作为数据处理的一种格式确实非常方便简洁。但是在我们做Flutter开发中,又需要接触到json解析时,就会感觉非常棘手,因为flutter没有反射,导致json转模型这块需要手写那繁杂的映射关系。就像下面这样子。

void fromJson(Map<String, dynamic>? json) {
if (json == null) return;
age = json['age'];
name = json['name'] ?? '';
}

数据量少还能接受,一旦量大,那么光手写这个解析方法都能让你怀疑人生。更何况手写还有出错的可能。好在官方有个工具json_serializable可以自动生成这块转换代码,也解决了flutter界json转模型的空缺。当然,业界也有专门解析json的网站,可以自动生成dart代码,使用者在生成后复制进项目中即可,也是非常方便的。

本项目以json解析为切入点,和大家一起来看下flutter是如何开发桌面应用的。

1、创建项目

要让我们的flutter项目支持桌面设备。我们首先需要修改下flutter的设置。如下,让我们的项目支持windowsmacos系统。

flutter config --enable-windows-desktop
flutter config --enable-macos-desktop

接下来使用flutter create命令创建我们的模版工程。

flutter create -t app --platforms macos,windows  hello_desktop

创建完项目后,我们就可以run起来了。

2、功能介绍

先来看下整体界面,界面四块,分别为功能模块、文件选择模块、输入模块、输出模块。


这里自动修正的功能能帮助我们将异常的格式不正确的json转为正确的格式,不过处于开发阶段,可以不必理会。

3、关键技术点&难点记录:

1、控制窗口Window

我们在新建一个桌面应用时,默认的模版又一个Appbar,此时应用可以用鼠标拖拽移动,放大缩小,还可以缩到很小。但是,我们一旦去掉这个导航栏,那么窗口就不能用鼠标拖动了,并且我们往往不希望用户将我们的窗口缩放的很小,这会导致页面异常,一些重要信息都展示不全。因此这里需要借助第三方组件bitsdojo_window。通过bitsdojo_window,我们可以实现窗口的定制化,拖动,最小尺寸,最大尺寸,窗口边框,窗口顶部放大、缩小、关闭的按钮等。

2、鼠标移动捕捉

通过InkWell组件,可以捕捉到手势、鼠标、触控笔的移动和停留位置

tip = InkWell(
   child: tip,
   hoverColor: Colors.white,
   highlightColor: Colors.white,
   splashColor: Colors.white,
   onHover: (value) {
     bool needChangeState = widget.showTip != value;
     if (needChangeState) {
       if (value) {
         // 鼠标在tip上,显示提示
         showTip(context, PointerHoverEvent());
       } else {
         overlay?.remove();
       }
     }
     widget.showTip = value;
   },
   onTap: () {},
 );

3、鼠标停在指定文字上时显示提示框,移开鼠标时隐藏提示框

这个功能是鼠标移动后的UI交互界面。要在窗口上显示一个提示框,可以使用Overlay。需要注意的是,由于在Overlay上的text的根结点不是Material风格的组件,因此会出现黄色的下划线。因此一定要用Material包一下text。并且你必须给创建的OverlayEntry一个位置,否则它将全屏显示。

Widget entry = const Text(
     '自动修复指输入的JSON格式不正确时,工具将根据正确的JSON格式自动为其补其确实内容。如“”、{}、:等',
     style: TextStyle(
       fontSize: 14,
       color: Colors.black
     ),
     );

entry = Material(
     child: entry,
   );

// ... 其他代码
OverlayEntry overlay = OverlayEntry(
         builder: (_) {
           return entry;
         },
       );
      Overlay.of(context)?.insert(overlay);

      this.overlay = overlay;
 }

4、读取鼠标拖拽的文件

读取说表拖拽的文件一开始想尝试使用InkWell组件,但是这个组件无法识别拖拽中的鼠标,并且也无法从中拿到文件信息。因此放弃。后来从文章《Flutter-2天写个桌面端APP》中发现一个可读取拖拽文件的组件desktop_drop ,能满足要求。

5、本地文件选取

使用开源组件file_picker ,选完图片后的操作和拖拽选择图片后的操作一致。

6、TextField显示富文本

Textfield如果要显示富文本,那么需要自定义TextEditingController。并重写buildTextSpan方法。

class RichTextEditingController extends TextEditingController {

// ...

@override
 TextSpan buildTextSpan(
    {required BuildContext context,
     TextStyle? style,
     required bool withComposing}) {
   if (highlight) {
     TextSpan text;
     String? input = OutputManager().inputJSON;
     text = _serializer.formatRich(input) ?? const TextSpan();
     return text;
  }
   String json = value.text;
   return TextSpan(text: json, style: style);
}
}

7、导出文件报错

在做导出功能时遇到下列报错,保存提示为没有权限访问对应目录下的文件。

flutter: path= /Users/zl/Library/Containers/com.example.jsonFormat/Data/Downloads
[ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: FileSystemException: Cannot open file, path = '/Users/zl/Library/Containers/com.example.jsonFormat/Data/Downloads/my_format_json.json' (OS Error: Operation not permitted, errno = 1)

通过Apple的开发文档找到有关权限问题的说明。其中有个授权私钥的key为com.apple.security.files.downloads.read-write ,表示对用户的下载文件夹的读/写访问权限。那么,使用Xcode打开Flutter项目中的mac应用,修改工程目录下的DebugProfile.entitlements文件,向entitlements文件中添加com.apple.security.files.downloads.read-write,并将值设置为YES,保存后重启Flutter项目。发现已经可以向下载目录中读写文件了。

当然,这是正常操作。还有个骚操作就是关闭系统的沙盒机制。将entitlements文件的App Sandbox设置为NO。这样我们就可以访问任意路径了。当然关闭应用的沙盒也就相当于关闭了应用的防护机制,因此这个选项慎用。


TODO List:

  • json自动修正

  • 模型代码高亮

  • 自定义导出路径

参考文档:

Flutter桌面支持

Flutter desktop support

Flutter-2天写个桌面端APP

pub.dev-window

Flutter Desktop - bitsdojo-window - bilibili

Apple开发权限文档

作者:ijinfeng
来源:https://juejin.cn/post/7069689952459554830

收起阅读 »

优秀的后端应该有哪些开发习惯?

前言毕业快三年了,前后也待过几家公司,碰到各种各样的同事。见识过各种各样的代码,优秀的、垃圾的、不堪入目的、看了想跑路的等等,所以这篇文章记录一下一个优秀的后端 Java 开发应该有哪些好的开发习惯。拆分合理的目录结构受传统的 MVC 模式影响,传统做法大多是...
继续阅读 »

前言

毕业快三年了,前后也待过几家公司,碰到各种各样的同事。见识过各种各样的代码,优秀的、垃圾的、不堪入目的、看了想跑路的等等,所以这篇文章记录一下一个优秀的后端 Java 开发应该有哪些好的开发习惯。

拆分合理的目录结构

受传统的 MVC 模式影响,传统做法大多是几个固定的文件夹 controller、service、mapper、entity,然后无限制添加,到最后你就会发现一个 service 文件夹下面有几十上百个 Service 类,根本没法分清业务模块。正确的做法是在写 service 上层新建一个 modules 文件夹,在 moudles 文件夹下根据不同业务建立不同的包,在这些包下面写具体的 service、controller、entity、enums 包或者继续拆分。



等以后开发版本迭代,如果某个包可以继续拆领域就继续往下拆,可以很清楚的一览项目业务模块。后续拆微服务也简单。

封装方法形参

当你的方法形参过多时请封装一个对象出来...... 下面是一个反面教材,谁特么教你这样写代码的!

public void updateCustomerDeviceAndInstallInfo(long customerId, String channelKey,
                  String androidId, String imei, String gaId,
                  String gcmPushToken, String instanceId) {}

写个对象出来

public class CustomerDeviceRequest {
  private Long customerId;
  //省略属性......
}

为什么要这么写?比如你这方法是用来查询的,万一以后加个查询条件是不是要修改方法?每次加每次都要改方法参数列表。封装个对象,以后无论加多少查询条件都只需要在对象里面加字段就行。而且关键是看起来代码也很舒服啊!

封装业务逻辑

如果你看过“屎山”你就会有深刻的感触,这特么一个方法能写几千行代码,还无任何规则可言......往往负责的人会说,这个业务太复杂,没有办法改善,实际上这都是懒的借口。不管业务再复杂,我们都能够用合理的设计、封装去提升代码可读性。下面贴两段高级开发(假装自己是高级开发)写的代码

@Transactional
public ChildOrder submit(Long orderId, OrderSubmitRequest.Shop shop) {
  ChildOrder childOrder = this.generateOrder(shop);
  childOrder.setOrderId(orderId);
  //订单来源 APP/微信小程序
  childOrder.setSource(userService.getOrderSource());
  // 校验优惠券
  orderAdjustmentService.validate(shop.getOrderAdjustments());
  // 订单商品
  orderProductService.add(childOrder, shop);
  // 订单附件
  orderAnnexService.add(childOrder.getId(), shop.getOrderAnnexes());
  // 处理订单地址信息
  processAddress(childOrder, shop);
  // 最后插入订单
  childOrderMapper.insert(childOrder);
  this.updateSkuInventory(shop, childOrder);
  // 发送订单创建事件
  applicationEventPublisher.publishEvent(new ChildOrderCreatedEvent(this, shop, childOrder));
  return childOrder;
}
@Transactional
public void clearBills(Long customerId) {
  // 获取清算需要的账单、deposit等信息
  ClearContext context = getClearContext(customerId);
  // 校验金额合法
  checkAmount(context);
  // 判断是否可用优惠券,返回可抵扣金额
  CouponDeductibleResponse deductibleResponse = couponDeducted(context);
  // 清算所有账单
  DepositClearResponse response = clearBills(context);
  // 更新 l_pay_deposit
  lPayDepositService.clear(context.getDeposit(), response);
  // 发送还款对账消息
  repaymentService.sendVerifyBillMessage(customerId, context.getDeposit(), EventName.DEPOSIT_SUCCEED_FLOW_REMINDER);
  // 更新账户余额
  accountService.clear(context, response);
  // 处理清算的优惠券,被用掉或者解绑
  couponService.clear(deductibleResponse);
  // 保存券抵扣记录
  clearCouponDeductService.add(context, deductibleResponse);
}

这段两代码里面其实业务很复杂,内部估计保守干了五万件事情,但是不同水平的人写出来就完全不同,不得不赞一下这个注释,这个业务的拆分和方法的封装。一个大业务里面有多个小业务,不同的业务调用不同的 service 方法即可,后续接手的人即使没有流程图等相关文档也能快速理解这里的业务,而很多初级开发写出来的业务方法就是上一行代码是 A 业务的,下一行代码是 B业务的,在下面一行代码又是 A 业务的,业务调用之间还嵌套这一堆单元逻辑,显得非常混乱,代码还多。

判断集合类型不为空的正确方式

很多人喜欢写这样的代码去判断集合

if (list == null || list.size() == 0) {
return null;
}

当然你硬要这么写也没什么问题......但是不觉得难受么,现在框架中随便一个 jar 包都有集合工具类,比如 org.springframework.util.CollectionUtilscom.baomidou.mybatisplus.core.toolkit.CollectionUtils 。 以后请这么写

if (CollectionUtils.isEmpty(list) || CollectionUtils.isNotEmpty(list)) {
return null;
}

集合类型返回值不要 return null

当你的业务方法返回值是集合类型时,请不要返回 null,正确的操作是返回一个空集合。你看 mybatis 的列表查询,如果没查询到元素返回的就是一个空集合,而不是 null。否则调用方得去做 NULL 判断,多数场景下对于对象也是如此。

映射数据库的属性尽量不要用基本类型

我们都知道 int/long 等基本数据类型作为成员变量默认值是 0。现在流行使用 mybatisplus 、mybatis 等 ORM 框架,在进行插入或者更新的时候很容易会带着默认值插入更新到数据库。我特么真想砍了之前的开发,重构的项目里面实体类里面全都是基本数据类型。当场裂开......

封装判断条件

public void method(LoanAppEntity loanAppEntity, long operatorId) {
if (LoanAppEntity.LoanAppStatus.OVERDUE != loanAppEntity.getStatus()
        && LoanAppEntity.LoanAppStatus.CURRENT != loanAppEntity.getStatus()
        && LoanAppEntity.LoanAppStatus.GRACE_PERIOD != loanAppEntity.getStatus()) {
  //...
  return;
}

这段代码的可读性很差,这 if 里面谁知道干啥的?我们用面向对象的思想去给 loanApp 这个对象里面封装个方法不就行了么?

public void method(LoanAppEntity loan, long operatorId) {
if (!loan.finished()) {
  //...
  return;
}

LoanApp 这个类中封装一个方法,简单来说就是这个逻辑判断细节不该出现在业务方法中。

/**
* 贷款单是否完成
*/
public boolean finished() {
return LoanAppEntity.LoanAppStatus.OVERDUE != this.getStatus()
        && LoanAppEntity.LoanAppStatus.CURRENT != this.getStatus()
        && LoanAppEntity.LoanAppStatus.GRACE_PERIOD != this.getStatus();
}

控制方法复杂度

推荐一款 IDEA 插件 CodeMetrics ,它能显示出方法的复杂度,它是对方法中的表达式进行计算,布尔表达式,if/else 分支,循环等。


点击可以查看哪些代码增加了方法的复杂度,可以适当进行参考,毕竟我们通常写的是业务代码,在保证正常工作的前提下最重要的是要让别人能够快速看懂。当你的方法复杂度超过 10 就要考虑是否可以优化了。

使用 @ConfigurationProperties 代替 @Value

之前居然还看到有文章推荐使用 @Value 比 @ConfigurationProperties 好用的,吐了,别误人子弟。列举一下 @ConfigurationProperties 的好处。

  • 在项目 application.yml 配置文件中按住 ctrl + 鼠标左键点击配置属性可以快速导航到配置类。写配置时也能自动补全、联想到注释。需要额外引入一个依赖 org.springframework.boot:spring-boot-configuration-processor


  • @ConfigurationProperties 支持 NACOS 配置自动刷新,使用 @Value 需要在 BEAN 上面使用 @RefreshScope 注解才能实现自动刷新

  • @ConfigurationProperties 可以结合 Validation 校验,@NotNull、@Length 等注解,如果配置校验没通过程序将启动不起来,及早的发现生产丢失配置等问题。

  • @ConfigurationProperties 可以注入多个属性,@Value 只能一个一个写

  • @ConfigurationProperties 可以支持复杂类型,无论嵌套多少层,都可以正确映射成对象

相比之下我不明白为什么那么多人不愿意接受新的东西,裂开......你可以看下所有的 springboot-starter 里面用的都是 @ConfigurationProperties 来接配置属性。

推荐使用 lombok

当然这是一个有争议的问题,我的习惯是使用它省去 getter、setter、toString 等等。

不要在 AService 调用 BMapper

我们一定要遵循从 AService -> BService -> BMapper,如果每个 Service 都能直接调用其他的 Mapper,那特么还要其他 Service 干嘛?老项目还有从 controller 调用 mapper 的,把控制器当 service 来处理了。。。

尽量少写工具类

为什么说要少写工具类,因为你写的大部分工具类,在你无形中引入的 jar 包里面就有,String 的,Assert 断言的,IO 上传文件,拷贝流的,Bigdecimal 的等等。自己写容易错还要加载多余的类。

不要包裹 OpenFeign 接口返回值

搞不懂为什么那么多人喜欢把接口的返回值用 Response 包装起来......加个 code、message、success 字段,然后每次调用方就变成这样

CouponCommonResult bindResult = couponApi.useCoupon(request.getCustomerId(), order.getLoanId(), coupon.getCode());
if (Objects.isNull(bindResult) || !bindResult.getResult()) {
throw new AppException(CouponErrorCode.ERR_REC_COUPON_USED_FAILED);
}

这样就相当于

  1. 在 coupon-api 抛出异常

  2. 在 coupon-api 拦截异常,修改 Response.code

  3. 在调用方判断 response.code 如果是 FAIELD 再把异常抛出去......

你直接在服务提供方抛异常不就行了么。。。而且这样一包装 HTTP 请求永远都是 200,没法做重试和监控。当然这个问题涉及到接口响应体该如何设计,目前网上大多是三种流派

  • 接口响应状态一律 200

  • 接口响应状态遵从HTTP真实状态

  • 佛系开发,领导怎么说就怎么做

不接受反驳,我推荐使用 HTTP 标准状态。特定场景包括参数校验失败等一律使用 400 给前端弹 toast。下篇文章会阐述一律 200 的坏处。

写有意义的方法注释

这种注释你写出来是怕后面接手的人瞎么......

/**
* 请求电话验证
*
* @param credentialNum
* @param callback
* @param param
* @return PhoneVerifyResult
*/

要么就别写,要么就在后面加上描述......写这样的注释被 IDEA 报一堆警告看着蛋疼

和前端交互的 DTO 对象命名

什么 VO、BO、DTO、PO 我倒真是觉得没有那么大必要分那么详细,至少我们在和前端交互的时候类名要起的合适,不要直接用映射数据库的类返回给前端,这会返回很多不必要的信息,如果有敏感信息还要特殊处理。

推荐的做法是接受前端请求的类定义为 XxxRequest,响应的定义为 XxxResponse。以订单为例:接受保存更新订单信息的实体类可以定义为 OrderRequest,订单查询响应定义为 OrderResponse,订单的查询条件请求定义为 OrderQueryRequest

尽量别让 IDEA 报警

我是很反感看到 IDEA 代码窗口一串警告的,非常难受。因为有警告就代表代码还可以优化,或者说存在问题。 前几天捕捉了一个团队内部的小bug,其实本来和我没有关系,但是同事都在一头雾水的看外面的业务判断为什么走的分支不对,我一眼就扫到了问题。

因为 java 中整数字面量都是 int 类型,到集合中就变成了 Integer,然后 stepId 点上去一看是 long 类型,在集合中就是 Long,那这个 contains 妥妥的返回 false,都不是一个类型。

你看如果注重到警告,鼠标移过去看一眼提示就清楚了,少了一个生产 bug。

尽可能使用新技术组件

我觉得这是一个程序员应该具备的素养......反正我是喜欢用新的技术组件,因为新的技术组件出现必定是解决旧技术组件的不足,而且作为一个技术人员我们应该要与时俱进~~ 当然前提是要做好准备工作,不能无脑升级。举个最简单的例子,Java 17 都出来了,新项目现在还有人用 Date 来处理日期时间......

结语

本篇文章简单介绍我日常开发的习惯,当然仅是作者自己的见解。暂时只想到这几点,以后发现其他的会更新。

作者:暮色妖娆丶
来源:https://juejin.cn/post/7072252275002966030

收起阅读 »

看看别人后端API接口写得,那叫一个优雅!

在分布式、微服务盛行的今天,绝大部分项目都采用的微服务框架,前后端分离方式。题外话:前后端的工作职责越来越明确,现在的前端都称之为大前端,技术栈以及生态圈都已经非常成熟;以前后端人员瞧不起前端人员,那现在后端人员要重新认识一下前端,前端已经很成体系了。一般系统...
继续阅读 »

在分布式、微服务盛行的今天,绝大部分项目都采用的微服务框架,前后端分离方式。题外话:前后端的工作职责越来越明确,现在的前端都称之为大前端,技术栈以及生态圈都已经非常成熟;以前后端人员瞧不起前端人员,那现在后端人员要重新认识一下前端,前端已经很成体系了。

一般系统的大致整体架构图如下:

需要说明的是,有些小伙伴会回复说,这个架构太简单了吧,太low了,什么网关啊,缓存啊,消息中间件啊,都没有。因为老顾这篇主要介绍的是API接口,所以我们聚焦点,其他的模块小伙伴们自行去补充。

接口交互

前端和后端进行交互,前端按照约定请求URL路径,并传入相关参数,后端服务器接收请求,进行业务处理,返回数据给前端。

针对URL路径的restful风格,以及传入参数的公共请求头的要求(如:app_version,api_version,device等),老顾这里就不介绍了,小伙伴们可以自行去了解,也比较简单。

着重介绍一下后端服务器如何实现把数据返回给前端?

返回格式

后端返回给前端我们一般用JSON体方式,定义如下:

{
  #返回状态码
  code:integer,      
  #返回信息描述
  message:string,
  #返回值
  data:object
}

CODE状态码

code返回状态码,一般小伙伴们是在开发的时候需要什么,就添加什么。
如接口要返回用户权限异常,我们加一个状态码为101吧,下一次又要加一个数据参数异常,就加一个102的状态码。这样虽然能够照常满足业务,但状态码太凌乱了

我们应该可以参考HTTP请求返回的状态码,下面是常见的HTTP状态码:

200 - 请求成功
301 - 资源(网页等)被永久转移到其它URL
404 - 请求的资源(网页等)不存在
500 - 内部服务器错误


我们可以参考这样的设计,这样的好处就把错误类型归类到某个区间内,如果区间不够,可以设计成4位数。

#1000~1999 区间表示参数错误
#2000~2999 区间表示用户错误
#3000~3999 区间表示接口异常

这样前端开发人员在得到返回值后,根据状态码就可以知道,大概什么错误,再根据message相关的信息描述,可以快速定位。

Message

这个字段相对理解比较简单,就是发生错误时,如何友好的进行提示。一般的设计是和code状态码一起设计,如

再在枚举中定义,状态码

状态码和信息就会一一对应,比较好维护。

Data

返回数据体,JSON格式,根据不同的业务又不同的JSON体。
我们要设计一个返回体类Result

控制层Controller

我们会在controller层处理业务请求,并返回给前端,以order订单为例

我们看到在获得order对象之后,我们是用的Result构造方法进行包装赋值,然后进行返回。小伙伴们有没有发现,构造方法这样的包装是不是很麻烦,我们可以优化一下。

美观优化

我们可以在Result类中,加入静态方法,一看就懂

那我们来改造一下Controller

代码是不是比较简洁了,也美观了。

优雅优化

上面我们看到在Result类中增加了静态方法,使得业务处理代码简洁了。但小伙伴们有没有发现这样有几个问题:

1、每个方法的返回都是Result封装对象,没有业务含义

2、在业务代码中,成功的时候我们调用Result.success,异常错误调用Result.failure。是不是很多余

3、上面的代码,判断id是否为null,其实我们可以使用hibernate validate做校验,没有必要在方法体中做判断。

我们最好的方式直接返回真实业务对象,最好不要改变之前的业务方式,如下图

这个和我们平时的代码是一样的,非常直观,直接返回order对象,这样是不是很完美。那实现方案是什么呢?

实现方案

小伙伴们怎么去实现是不是有点思路,在这个过程中,我们需要做几个事情

1、定义一个注解@ResponseResult,表示这个接口返回的值需要包装一下

2、拦截请求,判断此请求是否需要被@ResponseResult注解

3、核心步骤就是实现接口ResponseBodyAdvice和@ControllerAdvice,判断是否需要包装返回值,如果需要,就把Controller接口的返回值进行重写。

注解类

用来标记方法的返回值,是否需要包装

拦截器

拦截请求,是否此请求返回的值需要包装,其实就是运行的时候,解析@ResponseResult注解

此代码核心思想,就是获取此请求,是否需要返回值包装,设置一个属性标记。

重写返回体

上面代码就是判断是否需要返回值包装,如果需要就直接包装。这里我们只处理了正常成功的包装,如果方法体报异常怎么办?处理异常也比较简单,只要判断body是否为异常类。

怎么做全局的异常处理,篇幅原因,老顾这里就不做介绍了,只要思路理清楚了,自行改造就行。

重写Controller

在控制器类上或者方法体上加上@ResponseResult注解,这样就ok了,简单吧。到此返回的设计思路完成,是不是又简洁,又优雅。

总结

这个方案还有没有别的优化空间,当然是有的。如:每次请求都要反射一下,获取请求的方法是否需要包装,其实可以做个缓存,不需要每次都需要解析。

作者:码农出击HK
来源:https://juejin.cn/post/7068884412674342926

收起阅读 »

抽丝剥茧聊Kotlin协程之深入理解协程上下文CoroutineContext

1. 前言如果你对CoroutineContext不了解,本文值得你细细品读,如果一遍看不懂,不妨多读几遍。写作该文的过程也是我对CoroutineContext理解加深的过程。CoroutineContext是协程的基础,值得投入学习Android开发者对C...
继续阅读 »

1. 前言

如果你对CoroutineContext不了解,本文值得你细细品读,如果一遍看不懂,不妨多读几遍。写作该文的过程也是我对CoroutineContext理解加深的过程。CoroutineContext是协程的基础,值得投入学习

Android开发者对Context都不陌生。在Android系统中,Context可谓神通广大,它可以获取应用资源,可以获取系统资源,可以启动Activity。Context有几个大名鼎鼎的子类,Activity、Application、Service,它们都是应用中非常重要的组件。

协程中也有个类似的概念,CoroutineContext。它是协程中的上下文,通过它我们可以控制协程在哪个线程中执行,可以设置协程的名字,可以用它来捕获协程抛出的异常等。

我们知道,通过CoroutineScope.launch方法可以启动一个协程。该方法第一个参数的类型就是CoroutineContext。默认值是EmptyCoroutineContext单例对象。 

在开始讲解CoroutineContext之前我们来看一段协程中经常会遇到的代码 

刚开始学协程的时候,我们经常会和Dispatchers.Main、Job、CoroutineName、CoroutineExceptionHandler打交道,它们都是CoroutineContext的子类。我们也很容易单独理解它们,Dispatchers.Main指把协程分发到主线程执行,Job可以管理协程的生命周期,CoroutineName可以设置协程的名字,CoroutineExceptionHandler可以捕获协程的异常。但是+操作符对大部分的Java开发者甚至Kotlin开发者而言会感觉到新鲜又难懂,在协程中CoroutineContext+到底是什么意思?

其实+操作符就是把两个CoroutineContext合并成一个链表,后文会详细讲解

2. CoroutineContext类图一览

根据类图结构我们可以把它分成四个层级:

  1. CoroutineContext 协程中所有上下文相关类的父接口。
  2. CombinedContext、Element、EmptyCoroutineContext。它们是CoroutineContext的直接子类。
  3. AbstractCoroutineContextElement、Job。这两个是Element的直接子类。
  4. CoroutineName、CoroutineExceptionHandler、CoroutineDispatcher(包含Dispatchers.Main和Dispatchers.Default)。它们是AbstractCoroutineContextElement的直接子类。

图中红框处,CombinedContext定义了size()和contains()方法,这与集合操作很像,CombinedContext是CoroutineContext对象的集合,而Element和EmptyCoroutineContext却没有定义这些方法,真正实现了集合操作的协程上下文只有CombinedContext,后文会详细讲解

3. CoroutineContext接口

CoroutineContext源码如下:  首先我们看下官方注释,我将它的作用归纳为:

Persistent context for the coroutine. It is an indexed set of [Element] instances. An indexed set is a mix between a set and a map. Every element in this set has a unique [Key].

  1. CoroutineContext是协程的上下文。
  2. CoroutineContext是element的set集合,没有重复类型的element对象。
  3. 集合中的每个element都有唯一的Key,Key可以用来检索元素。

相信大多数的人看到这样的解释时,都会心生疑惑,既然是set类型为啥不直接用HashSet来保存Element。CoroutineContext的实现原理又是什么呢?原因是考虑到协程嵌套,用链表实现更好。

接着我们来看下该接口定义的几个方法 

4. Key接口

Key是一个接口定义在CoroutineContext中的一个接口,作为接口它没有声明任何的方法,那么其实它没有任何真正有用的意义,它只是用来检索。我们先来看下,协程库中是如何使用Key接口的。  通过观察协程官方库中的例子,我们发现Element的子类都必须重写Key这个属性,而且Key的泛型类型必须和类名相同。以CoroutineName为例,Key是一个伴生对象,同时Key的泛型类型也是CoroutineName。

为了方便理解,我仿照写了MyElement类,如下:

通过对比kt类和反编译的java类我们看到 Key就是一个静态变量,而且它的实现类,其实啥也没干。它的作用与HashMap中的Key类似:

  1. 实现key-value功能,为插入和删除提供检索功能
  2. Key是static静态变量,全局唯一,为Element提供唯一性保障

Kotlin语法糖

coroutineContext.get(CoroutineName.Key)

coroutineContext.get(CoroutineName)

coroutineContext[CoroutineName]

coroutineContext[CoroutineName.Key]

写法是等价的

5. CoroutineContext.get方法

源码(整理在一起,下同) 

使用方式 

讲解

通过Key检索Element。返回值只能是Element或者null,链表节点中的元素值。

  1. Element get方法:只要Key与当前Element的Key匹配上了,返回该Element否则返回null。
  2. CombinedContext get方法:遍历链表,查询与Key相等的Element,如果没找到返回null。

6. CoroutineContext.plus方法

源码 

使用方式 

讲解

将两个CoroutineContext组合成一个CoroutineContext,如果是两个类型相同的Element会返回一个新的Element。如果是两个不同类型的Element会返回一个CombinedContext。如果是多个不同类型的Element会返回一条CombinedContext链表。

我将上述算法总结成了5种场景,不过在介绍这5种场景前,我们先讲解CombinedContext的数据结构。

7. CombinedContext分析

因为CombinedContext是CoroutineContext的子类,left也是CoroutineContext类型的,所以它的数据结构是链表。我们经常用next来表示链表的下一个节点。那么为什么这里取名叫left呢?我甚至怀疑写这段代码的是个左撇子。真正的原因是,协程可以启动子协程,子协程又可以启动孙协程。父协程在左边,子协程在右边

嵌套启动协程  越是外层的协程的Context越在左边,大概示意图如下 (真实并非如此,比这更复杂) 

链表的两个知识点在此都有体现。CoroutineContext.plus方法中使用的是头插法。CombinedContext的toString方法采用的是链表倒序打印法。

8. 五种plus场景

根据plus源码,我总结出会覆盖到五种场景。 

  1. plus EmptyCoroutineContext
  2. plus 相同类型的Element
  3. plus方法的调用方没有Dispatcher相关的Element
  4. plus方法的调用方只有Dispatcher相关的Element
  5. plus方法的调用方是包含Dispatcher相关Element的链表

结果如下:

  1. Dispatchers.Main + EmptyCoroutineContext 结果:Dispatchers.Main
  2. CoroutineName("c1") + CoroutineName("c2")结果: CoroutineName("c2")。相同类型的直接替换掉。
  3. CoroutineName("c1") + Job()结果:CoroutineName("c1") <- Job。头插法被plus的(Job)放在链表头部
  4. Dispatchers.Main + Job()结果:Job <- Dispatchers.Main。虽然是头插法,但是ContinuationInterceptor必须在链表头部。
  5. Dispatchers.Main + Job() + CoroutineName("c5")结果:Job <- CoroutineName("c5") <- Dispatchers.Main。Dispatchers.Main在链表头部,其它的采用头插法。

如果不考虑Dispatchers.Main的情况。我们可以把+<-代替。CoroutineName("c1") + Job()等价于CoroutineName("c1") <- Job

9. CoroutineContext的minusKey方法

源码 

讲解

  1. Element minusKey方法:如果Key与当前element的Key相等,返回EmptyCoroutineContext,否则相当于没减成功,返回当前element
  2. CombinedContext minusKey方法:删除链表中符合条件的节点,分三种情况。

三种情况以下面链表为例

Job <- CoroutineName("c5") <-Dispatchers.Main

  1. 没找到节点:minusKey(MyElement)。在Job节点处走newLeft === left分支,依此类推,在CoroutineName处走同样的分支,在Dispatchers.Main处走同样的分支。

  2. 节点在尾部:minusKey(Job)。在CoroutineName("c5")节点走newLeft === EmptyCoroutineContext分支,依此往头部递归

  3. 节点不在尾部:minusKey(CoroutineName)。在Dispatchers.Main节点处走else分支

10. 总结

学习CoroutineContext首先要搞清楚各类之间的继承关系,其次,CombinedContext各具体Element的集合,它的数据结构是链表,如果读者对链表增删改查操作熟悉的话,那么很容易就能搞懂CoroutineContext原理,否则想要搞懂CoroutineContext那简直如盲人摸象。


作者:字节小站
链接:https://juejin.cn/post/7037256376887803912
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

flutter 项目中json转dart模型

前言 实际开发项目中,后端返回的数据大多数是json格式,例如上一篇 Flutter 自定义数据选择器 中显示的数据都是dart模型的数据,由于dart本身数据格式并没有json格式,所以需要转化成dart模型数据。 解决方案 1. 小项目手动序列化 dart...
继续阅读 »

前言


实际开发项目中,后端返回的数据大多数是json格式,例如上一篇 Flutter 自定义数据选择器 中显示的数据都是dart模型的数据,由于dart本身数据格式并没有json格式,所以需要转化成dart模型数据。


解决方案


1. 小项目手动序列化


dart:convert 中内置的JSON解码器 它将原始JSON字符串传递给JSON.decode() 方法,然后在返回的Map<String, dynamic>中查找所需的值。 它没有外部依赖或其它的设置,对于小项目很方便


2.对于大中型项目 使用 json to Dart 网站生成model类


3. 使用插件 json_serializable package


实际应用


以方案1 和方案2 在实际项目中展示,还是以上一篇的[Flutter 自定义数据选择器] 为例


由于json to Dart 这个转换已经支持空安全,所以如果需要改动


pubspec.yaml文件


environment:
sdk: ">=2.12.0 <3.0.0" #如果需要支持空安全,只需要这里改一下版本 >=2.7.0 改成2.12.0

执行:


flutter clean

flutter pub get

area-data-jsonString.dart 文件


定义一个json 字符串数据(由于dart 没有json数据类型, 本地模拟数据就用json数据组装成 字符串格式,'''XXX ''' 包裹起来, 注意是 三个单引号)


image.png


进入 json to Dart
粘贴 ,定义类型


image.png


转化的模型数据如下:


area-data-json.dart


class AreaDataToJson {
String? id;
String? name;
String? center;
int? level;
List<Children>? children;

AreaDataToJson({this.id, this.name, this.center, this.level, this.children});

AreaDataToJson.fromJson(Map<String, dynamic> json) {
id = json['id'];
name = json['name'];
center = json['center'];
level = json['level'];
if (json['children'] != null) {
children = <Children>[];
json['children'].forEach((v) {
children!.add(new Children.fromJson(v));
});
}
}

Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this.id;
data['name'] = this.name;
data['center'] = this.center;
data['level'] = this.level;
if (this.children != null) {
data['children'] = this.children!.map((v) => v.toJson()).toList();
}
return data;
}
}

class Children {
String? id;
String? name;
String? center;
int? level;

Children({this.id, this.name, this.center, this.level});

Children.fromJson(Map<String, dynamic> json) {
id = json['id'];
name = json['name'];
center = json['center'];
level = json['level'];
}

Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this.id;
data['name'] = this.name;
data['center'] = this.center;
data['level'] = this.level;
return data;
}
}

仔细对比发现这里生成数据有问题,省市区是有三层数据,这里只有两层(chilren下面下面可能还存在一层,这里后面再研究一下)


custom-pick.dart 这里就不贴出来,跟上一篇[Flutter 自定义数据选择器] 一样,去上面查看或者查看github源码


如下调用:
json-picker-page.dart



import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:personal_app/components/custom-picker.dart';
import 'package:personal_app/data/area-data-jsonString.dart';
import 'package:personal_app/Modal/area-data-json.dart';

class JsonPickerPage extends StatefulWidget {

@override
_JsonPickerPageState createState() => _JsonPickerPageState();
}

class _JsonPickerPageState extends State<JsonPickerPage> {
// 方案一
List<dynamic> provAreaDataIdxs = [];//省市区的选择数据下标集合
String provAreaName = '';// 省市区名称
dynamic provAreaValue; //省市区选的数据id
List<Map<String, dynamic>> resetAreaData = []; //省市区数据转换后的数据

// 方案二
List<dynamic> provAreaDataIdxs2 = [];//省市区的选择数据下标集合
String provAreaName2 = '';// 省市区名称
dynamic provAreaValue2; //省市区选的数据id
List<Map<String, dynamic>> resetAreaData2 = []; //省市区数据转换后的数据


@override
void initState() {
//方式1,因为数据是本地的数据,dart本身是没有dart数据格式,
// 所以在定义json数据格式时,需要加上'''XXX''' 先包裹成字符串
//所以需要解析成List ,真实的接口请求不需要这么处理
List<dynamic> AreaDataJson = json.decode(areaDataJsonSting);//Json 改成小写,升级原因
//需要注意的是 areaData 数据字段不是label 和value; 需要转化一下
resetAreaData =recursionDataHandle(AreaDataJson);

//方式 2
List<dynamic> AreaDataJson2 = json.decode(areaDataJsonSting);
List<dynamic> AreaDataDart2 = AreaDataJson2.map((item) =>
new AreaDataToJson.fromJson(item)).toList();
print(AreaDataDart2.toString()); //这里打印才发现AreaDataToJson类有问题,该网站不能对于这种树形结构的数据生成还存在问题,下面放开2层 即 省市是没问题的

// // List<Map<String, dynamic>> AreaDataDart = AreaDataToJson.fromJson(AreaDataJson);
//需要注意的是 areaData 数据字段不是label 和value; 需要转化一下
resetAreaData2 =recursionDataHandle(AreaDataDart2);
super.initState();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("自定义数据选择器"),
),
body: Center(
child: GestureDetector(
onTap: (){
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.max,
children: [
RaisedButton(
padding: EdgeInsets.all(0),
onPressed: () {
customPicker(context,
{"indexs":provAreaDataIdxs, "initData": resetAreaData, "colNum":3},
(opt) {
setState(() {
provAreaDataIdxs = opt['indexs'];
List names = opt['names'];
provAreaName = '';
for(int i = 0; i< names.length; i++) {
provAreaName += names[i]['label'] != '' ? i== 0 ? names[i]['label'] : '/' + names[i]['label'] : '';
}
provAreaValue = names[names.length-1]['value'];//value 这里逻辑只需要取最后一个
});
});
},
child: Row(
children: [
Text('省市区选择方案1'),

],
),
),
Text(provAreaName),
Container(
height: 26.0,
),
RaisedButton(
padding: EdgeInsets.all(0),
onPressed: () {
customPicker(context,
{"indexs":provAreaDataIdxs2, "initData": resetAreaData2, "colNum":2},
(opt) {
setState(() {
provAreaDataIdxs2 = opt['indexs'];
List names = opt['names'];
provAreaName2 = '';
for(int i = 0; i< names.length; i++) {
provAreaName2 += names[i]['label'] != '' ? i== 0 ? names[i]['label'] : '/' + names[i]['label'] : '';
}
provAreaValue2 = names[names.length-1]['value'];//value 这里逻辑只需要取最后一个
});
});
},
child: Row(
children: [
Text('省市区选择方案2'),
],
),
),
Text(provAreaName2),
Container(
height: 26.0,
),
]
),
),
),
);
}

//数据格式转换
recursionDataHandle(data) {
List<Map<String, dynamic>> resetData = [];
if(data?.length > 0) {
for (var i = 0; i <data?.length; i++) {
Map<String, dynamic> tmpData;
try {
tmpData = data?[i].toJson();
} catch(e) {
tmpData = data?[i];
}

resetData.add({
'value': tmpData['id'],
'label': tmpData['name'],
'center': tmpData['center'],
'level': tmpData['level'],
// 'children': data[i]['children'] ? recursionDataHandle(data[i]['children']): []
});
if(tmpData.containsKey('children')) { //是否包含key值children
if(tmpData['children']?.length > 0) {
resetData[i]['children'] = recursionDataHandle(tmpData['children']);
} else {
resetData[i]['children'] = [];
}
}
}
}
return resetData;
}
}

方式1 和方式2 分别对应着上面的方案1 和方案2


效果:


image.png


问题


需要研究一下 json to Dart 类似多层树形结构数据存在问题?


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

为什么IDEA不推荐你使用@Autowired ?

@Autowired注解相信每个Spring开发者都不陌生了!在DD的Spring Boot基础教程和Spring Cloud基础教程中也都经常会出现。 但是当我们使用IDEA写代码的时候,经常会发现@Autowired注解下面是有小黄线的,我们把小鼠标悬停在...
继续阅读 »

@Autowired注解相信每个Spring开发者都不陌生了!在DD的Spring Boot基础教程Spring Cloud基础教程中也都经常会出现。


但是当我们使用IDEA写代码的时候,经常会发现@Autowired注解下面是有小黄线的,我们把小鼠标悬停在上面,可以看到这个如下图所示的警告信息:


IDEA警告:Field injection is not recommended


那么为什么IDEA会给出Field injection is not recommended这样的警告呢?


下面带着这样的问题,一起来全面的了解下Spring中的三种注入方式以及他们之间在各方面的优劣。


Spring中的三种依赖注入方式


Field Injection


@Autowired注解的一大使用场景就是Field Injection


具体形式如下:


@Controller
public class UserController {

@Autowired
private UserService userService;

}

这种注入方式通过Java的反射机制实现,所以private的成员也可以被注入具体的对象。


Constructor Injection


Constructor Injection是构造器注入,是我们日常最为推荐的一种使用方式。


具体形式如下:


@Controller
public class UserController {

private final UserService userService;

public UserController(UserService userService){
this.userService = userService;
}

}

这种注入方式很直接,通过对象构建的时候建立关系,所以这种方式对对象创建的顺序会有要求,当然Spring会为你搞定这样的先后顺序,除非你出现循环依赖,然后就会抛出异常。


Setter Injection


Setter Injection也会用到@Autowired注解,但使用方式与Field Injection有所不同,Field Injection是用在成员变量上,而Setter Injection的时候,是用在成员变量的Setter函数上。


具体形式如下:


@Controller
public class UserController {

private UserService userService;

@Autowired
public void setUserService(UserService userService){
this.userService = userService;
}
}

这种注入方式也很好理解,就是通过调用成员变量的set方法来注入想要使用的依赖对象。


三种依赖注入的对比


在知道了Spring提供的三种依赖注入方式之后,我们继续回到本文开头说到的问题:IDEA为什么不推荐使用Field Injection呢?


我们可以从多个开发测试的考察角度来对比一下它们之间的优劣:


可靠性


从对象构建过程和使用过程,看对象在各阶段的使用是否可靠来评判:



  • Field Injection:不可靠

  • Constructor Injection:可靠

  • Setter Injection:不可靠


由于构造函数有严格的构建顺序和不可变性,一旦构建就可用,且不会被更改。


可维护性


主要从更容易阅读、分析依赖关系的角度来评判:



  • Field Injection:差

  • Constructor Injection:好

  • Setter Injection:差


还是由于依赖关键的明确,从构造函数中可以显现的分析出依赖关系,对于我们如何去读懂关系和维护关系更友好。


可测试性


当在复杂依赖关系的情况下,考察程序是否更容易编写单元测试来评判



  • Field Injection:差

  • Constructor Injection:好

  • Setter Injection:好


Constructor InjectionSetter Injection的方式更容易Mock和注入对象,所以更容易实现单元测试。


灵活性


主要根据开发实现时候的编码灵活性来判断:



  • Field Injection:很灵活

  • Constructor Injection:不灵活

  • Setter Injection:很灵活


由于Constructor Injection对Bean的依赖关系设计有严格的顺序要求,所以这种注入方式不太灵活。相反Field InjectionSetter Injection就非常灵活,但也因为灵活带来了局面的混乱,也是一把双刃剑。


循环关系的检测


对于Bean之间是否存在循环依赖关系的检测能力:



  • Field Injection:不检测

  • Constructor Injection:自动检测

  • Setter Injection:不检测


性能表现


不同的注入方式,对性能的影响



  • Field Injection:启动快

  • Constructor Injection:启动慢

  • Setter Injection:启动快


主要影响就是启动时间,由于Constructor Injection有严格的顺序要求,所以会拉长启动时间。


所以,综合上面各方面的比较,可以获得如下表格:


三种依赖注入的对比


结果一目了然,Constructor Injection在很多方面都是优于其他两种方式的,所以Constructor Injection通常都是首选方案!


Setter Injection比起Field Injection来说,大部分都一样,但因为可测试性更好,所以当你要用@Autowired的时候,推荐使用Setter Injection的方式,这样IDEA也不会给出警告了。同时,也侧面也反映了,可测试性的重要地位啊!


总结


最后,对于今天的问题讨论,我们给出两个结论,方便大家记忆:



  1. 依赖注入的使用上,Constructor Injection是首选。

  2. 使用@Autowired注解的时候,要使用Setter Injection方式,这样代码更容易编写单元测试。


好了,今天的学习就到这里!如果您学习过程中如遇困难?可以加入我们超高质量的Spring技术交流群,参与交流与讨论,更好的学习与进步!


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

LeetCode 动态规划之杨辉三角

题目 杨辉三角 给定一个非负整数 numRows, 生成「杨辉三角」的前 numRows 行。 在「杨辉三角」中,每个数是它左上方和右上方的数的和。 示例 1: 输入: numRows = 5 输出: [[1],[1,1],[1,2,1...
继续阅读 »

题目


杨辉三角

给定一个非负整数 numRows 生成「杨辉三角」的前 numRows 行。

在「杨辉三角」中,每个数是它左上方和右上方的数的和。



示例 1:


输入: numRows = 5
输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]

示例 2:


输入: numRows = 1
输出: [[1]]

提示:



  • 1 <= numRows <= 30


题解


解题分析


杨辉三角介绍:


杨辉三角,是二项式系数在三角形中的一种几何排列,南宋时期数学家杨辉在 1261年 所著《详解九章算法》中出现。

在欧洲,帕斯卡(1623-1662)在1654年发现这一数学规律,所以这个又叫做帕斯卡三角形。帕斯卡的发现比 杨辉 要迟393年,比 贾宪 迟600年。
杨辉三角是中国数学史上的一个伟大成就。


杨辉三角形的规律如题目中所示。


题解思路



  1. 定义一个集合存储结果集

  2. 定义一个两重循环,分别进行运算,row 数为 numRows, 列数的值为 0 -> i 其中 i 表示当前是第几行.

  3. 如果当前 row 的列是第一列或最后一个列的当前的值为 1。

  4. 当前行其他的列的其他数据, 等于 re.get(i-1).get(j-) + re.get(i -1).get(j) 之和


复杂度分析



  • 时间复杂度:O(num * numRow ^2)

  • 空间复杂度:O(1)


解题代码


题解代码如下(代码中有详细的注释说明):


class Solution {
public List<List<Integer>> generate(int numRows) {
List<List<Integer>> re = new ArrayList<>();
for (int i =0; i< numRows; i++) {
List<Integer> row = new ArrayList<>();
for (int j=0; j<=i; j++) {
// 行中的第一数据和最后一个数据的值为 1
if (j == 0 || j == i) {
row.add(1);
} else {
// 其他数据, 等于 re.get(i-1).get(j-) + re.get(i -1).get(j) 之和
row.add(re.get(i -1).get(j -1) + re.get(i -1).get(j));
}
}
re.add(row);
}
return re;
}
}

提交后反馈结果:


image.png


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

官方推荐 Flow 取代 LiveData,有必要吗?

前言 打开Android架构组件页面,我们可以发现一些最新发布的jetpack组件,如Room,DataStore, Paging3,DataBinding 等都支持了Flow Google开发者账号最近也发布了几篇使用Flow的文章,比如:从 LiveDat...
继续阅读 »

前言


打开Android架构组件页面,我们可以发现一些最新发布的jetpack组件,如RoomDataStore, Paging3,DataBinding 等都支持了Flow

Google开发者账号最近也发布了几篇使用Flow的文章,比如:从 LiveData 迁移到 Kotlin 数据流

看起来官方在大力推荐使用Flow取代LiveData,那么问题来了,有必要吗?

LiveData用得好好的,有必要再学Flow吗?本文主要回答这个问题,具体包括以下内容

1.LiveData有什么不足?

2.Flow介绍以及为什么会有Flow

3.SharedFlowStateFlow的介绍与它们之间的区别


本文具体目录如下所示:


1. LiveData有什么不足?


1.1 为什么引入LiveData?


要了解LiveData的不足,我们先了解下LiveData为什么被引入



LiveData 的历史要追溯到 2017 年。彼时,观察者模式有效简化了开发,但诸如 RxJava 一类的库对新手而言有些太过复杂。为此,架构组件团队打造了 LiveData: 一个专用于 Android 的具备自主生命周期感知能力的可观察的数据存储器类。LiveData 被有意简化设计,这使得开发者很容易上手;而对于较为复杂的交互数据流场景,建议您使用 RxJava,这样两者结合的优势就发挥出来了



可以看出,LiveData就是一个简单易用的,具备感知生命周期能力的观察者模式

它使用起来非常简单,这是它的优点,也是它的不足,因为它面对比较复杂的交互数据流场景时,处理起来比较麻烦


1.2 LiveData的不足


我们上文说过LiveData结构简单,但是不够强大,它有以下不足

1.LiveData只能在主线程更新数据

2.LiveData的操作符不够强大,在处理复杂数据流时有些捉襟见肘


关于LiveData只能在主线程更新数据,有的同学可能要问,不是有postValue吗?其实postValue也是需要切换到到主线程的,如下图所示:



这意味着当我们想要更新LiveData对象时,我们会经常更改线程(工作线程→主线程),如果在修改LiveData后又要切换回到工作线程那就更麻烦了,同时postValue可能会有丢数据的问题。


2. Flow介绍


Flow 就是 Kotlin 协程与响应式编程模型结合的产物,你会发现它与 RxJava 非常像,二者之间也有相互转换的 API,使用起来非常方便。


2.1 为什么引入Flow


为什么引入Flow,我们可以从Flow解决了什么问题的角度切入



  1. LiveData不支持线程切换,所有数据转换都将在主线程上完成,有时需要频繁更改线程,面对复杂数据流时处理起来比较麻烦

  2. RxJava又有些过于麻烦了,有许多让人傻傻分不清的操作符,入门门槛较高,同时需要自己处理生命周期,在生命周期结束时取消订阅


可以看出,Flow是介于LiveDataRxJava之间的一个解决方案,它有以下特点



  • Flow 支持线程切换、背压

  • Flow 入门的门槛很低,没有那么多傻傻分不清楚的操作符

  • 简单的数据转换与操作符,如 map 等等

  • 冷数据流,不消费则不生产数据,这一点与LiveData不同:LiveData的发送端并不依赖于接收端。

  • 属于kotlin协程的一部分,可以很好的与协程基础设施结合


关于Flow的使用,比较简单,有兴趣的同学可参阅文档:Flow文档


3. SharedFlow介绍


我们上面介绍过,Flow 是冷流,什么是冷流?



  • 冷流 :只有订阅者订阅时,才开始执行发射数据流的代码。并且冷流订阅者只能是一对一的关系,当有多个不同的订阅者时,消息是重新完整发送的。也就是说对冷流而言,有多个订阅者的时候,他们各自的事件是独立的。

  • 热流:无论有没有订阅者订阅,事件始终都会发生。当 热流有多个订阅者时,热流订阅者们的关系是一对多的关系,可以与多个订阅者共享信息。


3.1 为什么引入SharedFlow


上面其实已经说得很清楚了,冷流订阅者只能是一对一的关系,当我们要实现一个流,多个订阅者的需求时(这在开发中是很常见的),就需要热流

从命名上也很容易理解,SharedFlow即共享的Flow,可以实现一对多关系,SharedFlow是一种热流


3.2 SharedFlow的使用


我们来看看SharedFlow的构造函数


public fun <T> MutableSharedFlow(
replay: Int = 0,
extraBufferCapacity: Int = 0,
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T>

其主要有3个参数

1.replay表示当新的订阅者Collect时,发送几个已经发送过的数据给它,默认为0,即默认新订阅者不会获取以前的数据

2.extraBufferCapacity表示减去replayMutableSharedFlow还缓存多少数据,默认为0

3.onBufferOverflow表示缓存策略,即缓冲区满了之后Flow如何处理,默认为挂起


简单使用如下:


//ViewModel
val sharedFlow=MutableSharedFlow<String>()

viewModelScope.launch{
sharedFlow.emit("Hello")
sharedFlow.emit("SharedFlow")
}

//Activity
lifecycleScope.launch{
viewMode.sharedFlow.collect {
print(it)
}
}

3.3 将冷流转化为SharedFlow


普通flow可使用shareIn扩展方法,转化成SharedFlow


    val sharedFlow by lazy {
flow<Int> {
//...
}.shareIn(viewModelScope, WhileSubscribed(500), 0)
}

shareIn主要也有三个参数:



@param scope 共享开始时所在的协程作用域范围

@param started 控制共享的开始和结束的策略

@param replay 状态流的重播个数



started 接受以下的三个值:

1.Lazily: 当首个订阅者出现时开始,在scope指定的作用域被结束时终止。

2.Eagerly: 立即开始,而在scope指定的作用域被结束时终止。

3.WhileSubscribed: 这种情况有些复杂,后面会详细讲解


对于那些只执行一次的操作,您可以使用Lazily或者Eagerly。然而,如果您需要观察其他的流,就应该使用WhileSubscribed来实现细微但又重要的优化工作


3.4 Whilesubscribed策略


WhileSubscribed策略会在没有收集器的情况下取消上游数据流,通过shareIn运算符创建的SharedFlow会把数据暴露给视图 (View),同时也会观察来自其他层级或者是上游应用的数据流。

让这些流持续活跃可能会引起不必要的资源浪费,例如一直通过从数据库连接、硬件传感器中读取数据等等。当您的应用转而在后台运行时,您应当保持克制并中止这些协程。


public fun WhileSubscribed(
stopTimeoutMillis: Long = 0,
replayExpirationMillis: Long = Long.MAX_VALUE
)

如上所示,它支持两个参数:



  • 1.stopTimeoutMillis 控制一个以毫秒为单位的延迟值,指的是最后一个订阅者结束订阅与停止上游流的时间差。默认值是 0 (立即停止).这个值非常有用,因为您可能并不想因为视图有几秒钟不再监听就结束上游流。这种情况非常常见——比如当用户旋转设备时,原来的视图会先被销毁,然后数秒钟内重建。

  • 2.replayExpirationMillis表示数据重播的过时时间,如果用户离开应用太久,此时您不想让用户看到陈旧的数据,你可以用到这个参数


4. StateFlow介绍


4.1 为什么引入StateFlow


我们前面刚刚看了SharedFlow,为什么又冒出个StateFlow?

StateFlowSharedFlow 的一个比较特殊的变种,StateFlowLiveData 是最接近的,因为:



  • 1.它始终是有值的。

  • 2.它的值是唯一的。

  • 3.它允许被多个观察者共用 (因此是共享的数据流)。

  • 4.它永远只会把最新的值重现给订阅者,这与活跃观察者的数量是无关的。


可以看出,StateFlowLiveData是比较接近的,可以获取当前的值,可以想像之所以引入StateFlow就是为了替换LiveData

总结如下:

1.StateFlow继承于SharedFlow,是SharedFlow的一个特殊变种

2.StateFlowLiveData比较相近,相信之所以推出就是为了替换LiveData


4.2 StateFlow的简单使用


我们先来看看构造函数:


public fun <T> MutableStateFlow(value: T): MutableStateFlow<T> = StateFlowImpl(value ?: NULL)

1.StateFlow构造函数较为简单,只需要传入一个默认值

2.StateFlow本质上是一个replay为1,并且没有缓冲区的SharedFlow,因此第一次订阅时会先获得默认值

3.StateFlow仅在值已更新,并且值发生了变化时才会返回,即如果更新后的值没有变化,也没会回调Collect方法,这点与LiveData不同


SharedFlow类似,我们也可以用stateIn将普通流转化成StateFlow


val result: StateFlow<Result<UiState>> = someFlow
.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)

shareIn类似,唯一不同的时需要传入一个默认值

同时之所以WhileSubscribed中传入了5000,是为了实现等待5秒后仍然没有订阅者存在就终止协程的功能,这个方法有以下功能



  • 用户将您的应用转至后台运行,5 秒钟后所有来自其他层的数据更新会停止,这样可以节省电量。

  • 最新的数据仍然会被缓存,所以当用户切换回应用时,视图立即就可以得到数据进行渲染。

  • 订阅将被重启,新数据会填充进来,当数据可用时更新视图。

  • 在屏幕旋转时,因为重新订阅的时间在5s内,因此上游流不会中止


4.3 在页面中观察StateFlow


LiveData类似,我们也需要经常在页面中观察StateFlow

观察StateFlow需要在协程中,因此我们需要协程构建器,一般我们会使用下面几种



  1. lifecycleScope.launch : 立即启动协程,并且在本 ActivityFragment 销毁时结束协程。

  2. LaunchWhenStartedLaunchWhenResumed,它会在lifecycleOwner进入X状态之前一直等待,又在离开X状态时挂起协程




如上图所示:

1.使用launch是不安全的,在应用在后台时也会接收数据更新,可能会导致应用崩溃

2.使用launchWhenStartedlaunchWhenResumed会好一些,在后台时不会接收数据更新,但是,上游数据流会在应用后台运行期间保持活跃,因此可能浪费一定的资源


这么说来,我们使用WhileSubscribed进行的配置岂不是无效了吗?订阅者一直存在,只有页面关闭时才会取消订阅

官方推荐repeatOnLifecycle来构建协程

在某个特定的状态满足时启动协程,并且在生命周期所有者退出该状态时停止协程,如下图所示。



比如在某个Fragment的代码中:


onCreateView(...) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
myViewModel.myUiState.collect { ... }
}
}
}

当这个Fragment处于STARTED状态时会开始收集流,并且在RESUMED状态时保持收集,最终在Fragment进入STOPPED状态时结束收集过程。

结合使用repeatOnLifecycle APIWhileSubscribed,可以帮助您的应用妥善利用设备资源的同时,发挥最佳性能


4.4 页面中观察Flow的最佳方式


通过ViewModel暴露数据,并在页面中获取的最佳方式是:



  • ✔️ 使用带超时参数的 WhileSubscribed 策略暴露 Flow示例 1

  • ✔️ 使用 repeatOnLifecycle 来收集数据更新。示例 2




最佳实践如上图所示,如果采用其他方式,上游数据流会被一直保持活跃,导致资源浪费

当然,如果您并不需要使用到Kotlin Flow的强大功能,就用LiveData好了 :)


5 StateFlowSharedFlow有什么区别?


从上文其实可以看出,StateFlowSharedFlow其实是挺像的,让人有些傻傻分不清,有时候也挺难选择该用哪个的


我们总结一下,它们的区别如下:



  1. SharedFlow配置更为灵活,支持配置replay,缓冲区大小等,StateFlowSharedFlow的特化版本,replay固定为1,缓冲区大小默认为0

  2. StateFlowLiveData类似,支持通过myFlow.value获取当前状态,如果有这个需求,必须使用StateFlow

  3. SharedFlow支持发出和收集重复值,而StateFlowvalue重复时,不会回调collect

  4. 对于新的订阅者,StateFlow只会重播当前最新值,SharedFlow可配置重播元素个数(默认为0,即不重播)


可以看出,StateFlow为我们做了一些默认的配置,在SharedFlow上添加了一些默认约束,这些配置可能并不符合我们的要求



  1. 它忽略重复的值,并且是不可配置的。这会带来一些问题,比如当往List中添加元素并更新时,StateFlow会认为是重复的值并忽略

  2. 它需要一个初始值,并且在开始订阅时会回调初始值,这有可能不是我们想要的

  3. 它默认是粘性的,新用户订阅会获得当前的最新值,而且是不可配置的,而SharedFlow可以修改replay


StateFlow施加在SharedFlow上的约束可能不是最适合您,如果不需要访问myFlow.value,并且享受SharedFlow的灵活性,可以选择考虑使用SharedFlow


总结


简单往往意味着不够强大,而强大又常常意味着复杂,两者往往不能兼得,软件开发过程中常常面临这种取舍。

LiveData的简单并不是它的缺点,而是它的特点。StateFlowSharedFlow更加强大,但是学习成本也显著的更高.

我们应该根据自己的需求合理选择组件的使用



  1. 如果你的数据流比较简单,不需要进行线程切换与复杂的数据变换,LiveData对你来说相信已经足够了

  2. 如果你的数据流比较复杂,需要切换线程等操作,不需要发送重复值,需要获取myFlow.valueStateFlow对你来说是个好的选择

  3. 如果你的数据流比较复杂,同时不需要获取myFlow.value,需要配置新用户订阅重播无素的个数,或者需要发送重复的值,可以考虑使用SharedFlow

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

当我们谈部署时,我们在谈什么?

计算机网络把各地的计算机连接了起来,只要有一台可以上网的终端,比如手机、电脑,就可以访问互联网上任何一台服务器的资源(包括静态资源和动态的服务)。作为开发者的我们,就是这些资源、服务的提供者,把资源上传到服务器,并把服务跑起来的过程就叫做部署。代码部分的部署,...
继续阅读 »


计算机网络把各地的计算机连接了起来,只要有一台可以上网的终端,比如手机、电脑,就可以访问互联网上任何一台服务器的资源(包括静态资源和动态的服务)。

作为开发者的我们,就是这些资源、服务的提供者,把资源上传到服务器,并把服务跑起来的过程就叫做部署


代码部分的部署,需要先经过构建,也就是编译打包的过程,把产物传到服务器。

最原始的部署方式就是在本地进行 build,然后把产物通过 FTP 或者 scp(基于 SSH 的远程拷贝文件拷贝) 传到服务器上,如果是后端代码还需要重启下服务。


每个人单独构建上传,这样不好管理,也容易冲突,所以现在都会用专门的平台来做这件事构建和部署,比如 jenkins。

我们代码会提交到 gitlab 等代码库,然后 jenkins 从这些代码库里把代码下载下来进行 build,再把产物上传到服务器上。

流程和直接在本地构建上传差不多,只不过这样方便管理冲突、历史等,还可以跨项目复用一些东西。


构建、部署的过程最开始是通过 shell 来写,但写那个的要求还是很高的。后来就支持了可视化的编排,可以被编排的这个构建、部署的流程叫做流水线 pipeline。


比如这是 jenkins 的 pipeline 的界面:


除了构建、部署外,也可以加入一些自动化测试、静态代码检查等任务。

这种自动化了的构建、部署流程就叫做 CI(持续集成)、CD(持续部署)。

我们现在还是通过 scp / FTP 来上传代码做的部署,但是不同代码的运行环境是不同的,比如 Node.js 服务需要安装 node,Java 服务需要安装 JRE 等,只把代码传上去并不一定能跑起来。

那怎么办呢?怎么保证部署的代码运行在正确的环境?

把环境也给管理起来,作为部署信息的一部分不就行了?

现在流行的容器技术就是做这个的,比如 docker,可以把环境信息和服务启动方式放到 dockerfile 里,build 产生一个镜像 image,之后直接部署这个 docker image 就行。

比如我们用 nginx 作为静态服务器的时候,dockerfile 可能是这样的:

FROM nginx:alpine

COPY /nginx/ /etc/nginx/

COPY /dist/ /usr/share/nginx/html/

EXPOSE 80

这样就把运行环境给管理了起来。

所以,现在的构建产物不再是直接上传服务器,而是生成一个 docker image,上传到 docker registry,然后把这个 docker image 部署到服务器。


还有一个问题,现在前端代码、后端代码都部署在了我们的服务器上,共享服务器的网络带宽,其中前端代码是不会变动的、流量却很大,这样使得后端服务的可用带宽变小、支持的并发量下降。

能不能把这部分静态资源的请求分离出去呢?最好能部署到离用户近一点的服务器,这样访问更快。

确实可以,这就是 CDN 做的事情。

网上有专门的 CDN 服务提供商,它们有很多分散在各地的服务器,可以提供静态资源的托管。这些静态资源最终还是从我们的静态资源服务器来拿资源的,所以我们的静态资源服务器叫做源站。但是请求一次之后就会缓存下来,下次就不用再请求源站了,这样就减轻了我们服务器的压力,还能加速用户请求静态资源的速度。


这样就解决了静态资源分去了太多网络带宽的问题,而且还加速了资源的访问速度。

此外,静态资源的部署还要考虑顺序问题,要先部署页面用到的资源,再部署页面,还有,需要在文件名加 hash 来触发缓存更新等,这些都是更细节的问题。

这里说的都是网页的部署方式,对于 APP/小程序它们是有自己的服务器和分发渠道的,我们构建完之后不是部署,而是在它们的平台提交审核,审核通过之后由它们负责部署和分发。

总结

互联网让我们能够用手机、PC 等终端访问任何一台服务器的资源、服务。而提供这些资源、服务就是我们开发者做的事情。把资源上传到服务器上,并把服务跑起来,就叫做部署。

对于代码,我们可以本地构建,然后把构建产物通过 FTP/scp 等方式上传到服务器。

但是这样的方式不好管理,所以我们会有专门的 CI/CD 平台来做这个,比如 jenkins。

jenkins 支持 pipeline 的可视化编排,比写 shell 脚本的方式易用很多,可以在构建过程中加入自动化测试、静态代码检查等步骤。

不同代码运行环境不同,为了把环境也管理起来,我们会使用容器技术,比如 docker。把环境信息写入 dockerfile,然后构建生成 docker image,上传到 registry,之后部署这个 docker image 就行。

静态资源和动态资源共享服务器的网络带宽,为了减轻服务器压力、也为了加速静态资源的访问,我们会使用 CDN 来对静态资源做加速,把我们的静态服务器作为源站。第一个静态资源的请求会请求源站并缓存下来,之后的请求就不再需要请求源站,这样就减轻了源站的压力。此外,静态资源的部署还要考虑顺序、缓存更新等问题。

对于网页来说是这样,APP/小程序等不需要我们负责部署,只要在它们的平台提交审核,然后由它们负责部署和分发。

当我们在谈部署的时候,主要就是在谈这些。

作者:zxg_神说要有光
来源:https://juejin.cn/post/7073826878858985479

收起阅读 »

关于项目版本号命名的规范与原则

软件版本阶段说明Alpha版此版本表示该软件在此阶段主要是以实现软件功能为主,通常只在软件开发者内部交流,一般而言,该版本软件的Bug较多,需要继续修改Beta版版本相对于α版已有了很大的改进,消除了严重的错误,但还是存在着一些缺陷,需要经过多次测试来进一步消...
继续阅读 »

软件版本阶段说明

  • Alpha版

    此版本表示该软件在此阶段主要是以实现软件功能为主,通常只在软件开发者内部交流,一般而言,该版本软件的Bug较多,需要继续修改

  • Beta版

    版本相对于α版已有了很大的改进,消除了严重的错误,但还是存在着一些缺陷,需要经过多次测试来进一步消除,此版本主要的修改对像是软件的UI

  • RC版

    该版本已经相当成熟了,基本上不存在导致错误的BUG,与即将发行的正式版相差无几

  • Release版:

    该版本意味“最终版本”,在前面版本的一系列测试版之后,终归会有一个正式版本,是最终交付用户使用的一个版本。该版本有时也称为标准版。一般情况下,Release不会以单词形式出现在软件封面上,取而代之的是符号(R)

在上面我们大致了解了软件从开发到交付会经历4个版本阶段,而当我们开始搭建一个新项目时,不可避免地要配置 package.json 文件,这个 version 怎么命名呢?

// package.json
{
   "name": "项目名称",
   "version": "0.1.0",
   "description": "项目描述",
   "author": "项目作者",
   "license": "MIT",
}

版本命名规范

在说版本命名规范之前,先说说比较流行的版本命名格式

  • GNU 风格

  • Windows 风格

  • .Net Framework 风格

最常见的版本命名格式就是 GNU 风格,下面以 GNU 风格展开说明

主版本号.子版本号.修正(或阶段)版本号.日期版本号_希腊字母版本号

希腊字母版本号共有5种,分别为:base、alpha、beta、RC、release


版本命名修改规则

项目初版本时,版本号可以为 0.1.0

  • 希腊字母版本号(beta)

    此版本号用于标注当前版本的软件处于哪个开发阶段,当软件进入到另一个阶段时需要修改此版本号

  • 日期版本号

    用于记录修改项目的当前日期,每天对项目的修改都需要更改日期版本号(只有此版本号才可由开发人员决定是否修改)

  • 修正(或阶段)版本号改动

    当项目在进行了局部修改或bug修正时,主版本号和子版本号都不变,修正版本号加1

    // package.json
    {
       "name": "项目名称",
       "version": "0.1.0",
       "version": "0.1.1",
    }
  • 子版本号改动

    当项目在原有基础上增加了某些功能模块时,比如增加了对权限控制、增加自定义视图等功能,主版本号不变,子版本号加1,修正版本号复位为0

    // package.json
    {
       "name": "项目名称",
       "version": "0.1.8",
       "version": "0.2.0",
    }
  • 主版本号改动

    当项目在进行了重大修改或局部修正累积较多,而导致项目整体发生全局变化时,比如增加多个模块或者重构,主版本号加 1

    // package.json
    {
       "name": "项目名称",
       "version": "0.1.0",  // 一期项目
       "version": "1.1.0",  // 二期项目
       "version": "2.1.0",  // 三期项目
    }

文件命名规范

文件名称由四部分组成:第一部分为项目名称,第二部分为文件的描述,第三部分为当前软件的版本号,第四部分为文件阶段标识加文件后缀

例如:xx后台管理系统测试报告1.1.1.031222_beta_d.xls,此文件为xx后台管理系统的测试报告文档,版本号为:1.1.1.031222_beta


  • 如果是同一版本同一阶段的文件修改过两次以上,则在阶段标识后面加以数字标识,每次修改数字加1,xx后台管理系统测试报告1.1.1.031222_beta_d1.xls

  • 当有多人同时提交同一份文件时,可以在阶段标识的后面加入人名或缩写来区别,例如:xx后台管理系统测试报告 1.1.1.031222_beta_d_spp.xls。当此文件再次提交时也可以在人名或人名缩写的后面加入序号来区别,例如:xx后台管理系统测试报告1.1.1.031222_beta_d_spp2.xls

阶段标识

软件的每个版本中包括11个阶段,详细阶段描述如下:

阶段名称阶段标识
需求控制a
设计阶段b
编码阶段c
单元测试d
单元测试修改e
集成测试f
集成测试修改g
系统测试h
系统测试修改i
验收测试j
验收测试修改k

作者:Jesse90s
来源:https://juejin.cn/post/7073902470585384990

收起阅读 »

PAT 乙级 1029 旧键盘:找出键盘上的坏键

题目描述旧键盘上坏了几个键,于是在敲一段文字的时候,对应的字符就不会出现。现在给出应该输入的一段文字、以及实际被输入的文字,请你列出肯定坏掉的那些键。输入格式输入在 2 行中分别给出应该输入的文字、以及实际被输入的文字。每段文字是不超过 80 个字符的串,由字...
继续阅读 »


题目描述

旧键盘上坏了几个键,于是在敲一段文字的时候,对应的字符就不会出现。现在给出应该输入的一段文字、以及实际被输入的文字,请你列出肯定坏掉的那些键。

输入格式

输入在 2 行中分别给出应该输入的文字、以及实际被输入的文字。每段文字是不超过 80 个字符的串,由字母 A-Z(包括大、小写)、数字 0-9、以及下划线 _(代表空格)组成。题目保证 2 个字符串均非空。

输出格式:

按照发现顺序,在一行中输出坏掉的键。其中英文字母只输出大写,每个坏键只输出一次。题目保证至少有 1 个坏键。

输入样例:

7_This_is_a_test
_hs_s_a_es

输出样例:

7TI

思路分析

这道题用哈希表标记输入状态的话会很简单,我就是把字符串中所有字符的ASCII码值映射到数组下标中去,0为未输入,1为已输入。
先将第二个字符串所有字符标记(因为是第二个字符串少了字符),为了方便进行比较和后面的输出,我在标记之前将所有字符统一全部转换成了大写字母
然后再遍历第一个字符串,把所有第一个字符串中存在而第二个字符串里不存在的字符输出,也就是第二个字符串里缺少的字符,不要忘了输出完标记为-1,不然会重复输出哦
PS:其实我刚开始的思路也不是用哈希表,是想用两层循环把两个字符串一个一个字符比较输出,但是还没能实现只输出一遍

AC代码

版本一

#include<bits/stdc++.h>
using namespace std;
int main()
{
   int keyboard[128]={0};//下标映射为ASCII码值,0:未输入,1:已输入
   string s1,s2;
   cin>>s1>>s2;
   for(char c2:s2)
       keyboard[toupper(c2)]=1;//已输入标记为1
   for(char c1:s1)
       if(keyboard[toupper(c1)]==0)
      {
           putchar(toupper(c1));
           keyboard[toupper(c1)]=-1;//该字符已输出,标记为-1,避免重复输出
      }
   return 0;
}

版本2

#include <ctype.h>
#include<iostream>
#include <string>
using namespace std;
int main()
{
   int keyboard[128] = {0};
   string s1;
   char c2;
   getline(cin, s1);//因为后面换行之后还要输入字符所以不能直接用cin
   while((c2 = getchar()) != '\n')//输入第二行字符串
       keyboard[toupper(c2)] = 1;

   for(char c1:s1)
       if(keyboard[toupper(c1)] == 0)
{
           putchar(toupper(c1));
           keyboard[toupper(c1)] = -1;
      }
   return 0;
}

总结

用了一次Hash表,感觉也没有想象的那么困难嘛
看书上写的哈希散列可真是头疼,自己用一了遍反而好像也不难😂
真是纸上得来终觉浅,觉知此事要躬行。

作者:梦境无限
来源:https://juejin.cn/post/7074090679198023688

收起阅读 »

一个匿名内部类的导致内存泄漏的解决方案

泄漏原因匿名内部类默认会持有外部类的类的引用。如果外部类是一个Activity或者Fragment,就有可能会导致内存泄漏。 不过在使用kotlin和java中在匿名内部类中有一些不同。在java中,不论接口回调中是否调用到外部类,生成的匿名内部类都会持有外部...
继续阅读 »

泄漏原因

匿名内部类默认会持有外部类的类的引用。如果外部类是一个Activity或者Fragment,就有可能会导致内存泄漏。 不过在使用kotlin和java中在匿名内部类中有一些不同。

  • 在java中,不论接口回调中是否调用到外部类,生成的匿名内部类都会持有外部类的引用

  • 在kotlin中,kotlin有一些相关的优化,如果接口回调中不调用的外部类,那么生成的匿名内部类不会持有外部类的引用,也就不会造成内存泄漏。 反之,如果接口回调中调用到外部类,生成的匿名内部类就会持有外部类引用

我们可以看一个常见的例子:

class MainActivity : AppCompatActivity() {
  private lateinit var textView: TextView
  override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)
      textView = findViewById(R.id.text)
      test()
  }

  private fun test() {
      val client = OkHttpClient()
      val request = Request.Builder()
          .url("www.baidu.com")
          .build();
      client.newCall(request).enqueue(object : Callback {
          override fun onFailure(call: Call, e: IOException) {}
          override fun onResponse(call: Call, response: Response) {
              textView.text = "1111"
          }
      })
  }
}

在Activity的test方法,发起网络请求,在网络请求成功的回调中操作Activity的textView。当然在这个场景中,Callback返回的线程非主线程,不能够直接操作UI。为了简单的验证内存泄漏的问题,先不做线程切换。 可以看看对应编译后的字节码,这个callback会生成匿名内部类。

public final class MainActivity$test$1 implements Callback {
  final /* synthetic */ MainActivity this$0;

  MainActivity$test$1(MainActivity $receiver) {
      this.this$0 = $receiver;
  }

  public void onFailure(Call call, IOException e) {
      Intrinsics.checkNotNullParameter(call, NotificationCompat.CATEGORY_CALL);
      Intrinsics.checkNotNullParameter(e, "e");
  }

  public void onResponse(Call call, Response response) {
      Intrinsics.checkNotNullParameter(call, NotificationCompat.CATEGORY_CALL);
      Intrinsics.checkNotNullParameter(response, "response");
      TextView access$getTextView$p = this.this$0.textView;
      if (access$getTextView$p != null) {
          access$getTextView$p.setText("1111");
      } else {
          Intrinsics.throwUninitializedPropertyAccessException("textView");
          throw null;
      }
  }
}

默认生成了MainActivity$test$1辅助类,这个辅助类持有了外部Activity的引用。 当真正调用了enqueue时,会把这个请求添加请求的队列中。

private val readyAsyncCalls = ArrayDeque<AsyncCall>()
private val runningAsyncCalls = ArrayDeque<AsyncCall>()

网络请求处于等待中,callback会被添加到readyAsyncCalls队列中, 网络请求处于发起,但是未结束时,callback会被添加到runningAsyncCalls队列中。 只有网络请求结束之后,回调之后,才会从队列中移除。 当页面销毁时,网络请求未成功结束时,就会造成内存泄漏,整个引用链路如下图所示:


网络请求只是其中的一个例子,基本上所有的匿名内部类都可能会导致这个内存泄漏的问题。

解决方案

既然匿名内部类导致的内存泄漏场景这么常见,那么有没有一种通用的方案可以解决这类的问题呢?我们通过动态代理去解决匿名内部类导致的内存泄漏的问题。 我们把Activity和Fragment抽象为ICallbackHolder。

public interface ICallbackRegistry {
  void registerCallback(Object callback);
  void unregisterCallback(Object callback);
  boolean isFinishing();
}

提供了三个能力

  • registerCallback: 注册Callback

  • unregisterCallback: 反注册Callback

  • isFinishing: 当前页面是否已经销毁

在我们解决内存泄漏时需要用到这三个API。

还是以上面网络请求的例子,我们可以通过动态代理来解决这个内存泄漏问题。 先看看使用了动态代理之后的依赖关系图

实线表示强引用 虚线表示弱引用

  • 通过动态代理,将使用匿名内部类与okHttp-Dispatcher进行解耦,okHttp-Dispatcher直接引用的动态代理对象, 动态代理对象不直接依赖原始的callback和activity,而是以弱引用的形式依赖。

  • 此时callback并没有被其他对象强引用,如果不做任何处理,这个callback在对应的方法运行结束之后就可能被回收。

  • 所以需要有一个步骤,将这个callback和对应的Activity、Fragment进行绑定。此时就需要用到前面定义到的ICallbackHolder,通过registerCallback将callback注册到对应Activity、Fragment中。

  • 最后在InvocationHandler中的invoke方法,判断当前的Activity、Fragment是否已经finish了,如果已经finish了,就不再进行回调调,否则进行调用。

  • 回调完成后,如果当前的Callback是否是一次性的,就从callbackList中移除。

接下来可以看看我们怎么通过调用来构建这个依赖关系:

使用CallbackUtil

在创建匿名内部类时,同时传入对应的ICallbackHolder

client.newCall(request).enqueue(CallbackUtil.attachToRegistry(object : Callback {
          override fun onFailure(call: Call, e: IOException) {}
          override fun onResponse(call: Call, response: Response) {
              textView.text = "1111"
          }
      }, this))

创建动态代理对象

动态代理对象对于ICallbackHolder和callback的引用都是弱引用,同时将callback注册到ICallbackHolder中。

private static class MyInvocationHandler<T> extends InvocationHandler {

      private WeakReference<T> refCallback;
      private WeakReference<ICallbackHolder> refRegistry;
      private Class<?> wrappedClass;

      public MyInvocationHandler(T reference, ICallbackRegistry callbackRegistry) {
          refCallback = new WeakReference<>(reference);
          wrappedClass = reference.getClass();
          if (callbackRegistry != null) {
              callbackRegistry.registerCallback(reference);
              refRegistry = new WeakReference<>(callbackRegistry);
          }
      }
}

invoke方法处理

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  ICallbackRegistry callbackRegistry = callbackRegistry != null ? refRegistry.get() : null;
  T callback = refCallback.get();
  Method originMethod = ReflectUtils.getMethod(wrappedClass, method.getName(), method.getParameterTypes());
  if (callback == null || holder != null && holder.isFinishing()) {
      return getMethodDefaultReturn(originMethod);
  }

  if (holder != null && ....)
  {
      holder.unregisterCallback(callback);
  }
  ...
  return method.invoke(callback, args);
  }

在页面销毁时,不回调原始callback。这样,也避免了出现因为页面销毁了之后,访问页面的成员,比如被butterknife标注的view导致的内存泄漏问题。

作者:谢谢谢_xie
来源:https://juejin.cn/post/7074038402009530381

收起阅读 »

奇怪的电梯广搜做法~

一、题目描述:一种很奇怪的电梯,大楼的每一层楼都可以停电梯,而且第 i 层楼(1 ≤ i ≤ N)上有一个数字 Ki (0 ≤ Ki ≤ N)。电梯只有四个按钮:开,关,上,下。上下的层数等于当前楼层上的那个数字。当然,如果不能满足要求,相应的按钮就会失灵。例...
继续阅读 »

一、题目描述:

一种很奇怪的电梯,大楼的每一层楼都可以停电梯,而且第 i 层楼(1 ≤ i ≤ N)上有一个数字 Ki (0 ≤ Ki ≤ N)。电梯只有四个按钮:开,关,上,下。上下的层数等于当前楼层上的那个数字。当然,如果不能满足要求,相应的按钮就会失灵。例如: 3, 3, 1, 2, 5 代表了 Ki(K1=3, K2=3,……),从 1 楼开始。在 1 楼,按“上”可以到 4 楼,按“下”是不起作用的,因为没有 -2 楼。那么,从 A 楼到 B 楼至少要按几次按钮呢?

来源:洛谷 http://www.luogu.com.cn/problem/P11…

输入格式

共二行。 第一行为三个用空格隔开的正整数,表示 N, A, B(1≤N≤200,1≤A,B≤N )。 第二行为 N 个用空格隔开的非负整数,表示 Ki

5 1 5
3 3 1 2 5

输出格式

一行,即最少按键次数,若无法到达,则输出 -1

3

二、思路分析:

首先看一下输入数据是什么意思,首先输入一个N, A, B,也就是分别输入楼层数(N)、开始楼层(A)、 终点楼层(B)。 在例子中,我们的 楼层数N是5,也就是说有5层楼,第二行就是这5层楼的每层楼的数字k。

1、题目中说到只有四个按钮:开,关,上,下,上下的层数等于当前楼层上的那个数字,众所周知,电梯的楼层到了的时候啊,它是会自动打开的,没有人进来的时候,也会自动关上,这里求的是最少按几个按钮,所以我们在这里不用看开关,也就是可以看作该楼层只有两个按钮 +k 、 -k

2、题目中提到最少按几次,很明显,这是一个搜索题。当出现最少的时候,我们就可以考虑用广搜了(也可以用深搜做的啦)

3、这里注意一下,就是我们在不同的按钮次数时遇到停在同一楼层,这时候就会出现一个重复的且没有必要的搜索,所以我们需要在搜索的时候加个条件。

三、AC 代码:

import java.util.*;

public class Main {
public static void main(String[] args) {
Scanner sr = new Scanner (System.in);
int N = sr.nextInt();              
int A = sr.nextInt();            
int B = sr.nextInt();              

// 广搜必备队列
Queue<Fset> Q = new LinkedList<Fset>();
// 一个记忆判断,看看这层楼是不是来过
boolean[] visit = new boolean[N+1];
// 来存楼梯按钮的,假设第3层的k是2, 那么 k[3][0]=2 (向上的按钮)、 k[3][1]=-2 (向下的按钮)
int[][] k = new int [N+1][2];
for(int i = 1 ; i <= N ; i++){
k[i][0] = sr.nextInt();
k[i][1] = -k[i][0];
}

// 存一个起始楼层和按钮次数到队列
Q.add(new Fset(A,0));
// 当队列为空也就是所以能走的路线都走过了,没有找到就可以返回-1了
while(!Q.isEmpty()){
Fset t = Q.poll();
// 找到终点楼层,不用找了直接输出并退出搜索
if(t.floor == B){
System.out.println(t.count);
return;
}
//
for(int j = 0 ; j < 2 ; j++){
// 按键后到的楼层
int f = t.floor + k[t.floor][j];
// 判断按键后到的楼层是否有效和是否走过
if(f >= 1 && f <= N && visit[f]!=true) {
Q.add(new Fset(f,t.count+1));
// 做标记
visit[f]=true;
}  
      }
      }
       // 没找到
       System.out.println(-1);
}
}

class Fset{
int floor; // 当前楼层
int count; // 当前按键次数
public Fset(int floor, int count) {
this.floor = floor;
this.count = count;
}
}


四、总结:

为什么用的队列呢? 因为队列的排队取出的,首先判断的一定是按钮次数最少的,感觉这道题用广搜或者深搜效果其实差不多,我写的深搜多一个判断,就是当当前次数超过我找到的最少按钮次数,我就丢弃这个。 广搜像晕染吧,往四周分散搜索,

嗯,就酱~


作者:d粥
来源:https://juejin.cn/post/7073817170618089479

收起阅读 »

如何在网页中使用响应式图像

或许你已经在网页设计领域见过响应式设计(responsive design)这个术语。响应式设计是让你的网页在不同尺寸的设备屏幕上能得到最佳展示,也就是让你的网页能对各种屏幕尺寸自适应。那么,什么是响应式图像呢?响应式图像与响应式设计有什么关系吗?我们为什么要...
继续阅读 »

或许你已经在网页设计领域见过响应式设计(responsive design)这个术语。

响应式设计是让你的网页在不同尺寸的设备屏幕上能得到最佳展示,也就是让你的网页能对各种屏幕尺寸自适应。

那么,什么是响应式图像呢?

响应式图像与响应式设计有什么关系吗?我们为什么要使用它们?

在本文中,我们就这些问题展开讨论。

什么是响应式图像

如今,图像已成为网页设计中必不可少的元素之一。

绝大多数的网站都会使用图像。

然而你是否知道,尽管你的网站布局可以适应设备尺寸,但显示的图像却不是自适应的?

无论使用何种设备(移动设备、平板或台式机),默认下载的都是相同的图像。

例如,如果图像大小为 2 MB,那么无论在何种设备上,下载的都是 2 MB 的图像数据。

开发者可以编写代码,在移动设备上显示该图像的一部分,但是仍然需要下载整个 2 MB 图像数据。

这是不合时宜的。

如果要为同一个网页下载多个图像,应该如何实现?

手机和平板上的图像本来应该是较小尺寸的,如果下载了大量较大尺寸的图像,肯定会影响性能。

我们需要为不同尺寸的设备提供不同尺寸的图像,移动设备显示小尺寸图像,平板显示中等尺寸的图像,台式机显示大尺寸的图像,该如何实现?

通过使用响应式图像,我们可以避免在较小的设备上下载不必要的图像数据,并提高网站在这些设备上的性能。

让我们看看如何实现这一目标。

HTML 中的响应式图像


以上面的图像为例。

这幅图像是为桌面应用设计的,在小屏幕设备上就需要对图像大小进行压缩,我们可以对这幅图像进行裁剪,而非下载完整的图像。


我们可以在 HTML 中编写以下内容,以便在不同的尺寸屏幕中下载不同的图像。

<img src="racoon.jpg" alt="Cute racoon"
    srcset="small-racoon.jpg 500w,
            medium-racoon.jpg 1000w,
            large-racoon.jpg 1500w" sizes="60vw"/>

让我们看下这段代码的作用。

<img> 标签负责在 HTML 中渲染图像,而 src 属性告诉浏览器默认显示哪个图像。在这种情况下,如果浏览器不支持 srcset 属性,则默认为 src 属性。

在这段代码中 srcset 属性是最重要的属性之一。

srcset 属性通知浏览器图像的合适宽度,浏览器不需要下载所有图像。通过 srcset 属性,浏览器决定下载哪个图像并且适应该视口宽度。

你可能还注意到 srcset 中每个图像大小的 w 描述符。

srcset="small-racoon.jpg 500w,
      medium-racoon.jpg 1000w,
      large-racoon.jsp 1500w"

上面代码片段中的 w 指定了 srcset 中图像的宽度(以像素为单位)。

还有一个 sizes 属性,它通知浏览器具有 srcset 属性的 <img> 元素的大小。

sizes="60vw"

在这里,sizes 属性的值为 60 vw,它告诉浏览器图像的宽度为视口的 60%size 属性帮助浏览器从 srcset 中为该视口宽度选择最佳图像。

例如,如果浏览器视口宽度为 992 px,那么

992 px60%

= 592 px

根据上面的计算,浏览器将选择宽度为 500 w500 px,最接近 592 px 的图像显示在屏幕上。

最终由浏览器决定选择哪个图像。

注意,为不同视口宽度选择图像的决策逻辑可能因浏览器而异,你可能会看到不同的结果。

为较小的设备下载较少的图像数据,可以让浏览器快速显示这些图像,从而提高网站的性能。

本文总结

网站加载缓慢的最主要原因是下载了 MB 级数据的图像。

使用响应式图像可以避免下载不必要的图像数据,从而减少网站的加载时间并提供更好的用户体验。

唯一的缺点是我们放弃了对浏览器的完全控制,让浏览器选择要在特定视口宽度下显示的图像。

每个浏览器都有不同的策略来选择适当的响应式图像。这就是为什么你可能会在不同的浏览器中,看到以相同分辨率加载的不同图像。

放弃对浏览器的控制,根据视口宽度显示图像以获得性能优势,你需要在实际应用时做权衡考虑。


以上就是本文全部内容,我希望通过本文,你能对响应式图像有进一步的了解,知道为什么应该考虑将它们应用于网站。

如果你有任何问题、建议或意见,请随时在下面的评论区留言分享。

感谢你的阅读!

本文参考:

Image Optimization — Addy Osmani

原文地址:What Are Responsive Images And Why You Should Use Them
原文作者:Nainy Sewaney
译者:Z招锦

收起阅读 »

聊聊我常用的两个可视化工具,Echarts和Tableau

由于工作里常常要做图表,Excel没法满足复杂场景,所以Echarts和Tableau成为了我最得力的两个助手。作为声名远扬的可视化工具,Echarts和Tableau,它们的性质不太一样。Echarts是一个纯JavaScript 的开源可视化图表库,使用者...
继续阅读 »

由于工作里常常要做图表,Excel没法满足复杂场景,所以Echarts和Tableau成为了我最得力的两个助手。

作为声名远扬的可视化工具,Echarts和Tableau,它们的性质不太一样。

Echarts是一个纯JavaScript 的开源可视化图表库,使用者只需要引用封装好的JS,就可以展示出绚丽的图表。

就在前不久,Echarts成为了Apache的顶级项目。Apache顶级项目的家族成员有哪些呢?Mavan、Hadoop、Spark、Flink…都是软件领域的顶流

Tableau是一个BI工具,商业化的PC端应用,只需要拖拉拽就可以制作丰富多样的图表、坐标图、仪表盘与报告。Tableau制作的可视化项目可以发布到web上,分享给其他人。

2019年,Tableau被Salesforce斥157 亿美元收购,可见这个BI工具不一般。

你可以把Echarts看成一个可视化仓库,每个可视化零件拿来即用,而且不限场合。而Tableau则像一个自给自足的可视化生态,你能在里面玩转各种可视化神技,但不能出这个生态。

先来说说Echarts

Echarts几乎提供了你能用到的所有图表形式,而且对国内开发环境非常友好,因为它是百度鼓捣出来的。


你看,不仅有常规的统计图表:

还有炫酷的3D可视化

Echarts大部分图表形式都封装到JS中,你只需要更改数据和样式,就可以应用到自己的项目中。


Echarts还有个用户社区,里面有非常多的作品展示,大家可以去逛逛。


某个热门作品-区域地图


学习Echarts最好是看官网教程,再配合练习。中文文档非常接地气。


给出几个常用的学习地址

官方文档:

https://echarts.apache.org/zh/tutorial.html

官方示例:

https://echarts.apache.org/examples/zh/index.html

用户作品专区:

https://www.makeapie.com/explore.html

再来说说Tableau

Tableau目前在国内慢慢流行起来,说起来做数据的小伙伴都会知道。

它适合做可视化看板,讲数据故事,符合现在数字化运营的管理。


这里简单介绍下Tableau的使用方法。

首先在Tableau官网下载desktop,然后无脑安装。

接下来新手操作三大步:

1、连接数据

可以连接excel、csv以及mysql等各种数据库


2、了解什么是度量和维度

度量就是数据表中的数值数据,维度是类别数据


3、看看tableau中的各类图表

柱状图、点图、线图、饼图、直方图、地图等等


走完基础后,就是整个的可视化分析展示流程:


其中的各个步骤需要详细说明一下:

  • 1、连接到数据源

Tableau连接到所有常用的数据源。它具有内置的连接器,在提供连接参数后负责建立连接。无论是简单文本文件,关系源,无Sql源或云数据库,tableau几乎连接到所有数据源。

  • 2、构建数据视图

连接到数据源后,您将获得Tableau环境中可用的所有列和数据。您可以将它们分为维,度量和创建任何所需的层次结构。使用这些,您构建的视图传统上称为报告。Tableau提供了轻松的拖放功能来构建视图。

  • 3、增强视图

上面创建的视图需要进一步增强使用过滤器,聚合,轴标签,颜色和边框的格式。

  • 4、创建工作表

我们创建不同的工作表,以便对相同的数据或不同的数据创建不同的视图。

  • 5、创建和组织仪表板

仪表板包含多个链接它的工作表。因此,任何工作表中的操作都可以相应地更改仪表板中的结果。

  • 6、创建故事

故事是一个工作表,其中包含一系列工作表或仪表板,它们一起工作以传达信息。您可以创建故事以显示事实如何连接,提供上下文,演示决策如何与结果相关,或者只是做出有说服力的案例。

完成这些,一张生动的dashboard就诞生了。


这其中,需要不断地练习熟稔tableau的每一个组件、函数、连接等等。

我们可以选择合适的可视化表达,让Tableau实现。


不要以为Tableau只提供简单的几种样式,如果你想做出炫酷的图表,Tableau也能完美支持。

看看大神们是怎么玩转Tabelau的。


还有一张我最喜欢的dashboard


因为Tableau是商业软件,所以它的官网中文教程非常详细。

最后也给到Tableau的几个学习地址

官方文档:

https://help.tableau.com/current/pro/desktop/zh-cn/default.htm

用户展示社区:

https://public.tableau.com/zh-cn/gallery

最后

如果是你想做可视化开发建议用echarts,如果想设计商业可视化报表则用Tableau。

欢迎留言区交流你做可视化的经验。

作者:朱卫军
来源:Python大数据分析

收起阅读 »

Android 线程间通信 - Java 层 - MessagePool

线程间通信在 Android 系统中应用十分广泛,本文是一个系列文章,主要梳理了Android Java层的线程间通信机制-Handler 还是以一个简单的Handler示例开头 public void egHandler() { Looper.pr...
继续阅读 »

线程间通信在 Android 系统中应用十分广泛,本文是一个系列文章,主要梳理了Android Java层的线程间通信机制-Handler




还是以一个简单的Handler示例开头


public void egHandler() {
Looper.prepare();
Looper.loop();

Handler handler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
// TODO; 处理 Event
}
};

new Thread() {
@Override
public void run() {
Message msg = Message.obtain(); // 注意: 此处和上篇文章中的示例不同
msg.what = 0;
handler.sendMessage(msg);
}
}.start();
}

为了避免重复的创建以及销毁 Message 对象带来的系统开销,Google 团队在Message.java中搞了一个MessagePool,在MessagePool中缓存了定量的Message对象


上面示例中使用的Message.obtain()是从Pool中拿到一个message对象。


MessagePool


源码路径:frameworks/base/core/java/android/os/Message.java


在Message.java中定义了四个成员变量


// MessagePool 同步锁
public static final Object sPoolSync = new Object();
// MessagePool Node节点
private static Message sPool;
// MessagePool Node个数
private static int sPoolSize = 0;
// MessagePool 最大容量
private static final int MAX_POOL_SIZE = 50;

从MessagePool中获取一个message对象,需要调用obtain方法,该方法重载了多个


obtain message


Message.obtain()


public static Message obtain() {
// 上锁
synchronized (sPoolSync) {
if (sPool != null) {
// 从sPool链表拿出头节点
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}

Message.obtain(Message orig)


public static Message obtain(Message orig) {
// 获取一个message
Message m = obtain();
// 将m和形参orig同步
m.what = orig.what;
m.arg1 = orig.arg1;
m.arg2 = orig.arg2;
m.obj = orig.obj;
m.replyTo = orig.replyTo;
m.sendingUid = orig.sendingUid;
m.workSourceUid = orig.workSourceUid;
if (orig.data != null) {
m.data = new Bundle(orig.data);
}
m.target = orig.target;
m.callback = orig.callback;

return m;
}

Message.obtain(Handler h)


public static Message obtain(Handler h) {
Message m = obtain();
// 单独为新msg设置handler
m.target = h;

return m;
}

另外还有一些构造方法,整体实现思路差不多。


recycle message


Message.recycle


public void recycle() {
if (isInUse()) {
// InUse中被释放会抛出异常
if (gCheckRecycle) {
throw new IllegalStateException("This message cannot be recycled because it "
+ "is still in use.");
}
return;
}
// 释放
recycleUnchecked();
}

Message.recycleUnchecked


@UnsupportedAppUsage
void recycleUnchecked() {
// Mark the message as in use while it remains in the recycled object pool.
// Clear out all other details.
// 初始化flag,标记为InUse
flags = FLAG_IN_USE;
what = 0;
arg1 = 0;
arg2 = 0;
obj = null;
replyTo = null;
sendingUid = UID_NONE;
workSourceUid = UID_NONE;
when = 0;
target = null;
callback = null;
data = null;

// 上锁,将需要释放的msg插入到链表头
synchronized (sPoolSync) {
// 注意这里,回收的过程中会判断当前链表的长度是否大于最大长度
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}

recycle与recycleUnchecked的区别在于:



  • recycleUnchecked 不会检查当前msg是否在使用中,而是会直接回收掉该msg,放在链表的头部

  • recycle 会判断当前的msg是否在使用,在使用中会抛出异常,非使用中直接调用recycleUnchecked进行回收


另外,recycleUnchecked不支持app是否,App中只能是否recycle


Message.isInUse


// Message 中两个成员变量
/*package*/ static final int FLAG_IN_USE = 1 << 0;

/** If set message is asynchronous */
/*package*/ static final int FLAG_ASYNCHRONOUS = 1 << 1;

/*package*/ boolean isInUse() {
return ((flags & FLAG_IN_USE) == FLAG_IN_USE);
}

Message.markInUse


@UnsupportedAppUsage
/*package*/ void markInUse() {
flags |= FLAG_IN_USE;
}

这个flag的判断方式我有点不理解,还不如直接用数字判断,例如flag = 1就是InUse,难道是为了判断的更快一点采用|、&的方式?


理解为什么这么做的大佬在下面评论一下,不吝指出。


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

❤️Android 快别用Toast了,来试试Snackbar❤️

🔥 应用场景 Toast提示默认显示在界面底部,使用Toast.setGravity()将提示显示在中间,如下: Toast toast = Toast.makeText(this, str, Toast.LENGTH_SHORT); ...
继续阅读 »

🔥 应用场景


Toast提示默认显示在界面底部,使用Toast.setGravity()将提示显示在中间,如下:


        Toast toast = Toast.makeText(this, str, Toast.LENGTH_SHORT);
toast.setGravity(Gravity.CENTER, 0, 0);
toast.show();

运行在在Android 12上无法显示,查看Logcat提示如下:


Toast: setGravity() shouldn't be called on text toasts, the values won't be used

意思就是:你不能使用toast调用setGravity,调用无效。哎呀,看给牛气的,咱看看源码找找原因


🔥 源码


💥 Toast.setGravity()


    /**
* 设置Toast出现在屏幕上的位置。
*
* 警告:从 Android R 开始,对于面向 API 级别 R 或更高级别的应用程序,此方法在文本 toast 上调用时无效。
*/
public void setGravity(int gravity, int xOffset, int yOffset) {
if (isSystemRenderedTextToast()) {
Log.e(TAG, "setGravity() shouldn't be called on text toasts, the values won't be used");
}
mTN.mGravity = gravity;
mTN.mX = xOffset;
mTN.mY = yOffset;
}

妥了,人家就告诉你了 版本>=Android R(30),调用该方法无效。无效就无效呗,还不给显示了,过分。


Logcat的提示居然是在这里提示的,来都来了,咱们看看isSystemRenderedTextToast()方法。


💥 Toast.isSystemRenderedTextToast()


    /**
*Text Toast 将由 SystemUI 呈现,而不是在应用程序内呈现,因此应用程序无法绕过后台自定义 Toast 限制。
*/
@ChangeId
@EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
private static final long CHANGE_TEXT_TOASTS_IN_THE_SYSTEM = 147798919L;

private boolean isSystemRenderedTextToast() {
return Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM) && mNextView == null;
}

重点了。Text Toast 将由 SystemUI 呈现,而不是在应用程序内呈现。


清晰明了,可以这样玩,但是你级别不够,不给你玩。


事情整明白了,再想想解决解决方案。他说了Text Toast 将由 SystemUI 呈现,那我不用 Text 不就行了。


🔥 Toast 提供的方法


先看看Tast提供的方法:



有这几个方法。咱们实践一下。保险起见看看源码


💥 Toast.setView() 源码


    /**
* 设置显示的View
* @deprecated 自定义 Toast 视图已弃用。 应用程序可以使用 makeText 方法创建标准文本 toast,
* 或使用 Snackbar
*/
@Deprecated
public void setView(View view) {
mNextView = view;
}

这个更狠,直接弃用。




  • 要么老老实实的用默认的Toast。




  • 要么使用 Snackbar。




🔥 Snackbar


Snackbar 就是一个类似Toast的快速弹出消息提示的控件(我是刚知道,哈哈)。


与Toast相比:




  • 一次只能显示一个




  • 与用户交互



    • 在右侧设置按钮来添加事件,根据 Material Design 的设计原则,只显示 1 个按钮 (添加多个,以最后的为准)




  • 提供Snackbar显示和关闭的监听事件



    • BaseTransientBottomBar.addCallback(BaseCallback)




💥 代码实现


    showMessage(findViewById(android.R.id.content), str, Snackbar.LENGTH_INDEFINITE);

public static void showMessage(View view, String str, int length) {
Snackbar snackbar = Snackbar.make(view, str, length);

View snackbarView = snackbar.getView();
//设置布局居中
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(snackbarView.getLayoutParams().width, snackbarView.getLayoutParams().height);
params.gravity = Gravity.CENTER;
snackbarView.setLayoutParams(params);
//文字居中
TextView message = (TextView) snackbarView.findViewById(R.id.snackbar_text);
//View.setTextAlignment需要SDK>=17
message.setTextAlignment(View.TEXT_ALIGNMENT_GRAVITY);
message.setGravity(Gravity.CENTER);
message.setMaxLines(1);
snackbar.addCallback(new BaseTransientBottomBar.BaseCallback<Snackbar>() {
@Override
public void onDismissed(Snackbar transientBottomBar, int event) {
super.onDismissed(transientBottomBar, event);
//Snackbar关闭
}

@Override
public void onShown(Snackbar transientBottomBar) {
super.onShown(transientBottomBar);
//Snackbar显示
}
});
snackbar.setAction("取消", new View.OnClickListener() {
@Override
public void onClick(View v) {
//显示一个默认的Snackbar。
Snackbar.make(view, "我先走", BaseTransientBottomBar.LENGTH_LONG).show();
}
});
snackbar.show();
}

Snackbar.make的三个参数:



  • View:从View中找出当前窗口最外层视图,然后在其底部显示。

  • 第二个参数(text)

    • CharSequence

    • StringRes



  • duration(显示时长)

    • Snackbar.LENGTH_INDEFINITE 从 show()开始显示,直到它被关闭或显示另一个 Snackbar

    • Snackbar.LENGTH_SHORT 短时间

    • Snackbar.LENGTH_LONG 长时间

    • 自定义持续时间 以毫秒为单位




💥 效果


Android 12



Android 5.1



💥 工具类


如果觉得设置麻烦可以看看下面这边文章,然后整合一套适合自己的。


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

装了这几个IDEA插件,基本上一站式开发了!

前言 前几天有社区小伙伴私聊我,问我都用哪些IDEA插件,我的IDEA的主题看起来不错。 作为一个开源作者,每周要code大量的代码,提升日常工作效率是我一直追求的,在众多的IDEA插件中,我独钟爱这几款。这期就整理一些我日常中经常使用的IDEA插件,这些插件...
继续阅读 »

前言


前几天有社区小伙伴私聊我,问我都用哪些IDEA插件,我的IDEA的主题看起来不错。


作为一个开源作者,每周要code大量的代码,提升日常工作效率是我一直追求的,在众多的IDEA插件中,我独钟爱这几款。这期就整理一些我日常中经常使用的IDEA插件,这些插件有些挺小众,但是的确非常提升效率,推荐给大家。


Vuesion Theme


首先推荐是一款皮肤,每天对着看IDEA,默认的皮肤黑白两色,我个人总觉得白色太刺眼,黑色的有点太黑了,代码高亮也不好看,长时间难免看着有点审美疲劳。


颜值是生产力的第一要素,主题整好了,整个心情也好一点,心情好,自然bug就少点。。。是这个道理么?


在众多的IDEA的主题中,我钟爱这一款。非常适中的UI颜色,漂亮的代码高亮主题。看了半年多了,都没有审美疲劳。



废话不多说,直接看代码主题效果:



我知道每个人审美有所不同,有的小伙伴会说,我就是喜欢默认的暗黑色。okay啦,我只代表个人喜好。这里不杠。


Atom Material ICons


第二款推荐的是一款ICON插件,相信也有很多小伙伴也有用。


其实这个Icon虽然不难看,但是我也没觉得多好看。那我为什么还要特意推荐??


因为这款ICon插件附加了一个buff。。。这是我当时如何也想不通的。😂



部分效果如下:




其实不难看,那我就要说说这个icon插件附带的buff了。


idea在macOs下,无论是我用2018款的Macbook pro还是现在的Macbook pro m1版本,总感觉在拖动滚动条或是鼠标中键滚屏时有点卡顿,并不是电脑性能的问题,我在网上看到有其他小伙伴也遇到了这种情况。应该是idea对MacOs系统的优化问题。


我尝试过增大Idea的jvm缓存,尝试过优化参数。都无果,后来偶然一次机会在某个论坛上看到有一个人说,装了这个Icon插件之后就变的丝滑无比了,但不知道为啥。我抱着怀疑的态度装了下,卧槽,瞬间丝滑了。虽然我也不懂这是为什么,但是解决问题了之后这个Icon插件就变成必备插件了。如果有小伙伴遇到我想同的问题的话,那么请尝试。


这个buff是不是很强大呢。


File Expander


有了这个插件,有些小伙伴平时用的Jad工具就可以扔了,它能在Idea里直接打开Jar包,并且反编译代码查看。甚至于能打开tar.gz,zip等压缩格式。


这里补充下,你项目里之所以不需要装插件就能看jar包里的代码,是因为jar在你的classpath内。如果单独打开一个jar包,不装插件是看不了的。





GitToolBox


这款插件现在我几乎离不开它。


他能在项目上提示你还有多少文件没提交,远程还有多少文件没更新下来。还能在每一行代码上提示上次提交的时间。查版本提交问题的时候尤其方便。





Maven Helper


这个我想应该是所有使用Idea开发者的标配插件了吧。


我经常使用到的功能便是可视化依赖书,可以清晰的知道,哪个Jar包传递依赖了什么,哪个jar包什么版本和什么版本冲突了。


排查Jar包依赖等问题用这个简直是神器。这个插件也提供了一些其他的快捷命令,右键直接唤起maven命令,颇为方便。




Translation


源码中很多注解都是英文,有时候看着有点费劲。这款翻译插件基本上与Idea一体化,从集成度和方便程度来说,可以吊打其他的第三方翻译软件了。不需要你切换窗口,直接一个快捷键就可以翻译整段文本了。


关键是这个插件的翻译引擎可以与多个翻译接口集成对接,支持google翻译,有道翻译,百度翻译,阿里翻译。实时进行精准快速的翻译,自动识别语言。帮助你在阅读源码里的英文时理解的更加透彻。





arthas idea


Arthas是阿里开源的一款强大的java在线诊断工具,做java开发的小伙伴一定很熟悉。


这个工具几乎已经成为诊断线上java应用的必备工具了。


但是每次需要输入很长一段命令,有些命令遗忘的话,还要去翻看Arthas的命令文档,然后还要复制代码中类或方法的全路径,很是不方便。而这款arthas的插件就可以让你完全摆脱这些苦恼。生产力大大提升。



使用起来非常方便,进入代码片段,选择你要诊断的类或者方法上面,右击打开Arthas命令,选择一项,即可自动生成命令,省去你敲打命令的时间。



Search In Repository


平时我们如果要依赖一个第三方jar包,但是不知道它的maven/gradle的坐标。我们该怎么做?


搓点的做法基本上就是baidu了,稍微高级点的就是到中央仓库去查下,最新版本的坐标是什么。然后复制下来,贴到pom里去。


这款插件,就无需你来回跳转,直接把中央仓库的查找集成到了Idea里面。你只需要打开这款插件,输入jar包的名字或者gav关键字,就能查到到这个jar包所有的版本,然后可以直接复制gav坐标。方便又快捷,干净又卫生!




VisualGC


不知道大家去诊断JVM堆栈用什么工具呢,是不是大部分都是用jdk的原生工具呢。


这里推荐大家一个Idea堆栈的可视化工具,和Idea深度集成。直接显示所有进程,双击即可打开JVM的堆栈可视化界面。堆栈和垃圾收集情况一目了然!




Zoolytic


一款zookeeper节点的查看分析插件。其实第三方也有一些zk的节点信息查看工具,但是我都觉得不够方便,直到我发现了这款插件。


idea里面直接可以看zookeeper的节点信息,非常方便。




最后


以上这10款Idea插件是我平时中用的非常多且经过筛选的,因为有些大家耳熟能详就不介绍了,相信小伙伴们都有装。


希望大家能从以上插件中找到适合自己的那几款,或者有更好更效率的插件,也可以评论里留言。


我是铂赛东,是开一个开源作者和内容博主,热爱生活和分享。如果你对我的内容感兴趣,请关注元人部落。


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