안녕하세요!
오늘은 Flutter 에서 서버 기반 UI용 슈퍼 엔진을 만드는 방법을 보여 드리겠습니다. 이는 슈퍼 CMS의 필수 부분입니다(이것이 바로 제작자, 즉 제가 배치하는 방식입니다). 물론 당신은 다른 의견을 가지고 있을 수 있으며, 댓글을 통해 이에 대해 기꺼이 논의하겠습니다.
이 기사는 시리즈의 두 개 (이미 세 개) 중 첫 번째 기사입니다. 이번 글에서는 Nui를 직접 살펴보고 다음 글에서는 Nui가 Nanc CMS와 얼마나 깊이 통합되었는지 살펴보겠습니다 . 이 글과 다음 글 사이에는 Nui 성능에 대한 엄청난 양의 정보가 담긴 또 다른 글이 있을 것입니다.
이 기사에서는 Server-Driven UI, Nui(Nanc Server-Driven UI) 기능, 프로젝트 이력, 이기적인 관심, Doctor Strange에 대해 많은 흥미로운 내용을 다룰 것입니다. 아 예, GitHub 및 pub.dev에 대한 링크도 있으므로 마음에 드시고 1~2분의 시간을 투자해도 괜찮으시다면 별표 와 좋아요를 눌러주시면 기쁠 것입니다.
나는 이미 Nanc에 대한 기사를 썼지만 그 이후로 1년 이상이 지났고 프로젝트는 기능과 "완전성" 측면에서 크게 발전했으며 가장 중요한 것은 완성된 문서 와 함께 출시되었으며 MIT에 따라 특허.
백엔드를 끌지 않는 범용 CMS입니다. 동시에, 무언가를 생성하려면 수많은 코드를 작성해야 하는 React Admin과 같은 것이 아닙니다.
Nanc 사용을 시작하려면 다음 사항으로 충분합니다.
게다가 첫 번째 작업은 CMS 자체의 인터페이스를 통해 완전히 수행될 수 있습니다. 즉, UI를 통해 데이터 구조를 관리할 수 있습니다. 다음과 같은 경우 두 번째 단계를 건너뛸 수 있습니다.
따라서 일부 시나리오에서는 콘텐츠와 데이터를 관리하기 위해 CMS를 얻기 위해 한 줄의 코드를 작성할 필요가 없습니다. 앞으로 이러한 시나리오의 수는 GraphQL 및 RestAPI와 함께 증가할 것입니다. SDK를 구현할 수 있는 다른 용도에 대한 아이디어가 있다면 댓글에 있는 제안 사항을 기꺼이 읽어보겠습니다.
Nanc는 데이터 저장 계층 수준에서 테이블(SQL) 또는 문서(No-SQL)로 표시될 수 있는 엔터티, 즉 모델을 사용하여 작동합니다. 각 엔터티에는 SQL의 열 표현 또는 No-SQL의 동일한 "필드"인 필드가 있습니다.
가능한 필드 유형 중 하나는 소위 "화면" 유형입니다. 즉, 이 기사 전체는 CMS의 한 필드에 대한 텍스트입니다. 동시에 구조적으로는 다음과 같습니다. Nui라는 서버 기반 UI 엔진을 함께 구현하는 완전히 별도의 라이브러리( 실제로는 여러 라이브러리 ) 가 있습니다. 이 기능은 CMS에 통합되어 있으며 그 위에 많은 추가 기능이 포함되어 있습니다.
이것으로 Nanc에 대한 직접적인 소개 부분을 마무리하고 누이에 대한 이야기를 시작하겠습니다.
면책조항: 모든 우연은 우연입니다. 이 이야기는 허구입니다. 나는 그것을 꿈꿨다.
저는 한 대기업에서 동시에 여러 애플리케이션을 작업했습니다. 그것들은 대체로 유사했지만 많은 차이점도 있었습니다.
그러나 그것들에서 완전히 동일한 것은 내가 기사 엔진 이라고 부를 수 있는 것이었습니다. 백엔드에서 JSON을 처리하는 수천 줄(5-10-15, 더 이상 정확히 기억나지 않음)의 다소 구겨진 코드로 구성되었습니다. 이러한 JSON은 결국 UI로 바뀌거나 오히려 모바일 애플리케이션에서 읽을 수 있는 기사로 바뀌어야 했습니다.
기사는 관리 패널을 사용하여 작성 및 편집되었으며, 새로운 요소를 추가하는 과정은 매우, 믿을 수 없을 만큼, 극도로 고통스럽고 길었습니다. 이 공포를 보고 나는 첫 번째 최적화를 제안하기로 결정했습니다. 불쌍한 콘텐츠 관리자에게 자비를 베풀고 브라우저의 관리자 패널에서 실시간으로 기사를 미리 보는 기능을 구현하는 것입니다.
말하고 끝났습니다. 얼마 후, 애플리케이션의 앙상한 부분이 관리 패널에서 회전하면서 콘텐츠 관리자가 변경 사항을 미리 보는 데 많은 시간을 절약하게 되었습니다. 이전에는 딥 링크를 생성한 다음 각 변경 사항에 대해 개발 빌드를 열고 이 링크를 따라 다운로드를 기다린 다음 모든 것을 반복해야 했다면 이제 간단히 기사를 작성하고 즉시 볼 수 있습니다.
그러나 내 생각은 여기서 끝나지 않았습니다. 나는 이 엔진 과 다른 개발자들이 엔진에 뭔가를 추가해야 하는지 아니면 그냥 Augean 마구간을 청소해야 하는지 결정하는 것이 가능했기 때문에 너무 짜증이 났습니다.
후자라면 개발자는 회의할 때 항상 기분이 좋았을 것입니다. 하지만 냄새는... 카메라가 그것을 포착할 수는 없습니다.
전자라면 개발자는 자주 아프고, 지진을 겪으며 살았고, 컴퓨터 고장, 두통, 운석 충돌, 말기 우울증 또는 과도한 무관심을 겪었습니다.
엔진의 기능을 확장하려면 콘텐츠 관리자가 새로운 기능을 활용할 수 있도록 관리 패널에 수많은 새 필드를 추가해야 했습니다.
이 모든 것을 보면서 나는 놀라운 생각에 빠졌습니다. 이 문제에 대한 일반적인 해결책을 만들어 보면 어떨까요? 관리 패널과 각각의 새로운 요소에 대한 애플리케이션을 지속적으로 조정하고 확장하는 것을 방지하는 솔루션입니다. 문제를 한번에 해결해주는 솔루션! 그리고 여기에 온다 ...
나는 '이 문제는 내가 해결할 수 있다. 수십만은 아니더라도 수십만의 회사를 구할 수 있지만, 회사가 그냥 선물로 주기 에는 그 아이디어가 너무 가치가 있을지도 모른다'고 생각했다.
선물이란 회사의 잠재적 가치 비율이 회사가 급여 형태로 나에게 지불하는 금액과 크기별로 다르다는 것을 의미합니다. 이는 마치 초기 단계에 스타트업에 입사했지만 급여가 대기업에서 제공하는 것보다 적고 회사 지분도 없는 것과 같습니다. 그러면 그 스타트업은 유니콘이 되고 사람들은 이렇게 말하죠. "글쎄요, 우리가 당신에게 월급을 줬어요." 그리고 그들이 옳을 것입니다!
나는 비유를 좋아하지만 그것이 나의 장점이 아니라는 말을 자주 듣습니다. 그것은 마치 당신이 바다에서 헤엄치는 것을 좋아하는 물고기와 같지만, 당신은 민물고기인 것과 같습니다.
그런 다음 구현이 불가능할 수도 있는 몇 가지 아이디어를 제공하여 문제를 해결하지 않기 위해 여가 시간에 개념 증명(POC)을 만들기로 결정했습니다.
원래 계획은 마크다운 렌더링을 위해 기존의 기성 라이브러리를 사용하되, 마크다운 목록의 표준 요소뿐만 아니라 훨씬 더 복잡한 것도 렌더링할 수 있도록 기능을 확장하는 것이었습니다. 기사는 단순히 그림과 텍스트만 있는 것이 아니었습니다. 또한 아름다운 시각적 디자인, 내장 오디오 플레이어 등이 있었습니다.
저는 이 가설을 테스트하기 위해 금요일 저녁부터 월요일 아침까지 40시간을 보냈습니다. 이 라이브러리가 새로운 기능에 대해 얼마나 확장 가능한지, 일반적으로 모든 것이 얼마나 잘 작동하는지, 그리고 가장 중요한 것은 이 솔루션이 악명 높은 엔진을 왕좌에서 전복시킬 수 있는지 여부입니다. 가설이 확인되었습니다. 라이브러리를 뼈대까지 분해하고 약간의 패치를 적용한 후에는 키워드나 특수 구문 구성으로 모든 UI 요소를 등록하는 것이 가능해졌으며 이 모든 것이 쉽게 확장될 수 있었고 가장 중요한 것은 실제로 기사 엔진을 대체할 수 있다는 것입니다. . 나는 15 시간 만에 어딘가에 왔습니다. 나머지 25일은 POC를 마무리하는 데 사용되었습니다.
아이디어는 한 엔진을 다른 엔진으로 교체하는 것뿐만 아니라 아닙니다. 아이디어는 전체 프로세스를 대체하는 것이 었습니다! 관리자 패널에서는 기사를 작성할 수 있을 뿐만 아니라 애플리케이션에 표시되는 콘텐츠도 관리할 수 있습니다. 원래 아이디어는 특정 프로젝트에 얽매이지 않지만 이를 관리할 수 있는 완전한 대체품을 만드는 것이었습니다. 가장 중요한 것은 이 대체품이 해당 기사를 작성하고 즉시 결과를 볼 수 있도록 편리한 편집기를 제공해야 한다는 것입니다.
POC의 경우 에디터만 만들어도 충분하다고 생각했습니다. 그것은 다음과 같이 보였습니다:
40시간 후에는 마크다운과 여러 사용자 정의 XML 태그(예: <container>
)의 난잡한 혼합으로 구성된 작업 코드 편집기, 이 코드의 UI를 실시간으로 표시하는 미리보기, 그리고 가장 큰 이 세상에서 본 적이 없는 아이백. 사용된 "코드 편집기"가 구문 강조가 가능한 또 다른 라이브러리라는 점도 주목할 가치가 있습니다. 그러나 문제는 마크다운을 강조할 수 있고 XML도 강조할 수 있지만 잡동사니 강조가 지속적으로 중단된다는 점입니다. 따라서 40시간 동안 키메라의 원숭이 코딩을 위해 두 개를 더 추가하면 한 병에 두 가지를 모두 강조할 수 있습니다. 이제 물어볼 시간입니다. 다음에 무슨 일이 일어났나요?
다음은 데모였습니다. 나는 두 명의 고위 관리자를 모아 문제 해결에 대한 나의 비전과 실제로이 비전을 확인했다는 사실을 설명하고 무엇이 작동하고 어떻게 작동하며 어떤 가능성이 있는지 보여주었습니다.
사람들은 그 일을 좋아했습니다. 그리고 그것을 사용하고 싶은 욕구가있었습니다. 하지만 지독한 욕심도 있었습니다. 나의 욕심. 그냥 회사에 그렇게 주면 안되나요? 당연히 아니지. 하지만 나도 그럴 계획은 없었어요. 데모는 내가 그들에게 충격을 준 대담한 계획의 일부였습니다. 그들은 단순히 저항할 수 없었고 이 놀랍고 독점적이며 놀라운 개발을 사용하기 위해 어떤 조건도 충족할 준비가 되어 있었습니다. 이 허구(!) 이야기의 내용을 모두 공개하지는 않겠지만, 돈을 원했다는 것만 말씀드리겠습니다. 돈과 휴가. 유급 한 달 휴가와 돈. 돈이 얼마인지는 별로 중요하지 않고, 그 금액이 내 월급과 숫자 6과 연관되어 있는지가 중요할 뿐입니다.
그러나 나는 완전히 무모한 무모한 사람은 아니 었습니다.
도르마무, 거래하러 왔습니다. 그리고 거래는 다음과 같았습니다. 저는 2주 동안 제 모드( 4시간 수면, 20시간 작업 )로 일하고 POC를 "앱 목적으로 사용할 수 있음" 상태로 마무리했으며 이와 동시에 구현합니다. 응용 프로그램의 새로운 기능 - 이 초고속 기능을 사용하는 전체 화면(이 2주가 원래 할당됨). 그리고 2주 후에 또 다른 데모를 진행합니다. 이번에만 우리는 더 많은 사람들, 심지어 회사의 최고 경영진까지 모아서 그들이 본 것이 인상 깊고 그것을 사용하고 싶다면 거래가 완료되고 나는 나의 욕구를 얻고 회사는 슈퍼건을 얻습니다. 그들이 이것을 원하지 않는다면, 나는 지난 2주 동안 무료로 일했다는 사실을 받아들일 준비가 되어 있습니다.
글쎄, 한 달 간의 휴가를 위해 이미 계획했던 우루비치 로의 여행은 불행히도 결코 이루어지지 않았습니다. 매니저들은 그런 대담함에 감히 동의하지 않았습니다. 그리고 나는 시선을 땅으로 낮추고 "고전적인 방식"으로 새 화면을 쪼개러 갔다. 그러나 운명에 패배한 주인공이 무릎을 꿇고 다시 짐승을 길들이려 하지 않는 그런 이야기는 없다.
아니요... 다음과 같은 것 같습니다: 1 , 2 , 3 , 4 , 5 .
이 영화들을 다 보고 나서 나는 이것이 징조 라고 결정했습니다! 그리고 이렇게 하면 훨씬 더 좋습니다. 그런 유망한 개발을 그곳에서 일부 상품에 판매하는 것이 안타깝습니다( 농담인가요??? ). 그리고 저는 계속해서 제 프로젝트를 더욱 발전시킬 것입니다. 그리고 나는 계속했습니다. 하지만 더 이상 주말에는 40시간이 아니라 비교적 차분한 속도로 일주일에 15~20시간만 일합니다.
제4의 벽을 허무는 것은 쉬운 일이 아니다. 독자가 계속해서 읽고 회사와의 이야기가 어떻게 끝날지 기다리게 만드는 흥미로운 헤드라인을 생각해내려고 노력하는 것과 같습니다. 두 번째 글에서 이야기를 마무리하겠습니다. 이제 이론적으로 이 기사를 기술적이고 HackerNoon을 더 훌륭하게 만드는 구현, 기능적 기능 및 모든 항목으로 전환해야 할 때인 것 같습니다!
가장 먼저 이야기할 것은 구문입니다. 원래의 hodgepodge 아이디어는 POC에 적합했지만 실습에서 알 수 있듯이 마크다운은 그렇게 간단하지 않습니다. 게다가 일부 기본 마크다운 요소를 순수 Flutter 요소와 결합하는 것이 항상 일관되지는 않습니다.
첫 번째 질문은 이미지가 ![Description](Link)
또는 <image>
입니까?입니다.
첫 번째 경우 - 여러 매개변수를 어디에 넣어야 합니까?
두 번째라면 왜 첫 번째가 있습니까?
두 번째 질문은 문자입니다. 텍스트 스타일링에 대한 Flutter의 가능성은 무한합니다. 마크다운의 가능성은 "그렇다"입니다. 예, 텍스트를 굵게 또는 기울임꼴로 표시할 수 있으며 이러한 구성을 **
/ __
스타일에 사용하려는 생각도 있었습니다. 그러다가 <color="red">
text </color>
태그를 가운데에 밀어넣을까 하는 생각도 해보았지만, 이는 눈에서 피가 흘러나오는 곡선과 오돌토돌한 모습입니다. 자체적인 한계 구문을 갖춘 일종의 HTML을 얻는 것은 전혀 바람직하지 않았습니다. 게다가 이 코드는 기술적 지식이 없는 관리자라도 작성할 수 있다는 생각이었습니다.
차근차근 키메라 부분을 제거하고 마크다운된 슈퍼뮤턴트를 얻었습니다. 즉, 마크다운 렌더링을 위한 패치된 라이브러리가 있지만 사용자 정의 태그로 채워져 있고 마크다운 지원은 없습니다. 즉, 마치 XML을 얻은 것과 같습니다.
나는 다른 간단한 구문이 무엇인지 생각하고 실험하기 위해 자리에 앉았습니다. JSON은 슬래그입니다. 비뚤어진 Flutter 편집기에서 사람이 JSON을 작성하게 하면 당신을 죽이고 싶어하는 미치광이가 생길 것입니다. JSON은 일반적으로 사람이 입력하는 데 적합하지 않은 것 같습니다. 특히 UI의 경우 JSON은 지속적으로 오른쪽으로 성장하고 필수 ""
가 많으며 설명이 없습니다. YAML? 글쎄요. 그러나 코드는 옆으로도 크롤링됩니다. 흥미로운 링크가 있지만 이들의 도움만으로는 많은 것을 얻을 수 없습니다. TOML? Pf-ff.
좋아, 결국 나는 XML에 정착했다. 나에게는 이것이 UI에 매우 적합한 다소 "밀도가 높은" 구문인 것처럼 보였고 지금도 마찬가지입니다. 결국 HTML 레이아웃 디자이너는 여전히 존재하며 여기에서는 모든 것이 웹보다 훨씬 간단할 것입니다( 아마도 ).
다음으로 질문이 생겼습니다. 강조 표시/코드 완성 가능성을 얻는 것이 좋을 것입니다. 논리적 구성뿐만 아니라 일종의 {{ user.name }}
입니다. 그런 다음 Twig, Liquid로 실험을 시작했고 더 이상 기억하지 못하는 다른 템플릿 엔진을 살펴보았습니다. 그러나 나는 또 다른 문제에 직면했습니다. 예를 들어 Twig와 같은 표준 엔진에서 계획된 것의 일부를 구현하는 것은 가능하지만 모든 것을 구현하는 것은 확실히 작동하지 않습니다. 자동 완성 및 강조 표시 기능이 있다는 것은 좋지만 Flutter에 필요한 표준 Twig 구문 위에 자신만의 새로운 기능을 추가하는 경우에만 방해가 됩니다. 결과적으로 XML을 사용하면 모든 것이 잘 풀렸고 Twig/Liquid를 사용한 실험에서는 뛰어난 결과를 얻지 못했으며 특정 지점에서는 일부 기능을 구현할 수 없는 경우도 있었습니다. 따라서 선택은 여전히 XML로 남아 있습니다. 기능에 대해 더 자세히 설명하겠지만 지금은 Twig/Liquid에서 매우 매력적이었던 자동 완성 및 강조 표시에 중점을 두겠습니다.
다음으로 말하고 싶은 것은 Flutter의 텍스트 입력이 비뚤어졌다는 것입니다. 모바일 형식에서 잘 작동합니다. 높이가 최대 5-10줄인 경우 데스크톱 형식에도 적합합니다. 그러나 이 편집기가 Flutter에서 구현되는 본격적인 코드 편집기의 경우 눈물 없이는 볼 수 없습니다. 모든 작업을 추적하고 메모와 아이디어를 작성하는 Trello 에는 다음과 같은 "작업"이 있습니다.
사실 저는 프로젝트 작업 초기부터 Nui 코드 편집기를 좀 더 적합한 편집기로 교체하자는 생각을 염두에 두었습니다. 예를 들어 VS Code의 오픈 소스 부분이 포함된 웹 보기를 포함합니다. 그러나 지금까지 내 손은 여기에 도달하지 못했고, 게다가 이 편집기의 곡률 문제에 대한 버릇없지만 여전히 작동하는 솔루션이 내 마음에 떠올랐습니다. 대신 자신의 개발 환경을 사용하는 것입니다.
이는 다음과 같이 수행됩니다. 이상적으로는 .html
/ .twig
확장자를 사용하여 UI 코드(XML)로 파일을 생성하고 CMS(웹/데스크톱/로컬/배포)를 통해 동일한 파일을 엽니다. 중요하지 않습니다. 웹 버전의 VS Code를 통해서도 모든 IDE를 통해 동일한 파일을 열 수 있습니다. 짜잔, 즐겨 사용하는 도구에서 이 파일을 편집할 수 있으며, 브라우저나 어느 곳에서나 실시간 미리보기를 이용할 수 있습니다.
이러한 시나리오에서는 본격적인 자동 완성을 망칠 수도 있습니다. VS Code에서는 사용자 정의 HTML 태그를 통해 구현할 수 있습니다. 그러나 저는 VS Code를 사용하지 않습니다. 제가 선택한 것은 IntelliJ IDEA이며 이 IDE에는 더 이상 그렇게 간단한 솔루션이 없습니다(글쎄, 적어도 없었거나 적어도 찾지 못했습니다). 그러나 여기 저기 모두에서 작동하는 보다 일반적인 솔루션이 있습니다. 바로 XSD(XML 스키마 정의)입니다. 나는 이 괴물을 알아내기 위해 약 3일 저녁을 보냈지만 결코 성공하지 못했고 결국 이 문제를 포기하고 더 나은 시간을 위해 남겨 두었습니다.
또한 XML을 위젯으로 변환하는 엔진과 같은 실험과 업데이트를 여러 번 반복한 끝에 결국 언어가 특별히 중요하지 않은 솔루션을 얻었다는 점도 흥미롭습니다. UI 구조에 대한 정보 전달자로서 선택은 결국 XML에 속했지만 동시에 JSON 및 심지어 바이너리 형식인 컴파일된 Protobuf까지 안전하게 제공할 수 있습니다. 이는 다음 주제로 넘어갑니다.
이 문장에서 이 기사의 크기는 3218 단어가 됩니다. 내가 이 섹션을 작성하기 시작했을 때 모든 것을 질적으로 수행하려면 Nui와 일반 Flutter 렌더링 성능을 비교하는 많은 테스트 사례를 작성해야 했습니다. 이미 Nui에서 완전히 생성된 데모 화면을 구현했기 때문에:
기본적으로 (물론 Flutter의 맥락에서) 화면과 정확히 일치하는 항목을 생성해야 했습니다. 결과적으로 3주 이상이 걸렸고, 같은 것을 여러 번 다시 작성하고, 테스트 프로세스를 개선하고, 점점 더 흥미로운 숫자를 얻었습니다. 그리고 이 섹션의 크기만 해도 3500 단어가 넘었습니다. 따라서 나는 특별한 경우로서 Nui의 성능과 Nui를 사용하기로 결정한 경우 지불해야 하는 추가 비용에 대해 전적으로 그리고 완전히 전념할 별도의 기사를 작성하는 것이 합리적이라는 생각에 이르렀습니다. 접근 방식으로서의 서버 기반 UI.
그러나 작은 스포일러를 하나 하겠습니다. 제가 고려한 성능 평가에는 두 가지 주요 시나리오, 즉 초기 렌더링 시간이 있었습니다. 서버 기반 UI에서 전체 화면을 구현하기로 결정한 경우 중요하며 이 화면은 애플리케이션의 어딘가에서 열립니다.
따라서 이 화면이 매우 무거우면 기본 Flutter 화면이라도 렌더링하는 데 오랜 시간이 걸리므로 이러한 화면으로 전환할 때, 특히 이 전환에 애니메이션이 수반되는 경우 지연이 눈에 띄게 됩니다. 두 번째 시나리오는 동적 UI 변경이 포함된 프레임 시간(FPS) 입니다. 데이터가 변경되었습니다. 일부 구성요소를 다시 그려야 합니다. 문제는 이것이 렌더링 시간에 얼마나 영향을 미칠지, 화면이 업데이트될 때 사용자에게 지연이 나타날 정도로 영향을 미칠지 여부입니다. 그리고 여기에 또 다른 스포일러가 있습니다. 대부분의 경우 여러분이 보고 있는 화면이 Nui에서 완전히 구현되었는지 알 수 없을 것입니다. Nui 위젯을 일반적인 기본 Flutter 화면(예: 애플리케이션에서 매우 동적으로 변경되어야 하는 화면의 일부 영역)에 삽입하는 경우 이를 인식할 수 없다는 것이 보장됩니다. 물론 성능 저하도 있습니다. 그러나 120FPS의 프레임 속도에서도 FPS에 영향을 미치지 않습니다. 즉, 한 프레임의 시간은 거의 8ms
초과하지 않습니다. 이는 두 번째 시나리오에도 해당됩니다. 첫 번째는 모두 화면의 복잡성 수준에 따라 다릅니다. 그러나 여기서도 차이는 인식에 영향을 미치지 않으며 귀하의 애플리케이션이 사용자 스마트폰의 벤치마크가 되지 않을 정도입니다.
다음은 Pixel 7a(Tensor G2, 화면 새로 고침 빈도가 90프레임으로 설정됨(이 장치의 경우 최대), 비디오 녹화 속도가 초당 60프레임(녹화 설정의 경우 최대))의 세 가지 화면 녹화입니다. 500ms마다 요소의 위치는 목록은 처음 3개의 카드가 생성된 데이터에서 무작위로 지정되고, 500ms 후에 주문 상태가 다음 화면으로 전환됩니다. 이 화면 중 어느 화면이 전적으로 Nui에 구현되어 있는지 추측할 수 있습니까?
PS 이미지 로드 시간은 구현에 따라 달라지지 않습니다. 이 화면에는 모든 구현에 대해 모든 아이콘과 브랜드 로고 등 많은 Svg 이미지가 있기 때문입니다. 모든 svg(일반 사진 포함)는 GitHub에 호스팅으로 저장되므로 로드 속도가 상당히 느려질 수 있으며 이는 일부 실험에서 관찰됩니다.
유튜브:
Nui를 만들 때 나는 다음 개념을 고수했습니다. 우선 Flutter 개발자가 일반 Flutter 애플리케이션을 만드는 것만큼 쉽게 사용할 수 있는 도구를 만드는 것이 필요합니다. 따라서 모든 구성 요소의 이름을 지정하는 접근 방식은 간단했습니다. Flutter에서 이름을 지정하는 것과 동일한 방식으로 이름을 지정하는 것입니다.
위젯 매개변수에도 동일하게 적용됩니다. String
, int
, double
, enum
등과 같은 스칼라는 매개변수로 자체적으로 구성되지 않습니다. Nui 내에서 이러한 유형의 매개변수를 인수 라고 합니다. 그리고 Container
위젯의 decoration
과 같은 복잡한 클래스 매개변수에는 property 라고 합니다. 일부 속성은 너무 장황하므로 이 규칙은 절대적이지 않으므로 해당 이름이 단순화되었습니다. 또한 일부 위젯의 경우 사용 가능한 매개변수 목록이 확장되었습니다. 예를 들어 정사각형 SizedBox
또는 Container
만들려면 두 개의 동일한 width
+ height
대신 size
라는 사용자 정의 인수 하나만 전달할 수 있습니다.
구현된 위젯의 전체 목록은 제공하지 않습니다. 그 수가 꽤 많기 때문입니다(현재 53개). 간단히 말해서, 원칙적으로 서버 기반 UI를 접근 방식으로 사용하는 것이 적합한 거의 모든 UI를 구현할 수 있습니다. Slivers
와 관련된 복잡한 스크롤 효과를 포함합니다.
또한 구성 요소와 관련하여 클라우드 XML 코드를 전달해야 하는 진입점이나 위젯에 주목할 가치가 있습니다. 현재 NuiListWidget
및 NuiStackWidget
이라는 두 가지 위젯이 있습니다.
첫 번째 화면은 전체 화면을 구현해야 하는 경우에 사용하도록 설계되었습니다. 내부적으로는 원본 마크업 코드에서 구문 분석될 모든 위젯을 포함하는 CustomScrollView
입니다. 더욱이 구문 분석은 "지능적"이라고 말할 수 있습니다. CustomScrollView
의 콘텐츠는 slivers
여야 하므로 가능한 해결책은 스트림의 각 위젯을 SliverToBoxAdapter
로 래핑하는 것이지만 이는 매우 부정적인 영향을 미칩니다. 성능에. 따라서 위젯은 다음과 같이 부모에 포함됩니다. 첫 번째 위젯부터 시작하여 실제 sliver
만날 때까지 목록을 아래로 내려갑니다. sliver
만나자마자 이전 위젯을 모두 SliverList
에 추가하고 이를 상위 CustomScrollView
에 추가합니다. 따라서 slivers
수가 최소화되므로 전체 UI 렌더링 성능이 최대한 높아집니다. CustomScrollView
에 많은 slivers
있는 것이 왜 나쁜가요? 대답은 여기에 있습니다.
두 번째 위젯인 NuiStackWidget
전체 화면으로도 사용할 수 있습니다. 이 경우 생성한 모든 항목은 동일한 순서로 Stack
에 포함된다는 점을 명심해야 합니다. 또한 slivers
명시적으로 사용해야 합니다. 즉, slivers
목록을 원할 경우 CustomScrollView
추가하고 그 안에 목록을 이미 구현해야 합니다.
두 번째 시나리오는 기본 구성 요소에 포함될 수 있는 작은 위젯을 구현하는 것입니다. 예를 들어, 서버 주도로 완전히 사용자 정의할 수 있는 제품 카드를 만드는 것입니다. Nui를 사용하여 컴포넌트 라이브러리의 모든 컴포넌트를 구현하고 이를 일반 위젯으로 사용할 수 있는 매우 흥미로운 시나리오인 것 같습니다. 동시에 애플리케이션을 업데이트하지 않고도 완전히 변경할 수 있는 기회가 항상 있습니다.
NuiListWidget
전체 화면이 아닌 로컬 위젯으로도 사용될 수 있다는 점은 주목할 가치가 있습니다. 하지만 이 위젯의 경우 부모 위젯에 대해 명시적인 높이를 설정하는 등 적절한 제한을 적용해야 합니다.
Flutter를 사용하여 생성된 counter app
다음과 같습니다.
import 'package:flutter/material.dart'; import 'package:nui/nui.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Nui App', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const MyHomePage(title: 'Nui Demo App'), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({ required this.title, super.key, }); final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(widget.title), ), body: Center( child: NuiStackWidget( renderers: const [], imageErrorBuilder: null, imageFrameBuilder: null, imageLoadingBuilder: null, binary: null, nodes: null, xmlContent: ''' <center> <column mainAxisSize="min"> <text size="18" align="center"> You have pushed the button\nthis many times: </text> <text size="32"> {{ page.counter }} </text> </column> </center> ''', pageData: { 'counter': _counter, }, ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add), ), ); } }
그리고 여기에 완전히 Nui에 관한 또 다른 예가 있습니다(논리 포함):
import 'package:flutter/material.dart'; import 'package:nui/nui.dart'; void main() { runApp(const MyApp()); } final DataStorage globalDataStorage = DataStorage(data: {'counter': 0}); final EventHandler counterHandler = EventHandler( test: (BuildContext context, Event event) => event.event == 'increment', handler: (BuildContext context, Event event) => globalDataStorage.updateValue( 'counter', (globalDataStorage.getTypedValue<int>(query: 'counter') ?? 0) + 1, ), ); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return DataStorageProvider( dataStorage: globalDataStorage, child: EventDelegate( handlers: [ counterHandler, ], child: MaterialApp( title: 'Nui App', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const MyHomePage(title: 'Nui Counter'), ), ), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({ required this.title, super.key, }); final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(widget.title), ), body: Center( child: NuiStackWidget( renderers: const [], imageErrorBuilder: null, imageFrameBuilder: null, imageLoadingBuilder: null, binary: null, nodes: null, xmlContent: ''' <center> <column mainAxisSize="min"> <text size="18" align="center"> You have pushed the button\nthis many times: </text> <dataBuilder buildWhen="counter"> <text size="32"> {{ data.counter }} </text> </dataBuilder> </column> </center> <positioned right="16" bottom="16"> <physicalModel elevation="8" shadowColor="FF000000" clip="antiAliasWithSaveLayer"> <prop:borderRadius all="16"/> <material type="button" color="EBDEFF"> <prop:borderRadius all="16"/> <inkWell onPressed="increment"> <prop:borderRadius all="16"/> <tooltip text="Increment"> <sizedBox size="56"> <center> <icon icon="mdi_plus" color="21103E"/> </center> </sizedBox> </tooltip> </inkWell> </material> </physicalModel> </positioned> ''', pageData: {}, ), ), ); } }
강조표시되도록 UI 코드를 분리합니다.
<center> <column mainAxisSize="min"> <text size="18" align="center"> You have pushed the button\nthis many times: </text> <dataBuilder buildWhen="counter"> <text size="32"> {{ data.counter }} </text> </dataBuilder> </column> </center> <positioned right="16" bottom="16"> <physicalModel elevation="8" shadowColor="black" clip="antiAliasWithSaveLayer"> <prop:borderRadius all="16"/> <material type="button" color="EBDEFF"> <prop:borderRadius all="16"/> <inkWell onPressed="increment"> <prop:borderRadius all="16"/> <tooltip text="Increment"> <sizedBox size="56"> <center> <icon icon="mdi_plus" color="21103E"/> </center> </sizedBox> </tooltip> </inkWell> </material> </physicalModel> </positioned>
각 위젯의 인수와 속성은 물론 가능한 모든 값에 대한 자세한 정보를 보여주는 포괄적인 대화형 문서도 있습니다. 인수와 기타 속성을 모두 가질 수 있는 각 속성에 대해 사용 가능한 모든 값에 대한 전체 데모가 포함된 문서도 있습니다. 이 외에도 각 구성 요소에는 이 위젯의 구현을 실시간으로 확인하고 원하는 대로 변경하여 사용할 수 있는 대화형 예제가 포함되어 있습니다.
Nui는 Nanc CMS에 매우 긴밀하게 통합되어 있습니다. Nui를 사용하기 위해 반드시 Nanc를 사용할 필요는 없지만 Nanc를 사용하면 동일한 대화형 문서는 물론 레이아웃 결과를 실시간으로 확인하고 데이터를 가지고 놀 수 있는 Playground와 같은 이점을 얻을 수 있습니다. 그 안에 사용될 것입니다. 또한 CMS의 자체 로컬 빌드를 만들 필요가 없으며 게시된 데모를 통해 필요한 모든 작업을 수행할 수 있습니다.
링크를 따라간 다음 Page Interface / Screen
필드를 클릭하면 됩니다. 열린 화면은 플레이그라운드로 활용할 수 있으며, 동기화 버튼을 클릭하면 소스가 포함된 파일을 통해 Nanc와 IDE를 동기화할 수 있으며, 도움말 버튼을 클릭하면 모든 문서를 볼 수 있습니다.
PS 이러한 복잡성은 Nanc의 구성 요소에 대한 문서가 포함된 명시적인 별도 페이지를 만들 시간을 찾지 못했을 뿐만 아니라 이 페이지에 대한 직접 링크를 삽입할 수 없기 때문에 발생합니다.
XML에서 위젯까지 일반 매퍼를 생성하는 것은 너무 의미가 없습니다. 물론 이것은 유용할 수도 있지만 사용 사례는 훨씬 적습니다. 똑같은 것은 아닙니다. 상호 작용할 수 있고 세부적으로 업데이트할 수 있는 완전한 대화형 구성 요소 및 화면입니다(즉, 한 번에 모두가 아니라 업데이트가 필요한 부분만). 또한 이 UI에는 데이터가 필요합니다. Server-Driven UI라는 문구에 문자 S 가 있다는 점을 고려하면 서버의 레이아웃으로 직접 대체할 수 있지만 더 아름답게 할 수도 있습니다. 그리고 UI가 변경될 때마다 백엔드에서 레이아웃의 새로운 부분을 드래그하지 마세요(Nui는 jQuery의 모범 사례를 펄럭이게 하는 타임머신이 아닙니다).
논리부터 시작해 보겠습니다. 변수와 계산된 표현식을 레이아웃으로 대체할 수 있습니다. 위젯이 <container color="{{ page.background }}">
로 정의되었다고 가정하면 background
변수에 저장된 "상위 컨텍스트"에 전달된 데이터에서 직접 색상을 추출합니다. 그리고 <aspectRatio ratio="{{ 3 / 4}}">
하위 항목에 해당하는 종횡비 값을 설정합니다. 일부 로직을 사용하여 UI를 구축하는 데 사용할 수 있는 내장 함수, 비교 등이 있습니다.
두 번째 요점은 템플릿입니다 . <template id="your_component_name"/>
태그를 사용하여 UI 코드에서 직접 위젯을 정의할 수 있습니다. 동시에 이 템플릿의 모든 내부 구성 요소는 이 템플릿에 전달된 인수에 액세스할 수 있으므로 사용자 정의 구성 요소의 유연한 매개 변수화를 허용한 다음 <component id="your_component_name"/>
태그를 사용하여 재사용할 수 있습니다. 템플릿 내에서는 속성뿐만 아니라 다른 태그/위젯도 전달할 수 있으므로 어떤 복잡성이라도 재사용 가능한 구성 요소를 생성할 수 있습니다.
포인트 3 - "for 루프". Nui에는 반복을 사용하여 동일한(또는 여러) 구성 요소를 여러 번 렌더링할 수 있는 내장 <for>
태그가 있습니다. 이는 위젯의 목록/행/열을 생성해야 하는 데이터 세트가 있는 경우 편리합니다.
넷째 - 조건부 렌더링. 레이아웃 수준에서는 <show>
태그가 구현됩니다( <if>
라고 부르는 아이디어가 있음). 이를 통해 중첩된 구성 요소를 그리거나 다양한 조건에서 트리에 포함하지 않을 수 있습니다.
포인트 5 - 행동. 사용자가 상호 작용할 수 있는 일부 구성 요소는 이벤트를 보낼 수 있습니다 . 원하는 대로 완전히 제어할 수 있습니다. <inkWell onPressed="something">
이라고 가정해 보겠습니다. 이러한 선언을 사용하면 이 위젯을 클릭할 수 있게 되고 애플리케이션 또는 일부 EventHandler
이 이벤트를 처리하고 작업을 수행할 수 있게 됩니다. 논리와 관련된 모든 것은 애플리케이션에서 직접 구현되어야 하지만 무엇이든 구현할 수 있다는 아이디어입니다. "화면으로 이동" / "호출 방법" / "분석 이벤트 보내기"와 같은 작업 그룹을 처리할 수 있는 일반 핸들러를 만듭니다. 동적 코드를 구현할 계획도 있지만 여기에는 미묘한 차이가 있습니다. Dart의 경우 임의의 코드를 실행하는 방법이 있지만 이는 성능에 영향을 미치고 게다가 이 코드와 애플리케이션 코드의 상호 운용성은 거의 100%가 아닙니다. 즉, 이 동적 코드에서 로직을 생성하면 지속적으로 몇 가지 제한 사항에 직면하게 됩니다. 따라서 이 메커니즘을 실제로 적용하고 유용하게 사용하려면 매우 신중하게 작업해야 합니다.
여섯번째 포인트는 로컬 UI 업데이트입니다. 이는 <dataBuilder>
태그 덕분에 가능합니다. 이 태그(내부 블록)는 특정 필드를 "볼" 수 있으며, 변경되면 해당 하위 트리를 다시 그립니다.
처음에는 위에서 언급한 "상위 컨텍스트"라는 두 데이터 저장소의 경로를 따랐습니다. "데이터"는 물론 <data>
태그를 사용하여 UI에서 직접 정의할 수 있는 데이터입니다. 솔직히 말해서 데이터를 UI에 저장하고 전송하는 두 가지 방법을 구현해야 하는 이유에 대한 논쟁이 지금 기억나지 않지만 그러한 결정에 대해 나 자신을 가혹하게 비판할 수는 없습니다.
이는 다음과 같이 작동합니다. "상위 컨텍스트"는 NuiListWidget
/ NuiStackWidget
위젯에 직접 전달되는 Map<String, dynamic>
유형의 객체입니다. 이 데이터에 대한 접근은 접두사 page
통해 가능합니다:
<someWidget value="{{ page.your.field }}"/>
배열( {{ page.some.array.0.users.35.age }}
을 포함하여 모든 깊이에 대해 무엇이든 참조할 수 있습니다. 해당 키/값이 없으면 null
발생합니다. <for>
사용하여 목록을 반복할 수 있습니다.
두 번째 방법 - "데이터"는 전역 데이터 저장소입니다. 실제로 이는 NuiListWidget
/ NuiStackWidget
보다 트리에서 더 높은 곳에 위치한 특정 Bloc
입니다. 동시에 DataStorageProvider
통해 DataStorage
의 자체 인스턴스를 전달하여 로컬 스타일로 사용을 구성하는 것을 방해하는 것은 없습니다.
동시에 첫 번째 방법은 반응적이지 않습니다. 즉, page
의 데이터가 변경되면 UI가 자체적으로 업데이트되지 않습니다. 사실 이것은 StatelessWidget
의 인수일 뿐입니다. page
의 데이터 소스가 Nui...Widget
에 값 세트를 제공하는 자체 Bloc인 경우 일반 StatelessWidget
과 마찬가지로 newdata로 완전히 다시 그려집니다.
데이터를 사용하는 두 번째 방법은 반응형입니다. 이 클래스의 API인 updateValue
메소드를 사용하여 DataStorage
의 데이터를 변경하는 경우 Bloc 클래스의 emit
메소드가 호출되고 UI에 이 데이터의 활성 리스너가 있는 경우 <dataBuilder>
태그는 다음과 같습니다. 내용은 이에 따라 변경되지만 나머지 UI는 건드리지 않습니다.
따라서 우리는 매우 간단한 page
와 반응형 data
라는 두 가지 잠재적인 데이터 소스를 얻습니다. 이러한 소스의 데이터를 업데이트하는 논리와 이러한 업데이트에 대한 UI의 반응을 제외하면 둘 사이에는 차이가 없습니다.
나는 의도적으로 작업의 모든 뉘앙스와 측면을 설명하지 않았습니다. 왜냐하면 그것은 이미 존재하는 문서 의 사본으로 판명되기 때문입니다. 따라서 시도하거나 더 많은 것을 배우고 싶은 경우 여기에서 환영해 주시기 바랍니다. 작업의 어떤 측면이 명확하지 않거나 문서에서 어떤 내용을 다루지 않는 경우 문제를 나타내는 귀하의 메시지에 기뻐할 것입니다.
이 문서에서 다루지 않았지만 사용할 수 있는 몇 가지 기능을 간략하게 나열하겠습니다.
실시간 미리 보기를 통해 인수 및 속성과 마찬가지로 정확히 동일한 대화형 문서를 생성할 수 있는 기능을 사용하여 자신만의 태그/구성 요소를 생성합니다. 예를 들어 SVG 이미지를 렌더링하는 구성 요소가 구현되는 방법은 다음과 같습니다. 엔진의 핵심으로 밀어넣는 것은 의미가 없습니다. 왜냐하면 모든 사람이 필요로 하는 것은 아니지만 단지 하나의 변수를 전달하여 사용할 수 있는 확장으로 쉽고 간단하기 때문입니다. 직접 -구현 예 .
자신의 아이콘을 추가하여 확장할 수 있는 거대한 내장 아이콘 라이브러리(여기서는 일관성이 없고 "밀어넣은" 것으로 판명되었습니다. 논리는 가능한 한 많은 아이콘을 즉시 사용할 수 있도록 만드는 것이었고 그럴 필요가 없었습니다.) 새 아이콘을 사용하려면 애플리케이션을 업데이트하세요.) 기본적으로 fluentui_system_icons , Material_design_icons_flutter 및 remixicon 을 사용할 수 있습니다. Nanc , Page Interface / Screen -> Icons
사용하여 사용 가능한 모든 아이콘을 볼 수 있습니다.
즉시 사용 가능한 Google 글꼴을 포함한 맞춤 글꼴
XML을 JSON/protobuf로 변환하고 이를 UI용 "소스"로 사용
이 모든 것과 훨씬 더 많은 내용을 문서 에서 연구할 수 있습니다.
가장 중요한 것은 로직을 사용하여 코드를 동적으로 실행할 수 있는 가능성을 알아내는 것입니다. 이것은 Nui의 기능을 매우 심각하게 확장할 수 있게 해주는 매우 멋진 기능입니다. 또한 표준 Flutter 라이브러리에서 거의 사용되지 않지만 때로는 매우 중요한 나머지 위젯을 추가할 수 있고 추가해야 합니다. 모든 태그에 대한 자동 완성 기능이 IDE에 나타나도록 XSD를 마스터하려면(태그 문서에서 직접 이 구성표를 생성하는 아이디어가 있으므로 사용자 정의 위젯용으로 쉽게 생성할 수 있으며 항상 최신 상태를 유지합니다) -date, 그리고 Dart에서 생성된 DSL을 만들어 XML/Json/Protobuf로 변환할 수 있다는 아이디어도 있습니다. 음, 그리고 추가적인 성능 최적화 - 지금 당장은 나쁘지도, 아주 나쁘지도 않지만, 훨씬 더 좋아질 수도 있고 기본 Flutter에 더 가까워질 수도 있습니다.
그게 내가 가진 전부입니다. 다음 기사에서는 Nui의 성능, 테스트 케이스를 어떻게 생성했는지, 이 과정에서 몇십 개의 레이크를 통과했는지, 어떤 시나리오에서 어떤 수치를 얻을 수 있는지에 대해 자세히 설명하겠습니다.
Nui를 시험해 보거나 더 잘 알고 싶다면 문서 데스크 로 가십시오. 또한 어렵지 않다면 GitHub 에 별표를 표시하고 pub.dev 에 좋아요를 표시해 주세요. 여러분에게는 어렵지 않지만 이 거대한 보트를 타고 외로운 노 젓는 사람인 저에게는 매우 유용합니다.