项目开发中,选择城市功能算是一个常见功能,所以有必要封装一个组件方便团队其他人使用;本篇介绍使用webpack打包组件,webpack可以做代码拆分和运行时动态导入;如果要生成es module,可以选择rollup。
实现城市联动并发布自己的npm,主要包括以下:
- webpack配置
- 城市选择功能实现
- 发布到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企业级项目,当然组件开发和普通项目配置有一些差异,以下几点:
- 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的兼容模式或暴露给全局变量,跨平台的解决方案 |
- 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…