一、写在前面

这篇文章主要介绍了OpenStack Mitaka Horizon主题的开发。这里只是说明horiozn主题包的开发逻辑,不具体阐述css、js、html文件的开发, 仅仅是说明horizon主题开发的方式,因为时间仓促以及个人理解有限,固有错误的地方请指出,后续将会不定期的完善,谢谢!
如果转载,请保留作者信息。
邮箱地址:jpzhang.ht@gmail.com
主题下载地址:https://jianpengzhang.github.io/2017/02/26/2017022602/

二、环境准备

注意:如果没有特殊说明,一下所有命令均是在root用户下执行。

通过devstack 部署OpenStack Mitaka单节点基础版本(nova、keystone、glance、vloume、horizon),具体如何部署可以参考我的另一片博文,部署完成之后可以看到目录结构如下:
.
├── cinder
├── glance
├── horizon
├── keystone
├── logs
├── neutron
├── nova
├── noVNC
├── requirements
└── tempest
可以看到我这里安装的所有组件的源码包,根据实际情况会有不同,以自己的的为主。
这里主要介绍horizon组件theme的开发,其他组件这里不涉及,不需要关注,只要保证各个服务时运行正常即OK,后续将逐渐来说明其他的组件的开发,大家可以关注下我的博客或者最近刚建的博客,后续将同步更新,废话不多说,进入正题。
拷贝一份horizon代码包:

cp -r horizon horizon.dev

进入:cd horizon.dev
通过django 自带的测试服务,运行horizon组件

python manage.py runserver 0.0.0.0:8001输出:
Performing system checks...System check identified no issues (0 silenced).
June 12, 2016 - 13:21:59
Django version 1.8.10, using settings 'openstack_dashboard.settings'
Starting development server at http://0.0.0.0:8001/
Quit the server with CONTROL-C.

如果是通过devstack部署的,通过这种方式运行起来的horizon,通过浏览器访问,浏览器将会出现如下错误:

Page not found (404)
Request Method: GET
Request URL:    http://192.168.31.235:8001/dashboard/auth/login/?next=/
Using the URLconf defined in openstack_dashboard.urls, Django tried these URL patterns, in this order:
^$ [name='splash']
^api/
^home/$ [name='user_home']
^i18n/js/(?P<packages>\S+?)/$ [name='jsi18n']
^i18n/setlang/$ [name='set_language']
^i18n/
^jasmine-legacy/$ [name='jasmine_tests']
^jasmine/.*?$
^identity/
^developer/
^admin/
^settings/
^project/
^auth/
^dashboard\/static\/(?P<path>.*)$
^dashboard\/media\/(?P<path>.*)$
^500/$
The current URL, dashboard/auth/login/, didn't match any of these.
You're seeing this error because you have DEBUG = True in your Django settings file. Change that to False, and Django will display a standard 404 page.

你可以看到浏览器url地址栏默认跳转到了:
“http://192.168.31.235:8001/dashboard/auth/login/?next=/“
如果我们手动把浏览器地址栏中的/dashoboard/去除再次刷新就可以看到熟悉的horizon默认的登录界面

不用担心,这是因为horizon的配置文件配置的url里面默认根目录是从“dashboard”开始的,如果通过apache运行,就可以在apache horizon的配置中看到,默认horiozn的资源访问路径是从‘dashboard’开始,apache如下:

WSGIScriptAlias /dashboard /home/devstack.mitaka/horizon/openstack_dashboard/wsgi/django.wsgiWSGIDaemonProcess horizon user=stack group=stack processes=3 threads=10 home=/home/devstack.mitaka/horizon display-name=%{GROUP}WSGIApplicationGroup %{GLOBAL}SetEnv APACHE_RUN_USER stackSetEnv APACHE_RUN_GROUP stackWSGIProcessGroup horizonDocumentRoot /home/devstack.mitaka/horizon/.blackhole/Alias /dashboard/media /home/devstack.mitaka/horizon/openstack_dashboard/staticAlias /dashboard/static /home/devstack.mitaka/horizon/staticRedirectMatch "^/$" "/dashboard/"

