分类 资讯 下的文章

前言

某个普通的一天的早晨,水友群的小姐姐和我聊前端架构,因为她们组最近要筹备一些新项目,在做架构的中途出现了很多问题,所以我拿到了她们的架构项目脚手架代码。拿到代码之后我发现深圳那边的前端团队普遍做的很好,有先进的架构思想,也把ts用的很纯粹,最后没帮人家解决问题,反倒是自己学到了不少。最后我们聊到了前后端全栈开发,如何动态校验协议参数等问题,因为熟悉我开源项目(剑指题解)的朋友都知道,我的后端代码尤其是动态校验那块写的是真差,为了ts而用ts,这也是目前很多用ts的小伙伴的通病,所以我一直打算重构我的一部分后端代码,这个时候见多识广的小姐姐就推荐给我了一个框架,这个框架也是[see how]系列第一篇教程的主角,这个框架就叫做TSRPC

关于专栏

关于see how是什么,说来很巧,这也是TSRPC作者王大大对我的seho这个名字的猜测,其实我的一个名字也没那么多深意,然后被大佬解读成了see how,所以我感觉这是一个不错的idea,那么本来就是想要出一个tsrpc的系列教程,和大家一起学习这个优秀的框架,就正好作为see how 专栏的第一篇文章吧。

关于TSRPC

在正文开始之前,我希望大家可以去自行先去简单快速的浏览相关知识,tsrpc是一个ts的开源rpc框架,它是为了全栈项目而生的,从我上手的第一天开始,我就对这个框架有了以下的第一印象:

  1. 天然二进制传输
  2. 纯粹的ts,规避了极大部分开发中的错误
  3. 强大的运行时复杂检测
  4. 这种前后端开发模式,我闻所未闻

官方文档
视频教程

前期准备

学习tsrpc需要你有一些前置知识和其他准备:

  1. 熟悉typescript基本语法
  2. 准备一个mongodb数据库

开发

使用tsrpc开发全栈应用简单到没朋友,可以从官方提供的cli快速创建前后端一体项目:

npx create-tsrpc-app@latest

按照指引选择浏览器应用,等待完成安装之后,你的目录中会出现2个目录:

- backend 后端
- frontend 前端

我们直接一睹为快,在前端项目根目录运行

npm run dev

官方的脚手架为我们准备了一个简单的todolist应用

WX20210706-072702@2x.png

整个前后端的目录结构(摘抄官网)

|- backend --------------------------- 后端项目
    |- src
        |- shared -------------------- 前后端共享代码(同步至前端)
            |- protocols ------------- 协议定义
        |- api ----------------------- API 实现
        index.ts

|- frontend -------------------------- 前端项目
    |- src
        |- shared -------------------- 前后端共享代码(只读)
            |- protocols
        |- index.ts

诶,你可能会疑问了,为啥会有一个莫名其妙的shared目录,还要给前端项目去分享这个目录。是因为在shared这个目录我们要定义协议,啥玩意是协议呢?我们通过一个小小的接口来给大家解释什么是协议;

// PtlAddPost.ts

export interface ReqAddPost {
  newPost: {
     name: string;
  };
}

export interface ResAddPost {
  insertedId: string;
}

我们可以在shared/protocols中新建了一个文件PtlAddPost.ts,我们必须以Ptl进行开头定义协议,协议是用来描述一个接口的请求和响应的结构体的文件,你可以这么理解。协议文件通过shared目录共享到前端,你知道会发生什么事情吗?造成了我们前端在对接口的时候,全程代码提示以及严格和请求和返回类型校验。

那么我们接着后端继续聊,协议定义之后该如何做呢?

npm run proto 每当协议更改后,需要重新运行这个命令

tsrpc的设计是协议和api分离,我们必须要清楚,api在我的认知里就是一个异步函数,tsrpc可以帮助我们根据我们刚刚写的协议生成api,比如刚刚我们实现的PtlAddPost.ts,我们运行

npm run api 新协议生成一个新的api

在api目录中会多出一个ApiAddPost.ts

import { ApiCall } from "tsrpc";
import { ReqAddPost, ResAddPost } from "../shared/protocols/PtlAddPost";

export async function ApiAddPost(call: ApiCall<ReqAddPost, ResAddPost>) {
  
}

我们通过call这个方法获取请求参数以及响应给客户端一些信息,我们来一个简单的例子:

export async function ApiAddPost(call: ApiCall<ReqAddPost, ResAddPost>) {
  if(call.req.newpost.name){
     call.success({
         msg: "hello," + call.req.newpost.name
     })
  }else{
     call.error('Invalid name');
  }
}

我们的第一个api已经写完了,我们需要正常的过一次test,然后我们在让前端去调用。

tsrpc使用的是mocha这个测试框架。

// /test/api/**.test.ts

import { HttpClient } from "tsrpc";
import { serviceProto } from "../../src/shared/protocols/serviceProto";

// 1. EXECUTE `npm run dev` TO START A LOCAL DEV SERVER
// 2. EXECUTE `npm test` TO START UNIT TEST

describe("api 测试", async function () {
  let client = new HttpClient(serviceProto, {
    server: "http://127.0.0.1:3000",
    logger: console,
  });
  let ret = await client.callApi("AddTest", {
    newPost: {
      name: "seho"
    },
  });
});

这是我们后端的一个简单的测试用例,在运行这个测试用例之前,您必须要开启后端的服务:

npm run dev

然后可以再开启一个窗口运行npm run test,如果一切正常,你可以看到下面的控制台输出:

1625806333932.jpg

粗略计算了一下,我们从开始定义协议到api测试完成,一个简单的接口不到5分钟就已经完成。

这个时候我们可以把这个接口放到前端再继续测试一下。

当然在此之前,我们需要运行以下命令:

npm run sync

我们之前提到过,前后端有一个共享的目录,运行此命令我们就可以把协议等信息同步过来,这个时候我们可以在前端的index.ts文件中,可以获得非常完善的代码提示。

// frontend/src/index.ts

import { HttpClient } from "tsrpc-browser";
import { serviceProto } from "./shared/protocols/serviceProto";

// This is a demo code file
// Feel free to modify or clear it

// Create Client
let client = new HttpClient(serviceProto, {
  server: "http://127.0.0.1:3000",
  logger: console,
});

client.callApi("AddTest", {
  newPost: {
    name: "hello, seho"
  },
});

当我们回到浏览器前端页面上时,这个请求就会发出,如果你仔细观察控制台,会看到以下的场景:

1626042788250.jpg

我们的请求体被二进制序列化了,这也是tsrpc的特点之一,我们会在稍后的段落中对tsrpc各个特性做介绍,但是此时此刻我们已经完成了一个api的后端开发->test测试->前端调用。

完善我们的程序

上一个部分相信大家已经学会了如何使用tsrpc开发第一个api,这个部分结合了tsrpc的视频教程中的案例,我们需要做一个简单的CRUD,使用mongoDB。

我们需要在本地启动我们的mongoDB服务,然后我们需要添加一些代码到后端backend项目中。

代码开始之前,我们需要安装mongoDB的依赖,我们可以更方便的引入类型定义以及各种数据库方法。

npm install mongodb -s

为了和视频教程统一,我们的工具类/架构方式,将直接挪用视频教程中的代码:

  • 写一个数据库的表模型,名为Post.ts
// shared/protocols/models/Post.ts

export interface Post {
  _id: string;
  author: string;
  title: string;
  content: string;
  visitedNum: number;

  create: {
    uid: string;
    time: Date;
  };

  update?: {
    uid: string;
    time: Date;
  };
}

我们的数据库模型是需要共享到前端的,方便前端工程能够复用,但是为了确保后端的类型安全,我们需要在模型上多做一层处理。mongodb的id属性不是string,而是ObjectID,所以我们需要在后端对模型进行类型重写(只重写id字段)。

关于为什么要在后端多做一层封装是因为不可能在前端引入mongodb中的objectID

// shared/protocols/models/dbItems/DbPost.ts

import { ObjectID } from "mongodb";
import { Overwrite } from "tsrpc";
import { Post } from "../Post";

export type DbPost = Overwrite<Post, {
    _id: ObjectID
}>

我们使用tsrpc提供的Overwrite泛型对刚刚写的Post类型进行改写,将mongodb中的objectID类型引入进来进行替换,然后我们后端工程就要使用这个Dbpost类型,而不是刚刚我们写的Post类型。

  • 数据库相关配置
// 配置文件
// shared/protocols/models/BackConfig.ts (models文件夹是新建的)
export const BackConfig = {
  // 替换数据库url,数据库名test
  mongoDb: "mongodb://localhost:27017/test",
};
  • 定义数据库初始化类
// 全局一个类,里面写了初始化数据库的内容
// shared/protocols/models/Global.ts

import { Collection, Db, MongoClient } from "mongodb";
import { Logger } from "tsrpc";
import { BackConfig } from "./BackConfig";
import { DbPost } from "./dbItems/DbPost";

export class Global {
  static db: Db;

  static async init(logger?: Logger) {
    logger?.log(`Start connecting db...`);
    const client = await new MongoClient(BackConfig.mongoDb).connect();
    logger?.log(`Db connected successfully...`);
    this.db = client.db();
  }

  static collection<T extends keyof DbCollectionType>(
    col: T
  ): Collection<DbCollectionType[T]> {
    return this.db.collection(col);
  }
}

export interface DbCollectionType {
  Post: DbPost;
}
  • 改写后端index.ts
// backend/index.ts 添加如下内容

import { Global } from "../src/shared/protocols/models/Global";

