在Web前端的 React 项目中使用 Snowpack 作为编译工具(Webpack 的替代工具),并在项目中引用到了 AntV/L7 库。使用时遇到两个错误。
第一个错误,编译错误
解决办法
在 snowpack.config.js 配置文件中增加 polyfill 配置
其它解决方法,参考 https://www.snowpack.dev/#node.js-polyfills
问题原因
@antv/l7-core 库依赖了 merge-json-schemas 库,merge-json-schemas 中使用了 node 内置库 assert,导致编译失败。
第二个错误,运行时错误
编译通过后,打开浏览器
点击跟踪错误代码位置,查看报错代码
解决办法
在浏览器全局定义一个高阶方法 require(注意此时全局当中不应当存在名为 require 的方法,如果存在则需要自行兼容)
function require(libName) {
if (libName == "load-styles") {
// https://www.npmjs.com/package/load-styles/v/2.0.0
return function loadStyles(css, doc) {
// default to the global `document` object
if (!doc) doc = document;
var head = doc.head || doc.getElementsByTagName("head")[0];
// no <head> node? create one...
if (!head) {
head = doc.createElement("head");
var body = doc.body || doc.getElementsByTagName("body")[0];
if (body) {
body.parentNode.insertBefore(head, body);
} else {
doc.documentElement.appendChild(head);
}
}
var style = doc.createElement("style");
style.type = "text/css";
if (style.styleSheet) {
// IE
style.styleSheet.cssText = css;
} else {
// the world
style.appendChild(doc.createTextNode(css));
}
head.appendChild(style);
return style;
};
}
throw Error(
`requrie ${libName} failed, require syntax is not supported`
);
}
以下是问题的定位及解决方法的思考过程
问题原因
通过出现问题的上下文,逐步回溯寻找问题原因。
错误代码位置在哪?
从浏览器的错误代码上下文推测,出错代码在L7的库中,似乎想要通过 commonjs 语法来引入一个 load-styles 库,特征字符串是 require('load-styles')。然后到项目的 node_modules/@antv/ 目录下搜索特征字符串,发现出现多处。且都是为了引入 require('load-styles') 。require('load-styles') 代码为什么会报错?
Snowpack在编译项目时,使用了 ES Module 方式管理模块,而L7的发布代码在ES Module模块中使用了 require 语法!?这是不正确的require('load-styles') 是做什么用的?
因为node_modules目录下并非源码,不容易查看,所以到 github 上搜索 L7 源码: https://github.com/antvis/L7(L7的文档尚不完善,没有直接跳转连接)。找到后,看到先浏览 package.json 配置,发现存在 load-styles@2.0.0 库。于是先去查看 load-styles 库的功能,github上未找到该库,但是在npm上找到,意外的是该库为6年前发布的。下载后查看源码,代码很少,功能就是把一个字符创建为CSS标签并插入网页中。L7中的require('load-styles') 就是为了把一段CSS文本内容作为style标签插入网页。
到这里基本确定了问题的原因:L7库中使用一个6年前的方式来将一段CSS文本变成一个style标签,然后在生成ES Module模块时,load-styles代码被错误的转义成 commonjs 语法。Snowpack在编译时不会对 ES Module 做处理,因为它认为 ES Module 已经是目标格式,不需要编译,因此也忽略了 ES Module 中错误的 require 语法,所以代码被发送给浏览器,在浏览器执行时才报出运行时错误。
原因到这里已经可以解释我遇到的问题,那么,更深层次的原因是什么?为什么会L7会产生 require('load-styles') 这样的语法,其它库则不会?
- 目标代码是怎么包含require('load-styles') 的?
以 @antv/l7-component 库为例,通过 import "xxx.css" 的方式引入样式文件 查看代码
发布到npm的编译后代码目录结构如下
.
├── CHANGELOG.md
├── LICENSE.md
├── es
│ ├── index.js
│ ├── ...
├── lib
│ ├── index.js
│ ├── ...
└── package.json
因为使用的 Snowpack 采用 ES Module 方式,所以引用的是 es/index.js 文件。查看该文件中的编译结果文件,看到模块中含有 require 语法,即最初遇到的错误。
import css的编译结果不应该是require方式,所以问题出在编译过程
- 自己编译,查看问题是否存在?
下载源码查看编译过程(代码库太大导致从 github 下载总失败,后来从https://gitee.com/antv/L7 下载到了源码)。
yarn install
npm run build
自己编译后查看编译结果
问题复现,接着查看编译过程。顺序查找较慢,所以还是通过特征代码查找。
- 编译过程是怎么进行的?
首先确定特征代码 require('load-styles'),特征库是 load-styles。然后通过 yarn.lock 文件查找 load-styles 的依赖关系,发现只有一处依赖(好兆头)
只有 babel-plugin-transform-import-styles 库使用了 load-styles,从名字看,这是用于转换 import css 的语法的 babel 插件库。因为是babel插件,所以到 babel.config.js 配置项中查找(babel的配置文件可能有不同的名字,但包含babel)
需要根据条件选择是否使用 transform-import-styles 插件(babel插件在使用时可以忽略掉 babel-plugin- 前缀,所以不能直接搜索完整名称,呵呵呵)。然后查看条件变量 isCDNBundle 来源
isCDNBundle 变量来自于环境变量 bundle 参数,环境变量参数来自于编译命令参数
所以在编译l7-components的ES Module模块时 isCDNBundle 变量为空,所以 babel-plugin-transform-import-styles 插件生效了。然后查看插件转换 import css文件的方法 源码。
babel-plugin-transform-import-css 采用 babel 模板替换的方法,将 import 的 CSS 文件内容附加在 require('load-styles')(/* CSS */) 模板中。因此最终的ES Module编译结果中出现了 require 语句,导致错误。
解决思路
- 思路一:修改打包、编译的配置,兼容ES Module中包含 require 的语法
不可行 。当前项目打包、编译依赖3个配置文件:babel.config.js、tsconfig.js、snowpack.config.js,尝试修改后都不能解决问题。原因是因为,无论哪个项目配置型都是针对项目中自己编写的代码的,不会处理 node_modules 中的文件,node_modules 中的文件被认为编译好的 ES Module,Snowpack 直接复制过来使用,不做处理。所以修改配置项都是不起作用的。
- 思路二:对 node_modules 中的文件进行预处理
不可行。Snowpack的优势就是默认采用 ES Module。如果对 node_modules 中的模块逐个编译就无法使用 Snowpack。虽然Snowpack中可以配置 Webpack 插件,但那时在 production模式下做代码合并优化时使用的。
- 思路三:采用 Webpack 编译
可行,但不想,不想退回 Webpack是因为技术框架选型的惯性,一旦采用了哪种方案,就不会轻易变动。
- 思路四:修改 @antv/l7 代码中的编译错误
不可行。首先 @antv/l7 是有组织的官方库,修正其代码就意味着要提交pull request,然后是漫长的等待,正在开发中的项目不能承受这种等待,提交PR然后等待修复的时间,不如想其它方案。
- 思路五:自己fork @antv/l7 代码库修改问题
成本高。@antv/l7 的代码库下 有多个package,而且相互依赖,有多个地方使用了 load-styles ,因此fork并维护多个package成本过高。
- 思路六:hotfix,使用某种补丁方法规避掉这个错误
值得一试。常用的比如 try catch、monkey patch、proxy 等等 magic ways ,可以解决一些不好处理的问题,只不过这种解决方式本身就是一种问题。所以最终打算使用这种方式来解决运行时报错问题。
解决方式
运行时错误有多处,但都是同一种错误,require('load-styles') 未定义导致。L7 使用这个方法只有一个目的,就是把一段 CSS 文本变成一个 style 标签插入 html 中。因此,在L7代码运行前实现一个全局方法,能够将 require('load-styles')('/* 这是一段CSS 文本*/') 正确的运行并生成style标签即可。
function require(){
return function(css) {
var style = document.createElement("style");
style.appendChild(document.createTextNode(css));
document.head.appendChild(style)
}
}
重新编译运行,页面可以正常显示。
通过上述方法即可解决 Snowpack 的打包项目中引用 @antv/l7 项目的问题。虽然可以正常使用,但代码还可以做些优化,首先是创建style标签的功能,既然原本使用的是 load-styles 项目,这里就直接把 load-styles 源码直接复制过来,这样与原本的代码就完全一致了;其次是错误提醒,全局 require 方法是为了解决 require('load-style') 问题的,因此出现其他引用库时要抛出异常提示。最终的代码版本,参看上开始的“解决办法”内容。
(虽然最终解决,但问题根源是 @antv/l7 代码的编译问题。而且这个破事,花了我5个小时)
参考文档
- Snowpack: https://www.snowpack.dev/
- AntV/L7: https://l7.antv.vision/zh
- Babel: https://babeljs.io/docs/en/babel-preset-env#targetsesmodules
- Babel Template: https://babeljs.io/docs/en/next/babel-template.html
- load-styles https://www.npmjs.com/package/load-styles