最近在前端論壇閒逛,看到了一些講parcel、webpack的文章,就忽然很好奇,天天都在用的打包工具,他們打包的原理到底是什麼。只有知道了這一點,才能夠在衆多的打包工具裏,找到最適合的那個它。在瞭解打包原理以前,先花一些篇章說明了一下爲何要使用打包工具。html
前端產品的交付是基於瀏覽器,這些資源是經過增量加載的方式運行到瀏覽器端,如何在開發環境組織好這些碎片化的代碼和資源,而且保證他們在瀏覽器端快速、優雅的加載和更新,就須要一個模塊化系統。這個理想中的模塊化系統是前端工程師多年來一直探索的難題。前端
模塊系統主要解決模塊的定義、依賴和導出。 原始的<script>
標籤加載方式有一些常見的弊端:例如全局做用域下容易形成變量衝突;文件只能按照<script>
的書寫順序進行加載;開發人員必須主觀解決模塊和代碼庫的依賴關係等。node
所以衍生出不少模塊化方案:webpack
1.CommonJs:優勢:服務器端模塊便於重用。缺點:同步的模塊加載方式不適合在瀏覽器環境中,同步意味着阻塞加載,瀏覽器資源是異步加載的。git
2.AMD:依賴前置。優勢:適合在瀏覽器環境異步加載;缺點:閱讀和書寫比較困難。github
3.CMD:依賴就近,延遲執行。優勢:很容易在node中運行;缺點:依賴spm打包,模塊的加載邏輯偏重。web
4.ES6模塊::儘可能的靜態化,使得編譯時就能肯定模塊的依賴關係,以及輸入輸出的變量。CommonJS 和 AMD 模塊,都只能在運行時肯定這些東西。優勢:容易進行靜態分析;缺點:原生瀏覽器未實現該標準。數組
說到模塊的加載和傳輸,如果每一個文件都單獨請求,會致使請求次數過多,致使啓動速度過慢。如果所有打包在一塊只請求一次,會致使流量浪費,初始化過程慢。所以最佳方案是分塊傳輸,按需進行懶加載,在實際用到某些模塊的時候再增量更新。要實現模塊的按需加載,就須要一個對整個代碼庫中的模塊進行靜態分析、編譯打包的過程。Webpack 就是在這樣的需求中應運而生。瀏覽器
注:要注意一個概念,一切皆模塊。樣式、圖片、字體、HTML 模板等等衆多的資源,均可以視做模塊。緩存
Webpack 是一個模塊打包器。它將根據模塊的依賴關係進行靜態分析,而後將這些模塊按照指定的規則生成對應的靜態資源。 那麼問題來了,webpack真的能作到上述提到的靜態分析、編譯打包麼?咱們首先來看一下webpack能作什麼:
1.代碼拆分 Webpack 有兩種組織模塊依賴的方式,同步和異步。異步依賴做爲分割點,造成一個新的塊。在優化了依賴樹後,每個異步區塊都做爲一個文件被打包。
2.Loader Webpack 自己只能處理原生的 JavaScript 模塊,可是 loader 轉換器能夠將各類類型的資源轉換成 JavaScript 模塊。這樣,任何資源均可以成爲 Webpack 能夠處理的模塊。
3.智能解析 Webpack 有一個智能解析器,幾乎能夠處理任何第三方庫,不管它們的模塊形式是 CommonJS、 AMD 仍是普通的 JS 文件。
4.插件系統 Webpack 還有一個功能豐富的插件系統。大多數內容功能都是基於這個插件系統運行的,還能夠開發和使用開源的 Webpack 插件,來知足各式各樣的需求。
5.快速運行 Webpack 使用異步 I/O 和多級緩存提升運行效率,這使得 Webpack 可以以使人難以置信的速度快速增量編譯。
以上是webpack五個主要特色,可是看完仍是以爲有些霧裏看山,webpack究竟是如何把一些分散的小模塊,整合成大模塊?又是如何處理好各模塊的依賴關係?下面就以parcel核心開發者@ronami的開源項目minipack爲例,說明以上問題。
打包工具就是負責把一些分散的小模塊,按照必定的規則整合成一個大模塊的工具。與此同時,打包工具也會處理好模塊之間的依賴關係,將項目運行在平臺上。minipack項目最想說明的問題,也是打包工具最核心的部分,就是如何處理好模塊間的依賴關係。
首先,打包工具會從一個入口文件開始,分析裏面的依賴,並進一步地分析依賴中的依賴。 咱們新建三個文件,並創建依賴:
/* name.js */
export const name = 'World'
/* message.js */
import { name } from './name.js'
export default `Hello ${name}!`
/* entry.js */
import message from './message.js'
console.log(message)
複製代碼
首先引入必要的工具
/* minipack.js */
const fs = require('fs')
const path = require('path')
const babylon = require('babylon')
const traverse = require('babel-traverse').default
const { transformFromAst } = require('babel-core')
複製代碼
接着咱們將建立一個函數,參數是文件的路徑,做用是讀取文件內容並提取它的依賴關係。
function createAsset(filename) {
// 以字符串形式讀取文件的內容.
const content = fs.readFileSync(filename, 'utf-8');
// 如今咱們試圖找出這個文件依賴於哪一個文件。雖然咱們能夠經過查看其內容來獲取import字符串. 然而,這是一個很是笨重的方法,咱們將使用JavaScript解析器來代替。
// JavaScript解析器是能夠讀取和理解JavaScript代碼的工具,它們生成一個更抽象的模型,稱爲`ast (抽象語法樹)(https://astexplorer.net)`。
const ast = babylon.parse(content, {
sourceType: 'module',
});
// 定義數組,這個數組將保存這個模塊依賴的模塊的相對路徑.
const dependencies = [];
// 咱們遍歷`ast`來試着理解這個模塊依賴哪些模塊,要作到這一點,咱們須要檢查`ast`中的每一個 `import` 聲明。
// `Ecmascript`模塊至關簡單,由於它們是靜態的. 這意味着你不能`import`一個變量,或者有條件地`import`另外一個模塊。每次咱們看到`import`聲明時,咱們均可以將其數值視爲`依賴性`。
traverse(ast, {
ImportDeclaration: ({node}) =>
// 咱們將依賴關係存入數組
dependencies.push(node.source.value);
},
});
// 咱們還經過遞增簡單計數器爲此模塊分配惟一標識符.
const id = ID++;
// 咱們使用`Ecmascript`模塊和其餘JavaScript,可能不支持全部瀏覽器。
// 爲了確保咱們的程序在全部瀏覽器中運行,
// 咱們將使用[babel](https://babeljs.io)來進行轉換。
// 咱們能夠用`babel-preset-env``將咱們的代碼轉換爲瀏覽器能夠運行的東西.
const {code} = transformFromAst(ast, null, {
presets: ['env'],
});
// 返回有關此模塊的全部信息.
return {
id,
filename,
dependencies,
code,
};
}
複製代碼
如今咱們能夠提取單個模塊的依賴關係,那麼,咱們將提取它的每個依賴關係的依賴關係,並循環下去,直到咱們瞭解應用程序中的每一個模塊以及他們是如何相互依賴的。
function createGraph(entry) {
// 首先解析整個文件.
const mainAsset = createAsset(entry);
// 咱們將使用queue來解析每一個asset的依賴關係.
// 咱們正在定義一個只有entry asset的數組.
const queue = [mainAsset];
// 咱們使用一個`for ... of`循環遍歷 隊列.
// 最初 這個隊列 只有一個asset,可是當咱們迭代它時,咱們會將額外的assert推入到queue中.
// 這個循環將在queue爲空時終止.
for (const asset of queue) {
// 咱們的每個asset都有它所依賴模塊的相對路徑列表.
// 咱們將重複它們,用咱們的`createAsset() `函數解析它們,並跟蹤此模塊在此對象中的依賴關係.
asset.mapping = {};
// 這是這個模塊所在的目錄.
const dirname = path.dirname(asset.filename);
// 咱們遍歷其相關路徑的列表
asset.dependencies.forEach(relativePath => {
// 咱們能夠經過將相對路徑與父資源目錄的路徑鏈接,將相對路徑轉變爲絕對路徑.
const absolutePath = path.join(dirname, relativePath);
// 解析asset,讀取其內容並提取其依賴關係.
const child = createAsset(absolutePath);
// 瞭解`asset`依賴取決於`child`這一點對咱們來講很重要.
// 經過給`asset.mapping`對象增長一個新的屬性(值爲child.id)來表達這種一一對應的關係.
asset.mapping[relativePath] = child.id;
// 最後,咱們將`child`這個資產推入隊列,這樣它的依賴關係也將被迭代和解析.
queue.push(child);
});
}
return queue;
}
複製代碼
接下來咱們定義一個函數,傳入上一步的graph,返回一個能夠在瀏覽器上運行的包。
function bundle(graph) {
let modules = '';
// 在咱們到達該函數的主體以前,咱們將構建一個做爲該函數的參數的對象.
// 請注意,咱們構建的這個字符串被兩個花括號 ({}) 包裹,所以對於每一個模塊,
// 咱們添加一個這種格式的字符串: `key: value,`.
graph.forEach(mod => {
// 圖表中的每一個模塊在這個對象中都有一個entry. 咱們用模塊的id`做爲`key`,用數組做爲`value`
// 第一個參數是用函數包裝的每一個模塊的代碼. 這是由於模塊應該被限定範圍: 在一個模塊中定義變量不會影響其餘模塊或全局範圍.
// 對於第二個參數,咱們用`stringify`解析模塊及其依賴之間的關係(也就是上文的asset.mapping). 解析後的對象看起來像這樣: `{'./relative/path': 1}`.
// 這是由於咱們模塊的被轉換後會經過相對路徑來調用`require()`. 當調用這個函數時,咱們應該可以知道依賴圖中的哪一個模塊對應於該模塊的相對路徑.
modules += `${mod.id}: [
function (require, module, exports) { ${mod.code} },
${JSON.stringify(mod.mapping)},
],`;
/ 最後,使用`commonjs`,當模塊須要被導出時,它能夠經過改變exports對象來暴露模塊的值.
// require函數最後會返回exports對象.
const result = `
(function(modules) {
function require(id) {
const [fn, mapping] = modules[id];
function localRequire(name) {
return require(mapping[name]);
}
const module = { exports : {} };
fn(localRequire, module, module.exports);
return module.exports;
}
require(0);
})({${modules}})
`;
return result;
});
複製代碼
運行!
const graph = createGraph('./example/entry.js');
const result = bundle(graph);
//獲得結果,開心!
console.log(result);
複製代碼
更多信息可訪問項目github地址
webpack解決了包與包之間潛在的循環依賴難題,同時,按需合併靜態文件,以免瀏覽器在網絡取數階段的併發瓶頸。除了打包,還能夠進一步實現壓縮(減小網絡傳輸)和編譯(ES六、JSX等語法向下兼容)的功能。
基於對webpack.config.js文件的配置,執行打包時的工做原理,可總結爲:把頁面邏輯看成一個總體,經過一個給定的入口文件,webpack從這個文件開始,找到全部的依賴文件,進行打包、編譯、壓縮,最後輸出一個瀏覽器可識別的JS文件。
一個模塊打包工具,第一步會從入口文件開始,對其進行依賴分析,第二步對其全部依賴再次遞歸進行依賴分析,第三步構建出模塊的依賴圖集,最後一步根據依賴圖集使用CommonJS規範構建出最終的代碼。
https://mp.weixin.qq.com/s/w-oXmHNSyu0Y_IlfmDwJKQ
https://github.com/chinanf-boy/minipack-explain/blob/master/src/minipack.js
https://zhaoda.net/webpack-handbook/configuration.html