I. Thinking before the project begins

Browsing design drawings and product prototypes 2. Sharing function is needed 3. How to interoperate with Android and iOS native methods 4. How to debug the webpage embedded in APP 5. Screen adaptation of mobile phone 6. What should be done if Loading chunk xx failedCopy the code

Ii. Construction project

1. Use vue-CLI to create a project. Vue-router and vuex are used to 2. Partition API - Separate the project API and place it separately; Assets - Places static files such as IMG, CSS, and font. Router - project routing configuration store- project vuex data store view- project view The corresponding directory 3 can be further divided according to the project module. Public CSS is still needed. Get a copy of Pubilc. CSS in Assets and reset the style. CSS pretreatment with SCSS 4. Adaptation the phone's screen, with the most commonly used rem adaptation scheme, the dynamic calculation of js with [the adaptive. Js] (https://github.com/finance-sh/adaptive); 5. Use AXIos to request data, axios's interceptor can do a lot of things;Copy the code

Here is a copy of my AXIos configuration code

/** * HTTP configuration */
import Vue from 'vue'
import axios from 'axios'
import router from '@/router'
import store from '@/store'
import Qs from 'qs'// Serialize parameters

// Axios is configured by default
axios.defaults.timeout = 20000; // Request timeout
axios.defaults.withCredentials = false;
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'; // Set the request header
axios.defaults.baseURL = '/api'; //baseurl

