一、Ant Design Pro 与 Amis 结合

Amis

amis 是一个低代码前端框架,它使用 JSON 配置来生成页面,可以减少页面开发工作量,极大提升效率。amis 可以高效的实现数据的增删改查CRUD,对于大部分简单的数据表,我们能够不用写代码自然是最好的。它最大的优势是,不需要代码就可以实现许多我们需要实现的功能。具体见:https://aisuda.bce.baidu.com/amis/zh-CN/docs/index

做这个项目的初衷就是要用Amis实现简单的操作,用Ant design 组件实现更加自由的操作。前面我们已经用Ant design pro 实现了基础框架。现在是时候把Amis集成进来了。

Amis 安装

因为amis现在更新还非常频繁,为了能够确保我们安装的版本更加稳定一下,我选择了指定版本安装。

npm install amis@2.6.0 --save

安装成功之后,可以开始测试是不是能够把amis集成进来。为此我们需要增加一个测试页面。

新增页面

1、在 src / pages 下创建一个新的文件夹mytest,并且在改文件夹里面添加index.tsx 文件。

config
src
  models
  pages
+   mytest
    + index.tsx
  ...
...
package.json

2、页面代码如下

const testPage: React.FC = () => {
  return <div>my test page</div>;
};
export default testPage;

3、添加新路由
在config/config.ts里面的routes,添加新的路由。

{
      path: '/mytest',
      name: 'mytest',
      icon: 'smile',
      component: './mytest',
},

在这里插入图片描述
4、热更新之后,我们可以看到如下画面
在这里插入图片描述
5、参考amis官网,在新页面里面提交amis代码,实现用json代码展现功能页面,只是为了测试,我们随意加载amis官方一段代码,修改后的index.tsx代码如下:

import axios from 'axios';
import copy from 'copy-to-clipboard';
import 'amis/lib/themes/cxd.css';

import { render as renderAmis } from 'amis';
import { ToastComponent, AlertComponent, toast } from 'amis-ui';
const AmisComponent: React.FC = () => {
  const theme = 'cxd';
  const locale = 'zh-CN';
  // 请勿使用 React.StrictMode,目前还不支持
  return (
    <div>
      <p>通过 amis 渲染页面</p>
      <ToastComponent theme={theme} key="toast" position={'top-right'} locale={locale} />
      <AlertComponent theme={theme} key="alert" locale={locale} />
      {renderAmis(
        {
          type: 'page',
          body: {
            type: 'form',
            debug: true,
            api: '/amis/api/mock2/form/saveForm',
            body: [
              {
                name: 'city',
                type: 'input-city',
                label: '城市',
                searchable: true,
              },
            ],
          },
        },
        {
          // props...
          // locale: 'en-US' // 请参考「多语言」的文档
          // scopeRef: (ref: any) => (amisScoped = ref)  // 功能和前面 SDK 的 amisScoped 一样
        },
        {
          // 下面三个接口必须实现
          fetcher: ({
            url, // 接口地址
            method, // 请求方法 get、post、put、delete
            data, // 请求数据
            responseType,
            config, // 其他配置
            headers, // 请求头
          }: any) => {
            config = config || {};
            config.withCredentials = true;
            responseType && (config.responseType = responseType);

            if (config.cancelExecutor) {
              config.cancelToken = new (axios as any).CancelToken(config.cancelExecutor);
            }

            config.headers = headers || {};

            if (method !== 'post' && method !== 'put' && method !== 'patch') {
              if (data) {
                config.params = data;
              }

              return (axios as any)[method](url, config);
            } else if (data && data instanceof FormData) {
              config.headers = config.headers || {};
              config.headers['Content-Type'] = 'multipart/form-data';
            } else if (
              data &&
              typeof data !== 'string' &&
              !(data instanceof Blob) &&
              !(data instanceof ArrayBuffer)
            ) {
              data = JSON.stringify(data);
              config.headers = config.headers || {};
              config.headers['Content-Type'] = 'application/json';
            }

            return (axios as any)[method](url, data, config);
          },
          isCancel: (value: any) => (axios as any).isCancel(value),
          copy: (content) => {
            copy(content);
            toast.success('内容已复制到粘贴板');
          },
          theme,

          // 后面这些接口可以不用实现

          // 默认是地址跳转
          // jumpTo: (
          //   location: string /*目标地址*/,
          //   action: any /* action对象*/
          // ) => {
          //   // 用来实现页面跳转, actionType:link、url 都会进来。
          // },

          // updateLocation: (
          //   location: string /*目标地址*/,
          //   replace: boolean /*是replace,还是push?*/
          // ) => {
          //   // 地址替换,跟 jumpTo 类似
          // },

          // isCurrentUrl: (
          //   url: string /*url地址*/,
          // ) => {
          //   // 用来判断是否目标地址当前地址
          // },

          // notify: (
          //   type: 'error' | 'success' /**/,
          //   msg: string /*提示内容*/
          // ) => {
          //   toast[type]
          //     ? toast[type](msg, type === 'error' ? '系统错误' : '系统消息')
          //     : console.warn('[Notify]', type, msg);
          // },
          // alert,
          // confirm,
          // tracker: (eventTracke) => {}
        },
      )}
    </div>
  );
};
export default AmisComponent;

