preface

We all know that Vue is currently one of the top three front-end popular frameworks, which is launched by Chinese You Yuxi. Or for this reason, most small and medium-sized companies in China choose Vue. I think there is another important reason that Vue is easier to get started with. Today I’m not going to compare Vue to other frameworks, I’m just going to talk about how Vue should be written. I came up with this idea because I recently received a vUE middle and back office project, which made me realize that many engineers who claimed to know VUE actually knew how to use it.

My programming went through three stages:

  • In the first stage, WHEN I was an intern, I didn’t think about how to write the code at all, but how to realize the business. The business was directly stacked with code, and many lines of code would be written in a file.
  • In the second stage, I began to think about how to write the code, trying to complete the function with the least code as possible, so I tried to extract similar parts, began to encapsulate basic components and general business components, pay attention to the division of components, and began to contact the component-based programming.
  • The third stage begins to experience programming ideas and apply them when writing code. If there are five programming principles, there should be two that are useful for the front end, the single responsibility principle and the open closed principle, and the front end should follow the separation of concerns principle. I began to reject the idea of writing as little code as possible. You start to focus on extensibility, on decoupling components, and you often ask yourself, will this be easy to change later? Would it be easier to expand this way?

The body of the

Here are my thoughts on how Vue code should be written.

What variables should be in data?

When writing vUE components, many programmers need a shared variable within the component, or when they want to use a variable in the template, they will directly declare a variable in data. I often see that a component’s data has many variables, and most of these variables are used for configuration constants. A lot of people don’t even think about whether this variable should be in data before they write it, right?

As anyone who has read the vue source code knows, vue2 uses observer mode for declarative rendering. Vue2 does what it sets out to do by observing variables in data. The key to implementing this observer mode is Object.defineProperty, but I won’t go into details on how this works. Just remember that the variable vue in data sets the set and get hooks using Object.defineProperty. When a variable in data changes, render is triggered directly or indirectly, and the template recalculates and renders the changed DOM. In other words, the variable in Data is the switch that controls the rerender of the template, which is equivalent to the setState in React. There should be as few such variables as possible.

So how do I declare constants that I want to use in my templates? With computed, a variable in computed only computes when the variable in the data on which it depends changes, and when we rely on constants, we only perform one operation.

Powerful computed

Computed is my favorite in VUE, and it helps a lot. It is known to have a caching capability that does not recalculate until the data on which it depends changes. But that’s not why it’s so powerful, it’s because it provides a data conversion function, which is really useful. Often, we run into situations where the data returned from the back end is unreadable or in the wrong format, and this is where computed comes in handy. Here are a few small examples:

 data() {
    return {
        state: 0.// To store the list of tasks, I generally store the data returned from the back end in VUEX
        allTaskList: [],
        filterType: 0}},computed: {
    // The back end returns a state indicating the status of the task, 0 for paused, 1 for ongoing, and 2 for completed
    taskState() {
        const {state} = this;
        let stateStr;
        switch(state) {
            case 0:
                stateStr = 'suspend';
            break;
            case 1:
                stateStr = 'in progress';
            break;
            case 2:
                stateStr = 'complete';
            break;
        }
        return stateStr;
    },
    // There is a scenario where the back end returns the task list, and the front end needs to provide filtering in addition to displaying all the task lists
    // The user can choose to view only paused tasks or completed tasks
    filterTaskList() {
        const {allTaskList, filterType} = this;
        // After this, when the user selects the filter function, we do not need any other operations on the list side,
        // This is automatically recalculated and then re-rendered
        return allTaskList.filter(task= >task.state === filterType); }}Copy the code

Convenient filter

The most basic function of the front end is display, and you can use filters for anything related to display. Vue provides in-component filters and global filters. Its functions overlap with computed, and many people don’t like using filters. But I will say that defining some global filters is a great convenience. Here I post my usual global filter plugin:

import moment from 'moment';
import _ from 'lodash';

