前置
從建立一個簡單瀏覽器導航首頁專案展開,該篇隨筆包含以下內容的簡單上手:
- vite
- vue3
- vuex4
- vue-router next
預覽效果有助於理清這些內容,限於篇幅,不容易展開敘述。由於專案邏輯簡單,只使用了少量 API,我只是寫這個小專案過把手癮,所以對應標題 上手。如果您只是想學習 vue 周邊的 API,那麼,這篇文章將給您帶來有限的知識。
初始化專案
使用 vite 初始化 vue3 專案。什麼是 vite?Vite 是一個 Web 構建工具。開發過程中通過瀏覽器 ES Module 匯入為您的程式碼提供服務,生成環境與 Rollup 捆綁在一起進行打包。
特性:
- 閃電般快速的冷伺服器啟
- 動即時熱模組更換(HMR)
- 真正的按需編譯
vite 截至今天支援的功能:
- Bare Module Resolving
- Hot Module Replacement
- TypeScript
- CSS / JSON Importing
- Asset URL Handling
- PostCSS
- CSS Modules
- CSS Pre-processors
- JSX
- Web Assembly
- Inline Web Workers
- Custom Blocks
- Config File
- HTTPS/2
- Dev Server Proxy
- Production Build
- Modes and Environment Variables
npm init vite-app aweshome
npm install
npm run dev
npm run build
最終生成的目錄結構與使用 vue-cli 相似:
│ .npmignore
│ a.txt
│ index.html
│ package.json
├─public
│ favicon.ico
└─src
│ App.vue
│ index.css
│ main.js
├─assets
│ logo.png
└─components
HelloWorld.vue
可以在專案根目錄下建立 vite.config.js 配置 Vite:
module.exports = {
// 匯入別名
// 這些條目可以是精確的請求->請求對映*(精確,無萬用字元語法)
// 也可以是請求路徑-> fs目錄對映。 *使用目錄對映時
// 鍵**必須以斜槓開頭和結尾**
alias: {
// 'react': '@pika/react',
// 'react-dom': '@pika/react-dom'
// '/@foo/': path.resolve(__dirname, 'some-special-dir'),
},
// 配置Dep優化行為
optimizeDeps: {
// exclude: ['dep-a', 'dep-b'],
},
// 轉換Vue自定義塊的功能。
vueCustomBlockTransforms: {
// i18n: src => `export default Comp => { ... }`,
},
// 為開發伺服器配置自定義代理規則。
proxy: {
// proxy: {
// '/foo': 'http://localhost:4567/foo',
// '/api': {
// target: 'http://jsonplaceholder.typicode.com',
// changeOrigin: true,
// rewrite: path => path.replace(/^\/api/, ''),
// },
// },
},
// ...
}
更多配置可以參考Github。
另外,現在可以使用 vitepress 代替原來的 vuepress 構建文件或部落格。
vue-router next
npm i vue-router@next
src/router/index.js
import {createRouter, createWebHistory} from 'vue-router'
import Home from '../components/home/Home.vue'
import Cards from '../components/cards/Cards.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
// route -> routes
{
path: '/',
name: 'home',
component: Home,
},
{
path: '/cards',
name: 'cards',
component: Cards,
},
],
})
export default router
vue router next 還新增了動態路由,解決規則衝突的問題。做過許可權管理應該深有體會。更多配置可以參考 Github。
vuex4
使用與 vuex3 相同的 API。
安裝
npm i vuex@next
src/constants 下存放了靜態資料,它們都是如下形式:
export const vue = [
{
title: 'vue',
desc: 'Vue 是用於構建使用者介面的漸進式的框架',
link: 'https://cn.vuejs.org/v2/guide/',
img: import('../assets/images/vue.png'), // require -> import
},
{
title: 'vue Router',
desc: 'Vue Router 是 Vue.js 官方的路由管理器。',
link: 'https://router.vuejs.org/zh/',
img: import('../assets/images/vue.png'),
},
// ...
]
src/store/index.js
import {createStore} from 'vuex'
import {vue, react, wechat, across, compileBuild} from '../constants/docs'
import {frontEndTools, OfficeTools} from '../constants/tools'
import {tools, docs, community} from '../constants/asideData'
import {blogs} from '../constants/community'
const store = createStore({
state: {
asideData: [],
mainData: [],
},
mutations: {
setAsideData(state, key) {
const asideActions = {
'2': tools,
'3': docs,
'4': community,
}
state.asideData = asideActions[key]
},
setMainData(state, menuItemText) {
const actions = new Map([
['前端工具', frontEndTools],
['辦公工具', OfficeTools],
['vue', vue],
['react', react],
['微信開發', wechat],
['跨端框架', across],
['編譯構建', compileBuild],
['部落格', blogs],
])
state.mainData = actions.get(menuItemText)
},
},
actions: {},
modules: {},
})
export default store
main.js
結合上文的 vuex vue-router 可以看出,vue3 核心外掛的 api 都做了簡化。
import './index.css'
import {createApp} from 'vue'
import store from './store'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(store)
app.use(router)
app.mount('#app')
sass
npm i sass
package.json > dependencies
{
"dependencies": {
"vue": "^3.0.0-beta.15",
"vue-router": "^4.0.0-alpha.13",
"vuex": "^4.0.0-beta.2"
},
"devDependencies": {
"@vue/compiler-sfc": "^3.0.0-beta.15",
"sass": "^1.26.8",
"vite": "^1.0.0-beta.1"
}
}
components
這個小專案本質上可以只有一個頁面 .vue
構成,我將它拆分,便於閱讀。
App.vue
<template>
<Header />
<main>
<router-view></router-view>
</main>
<Footer />
</template>
<script>
import Header from './components/Header.vue'
import Footer from './components/Footer.vue'
export default {
name: 'app',
components: {
Header,
Footer,
},
}
</script>
<style>
main {
flex: 1;
}
</style>
components/cards/Aside.vue
<template>
<aside>
<ul>
<li :index="item.index" v-for="item in this.$store.state.asideData" :key="item.index" ref="menuItem" @click="handleSelect(item.value)">
<i class="fas fa-home"></i>
<span>{{ item.value }}</span>
</li>
</ul>
</aside>
</template>
<script>
import store from '../../store'
export default {
setup(props, context) {
return {
handleSelect(value) {
store.commit('setMainData', value)
},
}
},
}
</script>
<style lang="scss">
aside {
flex: 1;
background-color: rgb(238, 238, 238);
height: 100%;
li {
display: flex;
align-items: center;
height: 56px;
line-height: 56px;
font-size: 14px;
color: #303133;
padding: 0 1.4rem;
list-style: none;
cursor: pointer;
transition: border-color 0.3s, background-color 0.3s, color 0.3s;
white-space: nowrap;
&:hover {
background-color: rgb(224, 224, 224);
}
}
}
@media screen and (max-width: 768px) {
aside {
display: none;
&.active {
display: block;
}
}
}
</style>
components/cards/Cards.vue
<template>
<div id="card-outer">
<Aside />
<section></section>
</div>
</template>
<script>
import Aside from './Aside.vue'
import router from '../../router'
export default {
components: {
Aside,
},
}
</script>
<style lang="scss">
#card-outer {
display: flex;
align-content: stretch;
height: 100%;
& > section {
flex: 8;
}
}
.main-card {
margin: 10px 0;
cursor: pointer;
.main-card-content {
display: flex;
align-items: center;
img {
width: 30px;
height: 30px;
margin-right: 10px;
}
.main-card-content-info {
width: 90%;
h3 {
font-size: 14px;
}
p {
font-size: 12px;
color: #888ea2;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 100%;
line-height: 1.8;
}
}
span {
margin-left: 10px;
text-decoration: none;
&:nth-of-type(1) {
font-size: 18px;
font-weight: 700;
color: #ffa502;
white-space: nowrap;
}
&:nth-of-type(2) {
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
</style>
components/home/Home.vue
<template>
<section id="search">
<div class="search-sources" style="margin-bottom: 10px;">
<span size="mini" type="primary" v-for="(item, index) in source" @click="changeSource(item.name)" :key="index" :style="`background:${item.color};border-color:${item.color}`"
>{{ item.name }}
</span>
</div>
<div class="searchbox" :class="searchbarStyle.className">
<input :placeholder="searchbarStyle.placeholder" v-model="searchValue" clearable v-on:keyup.enter="submit" />
<button @click="submit" slot="append" icon="el-icon-search">
<i class="fas fa-search"></i>
</button>
</div>
</section>
</template>
<script>
export default {
data: () => ({
baseUrl: 'https://www.baidu.com/s?ie=UTF-8&wd=',
searchValue: '',
searchbarStyle: {
className: 'baidu',
placeholder: '百度一下,你就知道',
},
source: [
{
name: '百度',
color: '#2932E1',
},
{
name: '搜狗',
color: '#FF6F17',
},
{
name: 'Bing',
color: '#0c8484',
},
{
name: 'Google',
color: '#4285F4',
},
{
name: 'NPM',
color: '#EA4335',
},
],
}),
methods: { // 可以在 vue3 中使用 options API
changeSource(name) {
const actions = new Map([
[
'百度',
() => {
this.baseUrl = 'https://www.baidu.com/s?ie=UTF-8&wd='
this.searchbarStyle = {
className: 'baidu',
placeholder: '百度一下,你就知道',
}
},
],
[
'Bing',
() => {
this.baseUrl = 'https://cn.bing.com/search?FORM=BESBTB&q='
this.searchbarStyle = {
className: 'bing',
placeholder: '必應搜尋',
}
},
],
[
'搜狗',
() => {
this.baseUrl = 'https://www.sogou.com/web?query='
this.searchbarStyle = {
className: 'sougou',
placeholder: '搜狗搜尋',
}
},
],
[
'Google',
() => {
this.baseUrl = 'https://www.google.com/search?q='
this.searchbarStyle = {
className: 'google',
placeholder: 'Google Search',
}
},
],
[
'NPM',
() => {
this.baseUrl = 'https://www.npmjs.com/search?q='
this.searchbarStyle = {
className: 'npm',
placeholder: 'Search Packages',
}
},
],
])
actions.get(name)()
},
submit() {
const url = this.baseUrl + this.searchValue
window.open(url)
},
},
}
</script>
<style lang="scss">
#search {
display: flex;
flex-direction: column;
justify-content: center;
align-content: stretch;
margin: 0 auto;
height: 40vh;
width: 40%;
& > div {
display: flex;
}
}
.search-sources {
span {
margin-right: 0.5rem;
padding: 0.4rem 0.6rem;
color: #fff;
font-size: 14px;
line-height: 14px;
border-radius: 2px;
&:hover {
filter: contrast(80%);
transition: 0.3s;
}
}
}
.searchbox {
padding-left: 1rem;
height: 2.6rem;
border-radius: 6px;
background-color: #fff;
border: 1px #ccc solid;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
input {
flex: 7;
border: none;
font-size: 1rem;
}
button {
flex: 1;
i {
margin-right: 0;
}
}
}
$sources-color: (
baidu: #2932e1,
bing: #0c8484,
sougou: #ff6f17,
google: #4285f4,
npm: #ea4335,
);
$source-list: baidu bing sougou google npm;
@each $source in $source-list {
.#{$source} {
&:hover {
border-color: map-get($sources-color, $source);
box-shadow: 0 2px 4px map-get($sources-color, $source);
transition: all 0.5s;
}
input {
&:hover {
border-color: map-get($sources-color, $source);
}
}
}
}
@media screen and (max-width: 768px) {
#search {
width: 90%;
}
}
</style>
components/Header.vue
<template>
<header>
<ul class="nav">
<li @click="handleSelect('home')">
<i class="fas fa-home"></i>
<span>首頁</span>
</li>
<li @click="handleSelect('tools')">
<i class="fas fa-tools"></i>
<span>工具</span>
</li>
<li @click="handleSelect('docs')">
<i class="fas fa-file-alt"></i>
<span>文件</span>
</li>
<li @click="handleSelect('community')">
<i class="fas fa-comment-dots"></i>
<span>社群</span>
</li>
</ul>
<MobileMenu />
</header>
</template>
<script>
import MobileMenu from './MobileMenu.vue'
import store from '../store'
import router from '../router'
export default {
components: {
MobileMenu,
},
setup() {
const handleSelect = item => {
store.commit('setAsideData', item)
if (item === 'home') {
router.replace({name: 'home'})
} else {
const actions = {
tools: ['setMainData', '前端工具'],
docs: ['setMainData', 'vue'],
community: ['setMainData', '部落格'],
}
store.commit(actions[item][0], actions[item][1])
router.replace({name: 'cards'})
}
}
return {
handleSelect,
}
},
}
</script>
<style lang="scss">
header {
display: flex;
height: 60px;
align-content: stretch;
padding: 0 9.5rem;
}
.nav {
display: flex;
align-items: center;
align-content: stretch;
li {
padding: 0.5rem 0.75rem;
&:hover {
background-color: #f3f1f1;
& span {
color: #3273dc;
}
}
}
}
@media screen and (max-width: 768px) {
header {
padding: 0;
}
}
</style>
components/MobileMenu.vue
<template>
<section id="mobile-menu">
<div id="navbarBurger" class="navbar-burger burger" data-target="navMenuMore" :class="{active}" @click="sideToggle">
<span></span>
<span></span>
<span></span>
</div>
</section>
</template>
<script>
export default {
data: () => ({
active: false,
}),
methods: {
sideToggle() {
this.active = !this.active
const classList = document.querySelectorAll('aside')[0].classList
this.active ? classList.add('active') : classList.remove('active')
},
},
}
</script>
<style lang="scss">
#mobile-menu {
display: none;
position: absolute;
right: 0;
top: 0;
z-index: 999999;
}
@media screen and (max-width: 768px) {
#mobile-menu {
display: block;
.navbar-burger {
position: relative;
color: #835656;
cursor: pointer;
height: 60px;
width: 60px;
margin-left: auto;
span {
background-color: #333;
display: block;
height: 1px;
left: calc(50% - 8px);
position: absolute;
transform-origin: center;
transition-duration: 86ms;
transition-property: background-color, opacity, transform;
transition-timing-function: ease-out;
width: 16px;
&:nth-child(1) {
top: calc(50% - 6px);
}
&:nth-child(2) {
top: calc(50% - 1px);
}
&:nth-child(3) {
top: calc(50% + 4px);
}
}
&.active {
span {
&:nth-child(1) {
transform: translateY(5px) rotate(45deg);
}
&:nth-child(2) {
opacity: 0;
}
&:nth-child(3) {
transform: translateY(-5px) rotate(-45deg);
}
}
}
}
}
}
</style>
最後
一套流程下來,vite 給我的感覺就是“快”。對於 vue 周邊, API 都是做了一些簡化,如果你對 esm 有些瞭解,將更有利於組織專案,可讀性相比 vue2.x 也更高。也有一些問題,限於篇幅,本文沒有探討。做專案還是上 vue2.x 及其周邊。另外,我沒找到 vue3 元件庫。