标签: mywordle.org

  • 复盘 mywordle.org

    复盘 mywordle.org

    去年,有位开发者设计了一个填字游戏 wordle,取得了巨大的成功,最后被纽约时报斥资百万收购。就像众多成功产品一样,wordle 也有很多追随者和模仿者,其中就包括我们做的 mywordle.org

    刚上线时,因为优化得当,排名不错;如今,随着 wordle 游戏的关注度消退,这个产品已经趋于平静,访问量跌入谷底。于是写篇文章总结下技术、产品、运营方面的经验得失。

    技术向

    技术栈

    之前发过笔记:使用 Vite+Vue3+TypeScript+Tailwind CSS 开发 Wordle

    • Vue3 + Vue-router + Vuex
    • Vite
    • TypeScript
    • TailwindCSS
    • 骨架屏
    • nginx
    • i18n (编辑本地 json)
    • PWA

    纯静态页面,通过构建脚本一次性发布,后面就不需要服务器运算,只需要 CDN,运维成本很低,容易扩展。

    前端通过适当的分包实现按需加载,加快打开速度,提升用户体验。实际效果不错,搜索排名和留存都相当好。Lighthouse 一度基本满分。

    挑战0:全新技术栈

    项目启动时,Vite、Vue3、TypeScript 的内容不算很多,技术生态也没有完全适配,花了不少时间去学习。不过好在当时有时间,慢慢也捋顺了,虽然有一些问题到今天也没能妥善解决,但并没有影响整体进度。

    挑战1:多语言多模式共存

    当时存在两个模式:

    • hourly,每小时一个词
    • unlimited,随便玩

    以及十几种语言。因为我们是静态网站,想实现 /:lang/:type 和 /:type/:lang 共存,就要同时打包这么多组目录组合。如果将来又增加其它类型,就还要成倍增加。但是几个目录里的内容又是完全一样的,很浪费。

    现在想想,应该通过 nginx 来解决这个问题,不要放在前端构建脚本端。

    挑战2:WebRTC

    我们准备尝试用 WebRTC 实现多人对战,如果能成的话,将来还有很多应用场景。可惜 WebRTC 比我想象中复杂很多,不是抽点时间看看文档就能搞定的。当时我已经开始在 code.fun 的全职工作,时间不如启动时充足。

    于是此功能最终停留在 demo,未能整合进产品,更别提上线。

    未解决问题

    • import 类型的时候必须 import type { xxx } from '@/some/types',经常出错
    • TailwindCSS 添加新样式时无法即时生效,需要等下一次更新,或者手动刷新页面
    • ESLint 有很多误报,主要是 <script setup> 导致的未使用变量问题

    产品/运营向

    原始版 wordle 一天只能猜一次,一个词。很多玩家感觉不过瘾,所以搜索 wordle unlimited 就很多,我们也是那会儿做好,然后优化得当,排名很靠前,Google 前4(最好前3),吃了不少流量。

    但是很快,wordle unlimited 的搜索量就下降了,到现在跌了90%,只有一成。

    这是流量统计,外国人也是上班摸鱼,周末不玩页游 [Facepalm]

    这部分流量流去了 quodle 关键词,还是这个猜词游戏,但是一次猜 4 个词,更考验技巧和统筹能力。

    有趣的是,quodle 主流分两个模式:daily 和 practice。daily 还是一天一次,用的是高频词,比较好猜;practice 相当于我们做的 unlimited,不限次数,但是,用的是全部词库,几乎猜不到。

    本来有两个方向,quodle 和 pvp,我说服朋友搞 pvp:我说 quodle 这种玩法太硬核了,没人爱玩;pvp 用 WebRTC 搞联机,成本低效果好,用户粘性大。结果还是我的锅,以前没搞过 WebRTC,搞了两周没搞出来,工作一忙就扔掉了……

    总结,wordle 是个比较轻量的游戏,玩法简单,可玩性有限。daily 模式可能更合适;unlimited 有些饮鸩止渴,快速消耗掉了玩家的热情。quodle 的开发者注意到这一点,一方面提供新的玩法刺激用户,另一方面利用全量词库难以游戏的特点尽量将玩家留在每天一次的游戏里。

    总结

    从这个项目中,我学到很多技术之外的知识,产品和运营方面都刷新了我的知识边界、扩展了我的视野,很有意思。

    比如,mywordle.org 是纯静态网站,运维只需要 CDN,加上优化得当,费用很低。凭借早发优势和一些 SEO,流量和广告收入都不错。即使以我的工资标准和开发习惯(动不动重构、选择新技术栈,等)来支付开发费用,也能达到不错的收益结果(目前折算 1/3 吧)。后期收入虽低,但很稳定,也不需要继续投入研发成本,而且还能作为高质量搜索导入来源。以后我应该多做几个类似的网站。

    项目启动时,我刚被金山开除,于是可以投入大量的时间去学习使用没接触过的技术栈;后面开始继续全职工作了,就只有一点时间可以支配,以至于 WebRTC 都没能用上。不知道将来还有没有类似的机会——我的心情很矛盾,既希望有,又希望没有,哈哈。

  • 笔记:使用 Vite+Vue3+TypeScript+Tailwind CSS 开发 Wordle

    笔记:使用 Vite+Vue3+TypeScript+Tailwind CSS 开发 Wordle

    最近有个小游戏很火,叫 Wordle,是个填字游戏,推上随处可见相关分享。各种衍生版也层出不穷。有位朋友让我帮他复刻,反正过年,闲着也是闲着,我就尝试用标题里的新技术栈帮他写了一个。现在已经上线了,对填字游戏感兴趣的同学可以试试:

    My Wordle(特色:1. 支持自定义单词;2. 5~9字母;3. 无限模式,可以一直玩。)

    这篇文章简单记录一些开发过程和经验。

    0. 创建 vite 项目

    Terminal
    npm init vite@latest

    接下来按照提示选择喜欢的配置即可。

    小配置下 vite.config.js,添加 @ 别名:

    vite.config.js
    // 为了方便后面开发,改为函数式,这样获取状态更容易 export default defineConfig(({command, mode}) => { const isDev = command === 'serve'; // 这里先留个例子,后面骨架屏有用 return { server: { host: true, // 允许外部访问,方便手机调试 }, plugins: [ vue(), ], resolve: { alias: { '@': resolve(__dirname, './src'), }, }, }; });

    注意,上面的配置文件虽然是 ESM,但实际上仍然在 CommonJS 环境里执行,所以有 __dirname

    然后安装依赖:pnpm i

    最后 npm run dev 即可启动开发服务器进行开发。Vite 真是快,空项目 100ms 就启动了。

    1. 安装+配置 Tailwind CSS

    Terminal
    pnpm i -D tailwindcss postcss autoprefixer npx tailwindcss init -p

    配置 tailwind.config.js

    tailwind.config.js
    module.exports = { content: [ "./index.html", // 注意:因为我喜欢用 pug 开发模版,所以一定要在这里添加 `pug` "./src/**/*.{vue,js,ts,jsx,tsx,pug}", ], theme: { extend: {}, }, plugins: [], // 这部分 css 需要通过比较复杂的计算得出,所以要用 `safelist` 保证它们出现在最终 css 里 safelist: [ { pattern: /w-\d+\/\d+/, }, { pattern: /grid-cols-[5-9]/, }, ], }

    接下来创建 CSS 入口文件:./src/style/index.css

    index.css
    @tailwind base; @tailwind components; @tailwind utilities;

    最后在入口文件里引用它即可:

    main.ts
    import { createApp } from 'vue' import App from './App.vue' import './index.css' createApp(App).mount('#app')

    2. 配置 TypeScript

    默认的 TypeScript 就挺好使,这里简单配一下 @ 别名即可:

    tsconfig.json
    { "compilerOptions": { "paths": { "@/*": [ "./src/*" ] } }, }

    为开发方便,还需要装一些类型定义:

    Terminal
    pnpm i @types/gtag.js @types/node -D

    3. 配置 eslint

    安装所需依赖:

    Terminal
    pnpm i -D @typescript-eslint/eslint-plugin @typescript-eslint/parser @vue/eslint-config-typescript eslint eslint-plugin-babel eslint-plugin-vue

    配置 .eslintrc.js

    .eslintrc.js
    module.exports = { "env": { "browser": true, "es2021": true, "node": true }, parser: "vue-eslint-parser", parserOptions: { parser: "@typescript-eslint/parser", sourceType: 'module', ecmaVersion: 'latest', }, extends: [ 'eslint:recommended', 'plugin:vue/vue3-recommended', '@vue/typescript/recommended', ], plugins: [ 'babel', 'vue', ], globals: { // vue3 <script setup> 工具函数 withDefaults: true, defineProps: true, defineEmits: true, defineExpose: true, // 我自己定义的常用类型 Nullable: true, Timeout: true, $root: true, }, "rules": { // vue3 <script setup> 暂时必须关掉这几项 '@typescript-eslint/no-unused-vars': 0, '@typescript-eslint/ban-ts-comment': 0, '@typescript-eslint/no-non-null-assertion': 0, }, }

    4. 拆包与懒加载

    Wordle 游戏需要用到词库,5个字母的词库 200+KB,6789的词库都是 300KB,全部放到一起加载太浪费,所以计划按需加载。另外,词库几乎是不变的,但业务逻辑是多变的,如果不拆包的话,每次业务逻辑变更后,用户都要重复下载好几百 KB 的词库,所以要做拆包与懒加载。

    拆包方面,vite build 是基于 rollup 实现的,所以要按照 rollup 的要求,用 rollupOptions 来配置:

    vite.config.js
    export default defineConfig(({command, mode}) => { return { .... build: { rollupOptions: { output: { manualChunks(id) { // fN 是高频词,出题时只用高频词,校验单词时才用全量词库。高频词独立打包。 if (/[sf]\d\.txt\?raw$/.test(id)) { return 'dict'; // 分享文案,为方便朋友修改,独立打包 } else if (/share\d\.txt\?raw$/.test(id)) { return 'share'; // 其它依赖,打包成 vendor。不知道为什么,必须有这一配置,前面两项才能生效 } else if (id.includes('node_modules')) { return 'vendor'; } }, }, }, }, }; });

    默认只加载高频词,全量词库会在页面准备就绪后,通过 fetch API 异步加载。

    5. 添加骨架屏

    网页里有个静态 footer,因为 CSS 文件加载需要一些时间,所以会先渲染出来,然后再挪到正确的位置,造成抖动。这里有两个选择:1. 隐藏 footer,或挪到 vue 里;2. 添加加载中样式。我选择在(2)上再发展一点,做骨架屏。

    我的思路是:

    1. <div id="app"></div> 里填充只有标题的 header 和填字格
    2. 拆分 tailwind.css,分成
      1. 只包含 reset 和 index.html 所需的 inline css
      2. 包含全部业务样式的外部 css 文件
    3. 编写脚本,build 时往 index.html 里塞入填字格,并把 inline css 写入网页 <head>

    实现方面,首先编辑 vite.config.js

    vite.config.js
    export default defineConfig(({command, mode}) => { // 根据开发、构建环境,使用不同的 tailwind 入口文件 const tailwind = command === 'serve' ? 'tailwind-sketch.css' : 'tailwind.css'; return { resolve: { alias: { '@tailwindcss': resolve(__dirname, './src/style/' + tailwind), }, }, }; });

    我从 tailwind.css 里移除了 @tailwind base,即 reset 部分。这部分会被 inline 到 index.html 里。然后修改 tailwind.config.js,加入根据环境判断是否需要分析 index.html 的逻辑:

    const isDev = process.env.NODE_ENV ==='development';
    module.exports = {
      content: [
        './src/App.{pug,vue,ts,tsx}',
        './src/**/*.{vue,js,jsx,ts,tsx,pug}',
      ].concat(isDev ? './index.html' : []),
      theme: {
        extend: {},
      },
      plugins: [],
    }
    

    接着修改 build 脚本,增加编译 inline css 和注入功能:

    package.json
    { "name": "aw-woodle-game", "version": "0.4.0", "scripts": { "build": "vue-tsc --noEmit && vite build && npm run build:sketch && npm run build:sketch2 && build/build.js", "build:sketch": "tailwind -i src/style/tailwind-sketch.css -o tmp/sketch.css --content index.html --config build/tailwind.config.sketch.js --minify", "build:sketch2": "stylus --include-css --compress < src/style/sketch.styl > tmp/sketch2.css", }, }

    6. 总结

    最终我使用这套工具链完成了整个产品的开发。不过仍然有一些问题未解:

    1. Vite 开发环境与构建环境其实是两套工具链,所以能过 dev,未必能过 build,尤其是 TypeScript,会有很多差异。不过只要耐心调试一番,也不难搞。
    2. 不知道是否跟我用 pug 有关,Tailwind 总是慢一拍。即我改了模版,html 变了,但对应的样式并没有出来;过一会儿,就出来了。于是我要么继续改,期待后面它会加上来;要么只有手动刷新。
    3. build 时,rollup 会把 css 放在 <head> 里面,导致浏览器认为这段 CSS 是关键内容,加载完之后才进行第一次渲染(FCP),使得我的骨架屏失效,所以要手动把它挪到 <html> 的最后。(当然在 GA 等代码的前面)

    最终的 Lighthouse 得分比较喜人,贴一下:

    (更多…)