360 프론트엔드 스타프로젝트 세 번째 선발 숙제 문제입니다. 600명이 넘는 학생들이 참여해 질문에 답했고, 최종적으로 60명이 합격했습니다. 이 60명의 학생들은 훌륭한 일을 해냈고, 그들의 아이디어, 코딩 스타일, 기능 완성도는 상당히 뛰어납니다. 그러나 고려되지 않은 몇 가지 사항도 있습니다. 예를 들어, 많은 학생들이 다음과 같이 완전한 기능을 구현할 수 있는 것으로 나타났습니다. 요구 사항을 충족하지만 어떻게 해야 할지 모릅니다 개방형 API를 설계합니다. 즉, 제품 요구 사항과 향후 변화를 분석하고 예측하여 어떻게 해야 할지 결정합니다. 열려 있고 무엇을 캡슐화해야 하는지. 답이 맞고 틀리고의 문제가 아니라 경험의 문제입니다.
여기서 참조 버전을 제공합니다. 이는 이 버전이 최고라는 의미는 아니지만, 이 버전을 통해 이러한 복잡한 UI 요구 사항이 발생할 때 어떻게 생각하고 구현해야 하는지 분석할 수 있습니다.
구성요소 설계의 일반 단계
구성요소 설계에는 일반적으로 다음 프로세스가 포함됩니다.
요구사항 이해
기술 선정
구조(UI) 디자인
데이터 및 API 디자인
프로세스 설계
호환성 및 세부 최적화
도구 및 엔지니어링
이러한 프로세스는 모든 구성 요소를 설계할 때 발생하지 않지만 일반적으로 프로젝트는 이러한 프로세스 중 일부에서 해결해야 하는 문제에 항상 직면하게 됩니다. 아래에서 간단히 분석해 보겠습니다.
요구 사항 이해
과제 자체는 일반적인 제스처비밀번호 UI 상호 작용을 설계하는 것입니다. 확인 비밀번호를 선택하여 두 가지 상태 간에 전환할 수 있습니다. 각 상태마다 고유한 프로세스가 있습니다. 따라서 대부분의 학생들은 필요에 따라 전체 구성 요소의 상태 전환 및 프로세스를 캡슐화합니다. 일부 학생들은 특정 UI 스타일 구성 기능을 제공하지만 기본적으로 프로세스 및 상태 전환 프로세스에서 노드를 열 수 없습니다. 실제로 이 구성요소를 사용자가 사용하려면 당연히 프로세스 노드를 열어야 합니다. 즉, 사용자는 비밀번호 설정 과정, 비밀번호 확인 과정에서 어떤 작업을 수행할지 결정해야 합니다. 비밀번호 확인이 성공한 후 수행할 작업은 구성 요소 개발자가 사용자를 대신하여 결정할 수 없습니다.
var password = '11121323'; var locker = new HandLock.Locker({ container: document.querySelector('#handlock'), check: { checked: function(res){ if(res.err){ console.error(res.err); //密码错误或长度太短 [执行操作...] }else{ console.log(`正确,密码是:${res.records}`); [执行操作...] } }, }, update:{ beforeRepeat: function(res){ if(res.err){ console.error(res.err); //密码长度太短 [执行操作...] }else{ console.log(`密码初次输入完成,等待重复输入`); [执行操作...] } }, afterRepeat: function(res){ if(res.err){ console.error(res.err); //密码长度太短或者两次密码输入不一致 [执行操作...] }else{ console.log(`密码更新完成,新密码是:${res.records}`); [执行操作...] } }, } }); locker.check(password);
기술적 선택
이 문제의 UI 표시의 핵심은 9개의 정사각형 그리드와 선택된 작은 점입니다. 기술적으로 말하면 다음과 같은 세 가지 옵션이 있습니다. DOM/Canvas/SVG, 세 가지 모두 기본 UI를 구현할 수 있습니다.
DOM을 사용하는 경우 가장 쉬운 방법은 반응형으로 만들 수 있는 플렉스 레이아웃을 사용하는 것입니다.
DOM을 사용하는 것 외에도 Canvas를 사용하여 그리는 것도 매우 편리합니다.
Canvas는 그리기를 구현합니다. Canvas 사용에는 두 가지 작은 세부 사항이 있습니다. 반응성을 얻으려면 DOM을 사용하여 정사각형 컨테이너를 구성할 수 있습니다.
#container { position: relative; overflow: hidden; width: 100%; padding-top: 100%; height: 0px; background-color: white; }
여기에서는 padding-top:100%를 사용하여 컨테이너 높이를 컨테이너 너비와 동일하게 늘립니다. .
두 번째 세부 사항은 레티나 화면에서 선명한 표시 효과를 얻기 위해 캔버스의 너비와 높이를 두 배로 늘린 다음 변환을 통해 컨테이너의 너비와 높이에 맞게 줄입니다. 규모(0.5).
#container canvas{ position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%) scale(0.5); }
Canvas의 위치는 절대적이므로 기본 너비와 높이는 컨테이너의 너비와 높이와 동일하지 않습니다.
let width = 2 * container.getBoundingClientRect().width; canvas.width = canvas.height = width;
이런 식으로 캔버스에 실선과 연결된 선을 그려서 UI를 구현할 수 있습니다. 구체적인 방법은 이어지는 내용에서 더 자세히 설명하겠습니다.
마지막으로 SVG를 사용한 드로잉을 살펴보겠습니다.
드로잉의 SVG 구현
네이티브 SVG 작업을 위한 API는 그리 편리하지 않기 때문에 Snap.svg 라이브러리 여기서는 Canvas를 사용하는 것과 매우 유사하므로 여기서는 자세히 설명하지 않겠습니다.
SVG의 문제점은 DOM과 Canvas만큼 모바일 호환성이 좋지 않다는 것입니다.
위 세 가지 상황을 토대로 결국 Canvas를 사용하여 구현하기로 결정했습니다.
구조적 디자인
DOM 구조는 Canvas를 사용하여 구현하면 비교적 간단합니다. 반응성을 높이려면 적응형 너비를 갖춘 정사각형 컨테이너를 구현해야 합니다. 이 방법은 이전에 소개되었습니다. 그런 다음 컨테이너에 캔버스를 만듭니다. 여기서 주목해야 할 점은 Canvas를 레이어링해야 한다는 것입니다. 이는 Canvas의 렌더링 메커니즘에서 캔버스의 내용을 업데이트하려면 업데이트할 영역을 새로 고치고 다시 그려야 하기 때문입니다. 자주 변경되는 콘텐츠와 기본적으로 변경되지 않는 콘텐츠를 레이어에서 관리해야 하기 때문에 성능이 크게 향상될 수 있습니다.
3개의 레이어로 나누어짐
在这里我把 UI 分别绘制在 3 个图层里,对应 3 个 Canvas。最上层只有随着手指头移动的那个线段,中间是九个点,最下层是已经绘制好的线。之所以这样分,是因为随手指头移动的那条线需要不断刷新,底下两层都不用频繁更新,但是把连好的线放在最底层是因为我要做出圆点把线的一部分遮挡住的效果。
确定圆点的位置
圆点的位置有两种定位法,第一种是九个九宫格,圆点在小九宫格的中心位置。如果认真的同学,已经发现在前面 DOM 方案里,我们就是采用这样的方式,圆点的直径为 11.1%。第二种方式是用横竖三条线把宽高四等分,圆点在这些线的交点处。
在 Canvas 里我们采用第二种方法来确定圆点(代码里的 n = 3)。
let range = Math.round(width / (n + 1)); let circles = []; //drawCircleCenters for(let i = 1; i <p> </p><p>最后一点,严格说不属于结构设计,但是因为我们的 UI 是通过触屏操作,我们需要考虑 Touch 事件处理和坐标的转换。</p><pre class="brush:php;toolbar:false">function getCanvasPoint(canvas, x, y){ let rect = canvas.getBoundingClientRect(); return { x: 2 * (x - rect.left), y: 2 * (y - rect.top), }; }
我们将 Touch 相对于屏幕的坐标转换为 Canvas 相对于画布的坐标。代码里的 2 倍是因为我们前面说了要让 retina 屏下清晰,我们将 Canvas 放大为原来的 2 倍。
API 设计
接下来我们需要设计给使用者使用的 API 了。在这里,我们将组件功能分解一下,独立出一个单纯记录手势的 Recorder。将组件功能分解为更加底层的组件,是一种简化组件设计的常用模式。
我们抽取出底层的 Recorder,让 Locker 继承 Recorder,Recorder 负责记录,Locker 管理实际的设置和验证密码的过程。
我们的 Recorder 只负责记录用户行为,由于用户操作是异步操作,我们将它设计为 Promise 规范的 API,它可以以如下方式使用:
var recorder = new HandLock.Recorder({ container: document.querySelector('#main') }); function recorded(res){ if(res.err){ console.error(res.err); recorder.clearPath(); if(res.err.message !== HandLock.Recorder.ERR_USER_CANCELED){ recorder.record().then(recorded); } }else{ console.log(res.records); recorder.record().then(recorded); } } recorder.record().then(recorded);
对于输出结果,我们简单用选中圆点的行列坐标拼接起来得到一个唯一的序列。例如 "11121323" 就是如下选择图形:
为了让 UI 显示具有灵活性,我们还可以将外观配置抽取出来。
const defaultOptions = { container: null, //创建canvas的容器,如果不填,自动在 body 上创建覆盖全屏的层 focusColor: '#e06555', //当前选中的圆的颜色 fgColor: '#d6dae5', //未选中的圆的颜色 bgColor: '#fff', //canvas背景颜色 n: 3, //圆点的数量: n x n innerRadius: 20, //圆点的内半径 outerRadius: 50, //圆点的外半径,focus 的时候显示 touchRadius: 70, //判定touch事件的圆半径 render: true, //自动渲染 customStyle: false, //自定义样式 minPoints: 4, //最小允许的点数 };
这样我们实现完整的 Recorder 对象,核心代码如下:
[...] //定义一些私有方法 const defaultOptions = { container: null, //创建canvas的容器,如果不填,自动在 body 上创建覆盖全屏的层 focusColor: '#e06555', //当前选中的圆的颜色 fgColor: '#d6dae5', //未选中的圆的颜色 bgColor: '#fff', //canvas背景颜色 n: 3, //圆点的数量: n x n innerRadius: 20, //圆点的内半径 outerRadius: 50, //圆点的外半径,focus 的时候显示 touchRadius: 70, //判定touch事件的圆半径 render: true, //自动渲染 customStyle: false, //自定义样式 minPoints: 4, //最小允许的点数 }; export default class Recorder{ static get ERR_NOT_ENOUGH_POINTS(){ return 'not enough points'; } static get ERR_USER_CANCELED(){ return 'user canceled'; } static get ERR_NO_TASK(){ return 'no task'; } constructor(options){ options = Object.assign({}, defaultOptions, options); this.options = options; this.path = []; if(options.render){ this.render(); } } render(){ if(this.circleCanvas) return false; let options = this.options; let container = options.container || document.createElement('p'); if(!options.container && !options.customStyle){ Object.assign(container.style, { position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', lineHeight: '100%', overflow: 'hidden', backgroundColor: options.bgColor }); document.body.appendChild(container); } this.container = container; let {width, height} = container.getBoundingClientRect(); //画圆的 canvas,也是最外层监听事件的 canvas let circleCanvas = document.createElement('canvas'); //2 倍大小,为了支持 retina 屏 circleCanvas.width = circleCanvas.height = 2 * Math.min(width, height); if(!options.customStyle){ Object.assign(circleCanvas.style, { position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%) scale(0.5)', }); } //画固定线条的 canvas let lineCanvas = circleCanvas.cloneNode(true); //画不固定线条的 canvas let moveCanvas = circleCanvas.cloneNode(true); container.appendChild(lineCanvas); container.appendChild(moveCanvas); container.appendChild(circleCanvas); this.lineCanvas = lineCanvas; this.moveCanvas = moveCanvas; this.circleCanvas = circleCanvas; this.container.addEventListener('touchmove', evt => evt.preventDefault(), {passive: false}); this.clearPath(); return true; } clearPath(){ if(!this.circleCanvas) this.render(); let {circleCanvas, lineCanvas, moveCanvas} = this, circleCtx = circleCanvas.getContext('2d'), lineCtx = lineCanvas.getContext('2d'), moveCtx = moveCanvas.getContext('2d'), width = circleCanvas.width, {n, fgColor, innerRadius} = this.options; circleCtx.clearRect(0, 0, width, width); lineCtx.clearRect(0, 0, width, width); moveCtx.clearRect(0, 0, width, width); let range = Math.round(width / (n + 1)); let circles = []; //drawCircleCenters for(let i = 1; i { this.clearPath(); }); let records = []; let handler = evt => { let {clientX, clientY} = evt.changedTouches[0], {bgColor, focusColor, innerRadius, outerRadius, touchRadius} = options, touchPoint = getCanvasPoint(moveCanvas, clientX, clientY); for(let i = 0; i { recordingTask.cancel = (res = {}) => { let promise = this.recordingTask.promise; res.err = res.err || new Error(Recorder.ERR_USER_CANCELED); circleCanvas.removeEventListener('touchstart', handler); circleCanvas.removeEventListener('touchmove', handler); document.removeEventListener('touchend', done); resolve(res); this.recordingTask = null; return promise; } let done = evt => { moveCtx.clearRect(0, 0, moveCanvas.width, moveCanvas.height); if(!records.length) return; circleCanvas.removeEventListener('touchstart', handler); circleCanvas.removeEventListener('touchmove', handler); document.removeEventListener('touchend', done); let err = null; if(records.length o.pos.join('')).join('')}; resolve(res); this.recordingTask = null; }; document.addEventListener('touchend', done); }); recordingTask.promise = promise; this.recordingTask = recordingTask; return promise; } }
它的几个公开的方法,recorder 负责记录绘制结果, clearPath 负责在画布上清除上一次记录的结果,cancel 负责终止记录过程,这是为后续流程准备的。
流程设计
接下来我们基于 Recorder 来设计设置和验证密码的流程:
验证密码
设置密码
有了前面异步 Promise API 的 Recorder,我们不难实现上面的两个流程。
验证密码的内部流程
async check(password){ if(this.mode !== Locker.MODE_CHECK){ await this.cancel(); this.mode = Locker.MODE_CHECK; } let checked = this.options.check.checked; let res = await this.record(); if(res.err && res.err.message === Locker.ERR_USER_CANCELED){ return Promise.resolve(res); } if(!res.err && password !== res.records){ res.err = new Error(Locker.ERR_PASSWORD_MISMATCH) } checked.call(this, res); this.check(password); return Promise.resolve(res); }
设置密码的内部流程
async update(){ if(this.mode !== Locker.MODE_UPDATE){ await this.cancel(); this.mode = Locker.MODE_UPDATE; } let beforeRepeat = this.options.update.beforeRepeat, afterRepeat = this.options.update.afterRepeat; let first = await this.record(); if(first.err && first.err.message === Locker.ERR_USER_CANCELED){ return Promise.resolve(first); } if(first.err){ this.update(); beforeRepeat.call(this, first); return Promise.resolve(first); } beforeRepeat.call(this, first); let second = await this.record(); if(second.err && second.err.message === Locker.ERR_USER_CANCELED){ return Promise.resolve(second); } if(!second.err && first.records !== second.records){ second.err = new Error(Locker.ERR_PASSWORD_MISMATCH); } this.update(); afterRepeat.call(this, second); return Promise.resolve(second); }
可以看到,有了 Recorder 之后,Locker 的验证和设置密码基本上就是顺着流程用 async/await 写下来就行了。
细节问题
实际手机触屏时,如果上下拖动,浏览器有默认行为,会导致页面上下移动,需要阻止 touchmove 的默认事件。
this.container.addEventListener('touchmove', evt => evt.preventDefault(), {passive: false});
这里仍然需要注意的一点是, touchmove 事件在 chrome 下默认是一个 Passive Event ,因此 addEventListener 的时候需要传参 {passive: false},否则的话不能 preventDefault。
工具 & 工程化
因为我们的代码使用了 ES6+,所以需要引入 babel 编译,我们的组件也使用 webpack 进行打包,以便于使用者在浏览器中直接引入。
这方面的内容,在之前的博客里有介绍,这里就不再一一说明。
最后,具体的代码可以直接查看 GitHub 工程 。
总结
以上就是今天要讲的全部内容,这里面有几个点我想再强调一下:
在设计 API 的时候思考真正的需求,判断什么该开放、什么该封装
做好技术调研和核心方案研究,选择合适的方案
优化和解决细节问题
위 내용은 Native JS는 제스처 잠금 해제 구성 요소 인스턴스 메서드를 구현합니다(그림).의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

웹 개발에서 JavaScript의 주요 용도에는 클라이언트 상호 작용, 양식 검증 및 비동기 통신이 포함됩니다. 1) DOM 운영을 통한 동적 컨텐츠 업데이트 및 사용자 상호 작용; 2) 사용자가 사용자 경험을 향상시키기 위해 데이터를 제출하기 전에 클라이언트 확인이 수행됩니다. 3) 서버와의 진실한 통신은 Ajax 기술을 통해 달성됩니다.

보다 효율적인 코드를 작성하고 성능 병목 현상 및 최적화 전략을 이해하는 데 도움이되기 때문에 JavaScript 엔진이 내부적으로 작동하는 방식을 이해하는 것은 개발자에게 중요합니다. 1) 엔진의 워크 플로에는 구문 분석, 컴파일 및 실행; 2) 실행 프로세스 중에 엔진은 인라인 캐시 및 숨겨진 클래스와 같은 동적 최적화를 수행합니다. 3) 모범 사례에는 글로벌 변수를 피하고 루프 최적화, Const 및 Lets 사용 및 과도한 폐쇄 사용을 피하는 것이 포함됩니다.

