Author: Huang Yi, Didi Public Front End

Project background

Didi’s WebApp is a taxi-hailing app that runs on wechat, Alipay, Mobile Q and other third-party channels. We’ve given it a technical refactoring with Vue 2.0 with product-level functional and visual upgrades.

Technology stack

MVVM framework: Vue 2.0 source code: ES6 Code style checking: ESLint Build tools: WebPack Front-end routing: VUE-Router State management: VUex server communication: VUE-Resource

A few questions

  1. Is Didi WebApp a big SPA app? Didi WebApp contains numerous business lines, each of which has an independent set of order issuing process logic. So the question is, is all this business logic done in a single page?

  2. How to achieve componentization? The design idea of Didi WebApp 5.0 is componentization. The design provides many components, and each page is made up of components. The question then becomes, how do you distinguish the base component from the business component, and abstract the base component into a common component library?

  3. A code warehouse multiple lines of business, how to achieve a good simultaneous development and continuous integration of people? Didi has several lines of business, and for each line of business, a front-end student will develop the code. So how do you modularize your code? How do you minimize development conflicts and do continuous integration?

  4. Some service lines need to be loaded asynchronously. How to develop these service lines? Didi currently puts the codes of its car-like lines of business in a warehouse, but some lines of business, such as hitch, are not in the warehouse. So the question is, how is this code developed, using Vue, Vuex, Store, and some common methods and components?

  5. How to dynamically register asynchronously loaded business line components? We need to asynchronously load the JS code for the lines of business that implement a Vue Component. The question is, how do you gracefully register these components dynamically?

  6. How do asynchronously loaded lines dynamically register routes? When initializing a route using vue-router, we usually write a route mapping table. The question is, how do these asynchronously loaded lines of business register dynamically if they also want to use routing?

  7. How do I interact with the back-end interface in a test environment? In the development phase, we usually debug locally, and the local service domain name is usually localhost: port number. The problem is that this can cause cross-domain problems with Ajax requests, and we can not require the server to open all ajax request interfaces cross-domain. How to solve this problem?

  8. How to deploy to an offline test environment? After we have developed the code locally, we need to test it. Often, once the test gets the code, it needs to be deployed and tested, so the question is, how do we deploy the native code into our development machine test environment?

