1. H5+搭建移动端应用

前两篇介绍了全栈系统里面后台和前端:
后台篇:Flask搭建后台
前端篇:Vue2.0搭建PC前端
项目线上地址:项目访问链接,账号:admin 密码:admin

今天讲述搭建全栈系统里面的移动端。本文讲述用Vue2.0 + mint-ui创建一个移动端APP,这属于全栈系统中的移动端,项目包含以下内容:

入口页面:定义登录页面和路由跳转
登录页面:实现系统登录功能
业务页面:写了两个业务页面1.> three.js加载gltf格式3D模型;2.> echart画图;
个人页面:显示用户信息页面

效果图:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.1. 前端页面开发选用的技术栈如下:

开发语言:HTML+JS
开发框架:Vue2.0 + mint-ui + axios + echart + three.js
开发工具:Hbuilder X 2.7.9
系统后台:FlaskDemo

1.1.1. 技术选型

开发移动APP为什么不用原生语言来开发?为什么要用H5+来开发?下面详细说明,
不选原生开发的原因:1. 目前开发APP面临动态化内容需求日益增大,纯原生应用需要通过版本升级来更新内容,但应用上架、审核是需要周期的,这个周期对高速变化的互联网时代来说是很难接受的;2. 业务需求变化快,开发成本变大,一般都要维护 Android、iOS两个开发团队,版本迭代时,无论人力成本还是测试成本都会变大

为了避免原生开发面临的上述问题,我们选择跨平台技术,跨平台技术根据其原理,主要可分为如下三类,
a. H5(HTML5)+原生( Cordova、 Tonic、微信小程序)。
b. Javascript开发+原生渲染( React Native、快应用)。
c. 自绘UI原生( QT + qml、 Flutter)。
本文采用H5+技术,后面的文章再介绍后面两种技术

选择H5+开发的原因:1. h5+应用能满足大部分APP需求,性能和体验都可以;2. 和PC web端开发技术相同,减少学习成本;3. 只需要写一套代码就能支持多个平台

1.2. 系统的详细开发过程

1.2.1. 用Hbuilder创建项目

在这里插入图片描述

项目创建完成后运行如下图:
在这里插入图片描述
这里要注意:
创建好项目时,需要创建一个vue.config.js的配置文件,配置内容如下:

const webpack = require('webpack')

module.exports = {
  baseUrl: './',// 部署应用时的根路径(默认'/'),也可用相对路径(存在使用限制)
  outputDir: 'dist',// 运行时生成的生产环境构建文件的目录(默认''dist'',构建之前会被清除)
  assetsDir: '',//放置生成的静态资源(s、css、img、fonts)的(相对于 outputDir 的)目录(默认'')
  indexPath: 'index.html',//指定生成的 index.html 的输出路径(相对于 outputDir)也可以是一个绝对路径。
  pages: {//pages 里配置的路径和文件名在你的文档目录必须存在 否则启动服务会报错
  		index: {//除了 entry 之外都是可选的
  				entry: 'src/main.js',// page 的入口,每个“page”应该有一个对应的 JavaScript 入口文件
  				template: 'public/index.html',// 模板来源
  				filename: 'index.html',// 在 dist/index.html 的输出
  				title: 'Index Page',// 当使用 title 选项时,在 template 中使用:<title><%= htmlWebpackPlugin.options.title %></title>
  				chunks: ['chunk-vendors', 'chunk-common', 'index'] // 在这个页面中包含的块,默认情况下会包含,提取出来的通用 chunk 和 vendor chunk
  		},
  		subpage: 'src/main.js'//官方解释:当使用只有入口的字符串格式时,模板会被推导为'public/subpage.html',若找不到就回退到'public/index.html',输出文件名会被推导为'subpage.html'
  },
  lintOnSave: true,// 是否在保存的时候检查
  productionSourceMap: false,// 生产环境是否生成 sourceMap 文件,false表示隐藏vue代码
  css: {
  		extract: true,// 是否使用css分离插件 ExtractTextPlugin
  		sourceMap: false,// 开启 CSS source maps
  		loaderOptions: {},// css预设器配置项
  		modules: false// 启用 CSS modules for all css / pre-processor files.
  },
  devServer: {// 环境配置
  		host: '0.0.0.0',
  		port: 8081,
  		https: false,
  		hotOnly: false,
  		open: true, //配置自动启动浏览器
  		proxy: {// 配置多个代理(配置一个 proxy: 'http://localhost:4000' )
  				'/api': {
  						target: '<url>',
  						ws: true,
  						changeOrigin: true
  				},
  				'/foo': {
  						target: '<other_url>'
  				}
  		}
  },
  pluginOptions: {// 第三方插件配置
  	
  },
  configureWebpack: {
   plugins: [
      new webpack.ProvidePlugin({
        $:"jquery",
        jQuery:"jquery",
        "windows.jQuery":"jquery"
      })
    ]
  }
}

