簡易版webpack實現

簡易版webpack實現

項目搭建

  • 打開cmd,創建一個webpack-dev目錄

    mkdir webpack-dev
    cd webpack-dev
    

    把項目拖進vscode中,初始化項目,執行

    yarn init -y
    
  • 構建項目目錄結構

    |-- webpack-dev
        |-- package-lock.json 
        |-- package.json
        |-- yarn-error.log
        |-- yarn.lock
        |-- bin
        |   |-- xs-pack.js
        |-- lib
        |   |-- Compiler.js
        |   |-- main.ejs
        |-- src
    
    
  • 修改package.json,添加bin字段

    //package.json
    {
      "name": "webpack-dev",
      "version": "1.0.0",
      "main": "index.js",
      "license": "MIT",
      "bin":{
        "xs-pack":"./bin/xs-pack.js"
      }
    }
    
  • 指定文件編譯環境,在xs-pack.js頂部加上

    #! /usr/bin/env node  
    
  • 把包文件link到全局下,並添加xs-pack命令,在項目下打開cmd輸入

    npm link 
    

代碼編寫

  • 編寫xs-pack.js文件

    作用:接收webpack.config.js文件,把這個文件傳入Compiler類中,進行解析

    #! /usr/bin/env node
    
    //1.需要找到當前執行命令的路徑,webpack.config.js
    let path = require('path')
    //config配置文件
    let config = require(path.resolve(__dirname))
    //解析類
    let Compiler = require('../lib/Compiler.js')
    //把拿到的webpack.config.js傳入解析類中
    let compiler = new Compiler(config)
    // 標識運行編譯
    compiler.run()
    
    
  • 編寫Compiler.js

    作用:

    獲取入口文件路徑,保存模塊依賴。

    遞歸依賴模塊,獲取相應的源碼內容

    把模塊源碼收集,最終打包出去

    • 函數初始化

      //接收外部傳進來的webpack.config.js文件
      //進行數據初始化
      constructor(config){
          //外部webpack配置文件
          this.config = config
          // 保存入口文件的路徑 './src/index.js'
          this.entryId
          // 保存所有的模塊依賴
          this.modules = {}
          this.entry = config.entry //入口路徑  webpack.config.js中的entry字段
          this.root = process.cwd() //當前工作路徑 ,xs-pack命令 執行的路徑,一般爲項目根路徑
      }
      
      
    • bundleModule方法:接收入口模塊路徑,獲取模塊中的內容,同時遞歸廣度遍歷依賴模塊中的內容。

      bundleModule(modulePath, isEntry) {
          // 拿到模塊文件中的源碼內容
          let source = this.getSource(modulePath)
          let moduleName = './' + path.relative(this.root, modulePath) //相對路徑
          if (isEntry) {//判斷是否是入口模塊
              this.entryId = moduleName //保存入口名字
          }
          //解析把source源碼進行改造 返回一個依賴列表
          let { sourceCode, dependencies } = this.parse(
              source,
              path.dirname(moduleName)
          ) // path.dirname(moduleName)  ./src
          //把相對路徑和模塊中的內容,進行關聯起來
          this.modules[moduleName] = sourceCode
          dependencies.forEach(dep => {
              //依賴模塊加載
              this.bundleModule(path.join(this.root, dep), false)
          })
      }
      //獲取文件源碼函數,封裝出來利於複用
      getSource(modulePath) {
          let content = fs.readFileSync(modulePath, 'utf8')
          return content
      }
      
      
    • parse源碼解析改造方法

      作用:解析模塊源碼,改造源碼中的路徑

      這是webpack打包後bundle.js文件的部分源碼內容,從圖中我們可以看出該文件的引入路徑跟我們書寫的項目文件路徑是不同的,我們需要把這些路徑改造。
      在這裏插入圖片描述

      所以當前的首要問題是如何改造源碼?可以通過AST抽象語法樹。

      關於AST抽象語法樹不懂的同學可以閱讀這篇文章https://segmentfault.com/a/1190000016231512

      我們藉助幾個工具來完成這個源碼改造

      babylon :把源碼轉換爲ast

      @babel/traverse :遍歷節點

      @babel/types :主要用途是在創建AST的過程中判斷各種語法的類型。

      @babel/generator: 將AST解碼生 js代碼。

      安裝它們

      yarn add -D babylon @babel/traverse @babel/types @babel/generator
      
      

      想學習有關babel知識的同學可以參考https://www.jianshu.com/p/9aaa99762a52

      parse方法具體實現

      parse(source, parentPath) {
          //把源碼轉化爲ast語法樹
          let ast = babylon.parse(source)
          //依賴模塊數組
          let dependencies = []
          //編譯ast,修改源碼
          traverse(ast, {
              CallExpression(p) {
                  let node = p.node
                  if (node.callee.name === 'require') {
                  	//webpack自己實現了一個require方法:__webpack_require__
                      //所以我們要把項目中所有require變成 __webpack_require__
                      node.callee.name = '__webpack_require__'
                      //模塊引入路徑  require('./a') => ./a
                      let moduleName = node.arguments[0].value 
                      
                      //添加文件後綴 ./a =》 ./a.js
                      moduleName =
                          moduleName + (path.extname(moduleName) ? '' : '.js') 
                  	//添加文件路徑 ./a.js =》 ./src/a.js
                      moduleName = './' + path.join(parentPath, moduleName) 
                     	//在依賴列表中保存當前路徑
                      dependencies.push(moduleName)
                      //把修改好的路徑重新放進ast中
                      node.arguments = [t.stringLiteral(moduleName)]
                  }
              }
          })
          //AST 解析語法樹
          //把ast轉化爲源碼
          let sourceCode = generator(ast).code
          return {
              sourceCode,
              dependencies
          }
      }
      
      
    • emitFile渲染模板文件方法

      作用:把獲取到的全部模塊內容編寫進模板文件中,也就是webpack打包後bundle.js文件

      emitFile() {    // 用數據 渲染我們的模板
      	//獲取webpack.config.js 出口文件字段信息
          //拼接路徑
          let main = path.join(
              this.config.output.path,//出口文件路徑
              this.config.output.filename//出口文件名
          )
          //獲取ejs模板,該找裏面的內容
          let templateStr = this.getSource(path.join(__dirname, 'main.ejs'))
          //把獲取的模塊內容渲染進模塊
          let code = ejs.render(templateStr, {
              entryId: this.entryId,
              modules: this.modules
          })
          //考慮到後面可能需要打包多入口文件,使用對象存放。
          this.assets = {}
          this.assets[main] = code
          //把生成的模板內容,放進出口文件中並生成出來
          fs.writeFileSync(main, this.assets[main])
      }
      
      
      
      

      我們這裏使用的是ejs模板,需要進行安裝

      yarn add ejs
      
      

      編寫ejs模板

      main.ejs

      這段模板其實就是webpack生成的bundle.js中的最主要的功能實現的那部分。

      我們只需修改下入口文件和參數依賴項即可
      main.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 = '<%-entryId%>'))
      })({
          <%for(let key in modules){%>
              "<%-key%>":(function(module, exports, __webpack_require__) {
                  eval(
                      `<%-modules[key]%>`
                  )
              }),
          <%}%>
      })
      
      
    • 使用xs-pack

      創建test項目

      =[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-TXplDNlO-1582547214187)(C:\Users\Dell\AppData\Roaming\Typora\typora-user-images\1582546435116.png)]

      編寫測試文件
      在這裏插入圖片描述

      編寫webpack.config.js文件

      let path =require('path')
      module.exports = {
          mode:'development',
          entry: './src/index.js',
          output: {
              filename: 'bundle.js',
              path:path.resolve(__dirname,'dist')
          }
      }
      
      

      由於我們先前已經把xs-pack導入到全局環境中,我們只需要在當前項目根路徑下打開命令行輸入

      xs-pack
      
      

      最後即可在dist文件下看到打包成功後的文件,可以創建一個html引入這個js,並在瀏覽器上查看。

在這裏插入圖片描述

項目已上傳至github

簡易版webpack

總結

以上就是一個簡易版webpack的實現,從裏面我們可以看出webpack打包的原理,就是一些文件路徑的處理,文件源碼的改造。這個簡易版webpack,僅僅是簡單的打包下文件,像webpack的module,plugin,resolve等功能並未實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章