As the company’s projects became more and more complex, the early team lacked experience in using Vue, resulting in many pits. The project became more and more difficult to maintain, so I said to my supervisor, “Supervisor, I want to refactor,” which led to this refactoring experience. Through the analysis of the project, the main problems are as follows:

  • Global styles are flying around
  • More and more components, management is not convenient
  • Core page more than 1300 lines of code, very poor reading

This project is a financial project, which adopts visual asset structure description and generates tax report according to the asset structure. Vue family barrel was used for business development and was customized based on Element UI. MxGraph was used for visual modeling

Reduce global styles

There are several reasons why global styles are flying around in projects

  • In-component styles want to override child component styles, removing the scoped keyword
  • Styles are made global in order to reuse them across different components

After a component is destroyed, Vue does not remove the style label, so there is a risk of contaminating the global style by not writing scoped within the component.

To solve the first problem, this refactoring is to insist that all components use Scoped. Use depth action selectors when overwriting child components. This will not pollute the global style, but also the child component overwrite style at a glance.

For pop-ups that do have a global effect, we write them in a file named global.scss, using the BEM specification.

For styles that are reused across components, place them in the Modules folder and import them with @import when used within components.

After refactoring our global styling for the project is nomalize.css, some custom reset, the default style for element-UI, the global mentioned above, and the icon.

Category Management component

Before refactoring, the global base components were placed in the Components /common folder, and business components and other uncategorized components were all placed in the Components file, which looked very confusing.

After refactoring, components are divided into five categories: Business, Common, Function, Slot, and one that is specific to a page, as described below.

Business is a business component that is coupled to the business and can be reused across pages, but not for other projects. The slot, function, and common classes can be used independently of the current project. Slot and function are the same as common, except that Common is used frequently and is globally registered. Slot and function are used locally. Slot is unique in that this type of component only provides a style shell without much interaction and can be easily utilized by other components. Other Panel components can reuse Panel components in the Slot directory.

After this refactoring, I concluded that components should be created in two cases:

  • Reusable, as mentioned above in the Components directory
  • Non-reusable, purely to reduce the code on a page and make the template structure clearer.
    • For example, just pass props to do a data presentation
    • Or the component interacts directly with the page and has no other components nested

Splitting large files

The core page of our system is drawing page. The page has three components: the elements panel on the left, the node panel on the right, and the lines panel on the right. The interaction is the same as that of most visual modeling software. The user drags the left element onto the canvas and drags a line from node to another node. When the user selects a node on the canvas, the right panel shows the operation related to the node, as well as selecting a line (only one node or line can be selected at a time). It’s similar to draw. IO, but we’re not doing a drawing application.

To streamline the methods

A quick look at the code on this page reveals that all this code is due to the fact that during method writing we tend to break up large methods into smaller ones, which end up in methods, making the Template event handler very unobtrusive. So the goal of this refactoring is to remove all non-event handling methods from the Methods object except for page initialization methods. That is, every method in a methods object should correspond to a template event handler.

So the question is where do you put the small methods that are split apart instead of methods? Based on my code analysis of the Canvas page, I found that the page actually operates on only three things: architecture, nodes, and lines. So according to this idea, there are three independent JS files, passing this as a parameter to their respective modules, which are used to operate vm objects. At the same time, JS is independent from vue files, and the code after reconstruction is reduced to more than 300 lines from the original 1000 lines, which improves the maintainability.

Use interface oriented programming

Another problem with this page JS is a lot of if/else judgments. In mxGraph, all nodes and lines are called cells. When a node or line is removed, mxGraph will issue a CELL_REMOVE event, but it is up to the programmer to decide whether the Cell is a node or a line. This also leads to a lot of statements like the following in our system

