注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

APP与H5通信-JsBridge

背景 在移动开发领域,原生应用嵌入网页(H5)可以实现一套代码多端使用,那么原生应用(APP)和网页(H5)之间的通信就非常重要。 JsBridge作为一种实现此类通信的工具,用于实现原生应用和嵌入其中的网页之间的通信。 H5与native交互,本质上来说就两...
继续阅读 »

背景


在移动开发领域,原生应用嵌入网页(H5)可以实现一套代码多端使用,那么原生应用(APP)和网页(H5)之间的通信就非常重要。


JsBridge作为一种实现此类通信的工具,用于实现原生应用和嵌入其中的网页之间的通信。


H5与native交互,本质上来说就两种调用:



  1. JavaScript 调用 native 方法

  2. native 调用 JavaScript 方法


JavaScript调用native方法有两种方式:



  1. 注入,native 往 webview 的 window 对象中添加一些原生方法,h5可以通过注入的方法来调用 app 的原生能力

  2. 拦截,H5通过与 native 之间的协议发送请求,native拦截请求再去调用 app 原生能力


本文主要介绍H5端与App(android和ios)之间通信使用方式。


代码实现


实现步骤:


这段代码实现的是 APP(Android 和 iOS) 和 H5 之间的通信。这个通信过程主要依赖于 WebViewJavascriptBridge 这个桥接库。这里是具体的流程:



  1. 初始化 WebViewJavascriptBridge 对象:



    • 对于 Android,如果 WebViewJavascriptBridge 对象已经存在,则直接使用;如果不存在,则在 'WebViewJavascriptBridgeReady' 事件触发时获取 WebViewJavascriptBridge 对象。

    • 对于 iOS,如果 WebViewJavascriptBridge 对象已经存在,直接使用;如果不存在,则创建一个隐藏的 iframe 来触发 WebViewJavascriptBridge 的初始化,并在初始化完成后通过 WVJBCallbacks 回调数组来获取 WebViewJavascriptBridge 对象。



  2. 注册事件:


    提供了 callHandlerregisterHandler 两个方法,分别用于在 JS 中调用 APP 端的方法和注册供 APP 端调用的 JS 方法。


  3. 调用方法:


    当 APP 或 JS 需要调用对方的方法时,只需调用 callHandlerregisterHandler 方法即可。



const { userAgent } = navigator;
const isAndroid = userAgent.indexOf('android') > -1; // android终端

/**
* Android 与安卓交互时:
* 1、不调用这个函数安卓无法调用 H5 注册的事件函数;
* 2、但是 H5 可以正常调用安卓注册的事件函数;
* 3、还必须在 setupWebViewJavascriptBridge 中执行 bridge.init 方法,否则:
* ①、安卓依然无法调用 H5 注册的事件函数
* ①、H5 正常调用安卓事件函数后的回调函数无法正常执行
*
* @param {*} callback
*/

function androidFn(callback) {
if (window.WebViewJavascriptBridge) {
callback(window.WebViewJavascriptBridge);
} else {
document.addEventListener(
'WebViewJavascriptBridgeReady',
() => {
callback(window.WebViewJavascriptBridge);
},
false,
);
}
}

/**
* IOS 与 IOS 交互时,使用这个函数即可,别的操作都不需要执行
*/

function iosFn(callback) {
if (window.WebViewJavascriptBridge) { return callback(window.WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
const WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__BRIDGE_LOADED__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(() => { document.documentElement.removeChild(WVJBIframe); }, 0);
}

/**
* 注册 setupWebViewJavascriptBridge 方法
* 之所以不将上面两个方法融合成一个方法,是因为放在一起,那么就只有 iosFuntion 中相关的方法体生效
*/

const setupWebViewJavascriptBridge = isAndroid ? androidFn : iosFn;

/**
* 这里如果不做判断是不是安卓,而是直接就执行下面的方法,就会导致
* 1、IOS 无法调用 H5 这边注册的事件函数
* 2、H5 可以正常调用 IOS 这边的事件函数,并且 H5 的回调函数可以正常执行
*/

if (isAndroid) {
/**
* 与安卓交互时,不调用这个函数会导致:
* 1、H5 可以正常调用 安卓这边的事件函数,但是无法再调用到 H5 的回调函数
*
* 前提 setupWebViewJavascriptBridge 这个函数使用的是 andoirFunction 这个,否则还是会导致上面 1 的现象出现
*/

setupWebViewJavascriptBridge((bridge) => {
console.log('打印***bridge', bridge);
// 注册 H5 界面的默认接收函数(与安卓交互时,不注册这个事件无法接收回调函数)
bridge.init((message, responseCallback) => {
responseCallback('JS 初始化');
});
});
}

export default {
// js调APP方法 (参数分别为:app提供的方法名 传给app的数据 回调)
callHandler(name, params, callback) {
setupWebViewJavascriptBridge((bridge) => {
bridge.callHandler(name, params, callback);
});
},

// APP调js方法 (参数分别为:js提供的方法名 回调)
registerHandler(name, callback) {
setupWebViewJavascriptBridge((bridge) => {
bridge.registerHandler(name, (data, responseCallback) => {
callback(data, responseCallback);
});
});
},
};

使用 JSBridge 总结:


1、跟 IOS 交互的时候,只需要且必须注册 iosFuntion 方法即可,不能在 setupWebViewJavascriptBridge 中执行 bridge.init 方法,否则 IOS 无法调用到 H5 的注册函数;


2、与安卓进行交互的时候



  • 使用 iosFuntion,就可以实现 H5 调用 安卓的注册函数,但是安卓无法调用 H5 的注册函数,
    并且 H5 调用安卓成功后的回调函数也无法执行

  • 使用 andoirFunction 并且要在 setupWebViewJavascriptBridge 中执行 bridge.init 方法,
    安卓才可以正常调用 H5 的回调函数,并且 H5 调用安卓成功后的回调函数也可以正常执行了


H5使用


h5获取app返回的数据:


jsBridge.callHandler('getAppUserInfo', { title: '首页' }, (data) => {
console.log('获取app返回的数据', data);
});

app获取h5返回的数据:


 jsBridge.registerHandler('getInfo', (data, responseCallback) => {
console.log('打印***get app data', data);
responseCallback('我是返回的数据');
});


两者都可通信,只要一方使用registerHandler注册了事件,另一方通过callHandler接受数据


总结


主要介绍了原生应用嵌入网页(H5)与APP(android和ios)之间的通信实现方法。


这个通信过程主要依赖于 WebViewJavascriptBridge 这个桥接库。通过在JavaScript中调用native方法和native调用JavaScript方法,实现APP和H5的互通。


主要通过提供了 callHandlerregisterHandler 两个方法,分别用于在 JS 中调用 APP 端的方法和注册供 APP 端调用的 JS 方法。


更简单方式: APP与H5通信-postMessage


参考资料:


ios-webview


android-webview


参考案例


作者:一诺滚雪球
来源:juejin.cn/post/7293728293768855587
收起阅读 »

Flutter 用什么架构方式才合理?

前言 刚入门 Flutter 编程时,差点被 Flutter 的嵌套地狱吓走,不过当我看到 Flutter 支持 Windows 稳定后,于是下定决心尝试接受 Flutter,因为 Flutter 真的给的太多了:跨平台、静态编译、热加载界面。 Flutter...
继续阅读 »

前言


刚入门 Flutter 编程时,差点被 Flutter 的嵌套地狱吓走,不过当我看到 Flutter 支持 Windows 稳定后,于是下定决心尝试接受 Flutter,因为 Flutter 真的给的太多了:跨平台、静态编译、热加载界面。


Flutter 代码是写到文件夹中的,通过文件夹来管理代码,像是 c++ 语言那样,一个文件,即可以写类,也可以直接写方法😠。


不像 java 那样,全部都是类,整齐划一,通过包名来管理,但也支持类似的“导包”😆。


那么怎样才能像 Java 那样,有个框架优化代码,让项目看起来更整洁好维护呢?


我目前的答案是 MVC 🐷,合适自己的架构才是最好的架构,用这个架构,我感觉找到了家,大家先看看我的代码,然后再做评价。


使用部分


结合GetX, 使用方式如下:


import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:wenznote/commons/mvc/controller.dart';
import 'package:wenznote/commons/mvc/view.dart';

class CustomController extends MvcController {
var count = 0.obs;

void addCount() {
count.value++;
}
}

class CustomView extends MvcView<CustomController> {
const CustomView({super.key, required super.controller});

@override
Widget build(BuildContext context) {
return Center(
child: Column(
children: [
Obx(() => Text("点击次数:${controller.count.value}")),
TextButton(
onPressed: () {
controller.addCount();
},
child: Text("点我"),
),
],
),
);
}
}

简单粗暴,直接在 CustomView 中设计 UI, 在 CustomController 中编写业务逻辑代码,比如登录注册之类的操作。


至于 MVC 中的 Model 去哪里了?你猜猜😘。


代码封装部分


代码封装也很简洁,封装的 controller 代码如下


import 'package:flutter/material.dart';

class MvcController with ChangeNotifier {
late BuildContext context;

@mustCallSuper
void onInitState(BuildContext context) {
this.context = context;
}

@mustCallSuper
void onDidUpdateWidget(BuildContext context, MvcController oldController) {
this.context = context;
}

void onDispose() {}
}

封装的 view 代码如下


import 'package:flutter/material.dart';
import 'controller.dart';

typedef MvcBuilder<T> = Widget Function(T controller);

class MvcView<T extends MvcController> extends StatefulWidget {
final T controller;
final MvcBuilder<T>? builder;

const MvcView({
super.key,
required this.controller,
this.builder,
});

Widget build(BuildContext context) {
return builder?.call(controller) ?? Container();
}

@override
State<MvcView> createState() => _MvcViewState();
}

class _MvcViewState extends State<MvcView> with AutomaticKeepAliveClientMixin{
@override
bool get wantKeepAlive => true;

@override
void initState() {
super.initState();
widget.controller.onInitState(context);
widget.controller.addListener(onChanged);
}

void onChanged() {
if (context.mounted) {
setState(() {});
}
}

@override
Widget build(BuildContext context) {
super.build(context);
widget.controller.context = context;
return widget.build(context);
}

@override
void didUpdateWidget(covariant MvcView<MvcController> oldWidget) {
super.didUpdateWidget(oldWidget);
widget.controller.onDidUpdateWidget(context, oldWidget.controller);
}


@override
void dispose() {
widget.controller.removeListener(onChanged);
widget.controller.onDispose();
super.dispose();
}
}

结语


MVC 可以很简单快速的将业务代码和 UI 代码隔离开,改逻辑的时候就去找 Controller 就行,改 UI 的话就去找 View 就行,和后端开发一样的思路,完成作品就行。


附上的作品文件结构截图,亲喷哈~


04ab4670-d62d-11ee-b1e9-af9546993c52.png


感谢大家的关注与支持,后续继续更新更多 flutter 跨平台开发知识,例如:MVC 架构中的 Controller 应该在哪里创建?Controller 中的 Service 应该在哪里创建?


作品地址:github.com/lyming99/we…


作者:果冻橙橙君
来源:juejin.cn/post/7340472228927914024
收起阅读 »

uni-app开发小程序:项目架构以及经验分享

uni-app开发小程序:项目架构以及经验分享 2022年的时候,公司为了快速完成产品并上线,所以选用微信小程序为载体;由于后期还是打算开发App;虽然公司有ios和Android,但是如果能一套代码打包多端,一定程度上可以解决成本;前端技术栈也是vue,在...
继续阅读 »

uni-app开发小程序:项目架构以及经验分享



2022年的时候,公司为了快速完成产品并上线,所以选用微信小程序为载体;由于后期还是打算开发App;虽然公司有iosAndroid,但是如果能一套代码打包多端,一定程度上可以解决成本;前端技术栈也是vue,在考察选择了uni-app。后来多个小程序项目都采用了uni-app开发,积累了一定的经验以及封装了较多业务组件,这里就分享一下uni-app项目的整体架构、常用方法封装以及注意事项。全文代码都会放到github,先赞后看,年入百万!



创建项目


uni-app提供了两种创建项目的方式:




⚠️需要注意的是,一定要根据项目需求来选择项目的创建方式;如果只是单独的开发小程序App,且开发环境单一,可以使用HBuilderX可视化工具创建。如果多端开发,以及同一套代码可能会打包生成多个小程序建议使用vue-cli进行创建,不然后期想搞自动化构建以及按指定条件进行编译比较痛苦。关于按条件编译,文章后面会有详细说明。



使用vue-cli安装和运行比较简单:


1.全局安装 vue-cli


npm install -g @vue/cli

2.创建uni-app


vue create -p dcloudio/uni-preset-vue 项目名称

3.进入项目文件夹


cd 项目名称

4.运行项目,如果是已微信小程序为主,可以在package.json中的命令改为:


"scripts": {
"serve": "npm run dev:mp-weixin"
}

然后执行


npm run serve

使用cli创建项目默认不带css预编译,需要手动安装一下,这里已sass为例:


npm i sass --save-dev
npm i sass-loader --save-dev

整体项目架构


通过HBuilderX或者vue-cli创建的项目,目录结构有稍许不同,但基本没什么差异,这里就按vue-cli创建的项目为例,整体架构配置如下:


    ├──dist 编译后的文件路径
├──package.json 配置项
├──src 核心内容
├──api 项目接口
├──components 全局公共组件
├──config 项目配置文件
├──pages 主包
├──static 全局静态资源
├──store vuex
├──mixins 全局混入
├──utils 公共方法
├──App.vue 应用配置,配置App全局样式以及监听
├──main.js Vue初始化入口文件
├──manifest.json 配置应用名称、appid等打包信息
├──pages.json 配置页面路由、导航条、选项卡等页面类信息
└──uni.scss 全局样式

封装方法


工欲善其事,必先利其器。在开发之前,我们可以把一些全局通用的方法进行封装,以及把uni-app提供的api进行二次封装,方便使用。全局的公共方法我们都会放到/src/utils文件夹下。


封装常用方法


下面这些方法都放在/src/utils/utils.js中,文章末尾会提供github链接方便查看。如果项目较大,建议把方法根据功能定义不同的js文件。


小程序Toast提示


/**
* 提示方法
* @param {String} title 提示文字
* @param {String} icon icon图片
* @param {Number} duration 提示时间
*/

export function toast(title, icon = 'none', duration = 1500) {
if(title) {
uni.showToast({
title,
icon,
duration
})
}
}

缓存操作(设置/获取/删除/清空)


/**
* 缓存操作
* @param {String} val
*/

export function setStorageSync(key, data) {
uni.setStorageSync(key, data)
}

export function getStorageSync(key) {
return uni.getStorageSync(key)
}

export function removeStorageSync(key) {
return uni.removeStorageSync(key)
}

export function clearStorageSync() {
return uni.clearStorageSync()
}

页面跳转


/**
* 页面跳转
* @param {'navigateTo' | 'redirectTo' | 'reLaunch' | 'switchTab' | 'navigateBack' | number } url 转跳路径
* @param {String} params 跳转时携带的参数
* @param {String} type 转跳方式
**/

export function useRouter(url, params = {}, type = 'navigateTo') {
try {
if (Object.keys(params).length) url = `${url}?data=${encodeURIComponent(JSON.stringify(params))}`
if (type === 'navigateBack') {
uni[type]({ delta: url })
} else {
uni[type]({ url })
}
} catch (error) {
console.error(error)
}
}

图片预览


/**
* 预览图片
* @param {Array} urls 图片链接
*/

export function previewImage(urls, itemList = ['发送给朋友', '保存图片', '收藏']) {
uni.previewImage({
urls,
longPressActions: {
itemList,
fail: function (error) {
console.error(error,'===previewImage')
}
}
})
}

图片下载


/**
* 保存图片到本地
* @param {String} filePath 图片临时路径
**/

export function saveImage(filePath) {
if (!filePath) return false
uni.saveImageToPhotosAlbum({
filePath,
success: (res) => {
toast('图片保存成功', 'success')
},
fail: (err) => {
if (err.errMsg === 'saveImageToPhotosAlbum:fail:auth denied' || err.errMsg === 'saveImageToPhotosAlbum:fail auth deny') {
uni.showModal({
title: '提示',
content: '需要您授权保存相册',
showCancel: false,
success: (modalSuccess) => {
uni.openSetting({
success(settingdata) {
if (settingdata.authSetting['scope.writePhotosAlbum']) {
uni.showModal({
title: '提示',
content: '获取权限成功,再次点击图片即可保存',
showCancel: false
})
} else {
uni.showModal({
title: '提示',
content: '获取权限失败,将无法保存到相册哦~',
showCancel: false
})
}
},
fail(failData) {
console.log('failData', failData)
}
})
}
})
}
}
})
}

更多函数就不在文章中展示了,已经放到/src/utils/utils,js里面,具体可以到github查看。


请求封装


为了减少在页面中的请求代码,所以我们要对uni-app提供的请求方式进行二次封装,在/src/utils文件夹下建立request.js,具体代码如下:



import {toast, clearStorageSync, getStorageSync, useRouter} from './utils'
import {BASE_URL} from '@/config/index'

const baseRequest = async (url, method, data, loading = true) =>{
header.token = getStorageSync('token') || ''
return new Promise((reslove, reject) => {
loading && uni.showLoading({title: 'loading'})
uni.request({
url: BASE_URL + url,
method: method || 'GET',
header: header,
timeout: 10000,
data: data || {},
success: (successData) => {
const res = successData.data
uni.hideLoading()
if(successData.statusCode == 200){
if(res.resultCode == 'PA-G998'){
clearStorageSync()
useRouter('/pages/login/index', 'reLaunch')
}else{
reslove(res.data)
}
}else{
toast('网络连接失败,请稍后重试')
reject(res)
}
},
fail: (msg) => {
uni.hideLoading()
toast('网络连接失败,请稍后重试')
reject(msg)
}
})
})
}

const request = {};

['options', 'get', 'post', 'put', 'head', 'delete', 'trace', 'connect'].forEach((method) => {
request[method] = (api, data, loading) => baseRequest(api, method, data, loading)
})

export default request

请求封装好以后,我们在/src/api文件夹下按业务模块建立对应的api文件,拿获取用户信息接口举例子:


/src/api文件夹下建立user.js,然后引入request.js


import request from '@/utils/request'

//个人信息
export const info = data => request.post('/v1/api/info', data)

在页面中直接使用:


import {info} from '@/api/user.js'

export default {
methods: {
async getUserinfo() {
let info = await info()
console.log('用户信息==', info)
}
}
}

版本切换


很多场景下,需要根据不同的环境去切换不同的请求域名、APPID等字段,这时候就需要通过环境变量来进行区分。下面案例我们就分为三个环境:开发环境(dev)、测试环境(test)、生产环境(prod)。


建立env文件


在项目根目录建立下面三个文件并写入内容(常量名要以VUE开头命名):


.env.dev(开发环境)


VUE_APP_MODE=build
VUE_APP_ID=wxbb53ae105735a06b
VUE_APP_BASE=https://www.baidu.dev.com

.env.test(测试环境)


VUE_APP_MODE=build
VUE_APP_ID=wxbb53ae105735a06c
VUE_APP_BASE=https://www.baidu.test.com

.env.prod(生产环境)


VUE_APP_MODE=wxbb53ae105735a06d
VUE_APP_ID=prod
VUE_APP_BASE=https://www.baidu.prod.com

修改package.json文件


"scripts": {
"dev:mp-weixin": "cross-env UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch --mode dev",
"build:mp-weixin": "cross-env UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch --mode prod"
},

然后执行


npm run dev:mp-weixin

/src/pages/index/index.vue下,打印:


onLoad() {
console.log(process.env.VUE_APP_MODE, '====VUE_APP_BASE')
console.log(process.env.VUE_APP_BASE, '====VUE_APP_BASE')
},

此时输出结果就是


dev ====VUE_APP_BASE
https://www.baidu.dev.com ====VUE_APP_BASE

动态修改appid


如果同一套代码,需要打包生成多个小程序,就需要动态修改appid了;文章开头说过appid在/src/manifest.json文件中配置,但json文件又不能直接写变量,这时候就可以参考官方 提出的解决方案:建立vue.config.js文件,具体操作如下。


在根目录下建立vue.config.js文件写入以下内容:


// 读取 manifest.json ,修改后重新写入
const fs = require('fs')

const manifestPath = './src/manifest.json'
let Manifest = fs.readFileSync(manifestPath, { encoding: 'utf-8' })
function replaceManifest(path, value) {
const arr = path.split('.')
const len = arr.length
const lastItem = arr[len - 1]

let i = 0
let ManifestArr = Manifest.split(/\n/)

for (let index = 0; index < ManifestArr.length; index++) {
const item = ManifestArr[index]
if (new RegExp(`"${arr[i]}"`).test(item)) ++i
if (i === len) {
const hasComma = /,/.test(item)
ManifestArr[index] = item.replace(
new RegExp(`"${lastItem}"[\\s\\S]*:[\\s\\S]*`),
`"${lastItem}": ${value}${hasComma ? ',' : ''}`
)
break
}
}

Manifest = ManifestArr.join('\n')
}
// 读取环境变量内容
replaceManifest('mp-weixin.appid', `"${process.env.VUE_APP_ID}"`)

fs.writeFileSync(manifestPath, Manifest, {
flag: 'w'
})

结尾


关于uni-app项目的起步工作就到这里了,后面有机会写一套完整的uni搭建电商小程序项目,记得关注。代码已经提交到github,如果对你有帮助,记得点个star!


作者:陇锦
来源:juejin.cn/post/7259589417736847416
收起阅读 »

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

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

前言


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


为什么不喜欢HBuilderX呢?



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

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

  3. 习惯了vscode开发


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



点击查看 github



cli创建uni-app 项目


1、 创建 Vue3/Vite 工程


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

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

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



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

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



2、进入工程目录


cd uni-starter

3、更新 uni-app依赖版本


npx @dcloudio/uvm@latest

4、安装依赖


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


ni 或 pnpm install 或 bun install

5、运行


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

6、打包


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

dcloudio 官方更多模版地址


自动引入



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


如果不喜欢此方式可忽略



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


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

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


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


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

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

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


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


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


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


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



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


eslint自动格式化



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


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



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


1、 安装插件


pnpm add -D eslint @antfu/eslint-config

2、新建.eslintrc.cjs


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


3、新建.vscode/settings.json


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

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


eslint-format.gif


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


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

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


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



editorConfig 规范



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


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



# @see: http://editorconfig.org

root = true

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

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


安装组件库


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



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

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

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

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

  • 图鸟UI:高颜值UI库

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

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


1、安装组件


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

2、配置easycom自动引入组件


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

3、安装uni-uits类型库


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

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


后续


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


参考链接:



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

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

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

你的段落文字.png


前言


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


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


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


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


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


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


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



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



动机


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


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


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


为什么是Flutter


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


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


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


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


错怪的Dart


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


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


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


空安全


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


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

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


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


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


audioMediaItem?.mediaUrl ?? "default";

对应Kotlin的


audioMediaItem?.mediaUrl ?: "default"

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


延迟初始化


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


late String name;

相当于Kotlin的


lateinit var String name

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


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


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


late String? name;

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


扩展函数


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


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


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

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


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

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


将Kotlin的内置函数带入


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


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

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

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


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


顶层函数


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


注意这些代码不在类里


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

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


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


Dart也有这样的功能


final _cookieJar = CookieJar();

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


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


高阶函数


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


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



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

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

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


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


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


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


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


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

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

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

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


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


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


运算符重载


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


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

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

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


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

// 用法

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

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

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


总结


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


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


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


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

uniapp小程序包过大的问题

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

uniapp小程序包过大的问题


前言


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


一 开启分包subPackages


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


1680316261345.png

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


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


1680316550191.png

二 静态资源优化


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


image.png

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


三 去除冗余代码


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


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


image.png

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


image.png

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


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


image.png

四 通过uniapp上传发布


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


image.png

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


结语


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


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

Flutter 首个真正可商用的 JSBridge 框架(完全兼容的 DSBridge for Flutter)

DSBridge for Flutter 在 Android 和 iOS 平台上做过 Hybrid 开发的同学基本都会知道 DSBridge,该框架目前最受欢迎的 JSBridge 框架之一,为了在 Flutter 侧实现原生 Hybrid 的能力,于是我们将...
继续阅读 »

DSBridge for Flutter


在 Android 和 iOS 平台上做过 Hybrid 开发的同学基本都会知道 DSBridge,该框架目前最受欢迎的 JSBridge 框架之一,为了在 Flutter 侧实现原生 Hybrid 的能力,于是我们将其适配到了Flutter 平台。


dsbridge.png



三端易用的现代跨平台 JavaScript bridge,通过它你可以在 JavaScript 和 Flutter 之间同步或异步的调用彼此的函数.



概述


DSBridge for Flutter 完全兼容 Android 和 iOS DSBridge 的 dsbridge.js。不像其他类似的框架无法实现JavaScript 调用 Dart 并同步返回结果,本框架完整支持同步调用和异步调用。dsbridge_flutter 是首个完整实现了 DSBridge 在原 Android 和 iOS 上的所有功能,因此可以实现将原来通过原生实现的 Webview 业务完全迁移到 Flutter 实现,即一套代码实现APP与H5的Hybrid开发。在现有使用了 dsbridge.js 的 Web 项目中无须修改任何代码即可使用 DSBridge for Flutter。


本框架目前支持Android 和 iOS 平台,即将支持纯鸿蒙平台(OpenHarmony & HarmonyOS Next),敬请期待!


DSBridge for Flutter 基于 Flutter官方的 webview_flutter


目前已发布到官方pub.dev:dsbridge_flutter


特性



  1. Android、iOS、JavaScript 三端易用,轻量且强大、安全且健壮。

  2. 同时支持同步调用和异步调用

  3. 支持以类的方式集中统一管理API

  4. 支持API命名空间

  5. 支持调试模式

  6. 支持 API 存在性检测

  7. 支持进度回调:一次调用,多次返回

  8. 支持 JavaScript 关闭页面事件回调

  9. 支持 JavaScript 模态对话框


安装



  1. 添加依赖


    dependencies:
    ...
    dsbridge_flutter: x.y.z



示例


请参考工程目录下的 example 包。运行 example 工程并查看示例交互。


如果要在你自己的项目中使用 dsBridge :


使用



  1. 新建一个Dart类,实现API


    import 'package:dsbridge_flutter/dsbridge_flutter.dart';

    class JsApi extends JavaScriptNamespaceInterface {
    @override
    void register() {
    registerFunction(testSyn);
    registerFunction(testAsyn);
    }

    /// for synchronous invocation
    String testSyn(dynamic msg) {
    return "$msg[syn call]";
    }

    /// for asynchronous invocation
    void testAsyn(dynamic msg, CompletionHandler handler) {
    handler.complete("$msg [ asyn call]");
    }
    }

    所有Dart APIs必须在register函数中使用registerFunction来注册。


  2. 添加API类实例到DWebViewController


    import 'package:dsbridge_flutter/dsbridge_flutter.dart';
    ...
    late final DWebViewController _controller;
    ...
    _controller.addJavaScriptObject(JsApi(), null);


  3. 在 JavaScript 中调用 Dart API ,并注册一个 JavaScript API 供原生调用.



    • 初始化 dsBridge


      //cdn
      //<script src="https://unpkg.com/dsbridge@3.1.3/dist/dsbridge.js"> </script>
      //npm
      //npm install dsbridge@3.1.3
      var dsBridge=require("dsbridge")


    • 调用 Dart API;以及注册一个 JavaScript API 供 Dart 调用.



      //同步调用
      var str=dsBridge.call("testSyn","testSyn");

      //异步调用
      dsBridge.call("testAsyn","testAsyn", function (v) {
      alert(v);
      })

      //注册 JavaScript API
      dsBridge.register('addValue',function(l,r){
      return l+r;
      })




  4. 在 Dart 中调用 JavaScript API


    import 'package:dsbridge_flutter/dsbridge_flutter.dart';
    ...
    late final DWebViewController _controller;
    ...
    _controller.callHandler('addValue', args: [3, 4],
    handler: (retValue) {
    print(retValue.toString());
    });



Dart API 签名


为了兼容Android&iOS,我们约定Dart API 签名,注意,如果API签名不合法,则不会被调用!签名如下:



  1. 同步API.


    any handler(dynamic msg)


    参数必须是 dynamic 类型,并且必须申明(如果不需要参数,申明后不适用即可)。返回值类型没有限制,可以是任意类型。


  2. 异步 API.


    void handler(dynamic arg, CompletionHandler handler)



命名空间


命名空间可以帮助你更好的管理API,这在API数量多的时候非常实用,比如在混合应用中。DSBridge支持你通过命名空间将API分类管理,并且命名空间支持多级的,不同级之间只需用'.' 分隔即可。


调试模式


在调试模式时,发生一些错误时,将会以弹窗形式提示,并且Dart API如果触发异常将不会被自动捕获,因为在调试阶段应该将问题暴露出来。


进度回调


通常情况下,调用一个方法结束后会返回一个结果,是一一对应的。但是有时会遇到一次调用需要多次返回的场景,比如在 JavaScript 中调用端上的一个下载文件功能,端上在下载过程中会多次通知 JavaScript 进度, 然后 JavaScript 将进度信息展示在h5页面上,这是一个典型的一次调用,多次返回的场景,如果使用其它 JavaScript bridge, 你将会发现要实现这个功能会比较麻烦,而 DSBridge 本身支持进度回调,你可以非常简单方便的实现一次调用需要多次返回的场景,下面我们实现一个倒计时的例子:


In Dart


void callProgress(dynamic args, CompletionHandler handler) {
var i = 10;
final timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (i == 0) {
timer.cancel();
handler.complete(0);
} else {
handler.setProgressData(i--);
}
});
}

In JavaScript


dsBridge.call("callProgress", function (value) {
document.getElementById("progress").innerText = value
})

完整的示例代码请参考example工程。


Javascript 对话框


DSBridge 已经实现了 JavaScript 的对话框函数(alert/confirm/prompt),如果你想自定义它们,通过DWebViewController设置相关回调函数即可。DSBridge实现的对话框默认设置是模态的,这会挂起UI线程。


API 列表


Dart API


在 Dart 中我们把实现了供 JavaScript 调用的 API 类的实例称为 Dart API object.


DWebViewController.addJavaScriptObject(JavaScriptNamespaceInterface? object, String? namespace)

Dart API object到DWebViewController,并为它指定一个命名空间。然后,在 JavaScript 中就可以通过bridge.call("namespace.api",...)来调用Dart API object中的原生API了。


如果命名空间是空(null或空字符串), 那么这个添加的Dart API object就没有命名空间。在 JavaScript 通过 bridge.call("api",...)调用。


示例:


In Dart


class JsEchoApi extends JavaScriptNamespaceInterface {
@override
void register() {
registerFunction(syn);
registerFunction(asyn);
}

dynamic syn(dynamic args) {
return args;
}

void asyn(dynamic args, CompletionHandler handler) {
handler.complete(args);
}
}
//namespace is "echo"
controller.addJavaScriptObject(JsEchoApi(), 'echo');

In JavaScript


// call echo.syn
var ret=dsBridge.call("echo.syn",{msg:" I am echoSyn call", tag:1})
alert(JSON.stringify(ret))
// call echo.asyn
dsBridge.call("echo.asyn",{msg:" I am echoAsyn call",tag:2},function (ret) {
alert(JSON.stringify(ret));
})

DWebViewController.removeJavaScriptObject(String namespace)

通过命名空间名称移除相应的Dart API object。


DWebViewController.callHandler(String method, {List? args, OnReturnValue? handler})

调用 JavaScript API。handlerName 为 JavaScript API 的名称,可以包含命名空间;参数以数组传递,args数组中的元素依次对应 JavaScript API的形参; handler 用于接收 JavaScript API 的返回值,注意:handler将在Dart主isolate中被执行


示例:


_controller.callHandler('append', args: ["I", "love", "you"],
handler: (retValue) {
print(retValue.toString());
});
/// call with namespace 'syn', More details to see the Demo project
_controller.callHandler('syn.getInfo', handler: (retValue) {
print(retValue.toString());
});

DWebViewController.javaScriptCloseWindowListener

当 JavaScript 中调用window.close时,DWebViewController 会触发此监听器,你可以自定义回调进行处理。


Example:


controller.javaScriptCloseWindowListener = () {
print('window.close called');
};

DWebViewController.hasJavaScriptMethod(String handlerName, OnReturnValue existCallback)

检测是否存在指定的 JavaScript API,handlerName可以包含命名空间.


示例:


_controller.hasJavaScriptMethod('addValue', (retValue) {
print(retValue.toString());
});

DWebViewController.dispose()

释放资源。在当前页面处于dispose状态时,你应该显式调用它。


JavaScript API


dsBridge

"dsBridge" 在初始化之后可用 .


dsBridge.call(method,[arg,callback])

同步或异步的调用Dart API。


method: Dart API 名称, 可以包含命名空间。


arg:传递给Dart API 的参数。只能传一个,如果需要多个参数时,可以合并成一个json对象参数。


callback(String returnValue): 处理Dart API的返回结果. 可选参数,只有异步调用时才需要提供.


dsBridge.register(methodName|namespace,function|synApiObject)

dsBridge.registerAsyn(methodName|namespace,function|asynApiObject)

注册同步/异步的 JavaScript API. 这两个方法都有两种调用形式:



  1. 注册一个普通的方法,如:


    In JavaScript


    dsBridge.register('addValue',function(l,r){
    return l+r;
    })
    dsBridge.registerAsyn('append',function(arg1,arg2,arg3,responseCallback){
    responseCallback(arg1+" "+arg2+" "+arg3);
    })

    In Dart


    _controller.callHandler('addValue', args: [3, 4],
    handler: (retValue) {
    print(retValue.toString());
    });

    _controller.callHandler('append', args: ["I", "love", "you"],
    handler: (retValue) {
    print(retValue.toString());
    });


  2. 注册一个对象,指定一个命名空间:


    In JavaScript


    //namespace test for synchronous calls
    dsBridge.register("test",{
    tag:"test",
    test1:function(){
    return this.tag+"1"
    },
    test2:function(){
    return this.tag+"2"
    }
    })

    //namespace test1 for asynchronous calls
    dsBridge.registerAsyn("test1",{
    tag:"test1",
    test1:function(responseCallback){
    return responseCallback(this.tag+"1")
    },
    test2:function(responseCallback){
    return responseCallback(this.tag+"2")
    }
    })


    因为 JavaScript 并不支持函数重载,所以不能在同一个 JavaScript 对象中定义同名的同步函数和异步函数



    In Dart


    _controller.callHandler('test.test1',
    handler: (retValue) {
    print(retValue.toString());
    });

    _controller.callHandler('test1.test1',
    handler: (retValue) {
    print(retValue.toString());
    });



dsBridge.hasNativeMethod(handlerName,[type])

检测Dart中是否存在名为handlerName的API, handlerName 可以包含命名空间.


type: 可选参数,["all"|"syn"|"asyn" ], 默认是 "all".


//检测是否存在一个名为'testAsyn'的API(无论同步还是异步)
dsBridge.hasNativeMethod('testAsyn')
//检测test命名空间下是否存在一个’testAsyn’的API
dsBridge.hasNativeMethod('test.testAsyn')
// 检测是否存在一个名为"testSyn"的异步API
dsBridge.hasNativeMethod('testSyn','asyn') //false

最后


如果你喜欢DSBridge for Flutter,欢迎点点star和like,以便更多的人知道它, 谢谢 !


作者:gtbluesky
来源:juejin.cn/post/7328753414724681728
收起阅读 »

从Flutter到Compose,为什么都在推崇声明式UI?

Compose推出之初,就曾引发广泛的讨论,其中一个比较普遍的声音就是——“🤨这跟Flutter也长得太像了吧?!” 这里说的长得像,实际更多指的是UI编码的风格相似,而关于这种风格有一个专门的术语,叫做声明式UI。 对于那些已经习惯了命令式UI的Androi...
继续阅读 »

Compose推出之初,就曾引发广泛的讨论,其中一个比较普遍的声音就是——“🤨这跟Flutter也长得太像了吧?!”


这里说的长得像,实际更多指的是UI编码的风格相似,而关于这种风格有一个专门的术语,叫做声明式UI


对于那些已经习惯了命令式UI的Android或iOS开发人员来说,刚开始确实很难理解什么是声明式UI。就像当初刚踏入编程领域的我们,同样也很难理解面向过程编程面向对象编程的区别一样。


为了帮助这部分原生开发人员完成从命令式UI到声明式UI的思维转变,本文将结合示例代码编写、动画演示以及生活例子类比等形式,详细介绍声明式UI的概念、优点及其应用。


照例,先奉上思维导图一张,方便复习:





命令式UI的特点


既然命令式UI与声明式UI是相对的,那就让我们先来回顾一下,在一个常规的视图更新流程中,如果采用的是命令式UI,会是怎样的一个操作方式。


以Android为例,首先我们都知道,Android所采用的界面布局,是基于View与ViewGr0up对象、以树状结构来进行构建的视图层级。



当我们需要对某个节点的视图进行更新时,通常需要执行以下两个操作步骤:



  1. 使用findViewById()等方法遍历树节点以找到对应的视图。

  2. 通过调用视图对象公开的setter方法更新视图的UI状态


我们以一个最简单的计数器应用为例:



这个应用唯一的逻辑就是“当用户点击"+"号按钮时数字加1”。在传统的Android实现方式下,代码应该是这样子的:


class CounterActivity : AppCompatActivity() {

var count: Int = 0

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_counter)

val countTv = findViewById(R.id.count_tv)
countTv.text = count.toString()

val plusBtn = findViewById

这段代码看起来没有任何难度,也没有明显的问题。但是,假设我们在下一个版本中添加了更多的需求:




  • 当用户点击"+"号按钮,数字加1的同时在下方容器中添加一个方块。

  • 当用户点击"-"号按钮,数字减1的同时在下方容器中移除一个方块。

  • 当数字为0时,下方容器的背景色变为透明。


现在,我们的代码变成了这样:


class CounterActivity : AppCompatActivity() {

var count: Int = 0

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_counter)

// 数字
val countTv = findViewById(R.id.count_tv)
countTv.text = count.toString()

// 方块容器
val blockContainer = findViewById(R.id.block_container)

// "+"号按钮
val plusBtn = findViewById

已经开始看得有点难受了吧?这正是命令式UI的特点,侧重于描述怎么做,我们需要像下达命令一样,手动处理每一项UI的更新,如果UI的复杂度足够高的话,就会引发一系列问题,诸如:



  • 可维护性差:需要编写大量的代码逻辑来处理UI变化,这会使代码变得臃肿、复杂、难以维护。

  • 可复用性差:UI的设计与更新逻辑耦合在一起,导致只能在当前程序使用,难以复用。

  • 健壮性差:UI元素之间的关联度高,每个细微的改动都可能一系列未知的连锁反应。


声明式UI的特点


而同样的功能,假如采用的是声明式UI,则代码应该是这样子的:


class _CounterPageState extends State<CounterPage> {
int _count = 0;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: [
// 数字
Text(
_count.toString(),
style: const TextStyle(fontSize: 48),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
// +"号按钮
ElevatedButton(
onPressed: () {
setState(() {
_count++;
});
},
child: const Text("+")),
// "-"号按钮
ElevatedButton(
onPressed: () {
setState(() {
if (_count == 0) return;
_count--;
});
},
child: const Text("-"))
],
),
Expanded(
// 方块容器
child: Container(
width: 60,
padding: const EdgeInsets.all(10),
color: _count > 0 ? const Color(0xFF6200EE) : Colors.transparent,

child: ListView.separated(
itemCount: _count,
itemBuilder: (BuildContext context, int index) {
// 方块
return Container(width: 40, height: 40, color: Colors.white);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(color: Colors.transparent, height: 10);
},
),
))
],
),
);
}
}


在这样的代码中,我们几乎看不到任何操作UI更新的代码,而这正是声明式UI的特点,它侧重于描述做什么,而不是怎么做,开发者只需要关注UI应该如何呈现,而不需要关心UI的具体实现过程。


开发者要做的,就只是提供不同UI与不同状态之间的映射关系,而无需编写如何在不同UI之间进行切换的代码。


所谓状态,指的是构建用户界面时所需要的数据,例如一个文本框要显示的内容,一个进度条要显示的进度等。Flutter框架允许我们仅描述当前状态,而转换的工作则由框架完成,当我们改变状态时,用户界面将自动重新构建


下面我们将按照通常情况下,用声明式UI实现一个Flutter应用所需要经历的几个步骤,来详细解析前面计数器应用的代码:



  1. 分析应用可能存在的各种状态


根据我们前面对于“状态”的定义,我们可以很容易地得出,在本例中,数字(_count值)本身即为计数器应用的状态,其中还包括数字为0时的一个特殊状态。



  1. 提供每个不同状态所对应要展示的UI


build方法是将状态转换为UI的方法,它可以在任何需要的时候被框架调用。我们通过重写该方法来声明UI的构造:


对于顶部的文本,只需声明每次都使用最新返回的状态(数字)即可:


Text(
_count.toString(),
...
),

对于方块容器,只需声明当_count的值为0时,容器的背景颜色为透明色,否则为特定颜色:


Container(
color: _count > 0 ? const Color(0xFF6200EE) : Colors.transparent,
...
)

对于方块,只需声明返回的方块个数由_count的值决定:


ListView.separated(
itemCount: _count,
itemBuilder: (BuildContext context, int index) {
// 方块
return Container(width: 40, height: 40, color: Colors.white);
},
...
),


  1. 根据用户交互或数据查询结果更改状态


当由于用户的点击数字发生变化,而我们需要刷新页面时,就可以调用setState方法。setState方法将会驱动build方法生成新的UI:


// "+"号按钮
ElevatedButton(
onPressed: () {
setState(() {
_count++;
});
},
child: const Text("+")),
// "-"号按钮
ElevatedButton(
onPressed: () {
setState(() {
if (_count == 0) return;
_count--;
});
},
child: const Text("-"))
],

可以结合动画演示来回顾这整个过程:



最后,用一个公式来总结一下UI、状态与build方法三者的关系,那就是:



以命令式和声明式分别点一杯奶茶


现在,你能了解命令式UI与声明式UI的区别了吗?如果还是有些抽象,我们可以用一个点奶茶的例子来做个比喻:


当我们用命令式UI的思维方式去点一杯奶茶,相当于我们需要告诉制作者,冲一杯奶茶必须按照煮水、冲茶、加牛奶、加糖这几个步骤,一步步来完成,也即我们需要明确每一个步骤,从而使得我们的想法具体而可操作。


而当我们用声明式UI的思维方式去点一杯奶茶,则相当于我们只需要告诉制作者,我需要一杯“温度适中、口感浓郁、有一点点甜味”的奶茶,而不必关心具体的制作步骤和操作细节。


声明式编程的优点


综合以上内容,我们可以得出声明式UI有以下几个优点:



  • 简化开发:开发者只需要维护状态->UI的映射关系,而不需要关注具体的实现细节,大量的UI实现逻辑被转移到了框架中。

  • 可维护性强:通过函数式编程的方式构建和组合UI组件,使代码更加简洁、清晰、易懂,便于维护。

  • 可复用性强:将UI的设计和实现分离开来,使得同样的UI组件可以在不同的应用程序中使用,提高了代码的可复用性。


总结与展望


总而言之,声明式UI是一种更加高层次、更加抽象的编程方式,其最大的优点在于能极大地简化现有的开发模式,因此在现代应用程序中得到广泛的应用,随着更多框架的采用与更多开发者的加入,声明式UI必将继续发展壮大,成为以后构建用户界面的首选方式。


作者:星际码仔
来源:juejin.cn/post/7212622837063811109
收起阅读 »

在微信小程序里运行完整的 Flutter,我们是怎么做到的?

背景 小程序是一种全新的业务形态,特别是微信小程序,既结合了 Web 动态化特性,又拥有 Native 丰富的设备能力支持。 在微信这个宿主上,小程序不仅有稳定的分发渠道,更拥有完善的生命周期、数据、AI 能力支持。 在该微信上开发小程序,一般使用以下两种方法...
继续阅读 »

背景


小程序是一种全新的业务形态,特别是微信小程序,既结合了 Web 动态化特性,又拥有 Native 丰富的设备能力支持。


在微信这个宿主上,小程序不仅有稳定的分发渠道,更拥有完善的生命周期、数据、AI 能力支持。


在该微信上开发小程序,一般使用以下两种方法:



  • JavaScript + WXML + WCSS

  • Taro + React + JavaScript


本文要介绍的是使用 Flutter Framework 开发小程序的方法,以及该方法背后的技术原理。


技术挑战


尽管 Flutter 官方已经提供 Flutter Web 实现,Flutter Web 本身就是基于 dart2js 运行的,微信小程序可以运行 JavaScript,在原理上跑起 Flutter Web 是没有问题的。


但仍然存在以下技术挑战:



  • 微信小程序没有 W3C 标准的 JavaScript 对象,Flutter Web 不能直接运行。

  • 微信小程序也没有 DOM 实现,Flutter Web HTML Renderer 不能直接渲染。

  • 微信小程序对包大小的限制十分严格,主包不能超过 2M,而 Flutter Web 所编译的 main.dart.js 初始体积就有 1.3 M,必须有合理的分包机制才能上传。


我们在 MPFlutter 1.x 版本中,针对上述问题已有一定的探索,1.x 版本的解决方法如下:



  • 使用微信开源的 kbone 库,模拟 W3C 实现,并通过模拟的 DOM 对象渲染出符合 WXML 要求的视图树。

  • 通过 Shadow Element Tree 的方式,使用 JSON 在 Dart 与 JavaScript 上下文同步视图树。

  • Fork Flutter Framework,并对其进行外科手术式的裁剪,使 main.dart.js 初始体积降低到 600K。


MPFlutter 1.x 方案已经良好的运行了两年,也收到了开发者非常多的反馈,开发者常诟病于裁剪后的 Flutter Framework 不兼容 Flutter 生态上的插件,同时 material 库也无法使用,需要从头开始编写 UI。


在 MPFlutter 2.0 版本,我们重新思考在小程序上运行 Flutter 的最佳方式,并在最终使用 CanvasKit Renderer 解决以上全部问题。


技术方案


Summary


通过裁剪 Skia 生成符合微信小程序分包要求的 CanvasKit,使用 Flutter Web + W3C BOM + WebGL Canvas 跑通渲染流程。


技术选型


在介绍技术选型前,需要先介绍 Flutter Web 的两种 Renderer。


HTML Renderer


原理是 Flutter Framework 通过 dart:js 库调用 Document 对象,并基于此将各种 RenderObject 转换为对应的 Element + CSS 添加到 DOM 树中。


该方案优点在于兼容性很好,几乎没有额外的依赖;缺点是性能不佳,并且渲染内容一致性难以与 Native Flutter 对齐。


CanvasKit Renderer


原理是通过 WebGL + Skia 渲染界面,该渲染方式与 Native Flutter 是完全一致的。


该方案优点在于渲染性能非常好,一致性与 Native Flutter 几乎没有差别;缺点是内存占用大,且需要从远端加载字体。


MPFlutter 2.0 选型


我们在 1.x 版本中用的是 HTML Renderer,通过 kbone 运行的 DOM 模拟层存在很多的问题,最令人诟病的是数据更新后界面刷新慢。当然问题的并不在于 kbone,而是 MPFlutter 1.x 本身对于 Element Tree 的序列化、反序列化的处理存在天然的缺陷,尽管已经通过 Dirty 和 Diff 等手段优化。


在 2.x 版本中,我们直接抛弃 HTML Renderer 的想法,使用 CanvasKit Renderer。


使用 CanvasKit Renderer 有这几个大前提:



  • 微信小程序已支持 WebAssembly 并支持 Brotli 压缩;

  • 微信小程序 Canvas 的性能相比最初的版本有质的提升,并支持 WebGL;

  • 微信小程序全部分包限制放宽到 20M,足够使用。


Skia 裁剪


Skia 是 Google 开源的 2D 渲染库,凭借良好的跨设备能力,优秀的性能表现,在 Google 多个产品中被使用,包括 Chrome / Flutter / Android / Fuchsia 都有 Skia 的身影。


Skia 屏蔽了不同设备、平台的具体实现,对外统一以标准的 RenderObject、RenderCommand 开放。


Skia 其中一个 Render Target 是 WebGL,也就是 CanvasKit。


然而 Flutter Web 默认使用的 CanvasKit 足有 6M 之大,即使使用 Brotli 压缩后仍然不符合小程序分包要求。


我们可以通过指定编译选项的方式裁剪 CanvasKit 尺寸,以下是 MPFlutter 使用的 build 配置:


./modules/canvaskit/compile.sh release no_skottie no_sksl_trace no_alias_font no_effects_deserialization no_encode_jpeg no_encode_png no_encode_webp legacy_draw_vertices no_embedded_font no_woff2

从配置可见,我们去掉了 skottie、image encoder、内置字体等不必要的功能,这些功能我们可以使用微信小程序 API 补充回来。


Brotli 压缩后的 wasm 文件刚好符合 2M 分包要求。


CanvasKit 加载


Skia 构建完成后,会得到两个产物,canvaskit.wasmcanvaskit.js


canvaskit.js 暴露了 wasm 中的各个 c++ 方法调用,同时也提供加载 wasm 的脚手架。


但是 canvaskit.js 的实现默认是 Web 的,我们需要将其中的 fetch 以及 WebAssembly 替换为微信小程序对应的实现。


这里提供一个使用 Skia 绘制红色矩形的微信小程序工程,有兴趣的同学可以下载到本地研究。


mpflutter.feishu.cn/wiki/LWhrw3…


Flutter Web 在微信中运行


要使 Flutter Web 在微信中运行,最大难点在于 Flutter Web 要求的 Web API 如何补充完整。


特别是 Document 、Window、Navigator 这些类,这些类我已经在 GitHub 上开源了,感兴趣的可以逐个文件阅读。


github.com/mpflutter/m…


这里举一个 window 的文件节选段落讲解:


export class FlutterMiniProgramMockWindow {
// screens
get devicePixelRatio() {
return wxSystemInfo.pixelRatio;
}

get innerWidth() {
return wxSystemInfo.windowWidth;
}

get innerHeight() {
return wxSystemInfo.windowHeight;
}

// webs
navigator = {
appVersion: "",
platform: "",
userAgent: "",
vendor: "",
language: "zh",
};

// 还有更多。。。
}

Flutter Web 在运行过程中,会通过 window.innerWidth / window.innerHeight 获取当前窗口宽高,以便下一步创建合适大小的画布用于渲染。


在微信小程序中,我们需要使用 wx.getSystemInfoSync() 获取对应宽高,并在 MockWindow 中返回给 Flutter。


关于 BOM 的文件,就不详细展开,都是一些胶水代码。


而 Flutter 的 main.dart.js 也需要有一些改造才可以跑在小程序上,主要的改造是通过 export main.dart.js 中的 main 函数,使其适配 CommonJS 可暴露给 Page 调用。


字体的加载


CanvasKit 最大的问题在于字体加载,目前来看是无法复用系统本身的字体的。


我们的做法是通过裁剪 NotoSansSC 字体,只包含常用的 9000+ 汉字,内置于小程序包中优先加载它。


这样有一个好处,小程序不需要强制从 gstatic 下载字体,省流省加载时间。


后续,我们还会研究通过 Canvas 2D 的方式,从本地加载字体。


分包


关于分包,其实是最好做的,因为 Flutter Web 本身就有 defered load 编译能力。


开发者可以轻松地将 main.dart.js 切分成若干个 JS 文件,我们做的就是在 Flutter Web 编译完成后,智能地将这些 JS 文件分配到不同的分包就好了。


资源分包也同理,资源通过 brotli 压缩也可以减少包体积。


总结


整整一套下来,Flutter 已经可以在微信小程序里跑起来了,我们来总结一下做了什么?


我们通过裁剪 Skia 使得 CanvasKit 可以很好地跑在小程序上,通过 BOM 兼容的方法,使得 Flutter Web 可以在微信小程序中找到对应实现,通过字体内置、智能分包的方式很好地解决了微信包体积限制。


该方案目前已经完全跑通,并已可用,同学们可以在 v2.mpflutter.com 文档站了解到更多用法。


如果对方案有任何疑问,也欢迎添加微信交流,感谢大家的关注。


作者:PonyCui
来源:juejin.cn/post/7324923422295670834
收起阅读 »

刷了四百道算法题,我在项目里用过哪几道呢?

大家好,我是老三,今天和大家聊一个话题:项目中用到的力扣算法。 不知道从什么时候起,算法已经成为了互联网面试的标配,在十年前,哪怕如日中天的百度,面试也最多考个冒泡排序。后来,互联网越来越热,涌进来的人越来越多,整个行业越来越内卷的,算法也慢慢成了大小互联网公...
继续阅读 »

大家好,我是老三,今天和大家聊一个话题:项目中用到的力扣算法。


不知道从什么时候起,算法已经成为了互联网面试的标配,在十年前,哪怕如日中天的百度,面试也最多考个冒泡排序。后来,互联网越来越热,涌进来的人越来越多,整个行业越来越内卷的,算法也慢慢成了大小互联网公司面试的标配,力扣现在已经超过3000题了,那么这些题目有多少进入了面试的考察呢?


以最爱考算法的字节跳动为例,看看力扣的企业题库,发现考过的题目已经有1850道——按照平均每道题花20分钟来算,刷完字节题库的算法题需要37000分钟,616.66小时,按每天刷满8小时算,需要77.08天,一周刷五天,需要15.41周,按一个月四周,需要3.85个月。也就是说,在脱产,最理想的状态下,刷完力扣的字节题库,需要差不多4个月时间。


字节题库


那么,我在项目里用过,包括在项目中见过哪些力扣上的算法呢?我目前刷了400多道题,翻来覆去盘点了一下,发现,也就这么几道。


刷题数量


1.版本比较:比较客户端版本


场景


在日常的开发中,我们很多时候可能面临这样的情况,兼容客户端的版本,尤其是Android和iPhone,有些功能是低版本不支持的,或者说有些功能到了高版本就废弃掉。


这时候就需要进行客户端的版本比较,客户端版本号通常是这种格式6.3.40,这是一个字符串,那就肯定不能用数字类型的比较方法,需要自己定义一个比较的工具方法。


某app版本


题目


165. 比较版本号


这个场景对应LeetCode: 165. 比较版本号



  • 题目:165. 比较版本号 (leetcode.cn/problems/co…)

  • 难度:中等

  • 标签:双指针 字符串

  • 描述:


    给你两个版本号 version1version2 ,请你比较它们。


    版本号由一个或多个修订号组成,各修订号由一个 '.' 连接。每个修订号由 多位数字 组成,可能包含 前导零 。每个版本号至少包含一个字符。修订号从左到右编号,下标从 0 开始,最左边的修订号下标为 0 ,下一个修订号下标为 1 ,以此类推。例如,2.5.330.1 都是有效的版本号。


    比较版本号时,请按从左到右的顺序依次比较它们的修订号。比较修订号时,只需比较 忽略任何前导零后的整数值 。也就是说,修订号 1 和修订号 001 相等 。如果版本号没有指定某个下标处的修订号,则该修订号视为 0 。例如,版本 1.0 小于版本 1.1 ,因为它们下标为 0 的修订号相同,而下标为 1 的修订号分别为 010 < 1


    返回规则如下:



    • 如果 *version1* > *version2* 返回 1

    • 如果 *version1* < *version2* 返回 -1

    • 除此之外返回 0


    示例 1:


    输入:version1 = "1.01", version2 = "1.001"
    输出:0
    解释:忽略前导零,"01""001" 都表示相同的整数 "1"

    示例 2:


    输入:version1 = "1.0", version2 = "1.0.0"
    输出:0
    解释:version1 没有指定下标为 2 的修订号,即视为 "0"

    示例 3:


    输入:version1 = "0.1", version2 = "1.1"
    输出:-1
    解释:version1 中下标为 0 的修订号是 "0",version2 中下标为 0 的修订号是 "1"0 < 1,所以 version1 < version2

    提示:



    • 1 <= version1.length, version2.length <= 500

    • version1version2 仅包含数字和 '.'

    • version1version2 都是 有效版本号

    • version1version2 的所有修订号都可以存储在 32 位整数




解法


那么这道题怎么解呢?这道题其实是一道字符串模拟题,就像标签里给出了了双指针,这道题我们可以用双指针+累加来解决。


在这里插入图片描述



  • 两个指针遍历version1version2

  • . 作为分隔符,通过累加获取每个区间代表的数字

  • 比较数字的大小,这种方式正好可以忽略前导0


来看看代码:


class Solution {
   public int compareVersion(String version1, String version2) {
       int m = version1.length();
       int n = version2.length();

       //两个指针
       int p = 0, q = 0;

       while (p < m || q < n) {
           //累加version1区间的数字
           int x = 0;
           while (p < m && version1.charAt(p) != '.') {
               x += x * 10 + (version1.charAt(p) - '0');
               p++;
          }

           //累加version2区间的数字
           int y = 0;
           while (q < n && version2.charAt(q) != '.') {
               y += y * 10 + (version2.charAt(q) - '0');
               q++;
          }

           //判断
           if (x > y) {
               return 1;
          }
           if (x < y) {
               return -1;
          }

           //跳过.
           p++;
           q++;
      }
       //version1等于version2
       return 0;
  }
}


应用


这段代码,直接CV过来,就可以直接当做一个工具类的工具方法来使用:


public class VersionUtil {

   public static Integer compareVersion(String version1, String version2) {
       int m = version1.length();
       int n = version2.length();

       //两个指针
       int p = 0, q = 0;

       while (p < m || q < n) {
           //累加version1区间的数字
           int x = 0;
           while (p < m && version1.charAt(p) != '.') {
               x += x * 10 + (version1.charAt(p) - '0');
               p++;
          }

           //累加version2区间的数字
           int y = 0;
           while (q < n && version2.charAt(q) != '.') {
               y += y * 10 + (version2.charAt(q) - '0');
               q++;
          }

           //判断
           if (x > y) {
               return 1;
          }
           if (x < y) {
               return -1;
          }

           //跳过.
           p++;
           q++;
      }
       //version1等于version2
       return 0;
  }
}


前面老三分享过一个规则引擎:这款轻量级规则引擎,真香!


比较版本号的方法,还可以结合规则引擎来使用:



  • 自定义函数:利用AviatorScript的自定义函数特性,自定义一个版本比较函数


        /**
        * 自定义版本比较函数
        */

       class VersionFunction extends AbstractFunction {
           @Override
           public String getName() {
               return "compareVersion";
          }

           @Override
           public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2) {
               // 获取版本
               String version1 = FunctionUtils.getStringValue(arg1, env);
               String version2 = FunctionUtils.getStringValue(arg2, env);
               return new AviatorBigInt(VersionUtil.compareVersion(version1, version2));
          }
      }


  • 注册函数:将自定义的函数注册到AviatorEvaluatorInstance


        /**
        * 注册自定义函数
        */

       @Bean
       public AviatorEvaluatorInstance aviatorEvaluatorInstance() {
           AviatorEvaluatorInstance instance = AviatorEvaluator.getInstance();
           // 默认开启缓存
           instance.setCachedExpressionByDefault(true);
           // 使用LRU缓存,最大值为100个。
           instance.useLRUExpressionCache(100);
           // 注册内置函数,版本比较函数。
           instance.addFunction(new VersionFunction());
           return instance;
      }


  • 代码传递上下文:在业务代码里传入客户端、客户端版本的上下文


        /**
        * @param device 设备
        * @param version 版本
        * @param rule   规则脚本
        * @return 是否过滤
        */

       public boolean filter(String device, String version, String rule) {
           // 执行参数
           Map<String, Object> env = new HashMap<>();
           env.put("device", device);
           env.put("version", version);
           //编译脚本
           Expression expression = aviatorEvaluatorInstance.compile(DigestUtils.md5DigestAsHex(rule.getBytes()), rule, true);
           //执行脚本
           boolean isMatch = (boolean) expression.execute(env);
           return isMatch;
      }


  • 编写脚本:接下来我们就可以编写规则脚本,规则脚本可以放在数据库,也可以放在配置中心,这样就可以灵活改动客户端的版本控制规则


    if(device==bil){
    return false;
    }

    ## 控制Android的版本
    if (device=="Android" && compareVersion(version,"1.38.1")<0){
    return false;
    }

    return true;



2.N叉数层序遍历:翻译商品类型


场景


一个跨境电商网站,现在有这么一个需求:把商品的类型进行国际化翻译。


某电商网站商品类型国际化


商品的类型是什么结构呢?一级类型下面还有子类型,字类型下面还有子类型,我们把结构一画,发现这就是一个N叉树的结构嘛。


商品树


翻译商品类型,要做的事情,就是遍历这棵树,翻译节点上的类型,这不妥妥的BFS或者DFS!


题目


429. N 叉树的层序遍历


这个场景对应LeetCode:429. N 叉树的层序遍历



  • 题目:429. N 叉树的层序遍历(leetcode.cn/problems/n-…)

  • 难度:中等

  • 标签: 广度优先搜索

  • 描述:


    给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。


    树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。


    示例 1:


    img


    输入:root = [1,null,3,2,4,null,5,6]
    输出:[[1],[3,2,4],[5,6]]

    示例 2:


    img


    输入:root = [1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]
    输出:[[1],[2,3,4,5],[6,7,8,9,10],[11,12,13],[14]]

    提示:



    • 树的高度不会超过 1000

    • 树的节点总数在 [0, 10^4] 之间




解法


BFS想必很多同学都很熟悉了,DFS的秘诀是,BFS的秘诀是队列


层序遍历的思路是什么呢?


使用队列,把每一层的节点存储进去,一层存储结束之后,我们把队列中的节点再取出来,孩子节点不为空,就把孩子节点放进去队列里,循环往复。


N叉树层序遍历示意图


代码如下:


class Solution {
public List<List<Integer>> levelOrder(Node root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) {
return result;
}

//创建队列并存储根节点
Deque<Node> queue = new LinkedList<>();
queue.offer(root);

while (!queue.isEmpty()) {
//存储每层结果
List<Integer> level = new ArrayList<>();
int size = queue.size();
for (int i = 0; i < size; i++) {
Node current = queue.poll();
level.add(current.val);
//添加孩子
if (current.children != null) {
for (Node child : current.children) {
queue.offer(child);
}
}
}
//每层遍历结束,添加结果
result.add(level);
}
return result;
}
}

应用


商品类型翻译这个场景下,基本上和这道题目大差不差,不过是两点小区别:



  • 商品类型是一个属性多一些的树节点

  • 翻译过程直接替换类型名称即可,不需要返回值


来看下代码:



  • ProductCategory:商品分类实体


    public class ProductCategory {
    /**
    * 分类id
    */

    private String id;
    /**
    * 分类名称
    */

    private String name;
    /**
    * 分类描述
    */

    private String description;
    /**
    * 子分类
    */

    private List<ProductCategory> children;

    //省略getter、setter

    }




  • translateProductCategory:翻译商品类型方法


       public void translateProductCategory(ProductCategory root) {
    if (root == null) {
    return;
    }

    Deque<ProductCategory> queue = new LinkedList<>();
    queue.offer(root);

    //遍历商品类型,翻译
    while (!queue.isEmpty()) {
    int size = queue.size();
    //遍历当前层
    for (int i = 0; i < size; i++) {
    ProductCategory current = queue.poll();
    //翻译
    String translation = translate(current.getName());
    current.setName(translation);
    //添加孩子
    if (current.getChildren() != null && !current.getChildren().isEmpty()) {
    for (ProductCategory child : current.getChildren()) {
    queue.offer(child);
    }
    }
    }
    }
    }



3.前缀和+二分查找:渠道选择


场景


在电商的交易支付中,我们可以选择一些支付方式,来进行支付,当然,这只是交易的表象。


某电商支付界面


在支付的背后,一种支付方式,可能会有很多种支付渠道,比如Stripe、Adyen、Alipay,涉及到多个渠道,那么就涉及到决策,用户的这笔交易,到底交给哪个渠道呢?


这其实是个路由问题,答案是加权随机,每个渠道有一定的权重,随机落到某个渠道,加权随机有很多种实现方式,其中一种就是前缀和+二分查找。简单说,就是先累积所有元素权重,再使用二分查找来快速查找。


题目


先来看看对应的LeetCode的题目,这里用到了两个算法:前缀和二分查找


704. 二分查找



  • 题目:704. 二分查找(leetcode.cn/problems/bi…)

  • 难度:简单

  • 标签:数组 二分查找

  • 描述:


    给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1


    示例 1:


    输入: nums = [-1,0,3,5,9,12], target = 9
    输出: 4
    解释: 9 出现在 nums 中并且下标为 4

    示例 2:


    输入: nums = [-1,0,3,5,9,12], target = 2
    输出: -1
    解释: 2 不存在 nums 中因此返回 -1

    提示:



    1. 你可以假设 nums 中的所有元素是不重复的。

    2. n 将在 [1, 10000]之间。

    3. nums 的每个元素都将在 [-9999, 9999]之间。




解法


二分查找可以说我们都很熟了。


数组是有序的,定义三个指针,leftrightmid,其中midleftright的中间指针,每次中间指针指向的元素nums[mid]比较和target比较:


二分查找示意图



  • 如果nums[mid]等于target,找到目标

  • 如果nums[mid]小于target,目标元素在(mid,right]区间;

  • 如果nums[mid]大于target,目标元素在[left,mid)区间


代码:


class Solution {
public int search(int[] nums, int target) {
int left=0;
int right=nums.length-1;

while(left<=right){
int mid=left+((right-left)>>1);
if(nums[mid]==target){
return mid;
}else if(nums[mid]<target){
//target在(mid,right]区间,右移
left=mid+1;
}else{
//target在[left,mid)区间,左移
right=mid-1;
}
}
return -1;
}
}

二分查找,有一个需要注意的细节,计算mid的时候:int mid = left + ((right - left) >> 1);,为什么要这么写呢?


因为这种写法int mid = (left + right) / 2;,可能会因为left和right数值太大导致内存溢出。同时,使用位运算,也是除以2最高效的写法。


——这里有个彩蛋,后面再说。


303. 区域和检索 - 数组不可变


不像二分查找,在LeetCode上,前缀和没有直接的题目,因为本身前缀和更多是一种思路,一种工具,其中303. 区域和检索 - 数组不可变 是一道典型的前缀和题目。



  • 题目:303. 区域和检索 - 数组不可变(leetcode.cn/problems/ra…)

  • 难度:简单

  • 标签:设计 数组 前缀和

  • 描述:


    给定一个整数数组 nums,处理以下类型的多个查询:



    1. 计算索引 leftright (包含 leftright)之间的 nums 元素的 ,其中 left <= right


    实现 NumArray 类:



    • NumArray(int[] nums) 使用数组 nums 初始化对象

    • int sumRange(int i, int j) 返回数组 nums 中索引 leftright 之间的元素的 总和 ,包含 leftright 两点(也就是 nums[left] + nums[left + 1] + ... + nums[right] )


    示例 1:


    输入:
    ["NumArray", "sumRange", "sumRange", "sumRange"]
    [[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]]
    输出:
    [null, 1, -1, -3]

    解释:
    NumArray numArray = new NumArray([-2, 0, 3, -5, 2, -1]);
    numArray.sumRange(0, 2); // return 1 ((-2) + 0 + 3)
    numArray.sumRange(2, 5); // return -1 (3 + (-5) + 2 + (-1))
    numArray.sumRange(0, 5); // return -3 ((-2) + 0 + 3 + (-5) + 2 + (-1))

    提示:



    • 1 <= nums.length <= 104

    • -105 <= nums[i] <= 105

    • 0 <= i <= j < nums.length

    • 最多调用 104sumRange 方法




解法


这道题,我们如果不用前缀和的话,写起来也很简单:


class NumArray {
private int[] nums;

public NumArray(int[] nums) {
this.nums=nums;
}

public int sumRange(int left, int right) {
int res=0;
for(int i=left;i<=right;i++){
res+=nums[i];
}
return res;
}
}

当然时间复杂度偏高,O(n),那么怎么使用前缀和呢?



  • 构建一个前缀和数组,用来累积 (0……i-1)的和,这样一来,我们就可以直接计算[left,right]之间的累加和


前缀和数组示意图


代码如下:


class NumArray {
private int[] preSum;

public NumArray(int[] nums) {
int n=nums.length;
preSum=new int[n+1];
//计算nums的前缀和
for(int i=0;i<n;i++){
preSum[i+1]=preSum[i]+nums[i];
}
}

//直接算出区间[left,right]的累加和
public int sumRange(int left, int right) {
return preSum[right+1]-preSum[left];
}
}

可以看到,通过前缀和数组,可以直接算出区间[left,right]的累加和,时间复杂度O(1),可以说非常高效了。


应用


了解了前缀和和二分查找之后,回归我们之前的场景,使用前缀和+二分查找来实现加权随机,从而实现对渠道的分流选择。


渠道分流选择



  • 需要根据渠道和权重的配置,生成一个前缀和数组,来累积权重的值,渠道也通过一个数组进行分配映射

  • 用户的支付请求进来的时候,生成一个随机数,二分查找找到随机数载前缀和数组的位置,映射到渠道数组

  • 最后通过渠道数组的映射,找到选中的渠道


代码如下:


/**
* 支付渠道分配器
*/
public class PaymentChannelAllocator {
//渠道数组
private String[] channels;
//前缀和数组
private int[] preSum;
private ThreadLocalRandom random;

/**
* 构造方法
*
* @param channelWeights 渠道分流权重
*/
public PaymentChannelAllocator(HashMap<String, Integer> channelWeights) {
this.random = ThreadLocalRandom.current();
// 初始化channels和preSum数组
channels = new String[channelWeights.size()];
preSum = new int[channelWeights.size()];

// 计算前缀和
int index = 0;
int sum = 0;
for (String channel : channelWeights.keySet()) {
sum += channelWeights.get(channel);
channels[index] = channel;
preSum[index++] = sum;
}
}

/**
* 渠道选择
*/
public String allocate() {
// 生成一个随机数
int rand = random.nextInt(preSum[preSum.length - 1]) + 1;

// 通过二分查找在前缀和数组查找随机数所在的区间
int channelIndex = binarySearch(preSum, rand);
return channels[channelIndex];
}

/**
* 二分查找
*/
private int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;

while (left <= right) {
int mid = left + ((right - left) >> 2);
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
// 当找不到确切匹配时返回大于随机数的最小前缀和的索引
return left;
}
}

测试一下:


    @Test
void allocate() {
HashMap<String, Integer> channels = new HashMap<>();
channels.put("Adyen", 50);
channels.put("Stripe", 30);
channels.put("Alipay", 20);

PaymentChannelAllocator allocator = new PaymentChannelAllocator(channels);

// 模拟100次交易分配
for (int i = 0; i < 100; i++) {
String allocatedChannel = allocator.allocate();
System.out.println("Transaction " + (i + 1) + " allocated to: " + allocatedChannel);
}
}

彩蛋


在这个渠道选择的场景里,还有两个小彩蛋。


二分查找翻车


我前面提到了一个二分查找求mid的写法:


int mid=left+((right-left)>>1);

这个写法机能防止内存溢出,用了位移运算也很高效,但是,这个简单的二分查找写出过问题,直接导致线上cpu飙升,差点给老三吓尿了。


吓惨了


int mid = (right - left) >> 2 + left;

就是这行代码,看出什么问题来了吗?


——它会导致循环结束不了!


为什么呢?因为>>运算的优先级是要低于+的,所以这个运算实际上等于:


int mid = (right - left) >> (2 + left);

在只有两个渠道的时候没有问题,三个的时候就寄了。


当然,最主要原因还是没有充分测试,所以大家知道我在上面为什么特意写了单测吧。


加权随机其它写法


这里用了前缀和+二分查找来实现加权随机,其实加权随机还有一些其它的实现方法,包括别名方法、树状数组、线段树 随机列表扩展 权重累积等等方法,大家感兴趣可以了解一下。


加权随机的实现


印象比较深刻的是,有场面试被问到了怎么实现加权随机,我回答了权重累积前缀和+二分查找,面试官还是不太满意,最后面试官给出了他的答案——随机列表扩展


什么是随机列表扩展呢?简单说,就是创建一个足够大的列表,根据权重,在相应的区间,放入对应的渠道,生成随机数的时候,就可以直接获取对应位置的渠道。


public class WeightedRandomList {
private final List<String> expandedList = new ArrayList<>();
private final Random random = new Random();

public WeightedRandomList(HashMap<String, Integer> weightMap) {
// 填充 expandedList,根据权重重复元素
for (String item : weightMap.keySet()) {
int weight = weightMap.get(item);
for (int i = 0; i < weight; i++) {
expandedList.add(item);
}
}
}

public String getRandomItem() {
// 生成随机索引并返回对应元素
int index = random.nextInt(expandedList.size());
return expandedList.get(index);
}

public static void main(String[] args) {
HashMap<String, Integer> items = new HashMap<>();
items.put("Alipay", 60);
items.put("Adyen", 20);
items.put("Stripe", 10);

WeightedRandomList wrl = new WeightedRandomList(items);

// 演示随机选择
for (int i = 0; i < 10; i++) {
System.out.println(wrl.getRandomItem());
}
}
}

这种实现方式就是典型的空间换时间,空间复杂度O(n),时间复杂度O(1)。优点是时间复杂度低,缺点是空间复杂度高,如果权重总和特别大的时候,就需要一个特别大的列表来存储元素。


当然这种写法还是很巧妙的,适合元素少、权重总和小的场景。


刷题随想


上面就是我在项目里用到过或者见到过的LeetCode算法应用,416:4,不足1%的使用率,还搞出过严重的线上问题。


……


在力扣社区里关于算法有什么的贴子里,有这样的回复:


“最好的结构是数组,最好的算法是遍历”。


“最好的算法思路是暴力。”


……


坦白说,如果不是为了面试,我是绝对不会去刷算法的,上百个小时,用在其他地方,绝对收益会高很多。


从实际应用去找刷LeetCode算法的意义,本身没有太大意义,算法题的最大意义就是面试。


刷了能过,不刷就挂,仅此而已。


这些年互联网行业红利消失,越来越多的算法题,只是内卷的产物而已。


当然,从另外一个角度来看,考察算法,对于普通的打工人,可能是个更公平的方式——学历、背景都很难卷出来,但是算法可以。


我去年面试的真实感受,“没机会”比“面试难”更令人绝望。


写到这,有点难受,刷几道题缓一下!






参考:


[1].leetcode.cn/circle/disc…


[2].36kr.com/p/121243626…


[3].leetcode.cn/circle/disc…


[4].leetcode.cn/circle/disc…







备注:涉及敏感信息,文中的代码都不是真实的投产代码,作者进行了一定的脱敏和演绎。





作者:三分恶
来源:juejin.cn/post/7321271017429712948
收起阅读 »

一个 Kotlin 开发,对于纯函数的思考

什么是纯函数? 纯函数是数学上的一种理想状态,即相同的输入永远会得到相同的输出,而且没有任何可观察的副作用 在数学上函数的定义为 It must work for every possible input value And it has only one ...
继续阅读 »

什么是纯函数?


纯函数是数学上的一种理想状态,即相同的输入永远会得到相同的输出,而且没有任何可观察的副作用


在数学上函数的定义为



  • It must work for every possible input value

  • And it has only one relationship for each input value



即每个在值域内的输入都能得到唯一的输出,它只可能是多对一而不是一对多的关系:



副作用



Wikipedia Side Effect: In computer science, an operation, function or expression is said to have a side effect if it modifies some state variable value(s) outside its local environment, which is to say if it has any observable effect other than its primary effect of returning a value to the invoker of the operation. Example side effects include modifying a non-local variable, modifying a static local variable, modifying a mutable argument passed by reference, performing I/O or calling other functions with side-effects. In the presence of side effects, a program's behaviour may depend on history; that is, the order of evaluation matters. Understanding and debugging a function with side effects requires knowledge about the context and its possible histories.



副作用的形式很多样,一切影响到外部状态、或依赖于外部状态的行为都可以称为副作用。副作用是必须的,因为程序总是不可避免的要与外界交互,如:


更改外部文件、数据库读写、用户 UI 交互、访问其他具有副作用的函数、修改外部变量


这些都可以被视为副作用,而在函数式编程中我们往往希望使副作用最小化,尽量避免副作用,对应的纯函数则是希望彻底消除副作用,因为副作用让纯函数变得不“纯”,只要一个函数还需要依赖外部状态,那么这个函数就无法始终保持同样的输入得到同样的输出。


好处是什么?



You wanted a banana but what you got was a gorilla holding the banana and the entire jungle. — Joe Armstrong, creator of Erlang progamming




  • 可缓存性,由于唯一的输入代表唯一的输出,那么这意味着我们可以在输入不变的情况下直接返回运算过的结果。

  • 高度并行,由于纯函数不依赖外部状态,因此即便在多线程情况下外部怎么变动,纯函数始终能够返回预期的值,纯函数能够达到真正的无锁编程,高度并发。

  • 高度可测性,不需要依赖外部状态,传统的 OOP 测试我们都需要模拟一个真实的环境,比如在 Android 中将 Application 模拟出来,在执行完之后断言状态的改变。而纯函数只需要模拟输入,断言输入,这是如此的简单优雅。

  • 依赖清晰,面相对象编程总需要你将整个环境初始化出来,然后函数再依赖这些状态修改状态,函数往往伴随着大量外部的隐式依赖,而纯函数只依赖输入参数,仅此而已,也仅提供返回值。


更进一步


传统大学老师教的都是 OOP,所以大多数人最开始也不会去学习纯函数的思路,但纯函数是完全不一样的一套编程思路,下面是一个纯函数中实现循环的例子,传统的循环往往是这样的:


int sum(int[] array) {
int sum = 0;
for (int i = 0; i < array.length; i++) {
sum += array[i];
}
return sum;
}

尽管大多数语言也会提供 for...in 之类的语法,如 kotlin:


fun sum(array: IntArray): Int {
var sum = 0
for (i in array) {
sum += i
}
return sum
}

但我们注意到,在上面的例子中均引入了两个可变的变量,sum 和 i,站在 sum += i 的视角,它的外部依赖:i 是一个外部的可变状态,因此这个函数并不“纯”。


但另一方面来说,从整体函数对外的视角来看,其还是很“纯”的,因为对于传入的外部 array,始终有唯一的 int 返回值,那么我们追求完全的“纯”,完全不使用可变量的目的是什么呢?


在纯函数下要实现完全消灭不可变变量,我们可以这么做:


tailrec fun sum(array: IntArray, current: Int = 0, index: Int = 0): Int {
if (index < 0 || index >= array.size) return current
return sum(array, current + array[index], index + 1)
}

我们编写了退出条件,当 index 在不正常的情况下会,意味着没有东西可以加,直接返回 current,即当前已经算好的值;其余情况则直接返回 current 与当前 index 的值的和,再加上 index + 1 之后所有值的 sum。这个例子已经很简单了,但函数式,递归思维不免会让学传统 OOP 的人需要多加思考一下。


当然作为一个 kotlin 开发,我也毫不犹豫的使用了 tailrec 这个 kotlin 语言特性来帮助优化尾递归,否则在遇到相当长的列表的时候,这个函数会抛出 StackOverFlowError。


函数一等公民


许多面向对象语言通常会用 this 显式访问对象的属性或方法,也有一些语言会省掉编写 this,事实上在许多语言编译器的背后实现中,通常也会将“对象成员的调用”变成“额外给成员函数添加一个 this 变量”。


可见发挥重要作用的其实是函数,不如更进一步,函数是一等公民,对象只不过是个结构体;如此,在纯函数中你完全用不到 this,甚至很多情况下都用不到对象。


所谓的一等公民,就是希望函数包含对象,而不是对象包含函数,甚至可以不需要对象(暴论),下面就是一个例子,是一个常见的业务诉求:



  • UserService 接收用户 id,并提供两个函数来获取用户 token 和用户的本地储存

  • ServerService 需要服务器 ip 和 port,提供通过 secret 获取 token 和通过用户 token 获取用户数据两个能力

  • UserData 是一个用户数据类,它能够接收父布局参数来构建 UI 数据用于显示


class UserService(private  val id: Int) {
fun userToken(password: String): UserToken = TODO()
fun localDb(dbPassword: String): LocalDb = TODO()
}

class ServerService(private val ip: String, private val port: Int) {
fun serverToken(serverSecret: String): ServerToken = TODO()
fun getUser(userToken: UserToken): UserData = TODO()
}

class UserData(
val name: String, val avatarUrl: String, val description: String,
) {
fun uiData(parentLayoutParameters: LayoutParameters): UIData = TODO()
}

那么这些变成函数式会怎么样呢?会像下面这样!


typealias UserTokenService = (password: String) -> UserToken
typealias LocalDbService = (dbPassword: String) -> LocalDb

typealias UserService = (id: Int) -> Pair<UserTokenService, LocalDbService>

typealias ServerTokenService = (serverSecret: String) -> ServerToken
typealias ServerUserService = (userToken: UserToken) -> UserDataAbilities
typealias ServerService = (ip: String, port: Int) -> Pair<ServerTokenService, ServerUserService>

typealias UserUIData = (parentLayoutParameters: LayoutParameters) -> UIData
typealias UserDataAbilities = UserUIData

val userService: UserService = { userId: Int ->
val tokenService: UserTokenService = { password: String -> TODO() }
val localDbService: LocalDbService = { dbPassword: String -> TODO() }
tokenService to localDbService
}

val serverService: ServerService = { ip: String, port: Int ->
val tokenService: ServerTokenService = { serverSecret: String -> TODO() }
val userService: ServerUserService = { userToken: String -> TODO() }
tokenService to userService
}

是不是看起来这些东西变得相当的复杂?但实际上真正的代码并没有写多少行,大量的代码都用来定义类型了!这也就是为什么你能看到的大多数展示函数式的例子都是用 js 去实现的,因为 js 的类型系统很弱,这样函数式写起来会很方便。


我这里用 kt 的范例则是写了大量的类型标记代码,因为我本人对显式声明类型有极高的要求,如果愿意,也可以完全将类型隐藏全靠编译器推理,就像下面这样,一切都变得简洁了,写起来和常规的 OOP 并没有太大区别。


val userService = { userId: Int ->
val tokenService = { password: String -> TODO() }
val localDbService = { dbPassword: String -> TODO() }
tokenService to localDbService
}

val serverService = { ip: String, port: Int ->
val tokenService = { serverSecret: String -> TODO() }
val userService = { userToken: String -> TODO() }
tokenService to userService
}

但不同的是,你看上面的代码,完全没有类/结构体的存在,因为变量的存储全部在函数体内储存了!


对于使用处,两种方式的用法事实上也大同小异,但可以看到我们彻底抛弃了类的存在!甚至在 kotlin 的未来版本中,如果这种代码始终在字节码中编译成 invokeDynamic,那么通过这种方式,字节码中甚至都可以避免类的存在!(当然,在 Android DEX 中会被脱糖成静态内部类)


// OOP
val userService = UserService(id = 0)
val serverService = ServerService(ip = "0.0.0.0", port = 114514)
val userToken = userService.userToken(password = "undefined")
val userData = serverService.getUser(userToken)
val uiData = userData.uiData(parentLayoutParameters)

// functional
val (userTokenService, _) = userService(0)
val (_, userDataService) = serverService("0.0.0.0", 114514)
val userToken = userTokenService("undefined")
val userData = userDataService(userToken)
val uiData = userData(parentLayoutParameters)


BTW,这里函数式 argument 没加 name 主要 kt 现在不支持。。。




柯里化


在上面的例子中,其实我们也能看到,类的存在是不必须的,类的本质其实只是预设好了一部分参数的函数,柯里化要解决的问题就是如何更轻松的实现“预设一部分参数”这样的能力。将一个函数柯里化后,允许多参数函数通过多次来进行传入,如 foo(a, b, c, d, e) 能够变成 foo(a, b)(c)(d, e) 这样的连续函数调用


在下面的例子中,我将举一个计算重量的范例:


fun weight(t: Int, kg: Int, g: Int): Int {
return t * 1000_000 + kg * 1000 + g
}

将其柯里化之后:


val weight = { t: Int ->
{ kg: Int ->
{ g: Int ->
t * 1000_000 + kg * 1000 + g
}
}
}

使用处:


// origin
weight(2, 3, 4)
// currying
weight(2)(3)(4)

在这里我们能发现,柯里化其实让实现处变复杂了,不过在 js 中通常会通过 bind 来实现,kt 也有民间大神 github 开源的柯里化库,使用这些能够从一定程度上降低编写柯里化代码的复杂度。


让我们看看 skiplang 语言吧


skiplang.com/


Skiplang 的宗旨就在其网站主页,A programming language to skip the things you have already computed,在纯函数的情况下,意味着得知输入状态,那么输出状态就是唯一确定的,这种情况就非常适合做缓存,如果输入值已经计算过,那么直接可以返回缓存的输出值。


在纯函数的情况下,意味着运算可以做到高度并行,在 skiplang 中,多个异步线程之间不允许共享可变变量,自然也不会出现异步锁等东西,从而保证了异步的绝对安全。


个人思考


纯函数的收益非常诱人,但开发者往往不喜欢使用纯函数,一些常见的原因可能是:



  1. 对性能的担忧:纯函数不允许修改变量,只允许通过 copy 等方式,创建了大量的变量;编译器需要进行激进的尾递归优化。

  2. 开发者意识淡薄:大多数学校出身的开发者只会用老师教的那一套 OOP,想培养 OOP 向函数式的转变,通常会让很多开发者感到困难,从而认为传统 OOP 简单,也是主流,没必要学新的。


尽管我对纯函数也非常的心动,但是我不是激进的纯函数派,我在日常工作中对其部分认同,具体到 kotlin 编程中,我通常坚持的理念是:



  1. 可以使用类,也可以在类中定义函数,但不允许使用可变成员。

  2. 可以使用可变的 local variable(但不推荐),但不允许在多线程之间共享

  3. 同种副作用,单一数据源。


参考



个人主页原文:一个 Kotlin 开发,对于纯函数的思考


作者:zsqw123
来源:juejin.cn/post/7321049383571046409
收起阅读 »

App跨平台框架VS原生开发深度评测之2023版

App跨平台框架历史悠久,从cordova、react native、flutter,直到最近的uni-app x。江山代有才人出,每个都试图颠覆原生,但过去却一直未成功。 过去的问题到底在哪里? 我们先捋一捋各种技术路线,分析这些跨平台开发框架和原生应用的差...
继续阅读 »

App跨平台框架历史悠久,从cordovareact nativeflutter,直到最近的uni-app x。江山代有才人出,每个都试图颠覆原生,但过去却一直未成功。


过去的问题到底在哪里?


我们先捋一捋各种技术路线,分析这些跨平台开发框架和原生应用的差别具体在哪里。


逻辑层渲染层类型代表作
webviewwebview弱类型5+App、cordova
js引擎webview弱类型uni-app之app-vue 、小程序(dount)
js引擎原生渲染弱类型react native、uni-app之app-nvue、weex
dart引擎flutter渲染引擎强类型flutter
js引擎flutter渲染引擎弱类型微信skyline、webF、ArkUI-x
kotlin原生渲染强类型uni-app x
kotlin原生渲染强类型原生应用

上面的表格,除了行尾的原生应用外,各个跨平台框架按出现时间排序,可以看到跨平台框架是如何演进的。


上表中,uni-app x和原生应用是一样的,逻辑层和渲染层都是原生,都是强类型;而其他跨平台框架或者在逻辑层、或者在渲染层与原生不一致。


webview不行已经是业内常识了,启动慢、渲染慢、内存占用高。这块本文不再详述。


但那些非web-view的框架到底哪里不如原生?


1. js逻辑+ 原生渲染


react nativeweex等抛弃webview,改由原生渲染的跨平台方案,2014年就推出了。
如今手机硬件也越来越好了,为什么性能还达不到原生?


js+原生渲染的方案主要有2点缺陷:



  • JS引擎自身的性能问题

  • JS和原生之间的通信延迟


1.1 js引擎慢,启动速度和运行速度都弱于原生


所以很多开发者即便使用这类方案,首页也还是原生来写。


React Native的Hermes引擎和华为的arkUI,提供了js编译为字节码的方案,这是一种空间换时间的方案,启动速度有了一定优化,但仍然比不过原生。


弱类型在编译期可优化的幅度有限,还是需要一个运行时来跑,无法像强类型那样直接深入底层。


以数字运算为例,js的number运算确实比强类型的int慢,内存开销也更大。


1.2 js语言与原生之间通信卡顿


每个语言有自己的内存空间,跨语言通信都有折损,每次通信几十到几百毫秒不等,视手机当时的状态。一旦频繁通信,就会明显卡顿。


逻辑层的js,即要和原生渲染层通信,还要和原生API通信:


1.2.1 js与原生ui通信


举个简单的场景例子,在js里监听滚动,根据滚动变化实时调整界面上某些元素的高度变化。这个问题能难倒一大批跨平台开发框架。


如果全部在webview里,js操作ui还好一些,所以uni-app的app-vue里的renderjs操作UI性能高,就是这个道理。同理还有微信小程序的wsx


虽然小程序和uni-app都是js,但实际上逻辑层在独立js引擎里,通过原生桥来控制web-view,通信成本很高。


weex提供了bindingx技术,这是一种弱编程,渲染层预先定义了一些操作UI的方式,调用时全部在渲染层运行,不会来回与逻辑层通信。但这种预定义方式的适应面有限,无法做到在js里高性能、自由的操作所有UI。


1.2.2 js操作原生api


操作系统和三方SDK的API都是原生的,js调用这些能力也需要跨语言通信。比如js调用原生的Storage或IO,数据较多时遍历的性能非常差。


当然在js API的封装上可以做些优化,比如微信的storage提供了wx.batchGetStorageSync这种批量读取的API,既然遍历性能差,那干脆一次性从原生读出来再传给js。


这也只能是无奈的方案,如果在遍历时想用js做什么判断就实现不了了,而且一次性读出很大的数据后传给js这一下,也需要通信时间。


2. flutter方案


flutter在2018年发布,第一次统一了逻辑层和渲染层,而且使用了强类型。


它没有使用原生渲染,而是使用由dart驱动的渲染引擎,这样逻辑层的dart代码操作UI时,再也没有延时了!bindingx、wxs这种补丁方案再也不需要了。


并且dart作为强类型,编译优化很好做,启动速度和运行速度都胜过js。


在这个开源项目下gitcode.net/dcloud/test…,提供了一个flutter编写的100个slider同时滑动的示例, 项目下有源码也有打包好apk,可以直接安装体验。


100个slider同时滑动,非常考验逻辑和UI的通信。如果在webview内部,html和js写100个这样的slider,在新的手机上表现也还ok。但在小程序和react native这种逻辑和UI分离的模式下,100个slider是灾难。


下载安装apk后可以看到dart操作flutter的UI真的没有通信折损,100个slider的拖动非常流畅。


flutter看起来很完美。但为什么也没有成为主流呢?很多大厂兴奋的引入后为何又不再扩大使用范围呢?


2.1 dart与原生API的通信


别忘了上面1.2.2提到的原生API通信。flutter虽然在逻辑层和渲染层都是dart,但要调用原生API时,还是要通信。


操作系统和三方SDK的API是原生的,让dart调用需要做一层封装,又落到了跨语言通信的坑里。


gitcode.net/dcloud/test…这是一个开源测试项目,来测试原生的claas数据与dart的通信耗时。


项目里面有源码,大家可自行编译;根目录有打包好的apk,也可以直接安装体验。


这个项目首先在kotlin中构建了包含不同数据量的class,传递到dart然后渲染在界面上,并且再写回到原生层。


有0.1k和1k两种数据量(点击界面上的1k数字可切换),有读和读并写2个按钮,各自循环1000次。


以下截图的测试环境是华为mate 30 5G,麒麟990。手机上所有进程杀掉。如下图:



  • 1k数据从原生读到dart并渲染


flutter_1k_read.jpeg



  • 1k数据从原生读到dart并渲染再写回


flutter_1k_readwrite.jpeg



  • 0.1k数据从原生读到dart并渲染


flutter_0.1k_read.jpeg



  • 0.1k数据从原生读到dart并渲染再写回


flutter_0.1k_readwrite.jpeg


通信损耗非常明显。并且数据量从1k降低到0.1k时,通信时间并没有减少10倍,这是因为通信耗时有一个基础线,数据再小也降不下去。


为什么会这样?因为dartkotlin不是一种编程语言,不能直接调用kotlinclass,只能先序列化成字符串,把字符串数据从原生传到dart,然后在dart层再重新构造。


当然也可以在原生层为dart封装API时提供wx.batchGetStorageSync这类批处理API,把数据一次读好再给dart,但这种又会遇到灵活性问题。


而在uni-app x中,这种跨语言通信是不存在的,不需要序列化,因为uni-app x使用的编程语言uts,在android上就编译为了kotlin,它可以直接调用kotlin的class而无需通信和封装。示例如下,具体uni-app x的原理后续章节会专题介绍。


<template>
template>
<script lang="uts">
import Build from 'android.os.Build';
export default {
onLoad() {
console.log(Build.MODEL); //uts可以直接导入并使用原生对象,不需要封装,没有跨语言通信折损
}
}
script>

再分享一个知识:


很多人都知道iPhone上跨平台框架的应用,表现比android好。但大多数人只知道是因为iPhone的硬件好。


其实还有一个重要原因,iOS的jscore是c写的,OS的API及渲染层也都是ObjectC,js调用原生时,某些类型可以做共享内存的优化。但复杂对象也还是无法直接丢一个指针过去共享使用内存。


而android,不管java还是kotlin,他们和v8、dart通信仍然需要跨语言通信。


2.2 flutter渲染和原生渲染的并存问题


flutter的自渲染引擎,在技术上是不错的。但在生态兼容上有问题。


很多三方软件和SDK是原生的,原生渲染和flutter自渲染并存时,问题很多。


flutter开发者都知道的一个常见坑是输入法,因为输入法是典型的原生UI,它和flutter自绘UI并存时各种兼容问题,输入框被遮挡、窗体resize适应,输入法有很多种,很难适配。


混合渲染,还有信息流广告、map、图表、动画等很多三方sdk涉及。这个时候内存占用高、渲染帧率下降、不同渲染方式字体不一致、暗黑主题不一致、国际化、无障碍、UI自动化测试,各种不一致。。。


这里没有提供开源示例,因为flutter官方是承认这个问题的,它提供了2种方式:混合集成模式和虚拟显示模式模式。


但在渲染速度、内存占用、版本兼容、键盘交互上都各自有各自的问题。详见flutter官网:docs.flutter.dev/platform-in…。这个是中文翻译:flutter.cn/docs/platfo…


在各大App中,微信的小程序首页是为数不多的使用flutter UI的界面,已经上线1年以上。


下面是微信8.0.44(此刻最新版),从微信的发现页面进入小程序首页。


视频中手机切换暗黑主题后,这个UI却还是白的,而且flutter的父容器原生view已经变黑了,它又在黑底上绘制了一个白色界面,体验非常差。


这个小程序首页界面很简单,没有输入框,规避了混合渲染,点击搜索图标后又跳转到了黑色的原生渲染的界面里。


假使这个界面再内嵌一个原生的信息流SDK,那会看到白色UI中的信息流广告是黑底的,更无法接受。


当然这不是说flutter没法做暗黑主题,重启微信后这个界面会变黑。这里只是说明渲染引擎不一致带来的各种问题。



注:如何识别一个界面是不是用flutter开发的?在手机设置的开发者选项里,有一个GPU呈现模式分析,flutter的UI不触发这个分析。且无法审查布局边界。



flutter的混合渲染的问题,在所有使用原生渲染的跨平台开发框架中都不存在,比如react native、weex、uni-app x。


总结下flutter:逻辑层和UI层交互没有通信折损,但逻辑层dart和原生api有通信成本,自绘UI和原生ui的混合渲染问题很多。


3. js+flutter渲染


flutter除了上述提到的原生通信和混合渲染,还有3个问题:dart生态、热更新、以及比较难用的嵌套写法。


一些厂商把flutter的dart引擎换成了js引擎,来解决上述3个问题。比如微信skyline、webF、ArkUI-x。


其实这是让人困惑的行为。因为这又回到了react native和weex的老路了,只是把原生渲染换成了flutter渲染。


flutter最大的优势是dart操作UI不需要通信,以及强类型,而改成js,操作UI再次需要通信,又需要js运行时引擎。


为了解决js和flutter渲染层的通信问题,微信的skyline又推出了补丁技术worklet动画,让这部分代码运行在UI层。(当然微信的通信,除了跨语言,还有跨进程通信,会更明显)


这个项目gitcode.net/dcloud/test…, 使用ArkUI-x做了100个slider,大家可以看源码,下载apk体验,明显能看到由于逻辑层和UI层通信导致的卡顿。



上述视频中,注意看手指按下的那1个slider,和其他99个通过数据通讯指挥跟随一起行动的slider,无法同步,并且界面掉帧。


不过自渲染由于无法通过Android的开发者工具查看GPU呈现模式,所以无法从条状图直观反映出掉帧。



注意ArkUI-x不支持Android8.0以下的手机,不要找太老的手机测试。



很多人以为自渲染是王道,但其实自渲染是坑。因为flutter的UI还会带来混合渲染问题。


也就是说,js+flutter渲染,和js+原生渲染,这2个方案相比,都是js弱类型、都有逻辑层和渲染层的通信问题、都有原生API通信问题,而js+flutter还多了一个混合渲染问题。


可能有的同学会说,原生渲染很难在iOS、Android双端一致,自渲染没有这个问题。


但其实完全可以双端一致,如果你使用某个原生渲染框架遇到不一致问题,那只是这个框架厂商做的不好而已。


是的,很遗憾react native在跨端组件方面投入不足,官方连slider组件都没有,导致本次评测中未提供react native下slider-100的示例和视频。


4. uni-app x


2022年,uts语言发布。2023年,uni-app x发布。


uts语言是基于typescript修改而来的强类型语言,编译到不同平台时有不同的输出:



  • 编译到web,输出js

  • 编译到Android,输出kotlin

  • 编译到iOS,输出swift


而uni-app x,是基于uts语言重新开发了一遍uni-app的组件、API以及vue框架。


如下这段示例,前端的同学都很熟悉,但它在编译为Android App时,变成了一个纯的kotlin app,里面没有js引擎、没有flutter、没有webview,从逻辑层到UI层都是原生的。


<template>
<view class="content">
<button @click="buttonClick">{{title}}button>
view>
template>

<script> //这里只能写uts
export default {
data() {
return {
title: "Hello world"
}
},
onLoad() {
console.log('onLoad')
},
methods: {
buttonClick: function () {
uni.
showModal({
"showCancel": false,
"content": "点了按钮"
})
}
}
}
script>

<style>
.content {
width: 750rpx;
background-color: white;
}
style>

这听起来有点天方夜谭,很多人不信。DCloud不得不反复告诉大家,可以使用如下方式验证:



  • 在编译uni-app x项目时,在项目的unpackage目录下看看编译后生成的kt文件

  • 解压打包后的apk,看看里面有没有js引擎或flutter引擎

  • 手机端审查布局边界,看看渲染是不是原生的(flutter和webview都无法审查布局边界)


但是开发者也不要误解之前的uni-app代码可以无缝迁移。



  • 之前的js要改成uts。uts是强类型语言,上面的示例恰好类型都可以自动推导,不能推导的时候,需要用:as声明和转换类型。

  • uni-app x支持css,但是css的子集,不影响开发者排版出所需的界面,但并非web的css全都兼容。


了解了uni-app x的基本原理,我们来看下uni-app x下的100个slider效果怎么样。


项目gitcode.net/dcloud/test…下有源码工程和编译好的apk。


如下视频,打开了GPU呈现模式,可以看到没有一条竖线突破那条红色的掉帧安全横线,也就是没有一帧掉帧。



uni-app x在app端,不管逻辑层、渲染层,都是kotlin,没有通信问题、没有混合渲染问题。不是达到了原生的性能,而是它本身就是原生应用,它和原生应用的性能没差别。


这也是其他跨平台开发框架做不到的。


uni-app x是一次大胆的技术突破,分享下DCloud选择这条技术路线的思路:


DCloud做了很多年跨平台开发,uni-app在web和小程序平台取得了很大的成功,不管规模大小的开发者都在使用;但在app平台,大开发者只使用uni小程序sdk,中小开发者的app会整体使用。


究其原因,uni-app在web和小程序上,没有性能问题,直接编译为了js或wxml,uni-app只是换了一种跨平台的写法,不存在用uni-app开发比原生js或原生wxml性能差的说法。


但过去基于小程序架构的app端,性能确实不及原生开发。


那么App平台,为什么不能像web和小程序那样,直接编译为App平台的原生语言呢?


uni-app x,目标不是改进跨平台框架的性能,而是给原生应用提供一个跨平台的写法。


这个思路的转换使得uni-app x超越了其他跨平台开发框架。


在web端编译为js,在小程序端编译为wxml等,在app端编译为kotlin。每个平台都只是帮开发者换种一致的写法而已,运行的代码都是该平台原生的代码。


然而在2年前,这条路线有2个巨大的风险:



  1. 从来没有人走通过

  2. 即便能走通,工作量巨大


没有人确定这个产品可以做出来,DCloud内部争议也很多。


还好,经历了无数的困难和挑战,这个产品终于面世了。


换个写法写原生应用,还带来另一个好处。


同样业务功能的app,使用vue的写法,比手写纯原生快多了。也就是uni-app x对开发效率的提升不只是因为跨平台,单平台它的开发效率也更高。


其实google自己也知道原生开发写法太复杂,关于换种更高效的写法来写原生应用,他们的做法是推出了compose UI。


不过遗憾的是这个方案引入了性能问题。我们专门测试使用compose UI做100个slider滑动的例子,流畅度也掉帧。


源码见:gitcode.net/dcloud/test…, 项目下有打包后的apk可以直接安装体验。


打开GPU呈现模式,可以看到compose ui的100个slider拖动时,大多数竖线都突破那条红色的掉帧安全横线,也就是掉帧严重。


既然已经把不同开发框架的slider-100应用打包出来了,我们顺便也比较了不同框架下的包体积大小、内存占用:


包体积(单位:M)内存占用(单位:Kb)
flutter18141324.8
ArtUI-x45.7133091.2
uni-app x8.5105451.2
compose ui4.498575.2

包体积数据说明:



  • 包含3个CPU架构:arm64、arm32、x86_64。

  • flutter的代码都是编译为so文件,支持的cpu类型和包体积是等比关系,1个cpu最小需要6M体积,业务代码越多,cpu翻倍起来越多。

  • ArtUI-x的业务代码虽然写在js里,但除了引用了flutter外还引用了js引擎,这些so库体积都不小且按cpu分类型翻倍。

  • uni-app x里主业务都在kotlin里,kotlin和Android x的兼容库占据了不少体积。局部如图片引用了so库,1个cpu最小需要7M体积。但由于so库小,增加了2个cpu类型只增加了不到1M。

  • compose ui没有使用so库,体积裁剪也更彻底。

  • uni-app x的常用模块并没有裁剪出去,比如slider100的例子其实没有用到图片,但图片使用的fesco的so库还是被打进去了。实际业务中不可能不用图片,所以实际业务中uni-app x并不会比compose ui体积大多少。


内存占用数据说明:



  • 在页面中操作slider数次后停止,获取应用内存使用信息VmRSS: 进程当前占用物理内存的大小

  • 表格中的内存数据是运行5次获取的值取平均值

  • 自渲染会占据更多内存,如果还涉及混合渲染那内存占用更高


5. 后记


跨语言通信、弱类型、混合渲染、包体积、内存占用,这些都是过去跨平台框架不如原生的地方。


这些问题在uni-app x都不存在,它只是换了一种写法的原生应用。


各种框架类型逻辑层与UI通信折损逻辑层与OS API通信折损混合渲染
react native、nvue、weex
flutter
微信skyline、webF、ArkUI-x
uni-app x
原生应用

当然,作为一个客观的分析,这里需要强调uni-app x刚刚面世,还有很多不成熟的地方。比如前文diss微信的暗黑模式,其实截止到目前uni-app x还不支持暗黑模式。甚至iOS版现在只能开发uts插件,还不能做完整iOS应用。


需求墙里都是uni-app x该做还未做的。也欢迎大家投票。


另外,原生Android中一个界面不能有太多元素,否则性能会拉胯。flutter的自渲染和compose ui解决了这个问题。而原生中解决这个问题需要引入自绘机制来降低元素数量,这个在uni-app x里对应的是draw自绘API。


uni-app x这个技术路线是产业真正需要的东西,随着产品的迭代完善,它能真正帮助开发者即提升开发效率又不牺牲性能。


让跨平台开发不如原生,成为历史。


欢迎体验uni-app x的示例应用,感受它的启动速度,渲染流畅度。


源码在:gitcode.net/dcloud/hell…; 


这个示例里有几个例子非常考验通信性能,除了也内置了slider-100外,另一个是“模版-scroll-view自定义滚动吸顶”,在滚动时实时修改元素top值始终为一个固定值,一点都不抖动。


我们不游说您使用任何开发技术,但您应该知道它们的原理和差别。


欢迎指正和讨论。


作者:CHB
来源:juejin.cn/post/7317091780826497075
收起阅读 »

Kotlin魔法——优雅实现多函数回调

补充 写完这篇文章并发布了之后才发现,这个写法已经有人发布过了,也可以参考参考~ 如何让你的回调更具Kotlin风味 Kotlin DSL回调 写在前面 在网络请求时,经常面临一类情况:网络请求有可能成功,也有可能失败,这就需要两个回调函数来分别对成功和失败...
继续阅读 »

补充


写完这篇文章并发布了之后才发现,这个写法已经有人发布过了,也可以参考参考~


如何让你的回调更具Kotlin风味


Kotlin DSL回调


写在前面



在网络请求时,经常面临一类情况:网络请求有可能成功,也有可能失败,这就需要两个回调函数来分别对成功和失败的情况来进行处理,那么,在Kotlin这门无比强大的语言中,有没有一种“魔法”,能够优雅地实现这一类同时可能需要多个回调的场景呢?



场景


问题的场景已经提出,也就是当某一个行为需要有多个回调函数的时候,并且这些回调并不一定都会触发。


例如,网络请求的回调场景中,有时候是onSuccess触发,有时候是onFailure触发,这两个函数的函数签名也不一定相同,那么怎么实现这个需求呢?


接下来我们以一个具体的问题贯穿全文:


假设我们现在要写一个网络请求框架,在封装上层回调的时候,需要封装两个回调(onSuccess/onFailure)供上层(就假设是UI层吧,不搞什么MVVM架构了)调用,以便UI层能知道网络请求成功/失败了,并进行相应的UI更新。



注: 标题所说的“魔法”是指实现方式三,方式一和二只是为了三铺垫的引子,如果想直奔主题那么建议直接跳转实现方式三!



实现方式一:直接传参


最直接的当然是直接传参嘛,把这两个回调写成函数参数,直接传进去,这当然可以实现目标,简单的示例代码如下。


网络请求层


data class RequestConfig(val api: String, val bodyJson: String, val method: String = "POST")
data class Data(val myData1: Int, val myData2: Boolean)

//模拟网络请求,获取数据
fun fetchData(requestConfig: RequestConfig, onSuccess: (data: Data) -> Unit = {}, onFailure: (errorMsg: String) -> Unit = {}) {
//假设调用更底层如Retrofit等模块,成功拿到数据后调用
onSuccess(Data(1, true))

//或者,失败后调用
onFailure("断网啦")
}

UI层


@Composable
fun MyView() {
Button(onClick = {
fetchData(requestConfig = RequestConfig("/user/info", ""), onSuccess = {
//更新UI
}, onFailure = {
//弹Toast提示用户
})
}) { }
}

在网络请求层,通过把fetchData的回调参数设一个默认值,我们也能实现“回调可选”这一需求。


这似乎并没有什么问题,那么还有没有什么别的实现方式呢?


实现方式二:链式调用


简单的思考过后,发现链式调用似乎也能满足我们的需求,实现如下。


网络请求层


在网络请求层,我们预先封装一个表示请求结果的类MyResult,然后让fetchData返回这个结果。


data class MyResult(val code: Int, val msg: String, val data: Data) {
fun onSuccess(block: (data: Data) -> Unit) = this.also {
if (code == 200) { //判断交给MyResult,若code==200,则认为成功
block(data)
}
}

fun onFailure(block: (errorMsg: String) -> Unit) = this.also {
if (code != 200) { //判断交给MyResult,若code!=200,则认为失败
block(msg)
}
}
}

//模拟网络请求,获取数据
fun fetchData(requestConfig: RequestConfig): MyResult {
return retrofitRequest(requestConfig)
}

UI层


此时的UI层调用fetchData时,则是通过MyResult这个返回值进行链式调用,并且链式调用也是自由可选的。


@Composable
fun MyView() {
Button(onClick = {
//点击按钮后发送网络请求
fetchData(requestConfig = RequestConfig("/user/info", "")).onSuccess {
//更新UI
}.onFailure {
//弹Toast提示用户
}
}) { }
}

这也似乎并没有什么问题,但是,总感觉不够Kotlin!


其实写多了Kotlin就会发现,Kotlin似乎非常喜欢花括号{},也就是作用域或者lambda这个概念。


而且Kotlin还喜欢把最后一个花括号放在最后一个参数,以便提到最外层去。


那么!有没有一种办法,能够以Kotlin常见的作用域的方式,优雅地完成上述场景需求呢?


锵锵!主角登场!


实现方式三:继承+扩展函数=魔法!


不多说,让我们先来看看这种实现方式的效果!


用这种方式,上述UI层将会变成这样!



  • 如果什么也不需要处理


@Composable
fun MyView2() {
Button(onClick = {
//点击按钮后发送网络请求
fetchData(requestConfig = RequestConfig("/user/info", ""))
}) { }
}


  • 如果需要处理onSuccess


@Composable
fun MyView2() {
Button(onClick = {
//点击按钮后发送网络请求
fetchData(requestConfig = RequestConfig("/user/info", "")) {
onSuccess {
//更新UI
}
}
}) {

}
}


  • 如果需要同时能处理onSuccess和onFailure


@Composable
fun MyView2() {
Button(onClick = {
//点击按钮后发送网络请求
fetchData(requestConfig = RequestConfig("/user/info", "")) {
onSuccess {
//更新UI
}
onFailure {
//弹Toast提示用户
}
}
}) {

}
}

看到了吗!!!非常自由,而且没有任何多余的->.或者,,只有非常整齐的花括号!


真的太神奇啦!


那么,这是怎么做到的呢?


揭秘时刻


在网络请求层,我们需要先定义一个接口,用于定义我们需要的多个回调函数!


interface ResultScope {
fun onSuccess(block: (data: Data) -> Unit)
fun onFailure(block: (errorMsg: String) -> Unit)
}

接着我们自己在内部实现这个接口!


internal class ResultScopeImpl : ResultScope {
var onSuccessBlock: (data: Data) -> Unit = {}
var onFailureBlock: (errorMsg: String) -> Unit = {}

override fun onSuccess(block: (data: Data) -> Unit) {
onSuccessBlock = block
}

override fun onFailure(block: (errorMsg: String) -> Unit) {
onFailureBlock = block
}
}

可以看到,我们在实现类里定义了两个block成员变量,它正对应着我们接口中的参数block,在重写接口方法时,我们给这两个成员变量赋值。


其实就是把这个block先暂时记录下来啦。


最后就是我们的fetchData函数了。


//模拟网络请求,获取数据
fun fetchData(requestConfig: RequestConfig, resultScope: ResultScope.() -> Unit = {}) {
val result = retrofitRequest(requestConfig)
val resultScopeImpl = ResultScopeImpl().apply(resultScope)

resultScopeImpl.run {
if (result.code == 200) onSuccessBlock(result.data) else onFailureBlock(result.msg)
}
}

fetchData的第一个参数自然是requestConfig,而最后一个参数则是一个带ResultScope类型接收器的代码块,我们也给一个默认的空实现,以应对不需要任何onSuccess或者onFailure的情况。




那么首先就有第一个问题了!resultScope: ResultScope.() -> Unit这个参数怎么理解?


我们首先要理解什么是lambda,或者说理解什么是接口!



重要!精髓! 如何理解lambda的意义?


当面对一堆lambda,甚至是嵌套lambda的时候,你是否感觉到阅读困难,非常无力?如果是的话,其实有一个很简单的方法,lambda也就是一个函数表达式嘛~既然是函数,那么我们就只需要盯紧三件事!



  • 函数的签名(包括参数列表和返回值)

  • 函数的方法体(也就是函数的实现)

  • 谁来负责在什么时候调用这个函数


只要盯紧这三件事,那么lambda的绝大部分理解上的障碍,都会一扫而光


例如


我们经常所说的回调,比如这个网络请求回调,那不就是:



  • 网络请求框架负责约定函数的签名,其中

    • 参数列表代表待会儿我框架层拿到结果以后需要告诉你UI层哪些信息

    • 返回值代表你UI层在知道我框架给的信息,并处理完之后,需要再返回给我框架层什么结果



  • UI层负责这个lambda的具体实现,也就是

    • 怎么去处理刚刚从框架层传来的信息(即参数)

    • 告知框架层处理完毕后的结果(即返回值)



  • 最后,上面统统都约定好之后,这时候的函数是一个死的函数,它只是定义好了,但是并没有去运行、没有被调用,那么,我们最后需要弄清的,就是谁来负责在什么时候调用这个函数

  • 无疑是框架层来调用,框架层在从更下层获取到请求结果后,就会调用这个函数,并且按之前所约定、所定义好的一切去执行它


又例如


Android开发中,RecyclerView这一列表组件会使用适配器,其中abstract void onBindViewHolder(@NonNull VH holder, int position)这个方法就也可以看成是一个所谓的lambda



  • 这个方法的签名和返回值由抽象类Adapter所定义

  • 这个方法的实现由Adapter的子类完成,即我们自己写的适配器

  • 这个方法的调用由RecyclerView控件负责调用


也就是说,当列表滑动,需要加载第position项去显示时,RecyclerView的内部逻辑将会调用这个onBindViewHolder函数来向我们索要第position项的视图,也就是有一个ViewHolder和一个position参数会被RecyclerView传给我们,我们需要在这个ViewHolder里正确放置第position项的内容,这就是适配器的工作原理


小结


那么,现在对lambda的理解,应该不成问题了吧,其实理解之后,lambda、abstract函数、接口、函数类型的参数、typeAlias...等等都是一个意思,我们需要关注的是,它的定义、实现以及调用者和调用时机



回到正题,如何理解resultScope: ResultScope.() -> Unit呢?



ResultScope.() -> Unit 表示一个带ResultScope类型接收器的函数代码块,说通俗一点,就是:



  • 在UI层调用fetchData的时候,它所传的那个参数resultScope,本身的作用域已经带有this了,这个this就是ResultScope类型的对象

    • 再说通俗一点就是,resultScope那个代码块内,能直接访问ResultScope的方法或者属性,这也就是为什么在上面的示例代码里,我们能直接在花括号里写 onSuccess {} 的原因,因为那个花括号已经被ResultScope对象统治了,我们能在里面直接调用ResultScope类的方法onSuccess



  • 然后,在网络请求层,当请求有结果后,我们会调用ResultScope的实例的对应block方法

    • 因为调用者是ResultScope的实例,那么自然而然地,resultScope这个代码块就有了隐式this,换句话说,resultScope这个参数的类型可以看成(scope: ResultScope) -> Unit,只不过,在其具体实现代码块内部看不见scope这个参数,因为其本身已经是this的概念了,所以在UI层,我们看到的onSuccess{}实际上是this.onSuccess{}





好,下一个问题。


在刚刚如何理解resultScope参数的解读里,有一句粗体“我们会调用ResultScope的实例的对应block方法”,那么,下一个问题就是,ResultScope的实例是怎么来的


ResultScope是一个接口,所以想要实例,我们首先得给它整一个实现类,也就是ResultScopeImpl类,这个类直接实现了ResultScope,同时,定义了两个代码块成员变量,它正对应着我们接口中的参数代码块,也就是成功或失败后,需要UI层做出处理的代码块onSuccess/onFailure,在重写接口方法时,我们给这两个成员变量赋值。


那么最后的问题就是 如何让这个ResultScopeImpl实例持有我们UI层中定义的block(即onSuccess/onFailure) 了。


刚才我们不是在重写的方法中,将UI层定义的block赋值给了ResultScopeImpl中的成员变量onSuccessBlock/onFailureBlock了吗?


那我们只要触发赋值,也就是ResultScopeImpl中override fun onSuccess的调用就行了。


办法就是这个!ResultScopeImpl().apply(resultScope)


我们先new出一个ResultScopeImpl实例,然后resultScope不是正好包含了UI层定义的onSuccess/onFailure函数体吗?那我们apply应用/赋值/设置属性)一下就可以了呗~


什么?你不知道为什么apply一下就能赋值了


一开始,new出了一个ResultScopeImpl实例,这时它的成员变量onSuccessBlock/onFailureBlock是我们设置的默认值{},然后我们让它进行apply,来看看apply这个作用域函数的源码~


public inline fun <T> T.apply(block: T.() -> Unit): T {
block()
return this
}

发现了吗?apply的参数正好就是T.() -> Unit类型,这里的T不就是ResultScopeImpl吗?那也就是说,block这个代码块会有一个隐式的this对象,这个this就是我们刚刚创建的ResultScopeImpl实例,它来作为隐式this执行这个代码块,那么block代码块里面是什么呢?对啦,就是我们在UI层写的onSuccess和onFailure嘛!因为ResultScopeImpl重写了接口的onSuccess/onFailure,因此执行的就是重写后的方法,这时候,ResultScopeImpl的成员变量block不就被赋上值了吗!over!


那么,完整的流程就是~



  • UI层的Button触发onClick,进而触发fetchData调用

  • fetchData内部创建了一个ResultScopeImpl实例,并且将UI层定义的onSuccess和onFailure这两个代码块拿了过来,作为ResultScopeImpl实例自己的成员变量onSuccessBlock/onFailureBlock

  • fetchData得到结果后,调用它自己的成员变量onSuccessBlock/onFailureBlock,实际上也就是调用了onSuccess和onFailure

  • UI层得到响应,onSuccess/onFailure被调用,触发UI更新


结语


实现方式就介绍到这里啦,当然,第三种方式并不是没有缺点,如果说,需要多次实现onSuccess回调,那么第三种方式,以上面的代码就不方便做到啦,只能把override里改成add,然后成员变量block们用一个List存起来,然后依次触发~


而如果是链式调用的实现方式,就不会有这个问题啦!


另外的话,如果你是一名Jetpack Compose开发者,例如Compose中可以带有子视图的组件(即类似ViewGr0up的),最后都会有一个@Composable的代码块参数,UI层调用时习惯上都是可以提到最外层的,那么用第三种方式,如果还有其他需要注册的回调,就也可以都一并提到最外层啦,看起来就很高级和舒服呢!


就写到这里叭~


作者:彭泰强
来源:juejin.cn/post/7220220246506192952
收起阅读 »

公司敏感数据被上传Github,吓得我赶紧改提交记录

大家好,我是小富~ 说个事吧!最近公司发生了一个事故,有同事不小心把敏感数据上传到了GitHub上,结果被安全部门扫描出来了。这件事导致公司对所有员工进行了一次数据安全的培训。对于这个事我相信,有点工作经验的人都不会故意去上传这些敏感文件,多数应该是误操作导致...
继续阅读 »

大家好,我是小富~


说个事吧!最近公司发生了一个事故,有同事不小心把敏感数据上传到了GitHub上,结果被安全部门扫描出来了。这件事导致公司对所有员工进行了一次数据安全的培训。对于这个事我相信,有点工作经验的人都不会故意去上传这些敏感文件,多数应该是误操作导致的。


这个事件也给了提了个醒,我平时会写博客用GitHub比较多,吓得我赶紧对自己所有的GitHub仓库进行了排查,庆幸没有提交过敏感信息的记录。但我注意到在过往的提交记录中,有使用公司的Git账号信息提交过代码,TMD这就很难受了。


图中信息均为假数据,切勿当真


避免后续产生不必要的麻烦,我决定修改一下提交记录中涉及公司的信息。



注意:以下操作只限于用在自己的Git仓库,别在公司的项目里秀,切记!



设置用户信息


Git进行版本控制的时候,每次的代码提交记录中都包含用户的用户名和邮箱,这些信息在你进行每一次提交时都会被记录下来。我们保不齐会错误地使用了错误的信息,或者需要改用另一个邮箱地址。那这种情况,我们就需要更改我们提交记录中的用户名和邮箱。


可以通过全局设置或者特定仓库设置两种方式来修改我们提交时的用户信息。


全局


全局设置可以影响所有的代码提交。如果你在全局范围内设置了用户名和邮箱后,除非你在特定的项目中覆盖这个设置,否则这个设置会作为默认设置应用于所有的提交。


git config --global user.name "程序员小富"
git config --global user.email "邮箱信息"

你可以通过如下的命令来查看Git的全局配置:


git config --global -l

特定仓库


如果你只想修改某个特定仓库的用户信息,可以在特定仓库的根目录下进行如下操作,Git会将设置得用户名和邮箱仅应用于当前仓库。


git config user.name "程序员小富"
git config user.email "邮箱信息"

篡改提交记录


单条修改


Git提供了amend命令,可以用来修改最新的提交记录。注意,这个命令只会修改最近一次的提交,它能实现以下的功能:



  • 修改提交信息

  • 添加漏掉的文件到上一次的提交中

  • 修改之前提交的文件


用法


它的使用方法比较简单,直接替换用户名、邮箱信息,或者如果已经修改了仓库的用户信息,直接执行命令重置。


# 替换用户名、邮箱信息
git commit --amend --author="{username} <{email}>" --no-edit

#
如果已经修改了仓库的用户信息,直接执行命令重置
git commit --amend --reset-author --no-edit

看到最近一次提交的用户名是xiaofu,不是我的个人信息程序员小富,使用amend命令修改一下。



效果


执行命令后最近一次的提交信息从xiaofu变更到了程序员小富,更改成功和预期的效果一致。


git commit --amend --author="程序员小富 <515361725@qq.com>" --no-edit


修改完成之后,别忘了推送到远程仓库。


 git push origin master

批量修改


Git官网提供了很多种修改提交记录信息的方法,这里主要介绍下filter-branch,它可以通过脚本的方式批量修改历史提交记录信息。


filter-branch 它能实现如下的功能,正好符合我们要批量修改历史提交记录中用户、邮箱的需求。



  • 全局修改邮箱地址;

  • 从每一个提交中移除一个文件;

  • 使一个子目录做为新的根目录


用法


历史提交记录中有很多用户名xiaofu提交的记录,现在使用filter-branch批量将他们改写成程序员小富



以下是官网提供的脚本,其逻辑很简单:如果遇到用户名为xiaofu的提交记录,将该提交记录的用户名和邮箱修改为程序员小富515361725@qq.com


git filter-branch --commit-filter '
if [ "$GIT_AUTHOR_NAME" = "xiaofu" ];
then
GIT_AUTHOR_NAME="程序员小富";
GIT_AUTHOR_EMAIL="515361725@qq.com";
git commit-tree "$@";
else
git commit-tree "$@";
fi'
HEAD

为了方便操作,创建一个脚本modifyCommit.sh,放在项目的根目录执行。


chmod +x modifyCommit.sh
sh modifyCommit.sh

执行脚本后稍作等待,出现如下的输出说明已经在执行修改操作了。



执行完毕看到历史提交记录中的用户名xiaofu全都变更成了程序员小富,说明脚本生效了。



如果没有修改成功,可以再次执行,但会出现错误提示A previous backup already exists in refs/original/,说明已经执行过了,执行以下命令清除缓存即可再次执行。



git filter-branch -f --index-filter 'git rm --cached --ignore-unmatch Rakefile' HEAD

修改完成之后,别忘了推送到远程仓库。


 git push origin master

GitHub工具


管理GitHub项目,我推荐大家使用GitHub官方的Git客户端工具GitHub Desktop,这个工具专门用来管理GitHub仓库,洁面简洁使用也很方便,主打一个轻量。



而且在提交代码时,如果用户信息与当前账号GitHub信息不一致,还会有提示这样就不怕误用其他信息提交了。



总结


如果大家平时会维护自己的GitHub仓库,建议一定一定要仔细的检查提交的代码,像注释里的公司邮箱信息、代码包路径中的公司标识,凡事涉及公司信息的数据一概去除,不要惹一些不必要的麻烦,数据泄漏这种重可大可小不是闹着玩的。


还有GitHub别留太多的个人信息,手机号邮箱就别放了,头像也别傻乎乎的放个自己大头贴,给自己留点回旋的余地。核心思工作和生活要隔离!!!


我是小富~ 下期见


作者:程序员小富
来源:juejin.cn/post/7309784902311870516
收起阅读 »

flutter chat UI again flutter 漂亮聊天UI界面实现

flutter 漂亮聊天UI界面实现 flutter chat UI  之前写了一个聊天界面,但是只是花架子,并不能使用,无法点击,无法活动,并且由于时间问题也没有完全完成,右侧的聊天界面没有实现。现在,我准备完成一个比较美观且能使用的聊天界面。 寻找聊天界面...
继续阅读 »

flutter 漂亮聊天UI界面实现 flutter chat UI


 之前写了一个聊天界面,但是只是花架子,并不能使用,无法点击,无法活动,并且由于时间问题也没有完全完成,右侧的聊天界面没有实现。现在,我准备完成一个比较美观且能使用的聊天界面。


寻找聊天界面模板


 先找一个美观的模板来模仿吧。找模板的标准是简介、美丽、大方、清新。


1.png


 这次选的是一个比较简洁的界面,淡蓝色为主色,横向三个大模块排列开来,有设置界面、好友列表、聊天界面,就选定用这个了。


chatUI聊天界面实现


整体分析


 最外层使用横向布局,分别放置三个大组件,每个组件里面使用竖向布局来放置各种按钮、好友列表、聊天界面。每个组件里面的细节我们边实现边学习。


外层框架


 我们先实现最外边的框架。用SelectionArea包裹所有后续组件,实现所有文字可以选定。Selection现在有了官方的正式支持,该功能补全了Flutter长时间存在Selection异常等问题,尤其是在Web框架下经常会有选择文本时与预期的行为不匹配的情况。接着用Row水平布局组件来包裹三大块细分功能组件,代码里先用text组件代替。这样框架就设置好了。


import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false, //去掉右上角debug标识
theme: ThemeData(
//主题设置
primarySwatch: Colors.blue,
),
home: const SelectionArea(
//子组件支持文字选定 3.3新特性
child: Scaffold(
//子组件
body: MyAppbody(),
),
),
);
}
}

class MyAppbody extends StatelessWidget {
const MyAppbody({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Row(
//水平布局
children: const <Widget>[
//子组件
Expanded(
flex: 1, //空间占比
child: Text("按钮组件"), ),

Expanded(
flex: 1, //空间占比
child: Text("好友列表组件"), ),

Expanded(
flex: 3, //空间占比
child: Text("聊天框组件"), ),

],
),
);
}
}

 效果图:


2.png


第一个模块设计


 新建一个fistblock文件夹放置我们的第一个模块代码,实现代码分块抽离。还是先写大框架,外围放置竖向排列组件Column,然后再依次放进去头像模块和设置模块。Column是垂直布局,在Y轴排列,也就是纵轴上的排列方式,可以使其包含的子控件按照垂直方向排列,Column是Widget的容器,存放的是一组Widget,而Container里面一次只能存放一个child。


import 'package:flutter/material.dart';
class FistBlockMain extends StatelessWidget {
const FistBlockMain({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
////竖直布局
children: const <Widget>[
//子组件
Expanded(
flex:1, //空间占比
child: Text("头像"),
),

Expanded(
flex: 1, //空间占比
child: Text("设置"),
),

Expanded(
flex:1, //空间占比
child: Text("帮助"),
),

],
),
);
}
}

 效果图:


3.png


头像模块实现


 头像模块我们之前也实现过,现在可以直接拿来用,例子里在线状态小圆点在右上角,这里我们依旧利用Badge实现小圆点,同时圆点位置可以自由设置,我比较习惯放在右下角,当然,你也可以通过设置Badge的position参数改变位置。Badge是flutter的插件,flutter也有很多其他的优秀的插件可以使用,有了插件的帮忙,我们可以很方便的实现各种功能。


class User extends StatelessWidget {
const User({super.key});
@override
Widget build(BuildContext context) {
return ListTile(
leading: Badge(
//头部部件
//通知小圆点
badgeColor: Colors.green, //小圆点颜色
position: const BadgePosition(
start: 35, top: 35, end: 0, bottom: 0), //小圆点显示位置
borderSide:
const BorderSide(color: Colors.white, width: 1.5), //外层白色圆圈框框
child: const CircleAvatar(
//图片圆形剪裁
radius: 25, //圆形直径,(半径)?
backgroundColor: Colors.white, //背景颜色设置为白色
backgroundImage: AssetImage(
"images/5.jpeg", //图片
),
),
),
title: const Text(//标题
"George",
style: TextStyle(
fontSize: 15, //字体大小
fontWeight: FontWeight.bold, //字体加粗
),
),
);
}
}

 效果图:


4.png


第一模块蓝色背景模块实现


 写完头像模块突然想起来,第一模块的蓝色背景还没实现呢,现在来实现一个蓝色的背景。因为是背景,所以应该用层叠Stack组件。背景颜色用Container的decoration来设置,实际使用BoxDecoration实现背景颜色盒子的设置,同时还需要设置阴影。BoxDecoration类提供了多种绘制盒子的方法,这个盒子有边框、主体、阴影组成,盒子的形状可能是圆形或者长方形。如果是长方形,borderRadius属性可以控制边界的圆角大小。


class FistBlockMain extends StatelessWidget {
const FistBlockMain({super.key});
@override
Widget build(BuildContext context) {
return Stack(children: <Widget>[
const Backgroud(),
Column(
//竖直布局
children: const <Widget>[
//子组件
Expanded(
flex: 1, //空间占比
child: User(),
),

Expanded(
flex: 1, //空间占比
child: Text("设置"),
),
Expanded(
flex: 1, //空间占比
child: Text("帮助"),
),
],
),
]);
}
}

class Backgroud extends StatelessWidget {
const Backgroud({super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Color.fromARGB(220, 100, 149, 237),
boxShadow: [
BoxShadow(
color: Color.fromARGB(220, 100, 149, 237),
blurRadius: 30, //阴影模糊程度
spreadRadius: 1 //阴影扩散程度
)
],
),
);
}
}

 效果图:


5.png


 模板的这个颜色我找了半天也没找到,后来就找个相似的先用着,但是总是看起来没有原来的好看。当个程序员难道还需要懂美术和艺术吗。。。


按钮模块实现


 接着要实现若干带图标的按钮了。模板是一个带图标的按钮,我们用TextButton.icon组件实现。按钮能被选定会影响操作体验,这里使用SelectionContainer是他不能被选中。外层使用Column布局依次放置按钮组件。使用Padding调整间距,是他更好看一些。图标和文字大小都是可以设置的。通过Text组件的TextStyle设置文字的颜色、大小,这里我们使用白色的文字。图标使用Icon组件实现,直接使用Icons.lock_clock内置的icon图标。按钮的onPressed和autofocus需要设置,这样的话点击按钮才会有动画显示。Padding组件再一次使用,这个组件我感觉很好用,可以通过他进一步调整部件的位置,进行美化。


class Buttonblock extends StatelessWidget {
const Buttonblock({super.key});
@override
Widget build(BuildContext context) {
return SelectionContainer.disabled(//选定失效
child: Column(
children: <Widget>[
//子组件
Padding(
padding: const EdgeInsets.fromLTRB(0, 20, 0, 20),
child: TextButton.icon(
icon: const Icon(
size: 22,
Icons.lock_clock,
color: Colors.white,
), //白色图标
label: const Text(
"Timeline",
style: TextStyle(
fontSize: 14, //字体大小
fontWeight: FontWeight.bold, //字体加粗
color: Colors.white //白色文字
),
),
onPressed: (){},//点击事件
autofocus: true,
),
),

Padding(
padding: const EdgeInsets.fromLTRB(0, 20, 0, 20),
child: TextButton.icon(
icon: const Icon(
size: 22,
Icons.message,
color: Colors.white,
), //白色图标
label: const Text(
"Message",
style: TextStyle(
fontSize: 14, //字体大小
fontWeight: FontWeight.bold, //字体加粗
color: Colors.white //白色文字
),
),
onPressed: () {
},
autofocus: true,
),
),
],
),
);
}
}


 效果图:


6.png


按钮点击弹窗showDialog实现


 是按钮当然需要被点击,点击之后我们可以弹一个窗给用户进行各种操作。这里用showDialog实现弹窗。在TextButton.icon的onPressed下实现一个点击弹窗操作。在Flutter里有很多的弹出框,比如AlertDialog、SimpleDialog,调用函数是showDialog。对话框也是一个UI布局,通常会包含标题、内容,以及一些操作按钮。这里实现一个最简单的对话框,如果有需求可以在这个基础上进行修改。


 Padding(
padding: const EdgeInsets.fromLTRB(0, 20, 0, 20),
child: TextButton.icon(
icon: const Icon(
size: 22,
Icons.message,
color: Colors.white,
), //白色图标
label: const Text(
"Message",
style: TextStyle(
fontSize: 14, //字体大小
fontWeight: FontWeight.bold, //字体加粗
color: Colors.white //白色文字
),
),
onPressed: () {//点击弹框
showDialog<void>(
context: context,
builder: (BuildContext context) {
return SimpleDialog(
title: const Text('选择'),
children: <Widget>[
SimpleDialogOption(
child: const Text('选项 1'),
onPressed: () {
Navigator.of(context).pop();
},
),
SimpleDialogOption(
child: const Text('选项 2'),
onPressed: () {//点击事件
Navigator.of(context).pop();
},
),
],
);
},
).then((val) {
});
},
autofocus: true,
),
),

 效果图:


7.png


第二个模块设计


 第二个模块是两部分,上边部分是一个在线状态展示区域,下边部分是好友列表,中间有一道分隔线。所以第二部分外层使用Column竖直布局组件,结合Stack组件做一个背景色。Stack可以容纳多个组件,以叠加的方式摆放子组件,后者居上,覆盖上一个组件。Stack也是可以存放一组Widget的组件。


class SecondBlockMain extends StatelessWidget {
const SecondBlockMain({super.key});
@override
Widget build(BuildContext context) {
return
Stack(children: <Widget>[
const Backgroud(),
Column(
//竖直布局
children: const <Widget>[
//子组件
Expanded(
flex: 1, //空间占比
child: Text("上边"),
),
Expanded(
flex:4, //空间占比
child: Text("下边"),
),
],
),
]);
}
}

第二个模块灰色背景颜色实现


 仔细看第二部分发现也是有背景颜色的和阴影的,只不过很浅,不容易看出来。刚才已经实现了带阴影的背景,稍微改一下颜色就可以了,依旧要结合Stack组件。BoxShadow的两个参数blurRadius和spreadRadius经常使用,其中blurRadius是模糊半径,也就是阴影半径,SpreadRadius是阴影膨胀数值,也就是阴影面积扩大几倍。


class Backgroud extends StatelessWidget {
const Backgroud({super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: const Color.fromARGB(255, 238, 235, 235).withOpacity(0.6),
boxShadow: [
BoxShadow(
color: const Color.fromARGB(255, 204, 203, 203).withOpacity(0.5),
blurRadius: 20, //阴影模糊程度
spreadRadius: 20 ,//阴影扩散程度
offset:const Offset(20,20), //阴影y轴偏移量
)
],
),
);
}
}

 效果图:


8.png


在线状态展示区域实现


 本来想着放一个图片在这个位置就好了,这样简单。但是如果拖动界面,改变小大,那么图片就会变形,很不美观。所以利用横向布局组件Row放在外层,里面包裹Badge组件实现小圆点,通过position、badgeColor等组件调整圆点位置和颜色。


class Top extends StatelessWidget {
const Top({super.key});
@override
Widget build(BuildContext context) {
return Row(children: [
Expanded(
flex: 1,
child: ListTile(
leading: Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 0, 0),
child: Badge(
//小圆点
badgeColor: Colors.orange, //小圆点颜色
position: const BadgePosition(
start: -70, top: 0, end: 0, bottom: 0), //小圆点显示位置
borderSide: const BorderSide(
color: Colors.white, width: 1.5), //外层白色圆圈框框
child: const Text(
//标题
"Family",
style: TextStyle(
fontSize: 12, //字体大小
fontWeight: FontWeight.bold, //字体加粗
color: Colors.black),
),
),
),
)),
Expanded(
flex: 1,
child: ListTile(
leading: Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 0, 0),
child: Badge(
//小圆点
badgeColor: Colors.cyan, //小圆点颜色
position: const BadgePosition(
start: -70, top: 0, end: 0, bottom: 0), //小圆点显示位置
borderSide: const BorderSide(
color: Colors.white, width: 1.5), //外层白色圆圈框框
child: const Text(
//标题
"Friend",
style: TextStyle(
fontSize: 12, //字体大小
fontWeight: FontWeight.bold, //字体加粗
color: Colors.black),
),
),
),
)),
]);
}
}

 效果图:


9.png


好友列表实现


 好友列表之前也实现过,这次在以前的基础上修改。我们使用ListView组件实现列表,ListView是最常用的可滚动组件之一,它可以沿一个方向线性排布所有子组件。底层使用Column结合ListTile组件,ListTile结合CircleAvatar可以实现圆形头像效果,同时也可以设置主副标题,设置 focusColor改变鼠标悬停时列表颜色,


List listData = [  {"title": 'First', "imageUrl": "images/1.jpg", "description": '09:15'},  {"title": 'Second', "imageUrl": "images/2.jpg", "description": '13:10'},];

class FriendList extends StatelessWidget {
const FriendList({super.key});
@override
Widget build(BuildContext context) {
return ListView(
children: listData.map((value) {//重复生成列表
return Column(
children: <Widget>[
ListTile(
onTap: (){},
hoverColor: Colors.black,// 悬停颜色
focusColor: Colors.white,//聚焦颜色
autofocus:true,//自动聚焦
leading: CircleAvatar(//头像
backgroundImage: AssetImage(value["imageUrl"]),
),
title: Text(
value["title"],
style: const TextStyle(
fontSize: 25, //字体大小
color: Colors.black),
),
subtitle: Text(value["description"])),
const Padding(
padding: EdgeInsets.fromLTRB(70, 10, 0, 30),
child: Text(
maxLines: 2,
"There are moments in life when you miss someone so much that you just want to pick them from your dreams and hug them for real!",
style: TextStyle(
fontSize: 12,
height: 2, //字体大小
color: Colors.grey),
),
)
],
);
}).toList(), //注意这里要转换成列表,因为listView只接受列表
);
}
}


 效果图:


10.png


第三个模块设计


 现在来第三个模块,聊天界面。分析模板布局,从上到下依次是一个搜索框,分隔线,聊天主界面,输入框,表情、视频、语音工具栏和发送按钮。我们,从上到下把他分成四个小部分来实现,外层使用Column组件。


class ThirdBlockMain extends StatelessWidget {
const ThirdBlockMain({super.key});
@override
Widget build(BuildContext context) {
return Stack(children: <Widget>[
Column(
//竖直布局
children: const <Widget>[
//子组件
Text("1"),
Divider(
height: 0.5,
indent: 20.0,
color: Colors.grey,
),
Text("2"),
Divider(
height: 0.5,
indent: 20.0,
color: Colors.grey,
),
Text("3"),
Text("4"),

],
),
]);
}
}


 效果图:


11.png


搜索框实现


 之前实现过搜索框,直接拿过来改一改。外层添加一个SizedBox组件来控制一下搜索框的大小和位置。


class SearchWidget extends StatefulWidget {

const SearchWidget(
{Key? key,
this.height,
this.width,
this.hintText,
this.onEditingComplete})
: super(key: key);

@override
State<SearchWidget> createState() => _SearchWidgetState();
}

class _SearchWidgetState extends State<SearchWidget> {
var controller = TextEditingController();
@override
void initState() {
super.initState();
}

@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constrains) {
return SizedBox(
width: 400,
height: 40,
child: TextField(
controller: controller, //控制器
decoration: InputDecoration(
prefixIcon: const Icon(Icons.search), //头部搜索图标
filled: true,
fillColor: Colors.grey.withAlpha(50), // 设置输入框背景色为灰色,并设置透明度
hintText: "Search people",
hintStyle: const TextStyle(color: Colors.grey, fontSize: 14),
contentPadding: const EdgeInsets.only(bottom: 20),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15), //圆角边框
borderSide: BorderSide.none,
),
suffixIcon: IconButton(
//尾部叉叉图标
icon: const Icon(
Icons.close,
size: 17,
),
onPressed: clearKeywords, //清空操作
splashColor: Theme.of(context).primaryColor,
)),
),
);
});
}
}

 效果图:


12.png


聊天,信息发送界面实现


 因为我的这个并不能真的实现聊天,所以就先放text组件在这把吧,后边再进一步完善。这里简单做一些美化操作,输入框不需要背景颜色,图标需要设置成蓝色,同时调节两个模块的长宽高来适应屏幕。输入框使用TextField,与搜索框使用一致。这里要用到StatefulWidget来完成情况输入框的操作。


class ChatUi extends StatelessWidget {
const ChatUi({super.key});
@override
Widget build(BuildContext context) {
return const SizedBox(
width: 100,
height: 400,
child: Text(""),
);
}
}

class InPutUi extends StatefulWidget {

const InPutUi(
{Key? key,
this.height,
this.width,
this.hintText,
this.onEditingComplete})
: super(key: key);

@override
State<InPutUi> createState() => _InPutUi();
}

class _InPutUi extends State<InPutUi> {
var controller = TextEditingController();
@override
void initState() {
super.initState();
}

@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constrains) {
return TextField(
controller: controller, //控制器
decoration: InputDecoration(
filled: true,
fillColor: Colors.white.withAlpha(50), // 设置输入框背景色为灰色,并设置透明度
hintText: "Write something...",
hintStyle: const TextStyle(color: Colors.grey, fontSize: 14),
contentPadding: const EdgeInsets.only(bottom: 20),
border: const OutlineInputBorder(
borderSide: BorderSide.none,
),
suffixIcon: IconButton(
color: Colors.blue,
//尾部叉叉图标
icon: const Icon(
Icons.send,
size: 16,
),
onPressed: clearKeywords, //清空操作
splashColor: Theme.of(context).primaryColor,
)),

);
});
}
}

 效果图:


13.png


底部工具界面实现


 最后来实现底部工具栏。外层使用横向布局来依次放入带图标按钮。这里用到IconButton、MaterialButton两种组件来实现按钮,一种是图标按钮,一种是普通按钮,之前已经实现过,拿来就可以用了。外围使用Padding组件进行填充,方便后期调整每个组件的位置,使它更好看一点。



class Bottom extends StatelessWidget {
const Bottom({super.key});
@override
Widget build(BuildContext context) {
return Row(children: [
Padding(
padding: const EdgeInsets.fromLTRB(30, 0, 0, 0),
child: IconButton(
icon: const Icon(Icons.mood),
tooltip: 'click IconButton',
onPressed: () {},
),
),
Padding(
padding: const EdgeInsets.fromLTRB(580, 20, 0, 22),
child: MaterialButton(
height: 35,
color: Colors.blue,
onPressed: () {}, //点击事件
autofocus: true,
child: const Text(
'Send',
style: TextStyle(
fontSize: 12, //字体大小
fontWeight: FontWeight.bold, //字体加粗
color: Colors.white),
),
),
),
]);
}
}



 效果图:


14.png


总结


 到这里基本上就完成了, 当然,他是不能实际使用的,因为点击、数据交互等功能还没实现,因为我还不会。后期再边学边写吧。


 模板图:


1.png


 完成图:
14.png
 自己实现的与模板还是差距很大的。自己的看起来就没那么美观,我应该去学学美术了,一点艺术细胞都没有。


作者:头好晕呀
来源:juejin.cn/post/7232274061283115045
收起阅读 »

uniapp日常总结--uniapp页面传值

uniapp日常总结--uniapp页面传值在Uniapp中,不同页面之间传值可以通过以下几种方式实现:URL参数传递:可以通过在跳转链接中添加参数,然后在目标页面通过this.$route.params或this.$route.query来获取传递的参数。 ...
继续阅读 »

uniapp日常总结--uniapp页面传值

在Uniapp中,不同页面之间传值可以通过以下几种方式实现:

  1. URL参数传递:

    可以通过在跳转链接中添加参数,然后在目标页面通过this.$route.paramsthis.$route.query来获取传递的参数。


    <uni-link :url="'/pages/targetPage/targetPage?param1=' + value1 + '¶m2=' + value2">跳转到目标页面uni-link>
    // 在目标页面获取参数
    export default {
    mounted() {
    const param1 = this.$route.params.param1;
    const param2 = this.$route.params.param2;
    console.log(param1, param2);
    }
    }
  2. 使用页面参数(Query):

    1. 在触发页面跳转的地方,例如在一个按钮的点击事件中:
    // 在当前页面的某个事件触发时跳转到目标页面,并传递参数
    uni.navigateTo({
    url: '/pages/targetPage/targetPage',
    // 传递的参数,可以是一个对象
    success(res) {
    console.log(res);
    },
    fail(err) {
    console.error(err);
    },
    // 参数传递方式,query 表示通过 URL 参数传递
    // params 表示通过 path 参数传递
    // 一般情况下使用 query 就可以了
    // 使用 params 时,目标页面的路径需要定义成带参数的形式
    // 如 '/pages/targetPage/targetPage/:param1/:param2'
    method: 'query',
    // 要传递的参数
    query: {
    key1: 'value1',
    key2: 'value2'
    }
    });



    //简写 在当前页面的某个事件触发时跳转到目标页面,并传递参数
    uni.navigateTo({
    url: '/pages/targetPage/targetPage?key1=value1&key2=value2',
    });
    1. 在目标页面中,可以通过this.$route.query来获取传递的参数:
export default {
onLoad(query) {
// 获取传递的参数
const key1 = this.$route.query.key1;
const key2 = this.$route.query.key2;

console.log(key1, key2);
},
// 其他页面生命周期或方法等
};

在目标页面的onLoad生命周期中,this.$route.query可以获取到传递的参数。key1key2就是在跳转时传递的参数。如果使用uni.switchTab方法进行页面跳转,是无法直接传递参数的。因为uni.switchTab用于跳转到 tabBar 页面,而 tabBar 页面是在底部显示的固定页面,不支持传递参数。如果需要在 tabBar 页面之间传递参数,可以考虑使用全局变量、本地存储等方式进行参数传递。

  • Vuex状态管理:

    使用Vuex进行全局状态管理,可以在一个页面中修改状态,而在另一个页面中获取最新的状态。

    适用于需要在多个页面之间共享数据的情况。

    如果你的应用使用了Vuex,可以在一个页面的computed属性或methods中触发commit,然后在另一个页面通过this.$store.state获取值。

    在第一个页面:

    // 在页面中触发commit
    this.$store.commit('setValue', value);

    在第二个页面:

    // 在另一个页面获取值
    const value = this.$store.state.value;
    console.log(value);
  • 使用本地存储(Storage):

    使用本地存储(localStorage或uni提供的存储API)将数据存储到本地,然后在另一个页面中读取。适用于需要持久保存数据的情况。如果数据不大,你也可以将数据存储在本地存储中,然后在目标页面读取。

    其中根据使用情景可以使用同步StorageSync或者异步Storage来实现。

    两者存在一定的区别,简单介绍可以查看下方链接:

    uniapp日常总结--setStorageSync和setStorage区别

    同步:使用uni.setStorageSyncuni.getStorageSync等方法,将数据存储在本地,然后在另一个页面读取。

    // 在页面A中保存数据到本地存储
    uni.setStorageSync('key', value);
    // 在页面B中从本地存储中读取数据
    const value = uni.getStorageSync('key');
    console.log(value);

    异步:使用uni.setStorageuni.getStorage等方法,将数据存储在本地,然后在另一个页面读取。

    // 在页面A中保存数据到本地存储
    uni.setStorage({
    key: 'yourDataKey',
    data: yourData,
    });
    // 在页面B中从本地存储中读取数据
    uni.getStorage({
    key: 'yourDataKey',
    success: function (res) {
    const pageData = res.data;
    },
    });
  • 事件总线:

    使用uni提供的API进行页面传值,如uni.$emituni.$on

    通过事件触发和监听的方式在页面之间传递数据。

    使用Uniapp的事件总线来进行组件之间的通信。在发送组件中,使用uni.$emit触发一个自定义事件,并在接收组件中使用uni.$on监听这个事件。

    在发送组件:

    uni.$emit('customEvent', data);

    在接收组件:

    uni.$on('customEvent', (data) => {
    console.log(data);
    });
  • 应用全局对象:

    通过uni.$app访问应用全局对象,从而在不同页面之间共享数据。

    在发送页面:

    uni.$app.globalData.value = data;

    在接收页面:

    const value = uni.$app.globalData.value;
    console.log(value);
  • URL参数传递对于简单的场景比较方便。Vuex适用于较大的应用状态管理。本地存储适用于需要在页面刷新后仍然保持的简单数据。事件总线方法适用于简单的组件通信。页面参数相对常用于跳转。根据具体需求和应用场景,选择合适的方式进行数据传递。不同的场景可能需要不同的方法。


    作者:狐说狐有理
    来源:juejin.cn/post/7310786618390855717

    收起阅读 »

    Flutter 日记APP-开篇

    序言 在跟着wendux大佬的书学习flutter后,开始着手写个app进行实战。考虑到没有服务器,所以主要写工具类,无网络交互的app。之前看了《小狗钱钱》这本书,里面的梦想笔记让我印象深刻,便开始着手写一个记录自己梦想笔记的app。 App 构想 创建自...
    继续阅读 »

    序言


    在跟着wendux大佬的书学习flutter后,开始着手写个app进行实战。考虑到没有服务器,所以主要写工具类,无网络交互的app。之前看了《小狗钱钱》这本书,里面的梦想笔记让我印象深刻,便开始着手写一个记录自己梦想笔记的app。


    App 构想



    1. 创建自己的梦想

      1.1 梦想内容和描述

      1.2 梦想日记提醒时间,开启后会设置闹钟定时提醒

    2. 创建梦想日记

      2.1 日记标题和内容

      2.2 为了方便日记输入,接入苹果的文本扫描功能

      2.3 日记每天可多次添加或修改

    3. 日记走势

      3.1 根据每天记录的日记数量进行统计,展示一个charts图

    4. 设置功能

      4.1 支持日夜模式

      4.2 支持国际化语言切换


    目前大概就这些后面准备持续更新日记内容,比如新增记账日记,记录每一笔开销和收入,然后统计每月的开销和收入,让自己对于自己的账目管理更加一目了然;还有行程记录,比如出行提醒,旅游日记等等。为了后面更好的兼容,在开始构建的时候会预留相应的字段。


    App 三方选择



    1. get

      状态管理、国际化、皮肤管理于一体的三方库。当然还有其他功能,目前app比较简单仅使用这些。在选择的时候也在犹豫,要不要用BlocProvider,相对来说,另外两个三方要更加轻量一些,provider的侵入性也没有那么强。最后选择get是考虑到国际化管理和换肤等,使用get一步到位。比如国际化通常会用intl

    2. sqflite

      用于数据存储,把日记都保存到本地数据库进行缓存。

    3. shared_preferences
      本地轻量数据缓存,主要是用来存语言国际化等配置信息。

    4. easy_refresh

      上拉刷新,下拉加载

    5. fluttertoast

      Toast 弹窗,需要注意如果兼容其他平台(window)的话需要传入context。


    剩下的就是更新库到本地,传统技艺:put get


    基本上就是用了这些,可以说麻雀虽小,五脏俱全。后面会持续分享app的开发进度,和一些开发中遇到的问题。


    作者:WhiteMonkey
    来源:juejin.cn/post/7309158214481772553
    收起阅读 »

    Flutter 实现登录 UI

    本文,我将解析怎么前构建一个用户交互的登录页面。这里,我使用 TextField 挂件,这方便用户输入用户名和密码。还使用 FlatButton 挂件,来处理一些动作。当然,我还使用了 Image 挂件来设定登录页面的 logo。 效果图如下: 第一步: m...
    继续阅读 »

    本文,我将解析怎么前构建一个用户交互的登录页面。这里,我使用 TextField 挂件,这方便用户输入用户名和密码。还使用 FlatButton 挂件,来处理一些动作。当然,我还使用了 Image 挂件来设定登录页面的 logo


    效果图如下:




    第一步: main() 函数

    import 'package:flutter/material.dart';void main() {
    runApp(MyApp());
    }class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return MaterialApp(
    debugShowCheckedModeBanner: false,
    home: LoginDemo(),
    );
    }
    }

    这个 main() 函数也就是应用的入口。MyApp 类中添加了一个 LoginDemo 类作为 home 属性的参数。


    第二步:class LoginDemo


    • 设定脚手架的 appBar 属性来作为应用的标题,如下:
    appBar: AppBar(
    title: Text('Login Page'),
    ),

    • 在本次的 UI 布局中,所有的挂件都会放在 Column 挂件中,然后存放在脚手架的 body 中。Column 中的第一个是存放 Container 挂件,用来处理 Image 挂件。
    Container(
    height: 150.0,
    width: 190.0,
    padding: EdgeInsets.only(top: 40),
    decoration: BoxDecoration(
    borderRadius: BorderRadius.circular(200),
    ),
    child: Center(
    child: Image.asset('asset/images/flutter-logo.png'),
    ),
    ),

    flutter-logo.png 文件存放在 asset/images 文件夹中。我们需要在 pubspec.yaml 文件中配置路径。

    # To add assets to your application, add an assets section, like this:
    assets:
    - asset/images/



    添加完资源之后,我们可以运行应用了。


    • 然后,使用 TextField 挂件处理用户名和密码。 TextField 挂件是一个输入挂件,帮助我们处理用户的输入信息。
    Padding(
    padding: EdgeInsets.all(10),
    child: TextField(
    decoration: InputDecoration(
    border: OutlineInputBorder(),
    labelText: 'User Name',
    hintText: 'Enter valid mail id as abc@gmail.com'
    ),
    ),
    ),
    Padding(
    padding: EdgeInsets.all(10),
    child: TextField(
    obscureText: true,
    decoration: InputDecoration(
    border: OutlineInputBorder(),
    labelText: 'Password',
    hintText: 'Enter your secure password'
    ),
    ),
    ),

    这里的 Padding 挂件能够帮助你设定 TextField 挂件的内边距。



    obscureText 属性值为 true 的时候,帮助我们对 TextField 展示特殊的字符,而不是真正的文本。



    • 我们使用 FlatButton 挂件来处理忘记密码
    FlatButton(
    onPressed: (){
    //TODO FORGOT PASSWORD SCREEN GOES HERE
    },
    child: Text(
    'Forgot Password',
    style: TextStyle(color: Colors.blue, fontSize: 15),
    ),
    ),

    onPressed() 这个函数中,我们可以处理页面跳转或者其他的点击逻辑。


    • 对于登录按钮,我们使用 FlatButton 挂件,但是我们得装饰一下,这里我们使用 Container 进行包裹。
    Container(
    height: 50,
    width: 250,
    decoration: BoxDecoration(
    color: Colors.*blue*, borderRadius: BorderRadius.circular(20),
    ),
    child: FlatButton(
    onPressed: () {
    Navigator.push(
    context,
    MaterialPageRoute(builder: (_) => HomePage()),
    );
    },
    child: Text(
    'Login',
    style: TextStyle(color: Colors.*white*, fontSize: 25),
    ),
    ),
    ),

    上面我们设定了 Container 挂件的 heightwidth 属性,所以 flatbutton 也会获取到相同的高度和宽度。


    decoration 属性允许我们设计按钮,比如颜色 colorColors.blueborderRadiusBorderRadius.circular(20) 属性。


    • 最后指定 Text 挂件以为新用户创建账号

    这里我们可以通过 GestureDetector 挂件的 onTap() 功能进行导航操作。或者创建类似忘记密码按钮的 onPressed() 事件。


    这里是整个项目的完整代码:

    // lib/HomePage.dart

    import 'package:flutter/material.dart';

    class HomePage extends StatefulWidget {
    @override
    _HomePageState createState() => _HomePageState();
    }

    class _HomePageState extends State<HomePage> {
    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text('Home Page'),
    ),
    body: Center(
    child: Container(
    height: 80,
    width: 150,
    decoration: BoxDecoration(
    color: Colors.blue, borderRadius: BorderRadius.circular(10)),
    child: FlatButton(
    onPressed: () {
    Navigator.pop(context);
    },
    child: Text(
    'Welcome',
    style: TextStyle(color: Colors.white, fontSize: 25),
    ),
    ),
    ),
    ),
    );
    }
    }
    // lib/main.dart
    import 'package:flutter/material.dart';

    import 'HomePage.dart';

    void main() {
    runApp(MyApp());
    }

    class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return MaterialApp(
    debugShowCheckedModeBanner: false,
    home: LoginDemo(),
    );
    }
    }

    class LoginDemo extends StatefulWidget {
    @override
    _LoginDemoState createState() => _LoginDemoState();
    }

    class _LoginDemoState extends State<LoginDemo> {
    @override
    Widget build(BuildContext context) {
    return Scaffold(
    backgroundColor: Colors.white,
    appBar: AppBar(
    title: Text("Login Page"),
    ),
    body: SingleChildScrollView(
    child: Column(
    children: <Widget>[
    Padding(
    padding: const EdgeInsets.only(top: 60.0),
    child: Center(
    child: Container(
    width: 200,
    height: 150,
    /*decoration: BoxDecoration(
    color: Colors.red,
    borderRadius: BorderRadius.circular(50.0)),*/
    child: Image.asset('asset/images/flutter-logo.png')),
    ),
    ),
    Padding(
    //padding: const EdgeInsets.only(left:15.0,right: 15.0,top:0,bottom: 0),
    padding: EdgeInsets.symmetric(horizontal: 15),
    child: TextField(
    decoration: InputDecoration(
    border: OutlineInputBorder(),
    labelText: 'Email',
    hintText: 'Enter valid email id as abc@gmail.com'),
    ),
    ),
    Padding(
    padding: const EdgeInsets.only(
    left: 15.0, right: 15.0, top: 15, bottom: 0),
    //padding: EdgeInsets.symmetric(horizontal: 15),
    child: TextField(

    obscureText: true,
    decoration: InputDecoration(
    border: OutlineInputBorder(),
    labelText: 'Password',
    hintText: 'Enter secure password'),
    ),
    ),
    FlatButton(
    onPressed: (){
    //TODO FORGOT PASSWORD SCREEN GOES HERE
    },
    child: Text(
    'Forgot Password',
    style: TextStyle(color: Colors.blue, fontSize: 15),
    ),
    ),
    Container(
    height: 50,
    width: 250,
    decoration: BoxDecoration(
    color: Colors.blue, borderRadius: BorderRadius.circular(20)),
    child: FlatButton(
    onPressed: () {
    Navigator.push(
    context, MaterialPageRoute(builder: (_) => HomePage()));
    },
    child: Text(
    'Login',
    style: TextStyle(color: Colors.white, fontSize: 25),
    ),
    ),
    ),
    SizedBox(
    height: 130,
    ),
    Text('New User? Create Account')
    ],
    ),
    ),
    );
    }
    }


    本文采用意译的方式翻译。原文 levelup.gitconnected.com/login-page-…



    推荐阅读

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

    className 还能这么用,你学会了吗

    抛出问题 className大家都用过吧,用它在react项目中设置样式。它的用法很简单,除了可以设置一个样式外,react中也可以使用className引入多个类样式。 这次在写项目的时候,碰到一个非常小但是当时却一直解决不了的问题。后面在复盘的时候将它解决...
    继续阅读 »

    抛出问题


    className大家都用过吧,用它在react项目中设置样式。它的用法很简单,除了可以设置一个样式外,react中也可以使用className引入多个类样式。


    这次在写项目的时候,碰到一个非常小但是当时却一直解决不了的问题。后面在复盘的时候将它解决了。问题大致是这样的:


    有两个活动页,每个活动页上都有一个活动规则图标来弹出活动规则,活动规则图标距离顶部会有一个值。现在问题就是这个活动规则在这两个活动页距离顶部的这个值是不一样的,但是我已经将这个活动规则图标做成了组件,并在这两个活动页里都调用了它,从而导致两个页面的样式会相同。如下图所示:




    解决问题


    这个问题不算很大,但是属于细节问题。就和我的组长所说的一样,一个项目应该要做到先完成再完美。所以我当时的解决方法是再写一个活动规则组件,只是将距离顶部的值做出修改即可。效果确实是达到了,不过在最后复盘代码的时候,组长注意到了这两个组件,并开始询问我为什么这样做。


    组长:Rule_1Rule_2这两个组件是什么意思,我看它们没有很大的区别呀。


    我便简单说了一下缘由。


    组长接着说:你忘了组件是什么吗?一个CSS样式值不同就大费周章地新增一个组件,这岂不是太浪费了。再去想想其他方案。


    通过这一番谈话我想起了组件化思想的运用,发现之前解决的这个小问题解决的并不够好。于是,我就带着组件化思想又来重新完善它。


    我重新写了一个demo代码,将主要内容和问题在demo代码中体现出来。下面是原版活动规则组件demo代码,之后的代码都是基于demo代码完成的

    import React from "react";
    import "./index.css";
    const Header = ({ onClick }) => {
    return (
    <>
    <div className="container_hd">
    <div
    className='affix'
    onClick={onClick}
    ></div>
    </div>
    </>
    );
    };
    export default Header;

    组件化思想


    我自己问自己:既然已经写好了一个活动规则组件,为什么仅仅因为一个样式值的不同而去新增一个功能一样的组件?很显然,这种方法是最笨的方案。既然是组件,那就应该要有复用性,或者说只需在原有的基础上稍加改动就可达到效果。


    这是样式的问题,因此要从根本上解决问题。单纯地修改 CSS 样式肯定不行,因为两个页面两个不同的样式。


    className 运用


    className 就不用多介绍了,经常能使用,咱们直接来看如何解决问题。在这里我定义了一个 Value 值,用来区分是在哪个页面的,比如分别有提交页和成功页,我在成功页设置一个 Value 值,,然后将 Value 值传入到活动规则组件,那么在活动规则组件里只需要判断 Value 值是否等于成功页的 Value 值即可。在 className 处做一个三元判断,如下所示:

    className={`affix_${Value === "0" ? "main" : "submit"}`}

    相当于如果Value等于0的时候类名为affix_main,否则为affix_submit。最后再css将样式完善即可。完整代码可以参考如下:

    • 成功页组件
    import Header from "./components/Header";

    const Success = () => {
    const Value = "0";
    return (
    <div style={{ backgroundColor: "purple", width: "375px", height: "670px" }}>
    <Header Value={Value}></Header>
    </div>
    );
    };

    export default Success;

    • 活动规则组件
    import React from "react";
    import "./index.css";
    const Header = ({ onClick, Value }) => {
    return (
    <>
    <div className="container_hd">
    <div
    className={`affix_${Value === "0" ? "main" : "submit"}`}
    onClick={onClick}
    ></div>
    </div>
    </>
    );
    };
    export default Header;

    • 活动规则组件样式
    .container_hd {
    width: 100%;
    }
    .affix_main {
    position: absolute;
    top: 32px;
    right: -21px;
    z-index: 9;
    width: 84px;
    height: 26px;
    background: url('./assets/rule.png');
    background-size: contain;
    background-repeat: no-repeat;
    }
    .affix_submit {
    position: absolute;
    top: 12px;
    right: -21px;
    z-index: 9;
    width: 84px;
    height: 26px;
    background: url('./assets/rule.png');
    background-size: contain;
    background-repeat: no-repeat;
    }



    通过对比效果图可以看出,两者的效果确实发生变化。完成之后,我心里在想:为什么当时就没想出这个简单易行的方案呢?动态判断并设置类名,至少比最开始的新增一个组件的方法高级多了。


    总结问题


    对于这个问题的解决就这样告一段落了,虽然看起来比较简单(一个动态设置类名),但是通过这个className的灵活使用,让我对className的用法有了更进一步的掌握,也不得不感叹组件化思想的广泛运用,这里最大程度地将组件化思想通过className 发挥出来。


    因此,希望通过这个问题,来学会className的灵活用法,并理解好组件化思想。当然如果大家还有更好的解决方案的话,欢迎在评论区告诉我。


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

    手机网站支付(在uniapp同时支持H5和app!)

    前言 uniapp开发项目时,遇到对接支付宝手机网站支付。如果仅仅是H5端,那分分钟搞定的(不就是调用后端接口,提交返回表单即可调起支付)。然而,这次需求是H5和App都使用该支付。这倒是新奇了,App中能使用支付宝手机网站支付吗?那它怎么提交表单,怎么处理...
    继续阅读 »

    前言



    uniapp开发项目时,遇到对接支付宝手机网站支付。如果仅仅是H5端,那分分钟搞定的(不就是调用后端接口,提交返回表单即可调起支付)。然而,这次需求是H5和App都使用该支付。这倒是新奇了,App中能使用支付宝手机网站支付吗?那它怎么提交表单怎么处理支付成功时的回调页面跳转




    若你仅H5使用支付宝手机网站支付参考我的文章



    一、使用技术



    1. 解决app如何提交表单:

      renderjs: app-vue 中调用在视图层操作dom,运行for web的js库
      参考文章

    2. 解决app处理支付成功时的回调页面跳转:

      uni.webview.1.5.4.js: 引入该js,使得普通的H5支持uniapp路由跳转接口参考uniapp文档


    二、思路描述



    注意:此处会详细描述思路,请根据自身项目需要自行更改



    step1|✨用户点击支付


    async aliPhonePay() {
    let urlprefix = baseUrl == '/api' ?
    'http://192.168.105.43'
    :
    baseUrl;

    let params = {
    /**1. 支付成功回调页面-中转站*/
    // #ifdef H5
    frontUrl: `${urlprefix}/middle_html/h5.html?type=${this.formartOrderType(this.orderInfo.orderSn)}`,
    // #endif
    // #ifdef APP
    frontUrl: `${urlprefix}/middle_html/app.html?type=${this.formartOrderType(this.orderInfo.orderSn)}`,
    // #endif


    goodsDesc: this.orderInfo.itemName,
    goodsTitle: this.orderInfo.itemName,
    orderSn: this.orderInfo.orderSn,
    orderType: this.formartOrderType(this.orderInfo.orderSn),
    paymentPrice: (this.orderInfo.paymentPrice*1).toFixed(2),
    payChannel: this.paymentType,
    // 快捷支付必传
    bizProtocolNo: this.bankInfo.bizProtocolNo, //用户业务协议号 ,
    payProtocolNo: this.bankInfo.payProtocolNo, //支付协议号
    }

    this.$refs.dyToast.loading()
    let { data } = await PayCenterApi.executePayment(params)
    this.$refs.dyToast.hide()

    /**2. 保存请求得到的表单到strorage,跳转页面*/
    uni.setStorageSync('payForm', data.doPost);
    uni.redirectTo({
    url:`/pages/goods/goodsOrderPay/new-pay-invoke`
    })
    },

    /pages/goods/goodsOrderPay/new-pay-invoke: h5和app都支持的提交表单调起支付方式


    <template>
    <view class="new-pay-invoke-container">
    <view :payInfo="payInfo" :change:payInfo="pay.openPay" ref="pay"></view>
    <u-loading-page loading loading-text="调起支付中"></u-loading-page>
    </view>
    </template>

    <script>
    export default {
    name: 'new-pay-invoke',

    data() {
    return {
    payInfo: ''
    }
    },

    onLoad(options) {
    this.payInfo = uni.getStorageSync('payForm');
    }
    }
    </script>

    <script module="pay" lang="renderjs">
    export default {
    methods: {
    /**h5和app都支持的提交表单调起支付方式*/
    openPay(payInfo, oldVal, ownerInstance, instance) {
    // console.log(payInfo, oldVal, ownerInstance, instance);
    if(payForm) {
    document.querySelector('body').innerHTML = payInfo
    const div = document.createElement('div')
    div.innerHTML = payForm
    document.body.appendChild(div)
    document.forms[0].submit()
    }
    }
    }
    }
    </script>

    <style lang="scss" scoped>

    </style>

    step2|✨支付成功回调页面


    app.html: 作为一个网页,放到线上服务器,注意需要与传递给后端回调地址保持一致


    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
    <title>app支付成功回调页面-中转站</title>
    </head>
    <body>
    <!-- uni 的 SDK -->
    <!-- 需要把 uni.webview.1.5.4.js 下载到自己的服务器 -->
    <script type="text/javascript" src="https://gitee.com/dcloud/uni-app/raw/dev/dist/uni.webview.1.5.4.js"></script>
    <script type="text/javascript">
    // 待触发 `UniAppJSBridgeReady` 事件后,即可调用 uni 的 API。
    document.addEventListener('UniAppJSBridgeReady', function() {
    /**引入uni.webview.1.5.4.js后,就支持uni各种路由跳转,使得该H5页面能控制uniapp App页面跳转*/
    /**这里做的事是判断订单类型,跳转到app对应的订单支付成功页面 */
    uni.reLaunch({
    url: '对应支付成功页面?payCallback=1'
    // 加payCallback=1参数原因:支付成功页面有时是订单记录,而订单
    // 记录不用走支付流程,用户也能进入。这时就需要该参数判断点击
    // 返回是 返回上一级 还是 返回首页了
    });
    });
    </script>
    </body>
    </html>


    h5.html:与app.html做法一致,但不需要用到uni.webview.1.5.4.js,这里就不赘述了


    以上就是app和h5使用支付宝手机网站支付的全部流程了。
    app有点小瑕疵(app提交表单页面后,支付宝页面导航栏会塌陷到状态栏,用户体验稍微差点)
    我的猜想:
    h5按正常表单提交走,而app利用<webview src="本地网页?表单参数" />本地网页,获取表单参数并拼接表单提交
    还没具体去实现这个猜想,或者大家有更好的解决方式,欢迎评论区展示!!!

    作者:爆竹
    来源:juejin.cn/post/7276692859967864891
    收起阅读 »

    React的并发悖论

    大家好,我卡颂。 当一个React应用逻辑变得复杂后,组件render花费的时间会显著增长。如果从组件render到视图渲染期间消耗的时间过长,用户就会感知到页面卡顿。 为了解决这个问题,有两个方法:让组件render的过程从同步变为异步,这样render过程...
    继续阅读 »

    大家好,我卡颂。


    当一个React应用逻辑变得复杂后,组件render花费的时间会显著增长。如果从组件render视图渲染期间消耗的时间过长,用户就会感知到页面卡顿。


    为了解决这个问题,有两个方法:

    1. 组件render的过程从同步变为异步,这样render过程页面不会卡死。这就是并发更新的原理

    2. 减少需要render的组件数量,这就是常说的React性能优化


    通常,对于不同类型组件,我们会采取以上不同的方法。比如,对于下面这样的有耗时逻辑的输入框,方法1更合适(因为并发更新能减少输入时的卡顿):

    function ExpensiveInput({onChange, value}) {
    // 耗时的操作
    const cur = performance.now();
    while (performance.now() - cur < 20) {}

    return <input onChange={onChange} value={value}/>;
    }

    那么,能不能在整个应用层面同时兼顾这2种方式呢?答案是 —— 不太行。


    这是因为,对于复杂应用,并发更新与性能优化通常是相悖的。就是本文要聊的 —— 并发悖论。


    欢迎加入人类高质量前端交流群,带飞


    从性能优化聊起


    对于一个组件,如果希望他非必要时不render,需要达到的基本条件是:props的引用不变。


    比如,下面代码中Child组件依赖fn props,由于fn是内联形式,所以每次App组件render时引用都会变,不利于Child性能优化:

    function App() {
    return <Child fn={() => {/* xxx */}}/>
    }

    为了Child性能优化,可以将fn抽离出来:

    const fn = () => {/* xxx */}

    function App() {
    return <Child fn={fn}/>
    }

    fn依赖某些props或者state时,我们需要使用useCallback

    function App({a}) {
    const fn = useCallback(() => a + 1, [a]);
    return <Child fn={fn}/>
    }

    类似的,其他类型变量需要用到useMemo


    也就是说,当涉及到性能优化时,React的代码逻辑会变得复杂(需要考虑引用变化问题)。


    当应用进一步复杂,会面临更多问题,比如:

    • 复杂的useEffect逻辑

    • 状态如何共享


    这些问题会与性能优化问题互相叠加,最终导致应用不仅逻辑复杂,性能也欠佳。


    性能优化的解决之道


    好在,这些问题有个共同的解决方法 —— 状态管理。


    上文我们聊到,对于性能优化,关键的问题是 —— 保持props引用不变。


    在原生React中,如果a依赖bb依赖c。那么,当a变化后,我们需要通过各种方法(比如useCallbackuseMemo)保持bc引用的稳定。


    做这件事情本身(保持引用不变)对开发者来说就是额外的心智负担。那么,状态管理是如何解决这个问题的呢?


    答案是:状态管理库自己管理所有原始状态以及派生状态。


    比如:

    • Recoil中,基础状态类型被称为Atom,其他派生状态都是基于Atom组合而来

    • Zustand中,基础状态都是create方法创建的实例

    • Redux中,维护了一个全局状态,对于需要用到的状态通过selector从中摘出来


    这些状态管理方案都会自己维护所有的基础状态与派生状态。当开发者从状态管理库中引入状态时,就能最大限度保持props引用不变。


    比如,下例用Zustand改造上面的代码。由于状态a和依赖afn都是由Zustand管理,所以fn的引用始终不变:

    const useStore = create(set => ({
    a: 0,
    fn: () => set(state => ({ a: state.a + 1 })),
    }))


    function App() {
    const fn = useStore(state => state.fn)
    return <Child fn={fn}/>
    }

    并发更新的问题


    现在我们知道,性能优化的通用解决途径是 —— 通过状态管理库,维护一套逻辑自洽的外部状态(这里的外部是区别于React自身的状态),保持引用不变。


    但是,这套外部状态最终一定会转化为React的内部状态(再通过内部状态的变化驱动视图更新),所以就存在状态同步时机的问题。即:什么时候将外部状态与内部状态同步?


    在并发更新之前的React中,这并不是个问题。因为更新是同步、不会被打断的。所以对于同一个外部状态,在整个更新过程中都能保持不变。


    比如,在如下代码中,由于List组件的render过程不会打断,所以list在遍历过程中是稳定的:

    function List() {
    const list = useStore(state => state.list)
    return (
    <ul>
    {list.map(item => <Item key={item.id} data={item}/>}
    </ul>
    )
    }

    但是,对于开启并发更新的React,更新流程可能中断,不同的Item组件可能是在中断前后不同的宏任务中render,传递给他们的data props可能并不相同。这就导致同一次更新,同一个状态(例子中的list)前后不一致的情况。


    这种情况被称为tearing(视图撕裂)。


    可以发现,造成tearing的原因是 —— 外部状态(状态管理库维护的状态)与React内部状态的同步时机出问题。


    这个问题在当前React中是很难解决的。退而求其次,为了让这些状态库能够正常使用,React专门出了个hook —— useSyncExternalStore。用于将状态管理库触发的更新都以同步的方式执行,这样就不会有同步时机的问题。


    既然是以同步的方式执行,那肯定没法并发更新啦~~~


    总结


    实际上,凡是涉及到自己维护了一个外部状态的库(比如动画库),都涉及到状态同步的问题,很有可能无法兼容并发更新。


    所以,你会更倾向下面哪种选择呢:

    1. care并发更新,以前React怎么用,现在就怎么用

    2. 根据项目情况,平衡并发更新与性能优化的诉求


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

    2个奇怪的React写法

    大家好,我卡颂。 虽然React官网用大量篇幅介绍最佳实践,但因JSX语法的灵活性,所以总是会出现奇奇怪怪的React写法。 本文介绍2种奇怪(但在某些场景下有意义)的React写法。也欢迎大家在评论区讨论你遇到过的奇怪写法。 欢迎加入人类高质量前端交流群,带...
    继续阅读 »

    大家好,我卡颂。


    虽然React官网用大量篇幅介绍最佳实践,但因JSX语法的灵活性,所以总是会出现奇奇怪怪的React写法。


    本文介绍2种奇怪(但在某些场景下有意义)的React写法。也欢迎大家在评论区讨论你遇到过的奇怪写法。


    欢迎加入人类高质量前端交流群,带飞


    ref的奇怪用法


    这是一段初看让人很困惑的代码:

    function App() {
    const [dom, setDOM] = useState(null);

    return <div ref={setDOM}></div>;
    }

    让我们来分析下它的作用。


    首先,ref有两种形式(曾经有3种):

    1. 形如{current: T}的数据结构

    2. 回调函数形式,会在ref更新、销毁时触发


    例子中的setDOMuseStatedispatch方法,也有两种调用形式:

    1. 直接传递更新后的值,比如setDOM(xxx)

    2. 传递更新状态的方法,比如setDOM(oldDOM => return /* 一些处理逻辑 */)


    在例子中,虽然反常,但ref的第二种形式和dispatch的第二种形式确实是契合的。


    也就是说,在例子中传递给refsetDOM方法,会在div对应DOM更新、销毁时执行,那么dom状态中保存的就是div对应DOM的最新值。


    这么做一定程度上实现了感知DOM的实时变化,这是单纯使用ref无法具有的能力。


    useMemo的奇怪用法


    通常我们认为useMemo用来缓存变量propsuseCallback用来缓存函数props


    但在实际项目中,如果想通过缓存props的方式达到子组件性能优化的目的,需要同时保证:

    • 所有传给子组件的props的引用都不变(比如通过useMemo

    • 子组件使用React.memo


    类似这样:

    function App({todos, tab}) {
    const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]);

    return <Todo data={visibleTodos}/>;
    }

    // 为了达到Todo性能优化的目的
    const Todo = React.memo(({data}) => {
    // ...省略逻辑
    })

    既然useMemo可以缓存变量,为什么不直接缓存组件的返回值呢?类似这样:

    function App({todos, tab}) {
    const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]);

    return useMemo(() => <Todo data={visibleTodos}/>, [visibleTodos])
    }

    function Todo({data}) {
    return <p>{data}</p>;
    }

    如此,需要性能优化的子组件不再需要手动包裹React.memo,只有当useMemo依赖变化后子组件才会重新render


    总结


    除了这两种奇怪的写法外,你还遇到哪些奇怪的React写法呢?


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

    Kotlin注解探秘:让代码更清晰

    快速上手 @Target(   AnnotationTarget.CLASS,   AnnotationTarget.FUNCTION,   AnnotationTar...
    继续阅读 »

    快速上手


    @Target(  
     AnnotationTarget.CLASS,  
     AnnotationTarget.FUNCTION,  
     AnnotationTarget.VALUE_PARAMETER,  
     AnnotationTarget.EXPRESSION,  
     AnnotationTarget.CONSTRUCTOR  
    )

    @Retention(AnnotationRetention.SOURCE)
    @Repeatable
    @MustBeDocumented
    annotation class MyAnnotation

    @MyAnnotation @MyAnnotaion class Test @MyAnnotation constructor(val name: String) {
        @MyAnnotation fun test(@MyAnnotation num: Int)Int = (@MyAnnotation 1)
    }

    注解的声明


    注解使用关键字annotation来声明,比如快速上手中的例子,使用annotation class MyAnnotation就声明了一个注解,我们可以按照定义的规则将其放在其他元素身上


    元注解


    下面的注解了解过Java的肯定不陌生,元注解就是可以放在注解上面的注解





    • @Target: 用来指定注解可以应用到哪些元素上,有以下可选项



      • CLASS: 可以应用于类、接口、枚举类



      • ANNOTATION_CLASS: 可以应用于注解



      • TYPE_PARAMETER



      • PROPERTY



      • FIELD



      • LOCAL_VARIABLE



      • VALUE_PARAMETER: 可以应用于字面值



      • CONSTRUCTOR: 可以应用于构造函数



      • FUNCTION: 可以应用于函数



      • PROPERTY_GETTER



      • PROPERTY_SETTER



      • TYPE



      • EXPRESSION: 可以应用于表达式



      • FILE



      • TYPEALIAS





    • @Retention: 用来指定注解的生命周期



      • SOURCE: 仅保存在源代码中



      • BINARY: 保存在字节码文件中,但是运行是无法获取



      • RUNTIME: 保存在字节码文件中,运行时可以获取





    • @Repeatable: 允许此注解可以在单个元素上多次使用


    拿上方的代码来简单介绍几个元注解


    @Target


    可以看一下@Target的源码


     * This meta-annotation indicates the kinds of code elements which are possible targets of an annotation.
     *
     * If the target meta-annotation is not present on an annotation declaration, the annotation is applicable to the following elements:
     * [CLASS], [PROPERTY], [FIELD], [LOCAL_VARIABLE], [VALUE_PARAMETER], [CONSTRUCTOR], [FUNCTION], [PROPERTY_GETTER], [PROPERTY_SETTER].
     *
     * @property allowedTargets list of allowed annotation targets
     */
    @Target(AnnotationTarget.ANNOTATION_CLASS)
    @MustBeDocumented
    public annotation class Target(vararg val allowedTargets: AnnotationTarget)

    在源码中可以看到,Target注解中要传入的参数为allowedTargets,使用了vararh关键字,可传入多个参数,参数的类型为AnnotationTarget,它是一个枚举类,再进入AnnotationTarget的源码就可以看到它有上方元注解中列出的那些。


    在快速上手的示例中我们的@Target中传入了Class FUNCTION VALUE_PARAMETER EXPRESSION CONSTRCTOR,表示此注解可以放在类、接口、枚举、函数、字面值、表达式和构造函数上


    @Retention


    此注解就是指定它什么时候失效 默认是RUNTIME, 快速上手中是用的SOURCE,表示它仅存在于源码中,在编译成字节码后将会消失,如果指定了BINARY,则可以存在于字节码文件中,但是运行时无法获取,反射无法获取


    注解的属性


    注解可在主构造参数内传值


    annotation class MyAnnotation2(val effect: String)

    class Test2 {
        @MyAnnotation2("Test")
        fun test() {
            println("Run test")
        }
    }

    比如上面的例子,可以在主构造函数内传入一个参数,参数支持的类型有以下几种





    • Kotlin中的八种“基本数据类型”(Byte, Short, Int, Long, Float, Double, Boolean, Char)



    • String类型



    • 引用类型(Class)



    • 枚举类型



    • 注解类型



    • 以上类型的数组类型 需要注意的是,官网中特别说明参数不可以传入可空类型,比如"String?",因为JVM不支持null存储在注解的属性中


    注解的作用


    如果是熟悉Java的开发者对注解的作用肯定是非常熟悉。 注解可以提供给编译器、运行时环境、其他代码库以及框架提供很多可用信息。 可用作标记,可供第三方技术库、框架识别信息,比如大家熟悉的SpringBoot,很多事情就是通过注解和反射来实现 可用来提供更多的上下文信息,比如方法的类型参数、返回值类型、错误处理


    后面可结合反射来深入理解Kotlin在开发中的用途


    作者:AB-style
    来源:mdnice.com/writing/5b8eb45e3b1e4b23a57926bd58b7f540
    收起阅读 »

    原生应用要亡了!

    iOS
    跨平台混合应用(及替代方案)取代了性能优先的原生应用 纯粹的原生应用通常是一种依赖于平台的GUI程序, 它使用特定操作系统的本地开发语言和GUI框架. 例如, Gedit 是一个原生应用, 因为它使用 C 和 GTK 作为实现依赖. Notepad++ 是一...
    继续阅读 »

    跨平台混合应用(及替代方案)取代了性能优先的原生应用




    纯粹的原生应用通常是一种依赖于平台的GUI程序, 它使用特定操作系统的本地开发语言和GUI框架. 例如, Gedit 是一个原生应用, 因为它使用 C 和 GTK 作为实现依赖. Notepad++ 是一个原生应用, 因为它使用 C/C++ 和 Win32 GUI API. 这些原生应用还保留了操作系统特有的UI/UX原则和本地功能. 因此, 电脑用户可以轻松上手并与其他内置原生应用一起使用这些应用. 这些传统的原生应用即使在低端硬件上也能流畅运行, 因为它们没有使用中间消息传递模块或嵌入式渲染/代码执行引擎--它们只是触发内置SDK功能的二进制文件. 原生桌面应用和移动应用开发的情况都是一样的.


    混合应用开发运动结束了原生应用开发的黄金时代, 但却创造了一种新的方式, 可以在创纪录的时间内构建类似原生的跨平台应用. 此外, 混合应用的性能问题导致了另一种使用自定义渲染表面和代码执行环境的类原生应用的发展.


    让我们来谈谈传统原生应用开发的弊端.


    Why Native Apps Are the Best 为什么原生应用是最好的


    每个操作系统通常都预装了通用的GUI软件程序. 例如, Ubuntu提供了原生终端, 文本编辑器, Settings应用, 文件管理器等. 这些内置应用无疑遵循了相同的UI/UX原则, 而且由于出色的软件设计和原生SDK的使用, 占用的磁盘空间, 内存和CPU处理能力更低. 第三方原生应用的工作原理也与内置操作系统应用相同. 它们不会过度使用系统资源, 而是根据为用户提供的功能公平地使用计算能力.


    从所有面向用户的角度来看, 原生应用都非常出色. 它们绝不会拖慢低端电脑的运行速度. 此外, 它们也不会敦促用户改变操作系统特有的UI/UX做法. 看看Remmina RDP(原生GUI程序)与Ubuntu内置终端的对比:



     Remmina和Ubuntu上的终端


    每个移动操作系统都提供了原生SDK, 用于开发特定平台的应用捆绑包. 例如, 您可以使用Android SDK构建高性能, 轻量级和用户友好的移动应用. 看看著名的VLC媒体播放器的Android版本是如何通过XML布局实现"关于"视图的:



     VLC Android项目实现了原生应用视图.


    混合应用: 类似本地的Web应用


    即使原生应用为用户提供了最好的GUI程序, 为什么现代开发人员还是开始开发混合应用呢? 从应用用户的角度来看, 原生应用是非常好的, 但它们却给应用开发人员带来了一个关键问题. 尽管一些操作系统提供了与POSIX标准类似的底层应用接口, 但大多数内置的应用开发SDK都提供了不同编程语言的不同应用接口. 因此, 应用开发人员不得不为一个软件产品维护多个与平台相关的代码库. 这种情况增加了跨平台原生应用的开发难度, 因为一个新功能需要多个特定平台的实现.


    混合应用开发通过提供统一的SDK和语言来为多个平台开发应用, 从而解决了这一问题. 开发人员开始使用Electron, NW.js, Apache Cordova和类似Ionic的框架, 利用Web技术构建跨平台应用. 这些框架在Web浏览器组件内呈现基于HTML的类原生应用GUI, 并通过本地-JavaScript接口和桥接器调用基于JavaScript封装的特定平台本地API. 看看Skype如何在Ubuntu上用HTML呈现类似本地的屏幕:



     Skype的首选项窗口.


    桌面应用配有Web浏览器和Node.js运行模块. 移动应用则使用现有的特定平台浏览器视图(即Android Webview).


    混合应用解决方案解决了开发人员的问题, 却给用户带来了新的麻烦. 由于基于Web的解析和渲染, 混合应用的运行速度比原生应用慢数百倍. 一个简单的跨平台计算器应用可能会占用数百兆字节的存储空间. 运行多个跨平台应用窗口就像运行多个重型Web浏览器. 不幸的是, 大多数用户甚至感觉不到这些问题, 因为他们使用的是功能强大的现代硬件组件.


    混合替代方案的兴起


    一些开发人员仍然非常关注应用的性能--他们需要应用在低端机器上也能使用. 因此, 他们开始开发更接近原生应用的跨平台应用, 而不使用Web视图驱动方法. 开发人员开始使用Flutter和类似React Native的框架. 与基于网页视图的方法相比, 这些框架为跨平台应用开发提供了更好的解决方案, 但它们无法像真正的原生应用那样进行开发.


    Flutter没有使用原生的, 特定平台的UI/UX原则. React Native在每个应用中嵌入了JavaScript引擎, 性能不如原生应用. 与基于网页视图的方法相比, 这些混合替代方案无疑提供了更好的跨平台开发解决方案, 但在应用大小和性能方面仍无法与真正的原生应用相媲美.


    你可以从以下报道中了解Flutter如何与混合应用开发(Electron)竞争:


    拜拜Electron, 你好Flutter


    混合(和替代方案)赢得了软件市场!


    每个商业实体都试图通过开发网站和Web应用进入互联网. 与独立的应用相比, 计算机用户更愿意使用在线服务. 因此, Web浏览器开始改进, 增加了各种以开发者为中心的功能, 如新的Web API, 可访问性支持, 离线支持等. 对开发人员友好的JavaScript鼓励每个开发人员在任何情况下都使用它.


    借助混合应用开发技术, 开发人员可以在最短时间内将现有的Web应用转化为桌面应用(如WhatsApp, Slack 等). 他们将React, Vue和Svelte应用与本地窗口框架封装在一起, 创建了功能齐全的跨平台桌面应用. 这种方法节省了数千开发人员的工时和开发成本. 因此, Electron成为了现代桌面应用的开发解决方案. 然后, 一个只需几兆内存和存储空间的代码编辑器程序就变成了现在这样:



     Visual Studio Code占用约600M内存.


    一般用户不会注意到这一点, 因为每个人都至少使用8或16GB内存. 此外, 他们的存储设备也不会让他们感受到 500M字节代码编辑器的沉重(TauriNeutralinojs解决了应用大小的问题, 但它们仍在制作混合应用).


    同样, 如果应用变得缓慢, 典型的移动用户往往会将责任归咎于设备. 现代用户经常升级设备, 以解决应用开发人员造成的性能问题. 因此, 在当今的软件开发行业, 混合应用开发比本地应用开发更受欢迎. 此外, 混合替代方案(如 Flutter, React Native等)也变得更加流行.


    总结一下


    混合应用开发框架和其他替代框架为构建跨平台应用提供了一个高效, 开发人员优先的环境. 但是, 从用户的角度来看, 这些开发方法会产生一些隐藏的性能和可用性问题. 现代强大的硬件组件处理能力可以掩盖这些开发方法中的技术问题. 此外, 与依赖平台的原生应用开发相比, 这些方法提供了更富有成效, 开发人员优先的开发环境. 编程新手开始学习桌面应用的Electron开发, 移动应用的Flutter开发和React Native开发, 就像他们跳过C作为他们的第一门编程语言一样.


    因此, 原生应用的黄金时代走到了尽头. 幸运的是, 程序员仍在维护旧的原生应用代码库. 操作系统永远不会将其预先包含的应用迁移到混合应用中. 与此同时, 一些开发人员使用类似SDL的跨平台, 高性能原生绘图库构建轻量级跨平台应用. 尽管现代混合应用开发和替代方法已成为软件行业的默认方式, 但我们仍可以保留现有的纯原生定位.


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

    flutter有哪些架构的框架?该怎么选择

    flutter有哪些架构的框架? Flutter是一种新兴的跨平台移动应用开发框架,它提供了丰富的UI组件和工具,使得应用开发更加容易。在Flutter中,有很多架构框架可供选择,以下是几个比较常用的架构框架:BLoC (Business Logic Comp...
    继续阅读 »

    flutter有哪些架构的框架?


    Flutter是一种新兴的跨平台移动应用开发框架,它提供了丰富的UI组件和工具,使得应用开发更加容易。在Flutter中,有很多架构框架可供选择,以下是几个比较常用的架构框架:

    1. BLoC (Business Logic Component):BLoC是一种状态管理模式,它将应用程序中的业务逻辑和UI分离,使得应用程序更易于维护和测试。在Flutter中,可以使用flutter_bloc库来实现BLoC架构。 Provider:Provider是Flutter中的一个轻量级状态管理库,它使用InheritedWidget实现状态共享,可以有效地解决Flutter应用中的状态管理问题。
    2. MobX:MobX是一种基于响应式编程的状态管理库,它使用可观察对象来管理应用程序的状态,并自动更新与之相关的UI组件。在Flutter中,可以使用mobx库来实现MobX架构。
    3. Redux:Redux是一种流行的状态管理模式,在Flutter中也有相应的实现库redux_flutter。Redux通过单一数据源管理应用程序的状态,并使用纯函数来处理状态的更新,可以有效地解决Flutter应用中的状态管理问题。 以上是常用的Flutter架构框架,每个框架都有其优点和适用场景,开发者可以根据自己的需求选择合适的架构框架。

    除了上面提到的框架之外,还有以下几个Flutter架构框架:

    1. GetX:GetX是一种轻量级的Flutter架构框架,它提供了路由管理、状态管理和依赖注入等功能,可以大大简化Flutter应用的开发。
    2. MVC:MVC是一种经典的软件架构模式,它将应用程序分为模型、视图和控制器三个部分,可以有效地分离关注点,使得应用程序更易于维护和扩展。
    3. MVP:MVP是一种衍生自MVC的架构模式,它将应用程序分为模型、视图和Presenter三个部分,Presenter负责处理业务逻辑,将模型数据展示到视图上。
    4. MVVM:MVVM是一种流行的架构模式,它将应用程序分为模型、视图和视图模型三个部分,视图模型负责处理业务逻辑,将模型数据展示到视图上。

    总之,Flutter中有很多架构框架可供选择,每个框架都有其优点和适用场景,开发者可以根据自己的需求选择合适的架构框架。


    Flutter BLoC


    Flutter BLoC是一种状态管理模式,它将应用程序中的业务逻辑和UI分离,使得应用程序更易于维护和测试。BLoC这个缩写代表 Business Logic Component,即业务逻辑组件。
    BLoC的核心思想是将UI层和业务逻辑层分离,通过Stream或者Sink等异步编程方式,将UI层和业务逻辑层连接起来。具体来说,BLoC模式包含以下三个部分:
    Events:事件,即UI层的用户操作或其他触发条件,例如按钮点击,网络请求完成等等。
    Bloc:业务逻辑层,用于处理Events,处理业务逻辑,并向UI层提供状态更新。
    State:状态,即UI层的显示状态,例如页面的loading状态,数据请求成功或失败状态等等。


    BLoC的核心是Bloc和State之间的联系,Bloc接收Events,并根据业务逻辑处理后,通过Stream向UI层提供状态更新。UI层监听Bloc的Stream,获取最新的State,并根据State更新UI状态。
    在Flutter中,可以使用StreamController来创建BLoC。StreamController是一个异步数据流控制器,可以创建一个Stream用于事件流,创建一个Sink用于事件的注入。
    Flutter框架提供了一个非常好的BLoC框架flutter_bloc,它封装了BLoC的核心逻辑,使得开发者可以更加方便地使用BLoC模式进行状态管理。使用flutter_bloc框架,只需要定义好Events、Bloc和State,然后将它们组合起来,就可以实现状态管理,从而将UI层和业务逻辑层分离。


    总之,Flutter BLoC是一种状态管理模式,它通过将UI层和业务逻辑层分离,使用Stream或Sink等异步编程方式将它们连接起来,实现了应用程序的业务逻辑和UI分离。


    如何选择使用


    选择使用哪种Flutter架构框架取决于具体的应用程序需求和开发团队的技术水平。以下是一些常见的应用场景和推荐的架构框架:
    对于小型应用程序,可以考虑使用轻量级的状态管理库,如Provider或GetX。
    对于需要处理复杂业务逻辑的应用程序,可以使用BLoC、MobX或Redux等状态管理框架。
    对于需要实现高度可测试性的应用程序,可以考虑使用MVC、MVP或MVVM等经典的软件架构模式。
    总之,选择哪种Flutter架构框架取决于具体的应用程序需求和开发团队的技术水平。开发者应该根据自己的需求和技能水平选择最适合的架构框架,以提高开发效率和代码质量。


    GetX和BLoC的优缺点


    GetX和BLoC都是常见的Flutter架构框架,它们各有优缺点,下面是它们的主要特点和优缺点的比较:


    GetX


    优点:


    简单易用:GetX是一种轻量级的Flutter框架,提供了简单易用的依赖注入、路由管理和状态管理等功能,可以大大简化Flutter应用的开发。
    性能优秀:GetX使用原生的Dart语言构建,不需要任何代码生成,因此运行速度非常快,同时也具有很好的内存管理和性能优化能力。
    功能完备:GetX提供了路由管理、依赖注入、状态管理、国际化、主题管理等功能,可以满足大多数应用程序的需求。


    缺点:


    社区相对较小:相比其他流行的Flutter框架,GetX的社区相对较小,相关文档和教程相对较少,需要一定的自学能力。
    不适合大型应用:由于GetX是一种轻量级框架,不适合处理大型应用程序的复杂业务逻辑和状态管理,需要使用其他更加强大的框架。


    BLoC


    优点:


    灵活可扩展:BLoC提供了灵活的状态管理和业务逻辑处理能力,可以适应各种应用程序的需求,同时也具有良好的扩展性。
    可测试性强:BLoC将UI和业务逻辑分离,提高了代码的可测试性,可以更容易地编写和运行测试代码。
    社区活跃:BLoC是一种流行的Flutter框架,拥有较大的社区和用户群体,相关文档和教程比较丰富,容易入手。


    缺点:


    学习曲线较陡峭:BLoC是一种相对复杂的框架,需要一定的学习曲线和编程经验,初学者可能需要花费较多的时间和精力。
    代码量较大:由于BLoC需要处理UI和业务逻辑的分离,因此需要编写更多的代码来实现相同的功能,可能会增加开发成本和维护难度。
    总之,GetX和BLoC都是常见的Flutter架构框架,它们各有优缺点。选择哪种框架取决于具体的应用程序需求和开发团队的技术水平。


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

    一个写了3年半flutter的小伙,突然写了2个月uniapp的感悟!

    前言 因为某些原因,在过去的三年半时间,我除了flutter之外,很少接触其他的框架,期间除了学习了Android(主要是Kotlin、jetpack)、GoLang Gin之外基本上很少接触其他的框架。而在最近的两个月,突然来了一个要求用uniapp实现的项...
    继续阅读 »

    前言


    因为某些原因,在过去的三年半时间,我除了flutter之外,很少接触其他的框架,期间除了学习了Android(主要是Kotlin、jetpack)、GoLang Gin之外基本上很少接触其他的框架。而在最近的两个月,突然来了一个要求用uniapp实现的项目,在接下这个前,我是有些抵触的。第一点是觉得自己短期内去学一个新的框架,学到的东西不足以完成整个项目,第二点是不想脱离舒适圈。当然,最后我还是选择了直面困难,不然您也看不到这篇文章了🤣。


    本文更多的是帮助您解决是否要学习uni-app或flutter框架的这一问题,以及两个框架的一些代码对比。如果您想要判断的是一个新项目该使用哪个框架,那么本文就不是很合适了~


    跨平台层面的对比感悟


    在Flutter刚出来的这几年,经常会在各种跨平台框架对比的文章下,看到将其与uni-app进行比较。当时我也没有在意太多,以为uni-app也是个差不多的“正经”跨平台框架,但当我打开uni-app官网的时候,我震惊了,因为我看到了这样一句话:一套代码编到15个平台,这不是梦想。我瞬间就傻眼了,这么nb?Flutter不也才横跨六大平台 ?在仔细一想,不对啊,这哪来的15个平台?再仔细一看,然后我的心中只剩下一万个省略号了,横跨一堆小程序平台是吧...



    学习成本的对比感悟


    1. 开发语言的不同

    Flutter,要求开发者学习dart,了解dart和flutter的API,最好还会写点原生。而uni-app只需要学Vue.js,没有附加专有技术。所以从学习一个框架来看,很明显uni-app的学习成本很低。而从我个人的角度去分析,当年我只是一个刚入编程世界的菜鸡中的菜鸡,只学了半年的html+css+js和半年的java。抛开学了1个月的SpringBoot,Flutter可以算是我学习的第一个框架,当时我是直接上手学的Flutter,没有去单独学习dart,因为和java很相似。个人觉得学习成本也还好,如果你喜欢这个框架的话~而最近两个月学习uni-app,我也确实是感受到了学习成本很低,基本上看了看文档,就直接上手了,很多组件的名字也是和flutter大差不差。就是写css有点难受🤣,好在flex布局和flutter的rowcolumn用法一样,基本上半小时就能把基本的、简单的页面布局写好了。


    2. 第三方插件&社区氛围

    截至目前2023.7,flutter在github上有155K的star,uni-app有着38.4K的star。从star的数量也可以看出一个框架的热度,很明显,flutter是远高于uni-app的(毕竟uni-app的主要使用场景还是在国内小程序中)。对于第三方插件呢Flutter有着pub.dev,uni-app有插件市场,但相比Flutter呢可能略显不足。


    3. 开发工具的使用

    Flutter可以选择vscode或者android studio等来进行开发,uni-app可以选择HBuilderX,当然也可以使用vscode,用什么开发工具其实大差不差,如果你一直使用vscode,那么你对工具的使用会更加的熟悉,而如果你和我一样,用的是android studio,再去使用HBuilderX,说实话,有点点难受...例如我最常用的Alt+回车(提示),crtl+alt+l(代码格式化)。当然,反过来也是一样的(●'◡'●)


    编码实现对比


    1. 布局区别


    • 代码整体结构:Flutter使用Widget层级嵌套来构建用户界面,也是被很多人所不喜欢的嵌套地狱(这一点因人而异,根据自己的习惯和代码风格)。 uni-app 使用 Vue.js 的组件化布局方式,templatestylescripttemplate 定义了组件的 HTML 结构,style 定义了组件的样式,script 定义了组件的行为。

    • 布局原理区别:Flutter 中的布局是基于约束的,可以使用Constraints来控制小部件的最大和最小尺寸,并根据父级小部件的约束来确定自身的尺寸。uni-app则是,可以使用类似于 CSS 中 Flex 弹性布局的方式来控制组件的排列和布局。通过设置组件的样式属性,如 display: flexflexjustify-content 等,可以实现垂直和水平方向上的灵活布局。当然flutter也有和flex差不多的rowcolumn

    • 自定义布局:Flutter支持自定义布局,可以通过继承 SingleChildLayoutDelegateMultiChildLayoutDelegate 来实现自定义布局,而uni-app目前并没有直接提供类似的专门用于自定义布局的机制,不过uni-app常见的做法是创建一个自定义组件,并在该组件的 template 中使用各种布局方式、样式和组件组合来实现特定的布局效果。


    2. 状态管理的区别

    Flutter 提供了内置的状态管理机制,最常见的就是通过setState来管理小部件的状态,uni-app是利用Vue.js的响应式数据绑定和状态管理,通过 data 属性来定义和管理组件的状态。


    3. 开发语言的区别与联系

    区别:众所周知,JavaScript 是一门弱类型的语言,而 Dart 是强类型的语言(dart也支持一些弱类型,Dart中弱类型有var, Object 以及dynamic)。Dart有类和接口的概念,并支持面向对象编程,如果你喜欢 OOP 概念,那么你会喜欢使用 Dart 进行开发,此外,它还支持接口、Mixin、抽象类和静态类型等,这一点对写过java的朋友很友好,而JavaScript则支持基于原型的面向对象编程。Dart和JavaScript还有一个重要的区别就是:Dart是类型安全的,使用AOT和JIT编译器编译。


    联系:从一个学习这个两个语言的角度去看, 两者都支持异步编程模型,如 Dart 的 async/await和 JavaScript 的 Promiseasync/await,这就非常友好了。


    4. 一个简单的计数器例子,更好的理解他们直接的区别以及相关的地方:


    Flutter代码:


    import 'package:flutter/material.dart';

    void main() {
    runApp(const MyApp());
    }

    class MyApp extends StatelessWidget {
    const MyApp({super.key});

    @override
    Widget build(BuildContext context) {
    return MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(
    primarySwatch: Colors.blue,
    ),
    home: const MyHomePage(title: 'Flutter Demo Home Page'),
    )
    ;
    }
    }

    class MyHomePage extends StatefulWidget {
    const MyHomePage({super.key, required this.title});
    final String title;

    @override
    State<MyHomePage> createState() => _MyHomePageState();
    }

    class _MyHomePageState extends State {
    int _counter = 0;

    void _incrementCounter() {
    setState(() {
    _counter++;
    });
    }

    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text(widget.title),
    ),
    body: Center(
    child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
    const Text(
    '
    You have pushed the button this many times:',
    )
    ,
    Text(
    '$_counter',
    style:
    Theme.of(context).textTheme.headlineMedium,
    ),
    ],
    ),
    ),
    floatingActionButton:
    FloatingActionButton(
    onPressed: _incrementCounter,
    tooltip: '
    Increment',
    child: const
    Icon(Icons.add),
    ),
    )
    ;
    }
    }

    uniapp代码:


    <template>
    <view class="container">
    <text class="count">{{ count }}text>
    <view class="buttons">
    <button class="btn" @tap="incrementCounter">+button>
    view>
    view>
    template>

    <script>
    export default {
    data() {
    return {
    count: 0,
    };
    },
    methods: {
    incrementCounter() {
    this.count++;
    },
    },
    };
    script>

    <style>
    .container {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: space-between;
    height: 100vh;
    background-color: #f0f0f0;
    }

    .count {
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 48px;
    font-weight: bold;
    height: 100%;
    }

    .buttons {
    display: flex;
    width: 100vw;
    flex-direction: row;
    justify-content: flex-end;
    }

    .btn {
    width: 108rpx;
    height: 108rpx;
    font-size: 24px;
    display: flex;
    justify-content: center;
    align-items: center;
    margin: 8px;
    background-color: #2196F3;
    color: #fff;
    border-radius: 50%;
    }
    style>

    总结


    从App开发的角度来看,uni-app的最大价值在于让国内庞大的Vue开发群体也能够轻松地开发“高性能”的App,不用去承担flutter或react native的学习成本,短时间内开发一款简单的偏展示类的app的话,uni-app肯定是首选,小公司应该挺受益的。再加上uni-app可以同时开发多端小程序,就足以保证在国内有足够的市场。但是稍微有点动效或者说有video、map之类的app,那么要慎重考虑,个人觉得挺限制的。不过很多时候技术并不是一个项目选型第一标准,适合才是,uni-app很适合国内,毕竟试错成本低...


    注:本文仅为一个写了几年flutter小伙,突然写了2个月uniapp的感悟,存在一定个人主观,有错误欢迎指出😘

    作者:编程的平行世界
    来源:juejin.cn/post/7261162911615926331

    收起阅读 »

    像支付宝那样“致敬”第三方开源代码

    前言 通常我们在App中会使用第三方的开源代码,按照许可协议,我们应该在App中公开使用的开源代码并且附上对应的开源协议。当然,实际上只有少部分注重合规性的大厂才会这么干,比如下图是支付宝的关于界面的第三方信息。当然,对于小企业,基本上都不会放使用的第三方开源...
    继续阅读 »

    前言


    通常我们在App中会使用第三方的开源代码,按照许可协议,我们应该在App中公开使用的开源代码并且附上对应的开源协议。当然,实际上只有少部分注重合规性的大厂才会这么干,比如下图是支付宝的关于界面的第三方信息。当然,对于小企业,基本上都不会放使用的第三方开源代码的任何信息。 



    不过,作为一个有“追求”的码农,我们还是想对开源软件致敬一下的,毕竟,没有他们我都不知道怎么写代码。然而,我们的 App 里用了那么多第三方开源插件,总不能一个个找出来一一致敬吧?怎么办?其实,Flutter 早就为我们准备好了一个组件,那就是本篇要介绍的 AboutDialog


    AboutDialog 简介


    AboutDialog 是一个对话框,它可以提供 App 的基本信息,如 Icon、版本、App 名称、版权信息等。 



    同时,AboutDialog还提供了一个查看授权信息(View Licenses)的按钮,点击就可以查看 App 里所有用到的第三方开源插件,并且会自动收集他们的 License 信息展示。所以,使用 AboutDialog 可以让我们轻松表达敬意。怎么使用呢?非常简单,我们点击一个按钮的时候,调用 showAboutDialog 就搞定了,比如下面的代码:

    IconButton(
    onPressed: () {
    showAboutDialog(
    context: context,
    applicationName: '岛上码农',
    applicationVersion: '1.0.0',
    applicationIcon: Image.asset('images/logo.png'),
    applicationLegalese: '2023 岛上码农版权所有'
    );
    },
    icon: const Icon(
    Icons.info_outline,
    color: Colors.white,
    ),
    ),

    参数其实一目了然,具体如下:

    • context:当前的 context
    • applicationName:应用名称;
    • applicationVersion:应用版本,如果要自动获取版本号也可以使用 package_info_plus 插件。
    • applicationIcon:应用图标,可以是任意的 Widget,通常会是一个App 图标图片。
    • applicationLegalese:其他信息,通常会放置应用的版权信息。

    点击按钮,就可以看到相应的授权信息了,点击一项就可以查看具体的 License。我看了一下使用的开源插件非常多,要是自己处理还真的很麻烦。 



    可以说非常简单,当然,如果你直接运行还有两个小问题。


    按钮本地化


    AboutDialog 默认提供了两个按钮,一个是查看授权信息,一个是关闭,可是两个按钮 的标题默认是英文的(分别是VIEW LICENSES和 CLOSE)。 



    如果要改成本地话的,还需要做一个自定义配置。我们扒一下 AboutDialog 的源码,会发现两个按钮在DefaultMaterialLocalizations中定义,分别是viewLicensesButtonLabelcloseButtonLabel。这个时候我们自定义一个类集成DefaultMaterialLocalizations就可以了。

    class MyMaterialLocalizationsDelegate
    extends LocalizationsDelegate<MaterialLocalizations> {
    const MyMaterialLocalizationsDelegate();

    @override
    bool isSupported(Locale locale) => true;

    @override
    Future<MaterialLocalizations> load(Locale locale) async {
    final myTranslations = MyMaterialLocalizations(); // 自定义的本地化资源类
    return Future.value(myTranslations);
    }

    @override
    bool shouldReload(
    covariant LocalizationsDelegate<MaterialLocalizations> old) =>
    false;
    }

    class MyMaterialLocalizations extends DefaultMaterialLocalizations {
    @override
    String get viewLicensesButtonLabel => '查看版权信息';

    @override
    String get closeButtonLabel => '关闭';

    }

    然后在 MaterialApp 里指定本地化localizationsDelegates参数使用自定义的委托类对象就能完成AboutDialog两个按钮文字的替换。

    return MaterialApp(
    debugShowCheckedModeBanner: false,
    title: 'Flutter Demo',
    theme: ThemeData(
    primarySwatch: Colors.blue,
    ),
    home: const AboutDialogDemo(),
    localizationsDelegates: const [MyMaterialLocalizationsDelegate()],
    );

    添加自定义的授权信息


    虽然 Flutter 会自动收集第三方插件,但是如果我们自己使用了其他第三方的插件的话,比如没有在 pub.yaml 里引入,而是直接使用了源码。那么还是需要手动添加一些授权信息的,这个时候我们需要自己手动添加了。添加的方式也不麻烦,Flutter 提供了一个LicenseRegistry的工具类,可以调用其 addLicense 方法来帮我们添加授权信息。具体使用如下:

    LicenseRegistry.addLicense(() async* {
    yield const LicenseEntryWithLineBreaks(
    ['关于岛上码农'],
    '我是岛上码农,微信公众号同名。\f如有问题可以加本人微信交流,微信号:island-coder。',
    );
    });

    这个方法可以在main方法里调用。其中第一个参数是一个数组,是因为可以允许多个开源代码共用一份授权信息。同时,如果一份开源插件有多个授权信息,可以多次添加,只要名称一致,Flutter就会自动合并,并且会显示该插件的授权信息条数,点击查看时,会将多条授权信息使用分割线分开,代码如下所示:

    void main() {
    runApp(const MyApp());
    LicenseRegistry.addLicense(() async* {
    yield const LicenseEntryWithLineBreaks(
    ['关于岛上码农'],
    '我是岛上码农,微信公众号同名。如有问题可以加本人微信交流,微信号:island-coder。',
    );
    });

    LicenseRegistry.addLicense(() async* {
    yield const LicenseEntryWithLineBreaks(
    ['关于岛上码农'],
    '使用时请注明来自岛上码农、。',
    );
    });
    }



    总结


    本篇介绍了在 Flutter 中快速展示授权信息的方法,通过 AboutDialog 就可以轻松搞定,各位“抄代码”的码农们,赶紧用起来向大牛们致敬吧!


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

    如何优化 electron 应用在低配电脑秒启动

    背景 古茗门店使用的收银机,有些会因为使用年限长、装了杀毒软件、配置低等原因性能较差,导致进钱宝启动响应较慢。然后店员在双击进钱宝图标后,发现没反应,就会重复点击 因此我们希望优化到即使在这些性能不太好的收银机上,也能让进钱宝有较快的启动体验 优化思路 测...
    继续阅读 »


    背景


    古茗门店使用的收银机,有些会因为使用年限长、装了杀毒软件、配置低等原因性能较差,导致进钱宝启动响应较慢。然后店员在双击进钱宝图标后,发现没反应,就会重复点击


    因此我们希望优化到即使在这些性能不太好的收银机上,也能让进钱宝有较快的启动体验
    lAHPKHtEUt3mDUzM8Mzw_240_240.gif


    优化思路



    • 测量,得到一个大概的优化目标,并发现可优化的阶段

    • 主要方向是优化主进程创建出窗口的时间、让渲染进程页面尽快显示

    • 性能优化好后,尽量让人感觉上更快点

    • 上报各阶段耗时,建立监控机制,发现变慢了及时优化


    测量


    测量主进程


    编写一个 bat文件 放到应用根目录,通过bat启动程序并获取初始启动时间:


    @echo off

    set "$=%temp%\Spring"
    >%$% Echo WScript.Echo((new Date()).getTime())
    for /f %%a in ('cscript -nologo -e:jscript %$%') do set timestamp=%%a
    del /f /q %$%
    echo %timestamp%
    start yourAppName.exe

    pause

    项目内可以使用如下api打印主进程各时间节点:


    this.window.webContents.executeJavaScript(
    `console.log('start', ${start});console.log('onReady', ${onReady});console.log('inCreateWindow', ${inCreateWindow});console.log('afterCreateWindow', ${afterCreateWindow});console.log('beforeInitEvents', ${beforeInitEvents});console.log('afterInitEvents', ${afterInitEvents});console.log('startLoad', ${startLoad});`
    );

    如果发现主进程有不正常的耗时,可以通过v8-inspect-profiler捕获主进程执行情况,最终生成的文件可以放到浏览器调试工具中生成火焰图


    测量渲染进程


    1、可以console打印时间点,可以借助preformance API获取一些时间节点


    2、可以使用preformance工具测白屏时间等


    image.png


    进钱宝测量结果


    以下测量结果中每一项都是时间戳,括号里是距离上一步的时间(ms)


    最简单状态(主进程只保留唤起主渲染进程窗口的逻辑):


    执行exe(指双击应用图标)开始执行主进程代码主进程ready事件开始初始化渲染进程窗口开始加载渲染进程资源
    16776661416191677666142152(+533)1677666142224(+72)1677666142364(+140)1677666142375(+11)

    未优化状态:


    执行exe开始执行主进程代码主进程ready事件开始初始化渲染进程窗口开始加载渲染进程资源
    16776694148861677669417742(+2856)1677669417856(+114)1677669418043(+187)1677669418061(+18)

    通过上述数据,能看出主进程最大的卡点是执行exe到开始执行代码之间


    渲染进程的白屏时间,最初测试大概是1000ms


    那么我们的优化目标,就是往最简单应用的时间靠齐,优化重点就是主进程开始执行代码时间,和渲染进程白屏时间


    优化步骤


    一、让主进程代码尽快执行


    使用常见的方式,打包、压缩、支持tree-shaking,让代码体积尽可能的小;


    可以把一些依赖按需加载,减少初始包体积


    代码压缩


    使用electron的一个好处是:chrome版本较高,不用pollyfill,可以直接使用很新的es特性


    直接编译目标 ecma2020!!


    优化tree-shaking


    主进程存在global对象,但一些配置性的变量尽量不要挂载在global上,可以放到编译时配置里,以支持更好的tree-shaking


    const exendsGlobal = {
    __DEV__,
    __APP_DIR__,
    __RELEASE__,
    __TEST__,
    __LOCAL__,
    __CONFIG_FILE__,
    __LOG_DRI__,
    GM_BUILD_ENV: JSON.stringify(process.env.GM_BUILD_ENV),
    };

    // 这里把一些变量挂载在global上,这样不利于tree-shaking
    Object.assign(global, exendsGlobal);

    慎用注册快捷方式API


    实测这样的调用是存在性能损耗的


    globalShortcut.register('CommandOrControl+I', () => {
    this.window.webContents.openDevTools();
    });
    // 这个触发方式,我们改为了在页面某个地方连点三下,因为事件监听基本没性能损耗
    // 或者把快捷方式的注册在应用的生命周期中往后移,尽量不影响应用的启动

    优化require


    因为require在node里是一个耗时操作,而主进程最终是打包成一个cjs格式,里面难免有require


    可以使用 node --cpu-prof --heap-prof -e "require('request')" 获取一个包的引用时长。
    如下是一些在我本机的测量结果:


    时长(ms)
    fs-extra83
    event-kit25
    electron-store197
    electron-log61
    v8-compile-cache29

    具体理论分析可以看这里:
    如何加快 Node.js 应用的启动速度


    因此我们可以通过一些方式优化require



    • 把require的包打进bundle

      • 有两个问题

        • bundle体积会增加,这样还是会影响代码编译和加载时间

        • 有些库是必须require的,像node和electron的原生api;就进钱宝来说,我们可以通过其他方式优化掉require,因此没使用这种方式





    • 按需require

    • v8 code cache / v8 snapshot

    • 对应用流程做优化,通过减少启动时的事务,来间接减少启动时的require量


    按需require


    比如fx-extra模块的按需加载方式:


    const noop = () => {};

    const proxyFsExtra = new Proxy(
    {},
    {
    get(target, property) {
    return new Proxy(noop, {
    apply(target, ctx, args) {
    const fsEx = require('fs-extra');
    return fsEx[property](...args);
    },
    });
    },
    }
    );

    export default proxyFsExtra;

    前面的步骤总是做了没坏处,但这个步骤因为要重构代码,因此要经过验证


    因此我们测量一下:


    执行exe开始执行主进程代码主进程ready事件开始初始化渲染进程窗口开始加载渲染进程资源
    16776740873441677674089485(+2141)167767408960616776740898641677674089934

    可以看出,主进程开始执行时间已经有了较大优化(大概700ms)


    v8-compile-cache


    可以直接用 v8-compile-cache 这个包做require缓存


    简单测试如下:


    image.png


    脚本执行时间从388到244,因此这个技术确实是能优化执行时间的


    但也有可能没有优化效果:


    image.png


    在总require较少,且包总量不大的情况下,做cache是没有用的。实测对进钱宝也是没用的,因为经过后面的流程优化步骤,进钱宝代码的初始require会很少。因此我们没有使用这项技术


    但我们还是可以看下这个包的优化机制,这个包核心代码如下,其实是重写了node的Module模块的_compile函数,编译后把V8字节码缓存,以后要执行时直接使用缓存的字节码省去编译步骤


    Module.prototype._compile = function(content, filename) {
    ...

    // 读取编译缓存
    var buffer = this._cacheStore.get(filename, invalidationKey);

    // 这一步是去编译代码,但如果传入的cachedData有值,就会直接使用,从而跳过编译
    // 如果没传入cachedData,这段代码就会产生一份script.cachedData
    var script = new vm.Script(wrapper, {
    filename: filename,
    lineOffset: 0,
    displayErrors: true,
    cachedData: buffer,
    produceCachedData: true,
    });

    // 上面的代码会产生一份编译结果,把编译结果写入本地文件
    if (script.cachedDataProduced) {
    this._cacheStore.set(filename, invalidationKey, script.cachedData);
    }

    // 运行代码
    var compiledWrapper = script.runInThisContext({
    filename: filename,
    lineOffset: 0,
    columnOffset: 0,
    displayErrors: true,
    });

    ...
    };

    这里有个可能的优化点:v8-compile-cache 只是缓存编译结果,但require一个模块除了编译,还有加载这个io操作,因此是否可以考虑连io一起缓存


    v8-snapshot


    image.png


    原理是:把代码执行结果的内存,做一个序列化,存到本地,真正执行时,直接加载然后反序列化到内存中


    这样跳过了代码编译和执行两个阶段,因此可以提升应用的初始化速度。


    优化效果:


    image.png


    对react做快照后,代码中获取的react对象如下图,实际上获得的是一份react库代码执行后的内存快照,跟正常引入react库没什么区别:


    image.png


    这个方案看起来很香,但也存在两个小问题:


    1、不能对有副作用的代码做snapshot


    因为只是覆写内存,而没有实际代码执行,因此如果有 读写文件、操作dom、console 等副作用,是不会生效的


    因此这个步骤更多是针对第三方库,而不是业务代码


    2、需要修改打包配置


    目前项目一般通过import引用各种包,最终把这些包打包到bundle中;但该方案会在内存直接生成对象,并挂载在全局变量上,因此要使用snapshot,代码中包引用方式需要修改,这个可以通过对编译过程的配置实现


    这个技术看起来确实能有优化效果,但考虑如下几点,最后我们没有去使用这项技术:



    • 对主进程没用,因为主进程刚进来就是要做打开窗口这个副作用;

    • 对渲染进程性价比不高,因为

      • 我们的页面渲染已经够快(0.2s)

      • 启动时,最大的瓶颈不在前端,而在服务端初始化,前端会长时间停留在launch页面等待服务端初始化,基于这一点,对渲染进程js初始化速度做极限优化带来的收益基本没有,我们真实需要的是让渲染进程能尽快渲染出来一些可见的东西让用户感知

      • 维护一个新模块、修改编译步骤、引入新模块带来的潜在风险




    snapshot具体应用方式可看文尾参考文章


    二、优化主进程流程,让应该先做的事先做,可以后做的往后放


    D2E73602-B81D-4b87-8929-427AB6C51C2A.png
    基于上图的思想,我们对bundle包做了拆分:


    image.png


    新的测量数据:


    执行exe开始执行主进程代码主进程ready事件开始初始化渲染进程窗口开始加载渲染进程资源
    16779113945161677911395044(+528)1677911395133(+89)--

    可以看出,到这里主进程已经跟最简单状态差不多了。而且这一步明显优化非常明显。而这一步做的事情核心就是减少初始事务,从而减少了初始代码量以减少编译和加载负担,也避免了初始时过多比较耗性能的API的执行(比如require,比如new BrowserWindow())。


    那么我们主进程优化基本已经达到目标


    三、让渲染进程尽快渲染


    requestIdleCallback


    程序刚启动的时候,CPU占用会很高(100%),因此有些启动任务可以通过requestIdleCallback,在浏览器空闲时间执行,让浏览器优先去渲染


    去掉或改造起始时调用sendSync以及使用electron-store的代码


    原因是sendSync是同步执行,会阻塞渲染进程


    而electron-store里面初始时会调用sendSync


    只加载首屏需要的css


    对首屏不需要的ui库、components做按需加载,以减少初始css量,首屏尽量只加载首屏渲染所需的css



    因为css量会影响页面的渲染性能


    使用 tailwind 的同学可能会发现一个现象:如果直接加载所有预置css,页面动画会非常卡,因此 tailwind 会提供 Purge 功能自动移除未使用的css



    少用或去掉modulepreload


    我们使用的是vite,他会自动给一些js做modulepreload。但实测modulepreload(不是preload)是会拖慢首屏渲染的,用到的同学可以测测看


    四、想办法让应用在体验上更快


    使用骨架屏提升用户体感


    程序开始执行 -> 页面开始渲染, 这段时间内可以使用骨架屏让用户感知到应用在启动,而不是啥都没有


    我们这边用c++写了个只有loading界面的exe,在进钱宝启动时首先去唤起这个exe,等渲染进程渲染了,再关掉他(我们首屏就是一个很简单的页面,背景接近下图的纯色,因此loading界面也做的比较简单)


    动画.gif


    渲染进程骨架屏


    渲染进程渲染过程:加载解析html -> 加载并执行js渲染


    在js最终执行渲染前,就是白屏时间,可以在html中预先写一点简单的dom来减少白屏时间


    一个白屏优化黑科技


    我们先看两种渲染效果:


    渲染较快的

    image.png


    image.png


    渲染较慢的

    image.png


    image.png


    接下来看下代码区别:


    快的代码:
    <div id="root">
    <span style="color: #000;">哈哈</span> <!-- 就比下面那个多了这行代码 -->
    <div class="container">
    <div class="loading">
    <span></span>
    </div>
    </div>
    </div>

    慢的代码:
    <div id="root">

    <div class="container">
    <div class="loading">
    <span></span>
    </div>
    </div>
    </div>

    就是多了一行文字,就会更快地渲染出来


    从下图可以看到,文字渲染出来的同时,背景色和loading动画(就中间那几个白点)也渲染出来了


    image.png


    有兴趣的可以测一下淘宝首页,如果去掉所有文字,还是会较快渲染,但如果再去掉加载的css中的一个background: url(.....jpg),首次渲染就会变慢了


    我猜啊。。。 这个叫信息优先渲染原则。。。🐶就是文字图片可以明确传递信息,纯dom不知道是否传递信息,而如果页面里有明确能传递信息的东西,就尽快渲染出来,否则,渲染任务就可能排到其他初始化任务后面了。


    当然了,这只是我根据测试结果反推出来的猜测🐶


    好了,现在我们也可以让渲染进程较快的渲染了(至少能先渲染出来一个骨架屏🤣)


    五、其他


    升级electron版本


    electron 官方也是在不断优化bug和性能的


    保证后续的持续优化


    因为经过后续的维护,比如有人给初始代码加了些不该加的重量,是有可能导致性能下降的


    因此我们可以对各节点的数据做上报,数据大盘,异常告警,并及时做优化,从而能持续保证性能


    总结


    本文介绍了electron应用的优化思路和常见的优化方案。并在进钱宝上取得了实际效果,我们在一台性能不太好的机器上,把感官上的启动时间从10s优化到了1s(可能有人会提个问题,上面列的时间加起来没有10s,为啥说是10s。原因是我们最初是在渲染进程的did-finish-load事件后才显示窗口的,这个时间点是比较晚的)


    这其中最有效的步骤是优化流程,让应该先做的事先做,可以往后的就往后排,根据这个原则进行拆包,可以使得初始代码尽可能的简单(体积小,require少,也能减少一些耗性能的动作)。


    另外有些网上看起来很秀的东西,不一定对我们的应用有用,是要经过实际测量和分析的,比如code-cache 和 snapshot


    还有个点是,如果想进一步提升体验,可以先启动骨架屏应用,再通过骨架屏应用启动进钱宝本身,这样可以做到ms级启动体验,但这样会使骨架屏显示时间更长点(这种体验也不好),也需要考虑win7系统会不会有dll缺失等兼容问题


    最后


    关注公众号「Goodme前端团队」,获取更多干货实践,欢迎交流分享~


    参考文档


    v8 code cache


    v8.dev/blog/improv…

    v8.dev/blog/code-c…

    fed.taobao.org/blog/taofed…

    blog.csdn.net/szengtal/ar…


    v8 snapshot


    http://www.javascriptcn.com/post/5eedbc…

    blog.inkdrop.app/how-to-make…

    github.com/inkdropapp/…


    其他


    zhuanlan.zhihu.com/p/420238372


    blog.csdn.net/qq_37939251…


    medium.com/@felixriese…


    zhuanlan.zhihu.com/p/376

    638202

    收起阅读 »

    算法基础:归并排序

    上一篇文章介绍了什么是分治思想,今天就来看一下它其中一个继承人-- 归并排序,本章主要介绍归并排序的原理,以及对一个实际问题进行编码。 学习的内容 1. 什么是归并排序 比如我们拿到一个数组,如果想使用归并排序,应该怎么做呢?首先我们将数组从中间切分,分成左...
    继续阅读 »

    上一篇文章介绍了什么是分治思想,今天就来看一下它其中一个继承人-- 归并排序,本章主要介绍归并排序的原理,以及对一个实际问题进行编码。


    学习的内容




    1. 什么是归并排序


    比如我们拿到一个数组,如果想使用归并排序,应该怎么做呢?首先我们将数组从中间切分,分成左右两个部分,然后对左半部分和右半部分进行排序,两边部分又可以继续拆分,直至子数组中只剩下一个数据位置。


    然后就要将拆分的子数组进行合并,合并的时候会涉及到两个数据进行比较,然后按照大小进行排序,以此往上进行合并。


    拆分过程


    image.png
    合并过程


    image.png


    从上面我们可以看出,我们最终将大的数组拆分成只有单个数据的数组,然后进行合并,在合并过程中比较两个长度为1的数组,进行排序合并成新的子数组,然后依次类推,直至全部排序完成,也就意味着原数组排序完成。


    2.代码示例


    public class Solution {
       public static void main(String[] args) {
           int[] arr = {1,4,3,2,11};
           sortArray(arr);
           System.out.println(arr);
      }

       public static int[] sortArray(int[] nums) {
           quickSort(nums, 0, nums.length - 1);
           return nums;
      }

       private static void quickSort(int[] nums, int left, int right) {
           if (left >= right) {
               return;
          }
           int partitionIndex = getPartitionIndex(nums, left, right);
           quickSort(nums, left, partitionIndex - 1);
           quickSort(nums, partitionIndex + 1, right);
      }

       private static int getPartitionIndex(int[] nums, int left, int right) {
           int pivot = left;
           int index = pivot + 1;
           for (int i = index; i <= right; i++) {
               if (nums[i] < nums[pivot]) {
                   swap(nums, i, index);
                   index++;
              }
          }
           swap(nums, pivot, index - 1);
           return index - 1;
      }

       private static void swap(int[] nums, int i, int j) {
           int temp = nums[i];
           nums[i] = nums[j];
           nums[j] = temp;
      }
    }




    总结


    本章简单分析了归并排序的原理以及分享了一个实际案例,无论是归并还是归并算法,对理解递归还是很有帮助的,之前总是靠着想递归流程,复杂点的绕着绕着就晕了,后面会再看一下快速排序,他和本文提到的归并排序都是分治思想,等说完快排,再

    作者:花哥编程
    来源:juejin.cn/post/7250404077712048165
    一起对比两者的区别。

    收起阅读 »

    uniapp开发项目——问题总结

    前言 之前使用过uniapp开发微信小程序,但是没有遇到需要兼容H5页面的。因此在使用uniapp开发微信小程序和H5的过程中,遇到了好些问题。 1. button按钮存在黑色边框 使用button标签,在手机上查看存在黑色的边框,设置了border: non...
    继续阅读 »

    前言


    之前使用过uniapp开发微信小程序,但是没有遇到需要兼容H5页面的。因此在使用uniapp开发微信小程序和H5的过程中,遇到了好些问题。


    1. button按钮存在黑色边框


    使用button标签,在手机上查看存在黑色的边框,设置了border: none;也没有效果。


    原因:uniapp的button按钮使用了伪元素实现边框


    解决方法: 设置button标签的伪元素为display:none或者boder:none;


    button:after{
    boder:none;
    }

    2. 配置反向代理,处理跨域


    微信小程序没有跨域问题,如果当前小程序还没有配置服务器域名出现无法请求接口,只需要在微信开发工具勾选不校验合法域名,就可以请求到了


    在本地开发环境中,H5页面在浏览器中调试,会出现跨域问题。如果后端不处理,前端就需要配置反向代理,处理跨域


    a. 在manifest.json的源码视图中,找到h5的配置位置,配置proxy代理


    image.png


    注: "pathRewrite"是必要的,告诉连接要使用代理


    b.在请求接口中使用


    // '/api'就是manifest.json文件配置的devServer中的proxy
    uni.request({
    url: '/api'+ '接口url',
    ...
    })

    c. 配置完,需要重启项目


    3. 使用uni.uploadFile()API,上传图片文件


    在微信小程序使用该API上传图片没问题,但是在H5页面实现图片上传,后台始终不能获取到上传的文件。


    一开始使用uni.chooseImage()API实现从本地相册选择图片或使用相机拍照,成功之后可以返回图片的本地文件路径列表(tempFilePaths)和图片的本地文件列表(tempFiles,每一项是一个 File 对象)


    tempFilePaths 在微信小程序中得到临时路径图片,而在浏览器中得到 blob 路径图片。微信小程序使用uni.uploadFile()上传该临时路径图片,可以成功上传,但是H5无法成功(浏览器中的传值方式会显示为payload,不是文件流file)


    image.png


    f994e37fce7a5d62763f1c015b9553f.png



    可能原因:



    1. 使用 uni.uploadFile() 上传 blob 文件给服务端,后端无法获取到后缀名,进而上传失败。


    b. uni.uploadFile()上传的文件格式不正确



    解决方法:


    在H5中上传tempFiles文件,而不是tempFilePaths,并更改uni.uploadFile()上传的格式


    H5


    image.png


    微信小程序


    image.png


    4. 打包H5


    问题:打包出来,部署到线上,页面空白,控制台preview中展示please enable javascript tocontinue


    原因:uniapp的打包配置存在问题


    解决方法:


    a. web配置不选择路由模式、运行的基础路径也不填写(一开始都写了)


    image.png


    b. "pathRewrite"设置为空(不知道为啥,可能是不需要配置代理了,网站和接口是同一域名)


    "proxy" : {
    "/api" : {
    "target" : "xxx",
    "changeOrigin" : true,
    "secure" : true,
    "pathRewrite" : {}
    }
    }



    注: 之前接口中的'/api'也需要取消


    作者:sherlockkid7
    来源:juejin.cn/post/7250284959221809209

    收起阅读 »

    某外包面试官:你还不会uniapp?😲😲

    uniapp主要文件夹 pages.json 配置文件,全局页面路径配置,应用的状态栏、导航条、标题、窗口背景色设置等 main.js 入口文件,主要作用是初始化vue实例、定义全局组件、使用需要的插件如 vuex,注意uniapp无法使用vue-router...
    继续阅读 »

    uniapp主要文件夹


    pages.json


    配置文件,全局页面路径配置,应用的状态栏、导航条、标题、窗口背景色设置等


    main.js


    入口文件,主要作用是初始化vue实例、定义全局组件、使用需要的插件如 vuex,注意uniapp无法使用vue-router,路由须在pages.json中进行配置。如果开发者坚持使用vue-router,可以在插件市场找到转换插件。


    App.vue


    是uni-app的主组件,所有页面都是在App.vue下进行切换的,是页面入口文件。但App.vue本身不是页面,这里不能编写视图元素。除此之外,应用生命周期仅可在App.vue中监听,在页面监听无效。


    pages


    页面管理部分用于存放页面或者组件


    manifest.json


    文件是应用的配置文件,用于指定应用的名称、图标、权限等。HBuilderX 创建的工程此文件在根目录,CLI 创建的工程此文件在 src 目录。


    package.json


    配置扩展,详情内容请见官网描述package.json概述


    uni-app属性的绑定


    vue和uni-app动态绑定一个变量的值为元素的某个属性的时候,会在属性前面加上冒号":";


    uni-app中的本地数据存储和接收


    // 存储:
    uni.setStorage({key:“属性名”,data:“值”}) //异步
    ni.setStorageSync(KEY,DATA) //同步
    //接收:
    ni.getStorage({key:“属性名”,success(res){res.data}}) //异步
    uni.getStorageSync(KEY) //同步
    //移除:
    uni.removeStorage(OBJECT) //从本地缓存中异步移除指定 key。
    uni.removeStorageSync(KEY) //从本地缓存中同步移除指定 key。
    //清除:
    uni.clearStorage() //清理本地数据缓存。
    ni.clearStorageSync() //同步清理本地数据缓存。

    页面调用接口



    • getApp() 函数 用于获取当前应用实例,一般用于获取globalData

    • getCurrentPages() 函数 用于获取当前页面栈的实例,以数组形式按栈的顺序给出,第一个元素为首页,最后一个元素为当前页面。

    • uni.emit(eventName,OBJECT) uni.emit(eventName,OBJECT)uni.emit(eventName,OBJECT) uni.on(eventName,callback) :触发和监听全局的自定义事件

    • uni.once(eventName,callback):监听全局的自定义事件。uni.once(eventName,callback):监听全局的自定义事件。

    • 事件可以由 uni.once(eventName,callback):监听全局的自定义事件。

    • 事件可以由uni.emit 触发,但是只触发一次,在第一次触发之后移除监听器。

    • uni.$off([eventName, callback]):移除全局自定义事件监听器。


    uni-app的生命周期


      beforeCreate(创建前)
    created(创建后)
    beforeMount(载入前,挂载)
    mounted(载入后)
    beforeUpdate(更新前)
    updated(更新后)
    beforeDestroy(销毁前)
    destroyed(销毁后)

    路由与页面跳转



    1. uni.navigateTo 不关闭当前页的情况下跳转其他页面

    2. uni.redirectTo 关闭当前页的情况下跳转其他页面

    3. uni.switchTab 跳转去tabBar,关闭其他非tabBar页面

    4. uni.reLaunch 关闭所有页面,跳转到其他页面

    5. uni.navigateBack 返回

    6. edxit 退出app


    跨端适配—条件编译


    1. #ifdef APP-PLUS
    需条件编译的代码 //app
    #endif
    2. #ifndef H5
    需条件编译的代码 //H5
    endif
    3. #ifdef H5 || MP-WEIXIN
    需条件编译的代码 //小程序
    #endif

    uniapp上传文件时使用的api


    uni.uploadFile({
    url: '要上传的地址',
    fileType:'image',
    filePath:'图片路径',
    name:'文件对应的key',
    success: function(res){
    console.log(res)
    },})

    uniapp选择文件、图片上传


    选择文件


    uni.chooseFile({
    count: 6, //默认100
    extension:['.zip','.doc'],
    success: function (res) {
    console.log(JSON.stringify(res.tempFilePaths));
    }
    });

    选择图片文件


    uni.chooseFile({
    count: 10,
    type: 'image',
    success (res) {
    // tempFilePath可以作为img标签的src属性显示图片
    const tempFilePaths = res.tempFiles
    }
    })

    uni-app的页面传参方式


    第一种:
    直接在跳转页面的URL路径后面拼接,如果是数组或者json格式记得转成字符串格式哦。然后再目的页面onload里面接受即可


    //现页面
    uni.navigateTo({
    url:'/pages/notice/notice?id=1'
    })
    //目的页面接收
    //这里用onshow()也可以
    onLoad(options) {
    var data = options.id;
    console.log(data)
    }

    第二种:
    直接在main.js注册全局变量



    • 例如我用的是vue框架,先在main.js文件注册变量myName

    • Vue.prototype.myName= '玛卡巴卡';

    • 在目标文件读取全局变量,注意全局变量不要与我们在当前页声明的变量名重复

    • let name = this.myName; // 玛卡巴卡


    第三种:设置本地存储也比较方便



    • 这里建议使用uni.setStorageSync这个是同步,不会出现去了目标页面取值取不到的问题

    • uni.setStorage是异步存值,获取值也是一样建议使用uni.getStorageSync


    uniapp实现下拉刷新


    实现下拉刷新需要用到uni.onPullDownRefresh和uni.stopPullDownRefresh这个两个函数,函数与生命周期同等级可以监听页面下拉动作


    uniapp实现上拉加载


    uniapp中的上拉加载是通过onReachBottom()这个生命周期函数实现,当下拉触底时就会触发。我们可以在此函数内调用分页接口请求数据,用以获取更多的数据


    scroll-view吸顶问题



    • 问题:
      scroll-view 是常会用到的一个标签,我们可以使用 position:sticky 加一个边界条件例如top:0
      属性实现一个粘性布局,在容器滚动的时候,如果我们的顶部标签栏触碰到了顶部就不会再滚动了,而是固定在顶部。但是在小程序中如果你在scroll-view元素中直接为子元素使用sticky属性,你给予sticky的元素在到达父元素的底部时会失效。

    • 解决:
      在scroll-view元素中,再增加一层view元素,然后在再将使用了sticky属性的子元素放入view中,就可以实现粘贴在某个位置的效果了


    ios输入框字体移动bug



    • 问题:在IOS端有时,当输入框在输入后没有点击其他位置使输入框失焦的话,如果滚动窗口内部的字体也会跟着滚动

    • 解决:



    1. 尝试了下,发现textarea不会和input一样出现字体随着页面滚动的情况,这是一个兼容方案

    2. 还有个不优雅的方案是输入完成后使用其他事件让其失焦或者disable,例如弹窗或者弹出层出来的时候可以暂时让input禁止,然后弹窗交互完成后再放开


    rpx、px、em、rem、%、vh、vw的区别是什么?



    • rpx 相当于把屏幕宽度分为750份,1份就是1rpx

    • px 绝对单位,页面按精确像素展示

    • em 相对单位,相对于它的父节点字体进行计算

    • rem 相对单位,相对根节点html的字体大小来计算

    • % 一般来说就是相对于父元素

    • vh 视窗高度,1vh等于视窗高度的1%

    • vw 视窗宽度,1vw等于视窗宽度的1%


    uni-app的优缺点



    • 优点:



    1. 一套代码可以生成多端

    2. 学习成本低,语法是vue的,组件是小程序的

    3. 拓展能力强

    4. 使用HBuilderX开发,支持vue语法

    5. 突破了系统对H5条用原生能力的限制



    • 缺点:



    1. 问世时间短,很多地方不完善

    2. 社区不大

    3. 官方对问题的反馈不及时

    4. 在Android平台上比微信小程序和iOS差

    5. 文件命
      作者:margin_100px
      来源:juejin.cn/post/7245936314851622970
      名受限

    收起阅读 »

    uniapp 手机号码一键登录保姆级教程

    背景 通过uniapp来开发App,目前内部上架的App产品现有的登录方式有「账号/密码」 和 「手机号/验证码」两种登录方式;但这两种方式还是不够便捷,目前「手机号一键登录」是替代短信验证登录的下一代登录验证方式,能消除现有短信验证模式等待时间长、操作繁琐和...
    继续阅读 »

    背景


    通过uniapp来开发App,目前内部上架的App产品现有的登录方式有「账号/密码」 和 「手机号/验证码」两种登录方式;但这两种方式还是不够便捷,目前「手机号一键登录」是替代短信验证登录的下一代登录验证方式,能消除现有短信验证模式等待时间长、操作繁琐和容易泄露的痛点。


    因此,结合市面上的主流App应用,以及业务方的需求,我们的App产品也需要增加「手机号一键登录」功能。 DCloud联合个推公司整合了三大运营商网关认证的服务,通过运营商的底层SDK,实现App端无需短信验证码直接获取手机号。


    uni官方提供了对接的方案文档,可自行查阅,也可继续阅读本文


    准备工作


    1 目前支持的版本及运营商



    • 支持版本:HBuilderX 3.0+

    • 支持项目类型:uni-app的App端,5+ App,Wap2App

    • 支持系统平台: Android,iOS

    • 支持运营商: 中国移动,中国联通,中国电信


    2 费用


    2.1 运营商费用

    目前一键登录收费规则为每次登录成功请求0.02元,登录失败则不计费。


    2.2 云空间费用

    开通uniCloud是免费的,其中阿里云是全免费,腾讯云是提供一个免费服务空间。


    阿里云

    选择阿里云作为服务商时,服务空间资源完全免费,每个账号最多允许创建50个服务空间。阿里云目前处于公测阶段,如有正式业务对稳定性有较高要求建议使用腾讯云。


    image.png


    阿里云的服务空间是纯免费的。但为避免资源滥用,有一些限制,见下:


    image.png



    除上面的描述外,阿里云没有其他限制。
    因为阿里云免费向DCloud提供了硬件资源,所以DCloud也没有向开发者收费。如果阿里云后续明确了收费计划,DCloud也会第一时间公布。



    腾讯云

    选择腾讯云作为服务商时,可以创建一个免费的服务空间,资源详情参考腾讯云免费额度;如想提升免费空间资源配额,或创建更多服务空间,则需付费购买。


    image.png


    2.3 云函数费用

    如果你的一键登录业务平均每天获取手机号次数为10000次,使用阿里云正式版云服务空间后,对应云函数每天大概消耗0.139元


    接入


    1 重要前置条件



    • 手机安装有sim卡

    • 手机开启数据流量(与wifi无关,不要求关闭wifi,但数据流量不能禁用。)

    • 开通uniCloud服务(但不要求所有后台代码都使用uniCloud)

    • 开发者需要登录 DCloud开发者中心,申请开通一键登录服务。


    2 开发者中心-开通一键登录服务


    此官方文档详细步骤开通一键登录服务,开通后将当前项目加入一键登录内,审核2-3天;


    3 开通uniCloud


    一键登录在客户端获取 access_token 后,必须通过调用uniCloud中云函数换取手机号码,
    所以需要开通uniCould;


    登录uniCloud中web控制台里,新建服务空间,开通uniCloud


    在uniCloud的云函数中拿到手机号后,可以直接使用,也可以再转给传统服务器处理,也可以通过云函数url化方式生成普通的http接口给5+ App使用。


    4 客户端-一键登录


    当前项目关联云空间

    项目名称点击右键,创建云环境,创建的云环境应与之前开通的云空间类型保持一致,我这里选择腾讯云;


    image.png


    创建好后当前项目下会多个文件夹「uniCloud」,点击右键关联创建好的云空间


    image.png


    image.png


    关联成功


    image.png


    获取可用的服务提供商(暂时作用不大)

    一键登录对应的 provider ID为 'univerify',当获取provider列表时发现包含 'univerify' ,则说明当前环境打包了一键登录的sdk;


    uni.getProvider({
    service: 'oauth',
    success: function (res) {
    console.log(res.provider)// ['qq', 'univerify']
    }
    });

    参考文档


    预登录(可选)

    预登录操作可以判断当前设备环境是否支持一键登录,如果能支持一键登录,此时可以显示一键登录选项;


    uni.preLogin({
    provider: 'univerify',
    success(){ //预登录成功
    // 显示一键登录选项
    },
    fail(res){ // 预登录失败
    // 不显示一键登录选项(或置灰)
    // 根据错误信息判断失败原因,如有需要可将错误提交给统计服务器
    console.log(res.errCode)
    console.log(res.errMsg)
    }
    })

    参考文档


    请求登录授权

    弹出用户授权界面。根据用户操作及授权结果返回对应的回调,拿到 access_token,此时客户端登录认证完成;设置自定义按钮等;后续「需要将此数据提交到服务器获取手机号码」


    uni.login({
    provider: 'univerify',
    univerifyStyle: { // 自定义登录框样式
    //参考`univerifyStyle 数据结构`
    },
    success(res){ // 登录成功 在该回调中请求后端接口,将access_token传给后端
    console.log(res.authResult); // {openid:'登录授权唯一标识',access_token:'接口返回的 token'}
    },
    fail(res){ // 登录失败
    console.log(res.errCode)
    console.log(res.errMsg)
    }
    })

    参考文档


    获取用户是否选中了勾选框

    新增判断是否勾选一键登录相关协议函数;


    uni.getCheckBoxState({
    success(res){
    console.log(res.state) // Boolean 用户是否勾选了选框
    console.log(res.errMsg)
    },
    fail(res){
    console.log(res.errCode)
    console.log(res.errMsg)
    }
    })

    参考文档


    用access_token换手机号

    客户端获取到 access_token 后,传递给uniCloud云函数,云函数中通过uniCloud.getPhoneNumber方法获取真正的手机号。


    换取手机号有三种方式:




    1. 在前端直接写 uniCloud.callFunction ,将 access_token 传给指定的云函数。但需要在「云函数内部」请求服务端接口并将电话号码传到服务器;




    2. 使用普通ajax请求提交 access_token 给uniCloud的云函数(不考虑);




    3. 使用普通ajax请求提交 access_token 给自己的传统服务器,通过自己的传统服务器再转发给 uniCloud 云函数。但uniCloud上的「云函数需要做URL化」;




    我们目前使用的是第三种,防止电话号码暴露到前端,通过java小伙伴去请求uniCloud云函数,返回电话号码给后端;


    // 云函数验证签名,此示例中以接受GET请求为例作演示
    const crypto = require('crypto')
    exports.main = async(event) => {

    const secret = 'your-secret-string' // 自己的密钥不要直接使用示例值,且注意不要泄露
    const hmac = crypto.createHmac('sha256', secret);

    let params = event.queryStringParameters
    const sign = params.sign
    delete params.sign
    const signStr = Object.keys(params).sort().map(key => {
    return `${key}=${params[key]}`
    }).join('&')

    hmac.update(signStr);

    if(sign!==hmac.digest('hex')){
    throw new Error('非法访问')
    }

    const {
    access_token,
    openid
    } = params
    const res = await uniCloud.getPhoneNumber({
    provider: 'univerify',
    appid: 'xxx', // DCloud appid,不同于callFunction方式调用,使用云函数Url化需要传递DCloud appid参数
    apiKey: 'xxx', // 在开发者中心开通服务并获取apiKey
    apiSecret: 'xxx', // 在开发者中心开通服务并获取apiSecret
    access_token: access_token,
    openid: openid
    })
    // 返回手机号给自己服务器
    return res
    }

    res结果


    {
    "data": {
    "code": 0,
    "success": true,
    "phoneNumber": "166xxxx6666"
    },
    "statusCode": 200,
    "header": {
    "Content-Type": "application/json; charset=utf-8",
    "Connection": "keep-alive",
    "Content-Length": "53",
    "Date": "Fri, 06 Nov 2020 08:57:21 GMT",
    "X-CloudBase-Request-Id": "xxxxxxxxxxx",
    "ETag": "xxxxxx"
    },
    "errMsg": "request:ok"
    }

    参考文档


    客户端关闭一键登录授权界面

    请求登录认证操作完成后,不管成功或失败都不会关闭一键登录界面,需要主动调用closeAuthView方法关闭。完成业务服务登录逻辑后通知客户端关闭登录界面。


    uni.closeAuthView()

    参考文档


    错误码

    一键登录相关的错误码


    但其中状态码30006,官方未给出相关的说明,但与相关技术沟通得知,该状态码是运营商返回的,大概率是网络信号不好,或者其它等原因造成的,没办法修复,只能是想办法兼容改错误;


    目前我们的兼容处理方案是:程序检测判断如果出现该状态码,则关闭一键登录授权页面,并跳转到原有的「手机号验证码」登录页面


    参考文档


    5 云函数-一键登录


    自HBuilderX 3.4.0起云函数需启用uni-cloud-verify之后才可以调用getPhoneNumber接口,扩展库uni-cloud-verify


    需要在云函数的package.json内添加uni-cloud-verify的引用即可为云函数启用此扩展,无需做其他调整,因为HbuilderX内部已经集成了该扩展库,只需引入即可,不用安装,代码如下:


    {
    "name": "univerify",
    "extensions": {
    "uni-cloud-verify": {} // 启用一键登录扩展,值为空对象即可
    }
    }

    参考文档


    6 运行基座和打包


    使用uni一键登录,不需要制作自定义基座,使用HBuilder标准真机运行基座即可。在云函数中配置好apiKey、apiSecret后,只要一键登录成功,就会从你的账户充值中扣费。


    在菜单中配置模块权限


    image.png


    参考文档


    需要注意的问题


    1. 开通手机号一键登录是否同时需要开通苹果登录?


    目前只开通手机号一键登录,未开通苹果登录,在我们项目里是可以的,但是App云打包时是会弹框提示的,但是并不影响项目在App Store中发布;


    2. 如果同一个token多次反复获取手机号会重复扣费么?


    不会,这种场景应该仅限于联调测试使用,正式上线每次都应该获取最新token,避免过期报错;


    3. access_token过期时间



    • token过期时间是10分钟

    • 每次请求获取手机号接口时,都应该从客户端获取最新的token

    • 在取号成功时进行扣费,获取token不计费


    4. 预登录有效期


    预登录有效期为10分钟,超过10分钟后预登录失效,此时调用login授权登录相当于之前没有调用过预登录,大概需要等待1-2秒才能弹出授权界面。 预登录只能使用一次,调用login弹出授权界面后,如果用户操作取消登录授权,再次使用一键登录时需要重新调用预登录。


    作者:Wendy的小帕克
    来源:juejin.cn/post/7221422131857506359
    收起阅读 »

    Compose跨平台又来了,这次能开发iOS了

    /   今日科技快讯   /近日,有消息称百度3月将推出ChatGPT风格服务。经百度确认,该项目名字确定为文心一言,英文名ERNIE Bot,三月份完成内测,面向公众开放。目前,文心一言在做上线前的冲刺。百度方面表示,...
    继续阅读 »
    /   今日科技快讯   /

    近日,有消息称百度3月将推出ChatGPT风格服务。经百度确认,该项目名字确定为文心一言,英文名ERNIE Bot,三月份完成内测,面向公众开放。

    目前,文心一言在做上线前的冲刺。百度方面表示,ChatGPT相关技术,百度都有。百度在人工智能四层架构中,有全栈布局。包括底层的芯片、深度学习框架、大模型以及最上层的搜索等应用。文心一言,位于模型层。

    /   作者简介   /

    本篇文章转自黄林晴的博客,文章主要分享了如何使用Compose来进行IOS开发,相信会对大家有所帮助!

    原文地址:
    https://juejin.cn/post/7195770699524751421

    /   前言   /

    在之前,我们已经体验了Compose for Desktop与Compose for Web,目前Compose for IOS已经有尚未开放的实验性API,乐观估计今年年底将会发布 Compose for IOS。同时Kotlin也表示将在2023年发布KMM的稳定版本。



    届时Compose-jb + KMM将实现Kotlin全平台。



    /   搭建项目   /

    创建项目

    因为目前Compose for iOS阶段还在试验阶段,所以我们无法使用Android Studio或者IDEA直接创建Compose支持IOS的项目,这里我们采用之前的方法,先使用Android Studio创建一个KMM项目,如果你不知道如何创建一个KMM项目,可以参照之前的这篇文章KMM的初次尝试~,项目目录结构如下所示。



    创建好KMM项目后我们需要添加Compose跨平台的相关配置。

    添加配置

    首先在settings.gradle文件中声明compose插件,代码如下所示:

    pluginManagement {
        repositories {
            google()
            gradlePluginPortal()
            mavenCentral()
            maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
        }

        plugins {
            val composeVersion = extra["compose.version"as String
            id("org.jetbrains.compose").version(composeVersion)
        }
    }

    这里compose.version的版本号是声明在gradle.properties中的,代码如下所示:

    compose.version=1.3.0

    然后我们在shared模块中的build文件中引用插件:

    plugins {
        kotlin("multiplatform")
        kotlin("native.cocoapods")
        id("com.android.library")
        id("org.jetbrains.compose")
    }

    并为commonMain添加compose依赖,代码如下所示:

    val commonMain by getting {
        dependencies {
            implementation(compose.ui)
            implementation(compose.foundation)
            implementation(compose.material)
            implementation(compose.runtime)
        }
    }

    sync之后,你会发现一个错误警告:uikit还处于试验阶段并且有许多bug....



    uikit就是compose-jb暴露的UIKit对象。为了能够使用,我们需要在gradle.properties文件中添加如下配置:

    org.jetbrains.compose.experimental.uikit.enabled=true

    添加好配置之后,我们先来运行下iOS项目,确保添加的配置是无误的。果然,不运行不知道,一运行吓一跳。



    这个问题困扰了我两三天,实在是无从下手,毕竟现在相关的资料很少,经过N次的搜索,最终解决的方案很简单:Kotlin版本升级至1.8.0就可以了。

    kotlin("android").version("1.8.0").apply(false)

    再次运行项目,结果如下图所示。



    不过这是KMM的iOS项目,接下来我们看如何使用Compose编写iOS页面。

    /   开始iOS之旅   /

    我们替换掉iOSApp.swift中的原有代码,替换后的代码如下所示:

    import UIKit
    import shared

    @UIApplicationMain
    class AppDelegateUIResponderUIApplicationDelegate {
        var window: UIWindow?

        func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
            window = UIWindow(frame: UIScreen.main.bounds)
            let mainViewController = Main_iosKt.MainViewController()
            window?.rootViewController = mainViewController
            window?.makeKeyAndVisible()
            return true
        }
    }

    上面的代码看不懂没关系,我们只来看获取mainViewController的这一行:

    let mainViewController = Main_iosKt.MainViewController()

    Main_iosKt.MainViewController是通过新建在shared模块iOSMain目录下的main.ios.kt文件获取的,代码如下所示:

    fun MainViewController(): UIViewController = Application("Login") { //调用一个Compose方法 }

    接下来所有的事情就都可以交给Compose了。

    图片实现一个登录页面

    因为页面这部分是公用的,所以我们在shared模块下的commonMain文件夹下新建Login.kt文件,编写一个简单的登录页面,代码如下所示:

    @Composable
    internal fun login() {
        var userName by remember {
            mutableStateOf("")
        }
        var password by remember {
            mutableStateOf("")
        }
        Surface(modifier = Modifier.padding(30.dp)) {
            Column {
                TextField(userName, onValueChange = {
                    userName = it
                }, placeholder = { Text("请输入用户名") })
                TextField(password, onValueChange = {
                    password = it
                }, placeholder = { Text("请输入密码") })
                Button(onClick = {
                    //登录
                }) {
                    Text("登录")
                }
            }
        }
    }

    上述代码声明了一个用户名输入框、密码输入框和一个登录按钮,就是简单的Compose代码。然后需要在main.ios.kt中调用这个login方法:

    fun MainViewController(): UIViewController =
        Application("Login") {
            login()
        }

    运行iOS程序,效果如下图所示:



    嗯~,Compose 在iOS上UI几乎可以做到100%复用,还有不学习Compose的理由吗?

    实现一个双端网络请求功能

    在之前的第1弹和第2弹中,我们分别实现了在Desktop、和Web端的网络请求功能,现在我们对之前的功能在iOS上再次实现。

    添加网络请求配置

    首先在shared模块下的build文件中添加网络请求相关的配置,这里网络请求我们使用Ktor,具体的可参照之前的文章:KMM的初次尝试~

    配置代码如下所示:

    val commonMain by getting {
        dependencies {
            ...
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
            implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
            implementation("io.ktor:ktor-client-core:$ktorVersion")
            implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
            implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
        }
    }
    val iosMain by getting {
        dependencies {
            implementation("io.ktor:ktor-client-darwin:$ktorVersion")
        }
    }

    val androidMain by getting {
        dependencies {
            implementation("io.ktor:ktor-client-android:$ktorVersion")
        }
    }

    添加接口

    这里我们仍然使用wandroid中的每日一问接口。DemoReqData与之前系列的实体类是一样的,这里就不重复展示了。接口地址如下:
    https://wanandroid.com/wenda/list/1/json

    创建接口地址类,代码如下所示:

    object Api {
        val dataApi = "https://wanandroid.com/wenda/list/1/json"
    }

    创建HttpUtil类,用于创建HttpClient对象和获取数据的方法,代码如下所示。

    class HttpUtil {
        private val httpClient = HttpClient {
            install(ContentNegotiation) {
                json(Json {
                    prettyPrint = true
                    isLenient = true
                    ignoreUnknownKeys = true
                })
            }
        }

        /**
         * 获取数据
         */

        suspend fun getData(): DemoReqData {
            val rockets: DemoReqData =
                httpClient.get(Api.dataApi).body()
            return rockets
        }
    }

    这里的代码我们应该都是比较熟悉的,仅仅是换了一个网络请求框架而已。现在公共的业务逻辑已经处理好了,只需要页面端调用方法然后解析数据并展示即可。

    编写UI层

    由于Android、iOS、Desktop三端的UI都是完全复用的,所以我们将之前实现的UI搬过来即可。代码如下所示:

    Column() {
        val scope = rememberCoroutineScope()
        var demoReqData by remember { mutableStateOf(DemoReqData()) }
        Button(onClick = {
            scope.launch {
                try {
                    demoReqData = HttpUtil().getData()
                } catch (e: Exception) {
                }
            }
        }) {
            Text(text = "请求数据")
        }

        LazyColumn {
            repeat(demoReqData.data?.datas?.size ?: 0) {
                item {
                    Message(demoReqData.data?.datas?.get(it))
                }
            }
        }
    }

    获取数据后,通过Message方法将数据展示出来。这里只将作者与标题内容显示出来,代码如下所示:

    @Composable
    fun Message(dataDemoReqData.DataBean.DatasBean?) {
        Card(
            modifier = Modifier
                .background(Color.White)
                .padding(10.dp)
                .fillMaxWidth(), elevation = 10.dp
        ) {
            Column(modifier = Modifier.padding(10.dp)) {
                Text(
                    text = "作者:${data?.author}"
                )
                Text(text = "${data?.title}")
            }
        }
    }

    分别运行iOS、Android程序,点击请求数据按钮,结果如下图:



    这样我们就用一套代码,实现了在双端的网络请求功能。

    /   一个尴尬的问题   /

    我一直认为存在一个比较尴尬的问题,那就是像上面实现一个完整的双端网络请求功能需要用到KMM + Compose-jb,但是KMM与Compose-jb并不是一个东西,但是用的时候呢基本上都是一起用。Compose-jb很久之前已经发了稳定版本只是Compose-iOS目前还没有开放出来,而KMM当前还处于试验阶段,不过在2023年Kotlin的RoadMap中,Kotlin已经表示将会在23年中发布第一个稳定版本的KMM。而Compose for iOS何时发布,我想也是指日可待的事情。

    所以,这个系列我觉得改名为:Kotlin跨平台系列更适合一些,要不然以后就会存在KMM跨平台第n弹,Compse跨平台第n弹....

    因此,从第四弹开始,此系列将更名为:Kotin跨平台第N弹:~

    /   写在最后   /

    从自身体验来讲,我觉得KMM+Compose-jb对Android开发者来说是非常友好的,不需要像Flutter那样还需要额外学习Dart语言。所以,你觉得距离Kotlin一统“江山”的日子还会远吗?

    该文章转载自:https://mp.weixin.qq.com/s/LfD6AD-gDFdEYQS1X96CGw
    收起阅读 »

    环信 flutter sdk集成IM离线推送及点击推送获取推送信息(iOS版)

    前提条件1.macOS系统,安装了xcode和flutter集成环境2.有苹果开发者账号3.有环信开发者账号(注册地址:https://console.easemob.com/user/register)4.参考这篇文章https://www.imgeek.o...
    继续阅读 »

    前提条件

    1.macOS系统,安装了xcode和flutter集成环境

    2.有苹果开发者账号

    3.有环信开发者账号

    (注册地址:https://console.easemob.com/user/register)


    4.参考这篇文章https://www.imgeek.org/article/825360043,完成推送证书的创建和上传

    集成IM离线推送


    1.创建一个新的项目

    2.导入flutterSDK

    3.初始化环信sdk

    void initSDK() async {

      var options = EMOptions(

        appKey: “你的appkey”,

      );

      options.enableAPNs("EaseIM_APNS_Developer");

      await EMClient.getInstance.init(options);

      debugPrint("has init");

    }

    EaseIM_APNS_Developer是你在环信后台创建的证书名,需要注意,iOS需要上传开发证书和生产证书

    4.可以在 _incrementCounter 这个按钮点击事件中调用一下登录操作,到此flutter层的工作已经完成

    5.打开原生项目,修改包名,添加推送功能

    6.打开AppDelegate 文件 导入im_flutter_sdk,并且在didRegisterForRemoteNotificationsWithDeviceToken方面里面调用环信的registerForRemoteNotifications方法,进行token的绑定

    注:IM离线推送机制:

    1.环信这边需要针对设备deviceToken和环信的username进行绑定,

    2.IMserver 收到消息,会检测接收方是否在线,如果在线直接投递消息,如果不在线,则根据username 取设备的deviceToken

    3.根据设备的deviceToken 和 上传的证书给设备推送消息

    4.当app第一次运行的时候,就会走didRegisterForRemoteNotificationsWithDeviceToken方法,这个时候绑定token信息会报错,这个时候是正常的,因为你并没有登录,此时SDK内部会保存deviceToken,当你调用登录接口成功之后,SDK内部会进行一次绑定token的操作,

    到此,推送功能已经集成完毕,注意测试时建议先把项目杀死,保证该用户已经离线


    点击推送获取推送信息

    第一种方法 自己做桥接,实现原生层与flutter层做交互

    第二种方法 可以利用先有api 实现原生层给flutter层传递消息

    今天主要介绍第二种方法

    1.打开原生层 在didFinishLaunchingWithOptions和didReceiveRemoteNotification 方法里调用EMClientWrapper.shared().sendData(toFlutter: userInfo) 方法,把需要传递的数据传到flutter层

    didFinishLaunchingWithOptions 是在app没有打开的情况下点击推送,从launchOptions里面拿到推送信息

    didReceiveRemoteNotification是在 app已经打开的情况下点击推送,从userInfo里面拿到推送信息

    注意:EMClientWrapper.shared().sendData 这个方法填的参数必须是一个字典

    如下图所示

    2.打开flutter层 调用EMClient.getInstance.customEventHandler方法 需要赋值一个函数,这个函数就是接受来自原生层传递过来的消息

    3.此时 点击推送消息 在flutter层就能获取到信息,如图我测试的结果

    完毕




    收起阅读 »

    微信开放小程序运行SDK,我们的App可以跑小程序了

    前言这几天看到微信团队推出了一个名为 Donut 的小程序原生语法开发移动应用框架,通俗的讲就是将微信小程序的能力开放给其他的企业,第三方的 App 也能像微信一样运行小程序了。其实不止微信,面对潜力越来越大的 B 端市场,阿里早期就开放了这样产品——mPaa...
    继续阅读 »

    前言

    这几天看到微信团队推出了一个名为 Donut 的小程序原生语法开发移动应用框架,通俗的讲就是将微信小程序的能力开放给其他的企业,第三方的 App 也能像微信一样运行小程序了。



    其实不止微信,面对潜力越来越大的 B 端市场,阿里早期就开放了这样产品——mPaas,只不过阿里没有做太多的宣传推广,再加上并没有兼容市面中占比和使用范围最大的微信小程序,所以一直处于不温不火的状态。

    今天就主要对比分析下目前市面上这类产品的技术特点及优劣。

    有这些产品

    目前这类产品有一个统一的技术名称:小程序容器技术

    小程序容器顾名思义,是一个承载小程序的运行环境,可主动干预并进行功能扩展,达到丰富能力、优化性能、提升体验的目的。

    目前我已知的技术产品包括:mPaas、FinClip、uniSDK 以及上周微信团队才推出的 Donut。下面我们就一一初略讲下各自的特点。

    他们的特点

    1、mPaas

    mPaaS是源于支付宝 App 的移动开发平台,为移动开发、测试、运营及运维提供云到端的一站式解决方案,能有效降低技术门槛、减少研发成本、提升开发效率,协助企业快速搭建稳定高质量的移动 App。

    mPaaS 提供了包括 App 开发、H5 开发、小程序开发的能力,只要按照其文档可以开发 App,而且可以在其开发的 App 上跑 H5、也可跑基于支付宝小程序标准开发的的小程序。


    由于行业巨头之间互不对眼,目前 mPaas 仅支持阿里生态的小程序,不能直接兼容例如微信、百度、字节等其他生态平台的小程序。

    2、FinClip

    FinClip是一款小程序容器,不论是移动 App,还是电脑、电视、车载主机等设备,在集成 FinClip SDK 之后,都能快速获得运行小程序的能力。

    提供小程序 SDK 和小程序管理后台,开发者可以将已有的小程序迁移部署在自有 App 中,从而获得足够灵活的小程序开发与管理体验。

    FinClip 兼容微信小程序语法,提供全套的的小程序开发管理套件,开发者不需要学习新的语法和框架,使用 FinClip IDE、小程序管理后台、小程序开发文档、FinClip App就能低成本高质量地完成从开发测试,到预览部署的全部工作。


    3、Donut

    Donut多端框架是支持使用小程序原生语法开发移动应用的框架,开发者可以一次编码,分别编译为小程序和 Android 以及 iOS 应用,实现多端开发。

    基于该框架,开发者可以将小程序构建成可独立运行的移动应用,也可以将小程序构建成运行于原生应用中的业务模块。该框架还支持条件编译,开发者可灵活按需构建多端应用模块,可更好地满足企业在不同业务场景下搭建移动应用的需求。


    4、uniSDK

    Uni-app小程序 SDK,是为原生 App 打造的可运行基于 uni-app 开发的小程序前端项目的框架,从而帮助原生 App 快速获取小程序的能力。uni 小程序 SDK 是原生SDK,提供 Android 版本 和 iOS 版本,需要在原生工程中集成,然后即可运行用uni-app框架开发的小程序前端项目。

    Unisdk是 uni-app 小程序生态中的一部分,开发者 App 集成了该 SDK 之后,就可以在自有 App 上面跑起来利用 uni-app 开发的小程序。

    优劣势对比

    1、各自的优势

    mPaas

    • 大而全,App开发、H5开发、小程序开发一应俱全;

    • 技术产品来源于支付宝,背靠蚂蚁金服有大厂背书;

    • 兼容阿里系的小程序,例如支付宝、钉钉、高德、淘宝等;

    • 拥有小程序管理端、云端服务。

    FinClip

    • 小而巧,只专注小程序集成,集成SDK后体积增加3M左右,提供小程序全生命周期的管理 ;

    • 提供小程序转 App 服务,能够一定程度解决 App 开发难的问题;

    • 几个产品中唯一支持企业私有化部署的,可进行定制化开发,满足定制化需求;

    • 兼容微信小程序,之前开发者已拥有的微信小程序,可无缝迁移至 FinClip;

    • 多端支持:iOS、Android、Windows、macOS、Linux,国产信创、车载操作系统。

    Donut

    • 微信的亲儿子,对微信小程序兼容度有其他厂商无可比拟的优势(但也不是100%兼容微信小程序);

    • 提供小程序转 App 服务,能够一定程度解决 App 开发难的问题;

    • 体验分析支持自动接入功能,无需修改代码即可对应用中的所有元素进行埋点;

    • 提供丰富的登录方法:微信登录、苹果登录、验证码登录等。

    uniSDK

    • 开源社区,众人拾柴火焰高;

    • uniapp 开发小程序可迁移至微信、支付宝、百度等平台之上,如果采用 uni 小程序 SDK,之后采用 uni-app 开发小程序,那么就可以实现一次开发,多端上架;

    • 免费不要钱。

    2、各自的不足

    mPaas

    • 小程序管理略简单,没有小程序全生命周期的管理;

    • App 集成其 SDK 之后,体积会扩大 30M 左右;

    • 不兼容微信小程序,之前微信开发的小程序,需要用支付宝小程序的标准进行重写才可迁移到 mPaaS 上;

    • 目前只支持 iOS 与 Android 集成,不支持其他端。

    FinClip

    • 没有对应的移动应用开发平台,只专注于做小程序;

    • 生态能力相较于其他三者相对偏弱,但兼容微信语法可一定程度补齐;

    • 暂不支持 Serveless 服务;

    • 产品快速迭代,既有惊喜,也有未知。

    Donut

    • 对小程序的数量、并发数、宽带上限等有比较严格的规定;

    • 目前仅处于 beta 阶段,使用过程有一定 bug 感;

    • 集成后体积增加明显,核心 SDK 500 MB,地图 300 MB;

    • 没有小程序全生命周期的管理;

    • 目前仅支持 iOS 与 Android 集成,不支持其他端。

    uniSDK

    • 开源社区,质量由开源者背书,在集成、开发过程当中出现问题,bug解决周期长;

    • uni 小程序 SDK 仅支持使用 uni-app 开发的小程序,不支持纯 wxml 微信小程序运行;

    • 目前 uni 小程序 SDK 仅支持在原生 App 中集成使用,暂不支持 HBuilderX 打包生成的 App 中集成;

    • 目前只支持 iOS 与 Android 集成,不支持其他端。

    以上就是关于几个小程序容器的测评分析结果,可以看出并没有完美的选择,每个产品都有自己的一些优势和不足,选择适合自己的就是最好的。希望能给需要的同学一定的参考,如果你有更好的选择欢迎交流讨论。

    作者:Finbird
    来源:juejin.cn/post/7181301359554068541

    收起阅读 »

    如何使用 uni-app 30分钟快速开发即时通讯应用|开发者活动

    “一套代码,多端运行”是很多开发团队的梦想,基于 uni-app 跨平台框架支持 iOS、Android、Web以及各种小程序并支持平台间互通,快速实现搭建多端即时通讯功能,降低开发难度,提升开发效率。12月13日 晚 19:00,环信线上公开课《使用 uni...
    继续阅读 »


    “一套代码,多端运行”是很多开发团队的梦想,基于 uni-app 跨平台框架支持 iOS、Android、Web以及各种小程序并支持平台间互通,快速实现搭建多端即时通讯功能,降低开发难度,提升开发效率。
    12月13日 晚 19:00,环信线上公开课《使用 uniapp 30分钟快速开发即时通讯应用》为题,讲解多端 uni-app 基础框架知识及搭建即时通讯功能项目实战技巧,掌握开发步骤及思路,大大增强代码复用率,提升效率。来直播间 get 环信 IM 的正确打开方式!

    一、时间地点

    活动时间:12 月 13 日(星期二)19:00-20:00
    活动地点:线上直播

    二、演讲大纲

    • uni-app 跨平台框架介绍
    • 使用uni-app 生成 Android&iOS 应用
    • 如何搭建自己的即时通讯应用
    • IM实战篇-uni-app 经典问题答疑

    三、活动报名

    报名链接:https://mudu.tv/live/watch/meddae1l





    收起阅读 »

    哈啰 Quark Design 正式开源,下一代跨技术栈前端组件库

    官网:quark-design.hellobike.comQuark(夸克) Design 是由哈啰平台 UED 和增长&电商前端团队联合打造的一套面向移动端的跨框架 UI 组件库。与业界第三方组件库不一样,Quark Design 底层基于 Web ...
    继续阅读 »

    Quark Design 是什么?

    官网:quark-design.hellobike.com

    github:github.com/hellof2e/qu…

    Quark(夸克) Design 是由哈啰平台 UED 和增长&电商前端团队联合打造的一套面向移动端的跨框架 UI 组件库。与业界第三方组件库不一样,Quark Design 底层基于 Web Components 实现,它能做到一套代码,同时运行在各类前端框架中。

    Quark Design 历经一年多的开发时间,已在集团内部大量业务中得到验证,本着“共创、共建、共享”的开源精神,我们于即日起将 Quark 正式对外开源!Github地址:github.com/hellof2e/qu… (求star、求关注~😁)


    注:文档表现/样式参考了HeadlessUI/nutui/vant等。

    Quark Design 与现有主流组件库的区别是什么?

    Quark(夸克)有别于业界主流的移动端组件库,Quark 能同时运行在业界所有前端框架/无框架工程中,做到真正的技术栈无关 !我们不一样,:)

    • 不依赖技术栈(eg. Vue、React、Angular等)

    • 不依赖技术栈版本(eg. Vue2.x、Vue3.x)

    • 全新的Api设计(eg. 弹窗的打开属性由传统的 Visible 调整为符合浏览器原生弹窗的 open等)

    • 公司前端技术生态项目技术栈多时,保持视觉/交互统一

    • 完全覆盖您所需要的各类通用组件

    • 支持按需引用

    • 详尽的文档和示例

    • 支持定制主题

    性能优势-优先逻辑无阻塞

    我们以对 React 组件的 Web Components 化为例,一个普通的 React 组件在初次执行时需要一次性走完所有必须的节点逻辑,而这些逻辑的执行都同步占用在 js 的主线程上,那么当你的页面足够复杂时,一些非核心逻辑就将会阻塞后面的核心逻辑的执行。

    比如首次加载时,你的页面中有一个复杂的交互组件,交互组件中又包含 N多逻辑和按钮等小组件,此时页面的首次加载不应该优先去执行这些细节逻辑,而首要任务应当是优先渲染出整体框架或核心要素,而后再次去完善那些不必要第一时间完成的细节功能。 例如一些图像处理非常复杂,但你完全没必要在第一时间就去加载它们。

    当我们使用 Web Components 来优化 React的时候,这个执行过程将会变得简洁的多,比如我们注册了一个复杂的逻辑组件,在 React 执行时只是执行了一个 createElement 语句,创建它只需要 1-2 微秒即可完成,而真正的逻辑并不在同时执行,而是等到“核心任务”执行完再去执行,甚至你可以允许它在合适的时机再去执行。

    我们也可以简单的理解为,部分逻辑在之后进行执行然后被 render 到指定 id 的 Div 中的,那么为什么传统的组件为什么不能这么做呢?而非得 Web Components 呢?那就不得不提到它所包含的另一个技术特性:Shadow DOM


    组件隔离(Shadow Dom)

    Shadow DOM 为自定义的组件提供了包括 CSS、事件的有效隔离,不再担心不同的组件之间的样式、事件污染了。 这相当于为自定义组件提供了一个天然有效的保护伞。

    Shadow DOM 实际上是一个独立的子 DOM Tree,通过有限的接口和外部发生作用。 我们都知道页面中的 DOM 节点数越多,运行时性能将会越差,这是因为 DOM 节点的相互作用会时常在触发重绘(Repaint)和重排(reflow)时会关联计算大量 Frame 关系。


    而对 CSS 的隔离也将加快选择器的匹配速度,即便可能是微秒级的提升,但是在极端的性能情况下,依然是有效的手段。

    Quark 能为你带来什么?

    提效降本几乎是所有企业的主旋律,Quark 本身除了提供了通用组件之外,我们还为大家提供了开箱即用的 CLI,可以让大家在直接在日常开发中开发横跨多个技术栈/框架的业务组件。比如一个相同样式的营销弹窗,可以做到:

    • 同时运行在不同技术栈(Angular、Vue、React等)的前端工程中

    • 同时运行在不同版本的技术栈中,比如能同时运行在 Vue2.x、Vue3.x 中

    CLI 内部 Beta 版本目前初版已完成,github 地址:github.com/hellof2e/qu…

    适合场景:前端团队想发布一个独立的组件或npm包,让其他各类技术栈的工程使用,从而达到提效降本的目的。

    npm i -g @quarkd/quark-cli
    npx create-quark


    相关链接

    作者:Allan91
    来源:juejin.cn/post/7160483409691672606

    收起阅读 »

    uni-app跨端开发之疑难杂症

    今年,公司决定解决各个团队移动端开发的混战局面,由架构部出一套移动端框架,规范化开发标准。经过一段时间的调研,考虑到跨端以及公司主要技术栈为vue,最终选择了uni-app作为移动端框架,在大家都“很忙”的情况下,我成为了移动端框架的主要开发。以前就总听同事说...
    继续阅读 »

    前言

    今年,公司决定解决各个团队移动端开发的混战局面,由架构部出一套移动端框架,规范化开发标准。经过一段时间的调研,考虑到跨端以及公司主要技术栈为vue,最终选择了uni-app作为移动端框架,在大家都“很忙”的情况下,我成为了移动端框架的主要开发。以前就总听同事说,uni-app有很多坑,我对其也只是有些许了解,这回的全身心投入,才知道一入深坑愁似海

    这段时间也做了一些成效,头大如斗的路由拦截、必不可少的http请求封装、提高成效的组件库、仿照微信的oAuth 2.0登录、复杂逻辑的离线存储、用户需要的增量更新包

    有成效也踩了一些坑,百思不得解的console.log、烦到吐血的网络调试、爬坑许久的APP与h5通讯、性能极差的微信小程序端uni.canvasToTempFilePath

    今天就要聊聊一些疑难杂症,有些忘记了,有些还没碰到,后续持续更新吧!

    百思不得解的console.log

    移动端框架是采用npm包的方式提供给业务部门使用,其中包含oAuth2.0登录方式,这其中涉及到了h5通过scheme协议唤醒app并且带回code等参数,相应的参数会存放在plus.runtime.arguments,其他情况下,plus.runtime.arguments的值为空。在给同事排查问题时我就简单操作,在node_modules对应的npm包里面写了不是很严谨的如下代码:

    const args = plus.runtime.arguments;
    // 这个是业务部门出错时,我添加的调试代码
    console.log('>>>>>>'args)
    if (args) {
     const isLogout = args.includes('logout');
     if (isLogout) {
       await this.handleSession();
    else {
       await this.handleAuthorization(args);
    }
    }

    我测试是正常的,args是空值,所以是不会执行if内的逻辑的,但是他这边会执行if内的逻辑的,初步判断args由于某个原因导致存在值了,为了简单明了的查看输出内容,然后我就写了毁一生的console.log('>>>>>>', args),这行调试代码的输出内容如下,我一直以为args是空值,但是判断依旧为true,有点颠覆了我的人生观,后来灵机一动,删掉了第一个修饰参数,发现args原来是有值的,经过排查,是因为添加了微信小程序打开指定页面,导致记录当前页面数据。


    烦到吐血的网络调试

    网络调试对于我们的日常开发是很重要的,有助于快速判断资源请求问题,但uni-app在这方面有很大的缺陷,在讨论这个问题时,先来看一下uni-app的真机调试方式。

    终端调试工具

    当项目运行时,点击终端上的调试按钮,会弹出一个调试界面。


    从调试面板中,可以看到仅有ConsoleElementsSources三个选项,期待许久的Network并没有出现,这种调试方式没办法实现网络请求调试。


    webview调试控制台

    点击工具栏的 运行 -> 运行到手机或模拟器 -> 显示webview调试控制台 会出现一个跟谷歌浏览器一样的调试界面,虽然这里有Network,但是很可惜,这个功能存在问题,没办法监听到网络请求。


    Fiddler 抓取网络请求

    在走投无路之下,只能另辟蹊径,借助工具,抓取真机的网络请求,接下来阐述一下怎么使用Fiddler抓取真机的网络请求,配置完需要重启才生效。

    下载Fiddler

    这是一个免费工具,自行在网络上下载即可。

    Fiddler 基础配置

    点击工具栏的tools,选择options就会弹出一个配置界面



    HTTPS 配置

    选择HTTPS选项,勾选选矿中的Capture HTTPS CONNECTsDecrypt HTTPs trfficIgnore server certificate errors


    Connections 配置

    这边配置的端口号后面配置代理的时候需要使用到。


    手机配置代理

    注意需要和电脑连接同一网络,点击进入手机WIFI详情界面,有个代理,选择手动模式,输入电脑的IP地址和Fiddler的监听端口,即可拦截到真机的所有网络请求,包含我们app对应的网络请求。


    过滤

    这边可以选择过滤对应的ip或域名,多个的话通过分号隔开即可。


    爬坑许久的APP与h5通讯

    谈论这个问题时,先描述一下uni-app实现的app怎么和h5通讯

    app端

    对于app端的通讯,.vue.nvue有两点区别,1. 获取webView实例不一致,2. 监听方法不一致。app向h5传递数据时,需要借助webview.evalJS执行h5的全局方法,而h5向app传递参数时,类似于h5发送postMessage,可以在webview的message/onPostMessage监听函数获取数据。

    vue

    获取webView示例

    webView实例的获取,对于vue文件不是特别友好,需要借助于this.$scope.$getAppWebview(),如果是在组件中需要使用this.$parent.$scope.$getAppWebview(),添加延时的原因是,h5页面可能未加载完成,无法获取到对应的全局函数,会提示xxx函数undefined;

    <template>
       <web-view src="http://www.juejin.com"></web-view>
    </template>
    <script>
       export default {
           onReady() {
               const currentWebview = this.$scope.$getAppWebview();
               const account = '清欢bx'
               setTimeout(() => {
                   const webView = currentWebview.children()[0];
                   webView.evalJS(`setAccountInfo(${account})`);
              }, 1000);
          }
      }
    </script>

    监听方法

    vue文件采用@message触发监听函数

    <template>
       <web-view @message="handleMessage" src="http://www.juejin.com"></web-view>
    </template>
    <script>
       export default {
           methods: {
               handleMessage(data) {
                   console.log(data)
              }
          }
      }
    </script>

    nvue

    获取webView示例

    在nvue获取webView实例就很流畅了,直接通过this.$refs.webview就能获取到。

    <template>
       <web-view ref="webview" src="http://www.juejin.com"></web-view>
    </template>
    <script>
       export default {
           onReady() {
               const account = '清欢bx'
               this.$refs.webview.evalJs(`setAccountInfo(${account})`);
          }
      }
    </script>

    监听方法

    nvue文件采用@onPostMessage触发监听函数

    <template>
       <web-view @onPostMessage="handleMessage" src="http://www.juejin.com"></web-view>
    </template>
    <script>
       export default {
           methods: {
               handleMessage(data) {
                   console.log(data)
              }
          }
      }
    </script>

    h5 端

    发送数据

    需要引入一个uni-app的sdk,uni.webview.1.5.4.js,最低版本需要1.5.4,可以在index.html引入,也可以在main.js引入,注意点是传递的参数必须写在data里面,也就是维持这样的数据结构。

    uni.postMessage({
       data: {
         xxxxxx,
         xxxxxx
      }
    });

    如果是页面加载完成时就需要发送数据,需要等待UniAppJSBridgeReady钩子结束后触发postMessage;

    <script>
       export default {
           mounted() {
               document.addEventListener('UniAppJSBridgeReady'function() {
                   uni.webView.getEnv(function(res) {
                       console.log('当前环境:' + JSON.stringify(res));
                  });
                   uni.postMessage({
                       data: {
                         action'message'
                      }
                  });
              });
          }
      }
    </script>

    如果是通过事件点击发送数据,因为这时候页面已经加载完成,不需要再去监听UniAppJSBridgeReady钩子,直接触发uni.postMessage即可。

    <template>
       <view>
           <button @click="handlePostMessage">发送数据</button>
       </view>
    </template>
    <script>
       export default {
           methods: {
               handlePostMessage() {
                   uni.postMessage({
                       data: {
                         action'message'
                      }
                  });
              }
          }
      }
    </script>

    获取数据

    获取数据的函数,需要挂载到window上,可以直接写在main.js里面,数据需要共享到具体页面内,可以使用本地村存储localStorage、事件总线eventBusvuex,根据自己的需求选择。

    window.setAccountInfo = function(data) {
       console.log(data)
    }

    踩坑点

    uni is not defined

    app需要涉及到离线或者内网,索引uni.webview.js下载到本地进行引入,因为uni.webview.js已经被编译成了umd格式,在vue项目中在进行一次打包后,导致this指向不是window,所以没有把uni挂在到全局上,将this指向改为window即可。

    未改造之前的代码


    改造后


    或者


    app向h5传递参数时,无法传递对象,并且传递的参数需要字符串序列化

    在传递参数时,对象传递过去没办法识别,同时传递的参数需要执行JSON.stringify(),多个参数时,可以多个参数传递,也可以把多个参数进行字符串拼接,然后再h5端进行拆分处理。

    const { accountpassword } = accountInfo;
    const _account = JSON.stringify(account);
    const _password = JSON.stringify(password);
    setTimeout(() => {
       const webView = currentWebview.children()[0];
       webView.evalJS(`setAccountInfo(${_account}, ${_password})`);
    }, 1000);

    四、性能极差的canvas转图片

    自定义组件库里包含手写签名组件,需要用到uni.canvasToTempFilePathcanvas转成图片,这个方法的生成基础图片大小是根据当前屏幕分辨率,在模拟器上运行感觉性能还可以,但是在真机上的性能不高,如果笔画多的话,有时需要十几秒时间,这是没办法接受的,不过也有解决方式,可以通过设置destWidthdestHeight来自定义图片生成的大小,牺牲一些图片清晰度,来提高性能。

    uni.canvasToTempFilePath(
      {
         canvasIdthis.canvaId,
         destWidththis.imgWidth,
         destHeightthis.imgHeight,
         success: (res) => {
           console.log('success')
        },
         fail(e) {
           console.error(e);
        },
      },
       this,
    );

    小结

    我目前主要负责公司uni-app移动端框架的开发,包含组件库相应的生态工具多端适配离线存储hybrid,如果你也正在做相同的事,或者在使用uni-app开发,或者在学习uni-app都可以相互探讨,在这踩坑的过程中,我会持续完善此系类文章,帮助大家和自己更好的使用uni-app开发项目,fighting~

    作者:清欢bx
    来源:juejin.cn/post/7156017191169556511

    收起阅读 »

    uniapp热更新

    为什么要热更新热更新主要是针对app上线之后页面出现bug,修改之后又得打包,上线,每次用户都得在应用市场去下载很影响用户体验,如果用户不愿意更新,一直提示都不愿意更新,这个bug就会一直存在。 可能你一不小心写错了代码,整个团队的努力都会付之东流,苦不苦,冤...
    继续阅读 »

    为什么要热更新

    热更新主要是针对app上线之后页面出现bug,修改之后又得打包,上线,每次用户都得在应用市场去下载很影响用户体验,如果用户不愿意更新,一直提示都不愿意更新,这个bug就会一直存在。 可能你一不小心写错了代码,整个团队的努力都会付之东流,苦不苦,冤不冤,想想都苦,所以这个时候热更新就显得很重要了。

    首先你需要在manifest.json 中修改版本号

    如果之前是1.0.0那么修改之后比如是1.0.1或者1.1.0这样


    然后你需要在HBuilderX中打一个wgt包

    在顶部>发行>原生App-制作移动App资源升级包


    包的位置会在控制台里面输出


    你需要和后端约定一下接口,传递参数


    然后你就可以在app.vue的onLaunch里面编写热更新的代码了,如果你有其他需求,你可以在其他页面的onLoad里面编写。

        // #ifdef APP-PLUS  //APP上面才会执行
    plus.runtime.getProperty(plus.runtime.appid, function(widgetInfo) {
    uni.request({
                                           url:'请求url写你自己的',
    method: "POST",
    data: {
    version: widgetInfo.version, //app版本号
    name: widgetInfo.name    //app名称
    },
    success: (result) => {
    console.log(result)  //请求成功的数据
    var data = result.data.data  
    if (data.update && data.wgtUrl) {
    var uploadTask = uni.downloadFile({ //下载
    url: data.wgtUrl, //后端传的wgt文件
                                       success: (downloadResult) => { //下载成功执行
                             if (downloadResult.statusCode === 200) {
              plus.runtime.install(downloadResult.tempFilePath, {
                                                   force: flase
                                          }, function() {
                                       plus.runtime.restart();
                                                      }, function(e) {});
                                                      }
                                                  },
                                                  })
                             uploadTask.onProgressUpdate((res) => {
    // 测试条件,取消上传任务。
    if (res.progress == 100) { //res.progress 上传进度
    uploadTask.abort();
    }
      });
            }
                                          }
                                      });
                                  });
    // #endif

    不支持的情况

    • SDK 部分有调整,比如新增了 Maps 模块等,不可通过此方式升级,必须通过整包的方式升级。

    • 原生插件的增改,同样不能使用此方式。
      对于老的非自定义组件编译模式,这种模式已经被淘汰下线。但以防万一也需要说明下,老的非自定义组件编译模式,如果之前工程没有 nvue 文件,但更新中新增了 nvue 文件,不能使用此方式。因为非自定义组件编译模式如果没有nvue文件是不会打包weex引擎进去的,原生引擎无法动态添加。自定义组件模式默认就含着weex引擎,不管工程下有没有nvue文件。

    注意事项

    • 条件编译,仅在 App 平台执行此升级逻辑。

    • appid 以及版本信息等,在 HBuilderX 真机运行开发期间,均为 HBuilder 这个应用的信息,因此需要打包自定义基座或正式包测试升级功能。

    • plus.runtime.version 或者 uni.getSystemInfo() 读取到的是 apk/ipa 包的版本号,而非 manifest.json 资源中的版本信息,所以这里用 plus.runtime.getProperty() 来获取相关信息。

    • 安装 wgt 资源包成功后,必须执行 plus.runtime.restart(),否则新的内容并不会生效。

    • 如果App的原生引擎不升级,只升级wgt包时需要注意测试wgt资源和原生基座的兼容性。平台默认会对不匹配的版本进行提醒,如果自测没问题,可以在manifest中配置忽略提示,详见ask.dcloud.net.cn/article/356…

    • http://www.example.com 是一个仅用做示例说明的地址,实际应用中应该是真实的 IP 或有效域名,请勿直接复制粘贴使用。

    关于热更新是否影响应用上架

    应用市场为了防止开发者不经市场审核许可,给用户提供违法内容,对热更新大多持排斥态度。

    但实际上热更新使用非常普遍,不管是原生开发中还是跨平台开发。

    Apple曾经禁止过jspatch,但没有打击其他的热更新方案,包括cordovar、react native、DCloud。封杀jspatch其实是因为jspatch有严重安全漏洞,可以被黑客利用,造成三方黑客可篡改其他App的数据。

    使用热更新需要注意:

    • 上架审核期间不要弹出热更新提示

    • 热更新内容使用https下载,避免被三方网络劫持

    • 不要更新违法内容、不要通过热更新破坏应用市场的利益,比如iOS的虚拟支付要老老实实给Apple分钱

    如果你的应用没有犯这些错误,应用市场是不会管的。

    作者:是一个秃头
    来源:juejin.cn/post/7039273141901721608

    收起阅读 »

    uniapp的骨架屏生成指南

    骨架屏一般用于页面在请求远程数据尚未完成时,页面用灰色块预显示本来的页面结构,给用户更好的体验。 使用到的API有uni.createSelectorQuery()uni.getSystemInfoSync()。常规首页的布局一般而言,我们的首页的基础布局是包...
    继续阅读 »

    骨架屏一般用于页面在请求远程数据尚未完成时,页面用灰色块预显示本来的页面结构,给用户更好的体验。 使用到的API有uni.createSelectorQuery()uni.getSystemInfoSync()

    常规首页的布局

    一般而言,我们的首页的基础布局是包含的有:顶部搜索、轮播、金刚区、新闻简报、活动魔方。

    <template>
       <view class="content">
           <!-- 顶部搜索 -->
           <headerSerch></headerSerch>
           <!-- 轮播 -->
           <swiperBg></swiperBg>
           <!-- 金刚区 -->
           <menus></menus>
           <!-- 新闻简报 -->
           <news></news>
           <!-- 活动魔方 -->
           <activity></activity>
           <!-- 骨架屏 -->
           <skeleton :show="show"></skeleton>
       </view>
    </template>

    <script>
       import headerSerch from './components/headerSerch.vue'
       import swiperBg from './components/swiperBg.vue'
       import menus from './components/menus.vue'
       import news from './components/news.vue'
       import activity from './components/activity.vue'
       import skeleton from './components/skeleton.vue'
       export default {
           components: {
               headerSerch,
               swiperBg,
               menus,
               news,
               activity,
               skeleton
          },
           data() {
               return {
                   show: true
              }
          },
           mounted() {
               setTimeout(()=>{
                   this.show = false
              },1200)
          }
      }
    </script>

    <style scoped>

    </style>

    skeleton组件的实现

    代码如下,稍后给大家解释


    步骤一 设置骨架屏的基础样式

    我们通过绝对定位的方式把组件的根元素提高层级,避免被父组件的其他组件覆盖掉。使用 uni.getSystemInfoSync()同步获取系统的可使用窗口宽度和可使用窗口高度并赋值给组件根元素的宽高。

    <view :style="{
    width: windowWidth,
    height: windowHeight,
    backgroundColor: bgColor,
    position: 'absolute',
    zIndex: 9999,
    top: top,
    left: left
    }">
    ......
    ......
    </view>

    <script>
       let systemInfo = uni.getSystemInfoSync();
       export default {
           name: 'skeleton',
           props: {
               show: {
                   type: Boolean,
                   default: true
              },
          },
           data() {
               return {
                   windowWidth: systemInfo.windowWidth + 'px',
                   windowHeight: systemInfo.windowHeight + 'px',
                   bgColor: '#fff',
                   top: 0,
                   left: 0,
              }
          }
        }
    </script>

    步骤二 渲染出占位的灰色块

    通过uniapp的uni.createSelectorQuery()接口,查询页面带有指定类名的元素的位置和尺寸, 通过绝对定位的方式,用同样尺寸的灰色块定位到相同的位置。

    在骨架屏中多数用的主要的矩形节点rectNodes 和圆形节点circleNodes。

    首先给这些元素加上相同的skeleton-fade类,这个类的主要为了有一个灰色的背景并使用animate属性使其看到颜色的深浅变化。


    按照官方的API使用说明,我们得在mounted 后进行调用方法。 在uni.createSelectorQuery()的后面加in(this.$parent)在微信小程序才能生效,在H5端不用加也生效。(我们主要是获取指定元素的位置和高度详细并赋值给rectNodes、circleNodes,所以得到之后可以把这两个方法删掉。)

    mounted() {
       // 矩形骨架元素
       this.getRectEls();
       // 圆形骨架元素
       this.getCircleEls();
    },

    methods: {
       getRectEls() {
           let query = uni.createSelectorQuery().in(this.$parent)
           query.selectAll('.skeleton-rect').boundingClientRect(res => {
                   console.log('rect', JSON.stringify(res));
          }).exec(function() {

          })
      },
       getCircleEls() {
           let query = uni.createSelectorQuery().in(this.$parent)
           query.selectAll('.skeleton-circle').boundingClientRect(res => {
                   console.log('circle', JSON.stringify(res));
          }).exec(function() {

          })
      }
    },

    如下图,在控制台上可以得到我们想到的节点信息。


    然后再复制粘贴给data中的rectNodes、circleNodes。 skeleton组件基本上就完成了。我们再做下优化,skeleton组件接收父组件传的show值,默认是true,当父组件的数据接口请求完成之后show设置为false。

    大功告成,以下的在浏览器端和微信小程序端的骨架屏展示:



    作者:清风programmer
    来源:juejin.cn/post/7037476325480742920

    收起阅读 »

    被「羊了个羊」逼疯后,鹅厂程序员怒而自制「必通关版」|GitHub热榜

    「羊了个羊」有多恶心?能逼程序员气到撸起袖子自己上……这两天,GitHub上就出现这么一个仿制版,名曰「鱼了个鱼」。不同于以「极低通关率」肝死玩家的原版,此版作者放出话来——没广告!可自定义关卡和图案!道具无限!。甚至可以定制出这(离)样(谱)的界面:目前,该...
    继续阅读 »

    「羊了个羊」有多恶心?

    能逼程序员气到撸起袖子自己上……

    这两天,GitHub上就出现这么一个仿制版,名曰「鱼了个鱼」


    不同于以「极低通关率」肝死玩家的原版,此版作者放出话来——

    没广告!可自定义关卡和图案!道具无限!

    甚至可以定制出这(离)(谱)的界面:


    目前,该项目已登GitHub热榜,获297个Star。(链接已附在文末)


    比「羊」更让人舒适

    先看看这款「鱼了个鱼」体验如何。

    从最简单模式开启,简直不要太Easy,道具都无需使用。


    再看中等和困难模式,稍有难度,还好有道具!

    原版的洗牌、撤回、移出可无限次使用,还有更多玄妙功能。

    比如透视,能看到最下方两列叠起来图案依次是什么,这感觉,相当于斗地主把最后三张看完了。


    再比如圣光,能把一大堆图案下面的图层从灰变白,看得更清楚。


    最逆天的还是破坏功能,直接消掉3个同样图案:


    也就是说,一直狂按这个道具能直接通关。


    值得一提的是,通关后祝贺画面是这个:


    建议作者优化下前端,直接换成这个:


    怒而自制必通关版

    据作者介绍,自己也是玩「羊了个羊」几十次,其间,他用尽道具,看了几十遍借贷广告,向富家千金反复求婚,仍然过不了第二关——

    他发现事情不对劲。

    由于方块生成完全随机,那越到后期,越来越多方块叠压在一起,可选方块变少,自然越来越难,经常无解也是常事。

    另一方面,正是极低的通关率让每个「自以为必胜」的玩家上头得不行,形成了上瘾感。


    于是……他怒而自制一个必能通关的版本。

    要求嘛,务必无广告,务必道具无限,要能自定义难度和图案,那更是一件美事儿。

    具体到原理,作者提出四大纲领。

    首先,游戏全局参数设置上,需要将槽位数量、层数等变量抽取成统一的全局变量,每当修改一处,系统自动适配,此外,作者还开放了参数自定义——

    嫌槽位不足?可以自己多加一个!


    其次是整体网格设计。

    为了能快速做出游戏,作者直接将画布分为24×24的虚拟网格,类似一个棋盘——

    每个网格又被划分成3×3的小单元,各层图案生成时,会相互错开1-2个单元,形成层层交叠、密密麻麻的样子。


    第三步是设计随机生成块的图案和坐标。

    先根据全局参数计算总块数,游戏难度越高,块数和相应层数也越多,然后作者用shuffle函数打乱存储所有动物图案的数组,再依次,把图案重新填充到方块中。

    至于如何生成方块坐标,直接让程序随机选取坐标范围内的点,同时随层级变深,坐标范围也越来越小,造成一种——

    越往深了去,图案越拥挤,难度相应越高的效果。


    大致分布规律就是越「深」层越「挤」

    最后,设定上下层块与块的关系。

    作者先给每个块指定一个层级属性,随机生成时,给相互重叠的块绑定层级关系,确保消掉上层块,才能点击下层块。

    基于上述思路,作者熬夜爆肝几个小时,就把游戏雏形做出来了,还放到GitHub上将代码开源——

    他感慨道,总算是满足了自己的通关夙愿。


    作者介绍

    事实上,「鱼了个鱼」项目作者「程序员鱼皮」已小有名气。

    据其个人公开资料显示,「程序员鱼皮」98年出生,现在鹅厂,从事全栈应用开发,同时,也是腾讯云开发高级布道师。

    工作之外,鱼皮利用业余时间做了很多入职大厂经验、技术干货和资源分享,据他称,在校期间就带领工作室建设了几十个校园网站。


    最后,附上「鱼了个鱼」在线体验链接,收获通关喜悦(狗头):

    https://yulegeyu.cn

    参考链接:
    [1]https://github.com/liyupi/yulegeyu
    [2]https://www.bilibili.com/video/BV1Pe411M7wh
    [3]https://mp.weixin.qq.com/s/D_I1Tq-ofhKhlp0rkOpaLA

    来源:詹士 发自 凹非寺

    收起阅读 »

    由点汇聚成字的动效炫极了

    前言在引入 fl_chart 绘制图表的时候,看到插件有下面这样的动效,随机散乱的圆点最后组合成了 Flutter 的 Logo,挺酷炫的。本篇我们来探讨类似的效果怎么实现。点阵在讲解代码实现之前,我们先科普一个知识,即点阵。点阵在日常生活中很常见,比如广告屏...
    继续阅读 »

    由点汇聚成字的动效炫极了

    前言

    在引入 fl_chart 绘制图表的时候,看到插件有下面这样的动效,随机散乱的圆点最后组合成了 Flutter 的 Logo,挺酷炫的。本篇我们来探讨类似的效果怎么实现。

    logo 动画.gif

    点阵

    在讲解代码实现之前,我们先科普一个知识,即点阵。点阵在日常生活中很常见,比如广告屏,停车系统的显示,行业内称之为 LED 显示屏。

    image.png

    LED 显示屏实际上就是由很多 LED 灯组合成的一个显示面板,然后通过显示驱动某些灯亮,某些灯灭就可以实现文字、图形的显示。LED 显示屏的点距足够小时,色彩足够丰富时其实就形成了我们日常的显示屏,比如 OLED 显示屏其实原理也是类似的。之前报道过的大学宿舍楼通过控制每个房间的灯亮灯灭来形成文字的原理也是一样的。

    image.png

    现在来看看 LED显示文字是怎么回事,比如我们要 显示岛上码农的“岛”字,在16x16的点阵上,通过排布得到的就是下面的结果(不同字体的排布会有些差别)。

    因为每一行是16个点,我们可以对应为16位二进制数,把黑色的标记为1,灰色的标记为0,每一行就可以得到一个二进制数。比如上面的第一行第8列为1,其他都是0,对应的二进制数就是0000000100000000,对应的16进制数就是0x0100。把其他行也按这种方式计算出来,最终得到的“岛”字对应的是16个16进制数,如下所示。

     [
    0x0100, 0x0200, 0x1FF0, 0x1010,
    0x1210, 0x1150, 0x1020, 0x1000,
    0x1FFC, 0x0204, 0x2224, 0x2224,
    0x3FE4, 0x0004, 0x0028, 0x0010
    ];
    复制代码

    又了这个基础,我们就可以用 Flutter 绘制点阵图形。

    点阵图形绘制

    首先我们绘制一个“LED 面板”,也就是绘制一个有若干个点构成的矩阵,这个比较简单,保持相同的间距,逐行绘制相同的圆即可,比如我们绘制一个16x16的点阵,实现代码如下所示。

    var paint = Paint()..color = Colors.grey;
    final dotCount = 16;
    final fontSize = 100.0;
    var radius = fontSize / dotCount;
    var startPos =
    Offset(size.width / 2 - fontSize, size.height / 2 - 2 * fontSize);
    for (int i = 0; i < dotCount; ++i) {
    var position = startPos + Offset(0.0, radius * i * 2);
    for (int j = 0; j < dotCount; ++j) {
    var dotPosition = startPos + Offset(radius * 2 * j, position.dy);
    canvas.drawCircle(dotPosition, radius, paint);
    }
    }
    复制代码

    绘制出来的效果如下:

    image.png

    接下来是点亮对应的位置来绘制文字了。上面我们讲过了,每一行是一个16进制数,那么我们只需要判断每一行的16进制数的第几个 bit是1就可以了,如果是1就点亮,否则不点亮。点亮的效果用不同的颜色就可以了。 怎么判断16进制数的第几个 bit 是不是1呢,这个就要用到位运算技巧了。实际上,我们可以用一个第 N 个 bit 是1,其他 bit 都是0的数与要判断的数进行“位与”运算,如果结果不为0,说明要判断的数的第 N 个 bit 是1,否则就是0。听着有点绕,看个例子,我们以0x0100为例,按从第0位到第15位逐个判断第0位和第15位是不是1,代码如下:

    for (i = 0 ; i < 16; ++i) {
    if ((0x0100 & (1 << i)) > 0) {
    // 第 i 位为1
    }
    }
    复制代码

    这里有两个位操作,1 << i是将1左移 i 位,为什么是这样呢,因为这样可以构成0x0001,0x0002,0x0004,...,0x8000等数字,这些数字依次从第0位,第1位,第2位,...,第15位为1,其他位都是0。然后我们用这样的数与另外一个数做位与运算时,就可以依次判断这个数的第0位,第1位,第2位,...,第15位是否为1了,下面是一个计算示例,第11位为1,其他位都是0,从而可以 判断另一个数的第11位是不是0。

    位与运算

    通过这样的逻辑我们就可以判断一行的 LED 中第几列应该点亮,然后实现文字的“显示”了,实现代码如下。wordHex是对应字的16个16进制数的数组。dotCount的值是16,用于控制绘制16x16大小的点阵。每隔一行我们向下移动一段直径距离,每隔一列,我们向右移动一段直径距离。然后如果当前绘制位置的数值对应的 bit位为1,就用蓝色绘制,否则就用灰色绘制。这里说一下为什么左移的时候要用dotCount - j - 1,这是因为绘制是从左到右的,而16进制数的左边是高位,而数字j是从小到大递增的,因此要通过这种方式保证判断的顺序是从高位(第15位)到低位(第0位),和绘制的顺序保持一致。

     for (int i = 0; i < dotCount; ++i) {
    var position = startPos + Offset(0.0, radius * i * 2);
    for (int j = 0; j < dotCount; ++j) {
    var dotPosition = startPos + Offset(radius * 2 * j, position.dy);

    if ((wordHex[i] & ((1 << dotCount - j - 1))) != 0) {
    paint.color = Colors.blue[600]!;
    canvas.drawCircle(dotPosition, radius, paint);
    } else {
    paint.color = Colors.grey;
    canvas.drawCircle(dotPosition, radius, paint);
    }
    }
    }
    复制代码

    绘制的结果如下所示。

    image.png

    由点聚集成字的动画实现

    接下来我们来考虑如何实现开篇说的类似的动画效果。实际上方法也很简单,就是先按照文字应该“点亮”的 LED 的数量,先在随机的位置绘制这么多数量的 LED,然后通过动画控制这些 LED 移动到目标位置——也就是文字本该绘制的位置。这个移动的计算公式如下,其中 t 是动画值,取值范围为0-1.

    移动公式

    需要注意的是,随机点不能在绘图过程生成,那样会导致每次绘制产生新的随机位置,也就是初始位置会变化,导致上面的公式实际不成立,就达不到预期的效果。另外,也不能在 build 方法中生成,因为每次刷新 build 方法就会被调用,同样会导致初始位置发生变化。所以,生成随机位置应该在 initState方法完成。但是又遇到一个新问题,那就是 initState方法里没有 context,拿不到屏幕宽高,所以不能直接生成位置,我们只需要生成一个0-1的随机系数就可以了,然后在绘制的时候在乘以屏幕宽高就得到实际的初始位置了。初始位置系数生成代码如下:

    @override
    void initState() {
    super.initState();
    var wordBitCount = 0;
    for (var hex in dao) {
    wordBitCount += _countBitOne(hex);
    }
    startPositions = List.generate(wordBitCount, (index) {
    return Offset(
    Random().nextDouble(),
    Random().nextDouble(),
    );
    });
    ...
    }
    复制代码

    wordBitCount是计算一个字中有多少 bit 是1的,以便知道要绘制的 “LED” 数量。接下来是绘制代码了,我们这次对于不亮的直接不绘制,然后要点亮的位置通过上面的位置计算公式计算,这样保证了一开始绘制的是随机位置,随着动画的过程,逐步移动到目标位置,最终汇聚成一个字,就实现了预期的动画效果,代码如下。

    void paint(Canvas canvas, Size size) {
    final dotCount = 16;
    final fontSize = 100.0;
    var radius = fontSize / dotCount;
    var startPos =
    Offset(size.width / 2 - fontSize, size.height / 2 - fontSize);
    var paint = Paint()..color = Colors.blue[600]!;

    var paintIndex = 0;
    for (int i = 0; i < dotCount; ++i) {
    var position = startPos + Offset(0.0, radius * i * 2);
    for (int j = 0; j < dotCount; ++j) {
    // 判断第 i 行第几位不为0,不为0则绘制,否则不绘制
    if ((wordHex[i] & ((1 << dotCount - j))) != 0) {
    var startX = startPositions[paintIndex].dx * size.width;
    var startY = startPositions[paintIndex].dy * size.height;
    var endX = startPos.dx + radius * j * 2;
    var endY = position.dy;
    var animationPos = Offset(startX + (endX - startX) * animationValue,
    startY + (endY - startY) * animationValue);
    canvas.drawCircle(animationPos, radius, paint);
    paintIndex++;
    }
    }
    }
    }
    复制代码

    来看看实现效果吧,是不是很酷炫?完整源码已提交至:绘图相关源码,文件名为:dot_font.dart

    点阵汇聚文字动画.gif

    总结

    本篇介绍了点阵的概念,以及基于点阵如何绘制文字、图形,最后通过先绘制随机点,再汇聚成文字的动画效果。可以看到,化整为零,再聚零为整的动画效果还是蛮酷炫的。实际上,基于这种方式,可以构建更多有趣的动画效果。

    作者:岛上码农

    来源:juejin.cn/post/7120233450627891237

    收起阅读 »

    uniapp使用canvas实现二维码分享

    实现使用canvas在小程序H5页面进行二维码分享 如下图效果 可以保存并扫码总体思路:使用canvas进行绘制,为了节省时间固定部分采用背景图绘制 只有二维码以及展示图片及标题绘制,绘制完成后调用uni.canvasToTempFilePath将其转为图片展...
    继续阅读 »

    实现使用canvas在小程序H5页面进行二维码分享 如下图效果 可以保存并扫码


    总体思路:使用canvas进行绘制,为了节省时间固定部分采用背景图绘制 只有二维码以及展示图片及标题绘制,绘制完成后调用uni.canvasToTempFilePath将其转为图片展示

    1.组件调用,使用ref调用组件内部相应的canvas绘制方法,传入相关参数 包括名称 路由 展示图片等。

     <SharePoster v-if='showposter' ref='poster' @close='close'/>

    <script>
     import SharePoster from "@/components/share/shareposter.vue"
     export default {
       components: {
          SharePoster,
      },
      methods:{
          handleShare(item){
             this.showposter=true
             if(this.showvote){
               this.showvote=false
            }
             this.$nextTick(() => {
            this.$refs.poster.drawposter(item.name, `/pagesMore/voluntary/video/player?schoolId=${item.id}`,item.cover)
            })
          },
      }
    </script>

    2.组件模板放置canvas容器并赋予id以及宽度高度等,使用iscomplete控制是显示canvas还是显示最后调用uni.canvasToTempFilePath生成的图片

    <div class="poster-wrapper" @click="closePoster($event)">
         <div class='poster-content'>
             <canvas canvas-id="qrcode"
               v-if="qrShow"
              :style="{opacity: 0, position: 'absolute', top: '-1000px'}"
             ></canvas>
             <canvas
               canvas-id="poster"
              :style="{ width: cansWidth + 'px', height: cansHeight + 'px' ,opacity: 0, }"
               v-if='!iscomplete'
             ></canvas>
             <image
               v-if="iscomplete"
              :style="{ width: cansWidth + 'px', height: cansHeight + 'px' }"
              :src="tempFilePath"
               @longpress="longpress"
             ></image>
         </div>
     </div>

    3.data内放置相应配置参数

     data() {
         return {
             bgImg:'https://cdn.img.up678.com/ueditor/upload/image/20211130/1638258070231028289.png', //画布背景图片
             cansWidth:288, // 画布宽度
             cansHeight:410, // 画布高度
             projectImgWidth:223, // 中间展示图片宽度
             projectImgHeight:167, // 中间展示图片高度
             qrShow:true, // 二维码canvas
             qrData: null, // 二维码数据
             tempFilePath:'',// 生成图路径
             iscomplete:false, // 是否生成图片
        }
      },

    4.在created生命周期内调用uni.createCanvasContext创建canvas实例 传入模板内canvas容器id

    created(){
         this.ctx = uni.createCanvasContext('poster',this)
      },

    5.调用对应方法,绘制分享作品

       // 绘制分享作品
         async drawposter(name='重庆最美高校景象',url,projectImg){
              uni.showLoading({
                title: "加载中...",
                mask: true
              })
              // 生成二维码
             await this.createQrcode(url)
             // 背景
             await this.drawWebImg({
               url: this.bgImg,
               x: 0, y: 0, width: this.cansWidth, height: this.cansHeight
            })
             // 展示图
             await this.drawWebImg({
               url: projectImg,
               x: 33, y: 90, width: this.projectImgWidth, height: this.projectImgHeight
            })
             await this.drawText({
               text: name,
               x: 15, y: 285, color: '#241D4A', size: 15, bold: true, center: true,
               shadowObj: {x: '0', y: '4', z: '4', color: 'rgba(173,77,0,0.22)'}
            })
             // 绘制二维码
             await this.drawQrcode()
             //转为图片
             this.tempFilePath = await this.saveCans()
             this.iscomplete = true
             uni.hideLoading()
        },

    6.绘制图片方法,注意 this.ctx.drawImage方法第一个参数不能放网络图片 必须执行下载后绘制

      drawWebImg(conf) {
           return new Promise((resolve, reject) => {
             uni.downloadFile({
               url: conf.url,
               success: (res) => {
                 this.ctx.drawImage(res.tempFilePath, conf.x, conf.y, conf.width?conf.width:"", conf.height?conf.height:"")
                 this.ctx.draw(true, () => {
                   resolve()
                })
              },
               fail: err => {
                 reject(err)
              }
            })
          })
        },

    7.绘制文本标题

     drawText(conf) {
           return new Promise((resolve, reject) => {
             this.ctx.restore()
             this.ctx.setFillStyle(conf.color)
             if(conf.bold) this.ctx.font = `normal bold ${conf.size}px sans-serif`
             this.ctx.setFontSize(conf.size)
             if(conf.shadowObj) {
               // this.ctx.shadowOffsetX = conf.shadowObj.x
               // this.ctx.shadowOffsetY = conf.shadowObj.y
               // this.ctx.shadowOffsetZ = conf.shadowObj.z
               // this.ctx.shadowColor = conf.shadowObj.color
            }
             let x = conf.x
             conf.text=this.fittingString(this.ctx,conf.text,280)
             if(conf.center) {
               let len = this.ctx.measureText(conf.text)
               x = this.cansWidth / 2 - len.width / 2 + 2
            }

             this.ctx.fillText(conf.text, x, conf.y)
             this.ctx.draw(true, () => {
               this.ctx.save()
               resolve()
            })
          })
        },
    // 文本标题溢出隐藏处理
    fittingString(_ctx, str, maxWidth) {
               let strWidth = _ctx.measureText(str).width;
               const ellipsis = '…';
               const ellipsisWidth = _ctx.measureText(ellipsis).width;
               if (strWidth <= maxWidth || maxWidth <= ellipsisWidth) {
                 return str;
              } else {
                 var len = str.length;
                 while (strWidth >= maxWidth - ellipsisWidth && len-- > 0) {
                   str = str.slice(0, len);
                   strWidth = _ctx.measureText(str).width;
                }
                 return str + ellipsis;
              }
            },

    8.生成二维码

          createQrcode(qrcodeUrl) {
           // console.log(window.location.origin)
           const config={host:window.location.origin}
           return new Promise((resolve, reject) => {
             let url = `${config.host}${qrcodeUrl}`
             // if(url.indexOf('?') === -1) url = url + '?sh=1'
             // else url = url + '&sh=1'
             try{
               new qrCode({
                 canvasId: 'qrcode',
                 usingComponents: true,
                 context: this,
                 // correctLevel: 3,
                 text: url,
                 size: 130,
                 cbResult: (res) => {
                   this.qrShow = false
                   this.qrData = res
                   resolve()
                }
              })
            } catch (err) {
               reject(err)
            }
          })
        },

    9.画二维码,this.qrData为生成的二维码资源

      drawQrcode(conf = { x: 185, y: 335, width: 100, height: 50}) {
    return new Promise((resolve, reject) => {
    this.ctx.drawImage(this.qrData, conf.x, conf.y, conf.width, conf.height)
    this.ctx.draw(true, () => {
    resolve()
    })
    })
    },

    10.将canvas绘制内容转为图片并显示,在H5平台下,tempFilePath 为 base64

    // canvs => images
    saveCans() {
    return new Promise((resolve, reject) => {
    uni.canvasToTempFilePath({
    x:0,
    y:0,
    canvasId: 'poster',
    success: (res) => {
    resolve(res.tempFilePath)
    },
    fail: (err) => {
    uni.hideLoading()
    reject(err)
    }
    }, this)
    })
    },

    11.组件全部代码


    作者:ArvinC
    来源:juejin.cn/post/7041087990222815246

    收起阅读 »

    Uniapp 多端开发经验整理

    本文档目的在于帮助基于 Uniapp 进行移动开发的人员 快速上手、规避问题、提升效率。将以流程提纲的方式,整理开发过程各阶段可能出现的问题点以及思路。对官方文档中已有内容,会贴附链接,尽量不做过多阐述以免冗余。使用时可根据需求和自身掌握情况,从目录跳转查看。...
    继续阅读 »

    文档说明:

    本文档目的在于帮助基于 Uniapp 进行移动开发的人员 快速上手、规避问题、提升效率。将以流程提纲的方式,整理开发过程各阶段可能出现的问题点以及思路。对官方文档中已有内容,会贴附链接,尽量不做过多阐述以免冗余。

    使用时可根据需求和自身掌握情况,从目录跳转查看。

    Uniapp 使用 Vue 语法+微信小程序 API,有二者基础可快速上手,开发 APP 还会用到 HTML5+规范 ,有非常丰富的原生能力。在此还是建议尽量安排时间通读官方文档,至少留下既有功能的印象,来增强对 Uniapp 开发的掌握,游刃有余的应对各类开发需求。

    开发准备

    小程序

    后台配置

    • 小程序个别类目需要行业资质,需要一定时间来申请,根据项目自身情况尽早进行 服务类目 的设置以免影响上线时间。

    • 必须在后台进行 服务器域名配置,域名必须 为 https 。否则无法进行网络请求。注意 每月只有 5 次修改机会

      在开发工具中可配置不验证 https,这样可以临时使用非 https 接口进行开发。非 https 真机预览时需要从右上角打开调试功能。

    • 如果有 webview 需求,必须在小程序管理后台配置域名白名单。

    开发工具

    • 下载 微信开发者工具

    • 设置 → 安全 → 打开“服务端口”。打开后方可用 HbuilderX 运行并更新到微信开发者工具。

    APP

    证书文件

    • 准备苹果开发账号

    • ios 证书、描述文件 申请方法

      证书和描述文件分为开发(Development)和发布(Distribution)两种,Distribution 用来打正式包,Development 用来打自定义基座包。

      ios 测试手机需要在苹果开发后台添加手机登录的 Apple 账号,且仅限邮箱方式注册的账号,否则无法添加。

    Uniapp

    创建 Uni-app 项目

    根据 文档 操作即可,新建时建议先不选择模板,因为模板后期也可以作为插件导入。这里推荐一个 UI 框架 uView,兼容 Nvue 的 Uniapp 生态框架。

    路由

    • 配置: 路由的开发方式与 Vue 不同,不再是 router,而是参照小程序原生开发规则在 pages.json 中进行 配置,注意 path 前面不加"/"。

    • 跳转: 路由的 跳转方式,同样参照了小程序 有 navigator 标签API 两种。

      1. navigator 标签: 推荐使用 有助于 SEO(搜索引擎优化)。

      2. API: 常用跳转方式 uni.navigateTo()uni.redirectTo()uni.switchTab(),即可处理大部分路由情况。

      需注意:

      • tabBar 页面 仅能通过 uni.switchTab方法进行跳转。

      • 如需求特殊可以自定义开发 tabBar,即 pages.json 中不要设置 tabBar,这样也就不需要使用 uni.switchTab 了。

      • url 前面需要加"/"

    • 问题点: 小程序页面栈最多 10 层。也就是说使用 uni.navigateTo 最多只能跳转 9 层页面。

      解决: 这里不推荐直接使用 uni.redirectTo 取代来处理,会影响用户体验,除非产品设计如此。建议在会出现同页面跳转的页面(例:产品详情 → 点击底部更多产品 → 产品详情 →...),封装一下页面跳转方法,使用 getCurrentPages() 方法获取当前页面栈的列表,根据列表长度去判断使用什么路由方法。路由方法的选择根据实际情况决定 官方文档

      //页面跳转
      toPage(url){
       let pages=getCurrentPages()
       if(pages.length<9){
         uni.navigateTo({url})
      }else{
         uni.redirectTo({url})//根据实际情况选择路由方法
      }
      }

    分包加载

    提前规划好分包,使代码文件更加规整,全局思路更加清晰。可以根据业务流程或者业务类型来设计分包。官方文档

    • 分包加载的使用场景:

      1. 主包大小超过 2m。

      2. 访问落地页启动较慢(因为需要下载整个主包)。

    • 分包优化:

      除页面可以分包配置,静态文件、js 也可以配置分包。可以进一步优化落地页加载速度。

      manifest.json对应平台下配置 "optimization":{"subPackages":true} 来开启分包优化。开启后分包目录下可以放置 static 内容。

      //manifest.json源码
      {
      ...,
         "mp-weixin" : {//这里以微信为例,如有其他平台需要分别添加
          ...,
             "optimization" : {
                 "subPackages" : true
            }
        }
      }
    • 分包预载

      通过分包进入落地页后,可能会有跳转其他分包页面的需求。开启分包预载,在落地页分包数据加载完后,提前加载后续分包页面,详见 官方文档

    生命周期

    • Uniapp 的页面生命周期建议使用 onLoadonShowonReadyonHide 等,也可以使用 vue 生命周期 createdmounted 等,但是组件的生命周期仅支持vue 生命周期的写法。

    easycom 组件模式

    • 说明: 只要组件安装在项目的 components 目录下或 uni_modules 目录下,并符合 components/组件名称/组件名称.vue 的目录结构,就可以不用引用、注册,直接在页面中使用。

      easycom 为默认开启状态,可关闭。可以根据需求配置其他路径规则。详见 官方文档

    • 代码举例:

      非 easycom 模式

      <template>
       <view>
         <goods-list>goods-list>
       view>
      template>
      <script>
      import goodsList from '@/component/goods-list'; //引用组件
      export default {
       components: {
         goodsList //注册组件
      }
      };
      script>

      使用 easycom 模式

      <template>
       <view>
         
         <goods-list>goods-list>
       view>
      template>
      <script>
      export default {};
      script>

    是否使用 Nvue

    • Nvue 开发

      • 优点:原生渲染,性能优势明显(性能优势主要体现在长列表)、启用纯原生渲染模式( manifest 里设置 app-plus 下的 renderer:"native" ) 可进一步减少打包体积(去除了小程序 webview 渲染相关模块)

      • 缺点:与 Vue 开发存在 差异,上手难度相对较高。并且设备兼容性问题较多。

      • 使用:适合仅开发 APP,并且项目对性能有较高要求、组件有复杂层级需求的情况下使用。

    • Nvue+vue 混合开发

      • 优点:性能与开发难度折中的选择,即大部分页面使用 Vue 开发,部分有性能要求的页面用 Nvue 开发。

      • 缺点:同 Nvue 开发。并且当应用没有长列表时,与 Vue 开发相比性能提升不明显。

      • 使用:适合需要同时开发 APP+小程序或 H5,并且项目有长列表的情况下使用。

    • Vue 开发

      • 优点:直接使用 Vue 语法进行开发,所有开发平台皆可兼容。

      • 缺点:在 APP 平台,使用 webview 渲染,性能比较 Nvue 相对差。

      • 使用:适合除需要 Nvue 开发外的所有情况。如果 APP 没有性能要求可使用 vue 一锅端。

    跨域

    • 如需开发 H5 版本,本地调试会碰到跨域问题。

    • 3 种解决方案:

      1. 使用 HbuilderX 内置浏览器预览。内置浏览器经过处理,不存在跨域问题。

      2. manifest.json 中配置,然后在封装的接口中判断 url

        // manifest.json
        {
         "h5": {
           "devServer": {
             "proxy": {
               "/api": {
                 "target": "https://***.***.com",
                 "pathRewrite": {
                   "^/api": ""
                }
              }
            }
          }
        }
        }
        //判断当前是否生产环境
        let url = (process.env.NODE_ENV == 'production' ? baseUrl : '/api') + api;
      3. 创建一个 vue.config.js 文件,并在里面配置 devServer

        // vue.config.js
        module.exports = {
         devServer: {
           proxy: {
             '/api': {
               target: 'https://***.***.com',
               pathRewrite: {
                 '^/api': ''
              }
            }
          }
        }
        };

        如果 2、3 方法同时使用,2 会覆盖 3。

    一键登录

    • 5+APP 一键登录,顾名思义:使用了 HTML5+规范、仅 APP 能用。官方指南

    • 小程序、H5 没有 HTML5+扩展规范。小程序可以使用

    推送

    既然在 uniapp 生态,就直接使用 UniPush 推送服务。

    • 该服务由个推提供,但必须向 DCloud 重新申请账号,不能用个推账号。

    开发中

    CSS

    • 建议使用 flex 布局开发。因为 flex 布局更灵活高效,且便于适配 Nvue(Nvue 仅支持 flex 布局)。

    • 小程序 css 中 background 背景图不支持本地路径。解决办法改为网络路径或 base64。

    • 图片设置 display:block。否则图片下方会有 3px 的空隙,会影响 UI 效果。

    • 多行文字需要限制行数溢出隐藏时,Nvue 和非 Nvue 写法不同。

      Nvue 写法

      .text {
       lines: 2; //行数
       text-overflow: ellipsis;
       word-wrap: break-word;
      }

      非 Nvue 写法

      .text {
      display: -webkit-box;
      -webkit-line-clamp: 2; //行数
      -webkit-box-orient: vertical;
      overflow: hidden;
      text-overflow: ellipsis;
      }

    图片

    mode

    • Uniapp 的 与传统 web 开发中的 相比多了一个 mode 属性,用来设置图片的裁剪、缩放模式。

    • 在开发中尽量养成每一个 都设置 mode 的习惯。可以规避掉很多 UI 显示异常的问题

    • 一般只需要使用 widthFixaspectFill 这两个属性即可应对绝大多数情况。

      即只需设置宽度自动撑起高度的图片用 widthFix ;需要固定尺寸设置宽高,并保持图片不被拉伸的图片用 aspectFill

      例如:所有 icon、文章详情里、产品详情里的详情图一般会用 widthFix,用户头像、缩略图一般会用 aspectFill

      属性详情见 官方文档

    lazy-load

    • 图片懒加载,小程序支持,只针对 page 与 scroll-view 下的 image 有效。

    图片压缩

    • 静态图片未压缩。该问题不限于 Uniapp 开发,也包括其他开发方式。是非常常见的问题。

    • 图片压缩前后,包体大小可差距 50%甚至更多。对编译和加载速度提升显著!

    • 此处放上两个 在线压缩工具 自行取用:Tinypngiloveimg

    滚动穿透

    • 弹窗遮罩显示时,底层页面仍可滚动。给遮罩最外层 view 增加事件 @touchmove.stop.prevent

    底部安全区

    • 问题: iOS 全面屏设备的屏幕底部有黑色横条显示,会对 UI 造成遮挡,影响事件点击和视觉效果。Android 没有横条,不受影响。

    • 场景: 各页面底部悬浮菜单、相对于底部距离固定的悬浮按钮、长列表的最后一个内容。

    • 解决方案:

      • 使用 css 样式 constant(safe-area-inset-bottom) env(safe-area-inset-bottom) 来处理,兼容 iOS11.2+,根据 iOS 系统版本占比,可放心使用。需注意该方法小程序模拟器不支持,真机正常。


      • 如果使用 nvue,则不支持以上方案。可使用 HTML5+规范 的方法来处理。


    交互反馈

    移动端比 PC 画面小很多,但是要展示的内容并不少,甚至更多。为了让用户正常使用,并获得优良体验。交互反馈的设置是必不可少的。并且在 UI 设计评审时就应该确定好,所有交互反馈是否齐全。

    • 缺省样式: 所有数量可能为空的数据展示,都应添加缺省样式,乃至缺省样式后的后续引导。

      例如:评论区没有评论,不应显示空白,而是显示(具体按 UI 设计):一个 message 的 icon,下方跟一句"快来发表你的高见",下方再跟一个发表按钮。这样不仅体现了评论区的状态,还做了评论的引导,增加了互动概率。

    • 状态提醒: 所有需要时间相应的状态变化,或者逻辑变化。都应对用户提供状态提醒。同样需要在 UI 设计评审时确认。

      例如:无网络时,显示网络异常,点击重试。各种等待、 下拉刷新、上拉加载、上传、下载、提交成功、失败、内容未加载完成时的骨架屏。甚至可以在点赞时加一个 vibrateShort 等等。

    分享

    除非特别要求不分享,或者订单等特殊页面。否则在开发时各个页面中一定要有设置分享的习惯。可以使应用的功能更完整更合理并且有助于搜索引擎优化。是必须考虑但又容易忽略的地方。

    • 在页面的生命周期中添加 onShareAppMessage 并配置其参数,否则点击右上角三个点,分享相关按钮是不可点击状态。

    • 小程序可以通过右上角胶囊按钮或者页面中

    • 代码示例:


    • return 的 Object 中 imageUrl 必须为宽高比例 5:4 的图片,并且图片大小尽量小于 20K。imageUrl 可不填,会自动截取当前页面画面。

    • 另外 button 有默认样式,需要清除一下。


    获取用户手机号

    • 小程序通过点击 button 获取 code 来跟后端换取手机号。在开发者工具中无法获取到 code。真机预览中可以获取到。


    苹果登录

    • APP 苹果登录需要使用自定义基座打包才能获得 Apple 的登录信息进行测试

    • iOS 自定义基座打包需要用开发(Development)版的证书和描述文件

    H5 唤起 App

    两种实现方式:

    1. URL Sheme

      优点:配置简单

      缺点:会弹窗询问“是否打开***”,未安装时网页没有回调,而且会弹窗“打不开网页,因为网址无效”;微信微博 QQ 等应用中被禁用,用户体验一般。

    2. Universal Link

      优点:没有额外弹窗,体验更优。

      缺点:配置门槛更高,需要一个不同于 H5 域名的 https 域名(跨域才出发 UL);iOS9 以上有效,iOS9 一下还是要用 URL Sheme 来解决;未安装 App 时会跳转到 404 需要单独处理。

    打包发布

    摇树优化

    • H5 打包时去除未引用的组件、API。

    • 摇树优化(treeShaking)

      //manifest.json
      "h5" : {
      "optimization":{
      "treeShaking":{
      "enable":true //启用摇树优化
      }
      }
      }

    启动图标

    让 UI 帮忙切一个符合以下标准的图片,在 APP 图标配置中自动生成即可。

    • 格式为 png

    • UI 切图时不要带圆角

    • 分辨率不小于 1024×1024

    启动图

    • 如没有特殊要求,直接使用通用启动页面即可。

    • 如需自定义启动图:

      • Android 可直接使用普通 png,也可配置.9.png,可减少包体积,避免缩放影响清晰度。为了更好的效果和体验建议使用.9 图。

        如何制作.9.png?使用 Android studio、ps。或者找 UI 同事帮忙

      • iOS 需要制作storyboard,如所需效果与 uni 提供的 storyboard 模板类似,可直接使用模板修改代码即可(xml 格式)。否则需要使用 xcode 进行更深度的修改,以实现效果并适配各机型。

    权限配置

    HBuilderX 默认会勾选一些不需要的权限,为避免审核打回,需要注意以下权限配置

    • manifest.json 中的【App 权限配置】取消勾选“Android 自动添加第三方 SDK 需要的权限”,然后在下方配置处根据参考文档取消勾选没有用到的权限,额外注意核对推送、分享等功能的权限需求。

    • manifest.json 中的【App 模块配置】仅勾选所需模块(容易漏掉,也会影响权限)

    补充

    SEO(搜索引擎优化)

    用户流量是衡量产品的重要指标之一,受到很多方面影响,SEO 就是其中之一。在没有额外推广的情况下,搜索引擎带来的流量基本就是产品流量的主要来源。传统 web 开发通过设置 TDK、sitemap 等,现阶段移动开发方法有所变化,但是万变不离其宗,核心还是一样的。

    • 小程序:

      • 被动方式:

        1. 确保 URL 可直接打开,通俗说就是 url 要有效,不能是 404。

        2. 页面跳转优先采用 navigator 组件

        3. 清晰简洁的页面参数

        4. 必要的时候才请求用户进行授权、登录、绑定手机号等

        5. 不收录 web-view,若非不需 seo 内容(用户协议之类)、或已有 H5 页面节省开发,否则尽量不要用 web-view。

        6. 配置sitemap

        7. 设置标题和分享缩略图 类似于传统 web 中设置 TDK。在百度小程序中有专门的接口来传递 SEO 信息。

      • 主动方式:

        1. 使用页面路径推送能力让微信收录内容

      内容详情请查看 优化指南。所有被动方式可以作为开发习惯来养成。

    • H5: 因为 Uniapp 是基于 Vue 语法来开发,这种 SPA 对于 SEO 并不友好。业界有 SSR(服务端渲染) 方法,等了很久 Uniapp 官方也终于提供了 SSR 的方法,但是需要使用 uniCloud。所以如果没有使用 uniCloud,暂时没有更合适的方法来处理该问题。

    • APP: 方式脱离前端范畴,不做讨论。

    作者:Tigger
    来源:juejin.cn/post/7138221718518595621

    收起阅读 »

    uniapp项目优化方式及建议

    1.复杂页面数据区域封装成组件例如项目里包含类似论坛页面:点击一个点赞图标,赞数要立即+1,会引发页面级所有的数据从js层向视图层的同步,造成整个页面的数据更新,造成点击延迟卡顿对于复杂页面,更新某个区域的数据时,需要把这个区域做成组件,这样更新数据时就只更新...
    继续阅读 »

    介绍:性能优化自古以来就是重中之重,关于uniapp项目优化方式最全整理,会根据开发情况进行补充

    1.复杂页面数据区域封装成组件

    场景

    例如项目里包含类似论坛页面:点击一个点赞图标,赞数要立即+1,会引发页面级所有的数据从js层向视图层的同步,造成整个页面的数据更新,造成点击延迟卡顿

    优化方案

    对于复杂页面,更新某个区域的数据时,需要把这个区域做成组件,这样更新数据时就只更新这个组件

    注:app-nvue和h5不存在此问题;造成差异的原因是小程序目前只提供了组件差量更新的机制,不能自动计算所有页面差量

    2.避免使用大图

    场景

    页面中若大量使用大图资源,会造成页面切换的卡顿,导致系统内存升高,甚至白屏崩溃;对大体积的二进制文件进行 base64 ,也非常耗费资源

    优化方案

    图片请压缩后使用,避免大图,必要时可以考虑雪碧图或svg,简单代码能实现的就不要图片

    3.小程序、APP分包处理pages过多

    前往官网手册查看配置

    4.图片懒加载

    功能描述

    此功能只对微信小程序、App、百度小程序、字节跳动小程序有效,默认开启

    前往uView手册查看配置

    5.禁止滥用本地存储

    不要滥用本地存储,局部页面之间的传参用url,如果用本地存储传递数据要命名规范和按需销毁

    6.可在外部定义变量

    在 uni-app 中,定义在 data 里面的数据每次变化时都会通知视图层重新渲染页面;所以如果不是视图所需要的变量,可以不定义在 data 中,可在外部定义变量或直接挂载在 vue实例 上,以避免造成资源浪费

    7.分批加载数据优化页面渲染

    场景

    页面初始化时,逻辑层一次性向视图层传递很大的数据,使视图层一次性渲染大量节点,可能造成通讯变慢、页面切换卡顿

    优化方案

    以局部更新页面的方式渲染页面;如:服务端返回 100条数据 ,可进行分批加载,一次加载 50条 , 500ms 后进行下一次加载

    8.避免视图层和逻辑层频繁进行通讯

    1. 减少 scroll-view 组件的 scroll 事件监听,当监听 scroll-view 的滚动事件时,视图层会频繁的向逻辑层发送数据

    2. 监听 scroll-view 组件的滚动事件时,不要实时的改变 scroll-top / scroll-left 属性,因为监听滚动时,视图层向逻辑层通讯,改变 scroll-top / scroll-left 时,逻辑层又向视图层通讯,这样就可能造成通讯卡顿

    3. 注意 onPageScroll 的使用, onPageScroll 进行监听时,视图层会频繁的向逻辑层发送数据

    4. 多使用 css动画 ,而不是通过js的定时器操作界面做动画

    5. 如需在 canvas 里做跟手操作, app端 建议使用 renderjs ,小程序端建议使用 web-view 组件; web-view 里的页面没有逻辑层和视图层分离的概念,自然也不会有通信折损

    9.CSS优化

    要知道哪些属性是有继承效果的,像字体、字体颜色、文字大小都是继承的,禁止没有意义的重复代码

    10.善用节流和防抖

    防抖

    等待n秒后执行某函数,若等待期间再次被触发,则等待时间重新初始化

    节流

    触发事件n秒内只执行一次,n秒未过,再次触发无效

    11.优化页面切换动画

    场景

    页面初始化时存在大量图片或原生组件渲染和大量数据通讯,会发生新页面渲染和窗体进入动画抢资源,造成页面切换卡顿、掉帧

    优化方案

    1. 建议延时 100ms~300ms 渲染图片或复杂原生组件,分批进行数据通讯,以减少一次性渲染的节点数量

    2. App 端动画效果可以自定义; popin/popout 的双窗体联动挤压动画效果对资源的消耗更大,如果动画期间页面里在执行耗时的js,可能会造成动画掉帧;此时可以使用消耗资源更小的动画效果,比如 slide-in-right / slide-out-right

    3. App-nvue 和 H5 ,还支持页面预载,uni.preloadPage,可以提供更好的使用体验

    12.优化背景色闪白

    场景

    进入新页面时背景闪白,如果页面背景是深色,在vue页面中可能会发生新窗体刚开始动画时是灰白色背景,动画结束时才变为深色背景,造成闪屏

    优化方案

    1. 将样式写在 App.vue 里,可以加速页面样式渲染速度; App.vue 里面的样式是全局样式,每次新开页面会优先加载 App.vue 里面的样式,然后加载普通 vue 页面的样式

    2. app端 还可以在 pages.json 的页面的 style 里单独配置页面原生背景色,比如在 globalStyle->style->app-plus->background 下配置全局背景色

    "style": { "app-plus": { "background":"#000000" } }
    1. nvue页面不存在此问题,也可以更改为nvue页面

    13.优化启动速度

    1. 工程代码越多,包括背景图和本地字体文件越大,对小程序启动速度有影响,应注意控制体积

    2. App端的 splash 关闭有白屏检测机制,如果首页一直白屏或首页本身就是一个空的中转页面,可能会造成 splash 10秒才关闭

    3. App端使用v3编译器,首页为 nvue页面 时,并设置为fast启动模式,此时App启动速度最快

    4. App设置为纯 nvue项目 (manifest里设置app-plus下的renderer:"native"),这种项目的启动速度更快,2秒即可完成启动;因为它整个应用都使用原生渲染,不加载基于webview的那套框架

    14.优化包体积

    1. uni-app 发行到小程序时,如果使用了 es6 转 es5 、css 对齐的功能,可能会增大代码体积,可以配置这些编译功能是否开启

    2. uni-app 的 H5端,uni-app 提供了摇树优化机制,未摇树优化前的 uni-app 整体包体积约 500k,服务器部署 gzip 后162k。开启摇树优化需在manifest配置

    3. uni-app 的 App端,Android 基础引擎约 9M ,App 还提供了扩展模块,比如地图、蓝牙等,打包时如不需要这些模块,可以裁剪掉,以缩小发行包;体积在 manifest.json-App 模块权限里可以选择

    4. App端支持如果选择纯nvue项目 (manifest里设置app-plus下的renderer:"native"),包体积可以进一步减少2M左右

    5. App端在 HBuilderX 2.7 后,App 端下掉了 非v3 的编译模式,包体积下降了3M

    15.禁止滥用外部js插件

    描述

    有官方API的就不要额外引用js插件增加项目体积

    例如

    url传参加密直接用 encodeURIComponent() 和 decodeURIComponent()

    作者:Panda_HYC
    来源:juejin.cn/post/6997224351346982942

    收起阅读 »

    搞不定移动端性能,全球爆火的 Notion 从 Hybrid 转向了 Native

    7 月 20 日,Notion 笔记程序发布了版本更新,并表示更改了移动设备上的技术栈,将从 webview 逐步切换到本机应用程序,以获得更快更流畅的性能。该团队声称该应用程序现在在 iOS 上的启动速度提高了 2 倍,在 Android 上的启动速度提高了...
    继续阅读 »

    7 月 20 日,Notion 笔记程序发布了版本更新,并表示更改了移动设备上的技术栈,将从 webview 逐步切换到本机应用程序,以获得更快更流畅的性能。

    该团队声称该应用程序现在在 iOS 上的启动速度提高了 2 倍,在 Android 上的启动速度提高了 3 倍。


    Notion 发布的这条 Twitter 也得到了广泛的关注,几天之内就有了上千条转发。由于前几年 Notion 的技术栈一直没有公开,开发者对此充满了各种猜测,很多人认为 Notion 使用的是 React Native 或 Electron,因此这次 Notion 宣称切换为原生 iOS 和原生 Android,再一次引发了“框架之争”。

    其中有不少人发表了“贬低”跨平台开发的看法,对 React Native 等框架产生了质疑,毕竟现在向跨平台过渡是不可避免的,这些框架是对原生工具包的一个“威胁”,而 Notion 恰恰又切换到了“原生”开发模式。

    实际上,在 2020 年之前 Notion 使用的是 React Native,随后切换到了 Hybrid 混合开发模式:使用 Kotlin/Swift + 运行网络应用程序的 Web 视图。但移动端的性能一直是一个问题,2 年之后,Notion 再次切换到了原生开发模式。

    有网友认为,像 Notion 这样重 UI 和交互的产品,如果不知道如何掌握 Web 技术,那么对他们的产出速度表示担忧。面对这种吵翻天的状况,Notion 的前端工程师也因此再度出面回应这次切换的原因和一些思考。

    Notion 的发展和理念

    Notion 是一款将笔记、知识库和任务管理无缝衔接整合的多人协作平台。Notion 打破了传统的笔记软件对于内容的组合方式,将文档的内容分成一个个 Block,并且能够以乐高式的方式拖动组合这些 Block,让它使用起来十分灵活。

    Notion 由 Ivan Zhao、Simon Last 于 2013 年在旧金山创立。去年底,Notion 获得了 2.75 亿美元的 C 轮融资。截至 2021 年 10 月,Notion 估值 103 亿美元,在全球拥有超 2000 万用户。Notion 的创始人和 CEO Ivan Zhao 是一位 80 后华人。他出生于中国新疆,曾就读于清华附中,中学时随家人移居加拿大,现在被很多人认为将成为硅谷的下一个袁征(Zoom 的创始人)。Ivan 在大学时期主修认知科学,学习的是人的大脑怎么运作,外加对计算机也很感兴趣。



    Ivan 也曾表示“我的很多朋友都是艺术家。我是他们中唯一会编码的人。我想开发一款软件,它不仅可以为人们提供文档或网页。” 因此,在 2012 年大学毕业后,在文档共享初创公司 Inkling 工作期间,他创办了 Notion。原本的目标是构建一个无代码应用构建工具,不过项目很快失败了。随后 Ivan 与 Simon 迁往了日本京都,待了一年左右,小而安静的地方能“让我们专注在写代码”,在相对无压力和与世隔绝的环境下,构思并设计出了现在的 Notion 原型。用 Reddit 论坛上的一条获得高赞的网友总结就是:一个 Notion = Google docs + Evernote + Trello + Confluence + Github + Wiki +……

    “工具应该模仿人脑的工作方式。但由于每个人的思维和工作方式都不同,这意味着工具需要非常灵活。”Ivan 解释道。而 Notion 创建的目的,就是将用户从一堆各式各样的生产力工具之中解放出来,给予一个干净清爽、简便易行的 All in One 工作平台。企业用户也可以在 Notion 上基本实现公司的内部管理所需要涉及到的所有功能。包括公司知识库和资料库的创建与管理、项目进度管理、信息共享、工作日志、内部社交、协作办公等等。


    有人甚至说,Notion 堪比办公软件届的苹果。在 2016 年发布 1.0 版本后,因其独特的设计、专注于将事情做得更好、对投资人的冷淡态度,外加疫情远程办公潮,多方面因素让 Notion 迅速火遍全球。作为一款 All in one 的概念型工具,Notion 一直被众多企业抄作业,但它目前几乎未逢敌手。

    Notion 为什么要两次更换技术栈?

    Notion 在 2017 年、2018 年分别发布了 iOS 客户端和 Android 客户端。在发布 2.0 版本之后,该公司于 2019 年以 8 亿美元的估值筹集了 1000 万美元的资金。但也许和创始人的发展理念相关,Notion 的员工数量一直不多。

    2019 年 3 月的时候,工程团队总共才 4 个人,当时 Notion 用 React Native 来渲染 web 视图。Notion 在 Twitter 上解释说,这是为了更快地部署新功能和进行一些其他修复。

    但如果这个系统适合开发者,那么它对用户来说远非最佳:许多人抱怨移动版本非常缓慢。“即使是新 iPhone 也非常慢 - 大约 6-7 秒后我才能开始输入笔记。到那时我都快忘记了我之前想写什么。它基本上是一个非常重的 web 应用程序视图。”“如果 Notion 不选择改变,那么它将迅速被其它同类产品取代。”......



    2020 年,Notion 第一次因这个问题,更改了技术栈,放弃 React Native,切换到了 Hybrid 开发环境。

    Notion 前端负责人 Jake Teton‑Landis 表示,“React Native 的优势在于允许 Web 开发人员构建手机应用程序。如果我们已经有了 webview,那么 React Native 不会增加价值。对我们来说,它让一切变得更加困难:性能、代码复杂性、招聘等等。用 React Native 快速完成任务的同时,也在跟复杂性战斗,这让我们感觉束手束脚。”

    虽然这次移动端的性能有了一些提升,但也没有根本解决问题,更新之后,Android 端依然是一个相当大的痛点。


    Notion 也曾在 2019 年的时候表示不会很快发布本机应用程序,但他们同时强调“原生开发也是一个选择”。

    7 月 20 日,Notion 发布了版本更新,并表示将从主页选项卡开始,从 webview 逐步一个个地切换到本机应用程序。

    此时 Notion 工程团队也大约只有 100 人, 总共包含 3 位 iOS 工程师、4 位 android 工程师,除主页使用 SwiftUI/Jetpack Compose 进行渲染,其他部分仍然是 webview 进行绘制。

    “似乎这还是招聘不足产生的人员问题。”Jake 解释说,“我们的策略是随着团队的壮大逐步本地化我们应用程序的更多部分。我们这个程序必须使用本机性能,如果它是原生的,则更容易达到这个性能要求。

    凭借我们拥有的经验,以及对问题的了解,我们因此选择了原生 iOS 和原生 Android 开发。虽然出于复杂性的权衡,在可预见的未来,编辑器可能仍然是一个 webview,毕竟 Google Docs、Quip、Dropbox Paper、Coda 都使用原生 shell、webview 编辑器。”

    原生开发才是王道?!

    虽然无论是原生开发还是 Hybrid 都可以完成工作,但原生应用程序是按照操作系统技术和用户体验准则开发的,因此具有更快的性能优势,并能轻松访问和利用用户设备的内置功能(例如,GPS、地址簿、相机等)。

    Hybrid 开发方式,通常是在面对市场竞争需要尽快构建并发布应用程序时候的一种选择。如果期望的发布时间少于六个月,那么混合可能是一个更好的选择,因为可以构建一套源代码,跨平台发布,与原生开发相比,其开发时间和工作量要少得多,但这也意味着需要做出许多性能和功能上的妥协。

    如果有足够时间,那么原生方法最有意义,可以让应用程序具有最佳性能、最高安全性和最佳用户体验。毕竟,用户体验是应用程序成功的关键。互联网正在放缓,人们使用手机的时间越来越长,缓慢的应用程序意味着糟糕的业务。在这种情况下,对 Notion 来说,拥有一个快速应用程序比以往任何时候都更加重要。

    参考链接:

    https://www.notion.so/releases/2022-07-20

    https://twitter.com/jitl/status/1530326516013342723?s=20&t=xT0gfWhFvs0yNvc1GQ3sTQ

    收起阅读 »

    我对 React 实现原理的理解

    React 是前端开发每天都用的前端框架,自然要深入掌握它的原理。我用 React 也挺久了,这篇文章就来总结一下我对 react 原理的理解。react 和 vue 都是基于 vdom 的前端框架,我们先聊下 vdom:vdom为什么 react 和 vue...
    继续阅读 »

    React 是前端开发每天都用的前端框架,自然要深入掌握它的原理。我用 React 也挺久了,这篇文章就来总结一下我对 react 原理的理解。

    react 和 vue 都是基于 vdom 的前端框架,我们先聊下 vdom:

    vdom

    为什么 react 和 vue 都要基于 vdom 呢?直接操作真实 dom 不行么?

    考虑下这样的场景:

    渲染就是用 dom api 对真实 dom 做增删改,如果已经渲染了一个 dom,后来要更新,那就要遍历它所有的属性,重新设置,比如 id、clasName、onclick 等。

    而 dom 的属性是很多的:


    有很多属性根本用不到,但在更新时却要跟着重新设置一遍。

    能不能只对比我们关心的属性呢?

    把这些单独摘出来用 JS 对象表示不就行了?

    这就是为什么要有 vdom,是它的第一个好处。

    而且有了 vdom 之后,就没有和 dom 强绑定了,可以渲染到别的平台,比如 native、canvas 等等。

    这是 vdom 的第二个好处。

    我们知道了 vdom 就是用 JS 对象表示最终渲染的 dom 的,比如:

    {
       type'div',
       props: {
           id'aaa',
           className: ['bbb''ccc'],
           onClickfunction() {}
      },
       children: []
    }

    然后用渲染器把它渲染出来。

    但是要让开发去写这样的 vdom 么?

    那肯定不行,这样太麻烦了,大家熟悉的是 html 那种方式,所以我们要引入编译的手段。

    dsl 的编译

    dsl 是 domain specific language,领域特定语言的意思,html、css 都是 web 领域的 dsl。

    直接写 vdom 太麻烦了,所以前端框架都会设计一套 dsl,然后编译成 render function,执行后产生 vdom。

    vue 和 react 都是这样:


    这套 dsl 怎么设计呢?

    前端领域大家熟悉的描述 dom 的方式是 html,最好的方式自然是也设计成那样。

    所以 vue 的 template,react 的 jsx 就都是这么设计的。

    vue 的 template compiler 是自己实现的,而 react 的 jsx 的编译器是 babel 实现的,是两个团队合作的结果。

    比如我们可以这样写:


    编译成 render function 后再执行就是我们需要的 vdom。

    接下来渲染器把它渲染出来就行了。

    那渲染器怎么渲染 vdom 的呢?

    渲染 vdom

    渲染 vdom 也就是通过 dom api 增删改 dom。

    比如一个 div,那就要 document.createElement 创建元素,然后 setAttribute 设置属性,addEventListener 设置事件监听器。

    如果是文本,那就要 document.createTextNode 来创建。

    所以说根据 vdom 类型的不同,写个 if else,分别做不同的处理就行了。

    没错,不管 vue 还是 react,渲染器里这段 if else 是少不了的:

    switch (vdom.tag) {
     case HostComponent:
       // 创建或更新 dom
     case HostText:
       // 创建或更新 dom
     case FunctionComponent
       // 创建或更新 dom
     case ClassComponent
       // 创建或更新 dom
    }

    react 里是通过 tag 来区分 vdom 类型的,比如 HostComponent 就是元素,HostText 就是文本,FunctionComponent、ClassComponent 就分别是函数组件和类组件。

    那么问题来了,组件怎么渲染呢?

    这就涉及到组件的原理了:

    组件

    我们的目标是通过 vdom 描述界面,在 react 里会使用 jsx。

    这样的 jsx 有的时候是基于 state 来动态生成的。如何把 state 和 jsx 关联起来呢?

    封装成 function、class 或者 option 对象的形式。然后在渲染的时候执行它们拿到 vdom 就行了。

    这就是组件的实现原理:

    switch (vdom.tag) {
     case FunctionComponent
          const childVdom = vdom.type(props);
          
          render(childVdom);
          //...
     case ClassComponent
        const instance = new vdom.type(props);
        const childVdom = instance.render();
        
        render(childVdom);
        //...
    }

    如果是函数组件,那就传入 props 执行它,拿到 vdom 之后再递归渲染。

    如果是 class 组件,那就创建它的实例对象,调用 render 方法拿到 vdom,然后递归渲染。

    所以,大家猜到 vue 的 option 对象的组件描述方式怎么渲染了么?

    {
       data: {},
       props: {}
       render(h) {
           return h('div', {}, '');
      }
    }

    没错,就是执行下 render 方法就行:

    const childVdom = option.render();

    render(childVdom);

    大家可能平时会写单文件组件 sfc 的形式,那个会有专门的编译器,把 template 编译成 render function,然后挂到 option 对象的 render 方法上:


    所以组件本质上只是对产生 vdom 的逻辑的封装,函数的形式、option 对象的形式、class 的形式都可以。

    就像 vue3 也有了函数组件一样,组件的形式并不重要。

    基于 vdom 的前端框架渲染流程都差不多,vue 和 react 很多方面是一样的。但是管理状态的方式不一样,vue 有响应式,而 react 则是 setState 的 api 的方式。

    真说起来,vue 和 react 最大的区别就是状态管理方式的区别,因为这个区别导致了后面架构演变方向的不同。

    状态管理

    react 是通过 setState 的 api 触发状态更新的,更新以后就重新渲染整个 vdom。

    而 vue 是通过对状态做代理,get 的时候收集以来,然后修改状态的时候就可以触发对应组件的 render 了。

    有的同学可能会问,为什么 react 不直接渲染对应组件呢?

    想象一下这个场景:

    父组件把它的 setState 函数传递给子组件,子组件调用了它。

    这时候更新是子组件触发的,但是要渲染的就只有那个组件么?

    明显不是,还有它的父组件。

    同理,某个组件更新实际上可能触发任意位置的其他组件更新的。

    所以必须重新渲染整个 vdom 才行。

    那 vue 为啥可以做到精准的更新变化的组件呢?

    因为响应式的代理呀,不管是子组件、父组件、还是其他位置的组件,只要用到了对应的状态,那就会被作为依赖收集起来,状态变化的时候就可以触发它们的 render,不管是组件是在哪里的。

    这就是为什么 react 需要重新渲染整个 vdom,而 vue 不用。

    这个问题也导致了后来两者架构上逐渐有了差异。

    react 架构的演变

    react15 的时候,和 vue 的渲染流程还是很像的,都是递归渲染 vdom,增删改 dom 就行。

    但是因为状态管理方式的差异逐渐导致了架构的差异。

    react 的 setState 会渲染整个 vdom,而一个应用的所有 vdom 可能是很庞大的,计算量就可能很大。

    浏览器里 js 计算时间太长是会阻塞渲染的,会占用每一帧的动画、重绘重排的时间,这样动画就会卡顿。

    作为一个有追求的前端框架,动画卡顿肯定是不行的。但是因为 setState 的方式只能渲染整个 vdom,所以计算量大是不可避免的。

    那能不能把计算量拆分一下,每一帧计算一部分,不要阻塞动画的渲染呢?

    顺着这个思路,react 就改造为了 fiber 架构。

    fiber 架构

    优化的目标是打断计算,分多次进行,但现在递归的渲染是不能打断的,有两个方面的原因导致的:

    • 渲染的时候直接就操作了 dom 了,这时候打断了,那已经更新到 dom 的那部分怎么办?

    • 现在是直接渲染的 vdom,而 vdom 里只有 children 的信息,如果打断了,怎么找到它的父节点呢?

    第一个问题的解决还是容易想到的:

    渲染的时候不要直接更新到 dom 了,只找到变化的部分,打个增删改的标记,创建好 dom,等全部计算完了一次性更新到 dom 就好了。

    所以 react 把渲染流程分为了两部分: render 和 commit。

    render 阶段会找到 vdom 中变化的部分,创建 dom,打上增删改的标记,这个叫做 reconcile,调和。

    reconcile 是可以打断的,由 schedule 调度。

    之后全部计算完了,就一次性更新到 dom,叫做 commit。

    这样,react 就把之前的和 vue 很像的递归渲染,改造成了 render(reconcile + schdule) + commit 两个阶段的渲染。

    从此以后,react 和 vue 架构上的差异才大了起来。

    第二个问题,如何打断以后还能找到父节点、其他兄弟节点呢?

    现有的 vdom 是不行的,需要再记录下 parent、silbing 的信息。所以 react 创造了 fiber 的数据结构。


    除了 children 信息外,额外多了 sibling、return,分别记录着兄弟节点、父节点的信息。

    这个数据结构也叫做 fiber。(fiber 既是一种数据结构,也代表 render + commit 的渲染流程)

    react 会先把 vdom 转换成 fiber,再去进行 reconcile,这样就是可打断的了。

    为什么这样就可以打断了呢?

    因为现在不再是递归,而是循环了:

    function workLoop() {
     while (wip) {
       performUnitOfWork();
    }

     if (!wip && wipRoot) {
       commitRoot();
    }
    }

    react 里有一个 workLoop 循环,每次循环做一个 fiber 的 reconcile,当前处理的 fiber 会放在 workInProgress 这个全局变量上。

    当循环完了,也就是 wip 为空了,那就执行 commit 阶段,把 reconcile 的结果更新到 dom。

    每个 fiber 的 reconcile 是根据类型来做的不同处理。当处理完了当前 fiber 节点,就把 wip 指向 sibling、return 来切到下个 fiber 节点。:

    function performUnitOfWork() {
     const { tag } = wip;

     switch (tag) {
       case HostComponent:
         updateHostComponent(wip);
         break;

       case FunctionComponent:
         updateFunctionComponent(wip);
         break;

       case ClassComponent:
         updateClassComponent(wip);
         break;
       case Fragment:
         updateFragmentComponent(wip);
         break;
       case HostText:
         updateHostTextComponent(wip);
         break;
       default:
         break;
    }

     if (wip.child) {
       wip = wip.child;
       return;
    }

     let next = wip;

     while (next) {
       if (next.sibling) {
         wip = next.sibling;
         return;
      }
       next = next.return;
    }

     wip = null;
    }

    函数组件和 class 组件的 reconcile 和之前讲的一样,就是调用 render 拿到 vdom,然后继续处理渲染出的 vdom:

    function updateClassComponent(wip) {
     const { typeprops } = wip;
     const instance = new type(props);
     const children = instance.render();

     reconcileChildren(wipchildren);
    }

    function updateFunctionComponent(wip) {
     renderWithHooks(wip);

     const { typeprops } = wip;

     const children = type(props);
     reconcileChildren(wipchildren);
    }

    循环执行 reconcile,那每次处理之前判断一下是不是有更高优先级的任务,就能实现打断了。

    所以我们在每次处理 fiber 节点的 reconcile 之前,都先调用下 shouldYield 方法:

    function workLoop() {
    while (wip && shouldYield()) {
    performUnitOfWork();
    }

    if (!wip && wipRoot) {
    commitRoot();
    }
    }

    shouldYiled 方法就是判断待处理的任务队列有没有优先级更高的任务,有的话就先处理那边的 fiber,这边的先暂停一下。

    这就是 fiber 架构的 reconcile 可以打断的原理。通过 fiber 的数据结构,加上循环处理前每次判断下是否打断来实现的。

    聊完了 render 阶段(reconcile + schedule),接下来就进入 commit 阶段了。

    前面说过,为了变为可打断的,reconcile 阶段并不会真正操作 dom,只会创建 dom 然后打个 effectTag 的增删改标记。

    commit 阶段就根据标记来更新 dom 就可以了。

    但是 commit 阶段要再遍历一次 fiber 来查找有 effectTag 的节点,更新 dom 么?

    这样当然没问题,但没必要。完全可以在 reconcile 的时候把有 effectTag 的节点收集到一个队列里,然后 commit 阶段直接遍历这个队列就行了。

    这个队列叫做 effectList。

    react 会在 commit 阶段遍历 effectList,根据 effectTag 来增删改 dom。

    dom 创建前后就是 useEffect、useLayoutEffect 还有一些函数组件的生命周期函数执行的时候。

    useEffect 被设计成了在 dom 操作前异步调用,useLayoutEffect 是在 dom 操作后同步调用。

    为什么这样呢?

    因为都要操作 dom 了,这时候如果来了个 effect 同步执行,计算量很大,那不是把 fiber 架构带来的优势有毁了么?

    所以 effect 是异步的,不会阻塞渲染。

    而 useLayoutEffect,顾名思义是想在这个阶段拿到一些布局信息的,dom 操作完以后就可以了,而且都渲染完了,自然也就可以同步调用了。

    实际上 react 把 commit 阶段也分成了 3 个小阶段。

    before mutation、mutation、layout。

    mutation 就是遍历 effectList 来更新 dom 的。

    它的之前就是 before mutation,会异步调度 useEffect 的回调函数。

    它之后就是 layout 阶段了,因为这个阶段已经可以拿到布局信息了,会同步调用 useLayoutEffect 的回调函数。而且这个阶段可以拿到新的 dom 节点,还会更新下 ref。

    至此,我们对 react 的新架构,render、commit 两大阶段都干了什么就理清了。

    总结

    react 和 vue 都是基于 vdom 的前端框架,之所以用 vdom 是因为可以精准的对比关心的属性,而且还可以跨平台渲染。

    但是开发不会直接写 vdom,而是通过 jsx 这种接近 html 语法的 DSL,编译产生 render function,执行后产生 vdom。

    vdom 的渲染就是根据不同的类型来用不同的 dom api 来操作 dom。

    渲染组件的时候,如果是函数组件,就执行它拿到 vdom。class 组件就创建实例然后调用 render 方法拿到 vdom。vue 的那种 option 对象的话,就调用 render 方法拿到 vdom。

    组件本质上就是对一段 vdom 产生逻辑的封装,函数、class、option 对象甚至其他形式都可以。

    react 和 vue 最大的区别在状态管理方式上,vue 是通过响应式,react 是通过 setState 的 api。我觉得这个是最大的区别,因为它导致了后面 react 架构的变更。

    react 的 setState 的方式,导致它并不知道哪些组件变了,需要渲染整个 vdom 才行。但是这样计算量又会比较大,会阻塞渲染,导致动画卡顿。

    所以 react 后来改造成了 fiber 架构,目标是可打断的计算。

    为了这个目标,不能变对比变更新 dom 了,所以把渲染分为了 render 和 commit 两个阶段,render 阶段通过 schedule 调度来进行 reconcile,也就是找到变化的部分,创建 dom,打上增删改的 tag,等全部计算完之后,commit 阶段一次性更新到 dom。

    打断之后要找到父节点、兄弟节点,所以 vdom 也被改造成了 fiber 的数据结构,有了 parent、sibling 的信息。

    所以 fiber 既指这种链表的数据结构,又指这个 render、commit 的流程。

    reconcile 阶段每次处理一个 fiber 节点,处理前会判断下 shouldYield,如果有更高优先级的任务,那就先执行别的。

    commit 阶段不用再次遍历 fiber 树,为了优化,react 把有 effectTag 的 fiber 都放到了 effectList 队列中,遍历更新即可。

    在dom 操作前,会异步调用 useEffect 的回调函数,异步是因为不能阻塞渲染。

    在 dom 操作之后,会同步调用 useLayoutEffect 的回调函数,并且更新 ref。

    所以,commit 阶段又分成了 before mutation、mutation、layout 这三个小阶段,就对应上面说的那三部分。

    我觉得理解了 vdom、jsx、组件本质、fiber、render(reconcile + schedule) + commit(before mutation、mutation、layout)的渲染流程,就算是对 react 原理有一个比较深的理解了。


    作者:zxg_神说要有光
    来源:juejin.cn/post/7117051812540055588

    收起阅读 »

    Flutter中的异步

    同步与异步程序的运行是出于满足人们对某种逻辑需求的处理,在计算机上表现为可执行指令,正常情况下我们期望的指令是按逻辑的顺序依次执行的,而实际情况由于某些指令是耗时操作,不能立即返回结果而造成了阻塞,导致程序无法继续执行。这种情况多见于一些io操作。这时,对于用...
    继续阅读 »

    同步与异步

    程序的运行是出于满足人们对某种逻辑需求的处理,在计算机上表现为可执行指令,正常情况下我们期望的指令是按逻辑的顺序依次执行的,而实际情况由于某些指令是耗时操作,不能立即返回结果而造成了阻塞,导致程序无法继续执行。这种情况多见于一些io操作。这时,对于用户层面来说,我们可以选择stop the world,等待操作完成返回结果后再继续操作,也可以选择继续去执行其他操作,等事件返回结果后再通知回来。这就是从用户角度来看的同步与异步。

    从操作系统的角度,同步异步,与任务调度,进程间切换,中断,系统调用之间有着更为复杂的关系。

    同步I/O 与 异步I/O的区别


    为什么使用异步

    用户可以阻塞式的等待,因为人的操作和计算机相比是非常慢的,计算机如果阻塞那就是很大的性能浪费了,异步操作让您的程序在等待另一个操作的同时完成工作。三种异步操作的场景:

    • I/O操作:例如:发起一个网络请求,读写数据库、读写文件、打印文档等,一个同步的程序去执行这些操作,将导致程序的停止,直到操作完成。更有效的程序会改为在操作挂起时去执行其他操作,假设您有一个程序读取一些用户输入,进行一些计算,然后通过电子邮件发送结果。发送电子邮件时,您必须向网络发送一些数据,然后等待接收服务器响应。等待服务器响应所投入的时间是浪费的时间,如果程序继续计算,这将得到更好的利用

    • 并行执行多个操作:当您需要并行执行不同的操作时,例如进行数据库调用、Web 服务调用以及任何计算,那么我们可以使用异步

    • 长时间运行的基于事件驱动的请求:这就是您有一个请求进来的想法,并且该请求进入休眠状态一段时间等待其他一些事件的发生。当该事件发生时,您希望请求继续,然后向客户端发送响应。所以在这种情况下,当请求进来时,线程被分配给该请求,当请求进入睡眠状态时,线程被发送回线程池,当任务完成时,它生成事件并从线程池中选择一个线程发送响应

    计算机中异步的实现方式就是任务调度,也就是进程的切换

    任务调度采用的是时间片轮转的抢占式调度方式,进程是任务调度的最小单位。

    计算机系统分为用户空间内核空间,用户进程在用户空间,操作系统运行在内核空间,内核空间的数据访问修改拥有高于普通进程的权限,用户进程之间相互独立,内存不共享,保证操作系统的运行安全。如何最大化的利用CPU,确定某一时刻哪个进程拥有CPU资源就是任务调度的过程。内核负责调度管理用户进程,以下为进程调度过程


    在任意时刻, 一个 CPU 核心上(processor)只可能运行一个进程

    每一个进程可以包含多个线程,线程是执行操作的最小单元,因此进程的切换落实到具体细节就是正在执行线程的切换

    Future

    Future<T> 表示一个异步的操作结果,用来表示一个延迟的计算,返回一个结果或者error,使用代码实例:

    Future<int> future = getFuture();
    future.then((value) => handleValue(value))
        .catchError((error) => handleError(error))
    .whenComplete(func);

    future可以是三种状态:未完成的返回结果值返回异常

    当一个返回future对象被调用时,会发生两件事:

    • 将函数操作入队列等待执行结果并返回一个未完成的Future对象

    • 函数操作完成时,Future对象变为完成并携带一个值或一个错误

    首先,Flutter事件处理模型为先执行main函数,完成后检查执行微任务队列Microtask Queue中事件,最后执行事件队列Event Queue中的事件,示例:

    void main(){
     Future(() => print(10));
    Future.microtask(() => print(9));
     print("main");
    }
    /// 打印结果为:
    /// main
    /// 9
    /// 10

    基于以上事件模型的基础上,看下Future提供的几种构造函数,其中最基本的为直接传入一个Function

    factory Future(FutureOr<T> computation()) {
       _Future<T> result = new _Future<T>();
       Timer.run(() {
         try {
           result._complete(computation());
        } catch (e, s) {
           _completeWithErrorCallback(result, e, s);
        }
      });
       return result;
    }

    Function有多种写法:

    //简单操作,单步
    Future(() => print(5));
    //稍复杂,匿名函数
    Future((){
     print(6);
    });
    //更多操作,方法名
    Future(printSeven);

    printSeven(){
     print(7);
    }
     

    Future.microtask

    此工程方法创建的事件将发送到微任务队列Microtask Queue,具有相比事件队列Event Queue优先执行的特点

    factory Future.microtask(FutureOr<T> computation()) {
       _Future<T> result = new _Future<T>();
    //
       scheduleMicrotask(() {
         try {
           result._complete(computation());
        } catch (e, s) {
           _completeWithErrorCallback(result, e, s);
        }
      });
       return result;
    }

    Future.sync

    返回一个立即执行传入参数的Future,可理解为同步调用

    factory Future.sync(FutureOr<T> computation()) {
       try {
         var result = computation();
         if (result is Future<T>) {
           return result;
        } else {
           // TODO(40014): Remove cast when type promotion works.
           return new _Future<T>.value(result as dynamic);
        }
      } catch (error, stackTrace) {
         /// ...
      }
    }
    Future.microtask(() => print(9));
     Future(() => print(10));
     Future.sync(() => print(11));

    /// 打印结果: 11、9、10

    Future.value

    创建一个将来包含value的future

    factory Future.value([FutureOr<T>? value]) {
       return new _Future<T>.immediate(value == null ? value as T : value);
    }

    参数FutureOr含义为T value 和 Future value 的合集,因为对于一个Future参数来说,他的结果可能为value或者是Future,所以对于以下两种写法均合法:

        Future.value(12).then((value) => print(value));
     Future.value(Future<int>((){
       return 13;
    }));

    这里需要注意即使value接收的是12,仍然会将事件发送到Event队列等待执行,但是相对其他Future事件执行顺序会提前

    Future.error

    创建一个执行结果为error的future

    factory Future.error(Object error, [StackTrace? stackTrace]) {
    /// ...
    return new _Future<T>.immediateError(error, stackTrace);
    }

    _Future.immediateError(var error, StackTrace stackTrace)
    : _zone = Zone._current {
    _asyncCompleteError(error, stackTrace);
    }
    Future.error(new Exception("err msg"))
    .then((value) => print("err value: $value"))
    .catchError((e) => print(e));

    /// 执行结果为:Exception: err msg

    Future.delayed

    创建一个延迟执行回调的future,内部实现为Timer加延时执行一个Future

    factory Future.delayed(Duration duration, [FutureOr<T> computation()?]) {
    /// ...
    new Timer(duration, () {
    if (computation == null) {
    result._complete(null as T);
    } else {
    try {
    result._complete(computation());
    } catch (e, s) {
    _completeWithErrorCallback(result, e, s);
    }
    }
    });
    return result;
    }

    Future.wait

    等待多个Future并收集返回结果

    static Future<List<T>> wait<T>(Iterable<Future<T>> futures,
    {bool eagerError = false, void cleanUp(T successValue)?}) {
    /// ...
    }

    FutureBuilder结合使用:

    child: FutureBuilder(
    future: Future.wait([
    firstFuture(),
    secondFuture()
    ]),
    builder: (context,snapshot){
    if(!snapshot.hasData){
    return CircularProgressIndicator();
    }
    final first = snapshot.data[0];
    final second = snapshot.data[1];
    return Text("data $first $second");
    },
    ),

    Future.any

    返回futures集合中第一个返回结果的值

    static Future<T> any<T>(Iterable<Future<T>> futures) {
    var completer = new Completer<T>.sync();
    void onValue(T value) {
    if (!completer.isCompleted) completer.complete(value);
    }
    void onError(Object error, StackTrace stack) {
    if (!completer.isCompleted) completer.completeError(error, stack);
    }
    for (var future in futures) {
    future.then(onValue, onError: onError);
    }
    return completer.future;
    }

    对上述例子来说,Future.any snapshot.data 将返回firstFuturesecondFuture中第一个返回结果的值

    Future.forEach

    为传入的每一个元素,顺序执行一个action

    static Future forEach<T>(Iterable<T> elements, FutureOr action(T element)) {
    var iterator = elements.iterator;
    return doWhile(() {
    if (!iterator.moveNext()) return false;
    var result = action(iterator.current);
    if (result is Future) return result.then(_kTrue);
    return true;
    });
    }

    这里边action是方法作为参数,头一次见这种形式语法还是在js中,当时就迷惑了很大一会儿,使用示例:

    Future.forEach(["one","two","three"], (element) {
    print(element);
    });

    Future.doWhile

    执行一个操作直到返回false

    Future.doWhile((){
    for(var i=0;i<5;i++){
    print("i => $i");
    if(i >= 3){
    return false;
    }
    }
    return true;
    });
    /// 结果打印到 3

    以上为Future中常用构造函数和方法

    在Widget中使用Future

    Flutter提供了配合Future显示的组件FutureBuilder,使用也很简单,伪代码如下:

    child: FutureBuilder(
    future: getFuture(),
    builder: (context, snapshot){
    if(!snapshot.hasData){
    return CircularProgressIndicator();
    } else if(snapshot.hasError){
    return _ErrorWidget("Error: ${snapshot.error}");
    } else {
    return _ContentWidget("Result: ${snapshot.data}")
    }
    }
    )

    Async-await

    使用

    这两个关键字提供了异步方法的同步书写方式,Future提供了方便的链式调用使用方式,但是不太直观,而且大量的回调嵌套造成可阅读性差。因此,现在很多语言都引入了await-async语法,学习他们的使用方式是很有必要的。

    两条基本原则:

    • 定义一个异步方法,必须在方法体前声明 async

    • await关键字必须在async方法中使用

    首先,在要执行耗时操作的方法体前增加async:

    void main() async { ··· }

    然后,根据方法的返回类型添加Future修饰

    Future<void> main() async { ··· }

    现在就可以使用await关键字来等待这个future执行完毕

    print(await createOrderMessage());

    例如实现一个由一级分类获取二级分类,二级分类获取详情的需求,使用链式调用的代码如下:

    var list = getCategoryList();
    list.then((value) => value[0].getCategorySubList(value[0].id))
    .then((subCategoryList){
    var courseList = subCategoryList[0].getCourseListByCategoryId(subCategoryList[0].id);
    print(courseList);
    }).catchError((e) => (){
    print(e);
    });

    现在来看下使用async/await,事情变得简单了多少

    Future<void> main() async {
    await getCourses().catchError((e){
    print(e);
    });
    }
    Future<void> getCourses() async {
    var list = await getCategoryList();
    var subCategoryList = await list[0].getCategorySubList(list[0].id);
    var courseList = subCategoryList[0].getCourseListByCategoryId(subCategoryList[0].id);
    print(courseList);
    }

    可以看到这样更加直观

    缺陷

    async/await 非常方便,但是还是有一些缺点需要注意

    因为它的代码看起来是同步的,所以是会阻塞后面的代码执行,直到await返回结果,就像执行同步操作一样。它确实可以允许其他任务在此期间继续运行,但后边自己的代码被阻塞。

    这意味着代码可能会由于有大量await代码相继执行而阻塞,本来用Future编写表示并行的操作,现在使用await变成了串行,例如,首页有一个同时获取轮播接口,tab列表接口,msg列表接口的需求

    Future<String> getBannerList() async {
    return await Future.delayed(Duration(seconds: 3),(){
    return "banner list";
    });
    }

    Future<String> getHomeTabList() async {
    return await Future.delayed(Duration(seconds: 3),(){
    return "tab list";
    });
    }

    Future<String> getHomeMsgList() async {
    return await Future.delayed(Duration(seconds: 3),(){
    return "msg list";
    });
    }

    使用await编写很可能会写成这样,打印执行操作的时间

    Future<void> main2() async {
    var startTime = DateTime.now().second;
    await getBannerList();
    await getHomeTabList();
    await getHomeMsgList();
    var endTime = DateTime.now().second;
    print(endTime - startTime); // 9
    }

    在这里,我们直接等待所有三个模拟接口的调用,使每个调用3s。后续的每一个都被迫等到上一个完成, 最后会看到总运行时间为9s,而实际我们想三个请求同时执行,代码可以改成如下这种:

    Future<void> main() async {
    var startTime = DateTime.now().second;
    var bannerList = getBannerList();
    var homeTabList = getHomeTabList();
    var homeMsgList = getHomeMsgList();

    await bannerList;
    await homeTabList;
    await homeMsgList;
    var endTime = DateTime.now().second;
    print(endTime - startTime); // 3
    }

    将三个Future存储在变量中,这样可以同时启动,最后打印时间仅为3s,所以在编写代码时,我们必须牢记这点,避免性能损耗。

    原理

    线程模型

    当一个Flutter应用或者Flutter Engine启动时,它会启动(或者从池中选择)另外三个线程,这些线程有些时候会有重合的工作点,但是通常,它们被称为UI线程GPU线程IO线程。需要注意一点这个UI线程并不是程序运行的主线程,或者说和其他平台上的主线程理解不同,通常的,Flutter将平台的主线程叫做"Platform thread"


    UI线程是所有的Dard代码运行的地方,例如framework和你的应用,除非你启动自己的isolates,否则Dart将永远不会运行在其他线程。平台线程是所有依赖插件的代码运行的地方。该线程也是native frameworks为其他任务提供服务的地方,一般来说,一个Flutter应用启动的时候会创建一个Engine实例,Engine创建的时候会创建一个Platform thread为其提供服务。跟Flutter Engine的所有交互(接口调用)必须发生在Platform Thread,试图在其它线程中调用Flutter Engine会导致无法预期的异常。这跟Android/iOS UI相关的操作都必须在主线程进行相类似。

    Isolates是Dart中概念,本意是隔离,它的实现功能和thread类似,但是他们之间的实现又有着本质的区别,Isolote是独立的工作者,它们之间不共享内存,而是通过channel传递消息。Dart是单线程执行代码,Isolate提供了Dart应用可以更好的利用多核硬件的解决方案。

    事件循环

    单线程模型中主要就是在维护着一个事件循环(Event Loop) 与 两个队列(event queue和microtask queue)当Flutter项目程序触发如点击事件IO事件网络事件时,它们就会被加入到eventLoop中,eventLoop一直在循环之中,当主线程发现事件队列不为空时发现,就会取出事件,并且执行。

    microtask queue中事件优先于event queue执行,当有任务发送到microtask队列时,会在当前event执行完成后,阻塞当前event queue转而去执行microtask queue中的事件,这样为Dart提供了任务插队的解决方案。

    event queue的阻塞意味着app无法进行UI绘制,响应鼠标和I/O等事件,所以要谨慎使用,如下为流程图:


    这两个任务队列中的任务切换在某些方面就相当于是协程调度机制

    协程

    协程是一种协作式的任务调度机制,区别于操作系统的抢占式任务调度机制,它是用户态下面的,避免线程切换的内核态、用户态转换的性能开销。它让调用者自己来决定什么时候让出cpu,比操作系统的抢占式调度所需要的时间代价要小很多,后者为了恢复现场会保存相当多的状态(不仅包括进程上下文的虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态),并且会频繁的切换,以现在流行的大多数Linux机器来说,每一次的上下文切换要消耗大约1.2-1.5μs的时间,这是仅考虑直接成本,固定在单个核心以避免迁移的成本,未固定情况下,切换时间可达2.2μs


    对cpu来说这算一个很长的时间吗,一个很好的比较是memcpy,在相同的机器上,完成一个64KiB数据的拷贝需要3μs的时间,上下文的切换比这个操作稍微快一些


    协程和线程非常相似,是从异步执行任务的角度来看,而并不是从设计的实体角度像进程->线程->协程这样类似于细胞->原子核->质子中子这样的关系。可以理解为线程上执行的一段函数,用yield完成异步请求、注册回调/通知器、保存状态,挂起控制流、收到回调/通知、恢复状态、恢复控制流的所有过程

    多线程执行任务模型如图:


    线程的阻塞要靠系统间进程的切换,完成逻辑流的执行,频繁的切换耗费大量资源,而且逻辑流的执行数量严重依赖于程序申请到的线程的数量。

    协程是协同多任务的,这意味着协程提供并发性但不提供并行性,执行流模型图如下:


    协程可以用逻辑流的顺序去写控制流,协程的等待会主动释放cpu,避免了线程切换之间的等待时间,有更好的性能,逻辑流的代码编写和理解上也简单的很多

    但是线程并不是一无是处,抢占式线程调度器事实上提供了准实时的体验。例如Timer,虽然不能确保在时间到达的时候一定能够分到时间片运行,但不会像协程一样万一没有人让出时间片就永远得不到运行……

    总结

    • 同步与异步

    • Future提供了Flutter中异步代码链式编写方式

    • async-wait提供了异步代码的同步书写方式

    • Future的常用方法和FutureBuilder编写UI

    • Flutter中线程模型,四个线程

    • 单线程语言的事件驱动模型

    • 进程间切换和协程对比

    参考

    dart.cn/tutorials/l…

    dart.cn/codelabs/as…

    medium.com/dartlang/da…

    juejin.cn/post/684490…

    developer.mozilla.org/en-US/docs/…

    http://www.zhihu.com/question/19…

    http://www.zhihu.com/question/50…

    en.wikipedia.org/wiki/Asynch…

    eli.thegreenplace.net/2018/measur…


    作者:QiShare
    来源:juejin.cn/post/6987637272375984165

    收起阅读 »

    Flutter 小技巧之 ListView 和 PageView 的各种花式嵌套

    这次的 Flutter 小技巧是 ListView 和 PageView 的花式嵌套,不同 Scrollable 的嵌套冲突问题相信大家不会陌生,今天就通过 ListView 和 PageView 的三种嵌套模式带大家收获一些不一样的小技巧。正常嵌套最常见的...
    继续阅读 »

    这次的 Flutter 小技巧是 ListViewPageView 的花式嵌套,不同 Scrollable 的嵌套冲突问题相信大家不会陌生,今天就通过 ListViewPageView 的三种嵌套模式带大家收获一些不一样的小技巧。

    正常嵌套

    最常见的嵌套应该就是横向 PageView 加纵向 ListView 的组合,一般情况下这个组合不会有什么问题,除非你硬是要斜着滑

    最近刚好遇到好几个人同时在问:“斜滑 ListView 容易切换到 PageView 滑动” 的问题,如下 GIF 所示,当用户在滑动 ListView 时,滑动角度带上倾斜之后,可能就会导致滑动的是 PageView 而不是 ListView


    虽然从我个人体验上并不觉得这是个问题,但是如果产品硬是要你修改,难道要自己重写 PageView 的手势响应吗?

    我们简单看一下,不管是 PageView 还是 ListView 它们的滑动效果都来自于 Scrollable ,而 Scrollable 内部针对不同方向的响应,是通过 RawGestureDetector 完成:

    • VerticalDragGestureRecognizer 处理垂直方向的手势

    • HorizontalDragGestureRecognizer 处理水平方向的手势

    所以简单看它们响应的判断逻辑,可以看到一个很有趣的方法 computeHitSlop根据 pointer 的类型确定当然命中需要的最小像素,触摸默认是 kTouchSlop (18.0)

    image-20220613103745974

    看到这你有没有灵光一闪:如果我们把 PageView 的 touchSlop 修改了,是不是就可以调整它响应的灵敏度? 恰好在 computeHitSlop 方法里,它可以通过 DeviceGestureSettings 来配置,而 DeviceGestureSettings 来自于 MediaQuery ,所以如下代码所示:

    body: MediaQuery(
     ///调高 touchSlop 到 50 ,这样 pageview 滑动可能有点点影响,
     ///但是大概率处理了斜着滑动触发的问题
     data: MediaQuery.of(context).copyWith(
         gestureSettings: DeviceGestureSettings(
       touchSlop: 50,
    )),
     child: PageView(
       scrollDirection: Axis.horizontal,
       pageSnapping: true,
       children: [
         HandlerListView(),
         HandlerListView(),
      ],
    ),
    ),

    小技巧一:通过嵌套一个 MediaQuery ,然后调整 gestureSettingstouchSlop 从而修改 PageView 的灵明度 ,另外不要忘记,还需要把 ListViewtouchSlop 切换会默认 的 kTouchSlop

    class HandlerListView extends StatefulWidget {
     @override
     _MyListViewState createState() => _MyListViewState();
    }
    class _MyListViewState extends State<HandlerListView> {
     @override
     Widget build(BuildContext context) {
       return MediaQuery(
         ///这里 touchSlop 需要调回默认
         data: MediaQuery.of(context).copyWith(
             gestureSettings: DeviceGestureSettings(
           touchSlop: kTouchSlop,
        )),
         child: ListView.separated(
           itemCount: 15,
           itemBuilder: (context, index) {
             return ListTile(
               title: Text('Item $index'),
            );
          },
           separatorBuilder: (context, index) {
             return const Divider(
               thickness: 3,
            );
          },
        ),
      );
    }
    }

    最后我们看一下效果,如下 GIF 所示,现在就算你斜着滑动,也很触发 PageView 的水平滑动,只有横向移动时才会触发 PageView 的手势,当然, 如果要说这个粗暴的写法有什么问题的话,大概就是降低了 PageView 响应的灵敏度

    xiehuabudong

    同方向 PageView 嵌套 ListView

    介绍完常规使用,接着来点不一样的,在垂直切换的 PageView 里嵌套垂直滚动的 ListView , 你第一感觉是不是觉得不靠谱,为什么会有这样的场景?

    对于产品来说,他们不会考虑你如何实现的问题,他们只会拍着脑袋说淘宝可以,为什么你不行,所以如果是你,你会怎么做?

    而关于这个需求,社区目前讨论的结果是:PageViewListView 的滑动禁用,然后通过 RawGestureDetector 自己管理

    如果对实现逻辑分析没兴趣,可以直接看本小节末尾的 源码链接

    看到自己管理先不要慌,虽然要自己实现 PageViewListView 的手势分发,但是其实并不需要重写 PageViewListView ,我们可以复用它们的 Darg 响应逻辑,如下代码所示:

    • 通过 NeverScrollableScrollPhysics 禁止了 PageViewListView 的滚动效果

    • 通过顶部 RawGestureDetectorVerticalDragGestureRecognizer 自己管理手势事件

    • 配置 PageControllerScrollController 用于获取状态

    body: RawGestureDetector(
     gestures: <Type, GestureRecognizerFactory>{
       VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<
               VerticalDragGestureRecognizer>(
          () => VerticalDragGestureRecognizer(),
          (VerticalDragGestureRecognizer instance) {
         instance
          ..onStart = _handleDragStart
          ..onUpdate = _handleDragUpdate
          ..onEnd = _handleDragEnd
          ..onCancel = _handleDragCancel;
      })
    },
     behavior: HitTestBehavior.opaque,
     child: PageView(
       controller: _pageController,
       scrollDirection: Axis.vertical,
       ///屏蔽默认的滑动响应
       physics: const NeverScrollableScrollPhysics(),
       children: [
         ListView.builder(
           controller: _listScrollController,
           ///屏蔽默认的滑动响应
           physics: const NeverScrollableScrollPhysics(),
           itemBuilder: (context, index) {
             return ListTile(title: Text('List Item $index'));
          },
           itemCount: 30,
        ),
         Container(
           color: Colors.green,
           child: Center(
             child: Text(
               'Page View',
               style: TextStyle(fontSize: 50),
            ),
          ),
        )
      ],
    ),
    ),

    接着我们看 _handleDragStart 实现,如下代码所示,在产生手势 details 时,我们主要判断:

    • 通过 ScrollController 判断 ListView 是否可见

    • 判断触摸位置是否在 ListIView 范围内

    • 根据状态判断通过哪个 Controller 去生产 Drag 对象,用于响应后续的滑动事件

      void _handleDragStart(DragStartDetails details) {

       if (_listScrollController?.hasClients == true &&
           _listScrollController?.position.context.storageContext != null) {
         ///获取 ListView 的 renderBox
         final RenderBox? renderBox = _listScrollController
             ?.position.context.storageContext
            .findRenderObject() as RenderBox;

         if (renderBox?.paintBounds
                .shift(renderBox.localToGlobal(Offset.zero))
                .contains(details.globalPosition) ==
             true) {
           _activeScrollController = _listScrollController;
           _drag = _activeScrollController?.position.drag(details, _disposeDrag);
           return;
        }
      }

       ///这时候就可以认为是 PageView 需要滑动
       _activeScrollController = _pageController;
       _drag = _pageController?.position.drag(details, _disposeDrag);
    }

    前面我们主要在触摸开始时,判断需要响应的对象时ListView 还是 PageView ,然后通过 _activeScrollController 保存当然响应对象,并且通过 Controller 生成用于响应手势信息的 Drag 对象。

    简单说:滑动事件发生时,默认会建立一个 Drag 用于处理后续的滑动事件,Drag 会对原始事件进行加工之后再给到 ScrollPosition 去触发后续滑动效果。

    接着在 _handleDragUpdate 方法里,主要是判断响应是不是需要切换到 PageView:

    • 如果不需要就继续用前面得到的 _drag?.update(details)响应 ListView 滚动

    • 如果需要就通过 _pageController 切换新的 _drag 对象用于响应

    void _handleDragUpdate(DragUpdateDetails details) {
     if (_activeScrollController == _listScrollController &&

         ///手指向上移动,也就是快要显示出底部 PageView
         details.primaryDelta! < 0 &&

         ///到了底部,切换到 PageView
         _activeScrollController?.position.pixels ==
             _activeScrollController?.position.maxScrollExtent) {
       ///切换相应的控制器
       _activeScrollController = _pageController;
       _drag?.cancel();

       ///参考 Scrollable 里
       ///因为是切换控制器,也就是要更新 Drag
       ///拖拽流程要切换到 PageView 里,所以需要 DragStartDetails
       ///所以需要把 DragUpdateDetails 变成 DragStartDetails
       ///提取出 PageView 里的 Drag 相应 details
       _drag = _pageController?.position.drag(
           DragStartDetails(
               globalPosition: details.globalPosition,
               localPosition: details.localPosition),
           _disposeDrag);
    }
     _drag?.update(details);
    }

    这里有个小知识点:如上代码所示,我们可以简单通过 details.primaryDelta 判断滑动方向和移动的是否是主轴

    最后如下 GIF 所示,可以看到 PageView 嵌套 ListView 同方向滑动可以正常运行了,但是目前还有个两个小问题,从图示可以看到:

    • 在切换之后 ListView 的位置没有保存下来

    • 产品要求去除 ListView 的边缘溢出效果

    7777777777777

    所以我们需要对 ListView 做一个 KeepAlive ,然后用简单的方法去除 Android 边缘滑动的 Material 效果:

    • 通过 with AutomaticKeepAliveClientMixinListView 在切换之后也保持滑动位置

    • 通过 ScrollConfiguration.of(context).copyWith(overscroll: false) 快速去除 Scrollable 的边缘 Material 效果

    child: PageView(
     controller: _pageController,
     scrollDirection: Axis.vertical,
     ///去掉 Android 上默认的边缘拖拽效果
     scrollBehavior:
         ScrollConfiguration.of(context).copyWith(overscroll: false),


    ///对 PageView 里的 ListView 做 KeepAlive 记住位置
    class KeepAliveListView extends StatefulWidget {
     final ScrollController? listScrollController;
     final int itemCount;

     KeepAliveListView({
       required this.listScrollController,
       required this.itemCount,
    });

     @override
     KeepAliveListViewState createState() => KeepAliveListViewState();
    }

    class KeepAliveListViewState extends State<KeepAliveListView>
       with AutomaticKeepAliveClientMixin {
     @override
     Widget build(BuildContext context) {
       super.build(context);
       return ListView.builder(
         controller: widget.listScrollController,

         ///屏蔽默认的滑动响应
         physics: const NeverScrollableScrollPhysics(),
         itemBuilder: (context, index) {
           return ListTile(title: Text('List Item $index'));
        },
         itemCount: widget.itemCount,
      );
    }

     @override
     bool get wantKeepAlive => true;
    }

    所以这里我们有解锁了另外一个小技巧:通过 ScrollConfiguration.of(context).copyWith(overscroll: false) 快速去除 Android 滑动到边缘的 Material 2效果,为什么说 Material2, 因为 Material3 上变了,具体可见: Flutter 3 下的 ThemeExtensions 和 Material3

    000000000

    本小节源码可见: github.com/CarGuo/gsy_…

    同方向 ListView 嵌套 PageView

    那还有没有更非常规的?答案是肯定的,毕竟产品的小脑袋,怎么会想不到在垂直滑动的 ListView 里嵌套垂直切换的 PageView 这种需求。

    有了前面的思路,其实实现这个逻辑也是异曲同工:PageViewListView 的滑动禁用,然后通过 RawGestureDetector 自己管理,不同的就是手势方法分发的差异。

    RawGestureDetector(
             gestures: <Type, GestureRecognizerFactory>{
               VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<
                       VerticalDragGestureRecognizer>(
                  () => VerticalDragGestureRecognizer(),
                  (VerticalDragGestureRecognizer instance) {
                 instance
                  ..onStart = _handleDragStart
                  ..onUpdate = _handleDragUpdate
                  ..onEnd = _handleDragEnd
                  ..onCancel = _handleDragCancel;
              })
            },
             behavior: HitTestBehavior.opaque,
             child: ListView.builder(
                   ///屏蔽默认的滑动响应
                   physics: NeverScrollableScrollPhysics(),
                   controller: _listScrollController,
                   itemCount: 5,
                   itemBuilder: (context, index) {
                     if (index == 0) {
                       return Container(
                         height: 300,
                         child: KeepAlivePageView(
                           pageController: _pageController,
                           itemCount: itemCount,
                        ),
                      );
                    }
                     return Container(
                         height: 300,
                         color: Colors.greenAccent,
                         child: Center(
                           child: Text(
                             "Item $index",
                             style: TextStyle(fontSize: 40, color: Colors.blue),
                          ),
                        ));
                  }),
          )

    同样是在 _handleDragStart 方法里,这里首先需要判断:

    • ListView 如果已经滑动过,就不响应顶部 PageView 的事件

    • 如果此时 ListView 处于顶部未滑动,判断手势位置是否在 PageView 里,如果是响应 PageView 的事件

      void _handleDragStart(DragStartDetails details) {
       if (_listScrollController.offset > 0) {
         _activeScrollController = _listScrollController;
         _drag = _listScrollController.position.drag(details, _disposeDrag);
         return;
      }
       if (_pageController.hasClients) {
         ///获取 PageView
         final RenderBox renderBox =
             _pageController.position.context.storageContext.findRenderObject()
                 as RenderBox;

         ///判断触摸范围是不是在 PageView
         final isDragPageView = renderBox.paintBounds
            .shift(renderBox.localToGlobal(Offset.zero))
            .contains(details.globalPosition);

         ///如果在 PageView 里就切换到 PageView
         if (isDragPageView) {
           _activeScrollController = _pageController;
           _drag = _activeScrollController.position.drag(details, _disposeDrag);
           return;
        }
      }

       ///不在 PageView 里就继续响应 ListView
       _activeScrollController = _listScrollController;
       _drag = _listScrollController.position.drag(details, _disposeDrag);
    }

    接着在 _handleDragUpdate 方法里,判断如果 PageView 已经滑动到最后一页,也将滑动事件切换到 ListView


    当然,同样还有 KeepAlive 和去除列表 Material 边缘效果,最后运行效果如下 GIF 所示。

    22222222222

    本小节源码可见:github.com/CarGuo/gsy_…

    最后再补充一个小技巧:如果你需要 Flutter 打印手势竞技的过程,可以配置 debugPrintGestureArenaDiagnostics = true;来让 Flutter 输出手势竞技的处理过程

    import 'package:flutter/gestures.dart';
    void main() {
     debugPrintGestureArenaDiagnostics = true;
     runApp(MyApp());
    }

    image-20220613115808538

    最后

    最后总结一下,本篇介绍了如何通过 Darg 解决各种因为嵌套而导致的手势冲突,相信大家也知道了如何利用 ControllerDarg 来快速自定义一些滑动需求,例如 ListView 联动 ListView 的差量滑动效果:




    44444444444444


    作者:恋猫de小郭
    来源:juejin.cn/post/7116267156655833102

    收起阅读 »

    Flutter极简状态管理库Creator

    我之前一直用riverpod来做状态管理,最近发现了一个新发布的库,尝试了一下,非常简洁好用,给大家推荐一下。叫做Creator(地址),刚发布几天就有几十个👍。这个库的API跟riverpod很接近,但是更加简洁清晰,基本上没有什么上手难度。先看一下它的co...
    继续阅读 »

    我之前一直用riverpod来做状态管理,最近发现了一个新发布的库,尝试了一下,非常简洁好用,给大家推荐一下。叫做Creator(地址),刚发布几天就有几十个👍。

    这个库的API跟riverpod很接近,但是更加简洁清晰,基本上没有什么上手难度。

    先看一下它的counter例子:

    // 定义状态
    final counter = Creator.value(0);
    Widget build(BuildContext context) {
    return Column(
      children: [
        // 响应状态
        Watcher((context, ref, _) => Text('${ref.watch(counter)}')),
        TextButton(
          // 更新状态
          onPressed: () => context.ref.update<int>(counter, (count) => count + 1),
          child: const Text('+1'),
        ),
      ],
    );
    }

    它的核心概念极其简单,只提供两种creator:

    • Creator 产生一系列的 T

    • Emitter 产生一系列的 Future<T>

    这里T可以是任何类型,甚至可以是Widget。然后它把所有的creator都组织成一个有向图(叫做Ref)。

    还是举一个官网的例子吧。可以在DartPad上跑,显示摄氏温度或者华氏温度:



    // repo.dart

    // 假装调用一个后端API。
    Future<int> getFahrenheit(String city) async {
    await Future.delayed(const Duration(milliseconds: 100));
    return 60 + city.hashCode % 20;
    }
    // logic.dart

    // 简单的creator
    final cityCreator = Creator.value('London');
    final unitCreator = Creator.value('Fahrenheit');

    // 可以像Iterable/Stream那样使用 map, where, reduce 之类的.
    final fahrenheitCreator = cityCreator.asyncMap(getFahrenheit);

    // 组合不同的creator,产生新的业务逻辑。
    final temperatureCreator = Emitter<String>((ref, emit) async {
    final f = await ref.watch(fahrenheitCreator);
    final unit = ref.watch(unitCreator);
    emit(unit == 'Fahrenheit' ? '$f F' : '${f2c(f)} C');
    });

    // 摄氏华氏温度转换
    int f2c(int f) => ((f - 32) * 5 / 9).round();
    // main.dart

    Widget build(BuildContext context) {
    return Watcher((context, ref, _) =>
        Text(ref.watch(temperatureCreator.asyncData).data ?? 'loading'));
    }
    ... context.ref.set(cityCreator, 'Pairs'); // 会调用后端API
    ... context.ref.set(unitCreator, 'Celsius'); // 不会调用后端API

    可以看出,当用户改变所选城市之后, 状态会沿着图中的箭头传导,一直传到最后的Creator<Widget>,从而更新UI。

    我觉得这个有向图的设计还是非常独特的,很好理解,也很简单。组织比较复杂的业务逻辑的时候非常方便。

    这个库的核心代码才500行,感兴趣的同学可以去看官方文档和代码。

    欢迎讨论!

    作者:Jay_Guo
    来源:juejin.cn/post/7107433326054473736

    收起阅读 »