async function main() {
    // Auto implement APIs
    await server.autoImplementApi(path.resolve(__dirname, 'api'));
    // TODO:在这里初始化了数据库
    await Global.init(server.logger);
    await server.start();
};

ok,截止到目前,我们把第一张表的相关配置已经搞定了,请确保数据库已打开且配置正确,然后我们直接运行
一下服务器:

npm run dev

如果你运气好(狗头),那么你应该是成功开启这个服务器,并且控制台能看到连接成功的信息:

WX20210719-223641@2x.png

然后我们快速开发一下新增API,其他的更新和删除API,希望能大家举一反三,自行开发。

// shared/protocols/PtlAddPost.ts

import { Post } from "./models/Post";

export interface ReqAddPost {
  newPost: Omit<Post, "_id" | "create" | "update" | "visitedNum">;
}

export interface ResAddPost {
  insertedId: string;
}

我们规定的请求类型是只能让客户端传递除了id,create,update,visitedNum的Post类型。
然后我们还是运行那几个熟悉的命令:

npm run proto
npm run api
// api/ApiAddPost.ts

import { ApiCall } from "tsrpc";
import { Global } from "../shared/protocols/models/Global";
import { ReqAddPost, ResAddPost } from "../shared/protocols/PtlAddPost";

export async function ApiAddPost(call: ApiCall<ReqAddPost, ResAddPost>) {
  let op = await Global.collection("Post").insertOne({
    ...call.req.newPost,
    create: {
      uid: "xxx",
      time: new Date(),
    },
    visitedNum: 0,
  });
  call.succ({
    insertedId: op.insertedId.toHexString(),
  });
}

这一part完成~

如何做到动态类型校验

之前我们就提到过,前端在调用后端的api时候,会给出完整的代码提示,从api名称到api的请求体类型等等,那么这一定程度上杜绝了开发中常见的接口联调不细心的问题。在传统的前后端开发中,尤其是分离模式,有一个非常常见的问题就是动态类型校验。每个语言/框架都有自己类型校验的手段,比如springmvc我们可以通过注解的方式来校验(下面展示了控制器中的校验,还有其他校验手段):

@Controller
@RequestMapping("valid")
@Slf4j
public class ValidateController {
 private static final String BASE_PATH = "/valid/";
 @RequestMapping("index")
 public String index(@Validated() Student student,BindingResult result){ 
        return BASE_PATH + "index";
    }
}

那么tsrpc是如何保证数据传输的正确性的呢,首先我们如果在前端使用tsrpc的浏览器请求包,我们调用api时候不仅会在开发中提示开发者这个字段是错误的,而且会在请求发出之前做前端方面的遏制。在后端请求到达异步函数之前,也会去做第三次校验;所以我们在后端异步函数中使用到的参数一定是类型安全,完全不需要担心安全问题。

市面上有很多js领域解决动态校验的方案;最常见应该就是json schema,可以基于json自己实现一套校验方法可以在运行时来做校验。但是仍然有很多缺点,比如不能在前端进行运行时提示且可能重复写很多类型定义。那么tsrpc核心中使用到了一个库(这个库也是同个作者开发的):

tsrpc-buffer

为了实现ts动态类型校验,不可能把整个ts加进去,因为那有足足60m多,这是不现实的。所以作者开发了这个库。tsrpc依赖了这个库,它对ts的语法进行了兼容,目前支持了大部分的ts的写法,包括我们常用的string,number等,还支持一些复杂的泛形。

如果你想细细了解这方面,可以看一下文档支持的ts类型有哪些

当然,随着ts的更新,这个buffer也会支持更多的ts类型,可以做更完善的全栈应用。而且我们可以使用tsrpc进行原汁原味的ts开发,市面上的第三方工具/框架需要借助另外编程语言/DSL,tsrpc-buffer完全让你使用ts,你不会感觉到一丝违和感。

二进制序列化

tsrpc的二进制序列化机制是由我们上文中提到的tsrpc-buffer中实现的,那么这个特性带给我们的是比json更小的传输体积且支持更多的数据类型,ArrayBuffer, Date等。这意味着使用tsrpc的全栈应用在应对上传图片这种业务的时候简直就像是小儿科,我们可以用一个例子来证明。

// 定义一个协议在后端,PtlUpload.ts
export interface ReqUpload {
    fileName: string,
    fileData: Uint8Array
}

export interface ResUpload {
    url: string;
}

我们通过刚刚学到的一些命令,来生成协议以及api

npm run proto
npm run api
// api实现, 先提前把uploads文件夹建立好,或者使用mkdir方法
import { ApiCall } from "tsrpc";
import { ReqUpload, ResUpload } from "../shared/protocols/PtlUpload";
import fs from "fs/promises";

export async function ApiUpload(call: ApiCall<ReqUpload, ResUpload>) {
  await fs.writeFile("uploads/" + call.req.fileName, call.req.fileData);
  call.succ({
    url: "http://127.0.0.1:3000/uploads/" + call.req.fileName,
  });
}

为了让前端调用,同步shared下的协议

npm run sync

写一个简单的file选择器在index.html中

<input type="file" id="fileInput">
// index.ts
import { HttpClient } from "tsrpc-browser";
import { serviceProto } from "./shared/protocols/serviceProto";

// Create Client
let client = new HttpClient(serviceProto, {
  server: "http://127.0.0.1:3000",
  logger: console,
});

const input = document.getElementById("fileInput") as HTMLInputElement;

input.addEventListener("change", async () => {
  if (input.files) {
    const fileData = await loadFile(input.files?.[0]);
    upload(fileData, input.files?.[0].name);
  }
});

const upload = async (fileData: Uint8Array, fileName: string) => {
  const fr = new FileReader();
  client.callApi("Upload", {
    fileData,
    fileName,
  });
};

function loadFile(file: File): Promise<Uint8Array> {
  return new Promise((rs) => {
    let reader = new FileReader();
    reader.onload = (e) => {
      rs(new Uint8Array(e.target!.result as ArrayBuffer));
    };
    reader.readAsArrayBuffer(file);
  });
}
// 同步到前端
npm run sync

开发完毕,我们可以仔细看一下控制台:

1626218954109.jpg

1626218957442.jpg

尽管我们在日常开发中会用到一些组件库,组件库帮助我们做了上传的大部分工作,所以我们写原生的上传可能在代码量上更多,但是省去了前后端转换Formdata的时间。

向后兼容http(json)和WebSocket

tsrpc也向后支持json,我们可以在客户端进行一个简单的配置,发送的请求就是json啦:

// Create Client
let client = new HttpClient(serviceProto, {
  server: "http://127.0.0.1:3000",
  json: true,
  logger: console,
});

1626391401179.jpg

我其实暂时没有想到非要使用json的场景,使用二进制序列化比json体积更小传输更快,本地开发的日志也在控制台随时打印,所以我还是建议大家使用默认的二进制序列化的传输模式。

tsrpc设计之初是为了游戏,因为传输特性能让websocket更高效,我们可以用tsrpc简单做一个websocket-demo,具体实现我参考了官网的实现,如果你想直接了解官网的这一part的内容,直接移步:

websocket实时服务-tsrpc

tsrpc的实现和协议无关,意味着咱们之前写的代码都可以用,仅仅做一个简单的调整替换即可。

websocket的消息是tsrpc传输中最小单元,我们需要用另外一个方法去定义协议,我们的websocket例子如下:

客户端发起一个请求,服务端接收并且向所有客户端发送一个消息

首先我们需要定义一个MsgHello.ts这样的协议:

// shared/protocols/MsgHello.ts

export interface MsgHello {
  time: Date;
  content: string;
}

这个协议规定了前后端通讯的请求体。

我们需要改写后端backend中的index.ts,将原先的HTTP服务,改成Websocket服务

import { HttpServer, WsServer } from "tsrpc";

export const server = new WsServer(serviceProto, {
    port: 3000,
    logMsg: true
});

这里导出server是有用意的,我们将在之后的代码中会用到这个server。

改写frontend前端中的index.ts

import { HttpClient, WsClient } from "tsrpc-browser";

let ws = new WsClient(serviceProto, {
  server: "ws://127.0.0.1:3000",
  logger: console,
});

const init = async () => {
  // 与后端webscoket服务建立连接
  let result = await ws.connect();
};

init();

我们需要一个api来触发后端给client发送websocket消息:

// shared/protocols/PtlSend.ts

export interface ReqSend {
  content: string;
}

export interface ResSend {
  time: Date;
}

定义成功后,我们运行以下几个命令:

npm run proto
npm run api

运行成功,我们可以在api文件夹下的ApiSend.ts中写入以下内容:

import { ApiCall } from "tsrpc";
// 这里引入banckend/index.ts 导出的server
import { server } from "..";
import { ReqSend, ResSend } from "../shared/protocols/PtlSend";

export async function ApiSend(call: ApiCall<ReqSend, ResSend>) {
  const time = new Date();
  call.succ({
    time,
  });
  // 广播给所有客户端
  server.broadcastMsg("Hello", {
    content: call.req.content,
    time,
  });
}

我们的后端逻辑写完了,我们运行以下命令,将协议同步到前端

npm run sync

我们进一步改写前端frontend/src/index.ts:

import { HttpClient, WsClient } from "tsrpc-browser";
import { serviceProto } from "./shared/protocols/serviceProto";

let ws = new WsClient(serviceProto, {
  server: "ws://127.0.0.1:3000",
  logger: console,
});

