搭建 Vue CLI 4.x + Webpack5 + Vue 3.x 移动端框架

678 阅读12分钟

搭建 Vue CLI 4.x + Webpack5 + Vue 3.x 移动端框架

一、技术栈

  1. 这是基于 Vue CLI4 实现的移动端框架,其中包含常用的配置
  2. 技术选型:Vue CLI4 + Webpack5 + JavaScript(不上Ts)+ Vue 3.x + Vue Router 4.x + Vuex 4.x + Less + Vant-ui + rem适配 + eslint + husky + lint-staged
  3. 主要技术点包括:
    • Vue CLI4 脚手架
    • Vant 按需引入
    • 移动端 rem 适配
    • axios 拦截封装
    • vue-router 配置
    • 登录权限校验
    • Vuex 模块化配置
    • 多环境变量配置
    • vue.config.js 配置
    • 跨域代理设置
    • 代码规范
    • 可视化分析
    • 优化总结

二、项目搭建

请确保电脑上安装了 Node.js ,本项目采用 Vue CLI 快速搭建项目,Vue CLI 4.x 需要 Node.js v8.9 或更高版本 (推荐 v10 以上)。

查看 Node.js 版本

node -v

安装 Vue CLI

npm install -g @vue/cli
# OR
yarn global add @vue/cli

查看vue-cli 版本

vue -V
# OR
vue --version

使用 Vue CLI 快速创建一个新项目

1. 输入项目名称 - 例如:vue3-hello-world
vue create vue3-hello-world
2. 选择模板
Check the features needed for your project:
 (*) Choose Vue version
 (*) Babel
 ( ) TypeScript
 ( ) Progressive Web App (PWA) Support
 (*) Router
 (*) Vuex
 (*) CSS Pre-processors
 (*) Linter / Formatter
 ( ) Unit Testing
 ( ) E2E Testing
3. 选择对应的配置,等待项目创建
4. 安装依赖
npm install
5. 启动项目
npm run serve

三、目录结构

shop-web-h5 ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── prettier.config.js ├── public │ ├── baseurl.js │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── api │ │ ├── api.js │ │ ├── errorCode.js │ │ └── request.js │ ├── assets │ │ ├── css │ │ ├── iconfont │ │ ├── images │ │ └── less │ ├── components │ │ ├── Coupon.vue │ │ ├── Header.vue │ ├── config │ │ └── index.js │ ├── main.js │ ├── router │ │ └── index.js │ ├── store │ │ ├── index.js │ │ └── modules │ ├── utils │ │ ├── common.js │ │ ├── pointed.js │ │ ├── useMapper.js │ │ └── validate.js │ └── views │ ├── about │ ├── home └── vue.config.js

四、配置 Vant

vant 是轻量、可靠的移动端 Vue 组件库,使用 Vant 3.x 版本的文档,适用于 Vue 3 开发。

1. 安装
npm i vant@3
# OR
yarn add vant@3
2. 按需引入

对于第三方 UI 组件,如果全部引入,会造成打包体积过大,不推荐,babel-plugin-import 是一款 babel 插件,它会在编译过程中将 import 语句自动转换为按需引入的方式。

# 安装插件
npm i babel-plugin-import -D
3. 在 .babelrc 或 babel.config.js 中添加配置:
// babel.config.js
module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset',
  ],
  plugins: [
    [
      'import',
      {
        libraryName: 'vant',
        libraryDirectory: 'es',
        style: true,
      },
    ],
  ],
};
4. 组件用法 - 局部注册
import { Button } from 'vant';

export default {
  components: {
    [Button.name]: Button,
  },
};

五、rem 布局适配

postcss-pxtorem 是一款 PostCSS 插件,用于将 px 单位转化为 rem 单位; lib-flexible 用于设置 rem 基准值;

1. 安装插件
npm install lib-flexible -S
npm install postcss-pxtorem -D
2. main.js 里 引入 lib-flexible
import 'lib-flexible';
3. 设计稿基于750px,vant 基于375px 的设计稿,新增 .postcssrc.js 文件
// postcssrc.js

