What

想看一下 某个包的文件

查看 usb连接/模拟器 的设备 adb devices

连接一个具体的设备 adb -s <设备id> shell

列出手机里的安装包pm list packages 当然 grep命令也可以用

比如你要查看的包为com.xxx.yyy那么 run-as com.xxx.yyy

cd ls grep cat这些命令是有的 并没有vi XD,不过有cat 基本能看文件了

// 搜到了 单个文件 cat 出来的 但没搜到文件夹的

// 也有间接先cat到sdcard再pull 再删sdcard

// 还搜到了一个 用 adb backup 把整个app搞出来,再 转换成tar的 [见下面链接]

// 当然 还有一堆说root的 emmm 告辞

// Stetho?

参考

https://stackoverflow.com/questions/53634246/android-get-all-installed-packages-using-adb

https://gist.github.com/davidnunez/1404789

https://stackoverflow.com/questions/1043322/why-do-i-get-access-denied-to-data-folder-when-using-adb?rq=1

https://stackoverflow.com/questions/7399028/android-adb-permission-denied

https://stackoverflow.com/questions/15558353/how-can-one-pull-the-private-data-of-ones-own-android-app

https://stackoverflow.com/questions/9017073/is-it-possible-to-see-application-data-from-adb-shell-the-same-way-i-see-it-moun/16461386

https://stackoverflow.com/questions/15558353/how-can-one-pull-the-private-data-of-ones-own-android-app/31504263#31504263

What

闪耀暖暖的设计师 材料耗材计算器

基于一个已经有的原生js 进行改造

原来生 http://www.spongem.com/ajglz/tools/evalLevelUpCost.html

重构内容

  1. 首先把这个的源码 搞下来,比较无脑的操作
  2. 然后开个module 配一下routing
  3. 搞个component 然后把函数数据都搬进来
  4. 把相关全局变量 改为 this.
  5. 修改原来的一些$方法 改为原生js,或者改为[(ngModel)]双向绑定,至此基本可以运行。
  6. 解构一下用户输入和计算输出分别叫做Class: CardBasic/Card -> html:Card/CardOut // 好吧 这里变量名有点乱 现在看来
  7. 引入https://material.angular.io的组件,稍微调整画风
  8. 尝试搞一个computed,移除计算按钮, 改为 get XXX的方法

这里遇到问题, 根据调试,页面上的调用会反复调用get方法, 大量重复不必要计算

  1. 解决方案1: 引入Ramda,建立两个cache 分别是计算结果 和 用户输入,用 Ramda的深比较和深拷贝完成少的计算次数

写着还好,行数不多,就能实现,性能也好,但是问题是,相当于你依然要去关心的是,输出值输入值直接的具体关系才能写。

  1. 解决方案2: 引入rxjs,用Observable搞

这里实践上有些问题

  • 双向绑定如何和BehaviorSubject绑定,最后用(ngModelChange)来 单向到 具体字段
  • 能否监听整个Class
1
2
3
4
5
import { of } from 'rxjs';
let a = {x:1,y:2};
let a$ = of(a);
a$.subscribe(console.log);
a.x=3;

例如这样 并不会因为修改了a的子字段而自动发送next

搞了半天最后还是 感觉很 手工写依赖的输入

这一块的实现主要如下

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
// 相当于所有用户输入的部分 都会对应一个emitter
private textEmitter: Map<string, BehaviorSubject<string>> = new Map();
// 在初始化时 初始化它们并监听
// initial textEmitter
for (const key in this.card) {
this.textEmitter.set(key, new BehaviorSubject(this.card[key]));
}
this.obAllKeys(this.textEmitter)
.pipe(
map(() => {
// 细粒度触发 全体计算
console.log('fire calc')
return this.cardOut; // 就是最开始写的get方法 触发过程中只 告诉说 '哎!我变了!' 即可
})
).subscribe(this.cardOut$); // 页面通过这个读取 是个 BehaviorSubject

// 做的个功能 监听一个类 的所有字段 对应的 textEmitter的Observable
// 做了一点防抖
obAllKeys(emitter: Map<string, BehaviorSubject<string>>): Observable<string> {
let mergeOb: Observable<string> = new BehaviorSubject('');
emitter.forEach((emitValue, emitKey) => {
mergeOb = merge(mergeOb, // 关于merge的使用考虑 最开始想过使用combineLatest 但是 问题是 我明明可以直接通过 页面双向绑定拿到 用户输入,所以其实只需要一个变更通知即可,再另一个 combineLatest感觉要配上flat 才能 自动 绑定所有key,不然是手写的所有key,而对应的下标再反向生成 对象,表示 是不是有更好的方法 没学到没搜到
emitValue.pipe(
debounceTime(this.debounceInterval),
distinctUntilChanged(),
map((v) => emitKey + v) // 这个是避免 在段时间内 上次状态a, 改b 改a 改回最初的a 这样就不会触发改变
));
});
return mergeOb.pipe(debounceTime(this.debounceInterval)); // 按理说一次只会改动一个 这个是初次进入时 会有多个 当然 combineLatest 不会有这个问题
}

// 主要是和页面上的 输入绑定 具体就是 ngModelChange ,这一块的问题就是 写了 [(ngModel)] 又写了一遍这个绑定 光是key就要写两遍 还是字符串没法保证正确 靠的是下面的 调试强行保证部分
onChange(cardKey: string, value: string) {
console.log(`onCardIdChange: ${cardKey} => ${value}`);
if (this.textEmitter.has(cardKey)) {
this.textEmitter.get(cardKey).next(value);
} else { // 调试用
console.warn('onChange', cardKey, value);
}
}
  1. 最后增加了一点 try catch,因为 不同的卡可选范围并不同,或者用户输入不合法会 爆红=.=只在 两个LevelVal里加了

最后结果

https://cromarmot.github.io/Debuq/#/nikki4/lvlup

如果打开看到404,保持页面,等待一会儿再刷新即可。因为有worker service

TODO

比如搞成动态拉取

计算函数再向纯函数靠拢啊

怎么json 转换 Class 甚至 Map // 搜了一会没搜到简洁的方案

更好的双向绑定+BehaviorSubject 绑定?

是否可能简化某部分代码

输入控制 比如 现在其实可以输入 小数 和 范围以外的数

比如因为还是有不少json 导致 typescript的 类型判断并不完整 虽然

依赖

英文原文

rxjs入门

摘要

如今,越来越多开发者开始学 RxJs, 并跟随最佳实践正确使用它。但是完全不必要,那些所谓的最佳实践,需要学一些新的内容,并且在你的项目中增加额外的代码。

更多的是,使用最佳实践,是冒着创建好的代码库和让你的队友高兴的风险! 🌈

Stop being a gray mass! 打破常规,停止使用最佳实践

下面我将想你介绍,怎么改造那些所谓的最佳实践代码.

  • 不要unsubscribe
  • 嵌套使用Subscribe
  • 不要使用 纯函数
  • 手动subscribe,不要使用 async pipe
  • 向你的服务暴露subjects
  • 始终对子组件传递流
  • 宝石图? 并不适合你
閱讀全文 »

Why

模拟生产环境 尝试 升级 gitlab-ce 从 11.6 到 12.2, 以及尝试模拟之前产生的错误

官方说 先要到11.11.X再到12.X

物料准备

vmware on Ubuntu 18.04 LTS

gitlab-ce-11.6.5-ce.0.el6.x86_64.rpm

gitlab-ce-11.11.0-ce.0.el6.x86_64.rpm

gitlab-ce-12.2.5-ce.0.el7.x86_64.rpm

rhel-server-7.7-x86_64-dvd.iso

Install OS

https://access.redhat.com/downloads/content/69/ver=/rhel---7/7.7/x86_64/product-software

注册 下载 安装

需要注意的是 下4GB左右的 而不是boot.iso

为什么是7 不是8(当前最新)

因为想和某个生产环境保持一致 尝试 模拟某些操作

閱讀全文 »

How

参考

https://www.cnblogs.com/amiezhang/p/11337095.html

上面链接都有讲

按照文件大小排序前100:

git rev-list --objects --all | grep "$(git verify-pack -v .git/objects/pack/*.idx | sort -k 3 -n | tail -100 | awk '{print$1}')"

git filter-branch --force --index-filter 'git rm --cached --ignore-unmatch 文件名' --prune-empty --tag-name-filter cat -- --all

git filter-branch --index-filter 让每个提交的文件都复制到索引(.git/index)中

然后运行过滤器命令:git rm --cached --ignore-unmatch 文件名 ,让每个提交都删除掉“文件名”文件

然后--prune-empty把空的提交“修剪”掉