const init = async () => {
  let result = await ws.connect();
  console.log(result)
  if (result.isSucc) {
    // ws.callApi
    ws.callApi("Send", {
      content: "hello websocket",
    });
  }
};

init();

我们在页面初始化的时候,向后端发送刚刚写好的SendApi,这个时候我们既能收到api的返回,也能收到websocket的消息推送。

WX20210719-073153@2x.png

WX20210719-073210@2x.png

可以看到websocket传输也是二进制的,我们在开发中,也能发现,无论是callApi和发送websocket通知,从始至终都有类型推导,永远不会在传输中出现类型上的错误,这就是tsrpc的强大之处。

多平台

tsrpc支持多个平台,支持浏览器/小程序/原生ios 安卓/nodejs,甚至它还支持serverless,可以使用tsrpc开发基于阿里云/腾讯云的云函数;在后续我也会对tsrpc生态开发更多插件,使其兼容uniapp&unicloud,让他严格严格意义上跨多端,我相信tsrpc可以改变unicloud的开发习惯,让全栈应用更简单。

结语

本篇文章所有的知识点均在官网&视频教程有体现,视频教程在文章开始之前就有链接,非常希望大家能够先去看一下那个视频。tsrpc的教程还会出,下一篇关于tsrpc文章主要还是讲一下如何和serverless(unicloud)融合。这篇文章正在写大纲,相信也会在这个月之内能和大家见到。

剑指题解


互联网人题解神器,神器在手,offer我有

随手刷题 | 无广告绿色免费 | 个性推荐 | 每日自动推送 | 多维度丰富领域知识

项目的起源是一个 github 仓库,名为前端知识每日 3+1,正是这个优秀的题库项目才有了我们团队开发聚合题库的想法。在我们国内程序员圈子,大多数小伙伴为了更好的技术发展和 offer,都通过训练题来提高自己的眼界,其中不乏包括算法/业务/软技能的题目,但是因为我们的环境和侧重点不一样,所以我们接触的项目和题目都不一样,而市面上很少有一个项目能做到,涵盖不同圈子的聚合题库。 《剑指题解》项目就是一个优秀的互联网行业聚合题库神器。


开源免费不易,请关注我们的公众号以及开源仓库

沈昊Seho/InternetQuestionBank<br/>
Github 点 star, 拉取代码时请选择最新分支
Dcloud 插件市场支持我们

目录

特点

  1. 汇聚优秀的开源题目,以技术社区为主线展开的刷题 APP
  2. 根据用户的喜好,去推送相关的最新题目,不会再刷到 “牛头不对马嘴”无用的题目
  3. 我们绑定了服务号,每日定时推送给用户最新的题目,让你能在每天清晨就开启活力新一天
  4. 刷题 APP 无广告,无硬性推广,一切盈利皆在赞助/官网流量
  5. 我们还有很多有意思的待开发 feature,比如大佬带萌新模式,还将开启校园/企业的通道,让每一个学生/员工都能每时每刻提高自己
  6. 剑指题解开源项目,从原型图到小程序,UI 图,UML,API 文档,第三方 API 对接说明书,使用说明都将免费开源,欢迎各位同僚二开,让这个行业变得更好,我们将用心地辅助你们解决在技术上的难题。

技术栈

关于技术栈的选型可以参考我们的语雀文档
周边物料的开源,还需要感谢 mockPlus 以及语雀:

<img class="imgIcon" src="http://static.yinzhuoei.com/typecho/2021/01/17/69752764374919.png" width="10%">


<img class="imgIcon" src="http://static.yinzhuoei.com/typecho/2021/01/17/697832282748067.jpg" width="20%"></img>

UML

由于此部分在开发新 feature 的工作中可能会进行变更或者本就有实质性的错误,欢迎向我们反馈错误,我们将感激不尽。

类图

拉取任务

更多

更多相关资料请移步
out/doc或者语雀-UML

设计概览



设计源文件请移步:点击查看-为了您的体验,请务必下载最新版本的设计图
原型图在线预览: 点击查看, 如果失效请联系我们

API

《剑指题解》团队在 API 层面,开源的内容非常有限制,因为涉及到多位合作伙伴的题库授权,所以如果您要使用聚合题库 API 请联系我们商议,如果您是学习者,那么在语雀中的 API 文档会帮助到您,在这里我们将简单介绍。

uniapp 的云函数开发,我们是第一次接触,所以在此之前我们做了很多调研,在下文也提到了关于 explain.js 这个框架的作用,我们也是非常感谢 uniapp 开源社区有这样创作质量的第一批的 unicloud 框架开发者,在咨询了官方人员之后,我们了解到如果要使用 ts 来开发,需要把 ts 编译成 js 来放在云函数目录,所以我们选择了以下的技术栈,开发者可以很爽的使用 ts 来开发 unicloud 云函数:

  • esbuild-node-tsc
  • nodemon

esbuild-node-tsc 这个工具是基于 esbuild 的,利用其特性,我们可以比 ts 官方提出的 tsc 编译工具更快,快几十倍到上百倍,这也是得益于 esbuild 的使用 go 语言直接编译的特性。

nodemon 可以帮助我们监听目录文件,一旦改变就执行打包 ts 命令直接将编译好的 js 放在指定目录
所以我们把 explain.js 的配置也相应的改变,我们在 services 目录中存储的是我们 ts 文件,在 dist 中存储的是我们编译好的 js 文件,而 dist 目录就是 explain 需要的:

config.init({
  baseDir: __dirname,
  serviceDir: "/dist/",
});

需要注意的事情是:
由于云函数大小限制,我们需要把相关依赖全部安装到全局,包括 typescript

读写职责分离

读写职责分离模式(CQRS)是一种把查询(Queries) 数据和和更新(Commands) 数据通过使用各自独立的接口分开的模式。
Uniapp 的 Unicloud 很好,其中的 jql 也是大开眼界,确实统一了前端开发操作数据库的体验,让我们前端开发操作数据库非常友好,但是我们使用 CQRS 将这些原本封装的很好的 API 不予以使用。

  • 前端使用 jql 去读取想要的数据
  • 写入操作就交给云函数去执行

如图,前端读取一个列表数据

const db = uniCloud.database();
const databaseName = "testInit";
// 获取测试数据列表
export function getTestList() {
  return new Promise((resolve) => {
    db.collection(databaseName)
      .get()
      .then((res) => {
        // res 为数据库查询结果
        resolve(res);
      })
      .catch((err) => {
        // err.message 错误信息
        // err.code 错误码
      });
  });
}

我们将这样的文件抽出了一个一个查询模块,每一个模块对应了一个数据表(这里是 testInit)封装在了 API 这个文件夹中。

Vue 页面将这样去调用,这样我们在 API 查询层将做好数据的处理,确保 Vue 拿到的是可以直接渲染的干净数据。

import { getTestList } from "../../api/test";
const data = getTestList();

我们写入操作将通过云函数直接调用,唯一设计不同的地方在于,我们将云函数这个概念改变了,应该是模块化,我们把一个一个函数变成了模块。

在我们的初步技术调研过程中,由于 Uni 官方并没有提供给用户云函数开发框架,但是我们在插件市场中找到了一款名为 explain 的开发框架,它可以迅速的帮助我们实现 restapi 风格的单路由云函数,这款框架我们不多做介绍,文档在这里:explain.js 快速开发 uni 云函数的框架

// 注册用户根据手机号
  addUserByPhone() {
    return handleMustRequireParam(
        [{
            key: "username",
            value: "用户名",
          },
          {
            key: "password",
            value: "密码",
          },
        ],
        this.event.params
      )
      .then(async () => {
        const {
          username,
          password
        } = this.event.params;
        if (!/^1[3456789]\d{9}$/.test(username)) {
          return appErrorMessage("用户名格式不正确");
        } else if (password === "" || password.length < 6) {
          return appErrorMessage("密码格式不正确");
        } else {
          // 校验手机号
          return await uniID.register({
            username,
            password,
          });
        }
      })
      .catch((err) => {
        return err;
      });
  }

那么我们如果要调用 testPrint 这个模块中的增加操作

uniCloud.callFunction({
  name: "application",
  data: {
    route: "api/user",
    method: "POST",
    params: {
      username: "18291563764",
      password: "sas",
    },
  },
});

贡献者

感谢为《剑指题解》这个优秀项目贡献自己一份力的小伙伴们:

如果您想加入到我们的贡献者队列中,请联系我们,这里还有相关贡献者的介绍,希望对您有帮助

核心维护者

相关开源物料

题库合作伙伴

QQ 群和钉钉群欢迎大家加入


许可

Apache License © 剑指题解
如果您在其他有关我们的文档见到了与之不符合的协议内容,请联系我们,这可能是我们的历史遗留的代码问题。

Nginx是一款高性能服务器,最近这几年非常火,以轻量且高并发,高性能著称,那么此笔记将不会从0开始讲解API,而是会从各种问题入手,通过问题学习nginx。


特点:

  1. IO多路复用
  2. 高性能
  3. 高并发
  4. 占用系统资源少

Untitled.png

Nginx作为一个WEB服务器,有着大好的未来,市场份额非常给力,同时也是份额上升速度最快的web服务器。

Untitled.png

Nginx作为前端来说,需要学习什么?我们只需要学习Nginx在应用部署,反向代理,处理资源的进程,亦或者是搭建网站的基础知识,如果你还没有一个blog,那么就从现在开始学习nginx并且搭建你的第一个网站吧。


反向代理与正向代理

Untitled.png

我们在平时上外网的时候,比如谷歌,youtube,twitter,Ins等,如果使用我们内地网络,是访问不成功的,只有在香港台湾或者境外才能访问到类似的外网。那我们需要通过内地网络去访问外网只能通过一个proxy代理去做一个请求的转发,我们的内地网络请求在到达外网地址之前,会经过一层代理,这个代理会去请求外网,请求成功之后会把页面呈现给我们的客户端。

在这个过程中,外网服务器不知道我们的内地网络是谁,只知道代理地址,所以对于外网服务器来说,请求的真实客户端是看不到的。那么这个过程就叫做 正向代理,proxy代理的是客户端。

Untitled.png

反向代理是相反的,代理的是服务端,对于客户端而言,访问的服务器仅仅是多个真实服务器的一个代理而已,所以对于客户端用户而言,真实服务器的信息是不可见的。这样的过程也就是反向代理,proxy代理的是服务端

Nginx如何去做反向代理?

server{
        listen 80;
        server_name nginx.yinzhuoei.com;
        location / {
               proxy_pass http://yinzhuoei.com;
        }
}

其他的proxy配置:

proxy_set_header :在将客户端请求发送给后端服务器之前,更改来自客户端的请求头信息。
proxy_connect_timeout:配置Nginx与后端代理服务器尝试建立连接的超时时间。
proxy_read_timeout : 配置Nginx向后端服务器组发出read请求后,等待相应的超时时间。
proxy_send_timeout:配置Nginx向后端服务器组发出write请求后,等待相应的超时时间。
proxy_redirect :用于修改后端服务器返回的响应头中的Location和Refresh。

解决跨域

通过反向代理解决跨域:

server
{
   listen 3003;
   server_name localhost;
      ##  = /表示精确匹配路径为/的url
   location = / {
       proxy_pass http://localhost:5500;
   }
   ##  若 proxy_pass最后为/ 如http://localhost:3000/;匹配/no/son,则真实匹配为http://localhost:3000/son
   location /no {
       proxy_pass http://localhost:3000;
   }
   ##  /ok/表示精确匹配以ok开头的url,/ok2是匹配不到的,/ok/son则可以
   location /ok/ {
       proxy_pass http://localhost:3000;
   }
}

加header头允许跨域:

server
{
    listen 3002;
    server_name localhost;
    location /ok {
        proxy_pass http://localhost:3000;

        #   指定允许跨域的方法,*代表所有
        add_header Access-Control-Allow-Methods *;

        #   预检命令的缓存,如果不缓存每次会发送两次请求
        add_header Access-Control-Max-Age 3600;
        #   带cookie请求需要加上这个字段,并设置为true
        add_header Access-Control-Allow-Credentials true;

        #   表示允许这个域跨域调用(客户端发送请求的域名和端口) 
        #   $http_origin动态获取请求客户端请求的域   不用*的原因是带cookie的请求不支持*号
        add_header Access-Control-Allow-Origin $http_origin;

        #   表示请求头的字段 动态获取
        add_header Access-Control-Allow-Headers 
        $http_access_control_request_headers;

        #   OPTIONS预检命令,预检命令通过时才发送请求
        #   检查请求的类型是不是预检命令
        if ($request_method = OPTIONS){
            return 200;
        }
    }
}

Master&Woker模式

Nginx启动之后,启动了80端口进行服务监听,那么进程中就存在一个Mater主进程和多个Woker进程;

Untitled.png

Master进程的作用就是:读取&验证nginx.conf配置文件并且管理多个woker进程;接受外部信号;监控Woker,如果Woker挂掉,将自动重启Woker;

Woker进程的作用就是:多个Woker会拦截所有的请求并做出处理;每一个woker进程维护一个线程;woker的个数和CPU有关,从nginx.conf配置woker个数,配置几个就是几个,但是要避免配置过多,要充分利用CPU;

一个请求到响应的流程:

  1. Nginx启动,Matster进程根据nginx.conf初始化;初始化监听socket;fork出多个woker进程;
  2. 发起请求
  3. woker进程们一起竞争,胜出者通过三次握手,建立socket连接,处理请求。

如何做热部署呢?

热部署就和前端热部署一样的性质,即修改配置文件,不需要重启服务器就可以使用最新的配置。

nginx -s reload

通过这样的一个命令即可热部署,无需重启,随时改随时用。

一般情况下,我们做热部署可以有几个方案,比如前端,webpack的本地开发工具,webpack-dev-server,即本地启动一个服务,开启一个websocket,当我们的文件改动,就重新加载这个css/js。

而nginx也是同样的方式么?我们的主进程master去发布一个修改请求,然后woker去订阅这个消息,实现类似这样的热部署?

其实不然,nginx使用的是如下的方案,当master监听到配置文件的更改,会创建一批新的woker去执行新的请求,老的woker进程会在任务处理完毕之后,再由master杀掉进程。


如何做到高并发?

Nginx采用多进程+异步非阻塞方式(IO多路复用):

关于异步和同步,我需要做一些概念上的整理;

同步和异步指的是消息的通信机制,我们做web开发是最能理解同步异步的区别的,因为我们天天和接口打交道;

1)所谓同步指的就是发起一个请求/调用,在没有得到结果之前就不会返回,一旦得到结果就立即返回;

2)所谓异步指的就是发起一个请求/调用,调用者不会主动去care被调用者,而被调用者拿到结果之后会通知调用者

而阻塞非阻塞指的是程序在等待调用结果时的状态

1)阻塞调用指的就是,结果返回之前当前线程被挂起,调用线程在返回之后才返回;那么挂起的这个线程是会被阻塞的;

2)非阻塞调用指的就是,不能立刻得到结果之前,线程是不会被挂起的,仍然可以做其他事情;那么非阻塞调用如何知道得到结果了呢,需要定时去check的;

关于阻塞IO和非阻塞IO等我总结完了再说哈,还有关于Nginx的IO多路复用Epoll模型,这个是延申知识了,我也需要学习整理哈,现在还不清楚这一块的东西。

Nginx后续章节过段时间发,中间要发几篇shadowDom和剑指题解的文章,大家耐心等待...


学习资料如下:

nginx如何做到高并发?
8分钟带你深入浅出搞懂Nginx
理解同步异步&阻塞非阻塞

微信截图_20210331210548.png

好家伙,他闭着眼睛写Vue3

一个风和日丽的下午,我用了3个小时写了一个Vue3的小应用,这个应用小到不足为奇,但是Vue3的开发体验最值得我吹一波,这个小应用在登录我用了Vuex4,路由管理用到了最新的VueRouter4,而UI框架选择的是Vant3,有赞团队的Vant3最新版也是目前为止Vue3支持度最高的移动UI库,放一个项目预览GIF图

整个的开发体验,我总结了一下:有近似ReactHook的开发体验,又保留了Vue2的原汁原味,同时因为Vue3的本身设计原因,也提升了在react中没有的hook开发体验,所以如同标题,如果开发者拥有reactHook开发经验和vue2开发经验,可谓是“闭着眼睛”都能撸Vue3。

Github地址:https://github.com/1018715564/PlanListForVue3


main.js

新版的vue一改以前的构造函数去挂载,而是把函数风格贯彻到底了,在入口文件中我们使用链式调用,一直use一直爽。

import { createApp } from "vue";
import App from "./App.vue";
import Router from "./router";
import Vuex from "./store";
import Vant from "vant";
import "vant/lib/index.css";

createApp(App).use(Router).use(Vant).use(Vuex).mount("#app");

Router

import { createRouter, createWebHistory } from "vue-router";
import Login from "../pages/Login/Index.vue";
import Index from "../pages/Index/Index.vue";
const routerHistory = createWebHistory();
const Router = createRouter({
  history: routerHistory,
  routes: [
    {
      name: "Index",
      path: "/",
      component: Index
    },
    {
      name: "Login",
      path: "/Login",
      component: Login
    }
  ]
});

export default Router;

可以看到Router还是保留了vueRouter3的路由配置,但是创建路由的方式改为了createRouter,原先的路由模式也从原来的mode: "history"也改为了通过函数引入:

"history":createWebHistory()
"hash":createWebHashHistory()
"abstract":createMemoryHistory()

Vuex

import Vuex from "vuex";
export default Vuex.createStore({
  state: {
    user: null,
    planList: []
  },
  mutations: {
    // 删除一个计划
    deletePlan(state, index) {
      state.planList.splice(index, 1);
    },
    // 新增一个计划
    addPlan(state, plan) {
      state.planList.push(plan);
    },
    setUser(state, user) {
      state.user = user;
    }
  },
  actions: {},
  modules: {}
});

我们通过vuex4中的createStore创建了实例,其中我们定义了一些方法和state,这些我们在登录和添加删除计划时会用到,下来我们再看看如何在页面中使用Vue3的组合API以及路由和状态管理。

登录页面

