本文主要是对auto-export-plugin的进一步补充和改造,若是你没看过上一篇,请看这里 记开发一个webpack插件的心路历程javascript
这两天在使用auto-export-plugin使用过程当中,用起来有点不舒服,遂进行了以下部分改造。java
改造前 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'
复制代码
这样也免得咱们在写组件时手动导入到上级目录了。
改造点一
改造import A, { B } from './test'; export default { A, B }
这样的写法,总共须要三大步(假设变化的文件为test)
import A, { B } from './test'
语句删除。export { default as A, B } from './test'
语句若是原来不存在对应文件的导入和导出语句import { B } from './test'
或export { B } from './test'
,则须要在文件适当位置(不能插入在文件顶部,防止后面有import语句形成语法错误)插入导出语句export { B } from './test'
若是存在导出语句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地址