面试宝典APP-通用设施

项目物料

  • 参考设计

【腾讯 CoDesign】面试宝典 https://codesign.qq.com/s/2kY5j3LVeb9ExNd 访问密码:2YDD

  • 接口文档

【apifox】面试宝典 https://apifox.com/apidoc/shared-31e6a8d7-316a-4c5a-83c1-e5c7a2c343bf

  • 效果例图
登录 首页
我的 打卡
面试题 学习时间

基础能力

1. 创建项目

创建项目,参考华为 Codelabs 优秀案例结合项目自身情况对项目结构进行维护

1)创建项目

2)准备好项目结构

ets ├── common // 通用模块 │   ├── components // - 通用组件 │   ├── constants // - 常量数据 │   ├── images // - 图片资源 │   └── utils // - 工具类 ├── entryability // 入口UIAbility │   └── EntryAbility.ts ├── models // - 数据模型 ├── pages // - 页面组件 │   └── Index.ets └── views // - 页面对应自定义组件 └── Index

2. 日志工具类

1)封装 logger 工具类

common/utils/Logger.ts
1import hilog from '@ohos.hilog'
2
3const DOMAIN = 0xFF00
4const PREFIX = 'INTERVIEW'
5const FORMAT = '%{public}s, %{public}s'
6
7export class Logger {
8  static debug(...args: string[]) {
9    hilog.debug(DOMAIN, PREFIX, FORMAT, args)
10  }
11
12  static info(...args: string[]) {
13    hilog.info(DOMAIN, PREFIX, FORMAT, args)
14  }
15
16  static warn(...args: string[]) {
17    hilog.warn(DOMAIN, PREFIX, FORMAT, args)
18  }
19
20  static error(...args: string[]) {
21    hilog.error(DOMAIN, PREFIX, FORMAT, args)
22  }
23
24  static fatal(...args: string[]) {
25    hilog.fatal(DOMAIN, PREFIX, FORMAT, args)
26  }
27
28  static isLoggable(level: hilog.LogLevel) {
29    return hilog.isLoggable(DOMAIN, PREFIX, level)
30  }
31}

2)修改 entryAbility 的日志打印

entryAbility.ts
1import UIAbility from '@ohos.app.ability.UIAbility';
2import window from '@ohos.window';
3import { Logger } from '../common/utils/Logger';
4
5export default class EntryAbility extends UIAbility {
6  onCreate(want, launchParam) {
7    Logger.info('Ability onCreate')
8  }
9
10  onDestroy() {
11    Logger.info('Ability onDestroy')
12  }
13
14  onWindowStageCreate(windowStage: window.WindowStage) {
15    // Main window is created, set main page for this ability
16    Logger.info('Ability onWindowStageCreate')
17
18    windowStage.loadContent('pages/Index', (err, data) => {
19      if (err.code) {
20        Logger.info('Failed to load the content', JSON.stringify(err) ?? '')
21        return;
22      }
23      Logger.info('Succeeded in loading the content', JSON.stringify(data) ?? '');
24    });
25  }
26
27  onWindowStageDestroy() {
28    // Main window is destroyed, release UI related resources
29    Logger.info( 'Ability onWindowStageDestroy');
30  }
31
32  onForeground() {
33    // Ability has brought to foreground
34    Logger.info('Ability onForeground');
35  }
36
37  onBackground() {
38    // Ability has back to background
39    Logger.info('Ability onBackground');
40  }
41}

3. 访问权限工具类

1)用户信息持久化,获取用户,设置用户,删除用户以及用户访问页面控制

