手寫簡單的webpack

實現webpack打包功能

簡單分析webpack打包的結果

先創建三個文件 index.jsjavascript

let news = require('./news.js')
console.log(news.content)
複製代碼

message.jscss

module.exports = {
    content: `今天要下雨啦`
}
複製代碼

news.jshtml

let message = require('./message.js')
module.exports = {
  content: `今天有個大新聞,爆炸新聞!!!!內容是${message.content}`
}
複製代碼

而後咱們利用webpack進行打包,來分析這個打包後的結果前端

對這個打包後的結果進行簡化分析。抽離出來的核心代碼以下java

(function(modules) { 
	var installedModules = {};
	function __webpack_require__(moduleId) if(installedModules[moduleId]) {
        return installedModules[moduleId].exports;		
    }
    var module = installedModules[moduleId] = {
        i: moduleId,
		l: false,
     	exports: {}
	};
	modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
	module.l = true;

	return module.exports;
   }

 	return __webpack_require__(__webpack_require__.s = "./src/index.js");
 })
 ({ "./src/index.js": (function(module, exports, __webpack_require__) {eval("let news = __webpack_require__(/*! ./news.js */ \"./src/news.js\")\nconsole.log(news.content)\n\n//# sourceURL=webpack:///./src/index.js?");}),
    "./src/message.js":(function(module, exports) {eval("\nmodule.exports = {\n content: `今天要下雨啦`\n}\n\n//# sourceURL=webpack:///./src/message.js?")}),
    "./src/news.js":(function(module, exports, __webpack_require__) {eval("let message = __webpack_require__(/*! ./message.js */ \"./src/message.js\")\nmodule.exports = {\n content: `今天有個大新聞,爆炸新聞!!!!內容是${message.content}`\n}\n\n//# sourceURL=webpack:///./src/news.js?");})
})  

// 對這段代碼進行分析
// 首先是一個自調用函數 modules: 是一個對象 obj
// 執行了這個自調用函數,自調用函數。內部返回了一個__webpack_require__() 傳參爲entry 裏面的值 
// 執行__webpack_require__() 傳入entry的值
// 開始執行函數 
// 函數內部 module = {
// i: modulesId,
// l: false
// export: {}
// }
// 繼續執行,調用了最外層傳入的參數,改變this指向爲空{}, 最後就是一個__webpack_require__的遞歸調用
複製代碼

因此其實咱們要實現一個webpack.主要任務有兩個node

  1. 將全部的require替換成__webpack__require,
  2. 將模塊中的全部依賴進行讀取,拼接成一個對象,傳入自調用函數裏面去。

搭建項目基礎骨架

  1. 建立一個bin目錄,在bin目錄下建立yj-pack文件.這個文件的主要目的就是讀取webpack.config.js配置目錄(暫不支持webpack那種自定義配置名稱),將讀取的配置傳入compiler模塊,讓compiler模塊進行相應的處理。此模塊不作其餘操做
#! /usr/bin/env node 
const path = require('path') 
// 1. 讀取須要打包項目的配置文件
let config = require(path.resolve('webpack.config.js'))
// let config = require(process.cwd(),'webpack.config.js')
const Compiler = require('../lib/compiler')
let a = new Compiler(config)
a.start()
複製代碼
  1. 建立咱們的compiler模塊
class Compiler {
    constructor(config) {
       this.config = config   // 將配置初始化
       this.entry = config.entry   // 入口的相對路徑
       this.root = process.cwd()   // 執行命令的當前目錄
    }
    start() {
      // 進行相應的文件操做
    }
}
module.exports = Compiler

複製代碼

讀取入口文件

  1. 首先咱們思考一下,咱們須要作什麼?

咱們主要目的是讀取入口文件,分析各個模塊之間的依賴關係,將require替換成__webpack_require__react

