Vue advanced должен изучить высокоуровневые компоненты HOC

JavaScript Vue.js

предисловие

Когда-то концепция высокоуровневых компонентов была очень популярна в React, но она мало обсуждалась в сообществе Vue.Эта статья действительно поможет вам сыграть в продвинутую операцию.

Позвольте мне сначала сказать вам, что суть этой статьи состоит в том, чтобы изучить этот тип мышления, то есть智能组件а также木偶组件Развязка, услышанная об этом понятии, не имеет значения, далее объясню подробно.

Это можно сделать разными способами, напримерslot-scopes, например, будущееcomposition-api. Код, написанный в этой статье, не рекомендуется использовать в производственной среде. В производственной среде есть более зрелые библиотеки для использования. В этой статье подчеркивается, что思想, кстати, пересадить геймплей сообщества React на скин.

Не брызгай на меня, не брызгай на меня, не брызгай на меня! !Эта статья предназначена только для демонстрации идеи компонентов высокого уровня.Если вы хотите упростить асинхронное управление состоянием, упомянутое в статье, в реальном бизнесе, используйтеslot-scopesбиблиотека с открытым исходным кодомvue-promised

Также упоминается в названии20kНа самом деле это немного заглавная вечеринка.Что я хочу выразить больше, так это то, что у нас должен быть такой дух.Только это умение точно не позволит вам достичь20k. Но я верю, что до тех пор, пока у всех есть дух изучения расширенного использования, оптимизации бизнес-кода и повышения эффективности, мы всегда будем этого добиваться, и этот день не за горами.

пример

В этой статье используются наиболее распространенные требования в обычной разработке, то есть异步数据的请求Например, начнем с написания обычного плеера:

<template>
    <div v-if="error">failed to load</div>
    <div v-else-if="loading">loading...</div>
    <div v-else>hello {{result.name}}!</div>
</template>

<script>
export default {
  data() {
    return {
        result: {
          name: '',
        },
        loading: false,
        error: false,
    },
  },
  async created() {
      try {
        // 管理loading
        this.loading = true
        // 取数据
        const data = await this.$axios('/api/user')  
        this.data = data
      } catch (e) {
        // 管理error
        this.error = true  
      } finally {
        // 管理loading
        this.loading = false
      }
  },
}
</script>

Обычно мы пишем так, и обычно не чувствуем никакой проблемы, но на самом деле каждый раз, когда мы пишем асинхронные запросы, мы должны иметьloading,errorстатус, необходимо иметь取数据логика, и управлять этими состояниями.

Итак, придумайте способ абстрагировать его? Кажется, что хороших способов не так уж и много.До того, как Hook стал популярным, сообщество React часто использовалоHOC(компонент высокого порядка) — это компонент высокого порядка для работы с такой абстракцией.

Что такое компоненты высшего порядка?

Здесь мы должны думать о компоненте более высокого порядка, в конце концов, это то, что понятие, на самом деле, в конечном счете, является компонентами высокого порядка:

一个函数接受一个组件为参数,返回一个包装后的组件.

в реакции

В React компонентыClass, поэтому компоненты более высокого порядка иногда используют装饰器синтаксис для достижения, потому что装饰器Суть также в том, чтобы принятьClassвернуть новыйClass.

В мире React компоненты более высокого порядкаf(Class) -> 新的Class.

во Вью

В мире Vue компонент — это объект, поэтому компонент более высокого порядка — это функция, которая принимает объект и возвращает новый обернутый объект.

По аналогии с миром Vue компоненты более высокого порядкаf(object) -> 新的object.

Смарт-компоненты и кукольные компоненты

Если вы еще не знаете木偶компоненты и智能Концепция компонентов, позвольте мне дать вам краткое введение, это очень зрелая концепция в сообществе React.

木偶Компонент: как у марионетки, только на основе внешнего входящегоpropsдля отображения соответствующего представления, независимо от того, откуда были получены данные.

智能Компоненты: обычно упакованы в木偶Вне компонента данные получаются через запросы и т.п. и передаются в木偶компонент, который управляет его рендерингом.

В общем случае их структурная взаимосвязь выглядит следующим образом:

