精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

Webpack原理淺析

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

 [[336301]]

背景

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

開發(fā)者視角

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

  1. // index.js 根組件 
  2. import React from 'react' 
  3. import ReactDom from 'react-dom' 
  4. import App from './App' 
  5. ReactDom.render(<App />, document.querySelector('#container')) 
  1. // App.js 頁面組件 
  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.       <div> 
  20.         <h1>Hello, { toggle ? 'NervJS' : 'O2 Team'}</h1> 
  21.         <Switch handleToggle={this.handleToggle.bind(this)} /> 
  22.       </div> 
  23.     ) 
  24.   } 
  1. // Switch.js 按鈕組件 
  2. import React from 'react' 
  3.  
  4. export default function Switch({ handleToggle }) { 
  5.   return ( 
  6.     <button onClick={handleToggle}>Toggle</button> 
  7.   ) 

接著我們需要一個配置文件讓 Webpack 知道我們期望它如何工作,于是我們在根目錄下新建一個文件 webpack.config.js 并且向其中寫入一些基礎的配置。(如果不太熟悉配置內容可以先學習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 字段和文件名匹配成功時就用對應的 loader 對代碼進行編譯,Webpack本身只認識 .js 、 .json 這兩種類型的文件,而通過 loader,我們就可以對例如 css 等其他格式的文件進行處理。

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

經過以上處理之后。瀏覽器真正閱讀到的按鈕組件代碼其實大概是這個樣子的。

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

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

  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數組中傳入一個HtmlWebpackPlugin插件的實例 
  10.   plugins: [new HtmlWebpackPlugin()] 
  11. }; 

這樣,html-webpack-plugin 會被注冊在打包的完成階段,并且會獲取到最終打包完成的入口 JS 文件路徑,生成一個形如 的 script 標簽插入到 Html 中。這樣瀏覽器就可以通過 html 文件來展示頁面內容了。

ok,寫到這里,對于一個開發(fā)者而言,所有配置項和需要被打包的工程代碼文件都已經準備完畢,接下來需要的就是將工作交給打包工具 Webpack,通過 Webpack 將代碼打包成我們和瀏覽器希望看到的樣子

工具視角

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

 

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

為了既實現 Webpack 打包的功能,又只實現核心代碼。我們對這個流程做一些簡化

 

首先我們新建了一個 webpack 函數作為對外暴露的方法,它接受兩個參數,其中一個是配置項對象,另一個則是錯誤回調。

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

1. 構建配置信息

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

  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中的相對路徑,也是這個模塊的id 
  24.     this.entryId = getRootPath(this.root, entry, this.root) 
  25.   } 

