[作废] 给VanillaJS的油猴脚本添加jsx语法糖支持
本帖最后由 朱焱伟 于 2022-10-24 21:20 编辑# 给VanillaJS的油猴脚本添加jsx语法糖支持
## 更新:作废原因
说明一下作废原因,搬运一下评论区老哥的分析,这总结的很好,正是我想说的:
> 1.我之前认为 require 会让这个脚本在每次运行之前先发起网络请求获取cdn代码
2.因此我不想通过require引入库,但是又要保持 build.user.js 的大小尽量小
3.所以要自己替换 jsx h 函数,所以才有这个帖子。
现在我的第一个认知前提已经被纠正了,后面的自然没有存在的必要了
这个帖子配置过程可能没有什么太大的错误,但是这个探索目的是寻求避免require请求的方法,原来是我理解错误,require只需要安装时请求一次保存本地,并不是每次要请求。这样我要解决的问题本来就不存在,因此作废。
本文的核心就是自定义jsx的h函数,达到减少build.user.js的大小。如果是想让油猴脚本支持jsx,加个webpack-loader或者rollup-plugin就行,不必这样搞。
这个帖子的评论纠正了我对require错误的认识,也是很好的事。
----------------------------------------分割线-------------------------------------------
------------------------------------------------
普通的油猴脚本,在不使用React等框架的情况下,如何支持jsx语法糖?
## 问题
在[基于vite和rollup的两种油猴工程化方案](https://bbs.tampermonkey.net.cn/thread-3399-1-1.html)的帖子里,留下两个问题:
- 配置create-tampermonkey生成的工程,使之支持jsx语法糖
- 配置vite-plugin-monkey生成的工程,使之支持sourcemap
很荣幸,帖子得到monkey插件的作者留言,插件从(https://github.com/lisonge/vite-plugin-monkey/blob/v2.8.0/packages/vite-plugin-monkey/CHANGELOG.md)起,已支持为 build.user.js 生成正确的 sourcemap 映射。第二个问题得到解决。
之前讨论的油猴脚本工程化解决方案中,个人观点:
vite-plugin-monkey > create-tampermonkey > 火猴模板 > 暴力猴模板
油猴工程化,大部分情况应该使用monkey插件。
还剩第一个问题,本帖将试图解决。先说明配置方法(手写函数或使用vhtml);再讨论使用场景,描述摸索过程;最后再实现LOGO替换的简单Demo。
## <壹> 配置篇
这里先按照印度老哥Kartik Nair的方法,给create-tampermonkey生成的工程加上jsx语法糖支持,然后再顺便加上对css的sourcemap支持。
### jsx without React
在google上搜索`jsx without React`,第一条结果就是(https://betterprogramming.pub/how-to-use-jsx-without-react-21d23346e5dc)。
medium链接不能打开,可以看作者Kartik Nair博客上的(https://ospress.co/kartik/jsx-without-react),内容一样,这个能打开。
读完此文,可以相应修改(https://www.npmjs.com/package/create-tampermonkey)初始化后的工程,直接来吧!
#### step1:初始化项目
使用create-tampermonkey插件初始化
```bash
npx create-tampermonkey helloworld
cd helloworld
pnpm install
```
为简单起见,未添加typescript和linter支持
#### step2:添加babel下支持jsx的插件
```bash
pnpm install -D @babel/plugin-transform-react-jsx
pnpm install -D @babel/plugin-syntax-jsx
```
#### step3:修改rollup对babel插件的配置
工程目录下rollup_configs/default.js
用
```javascript
plugins: [
babel({
babelHelpers: 'bundled',
exclude: 'node_modules/**',
extensions,
plugins: [
["@babel/plugin-transform-react-jsx", {
// "runtime": "automatic"
}],
["@babel/plugin-syntax-jsx",{
"pragma": "createElement", // React.createElement
"pragmaFrag": "createFragment", // React.Fragment
}]
],
}),
commonjs(),
nodeResolve({
extensions
}),
```
来替换默认的
```javascript
// 请把我替换掉
plugins: [
commonjs(),
nodeResolve({
extensions
}),
babel({
babelHelpers: 'bundled',
exclude: 'node_modules/**',
extensions
}),
```
这里关键是要在rollup设置里把babel的配置放在commonjs之前。
#### step4:复制内容到main.js
把(https://codesandbox.io/s/jsx-in-the-browser-qd2hq?file=/main.js)里的代码复制到我们的main.js里。注意有个错误,作者在第二行多打了一个星号,要把这多出来的星号删除。第二行变成:
```javascript
/** @jsxFrag createFragment */
```
此时再`pnpm run build`,编译成功,已经可以编译类似如下的语句。
```
document.getElementById("root").appendChild(<UsingFragment name="foo" />);
```
至此,添加jsx语法糖支持已经成功,且编译结果是纯VanillaJS,不需要require像React这样的库。~~END撒花~~
### sourcemap
create-tampermonkey模板共有三处设置sourcemap的地方,rollup.config.js文件中devConfigs里的sourcemap已经默认开启,prodConfigs没开,这样build出来的user.js不带sourcemap; rollup.config.js里还会读取rollup_config/default.js里的配置,在default.js里可以设置开启postcss插件的sourceMap。
#### rollup_config的default.js里设置postcss的插件
```javascript
postcss({
plugins: ,
writeDefinitions: false,
sourceMap:"inline"
})
```
注意,postcss这里的sourceMap的M是大写。这样在`F12>Sources>被注入的目标网站的文件夹`可以看到写的css代码(js在脚本管理器文件夹看)。
#### rollup.config.js里设置prodConfigs
除了devConfigs还有prodConfigs里的output也可以设置`sourcemap:'inline'`
## <貳> vhtml篇
以上为按照印度老哥的方法设置油猴脚本添加jsx语法糖的过程,顺便也设置了一下sourcemap。
stackoverflow上,也有类似的讨论:(https://stackoverflow.com/questions/30430982/can-i-use-jsx-without-react-to-inline-html-in-script)。
从中可知,实现`jsx without React`也有很多方法。这里就另外尝试一下preact之父搞的(https://github.com/developit/vhtml),
在按照上文配置篇的设置方法操作后,从印度老哥的方法切换到使用vhtml的方法。操作并不复杂,两步就能改为由vhtml支持jsx语法糖。
### step1:安装vhtml
这里默认已按照配置篇设置了环境,这是前提。
```bash
pnpm install -D vhtml
```
### step2:替换main.js
把main.js里面换成(https://github.com/developit/vhtml)中usage下的代码段,然后就能`pnpm run build`成功。
引入h函数,并且通过注解告诉babel用h函数去替换。(下面省略了测试部分)
```
// import the library:
import h from 'vhtml';
// tell babel to transpile JSX to h() calls:
/** @jsx h */
```
## <叁> 痛点篇
油猴脚本在写复杂一点的界面时,通常有两种做法:一是反引号包裹大段html字符串,二是引入框架,用webpack之类工具打包。
方法一的问题是:反引号内的大段html写起来没有语法高亮和代码补全,也比较难维护和复用。不知道油猴脚本中写这种大段html字符串时,是不是有辅助的插件什么的,反正我感觉写起来比较困难。
方法二引入框架,~~每次访问目标网站要对require的库发起请求。大型脚本这样做没问题,但如果ui不太复杂,有点得不偿失。~~(表述有点问题,大概指访问目标网站要访问相应的cdn)
比如,在火猴LOGO替换的Demo里,引入React库会使性能下降,不能达到原生JS的速度,而做的仅仅是替换一两个元素,偷鸡不成蚀把米属于是。
换用solidjs,svelte也是一种可行的办法,它们和Vue,React本质区别在于,他们是编译器而不是框架,直接体现就在于,它们编译出来的user.js不需要额外的require。我觉得是比较好的思路,但试了下,加个按钮这类操作编译出来的user.js也差不多几百行。
对暴力猴模板的摸索让我了解到,设置一下babel的pragma和pragmaFrag,自己搞两个函数来替代React.createElement和React.Fragment,就能添加jsx语法糖支持。本帖对此进行了探索,设置后,编译出来的user.js比较短,还属于普通的VanillaJS,不需要额外require,性能上和反引号写html字符串没有太大差别。
所以:大中型脚本使用vite插件;功能不太复杂的写点小界面的脚本使用这种`jsx without react`的方法,我觉得是比较好的。
## <肆> 解释篇
### h函数
暴力猴脚手架,会将jsx语法,翻译成这样的语句:`document.body.append(VM.m(VM.h("div", null, "hello, world")));`,这是通过在babel里设置`pragma: 'VM.h'`实现的,h函数比较常见,就是上文粘贴的手写`createElement`函数,和React里的`React.createElement`。vue之父在(https://github.com/vuejs/babel-plugin-transform-vue-jsx/issues/6)里这样解释h函数:
> It comes from the term "hyperscript", which is commonly used in many virtual-dom implementations. "Hyperscript" itself stands for "script that generates HTML structures" because HTML is the acronym for "hyper-text markup language".
即:`createElement`函数是用来生成 HTML DOM 元素的,也就是上文中的 generate HTML structures,也就是 Hyperscript,因此把 createElement 简写成 h。
## <伍> 坑人篇
主要有两个坑:一是`create-tampermonkey`默认生成的工程里,rollup插件的顺序有问题;二是印度老哥的代码里多打了一个星号。
### 坑1:rollup插件的先后顺序
配置rollup_configs/default.js中最关键的一点是:插件中,babel要写在前面,commonjs写在后面。默认生成的工程中,这两个是反的,这直接导致我无论怎么配置babel,都会报语法错误。我不懂如何配置rollup,之前想依葫芦画瓢把暴力猴的jsx支持加进来,未遂,就卡在这里,看到CSDN上(https://blog.csdn.net/kalrase/article/details/110186870),才发现问题。
具体(https://blog.csdn.net/u014418267/article/details/120425000)有解释:
> 请注意,大多数情况下@rollup/plugin-commonjs应该在其他插件转化你的模块之前进行,这是为了防止其他插件的更改导致对 CommonJS 的检测被破坏。这个规则的一个例外是 Babel 插件,如果你正在使用它,那么把它放在 commonjs 插件之前。
### 坑2:`@jsx`注解前面只能有两个星号
配置篇直接复制(https://codesandbox.io/s/jsx-in-the-browser-qd2hq?file=/main.js)到main.js,若不作修改,`pnpm run build`同样可以编译成功(~~若不自宫,也能成功~~);但编译出来的user.js运行时会报`React.Fragment`未定义的错误。
这是因为`@jsxFrag`前面错误地打了三个星号,导致`babel-plugin-transform-react-jsx`插件不知道要用手写的`createFragment`函数去处理Fragment,默认编译成调用`React.Fragment`,运行时在没有引入React的情况下是undefined。正确的写法要移除一个\*:
```javascript
/** @jsxFrag createFragment */
```
## <陆> 摸索篇
### Error01.Unexpected token
开始时模板默认的babel和commonjs的配置顺序错误,写jsx会报语法错
```
[!] (plugin commonjs) SyntaxError: Unexpected token (33:4) in main.js
```
### Error02.jsx is not currently enabled
在default.js里把babel配置调到commonjs之前,会出现另外的报错:
```
[!] (plugin babel) SyntaxError: main.js: Support for the experimental syntax 'jsx' isn't currently enabled:
```
### Error03.automatic runtime
在babel的plugin里加上@babel/plugin-transform-react-jsx,并安装该插件,尝试这样配置babel:
```
babel({
babelHelpers: 'bundled',
exclude: 'node_modules/**',
extensions,
plugins: [
["@babel/plugin-transform-react-jsx", {
"runtime": "automatic"
}]
],
}),
```
报错变为:
```
[!] (plugin babel) SyntaxError: main.js: pragma and pragmaFrag cannot be set when runtime is automatic.
```
于是注释掉automatic那行再配,原因可见:(https://bin.zmide.com/?p=1037)
至此,编译已不报错。
### Error04.运行时缺少React.Fragment
但是,在运行编译出来的user.js时,发现在目标网站会报React.Fragment这个undefined。
检查编译出来的语句:
```
const UsingFragment = () => createElement("div", null, createElement("p", null, "This is regular paragraph"), createElement("div", null, createElement("p", null, "This is a paragraph in a fragment"), createElement(React.Fragment, null, createElement("p", null, "Hello")), createElement("ul", null, .map(item => createElement("li", null, item)))));
```
发现含React.Fragment,这是@jsxFrag注解前面多打一个星号导致的,删去即可移除该依赖,React.Fragment被手写的createFragment函数代替。
至此,运行时也不报错,编译产物是VanillaJS,不需要require React库。
## <柒> 讨论篇
上文仅说明了对create-tampermonkey插件生成的VanillaJS工程的jsx语法支持,没有配typescript的。`jsx without React`应该还有其他做法,这里猜测一下。
### 脚本猫注入
看到(https://codesandbox.io/s/jsx-in-the-browser-qd2hq)里两行:
```html
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel" src="/main.js"></script>
```
估计如果搞歪门邪道的话,让脚本猫内置babel,给目标网站先注入babel.min.js,然后注入user.js时把type设为`"text/babel"`,就能直接在用户脚本里写jsx。当然,效率是另一回事,这里仅猜测一下可能性。
### monkey插件
create-tampermonkey打包干净,monkey也接近。使用体验上,还是monkey插件更好。下次再试试在monkey插件生成的VanillaJS工程里配置jsx,应该也差不多。
### 其他库
这里的库,指的还是编译user.js时的库,不是指最后要require的。stackoverflow上有相关讨论:(https://stackoverflow.com/questions/30430982/can-i-use-jsx-without-react-to-inline-html-in-script)
从中可知,还有不少其他方法,本文仅试了其中preact之父的vhtml。关于其他方法的可能,引用一段论述过来:
> You should also look at libraries which do exactly this such as (https://github.com/NervJS/nerv), (https://www.npmjs.com/package/jsx-render) and (https://www.npmjs.com/package/deku). All of these use a JSX html syntax without react. Some (such as jsx-render) are only focused on converting JSX to the final JS, which might be what you're looking for.
比如凹♀凸♂工作室的nervjs为某东首页加载提供速度支持,体积比react小。不过本文讨论的是VanillaJS,希望语法糖转换都在编译user.js时完成,运行时不要require任何库。
##<捌> Demo篇
虽然某hub的LOGO已经被玩烂了,但不妨再搞两个Demo,这样和原版原生JS可以有速度的比较。
### 猫咪嘤文网LOGO替换
这是在配置篇完成后,接着做的Demo。是对印度老哥手写两个函数去替换React.createElement及React.Fragment的方法的演示。

### 大黄汪文网LOGO替换vhtml
这是在vhtml篇完成后,接着做的demo。是对vhtml库(非运行时require,仅编译期使用)方法的演示。

### 比较
本文的两个Demo,都比之前的火猴LOGO替换好很多,因为这次不需要require了,而且创建的是真实DOM。因为编译出来的就是VanillaJS,所以和原版LOGO替换在速度上没有大的差异。本帖的使用场景主要就是小脚本里用点jsx特性,这样有较好的语法高亮和补全支持;看(https://github.com/developit/vhtml)的描述,也可以写简单的Components。当然,如果开发较正式的脚本,还是使用vite脚手架。
##<玖> 源码篇
### 配置好的工程
以上完整工程`tmvanillaJSX.7z`的百度网盘秒传链接(里面两个branch分别对应两个Demo):
```
5295753fed94f1454b561559d5f90945#5295753fed94f1454b561559d5f90945#183227#tmvanillaJSX.7z
```
除了工程中的配置,Demo运行时,src中的main.js可以从F12的Sources看到。
### 印度佬源码
为方便查阅,把印度老哥在codesandbox的主要源码贴出来:
```javascript
/** @jsx createElement */
/** @jsxFrag createFragment */
const createElement = (tag, props, ...children) => {
if (typeof tag === "function") return tag(props, ...children);
const element = document.createElement(tag);
Object.entries(props || {}).forEach(() => {
if (name.startsWith("on") && name.toLowerCase() in window)
element.addEventListener(name.toLowerCase().substr(2), value);
else element.setAttribute(name, value.toString());
});
children.forEach((child) => {
appendChild(element, child);
});
return element;
};
const appendChild = (parent, child) => {
if (Array.isArray(child))
child.forEach((nestedChild) => appendChild(parent, nestedChild));
else
parent.appendChild(child.nodeType ? child : document.createTextNode(child));
};
const createFragment = (props, ...children) => {
return children;
};
```
## 相关资料
- (https://ospress.co/kartik/jsx-without-react)
- (https://medium.com/better-programming/how-to-use-jsx-without-react-21d23346e5dc)
- (https://stackoverflow.com/questions/30430982/can-i-use-jsx-without-react-to-inline-html-in-script)
- (https://codesandbox.io/s/jsx-in-the-browser-qd2hq?file=/main.js)
- [为什么函数式组件需要引进React](https://calpa.me/blog/why-import-react-from-react-in-a-functional-component/)
- (https://github.com/vuejs/babel-plugin-transform-vue-jsx/issues/6)
- (https://github.com/developit/htm)
- (https://github.com/developit/vhtml)
- (https://zhuanlan.zhihu.com/p/78651155)
- (https://github.com/egoist/rollup-plugin-postcss/issues/1)
- (https://bin.zmide.com/?p=1037)
- (https://babeljs.io/docs/en/babel-plugin-transform-react-jsx)
- (https://blog.csdn.net/kalrase/article/details/110186870)
- (https://blog.csdn.net/u014418267/article/details/120425000)
- (https://github.com/kartiknair/dhow)
- (https://www.gatsbyjs.com/blog/2019-08-02-what-is-jsx-pragma/)
- [暴力猴jsx语法](https://violentmonkey.github.io/guide/using-modern-syntax/)
- [猫咪嘤文网LOGO替换](https://scriptcat.org/script-show-page/686)
- [大黄汪文网LOGO替换vhtml](https://scriptcat.org/script-show-page/687/code)
图片怎么挂了,我前一次看还有的 王一之 发表于 2022-10-22 23:51
图片怎么挂了,我前一次看还有的
第一次上传多了一张图片,虽然在markdown编辑器删除了,但还是附在文末,我返回普通编辑器删去的,markdown又乱了,重新粘贴的。 > 方法二引入框架,每次访问目标网站要对require的库发起请求。大型脚本这样做没问题,但如果ui不太复杂,有点得不偿失。
这里是不是描述有点问题。require 引入的库是在安装的时候就已经下载好了的,后续每次运行拼接好的源码,是不需要二次下载的
--- shabby 发表于 2022-10-23 22:35
> 方法二引入框架,每次访问目标网站要对require的库发起请求。大型脚本这样做没问题,但如果ui不太复杂, ...
我说的有问题,大概指的是,脚本使用时要@require相关的cdn,不是指开发阶段安装的。比如"https://cdn.jsdelivr.net/npm/vue@next",每次要访问它 本帖最后由 shabby 于 2022-10-23 23:20 编辑
朱焱伟 发表于 2022-10-23 22:50
我说的有问题,大概指的是,脚本使用时要@require相关的cdn,不是指开发阶段安装的。比如"https://cdn.js ...
不好意思,我还是没懂这里的 "每次要访问它" 的意思
访问是指网络请求?还是执行这个库的代码?
---
还有,这个 “油猴中文网”能不能向 github 一样,在有评论消息时发邮件给我,我没找到这块的设置,我现在要主动刷新才能看见消息
本帖最后由 朱焱伟 于 2022-10-23 23:23 编辑
shabby 发表于 2022-10-23 23:15
不好意思,我还是没懂这里的 "每次要访问它" 的意思
访问是指网络请求?还是执行这个库的代码?
网络请求。大概意思是说,为了加个按钮这种小功能,每次网络请求cdn不划算。小功能不想:
// @require https://unpkg.com/react@18/umd/react.development.js
朱焱伟 发表于 2022-10-23 23:19
网络请求。大概意思是说,为了加个按钮这种小功能,每次网络请求cdn不划算 ...
require资源网络请求只会在安装脚本的时候产生
后续每次执行脚本都是用第一次下载好的库代码
这是我的理解,难不成每次都要请求 require 和 resource 标记的资源吗?
或者和 cdn 的缓存回复头有关吗?
shabby 发表于 2022-10-23 23:25
require资源网络请求只会在安装脚本的时候产生
后续每次执行脚本都是用第一次下载好的库代码
具体require原理我也不懂,可能是说的缓存好了的意思吧。有时候cdn的地址会失效,导致脚本不能运行。 朱焱伟 发表于 2022-10-23 23:30
具体require原理我也不懂,可能是说的缓存好了的意思吧。有时候cdn的地址会失效,导致脚本不能运行。 ...
根据 https://violentmonkey.github.io/api/metadata-block/#require 的描述
所需的脚本将随安装一起下载,并在执行脚本之前执行。
页:
[1]
2