在使用 keep-alive 做页面缓存时,要求路由名与页面名保持一致,我们可以通过 nodejs 来检查路由名和组件是否一致

脚本代码

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
91
92
93
94
95
96
97
98
99
const fs = require('fs')
const path = require('path')

const paths = {}

function getStat(dir, resolve, reject) {
fs.stat(path.resolve(dir), (eror, stats) => {
if (eror) {
// 防止死循环
if (dir.split('.vue').length > 2) return

typeof reject === 'function' && reject()
return
}
typeof resolve === 'function' && resolve(stats.isFile(), stats.isDirectory())
})
}

// 检测路由名与组件名是否一致
function checkName(dir, routeName) {
getStat(
dir,
(isFile, isDir) => {
if (isFile) {
const content = fs.readFileSync(path.resolve(dir), 'utf-8')

content.replace(/export default {.*\W*.*name: ['"](.+)['"].*/m, (str, $1) => {
if ($1 !== routeName) {
console.warn(`${dir} 路由名 ${routeName} 与组件名 ${$1} 不一致`)
}

// 直接替换为空字符串,避免重复匹配
return ''
})
} else if (isDir) {
checkName(dir + '/index.vue', routeName)
}
},
() => {
checkName(dir + '.vue', routeName)
}
)
}

function getRouteNameInReplace($1, $2, paths) {
if (!$1 || !$2) return
if (paths[$1]) {
console.error(`存在同名路由: ${$1}, ${paths[$1]}, ${$2}`)
} else {
const dir = $2.replace('@', './src')
paths[$1] = dir
checkName(dir, $1)
}
}

// 文件遍历方法
function fileDisplay(filePath) {
// 根据文件路径读取文件,返回文件列表
fs.readdir(filePath, (err, files) => {
if (err) {
console.warn(err)
} else {
// 遍历读取到的文件列表
files.forEach(filename => {
// 获取当前文件的绝对路径
const filedir = path.join(filePath, filename)
// 根据文件路径获取文件信息,返回一个fs.Stats对象
getStat(filedir, (isFile, isDir) => {
if (isFile) {
// 读取文件内容
const content = fs.readFileSync(filedir, 'utf-8')

content
// 路由表中 name 在 component 前面
.replace(/name: ['"](.+)['"].*\W*.*component:.*\W*.*import\(.*\W*.*['"](.+)['"].*\W*.*\)/gm, (str, $1, $2) => {
getRouteNameInReplace($1, $2, paths)

// 直接替换为空字符串,避免重复匹配
return ''
})
// 路由表中 name 在 component 后面
.replace(/component:.*\W*.*import\(.*\W*.*['"](.+)['"].*\W*.*\).*\W*.*name: ['"](.+)['"]/gm, (str, $1, $2) => {
getRouteNameInReplace($2, $1, paths)

// 直接替换为空字符串,避免重复匹配
return ''
})
} else if (isDir) {
// 递归,如果是文件夹,就继续遍历该文件夹下面的文件
fileDisplay(filedir)
}
})
})
}
})
}

// 调用文件遍历方法
fileDisplay(path.resolve('./src/router'))

用法

将上述脚本保存在项目目录中,比如 /routeNameChecker.js,直接执行 node ./routeNameChecker.js 即可,若存在不同会直接打印在终端

也可以在 package.js 中加一条 script "check": "node .\\routeNameChecker.js", 就可以直接执行 npm run check

为了在异步组件加载过程中显示加载中状态,引入了 LazyLoadView,但这种方案直接导致了组件内的导航守卫 beforeRouteEnter/beforeRouteLeave 失效,下面就是该问题的一种尝试解决方案:

创建 eventbus

1
2
3
4
5
6
// src/utils/eventbus.js
import Vue from 'vue'

const eventbus = new Vue()

export default eventbus

为了方便使用,我们将 eventbus 注册在 Vue 的原型上

1
2
3
4
// src/main.js
import Vue from 'vue'
import eventbus from '@/utils/eventbus'
Vue.prototype.$eventbus = eventbus

自定义导航守卫(也就是自定义事件)

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
// src/routeGuard.js

/**
* 由于 lazy-load-view 的影响导致组件内导航守卫不触发,在此通过 eventbus 实现自定义导航守卫
* @author shawchen08
*/

import eventbus from '@/utils/eventbus'

// Note: 在需要监听 beforeRouteEnter 的组件中,需要在 created 中执行
// eventbus.$emit(`before${route}Enter`, to, from) 原因在于 afterEach 先于组件的生命周期钩子
// 执行,那么首次进入就不会未执行 beforeRouteEnter 的回调逻辑,因此需手动触发一次

// 需要 beforeRouteEnter 的路由
const routeEnterList = ['RouteA', 'RouteB']
const beforeRouteEnter = {}

routeEnterList.forEach(route => {
beforeRouteEnter[route] = (to, from) => {
eventbus.$emit(`before${route}Enter`, to, from)
}
})

// 需要 beforeRouteLeave 的路由
const routeLeaveList = ['RouteC', 'RouteD', 'RouteE']
const beforeRouteLeave = {}

routeLeaveList.forEach(route => {
beforeRouteLeave[route] = (to, from, next) => {
eventbus.$emit(`before${route}Leave`, to, from, next)
}
})

export { beforeRouteEnter, beforeRouteLeave }

我们把需要使用组件内导航守卫 beforeRouteEnterbeforeRouteLeave 的组件名(路由名与组件名一致可以通过此脚本检测)分别添加到上面文件对应的数组 routeEnterListrouteLeaveList

通过全局守卫触发事件,模拟组件内导航守卫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/permission.js
import Vue from 'vue'
import router from './router'
import { beforeRouteEnter, beforeRouteLeave } from '@/routeGuard'
// ...

// ! Note: 由于 beforeEach 中有很多权限控制逻辑,
// ! 为了简单处理,我们在 beforeResolve 触发自定义 beforeRouteLeave 事件
router.beforeResolve((to, from, next) => {
if (beforeRouteLeave[from.name]) {
return beforeRouteLeave[from.name](to, from, next)
}
next()
})

router.afterEach((to, from) => {
// 记录 to 和 from,组件内需要使用
Vue.prototype.$beforeRouteEnterParams = { to, from }
if (beforeRouteEnter[to.name]) {
beforeRouteEnter[to.name](to, from)
}
// ...
})

组件内部监听相应的事件,模拟组件内导航守卫

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
<script>
// ...

export default {
// ...
created() {
// ! 先存储当前路由名(beforeDestroy 钩子中 this.$route 可能已经变了)
this._name = this.$route.name

// 这些不放在 beforeCreate 中的原因在于 beforeCreate 中还拿不到 methods 中的方法
// 类似 addEventlistioner 和 removeEventListioner,on 和 off 的回调函数必须是同一个
this.$eventbus.$off(`before${this._name}Enter`, this.beforeRouteEnter)
this.$eventbus.$on(`before${this._name}Enter`, this.beforeRouteEnter)
this.$eventbus.$off(`before${this._name}Leave`, this.beforeRouteLeave)
this.$eventbus.$on(`before${this._name}Leave`, this.beforeRouteLeave)

// 由于全局守卫 afterEach 会先于组件的 created 执行,导致第一次未触发
// `before${this.$route.name}Enter` 事件,这里手动触发一下,
// this.$beforeRouteEnterParams 是在全局守卫 afterEach 中记录的 to 和 from
// ! 注意:this.beforeRouteEnter 中有依赖数据初始化的,初始化逻辑需在此之前
this.$eventbus.$emit(`before${this._name}Enter`, this.$beforeRouteEnterParams.to, this.$beforeRouteEnterParams.from)
// ...
},
beforeDestroy() {
// ! 这里不用 this.$route.name 是因为销毁前路由可能已经改变
this.$eventbus.$off(`before${this._name}Enter`, this.beforeRouteEnter)
this.$eventbus.$off(`before${this._name}Leave`, this.beforeRouteLeave)
// ...
},
methods: {
// 模拟的 beforeRouteEnter
beforeRouteEnter(to, from) {
// ...
},
// 模拟的 beforeRouteLeave
beforeRouteLeave(to, from, next) {
// ...
// 记得执行 next()
}
}
// ...
}
</script>

加载状态组件

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
// /xx dir/LazyLoadView.js
export default function LazyLoadView(AsyncView, needLoading = true) {
// @see https://cn.vuejs.org/v2/guide/components-dynamic-async.html#%E5%A4%84%E7%90%86%E5%8A%A0%E8%BD%BD%E7%8A%B6%E6%80%81
const AsyncHandler = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: AsyncView,
// 异步组件加载时使用的组件
loading: needLoading ? require('./LazyLoadLoading').default : null,
// 加载失败时使用的组件
error: needLoading ? require('./LazyLoadError').default : null
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
// delay: 400,
// 如果提供了超时时间且组件加载也超时了,则使用加载失败时使用的组件。默认值是:`Infinity`
// timeout: 10000
})

return Promise.resolve({
functional: true,
render(h, { data, children }) {
// 传递 context 的各种属性及 children 到视图组件.
return h(AsyncHandler, data, children)
}
})
}

// /xx dir/LazyLoadLoading.js
export default {
functional: true,
render(h, context) {
return <div>loading</div>
}
}

// /xx dir/LazyLoadError.js
export default {
functional: true,
render(h, context) {
return <div>error</div>
}
}

启用加载状态组件

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
// /src/router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import LazyLoadView from '/xx dir/LazyLoadView'

Vue.use(VueRouter)

// ...

const routes = [
{
path: '/home',
name: 'Home',
component: () => LazyLoadView(import(/* webpackChunkName: "Home" */ '@/views/Home')),
meta: { title: 'Home' }
},

{
path: '/about',
name: 'About',
component: () => LazyLoadView(import(/* webpackChunkName: "About" */ '@/views/About')),
meta: { title: 'About' }
},

//...

// 404 page must be placed at the end !!!
{ path: '*', redirect: '/404', hidden: true }
]
// ...

LazyLoadView 导致组件内导航守卫失效问题解决方案

需要注意的是使用 LazyLoadView 会导致组件内导航守卫失效,这是我的一种尝试方案

vue 项目中退出登录后清理 vuex 状态的方法

vuex 目录结构及关键内容

1
2
3
4
5
6
7
src/
store/
getters.js
index.js
modules/
user.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
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
// src/store/index.js
...
const modulesFiles = require.context('./modules', true, /\.js$/)
const _initState = {}