class Compiler {
    constructor(config) {
       this.config = config   // 將配置初始化
       this.entry = config.entry   // 入口的相對路徑
       this.root = process.cwd()   // 執行命令的當前目錄
       this.analyseObj = {}   // 這個就是咱們最後須要的文件對象
    }
	// 工具函數--- 用來拼接路徑
    getOriginPath(path1,path2) {
        return path.resolve(path1,path2)
    }
    // 工具函數--- 讀取文件
    readFile(modulePath) {
		 return fs.readFileSync(modulePath,'utf-8')
	}
    // 入口函數
    start() {
      // 進行相應的文件操做
      // 拿到入口文件的路徑,進行分析
      let originPath = this.getOriginPath(this.root,this.entry)
      this.depAnalyse(originPath)
      }
     //核心函數
    depAnalyse(modulePath){
    // 這樣content,就是咱們就能夠讀取webpack.config.js裏面的入口文件
    let content =  this.readFile(modulePath)
   }
}
複製代碼

利用AST語法樹替換require

這一步讀取文件以後,將require替換成__webpack_require__ .主要是利用來babel插件webpack

const fs = require('fs')
const path = require('path')
const traverse = require('@babel/traverse').default;
const parser = require('@babel/parser');
const generate = require('@babel/generator').default
class Compiler {
    constructor(config) {
       this.config = config   // 將配置初始化
       this.entry = config.entry   // 入口的相對路徑
       this.root = process.cwd()   // 執行命令的當前目錄
       this.analyseObj = {}   // 這個就是咱們最後須要的文件對象
    }
    // 工具函數--- 用來拼接路徑
    getOriginPath(path1,path2) {
      return path.resolve(path1,path2)
    }
    // 工具函數--- 讀取文件
    readFile(modulePath) {
      return fs.readFileSync(modulePath,'utf-8')
    }
    // 入口函數
    start() {
      // 進行相應的文件操做
      // 拿到入口文件的路徑,進行分析
      let originPath = this.getOriginPath(this.root,this.entry)
      this.depAnalyse(originPath)
    }
    // 核心函數
    depAnalyse(modulePath){
      //這樣content,就是咱們就能夠讀取webpack.config.js裏面的入口文件
      let content =  this.readFile(modulePath)
      // 將代碼轉化爲ast語法樹 
      const ast = parser.parse(content) 
      // traverse是將AST裏面的內容進行替換
      traverse(ast, {
          CallExpression(p) {
            if(p.node.callee.name === 'require') {
                p.node.callee.name = '__webpack_require__'
            }
          }
      })
      // 最後將ast語法樹轉化爲代碼
      let sourceCode =  generate(ast).code
    }
}
這樣咱們就完成了第一步,讀取了當前入口文件,而後將內容的require進行了替換
複製代碼

遞歸實現模塊依賴分析

其實上述步驟還有必定的問題。若是index.js裏面有多個模塊依賴怎麼辦?相似 index.jsgit

let a = require('./news.js) let b = require('./news1.js)
複製代碼

由於咱們須要將每一個模塊的依賴放在一個數組中進行保存。而後在對每一個模塊進行遞歸遍歷。 那麼咱們繼續改進depAnalyse函數github

depAnalyse(modulePath){
  // 這樣content,就是咱們就能夠讀取webpack.config.js裏面的入口文件
  let content =  this.readFile(modulePath)
  // 將代碼轉化爲ast語法樹
  const ast = parser.parse(content) 
  // 用於存取當前模塊的全部依賴。便於後面遍歷
  let dependencies = []
  // traverse是將AST裏面的內容進行替換
   traverse(ast, {
       CallExpression(p) {
         if(p.node.callee.name === 'require') {
             p.node.callee.name = '__webpack_require__'
		     // 這裏是對路徑進行處理,由於在window下面文件路徑\.而lunix下面是/ 。因此咱們進行統一處理一下
             let oldValue = p.node.arguments[0].value
             p.node.arguments[0].value = './'+ path.join('src',oldValue).replace(/\\+/g,'/')
		     // 將當前模塊依賴的文件路徑推送到數組裏面,遍歷咱們後續進行遞歸遍歷
             dependencies.push(p.node.arguments[0].value)
          }
        }
    })
    // 最後將ast語法樹轉化爲代碼
    let sourceCode =  generate(ast).code
    // 把當前的依賴,和文件內容推到對象裏面去。
    let relavitePath = './'+ path.relative(this.root,modulePath).replace(/\\+/g,'/')
    this.analyseObj[relavitePath] = sourceCode
    // 每一個模塊可能還有其餘的依賴,因此咱們須要遞歸遍歷一下。
    dependencies.forEach(dep=>{
      //遞歸一下
      this.depAnalyse(this.getOriginPath(this.root,dep))
    })
}
複製代碼

