web app的敏捷开发

2017/11/14 程序

web app是指基于web的系统和应用,作用是通过web站点完成与用户的交互。web app运行在网络浏览器,基于网页开发技术实现交互功能。一种常见的应用场景是,通过微信公众号的入口,在下方的菜单链接跳转到公众号运营者的网站,获取相关信息以及网络服务。好处是用户不需要安装一个app到本地,而是通过网络来获取对应的服务。

目录

核心要素

完成一个web app需要的几个核心要素:前端设计、数据库、后端处理。它们分别扮演着不同的角色,是整套服务构建的根基。

前端设计

传统的网页前端设计采用HTMLCSSJavaScript三大技术来完成。其中HTML负责网页的框架布局,CSS负责网页元素的样式,JavaScript负责网页与用户的交互功能。其中JavaScript不是必须的,静态网页就很少需要JS的支持。在现代web app中,传统的三大技术依旧是必须的,这意味着你需要对这三种技术有一定的了解。

web app的前端设计面临的问题是:如何设计出能够媲美真实app的样式,以及如何模拟出与真实app相同的交互。例如常见的按钮(Button)、输入框(Input)、选项(Options)、提示(Alert)、确认(Confirm)、表单(Form)等等,由于web app的应用场景是手机移动端,这些样式设计需要考虑到不同设备的屏幕适应问题。而交互方面,例如常见的登录(Sign in)、注册(Sign up)、支付(Pay)、收藏夹(Favourite)、授权(Auth)、分享(Share)、评论(comment)等等,就需要考虑到Cookies、Sessions、跨域、鉴权、异步请求等与网络相关的细节。

如果使用传统的网页开发技术,也就是直接编写HTMLCSSJavaScript三个文件的代码,会发现工作量很大、效率低、速度慢并且功能上也不尽人意。有没有办法可以加快这个过程呢?

数据库

数据库可以分为关系型数据库非关系型数据库

关系型数据库的最大特点是事务的一致性(Consistency),指事务执行的结果是使数据库从一个一致性状态变到另一个一致性状态。举一个简单的例子,一个银行系统的数据库中保存了100个用户的资金数额,我们定义一致性状态是这100个用户的资金的总额。那么事务执行也就是对这个数据库的增、删、改、查动作不能够改变这100个用户的资金总额,便是维持了数据库的一致性。如果某一个事务执行之后,导致了这100个用户的资金总额发生了变化(变多或者是变少),那么我们说这个数据库不具备一致性。

关系型数据库为了保持一致性需要付出的代价是读写性能差,面对类似微博、微信等高并发应用,关系型数据库则力不从心。前段时间有一则新闻提到,歌手鹿晗在微博上发出一条消息后短时间内引起千万级的评论、转发,瞬间瘫痪微博的服务器,可见SNS型的应用对高并发的要求十分高。那么针对web app,这种事务一致性是不是必须的呢?

现代web app对这种需求已经减弱,例如用户A以及用户B看到用户C的内容更新不一致是可以容忍的,最简单的例子莫过于两位好友看到另一位好友的朋友圈消息更新的时间差是可以忍受的。由此非关系型数据库应运而生,它不依赖于固定的表结构,具备高并发的读写能力。根据业务的不同又能分为几种非关系型数据库,例如面向高性能并发读写的key-value数据库、面向海量数据访问的文档数据库、面向可拓展性的分布式数据库等等。

在web app中,常见的数据库选型是mySQLMongoDBRedis

后端处理

根据业务的不同,后端处理也会分为很多层。后端处理前端的交互命令,将完成的消息返回到前端,在我的理解中,它的最主要的目的是完成对前端的数据储存以及身份认证。常见的后端处理有认证授权数据库操作封装数据清洗

认证授权的目的是获得第三方API的能力,例如基于微信公众平台的网络服务,如果我需要知道用户的昵称、用户的头像等信息等,就需要完成与微信认证服务器之间的数据交换,经过一系列的授权获得微信app的功能,例如微信支付能力以及社交分享能力。

数据库操作封装是指在数据库之外封装一层接口,对数据的操作委托后端作转发,这样能够避免数据库直接暴露在互联网,前端并没有直接与数据库进行联系。

