本文主要是对auto-export-plugin的进一步补充和改造,如果你没看过上一篇,请看这里 记开发一个webpack插件的心路历程
这两天在使用auto-export-plugin使用过程中,用起来有点不舒服,遂进行了如下部分改造。
一、改造前后对比
改造点一
改造前
改造后
这里解释一下。 图一虽然也实现了对export的收集并导出,单纯看起来功能没什么问题,但是真正引用模块时感觉有些鸡肋。如下
import component from './component'
const { Table } = component
还需要再解构一下才能取到对应的组件, 用起来实在麻烦。
写这个插件的初衷就是为了减少代码的搬运,提高开发效率,这很显然与目的不符。
究其原因是由如下写法import A, { B } from './test'; export default { A, B }
造成的, 最后导出的是一个Object,所以肯定需要解构才能使用。
所以抽时间又进行了改造,效果如图二所示, 在应用时可以直接解构导出,如下:
import { Table } from './component'
插件没有直接用export * from './test'处理, 主要目的把变量名直接导出显得更直观,而且在多人维护项目时也能很清楚知道别人写的模块导出了哪些变量
改造点二
改造前
文件改动时,只会导出变量名写入同级目录的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)- 找到原来的导入语句,把变量收集起来为[A, B], 同时把
import A, { B } from './test'
语句删除。 - 过滤掉在export default语句中上一步收集的变量[A, B], 这里只能先用过滤,而不能直接删除(有可能还有其他导出的变量)
- 插入
export { default as A, B } from './test'
语句
- 找到原来的导入语句,把变量收集起来为[A, B], 同时把
-
如果原来不存在对应文件的导入和导出语句
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地址