vue前端开发规范v1.0.0

开发规范

Posted by kung on April 23, 2021

一、规范目的:

为提高团队协作效率,便于后台人员添加功能及前端后期优化维护,输出高质量的文档,特制订此文档。本规范文档一经确认,前端开发组人员必须按本文档规范进行前台页面开发。本文档如有不对或者不合适的地方请及时提出,经讨论决定后方可更改。使开发流程更加规范化。

二、基本准则:

符合 vue 标准,结构表现行为分离,兼容性优良。页面性能方面, 代码要求简洁明了有序,尽可能的减小服务器负载,保证最快的解析速度。

三、规范:

本规范主要包括以下部分 :

  • vue 框架构建及开发规范
  • 打包优化

1、vue 框架构建及开发规范

1.1、vue 项目框架搭建

1.1.1、脚手架构建

// 1.下载项目脚手架(默认安装node开发环境)
git clone nuxt-iview-template.git // PC模版
git clone /nuxt-vant-template.git // H5模版

// 2.安装项目依赖 // 2.1.安装依赖包:

npm install

// 2.2.本地开发:

npm run dev

// 2.3.打包静态html文件:

npm run dist

1.1.2、项目目录结构

├── app.html // 模版文件
├── assets // 静态资源,会被webpack打包
├── backpack.config.js // 打包配置,一般不会动
├── build // nuxt打包后自动生成
├── components // 封装的公共业务组件
├── config // 公共配置文件
│   ├── axios.js //axios请求库配置文件
│   └── index.js
├── layouts // nuxt布局文件,全局
│   ├── default.vue
│   ├── error.vue // 错误处理
│   └── nav.vue
├── lib //公共类
│   └── utils.js
├── middleware // nuxt中间件
│   └── auth.js
├── my-theme // 主题文件
│   └── index.less
├── nuxt.config.js // nuxt主配置文件,修改代理等配置在此,具体配置参考nuxt.js官网
├── pages // 业务页面
├── plugins // 插件
│   ├── apis.js // 全局请求接口封装
│   ├── http.js // 请求库封装this.$http.xxx
│   ├── qs.js // 请求参数处理this.$qs.xxx
│   ├── quill.js // 富文本
│   ├── ui.js // UI库
│   └── utils.js // 工具类this.$Utils.xxx
├── server
│   ├── api
│   ├── apis.js // 后端请求接口,调用this.$APIS.xxx
│   └── index.js
├── static // 静态资源,不会被打包
│   ├── css
│   │   ├── them.css // 主题文件
│   ├── fonts
│   ├── img
│   └── js
└── store // nuxt已经封装vuex状态管理
    ├── index.js
    └── sale.js

1.1.3、技术栈

vue、nuxt(基于vue框架)、axios(http请求库)、querystring

1.1.4、UI 框架选择

  • PC 端:推荐使用 iView
  • 移动端:推荐使用 vant

1.1.5、css 预处理器

推荐使用 less,scss , 可在 common.css 设置全局样式,如

  • 常用样式设置原子类名
  • 主题颜色, UI 设计规范等样式
  • 全局组件公共样式

1.1.6、移动端适配

  • 以蓝湖 750px 设计稿。
  • 使用 rem 适配,代码书写 px,用 postcss-px2rem 将 px 转化为 rem。
// nuxt.config.js
postcss: [
      require('autoprefixer')({
        browsers: ['last 3 versions']
      }),
      require('postcss-px2rem')({
        remUnit: 50
      })
],
// rem.js

