久久精品国产亚洲高清|精品日韩中文乱码在线|亚洲va中文字幕无码久|伊人久久综合狼伊人久久|亚洲不卡av不卡一区二区|精品久久久久久久蜜臀AV|国产精品19久久久久久不卡|国产男女猛烈视频在线观看麻豆

千鋒教育-做有情懷、有良心、有品質(zhì)的職業(yè)教育機(jī)構(gòu)

手機(jī)站
千鋒教育

千鋒學(xué)習(xí)站 | 隨時(shí)隨地免費(fèi)學(xué)

千鋒教育

掃一掃進(jìn)入千鋒手機(jī)站

領(lǐng)取全套視頻
千鋒教育

關(guān)注千鋒學(xué)習(xí)站小程序
隨時(shí)隨地免費(fèi)學(xué)習(xí)課程

當(dāng)前位置:首頁(yè)  >  技術(shù)干貨  > 如何用typescript寫(xiě)一個(gè)處理console的babel插件

如何用typescript寫(xiě)一個(gè)處理console的babel插件

來(lái)源:千鋒教育
發(fā)布人:qyf
時(shí)間: 2022-10-10 16:42:54 1665391374

  技術(shù)點(diǎn)介紹

  通過(guò)這篇文章你可以學(xué)到:

  · ts-mocha和chai來(lái)寫(xiě)測(cè)試用例,

  · 如何寫(xiě)一個(gè)babel插件,

  · 如何用schame-utils來(lái)做options校驗(yàn),

  · typescript雙重?cái)嘌缘囊粋€(gè)應(yīng)用場(chǎng)景

  · 如何組織測(cè)試代碼

  一、前言

  console對(duì)象對(duì)前端工程師來(lái)說(shuō)是必不可少的api,開(kāi)發(fā)時(shí)我們經(jīng)常通過(guò)它來(lái)打印一些信息來(lái)調(diào)試。但生產(chǎn)環(huán)境下console有時(shí)會(huì)引起一些問(wèn)題。

  如果項(xiàng)目報(bào)了一個(gè)bug,console對(duì)象被重寫(xiě)了但是沒(méi)有把所有的方法都重寫(xiě),導(dǎo)致了報(bào)錯(cuò),另外考慮到console會(huì)影響性能,所以最后定的解決方案是把源碼中所有的console都刪掉。

  生產(chǎn)環(huán)境下刪除console是沒(méi)問(wèn)題的,但是這件事不需要手動(dòng)去做。在打包過(guò)程中,我們會(huì)對(duì)代碼進(jìn)行壓縮,而壓縮的工具都提供了刪除一些函數(shù)的功能,比如terser支持drop_console來(lái)刪除console.*,也可以通過(guò)pure_funcs來(lái)刪除某幾種console的方法。

圖片26

  但是這種方案對(duì)我們是不適用的,因?yàn)槲覀兗扔衦eact的項(xiàng)目又有react-native的項(xiàng)目,react-native并不會(huì)用webpack打包,也就不會(huì)用terser來(lái)壓縮。

  其實(shí)源碼到最終代碼過(guò)程中會(huì)經(jīng)歷很多次ast的解析,比如eslint、babel、terser等,除了eslint主要是用來(lái)檢查ast,并不會(huì)做過(guò)多修改,其余的工具都可以來(lái)完成修改ast,刪除console這件事情。terser不可以用,那么我們可以考慮用babel來(lái)做。

  而且,我們只是希望在生產(chǎn)環(huán)境下刪除console,在開(kāi)發(fā)環(huán)境下console還是很有用的,如果能擴(kuò)展一下console,讓它功能更強(qiáng)大,比如支持顏色的打印,支持文件和代碼行數(shù)的提示就好了。于是我們就開(kāi)發(fā)了本文介紹的這個(gè)插件: babel-plugin-console-transform。

  二、演示

  先看下效果再講實(shí)現(xiàn)。比如源碼是這樣的:

圖片27

  生產(chǎn)環(huán)境下轉(zhuǎn)換后的代碼:

圖片28

  開(kāi)發(fā)環(huán)境下轉(zhuǎn)換后的代碼:

圖片29

  運(yùn)行效果:

