在VS Code Webview中使用ES6模块。

huangapple go评论67阅读模式
英文:

Using ES6 modules in VS Code Webview

问题

不翻译代码部分,只返回翻译好的内容:

tl;dr: (how) can I use an ES6 module that imports other modules' exports, in a VS Code extension creating a Webview?

情况

我试图更新和改进一个在四年前首次编写的 VS Code 扩展。该扩展创建了一个 Webview,使用 HTML 和 JavaScript 模块。以下是曾经有效的代码:

<head>
   <!-- ... -->
   <script type='module'>
   'use strict';

   import { loadFromString as loadSCXML } from '${scxmlDomJs}';
   import SCXMLEditor from '${scxmlEditorJs}';
   import NeatXML from '${neatXMLJs}';

   // …
   </script>
</head>

${…} 字符串的内容已被替换为通过以下方式生成的 URI:

path.join(extensionContext.extensionPath, 'resources', 'scxmlDOM.js')

如今,VS Code 中的 Webview 现在因安全问题而受限,我需要替换内联的 <script> 元素,类似于以下方式(链接到示例):

<head>
   <!-- ... -->
   <meta http-equiv="Content-Security-Policy"
         content="default-src 'none'; script-src 'nonce-${nonce}'">
   <script nonce="${nonce}" src="${mainJS}"></script>

其中 mainJS 是类似上述的路径,进一步包装在 webview.asWebviewUri(…) 的调用中。

问题

如果我将我的模块代码移至一个名为 main.js 的单独文件中,那么当需要生成这些模块的路径时,如何进行导入?

我找到了一些有效的示例(包括上面链接的示例),演示了如何使 Webview 中的脚本与 CORS 和非ces工作,但我找不到有关如何在这些脚本是模块时使其工作的资源。我找到的最接近的是这个问题,这个问题可能与之有关,但也没有答案。

英文:

tl;dr: (how) can I use an ES6 module that imports other modules' exports, in a VS Code extension creating a Webview?

Situation

I'm trying to update and improve a VS Code extension first written 4 years ago. The extension creates a webview, using HTML and JavaScript modules. This is the code that used to work:

&lt;head&gt;
   &lt;!-- ... --&gt;
   &lt;script type=&#39;module&#39;&gt;
   &#39;use strict&#39;;

   import { loadFromString as loadSCXML } from &#39;${scxmlDomJs}&#39;;
   import SCXMLEditor from &#39;${scxmlEditorJs}&#39;;
   import NeatXML from &#39;${neatXMLJs}&#39;;

   // …
   &lt;/script&gt;
&lt;/head&gt;

…where the contents of the ${…} strings were replaced with URIs generated via:

