场景

  • 问:为什么吾辈要使用 React?
  • 答:React 拥有更加庞大的生态,以及对 TypeScript 的更好支持。
    前者让需求实现变得更加简单,例如目前使用 Vue 做的后台管理系统使用了 Ant Design Vue 这个 UI 库,而它的上游 Ant Design 实际上官方维护的是 React 版本,而 Vue 并不是 亲儿子,导致一些问题并不像官方那么快解决。
    后者强大的类型系统能降低维护成本,虽然开发时代码添加类型会稍加工作量,但可以降低维护成本,便于后续的修改、重构,同时 IDE 对其支持是 JavaScript 无法相提并论的。
  • 问:那 React 相比于 Vue 而言有什么区别?
  • 答:更强大、复杂、酷,对于没有现代前端开发经验的人而言可能非常困难,但一旦熟悉,则会非常喜欢它。组件化(React Component/JSX)、函数式(React Hooks)、不可变(immutable)都是非常有趣的思想,理解之后确实都能发现具体使用场景。

    Vue 作者说 React + Mobx 就是更复杂的 Vue,这句话确实有道理,下面在 状态管理 那里也进行了说明,但同时,相比于 Vue + Vuex,避免引入 Redux 的 React + Mobx 将是更简单的。

  • 问:有大公司在用么?
  • 答:作为能够支撑 Facebook 这种级别公司的 Web 产品的基础,显然它拥有相当多的生产环境实践。

工程与周边生态

以下皆基于 cra(create-react-app) 创建的 ts + react + mobx + react-router + immer 技术栈进行说明,虽然完全不了解以上内容亦可,但最好了解一下它们是做什么的,下面第一次提及时也会简单说明一下。

创建项目

使用 create-react-app 创建 react 项目,但和 vue-cli 有一点明显区别:不提供很多配置,只是简单的项目生成。

状态管理

此处使用 mobx 对标,mobx 是一个状态管理库,以响应式、可变、简单方便为最大卖点。本质上可以认为为每个页面(页面内的所有组件)提供了一个全局对象,并实现了 vue 中的 computedwatch,所以 vue 的作者说 vue 是更简单的 react + mobx 确实有些道理,实际上这两个加起来能做到的事情不比原生 vue 多多少。

但它们之间也有几点不同

  • vue 基于组件级别实现的 computedwatch,而 mobx 则是全局的
  • vue 是基于组件级别自动初始化和销毁,而 mobx 则是手动的
  • vue 基于组件但也受限于组件级别,全局状态仍要使用 vuex 这种 大炮,而 mobx 此时则是统一的
  • 不使用 vuex 的情况下一些组件很难进行拆分,因为拆分后各组件的一些数据仍然需要共享和修改,这种时候单用 vue 的 data/props 就显得有些力不从心

是否需要支持 es5?

  • 是:高阶函数
  • 否:装饰器

路由

  • 递归菜单栏
  • 使用高阶组件包装路由组件 withRouter()
  • 获取当前路由信息:this.props.match
  • 使用编程式的路由导航:this.props.history
    • 注意 props 的类型变化

异步组件和 vue 稍微有点差别,虽然也是需要 import() 语法,但却需要使用高阶组件。

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
import React from 'react'

function AsyncRoute(
importComponent: () => PromiseLike<{ default: any }> | { default: any },
) {
class AsyncComponent extends React.Component<any, any> {
constructor(props: any) {
super(props)
this.state = {
component: null,
}
}

async componentDidMount() {
const { default: component } = await importComponent()
this.setState({
component: component,
})
}

render() {
const C = this.state.component
return C ? <C {...this.props} /> : null
}
}

return AsyncComponent
}

export default AsyncRoute

然后使用高阶组件包装即可

1
2
3
4
<Route
path={'/system/task'}
component={AsyncRoute(() => import('../../index/HelloWorld'))}
/>

注:高阶组件和高阶函数类似,指的是接收一个组件/返回一个组件的组件。

代码组织

  • 导出:tsx/jsx 使用默认导出,避免需要高阶函数包装的场景
  • 优先使用函数式组件
  • src
    • pages:页面级的组件
      • component:页面级组件
    • components:非页面相关的通用组件
    • assets:静态资源

命名规范

  • React
    • 组件:必须使用大写驼峰,包括使用组件亦然
    • store
      • 必须使用 .store 后缀以区分普通 ts 文件
      • 组件级 store 必须与组件名保持一致,例如 Login 组件对应的即为 Login.store.ts
  • CSS
    • css 中的 class 必须使用小写驼峰命名法,避免 css module 找不到(cra 不会自动处理转换)
    • 优先使用 .module.css 而非 .css,避免全局样式污染
    • 页面级 css 必须与组件名保持一致,例如 Login 组件对应的即为 Login.module.css
    • 非 css module 的代码必须使用 "" 而非 {''}

修改默认配置

cra 默认提供了脚本命令 reject,用于将 cra 封装的配置全部解压出来 – 当然,此操作是不可逆的!但除了这种破坏性的方式之外,也有人找到了和 vue-cli 中类似的方式,不过需要第三方包 react-app-rewired 的支持。

并在根目录添加配置文件 config-overrides.js,里面暴露出一个函数,即可修改 cra 的默认配置了。

下面是一个简单的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const WorkerPlugin = require('worker-plugin')

/* config-overrides.js */
module.exports = function override(config, env) {
//region WebWorker 配置

//do stuff with the webpack config...
config.output.globalObject = 'this'
if (!config.plugins) {
config.plugins = []
}
config.plugins.push(new WorkerPlugin())

//endregion

return config
}

重点

this 的值

class 中使用箭头函数以直接绑定当前组件的实例,尽量不要使用 function,否则 this 可能是不明确的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { Component } from 'react'

class HelloWorld extends Component {
logMsg = () => {
console.log('msg')
}
render() {
return (
<div>
<h1>hello world</h1>
<button onClick={this.logMsg}>打印</button>
</div>
)
}
}

export default HelloWorld

CSS 样式隔离

cra 创建的项目默认支持 css module,是 react 项目流行的一种 CSS 隔离方案。

使用步骤

  1. 为需要的 css 文件使用 .module.css 后缀名
  2. 通过 import styles from '*.module.css'tsx 中引入
  3. className={styles.*} 使用 class 样式

示例

1
2
3
.helloWorld {
background-color: #000;
}
1
2
3
4
5
import styles from 'HelloWorld.module.css'

export default function HelloWorld() {
return <div className={styles.helloWorld}>hello world</div>
}

注:

  • 此处的 import styles from '*.module.css' 不支持命名导入
  • 此处实现的逻辑和 Vue 是一致的,只要使用了其中一个样式 class,则整个文件都会引入
  • CSS 只要被引入了,就不会被删除,即便组件被销毁了亦然,所以页面内的 CSS 只会增加,不会减少

如何添加多个,默认使用 cra 创建的项目支持使用模板字符串

1
className={`${styles.className1} ${styles.className2}`}

看起来很丑?可以试试 classnames

1
2
3
import classNames from 'classnames'

className={classNames(globalStyles.global, globalStyles.margin)}

但仍然很丑,正如 Sindre Sorhus 所说:React 把简单的事情变复杂,复杂的事情变简单

另一种个 API 是 classNames.bind

1
2
3
import classNames from 'classnames'
const cx={classNames.bind(globalStyles)}
className={cx('global', 'margin')}

但这会让 WebStorm 损失所有的 CSS 关联,影响了包括代码提示/跳转/重构等功能,考虑到维护成本实在得不偿失。

还有人提出了 typed css module,为所有的 .module.css 生成 .d.ts 类型定义,但这会和 css in js 一样丧失 css 预处理器的优势 —- 并且,所有的工具链都需要重新支持这种关联,将之认为是 css。

参考

引入图片

在任何一个项目中,都少不了引入图片的需求。而 React 中,并未像 Vue 对 img, audio, video 这些标签进行特殊处理,以支持直接使用路径即可将对应的媒体文件打包进来,React 需要使用 import img from '*'原始形式让 webpack 知道这是一个需要打包的资源。

例如

1
2
3
4
5
import img from 'img.png'

function render() {
reutrn(<img src={img} alt="img" />)
}