<智能组件>
  <木偶组件 />
</智能组件>

У них есть другой псевдоним, который容器组件а такжеui组件, не очень изображение.

выполнить

Специально в приведенном выше примере (если вы забудете, вернемся и посмотрите, ха-ха), наше мышление это,

  1. Компоненты более высокого порядка принимают木偶组件а также请求的方法как параметр
  2. существуетmountedДанные запрашиваются в жизненном цикле
  3. Передайте запрошенные данные черезpropsПерейти к木偶组件.

Следующим шагом является реализация этой идеи. Впервые упомянутая выше,HOCЭто функция, на этот раз наше требование — реализовать управление запросами.HOC, затем сначала определите, чтобы он принимал два параметра, мы помещаем этоHOCназываетсяwithPromise.

а такжеloading,errorсостояние ожидания и加载中,加载错误Дождитесь соответствующего представления, мы должны新返回的包装组件, то есть в следующей функцииreturn 的那个新的对象определено в.

const withPromise = (wrapped, promiseFn) => {
  return {
    name: "with-promise",
    data() {
      return {
        loading: false,
        error: false,
        result: null,
      };
    },
    async mounted() {
      this.loading = true;
      const result = await promiseFn().finally(() => {
        this.loading = false;
      });
      this.result = result;
    },
  };
};

В параметре:

  1. wrappedТо есть объект компонента должен быть обернут.
  2. promiseFuncТо есть функция, соответствующая запросу, должна вернуть Promise.

Выглядит хорошо, но в функции мы не можем.vueнаписать в один файлtemplateНапишите шаблон так,

Но мы знаем, что шаблон в конечном итоге компилируется в объект-компонент.renderфункция, то мы можем написать это напрямуюrenderфункция. (注意,本例子是因为便于演示才使用的原始语法,脚手架创建的项目可以直接用jsxграмматика. )

в этотrenderВ функцию мы передаем входящийwrappedТо есть компоненты марионетки завернуты.

Это формы智能组件获取数据 -> 木偶组件消费数据, такие потоки данных.

const withPromise = (wrapped, promiseFn) => {
  return {
    data() { ... },
    async mounted() { ... },
    render(h) {
      return h(wrapped, {
        props: {
          result: this.result,
          loading: this.loading,
        },
      });
    },
  };
};

На данный момент это уже едва пригодный для использования прототип, давайте сделаем заявление木偶компоненты.

Это на самом деле逻辑和视图分离идея.

const view = {
  template: `
    <span>
      <span>{{result?.name}}</span>
    </span>
  `,
  props: ["result", "loading"],
};

Обратите внимание, что компоненты здесь могут быть любыми..vueфайл, я просто использую этот способ записи здесь для простоты.

Потом произошло что-то волшебное, не моргай, мы использовалиwithPromiseзавернуть этоviewкомпоненты.

// 假装这是一个 axios 请求函数
const request = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ name: "ssh" });
    }, 1000);
  });
};

const hoc = withPromise(view, request)

Затем визуализируйте его в родительском компоненте:

<div id="app">
  <hoc />
</div>

<script>
 const hoc = withPromise(view, request)

 new Vue({
    el: 'app',
    components: {
      hoc
    }
 })
</script>

В этот момент компонент отображает мое имя после того, как в течение секунды остается пустым.ssh, проходит весь асинхронный поток данных.

теперь добавляю加载中а также加载失败Просмотр делает взаимодействие более дружественным.

const withPromise = (wrapped, promiseFn) => {
  return {
    data() { ... },
    async mounted() { ... },
    render(h) {
      const args = {
        props: {
          result: this.result,
          loading: this.loading,
        },
      };

      const wrapper = h("div", [
        h(wrapped, args),
        this.loading ? h("span", ["加载中……"]) : null,
        this.error ? h("span", ["加载错误"]) : null,
      ]);

      return wrapper;
    },
  };
};

Код пока можно найти наПредварительный просмотр эффектаВы также можете просмотреть исходный код непосредственно в исходниках консоли.

Полный

