美文网首页Vue
手把手教你通过vue-cli搭建手机端框架

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

作者: 读书的鱼 | 来源:发表于2020-09-25 01:13 被阅读0次

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

    初衷:有的前端的小伙伴要说了,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
    原文地址:blog.sunnyfanfan.com/articles/20…

    参考文献:
    https://cli.vuejs.org/
    https://vant-contrib.gitee.io/vant/#/zh-CN/

    相关文章

      网友评论

        本文标题:手把手教你通过vue-cli搭建手机端框架

        本文链接:https://www.haomeiwen.com/subject/idjxuktx.html