Skip to content

内联JS处理(ES语法降级&内容压缩)

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

本文简单介绍一下当前html在现代工程中的现状,并阐述内联js代码可能存在的一些问题,同时介绍一系列处理页面内联脚本的方法,以及通过SWC如何转换目标代码,通过CLI工具如何组合这些能力

前言

当下大部分现代前端Web工程中,HTML文件大部分都是以public/index.html<projectRoot>/index.html存在

其内容也比较简单,通常像下面这样。

html
<!DOCTYPE html>
<html lang="en">
<head>
  <!-- some meta or link source -->
</head>
<body>
  <div id="app"></div>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <!-- some meta or link source -->
</head>
<body>
  <div id="app"></div>
</body>
</html>

项目通过构建工具,将编写的"源码"进行一系列操作后转换成供浏览器可直接运行的js

在产生的HTML文件中体现的就是通过<script src><link href>标签引入了构建后的资源

其中部分插件会往页面中直接插入内联的JS代码

当然也不排除一些项目也会主动通过CDN引入一些第三方的SDK,然后会在模板中插入一些初始化或者激活相应功能特性的代码

针对上面2种情况产生的JS代码,大部分情况下是没有通过babel进行编译的,可能存在一些质量问题(兼容性问题为主)。

如果只是ES语法检查,可以用前面文章介绍的增强ESCheck工具进行检测。

本文将进一步介绍一下提取HTML inline Code的多种方法,然后进一步使用SWC对内联脚本进行压缩ES语法转换降级等等操作。

InlineJS内容处理

用于测试的目标代码如下

html
<body>
  <div id="app"></div>
  <script>
    const hello = 'hello'
  </script>
  <script src="hello.js"></script>
  <script>
    const world = 'hello'
  </script>
  <script>
    console.log(hello,world);
  </script>
</body>
<body>
  <div id="app"></div>
  <script>
    const hello = 'hello'
  </script>
  <script src="hello.js"></script>
  <script>
    const world = 'hello'
  </script>
  <script>
    console.log(hello,world);
  </script>
</body>

目的是将里面的js code先提取出来,然后将里面的const简单的替换成var

ts
// 简单转换示例
function simpleConst2Var(code: string) {
  return code.replace(/const /g, 'var ')
}
// 简单转换示例
function simpleConst2Var(code: string) {
  return code.replace(/const /g, 'var ')
}

正则

搞文本内容的处理首先想到的方法,简单的正则如下

/<script>([\s\S]*?)<\/script>/g
/<script>([\s\S]*?)<\/script>/g

图片

利用replace方法,几行代码就能搞定

ts
function traverseScript(htmlCode: string, transformFn: (v: string) => string) {
  const rScriptTag = /<script>([\s\S]*?)<\/script>/g
  return htmlCode.replace(rScriptTag, (all, $1) => {
    return all.replace($1, transformFn($1))
  })
}
function traverseScript(htmlCode: string, transformFn: (v: string) => string) {
  const rScriptTag = /<script>([\s\S]*?)<\/script>/g
  return htmlCode.replace(rScriptTag, (all, $1) => {
    return all.replace($1, transformFn($1))
  })
}

局限性就是无法区分出注释,字符串的值,勉强可用。

示例代码地址:inline-code/regexp.ts

GoGoCode

GoGoCode 是一个基于 AST 的 JavaScript/Typescript/HTML 代码转换工具,API是jQuery风格,API还是很简洁好用

其中HTML的解析使用的是hyntax-yx看上去是 forkhyntax重新发了个版

因此用GoGoCode同样可以很简单的实现

先生成AST

ts
import $ from 'gogocode'
const htmlAST = $(htmlCode, { parseOptions: { language: 'html' } })
import $ from 'gogocode'
const htmlAST = $(htmlCode, { parseOptions: { language: 'html' } })

遍历<script>节点,其中$scriptNode节点结构如下,可以直接使用attr方法进行值的存取操作

ts
htmlAST.find(`<script>$_$</script>`).each(($scriptNode) => {
  const origin = $scriptNode.attr('content.value.content')
  $scriptNode.attr('content.value.content', transformFn(origin.toString()))
})
htmlAST.find(`<script>$_$</script>`).each(($scriptNode) => {
  const origin = $scriptNode.attr('content.value.content')
  $scriptNode.attr('content.value.content', transformFn(origin.toString()))
})

