您的当前位置:首页正文

《红包日历》ios版本——基于vue全家桶的webapp单页应用

来源:要发发知识网

项目介绍:制作一个网赚应用
平台:ios手机
技术选型:vue + vuex + vue-router + vue-resource + webpack + es6 + sass + postcss
最终效果图:更多可到App Store下载“悦动music”


首页 tipLayer

背景

毕业之后首个用新技术单独完成的项目,项目是从2015.10月开始的,当时vue还没有2.0。

产品需求

业务角度(不展开说,有兴趣私聊)
用户做任务(下载应用)-->检测有效性-->给分给用户

技术角度

  1. 可快速迭代
  2. 获取ios手机上的一些信息
  3. 用户体验友好

技术架构

出于以下的考量:
a. 获取ios手机上的一些信息,只能通过ios客户端来实现,这是业务的重心
b. ios客户端需要上传到App Store,每次迭代都需要至少2天的审核,这样对可快速迭代不利
c. 因为a的一些实现跟App Store的规则有打擦边球的嫌疑,有时候会突然下架

参考竞品,以及综合分析,然后我们定出了这样的架构:
Web App + Native App + 后端


架构

这里稍微说一下
黑线路径,要经过客户端,发挥客户端的优势,譬如说加密、一些客户端的功能(譬如说截图、第三方软件分享、登录)
蓝色路径,只要前端跟客户端拿到token,就可以直接跟后端通讯,免除每次都经过客户端

前端技术选型分析

单页面应用架构,有以下特点:
1)在一个页面下切换视图,而不需要重新加载整个网页,这样一来就减轻了加载资源的负载、缩短了用户的等待时间;
2)路由控制视图切换
3)组件化开发,利于分治、复用
4)MV*,免掉繁重的dom操作
5)方便共享数据

Vue是前端MVVM框架,它实现了组件化、模板渲染等功能
VueRouter可以控制路由,从而切换视图
VueResource封装了Promise的写法以及对Restful API更友好

最后不谋而合,我们就选取了Vue全家桶来制作单页应用

当然单页应用也有缺点:
1)首次加载比较慢
2)对SEO不友好
3)浏览器本身的历史回退

JUST DO IT √

  • 构建项目
    vue-cli:目录结构
    webpack gulp:构建项目,压缩代码,自动化脚本,打包代码
    npm:包管理

  • 开发flow
    git flow
     搭建开发/测试环境:webpack-dev-server hot-reload webpack.conf
    webpack打包大小优化:code splitting、压缩
    webpack本地构建优化:把第三方库放在vendor或者externals等等

  • 功能区分以及开发
    utils
    config
    mixins
    公用组件
    view

  • 布局
    z轴上,采用weui的规范
    区分公用组件和view进行布局
    自适应布局flexible.js + rem + flex布局
    -webkit-overflow-scrolling : touch造成的堆叠上下文


vue的功能:(√ 表示项目中用到的)

  • 数据驱动更新视图 √
  • 试图切换&过渡效果 √
  • 路由 √
  • 组件之间的通讯※ √
  • 状态管理 √
  • vdom
  • 单元测试
  • 后端渲染

制作的过程中,我觉得组件间通讯比较重要:
vue1.x:
方法①broadcast、dispatch(父子、兄弟组件通讯)vue2.x废弃该方法
方法②this.$root、this.$children(父子、兄弟组件通讯)
方法③prop、emit(父子组件通讯)vue1.x prop支持双向绑定
方法④this.$refs(父->子单向通讯)
vue2.x:
方法①event bus(兄弟组件通讯)
方法②prop、emit(父子组件通讯)vue2.x prop不支持双向绑定


遇到的问题

如果不跨域,前端直接使用document.cookie,发送请求到后端,会自动带上cookie;
如果跨域,默认是不带cookie的,如果需要跨域带上cookie需要做以下步骤:
1)前端设置cookie的domain为后端的域名

document.cookie = 
document.cookie = 
// 设置允许跨域的域名,注意如果是跨域传送cookie,是不能设置为*的,必须指定域名
Access-Control-Allow-Origin: 
// 设置允许跨域共享cookie
Access-Control-Allow-Credentials: true

浏览器将CORS的请求分为:简单请求非简单请求
简单请求必须同时满足以下要求,否则为非简单请求:

来源:阮一峰博客

3. 组件:无限滚动
关键点:
1)判断滚动到底部,触发拉取新数据,添加新数据
判断滚动是否到底部有用到:滚上去的高度scrollTop + 页面的高度clientHeight === 网页的高度scrollEle.offsetHeight
2)零部件:
设置flag变量,防止滚动到底部发送多个请求;
设置page、size变量表示拉取的页数、数据条数;
3)优化点:
首次进入的时候,未获取到数据的时候,用变量loading来记录;
当加载完毕(条目<size || 第一个的size===0),用变量nodata来记录。
使用-webkit-overflow-scrolling: touch在ios端滑动起来体验很好

