解讀webpack,簡單實現mypack

雖然使用webpack有一陣日子了,但是對其內部的相關打包過程,還不是很瞭解,今天翻閱了相關資料後,借鑑寫了一個webpack簡化版本,僅是其核心的部分,mypack

瞭解一下requireJS的大致運行過程

這裏需要node環境(用到了文件讀寫相關,webpack也是在node上運行的)

// a.js

let fs = require('fs')

function myRequire (moduleName) {
  // content指示文件的內容
  let content = fs.readFileSync(moduleName, 'UTF8')
  let fn = new Function('exports', 'module', 'require', '__dirname', 
    '__filename', content + '\n return module.exports')
  let module = {
    exports: {}
  }
  return fn(module.exports, module, myRequire, __dirname, __filename)
}

let str = myRequire('./b.js')
console.log(str)


// b.js

module.exports = "Hello RequireJS!"

模擬一下AMD的打包方式,雖然不常用,但是對理解起來是有幫助的

可以直接在瀏覽器中運行

// myAMD.js

let factories = {}
function myDefine (moduleName, dependencies, factory) {
  factory.dependencies = dependencies // 掛在該方法上的依賴關係
  factories[moduleName] = factory // 創建對應關係
}
function myRequire (mods, callback){
  let result = mods.map(mod => {
    let factory = factories[mod] // 獲取該方法
    let dependencies = factory.dependencies // 獲取該依賴
    let exports
    myRequire(dependencies, function (){
      exports = factory.apply(null, arguments)
    })
    return exports
  })
  return callback.apply(null, result)
}
// 申明依賴
myDefine('name', [], function () {
  return 'AMD'
})
myDefine('age', [], function () {
  return 666
})
myDefine('person', ['name', 'age'], function (name, age){
  let person = 'person ' + name + ' is ' + age
  return person
})
// 引入依賴 第一種方式
myRequire(['name', 'age'], function (name, age){
  console.log(name, age)
})
myRequire(['person'], function (person){
  console.log(person)
})
其中的核心思想就是
  1. 申明瞭一個全局的factories,並且通過其moduleName作爲key,其factory(也就是callback)作爲value,在申明的過程中就已經關聯到了全局的factories,後續直接從中取出即可。
  2. 在掛載其依賴的時候,require內部分別將其依賴轉換爲依賴的結果(map方法),這裏如果再有依賴可以利用遞歸的方式解決。

mypack的實現

  1. 配置mypack的環境
  • 首先在建立mypack,(並進入mypack文件夾中)並且將在bin中創建出mypack.js,並且初始化npm環境npm init -y
|-mypack
| |-bin
|  |-mypack.js
| |-package.json
|

// 注意修改package.json中的路徑問題 bin: bin/mypack.js
  • 然後在mypack.js中文件頭部#! /usr/bin/env node 不然會報沒有適當的js解析器

  • 之後我們進行鏈接 npm link 這樣我們就全局安裝,退回其他位置都可以執行命令 >mypack

  1. 首先確定入口文件,出口文件的位置,也即有執行的開始位置,將解析入口文件中的內容,遞歸的讀入,把require替換成本規則可識別的函數名(本例myRequire),並且以key(文件路徑),value(內容)的形式放到總的modules中。也即所有用到的依賴已經到內存中。
  2. 立即執行函數,並且第一個require的就是入口文件,依次執行,也就是碰見myRequire(已經在第二步全部正則替換)時,解析出內容返回。
  3. 遞歸的進行myRequire的解析,其參數就爲之前的modules的key,通過映射關係取出內容執行。
// mypack.js  mypack代碼

#! /usr/bin/env node

let entry = './src/index.js'  // 入口文件
let output = './dist/index.js'  // 出口文件

const fs = require('fs')
const path = require('path')
let modules = []
let script = fs.readFileSync(entry, 'UTF8')
// 匹配對應關係
let replaceReg = (script) => {
  return script.replace(/require\(['"](.+?)["']\)/g, function () {
    let name = path.join('./src', arguments[1])
    let content = fs.readFileSync(name, 'UTF8')
    // 對於每拿到的content都進行一次替換
    content = replaceReg(content)
    modules.push({name, content})
    return `mypack_require('${name}')`
  })
}
script = replaceReg(script)

const ejs = require('ejs')

let template = `
(function(modules) {
   // 引入
   function mypack_require(moduleId) {
     var module = {
       exports: {}
     };
     modules[moduleId].call(module.exports, module, module.exports, mypack_require);
     return module.exports;
   }
   return mypack_require("<%-entry%>");
 })
  ({
   "<%-entry%>": (function(module, exports, mypack_require) {
     eval(\`<%-script%>\`);
  }) // index.js的文件
<%for(let i = 0; i < modules.length; i++){
let module = modules[i]%>
  ,
    "<%-module.name%>": (function(module, exports, mypack_require) {
      eval(\`<%-module.content%>\`);
    })
<%}%>

 });
`
// 最終result爲打包後的結果
let result = ejs.render(template, {
  entry,
  script,
  modules
})
fs.writeFileSync(output, result)
console.log('mypack success')
// 目錄結構
|-src
| |a.js
| |c.js
| |index.js
// index.js打包前的內容