数据清洗是对于用户的数据进行过滤、安全性检查。任何用户的任何输入都必须认为是有害的、怀有恶意的,需要经过过滤器、正则规范化之后才能进入数据库。数据的规范化可以交由前端来完成,例如手机号码的格式、密码的强弱,而浏览器的地址输入则需要后端的数据清洗。

第一个web app

工具准备

  1. 引起注意 建议使用类Linux操作系统,例如UbuntuCentOS或者是MacOS,如果你使用windows操作系统,这会有些挑战。下面是针对MacOS的演示。
  2. Terminal,或者是你喜爱的各种各样的终端工具。
  3. 文本编辑器,例如Sublime TextVisual Studio Code
  4. Google Chrome浏览器。

你会发现开发web app时,规模巨大的IDE(集成开发环境)不是必须的。

安装依赖环境

引起注意 这个过程会非常轻松、简单,取决于你使用的网络。

Node.js

Node.js® 是一个基于Chrome V8引擎的JavaScript运行时。 Node.js使用高效、轻量级的事件驱动、非阻塞I/O模型。它的包生态系统npm,是目前世界上最大的开源库生态系统。在这里Node.js发挥的作用是本地调试的时候提供一个轻量级服务器环境,实时动态调试并返回结果。

访问 这里 下载安装包并安装好Node.js。安装完成之后在Terminal中输入:

$ node -v

来检查Node.js是否正确安装。

cnpm

Node.js正确安装之后会自带npm包管理工具,这个工具用于安装项目需要使用的js模块。由于部分原因使用npm去下载js模块的速度很慢,所以这里采用cnpm来取代。

Terminal中输入:

$ npm install -g cnpm --registry=https://registry.npm.taobao.org

Vue.js

Vue.js (读音 /vjuː/,类似于 view) 是一套构建用户界面的渐进式框架。与其他重量级框架不同的是,Vue 采用自底向上增量开发的设计。Vue 的核心库只关注视图层,它不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与单文件组件Vue 生态系统支持的库结合使用时,Vue 也完全能够为复杂的单页应用程序提供驱动。

Vue.js是目前主流三大前端框架之一,另外两个是Angular.jsReact.js。使用Vue.js可以便捷的生成上面前端设计提到的三种代码,它通过模版语法将数据渲染进入DOM系统。这里我们使用vue-cli脚手架快速搭建基于Vue.js的SPA单页面web app应用。

Terminal下进入工作目录,输入:

$ vue init webpack test

? Project name test
? Project description A Vue.js project
? Author alienx
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? Yes
? Pick an ESLint preset Standard
? Setup unit tests Yes
? Pick a test runner jest
? Setup e2e tests with Nightwatch? Yes

$ cd test
$ cnpm install
$ npm run dev

恭喜 你已经创建了第一个web app,在chrome浏览器的地址栏输入localhost:8080访问。由于我们需要创建适配手机屏幕的web app,所以在chrome的开发者工具中开启device toolbar来观察显示的效果。

项目结构

用任何一款你喜爱的文本编辑器打开test文件夹,观察其中的文件目录:

/build
/config
/dist
/node_modules
/src
/static
/test
index.html
package-lock.json
package.json
README.md

这里只需要了解几个重要的文件目录:

  1. /config:里面的文件用于配置服务器的端口、转发规则等。
  2. /dist:项目完成后所生成的/static以及index.html将会保存在这里。
  3. /node_modules:项目中所有使用到的node模块储存在这个文件夹中。使用npm对该项目添加新的模块也会保存在这个文件夹。
  4. /src/App.vue:项目调试阶段的全局组件。
  5. /src/assets:项目使用的静态资源例如图片可以保存在这里。
  6. /src/components:项目中的页面组件保存在这里。
  7. /src/main.js:项目调试阶段的全局入口与配置。
  8. package.json:保存当前项目需要的包索引。

Vux.js

Vux(读音 [v’ju:z],同views)是基于WeUI和Vue(2.x)开发的移动端UI组件库,主要服务于微信页面。基于webpack+vue-loader+vux可以快速开发移动端页面,配合vux-loader方便你在WeUI的基础上定制需要的样式。

进入到项目文件的根目录,安装Vux:

$ npm install vux --save

Vuex.js

Vuex是一个专为Vue.js应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。使用Vuex的目的是以全局调用的方式在本地缓存页面之间的交换数据,完成在SPA单页面应用中组件之间的数据传递。

