不讲概念,螺蛳壳里搞架构,写给初学者(一)

469 阅读7分钟

前言

背景

大家好,我是尘码,在鹅厂待过几年,带队从0到1搞过中大型前端项目。经历并主导了从jquery到vue,从刀耕火种到工程化,从CRUD重构到低代码的完整过程。

我发现,很多架构文章,都处在一个事后诸葛亮的角度,高屋建瓴的来讲架构设计。以至于架构设计文章对读者而言,懂的本身就懂,不懂的看了也云里雾里。

于是,我想从一个初学者的角度切入,在螺蛳壳里做道场,来讲讲架构设计这一件事,一步步揭开架构的神秘面纱。

章节设计

  1. 第一章 从需求出发,规划功能,浅谈思路,做好调研,实现demo,发布上线。
  2. 第二章 从demo出发,巩固基础,分析问题,逐步重构,搭建工程,完善产品。
  3. 第三章 从产品出发,丰富场景,落实理论,完善基建,掌握主动,巩固地位。
  4. 第四章 从整体出发,确定愿景,整合资源,提升名气,建立社区,完善生态。

第一章

第一步,要有一个需求

当我用 VSCode 写代码时,我发现了一个频繁的操作:

我总会打开某个文件,只是为了看看该文件 export 了什么。比如下面的文件 export 了 5 个函数: loadCode, loadJson, solveExports 等等

image.png

然后再打开另外一个文件,写下 import { loadJson } from '../xxx'

image.png

也许,我们可以开发一个VSCode插件,来优化这里的开发体验。

第二步,设计出产品

显然,上面的需求可以抽象成两个问题:

  1. 更方便的展示export
  2. 更方便的写出import

显然,我们可以想到下面两个功能

  1. 直接在文件目录展示export

    image.png
  2. 支持拖拽exports到文件中。

    xmouse-file-drag.gif

PS:该插件已发布,VSCode插件商店搜索XMouse即可。

也可以点击下方链接直达商店页面:

marketplace.visualstudio.com/items?itemN…

第三步,快速调研核心阻塞点

产品清晰了,就来看实现。对于我们来说,有两个陌生的地方:

  1. 怎么获取export?
  2. 怎么展示在vscode中?

让我们挨个思考。

3.1 怎么获取export?

在2024年,相信聪明的你首先想到的就是ChatGPT,回答如下:

image.png

好,现在给你三秒思考选择什么?

1……2……3……,时间到!

我选正则表达式,由于我们目前是demo,当然选最简单的。于是顺手让GPT生成一下代码:

const fs = require('fs');

// 读取 JavaScript 文件内容
const jsCode = fs.readFileSync('example.js', 'utf8');

// 正则表达式匹配 export 语句
const exportRegex = /export\s+(?:var|let|const|function|class|default)?\s*(\w+)/g;
let match;
const exports = [];

while ((match = exportRegex.exec(jsCode)) !== null) {
    exports.push(match[1]);
}

console.log('Exports:', exports);

霍,挺简单。

我们完全可以直接打开Chrome, 按下F12 试一试 image.png

很顺利。(虽然有一点小瑕疵,但不重要,后期再优化)

3.2 怎么展示在vscode中?

同样的,遇事不决,GPT4.(国内有挺多免费站的,随便找一个白嫖就好)

image.png

看起来有些新的名词,比如 TreeDataProvider。

此时我们只需要网上查询查询关键词了解下,便可以得到回到:

image.png

第一个搜索结果就是官方文档,描述清晰,符合预期,说明可行。

image.png

第四步,快速搞出demo

经过第三步的调研,我们只欠缺VSCode的使用。

4.1 那么先从HelloWorld开始

根据VSCode官方教程,我们仅需新建文件夹,并执行如下命令即可。

cmd npx --package yo --package generator-code -- yo code

执行之后我们发现启动了一个命令行工具,瞎选就完事,先上车再说。

image.png

生成项目之后,根据官方指引,我们可以按 F5 进行调试。

image.png

然后我们发现 VSCode 打开了一个新的窗口,我们在这个窗口中,用键盘上按下 Ctrl+Shift+P 组合键,便可以找到Hello World 命令,执行即可。

image.png

4.2 接着让我们理解理解 Hello World

总所周知,看 js 源码从package.json开始.

从package.json中,我们发现main文件指向'./dist/extention',这明显是打包之后的路径。

image.png

