LorneNote

通往互联网自由之路


  • 首页

  • 归档

搭建高效的前端工作流 -- 附带实战项目

发表于 2019-03-16

安装CLI

1
$ sudo npm install -g @vue/cli

快速构建原型

1
$ sudo npm install -g @vue/cli-service-global

创建一个文件夹,随意命名:instant-prototyping,在此文件夹下创建App.vue文件,文件内容如下

1
2
3
<template>
<h1>Hello!</h1>
</template>

而后在该文件目录下,执行:

1
vue serve

编译后可通过浏览器地址:http://localhost:8080/预览。
也可以作为生产包部署。使用:

1
$ vue build

将生成一个dist的包。

创建一个项目

1
$ vue create hello-world

弹出一个列表。默认选中(babel, eslint),回车即可,后期组件可根据需要添加。

当然也可以使用GUI通过浏览器的图形界面安装,比如自选插件:Router(ESLint with error prevention only)+Unit Testing(Jest),可以保存一个预置信息,以便下次构建其他项目时直接启用。内建了图形化操作界面:比如添加vue插件、管理项目依赖、进行Vue CLI和ESLint配置、执行运行、打包、代码风格检查等任务。

使用GUI

命令行根目录下执行:

1
$ vue ui

启动GUI界面,点击Create a new project,选择项目安装目录,进行初始化配置,根据步骤一步一步提交即可,等待几分钟即可创建完成。

而后会提示你,启动开发服务器。

1
2
$ cd [ project root folder]
$ npm run serve

开发

静态资源管理

两种方式:

  • 在JavaScript导入或templates/CSS导入相关路径。这种引用被webpack管理
  • 放在public目录和引用绝对路径。这些资源仅仅被复制而不通过webpack管理。

URL翻译规则

  • 绝对路径会保持原样(eg:/images/foo.png);
  • 以.开始作为一个模块请求解析为基于文件夹结构的文件系统;
  • 以~开始,在它之后的任何内容被解释为模块请求。这意味着你能引用node modules内部的资源;
  • 以@开始也被推荐为一个模块请求。这是有用的因为Vue CLI默认的别名@为<projectRoot>/src.(仅在templates)。

构建现代、灵活且功能强大的前端工作流

快捷流畅的前端工作站需要几个要素,简洁快速的界面搭建,我们使用一个库Element,管理组件间关系的路由,使用Vue-Router,用于与后端服务器进行数据交换的工具,使用Axios。我们一个一个看。

Vue-Router

首先安装Vue-Router

1
$ vue add router

Vue本身使用组件组合应用。vue + router,我们需要做的就是映射我们的组件到路由,使路由知道渲染他们。如果你在使用CLI创建项目的时候选择安装vue-router,那么项目会为我们生成router的基本使用示例router.js,router.js是完成组件映射关系的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// router.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'

Vue.use(Router)

export default new Router({
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
// 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: "about" */ './views/About.vue')
}
]
})
  1. 首先导入Vue和Router,然和导入需要路由的组件Home.vue
  2. 调用Vue.use(Router)使用路由
  3. 创建路由实例export default new Router({...})
  4. 在路由实例内部定义路由routes:[ {path: '/', name: 'home', component},...]

把该路由导入程序入口文件,并挂载到根实例,保证全局响应router。

1
2
3
4
5
6
7
8
9
10
import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

new Vue({
router,
render: h => h(App)
}).$mount('#app')

如上我们导入router.js,使用router选项注入router给Vue的根实例。

1
2
3
4
5
6
7
8
9
10
// App.vue
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view/>
</div>
</template>

在HTML中,使用router-link导航,使用to指定链接的页面,<router-link>将被渲染为<a>标签。<router-view>路由的出口,与路由匹配的组件内容将被渲染到这里。

在任何组件内部,我们可以通过两个方式访问路由:this.$router和this.$route。

Element

当然,接下来为了演示路由的其他细节功能,我就结合一个基于Vue的UI库element,由于我们使用@vue/cli version 3.x搭建的项目,element为vue-cli@3提供了一个插件Element plugin,可以快速构建一个基本的element工程,进入项目根目录,在Terminal键入如下命令:

1
$ vue add element

当然这期间会跟随构建过程做一些基本的配置:

  1. 选择导入Element的方式:全局导入即可Fully import(Default);
  2. 你希望重写Element的SCSS变量吗,填写Y(yes)后直接回车;
  3. 选择你想加载的本地化语言,直接回车默认中文(zh-CN(Default)即可。

OK,安装完成后我们看到src下多了一个element-variables.scss文件,这里面引入了element的icon和字体。

1
2
3
4
5
6
7
8
9
10
11
12
// eleemnt-variables.scss
/*
Write your variables here. All available variables can be
found in element-ui/packages/theme-chalk/src/common/var.scss.
For example, to overwrite the theme color:
*/
$--color-primary: teal;

/* icon font path, required */
$--font-path: '~element-ui/lib/theme-chalk/fonts';

@import "~element-ui/packages/theme-chalk/src/index";

而src/plugins/element.js,对element和scss文件进行引入并调用:

1
2
3
4
5
6
// element.js
import Vue from 'vue'
import Element from 'element-ui'
import '../element-variables.scss'

Vue.use(Element)

而后在应用的入口程序main.js导入element.js文件:

1
import './plugins/element.js'

项目实战演示

我们会定义一个基本项目的雏形,包含登录页面,输入用户名、密码后展示主页,而后采用侧边栏分为两个栏目切换,切换完成后退出页面的基本流程,ok,我们开始:

首先,我们创建我们用到的三个页面:一个登录页Login.vue,一个主页面Home.vue,和一个About.vue,

因为我们要使用页面导航,所有我们需要在Router里建立页面对应的路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// router.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'

Vue.use(Router)

export default new Router({
routes: [
{
path:'/',
redirect: '/login',
},
{
path: '/login',
name: 'login',
component: () => import('./views/Login.vue')
},
{
path: '/',
name: 'home',
component: Home,
children:[
{
path: '/helloworld',
name: 'helloworld',
component:() => import('./components/HelloWorld.vue')

},
{
path: '/about',
name: 'about',
// 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: "about" */ './views/About.vue')
}
]
},

]
})

以上我们添加了一级页面登录页Login.vue和主页Home.vue,在Home.vue里面我们添加了用于分栏的两个页面HelloWord.vue和About.vue。

接下来开始写项目代码,写一个简单的登录页面,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<template>
<div class="login-wrap">
<div class="ms-title">后台管理系统</div>
<div class="ms-login">
<el-form :model="ruleForm">
<el-form-item>
<el-input v-model="ruleForm.username"></el-input>
</el-form-item>
<el-form-item>
<el-input type="password" v-model="ruleForm.password"></el-input>
</el-form-item>
<div class="login-btn">
<el-button type="primary" @click="submitForm()">登录</el-button>
</div>
</el-form>
</div>
</div>
</template>

<script>
export default {
name: "Login",
data() {
return {
ruleForm: {
username: '',
password: ''
}
}
},
methods: {
submitForm() {
if (this.ruleForm.password == '123456' && this.ruleForm.username == 'admin') {
sessionStorage.setItem("username", "admin");
this.$router.push('/helloworld');
this.$message({
message: "登录成功",
type: 'success'
})
} else {
this.$message({
message: "用户名或密码不正确",
type: 'error'
})
}
}
}

}
</script>

<style scoped>

.login-wrap {
position: relative;
width: 100%;
height: 100%;
}

.ms-title {
position: absolute;
top: 50%;
width: 100%;
margin-top: -230px;
text-align: center;
font-size: 30px;
color: #fff;
}

.ms-login {
position: absolute;
left: 50%;
top: 50%;
width: 300px;
height: 160px;
margin: -150px 0 0 -190px;
padding: 40px;
border-radius: 5px;
background: #fff;
}

.login-btn {
text-align: center;
}

.login-btn button {
width: 100%;
height: 36px;
}
</style>

在登录页,我们拟定了一个用户名和密码,输入正确即可登录成功,进入主页,展示主页的一个默认分栏内容HelloWorld页。

注意:这里面有一个class=”login-wrap”,这个我们定义了一个css样式,因为我们的系统样式最好统一,为了方便统一管理,我们定义一个全局样式,在assets/css/theme-green目录下(后两个文件夹需要自己手动创建)创建一个css全局样式文件:color-green.css,定义全局样式:

1
2
3
4
5
6
.header{
background-color: #00d1b2;
}
.login-wrap{
background: rgba(56, 157, 170, 0.82);
}

.login-wrap就是我们登录页的背景颜色,.header一会用于我们主页的顶部栏–浅绿色,一会看到。

我们在assets/css/在创建一个main.css文件,用于创建我们全局的尺寸,字体,内容等样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
*{margin:0;padding:0;}
html,body,#app,.wrapper{
width:100%;
height:100%;
overflow: hidden;
}
body{
font-family:"Helvetica Neue",Helvetica, "microsoft yahei", arial, STHeiTi, sans-serif;
}
a{text-decoration: none}
.content{
background: none repeat scroll 0 0 #fff;
position: absolute;
left: 250px;
right: 0;
top: 70px;
bottom:0;
width: auto;
padding:40px;
box-sizing: border-box;
overflow-y: scroll;
}