Python은 부드러운 학습 곡선과 간결한 구문으로 초보자에게 더 적합합니다. JavaScript는 가파른 학습 곡선과 유연한 구문으로 프론트 엔드 개발에 적합합니다. 1. Python Syntax는 직관적이며 데이터 과학 및 백엔드 개발에 적합합니다. 2. JavaScript는 유연하며 프론트 엔드 및 서버 측 프로그래밍에서 널리 사용됩니다.

Python과 JavaScript는 커뮤니티, 라이브러리 및 리소스 측면에서 고유 한 장점과 단점이 있습니다. 1) Python 커뮤니티는 친절하고 초보자에게 적합하지만 프론트 엔드 개발 리소스는 JavaScript만큼 풍부하지 않습니다. 2) Python은 데이터 과학 및 기계 학습 라이브러리에서 강력하며 JavaScript는 프론트 엔드 개발 라이브러리 및 프레임 워크에서 더 좋습니다. 3) 둘 다 풍부한 학습 리소스를 가지고 있지만 Python은 공식 문서로 시작하는 데 적합하지만 JavaScript는 MDNWebDocs에서 더 좋습니다. 선택은 프로젝트 요구와 개인적인 이익을 기반으로해야합니다.

C/C에서 JavaScript로 전환하려면 동적 타이핑, 쓰레기 수집 및 비동기 프로그래밍으로 적응해야합니다. 1) C/C는 수동 메모리 관리가 필요한 정적으로 입력 한 언어이며 JavaScript는 동적으로 입력하고 쓰레기 수집이 자동으로 처리됩니다. 2) C/C를 기계 코드로 컴파일 해야하는 반면 JavaScript는 해석 된 언어입니다. 3) JavaScript는 폐쇄, 프로토 타입 체인 및 약속과 같은 개념을 소개하여 유연성과 비동기 프로그래밍 기능을 향상시킵니다.