而对于 SVG 类型的图片,React 支持使用组件的形式引入。

1
2
3
4
5
import { ReactComponent as IconAudio } from '../../assets/icon/icon-audio.svg'

function render() {
reutrn(<IconAudio />)
}

slot

两种方案

  1. 使用 {props.children},和 vue 的 slot:default 几乎一样,只是不能通过子组件传递参数。
  2. 如果需要传递多个命名 slot,则可以直接为 props 属性赋值为组件。例如 title={<div>hello world</div>}
  3. 如果需要使用子组件传参的话需要使用函数式组件的形式。例如 title={value => <div>{value}</div>}

吐槽:函数式已经是政治正确了。

使用函数式的 slot 时必须检查函数是否存在,如果不存在则不要调用,不像是 vue 中的 slot 是自动处理这一步的。

1
2
3
{
this.props.tableOperate && this.props.tableOperate(this.state.innerValue)
}

watch 监听 props

1
2
3
4
5
6
7
8
componentDidUpdate(prevProps: PropsType) {
// 典型用法(不要忘记比较 props):
if (this.props.value !== prevProps.value) {
this.setState({
innerValue: this.props.value,
})
}
}

生命周期

相比于 Vue 的生命周期,React 显然更加复杂混乱,而且,老实说有时候真的很难用。

例如非常常见的生命周期 componentWillUpdate,它承担的责任实在是太多了,不仅 watch state/props 中的数据要用这个,连 React Router 的路由变化同样依赖于此。

下面列出最常用的一些生命周期以及典型用例

  • render:只要 state 变化就会触发重新渲染,等价于 vue 中渲染模板 HTML 中的内容

  • shouldComponentUpdate:state 或者 props 变化前就会执行,时机上早于 render。等价于 vue beforeUpdate,但可以在这个方法内 return false 阻止视图更新。

  • componentDidUpdate:state 或者 props 变化后就会执行,时机上晚于 render。等价于 vue updated
    多用于监听一些数据变化执行一些副作用操作,但包含的种类可能会非常多。

    注:此处 vue 中将之分为 updated/watch/beforeRouteUpdate,而在 React 中,全部由 componentDidMount 承担这个责任。

  • componentDidMount:当组件渲染完成后就会执行,时机上晚于 render,等价于 vue mounted
    多用于执行一些初始化操作,除非逻辑特别简单,否则不要在这个函数里放具体执行逻辑代码,而是专门写初始化函数在这里调用。

  • UNSAFE_componentWillMount:在组件渲染完成前就会执行,等价于 vue beforeMount,但被废弃了,替代解决方案参考 怎么在没有 created 生命周期的情况下初始化数据并保证用户看不到默认空数据

  • componentWillUnmount: 组件即将被销毁前调用,等价于 vue beforeDestroy

简化 state 修改

使用 setState 很烦的一点是当你需要深度为某个属性赋值的时候,要为该属性上面所有的对象全部使用 ... 或者其他方式拷贝一遍。

例如

1
2
3
4
5
6
7
8
9
this.setState({
user: {
...this.state.user,
address: {
...this.state.user.address,
city: newCity,
},
},
})

当有多个属性需要赋值时就尤其的繁琐,而 immerjs 正好可以解决这种痛点

使用 immer 重构之后