其中,特别提醒,要导入css样式,否则的话呈现出来的界面会很混乱。代码如下:

import 'amis/lib/themes/cxd.css';

这行代码在amis的官网里面是没有的,这个坑我踩过。考虑到后面有可能会直接修改amis的样式,建议把依赖库里面的css文件copy到代码文件夹里面,这样可以不受amis升级影响,避免直接修改amis样式的时候,会被amis升级信息覆盖掉。

config
src
  models
  pages
+   mytest
    + index.tsx
    + cdx.css
  ...
...
package.json

index.ts的页面代码,修改如下:

import './cxd.css';

以上仅供参考,也可以考虑采用amis官网上面关于在amis中自定义样式的其它方式。

关于amis样式

amis官网上面关于amis自定义样式有四种方式
1、使用 CSS 变量动态修改,通过这种方式修改大部分 amis 组件的样式,所有组件都会生效,注意这种方法不支持 IE11。
2、使用辅助 class,可以对单个组件做定制修改。
3、自己生成主题 CSS,可以修改所有配置,目前只能通过源码方式,请参考 scss\themes\cxd.scss 文件,修改变量后重新编译一个 css,需要注意这种方式在更新 amis 版本的时候最好重新编译,否则就会出现使用旧版 css 的情况,可能导致出错,因此不推荐使用。
4、wrapper 组件可以直接写内嵌 style。

另外amis官网提供的主题编辑器,点击链接可以直接进入。通过主题编辑器,可以比较直观操作。

最终呈现界面

在这里插入图片描述

自由的左边栏菜单,本地Json保存菜单数据

因为我们使用amis,我们可以大胆的考虑我们日后可能实现整个系统交给不懂编程的人去维护甚至修改。那么我们对左边栏的菜单项目就要求更加自由的控制,如果这个菜单需要修改,我们不能够重新编译系统,我们要实现简单的修改某个json文档就可以了。
那么我们就需要修改左边栏菜单的工作方式。

从服务器加载 menu

先看看ProComponents官网上的说法

ProLayout 提供了强大的菜单功能,但是这样必然会封装很多行为,导致需要一些特殊逻辑的用户感到不满。所以我们提供了很多的 API,期望可以满足绝大部分客户的方式。
从服务器加载 menu 主要使用的 API 是 menuDataRender 和 menuRender,menuDataRender可以控制当前的菜单数据,menuRender可以控制菜单的 dom 节点。

根据官网所说,接下来我们可以这样修改
1、在src/service里面添加menu子目录,再添加customMenu.ts用来返回自定义菜单内容

config
src
  models
  service
  + menu
    + customMenu.ts
  ...
...
package.json

customenu.ts代码如下

export default [
  {
    path: '/',
    name: '欢迎',
    routes: [
      {
        path: '/welcome',
        name: 'one',
        routes: [
          {
            path: '/welcome/welcome',
            name: 'two',
            exact: true,
          },
        ],
      },
    ],
  },
  {
    path: '/demo',
    name: '例子',
  },
];

2、修改app.tsx里面的layout布局配置,增加menu:项目
增加相关导入和辅助函数

import customMenuDate from '@/services/menu/customMenu';
const waitTime = (time: number = 100) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(true);
    }, time);
  });
};

修改layout配置,增加menu项

