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);前後端單元測試腳本;系統運維方面的知識。