手写webpack loader是什么样的体验?

687 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

虽然是五月的尾巴,但是竟然也算6月更文呢!就很棒哟~😄

前言

大家好,我是小阵 🔥,一路奔波不停的码字业务员
身为一个前端小菜鸟,总是有一个飞高飞远的梦想,因此,每点小成长,我都想要让它变得更有意义,为了自己,也为了更多值得的人
如果喜欢我的文章,可以关注 ➕ 点赞,与我一同成长吧~😋
加我微信:zzz886885,邀你进群,一起学习交流,摸鱼学习两不误🌟

开开心心学技术大法~~

开心

来了来了,他真的来了~

正文

前面讲了webpack-loader的编写方法手写webpack-core中用到的babel相关库

这次基于之前的基础,我们手写一个webpack-loader来实现以下功能

  1. 去除项目中指定的console,可以配置去除log、info、error等
  2. 可以在不同console前加上指定前缀,包括手动配置的字符串或者自动生成的文件名和文件位置信息

首先我们搭建一个基础的loader

先在根目录的webpack.config.js中引用我们本地的loader

const path = require('path')

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'console-ast-loader',
            options: {
                // 我们预期能配置哪些console需要被过滤
                needRemovedConsoleArray: ['log', 'warn'],
                // 我们也希望可以手动配置不同的console前缀
                consolePrefix: {
                  error: '我是error前缀:',
                  info: {
                    // 或者通过boolean值来配置显示文件名和console出现的位置信息
                    filename: true,
                    location: true
                  },
                  count:'我是count前缀:'
                }
             }
          }
        ]
      },
    ]
  },
  mode: 'development',
  resolveLoader: {
    modules: [path.resolve(__dirname, 'loaders')]
  }
}

然后在/src/loaders/目录下新建一个console-ast-loader.js

写好loader的基本框架

// loader就是一个最基本的function,注意不要是箭头函数
module.exports = function (content, map, ast) {
  // 通过this.getOptions获取传入的options参数
  const { needRemovedConsoleArray, consolePrefix } = this.getOptions()
  // callback的第一个参数是错误,如果有就传,没有的话就是null,第二参数content就是test匹配文件的源代码的utf-8的编码内容
  this.callback(null, content, map, ast);
}

在之前的看完就会的webapck loader编写教程一文中我们用到了loader-utilsgetOptions方法来获取传入的options参数,但是在webpack5中,直接内置了类似的方法,我们只需要调用this.getOptions即可。

具体实现

首先我们分析下要实现上面说的功能要怎样实现

  1. 首先我要能识别到代码中的console,包括info、log、warn等
  2. 我要能把识别到的console给干掉,当然也不能影响其他代码
  3. 我还要能识别到console中的参数,比如console.log('zzz')中的zzz
  4. 识别之后还要将用户定义的字符串塞入进去
  5. 如果用户配置了显示文件名和文件行列信息的话,我们还要拿到这些信息并且塞到原来的console中

对第一点,我们能想到的方法应该只有两种,一种是正则,另一种则是ast。但是正则只是想一下就比较头疼,因为console参数中还可以嵌套很多其他的内容,要写很多的匹配规则。

所以这里我们用ast语法树来解决这个问题,简单而高效。

根据源代码生成ast语法树

首先我们要根据源代码生成ast语法树

const { parse } = require('@babel/parser')

const ast = parse(content, {
  sourceType: 'module'
})

借助@babel/parserparse方法很简单就生成了ast

通过ast语法树找到console节点

接着需要遍历ast拿到console节点

首先我们要知道console对应到ast上的节点类型是什么,我们可以在astexplorer上在线查看ast上查看

image.png

可以看到我们要找的console对应在ast语法树上的节点是CallExpression

接着我们通过@babel/traverse这个库遍历拿到console的节点

  const traverse = require("@babel/traverse");
 
  // 这里的ast就是前面生成的ast语法树
  traverse.default(ast, {
    CallExpression: function (path) {
        // 这里的path就是我们找到的节点
        console.log('path',path)
    }
  })

移除找到的console节点

已经找到了ast节点,那怎样移除掉这个节点呢?

// 可以通过path.remove()来移除掉ast的节点
// 注意,"节点"两个字描述不太清晰,事实上前面通过CallExpression传入的path是ast的path信息,可以通过path追溯到父path或兄弟path,而真正的节点node信息被包含在path中
// 当我们移除了path,当然也就一并移除了node节点
astPath.remove();

