日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問(wèn)題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷解決方案
Webpack原理淺析

 [[336301]]

成都創(chuàng)新互聯(lián)專注于企業(yè)全網(wǎng)營(yíng)銷推廣、網(wǎng)站重做改版、雨花臺(tái)網(wǎng)站定制設(shè)計(jì)、自適應(yīng)品牌網(wǎng)站建設(shè)、H5建站、電子商務(wù)商城網(wǎng)站建設(shè)、集團(tuán)公司官網(wǎng)建設(shè)、外貿(mào)網(wǎng)站建設(shè)、高端網(wǎng)站制作、響應(yīng)式網(wǎng)頁(yè)設(shè)計(jì)等建站業(yè)務(wù),價(jià)格優(yōu)惠性價(jià)比高,為雨花臺(tái)等各大城市提供網(wǎng)站開(kāi)發(fā)制作服務(wù)。

背景

Webpack 迭代到 4.x 版本后,其源碼已經(jīng)十分龐大,對(duì)各種開(kāi)發(fā)場(chǎng)景進(jìn)行了高度抽象,閱讀成本也愈發(fā)昂貴。但是為了了解其內(nèi)部的工作原理,讓我們嘗試從一個(gè)最簡(jiǎn)單的 webpack 配置入手,從工具設(shè)計(jì)者的角度開(kāi)發(fā)一款低配版的 Webpack。

開(kāi)發(fā)者視角

