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组件






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.使用示例


四、配置全局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








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.这样就可以在全局直接使用公共组件了

你可能感兴趣的:(vue项目工程化,大大提升开发效率和开发体验)