The solution

  1. Is Didi WebApp a big SPA app?

    Didi WebApp contains many business lines, and each business line has an independent set of process logic of order issuing -> receiving -> In-trip -> order completion. If the whole application is a SPA, the JS package will become very large. Although it can be loaded asynchronously through code spliting technology, it will inevitably increase the amount of code, which is very difficult to maintain.

    Therefore, we separated the order issuing and subsequent business logic into the order issuing front page and subsequent process page. Each line of business has its own independent process page after the order issuing. In this way, Didi’s WebApp is equivalent to multiple SPA applications, and data transfer between pages can be done through URL parameter transfer.

  2. How to achieve componentization?

    Componentization has almost become the standard of WebApp development, and Didi has been componentization from the design point of view. But design just breaks down the page into components, and we, as developers, need to figure out which of these components are fundamental, which are business components, which are reusable, and so on.

    Basic components are those that don’t contain any business logic themselves and can be easily reused, such as picker, TimePicker, Toast, Dialog, Actionsheet, etc… Based on Vue 2.0, we have implemented a set of mobile base components library, packaged all the base components, and hosted on NPM private server, very convenient to use. The base component communicates basically by passing a prop to the component and listening for the component $emit events.

    Business components are primarily those that contain business logic, including some that communicates with back-end interfaces. A business component consists of several base components, and usually we manage some business logic data through Vuex, then the component reads the data internally and submits the action to modify the data. It is important to note that when using Vuex, not all requests that communicate with the back end need to submit an action. If the request does not modify the data in our store, it can be digested within the component. As a practical example, when developing the Suggest component, each time a character is entered to retrieve the associated address, the request is made internally and rendered to the component’s list, since it does not modify the data in the store.

    The underlying components are usually reusable, as are some business components that have similar UI and business logic. We will publish individual reusable business components to THE NPM private server separately, using line of business dependencies. Note that we do not recommend using Vuex for business components. It should be taken into account that different users have different definitions and uses of Vuex internal variables.

  3. A code warehouse multiple lines of business, how to achieve a good simultaneous development and continuous integration of people?

    Didi webApp homepage has multiple business lines, and each business line has a developer. In order to minimize code conflicts, we divide codes into modules according to business lines. Since Vuex supports modules, we naturally split modules along lines of business, each maintaining its own getters, Actions, Mutaions, and states, Some public data, such as latitude and longitude, boarding and boarding information, and user login status, are shared by all business lines as root states. Similarly, components has made a more detailed division by line of business, so that the independent business components of each line of business are placed in their own directories, so that they do not conflict with each other before.

    It’s not enough to just split the catalog. We also need to consider continuous integration and release with the iteration of the product. Then each version of the requirements, each line of business will participate in the development, we use GitLab to manage the code, if each developer to open a branch, it will face too many branches, the trouble of function joint adjustment and other problems. Therefore, we have agreed on a set of Git management specifications. For each big demand version, we will agree on “dev + launch date “as the branch name to create a development branch, and everyone will develop on this branch. QA will test the branch after the development, and the branch will be released together with the main trunk before the launch. If there is a bugfix during the release of the two versions, the convention is to create a Bugfix branch with the name “Bugfix + Function Description”, and connect the main trunk after the repair. Before each launch, we would run scripts to add version numbers, compile and package, and ensure incremental release of front-end resources.

  4. Some service lines need to be loaded asynchronously. How to develop these service lines?

    Didi currently puts some lines of business code in a warehouse, but some lines of business, such as hitch Car code, are not in the warehouse. The home page loads the code of this part of the line of business through asynchronous loading JS. This part of the line of business obviously needs to be developed with Vue, but they can not introduce vue.js separately.

    Our solution is to register an XXApp object on the window and mount Vue, Vuex, and some common components and methods to the object. Then these asynchronously loaded lines of business can be accessed through the window.XXApp code is as follows:

     window.XXApp = {
         Vue, 
         Vuex,
         store, / / global store
         saveCurrentBiz, // Public method
         Location // Public components
         // Other public methods and components
     }Copy the code

    Once these objects are accessible to the line of business, the next thing you need to implement is a Vue Component.

  5. How to dynamically register asynchronously loaded business line components?

    The solution of asynchronous component provided by vue. js official website is mostly based on Webpack require.ensure to load components asynchronously, but it is obviously not applicable to didi’s business scenario, because our code is not under a warehouse. We need an AMD-like solution. What these asynchronous lines of business need to implement is a Vue Component. How do we gracefully register this component dynamically?

    Vue.component(‘async-example’, function(resolve){//… }) dynamically register the component in the factory function through the resolve method. Note that the factory function is executed when the component actually needs to be rendered, so the time we render these asynchronous components is when we switch the top navigation bar to that line of business.

    First of all, each line of business corresponds to an independent component, and the line of business has its own ID. Therefore, we first use an object to maintain such a mapping, the code is as follows:

     constModules = {line ID: Taxi,/ / taxi
         // Other synchronous service line components
     }Copy the code

    This object initializes all synchronous business line components. For asynchronously loaded business line components, we need dynamic registration. First of all, we will maintain a configuration relationship table of the line of business in the global config.js. Asynchronously loaded line of business will have a SRC attribute, the code is as follows:

    BizConf: {Id of the asynchronous line: {name: 'alift'.// Line name
           src: xxx // Load the js address of the asynchronous line of business}, sync line id: {name: 'taxi'
        }
        // Other line configurationsCopy the code

    Next we iterate over the object as follows:

      // Get the bizConf object
      const bizJSConf = config.get('bizConf') 
    
      for (let id in bizJSConf) {
         let conf = bizJSConf[id]
         if (conf.src) {
           modules[id] = conf.name
           Vue.component(conf.name, (resolve, reject) => {
             loadScript(conf.src).then((a)= > {
               resolve(modules[id])
             }).catch((a)= > {
               reject()
             })
           })
         }
       }Copy the code

    As you can see, for the asynchronous line of business, we add its name to the mapping of the Modules object and register an asynchronous component by that name. Note that the factory function to register the component is not executed at this time.

    We mentioned earlier that the time to render these asynchronous components is when we switch the top navigation bar to the line of business. Let’s see what logic is executed when we switch the top navigation bar. The key code is as follows:

      this.currentView = modules[productid]Copy the code

    We initialized currentView in app. vue’s data and mapped it to template as follows:

      <component :is="currentView"></component>Copy the code

    That’s right, here we use another advanced use of Vue, dynamic components. Our business line component corresponds to this dynamic component. Modules maps to a component object. Modules maps to a component object. Modules maps to a component object. But for asynchronous components, we’re mapping the component name, which is a string, and when currentView points to that string, the factory function that registers the asynchronous component is executed. Looking back, at that point it will load the JS of the asynchronous line of business. Perform the resolve (modules [id]).

    Wait, what is modules[ID], or is it the name of an asynchronous component? Of course not, modules[ID] corresponds to the component object of the asynchronous line of business. So how is it assigned as a component object? Let’s look at the code:

     window.XXApp = {
         // ...
         // Some public methods and components
         registerBiz(id, component) {
           modules[id] = component
         }
     }Copy the code

    We added a registerBiz method under window.XXApp. When we asynchronously load the JS of the business line, the asynchronous business line calls this method to actually register its implementation of the Vue component into our modules. So that’s the component object we resolve. Isn’t that elegant? At this point, we have completed the dynamic registration of asynchronous business line components.

  1. How do asynchronously loaded lines dynamically register routes? As the above problem continues, when initializing a route using vue-Router, we usually write a route mapping table. The mapping of routes is fine for known components such as synchronous lines of business, so what if some of the children of these asynchronously loaded lines of business also want to use routes?

    We needed a dynamic route registration solution, and the lazy route loading solution provided by the official website documentation did not meet our needs, so we came up with another workaround. Our routing configuration is as follows:

       {
         path: 'pathA' // The naming here is just a hint
         component: componentA
       },
       {
         path: 'pathB'.component: componentB
       },
       / /...
       {
         path: '/:name'.// Dynamic matching
         component: Dynamic // The component is known
       }Copy the code

    As you can see, after defining a series of regular routes, we finally define a Dynamic matching route, that is, any level path of name, as long as the previous path does not match, it will be mapped to the Dynamic component we defined. Let’s take a look at the Dynamic component implementation, starting with the template:

     <template>
       <transition :name="transitionName">
         <component :is="currentRouter"></component>
       </transition>
     </template>Copy the code

    Essentially, the Dynamic component takes advantage of Vue’s Dynamic component and can change the currently rendered component by modifying the currentRouter variable. CurrentRouter ¶ currentRouter ¶

     created() {
       this.setcurrent ()}, methods: {setCurrent() {const name = this.$route.params.name
         const component = this.routes[name]
         if (component) {
           this.currentRouter = component
         }
       }
     }Copy the code

    In the component-created hook function, we call this.setCurrent(), which first gets the name from the route argument, then gets the corresponding component from this.routes[name] and assigns it to this.currentrouter. So this.routes becomes more important. We actually stored routes in Vuex store and obtained them through Vuex mapGetters:

    computed: { ... mapGetters(['routes'])},Copy the code

    Since we can get this.routes via Vuex, we must have write logic. This logic is actually implemented by providing an API for these asynchronous lines of business:

     window.XXApp = {
         // ...
         // Some public methods and components
         registerRouter(name, component) {
           Vue.component(name, component)
           store.commit('ADD_ROUTES', {
             name,
             component
           })
         }
     }Copy the code

    We provide the registerRouter interface. The parameters are the name of the route and the corresponding component instance. We first register this component globally through Vue.com Ponent. A mutation of ADD_ROUTES was submitted via the COMMIT interface provided by Vuex.

      [types.ADD_ROUTES](state, data) {
        state.routes = Object.assign({}, state.routes, {[data.name]: data.component})
       },Copy the code

    At this point, we have completed the access logic of routes, the whole dynamic routing scheme is completed, asynchronous lines of business want to use dynamic routing, just need to call the registerRouter interface we provide, isn’t it very convenient ~

  1. How do I interact with the back-end interface in a test environment?

    In the development phase, we usually debug locally, and the local service domain name is usually localhost: port number. This can lead to cross-domain issues with some interfaces, and in addition to the usual cross-domain solutions, we can actually proxy these interfaces for us with node.js services.

    We used vue-CLI scaffolding to help us generate some initialization code. In config/index.js, we modify the proxyTable configuration under dev as follows:

      proxyTable: {
       '/xxxservice': {
         target: 'http://xxx.com.cn'.// Your target domain name
         changeOrigin: true
       },
       / /...
     }Copy the code

    In effect, it leverages Node.js to help us do a layer of service forwarding, which can solve cross-domain problems during development.

  1. How to deploy to an offline test environment?

    After we have developed the code locally, we need to test it. We wrote a deploy script to do this when the test takes the code and needs to deploy and test it. The scP2 node package is used to upload the code, which is executed after the webpack is compiled. The code is as follows:

     var client = require('scp2')
     / /...
     webpack(webpackConfig, function (err, stats) {
         // ...
         client.scp('deploy/home.html', {
             host,
             username,
             password,
             path
           }, function (err) {
             if (err) {
               console.log(err)
             } else {
               console.log('Finished, the page url is xxx')}})})Copy the code

    conclusion

    Technical reconstruction is always accompanied by product upgrade. From this major reconstruction, we have a deeper understanding and mastery of Vue. For its peripheral plug-ins such as Vuex and VUE-Router, our team partners also have a more in-depth study, the output of several articles here to share with you: Vuex 2.0 source code analysis vue-Router source code analysis – the overall process vue-Router source code analysis -history

Above, welcome clap brick ~


Welcome to DDFE GITHUB: github.com/DDFE wechat official account: wechat search the official account “DDFE” or scan the qr code below