# 开发笔记

返回:大前端 | 返回:最全导引

🐉 antd 框架学习与使用 🐉 CSS style 的学习
其他 antd 组件学习领悟 Table 组件
form 组件学习 setState 详细使用的学习
java 心得经验 公司事务
第三方系统对接 其他经历累积
四信培训分享 服务端主动推送数据,除了 WebSocket 你还能想到啥
umi相关

# 前端 react 工程移除不需要的依赖包

yarn start --reset-cache
1

# 采用【&&】控制子组件的加载问题,可保证不必要的数据请求加载等问题

{
  visible && (
    <MaterialAdd
      title={title}
      visible={visible}
      record={detail}
      onCancel={this.onCancel}
      onSubmit={this.onSubmit}
    />
  );
}
1
2
3
4
5
6
7
8
9
10
11

visible 如果为 false,则此子组件不会进行任务加载及初始化动作,但是会导致另外一个问题,就是在生命周期函数

componentWillUpdate = async (nextProps) => {
  console.info(
    nextProps.visible,
    "--------------",
    this.props.visible,
    "+++++++++++"
  );
  if (nextProps.record.id && nextProps.record.id !== this.props.record.id) {
    console.info("有没有在查询附件啊。。-----------------");
    await this.initAttachments(nextProps.record.id);
  }
};
1
2
3
4
5
6
7
8
9
10
11
12

此不会执行,因为组件根本没有初始化过,就不可能会有 update 一说,此时数据请求必须写在componentDidMount

componentDidMount = async () => {
  const { id } = this.props.record || {};
  if (id) {
    await this.initAttachments(id);
  }
};
1
2
3
4
5
6

# 下载文件

🔝🔝开发笔记

/**
 * 保存文件
 * @param blob 文件流
 * @param fileName 文件名
 */
export const saveFile = ({ blob, fileName }) => {
  if (window.navigator.msSaveOrOpenBlob) {
    navigator.msSaveBlob(blob, fileName);
  } else {
    const link = document.createElement("a");
    link.href = window.URL.createObjectURL(blob);
    link.download = fileName;
    // 此写法兼容可火狐浏览器
    document.body.appendChild(link);
    const evt = document.createEvent("MouseEvents");
    evt.initEvent("click", false, false);
    link.dispatchEvent(evt);
    document.body.removeChild(link);
    // 释放内存
    window.URL.revokeObjectURL(link.href);
  }
};

/**
 * 根据文件地址获取下载流
 * @param {文件地址} url
 */
export const getBolbByUrl = (url) =>
  new Promise((resolve) => {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", `${url}?random=${Math.random()}`, true);
    xhr.responseType = "blob";
    xhr.onload = () => {
      if (xhr.status === 200) {
        resolve(xhr.response);
      }
    };
    xhr.send();
  });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
 * 下载
 * @param  {String} url 目标文件地址
 * @param  {String} filename 想要保存的文件名称
 */
function download(url, filename) {
  getBolbByUrl(url).then((blob) => {
    saveFile({ blob, filename });
  });
}
1
2
3
4
5
6
7
8
9
10

# 多次异步请求统一接口防止老数据覆盖新数据

  • 方案一:限制用户快速进行同类操作,当用户通过操作触发了一次请求后,将同类的操作按钮禁用,成功响应数据后再将按钮解除禁用。
  • 方案二:分析用户行为会发现,用户快速操作同一接口后,只希望得到最后一次操作的数据。由此想到当用户快速操作同一接口时,只发送最后一次操作的 ajax 请求,即可在一定程度上解决该 bug。这里我设置一个名为timer的500毫秒的一次性定时器,每次用户操作时首先清除没有执行 timer 定时器,然后通过 timer 定时器延迟 500 毫秒执行 ajax 请求,这样用户在 500 毫秒内执行的相同操作时,没执行的 timer 定时器被清理,始终只会保留最后一次操作的 ajax 请求,500 毫秒内没有相同操作时,会发送该请求。
