React之npm发布Antd样式的组件

文章目录

  • 一、npm发包需要了解的知识
    • (1)判断包名是否合法
    • (2)npm初始化
    • (3)devDependencies、dependencies和peerDependencies
  • 二、测试发包
  • 三、使用react组件进行npm发包
    • (1)搭建基本组件
      • 1. create-react-app直接创建组件即可
      • 2. 组件开发
    • (2)基本组件开发
      • 1. header组件开发
      • 2. leftMenu组件
      • 3. base组件
    • (3)主要函数和样式
      • 1. getPrefixCls
      • 2. Var.scss
    • (4)导出组件
  • 四、webpack打包配置
    • (1)yarn eject暴露出配置
    • (2)修改配置
      • 1. 添加路径
      • 2. 修改webpack
    • (3)修改package.json
    • (4)发包
  • 五、测试发布的包
    • (1)下载
    • (2)配置组件
      • 1. 引入Base组件
      • 2. 在app中引入
      • 3. 引入样式
    • (1)搭建基本组件
      • 1. create-react-app直接创建组件即可
      • 2. 组件开发
    • (2)基本组件开发
      • 1. header组件开发
      • 2. leftMenu组件
      • 3. base组件
    • (3)主要函数和样式
      • 1. getPrefixCls
      • 2. Var.scss
    • (4)导出组件
  • 四、webpack打包配置
    • (1)yarn eject暴露出配置
    • (2)修改配置
      • 1. 添加路径
      • 2. 修改webpack
    • (3)修改package.json
    • (4)发包
  • 五、测试发布的包
    • (1)下载
    • (2)配置组件
      • 1. 引入Base组件
      • 2. 在app中引入
      • 3. 引入样式

一、npm发包需要了解的知识

(1)判断包名是否合法

这个可以去npm官网上搜索https://www.npmjs.com/,也可以直接install看看,是否存在

(2)npm初始化

创建一个文件夹,我在这里取名为welkin-test

cd welkin-test
npm init

需要注意的是,需要和package.json的name对应

{
	"name": "welkin-test",
  "version": "1.0.0", //这个分别代表  主版本号.次版本号.修订号
  "description": "基本布局",
 }

针对version简单介绍一下:

  • 主版本号:不兼容的api修改
  • 次版本号:做了向下兼容的功能性新增
  • 修订号:向下兼容的问题修正

(3)devDependencies、dependencies和peerDependencies

  • devDependencies:开发环境所需要的依赖,使用-D;
  • dependencies:生产环境需要的依赖,不仅生产环境使用,开发环境也可以使用;
  • peerDependencies:进行插件开发会用到,在这里指定的依赖,是为了避免重复下载。首先会在本地进行查找是否存在,如不存在进行安装

二、测试发包

在welkin-test里面新建index.js,随便写上一个函数

const a = () => {
  return a * 5;
};

export default a;

然后,npm publish,终端上没有报错就发布成功了

使用create-react-app 创建一个测试项目,将我们刚刚发布的包下载下来看看里面的内容是不是我们写的a函数,如果是那就正常

三、使用react组件进行npm发包

我这里是准备发布一个基本组件,样式和结构如下所示:【antd里也有:https://ant.design/components/layout-cn/】

React之npm发布Antd样式的组件_第1张图片

红色框内的就是准备自己开发的

(1)搭建基本组件

1. create-react-app直接创建组件即可

create-react-app xxxxxx --template typescript

2. 组件开发

其实可以先开发出自己的组件然后再分析,以我的为例子,主要分为三块组件:

  • header:主要是顶部,涉及到logo,title,username以及logoutUrl;
  • leftMenu:菜单信息;涉及到渲染的菜单数据,以及使用到的路由组件
  • base:需要整合header和leftMenu以及内容区域,以及是否涉及header头和leftMenu固定,这里使用fixed变量

(2)基本组件开发

我们将这三个组件放在一个目录下:

1. header组件开发

import React, { CSSProperties } from 'react';
import { Button, Dropdown, Layout, Menu } from 'antd';
import { DownOutlined } from '@ant-design/icons';
import './header.scss';
import getPrefixCls from '../_utils/get-prefix-cls'; //这个函数后面介绍

const { Header: LayoutHeader } = Layout;
//这里是header组件用到的props
export interface Props { 
  logoUrl?: string;
  title?: string;
  userName?: string;
  logoutUrl?: string;
  isFixed?: boolean;
}
const Header: React.FC<Props> = ({
  title,
  logoUrl,
  userName,
  logoutUrl,
  isFixed = false,
}) => {
  //根据是否固定做出的不一样的header style
  const style: CSSProperties = isFixed
    ? { position: 'fixed', zIndex: 3, width: '100%', backgroundColor: '#fff' }
    : { width: '100%', backgroundColor: '#fff' };
  const menu = (
    <Menu>
      <Menu.Item key="1">{logoutUrl && <a href={logoutUrl}>退出</a>}</Menu.Item>
    </Menu>
  );
  return (
    <LayoutHeader className={getPrefixCls('header')} style={style}>
      <div className={getPrefixCls('header__logo')}>
        {logoUrl && <img src={logoUrl} alt="logo" />}
        {title && <span>{title}</span>}
      </div>
      <nav className={getPrefixCls('header__nav')} />
      {userName && (
        <div className={getPrefixCls('header__user')}>
          <Dropdown overlay={menu}>
            <Button type="link">
              <span>{userName}</span>
              <DownOutlined />
            </Button>
          </Dropdown>
        </div>
      )}
    </LayoutHeader>
  );
};
export default Header;

