Skip to main content

快速上手

本地使用

可以在项目中使用以下命令安装。

pnpm i @lcap/nasl-unified-frontend-generator

游乐场

上手NASL前端翻译器最快的办法就是使用游乐场(nasl-frontend-generator-playground):一边自定义编译过程,一边查看它对样例应用翻译出来的制品的影响。

配方

列举一些常见需求的实现方法:

自定义目录结构

ProjectOrganizerPlugin中,存在着一个moveFilesAsYouWant方法。这个方法会在项目布局阶段被调用。

这个方法的默认实现会将src/__views__文件夹下的文件统一移动到src/pages文件夹下。

你可以通过直接修改ProjectOrganizerPlugin插件的原型来覆写此方法,将翻译器生成出来的文件移动到你想要的位置。

例1:将src/__views__文件夹下的所有文件都移动到src/views
ProjectOrganizerPlugin.prototype.moveFilesAsYouWant = async function (placedFiles: PlacedFile[]) {
for (const f of placedFiles) {
const originalViewFolderMark = `/__views__/`;
if (f.originalAbsolutePath.includes(originalViewFolderMark)) {
const to = f.getFolder().replace(originalViewFolderMark, '/views/');
f.moveFileTo(to);
}
}
}

修改 package.json

package.json描述了整个前端应用的依赖、启动命令等等。在定制源码时,可能会有添加依赖、调整依赖版本、自定义构建命令的需求。

接下来,我们将展示如何通过插件的方式修改package.json

创建一个名为NpmPackageJSONPlugin的新的插件。在这个插件里,你可以通过注入FileSystemProvider以获得读写虚拟文件系统的能力,并提供patch方法,将虚拟文件系统中的package.json读出、修改并写回。

// playground/customization/npm-package-plugin.ts
import "@abraham/reflection";
import {
GeneratorInfrastructureDomain,
ServiceMetaKind,
} from "@lcap/nasl-unified-frontend-generator";
import { inject, injectable } from "inversify";
import { merge } from "lodash";

export type PackageJSON = Record<string, any>;
export const NpmPackageJSONPluginSymbol = Symbol.for("NpmPackageJSONPlugin");

@injectable()
export class NpmPackageJSONPlugin {
constructor(
@inject(ServiceMetaKind.FileSystemProvider)
private fileSystemProvider: GeneratorInfrastructureDomain.FileSystemProvider
) {}

private packageJSONPath = "/package.json";

private readPkg(): PackageJSON | undefined {
try {
const pkg = this.fileSystemProvider.read(this.packageJSONPath);
if (pkg) {
return JSON.parse(pkg);
}
} catch (error) {}
return undefined;
}

private writePkg(pkg: PackageJSON) {
this.fileSystemProvider.write(
this.packageJSONPath,
JSON.stringify(pkg, null, 2)
);
}

patch(obj: {}) {
const pkg = this.readPkg() ?? {};
// recursively merge obj to pkg
merge(pkg, obj);
this.writePkg(pkg);
}
}

添加自定义ESLint配置

NpmPackageJSONPlugin的基础上,我们可以创建一个新的插件MyEslintPlugin来自定义ESLint配置:

  1. MyEslintPlugin插件需要继承LifeCycleHooksPlugin,在其中实现afterAllFilesGenerated方法。
  2. afterAllFilesGenerated是生命周期钩子,在代码生成结束后会被代码生成器自动地调用。这个钩子适合用来对文件系统的内容做一些修改。
  3. afterAllFilesGenerated中,调用NpmPackageJSONPlugin来写入ESLint配置文件和.eslintignore文件。
// playground/customization/custom-eslint.ts
import {
LifeCycleHooksPlugin,
ServiceMetaKind,
GeneratorInfrastructureDomain,
Logger,
} from "@lcap/nasl-unified-frontend-generator";
import dedent from "dedent";
import { injectable, inject, Container } from "inversify";
import {
NpmPackageJSONPluginSymbol,
NpmPackageJSONPlugin,
} from "./npm-package-plugin";

export function setupEslint(container: Container) {
@injectable()
class MyEslintPlugin extends LifeCycleHooksPlugin {
constructor(
@inject(ServiceMetaKind.FileSystemProvider)
protected fileSystemProvider: GeneratorInfrastructureDomain.FileSystemProvider,
@inject(NpmPackageJSONPluginSymbol)
private npmPackageJSONPlugin: NpmPackageJSONPlugin
) {
super(fileSystemProvider);
}
afterAllFilesGenerated(): void {
const logger = Logger("自定义Lint插件");

logger.info("写入eslint配置");
this.fileSystemProvider.write(
"/eslint.config.mjs",
dedent`
// @ts-check
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';

export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
);
`
);
this.fileSystemProvider.write(
"/.eslintignore",
dedent`
src/packages/**/*.js
`
);
logger.info("在package.json中添加依赖");
this.npmPackageJSONPlugin.patch({
devDependencies: {
eslint: "8.57.0",
typescript: "5.4.3",
"typescript-eslint": "7.4.0",
},
});
logger.info("在package.json中添加eslint启动脚本");
this.npmPackageJSONPlugin.patch({
scripts: {
lint: "pnpm eslint --fix",
},
});
const res = this.fileSystemProvider.read("/package.json");
logger.info(res, "package.json内容");
}
}

container
.bind<NpmPackageJSONPlugin>(NpmPackageJSONPluginSymbol)
.to(NpmPackageJSONPlugin);

container
.bind<GeneratorInfrastructureDomain.CodeGenerationLifecycleHooks>(
ServiceMetaKind.CodeGenerationLifecycleHooks
)
.to(MyEslintPlugin);

return container;
}

playground/index.ts中,引入刚刚实现的setupEslint,并在翻译流程中调用:

// playground/index.ts
import { makeDefaultContainer } from "@lcap/nasl-unified-frontend-generator";
import { startDemoTranslation } from "@lcap/nasl-unified-frontend-generator/playground";
import { setupEslint } from "./customization/custom-eslint";

Promise.resolve()
// 创建新的默认插件容器
.then(makeDefaultContainer)
// 新增eslint配置和脚本
.then(setupEslint)
// 在做完插件修改后,使用此时的插件容器启动Demo的翻译
.then(startDemoTranslation);

将页面组件拆分为逻辑和视图

ReactStateManagerMobxPlugin插件负责React下MobX状态管理的能力,被ReactCodeGenPlugin插件依赖。 其拥有一个produceHooks方法,用来在React组件中输出所有用到的Hooks。

为了将用到的Hooks和页面视图拆成两个文件,可以进行如下修改:

  1. 修改produceHooks方法,使其返回一个ReactFragment而不是原来的ReactHook[]
  2. ReactCodeGenPlugin中,需要处理produceHooks方法返回的ReactFragment
    1. 修改ReactFragment.fromComponentIR方法
    2. 修改ReactCodeGenPlugingenReactComponent方法

下一步...