0%

背景

如果有接触过 Angular 或者 Node 服务开发,应该会知道,代码里一般是充满了装饰器的,并且利用控制反转实现了依赖注入。

这本身没什么问题,开发者写的都是一个个的 class ,至于什么时候实例化,都是交给框架去做。

但是有关注入的方式,让我怎么也想不通:

1
2
3
4
5
6
7
import HttpService from 'nestjs'

class XXXService {
constructor (
private readonly httpService: HttpService,
) {}
}

就是框架在实例化 service 的时候,如何知道要给 constructor 传入什么的

试图分析

运行时固定传入参数?

由于 HttpServiceconstructor 中是作为 ts 的类型使用的,因此在运行时 js 是不知道其类型的

这个逻辑至少是确定的(后面打脸

阅读全文 »

从 Vue 转到 React 快要一年了,虽然写页面用哪个不是搬砖?但实际经历过用两种框架搬砖,还是有所感触的。本文不是什么源码解析,也不是要分个高低,甚至会刻意避开一些技术上的细节,单纯谈谈我个人从 Vue 转 React 后的一些感受上的变化,更偏向感性的认知,并没有什么技术干货。

设计理念

“我写 Vue 的时候,从来都不会考虑这些。”

这是我刚接触一两个月时,最直接的感受。在写 Vue 代码时,不会去想这是什么副作用,有请求就调用方法,有渲染没生效就 nextTick ,也没有受控组件的概念, UI = f(State) 这个公式,仿佛离我很远。

关于受控这块,有经历过困惑的时候,也知道 Vue 内部对原生 input 标签做了处理,但还是没有写受控组件的意识。之前在写树组件时,用 v-model 去控制了 selected 属性跟 checked 属性,但是 v-model 只能控制一个属性的 valueonChange ,多出来的属性,用 v-sync 总不是很舒服,而且当时本着需要传的 Props 能少就少的想法,提供了许多组件方法,让外部通过 this.$refs.xxx() 来控制子组件内部的状态。这个版本发布不久,我就后悔了,提供了这么多方法给外部来控制内部的状态,虽然当时也没有什么状态的概念,维护起来总有种莫名的难受,也可能是响应式带来的麻烦,我即想响应 value 属性,又想响应 data 属性,来决定选中的内容。

种种结合起来,虽然在 Vue 组件里最终都能解决,但总感觉不优雅,刚发布了 2.0 版本就想再重构,却无从下手。

Vue 对各种概念、场景都尽可能封装起来,帮你做了很多事情,所以你可以不用知道许多概念,就能直接上手。而且 Vue 的限制不多,以前开发表单这种有深层对象的东西,子组件直接修改传入的 Props 更是家常便饭,虽然我们都知道这是不好的。

或许是老外不用 996 ,又或许是自己真的菜, React 能有这么一套理念,这么多的概念可以推广。

刚上手 React 的那段时间,最经常遇到的疑惑就是:

  • 怎么又无限循环了?
  • styleclassName 为什么不能直接传进去?
  • re-render 了好多次耶,不过好像没啥影响,不管了吧?
  • 状态提升太难受了吧,业务稍微变一下,内部的状态要提升到天上去了
  • 每次 re-render 都创建了新的函数、新的对象,真的没事吗?
  • 又要把状态提出来,父组件直接调用子组件的方法不香吗?
  • 在请求返回后的 then 函数里调用两次 setState , effect 执行了两次?
阅读全文 »

背景

在开发流程图时,节点之间的连线一般有三种类型:直线、曲线和折线

其中直线与曲线的实现比较简单,只要知道起点与终点就能计算出来,也不用考虑与流程图节点重叠的问题,直接穿过节点即可。

flowchart-straight-link

但是折线就不一样了,需要考虑节点避让,在合适的地方“拐弯”

flowchart-polyline-link

那么,怎么确定这些拐点的位置?如何让折线避开节点,不与节点重叠?

可行的方法

拐点的位置,肯定是与节点的位置有关,而且跟连线的起点、终点坐标有关。

枚举法

阅读全文 »

背景

在写 Vue 组件的时候,其中的 computed 里面依赖了一个对象,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new Vue({
el: "#app",
data () {
return {
date: {
start: null,
end: null,
},
}
},
computed: {
dateRange () {
this.count++
return this.date
},
},
})

以上述代码为例, dateRange computed 属性依赖了 date 这个对象,理想情况下, dateRange 会随着 date 对象内容的改变而改变。

然而,当单独改变 date.startdate.end 时,dateRange 却没有做出响应,见下面的示例

原因分析

直接看 vue2 有关初始化 computed 部分的源码:

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
const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()

for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}

