關於webpack的loader小教程:如何刪除代碼中的console
在開發環境中,我們經常會加入很多console.log來做代碼的調試,但是我們並不希望當項目上線後,還會有打印的值,因此我們需要將這些console在上線前全部刪掉。雖然webpack4中已經集成了去除console的功能,但webpack3沒有這個功能,需要我們自己去處理。
如果有瀏覽過 webpack 官網的同學一定見過這張圖,這是webpack官方對自己的功能描述圖。webpack 能把左側各種類型的文件(在webpack中 它們都是模塊)打包爲右邊被通用瀏覽器支持的文件。
什麼是 Loader ?
在擼一個 loader 前,我們需要先知道它到底是什麼。本質上來說,loader 就是一個 node 模塊,在 webpack 的定義中,loader 導出一個函數,loader 會在轉換源模塊(resource)的時候調用該函數。在這個函數內部,我們可以通過傳入 this 上下文給 Loader API 來使用它們。
把 webpack 想像成一個工廠,loader 就是一個個身懷絕技的流水線工人,有的會處理 svg,有的會壓縮 css 或者圖片,有的會處理 less,有的會將 es6 轉換爲 es5。
回顧一下頭圖左邊的那些模塊,他們就是所謂的源模塊,會被 loader 轉化爲右邊的通用文件,因此我們也可以概括一下 loader 的功能:把源模塊轉換成通用模塊。
下面我來展示一個loader的基礎小功能,字符串轉換
基本示例:
// 如果有配置項,則會掛載到this上
module.exports = function (source) { // source 爲引入文件的源代碼(內容)
return source.replace('aaaaa', 'bbbbb') // 講代碼中所有的'aaaaa'轉換爲'bbbbb'
}
當然實際的loader並不會這麼粗暴的去做轉換,這裏僅僅是作爲一個入門案例
注意:我們刪除console的loader一定不能使用正則等其他方式,因爲正則刪除的時候,會將匹配到的內容全部刪除,可能會將我們寫在字符串中的相同內容一起刪掉
例:
console.log('我們想要刪除的console')
alert('這是一個字符串 console') // 如果是通過正則來做刪除,那麼連此處的字符串中的console也會被一起刪掉,這是錯誤的
正式開始
安裝依賴
下面我們正式開始寫一個可以刪除代碼中console的loader,首先我們需要做一些前置工作,下載我們在開發中所需要的一些依賴
- @babel/parser ---- 幫助我們分析代碼,並將代碼轉換爲抽象語法樹(AST)
- @babel/traverse ---- 幫助我們對抽象語法樹進行遍歷
- @babel/generator ---- 將抽象語法樹轉換回js代碼 注: 這裏也可以使用@babel/core,任選一個即可,只是使用上會稍有區別
- @babel/types ---- 對具體的AST節點進行增刪改查
書寫測試代碼
console.log('測試測試')
因爲我們的主要目的是刪除console,因此,在此處就不再書寫過多的代碼,我們直奔主題,刪掉它!
loader編寫
首先,我們創建一個deleteConsoleLoader.js,並將我們需要的依賴全部導入進來
const parser = require('@babel/parser') //將源代碼解析成AST
const traverse = require('@babel/traverse').default //對AST節點進行遞歸遍歷,生成一個便於操作、轉換的path對象
const generator = require('@babel/generator').default //將AST解碼回js代碼
const bableTypes = require('@babel/types') //對具體的AST節點進行增刪改查
module.exports = function (source) {
const ast = parser.parse(sourceStr, { sourceType: 'module' }) //支持ES6的module方式
console.log(ast); //打印一下我們的ast,看看他是個什麼東西
return null
}
打印結果:
Node {
type: 'File',
start: 0,
end: 28,
loc:
SourceLocation {
start: Position { line: 1, column: 0 },
end: Position { line: 1, column: 28 }
},
errors: [],
program:
Node {
type: 'Program',
start: 0,
end: 28,
loc: SourceLocation { start: [Position], end: [Position] },
sourceType: 'module',
interpreter: null,
body: [[Node]], // 這裏就是我們的代碼節點,可以看到只有一個node節點,就是剛剛我們測試代碼中的console
directives: []
},
comments: []
}
我們繼續通過console.log(ast.program.body)
把這個body打印出來,看看它的詳細內容
Node {
type: 'ExpressionStatement', // 代表是一個表達式語句,我們的console.log()
start: 0,
end: 28,
loc: SourceLocation { start: [Position], end: [Position] },
expression:
Node {
type: 'CallExpression', // 這裏是它的具體類型
start: 0,
end: 28,
loc: [SourceLocation],
callee: [Node],
arguments: [Array]
}
}
那麼下面就好辦了,通過@babel/traverse,我們可以很輕鬆的遍歷AST,同時,它還爲我們提供了很方便的獲取某種類型節點的方法,下面放上代碼
const parser = require('@babel/parser') //將源代碼解析成AST
const traverse = require('@babel/traverse').default //對AST節點進行遞歸遍歷,生成一個便於操作、轉換的path對象
const generator = require('@babel/generator').default //將AST解碼生成js代碼
const bableTypes = require('@babel/types') //對具體的AST節點進行增刪改查
module.exports = function (source) {
const ast = parser.parse(source, { sourceType: 'module' })
traverse(ast, { // 對ast進行遍歷
CallExpression (path) { // 如果節點類型爲CallExpression ,則會執行此函數,我們的console的type就是這個
console.log(path.node.callee) // 在這裏,我們打印path.node.callee
}
})
}
結果:
{
type: 'MemberExpression', // 這是整個console.log的類型
start: 0,
end: 11,
loc:
SourceLocation {
start: Position { line: 1, column: 0 },
end: Position { line: 1, column: 11 }
},
object:
Node {
type: 'Identifier', // 這裏是console和log的類型
start: 0,
end: 7,
loc:
SourceLocation { start: [Position], end: [Position], identifierName: 'console' },
name: 'console' // 這裏可以看到,是console.log的第一部分,log在下面的第二部分
},
property:
Node {
type: 'Identifier',
start: 8,
end: 11,
loc:
SourceLocation { start: [Position], end: [Position], identifierName: 'log' },
name: 'log'
},
computed: false
}
有了以上的鋪墊,我們就可以完成最後一步了,在上面那些步驟完成後,我們就獲得了刪除console所需要的所有條件
接下來,我們只需要將console所對應的類型及name作爲刪除的條件,就可以將代碼中所有的console.log刪除掉
直接上代碼
const parser = require('@babel/parser') //將源代碼解析成AST
const traverse = require('@babel/traverse').default //對AST節點進行遞歸遍歷,生成一個便於操作、轉換的path對象
const generator = require('@babel/generator').default //將AST解碼生成js代碼
const bableTypes = require('@babel/types') //對具體的AST節點進行增刪改查
module.exports = function (source) {
const ast = parser.parse(sourceStr, { sourceType: 'module' })
traverse(ast, {
CallExpression (path) {
// 刪除console
// 使用bableTypes 來對node節點的類型做判斷,如果節點的整體類型爲MemberExpression,並且子節點object的類型爲Identifier,同時節點中的name又爲console
if (bableTypes.isMemberExpression(path.node.callee) && bableTypes.isIdentifier(path.node.callee.object, { name: "console" })) {
path.remove() // 那麼將這個節點刪除掉
}
}
})
let output = generator(ast, {}); // 通過@babel/generator將AST重新解碼回js
return output.code // 最後將解碼好的代碼返回,給下一個loader使用
}
loader開發完成
那麼一個簡單的刪除console的loader就開發完成了,爲了驗證,我們可以對比使用loader前後打包代碼的區別
使用前:
/*! no static exports found */
/***/ (function(module, exports) {
eval("console.log('測試測試');\n\n//# sourceURL=webpack:///./src/index.js?");
/***/ })
/******/ });
使用後:
/***/ (function(module, exports) {
eval("\n\n//# sourceURL=webpack:///./src/index.js?");
/***/ })
/******/ });
可以看到console.log已經被刪除掉了,那麼我們的loader就完成了