同時,我們在構造函數中將所有的 plugin 掛載到實例的 hooks 屬性中去。Webpack 的生命周期管理基于一個叫做 tapable 的庫,通過這個庫,我們可以非常方便的創(chuàng)建一個發(fā)布訂閱模型的鉤子,然后通過將函數掛載到實例上(鉤子事件的回調支持同步觸發(fā)、異步觸發(fā)甚至進行鏈式回調),在合適的時機觸發(fā)對應事件的處理函數。我們在 hooks 上聲明一些生命周期鉤子:

  1. const { AsyncSeriesHook } = require('tapable') // 此處我們創(chuàng)建了一些異步鉤子 
  2. constructor(config, _callback) { 
  3.   ... 
  4.   this.hooks = { 
  5.     // 生命周期事件 
  6.     beforeRun: new AsyncSeriesHook(['compiler']), // compiler代表我們將向回調事件中傳入一個compiler參數 
  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. // 注冊所有的plugin 
  15. mountPlugin() { 
  16.   for(let i=0;i<this.plugins.length;i++) { 
  17.     const item = this.plugins[i] 
  18.     if ('apply' in item && typeof item.apply === 'function') { 
  19.       // 注冊各生命周期鉤子的發(fā)布訂閱監(jiān)聽事件 
  20.       item.apply(this) 
  21.     } 
  22.   } 
  23. // 當運行run方法的邏輯之前 
  24. run() { 
  25.   // 在特定的生命周期發(fā)布消息,觸發(fā)對應的訂閱事件 
  26.   this.hooks.beforeRun.callAsync(this) // this作為參數傳入,對應之前的compiler 
  27.   ... 

“冷知識:

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

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

2. 編譯

接下來我們需要聲明一個 Compilation 類,這個類主要是執(zhí)行編譯工作。在 Compilation 的構造函數中,我們先接收來自老板 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.   // 開始編譯 
  15.   async make() { 
  16.     await this.moduleWalker(this.entry) 
  17.   } 
  18.   // dfs遍歷函數 
  19.   moduleWalker = async () => {} 

因為我們需要將打包過程中引用過的文件都編譯到最終的代碼包里,所以需要聲明一個深度遍歷函數 moduleWalker (這個名字是筆者取的,不是 webpack 官方取的),顧名思義,這個方法將會從入口文件開始,依次對文件進行第一步和第二步編譯,并且收集引用到的其他模塊,遞歸進行同樣的處理。

編譯步驟分為兩步

  1. 第一步是使用所有滿足條件的 loader 對其進行編譯并且返回編譯之后的源代碼
  2. 第二步相當于是 Webpack 自己的編譯步驟,目的是構建各個獨立模塊之間的依賴調用關系。我們需要做的是將所有的 require 方法替換成 Webpack 自己定義的 __webpack_require__ 函數。因為所有被編譯后的模塊將被 Webpack 存儲在一個閉包的對象 moduleMap 中,而 __webpack_require__ 函數則是唯一一個有權限訪問 moduleMap 的方法。

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

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

i. moduleWalker 遍歷函數

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

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

 

ii. 第一步編譯 loaderParse函數


 

  1. async loaderParse(entryPath) { 
  2.   // 用utf8格式讀取文件內容 
  3.   let [ content, md5Hash ] = await readFileWithHash(entryPath) 
  4.   // 獲取用戶注入的loader 
  5.   const { loaders } = this 
  6.   // 依次遍歷所有l(wèi)oader 
  7.   for(let i=0;i<loaders.length;i++) { 
  8.     const loader = loaders[i] 
  9.     const { test : reg, use } = loader 
  10.     if (entryPath.match(reg)) { 
  11.       // 判斷是否滿足正則或字符串要求 
  12.       // 如果該規(guī)則需要應用多個loader,從最后一個開始向前執(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也可能來源于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 ] 

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

“This package allows transpiling JavaScript files using Babel and webpack.”不過好在 @babel/core 和 webpack 并無聯系,所以只能辛苦一下,再手寫一個 loader 方法去解析 JS 和 ES6 的語法。

  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語法 
  6.   }) 
  7.   return res.code 

當然,編譯規(guī)則可以作為配置項傳入,但是為了模擬真實的開發(fā)場景,我們需要配置一下 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 處理過的代碼之后,理論上任何一個模塊都已經可以在瀏覽器或者單元測試中直接使用了。但是我們的代碼是一個整體,還需要一種合理的方式來組織代碼之間互相引用的關系。

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

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

 

注意,在這一步中我們還可以“順便”搜集模塊的依賴項數組一同返回(用于 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方法來構建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é)點,當遇到函數調用表達式的時候執(zhí)行,對ast樹進行改寫 
  14.     CallExpression(p) { 
  15.       // 有些require是被_interopRequireDefault包裹的 
  16.       // 所以需要先找到_interopRequireDefault節(jié)點 
  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.   // 將改寫后的ast樹重新組裝成一份新的代碼, 并且和依賴項一同返回 
  28.   const moduleCode = generator(ast).code 
  29.   return [ moduleCode, relyInModule ] 
  30. /** 
  31.  * 將某個節(jié)點的name和arguments轉換成我們想要的新節(jié)點 
  32.  */ 
  33. convertNode = (node, dirpath, relyInModule) => { 
  34.   node.callee.name = '__webpack_require__' 
  35.   // 參數字符串名稱,例如'react''./MyName.js' 
  36.   let moduleName = node.arguments[0].value 
  37.   // 生成依賴模塊相對【項目根目錄】的路徑 
  38.   let moduleKey = completeFilePath(getRootPath(dirpath, moduleName, this.root)) 
  39.   // 收集module數組 
  40.   relyInModule.push(moduleKey) 
  41.   // 替換__webpack_require__的參數字符串,因為這個字符串也是對應模塊的moduleKey,需要保持統(tǒng)一 
  42.   // 因為ast樹中的每一個元素都是babel節(jié)點,所以需要使用'@babel/types'來進行生成 
  43.   node.arguments = [ types.stringLiteral(moduleKey) ] 

3. emit 生成 bundle 文件

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

我們在這里通過 ejs 模板去進行拼接,將之前收集到的 moduleMap 對象進行遍歷,注入到 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ā)射打包后的輸出結果文件 
  5.   // 首先對比緩存判斷文件是否變化 
  6.   const assets = this.compilation.assets 
  7.   const pastAssets = this.getStorageCache() 
  8.   if (loadsh.isEqual(assets, pastAssets)) { 
  9.     // 如果文件hash值沒有變化,說明無需重寫文件 
  10.     // 只需要依次判斷每個對應的文件是否存在即可 
  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.     // 將渲染后的代碼寫入輸出文件中 
  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.     // 將緩存信息寫入緩存文件 
  33.     fs.writeFileSync(resolve(this.distPath, 'manifest.json'), JSON.stringify(assets, null, 2)) 
  34.   } 

在這一步中我們根據文件內容生成的 Md5Hash 去對比之前的緩存來加快打包速度,細心的同學會發(fā)現 Webpack 每次打包都會生成一個緩存文件 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" 

這也是文件斷點續(xù)傳中常用到的一個判斷,這里就不做詳細的展開了

檢驗

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

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

運行 yarn build

 

(@ο@) 哇~激動人心的時刻到了。

然而...

 

看著打包出來的這一坨奇怪的東西報錯,心里還是有點想笑的。檢查了一下發(fā)現是因為反引號遇到注釋中的反引號于是拼接字符串提前結束了。好吧,那么我在 babel traverse 時加了幾句代碼,刪除掉了代碼中所有的注釋。但是隨之而來的又是一些其他的問題。

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

于是我們在 babel.config.js 中配置 alias 來替換 react 依賴項。(React項目轉NervJS就是這么簡單)

  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.   } 

運行 yarn build

 

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

毫無疑問,Webpack 是一個非常優(yōu)秀的代碼模塊打包工具(雖然它的官網非常低調的沒有任何 slogen)。一款非常優(yōu)秀的工具,必然是在保持了自己本身的特性的同時,同時能夠賦予其他開發(fā)者在其基礎上拓展設想之外作品的能力。如果有能力深入學習這些工具,對于我們在代碼工程領域的認知也會有很大的提升。

參考資料

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

 

 

責任編輯:武曉燕 來源: 51CTO專欄
相關推薦

2021-12-20 00:03:38

Webpack運行機制

2020-11-05 11:14:29

Docker底層原理

2023-05-11 07:25:57

ReduxMiddleware函數

2009-07-16 10:23:30

iBATIS工作原理

2021-09-13 09:40:35

Webpack 前端HMR 原理

2021-05-31 05:36:43

WebpackJavaScript 前端

2021-04-19 10:45:52

Webpack熱更新前端

2019-08-05 13:20:35

Android繪制代碼

2011-04-13 15:01:39

2022-06-09 15:53:16

移動端渲染GPU

2010-09-25 14:01:11

Java跨平臺

2021-08-26 10:30:29

WebpackTree-Shakin前端

2021-12-15 23:42:56

Webpack原理實踐

2022-08-26 13:24:03

version源碼sources

2009-07-03 17:48:34

JSP頁面翻譯

2011-04-29 12:54:14

筆記本

2023-02-12 23:23:30

2022-09-04 21:08:50

響應式設計Resize

2010-08-05 17:35:34

RIP路由協(xié)議

2009-07-06 09:23:51

Servlet定義
點贊
收藏

51CTO技術棧公眾號

美女隐私在线观看| 国产香蕉视频在线| 伦一区二区三区中文字幕v亚洲| 久久精品一区二区三区不卡牛牛| 国产精品热视频| tube国产麻豆| 欧美丝袜足交| 在线精品视频免费观看| 日韩国产精品毛片| 人妻少妇精品无码专区| 首页国产欧美日韩丝袜| 精品国产美女在线| 亚洲精品无码一区二区| 精品网站在线| 亚洲在线视频网站| 日韩精品欧美在线| 蜜桃av鲁一鲁一鲁一鲁俄罗斯的| 日本不卡一区二区三区 | 青青青草网站免费视频在线观看| 日韩av电影一区| 欧美日韩xxxxx| 免费成人深夜天涯网站| 大奶一区二区三区| 欧美日韩成人综合| 99精品视频播放| 女人黄色免费在线观看| 国产精品久久久久毛片软件| 国产美女在线精品免费观看| 亚洲中文字幕一区二区| 国产精品毛片一区二区三区| 欧美大码xxxx| 日本裸体美女视频| 精品免费一区二区| 日韩精品亚洲精品| 国内精品免费视频| 国产精品jizz视频| 青青草偷拍视频| 日韩不卡一区| 亚洲欧美制服第一页| 久久久男人的天堂| 国产精品视频一区二区三区| 欧美性色综合网| 欧美韩国日本在线| 黄色软件视频在线观看| 亚洲黄色免费网站| 一区二区视频在线免费| 成年人视频免费在线观看| 91美女片黄在线观看91美女| 国产午夜精品在线| 三级小视频在线观看| 盗摄精品av一区二区三区| 亚洲一区二区三区香蕉| 97在线播放免费观看| 久久99精品久久久久| 国产精品视频自在线| 日韩黄色一级视频| 日本成人中文字幕在线视频| 国产精品九九九| 懂色av蜜臀av粉嫩av喷吹| 久久亚洲色图| 国产精品高潮在线| 中文字幕 国产| 精品午夜久久福利影院| 91丝袜美腿美女视频网站| 99久久一区二区| 国产电影精品久久禁18| 99久久精品无码一区二区毛片 | 精品国产成人在线影院| 免费黄色av网址| 北条麻妃一区二区三区在线观看| 欧美xxxxx牲另类人与| 成人三级做爰av| 国产精品一线| 亚洲区一区二区| 日本一二三不卡视频| 久久国产精品亚洲人一区二区三区| 色婷婷成人综合| 杨钰莹一级淫片aaaaaa播放| 欧美日韩综合| 97av在线播放| 九九热最新视频| 国产一区二区日韩精品| 懂色一区二区三区av片| 免费在线性爱视频| 国产精品久99| 欧美亚洲黄色片| 久久爱91午夜羞羞| 欧美酷刑日本凌虐凌虐| 女同性αv亚洲女同志| 欧美色资源站| 日韩午夜在线视频| 国产精品二区一区二区aⅴ| 噜噜噜91成人网| 91久久精品视频| 五月激情婷婷网| 国产精品乱子久久久久| 成人免费a级片| 国产精品高清乱码在线观看 | 丰满人妻一区二区三区免费视频| 91丨porny丨蝌蚪视频| 五月天国产一区| 国产丝袜在线播放| 欧美在线免费播放| 日本美女视频网站| 日韩系列欧美系列| 韩国一区二区电影| 国产又粗又长又大视频| 久久综合九色综合欧美就去吻| 中文字幕一区二区三区最新| av岛国在线| 在线成人免费视频| 伊人网在线视频观看| 亚洲成人最新网站| 日韩女在线观看| 亚洲国产www| 国产精品国产三级国产aⅴ中文 | 一区二区免费在线视频| 色偷偷色偷偷色偷偷在线视频| 91精品在线免费观看| 免费看黄色的视频| 亚洲国内精品| 亚洲精品免费在线视频| 成年人在线观看网站| 精品久久久久久久久久| 日韩高清一二三区| 天天干天天玩天天操| 日韩免费福利视频| 亚洲国产精品悠悠久久琪琪| 我要看黄色一级片| 日韩国产一区二| 久久精品第九区免费观看| 制服丝袜在线播放| 欧美日韩精品一区二区三区四区 | 国产精品色呦| 欧美人在线视频| 国产精品视频一区二区三区,| 久久久久久久久久久99999| 国内自拍中文字幕| 国产精品毛片aⅴ一区二区三区| 伊人久久男人天堂| 欧美videossex极品| 国产一区在线看| 亚洲午夜精品久久久久久浪潮| 国模套图日韩精品一区二区| 亚洲国产精品国自产拍av秋霞| avove在线播放| 精品亚洲国内自在自线福利| 夜夜爽www精品| 欧美成人福利| 日韩在线观看免费高清| 一级片在线免费观看视频| 中文字幕精品一区二区三区精品| 日韩av一二三四| 精品久久成人| 国产有码一区二区| 免费在线观看黄色网| 91麻豆精品国产无毒不卡在线观看| 免费一级suv好看的国产网站| 美女尤物国产一区| 中文精品视频一区二区在线观看| 91国产一区| 欧美二区在线播放| 色婷婷中文字幕| 婷婷丁香久久五月婷婷| 3d动漫精品啪啪一区二区下载| 性欧美videos另类喷潮| 视频一区视频二区视频| 电影一区中文字幕| 国内精品小视频在线观看| 日韩一区二区三区不卡| 欧美日韩一区二区免费在线观看 | 丰满肉肉bbwwbbww| 五月综合激情网| 国产成人精品无码免费看夜聊软件| 日本在线播放一区二区三区| 国产又爽又黄ai换脸| 成人动漫视频| 日韩美女视频免费在线观看| 91.xxx.高清在线| 欧美一区永久视频免费观看| 国产亚洲精品码| 久久久久久久久一| 黄色三级视频在线播放| 在线国产日韩| 亚洲二区自拍| 日本精品国产| 日本精品视频在线观看| 毛片在线看片| 亚洲精品99久久久久中文字幕| 久久久国产免费| 一区二区在线看| 一区二区不卡免费视频| 美女视频一区在线观看| 久青草视频在线播放| 国产精品一区2区3区| 91久久国产综合久久91精品网站| 日韩精品极品| 久久夜色精品国产亚洲aⅴ| 视频一区 中文字幕| 欧美亚洲愉拍一区二区| 久久网中文字幕| 国产精品久久三区| 久久久久国产精品无码免费看| 免费在线视频一区| 欧美a级免费视频| 欧美亚洲精品在线| 国语精品中文字幕| 玖玖精品一区| 国产精品久久久久av| 1区2区3区在线| www国产亚洲精品久久网站| 亚洲 国产 欧美 日韩| 7777精品伊人久久久大香线蕉完整版 | 91地址最新发布| 麻豆传媒在线观看| 亚洲欧美日韩直播| 国产91绿帽单男绿奴| 欧美区视频在线观看| 亚洲 欧美 成人| 亚洲图片欧美色图| 秋霞欧美一区二区三区视频免费| 久久影院午夜片一区| 亚洲图片欧美另类| 国产精品一区二区无线| 免费一级特黄录像| 久久精品日韩欧美| 好吊妞无缓冲视频观看| 欧美日韩免费| 中文一区一区三区免费| 欧美丝袜丝交足nylons172| 久久涩涩网站| 福利欧美精品在线| 97av自拍| 日韩欧美激情电影| 亚洲伊人久久大香线蕉av| 精品美女一区| 国产日本欧美一区二区三区| 色8久久影院午夜场| 欧美综合在线观看| 中文在线а√天堂| 欧美在线视频免费观看| 理论片午夜视频在线观看| 久久久久久高潮国产精品视| av免费在线观看网址| 欧美成人精品xxx| a级影片在线观看| 欧美精品在线第一页| 尤物yw193can在线观看| 欧美高清在线播放| 黑人精品视频| 国内精品中文字幕| 日韩脚交footjobhd| 日本高清久久天堂| 日韩另类视频| 国产日韩欧美夫妻视频在线观看| 欧美一级网址| 亚洲xxxx在线| 嗯用力啊快一点好舒服小柔久久| 国产综合 伊人色| 日韩三级视频| 色狠狠久久av五月综合| 国产精品国产三级国产在线观看| 正在播放国产精品| 欧美.www| 免费看又黄又无码的网站| 香蕉国产精品偷在线观看不卡| 成人一级片网站| 日本午夜一本久久久综合| 日本国产一级片| 国产v日产∨综合v精品视频| 波多野结衣有码| 亚洲国产成人私人影院tom| 日韩亚洲欧美中文字幕| 一区二区三区中文字幕精品精品| 国产一级片久久| 日韩欧美一区二区在线| 在线观看国产成人| 日韩女优毛片在线| 青青青手机在线视频观看| 日日骚久久av| 成人免费网站观看| 国产精品久久久久久久久久久久| 国产日本亚洲| 久久免费视频1| 一本一本久久a久久综合精品| 阿v天堂2018| 美洲天堂一区二卡三卡四卡视频| 日本少妇xxx| 久久久久久久久99精品| 色欲一区二区三区精品a片| 亚洲va国产天堂va久久en| 综合久久中文字幕| 亚洲国产精品va| 黄色网页在线观看| 欧美亚洲午夜视频在线观看| 日韩成人免费av| 麻豆亚洲一区| 午夜精品av| 国产91在线视频观看| 免费的国产精品| 你懂得在线视频| 亚洲免费在线视频一区 二区| 日本午夜视频在线观看| 欧美一区二区大片| 波多野结衣在线影院| 国内伊人久久久久久网站视频| 激情久久一区二区| 久久精品99久久| 黄色综合网站| а 天堂 在线| 国产丝袜美腿一区二区三区| 久久精品国产亚洲AV无码男同| 欧美日韩免费观看一区三区| 亚洲 欧美 自拍偷拍| 欧美成人手机在线| 国产精品伊人| 日韩精品不卡| 国产欧美日本| 337p日本欧洲亚洲大胆张筱雨| 国产精品免费人成网站| 你懂的国产在线| 精品国产伦一区二区三区免费| 日本精品在线| 国产精品av电影| 欧美人妖在线观看| 亚洲精品蜜桃久久久久久| 久久99热狠狠色一区二区| 女人又爽又黄免费女仆| 午夜精品一区二区三区免费视频 | 国产美女www| 日韩成人在线网站| 国产乱码在线| 不卡一区二区三区四区五区| 亚州av乱码久久精品蜜桃| 亚洲xxxx2d动漫1| 国产日韩精品一区二区三区| 亚洲天堂av片| 日韩电影在线观看永久视频免费网站| 亚洲性图自拍| 亚洲综合中文字幕在线| 中文字幕乱码亚洲无线精品一区 | 国产亚洲一区二区在线观看| wwwxxx亚洲| 亚洲激情在线观看| av成人影院在线| 久久精品人成| 免费视频一区| 亚洲图片另类小说| 在线一区二区三区四区五区| 九色在线视频蝌蚪| 国产精品高清免费在线观看| 波多野结衣在线观看一区二区三区| 中文字幕第36页| 亚洲欧洲日产国产综合网| 国产精品久久久久久在线| 久久精品人人做人人爽| 91麻豆精品国产综合久久久 | 亚洲色欲久久久综合网东京热| 精品一区二区精品| 中文字幕av播放| 精品久久久久久亚洲综合网 | 欧美成人一品| 日本一区二区在线观看视频| 红桃av永久久久| 久色视频在线| 国产欧美韩国高清| 午夜久久黄色| 久久久午夜精品福利内容| 一本色道久久综合狠狠躁的推荐| 国产一二三区在线视频| 国产综合久久久久久| 欧美精品国产一区二区| 少妇一级淫片免费放播放| 色综合久久久久| 日本成人在线播放| 91精品久久香蕉国产线看观看| 亚洲经典在线| youjizz亚洲女人| 欧美一级黄色片| videos性欧美另类高清| 亚洲视频小说| 成人高清在线视频| 少妇又紧又色又爽又刺激视频 | 国产真实精品久久二三区| 久久综合激情网| 亚洲一区999| 奇米一区二区| 东京热加勒比无码少妇| 亚洲天堂网中文字| 天堂在线中文字幕| 亚洲国产精品久久不卡毛片 | 亚洲精品小视频| 日本成人一区二区| 又粗又黑又大的吊av| 国产精品传媒在线| 四虎在线免费看| 亚洲qvod图片区电影| 另类亚洲自拍| 欧美日韩中文视频| 色天天综合狠狠色|