圖片30

  生產(chǎn)環(huán)境刪除了console,開(kāi)發(fā)環(huán)境擴(kuò)展了一些方法,并且添加了代碼行數(shù)和顏色等。

  接下來(lái)是功能的細(xì)節(jié)還有實(shí)現(xiàn)思路。

  三、功能

  按照需求,這個(gè)插件需要在不同的環(huán)境做不同的處理,生產(chǎn)環(huán)境可以刪除console,開(kāi)發(fā)環(huán)境擴(kuò)展console。

  生產(chǎn)環(huán)境刪除console并不是全部刪除,還需要支持刪除指定name的方法,比如log、warn等,因?yàn)橛袝r(shí)console.error是有用的。而且有的時(shí)候根據(jù)方法名還不能確定能不能刪除,要根據(jù)打印的內(nèi)容來(lái)確定是不是要?jiǎng)h。

  開(kāi)發(fā)環(huán)境擴(kuò)展console要求不改變?cè)腶pi,擴(kuò)展一些方法,這些方法會(huì)被轉(zhuǎn)換成原生api,但是會(huì)額外添加一些信息,比如添加代碼文件和行數(shù)的信息,添加一些顏色的樣式信息等。

圖片31

  于是console-transform這個(gè)插件就有了這樣的參數(shù):

  {

  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是指定環(huán)境的,可以通過(guò)process.env.NODE_ENV來(lái)設(shè)置。

  removeMethods是在生產(chǎn)環(huán)境下要?jiǎng)h除的方法,可以傳一個(gè)name,支持glob,也就是 \*g*是刪除所有名字包含g的方法;而且可以傳一個(gè)函數(shù),函數(shù)的參數(shù)是console.xxx的所有參數(shù),插件會(huì)根據(jù)這個(gè)函數(shù)的返回值來(lái)決定是不是刪除改console.xxx。多個(gè)條件的時(shí)候,只要有一個(gè)生效,就會(huì)刪。

  additionalStyleMethods里面可以寫(xiě)一些擴(kuò)展的方法,比如succes、danger,分別定義了他們的樣式。其實(shí)插件本身提供了 red、green、orange、blue、bgRed、bgOrange、bgGreen、bgBlue方法,通過(guò)這個(gè)參數(shù)可以自定義,開(kāi)發(fā)環(huán)境console怎么用都行。

  四、實(shí)現(xiàn)

  接下來(lái)是重頭戲,實(shí)現(xiàn)思路了。

  首先介紹下用到的技術(shù),代碼是用typescript寫(xiě)的,實(shí)現(xiàn)功能是基于 @babel/core,@babel/types,測(cè)試代碼使用ts-mocha、chai寫(xiě)的,代碼的lint用的eslint、prettier。

  主要邏輯

  babel會(huì)把代碼轉(zhuǎn)成ast,插件里可以對(duì)對(duì)ast做修改,然后輸出的代碼就是轉(zhuǎn)換后的。babel的插件需要是一個(gè)返回插件信息的函數(shù)。

  如下, 參數(shù)是babelCore的api,里面有很多工具,我們這里只用到了 types來(lái)生成一些ast的節(jié)點(diǎn)。返回值是一個(gè)PluginObj類(lèi)型的對(duì)象。

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

  export default function({

  types,

  }: typeof BabelCore): PluginObj{

  return {

  name: 'console-transform',

  visitor: {...}

  }

  }

  其中ConsoleTransformState里面是我們要指定的類(lèi)型,這是在后面對(duì)ast處理時(shí)需要拿到參數(shù)和文件信息時(shí)用的。

  export interface PluginOptions {

  env: string;

  removeMethods?: Array;

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

  }

  export interface ConsoleTransformState {

  opts: PluginOptions;

  file: any;

  }

  PluginOptions是options的類(lèi)型,env是必須,其余兩個(gè)可選,removeMethods是一個(gè)值為string或Function的數(shù)組,additionalStyleMethods是一個(gè)值為string的對(duì)象。 這都是我們討論需求時(shí)確定的。(其中file是獲取代碼行列數(shù)用的,我們找到它的類(lèi)型,就用了any。)

  返回的插件信息對(duì)象有一個(gè)visitor屬性,可以聲明對(duì)一些節(jié)點(diǎn)的處理方式,我們需要處理的是CallExpression節(jié)點(diǎn)。(關(guān)于代碼對(duì)應(yīng)的ast是什么樣的,可以用astexplorer這個(gè)工具看)。

  {

  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'

  ) {

  ...

  }

  },

  }

  這個(gè)方法就會(huì)在處理到CallExpression類(lèi)型的節(jié)點(diǎn)時(shí)被調(diào)用,參數(shù)path 可以拿到一些節(jié)點(diǎn)的信息,通過(guò)path.get('callee')拿到調(diào)用信息,然后通過(guò)node.type過(guò)濾 console.xxx() 而不是xxx()類(lèi)型的函數(shù)調(diào)用,也就是MemberExpression類(lèi)型,再通過(guò)callee.node.object過(guò)濾出console的方法。

  實(shí)現(xiàn)production下刪除console

  接下來(lái)就是實(shí)現(xiàn)主要功能的時(shí)候了:

  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環(huán)境下,調(diào)用path.remove(),這樣console就沒(méi)了,其他環(huán)境對(duì)console的參數(shù)(path.node.arguments.)做一些修改,在前面多加一些參數(shù),然后把方法名(callee.node.property.name)改為log。大體框架就是這樣的。

  然后細(xì)化一下:

  production的時(shí)候,當(dāng)有removeMethods參數(shù)時(shí),要根據(jù)其中的name和funciton來(lái)決定是否刪除:

  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();

  通過(guò)把path.node.arguments把所有的args放到一個(gè)數(shù)組里,然后來(lái)匹配條件。如下,匹配時(shí)根據(jù)類(lèi)型是string還是function決定如何調(diào)用。

  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就把參數(shù)作為參數(shù)傳入,根據(jù)返回值確定是否刪除,如果是字符串,會(huì)用mimimatch做glob的解析,支持**、 {a,b}等語(yǔ)法。

  實(shí)現(xiàn)非production下擴(kuò)展console

  當(dāng)在非production環(huán)境下,插件會(huì)提供一些內(nèi)置方法:

  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',

  };

  結(jié)合用戶(hù)通過(guò)addtionalStyleMethods擴(kuò)展的方法,來(lái)對(duì)代碼做轉(zhuǎn)換:

  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';

  }

  通過(guò)methodName判斷出要擴(kuò)展的方法,然后在參數(shù)(path.node.arguments)中填入一些額外的信息 ,這里就用到了@babel/core提供的types(其實(shí)是封裝了@babel/types的api)來(lái)生成文本節(jié)點(diǎn)了,最后把擴(kuò)展的方法名都改成log。

  實(shí)現(xiàn)options的校驗(yàn)

  我們邏輯寫(xiě)完了,但是options還沒(méi)有校驗(yàn),這里可以用schema-utils這個(gè)工具來(lái)校驗(yàn),通過(guò)一個(gè)json對(duì)象來(lái)描述解構(gòu),然后調(diào)用validate的api來(lái)校驗(yàn)。webpack那么復(fù)雜的options就是通過(guò)這個(gè)工具校驗(yàn)的。

  schema如下,對(duì)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'],

  };

  五、測(cè)試

  代碼寫(xiě)完了,就到了測(cè)試環(huán)節(jié),測(cè)試的完善度直接決定了你這個(gè)工具可不可用。

