创建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": "其它示例"
}
]
最终界面呈现如下:(每个页面对应的schema内容详见下一篇amis组件的再改造)