main.css里面我们定义了页面的尺寸,body体的文字,定义了标准的文本a{…},为none即无修饰,以及content的样式等信息。

而后,我们将这两个css文件导入到App.vue进行全局引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// App.vue
<template>
<div id="app">
<router-view/>
</div>
</template>

<script>
export default {
name: 'app',
}
</script>

<style>
@import "assets/css/main.css";
@import "assets/css/theme-green/color-green.css";
</style>

我们已经写好的登录页面
Alt text

当然为了更好的理解,我们先来看一下主页的基本结构:

Alt text
Alt text

这样,一目了然,简单明了,我们点击登录按钮进来,首先映入眼帘的是主页的欢迎分栏页内容,主页上包含了顶部的header区域,左边的管理列表侧边栏,和中间的内容区域欢迎分栏(默认的首页–HelloWorld.vue)。点击关于分栏进入该页面,当然目前只有一句话内容:This is an about page。点击admin用户名会弹出带退出按钮下拉菜单,点击退出按钮回到登录页面。

为了让结构更为清晰,我们使用封装思想,在src/components/创建Header.vue和Sidebar.vue用于书写主页页头和侧边栏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// Header.vue
<template>
<div class="header">
<div class="logo">后台管理系统</div>
<div class="user-info">
<el-dropdown trigger="click" @command="handleCommand">
<span class="el-dropdown-link">
<img class="user-logo" src="../assets/logo.png">
{{ username }}
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="loginout">退出</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</template>

<script>
export default {
name: "Header",
data() {
return {
name: 'default',
}
},
computed: {
username() {
let username = sessionStorage.getItem('username')
return username ? username: this.name
}
},
methods: {
handleCommand(command) {
if (command == 'loginout') {
sessionStorage.removeItem('username')
this.$router.push('/login')
}
}
}
}
</script>

<style scoped>
.header {
position: relative;
box-sizing: border-box;
width: 100%;
height: 70px;
font-size: 22px;
line-height: 70px;
color: #fff;
}
.logo{
float: left;
width:250px;
text-align: center;
}
.user-info {
float: right;
padding-right: 50px;
font-size: 16px;
color: #fff;
}
.user-info .el-dropdown-link{
position: relative;
display: inline-block;
padding-left: 50px;
color: #fff;
cursor: pointer;
vertical-align: middle;
}
.user-info .user-logo{
position: absolute;
left:0;
top:15px;
width:40px;
height:40px;
border-radius: 50%;
}
</style>

如上这里面可以显示用户名称和icon,响应退出事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Sidebar.vue
<template>
<div class= "sidebar">
<el-row class="tac">
<el-col>
<el-menu default-active="helloworld"
class="el-menu-vertical-demo"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b" router>
<el-submenu index="1" style="width: 200px">
<template slot="title">
<span>管理列表</span>
</template>
<el-menu-item index="helloworld">欢迎</el-menu-item>
<el-menu-item index="about">关于</el-menu-item>
</el-submenu>
</el-menu>
</el-col>
</el-row>
</div>
</template>

<script>
export default {
name: "Sidebar",

}
</script>

<style scoped>
.sidebar{
display: block;
position: absolute;
width: 200px;
left: 0;
top: 70px;
bottom:0;
background-color: rgb(50, 86, 87);
}
</style>

Sidebar.vue主要实现侧边栏的分栏内容设置。
而后,在Home.vue页面引入这两个组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Home.vue
<template>
<div class="warpper">
<my-header></my-header>
<my-sidebar></my-sidebar>
<div class="content">
<transition name="move" mode="out-in"><router-view></router-view></transition>
</div>
</div>
</template>

<script>
// @ is an alias to /src
import MyHeader from '@/components/Header.vue'
import MySidebar from '@/components/Sidebar.vue'

export default {
name: 'home',
components: {
MyHeader,
MySidebar
}
}
</script>

至此,我们完成了后台管理界面基本雏形的搭建,项目目录结构如下:
Alt text

总结一下:
我们使用Vue CLI3搭建了项目,引入了Vue Router做页面路由,引入Element做界面组件,并完成了一个从登录-展示页面结构-退出的完整页面流程。通过这些操作我们学会了如何创建项目,以及路由和Element的基本用法,一个简单高效的前端工作流搭建完成。

其他

自定义主题

我们可以在主题预览,下载完整的主题包,而后导入主题,比如我们将下载的主题命名为element-green,放到项目的assets文件夹下面。直接在main.js导入即可:import './assets/element-green/index.css',默认主题可以注释掉了。

网络请求

很多时候我们都需要请求后台数据来调试我们的界面,Vue官方推荐使用axios。

安装axios

1
npm install axios

这个请求会在很多页面使用到,所以我们在程序的入口将它代理给Vue,打开main.js文件

1
2
3
import Axios from 'axios'

Vue.prototype.$http = Axios;

这样的我们在其他组件页面直接使用this.$http.get/this.$http.post的形式即可访问API,无需再导入头文件。

使用参考:
vue.js中使用Axios
使用vue-cli+axios配置代理进行跨域访问数据

跨域问题解决

由于我们的开发服务器和我们请求的API可能不在一台机器上,所以如果你直接使用axios请求API可能会出现跨域问题,即:请求被浏览器拦截。

我们需要在vue.config.js文件下作如下代理访问配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
lintOnSave: false,
devServer: {
port: 8080,
proxy: {
'/v1': {
target: 'https://api.coindesk.com',
ws: true,
changeOrigin: true
}
}
}
}

8080是我们要监听的端口,target后面为我们要请求的API的host地址。我是怎么知道的这个,搜索呗,百度:

关键词:vue axios cli跨域
Alt text