/* eslint-disable */
!function () {
    var a = "@charset \"utf-8\";html{color:#000;background:#fff;overflow-y:scroll;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}html *{outline:0;-webkit-text-size-adjust:none;-webkit-tap-highlight-color:rgba(0,0,0,0)}html,body{font-family:sans-serif}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td,hr,button,article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{margin:0;padding:0}input,select,textarea{font-size:100%}table{border-collapse:collapse;border-spacing:0}fieldset,img{border:0}abbr,acronym{border:0;font-variant:normal}del{text-decoration:line-through}address,caption,cite,code,dfn,em,th,var{font-style:normal;font-weight:500}ol,ul{list-style:none}caption,th{text-align:left}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:500}q:before,q:after{content:''}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}a:hover{text-decoration:underline}ins,a{text-decoration:none}",
        b = document.createElement("style");
    if (document.getElementsByTagName("head")[0].appendChild(b), b.styleSheet) b.styleSheet.disabled || (b.styleSheet.cssText = a); else try {
        b.innerHTML = a
    } catch (c) {
        b.innerText = a
    }
}();
!function (a, b) {
    function c() {
        var b = f.getBoundingClientRect().width;
        b / i > 540 && (b = 540 * i);
        var c = b / 7.5;
        f.style.fontSize = c + "px", k.rem = a.rem = c
    }

    var d, e = a.document, f = e.documentElement, g = e.querySelector('meta[name="viewport"]'),
        h = e.querySelector('meta[name="flexible"]'), i = 0, j = 0, k = b.flexible || (b.flexible = {});
    if (g) {
        console.warn("将根据已有的meta标签来设置缩放比例");
        var l = g.getAttribute("content").match(/initial\-scale=([\d\.]+)/);
        l && (j = parseFloat(l[1]), i = parseInt(1 / j))
    } else if (h) {
        var m = h.getAttribute("content");
        if (m) {
            var n = m.match(/initial\-dpr=([\d\.]+)/), o = m.match(/maximum\-dpr=([\d\.]+)/);
            n && (i = parseFloat(n[1]), j = parseFloat((1 / i).toFixed(2))), o && (i = parseFloat(o[1]), j = parseFloat((1 / i).toFixed(2)))
        }
    }
    if (!i && !j) {
        var p = (a.navigator.appVersion.match(/android/gi), a.navigator.appVersion.match(/iphone/gi)),
            q = a.devicePixelRatio;
        i = p ? q >= 3 && (!i || i >= 3) ? 3 : q >= 2 && (!i || i >= 2) ? 2 : 1 : 1, j = 1 / i
    }
    if (f.setAttribute("data-dpr", i), !g)if (g = e.createElement("meta"), g.setAttribute("name", "viewport"), g.setAttribute("content", "initial-scale=" + j + ", maximum-scale=" + j + ", minimum-scale=" + j + ", user-scalable=no"), f.firstElementChild) f.firstElementChild.appendChild(g); else {
        var r = e.createElement("div");
        r.appendChild(g), e.write(r.innerHTML)
    }
    a.addEventListener("resize", function () {
        clearTimeout(d), d = setTimeout(c, 300)
    }, !1), a.addEventListener("pageshow", function (a) {
        a.persisted && (clearTimeout(d), d = setTimeout(c, 300))
    }, !1), "complete" === e.readyState ? e.body.style.fontSize = 12 * i + "px" : e.addEventListener("DOMContentLoaded", function () {
        e.body.style.fontSize = 12 * i + "px"
    }, !1), c(), k.dpr = a.dpr = i, k.refreshRem = c, k.rem2px = function (a) {
        var b = parseFloat(a) * this.rem;
        return "string" == typeof a && a.match(/rem$/) && (b += "px"), b
    }, k.px2rem = function (a) {
        var b = parseFloat(a) / this.rem;
        return "string" == typeof a && a.match(/px$/) && (b += "rem"), b
    }
}(window, window.lib || (window.lib = {}));

1.1.7、字体图标

推荐使用 iconfont

1.2、路由

Nuxt.js 依据 pages 目录结构自动生成 vue-router 模块的路由配置。 具体参考 Nuxt.js 官方文档。

1.3、http 数据请求

推荐使用 axios 作为 http 请求库

1.3.1、设置全局请求拦截和响应封装

// config/http.js
import axios from 'axios'
import Vue from 'vue'
import querystring from "querystring";

if (process.server) {
    axios.defaults.baseURL = `http://${process.env.HOST || 'localhost'}:${process.env.PORT || 3000}/api`
}
axios.defaults.headers['Content-Type'] = 'application/json'
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
axios.defaults.withCredentials = true
axios.defaults.timeout = 100000

// 请求之前
axios.interceptors.request.use(req => {
    // console.log('request: ', JSON.stringify(req))
    return req
})

