【轉(zhuǎn)向JavaScript系列】AST in Modern JavaScript

What is AST

什么是AST?AST是Abstract Syntax Tree(抽象語(yǔ)法樹(shù))的縮寫。
傳說(shuō)中的程序員三大浪漫是編譯原理、圖形學(xué)、操作系統(tǒng),不把AST玩轉(zhuǎn)牺荠,顯得逼格不夠,而本文目標(biāo)就是為你揭示AST在現(xiàn)代化JavaScript項(xiàng)目中的應(yīng)用驴一。

var a = 42
function addA(d){
  return a + d;
}

按照語(yǔ)法規(guī)則書寫的代碼休雌,是用來(lái)讓開(kāi)發(fā)者可閱讀、可理解的肝断。對(duì)編譯器等工具來(lái)講杈曲,它可以理解的就是抽象語(yǔ)法樹(shù)了,在網(wǎng)站javascript-ast里胸懈,可以直觀看到由源碼生成的圖形化語(yǔ)法樹(shù)

生成抽象語(yǔ)法樹(shù)需要經(jīng)過(guò)兩個(gè)階段:

  • 分詞(tokenize)
  • 語(yǔ)義分析(parse)

其中担扑,分詞是將源碼source code分割成語(yǔ)法單元,語(yǔ)義分析是在分詞結(jié)果之上分析這些語(yǔ)法單元之間的關(guān)系趣钱。

以var a = 42這句代碼為例涌献,簡(jiǎn)單理解,可以得到下面分詞結(jié)果

[
    {type:'identifier',value:'var'},
    {type:'whitespace',value:' '},    
    {type:'identifier',value:'a'},
    {type:'whitespace',value:' '},
    {type:'operator',value:'='},
    {type:'whitespace',value:' '},
    {type:'num',value:'42'},
    {type:'sep',value:';'}
]

實(shí)際使用babylon6解析這一代碼時(shí)首有,分詞結(jié)果為



生成的抽象語(yǔ)法樹(shù)為

{
    "type":"Program",
    "body":[
        {
            "type":"VariableDeclaration",
            "kind":"var",
            "declarations":{
                "type":"VariableDeclarator",
                "id":{
                    "type":"Identifier",
                    "value":"a"
                },
                "init":{
                    "type":"Literal",
                    "value":42
                }
            }
        }
    ]
}

社區(qū)中有各種AST parser實(shí)現(xiàn)

AST in ESLint

ESLint是一個(gè)用來(lái)檢查和報(bào)告JavaScript編寫規(guī)范的插件化工具利术,通過(guò)配置規(guī)則來(lái)規(guī)范代碼,以no-cond-assign規(guī)則為例低矮,啟用這一規(guī)則時(shí),代碼中不允許在條件語(yǔ)句中賦值被冒,這一規(guī)則可以避免在條件語(yǔ)句中军掂,錯(cuò)誤的將判斷寫成賦值

//check ths user's job title
if(user.jobTitle = "manager"){
  user.jobTitle is now incorrect
}

ESLint的檢查基于AST轮蜕,除了這些內(nèi)置規(guī)則外,ESLint為我們提供了API蝗锥,使得我們可以利用源代碼生成的AST跃洛,開(kāi)發(fā)自定義插件和自定義規(guī)則。

module.exports = {
    rules: {
        "var-length": {
            create: function (context) {
                //規(guī)則實(shí)現(xiàn)
            }
        }
    }
};

自定義規(guī)則插件的結(jié)構(gòu)如上终议,在create方法中汇竭,我們可以定義我們關(guān)注的語(yǔ)法單元類型并且實(shí)現(xiàn)相關(guān)的規(guī)則邏輯,ESLint會(huì)在遍歷語(yǔ)法樹(shù)時(shí)穴张,進(jìn)入對(duì)應(yīng)的單元類型時(shí)细燎,執(zhí)行我們的檢查邏輯。

