本帖最后由 朱焱伟 于 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的两种油猴工程化方案的帖子里,留下两个问题:
- 配置create-tampermonkey生成的工程,使之支持jsx语法糖
- 配置vite-plugin-monkey生成的工程,使之支持sourcemap
很荣幸,帖子得到monkey插件的作者留言,插件从v2.8.0起,已支持为 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
,第一条结果就是medium上的how-to-use-jsx-without-react。
medium链接不能打开,可以看作者Kartik Nair博客上的jsx-without-react,内容一样,这个能打开。
读完此文,可以相应修改curlyのcreate-tampermonkey初始化后的工程,直接来吧!
step1:初始化项目
使用create-tampermonkey插件初始化
npx create-tampermonkey helloworld
cd helloworld
pnpm install
为简单起见,未添加typescript和linter支持
step2:添加babel下支持jsx的插件
pnpm install -D @babel/plugin-transform-react-jsx
pnpm install -D @babel/plugin-syntax-jsx
step3:修改rollup对babel插件的配置
工程目录下rollup_configs/default.js
用
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
}),
来替换默认的
// 请把我替换掉
plugins: [
commonjs(),
nodeResolve({
extensions
}),
babel({
babelHelpers: 'bundled',
exclude: 'node_modules/**',
extensions
}),
这里关键是要在rollup设置里把babel的配置放在commonjs之前。
step4:复制内容到main.js
把codesandbo上jsx-in-the-browser/main.js里的代码复制到我们的main.js里。注意有个错误,作者在第二行多打了一个星号,要把这多出来的星号删除。第二行变成:
/** @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的插件
postcss({
plugins: [autoprefixer()],
writeDefinitions: false,
sourceMap:"inline"
})
注意,postcss这里的sourceMap的M是大写。这样在F12>Sources>被注入的目标网站的文件夹
可以看到写的css代码(js在脚本管理器文件夹看)。
rollup.config.js里设置prodConfigs
除了devConfigs还有prodConfigs里的output也可以设置sourcemap:'inline'
<貳> vhtml篇
以上为按照印度老哥的方法设置油猴脚本添加jsx语法糖的过程,顺便也设置了一下sourcemap。
stackoverflow上,也有类似的讨论:can-i-use-jsx-without-react-to-inline-html-in-script。
从中可知,实现jsx without React
也有很多方法。这里就另外尝试一下preact之父搞的vhtml,
在按照上文配置篇的设置方法操作后,从印度老哥的方法切换到使用vhtml的方法。操作并不复杂,两步就能改为由vhtml支持jsx语法糖。
step1:安装vhtml
这里默认已按照配置篇设置了环境,这是前提。
pnpm install -D vhtml
step2:替换main.js
把main.js里面换成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之父在What does h mean?里这样解释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上rollup.js 打包错误原因:plugin有顺序,才发现问题。
具体rollup插件执行的先后顺序优先级有解释:
请注意,大多数情况下@rollup/plugin-commonjs应该在其他插件转化你的模块之前进行,这是为了防止其他插件的更改导致对 CommonJS 的检测被破坏。这个规则的一个例外是 Babel 插件,如果你正在使用它,那么把它放在 commonjs 插件之前。
坑2:@jsx
注解前面只能有两个星号
配置篇直接复制codesandbox上的代码到main.js,若不作修改,pnpm run build
同样可以编译成功(若不自宫,也能成功);但编译出来的user.js运行时会报React.Fragment
未定义的错误。
这是因为@jsxFrag
前面错误地打了三个星号,导致babel-plugin-transform-react-jsx
插件不知道要用手写的createFragment
函数去处理Fragment,默认编译成调用React.Fragment
,运行时在没有引入React的情况下是undefined。正确的写法要移除一个*:
/** @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那行再配,原因可见:pragma 和 pragmaFrag, automatic runtime
至此,编译已不报错。
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, [1, 2, 3].map(item => createElement("li", null, item)))));
发现含React.Fragment,这是@jsxFrag注解前面多打一个星号导致的,删去即可移除该依赖,React.Fragment被手写的createFragment函数代替。
至此,运行时也不报错,编译产物是VanillaJS,不需要require React库。
<柒> 讨论篇
上文仅说明了对create-tampermonkey插件生成的VanillaJS工程的jsx语法支持,没有配typescript的。jsx without React
应该还有其他做法,这里猜测一下。
脚本猫注入
看到jsx-in-the-browser@codesandbox里两行:
<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上有相关讨论:can-i-use-jsx-without-react-to-inline-html-in-script@stackoverflow
从中可知,还有不少其他方法,本文仅试了其中preact之父的vhtml。关于其他方法的可能,引用一段论述过来:
You should also look at libraries which do exactly this such as nervjs, jsx-render and 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特性,这样有较好的语法高亮和补全支持;看vhtml的描述,也可以写简单的Components。当然,如果开发较正式的脚本,还是使用vite脚手架。
<玖> 源码篇
配置好的工程
以上完整工程tmvanillaJSX.7z
的百度网盘秒传链接(里面两个branch分别对应两个Demo):
5295753fed94f1454b561559d5f90945#5295753fed94f1454b561559d5f90945#183227#tmvanillaJSX.7z
除了工程中的配置,Demo运行时,src中的main.js可以从F12的Sources看到。
印度佬源码
为方便查阅,把印度老哥在codesandbox的主要源码贴出来:
/** @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(([name, value]) => {
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;
};
相关资料