module.exports = {
  plugins: {
    // postcss-pxtorem 插件的版本需要 >= 5.0.0
    'postcss-pxtorem': {
      rootValue({ file }) {
        return file.indexOf('vant') !== -1 ? 37.5 : 75;
      },
      propList: ['*'],
    },
  },
};

六、axios 请求封装

1. 设置请求拦截和响应拦截
// src/api/request.js

import axios from 'axios';
import { Toast } from 'vant';
import cookies from 'js-cookie';
import errorCode from './errorCode';

// 请求超时时间
axios.defaults.timeout = 15000;
// post请求头
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';

// 请求拦截器
axios.interceptors.request.use(
  (config) => {
    const token = cookies.get('mobile-login-token')
    return {
      ...config,
      headers: {
        ...config.headers,
        'mobile-login-token': token || '',
        'x-flag': 'h5',
      }
    }
  },
  (error) => Promise.error(error)
)

// 响应拦截
axios.interceptors.response.use((res) => {
  const { message, result } = res.data

  // 当result=0或者特殊错误码时抛出res.data
  if (result === 0 || errorCode.includes(result)) {
    return Promise.resolve(res.data);
  }
  Toast(message || '服务器内部错误')
  return Promise.reject(res.data)
}, (error) => {
  const code = error.response.status
  // 接口返回401,重定向到登录页面
  if (code === 401) {
    cookies.set('mobile-login-token', '')
    localStorage.clear()
    Toast({
      message: '登录过期了',
      type: 'fail',
      duration: 2 * 1000,
    })
    // 跳转到登录页
    return Promise.reject()
  }
  Toast({
    message: '网络异常',
    type: 'fail',
    duration: 2 * 1000,
  })
  return Promise.reject(error)
})

/*
 * get方法,对应get请求
 * @param{String}url[请求的url地址]
 * @param{Object}params[请求时携带的参数]
 */
export function get(url, params) {
  return new Promise((resolve, reject) => {
    axios
      .get(url, {
        params,
      })
      .then((res) => {
        resolve(res);
      })
      .catch((err) => {
        reject(err);
      })
  })
}

/*
 *post方法,对应post请求
 * @param{String}url[请求的url地址]
 * @param{Object}params[请求时候携带的参数]
 */
export function post(url, params) {
  return new Promise((resolve, reject) => {
    axios
      .post(url, params)
      .then((res) => {
        resolve(res)
      })
      .catch((err) => {
        reject(err)
      })
  })
}
2. 异常业务码统一维护
// src/api/errorCode.js

const errorCode = [
  1319, // login-用户未注册,跳转验证码登录
];
export default errorCode;
3. api 接口列表统一维护
// src/api/api.js

import { post } from './request'

export const getShopInfo = (params) => post('/xxxx/xxxx/xxxx', params)

七、vue-router 配置

1. 路由懒加载配置

es6提案的import()

component: () => import('../views/Home.vue')
2. 登录权限校验

有些页面是不需要登录即可访问的,如首页,商品详情页等,都是用户在任何情况都能看到的;有些是需要登录后才能访问的,如购物车等。此时就需要对页面访问进行控制了; 此外,像一些需要记录用户信息和登录状态的项目,也是需要做登录权限校验的

配置路由的 meta 对象的 auth 属性

在路由守卫进行判断。当to.meta.auth为true(需要登录),且不存在登录信息缓存时,需要重定向去登录页面

3. 页面缓存配置

项目中,有一些页面我们是希望加载一次就缓存下来的,此时就用到 keep-alive 了,keep-alive 是 Vue 提供的一个抽象组件,用来对组件进行缓存,从而节省性能

通过配置路由的 meta 对象的 keepAlive 属性值来区分页面是否需要缓存

在 App.vue 做缓存判断