header组件的scss如下:

@import '../style/var.scss'; //这个后面介绍

.#{$name}-header {
  display: flex;
  border-bottom: 1px solid #dcdcdc;

  &__logo {
    display: flex;
    justify-content: center;

    img {
      width: 75px;
      align-self: center;
    }

    span {
      font-size: 14px;
      font-family: unset;
      color: #383838;
    }
  }

  &__nav {
    flex: 1;
  }

  &__user {
    :global {
      .ant-avatar {
        cursor: pointer;
      }
    }
  }
}

2. leftMenu组件

页面组件:

import React, { useMemo, useCallback } from 'react';
import { Menu } from 'antd';
import { Link } from 'react-router-dom';
import './leftMenu.scss';
import { Location } from 'history';
import getPrefixCls from '../_utils/get-prefix-cls';

const { SubMenu } = Menu;
//菜单项结构
export interface MenuItem {
  key: string;
  name: string;
  icon?: any;
  type: string;
  children?: MenuItem[];
}
//leftMenu组件用到的props
export interface Props {
  location: Location;
  MenuData: MenuItem[];
}
const LeftMenu: React.FC<Props> = ({ location, MenuData }) => {
  const getDefaultOpenKeys = useCallback(
    (
      data: MenuItem[] = [],
      openKeys: string[] = [],
      parentKeys: string[] = []
    ): string[] =>
      data.reduce((prev, curr): any => {
        if (curr.key === location.pathname) {
          return [...prev, ...openKeys, curr.key, ...parentKeys];
        }
        if (curr.children && curr.children.length !== 0) {
          return [
            ...prev,
            ...getDefaultOpenKeys(curr.children, openKeys, [
              ...parentKeys,
              curr.key,
            ]),
          ];
        }
        return [...prev];
      }, []),
    [location.pathname]
  );
  const defaultOpenKeys = useMemo(() => getDefaultOpenKeys(MenuData), [
    getDefaultOpenKeys,
  ]);
  // 遍历定义的菜单
  const MenuList: JSX.Element[] = useMemo(
    () =>
      MenuData.map((item) => {
        if (item.children && item.children.length !== 0) {
          return (
            <SubMenu
              key={item.key}
              title={
                <span>
                  {item.icon &&
                    React.createElement(item.icon, {
                      style: { verticalAlign: 'middle' },
                    })}
                  <span>{item.name}</span>
                </span>
              }
            >
              {item.children.map((subItem) => (
                <Menu.Item key={subItem.key}>
                  {subItem.type === 'link' ? (
                    <Link to={subItem.key}>{subItem.name}</Link>
                  ) : (
                    <a target="_blank " href={subItem.key}>
                      {subItem.name}
                    </a>
                  )}
                </Menu.Item>
              ))}
            </SubMenu>
          );
        }
        return (
          <Menu.Item key={item.key}>
            {item.type === 'link' ? (
              <Link to={item.key}>
                {item.icon &&
                  React.createElement(item.icon, {
                    style: { verticalAlign: 'middle' },
                  })}
                <span>{item.name}</span>
              </Link>
            ) : (
              <a target="_blank " href={item.key}>
                {item.icon &&
                  React.createElement(item.icon, {
                    style: { verticalAlign: 'middle' },
                  })}
                {item.name}
              </a>
            )}
          </Menu.Item>
        );
      }),
    []
  );
  return (
    <Menu
      className={getPrefixCls('leftMenu')}
      theme="light"
      mode="inline"
      defaultSelectedKeys={[location.pathname]}
      defaultOpenKeys={defaultOpenKeys}
    >
      {MenuList}
    </Menu>
  );
};
export default LeftMenu;

用到的scss

@import '../style/var.scss';

.#{$name}-leftMenu {
  border-right: none;
}

.#{$name}-iconStyle {
  display: flex;
  align-items: center;
}

3. base组件

import React, { CSSProperties, useCallback, useState } from 'react';
import { Layout } from 'antd';
import Header from '../Header/header';
import LeftMenu, { MenuItem } from '../LeftMenu/leftMenu';
import { Location } from 'history';

