赞
踩
项目需开发移动端,需支持以图表、表格等形式展示数据,对素材进行审核审批等功能。并需支持微信、企微小程序、h5等平台使用。
从落地场景分析,我们需要具备,微信小程序,企微小程序,h5等平台的支持。如果采用小程序/h5等单平台框架开发,在开发效率与人力占用上的成本显然会与需要支持的平台数量成正比。同时小程序在原生开发上也无法使用工程化带来的部分提效功能,所以在选型上会优先考虑跨平台的工程化开发框架。在技术选型过程中,预研了以下几种跨平台方案。
Taro :一个基于 React 技术栈的跨平台小程序框架,支持使用 React/Vue
/Nerv 等框架来开发 微信
/企微
/京东/百度/支付宝/字节跳动/QQ/飞书小程序 //H5
// RN 等应用。
uni-app :一个使用 Vue.js
技术栈的跨平台小程序框架,目前支持iOS、Android、Web(响应式)
、以及各种小程序(微信
/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝)、快应用等多个平台。
mpvue :由Meituan-Dianping团队开发,基于vue.js设计的跨端框架。目前支持微信小程序
、 百度智能小程序, 头条小程序 和 支付宝小程序。
综合下来,Taro的跨技术栈、平台支持和比较完善的生态会更适合我们当前需要落地的应用。具体的入门教程等可看官方文档,这里不详细讲解。
tailwind css ,移动端上,ui要求往往比较严格,乃至到1px的偏差,而tailwind可以更灵活的满足自定义样式,同时降低样式开发管理的成本。
sass :因为后续使用到的组件库是基于sass来开发样式,所以项目中也使用sass css预处理器。对于公用组件样式,需要使用css modules模式来避免样式冲突。
全局状态管理:市面上的状态管理库数不胜数,像redux, mobx, recoil等依赖库群魔乱舞,在此就不具体展开,有兴趣的可以看看这个视频 学习下,在项目中最终确定使用较轻量级、学习和使用成本也非常低的jotai 。
数据获取状态管理:对比了swr 与react-query 。相较于swr,react-query功能比较丰富,包括请求缓存、生命周期管理、请求取消、错误处理等,比较适用于复杂的应用场景,而swr则比较轻量级,API 设计简单易用,学成本相对较低,适合快速开发。鉴于小程序的场景交互较为简单,同时对于体积严格要求,最终选择轻量易用的swr。
commitizen
来规范commit规范
sudo npm install -g commitizen
// commitizen 的首选适配器,提供commit的提交标准配置
sudo npm install -g cz-conventional-changelog
echo '{ "path": "cz-conventional-changelog" }' > ~/.czrc
- 1
husky:提供githook,如在commit之前会触发pre-commit,只要编写对应的校验逻辑即可完成commit的规范检查。
lint-staged:通过执行lint-staged,能更高效的过滤出需要进行规范校验的文件
npm install lint-staged
//在package.json添加如下配置,表示会过滤出所有ts,tsx后缀的文件,然后对文件执行eslint --fix
"lint-staged": {
"**/**/*.{ts,tsx}": [
"eslint --fix"
]
}
- 1
npm install stylelint
// 对src目录下对应后缀的文件进行样式校验
npx stylelint src/**/*.{html,tsx,css,sass,scss} --fix
// 配置样式校验规则
.stylelintrc.json
- 1
为了方便追踪运行流程,截图为运行时环境,解析中的代码位置为源码位置
import useSWR from 'swr/immutable';
const useData = (key) => {
// 将查询条件作为key
const key = ...
// 只要key不变,则data不会变化
const { data, isValidating } = useSWR(
key,
fetch,
);
return {
data,
isValidating
};
};
- 1
import { mutate } from 'swr';
const useData(){
const key = ...
// 只要key不变,则data不会变化
const { data, isValidating } = useSWR(
key,
fetch,
);
return {
// 在前端进行数据操作后,将更新的数据传入
refresh(updateParams){
mutate(key, originData => ({
...originData,
...do something // 利用updateParams来更新原始数据
}), {
populateCache: true, // 告诉 SWR 用 mutate 的响应去更新本地数据
revalidate: false, // 不发起请求重新验证
rollbackOnError: true, // 操作失败了进行回滚
});
}
}
}
- 1
// app.tsx
const preloadImages = (imageUrls): Promise<string[]> => new Promise((resolve, reject) => {
const loadedImages: string[] = [];
let loadedCount = 0;
function loadHandler() {
loadedCount += 1;
// 当所有图片都完成加载,即返回结果
if (loadedCount === imageUrls.length) {
resolve(loadedImages);
}
}
for (let i = 0; i < imageUrls.length; i++) {
const imageUrl = imageUrls[i];
if (/http|https/.test(imageUrl)) {
wx.downloadFile({
url: imageUrl,
success: (res) => {
if (res.statusCode === 200) {
loadedImages[i] = res.tempFilePath;
loadHandler();
} else {
reject(new Error(`Failed to download image: ${imageUrl}`));
}
},
fail: (error) => {
console.log({ error });
reject(error);
},
});
} else {
loadHandler();
loadedImages[i] = imageUrl;
}
}
});
useEffect(() => {
// 预加载客服按钮的图片
preloadImages(customerPic).then((res) => {
setCustomerImg(res);
}).catch(() => {
// 预防加载失败
setCustomerImg(customerPic);
});
}, []);
- 1
// 通过当前路由获取到当前页面元素
const getParentElement = () => {
const currentPages = getCurrentPages();
const currentPage = currentPages[currentPages.length - 1];
const path = currentPage?.$taroPath;
return document.getElementById(path);
};
// 封装全局元素创建方法
export const appendChild = (Content) => {
const view = document.createElement('view');
if (id) {
view.className = id;
}
const pageElement = getParentElement();
const dom = pageElement?.getElementsByClassName(id);
if (dom?.length) return;
render(<Content />, view);
pageElement?.appendChild(view);
return () => destroyChild(view);
};
// 封装元素销毁方法
export const destroyChild = (node) => {
const pageElement = getParentElement();
unmountComponentAtNode(node);
pageElement?.removeChild(node);
};
- 1
然后在/src/app.tsx,也就是页面的入口处进行全局组件的全局插入
import { getCurrentPages } from '@tarojs/taro';
export function Entrance(props) {
const currentPages = getCurrentPages();
useEffect(() => {
// 可自行进行逻辑判断是否在某些页面不执行插入等
appendOnce(WaterMask, watermaskId);
}, [currentPages]);
....
return props.children
}
- 1
import Skeleton from '@/components/skeleton/skeleton';
function Test() {
const homeInfo = {
avatar: 'xxx.png',
name: 'coln',
};
const [info, setInfo] = useState({ avatar: '', name: '' });
const [loading, setLoading] = useState(false);
useEffect(() => {
// do something
}, []);
const selector = uuid();
return (
<>
<Skeleton selector={selector} loading={loading} bgColor="#c9c9c9" />
<View className={`... ${selector}`}>
<Image
...
className="... skeleton-round"
src={info.avatar}
/>
<View className="... skeleton-rect">{info.name}</View>
</View>
</>
);
}
- 1
以上是一个骨架屏组件Skeleton的使用例子,组件的底层实现是将需要骨架屏效果的元素className以selector属性传入,去遍历获取拥有该className的元素,并使其子元素中带有skeleton-round/skeleton-rect,在loading值为true的时候动态加入class样式来实现骨架效果。那这里主要的问题是元素的className取值比较麻烦,需要手动声明一个随机的className。并且需要手动import,作为一个局部loading的替代方案,使用的次数也会比较频繁,那么减少这部分代码的编写成本也会提高一定的开发效率。
function Test() {
...
return (
<>
{/* skeleton */[loading, '#c9c9c9']}
<View className="...">
...
</View>
</>
);
}
- 1
通过loader来解析
{/* skeleton */[loading, '#c9c9c9']}
变为
import Skeletonj4159ada from "@/components/skeleton/skeleton"
function Test() {
...
return <>
<Skeletonj4159ada selector="j4159ada-1caa-46e5-830b-32048ca6de64" bgColor="#c9c9c9" loading={loading} />
<View className="... j4159ada-1caa-46e5-830b-32048ca6de64">
...
</View>
</>;
}
- 1
// wdyr.tsx
import React, { Profiler } from 'react';
const elementMap = {};
const onRender = (
id,
phase,
actualDuration, baseDuration,
) => {
if (!elementMap[id]) {
elementMap[id] = { duration: actualDuration, count: 1 };
} else {
elementMap[id].duration += actualDuration;
elementMap[id].count += 1;
}
console.log({
id,
actualDuration,
baseDuration,
elementMap,
});
};
if (process.env.NODE_ENV === 'development') {
whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: true,
collapseGroups: true,
onlyLogs: true,
titleColor: 'green',
diffNameColor: 'darkturquoise',
trackHooks: true,
});
}
export const withProfilerAndWdyr = (Component) => {
Component.whyDidYouRender = true;
function WrappedComponent(props) {
return (
<Profiler id={Component.name} onRender={onRender}>
<Component {...props} />
</Profiler>
);
}
WrappedComponent.displayName = `withProfilerAndWdyr(${Component.name})`;
return WrappedComponent;
};
// example
import{ withProfilerAndWdyr } from 'wdyr';
function Example() {
const [count, setCount] = useState({ num: 0 });
const add = () => {
setCount({ num: 0 });
};
return (
<View>
<View
style={{
width: '100px',
height: '30px',
marginTop: 50,
textAlign: 'center',
lineHeight: '30px',
background: 'red',
}}
onClick={add}
>
+
</View>
{count.num}
</View>
);
}
export default withProfilerAndWdyr(Example);
- 1
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom)
// 初始化state
state = {
Page: [ View, ScrollView ]
}
// diff时导致数组重新赋值
const oldPage = this.state.Page
this.setState({
Page: [ ...oldPage, View ]
})
- 1
本文由 mdnice 多平台发布
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。