1.2.2. 安装项目需要的依赖库

项目里面需要用到axios、jquery、vue-router、vuex、echarts,需要安装,命令如下:
npm install --save axios jquery vue-router vuex mint-ui
编译菜单截图:
在这里插入图片描述

编译完成截图:
在这里插入图片描述

注意:如果编译过程中报错,根据提示安装缺失的包:npm install --save xxxx

1.2.3. 创建项目配置文件和目录

在这里插入图片描述

项目目录结构如上图,文件和目录的说明如下:
dist:项目编译后生成的目录,该目录内容放到FlaskDemo中static目录下,就可以访问web页面
node_modules:项目依赖包安装目录
public:项目资源文件目录,这里存放着一个3D模型文件,用于加载到页面展示
src:vue源文件目录,assets存放资源,components存放实现的业务组件,后面详细描述
vue.config.js:Vue-cli3配置文件
manifest.json:配置APP打包信息文件
其他文件:创建Vue项目时自动生成的

下面详细介绍vue源文件目录

1.2.3.1. 创建App.vue

这个文件定义前端页面入口,引入了路由和页面布局、页面导航,
**页面布局方式:**上面头部 + 中部路由显示 + 下面导航,页面布局是用mint-ui中mt-header、mt-tabbar组件实现
**页面导航:**有3个导航菜单,首页、业务、我的,通过监听绑定mt-tabbar组件的selected值来实现路由跳转

<template>
	<div id="app">
		<mt-header fixed title="Vue + mint-ui移动端展示" class="fixedheader">
			<mt-button class="huahuiLogo" slot="left"></mt-button>
		</mt-header>
		<router-view></router-view>
		<mt-tabbar fixed v-model="selected" v-if="this.$router.currentRoute.name != 'login'">
			<mt-tab-item id="tab1">
				<img slot="icon" :src="selected == 'tab1' ? require('./assets/images/icon-jk-1.png') : require('./assets/images/icon-jk-2.png')">
				首页
			</mt-tab-item>
			<mt-tab-item id="tab2">
				<img slot="icon" :src="selected == 'tab2' ? require('./assets/images/icon-yj-1.png') : require('./assets/images/icon-yj-2.png')">
				业务
			</mt-tab-item>
			<mt-tab-item id="tab3">
				<img slot="icon" :src="selected == 'tab3' ? require('./assets/images/icon-wd-1.png') : require('./assets/images/icon-wd-2.png')">
				我的
			</mt-tab-item>
		</mt-tabbar>
	</div>
</template>

监听selected值,实现路由跳转

watch:{
  selected(val) {
    console.log(val, this.selected, this.$store.state.userInfo);
    if (this.$store.state.userInfo == '') return;
    if (val == 'tab1') {
      this.$router.push('/home');
    } else if (val == 'tab2') {
      this.$router.push('/business');
    } else if (val == 'tab3') {
      this.$router.push('/my');
    }
  }
},
1.2.3.2. 创建main.js

这个文件引入项目需要的组件,创建Vue app,定义全局访问的方法
引入组件

import Vue from 'vue'
import App from './App.vue'
import Mint from 'mint-ui'
import 'mint-ui/lib/style.css'
import router from './router'
import $ from 'jquery'
import echarts from 'echarts'
import store from './store'

Vue.config.productionTip = false
Vue.prototype.$echarts = echarts
Vue.use(Mint)

//配置axios
import axios from 'axios'
import qs from 'qs'

定义全局方法,如http访问