Хотя компоненты высокого уровня до сих пор можно продемонстрировать, они не завершены, и в нем по-прежнему отсутствуют некоторые функции, такие как

  1. Чтобы получить параметры, определенные в дочернем компоненте, как параметры исходного запроса на отправку.
  2. Прослушивать изменения параметров запроса в подкомпонентах и ​​повторно отправлять запрос.
  3. внешние компоненты, переданныеhocПараметры компонента теперь не передаются.

Первый пункт понять несложно, параметры запрашиваемой сцены очень гибкие.

Второй пункт также является общим требованием в практических сценариях.

Третий момент заключается в том, что во избежание непонимания некоторых студентов, здесь более подробно, например, мы используем его в самом внешнем слое.hocкомпонент, вы можете передать некоторые дополнительныеpropsилиattrsЧетное插槽slotдо самого сокровенного木偶компоненты. ТакhocВ качестве моста компонент должен взять на себя ответственность за его прозрачную передачу.

Для достижения первого пункта мы согласилисьviewКонкретный компонент должен быть установлен на компонентеkeyПоле как параметр запроса, например, здесь мы договорились, что оно называетсяrequestParams.

const view = {
  template: `
    <span>
      <span>{{result?.name}}</span>
    </span>
  `,
  data() {
    // 发送请求的时候要带上它
    requestParams: {
      name: 'ssh'
    }  
  },
  props: ["result", "loading"],
};

переписать нашrequestфункцию, готовя ее к приему аргументов,

и пусть это响应数据вернуть как есть请求参数.

// 假装这是一个 axios 请求函数
const request = (params) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(params);
    }, 1000);
  });
};

Так что теперь вопрос в том, как намhocкомпонентыviewстоимость компонента,

Как мы обычно получаем экземпляры дочерних компонентов? Вот такref, также используйте его здесь:

const withPromise = (wrapped, promiseFn) => {
  return {
    data() { ... },
    async mounted() {
      this.loading = true;
      // 从子组件实例里拿到数据
      const { requestParams } = this.$refs.wrapped
      // 传递给请求函数
      const result = await promiseFn(requestParams).finally(() => {
        this.loading = false;
      });
      this.result = result;
    },
    render(h) {
      const args = {
        props: {
          result: this.result,
          loading: this.loading,
        },
        // 这里传个 ref,就能拿到子组件实例了,和平常模板中的用法一样。
        ref: 'wrapped'
      };

      const wrapper = h("div", [
        this.loading ? h("span", ["加载中……"]) : null,
        this.error ? h("span", ["加载错误"]) : null,
        h(wrapped, args),
      ]);

      return wrapper;
    },
  };
};

Для выполнения второго пункта, при изменении параметров запроса дочернего компонента, родительскому компоненту также необходимо响应式повторно отправить запрос и передать новые данные дочернему компоненту.

const withPromise = (wrapped, promiseFn) => {
  return {
    data() { ... },
    methods: {
      // 请求抽象成方法
      async request() {
        this.loading = true;
        // 从子组件实例里拿到数据
        const { requestParams } = this.$refs.wrapped;
        // 传递给请求函数
        const result = await promiseFn(requestParams).finally(() => {
          this.loading = false;
        });
        this.result = result;
      },
    },
    async mounted() {
      // 立刻发送请求,并且监听参数变化重新请求
      this.$refs.wrapped.$watch("requestParams", this.request.bind(this), {
        immediate: true,
      });
    },
    render(h) { ... },
  };
};

Третий пункт свойств прозрачной передачи, нам нужно только поставить$attrs,$listeners,$scopedSlotsПросто передайте это,

здесь$attrsсвойство, объявленное во внешнем шаблоне,$listenersфункция слушателя, объявленная во внешнем шаблоне,

Возьмите этот пример:

<my-input value="ssh" @change="onChange" />

Внутри компонента вы можете получить такую ​​структуру:

{
  $attrs: {
    value: 'ssh'
  },
  $listeners: {
    change: onChange
  }
}

