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

          結(jié)合代碼實踐,全面學(xué)習(xí)前端工程化

          共 11425字,需瀏覽 23分鐘

           ·

          2022-01-03 13:10

          點擊上方關(guān)注?前端技術(shù)江湖,一起學(xué)習(xí),天天進步


          前言

          前端工程化,簡而言之就是軟件工程 + 前端,以自動化的形式呈現(xiàn)。就個人理解而言:前端工程化,從開發(fā)階段到代碼發(fā)布生產(chǎn)環(huán)境,包含了以下幾個內(nèi)容:

          • 開發(fā)
          • 構(gòu)建
          • 測試
          • 部署
          • 性能
          • 規(guī)范

          下面我們根據(jù)上述幾個內(nèi)容,選擇有代表性的幾個方面進行深入學(xué)習(xí)前端工程化。

          回顧:【青訓(xùn)營】- 了解前端工程化[2]

          腳手架

          腳手架是什么?(What)

          現(xiàn)在流行的前端腳手架基本上都是基于NodeJs編寫,比如我們常用的Vue-CLI,比較火的create-react-app,還有Dva-CLI等。

          腳手架存在的意義?(Why)

          隨著前端工程化的概念越來越深入人心,腳手架的出現(xiàn)就是為減少重復(fù)性工作而引入的命令行工具,擺脫ctrl + c,?ctrl + v,此話怎講? 現(xiàn)在新建一個前端項目,已經(jīng)不是在html頭部引入css,尾部引入js那么簡單的事了,css都是采用Sass或則Less編寫,在js中引入,然后動態(tài)構(gòu)建注入到html中;除了學(xué)習(xí)基本的js,css語法和熱門框架,還需要學(xué)習(xí)構(gòu)建工具webpack,babel這些怎么配置,怎么起前端服務(wù),怎么熱更新;為了在編寫過程中讓編輯器幫我們查錯以及更加規(guī)范,我們還需要引入ESlint;甚至,有些項目還需要引入單元測試(Jest)。對于一個更入門的人來說,這無疑會讓人望而卻步。而前端腳手架的出現(xiàn),就讓事情簡單化,一鍵命令,新建一個工程,再執(zhí)行兩個npm命令,跑起一個項目。在入門時,無需關(guān)注配置什么的,只需要開心的寫代碼就好。

          如何實現(xiàn)一個新建項目腳手架(基于koa)?(How)

          先梳理下實現(xiàn)思路

          我們實現(xiàn)腳手架的核心思想就是自動化思維,將重復(fù)性的ctrl + c,?ctrl + v創(chuàng)建項目,用程序來解決。解決步驟如下:

          1. 創(chuàng)建文件夾(項目名)
          2. 創(chuàng)建 index.js
          3. 創(chuàng)建 package.json
          4. 安裝依賴

          1. 創(chuàng)建文件夾

          創(chuàng)建文件夾前,需要先刪除清空:

          //?package.json
          {
          ...
          ????"scripts":?{
          ????????"test":?"rm?-rf?./haha?&&?node?--experimental-modules?index.js"
          ????}
          ...
          }
          復(fù)制代碼

          創(chuàng)建文件夾:我們通過引入?nodejs?的?fs?模塊,使用?mkdirSync?API來創(chuàng)建文件夾。

          //?index.js
          import?fs?from?'fs';

          function?getRootPath()?{
          ??return?"./haha";
          }

          //?生成文件夾
          fs.mkdirSync(getRootPath());
          復(fù)制代碼

          2. 創(chuàng)建 index.js

          創(chuàng)建 index.js:使用?nodejs?的fs?模塊的?writeFileSync?API 創(chuàng)建 index.js 文件:

          //?index.js
          fs.writeFileSync(getRootPath()?+?"/index.js",?createIndexTemplate(inputConfig));
          復(fù)制代碼

          接著我們來看看,動態(tài)模板如何生成?我們最理想的方式是通過配置來動態(tài)生成文件模板,那么具體來看看?createIndexTemplate?實現(xiàn)的邏輯吧。

          //?index.js
          import?fs?from?'fs';
          import?{?createIndexTemplate?}?from?"./indexTemplate.js";

          //?input
          //?process
          //?output
          const?inputConfig?=?{
          ??middleWare:?{
          ????router:?true,
          ????static:?true
          ??}
          }
          function?getRootPath()?{
          ??return?"./haha";
          }
          //?生成文件夾?
          fs.mkdirSync(getRootPath());
          //?生成?index.js?文件
          fs.writeFileSync(getRootPath()?+?"/index.js",?createIndexTemplate(inputConfig));
          復(fù)制代碼
          //?indexTemplate.js
          import?ejs?from?"ejs";
          import?fs?from?"fs";
          import?prettier?from?"prettier";//?格式化代碼
          //?問題驅(qū)動
          //?模板
          //?開發(fā)思想??-?小步驟的開發(fā)思想
          //?動態(tài)生成代碼模板
          export?function?createIndexTemplate(config)?{
          ??//?讀取模板
          ??const?template?=?fs.readFileSync("./template/index.ejs",?"utf-8");
          ??
          ??//?ejs渲染
          ??const?code?=?ejs.render(template,?{
          ????router:?config.middleware.router,
          ????static:?config.middleware.static,
          ????port:?config.port,
          ??});
          ??
          ??//?返回模板
          ??return?prettier.format(code,?{
          ????parser:?"babel",
          ??});
          }
          復(fù)制代碼
          //?template/index.ejs
          const?Koa?=?require("koa");
          <%?if?(router)?{?%>
          ??const?Router?=?require("koa-router");
          <%?}?%>


          <%?if?(static)?{?%>
          const?serve?=?require("koa-static");
          <%?}?%>

          const?app?=?new?Koa();

          <%?if?(router)?{?%>
          const?router?=?new?Router();
          router.get("/",?(ctx)?=>?{
          ??ctx.body?=?"hello?koa-setup-heihei";
          });
          app.use(router.routes());
          <%?}?%>

          <%?if?(static)?{?%>
          app.use(serve(__dirname?+?"/static"));
          <%?}?%>

          app.listen(<%=?port?%>,?()?=>?{
          ??console.log("open?server?localhost:<%=?port?%>");
          });
          復(fù)制代碼

          3. 創(chuàng)建 package.json

          創(chuàng)建 package.json 文件,實質(zhì)是和創(chuàng)建 index.js 類似,都是采用動態(tài)生成模板的思路來實現(xiàn),我們來看下核心方法?createPackageJsonTemplate?的實現(xiàn)代碼:

          //?packageJsonTemplate.js
          function?createPackageJsonTemplate(config)?{
          ??const?template?=?fs.readFileSync("./template/package.ejs",?"utf-8");

          ??const?code?=?ejs.render(template,?{
          ????packageName:?config.packageName,
          ????router:?config.middleware.router,
          ????static:?config.middleware.static,
          ??});

          ??return?prettier.format(code,?{
          ????parser:?"json",
          ??});
          }
          復(fù)制代碼
          //?template/package.ejs
          {
          ??"name":?"<%=?packageName?%>",
          ??"version":?"1.0.0",
          ??"description":?"",
          ??"main":?"index.js",
          ??"scripts":?{
          ????"test":?"echo?\"Error:?no?test?specified\"?&&?exit?1"
          ??},
          ??"keywords":?[],
          ??"author":?"",
          ??"license":?"ISC",
          ??"dependencies":?{
          ????"koa":?"^2.13.1"
          <%?if?(router)?{?%>
          ????,"koa-router":?"^10.1.1"
          <%?}?%>

          <%?if?(static)?{?%>
          ????,"koa-static":?"^5.0.0"
          ??}
          <%?}?%>
          }
          復(fù)制代碼

          4. 安裝依賴

          要自動安裝依賴,我們可以使用?nodejs?的?execa?庫執(zhí)行?yarn?安裝命令:

          execa("yarn",?{
          ??cwd:?getRootPath(),
          ??stdio:?[2,?2,?2],
          });
          復(fù)制代碼

          至此,我們已經(jīng)用?nodejs?實現(xiàn)了新建項目的腳手架了。最后我們可以重新梳理下可優(yōu)化點將其升級完善。比如將程序配置升級成?GUI?用戶配置(用戶通過手動選擇或是輸入來傳入配置參數(shù),例如項目名)。

          編譯構(gòu)建

          編譯構(gòu)建是什么?

          構(gòu)建,或者叫作編譯,是前端工程化體系中功能最繁瑣、最復(fù)雜的模塊,承擔(dān)著從源代碼轉(zhuǎn)化為宿主瀏覽器可執(zhí)行的代碼,其核心是資源的管理。前端的產(chǎn)出資源包括JS、CSS、HTML等,分別對應(yīng)的源代碼則是:

          • 領(lǐng)先于瀏覽器實現(xiàn)的ECMAScript規(guī)范編寫的JS代碼(ES6/7/8...)。
          • LESS/SASS預(yù)編譯語法編寫的CSS代碼。
          • Jade/EJS/Mustache等模板語法編寫的HTML代碼。

          以上源代碼是無法在瀏覽器環(huán)境下運行的,構(gòu)建工作的核心便是將其轉(zhuǎn)化為宿主可執(zhí)行代碼,分別對應(yīng):

          • ECMAScript規(guī)范的轉(zhuǎn)譯。
          • CSS預(yù)編譯語法轉(zhuǎn)譯。
          • HTML模板渲染。

          那么下面我們就一起學(xué)習(xí)下如今3大主流構(gòu)建工具:Webpack、Rollup、Vite。

          Webpack

          image.png

          Webpack原理

          想要真正用好?Webpack?編譯構(gòu)建工具,我們需要先來了解下它的工作原理。Webpack?編譯項目的工作機制是,遞歸找出所有依賴模塊,轉(zhuǎn)換源碼為瀏覽器可執(zhí)行代碼,并構(gòu)建輸出bundle。具體工作流程步驟如下:

          1. 初始化參數(shù):取配置文件和shell腳本參數(shù)并合并
          2. 開始編譯:用上一步得到的參數(shù)初始化compiler對象,執(zhí)行run方法開始編譯
          3. 確定入口:根據(jù)配置中的entry,確定入口文件
          4. 編譯模塊:從入口文件出發(fā),遞歸遍歷找出所有依賴模塊的文件
          5. 完成模塊編譯:使用loader轉(zhuǎn)譯所有模塊,得到轉(zhuǎn)譯后的最終內(nèi)容和依賴關(guān)系
          6. 輸出資源:根據(jù)入口和模塊依賴關(guān)系,組裝成一個個chunk,加到輸出列表
          7. 輸出完成:根據(jù)配置中的output,確定輸出路徑和文件名,把文件內(nèi)容寫入輸出目錄(默認(rèn)是dist

          Webpack實踐

          1. 基礎(chǔ)配置

          entry

          入口配置,webpack 編譯構(gòu)建時能找到編譯的入口文件,進而構(gòu)建內(nèi)部依賴圖。

          output

          輸出配置,告訴 webpack 在哪里輸出它所創(chuàng)建的 bundle,以及如何命名這些文件。

          loader

          模塊轉(zhuǎn)換器,loader 可以處理瀏覽器無法直接運行的文件模塊,轉(zhuǎn)換為有效模塊。比如:css-loader和style-loader處理樣式;url-loader和file-loader處理圖片。

          plugin

          插件,解決 loader 無法實現(xiàn)的問題,在 webpack 整個構(gòu)建生命周期都可以擴展插件。比如:打包優(yōu)化,資源管理,注入環(huán)境變量等。

          下面是 webpack 基本配置的簡單示例:

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

          module.exports?=?{
          ??mode:?"development",
          ??entry:?"./src/index.js",
          ??output:?{
          ????filename:?"main.js",
          ????path:?path.resolve(__dirname,?"dist"),
          ??},
          ??devServer:?{
          ????static:?"./dist",
          ??},
          ??module:?{
          ????rules:?[
          ??????{
          ????????//?匹配什么樣子的文件
          ????????test:?/\.css$/i,
          ????????//?使用loader?,?從后到前執(zhí)行
          ????????use:?["style-loader",?"css-loader"],
          ??????}
          ????],
          ??},
          };
          復(fù)制代碼

          參考webpack官網(wǎng):webpack.docschina.org/concepts/[3]
          (注意:使用不同版本的 webpack 切換對應(yīng)版本的文檔哦)

          2. 性能優(yōu)化

          編譯速度優(yōu)化

          檢測編譯速度

          尋找檢測編譯速度的工具,比如?speed-measure-webpack-plugin插件[4]?,用該插件分析每個loader和plugin執(zhí)行耗時具體情況。

          優(yōu)化編譯速度該怎么做呢?

          1. 減少搜索依賴的時間
          • 配置 loader 匹配規(guī)則 test/include/exclue,縮小搜索范圍,即可減少搜索時間
          1. 減少解析轉(zhuǎn)換的時間
          • noParse配置,精準(zhǔn)過濾不用解析的模塊
          • loader性能消耗大的,開啟多進程
          1. 減少構(gòu)建輸出的時間
          • 壓縮代碼,開啟多進程
          1. 合理使用緩存策略
          • babel-loader開啟緩存
          • 中間模塊啟用緩存,比如使用 hard-source-webpack-plugin

          具體優(yōu)化措施可參考:webpack性能優(yōu)化的一段經(jīng)歷|項目復(fù)盤[5]

          體積優(yōu)化

          檢測包體積大小

          尋找檢測構(gòu)建后包體積大小的工具,比如?webpack-bundle-analyzer插件[6]?,用該插件分析打包后生成Bundle的每個模塊體積大小。

          優(yōu)化體積該怎么做呢?

          1. bundle去除第三方依賴
          2. 擦除無用代碼 Tree Shaking

          具體優(yōu)化措施參考:webpack性能優(yōu)化的一段經(jīng)歷|項目復(fù)盤[7]

          Rollup

          Rollup概述

          Rollup[8]?是一個 JavaScript 模塊打包器,可以將小塊代碼編譯成大塊復(fù)雜的代碼,例如 library 或應(yīng)用程序。并且可以對代碼模塊使用新的標(biāo)準(zhǔn)化格式,比如CommonJS?和?es module

          Rollup原理

          我們先來了解下?Rollup?原理,其主要工作機制是:

          1. 確定入口文件
          2. 使用?Acorn?讀取解析文件,獲取抽象語法樹 AST
          3. 分析代碼
          4. 生成代碼,輸出

          Rollup?相對?Webpack?而言,打包出來的包會更加輕量化,更適用于類庫打包,因為內(nèi)置了 Tree Shaking 機制,在分析代碼階段就知曉哪些文件引入并未調(diào)用,打包時就會自動擦除未使用的代碼。

          Acorn 是一個 JavaScript 語法解析器,它將 JavaScript 字符串解析成語法抽象樹 AST 如果想了解 AST 語法樹可以點下這個網(wǎng)址astexplorer.net/[9]

          Rollup實踐

          input

          入口文件路徑

          output

          輸出文件、輸出格式(amd/es6/iife/umd/cjs)、sourcemap啟用等。

          plugin

          各種插件使用的配置

          external

          提取外部依賴

          global

          配置全局變量

          下面是 Rollup 基礎(chǔ)配置的簡單示例:

          import?commonjs?from?"@rollup/plugin-commonjs";
          import?resolve?from?"@rollup/plugin-node-resolve";
          //?解析json
          import?json?from?'@rollup/plugin-json'
          //?壓縮代碼
          import?{?terser?}?from?'rollup-plugin-terser';
          export?default?{
          ??input:?"src/main.js",
          ??output:?[{
          ????file:?"dist/esmbundle.js",
          ????format:?"esm",
          ????plugins:?[terser()]
          ??},{
          ????file:?"dist/cjsbundle.js",
          ????format:?"cjs",
          ??}],
          ??//?commonjs?需要放到?transform?插件之前,
          ??//?但是又個例外,?是需要放到?babel?之后的
          ??plugins:?[json(),?resolve(),?commonjs()],
          ??external:?["vue"]
          };

          復(fù)制代碼

          Vite

          Vite概述

          Vite[10],相比 Webpack、Rollup 等工具,極大地改善了前端開發(fā)者的開發(fā)體驗,編譯速度極快。

          Vite原理

          為什么 Vite 開發(fā)編譯速度極快?我們就先來探究下它的原理吧。由上圖可見,Vite 原理是利用現(xiàn)代主流瀏覽器支持原生的 ESM 規(guī)范,配合 server 做攔截,把代碼編譯成瀏覽器支持的。

          Vite實踐體驗

          我們可以搭建一個Hello World版的Vite項目來感受下飛快的開發(fā)體驗:

          注意:Vite 需要?Node.js[11]?版本 >= 12.0.0。

          使用 NPM:

          $?npm?init?vite@latest
          復(fù)制代碼

          使用 Yarn:

          $?yarn?create?vite
          復(fù)制代碼

          上圖是Vite項目的編譯時間,363ms,開發(fā)秒級編譯的體驗,真的是棒棒噠!

          3種構(gòu)建工具綜合對比


          WebpackRollupVite
          編譯速度一般較快最快
          HMR熱更新支持需要額外引入插件支持
          Tree Shaking需要額外配置支持支持
          適用范圍項目打包類庫打包不考慮兼容性的項目

          測試

          當(dāng)我們前端項目越來越龐大時,開發(fā)迭代維護成本就會越來越高,數(shù)十個模塊相互調(diào)用錯綜復(fù)雜,為了提高代碼質(zhì)量和可維護性,就需要寫測試了。下面就給大家具體介紹下前端工程經(jīng)常做的3類測試。

          單元測試

          單元測試,是對最小可測試單元(一般為單個函數(shù)、類或組件)進行檢查和驗證。
          做單元測試的框架有很多,比如?Mocha[12]斷言庫Chai[13]、Sinon[14]Jest[15]等。我們可以先選擇 jest 來學(xué)習(xí),因為它集成了?Mocha,chai,jsdom,sinon?等功能。接下來,我們一起看看?jest?怎么寫單元測試吧?

          1. 根據(jù)正確性寫測試,即正確的輸入應(yīng)該有正常的結(jié)果。
          2. 根據(jù)錯誤性寫測試,即錯誤的輸入應(yīng)該是錯誤的結(jié)果。

          以驗證求和函數(shù)為例:

          //?add函數(shù)
          module.exports?=?(a,b)?=>?{
          ??return?a+b;
          }
          復(fù)制代碼
          //?正確性測試驗證
          const?add?=?require('./add.js');

          test('should?1+1?=?2',?()=>?{
          ??//?準(zhǔn)備測試數(shù)據(jù)?->?given
          ??const?a?=?1;
          ??const?b?=?1;
          ??//?觸發(fā)測試動作?->?when
          ??const?r?=?add(a,b);
          ??//?驗證?->?then
          ??expect(r).toBe(2);
          })
          復(fù)制代碼
          image.png
          //?錯誤性測試驗證
          test('should?1+1?=?2',?()=>?{
          ??//?準(zhǔn)備測試數(shù)據(jù)?->?given
          ??const?a?=?1;
          ??const?b?=?2;
          ??//?觸發(fā)測試動作?->?when
          ??const?r?=?add(a,b)
          ??//?驗證?->?then
          ??expect(r).toBe(2);
          })
          復(fù)制代碼
          image.png

          組件測試

          組件測試,主要是針對某個組件功能進行測試,這就相對困難些,因為很多組件涉及了DOM操作。組件測試,我們可以借助組件測試框架來做,比如使用?Cypress[16](它可以做組件測試,也可以做 e2e 測試)。我們就先來看看組件測試怎么做?

          以 vue3 組件測試為例:

          1. 我們先建好 `vue3` + `vite` 項目,編寫測試組件
          2. 再安裝 `cypress` 環(huán)境
          3. 在 `cypress/component` 編寫組件測試腳本文件
          4. 執(zhí)行 `cypress open-ct` 命令,啟動 `cypress component testing` 的服務(wù)運行 `xx.spec.js` 測試腳本,便能直觀看到單個組件自動執(zhí)行操作邏輯
          //?Button.vue?組件






          復(fù)制代碼
          //?cypress/plugin/index.js?配置

          const?{?startDevServer?}?=?require('@cypress/vite-dev-server')
          //?eslint-disable-next-line?no-unused-vars
          module.exports?=?(on,?config)?=>?{
          ??//?`on`?is?used?to?hook?into?various?events?Cypress?emits
          ??//?`config`?is?the?resolved?Cypress?config
          ??on('dev-server:start',?(options)?=>?{
          ????const?viteConfig?=?{
          ??????//?import?or?inline?your?vite?configuration?from?vite.config.js
          ????}
          ????return?startDevServer({?options,?viteConfig?})
          ??})
          ??return?config;
          }
          復(fù)制代碼
          //?cypress/component/Button.spec.js?Button組件測試腳本

          import?{?mount?}?from?"@cypress/vue";
          import?Button?from?"../../src/components/Button.vue";

          describe("Button",?()?=>?{
          ??it("should?show?button",?()?=>?{
          ????//?掛載button
          ????mount(Button);

          ????cy.contains("Button");
          ??});
          });
          復(fù)制代碼

          e2e測試

          e2e 測試,也叫端到端測試,主要是模擬用戶對頁面進行一系列操作并驗證其是否符合預(yù)期。我們同樣也可以使用 cypress 來做 e2e 測試,具體怎么做呢?

          以 todo list 功能驗證為例:

          1. 我們先建好 `vue3` + `vite` 項目,編寫測試組件
          2. 再安裝 `cypress` 環(huán)境
          3. 在 `cypress/integration` 編寫組件測試腳本文件
          4. 執(zhí)行 `cypress open` 命令,啟動 `cypress` 的服務(wù),選擇 `xx.spec.js` 測試腳本,便能直觀看到模擬用戶的操作流程
          //?cypress/integration/todo.spec.js?todo功能測試腳本

          describe('example?to-do?app',?()?=>?{
          ??beforeEach(()?=>?{
          ????cy.visit('https://example.cypress.io/todo')
          ??})

          ??it('displays?two?todo?items?by?default',?()?=>?{
          ????cy.get('.todo-list?li').first().should('have.text',?'Pay?electric?bill')
          ????cy.get('.todo-list?li').last().should('have.text',?'Walk?the?dog')
          ??})

          ??it('can?add?new?todo?items',?()?=>?{
          ????const?newItem?=?'Feed?the?cat'
          ????cy.get('[data-test=new-todo]').type(`${newItem}{enter}`)

          ????cy.get('.todo-list?li')
          ??????.should('have.length',?3)
          ??????.last()
          ??????.should('have.text',?newItem)
          ??})

          ??it('can?check?off?an?item?as?completed',?()?=>?{
          ????cy.contains('Pay?electric?bill')
          ??????.parent()
          ??????.find('input[type=checkbox]')
          ??????.check()

          ????cy.contains('Pay?electric?bill')
          ??????.parents('li')
          ??????.should('have.class',?'completed')
          ??})

          ??context('with?a?checked?task',?()?=>?{
          ????beforeEach(()?=>?{
          ??????cy.contains('Pay?electric?bill')
          ????????.parent()
          ????????.find('input[type=checkbox]')
          ????????.check()
          ????})

          ????it('can?filter?for?uncompleted?tasks',?()?=>?{
          ??????cy.contains('Active').click()

          ??????cy.get('.todo-list?li')
          ????????.should('have.length',?1)
          ????????.first()
          ????????.should('have.text',?'Walk?the?dog')

          ??????cy.contains('Pay?electric?bill').should('not.exist')
          ????})

          ????it('can?filter?for?completed?tasks',?()?=>?{
          ??????//?We?can?perform?similar?steps?as?the?test?above?to?ensure
          ??????//?that?only?completed?tasks?are?shown
          ??????cy.contains('Completed').click()

          ??????cy.get('.todo-list?li')
          ????????.should('have.length',?1)
          ????????.first()
          ????????.should('have.text',?'Pay?electric?bill')

          ??????cy.contains('Walk?the?dog').should('not.exist')
          ????})

          ????it('can?delete?all?completed?tasks',?()?=>?{
          ??????cy.contains('Clear?completed').click()

          ??????cy.get('.todo-list?li')
          ????????.should('have.length',?1)
          ????????.should('not.have.text',?'Pay?electric?bill')

          ??????cy.contains('Clear?completed').should('not.exist')
          ????})
          ??})
          })
          復(fù)制代碼

          總結(jié)

          本文前言部分通過開發(fā)、構(gòu)建、性能、測試、部署、規(guī)范六個方面,較全面地梳理了前端工程化的知識點,正文則主要介紹了在實踐項目中落地使用的前端工程化核心技術(shù)點。
          希望本文能夠幫助到正在學(xué)前端工程化的小伙伴構(gòu)建完整的知識圖譜~

          關(guān)于本文

          來源:小銘子

          https://juejin.cn/post/7033355647521554446


          The End

          歡迎自薦投稿到《前端技術(shù)江湖》,如果你覺得這篇內(nèi)容對你挺有啟發(fā),記得點個?「在看」


          點個『在看』支持下?

          瀏覽 77
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  人妻巨大乳HD无码 | 台湾成人久久综合网 | 一卡二卡国产精品 | 台湾无码中文字幕 | 成人免费视频 国产免费麻豆, |