vue项目工程化,大大提升开发效率和开发体验
前言
(大神绕路)vue create project-name创建的项目,基本上满足开发使用的需求。但是,在开发过程中,会发现一些问题:
- eslint+prettier检查发现的错误,没办法保存代码时自动修复,或者两者格式化存在冲突,比如格式化代码后,eslint发现的错误消除了,但是prettier依然提示代码格式不对;
- 使用公共组件时,都需要在业务组件内单独引入一次;
- 多人协同开发时,git提交日志信息格式不统一,杂乱无章;
等等基本优化都可以在接下来的说明中,一一解决,让你在开发中,如鱼得水,轻松自在。
一、代码格式化代码,样式格式化
1.安装eslint相关依赖 (如果vue create时选择了eslint+prettier,可以跳过步骤1和步骤3 的依赖安装)
npm i -D eslint @vue/cli-plugin-eslint eslint-plugin-vue @typescript-eslint/eslint-plugin @typescript-eslint/parser
如果需要校验的代码包含typescript,需要安装
npm i -D typescript
2.初始化生成eslint配置文件.eslintrc.js
node_modules/.bin/eslint --init
3.安装prettier相关依赖
npm i -D prettier eslint-plugin-prettier
4.在.eslintrc.js中关联prettier,相关属性增加内容
//.eslintrc.js
{
"plugins": ["prettier"],
"rules": {
"prettier/prettier": "warn"
}
}
5.创建prettier的配置文件,项目根目录新建prettier.config.js,规则根据自己的需要设置
//prettier.config.js
module.exports = {
// 单行最大长度
printWidth: 100,
// 设置编辑器每一个水平缩进的空格数
tabWidth: 2,
// 在句尾添加分号
semi: true,
// 使用单引号
singleQuote: true,
jsxSingleQuote: true,
// 在任何可能的多行中输入尾逗号。
trailingComma: 'all',
// 在对象字面量声明所使用的的花括号后({)和前(})输出空格
bracketSpacing: true,
// 在多行JSX元素最后一行的末尾添加 > 而使 > 单独一行(不适用于自闭和元素)
jsxBracketSameLinte: false,
// 为单行箭头函数的参数添加圆括号。
alwaysParens: 'always',
// 行结束
endOfLine: 'lf',
};
6.结合webpack设置保存时自动格式化,仅对vue的src目录生效
- 安装eslint-loader依赖
npm i -D eslint-loader
- 在vue.config.js(项目没有的话,在根目录自行创建)中增加代码
module.exports = {
chainWebpack: (config) => {
config.module
.rule('eslint')
.use('eslint-loader')
.loader('eslint-loader')
.tap((options) => {
options.fix = true
return options
})
},
}
7.非vue项目/src目录下文件的格式校验,需要结合vscode编辑器功能配置,根目录创建文件 .vscode/settings.json
//settings.json
{
"editor.formatOnSave": true
}
8.如果需要让样式代码也格式化,可以执行以下操作:可以统一整理样式代码的顺序
8.1.安装依赖
npm i -D stylelint stylelint-config-standard stylelint-order
8.2.项目根目录创建stylelint配置文件.stylelintrc
//.stylelintrc
{
"extends": "stylelint-config-standard",
"plugins": [
"stylelint-order"
],
"rules": {
"order/order": [
"declarations",
"custom-properties",
"dollar-variables",
"rules",
"at-rules"
],
// 规定样式顺序
"order/properties-order": [
"position",
"z-index",
"top",
"bottom",
"left",
"right",
"float",
"clear",
"columns",
"columns-width",
"columns-count",
"column-rule",
"column-rule-width",
"column-rule-style",
"column-rule-color",
"column-fill",
"column-span",
"column-gap",
"display",
"grid",
"grid-template-rows",
"grid-template-columns",
"grid-template-areas",
"grid-auto-rows",
"grid-auto-columns",
"grid-auto-flow",
"grid-column-gap",
"grid-row-gap",
"grid-template",
"grid-template-rows",
"grid-template-columns",
"grid-template-areas",
"grid-gap",
"grid-row-gap",
"grid-column-gap",
"grid-area",
"grid-row-start",
"grid-row-end",
"grid-column-start",
"grid-column-end",
"grid-column",
"grid-column-start",
"grid-column-end",
"grid-row",
"grid-row-start",
"grid-row-end",
"flex",
"flex-grow",
"flex-shrink",
"flex-basis",
"flex-flow",
"flex-direction",
"flex-wrap",
"justify-content",
"align-content",
"align-items",
"align-self",
"order",
"table-layout",
"empty-cells",
"caption-side",
"border-collapse",
"border-spacing",
"list-style",
"list-style-type",
"list-style-position",
"list-style-image",
"ruby-align",
"ruby-merge",
"ruby-position",
"box-sizing",
"width",
"min-width",
"max-width",
"height",
"min-height",
"max-height",
"padding",
"padding-top",
"padding-right",
"padding-bottom",
"padding-left",
"margin",
"margin-top",
"margin-right",
"margin-bottom",
"margin-left",
"border",
"border-width",
"border-top-width",
"border-right-width",
"border-bottom-width",
"border-left-width",
"border-style",
"border-top-style",
"border-right-style",
"border-bottom-style",
"border-left-style",
"border-color",
"border-top-color",
"border-right-color",
"border-bottom-color",
"border-left-color",
"border-image",
"border-image-source",
"border-image-slice",
"border-image-width",
"border-image-outset",
"border-image-repeat",
"border-top",
"border-top-width",
"border-top-style",
"border-top-color",
"border-top",
"border-right-width",
"border-right-style",
"border-right-color",
"border-bottom",
"border-bottom-width",
"border-bottom-style",
"border-bottom-color",
"border-left",
"border-left-width",
"border-left-style",
"border-left-color",
"border-radius",
"border-top-right-radius",
"border-bottom-right-radius",
"border-bottom-left-radius",
"border-top-left-radius",
"outline",
"outline-width",
"outline-color",
"outline-style",
"outline-offset",
"overflow",
"overflow-x",
"overflow-y",
"resize",
"visibility",
"font",
"font-style",
"font-variant",
"font-weight",
"font-stretch",
"font-size",
"font-family",
"font-synthesis",
"font-size-adjust",
"font-kerning",
"line-height",
"text-align",
"text-align-last",
"vertical-align",
"text-overflow",
"text-justify",
"text-transform",
"text-indent",
"text-emphasis",
"text-emphasis-style",
"text-emphasis-color",
"text-emphasis-position",
"text-decoration",
"text-decoration-color",
"text-decoration-style",
"text-decoration-line",
"text-underline-position",
"text-shadow",
"white-space",
"overflow-wrap",
"word-wrap",
"word-break",
"line-break",
"hyphens",
"letter-spacing",
"word-spacing",
"quotes",
"tab-size",
"orphans",
"writing-mode",
"text-combine-upright",
"unicode-bidi",
"text-orientation",
"direction",
"text-rendering",
"font-feature-settings",
"font-language-override",
"image-rendering",
"image-orientation",
"image-resolution",
"shape-image-threshold",
"shape-outside",
"shape-margin",
"color",
"background",
"background-image",
"background-position",
"background-size",
"background-repeat",
"background-origin",
"background-clip",
"background-attachment",
"background-color",
"background-blend-mode",
"isolation",
"clip-path",
"mask",
"mask-image",
"mask-mode",
"mask-position",
"mask-size",
"mask-repeat",
"mask-origin",
"mask-clip",
"mask-composite",
"mask-type",
"filter",
"box-shadow",
"opacity",
"transform-style",
"transform",
"transform-box",
"transform-origin",
"perspective",
"perspective-origin",
"backface-visibility",
"transition",
"transition-property",
"transition-duration",
"transition-timing-function",
"transition-delay",
"animation",
"animation-name",
"animation-duration",
"animation-timing-function",
"animation-delay",
"animation-iteration-count",
"animation-direction",
"animation-fill-mode",
"animation-play-state",
"scroll-behavior",
"scroll-snap-type",
"scroll-snap-destination",
"scroll-snap-coordinate",
"cursor",
"touch-action",
"caret-color",
"ime-mode",
"object-fit",
"object-position",
"content",
"counter-reset",
"counter-increment",
"will-change",
"pointer-events",
"all",
"page-break-before",
"page-break-after",
"page-break-inside",
"widows"
],
"no-empty-source": null,
"property-no-vendor-prefix": [
true,
{
"ignoreProperties": [
"background-clip"
]
}
],
"number-leading-zero": "never",
"number-no-trailing-zeros": true,
"length-zero-no-unit": true,
"value-list-comma-space-after": "always",
"declaration-colon-space-after": "always",
"value-list-max-empty-lines": 0,
"shorthand-property-no-redundant-values": true,
"declaration-block-no-duplicate-properties": true,
"declaration-block-no-redundant-longhand-properties": true,
"declaration-block-semicolon-newline-after": "always",
"block-closing-brace-newline-after": "always",
"media-feature-colon-space-after": "always",
"media-feature-range-operator-space-after": "always",
"at-rule-name-space-after": "always",
"indentation": 2,
"no-eol-whitespace": true,
"string-no-newline": null
}
}
8.3.vscode编辑器下载插件stylelint
8.4.修改 .vscode/settings.json
//settings.json
{
"editor.codeActionsOnSave": {
"source.fixAll": true
}
}
二、规范git提交
1.安装依赖
npm i -D husky lint-staged @commitlint/cli @commitlint/config-conventional
2.根⽬录创建 .huskyrc
{
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
}
等同于在package.json的如下配置:
{
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
}
}
3.根目录创建 .lintstagedrc
{
"*.{js, vue, css}": [
"eslint --fix",
"git add"
] //git commit前校验并纠正eslint语法问题,然后再add
}
等同于在package.json的如下配置:
{
"lint-staged": {
"*.{js, vue, css}": [
"eslint --fix",
"git add"
]
}
}
4.根目录创建git commit提交说明内容格式限制配置文件commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat', // 新功能(feature)
'fix', // 修补bug
'docs', // 文档(documentation)
'style', // 格式(不影响代码运行的变动)
'refactor', // 重构(即不是新增功能,也不是修改bug的代码变动)
'test', // 增加测试
'revert', // 回滚
'config', // 构建过程或辅助工具的变动
'chore', // 其他改动
],
],
//rule由name和配置数组组成,
//如:'name:[0, 'always', 72]',
//数组中第一位为level,可选0,1,2,0为disable,1为warning,2为error,
//第二位为应用与否,可选always | never,
//第三位该rule的值。
'type-empty': [2, 'never'], //提交类型是否允许为空
'subject-empty': [2, 'never'], //提交说明内容是否允许为空
}
}
到这里,项目在提交时就会校验提交说明内容是否符合标准。
接下来再记录如何通过终端交互规范commit内容的配置
1.安装依赖:
npm i -D commitizen conventional-changelog cz-conventional-changelog
2.配置依赖路径,在 package.json 中添加配置
{
//...
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
}
尝试在命令行中输入
提交代码
git add -A
暂存代码
git-cz
三、配置svg图标:svg-icon
1.安装svg-sprite-loader:
npm i -D svg-sprite-loader
2.vue.config.js中,覆盖原有项目对svg的处理loader:
const path = require("path");
module.exports = {
chainWebpack: (config) => {
config.module
.rule("svg")
.exclude.add(resolve("src/components/SvgIcon/svgs"))
.end()
config.module
.rule("icon")
.test(/\.svg$/)
.include.add(resolve("src/components/SvgIcon/svgs")) //处理svg目录
.end()
.use("svg-sprite-loader")
.loader("svg-sprite-loader")
.options({
symbolId: "icon-[name]",
});
},
};
function resolve(dir) {
return path.join(__dirname, dir);
}
3.创建svg-icon组件
<script>
export default {
name: "SvgIcon",
props: {
iconClass: {
type: String,
required: true,
},
},
computed: {
iconName() {
return `#icon-${this.iconClass}`;
},
},
};
</script>
<template>
<svg class="SvgIcon" aria-hidden="true">
<use :xlink:href="iconName" />
</svg>
</template>
<style scoped lang="less">
.SvgIcon {
width: 1em;
height: 1em;
// vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
</style>
4.创建引入组件和svg图片的index.js:
import Vue from 'vue';
import svgIcon from './index.vue';
Vue.component(svgIcon.name, svgIcon);
const req = require.context('@/components/SvgIcon/svgs', true, /\.svg$/);
const requireAll = requireContext => requireContext.keys().map(name => requireContext(name));
requireAll(req);
以下是我本地项目svg-icon组件相关的文件夹结构
src
components
SvgIcon
svgs
testIcon.svg
index.js
index.vue
5.main.js中引入
import "/components/SvgIcon/index.js";
6.使用示例
<svg-icon :icon-class='svg文件名' />
四、配置全局less
1.通过vue安装style-resources-loader,安装时我选择less,安装后自动创建vue.config.js
vue add style-resources-loader
完善vue.config.js配置
const path = require("path");
module.exports = {
pluginOptions: {
"style-resources-loader": {
preProcessor: "less",
patterns: [
path.resolve(__dirname, "./src/styles/reset.less"), //需要引入的文件路径列表,可以设置模糊匹配
path.resolve(__dirname, "./src/styles/variable.less"), //可以设置全局变量
]
}
}
};
五、axios
1.安装依赖(已安装的进入下一步)
npm i axios
2.创建api配置文件/src/utils/http/.env.default.js
const apiBase = {
dev: 'http://xxxx:8080',
local: 'http://xxxx',
stg: 'http://xxxx',
production: 'http://xxxx'
}
export const baseUrl = apiBase[process.env.NODE_ENV || 'dev']
3.创建接口响应处理方法/src/utils/http/handle.js
export const successCode = '1000'
// 根据自己接口返回的实际情况,转换属性名,假设接口返回的状态属性是returnCode,消息属性是message
export const key = {
code: 'returnCode',
msg: 'message'
}
export const handles = {
'error:0001': (err) => {
console.log(err[key.msg] || '系统异常')
},
errorTips: (err) => {
console.log(err[key.msg] || '系统异常')
}
}
4.创建文件/src/utils/http/index.js
import axios from 'axios'
import { baseUrl } from './.env.default.js'
import {
key,
handles,
successCode
} from './handle.js'
// axios 配置
const defaultBaseUrl = 'http://localhost:8080/'
// 默认超时时间
axios.defaults.timeout = 15000
// 数据接口域名统一配置.env
axios.defaults.baseURL = baseUrl || defaultBaseUrl
axios.defaults.baseURL = ''
// http request 拦截器
axios.interceptors.request.use(
(config) => {
config.headers = {}
return config
},
(err) => {
return Promise.reject(err)
}
)
// http response 拦截器
axios.interceptors.response.use(
(response) => {
const data = response.data
if (data[key.code] !== successCode) {
const fn = handles[data[key.code]]
if (fn) {
fn(data)
} else {
handles.errorTips(data)
}
}
return response
},
(error) => {
const data = error.response.data
if (data && data[key.code] !== successCode) {
const fn = handles[data[key.code]]
if (fn) {
fn(data)
} else {
handles.errorTips(data)
}
}
return Promise.reject(data || error)
}
)
export default axios
/**
* get 请求方法
* @param {*} url
* @param {*} params
*/
export function get(url, params = {}) {
return new Promise((resolve, reject) => {
axios
.get(url, {
params: params
})
.then((response) => {
resolve(response.data)
})
.catch((err) => {
reject(err)
})
})
}
/**
* post 请求方法,发送数据格式 json
* @param {*} url
* @param {*} data
*/
export function post(
url,
data = {},
config = {
transformRequest: [
function(fData, headers) {
headers['Content-Type'] =
'application/json'
return JSON.stringify(fData)
}
]
}
) {
return new Promise((resolve, reject) => {
axios.post(url, data, config).then(
(response) => {
resolve(response.data)
},
(err) => {
reject(err)
}
)
})
}
/**
* patch 请求方法,发送数据格式 json
* @param {*} url
* @param {*} data
*/
export function patch(url, data = {}) {
return new Promise((resolve, reject) => {
axios
.patch(url, data, {
transformRequest: [
function(fData, headers) {
headers['Content-Type'] =
'application/json'
return JSON.stringify(fData)
}
]
})
.then(
(response) => {
resolve(response.data)
},
(err) => {
reject(err)
}
)
})
}
export function del(url, data) {
return new Promise((resolve, reject) => {
axios.delete(url, { data }).then(
(response) => {
resolve(response.data)
},
(err) => {
reject(err)
}
)
})
}
5.创建某个业务模块对应的API集合文件/src/utils/http/services/public.js
import { post, get } from '../index.js'
export const list = (data = {}) =>
post(
'/user/info',
data
)
export const userinfo = (data = {}) => get('/user/list', data)
6.vue文件中的调用
import { userinfo } from '@/utils/http/services/public.js'
userinfo()
.then((data) => {
console.log(data)
})
.catch(() => {})
六、mock数据
1.安装依赖
npm i -D webpack-dev-server mockjs
2.改造vue.config.js
const webpackConfig = {
devServer: {}
}
//本地mock
if (process.env.NODE_ENV === 'local') {
webpackConfig.devServer.before = require('./src/mock') //引入mock/index.js
}
module.exports = webpackConfig
3.package.json增加脚本
{
script:{
"serve:mock": "vue-cli-service serve --mode=local --open "
}
}
4.创建mock入口文件/src/mock/index.js
const Mock = require('mockjs') //mockjs 导入依赖模块
const util = require('./util') //自定义工具模块
//返回一个函数
module.exports = function(app) {
//监听http请求
app.get('/user/userinfo', function(rep, res) {
//每次响应请求时读取mock data的json文件
//util.getJsonFile方法定义了如何读取json文件并解析成数据对象
var json = util.getJsonFile(
'./jsons/userinfo.json'
)
//将json传入 Mock.mock 方法中,生成的数据返回给浏览器
res.json(Mock.mock(json))
})
}
5.创建mock方法 /src/mock/util.js
const fs = require('fs') //引入文件系统模块
const path = require('path') //引入path模块
module.exports = {
//读取json文件
getJsonFile: function(filePath) {
//读取指定json文件
var json = fs.readFileSync(
path.resolve(__dirname, filePath),
'utf-8'
)
//解析并返回
return JSON.parse(json)
}
}
6.创建模拟接口返回数据json /src/mock/jsons/userinfo.json
{
"error": 0,
"data": {
"userid": "@id()",
"username": "@cname()",
"date": "@date()",
"avatar":
"@image('200x200','red','#fff','avatar')",
"description": "@paragraph()",
"ip": "@ip()",
"email": "@email()"
}
}
7.运行npm run serve:mock,发现可以获取到mock数据
七、vuex
1.安装依赖(已安装的进入下一步)
npm i vuex
2.创建vuex入口文件 /src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import root from './modules/root.js'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
testState: 1
},
mutations: {
changeState: (state, value) => {
state.testState = value
}
},
actions: {},
modules: {
root
}
})
3.分模块配置vuex,有利于管理维护,创建模块/src/store/modules/root.js
export default {
state: {
rootState: 1
},
mutations: {},
actions: {},
getters: {}
}
4.检查main.js是否引入/src/utils/tools/index.js
5.如果需要缓存数据,可以结合sessionStorage或者localStorage对数据进行本地缓存,可以做一下操作:
- 创建缓存方法
// /src/utils/tools/index.js
export const setSesStorage = (key, value) => {
try {
value = JSON.stringify(value)
// eslint-disable-next-line no-empty
} catch (error) {}
sessionStorage.setItem(
`${process.env.VUE_APP_BASE_NAME}${key}`, //全局配置,可以用于sessionStorage的命名空间,区分同域名不同项目的缓存
value
)
}
export const getSesStorage = (key) => {
let value = sessionStorage.getItem(
`${process.env.VUE_APP_BASE_NAME}${key}`
)
try {
return JSON.parse(value)
} catch (error) {
return value
}
}
- 改造mutations,如在index.js中改造:
// /src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import root from './modules/root.js'
import {
setSesStorage,
getSesStorage
} from '../utils/tools/index.js'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
testState: getSesStorage('testState') || ''
},
mutations: {
changeState: (state, value) => {
state.testState = value
setSesStorage('testState', value) // localStorage同理
}
},
actions: {},
modules: {
root
}
})
八、全局filter
1.创建文件 /src/filters/index.js
import Vue from 'vue'
Vue.filter('filterSome', (value) => {
return value || ''
})
2.main.js导入 /src/filters/index.js
九、路由router
个人建议,路由逻辑和路由配置分开编写
1.安装依赖(已安装的进入下一步)
npm i vue-router
2.创建路由逻辑文件 /src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import routes from './routes.js'
Vue.use(VueRouter)
// 解决跳转相同路由报错的问题
const originalPath = VueRouter.prototype.push
VueRouter.prototype.push = function push(
location
) {
return originalPath
.call(this, location)
.call((err) => err)
}
const router = new VueRouter({
routes
})
export default router
3.创建路由配置文件 /src/router/routes.js
export const routes = [
{
path: '/',
name: 'Home',
component: () =>
import(
// webpackChunkName后面的值表示按不同模块打包,按需加载,访问到对应的路由才开始加载对应的js
/* webpackChunkName: "home" */ '../views/Home.vue'
)
},
{
path: '/about',
name: 'About',
component: () =>
import(
// webpackChunkName后面的值表示按不同模块打包,按需加载,访问到对应的路由才开始加载对应的js
/* webpackChunkName: "about" */ '../views/About.vue'
)
}
]
十、全局批量注册组件
1.创建公共组件示例,如/src/components/global/AppTest.vue
<!-- 说明内容 -->
<!-- @author XXX -->
<script>
export default {
name: 'AppTest'
}
</script>
<template>
<section class="AppTest">
App Test Component
</section>
</template>
<style scoped lang="less">
.AppTest {
}
</style>
2.创建批量注册的入口文件/src/components/index.js
import Vue from 'vue'
const requireComponent = require.context(
// 其组件目录的相对路径
'./global/',
// 是否查询其子目录
false,
// 匹配基础组件文件名的正则表达式
/\.(vue|js)$/
)
requireComponent.keys().forEach((fileName) => {
// 获取组件配置
const componentConfig = requireComponent(
fileName
)
// 如果这个组件选项是通过 `export default` 导出的,
// 那么就会优先使用 `.default`,
// 否则回退到使用模块的根。
const ctrl =
componentConfig.default || componentConfig
// 全局注册组件
Vue.component(ctrl.name, ctrl)
})
3.main.js引入入口文件
import '@/components/index.js'
4.这样就可以在全局直接使用公共组件了