const { Sider, Content } = Layout;
export interface Props {
  logoUrl?: string;
  title?: string;
  userName?: string;
  logoutUrl?: string;
  location: Location;
  MenuData: MenuItem[];
  isFixed?: boolean;
}
const Base: React.FC<Props> = ({
  children,
  MenuData,
  location,
  title,
  logoUrl,
  userName,
  logoutUrl,
  isFixed = false,
}) => {
  const [collapsed, setCollapsed] = useState(false);
  // 点击展开,再次点击关闭
  const handleTootle = useCallback(() => {
    setCollapsed((v) => !v);
  }, [setCollapsed]);
  const layoutStyle: CSSProperties = isFixed
    ? {
        paddingTop: '70px',
        overflow: 'auto',
        height: '100vh',
        position: 'fixed',
        left: 0,
        zIndex: 1,
      }
    : {};
  const contentStyle = isFixed
    ? {
        margin: '24px 16px',
        marginTop: 90,
        padding: 20,
        marginLeft: collapsed ? '100px' : '220px',
        background: '#fff',
      }
    : {
        margin: '24px 16px',
        padding: 20,
        background: '#fff',
      };
  return (
    <Layout style={{ minHeight: '100vh' }}>
      <Header
        title={title}
        logoUrl={logoUrl}
        logoutUrl={logoutUrl}
        userName={userName}
        isFixed={isFixed}
      />
      <Layout>
        <Sider
          collapsible
          collapsed={collapsed}
          onCollapse={handleTootle}
          theme="light"
          style={layoutStyle}
        >
          <LeftMenu MenuData={MenuData} location={location} />
        </Sider>
        <Layout>
          <Content style={contentStyle}>{children}</Content>
        </Layout>
      </Layout>
    </Layout>
  );
};

export default Base;

(3)主要函数和样式

因为在打包中,我们的包被引用之后可能出现css找不到或者被覆盖的情况,所以要给我们自己的css加一个前缀

1. getPrefixCls

该函数是给页面级组件加前缀

const getPrefixCls = (
  suffixCls: string,
  customizePrefixCls = 'fui'
): string => {
  return `${customizePrefixCls}-${suffixCls}`;
};
export default getPrefixCls;

2. Var.scss

这是个scss函数主要在组件scss中引入,给scss加前缀

$name:fui

(4)导出组件

import Base from './Base';
import Header from './Header';
import LeftMenu from './LeftMenu';

export { Base, Header, LeftMenu };
export default { Base, Header, LeftMenu };

其实这个时候也可以在app中引入自己写的组件看看是否无误,就比如我在这里写了一个测试组件,之后在app中引入就可以看到该组件的样式:

测试组件代码如下:

import React, { FC } from 'react';
import { Base } from './components';
import { FormOutlined, FileSyncOutlined } from '@ant-design/icons';
import { MenuItem } from './components/LeftMenu/leftMenu';
import { useLocation } from 'react-router-dom';

const Ceshi: React.FC = () => {
  const location = useLocation();
  const menuData: MenuItem[] = [
    {
      key: '/search/*',
      name: '账号审计',
      icon: FormOutlined,
      type: 'link',
      children: [
        {
          key: '/search',
          name: '账号全量查询',
          type: 'link',
        },
        {
          key: '/search/inactive',
          name: '账号不活跃查询',
          type: 'link',
        },
        {
          key: '/search/match',
          name: '一人多账号查询',
          type: 'link',
        },
        {
          key: '/search/share',
          name: '账号共享查询',
          type: 'link',
        },
      ],
    },
    {
      key: '/config',
      name: '账号审计配置',
      icon: FileSyncOutlined,
      type: 'link',
      children: [
        {
          key: '/config/inactive',
          name: '账号不活跃管理',
          type: 'link',
        },
        {
          key: '/config/match',
          name: '一人多账号报备',
          type: 'link',
        },
      ],
    },
  ];
  return (
    <Base
      MenuData={menuData}
      location={location}
      title={'ceshi'}
      userName={'welkin'}
      logoutUrl={'/logout/ceshi'}
    ></Base>
  );
};
export default Ceshi;

app组件如下:

import React from 'react';
import Ceshi from './ceshi';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
const App: React.FC = () => {
  return (
    <div className="App">
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<Ceshi />} />
        </Routes>
      </BrowserRouter>
    </div>
  );
};

export default App;

四、webpack打包配置

(1)yarn eject暴露出配置

yarn eject

(2)修改配置

1. 添加路径

在config/path中添加路径,因为我的组件是在components目录下的,所以添加

comIndexJs: resolveModule(resolveApp, 'src/components/index'),
 //这个地方就是我们的导出组件文件index

2. 修改webpack

