神族九帝's blog 神族九帝's blog
首页
  • 神卡套餐 (opens new window)
  • 神族九帝 (opens new window)
  • 网盘资源 (opens new window)
  • 今日热点 (opens new window)
  • 在线PS (opens new window)
  • IT工具 (opens new window)
  • FC游戏 (opens new window)
  • 在线壁纸 (opens new window)
  • 面试突击
  • 复习指导
  • HTML
  • CSS
  • JavaScript
  • 设计模式
  • 浏览器
  • 手写系列
  • Vue
  • Webpack
  • Http
  • 前端优化
  • 项目
  • 面试真题
  • 算法
  • 精选文章
  • 八股文
  • 前端工程化
  • 工作笔记
  • 前端基础建设与架构 30 讲
  • vue2源码学习
  • 剖析vuejs内部运行机制
  • TypeScript 入门实战笔记
  • vue3源码学习
  • 2周刷完100道前端优质面试真题
  • 思维导图
  • npm发包
  • 重学node
  • 前端性能优化方法与实战
  • webpack原理与实战
  • webGl
  • 前端优化
  • Web3
  • React
  • 更多
  • 未来要做的事
  • Stirling-PDF
  • ComfyUI
  • 宝塔面板+青龙面板
  • 安卓手机当服务器使用
  • 京东自动评价代码
  • 搭建x-ui免流服务器(已失效)
  • 海外联盟
  • 好玩的docker
  • 收藏夹
  • 更多
GitHub (opens new window)

神族九帝,永不言弃

首页
  • 神卡套餐 (opens new window)
  • 神族九帝 (opens new window)
  • 网盘资源 (opens new window)
  • 今日热点 (opens new window)
  • 在线PS (opens new window)
  • IT工具 (opens new window)
  • FC游戏 (opens new window)
  • 在线壁纸 (opens new window)
  • 面试突击
  • 复习指导
  • HTML
  • CSS
  • JavaScript
  • 设计模式
  • 浏览器
  • 手写系列
  • Vue
  • Webpack
  • Http
  • 前端优化
  • 项目
  • 面试真题
  • 算法
  • 精选文章
  • 八股文
  • 前端工程化
  • 工作笔记
  • 前端基础建设与架构 30 讲
  • vue2源码学习
  • 剖析vuejs内部运行机制
  • TypeScript 入门实战笔记
  • vue3源码学习
  • 2周刷完100道前端优质面试真题
  • 思维导图
  • npm发包
  • 重学node
  • 前端性能优化方法与实战
  • webpack原理与实战
  • webGl
  • 前端优化
  • Web3
  • React
  • 更多
  • 未来要做的事
  • Stirling-PDF
  • ComfyUI
  • 宝塔面板+青龙面板
  • 安卓手机当服务器使用
  • 京东自动评价代码
  • 搭建x-ui免流服务器(已失效)
  • 海外联盟
  • 好玩的docker
  • 收藏夹
  • 更多
GitHub (opens new window)
  • 工作笔记

    • 国际化多语言一键替换,在线翻译
    • 虚拟列表
    • vscode插件开发
      • 创建项目
      • 目录结构
      • 运行插件
      • 功能实现
      • 打包发布
        • 账号准备
        • package.json修改
        • 打包命令
        • 发布
      • 参考链接
      • 原理讲解( AST 自动转换国际化方案分享)
        • 1. 整体思路
        • 2. 支持的文件类型
        • 3. AST 转换核心处理
    • Code Review
  • 前端基础建设与架构 30 讲

  • vue2源码学习

  • 剖析vuejs内部运行机制

  • TypeScript 入门实战笔记

  • vue3源码学习

  • 2周刷完100道前端优质面试真题

  • 思维导图

  • npm发包

  • 重学node

  • 前端性能优化方法与实战

  • webpack原理与实战

  • webGl

  • 前端优化

  • Web3

  • React

  • 更多

  • 笔记
  • 工作笔记
wu529778790
2024-08-24

vscode插件开发

# 创建项目

安装依赖

pnpm i -g yo generate-code

初始化插件目录

yo code

注:如果报错:You don't seem to have a generator with the name “generator-code” installed. 就先执行npm install generator-code.

# 目录结构

