A Brief Talk on Using Vue to Construct the Bottom Layer of Single Page Application Development with Front-end 10w+Code Quantity

Keywords: Vue axios npm git

Before the start

With the continuous accumulation of business, at present, our main project on ToC side, except node_modules, build configuration file, dist static resource file code is 137521 lines, the total number of sub-application codes under background management system, except dependencies and other files, also reaches a little more than 1 million lines.

Code volume means nothing, only a lot of modules, but the same two projects, in the same run-time performance, your 100,000 lines of code can accommodate and maintain 150 modules, and the development is smooth, but in my project, 100,000 lines of code can only accommodate 100 modules, add functions, and maintain more cumbersome, which is worth considering.

This article will mainly describe the technical subject of Vue technology stack and the business subject of ToC-end project. During the construction process, the points encountered or summarized (some scenarios of ToB project will also be mentioned) may not be suitable for your business scenario (for reference only). I will describe as many problems as possible and the thinking therein. I will help the students who need it as much as possible, and also work hard to develop them. Those who find problems or unreasonable/incorrect places to give me timely feedback, will amend as soon as possible, welcome a better way to achieve pr.

Git address
React project

You can refer to the article written by the Ant Golden Clothes Data Experience Technology Team.

This article is not based on the above article, but after reading their article, I felt similar. Compared with this article, this article may be more boring, there will be a lot of code, students can directly use the warehouse to read.

(1) Single page, multi-page

First of all, we should think about whether the main body of our project is single page, multi-page or single page + multi-page, and analyze their advantages and disadvantages.

  • Single Page (SPA)

    • Advantages: good experience, jump process between routes, customizable transit animation, lazy loading can effectively reduce the white screen time of the home page, compared with multiple pages, reduce the number of users accessing static resource servers, etc.
    • Disadvantages: Initially, it will load larger static resources, and with the growth of business, lazy loading also has its drawbacks, not doing special treatment is not conducive to SEO, etc.
  • Multi-page (MPA):

    • Advantages: It is friendly to search engines and less difficult to develop.
    • Disadvantages: resource requests are more, the whole page refresh experience is poorer, and data transfer between pages can only rely on URL, cookie, storage and other ways, which are more limited.
  • SPA + MPA

    • This method is common in the case of older MPA projects migrating to SPA. The disadvantage of combining the two methods is that the two main communication modes can only be compatible with MPA.
    • However, this approach also has its advantages. If you have similar article sharing in your SPA (without back-end straight out, back-end back HTML string), you want to ensure that the user experience develops a page in SPA, also develops a page in MPA, eliminating useless dependencies, or directly using native JS to develop, sharing out is the article page of MPA, this can be done. Speeding up the open speed of sharing can also reduce the pressure of static resource servers, because if the article pages of SPA are shared, the static resources required by SPA need to negotiate at least. Of course, if the service is configured with strong caching, the above mentioned will be ignored.

First, according to the business needs, we finally determine the main body of the construction, and we choose the experience-oriented SPA, and choose the Vue technology stack.

(2) Catalogue structure

In fact, we can see that in most open source projects, the directory structure will not be much worse. We can synthesize a general src directory:

src
├── assets          // Resource Catalog Pictures, Styles, iconfont
├── components      // Global Common Component Directory
├── config          // Project configuration, interceptors, switches
├── plugins         // Plug-ins are related, generating routing, requests, store s, and other instances, and mounting Vue instances
├── directives      // Extended instruction set
├── routes          // Routing configuration
├── service         // Service layer
├── utils           // Tool class
└── views           // View layer

(3) General components

Components, we will store those common common components in UI component libraries, and use them directly by setting aliases in projects. If other projects need to use them, they will be sent to npm.

structure

// Simple structure of components
components
├── dist
├── build
├── src      
    ├── modal
    ├── toast
    └── ...
├── index.js             
└── package.json        

Use in projects

If you want to compile into es5, you can use or deploy CDN directly in html, configure simple packing logic in build, build UI components with package.json, and then deploy the content under dist and publish it to npm.

And we can also use es6 code directly:

import 'Components/src/modal'

Use of other projects