//修改入口文件
entry: isEnvProduction ? paths.comIndexJs : paths.appIndexJs,
//修改output 部分
output:{
 //修改filename
 filename: isEnvProduction
        ? 'fui.js'
        : isEnvDevelopment && 'static/js/bundle.js',
     
 // 修改devtoolModuleFilenameTemplate
 devtoolModuleFilenameTemplate: isEnvProduction
   ? (info) =>
 path
   .relative(paths.appSrc, info.absoluteResourcePath)
   .replace(/\\/g, '/')
 : isEnvDevelopment &&
   ((info) =>
    path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
//添加如下:
library: 'fui',
libraryTarget: 'umd',
libraryExport: 'default',
}
//将 HtmlWebpackPlugin 设置为isEnvDevelopment 判断
plugins: [
  isEnvDevelopment &&
  new HtmlWebpackPlugin(
    Object.assign(xxxx)//....
  )
]
//增加 externals 里面的内容要和package.json中的peerDependencies挂钩
externals: {
  react: {
    commonjs: 'react',
    commonjs2: 'react',
    amd: 'react',
    root: 'React',
  },
  'react-dom': {
      commonjs: 'react-dom',
      commonjs2: 'react-dom',
      amd: 'react-dom',
      root: 'ReactDOM',
    },
    'react-router-dom': {
       commonjs: 'react-router-dom',
       commonjs2: 'react-router-dom',
       amd: 'react-router-dom',
       root: 'ReactRouterDom',
      },
},

(3)修改package.json

添加如下:

  "peerDependencies": {
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-router-dom": "^6.2.1"
  },

(4)发包

  1. 首先进行yarn build验证,如果没有问题
  2. 检查package.json

修改你的版本号【每一次npm publish都要在之前修改】,记住package.json重的name跟我们最开始发布的测试组件保持一致,因为这个项目是我们用create-react-app新建的,所以检查一下package.json

{
  "name": "welkin-test",
  "version": "1.0.1", //这个分别代表  主版本号.次版本号.修订号
  "description": "基本布局",
 }
  1. 添加打包的出口文件
{
  "name": "welkin-test",
  "version": "1.0.1",
  "description": "基本布局",
  "main": "build/fui.js",
}
  1. 执行npm publish

五、测试发布的包

(1)下载

首先需要你另起一个项目,然后下载我们的包

npm install welkin-test

(2)配置组件

1. 引入Base组件

我在这里写了一个测试组件,如下:Fui.Base中是我瞎写的,你们随便写

import React from 'react';
import { FormOutlined, FileSyncOutlined } from '@ant-design/icons';
import { useLocation } from 'react-router-dom';
import Fui from '@didi/layout-face';
import { Row, Col, Statistic, Button, Descriptions, Badge, Timeline } from 'antd';
import logo from './assets/img/logo.jpg';

const Ceshi: React.FC = () => {
  const location = useLocation();
  const menuData = [
    {
      key: '/search/*',
      name: '菜单',
      icon: FormOutlined,
      type: 'link',
      children: [
        {
          key: '/search',
          name: '子菜单1',
          type: 'link',
        },
        {
          key: '/search/inactive',
          name: '子菜单2',
          type: 'link',
        },
        {
          key: '/search/match',
          name: '子菜单3',
          type: 'link',
        },
        {
          key: '/search/share',
          name: '子菜单4',
          type: 'link',
        },
      ],
    },
    {
      key: '/config',
      name: '菜单2',
      icon: FileSyncOutlined,
      type: 'link',
      children: [
        {
          key: '/config/inactive',
          name: '子菜单1',
          type: 'link',
        },
        {
          key: '/config/match',
          name: '子菜单2',
          type: 'link',
        },
      ],
    },
  ];
  return (
    <Fui.Base
      isFixed
      MenuData={menuData}
      location={location}
      logoUrl={logo}
      title="测试xxx系统"
      userName="welkin"
      logoutUrl="/logout/ceshi"
    >
      <Row gutter={16}>
        <Col span={12}>
          <Statistic title="Active Users" value={112893} />
        </Col>
        <Col span={12}>
          <Statistic title="Account Balance (CNY)" value={112893} precision={2} />
          <Button style={{ marginTop: 16 }} type="primary">
            Recharge
          </Button>
        </Col>
        <Col span={12}>
          <Statistic title="Active Users" value={112893} loading />
        </Col>
      </Row>
      <Row>
        <Descriptions title="User Info" bordered>
          <Descriptions.Item label="Product">Cloud Database</Descriptions.Item>
          <Descriptions.Item label="Billing Mode">Prepaid</Descriptions.Item>
          <Descriptions.Item label="Automatic Renewal">YES</Descriptions.Item>
          <Descriptions.Item label="Order time">2018-04-24 18:00:00</Descriptions.Item>
          <Descriptions.Item label="Usage Time" span={2}>
            2019-04-24 18:00:00
          </Descriptions.Item>
          <Descriptions.Item label="Status" span={3}>
            <Badge status="processing" text="Running" />
          </Descriptions.Item>
          <Descriptions.Item label="Negotiated Amount">$80.00</Descriptions.Item>
          <Descriptions.Item label="Discount">$20.00</Descriptions.Item>
          <Descriptions.Item label="Official Receipts">$60.00</Descriptions.Item>
          <Descriptions.Item label="Config Info">
            Data disk type: MongoDB
            <br />
            Database version: 3.4
            <br />
            Package: dds.mongo.mid
            <br />
            Storage space: 10 GB
            <br />
            Replication factor: 3
            <br />
            Region: East China 1<br />
          </Descriptions.Item>
        </Descriptions>
      </Row>
      <Row>
        <Timeline>
          <Timeline.Item color="green">Create a services site 2015-09-01</Timeline.Item>
          <Timeline.Item color="green">Create a services site 2015-09-01</Timeline.Item>
          <Timeline.Item color="red">
            <p>Solve initial network problems 1</p>
            <p>Solve initial network problems 2</p>
            <p>Solve initial network problems 3 2015-09-01</p>
          </Timeline.Item>
          <Timeline.Item>
            <p>Technical testing 1</p>
            <p>Technical testing 2</p>
            <p>Technical testing 3 2015-09-01</p>
          </Timeline.Item>
          <Timeline.Item color="gray">
            <p>Technical testing 1</p>
            <p>Technical testing 2</p>
            <p>Technical testing 3 2015-09-01</p>
          </Timeline.Item>
          <Timeline.Item color="gray">
            <p>Technical testing 1</p>
            <p>Technical testing 2</p>
            <p>Technical testing 3 2015-09-01</p>
          </Timeline.Item>
        </Timeline>
      </Row>
    </Fui.Base>
  );
};
export default Ceshi;

2. 在app中引入

import React, { FC } from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import style from './app.module.scss';
import Ceshi from './ceshi';

const App: FC = () => (
  <div className={style.app}>
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Ceshi />} />
      </Routes>
    </BrowserRouter>
  </div>
);

