The front-end team will inevitably need to maintain some internal systems. Due to the unreasonable architecture design of some internal systems, as the complexity of the business increases, there will be more and more "bad smell" codes, which will lead to increased cognitive and communication costs and even problems. Frequently, at this time, reconstruction has naturally become a choice. However, refactoring is not a whim, nor does it happen overnight. It requires careful analysis and orderly implementation. Taking the experimental platform as an example, I will introduce the experience of refactoring the front-end of Zhilian.
The experimental platform is an A/B experimental ecology independently developed by Zhilian Recruitment. It relies on a data platform and is tailored to the company's business and technical characteristics. It provides rich experimental capabilities, scientific experimental mechanisms and complete process management.
The web side is developed and implemented using the Ant Design Vue component library based on Vue; the API layer is developed based on Node.js, pre-processing, combining, and encapsulating the original data returned by the back-end microservices, effectively reducing the UI and back-end interface Coupling to achieve parallel development and interface changes.
status quo
The overall design of UI layout and layout is not uniform, front-end interaction is complex, functions are redundant, and the increasing number of "bad smell" codes makes development and maintenance more difficult; the Api layer does not follow the mainstream RESTful Web API standard, and is only responsible The forwarding of the back-end interface is implemented, and the logic is all implemented on the Web layer, which does not effectively reduce the coupling between the UI and the Api layer interface, which increases the burden on the Web layer;
Based on the above reasons, we decided to refactor the experimental platform system to further improve its ease of use, cohesion and maintainability.
analyze
First, analyze the functions and usage of the experimental platform on a page-by-page basis to facilitate a preliminary understanding of the following refactoring work:
overview page : Mainly displays the statistical information of the use since the experimental platform was launched. In order to better display the statistical content and facilitate the maintenance of the data structure in the future, we have decided that the data is no longer provided by the back-end interface, but by our own Api Layer calculation
experiment list page : The list page is mainly used to display the key information of the experiment that users pay attention to, so the display fields are as simple as possible and the primary and secondary layout of the information is optimized; at the same time, it provides a quick jump entry (direct statistics, direct debugging), optimization User experience; add searchable experiment names and creators to optimize the search experience; for experiment states, some states are no longer needed (such as application for publication, consent for publication, published, archived), and compatibility with the old experiment state is also required. For this we New adjustments have been made to the experimental status:
- Draft: New
- Debug: debug status
- Run: run, apply for release, agree to release
- Stop: abandon, stop, released, archive
- World overview page: basic functions remain the same, just refactor the code according to the principles
variable page : After consideration, there is no need to release or restore the variable, so only the variable list that is "running" the experiment needs to be displayed;
setting page : The main purpose is to display and add administrators, so there is no need to display all users, so it can be simplified to delete and add administrators;
basic information page : The basic function of this page remains unchanged, the page layout is optimized, the user experience is unified, and the editing authority is uniformly controlled by the Api layer;
Statistical Analysis Page : The basic functions of this page remain unchanged. In order to facilitate maintenance, all statistical data is calculated and generated by the Api layer; in addition, the real-time function of the index page can be removed after analysis; the overall layout of the page is optimized and the code is reconstructed;
operation record page : Need to add the information of the clone experiment id to optimize the user experience;
control page : The basic functions of this page remain unchanged, the user experience is unified, the editing authority is uniformly controlled by the Api layer, the page layout and typesetting are optimized, and the code is refactored;
summary page : used to summarize the experimental results, this page is no longer needed after analysis;
in principle
At this point, based on the previous analysis, we have a preliminary understanding of the status quo of the experimental platform. Next, summarize some useful guiding principles:
Layering, the Web layer and the Api layer should perform their respective duties:
- The web layer is only responsible for UI interaction and display;
- The API layer follows the Restful Web API standard, adopts Typescript development with strong type checking, and is responsible for all functional logic processing and permission control;
Layout, the overall layout is consistent with the design:
- The layout is naturalized and maintainability is improved;
- Standardize the sections and keep the design uniform;
- The space between the top, bottom, left, and right of each section is the same, and the sections are aligned;
Module, keep single responsibilities and facilitate maintenance principles:
- Split modules according to responsibilities and decouple each other;
- Try not to maintain the state (especially the global state) as much as possible, but to achieve it through interaction with other modules;
- Logic is decentralized and distributed to various functional components;
- Componentization, keeping the responsibilities of components single;
- The components that are not reused are placed under the directory of the parent container component;
Development specifications, follow the Zhilian front-end development specifications and custom principles:
- Standardize styles to reduce update costs;
- Unified input and output specifications;
- Prohibit all magic numbers, but implement them through variables;
- Prohibit all inline styles, but implement it through a more general Class;
- Do not use absolute positioning and floating as much as possible, but implement it through a-layout components, standard document flow or Flex;
Process, using progressive reconstruction method:
- Progressive refactoring method, refactoring in stages, each stage does not destroy existing functions, and has the ability to release separately;
stage
Next, we divide the reconstruction cycle into several different stages for orderly implementation.
The first step: js migrate to ts
As we all know, JS is a dynamic language that dynamically processes types at runtime and is very flexible in use. This is the charm of dynamic languages. However, a flexible language has a drawback that there are no fixed data types and lack of static type checking, which leads to more The phenomenon of random assignment during human development makes it difficult to eliminate more problems during the compilation phase. Therefore, for projects that require long-term iterative maintenance and the participation of many developers, choose a strict type of language to find errors during compilation. It is very necessary, and TypeScript uses strong type constraints and static checks and smart IDE prompts, which can effectively reduce the speed of software corruption and improve the readability and maintainability of the code.
Therefore, this refactoring work first started from the migration of js to ts, laying a language foundation for subsequent model combing.
ts is limited to the node layer of the API project, because the front-end using Vue2 is not friendly to ts support, so the original js is still used.
Step 2: Sort out the data model
This step is relatively simple, mainly to sort out the input and output information requested by the existing API interface, and lay a good foundation for the subsequent sorting of data entities.
First, sort out all the pages of the experimental platform system, as shown below:
- set up
- variable
- world
- Overview
- Experiment list
- Create experiment
- View basic information
- Edit basic information
- View operation records
- View statistics
- control
Then, further statistics are performed on the API interfaces involved in each page, for example, the settings page: API interfaces such as obtaining user lists, adding and deleting users, and setting user roles.
Secondly, according to the results of the previous step, the input and output information of each API interface request is summarized and organized. For example, open the basic information page of the experiment, find the browser development tool and switch to [NetWork], right-click the request interface to find [ Copy as fetch] Copy the request result, as shown in the figure below:
The following shows the code structure of the API interface requesting input and output information:
【示例】
// [分组]: 获取实验分组信息列表
fetch(
"https://example.com/api/exp/groups?trialId=538",
{
credentials: "include",
headers: {
accept: "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin"
},
referrer: "https://example.com/exps/538",
referrerPolicy: "no-referrer-when-downgrade",
body: null,
method: "GET",
mode: "cors"
}
);
const response = {
code: 200,
data: {
groups: [
{
desp: "c_app_default_baselinev",
flow: 20,
groupId: 1368,
imageUrl: "",
type: "A,对照组",
vars: ["c_app_default_baselinev"]
},
{
desp: "c_App_flowControl_baseline",
flow: 20,
groupId: 1369,
imageUrl: "",
type: "B",
vars: ["c_app_flowControl_baseline"]
}
],
varName: ["gray_router_prapi_deliver"]
},
time: "2019-12-20 17:25:37",
message: "成功",
taskId: "5f4419ea73d8437e9b851a0915232ff4"
};
Similarly, we follow the above process to sort the input and output of the corresponding API interface requests of all pages respectively, and finally, we get the following file list:
Next, analyze the return value of each interface, extract the fields that will be used in the UI layer interaction, so as to define the basic data model. The data model should be able to intuitively display the basic structure of the data, as shown below:
【示例】
// 分组
const group = {
id: 123,
name: 'A', // 组别
description: 'CAPP变量', // 描述
variableValue: 'c_app_baselinev', // 变量值
preview: 'http://abc.jpg', // 预览图
bandwidth: 10 // 流量
}
The third step: sort out the data entities
Earlier we sorted out the data model. Next, we need to further sort out the data entities corresponding to the model according to the data model, as shown below:
【示例】
class ExperimentGroup {
id: number
name: string
description: string
variableValue: string
previewImage: string
bandwidth: number
constructor () {
this.id = null
this.name = null
this.description = null
this.variableValue = null
this.previewImage = null
this.bandwidth = 0
}
}
Define the fields that need to be calculated and processed in Object.defineProperties, as shown below:
【示例】
class ExperimentCompositeMetric extends ExperimentMetric {
unit: string
constructor () {
super()
this.unit = ''
Object.defineProperties(this, {
displayName: {
get: () => (this.unit ? `${this.title}(${this.unit})` : this.title),
enumerable: true
}
})
}
}
At the same time, according to the output of the interface, a unified data entity of the Api interface output specification is defined, as shown below:
【示例】
class Result {
error: boolean|string|Error
data: any
requestId: string|null
constructor () {
this.error = false
this.data = null
this.requestId = null
}
}
Step 4: Reorganize the interaction type interface
Next, according to the interactive function of the UI layer, define the input specifications of the interface (including methods, paths, and parameters, etc.), while enriching the entities associated with the interface, and finally, in the actual development, continuously adjust and optimize the entities as needed, as follows Shown:
【示例】
// 获取指标图表数据
get('/api/v2/experiment/stats/trending', {
params: {
id: 123,
type: "key",
period: "day", // or hour
from: "2019-12-17",
to: "2019-12-18"
}
})
Step 5: Reorganize the UI
This step is mainly to reorganize the UI (pages, layouts, components, etc.), which defines the basic display form and input and output specifications of the page:
【示例】
<template>
<editable-section @click="onEdit">
<h2 slot="header">分组</h2>
<table>
<thead>
<tr>
<th>组别</th>
<th>组名</th>
<th>变量 {{ experiment.variable.name }} 的值</th>
<th>预览图</th>
</tr>
</thead>
<tbody>
<tr v-for="group in experiment.groups">
<td rowspan="group.name | toRowspan">group.name | toType</td>
<th>group.name</th>
<td>
<p>
{{ group.variableValue }}
<small>{{ group.description }}</small>
</p>
</td>
<td>
<img src="group.previewImage"/>
</td>
</tr>
</tbody>
</table>
</editable-section>
</template>
<script>
import BaseSection from 'shared/components/EditableSection'
export default {
props: {
experiment: Object
},
filters: {
toType (value) {
return value === 'A' ? '对照组' : '实验组'
},
toRowspan (value) {
return value === 'A' ? 1 : this.experiment.groups.length - 1
}
},
methods: {
onEdit () {
alert('暂未实现,请使用V1。')
}
}
}
</script>
Step 6: Define the file layout
Before starting the reconstruction, we defined the basic file directory of the Web project. At the same time, based on past experience, we also extracted the commonly used public variables and methods, as shown in the following figure:
Shared file storage is used to store public file resources. Among them, components file stores public component resources, api.js file stores all API URL resources, styles file stores public css file resources, images file stores public pictures, and fonts stores public fonts.
In the variables.postcss file, some commonly used css variables are defined, as shown below:
:root {
--font-family--code: cascadia, pingfang sc, microsoft yahei ui light, 微软雅黑, arial, sans-serif;
--font-size--super: 70px;
--font-size--xl: 24px; /* 超大字号 */
--font-size--lg: 18px; /* 大字号 */
--font-size: 14px; /* 常规字号 */
--font-size--sm: 12px; /* 小字号 */
--space: 16px; /* 常规间距,适用于padding及margin */
--space--sm: 12px; /* 小间距 */
--space--xs: 8px; /* 超小间距 */
--color--white: #fff;
--color--black: #000;
--color--subtle: rgba(0, 0, 0, 0.45); /* 非显著颜色 */
--color--message: #93a1a1;
--color--info: #859900;
--color--warning: #b58900;
--color--trace: #657b83;
--color--error: #dc322f;
--color--normal: #268bd2;
--color--lightgrey: #f0f2f5;
--color--active: #1890ff;
}
In the global.postcss file, the style coverage of the third-party library and the global style are defined, as shown below
@import './variables.postcss';
@font-face {
font-family: 'Cascadia';
src: url('../fonts/cascadia.ttf');
}
// 全局样式
html,
body {
min-width: 1200px;
}
.text--description {
color: var(--color--subtle);
}
.text--mono {
font-family: var(--font-family)
}
// 第三方样式覆盖
.ant-modal-body {
max-height: calc(100vh - 240px);
overflow: auto;
}
.ant-table-body td {
vertical-align: top;
}
.ant-table-thead > tr > th {
background: #fafafa !important;
}
.ant-table-small > .ant-table-content > .ant-table-body {
margin: 0;
}
.ant-table {
& .ant-empty {
& .ant-empty-description {
display: none;
}
&::after {
content: '目前啥也没有';
display: block;
}
}
}
The template.js file is used to store the method of obtaining the html template, as shown below:
import favicon from 'shared/images/favicon.png'
function generate ({
ctx, title, ...pageContexts
}) {
const prepareDataString = Object.entries(pageContexts)
.map(([key, value]) => `var ${key} = ${typeof value === 'object' ? JSON.stringify(value) : value}\n`)
.join('')
const template = `<!DOCTYPE html>
<html>
<head>
//自定义title
<title>${title ? `${title} | ` : ''}智联实验平台</title>
<link rel='shortcut icon' href='${favicon}' />
// 资源文件占位
${ctx.template.placeholders.head.style}
${ctx.template.placeholders.head.link}
${ctx.template.placeholders.head.script}
<script>
//传入自定义全局页面数据
${prepareDataString}
</script>
</head>
<body>
// 资源文件占位
${ctx.template.placeholders.body.root}
${ctx.template.placeholders.body.script}
</body>
</html>`
return template
}
export default {
generate
}
Similarly, we also defined the file directory of the Api project, as shown in the following figure:
Shared is used to store public file resources, and utils stores some public methods. The files in the models file are the data models we organized earlier.
Step 7: Progressive development
Next, we can formally enter the next stage and carry out progressive refactoring.
According to the degree of difficulty of the project and the dependency of the function, the priority is defined for all pages, as shown below:
- frame
- set up
- variable
- world
- Overview
- Experiment list
- View operation records
- View statistics
- View basic information
- control
- Create experiment
- Edit basic information
Refactor in order from low to high, each stage does not destroy existing functions, and has the ability to release separately. Define the current version as v1, refactor the version v2, and differentiate on the url, such as v2/exps. Node layer version v1 is js, and then gradually replaced with version v2 written in ts (because ts is downward compatible with js, so it can exist at the same time in the project);
With the orderly progress of the refactoring work, we will find that the refactoring becomes more and more handy. It is still necessary to control the scope of the refactoring. After completing a functional test, we can start the next function.
Step 8: Pull out the public components
It is not difficult to find that this step is actually carried out at the same time as the previous step. In the refactoring process, in order to ensure the simplicity, uniformity, and maintenance of the code, we continue to extract components according to the usage scenarios and functions to ensure that The unification of code and interface, which scenarios can be separated from components?
- Use more than 3 repetitive codes;
- The usage scenarios are similar;
- The logic is relatively close, and the code is always updated together;
And follow the following principles:
- Keep component responsibilities single, high cohesion and low coupling;
- Keep the configuration of parameters simple and flexible;
- Keep the granularity appropriate and the number of lines of code moderate;
On this basis, we have removed common components such as layout, editing, avatar, menu components, experimental status, etc., which greatly reduces the tedious and repetitive workload, as follows:
BaseLayout : the main frame layout container, the navigation bar on the left, the header on the right side, and the section container on the right side.
Header : Top layout, with default style, Title on the left, operation buttons on the right, etc.
BaseSection : read-only layout container, the upper left of which can set the title and lower nested content area.
EditableSection : editable layout container, the title can be set on the upper left, operation buttons on the upper right, and the lower nested content area.
Step 9: Unify the overall layout, interactive experience and prompt information, etc.
As the refactoring work progresses, we need to further optimize some details of the UI to form a unified design system:
Check/adjust the overall layout, navigation and the proportion of each section, for example:
- spacing
The space between the top, bottom, left, and right of each section is the same, and the sections are aligned;
The internal padding of the board is uniform;
The spacing of the same elements is uniform; - layout
The position of the same component (such as left or right) is unified;
Use the navigation menu on the left and the content mode layout on the right, using the BaseLayout component;
Use the Header component at the top of the right side of the page;
The read-only content module uses the BaseSection component, and the editable section uses the EditableSection component, etc.; - sheet
All table data uses compact mode;
The first row of the table is locked, and the first column of some tables is locked;
In principle, pagination is not used for tables that return all data at once;
For tables that must have pagination, the pagination area should be displayed in the visible area;
The main field of the table content is automatically column width, and the minimum width of the column width of the secondary content is set;
- spacing
Interactive experience, for example;
- Edit the form using EditableSection component, click the "Edit" button in the upper right corner to pop up a modal box for editing;
- "Operation Button" is on the right side of Header;
- The pop-up prompt box PopConfirm component of the delete operation;
- Toast prompt box uses Tooltip component;
- Unified message prompt, title, click details to display the specific information of the error;
Standard font, font size, color, and font style of the same scene at the same level, for example:
- The font-size of the title of the top header: 24px;
- section title font-size: 20px;
- Set wide font text--mono for statistics, English variables, etc.;
- Setting style text–description for auxiliary text content such as description;
unified message
- How to use;
- Interactive form;
- Display information;
Step 10: Progressively launch the v2 page
In order to prevent the newly-launched v2 page, we have added a "switch version" function in the upper right corner of the page when an error occurs in some cases, which can quickly switch to the v1 page without affecting the normal use of users. And after a period of self-test and user feedback, gradually optimize the v2 function.
Step 11: Launch all v2 versions
After all the v2 pages and APIs are all online, after a week of self-test and user feedback, there are no more functional problems, and the version "switch button" in the upper right corner can be removed.
Step 12: Delete the v1 version
After running and observing for a month, the v2 version was running stably online, and there were no obvious functional problems in use, and then the v1 page and code were offline. Since then, most of the refactoring work has been completed.
The thirteenth step: pull out the public component library
Now that the refactoring work is nearing completion, the experimental platform has undergone completely new changes in architecture design, coding specifications, layout, and user experience.
Before announcing the completion of the refactoring, in order to further unify the user experience and improve the efficiency of system iteration, we extracted a public component library. Public components are not business-related and can be used in other scenarios.
As a result, the prototype of the AntNest component library was produced. It is implemented based on the Ant Design Vue component library, which covers the common components of layout, container, card, prompt, editing, data display and other functions. And soon the v1.0.0 version was released. First, the application was replaced on the experimental platform, Ada workbench, and the Rubik's Cube management system, and iterative optimization was continuously performed, and the operation was stable after a period of observation. It is gradually promoted and used in other internal systems of Zhilian. It has been successfully applied to multiple systems, such as Kunpeng, Fuxi, operating platforms, performance monitoring platforms, and more than a dozen projects. In the future, other internal systems will be replaced with AntNest components to form A unified management system to achieve a truly unified user experience.
Step 14: Provide engineering templates
In order to further simplify the development process and make it easier for everyone to quickly create projects, we also provide lightweight templates based on Web engineering and Api engineering.
Among them, the Web project template is based on the AntNest component library. In addition to the basic Web framework structure, we also have built-in overview pages (common overview and waterfall overview), list pages, and details pages (read-only and editable) commonly used by management systems. ), layout pages and error pages to meet the basic needs of users.
The Api project template is based on Node.js and developed by Typescript with strong type checking. The file directory structure consistent with the experimental platform is prefabricated to facilitate rapid development.
Summarize
Looking back at the entire refactoring process, you will find that the first thing we do is not coding, but an in-depth analysis of the status quo. In this process, by seeking common ground while reserving differences, some patterns will emerge naturally, and they are all "materials" for reconstruction.
When actually coding, we adopted a gradual strategy, decomposing the entire process into multiple steps. Strive to achieve that after each step is completed, the entire module can meet the release standards. This means that the changes involved in each step need to be limited to a controllable range, and each step needs to include a complete test.
The above is the history and experience of this experimental platform reconfiguration, hoping to provide help and reference for the future development of new projects or the reconstruction of old projects.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。