models/UserModel.ts
1export class UserModel {
2  id: string
3  username: string
4  avatar: string
5  refreshToken: string
6  token: string
7  totalTime?: number
8  nickName?: string
9  clockinNumbers?: number
10}
common/utils/Auth.ts
1import router from '@ohos.router'
2import { UserModel } from '../../models/User'
3
4const USER_KEY = 'interview-user'
5const PASS_LIST = ['pages/LoginPage']
6
7export class Auth {
8  static initLocalUser() {
9    PersistentStorage.PersistProp(USER_KEY, '{}')
10  }
11
12  static setUser(user: UserModel) {
13    AppStorage.Set(USER_KEY, JSON.stringify(user))
14  }
15
16  static getUser() {
17    const str = AppStorage.Get<string>(USER_KEY) || '{}'
18    return JSON.parse(str) as UserModel
19  }
20
21  static delUser() {
22    AppStorage.Set(USER_KEY, '{}')
23  }
24
25  // 需要登录页面使用
26  static pushUrl(options: router.RouterOptions) {
27    const user = this.getUser()
28    if (!PASS_LIST.includes(options.url) && !user.token) {
29      return router.pushUrl({
30        url: 'pages/LoginPage',
31        params: {
32          ...options.params,
33          return_url: options.url
34        }
35      })
36    }
37    return router.pushUrl(options)
38  }
39}

2)在 Index 初始化持久化用户

pages/Index.ets
1Auth.initLocalUser()

4. 请求工具类-请求处理

  • 维护基准地址
  • get 传参拼接地址上,其他请求方式在 extraData 上
  • 提供快捷调用静态方法
common/utils/Request.ts
1import http from '@ohos.net.http'
2import { Auth } from './Auth'
3
4const BASE_URL = 'https://api-harmony-teach.itheima.net/hm/'
5
6const req = http.createHttp()
7
8const request = <T = unknown>(
9  url: string,
10  method: http.RequestMethod = http.RequestMethod.GET,
11  params: object = {}
12) => {
13  let fullUrl = BASE_URL + url
14  const options: http.HttpRequestOptions = {
15    header: {
16      'Content-Type': 'application/json',
17    },
18    readTimeout: 30000,
19    method,
20  }
21
22  // 如果是对象数据拼接URL,如果是其他则携带在 extraData
23  if (method === http.RequestMethod.GET) {
24    const urlParams = Object.keys(params).map(key => `${key}=${params[key]}`)
25    fullUrl += `?${urlParams.join('&')}`
26  } else {
27    options.extraData = params
28  }
29
30  // 携带 token
31  const user = Auth.getUser()
32  if (user.token) {
33    options.header['Authorization'] = `Bearer ${user.token}`
34  }
35
36  return req.request(fullUrl, options)
37
38}
39
40export class Request {
41  static get<T>(url: string, params?: object) {
42    return request<T>(url, http.RequestMethod.GET, params)
43  }
44
45  static post<T>(url: string, params?: object) {
46    return request<T>(url, http.RequestMethod.POST, params)
47  }
48
49  static put<T>(url: string, params?: object) {
50    return request<T>(url, http.RequestMethod.PUT, params)
51  }
52
53  static delete<T>(url: string, params?: object) {
54    return request<T>(url, http.RequestMethod.DELETE, params)
55  }
56}

5. 请求工具类-响应处理

  • 处理业务失败
  • 处理登录失效
  • 添加日志行为
