unjs/nitro - 速查攻略不废话之文件下载

854 阅读2分钟

跨域与预载

让我们首先解决跨域与预载问题。

跨域请见unjs/nitro - 速查攻略不废话之跨域 - 掘金 (juejin.cn),根据这篇文章,我们可以在中间件中一起把预载也处理了:

export default defineEventHandler((e) => {
  handleCors(e, {
    origin: '*',
    methods: '*',
    allowHeaders: '*',
  })
  if (e.node.req.method === 'OPTIONS') {
    return null
  }
})

如果返回值是null,则unjs/h3会设定status为204 - no content,以解决预载问题。

静态资源

配置一下静态资源的driver:

// nitro.config.ts

export default defineNitroConfig({
  serverAssets: [
    {
      baseName: 'files',
      dir: '/publicAssets',
    },
  ],
})

这里的baseName是你给这个storage起的名字,dir是磁盘中的路径,对于这个配置,你可以像这样来使用他:useStorage('assets:files')

下载逻辑

这里使用了mimeunjs/pathe,其中mime用于根据后缀生成content-type,是个非常好用的包。pathe在这里只用来获取文件后缀,你也可以手写这个功能,只是他不是重点,在此不赘述。

直接上代码,接下来我会逐一解释:

// routes/foo.ts

import mime from 'mime'
import { extname } from 'pathe'

export default defineEventHandler(async (e) => {
  const name = getQuery<{ filename: string }>(e).filename

  const file = await useFilesStorage().getItemRaw(name)

  appendResponseHeaders(e, {
    'content-type': mime.getType(extname(name)),
    'content-disposition': `attachment; filename=${name}`,
  })

  return file
})

const name = getQuery<{ filename: string }>(e).filename

这里通过getQuery方法,从前端的query中获取下载的文件名。

const file = await useFilesStorage().getItemRaw(name)

在这里获取文件。其中useFilesStorage是自定义的工具函数,其源码很简单:
const useFilesStorage = () => useStorage('assets:files')
这样就不需要你每次都敲storage名了。

值得一提的是,useStorage来自unjs/unStorage,其中storage.getItem函数所得到的结果是被格式化后的结果,比如文件会被读取为字符串。如果你想到读取文件本身,而非文件内容(比如你想获取一个txt的文件内容可以用getItem),应该使用storage.getItemRaw

appendResponseHeaders

在这里添加了必要的响应头。content-type用来表示媒体类型,这里使用mime生成,content-disposition中,attachment用于指出这是需要被下载的文件,filename很显然表示文件名。为了方便前端通过content-disposition响应头获取到文件名,你还需要配置一个Access-Control-Expose-Headers响应头,这会在后面提到。

最后,直接返回getItemRaw的结果即可。其结果一般是个Buffer。

Access-Control-Expose-Headers

根据浏览器安全策略,不是所有headers都可以在前端的js中获取到,其中也包括了content-disposition。为了方便前端获取到文件名,使用如下代码实现:

// middleware/exposeHeaders.ts
// 我把它放在中间件中,这样比较方便

export default defineEventHandler((e) => {
  setResponseHeaders(e, {
    'Access-Control-Expose-Headers': ['content-disposition'],
  })
})

Access-Control-Expose-Headers头中指出你需要暴露content-disposition头即可。

报告完毕!