React元件之間互相傳遞資料
在react中,子產品元件化了之後,最常使用的就是兩個元件之間互相傳遞資料,其中就包括父元件向子元件傳遞資料和子元件向父元件傳遞資料兩種情景,下面通過前幾天寫的一個上傳圖檔時截圖的元件為例子,示範元件之間的資料傳遞。
-
父元件向子元件傳遞參數
父元件是一個标簽管理的頁面,上傳标簽圖檔的時候引用了上傳圖檔的元件,父元件向子元件傳遞參數通過props傳遞,傳遞過去的參數有pattern模式,width裁剪框的寬度,height裁剪框的高度,onChange回調函數,fileList初始的圖檔清單。
//父元件LabelManageEdit.js代碼片
<CropperUpload
pattern={1}//模式分為三種,1為隻能上傳一張圖檔,2為傳一張圖檔,并且可以預覽,3為傳多張圖檔
width={375}
height={120}
onChange={this.ChangeIconUrl.bind(this)}
fileList={this.state.iconUrl}
/>
子元件接收參數時,在constructor構造函數中對父元件傳遞過來的參數進行綁定,調用super(props)來擷取props,再通過props.指定的父元件傳過來的參數來綁定給子元件的state上。如下為子組接收父元件參數代碼。
//子元件CropperUpload.js代碼片
constructor(props) {
super(props);
this.state = {
width: props.width,
height: props.height,
pattern: props.pattern,
fileList: props.fileList ? props.fileList : [],
editImageModalVisible: false,
srcCropper: '',
selectImgName: '',
}
}
constructor構造函數隻會在首次建立子元件的執行個體的時候調用一次,後來頁面在進行重新的渲染的時候将不會在調用該構造函數,是以這種方式不适用于當父元件傳遞過來得參數是變化的情況,在該例子中,父元件向子元件傳遞參數fileList是從背景異步擷取來的。是以當通過fileList={this.state.fileList}向子元件傳遞該圖檔清單參數時,起初是為空數組,在後來請求資料回來重新加載頁面的時候才會或得到該資料。
//父元件LabelManageEdit.js代碼片
componentWillMount() {
const { dispatch } = this.props;
if (this.props.location.params) {
this.setState({
labelName: this.props.location.params.record.productName,
status: this.props.location.params.record.isActive,
});
if(this.props.location.params.record.iconUrl){
let iconUrl=[];
iconUrl.push({
uid: -1,
name: 'iconUrl.png',
status: 'done',
url:this.props.location.params.record.iconUrl,
});
this.setState({
iconUrl,
});
}
}
}
當是以上情況的時候,子元件在綁定父元件參數的時候就需要用componentWillReceiveProps函數,當props發生變化時執行,初始化render時不執行,在這個回調函數裡面,你可以根據屬性的變化,通過調用this.setState()來更新你的元件狀态,舊的屬性還是可以通過this.props來擷取,這裡調用更新狀态是安全的,并不會觸發額外的render調用。
//子元件CropperUpload.js代碼片
componentWillReceiveProps(nextProps) {
if ('fileList' in nextProps) {
this.setState({
fileList: nextProps.fileList ? nextProps.fileList : [],
});
}
}
以上就是父元件向子元件傳遞參數的方法。
2. 子元件向父元件傳遞參數
在這裡主要是子元件進行圖檔裁剪或者圖檔删除的時候,fileList發生變化,希望将這種變化同步到父元件中,同時更新父元件的state裡面的fileList,最開始的時候嘗試用ref來擷取元件的實時state的狀态。采用ref=“cropperUpload”也就是字元串的方式來挂載元件執行個體。最後取出執行個體中的state。列印結果。發現不能取出來子元件的state,隻能擷取到子元件的props。查閱資料說這種通過ref="字元串"的執行個體化方式已經廢棄掉。
是以就想了另一種回調函數的方法,當子元件的狀态發生變化,觸發回調函數,回調函數将子元件的state傳遞給父元件實作更新的目的。這裡将父元件的ChangeIconURL函數傳遞給子元件,并綁定到子元件的onChange函數上。
//父元件LabelManageEdit.js代碼片
<CropperUpload
pattern={1}//模式分為三種,1為隻能上傳一張圖檔,2為傳一張圖檔,并且可以預覽,3為傳多張圖檔
width={375}
height={120}
onChange={this.ChangeIconUrl.bind(this)}
fileList={this.state.iconUrl}
/>
在子元件中,fileList發生變化有兩處。上傳圖檔的回調,和删除圖檔的回調都會改變子元件fileList的狀态,是以在這兩個函數的最後,調用這個onChange函數,将更新後的fileList傳遞給父元件。以下為子元件中删除圖檔回調。調用了由父元件傳遞進來的回調函數this.prop.onChange(fileList);
//子元件CropperUpload.js代碼片
handleRemove(file) {
this.setState((state) => {
const index = state.fileList.indexOf(file);
const newFileList = state.fileList.slice();
newFileList.splice(index, 1);
this.props.onChange(newFileList);
return {
fileList: newFileList,
};
});
}
父元件在接收到這個fileList之後,更新父元件的state中的fileList。進而實作子元件向父元件傳遞參數
//父元件LabelManageEdit.js代碼片
//标簽内容圖檔的回調函數
ChangeIconUrl = (value) => {
this.setState({
iconUrl: value,
})
}
-
完整代碼
父元件LabelManageEdit.js代碼
import React, { PureComponent } from 'react';
import { routerRedux, Route, Switch } from 'dva/router';
import { Upload,Spin, Card, Button, Form, Icon, Col, Row, DatePicker, TimePicker, Input, Select, Popover } from 'antd';
import { connect } from 'dva';
import PageHeaderLayout from '../../../layouts/PageHeaderLayout';
import PartModal from '../../../components/Part/partModal';
import { message } from 'antd';
import stylesDisabled from './AddPart.less';
const { Option } = Select;
const { RangePicker } = DatePicker;
const FormItem = Form.Item;
import CropperUpload from '../../../components/Upload/CropperUpload';
const fieldLabels = {
name: '标簽名稱',
isActive: '狀态',
};
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 7 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 },
md: { span: 5 },
},
};
const submitFormLayout = {
wrapperCol: {
xs: { span: 24, offset: 0 },
sm: { span: 10, offset: 7 },
},
};
@connect(state => ({
changeData: state.modifyDataCQX.changeData,
submitLoading:state.modifyDataCQX.loading,
}))
@Form.create()
export default class LabelManageNew extends PureComponent {
constructor(props) {
super(props);
this.state = {
labelName: "",
status: "",
iconUrl:[],
}
}
componentWillMount() {
const { dispatch } = this.props;
if (this.props.location.params) {
this.setState({
labelName: this.props.location.params.record.productName,
status: this.props.location.params.record.isActive,
});
if(this.props.location.params.record.iconUrl){
let iconUrl=[];
iconUrl.push({
uid: -1,
name: 'iconUrl.png',
status: 'done',
url:this.props.location.params.record.iconUrl,
});
this.setState({
iconUrl,
});
}
}
}
returnClick = () => {
this.props.history.goBack();
}
validate = () => {
const { form, dispatch, submitting } = this.props;
const { getFieldDecorator, validateFieldsAndScroll, getFieldsError } = form;
validateFieldsAndScroll((error, values) => {
if (!error) {
let iconUrl='';
if (this.state.iconUrl.length) {
iconUrl=this.state.iconUrl[0].url;
}
else{
message.error("請添加分區圖示");
return;
}
values.iconUrl=iconUrl;
var data={
ID:this.props.location.params.record.id,
operatorName:this.props.location.params.record.operatorName,
labelName:values.labelName,
status:values.status,
iconURL:values.iconUrl,
}
dispatch({
type: 'modifyDataCQX/editLabelManagementFunc',
payload: data,
callback: () => {
const { changeData } = this.props;
if (changeData.success === true) {
message.success(changeData.msg);
this.props.history.goBack();
} else {
message.error(changeData.msg);
}
},
});
}
});
}
//标簽内容圖檔的回調函數
ChangeIconUrl = (value) => {
this.setState({
iconUrl: value,
})
}
render() {
const { form, dispatch } = this.props;
const { submitLoading } = this.props;
const { getFieldDecorator, validateFieldsAndScroll, getFieldsError } = form;
return (
<PageHeaderLayout >
<Card bordered={false} title="标簽資訊">
<Form hideRequiredMark className={stylesDisabled.disabled}>
<FormItem {...formItemLayout} label={fieldLabels.name} >
{getFieldDecorator('labelName', {
rules: [{ required: true, message: '請輸入标簽名稱' }],
initialValue: this.state.labelName,
})
(<Input placeholder="請輸入标簽名稱" />)
}
</FormItem>
<FormItem {...formItemLayout} label={fieldLabels.isActive} >
{getFieldDecorator('status', {
rules: [{ required: true, message: '請輸入狀态' }],
initialValue: this.state.status,
})
(<Select style={{ width: 100 }}>
<Option value={true}>有效</Option>
<Option value={false}>無效</Option>
</Select>)
}
</FormItem>
<FormItem {...formItemLayout} label='标簽圖示'>
<CropperUpload
pattern={1}//模式分為三種,1為隻能上傳一張圖檔,2為傳一張圖檔,并且可以預覽,3為傳多張圖檔
width={375}
height={120}
onChange={this.ChangeIconUrl.bind(this)}
fileList={this.state.iconUrl}
/>
</FormItem>
<FormItem {...submitFormLayout} style={{ marginTop: 32 }}>
<Button type="primary" onClick={this.validate} loading={submitLoading}>
送出 </Button >
< Button style={{ marginLeft: 8 }} onClick={this.returnClick} >
傳回
</Button >
</FormItem>
</Form>
</Card>
</PageHeaderLayout>
);
}
}
子元件CropperUpload.js代碼
import React, { PureComponent } from 'react';
import moment from 'moment';
import { routerRedux, Route, Switch, Link } from 'dva/router';
import { Upload, Button, Modal, Icon, message } from 'antd';
import "cropperjs/dist/cropper.css"
import Cropper from 'react-cropper'
import { connect } from 'dva';
import styles from './Upload.less';
@connect(state => ({
imagePictures: state.getData.imagePictures,
}))
class CropperUpload extends PureComponent {
constructor(props) {
super(props);
this.state = {
width: props.width,
height: props.height,
pattern: props.pattern,
fileList: props.fileList ? props.fileList : [],
editImageModalVisible: false,
srcCropper: '',
selectImgName: '',
}
}
componentWillReceiveProps(nextProps) {
if ('fileList' in nextProps) {
this.setState({
fileList: nextProps.fileList ? nextProps.fileList : [],
});
}
}
handleCancel = () => {
this.setState({
editImageModalVisible: false,
});
}
// 特産介紹圖檔Upload上傳之前函數
beforeUpload(file, fileList) {
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) { //添加檔案限制
MsgBox.error({ content: '檔案大小不能超過10M' });
return false;
}
var reader = new FileReader();
const image = new Image();
var height;
var width;
//因為讀取檔案需要時間,是以要在回調函數中使用讀取的結果
reader.readAsDataURL(file); //開始讀取檔案
reader.onload = (e) => {
image.src = reader.result;
image.onload = () => {
height = image.naturalHeight;
width = image.naturalWidth;
if (height < this.state.height || width < this.state.width) {
message.error('圖檔尺寸不對 寬應大于:'+this.state.width+ '高應大于:' +this.state.height );
this.setState({
editImageModalVisible: false, //打開控制裁剪彈窗的變量,為true即彈窗
})
}
else{
this.setState({
srcCropper: e.target.result, //cropper的圖檔路徑
selectImgName: file.name, //檔案名稱
selectImgSize: (file.size / 1024 / 1024), //檔案大小
selectImgSuffix: file.type.split("/")[1], //檔案類型
editImageModalVisible: true, //打開控制裁剪彈窗的變量,為true即彈窗
})
if (this.refs.cropper) {
this.refs.cropper.replace(e.target.result);
}
}
}
}
return false;
}
handleRemove(file) {
this.setState((state) => {
const index = state.fileList.indexOf(file);
const newFileList = state.fileList.slice();
newFileList.splice(index, 1);
this.props.onChange(newFileList);
return {
fileList: newFileList,
};
});
}
//将base64碼轉化成blob格式
convertBase64UrlToBlob(base64Data) {
var byteString;
if (base64Data.split(',')[0].indexOf('base64') >= 0) {
byteString = atob(base64Data.split(',')[1]);
} else {
byteString = unescape(base64Data.split(',')[1]);
}
var mimeString = base64Data.split(',')[0].split(':')[1].split(';')[0];
var ia = new Uint8Array(byteString.length);
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ia], { type: this.state.selectImgSuffix });
}
//将base64碼轉化為FILE格式
dataURLtoFile(dataurl, filename) {
var arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, { type: mime });
}
_ready() {
this.refs.cropper.setData({
width: this.state.width,
height: this.state.height,
});
}
saveImg() {
const { dispatch } = this.props;
var formdata = new FormData();
formdata.append("files", this.dataURLtoFile(this.refs.cropper.getCroppedCanvas().toDataURL(), this.state.selectImgName));
dispatch({
type: 'getData/postimage',
payload: formdata,
callback: () => {
const { success, msg, obj } = this.props.imagePictures;
if (success) {
let imageArry = this.state.pattern == 3 ? this.state.fileList.slice() : [];
imageArry.push({
uid: Math.random() * 100000,
name: this.state.selectImgName,
status: 'done',
url: obj[0].sourcePath,
// url:obj[0].sourcePath,
thumbUrl: this.refs.cropper.getCroppedCanvas().toDataURL(),
thumbnailPath: obj[0].thumbnailPath,
largePath: obj[0].largePath,
mediumPath: obj[0].mediumPath,
upload: true,
})
this.setState({
fileList: imageArry,
editImageModalVisible: false, //打開控制裁剪彈窗的變量,為true即彈窗
})
this.props.onChange(imageArry);
}
else {
message.error(msg);
}
},
});
}
render() {
const botton = this.state.pattern == 2 ?
<div>
<Icon type="plus" />
<div className="ant-upload-text">Upload</div>
</div> :
<Button>
<Icon type="upload" />選擇上傳</Button>
return (
<div>
<Upload
name="files"
action="/hyapi/resource/image/multisize/upload"
listType={this.state.pattern === 1 ? " " : this.state.pattern === 2 ? "picture-card" : "picture"}
className={this.state.pattern === 3 ? styles.uploadinline : ""}
beforeUpload={this.beforeUpload.bind(this)}
onRemove={this.handleRemove.bind(this)}
fileList={this.state.fileList}
>
{botton}
</Upload>
<Modal
key="cropper_img_icon_key"
visible={this.state.editImageModalVisible}
width="100%"
footer={[
<Button type="primary" onClick={this.saveImg.bind(this)} >儲存</Button>,
<Button onClick={this.handleCancel.bind(this)} >取消</Button>
]}>
<Cropper
src={this.state.srcCropper} //圖檔路徑,即是base64的值,在Upload上傳的時候擷取到的
ref="cropper"
preview=".uploadCrop"
viewMode={1} //定義cropper的視圖模式
zoomable={true} //是否允許放大圖像
movable={true}
guides={true} //顯示在裁剪框上方的虛線
background={false} //是否顯示背景的馬賽克
rotatable={false} //是否旋轉
style={{ height: '100%', width: '100%' }}
cropBoxResizable={false}
cropBoxMovable={true}
dragMode="move"
center={true}
ready={this._ready.bind(this)}
/>
</Modal>
</div>
);
}
}
export default CropperUpload;