图片

完整代码如下

ts
function traverseScript(htmlCode: string, transformFn: (v: string) => string) {
  const htmlAST = $(htmlCode, { parseOptions: { language: 'html' } })
  htmlAST.find(`<script>$_$</script>`).each(($scriptNode) => {
    const origin = $scriptNode.attr('content.value.content')
    $scriptNode.attr('content.value.content', transformFn(origin.toString()))
  })
  return htmlAST.generate()
}
function traverseScript(htmlCode: string, transformFn: (v: string) => string) {
  const htmlAST = $(htmlCode, { parseOptions: { language: 'html' } })
  htmlAST.find(`<script>$_$</script>`).each(($scriptNode) => {
    const origin = $scriptNode.attr('content.value.content')
    $scriptNode.attr('content.value.content', transformFn(origin.toString()))
  })
  return htmlAST.generate()
}

代码量和使用正则差不多,但使用AST操作准确性更高,可玩性也强。

hyntax只提供了AST与Tokens的生成,节点遍历与AST内容转换输出由GoGoCode实现

示例代码地址:inline-code/gogocode.ts

svelte

Svelte 是一种全新的构建用户界面的方法。传统框架如 React 和 Vue 在浏览器中需要做大量的工作,而 Svelte 将这些工作放到构建应用程序的编译阶段来处理。

当然不了解这个框架,也不影响理解后续的代码,可以接着往下看

这个是从 AST Explorer transform示例demo中看到的

看了一下demo实现代码

sevlte/compiler提供了直接生成AST的方法compile

ts
import * as svelte from 'svelte/compiler'

const AST = svelte.compile(htmlCode).ast
const htmlAST = AST.html
import * as svelte from 'svelte/compiler'

const AST = svelte.compile(htmlCode).ast
const htmlAST = AST.html

图片

同时提供了一个预处理方法preprocess,可以实现script,style与其他标签内容的遍历与修改,最后返回处理后的结果

使用示例如下,其返回值是promise

ts
import * as svelte from 'svelte/compiler'

svelte.preprocess(htmlCode, {
  script(ops) {
    return {
      code: transformFn(ops.content)
    }
  }
})
import * as svelte from 'svelte/compiler'

svelte.preprocess(htmlCode, {
  script(ops) {
    return {
      code: transformFn(ops.content)
    }
  }
})

同样按照上面要求实现script代码的转换,代码很简洁

ts
function traverseScript(htmlCode: string, transformFn: (v: string) => string) {
  return svelte
    .preprocess(htmlCode, {
      script(ops) {
        return {
          code: transformFn(ops.content)
        }
      }
    })
    .then((v) => v.code)
}
function traverseScript(htmlCode: string, transformFn: (v: string) => string) {
  return svelte
    .preprocess(htmlCode, {
      script(ops) {
        return {
          code: transformFn(ops.content)
        }
      }
    })
    .then((v) => v.code)
}

示例代码地址:inline-code/svelte.ts

posthtml

PostHTML 是一个支持使用用 JS 插件转换 HTML/XML 的库。本身只包含HTML parser, HTML node tree API, node tree stringifier三部分。

插件开发也很简单,其官方的awesome里提供了很多示例的插件,也有可参考的API文档

先通过AST Explorer demo 示例看一下其生成的AST面貌

其AST结构描述很朴素

图片

使用方法如下,也很简单

ts
import posthtml, { Node } from 'posthtml'

const result = posthtml()
    .use(posthtmlPlugin)
    .process(htmlCode, { sync: true }).html
import posthtml, { Node } from 'posthtml'

const result = posthtml()
    .use(posthtmlPlugin)
    .process(htmlCode, { sync: true }).html

这里就简单实现一下posthtmlScriptContentTransform

  • 利用match遍历script节点
  • 使用用户传入的transformFn处理content内容
ts
import type { Node } from 'posthtml'

function posthtmlScriptContentTransform(transformFn: (v: string) => string) {
  return (tree: Node) => {
    tree.match({ tag: 'script' }, (node) => {
      if (node?.content?.[0]) {
        node.content[0] = transformFn(node.content[0].toString())
      }
      return node
    })
  }
}
import type { Node } from 'posthtml'