圖片32

  options的測(cè)試就是傳入各種情況的options參數(shù),看報(bào)錯(cuò)信息是否正確。這里有個(gè)知識(shí)點(diǎn),因?yàn)閛ptions需要傳錯(cuò),所以肯定類(lèi)型不符合,使用as any as PluginOptions的雙重?cái)嘌钥梢岳@過(guò)類(lèi)型校驗(yàn)。

  describe('options格式測(cè)試', () => {

  const inputFilePath = path.resolve(

  __dirname,

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

  );

  it('env缺失會(huì)報(bào)錯(cuò)', () => {

  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只能是對(duì)象', () => {

  const pluginOptions: any = {

  env: 'production',

  additionalStyleMethods: [],

  };

  assertFileTransformThrows(

  inputFilePath,

  pluginOptions as PluginOptions,

  new RegExp(

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

  ),

  );

  });

  });

  主要的還是plugin邏輯的測(cè)試。

  @babel/core 提供了transformFileSync的api,可以對(duì)文件做處理,我封裝了一個(gè)工具函數(shù),對(duì)輸入文件做處理,把結(jié)果的內(nèi)容和另一個(gè)輸出文件做對(duì)比。

  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和其他環(huán)境的不同場(chǎng)景分別寫(xiě)了輸入文件actual和輸出文件expected。比如production下測(cè)試drop-all-console、drop-console-by-function等case,和下面的測(cè)試代碼一一對(duì)應(yīng)。