Suppose we publish an npm package called bm-ui and download it to the local npm I bm-ui-S:

Modify the outermost packaging configuration of the project by adding include, node_modules/bm-ui in rules, babel-loader or happypack:

// webpack.base.conf
...
    rules: [{
        test: /\.vue$/,
        loader: 'vue-loader',
        options: vueLoaderConfig
    },
    {
        test: /\.js$/,
        loader: 'babel-loader',
        // Add here
        include: [resolve('src'), resolve('test'), resolve('node_modules/bm-ui')]
    },{
    ...
    }]
...

Then it can be used directly in the project with babel-plugin-import:

import { modal } from 'bm-ui'

Multiple component libraries

If there are multiple component libraries at the same time, or if there are students who specialize in component development, put `components'.
Internal subdivision, one more file hierarchy.

components
├── bm-ui-1 
├── bm-ui-2
└── ...

Your packaging configuration files can be placed under components and packaged uniformly, of course, if you want to open source or under the corresponding libraries.

Global configuration, plug-ins and interceptors

In fact, this point is often overlooked in the project, or rarely aggregated together, but at the same time I think it is one of the important parts of the whole project, follow-up examples will be said.

Global configuration, interceptor directory structure

config
├── index.js             // Global Configuration/Switching
├── interceptors        // Interceptor
    ├── index.js        // Entry file
    ├── axios.js        // Request/response interception
    ├── router.js       // Route interception
    └── ...
└── ...

Global configuration

We may have the following configuration in config/index.js:

// config/index.js

// Current host platforms compatible with multiple platforms should be acquired through some specific functions
export const HOST_PLATFORM = 'WEB'
// That's not much to say.
export const NODE_ENV = process.env.NODE_ENV || 'prod'

// Whether to force all requests to access the local MOCK, it is not difficult to guess that students here, each request can also control whether to request MOCK independently.
export const AJAX_LOCALLY_ENABLE = false
// Whether to turn on monitoring
export const MONITOR_ENABLE = true
// Routing default configuration, routing table is not injected from here
export const ROUTER_DEFAULT_CONFIG = {
    waitForData: true,
    transitionOnLoad: true
}

// axios default configuration
export const AXIOS_DEFAULT_CONFIG = {
    timeout: 20000,
    maxContentLength: 2000,
    headers: {}
}

// vuex default configuration
export const VUEX_DEFAULT_CONFIG = {
    strict: process.env.NODE_ENV !== 'production'
}

// API default configuration
export const API_DEFAULT_CONFIG = {
    mockBaseURL: '',
    mock: true,
    debug: false,
    sep: '/'
}

// CONST default configuration
export const CONST_DEFAULT_CONFIG = {
    sep: '/'
}

// There are also some business-related configurations
// ...


// There are also some easy-to-develop configurations
export const CONSOLE_REQUEST_ENABLE = true      // Open request parameter printing
export const CONSOLE_RESPONSE_ENABLE = true     // Open Response Parameter Printing
export const CONSOLE_MONITOR_ENABLE = true      // Monitoring Record Printing

As you can see, here is a collection of all the configurations used in the project. Next, we instantiate the plug-in in plugins and inject the corresponding configurations. The directory is as follows:

Plug-in directory structure

plugins
├── api.js              // Service layer api plug-in
├── axios.js            // Request Instance Plug-in
├── const.js            // Service Layer const Plug-in
├── store.js            // vuex instance plug-in
├── inject.js           // Injecting Vue prototype plug-in
└── router.js           // Routing Instance Plug-in

Instantiate plug-ins and inject configurations

Here are two examples of how we inject configuration, interceptor and instantiate

Instantiate router:

import Vue from 'vue'
import Router from 'vue-router'
import ROUTES from 'Routes'
import {ROUTER_DEFAULT_CONFIG} from 'Config/index'
import {routerBeforeEachFunc} from 'Config/interceptors/router'

Vue.use(Router)

// Inject default configuration and routing tables
let routerInstance = new Router({
    ...ROUTER_DEFAULT_CONFIG,
    routes: ROUTES
})
// Injection interceptor
routerInstance.beforeEach(routerBeforeEachFunc)

export default routerInstance

Instantiate axios:

import axios from 'axios'
import {AXIOS_DEFAULT_CONFIG} from 'Config/index'
import {requestSuccessFunc, requestFailFunc, responseSuccessFunc, responseFailFunc} from 'Config/interceptors/axios'

let axiosInstance = {}

axiosInstance = axios.create(AXIOS_DEFAULT_CONFIG)

// Injection request interception
axiosInstance
    .interceptors.request.use(requestSuccessFunc, requestFailFunc)
// Injection response interception
axiosInstance
    .interceptors.response.use(responseSuccessFunc, responseFailFunc)

export default axiosInstance

We inject plug-ins in main.js:

// main.js
import Vue from 'vue'

GLOBAL.vbus = new Vue()

// Import'Components'// Global Component Registration
import 'Directives' // instructions

// Introducing plug-ins
import router from 'Plugins/router'
import inject from 'Plugins/inject'
import store from 'Plugins/store'
// Introducing Component Library and Its Component Library Style 
// The library that does not need to be configured is introduced here. 
// If you need to configure all plugin s
import VueOnsen from 'vue-onsenui'
import 'onsenui/css/onsenui.css'
import 'onsenui/css/onsen-css-components.css'
// Introducing root components
import App from './App'

Vue.use(inject)
Vue.use(VueOnsen)

// render
new Vue({
    el: '#app',
    router,
    store,
    template: '<App/>',
    components: { App }
})

We didn't directly refer to the axios example, and I'm sure you guessed that he was referencing it through the inject plug-in. Let's look at inject:

import axios from './axios'
import api from './api'
import consts from './const'
GLOBAL.ajax = axios
 
export default {
    install: (Vue, options) => {
        Vue.prototype.$api = api
        Vue.prototype.$ajax = axios
        Vue.prototype.$const = consts
        // All the things that need to be mounted are put here.
    }
}

Here you can mount the api that you want to easily access in your business (vue instance). Apart from $ajax, api and const are the main functions of our service layer. We will introduce them later, so that our plug-in process runs roughly. Here's how to write the corresponding interceptor.

Request, Route Interceptor

In the ajax interceptor (config/interceptors/axios.js):

// config/interceptors/axios.js

import {CONSOLE_REQUEST_ENABLE, CONSOLE_RESPONSE_ENABLE} from '../index.js'

export function requestSuccessFunc (requestObj) {
    CONSOLE_REQUEST_ENABLE && console.info('requestInterceptorFunc', `url: ${requestObj.url}`, requestObj)
    // Custom request interception logic, which can handle privileges, request sending monitoring, etc.
    // ...
    
    return requestObj
}

export function requestFailFunc (requestError) {
    // Customize sending request failure logic, disconnect, request sending monitoring, etc.
    // ...
    
    return Promise.reject(requestError);
}

export function responseSuccessFunc (responseObj) {
    // Custom response success logic, global interception interface, different processing according to different business, response success monitoring, etc.
    // ...
    // Let's assume that the body of our request is
    // {
    //     code: 1010,
    //     msg: 'this is a msg',
    //     data: null
    // }
    
    let resData =  responseObj.data
    let {code} = resData
    
    switch(code) {
        case 0: // If the business is successful, go directly to the successful callback  
            return resData.data;
        case 1111: 
            // If the business fails, do different processing according to different code s
            // For example, the most common authorization expiration jump login
            // Specific window
            // Jump specific pages, etc.
            
            location.href = xxx // The path here can also be placed in the global configuration.
            return;
        default:
            // There will also be some special code logic in the business, where we can do unified processing, or down to the business layer.
            !responseObj.config.noShowDefaultError && GLOBAL.vbus.$emit('global.$dialog.show', resData.msg);
            return Promise.reject(resData);
    }
}

export function responseFailFunc (responseError) {
    // Response failures can be monitored based on responseError.message and responseError.response.status
    // ...
    return Promise.reject(responseError);
}

Define routing interceptors (config/interceptors/router.js):

// config/interceptors/router.js

export function routerBeforeFunc (to, from, next) {
    // Page interception can be done here, and many background systems also like to do permission processing in it very much.
    
    // next(...)
}

Finally, it can be introduced and exposed in the entrance file (config/interceptors/index.js):

import {requestSuccessFunc, requestFailFunc, responseSuccessFunc, responseFailFunc} from './ajax'
import {routerBeforeEachFunc} from './router'

let interceptors = {
    requestSuccessFunc,
    requestFailFunc,
    responseSuccessFunc,
    responseFailFunc,
    routerBeforeEachFunc
}

export default interceptors

Request interception here is very simple code, for responseSuccessFunc switch default logic to do a brief description:

  1. ResponsseObj. config. noShowDefaultError may not be well understood here

When we request, we can pass in a meaningless noShow DefaultError parameter in axios for our business. When the value is false or does not exist, we trigger global.dialog.show, global.dialog.show, we register in app.vue:

// app.vue

export default {
    ...
    created() {
        this.bindEvents
    },
    methods: {
        bindEvents() {
            GLOBAL.vbus.$on('global.dialog.show', (msg) => {
                if(msg) return
                // We will register global events here that need to be manipulated at the attempt level to facilitate publish and subscribe calls in non-business code.
                this.$dialog.popup({
                    content: msg 
                });
            })
        }
        ...
    }
}
It's also possible to put the pop-up window state in the Store, where we are used to registering the common state involving view logic and distinguishing it from the business according to the team's preferences.
  1. GLOBAL is the global object on our mounted window. We put everything we need to mount in window.GLOBAL to reduce the possibility of namespace conflicts.
  2. vbus is actually what we started to mount new Vue().
GLOBAL.vbus = new Vue()
  1. If we go out here Promise.reject, we can only deal with our business logic in the error callback, while others such as disconnection, timeout, server errors and so on are handled uniformly through interceptors.

Contrast before and after interceptor processing

Compare the code that processes the request before and after sending in the service:

Before interceptor processing:

this.$axios.get('test_url').then(({code, data}) => {
    if( code === 0 ) {
        // Business success
    } else if () {}
        // em... various businesses are not successfully processed. If they encounter common processing, they need to be pulled out.
    
    
}, error => {
   // We need to do all kinds of good processing logic, disconnection, timeout and so on according to error.
})

After interceptor treatment:

// Business failure with default pop-up logic
this.$axios.get('test_url').then(({data}) => {
    // Successful business, direct operation of data can be
})

// Business Failure Customization
this.$axios.get('test_url', {
    noShowDefaultError: true // Optional
}).then(({data}) => {
    // Successful business, direct operation of data can be
    
}, (code, msg) => {
    // When a particular code needs special processing, pass in noShow DefaultError: true, and this callback processing will do.
})

Why do you configure interceptors like this?

In response to the unpredictability of requirements in the project development process, let's deal with them faster and better.

Many students here feel that such a simple introduction of judgment is dispensable.
As a recent requirement, our ToC-side project has been opened in the Wechat Public Number before, and we need to open most of the processes through webview in small programs, and we have no time, no space to rewrite nearly 100 + page processes in small programs, which we did not think of at the beginning of development. At this time, the project must be compatible with the small program side. In the process of compatibility, the following problems may need to be solved:

  1. The request path is completely different.
  2. Two different privilege systems need to be compatible.
  3. Some processes need to be changed on the small program side to jump to specific pages.
  4. Some public number APIs are useless in small programs. They need to call the logic of small programs and be compatible.
  5. Many of the elements on the page, the small program side does not show, and so on.
It can be seen that a little carelessness will affect the existing logic of the public number.
  • Add interceptors/minaAjax.js, interceptors/minaRouter.js, the original interceptors / office Ajax. js, interceptors / office Router. js, in the entry file interceptors/index.js, according to the current host platform, that is, the global configuration of HOST_PLATFORM, through agent mode and policy mode, inject the interceptors of the corresponding platform, in minaAjax.js. Rewriting request path and permission processing, adding page interception configuration in minaRouter.js, and jumping to a specific page solve the above problems 1, 2, 3.
  • Question 4: In fact, it's better to handle it. Copies need to be compatible with api pages, rewrite the logic inside, and do jump processing through routing interceptors.
  • Question 5 is also simple. Expand two custom instructions v-mina-show and v-mina-hide to use instructions directly where they are not synchronized.

Ultimately, with the least code and the fastest time to go online perfectly, it has not affected the existing toC-side business at all. Moreover, it aggregates most of all compatible logic together to facilitate secondary expansion and modification.

Although this may not be convincing based on its own business combination, it is not difficult to see that the global configuration/interceptor, although not much code, is one of the core of the whole project, and we can do more awesome things in it.

Routing Configuration and Lazy Loading

There's nothing to say about directives, but many problems can be solved by directives. Keep in mind that we can instruct virtual DOM to be operated in directives.

Routing configuration

According to the nature of our business, we eventually split and allocate according to the business process:

routes
├── index.js            // Entry file
├── common.js           // Public routing, login, prompt page, etc.
├── account.js          // Account process
├── register.js         // Registration process
└── ...

Finally, index.js is exposed to plugins/router instances, where the disassembly placement has two points of attention:

  • It needs to be decided according to the nature of its business. Some projects may be suitable for business line division, and some projects are more suitable for function division.
  • In the process of multi-person collaboration, avoid or reduce conflicts as much as possible.

Lazy load

At the beginning of the article, it is said that the static resource of single page is too large, and it will be slower after the first opening/each version upgrade. It can be used to split the static resource and reduce the white screen time by lazy loading. But at the beginning, it is also pointed out that lazy loading still needs to be discussed.

  • If more components are loaded asynchronously, it will bring more access pressure to the static resource server / CDN. At the same time, if many asynchronous components are modified, resulting in the change of version number, the risk of CDN breakdown will be greatly increased when released.
  • Lazy loading first loads the white screen of the non-cached asynchronous components, resulting in a bad user experience.
  • Asynchronous loading of generic components will show up unevenly when the page may be delayed by the network, and so on.

This requires us to make some trade-offs in space and time according to the project situation.

The following points can be used as a simple reference:

  • For projects with controllable access, such as the company's background management system, asynchronous loading can be carried out in the unit of operation view, and all common components can be loaded synchronously.
  • For some application types with high complexity and high real-time, it can be used to load asynchronous components separately according to functional modules.
  • If the project wants to ensure high integrity and experience, the iteration frequency is controllable, and the first loading time is not very concerned, asynchronous loading can be used on demand or not used directly.
The size of the packaged main.js is mostly a registered view component introduced in routing.

Service Service Service Layer

As another core of the project, the service layer has been the focus of attention since ancient times.

I wonder if you've seen the following ways to organize code:

views/
    pay/
        index.vue
        service.js
        components/
            a.vue
            b.vue

Write write data source in service.js

export const CONFIAG = {
    apple: 'Apple',
    banana: 'Banana'
}
// ...

// Processing Business Logic and Returning the Window
export function getBInfo ({name = '', id = ''}) {
    return this.$ajax.get('/api/info', {
        name, 
        id
    }).then({age} => {
        this.$modal.show({
            content: age
        })
    })
}

// (2) Do not handle business, just write request method
export function getAInfo ({name = '', id = ''}) {
    return this.$ajax.get('/api/info', {
        name, 
        id
    })
}

...

Simple analysis:

  • Not to mention more, the split is not simple enough, as a secondary development, you have to go to find where the bullet window came out.
  • (2) It looks very nice and does not mix business logic, but do not know that you have not encountered such a situation, there are often other businesses need to use the same enumeration, request the same interface, and students who develop other businesses do not know that you have a data source here, the ultimate result is that the code of the data source is redundant everywhere.

I believe that (2) can be seen in most projects.

Then our goal is obvious, to solve redundancy, easy to use, we enumerate and request the method of interface, through plug-ins, mount to a large object, inject the Vue prototype, aspect business use can be.

Catalog hierarchy (for reference only)

service
├── api
    ├── index.js             // Entry file
    ├── order.js             // Order-related interface configuration
    └── ...
├── const                   
    ├── index.js             // Entry file
    ├── order.js             // Order Constant Interface Configuration
    └── ...
├── store                    // vuex state management
├── expands                  // Expand
    ├── monitor.js           // Monitor
    ├── beacon.js            // Dot
    ├── localstorage.js      // Local storage
    └── ...                  // Expand on demand
└── ...

Extraction model

First, the request interface model is extracted, which can be extracted according to the domain model (service/api/index.js):

{
    user: [{
        name: 'info',
        method: 'GET',
        desc: 'Test Interface 1',
        path: '/api/info',
        mockPath: '/api/info',
        params: {
            a: 1,
            b: 2
        }
    }, {
        name: 'info2',
        method: 'GET',
        desc: 'Test Interface 2',
        path: '/api/info2',
        mockPath: '/api/info2',
        params: {
            a: 1,
            b: 2,
            b: 3
        }
    }],
    order: [{
        name: 'change',
        method: 'POST',
        desc: 'Order change',
        path: '/api/order/change',
        mockPath: '/api/order/change',
        params: {
            type: 'SUCCESS'
        }
    }]
    ...
}

Several functions required under customization:

  • Request parameters are intercepted automatically.
  • If the request parameter is not passed, the default configuration parameter is sent.
  • Namespaces are required.
  • Open debugging mode through global configuration.
  • Global configuration is used to control whether local mock s or on-line interfaces are used.

Plug-in writing

Customize the functions and start writing simple plugins/api.js plug-ins:

import axios from './axios'
import _pick from 'lodash/pick'
import _assign from 'lodash/assign'
import _isEmpty from 'lodash/isEmpty'

import { assert } from 'Utils/tools'
import { API_DEFAULT_CONFIG } from 'Config'
import API_CONFIG from 'Service/api'


class MakeApi {
    constructor(options) {
        this.api = {}
        this.apiBuilder(options)
    }


    apiBuilder({
        sep = '|',
        config = {},
        mock = false, 
        debug = false,
        mockBaseURL = ''
    }) {
        Object.keys(config).map(namespace => {
            this._apiSingleBuilder({
                namespace, 
                mock, 
                mockBaseURL, 
                sep, 
                debug, 
                config: config[namespace]
            })
        })
    }
    _apiSingleBuilder({
        namespace, 
        sep = '|',
        config = {},
        mock = false, 
        debug = false,
        mockBaseURL = ''
    }) {
        config.forEach( api => {
            const {name, desc, params, method, path, mockPath } = api
            let apiname = `${namespace}${sep}${name}`,// Namespace
                url = mock ? mockPath : path,//Control mock or Online
                baseURL = mock && mockBaseURL
            
            // Open debugging mode through global configuration.
            debug && console.info(`Calling Service Layer Interface${apiname},The interface is described as${desc}`)
            debug && assert(name, `${apiUrl} :Interface name Attribute cannot be empty`)
            debug && assert(apiUrl.indexOf('/') === 0, `${apiUrl} :Interface path path,The first character should be/`)

            Object.defineProperty(this.api, `${namespace}${sep}${name}`, {
                value(outerParams, outerOptions) {
                
                    // Request parameters are intercepted automatically.
                    // If the request parameter is not worn, the default configuration parameter is sent.
                    let _data = _isEmpty(outerParams) ? params : _pick(_assign({}, params, outerParams), Object.keys(params))
                    return axios(_normoalize(_assign({
                        url,
                        desc,
                        baseURL,
                        method
                    }, outerOptions), _data))
                }
            })      
        })
    }       
}

function _normoalize(options, data) {
    // Here you can do case-to-case conversion or compatibility with other types of RESTFUl
    if (options.method === 'POST') {
        options.data = data
    } else if (options.method === 'GET') {
        options.params = data
    }
    return options
} 
// Injection model and global configuration, exposed
export default new MakeApi({
    config: API_CONFIG,
    ...API_DEFAULT_CONFIG
})['api']

Mounted on the Vue prototype, as mentioned above, through plugins/inject.js

import api from './api'
 
export default {
    install: (Vue, options) => {
        Vue.prototype.$api = api
        // All the things that need to be mounted are put here.
    }
}

Use

In this way, we can happily use business layer code in our business:

// .vue medium
export default {
    methods: {
        test() {
            this.$api['order/info']({
                a: 1,
                b: 2
            })
        }
    }
}

It can be used even outside the business:

import api from 'Plugins/api'

api['order/info']({
    a: 1,
    b: 2
})

Of course, for projects with high operational efficiency, to avoid excessive memory usage, we support namespace as a hump, introduce it directly in the way of deconstruction, and ultimately use the tree-shaking of webpack to reduce the packaging volume.

import {orderInfo as getOrderInfo} from 'Plugins/api'

getOrderInfo({
    a: 1,
    b: 2
})
Generally speaking, when many people collaborate, everyone can first see if the API has a corresponding interface. When the traffic comes up, there will certainly be someone who can not find it, or find it more difficult. At this time, we can make a judgment in the request interceptor on the current request url and the request in the api. If there is a duplicate interface request path, we will remind the developer. Relevant requests have been configured, and it is enough to configure them again according to the situation.

Ultimately, we can expand the various functions of the Service layer:

Basics

  • api: asynchronous and back-end interaction
  • const: Constant enumeration
  • store: Vuex State Management

Expand

  • localStorage: Local data, slightly encapsulated, supports access to objects
  • monitor: monitor function, custom collection policy, calling interface sending in api
  • beacon: Docking function, custom collection strategy, calling interface sending in api
  • ...

const, local Storage, monitor and beacon can be exposed to business use according to their own business expansion, and the idea is the same. Let's focus on store(Vuex).

Interrupt: If you don't feel bad about seeing this, think about whether plugins/api.js above uses the singleton mode? Should it be used?

_State Management and View Splitting

Vuex source code analysis can see my previous articles.

Do we really need state management?

The answer is no, even if your project reaches 100,000 lines of code, that doesn't mean you have to use Vuex, which should be determined by the business scenario.

Business scenario

  1. The first type of project: Business/view complexity is not high, it is not recommended to use Vuex, which will bring the cost of development and maintenance. Use simple vbus to do a good job of namespace to decouple.
let vbus = new Vue()
vbus.$on('print.hello', () => {
    console.log('hello')
})

vbus.$emit('print.hello')
  1. The second type of project: similar to multi-person collaboration project management, there are Dow Cloud Notes, Netease Cloud Music, Wechat Web/Desktop Edition and other applications, centralized functions, high space utilization, real-time interactive projects, Vuex is undoubtedly a better choice. In such applications, we can extract business domain models directly:
store
├── index.js          
├── actions.js        // Root-level action
├── mutations.js      // Root level mutation
└── modules
    ├── user.js       // User module
    ├── products.js   // Product module
    ├── order.js      // Order module
    └── ...

Of course, vuex may not be the best choice for such projects. Interested students can learn rxjs.

  1. The third category of projects: projects with low business coupling between background systems or pages, which should account for a large proportion, let's think about such projects:

Global shared state is not much, but inevitably there will be more complex functions in a module (customer service system, real-time chat, multi-person collaboration function, etc.). At this time, if we want to manage the project, we also manage it in the store. With the iteration of the project, we will encounter such a situation:

store/
    ...
    modules/
        b.js
        ...
views/
    ...
    a/
        b.js
        ...
        
  • Imagine that there are dozens of modules, corresponding to hundreds of business modules on this side, the cost of debugging and developing between two horizontal directories is huge.
  • These modules can be accessed anywhere in the project, but often they are redundant, and there are few other modules to refer to except the functional modules that are referenced.
  • The maintainability of the project will increase with the increase of the project.

How to solve the store usage problem of the third type project?

First, sort out our goals:

  • The module in the project can decide whether to use Vuex or not. (progressive enhancement)
  • From the stateful management module to the module without, we don't want to upload the previous state to the store, we want to improve the efficiency of operation. (Rong Yu)
  • Make state management of such projects more maintainable. (Development Cost/Communication Cost)

Realization

We solve these problems with the help of registerModule and unregisterModule provided by Vuex. We put the state of global sharing in service/store:

service/
    store/
        index.js
        actions.js
        mutations.js
        getters.js
        state.js
Generally, the global state of such projects is not much, if there are more splitting module s.

Write plug-in generation store instance:

import Vue from 'vue'
import Vuex from 'vuex'
import {VUEX_DEFAULT_CONFIG} from 'Config'
import commonStore from 'Service/store'

Vue.use(Vuex)

export default new Vuex.Store({
    ...commonStore,
    ...VUEX_DEFAULT_CONFIG
})

Layer a need state management page or module:

views/
    pageA/
        index.vue
        components/
            a.vue
            b.vue
            ...
        children/
            childrenA.vue
            childrenB.vue
            ...
        store/
            index.js
            actions.js
            moduleA.js  
            moduleB.js

module directly contains getters, mutations, state. We write in store/index.js:

import Store from 'Plugins/store'
import actions from './actions.js'
import moduleA from './moduleA.js'
import moduleB from './moduleB.js'

export default {
    install() {
        Store.registerModule(['pageA'], {
            actions,
            modules: {
                moduleA,
                moduleB
            },
            namespaced: true
        })
    },
    uninstall() {
        Store.unregisterModule(['pageA'])
    }
}

Ultimately, rules for registering these States and managing states before page jumps are introduced in index.vue. Rules for unloading these States and managing states are unloaded before routing leaves:

import store from './store'
import {mapGetters} from 'vuex'
export default {
    computed: {
        ...mapGetters('pageA', ['aaa', 'bbb', 'ccc'])
    },
    beforeRouterEnter(to, from, next) {
        store.install()
        next()
    },
    beforeRouterLeave(to, from, next) {
        store.uninstall()
        next()
    }
}

Of course, if your state is shared globally, uninstall will not be executed.

This solves the first three problems. When developing a page, different developers can gradually enhance the choice of a development form according to the characteristics of the page.

Other

Here is a brief list of other aspects, need to be in-depth and use according to the project.

Package, build

There are many optimization methods on the internet: dll, happypack, multi-threaded packaging, etc. But with the code level of the project, the compilation speed will be slower and slower each time dev is saved, and when it is too slow, we have to split it. That's for sure. Before splitting, there are as many maintainable codes as possible. Several can be tried and avoided. Points:

  1. Optimizing project process: This point seems to be useless, but change is the most intuitive. Simplification of page/business will be directly reflected in the code, but it will also increase the maintainability and extensibility of the project.
  2. Reduce the vertical depth of project document hierarchy.
  3. Reduce useless business code and avoid using useless or overly dependent libraries such as moment.js.

style

  • As far as possible, each module should be separated to make the bottom of the whole style more flexible, and redundancy should be reduced as much as possible.
  • If sass is used, make good use of% placeholder to reduce unnecessary code packaging.
In MPA applications, style redundancy is too large, and% placeholder can also help you.

Mock

Many large companies have their own mock platforms. At present, the back end has fixed the interface format and put in the corresponding mock api. If there is no mock platform, then find a relatively useful tool such as json-server.

Code specification

Please force the use of eslint and hang it on the hook of git. Regular diff code, regular training, etc.

TypeScript

It is highly recommended to write projects with TS. It may be a bit awkward to write. vue, so that most of the errors in the front end can be solved at compile time. It can also improve the efficiency of browser runtime, and may reduce the re-optimization phase time.

test

This is also a very important point of the project, if your project has not yet used some testing tools, please access as soon as possible, here is no more details.

Resolution system

When the project reaches a certain level of business, because there are too many modules in the project, the maintenance cost of new students and the development cost will rise sharply, so we have to split the project. We will share the simple practice of ToB project in the splitting system in the future.

Last

At present, there are various mature solutions, here is only a simple build sharing, which relies on the version we stabilized, need to upgrade according to their actual situation.

The bottom-level construction of a project often becomes the neglect of the front-end. We should not only look at a project or the whole line of business from a general perspective, but also strive for excellence in each line of code, optimize the development experience, and gradually accumulate before we can better cope with the unknown changes.

Finally, let me make a small wave of advertisements.

EROS

If front-end students want to try using Vue to develop APP, or those who are familiar with weex development, they can try using our open source solution eros. Although they have not done any advertisements, but not fully counted, there are still 50 online apps. We look forward to your joining.

Finally, some screenshots of the products are attached.~

Posted by l00ph0le on Sun, 19 May 2019 05:50:02 -0700