未能加载文件或程序集或它的某一个依赖项_手写一个miniwebpack
前言
之前好友希望能介紹一下 webapck 相關的內容,所以最近花費了兩個多月的準備,終于完成了 webapck 系列,它包括一下幾部分:
webapck 系列一:手寫一個 JavaScript 打包器
webpack 系列二:所有配置項
webpack 系列三:優化 90% 打包速度
webpack 系列四:優化包體積
webapck 系列五:優化首屏加載時間與頁面流暢度
webapck 系列六:構建包分析
webapck 系列七:詳細配置
webapck 系列八:手寫一個 webapck 插件(模擬 HtmlWebpackPlugin 的實現)
webapck 系列九:webapck4 核心源碼解讀
webapck 系列十:webapck5 展望
所有的內容之后會陸續放出,如果你有任何想要了解的內容或者有任何疑問,可以公眾號后臺留言提問。
作為一個前端開發人員,我們花費大量的時間去處理 webpack、gulp 等打包工具,將高級 JavaScript 項目打包成更復雜、更難以解讀的文件包,運行在瀏覽器中,那么理解 JavaScript 打包機制就很必要,它幫助你更好的調試項目、更快的定位問題產生的問題,并且幫助你更好的理解、使用 webpack 等打包工具。
在這章你將會深入理解 JavaScript 打包器是什么,它的打包機制是什么?解決了什么問題?如果你理解了這些,接下來的 webpack 優化就會很簡單。
一、什么是模塊
一個模塊可以有很多定義,但我認為:模塊是一組與特定功能相關的代碼。它封裝了實現細節,公開了一個公共API,并與其他模塊結合以構建更大的應用程序。
所謂模塊化,就是為了實現更高級別的抽象,它將一類或多種實現封裝到一個模塊中,我們不必考慮模塊內是怎樣的依賴關系,僅僅調用它暴露出來的 API 即可。
例如在一個項目中:
<html>??<script?src="/src/man.js">script>
??<script?src="/src/person.js">script>
html>
其中?person.js?中依賴?man.js?,在引用時如果你把它們的引用順序顛倒就會報錯。在大型項目中,這種依賴關系就顯得尤其重要,而且極難維護,除此之外,它還有以下問題:
一切都加載到全局上下文中,導致名稱沖突和覆蓋
涉及開發人員的大量手動工作,以找出依賴關系和包含順序
所以,模塊就尤其重要。
由于前后端 JavaScript 分別擱置在 HTTP 的兩端,它們扮演的角色不同,側重點也不一樣。?瀏覽器端的 JavaScript 需要經歷從一個服務器端分發到多個客戶端執行,而服務器端 JS 則是相同的代碼需要多次執行。前者的瓶頸在于寬帶,后者的瓶頸則在于 CPU 等內存資源。前者需要通過網絡加載代碼,后者則需要從磁盤中加載,?兩者的加載速度也不是在一個數量級上的。所以前后端的模塊定義不是一致的,其中服務器端的模塊定義為:
CJS(CommonJS):旨在用于服務器端 JavaScript 的同步定義,Node 的模塊系統實際上基于 CJS;
但 CommonJS 是以同步方式導入,因為用于服務端,文件都在本地,同步導入即使卡住主線程影響也不大,但在瀏覽器端,如果在 UI 加載的過程中需要花費很多時間來等待腳本加載完成,這會造成用戶體驗的很大問題。鑒于網絡的原因, CommonJS 為后端 JavaScript 制定的規范并不完全適合與前端的應用場景,下面來介紹 JavaScript 前端的規范。
AMD(異步模塊定義):被定義為用于瀏覽器中模塊的異步模型,RequireJS 是 AMD 最受歡迎的實現;
UMD(通用模塊定義):它本質上一段 JavaScript 代碼,放置在庫的頂部,可讓任何加載程序、任何環境加載它們;
ES2015(ES6):定義了異步導入和導出模塊的語義,會編譯成?require/exports?來執行的,這也是我們現今最常用的模塊定義;
二、什么是打包器
所謂打包器,就是前端開發人員用來將 JavaScript 模塊打包到一個可以在瀏覽器中運行的優化的 JavaScript 文件的工具,例如 webapck、rollup、gulp 等。
舉個例子,你在一個 html 文件中引入多個 JavaScript 文件:
<html>??<script?src="/src/entry.js">script>
??<script?src="/src/message.js">script>
??<script?src="/src/hello.js">script>
??<script?src="/src/name.js">script>
html>
當瀏覽器打開該網頁時,每個 js 文件都需要一個單獨的 http 請求,即 4 個往返請求,才能正確的啟動你的項目。
我們知道瀏覽器加載模塊很慢,即使是 HTTP/2 支持有效的加載許多小文件,但其性能都不如加載一個更加有效(即使不做任何優化)。
因此,最好將所有 4 個文件合并為1個:
<html>??<script?src="/dist/bundle.js">script>
html>
這樣只需要一次 http 請求即可。
如何打包到一個文件喃?它通常有一個入口文件,從入口文件開始,獲取所有的依賴項,并打包到一個文件?bundle.js?中。例如上例,我們可以以?/src/entry.js?作為入口文件,進行合并其余的 3 個 JavaScript 文件。
當然合并不能是簡單的將 4 個文件所有內容放入一個?bundle.js?中。我們先思考一下,它具體該怎么實現喃?
1. 解析入口文件,獲取所有的依賴項
首先我們唯一確定的是入口文件的地址,通過入口文件的地址可以
獲取其文件內容
獲取其依賴模塊的相對地址
由于依賴模塊的引入是通過相對路徑(import './message.js'),所以,我們需要保存入口文件的路徑,結合依賴模塊的相對地址,就可以確定依賴模塊絕對地址,讀取它的內容。
如何在依賴關系中去表示一個模塊,以方便在依賴圖中引用
所以我們可以模塊表示為:
code: 文件解析內容,注意解析后代碼能夠在當前以及舊瀏覽器或環境中運行;
dependencies: 依賴數組,為所有依賴模塊路徑(相對)路徑;
filename: 文件絕對路徑,當?import?依賴模塊為相對路徑,結合當前絕對路徑,獲取依賴模塊路徑;
其中 filename(絕對路徑) 可以作為每個模塊的唯一標識符,通過 key: value 形式,直接獲取文件的內容一依賴模塊:
//?模塊'src/entry':?{
??code:?'',?//?文件解析后內容
??dependencies:?["./message.js"],?//?依賴項
}
2. 遞歸解析所有的依賴項,生成一個依賴關系圖
我們已經確定了模塊的表示,那怎么才能將這所有的模塊關聯起來,生成一個依賴關系圖,通過這個依賴關系可以直接獲取所有模塊的依賴模塊、依賴模塊的代碼、依賴模塊的來源、依賴模塊的依賴模塊。
如何去維護依賴文件間的關系
現在對于每一個模塊,可以唯一表示的就是?filename?,而我們在由入口文件遞歸解析時,我們可以獲取到每個文件的依賴數組?dependencies?,也就是每個依賴項的相對路徑,所以我們需要定義一個:
//?關聯關系let?mapping?=?{}
用來在運行代碼時,由?import?相對路徑映射到?import?絕對路徑。
所以我們模塊可以定義為[filename: {}]:
//?模塊'src/entry':?{
??code:?'',?//?文件解析后內容
??dependencies:?["./message.js"],?//?依賴項
??mapping:{
????"./message.js":?"src/message.js"???????
??}
}
則依賴關系圖為:
//?graph?依賴關系圖let?graph?=?{
??//?entry?模塊
??"src/entry.js":?{
????code:?'',
????dependencies:?["./src/message.js"],
????mapping:{
??????"./message.js":?"src/message.js"???????
????}
??},
??//?message?模塊
??"src/message.js":?{
????code:?'',
????dependencies:?[],
????mapping:{},
??}
}
當項目運行時,通過入口文件成功獲取入口文件代碼內容,運行其代碼,當遇到?import?依賴模塊時,通過?mapping?映射其為絕對路徑,就可以成功讀取模塊內容。
并且每個模塊的絕對路徑 filename 是唯一的,當我們將模塊接入到依賴圖?graph?時,僅僅需要判斷?graph[filename]?是否存在,如果存在就不需要二次加入,剔除掉了模塊的重復打包。
3. 使用依賴圖,返回一個可以在瀏覽器運行的 JavaScript 文件
現今,可立即執行的代碼形式,最流行的就是 IIFE(立即執行函數),它同時能夠解決全局變量污染的問題。
IIFE
所謂 IIFE,就是在聲明市被直接調用的匿名函數,由于 JavaScript 變量的作用域僅限于函數內部,所以你不必考慮它會污染全局變量。
(function(man){??function?log(name)?{
????console.log(`hello?${name}`);
??}
??log(man.name)
})({name:?'bottle'});
//?hello?bottle
4. 輸出到 dist/bundle.js
fs.writeFile?寫入?dist/bundle.js?即可。
至此,打包流程與實現方案已確定,接下來就實踐一遍吧!
三、創建一個minipack項目
新建一個 minipack 文件夾,并?npm init?,創建以下文件:
-?src-?-?entry.js?//?入口?js
-?-?message.js?//?依賴項
-?-?hello.js?//?依賴項
-?-?name.js?//?依賴項
-?index.js?//?打包?js
-?minipack.config.js?//?minipack?打包配置文件
-?package.json?
-?.gitignore
其中?entry.js?:
import?message?from?'./message.js'import?{name}?from?'./name.js'
message()
console.log('----name-----:?',?name)
message.js?:
import?{hello}?from?'./hello.js'import?{name}?from?'./name.js'
export?default?function?message()?{
??console.log(`${hello}?${name}!`)
}
hello.js?:
export?const?hello?=?'hello'name.js?:
export?const?name?=?'bottle'minipack.config.js?:
const?path?=?require('path')module.exports?=?{
????entry:?'src/entry.js',
????output:?{
????????filename:?"bundle.js",
????????path:?path.resolve(__dirname,?'./dist'),
????}
}
并安裝文件
npm?install?@babel/core?@babel/parser?@babel/preset-env?@babel/traverse?--save-dev至此,整個項目創建完成。接下來就是打包了:
解析入口文件,遍歷所有依賴項
遞歸解析所有的依賴項,生成一個依賴關系圖
使用依賴圖,返回一個可以在瀏覽器運行的 JavaScript 文件
輸出到 ?/dist/bundle.js
四、解析入口文件,遍歷所有依賴項
1. @babel/parser 解析入口文件,獲取 AST
在 ./index.js 文件中,我們創建一個打包器,首先解析入口文件,我們使用?@babel/parser解析器進行解析:
步驟一:讀取入口文件內容
//?獲取配置文件const?config?=?require('./minipack.config');
//?入口
const?entry?=?config.entry;
const?content?=?fs.readFileSync(entry,?'utf-8');
步驟二:使用?@babel/parser(JavaScript解析器)解析代碼,生成 ast(抽象語法樹)
const?babelParser?=?require('@babel/parser')const?ast?=?babelParser.parse(content,?{
??sourceType:?"module"
})
其中,sourceType?指示代碼應解析的模式。可以是"script",?"module"或?"unambiguous"?之一,其中 ?"unambiguous"?是讓?@babel/parser?去猜測,如果使用 ES6?import?或?export?的話就是?"module"?,否則為?"script"?。這里使用 ES6?import?或?export?,所以就是?"module"?。
由于 ast 樹較復雜,所以這里我們可以通過 https://astexplorer.net/ 查看:
我們已經獲取了入口文件所有的 ast,接下來我們要做什么喃?
解析 ast,解析入口文件內容(可在當前和舊瀏覽器或環境中向后兼容的 JavaScript 版本)
獲取它所有的依賴模塊?dependencies
2. 獲取入口文件內容
我們已經知道了入口文件的 ast,可以通過?@babel/core?的?transformFromAst?方法,來解析入口文件內容:
const?{transformFromAst}?=?require('@babel/core');const?{code}?=?transformFromAst(ast,?null,?{
??presets:?['@babel/preset-env'],
})
3. 獲取它所有的依賴模塊
就需要通過 ast 獲取所有的依賴模塊,也就是我們需要獲取 ast 中所有的?node.source.value?,也就是?import?模塊的相對路徑,通過這個相對路徑可以尋找到依賴模塊。
步驟一:定義一個依賴數組,用來存放 ast 中解析出的所有依賴
const?dependencies?=?[]步驟二:使用?@babel/traverse?,它和 babel 解析器配合使用,可以用來遍歷及更新每一個子節點
traverse?函數是一個遍歷?AST?的方法,由?babel-traverse?提供,他的遍歷模式是經典的?visitor?模式 ,visitor?模式就是定義一系列的?visitor?,當碰到?AST?的?type === visitor?名字時,就會進入這個?visitor?的函數。類型為?ImportDeclaration?的 AST 節點,其實就是我們的?import xxx from xxxx,最后將地址?push?到?dependencies?中.
const?traverse?=?require('@babel/traverse').defaulttraverse(ast,?{
??//?遍歷所有的?import?模塊,并將相對路徑放入?dependencies
??ImportDeclaration:?({node})?=>?{
????dependencies.push(node.source.value)
??}
})
3. 有效返回
{??dependencies,
??code,
}
完整代碼:
/**?*?解析文件內容及其依賴,
?*?期望返回:
?*??????dependencies:?文件依賴模塊
?*??????code:?文件解析內容?
?*?@param?{string}?filename?文件路徑
?*/
function?createAsset(filename)?{
??//?讀取文件內容
??const?content?=?fs.readFileSync(filename,?'utf-8')
??//?使用?@babel/parser(JavaScript解析器)解析代碼,生成?ast(抽象語法樹)
??const?ast?=?babelParser.parse(content,?{
????sourceType:?"module"
??})
??//?從?ast?中獲取所有依賴模塊(import),并放入?dependencies?中
??const?dependencies?=?[]
??traverse(ast,?{
????//?遍歷所有的?import?模塊,并將相對路徑放入?dependencies
????ImportDeclaration:?({
??????node
????})?=>?{
??????dependencies.push(node.source.value)
????}
??})
??//?獲取文件內容
??const?{
????code
??}?=?transformFromAst(ast,?null,?{
????presets:?['@babel/preset-env'],
??})
??//?返回結果
??return?{
????dependencies,
????code,
??}
}
五、遞歸解析所有的依賴項,生成一個依賴關系圖
步驟一:獲取入口文件:
const?mainAssert?=?createAsset(entry)步驟二:創建依賴關系圖:
由于每個模塊都是 key: value 形式,所以定義依賴圖為:
//?entry:?入口文件絕對地址const?graph?=?{
??[entry]:?mainAssert
}
步驟三:遞歸搜索所有的依賴模塊,加入到依賴關系圖中:
定義一個遞歸搜索函數:
/**?*?遞歸遍歷,獲取所有的依賴
?*?@param?{*}?assert?入口文件
*/
function?recursionDep(filename,?assert)?{
??//?跟蹤所有依賴文件(模塊唯一標識符)
??assert.mapping?=?{}
??//?由于所有依賴模塊的?import?路徑為相對路徑,所以獲取當前絕對路徑
??const?dirname?=?path.dirname(filename)
??assert.dependencies.forEach(relativePath?=>?{
????//?獲取絕對路徑,以便于?createAsset?讀取文件
????const?absolutePath?=?path.join(dirname,?relativePath)
????//?與當前?assert?關聯
????assert.mapping[relativePath]?=?absolutePath
????//?依賴文件沒有加入到依賴圖中,才讓其加入,避免模塊重復打包
????if?(!queue[absolutePath])?{
??????//?獲取依賴模塊內容
??????const?child?=?createAsset(absolutePath)
??????//?將依賴放入?queue,以便于繼續調用?recursionDep?解析依賴資源的依賴,
??????//?直到所有依賴解析完成,這就構成了一個從入口文件開始的依賴圖
??????queue[absolutePath]?=?child
??????if(child.dependencies.length?>?0)?{
????????//?繼續遞歸
????????recursionDep(absolutePath,?child)
??????}
????}
??})
}
從入口文件開始遞歸:
//?遍歷?queue,獲取每一個?asset?及其所以依賴模塊并將其加入到隊列中,直至所有依賴模塊遍歷完成for?(let?filename?in?queue)?{
??let?assert?=?queue[filename]
??recursionDep(filename,?assert)
}
六、使用依賴圖,返回一個可以在瀏覽器運行的 JavaScript 文件
步驟一:創建一個了立即執行函數,用于在瀏覽器上直接運行
const?result?=?`??(function()?{
??})()
`
步驟二:將依賴關系圖作為參數傳遞給立即執行函數
定義傳遞參數 modules:
let?modules?=?''遍歷?graph,將每個?mod?以?key: value,?的方式加入到?modules,
注意:由于依賴關系圖要傳入以上立即執行函數中,然后寫入到?dist/bundle.js?運行,所以,code?需要放在?function(require, module, exports){${mod.code}}?中,避免污染全局變量或其它模塊
for?(let?filename?in?graph)?{??let?mod?=?graph[filename]
??modules?+=?`'${filename}':?[
????function(require,?module,?exports)?{${mod.code}
????},${JSON.stringify(mod.mapping)},
??],`
}
步驟三:將參數傳入立即執行函數,并立即執行入口文件:
首先實現一個 require 函數,require('${entry}')?執行入口文件,entry?為入口文件絕對路徑,也為模塊唯一標識符
const?result?=?`??(function(modules)?{
????require('${entry}')
??})({${modules}})
`
注意:modules?是一組?key: value,,所以我們將它放入?{}?中
步驟四:重寫瀏覽器?require?方法,當代碼運行?require('./message.js')?轉換成?require(src/message.js)
const?result?=?`??(function(modules)?{
????function?require(moduleId)?{
??????const?[fn,?mapping]?=?modules[moduleId]
??????function?localRequire(name)?{
????????return?require(mapping[name])
??????}
??????const?module?=?{exports:?{}}
??????fn(localRequire,?module,?module.exports)
??????return?module.exports
????}
????require('${entry}')
??})({${modules}})
`
注意:
moduleId?為傳入的?filename?,為模塊的唯一標識符
通過解構?const [fn, mapping] = modules[id]?來獲得我們的函數包裝(function(require, module, exports) {${mod.code}})和?mappings?對象
由于一般情況下?require?都是?require?相對路徑,而不是絕對路徑,所以重寫?fn的?require?方法,將?require?相對路徑轉換成?require?絕對路徑,即?localRequire?函數
將?module.exports?傳入到?fn?中,將依賴模塊內容需要輸出給其它模塊使用時,當?require?某一依賴模塊時,就可以直接通過?module.exports?將結果返回
七、輸出到 dist/bundle.js
//?打包const?result?=?bundle(graph)
//?寫入?./dist/bundle.js
fs.writeFile(`${output.path}/${output.filename}`,?result,?(err)?=>?{
??if?(err)?throw?err;
??console.log('文件已被保存');
})
八、總結及源碼
本來想簡單的寫寫,結果修修改改又那么多??♀???♀???♀?,但總要吃透才好。
源碼地址:https://github.com/sisterAn/minipack
超強干貨來襲 云風專訪:近40年碼齡,通宵達旦的技術人生總結
以上是生活随笔為你收集整理的未能加载文件或程序集或它的某一个依赖项_手写一个miniwebpack的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: windows7系统损坏修复_修复损坏的
- 下一篇: dataframe两个表合并_R语言读取