export default App;

3. 引入样式

当你启动项目看见没有报错,但却没有样式时,需要在index.tsx中引入样式

import ReactDOM from 'react-dom';
import React from 'react';
import App from './app';
//如下所示,因为基于的antd,所以也要引入antd
import 'antd/dist/antd.css';
import '@didi/layout-face/build/fui.css';

ReactDOM.render(<App />, document.getElementById('app'));

然后打开页面就可以看见我们的组件了,基本上已经完成了一个npm组件的发布

红色框内的就是准备开发的

(1)搭建基本组件

1. create-react-app直接创建组件即可

create-react-app xxxxxx --template typescript

2. 组件开发

其实可以先开发出自己的组件然后再分析,以我的为例子,主要分为三块组件:

  • header:主要是顶部,涉及到logo,title,username以及logoutUrl;
  • leftMenu:菜单信息;涉及到渲染的菜单数据,以及使用到的路由组件
  • base:需要整合header和leftMenu以及内容区域,以及是否涉及header头和leftMenu固定,这里使用fixed变量

(2)基本组件开发

我们将这三个组件放在一个目录下:

1. header组件开发

import React, { CSSProperties } from 'react';
import { Button, Dropdown, Layout, Menu } from 'antd';
import { DownOutlined } from '@ant-design/icons';
import './header.scss';
import getPrefixCls from '../_utils/get-prefix-cls'; //这个函数后面介绍

const { Header: LayoutHeader } = Layout;
//这里是header组件用到的props
export interface Props { 
  logoUrl?: string;
  title?: string;
  userName?: string;
  logoutUrl?: string;
  isFixed?: boolean;
}
const Header: React.FC<Props> = ({
  title,
  logoUrl,
  userName,
  logoutUrl,
  isFixed = false,
}) => {
  //根据是否固定做出的不一样的header style
  const style: CSSProperties = isFixed
    ? { position: 'fixed', zIndex: 3, width: '100%', backgroundColor: '#fff' }
    : { width: '100%', backgroundColor: '#fff' };
  const menu = (
    <Menu>
      <Menu.Item key="1">{logoutUrl && <a href={logoutUrl}>退出</a>}</Menu.Item>
    </Menu>
  );
  return (
    <LayoutHeader className={getPrefixCls('header')} style={style}>
      <div className={getPrefixCls('header__logo')}>
        {logoUrl && <img src={logoUrl} alt="logo" />}
        {title && <span>{title}</span>}
      </div>
      <nav className={getPrefixCls('header__nav')} />
      {userName && (
        <div className={getPrefixCls('header__user')}>
          <Dropdown overlay={menu}>
            <Button type="link">
              <span>{userName}</span>
              <DownOutlined />
            </Button>
          </Dropdown>
        </div>
      )}
    </LayoutHeader>
  );
};
export default Header;

header组件的scss如下:

@import '../style/var.scss'; //这个后面介绍

.#{$name}-header {
  display: flex;
  border-bottom: 1px solid #dcdcdc;

  &__logo {
    display: flex;
    justify-content: center;

    img {
      width: 75px;
      align-self: center;
    }

    span {
      font-size: 14px;
      font-family: unset;
      color: #383838;
    }
  }

  &__nav {
    flex: 1;
  }

  &__user {
    :global {
      .ant-avatar {
        cursor: pointer;
      }
    }
  }
}

2. leftMenu组件

页面组件:

import React, { useMemo, useCallback } from 'react';
import { Menu } from 'antd';
import { Link } from 'react-router-dom';
import './leftMenu.scss';
import { Location } from 'history';
import getPrefixCls from '../_utils/get-prefix-cls';