Обратите внимание, что прохождение$attrs,$listenersТребование возникает не только в высокоуровневых компонентах, обычно если мы хотимel-inputЭтот компонент инкапсулирует слой вmy-inputЕсли вы хотите объявить один за другимel-inputпринятыйprops, это утомительно, прямая передача$attrs,$listenersВот такel-inputВнутри все переданные параметры все еще могут быть обработаны.

// my-input 内部
<template>
  <el-input v-bind="$attrs" v-on="$listeners" />
</template>

затем вrenderВ функцию его можно прозрачно передать так:

const withPromise = (wrapped, promiseFn) => {
  return {
    ...,
    render(h) {
      const args = {
        props: {
          // 混入 $attrs
          ...this.$attrs,
          result: this.result,
          loading: this.loading,
        },

        // 传递事件
        on: this.$listeners,

        // 传递 $scopedSlots
        scopedSlots: this.$scopedSlots,
        ref: "wrapped",
      };

      const wrapper = h("div", [
        this.loading ? h("span", ["加载中……"]) : null,
        this.error ? h("span", ["加载错误"]) : null,
        h(wrapped, args),
      ]);

      return wrapper;
    },
  };
};

На данный момент реализован полный код:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>hoc-promise</title>
  </head>
  <body>
    <div id="app">
      <hoc msg="msg" @change="onChange">
        <template>
          <div>I am slot</div>
        </template>
        <template v-slot:named>
          <div>I am named slot</div>
        </template>
      </hoc>
    </div>
    <script src="./vue.js"></script>
    <script>
      var view = {
        props: ["result"],
        data() {
          return {
            requestParams: {
              name: "ssh",
            },
          };
        },
        methods: {
          reload() {
            this.requestParams = {
              name: "changed!!",
            };
          },
        },
        template: `
          <span>
            <span>{{result?.name}}</span>
            <slot></slot>
            <slot name="named"></slot>
            <button @click="reload">重新加载数据</button>
          </span>
        `,
      };

      const withPromise = (wrapped, promiseFn) => {
        return {
          data() {
            return {
              loading: false,
              error: false,
              result: null,
            };
          },
          methods: {
            async request() {
              this.loading = true;
              // 从子组件实例里拿到数据
              const { requestParams } = this.$refs.wrapped;
              // 传递给请求函数
              const result = await promiseFn(requestParams).finally(() => {
                this.loading = false;
              });
              this.result = result;
            },
          },
          async mounted() {
            // 立刻发送请求,并且监听参数变化重新请求
            this.$refs.wrapped.$watch(
              "requestParams",
              this.request.bind(this),
              {
                immediate: true,
              }
            );
          },
          render(h) {
            const args = {
              props: {
                // 混入 $attrs
                ...this.$attrs,
                result: this.result,
                loading: this.loading,
              },

              // 传递事件
              on: this.$listeners,

              // 传递 $scopedSlots
              scopedSlots: this.$scopedSlots,
              ref: "wrapped",
            };

            const wrapper = h("div", [
              this.loading ? h("span", ["加载中……"]) : null,
              this.error ? h("span", ["加载错误"]) : null,
              h(wrapped, args),
            ]);

            return wrapper;
          },
        };
      };

      const request = (data) => {
        return new Promise((r) => {
          setTimeout(() => {
            r(data);
          }, 1000);
        });
      };

      var hoc = withPromise(view, request);

      new Vue({
        el: "#app",
        components: {
          hoc,
        },
        methods: {
          onChange() {},
        },
      });
    </script>
  </body>
</html>

допустимыйздесьПредварительный просмотр эффекта кода.

Мы разрабатываем новые компоненты, просто беритеhocПросто приходите и используйте его повторно, его ценность для бизнеса будет отражена, а код будет упрощен до невообразимой степени.

import { getListData } from 'api'
import { withPromise } from 'hoc'

const listView = {
  props: ["result"],
  template: `
    <ul v-if="result>
      <li v-for="item in result">
        {{ item }}
      </li>
    </ul>
  `,
};

export default withPromise(listView, getListData)

Все становится просто и элегантно.

комбинация

Обратите внимание, что эта глава может быть трудной для студентов, которые не знакомились с разработкой React, поэтому вы можете прочитать ее правильно или пропустить ее в первую очередь.

