注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

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过多