// 登陆失效跳转登录页面(没有实现)
axios.interceptors.response.use(
    response => {
        if (response.status === 200) {
            var status = response.data.status
            return Promise.reject(response.data)
        } else {
            return Promise.reject(response)
        }
    },
    error => {
        if (error.response.status) {
            switch (error.response.status) {
                case 401:
                   //
                    break
            }
        }
        return Promise.reject(error)
    }
)

export default axios

1.3.2、把 axios 方法挂载到 vue 实例上

// plugins/http.js
import Vue from 'vue'
import axios from '../config/http'

Vue.prototype.$http = axios //注册为vue内部方法 使用时this.$http.get/post

1.4、工具类

1.4.1、工具方法

具体查看代码lib/utils.js

1.4.2、将工具类挂载到 Vue 实例上

// plugins/utils.js
import * as Utils from '../lib/utils'
import Vue from 'vue'

Vue.prototype.$Utils = Utils; //注册为vue内部方法 使用时this.$Utils

1.5、接口地址

1.5.1、全局配置

// server/apis.js
const PREFIX = ""
const OSS = ""
const ERP = ""

const APIS = {
    FILE_BASE_URL: '',//文件默认url
}

export default APIS;

1.5.2、挂载到 vue 实例

// plugins/apis.js

import Vue from 'vue';
import apis from '../server/apis';
Vue.prototype.$APIS = apis; //注册为vue内部方法 使用时this.$APIS

1.6、命名规范

——让团队当中其他人看你的代码能一目了然

  • 1.文件夹和文件命名以业务或者模块名字为主,驼峰式命名。
  • 2.组件命名遵循以下原则,使用短横线(car-lib)进行组件声明,使用短横线分隔命名(`)进行使用。
  • 3.当项目中需要自定义比较多的基础组件的时候,比如一些 button,input,icon,建议以一个统一的前缀如 Erp 开头,这样做的目的是为了方便查找。
  • 4.method 方法命名使用驼峰式,动词+名词,如 getData, submitForm
  • 5.变量命遵循语义化原则,使用驼峰式。

1.7、命名规范

1.7.1、 vue 风格推荐

  • Props 定义应该尽量详细。
// bad
props: ['status']

// good
props: {
  status: String
}

// better
props: {
  status: {
    type: String,
    required: true,
    validator: function (value) {
      return ['syncing','synced','version-conflict','error'].indexOf(value) !== -1
    }
  }
}
  • 使用 v-for 必须加上 key 值
<!-- bad -->
<ul>
  <li v-for="todo in todos"></li>
</ul>

<!-- good -->
<ul>
  <li v-for="todo in todos" :key="todo.id"></li>
</ul>
  • 组件的 data 必须是一个函数
// bad
Vue.component('some-comp', {
  data: {
    foo: 'bar'
  }
})

// good
Vue.component('some-comp', {
  data: function() {
    return {
      foo: 'bar'
    }
  }
})
  • 组件模板应该只包含简单的表达式,复杂的表达式则应该重构为计算属性或方法。
// bad

  • 指令缩写
<!-- bad -->
<input v-bind:value="newTodoText" :placeholder="newTodoInstructions" v-on:input="onInput" />
<!-- good -->
<input :value="newTodoText" :placeholder="newTodoInstructions" @input="onInput" />

1.7.2 、关于组件内样式

  • 为组件样式设置作用域
/* bad  */
<style>
.btn-close {
  background-color: red;
}
</style>

/* good  */
<style scoped>
.button-close {
  background-color: red;
}
</style>

  • 若要改变第三方组件库的样式,需要加上顶级作用域。
/* bad */
.ivu-input {
  width: 254px !important;
}

/* good */
.customerForm .ivu-input {
  width: 254px !important;
}
/* .customerForm为当前组件的顶级dom  */

1.7.3、关于组件结构

  • 组件结构遵循从上往下 template,script,style 的结构。
<template>
  <div></div>
</template>

<script>
  export default {}
</script>


<style lang="scss" scoped></style>

script 部分各方法成员遵循以下顺序放置。

  • name
  • components
  • props
  • data
  • computed
  • watch
  • created
  • mounted
  • update
  • methods

1.7.4、关于注释规范

以下情况需要加注释,以方便代码维护和他人理解

  • 公共组件使用说明
  • 各组件中重要函数或者类说明
  • 复杂的业务逻辑处理说明
  • 多重 if 判断语句

1.7.5、其他规范

  • 建议不再使用双引号,静态字符串使用单引号,动态字符串使用反引号衔接。
// bad
const foo = 'jack'
const bar = foo + ',前端工程师'

// good
const foo = 'jack'
const bar = `${foo},前端工程师`

- 使用数组展开操作符 ... 复制数组。

// bad
const len = items.length
const itemsCopy = []
let i

for (i = 0; i < len; i += 1) {
  itemsCopy[i] = items[i]
}

// good
const itemsCopy = [...items]

  • 使用数组对象解构赋值
const arr = [1, 2, 3, 4]

// bad
const first = arr[0]
const second = arr[1]

// good
const [first, second] = arr

2、打包优化

为什么要优化打包?

  • 项目越做越大,依赖包越来越多,打包文件太大
  • 单页面应用首页白屏时间长,用户体验差

我们的目的

  • 减小打包后的文件大小
  • 首页按需引入文件
  • 优化 webpack 打包时间

下面是提供的思路:

2.1 、升级 nuxt

npm install nuxt@latest
npm run dev

2.2、升级配置

// 升级配置 package.json
"dependencies": {
 "ali-oss": "^6.0.1",
 "axios": "^0.16.2",
 "cross-env": "^5.0.1",
 "echarts": "^4.0.4",
 "express": "^4.15.3",
 "iview": "^3.4.0",
 "js-cookie": "^2.2.0",
 "less": "^2.7.2",
 "less-loader": "^4.0.5",
 "lodash-core": "^0.8.1",
 "moment": "^2.22.1",
 "nuxt": "^2.3.4",
 "postcss": "^7.0.6",
 "querystring": "^0.2.0",
 "source-map-support": "^0.5.9",
 "v-charts": "^1.16.8",
 "vue-style-loader": "^4.1.2"
},
"devDependencies": {
 "@nuxtjs/axios": "^5.3.6",
 "@nuxtjs/proxy": "^1.3.1",
 "autoprefixer-loader": "^3.2.0",
 "babel-eslint": "^10.0.1",
 "babel-polyfill": "^6.26.0",
 "backpack-core": "^0.8.3",
 "css-loader": "^1.0.1",
 "eslint": "^5.9.0",
 "eslint-config-standard": "^12.0.0",
 "eslint-loader": "^2.1.1",
 "eslint-plugin-html": "^5.0.0",
 "eslint-plugin-import": "^2.14.0",
 "eslint-plugin-node": "^8.0.0",
 "eslint-plugin-promise": "^4.0.1",
 "eslint-plugin-standard": "^4.0.0"
}

2.3、升级后可能碰到的问题及修复

  • 升级后每次修复都会弹 Are you interested in participation?
//nuxt.config.js https://blog.csdn.net/qq_39473019/article/details/107424181
telemetry: false,
  • 运行后有部分地方使用 loadash,发现会报错,解决方案,在 plugins 文件夹下创建 lodash.js,并在 nuxt.config.js 里配置
// https://blog.csdn.net/dexing07/article/details/78907639
// plugins/lodash.js
let _ = require('lodash')
if(process.browser){
    window._ = _
}
// nuxt.config.js
plugins: [
  {src: '~plugins/lodash', ssr: false},
],

2.4、打包优化方向

2.4.1、修改 nuxt 配置文件

  • 修改nuxt.config.js配置analyze: true,运行 npm run generate 分析包的大小
  • 处理 vendor.app.js 文件,修改 nuxt.config.js 配置文件
// nuxt.config.js
optimization:{
    splitChunks:{
        minSize:10000,
        maxSize:250000
    }
},

2.4.2、UI 框架优化

  • vant 按需加载,修改 plugins/ui.js,删除全局引用 vant,改为按需引用(H5 端)
  • 修改 vant 引用 css 的方式(H5 端)

2.4.3、其他优化方向

CDN、服务器开启 Gzip