进入到项目文件的根目录,安装Vuex:

$ npm install vuex --save

Less.js

Less是一门CSS预处理语言,它扩充了CSS语言,增加了诸如变量、混合(mixin)、函数等功能,让CSS更易维护、方便制作主题、扩充。使用Less可以用更高级的封装模式来完成针对CSS的编写。

进入到项目文件的根目录,安装Less:

$ npm install less --save

编写代码

main.js

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import FastClick from 'fastclick'
import App from './App'
import router from './router/index.js'
import { LoadingPlugin, AlertPlugin } from 'vux'
import Vuex from 'vuex'
import store from './vuex/store'

Vue.use(LoadingPlugin)
Vue.use(AlertPlugin)
Vue.use(Vuex)

FastClick.attach(document.body)

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app-box')

在这个文件中,import部分主要引入全局App、页面路由、Vux的载入、提醒模块、Vuex状态管理器以及store的全局储存。

App.vue

<template>
  <div id="app">
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: 'app'
}
</script>

<style lang="less">
@import '~vux/src/styles/reset.less';

body {
  background-color: #fbf9fe;
}
</style>

App.vue是一个全局组件,所有的页面组件将会封装在这里,可以认为该组件是所有组件的父组件。

/src/vuex/store.js

引起注意 该文件需要手动创建。

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    Mask_Mobile_Phone_Number: '', // 经过校验的手机号码
    weixin_nickname: '' // 微信昵称
  },
  mutations: {
    // 拦截newUser请求,并对state中的变量进行储存
    newUser (state, msg) {
      state.Mask_Mobile_Phone_Number = msg.Mask_Mobile_Phone_Number
      state.weixin_nickname = msg.weixin_nickname
    }
  }
})

export default store

该文件用于全局储存。其中变量state是一个全局缓存器,保存了两个重要变量Mask_Mobile_Phone_Numberweixin_nickname。而mutations是一个拦截器,用于抓捕引起全局变量发生变化的动作。在后面的组件中,通过发起mutations请求来完成数据的写入。

/src/router/index.js

引起注意 该文件需要手动创建。

import Vue from 'vue'
import Router from 'vue-router'

// Components List
import login from '@/components/login'
import mainInfo from '@/components/mainInfo'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/login',
      component: login
    },
    {
      path: '/mainInfo',
      component: mainInfo
    }
  ]
})

该文件的作用是引导SPA单页面的跳转,它负责告诉渲染引擎对指定的路径跳转到指定的组件,例如对于根目录下/login的访问则会加载login.vue组件。

/assets/login/blurtemp.jpg

引起注意 该文件需要手动创建。

这是一张静态图片资源,你可以使用任何喜爱的图片并按照上面的路径命名。

创建组件[重点]

后缀名为vue的文件在这里被称作为组件,它由三个基本元素组成,分别是<template>,<script>,<style>,对应的就是传统的HTML,JavaScript,CSS代码部分。通过编写vue的代码可以在调试阶段即刻生成上述3种技术的对应内容,从而提高效率。

/src/components目录下分别创建3个组件,命名为:

  1. blurpic.vue
  2. login.vue
  3. mainInfo.vue

下面分别解释其中的代码设计。(由于博客中的语法高亮没有兼容Less语法,所以部分代码高亮可能表现很糟糕)

blurpic.vue
<template>
  <div>
    <blur :blur-amount=10 :url="url">
      <p class="center"><img :src="urlvalue" /></p>
      <p :style="{color:textcolor,'text-align':'center','font-size':'22px'}"></p>
    </blur>
  </div>
</template>

<script>
import { Blur } from 'vux'

export default {
  components: {
    Blur
  },
  props: {
    urlvalue: {
      type: String
    },
    url: {
      type: String
    },
    nickname: {
      type: String
    },
    textcolor: {
      type: String
    }
  },
  data () {
    return {
    }
  }
}
</script>

<style scoped lang="less">
.center {
  text-align: center;
  padding-top: 20px;
  color: #fff;
  font-size: 18px;
  img {
    width: 100px;
    height: 100px;
    border-radius: 50%;
    border: 4px solid #ececec;
  }
}
</style>

