auto-export-plugin升級改造

本文主要是對auto-export-plugin的進一步補充和改造,若是你沒看過上一篇,請看這裏 記開發一個webpack插件的心路歷程javascript

這兩天在使用auto-export-plugin使用過程當中,用起來有點不舒服,遂進行了以下部分改造。java

1、改造先後對比

改造點一

改造前 node

改造後webpack

這裏解釋一下。 圖一雖然也實現了對export的收集並導出,單純看起來功能沒什麼問題,可是真正引用模塊時感受有些雞肋。以下git

import component from './component'
const { Table } = component
複製代碼

還須要再解構一下才能取到對應的組件, 用起來實在麻煩。github

寫這個插件的初衷就是爲了減小代碼的搬運,提升開發效率,這很顯然與目的不符。web

究其緣由是由以下寫法import A, { B } from './test'; export default { A, B }形成的, 最後導出的是一個Object,因此確定須要解構才能使用。bash

因此抽時間又進行了改造,效果如圖二所示, 在應用時能夠直接解構導出,以下:post

import { Table } from './component'
複製代碼

插件沒有直接用export * from './test'處理, 主要目的把變量名直接導出顯得更直觀,並且在多人維護項目時也能很清楚知作別人寫的模塊導出了哪些變量ui


改造點二

改造前

文件改動時,只會導出變量名寫入同級目錄的index.js文件中, 只能適用以下目錄

|--constant
    |--index.js
    |--common.js
    |--user.js
複製代碼

可是在寫組件時,咱們會進一步對組件按目錄劃分,以上應用場景明顯存在缺陷

改造後 新增了對多級目錄的支持。

|——components
    |--index.js
    |——Table
        |--index.js
        |--TableHead.js
        |--TableBody.js
    |--Form
        |--index.js
        |--FormItem.js
複製代碼

當TableHead改動時, 不只會自動導入到Table/index.js文件中

export default () => {}
export { default as TableHead } from './TableHead'
複製代碼

同時會把Table/index.js文件的改動向上層目錄/components/index.js自動導入

export { default as Table, TableHead } from './table'
複製代碼

這樣也免得咱們在寫組件時手動導入到上級目錄了。


2、改造點源碼解析

改造點一

  1. 改造import A, { B } from './test'; export default { A, B } 這樣的寫法,總共須要三大步(假設變化的文件爲test)

    • 找到原來的導入語句,把變量收集起來爲[A, B], 同時把import A, { B } from './test'語句刪除。
    • 過濾掉在export default語句中上一步收集的變量[A, B], 這裏只能先用過濾,而不能直接刪除(有可能還有其餘導出的變量)
    • 插入export { default as A, B } from './test'語句
  2. 若是原來不存在對應文件的導入和導出語句import { B } from './test'export { B } from './test',則須要在文件適當位置(不能插入在文件頂部,防止後面有import語句形成語法錯誤)插入導出語句export { B } from './test'

  3. 若是存在導出語句export { B } from './test',而且文件的導出語句有變化,則將該條語句替換export { defualt as A, B } from './test'