//配置axios
import axios from 'axios'
import qs from 'qs'

axios.defaults.timeout = 5000; //响应时间
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'; //配置请求头
axios.defaults.baseURL = 'http://127.0.0.1:5000'; //配置接口地址

//POST传参序列化(添加请求拦截器)
axios.interceptors.request.use((config) => {
	//在发送请求之前做某件事
	if (config.method === 'post') {
		config.data = qs.stringify(config.data);
	}
	return config;
}, (error) => {
	//console.log('错误的传参')
	return Promise.reject(error);
});

//返回状态判断(添加响应拦截器)
axios.interceptors.response.use((res) => {
	//对响应数据做些事
	if (!res.data.success) {
		return Promise.resolve(res);
	}
	return res;
}, (error) => {
	//console.log('网络异常')
	return Promise.reject(error);
});

//返回一个Promise(发送put请求)
Vue.prototype.$fetchPut = function(url, params) {
	return new Promise((resolve, reject) => {
		axios.put(url, params)
			.then(response => {
				resolve(response);
			}, err => {
				reject(err);
			})
			.catch((error) => {
				reject(error)
			})
	})
}

//返回一个Promise(发送delete请求)
Vue.prototype.$fetchDelete = function(url, params) {
	return new Promise((resolve, reject) => {
		axios({
				method: "delete",
				url: url,
				data: params,
			})
			.then(response => {
				resolve(response);
			})
			.catch((error) => {
				reject(error)
			})
	})
}

//返回一个Promise(发送post请求)
Vue.prototype.$fetchPost = function(url, params) {
	return new Promise((resolve, reject) => {
		axios.post(url, params)
			.then(response => {
				resolve(response);
			}, err => {
				reject(err);
			})
			.catch((error) => {
				reject(error)
			})
	})
}

//返回一个Promise(发送get请求)
Vue.prototype.$fetchGet = function(url, param) {
	return new Promise((resolve, reject) => {
		axios.get(url, {
				params: param
			})
			.then(response => {
				resolve(response)
			}, err => {
				reject(err)
			})
			.catch((error) => {
				reject(error)
			})
	})
}

创建Vue,绑定路由和存储模块

new Vue({
	render: h => h(App),
	store,
	router,
}).$mount('#app')
1.2.3.3. 创建router.js

这个文件定义前端路由,关联导航菜单,跳转到具体页面

import Vue from 'vue'
import Router from 'vue-router'

import login from './components/login'
import home from './components/home'
import my from './components/my'
import business from './components/business.vue'

Vue.use(Router);

export default new Router({
  // mode: 'history',  //去掉url中的#
  routes: [
	   { path: '/', name: 'login', lable: '登录', component: login },
	   { path: '/home', name: 'home', lable: '首页', component: home },
	   { path: '/my', name: 'my', lable: '我的', component: my },
	   { path: '/business', name: 'business', lable: '业务', component: business }
  ]
})

代码中定义的lable就是导航菜单里面的名称,导航菜单内容根据用户权限返回,就可以根据不同用户动态展示导航菜单

1.2.3.4. 创建store.js

这个文件定义vuex保存数据

export default new vuex.Store({
	state: {
		//xxxx: 'xxxxx',
	},
	mutations: {
		setData(state, obj) {
			for (let k in state) {
				if (obj.hasOwnProperty(k)) {
					//xxxx = xxxxx;
				}
			}
		},
		clearData(state) {
			for (let k in state) {
				//xxxx = '';
			}
		}
	}
});

由于vuex保存的数据在内存里面,页面一刷新,数据就会丢失,这里采用把数据临时保存到sessionStorage里面,刷后读取,再删除sessionStorage
具体代码在App.vue中created()方法实现。

created() {//处理刷新时vuex里面数据保存
    //在页面加载时读取sessionStorage里的状态信息
    if (sessionStorage.getItem("store")) {
    	this.$store.replaceState(Object.assign({}, this.$store.state, JSON.parse(sessionStorage.getItem("store"))));
    	sessionStorage.removeItem('store');
    }
    // console.log(sessionStorage.getItem("store"))
    //在页面刷新时将vuex里的信息保存到sessionStorage里
    window.addEventListener("beforeunload", () => {
    	sessionStorage.setItem("store", JSON.stringify(this.$store.state))
    });
}
1.2.3.5. 创建login.vue

