Разработка с Flask, ReactJS, gulp.js, bower, Browserify. Часть 1.

main-logo
image-4292

Давайте посмотрим на небольшую, но мощную JavaScript UI библиотеку ReactJS в действии. Это приложение работает на Python 3 и фремворке Flask, на back-end. Так же мы будем использовать на front-end: gulp.js (для выполнения задач), bower(front-end менеджер пакетов), и Browserify(пакетирование зависимостей JavaScript).

ReactJS

Реагировать библиотека, а не фреймворк. В отличие от клиентских MVC фреймворков таких как: Backbone, Ember, и AngularJS, React не делает никаких предположений о вашей технологии стека, так что вы можете легко интегрировать его в новый или унаследованного кода. Это часто используется, чтобы управлять конкретных областей UI приложение, а не всем пользовательским интерфейсом.

React заботится только о пользовательском интерфейсе, который определяется иерархией модульных views компонентов. Если вы знакомы с Anmgular эти компоненты аналогичны директивам. Компоненты используют синтаксис XML, именуемый JSX, который собирает в JavaScript.

Так компоненты определяются в иерархическом порядке, вам не придется повторно рендерить весь DOM при изменении состояния. Вместо этого, он использует виртуальную DOM, что только повторное оказывает индивидуальные компоненты после изменения состояния, в невероятно быстрых скоростях!

Настройка проекта

Давайте начнем с того, что мы знаем: Flask.

Скачать шаблонный кода из репозитория, извлеките файлы, создавайте и активируйте virtualenv, и установите необходимые модули: pip install -r requirements.txt

Теперь давайте запустим приложение:

 $ sh run.sh 

React — первый раунд

Давайте посмотрим на простой компонент.

Компонент: переход от статичного к React

Мы добавим этот скрипт JSX в наш hello.html. Остановитесь на минутку, чтобы проверить его.

<script type="text/jsx">
  /*** @jsx React.DOM */
  var realPython = React.createClass({
    render: function() {
      return (
<h2>Greetings, from Real Python!</h2>
)
    }
  });
  React.render(
    React.createElement(realPython, null),
    document.getElementById('content')
  );
</script>

Что происходит?

  1. Мы создаем компонент с помощью вызова createClass(), и дали ему имя realPython. React.createClass() принимает один аргумент, объект.
  2. Внутри этого объекта мы добавили функцию render, которая декларативно обновляет DOM во время вызова.
  3. Затем идет возвращение значения Greetings, from Real Python!, в JSX, который представляет актуальный HTML элемент, который будет добавлен к DOM.
  4. В конце, React.render() создает экземпляр компонента realPython и вводит элемент в DOM с ID селектором content.

Трансформация

Что дальше? Мы должны «трансформировать», или скомпилировать, JSX в JavaScript. Это делается очень просто. Обновляем hello.html:

<!DOCTYPE html>
<html>
  <head lang="en">
    <meta charset="UTF-8">
    <title>Flask React</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- styles -->
  </head>
  <body>
<div class="container">
<h1>Flask React</h1>
<div id="content"></div>
    </div>
    <!-- scripts -->
    <script src="http://cdnjs.cloudflare.com/ajax/libs/react/0.12.0/react.js"></script>
    <script src="http://cdnjs.cloudflare.com/ajax/libs/react/0.12.0/JSXTransformer.js"></script>
    <script type="text/jsx">
      /*** @jsx React.DOM */
      var helloWorld = React.createClass({
        render: function() {
          return (
<h2>Greetings, from Real Python!</h2>
)
        }
      });
      React.render(
        React.createElement(helloWorld, null),
        document.getElementById('content')
      );
    </script>
  </body>
</html>

Здесь мы добавили скрипты react.js и JSXTransformer.js. Последний из них используется для «преобразования» синтаксис JSX в обычный JavaScript в браузере.

Обратите внимание, мы не добавили JQuery, так как это не требуется для React.

Вот и все. Запустите сервер Flask и проверьте результат в браузере по адресу: http://localhost:5000/hello

flask-react-hello-world
image-4293

Bower

Вместо того чтобы вручную скачивать JavaScript файлы или подключать их с CDN, давайте использовать Bower для лучшего управлениями этими зависимостями. Bower это мощный менеджер пакетов для фронтальных зависимостей, таких как: JQuery, Bootstrap, React, Angular, Backbone.

Убедитесь, что у вас уже установлен Node и npm. Если его нет то установите его.

Инициализация

Установим Bower с помощью npm:

$ npm install -g bower

npm это менеджер пакетов используется для управления модулями Node. В отличие от PyPi/pip, npm по умолчанию устанавливает зависимости локально. Флаг -g используется для переопределения, чтобы установить Bower глобально.

bower.json

Bower использует файл bower.json для определения зависимостей проекта, который похож на файл requirements.txt. Выполните следующую команду, чтобы в интерактивном режиме создать этот файл:

$ bower init

Просто нажимайте enter что бы установить значения по умолчанию. После этого файл bower.json должен выглядеть примерно так:

{
"name": "ultimate-flask-front-end",
"version": "0.0.1",
"authors": [
"Michael Herman <michael@realpython.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
]
}

npm

npm использует аналогичный bower.json файл, называется package.json, он определяет зависимости проекта. Вы также можете создать его в интерактивном режиме:

$ npm init

Так же установите значения по умолчанию:

{
"name": "ultimate-flask-front-end",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}

Теперь, давайте добавим файл bower в npm зависимости:

$ npm install --save-dev bower

Конфигурация

Наряду с файлом bower.json, мы можем определить параметры конфигурации Bower в файле .bowerrc. Создадим такой файл в корне проекта. Структура проекта должна теперь выглядеть следующим образом:

├── .bowerrc
├── .gitignore
├── bower.json
├── package.json
├── project
│   ├── app.py
│   ├── static
│   │   ├── css
│   │   │   └── style.css
│   │   └── scripts
│   └── templates
│       ├── hello.html
│       └── index.html
├── requirements.txt
└── run.sh

По умолчанию Bower установит пакеты в каталоге «bower_components» в корне проекта. Мы должны изменить это, так как Flask необходимо что бы файлы лежали в static. Таким образом, добавьте следующий JSON код в файл .bowerrc, так что бы Bower автоматически устанавливал файл в нужную директорию:

{
"directory": "./project/static/bower_components"
}

Инициализация

Мы должны установить следующие пакеты для этого проекта:

  • Bootstrap
  • jQuery
  • React

Это можно сделать двумя способами:

  1. Запустите bower install --save для каждого из пакетов (флаг —save добавляет зависимость (название и версию) в файле bower.json.).
  2. Обновите файл bower.json добавив названию и версию пакета, а затем запустить bower install, чтобы установить все зависимости от файла.

Давайте воспользуемся вторым способом.

{
"name": "ultimate-flask-front-end",
"version": "0.0.1",
"authors": [
"Michael Herman <michael@realpython.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"./project/static/bower_components",
"test",
"tests"
],
"dependencies": {
"bootstrap": "~3.3.2",
"jquery": "~2.1.3",
"react": "~0.12.2"
}
}

И запустим bower install

$ bower install
bower cached git://github.com/twbs/bootstrap.git#3.3.2
bower validate 3.3.2 against git://github.com/twbs/bootstrap.git#~3.3.2
bower cached git://github.com/jquery/jquery.git#2.1.3
bower validate 2.1.3 against git://github.com/jquery/jquery.git#~2.1.3
bower cached git://github.com/facebook/react-bower.git#0.12.2
bower validate 0.12.2 against git://github.com/facebook/react-bower.git#~0.12.2
bower install react#0.12.2
bower install jquery#2.1.3
bower install bootstrap#3.3.2

react#0.12.2 project/static/bower_components/react

jquery#2.1.3 project/static/bower_components/jquery

bootstrap#3.3.2 project/static/bower_components/bootstrap
└── jquery#2.1.3

Теперь вы можете проверить файлы в каталоге «project/static/bower_components».

Тест

Обновите hello.html:

<script src="{{ url_for('static', filename='bower_components/react/react.min.js') }}"></script>
<script src="{{ url_for('static', filename='bower_components/react/JSXTransformer.js') }}"></script>

Протестируйте приложение, чтобы убедиться, что оно все еще работает.

Простой блог на Flask

Создание блога кажется квинтэссенцией опыта при изучении нового веб-фреймворк. Я надеюсь, что этот пост даст вам инструменты для создания блога. В этой статье мы рассмотрим основы, чтобы получить функциональный сайт, но оставим много места для персонализации и улучшения. Фактический исходник блога поместится в 200 строчек Python кода.

Эта статья предназначено для начинающих разработчиков Python, среднего уровня, или опытных разработчиков, которые решили изучить Flask.

Спецификация

Особенности создаваемого блога:

  • Записи, отформатированные с использованием markdown
  • Записи поддерживают подсветку синтаксиса
  • Автоматическое видео/мультимедиа вставка с помощью OEmbed.
  • Очень красивый полнотекстовый поиск благодаря расширению SQLite FST.
  • Пагинация
  • Публикация записей

Вот примерный вид того, как блог будет выглядеть в конце.

Главная страница

p1425775025.68_800x800
image-4249

 

Страница записей

p1425775019.9_800x800
image-4250

 

Начинаем

Если вы хотите, чтобы пропустить статью и перейти непосредственно к коду, вы можете перейти по ссылке на GitHub.

Для начала, давайте создадим virtualenv и установим необходимые пакеты. Virtualenv, это практически стандартная библиотека, и она используется для создания изолированных, автономных сред Python, в которую вы можете установить свои пакеты. Посмотрите документацию по установке virtualenv.