replaceContent(replaceFilePath, changedFileName, nameMap) {
    const ast = this.getAst(replaceFilePath);
    // 記錄是否存在export { xxx } from './xxx'
    let existedExport = false 
    let changed = false
    const relPath = `./${changedFileName}`
    let oldImportNames = []
    const exportExpression = t.exportNamedDeclaration(null, this.createExportDeclatationSpecifiers(nameMap), t.stringLiteral(relPath))
    traverse(ast, {
      Program: {
        exit(path) {
            //若是不存在則在最後一條語句插入
          if (!existedExport) {
            changed = true
            path.pushContainer('body', exportExpression)
          }
        }
      },
      ImportDeclaration(path) {
        if (path.node.source.value === relPath) {
          // 若是存在import xxx, { xxx } from relPath, 把舊的變量收集起來而且檢測export語句把這些變量刪除。 同時新增export { xx } from relPath
          oldImportNames = path.node.specifiers.reduce((prev, cur) => {
            if (t.isImportSpecifier(cur) || t.isImportDefaultSpecifier(cur)) {
              return [...prev, cur.local.name];
            }
            return prev;
          }, []);
          changed = true
          path.remove()
        }
      },
      ExportNamedDeclaration(path) {
        if (!existedExport && path.node.source && path.node.source.value === relPath) {
          existedExport = true
          changed = true
          if (_.isEmpty(nameMap)) {
            // 說明沒有變量導出或者文件刪除, 因此刪除該條語句
            path.remove()
          } else {
            path.replaceWith(exportExpression)
          }
        }
      },
      // 針對export { A, B }的寫法, 移除oldImportNames
      ExportSpecifier(path) {
        if (!_.isEmpty(oldImportNames) && oldImportNames.includes(path.node.exported.name)) {
          oldImportNames = oldImportNames.filter(item => item !== path.node.exported.name)
          path.remove()
          //進一步判斷是否還有其餘語句導出, 若是沒有移除該條語句, 防止export {}導出空對象
          if (_.isEmpty(path.parent.specifiers)) {
            path.parentPath.remove()
          }
        }
      },
      // 針對export defalut { A, B }的寫法,移除oldImportNames
      ExportDefaultDeclaration(path) {
        if (!_.isEmpty(oldImportNames) && t.isObjectExpression(path.node.declaration)) {
          const properties = []
          let isChange = false
          path.node.declaration.properties.forEach(item => {
            const index = oldImportNames.indexOf(item.key.name)
            if (index > -1) {
              oldImportNames.splice(index, 1)
              isChange = true
            } else {
              properties.push(item)
            }
          })
          // 進一步判斷export default語句是否還有其餘導出變量, 若是沒有把export default語句刪除,防止形成export default {}導出空變量
          if (isChange) {
            if (_.isEmpty(properties)) {
              path.remove()
            } else {
              path.replaceWith(t.exportDefaultDeclaration(t.objectExpression(properties)))
            }
          }
        }
      }
    })
    if (changed) {
      const output = generator(ast);
      fs.writeFileSync(replaceFilePath, output.code);
    }
  }
複製代碼

改造點二

由於須要寫入上層目錄,因此牽扯到的文件變化以下,

這樣就會有一個問題, 一直朝上層目錄寫入,何時截止呢?我作了一個處理, 若是插件參數的dir中包含當前文件名則截止。

handleIndexChange(changedFilePath, isDelete) {
    const dirName = getDirName(path.dirname(changedFilePath));
    const watchDirs = _.isArray(this.options.dir) ? this.options.dir : [this.options.dir];
    if (watchDirs.includes(dirName)) {
      // 若是watchDirs包含當前變化文件的目錄名,則不繼續向上層寫入。
      // 好比this.options.dir = ['constant', 'src'], 變化的文件爲constant/index.js, 則再也不向constant的上級目錄寫入
      return false;
    } else {
      this.handleWriteIndex(changedFilePath, isDelete, true);
    }
  }
複製代碼

由於是朝上層寫入,應該寫入到當前文件目錄的parentDir/index.js中。 並且對於index.js文件的變化,其export default語句應該用table/index.js目錄名中的「table」, 而不能用index.js的文件名「index」(如export { default as Table } from './table'而不是export { default as index } from './table'

handleWriteIndex(changedFilePath, isDelete, writeToParentDir) {
    let changedFileName = getFileName(changedFilePath);
    if (writeToParentDir) {
      // 向上層目錄寫入時, index的export default用其dirName
      const dirName = getDirName(path.dirname(changedFilePath))
      changedFileName = dirName
    }
    const exportNameMap = isDelete ? {} : this.getExportNames(changedFilePath, changedFileName);

    let dirPath = path.dirname(changedFilePath);
    if (writeToParentDir) {
      //寫入上層目錄
      dirPath = path.dirname(dirPath);
    }

    if (this.isRewritable(changedFilePath, exportNameMap)) {
      this.autoWriteFile(`${dirPath}/index.js`, changedFileName, exportNameMap, existIndex(dirPath));
    }
    ...
  }
複製代碼

結語

以上是對auto-export-plugin的改造, 歡迎提issue和PR。 github地址

相關文章
相關標籤/搜索