千锋教育-做有情怀、有良心、有品质的职业教育机构

手机站
千锋教育

千锋学习站 | 随时随地免费学

千锋教育

扫一扫进入千锋手机站

领取全套视频
千锋教育

关注千锋学习站小程序
随时随地免费学习课程

当前位置:首页  >  技术干货  > 如何用typescript写一个处理console的babel插件

如何用typescript写一个处理console的babel插件

来源:千锋教育
发布人:qyf
时间: 2022-10-10 16:42:54 1665391374

  技术点介绍

  通过这篇文章你可以学到:

  · ts-mocha和chai来写测试用例,

  · 如何写一个babel插件,

  · 如何用schame-utils来做options校验,

  · typescript双重断言的一个应用场景

  · 如何组织测试代码

  一、前言

  console对象对前端工程师来说是必不可少的api,开发时我们经常通过它来打印一些信息来调试。但生产环境下console有时会引起一些问题。

  如果项目报了一个bug,console对象被重写了但是没有把所有的方法都重写,导致了报错,另外考虑到console会影响性能,所以最后定的解决方案是把源码中所有的console都删掉。

  生产环境下删除console是没问题的,但是这件事不需要手动去做。在打包过程中,我们会对代码进行压缩,而压缩的工具都提供了删除一些函数的功能,比如terser支持drop_console来删除console.*,也可以通过pure_funcs来删除某几种console的方法。

图片26

  但是这种方案对我们是不适用的,因为我们既有react的项目又有react-native的项目,react-native并不会用webpack打包,也就不会用terser来压缩。

  其实源码到最终代码过程中会经历很多次ast的解析,比如eslint、babel、terser等,除了eslint主要是用来检查ast,并不会做过多修改,其余的工具都可以来完成修改ast,删除console这件事情。terser不可以用,那么我们可以考虑用babel来做。

  而且,我们只是希望在生产环境下删除console,在开发环境下console还是很有用的,如果能扩展一下console,让它功能更强大,比如支持颜色的打印,支持文件和代码行数的提示就好了。于是我们就开发了本文介绍的这个插件: babel-plugin-console-transform。

  二、演示

  先看下效果再讲实现。比如源码是这样的:

图片27

  生产环境下转换后的代码:

图片28

  开发环境下转换后的代码:

图片29

  运行效果:

图片30

  生产环境删除了console,开发环境扩展了一些方法,并且添加了代码行数和颜色等。

  接下来是功能的细节还有实现思路。

  三、功能

  按照需求,这个插件需要在不同的环境做不同的处理,生产环境可以删除console,开发环境扩展console。

  生产环境删除console并不是全部删除,还需要支持删除指定name的方法,比如log、warn等,因为有时console.error是有用的。而且有的时候根据方法名还不能确定能不能删除,要根据打印的内容来确定是不是要删。

  开发环境扩展console要求不改变原生的api,扩展一些方法,这些方法会被转换成原生api,但是会额外添加一些信息,比如添加代码文件和行数的信息,添加一些颜色的样式信息等。

