本节指标

  • strapi + graphql 插件 + docker 装置
  • strapi 治理数据结构、内容
  • flutter + graphql 插件 实现查问

视频

https://www.bilibili.com/vide...

代码

https://github.com/ducafecat/...

注释

后盾开发步骤

采纳 strapi + nodejs + 网关 的计划

1. strapi 装置

1.1 docker-compose 形式装置

  • .env
PASSWORD=123456
  • docker-compose.yml
version: "3"services:  mongo:    image: mongo    container_name: mongo    restart: always    ports:      - 27017:27017    environment:      - TZ=Asia/Shanghai      - MONGO_INITDB_ROOT_USERNAME=root      - MONGO_INITDB_ROOT_PASSWORD=${PASSWORD}    volumes:      - ./docker-data/mongo:/data/db    networks:      docker_net:        ipv4_address: 172.22.0.11  # starpi  # admin / 123456 / admin@ducafecat.tech  strapi-app:    image: strapi/strapi    container_name: strapi-app    restart: always    ports:      - 1337:1337    # command: strapi build    # command: strapi start    environment:      - TZ=Asia/Shanghai      - DATABASE_CLIENT=mongo      - DATABASE_HOST=mongo      - DATABASE_PORT=27017      - DATABASE_NAME=strapi      - DATABASE_USERNAME=root      - DATABASE_PASSWORD=${PASSWORD}      - DATABASE_AUTHENTICATION_DATABASE=strapi      # - NODE_ENV=production    depends_on:      - mongo    volumes:      - ./docker-data/strapi-app:/srv/app    networks:      docker_net:        ipv4_address: 172.22.0.12networks:  docker_net:    driver: bridge    ipam:      config:        - subnet: 172.22.0.0/16

http://localhost:1337/admin

1.2 装置 graphql 插件

2. 构建新闻数据结构

2.1 创立数据类型

  • 增加类型

  • 增加字段

  • 字段列表

2.2 调整数据编辑界面

2.3 调整数据列表界面

2.4 保护数据

  • 列表

  • 增加

3. 调试 graphql 申请

3.3 graphql 语法

  • 类型

    • query 查问
    • mutate 操作

3.4 调试新闻列表

http://localhost:1337/graphql

4. 编写 flutter 代码

4.1 退出 graphql 插件

https://pub.flutter-io.cn/pac...

  • pubspec.yaml
dependencies:  # graphql  graphql: ^3.0.2

4.2 封装 graphql client 工具类

  • lib/common/utils/graphql_client.dart