if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}

// 省略
}
}

可以看到,初始化的 computed 实际上就是个不带 deepWatcher ,所以在收集依赖的时候, dateRange 只收集到了 date ,而没有收集到 date.startdate.end ,因此也就不会对这两个属性的单独变化做出响应。

解决

阅读全文 »

背景

在使用 TypeScript 时,有些场景可能需要定义一些全局变量,或者是在 window 对象底下挂一些全局变量(这两种全局变量在 TypeScript 中略有不同),这时候我们可能会在项目 src 中自己手写一些 .d.ts 文件。

但是这个全局变量要怎么写?

TypeScript 官方文档 里有这么一段话:

In TypeScript, just as in ECMAScript 2015, any file containing a top-level import or export is considered a module. Conversely, a file without any top-level import or export declarations is treated as a script whose contents are available in the global scope (and therefore to modules as well).

一个有着顶级 importexport 语句的文件被认为是一个模块,相反,则被当成一个全局作用域的脚本。

全局作用域文件

有了这句话,就很清楚了,只要避免一个文件顶级有 importexport 语句,这个文件的作用域就是全局的,即无需引入就可以使用里面的类型。

例如:

1
2
// src/global.d.ts
declare var add: (a: number, b: number) => number
阅读全文 »

问题背景

在使用 Vue + TypeScript 的时候,应该都会有个疑问,这些选项都是怎么推导出来的?特别是 props 这个选项,有着好几种形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 数组
props: ['hello']

// 对象
props: {
hello: String,
}

// 对象里面的 prop 是数组
props: {
hello: [String, Boolean],
}

// 对象里面的 prop 是对象
props: {
hello: {
type: String,
default: 'hello',
},
}

有着这么多种形式,最后还能在其他地方推导出正确的类型

props

怎么做到的?

查看源码

我们从 Vue.extend 点进去,找到 vue.d.ts 中定义的 extend

1
2
3
4
5
export interface VueConstructor<V extends Vue = Vue> {
// ...省略其他部分代码
extend<Data, Methods, Computed, Props>(options?: ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): ExtendedVue<V, Data, Methods, Computed, Props>;
// ...
}

顺着 Props 相关的类型,找到 vue 的 options.d.ts 文件中的 ComponentOptions 接口,这是我们使用 Vue.extend 时传入选项的接口。

阅读全文 »

需要弄清的问题

关于 watch ,一般有两个疑问:

  1. 怎么做到监听的属性改变时调用 handler 的
  2. deep 是怎么实现的

假设有如下组件:

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
<template>
<div id="app">
{{ foo }}
{{ testComputed }}
</div>
</template>

<script>
export default {
name: 'App',
data () {
return {
foo: 'bar',
}
},
computed: {
testComputed () {
return this.foo
},
},
watch: {
foo (newVal, oldVal) {
console.log(newVal)
console.log(oldVal)
},
},
}
</script>

从 initWatch 看起

老样子,还是从 initWatch 看起:

1
2
3
4
5
6
7
8
9
10
11
12
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}

跳转到 createWatcher

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
阅读全文 »

关于 Props

props 的处理与 data 类似,因此不再单独水一篇。

需要弄清的问题

相信看过 Vue 文档的你,在看到计算属性那一章节时,一定会对一句话产生疑问:

计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。

为什么?怎么做到依赖改变时才重新求值的?

整理出来就是:

  1. computed 一般我们写成一个函数,为何可以像一个属性一样去使用它。
  2. computed 是如何做到相关响应式依赖改变时才去重新计算求值的。

我们还是先假设一个组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div id="app">
{{ foo }}
{{ testComputed }}
</div>
</template>

<script>
export default {
name: 'App',
data () {
return {
foo: 'bar',
}
},
computed: {
testComputed () {
return this.foo
},
},
}
</script>
阅读全文 »

data 初始化时都做了什么

假设我们有这么一个 Vue 组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div id="app">
{{ foo }}
</div>
</template>

<script>
export default {
name: 'App',
data () {
return {
foo: 'bar',
}
},
}
</script>

那么这个 foo 是怎么被 Vue 监听的呢?