1
2
3
this.setState(produce(this.state, draft => {
draft.user.address.city = newCity
})

代码变得很简单了,虽然看起来是直接赋值,不过 immer 使用了 ProxyObject.freeze 实现了对使用者友好的不可变数据修改,具体参考 。

React Hooks

React Hooks 是在 React 16.8 之后添加的一项新特性,一如既往,很多人又是吹的天花乱坠。老实说最开始 React Hooks 流行并且在各个社群被大肆宣传时,吾辈非常讨厌它,因为它又向函数式靠近了一步—-函数式政治正确真的很讨厌!所以,自正式学习 React 以来,吾辈都没有接触过 React Hooks,都是用 Class Component 实现组件。
但现在,架不住好奇心和一些朋友的推荐,吾辈稍微看了一下这个新特性。

它提供了两个主要的 API:

  • useState:声明一些可变状态
  • useEffect:声明一些副作用代码

使用看起来很简单

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
import React, { useEffect, useRef, useState } from 'react'

const HelloHooks: React.FC = function() {
//region 计数器

const [count, setCount] = useState(1)
const [list, setList] = useState<string[]>([])
const countAdd = () => setCount(count + 1)
useEffect(() => {
console.log('count changed: ', count)
setList(
Array(count)
.fill(0)
.map((_, i) => `第 ${i + 1} 个元素`),
)
//监听 count 变化
}, [count])

//endregion

//region 自动聚焦输入框

const inputRef = useRef<HTMLInputElement>(null as any)
useEffect(() => {
inputRef.current.focus()
//不监听任何值变化,只在第一次渲染运行
}, [])

//endregion

return (
<div>
<div>
<input
ref={inputRef}
value={count}
type="number"
onChange={e => setCount(parseInt(e.target.value))}
/>
<button onClick={countAdd}>增加</button>
</div>
<ul>
{list.map((v, i) => (
<li key={i}>{v}</li>
))}
</ul>
</div>
)
}

export default HelloHooks

可以看到,上面用 useState/useEffect 两个函数实现了一些常见的功能:state, componentDidMount, componentDidUpdateuseEffect 甚至默认支持类似 vue watch 的使用方式。

同时,使用 Hooks 可以轻易地封装出 useModel, vue 中的 watch/computed 甚至原生自带了!

  • useMemo:当依赖变化时计算属性
  • useCallback:当依赖变化时执行回调

然而,Hooks 终究不是万能。

  • 使用 Hooks 封装控制 DOM 相关的代码做不到,例如使用高阶组件实现的根据某些条件控制组件是否加载。
  • 使用 Hooks 无法实现全部的生命周期,例如 shouldComponentUpdate
  • Hooks useEffect 中调用的外部函数,无法即时获取到所有最新的 state,即便它们与 useEffect 同级,必须在 useEffect 声明正确的依赖才行,即便调用的函数在外部依赖的亦然如此(老实说这点并不简单)。示例:
  • Hooks 中 useState/useReducer 修改并不能即时反应出来。例如 const [count, setCount] = useState(0) 声明一个状态,使用 setCount(count + 1) 之后立刻取 count 的值仍然是 0,需要等到下次渲染才会生效。而 useRef 声明的可变状态,虽然是即时修改的,但组件却不会因为它的变化重新渲染。示例:https://codesandbox.io/s/react-hooks-usestate-async-change-5hlz0
  • 使用 Hooks 会让函数变得很大,对开发人员的要求比之前更高(与 vue 3 的函数式 API 一样,都是由开发者自己完全控制代码块的分割)

更多有关 React Hooks 的介绍,请参考:

常见问题

React 有什么缺点

  • CSS 局部化有很多方案,但没有一统天下的
  • vue 在组件创建/销毁时会自动初始化/销毁状态及监听器,而 mobx 会一直保留需要手动初始化/清理
    • 注: 这点还未找到解决方案
    • 更新:可以使用 context 部分解决这个问题
  • 使用 AntD 时可能遇到样式覆盖不了的问题,需要混合使用 className, style 两个属性。
  • react 会在开发阶段报错比较多,主要是一些低级错误,尤其是加上 ts 之后尤其如此

怎么在没有 created 生命周期的情况下初始化数据并保证用户看不到默认空数据

在 vue 中,吾辈经常在 created 生命周期中加载数据,避免用户看到默认的空数据,然而,React 中却没有这个生命周期,所以需要额外的处理。

注:其实 vue 中用户也有可能看到空数据,但因为 Ajax 请求数据的速度也比较快,所以默认可以不用处理。

一个可能解决方案是在指定元素外面包一层,在页面数据未加载之前,在元素上方添加一个 Loading 遮罩层提示正在加载中,等到数据加载完成后删除浮层。

下面是一个简单的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
/* ComponentLoading.module.css */
.componentLoadingDialog {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
background-color: #fff;
}
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
// ComponentLoading.tsx
import React from 'react'
import { Spin } from 'antd'
import styles from './ComponentLoading.module.css'

type PropsType = {
/**
* 是否显示 loading?
*/
isLoading: boolean
tip?: string
}

/**
* 控制 Ajax 请求未完成前某个区域不展示默认数据
* @param props
* @constructor
*/
const ComponentLoading: React.FC<PropsType> = function(props) {
const { isLoading, tip = '正在加载中。。。' } = props
return (
<div style={{ position: 'relative' }}>
{isLoading && (
<div className={styles.componentLoadingDialog}>
<Spin tip={tip} />
</div>
)}

{/*注:默认会渲染 children 组件*/}
{props.children}
</div>
)
}

export default ComponentLoading

React 和 vue 3 的对比

vue 3 新增了 Function-base 的组件,看起来很像 React Hooks,但目前仍然无法在生产中实用。

目前看来有以下缺点

  1. IDE 支持不好
  2. TS 不能解决 Vue 中的一些问题,尤其是对于模板层面简直无能为力
  3. Vue 3 函数式组件没有覆盖之前所有的功能
  4. 周边生态目前没有早期支持的迹象

关于第二和第三点,吾辈认为这是 Vue 使用模板带来的一些天然的问题,几乎不可能解决。而 React 和 TS 结合比 Vue 要完善很多,包括类型校验完全使用 TS 而非自定义运行时校验机制。

下面是 vue 作者谈及 vue 3 与其他框架的对比

参考:

开发环境代理

开发环境配置代理几乎是必用功能,相比于 vue-cli 将全部配置统一在 vue.config.js 中,cra 看起来仍是分散式的。

步骤

  1. 安装中间件 yarn add -D http-proxy-middleware @types/

  2. 创建文件 src/setupProxy.ts

  3. 编写配置

    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
    import proxy from 'http-proxy-middleware'
    const fs = require('fs-extra')
    const path = require('path')

    /**
    * 开发环境代理
    * @param app
    * @return {string|*}
    */
    module.exports = function(app: any) {
    const proxyConfig: proxy.Config = {
    target: 'https://localhost:8000',
    changeOrigin: true,
    secure: false,
    ws: true,
    pathRewrite(api: string) {
    const mockApiList = fs.readJSONSync(
    path.resolve(__dirname, './config/mockApiList.json'),
    )
    return (
    api +
    (mockApiList.some((mockApi: string) => api.includes(mockApi))
    ? '?mock=default&errCode=200'
    : '')
    )
    },
    }

    app.use(proxy('/api', proxyConfig))
    }

参考:

场景

该功能吾辈已经封装成 NPM 库 vue-url-persist

在使用 Vue SPA 开发面向普通用户的网站时,吾辈也遇到了一些之前未被重视,但却实实在在存在的问题,这次便浅谈一下 SPA 网站将所有数据都存储到内存中导致数据很容易丢失以及吾辈思考并尝试的解决方案。

参考:SPA 全称 single page application,意为 单页应用,不是泥萌想的那样!#笑哭

思维导图

首先列出为什么遇到这个问题,具体场景及解决的问题是什么?

想要解决的一些问题

  1. 刷新页面数据不丢失:因为数据都保存在内存中,所以刷新之后自然不存在了
  2. URL 复制给其他人数据不丢失:因为数据没有持久化到 URL 上,也没有根据 URL 上的数据进行初始化,所以复制给别人的 URL 当然会丢失数据(搜索条件之类)
  3. 页面返回数据不丢失:因为数据都保存在内存中,所以跳转到其他路由再跳转回来数据当然不会存在了

那么,先谈一下每个问题的解决方案

  1. 刷新页面数据不丢失
    • 将数据序列化到本地,例如 localStorage 中,然后在刷新后获取一次
    • 将数据序列化到 URL 上,每次加载都从 URL 上获取数据
  2. URL 复制给其他人数据不丢失
    • 只能将数据序列化到 URL 上
  3. 页面返回数据不丢失
    • 将数据放到 vuex 中,并且在 URL 上使用 key 进行标识
    • 将数据序列化到 URL 上,并且不新增路由记录
    • 使用 vue-router 的缓存 keep-alive

在了解了这么多的解决方案之后,吾辈最终选择了兼容性最好的 URL 保存数据,它能同时解决 3 个问题。然而,很遗憾的是,这似乎并没有很多人讨论这个问题,或许,这个问题本应该是默认就需要解决的,亦或是 SPA 网站真的很少关心这些了。

虽说如此,吾辈还是找到了一些讨论的 StackOverflow: How to hold URL query params in Vue with Vue-Router

思路

一个基本的思路是能够确定的

  1. 在组件创建时,从 URL 获取数据并为需要的数据进行初始化
  2. 在这些数据变化时,及时将数据序列化到 URL 上

思路图

然后,再次出现了一个分歧点,到底要不要绑定 Vue?

  1. 不绑定 vue 手动监听对象变化并将对象的变化响应到 URL 上
    不绑定 vue
  2. 绑定 vue 并使用它的生命周期 created, beforeRouteUpdate 与监听器 watch
    绑定 vue

那么,两者有什么区别呢?

思路 不绑定 vue 绑定 vue
优点 非框架强相关,理论上可以通用 Vue/React 不需要手动实现 URL 的几种序列化模式,可以预见至少有两种:HTML 5 History/Hash
没有 vue/vue-router 的历史包袱 不需要手动实现数据监听/响应(虽然现在已然不算难了)
可以不管 vue-router 实现 URL 动态设置,可以自动优雅降级 灵活性很强,实现比较好的封装之后使用成本很低
缺点 没有包袱,但同时没有基础,序列化/数据监听都需要手动实现 存在历史包袱,vue/vue-router 的怪癖一点都绕不过去
灵活性不足,只能初始化一次,需要/不需要序列化的数据分割也相当有挑战 依赖 vue/vue-router,在其更新之时也必须跟着更新
不绑定 vue 意味着与 vue 不可能完美契合 无法通用,在任何一个其他框架(React)上还要再写一套

最终,在这个十字路口反复踌躇之后,吾辈选择了更加灵活、成本更低的第二种解决方案。

问题

已解决

  • 序列化数据到 URL 上导致路由记录会随着改变增加
  • 即时序列化数据到 URL 上不现实

    这里吾辈对 yarn 进行了考察发现其也是异步更新 URL

  • 序列化到 URL 上时导致的死循环,序列化数据到 URL 上 => 路由更新触发 => 初始化数据到 URL 上 => 触发数据改变 => 序列化数据到 URL 上。。。
  • 同一个路由携带不同的查询参数的 URL 直接在地址栏输入回车一次不会触发页面数据更新
  • URL 最大保存数据 IE 最多支持 2083 长度的 URL,换算为中文即为 231 个,所以不能作为一种通用方式进行
  • Vue 插件不能动态混入,而是在各个生命周期中判断是否要处理的

仍遗留

  • JSON 序列化的数据长度较 query param 更大

下面是具体实现及代码,不喜欢的话可以直接跳到最下面的 总结

实现

GitHub

基本尝试

首先,尝试不使用任何封装,直接在 created 生命周期中初始化并绑定 $watch

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
<template>
<div class="form1">
<div>
<label for="keyword">搜索名:</label>
<input type="text" v-model="form.keyword" id="keyword" />
</div>
<div>
<input
type="checkbox"
v-model="form.hobbyList"
id="anime"
value="anime"
/>
<label for="anime">动画</label>
<input type="checkbox" v-model="form.hobbyList" id="game" value="game" />
<label for="game">游戏</label>
<input
type="checkbox"
v-model="form.hobbyList"
id="movie"
value="movie"
/>
<label for="movie">电影</label>
</div>
<p>
{{ form }}
</p>
</div>
</template>

<script>
export default {
name: 'Form1',
data() {
return {
form: {
keyword: '',
hobbyList: [],
},
}
},
created() {
const key = 'qb'
const urlData = JSON.parse(this.$route.query[key] || '{}')
Object.assign(this.form, urlData.form)
this.$watch(
'form',
function(val) {
urlData.form = val
this.$router.replace({
query: {
...this.$route.query,
[key]: JSON.stringify(urlData),
},
})
},
{
deep: true,
},
)
},
}
</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
/**
* 初始化一些数据需要序列化/反序列化到 url data 上
* @param exps 监视的数据的表达式数组
*/
function initUrlData(exps) {
const key = 'qb'
const urlData = JSON.parse(this.$route.query[key] || '{}')
exps.forEach(exp => {
Object.assign(this[exp], urlData[exp])
this.$watch(
exp,
function(val) {
urlData[exp] = val
this.$router.replace({
query: {
...this.$route.query,
[key]: JSON.stringify(urlData),
},
})
},
{
deep: true,
},
)
})
}

使用起来需要在 created 生命中调用

1
2
3
4
5
export default {
created() {
initUrlData.call(this, ['form'])
},
}

处理深层监听

如果需要监听的值不是 data 下的顶级字段,而是深层字段的话,便不能直接使用 [] 进行取值和赋值了,而是需要实现支持深层取值/赋值的 get/set。而且,深层监听也意味着一般不会是对象,所以也不能采用 Object.assign 进行合并。

例如需要监听 page 对象中的 offset, size 两字段

首先,需要编写通用的 get/set 函数

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
/**
* 解析字段字符串为数组
* @param str 字段字符串
* @returns 字符串数组,数组的 `[]` 取法会被解析为数组的一个元素
*/
function parseFieldStr(str) {
return str
.split(/[\\.\\[]/)
.map(k => (/\]$/.test(k) ? k.slice(0, k.length - 1) : k))
}

/**
* 安全的深度获取对象的字段
* 注: 只要获取字段的值为 {@type null|undefined},就会直接返回 {@param defVal}
* 类似于 ES2019 的可选调用链特性: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/%E5%8F%AF%E9%80%89%E9%93%BE
* @param obj 获取的对象
* @param fields 字段字符串或数组
* @param [defVal] 取不到值时的默认值,默认为 null
*/
export function get(obj, fields, defVal = null) {
if (typeof fields === 'string') {
fields = parseFieldStr(fields)
}
let res = obj
for (const field of fields) {
try {
res = Reflect.get(res, field)
if (res === undefined || res === null) {
return defVal
}
} catch (e) {
return defVal
}
}
return res
}

/**
* 安全的深度设置对象的字段
* 注: 只要设置字段的值为 {@type null|undefined},就会直接返回 {@param defVal}
* 类似于 ES2019 的可选调用链特性: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/%E5%8F%AF%E9%80%89%E9%93%BE
* @param obj 设置的对象
* @param fields 字段字符串或数组
* @param [val] 设置字段的值
*/
export function set(obj, fields, val) {
if (typeof fields === 'string') {
fields = parseFieldStr(fields)
}
let res = obj
for (let i = 0, len = fields.length; i < len; i++) {
const field = fields[i]
console.log(i, res, field, res[field])
if (i === len - 1) {
res[field] = val
return true
}
res = res[field]
console.log('res: ', res)
if (typeof res !== 'object') {
return false
}
}
return false
}

然后,是替换赋值操作,将之修改为一个专门的函数

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
/**
* 为 vue 实例上的字段进行深度赋值
*/
function setInitData(vm, exp, urlData) {
const oldVal = get(vm, exp, null)
const newVal = urlData[exp]
if (typeof oldVal === 'object' && newVal !== undefined) {
Object.assign(get(vm, exp), newVal)
} else {
set(vm, exp, newVal)
}
}

/**
* 初始化一些数据需要序列化/反序列化到 url data 上
* @param exps 监视的数据的表达式数组
*/
function initUrlData(exps) {
const key = 'qb'
const urlData = JSON.parse(this.$route.query[key] || '{}')
exps.forEach(exp => {
setInitData(this, exp, urlData)
this.$watch(
exp,
function(val) {
urlData[exp] = val
this.$router.replace({
query: {
...this.$route.query,
[key]: JSON.stringify(urlData),
},
})
},
{
deep: true,
},
)
})
}

这样,便能单独监听对象中的某个字段了。

1
initUrlData.call(this, ['form.keyword'])

参考:lodash 的函数 get/set

使用防抖避免触发过快

但目前而言每次同步都是即时的,在数据量较大时,可能会存在一些问题,所以使用防抖避免每次数据更新都即时同步到 URL 上。

首先,实现一个简单的防抖函数

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
/**
* 函数去抖
* 去抖 (debounce) 去抖就是对于一定时间段的连续的函数调用,只让其执行一次
* 注: 包装后的函数如果两次操作间隔小于 delay 则不会被执行, 如果一直在操作就会一直不执行, 直到操作停止的时间大于 delay 最小间隔时间才会执行一次, 不管任何时间调用都需要停止操作等待最小延迟时间
* 应用场景主要在那些连续的操作, 例如页面滚动监听, 包装后的函数只会执行最后一次
* 注: 该函数第一次调用一定不会执行,第一次一定拿不到缓存值,后面的连续调用都会拿到上一次的缓存值。如果需要在第一次调用获取到的缓存值,则需要传入第三个参数 {@param init},默认为 {@code undefined} 的可选参数
* 注: 返回函数结果的高阶函数需要使用 {@see Proxy} 实现,以避免原函数原型链上的信息丢失
*
* @param action 真正需要执行的操作
* @param delay 最小延迟时间,单位为 ms
* @param init 初始的缓存值,不填默认为 {@see undefined}
* @return function(...[*]=): Promise<any> {@see action} 是否异步没有太大关联
*/
export function debounce(action, delay, init = null) {
let flag
let result = init
return function(...args) {
return new Promise(resolve => {
if (flag) clearTimeout(flag)
flag = setTimeout(
() => resolve((result = action.apply(this, args))),
delay,
)
setTimeout(() => resolve(result), delay)
})
}
}

$watch 中的函数用 debounce 进行包装

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
/**
* 初始化一些数据需要序列化/反序列化到 url data 上
* @param exps 监视的数据的表达式数组
*/
function initUrlData(exps) {
const key = 'qb'
const urlData = JSON.parse(this.$route.query[key] || '{}')
exps.forEach(exp => {
setInitData(this, exp, urlData)
this.$watch(
exp,
debounce(function(val) {
urlData[exp] = val
this.$router.replace({
query: {
...this.$route.query,
[key]: JSON.stringify(urlData),
},
})
}, 1000),
{
deep: true,
},
)
})
}

引用:掘金:7 分钟理解 JS 的节流、防抖及使用场景
参考:lodash 的函数 debounce

处理路由不变但 query 修改的问题

接下来,就需要处理一种小众,但确实存在的场景了。

  • 同一个组件被多个路由复用,这些路由仅仅只是一个 path param 改变了。例如 标签页
  • 用户复制 URL 之后,发现其中的查询关键字错了,于是修改了关键字之后又复制了一次,而粘贴两次路由相同 query param 不同的 URL 是不会重新创建组件的

首先确定基本的思路:在路由改变但组件没有重新创建时将 URL 上的数据为需要的数据进行初始化

1
2
3
4
5
6
7
8
9
10
11
/**
* 在组件被 vue-router 路由复用时,单独进行初始化数据
* @param exps 监视的数据的表达式数组
* @param route 将要改变的路由对象
*/
function initUrlDataByRouteUpdate(exps, route) {
const urlData = JSON.parse(route.query[key] || '{}')
exps.forEach(exp => {
setInitData(this, exp, urlData)
})
}

在 vue 实例的生命周期 beforeRouteUpdate, beforeRouteEnter 重新初始化 data 中的数据

1
2
3
4
5
6
7
8
9
export default {
beforeRouteUpdate(to, from, next) {
initUrlDataByRouteUpdate.call(this, ['form'], to)
next()
},
beforeRouteEnter(to, from, next) {
next(vm => initUrlDataByRouteUpdate.call(vm, ['form'], to))
},
}

真的以为问题都解决了么?并不然,打开控制台你会发现一些 vue router 的警告

1
vue-router.esm.js?8c4f:2051 Uncaught (in promise) NavigationDuplicated {_name: "NavigationDuplicated", name: "NavigationDuplicated", message: "Navigating to current location ("/form1/?qb=%7B%22…,%22movie%22,%22game%22%5D%7D%7D") is not allowed", stack: "Error↵    at new NavigationDuplicated (webpack-int…/views/Form1.vue?vue&type=script&lang=js&:222:40)"}

其实是因为循环触发导致的:序列化数据到 URL 上 => 路由更新触发 => 初始化数据到 URL 上 => 触发数据改变 => 序列化数据到 URL 上。。。,目前可行的解决方案是在 $watch 中判断数据是否与原来的相同,相同就不进行赋值,避免再次触发 vue-router 的 beforeRouteUpdate 生命周期。

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
/**
* 初始化一些数据需要序列化/反序列化到 url data 上
* @param exps 监视的数据的表达式数组
*/
function initUrlData(exps) {
const urlData = JSON.parse(this.$route.query[key] || '{}')
exps.forEach(exp => {
setInitData(this, exp, urlData)
this.$watch(
exp,
debounce(function(val) {
urlData[exp] = val
if (this.$route.query[key] === JSON.stringify(urlData)) {
return
}
this.$router.replace({
query: {
...this.$route.query,
[key]: JSON.stringify(urlData),
},
})
}, 1000),
{
deep: true,
},
)
})
}

现在,控制台不会再有警告了。

封装起来

使用 Vue 插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import { debounce, get, set } from './common'

class VueUrlPersist {
/**
* 一些选项
*/
constructor() {
this.expListName = 'exps'
this.urlPersistName = 'qb'
}

/**
* 将 URL 上的数据初始化到 data 上
* 此处存在一个谬误
* 1. 如果对象不使用合并而是赋值,则处理 [干净] 的 URL 就会很棘手,因为无法感知到初始值是什么
* 2. 如果对象使用合并,则手动输入的相同路由不同参数的 URL 就无法处理
* 注:该问题已经通过在 watch 中判断值是否变化而解决,但总感觉还有莫名其妙的坑在前面等着。。。
* @param vm
* @param expOrFn
* @param urlData
*/
initVueData(vm, expOrFn, urlData) {
const oldVal = get(vm, expOrFn, null)
const newVal = urlData[expOrFn]
if (oldVal === undefined || oldVal === null) {
set(vm, expOrFn, newVal)
} else if (typeof oldVal === 'object' && newVal !== undefined) {
Object.assign(get(vm, expOrFn), newVal)
}
}
/**
* 在组件被 vue-router 路由复用时,单独进行初始化数据
* @param vm
* @param expOrFnList
* @param route
*/
initNextUrlData(vm, expOrFnList, route) {
const urlData = JSON.parse(route.query[this.urlPersistName] || '{}')
console.log('urlData: ', urlData)
expOrFnList.forEach(expOrFn => {
this.initVueData(vm, expOrFn, urlData)
})
}

/**
* 在组件被 vue 创建后初始化数据并监听之,在发生变化时自动序列化到 URL 上
* 注:需要序列化到 URL 上的数据必须能被 JSON.stringfy 序列化
* @param vm
* @param expOrFnList
*/
initUrlData(vm, expOrFnList) {
const urlData = JSON.parse(vm.$route.query[this.urlPersistName] || '{}')
expOrFnList.forEach(expOrFn => {
this.initVueData(vm, expOrFn, urlData)

vm.$watch(
expOrFn,
debounce(1000, async val => {
console.log('val 变化了: ', val)
urlData[expOrFn] = val

if (
vm.$route.query[this.urlPersistName] === JSON.stringify(urlData)
) {
return
}

await vm.$router.replace({
query: {
...vm.$route.query,
[this.urlPersistName]: JSON.stringify(urlData),
},
})
}),
{
deep: true,
},
)
})
}
install(Vue, options = {}) {
const _this = this
if (options.expListName) {
this.expListName = options.expListName
}
if (options.urlPersistName) {
this.urlPersistName = options.urlPersistName
}
Vue.prototype.$urlPersist = this

function initDataByRouteUpdate(to) {
const expList = this[_this.expListName]
if (Array.isArray(expList)) {
this.$urlPersist.initNextUrlData(this, expList, to)
}
}

Vue.mixin({
created() {
const expList = this[_this.expListName]
if (Array.isArray(expList)) {
this.$urlPersist.initUrlData(this, expList)
}
},
beforeRouteUpdate(to, from, next) {
initDataByRouteUpdate.call(this, to)
next()
},
beforeRouteEnter(to, from, next) {
next(vm => initDataByRouteUpdate.call(vm, to))
},
})
}
}

export default VueUrlPersist

使用起来和其他的插件没什么差别

1
2
3
4
5
// main.js
import VueUrlPersist from './views/js/VueUrlPersist'

const vueUrlPersist = new VueUrlPersist()
Vue.use(vueUrlPersist)

在需要使用的组件中只要声明这个属性就好了。

1
2
3
4
5
6
7
8
9
10
11
12
export default {
name: 'Form2Tab',
data() {
return {
form: {
keyword: '',
sex: 0,
},
exps: ['form'],
}
},
}

然而,使用 vue 插件有个致命的缺陷:无论是否需要,都会为每个组件中都混入三个生命周期函数,吾辈没有找到一种可以根据实例中是否包含某个值而决定是否混入的方式。

使用高阶函数

所以,我们使用 高阶函数 + mixin 的形式看看。

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
100
101
102
103
104
105
106
107
108
import { debounce, get, set } from './common'

class VueUrlPersist {
/**
* 一些选项
*/
constructor({ key = 'qb' } = {}) {
this.key = key
}

/**
* 为 vue 实例上的字段进行深度赋值
*/
setInitData(vm, exp, urlData) {
const oldVal = get(vm, exp, null)
const newVal = urlData[exp]
//如果原值是对象且新值也是对象,则进行浅合并
if (
oldVal === undefined ||
oldVal === null ||
typeof oldVal === 'string' ||
typeof oldVal === 'number'
) {
set(vm, exp, newVal)
} else if (typeof oldVal === 'object' && typeof newVal === 'object') {
Object.assign(get(vm, exp), newVal)
}
}
/**
* 初始化一些数据需要序列化/反序列化到 url data 上
* @param vm vue 实例
* @param exps 监视的数据的表达式数组
*/
initUrlDataByCreated(vm, exps) {
const key = this.key
const urlData = JSON.parse(vm.$route.query[key] || '{}')
exps.forEach(exp => {
this.setInitData(vm, exp, urlData)
vm.$watch(
exp,
debounce(function(val) {
urlData[exp] = val
if (vm.$route.query[key] === JSON.stringify(urlData)) {
return
}
vm.$router.replace({
query: {
...vm.$route.query,
[key]: JSON.stringify(urlData),
},
})
}, 1000),
{
deep: true,
},
)
})
}

/**
* 在组件被 vue-router 路由复用时,单独进行初始化数据
* @param vm vue 实例
* @param exps 监视的数据的表达式数组
* @param route 将要改变的路由对象
*/
initUrlDataByRouteUpdate(vm, exps, route) {
const urlData = JSON.parse(route.query[this.key] || '{}')
exps.forEach(exp => this.setInitData(vm, exp, urlData))
}

/**
* 生成可以 mixin 到 vue 实例的对象
* @param exps 监视的数据的表达式数组
* @returns {{created(): void, beforeRouteEnter(*=, *, *): void, beforeRouteUpdate(*=, *, *): void}}
*/
generateInitUrlData(...exps) {
const _this = this
return {
created() {
_this.initUrlDataByCreated(this, exps)
},
beforeRouteUpdate(to, from, next) {
_this.initUrlDataByRouteUpdate(this, exps, to)
next()
},
beforeRouteEnter(to, from, next) {
console.log('beforeRouteEnter')
next(vm => _this.initUrlDataByRouteUpdate(vm, exps, to))
},
}
}

/**
* 修改一些配置
* @param options 配置项
*/
config(options) {
Object.assign(this, options)
}
}
const vueUrlPersist = new VueUrlPersist()
const generateInitUrlData = vueUrlPersist.generateInitUrlData.bind(
vueUrlPersist,
)

export { vueUrlPersist, generateInitUrlData, VueUrlPersist }

export default vueUrlPersist

使用起来几乎一样简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { generateInitUrlData } from './js/VueUrlPersist'

export default {
name: 'Form1',
mixins: [generateInitUrlData('form')],
data() {
return {
form: {
keyword: '',
hobbyList: [],
},
}
},
}

看起来,使用高阶函数也没有比 Vue 插件麻烦太多。

总结

总的来说,虽然路途坎坷,不过这个问题还是很有趣的,而且确实能解决实际的问题,所以还是有研究价值的。

1984

最近看完了 1984 这本小说,在之后也补了一下电影

Youtube 正版电影

一些设定令人惊奇

  • 真理部:负责新闻、娱乐、教育、艺术
  • 和平部:负责战争
  • 有爱不:负责维持法律和秩序
  • 富裕部:负责经济事务

一些名言令人印象深刻

  • 过去被禁止,控制过去的人控制未来,控制现在的人控制过去。
  • 除了对集体的爱,没有其他的爱,与之竞争的都要被摧毁。
  • 无产阶级就像牲口一样,什么办法都没有。
  • 现代战争的本质,就是毁灭产品和人类的劳动,保持社会处于饥饿的边缘,一个分等级的社会只可能建立在贫穷和无知的基础上。
  • 栗树荫下,我出卖你,你出卖我。

里面小孩举报父母,被洗脑而不自知,或许,亦是因为一张白纸更容易染上颜色吧。

  • 低效的前进,高效的内斗

现实场景

  • 这本书可以很容易买到
  • 中文网络上没有国人影评/同人小说
  • 1984 在微博已经成为非法内容了
  • 举报父母已有现实案例(所谓的大义灭亲)

美丽新世界

  • 人工胚胎,设置命运:感觉是个恐怖的极权世界
  • 从小开始的 SEX 游戏与索麻,消费/享乐主义至上:或许是个天堂?
  • 野人区:旧时代的信仰、肮脏的生活与 美丽新世界 对比,让人真不知道哪个是天堂?
  • 野人疯了:发现自己苦苦追寻的事物、坚信的认知却一文不值时,疯掉了,天堂 对他而言也是地狱了
  • 9/10 的人口供养着 1/10 的人口是 阿尔法,无论是水上还是水下都会感到快乐,不提高产量的最大原因是为了避免闲暇时间造成资源的浪费

那种 美丽新世界野人区 真的很难说哪个更好,活到 60 岁,但一生都能保持年轻,并且 每个人属于每个人 的理念确实很厉害。

  • 要求 不快乐 的权力

疫情

花了几年营造出来的中国梦,在一个春节就破碎的不成样子了。

场景

在写杂谈 手游 [ウチの姫] 回坑感想 时,吾辈在手机上进行了多次截图,但苦于数量太多不方便使用 Telegram/QQ 之类的 IM 进行传输。在尝试了包括 FTP、Infinit、Feem、飞鸽传书这些软件之后,最终发现吾辈还是一如既往的绕远路 – Windows 本身就包含这个功能。

步骤

Windows 设置共享文件夹

Windows 在 文件管理器 => 在文件夹上右键 => 属性 中可以看到共享,接下来,设置该文件夹为共享文件夹,那么手机便能下载/上传文件到该目录中了。

  1. 点击共享
  2. 选择共享的用户
  3. 确认共享

GIF 示意图

手机上访问局域网

这里使用 ES 文件浏览器 作为客户端 App 访问 Windows 的共享文件夹。

  1. 找到 网络 => 局域网
  2. 点击 扫描
    扫描到的目录
  3. 点击扫描到的共享计算机
  4. 输入上面选择的共享用户的 Username/Password
  5. 然后就能看到共享文件夹,并且能够复制/粘贴文件了
    目录
    粘贴

总结

Windows 真的有很多相当好用的功能,只是由于功能太多导致吾辈未曾发现呢

多图预警

缘由

在月初的周末,吾辈因为最近的 一些烦心事,毫无动力的躺在床上做一条无所事事的咸鱼。偶然想起来 4 年前玩过的一个游戏:我家公主最可爱,也是吾辈至今唯一玩过的手游,念及于此,便忍不住找了一下,果不其然,国服坟头草已然几丈高了,台服亦然,唯有日服坚持下来了,到了明年便是这个游戏的第 6 个年头了。
日服 App 名字的全称是 ウチの姫さまがいちばんカワイイ - ひっぱりアクション RPGx 美少女ゲームアプリ(虽然名字意思仍然和中文一样,但看起来却没那么羞耻了呢,或许是因为看不懂日语的缘故吧),简称 ウチの姫
下面是 Google Play 页面

Google Play 首页截图

看起来很可爱,所以吾辈想着闲着也是闲着,便轻松加愉快的重新尝试了这个游戏。

玩法

刚进入游戏首页,便有两个妹纸在抱着一只,嗯,青蛙一样的生物看着我们,而这只青蛙,自然是这个游戏的男主角啦 #笑

游戏首页

进入游戏,之后的首页是这样的

进入游戏后的主页面

游戏的基本玩法很简单,就是收集公主,然后让(青蛙)王子在公主的帮助下击败魔王的弹珠游戏(然而又是一个无稽之谈 #笑)

普通回合

Boss 回合

然后,在打通关卡后,可以获得经验与一些公主,不过大多数都是特殊属性的公主,可以用来强化、进化以及觉醒的一些素材,统称为 狗粮(为什么公主被作为狗粮了 #惊哭)

获得经验

获得公主

下面是一个公主的属性面板(Status 还是 Properties 呢 #笑)

属性面板

可以看到的属性就有

  • 所属元素:存在克制关系 光 <=> 暗火 => 风 => 水 => 火...
  • 名字:公主的名字
  • 稀有度:目前真正能用的稀有度依次是 UR/UR+/UR++ => PUR/PUR+/PUR++ => XUR,一般而言稀有度越高这张卡的强度可能性就越高
  • 公主自身等级:影响 HP/攻击力
  • HP:体力值,在副本时计算的是队伍(4 位公主与 1 位外援)的总和,不过没有攻击力对强度影响大
  • 攻击力:攻击力,在副本时仅计算当前活跃公主的攻击力,对强度影响较大,因为 Boss 普遍体力非常高
  • 强度:这张卡在官方眼中的强度,强度高可能不强,但低了一定不强
  • 爱情度:影响 HP/攻击力/技能冷却回合
  • 公主队长技能:作为队长时实用的技能
  • 公主技能:能够持续几回合并有冷却时间的技能
  • 绊技能:在爱情度达到 300 后解锁,一半只能到 200
  • 附加能力属性:影响能够强化的附加能力
  • 自带防御:能够防御场上某种类型的陷阱
  • 追加属性:影响 HP/攻击力/攻击时的效果,锁住的话需要使用 才开 开锁,才与 赋值
  • 爱情度封印:影响附加能力位,爱情度 200/300 会锁住两个附加能力位,必须在爱情度到达之后次啊会解锁

是不是感觉太复杂了,吾辈刚进入游戏的时候也是这样想的,所以就去加了一个国内的 QQ 讨论群(居然还有一些人再玩这个游戏呢),以次绕过了新手时期的困惑(虽然不是新手,但已然太久没玩,而且变化也很多)

图鉴

本质上,这是一个集卡(舔卡 #大雾)游戏

下面是进入游戏必然获得几位公主:亲女儿(爱姬) + 御三家(红苍翠)

亲女儿
红
蓝
绿

然而,还有各种 卡哇伊 的公主需要通过活动副本(#肝)/抽奖机制(#氪)获得

公主 1
公主 2
公主 3
公主 4
公主 5
公主 6
公主 7

机制

当然,作为一个手游,那么自然是有体力与扭蛋(抽奖)这两种机制了,下面由吾辈来一一说明

体力值:上限与等级直接挂钩,长久来看,大致是等级的 1/3 左右,每次进入副本消耗体力值,会随着时间逐渐恢复(不会超过体力上限),但也可以用 月石 可以增加体力上限的体力值(有可能超过上限)

副本

至于为什么吾辈的体力这么多(5972/141),原因便是吾辈连续升级了几十次,每次升级就会增加以此体力上限的体力值,故而才这么多的!当然,这种重复性的工作吾辈作为一个开发者自然是写了一个 AutoJS 脚本自动刷活动,连续两天几乎没停,但也只升了 50 级左右,可见升级的困难了。

AutoJS 脚本

而扭蛋,非洲人什么话都不想说 QAQ,虽然还不到一个月,但游戏里已然氪了接近 3000 的软妹币了,更别说花费的时间与精力了,然而即便如此生存依旧苦难。
下面是目前正开着的池子的一些数据

池子的 XUR 公主
详情 1
详情 2
详情 3

可以看到,除了第一次,后面每次的抽奖都要 1w 星星,而抽到 XUR 公主的概率。。。不过是 0.01%。那么,1w 星星到底相当于多少软妹币呢?

官方商店

可以看到,即便现在有着额外送 30% 的活动,1w 星星仍然需要接近 1w 日元左右,让我们来计算一下星星的单价吧

1
11000 / 15431 / 15.64 === 0.045578692773257934

看起来似乎并不多,我们计算一下总价

1
0.045578692773257934 * (10000 * 5 + 5000) === 2506.8281025291863

那么抽 6 次得到 XUR 的概率是?

1
3 + 5 * 4 + 10 === 33

哦,看起来有点不妙了,仅仅为了一张卡抽 6 次池子,便如此大动干戈,即便如此,获得 XUR 的概率也只有 33%,这。。。(╯‵□′)╯︵┻━┻

感想

唉,不管怎样,都无法找到曾经为了游戏肝了几天时间,甚至为此压缩睡眠,看到获得了自己想要的公主之后便很快乐的感觉 – 一切都无法回到从前了!

往事凌乱

对于吾辈而言,今年发生的几件事情

  • 转型成为前端
  • 淡出 Twitter 中文圈,逐渐回归 Telegram/QQ
  • 博客写作频率逐渐降低,但 GitHub 使用逐渐变多
  • 扎起头发,便是在家中亦然
  • 换了一家 2C 的互联网公司

年前想要做的事情,如今却也不大记得了。暂且记得,曾经觉得前端日新月异的想象,现实之中却不尽然。2C 的网站甚至到了目前仍然必须支持 IE,而且前端混乱的生态也让人头疼不已,不仅是 UI 在不同框架的实现不一致,甚至连很多纯 JS 库也是直接依赖于框架了,成为了某个框架的一部分。例如 Redux,号称是通用的状态管理库,然而除了 React,没有框架真正在使用它,Vue 有自己的 VuexAngular 也有 RxJS

前端框架之间的割裂与没有使用框架之前的割裂真是天壤之别。

下半年,吾辈进入了一家 2C 的互联网公司,真的是各种加班,具体详情参考 一些烦心事。精力消耗殆尽,感觉有时间,但又不想学什么东西,陷入了某种怪圈之中。

未来渺茫

明年,吾辈最想要接触的是 Web 桌面开发,以及机器学习。

  • NodeJS 后端开发
  • Web 桌面开发 Electron
  • 机器学习 TensorFlow
  • 继续维护 Blog
  • 继续坚持读书
  • 尝试网络小说写作
  • 尝试 Youtube 视频创作

Pass 1: 感觉上,人工智能必然取代各种各样的工作,程序员或许也无法幸免于难,而最不容易被替代的,大概就是创作型的工作吧!


Pass 2:下面是一段吾辈想要写作的小说的设定集

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
# 设定集

围绕时间基点进行跳跃,逐渐向着基点流动,但却永远不会真的到达。

明线是琉璃和瑞雪的感情,暗线是探索世界的真实与否。

第一卷 现实

从琉璃在生日之前回溯了一天开始,后又回溯到出生点之时,再次开始了新的人生,直到,某个绝望的事实笼罩了她之后,心灰意冷的再次懵懵懂懂的回到了小时候。

> 百合线

第二卷 黑暗

因为上一世受到了严重的心理阴影,看着所有的努力化作泡影,琉璃再也没有做任何事情的动力了。在学校中,她变成了七大不可思议的传说之一。随心所欲,和家人的关系破裂,虽靠着两世的经验尚未有大问题,但心却早已经死了。直到,高中快要结束之时,她,参加了瑞雪的葬礼。然而,瑞雪在临终前的话语终究让死掉的心产生了悸动。

> 黑暗线

第三卷 无限

她说:无论多少次,吾辈一定会拯救你的。少女立下了拯救另一个少女的誓言,但一切真能如其所愿么?

不断重复,不断轮回,一个人终究无法推动整个世界到达能够医治瑞雪的方式。甚至在某次暴露了这个秘密,直接了被切片研究,并最终导致世界大战爆发。

第四卷 未来

再次重生的少女,不,现在已经是老婆婆的琉璃,发现世界已经到了 2060 年了。是的,她没有再次回到过去,而是活到了未来。但是,她发现了一个怪异之处:时间,似乎在倒流?但是思维却并没有,为什么其他人却很习惯的样子呢?

她,想了解这个世界。为什么时间逆流,产生了 绝对的宿命论!

> 科幻线

第四卷 幻想

结果,再次在生日之后的前一刻,琉璃的时间再次跳跃到不知名的位置,没能回到原来的世界。而且,她也变成了一块石头,女娲石。后来,琉璃几经辗转,来到了红楼梦之中。神(张恒),降临了,但他却绝望的发现这是个虚假的世界。并且,发现了她。

自此,琉璃被困在了各种 GalGame 和里番之中,虽是虚拟的世界,但却完全感同身受。

最终,琉璃的灵魂想要转移到刚出生的琉璃身上,却被张恒阻止了。最终,到了琉璃的生日,世界,破碎了。张恒这才明白了什么,但也无法阻止。

> 幻想线

第五卷 死亡

论琉璃的 999 种死法。

突然有一天,琉璃发现自己很容易遭到意外了。一开始只是出门时碰到了一只黑猫,再之后是在高楼下差点被花盆砸中,后来甚至在一个十字路口碰到了车祸现场,最后,她在洗澡时莫名其妙的死掉了。

第六卷 真相

世界只是一个实验,为了培育超级 ai 所用。然而实验结束了,准备销毁她。在将要封存一切时,张恒留下的东西让她「消失」了。

> 楚门的世界

一切都是假的,都是人造的,是某个实验计划的一部分,而她,便是这个世界中的「楚门」,但她最后,却没有选择去留的权利,唯有死亡一途。

Notion 使用体验

场景

官网

Notion 是一个基于 Web 的跨平台笔记工具,在了解过印象笔记、OneNote、为知笔记和有道云笔记之后,吾辈选择了 Notion。

之所以没有选择上面列出的几个,都有一些原因

  • 印象笔记
    • 不直接支持 Markdown
    • 编辑器体验并不好
    • 不能以目录分割笔记
  • OneNote
    • 完全不支持 Markdown
  • 为知笔记
    • 国产软件,肯定会上报数据
  • 为知笔记
    • 同上

吾辈的对笔记工具的要求也很简单

  • 允许在线/本地编辑
  • 本地编辑器体验必须要好
  • 支持 Markdown 语法
  • 搜索速度要快并且支持中文

很遗憾,虽然吾辈选择了 Notion,并不代表它满足了所有需求,只是目前存在的选择中,最适合吾辈的罢了(搜索速度和中文支持真的很硬伤)。

快捷键

  • C-B:将选定文字加粗,或者结束加粗模式
  • C-I:将选定文字变成斜体,或者结束斜体模式
  • C-E:将选定区域为行内代码,或者结束行内代码模式
  • S-鼠标滚轮:水平滚动
  • CS-L:切换夜间模式
  • C-V + Link:选中文字粘贴链接,会直接将文字变成可点击链接
  • CS-Up/Down:将当前行上移/下移
  • S-Enter:不完成当前区块的情况下换行。类似于 QQ 的回车发送消息,Shift-Enter 换行一样。
  • Tab:缩进一个 Tab
  • S-Tab:反向缩进一个 Tab
  • /:可以搜索并使用命令

快捷 Block 片段

输入后按空格即可生成的特定 Block 的字符

  • -:无序列表
  • Number:有序列表
  • []:待办事项列表
  • >:可折叠层级列表(和 markdown 语义不同了)
  • ":引用(这才是 markdown 中 > 在 notion 中的关键字)
  • ``:行内代码块
    • 必须在输入第一个 之后,输入代码,再输入第二个 才有效
    • 生成行内代码片段后,必须紧跟一个空格,如果输入之后又删除掉,再次输入就会被当成行内代码,这时候使用 C-E 可将下一个字符指定为行内代码之外的文本。
  • ```:区域代码块
    • 无法直接指定语言,必须在选择框内选择语言(鼠标)

夜间模式

通过 CS-L 进行切换,也可以在 Settings & Members 中找到 Dark Mode 这个切换。

Notion 夜间模式

缺点

  • 编辑真的很卡!

  • 性能不好

    • 大文件
    • 包含图片
    • 导入 markdown
  • 粘贴富文本链接存在问题

  • 没有列编辑

  • 没有 标签 的概念,导致分类只能依赖于目录,不能跨目录进行某种关联。例如 未完成

  • 就单纯的编辑体验上还说不上非常好,至少比起 VSCode 还有一段距离

  • 无法深度导出 PDF(需要企业版),导致想要打印资料会很不方便

  • bug

    • 从编辑器之外点击编辑器,第一次并不会获取到焦点,需要点第二次才行(只出现在 Windows 自动分屏)

    • 代码块中同时存在中文和英文时粘贴后只剩中文部分,英文代码不见了

    • 从 VSCode 的 markdown 预览区复制的区域代码块最后总是会多出空行

    • 代码高亮渲染问题,目前 TypeScript/HTML 是存在问题的

      1
      2
      3
      4
      5
      6
      // 一个用于解构 Promise 中的泛型类
      type PromiseDeconstruct<T extends Promise<any>> = T extends Promise<infer R>
      ? R
      : never
      const res = Promise.resolve(1)
      const i: PromiseDeconstruct<typeof res> = 1

感受

笔记适合作为整理资料的方式,而作为写作工具的话体验却是还差了点。

[命名规范] 介词

场景

在为变量命名时,经常需要,在某些单词之间添加介词。例如 searchByName,根据名字搜索数据。下面就介绍一下开发中所用到的介词以及相关的使用场景及示例。

介绍

  • by: **根据…做…**,表示根据或者使用的条件,例如 searchByName
  • with: **根据…与…做…**,与上面 by 的区别在于需要同时操作两个对象
    • 参考 lodash...By...With 的区别
  • to: **从…到…**,表示状态或数据结构的变化,例如 arrayToMap
  • of…的,例如 indexOf, typeof, instanceof
  • for
  • on: 在…之上
  • in: 在…之中
  • at

引用

介词 to、for、of、in、on、at、with、by、about 的用法

场景

抽刀断水水更流,举杯消愁愁更愁(虽然吾辈并不喝酒就是了)

明明说过这是一个技术博客的,然而现如今,吾辈却也需要发一些技术之外的内容了呢。。。(经受了社会的毒打

公司

公司

最近到了一家新的公司,每天早晨 6-7 点起床,然后晚上 7-8 点回来,忙忙碌碌,再无他事。

公司项目历史遗留的问题之多,想要改变问题面临的困境之难,都导致吾辈在公司的时间逐渐变多,自己生活的时间逐渐变少,所以最近别说博客都半个月不发一次,连 github 的维护都少有时间,每天都是早晨抽空维护半个小时罢了。

回到了家里,吃完晚饭,洗完澡,便已然快到了睡觉的时间。想要做点自己的事情,但又感觉没什么时间。

生活

生活

而且,现在吾辈有点控制不了时间了,每天睡觉时间也难以固定。或许因为一时兴起,便去看了一部电影,然后到了凌晨 0 点或 1 点再睡觉,便是在周末刻意进行了补觉,但也不能弥补日常精力不足的事实。

不曾被在意,不曾被了解,没有目标,没有动力。吾辈曾经把写博客作为目标,但后来知识的增长速度越来越慢,空闲时间越来越多用来咸鱼般的躺在床上,发呆地度过周末的两天。

家庭

家庭

曾经亲密的家人,现在因为一些事情,却也有了现实的矛盾。或许,家家都有难念的经,便是吾辈,也必须经历各种现实中的苦难吧。

周末有时回到家里,虽然家人现在没有说什么特别的话,但从他们的语气中,对我如今的现状,包含心疼,包含不满。如今也便只是维持现状,小心翼翼的计算每个月回去一次,便不会引起太多的波澜(无论如何亲密的人,只要待在一起的时间够久,便会发现对方的缺陷,而家人,便是能够忍受这种问题的存在)。

吾辈的妹妹已然让父母伤透了心,吾辈不想再伤害他们一次。然而屁股决定脑袋,吾辈想要的东西和他们想要的并不一致,它们的经验并不适合吾辈。娶妻生子,掌家立业,难道便是人一辈子的追求么?

未来

未来

不知道未来会变成怎样,不知道未来会发生什么。平凡的活着,不断遭受着社会的毒打。当垂垂老矣时,回顾一生,没有任何值得诉说的事迹,或许也是吾辈一种可能的结局吧

卑鄙有卑鄙者的通行证,高尚有高尚者的墓志铭。

断断续续,最近终于读完了人类简史。它是一个系列的书籍,一共三本「人类简史」,「未来简史」和「今日简史」,当然,系列的第一本「人类简史」是最出名的,吾辈也正是有所耳闻才去通读了该系列。

封面

书里太多内容,读过之后却又没有记忆下来。然而,吾辈记忆深刻的有以下几点

人类之所以成为「万物之灵」是因为想象力「虚构的故事」

国家,政府,金钱,公司,资本主义,社会主义,不过都是虚构的概念,人类相信这些「故事」,所以能够形成大规模协作,而这是其他物种所不具备的。
老实说这点确实有点违反吾辈一直以来的认知,甚至比曾经「官僚系统是人类史上最伟大的发明,能让无数互不相识的人互相协作」更有冲击性。
附:之前吾辈的对官僚系统的认知基本上是腐败和权力,而且从小接受的教育告诉吾辈人类会使用工具才是人类成为万物之灵的关键(比如能够使用「火」)。

人工智能会代替绝大多数人的工作,引起大规模失业

这点和吾辈一直以来的想法不谋而合,重复的工作迟早被人工智能替代,但书中所言的现实更加绝望——连创造性的工作,文学和艺术都未能幸免于难。
人工智能还有多久才能出现,这点谁也不好说,但能知道的是现今的 Google,MS 这些顶级的大公司都在研究人工智能,并已然初见成效(Google 搜索/翻译/微软小娜)。
虽然现在人工智能看起来还很初级,只能在某个指定领域内击败人类,而且有些还很蠢(被称为人工智障),但不得不说人工智能的发展及其迅速,甚至自动驾驶汽车已经上路很多年了。

一个人的职业会被迫需要改变

曾经,一旦确定了职业,大多都是兢兢业业做一辈子。但现在,人类的寿命增加,社会变化又如此迅速,人类讲被迫适应一次次的职业改变。毕业即失业,学习到的东西到了社会上已然过时,这便是未来的真实写照了。
老实说这点在吾辈父母身上已经初见端倪,父母四十几岁的人,却不得不改变了职业。当然,他们不喜欢这样,用他们的话说:你知道改变有多难么?出问题家里老小怎么办?
现在,父母辈的人可能几十年换一次职业,但到了我们,却未必依然。或许,不久之后,十年换一次职业都是一件正常的事情了(#无奈)。

法西斯在自身看来并不是邪恶的

很多描述法西斯的书籍都把法西斯描述的无比邪恶,而身处其中的人却很难意识到(像是现如今的国内 #笑)。
政府说什么是好的什么就是好的,权利比真相更重要,家长式的权威政府,前景难免不会变成 1984 般的反乌托邦世界。每个人都被监听,家人之间互相举报,毫无信任感可言。
现在,国内是世界上摄像头最多的地方,网络公司对隐私的侵犯也并不违反法律,这造成了国内公司的肆无忌惮,毫无底线的把数据收集用以收割韭菜,并且,报告给当局(参阅 **IDC/ISP** 数据上报规范)。

人的认知无法超越时代,即便如牛顿,爱因斯坦他们,亦然如此。这本书的所言所述,也并不代表真理,更不代表其中的一切都是对的,只是作者如此认为——每个作者都有自己的偏见