Skip to main content

Code beat first MVP

· 看完要2分钟嗷
Carrgan Yang
Software Engineer @ Ericsson

最近,正在构思一个游戏Code beat,一个音乐游戏,讲述程序员升职的故事。游戏需要根据音乐节奏来写代码,从而实现程序员升职。打算把之后每个重要的历史版本托管在Code beat

每一段代码中,每一个字符都必须在节奏上敲出来,如果早了晚了都会扣分。扣分之后程序会出现bug,可能在后面的关卡中要回头来解决bug。

如果bug太多,程序就直接不能运行,报错提示闯关失败。

在写代码的时候,有一些类似 IDE 快捷键的功能,可以通过.var来快速初始化变量 etc...

游戏提供不同的难度选择

  • Level 1: 用户只需要输入对应的字符,不需要在意大小写和特殊字符
  • Level 2: 大小写敏感
  • Level 3: 需要输入特殊字符

有了构思之后,用Phaser 3,搭建了一个脚手架,虽然使用rollup打包的部分还需要做一些优化,好在花了一天时间写出了一个 MVP(Minimum Viable Product),这里有个在线预览,包含一些核心功能。

在Controller返回之后,如何继续使用DBContext

· 看完要3分钟嗷
Carrgan Yang
Software Engineer @ Ericsson

在 .NET 中,可能会出现需要异步执行一些代码的情况,在 Controller 返回之后,尝试使用注入的 DbContext 会出错。这通常是因为 DbContext 的生命周期与 Controller 不同,导致 DbContext 已经失效或已被销毁。

在 ASP.NET Core 中,可以使用依赖注入来管理 DbContext 的生命周期。默认情况下,ASP.NET Core 使用 Scoped 生命周期来注册 DbContext,这意味着每个请求将获得一个独立的 DbContext 实例。这通常是很好的,因为这种生命周期的 DbContext 可以在请求处理期间持久化跨越多个操作的状态。

如果你在 Controller 返回之后仍需要使用已注入的 DbContext,则可以尝试以下方法之一:

  1. 将 DbContext 注册为 Singleton 生命周期。

    如果你明确了解你应用中 DbContext 的生命周期,并且你需要在 Controller 结束时后继续使用 DbContext 实例,则可以将其注册为 Singleton 生命周期。但是这通常不是一个好的解决方案,因为 DbContext 是不是一个线程安全的对象。

    services.AddSingleton<MyDbContext>();
  2. 使用 DbContextPool

    ASP.NET Core 中的 DbContext 池化提供了一种 DbContext 的重新利用机制,可以在 DbContext 实例不再使用时自动销毁 DbContext,以提高性能。若要使用 DbContextPool,需要执行以下操作:

    首先,需要将 DbContext 注册为 Scoped 生命周期,然后在 Startup 类的 ConfigureServices 方法中启用 context 池:

    services.AddDbContextPool<MyDbContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))
    .UseLoggerFactory(loggerFactory) // 选填日志工厂
    .EnableSensitiveDataLogging(true)); // 启用敏感数据记录

    接下来,在需要使用 DbContext 的地方,需要在 IServiceScopeFactory 接口中使用 scoped 服务对象。

    public class MyController : ControllerBase
    {
    private readonly IServiceScopeFactory _scopeFactory;

    public MyController(IServiceScopeFactory scopeFactory)
    {
    _scopeFactory = scopeFactory;
    }

    private async Task<MyEntity> GetMyEntityAsync(int id)
    {
    using (var scope = _scopeFactory.CreateScope())
    {
    var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();
    return await dbContext.MyEntities.FindAsync(id);
    }
    }
    }

    在上述示例中,我们创建了一个使用 scoped 生命周期的 DbContext 的作用域,然后使用该作用域中的 DbContext 来到查询 MyEntity 实体的记录。之所以要使用作用域,是因为在 Controller 返回之后,作用域将被销毁,由此产生的 DbContext 实例也将被销毁或重新放入 DbContext 池中,以便将来的请求使用。

前端的环境变量

· 看完要5分钟嗷
Carrgan Yang
Software Engineer @ Ericsson

Process Env

在正常的开发流程中,通常会经历Development/Acceptance(Test)/Production阶段,可能你会想在不同的环境做不同的事,或是给同一个变量赋不同的值。

后端代码获得环境变量的方法非常简单,只用在不同的环境下读取不同的配置文件,或者是读取操作系统定义的环境变量即可。

可是前端代码是运行在浏览上的我们如何去读取环境变量呢?

