React城市联动组件从开发到发布npm

695 阅读5分钟

项目开发中,选择城市功能算是一个常见功能,所以有必要封装一个组件方便团队其他人使用;本篇介绍使用webpack打包组件,webpack可以做代码拆分和运行时动态导入;如果要生成es module,可以选择rollup。

实现城市联动并发布自己的npm,主要包括以下:

  1. webpack配置
  2. 城市选择功能实现
  3. 发布到npm仓库

先看一下效果图

目录结构如下:

.
├── README.md
├── config          //webpack配置
│   ├── build.js    //生产打包脚本
│   ├── paths.js    //配置文件需要的路径
│   ├── start.js    //开发环境启动脚本
│   └── webpack.config.js
├── example         //开发调试🌰
│   ├── App.jsx
│   ├── favicon.ico
│   ├── index.html
│   └── index.js
├── package.json
├── postcss.config.js
├── src             //源码
│   ├── city.json   //省市区数据
│   ├── index.js
│   ├── pickerHeader.jsx
│   ├── pickerList.jsx
│   └── style.less
└── yarn.lock

webpack配置

关于配置的细节可以参考我之前写的一篇文章从npm init 搭建React企业级项目,当然组件开发和普通项目配置有一些差异,以下几点:

  1. output(输出文件)
output:{
  path:paths.appLib,
  filename:'index.js', //文件名不需要hash
  libraryTarget:'umd', // 模块规范
  library:'reactRegionPicker', //umd、amd 情况下需要指定名称
}
libraryTarget 描述
commonjs2 CommonJS规范;将库的返回值分配给 module.exports
amd RequireJS定义的模块规范
umd 是AMD和CommonJS的兼容模式或暴露给全局变量,跨平台的解决方案
  1. externals

有一些外部依赖包不需要打包,减少文件大小

const firstToUpperCase = str=>{
  return str.toLowerCase().split('-').map(item=>item.replace(/( |^)[a-z]/g, L => L.toUpperCase())).join('');
}

const transformExternals = str => ({
  root: firstToUpperCase(str),
  commonjs2: str,
  commonjs: str,
  amd: str,
  umd: str
})
{ // 定义外部依赖,避免把react和react-dom打包进去
  react: transformExternals('react'),
  "prop-types": transformExternals('prop-types'),
  "rc-toaster":transformExternals('rc-toaster')
}

城市选择功能实现

API

属性 说明 类型 默认值
regionList 数据源 Array<id,name,shortName,parentId,childRegionList> -
data 选中值 Array<number,number,number> -
maskCanClose 点击遮罩是否可以关闭 Boolean true
isShowPicker 主动显示 Boolean false
placeholder 默认显示文案 String '请选择城市'
onFinish 城市选择完成回调 Function -
onCancel 取消城市选择回调 Function -

入口文件

import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import Toast from 'rc-toaster';

import PickerList from './pickerList';
import PickerHeader from './pickerHeader';

import './style.less';