按江湖惯例在目录中找找,果然发现了 webpack.config.js , 里面指向了 src/extension.ts

image.png

跟着让我们打开 src/extension.ts 文件,发现目标:

原来要实现hello world,只需要在 activate 函数中,注册命令即可。

image.png

从demo代码不难看出两点:

  1. 从 activate和deactivate顾名思义,这是 VSCode插件 的生命周期。
  2. 从 context.subscriptions.push( xxx ) 顾名思义,VSCode 以发布订阅的方式提供 Hook。

这很好,很干净,也是常见的微内核架构,我会在后续的章节中详聊。

4.3 实现TreeView

现在,我们再回过头看细致研究下TreeView,网上翻一翻,找到两个教程:

  1. 掘金 VSCode系列教程-Treeview
  2. 官方 Treeview教程

总结一下, 需要做两步:

  1. package.json 中加添加下面这一段配置
// package.json 中加添加下面这一段配置

{
  ...
  "contributes": {
    "views": {
      "explorer": [
        {
          "id": "XMouse.helloTree",
          "name": "HelloWorldTree"
        }
      ]
    }
  },
 ...
}
  1. extention.ts中添加下面的代码
import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
	// Hello World
	context.subscriptions.push(
		vscode.commands.registerCommand('XMouse.helloWorld', () => {
			vscode.window.showInformationMessage('Hello World from XMouse!');
		})
	)

	// TreeView
	vscode.workspace.workspaceFolders?.forEach((uri) => {
		vscode.window.createTreeView('XMouse.helloTree', {
			treeDataProvider: new HelloTreeProvider(uri.uri.fsPath)
		});
	})
}
export class HelloTreeProvider implements vscode.TreeDataProvider<vscode.TreeItem> {
	constructor(private workspaceRoot: string) { }
	getTreeItem(element: vscode.TreeItem): vscode.TreeItem {
		return element;
	}
	getChildren(element?: vscode.TreeItem): Thenable<vscode.TreeItem[]> {
		return Promise.resolve([
			new vscode.TreeItem('包子'),
			new vscode.TreeItem('馒头'),
			new vscode.TreeItem('榨菜'),
			new vscode.TreeItem('稀饭', vscode.TreeItemCollapsibleState.Collapsed)
		])
	}
}

然后咱们按下F5体验一下咱们的TreeView,果然出现了包子馒头

image.png

对于Pakcage.json就不说了,只是一个规定,学会怎么配置就行。

对于TreeDataProvider,教程里描述如下,我就不重复了。

image.png

4.4 让我们实现一下拖拽

同样的,查找一下GPT、Google上Treeview拖拽相关教程就行。

只需要简单改一下 4.3小节 中的 extention.ts ,加一个类就行。

export function activate(context: vscode.ExtensionContext) {
  // TreeView
  vscode.workspace.workspaceFolders?.forEach((uri) => {
    vscode.window.createTreeView('XMouse.helloTree', {
      treeDataProvider: new HelloTreeProvider(uri.uri.fsPath),
      dragAndDropController: new TreeViewDragDrop()
    });
  })
}


export class HelloTreeProvider implements vscode.TreeDataProvider<vscode.TreeItem> {}