const { SubMenu } = Menu;
//菜单项结构
export interface MenuItem {
  key: string;
  name: string;
  icon?: any;
  type: string;
  children?: MenuItem[];
}
//leftMenu组件用到的props
export interface Props {
  location: Location;
  MenuData: MenuItem[];
}
const LeftMenu: React.FC<Props> = ({ location, MenuData }) => {
  const getDefaultOpenKeys = useCallback(
    (
      data: MenuItem[] = [],
      openKeys: string[] = [],
      parentKeys: string[] = []
    ): string[] =>
      data.reduce((prev, curr): any => {
        if (curr.key === location.pathname) {
          return [...prev, ...openKeys, curr.key, ...parentKeys];
        }
        if (curr.children && curr.children.length !== 0) {
          return [
            ...prev,
            ...getDefaultOpenKeys(curr.children, openKeys, [
              ...parentKeys,
              curr.key,
            ]),
          ];
        }
        return [...prev];
      }, []),
    [location.pathname]
  );
  const defaultOpenKeys = useMemo(() => getDefaultOpenKeys(MenuData), [
    getDefaultOpenKeys,
  ]);
  // 遍历定义的菜单
  const MenuList: JSX.Element[] = useMemo(
    () =>
      MenuData.map((item) => {
        if (item.children && item.children.length !== 0) {
          return (
            <SubMenu
              key={item.key}
              title={
                <span>
                  {item.icon &&
                    React.createElement(item.icon, {
                      style: { verticalAlign: 'middle' },
                    })}
                  <span>{item.name}</span>
                </span>
              }
            >
              {item.children.map((subItem) => (
                <Menu.Item key={subItem.key}>
                  {subItem.type === 'link' ? (
                    <Link to={subItem.key}>{subItem.name}</Link>
                  ) : (
                    <a target="_blank " href={subItem.key}>
                      {subItem.name}
                    </a>
                  )}
                </Menu.Item>
              ))}
            </SubMenu>
          );
        }
        return (
          <Menu.Item key={item.key}>
            {item.type === 'link' ? (
              <Link to={item.key}>
                {item.icon &&
                  React.createElement(item.icon, {
                    style: { verticalAlign: 'middle' },
                  })}
                <span>{item.name}</span>
              </Link>
            ) : (
              <a target="_blank " href={item.key}>
                {item.icon &&
                  React.createElement(item.icon, {
                    style: { verticalAlign: 'middle' },
                  })}
                {item.name}
              </a>
            )}
          </Menu.Item>
        );
      }),
    []
  );
  return (
    <Menu
      className={getPrefixCls('leftMenu')}
      theme="light"
      mode="inline"
      defaultSelectedKeys={[location.pathname]}
      defaultOpenKeys={defaultOpenKeys}
    >
      {MenuList}
    </Menu>
  );
};
export default LeftMenu;

用到的scss

@import '../style/var.scss';

.#{$name}-leftMenu {
  border-right: none;
}

.#{$name}-iconStyle {
  display: flex;
  align-items: center;
}

3. base组件

import React, { CSSProperties, useCallback, useState } from 'react';
import { Layout } from 'antd';
import Header from '../Header/header';
import LeftMenu, { MenuItem } from '../LeftMenu/leftMenu';
import { Location } from 'history';

const { Sider, Content } = Layout;
export interface Props {
  logoUrl?: string;
  title?: string;
  userName?: string;
  logoutUrl?: string;
  location: Location;
  MenuData: MenuItem[];
  isFixed?: boolean;
}
const Base: React.FC<Props> = ({
  children,
  MenuData,
  location,
  title,
  logoUrl,
  userName,
  logoutUrl,
  isFixed = false,
}) => {
  const [collapsed, setCollapsed] = useState(false);
  // 点击展开,再次点击关闭
  const handleTootle = useCallback(() => {
    setCollapsed((v) => !v);
  }, [setCollapsed]);
  const layoutStyle: CSSProperties = isFixed
    ? {
        paddingTop: '70px',
        overflow: 'auto',
        height: '100vh',
        position: 'fixed',
        left: 0,
        zIndex: 1,
      }
    : {};
  const contentStyle = isFixed
    ? {
        margin: '24px 16px',
        marginTop: 90,
        padding: 20,
        marginLeft: collapsed ? '100px' : '220px',
        background: '#fff',
      }
    : {
        margin: '24px 16px',
        padding: 20,
        background: '#fff',
      };
  return (
    <Layout style={{ minHeight: '100vh' }}>
      <Header
        title={title}
        logoUrl={logoUrl}
        logoutUrl={logoutUrl}
        userName={userName}
        isFixed={isFixed}
      />
      <Layout>
        <Sider
          collapsible
          collapsed={collapsed}
          onCollapse={handleTootle}
          theme="light"
          style={layoutStyle}
        >
          <LeftMenu MenuData={MenuData} location={location} />
        </Sider>
        <Layout>
          <Content style={contentStyle}>{children}</Content>
        </Layout>
      </Layout>
    </Layout>
  );
};

export default Base;

(3)主要函数和样式

因为在打包中,我们的包被引用之后可能出现css找不到或者被覆盖的情况,所以要给我们自己的css加一个前缀

1. getPrefixCls

该函数是给页面级组件加前缀

const getPrefixCls = (
  suffixCls: string,
  customizePrefixCls = 'fui'
): string => {
  return `${customizePrefixCls}-${suffixCls}`;
};
export default getPrefixCls;

