babel 就是把ecma較新的js語法翻譯成瀏覽器能夠識別的解釋器,具體詳見 babel官網css
關於 plugin 的設計結構node
plugin 是一個很常見的設計結構了,往前看 jquery的時期,jquery 暴露了一個extend 方法,把插件都掛$.extend 下react
以後像webpack這樣的plugin,就是註冊了webpack的生命週期鉤子,而後到了生命週期觸發jquery
關於 plugin 的設計理念webpack
通常都是某個工具提供一個core,作最核心的運算,其餘部分功能開放給第三方,jquery webpack babel 都是這樣的。git
若是你時間很充裕的話,能夠先看一下 官方的handbook,知識點很全,知識點不少,可能消化要一點時間,可是仍是建議讀一下。github
handbookweb
提煉一些比較基礎,重要的知識點瀏覽器
babel-core會把代碼轉成ast樹,ast 是一個個節點信息,例如緩存
源碼: export default class {} 對應的 ast 節點就是
{
type: ExportDefaultDeclaration
start: 501,
end: 2128
declaration: {....}
}
複製代碼
ast 節點通常是不建議直接操做的,緣由很簡單ast的node 信息比較獨立,babel 把這些獨立的 node 經過一些描述信息拼接成了一個program(總體),這些描述信息的最小單元就是path,path 還暴露了對節點添加 刪除 移動的方法,比直接修改ast 也方便不少
path 和 ast 用一個比較官方的說法是reactive的,簡單說就是ast 變了, path 也變了,path 變了,ast 也變了 是個雙向綁定
一個最基礎的例子 對單文件transform 通常例子以下
var babel = require('babel-core');
var t = require('babel-types');
const code = `import {uniq, extend, flatten, cloneDeep } from "lodash"`;
const visitor = {
Identifier(path){
console.log(path.node.name)
}
}
const result = babel.transform(code, {
plugins: [{
visitor: visitor
}]
})
複製代碼
聲明 Identifier 類型的節點,parse 到 Identifier 時 輸出node 名字 輸出: uniq extend flatten cloneDeep
現狀:項目中樣式存在污染問題,通常代碼組織 一個jsx 對應一個scss文件,可是項目中的部分scss文件存在這樣的寫法
input{
}
textarea{
}
.form{
}
複製代碼
對應的 jsx 代碼
render () {
return (<div>
....
</div>)
複製代碼
目標:會parse jsx,若是最外層元素沒有設置classname,就手動加上classname = {$__dirname}_container
這樣, 而後把這個 class 包到scss文件最外面,對應的scss文件
.$__dirname}_container{
input{
}
textarea{
}
.form{
}
}
複製代碼
對應的 jsx 代碼
render () {
return (<div className =`{$__dirname}_container`>
....
</div>)
複製代碼
項目中的文件上千個,確定不能經過手動babel一個一個文件執行,確定要結合webpack,由於webpack自動會幫咱們收集依賴,有優點。
基本寫法,bable.config.js 寫下以下代碼
module.exports = {
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"ignore": [
"node_modules/**",
"dist"
],
"env": {
"test": {
"plugins": ["@babel/plugin-transform-runtime"]
}
},
"plugins": [
["founder-transform", {test: 'fz'}], //測試插件
["syntax-dynamic-import"],
["@babel/plugin-proposal-export-default-from"],
["@babel/plugin-proposal-export-namespace-from"],
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose" : true }],
["import", {
"libraryName": "mtui-d",
"libraryDirectory": "lib",
"style":true
}
]
]
}
複製代碼
這裏有個注意點,咱們自定義的插件必定要寫在plugin最前面,關於plugin的執行順序,引用官網的話
大體就是。plugin: founder-transform ...... import -> presets: @babel/preset-react @babel/preset-env
preset 也是一系列插件啦,因此你能夠想象時一排插件對ast進行改寫
#####開始實踐
是否是很簡單?
主流程就兩步
第二步就不說了,兩行代碼解決
let content = fs.readFileSync(path);
fs.writeFileSync(path, 'utf-8')
第一步也很簡單,嘩嘩寫下以下代碼,截取關鍵代碼吧
提取jsx文件,捕獲 import 'xxxx.scss' 記錄scss 文件路
ImportDeclaration(path, _ref = {opts:{}}){
co(function *(){
const specifiers = path.node.specifiers;
const {value} = path.node.source;
if(specifiers.length === 0 && value.slice(-5) === ".scss" && _ref.filename.indexOf('/container/') > -1){ //只對container目錄下文件改造
const cssPath = paths.join(_ref.filename, '..' , value);
let exist = yield statPromise(cssPath);
let source = exist && fs.readFileSync(cssPath).toString()
if(!containerJSCSS[_ref.filename]){
containerJSCSS[_ref.filename] = {}
}
if(!containerJSCSS[_ref.filename].cssPath){
containerJSCSS[_ref.filename].cssPath = []
}
containerJSCSS[_ref.filename].filename = _ref.filename;
containerJSCSS[_ref.filename].cssPath.push(cssPath);
containerJSCSS[_ref.filename].cssSource = source;
}
})
複製代碼
},
在 render 的jsx 最外層綁個classname
ExportDefaultDeclaration(path, _ref = {opts: {}}){
const declaration = path.node.declaration;
let code = _ref.file.code;
let ast = _ref.file.ast; //class
if(_ref.filename.indexOf('/container/') > -1 && declaration.type === "ClassDeclaration"){
if(declaration && declaration.body){ // render return()
let {body: declarationBody} = declaration.body;
let render = declarationBody && declarationBody.find(_ => {
return _.key.name === "render" && _.type === "ClassMethod"
})
if(render && render.body){
let {body: renderBody} = render.body;
let returnStatement = renderBody.find(_ => _.type === "ReturnStatement") || {};
let {argument} = returnStatement;
if(argument && argument.type === "JSXElement"){ // render return (<> </>)
let {openingElement: {attributes, name}} = argument;
if(name.type === "JSXIdentifier"){
if(!containerJSCSS[_ref.filename]){
containerJSCSS[_ref.filename] = {}
}
containerJSCSS[_ref.filename]['wrapElement'] = name.name
}
let attributesClass = attributes.find(_ => {
return _.name && _.name.name !== "className"
})
containerJSCSS[_ref.filename]['wrapElementClass'] = !!attributesClass
containerJSCSS[_ref.filename]['filename'] = _ref.filename;
attributes.push(t.jSXAttribute(t.JSXIdentifier('className'), t.stringLiteral(_ref.filename))); //綁classname
const output = generate(ast, {}, code);
output.code += `/** ${_ref.filename} **/\n`;
fs.writeFile('./data.jsx', output.code, {
flag: 'a'
}, (err) => {
if (err) throw err;
})
}
}
}
}
}
複製代碼
代碼很簡單,就是一步一步獲取export class 下的 render return( ) A元素罷了
可是當代碼跑起來的時候,出現了兩個問題
第一個問題很簡單,拆分一下
babel plugin 調用的時機?
webpack 自己的設計是管道的,一個文件處理完,下一個文件進來,拿到第一個jsx 過一遍babel-loader, 而後一次執行 plugin 到 preset的插件
babel-loader 作了啥?
關鍵代碼 result = yield transform(source, options);
不看babel-loader其餘優化,最基礎的操做就是transform了,source是源文件,option是參數,babel-loader傳的參數
既然咱們寫的插件是第一個執行的,爲啥code 不是源碼呢,還出現了es5的代碼 ?
猜測:babel 對每個語句類型 例如 ImportDeclaration 就是import 語句 看成一個鉤子,當捕獲到了import 的時候,會按照插件的順序依次下發,出現了es5的代碼,頗有多是其餘的插件有比ImportDeclaration更前的鉤子先修改了ast樹 驗證:
Program: {
enter(path, _ref = {opts:{}}) {
if( _ref.filename.indexOf('/container/') > -1 ){
let _ast = _ref.file.ast;
const output = generate(_ast, {}, _ref.file.code);
output.code += `/** ${_ref.filename} **/\n`;
fs.writeFile('./data.jsx', output.code, {
flag: 'a'
}, (err) => {
if (err) throw err;
})
}
}
}
複製代碼
咱們在最插件多增長了一個鉤子,這個鉤子是最早執行的,先於ImportDeclaration 的,打印出來就是源碼,哈哈哈哈
解決思路:
在 Program enter裏緩存住 ast 樹,在基於這個ast樹 生成代碼就行了,可是成本是巨大的,由於ast 是引用類型,你須要作一次深拷貝,否則仍是會被修改
第二個問題
由於babel 自己是基於管道的,他並不知道進入這個管道的文件是否是最後一個文件,咱們直接把這個變量掛在global 下,而後在 webpack compilation done的鉤子作後續的批量操做,代碼示例
const fs = require('fs');
class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
compiler.hooks.done.tap('done', compilation => {
fs.writeFile('./data.txt', JSON.stringify(global.containerJSCSS), 'utf-8', (err) => {
if (err) throw err;
})
});
}
}
module.exports.default = ConsoleLogOnBuildWebpackPlugin
複製代碼
若是不是特殊需求 別在babel裏作狀態翻轉的操做,例如以前我作的這樣
其餘plugin 的 鉤子已經把ast 改寫了,你的鉤子執行的邏輯又依賴的以前的ast,這樣很很差