粥里有勺糖

vuePress-theme-reco 粥里有勺糖    2018 - 2023
粥里有勺糖 粥里有勺糖

Choose mode

  • dark
  • auto
  • light
关于我
备战春秋
  • 心得总结
  • 校招考点汇总
  • 面经汇总
  • 复习自查
技术笔记
  • 技术教程
  • 模板工程
  • 源码学习
  • 技术概念
  • 个人作品
  • 学习笔记
计算机基础
  • 算法与数据结构
  • 操作系统
  • 计算机网络
  • 设计模式
  • 剑指offer
大前端
  • javascript
  • vue
  • html
  • css
  • 🌏浏览器专题
  • Web性能优化
  • regexp
  • node
面试
  • 问解
  • javascript
  • css
  • 手撕代码
  • 性能优化
  • 综合问题
  • 面经汇总
  • 小程序
手撕代码
  • 数据结构与算法
  • javascript
  • css
个人站点
  • GitHub (opens new window)
  • 博客园 (opens new window)
  • 掘金 (opens new window)
线上作品
  • 轻取(文件收集) (opens new window)
  • 个人图床 (opens new window)
  • 考勤小程序 (opens new window)
  • 时光恋人 (opens new window)
  • 在线简历生成 (opens new window)
留言板
Github (opens new window)
author-avatar

粥里有勺糖

285

文章

40

标签

关于我
备战春秋
  • 心得总结
  • 校招考点汇总
  • 面经汇总
  • 复习自查
技术笔记
  • 技术教程
  • 模板工程
  • 源码学习
  • 技术概念
  • 个人作品
  • 学习笔记
计算机基础
  • 算法与数据结构
  • 操作系统
  • 计算机网络
  • 设计模式
  • 剑指offer
大前端
  • javascript
  • vue
  • html
  • css
  • 🌏浏览器专题
  • Web性能优化
  • regexp
  • node
面试
  • 问解
  • javascript
  • css
  • 手撕代码
  • 性能优化
  • 综合问题
  • 面经汇总
  • 小程序
手撕代码
  • 数据结构与算法
  • javascript
  • css
个人站点
  • GitHub (opens new window)
  • 博客园 (opens new window)
  • 掘金 (opens new window)
线上作品
  • 轻取(文件收集) (opens new window)
  • 个人图床 (opens new window)
  • 考勤小程序 (opens new window)
  • 时光恋人 (opens new window)
  • 在线简历生成 (opens new window)
留言板
Github (opens new window)
  • wheel

    • 个人作品
    • 时间管理CLI工具
    • 组装个支持记笔记的CodePen
    • ESCheck工具原理解析及增强实现
    • 一款检测代码中TODO的eslint插件
    • 实现一个Web UI检测(视觉走查)工具ing
    • 从0-1实现文件下载CLI工具
    • 内联JS处理(ES语法降级&内容压缩)
    • Node CLI工具原理解析
    • 我打造的在线简历生成应用
    • 助你轻松编写与分享snippet的VsCode插件
    • SourceMap解析CLI工具实现
    • 一个通过NPM包共(分)享代码块的解决方案
    • 实践:给女朋友个性化定制应用-体重记录(一)
    • 实践:给女朋友个性化定制应用-体重记录(二)
    • 实践:给女朋友个性化定制应用-体重记录(三)

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

vuePress-theme-reco 粥里有勺糖    2018 - 2023

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

粥里有勺糖 2022-10-22 技术笔记个人作品

# 内联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>
1
2
3
4
5
6
7
8
9

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

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

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

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

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

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

本文将进一步介绍一下提取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>
1
2
3
4
5
6
7
8
9
10
11
12
13

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

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

# 正则

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

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

图片

利用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))
  })
}
1
2
3
4
5
6

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

示例代码地址:inline-code/regexp.ts (opens new window)

# GoGoCode

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

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

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

先生成AST

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

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

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

图片

完整代码如下

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()
}
1
2
3
4
5
6
7
8

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

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

示例代码地址:inline-code/gogocode.ts (opens new window)

# svelte

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

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

这个是从 AST Explorer (opens new window) transform示例demo中看到的

看了一下demo实现代码 (opens new window)

sevlte/compiler (opens new window)提供了直接生成AST的方法compile

import * as svelte from 'svelte/compiler'

const AST = svelte.compile(htmlCode).ast
const htmlAST = AST.html
1
2
3
4

图片

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

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

import * as svelte from 'svelte/compiler'

svelte.preprocess(htmlCode, {
  script(ops) {
    return {
      code: transformFn(ops.content)
    }
  }
})
1
2
3
4
5
6
7
8
9

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

function traverseScript(htmlCode: string, transformFn: (v: string) => string) {
  return svelte
    .preprocess(htmlCode, {
      script(ops) {
        return {
          code: transformFn(ops.content)
        }
      }
    })
    .then((v) => v.code)
}
1
2
3
4
5
6
7
8
9
10
11

示例代码地址:inline-code/svelte.ts (opens new window)

# posthtml

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

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

先通过AST Explorer demo 示例 (opens new window)看一下其生成的AST面貌

其AST结构描述很朴素

图片

使用方法如下,也很简单

import posthtml, { Node } from 'posthtml'

const result = posthtml()
    .use(posthtmlPlugin)
    .process(htmlCode, { sync: true }).html
1
2
3
4
5

这里就简单实现一下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
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

最终使用代码如下

function traverseScript(htmlCode: string, transformFn: (v: string) => string) {
  return posthtml()
    .use(posthtmlScriptContentTransform(transformFn))
    .process(htmlCode, { sync: true }).html
}
1
2
3
4
5

示例代码地址:inline-code/posthtml.ts (opens new window)

# 小结

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

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

# 使用SWC处理

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

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

直接利用transformSync (opens new window)方法对代码进行语法降级

  • jsc (opens new window):js代码编译相关配置
  • minify (opens new window):是否压缩
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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

minifySync (opens new window)直接对代码进行压缩处理。

  • compress参数 (opens new window):可用于更细粒度的控制压缩策略
  • mangle参数 (opens new window):细粒度的控制代码混淆策略

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

例如

const hello = 'hello'
// 开启混淆后结果是
var l="hello";
1
2
3
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
}
1
2
3
4
5
6
7
8
9
10

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

# posthtml插件封装

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

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
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

使用示例如下

import posthtml from 'posthtml'

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

posthtml()
      .use(posthtmlSWCMinify())
      .process(htmlCode, { sync: true })
1
2
3
4
5
6
7
8
9

至此对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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

图片

# 指令实现

下面是 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')
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

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

# 效果

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

ijs minify __test__/test.html
1

图片

ijs transform __test__/test.html --minify
1

图片

# 最后

文章中涉及示例代码以及工具完整源码见 GitHub (opens new window)

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

Edit this page (opens new window)
Last Updated: 2022/10/29 17:10:45