2. Var.scss

这是个scss函数主要在组件scss中引入,给scss加前缀

$name:fui

(4)导出组件

import Base from './Base';
import Header from './Header';
import LeftMenu from './LeftMenu';

export { Base, Header, LeftMenu };
export default { Base, Header, LeftMenu };

其实这个时候也可以在app中引入自己写的组件看看是否无误,就比如我在这里写了一个测试组件,之后在app中引入就可以看到该组件的样式:

测试组件代码如下:

import React, { FC } from 'react';
import { Base } from './components';
import { FormOutlined, FileSyncOutlined } from '@ant-design/icons';
import { MenuItem } from './components/LeftMenu/leftMenu';
import { useLocation } from 'react-router-dom';

const Ceshi: React.FC = () => {
  const location = useLocation();
  const menuData: MenuItem[] = [
    {
      key: '/search/*',
      name: '账号审计',
      icon: FormOutlined,
      type: 'link',
      children: [
        {
          key: '/search',
          name: '账号全量查询',
          type: 'link',
        },
        {
          key: '/search/inactive',
          name: '账号不活跃查询',
          type: 'link',
        },
        {
          key: '/search/match',
          name: '一人多账号查询',
          type: 'link',
        },
        {
          key: '/search/share',
          name: '账号共享查询',
          type: 'link',
        },
      ],
    },
    {
      key: '/config',
      name: '账号审计配置',
      icon: FileSyncOutlined,
      type: 'link',
      children: [
        {
          key: '/config/inactive',
          name: '账号不活跃管理',
          type: 'link',
        },
        {
          key: '/config/match',
          name: '一人多账号报备',
          type: 'link',
        },
      ],
    },
  ];
  return (
    <Base
      MenuData={menuData}
      location={location}
      title={'ceshi'}
      userName={'welkin'}
      logoutUrl={'/logout/ceshi'}
    ></Base>
  );
};
export default Ceshi;

app组件如下:

import React from 'react';
import Ceshi from './ceshi';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
const App: React.FC = () => {
  return (
    <div className="App">
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<Ceshi />} />
        </Routes>
      </BrowserRouter>
    </div>
  );
};

export default App;

四、webpack打包配置

(1)yarn eject暴露出配置

yarn eject

(2)修改配置

1. 添加路径

在config/path中添加路径,因为我的组件是在components目录下的,所以添加

comIndexJs: resolveModule(resolveApp, 'src/components/index'),
 //这个地方就是我们的导出组件文件index

2. 修改webpack

