Kotlin x Nodejs 实现上传文件

Posted by rarnu on 04-22,2019

看这篇前,你需要有阅读过上一篇的基础,若是还未读过,建议先点我去读

在 Kotlin x Nodejs 体系下,实现上传文件是非常轻松的事,跟我一步步操作即可。

首先加入对 multerbody-parser 的依赖,完成后的 package.json 是这样的:

{
  "name": "ktnode",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "start": "node ./web/ktnode.js"
  },
  "author": "rarnu",
  "license": "GPLv3",
  "dependencies": {
    "kotlin": "^1.3.20",
    "express": "^4.15.4",
    "mongoose": "^4.11.7",
    "body-parser": "^1.15.2",
    "multer": "^1.4.1"
  }
}

然后进行依赖库的安装:

$ npm install

在以后的文章里,如果遇到修改 package.json 的场景,将不再提示你安装,这应当是一个类似于条件反射的,约定俗成的操作。

然后需要引用 multer,并实现对于上传目录的配置:

val multer = require("multer")
val upload = multer(js("({ dest: 'upload/'})"))

此处有一个小技巧,由于 multer 构造时需要传入一个 js 的 object,直接以 Kotlin 方式传参会引起错误,需要以 js 的方式来传,因此我们有两种方法来构造这样的参数:

val obj = js("({key: value, key2:value2, ... })")
multer(obj)

另一种方法是:

val obj: dynamic = object {}
obj["key"] = value
... ...
multer(obj)

初学者容易犯的错误是将以上代码写成:

multer(dest = "upload/")

这样是会报错的,在 Kotlin Javascript 体系内,必须按 js 的要求构造和传参,这点和 Kotlin 本身的写法并不一致。

然后我们可以完成服务器端的请求接受:

app.post("/upload", upload.single("file")) { req, resp ->
    println(req.file.path)
    resp.end()
}

此处可以看到上传的临时文件的路径,需要注意的是,在请求的最后需要使用 resp.end() 来通知请求已被处理完。不然如果是在浏览器内上传文件,你将会看到小圆圈一直在转,一直是上传中的状态。当然了,如果最后有数据返回,代码包含 resp.send() 的情况,可以不写 resp.end(),因为 send 方法自身带了 end 的操作。

对于 req.file 类型,其内部字段如下表所示:

字段含义
fieldname请求时传入的字段名
originalname请求时传入的原始文件名
encoding文件的编码
mimetypeMIMETYPE,通常是 application/octet-stream
destination临时目录,不含文件名
filename临时文件名,不含目录
path临时文件完整路径,含目录和文件名
size文件大小,单位为 byte

上述例子是上传单个文件,Nodejs 同样允许上传多个文件,如下:

app.post("/upload", upload.array("file", 2)) { req, resp ->
    println(req.files.length)
    req.files.forEach { f -> println(f.path) }
    resp.end()
}

这里就是允许上传2个文件,并且可以通过 forEach 进行遍历。

除此之外,我们还有需要将上传的文件保存到自己想要的目录的情况,这里提供一个简单的做法:

app.post("/upload", upload.single("file")) { req, resp ->
    loadFile(req.file.path) { c ->
        saveFile("files/${uuid()}", c) { succ ->
            println(if (succ) "succ" else "fail")
        }
    }
    resp.end()
}

最后,为了统一各种上传请求,在路由层面让开发者可以更快的编写代码,进行一定的抽象:

fun routing(path: String, method: String = "get", block: (req: dynamic, resp: dynamic) -> Unit) =
    when (method.toLowerCase()) {
        "get" -> app.get(path) { req, resp -> block(req, resp) }
        "post" -> app.post(path) { req, resp -> block(req, resp) }
        else -> {
        }
    }

fun routing(path: String, fileField: String = "file", isArray: Boolean = false, arraySize: Int = 0, block: (req: dynamic, file: dynamic, resp: dynamic) -> Unit) =
    if (isArray) {
        app.post(path, upload.array(fileField, arraySize)) { req, resp -> block(req, req.files, resp) }
    } else {
        app.post(path, upload.single(fileField)) { req, resp -> block(req, req.file, resp) }
    }

这样我们在接受一个上传文件的请求时,就可以写成这种很简单的形式了:

// 上传单个文件
routing("/upload", isArray = false) { _, file, resp ->
    println(file.path)
    resp.end()
}

// 上传多个文件
routing("/upload2", isArray = true, arraySize = 2) { _, file, resp ->
    file.forEach { f -> println(f.path) }
    resp.end()
}