我们在学习 vscode 的源码中,也是在探寻,如何去写一个非常复杂且性能极佳的应用,也是在探究 vscode 非常优秀的原因。
现在,我们要基于自己的学习,来实现一个自己的 vscode。
首先,来搭建一个空的项目,并开始第一步。
我们可能不会每一步都按照 vscode 去实现,那就是抄了,我们是需要根据自己的理解去实现,懂的地方,按照自己的思路去写,不懂的地方,则标记出来,不知道其作用,或许在后续的研究中,你可能就知道是因为什么了。
搭建完项目后,我们在进行集成时候,发现 vscode 的入口文件是 js 的,并用了一种很奇怪的方式进行加载。
/**
* Main startup routine
*
* @param {string | undefined} cachedDataDir
* @param {import('./vs/base/node/languagePacks').NLSConfiguration} nlsConfig
*/
function startup(cachedDataDir, nlsConfig) {
nlsConfig._languagePackSupport = true;
process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfig);
process.env['VSCODE_NODE_CACHED_DATA_DIR'] = cachedDataDir || '';
// Load main in AMD
perf.mark('willLoadMainBundle');
require('./bootstrap-amd').load('vs/code/electron-main/main', () => {
perf.mark('didLoadMainBundle');
});
}
所以到这里,我们发现这些问题:
- 为什么它的源码都是 ts 的而入口文件却是 js。
- 为什么载入文件需要如此异常, 似乎用自己的 loader 加载了页面:
require('./bootstrap-amd').load('vs/code/electron-main/main', () => {
perf.mark('didLoadMainBundle');
});
基于以上疑问,我们不懂,先记录。
我会首先在我的项目里,把 ts 集成进来, 以前写过,就照抄先集成一波
接下来,我们安装 electron。
yarn add electron@latest -D --registry=https://registry.npm.taobao.org/
到此,基本的项目搭建完毕了,我们就从主进程入口来继续学习吧。
禁用渲染进程重复使用
// Disable render process reuse, we still have
// non-context aware native modules in the renderer.
app.allowRendererProcessReuse = false;
这里这个点不是很明白在 Electron
中的作用,先记录,后续读到后再加上。
异步启动 bootstrap
接着 vscode
使用了 bootstrap
, 这个模块跟vscode
自身的异步启动相关。
const portable = bootstrap.configurePortable(product);
这块,我们不管他,因为不明白它异步加载的优势,我们记录下这个问题,后续回过头来在看。
设置一些用户数据
同上,记录问题,不作处理。
const args = parseCLIArgs();
const userDataPath = getUserDataPath(args);
app.setPath('userData', userDataPath);
// Configure static command line arguments
const argvConfig = configureCommandlineSwitchesSync(args);
存储 crashed 目录设置
这个看起来是应用需要的,我们在自己应用中实现此功能。这里使用了minimist
这个包用于解析参数,我们来操作一波。
yarn add minimist
我们来写个简单测试,测试下这块功能:
// a.js
function parseCLIArgs() {
const minimist = require('minimist');
return minimist(process.argv, {
string: [
'user-data-dir',
'locale',
'js-flags',
'max-memory',
'crash-reporter-directory'
]
});
}
console.log('parseCLIArgs:', parseCLIArgs()['crash-reporter-directory']);
运行下这个脚本:
my-vscode git:(master) ✗ node a.js --crash-reporter-directory=ccc
parseCLIArgs: ccc
可以看到,其实就是辅助获取一些运行参数,比如输入了:--crash-reporter-directory=ccc
即可拿到参数:parseCLIArgs()['crash-reporter-directory']
Ok,我们集成进去,这个 crash 冲突文件夹设置,看起来是辅助性的东西,我们放到utils下去,保持入口文件的干净整洁。
// src/utils/utils.ts
import minimist from 'minimist';
const parseCLIArgs = () => {
return minimist(process.argv, {
string: [
'user-data-dir',
'locale',
'js-flags',
'max-memory',
'crash-reporter-directory'
]
});
}
export {
parseCLIArgs
}
为了方便存在的变量管理,我们新建一个常量
// src/utils/constants.ts
const ARGS_NAME = {
crashReporterDirectory: 'crash-reporter-directory'
}
export {
ARGS_NAME
}
继续完成 crash
目录设置
import path from 'path';
import fs from 'fs';
import { app } from 'electron';
import { ARGS_NAME } from './constant';
const setCrashFolder = (args: any) => {
let crashReporterDirectory = args[ARGS_NAME.crashReporterDirectory];
if (crashReporterDirectory) {
// 存在变量,则进行设置
crashReporterDirectory = path.normalize(crashReporterDirectory);
if (!path.isAbsolute(crashReporterDirectory)) {
console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory must be absolute.`);
app.exit(1);
}
if (!fs.existsSync(crashReporterDirectory)) {
try {
fs.mkdirSync(crashReporterDirectory);
} catch (error) {
console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory does not seem to exist or cannot be created.`);
app.exit(1);
}
}
// Crashes are stored in the crashDumps directory by default, so we
// need to change that directory to the provided one
console.log(`Found --crash-reporter-directory argument. Setting crashDumps directory to be '${crashReporterDirectory}'`);
app.setPath('crashDumps', crashReporterDirectory);
} else {
// TODO
}
}
到这里,主要就是如果在运行时设置了运行变量,则设置自己的 crash
存放位置,如果不存在,则需要做一些事情:
这里,就遇到了我们原先跳过的内容:configureCommandlineSwitchesSync
既然遇到了,那我们就看看做了哪些事情,对我们来说有没有用:
首先是一些应用能力的支持的开关,这个我们倒是可以先设置着:
const getSwitches = () => {
const SUPPORTED_ELECTRON_SWITCHES = [
// alias from us for --disable-gpu
'disable-hardware-acceleration',
// provided by Electron
'disable-color-correct-rendering',
// override for the color profile to use
'force-color-profile'
];
if (process.platform === 'linux') {
// Force enable screen readers on Linux via this flag
SUPPORTED_ELECTRON_SWITCHES.push('force-renderer-accessibility');
}
const SUPPORTED_MAIN_PROCESS_SWITCHES = [
// Persistently enable proposed api via argv.json: https://github.com/microsoft/vscode/issues/99775
'enable-proposed-api'
];
return {
electronSwitches: SUPPORTED_ELECTRON_SWITCHES,
mainSwitches: SUPPORTED_MAIN_PROCESS_SWITCHES
}
}
const configureCommandlineSwitchesSync = (cliArgs: NativeParsedArgs) => {
const switches = getSwitches();
...
}
前面,我们有解读到,增加运行参数:
node a.js --crash-reporter-directory=ccc
而在这里呢,他们是将运行参数放到:argv.json
中的,并且在刚才上述过程中会去读配置文件,因此,我们现在需要设置下argv.json
的文件路径。
import path from 'path';
import product from '../../production.json';
import os from 'os';
const getArgvConfigPath = () => {
let dataFolderName = product.dataFolderName;
// dev 的时候我们增加一个 dev 后缀
if (process.env['VSCODE_DEV']) {
dataFolderName = `${dataFolderName}-dev`;
}
return path.join(os.homedir(), dataFolderName, 'argv.json');
}
export {
getArgvConfigPath
}
上面例子里涉及到了环境变量,因此,我们也定义一个常量去做环境变量的管理。
//src/utils/constant.ts
const PROCESS_ENV = {
MY_VSCODE_DEV: 'MY_VSCODE_DEV'
}
...
...
if (process.env[PROCESS_ENV.MY_VSCODE_DEV]) {
dataFolderName = `${dataFolderName}-dev`;
}
过程中,他也定义了一个
prodction.json
文件来描述产品的一些信息,其中就包含dataFolderName
这个也是在这里需要的,我们也新建一个prodction.json
// production.json
{
"dataFolderName": ".my-vscode"
}
接下来,我们来写读取变量配置文件:
const readArgvConfigSync = () => {
// Read or create the argv.json config file sync before app('ready')
const argvConfigPath = getArgvConfigPath();
let argvConfig;
try {
argvConfig = JSON.parse(stripComments(fs.readFileSync(argvConfigPath).toString()));
} catch (error) {
console.warn(`
Unable to read argv.json configuration file in ${argvConfigPath}, falling back to defaults (${error})`
);
}
// Fallback to default
if (!argvConfig) {
argvConfig = {
// Force pre-Chrome-60 color profile handling (for https://github.com/microsoft/vscode/issues/51791)
'disable-color-correct-rendering': true
};
}
return argvConfig;
}
- 配置文件存在则读取,否则就创建一默认的配置,但我们这里可以先简单处理,没有就没有: TODO
这里,有个这个函数:stripComments
,看起来是在文件的读取后,做一些格式的处理,我们就加上吧。
为什么 json 文件不直接去读取,而是用读文件的形式呢?留个疑问🤔️
const stripComments = (content) => {
const regexp = /("(?:[^\\"]*(?:\\.)?)*")|('(?:[^\\']*(?:\\.)?)*')|(\/\*(?:\r?\n|.)*?\*\/)|(\/{2,}.*?(?:(?:\r?\n)|$))/g;
return content.replace(regexp, function (match, m1, m2, m3, m4) {
// Only one of m1, m2, m3, m4 matches
if (m3) {
// A block comment. Replace with nothing
return '';
} else if (m4) {
// A line comment. If it ends in \r?\n then keep it.
const length_1 = m4.length;
if (length_1 > 2 && m4[length_1 - 1] === '\n') {
return m4[length_1 - 2] === '\r' ? '\r\n' : '\n';
}
else {
return '';
}
} else {
// We match a string
return match;
}
});
}
绕了一圈,我们又回来啦,继续配置: commandline switches 吧。
import { readArgvConfigSync } from './argv';
...
const configureCommandlineSwitchesSync = (cliArgs: NativeParsedArgs) => {
const switches = getSwitches();
const argvConfig = readArgvConfigSync();
}
接下来,就是根据argv.json
中配置的开关选项,来决定是否打开这些能力。为了结构清晰,我们简单调整下:
import { app } from 'electron';
const setSwitches = (supportElectronSwitches: any) => {
const argvConfig = readArgvConfigSync();
Object.keys(argvConfig).forEach(argvKey => {
const argvValue = argvConfig[argvKey];
// Append Electron flags to Electron
if (supportElectronSwitches.indexOf(argvKey) !== -1) {
// Color profile
if (argvKey === 'force-color-profile') {
if (argvValue) {
app.commandLine.appendSwitch(argvKey, argvValue);
}
}
// Others
else if (argvValue === true || argvValue === 'true') {
if (argvKey === 'disable-hardware-acceleration') {
app.disableHardwareAcceleration(); // needs to be called explicitly
} else {
app.commandLine.appendSwitch(argvKey);
}
}
}
// Append main process flags to process.argv
else if (supportElectronSwitches.indexOf(argvKey) !== -1) {
if (argvKey === 'enable-proposed-api') {
if (Array.isArray(argvValue)) {
argvValue.forEach(id => id && typeof id === 'string' && process.argv.push('--enable-proposed-api', id));
} else {
console.error(`Unexpected value for \`enable-proposed-api\` in argv.json. Expected array of extension ids.`);
}
}
}
});
}
const configureCommandlineSwitchesSync = (cliArgs: NativeParsedArgs) => {
const switches = getSwitches();
setSwitches(switches.electronSwitches);
...
}
接下来,就是配置了一些 js flags
,比如: max_old_space_size
啥的,感觉这个东西还有点用,咱们也搞进来吧。
// src/utils/commandLineSwitches.ts
const getJSFlags = (cliArgs: NativeParsedArgs) => {
const jsFlags = [];
// Add any existing JS flags we already got from the command line
if (cliArgs['js-flags']) {
jsFlags.push(cliArgs['js-flags']);
}
// Support max-memory flag
if (cliArgs['max-memory'] && !/max_old_space_size=(\d+)/g.exec(cliArgs['js-flags'])) {
jsFlags.push(`--max_old_space_size=${cliArgs['max-memory']}`);
}
return jsFlags.length > 0 ? jsFlags.join(' ') : null;
}
const configureCommandlineSwitchesSync = (cliArgs: NativeParsedArgs) => {
const switches = getSwitches();
setSwitches(switches.electronSwitches);
// Support JS Flags
const jsFlags = getJSFlags(cliArgs);
if (jsFlags) {
app.commandLine.appendSwitch('js-flags', jsFlags);
}
return argvConfig;
}
到此,我们就完成了configureCommandlineSwitchesSync
,唉!没想到要搞好一个 Electron
要考虑这么多事情,到现在都还没开始写内容,还在做准备工作。
还记得在configureCommandlineSwitchesSync
在之前,我们其实是在设置crash
目录,我们继续工作吧,
不过看起来,下面的逻辑都是基于存在appCenter
这个东西的,我看vscode
并没有配置,我们也没有使用到,那算了吧,记个TODO。
const appCenter = product.appCenter;
// Disable Appcenter crash reporting if
// * --crash-reporter-directory is specified
// * enable-crash-reporter runtime argument is set to 'false'
// * --disable-crash-reporter command line parameter is set
if (appCenter && argvConfig['enable-crash-reporter'] && !args['disable-crash-reporter']) {
...
}
好,设置冲突文件夹的逻辑,我们完成了!
import { configureCommandlineSwitchesSync } from './utils/commandLineSwitches';
import { setCrashFolder } from './utils/crash';
import { parseCLIArgs } from './utils/utils';
const args = parseCLIArgs();
// argv 参数配置
const argvConfig = configureCommandlineSwitchesSync(args);
setCrashFolder(args, argvConfig);
接下来,好像设置了crashRepoter
相关信息,这个值得学习和使用, 我们在production.json
中增加一些产品信息吧:
{
"nameShort": "my-vscode",
"companyName": "pk"
}
我们读取production.json
需要开启下json
支持。
{
"compilerOptions": {
"strictNullChecks": true,
"module": "commonjs",
"esModuleInterop": true,
"importHelpers": true,
"allowSyntheticDefaultImports": true,
"target": "es5",
"resolveJsonModule": true,
"lib": [
"es2015"
]
},
"files": [
"**/*.json"
],
"include": [
"src" // write ts in src, you can define your own dir
]
}
这里,我们启动一下crashReporter
, 在这里,我们好像遇到了上报crashReporter
路径设置问题,好像跟刚才提及的appCenter
相关,我们是想要这样的配置的,那我们来添加一下这样的功能吧。
// production.json
{
"dataFolderName": ".my-vscode",
"nameShort": "my-vscode",
"companyName": "pk",
"appCenter": {
"win32-ia32": "",
"win32-x64": "",
"linux-x64": "",
"drawn": ""
}
}
我们先配上,然后空着,后续使用的时候增加对应的Crash
报警URL。
这样,我们在设置Crash
文件夹那加上这些逻辑,最后主进程逻辑:
// src/main.ts
import { configureCommandlineSwitchesSync } from './utils/commandLineSwitches';
import { startCrash } from './utils/crash';
import { parseCLIArgs } from './utils/utils';
const args = parseCLIArgs();
// argv 参数配置
const argvConfig = configureCommandlineSwitchesSync(args);
// 设置 crasher
startCrash(args, argvConfig);
// src/utils/crash.ts
import path from 'path';
import fs from 'fs';
import { app, crashReporter } from 'electron';
import product from '../../production.json';
import { ARGS_NAME } from './constant';
const setCrashFolder = (args: any, argvConfig: any): {
submitURL: string;
crashReporterDirectory: string
} => {
let submitURL = '';
let crashReporterDirectory = args[ARGS_NAME.crashReporterDirectory];
if (crashReporterDirectory) {
// 存在变量,则进行设置
crashReporterDirectory = path.normalize(crashReporterDirectory);
if (!path.isAbsolute(crashReporterDirectory)) {
console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory must be absolute.`);
app.exit(1);
}
if (!fs.existsSync(crashReporterDirectory)) {
try {
fs.mkdirSync(crashReporterDirectory);
} catch (error) {
console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory does not seem to exist or cannot be created.`);
app.exit(1);
}
}
// Crashes are stored in the crashDumps directory by default, so we
// need to change that directory to the provided one
console.log(`Found --crash-reporter-directory argument. Setting crashDumps directory to be '${crashReporterDirectory}'`);
app.setPath('crashDumps', crashReporterDirectory);
} else {
const appCenter = product.appCenter;
// Disable Appcenter crash reporting if
// * --crash-reporter-directory is specified
// * enable-crash-reporter runtime argument is set to 'false'
// * --disable-crash-reporter command line parameter is set
if (appCenter && argvConfig[ARGS_NAME.enableCrashReporter] && !args[ARGS_NAME.disableCrashReporter]) {
const isWindows = (process.platform === 'win32');
const isLinux = (process.platform === 'linux');
const crashReporterId = argvConfig[ARGS_NAME.crashReporterId];
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (uuidPattern.test(crashReporterId)) {
submitURL = isWindows ? appCenter[process.arch === 'ia32' ? 'win32-ia32' : 'win32-x64'] : isLinux ? appCenter[`linux-x64`] : appCenter.darwin;
submitURL = submitURL.concat('&uid=', crashReporterId, '&iid=', crashReporterId, '&sid=', crashReporterId);
// Send the id for child node process that are explicitly starting crash reporter.
// For vscode this is ExtensionHost process currently.
const argv = process.argv;
const endOfArgsMarkerIndex = argv.indexOf('--');
if (endOfArgsMarkerIndex === -1) {
argv.push('--crash-reporter-id', crashReporterId);
} else {
// if the we have an argument "--" (end of argument marker)
// we cannot add arguments at the end. rather, we add
// arguments before the "--" marker.
argv.splice(endOfArgsMarkerIndex, 0, '--crash-reporter-id', crashReporterId);
}
}
}
}
return {
submitURL,
crashReporterDirectory
}
}
const startCrash = (args: any, argvConfig: any) => {
const productName = product.nameShort;
const companyName = product.companyName;
const result = setCrashFolder(args, argvConfig);
crashReporter.start({
companyName: companyName,
productName: process.env['VSCODE_DEV'] ? `${productName} Dev` : productName,
submitURL: result.submitURL,
uploadToServer: !result.crashReporterDirectory
});
}
export {
startCrash
}
今天先到这里吧,明天继续加油!!