那这里我们只要修改horizon的配置,让其默认的访问路径从“/”开始即可正常。
具体修改:

vi horizon.dev/openstack_dashboard/local/local_settings.py找到:WEBROOT="/dashboard/"
修改为:WEBROOT="/"cd horizon.dev/
运行(静态文件压缩):python manage.py compress
输出:
Found 'compress' tags in:/home/devstack.mitaka/horizon.dev/openstack_dashboard/templates/horizon/_conf.html/home/devstack.mitaka/horizon.dev/openstack_dashboard/templates/_stylesheets.html/home/devstack.mitaka/horizon.dev/openstack_dashboard/templates/horizon/_scripts.html
Compressing... done
Compressed 5 block(s) from 3 template(s) for 2 context(s).

重启服务:python manage.py runserver 0.0.0.0:8001
再次访问浏览器地址:http://192.168.31.235:8001/
这回自动跳转到:http://192.168.31.235:8001/auth/login/?next=/
地址栏中没有出现“/dashboard/” 并且正常出现horizon登录界面,至此我们的开发环境准备完成,至于用什么IDE来开发,我相信每个程序员都有自己的喜好,这里我使用Sublime来开发,个人比较偏向于pycharm来开发,无奈环境安装在虚拟机中,pycharm社区版不支持远程开发,正好sublime有sftp工具能够远程通过代码进行开发,一拍即合,这里就用sublime来进行开发工作。

三、目录结构

Horizon目录结构介绍,之前有一篇博文进行了专门的介绍,不了解的可以翻一翻,这里主要介绍下几个主题开发会用到的目录以及自己创建的主题包:
.
├── doc
├── horizon
├── horizon.egg-info
├── openstack_dashboard
├── releasenotes
├── static
└── tools
可以看到horizon最外层有一个static静态文件目录,这个静态目录是用来干什么的呢?进入目录可以看到它的结构如下:
.
├── app
├── auth
├── bootstrap
├── dashboard
├── framework
├── horizon
├── scss
└── themes

这个目录其实就是horizon界面访问的静态文件的统一入口,所有样式模版(css、js、html)都是这个目录提供的,那是不是我们开发的静态文件放在这个目录中呢,其实不然,我这个目录的文件是通过一个命名生成的,

命令:python manage.py collectstatic

一般我会在后面加一个 “-c” 的参数,把原先 static目录中的内容清空。
大家可以通过python manage.py collectstatic -h 具体查看这个命令的功能,简单来说会把horizon目录下以及dashboard目录下所有用到的静态文件统一拷贝到这个目录中,在通过命令进行压缩,为上层提供所有的样式文件。

命令:python manage.py compress

  • horizon.dev/horizon目录,这个目录用来定义horizon组件全局的样式文件,其中包括一个static、 templates、templatetags 目录,
    static:存放一些静态文件CSS、 JS
    templates:存放模版文件主要是html页面
    templatetags:定义html模版页面中出现出现的标签例如:在horizon.dev/horizon/templates/horizon/_nav_list.html {% if user|has_permissions:component %},horizon.dev/horizon/templatetags/horizon.py中就有定义。

  • horizon.dev/openstack_dashboard目录,同样在这个目录下也包括static、 templates、templatetags 目录,作用和上述的一样,不过这里定义的不是全局的,可以把它当作是对horizon目录中定义的文件,在这里在做差距化的定义。

那么horion加载的同一个文件在两个地方都有的情况下回调用哪个文件,这个你可以看下django 模版调用机制,horiozn组件很好的利用了这一点,这里我也记不太清了,记忆中是先调用openstck目录中的,找不到的情况下会去horizon里面去找。
好了,具体不再细聊了,接下去就开始我们定制化的开发工作。

四、bruce主题初始化

进入我们的主题开发目录,通过拷贝现有的material目录结构来具体说明:

cd horizon.dev/openstack_dashboard/themes