function posthtmlScriptContentTransform(transformFn: (v: string) => string) {
  return (tree: Node) => {
    tree.match({ tag: 'script' }, (node) => {
      if (node?.content?.[0]) {
        node.content[0] = transformFn(node.content[0].toString())
      }
      return node
    })
  }
}

最终使用代码如下

ts
function traverseScript(htmlCode: string, transformFn: (v: string) => string) {
  return posthtml()
    .use(posthtmlScriptContentTransform(transformFn))
    .process(htmlCode, { sync: true }).html
}
function traverseScript(htmlCode: string, transformFn: (v: string) => string) {
  return posthtml()
    .use(posthtmlScriptContentTransform(transformFn))
    .process(htmlCode, { sync: true }).html
}

示例代码地址:inline-code/posthtml.ts

小结

这部分除了正则外,介绍了3个可以用来提取inline js库(gogocodesvelteposthtml

从专业程度来看posthtml更加合适,拓展起来也方便,后面的功能也将基于其直接开发插件,方便复用。

使用SWC处理

SWC 是一个可以用于替换babel的工具,基于Rust实现,在单线程上比 Babel 快20倍。

前面主要阐述了html中js代码的提取,这个部分就简单阐述一下使用SWC对js部分代码进行ES语法降级压缩

直接利用transformSync方法对代码进行语法降级

  • jsc:js代码编译相关配置
  • minify:是否压缩
ts
import { transformSync } from '@swc/core'
import type { JscTarget } from '@swc/core'

export function transformCode(
  code: string,
  target: JscTarget = 'es5',
  minify = false
) {
  return transformSync(code, {
    jsc: {
      target
    },
    minify
  }).code
}
import { transformSync } from '@swc/core'
import type { JscTarget } from '@swc/core'

export function transformCode(
  code: string,
  target: JscTarget = 'es5',
  minify = false
) {
  return transformSync(code, {
    jsc: {
      target
    },
    minify
  }).code
}

minifySync直接对代码进行压缩处理。

由于在HTML中的JS 代码可能会被后续的script所引用。所以关掉混淆策略,避免影响代码正常工作

例如

ts
const hello = 'hello'
// 开启混淆后结果是
var l="hello";
const hello = 'hello'
// 开启混淆后结果是
var l="hello";
ts
import { minifySync } from '@swc/core'
import type { JsMinifyOptions } from '@swc/core'

export function minifyCode(code: string, ops?: JsMinifyOptions) {
  return minifySync(code, {
    compress: false,
    mangle: false,
    ...ops
  }).code
}
import { minifySync } from '@swc/core'
import type { JsMinifyOptions } from '@swc/core'

export function minifyCode(code: string, ops?: JsMinifyOptions) {
  return minifySync(code, {
    compress: false,
    mangle: false,
    ...ops
  }).code
}

其它压缩相关的细节参数这里就不展开介绍了,可以看结合文档介绍和官方的playground进行实践验证

posthtml插件封装

这里就封装2个posthtmlSWCMinifyposthtmlSWCTransform2个方法,用于压缩和转换两个场景

ts
export function posthtmlSWCTransform(
  target: JscTarget = 'es5',
  minify = false
) {
  return (tree: Node) => {
    tree.match({ tag: 'script' }, (node) => {
      if (node?.content?.[0]) {
        node.content[0] = transformCode(
          node.content[0].toString(),
          target,
          minify
        )
      }
      return node
    })
  }
}

export function posthtmlSWCMinify(ops?: JsMinifyOptions) {
  return (tree: Node) => {
    tree.match({ tag: 'script' }, (node) => {
      if (node?.content?.[0]) {
        node.content[0] = minifyCode(node.content[0].toString(), ops)
      }
      return node
    })
  }
}
export function posthtmlSWCTransform(
  target: JscTarget = 'es5',
  minify = false
) {
  return (tree: Node) => {
    tree.match({ tag: 'script' }, (node) => {
      if (node?.content?.[0]) {
        node.content[0] = transformCode(
          node.content[0].toString(),
          target,
          minify
        )
      }
      return node
    })
  }
}

export function posthtmlSWCMinify(ops?: JsMinifyOptions) {
  return (tree: Node) => {
    tree.match({ tag: 'script' }, (node) => {
      if (node?.content?.[0]) {
        node.content[0] = minifyCode(node.content[0].toString(), ops)
      }
      return node
    })
  }
}

使用示例如下

ts
import posthtml from 'posthtml'

posthtml()
      .use(posthtmlSWCTransform())
      .process(htmlCode, { sync: true })

posthtml()
      .use(posthtmlSWCMinify())
      .process(htmlCode, { sync: true })
import posthtml from 'posthtml'

posthtml()
      .use(posthtmlSWCTransform())
      .process(htmlCode, { sync: true })

posthtml()
      .use(posthtmlSWCMinify())
      .process(htmlCode, { sync: true })

至此对HTML中inlineJS的提取与使用SWC处理的方法进行了较为详细的阐述,下面就是通过CLI组合能力,然后对外提供使用。

CLI封装

通过封装一个简单的CLI工具,直接对目标HTML进行转换,调用起来更加的便捷,也方便的在现有工程中集成。

参数定义

使用commander做参数解析,先定义一下指令和传参,就2个指令transformminify,只包含上述方法的基本的传入参数

ts
#!/usr/bin/env node

import { Command } from 'commander'
import pkg from '../package.json'
import { minifyCommand, transformCommand } from './command'

const program = new Command()
program.version(pkg.version)

program
  .command('transform [paths...]')
  .description('transform inlineJS code ESVersion by SWC')
  .alias('t')
  .option(
    '-e, --ecmaVersion [ecmaVersion]',
    'set transform jsc target version',
    'es5'
  )
  .option('-m, --minify', 'minify transform result')
  .action(transformCommand)

program
  .command('minify [paths...]')
  .description('minify inlineJS code by SWC')
  .alias('m')
  .action(minifyCommand)

program.parse(process.argv)
#!/usr/bin/env node

import { Command } from 'commander'
import pkg from '../package.json'
import { minifyCommand, transformCommand } from './command'

const program = new Command()
program.version(pkg.version)

program
  .command('transform [paths...]')
  .description('transform inlineJS code ESVersion by SWC')
  .alias('t')
  .option(
    '-e, --ecmaVersion [ecmaVersion]',
    'set transform jsc target version',
    'es5'
  )
  .option('-m, --minify', 'minify transform result')
  .action(transformCommand)

program
  .command('minify [paths...]')
  .description('minify inlineJS code by SWC')
  .alias('m')
  .action(minifyCommand)

program.parse(process.argv)

图片

指令实现

下面是 transformCommand 的简单实现

  • 使用fs模块读写文件内容
  • 直接通过posthtml调用前面实现的posthtmlSWCTransform插件对内容进行转换
ts
// transformCommand
import fs from 'fs'
import type { JscTarget } from '@swc/core'
import posthtml from 'posthtml'
import { posthtmlSWCTransform } from '../index'

interface Options {
  ecmaVersion?: JscTarget
  minify?: boolean
}

export default function transformCommand(filesArg: string[], options: Options) {
  for (const filepath of filesArg) {
    const content = fs.readFileSync(filepath, 'utf-8')
    const result = posthtml()
      .use(posthtmlSWCTransform(options.ecmaVersion || 'es5', !!options.minify))
      .process(content, { sync: true }).html
    fs.writeFileSync(filepath, result, 'utf-8')
  }
}
// transformCommand
import fs from 'fs'
import type { JscTarget } from '@swc/core'
import posthtml from 'posthtml'
import { posthtmlSWCTransform } from '../index'

interface Options {
  ecmaVersion?: JscTarget
  minify?: boolean
}

export default function transformCommand(filesArg: string[], options: Options) {
  for (const filepath of filesArg) {
    const content = fs.readFileSync(filepath, 'utf-8')
    const result = posthtml()
      .use(posthtmlSWCTransform(options.ecmaVersion || 'es5', !!options.minify))
      .process(content, { sync: true }).html
    fs.writeFileSync(filepath, result, 'utf-8')
  }
}

minifyCommand的实现是类似的这里就不再赘述。

效果

安装 npm i -g @sugarat/inlinejs-transform

sh
ijs minify __test__/test.html
ijs minify __test__/test.html

图片

sh
ijs transform __test__/test.html --minify
ijs transform __test__/test.html --minify

图片

最后

文章中涉及示例代码以及工具完整源码见 GitHub

如内容有误还请评论区斧正,读者有其它💡想法可评论&私信交流探讨。

更新于: