简介
因开发需要,需要在一个产库中开发多个前端 H5 项目,类似 monorepo ,这样便于项目间的管理和资源复用。
技术选型
melos
缺点:使用 melos 提供的命令去运行项目时, vscode 会失去命令行热更新(运行项目后,后续改动可按r可热更新项目),未在其他 IDE 中尝试。且 melos 是使用 shell 执行命令,作为熟悉 JavaScript 的前端同学需增加学习成本。
pnpm
基于以上考虑,最终选择使用 pnpm 。虽然 pnpm只是包管理工具,但它允许运行定义在 package.json 文件中的脚本,再通过参数传递即可达到想要的效果。
架构搭建
初始化 package.json
pnpm init
创建运行脚本
在根目录下创建 scripts 文件夹,在文件夹下创建 run.js 脚本。在 package.json 中的 scripts 下新增
"serve": "node scripts/run.js run"
表示执行run.js 脚本中的 run 命令。
修改 package.json 中 type 为 module
编写运行脚本
项目结构
├─lib
│ ├─project_1
│ │ ├─lib
│ │ │ └─main.dart
│ │ ├─web
│ │ └─pubspec.yaml
│ │
│ ├─project_2
│ │ ├─lib
│ │ │ └─main.dart
│ │ ├─web
│ │ └─pubspec.yaml
│ │
因 flutter run 命令无法指定 web 文件夹目录,所以每个项目都有自己的 web 目录
安装依赖
pnpm i commander -D
使用 commander 检查参数
import { program } from "commander";
program
.command("run")
.requiredOption("-p, --project <string>", "project name") // 项目名
.addOption(
new Option("-e, --env <string>", "dev or prod environment") // 运行的环境
.choices(["dev", "prod"])
.default("dev")
)
.addOption(
new Option("--web-renderer <string>", "web renderer mode") // 渲染方式
.choices(["auto", "html", "canvaskit"])
.default("auto")
)
.action((cmd) => {
run(cmd);
});
program.parse(process.argv);
需要传递其他参数的,自行添加。
执行对应项目
主要使用 spawn 执行 flutter run 命令,使用 readline 接受命令行输入并传递,解决 melos 缺点。
import path from "path";
import readline from "readline";
import { spawn } from "child_process";
/**
* @param {{ project: string, env: string, webRenderer: string }} args
*/
function run(args) {
const runPath = path.resolve(`./lib/${args.project}`);
// 使用 spawn 执行命令
const flutterProcess = spawn(
"fvm",
[
"flutter",
"run",
"-d",
"chrome",
`--dart-define=INIT_ENV=${args.env}`, // 设置项目环境
"--web-renderer",
args.webRenderer,
],
{
cwd: runPath, // 设置命令执行路径
}
);
// 监听标准输出
flutterProcess.stdout.on("data", (data) => {
console.log(data.toString());
});
// 监听错误输出
flutterProcess.stderr.on("data", (data) => {
console.error(data.toString());
});
// 接受命令行输入并传递给 flutter run 进程
const readlineInterface = readline
.createInterface(process.stdin, process.stdout)
.on("line", (line) => {
flutterProcess.stdin.write(line);
});
flutterProcess.on("close", () => {
readlineInterface.close();
});
}
其中 --dart-define=INIT_ENV= 参数用来设置环境变量,在项目中可以使用 String.fromEnvironment("INIT_ENV") 来获取环境变量,根据变量值去做不同的事情,如:区分开发环境和生产环境使用的 api 地址等。
完整代码
import path from "path";
import readline from "readline";
import { spawn } from "child_process";
import { program } from "commander";
program
.command("run")
.requiredOption("-p, --project <string>", "project name") // 项目名
.addOption(
new Option("-e, --env <string>", "dev or prod environment") // 运行的环境
.choices(["dev", "prod"])
.default("dev")
)
.addOption(
new Option("--web-renderer <string>", "web renderer mode") // 渲染方式
.choices(["auto", "html", "canvaskit"])
.default("auto")
)
.action((cmd) => {
run(cmd);
});
program.parse(process.argv);
/**
* @param {{ project: string, env: string, webRenderer: string }} args
*/
function run(args) {
const runPath = path.resolve(`./lib/${args.project}`);
// 使用 spawn 执行命令
const flutterProcess = spawn(
"fvm",
[
"flutter",
"run",
"-d",
"chrome",
`--dart-define=INIT_ENV=${args.env}`, // 设置项目环境
"--web-renderer",
args.webRenderer,
],
{
cwd: runPath, // 设置命令执行路径
}
);
// 监听标准输出
flutterProcess.stdout.on("data", (data) => {
console.log(data.toString());
});
// 监听错误输出
flutterProcess.stderr.on("data", (data) => {
console.error(data.toString());
});
// 接受命令行输入并传递给 flutter run 进程
const readlineInterface = readline
.createInterface(process.stdin, process.stdout)
.on("line", (line) => {
flutterProcess.stdin.write(line);
});
flutterProcess.on("close", () => {
readlineInterface.close();
});
}
以后只要执行
npm run serve -- --project xxx
即可运行对应项目
编写打包脚本
在 package.json 的 scirpt 中添加 "build": "node scripts/build.js build"
脚本所需参数与运行脚本类似
import path from "path";
import { exec } from "child_process";
import { program, Option } from "commander";
program
.command("build")
.requiredOption("-p, --project <string>", "project name") // 要打包的项目名
.addOption(
new Option("-e, --env <string>", "dev or prod environment") // 运行的环境
.choices(["dev", "prod"])
.default("dev")
)
.addOption(
new Option("--web-renderer <string>", "web renderer mode") // 渲染方式
.choices(["auto", "html", "canvaskit"])
.default("auto")
)
.action((cmd) => {
build(cmd);
});
program.parse(process.argv);
/**
* @param {{ project: string, env: string, webRenderer: string }} args
*/
function build(args) {
// 要打包的项目路劲
const buildTargetPath = path.resolve(`./lib/${args.project}`);
// 打包文件输出位置,如:build/dev/project_1
const buildOutPath = path.resolve(`./build/${args.env}/${args.project}`);
// 见下方解释,具体根据部署路劲设置
const baseHref = `/${args.project}/`;
// 打包命令 -o 指定输出位置
// --release 构建发布版本,有对代码进行混淆压缩等优化
// --pwa-strategy none 不使用 pwa
const commandStr = `fvm flutter build web --base-href ${baseHref} --web-renderer ${args.webRenderer} --release --pwa-strategy none -o ${buildOutPath} --dart-define=INIT_ENV=${args.env} `;
exec(
commandStr,
{
cwd: buildTargetPath,
},
async (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return;
}
console.log(`stdout: ${stdout}`);
if (stderr) {
console.error(`stderr: ${stderr}`);
return;
}
}
);
}
--base-href
运行 flutter build web -h 可以查看 --base-href 官方解释:
Overrides the href attribute of the <base> tag in web/index.html.
No change is done to web/index.html file if this flag is not provided.
The value has to start and end with a slash "/".
For more information: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
中文:
覆盖web/index.html中<base>标签的href属性。
如果不提供此标志,则不会对web/index.html文件进行更改。
该值必须以斜杠“/”开始和结束。
更多信息请访问:https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
在打包产物 index.html 中,flutter.js 的 script 标签为:<script src="flutter.js" defer></script>
如果 project_1 的访问链接为:http://www.example.com/project_1/index.html ,且不设置 --base-href 时,当访问 project_1 链接时, flutter.js 的访问路径为 http://www.example.com/flutter.js 这样是错误的。
所以需要设置 --base-href 为 /project_1/,这样 flutter.js 的访问路径才会变为 http://www.example.com/project_1/flutter.js
打包优化见后续文章