内联JS处理(ES语法降级&内容压缩)
本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
本文简单介绍一下当前html
在现代工程中的现状,并阐述内联js代码可能存在的一些问题,同时介绍一系列处理页面内联脚本的方法,以及通过SWC
如何转换目标代码,通过CLI工具如何组合这些能力
。
前言
当下大部分现代前端Web工程中,HTML文件大部分都是以public/index.html
或<projectRoot>/index.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内容处理
用于测试的目标代码如下
<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
// 简单转换示例
function simpleConst2Var(code: string) {
return code.replace(/const /g, 'var ')
}
正则
搞文本内容的处理首先想到的方法,简单的正则如下
/<script>([\s\S]*?)<\/script>/g
利用replace
方法,几行代码就能搞定
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
import $ from 'gogocode'
const htmlAST = $(htmlCode, { parseOptions: { language: 'html' } })
遍历<script>
节点,其中$scriptNode
节点结构如下,可以直接使用attr
方法进行值的存取操作
htmlAST.find(`<script>$_$</script>`).each(($scriptNode) => {
const origin = $scriptNode.attr('content.value.content')
$scriptNode.attr('content.value.content', transformFn(origin.toString()))
})
完整代码如下
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
import * as svelte from 'svelte/compiler'
const AST = svelte.compile(htmlCode).ast
const htmlAST = AST.html
同时提供了一个预处理方法preprocess
,可以实现script
,style
与其他标签内容的遍历与修改,最后返回处理后的结果
使用示例如下,其返回值是promise
import * as svelte from 'svelte/compiler'
svelte.preprocess(htmlCode, {
script(ops) {
return {
code: transformFn(ops.content)
}
}
})
同样按照上面要求实现script代码的转换
,代码很简洁
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结构描述很朴素
使用方法如下,也很简单
import posthtml, { Node } from 'posthtml'
const result = posthtml()
.use(posthtmlPlugin)
.process(htmlCode, { sync: true }).html
这里就简单实现一下posthtmlScriptContentTransform
- 利用
match
遍历script
节点 - 使用用户传入的
transformFn
处理content
内容
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
})
}
}
最终使用代码如下
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
库(gogocode
,svelte
,posthtml
)
从专业程度来看posthtml
更加合适,拓展起来也方便,后面的功能也将基于其直接开发插件,方便复用。
使用SWC处理
SWC 是一个可以用于替换babel的工具,基于Rust实现,在单线程上比 Babel 快20倍。
前面主要阐述了html中js代码的提取,这个部分就简单阐述一下使用SWC
对js部分代码进行ES语法降级
与压缩
。
直接利用transformSync方法对代码进行语法降级
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直接对代码进行压缩处理。
- compress参数:可用于更细粒度的控制压缩策略
- mangle参数:细粒度的控制代码混淆策略
由于在HTML中的JS 代码可能会被后续的script
所引用。所以关掉混淆策略,避免影响代码正常工作
例如
const hello = 'hello'
// 开启混淆后结果是
var l="hello";
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个posthtmlSWCMinify
和posthtmlSWCTransform
2个方法,用于压缩和转换两个场景
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
})
}
}
使用示例如下
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个指令transform
和minify
,只包含上述方法的基本的传入参数
#!/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
插件对内容进行转换
// 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
ijs minify __test__/test.html
ijs transform __test__/test.html --minify
最后
文章中涉及示例代码以及工具完整源码见 GitHub
如内容有误还请评论区斧正,读者有其它💡想法可评论&私信交流探讨。