предисловие
оFlutterGoМожет быть, это не нуждается в большом представлении.
Если у вас есть друг, который впервые слышит об этом, вы можете переехатьОфициальный сайт FlutterGoОзнакомьтесь с кратким введением.
В этой итерации у FlutterGo было много обновлений, в этом обновлении автор отвечает за разработку бэкенда и соответствующей клиентской части. Вот краткое введение в реализацию нескольких функциональных модулей в бэкенд-коде FlutterGo.
В целом, бэкэнда мелки не сложна. Эта статья, вероятно, описана со следующей функцией (интерфейс) реализации:
- Функция входа во FlutterGo
- Функция сбора компонентов
- Любимая функция
- Функция обратной связи с предложениями
Информация об окружающей среде
Облачный сервер Alibaba Cloud ECS
Linux iz2ze3gw3ipdpbha0mstybz 3.10.0-957.21.3.el7.x86_64 #1 SMP Tue Jun 18 16:35:19 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
MySQL:mysql Ver 8.0.16 for Linux on x86_64 (MySQL Community Server - GPL)
node:v12.5.0
Язык разработки:midway
+ typescript
+ mysql
Структура кода:
src
├─ app
│ ├─ class 定义表结构
│ │ ├─ app_config.ts
│ │ ├─ cat.ts
│ │ ├─ collection.ts
│ │ ├─ user.ts
│ │ ├─ user_collection.ts
│ │ └─ widget.ts
│ ├─ constants 常量
│ │ └─ index.ts
│ ├─ controller
│ │ ├─ app_config.ts
│ │ ├─ auth.ts
│ │ ├─ auth_collection.ts
│ │ ├─ cat_widget.ts
│ │ ├─ home.ts
│ │ ├─ user.ts
│ │ └─ user_setting.ts
│ ├─ middleware 中间件
│ │ └─ auth_middleware.ts
│ ├─ model
│ │ ├─ app_config.ts
│ │ ├─ cat.ts
│ │ ├─ collection.ts
│ │ ├─ db.ts
│ │ ├─ user.ts
│ │ ├─ user_collection.ts
│ │ └─ widget.ts
│ ├─ public
│ │ └─ README.md
│ ├─ service
│ │ ├─ app_config.ts
│ │ ├─ cat.ts
│ │ ├─ collection.ts
│ │ ├─ user.ts
│ │ ├─ user_collection.ts
│ │ ├─ user_setting.ts
│ │ └─ widget.ts
│ └─ util 工具集
│ └─ index.ts
├─ config 应用的配置信息
│ ├─ config.default.ts
│ ├─ config.local.ts
│ ├─ config.prod.ts
│ └─ plugin.ts
└─ interface.ts
Функция входа
первый вclass/user.ts
определитьuser
Структура таблицы, примерно обязательные поля и вinterface.ts
Объявите соответствующий интерфейс в . вотmidway
а такжеts
Базовая конфигурация , не будет представлена.
FlutterGo предоставляет два метода входа в систему:
- Логин и пароль для входа
-
GitHubOAuth
Сертификация
Потому что это мобильный клиентGitHubOauth
Аутентификация, так что здесь на самом деле есть некоторые подводные камни, о которых речь пойдет позже. Здесь мы начинаем с простого
Вход по логину/паролю
Поскольку мы используем метод входа в систему с именем пользователя/паролем github, нам нужно указать API github здесь:developer.github.com/v3/auth/,
Основная часть документации:curl -u username https://api.github.com/user
(Вы можете проверить это сами на терминале), нажмите Enter и введите пароль. Итак, здесь мы можем полностью аутентифицировать githu после получения имени пользователя и пароля, введенных пользователем.
Основное использование midway здесь повторяться не будет. Весь процесс по-прежнему очень прост и понятен, как показано ниже:
Соответствующая реализация кода (соответствующая информация была десенсибилизирована: xxx):
service
часть
//获取 userModel
@inject()
userModel
// 获取 github 配置信息
@config('githubConfig')
GITHUB_CONFIG;
//获取请求上下文
@inject()
ctx;
//githubAuth 认证
async githubAuth(username: string, password: string, ctx): Promise<any> {
return await ctx.curl(GITHUB_OAUTH_API, {
type: 'GET',
dataType: 'json',
url: GITHUB_OAUTH_API,
headers: {
'Authorization': ctx.session.xxx
}
});
}
// 查找用户
async find(options: IUserOptions): Promise<IUserResult> {
const result = await this.userModel.findOne(
{
attributes: ['xx', 'xx', 'xx', 'xx', 'xx', "xx"],//相关信息脱敏
where: { username: options.username, password: options.password }
})
.then(userModel => {
if (userModel) {
return userModel.get({ plain: true });
}
return userModel;
});
return result;
}
// 通过 URLName 查找用户
async findByUrlName(urlName: string): Promise<IUserResult> {
return await this.userModel.findOne(
{
attributes: ['xxx', 'xxx', 'xxx', 'xxx', 'xxx', "xxx"],
where: { url_name: urlName }
}
).then(userModel => {
if (userModel) {
return userModel.get({ plain: true });
}
return userModel;
});
}
// 创建用户
async create(options: IUser): Promise<any> {
const result = await this.userModel.create(options);
return result;
}
// 更新用户信息
async update(id: number, options: IUserOptions): Promise<any> {
return await this.userModel.update(
{
username: options.username,
password: options.password
},
{
where: { id },
plain: true
}
).then(([result]) => {
return result;
});
}
controller
// inject 获取 service 和加密字符串
@inject('userService')
service: IUserService
@config('random_encrypt')
RANDOM_STR;
流程图中逻辑的代码实现
GitHubOAuth-аутентификация
Здесь есть дыра! Я вернусь и представлю
Аутентификация githubOAuth — это то, что мы часто называем приложением github, здесь я прямо выбрасываю документ:creating-a-github-app
笔者还是觉得文档类的无需介绍
Конечно, я должен был собрать все здесь, а затем записать некоторую базовую информацию в конфигурацию на стороне сервера.
Или следуйте приведенной выше процедуре, давайте сначала представим процесс. Затем речь идет о том, где находится дыра.
Клиентская часть
Код клиентской части достаточно прост, открываем новый webView и сразу переходим кgithub.com/login/oauth/authorize
приноситьclient_id
Вот и все.
серверная часть
Общий процесс аналогичен предыдущему, показана часть кода:
service
//获取 github access_token
async getOAuthToken(code: string): Promise<any> {
return await this.ctx.curl(GITHUB_TOKEN_URL, {
type: "POST",
dataType: "json",
data: {
code,
client_id: this.GITHUB_CONFIG.client_id,
client_secret: this.GITHUB_CONFIG.client_secret
}
});
}
controller
Логика кода заключается в вызове данных в службе для получения информации из приведенной выше блок-схемы.
Подводные камни в OAuth
Фактически, метод аутентификации приложения github очень подходит для среды браузера, но в флаттере, потому что мы являемся недавно открытым веб-представлением для запроса адреса входа в github. Когда наш бэкэнд успешно возвращается, мы не можем уведомить слой Flutter. В результате код, написанный dart в моем родном Flutter, не может получить отдачу интерфейса.
В середине мозга есть много решений, и, наконец, я смотрю вверхflutter_webview_pluginХороший метод находится в API:onUrlChanged
Короче говоря, клиентская часть Flutter открывает новый webView для запросаgithub.com/login
,github.com/login
экзаменclient_id
После этого он придет к серверной части с грязными вещами, такими как код.После успешной проверки серверной части,redirectНовый веб-представление Flutter, затемflutter_webview_plugin
Для прослушивания изменений в URL-адресе страницы. Отправьте соответствующие события, чтобы позволить Flutter уничтожить текущий веб-представление и обработать оставшуюся логику.
Частичный код FLUTTER
//定义相关 OAuth event
class UserGithubOAuthEvent{
final String loginName;
final String token;
final bool isSuccess;
UserGithubOAuthEvent(this.loginName,this.token,this.isSuccess);
}
webView page
:
//在 initState 中监听 url 变化,并emit event
flutterWebviewPlugin.onUrlChanged.listen((String url) {
if (url.indexOf('loginSuccess') > -1) {
String urlQuery = url.substring(url.indexOf('?') + 1);
String loginName, token;
List<String> queryList = urlQuery.split('&');
for (int i = 0; i < queryList.length; i++) {
String queryNote = queryList[i];
int eqIndex = queryNote.indexOf('=');
if (queryNote.substring(0, eqIndex) == 'loginName') {
loginName = queryNote.substring(eqIndex + 1);
}
if (queryNote.substring(0, eqIndex) == 'accessToken') {
token = queryNote.substring(eqIndex + 1);
}
}
if (ApplicationEvent.event != null) {
ApplicationEvent.event
.fire(UserGithubOAuthEvent(loginName, token, true));
}
print('ready close');
flutterWebviewPlugin.close();
// 验证成功
} else if (url.indexOf('${Api.BASE_URL}loginFail') == 0) {
// 验证失败
if (ApplicationEvent.event != null) {
ApplicationEvent.event.fire(UserGithubOAuthEvent('', '', true));
}
flutterWebviewPlugin.close();
}
});
login page
:
//event 的监听、页面跳转以及提醒信息的处理
ApplicationEvent.event.on<UserGithubOAuthEvent>().listen((event) {
if (event.isSuccess == true) {
// oAuth 认证成功
if (this.mounted) {
setState(() {
isLoading = true;
});
}
DataUtils.getUserInfo(
{'loginName': event.loginName, 'token': event.token})
.then((result) {
setState(() {
isLoading = false;
});
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => AppPage(result)),
(route) => route == null);
}).catchError((onError) {
print('获取身份信息 error:::$onError');
setState(() {
isLoading = false;
});
});
} else {
Fluttertoast.showToast(
msg: '验证失败',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.CENTER,
timeInSecForIos: 1,
backgroundColor: Theme.of(context).primaryColor,
textColor: Colors.white,
fontSize: 16.0);
}
});
Получение дерева компонентов
Структура таблицы
Прежде чем говорить о реализации интерфейса, давайте сначала разберемся, как выглядит наш дизайн табличного механизма в отношении компонентов.
На вкладке виджетов FlutterGO есть много категорий.Щелкните категорию, чтобы войти в категорию, затем щелкните, чтобы перейти к компоненту, и щелкните компонент, чтобы перейти на страницу сведений.
上图模块点进去就是组件 widget
上图是 widget,点进去是详情页
Итак, здесь нам нужны две таблицы для записи их отношений: cat (категория) и таблица виджетов.
В таблице cat у нас будет по одному на строку данныхparent_id
поле, поэтому в таблице есть отношение родитель-потомок, иwidget
для каждой строки данных в таблицеparent_id
Значение поля должно бытьcat
Последний слой в таблице. НапримерCheckbox
widget
изparent_id
Значениеcat
столButton
я бы.
Реализация спроса
При входе в систему мы надеемся получить все деревья компонентов.На стороне спроса требуется следующая структура:
[
{
"name": "Element",
"type": "root",
"child": [
{
"name": "Form",
"type": "group",
"child": [
{
"name": "input",
"type": "page",
"display": "old",
"extends": {},
"router": "/components/Tab/Tab"
},
{
"name": "input",
"type": "page",
"display": "standard",
"extends": {},
"pageId": "page1_hanxu_172ba42f_0520_401e_b568_ba7f7f6835e4"
}
]
}
],
}
]
Поскольку существуют трехсторонние компоненты совместной разработки, и наша страница сведений была значительно изменена по сравнению с версией FlutterGo 1.0, теперь есть только одна страница сведений о компонентах, и весь контент отображается с помощью md, а демонстрационная реализация компонента написано в мд. Итак, чтобы быть совместимым со старыми версиями виджетов, у нас естьdisplay
различать старые и новыеwidget
соответственно черезpageId
а такжеrouter
чтобы перейти на страницу.
Идентификатор страницы нового виджета проходит через скаффолдинг FlutterGo.goCliСгенерировано
Текущая реализация фактически возвращает:
{
"success": true,
"data": [
{
"id": "3",
"name": "Element",
"parentId": 0,
"type": "root",
"children": [
{
"id": "6",
"name": "Form",
"parentId": 3,
"type": "category",
"children": [
{
"id": "9",
"name": "Input",
"parentId": 6,
"type": "category",
"children": [
{
"id": "2",
"name": "TextField",
"parentId": "9",
"type": "widget",
"display": "old",
"path": "/Element/Form/Input/TextField"
}
]
},
{
"id": "12",
"name": "Text",
"parentId": 6,
"type": "category",
"children": [
{
"id": "3",
"name": "Text",
"parentId": "12",
"type": "widget",
"display": "old",
"path": "/Element/Form/Text/Text"
},
{
"id": "4",
"name": "RichText",
"parentId": "12",
"type": "widget",
"display": "old",
"path": "/Element/Form/Text/RichText"
}
]
},
{
"id": "13",
"name": "Radio",
"parentId": 6,
"type": "category",
"children": [
{
"id": "5",
"name": "TestNealya",
"parentId": "13",
"type": "widget",
"display": "standard",
"pageId": "page1_hanxu_172ba42f_0520_401e_b568_ba7f7f6835e4"
}
]
}
]
}
]
}
{
"id": "5",
"name": "Themes",
"parentId": 0,
"type": "root",
"children": []
}
]
}
Простой пример, сохранить 99% данных
Код
На самом деле этот интерфейс тоже очень простой, это двухцикловый обход, если быть точным, немного похожий на обход в глубину. Просто посмотрите на код
Получить все категории с одним и тем же parentId (далее — cat)
async getAllNodeByParentIds(parentId?: number) {
if (!!!parentId) {
parentId = 0;
}
return await this.catService.getCategoryByPId(parentId);
}
Преобразовать первую букву в нижний регистр
firstLowerCase(str){
return str[0].toLowerCase()+str.slice(1);
}
Нам нужно только внешне поддерживать дерево компонентов, а затемcat
каждый читается в таблицеparent_id
является узлом. токid
ничего большеcat
соответствующийparent_id
Это означает, что его следующий уровень - "лист"widget
, так что изwidget
можно поинтересоваться. легко~
//删除部分不用代码
@get('/xxx')
async getCateList(ctx) {
const resultList: IReturnCateNode[] = [];
let buidList = async (parentId: number, containerList: Partial<IReturnCateNode>[] | Partial<IReturnWidgetNode>[], path: string) => {
let list: IReturnCateNode[] = await this.getAllNodeByParentIds(parentId);
if (list.length > 0) {
for (let i = 0; i < list.length; i++) {
let catNode: IReturnCateNode;
catNode = {
xxx:xxx
}
containerList.push(catNode);
await buidList(list[i].id, containerList[i].children, `${path}/${this.firstLowerCase(containerList[i].name)}`);
}
} else {
// 没有 cat 表下 children,判断是否存在 widget
const widgetResult = await this.widgetService.getWidgetByPId(parentId);
if (widgetResult.length > 0) {
widgetResult.map((instance) => {
let tempWidgetNode: Partial<IReturnWidgetNode> = {};
tempWidgetNode.xxx = instance.xxx;
if (instance.display === 'old') {
tempWidgetNode.path = `${path}/${this.firstLowerCase(instance.name)}`;
} else {
tempWidgetNode.pageId = instance.pageId;
}
containerList.push(tempWidgetNode);
});
} else {
return null;
}
}
}
await buidList(0, resultList, '');
ctx.body = { success: true, data: resultList, status: 200 };
}
пасхальные яйца
Во FlutterGo есть функция поиска компонентов, потому что мы хранимwidget
, приносить не обязательноwidget
маршрут, что также неразумно (для старых комплектующих), поэтому вwidget
Найдите таблицу, а также обратный поиск, чтобы получить «старый», как описано выше.widget
изrouter
поле
Моя личная реализация кода выглядит примерно так:
@get('/xxx')
async searchWidget(ctx){
let {name} = ctx.query;
name = name.trim();
if(name){
let resultWidgetList = await this.widgetService.searchWidgetByStr(name);
if(xxx){
for(xxx){
if(xxx){
let flag = true;
xxx
while(xxx){
let catResult = xxx;
if(xxx){
xxx
if(xxx){
flag = false;
}
}else{
flag = false;
}
}
resultWidgetList[i].path = path;
}
}
ctx.body={success:true,data:resultWidgetList,message:'查询成功'};
}else{
ctx.body={success:true,data:[],message:'查询成功'};
}
}else{
ctx.body={success:false,data:[],message:'查询字段不能为空'};
}
}
Просите у великого Бога самую простую реализацию~🤓
Любимая функция
Функция сбора должна быть связана с пользователем. Тогда как избранные компоненты должны быть связаны с пользователем? Компоненты и пользователи多对多
Отношение.
Вот создаю новыйcollection
таблица, используемая для всех избранных компонентов. Почему бы просто не использоватьwidget
Что касается таблицы, то лично мне не хочется, чтобы таблица была слишком сложной, со слишком большим количеством бесполезных полей и ни одной функции.
Так как это любимый компонент и пользовательмногие ко многимотношения, поэтому здесь нам нужна промежуточная таблицаuser_collection
Чтобы сохранить отношения между двумя, отношения между тремя следующие:
Идеи реализации функций
-
Проверить избранное
- от
collection
Проверьте информацию о компоненте, переданную пользователем в таблице.collection
идентификатор в таблице - от
session
получить идентификатор пользователя - использовать
collection_id
а такжеuser_id
извлекатьuser_collection
Есть ли это поле в таблице
- от
-
Добавить в избранное
- Получить информацию о компоненте от пользователя
-
findOrCrate
поискcollection
таблицу и возвращаетcollection_id
- потом
user_id
а такжеcollection_id
депозит вuser_collection
В таблице (принцип взаимного недоверия, проверить наличие)
-
удалить избранное
- Шаги такие же, как указано выше, получить
collection
в таблицеcollection_id
- удалять
user_collection
соответствующее поле
- Шаги такие же, как указано выше, получить
-
Получить все избранное
- забрать
collection
Все в таблицеuser_id
для текущего пользователяcollection_id
- получено
collection_id
s, чтобы получить список любимых компонентов
- забрать
Часть реализации кода
В целом, идея все еще очень ясна. Итак, здесь мы используем только сбор и проверку, чтобы показать следующую часть кода:
service
Реализация кода слоя
@inject()
userCollectionModel;
async add(params: IuserCollection): Promise<IuserCollection> {
return await this.userCollectionModel.findOrCreate({
where: {
user_id: params.user_id, collection_id: params.collection_id
}
}).then(([model, created]) => {
return model.get({ plain: true })
})
}
async checkCollected(params: IuserCollection): Promise<boolean> {
return await this.userCollectionModel.findAll({
where: { user_id: params.user_id, collection_id: params.collection_id }
}).then(instanceList => instanceList.length > 0);
}
controller
Реализация кода слоя
@inject('collectionService')
collectionService: ICollectionService;
@inject()
userCollectionService: IuserCollectionService
@inject()
ctx;
// 校验组件是否收藏
@post('/xxx')
async checkCollected(ctx) {
if (ctx.session.userInfo) {
// 已登录
const collectionId = await this.getCollectionId(ctx.request.body);
const userCollection: IuserCollection = {
user_id: this.ctx.session.userInfo.id,
collection_id: collectionId
}
const hasCollected = await this.userCollectionService.checkCollected(userCollection);
ctx.body={status:200,success:true,hasCollected};
} else {
ctx.body={status:200,success:true,hasCollected:false};
}
}
async addCollection(requestBody): Promise<IuserCollection> {
const collectionId = await this.getCollectionId(requestBody);
const userCollection: IuserCollection = {
user_id: this.ctx.session.userInfo.id,
collection_id: collectionId
}
return await this.userCollectionService.add(userCollection);
}
потому что частоcollection
в таблицеcollection_id
поле, поэтому оно извлекается здесь как общедоступный метод
async getCollectionId(requestBody): Promise<number> {
const { url, type, name } = requestBody;
const collectionOptions: ICollectionOptions = {
url, type, name
};
const collectionResult: ICollection = await this.collectionService.findOrCreate(collectionOptions);
return collectionResult.id;
}
функция обратной связи
Функция обратной связи заключается в том, чтобы напрямую отправить вопрос в личные настройки FLUTTERGO.Alibaba/flutter-goВниз. Это в основном для вызова api интерфейса github для решения проблем.issues API.
Реализация внутреннего кода очень проста, просто получите данные и вызовите API github.
service
Пол
@inject()
ctx;
async feedback(title: string, body: string): Promise<any> {
return await this.ctx.curl(GIHTUB_ADD_ISSUE, {
type: "POST",
dataType: "json",
headers: {
'Authorization': this.ctx.session.headerAuth,
},
data: JSON.stringify({
title,
body,
})
});
}
controller
Пол
@inject('userSettingService')
settingService: IUserSettingService;
@inject()
ctx;
async feedback(title: string, body: string): Promise<any> {
return await this.settingService.feedback(title, body);
}
пасхальные яйца
Угадайте, какой компонент используется для этой обратной связи во FlutterGo ~ вот введение
pubspec.yaml
zefyr:
path: ./zefyr
Потому что во время разработки флаттер был обновлен, в результате чегоzefyrВыполнение ошибки. В то время также поднимался вопрос:chould not Launch FIle(увидела ответ только когда писала этот пост)
Но на тот момент, в связи с выходом разработки функции, я долго ждал без нее.zefyr
Ответ автора. Эта ошибка исправлена локально, и пакет напрямую импортирован в локальный пакет.
План совместного строительства
Кашель, постучи по доске~~
Flutter по-прежнему постоянно обновляется, но нам по-прежнему очень сложно поддерживать FlutterGo вне работы. Итак, мы искренне приглашаем всех энтузиастов Flutter в отрасли принять участие в совместном строительстве FlutterGo!
Еще раз спасибо всем здесьДрузья, отправившие pr
Инструкции по совместному строительству
из-заВерсия Flutter работает быстрее, производит больше контента, а мыОграниченная рабочая силаНевозможно более всесторонне и быстро поддерживать итерацию ежедневного обслуживания Flutter Go.Если вы заинтересованы в совместной разработке Flutter Go, вы можете принять участие в совместной разработке этого проекта.
Всем, кто участвует в совместном строительстве, мы разместим ваш аватар и личный адрес github на нашем официальном сайте.
Метод совместного строительства
- Компоненты совместной сборки
-
Это обновление открытоКоллекция содержимого виджетафункция, вам нужно пройтиgoCliИнструменты, создавайте стандартизированные компоненты, пишите уценочный код.
-
Чтобы лучше фиксировать цель ваших изменений, информацию о содержании, процесс коммуникации, каждый PR должен соответствовать одномуIssue, отправьте то, что вы нашли
BUG
или хотите увеличить新功能
, или хотите добавить новыйКомпоненты совместной сборки, -
Выберите свой первый
issue
в типе, а затем передатьPull RequestВ интерфейс виджета добавляется содержание статьи, описание API, использование компонентов и т. д.
- Публикуйте статьи и исправляйте ошибки
- Вы также можете использовать, например.Ежедневные баги. будущие функциии т.д. функциональный PR, отправьте заявку в наш основной репозиторий.
Участвовать в совместном строительстве
Чтобы узнать, как подать PR, сначала прочтите следующие документы.
- Как отправить запрос на слияние в репозиторий
- спецификация кода дартс
- Как создать страницу виджетов с помощью go-cli
Руководство по взносам
Этот проект следуетКодекс поведения авторов. Участвуя в этом проекте, вы соглашаетесь соблюдать его условия.
FlutterGo с нетерпением ждет совместной работы с вами и мной~
Для получения конкретных сведений о pr и процедурах см.FlutterGoREADME или напрямуюДин ДинОтсканируйте код, чтобы присоединиться к группе
учеба по обмену
Обратите внимание на официальный аккаунт: [Full-stack front-end selection] Получайте хорошие рекомендации статей каждый день. Вы также можете присоединиться к группе и учиться и общаться вместе~~