common/utils/Request.ts
1return req.request(fullUrl, options)
2    .then(res => {
3      if (res.result) {
4        // 1. 处理响应
5        const response = JSON.parse(res.result as string) as {
6          code: number
7          message: string
8          data: T
9        }
10        if (response.code === 10000) {
11          return response
12        }
13        // 2. 处理 token 失效
14        if (response.code === 401) {
15          Auth.delUser()
16          router.pushUrl({
17            url: 'pages/Login'
18          }, router.RouterMode.Single)
19        }
20      }
21      return Promise.reject(res.result)
22    })
23    .catch(err => {
24      promptAction.showToast({ message: '网络错误' })
25      return Promise.reject(err)
26    })
27    .finally(() => {
28      // 销毁请求释放资源
29      req.destroy()
30    })
1// 请求日志:
2+  Logger.info(`REQUEST→${url}→${method}`, JSON.stringify(params))
3
4  return req.request(fullUrl, options)
5    .then(res => {
6      if (res.result) {
7        // 响应日志:
8+        Logger.info(`RESPONSE→${url}→${method}`, res.result.toString().substring(0, 250))
9        // 1. 处理响应
10        const response = JSON.parse(res.result as string) as {
11          code: number
12          message: string
13          data: T
14        }
15        if (response.code === 10000) {
16          return response
17        }
18        // 2. 处理 token 失效
19        if (response.code === 401) {
20          Auth.delUser()
21          router.pushUrl({
22            url: 'pages/Login'
23          }, router.RouterMode.Single)
24        }
25      }
26      return Promise.reject(res.result)
27    })
28    .catch(err => {
29      promptAction.showToast({ message: '网络错误' })
30      // 错误日志:
31+      Logger.error(`RESPONSE→${url}→${method}`, JSON.stringify(err).substring(0, 250))
32      return Promise.reject(err)
33    })
34    .finally(() => {
35      // 销毁请求释放资源
36      req.destroy()
37    })

UI 管理

1. 界面一多

一个界面适配多类终端

手机-竖屏

pad-横屏

  • 媒体查询 tablet 且横屏
  • tabs 的横竖模式
  • 设置 bar 的位置和高度
Index.ets
1import mediaquery from '@ohos.mediaquery'
2class ToolBarItem {
3  defaultIcon: string | Resource
4  activeIcon: string | Resource
5  label: string
6}
7
8@Entry
9@Component
10struct Index {
11  @State
12  activeIndex: number = 0
13
14  @State
15  isLandscape: boolean = false
16
17  listenerScreen = mediaquery.matchMediaSync('(orientation: landscape) and (device-type: tablet)')
18
19  aboutToAppear() {
20    this.listenerScreen.on('change', (mediaQueryResult) => {
21      this.isLandscape = mediaQueryResult.matches
22    })
23  }
24
25  toolBars: ToolBarItem[] = [
26    { defaultIcon: $r('app.media.home'), activeIcon: $r('app.media.home_select'), label: '首页' },
27    { defaultIcon: $r('app.media.project'), activeIcon: $r('app.media.project_select'), label: '项目' },
28    { defaultIcon: $r('app.media.interview'), activeIcon: $r('app.media.interview_select'), label: '面经' },
29    { defaultIcon: $r('app.media.mine'), activeIcon: $r('app.media.mine_select'), label: '我的' }
30  ]
31
32  @Builder
33  TabBarBuilder(item: ToolBarItem, index: number) {
34    Column({ space: 4 }) {
35      Image(this.activeIndex === index ? item.activeIcon : item.defaultIcon)
36        .width(24)
37      Text(item.label)
38        .fontSize(12)
39        .fontColor(this.activeIndex === index ? '#000' : '#aaa')
40    }
41  }
42
43  build() {
44    Tabs({
45      index: this.activeIndex,
46    }) {
47      ForEach(this.toolBars, (item: ToolBarItem, index: number) => {
48        TabContent() {
49          Text(index.toString())
50        }
51        .tabBar(this.TabBarBuilder(item, index))
52      })
53    }
54    .barPosition(this.isLandscape ? BarPosition.Start : BarPosition.End)
55    .vertical(this.isLandscape)
56    .barHeight(this.isLandscape ? 400 : 50)
57    .onChange(index => this.activeIndex = index)
58  }
59}

2. 手机侧适配

手机端等比例缩放适配设备

  • 知道等比例缩放原理
1// 安卓一般 360 设计稿 1080,面试宝典是 375
2const designWidth = 375
3// 书写尺寸 = 设备宽度 / 设计稿宽度 * 测量尺寸
4const useSize =  deviceWidth / designWidth * measureSize
  • 封装等比例缩放函数