각각의 엔진의 구현 원리 및 최적화 전략이 다르기 때문에 JavaScript 엔진은 JavaScript 코드를 구문 분석하고 실행할 때 다른 영향을 미칩니다. 1. 어휘 분석 : 소스 코드를 어휘 단위로 변환합니다. 2. 문법 분석 : 추상 구문 트리를 생성합니다. 3. 최적화 및 컴파일 : JIT 컴파일러를 통해 기계 코드를 생성합니다. 4. 실행 : 기계 코드를 실행하십시오. V8 엔진은 즉각적인 컴파일 및 숨겨진 클래스를 통해 최적화하여 Spidermonkey는 유형 추론 시스템을 사용하여 동일한 코드에서 성능이 다른 성능을 제공합니다.

실제 세계에서 JavaScript의 응용 프로그램에는 서버 측 프로그래밍, 모바일 애플리케이션 개발 및 사물 인터넷 제어가 포함됩니다. 1. 서버 측 프로그래밍은 Node.js를 통해 실현되며 동시 요청 처리에 적합합니다. 2. 모바일 애플리케이션 개발은 재교육을 통해 수행되며 크로스 플랫폼 배포를 지원합니다. 3. Johnny-Five 라이브러리를 통한 IoT 장치 제어에 사용되며 하드웨어 상호 작용에 적합합니다.

일상적인 기술 도구를 사용하여 기능적 다중 테넌트 SaaS 응용 프로그램 (Edtech 앱)을 구축했으며 동일한 작업을 수행 할 수 있습니다. 먼저, 다중 테넌트 SaaS 응용 프로그램은 무엇입니까? 멀티 테넌트 SAAS 응용 프로그램은 노래에서 여러 고객에게 서비스를 제공 할 수 있습니다.


핫 AI 도구

Undresser.AI Undress
사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover
사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool
무료로 이미지를 벗다

Clothoff.io
AI 옷 제거제

AI Hentai Generator
AI Hentai를 무료로 생성하십시오.

인기 기사

뜨거운 도구

PhpStorm 맥 버전
최신(2018.2.1) 전문 PHP 통합 개발 도구

Eclipse용 SAP NetWeaver 서버 어댑터
Eclipse를 SAP NetWeaver 애플리케이션 서버와 통합합니다.

SublimeText3 영어 버전
권장 사항: Win 버전, 코드 프롬프트 지원!

Atom Editor Mac 버전 다운로드
가장 인기 있는 오픈 소스 편집기

Dreamweaver Mac版
시각적 웹 개발 도구