const modules = modulesFiles.keys().reduce((modules, modulePath) => {
const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
const value = modulesFiles(modulePath)
modules[moduleName] = value.default

// 备份初始状态,用于重置
_initState[moduleName] = deepClone(value.default.state) // 深克隆,自己实现或直接用 lodash 的

return modules
}, {})

const store = new Vuex.Store({
state: {
_initState
},
modules,
getters,
mutations: {
// 定义全局的清理方法
RESET_ALL_STATE(state, payload) {
const initState = deepClone(state._initState)
Object.keys(state).forEach(item => {
if (item !== '_initState') {
state[item] = initState[item]
}
})
}
}
})
...

// src/store/modules/user.js

...
const state = {}

const mutations = {}

const actions = {
...
// user logout
logout({ commit, state, dispatch }) {
return new Promise((resolve, reject) => {
...
// 清理所有状态
commit('RESET_ALL_STATE', null, { root: true })
resolve()
})
},
...
}

export default {
namespaced: true,
state,
mutations,
actions
}

使用 patch-package 修改第三方模块,及时解决第三方依赖包的 bug

安装

1
2
3
4
5
6

# npm
npm install patch-package --save-dev

# yarn
yarn add --dev patch-package postinstall-postinstall

创建补丁

直接在项目根目录下的 node_modules 文件夹中找到要修改依赖包的相关文件,然后回到根目录执行

1
2
3
4
5
# npm > 5.2
npx patch-package package-name

# yarn
yarn patch-package package-name

package-name 就是要修改的依赖包名
执行完成后,会在项目根目录的 patches 目录中创建补丁文件 package-name+0.44.0.patch(0.44.0 是依赖包版本),这个补丁需要提交到代码仓库中

options

  • --use-yarn
    patch-package 默认是根据项目中的 lockfile 来决定使用 npm 还是 yarn,如果两种都有,则使用 npm,可以通过这个参数启用 yarn

  • --exclude <regexp>
    创建补丁文件时,忽略与正则表达式匹配的路径,路径相对于要修改的依赖包的根目录,默认: package\\.json$

  • --include <regexp>
    --exclude <regexp> 相反,创建补丁文件时仅考虑与正则表达式匹配的路径,默认: .*

  • --case-sensitive-path-filtering
    使 --include--exclude 中使用的正则表达式区分大小写

  • --patch-dir
    指定放置补丁文件的目录

