最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 基于 react + typescript 实现 tree 组件

    正文概述 掘金(cherish)   2021-07-06   672

    基于 react + typescript 实现 tree 组件

    • 支持单元测试
    • 支持打开关闭
    • 支持取消全选
    • 支持动态加载
    • 支持拖动排序

    process line

    • 1.1 创建项目,安装依赖
    • 1.2 webpack 配置初始化,支持 typescript
    • 2.1 单元测试环境搭建
      1. 创建树型菜单
      1. 创建树型菜单
    • 5.1 打开关闭功能-icons
    • 5.2 打开关闭功能-keyToNodeMap
    • 5.3 打开关闭功能-images.d.ts 图片类型声明
    • 5.3 打开关闭功能-onCollapse
    • 5.4 选中/取消/全选等功能-parent
    • 5.5 loading 动态加载
    • 5.6 拖动排序

    1. 初始化项目

    • 项目预览

    1.1 创建项目

    mkdir crx-react-tree
    cd crx-react-tree
    npm init -y
    touch .gitignore
    

    1.2 安装依赖

    • cnpm i react @types/react react-dom @types/react-dom -S
    react 核心包
    @types/react 类型声明
    react-dom react的dom渲染包 把react元素渲染到页面上
    @types/react-dom react的dom渲染声明文件
    
    • cnpm i webpack webpack-cli webpack-dev-server -D
    webpack 核心文件
    webpack-cli 命令行文件
    webpack-dev-server 开发服务器
    
    • cnpm i typescript ts-loader source-map-loader style-loader css-loader less-loader less file-loader url-loader html-webpack-plugin -D
    ts-loader 加载ts的
    source-map-loader 转译source-map文件的 可以调试ts文件
    style-loader 把css文件插到页面中去
    file-loader url-loader 加载文件的 图标/二进制文件/图片
    html-webpack-plugin 生成html文件
    
    • cnpm i jest @types/jest ts-jest jest-junit enzyme @types/enzyme enzyme-adapter-react-16 @types/enzyme-adapter-react-16 -D
    单元测试 测试覆盖率
    jest
    @types/jest 类型声明
    ts-jest 运行ts版的单元测试文件
    jest-junit 单元测试工具
    enzyme 测react项目的工具
    @types/enzyme enzyme类型声明
    enzyme-adapter-react-16
    @types/enzyme-adapter-react-16
    
    • cnpm i axios express qs @types/qs -D
    • @types开头的包都是 typeScript 的声明文件,可以进入node_modules/@types/XX/index.d.ts进行查看
    模块名使用方式
    reactReact is a JavaScript library for creating user interfaces.react-domThis package serves as the entry point to the DOM and server renderers for React. It is intended to be paired with the generic React package, which is shipped as react to npm.webpackwebpack is a module bundler. Its main purpose is to bundle JavaScript files for usage in a browser, yet it is also capable of transforming, bundling, or packaging just about any resource or asset.webpack-cliThe official CLI of webpackwebpack-dev-serverUse webpack with a development server that provides live reloading. This should be used for development only.typescriptTypeScript is a language for application-scale JavaScript.ts-loaderThis is the TypeScript loader for webpack.source-map-loaderExtracts source maps from existing source files (from their sourceMappingURL).style-loaderInject CSS into the DOM.css-loaderThe css-loader interprets @import and url() like import/require() and will resolve them.less-loaderA Less loader for webpack. Compiles Less to CSS.lessThis is the JavaScript, official, stable version of Less.file-loaderThe file-loader resolves import/require() on a file into a url and emits the file into the output directory.url-loaderA loader for webpack which transforms files into base64 URIs.html-webpack-pluginPlugin that simplifies creation of HTML files to serve your bundlesjestjest is a delightful JavaScript Testing Framework with a focus on simplicity.jest-junitA Jest reporter that creates compatible junit xml filests-jestts-jest is a TypeScript preprocessor with source map support for Jest that lets you use Jest to test projects written in TypeScript.enzymeJavaScript Testing utilities for Reactenzyme-adapter-react-16Enzyme is a JavaScript Testing utility for React that makes it easier to test your React Components' output. You can also manipulate, traverse, and in some ways simulate runtime given the output.

    1.3 支持 typescript

    • 首先需要生成一个tsconfig.json文件来告诉ts-loader如何编译代码 TypeScript 代码
    tsc --init
    

    tsconfig.json

    {
      "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "jsx": "react",
        "outDir": "./dist",
        "rootDir": "./src",
        "noImplicitAny": true,
        "esModuleInterop": true
      },
      "include": ["./src/**/*", "./typings/**/*"]
    }
    
    参数含义
    target转换成 es5module代码规范jsxreact 模式会生成 React.createElement,在使用前不需要再进行转换操作了,输出文件的扩展名为.jsoutDir指定输出目录rootDir指定根目录sourceMap把 ts 文件编译成 js 文件的时候,同时生成对应的 sourceMap 文件noImplicitAny如果为 true 的话,TypeScript 编译器无法推断出类型时,它仍然会生成 JS 文件,但是它也会报告一个错误esModuleInterop是否转译 common.js 模块include需要编译的目录

    1.3 支持 typescript

    jest.config.js

    module.exports = {
      verbose: true, //冗余 显示详细信息
      clearMocks: true, // 清除 mocks
      collectCoverage: true, // 收集测试覆盖率信息
      reporters: ["default", "jest-junit"], // 报告器 报告的格式 默认单元测试
      moduleFileExtensions: ["js", "jsx", "ts", "tsx"], // 模块文件扩展名
      moduleDirectories: ["node_modules"], // 模块目录
      transform: {
        // 如果模块是.tsx结尾的文件,需要用ts-jest进行转译
        "^.+\\.tsx?$": "ts-jest",
      },
      // 表示要进行单元测试的正则匹配: 文件在__tests__目录下,或者以jsx|tsx结尾的文件
      testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx|tsx)$",
    };
    

    1.4 打开关闭功能

    src/index.tsx

    import React from "react";
    import ReactDOM from "react-dom";
    import Tree from "./components/tree";
    import data from "./data";
    
    ReactDOM.render(<Tree data={data} />, document.getElementById("root"));
    

    src/typings.tsx

    // 声明一个接口,名字叫 Treedata , 定义一个变量的类型,定义对象的属性名和属性的类型
    export interface TreeData {
      name: string;
      key: string;
      type: string;
      collapsed: boolean;
      children?: TreeData[]; // ?:表示可选属性,可以给也可以不给
      parent?: TreeData;
      checked?: boolean;
    }
    
    // 接口类型,可以用来装饰或者说约束组件属性对象
    export interface Props {
      data: TreeData;
      onCollapse: any;
      onCheck: any;
    }
    export interface State {
      data: TreeData;
    }
    export interface keyToNodeMap {
      [key: string]: TreeData;
    }
    

    src/components/tree.tsx

    import React from "react";
    import "./index.less";
    import { TreeData, Props, State, keyToNodeMap } from "../typings";
    import TreeNode from "./tree-node";
    
    class Tree extends React.Component<Props, State> {
      keyToNodeMap: keyToNodeMap;
      constructor(props: Props) {
        super(props);
        this.state = { data: this.props.data };
      }
      componentDidMount() {
        this.buildKeyMap();
      }
      buildKeyMap = () => {
        const data = this.state.data;
        this.keyToNodeMap = {};
        this.keyToNodeMap[data.key] = data;
        if (data.children && data.children.length > 0) {
          this.walk(data.children, data);
        }
        this.setState({ data: this.state.data });
      };
      walk = (children: TreeData[], data: TreeData) => {
        children.forEach((item: TreeData) => {
          item.parent = data;
          this.keyToNodeMap[item.key] = item;
          if (item.children && item.children.length > 0) {
            this.walk(item.children, item);
          }
        });
      };
      onCollapse = (key: string) => {
        let data = this.keyToNodeMap[key];
        if (data) {
          data.collapsed = !data.collapsed;
          data.children = data.children || []; // 后面会改成懒加载
          this.buildKeyMap();
        }
      };
      onCheck = (key: string) => {
        let data: TreeData = this.keyToNodeMap[key];
        if (data) {
          data.checked = !data.checked;
          if (data.checked) {
            this.checkChildren(data.children, true);
            this.checkParentCheckAll(data.parent);
          } else {
            this.checkChildren(data.children, false);
            this.checkParent(data.parent, false);
          }
          this.setState({ data: this.state.data });
        }
      };
      checkParentCheckAll = (parent: TreeData) => {
        while (parent) {
          parent.checked = parent.children.every((item) => item.checked);
          parent = parent.parent;
        }
      };
      checkParent = (parent: TreeData, checked: boolean) => {
        // 可以优化,不用找到顶点 只需要找到当前节点和传入的状态一致就可以停止递归查找了
        while (parent) {
          parent.checked = checked;
          parent = parent.parent;
        }
      };
      checkChildren = (children: Array<TreeData> = [], checked: boolean) => {
        children.forEach((item: TreeData) => {
          item.checked = checked;
          this.checkChildren(item.children, checked);
        });
      };
      render() {
        return (
          <div className="tree">
            <div className="tree-nodes">
              <TreeNode
                onCheck={this.onCheck}
                onCollapse={this.onCollapse}
                data={this.props.data}
              />
            </div>
          </div>
        );
      }
    }
    export default Tree;
    

    src/components/tree-node/tsx

    import React from "react";
    import { TreeData, Props } from "../typings";
    import closedFolder from "../assets/closed-folder.png";
    import file from "../assets/file.png";
    import openedFolder from "../assets/opened-folder.png";
    
    class TreeNode extends React.Component<Props> {
      constructor(props: Props) {
        super(props);
      }
      render() {
        let {
          data: { name, children, collapsed = false, checked = false, key },
        } = this.props;
        let caret, icon;
        if (children) {
          if (children.length > 0) {
            caret = (
              <span
                className={`collapse ${collapsed ? "caret-right" : "caret-down"}`}
                onClick={() => this.props.onCollapse(key)}
              />
            );
            icon = collapsed ? closedFolder : openedFolder;
          } else {
            caret = null;
            icon = file;
          }
        } else {
          caret = (
            <span
              className={`collapse caret-right`}
              onClick={() => this.props.onCollapse(key)}
            />
          );
          icon = closedFolder;
        }
        return (
          <div className="tree-node">
            <div className="inner">
              {caret}
              <span className="content">
                <input
                  type="checkbox"
                  checked={checked}
                  onChange={() => this.props.onCheck(key)}
                />
                <img style={{ width: 20 }} src={icon} />
                {name}
              </span>
            </div>
            {children && children.length > 0 && !collapsed && (
              <div className="children">
                {children.map((item: TreeData) => (
                  <TreeNode
                    onCheck={this.props.onCheck}
                    onCollapse={this.props.onCollapse}
                    key={item.key}
                    data={item}
                  />
                ))}
              </div>
            )}
          </div>
        );
      }
    }
    export default TreeNode;
    

    index.less

    .tree {
      position: fixed;
      left: 0;
      top: 0;
      bottom: 0;
      width: 80%;
      overflow-x: hidden;
      overflow-y: auto;
      background-color: #eee;
      .tree-nodes {
        position: relative;
        overflow: hidden;
        .tree-node {
          .inner {
            color: #000;
            font-size: 20px;
            position: relative;
            cursor: pointer;
            padding-left: 10px;
            .collapse {
              position: absolute;
              left: 0;
              cursor: pointer;
            }
            .caret-right:before {
              content: "\25B8";
            }
            .caret-down:before {
              content: "\25BE";
            }
    
            .content {
              display: inline-block;
              width: 100%;
              padding: 4px 5px;
            }
          }
          .children {
            padding-left: 20px;
          }
        }
      }
    }
    

    data.tsx

    import { TreeData } from "./typings";
    const data: TreeData = {
      name: "父亲",
      key: "1",
      type: "folder",
      collapsed: false,
      children: [
        {
          name: "儿子1",
          key: "1-1",
          type: "folder",
          collapsed: false,
          children: [
            {
              name: "孙子1",
              key: "1-1-1",
              type: "folder",
              collapsed: false,
              children: [
                {
                  name: "重孙1",
                  key: "1-1-1-1",
                  type: "file",
                  collapsed: false,
                  children: [],
                },
              ],
            },
          ],
        },
        {
          name: "儿子2",
          key: "1-2",
          type: "folder",
          collapsed: true,
        },
      ],
    };
    export default data;
    

    src/typings/images.d.ts

    declare module "*.svg";
    declare module "*.png";
    declare module "*.jpg";
    declare module "*.jpeg";
    declare module "*.gif";
    declare module "*.bmp";
    declare module "*.tiff";
    

    1.5 选中/取消/全选等功能

    typings.tsx

    export interface TreeData {
      name: string;
      key: string;
      type: string;
      collapsed: boolean;
      children?: TreeData[]; // ?:表示可选属性,可以给也可以不给
    +  parent?: TreeData;
    +  checked?: boolean;
    }
    export interface Props {
      data: TreeData;
      onCollapse: any;
    +  onCheck: any;
    }
    

    tree.tsx

    buildKeyMap = () => {
      const data = this.state.data;
      this.keyToNodeMap = {};
      this.keyToNodeMap[data.key] = data;
      if (data.children && data.children.length > 0) {
        this.walk(data.children, data);
      }
      this.setState({ data: this.state.data });
    };
    walk = (children: TreeData[], data: TreeData) => {
      children.forEach((item: TreeData) => {
        item.parent = data;
        this.keyToNodeMap[item.key] = item;
        if (item.children && item.children.length > 0) {
          this.walk(item.children, item);
        }
      });
    };
    onCheck = (key: string) => {
      let data: TreeData = this.keyToNodeMap[key];
      if (data) {
        data.checked = !data.checked;
        if (data.checked) {
          this.checkChildren(data.children, true);
          this.checkParentCheckAll(data.parent);
        } else {
          this.checkChildren(data.children, false);
          this.checkParent(data.parent, false);
        }
        this.setState({ data: this.state.data });
      }
    };
    checkParentCheckAll = (parent: TreeData) => {
      while (parent) {
        parent.checked = parent.children.every((item) => item.checked);
        parent = parent.parent;
      }
    };
    checkParent = (parent: TreeData, checked: boolean) => {
      // 可以优化,不用找到顶点 只需要找到当前节点和传入的状态一致就可以停止递归查找了
      while (parent) {
        parent.checked = checked;
        parent = parent.parent;
      }
    };
    checkChildren = (children: Array<TreeData> = [], checked: boolean) => {
      children.forEach((item: TreeData) => {
        item.checked = checked;
        this.checkChildren(item.children, checked);
      });
    };
    
    <TreeNode
      onCheck={this.onCheck}
      onCollapse={this.onCollapse}
      data={this.props.data}
    />
    

    tree-node.tsx

    <input
        type="checkbox"
        checked={checked}
        onChange={() => this.props.onCheck(key)}
      />
    
    render() {
      let {
        data: { name, children, collapsed = false, checked = false, key },
      } = this.props;}
    

    1.6 loading动态加载

    1.7 拖动排序


    起源地下载网 » 基于 react + typescript 实现 tree 组件

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元