whxaxes / blog

📖 My personal blog
https://wanghx.cn/
194 stars 12 forks source link

JS IntelliSense in Egg #16

Open whxaxes opened 5 years ago

whxaxes commented 5 years ago

IntelliSense(智能提示) 在 IDE 中已经是标配功能,它能在某种程度上提高我们的开发效率,让我们可以更关注功能开发,而不用去来回翻看代码查看变量或者方法的定义。因此在 Egg 中我也在一直尝试更优的开发体验。而用过 Egg 的都知道,Egg 中的模块,都是 Egg Loader 自动加载进去的,因此 IDE 很难自动识别到那些被自动 load 进 egg 中的模块,IntelliSense 就自然无法起作用了。

为了解决这个问题,提升开发体验,在几个月前,我参与了 egg 支持 ts 的开发工作( 戳:当 Egg 遇到 TypeScript,收获茶叶蛋一枚 ),当时写了一个 egg-ts-helper 的工具来自动生成 d.ts 并通过 TS 提供的 Declaration Merging 的能力将 loader 加载的模块合并到 egg 的声明当中。从而实现了 TS 项目中的 IntelliSense

实现 TS 的 IntelliSense 之后,就开始考虑如何在 JS 项目中也能够跟 TS 项目一样能有智能提示,毕竟 Egg 的大部分项目都还是用 js 的,做了一些尝试之后,发现只要结合 vscode & jsdoc & egg-ts-helper 就能在 js 项目中也有跟 TS 项目中差不多的 IntelliSense 效果了,( github 中会裁剪动图,因此请点击动图以便看到全图 ):

image

具体实现如下:

声明生成

跟 TS 项目一样,先用 egg-ts-helper 在 js 项目下生成 d.ts ( 请使用 egg-ts-helper 的最新版本 1.17.0 )

$ npx ets

然后项目下的 typings/app 目录就已经生成对应的 d.ts

// typings/app/controller/index.d.ts

import 'egg';
import ExportBlog = require('../../../app/controller/blog');
import ExportHome = require('../../../app/controller/home');

declare module 'egg' {
  interface IController {
    blog: ExportBlog;
    home: ExportHome;
  }
}

然后再在项目下创建一个 jsconfig.json ,然后写入以下代码

{
  "include": [
    "**/*"
  ]
}

这个 jsconfig.jsontsconfig.json 类似,具体可以看官方文档描述,创建好这个文件并且 include **/* 之后,就会去加载 egg-ts-helper 生成的 d.ts 了。

上面这个 jsconfig 有个需要注意的点,如果打开 vscode ,vscode 提醒 Configure Excludes 的话,就需要配置一下 exclude,因为 include 的文件超过一千个的话,vscode 就会提醒让配置 exclude,如果不配置的话 vscode 就不会去处理 d.ts 的文件了,比如我这边负责的项目,前端构建多次又没有去清目录的话,轻轻松松文件数就破千了。我这边的 exclude 配置如下,可以参考一二

{
  "include": [
    "**/*"
  ],
  "exclude": [
    "node_modules/",
    "app/web/",
    "app/view/",
    "public/",
    "app/mocks/",
    "coverage/",
    "logs/"
  ]
}

完成这些配置之后,你就会发现,在 controller 这些用类的形式来写的模块中就已经可以拿到代码提示了。

image

原理跟 TS 项目一样,有了 jsconfig.json 的配置之后,vscode 会去解析 egg-ts-helper 生成的声明,这些声明会引入项目中的各个模块,通过 Declaration Merging 合并到 egg 已有的类型中。而 controller 这些模块的写法,都是需要从 egg 中 import 相关类来进行拓展的,因此自然就能顺利读到 egg 的类型,从而获得代码提示。

JSDOC

上面在类的形式写的 js 中是可以获取到代码提示了,那在非类的形式中怎么来获取呢,比如在 router.js 中,也很简单,直接通过写个 jsdoc 即可。

// app/router.js

/**
 * @param {import('egg').Application} app
 */
module.exports = app => {
  const { controller, router } = app;
  router.post('/sync', controller.home.sync);
  router.get('/', controller.blog.index);
};

看到注释中的代码了么,就可以通过这种方式就能够指定 app 为 egg 中的某个类型

@param {import('egg').Application} app

注意:如果使用了最新版本的 egg-ts-helper ,会自动生成一个声明文件将 egg 注册到一个名为 Egg 的全局 namespace 中,就可以不使用 import ,而是直接使用 Egg 来拿类型即可。

@param {Egg.Application} app

添加 jsdoc 之后就获得代码提示了

image

在其他非拓展类的模块中也差不多,比如:

middleware

// app/middleware/access.js

/**
 * @returns {(ctx: import('egg').Context, next: any) => Promise<any>}
 */
module.exports = () => {
  return async (ctx, next) => {
    await next();
  };
};

config


/**
 * @param {import('egg').EggAppInfo} appInfo
 */
module.exports = appInfo => {
  /** @type {import('egg').EggAppConfig} */
  const config = exports = {};

  config.keys = appInfo.name + '123123';

  return {
    ...config,

    biz: {
      test: '123',
    },
  };
};

上面 biz 是在最后才写到返回对象中,是为了将这种自定义类型合并到 egg 的 EggAppConfig 中。