// 组件数据定时请求
useEffect(() => {
  const timer = setTimeout(() => {
    initAlarm();
  }, 500);
  return () => {
    clearTimeout(timer);
  };
}, [initSearchParams, parentParams]);
1
2
3
4
5
6
7
8
9
  • 方案三:做一个计时器,初始值为 0,每一次发送 ajax 请求前为计时器做一个递加操作,发送 ajax 请求时将该值作为参数传给后台,接收响应数据时后台将该值再返回来,我们根据前端存储的计数器的值与后端返回来的值作比较,只有二者相等时说明返回的是最后一次用户操作的数据。如二者不相等,则返回的数据不是最后一次操作的数据。

# iframe

export default function MyComponent(props) {
  return (
    <div>
      <div dangerouslySetInnerHTML={{ __html: props.iframe }}></div>
    </div>
  );
}

const iframe = `<iframe src="https://www.example.com/show?data..." width="540" height="450"></iframe>`;

<MyComponent iframe={iframe} />;
1
2
3
4
5
6
7
8
9
10
11

# 存在 onClick 函数没有使用箭头函数

Warning: React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in
1

import 问题

This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the property `target` on a released/nullified synthetic event. This is set to null. If you must
1

出于性能原因,将重用此合成事件。如果您看到这一点,那么您正在访问已释放/取消的合成事件的属性“target”。设置为空。如果必须保留原始合成事件,请使用event.persist()

WARNING

TypeError: Cannot read property 'classList' of null

It means that document.getElementById("lastName") is returning undefined and you're trying to call classList on undefined itself.
In your HTML input has the attribute name which equals lastName but there is no actual id="lastname"
Either add the attribute id to your input or change getElementById to getElementsByName.
Note that getElementsByName(此返回数组) doesn't return a single item.

WARNING

Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.

需定位到出错的地方,再排查原因

WARNING

Actions may not have an undefined "type" property. Have you misspelled a constant?

未定义的 dispatch 中的type属性

# a 标签的跳转安全问题

当您的页面链接至使用 target="_blank" 的另一个页面时,新页面将与您的页面在同一个进程上运行。 如果新页面正在执行开销极大的 JavaScript,您的页面性能可能会受影响。
此外,target="_blank" 也是一个安全漏洞。新的页面可以通过 window.opener 访问您的窗口对象,并且它可以使用 window.opener.location = newURL 将您的页面导航至不同的网址。
将 rel="noopener" 添加至 Lighthouse 在您的报告中识别的每个链接。 一般情况下,当您在新窗口或标签中打开一个外部链接时,始终添加 rel="noopener"。

  • Lighthouse 使用以下算法将链接标记为 rel="noopener" 候选项:
    • 收集所有包含属性 target="_blank"、但不包含属性 rel="noopener" 的 <a> 节点。
    • 过滤任何主机相同的链接。
<a
  href={item.filePath}
  className={stylesFault.detailImage}
  target="_blank"
  rel="noopener noreferrer"
>
1
2
3
4
5
6

a 标签跳转到新页面,需要加上rel="noopener noreferrer"

# column_onClick