可继续优化的点:
上拉加载更多,或者下拉加载更多,有多余的块显示

4. 布局、组件与功能的考虑
首先我们把组件分为公用组件和私有组件,后来看到资料,发现私有组件都在view(视图)里面,所以应该是这样分类:components(组件)和view(视图)。

然后以App.vue为根组件,公共组件和router view挂载在App.vue下,大概是这样的:

我的布局

其中遇到的问题:
1)一些可复用组件,譬如说confirm组件,它的模子就只有提示框的骨架,其中的内容需要用slot来写。它应该放在公共组件的位置,还是view里面?
当初我没有细想,就放在公共组件的位置,执行起来的时候,遇到超级不爽的地方:
①每个view都要跟公用组件通讯,传递提示框的自定义信息,包括插图地址、主标题、副标题、正文、提示;
②每个提示框的“取消事件”还好说,都是把confirm组件隐藏掉;但是”确定事件”就不是每个confirm组件是一样的,所以也需要动态绑定。
这种方案用以下两种方法实现组件间的通讯:
a. confirm组件作为全局组件,状态记录在vuex。这样对于①的操作就很简单了,传一个json过去就可以;但问题在于如何动态绑定“确定事件”,我的做法是在vuex添加一个变量yesCounter,每次confirm组件的“确定”按钮点击之后,yesCounter就+1,在view里面watch这个变量(yesCounter),方法写在view的methods里面,当监测到改变就触发事件。

b. confirm作为公共组件,挂在在App.vue下,指定组件名称confirm。在view里面,用this.$root.refs.confirm来调用里面的东西、以及赋值。

c. confirm组件作为view里面的组件,对于①的操作,很直观简单;对于②的操作用emit事件;而且这个方案的好处在于“按需加载”,因为有些view是不需要confirm组件的。

于是我采用方案c,但是呢,这又有一个问题,就是当confirm组件的出现的时候,我希望它把全屏遮住了,但是它又内嵌到view里面。《当时我没找到方法,就徘徊地用回方案a,但是的确太恶心了,就狠下心来把方案c产生的问题解决掉(这种习惯应该抛弃啊!)。当时想了三个方案:
i) app__header、app__content放在同一个wrapper里面,控制content的高度,超出的范围滚动条显示。这种ok


方案 i

ii) app__header、app__content放在同一个wrapper里面,但是是使用flex布局的。这种方案肯定不可以,因为view怎么样都覆盖不了header的;

iii) app__header用fixed布局,app__content的高度是100%,header跟content的堆叠上下文是相同的,但是我需要把header置顶,所以直接把header的顺序放在下面。然后放在app__content的confirm组件设置z-index就可以了。

App.vue confirm weui页面层级

5. 抽象view的逻辑 && promise && es6
由于每个view都有以下特点:
①每次加载的时候都会向后端ajax请求数据;
②通过设置route的data选项,如果①请求数据失败,视图就切换回去之前的;
③每次按刷新的时候,向后端ajax请求数据,更新data;

稍微分析一下,其实①②③的加载数据是可以复用的,但是②中,路由的data勾子要传入transition这个变量,用transition.next()和transition.abort()控制视图切换,是否但是在①③不需要。综合以上需求,就写了一个mixin,如下:

export let routerDataMixin = {
    route: {
        data: function (transition) {
            var that = this;
            if (this.assist.token && this.fetchOption) {
                new Promise(function(resolve, reject) {
                    that.fetchData({resolve, reject})
                })
                .then(function(data){
                    transition.next();
                })
                .catch(function(error){
                    console.log(error);
                    transition.abort();
                })
            } else {
                transition.next();
            }
        }   
        //waitForData: true
    },
    methods: {
        fetchData: function(...rest) {  // 用上es6的rest,很方便
            if (!this.fetchOption) {
                return false;
            }

            this.$http.get(
                this.fetchOption.url,
                {
                    params: this.fetchOption.params || {},
                    credentials: /hongbaorili/g.test(this.fetchOption.url)
                }
            ).then(
            function (response) {
                if (response.data.c === 0) {
                    if ( this.fetchSuccess ) {
                        this.fetchSuccess(response);
                    } else {
                        this.userData = response.data.d
                    }
                    try {
                        rest[0].resolve();
                    } catch(e) {}
                } else if (response.data.c === -10000){
                } else {
                    if ( this.fetchAbnormal ) {
                        this.fetchAbnormal(response);
                    }

                    try {
                        rest[0].reject(new Error("fetchData: c!=0"))
                    } catch(e) {}
                }
                this.endProgress();
            },
            function (response) {
                if ( this.fetchFail ) {
                    this.fetchFail(response);
                } else {
                    this.showToast();
                }

                try {
                    rest[0].reject(new Error("fetchData: fail"));
                } catch(e) {}

                this.endProgress();
            });
        }
}

7. Restful API
增删查改
post del get put
后端同事说项目小没必要这么复杂,就只做了get和post