export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => {
  return {
    rightContentRender: () => <RightContent />,
    disableContentMargin: false,
    waterMarkProps: {
      content: initialState?.currentUser?.name,
    },
    footerRender: () => <Footer />,
    onPageChange: () => {
      const { location } = history;
      // 如果没有登录,重定向到 login
      if (!initialState?.currentUser && location.pathname !== loginPath) {
        history.push(loginPath);
      }
    },
    menu: {
      request: async () => {
        await waitTime(2000);
        return customMenuDate;
      },
    },
    //.......后续代码略

保存热更新后
在这里插入图片描述

菜单项保存在本地Json

从服务端加载menu数据还是不够完美,我们还需要做一个菜单管理模块,实现不懂编程的人来修改菜单。但是考虑日后维护本系统的人还是需要学习amis的,如果我们把菜单数据保存在前端的json文件,只需要简单的说明,维护人员就可以轻松的修改json文档。
那么这就需要本项目可以读取本地json文档
1、json文档保存在项目哪里
因为项目的public文件夹是会被当做静态资源copy到编译后的目标文件夹里面的,我们的菜单json就是一个静态资源,放着public文件夹里面最合适,我们在public里面创建子文件夹json,并把菜单数据保存为menuData.json
2、读取本地json
为了能够实现本地json,我们根据上面第一点的设置,在src/services/ant-design-pro/api.ts里面添加函数getLocalJson,实现传递文件名就可以读取public/json里面的文件,并把读取文件以string的新式返回,代码如下:

/**读取本地Json文档 */
export async function getLocalJson(fileName: string) {
  return request<string>(`/json/${fileName}`, {
    method: 'get',
    headers: {
      'Content-Type': 'application/json',
    },
  });
}

3、菜单json文件示例

[
  {
    "path": "/",
    "name": "欢迎",
    "locale":false,
    "routes": [
      {
        "path": "/welcome",
        "name": "one",
        "routes": [
          {
            "path": "/welcome/welcome",
            "name": "two",
            "exact": true
          }
        ]
      }
    ]
  },
  {
    "path": "/demo",
    "locale":false,
    "name": "例子"
  }
]

进一步优化

1、读取本地json文件之后 我们只是简单的返回string类型,这个不利于获取到的数据进行进一步的操作,因为pro-layout的菜单采用的数据类型typing是已经定义好的,我们可以直接引用。
2、加载菜单文件的时候,应该考虑当前用户选择的语言,然后根据用户当前的语言来加载不同的菜单文件,为了实现此功能,我们的函数参数应该直接修改成用户当前的语言,然后我们本地文档名称也应该直接用用户locale的key作为文件名。为此,我们public/json下面再建一个menu子文件夹,把原来的menuData.json改成zh-CN.json并移动到menu子文件夹。
在src/services/ant-design-pro/api.ts里面添加函数getLocalMenu,代码如下:

import { MenuDataItem } from '@ant-design/pro-layout';
/**根据locale的key读取菜单的Json文档 */
export async function getLocalMenu(locale: string) {
  return request<MenuDataItem[]>(`/json/menu/${locale}.json`, {
    method: 'get',
    headers: {
      'Content-Type': 'application/json',
    },
  });
}

修改app.tsx

修改app.tsx我们前面添加的menu项。
1、因为我们需要自己根据当前用户选择的语言加载不同的菜单文件,我们不需要框架自己到源代码src/locales里面去匹配,所以我们设置locale选项为false。
2、修改menu里面的request

/// src/app.tsx
import { getLocalJson, getLocalMenu } from './services/ant-design-pro/api';
import { getLocale } from 'umi';
export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => {
  return {
    rightContentRender: () => <RightContent />,
    disableContentMargin: false,
    waterMarkProps: {
      content: initialState?.currentUser?.name,
    },
    footerRender: () => <Footer />,
    onPageChange: () => {
      const { location } = history;
      // 如果没有登录,重定向到 login
      if (!initialState?.currentUser && location.pathname !== loginPath) {
        history.push(loginPath);
      }
    },
    menu: {
      locale: false,//我们自己匹配语言
      request: async () => {
         //通过getLocale()获取当前用户选择的语言,据此决定加载的菜单文件
        const res = await getLocalMenu(getLocale());        
        return res;
      },
    },
    //.......后续代码略

添加版本号

大部分浏览器,比如Chrome都会对用户加载的文件采用缓存在本地的机制,这样我们在日后如果需要更新我们的菜单,那就有可能出现我们已经更新好了,但是客户端看起来还是原来的样子。为了避免日后更新菜单json的时候,浏览器由于文件缓存的原因没有自动刷新,我们需要在加载本地json文档的时候添加版本号,这样浏览器会比对不同版本号,从而判断出是否是新的文档。
1、public\json里面添加ver.json并且设置内容为 1.0.0.1
2、修改api.ts,添加获取版本号的函数

/**获取当前版本号 */
export async function getCurVer() {
  //ver.json后面跟一个v=v=/${new Date().getTime()}确保不会被缓存
  return request<string>(`/json/ver.json?v=${new Date().getTime()}`, {
    method: 'get',
    headers: {
      'Content-Type': 'application/json',
    },
  });
}

/**根据locale的key读取菜单的Json文档 */
export async function getLocalMenu(locale: string, ver: string) {
  return request<MenuDataItem[]>(`/json/menu/${locale}.json?v=${ver}`, {
    method: 'get',
    headers: {
      'Content-Type': 'application/json',
    },
  });
}

3、修改app.tsx里面getInitialState部分,增加初始化的时候获取当前版本号的功能,代码如下

/// src/app.tsx
import { getCurVer } from './services/ant-design-pro/api';
export async function getInitialState(): Promise<{
  ver?: string;//新增版本号项目
  settings?: Partial<LayoutSettings>;
  currentUser?: API.CurrentUser;
  loading?: boolean;
  fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
}> {
  const fetchUserInfo = async () => {
    try {
      const msg = await queryCurrentUser();
      return msg.data;
    } catch (error) {
      history.push(loginPath);
    }
    return undefined;
  };
  const curVer = await getCurVer();//读取版本号
  // 如果不是登录页面,执行
  if (history.location.pathname !== loginPath) {
    const currentUser = await fetchUserInfo();
    return {
      ver: curVer,
      fetchUserInfo,
      currentUser,
      settings: defaultSettings,
    };
  }
  return {
    fetchUserInfo,
    settings: defaultSettings,
  };
 }
 // ......其它代码忽略
 // 获取菜单数据的部分代码
 menu: {
      locale: false, //我们自己匹配语言
      request: async () => {        
        const ver = initialState?.ver ? initialState.ver : ' ';
        const res = await getLocalMenu(getLocale(), ver);        
        return res;
      },
    },
// ......其它代码忽略

菜单图标

至此我们已经基本实现自由的左边栏菜单功能了,还差了一个小东西,就是菜单项的图标,因为menu的request返回的只是图标的string类型的名称,我们还需要根据名称进行对应组件的渲染,因此还需要一下小的动作。
1、因为代码开始有点多了,不再时候直接写在app.ts里面,我们开始设计一个components,在src/compontents里面添加LocalMenu子文件夹,并创建index.tsx
2、开始代码
需要导入 @ant-design/icons,把string和相关组件导入

import { getLocalMenu } from '@/services/ant-design-pro/api';
import { getLocale } from 'umi';
import { HeartOutlined, SmileOutlined } from '@ant-design/icons';
import type { MenuDataItem } from '@ant-design/pro-layout';
const IconMap = {
  smile: <SmileOutlined />,
  heart: <HeartOutlined />,
};
const loopMenuItem = (menus: any[]): MenuDataItem[] =>
  menus.map(({ icon, routes, ...item }) => ({
    ...item,
    icon: icon && IconMap[icon as string],
    children: routes && loopMenuItem(routes),
  }));
export default async (ver: string) => {
  const res = await getLocalMenu(getLocale(), ver);
  return loopMenuItem(res);
};

3、修改app.tsx

import LocalMenu from './components/LocalMenu';
 menu: {
      locale: false, //我们自己匹配语言
      request: async () => {        
        const ver = initialState?.ver ? initialState.ver : ' ';
        const res = await LocalMenu(ver);
        return res;
      },
    },

4、修改public/json/menu/zh-CN.json

 [{
    "path": "/demo",
    "locale":false,
    "icon": "smile",
    "name": "例子"
  }]

最后呈现
在这里插入图片描述

umi request 的简单修改

design pro 的api访问采用request,在本项目的/src/service/ant-design-pro里面有访问mock模拟api的代码,由于我们的是要做前后端分离的,后端的api和前端不一定放在同一个服务器。而我们前面对菜单的改造是希望我们可以访问本地的json文件,这样我们就需要把这两种访问方式区分开来,并且amis的schema文档内部也有需要访问后端api的时候,为此我们需要简单的封装一下umi 的request。当然后面我们如果有需要,还可以进一步对请求进行拦截。对实例request进行请求拦截(interceptors)。
在src/utils/文件夹里面添加request.ts,代码如下

/* eslint-disable */
import { extend } from 'umi-request';
//api 网址前缀,为了方便日后更换服务器,直接用一个常量定义
const urlPrefix = '';//http://localhost:1898';//开发的时候如果都是本地服务器,把这个常量设置为空字符就可以
const remoteRequest = extend({
  // 路径前缀(基础路径)
  prefix: urlPrefix,
  timeout: 5000,
});
/**
 * 读取本地文件
 */
export const localRequest = extend({
  prefix: '',
  timeout: 5000,
});

/*
 * amis的schema文件专用request
 */
export async function amisRequest(url: string, method?: string, options?: { [key: string]: any }) {
  let newMethod = '';
  if (!method) newMethod = 'GET';
  else newMethod = method.toUpperCase();
  switch (newMethod) {
    case 'GET':
      return remoteRequest.get(url, options);
    case 'POST':
      return remoteRequest.post(url, options);
    case 'DELETE':
      return remoteRequest.delete(url, options);
    case 'PUT':
      return remoteRequest.put(url, options);
  }

}
export default remoteRequest;

我们封装了request,项目里面需要访问api的地方都需要改代码,目前有两个地方有Api访问,一个是service里面,一个是amis 的schema里面,这里我们先解决service里面,关于amis访问api见我们下一篇文章设计amis组件。

修改service里面的api.ts

这比较简单,import头改一下,读取本地json的地方改一下。代码分别如下:

//import { request } from 'umi';
import request, { localRequest } from '@/utils/request';

读取本地json的地方,全部改成用localRequest就可以了

/**读取本地Json文档 */
export async function getLocalSchema(fileName: string, ver?: string) {
  return localRequest<Schema>(`/json/${fileName}.json?v=${ver}`, {
    method: 'get',
    headers: {
      'Content-Type': 'application/json',
    },
  });
}
import { MenuDataItem } from '@ant-design/pro-layout';
import { Schema } from 'amis';

/**根据locale的key读取菜单的Json文档 */
export async function getLocalMenu(locale: string, ver: string) {
  return localRequest<MenuDataItem[]>(`/json/menu/${locale}.json?v=${ver}`, {
    method: 'get',
    headers: {
      'Content-Type': 'application/json',
    },
  });
}
/**获取当前版本号 */
export async function getCurVer() {
  return localRequest<string>(`/json/ver.json?v=/${new Date().getTime()}`, {
    method: 'get',
    headers: {
      'Content-Type': 'application/json',
    },
  });
}

设计React里面使用的Amis组件

创建amis组件

我们前面已经安装好了amis,并且copy官方Demo代码生成了一个react中使用amis的页面。但是这样是不方便的,我们每一个amis schema都需要重复做这样的页面是不合适的。考虑我们可以读取本地json文档,应该设计一个amis组件,这个组件可以获取当前url的path最后一层为文件名,根据文件名加载本地json文档做为组件的schema。
这样做的好处是,运营的时候可以交给不懂编程的运营人员直接修改,修改后的json格式schema上传后就可以了。这样,程序的修改,不需要重新编译程序。

Hook 方式的挂载事件

创建组件,我们采用函数方式:

const AmisComponent: React.FC = () => {
}

这种方式,因为没有采用class继承,没有this,没有生命周期函数比如:componentDidMount(),componentDidUpdate(),componentWillUnmount()等等。取而代之的是useEffect和useState。

useState

在使用class的时候,组件的数据通过this.state = {},来实现,但是在hook中我们采用的方式就是使用useState这个hook。示例如下:

import { useEffect, useState } from 'react';
const [count, setValue] = useState<number>(0);
setValue(3);

这个代码翻译成class方式如下:

this.state = {count:0};
this.setState({count:3});

useEffect

useEffect的原型如下:

function useEffect(effect: EffectCallback, deps?: DependencyList): void;

总共两个参数,一个是Effect影响函数,一个是依赖列表。影响函数好理解,就是我们希望对本组件进行影响的操作。
依赖列表一个数组,一个变量名称组成的数组,是触发影响函数的依赖变量。影响函数外面的任何可能改变函数运行结果的变量都可以写入依赖列表里面去。例如:

  //代码A
  let a=0,b=0,c=0;
  useEffect(() => {
    console.log(a,b,c);
    },[])

上面这段代码里面,a,b,c三个变量都可能改变函数的运行,所以都可以写到依赖列表里面去:

  //代码B
  let a=0,b=0,c=0;
  useEffect(() => {
    console.log(a,b,c);
    },[a,b,c])

上面两段代码A和B,由于A里面的代码依赖列表是空白的,所以useEffect里面的影响函数指执行一次。代码B里面的依赖列表是a,b,c只要a,b,c有变化,影响函数就会执行。

useEffect与class时代的生命周期

基于上面对useEffect的理解,我们可以整理出它和class时代的生命周期函数的对应关系:
1、componentDidMount() 对应如下代码:

useEffect(()=>{},[]);//组件渲染后,执行一次。因为依赖列表里面是空白的

2、componentWillUnmount(),用hook来代替,需要去return一个callback(回调函数),如下面的形式所示:

 useEffect(() => {
    return () => {
      //todo:clear something
    };
  }, []);

3、componentDidUpdate(),用一个特殊变量的去触发hook,如下面所示,count指的就是这个特殊的变量,该hook触发,只会是count的值改变时,这个也相当vue里面的watch,可以监控某个变量的变化:

useEffect(() => { },[count])

组件加载json文件

有了以上的知识基础,我们可以开始实现我们的功能。由于我们设计组件的初衷就要组件可以根据location中的path来自动加载,对应的json,我们需要解析location的最后一层,并且初始化myschema(用来保存json内容的变量)变量,由于我们需要把该变量丢给render渲染函数,根据amis里面schema类型的定义,我们发现有一个type属性是非空的,所有我们useState的时候,初始变量必须是{type:‘page’},实在懒得写,也可以给一个{}空白对象作为初始值,否则初次渲染的时候就会报错:TypeError: Cannot read property ‘mobile’ of undefined。这个是amis渲染器没有undefined判断的原因。

 //获取path路径中的json文件名称
  const location = useLocation();
  const schemaFile = location.pathname.substring(location.pathname.lastIndexOf('/') + 1);
  //初始化Schema
  const [myschema, setValue] = useState<Schema>({ type: 'page', title: '', body: '' });

加载schema文件时,我们采用umi 的request库,该库返回的是一个Promise,在useEffect也可以用.then来实现赋值,但是这种很破坏结构新的回调函数方式我比较不喜欢。通常解决办法是用async/await,由于我们采用函数式,React.FC如果前面加async,会有类型错误的提醒(typescript问题),为了确保函数类型一致,我们采用立即执行函数IIFE。因此useEffect被设计成这样:

useEffect(() => {   
    (async () => {
      const schema = await getLocalSchema(schemaFile, initialState?.ver);
      if (isMounted) setValue(schema);
    })();//IIFE
  }, [schemaFile]);//我们希望组件会根据当前location里面的path的变化重新加载不同的json文件,所以这个地方的依赖列表不能空

提醒注意,由于我们需要在用户点击不同菜单项的时候加载不一样的json文件,所以我们需要监控schemaFile的变化,必须要把它列入依赖列表。

综上,组件完整代码如下:

/* eslint-disable react-hooks/exhaustive-deps */
// import axios from 'axios';
import copy from 'copy-to-clipboard';
import './cxd.css';
import { render as renderAmis } from 'amis';
import { ToastComponent, AlertComponent, toast } from 'amis-ui';
import { useLocation } from 'react-router';
import { useModel } from 'umi';
import { getLocalSchema } from '@/services/ant-design-pro/api';
import { useEffect, useState } from 'react';
import type { Schema } from 'amis';
import request, { amisRequest } from '@/utils/request';
import remoteRequest from '@/utils/request';
//import request, { localRequest } from '@/utils/request';
const AmisComponent: React.FC = () => {
  //获取path路径中的json文件名称
  const location = useLocation();
  const schemaFile = location.pathname.substring(location.pathname.lastIndexOf('/') + 1);
  //获取版本号
  const { initialState } = useModel('@@initialState');
  //初始化Schema
  const [myschema, setValue] = useState<Schema>({ type: 'page', title: '', body: '' });
  //读取本地json文件到mySchema变量
  useEffect(() => {
    //https://zhuanlan.zhihu.com/p/454841748
    //设置isMounted是为了解决react内存泄露警告问题
    let isMounted = true;
    (async () => {
      const schema = await getLocalSchema(schemaFile, initialState?.ver);
      if (isMounted) setValue(schema);
    })();
    return () => {
      isMounted = false;
    };
  }, [schemaFile]);
  const theme = 'cxd';
  const locale = 'zh-CN';
  // 请勿使用 React.StrictMode,目前还不支持
  return (
    <div>
      <ToastComponent theme={theme} key="toast" position={'top-right'} locale={locale} />
      <AlertComponent theme={theme} key="alert" locale={locale} />
      {
        renderAmis(
        myschema,//这个就是json文件内容 
        {//第二个参数可以为空白
        },    
        {
          // 下面三个接口必须实现
          fetcher: ({
            url, // 接口地址
            method, // 请求方法 get、post、put、delete
            data, // 请求数据
            responseType,
            config, // 其他配置
            headers, // 请求头
          }: any) => {
            // eslint-disable-next-line no-param-reassign
            config = config || {};
            config.withCredentials = true;
            // eslint-disable-next-line @typescript-eslint/no-unused-expressions
            responseType && (config.responseType = responseType);

            if (config.cancelExecutor) {
              request.CancelToken = config.cancelExecutor;
              //config.cancelToken = new (axios as any).CancelToken(config.cancelExecutor);
            }

            config.headers = headers || {};

            if (method !== 'post' && method !== 'put' && method !== 'patch') {
              if (data) {
                config.params = data;
              }

              return amisRequest(url, method, { ...data, ...config }); // (axios as any)[method](url, config);
            } else if (data && data instanceof FormData) {
              config.headers = config.headers || {};
              config.headers['Content-Type'] = 'multipart/form-data';
            } else if (
              data &&
              typeof data !== 'string' &&
              !(data instanceof Blob) &&
              !(data instanceof ArrayBuffer)
            ) {
              // eslint-disable-next-line no-param-reassign
              data = JSON.stringify(data);
              config.headers = config.headers || {};
              config.headers['Content-Type'] = 'application/json';
            }
            //return (axios as any)[method](url, data, config);
            return amisRequest(url, method, { ...data, ...config });
          },
          isCancel: (value: any) => remoteRequest.isCancel(value), //isCancel(value),
          copy: (content) => {
            copy(content);
            toast.success('内容已复制到粘贴板');
          },
          theme,

         
        },
      )}
    </div>
  );
};
export default AmisComponent;

修改路由

有了组件,我们可以添加页面了:src/pages里面添加amispage,并完成代码如下:

import AmisComponent from '@/components/Amis';
export default AmisComponent;

这个页面承载了我们的设计思想,就是根据不同的path加载不同的json文件,那么这个页面就会频繁的被调用,它在路由里面是需要设置参数的,这个参数就是我们的json文件名。在config/config.ts的路由数组里面添加如下代码

{
      path: '/amispage/:f',
      name: 'amispage',
      icon: 'smile',
      component: './amispage',
},

我们在前面从服务器加载菜单的时候,为了实现菜单的图标,有做过一个组件LocalMenu实现对菜单数据的遍历。我们可以改造一下,对菜单数据里面的每个菜单项增加一个指向amispage的前缀,这样运营人员设计菜单的json文档的时候,就可以在path里面直接书写文件名了。src/components/LocalMenu/index.tsx代码修改如下:

const loopMenuItem = (menus: any[]): MenuDataItem[] =>
  menus.map(({ icon, routes, filename, path, ...item }) => {
    let newPath = path;
    if (path) {
      newPath = `/amispage/${path}`;
    }
    return {
      ...item,
      path: newPath,
      icon: icon && IconMap[icon as string],
      children: routes && loopMenuItem(routes),
    };
  });

这样,我们的运营人员就可以自由的设计如下的菜单json,其中每个path都对应一个在public/json文件夹里面的json文件,比如path:index对应public/json/index.json,菜单json示例如下:

[
  {
    "path": "/",
    "name": "表的crud",
    "locale":false,
    "icon": "smile",  
    "routes": [      
      {
        "path": "index",
        "name": "crud基本操作"        
      },
      {
        "path": "crudNew",
        "name": "crud新增操作"
      },
      {
        "path": "crudDelete",
        "name": "crud删除操作"
      }      
    ]
  },
  {
    "path": "echart",
    "locale":false,
    "icon": "heart",
    "name": "其它示例"
  }
]

最终界面呈现如下:在这里插入图片描述

Amis 组件再分析

消息提醒

这是两个UI组件,用来对用户操作进行消息提示的。
ToastComponent组件是广播式的,是一种非阻塞式的提醒,会自动关闭。可以设置展示位置和持续时间。提示显示位置,可用’top-right’、‘top-center’、‘top-left’、‘bottom-center’、‘bottom-left’、‘bottom-right’、‘center’。持续时间默认5秒可以通过timeout修改。
AlertComponent 用来做文字特殊提示,分为四类:提示类、成功类、警告类和危险类。

renderAmis

组件的核心是renderAmis,最基础的用法如下:

renderAmis(schema, {
    data: {
      username: 'amis'
    }
  });

其中schema就是我们设计的json文件,data是用来数据给渲染器内部使用的,大多数时候是用不上的。这个看起来是非常简单的。但是如果我们的组件需要访问Api,那我们还需要实现几个重要的接口

fetcher

fetcher是接口请求器,实现该函数才可以实现 ajax 发送,函数签名如下:

(config: {
  url; // 接口地址
  method; // 请求方法 get、post、put、delete
  data; // 请求数据
  responseType;
  config; // 其他配置
  headers; // 请求头
}) => Promise<fetcherResult>;

amis默认用axios作为ajax库来实现api的请求,我们项目的主框架式ant design它采用umi的request。我们已经对他做了二次封装了,为了统一使用Ajax库,在amis中我们也应该用自己封装好了的库。
为了专门给amis的api请求使用,我们再已经封装好的src/utils/request.ts里面做一些改进,代码如下

/*
 * amis的schema文件专用request
 */
export async function amisRequest(url: string, method?: string, options?: { [key: string]: any }) {
  let newMethod = '';
  if (!method) newMethod = 'GET';
  else newMethod = method.toUpperCase();
  switch (newMethod) {
    case 'GET':
      return remoteRequest.get(url, options);
    case 'POST':
      return remoteRequest.post(url, options);
    case 'DELETE':
      return remoteRequest.delete(url, options);
    case 'PUT':
      return remoteRequest.put(url, options);
  }

这样在renderAmis中就可以直接使用

 fetcher: ({
            url, // 接口地址
            method, // 请求方法 get、post、put、delete
            data, // 请求数据
            responseType,
            config, // 其他配置
            headers, // 请求头
          }: any) => {
            // eslint-disable-next-line no-param-reassign
            config = config || {};
            config.withCredentials = true;
            // eslint-disable-next-line @typescript-eslint/no-unused-expressions
            responseType && (config.responseType = responseType);

            if (config.cancelExecutor) {
              //这个必须传递
              request.CancelToken = config.cancelExecutor;             
            }

            config.headers = headers || {};

            if (method !== 'post' && method !== 'put' && method !== 'patch') {
              if (data) {
                config.params = data;
              }
              return amisRequest(url, method, { ...data, ...config }); 
            } else if (data && data instanceof FormData) {
              config.headers = config.headers || {};
              config.headers['Content-Type'] = 'multipart/form-data';
            } else if (
              data &&
              typeof data !== 'string' &&
              !(data instanceof Blob) &&
              !(data instanceof ArrayBuffer)
            ) {
              // eslint-disable-next-line no-param-reassign
              data = JSON.stringify(data);
              config.headers = config.headers || {};
              config.headers['Content-Type'] = 'application/json';
            }            
            return amisRequest(url, method, { ...data, ...config });
          },

踩坑记:axios改用umi的request之后,相同的mock给出的响应,用axios没问题,用request之后后显示Response is empty错误,跟踪数据后发现axios的响应会比umi多一个data,把mock的返回数据加上一层data包裹,就好了。修改后代码如下:

const resData: AmisData.ResponseData = {
      status: 0,
      msg: '新增成功',
      data: null,
    };
    res.send({data:resData});

使用fontawesome字体

ant design pro是摈弃font字体图标的,取而代之的是svg。这是一种好的做法,可以更加自由并且系统体积也更小了。但是amis采用font-awesome字体图标,这就有点问题,我们必须兼顾。

字体图标问题

最开始我们使用amis的时候,引入的样式表是amis精简后的css,这个文件比较小不到1MB,但是遇到一些带图标的按钮有可能显示不出来,比如以下的schema,我们再使用crud组件的时候希望在工具栏上面显示一个刷新按钮,代码如下:

"headerToolbar": [  
       "reload"
]

这个时候我们会发现,刷新按钮的图标显示不出来。
在这里插入图片描述
进入浏览器调试可以跟踪到,这个是一个使用fontawesom字体的图标。amis内部是使用fontawesome字体的,如果我们把amis全部css和字体库都导入到我们系统里面,就可以使用全部的样式库了。这样这个刷新按钮的图标也就可以显示出来了。
代码如下:

import 'amis/sdk/iconfont.css';
import 'amis/lib/themes/cxd.css';
import 'amis/lib/helper.css';
import 'amis/sdk/cxd.css';

但是这样做我们需要多导入伊特sdk/cxd.css 文件,这个文件接近3MB。并且fontawesome在其他React组件中并不能共享。为此,全局导入fontawesome要划算许多,整个awesome才1.25MB,如果全局导入我们可以在更多地方用到。

导入font-awesome字体图标

1、下载并解压到src/font=awesome
2、在global.tsx里面执行导入

import "./font-awesome/less/font-awesome.less";

问题解决
在这里插入图片描述

  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值