集成到项目

安装 egg-ts-helper

$ npm install egg-ts-helper --save-dev

添加 jsconfig.json 文件

{
  "include": [
    "**/*"
  ],
  "exclude": [
    "node_modules/",
    "app/web/",
    "app/view/",
    "public/"
  ]
}

更改 egg-bin dev 的运行指令

{
  ...
  "dev": "egg-bin dev -r egg-ts-helper/register",
  ...
}

执行 dev

$ npm run dev

当看到有 [egg-ts-helper] xxx created 的日志后,就说明声明已经生成好了,用 vscode 打开项目即可获得代码提示,在 router.js 这些需要按照上面描述的加一下 jsdoc 就行了。

如果有用到 custom loader,可以看一下 egg-ts-helper#Extend 配置,再或者直接参考下面这个 demo 。

https://github.com/whxaxes/egg-boilerplate-d-js

有兴趣的可以 clone 过去自行尝试一二。

最后

要集成该代码提示功能需要具备一些 typescript 的知识基础,可以阅读一下 egg-ts-helper 生成的声明文件,知道类型是如何合并的,会更好的帮助你们获得更优异的开发体验,有相关问题可以直接到 egg-ts-helper 项目下提 issue ,我会尽快回复。

zhump commented 5 years ago

对于extend/context.ts ,extend/helper.ts 扩展的 ctx和ctx.helper对象如何智能提示扩展后的对象方法。

whxaxes commented 5 years ago

@zhump 如果你使用 egg-ts-helper 自动生成 dts 的话,就自动支持了

zhump commented 5 years ago

其实已经使用了,但是没有提示出来。。。 image controller.ts image helper.ts image

whxaxes commented 5 years ago

@zhump 你这 ts ,干嘛还用 commonjs 的写法 ... 用了 commonjs ts 的类型检查就会失效 ...

zhump commented 5 years ago

我用了export default {} ,helper.ts里面就不能使用 this.ctx ,代码检查就失败了。 image

whxaxes commented 5 years ago

@zhump 可以指定 this type

// extend/helper.ts
import { BaseContextClass } from 'egg';
export default {
  test(this: BaseContextClass) {
    return this.ctx;
  }
}
whxaxes commented 5 years ago

@zhump 如果你用 module.exports ,你就跟写 js 无异了,那你干嘛还用 ts ?

zhump commented 5 years ago

嗯嗯 被代码检查逼的 extend/helper.ts 使用了 module.exports。 采用

test(this: BaseContextClass)

确实解决了代码提示的问题。但this有时还会使用本身的方法属性。有些类 我改成 this:any了。虽然有点损失。但是代码提示和代码检查问题都解决了,多谢了🙏

atian25 commented 5 years ago

已经内置到 egg-bin 了。

可以看下 https://zhuanlan.zhihu.com/p/56780733

whxaxes commented 5 years ago

@zhump

// extend/helper.ts
import { BaseContextClass, IHelper } from 'egg';
export default {
  test(this: BaseContextClass & IHelper) {
    return this.ctx;
  }
}

这样应该就可以完美了,这个确实要对 egg 的声明比较熟悉才知道,我晚点补充一下到官方文档

zhump commented 5 years ago

nb...果然完美了。

zhump commented 5 years ago

发现两个问题。 第一个: midwayjs这样的库,他把app文件放到src,用户配置只能一个个的去覆盖,没有一个的统一baseUrl,方便用户迁移,当然手动现在可以临时解决。 https://github.com/whxaxes/egg-ts-helper/blob/master/src/index.ts

第二个: 还是上午的疑惑:

  test(this: BaseContextClass & IHelper) {
    return this.ctx;
  }

vs

  test(this: any) {
    return this.ctx;
  }

前置无法在vscode中 this.ctx.helper.test提示出来,并且编译器报错。后者可以正常使用。

whxaxes commented 5 years ago

@zhump 编译器报什么错?给个可以稳定复现的 repo ,我帮你看看

whxaxes commented 5 years ago

@zhump midwayjs 不是推荐用 decorator 么,egg-ts-helper 在设计之初倒是没有想到这种完全改掉目录的,欢迎 PR

whxaxes commented 5 years ago

@zhump 哦,有,你配置 cwd 即可

zhump commented 5 years ago

😂公司不给传代码到外网。

egg-init --type=ts
yarn 
yarn dev

新增extend/helper.ts,除此之外一行代码未加。

import { BaseContextClass, IHelper } from 'egg';
export default {
  a(this: BaseContextClass & IHelper) {

  },
  b(this: any) {

  },
  c() {

  },
};

controller/home.ts ctx调用helper,如下。此时a是否被提示的。

image

whxaxes commented 5 years ago

@zhump 因为 ctx.helper 是 IHelper 类型,所以里面的方法指定 this type 也只能指定为 IHelper ,不然就导致被过滤了,可以先这么解决

import { IHelper } from 'egg';

declare module 'egg' {
  interface IHelper extends BaseContextClass { }
}

export default {
  test(this: IHelper) {
    console.info(this.ctx);
  },

  other(this: IHelper) {

  },
};

把 BaseContextClass 拓展到 IHelper 上

zhump commented 5 years ago

可以了,目前没啥问题了。感谢大佬解答和产出这么棒的辅助工具。👍