开箱即用 vue全家桶+vant移动端解决方案
作者:花花小仙女
转发链接:
https://mp.weixin.qq.com/s/c9uAYkWJu-zvKfELh_3V0A
前言
基于 vue-cli4.0+webpack 4+vant ui + sass+ rem 适配方案+axios 封装,构建手机端模板脚手架,开箱即用,让开发变得更简单。
先夸一下自己,收到了很多人的好评。这次好好整理一文,你们的鼓励就是我前进的动力。
github:https://github.com/sunniejs/vue-h5-template
Node 版本要求
Vue CLI它需要 Node.js 8.9 或更高版本 (推荐 8.11.0+)。你可以使用 nvm 或nvm-windows 在同一台电脑中管理多个 Node 版本。
本示例 Node.js 12.14.1
启动项目
git?clone?https://github.com/sunniejs/vue-h5-template.git
cd?vue-h5-template
npm?install
npm?run?serve
目录
- √ Vue-cli4
- √ 配置多环境变量
- √ rem 适配方案
- √ VantUI 组件按需加载
- √ Sass 全局样式
- √ Vuex 状态管理
- √ Axios 封装及接口管理
- √ Vue-router
- √ Webpack 4 vue.config.js 基础配置
- √ 配置 proxy 跨域
- √ 配置 alias 别名
- √ 配置 打包分析
- √ 配置 externals 引入 cdn 资源
- √ 去掉 console.log
- √ splitChunks 单独打包第三方模块
- √ 添加 IE 兼容
- √ Eslint+Pettier 统一开发规范
? 配置多环境变量
package.json 里的 scripts 配置 serve stage build,通过 --mode xxx 来执行不同环境
- 通过 npm run serve 启动本地 , 执行 development
- 通过 npm run stage 打包测试 , 执行 staging
- 通过 npm run build 打包正式 , 执行 production
"scripts":?{
??"serve":?"vue-cli-service?serve?--open",
??"stage":?"vue-cli-service?build?--mode?staging",
??"build":?"vue-cli-service?build",
}
配置介绍
??以 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'
这里我们并没有定义很多变量,只定义了基础的 VUE_APP_ENV development staging production变量我们统一在 src/config/env.*.js 里进行管理。
这里有个问题,既然这里有了根据不同环境设置变量的文件,为什么还要去 config 下新建三个对应的文件呢?修改起来方便,不需 要重启项目,符合开发习惯。
config/index.js
//?根据环境引入不同配置?process.env.NODE_ENV
const?config?=?require('./env.'?+?process.env.VUE_APP_ENV)
module.exports?=?config
配置对应环境的变量,拿本地环境文件 env.development.js 举例,用户可以根据需求修改
//?本地环境配置
module.exports?=?{
??title:?'vue-h5-template',
??baseUrl:?'http://localhost:9018',?//?项目地址
??baseApi:?'https://test.xxx.com/api',?//?本地api请求地址
??APPID:?'xxx',
??APPSECRET:?'xxx'
}
根据环境不同,变量就会不同了
//?根据环境不同引入不同baseApi地址
import?{baseApi}?from?'@/config'
console.log(baseApi)
? rem 适配方案
不用担心,项目已经配置好了 rem 适配, 下免仅做介绍:
Vant 中的样式默认使用px作为单位,如果需要使用rem单位,推荐使用以下两个工具:
- postcss-pxtorem 是一款 postcss 插件,用于将单位转化为 rem
- lib-flexible 用于设置 rem 基准值
PostCSS 配置
下面提供了一份基本的 postcss 配置,可以在此配置的基础上根据项目需求进行修改
//?https://github.com/michael-ciniawsky/postcss-load-config
module.exports?=?{
??plugins:?{
????autoprefixer:?{
??????overrideBrowserslist:?['Android?4.1',?'iOS?7.1',?'Chrome?>?31',?'ff?>?31',?'ie?>=?8']
????},
????'postcss-pxtorem':?{
??????rootValue:?37.5,
??????propList:?['*']
????}
??}
}
更多详细信息:vant
新手必看,老鸟跳过
很多小伙伴会问我,适配的问题。
我们知道 1rem 等于html 根元素设定的 font-size 的 px 值。Vant UI 设置 rootValue: 37.5,你可以看到在 iPhone 6 下 看到 (1rem 等于 37.5px):
切换不同的机型,根元素可能会有不同的font-size。当你写 css px 样式时,会被程序换算成 rem 达到适配。
因为我们用了 Vant 的组件,需要按照 rootValue: 37.5 来写样式。
举个例子:设计给了你一张 750px * 1334px 图片,在 iPhone6 上铺满屏幕,其他机型适配。
- 当rootValue: 70 , 样式 width: 750px;height: 1334px; 图片会撑满 iPhone6 屏幕,这个时候切换其他机型,图片也会跟着撑 满。
- 当rootValue: 37.5 的时候,样式 width: 375px;height: 667px; 图片会撑满 iPhone6 屏幕。
也就是 iphone 6 下 375px 宽度写 CSS。其他的你就可以根据你设计图,去写对应的样式就可以了。
当然,想要撑满屏幕你可以使用 100%,这里只是举例说明。
? VantUI 组件按需加载
项目采 用 Vant 自动按需引入组件 (推荐) 下 面安装插件介绍:
babel-plugin-import 是一款 babel 插件,它会在编译过程中将import 的写法自动转换为按需引入的方式
安装插件
npm?i?babel-plugin-import?-D
在babel.config.js 设置
//?对于使用?babel7?的用户,可以在?babel.config.js?中配置
const?plugins?=?[
??[
????'import',
????{
??????libraryName:?'vant',
??????libraryDirectory:?'es',
??????style:?true
????},
????'vant'
??]
]
module.exports?=?{
??presets:?[['@vue/cli-plugin-babel/preset',?{useBuiltIns:?'usage',?corejs:?3}]],
??plugins
}
使用组件
项目在 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 安装不成功,别放弃多试几次!!!
目录结构,在 src/assets/css/文件夹下包含了三个文件
├──?assets
│???├──?css
│???│???├──?index.scss???????????????#?全局通用样式
│???│???├──?mixin.scss???????????????#?全局mixin
│???│???└──?variables.scss???????????#?全局变量
每个页面自己对应的样式都写在自己的 .vue 文件之中
vue.config.js 配置注入 sass 的 mixin variables 到全局,不需要手动引入 ,配置$cdn通过变量形式引入 cdn 地址
const?IS_PROD?=?['production',?'prod'].includes(process.env.NODE_ENV)
const?defaultSettings?=?require('./src/config/index.js')
module.exports?=?{
??css:?{
????extract:?IS_PROD,
????sourceMap:?false,
????loaderOptions:?{
??????scss:?{
????????//?注入?`sass`?的?`mixin`?`variables`?到全局,?$cdn可以配置图片cdn
????????//?详情:?https://cli.vuejs.org/guide/css.html#passing-options-to-pre-processor-loaders
????????prependData:?`
??????????@import?"assets/css/mixin.scss";
??????????@import?"assets/css/variables.scss";
??????????$cdn:?"${defaultSettings.$cdn}";
??????????`
??????}
????}
??}
}
在 main.js 中引用全局样式(发现在上面的,prependData 里设置@import "assets/css/index.scss";并没有应用全局样式这里在 main.js 引入)
设置 js 中可以访问 $cdn,.vue 文件中使用this.$cdn访问
//?引入全局样式
import?'@/assets/css/index.scss'
//?设置?js中可以访问?$cdn
//?引入cdn
import?{$cdn}?from?'@/config'
Vue.prototype.$cdn?=?$cdn
在 css 和 js 使用
? 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)
})
使用
? 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
- config.hideloading 是在 api 文件夹下的接口参数里设置,下文会讲
- service.interceptors.response.use 里可以对接口返回数据处理,比如 401 删除本地信息,重新登录
import?axios?from?'axios'
import?store?from?'@/store'
import?{Toast}?from?'vant'
//?根据环境不同引入不同api地址
import?{baseApi}?from?'@/config'
//?create?an?axios?instance
const?service?=?axios.create({
??baseURL:?baseApi,?//?url?=?base?api?url?+?request?url
??withCredentials:?true,?//?send?cookies?when?cross-domain?requests
??timeout:?5000?//?request?timeout
})
//?request?拦截器?request?interceptor
service.interceptors.request.use(
??config?=>?{
????//?不传递默认开启loading
????if?(!config.hideloading)?{
??????//?loading
??????Toast.loading({
????????forbidClick:?true
??????})
????}
????if?(store.getters.token)?{
??????config.headers['X-Token']?=?''
????}
????return?config
??},
??error?=>?{
????//?do?something?with?request?error
????console.log(error)?//?for?debug
????return?Promise.reject(error)
??}
)
//?respone拦截器
service.interceptors.response.use(
??response?=>?{
????Toast.clear()
????const?res?=?response.data
????if?(res.status?&&?res.status?!==?200)?{
??????//?登录超时,重新登录
??????if?(res.status?===?401)?{
????????store.dispatch('FedLogOut').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 接口地址,请求的时候会拼接上 config 下的 baseApi
- method 请求方法
- data 请求参数 qs.stringify(params) 是对数据系列化操作
- hideloading 默认 false,设置为 true 后,不显示 loading ui 交互中有些接口不需要让用户感知
import?qs?from?'qs'
//?axios
import?request?from?'@/utils/request'
//user?api
//?用户信息
export?function?getUserInfo(params)?{
??return?request({
????url:?'/user/userinfo',
????method:?'get',
????data:?qs.stringify(params),
????hideloading:?true?//?隐藏?loading?组件
??})
}
如何调用
//?请求接口
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 Routerbase 保持一直
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:?false,
??productionSourceMap:?false,?//?如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。
??devServer:?{
????port:?9020,?//?端口号
????open:?false,?//?启动后打开浏览器
????overlay:?{
??????//??当出现编译器错误或警告时,在浏览器中显示全屏覆盖层
??????warnings:?false,
??????errors:?true
????}
????//?...
??}
}
? 配置 proxy 跨域
如果你的项目需要跨域设置,你需要打开 vue.config.js proxy 注释 并且配置相应参数
注意:你还需要将
src/config/env.development.js 里的 baseApi 设置成 '/'
module.exports?=?{
??devServer:?{
????//?....
????proxy:?{
??????//配置跨域
??????'/api':?{
????????target:?'https://test.xxx.com',?//?接口的域名
????????//?ws:?true,?//?是否启用websockets
????????changOrigin:?true,?//?开启代理,在本地创建一个虚拟服务端
????????pathRewrite:?{
??????????'^/api':?'/'
????????}
??????}
????}
??}
}
使用 例如: src/api/home.js
export?function?getUserInfo(params)?{
??return?request({
????url:?'/api/userinfo',
????method:?'get',
????data:?qs.stringify(params)
??})
}
? 配置 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'))
??}
}
? 配置 打包分析
const?BundleAnalyzerPlugin?=?require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports?=?{
??chainWebpack:?config?=>?{
????//?打包分析
????if?(IS_PROD)?{
??????config.plugin('webpack-report').use(BundleAnalyzerPlugin,?[
????????{
??????????analyzerMode:?'static'
????????}
??????])
????}
??}
}
npm?run?build
? 配置 externals 引入 cdn 资源
这个版本 CDN 不再引入,我测试了一下使用引入 CDN 和不使用,不使用会比使用时间少。网上不少文章测试 CDN 速度快,这个开发者可 以实际测试一下。
另外项目中使用的是公共 CDN 不稳定,域名解析也是需要时间的(如果你要使用请尽量使用同一个域名)
因为页面每次遇到 ????<%?}?%>
? 去掉 console.log
保留了测试环境和本地环境的 console.log
npm?i?-D?babel-plugin-transform-remove-console
在 babel.config.js 中配置
//?获取?VUE_APP_ENV?非?NODE_ENV,测试环境依然?console
const?IS_PROD?=?['production',?'prod'].includes(process.env.VUE_APP_ENV)
const?plugins?=?[
??[
????'import',
????{
??????libraryName:?'vant',
??????libraryDirectory:?'es',
??????style:?true
????},
????'vant'
??]
]
//?去除?console.log
if?(IS_PROD)?{
??plugins.push('transform-remove-console')
}
module.exports?=?{
??presets:?[['@vue/cli-plugin-babel/preset',?{useBuiltIns:?'entry'}]],
??plugins
}
? 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
andregenerator-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 统一开发规范
该文件 .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"
???????}
???}]
}
鸣谢
vue-cli4-config: https://github.com/staven630/vue-cli4-config
vue-element-admin:https://github.com/PanJiaChen/vue-element-admin
mdnice:https://mdnice.com
作者:花花小仙女
转发链接:
https://mp.weixin.qq.com/s/c9uAYkWJu-zvKfELh_3V0A