《基于Vue2.0的Web移动应用开发》- 基础

目录

  • 一、前言
  • 二、环境配置
    • 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进行访问即可。

    image.png

  • npm run dev
    在项目根目录下执行cmd指令:npm run dev,编译完成后如下:

    image.png

同时按下Ctrl+鼠标点击图中http://172.xx.xx.xxx,浏览器则打开如下图的页面:

image.png

开发小技巧:在Visual Studio Code下进行开发,无需另外打开一个cmd窗口进行指令执行,可在Visual Studio Code通过点击【查看】-【终端】打开Visual Studio Code自带的cmd入口进行指令执行和监控代码编译进度等。详情参考Visual Studio Code的使用说明。


四、代码讲解

image.png

其中:

  • 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 devprocess.env.LOGMODE=true;而在npm run buildprocess.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

image.png
  • 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\下:

image.png

如业务上需要新增一个页面并展示出来,如名称为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的应用,在项目中的代码结构如下:

image.png

  • 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下:

image.png

在需要模拟数据的页面,添加类似引用:

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;

五、业务开发


六、项目部署


七、总结

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容