uniapp
引导
移动端开发技术演进
- 原生开发
- Android Java、Kotlin 语言
- iOS ObjectC、Swift 语言
- 性能体验更好
- 开发成本比较高
- Hybrid 混合开发
- 核心功能
- 非核心功能
- 例如:京东
- 核心功能:首页、分类、个人中心 采用原生
- 非核心功能:积分上传、宣传页 采用H5前端 web-view
- 原生页面与web页面之间的交互 JSBridge
- 跨平台开发
一套代码、多端运行
- 打包iOS
- 打包Andriod
- 打包H5
- 打包小程序
- 微信小程序
- 支付宝小程序
- 百度小程序
- 优点:极大降低开发成本
- 缺点:性能体验比原生开发稍差,但是,目前的硬件状态下,体验几乎一样了
跨端技术方案汇总
- vue技术栈
- uniapp DCloud 跨13端,iOS、Android、web、小程序 文档
- React技术栈
- Flutter 谷歌团队 iOS、Android、web
- Dart语言
文档
- Dart语言
- Electron 使用前端技术开发桌面应用 文档
uniapp介绍
uni-app是一个使用Vue.js开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、H5、以及各种小程序(微信/支付宝/百度/头条/QQ/钉钉/淘宝)、快应用等多个平台。
内核框架图
uniapp 发展史
读 you ni,是统一的意思,DCloud于2012年开始研发小程序技术,优化 webview 的功能和性能,并推出了 Hbuilder 开发工具
- 2015年,Dcloud 正式商用了自己的小程序,产品名为“流应用”
- DCloud持续在业内普及小程序理念,推进各大流量业务
- 开发者面对如此多的私有标准,造成混乱的局面
- 开发一个免费开源的框架,通过这个框架为开发者抹平各平台差异
uniapp 优缺点
-
优点:
-
uni-app 对前端开发人员比较友好,学习成本比较低,基于 vue.js
-
uni-app 使用vue的语法+小程序的标签和API。
-
使用 hbx 进行开发,hbx 对于 vue 语法等支持可以说是比较完备
-
拓展能力强,封装了H5+,支持 nvue,也支持原生Android,ios开发
-
-
缺点:
- 问世的时间还比较短,有很多地方还不是完善,坑很多
- 对于使用中的一些 bug 及问题,官方回应的不是很及时
开发工具
HBuilder X 安装时注册一个免费账号
prettier安装prettier插件,协助代码格式化
px2rpx像素转换响应式插件,协助写入的px到响应式rpx转换
构建、调试、部署
创建uni-app
在点击工具栏里的文件 -> 新建 -> 项目(快捷键Ctrl+N):
选择uni-app类型,输入工程名,选择模板,点击创建,即可成功创建。
uni-app自带的模板有 默认的空项目模板、Hello uni-app 官方组件和API示例,还有一个重要模板是 uni ui项目模板,日常开发推荐使用该模板,已内置大量常用组件。
开发者也可以使用cli方式创建项目,另见文档 (opens new window)。
差别是:HBuilderX创建的项目根目录就是源码,可直接编辑。uni-app的编译器在HBuilderX的插件目录下,跟随HBuilderX升级而一起升级。
如果开发者习惯于node模式的项目,对HBuilderX可视化方式感到困惑,可另行参考文档:## cli创建项目和HBuilderX可视化界面创建项目的区别
运行uni-app
-
浏览器运行:进入hello-uniapp项目,点击工具栏的运行 -> 运行到浏览器 -> 选择浏览器,即可体验 uni-app 的 web 版。
-
运行App到手机或模拟器:使用电压足够的usb端口连接手机,设置中开启USB调试,手机上允许电脑设备调试手机,进入hello-uniapp项目,点击工具栏的运行 -> 运行App到手机或模拟器,即可在该设备里面体验uni-app。
- 如手机或模拟器无法识别,请点击常见故障排查指南 (opens new window)。
- 华为手机投屏网页版
- 如需运行在苹果手机真机上,注意需使用自定义基座。详见(opens new window)
-
在微信开发者工具里运行:进入hello-uniapp项目,点击工具栏的运行 -> 运行到小程序模拟器 -> 微信开发者工具,即可在微信开发者工具里面体验uni-app。
注意:如果是第一次使用,需要先配置小程序ide的相关路径,才能运行成功。如下图,需在输入框输入微信开发者工具的安装路径。
注意:微信开发者工具需要开启服务端口 在微信工具的设置->安全。
Tips
- 如果是第一次使用,需要配置开发工具的相关路径。点击工具栏的运行 -> 运行到小程序模拟器 -> 运行设置,配置相应小程序开发者工具的路径。
- 微信小程序工具需要配置允许权限,不然HBuilder无法调用微信小程序开发工具的命令行
- 如果自动启动小程序开发工具失败,请手动启动小程序开发工具并将 HBuilderX 控制台提示的项目路径,打开项目。
运行的快捷键是Ctrl+R。
HBuilderX 还提供了快捷运行菜单,可以按数字快速选择要运行的设备:
如需调试,可参考:uni-app调试
发布uni-app
打包为原生App
在HBuilderX工具栏,点击发行,选择原生app-云端打包,如下图:
出现如下界面,点击打包即可。
云端打包支持安心打包,保护用户隐私,不会上传代码和证书,通过差量包制作方式实现安心打包。详见:ask.dcloud.net.cn/article/379…
云打包也支持cli模式,通过HBuilderX的cli方式(不是uni-app的cli),可以调用命令行打包,方便持续集成。详见:hx.dcloud.net.cn/cli/pack(op…
虽然安心打包已经满足需求,但如仍然希望自己使用 xcode 或 Android studio 进行离线打包,则在 HBuilderX 发行菜单里找到本地打包菜单,生成离线打包资源,然后参考离线打包文档操作:nativesupport.dcloud.net.cn/AppDocs/REA…。
App打包时,注意如果涉及三方sdk,需进行申请并在manifest.json里配置,否则相关功能无法使用。
iOS App打包需要向Apple申请证书。
发布为Web网站
-
在
manifest.json的可视化界面,进行如下配置(发行在网站根目录可不配置应用基本路径),此时发行网站路径是 www.xxx.com/h5,如:
hellouniapp.dcloud.net.cn (opens new window)
。
-
在HBuilderX工具栏,点击发行,选择网站-H5手机版,如下图,点击即可生成 H5 的相关资源文件,保存于 unpackage 目录。
注意
history模式发行需要后台配置支持,详见:history 模式的后端配置(opens new window)- 打包后,推荐使用前端网页托管服务,一键上传,自带CDN加速,无需购买虚拟机,无需安装nginx等;
- 若使用传统服务器部署,建议在服务器端开启
gzip压缩。参考网上的分享:juejin.im/post/5af003…
发布为微信小程序
-
申请微信小程序AppID,参考:微信教程 (opens new window)。
-
在HBuilderX中顶部菜单依次点击 "发行" => "小程序-微信",输入小程序名称和appid点击发行即可
如果手动发行,则点击发行按钮后,会在项目的目录 unpackage/dist/build/mp-weixin 生成微信小程序项目代码。在微信小程序开发者工具中,导入生成的微信小程序项目,测试项目代码运行正常后,点击“上传”按钮,之后按照 “提交审核” => “发布” 小程序标准流程,逐步操作即可,详细查看:微信官方教程 (opens new window)。
如果在发行界面勾选了自动上传微信平台,则无需再打开微信工具手动操作,将直接上传到微信服务器提交审核。
开发规范
vue语法规范 + uni内置组件 + uni.api() + uni-ui跨端UI库 + flex布局
| 责任 | |
|---|---|
| Vue2/3 | script标签的js逻辑、template模板语法 |
| uni内置组件 | 代替vue template的所有标签、用法对齐小程序 |
| uni.api() | 访问小程序、app、网络、手机设备等能力,用法对齐小程序 |
| uni-ui、uView | 官方或第三方库,解决uni内置组件不足的情况 |
| flex布局 | 需要手写的样式部分、为兼容多端运行、只用class做选择器 |
项目结构
一个uni-app工程,默认包含如下目录及文件:
│─components 符合vue组件规范的uni-app组件目录
│ └─component-a
│ └─component-a.vue 可复用的a组件
├─pages 业务页面文件存放的目录
│ ├─index
│ │ └─index.vue index页面
│ └─list
│ └─list.vue list页面
├─static 存放应用引用的本地静态资源(如图片、视频等)的目录,注意:静态资源只能存放于此
├─uni_modules 存放uni-ui组件、插件、一般自动生成
├─unpackage 运行或发行的编译结果、零时生成、项目迁移不用带走
├─main.js Vue初始化入口文件、可汇入插件
├─App.vue 应用配置,用来配置App全局样式以及监听 应用生命周期
├─manifest.json 配置应用名称、appid、logo、版本等打包信息
├─pages.json 配置页面路由、导航条、tabbar等页面类信息
└─uni.scss 这里是uni-app内置的常用样式变量
全局变量
公用模块
定义一个专用的模块,用来组织和管理这些全局的变量,在需要的页面引入。
示例如下: 在 uni-app 项目根目录下创建 common 目录,然后在 common 目录下新建 helper.js 用于定义公用的方法。
const websiteUrl = 'http://uniapp.dcloud.io';
const now = Date.now || function () {
return new Date().getTime();
};
const isArray = Array.isArray || function (obj) {
return obj instanceof Array;
};
export default {
websiteUrl,
now,
isArray
}
接下来在 pages/index/index.vue 中引用该模块
<script setup>
import helper from '../../common/helper.js';
helper.now()
</script>
这种方式维护起来比较方便,但是缺点就是每次都需要引入。
挂载 vue实例或原型
app.config.globalProperties/Vue.prototype
将一些使用频率较高的常量或者方法,直接扩展到app.config.globalProperties 上
示例如下: 在 main.js 中挂载属性/方法
//V3
const app = createSSRApp(App)
app.config.globalProperties.websiteUrl = 'http://uniapp.dcloud.io';
app.config.globalProperties.now = Date.now || function () {
return new Date().getTime();
};
//V2
import Vue from 'vue'
Vue.prototype.websiteUrl = helper.websiteUrl;
Vue.prototype.now = helper.now;
Vue.prototype.isArr = helper.isArr;
App.mpType = 'app'
const app = new Vue({
...App
})
app.$mount()
然后在 pages/index/index.vue 中调用
//v3
<script setup>
import { getCurrentInstance } from 'vue';
const { websiteUrl, now } = getCurrentInstance().appContext.config.globalProperties;
console.log('websiteUrl', websiteUrl);
console.log('now', now());
</script>
//v2
<script>
mounted(){
this.now();
this.websiteUrl
}
</script>
这种方式,只需要在 main.js 中定义好即可在每个页面中直接调用。
globalData
小程序中有个globalData概念,可以在 App 上声明全局变量。 Vue 之前是没有这类概念的,但 uni-app 引入了globalData概念
定义:App.vue
<script setup>
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app';
import { reactive, ref } from 'vue';
onLaunch(() => console.log('onLaunch'));
onShow(() => console.log('onShow'));
onHide(() => console.log('onHide'));
// let globalData = { ver: 5.4 }; 不修改,获取不到
</script>
//
<script>
export default {
globalData: { ver: 4.3 }
};
</script>
js中操作globalData的方式如下:
赋值:getApp().globalData.ver = 'test'
取值:console.log(getApp().globalData.ver) // 'test'
Vuex、pinia
公共模块、挂载到实例或原型、globalData都是非响应式的
// #ifdef VUE3
import {
createSSRApp
} from 'vue'
import * as Pinia from 'pinia';
export function createApp() {
const app = createSSRApp(App)
app.use(Pinia.createPinia());
return {
app,
Pinia, // 此处必须将 Pinia 返回
}
}
// #endif
// stores/food.js
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useFoodStore = defineStore('food', () => {
const food = ref({});
return {
food
}
});
<template>
{{food}}
</template>
<script setup>
import { useFoodStore } from '@/store/food.js';
import { storeToRefs } from 'pinia';
const foodStore = useFoodStore();
const { food } = storeToRefs(foodStore);
</script>
全局文件
pages.json
类似微信小程序中app.json+pageName.json,注意定位权限申请等原属于app.json的内容,在uni-app中是在manifest中配置
manifest.json
UNI-APP(AppID)下载HBX工具时,注册且登录了账号,就会存在,不在重新获取
微信小程序配置:填入小程序AppID(wxf34e69dfcc966870)
uni.scss
全局scss变量堆放处,需要在 HBuilderX 里面安装 scss 插件(【工具】【插件安装】【scss/sass编译】)
App.vue
根组件,小程序主程,调用应用生命周期函数、配置全局样式、配置全局的存储globalData,小程序的生命周期钩子vue3写法,需要从@dcloudio/uni-app引入对应的钩子函数,vue2可直接使用小程序的钩子,uniapp支持小程和vue的生命周期钩子
main.js
uni-app 的入口文件,主要作用是初始化vue实例、定义全局组件、安装插件如 vuex
vite.config.js
网络请求(request、uploadFile、downloadFile等)在浏览器存在跨域限制
import { defineConfig } from 'vite';
import uni from '@dcloudio/vite-plugin-uni';
export default defineConfig({
plugins: [uni()],
server: {
port: 8080,
proxy: {
"/api": {//组件请求是使用 /api/..
//代理到
target: "http://localhost:9001",
changeOrigin: true, //开启代理
//别名替换
// rewrite: (path) => path.replace(/^\/api/, ""),
// ws: true, //socket协议开启
},
"/book": {
//目标代理到线上服务器
target: "https://api.zhuishushenqi.com",
changeOrigin: true, //开启代理
//别名替换
// rewrite: (path) => path.replace(/^\/api/, ""),
// ws: true, //socket协议开启
},
},
},
});
组件
内置组件
对齐微信内置组件,直接使用无需引入
扩展组件
uni-ui
【选择组件】【点击安装】【copy代码】【查询文档】
uView
暂不支持vue3(2022-11-29)、等等就有了
API
对齐小程序api用法,wx转uni
差异:回调获取结果的api都支持Promise使用了
uni
.request({
url: "https://www.example.com/request",
})
.then((res) => {
// 此处的 res 参数,与使用默认方式调用时 success 回调中的 res 参数一致
console.log(res.data);
})
.catch((err) => {
// 此处的 err 参数,与使用默认方式调用时 fail 回调中的 err 参数一致
console.error(err);
});
插件
获取插件方式
- uni-app插件市场
- 【工具】【插件安装】
举例 :使用SearchBar 搜索栏 、动画插件
条件编译
Uniapp存在一些无法跨平台的情况,此时通过条件编译来解决,让各个平台的独特的原生代码在指定平台运行,通过 #ifdef、#ifndef 的方式在一个工程里优雅的完成了平台个性化实现。
**写法:**以 #ifdef 或 #ifndef 加 %PLATFORM% 开头,以 #endif 结尾。
- #ifdef:仅在某平台存在
- #ifndef:除了某平台均存在
- %PLATFORM%:平台名称
| 条件编译写法 | 说明 |
|---|---|
| #ifdef APP-PLUS 需条件编译的代码 #endif | 仅出现在 App 平台下的代码 |
| #ifndef H5 需条件编译的代码 #endif | 除了 H5 平台,其它平台均存在的代码 |
| #ifdef H5 || MP-WEIXIN 需条件编译的代码 #endif | 在 H5 平台或微信小程序平台存在的代码 |
%PLATFORM% 可取值如下:
| 值 | 生效条件 |
|---|---|
| APP-PLUS | App |
| H5 | H5 |
| MP-WEIXIN | 微信小程序 |
支持条件编译的文件
- .vue
- .js
- .css
- pages.json
- 各预编译语言文件,如:.scss、.less、.stylus
注意:
- 条件编译是利用注释实现的,在不同语法里注释写法不一样,js使用
// 注释、css 使用/* 注释 */、vue 模板里使用<!-- 注释 -->;
JS的条件编译
// #ifdef 平台名称
平台特有的API实现
// #endif
结构的条件编译
<!-- #ifdef 平台名称 -->
平台特有的组件
<!-- #endif -->
示例,如下公众号关注组件仅会在微信小程序中出现:
<view>
<view>微信公众号关注组件</view>
<view>
<!-- uni-app未封装,但可直接使用微信原生的official-account组件-->
<!-- #ifdef MP-WEIXIN -->
<official-account></official-account>
<!-- #endif -->
</view>
</view>
样式的条件编译
/* #ifdef 平台名称 */
平台特有样式
/* #endif */
注意: 样式的条件编译,无论是 css 还是 sass/scss/less/stylus 等预编译语言中,必须使用 /*注释*/ 的写法。
pages.json 的条件编译
下面的页面,只有运行至 App 时才会编译进去。
static 目录的条件编译
在不同平台,引用的静态资源可能也存在差异,通过 static 的的条件编译可以解决此问题,static 目录下新建不同平台的专有目录(平台名称,但字母均为小写),专有目录下的静态资源只有在特定平台才会编译进去。
如以下目录结构,a.png 只有在微信小程序平台才会编译进去,b.png 在所有平台都会被编译。
┌─static
│ ├─mp-weixin
│ │ └─a.png
│ └─b.png
├─main.js
├─App.vue
├─manifest.json
└─pages.json
注意
- Android 和 iOS 平台不支持通过条件编译来区分,如果需要区分 Android、iOS 平台,请通过调用 uni.getSystemInfo 来获取平台信息。支持
ifios、ifAndroid代码块,可方便编写判断。
跨端注意
uniapp虽然使用vue语法,考虑到跨端、一些目前为止(2022-11-28)需要注意的地方
- 不支持*选择器、小程序无效
- 习惯约定:body的元素选择器请改为page,同样,div和ul和li等改为view、span和font改为text、a改为navigator、img改为image
- App、小程序未启用 scoped,H5端默认启用 scoped,推荐style内 添加scoped
- 不要使用新出来的的css写法,App会出现异常,单位使用rpx解决适配(有最大尺寸限定960)
url(//alicdn.net)等路径,改为url(https://alicdn.net),因为在App端//是file协议- 路由推荐小程序,不支持vue-router,路由接参数同小程序
- 生命周期: 组件支持vue生命周期、页面(pages)支持小程序+vue生命周期
- 不支持dom操作,v-html不可用,物理键盘事件不存在,不能操作document、window、localstorage、cookie,通过节点操作实现
- template内的、返单引 语法发布到小程序有问题
- 暂不支持vue3动画
- 小程序暂不支持v3 的$attrs,三端只支持组件内部defineEmits|defineProps(['xx-xx-xx'])的写法
- 编译到任意平台时,
static目录下的文件均会被完整打包进去,且不会编译。非static目录下的文件(vue、js、css 等)只有被引用到才会被打包编译进去。 static目录下的js文件不会被编译,如果里面有es6的代码,不经过转换直接运行,在手机设备上会报错。css、less/scss、js等资源不要放在static目录下,建议这些公用的资源放在自建的common目录下。
项目
数据mock
安装包
npm init
npm json-server -S
配置环境package.json
"scripts": {
"mock": "json-server --watch --port 3333 ./data.json"
},
启动
npm run mock
设计图测量工具
图标字体制作
- 图标字体: 使用IcoMoon将SVG格式的图标转换生成图标字体及样式
- 进入icoMoon官网: icomoon.io/
- 点击右上角【icoMoonApp】进入处理页面
- 点击左上角【ImportIcons】, 选择resource\SVG*.svg, 上传显示到页面
- 在页面选择所有svg, 点击右下角【GenerateFont】生成图标字体样式
- 点击【PreFerences】可更名, 点击右下角【download】下载到本地
- 解压zip包, 访问demo.html测试
- 我们项目需要的是fonts和style.css
高清适配
/*mixins.scss*/
@mixin bg-image($url) {
background-image: url('@/static/images/'+$url+'@2x.png');
@media (min-device-pixel-ratio: 3), (-webkit-min-device-pixel-ratio: 3) {
background-image: url('@/static/images/'+$url+'@3x.png');
}
}
HbuildX写px转换成rpx
- 通过插件实现: 安装好插件后,写px会有提示,优点是无需技术,缺点是每次都需要转换
- pxCook 标注设计稿时,使用rpx标注,无需转换
npm使用
npm init
npm i moment -S
<temlate>
{{moment(数据).format('YYYY-MM-DD HH:mm:ss')}}
</temlate>
<script setup>
import moment from 'moment';
</script>
数据请求封装(hooks)
const baseUrl = 'http://localhost:3333/';
import { ref } from 'vue';
const useRequest = (apiName, auto = true) => {
const data = ref({});
const run = () => uni.request({
// #ifdef H5
url: '/mock/' + apiName,
// #endif
// #ifndef H5
url: baseUrl + apiName
// #endif
}).then(
res => {
data.value = res.data
return res
}
)
auto && run()
return {
data,
run
}
}
export default useRequest;
节点操作
类似于dom操作,uniapp提供的是节点操作,语法对齐小程序,文档
//获取右边菜单每个item到顶部的距离
new Promise(resolve => {
let selectorQuery = uni.createSelectorQuery();
selectorQuery
.selectAll('.food-list')
.boundingClientRect(rects => {
// 如果节点尚未生成,rects值为[](因为用selectAll,所以返回的是数组),循环调用执行
if (!rects.length) {
setTimeout(() => {
getMenuItemTop();
}, 10);
return;
}
rects.forEach(rect => {
// 这里减去rects[0].top,是因为第一项顶部可能不是贴到导航栏(比如有个搜索框的情况)
data.menuItemPos.push(rect.top - rects[0].top);
resolve();
});
})
.exec();
});
//获取一个目标元素的高度
new Promise(resolve => {
let query = uni.createSelectorQuery();
query
.select('.' + elClass)
.fields(
{
size: true
},
res => {
// 如果节点尚未生成,res值为null,循环调用执行
if (!res) {
setTimeout(() => {
getElRect(elClass);
}, 10);
return;
}
data[dataVal] = res.height;
resolve();
}
)
.exec();
});
app端不隐藏tabbar
//app.vue 小程序,H5正常
onLaunch(async () => {
uni.hideTabBar();
});
//默认加载的页面goods 小程序,H5正常 ,app正常
onLoad(() => uni.hideTabBar());
scroll-view在H5端,不隐藏滚动条
.menu-wrapper {
flex: 0 0 160rpx;
width: 160rpx;
background: #f3f5f7;
/* #ifdef H5 */
::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
color: transparent;
background: transparent;
}
/* #endif */
}
过滤业务封装hooks
// hooks/useFilterRating.js
import { computed, ref, isRef } from "vue";
//ratings要求是个ref 外部遇见飞ref数据传入是可使用toRef
const useFilterRatings = (ratings) => {
const onlyContent = ref(false); //默认看所有
const selectType = ref(2); //全部
const togoSelectType = (st) => (selectType.value = st);
const switchOnlyContent = () => (onlyContent.value = !onlyContent.value);
//保证传入的ratings是个ref
const filterRatings = computed(() => {
if (!ratings.value) {
return;
}
// selectType: 0, 1, 2
// onlyContent: true false
return ratings.value.filter((rating) => {
if (selectType.value === 2) {
// 如果onlyContent为false, 直接返回true, 否则还要看text有没有值
return !onlyContent.value || !!rating.text;
} else {
// 既要比较type, 还要比较content
return (
selectType.value === rating.rateType &&
(!onlyContent.value || !!rating.text)
);
}
});
});
return {
filterRatings,
selectType,
switchOnlyContent,
onlyContent,
togoSelectType,
};
};
export default useFilterRatings;