[译] 如何用钩子解耦 Vue.js 应用

1,677 阅读7分钟

原文:Build Decoupled Vue.js Applications with Hooks
作者: Markus Oberlehner
发表时间:9 June 2019
译者: 西楼听雨
发表时间: 2019/07/15 (转载请注明出处)

Recently, I was wondering how best to decouple the code needed to track certain form submissions (e.g. conversion tracking in Google Analytics or Matomo) from the business logic of the forms.

最近我一直在思考,怎么用最好的方式将表单提交中的事件跟踪代码(例如 Google Analytics 和 Matomo 的用户转化事件跟踪)与业务逻辑代码进行分离。

Hooks are often used to solve these types of problems. Using hooks makes it possible to decouple our components responsible for handling business logic from the purely optional tracking logic, which we can then keep in one place instead of scattering across all our components.

钩子——常用于解决这类问题。使用钩子,可以将我们负责处理业务逻辑的组件与那些可有可无的单纯的只是用于做跟踪的逻辑进行解耦。这样我们就可以将他们集中在一块,而不是分散在我们的各个组件里。

准备阶段

Before we take a look at how this technique can decouple tracking from the rest of the application logic, we begin by setting up our hook system.

在我们了解这项技术如何将事件跟踪代码从除此之外的应用代码中进行解耦之前,我们先把我们的钩子系统搭建好。

// src/utils/hooks.js
const hooks = [];

export function addHook(hook) {
  hooks.push(hook);
}

export function runHooks(context) {
  return hooks
    // Only run hooks that fulfill their condition.
    // 只运行满足各自条件的钩子
    .filter(hook => hook.condition(context))
    .map(hook => hook.callback(context));
}

export function withHooks(func, context) {
  return (...args) => {
    const result = func(...args);

    if (result.then) {
      result
        .then(payload => runHooks({ ...context, payload }))
        .catch(error => runHooks({ ...context, error }));
      return result;
    }

    runHooks({ ...context, payload: result });
    return result;
  };
}

The code above makes it possible to add Hook objects to a stack of Hooks which are triggered as soon as runHooks() is called. Each Hook is an object with a condition and a callback. The given callback() function is only called if the condition() function returns true. Both functions are passed the context of the current method which is called.

上面这段代码可以让我们往一个钩子栈中添加钩子对象,当 runHooks() 被调用时,这些钩子就会被触发。每个钩子都是一个对象,都具有一个 condition 和一个 callback 。其中,callback() 函数只有在 condition() 函数返回 true 时才会被调用;两个函数都会接收到当前被调用方法的 context (上下文)对象。

用钩子来实现事件跟踪

Now we’re ready to use our Hook module to build a decoupled event tracking system. In the following code snippet you can see the code of the ContactFormContainer component which is responsible for injecting the dependencies for the ContactForm component.

现在我们的钩子模块已经写好了,我们可以开始来构建我们的解耦了的事件跟踪系统,下面的这段代码块,是ContactFromContainer 组件的代码,它的责任是为 ContactForm 组件注入依赖。

<template>
  <ContactForm/>
</template>

<script>
// src/components/ContactFormContainer.vue
import { post } from '../services/contact-form';
import { withHooks } from '../utils/hooks';

import ContactForm from './ContactForm.vue';

export default {
  components: {
    ContactForm,
  },
  provide: {
    // We pass an additional `id` context
    // property to make it easier to identify
    // calls of `post()` when running our Hooks.
    // 在这里,我们额外地传入了一个 `id` 上下文属性,
    // 当我们的钩子开始执行时,可以很容易地鉴别出是对
    // `post()` 方法的调用
    post: withHooks(post, { id: 'contact-form.post' }),
  },
};
</script>

If you’re also interested in the code of the ContactForm component you can take a look at it here.

如果你对 ContactForm 组件的代码感兴趣,可以到这查看

By wrapping the post() method withHooks() all Hooks are now executed every time the provided post() method is called in the ContactForm component.

通过将 post() 方法用 withHooks() 进行封装,接下来,每次只要注入的 post() 方法一被调用,所有的钩子就都会被执行。

添加事件跟踪钩子

There are currently no Hooks that could be executed as we have not added any Hooks yet. Let’s change that by adding a new file where we can register all our tracking Hooks.

目前为止,我们还没有添加任何钩子,所以没有钩子可执行。现在我们来新增一个文件,用来放置所有我们的事件跟踪钩子。

// src/utils/tracking.js
import { addHook } from './hooks';