比如我們要實(shí)現(xiàn)一條規(guī)則皂甘,要求賦值語(yǔ)句中玻驻,變量名長(zhǎng)度大于兩位

module.exports = {
    rules: {
        "var-length": {
            create: function (context) {
                return {
                    VariableDeclarator: node => {
                        if (node.id.name.length < 2) {
                            context.report(node, 'Variable names should be longer than 1 character');
                        }
                    }
                };
            }
        }
    }
};

為這一插件編寫package.json

{
    "name": "eslint-plugin-my-eslist-plugin",
    "version": "0.0.1",
    "main": "index.js",
    "devDependencies": {
        "eslint": "~2.6.0"
    },
    "engines": {
        "node": ">=0.10.0"
    }
}

在項(xiàng)目中使用時(shí),通過(guò)npm安裝依賴后偿枕,在配置中啟用插件和對(duì)應(yīng)規(guī)則

"plugins": [
    "my-eslint-plugin"
]

"rules": {
    "my-eslint-plugin/var-length": "warn"
}

通過(guò)這些配置璧瞬,便可以使用上述自定義插件。

有時(shí)我們不想要發(fā)布新的插件渐夸,而僅想編寫本地自定義規(guī)則嗤锉,這時(shí)我們可以通過(guò)自定義規(guī)則來(lái)實(shí)現(xiàn)。自定義規(guī)則與插件結(jié)構(gòu)大致相同墓塌,如下是一個(gè)自定義規(guī)則瘟忱,禁止在代碼中使用console的方法調(diào)用。

const disallowedMethods = ["log", "info", "warn", "error", "dir"];
module.exports = {
    meta: {
        docs: {
            description: "Disallow use of console",
            category: "Best Practices",
            recommended: true
        }
    },
    create(context) {
        return {
            Identifier(node) {
                const isConsoleCall = looksLike(node, {
                    name: "console",
                    parent: {
                        type: "MemberExpression",
                        property: {
                            name: val => disallowedMethods.includes(val)
                        }
                    }
                });
                // find the identifier with name 'console'
                if (!isConsoleCall) {
                    return;
                }

                context.report({
                    node,
                    message: "Using console is not allowed"
                });
            }
        };
    }
};

AST in Babel

Babel是為使用下一代JavaScript語(yǔ)法特性來(lái)開(kāi)發(fā)而存在的編譯工具桃纯,最初這個(gè)項(xiàng)目名為6to5酷誓,意為將ES6語(yǔ)法轉(zhuǎn)換為ES5。發(fā)展到現(xiàn)在态坦,Babel已經(jīng)形成了一個(gè)強(qiáng)大的生態(tài)盐数。


業(yè)界大佬的評(píng)價(jià):Babel is the new jQuery


Babel的工作過(guò)程經(jīng)過(guò)三個(gè)階段,parse伞梯、transform玫氢、generate,具體來(lái)說(shuō)谜诫,如下圖所示漾峡,在parse階段,使用babylon庫(kù)將源代碼轉(zhuǎn)換為AST喻旷,在transform階段生逸,利用各種插件進(jìn)行代碼轉(zhuǎn)換,如圖中的JSX transform將React JSX轉(zhuǎn)換為plain object,在generator階段槽袄,再利用代碼生成工具烙无,將AST轉(zhuǎn)換成代碼。


Babel為我們提供了API讓我們可以對(duì)代碼進(jìn)行AST轉(zhuǎn)換并且進(jìn)行各種操作

import * as babylon from "babylon";
import traverse from "babel-traverse";
import generate from "babel-generator";

const code = `function square(n) {
    return n * n;
}`

const ast = babylon.parse(code);
traverse(ast,{
    enter(path){
        if(path.node.type === 'Identifier' && path.node.name === 'n'){
            path.node.name = 'x'
        }
    }
})
generate(ast,{},code)