通常使用现代框架 (React/Vue) 的前端程序员在开发代码的时候,都会用到webpack,前端的环境变量其实是在webpack打包的时候被定义的。

通常,在我们启动开发服务器的时候 npm run start webpack将 process.env.NODE_ENV 打包为development,而我们在运行 npm run build 的是时候 webpack 把 process.env.NODE_ENV 打包为 production

警告

process并不是一个对象,process.env也不是一个对象,如果你写出如下语句console.log(process.env)你会得到一个process undefined错误。 但是你写出console.log(process.env.NODE_ENV)你会得到一个字符串。是什么如此神器?事实上在webpack打包的时候,webpack自动把process.env.NODE_ENV替换成"development"了。

Config.js

例用以上特性就可以实现在开发的时候和打包之后运行不同的代码或者给同一个变量赋不同的值

const config = process.env.NODE_ENV === "developemt" ? DEV_CONFIG : PROD_CONFIG;
process.env.NODE_ENV === "developemt" ? funcDev(): funcProd();

但是这远远还不够,在一个大的开发团队中,三个环境都需要打包,并运行在不同的机器上进行,这种情况下如何配置不同的变量呢?

我们可以在public文件夹下创建一个配置文件

public/runtime-config.js
window[config] = {
clintId: "xxxx"
}
public/index.html
<html>
<head>
<script src="%PUBLIC_URL%/runtime-config.js"></script>
</head>
</html>
Any File
const clintId = (window as any).config.clintId;

public/runtime-config.js 在打包之后会被放到打包文件的跟目录下,而写在头里的script会优先运行我们的config文件,将我们的config对象挂到window上,这样后续的代码就能拿到config对象的了。

我们只需要去到不同机器上修改runtime-config.js就可以对不同的环境应用不同的变量了。

CI/CD

当然在运用CI/CD的项目中我们就可以省去手动更改的步骤了,结合上述两种方法在CD的过程中直接替换Config里的变量就可以了。

public/runtime-config.js
window[config] = {
clintId: "#{clintID}#"
}
public/index.html
<html>
<head>
<script src="%PUBLIC_URL%/runtime-config.js"></script>
</head>
</html>
any file
const clintId = process.env.NODE_ENV === "development" ? "xxx" : (window as any).config.clintId;

这样在本地开发服务器运行的时候,我们获得的clintID永远是"xxx",而在设置部署CD的时候,我们根据不同的环境替换掉runtime-config.js中的#{clintId}#就可以了。

部署到服务器之后,我们会去读取不同环境挂在window上的不同配置。

不同CD工具有不同的配置方法,一下已Azure DevOps为例

首先在Pipelines->Library下创建Variable group,并写入当前环境下的key-value

Library

在Releases中构建CD的时候引用该Group到当前部署阶段

Link Variable

在CD中使用replace tokens替换变量

Replace Tokens

CSS自适应固定长款比容器

· 看完要2分钟嗷
Carrgan Yang
Software Engineer @ Ericsson

Padding

实现自适应长款比的关键就是padding,在设置padding的时候使用 % 计算的值是根据宽度计算的,

也就是说 padding: 50% 其实是基于容器宽度的50%。

容器实现

根据padding的这一特性实现这一容器就很简单了

16/9占满屏幕的容器
<div class="container">
<div class="content"></div>
</div>
16/9占满屏幕的容器
.container {
width: 100%;
padding-bottom: 56.25%
}
.content {
width: 100%;
height: 100%;
}

同理可对一个容器实现一个遮罩层

