Статьи

Создание CMS: goPress

Go — это сетевой язык программирования, разработанный Google, который облегчает написание сетевых программ. Имея множество великолепных библиотек, из которых можно выбрать, запустить веб-приложение совсем несложно.

В этом руководстве я собираюсь создать систему управления контентом (CMS) с использованием Go и некоторых вспомогательных библиотек. В этой CMS будет использоваться структура данных сайта, изложенная в первом руководстве « Создание CMS: структура и стилизация» .

Самый простой способ установить язык программирования go на Mac — это Homebrew . Если вы еще не установили Homebrew, учебник Homebrew Demystified: Ultimate Менеджер пакетов OS X покажет вам, как это сделать. Для других платформ просто следуйте инструкциям на странице загрузки Go .

В терминале введите:

1
brew install go

В вашем домашнем каталоге создайте каталог go . Язык Go будет хранить все загруженные библиотеки там. Добавьте в ваш файл .bashrc и / или .zshrc следующую строку:

1
export GOPATH=»/Users/<your user name>/go»

Если вы используете fish , добавьте это в файл config.fish :

1
set -xg GOPATH «/Users/<your user name>/go»

Далее необходимо установить библиотеки. Библиотека goWeb предоставляет структуру веб-сервера, библиотека amber предоставляет эквивалентный Jade-препроцессор HTML, а BlackFriday переводит Markdown в надлежащий HTML. Также я использую библиотеку Handlebars для шаблонов. Чтобы установить эти библиотеки, вам нужно набрать следующее в каталоге проекта:

1
2
3
4
go get github.com/hoisie/web
go get github.com/eknkc/amber
go get github.com/russross/blackfriday
go get github.com/murz/go-handlebars/handlebars

Теперь, когда Go и необходимые библиотеки находятся в вашей системе, следующий шаг — начать кодирование. Библиотека goPress имеет пять файлов, в то время как основная программа — это один файл для вызова функций библиотеки.

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

Используя структуру файла, настроенную в последнем уроке, создайте новый каталог в каталоге src с именем goPress . Это будет место, где размещены все файлы библиотеки goPress . Первый файл goPress.go . Создайте файл и начните вставлять следующий код.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package goPress
 
//
// Package: goPress
//
// Description: This package is for the goPress CMS
// written in the go programming
// language made by Google.
// defines everything for a full
// CMS.
//
 
//
// Import the libraries we use for this program.
//
import (
    «encoding/json»
    «github.com/hoisie/web»
    «io/ioutil»
    «log»
    «os»
    «strings»
)
 
//
// Define a structure to contain all the information
// important to the CMS.
// variables within the structure is imported
// and exported.
//
type goPressData struct {
    CurrentLayout string
    CurrentStyling string
    ServerAddress string
    SiteTitle string
    Sitebase string
    TemplatBase string
    CapatchaWidth int
    CapatchaHeight int
    Cache bool
    MainBase string
    content map[string]string
    layoutBase string
    mainpg string
    postbase string
    scripts string
    stylesheet string
    stylingBase string
    template string
}
 
var SiteData = new(goPressData)
var ServerParamFile string = «server.json»

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

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

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

Затем добавьте эту функцию в тот же файл:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
//
// Function: GetGlobals
//
// Description: This function is used to create the
// global variables initialize the
// global variables.
//
// Inputs:
//
func GetGlobals() {
    //
    // Load the Server Parameters from a file.
    //
    LoadServerParameters()
 
    //
    // Setup the basic paths to everything.
    //
    SiteData.layoutBase = SiteData.TemplatBase + «layouts/»
    SiteData.stylingBase = SiteData.TemplatBase + «styling/»
    SiteData.postbase = SiteData.Sitebase + «posts/»
 
    //
    // Create the content array that will hold the site
    // fragments.
    //
    SiteData.content = make(map[string]string)
    SiteData.content[«title»] = SiteData.SiteTitle
 
    //
    // Log that the data is being loaded.
    //
    log.Println(«Loading data for site: » + SiteData.SiteTitle)
 
    //
    // Get all the basic information that is generic and
    // in the styles and layout directories.
    // These will then be over written if a new default
    // in the site area is found.
    // the flexibility to load defaults from a directory
    // without having to make sure that all
    // the necessary ones are loaded.
    //
 
    //
    // Get the 404 page contents
    //
    SiteData.content[«404»] = GetPageContents(SiteData.stylingBase + SiteData.CurrentStyling + «/404»)
 
    //
    // Get the sidebar contents
    //
    SiteData.content[«sidebar»] = GetPageContents(SiteData.stylingBase + SiteData.CurrentStyling + «/sidebar»)
 
    //
    // Get the footer contents
    //
    SiteData.content[«footer»] = GetPageContents(SiteData.stylingBase + SiteData.CurrentStyling + «/footer»)
 
    //
    // Get the template contents
    //
    SiteData.template = GetPageContents(SiteData.layoutBase + SiteData.CurrentLayout + «/template»)
 
    //
    // Get the header contents
    //
    SiteData.content[«header»] = GetPageContents(SiteData.stylingBase + SiteData.CurrentStyling + «/header»)
 
    //
    // Get the main page contents
    //
    SiteData.mainpg = GetPageContents(SiteData.Sitebase + «pages/» + «main»)
 
    //
    // The following will load page parts from the
    // «parts» directory for the site.
    // overload those already defined or add new stuff
    // that the users site templates
    // will need.
    //
    partsdir := SiteData.Sitebase + «parts/»
 
    //
    // Read the directory.
    //
    fileList, err := ioutil.ReadDir(partsdir)
    if err != nil {
        //
        // Error reading the directory.
        //
        log.Printf(«Error reading directory: %s\n», partsdir)
    } else {
        //
        // Get the number of items in the directory list.
        //
        count := len(fileList)
 
        //
        // Loop through each directory element.
        //
        for i := 0;
            if !fileList[i].IsDir() {
                //
                // It is a file.
                // scripts variable.
                //
                filename := fileList[i].Name()
                parts := strings.Split(filename, «.»)
                if filename != «.DS_Store» {
                    SiteData.content[parts[0]] = LoadFile(partsdir + filename)
                }
            }
        }
    }
 
    //
    // Clear out the global variables not set.
    //
    SiteData.scripts = «»
    SiteData.stylesheet = «»
}

Функция GetGlobals загружает всю глобально сохраненную информацию для сайта. Хеш-карта на основе имени файла (без расширения) хранит данные из файла сервера, каталога раскладок и каталога стилей. Затем все в каталоге site/parts помещается в одну структуру. Таким образом, если сайт просто хочет использовать значения по умолчанию, указанные в теме, пользователю не нужно помещать файл для него в каталог site/parts .

В том же файле добавьте эти функции:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//
// Function: SaveServerParameters
//
// Description: This function is for saving the
// authorization secret for DropBox.
//
// Inputs:
//
func SaveServerParameters() {
    if wfile, err := os.Create(ServerParamFile);
        enc := json.NewEncoder(wfile)
        enc.Encode(&SiteData)
        wfile.Close()
    } else {
        log.Println(«Writing Server file denied.»)
    }
}
 
//
// Function: LoadServerParameters
//
// Description: This function is used to load the
// parameters for this server.
//
// Inputs:
//
func LoadServerParameters() {
    if wfile, err := os.Open(ServerParamFile);
        enc := json.NewDecoder(wfile)
        enc.Decode(&SiteData)
        wfile.Close()
        log.Println(«Read the » + ServerParamFile + » server parameter file. Site Title is: » + SiteData.SiteTitle)
    } else {
        log.Println(«No Server File found.»)
    }
}

Это вспомогательные функции SaveServerParameters () и LoadServerParameters () . Эти функции сохраняют и загружают различные настройки сервера в файл server.json .

Следующие функции предназначены для создания маршрутов и маршрутов по умолчанию. Добавьте эти функции в один файл:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//
// Function: DefaultRoutes
//
// Description: This function sets the default
// routes for a CMS.
//
// Inputs:
//
func DefaultRoutes() {
    SetGetRoute(«/», Mainpage)
    SetGetRoute(«/sitemap.xml», SiteMap)
    SetGetRoute(«/stylesheets.css», GetStylesheets)
    SetGetRoute(«/scripts.js», GetScripts)
    SetGetRoute(«/theme/images/(.*)», LoadThemeImage)
    SetGetRoute(«/(favicon.ico)», ImagesLoad)
    SetGetRoute(«/images/(.*)», ImagesLoad)
    SetGetRoute(«/posts/([a-zA-Z0-9]*)/([a-zA-Z0-9]*)», PostIndex)
    SetGetRoute(«/posts/([a-zA-Z0-9]*)/([a-zA-Z0-9]*)/(.*)», PostPages)
    SetGetRoute(«/(.*)», TopPages)
}
 
//
// Function: SetGetRoute
//
// Description: This function gives an easy access
// to the web variable setup in this
// library.
//
// Inputs:
// route Route to setup
// handler Function to run that route.
//
func SetGetRoute(route string, handler interface{}) {
    web.Get(route, handler)
}
 
//
// Function: StartServer
//
// Description: This function is for starting the web
// server using the SiteData
// configuration.
//
// Inputs:
//
func StartServer(serverAddress string) {
    web.Run(serverAddress)
}

Функция DefaultRoutes() создает маршруты по умолчанию для использования в нашей CMS. Функции для этих маршрутов находятся в других файлах библиотеки. SetGetRoute() создает каждый маршрут. Это просто оболочка над библиотечной функцией goWeb, которая принимает регулярное выражение для определения формата маршрута и функцию, которая выполняется, когда это выражение истинно. Если вы когда-либо использовали каркас Sinatra для Ruby или каркас Express для Node.js, то вы будете знакомы с этой настройкой.

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

Функция StartServer() запускает веб-сервер. Вызывает функцию goWeb Run() которая принимает адрес для сервера.

Во всем коде я хорошо использую log.PrintLn() . Это выводит на консоль сообщение с указанием даты и времени. Это отлично подходит для отладки, но также используется для анализа трафика.

Затем создайте файл PagesPosts.go в том же каталоге. Этот файл будет содержать весь код для работы со страницами и типами записей. Страница — это просто веб-страница. Пост — это что-то, созданное с течением времени: посты новостей, посты в блогах, учебные пособия и т. Д. В этом файле добавьте следующий код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
package goPress
 
import (
    «bytes»
    «encoding/json»
    «github.com/eknkc/amber»
    «github.com/hoisie/web»
    «github.com/murz/go-handlebars/handlebars»
    «github.com/russross/blackfriday»
    «io/ioutil»
    «log»
    «os»
    «strings»
    «time»
)

Как и в файле goPress.go , он начинается с объявления package и списка импортируемых библиотек. Этот файл будет использовать все библиотеки, которые мы скачали для go .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
//
// Function: Mainpage
//
// Description: This function is used to generate
// and display the main page for the
// web site.
// the user to setup the DropBox
// account if this is the first time
// being ran or the dropbox
// authorization secret gets zeroed.
//
// Inputs:
// ctx Contents from the request
//
func Mainpage(ctx *web.Context) string {
    //
    // Render the main page.
    //
    page := RenderPageContents(ctx, SiteData.mainpg, SiteData.Sitebase+»pages/main»)
 
    return page
}

Функция Mainpage() показывает первую страницу сайта. Это просто оболочка для функции RenderPageContents() указывающая главную страницу индекса для отображения. RenderPageContents() выполняет всю реальную работу.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
//
// Function: SiteMap
//
// Description: This function is to give a site map
// to requesters.
//
// Inputs:
// ctx Contents from the request
//
func SiteMap(ctx *web.Context) string {
    var contents string
 
    wfile, err := os.Open(SiteData.Sitebase + «sitemap.xml»)
    if err == nil {
        bcontents, _ := ioutil.ReadAll(wfile)
        contents = string(bcontents)
        wfile.Close()
    }
    return contents
}

Функция SiteMap() передает карту сайта запрашивающей стороне. Он извлекает информацию из sitemap.xml в верхней части каталога сайта.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
//
// Function: PostPages
//
// Description: This function generates the needed
// post page.
//
// Inputs:
// ctx What the browser sends
// posttype The type of post
// postname The name of the post
// type instance
// val The name of the post
// page to display
//
func PostPages(ctx *web.Context, posttype string, postname string, val string) string {
    //
    // Get the page contents and process it.
    //
    pgloc := SiteData.postbase + posttype + «/» + postname + «/» + val
    return RenderPageContents(ctx, GetPageContents(pgloc), pgloc)
}

Функция PostPages() отображает правильное запрошенное сообщение. Еще раз, это просто устанавливает вызов функции RenderPageContents() , которая выполняет всю основную работу.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
//
// Function: PostIndex
//
// Description: This function generates the needed post index.
//
// Inputs:
// ctx What the browser sends
// posttype The type of post
// postname The name of the post type instance
//
func PostIndex(ctx *web.Context, posttype string, postname string) string {
    //
    // Get the page contents and process it.
    //
    pgloc := SiteData.postbase + posttype + «/» + postname + «/index»
    return RenderPageContents(ctx, GetPageContents(pgloc), pgloc)
}

Функция PostIndex() собирает информацию для почтового индекса и передает ее в RenderPageContents() .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
//
// Function: topPages
//
// Description: This function will generate a
// «static» top level page that is not
// a post page.
//
// Inputs:
// val The name of the top level page
//
func TopPages(ctx *web.Context, val string) string {
    //
    // Look for the markdown of the page.
    //
    pgloc := SiteData.Sitebase + «pages/» + val
    return RenderPageContents(ctx, GetPageContents(pgloc), pgloc)
}

Функция topPages() устанавливает функцию RenderPageContents() для стандартной страницы. Все страницы находятся в каталоге pages/ .

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
//
// Function: GetPageContents
//
// Description: This function is used to retrieve
// the page contents.
// for a markdown page, then for a html
// page, and then it looks for an amber
// page.
//
// Inputs:
// filename The name of the file
//
func GetPageContents(filename string) string {
    //
    // Assume the page can not be found.
    //
    contents := SiteData.content[«404»]
 
    //
    // Let’s look for a markdown version first.
    //
    wfile, err := os.Open(filename + «.md»)
    if err == nil {
        bcontents, _ := ioutil.ReadAll(wfile)
        wfile.Close()
        contents = string(blackfriday.MarkdownCommon(bcontents))
        //
        // Double quotes were turned into &ldquo;
        // &rdquo;.
        // Handlebar macros will be broken.
        //
        contents = strings.Replace(contents, «&ldquo;», «\»», -1)
        contents = strings.Replace(contents, «&rdquo;», «\»», -1)
    } else {
        //
        // It must be an html.
        //
        wfile, err = os.Open(filename + «.html»)
        if err == nil {
            bcontents, _ := ioutil.ReadAll(wfile)
            contents = string(bcontents)
            wfile.Close()
        } else {
            //
            // It must be an amber.
            //
            wfile, err = os.Open(filename + «.amber»)
            if err == nil {
                wfile.Close()
                template, err2 := amber.CompileFile(filename+».amber», amber.Options{true, false})
                if err2 != nil {
                    //
                    // Bad amber file.
 
                    log.Println(«Amber file bad: » + filename)
                } else {
                    //
                    // Put the default site info.
                    //
                    pgData := SiteData.content
 
                    //
                    // read in that pages specific data
                    // to be added to the rest
                    // of the data.
                    // same place, but in a json
                    // file.
                    //
                    if wfile, err := os.Open(filename + «.json»);
                        //
                        // Load the json file of extra
                        // data for this page.
                        // override the standard data as
                        // well.
                        //
                        enc := json.NewDecoder(wfile)
                        enc.Decode(&pgData)
                        wfile.Close()
                    } else {
                        log.Println(«The page: » + filename + » did not have a json file.»)
                    }
 
                    pgData[«PageName»] = filename
 
                    //
                    // The amber source compiles okay.
                    // Run the template and return
                    // the results.
                    //
                    var b bytes.Buffer
                    template.Execute(&b, pgData)
                    contents = b.String()
                }
            } else {
                //
                // A file could not be found.
                //
                log.Println(«Could not find file: » + filename)
            }
        }
    }
 
    //
    // Return the file contains obtained.
    //
    return contents
}

Функция GetPageContents() загружает содержимое всех страниц / сообщений. Сначала он загружает содержимое страницы 404 not found из глобальной структуры данных. Затем функция сначала ищет файл Markdown, затем файл HTML, а затем файл Amber. Затем процедура конвертирует весь контент Markdown и Amber в HTML. Файл Amber может иметь связанные данные в файле JSON. Этот файл данных также загружается для обработки файла Amber.

Обработка уценки в Blackfriday имеет последствия для процессора Handlebars. Уценка Blackfriday до HTML-процессора заменяет все двойные кавычки на экранированный эквивалент HTML ( &ldquo; и &rdquo; ). Так как это не на 100% необходимо для рендеринга, я впоследствии изменил это изменение. Это сохраняет все макросы Handlebars, которые используют двойные кавычки.

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

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
//
// Function: RenderPageContents
//
// Description: This function is used to process
// and render the contents of a page.
// It can be the main page, or a post
// page, or any page.
// input as the contents for the page
// template, run the page template
// with it, process all shortcodes and
// embedded codes, and return the
// results.
//
// Inputs:
// ctx The calling context
// contents The pages main contents.
// filename The name of the file the
// contents was taken from.
//
func RenderPageContents(ctx *web.Context, contents string, filename string) string {
    //
    // Set the header information
    //
    SetStandardHeader(ctx)
 
    //
    // Put the default site info.
    //
    pgData := SiteData.content
 
    //
    // Add data specific to this page.
    //
    pgData[«content»] = contents
 
    //
    // read in that pages specific data to be added to
    // the rest of the data.
    // place, but in a json file.
    //
    if wfile, err := os.Open(filename + «.json»);
        //
        // Load the json file of extra data for this
        // page.
        // well.
        //
        enc := json.NewDecoder(wfile)
        enc.Decode(&pgData)
        wfile.Close()
    } else {
        log.Println(«The page: » + filename + » did not have a json file.»)
    }
 
    //
    // Set the Page Name data field.
    //
    pgData[«PageName»] = filename
 
    //
    // Register the helpers.
    //
    // NOTICE: All helpers can not have spaces in the
    // parameter.
    // helpers assume a «-» is a space.
    // translated to a space before using.
    //
    // Helper: save
    //
    // Description: This helper allows you do define
    // macros for expanding inside the
    // template.
    // «|», and text to expand into.
    // Currently, all spaces have to be
    // «-«.
    //
    handlebars.RegisterHelper(«save», func(params …interface{}) string {
        if text, ok := params[0].(string);
            parts := strings.Split(text, «|»)
            content := strings.Replace(parts[1], «-«, » «, -1)
            pgData[parts[0]] = content
            return content
        }
        return «»
    })
 
    //
    // The format has to use these sets of constants:
    // Stdlongmonth = «January»
    // Stdmonth = «Jan»
    // Stdnummonth = «1»
    // Stdzeromonth = «01»
    // Stdlongweekday = «Monday»
    // Stdweekday = «Mon»
    // Stdday = «2»
    // Stdunderday = «_2»
    // Stdzeroday = «02»
    // Stdhour = «15»
    // stdHour12 = «3»
    // stdZeroHour12 = «03»
    // Stdminute = «4»
    // Stdzerominute = «04»
    // Stdsecond = «5»
    // Stdzerosecond = «05»
    // Stdlongyear = «2006»
    // Stdyear = «06»
    // Stdpm = «Pm»
    // Stdpm = «Pm»
    // Stdtz = «Mst»
    //
    // Helper: date
    //
    // Description: This helper prints the current
    // date/time in the format
    // given.
    // chart for proper format codes.
    // EX: 07/20/2015 is «01/02/2006»
    //
    handlebars.RegisterHelper(«date», func(params …interface{}) string {
        if format, ok := params[0].(string);
            format = strings.Replace(format, «-«, » «, -1)
            tnow := time.Now()
            return tnow.Format(format)
        }
        return «»
    })
 
    //
    // Render the current for the first pass.
    //
    page := handlebars.Render(SiteData.template, pgData)
 
    //
    // Process any shortcodes on the page.
    //
    page1 := ProcessShortCodes(page)
 
    //
    // Render new content from Short Code and filters.
    //
    page2 := handlebars.Render(page1, pgData)
 
    //
    // Return the results.
    //
    return page2}