直接使用這些API的場(chǎng)景倒不多遍尺,項(xiàng)目中經(jīng)常用到的截酷,是各種Babel插件,比如 babel-plugin-transform-remove-console插件乾戏,可以去除代碼中所有對(duì)console的方法調(diào)用迂苛,主要代碼如下

module.exports = function({ types: t }) {
  return {
    name: "transform-remove-console",
    visitor: {
      CallExpression(path, state) {
        const callee = path.get("callee");

        if (!callee.isMemberExpression()) return;

        if (isIncludedConsole(callee, state.opts.exclude)) {
          // console.log()
          if (path.parentPath.isExpressionStatement()) {
            path.remove();
          } else {
          //var a = console.log()
            path.replaceWith(createVoid0());
          }
        } else if (isIncludedConsoleBind(callee, state.opts.exclude)) {
          // console.log.bind()
          path.replaceWith(createNoop());
        }
      },
      MemberExpression: {
        exit(path, state) {
          if (
            isIncludedConsole(path, state.opts.exclude) &&
            !path.parentPath.isMemberExpression()
          ) {
          //console.log = func
            if (
              path.parentPath.isAssignmentExpression() &&
              path.parentKey === "left"
            ) {
              path.parentPath.get("right").replaceWith(createNoop());
            } else {
            //var a = console.log
              path.replaceWith(createNoop());
            }
          }
        }
      }
    }
  };

使用這一插件,可以將程序中如下調(diào)用進(jìn)行轉(zhuǎn)換

console.log()
var a = console.log()
console.log.bind()
var b = console.log
console.log = func

//output
var a = void 0
(function(){})
var b = function(){}
console.log = function(){}

上述Babel插件的工作方式與前述的ESLint自定義插件/規(guī)則類似鼓择,工具在遍歷源碼生成的AST時(shí)三幻,根據(jù)我們指定的節(jié)點(diǎn)類型進(jìn)行對(duì)應(yīng)的檢查。

在我們開(kāi)發(fā)插件時(shí)惯退,是如何確定代碼AST樹(shù)形結(jié)構(gòu)呢赌髓?可以利用AST explorer方便的查看源碼生成的對(duì)應(yīng)AST結(jié)構(gòu)。

AST in Codemod

Codemod可以用來(lái)幫助你在一個(gè)大規(guī)模代碼庫(kù)中催跪,自動(dòng)化修改你的代碼锁蠕。
jscodeshift是一個(gè)運(yùn)行codemods的JavaScript工具,主要依賴于recast和ast-types兩個(gè)工具庫(kù)懊蒸。recast作為JavaScript parser提供AST接口荣倾,ast-types提供類型定義。

利用jscodeshift接口骑丸,完成前面類似功能舌仍,將代碼中對(duì)console的方法調(diào)用代碼刪除

export default (fileInfo,api)=>{
    const j = api.jscodeshift;
    
    const root = j(fileInfo.source);
    
    const callExpressions = root.find(j.CallExpression,{
        callee:{
            type:'MemberExpression',
            object:{
                type:'Identifier',
                name:'console'
            }
        }
    });
    
    callExpressions.remove();
    
    return root.toSource();
}

如果想要代碼看起來(lái)更加簡(jiǎn)潔,也可以使用鏈?zhǔn)紸PI調(diào)用

export default (fileInfo,api)=>{
    const j = api.jscodeshift;

    return j(fileInfo.source)
        .find(j.CallExpression,{
            callee:{
                type:'MemberExpression',
                object:{
                    type:'Identifier',
                    name:'console'
                }
            }
        })
        .remove()
        .toSource();
}

在了解了jscodeshift之后通危,頭腦中立即出現(xiàn)了一個(gè)疑問(wèn)铸豁,就是我們?yōu)槭裁葱枰猨scodeshift呢?利用AST進(jìn)行代碼轉(zhuǎn)換菊碟,Babel不是已經(jīng)完全搞定了嗎节芥?

