第三阶段培训
10月上旬的时候,对公司新进的一批实习生组织了培训。这是其中的一个阶段的教案内容。
内容:
- 项目搭建
- 项目创建
- 项目配置
- 提升开发质量和开发幸福度的 vue 功能
- directives
- transition (实现简单的列表过渡效果)
- plugin (实现一个表单校验的 plugin)
- git
- 工作流
- git flow
- github flow
- gitlab flow
- 工作流
- vue 原理
- 响应式
- 虚拟 dom
- vue 3
- vite
- composition API
- vue 生态周边推荐
- ui 框架
- css reset
- nuxt
- demo 上手
1. 项目搭建
1.1. 创建
vue create a-new-demo
在Please pick a preset,选择Manually select features
? Check the features needed for your project:
>(*) Babel
( ) TypeScript
( ) Progressive Web App (PWA) Support
(*) Router
(*) Vuex
(*) CSS Pre-processors
(*) Linter / Formatter
(*) Unit Testing # 单元测试,目前用到的不多,这里做一个了解
( ) E2E Testing
👇
? Check the features needed for your project: Babel, Router, Vuex, CSS Pre-processors, Linter, Unit
👇
? Use history mode for router? (Requires proper server setup for index fallback in production) (Y/n) y
👇
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default):
Sass/SCSS (with dart-sass)
> Sass/SCSS (with node-sass) # dart在web的某些场景下性能不如node,具体可以搜搜
Less
Stylus
👇
? Pick a linter / formatter config:
ESLint with error prevention only
ESLint + Airbnb config
ESLint + Standard config
> ESLint + Prettier # eslint结合prettier,规范参考大礼包里面的文档
👇
? Pick additional lint features:
>(*) Lint on save
(*) Lint and fix on commit # 代码符合规范才给提交
👇
? Pick a unit testing solution:
Mocha + Chai
> Jest # 使用jest作为单元测试工具
👇
? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys)
> In dedicated config files
In package.json
进入项目之后,使用vue add安装插件
vue add element
👇
? How do you want to import Element?
Fully import
> Import on demand # 把全局引入抠掉
👇
vue add axios
👇
vue add lodash # 先装上吧,好用
增加.editorconfig 文件
root = true
[*]
charset = utf-8
end_of_line = crlf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
增加.prettierrc 文件
{
"semi": false, // 语句结尾不写 ;
"singleQuote": true, // 使用 '' 不使用 ""
"jsxBracketSameLine": true
}
增加 eslint 规则
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
indent: ['error', 2], // 缩进:2个空格
eqeqeq: 'warn', // 相等运算符:强制使用===
'space-before-function-paren': ['error', 'never'], // 函数名与()之间的空格:不允许
'no-mixed-spaces-and-tabs': 'error', // 对象文字的大括号内间距:强制在{}两侧留空格
'object-curly-spacing': ['error', 'always'], // 生产环境中不允许console语句和debugger语句
camelcase: 'warn'
},
针对初始化项目时出现的样式问题,使用npm run lint解决
最后: npm install
思考:如何解决给多个项目进行重复配置的机械劳动问题?
设置脚本,自动化为项目进行相关配置(
bat、bash或node)
- editorconfig
- prettierrc
- eslint
- babel
1.2. 配置
项目结构
src
├─api # api
├─assets # 静态资源 css/image/font
├─components # 组件
├─libs # 公共方法
├─plugins # 插件
├─router # 路由
| └─routes # 路由组件
├─store # 状态
| └─modules # 模块
└─views # 视图 (与路由结构保持一致)
1.2.1. router
1.2.1.1. 导航守卫
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
else next()
})
- 未登录状态和登录状态的路由权限
- 不同账号的路由权限
- 配置页面跳转时的 loading bar
不同账号的路由权限的配置可以采取两种方法
- 使用
router.addRoutes(routes: Array<RouteConfig>),将后端回传的权限信息添加到路由配置种- 使用
router.beforeEach中的to,对比权限信息中是否有该路由的权限
1.2.2. vuex
- 统一使用 action 触发 axios 请求获取后端数据 (特殊情况除外,如文件下载 api 等特殊 api)
1.2.3. axios
1.2.3.1. 请求拦截器
1. 阻止请求重复发送
1. 当请求发起时,在一个array中缓存这个请求
2. 如果请求完成,清除该缓存
3. 当一个新请求发起时,如果缓存中有相同的请求,则阻止这个新请求
const _axios = axios.create(config)
let reqList = [] // * 存放正在发送的请求
// 请求
_axios.interceptors.request.use(
function (config) {
// * 阻止请求重复发送
let _cancel
config.cancelToken = new axios.CancelToken(function (cancel) {
_cancel = cancel
})
if (reqList.indexOf(config.url) === -1) {
reqList.push(config.url)
} else {
_cancel('opps...')
}
return config
},
function (error) {
// Do something with request error
return Promise.reject(error)
}
)
// 响应
_axios.interceptors.response.use(
function (response) {
// Do something with response data
reqList.splice(reqList.indexOf(config.url), 1)
return response
},
function (error) {
// Do something with response error
return Promise.reject(error)
}
)
1.2.3.2. 响应拦截器
1. 状态码处理
_axios.interceptors.response.use(
function (response) {
// Do something with response data
return response
},
function (error) {
// Do something with response error
try {
if (error.response.status === 400) {
Message.error('请求参数错误')
}
if (error.response.status === 401) {
Message.error('登录超时/未登录')
router.push({ name: 'Login' })
}
if (error.response.status === 403) {
Message.error('请求被远程服务禁止')
}
if (error.response.status === 405) {
Message.error('请求方法不能获取指定资源')
}
if (error.response.status === (422 || 421)) {
Message.error('当前请求链接超负荷,请稍后再试')
}
if (error.response.status === 500) {
Message.error('服务器端错误')
}
} catch (err) {
Message.error(err.toString())
}
return Promise.reject(error)
}
)
2. 提升开发质量和开发幸福度的 vue 功能
2.1. directive
作用:多用于组件
- 可以拿到 DOM 元素,直接对 DOM 进行操作
- 传递参数
基础用法
directives: {
yourDir: {
bind: function (el) {
// 只调用一次,一般用于初始化
},
inserted: function (el) {
// 被绑定元素插入父节点中,一般来讲这个钩子下元素已被渲染
},
update: function (el) {
// 触发更新时调用,能发生在其子 VNode 更新之前
},
componentUpdated: function (el) {
// 所在组件的 VNode 及其子 VNode 全部更新后调用
},
unbind: function (el) {
// 解绑时调用
}
}
}
钩子函数的参数
- el DOM 元素
- binding 包含指令信息
- vnode 当前 virtual DOM 节点
- oldVnode 更新前的 virtual DOM 节点
binding 内部
- name:指令名,不包括 v- 前缀。
- value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。
- oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
- expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。
- arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。
- modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。
VNode 的内容可以参考snabbdom
2.2. provide 和 inject
在父组件中使用provide配置项为后代组件提供data和method,在子组件中使用inject注入依赖
不过一般来讲,要谨慎使用这种依赖注入的方式,这会使得父子组件的耦合加深,增加维护成本。
// 父组件
export default {
name: 'parentComp',
data() {
return {
age: 18
}
},
methods: {
getAge() {
console.log(this.age)
}
},
provide() {
return {
age: this.age,
getAge: this.getAge
}
}
}
// 子组件
export default {
name: 'childComp',
inject: ['getAge']
}
2.3. 渲染函数
template 模板在构建前会编译为 render 函数,因此,render 函数与编译后的 vue 代码关系更近,帮助我们更加深入的了解 vue 的视图渲染机制。vue 2 的虚拟 DOM 来源于snabbdom。
- 一般来讲优先使用template,这样利于视图代码与逻辑代码的解耦和维护
- 但是某些情况,使用
render函数会更加的简洁优雅- 比如现在有一个需求,页面渲染
<h1>到<h6>标签- 需求发生了一些变化👉每级
h标签里面的数字加1/只渲染奇数层级/奇偶层级内部元素不同
<h1>1</h1>
<h2>2</h2>
<h3>3</h3>
<h4>4</h4>
<h5>5</h5>
<h6>6</h6>
new Vue({
render(h) {
return h(
'div',
[1, 2, 3, 4, 5, 6].map(function (item) {
return h(`h${item}`, item)
})
)
},
}).$mount('#app')
2.3.1. 用法
2.3.2. 案例
2.4. keep-alive
用于保留组件状态或避免重新渲染
- 在多标签页场景下较为实用
include包括xx组件exclude排除xx组件max最大缓存组件数量
<keep-alive include="a,b" :exclude="/c|d/" :max="10">
<router-view></router-view>
</keep-alive>
2.5. vue 过渡、动画
在vue3小demo中简单说明
2.6 跨组件通信
一个使用发布订阅者模式的简单尝试
// 组件 pubsub.js
import Vue from 'vue'
const vm = new Vue()
// 发布
export function pub(eventName) {
vm.$emit(eventName, ...Array.from(arguments).slice(1))
}
// 订阅
export function sub(eventName, callback) {
vm.$on(eventName, callback)
}
// 订阅 once
export function subOnce(eventName, callback) {
vm.$once(eventName, callback)
}
// 发布的组件
import { pub } from 'pubsub'
export default {
methods: {
foo() {
pub('someEvent', 1, 2, 3)
}
}
}
// 订阅的组件
import { sub, subOnce } from 'pubsub'
export default {
methods: {
bar() {
// ...
}
},
created() {
sub('someEvent', bar)
}
}
2.7 v-slot
2.6版本新增新增指令,替代了slot、slot-scope和scope
<base-layout>
<template v-slot:header>
具名插槽 header
</template>
default slot的内容
<template v-slot:footer="slotProps">
具名插槽 footer
接收参数 slotProps
{{ slotProps.name }}
{{ slotProps.age }}
</template>
</base-layout>
3. 基本原理
3.1. 响应式
- vue 2 👉 defineProperty
- vue 3 👉 proxy
const vm = {}
const data = {
name: '',
age: 0
}
// vue 2
function reactivity2() {
Object.keys(data).forEach(key => {
Object.defineProperty(vm, key, {
// 可枚举
enumerable: true,
// 可配置 (使用delete删除, 通过defineProperty重新定义)
configurable: true,
get () {
console.log('get: ', data[key])
return data[key]
},
set (newValue) {
console.log('set: ', newValue)
if (newValue === data[key]) {
return
}
data[key] = newValue
}
})
})
}
// vue 3
function reactivity3(data) {
vm = new Proxy(data, {
get (target, key) {
console.log('get: ', key, target[key])
return target[key]
},
set (target, key, newValue) {
console.log('set: ', key, newValue)
if (target[key] === newValue) {
return
}
target[key] = newValue
return target[key]
}
})
}
3.2. vitural dom
4. 插件
4.1. 方法
作用
- 添加全局方法或者 property。如:vue-custom-element
- 添加全局资源:指令/过滤器/过渡等。如 vue-touch
- 通过全局混入来添加一些组件选项。如 vue-router
- 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。
- 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router
注册插件
在Vue实例初始化之前调用
// 调用 `MyPlugin.install(Vue)`
Vue.use(MyPlugin, options)
new Vue({
// ...组件选项
})
// ✨ install 方法
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或 property
Vue.myGlobalMethod = function () {
// 逻辑...
}
// 2. 添加全局资源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 逻辑...
}
...
})
// 3. 注入组件选项
Vue.mixin({
created: function () {
// 逻辑...
}
...
})
// 4. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 逻辑...
}
}
4.2. 实现一个简易的表单校验插件
4.2.1. 思路
// vue 组件
export default {
data: {
username: '',
password: ''
},
// 👇在这里添加校验规则
rules: {
username: {
validate: value => {
/^[A-z]{6, 10}$/.test(value)
},
message: '请填写用户名',
require: true,
trigger: 'blur'
},
password: {
validate: value => {
/^[A-z0-9\s]{8, 20}$/.test(value)
},
message: '请填写用户名'
require: true,
trigger: 'input'
}
}
}
4.2.2. 实现 (一个不太好的实现方法)
// validate.js
import Vue from 'vue'
const RulesPlugin = {
install (Vue) {
Vue.mixin({
created () {
if (this.$options.hasOwnProperty('rules')) {
const rules = this.$options.rules
Object.keys(rules).forEach(key => {
const rule = rules[key]
this.$watch(key, newValue => {
const result = rule.validate(newValue)
if (!result) {
console.log(rule.message)
}
})
})
}
}
})
}
}
Vue.use(RulesPlugin)
5. Vue 3
Vite is an opinionated web dev build tool that serves your code via native ES Module imports during dev and bundles it with Rollup for production.
- Lightning-fast cold server start
- Instant hot module replacement (HMR)
- True on-demand compilation
- More details in How and Why
5.1. 搭建项目
# 初始化项目
npm init vite-app vue3-composition-api-demo
# 进入目录
cd vue3-composition-api-demo
# 安装依赖
npm install
# 运行项目
npm run dev
引入第三方库
npm install lodash
yarn add lodash
目前社区中大部分模块都没有设置默认导出
esm,基本上还都是cjs。所以直接引入lodash会报错
// vite.config.js
module.exports = {
optimizeDeps: {
include: ["lodash"]
}
}
// 引入lodash
import lodash from 'lodash'
// 下面的引入方式会报错
import { random, shuffle } from 'lodash'
import random from 'lodash/random'
import shuffle from 'lodash/shuffle'
5.2. 尝试Composition API
Composition API在Vue 3中开箱即用,如果想在Vue 2中使用Composition API,可以在项目中添加@vue/composition-api插件
- demo使用了两个data变量capacity和attending,一个computed属性spacesLeft和几个相关的函数。
<template>
<div>
<p>Spaces left: {{ spacesLeft }} out of {{ capacity }}</p>
<h2>Attending</h2>
<div>
<button @click="increaseCapacity">Increase Capacity</button>
</div>
<div>
<button @click="addAttending">addAttending</button>
</div>
<div>
<button @click="sort">sort</button>
</div>
<!-- 使用vue的过渡动画组件演示 -->
<transition-group name="flip-list" tag="ul" :style="{width: '200px', margin: '20px auto'}">
<li v-for="item in attending" :key="`Attending${item}`">
{{ item }}
</li>
</transition-group>
</div>
</template>
写法一
// 引入ref和computed
import { ref, computed } from "vue"
export default {
setup() {
const capacity = ref(4)
const attending = ref(["Tim", "Bob", "Joe"])
// computed计算属性的声明方式
const spacesLeft = computed(() => {
// 注意,通过ref声明的响应式数据需要通过.value拿到该值
return capacity.value - attending.value.length
})
// 代替methods声明方法
function increaseCapacity () {
capacity.value++
}
function sort () {
attending.value = lodash.shuffle(attending.value)
}
function addAttending () {
attending.value.push(randomName(lodash.random(3, 7)))
}
// 通过一个对象暴露以上变量和方法
return {
capacity,
attending,
spacesLeft,
increaseCapacity
}
}
}
其中,ref()接收一个value值作为入参,基本类型的入参将被包裹在一个object中(如capacity),通过proxy实现响应式(这很Vue 3)。computed计算属性,是通过computed()方法声明的(全部替换成了函数式的声明),注意computed中,我们需要调用capacity.value和attending.value来获取数据的值。最后,所有在template中使用的变量和方法通过return暴露出去。
写法二
import { reactive, computed, toRefs } from "vue"
export default {
// 将需要使用的变量添加到event的属性中
setup(props, context) {
const event = reactive({
capacity: 4,
attending: ['Tim', 'Bob', 'Joe'],
spacesLeft: computed(() => { return event.capacity - event.attending.length })
})
// 方法的声明方法不变
function increaseCapacity () {
event.capacity++
}
function sort () {
event.attending = lodash.shuffle(event.attending)
}
function addAttending () {
event.attending.push(randomName(lodash.random(3, 7)))
}
// toRefs()
return {...toRefs(event), increaseCapacity, addAttending, sort}
}
}
改变的地方是toRefs()。toRefs()方法接受一个object作为入参,返回一个响应式的object。在setup()函数体内直接通过event属性拿到相关变量值,不需要额外加上.value。两种写法在使用场景上略有区别。我个人更喜欢第二种写法,return更加简洁优雅👍。
5.2.1. setup()
setup(props, context) {
// Attributes (Non-reactive object)
console.log(context.attrs)
// Slots (Non-reactive object)
console.log(context.slots)
// Emit Events (Method)
console.log(context.emit)
}
setup()方法接受2个参数,第一个参数props,顾名思义就是Vue的props。setup()执行时还没有vue实例,所以在setup()函数体内拿不到data、computed和methods,所以需要借助第二个参数context拿到vue实例相关的数据,其中包含attrs、slots和emit。
setup生命周期hooks
对比打印一下新旧两版生命周期hooks:
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onUnmounted
} from "vue"
function printLifeCycle () {
onBeforeMount(() => {console.log('onBeforeMount')})
onBeforeUpdate(() => {console.log('onBeforeUpdate')})
onMounted(() => {console.log('onMounted')})
onUpdated(() => {console.log('onUpdated')})
onUnmounted(() => {console.log('onUnmounted')})
}
export default {
// setup内部生命周期hooks
setup(props, context) {
printLifeCycle()
},
// 老生命周期hooks
beforeCreate() {
console.log('beforeCreate')
},
created() {
console.log('created')
},
beforeMount() {
console.log('beforeMount')
},
mounted() {
console.log('mounted')
},
beforeUpdate() {
console.log('beforeUpdate')
},
updated() {
console.log('updated')
}
}
setup()内部的hooks执行早于vue实例上的老hooks。
5.3. vue过渡
- Vue 2
- Vue 3
注意!vue 3使用v-enter-from 而不是 v-enter
.flip-list-move {
transition: transform 1s;
}
.flip-list-enter-active, .flip-list-leave-active {
transition: all 1s;
}
.flip-list-enter-from, .flip-list-leave-to
/* .list-leave-active for below version 2.1.8 */ {
opacity: 0;
transform: translateY(30px);
}
6. Git
推荐实践方法
- 在gitlab中,项目组group中,创建项目主仓库
- 主仓库有
master和develop两个分支 - 假设主仓库地址为
git@github.com:group/test-project.git
- 主仓库有
- 在gitlab中将主仓库
fork到个人仓库- Fork 出来的仓库完全属于你自己,你可以任意修改该仓库的代码及配置,但是除非你向项目主仓库提交 pull request,并且被接受通过,你才可以将你fork 仓库修改的代码合并到主仓库,否则不会对主仓库产生任何影响。
- 此时,个人的仓库地址为
git@github.com:person/test-project.git
clone个人仓库到本地,并且在remote配置中upstreamupstream指向远程主仓库地址origin指向远程个人仓库地址
git remote add upstream git@github.com:group/test-project.git # 执行 git remote -v 可以看到remote中有origin和upstream git remote -v- 保持个人仓库和
upstream同步master和develop都要同步
# 同步upstream到本地 git fetch upstream git checkout master git merge upstream/master # 本地与远程个人仓库同步 git pull origin master - 开发
- 开发新功能
develop上新建feature/new-feature临时 分支 - bug修复
master上新建hotfix/new-bug临时 分支
# 新功能 git checkout -b feature/new-feature develop # 新bug git checkout -b hotfix/new-bug master - 开发新功能
- 提交代码到个人远程仓库
- 开发完成后,提交代码前,检查本地
feature分支是否落后于upstream的develop分支(在你提交代码前,可能有其他的PR合并到了upstream的develop分支)
git checkout develop git pull upstream develop # 检查 git log feature/new-feature..develop # 假设控制台有输出(如下),说明本地feature分支落后于upstream的develop分支 commit 58fae9f9355a820da713e830b0b4c8100bcb6dab (upstream/develop, develop) Author: overlyDramatic <maxjunedown@live.com> Date: Sun Sep 27 15:13:42 2020 +0800 code2- 如果落后了,则进行以下操作
git checkout feature/new-feature # 第一种使用rebase 更方便但是更加危险❗ git rebase develop # 第二种使用merge 更多的操作空间(--no-ff指禁止fast forward) git merge develop --no-ff --edit- 之后就可以提交到远程个人仓库分支啦💪
git push origin HEAD # 或者 git push origin feature/new-feature - 开发完成后,提交代码前,检查本地
- 发起 pull request 或 merge request
- github 👉 pull request
- gitlab 👉 merge request
- 在gitlab.hedongli.com/的个人仓库页面的左边菜单点击
merge request(👋记住是个人仓库) - 点击绿色按钮
new merge request - 左边的
Source branch选择feature/new-feature分支 - 右边的
Target branch选择主仓库的develop分支 (👋记住是主仓库) - 之后点击assignee选择你的名字
- 点击发布你的
PR
- 其他人查看你的
PR- 在当前
PR页面点击check out branck按钮 - 按照步骤在本地检出该
PR - 如果需要修改,则执行2,3,4步(本人修改则在他本地进行)
- 如果测试通过,就点击merge按钮或者执行上面的两步
- 在当前