嵌套模块

支持修改依赖包的依赖包,比如 node_modules/package/node_modules/another-package,通过 / 分隔

1
2
3
4
npx patch-package package/another-package

# scoped packages
npx patch-package @my/package/@my/other-package

更新补丁

与创建补丁过程一样

打补丁

不带参数执行 npx patch-package 或者 yarn patch-package 应用所有补丁

options

  • --reverse
    撤回所有补丁
    Note: 如果打补丁后,补丁文件被修改过,此操作将失败,此时可以重新安装 node_modules

  • --patch-dir
    指定补丁文件所在目录

部署

package.jsonscripts 中加入 "postinstall": "patch-package",后续执行 npm installyarn install 命令时,会自动为依赖包打补丁了

参考资料

浏览器提供的 localStorage 本身是没有过期时间的,但是我们可以通过简单的封装人为的给它添加一个过期时间:

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
const exp = 60 * 60 * 24 * 1000 // 过期时间一天
const prefix = 'prefix_' // 前缀,主要用于避免冲突

/**
* 读取值
* @param {string} keyName - 字段名
* @param {any} defaultVal - 默认值,在某些情况下非常有用,比如返回对象时可免除空值判断
* @returns {any}
*/
export function getStoreValue(keyName, defaultVal = '') {
const data = localStorage.getItem(prefix + keyName)

if (data == null || data === '') {
return defaultVal
}

try {
const jsonData = JSON.parse(data)
if (Date.now() - jsonData.time >= exp) {
removeStoreValue(keyName)
return defaultVal
}
return jsonData.value
} catch (error) {
return defaultVal
}
}

/**
* 写入值
* @param {string} keyName - 字段名
* @param {any} value - 待存储的值
*/
export function setStoreValue(keyName, value) {
localStorage.setItem(prefix + keyName, JSON.stringify({ value, time: Date.now() }))
}

/**
* 删除值
* @param {string} keyName - 字段名
*/
export function removeStoreValue(keyName) {
localStorage.removeItem(prefix + keyName)
}

前后端分离模式下,前端在开发阶段利用 mock 服务降低了对后台的依赖,到了联调阶段,突然发现所有请求都跨域了,这时候有两条路:

  1. 让后台开启跨域支持
  2. 前端通过代理支持跨域

这里要讲的就是第二条路,由于项目是基于 vue-cli 构建的,所以通过 webpackdevServer.proxy 来实现跨域支持。

devServer.proxy 简介

先简单介绍一下 devServer.proxy 的用法:

注: 这里的 devServer.proxy 就是 vue-cli 中位于 /config/index.jsdev.proxyTable,后面不再赘述

基础用法

localhost:3000 上有后端服务的话,你可以这样启用代理:

1
2
3
4
5
6
7
8
9
10
11
// webpack.config.js

module.exports = {
...
devServer: {
proxy: {
'/api': 'http://localhost:3000'
}
}
...
}

这时请求 /api/users 会被代理到请求 http://localhost:3000/api/users

重写路径

如果你不想始终传递 /api ,则需要重写路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// webpack.config.js

module.exports = {
...
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
pathRewrite: { '^/api' : '' } // remove base path
// pathRewrite: { '^/api' : '/api/new-path' } // rewrite path
}
}
}
...
}

这时请求 /api/users 会被代理到请求 http://localhost:3000/users

HTTPS 支持

默认情况下,不接受运行在 HTTPS 上,且使用了无效证书的后端服务器。如果你想要接受,修改配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// webpack.config.js

module.exports = {
...
devServer: {
proxy: {
'/api': {
target: 'https://other-server.example.com',
secure: false
}
}
}
...
}

选择性代理

有时你不想代理所有的请求。可以基于一个函数的返回值绕过代理。

在函数中你可以访问请求体、响应体和代理选项。必须返回 false 或路径,来跳过代理请求。

例如:对于浏览器请求,你想要提供一个 HTML 页面,但是对于 API 请求则保持代理。你可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// webpack.config.js

