<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          手把手教你寫一個迷你 Webpack

          共 7222字,需瀏覽 15分鐘

           ·

          2021-11-07 11:03

          一、前言

          最近正好在學(xué)習(xí) Webpack,覺得 Webpack 這種通過構(gòu)建模塊依賴圖來打包項目文件的思想很有意思,于是參考了網(wǎng)上的一些文章實現(xiàn)了一個簡陋版本的 mini-webpack,通過入口文件將依賴的模塊打包在一起,生成一份最終運行的代碼。想了解 Webpack 的構(gòu)建原理還需要補充一些相關(guān)的背景知識,下面一起來看看。

          二、背景知識

          1. 抽象語法樹(AST)

          什么是抽象語法樹?

          平時我們編寫程序的時候,會經(jīng)常在代碼中根據(jù)需要 import 一些模塊,那 Webpack 在構(gòu)建項目、分析依賴的時候是如何得知我們代碼中是否有 import 文件,import 的是什么文件的呢?Webpack 并不是人,無法像我們一樣一看到代碼語句就明白其含義,所以我們需要將編寫的代碼轉(zhuǎn)換成 Webpack 認(rèn)識的格式讓他它進(jìn)行處理,這份轉(zhuǎn)換后生成的東西就是抽象語法樹。下面這張圖能很好地說明什么是抽象語法樹:

          可以看到,抽象語法樹是源代碼的抽象語法結(jié)構(gòu)樹狀表現(xiàn)形式,我們每條編寫的代碼語句都可以被解析成一個個的節(jié)點,將一整個代碼文件解析后就會生成一顆節(jié)點樹,作為程序代碼的抽象表示。通過抽象語法樹,我們可以做以下事情:

          • IDE 的錯誤提示、代碼格式化、代碼高亮、代碼自動補全等

          • JSLint、JSHint、ESLint 對代碼錯誤或風(fēng)格的檢查等

          • Webpack、rollup 進(jìn)行代碼打包等

          • Babel 轉(zhuǎn)換 ES6 到 ES5 語法

          • 注入代碼統(tǒng)計單元測試覆蓋率

          想看看你的代碼會生成怎樣的抽象語法樹嗎?這里有一個工具?AST Explorer?能夠在線預(yù)覽你的代碼生成的抽象語法樹,感興趣的不妨上去試一試。

          2. Babel

          Babel 是一個工具鏈,主要用于將采用 ECMAScript 2015+ 語法編寫的代碼轉(zhuǎn)換為向后兼容的 JavaScript 語法,以便能夠運行在當(dāng)前和舊版本的瀏覽器或其他環(huán)境中。通過 Babel 我們可以做以下事情:

          • 語法轉(zhuǎn)換

          • 通過 Polyfill 方式在目標(biāo)環(huán)境中添加缺失的特性(通過第三方 Polyfill 模塊,例如?core-js,實現(xiàn))

          • 源碼轉(zhuǎn)換 (codemods)

          一般來說項目使用 Webpack 來打包文件都會配置 babel-loader 將 ES6 的代碼轉(zhuǎn)換成 ES5 的格式以兼容瀏覽器,這個過程就需要將我們的代碼轉(zhuǎn)換成抽象語法樹后再進(jìn)行轉(zhuǎn)換處理,轉(zhuǎn)換完成后再將抽象語法樹還原成代碼。

          // Babel 輸入:ES2015 箭頭函數(shù)
          [1,?2,?3].map((n)?=>?n?+?1);

          // Babel 輸出:ES5 語法實現(xiàn)的同等功能
          [1,?2,?3].map(function(n)?{
          ??return?n?+?1;
          });

          3. Webpack 打包原理

          Webpack 的構(gòu)建過程一般會分為以下幾步:

          • 讀取 Webpack 基礎(chǔ)配置
          ????//?讀取 webpack.config.js 配置文件:
          ????const?path?=?require"path"
          ????module.exports?=?{
          ????????entry:"./src/index.js"
          ????????mode:"development"
          ????????output:{
          ??????????path:path.resolve(__dirname,"./dist"),
          ??????????filename:"bundle.js"
          ????????}
          ????}
          • 入口文件分析

            • 分析依賴模塊

            • 分析內(nèi)容

            • 編譯內(nèi)容

          • 依賴模塊分析

            • 分析依賴模塊是否有其他模塊

            • 分析內(nèi)容

            • 編譯內(nèi)容

          • 生成打包文件

          ????//?基礎(chǔ)結(jié)構(gòu)為一個IIFE自執(zhí)行函數(shù)
          ????//?接收一個對象參數(shù),key?為入口文件的目錄,value為一個執(zhí)行入口文件里面代碼的函數(shù)
          ????(function?(modules)?{
          ??????//?installedModules?用來存放緩存
          ??????const?installedModules?=?{};
          ??????//?__webpack_require__用來轉(zhuǎn)化入口文件里面的代碼
          ??????function?__webpack_require__(moduleIid)?{?...?}
          ??????// IIFE將 modules 中的 key 傳遞給?__webpack_require__?函數(shù)并返回。
          ??????return?__webpack_require__(__webpack_require__.s?=?'./src/index.js');
          ????}({
          ??????'./src/index.js':?(function?(module,?exports)?{
          ????????eval('console.log(\'test?webpack?entry\')');
          ??????}),
          ????}));


          三、具體實現(xiàn)

          1. 安裝相關(guān)依賴

          我們需要用到以下幾個包:

          • @babel/parser:用于將輸入代碼解析成抽象語法樹(AST)

          • @babel/traverse:用于對輸入的抽象語法樹(AST)進(jìn)行遍歷

          • @babel/core:babel 的核心模塊,進(jìn)行代碼的轉(zhuǎn)換

          • @babel/preset-env:可根據(jù)配置的目標(biāo)瀏覽器或者運行環(huán)境來自動將 ES2015 + 的代碼轉(zhuǎn)換為 es5

          使用 npm 命令安裝一下:

          npm?install?@babel/parser?@babel/traverse?@babel/core?@babel/preset-env?-D

          2. 讀取基本配置

          要讀取 Webpack 的基本配置,首先我們得有一個全局的配置文件:

          //?mini-webpack.config.js
          const?path?=?require('path');

          module.exports?={
          ????entry:?"./src/index.js",
          ????mode:?"development",
          ????output:?{
          ??????path:?path.resolve(__dirname,"./dist"),
          ??????filename:?"bundle.js"
          ????}
          }

          然后我們新建一個類,用于實現(xiàn)分析編譯等函數(shù),并在構(gòu)造函數(shù)中初始化配置信息:

          const?options?=?require('./mini-webpack.config');

          class?MiniWebpack{
          ????constructor(options){
          ????????this.options?=?options;
          ????}
          ????//?...
          }

          3. 代碼轉(zhuǎn)換,獲取模塊信息

          我們使用?fs?讀取文件內(nèi)容,使用?parser?將模塊代碼轉(zhuǎn)換成抽象語法樹,再使用?traverse?遍歷抽象語法樹,針對其中的?ImportDeclaration?節(jié)點保存模塊的依賴信息,最終使用?babel.transformFromAst?方法將抽象語法樹還原成 ES5 風(fēng)格的代碼。

          parse?=?filename?=>?{
          ????//?讀取文件
          ????const?fileBuffer?=?fs.readFileSync(filename,?'utf-8');
          ????//?轉(zhuǎn)換成抽象語法樹
          ????const?ast?=?parser.parse(fileBuffer,?{?sourceType:?'module'?});

          ????const?dependencies?=?{};
          ????//?遍歷抽象語法樹
          ????traverse(ast,?{
          ????????//?處理ImportDeclaration節(jié)點
          ????????ImportDeclaration({node}){
          ????????????const?dirname?=?path.dirname(filename);
          ????????????const?newDirname?=?'./'?+?path.join(dirname,?node.source.value).replace('\\',?'/');
          ????????????dependencies[node.source.value]?=?newDirname;
          ????????}
          ????})
          ????//?將抽象語法樹轉(zhuǎn)換成代碼
          ????const?{?code?}?=?babel.transformFromAst(ast,?null,?{
          ????????presets:['@babel/preset-env']
          ????});
          ????
          ????return?{
          ????????filename,
          ????????dependencies,
          ????????code
          ????}
          }

          4. 分析依賴關(guān)系

          從入口文件開始,循環(huán)解析每個文件與其依賴文件的信息,最終生成以文件名為?key,以包含依賴關(guān)系與編譯后模塊代碼的對象為?value?的依賴圖譜對象并返回。

          analyse?=?entry?=>?{
          ????//?解析入口文件
          ????const?entryModule?=?this.parse(entry);
          ????const?graphArray?=?[entryModule];
          ????//?循環(huán)解析模塊,保存信息
          ????for(let?i=0;i????????const?{?dependencies?}?=?graphArray[i];
          ????????Object.keys(dependencies).forEach(filename?=>?{
          ????????????graphArray.push(this.parse(dependencies[filename]));
          ????????})
          ????}

          ????const?graph?=?{};
          ????//?生成依賴圖譜對象
          ????graphArray.forEach(({filename,?dependencies,?code})=>{
          ????????graph[filename]?=?{
          ????????????dependencies,
          ????????????code
          ????????};
          ????})

          ????return?graph;
          }

          5. 生成打包代碼

          生成依賴圖譜對象,作為參數(shù)傳入一個自執(zhí)行函數(shù)當(dāng)中??梢钥吹?,自執(zhí)行函數(shù)中有個 require 函數(shù),它的作用是通過調(diào)用 eval 執(zhí)行模塊代碼來獲取模塊內(nèi)部 export 出來的值。最終我們返回打包的代碼。

          generate?=?(graph,?entry)?=>?{
          ????return?`
          ????(function(graph){
          ????????function?require(filename){
          ????????????function?localRequire(relativePath){
          ????????????????return?require(graph[filename].dependencies[relativePath]);
          ????????????}
          ????????????const?exports?=?{};
          ????????????(function(require,?exports,?code){
          ????????????????eval(code);
          ????????????})(localRequire,?exports,?graph[filename].code)

          ????????????return?exports;
          ????????}
          ????????
          ????????require('${entry}');
          ????})(${graph})
          ????`

          }

          6. 輸出最終文件

          通過獲取 this.options 中的 output 信息,將打包代碼輸出到對應(yīng)文件中。

          fileOutput?=?(output,?code)?=>?{
          ????const?{?path:?dirPath,?filename?}?=?output;
          ????const?outputPath?=?path.join(dirPath,?filename);

          ????//?如果沒有文件夾的話,生成文件夾
          ????if(!fs.existsSync(dirPath)){
          ????????fs.mkdirSync(dirPath)
          ????}
          ????//?寫入文件中
          ????fs.writeFileSync(outputPath,?code,?'utf-8');
          }

          7. 模擬 run 函數(shù)

          我們將上面的流程集成到一個 run 函數(shù)中,通過調(diào)用該函數(shù)來將整個構(gòu)建打包流程跑通。

          run?=?()?=>?{
          ????const?{?entry,?output?}?=?this.options;
          ????const?graph?=?this.analyse(entry);
          ????// stringify依賴圖譜對象,防止在模板字符串中調(diào)用toString()返回[object Object]
          ????const?graphStr?=?JSON.stringify(graph);
          ????const?code?=?this.generate(graphStr,?entry);
          ????this.fileOutput(output,?code);
          }

          8.mini-webpack 大功告成

          通過上面的流程,我們的 mini-webpack 已經(jīng)完成了。我們將文件保存為 main.js,新建一個 MiniWebpack 對象并執(zhí)行它的 run 函數(shù):

          //?main.js
          const?options?=?require('./mini-webpack.config');

          class?MiniWebpack{
          ????constructor(options){
          ????????//?...
          ????}

          ????parse?=?filename?=>?{
          ????????//?...
          ????}

          ????analyse?=?entry?=>?{
          ????????//?...
          ????}

          ????generate?=?(graph,?entry)?=>?{
          ????????//?...
          ????}

          ????fileOutput?=?(output,?code)?=>?{
          ????????//?...
          ????}

          ????run?=?()?=>?{
          ????????//?...
          ????}
          }

          const?miniWebpack?=?new?MiniWebpack(options);
          miniWebpack.run();

          四、實際演示

          我們來實際試驗一下,看看這個 mini-webpack 能不能正常運行。

          1. 新建測試文件

          首先在根目錄下創(chuàng)建?src?文件夾,新建?a.js、b.jsindex.js?三個文件

          三個文件內(nèi)容如下:

          • a.js
          export?default?1;
          • b.js
          export?default?function(){
          ????console.log('I?am?b');
          }
          • index.js
          import?a?from?'./a.js';
          import?b?from?'./b.js';

          console.log(a);
          console.log(b);

          2. 填入配置文件

          配置好入口文件、輸出文件等信息:

          const?path?=?require('path');

          module.exports?={
          ????entry:?"./src/index.js",
          ????mode:?"development",
          ????output:?{
          ??????path:?path.resolve(__dirname,"./dist"),
          ??????filename:?"bundle.js"
          ????}
          }

          3. 完善 package.json

          我們在 package.json 的?scripts?中新增一個?build?命令,內(nèi)容為執(zhí)行 main.js:

          {
          ??"name":?"mini-webpack",
          ??"version":?"1.0.0",
          ??"description":?"",
          ??"main":?"index.js",
          ??"scripts":?{
          ????"test":?"echo?\"Error:?no?test?specified\"?&&?exit?1",
          ????"build":?"node?main.js"
          ??},
          ??"author":?"",
          ??"license":?"ISC",
          ??"devDependencies":?{
          ????"@babel/core":?"^7.15.4",
          ????"@babel/parser":?"^7.15.4",
          ????"@babel/preset-env":?"^7.15.4",
          ????"@babel/traverse":?"^7.15.4"
          ??}
          }

          4. 效果演示

          我們執(zhí)行?npm run build?命令,可以看到在根目錄下生成了 dist 文件夾,里面有個 bundle.js 文件,內(nèi)容正是我們輸出的打包代碼:

          執(zhí)行下?bundle.js?文件,看看會有什么輸出:

          可以看到,bundle.js 的輸出正是 index.js 文件中兩個 console.log 輸出的值,說明我們的代碼轉(zhuǎn)換沒有問題,到這里試驗算是成功了。

          五、項目 Git 地址

          項目代碼在此:mini-webpack

          六、參考文章

          1. 實現(xiàn)一個簡單的 Webpack

          2. Babel 中文文檔

          3. 【你應(yīng)該了解的】抽象語法樹 AST

          4. webpack 構(gòu)建原理和實現(xiàn)簡單 webpack



          瀏覽 48
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  婷婷亚洲五月天 | 亚洲毛多水多 | 91视频最新网址 | 欧美理论三级 | 自拍九区|