感谢--->记一次Vue Hybrid App(混合APP)开发
浅谈App Hybrid混合开发的五种方案
一、 项目开始前的思考
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;我贴一个示例代码,大家也许会觉得好用
确定
//将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中,这样就好使了。
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两个平台的互调方法不一样,所以需要判断不同的平台执行不同的方法,
4.当页面在手机上运行时,出现错误我们不好查看错误,不好去追踪;但是好在有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中设置动画
加载中...
四、一些优化问题
- 不要用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')