Однажды мы вдруг очень обрадовались и написали высокоуровневый компонент под названиемwithLog, это так же просто, какmountedЦикл объявления помогает распечатать журнал.

const withLog = (wrapped) => {
  return {
    mounted() {
      console.log("I am mounted!")
    },
    render(h) {
      return h(wrapped)
    },
  }
}

Здесь мы находим это сноваon,scopedSlotsНа самом деле довольно сложно дождаться извлечения и передачи атрибута.thisИнтегрируйте функции, требующие прозрачных атрибутов:

function normalizeProps(vm) {
  return {
    on: vm.$listeners,
    attr: vm.$attrs,
    // 传递 $scopedSlots
    scopedSlots: vm.$scopedSlots,
  }
}

затем вhВторой параметр можно извлечь и передать.

const withLog = (wrapped) => {
  return {
    mounted() {
      console.log("I am mounted!")
    },
    render(h) {
      return h(wrapped, normalizeProps(this))
    },
  }
}

Затем заверните его вhocПомимо:

var hoc = withLog(withPromise(view, request));

Видно, что такая вложенность — совсем головная боль, ставимreduxэто карриcomposeФункция перемещена сюда, этоcomposeФункции, по сути, постоянно делают функции более высокого порядка и возвращают новую функцию.

функциональный состав

function compose(...funcs) {
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

compose(a, b, c)Возвращается новая функция, эта функция будет проходить через несколько функций嵌套执行

Сигнатура возвращаемой функции:(...args) => a(b(c(...args)))

Эта функция может занять много времени у начинающих студентов, потому что она действительно очень сложная, но как только вы ее поймете, ваше функциональное мышление выйдет на новый уровень.

Я будуgithubдля многопараметрическогоcomposeВ примере сделал пошаговый разбор разборки, если интересно можете глянутьСоставьте принцип разборки

круговой состав

Если вы этого не понимаете函数式изcomposeМетод записи, далее мы используем обычный цикл для записи, то есть возвращаем функцию, выполняем входящий массив функций справа налево, а возвращаемое значение предыдущей функции будет использоваться как параметр следующего выполнения функции.

нормально написаноcomposeФункция такова:

function compose(...args) {
  return function(arg) {
    let i = args.length - 1
    let res = arg
    while(i >= 0) {
     let func = args[i]
     res = func(res)
     i--
    }
    return res
  }
}

Модернизация с помощью Promise

Но это также означает, что нам нужно преобразоватьwithPromiseФункции высшего порядка, т.к. внимательно наблюдайте за этимcompose, который оборачивает функцию так, что она принимает аргумент, и оборачивает первую функцию.返回值Передано следующей функции в качестве параметра.

Напримерcompose(a, b)Сказать,b(arg)Возвращаемое значение будетaпараметры, дальнейшие вызовыa(b(args))

Это необходимо для того, чтобы функции, принятые в compose, имели только один параметр для каждого элемента..

Затем, согласно этой идее, преобразуемwithPromise, на самом деле, чтобы еще больше его улучшить, пусть он возвращает функцию, которая принимает только один параметр:

const withPromise = (promiseFn) => {
  // 返回的这一层函数 wrap,就符合我们的要求,只接受一个参数
  return function wrap(wrapped) {
    // 再往里一层 才返回组件
    return {
      mounted() {},
      render() {},
    }
  }
}

С его помощью вы можете более элегантно комбинировать компоненты более высокого порядка:

const compsosed = compose(
    withPromise(request),
    withLog,
)

const hoc = compsosed(view)

надcomposeПолный код главыВ этот.

Обратите внимание, что нередко вы не понимаете эти концепции в первый раз в этом разделе.Они очень популярны в сообществе React, но редко обсуждаются в сообществе Vue! об этомcomposeФункция, когда я сначала наткнулся на него в сообществе React, я не мог понять это вообще. Не слишком поздно понять его использование первым.

Реальная деловая сцена

Многие могут подумать, что приведенный выше код не имеет большого практического значения, ноvue-routerизРасширенная документация по использованиюСуществует реальная сцена, где компоненты высокого порядка используются для решения проблем.

Сначала кратко опишите сценарий, который мы знаемvue-routerАсинхронную маршрутизацию можно настроить, но когда скорость сети очень низкая, эта асинхронная маршрутизация соответствуетchunkТо есть код компонента, который не будет прыгать, пока не завершится загрузка.

этот абзац下载异步组件время, когда мы хотим, чтобы страница отображалаLoadingКомпоненты, делающие взаимодействие более дружественным.

существуетДокументация Vue — асинхронные компонентыВ этой главе хорошо видно, что Vue поддерживает объявление асинхронных компонентов.loadingСоответствующий компонент рендеринга:

const AsyncComponent = () => ({
  // 需要加载的组件 (应该是一个 `Promise` 对象)
  component: import('./MyComponent.vue'),
  // 异步组件加载时使用的组件
  loading: LoadingComponent,
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 200,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000
})

Попробуем написать этот код какvue-router, перепишите исходный асинхронный маршрут:

new VueRouter({
    routes: [{
        path: '/',
-        component: () => import('./MyComponent.vue')
+        component: AsyncComponent
    }]
})

Вы обнаружите, что он вообще не поддерживается, и выполните его тщательную отладку.vue-routerИсходный код найден,vue-routerВнутренний разбор асинхронных компонентов иvueОбработка представляет собой два совершенно разных набора логики, вvue-routerРеализация не поможет вам отрендеритьLoadingкомпоненты.

Это не должно составить труда для остроумных общественных лидеров, давайте изменим образ мышления, давайтеvue-routerперейти к容器组件,это容器组件Помогите нам использовать внутренний механизм рендеринга Vue для рендерингаAsyncComponent, иначе это может быть отображеноloadingположение дел? Конкретный код выглядит следующим образом:

Благодаря vue-routercomponentполе принимаетPromise, поэтому мы используем компонентPromise.resolveОберните один слой.

function lazyLoadView (AsyncView) {
  const AsyncHandler = () => ({
    component: AsyncView,
    loading: require('./Loading.vue').default,
    error: require('./Timeout.vue').default,
    delay: 400,
    timeout: 10000
  })

  return Promise.resolve({
    functional: true,
    render (h, { data, children }) {
      // 这里用 vue 内部的渲染机制去渲染真正的异步组件
      return h(AsyncHandler, data, children)
    }
  })
}
  
const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: () => lazyLoadView(import('./Foo.vue'))
    }
  ]
})

