搭建 Vue CLI 4.x + Webpack5 + Vue 3.x 移动端框架
一、技术栈
- 这是基于 Vue CLI4 实现的移动端框架,其中包含常用的配置
- 技术选型:Vue CLI4 + Webpack5 + JavaScript(不上Ts)+ Vue 3.x + Vue Router 4.x + Vuex 4.x + Less + Vant-ui + rem适配 + eslint + husky + lint-staged
- 主要技术点包括:
- 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 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
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.json
里 scripts
不同环境的配置
"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-commit、commit-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
-
修改 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"
},
这行命令表示:只对 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"
命令,效果如下:
可以看出来,ESlint 校验未通过,提交被中断,我们需要去修复报错再次提交。
这些工具并不是必须的,没有它们同样可以可以完成功能开发,但是利用好这些工具,你可以写出更高质量的代码。
无论写代码还是做其他事情,都应该用长远的眼光来看,刚开始使用 ESlint 的时候可能会有很多问题,改起来也很费时费力,只要坚持下去,代码质量、开发效率、个人水平都会得到提升,前期的付出都是值得的。
十二、可视化分析
执行打包命令npm run report
,在 dist 目录下生成 report.html 以帮助分析包内容
"scripts": {
"report": "vue-cli-service build --report",
},
打开后,看到这样一份报告,依赖图
从以上的界面中,我们可以得到以下信息:
- 打包出的文件中都包含了什么,以及模块之间的依赖关系
- 每个文件的大小在总体中的占比,找出较大的文件,思考是否有替换方案,是否使用了它包含了不必要的依赖?
- 是否有重复的依赖项,对此可以如何优化?
- 每个文件的压缩后的大小。
十三、优化总结
后续项目开发完成结合实际情况补充,敬请期待...
十四、最后
本文是结合近段时间做的项目,从技术选型、项目搭建、代码规范做了一个小的总结。
文章篇幅较长,涉及技术点较多,难免出现错误,欢迎大家多多指正,谢谢!