<template>
  <router-view v-if="!$route.meta.keepAlive" :key="$route.fullPath"></router-view>
  <router-view v-slot="{ Component }" v-if="$route.meta.keepAlive">
    <keep-alive>
      <component :is="Component" />
    </keep-alive>
  </router-view>
</template>
4. router/index.js
// router/index.js

import { createRouter, createWebHashHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    redirect: '/home',
  },
  {
    path: '/home',
    name: 'Home',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "home" */ '../views/home/Home.vue'),
    meta: { keepAlive: true, auth: false },
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('../views/login/Login.vue'),
    meta: { keepAlive: false, auth: false },
  },
  {
    path: '/:w+',
    redirect: '/',
  },
]

const router = createRouter({
  history: createWebHashHistory(),
  routes,
  scrollBehavior: () => ({ y: 0 }), // 路由跳转后页面回到顶部
})

router.beforeEach((to, from, next) => {
  const userInfo = getCache('userInfo')
  // 当to.meta.auth 为 true,且不存在用户信息时,需要重定向到登录页面
  if (!userInfo && to.meta.auth) {
    next({ path: '/login/password', query: { redirect: to.path } })
    return false
  }
  next();
  return true
})
export default router

八、Vuex 模块化配置

1. vuex 是什么 ?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。 vuex

2. 什么情况下应该使用 Vuex ?

如果您的应用够简单,最好不要使用 Vuex,使用 Vuex 可能是繁琐冗余的。 但是,如果您需要构建一个中大型单页应用,你很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。

3. Modules(模块化)

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。 因此,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter。

4. 模块化配置使用

main.js 中引入

// main.js

import { createApp } from 'vue'
import store from './store'

const app = createApp(App)
app.use(store)

创建store

// store/index.js

import { createStore } from 'vuex'
import global from './modules/global'
import order from './modules/order'
import confirm from './modules/confirm'

export default createStore({
  ...global, // 全局状态
  modules: {
    order,
    confirm,
  },
})

示例

// store/modules/order.js

import { reqGetOrderList } from '@/api/api'

export default {
  namespaced: true, // 命名空间

  state: {
    orderList: [], // 订单列表
    pageIndex: 1, // 当前页码
    pageSize: 10, // 每页条数
  },

  mutations: {
    // 更新订单列表
    UPDATE_ORDERS(state, data) {
      state.orderList = data.orderList
    },
  },

  actions: {
    // 获取订单列表
    async getOrderList({ commit, state }, params) {
      const { pageIndex, pageSize } = state

      // 发送请求
      const {
        data: { records, total },
      } = await reqGetOrderList({ ...params, pageIndex, pageSize })

      let orderList = records

      // 判断是否是加载下一页数据
      if (pageIndex > 1) {
        orderList = [...state.orderList, ...records]
      }

      state.pageIndex += 1

      commit('UPDATE_ORDERS', { orderList, total })
    },
  },
}
5. 组合式API

访问 State 和 Getter 为了访问 state 和 getter,需要创建 computed 引用以保留响应性

import { computed } from 'vue'
import { useStore } from 'vuex'

export default {
  setup () {
    const store = useStore()

    return {
      // 在 computed 函数中访问 state
      count: computed(() => store.state.count),

      // 在 computed 函数中访问 getter
      double: computed(() => store.getters.double)
    }
  }
}

访问 Mutation 和 Action 要使用 mutation 和 action 时,只需要在 setup 钩子函数中调用 commit 和 dispatch 函数。

import { useStore } from 'vuex'

export default {
  setup () {
    const store = useStore()

    return {
      // 使用 mutation
      increment: () => store.commit('increment'),

      // 使用 action
      asyncIncrement: () => store.dispatch('asyncIncrement')
    }
  }
}
6. Vue 3.x 中使用 Vuex 的辅助函数

模块化的情况下封装一个 hooks

// src/utils/useMapper.js

import { computed } from 'vue'
import { mapGetters, mapState, useStore, createNamespacedHelpers } from 'vuex'