图片31

  于是console-transform这个插件就有了这样的参数:

  {

  env: 'production',

  removeMethods: ["log", "*g*", (args) => args.includes('xxxx')],

  additionalStyleMethods: {

  'success': 'padding:10px; color:#fff;background:green;',

  'danger': 'padding:20px; background:red;font-size:30px; color:#fff;'

  }

  }

  其中env是指定环境的,可以通过process.env.NODE_ENV来设置。

  removeMethods是在生产环境下要删除的方法,可以传一个name,支持glob,也就是 \*g*是删除所有名字包含g的方法;而且可以传一个函数,函数的参数是console.xxx的所有参数,插件会根据这个函数的返回值来决定是不是删除改console.xxx。多个条件的时候,只要有一个生效,就会删。

  additionalStyleMethods里面可以写一些扩展的方法,比如succes、danger,分别定义了他们的样式。其实插件本身提供了 red、green、orange、blue、bgRed、bgOrange、bgGreen、bgBlue方法,通过这个参数可以自定义,开发环境console怎么用都行。

  四、实现

  接下来是重头戏,实现思路了。

  首先介绍下用到的技术,代码是用typescript写的,实现功能是基于 @babel/core,@babel/types,测试代码使用ts-mocha、chai写的,代码的lint用的eslint、prettier。

  主要逻辑

  babel会把代码转成ast,插件里可以对对ast做修改,然后输出的代码就是转换后的。babel的插件需要是一个返回插件信息的函数。

  如下, 参数是babelCore的api,里面有很多工具,我们这里只用到了 types来生成一些ast的节点。返回值是一个PluginObj类型的对象。

  import BabelCore, { PluginObj } from '@babel/core';

  export default function({

  types,

  }: typeof BabelCore): PluginObj{

  return {

  name: 'console-transform',

  visitor: {...}

  }

  }

  其中ConsoleTransformState里面是我们要指定的类型,这是在后面对ast处理时需要拿到参数和文件信息时用的。

  export interface PluginOptions {

  env: string;

  removeMethods?: Array;

  additionalStyleMethods?: { [key: string]: string };

  }

  export interface ConsoleTransformState {

  opts: PluginOptions;

  file: any;

  }

  PluginOptions是options的类型,env是必须,其余两个可选,removeMethods是一个值为string或Function的数组,additionalStyleMethods是一个值为string的对象。 这都是我们讨论需求时确定的。(其中file是获取代码行列数用的,我们找到它的类型,就用了any。)

  返回的插件信息对象有一个visitor属性,可以声明对一些节点的处理方式,我们需要处理的是CallExpression节点。(关于代码对应的ast是什么样的,可以用astexplorer这个工具看)。

  {

  CallExpression(path, { opts, file }) {

  validateSchema(schema, opts);

  const { env, removeMethods, additionalStyleMethods } = opts;

  const callee = path.get('callee');

  if (

  callee.node.type === 'MemberExpression' &&

  (callee.node.object as any).name === 'console'

  ) {

  ...

  }

  },

  }

  这个方法就会在处理到CallExpression类型的节点时被调用,参数path 可以拿到一些节点的信息,通过path.get('callee')拿到调用信息,然后通过node.type过滤 console.xxx() 而不是xxx()类型的函数调用,也就是MemberExpression类型,再通过callee.node.object过滤出console的方法。

  实现production下删除console

  接下来就是实现主要功能的时候了:

  const methodName = callee.node.property.name as string;

  if (env === 'production') {

  ...

  return path.remove();

  } else {

  const lineNum = path.node.loc.start.line;

  const columnNum = path.node.loc.start.column;

  ...

  path.node.arguments.unshift(...);

  callee.node.property.name = 'log';

  }

  先看主要逻辑,production环境下,调用path.remove(),这样console就没了,其他环境对console的参数(path.node.arguments.)做一些修改,在前面多加一些参数,然后把方法名(callee.node.property.name)改为log。大体框架就是这样的。

  然后细化一下:

  production的时候,当有removeMethods参数时,要根据其中的name和funciton来决定是否删除:

  if (removeMethods) {

  const args = path.node.arguments.map(

  item => (item as any).value,

  );

  if (isMatch(removeMethods, methodName, args)) {

  return path.remove();

  }

  return;

  }

  return path.remove();

  通过把path.node.arguments把所有的args放到一个数组里,然后来匹配条件。如下,匹配时根据类型是string还是function决定如何调用。

  const isMatch = (

  removeMethods: Array,

  methodName: string,

  args: any[],

  ): boolean => {

  let isRemove = false;

  for (let i = 0; i < removeMethods.length; i++) {

  if (typeof removeMethods[i] === 'function') {

  isRemove = (removeMethods[i] as Function)(args) ? true : isRemove;

  } else if (mm([methodName], removeMethods[i] as string).length > 0) {

  isRemove = true;

  }

  }

  return isRemove;

  };

  如果是function就把参数作为参数传入,根据返回值确定是否删除,如果是字符串,会用mimimatch做glob的解析,支持**、 {a,b}等语法。

  实现非production下扩展console

  当在非production环境下,插件会提供一些内置方法:

  const styles: { [key: string]: string } = {

  red: 'color:red;',

  blue: 'color:blue;',

  green: 'color:green',

  orange: 'color:orange',

  bgRed: 'padding: 4px; background:red;',

  bgBlue: 'padding: 4px; background:blue;',

  bgGreen: 'padding: 4px; background: green',

  bgOrange: 'padding: 4px; background: orange',

  };

  结合用户通过addtionalStyleMethods扩展的方法,来对代码做转换:

  const methodName = callee.node.property.name as string;

  const lineNum = path.node.loc.start.line;

  const columnNum = path.node.loc.start.column;

  const allStyleMethods = {

  ...styles,

  ...additionalStyleMethods,

  };

  if (Object.keys(allStyleMethods).includes(methodName)) {

  const ss = path.node.arguments.map(() => '%s').join('');

  path.node.arguments.unshift(

  types.stringLiteral(`%c${ss}%s`),

  types.stringLiteral(allStyleMethods[methodName]),

  types.stringLiteral(

  `${file.opts.filename.slice(

  process.cwd().length,

  )} (${lineNum}:${columnNum}):`,

  ),

  );

  callee.node.property.name = 'log';

  }

  通过methodName判断出要扩展的方法,然后在参数(path.node.arguments)中填入一些额外的信息 ,这里就用到了@babel/core提供的types(其实是封装了@babel/types的api)来生成文本节点了,最后把扩展的方法名都改成log。

  实现options的校验

  我们逻辑写完了,但是options还没有校验,这里可以用schema-utils这个工具来校验,通过一个json对象来描述解构,然后调用validate的api来校验。webpack那么复杂的options就是通过这个工具校验的。

  schema如下,对env、removeMethods、additionalStyleMethods都是什么格式做了声明。

  export default {

  type: 'object',

  additionalProperties: false,

  properties: {

  env: {

  description:

  'set the environment to decide how to handle `console.xxx()` code',

  type: 'string',

  },

  removeMethods: {

  description:

  'set what method to remove in production environment, default to all',

  type: 'array',

  items: {

  description:

  'method name or function to decide whether remove the code',

  oneOf: [

  {

  type: 'string',

  },

  {

  instanceof: 'Function',

  },

  ],

  },

  },

  additionalStyleMethods: {

  description:

  'some method to extend the console object which can be invoked by console.xxx() in non-production environment',

  type: 'object',

  additionalProperties: true,

  },

  },

  required: ['env'],

  };

  五、测试

  代码写完了,就到了测试环节,测试的完善度直接决定了你这个工具可不可用。

