美团实习-参与组内基建的一个小需求

60 阅读6分钟

本文为作者在团子写的一个小的开发体验优化需求,截取片段

1 背景与目标

1.1 背景

商户服务需要对外提供 h5 / pc 等多种形态的页面,对应页面为 /pc.html 、/h5.html 等,但目前 yarn serve 时只会干巴巴的展示 127.0.0.1:8080,点击了又访问不了;同时页面访问时可能需要若干 query 参数,需要手动拼接等,影响开发体验。

目前有些服务需要口口相传、手动拼接;account 仓库也有不同方案,心智不统一:

  1. xx1 packages/xx1 ,放在 readme
  2. xx2 packages/xx2,根目录也会作为开发引导,暂时可开发访问的链接
  3. xx3 packages/xx3,需访问 127.0.0.1:8080/dev.html(根路径会作为 pc/h5 自适应的地址)

1.2 目标

yarn serve 执行成功后直接输出可访问的地址,如 127.0.0.1:8080/dev.html 等,快速进入开发阶段。

2 整体方案

2.1 如何去指明开发入口

方案说明选择备注
方案一:维护一个readme在每个服务下维护一个readme,作为文档及指导开发方式(比如入口在哪,需要怎么配置)
并且作为开发者,会自然的查看仓库下的readme,readme传递的信息比较丰富且有效。
方案二:对vue-cli打印的日志进行修改在vue-cli的devServer打印的日志上做修改,修改running at的url或者在其之前之后打印一些提示信息


打印日志的方式最为直接且方便,但是在实际项目中,打印的日志信息很可能被覆盖,对url的修改可能有侵入性,对代码维护可能不友好。
方案三:自动打开浏览器并指向对应的开发入口不做提示,只通过devServer的打开浏览器操作打开到正确的路径。
修改量最小,也能有效的引导到正确的路径,但是缺乏信息。

综合考虑,可以选择方案二。

2.2 怎么在控制台打印入口

在上述整体方案选择为方案二的同时,方案二也有几种实现方法

方案说明选择备注
实现一:在vue-cli输出之下添加自行拼接的url

通过获取开发服务器的协议(http/https)、host、端口、publicPath,并拼接上开发入口的路径,形成最终完整的路径。
实现二:使用子父进程监听vue-cli的输出并修改通过正则匹配到了url位置,但是其他的输出内容也有变化,比如颜色显示没有了。而且使用父进程控制yarn serve也可能引起一些未知的问题,在这里不推荐。

2.3 如何实现

背景说明: @mtfe/mwallet-build 是团队中构建配置的 npm 包,提供 Webpack 的诸多配置,其中 handleDevServer 为开发服务器的配置文件,配置了 Mock 代理、或者泳道代理等等

在启动开发服务器时打印信息,本质利用的是 Webpack 的 完成时 生命周期,使用 compiler.hooks.done 即可注册一个钩子,每当服务器启动时,或者热更新时,会触发钩子执行。

在生命周期操作,正常是需要使用插件实现的,本着最小改造量的目的,想到 devServerbeforeafter只在 webpack4中有效,webpack5中需要使用 setupMiddlewares 替代)也可以获取到 compiler对象进行操作

通过修改devServer在webpack dev server的各个时期去获取url的部分,并最终拼接成为开发入口,即可达到本次改造的目标

  1. 获取URL URL由protocol、host、port和publicPath组成,其中只有port是非指定的,可以是自动寻找的端口 因此获取port稍微复杂,查询webpack dev server的文档可以知道通过onListening函数拿到server实例 就可以获取 port(也仅为 webapck4 ),最终拼接 url 即可。

  2. 在 vue-cli 打印的信息之后打印 URL 并且在热更新时也打印 compiler下的 done hook 回调,可以实现在每次编译完成之后执行一段函数,就可以在这个函数中打印URL。 整体流程如下,在webpack dev server生命周期的各个部分,可以拿到不同的数据,最终拼接url。

注:本需求仅适配 webpack4

2.4 Webpack5的方法

before 注册钩子和 onListening 获取端口仅在 Webpack4 生效,Webpack5 可以参考下面方法

