Я слышал, что ваш апплет очень плавный?

JavaScript Апплет WeChat
Я слышал, что ваш апплет очень плавный?

предисловие

В последнее время из-за потребностей бизнеса необходимо начать новый проект апплета, поэтому у меня есть эта статья о выборе. В этой статье кратко описывается построение базовой структуры, а также некоторые проблемы совместимости и меры предосторожности. Добро пожаловать, чтобы дать указатели после прочтения.

Выбор

Хоть это и небольшой программный проект, учитывая, что в будущем могут быть дополнительные расширения (такие как H5, возможность пока совсем небольшая, но ее надо учитывать), тэги страницы написаны на нативном html, то есть традиционном div span и другие теги.

Я выбрал Uniapp для Taro и Uniapp.Во-первых, я чувствую, что экосистема Uniapp будет богаче и будет больше решений, ведь Uniapp основан на Vue, а у отечественного Vue тоже больше последователей.

Таро мало знает о времени из-за временных отношений.Хотя он поддерживает Vue, большинство решений основано на React, и большая часть команды знакома с Vue.Учитывая несогласованность стека технологий, он в итоге выбрал Uniapp .

Uniapp

После того, как основная структура действительно хороша, я использую шаблон Uniapp (Vue3.0 + TS), который поставляется с Vue-cli, в качестве технологического стека на этот раз.

vue create -p dcloudio/uni-preset-vue#vue3 <project Name>

После завершения создания структура выглядит следующим образом.

image.png

Просто настройте каталог

image.png

  • Здесь размещаются конкретные запросы API, которые также можно разделить по бизнес-модулю, разумеется.
  • активы — это статические ресурсы, в которых хранятся изображения.
  • компоненты общедоступный компонент.
  • константы Константы в основном используются для хранения некоторых часто используемых констант, таких как типы товаров (1: физические объекты, 2: виртуальные объекты) и т. д.
  • макет — это глобальный контейнер страницы.При разработке страниц самый внешний слой оборачивает слой макета, в основном для адаптации к различным моделям (таким как Liu Haiping из IphoneX), который можно адаптировать к безопасной области здесь, что будет обсуждаться позже.
  • lib не помечен выше (необязательный), он используется для хранения некоторых структур, определенных Ts.
  • Пакеты — это небольшой программный подпакет, который используется в качестве демонстрации для тестирования подпакетов.
  • страницы Бизнес-страницы.
  • Маршрутизация маршрутизатора.
  • Здесь реализованы метод обслуживания общедоступных запросов, перехватчик запросов и т. д.
  • статические статические ресурсы, забыли удалить, повторите вышеописанное, чтобы увидеть личные настройки именования.
  • хранить модуль управления глобальным состоянием, Vuex.
  • использует служебные функции.

Кратко объясните некоторые проблемы, которые могут возникнуть в мобильном терминале, например, общие компоненты, запросы и т. д. похожи, поэтому я не буду их повторять.

layout

Прежде всего, поговорим о смысле существования верстки.Он существует для адаптации к глобальной модели.При накрутке ему нужно только обратить внимание на бизнес-уровень, а адаптацию делать не надо.

Uniapp был очень дружелюбным и предоставил намgetSystemInfoSyncЭтот метод используется для получения системной информации, этот метод возвращаетsafeArea, в безопасной зоне под положительным направлением вертикального экрана, мы можем на этой основе поднять шумиху.

После этого проходимgetMenuButtonBoundingClientRectЧтобы получить информацию о местонахождении капсулы в правом верхнем углу апплета WeChat, достаточно просто адаптироваться к безопасной зоне.

/**
 * @feat < 获取 操作栏在视图上的top值 >
 * @param {number}  height 与小程序菜单按钮对齐的操作条高度
 */
export function getBarClientTop(height: number): number {
  let top: number;
  const { safeArea } = uni.getSystemInfoSync();
  const safeAreaInsets = safeArea ? safeArea.top : 0;

  // #ifdef  MP
  if (height > 0) {
    const menuBtnRect =
      uni.getMenuButtonBoundingClientRect &&
      uni.getMenuButtonBoundingClientRect();

    top = menuBtnRect.height / 2 + menuBtnRect.top - height / 2;
  } else {
    top = safeAreaInsets;
  }
  // #endif

  // #ifdef  H5 || APP-PLUS
  top = safeAreaInsets;
  // #endif

  return top;
}

Здесь значение высоты по умолчанию равно 44, что можно определить по фактическому результату. Мы получаем информацию о высоте через эти API для интервалов между нашими элементами.

.eslintrc.js

Так как проект разрабатывается на основе ts в среде Node, необходимо добавить глобальные правила uni и wx, чтобы ts мог нормально работать.

module.exports = {
  globals: {
    uni: true,
    wx: true,
  }
}

postcss

Так как набросок дизайна в 2 раза больше изображения (ширина наброска дизайна 750px, фактический размер 375px), то для адаптации под апплет нужно использовать rpx.

const path = require("path");
module.exports = {
  parser: require("postcss-comment"),
  plugins: [
    require("postcss-pxtorpx-pro")({
      // 转化的单位
      unit: "rpx",
      // 单位精度
      unitPrecision: 2,
      // 需要转化的最小的pixel值,低于该值的px单位不做转化
      minPixelValue: 1,
      // 不处理的文件
      exclude: /node_modules|components/gi,
      // 默认设计稿按照750宽,2倍图的出
      // 640 0.85
      transform: (x) => x * 2,
    }),
    require("postcss-import")({
      resolve(id) {
        if (id.startsWith("~@/")) {
          return path.resolve(process.env.UNI_INPUT_DIR, id.substr(3));
        } else if (id.startsWith("@/")) {
          return path.resolve(process.env.UNI_INPUT_DIR, id.substr(2));
        } else if (id.startsWith("/") && !id.startsWith("//")) {
          return path.resolve(process.env.UNI_INPUT_DIR, id.substr(1));
        }
        return id;
      },
    }),
    require("autoprefixer")({
      remove: process.env.UNI_PLATFORM !== "h5",
    }),
    require("@dcloudio/vue-cli-plugin-uni/packages/postcss"),
  ],
};

assets/css/constant.scss

Здесь определяются некоторые общие переменные стиля. Если на более позднем этапе есть функция скиннинга, ее можно легко переключить. Конкретные правила могут быть заданы дизайном.

// 主题色
$main-color: #EE5A61; // 品牌色
$sub-color: #FFA264; // 辅助色

// 背景色
$page-background-color: #F2F4F5; // 页面背景色
// More ...

Компоненты камеры и аудиокомпоненты

сборка камеры

Давайте сначала поговорим о компоненте камеры, потому что он включает в себя такие операции, как запись видео. И Uniapp также предоставляетcreateCameraContext, чтобы мы могли получить контекст камеры. Но этот API не совместим с H5 и App. Если вы хотите сделать мультиэнд с участием Н5, то это будет очень хлопотно, возможно потребуется настроить родную камеру (не вдаваться в подробности).

Этот компонент также не является полноэкранной камерой и сам по себе может управляться стилями. Итак, дана простая демонстрация.

<template>
  <LayoutMain>
    <template v-slot:mains>
      <div class="carmare-wrapper">
        <camera
          :device-position="cameraConfig.devicePosition"
          :flash="cameraConfig.flash"
          binderror="error"
          @error="handleOnError"
          style="width: 100%; height: 300px"
        ></camera>

        <button @click="handleTakePhoto">拍照</button>
        <button @click="handleStartReord">开始录像</button>
        <button @click="handleStopRecord">停止录像</button>
        <button @click="handleSwitchDevicePosition">
          切换摄像头朝向{{ cameraConfig.devicePosition }}
        </button>
        <button @click="handleSwitchFlashLight">
          {{ cameraConfig.flash }}闪光灯
        </button>

        <div v-if="photoList.length > 0">
          已拍出的照片
          <div v-for="(item, index) in photoList" :key="index">
            <img :src="item" alt="" />
          </div>
        </div>

        <div v-if="videoSrc">
          已录制的视频
          <video :src="videoSrc" style="width: 100px; height: 100px"></video>
        </div>
      </div>
    </template>
  </LayoutMain>
