弄了一个背单词的网站——eliseos.org,或者叫 jingtu.io。(这俩名字其实是一个意思。当然这个网站不主要是为了背单词而开发的,不过目前只有背单词的还能用。)
它是 Angular 开发的,同时用了一些时新的技术,比如前端的 redux-observable, graphql, apollo client,后端的 inversify, sequelize-typescript,另外我还自己搞了一些用于自动生成 graphql schema 的 decorator——
@Table export class Language extends NoPrimaryOrdinaryModel<Language> { @Default(DataType.UUIDV1) @Column @GraphQLFieldToType id: string; @PrimaryKey @Column @GraphQLFieldToType name: string; @GraphQLFieldJsonLike(() => Kv) @Column(DataType.JSONB) namesInLang: KvInterface; @HasMany(() => LanguageOfLanguage, 'languageName') languageOfLanguages: LanguageOfLanguage[]; }
是不是有点像 Java 了?这么写就会生成一个叫 Language 的表,有这么些 column 和 association,这部分是 sequelize-typescript 的功能,还会生成一个叫 Language 的 graphql schema,这部分是我自己写的。难也不难,主要是提供一种思路。
另外写的过程中我也理解了为什么 sequelize-typescript 的 decorator 参数里,类型是 lazy 的(即以上代码里的 () => LanguageOfLanguage。这是因为如果不 lazy,出现类的循环引用的时候,有一方会变成 undefined。我碰到了这个问题,所以我自己也用了 thunk 形式作为参数。
然后对于 query 也有这么些个 decorator,比如——
@GraphQLQueryField(userGraphqlType, { token: { type: new GraphQLNonNull(GraphQLString) } }, 'Checks if a token is valid. Returns the validated user.') async checkToken(parentValue: any, {token}: { token: string }, context: ContextInterface) { return await context.services.userService.checkToken(token); }
其实也没什么学问,主要是我看着原来 graphql-js 的 object literal 结构太不稳当了,嵌套一深,随便哪里类型写错了,TypeScript 就抛一大堆错(其实挺稳当的,只要你把 object literal 拆分开来,每一级都标注类型,报错就不会大规模传播)。
用上了 decorator,顺带用了 inversify——一个依赖注入管理器,以及 TypeScript 的一套东西,谁说 Node.js 不可以工程化开发呢?
至于前端,也是从 TypeScript 中获益良多。Angular 原生就是依赖 TS 的,也是原生就依赖 decorator 的,也是原生就依赖注入的。这么看 JS 生态系统前后端的技术选型已经趋同了,我相信这是好的趋势。
还有什么要说的话,就是我把以前答过的Axurez:如何评价 TypeScript 最新加入的 Discriminated union type?这个定义 tagged union 的简便语法,真的给用到了实战里——不仅仅是定义 redux actions,而且我写了一个可以深度 get 和 set 的 Map,因为要用递归指涉自身,还必须用到 tagged union。这个类型如下:
export type RecMap<K, V> = Map<K, { type: 'V', value: V } | { type: 'M', map: RecMap<K, V> }>;
整个弄下来的感想就是,Angular 是真的好用,Angular 生态是真的不错,universal 完全按官方走一遍就活了,现在线上运行的版本就是 universal 的,右键查看源码可以看到是渲染好的页面发过来的。angular cli 一路可以 generate 到底,基于 NgModule 的路由懒加载也是开箱即用,不需要任何配置,非常美妙。
还有一点就是,还是纯 css 库比较稳妥,带 JS 的反而不行,我用了 material design components 和 material design lite(因为前者没实现 chip……),还是把 js 拆了用的。一方面是它带的 JS 不一定合你的意,另一方面是它哪怕是用了你用的框架,它的 API 设计也不一定合你意(这也是我不用 Angular Material 的原因)。另外用了 Angular 这样的框架,它封装的那点功能你自己封装也要不了多久。
如果说还有一点感想,那就是我好像有点达成以前的目标了。以前总是感觉 sequelize 要写一套定义,要写一套接口,graphql 还要写一套定义,来来回回同样的东西要写好多遍。现在借助 decorator 真的实现了只写一遍。现在还有 sequelize-auto-migrations 这样的东西,可以自动根据模型的更改生成 migration,简直不要太省劳动力。
说了这么多关于开发的功能,那么这个网站怎么用呢?(推荐桌面使用,虽然移动端也完全可用)
关于背单词功能,如下图,点击右侧的「巴别」(巴别,用 wikipedia 的人应该见过,典出巴别塔,我用来指代和语言相关的模块)中的「学习」,就来到了学习界面(同时在线统计是用 socket.io 实现的,不过用户数量目前并不对,页面是对的。这里有个彩蛋,没网的时候 logo 就会黑掉):
你可以在右侧的「语料」里粘贴进英文文章,然后点击「使用这篇文章」:
它会把文章跟服务器同步,解析出你认识的、不认识的、没决定过的词汇,分别标上白色、红色和绿色:
正如提示所说的
在左边粘贴文章,并点击按钮,来提取文章中的单词。
绿色表示状态未知的单词,划过标记为认识,单击标记为不认识,再单击标记为认识,以此类推。
你可以在概览中回顾不认识的单词。
照着做就行了。我还弄了一个功能就是添加和查看单词释义。把鼠标悬停在不认识的(即红的)单词上,会出现悬浮框:
点击加号就会出现文本框,可以添加释义,以及选择释义的语言(这个网站处于我个人的恶趣味,是支持了四种语言的,用右上角的语言选单可以极速切换语言)。添加完之后,或者本来就已经有人添加过,就会是这个样子:
本来应当是可以代理一个其他网站的 API,或者干脆服务器上搞一个词库的,实际上应该是可行的,但我还是想先试试 UGC 一段时间(估计效果不好)。添加释义的另一个好处就是可以在个人主页上显示……比如我的主页:
个人主页怎么进?登陆之后右上角的用户名可以直接点击:
或者鼠标悬停,在下拉菜单里选「个人档」也行。
如果你误操作了,一方面你可以再次点击单词,在白色和红色之间切换,另一方面也可以在右下角看到所有红色、白色词汇的列表,点击就可以复归「未确认」状态,即绿色。
确认单词为未知之后,不仅单词会被同步到服务器,单词所在的句子也会被同步到服务器,在右侧的「巴别—概览」中可以回看:
未知单词在句子中的未知也会标红。(这里本来是给单词加上 html 标签,然后渲染 html 的,但是会有注入的危险,所以最后强行把句子 split 成一个结构体,用 ngFor + ngIf 渲染了)
以上大概就是目前能用的内容。设置的个人档也可以更新
还可以生成邀请链接,推广注册。
如果遇到什么 bug,请一定联系我。
至于为什么要做这个网站?这个网站除了背单词之外,还是干什么的?就如关于页面所说:
Eliseos 就是「极乐净土」的西班牙语形式。我用西班牙语是因为其他语种类似的域名都注册了(elysium, elysian, elysion 之类的)。其实我原来有个 elysion.tech,但是 .tech
太冷门了,很多地方识别不出来。这网站还有个域名,叫「净土」—— jingtu.io。
根据维基百科:
至福乐土(Elysium)或是乐土平原(Elysian Fields)(古希腊语:Ἠλύσιον πεδίον,Ēlýsion pedíon,音译:伊利西恩、伊利西昂。中文翻译上亦可名为归静乐土或是归净乐土,意为回归安静或回归纯净的乐土。)是一个来世观的理念,随著时间的推移并由一些希腊的宗教教派的仪式与哲学流派所延续发展。
我最早是在《圣斗士星矢》里接触到这概念的。在这里,我想用它指代这个网站,是因为我想做一个能进行深刻思想交流(包括语言、知识)的地方。