import 'package:flutter/material.dart';import 'package:flutter_ducafecat_news/common/values/values.dart';import 'package:flutter_ducafecat_news/common/widgets/widgets.dart';import 'package:graphql/client.dart';class GraphqlClientUtil {  static OptimisticCache cache = OptimisticCache(    dataIdFromObject: typenameDataIdFromObject,  );  static client() {    HttpLink _httpLink = HttpLink(      uri: '$SERVER_STRAPI_GRAPHQL_URL/graphql',    );    // final AuthLink _authLink = AuthLink(    //   getToken: () =>    //       'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlZmMzNDdhYzgzOTVjMDAwY2ViYzE5NyIsImlhdCI6MTU5MzY1NDcwNiwiZXhwIjoxNTk2MjQ2NzA2fQ.RYDmNSDJxcZLLPHAf4u59IER7Bs5VoWfBo1_t-TR5yY',    // );    // final Link _link = _authLink.concat(_httpLink);    return GraphQLClient(      cache: cache,      link: _httpLink,    );  }  // 查问  static Future query({    @required BuildContext context,    @required String schema,    Map<String, dynamic> variables,  }) async {    QueryOptions options = QueryOptions(      documentNode: gql(schema),      variables: variables,    );    QueryResult result = await client().query(options);    if (result.hasException) {      toastInfo(msg: result.exception.toString());      throw result.exception;    }    return result;  }  // 操作  static Future mutate({    @required BuildContext context,    @required String schema,    Map<String, dynamic> variables,  }) async {    QueryOptions options = QueryOptions(      documentNode: gql(schema),      variables: variables,    );    QueryResult result = await client().mutate(options);    if (result.hasException) {      toastInfo(msg: result.exception.toString());      throw result.exception;    }    return result;  }}

4.3 编写 graphql 查问申请

  • lib/common/graphql/news_content.dart
const String GQL_NEWS_LIST = r'''  query News {    newsContents {      title      category      author      url      addtime      thumbnail {        url      }    }  }''';

4.4 编写数据实体

lib/common/entitys/gql_news.dart

class GqlNewsResponseEntity {  GqlNewsResponseEntity({    this.id,    this.title,    this.category,    this.author,    this.url,    this.addtime,    this.thumbnail,  });  String id;  String title;  String category;  String author;  String url;  DateTime addtime;  Thumbnail thumbnail;  factory GqlNewsResponseEntity.fromJson(Map<String, dynamic> json) =>      GqlNewsResponseEntity(        id: json["id"],        title: json["title"],        category: json["category"],        author: json["author"],        url: json["url"],        addtime: DateTime.parse(json["addtime"]),        thumbnail: Thumbnail.fromJson(json["thumbnail"]),      );  Map<String, dynamic> toJson() => {        "id": id,        "title": title,        "category": category,        "author": author,        "url": url,        "addtime":            "${addtime.year.toString().padLeft(4, '0')}-${addtime.month.toString().padLeft(2, '0')}-${addtime.day.toString().padLeft(2, '0')}",        "thumbnail": thumbnail.toJson(),      };}class Thumbnail {  Thumbnail({    this.url,  });  String url;  factory Thumbnail.fromJson(Map<String, dynamic> json) => Thumbnail(        url: json["url"],      );  Map<String, dynamic> toJson() => {        "url": url,      };}

4.5 编写 API 拜访

  • lib/common/apis/gql_news.dart
import 'package:flutter/material.dart';import 'package:flutter_ducafecat_news/common/entitys/entitys.dart';import 'package:flutter_ducafecat_news/common/graphql/graphql.dart';import 'package:flutter_ducafecat_news/common/utils/utils.dart';import 'package:graphql/client.dart';/// 新闻class GqlNewsAPI {  /// 翻页  static Future<List<GqlNewsResponseEntity>> newsPageList({    @required BuildContext context,    Map<String, dynamic> params,  }) async {    QueryResult response =        await GraphqlClientUtil.query(context: context, schema: GQL_NEWS_LIST);    return response.data['newsContents']        .map<GqlNewsResponseEntity>(            (item) => GqlNewsResponseEntity.fromJson(item))        .toList();  }}

4.6 批改新闻列表页

  • lib/pages/main/main.dart
import 'dart:async';import 'package:flutter/material.dart';import 'package:flutter_ducafecat_news/common/apis/apis.dart';import 'package:flutter_ducafecat_news/common/entitys/entitys.dart';import 'package:flutter_ducafecat_news/common/utils/utils.dart';import 'package:flutter_ducafecat_news/common/values/values.dart';import 'package:flutter_ducafecat_news/common/widgets/widgets.dart';import 'package:flutter_ducafecat_news/pages/main/ad_widget.dart';import 'package:flutter_ducafecat_news/pages/main/categories_widget.dart';import 'package:flutter_ducafecat_news/pages/main/channels_widget.dart';import 'package:flutter_ducafecat_news/pages/main/news_item_widget.dart';import 'package:flutter_ducafecat_news/pages/main/newsletter_widget.dart';import 'package:flutter_ducafecat_news/pages/main/recommend_widget.dart';import 'package:flutter_easyrefresh/easy_refresh.dart';class MainPage extends StatefulWidget {  MainPage({Key key}) : super(key: key);  @override  _MainPageState createState() => _MainPageState();}class _MainPageState extends State<MainPage> {  EasyRefreshController _controller; // EasyRefresh控制器  // NewsPageListResponseEntity _newsPageList; // 新闻翻页  List<GqlNewsResponseEntity> _newsPageList; // 新闻翻页  NewsItem _newsRecommend; // 新闻举荐  List<CategoryResponseEntity> _categories; // 分类  List<ChannelResponseEntity> _channels; // 频道  String _selCategoryCode; // 选中的分类Code  @override  void initState() {    super.initState();    _controller = EasyRefreshController();    _loadAllData();    _loadLatestWithDiskCache();  }  // 如果有磁盘缓存,提早3秒拉取更新档案  _loadLatestWithDiskCache() {    if (CACHE_ENABLE == true) {      var cacheData = StorageUtil().getJSON(STORAGE_INDEX_NEWS_CACHE_KEY);      if (cacheData != null) {        Timer(Duration(seconds: 3), () {          _controller.callRefresh();        });      }    }  }  // 读取所有数据  _loadAllData() async {    _categories = await NewsAPI.categories(      context: context,      cacheDisk: true,    );    _channels = await NewsAPI.channels(      context: context,      cacheDisk: true,    );    // _newsRecommend = await NewsAPI.newsRecommend(    //   context: context,    //   cacheDisk: true,    // );    // _newsPageList = await NewsAPI.newsPageList(    //   context: context,    //   cacheDisk: true,    // );    _newsPageList = await GqlNewsAPI.newsPageList(      context: context,    );    _selCategoryCode = _categories.first.code;    if (mounted) {      setState(() {});    }  }  // 拉取举荐、新闻  _loadNewsData(    categoryCode, {    bool refresh = false,  }) async {    _selCategoryCode = categoryCode;    _newsRecommend = await NewsAPI.newsRecommend(      context: context,      params: NewsRecommendRequestEntity(categoryCode: categoryCode),      refresh: refresh,      cacheDisk: true,    );    // _newsPageList = await NewsAPI.newsPageList(    //   context: context,    //   params: NewsPageListRequestEntity(categoryCode: categoryCode),    //   refresh: refresh,    //   cacheDisk: true,    // );    _newsPageList = await GqlNewsAPI.newsPageList(      context: context,    );    if (mounted) {      setState(() {});    }  }  // 分类菜单  Widget _buildCategories() {    return _categories == null        ? Container()        : newsCategoriesWidget(            categories: _categories,            selCategoryCode: _selCategoryCode,            onTap: (CategoryResponseEntity item) {              _loadNewsData(item.code);            },          );  }  // 举荐浏览  Widget _buildRecommend() {    return _newsRecommend == null // 数据没到位,能够用骨架图展现        ? Container()        : recommendWidget(_newsRecommend);  }  // 频道  Widget _buildChannels() {    return _channels == null        ? Container()        : newsChannelsWidget(            channels: _channels,            onTap: (ChannelResponseEntity item) {},          );  }  // 新闻列表  Widget _buildNewsList() {    return _newsPageList == null        ? Container(            height: duSetHeight(161 * 5 + 100.0),          )        : Column(            children: _newsPageList.map((item) {              // 新闻行              List<Widget> widgets = <Widget>[                newsItem(item),                Divider(height: 1),              ];              // 每 5 条 显示广告              int index = _newsPageList.indexOf(item);              if (((index + 1) % 5) == 0) {                widgets.addAll(<Widget>[                  adWidget(),                  Divider(height: 1),                ]);              }              // 返回              return Column(                children: widgets,              );            }).toList(),          );  }  // ad 广告条  // 邮件订阅  Widget _buildEmailSubscribe() {    return newsletterWidget();  }  @override  Widget build(BuildContext context) {    return _newsPageList == null        ? cardListSkeleton()        : EasyRefresh(            enableControlFinishRefresh: true,            controller: _controller,            header: ClassicalHeader(),            onRefresh: () async {              await _loadNewsData(                _selCategoryCode,                refresh: true,              );              _controller.finishRefresh();            },            child: SingleChildScrollView(              child: Column(                children: <Widget>[                  _buildCategories(),                  Divider(height: 1),                  _buildRecommend(),                  Divider(height: 1),                  _buildChannels(),                  Divider(height: 1),                  _buildNewsList(),                  Divider(height: 1),                  _buildEmailSubscribe(),                ],              ),            ),          );  }}

4.7 批改新闻详情页

  • lib/pages/main/news_item_widget.dart
import 'package:auto_route/auto_route.dart';import 'package:flutter/material.dart';import 'package:flutter_ducafecat_news/common/entitys/entitys.dart';import 'package:flutter_ducafecat_news/common/utils/utils.dart';import 'package:flutter_ducafecat_news/common/values/values.dart';import 'package:flutter_ducafecat_news/common/widgets/widgets.dart';import 'package:flutter_ducafecat_news/common/router/router.gr.dart';/// 新闻行 ItemWidget newsItem(GqlNewsResponseEntity item) {  return Container(    height: duSetHeight(161),    padding: EdgeInsets.all(duSetWidth(20)),    child: Row(      mainAxisAlignment: MainAxisAlignment.spaceBetween,      crossAxisAlignment: CrossAxisAlignment.start,      children: <Widget>[        // 图        InkWell(          onTap: () {            ExtendedNavigator.rootNavigator.pushNamed(              Routes.detailsPageRoute,              arguments: DetailsPageArguments(item: item),            );          },          child: imageCached(            '$SERVER_STRAPI_GRAPHQL_URL${item.thumbnail.url}',            width: duSetWidth(121),            height: duSetWidth(121),          ),        ),        // 右侧        SizedBox(          width: duSetWidth(194),          child: Column(            crossAxisAlignment: CrossAxisAlignment.start,            children: <Widget>[              // 作者              Container(                margin: EdgeInsets.all(0),                child: Text(                  item.author,                  style: TextStyle(                    fontFamily: 'Avenir',                    fontWeight: FontWeight.normal,                    color: AppColors.thirdElementText,                    fontSize: duSetFontSize(14),                    height: 1,                  ),                ),              ),              // 题目              InkWell(                onTap: () {                  ExtendedNavigator.rootNavigator.pushNamed(                    Routes.detailsPageRoute,                    arguments: DetailsPageArguments(item: item),                  );                },                child: Container(                  margin: EdgeInsets.only(top: duSetHeight(10)),                  child: Text(                    item.title,                    style: TextStyle(                      fontFamily: 'Montserrat',                      fontWeight: FontWeight.w500,                      color: AppColors.primaryText,                      fontSize: duSetFontSize(16),                      height: 1,                    ),                    overflow: TextOverflow.clip,                    maxLines: 3,                  ),                ),              ),              // Spacer              Spacer(),              // 一行 3 列              Container(                child: Row(                  crossAxisAlignment: CrossAxisAlignment.center,                  children: <Widget>[                    // 分类                    ConstrainedBox(                      constraints: BoxConstraints(                        maxWidth: duSetWidth(60),                      ),                      child: Text(                        item.category,                        style: TextStyle(                          fontFamily: 'Avenir',                          fontWeight: FontWeight.normal,                          color: AppColors.secondaryElementText,                          fontSize: duSetFontSize(14),                          height: 1,                        ),                        overflow: TextOverflow.clip,                        maxLines: 1,                      ),                    ),                    // 增加工夫                    Container(                      width: duSetWidth(15),                    ),                    ConstrainedBox(                      constraints: BoxConstraints(                        maxWidth: duSetWidth(100),                      ),                      child: Text(                        '• ${duTimeLineFormat(item.addtime)}',                        style: TextStyle(                          fontFamily: 'Avenir',                          fontWeight: FontWeight.normal,                          color: AppColors.thirdElementText,                          fontSize: duSetFontSize(14),                          height: 1,                        ),                        overflow: TextOverflow.clip,                        maxLines: 1,                      ),                    ),                    // 更多                    Spacer(),                    InkWell(                      child: Icon(                        Icons.more_horiz,                        color: AppColors.primaryText,                        size: 24,                      ),                      onTap: () {},                    ),                  ],                ),              ),            ],          ),        ),      ],    ),  );}

资源

设计稿蓝湖预览

https://lanhuapp.com/url/lYuz1
明码: gSKl

蓝湖当初免费了,所以查看标记还请本人上传 xd 设计稿
商业设计稿文件不好间接分享, 能够加微信分割 ducafecat

参考

  • https://pub.flutter-io.cn/pac...
  • https://strapi.io/documentati...

© 猫哥

https://ducafecat.tech