前言:歡迎前端的小伙伴們前來圍觀丰刊、學(xué)習(xí)借鑒,如果你是后端瞒斩、測(cè)試和其他的小伙伴也沒關(guān)系破婆,如果自己也想玩一下前端,想搭建一個(gè)前端的框架胸囱,那么不妨靜下心來看看這篇文章祷舀。如果你不是從事開發(fā)工作的人員,內(nèi)容可能相對(duì)而言比較枯燥旺矾,但是如果想找錯(cuò)別字蔑鹦,也不妨進(jìn)來看看夺克。
初衷:有的前端的小伙伴要說了箕宙,vue-cli不是已經(jīng)幫我們封裝好了webpack(打包)嗎?為什么铺纽,還要進(jìn)行二次的搭建和封裝呢柬帕?我想說的是,是的這些很基礎(chǔ)的配置vue-cli都幫我們做好了,但是針對(duì)手機(jī)端樣式初始化陷寝,axios的請(qǐng)求封裝锅很,常用的工具包類封裝,vuex模塊化的處理凤跑,以及開發(fā)爆安、測(cè)試、正式環(huán)境變量的拆分配置仔引,webpack打包優(yōu)化配置扔仓,手機(jī)端響應(yīng)式的處理,手機(jī)端引入第三方UI框架vant的更好的方法等等都沒有給我們搭建咖耘,因?yàn)椴煌?xiàng)目可能有不同的方式翘簇,我這里介紹的是一種大眾的、通用的一些框架:vue-cli+vue-router+vuex+axios+vant儿倒。
目的:教你如何手動(dòng)搭建屬于自己的前端手機(jī)項(xiàng)目版保。
廢話不多說,直接上干貨夫否。
第一步: vue-cli初始化項(xiàng)目(相信很多前端小伙伴這一步操作都不難)
npm install -g @vue/cli
vue create my-project
注:這里的my-project自己可以按照自己的項(xiàng)目名稱來定義
如果你沒有安裝成功彻犁,那么需要把nodejs安裝一下。
第二步:配置全局環(huán)境變量
需要我們?cè)诟夸泟?chuàng)建四個(gè)文件:.env慷吊、.env.dev袖裕、.env.test、.env.pro
目的:我們不可能反復(fù)的去更改配置文件溉瓶,而是通過運(yùn)行不同的指令來調(diào)用同變量不同環(huán)境的值急鳄。
//.env 和 .env.dev 內(nèi)容一樣
VUE_APP_NODE_ENV="development"
VUE_APP_API="http://public-api-v1.aspirantzhang.com/"
VUE_APP_VERSION = "d-1.0"
//.env.test
VUE_APP_NODE_ENV="test"
VUE_APP_API="https://wwww.baidu.com/production"
VUE_APP_VERSION = "t-1.0"
//.env.pro
VUE_APP_NODE_ENV="production"
VUE_APP_API="https://wwww.baidu.com/production"
VUE_APP_VERSION = "p-1.0"
這四個(gè)配置文件是結(jié)合package.json來使用的,啟動(dòng)不同的命令,執(zhí)行不同變量參數(shù)
"scripts": {
"dev": "vue-cli-service serve",
"test": "vue-cli-service serve --mode test",
"pro": "vue-cli-service serve --mode pro",
"build:dev": "vue-cli-service build --mode dev",
"build:test": "vue-cli-service build --mode test",
"build:pro": "vue-cli-service build --mode pro",
"lint": "vue-cli-service lint"
},
第三步:路由配置
在配置路由之前我創(chuàng)建了兩個(gè)頁面:
首頁:src/views/Home/Home.vue
列表頁:src/views/List/List.vue
1.創(chuàng)建src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '/',
redirect: {
name: 'home'
}
},
{
path: '/home',
name: 'home',
meta: {
title: '首頁',
},
component: () => import(/* webpackChunkName: "Home" */ '../views/Home/Home.vue') // 首頁
},
{
path: '/list',
name: 'list',
meta: {
title: '列表頁面',
},
component: () => import(/* webpackChunkName: "List" */ '../views/List/List.vue') // 列表頁面
}
]
const router = new VueRouter({
base: process.env.BASE_URL,
routes
})
router.beforeEach((to, from, next) => {
/* 路由發(fā)生變化修改頁面title */
if (to.meta.title) {
document.title = to.meta.title
}
next()
})
export default router
2.在入口文件main.js中引用router
import router from './router'
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app')
3.在App.vue文件中通過router-view
來獲取路由指向的頁面堰酿,把頁面和路由關(guān)聯(lián)起來
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App',
created(){
console.log(process.env.VUE_APP_NODE_ENV, '-', process.env.VUE_APP_VERSION)
}
}
</script>
第四步:vuex模塊處理配置
1.創(chuàng)建src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import VuexPersistence from 'vuex-persist'
import home from './modules/home'
import list from './modules/list'
const vuexLocal = new VuexPersistence({
storage: window.localStorage,
modules: ["home"]
})
Vue.use(Vuex)
const store = new Vuex.Store({
strict: process.env.NODE_ENV !== 'production',
modules: { home, list },
plugins: [vuexLocal.plugin]
})
export default store
2.創(chuàng)建src/store/modules/home.js
export default {
namespaced: true,
state: {
list: [],
visible: false,
firstName: 'Sunny',
lastName: 'Fan'
},
mutations: {
MGetList(state, data){
state.list = data
},
MChangeVisible(state, value){
state.visible = value
}
},
actions: {
// 異步請(qǐng)求接口數(shù)據(jù)
AGetList ({ commit }, params) {
const url = '/users'
const error = '獲取數(shù)據(jù)失敗'
return $http.get(url, params).then(res => {
const { data } = res
// commit 去同步更改state里面的數(shù)據(jù)
return commit('MGetList', data)
}).catch(e => {
return Promise.resolve(e && e.statusText || error)
})
},
},
getters: {
getFullName: state => {
return state.firstName +'----'+ state.lastName
}
}
}
3.創(chuàng)建src/store/modules/list.js 這個(gè)參考2即可
4.在入口文件mian.js中引入store/index.js
import Vue from 'vue'
import router from './router'
import store from './store'
import Axios from '@/utils/Axios'
import App from './App.vue'
import 'lib-flexible/flexible' // 根據(jù)窗口不同疾宏,給html設(shè)置不同的font-size值
import './utils/vant' // 引入局部ui
import './assets/css/common.less'
import Vconsole from 'vconsole'
Vue.config.productionTip = false
// 在開發(fā)環(huán)境和測(cè)試環(huán)境打開console方便在真機(jī)上查看日志、追蹤問題
const environment = process.env.VUE_APP_NODE_ENV;
if(environment==='development'||environment==='test'){
const vConsole = new Vconsole()
Vue.use(vConsole)
}
// vue內(nèi)部全局注入
Vue.use({
install (vue) {
Object.assign(vue.prototype, {
$axios: Axios,
$store: store
})
}
})
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app')
第五步:手機(jī)端響應(yīng)式配置触创,以及初始化樣式坎藐、vant樣式框架引入(根據(jù)不同屏幕放大縮小適配)
1.在src創(chuàng)建assets/common.less
*{
padding: 0;
margin: 0;
box-sizing: border-box;
touch-action: auto;
-webkit-overflow-scrolling:touch;
}
html, body {
height:100vh;
width: 100vw;
margin: 0;
padding:0;
}
并且在我們的入口文件:main.js中引入common.less文件
import './assets/css/common.less'
2.安裝適配依賴
yarn add lib-flexible autoprefixer postcss-pxtorem babel-plugin-import
3.根據(jù)依賴進(jìn)行相關(guān)的配置
在項(xiàng)目的根目錄創(chuàng)建postcss.config.js
const autoprefixer = require('autoprefixer')
const pxtorem = require('postcss-pxtorem')
module.exports = ({ file }) => {
let rootValue
// vant 37.5 [link](https://github.com/youzan/vant/issues/1181)
// if (file && file.dirname && file.dirname.indexOf('vant') > -1 && file.dirname.indexOf('swiper') > -1) {
if (file && file.dirname && file.dirname.indexOf('vant') > -1) {
rootValue = 37.5
} else {
rootValue = 75
}
return {
plugins: [
autoprefixer(),
pxtorem({
rootValue: rootValue,
propList: ['*'],
selectorBlackList: ['.swiper'], // 要忽略的選擇器并保留為px。
minPixelValue: 0
})
]
}
}
4.根據(jù)vant的官網(wǎng)文檔哼绑,我們通過在babel.config.js文件中配置來引入vant的樣式
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: [
['import', {
libraryName: 'vant',
libraryDirectory: 'es',
style: true
}, 'vant']
]
}
5.頁面調(diào)用
<van-button type="info">按鈕</van-button>
6.頁面適配岩馍,在main.js中引入lib-flexible
依賴
import 'lib-flexible/flexible' // 根據(jù)窗口不同,給html設(shè)置不同的font-size值
第六步:vant UI的引入(按需引入抖韩,降低打包體積)
//通過 npm 安裝
npm i vant -S
//通過 yarn 安裝
yarn add vant
- 在src創(chuàng)建utils/vant.js
import Vue from 'vue'
import {Loading, Lazyload, Toast, Dialog,} from 'vant'
// 默認(rèn)vant組件
[Loading, Lazyload, Toast, Dialog,].forEach(item => Vue.use(item))
// 先預(yù)制蛀恩,后期做統(tǒng)一調(diào)整
Object.assign(window, {
Toast, Dialog
})
從代碼我們能看出來,每個(gè)組件都是按需引入茂浮,大大的降低了打包的體積双谆,并且把Toast和Dialog注入到了window全局變量里面壳咕,為了方便我們直接調(diào)用。
2.解決vant樣式適配問題,查看上面的postcss.config.js即可
3.在入口文件main.js 引入
import './utils/vant' // 引入局部ui
第七步:Axios的封裝(公共頭部顽馋、異常谓厘、不同請(qǐng)求方式配置處理)
1.創(chuàng)建src/utils/request.js
import axios from 'axios'
const codeMessage = {
200: '服務(wù)器成功返回請(qǐng)求的數(shù)據(jù)。',
201: '新建或修改數(shù)據(jù)成功寸谜。',
202: '一個(gè)請(qǐng)求已經(jīng)進(jìn)入后臺(tái)排隊(duì)(異步任務(wù))竟稳。',
204: '刪除數(shù)據(jù)成功。',
400: '發(fā)出的請(qǐng)求有錯(cuò)誤熊痴,服務(wù)器沒有進(jìn)行新建或修改數(shù)據(jù)的操作住练。',
401: '用戶沒有權(quán)限(令牌、用戶名愁拭、密碼錯(cuò)誤)讲逛。',
403: '用戶得到授權(quán),但是訪問是被禁止的岭埠。',
404: '發(fā)出的請(qǐng)求是不存在的盏混,服務(wù)器沒有進(jìn)行操作。',
406: '請(qǐng)求的格式不可得惜论。',
410: '請(qǐng)求的資源被永久刪除许赃。',
422: '當(dāng)創(chuàng)建一個(gè)對(duì)象時(shí),發(fā)生一個(gè)驗(yàn)證錯(cuò)誤馆类。',
500: '服務(wù)器發(fā)生錯(cuò)誤混聊,請(qǐng)檢查服務(wù)器。',
502: '網(wǎng)關(guān)錯(cuò)誤乾巧。',
503: '服務(wù)不可用句喜,服務(wù)器暫時(shí)過載或維護(hù)。',
504: '網(wǎng)關(guān)超時(shí)沟于。'
}
const baseURL = process.env.VUE_APP_NODE_ENV == 'development' ? '/api' : process.env.VUE_APP_API
const instance = axios.create({
baseURL
})
class Request {
constructor(baseURL) {
this.baseURL = baseURL
this.queue = {}
this.timeout = 5000
}
// 檢查返回狀態(tài)
checkStatus (response) {
const responseData = response.data
// 服務(wù)器返回默認(rèn)結(jié)果
if (response && (response.status === 200 || response.status === 304 || response.status === 400)) {
// 后臺(tái)自定義錯(cuò)誤
// 正常
if (responseData.status == 0) {
return responseData
}
// 登錄過期
if (responseData.errorCode === 402 || responseData.status === 401) {
return Promise.reject(errorText)
}
return Promise.reject(responseData)
}
// 服務(wù)器錯(cuò)誤
const errorText = response && (codeMessage[response.status] || response.statusText)
Promise.reject(response)
}
// 攔截器
interceptors (instance, scope) {
// 請(qǐng)求攔截
instance.interceptors.request.use(config => {
config.baseURL = baseURL;
config.scope = scope
return config
}, error => {
return Promise.reject(error)
})
// 響應(yīng)攔截
instance.interceptors.response.use(res => {
return res
}, error => {
let errorInfo = error.response
if (!errorInfo) {
try {
const { request: { statusText, status }, config } = JSON.parse(JSON.stringify(error))
errorInfo = {
statusText,
status,
request: { responseURL: config.url }
}
} catch (e) {
errorInfo = error
}
}
return Promise.reject(errorInfo)
})
}
// 失敗
error (e) {
return Promise.reject(e)
}
setRequest (method, url, data, scope, file = false) {
this.interceptors(instance, scope)
const options = { method, url }
let contentType = ''
if (file) {
contentType = 'multipart/form-data'
} else if (method == 'post') {
contentType = 'application/json'
} else {
contentType = 'application/x-www-form-urlencoded; charset=UTF-8'
}
const headers = {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': contentType,
// token: store.state.user.token || 1
}
Object.assign(options, {
headers,
[method == 'post' ? 'data' : 'params']: data
})
return instance(options).then(this.checkStatus).catch(this.error)
}
// post 請(qǐng)求封裝
post (url, data, scope) {
return this.setRequest('post', url, data, scope)
}
// get 請(qǐng)求封裝
get (url, data, scope) {
return this.setRequest('get', url, data, scope)
}
// post 請(qǐng)求封裝
POST (url, data, scope) {
return this.setRequest('post', url, data, scope).then(this.success)
}
// get 請(qǐng)求封裝
GET (url, data, scope) {
return this.setRequest('get', url, data, scope).then(this.success)
}
// 文件
File (url, data, scope) {
return this.setRequest('post', url, data, scope, true).then(this.fileSuccess)
}
success (da) {
return da.data
}
fileSuccess (da) {
return da
}
}
export default Request
2.創(chuàng)建src/utils/Axios.js
import Vue from "vue";
import Request from './request'
//import config from '@/config'
const Axios = new Request()
Plugin.install=(Vue)=>{
Vue.prototype.$http = Axios
}
Object.assign(window,{
$http:Axios
})
Vue.use(Plugin);
export default Axios
第八步:vue.config.js配置(針對(duì)webpack進(jìn)行了封裝)
這一步我們進(jìn)行了咳胃,icon圖標(biāo)雪碧圖處理,打包文件哈希命名旷太,解決緩存問題展懈,本地接口代理處理,打包引入cdn文件供璧,路徑過長別名處理等等
1.vue.config.js
const path = require('path')
const SpritesmithPlugin = require('webpack-spritesmith')// 雪碧圖
const TerserPlugin = require('terser-webpack-plugin')
const devServer = require('./server')
const CompressionPlugin = require('compression-webpack-plugin')
const cdn = {
// 開發(fā)環(huán)境
dev: {
css: [
],
js: [
]
},
// 生產(chǎn)環(huán)境
build: {
css: [
],
js: [
'https://lib.baomitu.com/vue/2.6.11/vue.min.js',
'https://lib.baomitu.com/vue-router/3.2.0/vue-router.min.js',
'https://lib.baomitu.com/vuex/3.5.1/vuex.min.js',
'https://lib.baomitu.com/axios/0.19.2/axios.min.js',
'https://lib.baomitu.com/hls.js/0.14.3/hls.min.js'
]
}
}
// 打包排除包存崖,通過cdn加載
const externals = {
'vue': 'Vue',
'vuex': 'Vuex',
'axios': 'axios',
'hls.js': 'hls.js',
'vue-router': 'VueRouter'
}
// 雪碧圖的自定義模板
const templateFunction = function (data) {
var shared = '.icon-sprite { display: inline-block; background-image: url(I); background-size: Dpx Hpx; }'
.replace('I', data.sprites[0].image)
.replace('D', data.sprites[0].total_width / 2)
.replace('H', data.sprites[0].total_height / 2)
var perSprite = data.sprites.map(function (sprite) {
return '.icon-N { width: Wpx; height: Hpx; background-position: Xpx Ypx; }'
.replace('N', sprite.name.replace(/_/g, '-'))
.replace('W', sprite.width / 2)
.replace('H', sprite.height / 2)
.replace('X', sprite.offset_x / 2)
.replace('Y', sprite.offset_y / 2)
}).join('\n')
return shared + '\n' + perSprite
}
const configureWebpackData = {
resolve: {
alias: {
// 別名
vue$: 'vue/dist/vue.esm.js',
'@': resolve('src'),
'@api': resolve('src/api'),
'@utils': resolve('src/utils'),
'@style': resolve('src/assets/css'),
'@images': resolve('src/assets/images'),
'@views': resolve('src/views')
}
},
plugins: [
new SpritesmithPlugin({
src: {
cwd: path.resolve(__dirname, './src/assets/icon'),
glob: '*.png'
},
target: { // 輸出雪碧圖文件及樣式文件,這個(gè)是打包后睡毒,自動(dòng)生成的雪碧圖和樣式
image: path.resolve(__dirname, './src/assets/images/sprite.png'),
css: [
[path.resolve(__dirname, './src/assets/css/sprite.less'), {
// 引用自己的模板
format: 'function_based_template'
}]
]
},
customTemplates: { // 自定義模板入口
function_based_template: templateFunction
},
apiOptions: { // 樣式文件中調(diào)用雪碧圖地址寫法
cssImageRef: '../images/sprite.png'
},
spritesmithOptions: { // 讓合成的每個(gè)圖片有一定的距離
padding: 20
}
})
]
}
function resolve (dir) {
return path.join(__dirname, './', dir)
}
module.exports = {
outputDir: "dist",
assetsDir: 'assets',
publicPath: './',
pages: {
index: {
entry: './src/main.js',
template: path.join(__dirname, 'public/index.html'),
filename: 'index.html',
cdn: process.env.VUE_APP_NODE_ENV === 'production' && cdn.build || cdn.dev,
title: ' '
}
},
lintOnSave: false, // 是否開啟編譯時(shí)是否不符合eslint提示
devServer,
configureWebpack: config => {
configureWebpackData.externals = process.env.VUE_APP_NODE_ENV === 'production' && externals || {};
if (process.env.VUE_APP_NODE_ENV === 'production' || process.env.VUE_APP_NODE_ENV === 'devproduction') {
config.plugins.push(
new TerserPlugin({
terserOptions: {
ecma: undefined,
warnings: false,
parse: {},
compress: {
drop_console: true,
drop_debugger: false,
pure_funcs: ['console.log'] // 移除console
}
}
})
)
}
if (process.env.VUE_APP_NODE_ENV === 'production') {
configureWebpackData.plugins.push(new CompressionPlugin({
test: /\.js$|\.html$|\.css/,
threshold: 10240,
deleteOriginalAssets: false
}))
}
return configureWebpackData
},
chainWebpack: config => {
config.output.filename('assets/js/[name].[hash].js').end()
config.output.chunkFilename('assets/js/[name].[hash].js').end()
},
productionSourceMap: false,
css: {
// extract: true,
sourceMap: false,
// modules: false,
requireModuleExtension: true,
loaderOptions: {
}
}
}
2.server.js 主要配置代理相關(guān)信息
module.exports = {
host: '0.0.0.0',
port: 8000,
https: false,
hotOnly: false,
proxy: {
'^/api': {
// 測(cè)試環(huán)境
target: process.env.VUE_APP_API,
changeOrigin: true, // 是否跨域
pathRewrite: {
'^/api': '' // 需要rewrite重寫的, // /mock
}
}
}
}
第九步:常見工具類的配置(時(shí)間来惧、正則、公共方法吕嘀、數(shù)據(jù)字典)
1.創(chuàng)建src/utils/index.js
//校驗(yàn)輸入文字為純數(shù)字
export function validNumber(value) {
const reg = /^\d+$/;
return reg.test(value);
}
//校驗(yàn)輸入的文字 --綜合搜索
export function validText(value) {
const reg = /^([\u4E00-\u9FA5])*$/;
return reg.test(value);
}
//電話號(hào)碼正則函數(shù)
export function checkPhone(value) {
const reg = /^[1][3,4,5,6,7,8,9][0-9]{9}$/;
return reg.test(value);
}
//郵箱正則函數(shù)
export function checkEmail(value) {
const reg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/;
return reg.test(value);
}
//2-10位中英文
export function checkUserName(value) {
const reg = /^[\u4E00-\u9FA5A-Za-z]{2,10}$/;
return reg.test(value);
}
//去除空格
export function removeSpace(value) {
const reg = /\s+/g;
return value.replace(reg, "");
}
//為空或全部為空格
export function checkSpace(value) {
const reg = /^[ ]*$/;
return reg.test(value);
}
//判斷密碼大于6位违寞,數(shù)字、字母大小寫組合
export function checkPassWord(value) {
let regNumber = /\d+/;
let regString = /[a-zA-Z]+/;
return regNumber.test(value) && regString.test(value) && value.length >= 8 && value.length <= 20;
}
//獲取周幾
export function weeks(day) {
let myDate = day ? new Date(day) : new Date();
let wk = myDate.getDay();
switch (wk) {
case 0:
return '星期日';
case 1:
return '星期一';
case 2:
return '星期二';
case 3:
return '星期三';
case 4:
return '星期四';
case 5:
return '星期五';
case 6:
return '星期六';
}
return wk;
}
export function checkIdCard(value) {
const idCardNo = value;
if(idCardNo.length === 18) {
const birStr = value.substr(6, 8);
const sexFlag = idCardNo.charAt(16) - 0; //奇數(shù)男 偶數(shù)女
const sexfromIDcard = sexFlag % 2; //1男 0女
return {sex: sexfromIDcard===1?0:1, birStr};
} else if(idCardNo.length === 15) {
const birStr = '19' + value.substr(6, 6);
const sexFlag2 = idCardNo.charAt(14) - 0; //奇數(shù)男 偶數(shù)女
const sexfromIDcard2 = sexFlag2 % 2; //1男 0女
return {sex: sexfromIDcard2===1?0:1, birStr};
}
}
// 獲取當(dāng)前時(shí)間年月日時(shí)分秒
export function getNowData(type) {
let date = new Date();
let year = date.getFullYear();
let month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1;
let day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
let lastDay = date.getDate() - 1 < 10 ? '0' + (date.getDate() - 1) : date.getDate() - 1;
let hour = date.getHours();
let minute = date.getMinutes();
let second = date.getSeconds();
switch (type) {
case 1:
return `${year}-${month}-${day}`;
case 2:
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
case 3:
return day;
case 4:
return `${year}.${month}`;
case 5:
return `${year}-${month}-${lastDay}`;
default:
return `${year}-${month}-${day}`;
}
}
//數(shù)組排序
export function compare(property) {
return function (a, b) {
var value1 = a[property];
var value2 = b[property];
return value1 - value2;
};
}
第十:總結(jié)
自己抽了一天時(shí)間偶房,一遍搭建趁曼,一遍寫文檔,反復(fù)修改棕洋,可能里面還有很多需要完善地方挡闰,后期我會(huì)出整個(gè)的搭建的過程的視頻,幫助大家更加直觀的去理解和學(xué)習(xí)掰盘。
碼字不易摄悯,如果有幫助到自己的地方或者看后對(duì)自己學(xué)習(xí)前端知識(shí)所有提升,請(qǐng)關(guān)注一下我的公眾號(hào)愧捕,后期會(huì)有更多精品的內(nèi)容推出奢驯,寫出來和大家一起分享學(xué)習(xí)。
走過路過不要錯(cuò)過次绘,既然都看到這個(gè)地方了瘪阁,那就留下一個(gè)評(píng)論和點(diǎn)贊吧。
源碼地址:https://github.com/fx35792/vue-mobile-template
原文地址:blog.sunnyfanfan.com/articles/20…
參考文獻(xiàn):
https://cli.vuejs.org/
https://vant-contrib.gitee.io/vant/#/zh-CN/