帶著這個(gè)問(wèn)題進(jìn)行一番搜索驱闷,發(fā)現(xiàn)Babel團(tuán)隊(duì)這處提交說(shuō)明babel-core: add options for different parser/generator娄琉。

前文提到,Babel處理流程中包括了parse羔砾、transform和generation三個(gè)步驟魄幕。在生成代碼的階段相艇,Babel不關(guān)心生成代碼的格式,因?yàn)樯傻木幾g過(guò)的代碼目標(biāo)不是讓開(kāi)發(fā)者閱讀的纯陨,而是生成到發(fā)布目錄供運(yùn)行的坛芽,這個(gè)過(guò)程一般還會(huì)對(duì)代碼進(jìn)行壓縮處理留储。

這一次過(guò)程在使用Babel命令時(shí)也有體現(xiàn),我們一般使用的命令形式為

babel src -d dist

而在上述場(chǎng)景中靡馁,我們的目標(biāo)是在代碼庫(kù)中欲鹏,對(duì)源碼進(jìn)行處理,這份經(jīng)過(guò)處理的代碼仍需是可讀的臭墨,我們?nèi)砸谶@份代碼上進(jìn)行開(kāi)發(fā),這一過(guò)程如果用Babel命令來(lái)體現(xiàn)膘盖,實(shí)際是這樣的過(guò)程

babel src -d src

在這樣的過(guò)程中胧弛,我們會(huì)檢查轉(zhuǎn)換腳本對(duì)源代碼到底做了哪些變更,來(lái)確認(rèn)我們的轉(zhuǎn)換正確性侠畔。這就需要這一個(gè)差異結(jié)果是可讀的结缚,而直接使用Babel完成上述轉(zhuǎn)換時(shí),使用git diff輸出差異結(jié)果時(shí)软棺,這份差異結(jié)果是混亂不可讀的红竭。

基于這個(gè)需求,Babel團(tuán)隊(duì)現(xiàn)在允許通過(guò)配置自定義parser和generator

{
    "plugins":[
        "./plugins.js"
    ],
    "parserOpts":{
        "parser":"recast"
    },
    "generatorOpts":{
        "generator":"recast"
    }
}

假設(shè)我們有如下代碼喘落,我們通過(guò)腳本茵宪,將代碼中import模式進(jìn)行修改

import fs, {readFile} from 'fs'
import {resolve} from 'path'
import cp from 'child_process'

resolve(__dirname, './thing')

readFile('./thing.js', 'utf8', (err, string) => {
  console.log(string)
})

fs.readFile('./other-thing', 'utf8', (err, string) => {
  const resolve = string => string
  console.log(resolve())
})

cp.execSync('echo "hi"')

//轉(zhuǎn)換為
import fs from 'fs';
import _path from 'path';
import cp from 'child_process'

_path.resolve(__dirname, './thing')

fs.readFile('./thing.js', 'utf8', (err, string) => {
  console.log(string)
})

fs.readFile('./other-thing', 'utf8', (err, string) => {
  const resolve = string => string
  console.log(resolve())
})

cp.execSync('echo "hi"')

完成這一轉(zhuǎn)換的plugin.js為

module.exports = function(babel) {
  const { types: t } = babel
  // could just use https://www.npmjs.com/package/is-builtin-module
  const nodeModules = [
    'fs', 'path', 'child_process',
  ]

  return {
    name: 'node-esmodule', // not required
    visitor: {
      ImportDeclaration(path) {
        const specifiers = []
        let defaultSpecifier
        path.get('specifiers').forEach(specifier => {
          if (t.isImportSpecifier(specifier)) {
            specifiers.push(specifier)
          } else {
            defaultSpecifier = specifier
          }
        })
        const {node: {value: source}} = path.get('source')
        if (!specifiers.length || !nodeModules.includes(source)) {
          return
        }
        let memberObjectNameIdentifier
        if (defaultSpecifier) {
          memberObjectNameIdentifier = defaultSpecifier.node.local
        } else {
          memberObjectNameIdentifier = path.scope.generateUidIdentifier(source)
          path.node.specifiers.push(t.importDefaultSpecifier(memberObjectNameIdentifier))
        }
        specifiers.forEach(specifier => {
          const {node: {imported: {name}}} = specifier
          const {referencePaths} = specifier.scope.getBinding(name)
          referencePaths.forEach(refPath => {
            refPath.replaceWith(
              t.memberExpression(memberObjectNameIdentifier, t.identifier(name))
            )
          })
          specifier.remove()
        })
      }
    }
  }
}