Таким образом, разрыв между загрузкой кода при прыжке, красивыйLoadingКомпоненты отображаются на странице.

Суммировать

Весь код для этой статьи хранится вРепозиторий на гитхабе, и предоставитьпредварительный просмотр.

Я хотел бы передать этот документ автору «Vue Technology Insider», который очень помог мне в моем пути изучения исходного кода.hcysunБольшой парень, хотя я еще не разговаривал с ним, когда я был новичком, проработавшим несколько месяцев, я нашел эту статью, подумав о потребностях бизнеса:Исследуйте компоненты высшего порядка Vue | HcySunYang

В то время я не мог понять проблемы с исходным кодом и решения по исправлению, затронутые в этой статье, а затем я переключился на другой способ реализации бизнеса, но вещи, упомянутые в этой статье, не давали мне покоя.После напряженной работы, Я усердно работаю над изучением исходного кода и надеюсь, что однажды смогу полностью понять эту статью.

Теперь я, наконец, могу понять, о чем говорится в статье.$vnodeа такжеcontextЧто это значит, но эта ошибка в версии Vue 2.6 из-заslotКстати, реализация была переписана и исправлена, и теперь она использует последнюю версию Vue.slotБлагодаря синтаксису и функциям более высокого порядка ошибки, упомянутые в этой статье, больше не будут встречаться.

❤️Спасибо всем

1. Если эта статья была вам полезна, пожалуйста, поддержите ее лайком, ваш "лайк" - движущая сила моего творчества.

2. Подпишитесь на официальный аккаунт «Front-end from advanced to accept», чтобы добавить меня в друзья, я втяну вас в «Front-end группу расширенного обмена», все смогут общаться и добиваться прогресса вместе.