Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面。
Fair是58技术开源的一个Flutter动态化的框架,能够实现UI和逻辑的动态化。
开发者在使用Fair开发过程中存在一些痛点,比如可能会出现使用语法糖不正确或者存在不支持的语法糖问题,所以我们需要一个配套插件去提示用户使用Fair语法糖。
在IDE中,Flutter语法检测机制是依赖Dart/Flutter插件实现的,即我们在开发Flutter前需要下载的Dart/Flutter插件。我们需要通过插件去提供Flutter的开发环境,同时插件也能提供语法检测功能。而插件其中的一个核心功能就是Analysis Server。
Analysis Server是Dart SDK提供的一个Dart/Flutter语法分析服务,主要功能包括语法静态分析、代码提示、代码补全等。我们常用的Dart/Flutter IDE如intellij、Android Studio和VS Code都是通过安装Dart插件实现Dart开发环境的配置。
以Android Studio为例(因为对应插件的语言是Java,比较好阅读和理解),语法检测核心是Analysis Server,每当用户代码有改动的时候,就会通过Socket的方式同步给Analysis Server,当Analysis Server分析结束后也会将分析结果返回来,Dart插件则就是根据Analysis Server返的事件类型去做不同的处理,最终将结果刷新在用户IDE界面上。流程图如下:
当用户配置完Dart SDK路径后,插件会获取到配置路径,在插件启动的时候会启动一个进程去执行Dart SDK里的dart可执行文件,同时也会获取到Dart SDK目录下的"/bin/snapshots/analysis_server.dart.snapshot"文件,并将其路径作为vmArguments。我们可以看一小段代码:
//获取配置的SDK路径
mySdkHome = sdk.getHomePath();
//找到dart可执行文件的路径
final String runtimePath = FileUtil.toSystemDependentName(DartSdkUtil.getDartExePath(sdk));
//找到analysis_server.dart.snapshot文件路径
String analysisServerPath = FileUtil.toSystemDependentName(mySdkHome + "/bin/snapshots/analysis_server.dart.snapshot");
//拼凑vmArguments
String firstArgument = useDartLangServerCall ? "language-server" : analysisServerPath;
//创建Socket
myServerSocket =
new StdioServerSocket(runtimePath, StringUtil.split(vmArgsRaw, " "), firstArgument, StringUtil.split(serverArgsRaw, " "), debugStream);
//创建AnalysisServer实现类
final RemoteAnalysisServerImpl startedServer = new RemoteAnalysisServerImpl(myServerSocket);
//这里的具体实现其实就是socket.start(),将socket启动起来
startedServer.start();
Dart插件与Analysis Server交互类型主要分为两种,一种是id,一种是event。每次Dart插件给Analysis Server同步数据时都会生成一个uniqueId并缓存在HashMap,每次Analysis Server返数据的时候也会带上event或id,优先处理event类型数据。
Dart插件同步给Analysis Server数据示例:
request: {
"id": String
"method": "analysis.setAnalysisRoots"
"params": {
"included": List<FilePath>
"excluded": List<FilePath>
"packageRoots": optional Map<FilePath, FilePath>
}
}
上述说到Dart插件每次给Analysis Server同步数据都会生成一个uniqueId,这里的生成规则就是通过具备原子属性的AtomicInteger的getAndIncrement()方法每同步一次数据+1。Dart插件对应Analysis Server每种事件实现一个Consumer或Processor(如果是event类型数据则是Processor,result事件类型的话就是Consumer)。
插件是有一个挂载过程的,插件的挂载流程如下图所示:
小结一下:
当我们掌握了前面的前置知识,再去开发语法插件就简单许多了。
Dart插件是基于Analysis Server实现了Dart语法的检测,Fair语法检测插件则是对Dart插件语法检测功能的补充和扩展,实现对Fair语法糖的检测,Fair语法插件本质页是一个Dart语法检测插件,可以通过配置pubspec.yaml和analysis_options.yaml来使用。
主要可以分为以下几步:
//示例
void main(List<String> args, SendPort sendPort) {
ServerPluginStarter(FairPlugin(PhysicalResourceProvider.INSTANCE)).start(sendPort);
}
Fair语法检测插件的核心就是抽象语法树遍历,通过对抽象语法树的遍历,获取到每个子节点,然后插入自定义语法检测逻辑。
createAnalysisDriver()是ServerPlugin的一个抽象方法,开发者实现这个方法创建一个AnalysisDriver,然后我们可以通过AnalysisDriver的results数据流去监听AnalysisResult的返回,其中这个AnalysisResult就是Analysis Server对文件的分析结果。示例代码如下所示:
@override
AnalysisDriverGeneric createAnalysisDriver(plugin.ContextRoot contextRoot) {
//获取contextRoot
final contextRoots =
ContextLocator(resourceProvider: resourceProvider).locateRoots(...);
//获取ContextBuilder
final contextBuilder =
ContextBuilderImpl(resourceProvider: resourceProvider);
//获取Analysis Context
final context = contextBuilder.createContext(
contextRoot: contextRoots.first, byteStore: byteStore);
//获取Analysis Driver
final dartDriver = context.driver;
//监听分析结果
dartDriver.results.listen((analysisResult) {_processResult(...);});
return dartDriver;
}
ServerPlugin提供contentChanged方法,我们只需要收到回调的时候将内容有改变的文件路径添加到AnalysisDriver即可
@override
void contentChanged(String path) {
super.driverForPath(path)?.addFile(path);
}
Dart的抽象语法树遍历是通过访问者模式实现的,我们可以在我们想要处理的节点插入一个自定义的Visitor对其及其子节点实现访问。其中我们可以从AnalysisResult拿到编译单元(CompilationUnit),每个编译单元就是一棵抽象语法树,我们可以通过其accept()方法插入一个Visitor。
我们了解到Dart抽象语法树是通过访问者模式实现的,就能够很简单地去实现自定义的逻辑了。我们要实现@FairPatch注解的识别,我们只需要在遍历Annotation节点时,判断一下annotation name是我们想要的FairPatch即可。示例代码如下:
class _FairVisitor extends RecursiveAstVisitor<void> {
@override
void visitAnnotation(Annotation node) {
node.visitChildren(this);
if (node.name.name == \'FairPatch\') {
//...
}
}
}
因为if语法检测是需要前置条件的,首先得是用@FairPatch标注的类,其次还需要是在build()方法下的if才进行检测。
@override
void visitMethodDeclaration(MethodDeclaration node) {
if (node.name.name == \'build\') {
//...
}
}
上述两张图分别对应IfStatement和IfElement具体含义。
在Fair中常用的Sugar.if相关语法糖有Sugar.ifEqual、Sugar.ifEqualBool和Sugar.ifRange
static K ifRange<T, K>(
T actual,
List<T> expect, {
required K trueValue,
required K falseValue,
}) =>
expect.contains(actual) ? trueValue : falseValue;
static K ifEqual<T, K>(
T actual,
T expect, {
required K trueValue,
required K falseValue,
}) =>
expect == actual ? trueValue : falseValue;
static K ifEqualBool<T, K>(
bool state, {
required K trueValue,
required K falseValue,
}) =>
state ? trueValue : falseValue;
使用Fair语法糖能够加快Fair的编译,所以我们都更推荐能使用语法糖就使用语法糖,我们要实现对语法糖分类,其实就是对if的condition进行区分,其中ifRange稍微复杂一点,则我们再讲下如何实现,其他也同理:
//ifRange有个前置条件就是如参expect是一个List,所以再检测的时候只需要判断用户在调用list.contains(xx)即可
//先判断用户是在调用contains()方法
@override
void visitMethodDeclaration(MethodDeclaration node) {
if (node.name.name == \'contains\') {
//...
}
}
//其次再判断调用contains方法的对象是List
@override
void visitMethodInvocation(MethodInvocation node) {
super.visitMethodInvocation(node);
//判断staticType即可
_result = node.target?.staticType?.isDartCoreList ?? false;
if (_result) {
_target = node.target;
_actual = node.argumentList.arguments.first;
}
}
欢迎大家使用 Fair,也欢迎大家为我们点亮star
<br>
Github地址:https://fair.58.com
Fair官网:https://fair.58.com
<br>
通过Issue提交问题,贡献代码请提交Pull Request,管理员将对代码进行审核。
对Fair感兴趣的小伙伴,可以加入交流群。
微信 |
---|
微信入群:请先添加58技术小秘书为好友,备注fair,小秘书邀请进群。
|