-
结构规范
对项目进行分层,比如网络层,组件层,路由层,工具层,存储层,他们表现为一个个具体的文件夹,其中文件夹的名字对应什么层是一目了然的。
比如网络层,将具体的请求方法封装到http,而数据接口封装到service,请求到的数据保存到VueX或者Rudex中。
网络层
-
代码规范
Eslint:插件方式的代码规范管理工具。值得注意的是eslint不仅可以规范普通的es规范,还能做到Vue组件的规范,比如生命周期顺序和指令等
// .eslintrc.js
module.exports = {
root: true,
env: {
browser: true,
node: true,
es6: true
},
extends: [
// 这个需要安装依赖 npm i @vue/eslint-config-standard eslint-plugin-vue -D
'plugin:vue/essential',
'@vue/standard'
],
plugins: [
'vue'
],
rules: {
// 'vue/no-parsing-error':'off',
// no-console error 代表存在console就会报错, off 代表时关闭,on代表时开启
'no-console': 0,
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'accessor-pairs': 2,
'arrow-spacing': [2, {
before: true,
after: true
}],
'block-spacing': [2, 'always'],
'brace-style': [2, '1tbs', {
allowSingleLine: true
}],
camelcase: [0, {
properties: 'always'
}],
'comma-dangle': [2, 'never'],
'comma-spacing': [2, {
before: false,
after: true
}],
'comma-style': [2, 'last'],
'constructor-super': 2,
curly: [2, 'multi-line'],
'dot-location': [2, 'property'],
'eol-last': 2,
eqeqeq: [2, 'allow-null'],
'generator-star-spacing': [2, {
before: true,
after: true
}],
'handle-callback-err': [2, '^(err|error)$'],
indent: [2, 2, {
SwitchCase: 1
}],
'jsx-quotes': [2, 'prefer-single'],
'key-spacing': [2, {
beforeColon: false,
afterColon: true
}],
'keyword-spacing': [2, {
before: true,
after: true
}],
'new-cap': [2, {
newIsCap: true,
capIsNew: false
}],
'new-parens': 2,
'no-array-constructor': 2,
'no-caller': 2,
// 'no-console': 'off',
'no-class-assign': 2,
'no-cond-assign': 2,
'no-const-assign': 2,
'no-control-regex': 0,
'no-delete-var': 2,
'no-dupe-args': 2,
'no-dupe-class-members': 2,
'no-dupe-keys': 2,
'no-duplicate-case': 2,
'no-empty-character-class': 2,
'no-empty-pattern': 2,
'no-eval': 2,
'no-ex-assign': 2,
'no-extend-native': 2,
'no-extra-bind': 2,
'no-extra-boolean-cast': 2,
'no-extra-parens': [2, 'functions'],
'no-fallthrough': 2,
'no-floating-decimal': 2,
'no-func-assign': 2,
'no-implied-eval': 2,
'no-inner-declarations': [2, 'functions'],
'no-invalid-regexp': 2,
'no-irregular-whitespace': 2,
'no-iterator': 2,
'no-label-var': 2,
'no-labels': [2, {
allowLoop: false,
allowSwitch: false
}],
'no-lone-blocks': 2,
'no-mixed-spaces-and-tabs': 2,
'no-multi-spaces': 2,
'no-multi-str': 2,
'no-multiple-empty-lines': [2, {
max: 1
}],
'no-native-reassign': 2,
'no-negated-in-lhs': 2,
'no-new-object': 2,
'no-new-require': 2,
'no-new-symbol': 2,
'no-new-wrappers': 2,
'no-obj-calls': 2,
'no-octal': 2,
'no-octal-escape': 2,
'no-path-concat': 2,
'no-proto': 2,
'no-redeclare': 2,
'no-regex-spaces': 2,
'no-return-assign': [2, 'except-parens'],
'no-self-assign': 2,
'no-self-compare': 2,
'no-sequences': 2,
'no-shadow-restricted-names': 2,
'no-spaced-func': 2,
'no-sparse-arrays': 2,
'no-this-before-super': 2,
'no-throw-literal': 2,
'no-trailing-spaces': 2,
'no-undef': 2,
'no-undef-init': 2,
'no-unexpected-multiline': 2,
'no-unmodified-loop-condition': 2,
'no-unneeded-ternary': [2, {
defaultAssignment: false
}],
'no-unreachable': 2,
'no-unsafe-finally': 2,
'no-unused-vars': [2, {
vars: 'all',
args: 'none'
}],
'no-useless-call': 2,
'no-useless-computed-key': 2,
'no-useless-constructor': 2,
'no-useless-escape': 0,
'no-whitespace-before-property': 2,
'no-with': 2,
'one-var': [2, {
initialized: 'never'
}],
'operator-linebreak': [2, 'after', {
overrides: {
'?': 'before',
':': 'before'
}
}],
'padded-blocks': [2, 'never'],
quotes: [2, 'single', {
avoidEscape: true,
allowTemplateLiterals: true
}],
semi: [2, 'never'],
'semi-spacing': [2, {
before: false,
after: true
}],
'space-before-blocks': [2, 'always'],
'space-before-function-paren': [2, 'never'],
'space-in-parens': [2, 'never'],
'space-infix-ops': 2,
'space-unary-ops': [2, {
words: true,
nonwords: false
}],
'spaced-comment': [2, 'always', {
markers: ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
}],
'template-curly-spacing': [2, 'never'],
'use-isnan': 2,
'valid-typeof': 2,
'wrap-iife': [2, 'any'],
'yield-star-spacing': [2, 'both'],
yoda: [2, 'never'],
'prefer-const': 2,
// 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'object-curly-spacing': [2, 'always', {
objectsInObjects: false
}],
'array-bracket-spacing': [2, 'never'],
// 追加vue的编码规范
"vue/max-attributes-per-line": ["error", { // 标签的属性必须在单独一行
"singleline": 1,
"multiline": {
"max": 1,
"allowFirstLine": false
}
}],
"vue/order-in-components": ["error", { // 规定组件的属性顺序
"order": [
"el",
"name",
"parent",
"functional",
["delimiters", "comments"],
["components", "directives", "filters"],
"extends",
"mixins",
"inheritAttrs",
"model",
["props", "propsData"],
"data",
"computed",
"watch",
"LIFECYCLE_HOOKS",
"methods",
["template", "render"],
"renderError"
]
}],
"vue/v-bind-style": ["error", "shorthand"], // v-bind: 和 : 统一使用:
"vue/v-on-style": ["error", "shorthand"], // v-on 和 @ 统一使用@
"vue/name-property-casing": ["error", "PascalCase"], // 组件明大写
"vue/component-name-in-template-casing": ["error", "kebab-case", {// 模板的组件使用小写加-
"registeredComponentsOnly": false,
"ignores": []
}],
"vue/this-in-template": ["error", "never"], // 禁止templent上使用this
"vue/attributes-order": ["error", { // 指定标签上属性的顺序
"order": [
"DEFINITION",
"LIST_RENDERING",
"CONDITIONALS",
"RENDER_MODIFIERS",
"GLOBAL",
"UNIQUE",
"TWO_WAY_BINDING",
"OTHER_DIRECTIVES",
"OTHER_ATTR",
"EVENTS",
"CONTENT"
]
}],
"vue/require-default-prop": ["error"], // 定义的属性必须要给默认属性
"vue/prop-name-casing": ["error", "camelCase"], // 属性的名称的命名方式
},
parserOptions: {
parser: 'babel-eslint'
},
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)'
],
env: {
jest: true
}
}
]
}
-
项目文档
对于一般的项目,一般有个README就够了,比较大型的项目会用到自己封装的框架,而这个框架最好内嵌一个文档。 -
组件规范
组件规范一部分可以在eslint上实现,还有一部分是需要结合官方提供的API,比如有时候在复用同一个子组件时,会想到 $attrs 和 $listener,但这样会造成信息的遗漏,不能一眼看出调用了子组件哪些方法,这个时候直接把事件和属性写上去会更直观。 -
工程自动化
项目中总会有些许机械化的工作,比如modules的导入导出,路由的导入导出,这些机械性的工作往往可以通过工程自动化来实现,比如VueX中讲modules统一地进行导入。
// load-modules.js
/**
* 自动加载store的二级 modules
*/
const modules = {}
// 需要将module名命名为index.js才能匹配到
const allModules = require.context('@/store/modules/', true, /index\.(js|ts)$/)
// ["./login/index.js", "./main/books/index.js", "./main/goods/index.js", "./register/index.js"]
console.log('allModules=', allModules.keys())
allModules.keys().forEach((item, index, array) => {
// item = ./login/index.js => login/index.js
// item = ./main/books/index.js => main/books/index.js
const module_path = item.substr(2)
const moduleNames = module_path.split('/') // [login, index.js] - [main, books, index.js]
moduleNames.pop()
const key = moduleNames.join('_')
// const module = require(`@/store/modules/${module_path}`)
const module = allModules(item)
modules[key] = module.default
})
/**
* modules:{
goods :{
// 0.启用命名空间
namespaced: true,
// 1.定义状态
state: {
data: {}, // 列表数据
recordDetail: {} // 详情数据
},
// 2.修改状态
mutations: {
// 这里的 `state` 对象是模块的局部状态
[Types.addData](state, payload) {
state.data = payload
},
[Types.recordDetail](state, payload) {
state.recordDetail = payload
}
},
// 3.提交action,来修改状态
actions: {
async list(context, payload) {
// context 对象 与 store对象有相同的方法;context != store
// 注意:局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState
const config = {
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
}
try {
const result = await interrogaterecordService.list(payload, config)
console.log('result=', result)
context.commit(Types.addData, result.data.data)
return Promise.resolve(result.data.data)
} catch (err) {
}
},
async recordDetail(context, payload) {
// context 对象 与 store对象有相同的方法;context != store
// 注意:局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState
const config = {
}
try {
const result = await interrogaterecordService.recordDetail(payload, config)
Console.log('recordDetail=', result)
context.commit(Types.recordDetail, result.data.data)
return Promise.resolve(result.data)
} catch (err) {
}
}
},
// 4.获取定义的状态, 通过store.getters获取里面的函数,例如:store.getters.count
getters: {
// state 是获取局部状态;rootState是获取根状态
...ComGetters, // list --> data.content ; listPageConfig -> pageConfig
recordDetail(state, getters, rootState, rootGetters) {
return state.recordDetail || {}
}
}
}
* }
*/
export default modules || {}
路由模块的工程化导出,这需要项目规范化的配合,即每个页面文件夹下都对应存放一个路由文件,最后通过load-routes.js统一进行导出,形成一个完整routes对象。
![](https://img.haomeiwen.com/i14626058/d43f6913b4da98f1.png)
// import Login from './login.vue'
const Login = () => import(/* webpackChunkName: "login" */ './login.vue')
export default {
path: '/login',
name: 'login',
pname: '', // 父亲路由的名称
level: 1, // 一级路由
component: Login,
children: [
// {
// path: 'goodslist', // /main/goodslist
// name: 'goodslist',
// component: GoodsList,
// children: [
// {
// path: 'create', // /main/goodslist/create
// name: 'create',
// component: GoodsListCreate
// }
// ]
// }
],
meta: {
keepAlive: false, // 不需要缓存
requireAuth: false // 不需要登录才能访问
}
}
// load-routes.js
const routes = []
// allRoute 是一个函数
const allRoute = require.context('@/views/', true, /route\.(js|ts)$/)
// ["./login/route.js", "./main/goods/list/route.js", "./main/route.js", "./no-find/route.js", "./register/route.js"]
// console.log('allRoute=', allRoute.keys())
allRoute.keys().forEach((file_path, index, array) => {
const route = allRoute(file_path)
routes.push(route.default) // router.default 拿到的才是导出的对象,router是模块对象
})
/**
* routes:[
{
path: '/main',
name: 'main',
level: 1,
pname:'',
component: Main,
children: [
]
},
{
path: '/login',
name: 'login',
level: 1,
pname:'',
component: Login,
children:[
]
},
{
children: []
component: {name: "List", components: {…}, staticRenderFns: Array(0), _compiled: true, render: ƒ, …}
level: 2
pname:'main',
name: "goodslist"
path: "goodslist"
pname: "main"
}
* ]
*/
export default routes || []
通过register-routes.js进行路由整合
// register-routes.js
import AllRoute from '@/views/load-routes.js'
// 获取一级路由
const aRoutes = AllRoute.filter((route) => {
return route.level === 1
})
// 获取一级路由
const bRoutes = AllRoute.filter((route) => {
return route.level === 2
})
// 获取一级路由
const cRoutes = AllRoute.filter((route) => {
return route.level === 3
})
aRoutes.forEach(element1 => {
// 遍历2级路由
bRoutes.forEach((element2) => {
if (element2.pname === element1.name) {
// 遍历3级路由
cRoutes.forEach((element3) => {
if (element3.pname === element2.name) {
element2.children.push(element3)
}
})
element1.children.push(element2)
}
})
})
// console.log(aRoutes)
export default aRoutes
-
服务器/持续集成规范
这一块运用的技术比较多,比如Docker可以生成一个统一的nginx镜像,里面include各个项目的.conf文件
再者可以用Jenkins或者Gulp搭载一个前端自动化发布的工具
// gulpfile.js
// 在Node中不能使用ES6语法的模块化
const gulpConfig = require('./gulpfile.config.js')
const gulp = require('gulp')
const GulpSSH = require('gulp-ssh')
// 需要上传到服务器的路径
const config = gulpConfig.devServerSShConfig.sshConfig
// const config = systemConfig.proServerSShConfig.sshConfig;
const gulpSSH = new GulpSSH({
ignoreErrors: false,
sshConfig: config.ssh
})
/**
* 1.备份:tar -zcvf 备份后文件的存储路径和名称 这里*代表压缩该命令所在文件夹下的所有内容
* 备份时最好要进入到指定的文件夹在开始压缩,这样压缩后的文件解压不会带路劲目录
*
* 备份的文件夹必须已经是存在,才能进行备份
*/
gulp.task('execSSHBackup', () => {
console.log('备份服务器上现有文件...')
return gulpSSH.shell(config.backups, { filePath: 'log/commands-backup.log' })
.pipe(gulp.dest('logs'))
})
/**
* 2.解压: tar -zxvf 将这个文件夹下的压缩文件 解压到这个目录下
* 执行这个脚本需要手动修改config/index 里面的historyProjectName属性,例如:2019-4-17-20,指定回滚到这个版本
*/
gulp.task('execSSHRollBack', () => {
console.log('回滚上一个版本...')
return gulpSSH.shell(config.rollback, { filePath: 'log/commands-unZip.log' })
.pipe(gulp.dest('logs')) // 会自动新建该目录
})
gulp.task('reloadNginx', () => {
console.log('重启服务器...')
return gulpSSH.shell(config.reload, { filePath: 'log/commands-reloadNginx.log' })
.pipe(gulp.dest('logs'))
})
/**
*上传前先删除服务器上现有文件...
*/
gulp.task('execSSHDelete', () => {
console.log('删除服务器上现有文件...')
return gulpSSH.shell(config.commands, { filePath: 'log/commands-delete.log' })
.pipe(gulp.dest('logs'))
})
/**
* publish 发布代码
*/
gulp.task('publish', () => {
console.log('开始上传文件到服务器...')
return gulp.src([gulpConfig.devServerSShConfig.uploadFile])
.pipe(gulpSSH.dest(config.remotePath))
})
/**
* gulp自动化部署。gulp.series:按照顺序执行
* 删除,发布,备份,重启
* 'execSSHDelete', 'publish', 'execSSHBackup', 'reloadNginx'
*/
gulp.task('default', gulp.series('execSSHDelete', 'publish', 'execSSHBackup', 'reloadNginx', (done) => {
console.log('发布完毕...', 'http://' + config.ssh.host + ':9090')
// Did you forget to signal async completion? 报错后需要调用done,以结束task
done() // 在不使用文件流的情况下,向task的函数里传入一个名叫done的回调函数,以结束task
}))
// gulpfile.config.js
/**
* 远程服务器的配置文件
* "gulp": "4.0.0",
* "gulp-ssh": "0.7.0",
*
* 测试服务器:196.168.0.124
* root/keduoli
*/
const data = new Date()
const time = data.getFullYear() + '-' + (data.getMonth() + 1) + '-' + data.getDate() + '-' + data.getHours() + '-' + data.getMinutes()
const remoteNginxPath = '/usr/local/nginx/' // 远程服务器的路径,结尾需要 / ( /usr/local/nginx/ 是nginx的源码 )
const remoteProjectPath = '/opt/web/' // 远程服务器项目的路径
// const remotePath = '/usr/local/nginx/' // 远程服务器的路径,结尾需要 / ( /usr/local/nginx/ 是nginx的源码 )
const projectName = 'gzmxweb' // 远程项目的名称,相当于dist
const historyProjectName = '2020-7-16-16-51' // 这个在回滚上一个版本的时候需要手动修改,滚动的版本号,例如:2019-4-17-20
const gulpConfig = {
devServerSShConfig: {
uploadFile: './dist/**',
sshConfig: {
// remotePath:'/root/nginx_szcg/website/zhifa/dist',
remotePath: remoteProjectPath + projectName, // 远程网站地址,会自动新建projectName文件夹
ssh: { // 外网测试
host: '172.16.121.74',
port: 22,
username: 'root',
password: 'password'
},
commands: [
// 删除现有文件
// `rm -rf /root/nginx_szcg/website/zhifa/ dist` ( 1.删除项目目录 )
'rm -rf ' + remoteProjectPath + projectName + '/*'
],
backups: [
// cd /root/nginx_szcg/website/zhifa/dist/ ( 2.进入项目目录 )
'cd ' + remoteProjectPath + projectName + '/',
// tar -zcvf /root/nginx_szcg/website/zhifa/dist-copy/2019-4-17-3-59.tar.gz ( 3.压缩备份,不会自动创建备份目录 )
'tar -zcvf ' + remoteProjectPath + projectName + '-copy/' + time + '.tar.gz *'
],
rollback: [
// tar -zxvf /root/nginx_szcg/website/zhifa/dist-copy/2019-4-17-3-59.tar.gz -C /root/nginx_szcg/website/zhifa/dist/(4.解压恢复)
'tar -zxvf ' + remoteProjectPath + projectName + '-copy/' + historyProjectName + '.tar.gz -C ' + remoteProjectPath + projectName + '/'
],
// 只有修改nginx服务器的配置文件才需要重启nginx
reload: [
// /usr/local/webserver/nginx/sbin/nginx -s stop ( nginx -s stop OR nginx -s reload OR nginx -s start)
remoteNginxPath + 'sbin/nginx -s stop',
// /usr/local/webserver/nginx/sbin/nginx -c /usr/local/webserver/nginx/conf/nginx.conf
remoteNginxPath + 'sbin/nginx -c /usr/local/nginx/conf/nginx.conf'
]
}
},
proServerSShConfig: {
}
}
module.exports = gulpConfig
-
版本控制
一个完整的应对各种突发情况的版本控制实践个人推荐 [git-flow]
(https://www.jianshu.com/p/41910dc6ef29) 模式
![](https://img.haomeiwen.com/i14626058/f61f2f1d5e8553b9.jpg)
![](https://img.haomeiwen.com/i14626058/aa4aea5404a74ab7.png)
另外一点是利用git的husky工具可以规范git的提交规范,配合commitlint
可以校验commit -msg的信息,同时配合commitizen进行命令行引导式提交。
npm i husky @commitlint/config-conventional @commitlint/cli commitizen -D
npx husky install
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'
npm i commitizen commitlint/config-conventional -D
// pakage.json
{
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"lint": "eslint src",
"lint:create": "eslint --init",
"commit": "cz"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint && npm run test",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
}
}
commitlint配置文件如下:
// commitlint.config.js
module.exports = {
// extends: ['@commitlint/config-conventional'],
rules: {
'body-leading-blank': [1, 'always'],
'footer-leading-blank': [1, 'always'],
'header-max-length': [2, 'always', 72],
'scope-case': [2, 'always', 'lower-case'],
'subject-case': [1, 'never', ['sentence-case', 'start-case', 'pascal-case',
'upper-case']],
'subject-empty': [2, 'never'],
'subject-full-stop': [2, 'never', '.'],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'type-enum': [
2,
'always',
['build', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor',
'revert', 'style', 'test', 'improvement']
]
}
}
// build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交
// ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交
// docs:文档更新
// feat:新增功能
// merge:分支合并 Merge branch ? of ?
// fix:bug 修复
// perf:性能, 体验优化
// refactor:重构代码(既没有新增功能,也没有修复 bug)
// style:不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑)
// test:新增测试用例或是更新现有测试
// revert:回滚某个更早之前的提交
// chore:不属于以上类型的其他类型
// git commit -m'feat: add commit valid'
-
工具规范
比如IDEA,我会设置一个editorconfig文件规范编辑器,这个需要在VScode装editor插件
// .editorconfig
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
再者其它工具如babel或者eslint的具体配置,会在项目根目录创建文件来配置
// babel.config.js
module.exports = {
// babel不用编译node_module下的文件
exclude: /node_modules/,
presets: [
'@vue/cli-plugin-babel/preset',
// polyfill 配置ie兼容
[
'@babel/preset-env',
{
useBuiltIns: 'entry',
corejs: 3
}
]
],
plugins: [
// polyfill 配置ie兼容
[
'@babel/plugin-transform-runtime',
{
absoluteRuntime: false,
corejs: false,
helpers: true,
regenerator: true,
useESModules: false
}
],
// element-ui按需加载
[
'component',
{
libraryName: 'element-ui',
styleLibraryName: 'theme-chalk'
}
]
]
}
网友评论