上图打开第二个链接指向这里:
Vue-cli@3版本下使用axios如何设置跨域配置?,而后这个链接提示来这里找,即官方文档有写:
Vue CLI | vue.config.js | devServer.proxy,由于官方提示的信息不全面,让参考 http-proxy-middleware,于是点进去看了看,没有合适的说明,怎么办呢,继续关键词搜索呗:

关键词:axios vue-cli@3跨域 devServer
Alt text
如上图,点开第二个链接:vue-cli3.0 axios跨域请求代理配置及端口修改,
ok,这里有配置完整的答案。在官方的代码段的基础上加一个端口号嘛。到此,解决了这个跨域问题,你看看,有时候正确的关键词搜索能力是高效解决问题的关键,因为你碰到的问题很可能别人也碰到并解决,而且分享出来了,找到即可。

注意:
配置完代理后要再次重启服务器配置才能生效。控制台运行npm run serve即可。

延伸阅读:
Vue CLI | Config REference: 看更多参数配置。

而后,打开Home.vue页面,在mounted()方法中,作如下请求:

1
2
3
4
mounted() {
this.$http.get('http://localhost:8080/v1/bpi/currentprice.json')
.then(response => (this.info = response))
}

如果使用Google Chrome浏览器,打开后台的network板块即可看到响应数据。

代码版本控制

  1. 创建远程仓库
    以码云代码管理平台为例,点击主页+号创建私有仓库,填写仓库配置信息:
1
2
3
4
5
仓库名称:XXX
仓库介绍:XXX
选择语言:JavaScript
添加.gitignore:Node
勾掉:使用Readme文件初始化这个仓库(因为我们创建的项目中会带有Readme文件,不需要用这个)

点击创建按钮即可。

2.将远程仓库克隆到本地,我们使用一个软件名为Sourcetree(Sourcetree具体用法,自行百度)。

打开SourceTree主页,点击New...下拉菜单,选择Clone from Url,把刚刚我们创建的远程仓库克隆到本地。会弹出一个对话框:

Alt text

Source URL:到码云上,找到刚刚我们创建的仓库,点击克隆/下载按钮,在弹出的对话框中点击复制按钮复制HTTPS链接,将复制到剪切版的URL黏贴到此处即可。

Destination Path:选择一个存储到本地项目文件夹,可点击...快捷选择。

Name:会在Destination Path填充后自动生成。

点击Clone即可。

到此,本地仓库已经创建好,将刚刚创建好的项目拖入这个文件夹即可。我们以后就可以用Sourcetree进行版本控制了。

注意
我们刚刚创建项目的时候项目本身有一个文件夹HelloWorld,而刚刚我们创建本地仓库也有一个文件夹,比如名为HelloWorldGit,这样就我们的项目内容路径为HelloWorldGit/HelloWorld/entity files,这样就会导致我们的实体内容路径较深,可以把第三层的实体内容移出来。中间一层的文件夹删掉,变成HelloWorldGit/entity files。

使用WebStrom进行版本管理的一些提示

我们开发工具使用的是WebStorm,我们可以将新创建的项目添加到WebStorm的版本控制下:

1
2
CVS | Git | Add (或 Alt+command+A)   添加代码为本地版本控制
⌘K 提交代码到本地仓库

使用WebStorm提交到本地仓库,WebStorm会进行代码检查。常见错误有:

  1. Warning -- Unused default export
    确定代码没有问题,可以去掉此提示,点击WebStrom程序主板右下角的小人头,搜索javasript general unused ,勾掉Unused global symbol。

  2. Warning: Unterminated statement
    找到对应位置,添加中断声明,可能是添加一个分号而已。

  3. .idea文件报错:Error:Element profile is not allowed here.
    .idea文件是WebStorm自动生成的配置文件,是隐藏文件,若损坏或删除。可能导致程序的实体文件夹无法在WebStorm的工程目录中显示。忽略此错误即可。

最后在使用WebStorm Commit代码时,如果代码检查依然有误,会提示直接提交还是Review审查代码,可以Review看一遍所有的错误和警告,确认没有问题直接commit提交即可。提交到本地仓库之后,还是建议使用SourceTree推送代码到远程仓库,因为这个界面更简洁更方便友好,尤其是同时管理多个项目时。


以上实战项目的git地址:hello-world

Vue代码规范 -- 推荐 & 谨慎使用篇[译]

发表于 2019-02-12

组件/实例选项顺序

组件/实例选项的顺序应该一致
这是我们推荐的组件选项的默认顺序。他们被分成不同的分类,所以你会知道在插件的哪里添加新的属性。

  1. 副作用(组件之外触发)
  • el
  1. 全局意识(要求组件之外的知识)
  • name
  • parent
  1. 组件类型(改变组件的类型)
  • functional
  1. 组件修饰符(改变组件被编译的方式)
  • delimiters
  • comments
  1. 组件依赖(资源在模板内使用)
  • components
  • directive
  • filters
  1. Composition(将属性合并到选项内)
  • extends
  • mixins
  1. Interface(组件界面)
  • inheritAttrs
  • model
  • props/propsData
  1. 本地状态(本地响应属性)
  • data
  • computed
  1. Event(响应事件触发回调)
  • watch
  • 事件循环(按它们被调用的顺序)
    • beforeCreate
    • created
    • beforeMount
    • mounted
    • beforeUpdate
    • updated
    • activated
    • deactivated
    • beforeDestory
    • destroyed
  1. 非响应式属性(独立于响应式系统的实例属性)
  • methods
  1. 渲染(组件输出的声明性描述)
  • template/render
  • renderError

元素特性顺序

元素的特性(包括组件)的顺序应该一致。
以下是我们推荐的组件选项的默认顺序。他们被分为不同的分类,所以你会知道在哪个添加自定义的特性和指令。

  1. 定义(提供组件选项)
  • is
  1. 列表渲染(创建相同元素的多个变体)
  • v-for
  1. 条件(元素被怎样渲染/展示)
  • v-if
  • v-else-if
  • v-else
  • v-show
  • v-cloak
  1. 渲染修饰符(改变元素渲染的方式)
  • v-pre
  • v-once
  1. 全局意识(要求组件以外的知识)
  • id
  1. 唯一特性(需要唯一值的特性)
  • ref
  • key
  • slot
  1. 双向绑定(结合绑定和事件)
  • v-model
  1. 其他特性(所有未指定的绑定 & 未绑定的特性)
  2. Events(组件事件监听)
  • v-on
  1. 内容(复写元素的内容)
  • v-html
  • v-text

在组件/实例选项内空行

你可能想在多行属性之间添加空行,特别是如果这些选项在非滚动的屏幕不合适时。

当组件感觉狭小或阅读困难,在多行属性之间添加空格可以使他们更容易浏览。在一些编辑器,例如Vim,这样的格式化选项也可以使他们更容易在键盘上导航。
Good

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
props: {
value: {
type: String,
required: true
},

focused: {
type: Boolean,
default: false
},

label: String,
icon: String
},

