手把手教你通过vue-cli搭建手机端框架

前言:欢迎前端的小伙伴们前来围观、学习借鉴,如果你是后端、测试和其他的小伙伴也没关系,如果自己也想玩一下前端,想搭建一个前端的框架,那么不妨静下心来看看这篇文章。如果你不是从事开发工作的人员,内容可能相对而言比较枯燥,但是如果想找错别字,也不妨进来看看。

初衷:有的前端的小伙伴要说了,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
  1. 在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
原文地址:http://blog.sunnyfanfan.com/articles/2020/09/24/1600938805892.html
参考文献:
https://cli.vuejs.org/
https://vant-contrib.gitee.io/vant/#/zh-CN/

  • 7
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值