前言
前端工程化,簡而言之就是軟體工程 + 前端,以自動化的形式呈現。就個人了解而言:前端工程化,從開發階段到代碼釋出生産環境,包含了以下幾個内容:
- 開發
- 建構
- 測試
- 部署
- 性能
- 規範
下面我們根據上述幾個内容,選擇有代表性的幾個方面進行深入學習前端工程化。
回顧:【青訓營】- 了解前端工程化[2]
腳手架
腳手架是什麼?(What)
現在流行的前端腳手架基本上都是基于
NodeJs
編寫,比如我們常用的
Vue-CLI
,比較火的
create-react-app
,還有
Dva-CLI
等。
腳手架存在的意義?(Why)
随着前端工程化的概念越來越深入人心,腳手架的出現就是為減少重複性工作而引入的指令行工具,擺脫
ctrl + c
,
ctrl + v
,此話怎講? 現在建立一個前端項目,已經不是在
html
頭部引入
css
,尾部引入
js
那麼簡單的事了,
css
都是采用
Sass
或則
Less
編寫,在
js
中引入,然後動态建構注入到
html
中;除了學習基本的
js
,
css
文法和熱門架構,還需要學習建構工具
webpack
,
babel
這些怎麼配置,怎麼起前端服務,怎麼熱更新;為了在編寫過程中讓編輯器幫我們查錯以及更加規範,我們還需要引入
ESlint
;甚至,有些項目還需要引入單元測試(
Jest
)。對于一個更入門的人來說,這無疑會讓人望而卻步。而前端腳手架的出現,就讓事情簡單化,一鍵指令,建立一個工程,再執行兩個
npm
指令,跑起一個項目。在入門時,無需關注配置什麼的,隻需要開心的寫代碼就好。
如何實作一個建立項目腳手架(基于koa)?(How)
先梳理下實作思路
我們實作腳手架的核心思想就是自動化思維,将重複性的
ctrl + c
,
ctrl + v
建立項目,用程式來解決。解決步驟如下:
- 建立檔案夾(項目名)
- 建立 index.js
- 建立 package.json
- 安裝依賴
1. 建立檔案夾
建立檔案夾前,需要先删除清空:
// package.json
{
...
"scripts": {
"test": "rm -rf ./haha && node --experimental-modules index.js"
}
...
}
複制代碼
建立檔案夾:我們通過引入
nodejs
的
fs
子產品,使用
mkdirSync
API來建立檔案夾。
// index.js
import fs from 'fs';
function getRootPath() {
return "./haha";
}
// 生成檔案夾
fs.mkdirSync(getRootPath());
複制代碼
2. 建立 index.js
建立 index.js:使用
nodejs
的
fs
子產品的
writeFileSync
API 建立 index.js 檔案:
// index.js
fs.writeFileSync(getRootPath() + "/index.js", createIndexTemplate(inputConfig));
複制代碼
接着我們來看看,動态模闆如何生成?我們最理想的方式是通過配置來動态生成檔案模闆,那麼具體來看看
createIndexTemplate
實作的邏輯吧。
// 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));
複制代碼
// indexTemplate.js
import ejs from "ejs";
import fs from "fs";
import prettier from "prettier";// 格式化代碼
// 問題驅動
// 模闆
// 開發思想 - 小步驟的開發思想
// 動态生成代碼模闆
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",
});
}
複制代碼
// 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 %>");
});
複制代碼
3. 建立 package.json
建立 package.json 檔案,實質是和建立 index.js 類似,都是采用動态生成模闆的思路來實作,我們來看下核心方法
createPackageJsonTemplate
的實作代碼:
// 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",
});
}
複制代碼
// 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"
}
<% } %>
}
複制代碼
4. 安裝依賴
要自動安裝依賴,我們可以使用
nodejs
的
execa
庫執行
yarn
安裝指令:
execa("yarn", {
cwd: getRootPath(),
stdio: [2, 2, 2],
});
複制代碼
至此,我們已經用
nodejs
實作了建立項目的腳手架了。最後我們可以重新梳理下可優化點将其更新完善。比如将程式配置更新成
GUI
使用者配置(使用者通過手動選擇或是輸入來傳入配置參數,例如項目名)。
編譯建構
編譯建構是什麼?
建構,或者叫作編譯,是前端工程化體系中功能最繁瑣、最複雜的子產品,承擔着從源代碼轉化為宿主浏覽器可執行的代碼,其核心是資源的管理。前端的産出資源包括JS、CSS、HTML等,分别對應的源代碼則是:
- 領先于浏覽器實作的ECMAScript規範編寫的JS代碼(ES6/7/8...)。
- LESS/SASS預編譯文法編寫的CSS代碼。
- Jade/EJS/Mustache等模闆文法編寫的HTML代碼。
以上源代碼是無法在浏覽器環境下運作的,建構工作的核心便是将其轉化為宿主可執行代碼,分别對應:
- ECMAScript規範的轉譯。
- CSS預編譯文法轉譯。
- HTML模闆渲染。
那麼下面我們就一起學習下如今3大主流建構工具:Webpack、Rollup、Vite。
Webpack
image.png
Webpack原理
想要真正用好
Webpack
編譯建構工具,我們需要先來了解下它的工作原理。
Webpack
編譯項目的工作機制是,遞歸找出所有依賴子產品,轉換源碼為浏覽器可執行代碼,并建構輸出bundle。具體工作流程步驟如下:
- 初始化參數:取配置檔案和shell腳本參數并合并
- 開始編譯:用上一步得到的參數初始化
對象,執行compiler
方法開始編譯run
- 确定入口:根據配置中的
,确定入口檔案entry
- 編譯子產品:從入口檔案出發,遞歸周遊找出所有依賴子產品的檔案
- 完成子產品編譯:使用
轉譯所有子產品,得到轉譯後的最終内容和依賴關系loader
- 輸出資源:根據入口和子產品依賴關系,組裝成一個個
,加到輸出清單chunk
- 輸出完成:根據配置中的
,确定輸出路徑和檔案名,把檔案内容寫入輸出目錄(預設是output
)dist
Webpack實踐
1. 基礎配置
entry
入口配置,webpack 編譯建構時能找到編譯的入口檔案,進而建構内部依賴圖。
output
輸出配置,告訴 webpack 在哪裡輸出它所建立的 bundle,以及如何命名這些檔案。
loader
子產品轉換器,loader 可以處理浏覽器無法直接運作的檔案子產品,轉換為有效子產品。比如:css-loader和style-loader處理樣式;url-loader和file-loader處理圖檔。
plugin
插件,解決 loader 無法實作的問題,在 webpack 整個建構生命周期都可以擴充插件。比如:打包優化,資源管理,注入環境變量等。
下面是 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 , 從後到前執行
use: ["style-loader", "css-loader"],
}
],
},
};
複制代碼
參考webpack官網:webpack.docschina.org/concepts/[3]
(注意:使用不同版本的 webpack 切換對應版本的文檔哦)
2. 性能優化
編譯速度優化
檢測編譯速度
尋找檢測編譯速度的工具,比如 speed-measure-webpack-plugin插件[4] ,用該插件分析每個loader和plugin執行耗時具體情況。
優化編譯速度該怎麼做呢?
- 減少搜尋依賴的時間
- 配置 loader 比對規則 test/include/exclue,縮小搜尋範圍,即可減少搜尋時間
- 減少解析轉換的時間
- noParse配置,精準過濾不用解析的子產品
- loader性能消耗大的,開啟多程序
- 減少建構輸出的時間
- 壓縮代碼,開啟多程序
- 合理使用緩存政策
具體優化措施可參考:webpack性能優化的一段經曆|項目複盤[5]
- babel-loader開啟緩存
- 中間子產品啟用緩存,比如使用 hard-source-webpack-plugin
體積優化
檢測包體積大小
尋找檢測建構後包體積大小的工具,比如 webpack-bundle-analyzer插件[6] ,用該插件分析打包後生成Bundle的每個子產品體積大小。
優化體積該怎麼做呢?
具體優化措施參考:webpack性能優化的一段經曆|項目複盤[7]
- bundle去除第三方依賴
- 擦除無用代碼 Tree Shaking
Rollup
Rollup概述
Rollup[8] 是一個 JavaScript 子產品打包器,可以将小塊代碼編譯成大塊複雜的代碼,例如 library 或應用程式。并且可以對代碼子產品使用新的标準化格式,比如
CommonJS
和
es module
。
Rollup原理
我們先來了解下
Rollup
原理,其主要工作機制是:
- 确定入口檔案
- 使用
讀取解析檔案,擷取抽象文法樹 ASTAcorn
- 分析代碼
- 生成代碼,輸出
Rollup
相對
Webpack
而言,打包出來的包會更加輕量化,更适用于類庫打包,因為内置了 Tree Shaking 機制,在分析代碼階段就知曉哪些檔案引入并未調用,打包時就會自動擦除未使用的代碼。
Acorn 是一個 JavaScript 文法解析器,它将 JavaScript 字元串解析成文法抽象樹 AST 如果想了解 AST 文法樹可以點下這個網址astexplorer.net/[9]
Rollup實踐
input
入口檔案路徑
output
輸出檔案、輸出格式(amd/es6/iife/umd/cjs)、sourcemap啟用等。
plugin
各種插件使用的配置
external
提取外部依賴
global
配置全局變量
下面是 Rollup 基礎配置的簡單示例:
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"]
};
複制代碼
Vite
Vite概述
Vite[10],相比 Webpack、Rollup 等工具,極大地改善了前端開發者的開發體驗,編譯速度極快。
Vite原理
為什麼 Vite 開發編譯速度極快?我們就先來探究下它的原理吧。
由上圖可見,Vite 原理是利用現代主流浏覽器支援原生的 ESM 規範,配合 server 做攔截,把代碼編譯成浏覽器支援的。
Vite實踐體驗
我們可以搭建一個Hello World版的Vite項目來感受下飛快的開發體驗:
注意:Vite 需要 Node.js[11] 版本 >= 12.0.0。
使用 NPM:
$ npm init vite@latest
複制代碼
使用 Yarn:
$ yarn create vite
複制代碼
上圖是Vite項目的編譯時間,363ms,開發秒級編譯的體驗,真的是棒棒哒!
3種建構工具綜合對比
Webpack | Rollup | Vite | |
編譯速度 | 一般 | 較快 | 最快 |
HMR熱更新 | 支援 | 需要額外引入插件 | 支援 |
Tree Shaking | 需要額外配置 | 支援 | 支援 |
适用範圍 | 項目打包 | 類庫打包 | 不考慮相容性的項目 |
測試
當我們前端項目越來越龐大時,開發疊代維護成本就會越來越高,數十個子產品互相調用錯綜複雜,為了提高代碼品質和可維護性,就需要寫測試了。下面就給大家具體介紹下前端工程經常做的3類測試。
單元測試
單元測試,是對最小可測試單元(一般為單個函數、類或元件)進行檢查和驗證。
做單元測試的架構有很多,比如 Mocha[12]、斷言庫Chai[13]、Sinon[14]、Jest[15]等。我們可以先選擇 jest 來學習,因為它內建了
Mocha
,
chai
,
jsdom
,
sinon
等功能。接下來,我們一起看看
jest
怎麼寫單元測試吧?
- 根據正确性寫測試,即正确的輸入應該有正常的結果。
- 根據錯誤性寫測試,即錯誤的輸入應該是錯誤的結果。
以驗證求和函數為例:
// add函數
module.exports = (a,b) => {
return a+b;
}
複制代碼
// 正确性測試驗證
const add = require('./add.js');
test('should 1+1 = 2', ()=> {
// 準備測試資料 -> given
const a = 1;
const b = 1;
// 觸發測試動作 -> when
const r = add(a,b);
// 驗證 -> then
expect(r).toBe(2);
})
複制代碼
image.png
// 錯誤性測試驗證
test('should 1+1 = 2', ()=> {
// 準備測試資料 -> given
const a = 1;
const b = 2;
// 觸發測試動作 -> when
const r = add(a,b)
// 驗證 -> then
expect(r).toBe(2);
})
複制代碼
image.png
元件測試
元件測試,主要是針對某個元件功能進行測試,這就相對困難些,因為很多元件涉及了
DOM
操作。元件測試,我們可以借助元件測試架構來做,比如使用 Cypress[16](它可以做元件測試,也可以做 e2e 測試)。我們就先來看看元件測試怎麼做?
以 vue3 元件測試為例:
我們先建好 `vue3` + `vite` 項目,編寫測試元件
再安裝 `cypress` 環境
在 `cypress/component` 編寫元件測試腳本檔案
執行 `cypress open-ct` 指令,啟動 `cypress component testing` 的服務運作 `xx.spec.js` 測試腳本,便能直覺看到單個元件自動執行操作邏輯
// Button.vue 元件
<template>
<div>Button測試</div>
</template>
<script>
export default {
}
</script>
<style>
</style>
複制代碼
// 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;
}
複制代碼
// 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");
});
});
複制代碼
e2e測試
e2e 測試,也叫端到端測試,主要是模拟使用者對頁面進行一系列操作并驗證其是否符合預期。我們同樣也可以使用 cypress 來做 e2e 測試,具體怎麼做呢?
以 todo list 功能驗證為例:
我們先建好 `vue3` + `vite` 項目,編寫測試元件
再安裝 `cypress` 環境
在 `cypress/integration` 編寫元件測試腳本檔案
執行 `cypress open` 指令,啟動 `cypress` 的服務,選擇 `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')
})
})
})
複制代碼
總結
本文前言部分通過開發、建構、性能、測試、部署、規範六個方面,較全面地梳理了前端工程化的知識點,正文則主要介紹了在實踐項目中落地使用的前端工程化核心技術點。
希望本文能夠幫助到正在學前端工程化的小夥伴建構完整的知識圖譜~