{
  "name": "i18n-automatically", // 定义了插件的包名,它必须是唯一的并且符合 npm 的包命名规范。
  "displayName": "I18n Automatically",  // 定义了插件在 Visual Studio Code 中显示的名称。
  "description": "vscode国际化多语言自动生成替换方案",
  "version": "0.0.1", 
  "engines": {
    "vscode": "^1.92.0"   // 定义了插件所支持的 Visual Studio Code 的最低版本。
  },
  "categories": [   // 定义了插件所属的分类,以便在 Visual Studio Code 中组织和展示插件。
    "Extension Packs",
    "Programming Languages",
    "Visualization",
    "Debuggers",
    "Notebooks"
  ],
  "activationEvents": [],    //  定义了激活插件的事件
  "main": "./out/extension.js",  // 定义插件的入口文件
  "contributes": {
    "commands": [
      {
        "command": "i18n-automatically.mytest",  // 注册的插件命令
        "title": "mytest"
      }
    ]
  },
  "scripts": {
    "vscode:prepublish": "npm run compile",
    "compile": "tsc -p ./",
    "watch": "tsc -watch -p ./",
    "pretest": "npm run compile && npm run lint",
    "lint": "eslint src --ext ts",
    "test": "node ./out/test/runTest.js",
    "prepare": "husky install",
    "commitlint": "commitlint --config commitlint.config.js -e -V"
  },
  "devDependencies": {},
  "lint-staged": {
    "**/*": "prettier --write --ignore-unknown"
  }
}

# 运行插件

  1. F5 运行插件 vsode会打开一个新的调试窗口 20240824153005

  2. 在新的调试窗口中,按 ctrl + shift + p,打开命令面板;

  3. 输入注册的插件命令,即可执行插件。 在package.json和extension文件中定义了命令,项目的默认命令是hello word,可自行更改。

# 功能实现

extension.ts作为项目的入口文件,里面的两个函数定义了插件激活/销毁时要执行的功能:

// 模块“vscode”包含VS Code可扩展性API
// 导入模块,并在下面的代码中使用别名vscode引用它
const vscode = require("vscode");

// 当您的扩展被激活时,会调用此方法
// 您的扩展在第一次执行命令时被激活

/**
 * @param {vscode.ExtensionContext} context
 */
function activate(context) {
  // 使用控制台输出诊断信息(console.log)和错误(console.error)
  // 当您的扩展被激活时,这行代码只会执行一次
  console.log(
    'Congratulations, your extension "i18n-automatically" is now active!'
  );

  // 该命令已在package.json文件中定义
  // 现在使用registerCommand提供命令的实现
  // commandId参数必须与package.json中的命令字段匹配
  const disposable = vscode.commands.registerCommand(
    "i18n-automatically.helloWorld",
    function () {
      // The code you place here will be executed every time your command is executed

      // Display a message box to the user
      vscode.window.showInformationMessage(
        "Hello World from i18n-automatically!"
      );
    }
  );

  context.subscriptions.push(disposable);
}

// This method is called when your extension is deactivated
function deactivate() {}

module.exports = {
  // 插件激活
  activate,
  // 插件销毁
  deactivate,
};

剩下的开发和平时一样。

# 打包发布

# 账号准备

https://marketplace.visualstudio.com/manage (opens new window)

# package.json修改

新增 publisher: publisher ID

# 打包命令

安装依赖

pnpm i @vscode/vsce -g

打包,在根目录下执行打包命令,执行成功后,会在根目录下生成 .vsix 文件

用yarn的话,后面加参数

vsce package --yarn

# 发布

  • 命令发布
vsce publish -p YOUR_PUBLISHER_ID

# 参考链接

https://rackar.github.io/vscode-ext-doccn/ (opens new window)

# 原理讲解( AST 自动转换国际化方案分享)

20241104194430

# 1. 整体思路

# 1.1. 技术选型

  • Vue SFC 解析: @vue/compiler-sfc

    • 用于解析 .vue 文件的模板和脚本部分
    • 分别处理 template、script、scriptSetup 块
  • Babel 工具链:

    const parser = require('@babel/parser'); // 将代码解析为 AST
    const traverse = require('@babel/traverse'); // 遍历和修改 AST
    const t = require('@babel/types'); // AST 节点类型处理
    const generate = require('@babel/generator'); // AST 转回代码
    

# 2. 支持的文件类型

# 2.1. Vue 单文件组件 (.vue)

// 文件处理器
function getFileProcessor(fileExt) {
  const processors = {
    '.vue': handleVueFile,  // Vue 处理器
    ...
  };
}

// Vue 文件解析
async function handleVueFile(filePath, config) {
  const { descriptor } = parseSfc(fileContent);

  // 1. 处理模板
  if (descriptor.template) {
    processTemplate(descriptor.template.ast);
  }

  // 2. 处理脚本
  if (descriptor.script) {
    processScript(descriptor.script.content);
  }

  // 3. 处理 setup
  if (descriptor.scriptSetup) {
    processScript(descriptor.scriptSetup.content);
  }
}

