记一次Vue Hybrid App(混合APP)开发

您所在的位置:网站首页 vue和html混合开发 记一次Vue Hybrid App(混合APP)开发

记一次Vue Hybrid App(混合APP)开发

2023-08-26 03:35| 来源: 网络整理| 查看: 265

一、 项目开始前的思考 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文件夹放置全局组件 utils-工具插件、或者自己封装的插件 router-项目的路由配置 store-项目的vuex数据存储 view-项目视图,可根据项目模块再划分相应的目录

3.公用css还是需要的,在assets中弄一份pubilc.css,重置样式;css预处理用的是scss 4.适配手机屏幕,用了最常用的rem适配方案,动态计算的js用的是adaptive.js 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;我贴一个示例代码,大家也许会觉得好用

确定 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 } } } @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; } //将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中,这样就好使了。

//比如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 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']) } };

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

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

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

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' ? webkissageHandlers.MOVIE_JSBRIDGE_MESSAGEHANDLE_NAME_OPEN_UPLOADFILE.postMessage(type) : movie_js_app_tool.MOVIE_JSBRIDGE_MESSAGEHANDLE_NAME_OPEN_UPLOADFILE(type) } catch(err) { console.error(err); } } } }

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

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中设置动画

加载中... 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 }, }; @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; } 四、一些优化问题 不要用vue.component直接注册所有组件,这样会使app.js过大import a from '@/components/a.vue’引入组件比import{a,b,c} from '@/components’引入组件,打包的体积小防止app.js过大,可以将vue.js、vue-router.js使用script在index.html中引入,在打包时不打包进去;或者用webpack的DllPlugin将不常改的文件打包成一个文件,既能减少请求又能减小app.js的体积《DllPlugin优化打包性能(基于vue-cli)》路由懒加载当匹配不到路由的时候可以设置跳转到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')


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3