構建webpack生產環境
我們編寫的babel外掛是所屬於babel-loader,而babel-loader基本執行與webpack環境.所以為了檢測babel外掛的是否起作用,我們必須構建webpack環境.
目錄結構
|-- babel-plugin-wyimport
|-- .editorconfig
|-- .gitignore
|-- package.json
|-- README.md
|-- build
| |-- app.be45e566.js
| |-- index.html
|-- config
| |-- paths.js
| |-- webpack.dev.config.js
| |-- webpack.prod.config.js
|-- scripts
| |-- build.js
| |-- start.js
|-- src
|-- index.js
複製程式碼
webpack.prod.config.js
配置檔案,沒有對程式碼進行壓縮和混淆,主要為了方便對比編譯前後的檔案內容
`use strict`
process.env.BABEL_ENV = `production`;
process.env.NODE_ENV = `production`;
const path = require(`path`);
const paths = require("./paths");
const fs = require(`fs`);
const webpack = require("webpack");
const ExtractTextPlugin = require(`extract-text-webpack-plugin`);
const { WebPlugin } = require(`web-webpack-plugin`);
module.exports = {
output: {
path: paths.build,
filename: `[name].[chunkhash:8].js`,
chunkFilename: `static/js/[name].[chunkhash:8].chunk.js`,
publicPath: "/"
},
entry: {
"app":path.resolve(paths.src, "index.js")
},
resolve:{
extensions:[".js", ".json"],
modules: ["node_modules", paths.src]
},
module: {
rules: [
{
test:/.css$/,
include:paths.src,
loader: ExtractTextPlugin.extract({
use: [
{
options:{
root: path.resolve(paths.src, "images")
},
loader: require.resolve(`css-loader`)
}
]
})
},
{
test:/.less$/,
include:paths.src,
use:[
require.resolve(`style-loader`),
{
loader:require.resolve(`css-loader`),
options:{
root: path.resolve(paths.src, "images")
}
},
{
loader: require.resolve(`less-loader`)
}
]
},
{
test: [/.bmp$/, /.gif$/, /.jpe?g$/, /.png$/],
loader: require.resolve(`url-loader`),
options: {
limit: 1000,
name: `static/images/[name].[hash:8].[ext]`,
},
},
{
test:/.(js|jsx)$/,
include:paths.src,
loader: require.resolve("babel-loader"),
options:{
presets:["react-app"],
plugins:[
//["wyimport", {libraryName:"lodash"}]
],
compact: true
//cacheDirectory: true
}
},
{
exclude: [
/.html$/,
/.(js|jsx)$/,
/.css$/,
/.less$/,
/.json$/,
/.bmp$/,
/.gif$/,
/.jpe?g$/,
/.png$/,
/.svg$/
],
loader: require.resolve(`file-loader`),
options: {
name: `static/[name].[hash:8].[ext]`,
},
}
]
},
plugins: [
new ExtractTextPlugin(`[name].[chunkhash:8].css`),
new WebPlugin({
//輸出的html檔名稱
filename: `index.html`,
//這個html依賴的`entry`
requires:["app"]
}),
]
}
複製程式碼
build.js
啟動檔案,主要計算編譯前後的檔案內容大小
const webpack = require(`webpack`);
const path = require(`path`);
const config = require(`../config/webpack.prod.config`);
const chalk = require(`chalk`);
const paths = require(`../config/paths`);
const fs = require("fs");
// 獲取目錄大小
const getDirSize = (rootPath, unit ="k") => {
if (!fs.existsSync(rootPath)) {
return 0;
}
let buildSize = 0;
const dirSize = (dirPath) => {
let files = fs.readdirSync(dirPath, "utf-8")
files.forEach((files) => {
let filePath = path.resolve(dirPath, files);
let stat = fs.statSync(filePath) || [];
if (stat.isDirectory()){
dirSize(filePath)
} else {
buildSize += stat.size
}
})
}
dirSize(rootPath)
let map = new Map([["k",(buildSize/1024).toFixed(2)+"k"], ["M",buildSize/1024/1024+"M"]])
return map.get(unit);
}
// 清空目錄檔案
const rmDir = (path, isDeleteDir) => {
if(fs.existsSync(path)) {
files = fs.readdirSync(path);
files.forEach(function(file, index) {
var curPath = path + "/" + file;
if(fs.statSync(curPath).isDirectory()) { // recurse
rmDir(curPath);
} else { // delete file
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(path);
}
}
const measureFileBeforeBuild = () => {
console.log(`打包之前build資料夾的大小: ${chalk.green(getDirSize(paths.build))}
`)
rmDir(paths.build) //刪除build資料夾
return build().then((stats) => {
console.log(chalk.green(`打包完成
`))
console.log(`打包之後資料夾大小:${chalk.green(getDirSize(paths.build))} 花費時間: ${chalk.green((stats.endTime-stats.startTime)/1000)}s`)
}, err => {
console.log(chalk.red(`Failed to compile.
`));
console.log((err.message || err) + `
`);
process.exit(1);
})
}
const build = () => {
const compiler = webpack(config)
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
console.log(chalk.green("開始打包..."))
if (err) {
return reject(err);
}
const message = stats.toJson({}, true)
if (message.errors.length) {
return reject(message.errors);
}
return resolve(stats)
})
})
}
measureFileBeforeBuild()
複製程式碼
小試牛刀
我們在src/index.js
檔案裡面輸入
import { uniq } from "lodash"
複製程式碼
然後 npm run build
大小是531k,很明顯lodash被全部引入了進來了,所以這樣引入lodash庫的同學注意咯!
正常我們應該這樣寫來按需載入
//import { uniq } from "lodash"
import uniq from "lodash/uniq"
複製程式碼
然後 npm run build
如果一個檔案引入lodash很多方法如
import uniq from "lodash/uniq";
import extend from "lodash/extend";
import flatten from "lodash/flatten";
import cloneDeep from "lodash/cloneDeep";
...
複製程式碼
這樣的寫法就相當臃腫,那麼能不能這麼寫import {uniq, extend, flatten, cloneDeep } from "lodash"
並且也實現按需載入呢? 很簡單,只要將它編譯輸出成
import uniq from "lodash/uniq";
import extend from "lodash/extend";
import flatten from "lodash/flatten";
import cloneDeep from "lodash/cloneDeep";
複製程式碼
就可以了
知識準備
編寫plugin之前首先我們要清楚以下二點
- plugin在什麼時候起作用?
- plugin是如何起作用
webpack編譯原理
babel-loader
作為webpack
的一個loader
.首先我們弄清楚webpack的編譯過程和loader
在webpack
中作用
這裡有一篇文章說很好,大家先去閱讀理解之後再往下看
babel的基本概念
知乎有一篇文章講得比較清楚,對babel不是很清楚的同學先進去瞭解之後再往下看!
在這裡,我主要想強調一下
babel
引數的配置,如果我寫了一個名叫fiveone
的babel
外掛,我在引數中這麼配置
{
presets:["react-app", "es2015"],
plugins:[
["fiveone", {libraryName:"lodash"}],
["transform-runtime", {}]
],
}
起作用的順序為fiveone->transform-runtime->es2015->react-app
複製程式碼
編譯順序為首先plugins
從左往右然後presets
從右往左
babel編譯原理
上面二節解釋了plugin
在什麼時候起作用,下面解釋一下plugin
如何起作用?
babylon
直譯器把程式碼字串轉化為AST樹, 例如import {uniq, extend, flatten, cloneDeep } from "lodash"
轉化為AST
樹
babel-traverse
對AST
樹進行解析遍歷出整個樹的path.- plugin轉換出新的
AST
樹. - 輸出新的程式碼字串
文獻地址
我們要編寫的plugin在第三步.通過path來轉換出新的
AST
樹?下面我們就開始如何進行第三步!
開始babel-plugin
首先我們需要安裝二個工具babel-core
和babel-types
;
npm install --save babel-core babel-types
;
babel-core
提供transform
方法將程式碼字串轉換為AST
樹babel-types
提供各種操作AST
節點的工具庫
我們在src/index.js
中輸入
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
}]
})
複製程式碼
執行node src index.js
visitor
babel對AST
樹進行遍歷,遍歷的過程會提供一個叫visitor物件的方法對某個階段訪問, 例如上面的
Identifier(path){
console.log(path.node.name)
}
複製程式碼
就是訪問了Identifier節點,AST
樹展開如下
為什麼會輸出二個uniq
,因為每個節點進入和退出都會呼叫該方法。
遍歷會有二次,一個是像下遍歷進入,一個是像上遍歷退出.
我們將src/index.js
中的Identifier
方法改為
Identifier:{
enter(path) {
console.log("我是進入的:",path.node.name)
},
exit(path) {
console.log("我是進入的:",path.node.name)
}
}
複製程式碼
執行node src index.js
遍歷流程: 向下遍歷-進入uniq->退出uniq->向上遍歷-進入uniq->退出uniq
path
path 表示兩個節點之間的連線,通過這個物件我們可以訪問到當前節點、子節點、父節點和對節點的增、刪、修改、替換等等一些操作。下面演示將uniq
替換_uniq
程式碼如下:
var babel = require(`babel-core`);
var t = require(`babel-types`);
const code = `import {uniq, extend, flatten, cloneDeep } from "lodash"`;
const visitor = {
Identifier(path){
if (path.node.name == "uniq") {
var newIdentifier = t.identifier(`_uniq`) //建立一個名叫_uniq的新identifier節點
path.replaceWith(newIdentifier) //把當前節點替換成新節點
}
}
}
const result = babel.transform(code, {
plugins: [{
visitor: visitor
}]
})
console.log(result.code) //import { _uniq, extend, flatten, cloneDeep } from "lodash";
複製程式碼
開始
有了以上概念我們現在把程式碼字串import {uniq, extend, flatten, cloneDeep } from "lodash"
轉化成
import uniq from "lodash/uniq";
import extend from "lodash/extend";
import flatten from "lodash/flatten";
import cloneDeep from "lodash/cloneDeep";
複製程式碼
程式碼如下
var babel = require(`babel-core`);
var t = require(`babel-types`);
const code = `import {uniq, extend, flatten, cloneDeep } from "lodash"`;
const visitor = {
ImportDeclaration(path, _ref = {opts:{}}){
const specifiers = path.node.specifiers;
const source = path.node.source;
if (!t.isImportDefaultSpecifier(specifiers[0]) ) {
var declarations = specifiers.map((specifier, i) => { //遍歷 uniq extend flatten cloneDeep
return t.ImportDeclaration( //建立importImportDeclaration節點
[t.importDefaultSpecifier(specifier.local)],
t.StringLiteral(`${source.value}/${specifier.local.name}`)
)
})
path.replaceWithMultiple(declarations)
}
}
}
const result = babel.transform(code, {
plugins: [{
visitor: visitor
}]
})
console.log(result.code)
複製程式碼
然後node src/index.js
KO,有人會問,小編你怎麼知道這麼寫?
很簡單在AST
上
配置到node_modules
程式碼寫完了,起作用的話需要配置,我們把這個外掛命名為fiveone所以在node_modules裡面新建一個名叫babel-plugin-fiveone的資料夾
在babel-plugin-fiveone/index.js
中輸入
var babel = require(`babel-core`);
var t = require(`babel-types`);
const visitor = {
// 對import轉碼
ImportDeclaration(path, _ref = {opts:{}}){
const specifiers = path.node.specifiers;
const source = path.node.source;
if (!t.isImportDefaultSpecifier(specifiers[0]) ) {
var declarations = specifiers.map((specifier) => { //遍歷 uniq extend flatten cloneDeep
return t.ImportDeclaration( //建立importImportDeclaration節點
[t.importDefaultSpecifier(specifier.local)],
t.StringLiteral(`${source.value}/${specifier.local.name}`)
)
})
path.replaceWithMultiple(declarations)
}
}
};
module.exports = function (babel) {
return {
visitor
};
}
複製程式碼
然後修改webpack.prod.config.js
中babel-loader
的配置項
options:{
presets:["react-app"],
plugins:[
["fiveone", {}]
],
}
複製程式碼
然後src/index.js
中輸入
import {uniq, extend, flatten, cloneDeep } from "lodash"
複製程式碼
npm run build
很明顯實現了按需載入
然而不能對所有的庫都進入這麼轉碼所以在babel-loader
的plugin增加lib
options:{
presets:["react-app"],
plugins:[
["fiveone", {libraryName:"lodash"}]
],
}
複製程式碼
在babel-plugin-fiveone/index.js
中修改為
var babel = require(`babel-core`);
var t = require(`babel-types`);
const visitor = {
// 對import轉碼
ImportDeclaration(path, _ref = {opts:{}}){
const specifiers = path.node.specifiers;
const source = path.node.source;
// 只有libraryName滿足才會轉碼
if (_ref.opts.libraryName == source.value && (!t.isImportDefaultSpecifier(specifiers[0])) ) { //_ref.opts是傳進來的引數
var declarations = specifiers.map((specifier) => { //遍歷 uniq extend flatten cloneDeep
return t.ImportDeclaration( //建立importImportDeclaration節點
[t.importDefaultSpecifier(specifier.local)],
t.StringLiteral(`${source.value}/${specifier.local.name}`)
)
})
path.replaceWithMultiple(declarations)
}
}
};
module.exports = function (babel) {
return {
visitor
};
}
複製程式碼
結束
如果文章有些地方有問題請指正,非常感謝!
github地址:github.com/Amandesu/ba…
如果大家有所收穫,可以隨手給個star不勝感激!
參考連結