最近想用
flutter
作一個簡單的web後臺管理系統,須要文件上傳功能,無賴網上找了半天資料,沒有web版的上傳組件,只有本身動手豐衣足食了。html
既然是web,那天然想到的就是須要dart:html
這個包了,基本步驟主要有三個:web
不用設計了,將就看吧,咱們重在功能 api
首先導入包bash
import 'dart:html' as html;
import 'package:flutter/material.dart';
複製代碼
組件上面部分,點擊彈出選擇文件框app
var picker = InkWell(
hoverColor: Colors.blue[50],
splashColor: Colors.blue[10],
onTap: _selectFile,
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.blue),
borderRadius: BorderRadius.all(Radius.circular(5.0)),
),
padding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0),
child: Text(
'Click to select file',
style: TextStyle(fontWeight: FontWeight.w500, color: Colors.grey),
),
),
);
複製代碼
重點來了,下面是如何選擇文件的代碼:async
_selectFile() {
html.InputElement uploadInput = html.FileUploadInputElement();
uploadInput.multiple = false;
uploadInput.click();
uploadInput.onChange.listen((e) {
final files = uploadInput.files;
var fileItem = UploadFileItem(files[0]);
setState(() {
_files.add(fileItem);
});
});
}
複製代碼
這裏獲取到文件是一個dart:html
裏面的一個File
實例,自定義一個UploadFileItem
包裝一下:ide
import 'dart:html' as html;
enum FileStatus {
normal,
uploading,
success,
fail,
}
class UploadFileItem {
html.File file;
FileStatus fileStatus;
double progress = 0.0;
UploadFileItem(this.file) {
fileStatus = FileStatus.normal;
}
}
複製代碼
到這裏,如何選擇文件咱們已經會了,而且獲得了文件對象,那麼如何實現上傳呢?優化
網上查詢了好多資料,基本上都是用dart:html
裏面的FileReader
讀取文件後再經過http
或者dio
這個包上傳文件內容,可是要把文件內容都讀取出來,真的好麼?我要上傳的多是大文件,試了試,果然卡得起飛😂。ui
😔就快要放棄了this
最後仍是在StackOverflow
找到了一個條有用的信息
原來解決辦法仍是用dart:html
,這個包好強大有沒有,竟然有一個HttpRequest
,使用還黑方便😃
直接看代碼:
final html.FormData formData = html.FormData()..appendBlob('file', file);
var url = 'http://localhost:8088/api/file/upload';
html.HttpRequest.request(
url,
method: 'POST',
sendData: formData,
onProgress: onProgress,
).then((httpRequest) {
handleRequest(httpRequest);
}).catchError((e) {
setState(() {
widget.fileItem.fileStatus = FileStatus.fail;
});
});
複製代碼
啊,勝利的曙光終於來了,寫完代碼,跑一遍,速度夠快,運行很流暢。
完工!
最後附上源碼,總共三個文件:
file_item.dart
import 'dart:html' as html;
enum FileStatus {
normal,
uploading,
success,
fail,
}
class UploadFileItem {
html.File file;
FileStatus fileStatus;
double progress = 0.0;
UploadFileItem(this.file) {
fileStatus = FileStatus.normal;
}
}
複製代碼
upload_widget.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:html' as html;
import 'package:web_file_upload/file_item.dart';
import 'package:web_file_upload/file_item_widget.dart';
class UploadWidget extends StatefulWidget {
@override
_UploadWidgetState createState() => _UploadWidgetState();
}
class _UploadWidgetState extends State<UploadWidget> {
List<UploadFileItem> _files = [];
_selectFile() {
html.InputElement uploadInput = html.FileUploadInputElement();
uploadInput.multiple = false;
uploadInput.click();
uploadInput.onChange.listen((e) {
final files = uploadInput.files;
var fileItem = UploadFileItem(files[0]);
setState(() {
_files.add(fileItem);
});
});
}
@override
Widget build(BuildContext context) {
var picker = InkWell(
hoverColor: Colors.blue[50],
splashColor: Colors.blue[10],
onTap: _selectFile,
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.blue),
borderRadius: BorderRadius.all(Radius.circular(5.0)),
),
padding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0),
child: Text(
'Click to select file',
style: TextStyle(fontWeight: FontWeight.w500, color: Colors.grey),
),
),
);
var filesList = _files.map(_fileItem);
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[picker, SizedBox(height: 10.0), ...filesList],
);
}
_delete(UploadFileItem fileItem) {
print('delete ${fileItem.file.name}');
setState(() {
_files.remove(fileItem);
});
}
Widget _fileItem(UploadFileItem fileItem) {
return FileItemWidget(
fileItem: fileItem,
onDeleteFile: _delete,
);
}
}
複製代碼
file_item_widget.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:html' as html;
import 'package:web_file_upload/file_item.dart';
typedef void DeleteFile(UploadFileItem fileItem);
class FileItemWidget extends StatefulWidget {
final UploadFileItem fileItem;
final DeleteFile onDeleteFile;
const FileItemWidget({Key key, this.fileItem, this.onDeleteFile}) : super(key: key);
@override
_FileItemWidgetState createState() => _FileItemWidgetState();
}
class _FileItemWidgetState extends State<FileItemWidget> {
@override
void initState() {
super.initState();
// _readFile();
_sendFormData(widget.fileItem.file);
}
_sendFormData(final html.File file) async {
setState(() {
widget.fileItem.fileStatus = FileStatus.uploading;
});
final html.FormData formData = html.FormData()..appendBlob('file', file);
handleRequest(html.HttpRequest httpRequest) {
print('upload resp: ${httpRequest.responseText}');
switch (httpRequest.status) {
case 200:
setState(() {
widget.fileItem.fileStatus = FileStatus.success;
});
return;
default:
setState(() {
widget.fileItem.fileStatus = FileStatus.fail;
});
break;
}
}
onProgress(e) {
print('upload sending: ${e.loaded} ${e.total}');
double progress = e.lengthComputable ? (e.loaded * 100 ~/ e.total) / 100.0 : e.loaded / 100.0;
print('upload sending: $progress');
if (widget.fileItem.progress == progress) return;
setState(() {
widget.fileItem.progress = progress;
});
}
// var url = 'https://www.mocky.io/v2/5cc8019d300000980a055e76';
var url = 'http://localhost:8088/api/file/upload';
html.HttpRequest.request(
url,
method: 'POST',
sendData: formData,
onProgress: onProgress,
).then((httpRequest) {
handleRequest(httpRequest);
}).catchError((e) {
setState(() {
widget.fileItem.fileStatus = FileStatus.fail;
});
});
// final html.HttpRequest httpRequest = html.HttpRequest();
// httpRequest
// ..onProgress.listen(onProgress)
// ..onLoadEnd.listen((e) {
// handleRequest(httpRequest);
// })
// ..open('POST', url)
// ..send(formData);
}
@override
Widget build(BuildContext context) {
UploadFileItem fileItem = widget.fileItem;
var prefixIcon;
if (fileItem.fileStatus == FileStatus.uploading || fileItem.fileStatus == FileStatus.normal) {
prefixIcon = CupertinoActivityIndicator(
animating: true,
radius: 8.0,
);
} else {
prefixIcon = Icon(
Icons.attach_file,
color: Colors.lightBlue,
size: 20.0,
);
}
return InkWell(
hoverColor: Colors.blue[100],
onTap: () {},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
prefixIcon,
SizedBox(width: 8.0),
Expanded(
child: Text(
'${fileItem.file.name}',
style: TextStyle(color: fileItem.fileStatus == FileStatus.fail ? Colors.red : Colors.black87, fontSize: 18.0),
),
),
IconButton(
padding: EdgeInsets.all(2.0),
iconSize: 18.0,
icon: Icon(Icons.delete),
onPressed: () {
widget.onDeleteFile(fileItem);
},
color: Colors.red[500],
),
],
),
if (fileItem.fileStatus == FileStatus.uploading)
LinearProgressIndicator(
value: fileItem.progress,
backgroundColor: Colors.transparent,
),
],
),
),
);
}
}
複製代碼
功能算實現了,還有好多優化的地方,好比作一個統一平臺的插件,支持移動端,你們以爲有用就本身去優化吧😎