webpack 插件开发:实现阿里云oss的上传下载并自动引用cdn链接

1,425 阅读3分钟

阿里云 oss 概述

对象存储 OSS(Object Storage Service)是阿里云提供的海量、安全、低成本、高持久的云存储服务。其数据设计持久性不低于 99.9999999999%(12 个 9),服务设计可用性不低于 99.995% 。OSS 具有与平台无关的 RESTful API 接口,您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。

为什么要使用阿里云 oss

由于自身提供的服务器容量、速度有限,在数据量过于庞大的时候,自己搭建的服务器无法承受如此大的数据量,导致服务出现问题,因此可以选择把文件存储到 oss 上,每次请求数据通过 cdn 的形式获取。

为什么需要这个插件

为了减少重复工作。在每次 webpack build 时,把 build 的文件上传到 oss 中,并拿到 cdn 链接,并将其添加到生成的 index.html 中,如果每次发布产品的时候,都要进行这些步骤,那么无疑浪费了大把大把的摸鱼时间,加班、秃头接踵而来。将其打包为一个插件、一个自动化模块,每次打包的时候实现自动操作,那无疑能够减少相当多的工作量。

确认操作步骤

  • 拿到 OSS 对象
  • 删除目标 bucket 中原有文件 (delete function)
  • 将 build 文件夹中的 js 文件上传 (put function)
  • 获取所有文件的 cdn 路径 (signatureUrl function)
  • 加入到 build 文件夹下的 index.html

拿到 OSS 对象

  1. 阅读阿里云 oss 的官方文档,了解相关 jssdk:github.com/ali-sdk/ali…
  2. 首先需要安装 ali-oss
yarn add --dev ali-oss
  1. 根据文档提示,使用 OSS 创建实例对象
const OSS = require('ali-oss');
const client = new OSS({
  region: '<oss region>',
  accessKeyId: '<Your accessKeyId>',
  accessKeySecret: '<Your accessKeySecret>',
  bucket: '<Your bucket name>'
});
  • 此时就可以根据文档提供的 jssdk 来进行其他相关操作

删除目标 bucket 中原有文件

  1. 首先需要判断,需要删除哪些文件,
  • 使用 node.js 提供的 readdirSync 方法,获取目录下的所有文件和文件夹
this.readDirsAndFiles = fs.readdirSync(this.filesPath)
  • 使用 indexOf 判断文件是否是 .js 结尾的 js 文件
this.readJsFiles = this.readDirsAndFiles.filter(dir => dir.indexOf('.js') !== -1 && dir.indexOf('.json') === -1)
  • 由于 bucket 中存储的文件是没有 .js 等文件后缀的,因此需要做一个“去尾”工作
this.sliceReadFiles = this.readJsFiles.map(file => file.slice(0, -3))
  1. 根据文件前缀删除文件
async handleDel(name, options) {
  try {
    await this.client.delete(name)
  } catch (error) {
    error.failObjectName = name
    return error
  }
}

async deletePrefix(prefix) {
  const list = await this.client.list({
    prefix: prefix
  })
  list.objects = list.objects || []
  await Promise.all(list.objects.map((v) => this.handleDel(v.name)))
}

for (let i = 0; i < this.sliceReadFiles.length; i++) {
  this.deletePrefix(this.sliceReadFiles[i])
}

使用 ali-oss 提供的 put() sdk,将 build 文件夹中的 js 文件上传

async uploadFile(name) {
  await this.client.put(name, `${this.filesPath}/${name}.js`)
}

uploadFiles() {
  for (let i = 0; i < this.readJsFiles.length; i++) {
    const index = this.readJsFiles[i].indexOf('.js')
    const name = this.readJsFiles[i].slice(0, index)
    this.uploadFile(name)
  }
}

获取所有文件的 cdn 路径 (signatureUrl function)

async getFileUrl(name) {
  return await this.client.signatureUrl(name)
}

async getFilesUrls() {
  for (let i = 0; i < this.readJsFiles.length; i++) {
    const index = this.readJsFiles[i].indexOf('.js')
    const name = this.readJsFiles[i].slice(0, index)
    const fileUrl = await this.getFileUrl(name)
    this.fileUrlsList.push({name, url: fileUrl})
  }
}