刪除和加上parserOpts和generatorOpts設(shè)置允許兩次,使用git diff命令輸出結(jié)果瘦棋,可以看出明顯的差異


使用recast

不使用recast

AST in Webpack

Webpack是一個(gè)JavaScript生態(tài)的打包工具稀火,其打出bundle結(jié)構(gòu)是一個(gè)IIFE(立即執(zhí)行函數(shù))

(function(module){})([function(){},function(){}]);

Webpack在打包流程中也需要AST的支持,它借助acorn庫(kù)解析源碼赌朋,生成AST凰狞,提取模塊依賴關(guān)系


在各類打包工具中,由Rollup提出沛慢,Webpack目前也提供支持的一個(gè)特性是treeshaking赡若。treeshaking可以使得打包輸出結(jié)果中,去除沒(méi)有引用的模塊团甲,有效減少包的體積逾冬。

//math.js
export {doMath, sayMath}

const add = (a, b) => a + b
const subtract = (a, b) => a - b
const divide = (a, b) => a / b
const multiply = (a, b) => a * b

function doMath(a, b, operation) {
  switch (operation) {
    case 'add':
      return add(a, b)
    case 'subtract':
      return subtract(a, b)
    case 'divide':
      return divide(a, b)
    case 'multiply':
      return multiply(a, b)
    default:
      throw new Error(`Unsupported operation: ${operation}`)
  }
}

function sayMath() {
  return 'MATH!'
}

//main.js
import {doMath}
doMath(2, 3, 'multiply') // 6

上述代碼中,math.js輸出doMath,sayMath方法伐庭,main.js中僅引用doMath方法粉渠,采用Webpack treeshaking特性,再加上uglify的支持圾另,在輸出的bundle文件中霸株,可以去掉sayMath相關(guān)代碼,輸出的math.js形如

export {doMath}

const add = (a, b) => a + b
const subtract = (a, b) => a - b
const divide = (a, b) => a / b
const multiply = (a, b) => a * b

function doMath(a, b, operation) {
  switch (operation) {
    case 'add':
      return add(a, b)
    case 'subtract':
      return subtract(a, b)
    case 'divide':
      return divide(a, b)
    case 'multiply':
      return multiply(a, b)
    default:
      throw new Error(`Unsupported operation: ${operation}`)
  }
}

進(jìn)一步分析main.js中的調(diào)用集乔,doMath(2, 3, 'multiply') 調(diào)用僅會(huì)執(zhí)行doMath的一個(gè)分支去件,math.js中定義的一些help方法如add,subtract,divide實(shí)際是不需要的坡椒,理論上,math.js最優(yōu)可以被減少為

export {doMath}

const multiply = (a, b) => a * b

function doMath(a, b) {
  return multiply(a, b)
}

基于AST尤溜,進(jìn)行更為完善的代碼覆蓋率分析倔叼,應(yīng)當(dāng)可以實(shí)現(xiàn)上述效果,這里只是一個(gè)想法宫莱,沒(méi)有具體的實(shí)踐丈攒。參考Faster JavaScript with SliceJS

