前段时间研究了几个比较常用的浏览器翻译源以后,我写了 translator-utils 的 最初版本 ,后来打算将它从大的 monorepo 拆分出来时,麻烦就来了
引入依赖
上一篇文章
我也提到过,有一部分的翻译源的链接是允许跨域的,所以在把这玩意拆分出来的同时我准备打一个 umd bundle 出来给浏览器直接引入。然而,我使用的 Axios 并没有自带用于代理请求的 Http(s) Agent,于是我用了
hpagent
模块,而这个模块使用的 Agent 是 Node.js 的
http
/
https
模块独有的,因此并不能直接用于浏览器环境,这时就需要想办法绕过。
动态导入❌
在看完 前人留下的文章 后,我先想到的是用动态引入的办法绕过,于是有了第一版:
...
//为什么要套一个async函数,因为UMD不支持 top level await 啊
const axiosConfig = async () => {
if (typeof process !== 'undefined') {
const https_proxy = process.env.https_proxy ?? process.env.HTTPS_PROXY ?? ''
const http_proxy = process.env.http_proxy ?? process.env.HTTP_PROXY ?? ''
const { HttpProxyAgent, HttpsProxyAgent } = await import('hpagent')
axios.create({
httpsAgent: HttpsProxyAgent({proxy: https_proxy})
httpAgent: HttpProxyAgent({proxy: https_proxy})
显而易见地,这种操作本身没啥问题,放到一个新建的 vite 工程里也能用,然而到了打UMD包的时候就会提示缺少依赖,并且正式丢进浏览器中后就会发现完全不能用,所以需要换一种思路
(!) Missing shims for Node.js built-ins Creating a browser bundle that depends on "https", "http" and "url". You might need to include https://github.com/FredKSchott/rollup-plugin-polyfill-node
另外在研究这种操作时我看到了 一条issue
导入空 Object ✔
反复翻阅 Rollup 的文档后,我发现可以通过
external
先将
haproxy
变成一个从外部引入的模块,再通过
output.globals
为
haproxy
设置一个默认值,再加点判断条件,那么就得到了第二个版本:
//rollup.config.js
export default [{
input: 'src/index.browser.ts',
output: {
file: 'dist/translator.js',
format: 'umd',
sourcemap: true,
name: "translator",
exports: "default",
globals: {
hpagent: '{}'
external: ['hpagent'],
plugins: [
commonjs(),
typescript({tsconfig: './tsconfig.rollup.json'}),
resolve({browser: true}),
peerDepsExternal('./package.json'),
babel({
babelHelpers: 'bundled',
presets: ['@babel/preset-env']
//axios.config.js
import { HttpProxyAgent, HttpsProxyAgent } from "hpagent"
const axiosConfig = (config) => {
if (typeof process !== 'undefined' && HttpProxyAgent && HttpsProxyAgent) {
//这里面跟上一版基本一致
虽然确实能用,但这玩意属于奇技淫巧……不能称作优雅
Workspaces ✔✔
Workspaces
是 npm 7.x+ / Yarn 支持的功能,可以用来写自己的模块,而npm引入模块时是可以根据
package.json
的键
exports
区分入口的,于是我搞了第三版:
//package.json
"workspaces": [
"packages/*"
//packages/translator-utils-axios-helper/package.json
"name": "translator-utils-axios-helper",
"version": "0.0.1",
"private": true,
"main": "index.node.js",
"types": "index.node.d.ts",
"exports": {
".": {
"browser": {
"default": "./index.js"
"default": {
"default": "./index.node.js"
"./package.json": "./package.json"
"type": "module"
//packages/translator-utils-axios-helper/index.js
import axios from 'axios'
const axiosFetch = axios.create({})
export default axiosFetch
//packages/translator-utils-axios-helper/index.node.js
import axios from 'axios'
import { HttpsProxyAgent } from "hpagent"
const axiosFetch = axios.create({ httpsAgent = new HttpsProxyAgent({proxy: HTTPS_PROXY}) })
export default axiosFetch
//other scripts
import axiosFetch from 'translator-utils-axios-helper'
这下就彻底分了家,后面可以通过插件
@rollup/plugin-node-resolve
来区分入口
运行环境
这就完了吗,那确实还没有,经过一番操作我彻底删掉了axios,然后麻烦又来了:不同的运行环境都会有一些独有的方法,这时候就需要挨个适配了
node:https
既然去掉了axios,那后续的操作自然就是给原生的https库套Promise,这没啥好说的,也就只有
data
事件并非一次性读完全部内容可能会被忽略掉(也只有我这种没接触过的会被坑了吧)
new Promise((resolve, reject) => {
const req = https.request(url, options, (res) => {
let chunkArray = []
res.on('data', (data) => {
chunkArray.push(data)
res.on('close', () => {
resolve(Buffer.concat(chunkArray))
req.on('error', (e) => {
reject(e)
req.end()
fetch
浏览器/Workers/Deno/Node.js 18.x 所自带的
fetch()
都自带一系列的转换方法,不需要像
node:https
那样手操
Buffer
(想硬干
也不是不行
),然而 fetch 看似轻松简单,实际暗藏玄机……平时在浏览器经常用 fetch,但很少用到headers,于是也忽略了这玩意
正好这次需要拿cookie,而
Set-Cookie
是一个比较特殊的存在,是唯一允许重复使用的标头,而
fetch.headers
的类型
Headers
本质上就是个
Map
,因此会常规的
fetch.headers.get('set-cookie')
只会得到第一个
set-cookie
的值,因此不同的环境都做出了自己的解决方案
-
浏览器:
根本没有!
所以不需要管,我直接摘抄MDN的描述:
警告: 根据 Fetch 规范,Set-Cookie 是一个 禁止的响应标头 ,对应的响应在被暴露给前端代码前, 必须滤除 这一响应标头,即浏览器会阻止前端 JavaScript 代码访问 Set-Cookie 标头。
-
Workers:CloudFlare给
set-cookie
一个专有的方法.getAll()
,将会返回一个数组;我在翻issue过程中发现 github/fetch #236 也是这样操作的const res = await fetch('https://www.google.com') res.headers.getAll('set-cookie') res.headers.getAll('content-type') //Uncaught (in promise) TypeError: getAll() can only be used with the header name "Set-Cookie".
-
Deno:Deno 通过合并
<Merged> All multiple Set-Cookie headers #5100
终结了这个问题
const res = await fetch('https://www.google.com') res.headers.get('set-cookie') //"1P_JAR=2023-03-30-09; expires=Sat, 29-Apr-2023 09:42:39 GMT; path=/; domain=.google.com; Secure, AEC=AUEFqZdWR3Fxxxxxxxxxl6U1vCxxxxSeb6VhSYLLrQ5jzo--xxxg9Hak_g; expires=Tue, 26-Sep-2023 09:42:39 GMT; path=/; domain=.google.com; Secure; HttpOnly; SameSite=lax, NID=511=Q_yBq0zEyZXlxxxxxcmbHsMPEVo6ytDJCixxxxx_G-nNOtJ-FvnznDxxxxx5ey-WCFP7_ye4DUW_CLC1ok3xxxxxdQ9KXLPh7q7o_bHDQI83sioqoDTPubYdUrLKKiie5L4icxxxxxjUk_hcZNK3bOvr3jeJG3xxxxxAzz_wLI; expires=Fri, 29-Sep-2023 09:42:39 GMT; path=/; domain=.google.com; HttpOnly" [...res.headers.entries()].filter(header => header[0] === 'set-cookie').map(header => header[1]) // "1P_JAR=2023-03-30-09; expires=Sat, 29-Apr-2023 09:42:39 GMT; path=/; domain=.google.com; Secure", // "AEC=AUEFqZdWR3Fxxxxxxxxxl6U1vCxxxxSeb6VhSYLLrQ5jzo--xxxg9Hak_g; expires=Tue, 26-Sep-2023 09:42:39 GM...", // "NID=511=Q_yBq0zEyZXlxxxxxcmbHsMPEVo6ytDJCixxxxx_G-nNOtJ-FvnznDxxxxx5ey-WCFP7_ye4DUW_CLC1ok3xxxxxdQ9..."
- Node.js:用法同上
- Bun:同上
上面说了一大堆,总结起来就是除了 Workers 其他环境都是通用的,因此最终我处理这部分的代码就变成
let headers = Object.fromEntries(res.headers.entries())
if (headers['set-cookie'] && res.headers.getAll) {