大家好,我是苏先生,一名热爱钻研、乐于分享的前端工程师,跟大家分享一句我很喜欢的话:人活着,其实就是一种心态,你若觉得快乐,幸福便无处不在
github与好文
背景
为什么直到现在才做这个需求,这是因为项目一直都是没有启用webpack-dev-server的自动打开浏览器功能的,直到我后来对webpack4升级到webpack5之后才开始启用该属性
直到前两天,突然发现启用了open后每次都会重新开一个浏览器tab,虽说不是不能用,但总觉得别扭,于是就在想能不能让它复用一下子,如果页面中已经有打开的url,那下次就复用这个tab就好了
技术分析
首先,打开浏览器复用tab的能力一定是可以做到的,因为vite和cra里我都见过,但是我们项目有两个地方是比较特殊的:
-
我们真正打开使用的地址实际上来自于在hosts文件中配置的映射关系,因此如何取hosts文件,如果找到映射关系
-
如何接管webpack-dev-server自身的open行为,比如假如某个项目是不需要设置hosts时,是否能回退到webpack-dev-server
-
当配置的hosts的映射关系为一对多时,是否会报错,报错后应该怎么处理,是否需要逐个尝试
-
有没有可能全部都出错,此时应该如何降级
源码地址
代码实现
首先,我们做一个webpack plugin的基础架子,它是一个包含apply接口的固定写法的class
ts
class OpenBrowser {
apply(){}
}
第二步来考虑参数
- port
端口我们其实是可以复用webpack-dev-server中的端口的,但是为了更加保险一点,我们也让用户传递一个进来
- opened
我们当前的webpack项目是采用的lazy模式,每次访问新的页面都会重新compiler,这就意味着buildStart会被多次触发,因此需要一个标记来记录是否已经打开过,如果为true就直接return
- address
我们将从hosts查找到的域名丢给用户一份,给它一个可检查、可修改的时机,因此它应当是一个函数
- fallback
我们的项目是介入了单点的,登录页是另外一个项目,它的域名地址是跟将来我们要检测浏览器tab里是否已经存在的目标地址是不一样的,因此我这里搞了个降级,即默认查不到,就尝试查找这个
接着,接管webpack-dev-server的默认open行为,并设置buildStart hook
ts
// 将webpack-dev-server的open删除即可
delete compiler.options.devServer.open;
// 设置buildStart hook
compiler.hooks.emit.tapPromise(PLUGINNAME, async () => {
// 仅在首次打开浏览器
if(!this.opened){
// TODO:validate it
urls = urls.map((u) => u.replace(port, realPort));
// 真正的打开方法
await _openBrowser(urls, this.fallback);
}
this.opened = true;
});
现在,进入_openBrowser函数,该函数由两部分组成,一是调用applescript尝试打开浏览器,如果applescript打不开就降级使用javascript脚本打开
如下,考虑到hosts中一对多的情况,这里搞了个数组并依次去执行,当浏览器页面中没有已经打开的tab时applescript脚本执行会报错,当报错时,表示当前已经尝试过一个,我们停掉当前的for循环并递归调用_openBrowser进行下一轮,如果全部的都尝试过并且都报错了,则调用openByJs来打开
ts
async function _openBrowser(urls, fallback) {
// 获取进程信息
const ps = await execAsync("ps cax");
// 查找进程中是否包含指定的浏览器
const legalBrowser = AIMBROWSERS.find((b) => ps.includes(b));
if (legalBrowser) {
let isBreak = false
// 根据地址依次尝试打开
for (let i = 0; i < urls.length && !isBreak; i++) {
const openStatus = await tryOpen({
url: urls[i],
fallback,
legalBrowser,
onError:()=>{
// 执行错误时停止当前for并记录进errs
isBreak = true
const act = urls[i]
urls.splice(i,1)
setTimeout(()=>{
errs.push(act)
_openBrowser(urls,fallback)
},100)
}
});
// 打开了就跳出循环,程序结束
if (openStatus === true) {
tryTimes = -1
errs.length = 0
break;
}
}
if(tryTimes === errs.length){
// 降级到js执行打开
openByJs(errs,legalBrowser)
}
}
}
applescript脚本可以通过exec来进行调用,这里我们传递三个参数:要检查的url地址、要打开浏览器、如果要检测的url没有找到,则需要降级查找的url
ts
async function tryOpen(payload) {
const { url, fallback, legalBrowser,onError } = payload;
try {
await execAsync(
`osascript openChrome.applescript "${encodeURI(url)}" "${legalBrowser}" "${fallback}"`,
{
cwd: resolve(__dirname, ".."),
}
);
} catch (_) {
onError()
}
}
最后,降级到js打开一个新的,这种情况一般是浏览器中一个相关的都没有打开,否则,但凡有一个,就不会调用到这里的
ts
function openByJs(urls, legalBrowser) {
try {
const cliArguments = ["-a", legalBrowser, urls[urls.length - 1]];
spawn("open", cliArguments);
} catch (_) {}
}
至于applescript脚本,它copy自cra,我只是在它的基础上加了一个fallback以支持降级查找,既然不是我写的,那我就不贴出来了
最后是关于hosts的获取,这也分成了三步
- 获取本地可用的ipv4地址,并将符合条件的加入数组并最终返回
ts
function getIps() {
const interfaces = networkInterfaces();
const ips = [];
for (const name in interfaces) {
const networkInterface = interfaces[name];
for (const network of networkInterface) {
if (network.family === "IPv4" && !network.internal) {
ips.push(network.address);
}
}
}
return ips;
}
- 获取本地hosts文件,每一个ip对应的域名都是一个数组形式,因为要兼容一对多的情况
ts
function parseHosts() {
const mappings = {};
if (existsSync(HOSTSPATH)) {
try {
const code = readFileSync(HOSTSPATH, "utf-8");
const lines = code.split("\n");
for (let line of lines) {
line = line.trim();
if (line.startsWith("#") || line === "") {
continue;
}
const tokens = line.split(/\s+/);
const ip = tokens[0];
for (let i = 1; i < tokens.length; i++) {
const domain = tokens[i];
if (!mappings[ip]) {
mappings[ip] = [];
}
mappings[ip].push(domain);
}
}
} catch (_) {}
}
return mappings;
}
- 最后对两者进行合并即可
ts
for (let i = 0; i < ips.length; i++) {
const hosts = mapping[ips[i]];
if (Array.isArray(hosts) && hosts.length) {
...
}
}
使用
- 安装
ts
yarn add open-browser-for-webpack-mac -D
- 使用
ts
const OpenBrowser = require('open-browser-for-webpack-mac');
new OpenBrowser({
port: 9090,
address: (host, port) => `http://${host}:${port}`,
fallback: 'http://sso.test.weidiango.com',
})
如果本文对您有用,希望能得到您的点赞和收藏
订阅专栏,每周更新1-2篇类型体操,等你哟😎