该组件是依托Vux下的一个模糊窗口组件,用来显示一个头像还有用户昵称。这个组件接受4个参数,分别是urlurlvaluenicknametextcolor,分别对应的是背景层的图片地址、头像层的图片地址、昵称、昵称字体颜色。props域的作用是用于父子组件之间的数据传递,读到这里可能有疑问,文章前面提到了使用Vuex的全局储存作为数据传递,跟这里的props域有什么区别吗?

props域起作用的是父组件与子组件之间的数据传递,而Vuex起作用的是子组件与子组件之间的数据传递。稍后你会发现,该组件是login.vue的子组件,而login.vuemainInfo.vue是子组件与子组件的关系,它们之间并没有继承关系。

login.vue
<template>
  <div>
    <blurpic :urlvalue="urlvalue" :url="urlvalue" :nickname="nickname" textcolor="white"></blurpic>
    <box gap="10px 10px">
      <div class="inputbox">
        <group>
          <x-input placeholder="请输入手机号码" keyboard="number" is-type="china-mobile" v-model="Mask_Mobile_Phone_Number" ref="Mask_Mobile_Phone_Number_Ref" class="weui-vcode">
            <x-button disabled slot="right" type="primary" mini v-show="!Computered_Time && !Get_Vacode">获取验证码</x-button>
            <x-button slot="right" style="margin-top:0px;" type="primary" mini @click.native="Send_Vacode" v-show="!Computered_Time && Get_Vacode">获取验证码</x-button>
            <x-button disabled style="margin-top:0px;" slot="right" type="primary" mini v-show="Computered_Time">已发送()</x-button>
          </x-input>
          <x-input placeholder="请输入验证码" keyboard="number" class="weui-vcode" v-model="Input_Vacode"></x-input>
        </group>
      </div>
    </box>
    <box gap="10px 10px">
      <x-button type="primary" @click.native="submit" >提交</x-button>
    </box>
    <div v-transfer-dom>
      <alert v-model="errorshow" title="错误"></alert>
    </div>
  </div>
</template>

<script>
import { XHeader, Group, Cell, Blur, Selector, XInput, XButton, Box, Alert, Flexbox, FlexboxItem, TransferDomDirective as TransferDom } from 'vux'
import blurpic from '@/components/blurpic'
import blurtemp from '@/assets/login/blurtemp.jpg'

export default {
  components: {
    XHeader,
    Group,
    Cell,
    Blur,
    Selector,
    XInput,
    XButton,
    Box,
    blurpic,
    blurtemp,
    Alert,
    Flexbox,
    FlexboxItem
  },
  directives: {
    TransferDom
  },
  data () {
    return {
      Mask_Mobile_Phone_Number: '', // 经过校验的手机号码
      Computered_Time: 0, // 再一次发送验证码前的倒计时
      Get_Vacode: false, // 是否已经发起申请验证码
      Input_Vacode: '', // 输入的验证码
      urlvalue: blurtemp, // 向bulrpic组件传递的图片地址
      nickname: '小神先生', // 昵称
      errorshow: false, // 是否弹出错误报告
      errormsg: '' // 错误内容
    }
  },
  watch: {
    Mask_Mobile_Phone_Number: function () {
      if (this.Mask_Mobile_Phone_Number !== '' && this.$refs.Mask_Mobile_Phone_Number_Ref.valid === true) {
        this.Get_Vacode = true
      } else {
        this.Get_Vacode = false
      }
    }
  },
  methods: {
    // When user click button 'Send Vacode'. Connect to the message API.
    Send_Vacode () {
      this.Computered_Time = 60
      this.$vux.alert.show({
        content: '发送成功'
      })
      setTimeout(() => {
        this.$vux.alert.hide()
      }, 2000)
      this.timer = setInterval(() => {
        this.Computered_Time --
        if (this.Computered_Time === 0) {
          clearInterval(this.timer)
        }
      }, 1000)
    },
    submit () {
      if (this.Mask_Mobile_Phone_Number === '') {
        this.errorshow = true
        this.errormsg = '没有填写手机号码'
      } else if (this.$refs.Mask_Mobile_Phone_Number_Ref.valid === false) {
        this.errorshow = true
        this.errormsg = '手机号码不正确'
      } else if (this.Input_Vacode === '') {
        this.errorshow = true
        this.errormsg = '没有填写验证码'
      } else {
        this.$vux.loading.show({
          text: '跳转中'
        })
        setTimeout(() => {
          this.$store.commit('newUser', {
            Mask_Mobile_Phone_Number: this.Mask_Mobile_Phone_Number,
            weixin_nickname: this.nickname
          })
          this.$vux.loading.hide()
          this.$router.push('/mainInfo')
        }, 1000)
      }
    }
  }
}
</script>