computed: {
formattedValue: function () {
// ...
},

inputClasses: function () {
// ...
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 没有空格也可以只要是组件
// 容易阅读和导航
props: {
value: {
tpye: String,
required: true
},
focused: {
type: Boolean,
default: false
},
label: String,
icon: String
},
computed: {
formattedValue: function () {
// ...
},
inputClasses: function() {
// ...
}
}

单文件组件最外层元素的顺序

单文件组件应该总是<script>,<template>,和<style>标签顺序,<style>在最后,因为另外两个标签至少要有一个。
Bad

1
2
3
4
// In HTML
<style>/* ... */</style>
<script>/* ... */</script>
<template>...</template>

1
2
3
4
5
6
7
8
9
10
// In HTML
<!-- ComponentA.vue -->
<script>/* ... */</script>
<template>...</template>
<style>/* ... */</style>

<!-- Component.vue> -->
<template>...</template>
<script>/* ... */</script>
<style>/* ... */</style>

Good

1
2
3
4
5
6
7
8
9
10
// In HTML
<!-- ComponentA.vue -->
<script>/* ... */</script>
<template>...</template>
<style>/* ... */</style>

<!-- ComponentB.vue -->
<script>/* ... */</script>
<template>...</template>
<style>/* ... */</style>

1
2
3
4
5
6
7
8
9
<!-- ComponentA.vue -->
<template>...</template>
<script>/* ... */</script>
<style>/* ... */</style>

<!-- ComponentB.vue -->
<template>...</template>
<script>/* ... */</script>
<style>/* ... */</style>

优先级D规则:谨慎使用(有潜在危险的模式)

v-if/v-else-if/v-else没有key

最好使用携带key的v-if + v-else,如果他们是相同而元素类型(e.g.都是<div>元素)。
默认情况下,Vue尽可能高效的更新DOM,那意味着当切换两个相同元素的时候,它仅仅分发已经存在的元素,而不出删除它再去添加一个新的。如果这些元素实际上不应该被认为是同一个可能产生意外的后果。
Bad

1
2
3
4
5
6
<div v-if="error">
Error: {{ error }}
</div>
<div v-else>
{{ results }}
</div>

Good

1
2
3
4
5
6
7
8
9
10
11
12
<div
v-if="error"
key="seach-status"
>
Error: {{ error }}
</div>
<div
v-else
key="search-results"
>
{{ results }}
</div>

1
2
3
4
5
6
<p v-if="error">
Error:{{error}}
</p>
<div v-else>
{{ results }}
</div>

使用scoped的元素选择器

元素选择器应该避免在scoped中出现。

在scoped样式中,应该首选类选择器而不是元素选择器,因为大量使用元素选择器是很慢的。

详细解释
对于scoped样式,Vue为组件元素添加了唯一特性,例如:data-v-f3f3eg9 。然后修改选择器,以便匹配这些与此元素匹配的元素(例如:button[data-v-f3f3eg9]).

问题是大量的元素特性选择器(例如:button[data-v-f3f3eg9])将会相当慢比类特性选择器,所以无论何时类选择器是首选。

Bad

1
2
3
4
5
6
7
8
9
<template>
<button>X</button>
</template>

<style scoped>
button {
background-color: red;
}
</style>

Good

1
2
3
4
5
6
7
8
9
<template>
<button class="btn btn-close">X</button>
</template>

<style scoped>
.btn-close {
background-color: red
}
</style>

隐式的父-子组件通信

Props和事件应该作为父-子间通信的首选,代替this.$parent或改变props。

一个理想的Vue应用是props向下传递,事件向上传递。坚持这个惯例使你的组件更容易理解。然而,在一些边缘情况下prop的变更或this.$parent能够简化两个已经深度耦合的组件。

问题是,在许多简单的场景下这种模式可能提供便利。但请当心:不要为了短期的方便(少写代码)而以能够理解的状态流作为交换。

bad

1
2
3
4
5
6
7
8
9
10
// In JS
Vue.component('TodoItem',{
props: {
todo: {
type: Object,
required: true
}
},
template: '<input v-model="todo.text">'
})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Vue.component('TodoItem',{
props: {
todo: {
type: Object,
required: true
}
},
methods: {
removeTodo () {
var vm = this
vm.$parent.todos = vm.$parent.todos.filter(function (todo) {
return todo.id !== vm.todo.id
})
}
},
template: `
<span>
{{ todo.text }}
<button @click="removeTodo">
X
</button>
</span>
`
})

Good

1
2
3
4
5
6
7
8
9
10
11
12
Vue.component('TodoItem', {
props: {
todo: {
type: Object,
required: true
}
},
template: `
<input
:value="todo.text"
@input="$emit('input', $event.target.value)"
})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Vue.component('TodoItem',{
props: {
todo: {
type: Object,
required: true
}
},
template: `
<span>
{{todo.text}}
<button @click="$emit('delete')"
X
</button>
</span>
`
})

非Flux状态管理

Vuex是全局状态管理的首选,代替this.$root或一个全局事件总线。

使用this.$root和/或使用全局事件总线管理状态是一种惯例对于非常简单的场景,但是对大多数应用不合适。Vuex不仅提供了一个中心区域去管理状态,而且也是组织、追踪和调试状态改变的工具。

Bad

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// main.js
new Vue({
data: {
todos:[]
},
created: function () {
this.$on('remove-todo',this.removeTodo)
},
methods:{
removeTodo: function (todo) {
var todoIdToRemove = tood.id
this.todos = this.todos.filter(function (todo) {
return todo.id !== todoToRemove
})
}
}
})

Good

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// store/modules/todos.js
export default {
state: {
list: []
},
mutations: {
REMOVE_TODO(state,todoId) {
state.list = state.list.filter(todo => todo.id !== toodId)
}
},
actions: {
removeTodo ({ commit, state }, todo) {
commit('REMOVE_TODD',todo.id)
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// In HTML
<!-- TodoItem.vue -->
<template>
<span>
{{ todo.text }}
<button @click="removeTodo(todo)">
X
</button>
</span>
</template>

<script>
import { mapActions } from 'vuex'

export default {
props: {
todo: {
type: Object,
required: true
}
},
methods: mapAcitons([''removeTod'])
}
</script>

原文:Priority C & D Rules: Recommended and Use with Caution

Vue代码规范 -- 强烈推荐篇[译]

发表于 2019-02-10

优先级B:强烈推荐(提高可读性)

组件文件

只要有能够拼接文件的构建系统,每一个组件应该在它自己的文件里。

当你需要编辑或查看怎样使用它时能够帮助你快速的发现组件。

Bad

1
2
3
4
5
6
Vue.component('TodoList', {
// ...
})
Vue.component('TodoItem', {
// ...
})


Good

1
2
3
components/
| - TodoList.js
| - TodoItem.js

1
2
| - TodoList.vue
| - TodoItem.vue

单文件组件文件名大小写

单文件组件的文件名应该总是使用PascalCase(首字母大写的驼峰式)或总是使用kebab-case(下划线连接式)的写法。
PascalCase对于代码编辑器的自动完成最为友好。因为它和我们在JS(X)和模板中引用组件尽可能是一致的。然而,混用文件名可能导致在大小写不敏感的文件系统中出问题,这就是为什么kebab-case也完全是可以接收的。

Bad

1
2
components/
| - mycomponent.vue

1
2
components/
| - myComponent.vue

Good

1
2
3
components/
| - MyComponent.vue
| - my-component.vue

基本的组件名

基本组件(也称表现的,非智能的或纯组件)应用app的特定样式和惯例应该总是有一个特定的前缀,例如:Base,App,或V。

详解解释
这些组件为应用程序中的一致样式和行为奠定了基础。他们仅仅包含:

  • HTML元素,
  • 其他基本组件,和
  • 第三方UI组件。

但是他们绝对不会包含全局状态(eg:来自Vuex商店)。

这些名称常常包含了他们封装的元素名(例如:BaseButton,BaseTable),除非为了某个特定的目的没有元素的存在(例如:BaseIcon)。如果你为更多的具体上下文构建了类似的组件,他们几乎总成充满这些组件(例如:BaseButton可能被用在ButtonSubmit)。
这样做的几点好处:

  • 当编辑器按字母顺序组织排版时,你的应用的基本组件总是被列在一起,使他们更容易识别。
  • 用于组件名称应由多个词构成,这个用法避免你去随意选择前缀来封装你的组件(e.g. Mybutton,VueButton)。
  • 用于这些组件也时常被使用,你可能想要把他们设置成全局属性而避免每次使用他们。使用相同的前缀可以让Webpack这样工作:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    	var requireComponent = require.context("./src", true, /^Base[A-Z]/)
    requireComonent.keys().forEach( function (fileName) {
    var baseComponentConfig = requireComponent(fileName)
    baseComponentConfig = baseComponentConfig.default || baseComponentConfig
    var baseComponentName = baseComponentConfig.name || (
    fileName
    .replace(/^.+\//, '')
    .replace(/\.\w+$/, '')
    )
    Vue.component(baseComponentName, baseComponentConfig)
    })
    >

Bad

1
2
3
4
components/
| - MyButton.vue
| - VueTable.vue
| - Icon.vue


Good

1
2
3
4
components/
| - BaseButton.vue
| - BaseTable.vue
| - BaseIcon.vue

1
2
3
4
components/
| - AppButton.vue
| - AppTable.vue
| - AppIcon.vue
1
2
3
4
compoennts/
| - VButton.vue
| - VTable.vue
| - VIcon.vue

单文件实例组件名

组件应该仅仅有一个激活实例组件以The前缀开头,以表示他们仅仅有一个。

这并不表示组件只能被用在一个单独的页面,而是在每页只能用一次。这些组件不接受任何props,因为他们是为你的app定制的,而不是他们在你的应用中的上下文。如果你发现需要添加props。这是一个好兆头,因为那意味着到现在为止它实际上是一个可以复用的组件而不是每页只能用一次。

Bad

1
2
3
components/
| - Heading.vue
| - MySidebar.vue


Good

1
2
3
components/
| - TheHeading.vue
| - TheSidebar.vue

紧密耦合的组件名

子组件是和他们的父组件紧密耦合的,所有应该包含父组件名称作为前缀。
如果一个组件仅仅在一个单独的父组件的上下文是有意义的,那么他们的关系应该体现在他们的名字上。由于编辑器使用按字母顺序组织文件,所有他们也能够在文件中保持这种关系。

详细解释
你可能想要通过在父组件的目录下嵌套子组件的方式解决这个问题,例如:

1
2
3
4
5
6
components\
| - TodoList/
| - Item/
| - index.vue
| - Button.vue
| - index.vue

或:

1
2
3
4
5
6
components/
| - TodoList\
| - Item/
| - Button.vue
| - Item.vue
| - TodoList.vue

不推荐这样做,其结果是:

  • 许多文件使用相似的名字,使在编辑器中快速切换文件变得困难。
  • 多层被嵌套的子目录,在编辑器的侧边栏查看组件会增加查找时间。

Bad

1
2
3
4
components/
| - TodoList.vue
| - TodoItem.vue
| - TodoButton.vue

1
2
3
components/
| - SearchSidebar.vue
| - NavigationForSearchSidebar.vue

Good

1
2
3
4
components/
| - TodoList.vue
| - TodoListItem.vue
| - TodoListItemButton.vue

1
2
3
components/
| - SearchSidebar.vue
| - SearchSidebarNavigation.vue

组件名中的单词顺序

组件名称应用总是以最高等级的(通常是最常用的)单词开始,以描述修改的单词结尾。

详细解释
你可能会惊讶:
为什么我们强制组件名去使用较少的原生语言?
在本土英语里,形容词和其他描述符一般出现在名词之前,有连接词的例外,例如:

  • Coffee with milk
  • Soup of the day
  • Visitor to the museum

如果你喜欢的话,可以定义这些包含连接符的词在组件名里,但是顺序仍然是重要的。

也要注意在你的上下文中什么被认为是“最高级的”。例如,想象一个带搜索表单的app,它可能包含组件像这样:

1
2
3
4
5
6
7
components/
| - ClearSearchButton.vue
| - ExcludeFromSearchInput.vue
| - LaunchOnStartupCheckbox.vue
| - RunSearchButton.vue
| - SearchInput.vue
| - TermsCheckbox.vue

你可能注意到了,它查找特定的搜索组件看起来十分的困难。现在让我们重命名组件根据这个规则:

1
2
3
4
5
6
components/
| - SearchButtonClear.vue
| - SearchButtonRun.vue
| - SearchInputExcludeGlob.vue
| - SettingsCheckboxLaunchOnStartup.vue
| - SettingsCheckboxTerms.vue

由于编辑器使用字母组织文件,组件间的重要关系现在非常明显的看到。

你可能想要使用不同的方式解决这个问题,嵌套所有搜索组件在“search“目录,而后所有的设置组件在settings目录。我们只推荐这种实现在非常大的应用里,(比如超过了100个组件),例如:

  • 在子目录下嵌套常常比在一个components目录下滚动要花费更多的时间。
  • 命名冲突(例如:多个ButtonDelete.vue组件)使代码编辑器快速定位到特定组件变的困难。
  • 重构变得困难,因为查找和替换常常不足以更新对一个移动组件的相对引用。

Bad

1
2
3
4
5
6
7
components/
| - ClearSearchButton.vue
| - ExcludeFromSearchInput.vue
| - LanuchOnStartupCheckbox.vue
| - RunSearchButton.vue
| - SearchInput.vue
| - TermsCheckbox.vue


Good

1
2
3
4
5
6
7
components/
| - SearchButtonClear.vue
| - SearchButtonRun.vue
| - SearchInputQuery.vue
| - SearchInputExcludeGlob.vue
| - SettingsCheckboxTerms.vue
| - SettingsCheckboxLaunchOnStartup.vue

自闭合组件

在单文件组件(英文)、字符串模板、和JSX(英文)没有内容的组件应该是自闭合的,但是在DOM模板不要这样做。

自闭合组件传递的信息时不仅没有内容,而且注定没有内容。他们和书的空白页是不同的,有一个标签”这页有意留空“你的代码也会清除不需要的闭合标签。

幸运的是,HTML不允许自定义的元素自闭合的-除了官方的“void”元素(英文)。这就是为什么这个策略是可行的,当Vue的模板编译器可以在DOM之前达到模板,然后产出符合DOM规范的HTML。

Bad

1
2
<! -- 在单文件组件中,字符串模板,JSX -->
<MyComponent></myComponent>

1
<my-component/>

Good

1
2
<! --  在单文件组件,字符串模板,JSX -- >
<MyComponent/>

1
2
<! -- 在DOM模板 -->
<my-component></my-component>

在模板中组件名称大小写

在大部分项目里,组件名称应用总是使用大写开头的驼峰写法在单文件组件和字符串模板里,-但在DOM模板中使用kebab-case。

PascalCase比kebab-case有一些优势:

  • 编辑器在模板中自动完成组件名,因为PascalCase被用在JavaScript。
  • <MyComponent>比<my-component>更直观的区别于HTML元素,因为有两个不同的自身(两个大写字母),而不是只有一个(连字符)。
  • 如果你使用了任何非Vue自定义元素在你的模板中,例如一个web组件,PascalCase能够确保你的Vue组件仍然明显可见的。

不幸的是,由于HTML不区分大小写,DOM模板必须一致使用kebab-case。

也要注意如果你已经是kebab-case的重度用户,为了保证HTML惯例的一致在你所有的项目中使用同样的大小写可能比上面所列出的要更重要,在这种情况下,使用kebab-case在所有地方是可以接受的。

Bad

1
2
<! -- 在单文件组件和字符串模板中 -->
<mycomponent/>

1
2
<! --  在DOM模板 -- >
<MyComponent></MyComponent>

Good

1
2
<! -- 在当文件组件和字符串模板中 -->
<MyComponent/>

1
2
<! -- 在DOM模板 -->
<my-component></my-component>

或

1
2
<! -- 在任何地方 -->
<my-component></my-component>

在JS/JSX组件名大小写

在JS/JSX中应该总是使用首字母大写的驼峰写法,虽
然他们可能使用连字符kebab-case的写法,但这仅仅用于一些通过Vue.component全局组件注册的简单应用程序中。

详细解释
在JavaScript中,PascalCase(驼峰)的写法对于类和原型构造器是惯例 –基本上,任何东西都拥有实例。Vue组件也是实例,所以它使用PascalCase的写法是有意义的。作为一个额外的好处,在JSX(和模板中)使用PascalCase写法可以使读者更好的区分组件和HTML元素的不同。

然而,对与使用Vue.component定义的全局组件来说,我们推荐使用kebab-case(下划线连接)的写法代替。因为:

  • 全局组件在JavaScript中引用比较少见,所以遵循这个惯例对应JavaScript来说意义不大。
  • 这些应用内总是包含许多内在的DOM模板,使用kebab-case是必须的。

Bad

1
2
3
Vue.component('myComponent', {
// ...
})

1
import myComponent from './MyComponent.vue'
1
2
3
4
export default {
name: 'myComponent',
// ...
}
1
2
3
4
export default {
name: 'my-component',
// ...
}

Good

1
2
3
Vue.component('MyComponent', {
// ...
})

1
2
3
Vue.component('my-component',{
// ...
})
1
import MyComponent from './MyComponent.vue'
1
2
3
4
export default {
name: 'MyComponent',
// ...
}

完整单词的组件名

组件名称应该尽量用完整的单词而不是缩写
编辑器里的自动完成会使我们写长词花费的时间非常少,而且完整的单词会使释义非常清晰。尤其是不常用的缩写,应该尽量避免。
Bad

1
2
3
components/
|- SdSettings.vue
|- UProfOpts.vue

Good

1
2
3
components/
|- StudentDashboardSettings.vue
|- UserProfileOptions.vue

Prop名称的大小写

Prop的名称在声明期间应该总是使用cameCase(首字母小写的驼峰式),但是kebab-case用在模板和JSX。

我们需要简单的遵守每一种语言的惯例。在JavaScript中,camelCase更常用,在HTML,是kebab-case。

Bad

1
2
3
4
// In JS
props: {
'greeting-text':String
}

1
2
// In HTML
<WelcomeMesssge greetingText="hi"/>

Good

1
2
3
4
// In JS
props: {
greetingText: String
}

1
2
// In HTML
<WelcomeMessage greeting-text="hi"/>

多个attribute元素

使用多个attributes的元素应该分为多行,每行使用一个attribute。

在JavaScript,用多行切分对象的多个properties被认为是一种好习惯,因为易读。我们的模板和JSX应该使用相同的方式。
Bad

1
<img src="https://vuejs.org/images/logo.png" alt="Vue Logo">

1
<MyComponent foo="a" bar="c" baz="c" />

Good

1
2
3
4
<img
src="https://vuejs.org/images/logo.png"
alt="Vue Logo"
>

1
2
3
4
5
<MyComponent
foo="a"
bar="b"
baz="c"
/>

在模板中的简单表达式

组件模板应该仅仅包含简单的表达式,多个复杂的表达式应该使用计算属性或方法重构。

模板中的复杂表达式使他们不那么具有声明性。我们应该集中描述会出现什么,而不是我们怎样计算它的值。计算属性和方法也允许我们代码复用。
Bad

1
2
3
4
5
6
// In HTML
{{
fullName.split(' ').map(function (word){
return word[0].toUpperCase() + word.slice(1)
}).join(' ')
}}

Good

1
2
3
4
5
6
7
8
9
10
11
12
13
// In HTML
<!-- In a template>
{{ normallizedFullName }}

// In JavaScript
// The complex expression has been moved to a computed properties
computed: {
normalizedFullName: function() {
return this.split(' ').map(function (word) {
return word[0].toUpperCase() + word.slice(1)
}).join(' ')
}
}

简单的计算属性

复杂的计算属性应该尽可能的分割为多个简单的计算属性。

详细解释
简单的,命名良好的计算属性是这样的:

  • 容易测试
    当每一个计算属性包含仅仅一个简单表达式的时候,使用比较少的依赖,
    编写测试来确认他们可以正确工作要容易的多。
  • 容易阅读
    特定的计算属性集中于你名字所描述的每一个值,即使他们不可以复用。
    这使得其他开发者(和未来的你)能够更集中于关心和计算你的代码发生
    了什么。
  • 更能适应需求的改变
    被命名的任何值都可能是对视图有用的,例如,我们可能要去显示一条消
    息来告诉用户他们存了多少钱。我们也可能打算去计算销售税,但是可能
    分开显示他们,而不是作为最终价格的一部分。

小的,专注于计算属性使信息怎样被使用的假设条件更少,所以当需求改变时减少重构。

Bad

1
2
3
4
5
6
7
8
9
10
// In JS
computed: {
price: function() {
var basePrice = this.manufactureCost / (1 - this.profitMargin)
return (
basePrice -
basePrice * (this.discountPercent || 0)
)
}
}

Good

1
2
3
4
5
6
7
8
9
10
11
12
// In JS
computed: {
basePrice: function () {
return this.manufactueCost / (1 - this.profitMargin)
},
discount: function () {
return this.basePrice * (this.discountPercent || 0)
},
finalPrice: function() {
return this.basePrice - this.discount
}
}

带引号的特性值

非空HTML特性值应该总是在引号内部(单引号或双引号,在JS里不用的那个)

在HTML中不带空格的特性值是可以没有引号的,但这里鼓励大家不写空格,使特性值的可读性变差。

Bad

1
<input type=text>
1
<AppSidebar :style={width:sidebarWidth+'px'}>

Good

1
<input type="text">
1
<AppSidebar :style="{ width: sidebarWidth + 'px' }">

指令缩写

指令缩写(:for v-bind: 和 @ for v-on:)应该总是使用或从不使用。

Bad

1
2
3
4
<input
v-bind:value="newTodoText"
:placeholder="newTodoINstructions"
>
1
2
3
4
<input
v-on:input="onInput"
@focus="onFocus"
>

Good

1
2
3
4
<input
:value="newTodoText"
:placeholder="newTodoInstructions"
>
1
2
3
4
<input
v-bind:value="newTodoText"
v-bind:placeholder="newTodoInstructions"
>
1
2
3
4
<input
@input="onInput"
@focus="onFocus"
>
1
2
3
4
<input
v-on:input="onInput"
v-on:focus="onFocus"
>

原文:Priority B Rules: Strong Recommended(Improving Readalibity)(英)

从0到1构建极简Vue项目

发表于 2019-02-07

注意
本文默认读者掌握了一定的前端知识,如果你对前端知识一无所知,请先自行查看前端的三大知识点(HTML + CSS + JavaScript),而后再回来继续。

初步搭建开发环境

安装CLI

要搭建Vue项目,我们需要使用一个工具CLI。CLI需要Node.js环境,我们使用的包管理工具是npm,如果Node.js和npm没有安装,请先行安装。如已经安装,那么我们继续。请打开Mac(注意这里的演示使用MacOS系统)的Terminal,输入如下命令:

1
$ npm install -g @vue/cli

安装完成后,键入如下命令查看是否正确安装:

1
$ vue --version

已安装完成会显示Vue的版本信息,比如:3.4.0。

创建Vue项目

创建一个存储项目的目录文件夹(VueProject–目录名,可自行定义)并进入该目录:

1
2
$ mkdir VueProject
$ cd VueProject

输入如下命令初始化项目(hello-world为项目名,可
自行定义,当然,该项目名最终也会生成一个文件夹,内部为项目相关文件):

1
$ vue create hello-world

会出现一个选择项:Please pick a preset:,直接回车默认default(babel,eslint)安装项即可。等待一分钟左右,项目即构建完成。整个项目大概113.3M左右,不用担心,多是一些开发环境需要用到的依赖包,将来发布到线上的包一般只有几兆而已。

目录结构:
Alt text

随便翻翻,了解一个Vue项目的基本目录结构,package.json里面是项目依赖包的信息及版本号。

最后一步,进入我们刚刚创建的项目,启动服务器编译运行:

1
2
$ cd hello-world
$ npm run serve

启动完毕,会给出访问地址提示,如:

1
2
3
App running at:
- Local: http://localhost:8080/
- Network: http://192.168.1.4:8080/

打开浏览器,分别输入本地地址(http://localhost:8080/ )或网络地址(http://192.168.1.4:8080/)皆可访问。

至此,一个极简的Vue项目搭建完成,Vue CLI是一个基于插件的体系结构,使得它非常灵活且可拓展,可以根据自己的需要安装对应插件。接下来就是更深入的需求定制,可通读Vue CLI的官方文档,熟悉更多项目搭建的细节,不要急,一步一步来,链接如下:
Vue CLI(英)

Vue代码规范 -- 基础篇[译]

发表于 2019-02-04

这是Vue具体代码的官方风格指南。如果你在项目中使用Vue,为了避免错误,小纠结,和反模式,这是一份很好的指导。然而,我们不相信任何风格指导对所有团队和项目是理想的,所以我们鼓励基于过去的经验,围绕着技术栈和个人的价值观做有意义的的偏差。

很大程度上,我们也总体上避免就JavaScript或HTML提出建议。我们不介意你使用分号还是逗号。我们不介意你对于HTML的属性值使用单引号还是双引号。一些特例除外,比如,我们发现某种模式在Vue的上下文是有用的。

很快,我们也将提供一些强制执行的建议。你将不得不尊重规则,但只要有可能,我们将尽可能向你展示如何使用语法检查和其他自动处理使强制行为变得简单。

最后,我们使用四类来区分我们的规则:

规则类别

优先级A:必要的

这些规则将帮助你避免错误,所以要不惜一切代价的学习和遵守他们。例外可能存在,但非常少,只有你同时精通JavaScript和Vue的专业知识才会这样做。

优先级B:强烈建议

这些规则可以提供可读性或为大多数项目的开发者经验。如果违反它们你的代码仍然可以运行,但违反它们比较少见且有正当的理由。

优先级C:被建议

当有多个同样好的选择存在,任意选择以确保一致性。在这些规则里,我们描述每一个可以接收的选项和建议一个默认选择。那意味着在你的代码里作出不同的选择会感到自由,一旦你始终如一并且有一个好的理由。请有一个好的理由去写代码!并且遵守社区标准,你将:

  1. 你可以训练你的大脑更容易的解析社区里的代码
  2. 可以复制和粘贴大多数社区代码例子而不需要修改
  3. 常常发现新的员工已经习惯了你喜欢的代码风格,至少在Vue方面是如此

优先级D: 小心使用

Vue存在的一些特性是去适应边缘情况或从历史代码库平滑合并。然而当使用不当的时候,他们可能使你的代码维护困难甚至变成bug的来源。这些规则揭露了潜在的危险性,描述了他们什么时候以及为什么应该避免。

优先级A级规则:必要的(预防错误)

多个字的组件名

组件名应该总是多个词,除了根App组件。

这个阻止冲突是存在于现在或未来的HTML元素中,由于所有的HTML元素是单个词。

1
2
3
4
5
6
7
8
Bad
Vue.component('todo',{
// ...
})
export default {
name: 'Todo',
// ...
}

1
2
3
4
5
6
7
8
Good
Vue.component('todo-item',{
// ...
})
export default {
name: 'TodoItem',
// ...
}

组件数据

组件data必须是一个函数。
当在一个组件使用data属性(任何地方除了new Vue),值必须是一个返回一个对象的函数。

详细解释
当data的值是一个对象的时候,它在组件的所有实例之间共享。想象一下,例如,一个携带data的TodoList组件:

1
2
3
4
data: {
listTitle: '',
todos: []
}

我们可能想要复用这个组件,允许用户去操作多个lists(例如:购物,愿望清单,日常家务,等等)。然而有一个问题。由于每一个组件的实例引用了相同的data对象,改变一个列表的标题也会改变其他列表的标题。添加/编辑/删除操作也是如此。

替代方案是,我们想要每一个组件实例单独管理自己的data。为了实现这一点,每一个实例必须生成一个唯一的data对象。在JavaScript,这能够通过在函数中返回一个对象实现:

1
2
3
4
5
6
data: function ( ) {
return {
listTitle: ' ',
todos: [ ]
}
}

Bad

1
2
3
4
5
Vue.component('some-comp', {
data: {
foo: 'bar'
}
})

1
2
3
4
export default {
data: {
foo: bar
}

Good

1
2
3
4
5
6
7
 Vue.component('some-comp', {
data: function () {
return {
foo: 'bar'
}
}
})

1
2
3
4
5
6
7
8
// In a .vue file
export default {
data () {
return {
foo: 'bar'
}
}
}
1
2
3
4
5
6
// 在一个根Vue实例直接使用一个对象是可以的,因为一个单一的实例将永远存在
new Vue({
data: {
foo: 'bar'
}
})

Prop 定义

Prop定义应该尽可能的详细。
在提交的代码中,prop定义应该尽可能的详细,至少指定类型。

详细解释
详细的prop 定义有两个优势:

  • 他们记录组件的API,这样就很容易看出组件是如何被使用的。
  • 在开发中,如果一个组件的格式不正确,Vue将警告你,帮助你追踪潜在的错误来源。

Bad

1
2
// 只有当原型化的时候是可以的
props:['status']

Good

1
2
3
props: {
status: String
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 甚至更好
props:{
status: {
type: String,
required: true,
validator: function (value) {
return [
'syncing',
'synced',
'version-conflict',
'error'
].indexOf(value) !== -1
}
}
}

有键的v-for

总是使用带key的v-for。
在组件上使用带key的v-for总是有效的,以便维护内部组件及子组件的状态。即使对元素来说,保持可预测的行为也是很好的实践,比如动画中的对象固化(object constancy)。

详解解释
假如你有一个代办事项列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
data: function () {
return {
todos: [
{
id: 1,
text: 'Learn to use v-for'
},
{
id: 2,
text: 'Learn to use key'
}
]
}
}

然后按字母顺序。当更新DOM时,我们将优化渲染尽可能的降低DOM改变的消耗。那可能意味着删除第一个代办元素,然后在列表的末尾再增加一个。

问题是,在这些例子中,即:更新了元素又不删除元素是重要的。例如,你可能使用<transition-group>动态的排序列表。或者渲染的元素是一个<input>维持焦点。在这些例子中,对于每一个item添加唯一的key(e.g. :key="todo.id")将告诉Vue怎么样的行为更智能。

在我们的经验当中,总是增加一个唯一的key总是更好的,你和你的团队根本不用担心边缘问题。然后在罕见的情况下,在罕见的、性能关键的场景中,对象的稳定性是不必要的,你可以有意识的破例。


Bad

1
2
3
4
5
<ul>
<li v-for="todo in todos">
{{ todo.text }}
</li>
</ul>

Good

1
2
3
4
5
6
7
<ul>
<li
v-for="todo in todos"
:key="todo.id">
{{ todo.text }}
</li>
</ul>

避免在v-for中使用v-if

不要在与v-for相同的元素上使用v-if。
在两种情况下这可能是诱人的:

  • 在列表中过滤items(e.g. v-for="user in users" v-if="user.isActive")。在这些例子中,使用一个新的计算属性替换users,并返回过滤后的list(例如:activeUsers)。
  • 避免在应该隐藏列表时渲染列表(eg:v-for="user in users" v-if="shouldShowUsers").在这些例子中,移动v-if到容器元素(e.g. ul, ol)。

详细解释
当Vue处理指令的时候,v-for比v-if有更高的优先级,所以这个模板:

1
2
3
4
5
6
7
8
<ul>
<li
v-for="user in users"
v-if="user.isActive"
:key="user.id">
{{ user.name }}
</li>
</ul>

将被评估类似于:

1
2
3
4
5
this.users.map(function (user) {
if (user.isActive) {
return user.name
}
})

因此即使我们仅仅为一小部分用户渲染元素,我们每次重新渲染的时候我们不得不迭代整个列表,去判断是否设置的活跃用户已经改变。

通过迭代计算属性代替,像这样:

1
2
3
4
5
6
7
computed: {
activeUsers: function () {
return this.users.filter( function (user) {
return user.isActive
})
}
}
1
2
3
4
5
6
7
<ul>
<li
v-for="user in activeUsers"
:key="user.id">
{{ user.name }}
</li>
</ul>

我们将得到以下好处:

  • 如果users数组有改变,仅仅被过滤的列表重新计算,使过滤更高效。
  • 使用v-for="user in activeUsers",在渲染期间我们仅仅迭代被激活的元素,使渲染更高效。
  • 逻辑在表现层解耦,使维护(逻辑的改变或拓展)变得更容易。

我们从更新中得到类似的好处:

1
2
3
4
5
6
7
8
<ul>
<li
v-for="user in users"
v-if="shouldShowUsers"
:key="user.id">
{{ user.name }}
</li>
</ul>

到:

1
2
3
4
5
6
<ul v-if="shouldShowUsers">
<li
v-for="user in users"
:key="user.id">
</li>
</ul>

通过移动v-if给一个容器元素,我们在每一个用户列表将不再检查shouldShowUsers。作为替代,我们检查一次,并且如果shouldShowUsers是false我们将不再计算。


Bad

1
2
3
4
5
6
7
8
<ul>
<li
v-for="user in users"
v-if="user.isActive"
:key="user.id">
{{ user.name }}
</li>
</ul>

1
2
3
4
5
6
7
8
<ul>
<li
v-for="user in users"
v-if="shouldShowUsers"
:key="user.id">
{{ user.name }}
</li>
</ul>

Good

1
2
3
4
5
6
7
<ul>
<li
v-for="user in activeUsers"
:key="user.id">
{{ user.name }}
</li>
</ul>

1
2
3
4
5
6
7
<ul v-if="shouldShowUsers">
<li
v-for="user in users"
:key="user.id">
{{ user.name }}
</li>
</ul>

组件样式作用域

对于应用,一个顶层的App组件与布局组件的样式可能是全局的,但其他组件应用有自己的作用域。
这条仅仅与单文件组件有关。scoped特性的使用不是必须的,设置作用域可以通过CSS modules实现。一个基于类的类似于BEM的策略,或其他库/惯例。

然而,组件库应该更倾向于使用基于类的策略而不是作用域属性。

这使你复写内部样式更容易,使用没有很高的差异性的可读性强的类名,而且不太会导致冲突。

详细解释
如果你正在开发一个大项目,并且和其他的开发者合作,或有时包含第三方HTML/CSS(例如:来自Auth0),设置一致的样式能够保证你的样式只适用于他们想要的组件。

在scoped特性之上,使用唯一的class名称能够确保第三方CSS不会应用于你自己的HTML。例如,许多项目使用button,btnor icon类名,所以即使不使用类似于BEM的策略。添加特定应用或特定组件的前缀(例如:ButtonClose-icon)也能启到一些保护作用。


Bad

1
2
3
4
5
6
7
8
9
<template>
<button class="btn btn-close">X</button>
</template>

<style>
.btn-close {
background-color: red;
}
</style>

Good

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<button class="button button-close">X</button>
</template>

<! -- Using the `scoped` attribute -- >
<style scoped>
.button {
border: none;
border-radius: 2px;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<button :class="[$style.button, $style.buttonClose]">X</button>
</template>

<! -- 使用CSS模块 -- >
<style module>
.button {
border: none;
border-radius: 2px;
}

.buttonClose {
background-color: red;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<button class="c-Button--close">X</button>
</template>

<! -- 使用BEM惯例 -- >
<style>
.c-Button {
border: none;
border-radius: 2px;
}
.c-Button-close {
background-color: red;
}
</style>

私有属性名字

在一个插件,混入等,对于自定义属性总是使用$_前缀。然后避免和其它作者的代码冲突,也包含一个被命名的范围(例如:$_yourPluginName_)。

详细解释
Vue使用下划线_前缀去定义自己的私有属性,所以使用相同前缀(例如:_update)的风险可能会覆写一个实例属性。即使你检查了Vue当前没有使用一个特定的属性名,并不能保证在以后的升级中没有冲突。

正如$前缀,它在Vue生态系统中的目的是向用户公开的特殊的实例属性,所以在私有属性里使用它并不合适。

替代的,我们推荐把这两个前缀联合起来$_,作为一个用户自定义的私有属性不和Vue冲突的习惯用法。


Bad

1
2
3
4
5
6
7
8
var myGreatMixin = {
// ...
methods: {
update: function () {
// ...
}
}
}

1
2
3
4
5
6
7
8
9
10
var myGreatMixin = {
// ...
methods: {
methods: {
_update: function () {
// ...
}
}
}
}
1
2
3
4
5
6
7
8
var myGreatMixin = {
// ...
methods: {
$update: function () {
// ...
}
}
}
1
2
3
4
5
6
7
8
var myGreatMixin = {
// ...
methods: {
$_update: function () {
// ...
}
}
}

Good

1
2
3
4
5
6
7
8
var myGreatMixin = {
// ...
methods: {
$_myGreatMixin_update: function () {
// ...
}
}
}


原文:Style Guide for Vue(英)

123
Lorne Zhang

Lorne Zhang

日拱一卒

25 日志
© 2019 - 2022 Lorne Zhang
由 Hexo 强力驱动
主题 - NexT.Muse