module.exports = {
...
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
bypass: function(req, res, proxyOptions) {
if (req.headers.accept.indexOf('html') !== -1) {
console.log('Skipping proxy for browser request.');
return '/index.html';
}
}
}
}
}
...
}

代理多个路径

如果你想要代理多个路径特定到同一个 target 下,你可以使用由一个或多个「具有 context 属性的对象」构成的数组:

1
2
3
4
5
6
7
8
9
10
11
12
// webpack.config.js

module.exports = {
...
devServer: {
proxy: [{
context: ['/auth', '/api'],
target: 'http://localhost:3000',
}]
}
...
}

跨域支持

大部分情况都会涉及跨域问题,我们可以通过设置 changeOrigin: true 实现跨域支持:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// webpack.config.js

module.exports = {
...
devServer: {
proxy: {
'/api': {
target: 'https://other-server.example.com',
changeOrigin: true
}
}
}
...
}

基于 vue-cli 的跨域处理

回到最开始的问题,基于 vue-cli 的跨域处理,我们只要将上述相关配置写到 /config/index.jsdev.proxyTable 中即可,唯一需要注意的问题是我们可能在 /config/dev.env.js 中配置了 BASE_API: '"http://your-mock-server:port"',我们改造一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// /config/dev.env.js
...
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
BASE_API: '"/"'
})

// /config/index.js
...
module.exports = {
dev: {
...
proxyTable: {
'/api': {
target: 'http://your-api-server:port/', // 后端服务地址
changeOrigin: true
}
},
...
},
...
}

这样就实现了前后端联调时出现的跨域问题

我们完善一下,通过命令行传参来实现 mock 地址和联调地址的切换

