在开发的一个webapp或者h5项目时候,往往我们都需要经历一些繁琐的流程,为了前端的工程化,我们需要做一些基础模板,方便我们更加迅速的开发项目,如何设计我们的目录结果,如何管理api接口,如何封装工具类函数封装等一下重复性的工作。
基于Vant UI + Vue移动端通用模板
基于 vue-cli4.0 + webpack 4 + vant ui + sass+ vw(postcss-px-to-viewport)适配方案+axios 封装构建手机端模板脚手架
Node 版本要求
Vue CLI 需要 Node.js 8.9 或更高版本 (推荐 8.11.0+)
启动项目
git@github.com:z-xuanyu/h5-vue-template.git
cd h5-vue-template
yarn install 或者 npm install (推荐使用yarn,如果没有安装yarn,现在全局安装执行 npm install yarn -g)
yarn serve 或者 npm run serve
Demo演示图
目录
- √ Vue-cli4
- √ 配置多环境变量
- √ vw 适配方案
- √ VantUI 组件按需加载
- √ Sass 全局样式
- √ Vuex 状态管理
- √ Vue-router
- √ Axios 封装及接口管理
- √ Webpack 4 vue.config.js 基础配置
- √ 配置 alias 别名
- √ 配置 proxy 跨域
- √ 配置 打包分析
- √ 去掉 console.log
- √ splitChunks 单独打包第三方模块
- √ 添加 IE 兼容
- √ Eslint+Pettier 统一开发规范
✅ 配置多环境变量
package.json
里的 scripts
配置 serve
stage
build
,通过 --mode xxx
来执行不同环境
- 通过
yarn serve
或者npm run serve
启动本地 , 执行development
- 通过
yarn stage
或者npm run stage
打包测试 , 执行staging
- 通过
yarn report
或者npm run report
打包分析 - 通过
yarn build
或者npm run build
打包正式 , 执行production
"scripts": {
"serve": "vue-cli-service serve --open",
"build": "vue-cli-service build",
"stage": "vue-cli-service build --mode staging",
"report": "vue-cli-service build --report",
"lint": "vue-cli-service lint"
},
配置介绍
以 VUE_APP_
开头的变量,在代码中可以通过 process.env.VUE_APP_
访问。
比如,VUE_APP_ENV = 'development'
通过process.env.VUE_APP_ENV
访问。
除了 VUE_APP_*
变量之外,在你的应用代码中始终可用的还有两个特殊的变量NODE_ENV
和BASE_URL
在项目根目录中新建.env.*
- .env.development 本地开发环境配置
NODE_ENV='development'
# must start with VUE_APP_
VUE_APP_ENV = 'development'
- .env.staging 测试环境配置
NODE_ENV='production'
# must start with VUE_APP_
VUE_APP_ENV = 'staging'
- .env.production 正式环境配置
NODE_ENV='production'
# must start with VUE_APP_
VUE_APP_ENV = 'production'
✅ vw 适配方案
在之前有一种流行已久的移动端适配方案,那就是rem
,我想下面这两句代码,有不少老移动端都不会陌生:
const deviceWidth = document.documentElement.clientWidth || document.body.clientWidth
document.querySelector('html').style.fontSize = deviceWidth / 7.5 + 'px'
没错,在那个移动端 UI 稿尺寸为750*1334
满天飞的时代,这两句代码确实给开发者带来了很大的方便,这样设置根font-size
后
,px
和rem
的转换比例成了100
, 为比如 UI 稿一个长宽分别为120px*40px
,那么开发者对应的写成1.2rem*0.4rem
就可以了
这种换算已经是颇为方便,但是并非所有的项目都能这样去设置一个方便换算的比例系数,当比例系数为 100 时,小数点往前面挪两位
就行了,然而有的项目设置的换算系数千奇百怪,有 50 的,有 16 的,很多已经严重超出口算力所能及的范畴了。所以后来诞生
的px-to-rem
或者px2rem
就是为了解决这个问题
人们希望有这样一种方案...
- 首先,无论换算方不方便,我都不想换算(就是这么懒
- 其次,有些属性或者类选择器我不想进行转换
- css 代码要足够简洁,我只希望看到一种单位,那就是 px
两种方案都很好,但我偏爱后者
第一种方案是lib-flexible+postcss-pxtorem
,在相当长一段时间里,这两个插件搭配都是解决移动端布局的神器,lib-flexible
是
阿里手淘系开源的一个库,用于设置 font-size,同时处理一些窗口缩放的问题。其中一位主要贡献者正是阿里的大神 winter。
直到 2020 年的今天,我仍然可以说,lib-flexible+postcss-pxtorem 是解决移动端布局的主流,但是我们可以好好想一想,它是否有
什么不足?
从我个人来说,我认为它主要有以下两个不足:
- 两个插件需要配套使用,而且 rootValue 设置的值不好理解
- rem 是相对于 html 元素字体单位的一个相对单位,从本质上来说,它属于一个字体单位,用字体单位来布局,并不是太合适
翻阅其 github 地址,可以看到这样一段有意思的话:
第二种方案是 viewport,postcss-px-to-viewport
就是这样一款优秀的插件,它解决了以上提到的痛点,也满足以上提到的理想要求
。它将 px 转换成视口单位 vw,众所周知,vw 本质上还是一种百分比单位,100vw 即等于 100%,即window.innerWidth
在 vue.config.js 中配置:
postcss: {
plugins: [
require('postcss-px-to-viewport')({
unitToConvert: 'px', //需要转换的单位,默认为"px"
viewportWidth: 375, // 视窗的宽度,对应的是我们设计稿的宽度
viewportHeight: 667, //视窗的高度,根据375设备的宽度来指定,一般指定667,也可以不配置
unitPrecision: 13, // 指定`px`转换为视窗单位值的小数位数(很多时候无法整除)
propList: ['*'], // 能转化为vw的属性列表
viewportUnit: 'vw', // 指定需要转换成的视窗单位,建议使用vw
fontViewportUnit: 'vw', //字体使用的视口单位
selectorBlackList: ['.ignore-', '.hairlines'], //指定不转换为视窗单位的类,可以自定义,可以无限添加,建议定义一至两个通用的类名
minPixelValue: 1, // 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值
mediaQuery: false, // 允许在媒体查询中转换`px`
replace: true, //是否直接更换属性值,而不添加备用属性
// exclude: /node_modules/i, //忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件
landscape: false, //是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape)
landscapeUnit: 'vw', //横屏时使用的单位
landscapeWidth: 1134 //横屏时使用的视口宽度
})
]
}
注意:详细配置可以看项目的vue.config.js文件
✅ VantUI 组件按需加载
项目采
用Vant 自动按需引入组件 (推荐)下
面安装插件介绍:
babel-plugin-import 是一款 babel
插件,它会在编译过程中将
import
的写法自动转换为按需引入的方式
安装插件
npm i babel-plugin-import -D
在babel.config.js
设置
// 对于使用 babel7 的用户,可以在 babel.config.js 中配置
module.exports = {
presets: [
['@vue/cli-plugin-babel/preset', { useBuiltIns: 'usage', corejs: 3 }]
],
plugins: [
['import', {
libraryName: 'vant',
libraryDirectory: 'es',
style: true
}, 'vant']
]
}
使用组件
项目在 src/plugins/vant.js
下统一管理组件,用哪个引入哪个,无需在页面里重复引用
// 按需全局引入 vant组件
import Vue from 'vue'
import { Button, List, Cell, Tabbar, TabbarItem } from 'vant'
Vue.use(Button)
Vue.use(Cell)
Vue.use(List)
Vue.use(Tabbar).use(TabbarItem)
✅ Sass 全局样式
首先 你可能会遇到 node-sass
安装不成功,别放弃多试几次!!!
每个页面自己对应的样式都写在自己的 .vue 文件之中 scoped
它顾名思义给 css 加了一个域的概念。
<style lang="scss">
/* global styles */
</style>
<style lang="scss" scoped>
/* local styles */
</style>
目录结构
vue-h5-template 所有全局样式都在 @/src/assets/css
目录下设置
├── assets
│ ├── css
│ │ ├── index.scss # 全局通用样式
│ │ ├── mixin.scss # 全局mixin
│ │ └── variables.scss # 全局变量
引入全局sass
//在main.js入口文件 引入全局样式
import '@/assets/css/index.scss'
自定义 vant-ui 样式
现在我们来说说怎么重写 vant-ui
样式。由于 vant-ui
的样式我们是在全局引入的,所以你想在某个页面里面覆盖它的样式就不能
加 scoped
,但你又想只覆盖这个页面的 vant
样式,你就可在它的父级加一个 class
,用命名空间来解决问题。
.about-container {
/* 你的命名空间 */
.van-button {
/* vant-ui 元素*/
margin-right: 0px;
}
}
父组件改变子组件样式 深度选择器
当你子组件使用了 scoped
但在父组件又想修改子组件的样式可以 通过 >>>
来实现:
<style scoped>
.a >>> .b { /* ... */ }
</style>
✅ Vuex 状态管理
目录结构
├── store
│ ├── modules
│ │ └── app.js
│ ├── index.js
│ ├── getters.js
main.js
引入
import Vue from 'vue'
import App from './App.vue'
import store from './store'
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})
使用
<script>
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters(['userName'])
},
methods: {
// Action 通过 store.dispatch 方法触发
setUserName() {
this.$store.dispatch('setUserName', 'xuanyu')
}
}
}
</script>
✅ Vue-router
本案例采用 hash
模式,开发者根据需求修改 mode
base
注意:如果你使用了 history
模式,vue.config.js
中的 publicPath
要做对应的修改
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export const router = [
{
path: '/',
name: 'index',
component: () => import('@/views/home/index'), // 路由懒加载
meta: {
title: '首页', // 页面标题
keepAlive: false // keep-alive 标识
}
}
]
const createRouter = () =>
new Router({
// mode: 'history', // 如果你是 history模式 需要配置 vue.config.js publicPath
// base: '/app/',
scrollBehavior: () => ({ y: 0 }),
routes: router
})
export default createRouter()
更多:Vue Router
✅ Axios 封装及接口管理
utils/request.js
封装 axios ,开发者需要根据后台接口做修改。
service.interceptors.request.use
里可以设置请求头,比如设置token
service.interceptors.response.use
里可以对接口返回数据处理,比如 401 删除本地信息,重新登录
import axios from 'axios'
import store from '@/store'
import { Toast } from 'vant'
import { getToken } from '@/utils/auth'
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
withCredentials: true, // 跨域请求时发送Cookie
timeout: 5000 // 请求延时
})
// 请求拦截器
service.interceptors.request.use(
config => {
Toast.loading({
forbidClick: true
})
// 获取用户的token
if (store.getters.token) {
config.headers['Authorization'] = `Bearer ${getToken()}`
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
response => {
Toast.clear()
const res = response.data
if (res.status && res.status !== 200) {
// 登录超时,重新登录
if (res.status === 401) {
store.dispatch('LogOut').then(() => {
location.reload()
})
}
return Promise.reject(res || 'error')
} else {
return Promise.resolve(res)
}
},
error => {
Toast.clear()
console.log('err' + error) // for debug
return Promise.reject(error)
}
)
export default service
接口管理
在src/api
文件夹下统一管理接口
- 你可以建立多个模块对接接口, 比如
home.js
里是首页的接口这里讲解user.js
url
接口地址,请求的时候会拼接上VUE_APP_BASE_API
下的环境变量
method
请求方法data
请求参数qs.stringify(params)
是对数据系列化操作
import qs from 'qs'
// axios
import request from '@/utils/request'
//user api
// 用户信息
export function getUserInfo(params) {
return request({
url: '/user/userinfo',
method: 'post',
data: qs.stringify(params)
})
}
如何调用
// 请求接口
import { getUserInfo } from '@/api/user.js'
const params = { user: 'sunnie' }
getUserInfo(params)
.then(() => {})
.catch(() => {})
✅ Webpack 4 vue.config.js 基础配置
如果你的 Vue Router
模式是 hash
publicPath: './',
如果你的 Vue Router
模式是 history 这里的 publicPath 和你的 Vue Router
base
保持一直
publicPath: '/app/',
const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV)
module.exports = {
publicPath: './', // 署应用包时的基本 URL。 vue-router hash 模式使用
// publicPath: '/app/', // 署应用包时的基本 URL。 vue-router history模式使用
outputDir: 'dist', // 生产环境构建文件的目录
assetsDir: 'static', // outputDir的静态资源(js、css、img、fonts)目录
lintOnSave: !IS_PROD,
productionSourceMap: false, // 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。
devServer: {
port: 9020, // 端口号
open: false, // 启动后打开浏览器
overlay: {
// 当出现编译器错误或警告时,在浏览器中显示全屏覆盖层
warnings: false,
errors: true
}
// ...
}
}
✅ 配置 alias 别名
const path = require('path')
const resolve = dir => path.join(__dirname, dir)
const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV)
module.exports = {
chainWebpack: config => {
// 添加别名
config.resolve.alias
.set('@', resolve('src'))
.set('assets', resolve('src/assets'))
.set('api', resolve('src/api'))
.set('views', resolve('src/views'))
.set('components', resolve('src/components'))
}
}
✅ 配置 proxy 跨域
如果你的项目需要跨域设置,你需要打来 vue.config.js
proxy
注释 并且配置相应参数
!!!注意:你还需要将 src/config/env.development.js
里的 baseApi
设置成 '/'
module.exports = {
devServer: {
// ....
proxy: {
//配置跨域
'/api': {
target: 'https://www.zhouxuanyu.com', // 接口的域名
// ws: true, // 是否启用websockets
changOrigin: true, // 开启代理,在本地创建一个虚拟服务端
pathRewrite: {
'^/api': '/'
}
}
}
}
}
使用 例如: src/api/home.js
export function getUserInfo(params) {
return request({
url: '/api/userinfo',
method: 'post',
data: qs.stringify(params)
})
}
✅ 配置 打包分析
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
chainWebpack: config => {
// 打包分析
if (IS_PROD) {
config.plugin('webpack-report').use(BundleAnalyzerPlugin, [
{
analyzerMode: 'static'
}
])
}
}
}
yarn build 或者yarn report
✅ 去掉 console.log
保留了测试环境和本地环境的 console.log
npm i -D babel-plugin-transform-remove-console
在 babel.config.js 中配置
const IS_PROD = ['production', 'prod'].includes(process.env.VUE_APP_ENV)
// 去除 console.log
let transformRemoveConsolePlugin = []
if (IS_PROD) {
transformRemoveConsolePlugin = ['transform-remove-console']
}
module.exports = {
presets: [
['@vue/cli-plugin-babel/preset', { useBuiltIns: 'usage', corejs: 3 }]
],
plugins: [
['import', {
libraryName: 'vant',
libraryDirectory: 'es',
style: true
}, 'vant'],
...transformRemoveConsolePlugin
]
}
✅ splitChunks 单独打包第三方模块
module.exports = {
chainWebpack: config => {
config.when(IS_PROD, config => {
config
.plugin('ScriptExtHtmlWebpackPlugin')
.after('html')
.use('script-ext-html-webpack-plugin', [
{
// 将 runtime 作为内联引入不单独存在
inline: /runtime\..*\.js$/
}
])
.end()
config.optimization.splitChunks({
chunks: 'all',
cacheGroups: {
// cacheGroups 下可以可以配置多个组,每个组根据test设置条件,符合test条件的模块
commons: {
name: 'chunk-commons',
test: resolve('src/components'),
minChunks: 3, // 被至少用三次以上打包分离
priority: 5, // 优先级
reuseExistingChunk: true // 表示是否使用已有的 chunk,如果为 true 则表示如果当前的 chunk 包含的模块已经被抽取出去了,那么将不会重新生成新的。
},
node_vendors: {
name: 'chunk-libs',
chunks: 'initial', // 只打包初始时依赖的第三方
test: /[\\/]node_modules[\\/]/,
priority: 10
},
vantUI: {
name: 'chunk-vantUI', // 单独将 vantUI 拆包
priority: 20, // 数字大权重到,满足多个 cacheGroups 的条件时候分到权重高的
test: /[\\/]node_modules[\\/]_?vant(.*)/
}
}
})
config.optimization.runtimeChunk('single')
})
}
}
✅ 添加 IE 兼容
之前的方式 会报 @babel/polyfill
is deprecated. Please, use required parts of core-js
and
regenerator-runtime/runtime
separately
@babel/polyfill
废弃,使用 core-js
和 regenerator-runtime
npm i --save core-js regenerator-runtime
在 main.js
中添加
// 兼容 IE
// https://github.com/zloirock/core-js/blob/master/docs/2019-03-19-core-js-3-babel-and-a-look-into-the-future.md#babelpolyfill
import 'core-js/stable'
import 'regenerator-runtime/runtime'
配置 babel.config.js
const plugins = []
module.exports = {
presets: [['@vue/cli-plugin-babel/preset', { useBuiltIns: 'usage', corejs: 3 }]],
plugins
}
✅ Eslint + Pettier 统一开发规范
VScode (版本 1.47.3)安装 eslint
prettier
vetur
插件 .vue
文件使用 vetur 进行格式化,其他使用prettier
,后面会
专门写个如何使用配合使用这三个玩意
在文件 .prettierrc
里写 属于你的 pettier 规则
{
"printWidth": 120,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "none",
"semi": false,
"wrap_line_length": 120,
"wrap_attributes": "auto",
"proseWrap": "always",
"arrowParens": "avoid",
"bracketSpacing": false,
"jsxBracketSameLine": true,
"useTabs": false,
"overrides": [{
"files": ".prettierrc",
"options": {
"parser": "json"
}
}]
}
Vscode setting.json 设置
{
// 将设置放入此文件中以覆盖默认设置
"files.autoSave": "off",
// 控制字体系列。
"editor.fontFamily": "Consolas, 'Courier New', monospace,'宋体'",
"terminal.integrated.shell.windows": "C:\\Program Files\\Git\\bin\\bash.exe",
// 以像素为单位控制字号。
"editor.fontSize": 16,
// 控制选取范围是否有圆角
"editor.roundedSelection": false,
// 建议小组件的字号
"editor.suggestFontSize": 16,
// 在“打开的编辑器”窗格中显示的编辑器数量。将其设置为 0 可隐藏窗格。
"explorer.openEditors.visible": 0,
// 是否已启用自动刷新
"git.autorefresh": true,
// 以像素为单位控制终端的字号,这是 editor.fontSize 的默认值。
"terminal.integrated.fontSize": 14,
// 控制终端游标是否闪烁。
"terminal.integrated.cursorBlinking": true,
// 一个制表符等于的空格数。该设置在 `editor.detectIndentation` 启用时根据文件内容进行重写。
// Tab Size
"editor.tabSize": 2,
// By default, common template. Do not modify it!!!!!
"editor.formatOnType": true,
"window.zoomLevel": 0,
"editor.detectIndentation": false,
"css.fileExtensions": ["css", "scss"],
"files.associations": {
"*.string": "html",
"*.vue": "vue",
"*.wxss": "css",
"*.wxml": "wxml",
"*.wxs": "javascript",
"*.cjson": "jsonc",
"*.js": "javascript"
},
// 为指定的语法定义配置文件或使用带有特定规则的配置文件。
"emmet.syntaxProfiles": {
"vue-html": "html",
"vue": "html"
},
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true
},
//保存时eslint自动修复错误
"editor.formatOnSave": true,
// Enable per-language
//配置 ESLint 检查的文件类型
"editor.quickSuggestions": {
"strings": true
},
// 添加 vue 支持
// 这里是针对vue文件的格式化设置,vue的规则在这里生效
"vetur.format.options.tabSize": 2,
"vetur.format.options.useTabs": false,
"vetur.format.defaultFormatter.html": "js-beautify-html",
"vetur.format.defaultFormatter.css": "prettier",
"vetur.format.defaultFormatter.scss": "prettier",
"vetur.format.defaultFormatter.postcss": "prettier",
"vetur.format.defaultFormatter.less": "prettier",
"vetur.format.defaultFormatter.js": "vscode-typescript",
"vetur.format.defaultFormatter.sass": "sass-formatter",
"vetur.format.defaultFormatter.ts": "prettier",
"vetur.format.defaultFormatterOptions": {
"js-beautify-html": {
"wrap_attributes": "aligned-multiple", // 超过150折行
"wrap-line-length": 150
},
// #vue组件中html代码格式化样式
"prettier": {
"printWidth": 120,
"tabWidth": 2,
"singleQuote": false,
"trailingComma": "none",
"semi": false,
"wrap_line_length": 120,
"wrap_attributes": "aligned-multiple", // 超过150折行
"proseWrap": "always",
"arrowParens": "avoid",
"bracketSpacing": true,
"jsxBracketSameLine": true,
"useTabs": false,
"overrides": [
{
"files": ".prettierrc",
"options": {
"parser": "json"
}
}
]
}
},
// Enable per-language
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"vetur.validation.template": false,
"html.format.enable": false,
"json.format.enable": false,
"javascript.format.enable": false,
"typescript.format.enable": false,
"javascript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": false,
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "octref.vetur"
},
"emmet.includeLanguages": {
"wxml": "html"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// 开启eslint自动修复js/ts功能
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"minapp-vscode.disableAutoConfig": true,
"javascript.implicitProjectConfig.experimentalDecorators": true,
"editor.maxTokenizationLineLength": 200000
}
10 comments
2025年10月新盘 做第一批吃螃蟹的人coinsrore.com
新车新盘 嘎嘎稳 嘎嘎靠谱coinsrore.com
新车首发,新的一年,只带想赚米的人coinsrore.com
新盘 上车集合 留下 我要发发 立马进裙coinsrore.com
做了几十年的项目 我总结了最好的一个盘(纯干货)coinsrore.com
新车上路,只带前10个人coinsrore.com
新盘首开 新盘首开 征召客户!!!coinsrore.com
新项目准备上线,寻找志同道合的合作伙伴coinsrore.com
新车即将上线 真正的项目,期待你的参与coinsrore.com
新盘新项目,不再等待,现在就是最佳上车机会!coinsrore.com
新盘新盘 这个月刚上新盘 新车第一个吃螃蟹!coinsrore.com
2025年10月新盘 做第一批吃螃蟹的人
看的我热血沸腾啊www.jiwenlaw.com
发帖软件 www.teamczyx.com
酒要喝饱,话要说足,爱就爱个够,走了就别回头。都记住啦,酒能解决的事,绝不能浪费眼泪
时间,带不走真正的朋友;岁月,留不住虚幻的拥有
分手那天,你祝我快乐,我应该,说些什么呢
喜欢你,始于颜值,陷于才华,忠于人品
作为新手看这个文章非常的有用啊。。。
文章不错支持一下