common/utils/Basic.ets
1import display from '@ohos.display'
2import deviceInfo from '@ohos.deviceInfo'
3
4const designWidth = 375
5const devicePhysics = display.getDefaultDisplaySync().width
6
7export const vp2vp = (originSize: number) => {
8  // 不是PAD是手机等比例缩放
9  if ( deviceInfo.deviceType !== 'tablet' ) {
10    return px2vp(devicePhysics) / designWidth * originSize
11  }
12  return originSize
13}
  • 修改 Tabs 相关尺寸
1@Builder
2  TabBarBuilder(item: ToolBarItem, index: number) {
3    Column({ space: vp2vp(4) }) {
4      Image(this.activeIndex === index ? item.activeIcon : item.defaultIcon)
5        .width(vp2vp(24))
6      Text(item.label)
7        .fontSize(vp2vp(12))
8        .fontColor(this.activeIndex === index ? '#000' : '#aaa')
9    }
10  }
11
12  build() {
13    Tabs({
14      index: this.activeIndex,
15    }) {
16      ForEach(this.toolBars, (item: ToolBarItem, index: number) => {
17        TabContent() {
18          Text(index.toString())
19        }
20        .tabBar(this.TabBarBuilder(item, index))
21      })
22    }
23    .barPosition(this.isLandscape ? BarPosition.Start : BarPosition.End)
24    .vertical(this.isLandscape)
25    .barHeight(this.isLandscape ? vp2vp(400) : vp2vp(50))
26    .onChange(index => this.activeIndex = index)
27  }

3. 配置文件

  • 应用名称

  • 主题颜色

1)应用名称

resources/zh_CN/element/string.json
1{
2  "string": [
3    {
4      "name": "module_desc",
5      "value": "模块描述"
6    },
7    {
8      "name": "EntryAbility_desc",
9      "value": "前端面试宝典,包含常见面试题,和企业真实面经"
10    },
11    {
12      "name": "EntryAbility_label",
13      "value": "面试通"
14    },
15    {
16      "name": "reason",
17      "value": "获取通知权限"
18    }
19  ]
20}

2)主题颜色

resources/base/element/color.json
1{
2  "color": [
3    {
4      "name": "start_window_background",
5      "value": "#FFFFFF"
6    },
7    {
8      "name": "green",
9      "value": "#27AE60"
10    },
11    {
12      "name": "gray",
13      "value": "#979797"
14    },
15    {
16      "name": "black",
17      "value": "#121826"
18    },
19    {
20      "name": "gray_bg",
21      "value": "#F6F7F9"
22    },
23    {
24      "name": "orange",
25      "value": "#F2994A"
26    },
27    {
28      "name": "blue",
29      "value": "#3266EE"
30    },
31    {
32      "name": "blue_bg",
33      "value": "#EDF2FF"
34    }
35  ]
36}

4. 准备页面

  • 准备首页、项目、面经、我的四个 tabs 页面

  • 使用 页面组件

1)准备组件

views/Index/Home.ets
1@Component
2export struct Home {
3  build() {
4    Column(){
5      Text('首页')
6    }
7    .width('100%')
8    .height('100%')
9  }
10}
views/Index/Project.ets
1@Component
2export struct Project {
3  build() {
4    Column(){
5      Text('项目')
6    }
7    .width('100%')
8    .height('100%')
9  }
10}
views/Index/Interview.ets
1@Component
2export struct Interview {
3  build() {
4    Column(){
5      Text('面经')
6    }
7    .width('100%')
8    .height('100%')
9  }
10}
views/Index/Mine.ets
1@Component
2export struct Mine {
3  build() {
4    Column(){
5      Text('我的')
6    }
7    .width('100%')
8    .height('100%')
9  }
10}

2)使用组件

pages/Index.ets
1import { Home } from '../views/Index/Home'
2import { Project } from '../views/Index/Project'
3import { Interview } from '../views/Index/Interview'
4import { Mine } from '../views/Index/Mine'
5
6// ...
7
8        TabContent() {
9          if (index === 0) Home()
10          else if (index === 1) Project()
11          else if (index === 2) Interview()
12          else Mine()
13        }
14        .tabBar(this.TabBarBuilder(item, index))

