大家好,我是前端dog君,一名95后前端小兵。2019年毕业于北京化工大学,天津人,不知道有校友和老乡嘛?对前端的热爱,让我们在此相聚,希望这篇文章,能帮助到您,也同时希望能交到志同道合的小伙伴,共同发展,一起进步。我的微信号dm120225,备注简书,期待您的光临。
最近这段时间,应公司需求,需要开发一款App,但是公司只有前端团队,可能大家一致认为,只要是关于GUI的开发前端都可以搞定吧,于是,dog君就接下了App的活儿,开始了App探索之旅。
在做技术选型的过程中,dog君调研了几种混合App开发方案,像mui、Ionic、React-native、cordova、uni-app、Flutter等等,从团队技术栈和上手速度的角度来说,我们最终选用了uni-app,vue的语法,小程序的API,来开发一款App应用。下面跟随dog君的脚步,让我们一起来体验uni-app开发跨端应用。
如何学习新技术
在我们的前端圈,新技术层出不穷,每天都会有新轮子的产生,那么在开始之前呢,先来谈下当我们需要使用一种没接触过的技术进行日常开发工作时,我们应该如何来学习,dog君是这样做的:
- 找一个关于uni-app开发的视频开始学习,大概搞清楚如何使用及流程是什么样的。
- github上找一个比较好的uni-app开发的项目,学习一下他们是如何开发的,同时不断的查阅,实践,扫盲,踩坑。
- 开始搭建uni-app 项目架构,尽量不脱离现有的开发习惯,降低学习成本。学习他人的项目架构,靠近社区,使用现有的解决方案。
- 搭建完毕后,根据需求开发出一款应用,团队之间互相磨合。
- 项目完毕后,整理总结,输出一份文档,方便其他同事维护。
什么是uni-app
uni-app
是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、Web(响应式)、以及各种小程序(微信/支付宝/百度/头条/QQ/钉钉/淘宝)、快应用等多个平台。
搭建开发环境
uni-app是DCloud公司产品,DCloud公司为我们提供了HBuilder X集成开发环境,开箱即用,无需配置nodejs。
我们首先下载HBuilder X
- HBuilderX:官方IDE下载地址
HBuilderX是通用的前端开发工具,但为uni-app做了特别强化。
下载App开发版,可开箱即用;如下载标准版,在运行或发行uni-app时,会提示安装uni-app插件,插件下载完成后方可使用。这里我们选择App开发版。
下载完毕后,我们打开打开HBuilder X,出现以下页面
好,到这里你会惊奇的发现,我们的开发环境已经搭建完毕。
这里呢需要提醒大家一点,在dog君使用过程中呢,发现第一次下载后的HBuilder X存在打不开的问题,当然,小伙伴们遇到的情况也各不相同,如果遇到一些奇奇怪怪的问题,我们重新启动下电脑,或者重新启动下HBuilder X,所有的问题就都迎刃而解了。
创建uni-app项目
我们点击工具栏的文件 -> 新建 -> 项目
选择uni-app类型,输入工程名,选择模板,点击创建,即可成功创建。
uni-app自带的模板有 Hello uni-app ,是官方的组件和API示例。还有一个重要模板是 uni ui项目模板,日常开发推荐使用该模板,已内置大量常用组件。
此时,我们的uni-app 项目已经创建完毕。
运行uni-app
我们的uni-app强大之处,在于一套代码,多端运行,目前已支持Android、IOS、Web、微信小程序、支付宝、百度、字节跳动、QQ、360小程序和快应用。这里呢,我们讨论下运行到安卓、Web和微信小程序的步骤。
1.运行到浏览器
选择我们刚才创建的项目,点击工具栏的运行 -> 运行到浏览器 -> 选择浏览器,即可在浏览器里面体验uni-app 的 H5 版。
2.运行到安卓手机
选择我们刚才创建的项目,连接手机,开启USB调试,进入hello-uniapp项目,点击工具栏的运行 -> 真机运行 -> 选择运行的设备,即可在该设备里面体验uni-app。
这里dog君总结下我们可能遇到的问题及解决方案:
1.如何开启手机USB调试?
开启手机usb调试模式
2.开启后并连接手机,但是HBuilder X并没有反应?
点击工具栏运行,找到运行到手机或模拟器,反复的触摸,直到显示连接为止。实在不行,重启HBuilder X或电脑。
如手机无法识别,请点击菜单运行-运行到手机或模拟器-真机运行常见故障排查指南。 注意目前开发App也需要安装微信开发者工具。
3.运行到微信开发者工具
选择我们刚才创建的项目,点击工具栏的运行 -> 运行到小程序模拟器 -> 微信开发者工具,即可在微信开发者工具里面体验uni-app。
这里有一些需要注意的点:
如果是第一次使用,需要先配置微信开发者工具的相关路径,才能运行成功。如下图,点击工具栏的运行-> 运行到小程序模拟器 -> 运行设置,找到微信开发者工具路径。 若HBuilderX不能正常启动微信开发者工具,需要开发者手动启动,然后将uni-app生成小程序工程的路径拷贝到微信开发者工具里面,在HBuilderX里面开发,在微信开发者工具里面就可看到实时的效果。
好,到此为止呢,我们已经成功的将项目运行在多个平台了。
发布uni-app
打包为远程App(云端)
在HBuilderX工具栏,点击发行,选择原生app-云端打包,如下图:
配置相关项,点击打包即可。
发布为H5
1.在 manifest.json
的可视化界面,进行如下配置(发行在网站根目录可不配置应用基本路径),此时发行网站路径是 www.xxx.com/h5,如:https://hellouniapp.dcloud.net.cn。
2.在HBuilderX工具栏,点击发行,选择网站-H5手机版,如下图,点击即可生成 H5 的相关资源文件,保存于 unpackage 目录。
发布为微信小程序
1.申请微信小程序AppID
2.在HBuilderX中顶部菜单依次点击 "发行" => "小程序-微信",输入小程序名称和appid点击发行即可在 unpackage/dist/build/mp-weixin 生成微信小程序项目代码。
3.在微信小程序开发者工具中,导入生成的微信小程序项目,测试项目代码运行正常后,点击“上传”按钮,之后按照 “提交审核” => “发布” 小程序标准流程,逐步操作即可
到此为止,我们的uni-app项目初体验已经完毕。
uni-app开发注意事项
uni-app 是一款使用vue的语法,小程序的api开发多端应用的框架,uni-app本身并不难,难得是我们在开发多端应用的过程中需要遵循各端的规则,这不是uni-app在技术层面可以抹平的:
- 比如H5端的浏览器有跨域限制;
- 比如微信小程序会强制要求https链接,并且所有要联网的服务器域名都要配到微信的白名单中;
- 比如App端,iOS对隐私控制和虚拟支付控制非常严格;
- 比如App端,Android、国产rom各种兼容性差异,尤其是因为谷歌服务被墙,导致的push、定位等开发混乱的坑;
- 如果你的App要使用三方sdk,比如定位、地图、支付、推送...还要遵循他们的规则和限制;
dog君在使用uni-app开发App的过程中踩了很多坑,在这里分享给大家:
- 开发规范:使用vue单文件规范,组件化和接口能力靠近小程序规范
- 使用flex布局开发
- 不要在static目录下写less scss 和js,该目录下HBuilder并不会打包,有需要,最好放在common目录下。
- @指向的是项目根目录
- 运行环境通过process.env.NODE_ENV来判断,默认存在开发环境和生产环境,当然,也可以自定义配置测试环境。
- uni-app的尺寸单位有px rpx rpx为响应式单位,750rpx为屏幕宽度,我们再和设计师交流过程中,跟设计师说明下视觉稿使用的是iPhone 6设计稿作为标准(很重要)。
- uni-app中css不能使用 * page元素即为body
- uni-app中导航栏和底部选项卡高度不可修改
- 在css中使用背景图片,引用路径推荐使用~@/static/logo.png
- uni-app的vue页面在Android低端机上只有css浏览器兼容问题,因为vue页面仍然渲染在webview中
- uni-app支持使用npm,但是为多端考虑,建议优先从uni-app插件市场获取插件使用。
- 各端的特性,我们使用条件编译来满足各端规则。
搭建项目架构
dog君开发的是App,个人认为App的规则还是比较多的,基本上覆盖掉了小程序和H5。那么我们就以开发一个App为例,搭建uni-app项目架构,一起来看看开发一款完整App应用,都需要有什么。
我们先来梳理一下打开一款App,它的运行流程和界面展示大概是什么样子的,会涉及到什么。
1.首先,扫码下载App到手机(非上架应用,用户通常会使用微信来扫描,进入H5页面,作用是引导用户点击右上角,从浏览器中打开)
2.用户打开浏览器,下载App
3.下载完毕后,打开安装包,弹出安装App界面,安装App到手机上。
4.安装完成,手机上会出现该应用(应用图标,应用名称)
5.打开App,首次打开会弹出服务协议(服务协议配置)
6.看到App启动图(安卓使用的是.9.png格式启动图)
7.首次打开,进入App引导页(检查应用是否存在更新)
8.打开App,检查用户是否登录,已登录,进入首页,未登录,进入登录页(这里我们以未登录为例)
9.进入登录页,输入账号密码,点击登录,进入首页(vuex存储用户信息,请求封装,全局错误异常处理,各种工具函数)
10.进入首页,查看页面展示(导航栏配置,底部选项卡配置)
11.进入列表页(下拉刷新,上拉加载,全局loading,暂无数据组件,返回顶部组件,检查网络是否连接组件,icon组件)
12.旋转手机,应用仍竖屏显示(配置竖屏锁定)
13.点击列表页,进入详情页(UI库,表单校验,图表库)
好,到此为止,我们梳理下搭建一个uni-app项目架构需要什么。
- 扫码进入App下载的H5页面
- 应用图标,应用名称
- 服务协议配置
- 安卓App .9.png格式启动图制作
- App引导页
- app更新机制
- uni-app使用npm包,构建vuex moment等常用js库
- uni-app全局配置 导航栏、底部选项卡等
- 请求封装,环境配置,http状态码及自定义状态码,restful风格接口规范及格式,请求超时处理,请求异常错误处理
- 工具函数封装
- 上拉刷新、下拉加载,全局loading,暂无数据组件,返回顶部组件,检查网络连接组件,uni-app集成iconfont,配置组件自动引入。
- 配置竖屏锁定
- 常用UI库,图表库,表单校验等相关uni-app插件。
好,下面我们就来针对以上需要解决的问题,一个一个的给出解决方案,最终搭建起我们的uni-app项目架构。
扫码进入下载App的H5页面
这个在此不详细展开,因为不同公司对于推广方式是不一样的,我们新建一个html文件,可以使用以下代码来判断是否在微信浏览器打开,如果是,隐藏下载按钮,同时展示一层mask蒙版,提示用户点击右上角从浏览器打开,即可,进入浏览器,判断不是在微信浏览器打开,下载按钮展示出来,点击下载按钮,手机浏览器开始下载apk安装包。
function is_weixn(){
var ua = navigator.userAgent.toLowerCase();
if(ua.match(/MicroMessenger/i)=="micromessenger") {
return true;
} else {
return false;
}
}
应用图标,应用名称
uni-app配置应用图标有一定的要求,我们可以在创建的项目中找到mainfest.json文件,点击App图标配置,如图所示
和设计师沟通好,设计1024*1024尺寸的png格式图片即可,可以自动生成图标。
关于应用名称,与产品经理沟通完毕后点击基础配置,填写应用名称即可,如图所示。
安卓App.9.png制作
我们的启动图通常是一张图片,为避免在不同尺寸不同分辨率下的手机导致启动图拉伸或压缩导致变形,安卓平台出现了可以适配各种尺寸的一种图片格式“.9.png”。这是一种特殊的图片格式,它可以指定特定的区域进行拉伸而不失真。
关于.9.png图片的制作,我们可以参考下面这篇文章
Android Studio制作.9.png图片
从下面开始呢,我们首先使用HBuilder X创建一个uni-app项目,使用默认模板,清空文件内容,保持一个空架子的状态,我们开始基于此项目搭建uni-app项目架构。
首先我们来看下项目目录结构
── App.vue 根组件
├── README.md 项目文档
├── api 存放相关接口
├── components 存放相关组件
├── config 存放相关配置
├── eventBus.js 事件总线
├── filters 存放相关过滤器
├── main.js 入口文件
├── manifest.json 打包配置文件
├── node_modules 项目依赖包
├── package-lock.json 依赖包版本
├── package.json 包配置文件
├── pages 存放页面
├── pages.json 页面配置
├── static 存放静态资源
├── store vuex目录
├── styles 全局样式目录
├── uni.scss 全局scss变量
├── unpackage 项目打包文件
└── utils 相关工具函数
服务协议配置
应用上传应用宝,需要在下载首页展示用户协议和隐私政策弹窗提醒, 以及在应用内有查看的位置, 登录或者注册页面有同意服务协议和隐私政策的提醒.
用户协议和隐私政策弹窗提醒:
1.在uniapp项目manifest.json文件的源码视图中,找到app-plus
2.在app-plus节点下,添加privacy
"privacy" : {
"prompt" : "template",
"template" : {
//prompt取值为template时有效,用于配置模板提示框上显示的内容
"title" : "服务协议和隐私政策",
"message" : " 尊敬的用户,欢迎您注册成为本应用用户,在注册前请您仔细阅读<a href='这里需要写个单独展示用户协议的jsp或者HTML页面'>《用户协议》</a>及<a href='这里需要写个单独展示隐私政策的jsp或者HTML页面''>《隐私政策》</a>,了解我们对您使用我们APP制定的规则,您个人信息的处理以及申请权限的目的和使用范围。<br/> 经您确认后,本用户协议和隐私权政策即在您和本应用之间产生法律效力。请您务必在注册之前认真阅读全部服务协议内容,如有任何疑问,可向本应用客服咨询。",
"buttonAccept" : "我知道了",//继续下一步
"buttonRefuse" : "暂不使用"//退出下载
}
}
提示:
在第一次下载应用时出现此弹窗;
不能真机测试.测试需打包;
App引导页
我们在pages目录下新建4个页面,分别为init,guide,index,login,在pages.json中会自动生成页面路径,将init页面放在首位。
当我们进入到应用时,首先进入init页面,onLoad钩子下去判断当前用户是否为第一次进入,如果是则跳转到guide引导页面,如果不是,则再去判断用户是否登录,如果已登录则跳转到首页,如果未登录,则跳转到登录页面。这里init作用相当于做一个路由中转的作用。具体的实现方案我们可以参考下面的方案
Uni-App 启动页和引导页介绍
app更新机制
App端的升级,又分为整包更新和资源热更新两种。
- 整包更新,即常规的整个App安装包重新下载安装。
- 资源热更新,即App并重新安装,里面的js等前端代码进行更新。
资源热更新实现起来比较麻烦,这里我们重点介绍下整包更新。
Android App,可以直接下载新的apk,只要包名和证书不变,就可以覆盖安装。
接口约定
如下数据接口约定仅为示例,开发者可以自定义接口参数。
请求地址:https://www.example.com/update
请求方法:GET
请求数据:
{
"appid": plus.runtime.appid,
"version": plus.runtime.version
}
响应数据:
{
"status":1,//升级标志,1:需要升级;0:无需升级
"note": "修复bug1;\n修复bug2;",//release notes
"url": "http://www.example.com/uniapp.apk" //更新包下载地址
}
客户端实现
App启动时,向服务端上报当前版本号,服务端判断是否提示升级。
在App.vue的onLaunch中,发起升级检测请求,如下:
onLaunch: function () {
//#ifdef APP-PLUS
var server = "https://www.example.com/update"; //检查更新地址
var req = { //升级检测数据
"appid": plus.runtime.appid,
"version": plus.runtime.version
};
uni.request({
url: server,
data: req,
success: (res) => {
if (res.statusCode == 200 && res.data.status === 1) {
uni.showModal({ //提醒用户更新
title: "更新提示",
content: res.data.note,
success: (res) => {
if (res.confirm) {
plus.runtime.openURL(res.data.url);
}
}
})
}
}
})
//#endif
}
注意:App的升级检测代码必须使用条件编译,否则在非App环境由于不存在plus相关API,将会报错。升级地址URL,如果是自行托管的App,就提供自己的包下载地址。
服务端实现
PHP代码:
header("Content-type:text/json");
$appid = $_GET["appid"];
$version = $_GET["version"]; //客户端版本号
$rsp = array("status" => 0); //默认返回值,不需要升级
if (isset($appid) && isset($version)) {
if ($appid === "__UNI__123456") { //校验appid
if ($version !== "1.0.1") { //这里是示例代码,真实业务上,最新版本号及relase notes可以存储在数据库或文件中
$rsp["status"] = 1;
$rsp["note"] = "修复bug1;\n修复bug2;"; //release notes
$rsp["url"] = "http://www.example.com/uniapp.apk"; //应用升级包下载地址
}
}
}
echo json_encode($rsp);
exit;
注意:版本检测需要打包app,真机运行基座无法测试。因为真机运行的plus.runtime.version是固定值。
uni-app使用npm包
执行 npm init 命令,初始化package.json文件
安装moment:
npm install moment -S
和我们在vue中使用npm包操作相同,
import moment from 'moment'
引入后即可使用
注意:uni-app 是跨端app,一些关联dom和bom的npm包只能在uni-app打包为h5页面才能使用。
uni-app全局配置
我们在pages.json中配置好我们的页面路径、标题栏、底部选项卡后,像使用easycom组件自动化引入也需要在里面进行配置,具体的配置项请查阅官方文档。
请求封装
我们的一款app脱离了服务端是没有灵魂的,我们的项目基于restful风格对原生的uni.request进行了二次封装,当然,不同公司对请求封装的要求不同,这里给出我的封装方案供大家参考:
// utils/request.js
import { baseUrl } from '@/config/baseUrl.js'
import util from '@/utils/index.js'
import { httpErrorStatusMessage, codeErrorStatusMessage, errorMessageSet } from '@/config/errCode'
// 返回header对象
const createHeader = () => {
const header = {}
header['Content-Type'] = 'application/json;charSet=UTF-8'
const token = uni.getStorageSync('access_token')
header['access_token'] = token ? token : '';
return header
}
// 全局错误异常处理
const requestThrowError = (err = {}, type = 'fail') => {
if(type == 'fail') {
const { errMsg } = err
if(errorMessageSet[errMsg]) {
util.showToast(errorMessageSet[errMsg],'tip');
return err
}
} else {
const { data, statusCode } = err
if(statusCode == 401) {
return userPermissError(err)
}
util.showToast(data.message,'fail')
return new Error(data.message)
}
}
// 权限处理
const userPermissError = (err) => {
const { data } = err
util.showToast('登陆过期','tip')
// 清空token 用户信息 跳转到登录页面 start
// end
return new Error(data.message)
}
// 创建请求
const createReqInstance = (url = '',method = 'GET',data = {}) => {
return new Promise((resolve,reject) => {
uni.request({
url:baseUrl + url, // 请求地址
data, // 请求数据
header:createHeader(), // 请求头
method, // 请求方法
dataType:'json', // 返回数据首先JSON.parse
// #ifdef H5
withCredentials:false, // 跨域请求是否携带cookies
// #endif
// #ifdef APP-PLUS
sslVerify: false, // 安卓端不验证ssl证书
// #endif
success(res) { // 请求成功
console.log(res)
const { data,statusCode } = res;
if(statusCode != 200 || data.code != 0) {
const err = requestThrowError(res,'success')
reject(err)
} else {
resolve(data)
}
},
fail(err) { //请求失败
console.log(err)
// 超时err.errMsg "request:fail timeout"
err = requestThrowError(err,'fail')
reject(err)
},
complete() { // 请求成功或失败最后都会执行
}
})
})
}
const getRequest = (url,params = {}) => {
return createReqInstance(url,'GET',params)
}
const postRequest = (url,params = {}) => {
return createReqInstance(url,'POST',params)
}
const putRequest = (url,params = {}) => {
return createReqInstance(url,'PUT',params)
}
const deleteRequest = (url,params = {}) => {
return createReqInstance(url,'DELETE',params)
}
export default {
get: getRequest,
post: postRequest,
put: putRequest,
delete: deleteRequest
}
// config/baseUrl.js
const baseUrls = {
'development':'http://xxx.xxx.xx.xx:3000',
'production':'http://www.production.com'
}
const baseUrl = baseUrls[process.env.NODE_ENV]
export {
baseUrl
}
// config/errCode.js
const httpErrorStatusMessage = {
}
const codeErrorStatusMessage = {
}
const errorMessageSet = {
"request:fail timeout":"请求超时"
}
export {
httpErrorStatusMessage,
codeErrorStatusMessage,
errorMessageSet
}
//
我们针对请求,封装了get post put delete 四种请求,自定义请求头,全局错误异常处理,权限处理,请求超时处理等等。
工具函数封装
我们在util/index.js文件中封装了一些常用的工具函数,像校验类,字符串类,数组类,正则类等等,下面给出一些对loading和toast的封装
const showLoading = (title = '',mask = true) => {
uni.showLoading({
title,
mask
})
}
const hideLoading = () => {
uni.hideLoading();
}
const showToast = (title = '',icon = 'none',image = '',mask = false,duration = 2000,position = 'center') => {
const filterImage = (icon) => {
let imageUrl;
switch(icon) {
case 'success': imageUrl = '/static/images/public/check-circle.png';break;
case 'fail': imageUrl = '/static/images/public/fail-circle.png';break;
case 'tip': imageUrl = '/static/images/public/info-circle.png';break;
default: imageUrl = '';break;
}
return imageUrl
}
uni.showToast({
title,
icon: icon,
image:filterImage(icon),
mask,
duration,
position
})
}
const hideToast = () => {
uni.hideToast();
}
export default {
typeFn,
strRegexp,
arrayFn,
stringFn,
numberFn,
otherFn,
showLoading,
hideLoading,
showToast,
hideToast
}
// main.js
···
import util from '@/utils/index'
···
Vue.prototype.$util = util
// 最后我们挂载到Vue原型上,我们就可以通过this.$util.xxx来访问工具函数
集成插件
下面介绍下我们项目中常用的一些插件
- 上拉刷新,下拉加载: 下拉刷新,上拉加载 s-pull-scroll
- UI库 Thor UI
- 图表库 uCharts高性能跨全端图表
- 字体图标:iconfont
uni-app使用iconfont - 全局loading 暂无数据 返回顶部组件这里不再展开,根据公司需求进行定制化页面即可,重点说下网络监听
我们使用uni.getNetworkType
和uni.onNetworkStatusChange
两个api来完成网络监听,代码如下:
// 获取网络状态
uni.getNetworkType({
success(res) {
console.log(res)
const { networkType } = res
let result = {
isConnected:true,
networkType:''
}
result.isConnected = networkType == 'none' ? false : true,
result.networkType = networkType
store.dispatch('network/setNetwork', result);
}
})
// 监听网络变化
uni.onNetworkStatusChange(function(res) {
console.log(res)
store.dispatch('network/setNetwork', res);
});
我们使用vuex存储网络状态,监听网络变化,关联网络组件显隐。
配置竖屏锁定
// App.vue
···
onLaunch: function() {
// #ifdef APP-PLUS
plus.screen.lockOrientation('portrait-primary'); // 竖屏锁定
···
}
main.js
下面我们通过查看main.js,将我们余下的工作整理完毕
import Vue from 'vue'
import moment from 'moment'
import store from './store'
import App from './App'
import '@/filters/index'
import '@/config/index'
import util from '@/utils/index'
Vue.config.productionTip = false
Vue.prototype.$util = util
Vue.prototype.$store = store
Vue.prototype.$moment = moment
Vue.prototype.$moment.locale('zh-cn')
App.mpType = 'app'
const app = new Vue({
...App
})
app.$mount()
- vuex封装,我们是采用modules的方式使用命名了空间将vuex的状态进行模块化拆分,大家根据需要自定义封装即可。
- 我们在App.vue中引入了一些全局样式,如下:
// App.vue
<style>
@import url("@/styles/index.scss");
</style>
// styles/index.scss
@import url("@/styles/font/iconfont.css"); // 字体图标css
@import url("@/styles/animate.min.css"); // 动画css
@import url("@/styles/uCharts.css"); // 图标css
@import url("@/styles/common.scss"); // 公共css
@import url("@/styles/thorUI.scss"); // UI库样式二次封装
总结
到此为止呢,我们的uni-app项目架构搭建完毕了,当然,我们也可以在uni.scss中添加一些全局scss变量,在README.md中撰写相关的项目文档。总之,uni-app的出现为我们的开发工作带来的方便,以前开发这一套,我们需要安卓团队,IOS团队,前端团队,三队人马,uni-app的出现大大提升了我们的生产效率,在此向DCloud公司致敬。学好uni-app,需要我们在实践中不断摸索,在需求中不断的突破自己,uni-app的好多内容等着我们去探索。神枪手都是子弹喂出来的,无他,惟手熟尔。大家加油💪!
参考链接:
uni-app官网
我是前端dog君,一名95后前端小兵。对前端的热爱,让我们在此相聚,希望这篇文章,能帮助到您,也同时希望能交到志同道合的小伙伴,共同发展,一起进步。我的微信号dm120225,备注简书,期待您的光临。