加入到 build 文件夹下的 index.html,使用 node.js 的 readFile 和 writeFile 来读写文件

  1. 由于要获取 html 文件的 script 标签,暂时没有想到好的方法,直接使用字符串的 indexOf 方法来获取到 '<script src' 的位置,并以此为界限,分为 body 和 result 的部分
const index = data.toString().indexOf('<script src')
const body = data.toString().slice(0, index)
  1. 将通过 signatureUrl 方法获取到的 url 添加到 scriptsArr 数组中
let scriptsArr = []
if(fileUrlsList.length >= 1){
  for(let i=0; i < fileUrlsList.length; i ++){
    const url = fileUrlsList[i].url.toString()
    scriptsArr.push(`<script src='${url}'/></script>`)
  }
}
  1. 由于 slice 的切割,html 文件是缺少 闭合标签的,再次将其添加组合
const postfix = '</body></html>'
const result = body + scriptsArr.join('') + postfix
  1. 最后将其写入到 index.html 文件中
fs.writeFile(`${this.filesPath}/index.html`, result, (error) => {
  if(error) throw error;
  console.log('修改成功')
})

将其打包为一个 webpack 的插件

  1. 使用 class 类的形式,将代码放入 modules.js 中
class MyWebpackPlugin {
  constructor({filesPath, region, accessKeyId, accessKeySecret, bucket}) {
    this.fileUrlsList = []
    this.filesPath = filesPath
    this.client = new OSS({
      region: region,
      accessKeyId: accessKeyId,
      accessKeySecret: accessKeySecret,
      bucket: bucket,
    })
  }
    
  async apply(compiler) {
    compiler.hooks.afterEmit.tap('AliossWebpackPlugin', async (compilation) => {
      //读取 .js 结尾的相关文件
      this.readDirsAndFiles = fs.readdirSync(this.filesPath)
      this.readJsFiles = this.readDirsAndFiles.filter(dir => dir.indexOf('.js') !== -1 && dir.indexOf('.json') === -1)
      this.sliceReadFiles = this.readJsFiles.map(file => file.slice(0, -3))
      //删除 oss bucket 的文件
      await this.deleteAllPrefix()
      // 将源文件夹中的文件上传
      setTimeout(async() => {
        await this.uploadFiles()
      }, 1000)
      //获取所有文件的相关路径 url
      await this.getFilesUrls()
      // 更改路径
      await this.buildPath(this.fileUrlsList)
    })
  }
}
  1. 在 webpack.config.js 中导入
const AliosscdnWebpackPlugin = require('./module')

module.exports = {
    ...,
    plugins: [
    	...,
    	new AliosscdnWebpackPlugin({
            filesPath: <filesPath> // packaged path,
            region: '<Your oss region>',
            accessKeyId: '<Your oss accessKeyId>',
            accessKeySecret: '<Your oss accessKeySecret>',
            bucket: '<Your oss bucket name>'
    	})
    ]
}
  1. 由于考虑到 webpack.config.js 最终上传代码时也会上传到 github 中,因此需要考虑安全问题,将 oss 的相关信息 region、accessKeyId、accessKeySecret、bucket 属性单独放置到一个文件中,并将其放入 .gitignore 中。

将其发布到 npm 中

  • package.json
{
	"name": "my-webpack-plugin",
    "version": "0.0.1",
    ...
}
  • npm pulish

注意点

  • 由于这个 plugin 要实现的是,在 webpack 打包完成之后,再获取到打包后的文件,然后将其上传到 oss 中,获取到每个文件的 cdn,最后将 cdn 放入 index.html 中。因此,根据 webpack 生命周期,需要在其打包完成后,再执行相关操作,也就是 afterEmit 中。
  • 由于是在 webpack 打包完成后进行的操作,因此最后一步写入操作,不能通过设置 webpack 的 output 来进行写入,会出现获取到的文件夹为空的结果,因为此时打包还未完成,打包的文件夹为空。

github 地址:github.com/chainx-org/…