我们定位一下 src/core/instance/state.js 文件,找到 initData 方法:

1
2
3
4
5
6
function initData (vm: Component) {
let data = vm.$options.data
// ...省略部分代码
// observe data
observe(data, true /* asRootData */)
}

在方法末尾,可以看到 observe 方法,跳转到这个方法 src/core/observer/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}

这步重点是 new Observer ,最终会返回一个 Observer 的实例。

跳转到同一个文件的 Observer 类:

阅读全文 »

背景

有点高阶组件的感觉,把一个组件功能增强或修改组件的一些默认配置,同时外界能像原有组件那样使用新的组件。不过高阶组件是一个函数,而我们是直接写一个新的 Vue 组件,相当于是直接写这个函数的返回值了。

假设有一个 Input 组件,可以使用 v-model ,可以传 Props ,可以触发事件,可以传 Slots (包括 scopedSlots)。
但是我们在使用过程中发现,它的配置太自由了,但默认值不是我们想用的,每次使用都要配置一次,而且我们希望旁边能有个按钮,加个搜索功能。

于是我们新建一个 Vue 组件 SearchInput

Template

首先来实现新组件的 DOM 结构:

1
2
3
4
5
6
<template>
<div>
<Input/>
<button @click="handleSearch">搜索</button>
</div>
</template>

Props

然后接收 Input 组件所有的 Props ,甚至你还想加几个 Props 上去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>
<Input
v-bind="$attrs"
/>
<button
v-if="canSearch"
@click="handleSearch"
>搜索</button>
</div>
</template>

<script>
export default {
name: 'SearchInput',
props: {
canSearch: {
type: Boolean,
default: true,
},
},
}
</script>
阅读全文 »

ES6 中的 class 容易让人遗忘 JavaScript 在没有 class 的时候,是用什么骚操作去实现类似其他语言的继承的,因此再梳理一遍。

三年前

从前从前,老夫写代码还是一把梭,jQuery ,function ,$(document).ready 就是干。

到后来出社会,发现前端的需求一个比一个骚,不能再 $(document).ready 就直接开干了。

由于 JavaScript 这东西是基于原型的,必须通过一些骚操作才能实现一个类似 class 的东西,并且能够继承复用。

那时候对原型和继承的知识的掌握是必不可少的,一入职就要求要学习并输出一篇笔记,本菜鸡之前只是稍微知道有 prototype 这东西,还不明白有这么些操作,赶紧打开 《JavaScript高级程序设计》 (没错,就是这本,虽然很菜但书还是要看高级的)翻到继承那章,稀里糊涂的就记上了一篇笔记,当时应该是似懂非懂,大概懂了的感觉(然而现在已经忘得差不多了)。

原型、构造函数与实例

由于 JavaScript 是基于原型的,因此我们需要先搞清楚原型是个什么玩意儿。

说起原型,就会想到 prototype ,经常看浏览器控制台的话,还会看到一个 __proto__ 。那么这些东西,哪些是原型,其他属性又跟原型有什么关系呢?

首先,原型,就打个不恰当的比方吧,好比一个叫 Fn 的需求过来时给了你一个原型界面,诶,就把这个当成原型。

阅读全文 »

Vue 官方风格指南

在 Vue 官方的风格指南中,强烈推荐在 JS/JSX 中组件名应该是 PascalCase 的。

JS/JSX 中的组件名应该始终是 PascalCase 的,尽管在较为简单的应用中只使用 Vue.component 进行全局组件注册时,可以使用 kebab-case 字符串。

我原以为在 JSX 中组件名都写成 PascalCase 就好了,然而在使用一些 UI 组件库时,在 render 函数中常常会报某个组件未注册的问题。

例如 iView ,在其官方文档的 快速上手 章节底下,有个 组件使用规范 ,是这样写的:

在非 template/render 模式下(例如使用 CDN 引用时),组件名要分隔,例如 DatePicker 必须要写成 date-picker。

看到这个,我以为这只是这个组件库的规范,于是在报错的时候直接把报错的组件名改成 kebab-case 就好了,也就没再深究。

然而一知半解的下场就是,下次遇到同样的问题,还是会一脸懵逼。

官方示例代码大小写使用情况

我们看到 Vue 官网解释 JSX 的部分有这样一段代码:

阅读全文 »

写树组件的思路

一听说要写树组件,像我这样的菜鸡肯定都是想到,就按照树形的结构,递归来组织组件。