到这里,就已经完成了第二步,剩下的几部可以一起来看,无非是我们要找到我们所需要的信息,真正的拼接字符串其实不算太难。当然,我们这里是ast语法树的节点拼接。

获取拼接的前缀信息

那怎样获取我们需要的信息呢?

通过打断点我们,我们看到我们能拿到console的properties信息,能分别出是log还是其他。还能拿到console的start位置的line和colum信息,当然也能拿到console的参数信息value

断点信息

还少一个文件信息,我们可以通过this.resource拿到加载的资源信息,然后通过path拿到文件名称

  const path = require('path')
  // 注意,这里的this是loader的this,而不是babel中任意方法体的this
  const resourceFileName = path.basename(this.resource);

想要的信息都有了,我们还要知道一个知识点,怎样在ast中添加一个ast节点,然后,添加完节点之后,又要怎样才可以将ast语法树转换成我们正常的文件的utf-8的编码?

先来添加ast节点

在已有ast语法树上添加新的ast节点

// 通过@babel/types暴露的方法可以让我们自己定义我们想要的ast节点
// 比如下面的types.stringLiteral,其中stringLiteral表示的就是字符串。更多节点类型可以参考babel的官网
const types = require('@babel/types')

// path是之前我们在traverse中拿到的path
path.node.arguments.unshift(types.stringLiteral(prefix))

再来看转化成utf-8的源代码

将更改过的ast输出到源代码

  // @babel/generator是一个专门将ast转换成源代码的方法,还有更多的options可以参考官网
  const generate = require('@babel/generator')
  // 我们只需要拿到转化后的源代码即可。不传options,默认转化成utf-8
  const { code } = generate.default(ast)

两个关键的点搞完之后,之后就是正常的coding阶段啦

  traverse.default(ast, {
    CallExpression: function (path, state) {
      // 拿到console的property,log、info、warn、table、count等
      const consoleName = path.container.expression.callee && path.container.expression.callee.property && path.container.expression.callee.property.name
      // 拿到console定义时的位置信息
      const consoleLocation = path.container.expression.loc && path.container.expression.loc.start

      // 如果在需要移除的console中,则移除
      if (needRemovedConsoleArray.includes(consoleName)) {
        return path.remove()
      }
      for (const consoleType in consolePrefix) {
        // 如果跟options传入的信息匹配则塞入节点
        if (Object.hasOwnProperty.call(consolePrefix, consoleType) && consoleName === consoleType) {
          const prefix = consolePrefix[consoleType];
          // 根据用户传入的配置做string和object的区分
          if (typeof prefix === 'string') {
            path.node.arguments.unshift(types.stringLiteral(prefix))
          }
          if (prefix instanceof Object) {
            path.node.arguments.unshift(types.stringLiteral(`${resourceFileName} \nline:${consoleLocation.line};column:${consoleLocation.column} \n`))
          }
        }
      }
    }
  })

之后再将改变后的ast通过前面说到的generator生成源代码之后通过loadercallback抛出去即可。

  const { code } = generate.default(ast)
  this.callback(null, code)

享受成果

ok,前面的搞完了,接下来享受甜丝丝的成果吧!

/src/目录下新建index.js

const a = 'test1'

console.log('a',a)

console.log('我是log')

console.info('我是info')
console.warn('我是warn')
console.error('我是error')
console.count('我是count')
console.trace('我是trace')
console.dir('我是dir')
console.dirxml('我是dirxml')
console.assert('我是assert')
console.table('我是table')
console.time('我是time')
console.timeEnd('我是timeEnd')
console.group('我是group')
console.groupEnd('我是groupEnd')
console.profile('我是profile');
console.profileEnd('我是profileEnd');

在根目录通过npx webpack来生成build后的main.js文件,新建一个index.html文件将main.js文件引入,或者直接引入一个html-webpack-plugin帮你做好这一切

接着浏览器打开index.html文件看效果

image.png

哎哟不错哟,又学会了一项技能呢!鼓掌👏🏻哈哈哈

开心哈哈哈

插件功能不难,重在中间的过程,看完的小伙伴肯定都有更加闪亮✨的想法啊,大家加油!

完整代码传送门

比心

结语

往期好文推荐「我不推荐下,大家可能就错过了史上最牛逼vscode插件集合啦!!!(嘎嘎~)😄」