const useMapper = (mapper, mapFn) => {
  const store = useStore()

  const storeStateFns = mapFn(mapper)
  const storeState = {}
  Object.keys(storeStateFns).forEach((keyFn) => {
    const fn = storeStateFns[keyFn].bind({ $store: store })
    storeState[keyFn] = computed(fn)
  })

  return storeState
}

export const useState = (moduleName, mapper) => {
  let myMapper = mapper
  let mapperFn = mapState
  if (typeof moduleName === 'string' && moduleName.length > 0) {
    mapperFn = createNamespacedHelpers(moduleName).mapState
  } else {
    myMapper = moduleName
  }
  return useMapper(myMapper, mapperFn)
}

export const useGetters = (moduleName, mapper) => {
  let myMapper = mapper
  let mapperFn = mapGetters
  if (typeof moduleName === 'string' && moduleName.length > 0) {
    mapperFn = createNamespacedHelpers(moduleName).mapGetters
  } else {
    myMapper = moduleName
  }
  return useMapper(myMapper, mapperFn)
}

使用

import { useState } from '@/utils/useMapper'

setup() {
  const storeState = useState('confirm', ['checkboxBean', 'checkboxCard', 'extraOrderParams'])
  return {
    ...storeState
  }
}

九、多环境变量配置

一般情况下我们的项目会有三个环境,本地环境(development),测试环境(test),生产环境(production),我们可以在项目根目录下建三个配置环境变量的文件.env.development.env.test.env.production

一个环境文件只包含环境变量的“键=值”对:

NODE_ENV = 'production'
VUE_APP_ENV = 'production' // 只有VUE_APP开头的环境变量可以在项目代码中直接使用

除了 VUE_APP_* 变量之外,在你的应用代码中始终可用的还有两个特殊的变量:

  • NODE_ENV - 会是 "development""production""test" 中的一个。具体的值取决于应用运行的模式。
  • BASE_URL - 会和 vue.config.js 中的 publicPath 选项相符,即你的应用会部署到的基础路径。

下面我们开始配置我们环境变量

1. 在项目根目录下新建 .env.*
  • .env.development 本地开发环境配置
 NODE_ENV = 'development'
 VUE_APP_ENV = 'development'
 VUE_APP_PIC_URL = 'https://xxxxxx.obs.cn-south-1.myhuaweicloud.com'
  • .env.test 测试环境配置
 NODE_ENV = 'production'
 VUE_APP_ENV = 'test'
 VUE_APP_PIC_URL = 'https://xxxxxx.obs.cn-south-1.myhuaweicloud.com'
  • .env.production 生产环境配置
 NODE_ENV = 'production'
 VUE_APP_ENV = 'production'
 VUE_APP_PIC_URL = 'https://xxxxxxx.oss-cn-shenzhen.aliyuncs.com'

模式 - 默认情况下,一个 Vue CLI 项目有三个模式:

  • development 模式用于 vue-cli-service serve
  • test 模式用于 vue-cli-service test:unit
  • production 模式用于 vue-cli-service build

你可以通过传递 --mode 选项参数为命令行覆写默认的模式。例如,如果你想要在构建命令中使用开发环境变量:

vue-cli-service build --mode development

当运行 vue-cli-service 命令时,所有的环境变量都从对应的环境文件中载入。如果文件内部不包含 NODE_ENV 变量,它的值将取决于模式。

NODE_ENV 将决定您的应用运行的模式,是开发,生产还是测试,因此也决定了创建哪种 webpack 配置。

使用环境变量

console.log('VUE_APP_ENV:', process.env.VUE_APP_ENV);
console.log('VUE_APP_PIC_URL:', process.env.VUE_APP_PIC_URL);
2. 配置打包命令

package.jsonscripts 不同环境的配置

"scripts": {
  "serve": "vue-cli-service serve",
  "build:dev": "vue-cli-service build --mode development",
  "build:test": "vue-cli-service build --mode test",
  "build": "vue-cli-service build",
  "report": "vue-cli-service build --report",
  "prepare": "husky install",
  "lint": "vue-cli-service lint"
},