这个思路本身是没问题的,我之前就按照这个思路写了一版,结局嘛…如果是 happy ending 的话就不会有这篇文了。

递归组件的缺陷

先说说递归写法的优点:

  1. 结构直白,数据什么样,组件就什么样,傻瓜式组织。
  2. 父子节点相关样式控制方便,展开折叠什么的,一个 v-show 就能搞定。

然后是缺陷:

  1. 结构太直白,导致 DOM 层级深,数据层级有多深, DOM 就有多深。
  2. 父子组件通信不便,需要用 event bus , vuex 或 provide 之类的方式进行深层的通信。
  3. 数据遍历困难,如果没有预先处理,需要经常一层层往下处理数据,导致代码跟💩一样。
  4. 最重要的就是性能问题,这种类似列表的数据,量一多,渲染方面肯定先撑不住, DOM 数量多、层级深,会导致浏览器卡顿,递归遍历数据也可能导致 UI 阻塞(虽然实际上并不会, V8 很强的。

需要优化的点

根据列出的缺陷,可以简单得出一些优化关键点:

阅读全文 »

废话在前

原本为了了解一下 slot 是怎么处理的,于是看了源码,最后虽然还没看到那边,但是大概了解了 new Vue() 的过程,记录一下。

下载仓库并安装依赖

运行、在源码中打断点

vue 使用的是 rollup 打包。

scripts/config.js 中的 genConfig 方法中的 config 对象中加上

1
sourcemap: true

然后 npm run dev

会打包 vue.jsdist

阅读全文 »

寻找 Vue 函数

要知道 new Vue() 的时候发生了什么,首先要知道 Vue 定义在哪里。

我们从 package.json 开始寻找 Vue 在哪里

  1. scripts 中,可以看到在 build 时执行了 scripts/build.js
  2. 找到 scripts/build.js ,发现在构建时读取了同目录下的 config.js
  3. 找到 scripts/config.js ,根据 package.json 中的 main 字段可以知道最终入口是 dist/vue.runtime.common.js ,于是我们找 dest 为 dist/vue.runtime.common.js 对应的 entry ,发现是 web/entry-runtime.js
  4. 找到 entry-runtime.js (别名在 scripts/alias.js 中),顺着 import Vue 一直找下去
  5. 最后发现,在 src/core/instance/index.js 中定义了 function Vue

import Vue 时发生了什么

平时在写 Vue 项目时,一般都是先导入 Vue ,然后再执行 new Vue()

那么,在我们 import Vue from 'vue' 的时候都发生了什么呢,或者说,new Vue() 之前框架都做了哪些准备工作

我们从 rollup 打包的入口,也就是上一步中的 web/entry-runtime.js 开始看(单页应用的话引入的可能是 vue.esm.js ,对应入口是 web/entry-runtime-with-compiler.js ,与运行时版本相比,其 $mount 是带了编译模板功能的,这个暂时不看),会发现在 web/runtime/index.js 中,主要是在 Vue.prototype 上挂了一个 $mount 方法,这个方法,就是我们执行 (new Vue()).$mount() 时调用的方法

从当前文件接着寻找 import Vue ,可以找到 core/index.js ,在这个文件中,主要是挂载 ssr 相关的属性, $isServer, $ssrContext

接着找 import Vue ,就找到了定义 Vue 的地方, core/instance/index.js 。在 function Vue 底下对 Vue 函数执行了一些混入操作:

阅读全文 »

背景

假设你已经写好了一个 Electron 应用,想要打包为安装包分发给用户。

安装 electron-builder

这里使用 electron-builder 来完成这项光荣的任务,首先安装 electron-builder

1
yarn add electron-builder --dev

配置 package.json

如果你看过他们的官方文档就知道,下一步该配置 package.json

添加一个 build 字段

下面直接放出示例:

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
"build": {
"appId": "Your.App.Id",
"productName": "Your product name",
"directories": {
// 输出 exe 的文件夹,默认是 dist
// 如果使用 webpack 配置了输出文件夹是 dist ,这边可以选择换一个输出文件夹
"output": "output"
},
"win": { // Windows
"target": [
{
"target": "nsis", // target 默认是 nsis
"arch": [
"x64" // 如果需要打包 32 位,数组里加上 'ia32'
]
}
],
"icon": "build/icon.ico", // icon 地址
"publish": { // 发布配置,如果使用 Auto Update 应该需要配置
"provider": "generic",
"url": ""
}
},
"nsis": { // nsis 配置
// 是否非一键安装
"oneClick": false,
// 是否可以选择安装位置
"allowToChangeInstallationDirectory": true
},
"files": [ // 需要打包进 exe 的文件,具体书写规则参照文档
"dist/*",
"build/icon.ico",
"!node_modules"
]
}
阅读全文 »

说明

本文记录了从一个 package.json 开始搭建一个 Electron + Vue 项目的过程。

如果不想这么麻烦,可以直接使用 Electron-vue 项目生成脚手架,里面包含了各种开发工具,包括 Vue.js devtools, 热重载 等。

Electron 文档

init 项目

首先在根目录 npm init 一下,创建 package.json 文件

然后安装 Electron

npm i electron --save-dev

或者

yarn add electron --dev

阅读全文 »

说明

传送门:搭建Electron+Vue项目

如果使用了 electron-vue 搭建的项目,基本可以忽略本篇文章,推荐只看后面两个大标题

本文的 dev-server 参考了 electron-vue 中服务器的搭建方式

通过上一篇文章,我们搭建了一个 Electron + Vue 项目,但是每次项目启动都要执行两条命令, npm run build && npm start 。或许你会说,这就是一条命令,是的没错,webpack && electron . 的确可以在一条命令中完成。但开发过程中,一个本地 server 是必不可少的(对这类项目来说),而带上一个 server 就不能通过 xxx && xxx 这样的形式实现了,因为 server 启动的时候会进行监听,导致下一条命令无法执行。

本篇文章主要致力于实现一条命令实现启动 server + electron 并实现调试(concurrently 同学请你先坐下)。

搭建 Webpack-dev-server 实现热重载

平时我们调试 Vue 项目,启动一个 webpack-dev-server 就可以打开浏览器调试了,但是在 Electron 项目中,除了渲染进程,我们还需要启动主进程, dev-server 在项目中用来提供渲染进程的页面。

简单但麻烦的方法

webpack-dev-server 可以通过命令行启动,而主进程也是通过命令行启动,那么只要在两个不同的命令行窗口分别执行

阅读全文 »

先挂上项目链接:wsmock-js

背景

在一个前端项目开发过程中若用到了 ajax ,可以使用 jquery-mockjaxmockajax 等库来拦截 ajax 请求,并模拟服务器返回一份自定义的模拟数据给浏览器。这个过程没有发起网络请求,却模拟了 ajax 过程的行为,定义好模拟数据后,只要照常使用 ajax 即可,基本覆盖了日常使用 ajax 的需要。

那么,同样的思路,在开发过程中如果用到了 WebSocket ,也可以设法拦截 WebSocket 请求,并返回事先定义好的模拟数据,从而不用等待后端开发完成再来进行调试。

思路

要拦截 WebSocket 请求,我首先得到的思路有

  1. 通过浏览器插件拦截网络请求,并返回对应模拟数据 (未探索)
  2. 重写进行 WebSocket 连接与数据交换的方法 (阅读上述两个 mock ajax 库源码了解的思路)

退一步说,还可以搭设一个本地服务器,在开发时将请求 URL 都指向本地,进行真实的 WebSocket 流程,不过这样不太符合上述背景中 “照常使用” 的要求, URL 需要在本地跟实际 URL 中切换。

另外能想到的是在系统层面做手脚,这个没考虑得这么多。

本文按照上述第 2 点思路进行开发。

阅读全文 »

clickoutside指令介绍

iview里面有个clickoutside指令,用来处理点击元素外面的事件。

比如在cascader组件里的最外层div就用到了该指令

1
<div :class="classes" v-clickoutside="handleClose">

当用户点击了这个元素的外面,就会执行传到指令中的handleClose函数。

clickoutside指令原理

首先来看一下该指令的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default {
bind (el, binding, vnode) {
function documentHandler (e) {
if (el.contains(e.target)) {
return false;
}
if (binding.expression) {
binding.value(e);
}
}
el.__vueClickOutside__ = documentHandler;
document.addEventListener('click', documentHandler);
},
update () {

},
unbind (el, binding) {
document.removeEventListener('click', el.__vueClickOutside__);
delete el.__vueClickOutside__;
}
};

定义了bind和unbind两个钩子函数。

有关Vue自定义指令钩子函数的知识参照官方文档

阅读全文 »