圖片33

  代碼里面是對(duì)各種情況的測(cè)試。

  describe('plugin邏輯測(cè)試', () => {

  describe('production環(huán)境', () => {

  it('默認(rèn)會(huì)刪除所有的console', () => {

  const pluginOptions: PluginOptions = {

  env: 'production',

  };

  assertFileTransformResultEqual(

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

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

  pluginOptions,

  );

  });

  it('可以通過(guò)name刪除指定console,支持glob', () => {...});

  it('可以通過(guò)function刪除指定參數(shù)的console', () => {...}

  });

  describe('其他環(huán)境', () => {

  it('非擴(kuò)展方法不做處理', () => {...});

  it('默認(rèn)擴(kuò)展了red 、green、blue、orange、 bgRed、bgGreen等方法,并且添加了行列數(shù)', () => {...});

  it('可以通過(guò)additionalStyleMethods擴(kuò)展方法,并且也會(huì)添加行列數(shù)', () => {...});

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

  });

  });

  六、總結(jié)

  這個(gè)插件雖然功能只是處理console,但細(xì)節(jié)還是蠻多的,比如刪除的時(shí)候要根據(jù)name和function確定是否刪除,name支持glob,非production環(huán)境要支持用戶(hù)自定義擴(kuò)展等等。

  技術(shù)方面,用了schema-utils做options校驗(yàn),用ts-mocha結(jié)合斷言庫(kù)chai做測(cè)試,同時(shí)設(shè)計(jì)了一個(gè)比較清晰的目錄結(jié)構(gòu)來(lái)組織測(cè)試代碼。

tags:
聲明:本站稿件版權(quán)均屬千鋒教育所有,未經(jīng)許可不得擅自轉(zhuǎn)載。
10年以上業(yè)內(nèi)強(qiáng)師集結(jié),手把手帶你蛻變精英
請(qǐng)您保持通訊暢通,專(zhuān)屬學(xué)習(xí)老師24小時(shí)內(nèi)將與您1V1溝通
免費(fèi)領(lǐng)取
今日已有369人領(lǐng)取成功
劉同學(xué) 138****2860 剛剛成功領(lǐng)取
王同學(xué) 131****2015 剛剛成功領(lǐng)取
張同學(xué) 133****4652 剛剛成功領(lǐng)取
李同學(xué) 135****8607 剛剛成功領(lǐng)取
楊同學(xué) 132****5667 剛剛成功領(lǐng)取
岳同學(xué) 134****6652 剛剛成功領(lǐng)取
梁同學(xué) 157****2950 剛剛成功領(lǐng)取
劉同學(xué) 189****1015 剛剛成功領(lǐng)取
張同學(xué) 155****4678 剛剛成功領(lǐng)取
鄒同學(xué) 139****2907 剛剛成功領(lǐng)取
董同學(xué) 138****2867 剛剛成功領(lǐng)取
周同學(xué) 136****3602 剛剛成功領(lǐng)取
相關(guān)推薦HOT
大數(shù)據(jù)測(cè)試工程師需要具備哪些技能?

一、理解大數(shù)據(jù)概念大數(shù)據(jù)測(cè)試工程師需要理解大數(shù)據(jù)的基本概念和原理,如分布式存儲(chǔ)、MapReduce、實(shí)時(shí)計(jì)算等。他們還需要了解如何處理大規(guī)模的...詳情>>

2023-10-14 23:43:03
為什么SpringBoot的 jar 可以直接運(yùn)行?

一、JAR文件的結(jié)構(gòu)與執(zhí)行方式Spring Boot的JAR包是Java Archive的縮寫(xiě),它是一種壓縮文件格式,可以將Java項(xiàng)目的類(lèi)文件、資源文件以及依賴(lài)庫(kù)等...詳情>>

2023-10-14 23:01:49
站群服務(wù)器是什么?

站群服務(wù)器的含義與用途站群服務(wù)器主要用于支持站群,即由一組相互鏈接的網(wǎng)站組成的群體。這些網(wǎng)站通常由同一組織或個(gè)人擁有,并且經(jīng)常會(huì)互相鏈...詳情>>

2023-10-14 22:46:12
自編碼器是什么?

一、自編碼器原理自編碼器的設(shè)計(jì)靈感源于神經(jīng)科學(xué)中關(guān)于感知系統(tǒng)的認(rèn)知原理,它的核心思想是將輸入數(shù)據(jù)經(jīng)過(guò)編碼過(guò)程,形成一個(gè)隱藏層的特征表示...詳情>>

2023-10-14 22:41:10
什么是云網(wǎng)融合?

一、云網(wǎng)融合的定義云網(wǎng)融合是指將云計(jì)算與網(wǎng)絡(luò)技術(shù)相結(jié)合,實(shí)現(xiàn)資源的共享、業(yè)務(wù)的協(xié)同,將網(wǎng)絡(luò)與云端服務(wù)深度融合,提供更靈活、高效、安全的...詳情>>

2023-10-14 22:31:47