前言:欢迎前端的小伙伴们前来围观、学习借鉴,如果你是后端、测试和其他的小伙伴也没关系,如果自己也想玩一下前端,想搭建一个前端的框架,那么不妨静下心来看看这篇文章。如果你不是从事开发工作的人员,内容可能相对而言比较枯燥,但是如果想找错别字,也不妨进来看看。
初衷:有的前端的小伙伴要说了,vue-cli不是已经帮我们封装好了webpack(打包)吗?为什么,还要进行二次的搭建和封装呢?我想说的是,是的这些很基础的配置vue-cli都帮我们做好了,但是针对手机端样式初始化,axios的请求封装,常用的工具包类封装,vuex模块化的处理,以及开发、测试、正式环境变量的拆分配置,webpack打包优化配置,手机端响应式的处理,手机端引入第三方UI框架vant的更好的方法等等都没有给我们搭建,因为不同项目可能有不同的方式,我这里介绍的是一种大众的、通用的一些框架:vue-cli+vue-router+vuex+axios+vant。
目的:教你如何手动搭建属于自己的前端手机项目。
废话不多说,直接上干货。
第一步: vue-cli初始化项目(相信很多前端小伙伴这一步操作都不难)
npm install -g @vue/cli
vue create my-project
注:这里的my-project自己可以按照自己的项目名称来定义
如果你没有安装成功,那么需要把nodejs安装一下。
第二步:配置全局环境变量
需要我们在根目录创建四个文件:.env、.env.dev、.env.test、.env.pro
目的:我们不可能反复的去更改配置文件,而是通过运行不同的指令来调用同变量不同环境的值。
//.env 和 .env.dev 内容一样
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"
这四个配置文件是结合package.json来使用的,启动不同的命令,执行不同变量参数
"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"
},
第三步:路由配置
在配置路由之前我创建了两个页面:
首页:src/views/Home/Home.vue
列表页:src/views/List/List.vue
1.创建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) => {
/* 路由发生变化修改页面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
来获取路由指向的页面,把页面和路由关联起来
<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.创建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.创建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: {
// 异步请求接口数据
AGetList ({ commit }, params) {
const url = '/users'
const error = '获取数据失败'
return $http.get(url, params).then(res => {
const { data } = res
// commit 去同步更改state里面的数据
return commit('MGetList', data)
}).catch(e => {
return Promise.resolve(e && e.statusText || error)
})
},
},
getters: {
getFullName: state => {
return state.firstName +'----'+ state.lastName
}
}
}
3.创建src/store/modules/list.js 这个参考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' // 根据窗口不同,给html设置不同的font-size值
import './utils/vant' // 引入局部ui
import './assets/css/common.less'
import Vconsole from 'vconsole'
Vue.config.productionTip = false
// 在开发环境和测试环境打开console方便在真机上查看日志、追踪问题
const environment = process.env.VUE_APP_NODE_ENV;
if(environment==='development'||environment==='test'){
const vConsole = new Vconsole()
Vue.use(vConsole)
}
// vue内部全局注入
Vue.use({
install (vue) {
Object.assign(vue.prototype, {
$axios: Axios,
$store: store
})
}
})
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app')
第五步:手机端响应式配置,以及初始化样式、vant样式框架引入(根据不同屏幕放大缩小适配)
1.在src创建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.根据依赖进行相关的配置
在项目的根目录创建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.根据vant的官网文档,我们通过在babel.config.js文件中配置来引入vant的样式
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: [
['import', {
libraryName: 'vant',
libraryDirectory: 'es',
style: true
}, 'vant']
]
}
5.页面调用
<van-button type="info">按钮</van-button>
6.页面适配,在main.js中引入lib-flexible
依赖
import 'lib-flexible/flexible' // 根据窗口不同,给html设置不同的font-size值
第六步:vant UI的引入(按需引入,降低打包体积)
//通过 npm 安装
npm i vant -S
//通过 yarn 安装
yarn add vant
- 在src创建utils/vant.js
import Vue from 'vue'
import {Loading, Lazyload, Toast, Dialog,} from 'vant'
// 默认vant组件
[Loading, Lazyload, Toast, Dialog,].forEach(item => Vue.use(item))
// 先预制,后期做统一调整
Object.assign(window, {
Toast, Dialog
})
从代码我们能看出来,每个组件都是按需引入,大大的降低了打包的体积,并且把Toast和Dialog注入到了window全局变量里面,为了方便我们直接调用。
2.解决vant样式适配问题,查看上面的postcss.config.js即可
3.在入口文件main.js 引入
import './utils/vant' // 引入局部ui
第七步:Axios的封装(公共头部、异常、不同请求方式配置处理)
1.创建src/utils/request.js
import axios from 'axios'
const codeMessage = {
200: '服务器成功返回请求的数据。',
201: '新建或修改数据成功。',
202: '一个请求已经进入后台排队(异步任务)。',
204: '删除数据成功。',
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
401: '用户没有权限(令牌、用户名、密码错误)。',
403: '用户得到授权,但是访问是被禁止的。',
404: '发出的请求是不存在的,服务器没有进行操作。',
406: '请求的格式不可得。',
410: '请求的资源被永久删除。',
422: '当创建一个对象时,发生一个验证错误。',
500: '服务器发生错误,请检查服务器。',
502: '网关错误。',
503: '服务不可用,服务器暂时过载或维护。',
504: '网关超时。'
}
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
}
// 检查返回状态
checkStatus (response) {
const responseData = response.data
// 服务器返回默认结果
if (response && (response.status === 200 || response.status === 304 || response.status === 400)) {
// 后台自定义错误
// 正常
if (responseData.status == 0) {
return responseData
}
// 登录过期
if (responseData.errorCode === 402 || responseData.status === 401) {
return Promise.reject(errorText)
}
return Promise.reject(responseData)
}
// 服务器错误
const errorText = response && (codeMessage[response.status] || response.statusText)
Promise.reject(response)
}
// 拦截器
interceptors (instance, scope) {
// 请求拦截
instance.interceptors.request.use(config => {
config.baseURL = baseURL;
config.scope = scope
return config
}, error => {
return Promise.reject(error)
})
// 响应拦截
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 请求封装
post (url, data, scope) {
return this.setRequest('post', url, data, scope)
}
// get 请求封装
get (url, data, scope) {
return this.setRequest('get', url, data, scope)
}
// post 请求封装
POST (url, data, scope) {
return this.setRequest('post', url, data, scope).then(this.success)
}
// get 请求封装
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.创建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配置(针对webpack进行了封装)
这一步我们进行了,icon图标雪碧图处理,打包文件哈希命名,解决缓存问题,本地接口代理处理,打包引入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 = {
// 开发环境
dev: {
css: [
],
js: [
]
},
// 生产环境
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: { // 输出雪碧图文件及样式文件,这个是打包后,自动生成的雪碧图和样式
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: { // 样式文件中调用雪碧图地址写法
cssImageRef: '../images/sprite.png'
},
spritesmithOptions: { // 让合成的每个图片有一定的距离
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, // 是否开启编译时是否不符合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 主要配置代理相关信息
module.exports = {
host: '0.0.0.0',
port: 8000,
https: false,
hotOnly: false,
proxy: {
'^/api': {
// 测试环境
target: process.env.VUE_APP_API,
changeOrigin: true, // 是否跨域
pathRewrite: {
'^/api': '' // 需要rewrite重写的, // /mock
}
}
}
}
第九步:常见工具类的配置(时间、正则、公共方法、数据字典)
1.创建src/utils/index.js
//校验输入文字为纯数字
export function validNumber(value) {
const reg = /^\d+$/;
return reg.test(value);
}
//校验输入的文字 --综合搜索
export function validText(value) {
const reg = /^([\u4E00-\u9FA5])*$/;
return reg.test(value);
}
//电话号码正则函数
export function checkPhone(value) {
const reg = /^[1][3,4,5,6,7,8,9][0-9]{9}$/;
return reg.test(value);
}
//邮箱正则函数
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位,数字、字母大小写组合
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; //奇数男 偶数女
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; //奇数男 偶数女
const sexfromIDcard2 = sexFlag2 % 2; //1男 0女
return {sex: sexfromIDcard2===1?0:1, birStr};
}
}
// 获取当前时间年月日时分秒
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}`;
}
}
//数组排序
export function compare(property) {
return function (a, b) {
var value1 = a[property];
var value2 = b[property];
return value1 - value2;
};
}
第十:总结
自己抽了一天时间,一遍搭建,一遍写文档,反复修改,可能里面还有很多需要完善地方,后期我会出整个的搭建的过程的视频,帮助大家更加直观的去理解和学习。
码字不易,如果有帮助到自己的地方或者看后对自己学习前端知识所有提升,请关注一下我的公众号,后期会有更多精品的内容推出,写出来和大家一起分享学习。
走过路过不要错过,既然都看到这个地方了,那就留下一个评论和点赞吧。
源码地址:https://github.com/fx35792/vue-mobile-template
原文地址:blog.sunnyfanfan.com/articles/20…
参考文献:
https://cli.vuejs.org/
https://vant-contrib.gitee.io/vant/#/zh-CN/
网友评论