<style scoped lang="less">
@paddingvalue: 5px;
@gray: #888888;
.inputbox {
  border: solid 1px @gray;
  border-radius: @paddingvalue * 3;
  padding: @paddingvalue;
  background-color: white;
  box-shadow: 3px 3px 5px @gray;
}
</style>

  1. data域寄存这张页面的变量,你会注意到部分变量已经被初始化,它们在页面渲染后表达。而部分变量没有被初始化,它们在后续的交互动作中填入数值。
  2. watch域用于监控属性,其中的Mask_Mobile_Phone_Number变量的任何一次改变都会触发监视器。这里用于校验用户输入的手机号码是否符合规范。
  3. methods域用于系统交互,其中有两个方法,分别是Send_Vacodesubmit。前者用于演示发送验证码这个动作,而后者用于检查用户的输入并跳转到下一个页面。
mainInfo.vue
<template>
  <div>
    <x-header>我的信息</x-header>
    <div class="ui_content">
      <div class="bindInfo">
        <div style="float:left; padding-top:2px;"><icon type="download"></icon></div>
        <div class="title"><p>上一个页面输入的信息</p></div>
        <div class="content">
          <p>手机号码: </p>
          <p>微信昵称: </p>
        </div>
      </div>
      <box gap="10px 10px">
        <x-button type="warn">恭喜</x-button>
      </box>
    </div>
  </div>
</template>

<script>
import { XHeader, Icon, XButton, Box } from 'vux'

export default {
  components: {
    XHeader,
    Icon,
    XButton,
    Box
  },
  computed: {
    weixin_nickname () {
      return this.$store.state.weixin_nickname
    },
    Mask_Mobile_Phone_Number () {
      return this.$store.state.Mask_Mobile_Phone_Number
    }
  }
}
</script>

<style scoped lang="less">
@paddingvalue: 3px;
@gray: #cccccc;
.ui_content {
  border-width: 0;
  overflow: visible;
  overflow-x: hidden;
  padding: 1em;
  .bindInfo {
    border-radius: @paddingvalue * 2;
    background-color: white;
    box-shadow: 3px 3px 5px @gray;
    .title {
      color: white;
      background-color: #0080ff;
      border-top-left-radius: @paddingvalue * 2;
      border-top-right-radius: @paddingvalue * 2;
      height: 32px;
      p {
        padding-left: 10px;
        font-size: 19px;
      }
    }
    .content {
      padding: 10px;
      p {
        font-size: 17px;
      }
    }
  }
}
</style>

其中的computed域用于计算属性,在页面渲染时从全局储存store中获取login.vue页面中填入的数据并表达。

效果图

你已经完成了一个SPA单页面应用,赶快在浏览器中尝试使用!

引起注意 可以访问 这里 来查看其显示效果,需要开启浏览器的移动端显示界面。例如你使用Chrome浏览器,请打开device toolbar获得更佳的浏览体验。

或者你可以在手机上访问这篇文章。

发布

调试阶段结束后,在Terminal中输入:

$ npm run build

等待编译完成后,内容会被打包到/dist文件夹,里面包含一个/static以及index.html,将这两个文件上传到HTTP服务器,任务到这里就结束了。

结语

使用以Vue.js为核心的前端框架进行敏捷的web app开发,不仅效率大大提高,而且学习成本降低,提高生产力。但是稍微注意你会发现前端工作变得更加繁杂,传统的前端工程师需要掌握HTMLCSSJavaScript三种基本基本的技术,而现在的前端需求已经不仅限于此了。

虽然有挑战,但是我相信新的技术会带来更好的开发体验。

彩蛋

  1. 其中的登录组件在前端完成了手机号码的验证以及表单的输入完整性,这是可行的,但是更加严格的数据清洗还是要交给后端处理。
  2. vuex是一种状态储存,不是永久储存。所以页面的跳转时如果发生了刷新或者是重新加载,那么store中的所有数据都会丢失,因此对于持久数据应当要使用数据库来存储。

Search

    Table of Contents