然后--tag-name-filter cat把每个tag保持原名字,指向修改后的对应提交

最后-- --all将所有ref(包括branch、tag)都执行上面的重写

git for-each-ref --format='delete %(refname)' refs/original | git update-ref --stdin

git reflog expire --expire=now --all

git gc --prune=now

注意

  1. 上面的文件名可以写通配符号

  2. 众所周知只要我加到git中过的,即使把分支删了,也可以从reflog中找到,但这里 有 清除reflog所以 想要这样做的,要么 单独开文件夹搞,要么 确定 reflog都不会再用

  3. 什么时候使用? 使用频率不应高,因为 这种操作无疑是会对 分支重写,也就完全不同的commit hash,所以最后push都会带上–force, 目前能想到场景,比如 不必要的大文件,如不小心提交的下下来的打包后文件,大zip.也有场景 比如 很多分支前 不小心提交了一个涉密的文件。 当然push force以后 所有用仓库的人要想继续用只有去新的分支里搞了,已经被别人下的还会在别人硬盘里

19-08-22 -> 19-09-15

ISBN:9787508672069

Main

回顾历史,发现很多现在有的,都是由各种其它混杂原因的组合

历史解决了三大 饥荒 瘟疫 战争

将可能考虑新的问题 永生 快乐

历史的解决问题的贡献,很多也用于人类升级

閱讀全文 »

依赖版本

element-ui 版本 10592d12ea981912165542920160669fd8874bd9

文档 https://element.eleme.io/#/zh-CN/component/message-box

要解决的问题

像是

1
2
3
4
$msgbox(options)
$alert(message, title, options) 或 $alert(message, options)
$confirm(message, title, options) 或 $confirm(message, options)
$prompt(message, title, options) 或 $prompt(message, options)

这样的api是怎么实现的

代码阅读

大概看一眼怎么搞的

src/index.js 看 这些方法 ,都是来自 import MessageBox from '../packages/message-box/index.js';

1
2
3
4
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;
Vue.prototype.$confirm = MessageBox.confirm;
Vue.prototype.$prompt = MessageBox.prompt;

那么看 packages/message-box/src文件夹

两个文件 main.jsmain.vue

main.vue瞄一眼,都是我们常见的写法

main.js,看这些方法 最后 都是 调用 MessageBox(配置参数)的形式调用

