手把手教你搭建集成Vue全家桶、vant和axios等模块的移动端脚手架

写在最前面

前端要封装一套顺手的开发脚手架,一个是提高开发效率,一个是公司中每个前端搭建的风格都不一样,项目比较难维护,所以就搞了这么一个东西,主要集成了以下内容:

  • vue(2.6.10)
  • vuex
  • vue-router
  • typescript
  • sass
  • 移动端界面适配,自动转为vw
  • 引入并封装了axios
  • 按需引入vant
  • husky + @commitlint/cli 规范化提交
  • ESlint
  • 优化了打包体积
  • 优化了构建速度

项目地址在这:github,如果有写的不对的地方欢迎指出;如果觉得还行,能否给个star,感谢~

检查环境

在搭建之前检查一下vue-cli的版本(vue --version)和node是否安装(node --version):

  • vue:3.0.4
  • node:v14.15.0

如果怕搭建环境的过程中网速影响安装,可以执行下列命令:

yarn config set registry https://registry.npm.taobao.org/

脚手架安装

vue create vue2-mobild-cli

不采用默认配置项,选择: Manually select features

这里我们要配置babelTyperScriptRouterVuexCSS Pre-processors(css预处理)、Linter Formatter(代码风格、格式校验)、Unit Testing(单元测试)和E2E Testing(端对端测试)。

手把手教你搭建集成Vue全家桶、vant和axios等模块的移动端脚手架

具体选择如下:

手把手教你搭建集成Vue全家桶、vant和axios等模块的移动端脚手架

  • Use class-style component syntax?:保持使用TypeScriptclass风格;
  • Use Babel alongside TypeScript?:使用BabelTypeScript一起用于自动检测的填充;
  • Use history mode for router?: 路由模式选择hash
  • Pick a CSS pre-processorCSS 预加载器我选择使用Sass
  • Linter / Formatter: 这里不采用TSLintTSLint的团队已经停止维护,且建议用ESLint的,这里使用Airbnb的规范;
  • Pick additional lint features:校验的时间选择保存的时候,这样可以即时更正嘛;
  • Pick a unit testing solution: 单元测试解决方案跟随主流Mocha + chai
  • Pick a E2E testing solution:端对端测试类型Cypress

看看安装之后运行是否成功。

提交规范

yarn add --dev husky @commitlint/config-conventional @commitlint/cli

husky 的作用就是在提交的时候,会通过读取husky的配置中的hooks可以理解成GIT生命钩子)自动触发某些动作。

@commitlint/config-conventional和@commitlint/cli校验commit是否符合提交规范,常见的提交类型有:

[
  'chore',    // 构建过程或辅助工具的变动
  'docs',     // 文档
  'feat',     // 新功能
  'fix',      // 修补bug
  'refactor', // 重构(即不是新增功能,也不是修改bug的代码变动)
  'revert',
  'style',    // 格式(不影响代码运行的变动)
  'test'      // 增加测试
];

新建commitlint.config.js

module.exports = {extends: ['@commitlint/config-conventional']};

配置package.json

