前言
背景
大家好,我是尘码,在鹅厂待过几年,带队从0到1搞过中大型前端项目。经历并主导了从jquery到vue,从刀耕火种到工程化,从CRUD重构到低代码的完整过程。
我发现,很多架构文章,都处在一个事后诸葛亮的角度,高屋建瓴的来讲架构设计。以至于架构设计文章对读者而言,懂的本身就懂,不懂的看了也云里雾里。
于是,我想从一个初学者的角度切入,在螺蛳壳里做道场,来讲讲架构设计这一件事,一步步揭开架构的神秘面纱。
章节设计
- 第一章 从需求出发,规划功能,浅谈思路,做好调研,实现demo,发布上线。
- 第二章 从demo出发,巩固基础,分析问题,逐步重构,搭建工程,完善产品。
- 第三章 从产品出发,丰富场景,落实理论,完善基建,掌握主动,巩固地位。
- 第四章 从整体出发,确定愿景,整合资源,提升名气,建立社区,完善生态。
第一章
第一步,要有一个需求
当我用 VSCode 写代码时,我发现了一个频繁的操作:
我总会打开某个文件,只是为了看看该文件 export
了什么。比如下面的文件 export 了 5 个函数: loadCode
, loadJson
, solveExports
等等
然后再打开另外一个文件,写下 import { loadJson } from '../xxx'
也许,我们可以开发一个VSCode插件,来优化这里的开发体验。
第二步,设计出产品
显然,上面的需求可以抽象成两个问题:
- 更方便的展示export
- 更方便的写出import
显然,我们可以想到下面两个功能
-
直接在文件目录展示export
-
支持拖拽exports到文件中。
PS:该插件已发布,VSCode插件商店搜索XMouse即可。
也可以点击下方链接直达商店页面:
第三步,快速调研核心阻塞点
产品清晰了,就来看实现。对于我们来说,有两个陌生的地方:
- 怎么获取export?
- 怎么展示在vscode中?
让我们挨个思考。
3.1 怎么获取export?
在2024年,相信聪明的你首先想到的就是ChatGPT,回答如下:
好,现在给你三秒思考选择什么?
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 试一试
很顺利。(虽然有一点小瑕疵,但不重要,后期再优化)
3.2 怎么展示在vscode中?
同样的,遇事不决,GPT4.(国内有挺多免费站的,随便找一个白嫖就好)
看起来有些新的名词,比如 TreeDataProvider。
此时我们只需要网上查询查询关键词了解下,便可以得到回到:
第一个搜索结果就是官方文档,描述清晰,符合预期,说明可行。
第四步,快速搞出demo
经过第三步的调研,我们只欠缺VSCode的使用。
4.1 那么先从HelloWorld开始
根据VSCode官方教程,我们仅需新建文件夹,并执行如下命令即可。
cmd npx --package yo --package generator-code -- yo code
执行之后我们发现启动了一个命令行工具,瞎选就完事,先上车再说。
生成项目之后,根据官方指引,我们可以按 F5 进行调试。
然后我们发现 VSCode 打开了一个新的窗口,我们在这个窗口中,用键盘上按下 Ctrl+Shift+P
组合键,便可以找到Hello World 命令,执行即可。
4.2 接着让我们理解理解 Hello World
总所周知,看 js 源码从package.json开始.
从package.json中,我们发现main文件指向'./dist/extention',这明显是打包之后的路径。
按江湖惯例在目录中找找,果然发现了 webpack.config.js , 里面指向了 src/extension.ts
跟着让我们打开 src/extension.ts 文件,发现目标:
原来要实现hello world,只需要在 activate 函数中,注册命令即可。
从demo代码不难看出两点:
- 从 activate和deactivate顾名思义,这是 VSCode插件 的生命周期。
- 从 context.subscriptions.push( xxx ) 顾名思义,VSCode 以发布订阅的方式提供 Hook。
这很好,很干净,也是常见的微内核架构,我会在后续的章节中详聊。
4.3 实现TreeView
现在,我们再回过头看细致研究下TreeView,网上翻一翻,找到两个教程:
总结一下, 需要做两步:
- package.json 中加添加下面这一段配置
// package.json 中加添加下面这一段配置
{
...
"contributes": {
"views": {
"explorer": [
{
"id": "XMouse.helloTree",
"name": "HelloWorldTree"
}
]
}
},
...
}
- 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,果然出现了包子馒头
对于Pakcage.json就不说了,只是一个规定,学会怎么配置就行。
对于TreeDataProvider,教程里描述如下,我就不重复了。
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中嘞。
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 了
最后,思考总结一下
我们目前已经简单实现了自己的初步目标,也学会了陌生的VSCode。
不过,你可能慢慢发现了很多问题:
- 比如可读性上:
- HelloTreeProvider逻辑有点多了,不太好懂了。
- extend里逻辑太多了,心智负担有点重了。
- solveExports逻辑看起来有点怪,不好理解。
- 比如健壮性上:
- 对于export的分析,正则分析会不会不靠谱?
- 如何拖拽到已有的import中?代码写在哪?
- 比如扩展性上:
- 假如我想扩展C++、C#、Python等等的Export怎么写?
- 等等等等
很好,都是非常好的问题。
当你考虑这些问题的时候,恭喜你,你就是在考虑架构设计。
你也许会说,不会吧,架构设计就这?这也配?不应该是什么分层、什么微内核、什么微前端吗?
不用担心,听我一言。
其实所谓分层模型、微内核、微前端,都是从这样一个一个小问题开始的。
当你去发现、去思考、去解决这些小问题,聚沙成塔之后,再回顾总结一下,你就会发现,你已经实现了大家常说的分层、微内核、微前端。你已经理解了架构,只欠缺一些经验和方法。
我会在下一章来重构,运用一些常用的设计模式和架构模式,依次解决这些问题。
我是尘码,点赞、关注,带你一步一步搞懂架构,学会设计
有兴趣也可私聊我,加入我的技术群,一起进步