该目录用来存放horizon组件的所有的主题包,默认已经有两个目录包default、material 在上个版本中,已经支持主题开发,甚至在juno版本中也支持了,只是在Mitaka更加完善了这部分,如果学会了在Mitaka版本的开发原理,在juno版本中开发也是如此。
默认horizon采用的是default 目录,但是可以通过dashboard界面的切换主题的按钮进行主题切换如下:

拷贝一份“Material”目录为bruce目录

cp -r material/ bruce

如果仅仅拷贝一份还是不行的,要让horizon识别,还需要在配置文件中进行配置,
这里我为了方便直接修改了settings.py(你也可以修改local/local_settings.py):

vi horizon.dev/openstack_dashboard/settings.py
AVAILABLE_THEMES = [('default',pgettext_lazy('Default style theme', 'Default'),'themes/default'), ('material',pgettext_lazy("Google's Material Design style theme", "Material"),'themes/material'),('bruce',pgettext_lazy("Google's Material Design style theme", "Bruce"),'themes/bruce'),
]

这个时候你刷新浏览器会提示错误,需要你运行“python manage.py compress”,重新编译压缩下,那就按照提示做,运行命令:

python manage.py compress
重启: python manage.py runserver 0.0.0.0:8001

再次刷新浏览器,你就可以看到刚刚我们定义的主题bruce:

切换至Bruce主题可以看到和Material一样的样式主题

Bruce从Material拷贝过来当然一样。
那么如何让我们的Bruce主题成为horizon主题默认主题呢,还是一样,修改

vi horizon.dev/openstack_dashboard/settings.py
DEFAULT_THEME = 'default' => DEFAULT_THEME = 'bruce'

这个时候你清空浏览器缓存,重新登录horiozn,你就会发现horizon默认的主题采用的不是“default”而是我们定义的“bruce”。

接下来我们来具体认识下从Material拷贝过来的目录包括了哪些东西,具体有什么用。
.
├── static
│ ├── bootstrap
│ ├── horizon
│ └── js
│ └── _styles.scss
│ └── _variables.scss

└── templates
├── auth
├── header
├── horizon
└── material

static(静态文件):
bootstrap:引入bootstrap目录,具体可以看看bootstrap目录中的_styles.scss、_variables.scss

  • horizon:引入horizon组件的静态文件包
  • js:引入自定义的js文件
  • _styles.scss:引入bootstrap、horizon目录中的样式文件,通过“@import “bootstrap/styles”;”其中styles指的是bootstrap目录中_styles.scss文件,同理horizon。
  • _variables.scss:引入bootstrap、horizon目录中的定义的scss定义的变量文件,通过“@import “bootstrap/variables”;”其中variables指的是bootstrap目录中_variables.scss文件,同理horizon。

templates html模版文件:

其实修改horizon的模版文件很简单,我只要按照上文中提到的horizon模版定义的路径,在自己的主题模版中再定义一遍就能覆盖,其实也不叫覆盖,就是在我的主题模版中一模一样的路径以及文件名定义一份,那么优先加载我定义的,当然前提是主题需要切换到我定义的主题上,不然还是不会加载的。
以下目录的介绍不单单指的是当前主题中定义的,还包括horizon主题模版中这些目录包含的内容,如果我对这些模版有修改的需求,我只需要在这里定义一遍,切换到我定义的主题时,就能应用上。

auth:horizon登录界面相关的模版文件
header:horizon内页header样式文件,包括项目切换等。
horizon:这个目录包括的东西比较多,包括内页左侧导航、form表单、table、model、tab等
material:这里我不需要了,直接删除就可以了

梳理了一遍,现在把bruce主题目录下面不需要的目录删除,添加自己需要的目录,具体目录结构如下:
为了方便开发,我这里直接把auth、templates、horizon拷贝到bruce templates目录中,按照正常的开发来说,我需要重定制什么那么就把什么文件在主题中定义一遍,我这边为了方便直接把整个目录拷贝过来了。

cp -r /horizon.dev/horizon/templates/auth/cp -r /horizon.dev/openstack_dashboard/templates/header/cp -r /horizon.dev/horizon/templates/horizon/