path.join(extensionContext.extensionPath, &#39;resources&#39;, &#39;scxmlDOM.js&#39;)

These days the Webview in VS Code is now locked down for security, and (as I understand it) I need to replace the inline &lt;script&gt; element with something like the following:

&lt;head&gt;
   &lt;!-- ... --&gt;
   &lt;meta http-equiv=&quot;Content-Security-Policy&quot;
         content=&quot;default-src &#39;none&#39;; script-src &#39;nonce-${nonce}&#39;&quot;&gt;
   &lt;script nonce=&quot;${nonce}&quot; src=&quot;${mainJS}&quot;&gt;&lt;/script&gt;

where mainJS is a path like above, further wrapped in a call to webview.asWebviewUri(…).

The Question

If I move my module code into a separate file main.js, how can it import other modules, when the paths to those modules need to be generated?

I've found several working examples (including the one linked above) for how to make script in webviews work with CORS and nonces, but I cannot find a resource on how to make it work when those scripts are modules. The closest I've found is this question which only might be related, but which is also unanswered.

答案1

得分: 1

一种有效的解决方法(已测试)是使用导入映射来将简单名称映射到HTML中的URI,并修改main.js以通过简单名称导入。

Webview HTML:

<head>
   <!-- ... -->
   <meta http-equiv="Content-Security-Policy"
         content="default-src 'none'; script-src 'nonce-${nonce}'">
   <script nonce="${nonce}" type="importmap">
      {
         "imports": {
            "scxmlDOM":    "${scxmlDOMJS}",
            "scxmlEditor": "${scxmlEditorJS}",
            "neatXML":     "${neatXMLJS}"
         }
      }
   </script>
   <script nonce="${nonce}" type="module" src="${mainJS}"></script>

main.js:

'use strict';

import { loadFromString as loadSCXML } from 'scxmlDOM';
import SCXMLEditor from 'scxmlEditor';
import NeatXML from 'neatXML';

// …

我不确定导入映射的<script>元素上是否严格需要nonce,但在存在它的情况下,它确实有效。

请注意,${...} URI不是文字,而是预计将被替换为asWebviewUri()函数的输出。

英文:

One solution that works (tested) is to use an import map to map simple names to the URI in the HTML, and modify the main.js to import by the simple names.

Webview HTML:

&lt;head&gt;
   &lt;!-- ... --&gt;
   &lt;meta http-equiv=&quot;Content-Security-Policy&quot;
         content=&quot;default-src &#39;none&#39;; script-src &#39;nonce-${nonce}&#39;&quot;&gt;
   &lt;script nonce=&quot;${nonce}&quot; type=&quot;importmap&quot;&gt;
      {
         &quot;imports&quot;: {
            &quot;scxmlDOM&quot;:    &quot;${scxmlDOMJS}&quot;,
            &quot;scxmlEditor&quot;: &quot;${scxmlEditorJS}&quot;,
            &quot;neatXML&quot;:     &quot;${neatXMLJS}&quot;
         }
      }
   &lt;/script&gt;
   &lt;script nonce=&quot;${nonce}&quot; type=&quot;module&quot; src=&quot;${mainJS}&quot;&gt;&lt;/script&gt;

main.js:

&#39;use strict&#39;;

import { loadFromString as loadSCXML } from &#39;scxmlDOM&#39;;
import SCXMLEditor from &#39;scxmlEditor&#39;;
import NeatXML from &#39;neatXML&#39;;

// …

I don't know if the nonce is strictly needed on the import map &lt;script&gt; element, but it certainly works with it present.

Note that the ${…} URIs are not literals, but expected to be replaced with the output from the asWebviewUri() function.

答案2

得分: 1

我的扩展 vscode-antlr4 中,我没有使用导入映射(import map)。相反,我设置了我的项目,使得对于 webview 内容,我有一个独立的 tsconfig.json 文件,该文件会导致 tsc 生成 ES2022 模块(而对于扩展本身则使用 CommonJS)。

{
    "compilerOptions": {
        "declaration": true,
        "module": "ES2022",
        "target": "ES2022",
        "outDir": "../../out",
        "removeComments": true,
        "noImplicitAny": true,
        "inlineSources": true,
        "inlineSourceMap": true,
        "isolatedModules": false,
        "allowSyntheticDefaultImports": true,
        "allowUmdGlobalAccess": true, // 对于 D3.js
        "moduleResolution": "node",
        "experimentalDecorators": true,
        "strictNullChecks": true,
        "alwaysStrict": true,
        "composite": true,
        "rootDir": "../.."
    },
    "include": [
        "./*.ts"
    ],
    "exclude": []
}

这个设置允许我导入第三方库,例如来自 node_modules 文件夹的 antlr4ts 和 d3。现在我可以像在铁路图示例提供程序中所示的那样,在我的 webview 内容代码中导入这些 webview 脚本。

public generateContent(webview: Webview, uri: Uri, options: IWebviewShowOptions): string {
        const fileName = uri.fsPath;
        const baseName = basename(fileName, extname(fileName));

        const nonce = this.generateNonce();
        const scripts = [
            FrontendUtils.getMiscPath("railroad-diagrams.js", this.context, webview),
        ];
        const exportScriptPath = FrontendUtils.getOutPath("src/webview-scripts/GraphExport.js", this.context,
            webview);

        if (!this.currentRule || this.currentRuleIndex === undefined) {
            return `<!DOCTYPE html>
                <html>
                    <head>
                        ${this.generateContentSecurityPolicy(webview, nonce)}
                    </head>
                    <body><span style="color: #808080; font-size: 16pt;">No rule selected</span></body>
                </html>`;
        }

        let diagram = `<!DOCTYPE html>
            <html>
            <head>
                <meta http-equiv="Content-type" content="text/html; charset=UTF-8"/>
                ${this.generateContentSecurityPolicy(webview, nonce)}
                ${this.getStyles(webview)}
                <base href="${uri.toString(true)}">
                <script nonce="${nonce}">
                    let graphExport;
                </script>
            </head>
            <body>
            ${this.getScripts(nonce, scripts)}`;

        if (options.fullList) {
            diagram += `
                <div class="header">
                    <span class="rrd-color"><span class="graph-initial">Ⓡ</span>rd&nbsp;&nbsp;</span>All rules
                    <span class="action-box">
                        Save to HTML<a onClick="graphExport.exportToHTML('rrd', '${baseName}');">
                            <span class="rrd-save-image" />
                        </a>
                    </span>
                </div>
                <div id="container">`;

            const symbols = this.backend.listTopLevelSymbols(fileName, false);
            for (const symbol of symbols) {
                if (symbol.kind === SymbolKind.LexerRule
                    || symbol.kind === SymbolKind.ParserRule
                    || symbol.kind === SymbolKind.FragmentLexerToken) {
                    const script = this.backend.getRRDScript(fileName, symbol.name);
                    diagram += `<h3 class="${symbol.name}-class">${symbol.name}</h3>
                        <script nonce="${nonce}">${script}</script>`;
                }
            }
            diagram += `</div>`;
        } else {
            diagram += `
                <div class="header">
                    <span class="rrd-color">
                        <span class="graph-initial">Ⓡ</span>ule&nbsp;&nbsp;
                    </span>
                        &nbsp;&nbsp;${this.currentRule} <span class="rule-index">(rule index: ${this.currentRuleIndex})
                    </span>
                    <span class="action-box">
                        Save to SVG
                        <a onClick="graphExport.exportToSVG('rrd', '${this.currentRule}');">
                            <span class="rrd-save-image" />
                        </a>
                    </span>
                </div>
                <div id="container">
                    <script nonce="${nonce}" >${this.backend.getRRDScript(fileName, this.currentRule)}</script>
                </div>`;
        }

        diagram += `
            <script nonce="${nonce}" type="module">
                import { GraphExport } from "${exportScriptPath}";
                graphExport = new GraphExport();
            </script>
        </body></html>`;

        return diagram;
    }

正如你所看到的,我在代码中设置了一个基本链接(base href),以帮助处理相对导入。整个实现分为两部分。一部分在 <head> 标签中,声明了 graphExport 变量,以便可以在事件处理代码中使用它。然后在 <body> 标签中初始化了这个变量,并导入了 GraphExport 类。

英文:

In my extension vscode-antlr4 I don't use an import map. Instead I set up my project such that for the webview contents I have an own tsconfig.json file which causes tsc to produce ES2022 modules (while for the extension itself CommonJS is used).


{
    &quot;compilerOptions&quot;: {
        &quot;declaration&quot;: true,
        &quot;module&quot;: &quot;ES2022&quot;,
        &quot;target&quot;: &quot;ES2022&quot;,
        &quot;outDir&quot;: &quot;../../out&quot;,
        &quot;removeComments&quot;: true,
        &quot;noImplicitAny&quot;: true,
        &quot;inlineSources&quot;: true,
        &quot;inlineSourceMap&quot;: true,
        &quot;isolatedModules&quot;: false,
        &quot;allowSyntheticDefaultImports&quot;: true,
        &quot;allowUmdGlobalAccess&quot;: true, // For D3.js
        &quot;moduleResolution&quot;: &quot;node&quot;,
        &quot;experimentalDecorators&quot;: true,
        &quot;strictNullChecks&quot;: true,
        &quot;alwaysStrict&quot;: true,
        &quot;composite&quot;: true,
        &quot;rootDir&quot;: &quot;../..&quot;
    },
    &quot;include&quot;: [
        &quot;./*.ts&quot;
    ],
    &quot;exclude&quot;: []
}

This setup allows me to import 3rd party libs, like antlr4ts and d3 from the node_modules folder. I can now import these webview scripts in my webview content code like shown for example in the railroad diagram provider.

public generateContent(webview: Webview, uri: Uri, options: IWebviewShowOptions): string {
        const fileName = uri.fsPath;
        const baseName = basename(fileName, extname(fileName));

        const nonce = this.generateNonce();
        const scripts = [
            FrontendUtils.getMiscPath(&quot;railroad-diagrams.js&quot;, this.context, webview),
        ];
        const exportScriptPath = FrontendUtils.getOutPath(&quot;src/webview-scripts/GraphExport.js&quot;, this.context,
            webview);

        if (!this.currentRule || this.currentRuleIndex === undefined) {
            return `&lt;!DOCTYPE html&gt;
                &lt;html&gt;
                    &lt;head&gt;
                        ${this.generateContentSecurityPolicy(webview, nonce)}
                    &lt;/head&gt;
                    &lt;body&gt;&lt;span style=&quot;color: #808080; font-size: 16pt;&quot;&gt;No rule selected&lt;/span&gt;&lt;/body&gt;
                &lt;/html&gt;`;
        }

        let diagram = `&lt;!DOCTYPE html&gt;
            &lt;html&gt;
            &lt;head&gt;
                &lt;meta http-equiv=&quot;Content-type&quot; content=&quot;text/html; charset=UTF-8&quot;/&gt;
                ${this.generateContentSecurityPolicy(webview, nonce)}
                ${this.getStyles(webview)}
                &lt;base href=&quot;${uri.toString(true)}&quot;&gt;
                &lt;script nonce=&quot;${nonce}&quot;&gt;
                    let graphExport;
                &lt;/script&gt;
            &lt;/head&gt;
            &lt;body&gt;
            ${this.getScripts(nonce, scripts)}`;

        if (options.fullList) {
            diagram += `
                &lt;div class=&quot;header&quot;&gt;
                    &lt;span class=&quot;rrd-color&quot;&gt;&lt;span class=&quot;graph-initial&quot;&gt;Ⓡ&lt;/span&gt;rd&amp;nbsp;&amp;nbsp;&lt;/span&gt;All rules
                    &lt;span class=&quot;action-box&quot;&gt;
                        Save to HTML&lt;a onClick=&quot;graphExport.exportToHTML(&#39;rrd&#39;, &#39;${baseName}&#39;);&quot;&gt;
                            &lt;span class=&quot;rrd-save-image&quot; /&gt;
                        &lt;/a&gt;
                    &lt;/span&gt;
                &lt;/div&gt;
                &lt;div id=&quot;container&quot;&gt;`;

            const symbols = this.backend.listTopLevelSymbols(fileName, false);
            for (const symbol of symbols) {
                if (symbol.kind === SymbolKind.LexerRule
                    || symbol.kind === SymbolKind.ParserRule
                    || symbol.kind === SymbolKind.FragmentLexerToken) {
                    const script = this.backend.getRRDScript(fileName, symbol.name);
                    diagram += `&lt;h3 class=&quot;${symbol.name}-class&quot;&gt;${symbol.name}&lt;/h3&gt;
                        &lt;script nonce=&quot;${nonce}&quot;&gt;${script}&lt;/script&gt;`;
                }
            }
            diagram += &quot;&lt;/div&gt;&quot;;
        } else {
            diagram += `
                &lt;div class=&quot;header&quot;&gt;
                    &lt;span class=&quot;rrd-color&quot;&gt;
                        &lt;span class=&quot;graph-initial&quot;&gt;Ⓡ&lt;/span&gt;ule&amp;nbsp;&amp;nbsp;
                    &lt;/span&gt;
                        &amp;nbsp;&amp;nbsp;${this.currentRule} &lt;span class=&quot;rule-index&quot;&gt;(rule index: ${this.currentRuleIndex})
                    &lt;/span&gt;
                    &lt;span class=&quot;action-box&quot;&gt;
                        Save to SVG
                        &lt;a onClick=&quot;graphExport.exportToSVG(&#39;rrd&#39;, &#39;${this.currentRule}&#39;);&quot;&gt;
                            &lt;span class=&quot;rrd-save-image&quot; /&gt;
                        &lt;/a&gt;
                    &lt;/span&gt;
                &lt;/div&gt;
                &lt;div id=&quot;container&quot;&gt;
                    &lt;script nonce=&quot;${nonce}&quot; &gt;${this.backend.getRRDScript(fileName, this.currentRule)}&lt;/script&gt;
                &lt;/div&gt;`;
        }

        diagram += `
            &lt;script nonce=&quot;${nonce}&quot; type=&quot;module&quot;&gt;
                import { GraphExport } from &quot;${exportScriptPath}&quot;;
                graphExport = new GraphExport();
            &lt;/script&gt;
        &lt;/body&gt;&lt;/html&gt;`;

        return diagram;
    }

As you can see I set a base href in the code, which helps with relative imports. The entire implementation is split into two parts. One is in the <head> tag where the graphExport variable is declared, to allow it to be used by event handling code. This variable is then initialized in the <body> tag, where the GraphExport class is imported.

huangapple
  • 本文由 发表于 2023年2月19日 00:37:41
  • 转载请务必保留本文链接:https://go.coder-hub.com/75494793.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定