RenderPageContents() — это основная функция, используемая для создания веб-страницы. После того, как он устанавливает стандартный заголовок для ответа, эта подпрограмма создает структуру данных и заполняет ее содержимым по умолчанию, содержимым страницы и связанным файлом JSON для страницы. Шаблонатор Handlebars использует структуру данных для отображения всей страницы.

Далее подпрограмма определяет все вспомогательные функции Handlebars. На данный момент их два: save helper и date helper. Если вам нужны дополнительные вспомогательные функции, вы можете добавить их в свой проект.

Помощник save принимает два параметра: имя, отделенное от содержимого символом | , Поскольку вспомогательные параметры Handlebars не могут содержать пробелы, в параметрах вместо пробела используется символ. Это позволяет вам создавать для каждой страницы переменные шаблона внутри контекста страницы. Например, макрос {{save site|Custom-Computer-Tools}} поместит Custom Computer Tools в точку определения и в любое место на странице, на которой есть {{site}} .

Помощник по date принимает строку формата и создает правильную дату в соответствии с этой строкой формата. Например, макрос {{date January-2,-2006}} создает October 13, 2015 в этот день.

Шаблонатор Handlebars обрабатывает страницу дважды: перед отображением коротких кодов, если в расширении шаблона есть какие-либо короткие коды, и после запуска коротких кодов, если короткий код добавляет какие-либо действия шаблона Handlebars. В конце функция возвращает полное HTML-содержимое для запрашиваемой страницы.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//
// Function: SetStandardHeader
//
// Description: This function is used as a one place
// for setting the standard
// header information.
//
// Inputs:
//
func SetStandardHeader(ctx *web.Context) {
    //
    // Set caching for the item
    //
    ctx.SetHeader(«Cache-Control», «public», false)
 
    //
    // Set the maximum age to one month (30 days)
    //
    ctx.SetHeader(«Cache-Control», «max-age=2592000», false)
 
    //
    // Set the name to gpPress for the server type.
    //
    ctx.SetHeader(«Server», «goPress — a CMS written in go from Custom Computer Tools: http://customct.com.», true)
}

Функция SetStandardHeader() устанавливает любые пользовательские элементы заголовка в ответ. Здесь вы устанавливаете информацию о сервере и любые элементы управления кэшированием.

Следующий файл для работы — это файл Images.go и все функции, необходимые для отправки изображения в браузер. Поскольку это будет полноценный веб-сервер, он должен иметь дело с двоичными данными отправки изображения. Создайте файл Images.go и поместите в него этот код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package goPress
 
import (
    «github.com/hoisie/web»
    «io»
    «io/ioutil»
    «log»
    «math/big»
    «os»
    «path/filepath»
)
 
//
// Function: ImagesLoad
//
// Description: This function is called to upload an image for the
// images directory.
//
// Inputs:
// val Name of the image with relative path
//
func ImagesLoad(ctx *web.Context, val string) {
    LoadImage(ctx, SiteData.Sitebase+»images/»+val)
}
 
//
// Function: LoadThemeImage
//
// Description: This function loads images from the theme’s directory.
//
// Inputs
// image Name of the image file to load
//
func LoadThemeImage(ctx *web.Context, image string) {
    LoadImage(ctx, SiteData.stylingBase+SiteData.CurrentStyling+»/images/»+image)
}

Этот файл библиотеки запускается так же, как и другие: объявление пакета и объявление библиотеки. Функция ImagesLoad() функция LoadThemeImage() устанавливают вызов функции LoadImage() для выполнения реальной работы. Эти функции позволяют загружать изображения из каталога сайта или из каталога текущей темы.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
//
// Function: LoadImage
//
// Description: This function does the work of
// loading an image file and passing it
// on.
//
// Inputs:
//
func LoadImage(ctx *web.Context, val string) {
    //
    // Get the file extension.
    //
    fileExt := filepath.Ext(val)
 
    //
    // Set the http header based on the file type.
    //
    SetStandardHeader(ctx)
    ctx.ContentType(fileExt)
    if fileExt == «.svg» {
        //
        // This is a text based file.
        //
        wfile, err := os.Open(val)
        if err == nil {
            bcontents, _ := ioutil.ReadAll(wfile)
            wfile.Close()
            ctx.WriteString(string(bcontents))
        }
    } else {
        //
        // This is a binary based file.
        //
        fi, err := os.Open(val)
 
        //
        // Set the size of the binary coming down the pipe.
        // one larger than real.
        //
        finfo, _ := os.Stat(val)
        i := big.NewInt(finfo.Size())
        ctx.SetHeader(«Accept-Ranges», «bytes», true)
        ctx.SetHeader(«Content-Length», i.String(), true)
 
        if err != nil {
            log.Println(err)
            return
        }
        defer fi.Close()
 
        //
        // Create a buffer to contain the image data.
        // very big.
        //
        buf := make([]byte, 1024)
 
        //
        // Go through the binary file 1K at a time and send to the browser.
        //
        for {
            //
            // Read a buffer full.
            //
            n, err := fi.Read(buf)
            if err != nil && err != io.EOF {
                log.Println(err)
                break
            }
 
            //
            // If nothing was read, then exit.
            //
            if n == 0 {
                break
            }
 
            //
            // Write the binary buffer to the browser.
            //
            n2, err := ctx.Write(buf[:n])
            if err != nil {
                log.Println(err)
                break
            } else if n2 != n {
                log.Println(«Error in sending » + val + » to the browser. Amount read does not equal the amount sent.»)
                break
            }
        }
    }
}

Функция LoadImage() проверяет тип изображения. Если это svg-файл, то он загружается как обычный текст. Предполагая, что все другие типы файлов являются бинарными файлами, процедура загружает их более тщательно. Это загрузит двоичные файлы в блоки 1K.

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

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package goPress
 
import (
    «github.com/hoisie/web»
    «io/ioutil»
    «log»
    «os»
)
 
//
// Function: GetStylesheets
//
// Description: This function is used to produce
// the stylesheet to the user.
//
// Inputs:
//
func GetStylesheets(ctx *web.Context) string {
    //
    // See if we have already loaded them or not.
    // just return the pre-loaded stylesheet.
    //
    ctx.SetHeader(«Content-Type», «text/css», true)
    SetStandardHeader(ctx)
    tmp := «»
    if SiteData.stylesheet == «» {
        tmp = LoadFile(SiteData.Sitebase + «css/final/final.css»)
        //
        // If we are testing, we do not want the server
        // to cache the stylesheets.
        // cache is true, cache them.
        // do not.
        //
        if SiteData.Cache == true {
            SiteData.stylesheet = tmp
        }
    } else {
        //
        // We have a cached style sheet.
        // browser.
        //
        tmp = SiteData.stylesheet
    }
 
    //
    // Return the stylesheet.
    //
    return tmp
}
 
//
// Function: GetScripts
//
// Description: This function is to load JavaScripts
// to the browser.
// actually load all the JavaScript
// files into one compressed file
// for uploading to the browser.
//
// Inputs:
//
func GetScripts(ctx *web.Context) string {
    //
    // See if we have already loaded them or not.
    // just return the pre-loaded scripts.
    //
    ctx.SetHeader(«Content-Type», «text/javascript», true)
    SetStandardHeader(ctx)
    tmp := «»
    if SiteData.scripts == «» {
        tmp = LoadFile(SiteData.Sitebase + «js/final/final.js»)
 
        //
        // If we are testing, we do not want the server
        // to cache the scripts.
        // true, cache them.
        //
        if SiteData.Cache == true {
            SiteData.scripts = tmp
        }
    } else {
        //
        // We have a cached style sheet.
        // browser.
        //
        tmp = SiteData.scripts
    }
 
    //
    // Return the resulting compiled stylesheet.
    //
    return tmp
}
 
//
// Function: LoadFile
//
// Description: This function if for loading
// individual file contents.
//
// Inputs
// file name of the file to be
// loaded
//
func LoadFile(file string) string {
    ret := «»
    log.Println(«Loading file: » + file)
    wfile, err := os.Open(file)
    if err == nil {
        bcontents, err := ioutil.ReadAll(wfile)
        err = err
        ret = string(bcontents)
        wfile.Close()
    } else {
        //
        // Could not read the file.
        //
        log.Println(«Could not read: » + file)
    }
    return ret
}

Этот файл имеет три функции. Функция GetStylesheets() загружает скомпилированный файл CSS. Функция GetScripts() загружает скомпилированный файл JavaScript. С установленным флагом кэширования обе эти функции будут кэшировать содержимое. Я отключаю флаг Cache во время тестирования. Функция LoadFile() — это простая функция загрузки файла для получения содержимого файла.

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

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

Создайте файл Shortcodes.go и поместите его в него:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package goPress
 
//
// Library: Shortcodes
//
// Description: This library gives the functionality
// of shortcodes in the CMS.
// runs a function with specified
// argument and a surrounded contents.
// The function should process the
// contents according to the arguments
// and return a string for placement
// into the page.
// recursively processed, therefore you
// can have different shortcodes inside
// of other shortcodes.
// have the same shortcode inside of
// itself.
//
import (
    «bytes»
    «log»
    «regexp»
)
 
//
// Type: ShortcodeFunction
//
// Description: This type defines a function that
// implements a shortcode.
// should receive two strings and return
// a string.
//
type ShortcodeFunction func(string, string) string
 
//
// Library Variables:
//
// shortcodeStack This array of functions
// holds all of the
// shortcodes usable by the
// CMS.
// using the AddShortCode
// function.
//
var (
    shortcodeStack map[string]ShortcodeFunction
)
 
//
// Library Function:
//
// init This function is called upon
// library use to initialize any
// variables used for the
// library before anyone can
// make a call to a library
// function.
//
 
func init() {
    shortcodeStack = make(map[string]ShortcodeFunction)
}

Этот файл начинается, как и все остальные, с объявления пакета и используемых библиотек. Но это быстро отличается от определения специального ShortcodeFunction переменной ShortcodeFunction , библиотечной переменной и функции init() . Библиотечные переменные видны только функцией библиотеки. Эта библиотечная переменная, shortcodeStack , является отображением строк в функцию.

Функции библиотеки init() позволяют запускать код перед любыми другими вызовами библиотеки. Здесь я инициализирую структуру данных shortcodeStack для хранения списка шорткодов.

01
02
03
04
05
06
07
08
09
10
11
12
13
//
// Function: AddShortCode
//
// Description: This function adds a new shortcode to
// be used.
//
// Inputs
// name Name of the shortcode
// funct function to process the shortcode
//
func AddShortCode(name string, funct ShortcodeFunction) {
    shortcodeStack[name] = funct
}

Функция AddShortCode() позволяет загрузить функцию для обработки шорткода в библиотечную переменную для всех шорткодов.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
//
// Function: ProcessShortCodes
//
// Description: This function takes in a string,
// searches for shortcodes, process the
// shortcode, put the results into the
// string, and then return the fully
// processed string.
//
// Inputs
// page String with possible shortcodes
//
func ProcessShortCodes(page string) string {
    //
    // Create a work buffer.
    //
    var buff bytes.Buffer
 
    //
    // Search for shortcodes.
    // shortcode.
    //
    r, err := regexp.Compile(`\-\[([^\]]*)\]\-`)
    if err == nil {
        match := r.FindString(page)
        if match != «» {
            //
            // Get the indexes to the matching area.
            //
            index := r.FindStringIndex(page)
 
            //
            // Save all the text before the shortcode
            // into the buffer.
            //
            buff.WriteString(page[0:index[0]])
 
            //
            // Get everything that is left after the
            // shortcode.
            //
            remander := page[index[1]:]
 
            //
            // Separate the strings out and setup
            // variables for their contents.
            //
            submatch := r.FindStringSubmatch(match)
            name := «»
            contents := «»
            args := «»
 
            //
            // Get the name of the shortcode and separate
            // the extra arguments.
            //
            r3, err3 := regexp.Compile(`(\w+)(.*)`)
            if err3 == nil {
                submatch2 := r3.FindStringSubmatch(submatch[1])
 
                //
                // The first submatch is the name of the
                // shortcode.
                //
                name = submatch2[1]
 
                //
                // The second submatch, if any, are the
                // arguments for the shortcode.
                //
                args = submatch2[2]
            } else {
                //
                // Something happened to the internal
                // matching.
                //
                name = submatch[1]
                args = «»
            }
 
            //
            // Find the end of the shortcode.
            //
            final := «\\-\\[\\/» + name + «\\]\\-«
            r2, err2 := regexp.Compile(final)
            if err2 == nil {
                index2 := r2.FindStringIndex(remander)
                if index2 != nil {
                    //
                    // Get the contents and what is left
                    // over after the closing of the
                    // shortcode.
                    //
                    contents = remander[:index2[0]]
                    remander2 := remander[index2[1]:]
 
                    //
                    // If it is a real shortcode, then
                    // run it!
                    //
                    if shortcodeStack[name] != nil {
                        //
                        // See if there is any shortcodes
                        // inside the contents area.
                        //
                        contents = ProcessShortCodes(contents)
 
                        //
                        // Run the shortcode and add it’s
                        // result to the buffer.
                        //
                        buff.WriteString(shortcodeStack[name](args, contents))
                    }
 
                    //
                    // Process any remaining shortcodes.
                    //
                    buff.WriteString(ProcessShortCodes(remander2))
 
                } else {
                    //
                    // We have a bad shortcode
                    // definition.
                    // to be closed.
                    // simply do not process anything and
                    // tell the logs.
                    //
                    log.Println(«Bad Shortcode definition. It was not closed. Name: » + name)
                    buff.WriteString(page[index[0]:index[1]])
                    buff.WriteString(ProcessShortCodes(remander))
                }
            } else {
                //
                // There was an error in the regular
                // expression for closing the shortcode.
                //
                log.Println(«The closing shortcode’s regexp did not work!»)
            }
        } else {
            //
            // No shortcodes, just copy the page to the
            // buffer.
            //
            buff.WriteString(page)
        }
    } else {
        //
        // If the Regular Expression is invalid, tell the
        // world!
        //
        log.Println(«RegEx: Invalid expression.»)
    }
 
    //
    // Return the resulting buffer.
    //
    return buff.String()
}

Функция ProcessShortCodes() берет строку, которая является содержимым веб-страницы, и ищет все шорткоды в ней. Поэтому, если у вас есть шорткод, называемый box , вы должны вставить его на свою веб-страницу в следующем формате:

1
2
3
-[box args=»some items»]-
<p>This should be inside the box.</p>
-[/box]-

Все после пробела в открывателе шорткода является аргументом для шорткода для обработки. Форматирование аргументов зависит от функции шорткода для обработки.

Все короткие коды должны иметь закрывающий шорткод. Внутри открывающего и закрывающего шорткода находится процесс для коротких кодов, а также перед отправкой в ​​функцию обработки шорткодов. Я использую -[]- для определения шорткода, чтобы индексирование встроенного JavaScript не путалось как шорткод.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
//
// Function: ShortcodeBox
//
// Description: This shortcode is used to put the
// surrounded HTML in a box div.
//
// Inputs:
// parms The parameters used by the
// shortcode
// context The HTML enclosed by the opening
// and closing shortcodes.
//
func ShortcodeBox(parms string, context string) string {
    return («<div class=’box’>» + context + «</div>»)
}
 
//
// Function: ShortcodeColumn1
//
// Description: This shortcode is used to put the
// surrounded HTML in the first column.
//
// Inputs:
// parms The parameters used by the
// shortcode
// context The HTML enclosed by the opening
// and closing shortcodes.
//
func ShortcodeColumn1(parms string, context string) string {
    return («<div class=’col1′>» + context + «</div>»)
}
 
//
// Function: ShortcodeColumn2
//
// Description: This shortcode is used to put the
// surrounded HTML in the second column.
//
// Inputs:
// parms The parameters used by the
// shortcode
// context The HTML enclosed by the opening
// and closing shortcodes.
//
func ShortcodeColumn2(parms string, context string) string {
    return («<div class=’col2′>» + context + «</div>»)
}
 
//
// Function: ShortcodePHP
//
// Description: This shortcode is for surrounding a
// code block and formatting it’s look
// and feel properly.
// PHP code block.
//
// Inputs:
// parms The parameters used by the
// shortcode
// context The HTML enclosed by the opening
// and closing shortcodes.
//
func ShortcodePHP(params string, context string) string {
    return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: php'>" + context + "</pre></div>")
}
 
//
// Function: ShortcodeJS
//
// Description: This shortcode is for surrounding a
// code block and formatting it's look
// and feel properly. This one is for a
// JavaScript code block.
//
// Inputs:
// parms The parameters used by the
// shortcode
// context The HTML enclosed by the opening
// and closing shortcodes.
//
func ShortcodeJS(params string, context string) string {
    return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: javascript'>" + context + "</pre></div>")
}
 
//
// Function: ShortcodeHTML
//
// Description: This shortcode is for surrounding a
// code block and formatting it's look
// and feel properly. This one is for a
// HTML code block.
//
// Inputs:
// parms The parameters used by the
// shortcode
// context The HTML enclosed by the opening
// and closing shortcodes.
//
func ShortcodeHTML(params string, context string) string {
    return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: html'>" + context + "</pre></div>")
}
 
//
// Function: ShortcodeCSS
//
// Description: This shortcode is for surrounding a
// code block and formatting it's look
// and feel properly. This one is for a
// CSS code block.
//
// Inputs:
// parms The parameters used by the
// shortcode
// context The HTML enclosed by the opening
// and closing shortcodes.
//
func ShortcodeCSS(params string, context string) string {
    return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: css'>" + context + "</pre></div>")
}
 
//
// Function: LoadDefaultShortcodes
//
// Description: This function is used to load in all
// the default shortcodes.
//
// Inputs:
//
func LoadDefaultShortcodes() {
    AddShortCode("Box", ShortcodeBox)
    AddShortCode("Column1", ShortcodeColumn1)
    AddShortCode("Column2", ShortcodeColumn2)
    AddShortCode("php", ShortcodePHP)
    AddShortCode("javascript", ShortcodeJS)
    AddShortCode("html", ShortcodeHTML)
    AddShortCode("css", ShortcodeCSS)
}

The last section of code defines seven simple shortcodes and adds them to the shortcode array using the LoadDefaultShortcodes() function. If you want a different functionality, you just have to change this code and it will update it everywhere in your web site.

The last file to create is the main program file. In the top of the development directory, create the file goPressServer.go and place this information:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main
 
import (
    "./src/goPress"
)
 
//
// Function: main
//
// Description: This is the main function that is
// called whenever the program is
// executed. It will load the globals,
// set the different routes, and
// start the server.
//
// Inputs:
//
func main() {
    //
    // Load the default Shortcodes.
    //
    goPress.LoadDefaultShortcodes()
 
    //
    // Load all global variables.
    //
    goPress.GetGlobals()
 
    //
    // Setup the Default routes.
    //
    goPress.DefaultRoutes()
 
    //
    // Run the web server
    //
    goPress.StartServer(goPress.SiteData.ServerAddress)
}

The main() function is the routine called when the program runs. It will first set up the shortcodes, load in global variables, set the default routes, and then start the server.

To compile the whole program, move to the top directory where the goPressServer.go file is and type:

1
go build goPressServer.go

If all the files are in place, it should compile to goPressServer on the Mac and Linux systems, and goPressServer.exe on Windows.

Running goPressServer in the Terminal

When you execute the program in a terminal, you will see its log statements with the date and time as above.

The Front Page From the Server

If you open your browser to the server’s address, you will get the front page. You will see the example shortcode and the two different Handlebars helper functions used. You now have your own web server!

As you can tell, I changed the front page and added three more pages to the original site design given in the tutorial Building a CMS: Structure . I also added the JavaScript library Syntax Highlighter in the site/js/ directory for displaying the code on the web page using the shortcode.

All of these changes are to show off the Handlebars and shortcode processing. But, due to Syntax Highlighter not working well with compression, I removed the JavaScript compression from the Gulp file. All of the changes are in this tutorial’s download file.

There is a new course out, Go Fundamentals for Building Web Servers , that gives a great introduction to the Go language and how to program with it.

Now that you know how to build a simple yet powerful webserver using the go language, it’s time for you to experiment. Создавайте новые страницы, сообщения, встраиваемые части и шорткоды. Эта простая платформа намного быстрее, чем WordPress, и она полностью под вашим контролем. Расскажите мне о вашем сервере в комментариях ниже.