</template>

<script lang="ts">
import { onReady } from "@dcloudio/uni-app";
import { defineComponent, reactive, ref, Ref } from "vue";
import LayoutMain from "@/layout/layoutMain.vue";

export default defineComponent({
  setup() {
    let carmareContext: any;
    let videoSrc: Ref<string> = ref("");
    let currentFlashLightStatus = 0;
    const statusList = ["off", "on", "auto"];
    const cameraConfig: any = reactive({
      devicePosition: "back",
      flash: statusList[currentFlashLightStatus],
    });
    onReady(() => {
      carmareContext = uni.createCameraContext();
    });
    const photoList: Ref<string[]> = ref([]);
    return {
      cameraConfig,
      photoList,
      videoSrc,
      handleOnError: (error: any) => {
        console.error("handleOnError-eerror", error);
      },
      handleTakePhoto: () => {
        carmareContext.takePhoto({
          quality: "high",
          success: (res: any) => {
            console.info("res", res);
            photoList.value.push(res.tempImagePath);
          },
        });
      },
      handleSwitchDevicePosition: () => {
        console.info("cameraConfig.devicePosition");
        cameraConfig.devicePosition =
          cameraConfig.devicePosition === "back" ? "front" : "back";
      },
      handleSwitchFlashLight: () => {
        const lastStatus = statusList.length - 1;
        console.info("333handleSwitchFlashLight");
        if (currentFlashLightStatus < lastStatus) {
          cameraConfig.flash = statusList[(currentFlashLightStatus += 1)];
        } else {
          currentFlashLightStatus = 0;
          cameraConfig.flash = statusList[currentFlashLightStatus];
        }
      },
      // 开始录像
      handleStartReord: () => {
        carmareContext.startRecord({
          success: (res: any) => {
            console.log("handleStartReord-success", res);
            uni.showToast({
              title: "开始录像",
            });
          },
          fail: (error: any) => {
            console.error("handleStartReord-error", error);
          },
        });
      },
      // 停止录像
      handleStopRecord: () => {
        carmareContext.stopRecord({
          success: (res: any) => {
            console.log("handleStopRecord-success", res);
            uni.showToast({
              title: "停止录像",
            });
            videoSrc.value = res.tempVideoPath;
          },
          fail: (error: any) => {
            console.error("handleStopRecord-error", error);
          },
        });
      },
    };
  },
  components: { LayoutMain },
});
</script>

Нажмите на реальную отладку машины в инструменте разработчика WeChat, эффект будет таким, как показано на рисунке:

image.png

аудио компоненты

Кроме того, Uniapp также предоставляетcreateInnerAudioContext(), который создает и возвращает внутренний аудиоконтекстinnerAudioContextобъект.

API совместим с H5 и App. На стороне IOS этот компонент поддерживает меньше форматов, только форматы m4a, wav, mp3, aac, aiff и caf. Но это будет относительно больше на стороне Android. Он также дает краткий ответ Демо для отладки.

<template>
  <LayoutMain>
    <template v-slot:container>
      <button @click="handleStartRecord">开始录音</button>
      <button @click="handleEndRecord">结束录音</button>
      <button @click="handlePlay">播放录音</button>
      <button @click="handlePausePlay">暂停播放录音</button>
      <button @click="handlePausePlay">暂停播放录音</button>
      <button @click="handleEndPlay">结束播放录音</button>

      <div>
        操作记录

        <div v-for="(item, index) in operateRecordList" :key="index">
          {{ item }}
        </div>
      </div>
    </template>
  </LayoutMain>
</template>

<script lang="ts">
import { ref, onMounted, Ref } from "vue";