{
title: formatMessage({
  id: 'items.operation',
}),
className: 'operator',
align: 'center',
fixed: 'right',
width: 160,
render: (value, param) => {
  // 待审核
  if (param.status === '0') {
    return (
      <div>
        <Authorized authority={funcAuth.confirm}>
          <Popconfirm
            title={
              <span className={styleFault.errorFont}>
                <FormattedMessage id="items.approve.confirm" />
              </span>
            }
            onConfirm={() => approve(param)}
            okText={formatMessage({
              id: 'common.confirm',
            })}
            cancelText={formatMessage({
              id: 'common.cancel',
            })}
            okType="danger"
          >
            <a href="#">
              <span className={styleFault.errorFont}>
                <FormattedMessage id="items.approve" />
              </span>
            </a>
          </Popconfirm>
          <Divider type="vertical" />
        </Authorized>
        <Authorized authority={funcAuth.save}>
          <a onClick={() => editRecord(param)}>
            <FormattedMessage id="common.edit" />
          </a>
          <Divider type="vertical" />
        </Authorized>
        <Authorized authority={funcAuth.query}>
          <a onClick={() => viewRecord(param)}>
            <FormattedMessage id="common.view" />
          </a>
        </Authorized>
      </div>
    );
  }
  return (
    <div>
      <Authorized authority={funcAuth.save}>
        <a onClick={() => editRecord(param)}>
          <FormattedMessage id="common.edit" />
        </a>
        <Divider type="vertical" />
      </Authorized>
      <Authorized authority={funcAuth.query}>
        <a onClick={() => viewRecord(param)}>
          <FormattedMessage id="common.view" />
        </a>
      </Authorized>
    </div>
  );
},
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

函数(function)组件情况下,其中<a onClick={() => viewRecord(param)}>,必须写出箭头函数形式,如果直接写<a onClick={viewRecord(param)}>,那么函数会不断执行,直到内存溢出

# 下拉刷新上滑加载更多

# 日期可选

🔝🔝开发笔记

  • 不能选择今天之前的日期,包括今天的日期也不可以选择
const disabledDate = (current) => {
  return current && current < moment().endOf("day");
};
1
2
3
  • 不能选择今天之前的日期,今天日期可以选择
const disabledDate = (current) => {
  return current && current < moment().subtract(1, "day");
};
1
2
3
  • 当前时间之后的时间点,精确到小时
const [upgradeTime, setUpgradeTime] = useState(moment("00:00:00", "HH:mm:ss"));

const disabledDate = (current) => {
  return current && current < moment().subtract(1, "day"); // 今天可以选择
};

const disabledDateTime = () => {
  const hours = moment().hours(); // 0~23
  // 当日只能选择当前时间之后的时间点
  if (upgradeTime.date() === moment().date()) {
    return {
      disabledHours: () => range(0, hours + 1),
    };
  }
};

<Form.Item label="发送时间">
  {getFieldDecorator("pushTime", {
    rules: [{ required: false, message: "请输入发送时间" }],
    initialValue:
      record.pushType === 0
        ? null
        : record.pushTime
        ? moment(record.pushTime, "YYYY-MM-DD HH:mm:ss")
        : null, // 定时发送才显示时间
  })(
    <DatePicker
      format="YYYY-MM-DD HH:mm:ss"
      disabledDate={disabledDate}
      disabledTime={disabledDateTime}
      style={{ width: "100%" }}
      onChange={(timer) => setUpgradeTime(timer)} // !!!
      showTime={{ defaultValue: moment(upgradeTime) }} // !!!
    />
  )}
</Form.Item>;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  • 当前时间之后的时间点,精确到分
const [upgradeTime, setUpgradeTime] = useState(moment("00:00:00", "HH:mm:ss"));

const disabledDate = (current) => {
  return current && current < moment().subtract(1, "day"); // 今天可以选择
};

const disabledDateTime = () => {
  const hours = moment().hours(); // 0~23
  const minutes = moment().minutes(); // 0~59
  // 当日只能选择当前时间之后的时间点
  if (upgradeTime.date() === moment().date()) {
    return {
      disabledHours: () => range(0, hours),
      disabledMinutes: () => range(0, minutes), // 精确到分
    };
  }
};

<Form.Item label="发送时间">
  {getFieldDecorator("pushTime", {
    rules: [{ required: false, message: "请输入发送时间" }],
    initialValue:
      record.pushType === 0
        ? null
        : record.pushTime
        ? moment(record.pushTime, "YYYY-MM-DD HH:mm:ss")
        : null, // 定时发送才显示时间
  })(
    <DatePicker
      format="YYYY-MM-DD HH:mm:ss"
      disabledDate={disabledDate}
      disabledTime={disabledDateTime}
      style={{ width: "100%" }}
      onChange={(timer) => setUpgradeTime(timer)} // !!!
      showTime={{ defaultValue: moment(upgradeTime) }} // !!!
    />
  )}
</Form.Item>;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# tree

# 默认属性无效问题_defaultExpandAll

由于 tree 的数据都是异步获取的,而 tree 的默认属性只在其第一次渲染时候才生效,但是一般第一次渲染时,数据为空,导致后续异步获取的数据到达后,一些 default 属性无效

  • 解决方案一:
    • 当数据回来的时候再渲染 tree
<div>{findAreasTreeLoading ? <Spin /> : generateTree()}</div>
1
const generateTree = () => {
  return treeData.length === 0 ? null : (
    <Tree treeData={treeData} onSelect={onSelect} defaultExpandAll></Tree>
  );
};
1
2
3
4
5

# treeRight

<Tree onRightClick="{this.treeNodeonRightClick}"></Tree>
1
treeNodeonRightClick(e) {
    this.setState({
        rightClickNodeTreeItem: {
            pageX: e.event.pageX,
            pageY: e.event.pageY,
            id: e.node.props['data-key'],
            categoryName: e.node.props['data-title']
        }
    });
}

// id 和 categoryName 是生成时绑上去的
<TreeNode
  key={item.id}
  title={title}
  data-key={item.id}
  data-title={item.categoryName}
/>);