function CityPicker({
  data = [],
  regionList = [],
  maskCanClose = true,
  isShowPicker = false,
  placeholder = '请选择城市',
  onFinish,
  onCancel,
}) {
  let [isShow, setIsShow] = useState(isShowPicker); //显示城市与否
  let [tempRegionProvince, setTempRegionProvince] = useState([]); //展示省列表数据
  let [tempRegionCity, setTempRegionCity] = useState([]); //展示市列表数据
  let [tempRegionDistrict, setTempRegionDistrict] = useState([]); //展示区列表数据

  let [region, setRegion] = useState([]); //选择临时数据
  let [regionComplete, setRegionComplete] = useState([]); //选择完成数据
  let [selectTab,setSelectTab] = useState(data.length); //选中tab层级
  let [isComplete, setIsComplete] = useState(false); //是否选择完成

  let [showListTab,setShowListTab] = useState(data.length||1); //显示省市区其一列表
  
  useEffect(() => {
    const provinceList = findRegion(1);
    setTempRegionProvince(provinceList);
    if (data.length === 3) {
      const cityList = findRegion(2,data);
      const districtList = findRegion(3,data);
      const province = provinceList.find(item=>item.id === data[0]);
      const city = cityList.find(item=>item.id === data[1]);
      const district = districtList.find(item=>item.id === data[2]);

      const regionRes = [province,city,district].reduce((res,current,index)=>{
        res.push({
          selected:index === 2?true:false,
          id:current.id,
          parentId:current.parentId,
          name:current.name
        })
        return res
      },[]);

      setTempRegionCity(cityList);
      setTempRegionDistrict(districtList);
      setIsComplete(true);
      setRegion(regionRes);
      setRegionComplete(regionRes)
    }
  }, [data]);

  const _isArray = (arg) => {
    return Object.prototype.toString.call(arg) === '[object Array]'
  };
  const findRegion=(level,region)=>{
    //默认查询省
    if(level === 1){
      return _delChildRegionList(regionList);
    } else if(level === 2){
      const provinceId = _isArray(region)?region[0]:region.id;
      return regionList.find((item) => provinceId === item.id).childRegionList||[];
    } else if(level === 3){
      const provinceId = _isArray(region)?region[0]:region.parentId;
      const districtId = _isArray(region)?region[1]:region.id;
      return regionList.find((item) => provinceId == item.id).childRegionList.find((item) => districtId === item.id).childRegionList||[]||[];
    }
  }
  const _delChildRegionList=(arr)=>{
    let newArr = JSON.parse(JSON.stringify(arr));
    console.log(newArr)
    return newArr.map(item=>{
      delete item.childRegionList;
      return item;
    });
  }
  const handleRegion = (regionData,index) => {
    //返回市信息
      setIsComplete(false);
    if (index === 1) {
      setTempRegionDistrict([]);
      setTempRegionCity(findRegion(2,regionData));
    } else if(index === 2) {
      //返回区信息
      setTempRegionDistrict(findRegion(3,regionData));
    } else{
      setSelectTab(3);
    }
    setRegion(()=>{
      let newArr=region;
      newArr=newArr.map(item=>{
        item.selected = false;
        return item;
      })
      newArr=newArr.slice(0,index-1)
      newArr = [...newArr,{
        selected:index === 3?true:false,
        id:regionData.id,
        parentId:regionData.parentId,
        name:regionData.name
      }]
      return newArr;
    });
    if(index<3){setShowListTab(index+1);}
  };

  const changeTab=(item,index)=>{
    let oldSelectTab = selectTab;
    let newArr = region;
    if(oldSelectTab!==0){
      newArr[oldSelectTab-1].selected = false;
    }
    newArr[index].selected = true;
    setRegion(newArr);
    setSelectTab(index+1);
    setShowListTab(index+1);
  }
  const hideMask=()=>{
    resetRegion();
    if(maskCanClose){
      handleMask();
    }
  }
  const showPicker=()=>{
    setRegion(regionComplete);
    handleMask();
  }
  const handleMask=()=>{
    setIsShow(!isShow);
    if(typeof onCancel === 'function'){
      onCancel.call(this,region)
    }
  }
  const resetRegion=()=>{
    setRegion([]);
    setIsComplete(false);
  }
  const submitPicker=()=>{
    if(region.length!==3){
      return Toast.text('请完善城市信息');
    }
    setRegionComplete(region);
    setIsComplete(true);
    setRegion([]); //清空缓存选项
    setIsShow(!isShow);
    if(typeof onCancel === 'function'){
      onFinish.call(this,region);
    }
  }
  let ShowCity = (<span className="placeholder">{placeholder}</span>);
  if(regionComplete.length===3){
    ShowCity= (<span>{regionComplete.reduce((regionRes,current)=>{regionRes.push(current.name);return regionRes},[]).join(',')}</span>)
  }
  return (
    <div className="w-picker-container">
      <div onClick={showPicker} className="picker-region">
        {ShowCity}
      </div>
      <div className={"w-picker-popup"+(isShow?' show':' hide')}>
        <div onClick={hideMask} className="w-picker-mask"></div>
        <div className="w-picker-body">
          <PickerHeader tempSelectRegion={region} onSubmitPicker={submitPicker} onChangeTab={changeTab}/>
          <div className="w-picker">
              <PickerList showListTab={showListTab} showTabIndex={1} tempRegionList={tempRegionProvince} tempSelectRegion={region} onSelectRegion={handleRegion} />
              <PickerList showListTab={showListTab} showTabIndex={2} tempRegionList={tempRegionCity} tempSelectRegion={region} onSelectRegion={handleRegion} />
              <PickerList showListTab={showListTab} showTabIndex={3} tempRegionList={tempRegionDistrict} tempSelectRegion={region} onSelectRegion={handleRegion} />
          </div>
        </div>
      </div>
    </div>
  );
}

