美文网首页
Vue Hybrid App

Vue Hybrid App

作者: 考拉程序媛 | 来源:发表于2019-07-25 09:03 被阅读0次

    一、 项目开始前的思考

    1.浏览设计图、产品原型
    2.需要用到分享功能
    3.怎么和Android和iOS原生方法互掉
    4.网页嵌入到APP中怎么调试
    5.手机屏幕适配
    6.如果出现Loading chunk xx failed该怎么处理
    复制代码
    

    二、搭建项目

    1.使用vue-cli直接创建项目,vue-router、vuex都有用到
    2.划分目录
      api- 将项目的api抽离出来单独放置;
      assets- 放置img、css、font等静态文件;
      components- 放置组件文件,我在当中新建了一个global文件夹放置全局组件
      plugins- 第三方插件、或者自己封装的插件
      router- 项目的路由配置
      store- 项目的vuex数据存储
      view- 项目视图,可根据项目模块再划分相应的目录
    
    3.公用css还是需要的,在assets中弄一份pubilc.css,重置样式;css预处理用的是scss
    4.适配手机屏幕,用了最常用的rem适配方案,动态计算的js用的是[adaptive.js](https://github.com/finance-sh/adaptive);
    5.使用axios来请求数据,axios的拦截器可以干很多事情;
    复制代码
    

    下面贴一份我的axios配置代码

    /**
     * http 配置
     */
    import Vue from 'vue'
    import axios from 'axios'
    import router from '@/router'
    import store from '@/store'
    import Qs from 'qs'//序列化参数
    
    // axios默认配置
    axios.defaults.timeout = 20000; //请求超时时间
    axios.defaults.withCredentials = false;
    axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'; //设置请求头
    axios.defaults.baseURL = '/api'; //baseurl
    
    // http request 拦截器
    axios.interceptors.request.use(
        config => {
            let method = config.method;
            let TOKRN = store.state.access_token;
            //判断是否显示loading
            if(config.isLoading == true && !store.state.isLoading){ 
                store.commit('updateLoadingStatus', true)
            }
    
            //在请求中统一带上token,token从vuex中取
            if (config.data) {
                config.data.access_token = TOKRN;
                config.data = Qs.stringify(config.data);
            } else {
                let url = config.url;
    
                config.url = (url.indexOf("?") != -1) 
                    ? url + '&access_token='+TOKRN 
                    : url + '?access_token='+TOKRN
            }
    
            return config;
        },
        error => {
            store.commit('updateLoadingStatus', false);
            return Promise.reject(error);
        }
    )
    
    //http respone 拦截器
    axios.interceptors.response.use(
        response => {
            let result = response.data;
            let resCode = result.code;
            //对后台返回的状态码进行处理
            switch (Number(resCode)) {
                case 9004:
                    //...
                    break;
    
                case 12000:
    
                    break;
    
                case 12001:
                    //没有实名认证
    
                    break;
    
                case 9000 || 9001 || 9002:
    
                    break;
                default:
    
                    break;
            }
    
            setTimeout(() => {
                if(store.state.isLoading) {
                    store.commit('updateLoadingStatus', false);
                }
            }, 300);
    
            return response.data;
        },
        error => {
            if (error.response) {
            //请求出错,根据http状态码做相应处理
                switch (error.response.status) {
                    case 400:
                        console.log('service 400 操作失败!')
                        break;
                    case 404:
                       // router.push({name:'404'})
                        console.log('service 404 请求不存在!')
                        break;
                    case 408:
                        router.push({name:'500',query:{code:408}})
                        console.log('service 408 请求超时')
                        break;
                    case 500:
                        router.push({name:'500',query:{code:500}})
                        console.log('service 500 内部服务器错误')
                        break;
                }
            }
    
            store.commit('updateLoadingStatus', false);
            return Promise.reject(error);
        }
    )
    
    export default axios;
    复制代码
    

    三、项目开发中

    1.将UI设计师提供的控件图作为公共组件实现,如header、footer、常用btn等; 但是弹窗部分是一个高频使用的组件,每次使用组件又略显麻烦,于是借鉴vux的做法,将弹窗部分做成了vue插件,可以通过this调用,方便省事了不少;我将它发布在了npm,有需要的欢迎来使用v-m-layer;我贴一个示例代码,大家也许会觉得好用

    <!--alert.vue组件-->
    <template>
        <div>
            <transition name="overlay"><div class="mv-modal-overlay"  v-show="show"></div></transition>
            <transition name="modal">
                <div class="mv-modal" id="alert" v-show="show">
                    <div class="mv-modal-inner">
                        <div class="mv-modal-text" v-if="text" v-html="text"></div>
                        <div class="mv-modal-text" v-else><slot></slot></div>
                        <span class="alert-btn" @click="_onOk">确定</span>
                    </div>
                </div>
            </transition>
        </div>
    </template>
    
    <script>
    export default {
        props: {
            text: String,
            value: {
                type: Boolean,
                default: false
            }
        },
        data() {
            return {
                show: false
            }
        },
        created() {
            if(this.value) {
                this.show = true;
            }
        },
        methods: {
            _onCancle() {
                this.$emit('onCancle')
                this.show = false;
            },
            _onOk() {
                this.$emit('onOk');
                this.show = false;
            }
        },
        watch: {
            show(val) {
                this.$emit('input', val)
            },
            value(val, oldVal) {
                this.show = val
            }
        }
    }
    </script>
    
    <style scoped>
    @import url('../../../assets/css/layer.css');
    .alert-btn{
        display: block;
        width: 80%;
        height: 40px;
        line-height: 40px;
        margin-left: 10%;
        margin-bottom: 15px;
        text-align: center;
        font-size: 16px;
        background: #FFD00D;
        color: #242832;
        border-radius: 4px; 
    }
    </style>
    
    复制代码
    
    //将alert.vue封装成插件
    import AlertComponent from '../../components/layer/alert/alert'
    import { mergeOptions } from '../helper'
    
    let $vm;
    
    const plugin = {
        install(vue, options) {
            const Alert = vue.extend(AlertComponent);
    
            if(!$vm){
                $vm = new Alert({
                    el: document.createElement('div')
                })
                document.body.appendChild($vm.$el)
            }
    
            const alert = function(text, onOk) {
    
                let opt = {
                    text,
                    onOk
                }
    
                mergeOptions($vm, opt)
    
                this.watcher && this.watcher();
    
                this.watcher = $vm.$watch('show', (val) => {
                    if(val == false){
                        opt.onOk && opt.onOk($vm)
                        this.watcher && this.watcher();
                    }
                })
                $vm.show = true
            }
    
            if(!vue.$layer){
                vue.$layer = {
                    alert
                }
            } else{
                vue.$layer.alert = alert;
            }
    
            vue.mixin({
                created: function () {
                    this.$layer = vue.$layer
                }
            })
        }
    }
    
    export default plugin
    export const install = plugin.install
    复制代码
    

    2.由于登录是原生实现的,所以在登录完成跳转到h5时要传递相关参数; 开始的做法是原生调用我们h5定义的全局方法,我们在方法中将参数存储到vuex中

    window.GET_AUTHENTICATION = function(token,userId) {
        store.commit('refreshToken', token);//存储token
        store.commit('USER_ID', userId);//存储用户ID
    }
    复制代码
    

    但是这种做法会存在异步的问题,比如进入页面需要用token去获取数据,但是token还没来得及被存储就不好玩了;所以使用第二种方法,让APP跳转时将参数携带在url中,我们在APP.vue入口文件中将url中的参数都存到vuex中,这样就好使了。

    <script>
    //比如APP跳转过来的url是http://192.168.3.56:8081/#/index?token=123456&userid=2&from=ios
        import { mapMutations,mapState } from 'vuex'
        export default {
            name: "App",
            data(){
                return{
    
                }
            },
            created() {
                let url = window.location.href;
                let arr,Json={};  
                let str = null;
                let iterms = null;
    
                if(url.indexOf("?") != -1) {
    
                    str = url.split("?")[1];  
                    iterms = str.split("&");  
    
                    for(var i=0;i<iterms.length;i++){  
                        arr=iterms[i].split("=");  
                        Json[arr[0]]=arr[1];  
                    }  
                }
    
                if(Json.token) {
                    this.refreshToken(Json.token)
                    window.sessionStorage.setItem('token',Json.token)
                    console.log('Token => '+Json.token)
                } 
                if(Json.userid) {
                    this.USER_ID(Json.userid)
                    console.log('userid => '+Json.userid)
                }
                if(Json.from) {
                    this.PLATFORM(Json.from)
                    console.log('platform => '+Json.from)
                } 
            },
            methods: {
                ...mapMutations(['refreshToken','SAVE_MSGCOUNT','USER_INFO','USER_ID','PLATFORM'])
            }
    
        };
    
    </script>
    
    复制代码
    

    3.web和app需要互调方法; 开始想去看看JSBridge怎么使用的,后面APP说他们提供简单的调用方法;

    //h5调用APP的方法,webkit.messageHandlers是原生的方法前缀,MOVIE_JSBRIDGE_MESSAGEHANDLE_NAME_OPEN_UPLOADIDCARD是方法名,postMessage是固定的调用函数,可以传参
    webkit.messageHandlers.MOVIE_JSBRIDGE_MESSAGEHANDLE_NAME_OPEN_UPLOADIDCARD.postMessage(type)
    
    //APP调用h5的方法,只需要h5将方法挂在到window对象即可
    window.getToken = function(token) {
        //....
    }
    复制代码
    

    但是我们Android和iOS两个平台的互调方法不一样,所以需要判断不同的平台执行不同的方法,

    <script>
    export default {
      mounted() {
        const _this = this;
    
            //上传完成后APP返回给H5资源地址,参数({code:'',imgUrl:'',videoUrl:'',msg:''})
            window.RETURN_RESOURCES = function(data) {
                if(data.code == 1) {
                    _this.params.avatar = data.imgUrl;
                } else{
                    _this.$layer.toast(data.msg ? data.msg : '未知错误!')
                }
            }
      },
      methods: {
        //上传头像
        openAppFile(type){
          const platform = this.$store.state.platform;//区分是iOS还是Android
    
          try {
            platform == 'ios'
            ? webkit.messageHandlers.MOVIE_JSBRIDGE_MESSAGEHANDLE_NAME_OPEN_UPLOADFILE.postMessage(type)
            : movie_js_app_tool.MOVIE_JSBRIDGE_MESSAGEHANDLE_NAME_OPEN_UPLOADFILE(type)
          } catch(err) {
            console.error(err);
          }
        }
      }
    }
    </script>
    复制代码
    

    4.当页面在手机上运行时,出现错误我们不好查看错误,不好去追踪;但是好在有vconsole这个插件,可以使我们在手机上查看控制台信息。

    [图片上传失败...(image-a6daef-1564016523390)]

    <figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>

    5.在iOS上点击事件是有300ms延迟的,可以引入fastclick来解决

    //main.js
    import FastClick from 'fastclick'
    FastClick.attach(document.body);
    复制代码
    

    6.为了看起来像APP,在页面切换时需要有切换动画;想了半天没有什么好的方案,在逛GitHub时发现了一个还不错的方案。 在vuex中存一个变量isBack:false,只要isBack为false就是执行前进动画,为true就执行后退动画;但何时为false,何时为true呢? https://github.com/zhengguorong/pageAinimate

    // 只要页面切换,并且执行了300ms的动画就设置为false
    router.afterEach((to, from, next) => {
        setTimeout(() => {
            store.commit('SAVE_BACK',false);
        }, 300);
    });
    
    //监听返回事件,只要用户点击了返回就设置为true,这样就执行了返回动画,根据上面的代码,300ms后就会自动设置为false;
    //以此推,只要没有监听到返回事件,执行的都是前进动画;监听到了返回事件就执行后退动画,后退动画执行完就会300ms后就会自动设置为false
    //router.back()和router.go(-1)会触发返回事件
    window.addEventListener('popstate', function (e) { //监听返回事件
        store.commit('SAVE_BACK',true);
    }, false)
    复制代码
    

    在APP.vue中设置动画

    <template>
        <div id="app">
            <transition :name="viewTransition" >
                <router-view v-if="!$route.meta.keepAlive" class="child-view"></router-view>
            </transition>
    
            <loading v-show="isLoading">加载中...</loading>
        </div>
    </template>
    
    <script>
        import { loading,confirm } from '@/components/layer'
        import { mapMutations,mapState } from 'vuex'
        export default {
            name: "App",
            data(){
                return{
                    transitionName: 'slide-left',
                }
            },
            methods: {
                ...mapMutations(['refreshToken','SAVE_MSGCOUNT','USER_INFO','USER_ID','PLATFORM'])
            },
            computed:{
                ...mapState({
                    isBack: state => state.isBack,
                    isLoading: state => state.isLoading,
                    route: state => state.route
                }),
                viewTransition() {
                    if (this.route.meta && typeof this.route.meta.index === 'number') {return ''};
                    return this.isBack ? 'slide-right' : 'slide-left';
                }
            },
            components:{
                loading,
                confirm
            },
        };
    
    </script>
    
    <style>
    @import url('./assets/css/public');
    
    #app{
        display: block;
        width: 100%;
    
    }
    .child-view {
        position: absolute;
        top: 0;
        bottom: 0;
        left: 0;
        right: 0;
        transition: transform 300ms;
        will-change: transform;
        background: #181B22;
        -webkit-backface-visibility: hidden;
        backface-visibility:hidden;
        perspective: 1000;
    }
    
    .slide-left-enter,
    .slide-right-leave-active {
        -webkit-transform: translate3d(100%,0,0);
        transform: translate3d(100%,0,0);
        z-index: 1;
    }
    
    .slide-left-leave-active,
    .slide-right-enter {
        -webkit-transform: translate3d(0,0,0);
        transform: translate3d(0,0,0);
        z-index: -1;
    }
    
    </style>
    
    复制代码
    

    四、一些优化问题

    1. 不要用vue.component直接注册所有组件,这样会使app.js过大
    2. import a from '@/components/a.vue'引入组件比import{a,b,c} from '@/components'引入组件,打包的体积小
    3. 防止app.js过大,可以将vue.js、vue-router.js使用script在index.html中引入,在打包时不打包进去;或者用webpack的DllPlugin将不常改的文件打包成一个文件,既能减少请求又能减小app.js的体积《DllPlugin优化打包性能(基于vue-cli)》
    4. 路由懒加载
    5. 当匹配不到路由的时候可以设置跳转到404页面,防止出现空白页面
    router.beforeEach(function (to, from, next) {
        if(to.name == null) {
            next({name:'404'})
        }
        next()
    })
    复制代码
    

    6.用户点击过的A模块被浏览器缓存了,当再重新打包上线后,用户在A模块依然是读取的缓存可以正常浏览;如果从A模块中点击链接到B模块中,由于每次打包的文件hash值不同,导致从服务器中找不到该模块,所以就抛出了Loading chunk xx failed的错误。所以需要捕捉模块加载的错误

    //routerUtils.js
    import router from '../router'
    import store from '../store'
    
    export default {
        catchImport(err) {
            try {
                console.log('我已经捕捉到了router Loading chunk fail错误');
                let routeName = store.state.route.name;
                if(routeName && routeName.indexOf('recruit') != -1) {
                    router.push({name:'recruitIndex'});
                } else{
                    router.push({name:'index'});
                }
    
                setTimeout(() => {
                    window.location.reload();
                }, 500);
    
            } catch (error) {
                console.log('router:'+error)
            }
        }
    }
    复制代码
    
    import routerUtils from '../plugins/routerUtils'
    //一个模块设置一个捕获
    const index = () => import(/* webpackChunkName: "index" */ '@/view/home/index/').catch(routerUtils.catchImport)
    const artistResume = () => import(/* webpackChunkName: "index" */ '@/view/home/artistResume')
    

    相关文章

      网友评论

          本文标题:Vue Hybrid App

          本文链接:https://www.haomeiwen.com/subject/dchcrctx.html