module.exports = defineConfig({
	transpileDependencies: true,
	configureWebpack: {
		devServer: {
			port: 7890,
			setupMiddlewares(middlewares, devServer) {
				if (!devServer) {
					throw new Error("webpack-dev-server is not defined");
				}

				devServer.compiler.hooks.done.tap("printEntry", () => {
					console.log("编译完成", devServer.compiler.options.devServer?.port ?? 8080);
				});
				return middlewares;
			},
		},
	},
});

3. 拓展

3.1 Vite 生命周期和钩子

阶段钩子名称类型触发时机参数
配置阶段config同步/异步在 Vite 配置加载之前(config, { command, mode })
configResolved同步配置解析完成后(resolvedConfig)
优化阶段buildStart同步/异步构建开始时()
resolveId同步/异步模块解析时(source, importer, options)
load同步/异步模块加载时(id)
transform同步/异步模块转换时,如编译 TypeScript、处理 JSX(code, id)
handleHotUpdate异步处理热更新(HMR)时({ file, server, modules })
开发服务器阶段configureServer异步配置开发服务器时,添加中间件或路由(server)
HTML 转换阶段transformIndexHtml同步/异步转换 index.html 时,修改或注入内容(html, ctx)
构建阶段generateBundle同步/异步生成最终包之前(options, bundle, isWrite)
writeBundle同步/异步所有文件写入磁盘后()
closeBundle同步/异步构建完成后,适合做清理工作()

3.2 Webpack 生命周期和钩子

阶段钩子名称类型触发时机参数
初始化阶段environment同步准备编译环境时-
afterEnvironment同步环境准备完成后-
entryOption同步entry 配置项处理后(context, entry)
afterPlugins同步插件注册完成后(compiler)
afterResolvers同步resolver 安装完成后(compiler)
编译阶段beforeRun异步开始正式编译前(compiler)
run异步开始编译时(compiler)
watchRun异步监听模式下,开始编译时(compiler)
beforeCompile异步compilation 参数创建后(compilationParams)
compile同步一次 compilation 创建前(compilationParams)
compilation同步compilation 创建后(compilation, compilationParams)
make异步从 entry 开始递归分析依赖时(compilation)
构建阶段afterCompile异步完成构建、封存完成后(compilation)
shouldEmit同步是否输出文件(compilation)
emit异步输出 assets 到 output 目录之前(compilation)
afterEmit异步输出 assets 到 output 目录之后(compilation)
完成阶段done同步编译完成(stats)
failed同步编译失败(error)
监听相关watchClose同步监听模式停止-
infrastructureLog同步日志输出时(name, type, args)
补充说明:
  1. 钩子类型:
  2. 同步钩子:使用 tap() 方法注册
  3. 异步钩子:可使用 tapAsync () 或 tapPromise () 方法注册

3.3 Vite 如何实现

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

function PreStartLoggerPlugin() {
	return {
		name: "pre-start-logger",
		config() {
			console.log("🔧 准备启动 Vite 开发服务器...");
		},
		configResolved(config) {
			console.log(`🛠 当前工作目录: ${config.root}`);
			console.log(`📂 输出目录: ${config.build.outDir}`);
		},
		configureServer(server) {
			server.httpServer?.once("listening", () => {
				const address = server.httpServer?.address();
				if (address && typeof address === "object") {
					console.log(
						`🚀 开发服务器已启动,访问地址: http://localhost:${address.port}`
					);
				}
			});
		},
	};
}

// https://vite.dev/config/
export default defineConfig({
  plugins: [vue(),PreStartLoggerPlugin()],
})

3.4 Webpack 插件实现打印

const { defineConfig } = require("@vue/cli-service");

class PreStartLoggerPlugin {
	apply(compiler) {
		compiler.hooks.watchRun.tap("PreStartLoggerPlugin", (compiler) => {
			console.log("🔧 准备启动 Webpack 开发服务器...");
			console.log(`🛠 当前工作目录: ${process.cwd()}`);
			console.log(`📂 输出目录: ${compiler.options.output.path}`);
		});
    // 主要依靠这个hook实现打印效果
		compiler.hooks.done.tap("PreStartLoggerPlugin", (stats) => {
			const devServer = compiler.options.devServer;
			const port = devServer?.port || 8080;
			console.log(`🚀 开发服务器已启动,访问地址: http://localhost:${port}`);
		});
	}
}

module.exports = defineConfig({
	transpileDependencies: true,
	configureWebpack: {
		plugins: [new PreStartLoggerPlugin()],
    devServer: {
      port: 7890
    }
	},
});