记开发一个webpack插件的心路历程
栏目: JavaScript · 发布时间: 6年前
内容简介:作为一名前端菜:chicken:,日常工作就是写各种业务代码, 看着大佬们写的小工具、插件啥的,羡慕不已。 偶然想到要不也写个插件试试?试试就试试,抱着试试看的态度,开始了。第一次写,有不当之处还望各位大佬指正。
作为一名前端菜:chicken:,日常工作就是写各种业务代码, 看着大佬们写的小 工具 、插件啥的,羡慕不已。 偶然想到要不也写个插件试试?试试就试试,抱着试试看的态度,开始了。
第一次写,有不当之处还望各位大佬指正。
一、插件介绍。
看图
良辰:在左边test文件中写export语句时, 会自动在右边的index文件中导入并加入到export default语句中。
没错!
橘子:这样省得自己再去手动引入到index中去, 能提升不少开发效率。
没错!!
主要应用场景
类似上图的文件目录
插件安装
npm i auto-export-plugin -D 复制代码
插件功能
- 文件改动时, 搜索当前文件的export语句,在同级目录index文件中自动导出。
- 文件删除时, 自动从index文件中删除该文件的导出语句。
用法
const path = require('path')
const AutoExport = require('auto-export-plugin')
module.exports = {
entry: './index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
}
]
},
plugins: [
new AutoExport({
dir: ['src', 'constants', 'utils']
})
]
}
复制代码
二、原理解析
- 运用babel将被改动文件内容解析成ast, 通过ast找到export
- 对同级目录index.js文件做ast转换, 插入收集到的export
- 将index.js转换过的ast重新生产代码,写入index.js文件
三、源码解读
预备知识
AST即 抽象语法树 ,我们写的每行代码, 每个字符都可以解析成AST。
// test.js export const AAA = 2 复制代码
整个文件解析成的AST(省去部分结构)如下
{
"type": "File"
"program": {
...
"body": [{
// ExportNamedDeclaration即为export语句
"type": "ExportNamedDeclaration",
"declaration": {
"type": "VariableDeclaration",
"declarations": [{
"type": "VariableDeclarator",
...
"id": {
"type": "Identifier",
// name即为我们导出的变量明 AAA
"name": "AAA",
...
},
"init": {
"type": "NumericLiteral",
// value即为我们导出的变量值
"value": 2
...
}
}],
"kind": "const"
}
}],
},
}
复制代码
你会留意到 AST 的每一层都拥有如下相同的结构, 每一层称之为一个节点
{
type: 'xxxxx',
....
}
复制代码
我们从某个文件导入时只会引入变量名,如 import {AAA} from './test' 。因此我们只需收集当前文件所有导出的变量名(如:"AAA"),无需关注导出的变量值(如:"2")。
一开始我的想法是对ast的body做遍历处理, 用'==='做类型判断,遇到ExportNamedDeclaration类型就收集变量名。
后来发现文档上有更便捷的方式,对每个节点都有两个hook: enter、exit, 在访问到该节点和离开该节点时执行,在这两个hook中可以对节点进行插入、删除、替换节点的操作。
源码来了(部分)
完整源码地址 auto-export-plugin
以下代码省去了部分片段, 可以对照完整源码进行解读。
获取改动文件的exportNames
getExportNames(filename) {
const ast = this.getAst(filename);
let exportNameMap = {};
traverse(ast, {
// 主要处理export const a = 1这种写法
ExportNamedDeclaration(path) {
if (t.isVariableDeclaration(path.node.declaration)) {
...
}
},
// 处理 export function getOne(){}写法
FunctionDeclaration(path) {
if (t.isExportNamedDeclaration(path.parent)) {
...
}
},
// 处理const A = 1; export { A }这种写法
ExportSpecifier(path) {
const name = path.node.exported.name;
exportNameMap[name] = name;
},
// 处理export default写法, 如果是export default会用文件名作为变量名
ExportDefaultDeclaration() {
...
}
});
return exportNameMap;
}
复制代码
这样就会拿到对应文件对所有export变量名。
目前只想到了4种对export语句写法(如还有其他写法麻烦留言告知)。这里考虑到一个文件中可能变量声明语句较多但不一定都是export,所以对于 export const a = 1 这种写法,没有采用像其他3种方式一样单独对类型做处理,而是在ExportNamedDeclaration中进一步做判断并处理
写入index文件
autoWriteIndex(filepath, isDelete = false) {
// 根据变动文件的路径找到对应的dir
const dirName = path.dirname(filepath);
const fileName = getFileName(filepath);
// 遍历该目录下的所有文件, 如果存在index.js则通过ast转换,
// 如果不存在直接创建index.js并写入
fs.readdir(dirName, {
encoding: 'utf8',
}, (err, files) => {
let existIndex = false;
if (!err) {
files.forEach(file => {
if (file === 'index.js') {
existIndex = true;
}
});
if (!existIndex) {
...
let importExpression = `import { ${exportNames.join(', ')} } from './${fileName}'`;
...
const data = `
${importExpression}\n
export default {
${Object.values(nameMap).join(', \n')}
}
`;
fs.writeFileSync(`${dirName}/index.js`, data);
} else {
// 通过对index.js的ast做转换处理写入exportName
this.replaceContent(`${dirName}/index.js`, filepath, nameMap);
}
}
});
}
复制代码
如果index.js文件存在则对它的ast做替换、插入、删除处理
replaceContent(indexpath, filePath, nameMap) {
...
traverse(indexAst, {
Program(path) {
const first = path.get('body.0');
// 因为js语法要求import语句必须写在文件最顶部,
// 所以如果index.js文件的第一个语句不是import语句,说明当前文件不存在import
// 需要创建import语句并插入文件第一个语句中
if (!t.isImportDeclaration(first)) {
const specifiers = self.createImportSpecifiers(nameMap);
path.unshiftContainer('body', self.createImportDeclaration(specifiers)(relPath));
}
// 如果不存在export default语句,需要创建并插入body下
const bodys = path.get('body')
if (!bodys.some(item => t.isExportDefaultDeclaration(item))) {
path.pushContainer('body', self.createExportDefaultDeclaration(Object.values(nameMap)))
}
},
ImportDeclaration: {
enter(path) {
if (!firstImportKey) {
firstImportKey = path.key;
}
// 如果存在改动文件的import语句, 比如改动的是test文件, index中原来存在`import { xx } from './test'`这样
// 的语句,需要将原来import的变量名替换掉
if (path.node.source.value === relPath && !importSetted) {
// 记录旧的export变量名。这里记录有两个作用
// 1.用旧的exportName去和新的exportName做对比, 如果相同,则无需修改index.js文件。
// 2.在后面的ExportDefaultDeclaration语句时,需要将旧的exportNames中的变
// 量全部删除掉(因为可能某些export语句再原文件中已经删除或者重命名了), 并且把新的exportName加到export default中去。
oldExportNames = path.node.specifiers.reduce((prev, cur) => {
if (t.isImportSpecifier(cur) || t.isImportDefaultSpecifier(cur)) {
return [...prev, cur.local.name];
}
return prev;
}, []);
importSetted = true
path.replaceWith(self.createImportDeclaration(specifiers)(relPath));
}
},
exit(path) {
// 当每个ImportDeclaration执行完enter之后, 如果没有进入enter内部逻辑,说
// 明当前node不是改动文件的import语句, 所以判断下一条node是不是import语句,
// 如果是,继续进入下一条import语句的enter,继续进行;
// 如果不是,则说明当前index.js文件中不存在变动文件的导入语句, 在其后面插入import语句
const pathKey = path.key;
const nextNode = path.getSibling(pathKey + 1);
if (!importSetted && !_.isEmpty(nameMap) && nextNode && !t.isImportDeclaration(nextNode)) {
...
path.insertAfter(self.createImportDeclaration(specifiers)(relPath));
}
}
},
ExportDefaultDeclaration(path) {
// 写入export default时会再次访问ExportDefaultDeclaration, 加exportSetted判断防止造成死循环
if (changed && !exportSetted && t.isObjectExpression(path.node.declaration)) {
...
path.replaceWith(self.createExportDefaultDeclaration(allProperties));
}
}
});
...
const output = generator(indexAst);
fs.writeFileSync(indexpath, output.code);
}
复制代码
代码中做了优化的部分
- 对文件的改动和删除事件,做了防抖处理, 防止频繁执行
this.watcher.on('change', _.debounce(this.handleChange.bind(this), 1000))
.on('unlink', _.debounce(this.handleDeleteFile.bind(this), 1000));
复制代码
- 对改动文件的export出的变量名缓存在
this.cacheExportNameMap = {};,这样如果文件改动部分不是export相关的改动(比如新定义了一个函数或变量但是并没有export出去),就不会对index文件做转换处理。
TODO List
- 扩展对类似下图多级目录的支持, 自动导出到外层index文件中
- 扩展对commonjs的支持
四、总结
在写插件、打包、发布npm的过程中,遇到了很多平时写业务代码所不能遇见的问题,也进一步了解了webpack和node,包括发布npm。 也算是没有白浪费时间。
后续文章会把遇到的问题总结出来,敬请期待。
读到这里,如果对你有点帮助的话,烦请给个 star , 多谢:smile:, 欢迎提issue和PR。
好了先不说了,一大堆需求等着呢,我该去写了:smile:。
参考文档
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 三方对接心路历程
- 心路历程:爬虫实战——从数据到产品
- 围观:基于事件机制的内部解耦之心路历程
- 一个前端自学者从面试被吊打,到拿 offer 的心路历程
- 学习新的计算机知识为什么这么难?看看我的心路历程
- 鹅厂7年终有离开之日,记离职鹅厂最后30天的真实心路历程
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Practical Django Projects, Second Edition
James Bennett / Apress / 2009 / 44.99
Build a django content management system, blog, and social networking site with James Bennett as he introduces version 1.1 of the popular Django framework. You’ll work through the development of ea......一起来看看 《Practical Django Projects, Second Edition》 这本书的介绍吧!