天天看點

【工程化】1191- 結合代碼實踐,全面學習前端工程化

【工程化】1191- 結合代碼實踐,全面學習前端工程化

前言

前端工程化,簡而言之就是軟體工程 + 前端,以自動化的形式呈現。就個人了解而言:前端工程化,從開發階段到代碼釋出生産環境,包含了以下幾個内容:

  • 開發
  • 建構
  • 測試
  • 部署
  • 性能
  • 規範
【工程化】1191- 結合代碼實踐,全面學習前端工程化

下面我們根據上述幾個内容,選擇有代表性的幾個方面進行深入學習前端工程化。

回顧:【青訓營】- 了解前端工程化[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​

​建立項目,用程式來解決。解決步驟如下:

  1. 建立檔案夾(項目名)
  2. 建立 index.js
  3. 建立 package.json
  4. 安裝依賴

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

【工程化】1191- 結合代碼實踐,全面學習前端工程化

image.png

Webpack原理

想要真正用好 ​

​Webpack​

​​ 編譯建構工具,我們需要先來了解下它的工作原理。​

​Webpack​

​ 編譯項目的工作機制是,遞歸找出所有依賴子產品,轉換源碼為浏覽器可執行代碼,并建構輸出bundle。具體工作流程步驟如下:

  1. 初始化參數:取配置檔案和shell腳本參數并合并
  2. 開始編譯:用上一步得到的參數初始化​

    ​compiler​

    ​對象,執行​

    ​run​

    ​方法開始編譯
  3. 确定入口:根據配置中的​

    ​entry​

    ​,确定入口檔案
  4. 編譯子產品:從入口檔案出發,遞歸周遊找出所有依賴子產品的檔案
  5. 完成子產品編譯:使用​

    ​loader​

    ​轉譯所有子產品,得到轉譯後的最終内容和依賴關系
  6. 輸出資源:根據入口和子產品依賴關系,組裝成一個個​

    ​chunk​

    ​,加到輸出清單
  7. 輸出完成:根據配置中的​

    ​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執行耗時具體情況。

優化編譯速度該怎麼做呢?

  1. 減少搜尋依賴的時間
  • 配置 loader 比對規則 test/include/exclue,縮小搜尋範圍,即可減少搜尋時間
  1. 減少解析轉換的時間
  • noParse配置,精準過濾不用解析的子產品
  • loader性能消耗大的,開啟多程序
  1. 減少建構輸出的時間
  • 壓縮代碼,開啟多程序
  1. 合理使用緩存政策
  • babel-loader開啟緩存
  • 中間子產品啟用緩存,比如使用 hard-source-webpack-plugin
具體優化措施可參考:webpack性能優化的一段經曆|項目複盤[5]

體積優化

檢測包體積大小

尋找檢測建構後包體積大小的工具,比如 webpack-bundle-analyzer插件[6] ,用該插件分析打包後生成Bundle的每個子產品體積大小。

優化體積該怎麼做呢?

  1. bundle去除第三方依賴
  2. 擦除無用代碼 Tree Shaking
具體優化措施參考:webpack性能優化的一段經曆|項目複盤[7]

Rollup

Rollup概述

Rollup[8] 是一個 JavaScript 子產品打包器,可以将小塊代碼編譯成大塊複雜的代碼,例如 library 或應用程式。并且可以對代碼子產品使用新的标準化格式,比如​

​CommonJS​

​​ 和 ​

​es module​

​。

Rollup原理

我們先來了解下 ​

​Rollup​

​ 原理,其主要工作機制是:

  1. 确定入口檔案
  2. 使用​

    ​Acorn​

    ​ 讀取解析檔案,擷取抽象文法樹 AST
  3. 分析代碼
  4. 生成代碼,輸出

​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 開發編譯速度極快?我們就先來探究下它的原理吧。

【工程化】1191- 結合代碼實踐,全面學習前端工程化

由上圖可見,Vite 原理是利用現代主流浏覽器支援原生的 ESM 規範,配合 server 做攔截,把代碼編譯成浏覽器支援的。

【工程化】1191- 結合代碼實踐,全面學習前端工程化

Vite實踐體驗

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

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

使用 NPM:

$ npm init vite@latest
複制代碼      

使用 Yarn:

$ yarn create vite
複制代碼      
【工程化】1191- 結合代碼實踐,全面學習前端工程化

上圖是Vite項目的編譯時間,363ms,開發秒級編譯的體驗,真的是棒棒哒!

3種建構工具綜合對比

Webpack Rollup Vite
編譯速度 一般 較快 最快
HMR熱更新 支援 需要額外引入插件 支援
Tree Shaking 需要額外配置 支援 支援
适用範圍 項目打包 類庫打包 不考慮相容性的項目

測試

當我們前端項目越來越龐大時,開發疊代維護成本就會越來越高,數十個子產品互相調用錯綜複雜,為了提高代碼品質和可維護性,就需要寫測試了。下面就給大家具體介紹下前端工程經常做的3類測試。

單元測試

單元測試,是對最小可測試單元(一般為單個函數、類或元件)進行檢查和驗證。

做單元測試的架構有很多,比如 Mocha[12]、斷言庫Chai[13]、Sinon[14]、Jest[15]等。我們可以先選擇 jest 來學習,因為它內建了 ​

​Mocha​

​​,​

​chai​

​​,​

​jsdom​

​​,​

​sinon​

​​ 等功能。接下來,我們一起看看 ​

​jest​

​ 怎麼寫單元測試吧?

  1. 根據正确性寫測試,即正确的輸入應該有正常的結果。
  2. 根據錯誤性寫測試,即錯誤的輸入應該是錯誤的結果。

以驗證求和函數為例:

// 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);
})
複制代碼      
【工程化】1191- 結合代碼實踐,全面學習前端工程化

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);
})
複制代碼      
【工程化】1191- 結合代碼實踐,全面學習前端工程化

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')
    })
  })
})
複制代碼      

總結

本文前言部分通過開發、建構、性能、測試、部署、規範六個方面,較全面地梳理了前端工程化的知識點,正文則主要介紹了在實踐項目中落地使用的前端工程化核心技術點。

希望本文能夠幫助到正在學前端工程化的小夥伴建構完整的知識圖譜~

小銘子