分类 未分类 下的文章

芯片

主要系列是按CPU划分的.
其中ESP32和S2/S3/C3已经是不同的产品了, 这样命名挺讨厌, 自己既是大类的名称又是小类的名称...

芯片系列

系列 CPU
8266/8285 Xtensa L106
ESP32 Xtensa LX6 单核或双核
ESP32-S2 Xtensa LX7单核
ESP32-S3 Xtensa LX7双核
ESP32-C3 RISC-V

芯片后缀

后缀 意义
FHn n MB的flash
Rn n MB的PSRAM
FNn n MB的Flash

多年来都是瞎打,中午虽然有半小时出汗,也还想多获得一点儿乐趣。

提升命中率

视频: https://www.bilibili.com/video/BV1ux411L73q
要找到适合自己的舒服的投篮姿势. 每个人都是不同的.

  • 身体侧对篮筐而不要正对篮筐. 投篮侧的脚在前指向篮筐, 可减轻肩膀压力有利于命中.
  • 脚后跟不能着地而要脚尖着地跳投. 跳投前脚后跟接触地面会泄力. 脚尖着地也能增加射程
  • 跳投双脚前摆(而不是落在原地)能让出手点提高弧度提高从而提高命中率
  • 投篮手部姿势从起手位就要弯好,而不是要发力投球的时候再后弯手腕和准备其他手指
  • 下盘动作可以越快越好, 目标是最快速度踩到舒适投篮位置, 做好舒适投篮动作
  • 上盘动作要越舒展越好, 要保持固有节奏, 不应因为要着急快速投篮而肆意加快上盘动作, 导致投篮失准
  • 将球快速拉到自己舒适的起球垂面上, 藏于篮球口袋(大概意思就是在衣服下口袋的位置?), 并沿着垂直面向上起球. 同样不能因为着急而不沿着自己熟悉的投球线路起球.

总结: 投球要准就是要将身体准备姿势放置到最舒适位置才投.

AHK用了很久却一直没有深入学过, 今天深入一点写个入门.
AHK不比什么RPA工具效率低. 最大的缺失可能是元素识别.

五种语句体

;0. 自动执行的,跟js一样写什么执行什么
Some code
return

;1.定义快捷键(热键)的
^j::
  Some Code
  Return

;2. 定义热字符串的
::btw::
  Send, by the way
  return

;3. 定义子程序的. 相当于汇编里面的label, 然后用gosub调用(跟goto有点像)
subName:
  Some Code
  return

;4. 定义函数的
function(param1, param2:="xxx"){
  Some Code
  return "A value"
}

多文件组织

可以用#include包含其他ahk文件

传统语法

ahk说明里面讲到ahk历史悠久,所以有一些传统语法(就是看上去有些恶心的语法), 比如说某些命令里面字符串可以不加引号让你搞不清哪儿是字符串开始和结尾...

基础语法

; 单行注释
/*
多行注释
但有趣(烦躁)的是, /*和*/必须在行首. 像这一行的这俩是不起作用的
*/
"这是一个字符串, 需要双引号" 
变量 := 赋值