// 最后绑个菜单就可以实现了
getNodeTreeRightClickMenu() {
        const {pageX, pageY} = {...this.state.rightClickNodeTreeItem};
        const tmpStyle = {
            position: 'absolute',
            left: `${pageX - 220}px`,
            top: `${pageY - 70}px`
        };
        const menu = (
            <Menu
                onClick={this.handleMenuClick}
                style={tmpStyle}
                className={style.categs_tree_rightmenu}
            >
                <Menu.Item key='1'><Icon type='plus-circle'/>{'加同级'}</Menu.Item>
                <Menu.Item key='2'><Icon type='plus-circle-o'/>{'加下级'}</Menu.Item>
                <Menu.Item key='4'><Icon type='edit'/>{'修改'}</Menu.Item>
                <Menu.Item key='3'><Icon type='minus-circle-o'/>{'删除目录'}</Menu.Item>
            </Menu>
        );
        return (this.state.rightClickNodeTreeItem == null) ? '' : menu;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# connectForm

function mapStateToProps({ onlineCamera }) {
  return {
    favorites: onlineCamera.favorites,
  };
}

export default connect(mapStateToProps)(Form.create()(TreeModal));
1
2
3
4
5
6
7

# keyWarn

  • 一般都是使用 rowkey 方法一解决(后台数据要保证没有重复);
  • 方法二:dataSource 数据新增 key

# 注入顺序

@Form.create()
@inject(({stores}) => ({clusterModel:stores.clusterModel}))
@observer
1
2
3

# 表格 Columns 字段 id 页面不展示情况

🔝🔝开发笔记

一般而言,表格 Columns 字段 id 是在界面不展示的,但是,对于有些逻辑的处理,又是需要的,可以使用相应样式隐藏的处理方式。

// 常规展示的情况:
{
  title: '序号',
  dataIndex: 'algoId',
  key: 'algoId'
},

// 不展示id字段:
{
  title: '',
  dataIndex: 'algoId',
  key: 'algoId',
  width: 0,
  render: item => {
    return (
      <span style={{ display: 'none' }} title={item}>
        {item}
      </span>
    );
  }
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# connect 装饰器

back

dva 所封装的 react-redux 的 @connect装饰器,用来接收绑定的 list 这个 model 对应的 redux store。注意到这里的装饰器实际除了 app.state.list 以外还实际接收 app.state.loading 作为参数,这个 loading 的来源是 src/index.js 中调用的 dva-loading 2 这个插件。

# 跨域解决

// FROM https://github.com/sorrycc/roadhog#proxy
"proxy": {
  "/api": {
    "target": "http://localhost:8080",
    "changeOrigin": true,
    "pathRewrite": { "^/api" : "" }
  }
}
1
2
3
4
5
6
7
8