// HTTP request interceptor
axios.interceptors.request.use(
    config= > {
        let method = config.method;
        let TOKRN = store.state.access_token;
        // Check whether loading is displayed
        if(config.isLoading == true && !store.state.isLoading){ 
            store.commit('updateLoadingStatus'.true)}// Add a token to the request. The token is taken from 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 interceptor
axios.interceptors.response.use(
    response= > {
        let result = response.data;
        let resCode = result.code;
		// Process the status code returned by the background
        switch (Number(resCode)) {
            case 9004:
                / /...
                break;

            case 12000:
               	
                break;

            case 12001:
                // There is no real name authentication
                
                break;
            
            case 9000 || 9001 || 9002:
                
                break;
            default:
                
                break;
        }

        setTimeout((a)= > {
            if(store.state.isLoading) {
                store.commit('updateLoadingStatus'.false); }},300);

        return response.data;
    },
    error => {
        if (error.response) {
        // Request error, according to the HTTP status code processing
            switch (error.response.status) {
                case 400:
                    console.log('Service 400 operation failed! ')
                    break;
                case 404:
                   // router.push({name:'404'})
                    console.log('Service 404 request does not exist! ')
                    break;
                case 408:
                    router.push({name:'500'.query: {code:408}})
                    console.log('Service 408 request timeout')
                    break;
                case 500:
                    router.push({name:'500'.query: {code:500}})
                    console.log('Service 500 internal server error')
                    break;
            }
        }

        store.commit('updateLoadingStatus'.false);
        return Promise.reject(error); })export default axios;
Copy the code

Iii. The project is under development

1. Implement the control graph provided by UI designer as a public component, such as header, footer and commonly used BTN, etc.; However, the popover part is a frequently used component, and it is slightly troublesome to use the component every time. Therefore, drawing on the practice of VUX, the popover part is made into a VUE plug-in, which can be called through this, which is convenient and saves a lot of trouble. I posted it on NPM, welcome to use V-M-layer if necessary; I’ll post a sample code that you might find useful

<! -- alert. Vue component - >
<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">determine</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>


Copy the code
// Wrap alert.vue as a plug-in
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
Copy the code

2. Since login is implemented natively, relevant parameters should be passed when switching to H5 after login; We start by natively calling our h5 defined global methods, where we store parameters in VUEX

window.GET_AUTHENTICATION = function(token,userId) {
    store.commit('refreshToken', token);/ / store token
    store.commit('USER_ID', userId);// Storage user ID
}
Copy the code

However, there will be asynchronous problems in this way. For example, when entering the page, you need to use token to obtain data, but it is not fun before the token is stored. So using the second method, let the APP jump to carry parameters in the URL, we in the app.vue entry file url parameters are stored in vuex, so it works.

<script>
/ / such as APP to jump over the url is 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>

Copy the code

3. Web and APP need intermodulation methods; I want to see how to use JSBridge. The APP says that they provide a simple call method.

MessageHandlers are the native method prefix, MOVIE_JSBRIDGE_MESSAGEHANDLE_NAME_OPEN_UPLOADIDCARD is the method name, postMessage is the fixed calling function, Can pass to participate
webkit.messageHandlers.MOVIE_JSBRIDGE_MESSAGEHANDLE_NAME_OPEN_UPLOADIDCARD.postMessage(type)

//APP calls h5's method, just need H5 to hang the method on the window object
window.getToken = function(token) {
	//....
}
Copy the code

However, the intermodulation methods of Android and iOS platforms are different, so we need to judge that different platforms implement different methods.

<script>
export default {
  mounted() {
  	const _this = this;
        
        / / upload after the completion of the APP is returned to the address, H5 resources parameters ({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 : 'Unknown error! ')}}},methods: {
    // Upload profile picture
  	openAppFile(type){
      const platform = this.$store.state.platform;// Differentiate between iOS and 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>
Copy the code

4. When the page is running on the phone, there is an error we are not good to view the error, not good to track; Fortunately, vConsole is a plug-in that allows you to view console information on your phone.

5. Click events on iOS have a 300ms delay, which can be fixed by fastClick

//main.js
import FastClick from 'fastclick'
FastClick.attach(document.body);
Copy the code

6. In order to look like APP, there needs to be a switching animation during page switching; After thinking for a long time, I found a good solution when browsing GitHub. Store a variable isBack:false in vuex. If isBack is false, the forward animation will be performed. If isBack is true, the backward animation will be performed. But when is false and when is true? https://github.com/zhengguorong/pageAinimate

// set to false as soon as the page switches and 300ms of animation is performed
router.afterEach((to, from, next) = > {
    setTimeout((a)= > {
        store.commit('SAVE_BACK'.false);
    }, 300);
});

// Listen for the return event and set it to true as soon as the user clicks return, which executes the return animation.
// As long as no return event is monitored, the forward animation is executed; When the return event is detected, the back animation will be executed. After 300ms, the back animation will be automatically set to false
//router.back() and router.go(-1) trigger return events
window.addEventListener('popstate'.function (e) { // Listen for return events
    store.commit('SAVE_BACK'.true);
}, false)
Copy the code

Set the animation in 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 in...</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);transform: translate3d(0, 0);z-index: -1;
}

</style>

Copy the code

Fourth, some optimization problems

  1. Don’t register all components directly with vue.com Ponent as this will make app.js too large
  2. Import a from ‘@/components/a.vue’ imports components in a smaller package than import{a,b,c} from ‘@/components’
  3. To prevent app.js from becoming too large, you can introduce vue.js and vue-router.js into the index. Or use Webpack’s DllPlugin to package the files that are not frequently changed into a file, which can reduce both the request and the size of app.js.
  4. Route lazy loading
  5. If no route is matched, you can redirect to 404 to prevent blank pages
router.beforeEach(function (to, from, next) {
    if(to.name == null) {
        next({name:'404'})
    }
    next()
})
Copy the code

6. The module A clicked by the user is cached by the browser. After repackaging and going online, the user still reads the cache in module A and can browse normally. If you click the link from module A to module B, the module cannot be found in the server because the hash value of the packed file is different each time. Therefore, Loading Chunk xx failed is thrown. So you need to catch module loading errors

//routerUtils.js
import router from '.. /router'
import store from '.. /store'

export default {
    catchImport(err) {
        try {
            console.log('I have caught the router Loading chunk fail error');
            let routeName = store.state.route.name;
            if(routeName && routeName.indexOf('recruit') != - 1) {
                router.push({name:'recruitIndex'});
            } else{
                router.push({name:'index'});
            }
                
            setTimeout((a)= > {
                window.location.reload();
            }, 500);

        } catch (error) {
            console.log('router:'+error)
        }
    }
}
Copy the code
import routerUtils from '.. /plugins/routerUtils'
// A module sets a capture
const index = (a)= > import(/* webpackChunkName: "index" */ '@/view/home/index/').catch(routerUtils.catchImport)
const artistResume = (a)= > import(/* webpackChunkName: "index" */ '@/view/home/artistResume')
Copy the code