这个文件创建登录页面,登录框是通过mint-ui中的mt-field、mt-button实现,

<template>
	<div>
		<div class="show-logo">
			<div class="tip">欢迎来到XXXXXX系统</div>
			<div class="show-logo-content"></div>
		</div>

		<div class="login-form">
			<mt-field label="用户名" placeholder="请输入用户名" v-model="form.username"></mt-field>
			<mt-field label="密码" placeholder="请输入密码" type="password" v-model="form.password"></mt-field>
			<div class="rememberPsd">
				<input type="checkbox" name="vehicle" value="Car"  v-model="form.record"/> 记住密码
			</div>

			<mt-button type="primary" size="large" @click='login'>登录</mt-button>
		</div>

	</div>
</template>

数据结构定义

data() {
	return {
		form: {
			username: '',
			password: '',
			record: false
		}
	}
},

登录功能实现

async login() {
	if (this.form.username == '' || this.form.password == '') {
		this.$toast('请输入账号名或者密码');
		// this.$message.info('请输入账号名或者密码');
		return;
	}

	let argc = {
		'username': this.form.username,
		'password': this.form.password
	};
	let result = await this.$fetchPost('/login', argc);
	if (result.status == 200) {
		console.log(result.data);
		if (result.data.code == '0') {
			let groups = '{"首页": [], "业务菜单": ["3D模型", "画图展示", "业务3"], "系统设置": ["用户管理", "系统日志"]}';
			let roles =
				'{"首页": ["读"], "3D模型": ["读", "写"], "业务2": ["读", "写"], "业务3": ["读", "写"], "用户管理": ["读", "写"], "系统日志": ["读", "写"]}';
			localStorage.setItem('record', this.form.record);
			localStorage.setItem('username', this.form.username);
			this.$store.commit('setData', {
				'access_token': this.form.username,
				'userInfo': this.form.username,
				'groups': this.$isJSONStr(groups) ? JSON.parse(groups) : {},
				'roles': this.$isJSONStr(roles) ? JSON.parse(roles) : {},
			});
			this.$router.push('/home');
			this.form.password = '';
		} else {
			this.$toast(result.data.msg);
		}
	}
}

login函数说明:
1.> async配合await使用,http请求接口this.$fetchPost不需要写回调函数处理请求返回的结果,按顺序写处理结果代码,这样写逻辑清晰还能避免回调地狱
2.> 收到http请求后定义两个变量groups、roles模拟用户返回的权限,这里可以自己修改里面内容,看下登录后菜单显示的内容

1.2.3.6. 创建loadmodel.vue

这个文件是展示3D模型的组件,用来加载3D模型,了解更多WEB 3D知识:three.js
定义页面

<template>
	<div class="container" id="scene-container"></div>
</template>

引入组件

import * as THREE from 'three'
import {OBJLoader, MTLLoader} from 'three-obj-mtl-loader'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader'

定义数据模型

data() {
	return {
		camera: null,
		scene: null,
		light: null,
		renderer: null,
		controls: null,
		stats: null,
	}
}

定义展示three.js3D模型的基本方法

methods: {
    initThree() {},//初始化three.js对象
    initCamera(),//初始化相机
    initScene() {},//初始场景
    initLight() {},//初始化灯光
    loadmodels() {},//加载gltf格式3D模型
    initControl() {},//初始化模型控制器
    onWindowResize() {},//渲染模型
    render() {},
    threeStart() { //启动流程函数
    	this.initThree(); 
    	this.initCamera(); 
    	this.initScene(); 
    	this.initLight(); 
    	this.loadmodels(); 
    	this.initControl(); 
    	this.renderer.clear(); 
        this.renderer.render(this.scene, this.camera);
    }
}
1.2.3.7. 创建business2.vue

这个文件是展示echart画图的组件
定义页面

<template>
	<div class="container" id="container" :style="`height: ${height}px;`">
	</div>
</template>

定义数据模型

data() {
	return {
		height: document.documentElement.clientHeight - 160,
		builderJson: {},
		downloadJson: {},
		themeJson: {},
	}
}

定义画图方法