const DATE_FORM = 'YYYY-MM-DD';
const TIME_FORM = 'HH:mm:ss';
const DATETIME_FORM = `${DATE_FORM} ${TIME_FORM}`;
const DATE_MINUTE_FORM = 'MM-DD HH:mm';
const DATE_YEAR_MINUTE_FORM = 'YYYY-MM-DD HH:mm';

const parseServiceStringToDate = (t) = > moment(t).local();

const SECOND_SPAN = 60 * 1000;
const MIN_SPAN = 60 * SECOND_SPAN;
const DAY_SPAN = 24 * MIN_SPAN;
const WEEK_SPAN = 7 * DAY_SPAN;
const MONTH_SPAN = 30 * DAY_SPAN;
const YEAR_SPAN = 12 * MONTH_SPAN;

const secondToDate = (result, lng) = > {
  const h = Math.floor(result / 3600);
  const m = Math.floor((result / 60) % 60);
  const s = Math.floor(result % 60);
  if (lng === 'en') {
    return `${h > 0 ? `${h} Hour` : ' '}${m > 0 ? `${m} Min` : ' '}${s > 0 ? `${s} Sec` : '0 Sec'}`;
  }
  return `${h > 0 ? `${h}Hours ` : ' '}${m > 0 ? `${m}Minutes ` : ' '}${s > 0 ? `${s}Second ` : '0 seconds'}`;
};

const canConvertDate = (t) = >_.isDate(t) || ! _.isNaN(Date.parse(t)) || _.isNumber(t);

export default {
  install(Vue) {
    Vue.filter(
      'toDateString',
      (t, defaultVal = ' ') => canConvertDate(t) ? parseServiceStringToDate(t).format(DATE_FORM) : defaultVal
    );
    Vue.filter(
      'toTimeString',
      (t, defaultVal = ' ') => canConvertDate(t) ? parseServiceStringToDate(t).format(TIME_FORM) : defaultVal
    );
    Vue.filter(
      'toDatetimeString',
      (t, defaultVal = ' ') => canConvertDate(t) ? parseServiceStringToDate(t).format(DATETIME_FORM) : defaultVal
    );
    Vue.filter(
      'toDateMinuteString',
      (t, defaultVal = ' ') => canConvertDate(t) ? parseServiceStringToDate(t).format(DATE_MINUTE_FORM) : defaultVal
    );
    Vue.filter(
      'toDateYearMinuteString',
      (t, defaultVal = ' ') => canConvertDate(t) ? parseServiceStringToDate(t).format(DATE_YEAR_MINUTE_FORM) : defaultVal
    );
    Vue.filter('timeSpan', (t, subfix = 'before') = > {const localTime = parseServiceStringToDate(t)
        .toDate()
        .getTime();
      const now = Date.now();
      const span = now - localTime;

      const timeSpanUnit = [
        [YEAR_SPAN, 'years'],
        [MONTH_SPAN, 'month'],
        [WEEK_SPAN, 'week'],
        [DAY_SPAN, 'day'],
        [MIN_SPAN, 'points'],
        [SECOND_SPAN, '秒']].for (let i = 0; i < timeSpanUnit.length; i++) {
        const s = span / timeSpanUnit[i][0];
        if (s > 1) {
          return `${s.toFixed(1)}${timeSpanUnit[i][1]}${subfix}`; }}return 'just';
    });
    Vue.filter('duration', (value, compare, lng) => {
      const t1 = parseServiceStringToDate(value);
      const t2 = _.isString(compare) ? parseServiceStringToDate(compare) : moment();
      const span = t1.toDate() - t2.toDate();
      const seconds = moment.duration(Math.abs(span)).asSeconds();
      returnsecondToDate(seconds, lng); }); }};Copy the code

This is mainly used for time formatting, the back end may not return Beijing time, or return a timestamp, even if it is normal time, may be the front end only need to display the date or time, or to display the time before the operation, the above basic plug-in basically covers these operations. Use only in the template:

<template>
    {{date | toTimeString}}
</template>    
Copy the code

The controversial Mixin