Для нашего приложения мы должны установить следующие пакеты:

  • Flask, сам веб-фреймворк.
  • Peewee,  для хранения записей в базе данных и выполнения запросов.
  • pygments, подсветка синтаксиса с поддержкой огромного количества разных языков.
  • Markdown, форматирование для наших записей.
  • micawber, для преобразования адресов в обьекты. Например, если вы хотите встроить видео с YouTube, просто поместите URL на видео в свой пост и видео-плеер будет автоматически появляться на этом месте.
  • BeautifulSoup, требуется Микобером для разбора HTML.

 

$ virtualenv blog
New python executable in blog/bin/python2
Also creating executable in blog/bin/python
Installing setuptools, pip...done.
$ cd blog/
$ source bin/activate
(blog)$ pip install flask peewee pygments markdown micawber BeautifulSoup
...
Successfully installed flask peewee pygments markdown micawber BeautifulSoup Werkzeug Jinja2 itsdangerous markupsafe
Cleaning up...

Наш приложение будет находится в файле app.py. Мы также создадим несколько папок для статических файлов(стилей, JavaScript файлов) и папку для шаблонов.

(blog)$ mkdir app
(blog)$ cd app
(blog)$ touch app.py
(blog)$ mkdir {static,templates}

Настройка приложения Flask

Давайте начнем редактирования app.py и настроим наше приложение. Значения конфигурации могут быть размещены в отдельном модуле, но для простоты мы будем просто добавим их в том же файле что и наше приложение.

# app.py
import datetime
import functools
import os
import re
import urllib
from flask import (Flask, abort, flash, Markup, redirect, render_template,
                   request, Response, session, url_for)
from markdown import markdown
from markdown.extensions.codehilite import CodeHiliteExtension
from markdown.extensions.extra import ExtraExtension
from micawber import bootstrap_basic, parse_html
from micawber.cache import Cache as OEmbedCache
from peewee import *
from playhouse.flask_utils import FlaskDB, get_object_or_404, object_list
from playhouse.sqlite_ext import *
ADMIN_PASSWORD = 'secret'
APP_DIR = os.path.dirname(os.path.realpath(__file__))
DATABASE = 'sqliteext:///%s' % os.path.join(APP_DIR, 'blog.db')
DEBUG = False
SECRET_KEY = 'shhh, secret!'  # Used by Flask to encrypt session cookie.
SITE_WIDTH = 800
app = Flask(__name__)
app.config.from_object(__name__)
flask_db = FlaskDB(app)
database = flask_db.database
oembed_providers = bootstrap_basic(OEmbedCache())

Вы заметите, что пароль администратора хранится как значение конфигурации в незашифрованном виде. Это нормально для прототипирования, но если вы в конечном итоге развернете приложения вы могли бы рассмотреть, по крайней мере, использование хотя бы одностороннего хэша для хранения пароля.

Определение моделей баз данных

В нашем блоге, мы сосредоточимся на простоте. Записи будут храниться в одной таблице, и у нас будет отдельная таблицу для индекса поиска.

У модели записей будут следующие столбцы:

  • title
  • slug: для понятного url.
  • content: содержание записи.
  • published:  флаг, указывающий публикуется ли запись.
  • timestamp: раз создается запись.
  • id: PeeWee автоматически создаст первичный ключ автоинкрементный для нас, так что мы не должны определять его явно.

Индекс поиска будут храниться с использованием модели класса FTSEntry:

  • entry_id: первичный ключ индексированной записи.
  • content: поиск контента для данной записи.

Добавим следующий код после конфигурации в наш app.py:

class Entry(flask_db.Model):
    title = CharField()
    slug = CharField(unique=True)
    content = TextField()
    published = BooleanField(index=True)
    timestamp = DateTimeField(default=datetime.datetime.now, index=True)
    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = re.sub('[^\w]+', '-', self.title.lower())
        ret = super(Entry, self).save(*args, **kwargs)
        # Store search content.
        self.update_search_index()
        return ret
    def update_search_index(self):
        try:
            fts_entry = FTSEntry.get(FTSEntry.entry_id == self.id)
        except FTSEntry.DoesNotExist:
            fts_entry = FTSEntry(entry_id=self.id)
            force_insert = True
        else:
            force_insert = False
        fts_entry.content = '\n'.join((self.title, self.content))
        fts_entry.save(force_insert=force_insert)
class FTSEntry(FTSModel):
    entry_id = IntegerField(Entry)
    content = TextField()
    class Meta:
        database = database

Этот код определяет два класса моделей и их соответствующие поля. У модели записей есть два дополнительных метода, которые используются для того, чтобы, когда запись сохраняется, мы также генерировали slug из названия, и обновляли поисковой индекс.

Также обратите внимание, что мы создали несколько полей с индексом = True. Это говорит Peewee создать вторичный индекс по столбцам.

 

Код инициализации

Теперь, когда мы определили наши модели, давайте добавим код инициализации приложения. Когда мы запустим приложение в режиме отладки,  мы автоматически создадим таблицы базы данных, если они не существуют. Добавьте следующий код в конец файла app.py:

@app.template_filter('clean_querystring')
def clean_querystring(request_args, *keys_to_remove, **new_values):
    querystring = dict((key, value) for key, value in request_args.items())
    for key in keys_to_remove:
        querystring.pop(key, None)
    querystring.update(new_values)
    return urllib.urlencode(querystring)