"husky": {
    "hooks": {
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
}

然后测试一下:

git add .
git commit -m "add git-lint"           ## 会提示你不规范
git commit -m "feat: add git-lint"     ## 提交成功

搭配ESlint,确保每次提交代码前走一遍ESLint,修改package.json

"lint": "eslint --ext .js --ext .vue --ext .ts src",
"lint:fix": "vue-cli-service lint",

lint是检测代码是否符合规范;lint:fix是自动规范化代码。

"husky": {
    "hooks": {
      "pre-commit": "npm run lint",
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
 }

然后在任意一个文件中删除分号(因为现在ESLint默认是强制分号的),执行:

git add .
git commit  -m "feat: test ESLint"       ## 会报错
npm run lint:fix
git commit  -m "feat: test ESLint"       ## 提交成功

如果提交的时候看不惯这种报错:

CRLF will be replaced by LF in xxxx

可以配置:

git config --global core.autocrlf true

整理路由

我喜欢按模块分路由,如果现在项目中有个模块是笔记,我们可以新增笔记、查看笔记列表和详情,我们修改views目录,新增以下文件:

手把手教你搭建集成Vue全家桶、vant和axios等模块的移动端脚手架

每个文件大致是这样的内容:

<!--
  作者:Armouy
  创建时间:2021年01月25日 16:11:08
  备注:新增笔记
-->
<template>
  <div>新增笔记</div>
</template>

<script lang='ts'>
import { Component, Prop, Vue } from 'vue-property-decorator';

@Component
export default class AddNote extends Vue {}
</script>

<style scoped lang='scss'>

</style>

修改App.vue

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link> |
      <router-link to="/note/add">AddNote</router-link> |
      <router-link to="/note/list">NoteList</router-link>
    </div>
    <router-view/>
  </div>
</template>

然后删除router.ts,新建router文件夹:

手把手教你搭建集成Vue全家桶、vant和axios等模块的移动端脚手架

是的,没错,以后每新增一个模块,我们就可以根据这个模块新增一个路由文件,这样就不用所有路由都放在一个文件中了:

// note.ts
import Router, { RouteConfig } from 'vue-router';

const R = (name: string) => () => import(`@/views/note/${name}.vue`);

const RoutesConfig: RouteConfig[] = [{
  name: 'AddNote',
  path: '/note/add',
  component: R('Add'),
  meta: {
    title: '新增笔记',
  },
}, {
  name: 'NoteList',
  path: '/note/list',
  component: R('List'),
  meta: {
    title: '笔记列表',
  },
}, {
  name: 'NoteDetails',
  path: '/note/details',
  component: R('Details'),
  meta: {
    title: '笔记详情',
  },
}];

export default RoutesConfig;

// index.ts
import Vue from 'vue';
import Router, { RouteConfig } from 'vue-router';
import note from './note';

const R = (name: string) => () => import(`@/views/${name}.vue`);

Vue.use(Router);

const RoutesConfig: RouteConfig[] = [
  ...note,
  {
    name: 'Home',
    path: '/',
    component: R('Home'),
    meta: {
      title: '首页',
    },
  }, {
    name: 'About',
    path: '/about',
    component: R('About'),
    meta: {
      title: '关于',
    },
  }, {
    name: 'NotFound',
    path: '*',
    component: R('NotFound'),
    meta: {
      title: '404',
    },
  },
];

const createRouter = () => new Router({
  mode: 'hash',
  routes: RoutesConfig,
});
const router = createRouter();

export default router;

如果根据http://localhost:8080/#/note/add可以成功访问路径,就说明成功了。然后我们来补充一下index.ts的细节,加入三个钩子:

// 在路由跳转前触发,可以拦截未登录的情况
router.beforeEach((to: any, from: any, next: () => void): void => {
  const title = to.meta && to.meta.title ? to.meta.title : '首页';
  document.title = title;

  // check login, so that ...
                  
  next();
});

// 路由跳转后触发
router.afterEach((to: any, from: any): void => {
  // do something ...
});

// 跳转报错的情况
router.onError((error: any) => {
  // do something ...
});

最后,在App.vue上加上keep-alive,进行缓存:

<keep-alive>
  <router-view/>
</keep-alive>

配置样式

assets中新建如下文件:

  • var.scss:主要的变量,如颜色变量、字号变量等;
  • reset.css: 初始化样式;
  • common.scss: 公用样式。

main.ts中引入reset.csscommon.scss

import './assets/style/reset.scss';
import './assets/style/common.scss';

全局变量的使用需要配置vue.config.js(还有一种方法就是还可以搭配使用style-resources-loader):

// vue.config.js
const path = require('path');

module.exports = {
  css: {
    loaderOptions: {
      scss: {
        prependData: `@import "~@/assets/style/var.scss";`
      }
    }
  }
}

var.scss中加入变量:

$primaryColor:  rgba(41, 113, 255, 1);

然后在App.vue中测试一下:

color: $primaryColor;

颜色显示则成功。

界面适配

可以参考这篇文章,移动端布局之postcss-px-to-viewport

yarn add --dev postcss-px-to-viewport

新增.postcssrc.js

module.exports = {
  plugins: {
    autoprefixer: {}, // 用来给不同的浏览器自动添加相应前缀,如-webkit-,-moz-等等
    "postcss-px-to-viewport": {
      unitToConvert: "px", // 要转化的单位
      viewportWidth: 750, // UI设计稿的宽度
      unitPrecision: 6, // 转换后的精度,即小数点位数
      propList: ["*"], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
      viewportUnit: "vw", // 指定需要转换成的视窗单位,默认vw
      fontViewportUnit: "vw", // 指定字体需要转换成的视窗单位,默认vw
      selectorBlackList: ["wrap"], // 指定不转换为视窗单位的类名,
      minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
      mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
      replace: true, // 是否转换后直接更换属性值
      exclude: [/node_modules/], // 设置忽略文件,用正则做目录名匹配
      landscape: false // 是否处理横屏情况
    }
  }
};

运行之后会发现这段代码:

.home{
  height: 100px;
  font-size: 40px;
  text-align: center;
  line-height: 100px;
}

会被转为:

.home[data-v-fae5bece] {
    height: 13.333333vw;
    font-size: 5.333333vw;
    text-align: center;
    line-height: 13.333333vw;
}

引入vant

yarn add vant
yarn add --dev babel-plugin-import

修改babel.config.js

module.exports = {
  presets: [
    '@vue/app',
  ],
  plugins: [
    ['import', {
      libraryName: 'vant',
      libraryDirectory: 'es',
      style: true
    }, 'vant']
  ]
};

测试一下,在main.ts中使用:

import { Button } from 'vant';
Vue.use(Button);

Home.vue中使用:

<van-button type="primary">主要按钮</van-button>

能正常显示即成功。

为了代码好维护,在根目录新建plugins/vant

import Vue from 'vue';
import { Button } from 'vant';

Vue.use(Button);

修改main.ts

import './plugins/vant';

引入axios

yarn add axios
yarn add qs
yarn add --dev @types/qs

配置拦截器

src/utils下新建axios.ts,设置一下拦截器等内容:

import axios, { AxiosRequestConfig } from 'axios';

const instance: AxiosRequestConfig | any = axios.create({
  timeout: 100000,
  headers: {
    post: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
  },
  withCredentials: true,
});

instance.interceptors.request.use((config: AxiosRequestConfig) => {
  const token = localStorage.getItem('token');
  if (token && token !== '') {
    config.headers.Authorization = token;
  }
  console.log('config', config);
  return config;
});

instance.interceptors.response.use(
  (res: any) => res.data,
  (error: any) => {
    console.log('%c 接口异常 ', 'background-color:orange;color: #FFF;border-radius: 4px;', error);

    // 可以根据状态码做其他操作:登录过期时、网络请求不存在时...
  },
);

export default instance;

src/api下新建config.tsnote.tsindex.ts

// config.ts
// 一个项目可能会请求多个域名
const baseUrl: {[key: string]: any} = {
  crm: '/api',
  loan: '/loan',
};

export default baseUrl;

路由也是分模块的,我们把关于note的相关路由都写在note.ts中:

const note = {
  
};

export default note;

最后是index.ts

import note from './note';

export default {
  note,
};

挂载全局

为了方便调用,我们将axios请求挂载在全局:

// main.ts
import api from '@/api';
Vue.prototype.$api = api;

声明

如果我们直接使用this.$api会报错,我们需要改一下ts声明文件:

// shims-vue.d.ts
import Vue from 'vue';

// 对vue进行类型补充说明
declare module 'vue/types/vue' {
  interface Vue {
    $api: any
  }
}

然后再新建一个shims.d.ts文件:

declare module '*.vue' {
  import Vue from 'vue';

  export default Vue;
}

我们修改了shims-vue.d.ts,此时main.ts中的import App from './App.vue';会提示找不到这个模块,所以才新建了shimx.d.ts。(其实这个文件的内容就是原先shims-vue.d.ts中 的内容,但是两个写在一起时import App from './App.vue';会提示找不到这个模块)。

get请求

我们先实现一个get请求,获得列表:

import axios from '@/utils/axios';
import baseUrl from './config';

const note = {
  getList(){
    return axios.get(`${baseUrl.crm}/note/all`);
  },
};

export default note;

然后我们在home.vue中点击按钮只会调用一下:

private async handleClick(){
    this.$api.note.getList().then((res: any) => {
      console.log('res', res);
    });
}

这时候会报跨域的问题需要配置一下:

// vue.config.js
devServer: {
    hot: true,
    proxy: {
      '/api': {
        target: 'http://xxx.com/api',
        changeOrigin: true,
        ws: true,
        pathRewrite: {
          '^/api': '',
        },
      },
    },
  },

即我们请求api/note/all时会变为http://xxx.com/api/note/all。(这就是为啥上面我们的baseUrl中写的是没有域名的url前缀了)。

post请求

接下来是post请求,这里我们要配合一下qs这个库:

import axios from '@/utils/axios';
import qs from 'qs';
import baseUrl from './config';

const note = {
  getList(){
    return axios.get(`${baseUrl.crm}/note/all`);
  },
  add(params: any){
    return axios.post(`${baseUrl.crm}/note/add`, qs.stringify(params));
  }
};

export default note;
private async handleClick(){
    await this.$api.note.add({
      name: '客户姓名',
      mobile: '13536548511',
    }).then((res: any) => {
      console.log(res);
    });
  }

需要考虑一下使用FormData上传文件的情况

import axios from '@/utils/axios';
import qs from 'qs';
import baseUrl from './config';

const note = {
 getList(){
    return axios.get(`${baseUrl.crm}/note/all`);
  },
  add(params: any){
    return axios.post(`${baseUrl.crm}/note/add`, qs.stringify(params));
  },
  upload(params: any){
    return axios.request({
      url: `${baseUrl.crm}/note/upload`,
      method: 'post',
      headers: { 'Content-Type': 'multipart/form-data' },
      data: params,
    });
  },
};

export default note;

private async handleClick(){
    const param = new FormData();
    param.append('file', this.file);
    await this.$api.note.upload(param);
  }

最后我们来区分一下生产环境和测试环境,修改一下config.ts

const env = process.env.NODE_ENV;
let crmUrl;
let loanUrl;
switch (env){
  case 'development':
    crmUrl = '/api';
    loanUrl = '/loan';
    break;
  case 'production':
    crmUrl = 'http://aaa.com/api';
    loanUrl = 'https://bbb.com/loan';
    break;
  default:
    crmUrl = '/api';
    loanUrl = '/loan';
    break;
}

const baseUrl: {[key: string]: any} = {
  crm: crmUrl,
  loan: loanUrl,
};

export default baseUrl;

到这里axios的配置就差不多了。

使用vuex

yarn add vuex-module-decorators vuex-class
yarn add vuex-class

vuex依旧采用的是分模块的思想,删除原先的store.ts,在根目录新建store文件夹:

手把手教你搭建集成Vue全家桶、vant和axios等模块的移动端脚手架

note.ts中我们来实现state、mutation和action的写法:

import api from '@/api';
import {
  VuexModule, Module, Mutation, Action,
} from 'vuex-module-decorators';

@Module({ namespaced: true })
export default class NoteModule extends VuexModule {
  public noteList: any[] = [];

  @Mutation
  public initNote(noteList: any[]){
    this.noteList = noteList;
  }

  @Action({ commit: 'initNote' })
  async initNoteList(){
    const res = await api.note.getList();
    return res.data ? res.data : [];
  }
}

store/index.ts中使用:

import Vue from 'vue';
import Vuex from 'vuex';
import NoteModule from './modules/note';

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    Note: NoteModule,
    // 其他模块...
  },
});

