天天看點

H5+搭建移動端應用1. H5+搭建移動端應用

1. H5+搭建移動端應用

前兩篇介紹了全棧系統裡面背景和前端:

背景篇:Flask搭建背景

前端篇:Vue2.0搭建PC前端

項目線上位址:項目通路連結,賬号:admin 密碼:admin

今天講述搭建全棧系統裡面的移動端。本文講述用Vue2.0 + mint-ui建立一個移動端APP,這屬于全棧系統中的移動端,項目包含以下内容:

入口頁面:定義登入頁面和路由跳轉

登入頁面:實作系統登入功能

業務頁面:寫了兩個業務頁面1.> three.js加載gltf格式3D模型;2.> echart畫圖;

個人頁面:顯示使用者資訊頁面

效果圖:

H5+搭建移動端應用1. H5+搭建移動端應用
H5+搭建移動端應用1. H5+搭建移動端應用
H5+搭建移動端應用1. H5+搭建移動端應用
H5+搭建移動端應用1. H5+搭建移動端應用
H5+搭建移動端應用1. H5+搭建移動端應用
H5+搭建移動端應用1. H5+搭建移動端應用
H5+搭建移動端應用1. H5+搭建移動端應用

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建立項目

H5+搭建移動端應用1. H5+搭建移動端應用

項目建立完成後運作如下圖:

H5+搭建移動端應用1. H5+搭建移動端應用

這裡要注意:

建立好項目時,需要建立一個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

編譯菜單截圖:

H5+搭建移動端應用1. H5+搭建移動端應用

編譯完成截圖:

H5+搭建移動端應用1. H5+搭建移動端應用

注意:如果編譯過程中報錯,根據提示安裝缺失的包:npm install --save xxxx

1.2.3. 建立項目配置檔案和目錄

H5+搭建移動端應用1. H5+搭建移動端應用

項目目錄結構如上圖,檔案和目錄的說明如下:

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實作多個資訊顯示,如圖

H5+搭建移動端應用1. H5+搭建移動端應用

頁面定義

<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檔案,主要講常用的基礎配置和圖示配置

基礎配置:

H5+搭建移動端應用1. H5+搭建移動端應用

a. 如果還沒有AppID,點選擷取,生成一個新的AppID,一個應用對應一個AppID

b.輸入應用名稱、應用描述

c.配置應用入口頁面,預設為index.html

d.勾選配置app顯示橫屏、豎屏、橫豎屏

圖示配置:

H5+搭建移動端應用1. H5+搭建移動端應用

如果為了簡單省事,可以浏覽一張圖檔,再點選“自動生成是以圖示并替換”,就會生成各種尺寸的圖檔

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