export default {
  data() {
    return {};
  },
  setup() {
    const operateRecordList: Ref<string[]> = ref([]);

    // let getRecorderManager;
    let recorderManager: any;
    let innerAudioContext: any;
    let voicePath: string;

    onMounted(() => {
      const current = (recorderManager = uni.getRecorderManager());

      operateRecordList.value.push("prending");

      current.onError(function (e: unknown) {
        uni.showToast({
          title: "getRecorderManager.onError",
        });

        console.error("getRecorderManager.onError", e);
      });

      current.onStart(function () {
        operateRecordList.value.push("开始录音");
      });
      current.onStop(function (res: any) {
        operateRecordList.value.push("结束录音");

        console.log("recorder stop" + JSON.stringify(res));
        voicePath = res.tempFilePath;
      });
      current.onPause(function () {
        operateRecordList.value.push("暂停录音");
      });
    });

    onMounted(() => {
      const current = (innerAudioContext = uni.createInnerAudioContext());

      current.obeyMuteSwitch = false;

      uni.setInnerAudioOption({
        obeyMuteSwitch: false,
      });

      current.onError((res) => {
        console.error("innerAudioContext-onError", res);
      });

      current.onPlay(() => {
        operateRecordList.value.push("开始播放");
      });
      current.onPause(() => {
        operateRecordList.value.push("暂停播放");
      });
      current.onStop(() => {
        operateRecordList.value.push("结束播放");
      });
    });

    return {
      operateRecordList,
      handleStartRecord: () => {
        recorderManager.start({
          duration: 60000, //录音的时长,单位 ms,最大值 600000(10 分钟)
          format: "mp3",
        });
      },
      handleEndRecord: () => {
        recorderManager.stop();
      },
      handlePlay: () => {
        innerAudioContext.src = voicePath;
        innerAudioContext.play();
      },
      handleEndPlay: () => {
        innerAudioContext.stop();
      },
      handlePausePlay: () => {
        innerAudioContext.pause();
      },
    };
  },
};
</script>


Запись и воспроизведение не очень удобно отображать, поэтому скриншотов нет, заинтересованные друзья могут их скопировать и использовать сами.

яма столкнулась

layout

Упомянутые выше компоненты макета обнаруживаются, когда работают компьютеры разных коллег, и есть несколько отдельных случаев, когда макет не действует, то есть страница не оборачивает слой макета.

Зарегистрируйтесь глобально в main.ts:

import { createApp } from "vue";
import Layout from "@/layout/layoutMain.vue";
import store from "@/store/index";
import App from "./App.vue";
const app = createApp(App);
app.use(store);
// 全局注册组件
app.component("Layout", Layout);
app.mount("#app");

Используйте на странице (здесь макет не вступает в силу, однако мой компьютер вступает в силу):

<template>
  <Layout>
    <template v-slot:mains>
      <div>分类页</div>
    </template>
  </Layout>
</template>

Это все еще тот случай, когда гарантируется, что среда такая же, плагины и все аспекты не затронуты.Пока неясно, что происходит.Я надеюсь, что кто-то может указать, что текущий план состоит в том, чтобыLayoutпереименовать вLayoutMainвступает в силу немедленно. (черный вопросительный знак?)

image

Ленивая загрузка компонента Image, который идет с Uniapp, не действует, этот момент проверен, и есть подозрение, что атрибут lazy-load является украшением QA Q.

Визуально мне приходится вручную реализовывать ленивую загрузку изображений.

О передаче компонента по значению

Предположим, у вас есть следующие компоненты:

<template>
    <div>{{ hello }}</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { Props } from "./interface";

export default defineComponent({
    props: {
        hello: {
          type: String as PropType<Props["hello"]>,
          default: 'fff',
        },
    }
})
</script>

Вы можете видеть, что hello имеет тип String, а значение по умолчанию — fff.

Но когда hello = undefined, hello будет отображать пустую строку "". "fff", если приветствие не передано.

наконец

Вы все это здесь видели, не ставите лайки и комментарии перед уходом?

Обратите внимание на паблик-аккаунт: передняя часть куков, сблизьте нас с вами (^▽^)