本文適合2~4年的前端開發人員的進階,且對vue或者react搭配webpack有必定的經驗。若是webpack的基本使用都未了解,建議實踐後再看本文。javascript
本文是原創,均爲本人手寫。部分思惟, 借鑑了「宮小白」的webpack文章。文章結尾標註了「感謝」。css
再談談博客源碼的進度html
此前有人關心博客源碼的進度問題。筆者是打算利用額外的時間,把腦殼中的源碼都擼一份。即源碼篇,具體可參考以下:前端
序號 | 博客主題 | 相關連接 |
---|---|---|
1 | 手寫vue_mini源碼解析 | juejin.im/post/5f0326… |
2 | 手寫react_mini源碼解析 | juejin.im/post/5f154c… |
3 | 手寫webpack_mini源碼解析(即本文) | juejin.im/post/5f1793… |
4 | 手寫jquery_mini源碼解析 | juejin.im/post/5f1e38… |
5 | 手寫vuex_mini源碼解析 | 預計下週 |
6 | 手寫vue_router源碼解析 | 預計8月 |
7 | 手寫diff算法源碼解析 | 預計8月 |
8 | 手寫promis源碼解析 | 預計8月 |
9 | 手寫原生js源碼解析(手動實現常見api) | 預計8月 |
10 | 手寫react_redux,fiberd源碼解析等 | 待定,本計劃先出該文,整理有些難度 |
11 | 手寫koa2_mini | 預計9月,前端優先 |
期間除了除了源碼篇,可能會寫一兩篇優化篇,基礎篇等。有興趣的歡迎持續關注。vue
當前主流的前端,不管angular,vue仍是react,都離不開的構建工具的編譯與協助。比較有名氣是有Webpack、Gulp 和 Grunt。java
Grunt筆者沒有實踐經驗,且相對另外二者比較偏冷門些,本文不作比較。node
來個簡單的高頻面試題:react
gulp與webpack有什麼區別?jquery
講述一下我的理解(僅供參考):webpack
gulp官方原話:gulp 將開發流程中讓人痛苦或耗時的任務自動化,從而減小你所浪費的時間、創造更大價值。他更像是一個流水線上的過濾器,定義多task(即便多個過濾條件),講咱們的文件進行轉譯。好比咱們將sass統一轉譯成css,或者文件的合併壓縮等,專一於task的執行。筆者開發一個門戶網站的時候,就將起引入,他精簡的 API 集,只需一個文件代碼,即幫咱們完成了一個項目針對某些文件的改造,且使用於多頁面的構建。
但單頁面的構建,不得不用webpack。webpack有人稱模塊打包機,更側重於模塊打包。本質是爲了打包js設計的,可是後期由於要針對整個前端項目,即支持loader對其餘文件進行編譯。
寫webpack的源碼,你首先得懂得loader跟plugins是如何去加載的。本文寫一個簡單的栗子,讓你們理解loader是個什麼東西。
那咱們就先看看大神寫的loader是個什麼東西。咱們以less-loader爲栗子:
npm install less-loader安裝後,咱們直接進入node_moudels/less-loader/package.json
查看main變量 咱們能夠看到: "main": "dist/cjs.js",
那麼咱們再看dist/cjs.js又指向index。他的源碼在index.js文件中,咱們來看index.js的代碼:
圖解中,咱們能夠分析到,他的首個參數,便是接受到文本內容。loaderContext便是將less轉換爲css的過程。可是整個loader,其實能夠很清晰到看到。接受到source,而後經過轉換,再暴露出去。按着這個思路,咱們手寫一個loader應該不難。
咱們定義一個簡單的需求:
正式環境中,去除項目中全部的console.log()
最後再打印出一個,關注博主博客
const join = require('path').join; const fs = require('fs');
function wzJsLoader(source) {
/*過濾輸出*/
const mode = process.env.NODE_ENV === 'production'
if( mode ){//正式環境
source = source.replace(/console.log\(.*?\);/ig, value => "" );
}
/* 打上我的標記 */
const tip = "關注博主博客:https://juejin.im/user/5e8fddc9e51d4546be39a558/posts ";
source = source + `console.log("${tip}");`
return source;
複製代碼
};
module.exports = wzJsLoader;
而後再咱們的配置文件中,配置上上述的loader。
{
test: /\.js$/, //js文件加載器
//loader: 'happypack/loader?id=happyBabel',
exclude: /node_modules/,
use: [
{
loader: 'babel-loader?cacheDirectory=ture',
options: {
presets: ['@babel/preset-env']
}
},
{
loader: require.resolve("./myWebPackConfig/wz-js-loader"),
options: { name: __dirname },
},
]
},
複製代碼
那麼,咱們就完成咱們的第一個手寫loader。
其實plugins也同樣的原理,這裏不作重複講解,本文demo中會提到plugins。
tapable一些前端朋友(沒了解過的webpack)可能很陌生,包括筆者以前也是,直到了解到webpack時纔去理解這個概念。咱們首先要了解他是個什麼東西,再去寫webpack可能更好了解一下。這裏分爲簡版說明,跟代碼解釋。如知識想了解webpack的執行機制,可能看簡版說明便可。
同步:
名稱 | 解釋 |
---|---|
SyncHook | 同步執行,無需返回值 |
SyncBailHook | 同步執行,無需返回值,返回undefined終止 |
SyncWaterfallHook | 同步執行,上一個處理函數的返回值是下一個的輸入,返回undefined終止 |
SyncLoopHook | 同步執行, 訂閱的處理函數有一個的返回值不是undefined就一直循環它 |
異步:
名稱 | 解釋 |
---|---|
AsyncSeriesHook | 異步執行,無需返回值 |
AsyncParallelHook | |
AsyncSeriesBailHook | 異步執行,無需返回值,返回undefined終止 |
AsyncSeriesWaterfallHook | 異步執行,上一個處理函數的返回值是下一個的輸入,返回undefined終止 |
看了tapable具體的幾個方法的做用,大概瞭解,那麼爲何webpack要扯上他呢? 由於webpack的構建流程,不少的跟着tapable的執行順序方案有關。
SyncHook就最好理解,同步執行,咱們不一樣的plugins之間,只須要同步執行下去便可。這時候咱們能夠利用SyncHook按順序同步執行SyncHook執行咱們的plugins便可。那麼另外的場景是什麼?
HappyPack知道是什麼麼?例如開啓多個線程,同步打包。是否是符合AsyncSeriesHook的場景。
再舉個最簡單的栗子: 咱們生成文件時候,是否是有一個hash值呢?假設這個hash值,有多個插件用到,咱們是否是應該在第一個方法生成一個hash,而後後續用到的方法,讀取到這個值。那麼,這個場景,SyncWaterfallHook是否派上用場?此時的hash一直傳遞下去,就完成了不一樣plugins之間的hash值傳遞。
仍是不明白?案例實現會根據這個栗子實現。 若是簡版本的介紹,仍是一臉懵逼,建議看一下下列源碼。
代碼的解釋,是爲了讓你更瞭解tapable。若是你只想知道webpack,能夠直接忽略。
tapable這裏,他自己是暴露了8個方法(上述)。可是咱們簡版沒有那麼複雜,咱們講解一下下述用到SyncHook跟AsyncSeriesWaterfallHook。
SyncHook咱們大體理解,他有一個tap將任務添加的內部的執行隊列中。此時,若是調用call方法,將執行同步執行全部tap過的方法。具體能夠經過下述代碼理解:
class SyncHook {
constructor(args){//args => ['name']
this.tasks = [];
}
call(...args){
this.tasks.forEach((task) => task(...args))
}
tap(name,task){
this.tasks.push(task);
}
}
let hook = new SyncHook(['name']);
hook.tap('plugins_0',function(name){
console.log('plugins_0',name)
})
hook.tap('plugins_1',function(name){
console.log('plugins_1',name)
})
hook.call('hello word');
複製代碼
AsyncSeriesWaterfallHook咱們大體理解,他有一個tap將任務添加的內部的執行隊列中。此時,若是調用call方法,將執行同步執行全部tap過的方法,且需上一個返回值,給下一個函數隊列。具體能夠經過下述代碼理解:
class SyncWaterfallHook {
//通常是能夠接收一個數組參數的,可是下面沒有用到
constructor(args) {
this.tasks = []
}
tap(name, cb) {
let obj = {}
obj.name = name
obj.cb = cb
this.tasks.push(obj)
}
call(...arg) {
let [first, ...others] = this.tasks
let ret = first.cb(...arg)
others.reduce((pre, next) => {
return next.cb(pre)
}, ret)
}
}
複製代碼
該章節,建議移步文章:www.jianshu.com/p/019d449a9…
新建一個package.json,入口文件咱們制定了bin/index.js
{
"name": "zwzpack",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "node ./bin/index.js"
},
"devDependencies": {},
"dependencies": {
}
}
複製代碼
再新建bin/index.js表示咱們的入口文件,參考咱們webpack常規寫法,引入自寫的webpack的lib包,再引入咱們的配置文件webpack.config.js。啓動他
const path = require('path')
const config = require(path.resolve('webpack.config.js'))
const WebpackCompiler = require('../lib/WebpackCompiler.js')
const webpackCompiler = new WebpackCompiler(config)
webpackCompiler.run();
複製代碼
根目錄定義webpack.config.js,咱們首先定義一個webpack的基本配置文件。
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.join(__dirname, './dist')
},
module: {
rules: []
},
plugins: [
]
}
複製代碼
public/index.html,做爲咱們測試webpack是否打包成功測試Html,引入打包後的js:
<html>
<head>
<meta charset="UTF-8">
<title>我的webpack</title>
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />
</head>
<script type="text/javascript" src="./main.js" ></script>
<body>
<div class="my_page" >
<h1 class="">歡迎小夥子小姑涼!</h1>
<div class="">歡迎關注掘金:<a target="_blank" href="https://juejin.im/user/5e8fddc9e51d4546be39a558">點擊</a></div>
<div class="">手寫mini_webpack教程:<a target="_blank" href="https://juejin.im/editor/drafts/5f1793716fb9a07e8b215a62">點擊</a></div>
</div>
</body>
<style>
</style>
</html>
複製代碼
WebpackCompiler是咱們的核心編譯類,下方重點講解。
class WebpackCompiler {
constructor(config) {
this.config = config;
}
run() {
//編譯開始
}
}
module.exports = WebpackCompiler;
複製代碼
就這樣,完成咱們基本的架子,此部分暫時未涉及wepback的編譯過程。
webpack的最終目的,是爲了生成js文件。(暫不考慮其餘文件因素) 咱們要本身手寫一個webpack生成的js文件,那麼首先,咱們須要瞭解官方webpack生成的js文件究竟是個啥?
咱們新建一個官方webpack項目(百度一下很簡單)
新建test.js,跟testChildren.js進行編譯:
test.js:
const obj = require("./testChildren.js");
module.exports = { name: "weizhan", obj }
複製代碼
testChildren.js
module.exports = { age: 18}
複製代碼
咱們來觀察編譯結果:
圖1:
圖2:
截圖你們還看不懂生成了什麼的話,我再簡化一下官方最後生成的文件,其中有變化的部分爲:
((function(modules) {
...
//固定代碼
return __webpack_require__(__webpack_require__.s = "<%-文件路徑 %>");
})
"./src/js/test.js": (function (module, exports,__webpack_require__) {
eval(` const obj = __webpack_require__("./testChildren.js"); test1.js 剩餘文件代碼`);
}),
"./src/js/testChildren.js": (function (module, exports,__webpack_require__) {
eval(`testChildren.js 文件代碼`);
}),
複製代碼
咱們能夠觀察到,生成的js文件中,將生成原來的js文件,套入一個模板中,而咱們本身js代碼給嵌入到結尾部分。 觀察我簡化後的文件,咱們能夠看到,webpack官方生成的js文件,他的生成規律:
瞭解完官方的生成規矩後,咱們來模擬這個過程:
根據規則,咱們新建lib/main.ejs文件:
(function(modules) {
...
//固定代碼
return __webpack_require__(__webpack_require__.s = "<%- entryPath %>");
})
({
<% for(let key in modules){ %>
"<%- key %>": (function (module, exports,__webpack_require__) {
eval(`<%-modules[key] %>`);
}),
<% } %>
});
複製代碼
生成JS的模板有了,那麼咱們剩餘的問題就是,怎麼拿到modules跟entryPath的問題。
entryPath好處理,用過webpack的都知道,入口文件便是webpack.config.js的entry變量。 那麼modules呢?首先咱們須要讀取原來的文件內容。上文提到,兩個重點:
需求便是如此,我首先想到的時候,寫一個正則表達式,將require替換成__webpack_require__,且用node的fs,將文件內容讀取。可是後續發現,兼容性實在是太爛了,空白符,註釋等,都會成爲頭疼的地方。
後續發現大神的解析方法很是贊,用的是babylon 轉換 AST轉換的方法,咱們來看看代碼:
// babylon主要把源碼轉成ast。Babylon 是 Babel 中使用的 JavaScript 解析器。
// @babel/traverse 對ast解析遍歷語法樹 負責替換,刪除和添加節點
// @babel/types 用於AST節點的Lodash-esque實用程序庫
// @babel/generator 結果生成
const babylon = require('babylon')
const traverse = require('@babel/traverse').default;
const type = require('@babel/types');
const generator = require('@babel/generator').default
// 根據路徑解析源碼
parse(source, parentPath) {
let ast = babylon.parse(source)//
// 用於存取依賴
let dependencies = []
traverse(ast, {//對ast解析遍歷語法樹 負責替換,刪除和添加節點
CallExpression(p) {
let node = p.node
if (node.callee.name === 'require') {
node.callee.name = '__webpack_require__';//將require替換成__webpack_require__
const moduledName = './' + path.join(parentPath, node.arguments[0].value )
dependencies.push(moduledName);//記錄包含的requeir的名稱,後邊須要遍歷替換成源碼
node.arguments = [type.stringLiteral(moduledName)] // 源碼替換
}
}
})
let sourceCode = generator(ast).code
return { sourceCode, dependencies };
}
複製代碼
AST的內容若是不清楚,上述已經給了連接,先繼續步驟繼續下去,後續能夠根據連接再補充一下本身。 js轉換的方法已經抒寫完畢,咱們來寫轉換的入口。
// 編譯生成完成的main文件,完成遞歸
buildMoudle(modulePath, isEntry) {
const source = this.getSourceByPath(modulePath);//根據路徑拿到源碼
const moduleName = './' + path.relative(this.root, modulePath);//轉換一下路徑名稱
const { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName))//根據路徑拿到源碼,以及源碼中已經require的文件名稱數組
this.modules[moduleName] = sourceCode;// 每一個模塊的代碼都經過路徑爲Key,存入到modules對象中
dependencies.forEach(item => { // 遞歸須要轉換的文件名稱
this.buildMoudle(path.resolve(this.root, item));////再對應的文件名稱,替換成對應的源碼
})
}
getSourceByPath(modulePath){
let content = fs.readFileSync(modulePath, 'utf8')
return content
}
複製代碼
此時,若是程序執行完畢,咱們就能夠拿到咱們的modules參數。
這個章節很好理解,根據文件路徑,拿到模板內容,模板變量的值,輸出到指定位置便可。
//輸出文件
outputFile() {
let templateStr = this.getSourceByPath(path.join(__dirname, 'main.ejs')); // 拿到步驟1寫好的模板
let code = ejs.render(templateStr, {
entryPath: this.entryPath,
modules: this.modules,
})// 填充模板數據
let outPath = path.join(this.config.output.path, this.config.output.filename)// 拿到輸出地址
fs.writeFileSync(outPath, code )// 寫入
}
複製代碼
咱們改一下咱們的編譯類入口,在run方法執行他們:
class WebpackCompiler {
constructor(config) {
this.config = config;
this.modules = {}
this.root = process.cwd() //當前項目地址
this.entryPath = './' + path.relative(this.root, this.config.entry);
}
run() {
this.buildMoudle( this.entryPath )
this.outputFile();
}
}
複製代碼
此時,執行咱們咱們的基本架子,已經能夠輸出webpack的編譯後的js文件。
普通的js文件,已經抒寫完畢。那咱們來看看怎麼嵌入咱們的loader呢?看完了"寫源碼以前的必備知識點",咱們應該明白,loader便是將咱們的對應的文件後綴,經過loader的轉移成咱們的js文件支持的語法。
咱們作一下前期工做。
新建一個新的loader文件:
const less = require('less') function loader(source) { let css = '' less.render(source, function(err, output) { css = output.css })
css = css.replace(/\n/g, '\\n')
let style = `
let style = document.createElement('style')
style.innerHTML = \n${JSON.stringify(css)}
document.head.appendChild(style)
`
return style
複製代碼
} module.exports = loader;
具體less的轉換規則比較複雜我就不折騰了。這裏借用一下官方的less,賦值到js中。修改一下入口:webpack.config.js的module:
module: {
rules: [{
test: /\.less$/,
use: [path.join(__dirname, './lib/loader/less-loader.js')]
}]
},
複製代碼
再在咱們的src文件中新建test.less: body{ text-align: center; .my_page{ margin-top: 50px; line-height: 50px; } }
在測試文件(src/index.js)引入他:
require("./test.less");
const index2Obj = require("./index2.js");
alert("index文件告訴你:小夥子很帥" );
alert("index2文件告訴你:" + index2Obj.value );
module.exports = {}
複製代碼
作完前期工做,咱們來實現編譯過程:
咱們上述獲取源文件代碼用了:
getSourceByPath(modulePath){
let content = fs.readFileSync(modulePath, 'utf8')
return content
}
複製代碼
loader的實現,就是在拿到源文件後,經過rules的規則匹配後綴,多作一層轉換:
getSourceByPath(modulePath) {
let content = fs.readFileSync(modulePath, 'utf8')
// 事先拿module中的匹配規則與路徑進行匹配
const rules = this.config.module.rules
for (let i = 0; i < rules.length; i++) {
let { test, use } = rules[i]
let len = use.length
if (test.test(modulePath)) {
function changeLoader() {
// 先拿最後一個
let loader = require(use[--len])//倒敘執行
content = loader(content)
if (len > 0) {
changeLoader()
}
}
changeLoader()
}
}
return content
}
複製代碼
用了閉包,是由於一個後綴可能對應多個loader,因此這裏寫了循環執行。 再執行一次npm run start,此時你會發現,js中已經包含了less文件的內容。此時咱們完成了loader的嵌入。
理論上咱們應該講plugins的手寫,可是plugins有一個問題,plugins能夠設置在不一樣的編譯階段。例如在編譯前,作什麼?編譯中,需作什麼?這涉及到webpack的生命週期。咱們來處理一下生命週期。
咱們上述描述了tapable,SyncHook咱們大體理解,他有一個tap將任務添加的內部的執行隊列中,而後最後經過執行call方法,一次執行他們。
咱們的plugins的執行也是如何,咱們能夠將咱們的每一個plugin經過tap任務放置在SyncHook中,等時機到了,調用call方法便可。
咱們給咱們的webpack定義五個生命週期,並在run方法適當的時機嵌入他們:
class WebpackCompiler {
constructor(config) {
this.config = config;
this.modules = {}
this.root = process.cwd() //當前項目地址
this.entryPath = './' + path.relative(this.root, this.config.entry);
this.hooks = {
entryInit: new tapable.SyncHook(),
beforeCompile: new tapable.SyncHook(),
afterCompile: new tapable.SyncHook(),
afterPlugins: new tapable.SyncHook(),
afteremit: new tapable.SyncWaterfallHook(['hash']),
}
}
run() {
this.hooks.entryInit.call(); //啓動項目
this.hooks.beforeCompile.call(); //編譯前運行
this.buildMoudle( this.entryPath )
this.hooks.afterCompile.call( ); //編譯後運行
this.outputFile();
this.hooks.afterPlugins.call( );//執行完plugins後運行
this.hooks.afteremit.call( );//結束後運行
}
}
複製代碼
該webpack生命週期定義結束,下邊咱們手寫plugins嵌入到生命週期中。
定義一個需求: 1)啓動時,添加打印:前端小夥子,編譯開始咯。 2)編譯前:清空原來的dist文件夾內容 3)輸出文件後:將咱們的生成文件main.js,更名main.${hash}.js,並在index.html正確引入該文件。
class InitPlugin { run(compiler) { // 將的在執行期放到剛開始解析入口前 compiler.hooks.entryInit.tap('Init', function(res) { console.log(前端小夥子,編譯開始咯。
); }) } }
class CleanDistPlugins { run(compiler) { // 將自身方法訂閱到hook以備使用 //假設它的運行期在編譯完成以後 compiler.hooks.beforeCompile.tap('CleanDistPlugins', function(res) { delFileFolderByName('./dist/'); }) } }
重命名文件:
class JsCopyPlugins {
run(compiler) {
compiler.hooks.afterPlugins.tap('JsCopyPlugins', function(res) {
const ranNum = parseInt( Math.random() * 100000000 );
fs.copyFile('./dist/main.js',`./dist/main.${ranNum}.js`,function(err){
if(err) console.log('獲取文件失敗');
delFileByName('./dist/main.js');
})
console.log("從新生成js成功" );
return ranNum;
})
}
}
複製代碼
修改html的js引入:
class HtmlReloadPlugins {
run(compiler) {
compiler.hooks.afterPlugins.tap('HtmlReloadPlugins', function(res) {
let content = fs.readFileSync('./public/index.html', 'utf8')
content = content.replace('main.js', `main.${res}.js`);
fs.writeFileSync( './dist/index.html', content)
})
}
}
複製代碼
因爲「重命名文件」「修改html的js引入」須要用到同一個hash值,上述SyncWaterfallHook能夠傳遞值,SyncHook則無傳遞值,這就是我爲何將最後一步改成SyncWaterfallHook的緣由
全部的pulgins都寫好了,咱們來寫webpack如何執行pulgins的過程:
其實生命週期幫咱們實現了SyncHook的start函數。咱們只須要將plugins註冊到編譯類中接口。plugins中已經將方法 tap到SyncHook中,咱們只須要執行plugins便可。方法很簡單:
class WebpackCompiler {
constructor(config) {
this.config = config
//...省略
const plugins = this.config.plugins
if (Array.isArray(plugins)) {
plugins.forEach(item => {
// 每一個均是實例,調用實例上的一個方法便可,傳入當前Compiler實例
item.run(this)
})
}
}
}
複製代碼
至此,咱們已經完成了webpack_mini的手寫。
給於核心類WebpackCompiler的完整代碼,如還不清晰,建議導下項目查看,連接:github.com/zhuangweizh…
WebpackCompiler源碼:
const path = require('path')
const fs = require('fs')
const { assert } = require('console')
// babylon 將源碼轉成ast Babylon 是 Babel 中使用的 JavaScript 解析器。
// @babel/traverse 對ast解析遍歷語法樹
// @babel/types 用於AST節點的Lodash-esque實用程序庫
// @babel/generator 結果生成
const babylon = require('babylon')
const traverse = require('@babel/traverse').default;
const type = require('@babel/types');
const generator = require('@babel/generator').default
const ejs = require('ejs')
const tapable = require('tapable')
class WebpackCompiler {
constructor(config) {
this.config = config
this.modules = {}
this.root = process.cwd() //當前項目地址
this.entryPath = './' + path.relative(this.root, this.config.entry);
this.hooks = {
entryInit: new tapable.SyncHook(),
beforeCompile: new tapable.SyncHook(),
afterCompile: new tapable.SyncHook(),
afterPlugins: new tapable.SyncHook(),
afteremit: new tapable.SyncWaterfallHook(['hash']),
}
const plugins = this.config.plugins
if (Array.isArray(plugins)) {
plugins.forEach(item => {
// 每一個均是實例,調用實例上的一個方法便可,傳入當前Compiler實例
item.run(this)
})
}
}
// 獲取源碼
getSourceByPath(modulePath) {
// 事先拿module中的匹配規則與路徑進行匹配
const rules = this.config.module.rules
let content = fs.readFileSync(modulePath, 'utf8')
for (let i = 0; i < rules.length; i++) {
let { test, use } = rules[i]
let len = use.length
// 匹配到了開始走loader,特色從後往前
if (test.test(modulePath)) {
function changeLoader() {
// 先拿最後一個
let loader = require(use[--len])
content = loader(content)
if (len > 0) {
changeLoader()
}
}
changeLoader()
}
}
return content
}
// 根據路徑解析源碼
parse(source, parentPath) {
let ast = babylon.parse(source)//
// 用於存取依賴
let dependencies = []
traverse(ast, {//對ast解析遍歷語法樹 負責替換,刪除和添加節點
CallExpression(p) {
let node = p.node
if (node.callee.name === 'require') {
node.callee.name = '__webpack_require__';//將require替換成__webpack_require__
const moduledName = './' + path.join(parentPath, node.arguments[0].value )
dependencies.push(moduledName);//記錄包含的requeir的名稱,後邊須要遍歷替換成源碼
node.arguments = [type.stringLiteral(moduledName)] // 源碼替換
}
}
})
let sourceCode = generator(ast).code
return { sourceCode, dependencies };
}
// 構建模塊
buildMoudle(modulePath) {
const source = this.getSourceByPath(modulePath);//根據路徑拿到源碼
const moduleName = './' + path.relative(this.root, modulePath);//轉換一下路徑名稱
const { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName))//根據路徑拿到源碼,以及源碼中已經require的文件名稱數組
this.modules[moduleName] = sourceCode;// 每一個模塊的代碼都經過路徑爲Key,存入到modules對象中
dependencies.forEach(item => { // 遞歸須要轉換的文件名稱
this.buildMoudle(path.resolve(this.root, item));////再對應的文件名稱,替換成對應的源碼
})
}
//輸出文件
outputFile() {
let templateStr = this.getSourceByPath(path.join(__dirname, 'main.ejs')); // 拿到步驟1寫好的模板
let code = ejs.render(templateStr, {
entryPath: this.entryPath,
modules: this.modules,
})// 填充模板數據
let outPath = path.join(this.config.output.path, this.config.output.filename)// 拿到輸出地址
fs.writeFileSync(outPath, code )// 寫入
}
run() {
this.hooks.entryInit.call(); //啓動項目
this.hooks.beforeCompile.call(); //編譯前運行
this.buildMoudle( this.entryPath )
this.hooks.afterCompile.call( ); //編譯後運行
this.outputFile();
this.hooks.afterPlugins.call( );//執行完plugins後運行
this.hooks.afteremit.call( );//結束後運行
}
}
module.exports = WebpackCompiler;
複製代碼
筆者本來即將完成mini_webpack源碼,後續發現有人的源碼核心原理的實現,寫的比我本來的要好一些,因此後期我作了改動。本文部分原理跟部分源碼借鑑了「宮小白」,他的原文連接是:juejin.im/post/5f0494…
此外, AST語法樹不清晰建議文章:www.jianshu.com/p/019d449a9…
本計劃將webpack如何優化,寫在本文中。發現文章已經差很少6K+字,太長小夥伴們也看不完,後續會單獨一篇文章基於本文描述。
筆者也會繼續寫本身的mini框架源碼,有興趣繼續關注:
序號 | 博客主題 | 相關連接 |
---|---|---|
1 | 手寫vue_mini源碼解析 | juejin.im/post/5f0326… |
2 | 手寫react_mini源碼解析 | juejin.im/post/5f154c… |
3 | 手寫webpack_mini源碼解析(即本文) | juejin.im/post/5f1793… |
4 | 手寫jquery_mini源碼解析 | juejin.im/post/5f1e38… |
5 | 手寫vuex_mini源碼解析 | 預計下週 |
6 | 手寫vue_router源碼解析 | 預計8月 |
7 | 手寫diff算法源碼解析 | 預計8月 |
8 | 手寫promis源碼解析 | 預計8月 |
9 | 手寫原生js源碼解析(手動實現常見api) | 預計8月 |
10 | 手寫react_redux,fiberd源碼解析等 | 待定,本計劃先出該文,整理有些難度 |
11 | 手寫koa2_mini | 預計9月,前端優先 |