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.js | Vue2.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);前后端单元测试脚本;系统运维方面的知识。