require('./a.js')
console.log('hello mypack')


// a.js
require('./c.js')
console.log('hello mypack666')

// c.js
console.log('=====c.js======')
// index.js 打包後的內容


(function(modules) {
   // 引入
   function mypack_require(moduleId) {
     var module = {
       exports: {}
     };
     modules[moduleId].call(module.exports, module, module.exports, mypack_require);
     return module.exports;
   }
   return mypack_require("./src/index.js");
 })
  ({
   "./src/index.js": (function(module, exports, mypack_require) {
     eval(`mypack_require('src\a.js')
console.log('hello mypack')`);
  }) // index.js的文件

  ,
    "src\a.js": (function(module, exports, mypack_require) {
      eval(`console.log('hello mypack666')`);
    })


 });
  1. 添加一個styleLoader,這裏以函數的形式進行放入,也可以單獨寫一個模塊引入
  • loader 其實就是在解析所有的代碼時(遞歸讀入代碼,即replaceReg之中,對模塊內容在進行一些處理)
  • loader 解析不同的文件是根據正則表達式的後綴進行區分的。
  • 這裏針對於styleLoader中的css代碼,第一是進行了壓縮成一行,並且替換掉了/r/n解析時的標誌,因爲innerText接收的是字符串,然而字符串是無法換行的,所以只能壓縮成一行,在將原來空行形成的/r/n替換掉。(文末提出一種不壓縮的方案)
//  mypack.js  mypack代碼加入了styleLoader 

#! /usr/bin/env node

let entry = './src/index.js'  // 入口文件
let output = './dist/index.js'  // 出口文件

const fs = require('fs')
const path = require('path')
let modules = []
let script = fs.readFileSync(entry, 'UTF8')
//======================styleLoader申明===================
let styleLoader = function (source) { //模擬styleLoader
  return `
    let style = document.createElement('style')
    style.innerText = ${JSON.stringify(source).replace(/\\r\\n/g, '')}
    document.head.appendChild(style)
  `
}
//=====================styleLoader申明結束===================
// 匹配對應關係
let replaceReg = (script) => {
  return script.replace(/require\(['"](.+?)["']\)/g, function () {
    let name = path.join('./src', arguments[1])
    let content = fs.readFileSync(name, 'UTF8')
    // 對於每拿到的content都進行一次替換
    content = replaceReg(content)
//======================應用規則=====================
    if(/\.css$/.test(name)){
      content = styleLoader(content) // 處理styleLoader
    }
//=====================應用規則結束===================
    modules.push({name, content})
    return `mypack_require('${name}')`
  })
}
script = replaceReg(script)

const ejs = require('ejs')

let template = `
(function(modules) {
   // 引入
   function mypack_require(moduleId) {
     var module = {
       exports: {}
     };
     modules[moduleId].call(module.exports, module, module.exports, mypack_require);
     return module.exports;
   }
   return mypack_require("<%-entry%>");
 })
  ({
   "<%-entry%>": (function(module, exports, mypack_require) {
     eval(\`<%-script%>\`);
  }) // index.js的文件
<%for(let i = 0; i < modules.length; i++){
let module = modules[i]%>
  ,
    "<%-module.name%>": (function(module, exports, mypack_require) {
      eval(\`<%-module.content%>\`);
    })
<%}%>

 });
`
// 最終result爲打包後的結果
let result = ejs.render(template, {
  entry,
  script,
  modules
})
fs.writeFileSync(output, result)
console.log('mypack success')
// 目錄結構
|-src
| |a.js
| |c.js
| |b.css
| |index.js
// index.js打包前的內容

require('./a.js')
require('./b.css')
console.log('hello mypack')


// a.js
require('./c.js')
console.log('hello mypack666')

// c.js
console.log('=====c.js======')

//b.css
body{
  background: hotpink;
}
// index.js 打包後的內容


(function(modules) {
   // 引入
   function mypack_require(moduleId) {
     var module = {
       exports: {}
     };
     modules[moduleId].call(module.exports, module, module.exports, mypack_require);
     return module.exports;
   }
   return mypack_require("./src/index.js");
 })
  ({
   "./src/index.js": (function(module, exports, mypack_require) {
     eval(`mypack_require('src\a.js')
mypack_require('src\b.css')
console.log('hello mypack')`);
  }) // index.js的文件

  ,
    "src\c.js": (function(module, exports, mypack_require) {
      eval(`console.log('=====c.js======')`);
    })

  ,
    "src\a.js": (function(module, exports, mypack_require) {
      eval(`mypack_require('src\c.js')
console.log('hello mypack666')`);
    })

  ,
    "src\b.css": (function(module, exports, mypack_require) {
      eval(`
    let style = document.createElement('style')
    style.innerText = "body{  background: hotpink;}"
    document.head.appendChild(style)
  `);
    })


 });
  1. 不壓縮css的一種寫法
style.innerText = \\\` ${source} \\\`
// 代碼在構建的時候 輸出到index.js變成了 evel(` ...\` backgrounde... \` ... `)
// 代碼再被執行的時候,執行evel的時候,就以模板字符串` background ...` 呈現出來


// index.js 打包後變爲
style.innerText = \` body{
  background: hotpink;
} \`
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章