創(chuàng)建應用
$ node -v
v12.14.0
$ npm -v
6.13.4
$ npm i -g @vue/cli
$ vue -V
@vue/cli 4.1.2
$ vue create stats
創(chuàng)建Vue項目選擇基礎預設,其中包括babel和eslint插件。
文件結(jié)構
文件 | 描述 |
---|---|
public | 靜態(tài)資源文件夾 |
src | 項目源代碼包括目錄 |
src/config | 應用配置文件夾,推薦使用JSON格式。 |
src/utils | 工具類庫文件夾 |
src/assets | 靜態(tài)資源文件夾 |
src/components | 組件文件夾,根據(jù)單頁劃分模塊,一個單頁一個文件夾,保存單頁所使用的組件。 |
src/entry | 多頁入口文件夾,主要是JS文件。 |
src/pages | 多頁文件夾,主要是vue文件。 |
src/router | 路由文件夾,每個單頁一個路由。 |
項目文件結(jié)構說明,為合理規(guī)劃文件組織結(jié)構。會將MPA應用中每個page的.html
、.js
、.vue
三個文件分別劃分到public
、src/entry
、src/pages
文件夾下。
應用配置
創(chuàng)建vue核心配置
$ cd stats
$ vim vue.config.js
安裝組件
$ npm i -S path
const path = require("path");
const utils = require("./src/utils/utils");
//是否開發(fā)調(diào)試模式
const debug = process.env.NODE_ENV === "development" ? true : false;
module.exports = {
publicPath:debug?"/":"",
outputDir:"dist",
assetsDir:"assets",
filenameHashing:true,
lintOnSave:!debug,
runtimeCompiler:!debug,
pages:utils.getPages(),
configureWebpack:config=>{
const extensions = [".js", ".json", ".vue", ".css"];
const alias = {
"@":path.join(__dirname, "src"),
"src":path.join(__dirname, "../src"),
"assets":path.join(__dirname, "../src/assets"),
"components":path.join(__dirname, "../src/components")
};
config.resolve = {extensions, alias};
}
};
pages選項
vue核心配置項中的pages選項為多頁應用MPA的配置位置,提取出來放到工具類庫utils/utils.js
文件中。
pages配置是Object
類型,默認值為undefined
,在multi-page
模式下構建應用。每個page對應一個JavaScript入口文件。
pages的值是一個對象,對象的key為page單頁入口名稱,value是一個指定entry、template、filename、title、chunks的對象。其中entry為必填且為字符串。
page選項
page選項 | 必填 | 描述 |
---|---|---|
entry | 是 | page入口JS文件路徑 |
template | 否 | page模板文件路徑 |
filename | 否 | 輸出到dist目錄中的文件名 |
title | 否 | 頁面title標簽內(nèi)容 |
chunks | 否 | page中包含的塊,默認會提取通用塊。 |
示例代碼
module.exports = {
pages: {
index: {
// page 的入口
entry: 'src/index/main.js',
// 模板來源
template: 'public/index.html',
// 在 dist/index.html 的輸出
filename: 'index.html',
// 當使用 title 選項時,
// template 中的 title 標簽需要是 <title><%= htmlWebpackPlugin.options.title %></title>
title: 'Index Page',
// 在這個頁面中包含的塊,默認情況下會包含
// 提取出來的通用 chunk 和 vendor chunk。
chunks: ['chunk-vendors', 'chunk-common', 'index']
},
// 當使用只有入口的字符串格式時,
// 模板會被推導為 `public/subpage.html`
// 并且如果找不到的話,就回退到 `public/index.html`。
// 輸出文件名會被推導為 `subpage.html`。
subpage: 'src/subpage/main.js'
}
}
根據(jù)page
選項,并配合項目組織結(jié)構,將每個page
的entry
入口文件都保存到src/entry
文件夾下,入口文件均為JS文件,template
模板文件均使用public/index.html
,title
標簽內(nèi)容每個頁面均不一樣,后續(xù)會進行處理,默認使用入口名稱。這些內(nèi)容均會在提取到工具類庫src/utils/utils.js
文件的getPages()
方法中。
工具類庫
創(chuàng)建工具類庫
$ vim src/utils/utils.js
安裝組件
$ npm i -S fs glob
const fs = require("fs");
const path = require("path");
const glob = require("glob");
const pagePath = path.resolve(__dirname, "..", "pages");
const entryPath = path.resolve(__dirname, "..", "entry");
const configPath = path.resolve(__dirname, "..", "config");
/*獲取配置*/
exports.config = (filename,field="")=>{
const file = path.join(configPath, filename);
let value = require(file);
if(field!==""){
value = value[field];
}
return value;
};
/*獲取多頁面配置選項*/
exports.getPages = ()=>{
let pages = {};
//獲取所有vue文件
let files = glob.sync(`${pagePath}/*/*.vue`);
if(files.length < 1){
console.error("util getPages no file");
}
files.forEach(filepath=>{
const extname = path.extname(filepath);
const basename = path.basename(filepath, extname);
//統(tǒng)一入口文件保存路徑
const entry = path.join(entryPath, `${basename}.js`);//絕對路徑
//自動生成入口文件
const exists = fs.existsSync(entry);
console.log(exists, entry);
if(!exists){
let code = `import Vue from 'vue';\n`;
code += `import App from '${filepath}';\n`;
code += `Vue.config.productionTip = false;\n`;
code += `new Vue({render:h=>h(App)}).$mount('#${basename}');`;
fs.writeFileSync(entry, code);
}
//頁面配置選項
const template = "index.html";
const filename = `${basename}.html`;
const chunks = ['chunk-vendors', 'chunk-common', basename];
const chunksSortMode = "manual";
const minify = false;
const inject = true;
//自定義頁面數(shù)據(jù)
const pageData = this.config("page", basename) || {};
if(pageData.title === undefined){
Object.assign(pageData, {title:basename});
}
if(pageData.idname === undefined){
Object.assign(pageData, {idname:basename});
}
pages[basename] = {entry, template, filename, pageData, chunks, chunksSortMode, minify, inject};
});
return pages;
};
getPages()
方法對vue.config.js
中的pages
參數(shù)進行提取并根據(jù)提前規(guī)劃好的結(jié)構進行組織文件,其中會判斷入口文件是否已經(jīng)存在,若不存在則會生成。
模板文件
$ vim public/index.html
<% const page = htmlWebpackPlugin.options.pageData; %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= page.title %></title>
</head>
<body>
<noscript>
<strong>很抱歉,如果沒有啟用javascript,vue-cli3無法正常工作。請啟用它以繼續(xù)。</strong>
</noscript>
<div id="app">
<div id="<%= page.idname %>"></div>
</div>
</body>
</html>
頁面配置
$ vim src/config/page.json
{
"index":{"title":"index page"}
}
單頁應用
多頁應用基礎結(jié)構搭建完畢后,接下來針對index單頁進行開發(fā)。每個單頁分為入口、路由、布局、模板、組件、樣式等一系列部分組成。
$ vim src/pages/index/index.vue
<template>
<div class="page">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: "index.vue"
}
</script>
<style scoped>
</style>
作為index單頁,默認會加載頁面布局組件,創(chuàng)建.vue
文件會自動生成基礎的入口文件。入口文件中會加載路由和UI組件。
入口文件
安裝組件
$ npm i -S vant less less-loader
項目使用vant作為UI組件,vant是有贊團隊基于有贊統(tǒng)一的規(guī)范實現(xiàn)的一個輕量、可靠的移動端Vue組件庫,用于移動端開發(fā)。由于vant使用less,因此需配置less和less-loader。
$ vim src/entry/index.js
import Vue from 'vue';
import Vant from "vant";
import router from "../router/index.js";
import app from '../pages/index/index.vue';
import layout from "../components/index/layout.vue";
// import "vant/lib/index.less";
Vue.config.productionTip = false;
Vue.use(Vant);
new Vue({
render:h=>h(app),
router:router,
components:{layout},
template:"<layout/>"
}).$mount('#index');
路由文件
安裝組件
$ npm i -S vue-router
$ vim src/router/index.js
import Vue from "vue";
import Router from "vue-router";
import layout from "../components/index/layout.vue";
Vue.use(Router);
const routes = [
{
path:"/index/layout",
name:"layout",
meta:{title:"index layout", requireAuth:false},
component:layout
}
];
export default new Router({
mode:"history",
base:process.env.BASE_URL,
routes
});
vant
Vant是由有贊前端團隊開發(fā)的一款輕量、可靠的移動端Vue組件庫。
安裝組件
$ npm i -S vant
$ yarn add vant
babel-plugin-import
引入組件
babel-plugin-import是一款babel插件,會在編譯過程中將import的寫法自動轉(zhuǎn)換為按需引入的方式。
使用vant組件可以一次性全局導入組件,但這種做法會帶來增加代碼包的體積,因此推薦使用babel-plugin-import進行按需加載,以節(jié)省資源。
$ npm i -D babel-plugin-import
$ npm i --save-dev babel-plugin-import
備注:若項目僅在開發(fā)環(huán)境下需要npm包而在上線后不需要,則是可使用--save-dev
或-D
。若上線后需要使用依賴包則需使用--save
或-S
。
查看babel-plugin-import版本
$ npm view babel-plugin-import version
1.13.0
$ npm ls babel-plugin-import
sxyh_web_stats@0.1.0 D:\vue\workspace\sxyh_web_stats
`-- babel-plugin-import@1.13.0
查看babel版本
$ npm info babel version
6.23.0
配置插件
$ vim babel.config.js
module.exports = {
presets: ['@vue/cli-plugin-babel/preset'],
plugins: [
['import', {
libraryName: 'vant',
libraryDirectory: 'es',
style: true
}, 'vant']
]
};
配置按需引入后將不再允許直接導入所有組件,雖然Vant支持一次性導入所有組件,但直接導入所有組件會增加代碼包體積,因此并不推薦這種做法。
按需引入后,在vue文件中使用組件時,首先需要導入所需使用的組件,然后在Vue中進行注冊組件。注冊組件后,才能使用vant提供的組件標簽。
$ vim home.vue
<template>
<div class="layout">
<van-nav-bar title="排行榜" left-text="返回" right-text="按鈕" left-arrow @click-left="onClickLeft" @click-right="onClickRight"/>
<van-image round width="5rem" height="5rem" src="https://img.yzcdn.cn/vant/cat.jpeg"/>
<van-panel title="我的昵稱" desc="ID:123456" status="第10名"></van-panel>
<van-list v-model="loading" :finished="finished" finished-text="沒有更多了" @load="onLoad">
<van-cell v-for="item in list" :key="item" :title="item" />
</van-list>
<van-tabbar v-model="active">
<van-tabbar-item icon="home-o">排行榜</van-tabbar-item>
<van-tabbar-item icon="search">積分</van-tabbar-item>
<van-tabbar-item icon="friends-o">茶館</van-tabbar-item>
<van-tabbar-item icon="setting-o">分組</van-tabbar-item>
</van-tabbar>
</div>
</template>
<script>
import {NavBar, Image, Panel, List, Cell, Tabbar, TabbarItem, Toast} from "vant";
export default {
name: "layout",
data(){
return {
active:0,
list:[],
loading:false,
finished:false
};
},
//注冊組件
components:{
[NavBar.name]:NavBar,
[Image.name]:Image,
[Panel.name]:Panel,
[List.name]:List,
[Cell.name]:Cell,
[Tabbar.name]:Tabbar,
[TabbarItem.name]:TabbarItem,
},
methods:{
goback(){
this.$router.go(-1);
},
onClickLeft(){
Toast("返回");
},
onClickRight(){
Toast("按鈕");
},
onLoad(){
setTimeout(()=>{
for(let i=0; i<10; i++){
this.list.push(this.list.length + 1);
}
this.loading = false;
if(this.list.length>=40){
this.finished = true;
}
},1000);
}
}
}
</script>
<style scoped>
</style>
postcss-px-to-viewport
移動端適配可采用viewport單位,由于viewport單位得到眾多瀏覽器的兼容,flexible的過渡方案可以放棄了。
viewport以vw和vh作為單位,以viewport為基準,其中1vw表示view width的1/100, 1vh表示 view height的1/100。
使用viewport單位需提前設置meta標簽中的viewport
<!-- 在 head 標簽中添加 meta 標簽,并設置 viewport-fit=cover 值 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover">
postcss-px-to-viewport 會將px單位自動轉(zhuǎn)化為視口單位vw、vh、vmin、vmax的PostCSS插件。
安裝組件
$ npm i -S postcss-loader postcss-px-to-viewport
@vue/cli3中由于postcss-px-to-viewport配置項中的exclude選項只能支持正則表達式,因此無法在package.json中進行配置,可配置在vue.config.js文件中。
$ vim vue.config.js
const pxtoviewport = require("postcss-px-to-viewport");
module.exports = {
css:{
loaderOptions:{
postcss:{
plugins:[
pxtoviewport({
unitToConvert:"px",
unitPrecision:3,
viewportWidth:750,
viewportUnit:"vw",
fontViewportUnit:"vw",
minPixelValue:1,
mediaQuery:false,
replace:true,
propList:["*"],
selectorBlackList:[],
exclude:/(\/|\\)(node_modules)(\/|\\)/
landscape:false,
landscapeUnit:"vh",
landscapeWidth:1334
})
]
}
}
}
};
配置項 | 配置值 | 描述 |
---|---|---|
unitToConvert | "px" | 需要轉(zhuǎn)換的單位,默認為"px"像素。 |
viewportWidth | 750 | 設計稿視口寬度,配置后將根據(jù)視口做比例換算。 |
unitPrecison | 2 | 轉(zhuǎn)化進度,轉(zhuǎn)換后保留小數(shù)位數(shù)。 |
propList | [] | 允許轉(zhuǎn)換為vw的屬性列表 |
viewportUnit | "vw" | 視口單位 |
fontViewportUnit | "vw" | 字體使用的視口單位 |
selectorBlackList | [] | 需忽略的CSS選擇器 |
minPixelValue | 1 | 最小轉(zhuǎn)換數(shù)值 |
mediaQuery | true | 媒體查詢中的單位是否需要轉(zhuǎn)換 |
replace | true | 轉(zhuǎn)換后是否需要添加備用單位 |
exclude | ["node_modules"] | 需要忽略的文件夾 |
landscape | false | 是否添加根據(jù)landscopeWidth生成媒體查詢條件@media(orientation:landscape) |
landscapeUnit | "vh" | 橫屏時使用的單位 |
landscapeWidth | 1334 | 橫屏時使用的視口寬度 |
fastclick
移動端開發(fā)存在touchstart、touchmove、touchend、touchcancel等事件,而click點擊事件執(zhí)行時瀏覽器需要等待300毫秒,以判斷用戶是否再次點擊了屏幕。這就造成了很多問題,比如點擊穿透等。那么為什么click事件執(zhí)行時瀏覽器會等待300ms呢?
2007年蘋果為了解決iPhone Safari訪問PC頁面縮放的問題,提供了一個雙擊縮放功能。當用戶第一次觸摸屏幕時瀏覽器等待300ms才會判斷用戶是需要click點擊還是zoom縮放。這就造成用戶觸摸屏幕到click點擊事件觸發(fā)存在300ms的延遲。
隨著iPhone的成功,后續(xù)的無限瀏覽器復制了其大部分操作系統(tǒng),其中就包括雙擊縮放,這也成為主流瀏覽器的一個功能。雖然300ms延遲在平時瀏覽網(wǎng)頁時并不會帶來嚴重問題,但對于高性能的web app則是一個嚴重的問題,另外響應式設計的流行也讓雙擊縮放逐漸失去了用武之地。
一般而言,觸摸屏幕時間觸發(fā)流程是這樣的:
- touchstart
- touchmove
- touchend
- wait 300ms in case of another tap
- click
因為這300ms的存在,受到這個延遲影響的場景有:
- JavaScript監(jiān)聽的click事件
- 基于click事件交互的元素,比如鏈接、表單元素。
FastClick是FT Labs專門為解決移動端瀏覽器300ms點擊延遲問題所開發(fā)的輕量級庫。
使用FastClick時input
文本框在iOS設備上點擊輸入時調(diào)取手機自帶鍵盤存在不靈敏,有時甚至無法調(diào)起的情況。而在Android上則完全沒有問題,這個原因是因為FastClick的點擊穿透所帶來的。
axios
axios是一個基于promise的HTTP庫,可用于瀏覽器和Node.js環(huán)境中。
axios主要特性
- 從瀏覽器中創(chuàng)建XMLHttpRequests請求
- 從Node.js中創(chuàng)建HTTP請求
- 支持Promise API
- 支持攔截請求和響應
- 支持轉(zhuǎn)換請求數(shù)據(jù)和響應數(shù)據(jù)
- 支持取消請求
- 支持自動轉(zhuǎn)換JSON數(shù)據(jù)
- 客戶端支持防御XSRF攻擊
vue cli 3配置axios插件
$ vue add axios
$ npm ls axios version
$ npm ls axios version
sxyh_web_stats@0.1.0 D:\vue\workspace\sxyh_web_stats
`-- axios@0.18.1
vue cli 3配置中設置代理
如果前端應用和端口API服務器沒有運行在同一個主機上,則需在開發(fā)環(huán)境下將API請求代理到API服務器,此時可通過vue.config.js中的devServer.proxy選項來配置。devServer.proxy使用http-proxy-middleware中間件。
http-proxy-middleware可用于將后臺請求轉(zhuǎn)發(fā)給其他服務器,比如在當前主機為http://127.0.0.1:3000,瀏覽器訪問當前主機的/api接口,請求的數(shù)據(jù)確在另一臺服務器 http://127.0.0.1:40 上。此時可通過在當前主機上設置代理,將請求轉(zhuǎn)發(fā)給數(shù)據(jù)所在的服務器上。
選項 | 描述 |
---|---|
target | 設置目標服務器的主機地址 |
changeOrigin | 是否需要更改原始主機頭為目標URL |
ws | 是否代理websocket |
pathRewrite | 重寫目標URL路徑 |
router | 重寫指定請求轉(zhuǎn)發(fā)目標 |
$ vim vue.config.js
module.exports = {
//開發(fā)服務器
devServer:{
//設置代理
proxy:{
"/api":{
target:"http://127.0.0.1:8080",
ws:true,
changeOrigin:true
}
}
}
};
這里由于前端應用使用的是 http://127.0.0.1:8080地址,因此相當于訪問前端應用自身。
模擬數(shù)據(jù)
$ vim public/api/db.json
模擬數(shù)據(jù)為接口返回的數(shù)據(jù),為此需要提前統(tǒng)一規(guī)劃好數(shù)據(jù)格式。
{
"code":200,
"message":"success",
"data":[
"alice", "bob", "carl"
]
}
組件中使用axios
由于這里使用MPA多頁應用,針對每個單頁的入口文件需要單獨引入axios。
$ vim src/entry/index.js
import Vue from 'vue';
import Axios from "axios";
import router from "../router/index.js";
import app from '../pages/index/index.vue';
import layout from "../components/index/layout.vue";
Vue.config.productionTip = false;
Vue.prototype.axios = Axios;
new Vue({
render:h=>h(app),
router:router,
components:{layout},
template:"<layout/>"
}).$mount('#index');
接在在index.vue組件內(nèi)使用axios獲取db.json中的數(shù)據(jù)
$ vim src/pages/index/index.vue
<template>
<div class="page">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: "index.vue",
data(){
return {
ranklist:[]
}
},
created(){
this.fetchRanklist();
},
methods:{
fetchRanklist(){
let self = this;
this.axios.get("/api/db.json").then(res=>{
const data = res.data;
console.log(res, data);
if(data.code === 200){
self.ranklist = data.data;
}
}).catch(err=>{
console.error(err);
});
}
}
}
</script>
<style scoped>
</style>
此時訪問 http://127.0.0.1:8080/index 會自動向 http://127.0.0.1:8080/api/db.json 發(fā)送請求獲取數(shù)據(jù)。
使用axios返回的數(shù)據(jù)格式為
{
config:{...},
data:{...},
headers:{...},
request:...
status:...,
statusText:...
}
其中接口返回的數(shù)據(jù)在data選項中