图片32

  options的测试就是传入各种情况的options参数,看报错信息是否正确。这里有个知识点,因为options需要传错,所以肯定类型不符合,使用as any as PluginOptions的双重断言可以绕过类型校验。

  describe('options格式测试', () => {

  const inputFilePath = path.resolve(

  __dirname,

  './fixtures/production/drop-all-console/actual.js',

  );

  it('env缺失会报错', () => {

  const pluginOptions = {};

  assertFileTransformThrows(

  inputFilePath,

  pluginOptions as PluginOptions,

  new RegExp(".*configuration misses the property 'env'*"),

  );

  });

  it('env只能传字符串', () => {

  const pluginOptions = {

  env: 1,

  };

  assertFileTransformThrows(

  inputFilePath,

  (pluginOptions as any) as PluginOptions,

  new RegExp('.*configuration.env should be a string.*'),

  );

  });

  it('removeMethods的元素只能是string或者function', () => {

  const pluginOptions = {

  env: 'production',

  removeMethods: [1],

  };

  assertFileTransformThrows(

  inputFilePath,

  (pluginOptions as any) as PluginOptions,

  new RegExp(

  '.*configuration.removeMethods[.*] should be one of these:s[ ]{3}string | function.*',

  ),

  );

  });

  it('additionalStyleMethods只能是对象', () => {

  const pluginOptions: any = {

  env: 'production',

  additionalStyleMethods: [],

  };

  assertFileTransformThrows(

  inputFilePath,

  pluginOptions as PluginOptions,

  new RegExp(

  '.*configuration.additionalStyleMethods should be an object.*',

  ),

  );

  });

  });

  主要的还是plugin逻辑的测试。

  @babel/core 提供了transformFileSync的api,可以对文件做处理,我封装了一个工具函数,对输入文件做处理,把结果的内容和另一个输出文件做对比。

  const assertFileTransformResultEqual = (

  inputFilePathRelativeToFixturesDir: string,

  outputFilePath: string,

  pluginOptions: PluginOptions,

  ): void => {

  const actualFilePath = path.resolve(__dirname, './fixtures/', inputFilePathRelativeToFixturesDir,);

  const expectedFilePath = path.resolve(__dirname,'./fixtures/',outputFilePath);

  const res = transformFileSync(inputFilePath, {

  babelrc: false,

  configFile: false,

  plugins: [[consoleTransformPlugin, pluginOptions]]

  });

  assert.equal(

  res.code,

  fs.readFileSync(expectedFilePath, {

  encoding: 'utf-8',

  }),

  );

  };

  fixtures下按照production和其他环境的不同场景分别写了输入文件actual和输出文件expected。比如production下测试drop-all-console、drop-console-by-function等case,和下面的测试代码一一对应。