假設(shè)某一天,我們接到了需求,需要開(kāi)發(fā)一個(gè) react 單頁(yè)面應(yīng)用,頁(yè)面中包含一行文字和一個(gè)按鈕,需要支持每次點(diǎn)擊按鈕的時(shí)候讓文字發(fā)生變化。于是我們新建了一個(gè)項(xiàng)目,并且在 [根目錄](méi)/src 下新建 JS 文件。為了模擬 Webpack 追蹤模塊依賴進(jìn)行打包的過(guò)程,我們新建了 3 個(gè) React 組件,并且在他們之間建立起一個(gè)簡(jiǎn)單的依賴關(guān)系。

 
 
 
 
  1. // index.js 根組件 
  2. import React from 'react' 
  3. import ReactDom from 'react-dom' 
  4. import App from './App' 
  5. ReactDom.render(, document.querySelector('#container')) 
  
 
 
 
  1. // App.js 頁(yè)面組件 
  2. import React from 'react' 
  3. import Switch from './Switch.js' 
  4. export default class App extends React.Component { 
  5.   constructor(props) { 
  6.     super(props) 
  7.     this.state = { 
  8.       toggle: false 
  9.     } 
  10.   } 
  11.   handleToggle() { 
  12.     this.setState(prev => ({ 
  13.       toggle: !prev.toggle 
  14.     })) 
  15.   } 
  16.   render() { 
  17.     const { toggle } = this.state 
  18.     return ( 
  19.       
     
  20.         

    Hello, { toggle ? 'NervJS' : 'O2 Team'}

     
  21.          
  22.       
 
  •     ) 
  •   } 
  •   
     
     
     
    1. // Switch.js 按鈕組件 
    2. import React from 'react' 
    3.  
    4. export default function Switch({ handleToggle }) { 
    5.   return ( 
    6.     Toggle 
    7.   ) 

    接著我們需要一個(gè)配置文件讓 Webpack 知道我們期望它如何工作,于是我們?cè)诟夸浵滦陆ㄒ粋€(gè)文件 webpack.config.js 并且向其中寫(xiě)入一些基礎(chǔ)的配置。(如果不太熟悉配置內(nèi)容可以先學(xué)習(xí)webpack 中文文檔[1])

     
     
     
     
    1. // webpack.config.js 
    2. const resolve = dir => require('path').join(__dirname, dir) 
    3.  
    4. module.exports = { 
    5.   // 入口文件地址 
    6.   entry: './src/index.js', 
    7.   // 輸出文件地址 
    8.   output: { 
    9.         path: resolve('dist'), 
    10.     fileName: 'bundle.js' 
    11.   }, 
    12.   // loader 
    13.   module: { 
    14.     rules: [ 
    15.       { 
    16.         test: /\.(js|jsx)$/, 
    17.         // 編譯匹配include路徑的文件 
    18.         include: [ 
    19.           resolve('src') 
    20.         ], 
    21.         use: 'babel-loader' 
    22.       } 
    23.     ] 
    24.   }, 
    25.   plugins: [ 
    26.     new HtmlWebpackPlugin() 
    27.   ] 

    其中 module 的作用是在 test 字段和文件名匹配成功時(shí)就用對(duì)應(yīng)的 loader 對(duì)代碼進(jìn)行編譯,Webpack本身只認(rèn)識(shí) .js 、 .json 這兩種類型的文件,而通過(guò) loader,我們就可以對(duì)例如 css 等其他格式的文件進(jìn)行處理。

    而對(duì)于 React 文件而言,我們需要將 JSX 語(yǔ)法轉(zhuǎn)換成純 JS 語(yǔ)法,即 React.createElement 方法,代碼才可能被瀏覽器所識(shí)別。平常我們是通過(guò) babel-loader并且配置好 react 的解析規(guī)則來(lái)做這一步。

    經(jīng)過(guò)以上處理之后。瀏覽器真正閱讀到的按鈕組件代碼其實(shí)大概是這個(gè)樣子的。

     
     
     
     
    1. ... 
    2. function Switch(_ref) { 
    3.   var handleToggle = _ref.handleToggle; 
    4.   return _nervjs["default"].createElement("button", { 
    5.     onClick: handleToggle 
    6.   }, "Toggle"); 

    而至于 plugin 則是一些插件,這些插件可以將對(duì)編譯結(jié)果的處理函數(shù)注冊(cè)在 Webpack 的生命周期鉤子上,在生成最終文件之前對(duì)編譯的結(jié)果做一些處理。比如大多數(shù)場(chǎng)景下我們需要將生成的 JS 文件插入到 Html 文件中去。就需要使用到 html-webpack-plugin 這個(gè)插件,我們需要在配置中這樣寫(xiě)。

     
     
     
     
    1. const HtmlWebpackPlugin = require('html-webpack-plugin'); 
    2.  
    3. const webpackConfig = { 
    4.   entry: 'index.js', 
    5.   output: { 
    6.     path: path.resolve(__dirname, './dist'), 
    7.     filename: 'index_bundle.js' 
    8.   }, 
    9.   // 向plugins數(shù)組中傳入一個(gè)HtmlWebpackPlugin插件的實(shí)例 
    10.   plugins: [new HtmlWebpackPlugin()] 
    11. }; 

    這樣,html-webpack-plugin 會(huì)被注冊(cè)在打包的完成階段,并且會(huì)獲取到最終打包完成的入口 JS 文件路徑,生成一個(gè)形如 的 script 標(biāo)簽插入到 Html 中。這樣瀏覽器就可以通過(guò) html 文件來(lái)展示頁(yè)面內(nèi)容了。

    ok,寫(xiě)到這里,對(duì)于一個(gè)開(kāi)發(fā)者而言,所有配置項(xiàng)和需要被打包的工程代碼文件都已經(jīng)準(zhǔn)備完畢,接下來(lái)需要的就是將工作交給打包工具 Webpack,通過(guò) Webpack 將代碼打包成我們和瀏覽器希望看到的樣子

    工具視角

    首先,我們需要了解 Webpack 打包的流程

     

    從 Webpack 的工作流程中可以看出,我們需要實(shí)現(xiàn)一個(gè) Compiler 類,這個(gè)類需要收集開(kāi)發(fā)者傳入的所有配置信息,然后指揮整體的編譯流程。我們可以把 Compiler 理解為公司老板,它統(tǒng)領(lǐng)全局,并且掌握了全局信息(客戶需求)。在了解了所有信息后它會(huì)調(diào)用另一個(gè)類 Compilation 生成實(shí)例,并且將所有的信息和工作流程托付給它,Compilation 其實(shí)就相當(dāng)于老板的秘書(shū),需要去調(diào)動(dòng)各個(gè)部門按照要求開(kāi)始工作,而 loader 和 plugin 則相當(dāng)于各個(gè)部門,只有在他們專長(zhǎng)的工作( js , css , scss , jpg , png...)出現(xiàn)時(shí)才會(huì)去處理

    為了既實(shí)現(xiàn) Webpack 打包的功能,又只實(shí)現(xiàn)核心代碼。我們對(duì)這個(gè)流程做一些簡(jiǎn)化

     

    首先我們新建了一個(gè) webpack 函數(shù)作為對(duì)外暴露的方法,它接受兩個(gè)參數(shù),其中一個(gè)是配置項(xiàng)對(duì)象,另一個(gè)則是錯(cuò)誤回調(diào)。

     
     
     
     
    1. const Compiler = require('./compiler') 
    2.  
    3. function webpack(config, callback) { 
    4.   // 此處應(yīng)有參數(shù)校驗(yàn) 
    5.   const compiler = new Compiler(config) 
    6.   // 開(kāi)始編譯 
    7.   compiler.run() 
    8.  
    9. module.exports = webpack 

    1. 構(gòu)建配置信息

    我們需要先在 Compiler 類的構(gòu)造方法里面收集用戶傳入的信息

     
     
     
     
    1. class Compiler { 
    2.   constructor(config, _callback) { 
    3.     const { 
    4.       entry, 
    5.       output, 
    6.       module, 
    7.       plugins 
    8.     } = config 
    9.     // 入口 
    10.     this.entryPath = entry 
    11.     // 輸出文件路徑 
    12.     this.distPath = output.path 
    13.     // 輸出文件名稱 
    14.     this.distName = output.fileName 
    15.     // 需要使用的loader 
    16.     this.loaders = module.rules 
    17.     // 需要掛載的plugin 
    18.     this.plugins = plugins 
    19.      // 根目錄 
    20.     this.root = process.cwd() 
    21.      // 編譯工具類Compilation 
    22.     this.compilation = {} 
    23.     // 入口文件在module中的相對(duì)路徑,也是這個(gè)模塊的id 
    24.     this.entryId = getRootPath(this.root, entry, this.root) 
    25.   } 

    同時(shí),我們?cè)跇?gòu)造函數(shù)中將所有的 plugin 掛載到實(shí)例的 hooks 屬性中去。Webpack 的生命周期管理基于一個(gè)叫做 tapable 的庫(kù),通過(guò)這個(gè)庫(kù),我們可以非常方便的創(chuàng)建一個(gè)發(fā)布訂閱模型的鉤子,然后通過(guò)將函數(shù)掛載到實(shí)例上(鉤子事件的回調(diào)支持同步觸發(fā)、異步觸發(fā)甚至進(jìn)行鏈?zhǔn)交卣{(diào)),在合適的時(shí)機(jī)觸發(fā)對(duì)應(yīng)事件的處理函數(shù)。我們?cè)?hooks 上聲明一些生命周期鉤子:

     
     
     
     
    1. const { AsyncSeriesHook } = require('tapable') // 此處我們創(chuàng)建了一些異步鉤子 
    2. constructor(config, _callback) { 
    3.   ... 
    4.   this.hooks = { 
    5.     // 生命周期事件 
    6.     beforeRun: new AsyncSeriesHook(['compiler']), // compiler代表我們將向回調(diào)事件中傳入一個(gè)compiler參數(shù) 
    7.     afterRun: new AsyncSeriesHook(['compiler']), 
    8.     beforeCompile: new AsyncSeriesHook(['compiler']), 
    9.     afterCompile: new AsyncSeriesHook(['compiler']), 
    10.     emit: new AsyncSeriesHook(['compiler']), 
    11.     failed: new AsyncSeriesHook(['compiler']), 
    12.   } 
    13.   this.mountPlugin() 
    14. // 注冊(cè)所有的plugin 
    15. mountPlugin() { 
    16.   for(let i=0;i
    17.     const item = this.plugins[i] 
    18.     if ('apply' in item && typeof item.apply === 'function') { 
    19.       // 注冊(cè)各生命周期鉤子的發(fā)布訂閱監(jiān)聽(tīng)事件 
    20.       item.apply(this) 
    21.     } 
    22.   } 
    23. // 當(dāng)運(yùn)行run方法的邏輯之前 
    24. run() { 
    25.   // 在特定的生命周期發(fā)布消息,觸發(fā)對(duì)應(yīng)的訂閱事件 
    26.   this.hooks.beforeRun.callAsync(this) // this作為參數(shù)傳入,對(duì)應(yīng)之前的compiler 
    27.   ... 

    “冷知識(shí):

    每一個(gè) plugin Class 都必須實(shí)現(xiàn)一個(gè) apply 方法,這個(gè)方法接收 compiler實(shí)例,然后將真正的鉤子函數(shù)掛載到 compiler.hook 的某一個(gè)聲明周期上。

    如果我們聲明了一個(gè) hook 但是沒(méi)有掛載任何方法,在 call 函數(shù)觸發(fā)的時(shí)候是會(huì)報(bào)錯(cuò)的。但是實(shí)際上 Webpack 的每一個(gè)生命周期鉤子除了掛載用戶配置的 plugin ,都會(huì)掛載至少一個(gè) Webpack 自己的 plugin,所以不會(huì)有這樣的問(wèn)題。更多關(guān)于 tapable 的用法也可以移步 Tapable[2]”

    2. 編譯

    接下來(lái)我們需要聲明一個(gè) Compilation 類,這個(gè)類主要是執(zhí)行編譯工作。在 Compilation 的構(gòu)造函數(shù)中,我們先接收來(lái)自老板 Compiler 下發(fā)的信息并且掛載在自身屬性中。

     
     
     
     
    1. class Compilation { 
    2.   constructor(props) { 
    3.     const { 
    4.       entry, 
    5.       root, 
    6.       loaders, 
    7.       hooks 
    8.     } = props 
    9.     this.entry = entry 
    10.     this.root = root 
    11.     this.loaders = loaders 
    12.     this.hooks = hooks 
    13.   } 
    14.   // 開(kāi)始編譯 
    15.   async make() { 
    16.     await this.moduleWalker(this.entry) 
    17.   } 
    18.   // dfs遍歷函數(shù) 
    19.   moduleWalker = async () => {} 

    因?yàn)槲覀冃枰獙⒋虬^(guò)程中引用過(guò)的文件都編譯到最終的代碼包里,所以需要聲明一個(gè)深度遍歷函數(shù) moduleWalker (這個(gè)名字是筆者取的,不是 webpack 官方取的),顧名思義,這個(gè)方法將會(huì)從入口文件開(kāi)始,依次對(duì)文件進(jìn)行第一步和第二步編譯,并且收集引用到的其他模塊,遞歸進(jìn)行同樣的處理。

    編譯步驟分為兩步

    1. 第一步是使用所有滿足條件的 loader 對(duì)其進(jìn)行編譯并且返回編譯之后的源代碼
    2. 第二步相當(dāng)于是 Webpack 自己的編譯步驟,目的是構(gòu)建各個(gè)獨(dú)立模塊之間的依賴調(diào)用關(guān)系。我們需要做的是將所有的 require 方法替換成 Webpack 自己定義的 __webpack_require__ 函數(shù)。因?yàn)樗斜痪幾g后的模塊將被 Webpack 存儲(chǔ)在一個(gè)閉包的對(duì)象 moduleMap 中,而 __webpack_require__ 函數(shù)則是唯一一個(gè)有權(quán)限訪問(wèn) moduleMap 的方法。

    一句話解釋 __webpack_require__的作用就是,將模塊之間原本 文件地址 -> 文件內(nèi)容 的關(guān)系替換成了 對(duì)象的key -> 對(duì)象的value(文件內(nèi)容) 這樣的關(guān)系。

    在完成第二步編譯的同時(shí),會(huì)對(duì)當(dāng)前模塊內(nèi)的引用進(jìn)行收集,并且返回到 Compilation 中, 這樣moduleWalker 才能對(duì)這些依賴模塊進(jìn)行遞歸的編譯。當(dāng)然其中大概率存在循環(huán)引用和重復(fù)引用,我們會(huì)根據(jù)引用文件的路徑生成一個(gè)獨(dú)一無(wú)二的 key 值,在 key 值重復(fù)時(shí)進(jìn)行跳過(guò)。

    i. moduleWalker 遍歷函數(shù)

     
     
     
     
    1. // 存放處理完畢的模塊代碼Map 
    2. moduleMap = {} 
    3.  
    4. // 根據(jù)依賴將所有被引用過(guò)的文件都進(jìn)行編譯 
    5. async moduleWalker(sourcePath) { 
    6.   if (sourcePath in this.moduleMap) return 
    7.   // 在讀取文件時(shí),我們需要完整的以.js結(jié)尾的文件路徑 
    8.   sourcePath = completeFilePath(sourcePath) 
    9.   const [ sourceCode, md5Hash ] = await this.loaderParse(sourcePath) 
    10.   const modulePath = getRootPath(this.root, sourcePath, this.root) 
    11.   // 獲取模塊編譯后的代碼和模塊內(nèi)的依賴數(shù)組 
    12.   const [ moduleCode, relyInModule ] = this.parse(sourceCode, path.dirname(modulePath)) 
    13.   // 將模塊代碼放入ModuleMap 
    14.   this.moduleMap[modulePath] = moduleCode 
    15.   this.assets[modulePath] = md5Hash 
    16.   // 再依次對(duì)模塊中的依賴項(xiàng)進(jìn)行解析 
    17.   for(let i=0;i
    18.     await this.moduleWalker(relyInModule[i], path.dirname(relyInModule[i])) 
    19.   } 

    如果將 dfs 的路徑給 log 出來(lái),我們就可以看到這樣的流程

     

    ii. 第一步編譯 loaderParse函數(shù)

     
     
     
     
    1. async loaderParse(entryPath) { 
    2.   // 用utf8格式讀取文件內(nèi)容 
    3.   let [ content, md5Hash ] = await readFileWithHash(entryPath) 
    4.   // 獲取用戶注入的loader 
    5.   const { loaders } = this 
    6.   // 依次遍歷所有l(wèi)oader 
    7.   for(let i=0;i
    8.     const loader = loaders[i] 
    9.     const { test : reg, use } = loader 
    10.     if (entryPath.match(reg)) { 
    11.       // 判斷是否滿足正則或字符串要求 
    12.       // 如果該規(guī)則需要應(yīng)用多個(gè)loader,從最后一個(gè)開(kāi)始向前執(zhí)行 
    13.       if (Array.isArray(use)) { 
    14.         while(use.length) { 
    15.           const cur = use.pop() 
    16.           const loaderHandler = 
    17.             typeof cur.loader === 'string' 
    18.             // loader也可能來(lái)源于package包例如babel-loader 
    19.               ? require(cur.loader) 
    20.               : ( 
    21.                 typeof cur.loader === 'function' 
    22.                 ? cur.loader : _ => _ 
    23.               ) 
    24.           content = loaderHandler(content) 
    25.         } 
    26.       } else if (typeof use.loader === 'string') { 
    27.         const loaderHandler = require(use.loader) 
    28.         content = loaderHandler(content) 
    29.       } else if (typeof use.loader === 'function') { 
    30.         const loaderHandler = use.loader 
    31.         content = loaderHandler(content) 
    32.       } 
    33.     } 
    34.   } 
    35.   return [ content, md5Hash ] 

    然而這里遇到了一個(gè)小插曲,就是我們平常使用的 babel-loader 似乎并不能在 Webpack 包以外的場(chǎng)景被使用,在 babel-loader 的文檔中看到了這樣一句話

    “This package allows transpiling JavaScript files using Babel and webpack.”不過(guò)好在 @babel/core 和 webpack 并無(wú)聯(lián)系,所以只能辛苦一下,再手寫(xiě)一個(gè) loader 方法去解析 JS 和 ES6 的語(yǔ)法。

     
     
     
     
    1. const babel = require('@babel/core') 
    2.  
    3. module.exports = function BabelLoader (source) { 
    4.   const res = babel.transform(source, { 
    5.     sourceType: 'module' // 編譯ES6 import和export語(yǔ)法 
    6.   }) 
    7.   return res.code 

    當(dāng)然,編譯規(guī)則可以作為配置項(xiàng)傳入,但是為了模擬真實(shí)的開(kāi)發(fā)場(chǎng)景,我們需要配置一下 babel.config.js文件

     
     
     
     
    1. module.exports = function (api) { 
    2.   api.cache(true) 
    3.   return { 
    4.     "presets": [ 
    5.       ['@babel/preset-env', { 
    6.         targets: { 
    7.           "ie": "8" 
    8.         }, 
    9.       }], 
    10.       '@babel/preset-react', // 編譯JSX 
    11.     ], 
    12.     "plugins": [ 
    13.       ["@babel/plugin-transform-template-literals", { 
    14.         "loose": true 
    15.       }] 
    16.     ], 
    17.     "compact": true 
    18.   } 

    于是,在獲得了 loader 處理過(guò)的代碼之后,理論上任何一個(gè)模塊都已經(jīng)可以在瀏覽器或者單元測(cè)試中直接使用了。但是我們的代碼是一個(gè)整體,還需要一種合理的方式來(lái)組織代碼之間互相引用的關(guān)系。

    上面也解釋了我們?yōu)槭裁匆褂?__webpack_require__ 函數(shù)。這里我們得到的代碼仍然是字符串的形式,為了方便我們使用 eval 函數(shù)將字符串解析成直接可讀的代碼。當(dāng)然這只是求快的方式,對(duì)于 JS 這種解釋型語(yǔ)言,如果一個(gè)一個(gè)模塊去解釋編譯的話,速度會(huì)非常慢。事實(shí)上真正的生產(chǎn)環(huán)境會(huì)將模塊內(nèi)容封裝成一個(gè)IIFE(立即自執(zhí)行函數(shù)表達(dá)式)

    總而言之,在第二部編譯 parse 函數(shù)中我們需要做的事情其實(shí)很簡(jiǎn)單,就是將所有模塊中的 require 方法的函數(shù)名稱替換成 __webpack_require__ 即可。我們?cè)谶@一步使用的是 babel 全家桶。 babel 作為業(yè)內(nèi)頂尖的 JS 編譯器,分析代碼的步驟主要分為兩步,分別是詞法分析和語(yǔ)法分析。簡(jiǎn)單來(lái)說(shuō),就是對(duì)代碼片段進(jìn)行逐詞分析,根據(jù)當(dāng)前單詞生成一個(gè)上下文語(yǔ)境。然后進(jìn)行再判斷下一個(gè)單詞在上下文語(yǔ)境中所起的作用。

     

    注意,在這一步中我們還可以“順便”搜集模塊的依賴項(xiàng)數(shù)組一同返回(用于 dfs 遞歸)

     
     
     
     
    1. const parser = require('@babel/parser') 
    2. const traverse = require('@babel/traverse').default 
    3. const types = require('@babel/types') 
    4. const generator = require('@babel/generator').default 
    5. ... 
    6. // 解析源碼,替換其中的require方法來(lái)構(gòu)建ModuleMap 
    7. parse(source, dirpath) { 
    8.   const inst = this 
    9.   // 將代碼解析成ast 
    10.   const ast = parser.parse(source) 
    11.   const relyInModule = [] // 獲取文件依賴的所有模塊 
    12.   traverse(ast, { 
    13.     // 檢索所有的詞法分析節(jié)點(diǎn),當(dāng)遇到函數(shù)調(diào)用表達(dá)式的時(shí)候執(zhí)行,對(duì)ast樹(shù)進(jìn)行改寫(xiě) 
    14.     CallExpression(p) { 
    15.       // 有些require是被_interopRequireDefault包裹的 
    16.       // 所以需要先找到_interopRequireDefault節(jié)點(diǎn) 
    17.       if (p.node.callee && p.node.callee.name === '_interopRequireDefault') { 
    18.         const innerNode = p.node.arguments[0] 
    19.         if (innerNode.callee.name === 'require') { 
    20.           inst.convertNode(innerNode, dirpath, relyInModule) 
    21.         } 
    22.       } else if (p.node.callee.name === 'require') { 
    23.         inst.convertNode(p.node, dirpath, relyInModule) 
    24.       } 
    25.     } 
    26.   }) 
    27.   // 將改寫(xiě)后的ast樹(shù)重新組裝成一份新的代碼, 并且和依賴項(xiàng)一同返回 
    28.   const moduleCode = generator(ast).code 
    29.   return [ moduleCode, relyInModule ] 
    30. /** 
    31.  * 將某個(gè)節(jié)點(diǎn)的name和arguments轉(zhuǎn)換成我們想要的新節(jié)點(diǎn) 
    32.  */ 
    33. convertNode = (node, dirpath, relyInModule) => { 
    34.   node.callee.name = '__webpack_require__' 
    35.   // 參數(shù)字符串名稱,例如'react', './MyName.js' 
    36.   let moduleName = node.arguments[0].value 
    37.   // 生成依賴模塊相對(duì)【項(xiàng)目根目錄】的路徑 
    38.   let moduleKey = completeFilePath(getRootPath(dirpath, moduleName, this.root)) 
    39.   // 收集module數(shù)組 
    40.   relyInModule.push(moduleKey) 
    41.   // 替換__webpack_require__的參數(shù)字符串,因?yàn)檫@個(gè)字符串也是對(duì)應(yīng)模塊的moduleKey,需要保持統(tǒng)一 
    42.   // 因?yàn)閍st樹(shù)中的每一個(gè)元素都是babel節(jié)點(diǎn),所以需要使用'@babel/types'來(lái)進(jìn)行生成 
    43.   node.arguments = [ types.stringLiteral(moduleKey) ] 

    3. emit 生成 bundle 文件

    執(zhí)行到這一步, compilation 的使命其實(shí)就已經(jīng)完成了。如果我們平時(shí)有去觀察生成的 js 文件的話,會(huì)發(fā)現(xiàn)打包出來(lái)的樣子是一個(gè)立即執(zhí)行函數(shù),主函數(shù)體是一個(gè)閉包,閉包中緩存了已經(jīng)加載的模塊 installedModules ,以及定義了一個(gè) __webpack_require__ 函數(shù),最終返回的是函數(shù)入口所對(duì)應(yīng)的模塊。而函數(shù)的參數(shù)則是各個(gè)模塊的 key-value 所組成的對(duì)象。

    我們?cè)谶@里通過(guò) ejs 模板去進(jìn)行拼接,將之前收集到的 moduleMap 對(duì)象進(jìn)行遍歷,注入到 ejs 模板字符串中去。

    模板代碼

     
     
     
     
    1. // template.ejs 
    2. (function(modules) { // webpackBootstrap 
    3.   // The module cache 
    4.   var installedModules = {}; 
    5.   // The require function 
    6.   function __webpack_require__(moduleId) { 
    7.       // Check if module is in cache 
    8.       if(installedModules[moduleId]) { 
    9.           return installedModules[moduleId].exports; 
    10.       } 
    11.       // Create a new module (and put it into the cache) 
    12.       var module = installedModules[moduleId] = { 
    13.           i: moduleId, 
    14.           l: false, 
    15.           exports: {} 
    16.       }; 
    17.       // Execute the module function 
    18.       modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 
    19.       // Flag the module as loaded 
    20.       module.l = true; 
    21.       // Return the exports of the module 
    22.       return module.exports; 
    23.   } 
    24.   // Load entry module and return exports 
    25.   return __webpack_require__(__webpack_require__.s = "<%-entryId%>"); 
    26. })({ 
    27.  <%for(let key in modules) {%> 
    28.      "<%-key%>": 
    29.          (function(module, exports, __webpack_require__) { 
    30.              eval( 
    31.                  `<%-modules[key]%>` 
    32.              ); 
    33.          }), 
    34.      <%}%> 
    35. }); 

    生成 bundle.js

     
     
     
     
    1. /** 
    2.  * 發(fā)射文件,生成最終的bundle.js 
    3.  */ 
    4. emitFile() { // 發(fā)射打包后的輸出結(jié)果文件 
    5.   // 首先對(duì)比緩存判斷文件是否變化 
    6.   const assets = this.compilation.assets 
    7.   const pastAssets = this.getStorageCache() 
    8.   if (loadsh.isEqual(assets, pastAssets)) { 
    9.     // 如果文件hash值沒(méi)有變化,說(shuō)明無(wú)需重寫(xiě)文件 
    10.     // 只需要依次判斷每個(gè)對(duì)應(yīng)的文件是否存在即可 
    11.     // 這一步省略! 
    12.   } else { 
    13.     // 緩存未能命中 
    14.     // 獲取輸出文件路徑 
    15.     const outputFile = path.join(this.distPath, this.distName); 
    16.     // 獲取輸出文件模板 
    17.     // const templateStr = this.generateSourceCode(path.join(__dirname, '..', "bundleTemplate.ejs")); 
    18.     const templateStr = fs.readFileSync(path.join(__dirname, '..', "template.ejs"), 'utf-8'); 
    19.     // 渲染輸出文件模板 
    20.     const code = ejs.render(templateStr, {entryId: this.entryId, modules: this.compilation.moduleMap}); 
    21.  
    22.     this.assets = {}; 
    23.     this.assets[outputFile] = code; 
    24.     // 將渲染后的代碼寫(xiě)入輸出文件中 
    25.     fs.writeFile(outputFile, this.assets[outputFile], function(e) { 
    26.       if (e) { 
    27.         console.log('[Error] ' + e) 
    28.       } else { 
    29.         console.log('[Success] 編譯成功') 
    30.       } 
    31.     }); 
    32.     // 將緩存信息寫(xiě)入緩存文件 
    33.     fs.writeFileSync(resolve(this.distPath, 'manifest.json'), JSON.stringify(assets, null, 2)) 
    34.   } 

    在這一步中我們根據(jù)文件內(nèi)容生成的 Md5Hash 去對(duì)比之前的緩存來(lái)加快打包速度,細(xì)心的同學(xué)會(huì)發(fā)現(xiàn) Webpack 每次打包都會(huì)生成一個(gè)緩存文件 manifest.json,形如

     
     
     
     
    1.   "main.js": "./js/main7b6b4.js", 
    2.   "main.css": "./css/maincc69a7ca7d74e1933b9d.css", 
    3.   "main.js.map": "./js/main7b6b4.js.map", 
    4.   "vendors~main.js": "./js/vendors~main3089a.js", 
    5.   "vendors~main.css": "./css/vendors~maincc69a7ca7d74e1933b9d.css", 
    6.   "vendors~main.js.map": "./js/vendors~main3089a.js.map", 
    7.   "js/28505f.js": "./js/28505f.js", 
    8.   "js/28505f.js.map": "./js/28505f.js.map", 
    9.   "js/34c834.js": "./js/34c834.js", 
    10.   "js/34c834.js.map": "./js/34c834.js.map", 
    11.   "js/4d218c.js": "./js/4d218c.js", 
    12.   "js/4d218c.js.map": "./js/4d218c.js.map", 
    13.   "index.html": "./index.html", 
    14.   "static/initGlobalSize.js": "./static/initGlobalSize.js" 

    這也是文件斷點(diǎn)續(xù)傳中常用到的一個(gè)判斷,這里就不做詳細(xì)的展開(kāi)了

    檢驗(yàn)

    做完這一步,我們已經(jīng)基本大功告成了(誤:如果不考慮令人智息的 debug 過(guò)程的話),接下來(lái)我們?cè)?package.json 里面配置好打包腳本

     
     
     
     
    1. "scripts": { 
    2.   "build": "node build.js" 

    運(yùn)行 yarn build

     

    (@ο@) 哇~激動(dòng)人心的時(shí)刻到了。

    然而...

     

    看著打包出來(lái)的這一坨奇怪的東西報(bào)錯(cuò),心里還是有點(diǎn)想笑的。檢查了一下發(fā)現(xiàn)是因?yàn)榉匆?hào)遇到注釋中的反引號(hào)于是拼接字符串提前結(jié)束了。好吧,那么我在 babel traverse 時(shí)加了幾句代碼,刪除掉了代碼中所有的注釋。但是隨之而來(lái)的又是一些其他的問(wèn)題。

    好吧,可能在實(shí)際 react 生產(chǎn)打包中還有一些其他的步驟,但是這不在今天討論的話題當(dāng)中。此時(shí),鬼魅的框架涌上心頭。我腦中想起了京東凹凸實(shí)驗(yàn)室自研的高性能,兼容性優(yōu)秀,緊跟 react 版本的類 react 框架 NervJS ,或許 NervJS 平易近人(誤)的代碼能夠支持這款令人抱歉的打包工具

    于是我們?cè)?babel.config.js 中配置 alias 來(lái)替換 react 依賴項(xiàng)。(React項(xiàng)目轉(zhuǎn)NervJS就是這么簡(jiǎn)單)

     
     
     
     
    1. module.exports = function (api) { 
    2.   api.cache(true) 
    3.   return { 
    4.         ... 
    5.     "plugins": [ 
    6.             ... 
    7.       [ 
    8.         "module-resolver", { 
    9.           "root": ["."], 
    10.           "alias": { 
    11.             "react": "nervjs", 
    12.             "react-dom": "nervjs", 
    13.             // Not necessary unless you consume a module using `createClass` 
    14.             "create-react-class": "nerv-create-class" 
    15.           } 
    16.         } 
    17.       ] 
    18.     ], 
    19.     "compact": true 
    20.   } 

    運(yùn)行 yarn build

     

    (@ο@) 哇~代碼終于成功運(yùn)行了起來(lái),雖然存在著許多的問(wèn)題,但是至少這個(gè) webpack 在設(shè)計(jì)如此簡(jiǎn)單的情況下已經(jīng)有能力支持大部分 JS 框架了。感興趣的同學(xué)也可以自己嘗試寫(xiě)一寫(xiě),或者直接從這里[3]clone 下來(lái)看

    毫無(wú)疑問(wèn),Webpack 是一個(gè)非常優(yōu)秀的代碼模塊打包工具(雖然它的官網(wǎng)非常低調(diào)的沒(méi)有任何 slogen)。一款非常優(yōu)秀的工具,必然是在保持了自己本身的特性的同時(shí),同時(shí)能夠賦予其他開(kāi)發(fā)者在其基礎(chǔ)上拓展設(shè)想之外作品的能力。如果有能力深入學(xué)習(xí)這些工具,對(duì)于我們?cè)诖a工程領(lǐng)域的認(rèn)知也會(huì)有很大的提升。

    參考資料

    [1]webpack 中文文檔: https://www.webpackjs.com/[2]Tapable: https://github.com/webpack/tapable[3]這里: https://github.com/XHFkindergarten/jerkpack


    本文名稱:Webpack原理淺析
    標(biāo)題路徑:http://m.5511xx.com/article/ccdhgsd.html