Skip to content
0

JS 技巧

浏览器环境解析 CJS Export

利用 eval 解析 CJS 格式的模块导出,这在浏览器环境非常有用

const moduleStr = `
module.exports = {
    entry: './src/index.js',
    process: () => {}
}
`
console.log(eval(moduleStr))
// => { entry: './src/index.js', process: [Function: process] }

浏览器环境动态使用 NPM 包

通常的印象中 NPM 包通常与 NodeJs 绑定,但是我们也可以直接在浏览器环境中获取并使用,需要用到 esm.sh

esm.sh

暴露了 ESM 的能力,让我们能在浏览器中通过 ESM 使用到 NPM 模块

一个例子:

<script type="module">
    import { esm } from "https://esm.sh/build";

    const { sayHi } = await esm`
        import chalk from "chalk";
        export const sayHi = () => chalk.blue("Hi!");
    `;
    console.log(sayHi()); // prints "Hi!" message in blue color
</script>

更有意思的例子,能够在浏览器上完成更复杂的事情

<script type="module">
    import build from "https://esm.sh/build";

    const ret = await build({
    dependencies: {
        "preact": "^10.13.2",
        "preact-render-to-string": "^6.0.2",
    },
    code: `
        /* @jsx h */
        import { h } from "preact";
        import { renderToString } from "preact-render-to-string";
        export function render(): string {
        return renderToString(<h1>Hello world!</h1>);
        }
    `,
    // for types checking and LSP completion
    types: `
        export function render(): string;
    `,
    });

    // import module
    const { render } = await import(ret.url);
    // import bundled module
    const { render } = await import(ret.bundleUrl);

    render(); // "<h1>Hello world!</h1>"
</script>

new AsyncFunction

new AsyncFunction,顾名思义就是 new Function 的异步版本。目前 JS 并没有直接提供 AsyncFunction,我们需要自己模拟。

const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor

比如我们结合上面的 esm.sh 举个例子

const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor
const CDN_BASE = 'https://esm.sh/'

export async function evaluateUserConfig<U = UserConfig>(configStr: string): Promise<U | undefined> {
  const code = configStr
    .replace(/import\s*(\{[\s\S]*?\})\s*from\s*(['"])([\w-@/]+)\2/g, 'const $1 = await __import("$3");')
    .replace(/import\s*(.*?)\s*from\s*(['"])([\w-@/]+)\2/g, 'const $1 = (await __import("$3")).default;')
    .replace(/export default /, 'return ')
    .replace(/\bimport\s*\(/, '__import(')

  // bypass vite interop
  // eslint-disable-next-line no-new-func
  const _import = new Function('a', 'return import(a);')
  const __import = (name: string): any => {
    if (!modulesCache.has(name)) {
      modulesCache.set(name,
        name.endsWith('.json')
          ? $fetch(CDN_BASE + name, { responseType: 'json' }).then(r => ({ default: r }))
          : _import(CDN_BASE + name),
      )
    }
    return modulesCache.get(name)
  }

  const fn = new AsyncFunction('__import', code)
  const result = await fn(__import)

  if (result)
    return result
}

上面的代码也可以实现解析 ESM 模块的效果,实际上就是 unocss 的 plaground 的解析 ESM 部分的原理 ,代码位于这里

logger.js

打印所在文件路径和行

// Use like this: node --import logger.js yourapp.js 

import path from 'path';

const { log } = console;
[`debug`, `log`, `warn`, `error`, `table`, `dir`].forEach((methodName) => {
  const originalLoggingMethod = console[methodName];
  console[methodName] = (...args) => {
    const originalPrepareStackTrace = Error.prepareStackTrace;
    Error.prepareStackTrace = (_, stack) => stack;
    const callee = new Error().stack[1];
    Error.prepareStackTrace = originalPrepareStackTrace;
    const relativeFileName = path
      .relative(process.cwd(), callee.getFileName())
      .replace(process.cwd(), ``)
      .replace(`file:/`, ``);
    // Log in dark grey
    const label = `${relativeFileName}:${callee.getLineNumber()}`;
    log(`🪵 \x1b[90m%s\x1b[0m`, label);
    originalLoggingMethod(...args);
  };
});

Released under the MIT License.