目录
-
一、前言
-
二、环境配置
-
2.1 Nodejs下载与安装
-
2.2 Visual Studio Code安装
-
2.3 npm淘宝镜像配置
-
-
三、代码运行
-
四、代码讲解
-
4.1 config配置
-
4.1.1 dev与prod
-
4.1.2 跨域代理
-
-
4.2 代码结构-src
-
4.2.1 程序入口-main.js
-
4.2.2 路由配置-router
-
4.2.3 页面布局-view
-
4.2.4 组件-component
-
4.2.5 服务-service
-
4.2.6 状态管理-store
-
4.2.7 模拟数据-mock
-
4.2.8 其他
-
-
-
五、业务开发
-
六、项目部署
-
七、总结
一、前言
本文主要描述开发一个基于vue2.0的移动应用所需的环境安装、配置、代码获取、调试、业务逻辑开发、与企业微信集成等等整个过程;通读全文后可以完成一个移动应用从无到有的搭建、开发和部署。主要用到的技术栈如下:
- Nodejs
- vue2.0
- Vue-Router
- webpack
- Less
- vuex
- vux
- axios
- 阿里iconfont
- 企业微信SDK
文中用到了开源社区基于微信团队weUI的Vux UI组件库,在此表示对Vux作者的万分感谢。
二、环境配置
2.1 Nodejs下载与安装:地址
下载并安装成功后,在cmd下执行如下指令,确认是否安装成功:
C:\Users\xxx> node -v
v10.15.0
输出版本号则表明安装成功了。
本文仅举例了Windows平台的安装过程,Linux/Unix等其他操作系统请从查找其他网络资源参考。
2.2 Visual Studio Code安装:地址
vscode的安装就不赘述了。安装完成后,可以一并安装vscode的下列插件:
2.1.Vetur
2.2.Chinese (Simplified) Language Pack for Visual Studio Code
2.3.CSS Formatter
等等实用插件。
2.3 npm淘宝镜像配置:
在cmd中执行代码:npm install -g cnpm --registry=https://registry.npm.taobao.org
稍等几分钟,完成npm淘宝镜像的安装。
如在日常的开发工作中,发现npm或cnpm相关指令执行非常慢,甚至获取版本号都非常慢直至卡死,可以执行如下语句:
npm config set registry "http://registry.npm.taobao.org/"
三、代码运行
获取代码地址:git地址
获取到代码后,用Visual Studio Code编辑器打开代码的根目录。具体如下:启动Visual Studio Code --【文件】--【打开文件夹】选择项目的根目录。cnpm install
在项目根目录下执行cmd指令:cnpm install
,以安装项目的依赖。-
配置本地ip:
如果需要在手机中调试,直接使用localhost是不可取的,需要配置ip,然后手机连接到与pc同网段下的wifi进行访问即可。
-
npm run dev
在项目根目录下执行cmd指令:npm run dev
,编译完成后如下:
同时按下Ctrl+鼠标点击图中http://172.xx.xx.xxx
,浏览器则打开如下图的页面:
开发小技巧:在Visual Studio Code下进行开发,无需另外打开一个cmd窗口进行指令执行,可在Visual Studio Code通过点击【查看】-【终端】打开Visual Studio Code自带的cmd入口进行指令执行和监控代码编译进度等。详情参考Visual Studio Code的使用说明。
四、代码讲解
其中:
- build:webpack相关打包应用配置
- config:应用开发环境下的dev和prod相关配置
- src:源码
- static:静态资源,如多语言配置
- test:单元测试相关
4.1 config-配置
-
4.1.1 dev与prod:
这是环境变量定义,即,在dev和prod两个文件定义一个变量,如,dev下,a=0,而prod下,a=1,那么,在执行npm run dev
命令下的上下文中,a=0,而在采用npm run build
指令下,a=1。
如dev文件:\config\dev.env.js
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
API_ROOT:'"http://172.0.0.1:8443/yyy/rest/"',
LOGMODE:true,
})
prod文件:\config\prod.env.js
'use strict'
module.exports = {
NODE_ENV: '"production"',
LOGMODE: false,
API_ROOT: '"https://xxx.xx.xx.xxx:8443/yyy/rest/"',
}
如此,在其他上下文中,可以通过process.env.LOGMODE
来访问当前环境变量下的值了。即、在npm run dev
下process.env.LOGMODE=true;而在npm run build
下process.env.LOGMODE=false;
使用实例:
//日志记录,在调试模式下才打印日期,而在build下不会打印日志。
log(msg) {
if (process.env.LOGMODE) {
console.log("Gac Logger:" + msg);
}
},
-
4.1.2 跨域代理:
大部分前后端分离的项目中,前端访问的后台接口,都不在前端的同一个域名/ip/端口之下,这样一来是必须解决跨域问题的。在\config\index.js
文件下的module.exports
下的dev
节点下,增加类似如下代码进行跨域代理:
proxyTable: {
//配置代理,解决跨域问题 by LeoFeng 2018-9-13 09:27:15
'/api': {
target: 'http://172.31.254.125:8443/gacrdp/rest/',
changeOrigin: true,
pathRewrite: {
'^/api': '' //路径重写
}
}
}
...
如此一来,在dev环境下访问相关后台的api时,可以用'/api'前缀来替代出现跨域问题的后台接口前缀,nodejs将为应用处理中转。
如当前需要调用后台的getList
的Rest API接口,值可以这样:
axios.get('/api/getList')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
nodejs会把请求的/api/getList
地址,中转到http://172.31.254.125:8443/gacrdp/rest/getList
这个接口地址上,并最终完成数据请求。
4.2 代码结构-src
- assets:资源如css,js,image
- components:用户自定义组件
- config:用户自定义配置
- mock:模拟数据
- router:路由配置
- service:请求服务配置
- store:状态管理
- utils:通用工具
- views:应用开发的页面
- App.vue:启动页
- main.js:程序主函数
4.2.1 程序入口-main.js
公共UI组件注册:
import {
AlertPlugin,
AjaxPlugin, WechatPlugin, LoadingPlugin, ToastPlugin, ConfirmPlugin, Popup, PopupPicker, Popover, XButton, PopupHeader, Cell, Group, CellBox, Badge, XHeader, Search
, XInput
} from 'vux'
Vue.component('popup', Popup)
Vue.component('popover', Popover)
Vue.component('x-button', XButton)
Vue.component('popup-header', PopupHeader)
Vue.component('cell', Cell)
Vue.component('group', Group)
Vue.component('cell-box', CellBox)
Vue.component('badge', Badge)
Vue.component('x-header', XHeader)
Vue.component('search', Search)
Vue.component('popup-picker', PopupPicker)
Vue.component('x-input', XInput)
状态管理与多语言引入:
import store from './store/index'//状态管理(全局使用) LeoFeng 2018-10-8 15:09:39
import LangEn from '../static/lang/en'
import LangZhCHS from '../static/lang/zh-CNS'
import LangZhCHT from '../static/lang/zh-CNT'
//多语言
const i18n = new VueI18n({
locale: 'zh-CHS',
messages: {
'en': LangEn,
'zh-CHS': LangZhCHS,
'zh-CHT': LangZhCHT
}
})
渲染挂载:
new Vue({
router,
i18n,
store,
render: h => h(App)
}).$mount('#app-box')
4.2.2 路由配置-router
本框架采用了Vue-Router进行页面导航,每一个需要展示的页面都需要在路由配置中申明。\src\router\index.js
代码节选如下:
import Vue from 'vue'
import Router from 'vue-router'
import GacDefault from '@/views/Default/Default'
import DeviceMarket from '@/views/DeviceMarket/DeviceMarket'
import DeviceDetail from '@/views/DeviceDetail/DeviceDetail'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'default',
meta: {
title: '设备仓库'
},
component: GacDefault,
children: [{
path: '/',
name: 'DeviceMarket',
meta: {
title: '设备仓库'
},
component: DeviceMarket,
children: []
}
]
},
{
path: '/device-detail/:id',
name: 'device-detail',//path跟name最好相同。
meta: {
title: '设备明细'
},
component: DeviceDetail,
children: []
}
]
})
使用方式:
普通导航方式:<router-link to="/">default</router-link>
编程式的导航[带参数]方式:this.$router.push({ name: "device-detail", params: { id } });
更多导航方式和参数获取方式请参考Vue-Router官方文档。
4.2.3 页面布局-views
应用开发的页面,集中存放在\src\views\
下:
如业务上需要新增一个页面并展示出来,如名称为
student
的页面,步骤如下:
- 在
views
文件夹下新增一个Student
子文件夹,并在子文件夹内新建一个页面的Student.vue
文件; - 在
student.vue
内进行页面布局和相关逻辑开发; - 开发完成后,在
\src\router\index.js
的路由配置文件下进行引用:
import Student from '@/views/Student/Student'
然后在路由根节点中申明【非嵌套路由场景】:
{
path: '/student',
name: 'student',
meta: {
title: '学生记录'
},
component: Student,
children: []
}
展示时只需添加如下导航标签即可:
<router-link to="/student">学生记录</router-link>
4.2.4 组件-components
当应用的业务包含较多通用功能,且这些功能都相对独立时,我们就自然而然的会想到将这些通用的功能独立出来,写成一个通用的组件。
Vue的组件无非包含以下几个常用的概念:
-
属性定义:
props
-
事件上报:
$emit('事件名','参数')
-
插槽:
<slot></slot>
-
值绑定:
v-model
当涉及到父、子组件通信时,通常采用props实现父组件向子组件传递数据;而子组件则通过$emit()上报事件到父组件,父组件再通过@event-name来捕捉子组件上报的事件。
属性-定义:代码节选自:\src\components\gacDropItem.vue
...
props: {
options: {
type: [Object],//申明属性类型
required: false//申明是否为必选属性
},
value: {
default: ""//value的定义,是为了给当前组件实现v-model的功能
}
},
methods: {
onCheck: function(eqId) {
...
this.$emit("input", param); //v-model必备!配合props中的value,组件的v-model功能就实现了。
this.$emit("on-checked", param);//上报on-checked事件
},
...
属性-插槽
例举:\src\components\gacScroller.vue
<template>
<div class="cScroller">
<scroller ...[省略属性设置]>
<slot></slot><!--定义插槽-->
</scroller>
</div>
</template>
属性-应用
属性、事件和v-model的应用:
<gacDropItem
v-for="(item2,index2) in item.data"
:key="item2.equipmentId"
:options="item2"
v-model="toApproveEqIds[parseInt(index1.toString()+index2.toString())]"
@on-checked="onDropItemCheck"
>
</gacDropItem>
插槽的应用:
<gacScroller
ref="gacScroller"
:xHeight="'-162'"
@on-load-more="loadMore"
@on-refresh="refresh"
>
<div>
<!--这里就是插槽的容器,放置要显示的内容-->
</div>
</gacScroller>
注意!自定义组件的应用,首先需要完成组件的开发;其次在需要引用组件的页面引用组件:
import gacDropItem from "./../../components/gacDropItem";
;接着在页面中注册组件:components: { gacDropItem },
;最后才参考上图进行组件的应用。
4.2.5 服务
服务,专注于应用的数据请求,主要包括:
- API URL配置
- API 封装
- API 请求
4.2.5.1 api url 配置: \src\service\api.js
,详细如下:
const api = {
GetToken: '/api/tokens',
GetUserInfo: '/api/meetingApi/getUserInfo',
/************************业务系统api************************/
...
//获取用户类型
nvh_getUserType: '/nvh-api/getUserType',
//获取设备分类
nvh_getTypeList: '/nvh-api/typeList',
...
/************************业务系统api************************/
}
export default api
4.2.5.2 API封装:
import Vue from 'vue'
import api from '../api'
import { USER_INFO } from '@/store/mutation-types'
import { axios } from '@/utils/request'
//获取类型列表
export function getTypeList(deptCode = '') {
return axios({
url: api.nvh_getTypeList + "?deptCode=",
method: 'GET',
data: {}
})
}
...
//确认下单
export function doSubmitBorrow(params) {
return axios({
url: api.nvh_doSubmitBorrow ,
method: 'POST',
data: params
})
}
...
4.2.5.3 API请求:
- 在需要请求api接口的页面,引用服务:
import { getEquipmentList } from "./../../service/modules/nvhDevice";
- 在需要调用API接口的地方,写类似如下请求即可:
...
getEquipmentList(this.equipmentParams).then(
res => {
//todo with res.data
},
err => {
//todo with err
}
);
...
4.2.6 状态管理
无论是Vue还是React,状态管理都是一个非常重要的概念。本项目应用了Vuex的状态管理组件,需要更深入了解的请参考Vuex官方文档。以下主要讲解Vuex的应用,在项目中的代码结构如下:
-
modules
文件夹下,主要是将不同业务的状态管理分隔开来,便于管理。 -
index.js
文件是全局的状态管理入口,它集合了modules
文件夹下各子业务的状态管理实现,并对外暴露了一个Vuex.Store
对象。
状态管理主要包括:
State:可以理解为据图存储的数据对象;
Mutation:更改 Vuex 的 store 中的状态的唯一方法是提交 mutation;
-
Action:Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作
Getter:可以理解为数据库中的视图view,是state的一种过滤后的展现。
Module:使得状态管理可以模块化,便于代码管理。
4.2.6.1 状态定义
例举:\src\store\modules\app.js
import Vue from 'vue'
import { ACCESS_TOKEN, USER_INFO } from '@/store/mutation-types'
import { getToken, getUserInfo } from './../../service/modules/app'
const app = {
state: {
token: '',//最终存储Token的状态
userinfo :'',//用户信息的状态
...
},
mutations: {
SET_ACCESS_TOKEN: (state, _token) => {
state.token = _token
Vue.ls.set(ACCESS_TOKEN, _token)
},
...
},
actions: {
GetAccessToken({ commit }, params) {
return new Promise((resolve, reject) => {
console.log('innnnnnnnn Action.')
getToken(params).then(response => {
console.log(JSON.stringify(response))
if (response.length <= 128) {
Vue.ls.set(ACCESS_TOKEN, response, 7 * 24 * 60 * 60 * 1000)
commit('SET_ACCESS_TOKEN', response)
console.log("get Token success.")
} else {
console.log('Token 长度明显异常。')
}
resolve()
}).catch(error => {
reject(error)
})
})
},
...
},
getters: {
gCoverList: state => { return state.coverList },
gUserInfo: state => { return state.userinfo }
}
}
export default app
4.2.6.2 模块组合
完成上述模块的定义后,在\src\store\index.js
中进行集成。
import Vue from 'vue'
import Vuex from 'vuex'
import app from './modules/app'
import wechat from './modules/wechat'
import carList from './modules/deviceCar'
import nvhDevice from './modules/nvhDevice'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
app,
wechat,
carList,
nvhDevice
},
state: {
},
mutations: {
},
actions: {
},
getters: {
}
})
4.2.6.3 状态应用
步骤如下:
- 在需要应用指定状态管理的页面,引用如下代码(参考:
\src\views\Car\Car.vue
):
import { mapGetters, mapActions } from "vuex";
然后,向当前上下文的methods
下注册如下方法:
//具体应用-action
...mapActions(["RemoveFromCarList", "AddToCarList", "ClearCarList"]),
接着向当前上下文中的computed
注册如下计算属性:
//具体应用-getters
...mapGetters(["carList", "gUserInfo"])
此时,在当前页面的上下文下,即可直接调用方法和获取属性:
//action的调用
this.RemoveFromCarList(_id);
//carList属性的使用
this.carList
另外,提交状态也可以通过直接提交(commit) Mutation来实现:
this.$store.commit("mutation-name", 'params')
getter和state也可以直接通过$store
来访问:
//直接访问getter
this.$store.getters.[getter名];
//直接访问state
this.$store.state.[module-name].[state-name]
注意:状态的变更响应机制常常会给我们带来很大的便捷,但是,并不是所有的数据都有必要存储在store中,否则会大大增加系统的开销,影响性能。另外,如果一个非常小的应用,也不一定非要使用Vuex这样的状态管理框架,反倒是可以通过简单的数据总线就可以实现类似的功能。
4.2.7 模拟数据
应用在调试过程中需要很多模拟数据,集中放在\src\mock
下:
在需要模拟数据的页面,添加类似引用:
import TabMenuitems from "./../../mock/app/tabMenus";
便可在上下文中直接引用这些模拟数据,如:
computed: {
...mapGetters(["carListCount"]),
tabItems: () => {
return TabMenuitems;
}
},
4.2.8 其他
- 皮肤重写
本项目采用了基于weUI的Vux UI组件,这样要面临一个问题就是,当实际项目中需要修改指定的样式时,会比较麻烦。因此项目统一接入了主题重写入口,具体如下:
首先,在\build\webpack.base.conf.js
文件下的module.exports
->plugins
下新增一个插件less-theme
,指定文件路径为:src/assets/theme/theme.less
module.exports = vuxLoader.merge(webpackConfig, {
plugins: [
'vux-ui',
'progress-bar',
...,
{ name: 'less-theme', path: 'src/assets/theme/theme.less' }/*提供重写Vux框架皮肤功能 add by LeoFeng 2019-3-29 15:51:19*/
]
})
接着,在\src\assets\theme
目录下新增theme.less
文件,这样就可以重载vux的样式了:
/*提供重写Vux框架皮肤功能
**add by LeoFeng 2019-3-29 15:51:19
*/
@search-cancel-font-color: #80b1f0;
@search-bg-color:#EFEFF4;
@tabbar-text-active-color:#80b1f0;
@header-background-color:#fff;
@header-title-color:#333;
@header-text-color:#333;