methods: {
	setOptionData(item, option) {
		var data;
		if (typeof option == "object") {
		    data = option;
		} else {
		    data = JSON.parse(option);
		}
		
		data["animation"] = true;
		var dom = document.getElementById(item);
		var myChart = this.$echarts.getInstanceByDom(dom);
		if (myChart != null && myChart != "" && myChart != undefined) {
		    myChart.dispose();
		}
		
		myChart = this.$echarts.init(dom, "roma");
		
		if (data && typeof data === "object") {
		    myChart.setOption(data, true);
		}
	}
}
1.2.3.8. 创建my.vue

这个文件显示用户个人信息,通过定义用户信息userInfo数据结构,利用vue里面v-for和mint-ui里面mt-field实现多个信息显示,如图
在这里插入图片描述
页面定义

<template>
	<div class="my-mine-container page-container">
		<div class="page-header">
			<div class="page-header-text">个人中心</div>
		</div>
		<div class="page-content">
			<div class="user-header-img">
			</div>
			<div class="user-info-list">
				<div class="user-info-item" v-for="(info,idx) in userInfo" :key="idx">
					<mt-field :label="idx" >{{info}}</mt-field>
				</div>
			</div>
			<mt-button type="primary" size="large" @click='loginOut'>退出</mt-button>
		</div>
	</div>
</template>

数据模型定义

data() {
	return {
		userInfo: {
			'部门': '',
			'岗位': '',
			'账户': '',
			'姓名': '',
			'手机号': '',
			'邮箱': '',
			'版本': 'V1.0'
		},
		userName: this.$store.state.userInfo
	}
}

处理用户信息方法

//查询用户信息,初始化数据
async init() {
	let userList = [];
	let results = await this.$fetchGet('/get/AccountUsers/get_value_list', {});
	if (results.status == 200) {
		if (results.data.code == '0') {
			this.tableData = [];
			let info = results.data.data.filter(res => {
				return res[0].Name === this.userName;
			});
			console.log(info);
			this.userInfo['部门'] = info[0][2].Name;
			this.userInfo['岗位'] = info[0][1].Name;
			this.userInfo['账户'] = info[0][0].Name;
			this.userInfo['姓名'] = info[0][0].Nick;
			this.userInfo['手机号'] = info[0][0].Mobile;
			this.userInfo['邮箱'] = info[0][0].Email;
		}
	}
}

//用户登出
async loginOut() {
	let results = await this.$fetchPost('/logout', {});
	if (results.status == 200) {
		if (results.data.code == '0') {
			console.log(this.$store.state.userInfo);
			this.$store.commit('clearData');
			console.log(this.$store.state.userInfo);
			this.$router.push('/');
		} else {
			this.$toast(results.data.msg);
		}
	}
}

1.3. 打包成APP

这里讲述打包成Android应用,
首先配置manifest.json文件,主要讲常用的基础配置图标配置
基础配置:
在这里插入图片描述
a. 如果还没有AppID,点击获取,生成一个新的AppID,一个应用对应一个AppID
b.输入应用名称、应用描述
c.配置应用入口页面,默认为index.html
d.勾选配置app显示横屏、竖屏、横竖屏

图标配置:
在这里插入图片描述
如果为了简单省事,可以浏览一张图片,再点击“自动生成所以图标并替换”,就会生成各种尺寸的图片

1.4. 源码文件

后台源码:VueMobileDemo.zip
默认用户名:admin
默认密码:admin

1.5. 总结

我们来对比一下PC端和移动端

PC移动端
技术对比Vue2.0 + element-ui + axios + echart + three.jsVue2.0 + mint-ui + axios + echart + three.js
导航对比在navmennu.vue中实现在app.vue中通过tab实现
业务代码共用共用

用h5+开发移动APP用到的技术基本一致,掌握了PC前端开发技术,开发移动APP也可以轻易实现

1.6. 后记

本文完整讲述了全栈系统中的移动端:利用Vue2.0+mint-ui创建移动端应用。
现在,后台、前端、移动端开发都讲完了。下章开始讲解后台部署(docker + nginx + uwsgi);前后端单元测试脚本;系统运维方面的知识。

Logo

前往低代码交流专区

更多推荐