//修改入口文件
entry: isEnvProduction ? paths.comIndexJs : paths.appIndexJs,
//修改output 部分
output:{
 //修改filename
 filename: isEnvProduction
        ? 'fui.js'
        : isEnvDevelopment && 'static/js/bundle.js',
     
 // 修改devtoolModuleFilenameTemplate
 devtoolModuleFilenameTemplate: isEnvProduction
   ? (info) =>
 path
   .relative(paths.appSrc, info.absoluteResourcePath)
   .replace(/\\/g, '/')
 : isEnvDevelopment &&
   ((info) =>
    path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
//添加如下:
library: 'fui',
libraryTarget: 'umd',
libraryExport: 'default',
}
//将 HtmlWebpackPlugin 设置为isEnvDevelopment 判断
plugins: [
  isEnvDevelopment &&
  new HtmlWebpackPlugin(
    Object.assign(xxxx)//....
  )
]
//增加 externals 里面的内容要和package.json中的peerDependencies挂钩
externals: {
  react: {
    commonjs: 'react',
    commonjs2: 'react',
    amd: 'react',
    root: 'React',
  },
  'react-dom': {
      commonjs: 'react-dom',
      commonjs2: 'react-dom',
      amd: 'react-dom',
      root: 'ReactDOM',
    },
    'react-router-dom': {
       commonjs: 'react-router-dom',
       commonjs2: 'react-router-dom',
       amd: 'react-router-dom',
       root: 'ReactRouterDom',
      },
},

(3)修改package.json

添加如下:

  "peerDependencies": {
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-router-dom": "^6.2.1"
  },

(4)发包

  1. 首先进行yarn build验证,如果没有问题
  2. 检查package.json

修改你的版本号【每一次npm publish都要在之前修改】,记住package.json重的name跟我们最开始发布的测试组件保持一致,因为这个项目是我们用create-react-app新建的,所以检查一下package.json

{
	"name": "welkin-test",
  "version": "1.0.1", //这个分别代表  主版本号.次版本号.修订号
  "description": "基本布局",
 }
  1. 添加打包的出口文件
{
	"name": "welkin-test",
  "version": "1.0.1",
  "description": "基本布局",
  "main": "build/fui.js",
}
  1. 执行npm publish

五、测试发布的包

(1)下载

首先需要你另起一个项目,然后下载我们的包

npm install welkin-test

(2)配置组件

1. 引入Base组件

我在这里写了一个测试组件,如下:Fui.Base中是我瞎写的,你们随便写

import React from 'react';
import { FormOutlined, FileSyncOutlined } from '@ant-design/icons';
import { useLocation } from 'react-router-dom';
import Fui from '@didi/layout-face';
import { Row, Col, Statistic, Button, Descriptions, Badge, Timeline } from 'antd';
import logo from './assets/img/logo.jpg';

const Ceshi: React.FC = () => {
  const location = useLocation();
  const menuData = [
    {
      key: '/search/*',
      name: '菜单',
      icon: FormOutlined,
      type: 'link',
      children: [
        {
          key: '/search',
          name: '子菜单1',
          type: 'link',
        },
        {
          key: '/search/inactive',
          name: '子菜单2',
          type: 'link',
        },
        {
          key: '/search/match',
          name: '子菜单3',
          type: 'link',
        },
        {
          key: '/search/share',
          name: '子菜单4',
          type: 'link',
        },
      ],
    },
    {
      key: '/config',
      name: '菜单2',
      icon: FileSyncOutlined,
      type: 'link',
      children: [
        {
          key: '/config/inactive',
          name: '子菜单1',
          type: 'link',
        },
        {
          key: '/config/match',
          name: '子菜单2',
          type: 'link',
        },
      ],
    },
  ];
  return (
    <Fui.Base
      isFixed
      MenuData={menuData}
      location={location}
      logoUrl={logo}
      title="测试xxx系统"
      userName="welkin"
      logoutUrl="/logout/ceshi"
    >
      <Row gutter={16}>
        <Col span={12}>
          <Statistic title="Active Users" value={112893} />
        </Col>
        <Col span={12}>
          <Statistic title="Account Balance (CNY)" value={112893} precision={2} />
          <Button style={{ marginTop: 16 }} type="primary">
            Recharge
          </Button>
        </Col>
        <Col span={12}>
          <Statistic title="Active Users" value={112893} loading />
        </Col>
      </Row>
      <Row>
        <Descriptions title="User Info" bordered>
          <Descriptions.Item label="Product">Cloud Database</Descriptions.Item>
          <Descriptions.Item label="Billing Mode">Prepaid</Descriptions.Item>
          <Descriptions.Item label="Automatic Renewal">YES</Descriptions.Item>
          <Descriptions.Item label="Order time">2018-04-24 18:00:00</Descriptions.Item>
          <Descriptions.Item label="Usage Time" span={2}>
            2019-04-24 18:00:00
          </Descriptions.Item>
          <Descriptions.Item label="Status" span={3}>
            <Badge status="processing" text="Running" />
          </Descriptions.Item>
          <Descriptions.Item label="Negotiated Amount">$80.00</Descriptions.Item>
          <Descriptions.Item label="Discount">$20.00</Descriptions.Item>
          <Descriptions.Item label="Official Receipts">$60.00</Descriptions.Item>
          <Descriptions.Item label="Config Info">
            Data disk type: MongoDB
            <br />
            Database version: 3.4
            <br />
            Package: dds.mongo.mid
            <br />
            Storage space: 10 GB
            <br />
            Replication factor: 3
            <br />
            Region: East China 1<br />
          </Descriptions.Item>
        </Descriptions>
      </Row>
      <Row>
        <Timeline>
          <Timeline.Item color="green">Create a services site 2015-09-01</Timeline.Item>
          <Timeline.Item color="green">Create a services site 2015-09-01</Timeline.Item>
          <Timeline.Item color="red">
            <p>Solve initial network problems 1</p>
            <p>Solve initial network problems 2</p>
            <p>Solve initial network problems 3 2015-09-01</p>
          </Timeline.Item>
          <Timeline.Item>
            <p>Technical testing 1</p>
            <p>Technical testing 2</p>
            <p>Technical testing 3 2015-09-01</p>
          </Timeline.Item>
          <Timeline.Item color="gray">
            <p>Technical testing 1</p>
            <p>Technical testing 2</p>
            <p>Technical testing 3 2015-09-01</p>
          </Timeline.Item>
          <Timeline.Item color="gray">
            <p>Technical testing 1</p>
            <p>Technical testing 2</p>
            <p>Technical testing 3 2015-09-01</p>
          </Timeline.Item>
        </Timeline>
      </Row>
    </Fui.Base>
  );
};
export default Ceshi;

2. 在app中引入

import React, { FC } from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import style from './app.module.scss';
import Ceshi from './ceshi';

const App: FC = () => (
  <div className={style.app}>
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Ceshi />} />
      </Routes>
    </BrowserRouter>
  </div>
);

export default App;

3. 引入样式

当你启动项目看见没有报错,但却没有样式时,需要在index.tsx中引入样式

import ReactDOM from 'react-dom';
import React from 'react';
import App from './app';
//如下所示,因为基于的antd,所以也要引入antd
import 'antd/dist/antd.css';
import '@didi/layout-face/build/fui.css';

ReactDOM.render(<App />, document.getElementById('app'));

然后打开页面就可以看见我们的组件了,基本上已经完成了一个npm组件的发布

你可能感兴趣的:(react,npm,react.js,npm,javascript)