打包命令

  • 启动本地开发:npn run serve
  • 打包开发环境:npm run build:dev
  • 打包测试环境:npm run build:test
  • 打包生产环境:npm run build
3. 使用命令

vue-cli-service serve

用法:vue-cli-service serve [options] [entry]

选项:

  --open    在服务器启动时打开浏览器
  --copy    在服务器启动时将 URL 复制到剪切版
  --mode    指定环境模式 (默认值:development)
  --host    指定 host (默认值:0.0.0.0)
  --port    指定 port (默认值:8080)
  --https   使用 https (默认值:false)

vue-cli-service build

用法:vue-cli-service build [options] [entry|pattern]

选项:

  --mode        指定环境模式 (默认值:production)
  --dest        指定输出目录 (默认值:dist)
  --target      app | lib | wc | wc-async (默认值:app)
  --name        库或 Web Components 模式下的名字 (默认值:package.json 中的 "name" 字段或入口文件名)
  --no-clean    在构建项目之前不清除目标目录
  --report      生成 report.html 以帮助分析包内容
  --report-json 生成 report.json 以帮助分析包内容
  --watch       监听文件变化

十、vue.config.js 配置

新建的脚手架项目我们可以在 vue.config.js 中配置项目中的东西。主要包括:

  • 打包后文件输出位置
  • 配置 alias 别名
  • 本地开发跨域代理设置

此外,还有很多属于优化打包的配置,后面补充...

const path = require('path')
const baseurl = require('./public/baseurl')

function resolve(dir) {
  return path.join(__dirname, dir)
}

module.exports = {
  // 选项...
  publicPath: './', // 默认为'/'
  devServer: {
    open: false, // 配置自动启动浏览器
    inline: true,
    port: 8081,
    hot: true, // 热更新
    // 配置多个代理
    proxy: {
      '/mallv2': {
        target: baseurl.BASE_API,
        changeOrigin: true,
      },
    },
  },
  chainWebpack: (config) => {
    // 配置别名
    config.resolve.alias
      .set('@', resolve('src'))
  },
}

配置本地开发代理的环境

module.exports = {
  // BASE_API: 'https://devxxx.xxx.com', // 开发环境
  BASE_API: 'https://testxxx.xxx.com', // 测试环境
}

十一、代码规范

随着前端应用变得越来越大型化和复杂,一个项目中多个人合作开发很常见,然而每个人的编码能力和风格不一致,会导致代码的健壮性和可读性变差,口头约定和代码审查沟通成本太高,因此我们不得不在项目中使用一些工具来约束代码规范。

使用 EditorConfig + ESLint 组合来实现代码规范化。

1. 集成 Eslint 配置

在搭建框架时选择 eslint 配置,在跟目录下生成 .eslintrc.js 配置文件

module.exports = {
  root: true,
  env: {
    node: true,
  },
  extends: [
    'plugin:vue/vue3-essential',
    '@vue/airbnb',
  ],
  parserOptions: {
    parser: 'babel-eslint',
  },
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
  },
};

根据项目实际情况,需要配置额外的 Eslint 规则,在此文件中追加 VSCode 使用 ESLint 配置文件需要去插件市场下载插件 ESLint

2. 集成 EditorConfig 配置

EditorConfig 有助于为不同 IDE 编辑器上处理同一项目的多个开发人员维护一致的编码风格。

在项目根目录下增加 .editorconfig 文件:

# Editor configuration, see http://editorconfig.org

# 表示是最顶层的 EditorConfig 配置文件
root = true