Mixins have been said to be vue’s biggest failure, reducing the readability of code. I think it’s a good knife if it’s used well. Mixins are used to extract common code logic and reduce code. I think when should mixins be used, if you simply want to reduce code and still not use mixins? It should be used to define an interface.

Provide a form entry, and the outside world passes in a selectedType parameter to select a different form. The outside world does not need to know how the type and form map.

<template>
  <div class="demand-form">
    <component
      :is="currentComponent"
      v-on="$listeners"
      v-bind="{selectedType, ... $attrs}"/>
  </div>
</template>
<script>
  import GeneralForm from './General';
  import NewProductForm from './NewProduct';

  const BUSINESS_TYPE = {
    OEM: 5.NEW_PRODUCT: 35.//
  };

  export default {
    name: 'DemandForm'.components: {GeneralForm, NewProductForm},
    props: {
      selectedType: {type: Object.default: (a)= >({})}},computed: {
      currentComponent() {
        const {id} = this.selectedType;
        let component = GeneralForm;
        switch (id) {
          case BUSINESS_TYPE.NEW_PRODUCT:
            component = NewProductForm;
            break;
        }
        returncomponent; }}};</script>
Copy the code

Define an interface. Each specific form should implement the following interface:

export default {
  props: {
    detailData: {type: Object.default: (a)= > ({})},
    parentDetailData: {type: Object.default: (a)= > ({})},
    selectedType: {type: Object.default: (a)= > ({})},
    // Whether to create a subproject
    isCreateSonProject: {type: Boolean.default: false},
    // Whether the status is updated
    isEdit: {type: Boolean.default: false},
  },
  data() {
    return {
      uploadLoading: false}; },methods: {
    initFormData() {
      // This method is used to initialize the form data when the form is updated
      console.error("The 'initFormatData' method must be defined");
    },
    uploadForm() {
      console.error("Method 'uploadForm' must be defined");
    },
    // Call the actual interface to submit the form
    async doUpload(data) {
        // Call the interface here to submit the form, lodaing and all that stuff
    },
  },
  created() {
    if (this.isEdit) {
      this.initFormData();
    }
    // Receive the specified time from the outside world via eventBus
    this.bus.$on('onFormConfirm', () = > {this.uploadForm();
    });
  },
  beforeDestroy() {
    this.bus.$off('onFormConfirm'); }};Copy the code

How should the form interface be implemented?

<template> <div class="form" v-loading="uploadLoading"> <ElForm ref="form" size="mini" label-position="top" // Form contents </ElForm> </div> </template> <script> export default {// Import mixins: [demandFormMixin], data() { return { formData: { name: '' }, }; }, computed: {rules() {return {// Form validation rules}; },}, methods: {initFormData() {const {name} = this.detaildata; const updateData = { name }; this.formData = updateData; }, async uploadForm() { await this.$refs.form.validate(); const { formData: { name } } = this; // Do not submit the form using... Const data = {name}; const data = {name}; this.doUpload(data); ,}}}; </script>Copy the code

Each form only needs to implement initFormData, which is used to fill in data when updating, and uploadForm, which is used to submit data. The form only needs to focus on inserting data when it is updated and preparing data when it is submitted. When you add a new type of form, the amount of code is reduced without causing confusion.

Use $attrs and $Listeners properly

$Listeners and $attrs can also reduce the readability of components to some extent, but you should use them sometimes. I often use it in two situations:

  • $listeners and $listeners are often needed to wrap components of a third party, such as Element, to extract a generic component, but the wrapper should not cause the original attributes or events to be lost.
  • The second is for pass-through. There are parameters that my current component doesn’t need, but my child components do, and this is a good time to use $attrs.
<template>
  <ElDialog
    width="85%"
    :visible="show"
    :before-close="closeModal"
    :close-on-click-modal="false">
    <template  v-if="show">
      <component
        :is="contentComponent"
        @onSuccess="onSuccess"
        @onCancel="closeModal"
        v-bind="$attrs"/>
    </template>
  </ElDialog>