import { reactive } from "vue";
import { useRouter } from "vue-router";
import { useStore } from "vuex";
import { Toast } from "vant";
export default {
  name: "App",
  setup() {
    const store = useStore();
    const router = useRouter();
    // 用户名和密码
    const Form = reactive({
      username: "",
      password: "",
    });
    // 登录
    function handelLogin() {
      store.commit("setUser", {
        username: Form.username,
        password: Form.password,
      });
      Toast(`登录成功,你好: ${Form.username}, 请添加你的计划吧~`);
      // 跳转到首页
      router.push({
        path: "/",
      });
    }
    return {
      Form,
      handelLogin,
      handelNavBack,
    };
  }

我们使用路由以及状态管理需要从2个包中引入对应的hook去调用

调用vuex中的mutation:

store.commit(fnName, args);

路由也沿用了VueRouter3中一些老API,replace, go, back等方法都基本不变

 const router = useRouter();
 router.push({
   path: "/",
 });

如果从来没有使用过组合API的开发者,应该需要了解一下前置知识,例如在老版本中,我们定义变量和方法是这样:

data(){
  return {
    count: 0
  }
},
methods: {
  back(){
    return "It is back"
  }
}
{{count}} //在template中这样渲染

我们在vue3中需要把这些东西写到setup这个函数中,包括变量,函数,监听,计算属性,生命周期等等,然后把变量return出去供模板使用,很显然我们在登录时候用了reactive这个API,它可以传入一个普通对象,返回一个响应式的代理,我们用这个对象中的用户名和密码与视图绑定最后调用vuex的方法即可让全局状态管理知道此时 “计划APP” 是有身份进入的。

const Form = reactive({
  username: "",
  password: "",
});

// 调用vuex登录方法
store.commit("setUser", {
  username: Form.username,
  password: Form.password,
});

首页

import { ref, reactive, computed, watchEffect } from "vue";
import { useStore } from "vuex";
import { useRouter } from "vue-router";
import { Toast, Dialog } from "vant";

export default {
  name: "Index",
  setup() {
    const store = useStore();
    const router = useRouter();
    const planList = store.state.planList;
    const addPlanForm = reactive({
      title: "",
      remark: "",
    });
    // 添加的弹出层
    const addPopup = ref(false);
    // 计算计划的个数
    const planCount = computed(() => store.state.planList.length);
    // 如果没登录重定向到登录
    if (store.state.user === null) {
      Toast("未登录,请先登录");
      router.replace("Login");
    }
    const handelNavBack = () => {
      router.go(-1);
    };
    const handelAddPlan = (e) => {
      store.commit("addPlan", {
        ...e,
        id: Math.random().toString(36).substr(2),
        date: new Date().toDateString(),
      });
      Toast("添加计划成功");
      addPopup.value = false;
      addPlanForm.title = "";
      addPlanForm.remark = "";
    };
    const handelDeletePlan = (index) => {
      Dialog.confirm({
        title: "提示",
        message: "您缺点要删除此计划吗?",
      }).then(() => {
        store.commit("deletePlan", index);
        Toast("删除计划成功");
      });
    };
    return {
      handelNavBack,
      planList,
      addPopup,
      addPlanForm,
      handelAddPlan,
      handelDeletePlan,
      planCount,
    };
  },
};

引入了vuex和router的hook之后,我们导出了vuex和router的实例,从这个实例中我们可以获得到vuex中的state,则可以判断APP是否有登录用户,如果没有登录就重定向到登录页面。

const store = useStore();
if (store.state.user === null) {
  Toast("未登录,请先登录");
  router.replace("Login");
}

添加计划额外传递id和时间

store.commit("addPlan", {
    ...e, // Form表单的回调,是计划标题和计划备注
    id: Math.random().toString(36).substr(2),
    date: new Date().toDateString(),
  });

使用refAPI将传入的参数返回其响应式代理

// 控制弹窗显示/隐藏的变量
const addPopup = ref(false);

如果传入的是一个对象,那么Vue内部自动会调用reactive,值得注意的是如果需要更改此响应变量,需要对响应式对象中的value属性进行更改。

响应式变量在template渲染时我们不需要写.value

计划APP的全部代码都已经梳理完毕,我们可以在这个APP中学到,状态管理,路由,组合常用的API的简单应用,在组合API中还有一些我们以前经常用到的API,比如计算属性,生命周期,watch等等在组合API拓展阅读中有简单的总结。


组合API拓展阅读

  • computed

计算属性在vue3中非常简单和vue2中如出一辙,在新版本中计算属性需要从vue引入:

import { computed } from "vue"

setup(){
  const planList = computed(() => state.list);
}

计算属性默认传入一个get函数,当然可以像以前一样传入一个set函数

 const planList = computed({
  get(){
    return state.list
  },
  set(list){
    state.list = list
  }
})

planList.value = []; // 重新set值
  • watchEffect

这个Hook非常简单,如果使用过reactHook的useEffect应该非常好理解,如果这个函数所依赖的内容变化,它会重新执行这个函数。

const countEffect = watchEffect(() => {
  console.log(count); // 定义的响应式变量,如果该变量有变更,将会重新打印count
});

当组件卸载或者调用返回中的stop方法即可停止监听,这个机制和vue2中的watch是一样滴。

watchEffect我们称之为副作用函数,我们的业务场景中如果有好友列表,鼠标移动上去能异步获取好友详情,那么如果在鼠标在移动新好友之后,上一次好友详情的请求还没结束,这可能会造成数据混淆的Bug,所以我们副作用函数支持我们这样清除副作用:

watchEffect((onInvalidate) => {
  const detail = getFriendDetail(id.value)
  onInvalidate(() => {
    // id 改变时 或 停止侦听时
    detail.cancel() // 取消请求
  })
})

第一遍看文档时感觉这样清除副作用很麻烦,React是这样清除的:

useEffect(() => {
  // do something
  return () => {
    // 清除副作用
  }
})

Vue设计副作用返回是通过传入一个回调注册清除函数,是因为使用effect钩子往往是异步请求,而异步请求返回的是Promise,所以清除函数一定要在被resolve之前注册,当然文档还说这样的好处还有可以自动帮助我们处理Promise潜在的错误(这个就是后话了嘻嘻)。

下面就是重头戏了,大胆预测一波,未来关于Vue3面试题必将有它的一席之地:watchEffect的刷新时机是什么?

第一次看文档就有猜测过这样傻白甜的问题,副作用监听了依赖它的变量,是如何很好的控制多个变量触发的机制呢,是每个变量触发都会快速的执行一次吗?

用户自定义的副作用函数会在全局缓存一遍会异步地刷新它们,Vue组件的更新函数也是一个副作用函数,刷新机制是在更新函数之后去一遍一遍走自定义的副作用函数
  • watch

watch和Vue2一样,只不过和watchEffect的区别就是:

1. 仅在依赖数据源变化才会回调副作用函数

2. 可以访问到变化前和变化后的值

3. 可以自由设置哪些值是需要监听的

共同点就是:

1. 清除副作用和停止监听

2. 副作用刷新机制也是一样的哟

const star = ref(0);
watch(star, (star, prevStar) => {
  // do something
});

// 监听多个
watch([star, rose, flower], ([star, rose, flower], [prevstar, prevrose, prevflower]) => {
  // do something
});


结语

这段时间拖得太久,Vue系列还有一个Vite的浅析还没发,也基本差亿点就结束了,因为是国庆节写的,也希望大家多多支持呀,冲鸭!

前排提示:非推广软文

微信公众号: 因卓诶;此文已同步到因卓诶blog

Uniapp这两年是Vue开发者很喜欢的跨平台开发框架,作为一个国产开发框架,其实文档和周边工具都对国人非常友好,但是由于框架本身的跨多端所以从诞生以来都被很多开发者诟病“坑太多”,那么这篇文章将结合本人2年Uniapp开发经验,给新手小白一个从0到1的教程&踩坑说明。


目录

  • 历史
  1. uniapp编译器发展历程
  • 踩坑经验(框架部分)
  1. 条件编译
  2. onLoad和onShow 用合适的生命周期
  3. 触底加载和下拉加载的实例
  4. 如何操作DOM
  5. 上传图片/文件
  6. websocket
  7. Nvue介绍
  8. SubNvue介绍
  9. 全局通信
  10. h5+API介绍
  11. 应用内更新和wgt的介绍和使用
  12. APP自定义tabbar
  13. 如何协同原生插件来工作
  14. manifest.json简介
  15. uniapp如何请求数据
  16. uniapp应该用什么方式创建项目
  • 社区
  • 插件市场
  • HbuilderX
  1. 简介和快速开发的准备工作
  2. 打包APP前的准备
  3. uniapp的debug
  4. APP打包
  5. 关于IOS
  • 拓展阅读

历史
Uniapp从诞生到目前,经历了3次重大变革,首先是最初借鉴了Mpvue的模板语法(非自定义组件模式)这个模式是性能最差且支持Vue语法有很多欠缺的版本,在当时我认为Uniapp开发APP是满足不了企业需要的,倒是做一款小程序开发框架很是不错。

在2018-2019这个时间段,也就是uniapp是模板编译模式的时代,网上对于uniapp也是褒贬不一,社区的完善度还有官方的UI支持都是非常落后的,唯一吹一吹的就是QQ群当时很火,官方的解答和处理BUG的速度还很快。我个人认为web前端跨平台框架受到了很多原生安卓&IOS开发者的排挤,拿生成的代码质量和性能说事,说不定自己也没有使用过。

在我接触uniapp不久之后(大概几个月)就出了自定义组件模式,这个组件模式相当于革命性的更新,支持用户编写的Vue组件转换成微信小程序的自定义组件,基于这个uniapp在安卓平台上放置了JSCore,从这个时候开始uniapp开始慢慢的被更多的原生开发者接受,因为跨平台开发的好处是在是太大了。

那么接下来的一次更新让我瞠目结舌,因为由于19年的年底这一段时间我没有从事uniapp开发,直到今年才继续开发uniapp,发现了uniapp的一次重大更新:“v3引擎”。

V3引擎同样也是Dcloud自研,我认为主要的更新在于安卓和IOS端,开始注重了启动速度和包体积大小,我们可以在V3更新说明中看到。

到此为止,我认为uniapp到此刻应该是推广的时候,让前端开发人员坐上跨平台开发的小车车。

踩坑经验(框架部分)

首先,uniapp绝大部分官网描述的API,比如设备信息,内存,蓝牙等等都是原生开发比较常用的,uniapp的优点就体现出来了,不同平台的各种API都会略微有差异,那么我们应该仔细看文档。

如果文档已经说明此API存在【平台差异】,那么我们应该注意API下方可能会有这样的【tips】

这也是文档的一个小坑,很多新手如果不仔细看文档,在调API的时候不考虑兼容问题,导致在小程序/APP中,比如出现安卓和ios功能不一致的问题,如果看仔细文档,那么百分之99以上的问题都会在tips中说明。

注释魔法:条件编译

其次uniapp还有一个非常强大的功能【条件编译】,这也是开发中非常常用的功能,如下,我们可以使用像C语言中核心注释,注释中的代码片段将在指定的平台出现,反之亦然;

<!-- #ifdef APP-PLUS -->代码片段,APP-PLUS代表着APP端,ifdef包含,ifndef不包含,多个平台可以用空格隔开<!--#endif-->
<!--#ifndefAPP-PLUS-NVUEAPP-NVUEMP-->编译多个平台<!--#endif-->

条件编译可以存在于任何地方,template css js;同样uniapp支持更强大的条件编译:比如支持在page.json中进行判断,达到不同端不同的分包功能;静态资源也可以进行条件编译,通过static下构建platform目录即可把不同的静态资源编译到不同的平台上去;那么同理也可以把页面进行条件编译,即不同平台不同页面;

onLoad&onShow

我们已经了解了uniapp极具特色的条件编译之后,我们可以了解一下uniapp必不可少的生命周期,uniapp在目前版本支持vue的所有生命周期以及绝大多数API;比较重要的nextTickcompile不支持,组件选项中比较重要的是render函数不支持,具体更多的支持特性表请移步官网查看(https://uniapp.dcloud.io/use?id=vue%e7%89%b9%e6%80%a7%e6%94%af%e6%8c%81%e8%a1%a8),下面我们将推荐开发中常用的生命周期以及应用场景:

onLoad(e){// 组件渲染未完成但是已创建的钩子,与Vue的create同理,这里推荐使用onLoad代替create// 行参e可以获取当前路径的参数,比如当前页面是b,如果a跳转到b页面是如下url:///pages/index/a?type=1 console.log(e.type); // "1"}

onShow(){//用于监听:页面在屏幕上显示时触发,从APP应用后台到APP前台也会触发//通常我们可以使用这个钩子做一些非列表页面的数据刷新(比如从上一个页面返回,然后此页面刷新)}

触底加载&下拉加载的实践

APP中有一个非常常见的场景叫做触底加载,那么针对APP页面级别的滚动触底回调就是onReachBottom

onReachBottom(){//在这里进行当前页加1this.listConfig.nowPage ++;// 调用加载列表的方法this.initList();}methods: {async initList(){consttoList=awaitgetALLTodoList({limit:this.listConfig.pageSize,offset:(this.listConfig.nowPage-1)*this.listConfig.pageSize),})}}

那么下拉加载这个功能在APP中非常常见,我们需要在page.json中对应的节点style下开启下拉加载

"enablePullDownRefresh": true

那么对应页面的监听钩子将会触发

onPullDownRefresh(){//用户下拉了页面}

【注意事项】不管是触底加载和下拉刷新此实例都是针对页面级别,如果你不知道何为页面级,那么请耐心看完uniapp的组件相关内容,在uniapp组件中有一个<scroll-view>,它提供一个滚动视图,在这个滚动视图中有着自己的下拉加载和触底加载方法,因此如果页面中存在此组件,开发者应该妥善处理组件级别的滚动和页面级别的滚动,这是非常重要的。

在开发中,我们经常会出现一些列表的场景,那么在uniapp中处理长列表,我们不应该使用scroll-view组件级别的滚动,而应该使用页面级别的滚动,这样性能会更好

如何操作DOM?

我们通常在开发Vue应用时,很少直接操作DOM,在uniapp中没有暴漏DOM这个概念,但是我们可以通过指定的API去操作DOM,尽管这是非常不建议的,但是在某些业务场景不得不说

它是很方便的;

lettabInfo=uni.createSelectorQuery().select("#tab");tabInfo.boundingClientRect((data)=>{//目标区域的信息}).exec();

文件的选择

uniapp在文件的选择上非常丰富,举一个例子比如说图片,我们可以在文档中很清楚的查询到对应的API,通过封装的API我们可以通过配置参数来chooseImage,然后拿到临时路径再继续上传;

uni.chooseImage({count: 6, //默认9    sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有    sourceType: ['album'], //从相册选择    success: function (res) {//临时路径 res.tempFilePathsuni.uploadFile({url: '', //仅为示例,非真实的接口地址            filePath: tempFilePaths[0],name: 'file',formData: {'user': 'test'            },success: (uploadFileRes) => {console.log(uploadFileRes.data);            }        });    }});

重点来了,我们上传图片固然简单,但是上传文件是非常难的,我们如果在APP端想做一个上传文件的功能,就要尽量使用html的帮助,由html的input type=“file”来做上传,那么首先得简单了解一下利用html如何上传文件。

  1. 首先我们需要使用h5+API中的webview相关的API

    let wv = plus.webview.create("", "/hybrid/html/index.html", {'uni-app': 'none', //不加载uni-app渲染层框架,避免样式冲突 top: 0,height: '100%',background: 'transparent' }, { url, header,key: name, ...formData, });wv.loadURL("/hybrid/html/index.html"); // 加载本地的html文件currentWebview.append(wv);//把html追加到当前页面中
    把本地的HTML加载到APP之上之后,我们在本地编写的HTML文件中引入对应uni.webview文件以及需要的业务JS文件,我们在HTML是这样做的(核心代码)
    <scripttype="text/javascript"src="https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js"></script> <scriptsrc="js/h5-uploader.js"type="text/javascript"charset="utf-8"></script>

我们在本地编写的业务JS

xhr.onreadystatechange=(ev)=>{if(xhr.readyState == 4) {if (xhr.status == 200) {progress.innerText='成功';//从这里可以看出,上传成功之后,我们通过改变html的title把信息返回        title.innerText = `${file.name}|^$%^|${src}|^$%^|${suffix}`;      }else {        progress.innerText = '上传失败了';}      setTimeout(()=>{        tis.style.display = 'none';// 上传成功,关闭了上传webview的窗口        plus.webview.currentWebview().close();},1000);    }  };  xhr.send(formData);

当index.html的title变化之后,在vue组件创建webview的代码之后,我们可以在当前webview去监听标题的变化。

//wv就是plus.webview.create的返回,上述代码有举例子wv.addEventListener('titleUpdate', ({  title}) => {  console.log(title)}

那vue组件拿到回调信息之后,就可以正常拿到图片信息去在页面中渲染。

顺便说一句,在文档的后面,会给大家介绍uniapp相关生态,一些业务组件插件都会在相关生态中查询到。

websocket

oh,又到了这个该死的东西了,今年的一段时间内,在uniapp上使用websocket可是真的太难了,先说一下事情的经过,我司开发的APP中本来使用了聊天相关功能,之后在部分业务中加入了websocket,前端需要根据websocket实时获取一些信息,在测试APP的时候发现,当2个websocket并行连接的时候,发现只有一个连接能有效的发送和接受消息,无奈只好去文档中寻找答案。

App平台自定义组件模式下,以及支付宝小程序下,所有 vue 页面只能使用一个 websocket 连接。App下可以使用 plus-websocket
插件替代实现多链接。App平台,2.2.6+起支持多个socket链接,数量没有限制。
uniapp-websocket文档

import socket from'plus-websocket'// #ifdef APP-PLUSObject.assign(uni, socket)// #endif

通过把依赖中的socket合并到uni中,把uni中原本封装的socket进行替换;

Nvue

uniapp支持用户编写.nvue的组件,那么此组件将会使用改进后的weex引擎进行渲染,提供了原生渲染的能力,那么如果你是weex用户如果用nvue写,那么全程和weex的写法无异,如果是普通的vue开发者使用nvue开发,那么将注意,nvue对于css和js的限制有非常多,尤其是css很多常用的简写都将不能支持,所以我们需要额外查询对应的weex框架的文档。

那么我们在APP中有必要全部使用nvue原生渲染么,使用普通vue组件进行webview渲染有什么缺点么?那么我建议APP中核心/用户访问次数多/需要更强劲的性能的页面可以使用nvue,其余普通页面我们仍然可以使用vue页面。

那么如果是weex用户转uniapp,可以全项目使用nvue,uniapp支持纯原生渲染模式,可以减少APP包体积,也可以减少内存占用,因为此时webview相关依赖是不存在的。使用nvue开发,即保留了原weex开发者的习惯又可以提供强大的API能力,这是非常让人兴奋的开发体验。

SubNvue

subNvue本质上是一个nvue组件,支持在普通vue页面之上运行,subNvue能覆盖map,video等原生组件,uniapp有很多办法支持覆盖原生组件,subNvue是我认为最好的方法,因为相比<cover-view>组件,不能嵌套,而且写起来和原页面耦合;又和webview相比,需要原生h5+api做技术支撑,nvue相对来说更多vue开发者友好。

实例:由于首页有swiper那么悬浮的头部需要遮挡住swiper,在pages.json中注册即可

"subNVues":[{"id":"indexTitle",//唯一标识"path":"pages/index/subNVue/indexTitle",//页面路径"style":{"position":"fixed","dock":"top","background":"transparent"}}]

uni全局通信

在uniapp中,vue,nvue组件中进行信息传递是很难的,尤其是vue和nvue中的信息传递我们可以通过全局通信的方式来做业务逻辑。

uni.$emit('update',{msg:'我是首页,用户下滑了'})

uni.$on('update',function(data){console.log("我是subnvue头部,你下滑了,那我就改变自身的透明度动画")})

适用场景:

vue与nvue,nvue与vue间的通讯
tabbar页面之间的通讯
父页面与多级子页面间的通讯
uniapp-emit文档

注意事项:

  1. 只有在页面打开时候才能注册或者监听

h5+API

其实像uniapp这样的框架(或者其他的跨平台框架)我认为h5+api带动了整个相关技术,我们在没有这样框架的时候,都是使用h5+api完成一个又一个优秀的APP,这都离不开这个非常强大的API

在前面的demo中,我们或多或少地看到了h5API的影子,下面我们再复述一遍在uniapp中使用h5+API时的一些注意事项,这非常重要。

首先uniapp内置了HTML5+引擎,我们可以直接调用h5+相关规范,但是在小程序,H5端并没有对应的规范拓展,所以在这些平台不会识别“plus”这个变量,所以我们需要写条件编译

// #ifdef APP-PLUSconsole.log(plus)//#endif

在普通的H5项目中,我们如果要使用H5+API,我们需要进行ready,但是在uniapp不需要ready,直接上去就是一套军体拳就是干。

还有我们使用一些监听事件的时候,由于uniapp没有document这个对象,所以需要使用

plus.globalEvent.addEventListener("这里写h5+拓展的事件")

应用内更新&WGT的使用

应用内更新和WGT这一块是每一个APP不可缺少的部分,所以我这里会比较详细的做一些介绍和实践。

首先我们来了解一下WGT是干嘛的。wgt是APP资源更新包,通常来讲这个包体积很小,只有1m2m左右,APP在没有拓展原生模块下或者没有增加修改一些原生的插件情况(如果不懂这个概念,后面会讲到)APP是可以使用wgt资源包升级的,整个升级过程用户可以是“无感知的”,我们通常在APP中可以看到所谓的升级提示,这个是有感知的;而有的时候你并没有升级这个APP缺发现ui变化,功能变化,那么这个时候就是无感知升级,技术手端也有很多,比如云端更新代码,在uniapp中我们可以用wgt来实现这样的功能。

我们先来APP普通的有感知更新,uniapp的实例。首先请求接口去请求服务器,拿到最新版本的下载地址,在这之前我们需要判断当前APP的版本号,那么这里就有一个小坑,我们千万不要使用下面这个API获取版本号。

plus.runtime.version

我们如果使用wgt作为资源升级包的话,那么此API获取的版本号不是准确的,它会获取APP内核的应用版本号,我们必须要使用

plus.runtime.getProperty(plus.runtime.appid,function(inf){console.log(inf.version)});

我们用此API获取到的版本号去数据库比对(前方伪代码):

// 比对版本方法, 此方法网友提供,侵权删除const compare = (curV, reqV) => {if (curV && reqV) {//将两个版本号拆成数字var arr1 = curV.split('.'),        arr2 = reqV.split('.');var minLength = Math.min(arr1.length, arr2.length),        position = 0,        diff = 0;//依次比较版本号每一位大小,当对比得出结果后跳出循环(后文有简单介绍)while (position < minLength && ((diff = parseInt(arr1[position]) - parseInt(arr2[position])) == 0)) {        position++;      }      diff = (diff != 0) ? diff : (arr1.length - arr2.length);//若curV大于reqV,则返回truereturn diff >= 0;    } else {//输入为空returnfalse;    }}
const downLoadFail=()=>{    uni.hideToast();    uni.showToast({title: "下载新版本失败,请在设置页面检查更新再试",duration: 2000,icon: "none",position: "bottom"    });}


// 获取更新列表,取最新的更新包constupdatePackageList=awaitgetUpdatePackageList();if(compare(updatePackageList[0].version,"根据上面的方法获取的版本号")){// 如果存在更新// 整包更新  uni.downloadFile({url: "整包APK的下载地址(非wgt包地址)",success: res => {if (res.statusCode === 200) {//下载之后打开临时路径的文件        plus.runtime.openFile(res.tempFilePath);      }else {        downLoadFail(); // 调用更新失败的方法      }    },fail: error => {      downLoadFail(); // 调用更新失败的方法    }  });}


以上是普通的整包升级的伪代码,如果遇到强制更新,非强制更新,即开发者需要自己控制对应的button,这里就不阐述了。

对于wgt的更新,相比整包更新有一定区别,因为整包更新非常简单,无非就是下载apk文件,然后下完之后打开,让用户自己安装。wgt的生成需要在hbuilderx中操作(后续hbuilderx篇会讲到),我们需要把wgt包上传在服务器上,前方伪代码:

uni.downloadFile({url:"wgt下载地址",success: res => {if (res.statusCode === 200) {// 安装wgt        plus.runtime.install(res.tempFilePath, {  force: false        }, function() {  // wgt安装成功if(silence){            uni.showToast({title: "已更新最新的资源,重启应用获取更佳的用户体验",duration: 4000,icon: "none",position: "bottom"            });          }else {            plus.runtime.restart();// 不是静默升级,就立即重启应用          }        }, function(e) {if(!silence){            downLoadFail();          }        });       }else {        downLoadFail();      }    },fail: error => {      downLoadFail();    }  });

细心的同学已经发现了,wgt安装需要用户重启,否则不会生效。所以我们在开发中会有一个silence的选项,如果是true那么会选择静默更新(用户无感知)这个时候我们只需要install安装即可,如果不是无感知,那么需要自动重启。

APP自定义tabbar

我们在企业级开发中,总会有一些沙雕产品想这种“凸起”或者“奇奇怪怪”的tabbar(希望我司产品不会看到)事实上,这种自定义tabbar对于uniapp开发者来说,是有一定的难度,首先如果是完全定制,那么需要有较强的webview功底或者subnvue功底。

由于自定义tabbar代码较多,我们可以借助一些插件来实现,但是值得注意的事情是,如果自定义tabbar在每一个页面都引用,会出现抖动闪烁的问题,所以我们应该在main.js中去draw这个tabbar

Vue.prototype.$tabbarView=newTabbarView();

然后在每一个tabbar页面去watch对应的路径变化,然后改变当前选中是第几项item

onShow(){//#ifdef APP-PLUS  this.$tabbarView.$watch();//#endif},onHide() {//#ifdef APP-PLUS  this.$tabbarView.$watch();//#endif},

这个watch等方法,是目前我司使用的组件的API,所以具体的定制tabbar思路知道了,我们使用一些第三方插件,可以尽量少踩一些坑。

引入原生安卓/IOS插件

ios/安卓原生的插件我们可以在【插件市场】(之后会讲到)中找到,我们在manifest.json配置本地插件即可,那么具体的插件的添加办法,如果是在插件市场直接点击添加到APP中。

如果我们是云打包(之后会说到这个概念)那么建议大家去社区直接购买然后添加,在配置文件中选中即可。

constPluginName=uni.requireNativePlugin(PluginName);

此API只存在于APP端,所以需要条件编译,传入插件名称,就会在对应的APP中找已添加的插件是否存在(如果不懂这块,可以看下面一篇说明:《manifest.json:uniapp的半壁江山》),如果插件已被添加则正常使用。

我们这边实例选用的是uniapp官方文档中的demo例子,插件是官方提供的原生增强提示框:

constdcRichAlert=uni.requireNativePlugin('DCloud-RichAlert')dcRichAlert.show({    position: 'bottom',    title: "提示信息",    titleColor: '#FF0000',    content: "<a href='https://uniapp.dcloud.io/' value='Hello uni-app'>uni-app</a> 是一个使用 Vue.js 开发跨平台应用的前端框架!\n免费的\n免费的\n免费的\n重要的事情说三遍",    contentAlign: 'left',    checkBox: {        title: '不再提示',        isSelected: true    },    buttons: [{        title: '取消'    }, {        title: '否'    }, {        title: '确认',        titleColor: '#3F51B5'    }]}, result => {console.log(result)});

说明一下引入原生插件的API,不管是vue还是nvue页面都可以使用。

如果要debug,那么就必须重新打一个自定义基座包(下面会讲到这个概念)否则不会生效。

manifest.json:uniapp的半壁江山

由于这个地方是uniapp配置项重中之重的地方,虽然用hbx直接预览它会帮助我们自动格式化,但是可能我的解释能让这块变得更简单。

app的ID是非常重要的,一般建立成功之后就不需要再更改,我们在注册微信开放平台,支付宝或者高德等等,都需要用到APP的ID和包名(packageName),包名设置会在之后介绍到,基本设置中包含了几个重要的信息,一个是应用版本名称一个是应用版本号,我们在做更新的时候需要+1,包括生成wgt包的时候。

我们可以在此处可视化的配置APP的模块权限,那么uniapp封装的模块权限真的是傻瓜式的,配置一些平台的key和secret然后再使用uniapp对应的api即可,那么肯定的事是这是增加包体积的。

引入原生插件,app如果有对应特殊的业务需求也可以编译原生的插件,具体如何引入本地原生插件,我们之前也提到过。

剩余的配置都是针对于小程序/H5的配置,所以根据我们自己的业务可以自己去查询文档。

uniapp应该使用哪种请求库呢?axios支持么?

uniapp是跨端框架,为了迎合跨端,我们不可以使用axios,因为axios不支持APP原生端,它仅仅支持网页端,所以我们可以使用uniapp提供的API(uni.request)一般这种API就够用,但是如果你想有像axios一样的配置体验的话,强烈建议你使用flyio.js这个库我们在新建项目时候会帮助我们安装好的。

uniapp项目是cli创建好还是hbx创建好?

创建uniapp项目,可以分为2种模式:

  1. cli命令行创建
  2. hbuilderX创建

结论:优先使用cli命令行创建,因为对于开发vue
web的人来说,cli命令行是最熟悉的,那么uniapp的cli方式创建的项目整体的目录结构类似于普通vue
web项目,而且我们直接使用npm快速安装依赖,像普通方式去引入;uniapp在版本支持上也是在cli项目上最先上线,可以使用cli体验到最新的功能,对于初学者来说,标签的学习是一个必须考虑进去的成本,cli方式创建的工程允许使用普通html标签,比如<div><span>它们在编译的时候会转换成uniapp的标签组件(尽管这样写并不推荐

hbx创建的uniapp项目,是随着hbx软件的版本升级而升级,cli创建的版本必须执行

npm update

才能更新到最新版本,所以我建议大家搭建项目使用cli项目,在开发中使用uniapp的标签不要使用html标签。

社区

uniapp有着非常活跃的社区,这是真的切身体验过,使得整个开发遇到的问题我们都可以在社区找到(https://ask.dcloud.net.cn/

我们遇到了所有的bug,首选需要查询是否是自身开发问题,如果自测确认没问题那么就可以在社区发布bug贴,如图

发布成功之后我们在文章底部邀请,输入dcloud就可以邀请官方人员解答,如果出现bug,也可以上传代码压缩包(指定官方人员查看)这样可以更快地让官方找到问题帮助你解决问题。

插件市场

打一个广告先,我的UI库的alpha版本在插件市场发布,尽管现在不开放下载,大概在年底左右会重构给大家带来高质量的组件(名子叫i-uniapp)希望大家关注一下

https://ext.dcloud.net.cn/ 插件市场地址

插件市场有很多vue组件/原生sdk等等,具体的安装方法我们已经在前面提到了,使用hbx开发uniapp项目可以直接点击导入插件到项目中

HbuilderX:开发uniapp神器(本文章重点)

介绍Hbx和快速开发指南

我可以很负责任的说,虽然内核是eclipse,但是也算是国产的编辑器IDE,它在Vue的支持程度上足以让我震惊,说实话从Vue开发角度来说,HbuilderX比vscode好。但是hbx被吐槽地点也有很多,比如占用内存高,UI不如vscode/sublime等等,但是这是IDE之争不在我们文章的讨论重点之内,所以我们如果要开发uniapp项目,使用hbx开发是上上策

我们如果是使用vscode或者sublime,可以在hbx调整快捷键语法

插件安装,我目前安装的是这些,为了教程的顺利和以后开发者的开发顺利,请务必装这几个基础插件。

打包APP前的准备

我们首先来看一下打包APP需要什么

我们需要一个安卓证书,我们来生成一下这个安卓证书

  1. 首先需要安装jdk,如果没有jdk,就去安装,然后在其bin文件夹下运行cmd
    keytool-genkey-alias这里写证书别名-keyalgRSA-validity36500-keystore这里写证书名称.keystore

其中参数-validity为证书有效天数,我们可以写的大写。-alias后面是证书别名
输入密码的时候没有显示,就输入就行了。退格,tab等都属于密码内容,这个密码在给.apk文件签名的时候需要。输入这个命令之后会提示您输入秘钥库的口令
接着是会提示你输入:姓氏,组织单位名称,组织名称,城市或区域名称,省市,国家、地区代码,密钥口令。确认正确输入y,回车
作者:草字头乌君
链接:https://www.jianshu.com/p/14add4a02ed6 来源:简书

keystore的密码一定要记住,如果忘记需要执行另外一个命令去查询
生成keystore成功之后我们可以在其bin目录下发现一个以你设置为名字后缀为keystore文件

  1. 填写包名
    这个包名一般标准是以com开始的(java工程中的包名标准),ios不清楚,具体可以询问具体的ios开发,命名包名是一个非常重要的事情,因为包名关系着一个APP的主要信息,在上架应用商店的时候,包名则代表着APP的唯一性,所以一定要设置一个尽量贴合企业/个人的信息相关的名字,比如公司叫“将进酒”:
    com.qiangjinjiu.andriod
    或者公司有具体的规定,则按照自身公司进行设置,这里就不阐述了
  2. 渠道包选项
    对应的渠道包,可以在对应的平台做一个标识,可以在后台看到每个平台的使用指数,这个根据业务需求可以打开,一般不会选择
  3. 原生混淆
    可以对js和nvue文件进行原生混淆,提高安全性
  4. 广告投放选择
    uniapp自带的广告插件,根据业务需要自行选择

debug:自定义基座/小程序模拟器

我们在上一段讲解了如何打包APP,打包APP之前的准备工作,那么细心的同学会发现,打包APP中有这样的一个选项

什么是自定义基座,自定义基座是我们开发日常debug的包,这个包没有压缩,所以会比正式包大,正式包记得选择正式包不要选择基座包。自定义基座APP包安装到手机上,可以连接HbuilderX上,进行调试,下面介绍一下如何打自定义基座然后debug调试

  1. 云打包:发布之前的编译阶段

  1. 打包成功会返回成功信息,自定义基座打包

打包成功之后,我们拿起我们的诺基亚手机,打开usb调试模式(作为开发,如果连这个都不知道咋打开,那就去google吧),调试模式打开之后,一定要选中hbx左边的项目,然后点击运行,运行手机模拟器,如图(重点):

基座选择一定要选择自定义基座(如果你没打包自定义基座这里就没有这个选项),然后等待手机,会出现安装基座(app)的提示,点击安装,然后手机会自动打开APP,然后这个时候我们就可以更改代码,ui,然后手机及时更新变化。

那么debug自定义基座有什么注意事项呢?

1.当我们要调试登录,分享,地图等具有原生权限功能的话,需要配置对应平台的key和secret。那么我们增加,修改了原生插件配置/原生插件的修改,这个时候我们需要重新打包自定义基座查看最新的代码。

2.切记不要把自定义基座当成正式包发布

3.不要打包打正式包debug,因为云打包正式包一天只有免费20次,我们debug用自定义基座就行了。

那么我们觉得,日常的开发除了调试第三方登录,地图等需要用手机去实际测试,那么平时我们更改UI布局,难道也用自定义基座这么麻烦么?

答案是否定的,我们要知道,虽然布局UI在不同机型可能会产生偏差,但是我们日常可以使用小程序模拟器进行开发,然后开发完毕,在实体机型上进行初步测试,看看是否Ui或者功能有偏差,再具体更改,因为小程序模拟器是最接近APP的,大部分UI布局功能都是最贴近用户手机的。

那么使用小程序模拟器调试,这里也有一些注意事项,我们如果运行项目到微信小程序,如果是第一次,我们需要提前在小程序模拟器中的安全打开RFC调用

设置->安全->端口开启

然后此时我们可以使用hbx来启动微信小程序进行开发啦。

APP打包

我们项目模块迭代开发之后,需要打包成APP,那么我们有2种途径打包

  1. 云打包
  2. 离线打包

以及特殊的wgt资源包(对应之前提到的wgt更新)

首先我们来说一下云打包,云打包是dcloud提供的云打包服务器,我们在客户端HBX编译之后就会进入云打包的队列,由dcloud打包服务器打包成功之后返回给hbx控制台临时的下载链接。

那么在之前,我们已经了解了自定义基座,那么这个自定义基座的打包和云打包(正式包)只是一个选项的问题,云打包每天有20次的免费机会,所以切记我们debug千万不要生成线上包

离线打包指的是我们可以把uniapp工程生成出原生工程,由我们开发者自己去打包,那么我相信如果有原生基础的开发者会很乐意这样做,但是务必提醒一点就是社区很多的问题都是离线打包造成的,那么为了更稳定的APP开发,我建议使用云打包。

我们在之前提到的wgt资源包,我们应该这样生成,生成成功的wgt资源包上传到服务器,然后按照前面的demo进行更新

关于IOS

其实关于IOS,本人并不是了解很多,我可以简单讲一下,可能对于新手很重要。

1.uniapp的应用是可以被审核成功的,因为19年年底苹果发了一则通知,大概是性能差劲的套壳APP不会被上架,但是2020年,必须知道uniapp不是套壳APP,它也有原生宣传,webview渲染性能也是非常强劲的,没有网上说的那么不堪。
2.如果是windows用户那么我建议你使用mac开发uniapp,因为windows开发打包出的ios包不太方便去上架等等,而mac系统没有这个顾虑。
3.windows新手如果没有上架过,可以在windows下载ApplicationLoader, 如果是mac用户那么强烈推荐你下载Transporter App ,老司机就用xcode内置的上传吧~

拓展阅读
1.uniapp统计功能,免费,安全,统计数据非常详细,支持自定义埋点。

https://tongji.dcloud.net.cn/

2.uniapp云开发,现在云开发,云数据库这么火,早在几年前小程序就有这样的东西,云开发也是今年uniapp非常大的更新,现在支持腾讯云和阿里云,据说还比较便宜,感兴趣的小伙伴可以去看看。

https://unicloud.dcloud.net.cn/login

3.uniapp广告平台,非常适用于个人,个人做的APP可以通过这个广告平台变现。

https://uniad.dcloud.net.cn/login