如何使用呢?回到home.vue中:

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { namespace } from 'vuex-class';

const NoteModule = namespace('Note');

@Component
export default class Home extends Vue {
  @NoteModule.State('noteList') public noteList: any[] | undefined;
  @NoteModule.Action('initNoteList') public initNoteList: any;

  private async handleClick(){
   // ...
  }

  private async handleClickGetVuex(){
    console.log(this.noteList);
    await this.initNoteList();
    console.log(this.noteList);
  }
}
</script>

优化打包和构建速度

最主要的配置打包压缩,代码经常重构优化也有助于体积减少哦,其他常见的优化可以查看我之前写的 Vue项目优化小结Vue项目优化小结(二)

yarn add --dev speed-measure-webpack-plugin compression-webpack-plugin hard-source-webpack-plugin
const path = require('path');
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const CompressionWebpackPlugin = require('compression-webpack-plugin');
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');

const productionGzipExtensions = ['js', 'css'];
const resolve = dir => path.join(__dirname, dir);

module.exports = {
  // 是否在构建生产包时生成 sourceMap 文件,false将提高构建速度
  productionSourceMap: false,
  publicPath: './',
  css: {
    loaderOptions: {
      scss: {
        prependData: '@import "~@/assets/style/var.scss";',
      },
    },
  },
  configureWebpack: () => {
    const plugins = [];
    plugins.push(new SpeedMeasurePlugin());
    if (process.env.NODE_ENV === "'production'"){
      plugins.push(new CompressionWebpackPlugin({
        filename: '[path].gz[query]',
        algorithm: 'gzip',
        test: productionGzipExtensions,
        threshold: 10240,
        minRatio: 0.8,
        deleteOriginalAssets: false, // 是否删除源文件
      }));
    } else {
      plugins.push(new HardSourceWebpackPlugin());
    }

    return {
      output: {
        filename: 'js/[name].js',
        chunkFilename: 'js/[name].js',
      },

      plugins,
    };
  },

  devServer: {
    hot: true,
    proxy: {
      '/api': {
        target: 'http://crmtest.8kqw.com/api',
        changeOrigin: true,
        ws: true,
        pathRewrite: {
          '^/api': '',
        },
      },
    },
  },
};

最后

附上github地址: vue2-mobile-cli
如有不对欢迎指出与交流,感谢阅读~

上一篇:002.小程序-VantWeapp组件库


下一篇:Nuxt使用vant导航栏组件tabbar