export class TreeViewDragDrop implements vscode.TreeDragAndDropController<vscode.TreeItem> {
  dropMimeTypes: readonly string[] = ['xmouse/drop'];
  dragMimeTypes: readonly string[] = ['xmouse/drag'];
  public async handleDrop(target: vscode.TreeItem | undefined, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<void> {
    console.log('drop', target, dataTransfer, token)
  }
  public async handleDrag(source: vscode.TreeItem[], treeDataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<void> {
    console.log('drag', source, treeDataTransfer, token)
    treeDataTransfer.set('text/plain', new vscode.DataTransferItem(source[0].label));
  }
}

然后我们便可以愉快的拖拽包子到editor中嘞。

xmouse-file-drag-done.gif

4.5 再让我们实现一下文件目录和exports的展示

以我们的实力,其实到这儿就很简单了,首先修改下TreeProvider,写一个简单的目录


export class HelloTreeProvider implements vscode.TreeDataProvider<vscode.TreeItem> {
  constructor(private workspaceRoot: string) { }
  getTreeItem(element: vscode.TreeItem): vscode.TreeItem {
    return element;
  }
  getChildren(element?: vscode.TreeItem): vscode.ProviderResult<vscode.TreeItem[]> {
    return new Promise((resolve) => {
      const uri = element?.resourceUri || vscode.Uri.file(this.workspaceRoot)
      vscode.workspace.fs.readDirectory(uri).then((directories: [string, vscode.FileType][]) => {
        const treeItems = directories.map(([name, type]) =>
          new vscode.TreeItem(
            vscode.Uri.joinPath(uri, name),
            type === vscode.FileType.Directory
              ? vscode.TreeItemCollapsibleState.Collapsed
              : vscode.TreeItemCollapsibleState.None
          )
        )
        resolve(treeItems)
      })
    })
  }
}

然后再用刚才GPT给的正则,写一下export的分析,改一改 getChildren 逻辑如下

有点复杂了,对吧?

这很好,说明你发现了可读性的问题。

我在下一章会解决他。


export class HelloTreeProvider implements vscode.TreeDataProvider<vscode.TreeItem> {
  constructor(private workspaceRoot: string) { }
  getTreeItem(element: vscode.TreeItem): vscode.TreeItem {
    return element;
  }
  getChildren(element?: vscode.TreeItem): vscode.ProviderResult<vscode.TreeItem[]> {
    return new Promise(async (resolve) => {
      const uri = element?.resourceUri || vscode.Uri.file(this.workspaceRoot)
      const extname = path.extname(uri.fsPath)
      if (['.js', '.jsx', '.ts', '.tsx'].includes(extname)) {
        const exports = await this.solveExports(uri)
        const treeItem =  exports.map(item => {
          return new vscode.TreeItem(item)
        })
        resolve(treeItem)
      }


      const directories: [string, vscode.FileType][] = await vscode.workspace.fs.readDirectory(uri);
      const treeItems = directories.map(([name, type]) => {
        const resourceUri = vscode.Uri.joinPath(uri, name);
        const extname = path.extname(resourceUri.fsPath)
        if (['.js', '.jsx', '.ts', '.tsx'].includes(extname)) {
          return new vscode.TreeItem(resourceUri, vscode.TreeItemCollapsibleState.Collapsed)
        }
        const collapsibleState = type === vscode.FileType.Directory
          ? vscode.TreeItemCollapsibleState.Collapsed
          : vscode.TreeItemCollapsibleState.None
        return new vscode.TreeItem(resourceUri, collapsibleState)
      })
      resolve(treeItems)
    })
  }
  async solveExports(uri: vscode.Uri) {
    const file: Uint8Array = await vscode.workspace.fs.readFile(uri)
    const jsCode = file.toString();

    // 正则表达式匹配 export 语句
    const exportRegex = /export\s+(?:var|let|const|function|class|default)?\s*(\w+)/g;
    let match;
    const exports = [];

    while ((match = exportRegex.exec(jsCode)) !== null) {
      exports.push(match[1]);
    }

    return exports;
  }
}


写好后再调试一下,我们可以看见咱们的目录中的 viteConfigJS 已经出现了 defineConfig 了

image.png

最后,思考总结一下

我们目前已经简单实现了自己的初步目标,也学会了陌生的VSCode。

不过,你可能慢慢发现了很多问题:

  1. 比如可读性上:
    1. HelloTreeProvider逻辑有点多了,不太好懂了。
    2. extend里逻辑太多了,心智负担有点重了。
    3. solveExports逻辑看起来有点怪,不好理解。
  2. 比如健壮性上:
    1. 对于export的分析,正则分析会不会不靠谱?
    2. 如何拖拽到已有的import中?代码写在哪?
  3. 比如扩展性上:
    1. 假如我想扩展C++、C#、Python等等的Export怎么写?
  4. 等等等等

很好,都是非常好的问题。

当你考虑这些问题的时候,恭喜你,你就是在考虑架构设计。

你也许会说,不会吧,架构设计就这?这也配?不应该是什么分层、什么微内核、什么微前端吗?

不用担心,听我一言。

其实所谓分层模型、微内核、微前端,都是从这样一个一个小问题开始的。

当你去发现、去思考、去解决这些小问题,聚沙成塔之后,再回顾总结一下,你就会发现,你已经实现了大家常说的分层、微内核、微前端。你已经理解了架构,只欠缺一些经验和方法。

我会在下一章来重构,运用一些常用的设计模式和架构模式,依次解决这些问题。


我是尘码,点赞、关注,带你一步一步搞懂架构,学会设计

有兴趣也可私聊我,加入我的技术群,一起进步

DustyCoder.png