這樣你打印this.analyseObj就發現已經獲得了咱們想要的對象。接下來咱們思考怎麼生成webpack模版。

生成webpack模版文件

  1. 首先咱們找到最開始簡化後webpack的打包文件。咱們利用ejs進行相應的改造,創建一個template文件夾,創建output.ejs模版。模版以下
(function(modules) { 
	var installedModules = {};
	function __webpack_require__(moduleId) {
	if(installedModules[moduleId]) {
        return installedModules[moduleId].exports;		
    }
    var module = installedModules[moduleId] = {
        i: moduleId,
		l: false,
     	exports: {}
	};
	modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
	module.l = true;

	return module.exports;
   }

 	return __webpack_require__(__webpack_require__.s = "<%-entry%>");
 })
 ({
     <% for (let k in modules) { %>	
		"<%-k%>": 
		(function(module, exports, __webpack_require__) {eval(`<%-modules[k]%>`)
	 }),
	 <%}%>
})
複製代碼

讓webpack結合ejs,輸出文件

  1. 這一步主要是當咱們分析完成文件以後。將模版和咱們分析的this.analyseObj進行結合
start() {
  let originPath = this.getOriginPath(this.root,this.entry)
  this.depAnalyse(originPath)
  // 編譯完成
  this.emitFile()
}
emitFile() {
  let template= this.readFile(path.join(__dirname,'../template/output.ejs'))
  let result =  ejs.render(template,{
    entry: this.entry,
    modules: this.analyseObj
  })
  let outputPath = path.join(this.config.output.path,this.config.output.filename)
  fs.writeFileSync(outputPath,result)
  // console.log(result)
}
複製代碼

這樣咱們就將文件輸出到指定目錄了。而後利用node執行,或者放在瀏覽器執行。就能夠讀取了。這樣咱們就實現了簡單的webpack打包功能

實現webpack的loader功能

loader其實本質上就是一個函數

在webpack中怎麼開發一個本身的loader

首先在webpack.config.js中定義本身的loader

