在开发的一个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
}
8 comments
看的我热血沸腾啊www.jiwenlaw.com
发帖软件 www.teamczyx.com
酒要喝饱,话要说足,爱就爱个够,走了就别回头。都记住啦,酒能解决的事,绝不能浪费眼泪
时间,带不走真正的朋友;岁月,留不住虚幻的拥有
分手那天,你祝我快乐,我应该,说些什么呢
喜欢你,始于颜值,陷于才华,忠于人品
作为新手看这个文章非常的有用啊。。。
文章不错支持一下