結(jié)合代碼實踐,全面學(xué)習(xí)前端工程化
點擊上方關(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)建項目,用程序來解決。解決步驟如下:
創(chuàng)建文件夾(項目名) 創(chuàng)建 index.js 創(chuàng)建 package.json 安裝依賴
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

Webpack原理
想要真正用好?Webpack?編譯構(gòu)建工具,我們需要先來了解下它的工作原理。Webpack?編譯項目的工作機制是,遞歸找出所有依賴模塊,轉(zhuǎn)換源碼為瀏覽器可執(zhí)行代碼,并構(gòu)建輸出bundle。具體工作流程步驟如下:
初始化參數(shù):取配置文件和shell腳本參數(shù)并合并 開始編譯:用上一步得到的參數(shù)初始化 compiler對象,執(zhí)行run方法開始編譯確定入口:根據(jù)配置中的 entry,確定入口文件編譯模塊:從入口文件出發(fā),遞歸遍歷找出所有依賴模塊的文件 完成模塊編譯:使用 loader轉(zhuǎn)譯所有模塊,得到轉(zhuǎn)譯后的最終內(nèi)容和依賴關(guān)系輸出資源:根據(jù)入口和模塊依賴關(guān)系,組裝成一個個 chunk,加到輸出列表輸出完成:根據(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)化編譯速度該怎么做呢?
減少搜索依賴的時間
配置 loader 匹配規(guī)則 test/include/exclue,縮小搜索范圍,即可減少搜索時間
減少解析轉(zhuǎn)換的時間
noParse配置,精準(zhǔn)過濾不用解析的模塊 loader性能消耗大的,開啟多進程
減少構(gòu)建輸出的時間
壓縮代碼,開啟多進程
合理使用緩存策略
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)化體積該怎么做呢?
bundle去除第三方依賴 擦除無用代碼 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?原理,其主要工作機制是:
確定入口文件 使用? Acorn?讀取解析文件,獲取抽象語法樹 AST分析代碼 生成代碼,輸出
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)建工具綜合對比
| Webpack | Rollup | Vite | |
|---|---|---|---|
| 編譯速度 | 一般 | 較快 | 最快 |
| 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?怎么寫單元測試吧?
根據(jù)正確性寫測試,即正確的輸入應(yīng)該有正常的結(jié)果。 根據(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ù)制代碼

//?錯誤性測試驗證
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ù)制代碼

組件測試
組件測試,主要是針對某個組件功能進行測試,這就相對困難些,因為很多組件涉及了DOM操作。組件測試,我們可以借助組件測試框架來做,比如使用?Cypress[16](它可以做組件測試,也可以做 e2e 測試)。我們就先來看看組件測試怎么做?
以 vue3 組件測試為例:
我們先建好 `vue3` + `vite` 項目,編寫測試組件再安裝 `cypress` 環(huán)境在 `cypress/component` 編寫組件測試腳本文件執(zhí)行 `cypress open-ct` 命令,啟動 `cypress component testing` 的服務(wù)運行 `xx.spec.js` 測試腳本,便能直觀看到單個組件自動執(zhí)行操作邏輯
//?Button.vue?組件
??Button測試
復(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 功能驗證為例:
我們先建好 `vue3` + `vite` 項目,編寫測試組件再安裝 `cypress` 環(huán)境在 `cypress/integration` 編寫組件測試腳本文件執(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ā),記得點個?「在看」哦
點個『在看』支持下?