1
2
3
4
5
6
7
8
9
// /package.json
{
...
"scripts": {
...
"api": "npm run dev --api"
},
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// /config/index.js
...
module.exports = {
dev: {
...
proxyTable: {
'/api': {
// 通过命令区分后端服务地址和 mock 地址
target: process.env.npm_config_api ? 'http://your-api-server:port/' : 'http://your-mock-server:port/',
changeOrigin: true
}
},
...
},
...
}

我们自己开发时运行 npm run dev,前后端联调时运行 npm run api

参考资料

webpack 官网 devServer.proxy 配置
http-proxy-middleware

记录一些常用的 Git 命令,便于查看

config

设置用户名和邮箱

1
2
git config –global user.name "[name]"
git config –global user.email "[email]"

别名配置

1
2
3
4
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.ci commit
git config --global alias.st status

init

初始化一个仓库

1
2
3
4
5
# 将当前目录初始化成一个Git代码库
git init

# 新建一个目录,将其初始化为Git代码库
git init [repository name]

clone

1
2
3
4
5
# 克隆现有的仓库
git clone [repository url]

# 克隆现有的仓库并自定义本地仓库名
git clone [repository url] [new repository name]

add

暂存改变的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 暂存指定文件
git add [file1] [file2] ...

# 暂存所有改变(新增,修改,删除)
git add -A

# 暂存所有改变(新增,修改,删除),git v1 版本不会暂存 删除文件
git add .

# 暂存除删除外的所有改变(新增,修改)
git add --ignore-removal .

# 暂存除新增外的所有改变(修改,删除)
git add -u

commit

提交

1
2
3
4
git commit -m "[commit message]"

# 跳过 add 步骤,直接提交
git commit -a -m "[commit message]"

fetch

1
2
3
4
5
# 下载远程仓库的所有变动
git fetch [remote]

# remove any remote-tracking references that no longer exist on the remote
git fetch --prune

pull

1
2
# 取回远程仓库的变化,并与本地分支合并
git pull [remote] [branch]

push

1
2
3
4
5
6
7
8
# 上传本地分支 branch1 到远程仓库
git push origin branch1

# 强行推送当前分支到远程仓库,即使有冲突
git push [remote] --force

# 推送所有分支到远程仓库
git push [remote] --all

checkout

1
2
3
4
5
6
7
8
# 切换分支
git checkout branch1

# 创建并切换到 branch1 分支
git checkout -b branch1

# 丢弃指定文件的修改,恢复至上一次 git commit 或 git add 时的状态
git checkout -- file

branch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 查看分支
git branch

# 创建分支 branch1
git branch branch1

# 删除本地分支
git branch -d branch1

# 强制删除本地分支
git branch -D branch1

# 删除远程分支(若报远程分支不存在,可以执行 git fetch --prune)
git push origin --delete branch1

merge

1
2
# 合并 branch1 到当前分支
git merge branch1

diff

1
2
3
4
5
# 比较当前分支文件的修改
git diff

# 比较指定文件的修改
git diff file

status

查看当前分支的状态

1
git status

log

查看提交记录

1
2
3
git log

git log --pretty=oneline

reflog

查看操作的历史命令记录

1
git reflog

reset

1
2
3
4
5
# HEAD^ 表示上一个版本,HEAD^^ 表示上上一个版本,HEAD~n 表示回退 n 个版本
git reset --hard HEAD^

# 回退到指定提交,commit id 没必要写全,前几位就可以了,Git会自动去找
git reset --hard [commit id]

关联并提交到远程仓库

1
2
3
git remote add origin [repository url]

git push -u origin master

stash

git 暂存,在临时需要开发其他功能时暂存当前修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 暂存当前修改
git stash save 'message'

# 查看已有的暂存
git stash list

# 清空已有的暂存
git stash clear

# 移除编号为 0 的暂存
git stash drop stash@{0}

# 使用编号为 0 的暂存(不指定则默认使用最近的)
git stash aplly stash@{0}

# 使用编号为 0 的暂存,并移除该暂存
git stash pop

# 查看编号为 0 的暂存
git stash show stash@{0}

# 从 stash 创建分支
git stash branch [branch name]

tag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 查看 tag
git tag

# 用特定的搜索模式列出符合条件的 tag
git tag -l 'v1.4.2.*'

# 新建 tag
git tag -a [tag name] -m [tag message]

# 查看指定 tag 的信息
git show [tag name]

# 推送 tag
git push origin [tag name]

# 一次推送所有本地新增的 tag
git push origin --tags

# 删除本地 tag
git tag -d [tag name]

# 删除远程 tag
git push origin :refs/tags/[tag name]

仓库迁移

Git仓库完整迁移 含历史记录

方法一

1
2
3
4
5
6
7
8
# 1. 从原地址克隆一份裸版本库
git clone --bare git://github.com/username/project.git
cd project.git

# 2. 然后到新的 Git 服务器上创建一个新项目

# 3. 以镜像推送的方式上传代码到新服务器上的新仓库
git push --mirror git@mygit.com/username/newproject.git

方法二

1
2
3
4
git clone --mirror <URL to my OLD repo location>
cd <New directory where your OLD repo was cloned>
git remote set-url origin <URL to my NEW repo location>
git push -f origin

环境准备

  • nodejs(7.6+)
  • mongodb(2.6+)
  • git

nodejs 与 git

1
sudo apt install nodejs npm git

mongodb

安装 MongoDB

  1. 导入公钥

Ubuntu 软件包管理器 apt(高级软件包工具)需要软件分销商的 GPG 密钥来确保软件包的一致性和真实性。 运行此命令将 MongoDB 密钥导入到您的服务器。

1
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 2930ADAE8CAF5059EE73BB4B58712A2291FA4AD5
  1. 创建源列表文件

使用以下命令在 /etc/apt/sources.list.d/ 中创建一个 MongoDB 列表文件:

1
echo "deb http://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/3.6 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.6.list
  1. 安装 MongoDB
1
2
sudo apt-get update
sudo apt-get install -y mongodb-org

MongoDB apt 安装程序自动为 Systemd 创建了一个 mongod.service 文件,因此不需要再手动创建它。

启动 MongoDB 并将其添加为在启动时启动的服务:

1
2
systemctl start mongod
systemctl enable mongod

配置 MongoDB 用户名和密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mongo

# 进入 MongoDB shell 后,切换到数据库管理员
use admin
# 创建 root 用户
db.createUser({user:"admin", pwd:"admin123", roles:[{role:"root", db:"admin"}]})


# 切换到数据库 yapi
use yapi
# 创建 yapi 管理员用户
db.createUser({user:"yapiAdmin", pwd:"yapi123", roles:[{role:"dbOwner", db:"yapi"}]})

# 退出
exit

# 重新启动MongoDB并连接创建的用户
sudo service mongod restart

MongoDB 数据迁移

备份
1
mongodump -h dbhost  -d dbname -o dbdirectory
  • -h: mongodb 所在服务器地址,例如 127.0.0.1,也可以指定端口 127.0.0.1:27017
  • -d: 需要备份的数据库名称,例如:yapi
  • -o: 备份的数据存放的位置,例如:/home/bak
  • -u: 用户名称,使用权限验证的 mongodb 服务,需要指明导出账号
  • -p:用户密码,使用权限验证的 mongodb 服务,需要指明导出账号密码
恢复
1
mongorestore -h dbhost -d dbname --drop <path>
  • -h: mongodb 所在服务器地址
  • -d: 需要恢复备份的数据库名称,例如:yapi,可以跟原来备份的数据库名称不一样
  • --drop: 加上这个参数的时候,会在恢复数据之前删除当前数据
  • <path>: 备份数据所在位置,例如:/home/bak/yapi
  • --dir: 指定备份的目录,你不能同时指定 <path>--dir 选项

yapi

方式一. 可视化部署

执行 yapi server 启动可视化部署程序,输入相应的配置和点击开始部署,就能完成整个网站的部署。部署完成之后,可按照提示信息,执行 node/{网站路径/server/app.js} 启动服务器。在浏览器打开指定 url, 点击登录输入您刚才设置的管理员邮箱,默认密码(ymfe.org) 登录系统(默认密码可在个人中心修改)。

1
2
npm install -g yapi-cli --registry https://registry.npm.taobao.org
yapi server

经测试,ubuntu 18.04 中 9090 端口被占用,目前需在源码中修改端口:

1
2
3
4
5
6
7
8
// 注:路径可能有所不同,下面的是我机器上的
// /usr/local/lib/node_modules/yapi-cli/src/commands/server.js
// line 91

...
// app.listen(9090)
app.listen(argv.port || 9090) // 支持 yapi server --port 9290
...

方式二. 命令行部署

1
2
3
4
5
6
7
8
9
10
mkdir yapi
cd yapi
git clone --depth=1 https://github.com/YMFE/yapi.git vendors
cp vendors/config_example.json ./config.json // 复制完成后请修改相关配置
cd vendors
npm install --production --registry https://registry.npm.taobao.org

# 在此之前请先修改配置 config.json
npm run install-server // 安装程序会初始化数据库索引和管理员账号,管理员账号名可在 config.json 配置
node server/app.js // 启动服务器后,请访问 127.0.0.1:{config.json配置的端口},初次运行会有个编译的过程,请耐心等候

请在启动前进行配置

1
sudo vim config.json

pm2 进程守护

1
2
3
4
npm install pm2@latest -g

# 注意目录 app.js 的目录,pm2 start `real_path`/app.js
pm2 start ./server/app.js

pm2 常用命令

  • 开机自启动:pm2 startup,然后按提示操作
  • 取消开机自启动:pm2 unstartup,然后按提示操作
  • 保存当前应用:pm2 save
  • 清理已保存的应用:pm2 cleardump

参考资料

https://yapi.ymfe.org/devops/index.html
https://www.linuxidc.com/Linux/2018-05/152253.htm
https://pm2.io/doc/en/runtime/guide/startup-hook/

nuxt 使用过程中报错,错误信息如下:

1
[Vue warn]: The client-side rendered virtual DOM tree is not matching server-rendered content. This is likely caused by incorrect HTML markup, for example nesting block-level elements inside <p>, or missing <tbody>. Bailing hydration and performing full client-side render.

给的错误信息很全,关键是如何定位。这里给出一种方案(基于 Chrome 浏览器):

  1. F12 打开控制台,然后重载报错页面
  2. 在控制台找到该错误位置,点击错误信息右边的定位链接跳转到错误代码行并在此打个断点
  1. 重载报错页面进入断点,将鼠标移到变量 msg 上,确定是否为当前的错误
  1. 在调用堆栈(Call Stack)中点击 patch,浏览器会自动定位至新的调用栈,将鼠标移至光标上方第四行的 hydrate 方法上,然后点击弹出信息框右上角的链接进入 hydrate 方法内部,找到光标下方的第 14 行 return false 的地方,打个断点,把其他断点移除
  1. 再次重载页面进入断点,这时可以将 elm 变量打印出来,elm 就是导致报错的元素
0%