@app.errorhandler(404)
def not_found(exc):
    return Response('
&amp;amp;amp;lt;h3&amp;amp;amp;gt;Not found&amp;amp;amp;lt;/h3&amp;amp;amp;gt;
'), 404
def main():
    database.create_tables([Entry, FTSEntry], safe=True)
    app.run(debug=True)
if __name__ == '__main__':
    main()

Если вы хотите, вы можете попробовать запустить приложение сейчас. Но вы не сможем сделать какие-либо запросы, так как нет еще views, но база данных будет создана, и вы увидите следующий вывод:

$ cd blog  # switch to the blog virtualenv directory.
$ source bin/activate  # activate the virtualenv
(blog)$ cd app  # switch to the app subdirectory
(blog)$ python app.py
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with reloader

Добавим функции входа и выхода

Для того, чтобы создать и редактировать записи, мы добавим аутентификацию на сайте. У Flask есть печеньки на основе объекта сеанса, который мы будем использовать для хранения аутентификации на сайте.

def login_required(fn):
    @functools.wraps(fn)
    def inner(*args, **kwargs):
        if session.get('logged_in'):
            return fn(*args, **kwargs)
        return redirect(url_for('login', next=request.path))
    return inner
@app.route('/login/', methods=['GET', 'POST'])
def login():
    next_url = request.args.get('next') or request.form.get('next')
    if request.method == 'POST' and request.form.get('password'):
        password = request.form.get('password')
        if password == app.config['ADMIN_PASSWORD']:
            session['logged_in'] = True
            session.permanent = True  # Use cookie to store session.
            flash('You are now logged in.', 'success')
            return redirect(next_url or url_for('index'))
        else:
            flash('Incorrect password.', 'danger')
    return render_template('login.html', next_url=next_url)
@app.route('/logout/', methods=['GET', 'POST'])
def logout():
    if request.method == 'POST':
        session.clear()
        return redirect(url_for('login'))
    return render_template('logout.html')

Обратите внимание, что вьюхи входа и выхода делают разные вещи в зависимости от того, был ли запрос GET или POST. При переходе на страницу /login/, вы увидите шаблон с полем ввода. Когда вы отправляете форму, вьюха будет проверять пароль, и перенаправить на главную или отобразить сообщение об ошибке.

Реализация views

Теперь, когда мы создали фундамент нашего сайта, мы можем начать работать над тем, что на самом деле будет использоваться для отображения и управления записей в блоге. Благодаря некоторым из помощников в модуле flask_utils Playhouse, код будет минимальным.

Главная, поиск, черновики

Давайте начнем с главной страницы. Это будет страница с записями, с сортировкой по времени, и будут отображаться последние 20 записей. Мы будем использовать object_list из flask_utils, который принимает запрос и возвращает запрошенную страницу объектов. Кроме того, индекс страницы позволит пользователям осуществлять поиск.

Добавим следующий код после кода аутентификации:

@app.route('/')
def index():
    search_query = request.args.get('q')
    if search_query:
        query = Entry.search(search_query)
    else:
        query = Entry.public().order_by(Entry.timestamp.desc())
    return object_list('index.html', query, search=search_query)

Если поисковый запрос присутствует, как указано в GET аргумента q, будем вызывать метод Entry.search(). Этот метод будет использовать SQLite полнотекстовый поисковый индекс для запроса соответствующих записей. Полнотекстовый поиск SQLite поддерживает логические запросы, цитирование, и многое другое.

Вы можете заметить, что мы также вызываем Entry.public(),  если нет поискового запроса. Этот метод вернет только опубликованные записи.

Для реализации этого, добавим следующие методы в наш класс:

@classmethod
def public(cls):
    return Entry.select().where(Entry.published == True)
@classmethod
def search(cls, query):
    words = [word.strip() for word in query.split() if word.strip()]
    if not words:
        # Return empty query.
        return Entry.select().where(Entry.id == 0)
    else:
        search = ' '.join(words)
    return (FTSEntry
            .select(
                FTSEntry,
                Entry,
                FTSEntry.rank().alias('score'))
            .join(Entry, on=(FTSEntry.entry_id == Entry.id).alias('entry'))
            .where(
                (Entry.published == True) &amp;amp;
                (FTSEntry.match(search)))
            .order_by(SQL('score').desc()))

Давайте кратко разберемся в методе search. Здесь мы запрашиваем таблицу FTSEntry, в которой хранится поисковые индексы наших записей. Полнотекстовый поиск SQLite внедряет оператор MATCH, который мы будем использовать, чтобы соответствовать индексированный контент от поискового запроса. Мы также присоединяемся к таблице записей таким образом, что мы вернем только опубликованные записи.

На главной странице отображаются только опубликованные записи, но мы должны еще дать возможность авторизованному пользователю управлять записями в черновиках. Давайте добавим защищенный views для отображения неопубликованных записей.

@classmethod
def drafts(cls):
    return Entry.select().where(Entry.published == False)
@app.route('/drafts/')
@login_required
def drafts():
    query = Entry.drafts().order_by(Entry.timestamp.desc())
    return object_list('index.html', query)

Детальная страница записей

Мы будем использовать дружественные URL. Вы, возможно, помните, что мы перегрузили метод Entry.save() для заполнения поля slug с URL. Добавим следующий код в наше приложение:

@app.route('/<slug>/')
def detail(slug):
    if session.get('logged_in'):
        query = Entry.select()
    else:
        query = Entry.public()
    entry = get_object_or_404(query, Entry.slug == slug)
    return render_template('detail.html', entry=entry)

Get_object_or_404 определен в модуле Playhouse flask_utils и, если объект, соответствующих запросу не найдено, возвращает ответ 404.

Отображение содержание записей

Для того, чтобы преобразовать тексты записей в формате HTML, мы добавим дополнительное свойство к нашему классу. Это свойство будет включить содержание записи в HTML и конвертировать медиа ссылки в встроенных объекты (т.е. URL YouTube становится видеоплеер).

Добавим следующее свойство модели:

@property
def html_content(self):
    hilite = CodeHiliteExtension(linenums=False, css_class='highlight')
    extras = ExtraExtension()
    markdown_content = markdown(self.content, extensions=[hilite, extras])
    oembed_content = parse_html(
        markdown_content,
        oembed_providers,
        urlize_all=True,
        maxwidth=app.config['SITE_WIDTH'])
    return Markup(oembed_content)

Объект Markup говорит Flask что мы доверяем содержание HTML, поэтому он не будет обрезать его при выводе.

Создание и редактирование записей

Теперь, когда мы рассмотрели views для отображения записей, черновиков и подробных страниц, нам нужно два новых вида для создания и редактирования записей. Они будут иметь много общего, но для ясности мы будем реализовывать их в виде двух отдельных функций.

@app.route('/create/', methods=['GET', 'POST'])
@login_required
def create():
    if request.method == 'POST':
        if request.form.get('title') and request.form.get('content'):
            entry = Entry.create(
                title=request.form['title'],
                content=request.form['title'],
                published=request.form.get('published') or False)
            flash('Entry created successfully.', 'success')
            if entry.published:
                return redirect(url_for('detail', slug=entry.slug))
            else:
                return redirect(url_for('edit', slug=entry.slug))
        else:
            flash('Title and Content are required.', 'danger')
    return render_template('create.html')
@app.route('/<slug>/edit/', methods=['GET', 'POST'])
@login_required
def edit(slug):
    entry = get_object_or_404(Entry, Entry.slug == slug)
    if request.method == 'POST':
        if request.form.get('title') and request.form.get('content'):
            entry.title = request.form['title']
            entry.content = request.form['content']
            entry.published = request.form.get('published') or False
            entry.save()
            flash('Entry saved successfully.', 'success')
            if entry.published:
                return redirect(url_for('detail', slug=entry.slug))
            else:
                return redirect(url_for('edit', slug=entry.slug))
        else:
            flash('Title and Content are required.', 'danger')
    return render_template('edit.html', entry=entry)

Далее осталось добавить только шаблоны у статический файлы. Их вы можете скачать с GitHux, ссылка в начале статьи.

Валидация AngularJS форм с ngMessages

angular
image-3100

Angular всегда стремились предоставить инструменты для облегчения работы с формами.
В Angular 1.3, есть новый инструмент для создания и управления сообщениями формамы — ngMessages. Этот модуль помогает нам справиться с отображением сообщений об ошибках валидации форм.

Вывод сообщения без ngMessages

Давайте взглянем на то, как формы выглядят без использования этого модуля.

<form name="userForm">
    <input
        type="text"
        name="username"
        ng-model="user.username"
        ng-minlength="3"
        ng-maxlength="8"
        required>
    <!-- show an error if username is too short -->
    <p ng-show="userForm.username.$error.minlength">Username is too short.</p>
    <!-- show an error if username is too long -->
    <p ng-show="userForm.username.$error.maxlength">Username is too long.</p>
    <!-- show an error if this isn't filled in -->
    <p ng-show="userForm.username.$error.required">Your username is required.</p>
</form>

Мы явно показывая каждое сообщение об ошибке только если ошибка существует. Это может стать утомительным, если у нас есть много видов ошибок, которые мы хотим показать.
Тут и понадобится ngMessages. Этот модуль дает некоторый порядок к сообщениям валидации.

Быстрый взгляд на ngMessages

Давайте используем предыдущий пример и посмотрим, как это будет выглядеть с ngMessages.

<form name="userForm">
    <input
        type="text"
        name="username"
        ng-model="user.username"
        ng-minlength="3"
        ng-maxlength="8"
        required>
    <div ng-messages="userForm.name.$error">
        <p ng-message="minlength">Your name is too short.</p>
        <p ng-message="maxlength">Your name is too long.</p>
        <p ng-message="required">Your name is required.</p>
    </div>
</form>

Гораздо лучше. ngMessages будет обрабатывать, показывая и скрывая определенные сообщения, основанные на ошибках.

Использование ngMessages

Установить ngMessages очень просто. Нам просто нужно загрузить модуль после AngularJS и затем добавить его в нашем приложении.

Загрузка зависимости и добавление

<!-- load angular -->
<script src="//code.angularjs.org/1.4.0/angular.js"></script>
<!-- load ngmessages -->
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular-messages.js"></script>
<!-- load our custom app -->
<script src="app.js"></script>

Теперь мы можем добавить в наше приложение в app.js.

angular.module('app', ['ngMessages']);

Отображение сообщений

Просто используйте директиву ng-messages и передайте в поле то что вы хотите увидеть при возникновении ошибки.
Формат использования:

<div ng-messages="<formName>.<inputName>.$error">
    <p ng-message="<validationName>">Your message here.</p>
</div>

Заключение

С помощью этого простого модуля, валидация форм AngularJS становится гораздо проще. Попробуйте его в ваших собственных приложениях и вы сами все поймете.

Зум слайдер

Простой слайдер контента с функцией зума в каждом слайде.

ZoomSlider
image-3065

Демо

Каждый слайд имеет предопределенный области масштабирования, который будет использоваться для расчета соответствующего значения масштаба для полноэкранной заливки. После того, как значок масштабирования нажат, область увеличения, а также страницы масштабируются, создавая иллюзию, что зритель приближается к контенту.

Мы используем CSS переходы и dymanic.js для перемещения элементов слайд. Dymanic.js Майкла Виллара является библиотекой JavaScript для создания физики на основе анимации.

Пожалуйста, обратите внимание, что мы используем пару современных свойств CSS, так что только современные браузеры поддерживают его.

HTML

<!-- Main container -->
<div class="container">
	<!-- Blueprint header -->
	<header class="bp-header cf">
		<!-- Page title etc. -->
	</header>
	<!-- Grid -->
	<section class="slider">
		<div class="slide slide--current" data-content="content-1">
			<div class="slide__mover">
				<div class="zoomer flex-center">
					<img class="zoomer__image" src="images/iphone.png" alt="iPhone" />
					<div class="preview">
						<img src="images/iphone-content-preview.png" alt="iPhone app preview" />
						<div class="zoomer__area zoomer__area--size-2"></div>
					</div>
				</div>
			</div>
			<h2 class="slide__title"><span>The Classy</span> iPhone 6</h2>
		</div>
		<div class="slide" data-content="content-2">
			<!-- ... -->
		</div>
		<!-- ... -->
		<nav class="slider__nav">
			<button class="button button--nav-prev">
				<i class="icon icon--arrow-left"></i>
				<span class="text-hidden">Previous product</span>
			</button>
			<button class="button button--zoom">
				<i class="icon icon--zoom"></i>
				<span class="text-hidden">View details</span>
			</button>
			<button class="button button--nav-next">
				<i class="icon icon--arrow-right"></i>
				<span class="text-hidden">Next product</span>
			</button>
		</nav>
	</section>
	<!-- /slider-->
	<!-- content -->
	<section class="content">
		<div class="content__item" id="content-1">
			<img class="content__item-img rounded-right" src="images/iphone-content.png" alt="Apple Watch Content" />
			<div class="content__item-inner">
				<h2>The iPhone 6</h2>
				<h3>Incredible performance for powerful apps</h3>
				<p>...</p>
			</div>
		</div>
		<div class="content__item" id="content-2">
			<!-- ... -->
		</div>
		<!-- ... -->
		<button class="button button--close">
			<i class="icon icon--circle-cross"></i>
			<span class="text-hidden">Close content</span>
		</button>
	</section>
	<!-- /content -->
</div>
<script src="js/classie.js"></script>
<script src="js/dynamics.min.js"></script>
<script src="js/main.js"></script>

CSS

/* Helper classes */
html,
body {
	overflow: hidden;
	height: 100%;
}
.container {
	position: relative;
	overflow: hidden;
	overflow-y: scroll;
	width: 100%;
	height: 100%;
	-webkit-overflow-scrolling: touch;
}
.noscroll .container {
	overflow-y: hidden;
}
.slider {
	position: relative;
	z-index: 200;
	width: 100%;
	margin: 0 auto;
	padding: 0 0 7em;
	text-align: center;
	-webkit-user-select: none;
	-moz-user-select: none;
	-ms-user-select: none;
	user-select: none;
	-webkit-touch-callout: none;
	-khtml-user-select: none;
}
.slide {
	position: absolute;
	top: 0;
	visibility: hidden;
	width: 100%;
	opacity: 0;
}
.slide--current {
	position: relative;
	z-index: 100;
	visibility: visible;
	opacity: 1;
}
.slide__mover {
	position: relative;
	z-index: 100;
}
.slide__title {
	font-size: 1.75em;
	font-weight: normal;
	margin: 0 auto;
	padding: 1em 0 0 0;
}
.slide__title span {
	font-size: 55%;
	font-weight: bold;
	display: block;
	letter-spacing: 2px;
	text-transform: uppercase;
	color: #35303d;
}
.slider__nav {
	position: absolute;
	bottom: 2em;
	width: 100%;
	text-align: center;
}
.button {
	font-size: 1.31em;
	position: relative;
	display: inline-block;
	overflow: hidden;
	margin: 0 25px;
	padding: 0;
	cursor: pointer;
	color: #5c5edc;
	border: none;
	background: none;
}
.button:focus {
	outline: none;
}
.button:hover {
	color: #fff;
}
.text-hidden {
	position: absolute;
	top: 200%;
}
.button--close {
	font-size: 1.55em;
	position: absolute;
	top: 30px;
	right: 30px;
	margin: 0;
	opacity: 0;
	color: #50505a;
	-webkit-transition: opacity 0.3s;
	transition: opacity 0.3s;
}
.content--open .button--close {
	opacity: 1;
}
/* Zoomer */
.zoomer {
	position: relative;
	height: 360px; /* this is needed for IE10 so that vertical flexbox centering works */
}
.flex-center {
	display: -webkit-flex;
	display: -ms-flexbox;
	display: flex;
	-webkit-align-items: center;
	-ms-flex-align: center;
	align-items: center;
	-webkit-justify-content: center;
	-ms-flex-pack: center;
	justify-content: center;
}
.zoomer__image {
	display: block;
	margin: 0;
	-webkit-flex: none;
	-ms-flex: none;
	flex: none;
}
.zoomer__area,
.preview {
	position: absolute;
	top: 50%;
	left: 50%;
	-webkit-transform: translate3d(-50%,-50%,0);
	transform: translate3d(-50%,-50%,0);
}
.zoomer__area:focus {
	outline: none;
}
.zoomer__area--size-1 {
	/* Apple Watch */
	width: 96px;
	height: 118px;
}
.zoomer__area--size-2 {
	/* iPhone */
	width: 112px;
	height: 198px;
}
.zoomer__area--size-3 {
	/* MacBook */
	width: 315px;
	height: 200px;
}
.zoomer__area--size-4 {
	/* iPad */
	width: 150px;
	height: 200px;
}
.zoomer__area--size-5 {
	/* iMac */
	width: 315px;
	height: 189px;
}
.preview {
	overflow: hidden;
	background: #18191b;
}
.preview img {
	display: block;
	border-radius: inherit;
	-webkit-transform: translate3d(0,0,0);
	transform: translate3d(0,0,0);
}
.zoomer--active .preview img {
	-webkit-transform: translate3d(100%,0,0);
	transform: translate3d(100%,0,0);
}
.rounded {
	border-radius: 15px;
}
.rounded-right {
	border-radius: 0 15px 15px 0;
}
.preview__content {
	position: absolute;
	top: 0;
	left: 100%;
	width: 100%;
	height: 100%;
	border-radius: inherit;
}
/* Content */
.content {
	position: fixed;
	z-index: 1000;
	top: 0;
	left: -100%;
	overflow: hidden;
	overflow-y: scroll;
	width: 100%;
	height: 100vh;
	background: #18191b;
	-webkit-overflow-scrolling: touch;
}
.content--open {
	left: 0;
}
.content__item {
	position: absolute;
	top: 0;
	display: -webkit-flex;
	display: -ms-flexbox;
	display: flex;
	overflow: hidden;
	height: 0;
	min-height: 100%;
	margin: 0 auto;
	padding: 2em 0;
	pointer-events: none;
	opacity: 0;
	color: #fff;
	-webkit-align-items: center;
	-ms-flex-align: center;
	align-items: center;
}
.content__item--current {
	pointer-events: auto;
	opacity: 1;
}
.content__item--reset {
	height: auto;
}
.content h2 {
	font-size: 3.5em;
	font-weight: normal;
	margin: 0;
}
.content h3 {
	font-size: 1.95em;
	font-weight: normal;
	margin: 0.25em 0 0.5em;
	color: #685884;
}
.content p {
	font-size: 1.25em;
	line-height: 1.5;
}
.content__item-img {
	display: block;
	max-width: 40vw;
	max-height: 80vh;
	-webkit-transform: translate3d(-120%,0,0);
	transform: translate3d(-120%,0,0);
	-webkit-flex: none;
	-ms-flex: none;
	flex: none;
}
.content__item--current .content__item-img {
	-webkit-transform: translate3d(-10px,0,0);
	transform: translate3d(-10px,0,0);
}
.content__item-inner {
	padding: 0 10vw 0;
	opacity: 0;
	-webkit-transform: translate3d(0,50px,0);
	transform: translate3d(0,50px,0);
}
.content__item--current .content__item-inner {
	opacity: 1;
	-webkit-transform: translate3d(0,0,0);
	transform: translate3d(0,0,0);
}
/**************************/
/* All synced transitions */
/**************************/
.zoomer {
	-webkit-transition: -webkit-transform 0.5s;
	transition: transform 0.5s;
	-webkit-transition-timing-function: cubic-bezier(0.7,0,0.3,1);
	transition-timing-function: cubic-bezier(0.7,0,0.3,1);
}
.zoomer.zoomer--notrans {
	-webkit-transition: none;
	transition: none;
}
.zoomer__image {
	-webkit-transition: opacity 0.3s 0.3s;
	transition: opacity 0.3s 0.3s;
}
.zoomer--active .zoomer__image {
	opacity: 0;
	-webkit-transition-delay: 0s;
	transition-delay: 0s;
}
.preview img {
	-webkit-transition: -webkit-transform 0.6s 0.3s;
	transition: transform 0.6s 0.3s;
	-webkit-transition-timing-function: cubic-bezier(0.2,1,0.3,1);
	transition-timing-function: cubic-bezier(0.2,1,0.3,1);
}
.zoomer--active .preview img {
	-webkit-transition: -webkit-transform 0.3s;
	transition: transform 0.3s;
}
.content {
	-webkit-transition: left 0s;
	transition: left 0s;
}
.content__item {
	-webkit-transition: opacity 0s;
	transition: opacity 0s;
}
.content,
.content__item {
	/* delay for content to disappear and zoomer to start transitioning back to 0 */
	-webkit-transition-delay: 0.3s;
	transition-delay: 0.3s;
}
.content--open,
.content__item--current {
	-webkit-transition: none;
	transition: none;
}
.content__item-img {
	-webkit-transition: -webkit-transform 0.4s;
	transition: transform 0.4s;
	-webkit-transition-timing-function: cubic-bezier(0.7,1,0.8,1);
	transition-timing-function: cubic-bezier(0.7,1,0.8,1);
}
.content__item--current .content__item-img {
	-webkit-transition-timing-function: cubic-bezier(0.2,1,0.3,1);
	transition-timing-function: cubic-bezier(0.2,1,0.3,1);
	-webkit-transition-duration: 1s;
	transition-duration: 1s;
}
.content__item-inner {
	-webkit-transition: -webkit-transform 0.6s, opacity 0.3s;
	transition: transform 0.6s, opacity 0.3s;
	-webkit-transition-timing-function: cubic-bezier(0.7,1,0.8,1), ease;
	transition-timing-function: cubic-bezier(0.7,1,0.8,1), ease;
}
.content__item--current .content__item-inner {
	-webkit-transition-timing-function: cubic-bezier(0.2,1,0.3,1), ease;
	transition-timing-function: cubic-bezier(0.2,1,0.3,1), ease;
	-webkit-transition-duration: 1.7s;
	transition-duration: 1.7s;
}
/* Media Queries */
@media screen and (max-width: 50em) {
	.content__item {
		display: block;
	}
	.content__item-img {
		max-width: calc(100% - 80px);
		max-height: 70vh;
	}
	.content h2 {
		font-size: 3em;
	}
	.content__item-inner {
		font-size: 82%;
		padding: 4em 3em 2em;
	}
}

JavaScript

/**
 * main.js
 * http://www.codrops.com
 *
 * Licensed under the MIT license.
 * http://www.opensource.org/licenses/mit-license.php
 *
 * Copyright 2015, Codrops
 * http://www.codrops.com
 */
;(function(window) {
	'use strict';
	var bodyEl = document.body,
		docElem = window.document.documentElement,
		support = { transitions: Modernizr.csstransitions },
		// transition end event name
		transEndEventNames = { 'WebkitTransition': 'webkitTransitionEnd', 'MozTransition': 'transitionend', 'OTransition': 'oTransitionEnd', 'msTransition': 'MSTransitionEnd', 'transition': 'transitionend' },
		transEndEventName = transEndEventNames[ Modernizr.prefixed( 'transition' ) ],
		onEndTransition = function( el, callback ) {
			var onEndCallbackFn = function( ev ) {
				if( support.transitions ) {
					if( ev.target != this ) return;
					this.removeEventListener( transEndEventName, onEndCallbackFn );
				}
				if( callback && typeof callback === 'function' ) { callback.call(this); }
			};
			if( support.transitions ) {
				el.addEventListener( transEndEventName, onEndCallbackFn );
			}
			else {
				onEndCallbackFn();
			}
		},
		// window sizes
		win = {width: window.innerWidth, height: window.innerHeight},
		// some helper vars to disallow scrolling
		lockScroll = false, xscroll, yscroll,
		scrollContainer = document.querySelector('.container'),
		// the main slider and its items
		sliderEl = document.querySelector('.slider'),
		items = [].slice.call(sliderEl.querySelectorAll('.slide')),
		// total number of items
		itemsTotal = items.length,
		// navigation controls/arrows
		navRightCtrl = sliderEl.querySelector('.button--nav-next'),
		navLeftCtrl = sliderEl.querySelector('.button--nav-prev'),
		zoomCtrl = sliderEl.querySelector('.button--zoom'),
		// the main content element
		contentEl = document.querySelector('.content'),
		// close content control
		closeContentCtrl = contentEl.querySelector('button.button--close'),
		// index of current item
		current = 0,
		// check if an item is "open"
		isOpen = false,
		isFirefox = typeof InstallTrigger !== 'undefined',
		// scale body when zooming into the items, if not Firefox (the performance in Firefox is not very good)
		bodyScale = isFirefox ? false : 3;
	// some helper functions:
	function scrollX() { return window.pageXOffset || docElem.scrollLeft; }
	function scrollY() { return window.pageYOffset || docElem.scrollTop; }
	// from http://www.sberry.me/articles/javascript-event-throttling-debouncing
	function throttle(fn, delay) {
		var allowSample = true;
		return function(e) {
			if (allowSample) {
				allowSample = false;
				setTimeout(function() { allowSample = true; }, delay);
				fn(e);
			}
		};
	}
	function init() {
		initEvents();
	}
	// event binding
	function initEvents() {
		// open items
		zoomCtrl.addEventListener('click', function() {
			openItem(items[current]);
		});
		// close content
		closeContentCtrl.addEventListener('click', closeContent);
		// navigation
		navRightCtrl.addEventListener('click', function() { navigate('right'); });
		navLeftCtrl.addEventListener('click', function() { navigate('left'); });
		// window resize
		window.addEventListener('resize', throttle(function(ev) {
			// reset window sizes
			win = {width: window.innerWidth, height: window.innerHeight};
			// reset transforms for the items (slider items)
			items.forEach(function(item, pos) {
				if( pos === current ) return;
				var el = item.querySelector('.slide__mover');
				dynamics.css(el, { translateX: el.offsetWidth });
			});
		}, 10));
		// keyboard navigation events
		document.addEventListener( 'keydown', function( ev ) {
			if( isOpen ) return;
			var keyCode = ev.keyCode || ev.which;
			switch (keyCode) {
				case 37:
					navigate('left');
					break;
				case 39:
					navigate('right');
					break;
			}
		} );
	}
	// opens one item
	function openItem(item) {
		if( isOpen ) return;
		isOpen = true;
		// the element that will be transformed
		var zoomer = item.querySelector('.zoomer');
		// slide screen preview
		classie.add(zoomer, 'zoomer--active');
		// disallow scroll
		scrollContainer.addEventListener('scroll', noscroll);
		// apply transforms
		applyTransforms(zoomer);
		// also scale the body so it looks the camera moves to the item.
		if( bodyScale ) {
			dynamics.animate(bodyEl, { scale: bodyScale }, { type: dynamics.easeInOut, duration: 500 });
		}
		// after the transition is finished:
		onEndTransition(zoomer, function() {
			// reset body transform
			if( bodyScale ) {
				dynamics.stop(bodyEl);
				dynamics.css(bodyEl, { scale: 1 });
				// fix for safari (allowing fixed children to keep position)
				bodyEl.style.WebkitTransform = 'none';
				bodyEl.style.transform = 'none';
			}
			// no scrolling
			classie.add(bodyEl, 'noscroll');
			classie.add(contentEl, 'content--open');
			var contentItem = document.getElementById(item.getAttribute('data-content'))
			classie.add(contentItem, 'content__item--current');
			classie.add(contentItem, 'content__item--reset');
			// reset zoomer transform - back to its original position/transform without a transition
			classie.add(zoomer, 'zoomer--notrans');
			zoomer.style.WebkitTransform = 'translate3d(0,0,0) scale3d(1,1,1)';
			zoomer.style.transform = 'translate3d(0,0,0) scale3d(1,1,1)';
		});
	}
	// closes the item/content
	function closeContent() {
		var contentItem = contentEl.querySelector('.content__item--current'),
			zoomer = items[current].querySelector('.zoomer');
		classie.remove(contentEl, 'content--open');
		classie.remove(contentItem, 'content__item--current');
		classie.remove(bodyEl, 'noscroll');
		if( bodyScale ) {
			// reset fix for safari (allowing fixed children to keep position)
			bodyEl.style.WebkitTransform = '';
			bodyEl.style.transform = '';
		}
		/* fix for safari flickering */
		var nobodyscale = true;
		applyTransforms(zoomer, nobodyscale);
		/* fix for safari flickering */
		// wait for the inner content to finish the transition
		onEndTransition(contentItem, function(ev) {
			classie.remove(this, 'content__item--reset');
			// reset scrolling permission
			lockScroll = false;
			scrollContainer.removeEventListener('scroll', noscroll);
			/* fix for safari flickering */
			zoomer.style.WebkitTransform = 'translate3d(0,0,0) scale3d(1,1,1)';
			zoomer.style.transform = 'translate3d(0,0,0) scale3d(1,1,1)';
			/* fix for safari flickering */
			// scale up - behind the scenes - the item again (without transition)
			applyTransforms(zoomer);
			// animate/scale down the item
			setTimeout(function() {
				classie.remove(zoomer, 'zoomer--notrans');
				classie.remove(zoomer, 'zoomer--active');
				zoomer.style.WebkitTransform = 'translate3d(0,0,0) scale3d(1,1,1)';
				zoomer.style.transform = 'translate3d(0,0,0) scale3d(1,1,1)';
			}, 25);
			if( bodyScale ) {
				dynamics.css(bodyEl, { scale: bodyScale });
				dynamics.animate(bodyEl, { scale: 1 }, {
					type: dynamics.easeInOut,
					duration: 500
				});
			}
			isOpen = false;
		});
	}
	// applies the necessary transform value to scale the item up
	function applyTransforms(el, nobodyscale) {
		// zoomer area and scale value
		var zoomerArea = el.querySelector('.zoomer__area'),
			zoomerAreaSize = {width: zoomerArea.offsetWidth, height: zoomerArea.offsetHeight},
			zoomerOffset = zoomerArea.getBoundingClientRect(),
			scaleVal = zoomerAreaSize.width/zoomerAreaSize.height < win.width/win.height ? win.width/zoomerAreaSize.width : win.height/zoomerAreaSize.height;
		if( bodyScale && !nobodyscale ) {
			scaleVal /= bodyScale;
		}
		// apply transform
		el.style.WebkitTransform = 'translate3d(' + Number(win.width/2 - (zoomerOffset.left+zoomerAreaSize.width/2)) + 'px,' + Number(win.height/2 - (zoomerOffset.top+zoomerAreaSize.height/2)) + 'px,0) scale3d(' + scaleVal + ',' + scaleVal + ',1)';
		el.style.transform = 'translate3d(' + Number(win.width/2 - (zoomerOffset.left+zoomerAreaSize.width/2)) + 'px,' + Number(win.height/2 - (zoomerOffset.top+zoomerAreaSize.height/2)) + 'px,0) scale3d(' + scaleVal + ',' + scaleVal + ',1)';
	}
	// navigate the slider
	function navigate(dir) {
		var itemCurrent = items[current],
			currentEl = itemCurrent.querySelector('.slide__mover'),
			currentTitleEl = itemCurrent.querySelector('.slide__title');
		// update new current value
		if( dir === 'right' ) {
			current = current < itemsTotal-1 ? current + 1 : 0;
		}
		else {
			current = current > 0 ? current - 1 : itemsTotal-1;
		}
		var itemNext = items[current],
			nextEl = itemNext.querySelector('.slide__mover'),
			nextTitleEl = itemNext.querySelector('.slide__title');
		// animate the current element out
		dynamics.animate(currentEl, { opacity: 0, translateX: dir === 'right' ? -1*currentEl.offsetWidth/2 : currentEl.offsetWidth/2, rotateZ: dir === 'right' ? -10 : 10 }, {
			type: dynamics.spring,
			duration: 2000,
			friction: 600,
			complete: function() {
				dynamics.css(itemCurrent, { opacity: 0, visibility: 'hidden' });
			}
		});
		// animate the current title out
		dynamics.animate(currentTitleEl, { translateX: dir === 'right' ? -250 : 250, opacity: 0 }, {
			type: dynamics.bezier,
			points: [{"x":0,"y":0,"cp":[{"x":0.2,"y":1}]},{"x":1,"y":1,"cp":[{"x":0.3,"y":1}]}],
			duration: 450
		});
		// set the right properties for the next element to come in
		dynamics.css(itemNext, { opacity: 1, visibility: 'visible' });
		dynamics.css(nextEl, { opacity: 0, translateX: dir === 'right' ? nextEl.offsetWidth/2 : -1*nextEl.offsetWidth/2, rotateZ: dir === 'right' ? 10 : -10 });
		// animate the next element in
		dynamics.animate(nextEl, { opacity: 1, translateX: 0 }, {
			type: dynamics.spring,
			duration: 2000,
			friction: 600,
			complete: function() {
				items.forEach(function(item) { classie.remove(item, 'slide--current'); });
				classie.add(itemNext, 'slide--current');
			}
		});
		// set the right properties for the next title to come in
		dynamics.css(nextTitleEl, { translateX: dir === 'right' ? 250 : -250, opacity: 0 });
		// animate the next title in
		dynamics.animate(nextTitleEl, { translateX: 0, opacity: 1 }, {
			type: dynamics.bezier,
			points: [{"x":0,"y":0,"cp":[{"x":0.2,"y":1}]},{"x":1,"y":1,"cp":[{"x":0.3,"y":1}]}],
			duration: 650
		});
	}
	// disallow scrolling (on the scrollContainer)
	function noscroll() {
		if(!lockScroll) {
			lockScroll = true;
			xscroll = scrollContainer.scrollLeft;
			yscroll = scrollContainer.scrollTop;
		}
		scrollContainer.scrollTop = yscroll;
		scrollContainer.scrollLeft = xscroll;
	}
	init();
})(window);

Демо

Готовимся к php 7

Готовимся к php 7
image-3045

2015 год стал важный годом для PHP. Спустя одиннадцать лет после выпуска PHP 5.0, новая версия, наконец, готовится к выходу! PHP 7 выйдет до конца этого года, в результате чего мы получим много новых возможностей языка и впечатляющий прирост производительности.
Но как это повлияет на текущий ваш PHP код? Что действительно изменилось? Насколько это безопасно для обновления? Эта статья ответит на эти вопросы и покажет пару новинок которые придут с PHP 7.

Улучшения производительности

Производительность, несомненно, самая главная причина, почему вы должны обновить свои серверы, как только выйдет стабильная версия PHP. Рефакторинг ядра представленный в phpng RFC делает PHP 7, таким же быстрым (или даже быстрее чем) HHVM. Официальные тесты впечатляют, большинство реальных приложений, работающих на PHP 5.6 будет работать, по крайней мере в два раза быстрее на PHP 7.
Для получения подробной информации о тестах производительности, посмотрите на презентации Rasmus Лердорф. Вот показатели WordPress из этого доклада:

image-3046

PHP 7 обрабатывает в два раза больше запросов в секунду, что в практическом плане будет представлять улучшение производительности WordPress сайтов на 100%.

Обратная совместимость

Давайте поговорим о некоторых вещах, которые потенциально могут нарушить работоспособность приложения, работающие на старых версиях PHP.

Устаревшие элементы удалены

Удаленны многие устаревших элементы . Они были устаревшими в течение долгого времени, мы надеемся, вы не используете их! Однако, это может влиять на унаследованные приложения.

В частности, теги ASP-стиля (<%, <% = и %>), были удалены вместе с тегом сценариев (<script language=”php”>). Убедитесь, что вы используете рекомендуемый тег <?php. Другие функции, которые ранее были устаревшие, также были удалены в PHP 7.

Расширение EREG (и все ereg_* функции) были устаревшими, начиная с PHP 5.3. Они должны быть заменено с расширением PCRE (preg_* функции), который предлагает гораздо больше возможностей. Расширение MySQL (и mysql_* функции) были устаревшими, начиная с PHP 5.5. Для прямого перехода, вы можете использовать MySQLi расширение и mysqli_* функции вместо них.

Единый синтаксис переменных

Единый синтаксис переменных призвана решить ряд несоответствий при оценке переменная-переменная выражений. Рассмотрим следующий код:

<?php
class Person
{
   public $name = 'Erika';
   public $job = 'Developer Advocate';
}
$person = new Person();
$property = [ 'first' => 'name', 'second' => 'info' ];
echo "\nMy name is " . $person->$property['first'] . "\n\n";

В PHP 5, выражение $person->$property['first'] оценивается как $person->{$property['first']}. В практическом плане это будет интерпретироваться как $person->name, давая вам результат «My name is Erika». Даже если это крайний случай, это показывает четкие несоответствия с нормальным порядком вычисления выражения, слева направо.

В PHP 7, выражение $person->$property['first'] оценивается как {$person->$property}['first']. Интерпретатор оценить $person->$property в первую очередь; следовательно, предыдущий пример кода не будет работать в PHP7, потому что $property массива и не может быть преобразован в строку.

Быстрый и простой способ, чтобы исправить эту проблему, явно определяющий порядок оценки с помощью фигурных скобок (например, $person->{$property['first']}), который будет гарантировать такое же поведение как на PHP 5 и PHP 7.
Благодаря новой форме слева на права переменной синтаксиса, многие выражения ранее обработанные некорректно теперь работают без проблем. Чтобы проиллюстрировать это новое поведение, рассмотрим следующий класс:

<?php
class Person
{
   public static $company = 'DigitalOcean';
   public function getFriends()
   {
       return [
           'erika' => function () {
               return 'Elephpants and Cats';
           },
           'sammy' => function () {
               return 'Sharks and Penguins';
           }
       ];
   }
   public function getFriendsOf($someone)
   {
       return $this->getFriends()[$someone];
   }
   public static function getNewPerson()
   {
       return new Person();
   }
}

С PHP 7, мы можем создать вложенные ассоциации и различные комбинации между операторами:

$person = new Person();
echo "\n" . $person->getFriends()['erika']() . "\n\n";
echo "\n" . $person->getFriendsOf('sammy')() . "\n\n";

Этот фрагмент даст нам сообщение об ошибке в PHP 5, но работает, как ожидалось в PHP 7.

Аналогично, вложенными статического доступа также возможно:

echo "\n" . $person::getNewPerson()::$company . "\n\n";

В PHP 5, это даст нам классическую ошибку синтаксиса T_PAAMAYIM_NEKUDOTAYIM.

Фатальная ошибка с множественным «default» классами

Это, опять же, крайний случай, и это больше связано с логической ошибкой в коде. Там нет смысла для нескольких условий по умолчанию в коммутаторе, а потому, что он никогда не вызвало никаких проблем (например, без предупреждений), это может быть трудно обнаружить ошибку. В PHP 5, будет использоваться последний умолчанию, но в PHP 7 теперь будет получить Фатальная ошибка: переключатель заявления могут содержать только один пункт по умолчанию.

Исключения

Исключение предназначены для облегчения обработки ошибок в вашем приложении. Существующие фатальные и извлекаемые фатальные ошибки были заменены исключениями, что делает возможным для нас, отловить ошибки и принять меры – для их отображения в красивом виде, их регистрации, или выполнения процедуры восстановления.

Реализация исключений было сделано таким образом, чтобы сохранить обратную совместимость, но есть случаи, которые могли бы повлиять на унаследованные приложения, когда они имеют пользовательские функции обработки ошибок. Рассмотрим следующий код:

<?php
set_error_handler(function ($code, $message) {
   echo "ERROR $code: " . $message . "\n\n";
});
function a(ArrayObject $b){
   return $b;
}
a("test");
echo "Hello World";

Этот код генерирует возмещаемую ошибку, вызванную несоответствием типа при вызове функции a(), используя строку в качестве параметра. В PHP 5, она генерирует E_RECOVERABLE и вы получите:

ERROR 4096: Argument 1 passed to a() must be an instance of ArrayObject, string given, called in /data/Projects/php7dev/tests/test04.php on line 12 and defined(...)
Hello World

Обратите внимание, что выполнение продолжается, потому что ошибка была обработана. В PHP 7, этот код генерирует исключение TypeError (не ошибка!), Так что обработчик ошибок не будет вызвана. Это вывод, который вы получите:

Fatal error: Uncaught TypeError: Argument 1 passed to a() must be an instance of ArrayObject, string given, called in /vagrant/tests/test04.php on line 12 and defined in /vagrant/tests/test04.php:7
Stack trace:
#0 /vagrant/tests/test04.php(12): a('test')
#1 {main}
  thrown in /vagrant/tests/test04.php on line 7

Выполнение останавливается, потому что исключение не поймали. Чтобы решить эту проблему, вы должны ловить исключения, используя Try / Catch блоки. Важно заметить, что иерархии исключений пришлось изменить, чтобы приспособить новые исключения с минимальным воздействием на унаследованного кода:

  • Throwable интерфейс
    • Исключение реализует Throwable
      • ErrorException распространяется исключение
      • RuntimeException распространяется исключение
    • Ошибка реализует Throwable
      • Ошибка типа распространяется Ошибка
      • ParseError распространяется Ошибка
      • AssertionError распространяется Ошибка

В основном это означает, что новый всеохватывающий Исключение теперь Throwable вместо Exception. Это не должно повлиять на любой унаследованный код, но держать его в уме при обращении новых исключений PHP 7.

Новые возможности языка

Давайте поговорим о наиболее интересных новых функций, которые будут доступны при обновлении до PHP 7.

Новые операторы

PHP 7 поставляется с двумя блестящими новыми операторами: spaceship (или сочетание операторов сравнения) и нулевого сливания оператор.
Оператор spaceship (<=>), также известный как комбинированного оператора сравнения, может быть использованы, чтобы сделать ваши блоки сравнение более кратким. Рассмотрим следующее выражение:

$a <=> $b

Это выражение будет -1, если $a меньше, чем $b, 0, если $a равен $b, и 1, если $a больше, чем $b. Раньше это было бы:

($a < $b) ? -1 : (($a > $b) ? 1 : 0)

Оператор null coalesce(??) также работает в качестве ярлыка для общего пользования в случае: условного авторства, который проверяет, если значение установлено, прежде чем использовать его. В PHP 5, вы, как правило, что-то вроде этого:

$a = isset($b) ? $b : "default";

С новым оператором в PHP 7, мы можем просто использовать:

$a = $b ?? "default";

Скалярные type-hint

Одна из самых обсуждаемых новых возможностях в PHP 7: скалярные type-hint будут доступны как типы для функций и методов. По умолчанию, скалярные типа не являются ограничительными, что означает, что если вы передаете значение с плавающей точкой в целое параметра, это будет просто заставить его Int без создания каких-либо ошибок или предупреждений.
Это возможно, однако, в строгом режиме, который будет выдавать ошибки при неверном типе переданном в качестве аргумента. Рассмотрим следующий код:

<?php
function double(int $value)
{
   return 2 * $value;
}
$a = double("5");
var_dump($a);

Этот код не будет генерировать какие-либо ошибки, потому что мы не используем строгий режим. Единственное, что будет происходить в преобразование типа, так что строка «5» передается в качестве аргумента будет принудительно преобразовано к целому числу внутри функции.
Если мы хотим, что бы только целые числа могли быть переданы в функцию, мы можем включить строгий режим DECLARE (strict_types = 1) в качестве самой первой строке в нашем сценарии:

<?php
declare(strict_types = 1);
function double(int $value)
{
   return 2 * $value;
}
$a = double("5");
var_dump($a);

Этот код будет генерировать фатальную ошибку: TypeError: Uncaught Argument 1 passed to double() must be of the type integer, string given.

Возвращение type-hint

Еще одна важная новая функция идет с PHP 7 это возможность определить тип возвращаемого значения метода и функций:

<?php
function a() : bool
{
   return 1;
}
var_dump(a());

Этот фрагмент будет работать без предупреждения и возвращенное значение будет автоматически переводятся в Bool. Если вы включите строгий режим, вы получите фатальную ошибку, вместо этого:

Fatal error: Uncaught TypeError: Return value of a() must be of the type boolean, integer returned

Еще раз обратите внимание, что эти ошибки являются на самом деле Исключения, которые могут быть перехвачены и обработаны с помощью Try / Catch блоками. Важно также подчеркнуть, что вы можете использовать любой допустимый тип подсказки, а не только скалярные типы.

Что дальше?

PHP 7 планирует выпустить стабильную версию в конце ноября. Бета-версия уже доступна для тестов. Проверьте RFC все изменения идут с PHP 7 для получения дополнительной информации.

Обратите внимание, что, хотя никаких новых функций не будет включен, некоторые изменения могут произойти еще до финального релиза, так что, пожалуйста, не используйте PHP 7 в продакшене! Настоятельно рекомендуется тестировать приложения и сообщать о любых проблем.

Счастливые кодирования!

AngularJS маршрутизацию с помощью UI-Router

angular
image-985

AngularJS обеспечивает отличный способ сделать приложения на одной странице. При создании приложений на одной странице, маршрутизация будет очень важной.

Сегодня посмотрим на маршрутизацию с помощью UI-Router.

Обзор

Что такое AngularUI маршрутизатор?

UI-Router маршрутизация фреймворка для AngularJS созданная AngularUI командой. Она обеспечивает иной подход, чем ngRoute, и меняет, основанное на состояние приложения, а не только URL маршрута.

Состояния vs URL пути

При таком подходе, ваши виды и маршруты не привязаны к URL сайта. Таким образом, вы можете изменить детали вашего сайта с помощью маршрутизации, даже если URL не меняется.

При использовании ngRoute, вам придется использовать ngInclude или другие методы, и это может привести к путанице. Теперь, когда все ваши состояния, маршрутизации и виды обрабатываются в одном .config().

Простое приложение

Давайте создадим простое приложение. У нас будет две страницы Главная и О нас.

Установка

Нам понадобится несколько файлов:

- index.html                    // главный шаблон нашего приложения
- app.js                        // код angular приложения
- partial-about.html            // страница о нас
- partial-home.html             // главная страница
- partial-home-list.html        // вывод на главной странице
- table-data.html               // таблица

С нашей структура приложения выяснили, давайте перейдем к самим файлам.

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
    <!-- CSS (загрузка bootstrap) -->
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
    <style>
        .navbar { border-radius:0; }
    </style>
    <!-- JS (загрузка angular, ui-router) -->
    <script src="http://code.angularjs.org/1.2.13/angular.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.8/angular-ui-router.min.js"></script>
    <script src="app.js"></script>
</head>
<!-- app нашего приложения -->
<body ng-app="routerApp">
<!-- навигация -->
<nav class="navbar navbar-inverse" role="navigation">
    <div class="navbar-header">
        <a class="navbar-brand" ui-sref="#">AngularUI Router</a>
    </div>
    <ul class="nav navbar-nav">
        <li><a ui-sref="home">Главная</a></li>
        <li><a ui-sref="about">О нас</a></li>
    </ul>
</nav>
<!-- основной контент -->
<div class="container">
    <div ui-view></div>
</div>
</body>
</html>

Мы будем использовать Bootstrap, для стилизации наших страниц. Обратите внимание, что мы также загрузили ui-router. UI Router находится отделена от основного Angular ядра.

При создании связь с UI-Router, вы будете использовать ui-sref. Ссылки будет генерироваться из состояние вашего приложения и указывать на них. Они создаются в app.js.

Мы также используем

вместо

как в ngRoute.

Перейдем к нашему app.js.

// app.js
var routerApp = angular.module('routerApp', ['ui.router']);
routerApp.config(function($stateProvider, $urlRouterProvider) {
    $urlRouterProvider.otherwise('/home');
    $stateProvider
        // главная, состояние и вид ========================================
        .state('home', {
            url: '/home',
            templateUrl: 'partial-home.html'
        })
        // о нас =================================
        .state('about', {
            // we'll get to this in a bit
        });
});

Теперь мы создали routerApp, что мы уже обратились к нашему body в index.html.

Здесь у нас .state() для страниц главная и о нас. На главной, мы используем файл шаблона partial-home.html.

Давайте заполним нашу страницу partial-home.html что бы мы смогли ее увидеть в приложении.

<!-- partial-home.html -->
<div class="jumbotron text-center">
    <h1>The Homey Page</h1>
    <p>This page demonstrates <span class="text-danger">nested</span> views.</p>
</div>

Теперь у нас есть наше приложение. Это не так уж много, но у нас это есть.

angular-ui-router-home-first[1]
image-986

Вложенные виды

Давайте посмотрим, как мы можем реализовать вложенные виды. Мы добавим две кнопки для нашей домашней странице и оттуда, мы хотим, показать различную информацию.

Мы собираемся добавить наши кнопки в partial-home.html.

<!-- partial-home.html -->
<div class="jumbotron text-center">
    <h1>Главная страница</h1>
    <p>Эта страница показывает <span class="text-danger">вложенные</span> виды.</p>
    <a ui-sref=".list" class="btn btn-primary">Список</a>
    <a ui-sref=".paragraph" class="btn btn-danger">Абзац</a>
</div>
<div ui-view></div>

ui-sref=».list» и ui-sref=».paragraph» будут определены в нашей app.js и как только мы добавим их, мы сможем выводить информацию в

.

// app.js
...
$stateProvider
    // HOME STATES AND NESTED VIEWS ========================================
    .state('home', {
        url: '/home',
        templateUrl: 'partial-home.html'
    })
    // nested list with custom controller
    .state('home.list', {
        url: '/list',
        templateUrl: 'partial-home-list.html',
        controller: function($scope) {
            $scope.dogs = ['Bernese', 'Husky', 'Goldendoodle'];
        }
    })
    // nested list with just some random string data
    .state('home.paragraph', {
        url: '/paragraph',
        template: 'I could sure use a drink right now.'
    })
...

Теперь ui-sref мы определили в home.html, связаны с фактическим состоянием. С home.list и home.paragraph создали, эти ссылки будут теперь принимать предоставленный шаблон и ввести его в ui-view.

Последнее, что нам нужно сделать для домашней странице определить файл partial-home-list.html.

<!-- partial-home-list.html -->
<ul>
    <li ng-repeat="dog in dogs">{{ dog }}</li>
</ul>

Теперь мы получаем следующее приложение со вложенными страницами.

angular-routing-ui-router-home-list[1]
image-987

Вывод

Это небольшой обзор отличный инструмент UI-Router. То, что вы можете делать с ним невероятные и когда вы смотрите на ваше приложения как на состояния, Angular приложения могут быть легко созданы, чтобы быть модульными и расширяемыми.

Легкая автоматизация задач с Gulp.js

Gulp является системой потоковой сборки, используя файловые потоки для манипуляций, Все это делается в памяти, и файл не создается, пока вы не скажете сделать это.

gulp-808x500[1]
image-978

Gulp предпочитает код по конфигурации. Будучи, что ваши задачи записываются в коде, Gulp дает вам инструменты для создания задач, которые соответствуют вашим потребностям.

Установка

Gulp легко установить и начать работать.

  1. Установка Gulp глобально
  2. Установка Gulp в devDependencies
  3. Создание gulpfile.js

Первый шаг, чтобы установить Gulp глобально.

$ npm install --global gulp

После этого, вам нужно добавить Gulp в devDependencies на любом из ваших проектов где вы хотите использовать его. Убедитесь, что у вас есть package.json. Если у вас есть package.json, давайте установим Gulp в devDependencies

$ npm install --save-dev gulp

И наконец, вы должны создать gulpfile.js в корне проекта, который содержит ваши задачи. В качестве промежуточного шага мы добавим Gulp Utilites модуль, у нас есть исполняемые задачи, которые явно показывает, что она выполнена.

$ npm install --save-dev gulp-util

В файле gulpfile.js, который вы только что создали, мы сделаем простой gulpfile, который только регистрирует, что Gulp работает.

/* File: gulpfile.js */
// grab our gulp packages
var gulp  = require('gulp'),
    gutil = require('gulp-util');
// create a default task and just log a message
gulp.task('default', function() {
  return gutil.log('Gulp is running!')
});

И если все пройдет, как ожидается, работающей Gulp в командной строке должно вам выдать следующее:

> gulp
[12:32:08] Using gulpfile ~/Projects/gulp-scotch-io/gulpfile.js
[12:32:08] Starting 'default'...
[12:32:08] Gulp is running!
[12:32:08] Finished 'default' after 1 ms

Обзор

Структура каталога для этого урока

Мы определили следующую структуру нашего проекта. Вы можете создать пустые файлы на данный момент.

public/
  |  assets/
  |  |  stylesheets/
  |  |  |  style.css
  |  |  javascript/
  |  |  |  vendor/
  |  |  |  |  jquery.min.js
  |  |  |  bundle.js
source/
  |  javascript/
  |  |  courage.js
  |  |  wisdom.js
  |  |  power.js
  |  scss/
  |  |  styles.scss
  |  |  grid.scss
gulpfile.js
packages.json

В source мы будем делать нашу работу. assets/style.css создает Gulp, когда мы обрабатываем и объединить наши Sass файлы в source/scss. Файл bundle.js создает так же Gulp, когда мы минимизируем и объединяем все наши JS файлы.

Краткий обзор Gulp

Gulp является системой потоковой сборки, расширить его функционал позволяют плагины. Плагины Gulp предназначены для выполнения только одной задачи каждый, так что это не редкость, когда файл проходит через несколько плагинов.

Gulp API является очень простым, содержащий 4 функции верхнего уровня.

  • gulp.task
  • gulp.src
  • gulp.dest
  • gulp.watch

gulp.task определяет ваши задачи. Его аргументы name, deps and fn.

Где name строка, deps является массив имен задача, и fn является функция, которая выполняет вашу задачу. deps не является обязательным, по этому gulp.task имеет две формы:

gulp.task('mytask', function() {
  //do stuff
});
gulp.task('dependenttask', ['mytask'], function() {
  //do stuff after 'mytask' is done.
});

gulp.src указывает на файлы, которые мы хотим использовать. Это глобальные параметры и является необязательной опцией. Он использует .pipe для построения цепочки из его вывода в других плагинов.

gulp.dest указывает на папку вывода куда мы хотим записать файлы.

gulp.src и gulp.dest простой пример для копирования файлов выглядит следующим образом:

gulp.task('copyHtml', function() {
  // copy any html files in source/ to public/
  gulp.src('source/*.html').pipe(gulp.dest('public'));
});

gulp.watch как gulp.task имеет две основные формы. Оба возвращает EventEmitter, который выделяют change событий. Первый из которых занимает глобально, необязательный параметр, и массив задач его параметров.

gulp.watch('source/javascript/**/*.js', ['jshint']);

Проще говоря, когда любой из файлов, глобально изменяется, выполняется задача. В приведенном выше примере, когда какие-либо файлы в source/javascript с расширением .js изменятся, то jshint задача будет выполнена в отношении тех файлов.

Вторая форма принимает универсальный символ, дополнительный объект вариантов, и дополнительный обратный вызов, который будет выполняться, если были изменения.

Для получения дополнительной информации обратитесь к API.

Некоторые полезные задачи.

Мы начнем с простых задач.

Jshint  на сохранение

Наша первая задача будет с нашим JavaScript (проверить на наличие ошибок), используя jshint и мы также настроить его для выполнения задания каждый раз, когда мы сохраним файл.

Для начала нам нужен пакет gulp-jshint, установим его с помощью npm. Мы также должны настроить jshint вывод в удобном формате и цветом оформлении.

$ npm install --save-dev gulp-jshint jshint-stylish

Теперь мы добавим задачу в наш gulpfile.

/* File: gulpfile.js */
// grab our packages
var gulp   = require('gulp'),
    jshint = require('gulp-jshint');
// define the default task and add the watch task to it
gulp.task('default', ['watch']);
// configure the jshint task
gulp.task('jshint', function() {
  return gulp.src('source/javascript/**/*.js')
    .pipe(jshint())
    .pipe(jshint.reporter('jshint-stylish'));
});
// configure which files to watch and what tasks to use on file changes
gulp.task('watch', function() {
  gulp.watch('source/javascript/**/*.js', ['jshint']);
});

Мы переписали нашу задачу по умолчанию, чтобы следить за зависимостями задачи. Это означает, что для запуска

$ gulp

будем следить за выполнением задачи.

Теперь давайте посмотрим на jshint задачи. Его источники любые .js файлы, которые существуют в source/javascript или в подкаталогах. Так файл source/javascript/carousel/main.js будет определена для задачи так же.

Эти файлы затем передаются в наш плагин gulp-jshint, который затем передает его в стилизованный репортер, чтобы выдать нам результаты jshint.

Мы можем выполнить эту задачу, выполнив:

$ gulp jshint

Хорошо, теперь о задаче watch. Это просто на самом деле, если обнаруживается изменение в любом из наших файлов JavaScript, то запускается задача jshint.

Sass компиляция с libsass

Sass расширяет CSS и дает поддержку переменных, вложенных правил, mixins, встроенных импорта, и многое другое.

Для Sass компиляции мы будем использовать gulp-sass

/* file: gulpfile.js */
var gulp   = require('gulp'),
    jshint = require('gulp-jshint'),
    sass   = require('gulp-sass');
/* jshint task would be here */
gulp.task('build-css', function() {
  return gulp.src('source/scss/**/*.scss')
    .pipe(sass())
    .pipe(gulp.dest('public/assets/stylesheets'));
});
/* updated watch task to include sass */
gulp.task('watch', function() {
  gulp.watch('source/javascript/**/*.js', ['jshint']);
  gulp.watch('source/scss/**/*.scss', ['build-css']);
});

Мы можем также добавить sourcemaps, используя gulp-sourcemaps.

/* file: gulpfile.js */
var gulp       = require('gulp'),
    jshint     = require('gulp-jshint'),
    sass       = require('gulp-sass'),
    sourcemaps = require('gulp-sourcemaps');
gulp.task('build-css', function() {
  return gulp.src('source/scss/**/*.scss')
    .pipe(sourcemaps.init())  // Process the original sources
      .pipe(sass())
    .pipe(sourcemaps.write()) // Add the map to modified source.
    .pipe(gulp.dest('public/assets/stylesheets'));
});

Javascript минификация

При работе с большим количеством JavaScript, вам как правило, необходимо собрать файлы вместе. Общего назначения плагин gulp-concat позволяет выполнить это легко.

Мы также можем пойти дальше и запустить его через минификацию и получить файл гораздо меньшего размера.

gulp.task('build-js', function() {
  return gulp.src('source/javascript/**/*.js')
    .pipe(sourcemaps.init())
      .pipe(concat('bundle.js'))
      //only uglify if gulp is ran with '--type production'
      .pipe(gutil.env.type === 'production' ? uglify() : gutil.noop())
    .pipe(sourcemaps.write())
    .pipe(gulp.dest('public/assets/javascript'));
});

Вывод

Мы рассмотрели только самый минимум Gulp. С ним легко соединить файлы JavaScript. Gulp дает вам инструменты, чтобы сделать то что вы хотите быстро и легко.

Vim подборка материала для изучения

vim-logo[1]
image-967

Vim является чрезвычайно мощным текстовым редактором, который позволяет делать почти все, с помощью сочетаний клавиш. Вы можете заменить текст в документе, переместить или удалить строки, автоматизировать изменения и больше никогда не пользоваться мышкой. Vim является любимым редактором программистов, но нет никакой причины, почему вы не можете использовать редактор для ваших регулярных задач редактирования текста записывая идеи или сочинению длинных писем.

Если вы всегда использовали графический текстовый редактор такие как Блокнот или TextEdit, вам покажется Vim запутанным, но потратив некоторое время на изучение вы уже не захотите возвращаться к своим старым редакторам.

Здесь представлен список электронных учебников и полезных ресурсов, которые помогут вам изучить Vim.

  1. OpenVim — Интерактивный учебник для изучения основ Vim. Перейдите на страницу практике что бы проверить свои навыки в Vim.
  2. Vim Adventures  — Онлайн игра-головоломка для обучения и запоминания команды Vim. Вы мигающий курсор, ориентируйтесь в лабиринте с помощью клавиатуры.
  3. Vim Genius — Эта игра, которая научит вас основам Vim.
  4. Learn to Love Vim — Видеурок от Linux Voice,  более наглядно показаны основы Vim.
  5. Vim Basics — Дерек Уайатт выпустил много видеоуроков о Vim. Для тех кто предпочитает наглядное обучение.
  6. Learning Vim — Виедо с Майк Коутермаш — вставая и работает.
  7. Vim – Precision Editing — Дрю Нил из Vimcasts.org проведет вас через Vim и как текстовый редактор, оптимизированный для Mouseless операций.
  8. Practical Vim — Единственная книга, которая когда-нибудь вам понадобится для освоения Vim.
  9.  Vim Tutorial — Официальная документация Vim включает в себя учебник, который вы также можете получить доступ в программе Vim через команду :vimtutor.
  10. Vim Cheat Sheet — Распечатайте это, потому что вы будете нуждаться в этом позже.
  11. A Byte of Vim — Бесплатно PDF книга, которая вам поможет в изучении редактора Vim.
  12. Vim 101 — Коллекция небольших статей, которые охватывают различные аспекты редактирования с Vim.

Если вы проводите много времени при вводе текста, изучение Vim будет полностью стоить усилий потраченного на него.