Vue3 + Vite + Pinia創(chuàng)建單頁(yè)面應(yīng)用
原文鏈接:https://labs.pineview.io/learn-how-to-build-test-and-deploy-a-single-page-app-with-vue-3-vite-and-pinia/[1]
作者:Andrei Rusu[2]
正文從這開(kāi)始~
介紹
誕生于2014年的Vue.js,無(wú)疑是目前領(lǐng)先的前端框架之一,隨著社區(qū)的發(fā)展以及生態(tài)系統(tǒng)的壯大,在相當(dāng)一段時(shí)間內(nèi),它的低位都是穩(wěn)固的。幾年前我曾在個(gè)別項(xiàng)目中使用過(guò)Vue 2,那是一種令人愉快的體驗(yàn)。
我覺(jué)得是時(shí)候把我的工具集升級(jí)到最新版本了。與此同時(shí),也要升級(jí)諸如Vite和Pinia的新型工具。
本篇指南將涵蓋詳盡的步驟,使用Vue 3來(lái)創(chuàng)建一個(gè)功能性的書(shū)店SPA實(shí)例,并使用Vite來(lái)運(yùn)行它。它還包括如何使用Pinia(Vuex的后繼者)添加狀態(tài)管理,以及如何使用Vue Router進(jìn)行路由管理的細(xì)節(jié)。
將涵蓋的核心概念有:
-
使用Vite創(chuàng)建Vue 3單頁(yè)應(yīng)用(SPA) -
使用Vue Router管理路由 -
使用Pinia管理應(yīng)用狀態(tài) -
使用VIte運(yùn)行、構(gòu)建、發(fā)布應(yīng)用 -
編寫、運(yùn)行Vue組件單元測(cè)試 -
使用Nightwatch.js編寫、運(yùn)行自動(dòng)化的端到端測(cè)試
這似乎看起來(lái)有很多內(nèi)容,但我認(rèn)為完全有可能在20分鐘內(nèi)完成所有。上面列出的一些概念可以擴(kuò)展成單獨(dú)的完整教程,但這里我只涵蓋了啟動(dòng)和運(yùn)行項(xiàng)目所必需的內(nèi)容。
最后需要提到的是,本教程不涉及到后端。盡管數(shù)據(jù)是使用瀏覽器的Fetch API(XHR的后繼者)加載的,但本身是沒(méi)有服務(wù)端組件的。也就是說(shuō),可以很容易地添加一個(gè)后端組件。
總體而言,我們即將在這里構(gòu)建的應(yīng)用程序可以作為一個(gè)靜態(tài)網(wǎng)站部署。如果你渴望馬上開(kāi)始編程,并立刻投入其中,你可以直接使用以下方法來(lái)啟動(dòng)和運(yùn)行該項(xiàng)目:
git clone <https://github.com/beatfactor/middlemarch>
npm install
npm run dev
或者在Github上fork本項(xiàng)目: https://github.com/beatfactor/middlemarch[3]
步驟一:使用create-vite腳手架工具設(shè)置應(yīng)用程序
我們將要使用官方腳手架工具create-vite來(lái)設(shè)置項(xiàng)目架構(gòu),因此你要確保已經(jīng)安裝了Node 12+與NPM 6+。腳手架工具也支持Yarn和PNPM作為包管理器,但這里我們只涉及NPM。
create-vite會(huì)為你創(chuàng)建項(xiàng)目文件夾,所以首先要確保使用cd命令進(jìn)入到符父文件夾:
cd /workspace
使用以下命令安裝Vite并初始化項(xiàng)目:
$ npm init vite@latest
然后你會(huì)被提示輸入項(xiàng)目名稱并選擇你想要使用的庫(kù)。我們從列表中選擇vue:
~/workspace % npm init vite@latest
npx: installed 6 in 1.051s
? Project name: … vue-bookstore
? Select a framework: ? - Use arrow-keys. Return to submit.
vanilla
? vue
react
preact
lit
svelte
然后選擇vue作為變量,因?yàn)槲覀冞@里不使用TypeScript :
? Select a variant: ? - Use arrow-keys. Return to submit.
? vue
vue-ts
你應(yīng)該會(huì)看到如下輸出:
npx: installed 6 in 1.051s
? Project name: … vue-bookstore
? Select a framework: ? vue
? Select a variant: ? vue
Scaffolding project in /Users/andrei/workspace/vue-bookstore...
Done. Now run:
cd vue-bookstore
npm install
npm run dev
一旦我們按照上述說(shuō)明進(jìn)行操作,我們將從Vite得到以下輸出,來(lái)告訴我們?cè)搼?yīng)用正在運(yùn)行。
vite v2.7.7 dev server running at:
> Local: <http://localhost:3000/>
> Network: use `--host` to expose
ready in 611ms.
訪問(wèn)localhost:3000[4],歡迎頁(yè)面會(huì)像下面這樣:
步驟二:添加路由和狀態(tài)管理
我們先來(lái)看下由create-vite創(chuàng)建的項(xiàng)目目錄結(jié)構(gòu):
vue-bookstore/
├── public/
| ├── favicon.ico
├── src/
| ├── assets/
| | └── logo.png
| ├── components/
| | └── HelloWorld.vue
| ├── App.vue
| └── main.js
├─── package.json
├─── README.md
└─── vite.config.js
在本篇指南的本節(jié)中,將要為我們的項(xiàng)目添加兩個(gè)新的依賴:vue-router 和 pinia 。首先使用NPM來(lái)安裝他們。
Vue Router
vue-router是Vue.js官方的路由管理工具。我們需要安裝v4來(lái)兼容Vue 3:
$ npm install vue-router@4 --save
Pinia
Pinia是Vue生態(tài)系統(tǒng)中新涌現(xiàn)的項(xiàng)目之一,它是Vue.js應(yīng)用程序最新的官方狀態(tài)管理工具。它的API與Vuex(其前身)非常相似,它被設(shè)計(jì)得更快速、更輕量。
可以使用NPM來(lái)安裝Pinia :
$ npm install pinia --save
設(shè)置路由
如果你不熟悉在SPA中使用路由或者狀態(tài)管理工具,不要擔(dān)心;這兩個(gè)概念都非常容易理解,一旦你看到它是如何工作的,他們就會(huì)自行解釋。
另外請(qǐng)謹(jǐn)記,在這里我們只是創(chuàng)建一個(gè)教程,目標(biāo)是在20分鐘內(nèi)完成所有工作并運(yùn)行。這并不要求我們學(xué)習(xí)Vue.js所有的相關(guān)知識(shí)。
什么是單頁(yè)面應(yīng)用(SPA)?
既然我們?cè)谶@里構(gòu)建的是SPA,那么考慮一下這意味著什么,以及什么是單頁(yè)面,這也許是有用的。
單頁(yè)面應(yīng)用只是一個(gè)web應(yīng)用,當(dāng)你導(dǎo)航到另一個(gè)子頁(yè)面時(shí),它不會(huì)重新加載頁(yè)面。不過(guò)瀏覽器的url會(huì)被修改,就好像頁(yè)面被重新加載一樣,這是使用
HTML5的History API做到的。
在Vite中使用Vue組件
使用create-vite腳手架工具創(chuàng)建的項(xiàng)目,默認(rèn)添加了一個(gè)非常基礎(chǔ)的vue組件,位于src/components/HelloWorld.vue 。然后它被用在位于src/App.vue的主應(yīng)用組件中。
這里還有另外兩個(gè)很重要的文件:
-
「index.html」 -
「src/main.js」
index.html文件是當(dāng)瀏覽器導(dǎo)航到我們應(yīng)用程序頁(yè)面時(shí)看到的內(nèi)容,main.js是Vue.js應(yīng)用程序的入口。
下面是這些文件的內(nèi)容:
「index.html」
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
「src/main.js」
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
添加路由
現(xiàn)在是時(shí)候創(chuàng)建我們應(yīng)用程序的主路由了。在Vue中,每一個(gè)路由必須與一個(gè)組件相對(duì)應(yīng)。在當(dāng)前應(yīng)用程序中,我們將考慮每個(gè)子頁(yè)面的一個(gè)組件,就像這樣:
-
「Homepage」 - 我們的書(shū)店主頁(yè)面 -
「Cart」 - 購(gòu)物車和結(jié)算頁(yè)面 -
「Sign-In」 - 用戶登錄頁(yè)面
既然這只是一個(gè)示例,像用戶注冊(cè)、產(chǎn)品詳情等其他頁(yè)面,將會(huì)被忽略。而且,登錄頁(yè)面只包含一個(gè)模擬登錄。
對(duì)于基礎(chǔ)的HTML和CSS,我使用Bootstrap 5做了一些事情,比如UI下拉菜單和表單。當(dāng)然你可以使用任何你想用的UI庫(kù)。
我們將暫時(shí)創(chuàng)建空的頁(yè)面組件,好讓我們可以設(shè)置路由。新的src目錄結(jié)構(gòu)將會(huì)長(zhǎng)這樣(稍后將移除樣板代碼):
src/
├── components/
| └── TopNavbar.js
├── lib/
| ├── router.js
| └── store.js
├── pages/
| ├── cart/
| | ├── cart.css
| | ├── cart.html
| | └── Cart.vue
| ├── home/
| | ├── home.css
| | ├── home.html
| | └── Home.vue
| ├── sign-in/
| | ├── sign-in.css
| | ├── sign-in.html
| | └── SignIn.vue
| └── routes.js
├── App.vue
└── main.js
我們添加了三個(gè)頁(yè)面,每一個(gè)頁(yè)面都非常基礎(chǔ)。我們將添加TobNavbar 組件,使得路由導(dǎo)航生效,而不需要重新加載頁(yè)面。
為src/pages/cart/Cart.vue、src/pages/home/Home.vue和src/pages/sign-in/SignIn.vue添加如下內(nèi)容:
<script setup>
import TopNavbar from '../../components/TopNavbar.vue';
</script>
<template>
<TopNavbar />
</template>
<style></style>
<script>
export default {
components: {
TopNavbar
},
computed: {},
mounted() {
},
data() {
return {
};
},
};
</script>
TobNavbar 組件位于src/components ,只包含了路由導(dǎo)航鏈接。需要注意的是,router-link組件是vue-router的一部分:
<template>
<router-link to="/">Home</router-link>
<router-link to="/cart/">Cart</router-link>
<router-link to="/sign-in/">Sign In</router-link>
</template>
pages/routes.js文件包含了應(yīng)用程序中所有的路由聲明。如下所示:
import {createRouter} from 'vue-router'
import Homepage from './home/Home.vue';
import SignIn from './sign-in/SignIn.vue';
import Cart from './cart/Cart.vue';
const routes = [
{
path: '/',
component: Homepage
},
{
path: '/sign-in/',
component: SignIn
},
{
path: '/cart/',
component: Cart
},
]
export default function (history) {
return createRouter({
history,
routes
})
}
在我們準(zhǔn)備看到vue-router成功運(yùn)行之前,我們只需要再做兩件事:
1)創(chuàng)建路由并將其添加到src/main.js里的Vue應(yīng)用程序?qū)嵗校?/p>
import { createApp } from 'vue'
import { createWebHistory } from 'vue-router'
import createRouter from './pages/routes.js'
import App from './App.vue'
const router = createRouter(createWebHistory())
const app = createApp(App)
app.use(router).mount('#app')
2)在src/App.vue里添加<router-view>組件:
<template>
<router-view></router-view>
</template>
如果需要的話,我們重新運(yùn)行npm run dev ,然后導(dǎo)航到http://localhost:3000 。你將擁有一個(gè)啟用了路由的Vue 3應(yīng)用程序。
使用Pinia設(shè)置狀態(tài)管理
我們繼續(xù)。現(xiàn)在我們需要為我們的app設(shè)置Pinia store。store(倉(cāng)庫(kù))是應(yīng)用程序維護(hù)狀態(tài)(state)的地方。
Pinia是Vue.js核心團(tuán)隊(duì)的一個(gè)新項(xiàng)目,現(xiàn)在是使用應(yīng)用程序狀態(tài)的推薦方法。如果你已經(jīng)很熟悉vuex,那么適應(yīng)Pinia將會(huì)非常簡(jiǎn)單。事實(shí)上,Pinia的API比vuex稍微簡(jiǎn)單一點(diǎn),也更加簡(jiǎn)潔明了。
在vue3中使用Pinia,將會(huì)有一個(gè)根store以及任意數(shù)量的獨(dú)立store。針對(duì)我們的書(shū)店app,我們將只使用兩個(gè)store:
-
目錄 store: 可用的書(shū)單 -
購(gòu)物車 store: 用戶想要訂購(gòu)的書(shū)籍
創(chuàng)建Pinia
我們需要?jiǎng)?chuàng)建第一個(gè)根store,并傳遞給vue實(shí)例。在src/main.js文件中進(jìn)行代碼更新,如下所示:
import { createApp } from 'vue'
import { createWebHistory } from 'vue-router'
import { createPinia } from 'pinia'
import createRouter from './pages/routes.js'
import App from './App.vue'
const store = createPinia()
const router = createRouter(createWebHistory())
const app = createApp(App)
app.use(router).use(store).mount('#app')
接下來(lái)我們需要?jiǎng)?chuàng)建獨(dú)立的目錄store 以及購(gòu)物車store,并在組件中使用他們。
添加目錄store
創(chuàng)建一個(gè)Pinia store意味著兩件主要的事情:
-
定義store -
在一個(gè)或多個(gè)組件中使用store
「定義store」
和Vuex一樣,Pinia store包含狀態(tài)(state)以及兩種類型的方法:「getters」 和 「actions」。
關(guān)于一個(gè)store需要考慮的事情:
-
Getters是一個(gè)同步方法,用來(lái)從狀態(tài)中獲取數(shù)據(jù) -
Actions可以是一個(gè)異步的方法,用來(lái)更新?tīng)顟B(tài) -
state被定義為一個(gè)返回初始狀態(tài)的函數(shù)
是時(shí)候在src/stores/catalog.js里面創(chuàng)建目錄store了:
import { defineStore } from 'pinia'
export const useCatalog = defineStore('catalog-store', {
state: () => {
return {
newArrivals: [],
fetching: false
}
},
getters: {
results(state) {
return state.newArrivals;
},
isFetching(state) {
return state.fetching;
}
},
actions: {
async fetchNewArrivals() {
this.fetching = true;
const response = await fetch('/data/new-arrivals.json');
try {
const result = await response.json();
this.newArrivals = result.books;
} catch (err) {
this.newArrivals = [];
console.error('Error loading new arrivals:', err);
return err;
}
this.fetching = false;
}
}
})
查看上面的源碼,你可以注意到我們有兩個(gè)getters和一個(gè)actions。我們沒(méi)有真正的后端,只有一個(gè)位于/data/new-arrivals.json的json文件。其中包含一些書(shū)籍,我們將把它們作為我們的目錄。
你也可以注意到,我們的getters沒(méi)有對(duì)數(shù)據(jù)做任何特殊的處理,導(dǎo)致他們有一點(diǎn)多余。但我認(rèn)為用來(lái)展示如何定義他們也是不錯(cuò)的。
在模板文件中使用store
將上面的定義鏈接到模板文件也非常的簡(jiǎn)單。
讓我們?cè)?code style="overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(145, 109, 213);font-weight: bolder;background-image: none;background-position: initial;background-size: initial;background-repeat: initial;background-attachment: initial;background-origin: initial;background-clip: initial;">src/components/NewArrivals.vue中創(chuàng)建一個(gè)叫做NewArrivals的新組件,我們將在Home.vue頁(yè)面組件中使用它。
<script setup>
import {useCatalog} from '../../store/catalog.js'
</script>
<template>
</template>
<style scoped></style>
<script>
import { mapState, mapActions } from 'pinia'
export default {
computed: {
...mapState(useCatalog, {newArrivals: 'results'})
},
methods: {
...mapActions(useCatalog, ['fetchNewArrivals']),
addToCart() {
// we'll populate this later
}
},
created() {
// when the template is created, we call this action
this.fetchNewArrivals();
}
};
</script>
Home.vue 組件將會(huì)變成:
<script setup>
import TopNavbar from '../../components/TopNavbar.vue';
import NewArrivals from '../../components/NewArrivals.vue';
</script>
<template>
<TopNavbar />
<NewArrivals />
</template>
<style></style>
<script>
export default {
components: {
TopNavbar,
NewArrivals
},
computed: {},
mounted() {},
data() {
return {};
},
};
</script>
下面是store和組件如何在應(yīng)用程序中協(xié)同工作的圖示:
我還為購(gòu)物車寫了一個(gè)store和一個(gè)組件,但我不會(huì)把它包含在教程中,因?yàn)闄C(jī)制是相似的。你可以在倉(cāng)庫(kù)中檢查源代碼,它包括了所有東西,甚至包括了一些樣式。
步驟三:測(cè)試Vue.js組件
組件測(cè)試是UI測(cè)試中的一種。在這種測(cè)試中,組件被獨(dú)立地進(jìn)行渲染,沒(méi)有其他的應(yīng)用組件,目的是為了驗(yàn)證其功能。它通常是發(fā)生在端到端測(cè)試步驟之前的一種測(cè)試策略,我們將在下一小節(jié)進(jìn)行闡述。
我們需要安裝Vue Test Utils ,它是Vue.js的官方單元測(cè)試庫(kù)。我們需要的是針對(duì)于Vue 3的那個(gè)版本。你可以從NPM上面進(jìn)行安裝:
npm install @vue/test-utils@next --save-dev
安裝Nightwatch.js和ChromeDriver
我們將使用Nightwatch.js ,用于組件測(cè)試和端到端測(cè)試。Nightwatch已經(jīng)是Vue.js團(tuán)隊(duì)推薦的測(cè)試框架之一,與Vue同一時(shí)間發(fā)布。
它最近通過(guò)vite-plugin-nightwatch[5]獲得了對(duì)Vue組件測(cè)試的支持。我們將繼續(xù)安裝Nightwatch v2.0。
npm install nightwatch --save-dev
我們還需要安裝剛才提到的插件vite-plugin-nightwatch :
npm install vite-plugin-nightwatch --save-dev
Nightwatchs使用 W3C WebDriver API[6] 進(jìn)行瀏覽器自動(dòng)化任務(wù),我們也需要安裝chromedriver NPM包。因?yàn)槲覀儗⒁褂肅hrome來(lái)運(yùn)行我們的測(cè)試用例。
npm install chromedriver --save-dev
測(cè)試<NewArrivals>組件
vite-plugin-nightwatch包含了一個(gè)測(cè)試渲染頁(yè)面,Nightwatch已經(jīng)包含了為我們的組件運(yùn)行初始化測(cè)試所需的一切。
創(chuàng)建test文件夾,里面包含兩個(gè)子文件:
-
component- 這將進(jìn)行組件測(cè)試 -
e2e- 這將進(jìn)行端到端測(cè)試
我們還需要nightwatch.conf.js配置文件,但是我們可以直接運(yùn)行Nightwatch ,將會(huì)自動(dòng)為我們創(chuàng)建配置文件。因此只需要確保chromedriver已經(jīng)被安裝。
確保當(dāng)前的工作目錄是項(xiàng)目的根目錄,然后簡(jiǎn)單地運(yùn)行一個(gè)與Nightwatch捆綁的測(cè)試實(shí)例。我們將選擇duckDuckGo測(cè)試,因?yàn)樗亲羁斓模?/p>
npx nightwatch examples/tests/duckDuckGo.js
現(xiàn)在項(xiàng)目結(jié)構(gòu)看起來(lái)長(zhǎng)這樣:
vue-bookstore/
├── public/
| ├── data/
| └── favicon.ico
├── src/
├── ...
| └── main.js
├── test/
| ├── component/
| └── e2e/
├─── nightwatch.conf.js
├─── package.json
├─── README.md
└─── vite.config.js
我們?cè)?code style="overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(145, 109, 213);font-weight: bolder;background-image: none;background-position: initial;background-size: initial;background-repeat: initial;background-attachment: initial;background-origin: initial;background-clip: initial;">test/component目錄里面創(chuàng)建一個(gè)新文件叫做newArrivalsTest.js 。在這個(gè)js文件里,我們將添加一個(gè)基礎(chǔ)的掛載組件的測(cè)試用例,檢查返回的元素是否可以在頁(yè)面中找到。
describe('New Arrivals Component Test', function() {
it('checks if the component has been mounted', async (browser) => {
const component = await browser.mountVueComponent('/src/components/new-arrivals/NewArrivals.vue', {
plugins: {
router: '/src/lib/router.js'
}
});
expect(component).to.be.present;
});
});
Nightwatch和Mocha一樣,使用相同的describe() 語(yǔ)法。如果你已經(jīng)很熟悉Mocha,你甚至可以使用它作為測(cè)試運(yùn)行器,但我們現(xiàn)在不打算這么做。
是時(shí)候運(yùn)行上述測(cè)試用例了,為此我們將使用Chrome瀏覽器運(yùn)行Nightwatch,就像這樣:
npx nightwatch test/component/newArrivalsTest.js --env chrome
這將打開(kāi)Chrome瀏覽器,并且渲染這個(gè)組件,然后執(zhí)行測(cè)試用例。如果你不喜歡在測(cè)試過(guò)程中看到彈出的瀏覽器窗口,你可以傳入--headless參數(shù),就像這樣。
npx nightwatch test/component/newArrivalsTest.js --env chrome --headless
測(cè)試的輸出如下所示:
[New Arrivals Component Test] Test Suite
──────────────────────────────────────────────────────────────
? Connected to ChromeDriver on port 9515 (652ms).
Using: chrome (97.0.4692.99) on MAC OS X.
Running tests the component:
──────────────────────────────────────────────────────────────
? Expected element <web element{e53f9b1e-11d3-4dc4-8728-4d3cd077343e}> to be present (1ms)
OK. 1 assertions passed. (781ms)
當(dāng)然,你可以通過(guò)以下方式來(lái)查閱Nightwatch運(yùn)行器提供的所有CLI選項(xiàng):訪問(wèn)文檔頁(yè)面[7]或運(yùn)行以下命令:
npx nightwatch --help
擴(kuò)展<NewArrivals>測(cè)試
你可能已經(jīng)注意到,我們的組件測(cè)試并沒(méi)有測(cè)試很多東西,這意味著該測(cè)試并不像它能提供的那樣有用。所以我們要繼續(xù)對(duì)它進(jìn)行一點(diǎn)點(diǎn)的擴(kuò)展。
我們只需要檢查NewArrivals 組件,并檢查是否有一個(gè)叫做newArrivals的屬性。該屬性在HTML中被用來(lái)渲染結(jié)果。
現(xiàn)在測(cè)試用例看起來(lái)是這樣的。我們重構(gòu)了組件掛載到before鉤子中,因此我們只能在測(cè)試內(nèi)部進(jìn)行檢查,也就是it代碼塊。expect(斷言)庫(kù)是由Nightwatch提供的,它是基于流行的、多功能的Chai.js斷言庫(kù)。關(guān)于如何使用 expect 的更多信息,詳見(jiàn) Nightwatch docs[8] 網(wǎng)站。
describe('New Arrivals Component Test', function() {
let component;
before(async () => {
component = await browser.mountVueComponent('/src/components/new-arrivals/NewArrivals.vue', {
plugins: {
router: '/src/lib/router.js'
}
})
});
it('checks if the component has been mounted', function(browser) {
expect(component).to.be.present;
expect(component).to.have.property('newArrivals');
expect(component).text.toContain('The Memory Police')
expect.elements('div.col-md-6').count.toEqual(4); expect(component.property('newArrivals')).to.be.an('array').with.length(1);
});
});
步驟四:添加端到端測(cè)試
我們已經(jīng)接近本教程的尾聲,在我們認(rèn)為擁有一個(gè)可以運(yùn)行的Vue.js app之前,我們需要添加對(duì)端到端測(cè)試的支持,并在Github Actions上設(shè)置一個(gè)CI pipeline。
幸運(yùn)的是,我們不需要安裝、配置任何其他工具,除非是一些花里胡哨的報(bào)告器。但現(xiàn)在我們可以從Nightwatch中獲得我們所需要的一切端到端的自動(dòng)化測(cè)試。除了Chrome瀏覽器,Nightwatch也內(nèi)置支持所有主流瀏覽器,包括 Firefox、Edge、Safari。這都要?dú)w功于它與W3C Webdriver API和Selenium的整合。它還允許你使用分布式云測(cè)試平臺(tái),比如BrowserStack[9]、SauceLabs[10]、 CrossBrowserTesting[11] 、LambdaTest[12]等等。
編寫Homepage端到端測(cè)試
我們從homepage端到端測(cè)試開(kāi)始。創(chuàng)建一個(gè)新文件,位于test/e2e/homePageTest.js 。語(yǔ)法和組件測(cè)試的語(yǔ)法相同,但為了運(yùn)行端到端測(cè)試,我們將使用應(yīng)用程序的編譯版本。
我們當(dāng)然可以在開(kāi)發(fā)環(huán)境中運(yùn)行這些測(cè)試。但據(jù)我所知,軟件開(kāi)發(fā)中約定俗成的做法是,在一個(gè)盡可能模擬生產(chǎn)的環(huán)境中運(yùn)行端到端測(cè)試。這也是為什么它們被稱為端到端測(cè)試。
運(yùn)行生產(chǎn)構(gòu)建
為了運(yùn)行生產(chǎn)構(gòu)建,我們有幾個(gè)選項(xiàng),每個(gè)選項(xiàng)都涉及到運(yùn)行Vite命令,它被含在NPM任務(wù)中。
-
npm run build- 這將生成index.html以及其他靜態(tài)資源。如果你已經(jīng)有本地配置好的web server,你可以使用這個(gè)選項(xiàng)。 -
npm run preview- 這將生成生產(chǎn)構(gòu)建版本,并使用內(nèi)置的dev server運(yùn)行它。默認(rèn)地址是http://localhost:5000/[13]。
第二個(gè)選項(xiàng)更加簡(jiǎn)單直接,所以我們直接運(yùn)行preview命令,看看會(huì)發(fā)生什么:
$ npm run preview
> [email protected] preview /Users/andrei/workspace/vue-bookstore
> vite preview
> Local: <http://localhost:5000/>
> Network: use `--host` to expose
編寫測(cè)試腳本
現(xiàn)在,我們有一個(gè)生產(chǎn)就緒的構(gòu)建版本正在運(yùn)行,我們可以在test/e2e/homePageTest.js中開(kāi)始編寫真正的測(cè)試用例。我們從小處著手,只寫以下內(nèi)容:
describe('Homepage End-to-end Test', () => {
it('tests if homepage is loaded', browser => {
browser
.navigateTo('<http://localhost:3000>')
.assert.visible('#app .new-arrivals-panel')
.expect.elements('#app .new-arrivals-panel .col-md-6').count.toEqual(4)
});
it('adds 2 volumes of "Rhinoceros and Other Plays" to cart', browser => {
browser
.click('.new-arrivals-panel .col-md-6:nth-child(2) button.add-to-cart')
.click('.new-arrivals-panel .col-md-6:nth-child(2) button.add-to-cart')
.assert.textEquals('.shopping-cart .badge', '2');
});
after(browser => browser.end());
});
該測(cè)試驗(yàn)證了New Arrivals面板是否顯示在頁(yè)面中,以及它是否包含我們已經(jīng)看到的4個(gè)入口。
在Chrome中運(yùn)行測(cè)試腳本
在Chrome中運(yùn)行測(cè)試腳本的命令,與運(yùn)行組件測(cè)試用例的命令非常相似:
npx nightwatch test/e2e/homePageTest.js --env chrome
輸出如下所示:
[Homepage End-to-end Test] Test Suite
──────────────────────────────────────────────────────────────
? Connected to ChromeDriver on port 9515 (2454ms).
Using: chrome (97.0.4692.99) on MAC OS X.
Running tests the homepage:
──────────────────────────────────────────────────────────────
? Testing if element <#app .new-arrivals-panel> is visible (157ms)
? Expected elements <#app .new-arrivals-panel .col-md-6> count to equal: "4" (18ms)
OK. 2 assertions passed. (765ms)
在Firefox中運(yùn)行測(cè)試腳本
如果我們想要在Firefox瀏覽器中運(yùn)行端到端測(cè)試,我們只需要安裝GeckoDriver[14]。除非你想進(jìn)一步定制,否則不需要其他配置就可以工作。
讓我們繼續(xù),使用NPM來(lái)進(jìn)行安裝:
npm i geckodriver --save-dev
然后使用下面的命令運(yùn)行Nightwatch :
npx nightwatch test/e2e/homePageTest.js --env firefox
輸出如下所示:
[Homepage End-to-end Test] Test Suite
──────────────────────────────────────────────────────────────
? Connected to GeckoDriver on port 4444 (1737ms).
Using: firefox (96.0.2) on MAC (20.6.0).
Running tests the homepage:
──────────────────────────────────────────────────────────────
? Testing if element <#app .new-arrivals-panel> is visible (54ms)
? Expected elements <#app .new-arrivals-panel .col-md-6> count to equal: "4" (6ms)
OK. 2 assertions passed. (612ms)
在Safari中運(yùn)行測(cè)試腳本
如果你在使用Mac,那么safaridriver可能已經(jīng)安裝了,這取決于你的Safari版本。
可以使用下面命令進(jìn)行檢查:
safaridriver --help
輸出可能長(zhǎng)這樣:
Usage: safaridriver [options]
-h, --help Prints out this usage information.
--version Prints out version information and exits.
-p, --port Port number the driver should use. If the server is already running, the port cannot be changed. If port 0 is specified, a default port will be used.
--enable Applies configuration changes so that subsequent WebDriver sessions will run without further authentication.
--diagnose Causes safaridriver to log diagnostic information for all sessions hosted by this instance. See the safaridriver(1) man page for more details about diagnostic logging.
在Safari中運(yùn)行你的第一個(gè)測(cè)試之前,你只需要通過(guò)以下命令啟用自動(dòng)化:
safaridriver --enable
然后使用下面命令簡(jiǎn)單的運(yùn)行Nightwatch測(cè)試:
npx nightwatch test/e2e/homePageTest.js --env safari
在多個(gè)瀏覽器中并行運(yùn)行
如果你需要在一個(gè)以上的瀏覽器中運(yùn)行Nightwatch測(cè)試,你可以在多個(gè)瀏覽器中并行運(yùn)行。
只需將瀏覽器作為逗號(hào)分隔的列表(沒(méi)有空格)進(jìn)行傳遞。
「在Firefox+Chrome中運(yùn)行」
npx nightwatch test/e2e/homePageTest.js --env firefox,chrome
「在Firefox+Chrome+Safari中運(yùn)行」
npx nightwatch test/e2e/homePageTest.js --env firefox,chrome,safari
更多關(guān)于并行測(cè)試的內(nèi)容,請(qǐng)查看Nightwatch docs[15]網(wǎng)站。
步驟五:使用Github Actions啟用持續(xù)集成(CI)
是時(shí)候進(jìn)行收尾工作,將他們放在一起了。在Github Actions中啟用持續(xù)部署(CD)之前,我們需要?jiǎng)?chuàng)建npm test任務(wù)。
創(chuàng)建npm test任務(wù)
現(xiàn)在我們已經(jīng)在示例項(xiàng)目中具備組件測(cè)試和端到端測(cè)試。當(dāng)然這只是一個(gè)最低水平,所以它沒(méi)有涵蓋所有內(nèi)容,但我認(rèn)為這是一個(gè)良好的開(kāi)端。
告訴Nightwatch運(yùn)行測(cè)試文件夾中的所有測(cè)試的最簡(jiǎn)單方法是,將文件夾作為第二個(gè)CLI參數(shù)。我們將要添加該命令到作為一個(gè)新的被稱為test的NPM任務(wù)中。讓我們編輯package.json文件,在 "scripts "字段中添加以下內(nèi)容:
"test": "nightwatch ./test"
我們可以像這樣來(lái)運(yùn)行NPM任務(wù),并傳遞Nightwatch相關(guān)的CLI參數(shù):
npm test -- --env chrome --headless
為了能在Github Actions中運(yùn)行測(cè)試用例,我們將使用--headless模式。
添加Github Actions工作流
最后,我們可以添加Github Actions工作流。這樣我們的測(cè)試就可以運(yùn)行在每個(gè)推送和每個(gè)拉動(dòng)請(qǐng)求上。
想要做到上述流程非常簡(jiǎn)單。我們將使用Node.js模板,在列表中添加幾個(gè)新的步驟(step),比如:
-
在后臺(tái)啟動(dòng)dev server -
在后臺(tái)構(gòu)建項(xiàng)目并在預(yù)覽模式下啟動(dòng)dev server -
在Chrome中使用無(wú)頭模式運(yùn)行組件以及端到端測(cè)試
創(chuàng)建Github Actions工作流程意味著,在.github/workflows文件夾中添加一個(gè)名為node.js.yml的新文件,內(nèi)容如下所示。當(dāng)你從Github項(xiàng)目導(dǎo)航到Actions部分并選擇Node.js模板時(shí),其中大部分內(nèi)容都是自動(dòng)生成的。
name: Node.js CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x, 14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- name: Start vite dev server
run: npm run dev &
- name: Build the app
run: npm run build
- name: Start vite dev server in preview
run: npm run preview &
- name: Run Nightwatch tests
run: npm test
這樣就可以了。每當(dāng)有新的git推送或新的PR被發(fā)送時(shí),就會(huì)運(yùn)行一個(gè)新的構(gòu)建。構(gòu)建將在2個(gè)獨(dú)立的環(huán)境中運(yùn)行,一個(gè)是Node 12,另一個(gè)是Node 14,如工作流中定義的那樣。
今后的發(fā)展方向
該項(xiàng)目在Github上的網(wǎng)址是:https://github.com/beatfactor/middlemarch[16]。這里涵蓋了所有的代碼,還有一些樣式和圖片。它還包含了購(gòu)物車的代碼和一個(gè)模擬的結(jié)賬頁(yè)面。
你可以通過(guò)常規(guī)步驟讓它在你的本地機(jī)器上運(yùn)行:
git clone <https://github.com/beatfactor/middlemarch>
npm install
npm run dev
歡迎發(fā)送PR或報(bào)告問(wèn)題。
參考資料
https://labs.pineview.io/learn-how-to-build-test-and-deploy-a-single-page-app-with-vue-3-vite-and-pinia/: https://labs.pineview.io/learn-how-to-build-test-and-deploy-a-single-page-app-with-vue-3-vite-and-pinia/
[2]Andrei Rusu: https://labs.pineview.io/author/andrei/
[3]https://github.com/beatfactor/middlemarch: https://github.com/beatfactor/middlemarch
[4]localhost:3000: http://localhost:3000
[5]vite-plugin-nightwatch: https://www.npmjs.com/package/vite-plugin-nightwatch
[6]W3C WebDriver API: https://w3c.github.io/webdriver/
[7]文檔頁(yè)面: https://nightwatchjs.org/guide/running-tests/command-line-options.html
[8]Nightwatch docs: https://nightwatchjs.org/api/expect/
[9]BrowserStack: https://www.browserstack.com/
[10]SauceLabs: https://saucelabs.com/
[11]CrossBrowserTesting: https://crossbrowsertesting.com/
[12]LambdaTest: https://www.lambdatest.com/
[13]http://localhost:5000/: http://localhost:5000/
[14]GeckoDriver: https://github.com/mozilla/geckodriver
[15]Nightwatch docs: https://v2.nightwatchjs.org/guide/running-tests/parallel-running.html
[16]https://github.com/beatfactor/middlemarch: https://github.com/beatfactor/middlemarch
