From ec78d5b1d9f315e374680d12a79720300bc58f2f Mon Sep 17 00:00:00 2001
From: neverland <chenjiahan@youzan.com>
Date: Sun, 3 Jul 2022 13:39:27 +0800
Subject: [PATCH] refactor(Notify): redesign function-call API (#10782)

* refactor(Notify): redesign function-call API

* docs: update
---
 .../docs/markdown/migrate-from-v3.zh-CN.md    | 52 +++++++++++
 packages/vant/src/icon/demo/index.vue         |  4 +-
 packages/vant/src/notify/README.md            | 60 +++++++-----
 packages/vant/src/notify/README.zh-CN.md      | 91 +++++++------------
 packages/vant/src/notify/demo/index.vue       | 18 ++--
 packages/vant/src/notify/function-call.tsx    | 75 ++++++---------
 packages/vant/src/notify/index.ts             | 12 ++-
 packages/vant/src/notify/test/index.spec.ts   | 48 +++++-----
 packages/vant/src/notify/types.ts             |  7 --
 9 files changed, 192 insertions(+), 175 deletions(-)

diff --git a/packages/vant/docs/markdown/migrate-from-v3.zh-CN.md b/packages/vant/docs/markdown/migrate-from-v3.zh-CN.md
index 22a0df264..3c1e7837a 100644
--- a/packages/vant/docs/markdown/migrate-from-v3.zh-CN.md
+++ b/packages/vant/docs/markdown/migrate-from-v3.zh-CN.md
@@ -120,6 +120,58 @@ declare module '@vue/runtime-core' {
 }
 ```
 
+### Notify 调用方式调整
+
+Vant 4 中,`Notify` 组件的调用方式也进行了调整,与 `Dialog` 组件的改动一致:
+
+```js
+// Vant 3
+Notify(); // 函数调用
+Notify.Component; // 组件对象
+
+// Vant 4
+showNotify(); // 函数调用
+Notify; // 组件对象
+```
+
+`Notify` 上挂载的其他方法也进行了重命名,新旧 API 的映射关系如下:
+
+```js
+Notify(); // -> showNotify()
+Notify.clear(); // -> hideNotify()
+Notify.setDefaultOptions(); // -> setNotifyDefaultOptions()
+Notify.resetDefaultOptions(); // -> resetNotifyDefaultOptions()
+```
+
+同时,Vant 4 将不再在 `this` 对象上全局注册 `$notify` 方法,这意味着 `this` 对象上将无法访问到 `$notify`。
+
+```js
+export default {
+  mounted() {
+    // 无效代码
+    this.$notify({
+      message: '内容',
+    });
+  },
+};
+```
+
+如果需要全局方法,可以手动在 `app` 对象上注册:
+
+```js
+import { showNotify } from 'vant';
+
+// 注册 $notify 方法
+app.config.globalProperties.$notify = showNotify;
+
+// 添加 TS 类型定义
+declare module '@vue/runtime-core' {
+  interface ComponentCustomProperties {
+    $notify: typeof showNotify;
+  }
+}
+```
+
 ### 事件命名调整
 
 从 Vant 4 开始,所有的事件均采用 Vue 官方推荐的**驼峰格式**进行命名。
diff --git a/packages/vant/src/icon/demo/index.vue b/packages/vant/src/icon/demo/index.vue
index fa111dd7e..a0e7dc089 100644
--- a/packages/vant/src/icon/demo/index.vue
+++ b/packages/vant/src/icon/demo/index.vue
@@ -7,7 +7,7 @@ import VanCol from '../../col';
 import icons from '@vant/icons';
 import { ref } from 'vue';
 import { cdnURL, useTranslate } from '../../../docs/site';
-import { Notify } from '../../notify';
+import { showNotify } from '../../notify';
 
 // from https://30secondsofcode.org
 function copyToClipboard(str: string) {
@@ -82,7 +82,7 @@ const copy = (icon: string, option: Record<string, unknown> = {}) => {
   tag = `${tag} />`;
   copyToClipboard(tag);
 
-  Notify({
+  showNotify({
     type: 'success',
     duration: 1500,
     className: 'demo-icon-notify',
diff --git a/packages/vant/src/notify/README.md b/packages/vant/src/notify/README.md
index 7dac56de8..fd3212c02 100644
--- a/packages/vant/src/notify/README.md
+++ b/packages/vant/src/notify/README.md
@@ -2,7 +2,7 @@
 
 ### Intro
 
-The display message prompt is at the top of the page, and supports two methods: function call and component call.
+The display message prompt is at the top of the page, and supports two methods: component call and function call.
 
 ### Install
 
@@ -16,55 +16,65 @@ const app = createApp();
 app.use(Notify);
 ```
 
+### Function Call
+
+Vant provides some utility functions that can quickly evoke global `Notify` components.
+
+For example, calling the `showNotify` function will render a Dialog directly in the page.
+
+```js
+import { showNotify } from 'vant';
+
+showNotify('Notify Message');
+```
+
 ## Usage
 
 ### Basic Usage
 
 ```js
-Notify('Notify Message');
+import { showNotify, hideNotify } from 'vant';
+
+// auto close after 3s
+showNotify('Message');
+
+// manually close
+hideNotify();
 ```
 
 ### Notify Type
 
 ```js
-Notify({ type: 'primary', message: 'Notify Message' });
-Notify({ type: 'success', message: 'Notify Message' });
-Notify({ type: 'danger', message: 'Notify Message' });
-Notify({ type: 'warning', message: 'Notify Message' });
+import { showNotify } from 'vant';
+
+showNotify({ type: 'primary', message: 'Notify Message' });
+showNotify({ type: 'success', message: 'Notify Message' });
+showNotify({ type: 'danger', message: 'Notify Message' });
+showNotify({ type: 'warning', message: 'Notify Message' });
 ```
 
 ### Custom Notify
 
 ```js
-Notify({
+import { showNotify } from 'vant';
+
+showNotify({
   message: 'Custom Color',
   color: '#ad0000',
   background: '#ffe1e1',
 });
 
-Notify({
+showNotify({
   message: 'Custom Position',
   position: 'bottom',
 });
 
-Notify({
+showNotify({
   message: 'Custom Duration',
   duration: 1000,
 });
 ```
 
-### Global Method
-
-After registering the Notify component through `app.use`, the `$notify` method will be automatically mounted on all subcomponents of the app.
-
-```js
-export default {
-  mounted() {
-    this.$notify('Notify Message');
-  },
-};
-```
-
 ### Component Call
 
 ```html
@@ -103,10 +113,10 @@ export default {
 
 | Methods | Attribute | Return value | Description |
 | --- | --- | --- | --- |
-| Notify | `options \| message` | notify instance | Show notify |
-| Notify.clear | - | `void` | Close notify |
-| Notify.setDefaultOptions | `options` | `void` | Set default options of all notifies |
-| Notify.resetDefaultOptions | - | `void` | Reset default options of all notifies |
+| showNotify | `options \| message` | notify instance | Show notify |
+| hideNotify | - | `void` | Close notify |
+| setNotifyDefaultOptions | `options` | `void` | Set default options of all notifies |
+| resetNotifyDefaultOptions | - | `void` | Reset default options of all notifies |
 
 ### Options
 
diff --git a/packages/vant/src/notify/README.zh-CN.md b/packages/vant/src/notify/README.zh-CN.md
index 11fb4c6cb..30f148f7b 100644
--- a/packages/vant/src/notify/README.zh-CN.md
+++ b/packages/vant/src/notify/README.zh-CN.md
@@ -2,51 +2,30 @@
 
 ### 介绍
 
-在页面顶部展示消息提示,支持函数调用和组件调用两种方式。
+在页面顶部展示消息提示,支持组件调用和函数调用两种方式。
 
-### 函数调用
+### 引入
 
-Notify 是一个函数,调用后会直接在页面中弹出相应的消息提示。
-
-```js
-import { Notify } from 'vant';
-
-Notify('通知内容');
-```
-
-### 组件调用
-
-通过组件调用 Notify 时,可以通过下面的方式进行注册:
+通过以下方式来全局注册组件,更多注册方式请参考[组件注册](#/zh-CN/advanced-usage#zu-jian-zhu-ce)。
 
 ```js
 import { createApp } from 'vue';
 import { Notify } from 'vant';
 
-// 全局注册
 const app = createApp();
 app.use(Notify);
-
-// 局部注册
-export default {
-  components: {
-    [Notify.Component.name]: Notify.Component,
-  },
-};
 ```
 
-在 `script setup` 中,可以通过以下方式使用:
+### 函数调用
 
-```html
-<script setup>
-  const VanNotify = Notify.Component;
-</script>
+为了便于使用 `Notify`,Vant 提供了一系列辅助函数,通过辅助函数可以快速唤起全局的消息提示。
 
-<template>
-  <!-- 中划线命名 -->
-  <van-notify />
-  <!-- 也支持大驼峰命名 -->
-  <VanNotify>
-</template>
+比如使用 `showNotify` 函数,调用后会直接在页面中渲染对应的提示。
+
+```js
+import { showNotify } from 'vant';
+
+showNotify({ message: '提示' });
 ```
 
 ## 代码演示
@@ -54,7 +33,13 @@ export default {
 ### 基础用法
 
 ```js
-Notify('通知内容');
+import { showNotify, hideNotify } from 'vant';
+
+// 3 秒后自动关闭
+showNotify('通知内容');
+
+// 主动关闭
+hideNotify();
 ```
 
 ### 通知类型
@@ -62,17 +47,19 @@ Notify('通知内容');
 支持 `primary`、`success`、`warning`、`danger` 四种通知类型,默认为 `danger`。
 
 ```js
+import { showNotify } from 'vant';
+
 // 主要通知
-Notify({ type: 'primary', message: '通知内容' });
+showNotify({ type: 'primary', message: '通知内容' });
 
 // 成功通知
-Notify({ type: 'success', message: '通知内容' });
+showNotify({ type: 'success', message: '通知内容' });
 
 // 危险通知
-Notify({ type: 'danger', message: '通知内容' });
+showNotify({ type: 'danger', message: '通知内容' });
 
 // 警告通知
-Notify({ type: 'warning', message: '通知内容' });
+showNotify({ type: 'warning', message: '通知内容' });
 ```
 
 ### 自定义通知
@@ -80,37 +67,25 @@ Notify({ type: 'warning', message: '通知内容' });
 自定义消息通知的颜色、位置和展示时长。
 
 ```js
-Notify({
+import { showNotify } from 'vant';
+
+showNotify({
   message: '自定义颜色',
   color: '#ad0000',
   background: '#ffe1e1',
 });
 
-Notify({
+showNotify({
   message: '自定义位置',
   position: 'bottom',
 });
 
-Notify({
+showNotify({
   message: '自定义时长',
   duration: 1000,
 });
 ```
 
-### 全局方法
-
-通过 `app.use` 全局注册 Notify 组件后,会自动在 app 的所有子组件上挂载 `$notify` 方法,便于在组件内调用。
-
-```js
-export default {
-  mounted() {
-    this.$notify('提示文案');
-  },
-};
-```
-
-> Tips: 由于 setup 选项中无法访问 this,因此不能使用上述方式,请通过 import 引入。
-
 ### 组件调用
 
 如果需要在 Notify 内嵌入组件或其他自定义内容,可以使用组件调用的方式。
@@ -151,10 +126,10 @@ export default {
 
 | 方法名 | 说明 | 参数 | 返回值 |
 | --- | --- | --- | --- |
-| Notify | 展示提示 | `options \| message` | notify 实例 |
-| Notify.clear | 关闭提示 | - | `void` |
-| Notify.setDefaultOptions | 修改默认配置,对所有 Notify 生效 | `options` | `void` |
-| Notify.resetDefaultOptions | 重置默认配置,对所有 Notify 生效 | - | `void` |
+| showNotify | 展示提示 | `options \| message` | notify 实例 |
+| hideNotify | 关闭提示 | - | `void` |
+| setNotifyDefaultOptions | 修改默认配置,影响所有的 `showNotify` 调用 | `options` | `void` |
+| resetNotifyDefaultOptions | 重置默认配置,影响所有的 `showNotify` 调用 | - | `void` |
 
 ### Options
 
diff --git a/packages/vant/src/notify/demo/index.vue b/packages/vant/src/notify/demo/index.vue
index 449fcba48..95ef3ee43 100644
--- a/packages/vant/src/notify/demo/index.vue
+++ b/packages/vant/src/notify/demo/index.vue
@@ -2,11 +2,9 @@
 import VanCell from '../../cell';
 import VanIcon from '../../icon';
 import { ref } from 'vue';
-import { Notify, type NotifyType } from '..';
+import { showNotify, Notify as VanNotify, type NotifyType } from '..';
 import { useTranslate } from '../../../docs/site';
 
-const VanNotify = Notify.Component;
-
 const t = useTranslate({
   'zh-CN': {
     primary: '主要通知',
@@ -38,12 +36,12 @@ const t = useTranslate({
 
 const show = ref(false);
 
-const showNotify = () => {
-  Notify(t('content'));
+const showBasicNotify = () => {
+  showNotify(t('content'));
 };
 
 const showCustomColor = () => {
-  Notify({
+  showNotify({
     color: '#ad0000',
     message: t('customColor'),
     background: '#ffe1e1',
@@ -51,21 +49,21 @@ const showCustomColor = () => {
 };
 
 const showCustomDuration = () => {
-  Notify({
+  showNotify({
     message: t('customDuration'),
     duration: 1000,
   });
 };
 
 const showCustomPosition = () => {
-  Notify({
+  showNotify({
     message: t('customPosition'),
     position: 'bottom',
   });
 };
 
 const showType = (type: NotifyType) => {
-  Notify({
+  showNotify({
     message: t('content'),
     type,
   });
@@ -81,7 +79,7 @@ const showComponentCall = () => {
 
 <template>
   <demo-block card :title="t('basicUsage')">
-    <van-cell is-link :title="t('basicUsage')" @click="showNotify" />
+    <van-cell is-link :title="t('basicUsage')" @click="showBasicNotify" />
   </demo-block>
 
   <demo-block card :title="t('notifyType')">
diff --git a/packages/vant/src/notify/function-call.tsx b/packages/vant/src/notify/function-call.tsx
index eac843c32..0fc8599af 100644
--- a/packages/vant/src/notify/function-call.tsx
+++ b/packages/vant/src/notify/function-call.tsx
@@ -1,11 +1,4 @@
-import { App } from 'vue';
-import {
-  extend,
-  isObject,
-  inBrowser,
-  withInstall,
-  type ComponentInstance,
-} from '../utils';
+import { extend, isObject, inBrowser, type ComponentInstance } from '../utils';
 import { mountComponent, usePopupState } from '../utils/mount-component';
 import VanNotify from './Notify';
 import type { NotifyMessage, NotifyOptions } from './types';
@@ -25,27 +18,6 @@ function initInstance() {
   }));
 }
 
-function Notify(options: NotifyMessage | NotifyOptions) {
-  if (!inBrowser) {
-    return;
-  }
-
-  if (!instance) {
-    initInstance();
-  }
-
-  options = extend({}, Notify.currentOptions, parseOptions(options));
-
-  instance.open(options);
-  clearTimeout(timer);
-
-  if (options.duration! > 0) {
-    timer = window.setTimeout(Notify.clear, options.duration);
-  }
-
-  return instance;
-}
-
 const getDefaultOptions = (): NotifyOptions => ({
   type: 'danger',
   color: undefined,
@@ -60,27 +32,38 @@ const getDefaultOptions = (): NotifyOptions => ({
   background: undefined,
 });
 
-Notify.clear = () => {
+let currentOptions = getDefaultOptions();
+
+export const hideNotify = () => {
   if (instance) {
     instance.toggle(false);
   }
 };
 
-Notify.currentOptions = getDefaultOptions();
+export function showNotify(options: NotifyMessage | NotifyOptions) {
+  if (!inBrowser) {
+    return;
+  }
 
-Notify.setDefaultOptions = (options: NotifyOptions) => {
-  extend(Notify.currentOptions, options);
+  if (!instance) {
+    initInstance();
+  }
+
+  options = extend({}, currentOptions, parseOptions(options));
+
+  instance.open(options);
+  clearTimeout(timer);
+
+  if (options.duration! > 0) {
+    timer = window.setTimeout(hideNotify, options.duration);
+  }
+
+  return instance;
+}
+
+export const setNotifyDefaultOptions = (options: NotifyOptions) =>
+  extend(currentOptions, options);
+
+export const resetNotifyDefaultOptions = () => {
+  currentOptions = getDefaultOptions();
 };
-
-Notify.resetDefaultOptions = () => {
-  Notify.currentOptions = getDefaultOptions();
-};
-
-Notify.Component = withInstall(VanNotify);
-
-Notify.install = (app: App) => {
-  app.use(Notify.Component);
-  app.config.globalProperties.$notify = Notify;
-};
-
-export { Notify };
diff --git a/packages/vant/src/notify/index.ts b/packages/vant/src/notify/index.ts
index 973c8839f..983ee5cf4 100644
--- a/packages/vant/src/notify/index.ts
+++ b/packages/vant/src/notify/index.ts
@@ -1,7 +1,15 @@
-import { Notify } from './function-call';
+import { withInstall } from '../utils';
+import _Notify from './Notify';
 
+export const Notify = withInstall(_Notify);
 export default Notify;
-export { Notify };
+export {
+  showNotify,
+  hideNotify,
+  setNotifyDefaultOptions,
+  resetNotifyDefaultOptions,
+} from './function-call';
+
 export type { NotifyProps } from './Notify';
 export type { NotifyType, NotifyOptions } from './types';
 
diff --git a/packages/vant/src/notify/test/index.spec.ts b/packages/vant/src/notify/test/index.spec.ts
index 369d54f3e..afe657ee5 100644
--- a/packages/vant/src/notify/test/index.spec.ts
+++ b/packages/vant/src/notify/test/index.spec.ts
@@ -1,20 +1,23 @@
-import { createApp } from 'vue';
 import { later } from '../../../test';
-import { Notify } from '../function-call';
-import NotifyComponent from '../Notify';
+import {
+  showNotify,
+  hideNotify,
+  setNotifyDefaultOptions,
+  resetNotifyDefaultOptions,
+} from '../function-call';
 
 test('should not throw error if calling clear method before render notify', () => {
-  Notify.clear();
+  hideNotify();
 });
 
 test('should render Notify correctly', async () => {
-  Notify('test');
+  showNotify('test');
   await later();
   expect(document.querySelector('.van-notify')).toMatchSnapshot();
 });
 
 test('should add "van-notify--success" class when type is success', async () => {
-  Notify({
+  showNotify({
     message: 'test',
     type: 'success',
   });
@@ -24,28 +27,23 @@ test('should add "van-notify--success" class when type is success', async () =>
   expect(notify.classList.contains('van-notify--success')).toBeTruthy();
 });
 
-test('should register component to app', () => {
-  const app = createApp({});
-  app.use(Notify);
-  expect(app.component(NotifyComponent.name)).toBeTruthy();
-});
+test('should change default options after calling setDefaultOptions method', async () => {
+  setNotifyDefaultOptions({ message: 'foo' });
+  showNotify({});
+  await later();
+  const notify = document.querySelector('.van-notify') as HTMLElement;
+  expect(notify.innerHTML.includes('foo')).toBeTruthy();
 
-test('should change default duration after calling setDefaultOptions method', () => {
-  Notify.setDefaultOptions({ duration: 1000 });
-  expect(Notify.currentOptions.duration).toEqual(1000);
-  Notify.resetDefaultOptions();
-  expect(Notify.currentOptions.duration).toEqual(3000);
-});
-
-test('should reset to default duration after calling resetDefaultOptions method', () => {
-  Notify.setDefaultOptions({ duration: 1000 });
-  Notify.resetDefaultOptions();
-  expect(Notify.currentOptions.duration).toEqual(3000);
+  resetNotifyDefaultOptions();
+  showNotify({});
+  await later();
+  const notify2 = document.querySelector('.van-notify') as HTMLElement;
+  expect(notify2.innerHTML.includes('foo')).toBeFalsy();
 });
 
 test('should call onClose option when closing', async () => {
   const onClose = jest.fn();
-  Notify({
+  showNotify({
     message: 'test',
     onClose,
     duration: 1,
@@ -57,7 +55,7 @@ test('should call onClose option when closing', async () => {
 
 test('should call onClick option when clicked', async () => {
   const onClick = jest.fn();
-  Notify({
+  showNotify({
     message: 'test',
     onClick,
   });
@@ -69,7 +67,7 @@ test('should call onClick option when clicked', async () => {
 });
 
 test('should align to bottom when position option is bottom', async () => {
-  Notify({
+  showNotify({
     message: 'test',
     position: 'bottom',
   });
diff --git a/packages/vant/src/notify/types.ts b/packages/vant/src/notify/types.ts
index 8dfbd5bb5..cb4e6373a 100644
--- a/packages/vant/src/notify/types.ts
+++ b/packages/vant/src/notify/types.ts
@@ -1,4 +1,3 @@
-import { Notify } from './function-call';
 import type { Numeric } from '../utils';
 
 export type NotifyMessage = Numeric;
@@ -20,9 +19,3 @@ export type NotifyOptions = {
   onClose?: () => void;
   onOpened?: () => void;
 };
-
-declare module '@vue/runtime-core' {
-  interface ComponentCustomProperties {
-    $notify: typeof Notify;
-  }
-}