16/9占满屏幕的背景和一个悬浮居中的图标
<div class="container">
<div class="background"></div>
<div class="content">
<svg width="35px" height="35px"></svg>
</div>
</div>
16/9占满屏幕的背景和一个悬浮居中的图标
.container {
width: 100%;
padding-bottom: 56.25%;
position: relative;
top: 0;
left: 0;
}
.background {
width: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
.content {
width: 100%;
position: absolute;
top: calc(50% - 35px);
left: calc(50% - 35px);
z-index: 2;
}

用Hook做高级封装

· 看完要5分钟嗷
Carrgan Yang
Software Engineer @ Ericsson

在代码中有大量相同的组件引用和状态引用场景的情况下,利用自定义Hook对功能进行统一封装,有利于代码复用和维护。

例如在不同的三个视图中都存在表格,如果我们都需要对表格的每一列排序并构建Filter,就会存在大量的代码耦合情况。

采用传统的构建结构我们需要在每一个视图中记录一个Filter状态。

const [editedFilter, setEditedFilter] = useState<IViewADefaultFilter>(defaultFilter);
const [editedSort, setEditedSort] = useState<IViewADefaultSort>(defaultSort);

在每一个View中,重复对每一列引用Filter组件

/src/view/viewA.jsx
<>
Name
<InputDropdownFilter
value={editedFilter.name}
onValueChange={handelFilterChange("name")}
onSortChange={handelSortChange("name")}
sortStatus={editedSort.columnsA}
onApply={handelFilterApply}
onClean={() => setEditedFilter(f => ({ ...f, [filterKey]: "" }))}
/>
Birthday
<TimeDropdownFilter
dateString={editedFilter.birthday}
onValueChange={handelFilterChange("birthday")}
onSortChange={handelSortChange("birthday")}
sortStatus={editedSort.birthday}
onApply={handelFilterApply}
onClean={() => setEditedFilter(f => ({ ...f, [filterKey]: { from: "", to: "" } }))}
/>
</>

可见在ViewA、ViewB、ViewC中有90%的代码都是相似的,唯一不同的就是列名,还有每一列的顺序和类型。

整理每个组件的类型后,构建高级Hook来封装状态,并统一暴露方法。

用Hook的实现方式
src/component/helper.tsx
export const useDropdownFilter = <
IDefaultFilter extends { [key: string]: string | IDateString | IListBuilderItem[] },
IDefaultSort extends { [key: string]: ISortStatus }
>(
defaultFilter: IDefaultFilter,
defaultSort: IDefaultSort,
handelFilterApply: () => void
) => {
const [editedFilter, setEditedFilter] = useState<IDefaultFilter>(defaultFilter);
const [editedSort, setEditedSort] = useState<IDefaultSort>(defaultSort);
const handelFilterChange =
(name: keyof IDefaultFilter) => (value: string | IDateString | IListBuilderItem[]) => {
setEditedFilter(f => ({ ...f, [name]: value }));
};

const handelSortChange = (name: keyof IDefaultSort) => (value: ISortStatus) => {
setEditedSort(f => ({ ...f, [name]: value }));
};

const inputDropdownFilter = (
filterKey: keyof IDefaultFilter,
label: string,
position?: "left" | "right" | undefined
) => (
<>
{label}
<InputDropdownFilter
position={position}
value={editedFilter[filterKey] as string}
onValueChange={handelFilterChange(filterKey)}
onSortChange={handelSortChange(filterKey as keyof IDefaultSort)}
sortStatus={editedSort[filterKey as keyof IDefaultSort]}
onApply={handelFilterApply}
onClean={() => setEditedFilter(f => ({ ...f, [filterKey]: "" }))}
/>
</>
);

const timeDropdownFilter = (filterKey: keyof IDefaultFilter, label: string) => (
<>
{label}
<TimeDropdownFilter
dateString={editedFilter[filterKey] as IDateString}
onValueChange={handelFilterChange(filterKey)}
onSortChange={handelSortChange(filterKey as keyof IDefaultSort)}
sortStatus={editedSort[filterKey as keyof IDefaultSort]}
onApply={handelFilterApply}
onClean={() => setEditedFilter(f => ({ ...f, [filterKey]: { from: "", to: "" } }))}
/>
</>
);

const listDropdownFilter = (
filterKey: keyof IDefaultFilter,
label: string,
item: IListBuilderItem[]
) => (
<>
{label}
<ListDropdownFilter
selectedItem={editedFilter[filterKey] as IListBuilderItem[]}
item={item}
onValueChange={handelFilterChange(filterKey)}
onSortChange={handelSortChange(filterKey as keyof IDefaultSort)}
sortStatus={editedSort[filterKey as keyof IDefaultSort]}
onApply={handelFilterApply}
onClean={() => setEditedFilter(f => ({ ...f, [filterKey]: defaultFilter[filterKey] }))}
/>
</>
);

return {
inputDropdownFilter,
timeDropdownFilter,
listDropdownFilter,
editedFilter,
editedSort
};
};

现在在每一个View中只需要应用次Hook并调用合适的方法渲染Filter就可以了

/src/view/viewA.jsx
const { editedFilter, editedSort, inputDropdownFilter, timeDropdownFilter } =
useDropdownFilter(
{
name: "",
birthday: { from: "", to: "" }
},
filterInit<ISorts>(Object.keys(EViewAFilterKey), undefined),
handelFilterApply
);
return (
<>
{inputDropdownFilter("name", "Name"}
{timeDropdownFilter("birthDay", "Name"}
</>
)

减少了非常多的代码量,代码也更加利于读懂,维护起来也只用维护hook就可以了