uni-app

758 阅读12分钟

uniapp

引导

移动端开发技术演进

  1. 原生开发
    • Android   Java、Kotlin 语言
    • iOS       ObjectC、Swift 语言
    • 性能体验更好
    • 开发成本比较高
  2. Hybrid 混合开发
    • 核心功能
    • 非核心功能
    • 例如:京东
      • 核心功能:首页、分类、个人中心  采用原生
      • 非核心功能:积分上传、宣传页   采用H5前端  web-view
    • 原生页面与web页面之间的交互 JSBridge
  3. 跨平台开发

一套代码、多端运行

  • 打包iOS
  • 打包Andriod
  • 打包H5
  • 打包小程序
    • 微信小程序
    • 支付宝小程序
    • 百度小程序
  • 优点:极大降低开发成本
  • 缺点:性能体验比原生开发稍差,但是,目前的硬件状态下,体验几乎一样了

跨端技术方案汇总

  1. vue技术栈
    • uniapp   DCloud    跨13端,iOS、Android、web、小程序 文档
  2. React技术栈
    • Taro  京东团队      iOS、Android、web、小程序 文档
    • ReactNative   FaceBook    iOS、Android 文档
  3. Flutter     谷歌团队 iOS、Android、web
  4. Electron   使用前端技术开发桌面应用 文档

uniapp介绍

uni-app是一个使用Vue.js开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、H5、以及各种小程序(微信/支付宝/百度/头条/QQ/钉钉/淘宝)、快应用等多个平台。

image-20211015153116722

内核框架图

uniapp 发展史

读 you ni,是统一的意思,DCloud于2012年开始研发小程序技术,优化 webview 的功能和性能,并推出了 Hbuilder 开发工具

  • 2015年,Dcloud 正式商用了自己的小程序,产品名为“流应用”
  • DCloud持续在业内普及小程序理念,推进各大流量业务
  • 开发者面对如此多的私有标准,造成混乱的局面
  • 开发一个免费开源的框架,通过这个框架为开发者抹平各平台差异

uniapp 优缺点

  • 优点:

    1. uni-app 对前端开发人员比较友好,学习成本比较低,基于 vue.js

    2. uni-app 使用vue的语法+小程序的标签和API。

    3. 使用 hbx 进行开发,hbx 对于 vue 语法等支持可以说是比较完备

    4. 拓展能力强,封装了H5+,支持 nvue,也支持原生Android,ios开发

  • 缺点:

    1. 问世的时间还比较短,有很多地方还不是完善,坑很多
    2. 对于使用中的一些 bug 及问题,官方回应的不是很及时

开发工具

HBuilder X 安装时注册一个免费账号

prettier安装prettier插件,协助代码格式化

px2rpx像素转换响应式插件,协助写入的px到响应式rpx转换

构建、调试、部署

创建uni-app

在点击工具栏里的文件 -> 新建 -> 项目(快捷键Ctrl+N):

img

选择uni-app类型,输入工程名,选择模板,点击创建,即可成功创建。

uni-app自带的模板有 默认的空项目模板、Hello uni-app 官方组件和API示例,还有一个重要模板是 uni ui项目模板,日常开发推荐使用该模板,已内置大量常用组件。

img

开发者也可以使用cli方式创建项目,另见文档 (opens new window)

差别是:HBuilderX创建的项目根目录就是源码,可直接编辑。uni-app的编译器在HBuilderX的插件目录下,跟随HBuilderX升级而一起升级。

如果开发者习惯于node模式的项目,对HBuilderX可视化方式感到困惑,可另行参考文档:## cli创建项目和HBuilderX可视化界面创建项目的区别

运行uni-app

  1. 浏览器运行:进入hello-uniapp项目,点击工具栏的运行 -> 运行到浏览器 -> 选择浏览器,即可体验 uni-app 的 web 版。

    img
  2. 运行App到手机或模拟器:使用电压足够的usb端口连接手机,设置中开启USB调试,手机上允许电脑设备调试手机,进入hello-uniapp项目,点击工具栏的运行 -> 运行App到手机或模拟器,即可在该设备里面体验uni-app。

    img
  1. 在微信开发者工具里运行:进入hello-uniapp项目,点击工具栏的运行 -> 运行到小程序模拟器 -> 微信开发者工具,即可在微信开发者工具里面体验uni-app。

    img

    注意:如果是第一次使用,需要先配置小程序ide的相关路径,才能运行成功。如下图,需在输入框输入微信开发者工具的安装路径。

    img

    注意:微信开发者工具需要开启服务端口 在微信工具的设置->安全。