const path = require('path')
module.exports = {
    entry: './src/index.js',
    output: {
        path: path.join(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    mode: 'development',
    module:{
        rules: [
            // {
            // test: /\.js$/,
            // use: ['./src/loaders/loader1.js','./src/loaders/loader2.js','./src/loaders/loader3.js' ] 
            // }
            // {
            // test: /\.js$/,
            // use: './src/loaders/loader2.js'
            // },
            {
            test: /\.js$/,
            use: {
              loader: './src/loaders/loader1.js',
              options: {
                  name: '今天111111111'
              }
            }
           }
        ]
    }
}
複製代碼

而後在本身新建本身的loader

module.exports = function(source){
    console.log(this.query)
    return source.replace(/今天/g,this.query.name)
}
複製代碼

這樣咱們最基礎的loader就已經實現了。這個loader的功能就是將js的全部 ‘今天’替換成‘'今天111111111’,如今咱們知道怎麼寫一個簡單的loader,那麼接下來咱們看一下怎麼讓咱們本身寫的webpck支持loader功能

怎麼讓咱們寫的webpack支持loader功能

  1. 由上面的loader進行簡單的分析。咱們主要是讀取webpack.config.js的modules。而後匹配相應的文件,最後對文件進行相應的處理.

咱們思考一下loader在何時執行呢?,應該在咱們讀取文件以後就應該執行,因此咱們繼續對dpAnalyse進行改造 注意: loader的執行順序是從下到上,從右到左

depAnalyse(modulePath){
  // 這樣content,就是咱們就能夠讀取webpack.config.js裏面的入口文件
  let content =  this.readFile(modulePath)
  // loader的處理主要是首先讀取webpack.config.js裏面的配置文件module.rules。 首先判斷js裏面的配置文件module.rules 存在與否,而後在對裏面的loader進行倒敘遍歷
  // 開始處理loader
    for(var i = this.rules.length-1;i>=0;i--){
        // this.rules[i] 
        let {test,use} = this.rules[i]
        // 匹配是否符合規則
        if(test.test(modulePath)){
            if(Array.isArray(use)){
            // 這裏要判斷數字,對象,字符
            // 這兒能夠封裝一下,這裏面沒有封裝
            for(var j=use.length-1;j>=0;j--){
                let loader =  require(path.join(this.root,use[j])) 
                content = loader(content)
              }
            }else if(typeof use === 'string') {
                let loader =  require(path.join(this.root,use)) 
                content = loader(content)
            }else if(use instanceof Object){
                // console.log(use.options)
                // console.log("如今use是第一項")
                let loader =  require(path.join(this.root,use.loader)) 
                content = loader.call({query:use.options},content)
            }
           
        } 
    }
  // 將代碼轉化爲ast語法樹
  const ast = parser.parse(content) 
  // 用於存取當前模塊的全部依賴。便於後面遍歷
  let dependencies = []
  // traverse是將AST裏面的內容進行替換
   traverse(ast, {
       CallExpression(p) {
         if(p.node.callee.name === 'require') {
             p.node.callee.name = '__webpack_require__'
		     // 這裏是對路徑進行處理,由於在window下面文件路徑\.而lunix下面是/ 。因此咱們進行統一處理一下
             let oldValue = p.node.arguments[0].value
             p.node.arguments[0].value = './'+ path.join('src',oldValue).replace(/\\+/g,'/')
		     // 將當前模塊依賴的文件路徑推送到數組裏面,遍歷咱們後續進行遞歸遍歷
             dependencies.push(p.node.arguments[0].value)
          }
        }
    })
    // 最後將ast語法樹轉化爲代碼
    let sourceCode =  generate(ast).code
    // 把當前的依賴,和文件內容推到對象裏面去。
    let relavitePath = './'+ path.relative(this.root,modulePath).replace(/\\+/g,'/')
    this.analyseObj[relavitePath] = sourceCode
    // 每一個模塊可能還有其餘的依賴,因此咱們須要遞歸遍歷一下。
    dependencies.forEach(dep=>{
      //遞歸一下
      this.depAnalyse(this.getOriginPath(this.root,dep))
    })
}
複製代碼

這樣咱們就簡單實現了loader的功能

實現webpack的plugins功能

怎麼開發一個自定義的plugins

plugins 其實就是一個自定義的類。但webpack中規定了咱們這個類裏面必需要實現apply方法。 其實原理就是webpack中內部實現了本身的一套生命週期,而後你只須要在大家apply方法裏面去調用webpack裏面提供的生命週期就行

  1. 接下來咱們就實現本身最簡單的helloworld的plugins
class HelloWorldPlugin{
    apply(Compiler){
       // 在文件打包結束後執行
        Compiler.hooks.done.tap('HelloWorldPlugin',(compilation)=> {
            console.log("整個webpack打包結束")
        })
        // 在webpack輸出文件的時候執行
        Compiler.hooks.emit.tap('HelloWorldPlugin',(compilation)=> {
            console.log("文件開始發射")
        })
        // console.log('hello world')
    }
}
module.exports = HelloWorldPlugin
複製代碼

最後咱們只須要在webpack.config.js引入相應的plugins就能夠了。

利用tapable實現生命週期

上面能夠看到webpack實現了本身的生命週期。那麼他是怎麼實現的呢?核心其實就是一個發佈訂閱者模式,webpack其實主要是利用了一個核心庫tapable 那麼咱們本身來構建一個簡單的類,來實現相應的生命週期。主要分爲三步

  1. 註冊自定義的事件
  2. 在適當的時機去綁定事件
  3. 在適當的時機去調用事件
// study 前端
const {SyncHook}  = require('tapable')
class Frontend{
    constructor() {
        this.hooks = {
           beforeStudy: new SyncHook(),
           afterHtml: new SyncHook(),
           afterCss: new SyncHook(),
           afterJs: new SyncHook(),
           afterReact: new SyncHook() 
        }
    }
    study() {
        console.log('開始準備學習')
        this.hooks.beforeStudy.call()
        console.log('開始準備學習html')
        this.hooks.afterHtml.call()
        console.log('開始準備學習css')
        this.hooks.afterCss.call()
        console.log('開始準備學習js')
        this.hooks.afterJs.call()
        console.log('開始準備學習react')
        this.hooks.afterReact.call()
    }
}
let f = new Frontend()
f.hooks.afterHtml.tap('afterHtml',()=>{
    console.log("學完html後我想造淘寶")
})

f.study()
複製代碼

這樣咱們就實現了本身的生命週期函數

讓咱們手寫的webpack支持plugins功能

在compiler函數一初始化的時候就定義本身的webpack的生命週期,而且在start中間進行相應的調用,這樣咱們就實現了本身的生命週期

class Compiler {
    constructor(config) {
       this.config = config 
       this.entry = config.entry
       this.root = process.cwd()
       this.analyseObj = {}
       this.rules = config.module.rules
       this.hooks = {
        // 生命週期的定義
        compile: new SyncHook(),
        afterCompile: new SyncHook(),
        emit: new SyncHook(),
        afterEmit: new SyncHook(),
        done: new SyncHook()
       }
       // plugins數組中全部插件對象,調用apply方法,至關於註冊事件
       if(Array.isArray(this.config.plugins)){
        this.config.plugins.forEach(plugin => {
            plugin.apply(this)
        })
       }
    }
    start() {
        // 開始編譯了
        this.hooks.compile.call()
        // 開始打包
        // 依賴分析
        let originPath = this.getOriginPath(this.root,this.entry)
        this.depAnalyse(originPath)
        // 編譯完成了
        this.hooks.afterCompile.call()
        // 開始發射文件了
        this.hooks.emit.call()
        this.emitFile()
        this.hooks.afterEmit.call()
        this.hooks.done.call()
    }
  }
複製代碼

爲本身的webpack寫一個htmlplugin插件

那麼接下來,咱們爲本身的webpack寫一個htmlplugin插件 實現本身的htmlplugin類。而且在相應的webpack週期內執行相應的函數 這裏使用了cheerio庫,主要是能夠對html字符串進行dom操做

const fs = require('fs')
const cheerio = require('cheerio')

class HTMLPlugin{
    constructor(options){
        this.options = options
    }
    apply(Compiler){
        // 註冊afterEmit 事件
        Compiler.hooks.afterEmit.tap('HTMLPlugin',(compilation)=> {
            // console.log("整個webpack打包結束")
           let result = fs.readFileSync(this.opions.template,'utf-8')
            // console.log(this.options)
            // console.log(result)
            let $ = cheerio.load(result)
            Object.keys(compilation.assets).forEach(item=>{
                $(`<script src="${item}"></script>`).appendTo('body')
            })
            // 生成html。輸出
            // $.html()
            fs.writeFileSync('./dist/' + this.options.filename,$.html())
        })
        // console.log('hello world')
    }
}
module.exports = HTMLPlugin
複製代碼

源碼地址

最後咱們就大功告成。詳細代碼(github):
github.com/yujun96/yj-…
github.com/yujun96/web…

相關文章
相關標籤/搜索