讲讲字符串里面的转义. C语言是用\转义, AHK比较变态使用<code></code>转义,搞得我markdown都得写code标签. 主要转义有%t rn. %用于创建动态变量引用和动态函数调用, 有点让我想起来.bat格式
但是但是但是, "的转义是"", 很神奇吧, 那就是说如果要表示一个引号字符串,加上把它括起来的引号实际上是4个引号哈哈哈哈""""

+-*/(), 和++ -- += -=等运算符都支持, condition?value1:value2的三元运算也支持.
:=是赋值运算符, 不太一样. and or not是逻辑运算符

支持类似js的对象, 可以用这样的obj := {Key1: Value1, Key2: Value2}形式创建, 以obj.Key1的形式访问
数组也是[]形式的, 但是第一个成员的序号是1而不是0

Ctrl键未绑定的快捷方式

按键:

软件 ABCDEFGHIJKLMNOPQRSTUVWXYZ 12346890 F123456789⒑⒒⒓
EXCEL JMQ F1 F2

Excel参考: https://www.asap-utilities.com/excel-tips-shortcuts.php

自带的常用ahk

Windowspy.ahk 用于查找句柄ClassNN

部分常用命令

; 设置鼠标坐标为屏幕
CoordMode, Mouse, Screen

; 按窗口标题名称点击登录
ControlClick, X161 Y489, 系统登录

; 点击坐标
Click, 123 456

; 激活不在前台的窗口
WinWaitActive, ahk_exe EXCEL.EXE

; 停1000ms
Sleep, 1000

; 发送alt+F4
Send, !{F4}

; 发送字符串并且不激活输入法
Send, {text}some string

; 运行程序
Run, "D:\Program Files\网店管家云端版\wdgjyun.exe"

; 创建函数
saveExcel(filename)
{
  ; 删除文件
  FileDelete, %filename%
  Sleep, 1000
  send, {F12}
  Sleep, 2000
  send, {text}%filename%
  Sleep, 1000
  send, {Enter}
  Sleep, 1000
  send, {Enter}
  Sleep, 2000
  Send, !{F4}
}

; 点击图片. x1,y1 ~ x2,y2是图片的搜索区域. delta是点击位置与图片左上角在xy坐标上的偏移位置
imageClick(x1, y1, x2, y2, imageFile, delta){
  ImageSearch, FoundX, FoundY, x1, y1, x2, y2, %imageFile%
  if (ErrorLevel = 2)
      MsgBox Could not conduct the search.
  else if (ErrorLevel = 1)
      MsgBox Icon could not be found on the screen.
  else{
      MouseClick, left, FoundX+delta, FoundY+delta
  }
}

Arduino方式开发

NonOS开发

FreeRTOS开发

ESP-AT开发

乐鑫已经停止了对8266 AT开发的支持, 后续只维护BUG. 现在乐鑫推荐 ESP32-C3也即是RISC-V平台.
ESP-AT Git项目能找到8266最后版本的技术文档

ESP-IDF开发

官方文档是这个: ESP8266 SDK的开发指南, 目前只有英文

CSDN上半颗心脏同学的8266开发笔记
乐鑫关于8266和32开发用IDF的区别, 8266 SDK从V3.0开始与IDF风格统一, 但由于不是完全兼容, 所以名称没有改为IDF.
ESP8266 SDK的GIT库
ESP-IDF的GIT库

环境搭建

还挺费劲, 下载的东西还挺多

准备工作

  1. 大多数人可能还是用windows 环境吧
  2. 需要下载3个包, 1个是msys2模拟linux环境, 1个是8266 编译器, 1个是8266项目包
  3. 需要用到github,所以准备好梯子

具体步骤

  1. 下载并解压 toolchain: https://dl.espressif.com/dl/esp32_win32_msys2_environment_and_toolchain-20181001.zip, 解压缩后把里面的msys32放到一个简短点儿的目录里比较好, 如D:\msys32
  2. 双击里面的mingw32.exe, 这就是以后的主要工作环境了. 先在home\用户名\也就是~目录下建立一个esp目录, 把开发支持工具(等会儿会有2个), 放在这儿. home目录就在D:\msys32下面.
  3. 下载第一个开发支持工具: esp8266 toolchain, https://dl.espressif.com/dl/xtensa-lx106-elf-gcc8_4_0-esp-2020r3-win32.zip, 解压缩到esp目录里面.
  4. mingw32里面用vim .bashrc增加一句话export PATH="$PATH:$HOME/esp/xtensa-lx106-elf/bin"
  5. esp目录里下载第二个支持工具. 这儿要用gitgithub. 所以你可能要先在.bashrc里面设置个代理export http_proxy="192.168.0.1:8080", 然后重启mingw32, 进入esp目录运行: git clone https://github.com/espressif/ESP8266_RTOS_SDK.git , 也可以下载zip包https://github.com/espressif/ESP8266_RTOS_SDK/releases/download/v3.4/ESP8266_RTOS_SDK-v3.4.zip直接解压, 然后把clone后的路径设置为系统变量IDF_PATH. 可以在.bashrc里面设置, 增加一行: export IDF_PATH="$HOME/esp/ESP8266_RTOS_SDK",也可以再windows系统的环境变量里面设置, 如果在winows环境变量设置, 值要设置为/home/用户名/esp/ESP8266_RTOS_SDK这样的形式. 似乎~符号不认. 重启mingw32使设置生效.
  6. 运行 python -m pip install --user -r $IDF_PATH/requirements.txt
  7. 复制一个hello world出来cp -r $IDF_PATH/examples/get-started/hello_world
  8. 进到hello_world目录运行make menuconfig, 出来个UI界面配置一下串口名称如COM18, 保存退出. 注意这个是ESP8266的烧录串口, 波特率74880
  9. 运行make flash, 会检查环境, 并且从github下载项目, 编译, 挺花时间, 没个十几分钟搞不定. 可以增加一个-j8参数, 使用8核编译,速度会快很多
  10. 运行make monitor可以监控串口输出.

好了, 看起来终于可以开始了.

其他有用的命令

make all 只编译而不烧录, 也会在最后显示烧录命令.
make print_flash_cmd 打印烧录命令, 可以用在其他地方
make partition_table 打印分区表, 一般分为两种: 非OTA的分区表Single factory app, no OTA和OTA分区表Two OTA app, 可以在make menuconfig中选择.

分区表

分区表烧录在0x8000位置, 长度为0xC00
非OTA的分区表:

# Espressif ESP8266 Partition Table
# Name,   Type, SubType, Offset,  Size
nvs,      data, nvs,     0x9000,  0x6000
phy_init, data, phy,     0xf000,  0x1000
factory,  app,  factory, 0x10000, 0xF0000

NVS是非易失存储, PHY似乎是物理什么的
OTA的分区表

# Espressif ESP8266 Partition Table
# Name,   Type, SubType, Offset,   Size
nvs,      data, nvs,     0x9000,   0x4000
otadata,  data, ota,     0xd000,   0x2000
phy_init, data, phy,     0xf000,   0x1000
ota_0,    0,    ota_0,   0x10000,  0xF0000
ota_1,    0,    ota_1,   0x110000, 0xF0000

可以看到OTA分区表是兼容非OTA分区表的, 兼容性明显比以前的8266项目好多了. 网上有很多介绍以前的8266分区表的, OTA分为user0/user1, user0起始于0x1000而不是现在的0x10000, 现在应该已经不适用了.
otadata是指示启动那个app的, 如果为空就启动ota_0
分区表可以自定义, 不过我感觉一般是没必要了

系统任务

大写T在最后面的是IDF的任务, 其他的是FREERTOS的任务.

uiT 用户初始化任务, 初始化完以后调用app_main再销毁自己
IDLE FREERTOS的空闲任务, 在其钩子vApplicationIdleHook中调用sleep和喂狗任务
Tmr FREERTOS的软时钟
ppT 处理WIFI硬件驱动,我猜是process protocol的缩写
ppT 系统电源管理, power managerment.
rtT 高优先级的硬时钟中断任务. 主要处理WIFI实时事件. 基于这个组件的程序不要在应用(application)里面调用,以免影响WIFI收发.
tiT Tcp-Ip协议栈的任务(LwIP), 处理TCP-IP包.
esp_event_loop_task 处理系统事件
优先级从低到高依次是:

0 IDLE 
2 Tmr 
8 tiT 
10 esp_event_loop_task
11 pmT
12 rtT
13 ppT
14 uiT

用户任务的优先级要低于rtT也就是12.
如果要加快 TCP/UDP 吞吐量,可以尝试将发送/接收任务的优先级设置为高于“tiT”任务的优先级(8)

PWM和嗅探器Sniffer共存问题

SmartConfig应该属于一种Sniffer.
8266是没有硬件PWM的, 它是用硬时钟模拟的软件PWM. 硬时钟同时用于WIFI, 所以WIFI sniffer和PWM同时用会出现资源争用的问题.
N个通道的PWM每次翻转GPIO将占用6+8N ns的时长. 例如一个通道PWM就是14ns.
占用期间, 如果收到了必须要实时处理的LPDC或HT40包, 这些包就会被丢弃.
同时使用PWM和smartconfig会使得配网时间更长. 而且调整PWM的频率/duty cycle/phase都会影响smartconfig的速度.
如果要同时使用, 需要:

  • PWM 的频率不能太高,最多 2KHz。
  • 修改PWM的占空比和相位,使每个通道反相之间的时间间隔(Tn)等于0或大于50us(Tn = 0,或Tn > 50)
    这样看起来这个软件PWM似乎不太适合用于LED驱动~~

使用API-Reference文档

非常神奇的, 8266仿照IDF风格的API编程手册, 乐鑫已经放弃治疗了, 8266 RTOS SDK 编程指南最后版本V3.4中, 居然只是把每个函数罗列了一遍, 有个球用. 找了网上各种指南, 发现只能参考ESP32的手册, 在同样的目录结构下, Uart的使用就有非常详细的描述.

使用 make menuconfig工具

感觉有些操蛋~ 很多在程序里面定义的参数还要再menuconfig里面预先定义一下, 否则程序编译后运行得总有些问题. 而且make menuconfig重新定义以后,这个编译速度就慢的惊人了

使用linux服务器或者使用docker中加载linux环境, make的速度可以快好几倍.

menuconfig主要是在根目录下生成sdkconfig文件作为编译依据. 同时会把旧的文件改名为sdkconfig.old. 如果熟练的情况可以直接修改sdkconfig文件.

是否要重新编译, 似乎是根据sdkconfig有没有更新决定的. 所以即使修改了一个和编译完全无关的项目, 如PC烧录串口, 也会导致全部重新编译.

IDF版本兼容性

IDF各个版本兼容性不佳, 如8266 v3.4和v3.1是不兼容的. 发现的就有 uart_driver_install在v3.4比v3.1多一个参数,导致编译不能通过.

8266的串口输出

8266只有1.5个串口, 也就是1个完整串口UART0和1个只有TX的串口. 所谓有时候看到的UART2(IO13/15)是UART0交换(swap)过去的.

基于idf, 函数中直接使用printf函数将直接从uart0口输出, 不需要串口初始化的步骤.

  • 在menuconfig中将Swap UART0 I/O pins选上, 编译烧录后, 刚刚启动打印的boot信息在UART0输出, 后面printf的信息都在UART2上输出. 如果使用了软重启esp_restart();, 软重启后的boot信息也将在UART2上输出.

  • 在menuconfig中将Uart for console output选项改为Custom, 如果UART peripheral to use for console output (0-1)仍然为UART0, 结果和上面的一样不变.

  • 如果将UART peripheral to use for console output (0-1)改为UART1, 那么第一次启动的boot信息在UART0输出, printf的信息在UART1输出, 软重启后的boot信息在UART2输出.

  • 在meunconfig中将Partition Table改为Factory app, two OTA definitions不会对运行结果有什么影响, 但是flash烧录的固件名称会改为partitions_two_ota.bin
    如果是单文件, 烧录命令是(project_template是项目的目录名):

    D:\r\esp\esp-idf\components\esptool_py\esptool\esptool.py --chip esp8266 --port COM18 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size 2MB 0x0000 build/bootloader/bootloader.bin 0x10000 build/project_template.bin 0x8000 build/partitions_singleapp.bin

    如果是ota,烧录命令是:

    D:\r\esp\esp-idf\components\esptool_py\esptool\esptool.py --chip esp8266 --port COM18 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size 2MB 0x0000 build/bootloader/bootloader.bin 0x10000 build/project_template.bin 0x8000 build/partitions_two_ota.bin
  • menuconfig只能控制所谓console的uart设置, 如果要使用uart,还是要自己通过程序进行uart初始化设置.

  • uart初始化使用uart0的影子uart2, 如果在menuconfig中已经设置了swap, 在初始化中就不需要再次调用swap函数了.

    static void uartInit()
    {
      // Configure parameters of an UART driver,
      // communication pins and install the driver
      uart_config_t uart_config = {
          .baud_rate = 115200,
          .data_bits = UART_DATA_8_BITS,
          .parity    = UART_PARITY_DISABLE,
          .stop_bits = UART_STOP_BITS_1,
          .flow_ctrl = UART_HW_FLOWCTRL_DISABLE
      };
      uart_param_config(UART_NUM_0, &uart_config);
      uart_driver_install(UART_NUM_0, BUF_SIZE * 2, 0, 0, NULL);
    }
  • v3.1printf的问题: 有时候不会及时输出, 尤其是在使用了格式化符号%d%x等等后, 必须要用fflush(stdout)后才能输出.

include顺序

idf头文件#include是有顺序的,否则编译不通过.
感觉顺序似乎应该是标准库文件->freertos头文件->esp系统文件->驱动文件

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_spi_flash.h"
#include "driver/uart.h"

  1. 签名要用Adobe Reader XI 11.07版本. 不多不少.
  2. Adobe Reader XI 已经被Adobe官方停更了, 也就是在官网下载不到咯, 目前官网最新版是 Reader DC.
  3. 但你不能用Reader DC, 因为很多CA证书仍然是使用SHA1加密,但Reader DC因为其不安全已经不支持了;
  4. 好吧,其实DC是支持的,只不过要改注册表,然而他还不推荐你改,就不好好给你提供个地址让你去改;
  5. 然后你还是要退回到 Reader XI 11.07版本, 安装的时候记得所有都要点同意。杀软要是拦截一定得放行了,否则你会在签名的时候遇到奇妙的闪退问题;
  6. 是的,你不得不用不安全的落后的CA证书和已经被弃用的Reader版本来完成签名。X蛋不?

最近用一款51单片机与8266(AT固件)的UART2接口TTL串口通讯, 使用115200的波特率总是每70多个字节或100多个字节出现误码, 8266使用的WROOM-02D模块, 26MHz晶振, 而51使用的是RC震荡.
具体误码的情况是: 8266发送到51不会误码, 而51发送给8266就会误码. 使用电脑HTerm查看, 8266只要启动了AT固件, 51发送的就会误码, 8266不正常启动(如进入烧录模式)或者启动一个啥也不干的空程序, 就不会误码. 怀疑是8266初始化UART2串口以后才有的问题.
尝试了如下各种方法都失败了:

  • 尝试将51的波特率降低一半到57600, 仍然误码.
  • 尝试将51的波特率改一点点如115201, 结果根本就读不到数据, 这不科学! 难道是硬件串口内置了集中固定的波特率? 因为波特率都需要时钟发生器, 这倒是很有可能无法改一点点.
  • 尝试电脑端更改波特率, 发现大约在(115200-3000, 115200+6000)左右都能正确接收, 超出一点儿之后会出现误码,超出太多就全部乱码了. 算下来容忍度有8%左右.

参考了这篇文章, 里面讲到一次不要发的太多. 考虑到每次发送都是在相对固定的位置误码(70个字符以后), 最后成功的方法是:

每次发送50个字符后, 延时5ms再继续发送.

延时时长是反复测试得出的.测试发现每发送50字符延时2.5ms正好可以不误码, 为了容错将这个时间加大一倍为5ms.
硬件上继续尝试解决, 看是否能够找到最终原因.

久了不用就会忘
参考: https://zhuanlan.zhihu.com/p/85383698

  • A的所有B后代: .A .B{}
  • A的所有B子代: .A>.B{}
  • A后面一个邻居: .A+.B{}
  • A所有邻居: .A~.B{}
  • A和B的交集: AB{}, 选择这样的元素: <div class="A B"></div>
  • A和B的并集: A,B{}, 选择这样的元素 <div class="A"></div><div class="B"></div>

b站这个9分钟教程很好https://www.bilibili.com/video/BV12J411z7j7?p=1

原理图

基本操作

  • 新建工程(图标:左上角的本子)
  • 新建原理图(原理图编辑器)
  • 图框设置(图标:一张纸上有个坐标轴): 设置信息
  • A键, 选择元器件, 其中: C是电容, R是电阻, 一般的接口在Connector_generic中, USB在connector中
  • 放好元器件, 按M移动并调整位置, R旋转, X垂直镜像, Y水平镜像
  • P放置电源和地线. 如5VGND. 对于电源和地线,还需要选择两个PWR_FLAG并分别连接到电源和地线上
  • W键进行连线
  • C可以复制当前指向的元器件
  • 直接左键框选可移动框选住的目标, 按Shift+左键框选可以复制框选目标并移动
  • 连线后按M键移动元器件, 连线不会跟着动, 这是需要按G键, 可以带着线移动元器件
  • V键可以修改元器件的值, 按E键也可以改
  • L键可以为线增加标签, 相同标签的线认为是连在一起的

自动序号

标注原理图(图标: 一张纸上一个铅笔), 点击标注就可以给元器件自动上符号

设置封装

编辑符号字段(图标: 一张表格), 在对应的元器件的Footprint栏里面, 点击后面的书架样子的图标, 打开封装库, 选择封装. 选择完封装才能做PCB.

检查

点击执行电器规则检查(图标: 瓢虫), 点击运行即可以看到检查到的问题.
修改后再次检查直到没有问题.

PCB

在原理图上按F8键, 点击更新PCB,即进入PCB编辑器, 鼠标上带着所有元器件. 单击放置.

基本操作

  • 设置原点. 点击右侧设置网格原点(图标: 4x4个点带1个红点), 选择要画的PCB的左下角
  • 在右侧的中选择Edge.Cuts即PCB刀切层, 选择后会有个小三角指向这个层
  • 点击添加图形线条(图标: 4个点3条线连着), 在绘图区点击右键, 选择网格中合适的网格大小, 如1.0mm, 从上面的原点开始画图, 画一圈边线连接起来.
  • Margin层, 画在刀切层内0.3mm画一圈布线区域. 不能超过这个区域布线以免被刀切断
  • 在元器件上按M键可以移动, R键旋转. 但是XY是用不了的. X另有它用
  • 在元器件上双击, 弹窗中可以修改坐标数值精确定位
  • 放好器件后, F.FabB.Fab层可能干扰实现, 可以关闭.

布线

  • 布线前先定义线. 点击电路板设置(图标: 电路板上一个齿轮), 按需要设置设计规则, 网络类表, 导线和过孔, 阻焊/锡膏
  • 一对线的走法可以按Alt+6选择差分对布线, 选择后点击右键选择差分对标注并选择定义好的差分对值, 然后给一对管脚布线
  • 在管脚上点击X键可以进行单线布线. 布线的宽度可以在右上角下拉菜单里选择
  • 在布线中可以点击/键修改布线的方向, 在水平/垂直/45度方向间切换
  • 在布线中可以点击V键增加过孔
  • 布线后, 点击V键可以看背面的线
  • 布线后, 点击一条线后点击I键可以选择所有相连的线
  • 布线后, 点击D键可以不中断线的情况下平移线
  • 最后除了GND以外, 所有的线都布好
  • 点击Alt+3可以看3D图

标注和覆铜

(对应第三节: 出Gerber文件)

  • 选择F.SilkS层, 再点击添加标注(图标: 测距符号上有个N), 然后点击两个点就可以标注距离
  • 点击F.Cu层, 再点击添加填充区域(图标: 绿色底上有条过孔线), 点击覆铜区域的一个角, 弹出窗口, 选择合适的值, 确定后, 把区域画完,即可.
  • 在一个角上点右键选择覆铜->重复区域区域到层, 弹窗中选择B.Cu就可以给背面覆铜

官方原文: https://webpack.docschina.org/concepts/
Webpack似乎已经到了不得不深入学习的程度了, 要不然有的概念看不明白. 现在居然还有绕过npm install, 用webpack来引入外部库的方式!(PS: 通过后面的学习, 发现webpack确实可以在externals中声明CDN地址的方式来引入外部包)

Webpack是一个静态打包工具. 也就是从各种依赖里把需要的抽取出来, 形成bundle. bundle在webpack里就是的意思, 也就是"打包"的"包"
所有的配置都是在webpack.config.js中完成的
下面说的react和vue都指的是其cli工具

entry

是入口, 也被称之为chunk块. 默认是./src/index.js , 在react中是app.jsx,在vue中是./src/main.js,

output

是输出的bundle和其他文件的存储位置. 默认是./dist/main.js, 文件位置在dist文件夹中. react和vue都没有改这个文件夹.

loader

是处理除了.js.json文件打包以外, 其他文件的打包工具. 包括.css, .txt等等. loader是在module.rules中以数组的方式规定的. 每个数组元素包括以正则定义的后缀名test(不知道为啥叫test),和loader名.

plugin

执行除了loader以外的更宽泛任务. plugin需要通过npm install安装后在webpack.config.js前面require进来,然后在plugins下面定义数组元素.

mode

有三种值: production, development, none, 是对开发环境的定义

模块的导入方式

所有导入方式都需要预先npm install这个模块

  • 静态导入, 就是常规的在文件开始import.
  • 动态导入, 在使用模块的函数中, 先使用import导入(异步方式), 再使用模块.
  • 预获取prefetch. 提前动态获取下一个页面要用的模块. 这是通过在js的注释/**/中添加webpack命令实现的.
  • 预加载preload. 在加载本页面前先加载指定模块. 与上面同样是在js注释中实现.

外部化 externals

在创建library时尤其需要关联外部库, 而不是把外部库也打包到自己的库里面. 这儿就是使用externals. 使用了externals之后, 打包bundle就会把import的外部库排除掉.

为生产环境配置不同的config文件

本来嘛, 默认的config文件是webpack.config.js,然而为了生产的打包文件更小, 开发的打包文件更方便/热更新等, 就需要配置两个不同的config文件webpack.prod.js, webpack.dev.js, 然后为了把两个不同的config中共同部分提取公约数, 又有了第三个公共文件webpack.common.js.
另外我又发现在react的项目中, package.json又有一种用一个webpack.config.js实现两种环境的方式. 区分时加上不同的命令行参数:

"scripts": {
    "dev": "webpack-dev-server --config webpack/webpack.config.js --env.mode=development --watch",
    "release": "webpack --config webpack/webpack.config.js --env.mode=production",
    "dev:dualmode": "npm run dev -- --demo=DualmodePanel",
    "release:dualmode": "npm run release -- --demo=DualmodePanel",
    "dev:wugan": "npm run dev -- --demo=NonInductiveDemo",
    "release:wugan": "npm run release -- --demo=NonInductiveDemo"
  },

externals

这个属性只是为了不让webpack去打包指定的内容. 然而在开发时, 这个内容还是要能访问到的. 不管是用npm install,或者在index.html里面用<script>引入, 还是说在externals里面直接指定CDN地址.

原文

语言服务扩展包指南

正如你在《编程语言特性》章节看到的那样,我们可以直接使用languages.* API实现语言特性。而语言服务扩展包提供另一种方式实现对编程语言的支持。
本章节将:

  • 说明语言服务扩展包的特性
  • 带你应用Microsoft/vscode-languageserver-node库实现一个语言服务。你也可以直接在lsp-sample中浏览代码。

为什么使用语言服务?

语言服务是VS Code的一类特殊扩展包,可以增强多种编程语言的编辑体验。使用语言服务,你可以实现自动完成、错误检查(诊断)、转到定义和其他许多VS Code支持的语言功能。
然而,实现对这些语言功能的支持时,我们发现了三个常见问题:
第一,语言服务一般是以其原生语言实现,而在以Node.js为运行时的VS Code中集成它们却是个挑战。
其次,语言特性可能极耗资源。例如,要正确地验证一个文件,语言服务需要解析一大堆的文件,建立抽象语法树(AST),实施静态程序分析。这些操作可能导致CPU和内存的大量占用,而同时我们还要保证VS Code的性能不受影响。
最后,在多种代码编辑器中集成多种语言工具可能导致巨大负担。对语言工具来说,它们需要为代码编辑器适配不同的API。而对代码编辑器来说,他们却不能期望语言工具能提供统一的API。这使得要对M种语言在N种代码编辑器中实现语言支持的话,工作量变成了M*N倍。
要解决这些问题,微软定义了语言服务协议LSP,将语言工具和代码编辑之间的通讯标准化。此时,当语言服务使用LSP与代码编辑器通讯时,它可以用任何语言实现,在其自身进程中执行,避免性能消耗。进一步,任何兼容LSP的语言工具都可以和多种兼容LSP的语言编辑器集成,当然任何兼容LSP的语言编辑器也很容易选择多种兼容LSP的语言工具。LSP能让语言工具提供者和代码编辑器提供者获得双赢!

本指南中,我们将:

  • 解释如何使用提供的Node SDK,在VS Code中建立语言服务扩展包
  • 解释如何运行、调试、记录日志和测试语言服务扩展包
  • 带给您一些语言服务的高级内容

实现语言服务

概述

在VS Code中, 语言服务由两部分组成:

  • 语言客户端: 用JavaScript/TypeScript写的普通VS Code扩展包.这个扩展包能访问所有VS Code的命名空间API
  • 语言服务端:独立进程运行的语言分析工具。

如上面所提到的,独立进程的语言服务端有两个好处:

  • 分析工具可以用用任何语言实现,同时它可与语言客户端依照语言服务协议通讯。
  • 语言分析工具通常带来CPU和内存的高占用,在独立进程运行可以避免性能损耗。
    下图说明VS Code如何运行两个语言服务扩展包。HTML语言客户端和PHP语言客户端是用Typescript写的普通VS Code扩展包。他们实例化相应的语言服务端并且与之通过LSP通讯。虽然PHP语言服务端是PHP写的,它依然可以通过LSP和PHP语言客户端通讯。

    本指南将叫你如何使用Node SDK构建语言客户端/服务端。下面部分将假定您已经对VS Code扩展包API很熟悉了。

LSP示例:纯文本文件的简单语言服务端

让我们对纯文本文件构建一个简单的语言服务端,实现自动完成和诊断功能。我们还将介绍客户端/服务端之间的配置同步。
如果您更喜欢直接看代码:

  • lsp-sample:本指南的源代码完整存档。
  • lsp-multi-server-sample:lsp-sample 高级版的源码存档,它为每个工作区文件夹启动一个单独的服务端实例,以支持 VS Code 中的多根工作区功能。
    Clone Microsoft/vscode-extension-samples库,打开示例:
    > git clone https://github.com/microsoft/vscode-extension-samples.git
    > cd vscode-extension-samples/lsp-sample
    > npm install
    > npm run compile
    > code .

    以上安装所有依赖项并打开包含客户端和服务端代码的 lsp-sample 工作区。以下是lsp-sample结构的概览:

    .
    ├── client // Language Client
    │   ├── src
    │   │   ├── test // End to End tests for Language Client / Server
    │   │   └── extension.ts // Language Client entry point
    ├── package.json // The extension manifest
    └── server // Language Server
      └── src
          └── server.ts // Language Server entry point

    语言客户端说明

    我们先来看看/package.json,它描述了语言客户端的能力。有三个有趣的部分:
    首先看activationEvents

    "activationEvents": [
      "onLanguage:plaintext"
    ]

    这部分告诉 VS Code 在打开纯文本文件(例如扩展名为 .txt 的文件)后立即激活扩展。

接下来看configuration部分:

"configuration": {
    "type": "object",
    "title": "Example configuration",
    "properties": {
        "languageServerExample.maxNumberOfProblems": {
            "scope": "resource",
            "type": "number",
            "default": 100,
            "description": "Controls the maximum number of problems produced by the server."
        }
    }
}

本节为 VS Code 提供configuration设置。该示例将解释如何在启动时和每次更改设置时将这些设置发送到语言服务端。

语言客户端实际代码和相应的 package.json 位于 /client 文件夹中。 /client/package.json 文件中有趣的部分是它通过engines 字段引用了vscode 扩展包主API,并向vscode-languageclient 库添加了一个依赖项:

"engines": {
    "vscode": "^1.52.0"
},
"dependencies": {
    "vscode-languageclient": "^7.0.0"
}

如前所述,客户端是作为普通的 VS Code 扩展包实现的,它可以访问所有 VS Code 命名空间 API。

下面是对应的extension.ts文件的内容,也是lsp-sample扩展的入口:

import * as path from 'path';
import { workspace, ExtensionContext } from 'vscode';

import {
  LanguageClient,
  LanguageClientOptions,
  ServerOptions,
  TransportKind
} from 'vscode-languageclient/node';

let client: LanguageClient;

export function activate(context: ExtensionContext) {
  // The server is implemented in node
  let serverModule = context.asAbsolutePath(path.join('server', 'out', 'server.js'));
  // The debug options for the server
  // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging
  let debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] };

  // If the extension is launched in debug mode then the debug server options are used
  // Otherwise the run options are used
  let serverOptions: ServerOptions = {
    run: { module: serverModule, transport: TransportKind.ipc },
    debug: {
      module: serverModule,
      transport: TransportKind.ipc,
      options: debugOptions
    }
  };

  // Options to control the language client
  let clientOptions: LanguageClientOptions = {
    // Register the server for plain text documents
    documentSelector: [{ scheme: 'file', language: 'plaintext' }],
    synchronize: {
      // Notify the server about file changes to '.clientrc files contained in the workspace
      fileEvents: workspace.createFileSystemWatcher('**/.clientrc')
    }
  };

  // Create the language client and start the client.
  client = new LanguageClient(
    'languageServerExample',
    'Language Server Example',
    serverOptions,
    clientOptions
  );

  // Start the client. This will also launch the server
  client.start();
}

export function deactivate(): Thenable<void> | undefined {
  if (!client) {
    return undefined;
  }
  return client.stop();
}

语言服务端说明

注意:从 GitHub 克隆的“服务端”为练习的最终实现效果。要遵循练习步骤,您可以创建新的 server.ts 或修改克隆版本的内容。

在示例中,服务端也是用 TypeScript 实现的,并使用 Node.js 执行。由于 VS Code 已经附带了 Node.js 运行时,因此无需提供您自己的运行时,除非您对运行时有特定要求。

语言服务端的源代码位于 /server。服务端的 package.json 文件中有趣的部分是:

"dependencies": {
    "vscode-languageserver": "^7.0.0",
    "vscode-languageserver-textdocument": "^1.0.1"
}

这会引入 vscode-languageserver 库。

下面是一个服务端实现,它使用简单的文本文档管理器,其同步文档的方式为:总是将文件的完整内容从 VS Code 发送到服务器。

import {
  createConnection,
  TextDocuments,
  Diagnostic,
  DiagnosticSeverity,
  ProposedFeatures,
  InitializeParams,
  DidChangeConfigurationNotification,
  CompletionItem,
  CompletionItemKind,
  TextDocumentPositionParams,
  TextDocumentSyncKind,
  InitializeResult
} from 'vscode-languageserver/node';

import { TextDocument } from 'vscode-languageserver-textdocument';

// Create a connection for the server, using Node's IPC as a transport.
// Also include all preview / proposed LSP features.
let connection = createConnection(ProposedFeatures.all);

// Create a simple text document manager.
let documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);

let hasConfigurationCapability: boolean = false;
let hasWorkspaceFolderCapability: boolean = false;
let hasDiagnosticRelatedInformationCapability: boolean = false;

connection.onInitialize((params: InitializeParams) => {
  let capabilities = params.capabilities;

  // Does the client support the `workspace/configuration` request?
  // If not, we fall back using global settings.
  hasConfigurationCapability = !!(
    capabilities.workspace && !!capabilities.workspace.configuration
  );
  hasWorkspaceFolderCapability = !!(
    capabilities.workspace && !!capabilities.workspace.workspaceFolders
  );
  hasDiagnosticRelatedInformationCapability = !!(
    capabilities.textDocument &&
    capabilities.textDocument.publishDiagnostics &&
    capabilities.textDocument.publishDiagnostics.relatedInformation
  );

  const result: InitializeResult = {
    capabilities: {
      textDocumentSync: TextDocumentSyncKind.Incremental,
      // Tell the client that this server supports code completion.
      completionProvider: {
        resolveProvider: true
      }
    }
  };
  if (hasWorkspaceFolderCapability) {
    result.capabilities.workspace = {
      workspaceFolders: {
        supported: true
      }
    };
  }
  return result;
});

connection.onInitialized(() => {
  if (hasConfigurationCapability) {
    // Register for all configuration changes.
    connection.client.register(DidChangeConfigurationNotification.type, undefined);
  }
  if (hasWorkspaceFolderCapability) {
    connection.workspace.onDidChangeWorkspaceFolders(_event => {
      connection.console.log('Workspace folder change event received.');
    });
  }
});

// The example settings
interface ExampleSettings {
  maxNumberOfProblems: number;
}

// The global settings, used when the `workspace/configuration` request is not supported by the client.
// Please note that this is not the case when using this server with the client provided in this example
// but could happen with other clients.
const defaultSettings: ExampleSettings = { maxNumberOfProblems: 1000 };
let globalSettings: ExampleSettings = defaultSettings;

// Cache the settings of all open documents
let documentSettings: Map<string, Thenable<ExampleSettings>> = new Map();

connection.onDidChangeConfiguration(change => {
  if (hasConfigurationCapability) {
    // Reset all cached document settings
    documentSettings.clear();
  } else {
    globalSettings = <ExampleSettings>(
      (change.settings.languageServerExample || defaultSettings)
    );
  }

  // Revalidate all open text documents
  documents.all().forEach(validateTextDocument);
});

function getDocumentSettings(resource: string): Thenable<ExampleSettings> {
  if (!hasConfigurationCapability) {
    return Promise.resolve(globalSettings);
  }
  let result = documentSettings.get(resource);
  if (!result) {
    result = connection.workspace.getConfiguration({
      scopeUri: resource,
      section: 'languageServerExample'
    });
    documentSettings.set(resource, result);
  }
  return result;
}

// Only keep settings for open documents
documents.onDidClose(e => {
  documentSettings.delete(e.document.uri);
});

// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
documents.onDidChangeContent(change => {
  validateTextDocument(change.document);
});

async function validateTextDocument(textDocument: TextDocument): Promise<void> {
  // In this simple example we get the settings for every validate run.
  let settings = await getDocumentSettings(textDocument.uri);

  // The validator creates diagnostics for all uppercase words length 2 and more
  let text = textDocument.getText();
  let pattern = /\b[A-Z]{2,}\b/g;
  let m: RegExpExecArray | null;

  let problems = 0;
  let diagnostics: Diagnostic[] = [];
  while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
    problems++;
    let diagnostic: Diagnostic = {
      severity: DiagnosticSeverity.Warning,
      range: {
        start: textDocument.positionAt(m.index),
        end: textDocument.positionAt(m.index + m[0].length)
      },
      message: `${m[0]} is all uppercase.`,
      source: 'ex'
    };
    if (hasDiagnosticRelatedInformationCapability) {
      diagnostic.relatedInformation = [
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Spelling matters'
        },
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Particularly for names'
        }
      ];
    }
    diagnostics.push(diagnostic);
  }

  // Send the computed diagnostics to VS Code.
  connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}

connection.onDidChangeWatchedFiles(_change => {
  // Monitored files have change in VS Code
  connection.console.log('We received a file change event');
});

// This handler provides the initial list of the completion items.
connection.onCompletion(
  (_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
    // The pass parameter contains the position of the text document in
    // which code complete got requested. For the example we ignore this
    // info and always provide the same completion items.
    return [
      {
        label: 'TypeScript',
        kind: CompletionItemKind.Text,
        data: 1
      },
      {
        label: 'JavaScript',
        kind: CompletionItemKind.Text,
        data: 2
      }
    ];
  }
);

// This handler resolves additional information for the item selected in
// the completion list.
connection.onCompletionResolve(
  (item: CompletionItem): CompletionItem => {
    if (item.data === 1) {
      item.detail = 'TypeScript details';
      item.documentation = 'TypeScript documentation';
    } else if (item.data === 2) {
      item.detail = 'JavaScript details';
      item.documentation = 'JavaScript documentation';
    }
    return item;
  }
);

// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection);

// Listen on the connection
connection.listen();

添加一个简单的验证

为了向服务端添加文档验证,我们向文本文档管理器添加了一个侦听器,每当文本文档的内容发生更改时都会调用该侦听器。然后由服务器决定验证文档的最佳时间。在示例实现中,服务器验证纯文本文档并标记所有使用全部大写的单词。相应的代码片段如下所示:

// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
documents.onDidChangeContent(async change => {
  let textDocument = change.document;
  // In this simple example we get the settings for every validate run.
  let settings = await getDocumentSettings(textDocument.uri);

  // The validator creates diagnostics for all uppercase words length 2 and more
  let text = textDocument.getText();
  let pattern = /\b[A-Z]{2,}\b/g;
  let m: RegExpExecArray | null;

  let problems = 0;
  let diagnostics: Diagnostic[] = [];
  while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
    problems++;
    let diagnostic: Diagnostic = {
      severity: DiagnosticSeverity.Warning,
      range: {
        start: textDocument.positionAt(m.index),
        end: textDocument.positionAt(m.index + m[0].length)
      },
      message: `${m[0]} is all uppercase.`,
      source: 'ex'
    };
    if (hasDiagnosticRelatedInformationCapability) {
      diagnostic.relatedInformation = [
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Spelling matters'
        },
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Particularly for names'
        }
      ];
    }
    diagnostics.push(diagnostic);
  }

  // Send the computed diagnostics to VS Code.
  connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
});

诊断提示和技巧

  • 如果开始和结束位置相同,VS Code 将在该位置用波浪线下划线。

  • 如果您想用波浪线下划线直到行尾,则将结束位置的字符设置为 Number.MAX_VALUE。

要运行语言服务端,请执行以下步骤:

  • Ctrl+Shift+B 开始构建任务。该任务编译客户端和服务端。

  • 打开 Run 视图,选择 Launch Client 启动配置,然后按 Start Debugging 按钮启动执行扩展代码的 VS Code 的 Extension Development Host 实例。

在根文件夹创建一个test.txt文件,粘贴如下内容:

TypeScript lets you write JavaScript the way you really want to.
TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
ANY browser. ANY host. ANY OS. Open Source.

Extension Development Host 实例将如下所示:

调试客户端和服务端

陶氏客户端代码和调试普通扩展包一样容易,在客户端代码中设置断点,按F5调试扩展包。

由于服务端是由运行在扩展包(客户端)中的LanguageClient 启动的,因此我们需要将调试器附加到正在运行的服务器上。为此,切换到Run视图并选择启动配置Attach to Server并按 F5。这会将调试器附加到服务端。

语言服务的日志支持

如果你使用 vscode-languageclient 来实现客户端,你可以指定[langId].trace.server 来要求客户端把其与服务器之间的通信记录日志记录到,客户端name通道中。
对于 lsp-sample,您可以设置此设置:"languageServerExample.trace.server": "verbose"。然后切换到Language Server Example通道。您应该会看到如下日志:

在服务端使用配置

在编写扩展的客户端部分时,我们已经定义了一个设置来控制最大报告问题数。我们还在服务端编写了代码来从客户端读取这些设置:

function getDocumentSettings(resource: string): Thenable<ExampleSettings> {
  if (!hasConfigurationCapability) {
    return Promise.resolve(globalSettings);
  }
  let result = documentSettings.get(resource);
  if (!result) {
    result = connection.workspace.getConfiguration({
      scopeUri: resource,
      section: 'languageServerExample'
    });
    documentSettings.set(resource, result);
  }
  return result;
}

我们现在唯一需要做的就是监听服务端的配置更改,如果设置发生更改,则重新验证打开的文本文档。为了能够重用文档更改事件处理的验证逻辑,我们将代码提取到 validateTextDocument 函数中并修改代码以支持 maxNumberOfProblems 变量:

async function validateTextDocument(textDocument: TextDocument): Promise<void> {
  // In this simple example we get the settings for every validate run.
  let settings = await getDocumentSettings(textDocument.uri);

  // The validator creates diagnostics for all uppercase words length 2 and more
  let text = textDocument.getText();
  let pattern = /\b[A-Z]{2,}\b/g;
  let m: RegExpExecArray | null;

  let problems = 0;
  let diagnostics: Diagnostic[] = [];
  while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
    problems++;
    let diagnostic: Diagnostic = {
      severity: DiagnosticSeverity.Warning,
      range: {
        start: textDocument.positionAt(m.index),
        end: textDocument.positionAt(m.index + m[0].length)
      },
      message: `${m[0]} is all uppercase.`,
      source: 'ex'
    };
    if (hasDiagnosticRelatedInformationCapability) {
      diagnostic.relatedInformation = [
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Spelling matters'
        },
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Particularly for names'
        }
      ];
    }
    diagnostics.push(diagnostic);
  }

  // Send the computed diagnostics to VS Code.
  connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}

配置更改的处理是通过将配置更改的通知处理程序添加到connection来完成的。对应的代码如下所示:

connection.onDidChangeConfiguration(change => {
  if (hasConfigurationCapability) {
    // Reset all cached document settings
    documentSettings.clear();
  } else {
    globalSettings = <ExampleSettings>(
      (change.settings.languageServerExample || defaultSettings)
    );
  }

  // Revalidate all open text documents
  documents.all().forEach(validateTextDocument);
});

再次启动客户端并将最大报告问题数更改为1,会导致以下验证结果:

添加额外的语言功能

语言服务通常实现的第一个有趣的功能是文档验证。从这个意义上说,即使是 linter 也算作语言服务,在 VS Code 中,linter 通常被实现为语言服务(参见 eslintjshint 示例)。但是语言服务还有更多。它们可以提供代码完成、查找所有引用或转到定义。下面的示例代码向服务端添加了代码完成功能。它提出了“TypeScript”和“JavaScript”这两个词。

// This handler provides the initial list of the completion items.
connection.onCompletion(
  (_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
    // The pass parameter contains the position of the text document in
    // which code complete got requested. For the example we ignore this
    // info and always provide the same completion items.
    return [
      {
        label: 'TypeScript',
        kind: CompletionItemKind.Text,
        data: 1
      },
      {
        label: 'JavaScript',
        kind: CompletionItemKind.Text,
        data: 2
      }
    ];
  }
);

// This handler resolves additional information for the item selected in
// the completion list.
connection.onCompletionResolve(
  (item: CompletionItem): CompletionItem => {
    if (item.data === 1) {
      item.detail = 'TypeScript details';
      item.documentation = 'TypeScript documentation';
    } else if (item.data === 2) {
      item.detail = 'JavaScript details';
      item.documentation = 'JavaScript documentation';
    }
    return item;
  }
);

data字段用于唯一标识解析处理程序中的完成项。data属性对于协议是透明的。由于底层消息传递协议是基于 JSON 的,因此 data 字段应该只保存可序列化为JSON 或从 JSON 序列化的数据。
剩下的就是告诉 VS Code, 服务端支持代码完成请求的功能。为此,请在初始化处理程序中标记相应的功能:

connection.onInitialize((params): InitializeResult => {
    ...
    return {
        capabilities: {
            ...
            // Tell the client that the server supports code completion
            completionProvider: {
                resolveProvider: true
            }
        }
    };
});
下面的屏幕截图显示了在纯文本文件上运行的已完成的代码:
![](https://code.visualstudio.com/assets/api/language-extensions/language-server-extension-guide/codeComplete.png)

### 测试语言服务
要创建高质量的语言服务,我们需要构建一个良好的测试套件,涵盖其功能。有两种常见的测试语言服务方式:
* 单元测试:通过mock up发送所有信息,来在语言服务中测试特定功能。 VS Code的HTML / CSS / JSON语言服务采用这一方法进行测试。 LSP NPM模块也使用这一方法。使用NPM协议模块编写单元测试,请参阅[这儿](https://github.com/microsoft/vscode-languageserver-node/blob/main/protocol/src/node/test/connection.test.ts)。
* 端到端测试:这类似于[VS Code扩展包测试](https://code.visualstudio.com/api/working-with-extensions/testing-extension)。这种方法的好处是,它运行测试的方式是:使用工作空间实例化VS Code实例,打开文件,激活语言客户端/服务端,运行[VS Code命令](https://code.visualstudio.com/api/references/commands)。如果你有难以mockup的文件、设置或依赖关系(例如Node_Modules),则此方法更好。流行的Python扩展包采用这种方法进行测试。

你可以在选择的任何测试框架中进行单元测试。在这里,我们介绍了如何为语言服务扩展包进行端到端测试。
打开`.vscode / launch.json`,您可以找到`E2E`测试目标:
```ts
{
  "name": "Language Server E2E Test",
  "type": "extensionHost",
  "request": "launch",
  "runtimeExecutable": "${execPath}",
  "args": [
    "--extensionDevelopmentPath=${workspaceRoot}",
    "--extensionTestsPath=${workspaceRoot}/client/out/test/index",
    "${workspaceRoot}/client/testFixture"
  ],
  "outFiles": ["${workspaceRoot}/client/out/test/**/*.js"]
}

如果您运行这个调试目标,它将使用Client / TestFixture启动VS code实例作为活动工作区。然后,VS Code将继续执行client/ src / test中的所有测试。有个调试技巧,您可以在Client / SRC / Test中的Typescript文件中设置断点,并执行到断点处。
让我们来看看completion.test.ts文件:

import * as vscode from 'vscode';
import * as assert from 'assert';
import { getDocUri, activate } from './helper';

suite('Should do completion', () => {
  const docUri = getDocUri('completion.txt');

  test('Completes JS/TS in txt file', async () => {
    await testCompletion(docUri, new vscode.Position(0, 0), {
      items: [
        { label: 'JavaScript', kind: vscode.CompletionItemKind.Text },
        { label: 'TypeScript', kind: vscode.CompletionItemKind.Text }
      ]
    });
  });
});

async function testCompletion(
  docUri: vscode.Uri,
  position: vscode.Position,
  expectedCompletionList: vscode.CompletionList
) {
  await activate(docUri);

  // Executing the command `vscode.executeCompletionItemProvider` to simulate triggering completion
  const actualCompletionList = (await vscode.commands.executeCommand(
    'vscode.executeCompletionItemProvider',
    docUri,
    position
  )) as vscode.CompletionList;

  assert.ok(actualCompletionList.items.length >= 2);
  expectedCompletionList.items.forEach((expectedItem, i) => {
    const actualItem = actualCompletionList.items[i];
    assert.equal(actualItem.label, expectedItem.label);
    assert.equal(actualItem.kind, expectedItem.kind);
  });
}

在这个测试中,我们:

  • 激活扩展包
  • 使用URI和位置运行命令vscode.executeCompletionItemprovider,来模拟完成触发。
  • 根据我们预期的完成项目断言返回的完成项目。
    让我们深入了解activate(docURI)功能。它是在client/ src / test / helper.ts中定义的:
import * as vscode from 'vscode';
import * as path from 'path';
export let doc: vscode.TextDocument;
export let editor: vscode.TextEditor;
export let documentEol: string;
export let platformEol: string;

/**
 * Activates the vscode.lsp-sample extension
 */
export async function activate(docUri: vscode.Uri) {
  // The extensionId is `publisher.name` from package.json
  const ext = vscode.extensions.getExtension('vscode-samples.lsp-sample')!;
  await ext.activate();
  try {
    doc = await vscode.workspace.openTextDocument(docUri);
    editor = await vscode.window.showTextDocument(doc);
    await sleep(2000); // Wait for server activation
  } catch (e) {
    console.error(e);
  }
}

async function sleep(ms: number) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

在激活部分中,我们:

  • 使用{publisher.name}.{extensionId}获取扩展, 并在package.json中定义。
  • 打开指定的文档,并在活动文本编辑器中显示。
  • 睡眠2秒,所以我们确保语言服务已经实例化。

准备完成后,我们可以运行与每个语言功能相对应的VS Code命令,并根据返回的结果断言。
另外还有一个涵盖我们刚刚实施的诊断功能的测试, 可以查看client/ src / test / diagnostics.test.ts文件。

高级主题

到目前为止,本指南涵盖:

  • 语言服务和语言服务协议的简要概述。
  • VS Code中语言服务扩展包的体系结构
  • lsp-Sample扩展,以及如何开发/调试/检查/测试它。
    有一些更高级的主题不太适合在本指南中叙述。我们将在进一步学习语言服务开发时,给出这些资源的链接。

其他语言服务的功能

语言服务器目前支持以下语言功能以及代码完成:

  • 文档高亮:突出显示文本文档中的所有“相等”符号。
  • 悬停:为在文本文档中选择的符号提供悬停信息。
  • 签名帮助:为文本文档中选择的符号提供签名帮助。
  • 转到定义:为文本文档中选择的符号提供转到定义的支持。
  • 转到类型定义:为文本文档中选择的符号提供转到类型/接口定义的支持。
  • 转到实现:为文本文档中选择的符号提供转到实现定义的支持。
  • 查找引用:查找在文本文档中选择的符号的所有项目范围引用。
  • 列出文档符号:列出文本文档中定义的所有符号。
  • 列表工作区符号:列出所有项目范围的符号。
  • 代码操作:计算要为给定文本文档和范围运行的命令(通常是美化/重构)。
  • 代码长度Codelens:计算给定文本文档的代码长度统计信息。
  • 文档格式化:这包括格式化的整个文档,部分文档和按类型格式化。
  • 重命名:项目范围的符号重命名。
  • 文档链接:计算和解析文档中的链接。
  • 文档颜色:在文档中计算和解析颜色, 并编辑器中提供取色器。

程序语言功能主题描述了上面的每个语言功能,并指导你通过语言服务协议、或直接从你的扩展包中使用可扩展性API,来实现它们。

增量文本文档同步

根据知乎文章https://zhuanlan.zhihu.com/p/48968584, 对于一项著作权的估值, 就是三种方式.
一是看做这个花了多少钱, 二是看别人做的卖多少钱, 三是看自己做的能卖多少钱.
话说回来, 这三种方法似乎也适用于其他一切东西的估值.

缘起

2021/11.
想选基于vue的UI框架, 搜索一圈后, 发现比较集中于以下3个选项:

  • 饿了么的ElementUI
  • Vuetify
  • Quasar
    通过知乎的了解, 似乎ElementUI上手容易但永久了会发现一些小毛病. 于是在Vuetify和Quasar里面选择.
    看了两者的安装文档, 发现Vuetify是完全基于vue-cli的, 而Quasar更推荐使用自己的cli工具. 尝试在vuetify基础上再装Quasar, 发现结果不能渲染... 也许是某种冲突? 另外Quasar是默认使用vue3, 而vuetify默认是vue2. 目前这个时间看vue-cli默认的还是vue2, 那就先用基于vue-cli和vue2的vuetify吧, 再者说了, EvanYou在知乎上也推荐的是这个.

文档

不少知乎上的回答者说vuetify的文档跟屎一样... 我开始也是摸不到门儿, 按文档的Getting Started安装以后, 后面的章节不是按入门学习逻辑顺序排列而是按字母序排列的, 哈...
另外还有个问题是, 中文翻译的文档/网站和英文存在一定的差异性. 我开始是在中文下阅读的, 读到一个莫名其妙的断点的概念的时候, 点链接进去竟然是响应式布局! 连忙切换英文看看, 原来这一章节名字就是Display Breakpoints 显示断点, 而译者意译为了响应式布局, 因为断点的作用就是为了响应式布局. 然而, 除了标题以外,其他地方的翻译又统统变成了断点. 这个时候忽然看了下右上角菜单有个Learn, 里面有个Guide!这不就是要找的学习指南吗?! 点进去, 这个章节竟然不在Getting Started里面, 而是Introduction->Why Vuetify文章的一个子章节! 我想很多看文档的人是为了开始学习, 大概已经不需要告诉自己为什么还要用Vuetify了吧, 估计都会跳过这最重要的入门一章不看了.

安装

倒也简单. 基于vue-cli的. vue-cli的安装方式和以前不一样了. 以前是npm i -g vue-cli,现在变成了:

npm i -g @vue/cli

安装完以后, 创建一个项目, 再加上vuetify插件支持就搞定!

npm create someproject
cd someproject
vue add vuetify

生产环境编译使用npm run build,会在dist目录下生成目标文件.
开发环境编译使用npm run serve. 提示建立了服务器后, 在浏览器打开即可. 后面开发保存即编译. 后面语法错误/编译问题都会在浏览器中查阅.

源文件

都在src目录中. 其中:

  • 入口文件main.js是不需要改的.
  • app.vue是页面框架, 包括页头页尾和中间的内容, 其中页头页尾需要在这儿修改. Vuetify的根标签是, 内容标签是. 页头可以用
  • components/HelloWorld.vue是内容区, 主要修改的内容在这儿. 当然HelloWorld这个名称根据自己需要修改就好了.
  • plugins/vuetify.js在需要配置修改vuetify的时候, 就需要修改这个文件, 而不是修改main.js哦.
  • 自定义css的话, 需要建立一个sass/variables.scss的目录和文件.

基础概念

接着看文档的Why Vuetify最后一个章节功能指南.
双向性可以跳过, 反正我们都是从左到右的.
全局配置可以花10秒看一下, 里面只有一句话就是在vuetify.js中可以配置一个全局参数Vuetify.config且目前只有一个布尔值silent能配置.

海拔

所有元素都支持elevation=n属性, n取值0~24.

图标icon

快速了解下, 就是Vuetify默认带了一套Material Design的图标, 可以直接在Vue的Template里面调用. 图标名称是mdi-加上Material Design图标网站中搜索出来的图标名称. 不过要注意的是, 其中的图标有可能部分在@mdi/js图标库中没有. 这时候可以试着重新安装最新的版本npm install @mdi/js -D

<v-icon large color="yellow">mdi-dots-horizontal</v-icon>

有一系列的大小属性: x-small, small, medium (默认), large, and x-large
有left/right的位置属性, 可以放置在v-btn内,指示图标的位置.
颜色可以通过color属性修改

要将图标嵌入打包的文件, 而不是每次都从CDN上获取, 查看https://vuetifyjs.com/zh-Hans/features/icon-fonts/#material-design56fe6807, 这儿也可以搜索图标名称.
方法是:

$ yarn add @mdi/js -D
// 或
$ npm install @mdi/js -D
// src/plugins/vuetify.js

import Vue from 'vue'
import Vuetify from 'vuetify/lib'

Vue.use(Vuetify)

export default new Vuetify({
  icons: {
    iconfont: 'mdiSvg',
  },
})

在.vue文件中如下书写,导入需要的图标

<!-- Vue Component -->

<template>
  <v-icon>{{ svgPath }}</v-icon>
</template>

<script>
  import { mdiAccount } from '@mdi/js'

  export default {
    data: () => ({
      svgPath: mdiAccount
    }),
  }
</script>

布局layouts

文档说的太简单, 就给了一个下面的格式.

<v-app>
  <v-app-bar app></v-app-bar>

  <v-main>
    <v-container>
      Hello World
    </v-container>
  </v-main>
</v-app>

除了前面说过的v-app, v-app-bar, v-main以外, 还有一个v-footer. 其中v-app-barv-footer需要与v-main并列, 而且需要有app这个属性. 有了这个属性后, 页头页脚才会挤占main的空间, 并且在每个页面上都显示出来.
v-container属于网格布局Grid的一部分, 后面讲到网格布局的时候再说.
跟布局相关的有这么几个

  • 断点breakpoints
  • 网格grids
  • 弹性布局flex
  • 显示辅助display Helpers属性

主题Themes

需要修改vuetify.js文件.
如下.
默认的颜色库需要单独引入.
默认的主题名称是light, 如果不需要换主题的话, 只需要在这儿增加light就可以.

// src/plugins/vuetify.js

import Vue from 'vue'
import Vuetify from 'vuetify/lib'

import colors from 'vuetify/lib/util/colors'

const vuetify = new Vuetify({
  theme: {
    themes: {
      light: {
        primary: colors.purple,
        secondary: colors.grey.darken1,
        accent: colors.shades.black,
        error: colors.red.accent3,
      },
      dark: {
        primary: colors.blue.lighten3,
      },
    },
  },
})

主题的使用方式, 如建一个按钮, 使用primary主题, 定义color属性即可.

            <v-btn tile outlined color="primary">
              <v-icon left>mdi-pencil</v-icon> Edit
            </v-btn>

断点布局breakpoint

不知道为什么要起这样奇怪的名字. 断点实际上是设备的大小分类. 从超小号到超大号分成了5类. 其中超小号是默认类

  • xs, 超小号, 指的是手机
  • sm, 小号, 指的是平板
  • md, 中号, 指的是笔记本电脑(或特大号平板)
  • lg, 大号, 指的是台式机
  • xl, 超大号, 指的是4k显示设备

网格Grid

网格是12列的
网格由上到下结构依次为<v-container>, <v-row>, <v-col>也就是面-行-列的顺序. 其中<v-row>标准是24px的.
另外还有一个<v-spacer>可以夹在中间或者中间, 用于均分剩余的空白.

  • no-gutters属性在一行中不留"排水沟", 也就是列与列中间没有空隙.
  • align属性是垂直对齐方式, 也适用于v-col
  • justify属性是水平对齐方式, 如justify="end"是所有列对齐到最后面

内容可以是<v-card>, 感觉类似一个div.

  • cols属性指定占用的列数, 如cols=3. 不指定则所有列均分. 其他尺寸屏幕下直接用断点名称指定, 如sm=4
  • offset属性是偏移的列数, 如offset=2, 其他尺寸用offset-断点指定, 如offset-sm=2
  • order属性是指定位置. order=数字, 数字越小越靠前. 但没有order属性的将排在最前面. order="first"是排在第一 order="last"是排在最后.

显示辅助display Helpers

实际是一类class,以d-开头, 指示标签css的display属性类型以及适配的断点类型.
包括none,inline, block, inline-block, flex, inline-flex,table, table-row, table-cell等. 如d-inline. 其中d-none是隐藏的意思.
其他屏幕大小, 将断电名放在中间, 如d-sm-inline

flex弹性布局

  • 父元素class都是d-flex就可以使用.
  • 父元素class可以增加flex-方向可以设置浮动方向(轴),包括flex-row, flex-row-reverse, flex-column, flex-column-reverse, 相当于改变css的flex-direction
  • 父元素class增加justify-位置来设置沿轴浮动位置, 包括justify-start, justify-center, justify-end, justify-space-between, justify-space-around, 相当于改变css的justify
  • 父元素class增加align-位置来设置垂直于轴的浮动位置, 包括align-start, align-, align-center, align-end, align-baseline, align-stretch, 相当于改变css的align-items.
  • 元素自身class增加align-self-位置可修改自身的位置. 相当于改变css的align-self
  • 元素自身class增加m位置字母-auto, (位置字母是l, r, t, b之一, 指的是left/right/top/bottom)似乎可以让此元素的对应位置(m指的是margin)充满空间, 将其他元素挤到边上.
  • 元素自身class增加order-数字可以排序. n=0~12,或者first/last
  • 元素自身class增加flex-grow-布尔值``flex-shrink-布尔值可以让此元素在空间不足的时候挤占其他元素空间或者被其他元素挤占.

间距margin/padding

是一组class, 构成是这样的:
m位置字母-可选断点-数字p可选位置字母-可选断点-数字

  • 位置字母是a/l/r/t/b/x/y
  • 可选断点是断点名称
  • 数字为n16(负16)~16, 每加1表示4px.
    举个例子,如ma-4',pa-n5,mr-sm-7`

字体

全部通过class设置.
字体大小是一组class
text-值, 具体为如下之一:

text-h1
text-h2
text-h3
text-h4
text-h5
text-h6
text-subtitle-1
text-subtitle-2
text-body-1
text-body-2
text-button
text-caption
text-overline

字体重量一组class, 具体为如下之一:

font-weight-black
font-weight-bold
font-weight-medium
font-weight-regular
font-weight-light
font-weight-thin

斜体是font-italic
文字横向位置:

text-left
text-center
text-right