Tips

  • 如果是第一次使用,需要配置开发工具的相关路径。点击工具栏的运行 -> 运行到小程序模拟器 -> 运行设置,配置相应小程序开发者工具的路径。
  • 微信小程序工具需要配置允许权限,不然HBuilder无法调用微信小程序开发工具的命令行
  • 如果自动启动小程序开发工具失败,请手动启动小程序开发工具并将 HBuilderX 控制台提示的项目路径,打开项目。

运行的快捷键是Ctrl+R

HBuilderX 还提供了快捷运行菜单,可以按数字快速选择要运行的设备:

img

如需调试,可参考:uni-app调试

发布uni-app

打包为原生App

在HBuilderX工具栏,点击发行,选择原生app-云端打包,如下图:

img

出现如下界面,点击打包即可。

img

云端打包支持安心打包,保护用户隐私,不会上传代码和证书,通过差量包制作方式实现安心打包。详见: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网站

  1. manifest.json
    

    的可视化界面,进行如下配置(发行在网站根目录可不配置应用基本路径),此时发行网站路径是 www.xxx.com/h5,如:

    hellouniapp.dcloud.net.cn (opens new window)

    img
  2. 在HBuilderX工具栏,点击发行,选择网站-H5手机版,如下图,点击即可生成 H5 的相关资源文件,保存于 unpackage 目录。

img img

注意

发布为微信小程序

  1. 申请微信小程序AppID,参考:微信教程 (opens new window)

  2. 在HBuilderX中顶部菜单依次点击 "发行" => "小程序-微信",输入小程序名称和appid点击发行即可

    img

如果手动发行,则点击发行按钮后,会在项目的目录 unpackage/dist/build/mp-weixin 生成微信小程序项目代码。在微信小程序开发者工具中,导入生成的微信小程序项目,测试项目代码运行正常后,点击“上传”按钮,之后按照 “提交审核” => “发布” 小程序标准流程,逐步操作即可,详细查看:微信官方教程 (opens new window)

如果在发行界面勾选了自动上传微信平台,则无需再打开微信工具手动操作,将直接上传到微信服务器提交审核。

开发规范

vue语法规范 + uni内置组件 + uni.api() + uni-ui跨端UI库 + flex布局

责任
Vue2/3script标签的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);
  });

插件

获取插件方式

举例 :使用SearchBar 搜索栏 、动画插件

image-20211015173052943

条件编译

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-PLUSApp
H5H5
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 时才会编译进去。

img

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 来获取平台信息。支持ifiosifAndroid代码块,可方便编写判断。

跨端注意

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 的代码,不经过转换直接运行,在手机设备上会报错。
  • cssless/scssjs 等资源不要放在 static 目录下,建议这些公用的资源放在自建的 common 目录下。

项目

数据mock

安装包

npm init
npm json-server -S

配置环境package.json

"scripts": {
  "mock": "json-server --watch --port 3333  ./data.json"
},

启动

npm run mock

设计图测量工具

PxCook

稿定

图标字体制作

  1. 图标字体: 使用IcoMoon将SVG格式的图标转换生成图标字体及样式
  2. 进入icoMoon官网: icomoon.io/
  3. 点击右上角【icoMoonApp】进入处理页面
  4. 点击左上角【ImportIcons】, 选择resource\SVG*.svg, 上传显示到页面
  5. 在页面选择所有svg, 点击右下角【GenerateFont】生成图标字体样式
  6. 点击【PreFerences】可更名, 点击右下角【download】下载到本地
  7. 解压zip包, 访问demo.html测试
  8. 我们项目需要的是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;

常见业务

参考

自定义tabbar

登录流程

获取用户信息

获取用户手机

获取地址

付款流程

分包

消息订阅