CityPicker.propTypes = {
  data: PropTypes.array, //选中值
  regionList: PropTypes.array.isRequired, //所有城市
  maskCanClose: PropTypes.bool, //点击遮罩是否可以关闭 true
  isShowPicker:PropTypes.bool, //默认是否显示 false
  placeholder:PropTypes.string, //默认显示文案
  onFinish: PropTypes.func, //城市选择完成回调
  onCancel: PropTypes.func, //取消城市选择回调
};

export default CityPicker;

pickerList 选择城市列表

function pickerList({showListTab,showTabIndex,tempRegionList,tempSelectRegion,onSelectRegion}) {
  const selectRegion = tempSelectRegion[showTabIndex-1];
  return (
    <ul className={'picker-list'+(showListTab===showTabIndex?' show':'')}>
      {tempRegionList.map(item => (
        <li
          className={(selectRegion && selectRegion.id === item.id) ? 'selected':''}
          onClick={() => {
            onSelectRegion(item,showTabIndex);
          }}
          key={item.id}
        >
          {item.name}
        </li>
      ))}
    </ul>
  )
}
pickerList.propTypes = {
  showListTab: PropTypes.number, //当前地区层级
  showTabIndex: PropTypes.number, //当前地区层级(标识)
  tempRegionList: PropTypes.array, //当前地区列表
  tempSelectRegion: PropTypes.array, //已选地区集合
  onSelectRegion: PropTypes.func, //选择后回调
};

pickerHeader 已选城市

const emptyArray = ['城市', '区县'];

function pickerHeader({
  tempSelectRegion,
  onSubmitPicker,
  onChangeTab
}) {
  return (
    <div className="w-picker-header">
      <div className="picker-header_tab">
        {/* <div className="empty-text">请选择</div> */}

        {tempSelectRegion.length ? (
          tempSelectRegion.map((item, index) => (
            <div key={item.id} className={'choose-item' + (item.selected ? ' selected' : '')} onClick={
              () => { 
                onChangeTab(item, index) 
                }
              }>
              <span>{item.name}</span>
            </div>
          ))
        ) : (
            <div className="no-data">请选择</div>
          )}
        {
          tempSelectRegion.length > 0 && tempSelectRegion.length < 3 && (<div className="choose-item selected">{emptyArray[tempSelectRegion.length - 1]}</div>)
        }
      </div>
      <div onClick={onSubmitPicker} className="picker-header_right">确定</div>
    </div>
  )
}
pickerHeader.propTypes = {
  tempSelectRegion: PropTypes.array, //已选地区集合
  onSubmitPicker: PropTypes.func, //提交回调
  onChangeTab: PropTypes.func, //切换省市区回调
};

发布npm

简单介绍下package.json的两个配置

属性 说明 配置
main 指定加载的入口文件 "lib/index.js"
files 需要发布的目录或文件 ["lib"]
npm build //打包
npm login //登录npm
npm publish //发布

使用

import CityPicker from 'react-region-picker';
import CityData from 'react-region-picker/lib/city.json';

<CityPicker regionList={CityData} data={[]} />

github地址:github.com/futurewan/r…