theme: channing-cyan
前言
最近,因为业务需要,要新启一个小程序项目,于是乎便有了这篇选型的文章,本篇将简单讲述基础框架建设及部分兼容问题和注意事项,欢迎阅完后指点。
选型
虽说是小程序项目,但是考虑到后续可能有额外拓展(如H5等其实可能性还是蛮小的,但确实得考虑到),页面标签采用原生 html 写法,即传统 div span 等标签。
Taro 和 Uniapp 我选择了 Uniapp,首先两者的生态圈,我是感觉 Uniapp 的生态圈会更丰富一些,解决方案也相对多些,Uniapp 毕竟是基于 Vue,国内的 Vue 也是追随者比较多。
Taro 由于时间关系了解并不多,虽然说支持 Vue 但是大多数方案都是基于 React 来的,而团队中熟悉 Vue 居多,考虑技术栈不一致的问题,所以最终还是选择了 Uniapp。
Uniapp
在确实好主框架后,我采用 Vue-cli 中自带的 Uniapp模板(Vue3.0 TS)作为此次技术栈。
代码语言:javascript复制vue create -p dcloudio/uni-preset-vue#vue3 <project Name>
建立完成后,结构如下
简单调整下目录
- api 具体的请求都放在这里,这里面还可以根据业务模块来细分,不必多说。
- assets 静态资源,存放图片。
- components 公共组件。
- constant 常量,主要用于储存一些使用居多的常量比如商品类型(1:实物,2:虚拟物品)等。
- layout 全局页面容器,在开发页面时最外层包裹一层 layout,主要是为了适配各个机型(如IphoneX的刘海屏),可以在这里安全区域的适配处理,这个后面说。
- lib 这里我上面没有标(可选),这里用来存放一些 Ts 定义好的结构。
- Packages 小程序分包,用来做分包测试用的 Demo。
- pages 业务页面。
- router 路由。
- serve 公共请求方法,请求拦截器等都在这里实现。
- static 静态资源,忘记删了,跟上面重复了看个人命名喜好。
- store 全局状态管理模块,Vuex。
- utils 工具函数。
简单讲解一些做移动端会遇到的问题,诸如公共组件、请求等大同小异,不再赘叙。
layout
首先说一下,layout 存在的意义,他就是为了给全局适配机型而存在,裹上他只需要关注业务层面即可,无需在做适配。
Uniapp 非常的友好为我们提供了 getSystemInfoSync
这个方法用于获取系统信息,该方法会返回一个 safeArea
,在竖屏正方向下的安全区域,我们可以基于此来做文章。
之后我们通过 getMenuButtonBoundingClientRect
来获取微信小程序右上角的胶囊位置信息,来简单适配一下安全区域。
/**
* @feat < 获取 操作栏在视图上的top值 >
* @param {number} height 与小程序菜单按钮对齐的操作条高度
*/
export function getBarClientTop(height: number): number {
let top: number;
const { safeArea } = uni.getSystemInfoSync();
const safeAreaInsets = safeArea ? safeArea.top : 0;
// #ifdef MP
if (height > 0) {
const menuBtnRect =
uni.getMenuButtonBoundingClientRect &&
uni.getMenuButtonBoundingClientRect();
top = menuBtnRect.height / 2 menuBtnRect.top - height / 2;
} else {
top = safeAreaInsets;
}
// #endif
// #ifdef H5 || APP-PLUS
top = safeAreaInsets;
// #endif
return top;
}
这里 height 默认是可以传 44,具体可以根据实际结果来。我们通过这些api获取到高度信息用于我们元素的间距。
.eslintrc.js
由于项目是基于 Node 环境的 ts 来开发,需要添加一下 uni 及 wx 全局规则以便 ts 能够正常运行。
代码语言:javascript复制module.exports = {
globals: {
uni: true,
wx: true,
}
}
postcss
由于设计稿是2倍出的图(设计稿宽度为750px,实际为375px),并且需要用 rpx 来适配小程序。
代码语言:javascript复制const path = require("path");
module.exports = {
parser: require("postcss-comment"),
plugins: [
require("postcss-pxtorpx-pro")({
// 转化的单位
unit: "rpx",
// 单位精度
unitPrecision: 2,
// 需要转化的最小的pixel值,低于该值的px单位不做转化
minPixelValue: 1,
// 不处理的文件
exclude: /node_modules|components/gi,
// 默认设计稿按照750宽,2倍图的出
// 640 0.85
transform: (x) => x * 2,
}),
require("postcss-import")({
resolve(id) {
if (id.startsWith("~@/")) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(3));
} else if (id.startsWith("@/")) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(2));
} else if (id.startsWith("/") && !id.startsWith("//")) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(1));
}
return id;
},
}),
require("autoprefixer")({
remove: process.env.UNI_PLATFORM !== "h5",
}),
require("@dcloudio/vue-cli-plugin-uni/packages/postcss"),
],
};
assets/css/constant.scss
在这里,定义一些常用的样式变量,如果后期有换肤功能即可很方便的切换,具体规则可以由设计给出。
代码语言:javascript复制// 主题色
$main-color: #EE5A61; // 品牌色
$sub-color: #FFA264; // 辅助色
// 背景色
$page-background-color: #F2F4F5; // 页面背景色
// More ...
相机组件 && 音频组件
相机组件
先说相机组件,因为涉及到录视频等操作。而 Uniapp 也提供了 createCameraContext
,以便我们获取 Camera 的上下文。但是该 api 不能兼容 H5 及 App。如果要做多端涉及到 H5 这就很麻烦,可能需要调原生的摄像头(不赘叙)。
该组件也并非全屏摄像头,可以自己用样式控制。于是,给出一个简单 Demo。
代码语言:javascript复制<template>
<LayoutMain>
<template v-slot:mains>
<div class="carmare-wrapper">
<camera
:device-position="cameraConfig.devicePosition"
:flash="cameraConfig.flash"
binderror="error"
@error="handleOnError"
style="width: 100%; height: 300px"
></camera>
<button @click="handleTakePhoto">拍照</button>
<button @click="handleStartReord">开始录像</button>
<button @click="handleStopRecord">停止录像</button>
<button @click="handleSwitchDevicePosition">
切换摄像头朝向{{ cameraConfig.devicePosition }}
</button>
<button @click="handleSwitchFlashLight">
{{ cameraConfig.flash }}闪光灯
</button>
<div v-if="photoList.length > 0">
已拍出的照片
<div v-for="(item, index) in photoList" :key="index">
<img :src="item" alt="" />
</div>
</div>
<div v-if="videoSrc">
已录制的视频
<video :src="videoSrc" style="width: 100px; height: 100px"></video>
</div>
</div>
</template>
</LayoutMain>
</template>
<script lang="ts">
import { onReady } from "@dcloudio/uni-app";
import { defineComponent, reactive, ref, Ref } from "vue";
import LayoutMain from "@/layout/layoutMain.vue";
export default defineComponent({
setup() {
let carmareContext: any;
let videoSrc: Ref<string> = ref("");
let currentFlashLightStatus = 0;
const statusList = ["off", "on", "auto"];
const cameraConfig: any = reactive({
devicePosition: "back",
flash: statusList[currentFlashLightStatus],
});
onReady(() => {
carmareContext = uni.createCameraContext();
});
const photoList: Ref<string[]> = ref([]);
return {
cameraConfig,
photoList,
videoSrc,
handleOnError: (error: any) => {
console.error("handleOnError-eerror", error);
},
handleTakePhoto: () => {
carmareContext.takePhoto({
quality: "high",
success: (res: any) => {
console.info("res", res);
photoList.value.push(res.tempImagePath);
},
});
},
handleSwitchDevicePosition: () => {
console.info("cameraConfig.devicePosition");
cameraConfig.devicePosition =
cameraConfig.devicePosition === "back" ? "front" : "back";
},
handleSwitchFlashLight: () => {
const lastStatus = statusList.length - 1;
console.info("333handleSwitchFlashLight");
if (currentFlashLightStatus < lastStatus) {
cameraConfig.flash = statusList[(currentFlashLightStatus = 1)];
} else {
currentFlashLightStatus = 0;
cameraConfig.flash = statusList[currentFlashLightStatus];
}
},
// 开始录像
handleStartReord: () => {
carmareContext.startRecord({
success: (res: any) => {
console.log("handleStartReord-success", res);
uni.showToast({
title: "开始录像",
});
},
fail: (error: any) => {
console.error("handleStartReord-error", error);
},
});
},
// 停止录像
handleStopRecord: () => {
carmareContext.stopRecord({
success: (res: any) => {
console.log("handleStopRecord-success", res);
uni.showToast({
title: "停止录像",
});
videoSrc.value = res.tempVideoPath;
},
fail: (error: any) => {
console.error("handleStopRecord-error", error);
},
});
},
};
},
components: { LayoutMain },
});
</script>
在微信开发者工具上点击真机调试,效果如图:
音频组件
同样,Uniapp也提供了 createInnerAudioContext()
,创建并返回内部 audio 上下文 innerAudioContext
对象。
该 api 可兼容 H5 及 App,在 IOS 端该组件支持的格式会比较少些,仅支持m4a、wav、mp3、aac、aiff、caf格式。但在安卓端会相对多些。同样也是给出简答 Demo 用于调试。
代码语言:javascript复制<template>
<LayoutMain>
<template v-slot:container>
<button @click="handleStartRecord">开始录音</button>
<button @click="handleEndRecord">结束录音</button>
<button @click="handlePlay">播放录音</button>
<button @click="handlePausePlay">暂停播放录音</button>
<button @click="handlePausePlay">暂停播放录音</button>
<button @click="handleEndPlay">结束播放录音</button>
<div>
操作记录
<div v-for="(item, index) in operateRecordList" :key="index">
{{ item }}
</div>
</div>
</template>
</LayoutMain>
</template>
<script lang="ts">
import { ref, onMounted, Ref } from "vue";
export default {
data() {
return {};
},
setup() {
const operateRecordList: Ref<string[]> = ref([]);
// let getRecorderManager;
let recorderManager: any;
let innerAudioContext: any;
let voicePath: string;
onMounted(() => {
const current = (recorderManager = uni.getRecorderManager());
operateRecordList.value.push("prending");
current.onError(function (e: unknown) {
uni.showToast({
title: "getRecorderManager.onError",
});
console.error("getRecorderManager.onError", e);
});
current.onStart(function () {
operateRecordList.value.push("开始录音");
});
current.onStop(function (res: any) {
operateRecordList.value.push("结束录音");
console.log("recorder stop" JSON.stringify(res));
voicePath = res.tempFilePath;
});
current.onPause(function () {
operateRecordList.value.push("暂停录音");
});
});
onMounted(() => {
const current = (innerAudioContext = uni.createInnerAudioContext());
current.obeyMuteSwitch = false;
uni.setInnerAudioOption({
obeyMuteSwitch: false,
});
current.onError((res) => {
console.error("innerAudioContext-onError", res);
});
current.onPlay(() => {
operateRecordList.value.push("开始播放");
});
current.onPause(() => {
operateRecordList.value.push("暂停播放");
});
current.onStop(() => {
operateRecordList.value.push("结束播放");
});
});
return {
operateRecordList,
handleStartRecord: () => {
recorderManager.start({
duration: 60000, //录音的时长,单位 ms,最大值 600000(10 分钟)
format: "mp3",
});
},
handleEndRecord: () => {
recorderManager.stop();
},
handlePlay: () => {
innerAudioContext.src = voicePath;
innerAudioContext.play();
},
handleEndPlay: () => {
innerAudioContext.stop();
},
handlePausePlay: () => {
innerAudioContext.pause();
},
};
},
};
</script>
录音及播放等不好展示,故没有截图,感兴趣的朋友可以自行拷贝拿去用。
遇到的坑
layout
前面提到的 layout 组件在不同同事的电脑运行时发现,少数个别的会存在 layout 没有生效即页面没有包裹 layout层。
在main.ts中全局注册:
代码语言:javascript复制import { createApp } from "vue";
import Layout from "@/layout/layoutMain.vue";
import store from "@/store/index";
import App from "./App.vue";
const app = createApp(App);
app.use(store);
// 全局注册组件
app.component("Layout", Layout);
app.mount("#app");
在页面中使用(此处layout没生效,然而我的电脑生效):
代码语言:javascript复制<template>
<Layout>
<template v-slot:mains>
<div>分类页</div>
</template>
</Layout>
</template>
在确保环境相同、插件及各方面都没有影响的情况下也还是这样,这点现在还不清楚咋回事,希望有大佬可以指出,现在的方案是将 Layout
改名为 LayoutMain
即生效。(黑人问号?)
image
Uniapp 自带的 Image 组件的懒加载是不生效的,这点针对测试过,怀疑 lazy-load 这个属性就是个摆设 Q A Q。
目测得自己手动实现图片懒加载。
关于组件传值
假设有如下组件:
代码语言:javascript复制<template>
<div>{{ hello }}</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { Props } from "./interface";
export default defineComponent({
props: {
hello: {
type: String as PropType<Props["hello"]>,
default: 'fff',
},
}
})
</script>
可以看到 hello 为 String 类型,默认值为 fff。
但当 hello = undefined 时,hello 会显示空字符串 ""。 如果 hello 不传就会 "fff"。
最后
都看到这里了不点赞留言再走吗?
关注公众号:饼干前端,拉近你我距离(^▽^)