标签: typescript

  • 【代友招聘】【成都】Web3 教学网站 后端工程师

    【代友招聘】【成都】Web3 教学网站 后端工程师

    Hackquest.io 是我长期关注并辅佐的一家专注于 Web3 教学的网站的。他们由一群很有热情的年轻人组成,努力勤奋又有天赋,从创业至今取得了相当可观的成长。现在他们需要招聘一名 Node.js 后端工程师,需求大约如下:

    Node.js开发工程师

    「职责描述」

    • 负责有效的设计、开发和使用性测试。
    • 制定应用部署和基础架构维护的最佳策略。
    • 确保开发能够被有效低成本的迁移和部署。
    • 设置测试环境并提高开发准确率。
    • 优化数据库结构。

    「后端技术栈」

    • Node.js(核心),Nest.js(核心)
    • PostgresSQL, MySQL 等任意一种关系型数据库以及 Redis
    • 熟悉 Google Cloud 或其他 Cloud 相关服务,部署,数据库管理
    • TypeScript(核心)
    • 熟悉 Prisma

    「任职要求」

    • 工作及项目经验 3 年及以上
    • 大型项目经验优先
    • 具备扎实的数据结构和计算机系统基础,编码功底扎实,代码习惯优秀

    有兴趣、有能力的同学,请与我取得联系,我会推荐给他们进行进一步的面试。

  • 在 Code.fun 做 Code Review(四)

    在 Code.fun 做 Code Review(四)

    时光如梭,一晃 2022 年已经过去 2/3,我们一起迎来 9 月。秋风送爽,丹桂漂亮,下面,我们一起回顾 8 月份我在 code.fun 完成的 Code Review 吧。

    关于 Code Review 的介绍和经验,欢迎大家回顾前三篇,本文暂不重复:


    (更多…)
  • 在 Code.fun 做 Code Review(二)

    在 Code.fun 做 Code Review(二)

    这周周会上,有同事说:

    以前他的 PR 被 approved 就直接合;现在他会等上一两天,期待我来 review。晚上耍手机时如果突然一阵爆响就说明我 reviewing,他就爬起来好好看我的评论然后作出修改,学到很多东西。

    被承认当然很爽、很骄傲。于是我想起这个系列,该再更一篇了。本文选材自七月的 Code Review 经验,趁热先分享一波。更早的 Code Review 过两天再往前翻翻看能否整理出来。

    关于 Code Review 的介绍和经验,欢迎大家回顾:在 Code.fun 做 Code Review,本文我就不重复了。

    0. 不应该使用独立 <script src="...">

    在这个需求中,我们要使用火山引擎统计用户行为,某同事就直接在模版里添加了 <script src="..."> 导入初始化 JS。这段 JS 非常之短,只是帮火山引擎初始化执行环境,但是为了保证执行状态,它既不能 async 也不能 defer

    这样做有几个问题:

    1. <script> 会阻塞后面的 HTML 渲染和 JS 执行;
    2. 这段 JS 虽然短,但是它的下载时间不会短:
      1. 浏览器要解析 DNS,
      2. 然后文件服务器要找到文件
      3. 文件服务器甚至没有 http2

    正确的做法是把它直接塞到现在 JS 里,只增加几十个字节而已。

    1. mounted 钩子添加的事件处理函数没有清理;直接使用 window.onblur

    这位同学犯了两个错误:

    1. 他在 mounted 钩子里添加了事件处理函数,但是并没有在 destroyed 钩子里清理。这可能导致内存泄漏,也可能导致奇怪的表现。
    2. 他直接将处理函数绑在 window 上,如果其它库、其它代码使用了同样的事件,就会导致冲突。

    2. 糟糕的命名

    这里这位同学犯了两个错误:

    1. event 是名词,不应该用做函数名。函数名应该是动词、动宾短语、动副短语,表达一个动作。
    2. event 是个非常非常通用的单词,用作全局函数太容易和其它全局对象起冲突或起混淆,应该加长。

    结合上下文,合适的写法是:triggerHuoshanEvent()

    3. 依赖 JSON.parse()

    我们需要记录一个小数据:sourceType,这个小数据被封装在一堆很大的 JSON 里面。这位同学直接拿 JSON.parse() 解码,然后打点。这样做当然可行,但是效率上非常之浪费。

    大家千万不要小看 JSON.parse(),它其实有很多细节:

    1. JSON 数据格式非常之严格,换言之,绝大对数情况下,必须把所有字符串都分析完才能结束
    2. 这个过程中,会构建大量对象
    3. 之后不再使用,这些对象又要被逐个回收

    所以正确的写法,应该使用字符串匹配或者正则。字符串操作都是 O(1),而且会在匹配到结果后立即返回,所以性能优秀非常多。

    (其实吧,这位同学的正则也有些一言难尽,不过今天先不展开。)

    4. TypeScript 的数据转换

    使用 TypeScript 之后,我们需要声明函数的返回值类型。对于一些比较通用的返回值,比如截图中的 mongoose lean(),就要进行类型声明转换,后面的代码才知道取到的值是什么类型。这里大家要记住,无论是浏览器还是 node,TS 都要经历编译之后,变成 JS 再被执行。

    所以 TS 阶段的类型转换不会出现在最终执行的代码里,也不会影响效率。但是截图中属于不同的情况,它会留下一个 .then() ,并增加一个微任务,不是我们想要的结果。

    5. TS 的枚举值可以适当调整

    TypeScript 提供 enum 关键字,用来声明枚举值,可以大大提升开发效率,降低出错概率。枚举的值可以手动指定,也可以省略,省略后 TS 会自动帮我们从 0 开始递增。上面就是典型的使用场景,列举若干设计软件,最后是 Other(其它),可以做成单选多选放在表单里。

    这段代码的问题在于,设计软件很多,我们列出来的肯定有疏漏;即使没有,将来也会发布更多设计软件。比如有天,我们自己做了设计软件,叫 design.fun,那么该放到哪里呢?放到 Other 前面,它的值就会跟 Other 冲突;放到后面,又会使得数据看起来很奇怪。

    此时只要给 Other 一个大点的值,拉开差距,就可以解决这个问题:

    export enum DesignSoftware {
      Photoshop,
      XD,
      JsDesign,
      Other = 100, // 再大点也行,不过 100 应该够我们用了
    }

    6. 组件名称和根节点样式应包含不少于两个单词

    这个其实是 Vue 官方的风格建议。原因很简单,一个单词,无论作为组件名称还是根节点样式名,都太容易和其它组件重复,也会影响 IDE 里快速跳转的效率,所以尽量多写几个单词,反正编译过程可以自动优化。


    总结

    时间过的真快,距离上次分享 Code Review 经验已经过去四个月了,我在 Code.fun 也是半年多工龄的老员工了。经过半年多的努力,我厂的产品完整了很多,但也很难称得上完善。希望这次分享的内容对大家有用,也希望大家多来试用我们的产品。

    如果诸位读者老爷对软件质量管理、软件开发效率、Code Review 有什么想问的、想说的,敬请在评论区与我互动。

    我现在在 code.fun 工作,我们的产品会把设计稿自动转换成代码,期望大大提升前端的开发效率,如果你对这个产品感兴趣,欢迎联系我,或直接试用。

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

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

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

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

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

    0. 创建 vite 项目

    npm init vite@latest

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

    小配置下 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

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

    配置 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

    @tailwind base;
    @tailwind components;
    @tailwind utilities;

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

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

    2. 配置 TypeScript

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

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

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

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

    3. 配置 eslint

    安装所需依赖:

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

    配置 .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 来配置:

    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

    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 和注入功能:

    {
      "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 得分比较喜人,贴一下:

    (更多…)
  • Vue3 < script setup > + TypeScript 笔记

    Vue3 < script setup > + TypeScript 笔记

    近期把一个老项目(Vue 组件库)从 Options API 风格重构成了 <script setup> 风格 + TypeScript,写篇博客记录下坑和心得。

    注:这篇博客只记录超出我踩过坑和不熟悉的内容,符合直觉、看了文档写完一次过的部分,我就不记录了。

    0. Composition API 与 <script setup>

    Vue 3 给我们带来了 Composition API,改变了之前 Options API 使用 mixins 继承和扩展组件的开发模式。这种模式让前端在开发时复用组件、改进组件变得更容易。具体改进我在本文里不再详述,大家可以看下 Vue master 的视频教程:Why the Composition API

    之后尤大又提出了 script-setup rfc,进一步改进了使用 Composition API 的体验。简单总结下新提案:

    1. 使用 <script setup> 取代 setup(),里面定义的所有变量,包括函数,都可以直接在模版中使用,不需要手动 return
    2. 可以使用顶级 await
    3. <script setup> 可以与其它 <script> 在同一个 SFC 里共存

    实际体验之后,我觉得这个提案的确能节省不少代码,不过也会增加一些复杂度。

    1. 导出 name 等非模版属性

    <script setup>setup() 的语法糖,主要目的是降低 composition API 的代码复杂度。它直接向模板暴露整个 <script setup> 的上下文,所有定义过的变量自动导出供模版使用,这样我们就不需要手动一个一个 return 了。

    所以它就不适合导出其它非模版属性,比如组件名称 name。如有需要,可添加一个普通 <script> 节点,用常见的 export default 导出初始化对象。如下:

    <script>
    export default {
      name: 'SomeVue3Component',
    };
    </script>
    
    <script setup>
    import {ref} from 'vue';
    
    const foo = ref('bar');
    </script>

    2. defineProps/defineEmits

    要在 <script setup> 里使用 props(传入参数)和 emit(广播事件),需要使用 defineProps()defineEmits() 定义。但要注意,这俩东西其实是编译宏,并非真实函数,不能把它们当作函数来使用。所以也不需要 import,当它们是全局函数直接用就好。同时记得修改 .eslintrc.js 把它们添加到 global 里。

    最简单的使用方法如下,以前的 props 定义规则可以沿用。

    const props = defineProps({
      foo : {
        type: String,
        default: 'bar',
      },
    });
    const emit = defineEmits(['change']);

    要配合 TypeScript,通常需要使用 withDefaults()props 生成默认值。这也是个宏,不能当函数用,也不能使用当前环境里的变量。改造后的代码如下:

    interface Props = {
      foo: string;
    };
    const props = withDefaults(defineProps<Props>(), {
      foo: 'bar', // 'bar' 不能是变量
    });
    const {
      foo,
    } = toRefs(props);
    
    const emit = defineEmits<{
      (e:'change', value:string),
    }>();

    这个地方的设计相当不完善,我不知道 Vue 团队会如何改进这里。比如,我们不能使继承 interface,然后再初始化 props,因为继承是常规 ts 语句,而 defineProps 是编译宏,两者的工作环境不一样。而因为无法使用变量,我们也无法将父级组件的 props 混入本地 props。于是复用组件又变得麻烦起来。

    耐心等待吧。

    3. 使用 undefined 初始化对象

    定义 props 问题真不少。有一些参数是可选参数,不一定要定义,也不一定要用到;但是使用 ts 定义时,即使如此,也要初始化他们,可以传值为 undefined,不然 tsc 可能会报告错误:变量可能为空。之所以用 undefined,而不是 null,则是因为 TypeScript 认为 null 是独立类型。

    即:

    interface Props {
      foo?: string;
    }
    const props = withDefaults(defineProps<Props>(), {
      foo: undefined,
    });
    const {
      foo,
    } = toRefs(props);
    
    // 接下来才能正常使用
    const bar = computed(() => {
      return foo.value || 'bar';
    });

    4. 使用的变量未初始化错误

    有些函数,可以传入为定义变量做参数,但是函数自身的签名没有很好的体现这一点,就会报错。比如 setTimeout,以下这段代码就会报告:

    type timer = ReturnType<typeof setTimeout>;
    let timeout:timer;
    function doAction() {
      clearTimeout(timeout);
    }

    我确定这段代码没问题,但是 tsc 不给过,只好给 timeout 加上 ! 修饰符,即:let timeout!:timer。这样就可以了。

    5. Vue SFC 文件类型定义

    为让 tsc 能够正确理解 Vue SFC 格式,需要创建这个描述文件,并且告诉 tsc 加载这个描述文件。

    declare module "*.vue" {
      import { defineComponent } from "vue";
      const component: ReturnType<typeof defineComponent>;
      export default component;
    }
    {
      "compilerOptions": {
        "target": "esnext",
        "module": "esnext",
        "strict": true,
        "jsx": "preserve",
        "moduleResolution": "node",
        "suppressImplicitAnyIndexErrors": true,
        "allowSyntheticDefaultImports": true,
        "allowJs": true,
        "resolveJsonModule": true,
        "esModuleInterop": true,
    
        "paths": {
          "@/*": [
            "./src/*"
          ]
        },
        "lib": ["DOM", "ESNext"]
      },
      "include": [
        "src/**/*.ts",
        "src/**/*.vue",
        "test/**/*.ts"
      ],
      "exclude": [
        "node_modules"
      ],
      "files": ["src/types/vue-shims.d.ts"]
    }

    6. 待解决问题

    • 因为第一次使用 TypeScript,很多不熟悉的地方,tsc 大量报错。但是由于使用了 vue-loader,所以 tsc 报告的错误行号基本都不对,很难查找问题所在,浪费了大量时间。
    • 导入了 moment locale 文件,但是缺少定义,不知道该怎么声明某个对象属于某个接口。可能要通过前面类似定义 Vue SFC 的方式。

    7. 项目地址

    对项目感兴趣,或者寻求范例的同学可以在 GitHub 上找到这个项目:meathill/muimui-ui: A simple vue 3 ui components suit. (github.com)