functoin syncRemove(cell) {
	// Determine whether it is a node or a line
	const cellIsVertex = cell.vertex;
	if(cellIsVertex){
		// Delete the node
	} else {
		// Perform delete line}}Copy the code

After thinking for a long time, I came up with two plans

  1. Each Cell event in the mxGraph is subdivided into node and line events. For example, after receiving the CELL_REMOVE event, determine whether it is a node or a line and then trigger the custom event VERTEX_REMOVE or EDGE_REMOVE. After that, we only need to listen for VERTEX_XXX and EDGE_XXX. This makes the event more concrete, but it is also unavoidable to write multiple if/else checks whether it is a node or a line during the subdivision process, so this scheme was abandoned.

  2. Use interface oriented programming. As MENTIONED earlier, I divided the page interaction into three modules: nodes, lines, and architecture, and since nodes and lines have the same operations, they should implement a common interface. And so they changed the code to look like this

    // vertexOp.js
    const vertexOp = {
      / / * * * * * * * * *
      // Interface
      / / * * * * * * * * *
      handleActive(vertex) {
      	// The operation to perform when the node is pointed out
      },
      async syncAdd(vertex) {},
      syncRemove(vertex) {},
      // Others ....
    }
      
      
    // edgeOp.js
    const edgeOp = {
      / / * * * * * * * * *
      // Interface
      / / * * * * * * * * *
      handleActive(edge) {},
      async syncAdd(edge) {},
      syncRemove(edge) {},
      // Others ....
    }  
    Copy the code

    When the user selects different cells, it only needs to make a decision once in the selection event handler.

    // index.js 
    let opContex = null;
    let activeCell = null;
    
    const listenSelectionChange = () = > {
    	activeCell = graph.getSelectionCell();
    	const cellIsVertex = activeCell.vertex;
    	if(cellIsVertex){
    		opContex = vertexOp;
    	} else{ opContex = edgeOp; }}const handleRemoveEvent = () = > {
    	contexOp.syncRemove(activeCell);
    }
    Copy the code

Use request interception to bring piecemeal method calls together

Another problem with this page is how scattered the method calls are. Such as the following requirements

Requirement: When the user makes any changes to the appearance of the architecture, the snapshot of the current architecture is synchronized to the server as the cover of the architecture.

  • Practices before refactoring:

    • Add a node and send a request with a screenshot at the end of the corresponding processing method
    • Modify node information and add a screenshot at the end of the corresponding processing method to send a request
    • Move the node and send the request with a screenshot at the end of the corresponding processing method
    • Add lines and a screenshot at the end of the corresponding processing method to send the request
    • Modify line information and add a screenshot at the end of the corresponding processing method to send a request
    • . Send the request with a screenshot at the end of the corresponding processing method
  • Method after reconstruction: Intercepts global requests and sends screenshots to the requests identified as related operations

You do this by naming the request and then sending a request completion event using the global eventBus when each request is sent. The event handler only needs to determine whether a screenshot is needed to send the request based on the request name. Here’s the code idea

/ / API layer
// api/nodes.js
import http from '@/config/axios/index';

export default {
  all: data= > http('/nodes', data, 'GET'),
  one: id= > http(`/nodes/${id}`.'GET'),
  save: data= > http('/nodes', data, 'POST'.'nodes-save'),
  del: id= > http(`/nodes/${id}`.'DELETE'.'nodes-del'),
  / /...
};


/ / encapsulation axios
// config/axios/index.js
export default function (. args) {
  const url = args[0];
  let data;
  let method;
  let name;
  // Parameter overload
  if (args.length === 2) {
    method = args[1];
  } else if (args.length === 3) {
    if (_.isString(args[1]) && _.isString(args[2])) {
      method = args[1];
      name = args[2];
    } else {
      data = args[1];
      method = args[2]; }}else if (args.length === 4) {
    data = args[1];
    method = args[2];
    name = args[3];
  } else {
    throw new Error('http support max 4 args');
  }

  if (_.isNil(name)) {
    // The request is not named. The default name is the current timestamp
    name = String(Date.now());
  } else {
	// There is a named request with a timestamp suffix to ensure uniqueness of the request
    name = `${name}__The ${Date.now()}`;
  }
  return $axios({ url, data, method }, name);
}

async function $axios(initialOptions, requestName) {
  const options = getOptions(initialOptions);
  initialOptions.requestName = requestName;
  requestManager.addReq({
    name: requestName,
    config: initialOptions,
  });
	
  try {
    const response = await axios(options);
    requestManager.popReq({
      name: requestName,
      response,
    });
    return response.data;
  } catch (error) {
    // Ensure that the request is ejected from the queue even if it fails
    requestManager.popReq({
      name: requestName,
      error,
    });
    return{}; }}// Send request-related events
// requestManager.js
import Vue from 'vue';

const $bus = Vue.prototype.$bus;

class RequestManager {
  constructor() {
    this._updateRequests = [];
  }

  addReq(req) {
    if (req.config.method.toLowerCase() === 'get') {
      return;
    }
    this._updateRequests.push(req);
    $bus.$emit('before-modify-req-send', req);
  }

  popReq({ name, response }) {
    if (response && response.config.method.toLowerCase() === 'get') {
      return;
    }
    const idx = this._updateRequests.findIndex(item= > item.name === name);
    if (idx >= 0) {
      this._updateRequests.splice(idx, 1);
      $bus.$emit('modify-req-finished', name, response);
      if (this._updateRequests.length === 0) {
        $bus.$emit('modify-req-empty'); }}}}RequestManager is a global singleton
export default new RequestManager();
Copy the code

Ultimately, you can drastically reduce the number of scattered method calls by simply intercepting the request

// xxx.vue
  this.$bus.$on('modify-req-finished'.(name, response) = > {
    const reqs = ['c-transitions-updateRatio'.'c-transitions-save'.'c-transitions-del'./ *... * /];
    const reqName = name.split('__') [0];
    if (reqs.includes(reqName)) {
      // Screenshot, send request}});Copy the code

In addition, request interception also applies to this scenario: When a user performs an operation, the system prompts the user to save the operation in real time, and prompts the user to save the operation after the saving is complete. This is very easy to do with request interception, just listening for sending request events, request queue emptying events and corresponding prompts.

conclusion

This refactoring was done by one person, took a week, and did the following

  • Reduce global styling by partitioning modules

  • Classified component

  • Use the following method to split large files

    • To streamline the methods
    • Break up the module
    • Use interface oriented programming
    • Using request interception

Although many articles on the Internet teach you how to do a good job with Vue, the actual situation is still to start from the project itself, I must think about the project, I believe that there is no “Vue best practice “suitable for all projects. As long as you constantly think about and improve your project, you can find the most suitable architecture for your project.