</template>
<script>
  import EndApply from './EndApply';
  import WorkloadAssess from './WorkloadAssess';
  import ChangeProject from './change-project';

  const COMPONENT_MAP = {
    END_APPLY: 'EndApply'.// Closing application
    WORKLOAD_ASSESS: 'WorkloadAssess'.// Workload assessment
    CHANGE_PROJECT: 'ChangeProject' // Request for change
  };

  export default {
    components: {EndApply, WorkloadAssess, ChangeProject},
    props: {
      show: {type: Boolean.required: true},
      componentId: {type: String}},computed: {
      contentComponent() {
        return COMPONENT_MAP[this.componentId]; }},methods: {
      closeModal() {
        this.$emit('update:show'.false);
      },
      onSuccess() {
        this.$emit('onSuccess');
        this.closeModal(); }}};</script>
Copy the code
    <ProgressModal
      :show.sync="showProgressModal"
      @onSuccess="pauseRefresh"
      v-bind="{... progressModalParams}"/>
Copy the code

The contents of the Dialog are mutable, and the current container component only needs to handle the closing of the Dialog. As for the parameters of the content component, they are passed directly down. The benefit of this is obvious. The current Dialog component doesn’t have any other parameters that I didn’t use. This component is just responsible for closing the Dialog, but it also doesn’t block the passage of parameters to its children, which can be anything. The progressModalParams parameter can be configured before show is true.

Componentized programming

A lot of people think they know componentized programming, but I think componentized programming is pretty deep, and I just have an opinion about it. In the project I was in touch with, there were thousands of lines of code in one component, and the whole component was filled with the smell of CTRL + C and CTRL + V. The students in subsequent maintenance were really uncomfortable, and it was extremely difficult to change the whole body and add new functions. The core ideas of componentized programming should be separation of concerns, single responsibility principle and open closed principle.

  • Separation of concerns: Vue makes good use of separation of concerns, such as separating template, script, and style. You have to use this idea in componentized programming. Each part of a component should be reasonably separated according to its concerns. This separation is not necessarily divided into two components. When the business volume is large, the two components will be separated within the component group, and the smallest unit should be the component. Once you use separation of concerns, you will find that complex problems are suddenly simpler, and more importantly, maintainers can easily take over the project.
  • Single responsibility principle: many students want to write components in a component to write all things, for fear of more than one component. A single responsibility tells you that a component, a function should only do one thing. Officially, a component has only one reason to change it. There are two cases of independent components:
    1. One is that this component is a generic component, which I divide into basic components and generic business components. Basic components are components like those provided by elementUI and iView, which cannot be coupled to interfaces and vuex and can be used in any project. A business common component is a common business block that can be coupled to interfaces and vuEX, but is common throughout the project. For example, if I have a component that input search for company members, I will completely encapsulate this piece with the interface and provide the external interface V-Model and some parameters. Common business components can greatly improve the efficiency of the overall project.
    2. The other is a pure business component that is not generic. So why separate them? For the sake of maintainability and extensibility, when we separate it as a component, we separate the concerns of the business. The requirement of this component independence is that it has a high degree of business cohesion, and only when it has a high degree of business cohesion can we extract it well. High cohesion for those of you who don’t understand this can mean that things in this block have little or no relationship to anything else.
  • The Open closed principle: Open for extension and closed for modification, a good architecture should follow this principle. We might as well think about this when we write front-end components. Ask yourself a few questions about how this will expand. Sometimes when faced with two or more pages that are similar, we tend to pull them out, which is fine most of the time, but can cause problems when requirements are variable. When the business changes and the two components become different, many people choose to simply add logic to distinguish them, but as the changes increase, the family code is born in your hands. Left with such bad code, it should have been separated at the first change. Sometimes there is a trade-off between code volume and extensibility.

conclusion

In general, VUE is a relatively simple framework, but writing a good VUE requires a process, more thinking, more summary. More often than not, we should do what is right rather than what is convenient. The programmer most avoid is inertia, inertia to progress. The above are some of my experience in the work, there are insufficient places please understand and correct.