5. 通用展示型组件

  • 搜索框组件

  • 打卡组件

  • 骨架屏组件

1)搜索框组件

common/components/IvSearch.ets
1import { vp2vp } from '../utils/Basic'
2
3@Component
4export struct IvSearch {
5
6  textAlign: FlexAlign = FlexAlign.Start
7
8  build() {
9    Row() {
10      Image($r('app.media.icon_public_search'))
11        .width(vp2vp(15))
12        .aspectRatio(1)
13        .fillColor($r('app.color.gray'))
14
15      Text('请输入关键词')
16        .fontSize(vp2vp(14))
17        .fontColor($r('app.color.gray'))
18        .margin({ left: vp2vp(3) })
19    }
20    .layoutWeight(1)
21    .height(vp2vp(28))
22    .backgroundColor($r('app.color.gray_bg'))
23    .borderRadius(vp2vp(14))
24    .padding({ left: vp2vp(10), right: vp2vp(10) })
25    .justifyContent(this.textAlign)
26  }
27}

2)打卡组件

common/components/IvClock.ets
1import { vp2vp } from '../utils/Basic'
2
3@Component
4export struct IvClock {
5  @Prop
6  clockCount: number = 0
7
8  build() {
9    Stack({ alignContent: Alignment.End }) {
10      Image(this.clockCount > 0 ? $r('app.media.clocked') : $r('app.media.unclock'))
11        .objectFit(ImageFit.Fill)
12      if (this.clockCount > 0) {
13        Column() {
14          Text('已连续打卡')
15            .fontSize(8)
16          Text() {
17            Span(this.clockCount.toString())
18              .fontWeight(600)
19              .fontSize(12)
20            Span(' 天')
21              .fontSize(10)
22          }
23        }
24        .width('50')
25      } else {
26        Text('打卡')
27          .width('50')
28          .textAlign(TextAlign.Center)
29          .fontSize(vp2vp(18))
30          .fontWeight(500)
31          .fontColor('#333C4F')
32          .margin({ bottom: vp2vp(4) })
33      }
34    }
35    .width(vp2vp(74))
36    .height(vp2vp(28))
37  }
38}
views/Index/Home.ets
1import { IvClock } from '../../common/components/IvClock'
2import { IvSearch } from '../../common/components/IvSearch'
3
4@Component
5export struct Home {
6  build() {
7    Column() {
8      Row({ space: 15 }) {
9        IvSearch()
10        IvClock()
11      }
12      .padding(15)
13
14      Text('首页')
15    }
16    .width('100%')
17    .height('100%')
18  }
19}

3)骨架屏组件

  • 背景渐变 linearGradient
1linearGradient({
2// 角度,默认 180
3angle?: number | string,
4// 颜色,比例
5colors: Array<[ResourceColor, number]>,
6})
  • 组件结构
common/components/IvSkeleton.ets
1import { vp2vp } from '../utils/Basic'
2
3@Component
4export struct IvSkeleton {
5  widthValue: number | string = 100
6  heightValue: number | string = 20
7  @State
8  translateValue: string = '-100%'
9
10  build() {
11    Stack() {
12      Text()
13        .backgroundColor($r('app.color.gray_bg'))
14        .height('100%')
15        .width('100%')
16      Text()
17        .linearGradient({
18          angle: 90,
19          colors: [
20            ['rgba(255,255,255,0)', 0],
21            ['rgba(255,255,255,0.5)', 0.5],
22            ['rgba(255,255,255,0)', 1]
23          ]
24        })
25        .height('100%')
26        .width('100%')
27        .translate({
28          x: this.translateValue,
29        })
30        .animation({
31          duration: 1500,
32          iterations: -1
33        })
34    }
35    .height(this.heightValue)
36    .width(this.widthValue)
37    .borderRadius(vp2vp(4))
38    .clip(true)
39    .onAppear(() => {
40      this.translateValue = '100%'
41    })
42  }
43}