[打包] 替换打包产物内容
一个简单的小需求,在打包出的 js 文件里通过正则替换一些内容。这个需求相对简单,不需要 loader,可以用 webpack plugin API 的 compliation hooks 来做。
在 processAssets
hook 中使用 compilation.getAssets()
获取到产物列表,针对产物进行处理即可。以下是一个例子,替换掉产物中的 AMD 判断代码 (typeof define == 'function'
)。
const amdDetectRegex = /typeof\sdefine\s(=)+\s["']function["']/g;
class RemoveAMDWebpackPlugin {
apply(compiler) {
compiler.hooks.compilation.tap('RemoveAMDWebpackPlugin', (compilation) => {
compilation.hooks.processAssets.tapPromise(
{
name: 'RemoveAMDWebpackPlugin',
stage: compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
},
async () => {
const assets = compilation.getAssets();
for (const asset of assets) {
const assetName = asset.name;
const assetContent = asset.source.source();
if (assetName.endsWith('.js') && assetContent.match(amdDetectRegex)) {
const newContent = assetContent.replace(amdDetectRegex, 'false');
compilation.updateAsset(assetName, {
source: () => newContent,
size: () => newContent.length,
});
}
}
}
);
});
}
}
module.exports = RemoveAMDWebpackPlugin;
[Monorepo] Monorepo 源码引用配置 TailwindCSS
背景
基于开发效率的考虑,准备给我们的 React 客户端项目接入 TailwindCSS.
目前我们项目是以 monorepo 的形式维护,组织形式是一个主应用 + 多个领域包,主应用通过源码引用子包 src/index.ts
导出的内容、使用子包导出的组件等,子应用不进行任何打包操作,在主应用配置好 Rspack 统一打包。原以为装上依赖,配好 tailwind.config.ts
,再给 Rspack 装上 postcss 就可以了,但是按照官方流程走完一套下来简单写了几个 utility classes 测试了一下,发现有的部分样式正常,而有的地方没有样式。
只在主应用配置 tailwind
遂开始排查,发现只有写在主应用里的 utility class 生效了,子包里的都没有生效,便怀疑 tailwind config 中的 content
没有生效。官方只配置了 ./src/**/*.{html,js,ts,tsx}
,所以 postcss 只处理了主应用 src 目录下的源文件,忽略了子包中的源文件。
问题来了,子包中也有 utility class 的情况下,这个 content
怎么写呢。查了一下 GitHub Issues,告诉我应该这样写:
content: [
'./src/**/*.{html,js,ts,tsx}',
'./node_modules/@scope/**/*.{html,js,ts,tsx}',
]
试了一下,果然正常地扫描到子包的 utility class 并且正常打包了样式。这个时候,不出意外的话就要出意外了,接下来发现的第二个问题是,修改主应用和子包的组件,HMR 把组件更新了,但是新的样式没被热更上去。一番排查发现是热更上来的 CSS 文件似乎没有成功应用。
Tailwind HMR 不生效的排查
看起来似乎是 Rspack 的问题?尝试更新了一波 Rspack 和 HMR 的版本,还是一样的问题。但是创建了一份干净的模版,tailwind 是能正常热更的。
经过一番排除法,最终发现是 output
中设置了自定义的文件名生成策略。我们的项目 output
配置是这样的:
output: {
filename: '[name].[contenthash:8].js',
path: path.resolve(baseRootPath, './dist'),
},
把它去掉之后就能够正常热更新了。一脸懵逼,为啥 HMR 会和这玩意儿有关系。这个时候才发现 Rsbuild 的文档里有说明,如果输出文件中存在 hash 可能会导致热更时一些插件无法读取到最新的内容:https://rsbuild.dev/zh/guide/faq/hmr
在开发模式下把文件名的 contenthash 去掉,这回热更貌似正常了:
output: {
filename: process.env.NODE_ENV === 'production' ? '[name].[contenthash:8].js' : '[name].js',
path: path.resolve(baseRootPath, './dist'),
},
正确配置 content 包含源码路径的方法
正觉得大功告成的时候,突然发现主应用的热更是正常了,可是修改子应用的组件还是不能热更,热更的 CSS 里也没带上子应用里新增的 utility class. 猜测这是个在 monorepo 底下独有的问题,这回在网上搜了半天也没找到是什么原因,盲猜了一波 node_modules
底下的文件是不是会被热更新忽略掉。顶着怀疑重新修改了一下 tailwind.config.ts
的 content
,把 node_modules
下的引用改成了相对路径引用:
content: [
'./src/**/*.{html,js,ts,tsx}',
'../packages/**/src/**/*.{html,js,ts,tsx}',
]
修改成绝对路径引用之后,修改子包的 utility class 也能够触发 HMR 了。
减少构建耗时影响的小 trick
但是,这样又双叒带来了一个问题,那就是 ../packages/**/src/**/*.{html,js,ts,tsx}
这么配置路径让构建变慢了!!原来只需要 20 秒冷启动构建、300ms HMR,加上这个路径匹配直接拖慢到了 1min cold start / 2s HMR. 这我能忍?
我猜是因为路径里的 **
通配符太多了导致的,于是决定干掉一层,手动声明一下需要包含的目录:
// not using ** to resolve sources to improve build / HMR performance (4s -> 200ms)
function getTailwindContentIncludes() {
const appDirectory = resolve(__dirname, '../');
const packagesDirectory = resolve(__dirname, '../../packages');
const getSubDirectoryIncludes = (path: string) => {
const packages = readdirSync(path);
return packages.map(
(pkgDir) =>
`${relative(__dirname, join(path, pkgDir, 'src')).replace(/\\/g, '/')}/**/*.{html,ts,tsx,js,jsx}`
);
};
return [
...getSubDirectoryIncludes(appDirectory),
...getSubDirectoryIncludes(join(packagesDirectory, 'domains')),
...getSubDirectoryIncludes(join(packagesDirectory, 'shared')),
];
}
const config: Config = {
content: getTailwindContentIncludes(),
// ...
};
干掉了一层 **
,手动写扫描路径,把引用路径限制到当前项目的 src/
和几个特定的目录下 src/
。这样改完之后速度果然有很大的提升,HMR 也回到了 400ms。考虑到引入了 postcss 肯定会有一些性能损耗,虽然有一些增长但是仍然在 ms 级别可以接受。(所以 tailwind 什么时候可以用上 lightingcss?)
[Monorepo] rush update-autoinstaller 无法创建 shrinkwrap 文件
问题: 在把 monorepo 迁移到 Rush 、配置 command-line.json
的过程中,通过 rush init-autoinstaller
创建 autoinstaller 、更新 package.json
之后,执行 rush update-autoinstaller
使用 pnpm 更新依赖,报错 The package manager did not create the expected shrinkwrap file.
原因: 需要把 monorepo 根目录下的 pnpm-workspace.yaml
删除,保留这个文件会导致 pnpm 不会在 autoinstaller 的目录下产生 pnpm-lock.yaml. Rush 自带了 pnpm workspace 的支持,不需要再使用 pnpm-workspace.yaml
声明 packages 了。
[安全] 关于 iframe sandbox 和 Content-Security-Policy
今天遇到一个用户提的 oncall,用户在我们的系统底下嵌入了一个 iframe,iframe 内容是用户公司自建的页面。用户的诉求是从子页面拿到顶层页面 (iframe 宿主) 的 URL,然后遇到了跨域问题,用户提问是否可以配置 iframe 属性或者配置 CSP 绕过,把我问住了。
我们来尝试复现一下用户的场景,用户是这样做的:
http://localhost:8080
<html>
<head>
<title>Parent Page</title>
</head>
<body>
<iframe src="http://localhost:8081/" />
</body>
</html>
http://localhost:8081
<html>
<head>
<title>Child Page</title>
</head>
<body>
<script>
console.log(window.top.location.href);
</script>
</body>
</html>
不出意外就要出意外地报错了:
Uncaught SecurityError: Failed to read a named property 'href' from 'Location': Blocked a frame with origin "http://localhost:8081" from accessing a cross-origin frame.
很明显问题是出在了 window.top.location.href
的读取(跨源访问)被浏览器的同源安全策略拦截了。根据 MDN 上对 浏览器同源策略 的描述,大部分属性的跨源访问是只读的,唯独对 Location
对象的 href
属性是只写的(实际上大部分的 “跨源读操作” 一般是不被允许的):
同样地从父页面访问子页面的 window 和 Location 对象也是受限的,这种情况下设置 Access-Control-Allow-Origin
也不好使。一般来说这种情况最好的办法是直接通过 iframe src 设置 URL query 来传递父页面 URL,或者需要传递复杂信息的时候使用 window.postMessage
.
问题其实到这里解决了,但是用户一开始提出的方案 “配置 iframe 属性或者配置 CSP 绕过” 是否有可行性呢。
iframe sandbox
先说说是否能配置 iframe 的属性来绕过一些浏览器的安全限制。iframe 有一个 sandbox
属性,允许开发者限制 iframe 中页面的行为,如执行 JS、下载文件、提交表单、弹窗等,也可以强制浏览器将一个同源的 iframe 页面视为非同源页面。
但实际上我们直接使用 <iframe src="xxx" />
这样来嵌入一个 iframe 的时候,不会应用任何限制,所以其实没有必要特地指定属性,对 iframe 和浏览器来说同源就是同源,非同源就是非同源,指定 sandbox
的 allow-scripts
或 allow-same-origin
限制并不能改变这一点。
Content Security Policy
再说说内容安全策略 (Content Security Policy, CSP)。 其实今天之前我也不知道 CSP 具体有啥用,所以用户提到这个的时候我也有些懵逼。
简而言之,CSP 主要是控制浏览器可以为该页面获取哪些资源,一般我们可以在返回的响应头或者 meta 中设置 CSP:
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; img-src https://*; child-src 'none';" />
CSP 通过特定的 DSL 来描述访问限制的策略,格式是 key-src domain1 [domain2] ... ;
,更多示例可以看 MDN.
- 必须包含一个
default-src
default-src 'self'
表示当前页面引用的所有内容,都只能来自当前页面所在的域名(子域名也不行)- 可以使用通配符,指定一个白名单,信任除当前页面域名外的某个域名及其子域名:
default-src 'self' *.trusted.com
- 通过
img-src
限制图片引用、media-src
限制媒体文件引用、script-src
限制脚本引用:default-src 'self'; img-src *; media-src media1.com media2.com; script-src userscripts.example.com
- 利用 CSP 也可以用来做其它的限制,例如强制使用来自 SSL 的资源:
default-src https://xxx.com
CSP 还可以设置为警告模式,并且还能够通过 report-uri
来将违反 CSP 的行为报告给服务器。
总之 CSP 是个比较冷门的功能,可以用 CSP 来减少 XSS 攻击的发生。同样,默认不设置 CSP 的时候,其实也是采取了最宽松的引用限制的。所以它能解决用户的问题吗?答案也是不能。