const CONTACT_FORM = 'contact-form.post';

addHook({
  condition({ error, id }) {
    return !error && id === CONTACT_FORM;
  },
  callback(context) {
    // This is where you'd trigger your Google
    // Analytics or Matomo tracking event.
    console.log('track contact form submission', context);
  }
});

Here you can see that we add a new Hook which is only fired if there is no error and the id context parameter matches the CONTACT_FORM id. In the callback() function we’d usually trigger an event in our tracking service of choice but because this is only a demo we simply trigger a console.log().

在上面代码中可以看到,我们添加了一个新的钩子,这个钩子只有在没有发生错误且上下文 (context) 中的 id 参数匹配 CONTACT_FORM 的情况下才会被触发。在 callback() 函数中,通常,我们会触发一个事件跟踪服务的事件,但由于这只是个 demo 我们就只是简单地触发一个 console.log()

防止特定环境下触发跟踪事件

You most likely do not want to send tracking events in your development environment or, for example, when running unit tests. Because we have everything in one place with this approach, we can easily prevent tracking in certain environments.

在开发环境中,或者,比如在运行单元测试时,我们可能不想发送跟踪事件,采用了这种方式后,我们可以很容易做到这一点,因为我们把所有东西都放在了一个地方。

// src/utils/tracking.js
import { addHook } from './hooks';

const CONTACT_FORM = 'contact-form.post';
const TRACKING_ENABLED = process.env.NODE_ENV !== 'development';

if (TRACKING_ENABLED) {
  addHook({
    condition({ error, id }) {
      return !error && id === CONTACT_FORM;
    },
    callback(context) {
      // This is where you'd trigger your Google
      // Analytics or Matomo tracking event.
      // 在这里触发你的 Google Analytics 或者 Matomo 跟踪事件
      console.log('track contact form submission', context);
    }
  });
}

进行错误跟踪

In the following example you can see how we can also use Hooks to implement a centralized error tracking system.

在下面这个例子中可以看到,我们又是如何使用钩子来实现集中化的错误跟踪系统的。

// src/utils/tracking.js
// ...

addHook({
  condition({ id }) {
    return id === USER_CREATED;
  },
  callback(context) {
    if (context.error) {
      // This is where you'd trigger an event in
      // Sentry or some other error tracking service.
      // 在这里触发 Sentray 或者其他错误跟踪服务的事件
      return console.log('发生错误', context.error);
    }
    console.log('新增用户', context);
  }
});

If the context contains an error property we don’t track a Google Analytics or Matomo event but send an error event to our error tracking service instead.

如果 context 中包含 error 属性,我们就不发送 Google Analytics 或者 Matomo 跟踪事件了,而是改为发送错误事件给我们的错误跟踪服务。

用钩子跟踪点击事件

Hooks are especially useful for intercepting API requests but we can basically use it for everything we want. But keep in mind that this pattern is best with an all or nothing approach. You might run Hooks for every API request and you might build a custom router link or button component to run Hooks every time a link or a button is clicked. But I’d recommend you to not use withHooks() for individual cases.

钩子特别适合用来做 API 请求的拦截,但除此之外其实我们还可以用它来做任何事。不过要记住的一点是,这种设计模式最适用的是那种要么完全采用要么完全不用的情形。你可以为每个 API 请求都挂上钩子,也可以构建一个自定义的 router-link 或者 button 组件,在他们被点击的时候运行钩子;但是我并不推荐单独地对某个场景加以使用

总结

As with almost every advanced pattern in programming, hooks also have their downsides. First of all, they add another layer of complexity. Adding tracking logic directly into the code of your components may not be the cleanest solution, but it is definitely the most straightforward. Especially if your application is very small, using Hooks might only make your codebase more complicated instead of making it simpler.

和其他几乎所有高级设计模式一样,钩子也有它自己的缺点。首先第一点,他们又新增了一层复杂度。**直接在组件中添加事件跟踪逻辑,也许不是最整洁的做法,但它确实是最直接了当的方式。**特别是当你的应用程序很小的时候,使用钩子,只会让你的代码库变得更加复杂而不是更加简单。

I strongly recommend that you first think about all the advantages and disadvantages before deciding whether you want to implement this pattern or not. However, in the right circumstances, it can greatly improve the overall architecture of your Vue.js app.

**强烈建议,在决定是否采用这种设计模式之前,首先要想清楚它的所有优势和劣势。**话虽如此,但在一些合适的情况下,它确实是可以给你的 Vue 应用整体架构带来很大的提升的。