# 2.2. JavaScript/TypeScript 文件

const processors = {
  '.js': handleJsFile, // JavaScript
  '.jsx': handleJsFile, // React JSX
  '.ts': handleJsFile, // TypeScript
  '.tsx': handleJsFile, // React TSX
};

# 2.3. 解析配置

// Babel 解析配置
const parseOptions = {
  sourceType: 'module',
  plugins: [
    'jsx', // 支持 JSX 语法
    'typescript', // 支持 TypeScript
    'decorators-legacy', // 支持装饰器
  ],
};

// Vue 解析配置
const vueOptions = {
  filename: 'component.vue',
  sourceMap: false,
};

# 3. AST 转换核心处理

# 核心工作流程

20241104194519

# 3.1. Template AST 转换

# 3.1.1. 节点类型处理
// 处理不同类型的模板节点
function processTemplate(templateAst, context) {
  const nodeTypeHandlers = {
    // 注释节点
    3: (node) => node.loc.source,

    // 文本节点 - 处理纯文本内容
    2: (node) => {
      if (containsChinese(node.content)) {
        const key = generateKey(context);
        context.translations.set(key, node.content.trim());
        return `{{ $t('${key}') }}`;
      }
      return node.content;
    },

    // 插值节点 - 处理 {{ }} 内的表达式
    5: (node) => {
      if (node.content.ast) {
        // 处理包含 JS 表达式的插值
        return processInterpolationNode(node, context);
      }
      return processTemplateString(node.content.content, context);
    },

    // 元素节点 - 处理标签及其属性
    1: (node) => processElementNode(node, context),
  };

  return astArrayToTemplate(templateAst, nodeHandlers);
}
# 3.1.2. 元素节点处理
function processElementNode(node, context) {
  let result = `<${node.tag}`;

  // 处理属性
  if (node.props) {
    for (const prop of node.props) {
      // 处理指令 (v-bind, v-on 等)
      if (prop.type === 7) {
        result += processDirective(prop, context);
      }
      // 处理普通属性
      else if (prop.type === 6) {
        result += processAttribute(prop, context);
      }
    }
  }

  // 处理子节点
  if (!node.isSelfClosing) {
    result += '>';
    if (node.children) {
      result += node.children
        .map((child) => astToTemplate(child, context))
        .join('');
    }
    result += `</${node.tag}>`;
  } else {
    result += ' />';
  }

  return result;
}
# 3.1.3. 指令处理
function processDirective(prop, context) {
  const name = getDirectiveName(prop);

  // 没有表达式的指令
  if (!prop.exp) return `\n${name}`;

  // 处理带表达式的指令
  if (containsChinese(prop.exp.content)) {
    // 处理包含 HTML 的情况
    if (stringWithDom(prop.exp.content)) {
      const handlerContent = prop.exp.content
        .trim()
        .replace(/^[\s\n]*[`'"]|[`'"][\s\n]*$/gm, '');
      const result = handlerDomNode(handlerContent, context);
      return `\n${name}="\`${replaceForI18nCall(result, context)}\`"`;
    }

    // 处理普通中文表达式
    const result = handlerForJs(prop.exp, context);
    return `\n${name}="${replaceForI18nCall(result, context)}"`;
  }

  return `\n${name}="${prop.exp.content}"`;
}

# 3.2. JavaScript AST 转换

# 3.2.1. AST 遍历处理
function processJsAst(context) {
  const ast = parser.parse(context.contentSource, {
    sourceType: 'module',
    plugins: ['jsx', 'typescript', 'decorators-legacy'],
  });

  traverse(ast, {
    // 处理字符串字面量
    StringLiteral(path) {
      handleChineseString(path, context);
    },

    // 处理模板字符串
    TemplateElement(path) {
      handleChineseString(path, context, true);
    },

    // 处理 JSX 文本
    JSXText(path) {
      handleChineseString(path, context);
    },

    // 处理 JSX 属性
    JSXAttribute(path) {
      handleJSXAttribute(path, context);
    },

    // 处理 JSX 表达式容器
    JSXExpressionContainer(path) {
      handleJSXExpressionContainer(path, context);
    },

    // 处理程序入口,检查 i18n 导入
    Program: {
      enter(path) {
        checkForI18nImport(path, context);
      },
    },
  });

  return generateCode(ast, context);
}
# 3.2.2. 中文字符串处理
function handleChineseString(path, context, isTemplateLiteral = false) {
  try {
    const value = isTemplateLiteral ? path.node.value.raw : path.node.value;

    // 跳过非中文或调试上下文
    if (!containsChinese(value) || isInDebugContext(path)) return;

    // 处理包含 HTML 的字符串
    if (stringWithDom(value)) {
      handleStringWithDom(path, context, isTemplateLiteral);
      return;
    }

    const key = generateKey(context);

    // 根据不同场景选择替换策略
    if (isTemplateLiteral) {
      handleTemplateLiteral(path, context, key);
    }
    // JSX 场景
    else if (path.type === 'JSXText' || path.parent?.type.includes('JSX')) {
      replaceWithJSXI18nCall(path, context, key);
    }
    // 普通字符串场景
    else {
      replaceWithI18nCall(path, context, key);
    }

    // 保存翻译
    context.translations.set(key, value.trim());
  } catch (error) {
    context.index--;
    customConsole.error('handleChineseString 错误:', error);
  }
}
# 3.2.3. 特殊场景处理
// 处理带 HTML 的字符串
function handleStringWithDom(path, context, isTemplateLiteral) {
  if (path.type === 'StringLiteral') {
    convertStringLiteralToTemplateLiteral(path, context);
  } else if (isTemplateLiteral) {
    processTemplateElement(path, context);
  }
}

// 处理模板字符串
function handleTemplateLiteral(path, context, key) {
  const templateLiteral = path.parentPath;
  const newExpression = t.callExpression(
    t.identifier(context.config.scriptI18nCall),
    [t.stringLiteral(key)],
  );

  // 保持原有表达式的顺序
  const existingExpressions = templateLiteral.node.expressions.map((exp) => ({
    node: exp,
    start: exp.start,
  }));
  existingExpressions.push({
    node: newExpression,
    start: path.node.start,
  });

  // 重新排序并更新节点
  templateLiteral.node.expressions = existingExpressions
    .sort((a, b) => a.start - b.start)
    .map((item) => item.node);
}

// 上下文检查
function isInDebugContext(path) {
  const debugContexts = [
    // 检查 console 调用
    (p) =>
      p.isCallExpression() &&
      p.get('callee').isMemberExpression() &&
      p.get('callee.object').isIdentifier({ name: 'console' }),

    // 检查 Error 抛出
    (p) =>
      p.isNewExpression() && p.get('callee').isIdentifier({ name: 'Error' }),

    // 检查断言
    (p) =>
      p.isCallExpression() && p.get('callee').isIdentifier({ name: 'assert' }),

    // 检查调试器语句
    (p) => p.isDebuggerStatement(),
  ];

  return debugContexts.some((context) => path.findParent(context));
}
# 处理模板字符串,为什么要重新排序?
  1. 模板字符串的结构:
// 一个模板字符串可能包含多个表达式,例如:
`Hello ${name}, your score is ${score}!`;
  1. 在 AST 中,这个模板字符串被表示为:
  • quasis(静态文本部分):["Hello ", ", your score is ", "!"]
  • expressions(表达式):[name, score]
  1. 当我们在处理国际化时,可能会遇到这样的情况:
`用户 ${name} 的分数是 ${score} 分`;
  1. 处理后我们希望变成:
`${t('user-key')} ${name} ${t('score-key')} ${score} ${t('point-key')}`;
  1. 需要排序的原因:

    • AST 遍历过程中可能不是按照代码中的顺序处理表达式的
    • 当我们添加新的 i18n 函数调用时,需要确保它们被插入到正确的位置
    • expressions 数组中表达式的顺序必须与模板字符串中出现的顺序完全匹配
  2. 来看一个具体例子:

// 原始代码
`用户 ${name} 的分数是 ${score} 分`

// AST处理过程中可能的顺序:
1. 处理 "用户"
2. 处理 "的分数是"
3. 处理 "分"

// 如果不排序,可能会变成:
`${t('point-key')} ${t('user-key')} ${name} ${t('score-key')} ${score}`

// 排序后,正确的顺序:
`${t('user-key')} ${name} ${t('score-key')} ${score} ${t('point-key')}`

通过使用 start 位置进行排序,我们可以确保表达式按照它们在原始代码中出现的顺序排列,从而保持模板字符串的语义不变。这对于保持代码的正确性和可读性都是至关重要的。

编辑 (opens new window)
上次更新: 2025/03/17, 12:21:00
虚拟列表
Code Review

← 虚拟列表 Code Review→

最近更新
01
Code Review
10-14
02
ComfyUI
10-11
03
国际化多语言一键替换,在线翻译
08-18
更多文章>
Power by vuepress | Copyright © 2015-2025 神族九帝
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×