并且在static目录中新建了一个bruce目录用来放我的静态文件。在最外层_styles.scss、_variables.scss文件中引入bruce目录中styles、variables定义的内容。引入方式很简单,分别在最外层_styles.scss、_variables.scss文件增加以下内容:

_styles.scss:
@import "bruce/styles";_variables.scss:
@import "bruce/variables";

在运行命令:
这里不再细说scss样式文件的开发,后续有时间在通过其他博文具体来说明。

python manage.py collectstatic -c
python manage.py compress

把你新增的scss文件编译压缩。

最终你的目录结构看起来会是这个样子:
.
├── static
│ ├── bootstrap
│ ├── bruce
│ ├── _styles.scss
│ └── _variables.scss
│ ├── horizon
│ ├── _styles.scss
│ └── _variables.scss
└── templates
├── auth
├── header
└── horizon

其中,在templates/horizon目录下面,我从
openstack_dashboard/templates/horizon/_scripts.html文件拷贝了一份,现在你可以把你的js文件放在static/bruce/js目录中,通过_scripts.html文件加载进来,不过,通过这个文件加载的js文件时全局的,即各个页面只要按照horizon的html结构就能全部应用到,如果不想在各个页面使用,可以单独在html页面中引用,css文件也可以通过在html页面中单独引用,具体怎么引你可以看下_scripts.html引用方式即可。

五、登录界面开发

之前提过,horizon登录界面的模版文件全部定义在auth目录中,现在我们简单进行开发,

打开:/themes/bruce/templates/auth/login.html 登录界面的入口文件

{% extends "base.html" %}
{% load i18n %}{% block title %}{% trans "Login" %}{% endblock %}{% block body_id %}splash{% endblock %}{% block content %}{% include 'auth/_login.html' %}
{% endblock %}{% block footer %}{% include '_login_footer.html' %}
{% endblock %}

可以看到,这个文件继承自base.html,定义在
horizon.dev/openstack_dashboard/templates目录中,
base.html:文件定义了整个html的目录结构,如果你需要调整html结构,可以把这个文件复制到/themes/bruce/templates/目录中,如果登录界面的html结构和内页的结构不一样,可以重定义一个login_base.html,只要{% extends “login_base.html” %}即可。
这里同样我把这个文件复制了过来。为了说明,草率的进行了修改
修改加了一个div:

  <body id="{% block body_id %}{% endblock %}" ng-app='horizon.app' ng-strict-di><div style="background-color: #DE1426;">Test</div>......

可以看到生效了,看起来比较丑,哈哈,说明我们的结构修改生效了。

接下来我们再来看下auth/_login.html,

{% load i18n %}{% if 'is_modal' in request.GET or 'is_modal' in request.POST %}{% include 'auth/_login_modal.html' %}
{% else %}{% include 'auth/_login_page.html' %}
{% endif %}

这个文件引入了_login_modal.html文件和_login_page.html文件,如果你觉的这样的引入层级太复杂了,也可以直接修改掉。

auth/_login_modal.html:定义登录表单,打开这个文件,可以看到它时继承自auth/_login_form.html文件,这个文件中详细定义了登录界面,
修改如下:

    <div class="panel-heading">{% block login_header %}<h3 class="login-title">{% trans 'Log in' %}Hello World</h3>{% endblock %}</div>