[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行首的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行

[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false

VSCode 使用 EditorConfig 需要去插件市场下载插件 EditorConfig for VS Code

3. 集成 husky 和 lint-staged

我们在项目中已经集成了 ESlint 和 EditorConfig,在编码时,这些工具可以对我们写的代码进行实时校验,一定程度上能规范我们的代码,但团队还是有人觉得条条框框麻烦,视‘提示’视而不见,依然按照自己的风格写代码,开发完直接把代码提交到仓库,就会导致其他人 pull 最新代码后,会报错,还要修复其他人的问题,影响开发体验。 因此,还需要做一些限制,新增了钩子检验 husky + lint-staged,每次git commit 会校验缓存区代码,简单的eslint 问题会自动修复,不能修复的问题会中断提交,必须修复后才能提交到仓库,从而保证仓库代码都是符合规范的。

husky —— Git Hook 工具,可以设置在 git 各个阶段(pre-commitcommit-msg、pre-push 等)触发我们的命令。
lint-staged —— 在 git 暂存的文件上运行 linters。

配置husky

使用 husky-init 命令快速在项目初始化一个 husky 配置。

npx husky-init && npm install

这行命令做了四件事:

  • 安装 husky 到开发依赖

  • 在项目根目录下创建 .husky 目录

  • 在 .husky 目录创建 pre-commit hook,并初始化 pre-commit 命令为 npm test pre-commit

  • 修改 package.json 的 scripts,增加 "prepare": "husky install"

到这里,husky 配置完成。

pre-commit hook 文件的作用是:当我们执行 git commit -m "xxx" 时,会先对 src 目录下所有的文件执行 eslint --fix 命令,如果 ESLint 通过,成功 commit,否则终止 commit。 但是,如果我们只改动了一两个文件,却要对所有的文件执行 eslint --fix,显然不是我们想要的效果,我们只需要做到用 ESLint 修复自己此次写的代码,而不去影响其他的代码,所以我们还需借助另外一个工具lint-staged

配置 lint-staged

lint-staged 这个工具一般结合 husky 来使用,它可以让 husky 的 hook 触发的命令只作用于 git add 那些文件(即 git 暂存区的文件),而不会影响到其他文件。

1.安装 lint-staged

npm i lint-staged -D

2.在 package.json 里增加 lint-staged 配置项

"lint-staged": {
  "*.{vue,js}": "vue-cli-service lint"
},

lint-staged

这行命令表示:只对 git 暂存区的 .vue、.js 文件执行 vue-cli-service lint

3.修改 .husky/pre-commit hook 的触发命令为:npx lint-staged

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged

至此,husky 和 lint-staged 组合配置完成。

验证一下

故意写一行不符合规范的代码,声明一个变量但不使用:

const test = 'lint-staged'

执行 git commit -m "feat(): xxx"命令,效果如下:

git-commit

可以看出来,ESlint 校验未通过,提交被中断,我们需要去修复报错再次提交。

这些工具并不是必须的,没有它们同样可以可以完成功能开发,但是利用好这些工具,你可以写出更高质量的代码。

无论写代码还是做其他事情,都应该用长远的眼光来看,刚开始使用 ESlint 的时候可能会有很多问题,改起来也很费时费力,只要坚持下去,代码质量、开发效率、个人水平都会得到提升,前期的付出都是值得的。

十二、可视化分析

执行打包命令npm run report,在 dist 目录下生成 report.html 以帮助分析包内容

  "scripts": {
    "report": "vue-cli-service build --report",
  },

打开后,看到这样一份报告,依赖图

从以上的界面中,我们可以得到以下信息:

  • 打包出的文件中都包含了什么,以及模块之间的依赖关系
  • 每个文件的大小在总体中的占比,找出较大的文件,思考是否有替换方案,是否使用了它包含了不必要的依赖?
  • 是否有重复的依赖项,对此可以如何优化?
  • 每个文件的压缩后的大小。

十三、优化总结

后续项目开发完成结合实际情况补充,敬请期待...

十四、最后

本文是结合近段时间做的项目,从技术选型、项目搭建、代码规范做了一个小的总结。

文章篇幅较长,涉及技术点较多,难免出现错误,欢迎大家多多指正,谢谢!