參考文章

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市授霸,隨后出現(xiàn)的幾起案子巡验,更是在濱河造成了極大的恐慌,老刑警劉巖碘耳,帶你破解...
    沈念sama閱讀 218,640評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件显设,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡辛辨,警方通過(guò)查閱死者的電腦和手機(jī)捕捂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,254評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)斗搞,“玉大人指攒,你說(shuō)我怎么就攤上這事“竦” “怎么了幽七?”我有些...
    開(kāi)封第一講書人閱讀 165,011評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)溅呢。 經(jīng)常有香客問(wèn)我澡屡,道長(zhǎng),這世上最難降的妖魔是什么咐旧? 我笑而不...
    開(kāi)封第一講書人閱讀 58,755評(píng)論 1 294
  • 正文 為了忘掉前任驶鹉,我火速辦了婚禮,結(jié)果婚禮上铣墨,老公的妹妹穿的比我還像新娘室埋。我一直安慰自己,他們只是感情好伊约,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,774評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布姚淆。 她就那樣靜靜地躺著,像睡著了一般屡律。 火紅的嫁衣襯著肌膚如雪腌逢。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,610評(píng)論 1 305
  • 那天超埋,我揣著相機(jī)與錄音搏讶,去河邊找鬼佳鳖。 笑死,一個(gè)胖子當(dāng)著我的面吹牛媒惕,可吹牛的內(nèi)容都是我干的系吩。 我是一名探鬼主播,決...
    沈念sama閱讀 40,352評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼妒蔚,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼穿挨!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起面睛,我...
    開(kāi)封第一講書人閱讀 39,257評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤絮蒿,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后叁鉴,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,717評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡佛寿,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,894評(píng)論 3 336
  • 正文 我和宋清朗相戀三年幌墓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片冀泻。...
    茶點(diǎn)故事閱讀 40,021評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡常侣,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出弹渔,到底是詐尸還是另有隱情胳施,我是刑警寧澤,帶...
    沈念sama閱讀 35,735評(píng)論 5 346
  • 正文 年R本政府宣布肢专,位于F島的核電站舞肆,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏博杖。R本人自食惡果不足惜椿胯,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,354評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望剃根。 院中可真熱鬧哩盲,春花似錦、人聲如沸狈醉。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,936評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)苗傅。三九已至抒线,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間金吗,已是汗流浹背十兢。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,054評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工趣竣, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人旱物。 一個(gè)月前我還...
    沈念sama閱讀 48,224評(píng)論 3 371
  • 正文 我出身青樓遥缕,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親宵呛。 傳聞我的和親對(duì)象是個(gè)殘疾皇子单匣,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,974評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • ESLint 配置 ESlint 被設(shè)計(jì)為完全可配置的,這意味著你可以關(guān)閉每一個(gè)規(guī)則而只運(yùn)行基本語(yǔ)法驗(yàn)證宝穗,或混合和...
    靜默虛空閱讀 41,307評(píng)論 3 14
  • 前言 webpack2和vue2已經(jīng)不是新鮮東西了户秤,滿大街的文章在講解webpack和vue,但是很多內(nèi)容寫的不是...
    技術(shù)宅小青年閱讀 6,541評(píng)論 4 43
  • EsLint入門學(xué)習(xí)整理 這兩天因?yàn)楣疽蟠蛯?duì)ESLint進(jìn)行了初步的了解鸡号,網(wǎng)上的內(nèi)容基本上都差不多,但是內(nèi)容...
    點(diǎn)柈閱讀 26,027評(píng)論 3 42
  • 寫在開(kāi)頭 先說(shuō)說(shuō)為什么要寫這篇文章, 最初的原因是組里的小朋友們看了webpack文檔后, 表情都是這樣的: (摘...
    Lefter閱讀 5,290評(píng)論 4 31
  • 飽暖之后须鼎,前進(jìn)的動(dòng)力何在: 對(duì)人類而言鲸伴,在擺脫了饑餓之后,有兩種體驗(yàn)就變得極為重要晋控。 1汞窗,最深刻的需求就是運(yùn)用技能...
    馬唐閱讀 145評(píng)論 0 0