简单吧,登录界面的修改就是这些auth/*.html页面中,具体就要看你需要修改成什么样子。

五、header 修改

好了,现在我们来看下登录进去之后header这块怎么来修改,
同理:header html页面主要在/themes/bruce/templates/header目录中
通过base.html:

 <div class='topbar'>{% include "header/_header.html" %}</div>

可以得知_header.html是header模块的入口文件,打开
/themes/bruce/templates/header/_header.html
修改如下:

<nav class="navbar navbar-default navbar-fixed-top" style="background-color: #3E3B3B;">
....

效果如下:

继续往下看,可以看到_header.html文件中引入

  • {% include “header/_brand.html” %}:
    没什么好讲,这个文件主要用来加载logo,如果你对logo的部署有所调整可以在这个文件中修改。

  • {% include “header/_context_selection.html” %}:
    该文件主要用来在定义项目/区域切换的样式:
    简单修改如下:

    {% else %}
    <li class="dropdown" style="background-color: #57b382;">
    ......

可以看到项目切换颜色发生了变化。
在这个文件中通过{% load context_selection %}自定义的标签引入了horizon.dev/openstack_dashboard/templatetags/context_selection.py 整个horizon项目中templatetags目录中定义的私有的django模版标签即自定义模版标签。
通过引入这个文件,在_context_selection.html中使用的标签都可以在context_selection.py文件中找到他的实现。
{% show_overview %}:对应“def show_overview(context)”方法,并且可以根据这个方法得知它所使用的模版horizon.dev/openstack_dashboard/templates/context_selection/_overview.html,如果你想修改这个模版片段,原理都一样,那这个文件安相同的目录结构在我们的主题/themes/bruce/templates/context_selection/建立起来再进行修改即可,这里不再详细说明。
{% show_domain_list %}:修改原理同上
{% show_project_list %}:修改原理同上
{% show_region_list %}:修改原理同上

  • {% include “header/_user_menu.html” %}:
    主要用来显示右侧点击当前登录用户名的下拉列表框,包括帮助、主题切换、退出等功能
    简单修改如下:

    {% else %}
    <li class="dropdown user-menu" style="background-color: #57b382;">

    效果:

  • {% include “header/_region_selection.html” %}:
    用来定义区域选择,因为本地开发环境没有多区域,不能简单修改样式,但基本和上述一样进行修改即可。

如果你想在header下面添加面包屑,这一块horizon默认已经给我写好了一个样式,通过下面的修改很快就能实现一个简单样式的面包屑,当然样式就不咋的了,
修改:/themes/bruce/templates/header/_header.html

......
<div style="    margin-top: 43px; margin-left: 240px;">
{% include 'horizon/common/_breadcrumb_nav.html' %}
</div>
{% endspaceless %}

header的开发就讲到这里,其实都还比较简单。

五、sidebar修改(侧边导航)

根据/themes/bruce/templates/base.html 可知侧边导航入口文件为:{% include ‘horizon/common/_sidebar.html’ %}

......
{% block sidebar %}{% include 'horizon/common/_sidebar.html' %}
{% endblock %}
......

ok,我们打开/themes/bruce/templates/horizon/common/_sidebar.html

{% load branding horizon i18n %}<div id='sidebar' style="background: #353644;">{% horizon_nav %}
</div>

那么在哪里定义了horizon_nav这个变量呢?可以看到它加载了{% load branding horizon i18n %},注意,这里有“horizon”这个文件指的是horizon.dev/horizon/templatetags/horizon.py,其中{% horizon_nav %}标签通过文件中的”def horizon_nav(context):“函数方法定义,并且该函数方法指定了模版文件为horizon/_sidebar.html。如果需要对返回的数据格式有修改就需要改动这个文件,不建议直接修改,可以重构一个方法来进行修改合适。打开/themes/bruce/templates/horizon/_sidebar.html,没什么好讲都是html代码,其中导航就是通过for标签循环components变量加载出来的,直接进行修改就行了。
简单的在:

/themes/bruce/templates/horizon/common/_sidebar.html
{% load branding horizon i18n %}
<div id='sidebar' style="background: #43444C;">{% horizon_nav %}
</div>/themes/bruce/templates/horizon/_sidebar.html
在不同的<li>以及<a>便签添加了
style="color: #fff"
style="background: #353644;"

效果如下:

六、page header 修改

/themes/bruce/templates/base.html文件中包括了html
horizon/common/_page_header.html:

.....{% block page_header %}{% include "horizon/common/_page_header.html" with title=page_title %}{% endblock %}......

这个文件主要用来定义右侧页面用来显示当前plan名字位置区域,开打该文件:
/themes/bruce/templates/horizon/common/_page_header.html

{% load i18n %}
{% block page_header %}<div class='page-header'>{% if actions %}<form class='actions_column pull-right' action='{{ url }}' method="POST">{% csrf_token %}{{ actions }}</form>{% endif %}</div>
{% endblock %}

修改为:

{% load i18n %}
{% block page_header %}<div class='page-header'><div class="alert" style="background-color:#bce8f1;border-color: #bce8f1;color: #31708f;" role="alert">BruceCloud 为您提供一种随时获取的、弹性的计算能力,这种计算能力的体现就是 <em>主机(Instance)</em>。主机就是一台配置好了的服务器,它有您期望的硬件配置、操作系统和网络配置。通常情况下,您的请求可以在10秒到60秒的时间之内 完成,所以您完全可以动态地、按需使用计算能力。</div>{% if actions %}<form class='actions_column pull-right' action='{{ url }}' method="POST">{% csrf_token %}{{ actions }}</form>{% endif %}</div>
{% endblock %}

效果:

六、footer 修改

/themes/bruce/templates/base.html文件中包括了html
_footer.html :

{% block footer %}{% include "_footer.html" %}
{% endblock %}

该文件默认放在horizon.dev/openstack_dashboard/templates/_footer.html目录下,老方法,拷贝一份到/themes/bruce/templates/目录下,打开:

{% comment %}A simple placeholder template for custom global footer content
{% endcomment %}

默认控制为不显示出来,不细究,反正比较简单。

六、messages 修改

/themes/bruce/templates/base.html文件中包括了html
horizon/_messages.html:

......
<div id='main_content' style="margin-top: -90px;">{% include "horizon/_messages.html" %}
......

打开:/themes/bruce/templates/horizon/_messages.html:

{% for message in messages %}{% if "info" in message.tags %}<div class="alert alert-info alert-dismissable fade in"><a class="close" data-dismiss="alert" href="#"><span class="fa fa-times"></span></a><p><strong>{% trans "Info: " %}</strong>{{ message }}</p></div>{% endif %}
......

文件中定义不同消息类型显示的样式。需要修改的直接改这个文件就可以了。

七、project/instances修改

原理很简单,在每个模块下面都有一个templates,当然不直接修改这个目录下面的模版文件,我们把整个模版目录拷贝到我们的主题模版目录下面来进行修改,例如:
建立如下目录结构:
horizon.dev/openstack_dashboard/themes/bruce/templates/project
拷贝:
cp -r ../../../../dashboards/project/instances/templates/instances/ ./
好了,现在我们开始修改我们主机模版目录下面的instances模版样式文件:
修改云主机下拉列表页面,打开目录:themes/bruce/templates/project/instances/index.html

{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Instances" %}{% endblock %}{% block main %}<div class="jumbotron"><h1>Hello, world!</h1><p>...</p><p><a class="btn btn-primary btn-lg" href="#" role="button">Learn more</a></p>
</div>{{ table.render }}
{% endblock %}

效果:

修改table样式:
类似table、modal、form样式的定义不在instances这个目录下面,这个目录下定义的都是instances所特有的样式结构;table、modal、form这些样式的定义往往是全局的,基本都是放在themes/bruce/templates/horizon/common目录下面,
例如:
themes/bruce/templates/horizon/common/_data_xxx.html用来定义table的样式;
themes/bruce/templates/horizon/common/_form_xxx.html定义form表单;
themes/bruce/templates/horizon/common/_modal_xxx.html定义modal表单;定义弹出框;
……
其他模块的修改都是类似的,例如概况页面、云硬盘等,拷贝到我们的主题目录下面进行修改。

主题切换回default:

对原来主题没有任何影响,不得不佩服OpenStack Horizon代码架构越来越好了,

好了OpenStack Horizon的主题开发就讲到这里,基本上都说了一些,时间仓促,有错误望指出,谢谢!后续将推出更多这样的文章!

OpenStack Mitaka Horizon 主题开发相关推荐

  1. OpenStack Ocata Horizon 开发(一)—— 快速开始

    一.写在前面 这篇文章主要介绍了OpenStack Ocata Horizon 根据官方的快速开始文档进行实际的调试验证,涉及在基于基本组件部署完成的情况下,源码部署Horizon开发环境,在后续的文 ...

  2. OpenStack Mitaka for Ubuntu 16.04 LTS 部署指南

    [声明] 欢迎转载,转载本文请注明作者和出处 https://www.zybuluo.com/ncepuwanghui/note/389373 http://blog.csdn.net/ncepuwa ...

  3. CENTOS7.2使用RDO方式安装OpenStack Mitaka笔记

    CENTOS7.2使用RDO方式安装OpenStack Mitaka笔记 1.配置/etc/hosts 192.168.13.108 openstack 2.配置OpenStack Mitaka安装源 ...

  4. wordpress模版post.php,WordPress主题开发手册

    WordPress使用多个模板文件来显示[文章]这个文章类型. 处理博客或其文章的任何内容都属于[文章]文章类型. Index.php 如果没有其他模板文件,index.php将显示[文章]文章类型. ...

  5. liferay开发文档_Liferay –简单主题开发

    liferay开发文档 实际上,Liferay的6.1版本已经走了很长一段路,该版本完全支持JSF和IceFaces. 我一直在努力学习它的绳索,因为我希望使其成为我们团队中的标准协作工具. 好的软件 ...

  6. Liferay –简单主题开发

    实际上,Liferay的6.1版本已经走了很长一段路,该版本完全支持JSF和IceFaces. 我的目标是使它成为我们团队中的标准协作工具,因此我仍在尝试学习它的精髓. 好的软件应用程序可以解决问题, ...

  7. wordpress 主题开发

    https://www.cnblogs.com/welhzh/p/6937243.html wordpress 主题开发 https://yusi123.com/3205.html https://t ...

  8. drupal主题开发_Drupal开发人员,关于如何使您的网站更易于访问

    drupal主题开发 对于OpenConcept Consulting Inc.的创始人兼总裁开放源代码开发人员Mike Gifford ,在他的名字后面提到Drupal可访问性是多余的. 他花了十年 ...

  9. 2020年wordpress主题开发视频教程、WP主题WP模板开发视频教程

    这个<2020年wordpress主题开发视频教程>是由码不停蹄官网录制的wordpress建站系列教程之一.本套视频教程非常适合wordpress新手用来学习开发wordpress主题模 ...

最新文章

  1. 新手學python之新體驗
  2. 分类模型的评估方法-召回率(Recall)
  3. Caffe学习(十)protobuf及caffe.proto解析
  4. 如何评审功能测试用例?
  5. Coding and Paper Letter(四十)
  6. 制作全功能系统维护U盘 PE Linux BT3 DOS
  7. (五)cobbler自定义系统安装
  8. java 按分割为数组中_[Java教程]JS中,split()用法(将字符串按指定符号分割成数组)...
  9. js获取当前html路径,JavaScript获取当前url根目录(路径)
  10. 解决weka打开不了package manner的方法!
  11. 从零开始学黑苹果-基础安装教程(10.11.6)
  12. 齐博x1用户登录接口
  13. Python代码编译,py文件编译为pyc文件
  14. 读书笔记:Java 编程的逻辑(四)
  15. 有人说“心动不是真正的爱情,心定才是真正的爱情”,你怎么看?
  16. MPEG-4 AVC/H.264 视频编码资源列表(中文)
  17. 商品分类管理模块基本完成
  18. 4412第一部分 开发板入门
  19. 【Flutter 问题系列第 40 篇】如何给 Container 四周设置圆角以及给某一角设置圆角
  20. 游戏开发中的物理之布娃娃系统

热门文章

  1. 二十四、红孩儿亲爹的考证
  2. 关于策略优化的一些简单想法
  3. 中国就业市场“稳”字当头 新兴技能岗位大幅增加
  4. 微信开发 新浪SAE开发平台 验证Token 一直失败
  5. PixiJS 文字模糊处理策略
  6. 电商商品列表应以SPU还是SKU展示商品?
  7. 那些你可能不知道的 PDF 工具
  8. PF_INET 和 AF_INET的区别
  9. nuc977 led 新唐自带gpio linux驱动
  10. 康宁玻璃ct值计算公式_CT值的计算公式?