再看const MessageBox = function(options, callback) {的实现

都是 先msgQueue.push(配置参数) + showNextMsg()

msgQueueshowNextMsg,分别是个数组,和从数组中shift() 取出值 进行具体展示执行

然后 看 showNextMsg()

1
2
3
if (!instance) {
initInstance();
}

然后

1
2
3
4
5
6
7
const initInstance = () => {
instance = new MessageBoxConstructor({
el: document.createElement('div')
});

instance.callback = defaultCallback;
};

1
const MessageBoxConstructor = Vue.extend(msgboxVue);

说明了基本这就是 工厂模式+单例模式+Vue.extend来创建弹框单例,所以我们页面上不需要写什么,就能直接调用

然后这些confirm调用过程 也就是对这个单例的参数改动 比如控制样式 数据 显示之类的

总结

  1. 用户调用 $msgbox
  2. 调用到packages/message-box/src/main.jsMessageBox(options,callback)
  3. MessageBox(options,callback)通过 数组 + 数组.shift,依次提取数据展示
  4. 对于展示的实例 是 通过 Vue.extend(packages/message-box/src/main.vue) + 工厂单例来产生的一个实例
  5. 他们 搞了一个element/examples/components/demo-block.vue 可以在markdown写示例代码,并且在网页上查看 可以运行 66666

那常见的问题可能有说,连续调用2此带有回调函数的$msgbox会怎样

我们可以看showNextMsg()实现

1
2
3
4
5
6
7
8
9
10
11
12
13
//得到 instance
if (!instance.visible || instance.closeTimer) {
//...
let oldCb = instance.callback;
instance.callback = (action, instance) => {
oldCb(action, instance);
showNextMsg();
};
//...
Vue.nextTick(() => {
instance.visible = true;
});
//...

意思就是如果我们同步调用两个,

  • 因为我们visible设置为true是 异步里发生的,那么 这两个调用时 visible默认都是false,所以 只会最后一个生效
  • 如果我们异步调用两次msgbox,那么 后一个调用 会在!visible的地方为false,不会直接触发,但是因为设计了msgQueue,因此 会放在数组中,然后看到 上面的callback,会在调用oldCb以后 再调用showNextMsg,所以会在对话框回调以后 再回调 那个时刻,msgQueue里首个 并从数组中移除

总结

总流程实现

也就是方法是src/index.js: Vue.prototype.方法名 = 方法函数 来让全局可用

通过 document.createElement('div') 和 全局变量, 创建绑定维护对应的单例实例.

这样就实现了 任何页面,不需要写组件,就能this.方法名(参数) 来弹窗

适用场景,不需要业务定UI,否则需要页面v-slot之类插入业务内容

参数设计(核心参数与可选参数与回调)

核心参数是函数的直接参数

可选参数组合成一个对象,提供默认值

回调既有成功也有失败,随是promise,总觉得用reject来做取消和关闭其实不太好, 可以全resolve走自定义状态

提供一定的重载实现

内部具体

队列, 同步异步, callback持有

常用命令

submodule

git submodule add 仓库url

git diff --cached --submodule

clone后

1
2
git submodule init
git submodule update

clone时自动 拉取git clone --recurse-submodules

git log -p --submodule

subtree

git subtree add --squash --prefix=文件夹名 仓库url 分支名

分割

git subtree split --prefix=文件夹名 -b 分支名

配合remote使用

比较

subtree submodule
相当于拷贝文件甚至commit到当前仓库 相当于只记录了另一个仓库的某个提交的指针
因此 pull容易push难 push容易pull难

参考

https://git-scm.com/book/en/v2/Git-Tools-Submodules

https://git-scm.com/book/en/v1/Git-Tools-Subtree-Merging

本文对应版本 638278b334199f17e052a54a0837c97624940c0c

获得代码

1
2
3
4
git init
git remote add origin https://github.com/vuejs/vue-router.git
git fetch origin 638278b334199f17e052a54a0837c97624940c0c
git reset --hard FETCH_HEAD

前置知识

https://cn.vuejs.org/v2/guide/mixins.html

https://cn.vuejs.org/v2/guide/plugins.html

总览

行数 文件
262 index.js
200 create-matcher.js
353 history/base.js
22 history/errors.js
69 history/abstract.js
80 history/html5.js
157 history/hash.js
190 components/link.js
124 components/view.js
52 install.js
193 create-route-map.js
6 util/misc.js
95 util/query.js
3 util/dom.js
25 util/warn.js
152 util/scroll.js
35 util/params.js
132 util/route.js
64 util/location.js
22 util/state-key.js
18 util/async.js
42 util/push-state.js
74 util/path.js
108 util/resolve-components.js

总共2478行

目录

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
src/
├── components
│   ├── link.js 控制url显示
│   └── view.js 控制页面渲染
├── create-matcher.js 路由匹配
├── create-route-map.js
├── history
│   ├── abstract.js
│   ├── base.js
│   ├── errors.js
│   ├── hash.js
│   └── html5.js
├── index.js 包含 VueRouter 类
├── install.js 日常mixin
└── util
├── async.js
├── dom.js
├── location.js
├── misc.js
├── params.js
├── path.js
├── push-state.js
├── query.js
├── resolve-components.js
├── route.js
├── scroll.js
├── state-key.js
└── warn.js

回顾那个只有1100行左右的vuex源码,你发现 除了两者都有index.js,其它目录结构大不相同了

准备

首先我们看一段代码,最好先了解它的功能,所以没有用过vue-router的建议先照着tutorial知道它大概的功能

然后看看源码里的demo:

npm installPORT=8085 npm run dev即可在localhost:8085上查看,把上面每个example都看一看

然后就可以看看flow文件夹里的,也不长,’类’的数量也不多,通过阅读flow的文件,你可以对 整个router有个印象

从代码使用上看

  • 实例类router
  • 配置路径 和 对应component
  • 页面<router-view>,<router-link> 以及 a标签等跳转
  • Vue.use(VueRouter) // 注册插件
  • new Vue({router传入实例})

this上会有$route$router

src/

代码阅读

  1. 先看install.js,调用Vue.mixin 注入
  2. 然后index.js, 看完这一部分基本 就大概看到了 主要是靠XXXHistory来 实现 this.history然后方法 不少只是对 this.history的方法转发
  3. 既然知道了主要是XXXHistory来实现,那么 反过来 先按照顺序看util文件夹,伴随着test里的测试用例看完util的代码,把大多代码看懂即可,少部分比较复杂也没有测试用例的大自看看
  4. 然后看history的代码,顺序就errors.js ,base.js,最后 abstract/hash/html5这三个任意顺序
  5. 然后是两个create-*.js,工具人 工具函数
  6. 最后看两个components

下面目录是按字典序排列的,不是按照阅读顺序

components

emmm, noop = ()=>{}

所以 也就会想到说 ,是 函数内 增加一堆if来处理 有值 无值 空值,还是说 强行要求传入符合格式,这两种方式 如何选择

也就是 render函数

可以看到 有从 初始化的options中 读取 linkActiveClass/linkExactActiveClass用什么 或者用默认的router-link-active/router-link-exact-active

这里实现了一个 guardEvent(e) 不会拦截 metaKey,altKey,ctrlKey,shiftKey等等

当拦截时,执行replace或者push

定义变量on配置了click和 传入的event或event数组里所有事件 调用 handler也就是 guardEvent

这里通过 this.$scopedSlots.default({ 拿去默认slot

然后对它渲染, 这里又可以看到 如果有多个default slot,在production时会报warn

然后 props的tag默认是’a’标签,

如果是 tag=='a'那么 绑上onattrs

如果不是tag=='a'// find the first <a> child and apply listener and href

const a = findAnchor(this.$slots.default) 递归 找第一个 achild

如果没找到data.on = on;

最后 调用 h(this.tag, data包含了on class 等等, this.$slots.default)

其中提供 的两个辅助函数 guardEventfindAnchor分别时 用来 拦截默认 a标签事件和 深搜找到首个a标签的

view.js

也就是 渲染html上的 RouterView 或者说<router-view>

functional:true

意味着 没有响应式数据,没有this上下文

https://cn.vuejs.org/v2/guide/render-function.html#%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BB%84%E4%BB%B6

接受一个props,

1
2
3
4
name: {
type: String,
default: 'default'
}

可以看到接受的render 也和 link的不一样,link的是 render(h)

1
render (_, { props, children, parent, data }) {

给devtools用的

1
2
// used by devtools to display a router-view badge
data.routerView = true

因为没有this,没有接受 h ,这里是通过 h = parent.$createElement

此外 $route,_routerViewCache也是从 parent拿的

通过 比较 parent == parent._routerRoot 来看parent是否为根节点,并递归向上找根节点

如果 某个祖先上有parent.$vnode.data.routerView 那么 深度计数++

如果 某个祖先上有parent.$vnode.data.keepAliveparent._inactive 那么 inactive标识为 true

data.routerViewDepth = 深度计数

如果非活跃

1
2
3
if (inactive) {
return h(cache[name], data, children)
}

通过 $route.matched[深度] 当前的路由的matched数组

1
2
3
4
5
6
const matched = route.matched[depth]
// render empty node if no matched route
if (!matched) {
cache[name] = null
return h()
}

更新cache指针

const component = cache[name] = matched.components[name]

也就是 我们在src/install.js中看到的 registerRouteInstance

1
2
3
4
5
6
7
8
9
10
11
12
// attach instance registration hook
// this will be called in the instance's injected lifecycle hooks
data.registerRouteInstance = (vm, val) => {
// val could be undefined for unregistration
const current = matched.instances[name]
if (
(val && current !== vm) ||
(!val && current === vm)
) {
matched.instances[name] = val
}
}

注册 prepatch 和 init的钩子,这个一个应该是 vue相关的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// also register instance in prepatch hook
// in case the same component instance is reused across different routes
;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
matched.instances[name] = vnode.componentInstance
}

// register instance in init hook
// in case kept-alive component be actived when routes changed
data.hook.init = (vnode) => {
if (vnode.data.keepAlive &&
vnode.componentInstance &&
vnode.componentInstance !== matched.instances[name]
) {
matched.instances[name] = vnode.componentInstance
}
}

只搜到了 https://github.com/vuejs/vue/issues/8657

最后是 传递matched.props[name]的内容

具体代码就是 把在data.props中,不在 components.props中的 key以及对应的值搬运到 data.attrs里

最后 return h(component, data, children)

层级router-view实现原理

index.js -> createMatcher-> createRoute -> .matched = record ? formatMatch(record) : []

其中record是 RouteRecord类型,formatMatch通过 递归 parent 来把 它变 数组

其中parent的来源 是create-route-mapaddRouteRecord递归计算的

然后 在 解析 元素时 可以通过当前 $route.matched[depth]直接获得实例

create-matcher.js

1
2
3
4
5
6
export function createMatcher (
routes: Array<RouteConfig>,
router: VueRouter
): Matcher {
const { pathList, pathMap, nameMap } = createRouteMap(routes)
}
1
2
3
4
5
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {

匹配name(nameMap找) -> 匹配 location.path(pathList + pathMap) -> create 一个新的Route

这里用list 也可以看出 这里期望的个数应该要小(?),否则效率O(n)大了性能会较差?(不过O(n*操作)似乎1000个也不会很久? 反正这也不是频繁操作?所以不用在意?

1
2
3
4
function redirect (
record: RouteRecord,
location: Location
): Route {

同样是redirect(record.redirect)中的 name匹配 path匹配

1
2
3
4
5
function alias (
record: RouteRecord,
location: Location,
matchAs: string
): Route {

通过匹配 fillParams(matchAs, location.params,...) 到path 同样返回Route

1
2
3
4
5
function _createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {

再调用 真的createRoute 或者 调上面的 redirect/alias 只要 record.redirect/matchAs存在, 也就是 可能产生无限循环?的了

create-route-map.js

1
2
3
4
5
6
7
8
9
10
export function createRouteMap (
routes: Array<RouteConfig>,
oldPathList?: Array<string>,
oldPathMap?: Dictionary<RouteRecord>,
oldNameMap?: Dictionary<RouteRecord>
): {
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>
} {

还会自动把 '*' wildcard routes 放到最后

1
2
3
4
5
6
7
8
function addRouteRecord (
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>,
route: RouteConfig,
parent?: RouteRecord,
matchAs?: string
) {

为什么 其他地方 都是直接 用true/false,这边 route.caseSensitive要判断typeof === 'boolean'

递归对.children节点调用 addRouteRecord

这两个函数 主要是封装 对 传入的 pathList,pathMap,nameMap 这些 进行 添加

因为处理了没有传值的情况,所以也可以用于初始化

history

abstract.js

正如其名,所有浏览器上地址栏,url,scroll都没有了,取代的 是 实现了 stack:Array<Route>index:number

base.js

是整个源码中最大的单个文件了, hash/html5/abstract 这三个 都是基于base实现的,反过来想在 index.js中我们有看到根据模式不同 把this.history 赋予了不同的值 也就会想要这三个有共同的基类

对外只提供一个History类

这些 加号 是干嘛的,我看flow的文档只看到 说 readonly,是我没有找到正确的位置吗

1
2
3
4
5
6
// implemented by sub-classes
+go: (n: number) => void
+push: (loc: RawLocation) => void
+replace: (loc: RawLocation) => void
+ensureURL: (push?: boolean) => void
+getCurrentLocation: () => string

初始化

1
2
3
4
5
6
7
8
9
10
11
constructor (router: Router, base: ?string) {
this.router = router
this.base = normalizeBase(base)
// start with a route object that stands for "nowhere"
this.current = START
this.pending = null
this.ready = false
this.readyCbs = []
this.readyErrorCbs = []
this.errorCbs = []
}

listen(cb) 简单粗暴 直接 this.cb=cb

1
onReady (cb: Function, errorCb: ?Function) {}

如果当前ready同步执行cb,否则readyCbs数组push进cb,错误回调push进readyErrorCbs

1
onError (errorCb: Function) {`

push进errorCbs

1
2
3
4
5
transitionTo (
location: RawLocation,
onComplete?: Function,
onAbort?: Function
) {}

调用 confirmTransition

成功执行this.updateRoute(route) & onComplete(route) & this.ensureURL()调用所有 readyCbs Once

失败onAbort(err) 所有readyErrorCbs

1
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {}

如果目标和当前Route一样, 触发 NavigationDuplicated(route),调用errorCbs再 调用onAbort(err)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const { updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
)

const queue: Array<?NavigationGuard> = [].concat(
// in-component leave guards
extractLeaveGuards(deactivated),
// global before hooks
this.router.beforeHooks,
// in-component update hooks
extractUpdateHooks(updated),
// in-config enter guards
activated.map(m => m.beforeEnter),
// async components
resolveAsyncComponents(activated)
)

设计了一个iterator函数,用runQueue对上面queue进行执行

真是艹了 就非要重复使用变量名吗?? 这里queue

上面的每个如果都成功了,那么执行

1
2
extractEnterGuards(activated, postEnterCbs, ()=> this.current === route)
this.router.resolveHooks

这里 各种调用,也就意味着可能 不同步 吗? 反正这边是用 pending在 做 跳转前和跳转后的route一样保证

如果上面再成功则 回调onComplete 和 异步回调所有 postEnterCbs

1
updateRoute (route: Route) {}

更新this.current 回调this.cb,调用所有afterHooks

其它自产自用函数

1
function normalizeBase (base: ?string): string {}

// 这里是不是漏处理了file://开头的 对应之前有个bugfix类型

传了base值就前面加个/,否则有<base> tag就取 其中base的部分,否则就空

1
2
3
4
5
6
7
8
function resolveQueue (
current: Array<RouteRecord>,
next: Array<RouteRecord>
): {
updated: Array<RouteRecord>,
activated: Array<RouteRecord>,
deactivated: Array<RouteRecord>
} {}

找第一个current和next中不一样的,下标idx,返回

1
2
3
4
5
{
updated: next.slice(0, idx),
activated: next.slice(idx),
deactivated: current.slice(idx)
}
1
2
3
4
5
6
7
8
9
10
11
function extractGuards (
records: Array<RouteRecord>,
name: string,
bind: Function,
reverse?: boolean // 控制是返回的数组的顺序
): Array<?Function> {}

function extractGuard (
def: Object | Function,
key: string
): NavigationGuard | Array<NavigationGuard> {}

把records中所有 _Vue.extend(具体元素).options[name]处理,如果是数组 对每一个 bind,如果非数组单个bind,最后flatten

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> {
return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}

function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> {
return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
}

function bindGuard (guard: NavigationGuard, instance: ?_Vue): ?NavigationGuard {
if (instance) {
return function boundRouteGuard () {
return guard.apply(instance, arguments) // 这里把参数`带`过去
}
}
}
1
2
3
function extractEnterGuards (
// 和上面同理绑定的是 'beforeRouteEnter' 和 'bindEnterGuard'
)
1
2
3
4
5
6
7
function bindEnterGuard (
guard: NavigationGuard,
match: RouteRecord,
key: string,
cbs: Array<Function>,
isValid: () => boolean
): NavigationGuard {}

增加回调调用poll

1
2
3
4
5
6
function poll (
cb: any, // somehow flow cannot infer this is a function
instances: Object,
key: string,
isValid: () => boolean
) {}

instances[key]._isBeingDestroyed为false时 调用cb(instances[key])

这里 用isValid()也就是上面传入的this.current === route+ setTimeout(16ms)来判断要不要调用 poll

这是为了页面跳转了 但是 instances没有实例化完成 所以不停异步尝试?

errors.js

一个错误类 NavigationDuplicated

//support IE9 emmm

hash.js

和 html5,index 一样 都是继承于上面的 History

1
// check history fallback deeplinking
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
export function getHash (): string {
// We can't use window.location.hash here because it's not
// consistent across browsers - Firefox will pre-decode it!
let href = window.location.href
const index = href.indexOf('#')
// empty path
if (index < 0) return ''

href = href.slice(index + 1)
// decode the hash but not the search or hash
// as search(query) is already decoded
// https://github.com/vuejs/vue-router/issues/2708
const searchIndex = href.indexOf('?')
if (searchIndex < 0) {
const hashIndex = href.indexOf('#')
if (hashIndex > -1) {
href = decodeURI(href.slice(0, hashIndex)) + href.slice(hashIndex)
} else href = decodeURI(href)
} else {
if (searchIndex > -1) {
href = decodeURI(href.slice(0, searchIndex)) + href.slice(searchIndex)
}
}

return href
}

setupListeners()调用 setupScroll()

监听popstate或者hashchange 触发 transitionTo()

push和replace也都是 调用 transitionTo() 回调的时候 push/replace Hash()然然后handleScroll, 和onComplete(route)

html5.js

和 hash不同的是, 在constructor里直接 初始化了 ,比如setupScroll()和 增加popstate事件触发,而不是让index.js调用

在 这里默认 pushState和replaceState都是可以用的

这里我在想 因为兼容而写的代码 值得吗,留多久,多久抛弃呢?

index.js

import引入,不用细看,总之是引入依赖的

VueRouter类实现

想说 这种

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static install: () => void;
static version: string;

app: any;
apps: Array<any>;
ready: boolean;
readyCbs: Array<Function>;
options: RouterOptions;
mode: string;
history: HashHistory | HTML5History | AbstractHistory;
matcher: Matcher;
fallback: boolean;
beforeHooks: Array<?NavigationGuard>;
resolveHooks: Array<?NavigationGuard>;
afterHooks: Array<?AfterNavigationHook>;

写法对阅读来说真香,这是flow还是ts 来着

  • this.matcher = createMatcher(options.routes || [], this) // 用户传入的routes

三种 mode

‘hash’(默认 只识别井号后面的路径前面的忽略? window.location.hash),’history’,’abstract’(服务端node)

分别调用

  • this.history = new HTML5History(this, options.base)
  • this.history = new HashHistory(this, options.base, this.fallback)
  • this.history = new AbstractHistory(this, options.base)

实例对外提供的方法 整理如下

  • match ( raw: RawLocation, current?: Route, redirectedFrom?: Location): Route
    转发了一下`this.matcher.match(…)

  • get currentRoute (): ?Route 取的this.history.current的内容

  • init (app: any /* Vue component instance */) TODO 暂时不知道这个怎么用

  • beforeEach (fn: Function): Function

  • beforeResolve (fn: Function): Function

  • afterEach (fn: Function): Function
    这三个 都是i把函数注册通过registerHook注册到对应的XXXHooks数组中,返回的是从数组中移除他们的函数

  • onReady (cb: Function, errorCb?: Function)

  • onError (errorCb: Function)
    转发了this.history上对应的方法

  • push (location: RawLocation, onComplete?: Function, onAbort?: Function)

  • replace (location: RawLocation, onComplete?: Function, onAbort?: Function)
    !onComplete 且 !onAbort 且 Promise可用时,返回promise,否则同步执行,都是调用this.history.push/replace(...)

  • go (n: number)

  • back ()

  • forward ()
    this.history.go(数值)的转发

  • getMatchedComponents (to?: RawLocation | Route): Array
    通过对to解析成一个Route,获取其matched中的所有components

  • resolve (to: RawLocation,current?: Route,append?: boolean): { location: Location, route: Route, href: string, normalizedTo: Location, resolved: Route }
    这里 实际只有三个返回, 其中 location和normalizedTo一样,resolved 和route一样, 多最后两个 只是为了 向后兼容

  • addRoutes (routes: Array)

1
2
3
4
this.matcher.addRoutes(routes)
if (this.history.current !== START) {
this.history.transitionTo(this.history.getCurrentLocation())
}

然后是两个工具函数

registerHook(list,fn), 注册一个函数fn到list, 返回它的移除函数,概念就是以c++角度看作函数指针可以搜,同时问题就是, 没有防止重复,也就是同一个函数可以 加到list两次,而移除 每次只会移除一个,所以整体还是基于函数指针,并没有完全的实现 返回移除自己的函数。不过只要正确调用就不会出问题

createHref 完整路径拼接

最后是 向Vue里注入的 window.Vue.use(VueRouter)

install.js

防重install

1
2
3
4
beforeCreate(){
注入和调用 registerInstance(this,this)
延伸到vm.$options._parentVnode.data.registerRouteInstance(this,this)
}

mixin 渲染顺序 vue的组件 前序深搜, 子组件从父组件拿

属性定义 $router(实例),$route(当前状态),并且通过Vue.util.defineReactive(this, '_route', this._router.history.current) 来保证改变时相应式触发

component定义 RouterViewRouterLink

1
2
3
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created

util

这一块的使用可以看test/unit/specs里的用例

async.js

export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {

对数组中逐个调用 函数fn(queue[index],回调),在上一个回调后调用下一个函数,最后触发cb()

用的 自定义step箭头函数 依次step(index)

dom.js

export const inBrowser = typeof window !== 'undefined'

location.js

1
2
3
4
5
6
export function normalizeLocation (
raw: RawLocation,
current: ?Route,
append: ?boolean,
router: ?VueRouter
): Location {}

_normalized来标记处理过

其它字段 path,query:resolveQuery(...),hash,name,params

misc.js

1
2
3
4
5
6
export function extend (a, b) {
for (const key in b) {
a[key] = b[key]
}
return a
}

params.js

regexpCompileCache 缓存 编译后的正则

使用path-to-regexp来完成 路径正则匹配

https://www.npmjs.com/package/path-to-regexp

1
2
3
4
5
export function fillParams (
path: string,
params: ?Object,
routeMsg: string
): string {}

path.js

比如这个文件感觉 看测试 比看代码更能理解函数功能

1
2
3
4
5
export function resolvePath (
relative: string,
base: string,
append?: boolean
): string {}

路径解析咯 甚至还”解析”了 ...

1
2
3
4
5
export function parsePath (path: string): {
path: string; // 去掉 query和 hash的部分
query: string; // 问号以后
hash: string; // 井号及以后
}

cleanPath(path: string):string ,把路径里的连续两个斜杠变为一个斜杠

push-state.js

  • 常量布尔 supportsPushState, 可以从这个源代码 看到特殊判断不支持的ua,剩余的 通过window.history是否有 pushState方法进行判断
  • function pushState (url?: string, replace?: boolean) { 依赖于 window.history .pushState/replaceState这俩个那个方法,通过replace参数决定调用哪个,如果挂掉(..) 则改为调用window.location.replace/assign(url)的方法
  • function replaceState (url?: string) { 调用封装的pushState

这里可能挂掉的注释是

1
2
// try...catch the pushState call to get around Safari
// DOM Exception 18 where it limits to 100 pushState calls

query.js

1
2
3
4
5
export function resolveQuery (
query: ?string,
extraQuery: Dictionary<string> = {},
_parseQuery: ?Function
): Dictionary<string> {}

基本就是,把url里的请求参数query 和 字典里的extraQuery,转化为 Dictionary

如果有相同的让extraQuery覆盖query里的 这里明明是可以用misc.jsextend 为何没用=。= 是有什么考虑么

然后默认内部实现了如上所述的parseQuery函数,你也可以自己实现一个传进去,从测试样例上看,是没有测这个_parseQuery

1
export function stringifyQuery (obj: Dictionary<string>): string {}

转化为url上的请求参数格式 记得encode

此外就是上面 使用了一些 url的编码解码 函数

resolve-components.js

1
2
3
4
export function flatMapComponents (
matched: Array<RouteRecord>,
fn: Function
): Array<?Function> {}

对matched的每个元素m,中m.components的每个key 调用fn(m.components[key],m.instances[key],m,key)

1
export function resolveAsyncComponents (matched: Array<RouteRecord>): Function {}

返回一个闭包函数 (to,from,next)=>{}

使用了上面的flatMapComponents

通过内部 两个变量hasAsyncpending来控制 next()

这里没有使用tofrom只有 next()matched

没有很懂的是 这里def = matched某个 的components[key],就是其某个_Vue.extend()

但是这里resolve,reject都是once

下面为什么 既有res = def(resolve,reject)又有 res.then(resolve, reject)

emmmmmmmmm所以这里正向功能 是配置所有 components[key] 之后调用 next()?

然后似乎为了兼容不同语法,写了比较神奇的

1
export function flatten (arr: Array<any>): Array<any> {}

emmm名为flatten,实际 只是 Array.prototype.concat.apply([],arr),所以最多flatten一层

比如[1,[2,[3,[4]]]] -> [1,2,[3,[4]]]

route.js

1
2
3
4
5
6
export function createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: ?Location,
router?: VueRouter
): Route {}

返回的是一个Object.freeze(Route)

1
2
3
4
// the starting route that represents the initial state
export const START = createRoute(null, {
path: '/'
})
1
export function isSameRoute (a: Route, b: ?Route): boolean {}

如果b=== START,那么 return a===START 所以不会再生成START?

对 a和b的path,name,hash,query,params 这些 需要比较的进行深比较

1
export function isIncludedRoute (current: Route, target: Route): boolean {}

目标path是当前path的前缀,目标hash为空或者和当前hash相等,目标query的key在当前key中都出现过 =.=这个规则 好迷啊

这里 有单独实现cloneisObjectEqual两个方法,都实现了深度处理,但是没有实现可能出现的循环引用。不过因为都是 Route中的 “可控制的”参数,认为是不会出现循环引用的。

scroll.js

1
export function setupScroll () {}

看得出自闭的感受了 这里三个注释 Fix #balabala

监听popstate => export function saveScrollPosition () { positionStore[当前 状态key] = 保存x,y偏移

1
2
3
4
5
6
export function handleScroll (
router: Router,
to: Route,
from: Route,
isPop: boolean
) {}

// wait until re-render finishes before scrolling

router.app.$nextTick()里执行 调用router.options.scrollBehavior.call(router,to,from,isPop?position:null) 来判断是否需要滚动,如果需要
则调用 scrollToPosition(...)

state-key.js

上面文件会用到 gen/get/set StateKey,然后 值是直接取用的 Time.now().toFixed(3)

至于Time 可能取 window.performance或者Date

warn.js

assert/warn/isError/isExtendedError

如果grep代码 发现 都是说 process.env.NODE_ENV !== 'production'

那么问题来了 为什么 不写成 只在 函数内,外部直接调用

或者说

为什么不写成,传递参数增加一个env?

再或者

详细命名出一个函数 warnNonPro

总结

Onhashchange 的触发来源

  • 修改浏览器地址 增加改变#hash
  • 修改location.href / location.hash
  • 点击锚点链接
  • 浏览器前进后退变化

hash

routeLink负责

  • 阻止默认行为 如click e.preventDefault()
  • 设置 location.hash

routeView负责

  • window.addEventListener(‘hashchange’,e=>{具体工作 比如页面渲染});

history(用的h5 api)

pushState 不触发页面刷新,只改变history对象,是同源策略保护限制的

popstate 页面组件刷新

routeLink负责

  • 阻止默认行为 如click e.preventDefault()
  • polyfill补丁,支持低版本浏览器。新版本的才有history.pushState()
  • window.history.pushState(对象,link,link);
    routeView负责
  • window.addEventListener(‘popstate’,e=>{具体工作 比如页面渲染}); 由浏览器前进后退按钮 触发,或者history方法触发

不支持pushState会降级到hash模式

examples

对照 源码里的 example再来回顾实现

在源码的examples/文件夹里

通过npm run dev来启动

basic

mode: 'history',
base: __dirname,

grep -r "base" src/ 可以回顾一下 base相关的实现

    <router-link tag="li" to="/bar" :event="['mousedown', 'touchstart']">
      <a>/bar</a>
    </router-link>
    

这一部分 回顾 link的实现 , tag不为a 则会 递归在this.$slots.default找第一个a元素,然后 绑上事件, 然后 event 是对应 在找到的标签a上的所有 on事件改为 阻止默认事件 调用
router.push/replace,

所以这个/bar鼠标点击下 就会触发, 页面上另一个/bar 要释放才会触发

那么,跟觉源码的逻辑 这样 做后会触发两次 push ,两次 transitionTo,两次 confirmTransition,然后 通过 base.js中的isSameRoute中判读是否是同一个Route,如果是 则调用ensureURL,最后调用 abort,

而 abort中 如果是 NavigationDuplicated的 错误 则不会 warn,会调用 回调函数(如果传递了),

当然 如果你已经在一个路径下 那么你点击 一个指向当前的路由 也会 走上面的逻辑(不是两次),在isSameRoute后就不会再走动

其中的to 是通过router.resolve(this.to,current,this.append)解析出的目标地址

navigateAndIncrement () {

实现了直接去调用$router.push方法 ,你可以通过浏览器的返回看到push的效果

    <router-link to="/foo" v-slot="props">
      <li :class="[props.isActive && 'active', props.isExactActive && 'exact-active']">
        <a :href="props.href" @click="props.navigate">{{ props.route.path }} (with v-slot).</a>
      </li>
    </router-link>

这一段 会对应link中的 scopedSlot, 注意到 它并没有 传递activeClassexactActiveClass而是 自己组件里 动态计算class,然后这里

这里也是把 源码中传递的所有参数都用到了

1
2
3
4
5
6
7
this.$scopedSlots.default({
href,
route,
navigate: handler,
isActive: classes[activeClass],
isExactActive: classes[exactActiveClass]
})

html最下面

  <pre id="query-t">{{ $route.query.t }}</pre>
  <pre id="hash">{{ $route.hash }}</pre>

都是 过程中计算出来的当前 Route上的参数 query

同时可以发现 当 路由改变时 所有 RouterLink 的render函数 都被重新调用

hash-mode

和basic vimdiff一下

首先 最主要的变化是 mode:'hash'

此外这里加入了 /xxx/:yyy这样的匹配

然后在router-link部分 就只使用了最基本的写法

mode也就是 直接文件history/hash.js

然后 冒号路径 同样是 link.js中render里的 router.resolve, 源码反过去搜的话是 index.js: resolve(to...) -> location.js: normalizeLocation(raw...), -> index.js:match(raw...) -> create-matcher.js:match

这里 有点问题! 虽然说index.js::matchcreate-matcher.js里都是有返回Route的,但是,在 match的一些情况下 传入的raw被更改了 比如加上了params,因为 在 normalizeLocation里 返回的location == raw,就有了 后面 match中修改改location时修改了 raw,

也就是 说 在link中 这个返回的location可能是带上 params 也可能没有,所以这是不可靠的=.=

每当这时 就会怀念 c++中的 const引用参数

然后 当点击时 是触发hash.js::push -> bash.js::transitionTo -> router.match 匹配出Route -> base.js updateRoute 跟心 current = route

其中处理冒号格式的是靠 src/util/params.js 中使用’path-to-regexp’,在 match函数中使用

这样也就是 把 计算出的params 之类的 丢到了$route

nested-routes

  1. route表中 有 name' 这样写to就可以 不用写详细路径

实现就是靠 create-matcher.jsnameMap来实现名字-> url/:xxx/yy 以及 对应的正则

emmmmm 经过调试 都在 createRouteMap中把 nameMap做好了,在 下面 record=nameMap[name]始终 都有值

路径也在create-matcher.js中的locatoin.path = fillParams合并

例如

fillParams('/parent/qux/:quxId/quux',{quxId: "1", zapId: 2})

我们可以尝试添加 fillParams /parent/qux/:quxId/quux {quxId: "1", zapId: 2}

你会发现 点击会跳转到根(如果没有 quxId) 会报warn [vue-router] missing param for named route "quux": Expected "quxId" to be defined

在url上是根但是 在 router-view上 还是 按照 name的层次渲染的

这里一个问题就是 说 name 不能重复 会报错 [vue-router] Duplicate named routes definition: { name: "quuy", path: "/parent/qux/:quxId/quuy" } 但如果不管的话, name根据不重复建立 只会保存第一个

第二个就是说 即使从xxx/:quxId 直接跳 name:'quux'也是会回到主页, 因为虽然原来 quxId有值,但树形解析 上并没有quxId的值

  1. 是嵌套的 路由表 和 嵌套的 router-view

路由表嵌套 上面1.已经说了,然后 router-view 嵌套 靠的就是上面 源码阅读中讲的 matched 的计算,见view.js

即每一层router-view渲染是通过depth 去$route.matched[depth]取值

named-routes

基本就是 router配置的时候 带name,然后 router-link 的to时 配置 name,也可以配置 params

没啥新的东西

上面nested-routes都展示了

named-view

router-view 上加上了 name

路由里配置

  components: {
    default: Baz,
    a: Bar,
    b: Foo
  }

那么在 取的时候 根据props的name 去matched.components[name]中获得

和vue实例生命周期顺序

打开codesandbox.iomain.js替换为下面,记得引入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
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
import Vue from "vue";
import VueRouter from "vue-router";
Vue.config.productionTip = false;

Vue.use(VueRouter);

const Foo = {
template:
'<div><h1>foo</h1><router-link to="/bar">Go to Bar</router-link></div>',

beforeRouteEnter(to, from, next) {
console.log("foo inner beforeRouteEnter");
next();
},
beforeRouteUpdate(to, from, next) {
console.log("foo inner beforeRouteUpdate");
next();
},
beforeRouteLeave(to, from, next) {
console.log("foo inner beforeRouteLeave");
next();
},

beforeCreated() {
console.log("foo beforeCreated");
},
created() {
console.log("foo created");
},
beforeMount() {
console.log("foo beforeMount");
},
mounted() {
console.log("foo mounted");
},
beforeDestroy() {
console.log("foo beforeDestroy");
},
destroyed() {
console.log("foo destroyed");
}
};
const Bar = {
template:
'<div><h1>bar</h1><router-link to="/foo">Go to foo</router-link></div>'
};

const routes = [
{
path: "/foo",
component: Foo,
beforeEnter: (to, from, next) => {
console.log("Foo beforeEnter");
next();
}
},
{ path: "/bar", component: Bar }
];

const router = new VueRouter({
routes
});

router.beforeEach((to, from, next) => {
console.log("global beforeEach");
next();
});

router.afterEach((to, from) => {
console.log("global afterEach");
});

new Vue({
template: '<div id="app"><router-view /></div>',
router
}).$mount("#app");

查看console

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
global beforeEach 
Foo beforeEnter
foo inner beforeRouteEnter
global afterEach
foo created
foo beforeMount
foo mounted





foo inner beforeRouteLeave
global beforeEach
global afterEach
foo beforeDestroy
foo destroyed

验证了我们上面阅读的源码,router-view 来管理了组件的渲染,虽然有的函数从代码视角写在组件内部,但实际上是vuer-router确定要渲染组件以后,才会调用vue提供的产生VNode的方法,因此,给router用的是由router来用,也就自然早于vue本身的生命周期了

个人其它收获

  1. vuex,vue-router的example 都是 用express写的
  2. vuex,vue-router的文档都是vuepress生成的
  3. vuex的开发测试 目测没有flow,vue-router的测试有用到flow,两者似乎都用到了tsc
  4. dev,测试,release 全部脚本化了
  5. 之前有不少地方建议用typeof == ‘undefined’来比较undefined,这里源码写的依然是 !== undefined来比较,感觉这些就算可有可无的建议吧(吗)
  6. 另外就是 之前有想过说 代码里尽量避免字符串 作为逻辑运算,用enum或者常量,或者常量意义的变量代替,这里看源码依然后很多case 字符串,或者字符串直接比较的。
  7. 又多了一点源码阅读经验,因为 很多源码现在都已经有自动测试了,所以在直接看 util /helper之类的 代码时,先看对应的测试代码可以 快速知道这个代码是干啥用的,再阅读代码就会更容易理解
  8. 比如query里明明是可以用misc.jsextend 为何没用=。= 是有什么考虑么
  9. Object.freeze,没有深入研究,但拉去属性可以看到writable都变成false
  10. 这里为了兼容多种语法,采用的是在具体的函数里接受+一堆if来处理,而不是提供单一标准参数让调用者控制参数。
  11. this.$scopedSlots.default({没有查到,但是 看源码中的ts 是 $scopedSlots[slot名字]=(props:any)=>ScopedSlotChildren;)
  12. 看多了源码 你会发现很多process.env.NODE_ENV !== 'production' 时会报warn,同时 也知道了如果你要负责构建打包,看似 一个字符串’production’其实是会参与逻辑
  13. functional 组件
  14. 里面实现的函数有一些潜在side-effect的 比如 match,可能会修改到raw,这种时候就会怀念C++的const引用

参考

hashchange 事件 mozilia文档

popstate

flow

base tag

TODO

看一下 mixin文档了,不然有些方法看得不太理解

  • install, beforeCreate, $options registerRouteInstance?,defineProperty

https://vuejs.org/v2/api/#optionMergeStrategies

Vue.util.defineReactive(this, ‘_route’, this._router.history.current)

pwa

vue-ssr

ivew

单元测试

nuxtjs

https://npmdoc.github.io/node-npmdoc-vue/build..beta..travis-ci.org/apidoc.html#apidoc.element.vue.util.defineReactive

19-08-26 -> 19-

https://www.runoob.com/design-pattern/design-pattern-tutorial.html

relation

创建型模式

这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。

1、开闭原则(Open Close Principle)

开闭原则的意思是:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。

2、里氏代换原则(Liskov Substitution Principle)

里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

3、依赖倒转原则(Dependence Inversion Principle)

这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。

4、接口隔离原则(Interface Segregation Principle)

这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。

5、迪米特法则,又称最少知道原则(Demeter Principle)

最少知道原则是指:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。

6、合成复用原则(Composite Reuse Principle)

合成复用原则是指:尽量使用合成/聚合的方式,而不是使用继承。

模式名称 简单概括
工厂模式(Factory Pattern) 从new变为从工厂拿具体类
抽象工厂模式(Abstract Factory Pattern) 其它工厂的工厂
单例模式(Singleton Pattern) 某作用域唯一的,存在0或1个
建造者模式(Builder Pattern) 分离不变(基本元素)和易变模块(组合基本元素的方法)
原型模式(Prototype Pattern) 重写clone方法 控制重复对象克隆的代价

工厂模式(Factory Pattern)

复杂对象适合使用工厂模式,而简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。

factory pattern

主程序 new 工厂

通过工厂方法获得有某些接口实现的实例

调用实例的接口方法

如上图,对于main来说左边框中除了 interface Shape以外 是未知的,通过调用Shape Factory来创建具体实现了Shape的实例

抽象工厂模式(Abstract Factory Pattern)

其他工厂的工厂

Abstract Factory Pattern

讲就是,通过其它工厂的工厂来创建工厂,再通过创建出的工厂来调用具体的对象创建

单例模式(Singleton Pattern)

  1. 单例类只能有一个实例。
  2. 单例类必须自己创建自己的唯一实例。
  3. 单例类必须给所有其他对象提供这一实例。

singleton pattern

实现方式

是否 Lazy 初始化:是

是否多线程安全:否

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {  
private static Singleton instance;
private Singleton (){}

public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

是否 Lazy 初始化:是

是否多线程安全:是

这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步

1
2
3
4
5
6
7
8
9
10
public class Singleton {  
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() { // 相对来说 保证了多线程安全
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

是否 Lazy 初始化:否

是否多线程安全:是

1
2
3
4
5
6
7
public class Singleton {  
private static Singleton instance = new Singleton(); // 默认初始化单例实例 但可能不会用到
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}

双检锁/双重校验锁(DCL,即 double-checked locking)

JDK 版本:JDK1.5 起

是否 Lazy 初始化:是

是否多线程安全:是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {  
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) { // 因为最多0次或1此创建 而如果使用大多数都是 != null
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

登记式/静态内部类

利用了 classloader 机制来保证初始化 instance 时只有一个线程

1
2
3
4
5
6
7
8
9
public class Singleton {  
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE; // 只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance
}
}

JDK 版本:JDK1.5 起

是否 Lazy 初始化:否

是否多线程安全:是

这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。

这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。

不能通过 reflection attack 来调用私有构造方法。

1
2
3
4
5
public enum Singleton {  
INSTANCE;
public void whateverMethod() {
}
}

经验之谈:一般情况下,不建议使用第 1 种和第 2 种懒汉方式,建议使用第 3 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 5 种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。如果有其他特殊的需求,可以考虑使用第 4 种双检锁方式。

建造者模式(Builder Pattern)

分离 常变和不变的

builder pattern

如上,具体的食物 等是不变的,而 套餐组合是变化的=.=我暂时没有想到代码中的用例

使用场景: 1、需要生成的对象具有复杂的内部结构。 2、需要生成的对象内部属性本身相互依赖。

注意事项:与工厂模式的区别是:建造者模式更加关注与零件装配的顺序。

原型模式(Prototype Pattern)

原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。

关键代码: 1、实现克隆操作,在 JAVA 继承 Cloneable,重写 clone(),在 .NET 中可以使用 Object 类的 MemberwiseClone() 方法来实现对象的浅拷贝或通过序列化的方式来实现深拷贝。 2、原型模式同样用于隔离类对象的使用者和具体类型(易变类)之间的耦合关系,它同样要求这些”易变类”拥有稳定的接口。

在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过 clone 的方法创建一个对象,然后由工厂方法提供给调用者。原型模式已经与 Java 融为浑然一体,大家可以随手拿来使用。

Prototype Pattern

结构型模式

这些设计模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。

模式名称 简单概括
适配器模式(Adapter Pattern) 解决 接口不兼容 如wine
桥接模式(Bridge Pattern) 抽象实体解耦
过滤器模式(Filter、Criteria Pattern) 抽象过滤方法 实现不同过滤器
组合模式(Composite Pattern) 树形结构
装饰器模式(Decorator Pattern) 不使用子类 包一层 增加方法
外观模式(Facade Pattern) 复杂化内部 简化对外接口
享元模式(Flyweight Pattern) 复用 大量细粒度对象
代理模式(Proxy Pattern) 中间商 抽象/管理cache等 os里常见

适配器模式(Adapter Pattern)

不兼容借口之间的桥梁

1、美国电器 110V,中国 220V,就要有一个适配器将 110V 转化为 220V。 2、JAVA JDK 1.1 提供了 Enumeration 接口,而在 1.2 中提供了 Iterator 接口,想要使用 1.2 的 JDK,则要将以前系统的 Enumeration 接口转化为 Iterator 接口,这时就需要适配器模式。 3、在 LINUX 上运行 WINDOWS 程序。 4、JAVA 中的 jdbc。

这个自己有遇到过

  1. 之前改写的oiTerminal,目的是同时兼容不同的oj 的页面访问请求 和解析
  2. 之前也有在wukong项目里看过 网路访问做的 adapter

有动机地修改一个正常运行的系统的接口,这时应该考虑使用适配器模式。

Adapter Pattern

桥接模式(Bridge Pattern)

用抽象解耦实现化

对于两个独立变化的维度,使用桥接模式再适合不过了。

Bridge

过滤器模式(Filter、Criteria Pattern)

Filter

接口是 meetCriteria

不同的过滤器不同的实现方式

组合模式(Composite Pattern)

整体部分模式,树形模式?

Composite Pattern

装饰器模式(Decorator Pattern)

现有类的一个包装

动态的给一个对象添加额外的职责

比子类更灵活 比如rust的macro的 https://cromarmot.github.io/Blog/19-08-15-rust/#Macros

Decorator Pattern

外观模式(Facade Pattern)

不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。

使用场景: 1、为复杂的模块或子系统提供外界访问的模块。 2、子系统相对独立。 3、预防低水平人员带来的风险。

Facade Pattern

享元模式(Flyweight Pattern)

抽离出大量细力度对象复用

HashMap ,如下图的circleMap

Flyweight Pattern

代理模式(Proxy Pattern)

按职责来划分,通常有以下使用场景: 1、远程代理。 2、虚拟代理。 3、Copy-on-Write 代理。 4、保护(Protect or Access)代理。 5、Cache代理。 6、防火墙(Firewall)代理。 7、同步化(Synchronization)代理。 8、智能引用(Smart Reference)代理。

写过操作系统lab代码的应该是 再熟悉不过了

Proxy Pattern

#行为型模式

这些设计模式特别关注对象之间的通信。

责任链模式(Chain of Responsibility Pattern)

1、不能保证请求一定被接收。 2、系统性能将受到一定影响,而且在进行代码调试时不太方便,可能会造成循环调用。 3、可能不容易观察运行时的特征,有碍于除错。

使用场景: 1、有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。 2、在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。 3、可动态指定一组对象处理请求。

我能想到的如nodejs中express的中间件?

自己能处理则处理否则传递给链上的下一个处理函数

Chain

链上所有的类都需要实现抽象类,如上面的AbstractLogger

命令模式(Command Pattern)

关键代码:定义三个角色:1、received 真正的命令执行对象 2、Command 3、invoker 使用命令对象的入口

command

如上 Stock是实体用的类

Order的具体实现BuyStock 和 SellStock是要对一个具体Stock操作的方法

Broker是执行者

解释器模式(Interpreter Pattern)

如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。

如之前写过的sparql解析器

Interpreter Pattern

迭代器模式(Iterator Pattern)

聚合对象内部遍历方法,但是不暴露具体内部实现

比如rust已经说做到用iter能和 for index做当相同效率的迭代器了

C++里常见的stl里各种迭代器

// 就我对迭代器使用理解来说,这里提出的缺点是个什么鬼

iterator

中介者模式(Mediator Pattern)

何时使用:多个类相互耦合,形成了网状结构。

如何解决:将上述网状结构分离为星型结构。

2、机场调度系统。 3、MVC 框架,其中C(控制器)就是 M(模型)和 V(视图)的中介者。

Mediator

备忘录模式(Memento Pattern)

存档,返回上一步,数据库事务管理

备忘录模式使用三个类 Memento、Originator 和 CareTaker。Memento 包含了要被恢复的对象的状态。Originator 创建并在 Memento 对象中存储状态。Caretaker 对象负责从 Memento 中恢复对象的状态。

MementoPatternDemo,我们的演示类使用 CareTaker 和 Originator 对象来显示对象的状态恢复。

Memento

这个东西 感觉没有什么真实实践经验

以前做过一个很简单版本管理,用的是二进制备份和恢复

也做过基于数据库的备份和恢复,但实际还是靠的数据库

所以在实践中 如果对于一个 大的内容,感觉直接备份整体 消耗是巨大的。看vim的undo tree或者 git的版本管理,这些工程实践过的东西,都是交互式但都是记录的差异

那么问题又是,如何记录差异,如果一个系统的操作是具有连带操作的,那么所有连带影响都应该被记录,而在恢复时不应该触发任何连带反应,从而设计上 感觉就这里所举的例子远远不够

观察者模式(Observer Pattern)

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

Observer

一个是 vue的 watch以及computed设计

另一个是RxJS里的 Observerable的设计

这种描述式的表述法直接代码阅读体验是会比命令式的好一些

比如 挖掘Vue的声明式的交互能力 https://www.bilibili.com/video/av37345007?from=search&seid=18134472664073504003

状态模式(State Pattern)

内部有状态,不同的状态下 不同的方法有不同的行为

使用场景: 1、行为随状态改变而改变的场景。 2、条件、分支语句的代替者。

State Pattern

对于if 比较多且复杂的时候会考虑

然后作为简单的练习有 很多算法上的,比如AC自动机等等

空对象模式(Null Object Pattern)

一个空对象取代 NULL 对象实例的检查。Null 对象不是检查空值,而是反应一个不做任何动作的关系。这样的 Null 对象也可以在数据不可用的时候提供默认的行为。

Null object

个人感觉可以看作是一种 数据的兜底行为,这个感觉要看具体是希望 抛错出去还是说用兜底行为保护,看具体业务希望

策略模式(Strategy Pattern)

Strategy

多个封装起来可以“替换”的策略类

比如复杂的决策功能而不是简单的state

模板模式(Template Pattern)

template

这样看 之前的 基类模式写的oiTerminal 应该是属于模板模式的

访问者模式(Visitor Pattern)

visitor

需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作”污染”这些对象的类,使用访问者模式将这些封装到类中。

也就是 某一些方法 不应该直接放在 类和子类中时

优点: 1、符合单一职责原则。 2、优秀的扩展性。 3、灵活性。

缺点: 1、具体元素对访问者公布细节,违反了迪米特原则。 2、具体元素变更比较困难。 3、违反了依赖倒置原则,依赖了具体类,没有依赖抽象。

结构内很少改变 但需要在对象结构上定义新的操作

J2EE 模式

这些设计模式特别关注表示层。这些模式是由 Sun Java Center 鉴定的。

MVC 模式(MVC Pattern)

mvc

Model(模型) - 模型代表一个存取数据的对象或 JAVA POJO。它也可以带有逻辑,在数据变化时更新控制器。

View(视图) - 视图代表模型包含的数据的可视化。

Controller(控制器) - 控制器作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开。

mvc pattern

业务代表模式(Business Delegate Pattern)

客户端(Client) - 表示层代码可以是 JSP、servlet 或 UI java 代码。

业务代表(Business Delegate) - 一个为客户端实体提供的入口类,它提供了对业务服务方法的访问。

查询服务(LookUp Service) - 查找服务对象负责获取相关的业务实现,并提供业务对象对业务代表对象的访问。

业务服务(Business Service) - 业务服务接口。实现了该业务服务的实体类,提供了实际的业务实现逻辑。

Business delegate

我个人里的理解是 比如 有前端和服务端的交互,那么服务端会开发一层扁平的 接口给前端使用,前端直接调用的这一层就是

隐藏了内部实现

组合实体模式(Composite Entity Pattern)

Composite

组合实体模式(Composite Entity Pattern)用在 EJB 持久化机制中。一个组合实体是一个 EJB 实体 bean,代表了对象的图解。当更新一个组合实体时,内部依赖对象 beans 会自动更新,因为它们是由 EJB 实体 bean 管理的。以下是组合实体 bean 的参与者。

  • 组合实体(Composite Entity) - 它是主要的实体 bean。它可以是粗粒的,或者可以包含一个粗粒度对象,用于持续生命周期。

  • 粗粒度对象(Coarse-Grained Object) - 该对象包含依赖对象。它有自己的生命周期,也能管理依赖对象的生命周期。

  • 依赖对象(Dependent Object) - 依赖对象是一个持续生命周期依赖于粗粒度对象的对象。

  • 策略(Strategies) - 策略表示如何实现组合实体。

没有理解到 具体的目的和解决的问题,查wikipedia有说, 消除实体之间关系 减少实体bean提高可管理性。

数据访问对象模式(Data Access Object Pattern)

数据访问对象模式(Data Access Object Pattern)或 DAO 模式用于把低级的数据访问 API 或操作从高级的业务服务中分离出来。以下是数据访问对象模式的参与者。

数据访问对象接口(Data Access Object Interface) - 该接口定义了在一个模型对象上要执行的标准操作。

数据访问对象实体类(Data Access Object concrete class) - 该类实现了上述的接口。该类负责从数据源获取数据,数据源可以是数据库,也可以是 xml,或者是其他的存储机制。

模型对象/数值对象(Model Object/Value Object) - 该对象是简单的 POJO,包含了 get/set 方法来存储通过使用 DAO 类检索到的数据。

Data Access Object

原来DAO的中文全称是这个

DAO在我的印象的java中是连接 数据库 和 java代码的

前端控制器模式(Front Controller Pattern)

前端控制器模式(Front Controller Pattern)是用来提供一个集中的请求处理机制,所有的请求都将由一个单一的处理程序处理。该处理程序可以做认证/授权/记录日志,或者跟踪请求,然后把请求传给相应的处理程序。以下是这种设计模式的实体。

前端控制器(Front Controller) - 处理应用程序所有类型请求的单个处理程序,应用程序可以是基于 web 的应用程序,也可以是基于桌面的应用程序。

调度器(Dispatcher) - 前端控制器可能使用一个调度器对象来调度请求到相应的具体处理程序。

视图(View) - 视图是为请求而创建的对象。

Front Controller

这个不是很理解 这样划分出的模式

我感觉上面没有讲到单独的dispatcher模式 ,但是有提到 业务代表模式

然后就这个图上看,我感觉 像是 业务代表模式和dispatcher模式的融合

拦截过滤器模式(Intercepting Filter Pattern)

拦截过滤器模式(Intercepting Filter Pattern)用于对应用程序的请求或响应做一些预处理/后处理。定义过滤器,并在把请求传给实际目标应用程序之前应用在请求上。过滤器可以做认证/授权/记录日志,或者跟踪请求,然后把请求传给相应的处理程序。以下是这种设计模式的实体。

过滤器(Filter) - 过滤器在请求处理程序执行请求之前或之后,执行某些任务。

过滤器链(Filter Chain) - 过滤器链带有多个过滤器,并在 Target 上按照定义的顺序执行这些过滤器。

Target - Target 对象是请求处理程序。

过滤管理器(Filter Manager) - 过滤管理器管理过滤器和过滤器链。

客户端(Client) - Client 是向 Target 对象发送请求的对象。

intercepting filter

为什么看下面的实现代码 感觉是让每一个过滤管理器 都执行一边操作

而 过滤器链更像是一系列操作函数的数组。

执行过程是让数组中的一系列操作都 对输入数据执行一遍

服务定位器模式(Service Locator Pattern)

服务定位器模式(Service Locator Pattern)用在我们想使用 JNDI 查询定位各种服务的时候。考虑到为某个服务查找 JNDI 的代价很高,服务定位器模式充分利用了缓存技术。在首次请求某个服务时,服务定位器在 JNDI 中查找服务,并缓存该服务对象。当再次请求相同的服务时,服务定位器会在它的缓存中查找,这样可以在很大程度上提高应用程序的性能。以下是这种设计模式的实体。

  • 服务(Service) - 实际处理请求的服务。对这种服务的引用可以在 JNDI 服务器中查找到。

  • Context / 初始的 Context - JNDI Context 带有对要查找的服务的引用。

  • 服务定位器(Service Locator) - 服务定位器是通过 JNDI 查找和缓存服务来获取服务的单点接触。

  • 缓存(Cache) - 缓存存储服务的引用,以便复用它们。

  • 客户端(Client) - Client 是通过 ServiceLocator 调用服务的对象

Service Locator

感觉算是上面 模式的合体了

传输对象模式(Transfer Object Pattern)

Transfer Object

传输对象模式(Transfer Object Pattern)用于从客户端向服务器一次性传递带有多个属性的数据。传输对象也被称为数值对象。传输对象是一个具有 getter/setter 方法的简单的 POJO 类,它是可序列化的,所以它可以通过网络传输。它没有任何的行为。服务器端的业务类通常从数据库读取数据,然后填充 POJO,并把它发送到客户端或按值传递它。对于客户端,传输对象是只读的。客户端可以创建自己的传输对象,并把它传递给服务器,以便一次性更新数据库中的数值。以下是这种设计模式的实体。

  • 业务对象(Business Object) - 为传输对象填充数据的业务服务。
  • 传输对象(Transfer Object) - 简单的 POJO,只有设置/获取属性的方法。
  • 客户端(Client) - 客户端可以发送请求或者发送传输对象到业务对象。

我们将创建一个作为业务对象的 StudentBO 和作为传输对象的 StudentVO,它们都代表了我们的实体。

0%