图片33

  代码里面是对各种情况的测试。

  describe('plugin逻辑测试', () => {

  describe('production环境', () => {

  it('默认会删除所有的console', () => {

  const pluginOptions: PluginOptions = {

  env: 'production',

  };

  assertFileTransformResultEqual(

  'production/drop-all-console/actual.js',

  'production/drop-all-console/expected.js',

  pluginOptions,

  );

  });

  it('可以通过name删除指定console,支持glob', () => {...});

  it('可以通过function删除指定参数的console', () => {...}

  });

  describe('其他环境', () => {

  it('非扩展方法不做处理', () => {...});

  it('默认扩展了red 、green、blue、orange、 bgRed、bgGreen等方法,并且添加了行列数', () => {...});

  it('可以通过additionalStyleMethods扩展方法,并且也会添加行列数', () => {...});

  it('可以覆盖原生的log等方法', () => {...});

  });

  });

  六、总结

  这个插件虽然功能只是处理console,但细节还是蛮多的,比如删除的时候要根据name和function确定是否删除,name支持glob,非production环境要支持用户自定义扩展等等。

  技术方面,用了schema-utils做options校验,用ts-mocha结合断言库chai做测试,同时设计了一个比较清晰的目录结构来组织测试代码。

tags:
声明:本站稿件版权均属千锋教育所有,未经许可不得擅自转载。
10年以上业内强师集结,手把手带你蜕变精英
请您保持通讯畅通,专属学习老师24小时内将与您1V1沟通
免费领取
今日已有369人领取成功
刘同学 138****2860 刚刚成功领取
王同学 131****2015 刚刚成功领取
张同学 133****4652 刚刚成功领取
李同学 135****8607 刚刚成功领取
杨同学 132****5667 刚刚成功领取
岳同学 134****6652 刚刚成功领取
梁同学 157****2950 刚刚成功领取
刘同学 189****1015 刚刚成功领取
张同学 155****4678 刚刚成功领取
邹同学 139****2907 刚刚成功领取
董同学 138****2867 刚刚成功领取
周同学 136****3602 刚刚成功领取
相关推荐HOT