## 發布前臺
首先,我們看一下點點的發布界面

基本上他的發布框都是左右結構,左邊主要內容,右邊輔助字段。
主要有3個組件:文本編輯器、標簽、日期選擇器。
### 文本編輯器
經過我的研究是ueditor。

看console,里編輯器按鈕的圖片資源目錄。那么我們也用ueditor。
本人比較喜歡這個編輯器,因為他提供了下圖我看中的三個功能:

當然這需要我們去整合到應用里。
這對于新手有點難度,幸好我找到了一個完美的輪子:[Ueditor-thinkphp](https://github.com/Nintendov/Ueditor-thinkphp)

我就下載了他的資源文件,將編輯器本身放入Public/static/ueditor里。

公共項目配置里放上他的配置。
然后UploadController里寫上他的 ueditor方法:

然后文本編輯器的模板部分,因為不同文章發布的類型要多次使用到編輯器,我就提取成了模板:

base/editor.html
表單名和表單值讀取了模板引入include的的屬性`form_name`和`form_value`。
并且精簡了按鈕做到和點點的一致。
至于編輯器里的上傳,后面“文件上傳”會講。
### 標簽
找了許多標簽庫插件,最后還是發現之前自己OneBlog里的 [jquery.tagsinput](https://github.com/xoxco/jQuery-Tags-Input)
好用一些。
bower安裝,引入資源,表單隱藏提交,js代碼區里
~~~
<div class="form-group">
<textarea name="tags" id="tags" placeholder="添加標簽,用逗號或回車分割" class="form-control hide-data" style="height:90px"></textarea>
</div>
~~~
~~~
//標簽
$('#tags').tagsInput({
'height':'90px',
'width':'182px',
'defaultText': '添加標簽'
});
~~~
最后美化一下,就搞定了:

輸入標簽詞后回車添加上標簽。
### 日期選擇器
點點可以定時發布,發布時間下拉里選擇定時發布:

會顯示一個日期+小時+分鐘的 輸入控件:

由于bootstrap默認沒有日期時間選擇器,我就去找了一個 [bootstrap-datetimepicker](http://www.malot.fr/bootstrap-datetimepicker/)
引入資源后,
先是默認隱藏的表單
~~~
<div class="form-group input-group hidden">
<input type="datetime" name="deadline" class="form-control hide-data" id="deadline">
<span class="input-group-addon glyphicon glyphicon-calendar" aria-hidden="true" style="top:0;"></span>
</div>
~~~
然后js代碼:
~~~
//發布時間切換
$('#create_time_choose').on('change', function(){
var choosen = this.value;
if('choose' == choosen)
$('#deadline').parent().removeClass('hidden');
else
$('#deadline').parent().addClass('hidden');
});
//定時使用支持 日期 小時 分鐘選擇
$('#deadline').datetimepicker({
format: 'yyyy-mm-dd hh:ii',
autoclose: true,
language: 'zh-CN'
});
~~~
這樣,實現了和點點差不多的定時發布字段控件:


## 整體代碼
### 前臺模板目錄

前臺主要分為 列表頁、詳情頁、發布頁及用戶個人中心頁。
列表頁主要就是Index目錄下的,base 及其各種子詳情頁。
紅框中的editor是我嘗試之前ueditor的demo寫的demo頁。
然后詳情頁是Detail目錄下的。
發布頁是Post目錄下的base 及其發布類型對應的子模板繼承頁。
### 模板繼承
觀察了Bootstrap的博客案列,整體來說左右結構,發布頁面變的是左邊。
所以我寫了個base.html 供繼承使用:
~~~
<html>
<head>
<meta charset="UTF-8">
<title>Freelog - 自由的輕博客</title>
<script src="__BOWER__/jquery/dist/jquery.min.js"></script>
<script src="__BOWER__/bootstrap/dist/js/bootstrap.min.js"></script>
<link href="__BOWER__/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
<block name="style"></block>
<link rel="stylesheet" href="__CSS__/blog.css">
<script type="text/javascript">
var is_login = '{:is_login()}';
</script>
<script type="text/javascript" src="__JS__/common.js"></script>
<script type="text/javascript">
// var url = window.location.pathname + window.location.search;
// url = url.replace(/(\/(p)\/\d+)|(&p=\d+)|(\/(id)\/\d+)|(&id=\d+)|(\/(group)\/\d+)|(&group=\d+)/, "");
var url = '__SELF__';
var no_pic = '__PUBLIC__/images/no-cover.png';
(function(){
var ThinkPHP = window.Think = {
"ROOT" : "__ROOT__", //當前網站地址
"APP" : "__APP__", //當前項目地址
"PUBLIC" : "__PUBLIC__", //項目公共目錄地址
"DEEP" : "{:C('URL_PATHINFO_DEPR')}", //PATHINFO分割符
"MODEL" : ["{:C('URL_MODEL')}", "{:C('URL_CASE_INSENSITIVE')}", "{:C('URL_HTML_SUFFIX')}"],
"VAR" : ["{:C('VAR_MODULE')}", "{:C('VAR_CONTROLLER')}", "{:C('VAR_ACTION')}"]
}
})();
$(function(){
//單頁高亮
$('.navbar-nav li').removeClass('active');
$('a.blog-nav-item[href="'+url+'"]').parent().addClass('active');
})
</script>
<link rel="stylesheet" href="__BOWER__/smalot-bootstrap-datetimepicker/css/bootstrap-datetimepicker.min.css">
<script type="text/javascript" src="__BOWER__/smalot-bootstrap-datetimepicker/js/bootstrap-datetimepicker.min.js"></script>
<script type="text/javascript" src="__BOWER__/smalot-bootstrap-datetimepicker/js/locales/bootstrap-datetimepicker.zh-CN.js"></script>
<link rel="stylesheet" href="__BOWER__/jqnotifybar/css/jquery.notifyBar.css">
<script type="text/javascript" src="__BOWER__/jqnotifybar/jquery.notifyBar.js"></script>
<script src="__BOWER__/jquery.tagsinput/jquery.tagsinput.js"></script>
<link rel="stylesheet" href="__BOWER__/jquery.tagsinput/jquery.tagsinput.css" />
<script type="text/javascript" src="__STATIC__/ueditor/ueditor.config.js"></script>
<!-- 編輯器源碼文件 -->
<script type="text/javascript" src="__STATIC__/ueditor/ueditor.all.js"></script>
<!-- 實例化編輯器 -->
<!-- Your site ends -->
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top blog-masthead">
<div class="container">
<div class="navbar-header">
<button class="navbar-toggle collapsed" type="button" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="javascript:;">Freelog</a>
</div>
<div class="blog-nav collapse navbar-collapse" id="example-navbar-collapse">
<ul class="nav navbar-nav">
<li><a class="blog-nav-item" href="{:U('/')}">首頁</a></li>
<?php if (is_login()): ?>
<li><a class="blog-nav-item" href="{:U('/mine/')}">我的文章</a></li>
<?php endif ?>
<li>
<form class="navbar-form navbar-right" id="search" method="GET" action="/Search" role="search">
<div class="form-group input-group">
<input type="text" name="kw" class="form-control" placeholder="輸入關鍵字搜索" value="{$kw|default=""}">
<span class="input-group-btn">
<button class="btn btn-default">
<span class="glyphicon glyphicon-search f20" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="javascript:;" role="button" aria-expanded="false"><if condition="is_login()">{:session('user.nickname')}</if></a></li>
<if condition="is_login()">
<li><a class="blog-nav-item" href="{:U('/user/profile')}">個人信息</a></li>
<li><a class="blog-nav-item" href="{:U('/user/logout')}" class="ajax-get">退出</a></li>
<else/>
<li><a class="blog-nav-item" href="{:U('/user/login')}">登錄</a></li>
<li><a class="blog-nav-item" href="{:U('/user/reg')}">注冊</a></li>
</if>
</ul>
</div>
</div>
</div>
<div class="container" id="main">
<block name="header"></block>
<div class="row">
<form class="post">
<div class="col-sm-8 blog-main">
<block name="main"></block>
</div><!-- /.blog-main -->
<div class="col-sm-3 col-sm-offset-1 blog-sidebar">
<block name="sidebar">
<include file="Index/post_btn" />
<div class="sidebar-module sidebar-module-inset">
<h4>選項</h4>
<div class="form-group">
<label for="title">發布時間</label>
<select name="create_time_choose" class="form-control hide-data" id="create_time_choose">
<option value="now">現在發布</option>
<option value="choose">定時發布</option>
</select>
</div>
<div class="form-group input-group hidden">
<input type="datetime" name="deadline" class="form-control hide-data" id="deadline">
<span class="input-group-addon glyphicon glyphicon-calendar" aria-hidden="true" style="top:0;"></span>
</div>
<div class="form-group">
<textarea name="tags" id="tags" placeholder="添加標簽,用逗號或回車分割" class="form-control hide-data" style="height:90px"></textarea>
</div>
</div>
<script type="text/javascript">
$(function(){
//發布時間切換
$('#create_time_choose').on('change', function(){
var choosen = this.value;
if('choose' == choosen)
$('#deadline').parent().removeClass('hidden');
else
$('#deadline').parent().addClass('hidden');
});
//定時使用支持 日期 小時 分鐘選擇
$('#deadline').datetimepicker({
format: 'yyyy-mm-dd hh:ii',
autoclose: true,
language: 'zh-CN'
});
//標簽
$('#tags').tagsInput({
'height':'90px',
'width':'182px',
'defaultText': '添加標簽'
});
})
</script>
</block>
</div><!-- /.blog-sidebar -->
</form>
</div><!-- /.row -->
</div><!-- /.container -->
<footer class="blog-footer">
<p>Blog template built for <a href="http://getbootstrap.com">Bootstrap</a> by <a href="https://twitter.com/mdo">@mdo</a>.Modified by <a href="http://weibo.com/u/1342658313">@黑白世界4648</a></p>
<p>? {:date('Y', time())} Freelog. 由 Thinkphp 強力驅動.</p>
<p>
<a href="#">Back to top</a>
</p>
</footer>
<block name="script"></block>
</body>
</html>
~~~
### 模板包含
由于模板繼承里的每個block只是結構上的替換,一些組件級的html代碼和js。還是封裝成小html片段模板,用include 標簽復用才好。
像文字類發布頁、圖片發布頁和其他發布頁,文本編輯器都有,只不過文字類發布頁的內容字段,就和其他類型發布頁的描述字段一樣是文本編輯器,只不過字段名變了。
在text.html 中:
`<include file="Post/editor" form_name="content" form_value="" height="286" />`
一句話就完成了文本編輯器的引入。父模板里引入編輯器資源,因為都要用到,封裝js 部分就好了。用include屬性傳參。是不是和最近熱門概念web component 類似,TP早就有了。
### 整體代碼
文字頁發布Post/text.html的代碼如下:
~~~
<extend name="Post/base" />
<block name="header">
<div class="blog-header">
<h1 class="blog-title">發布文字</h1>
</div>
</block>
<block name="main">
<div class="blog-post" id="text">
<div class="form-group">
<label for="title">標題 <span class="optional">(可不填)</span></label>
<input type="text" class="form-control hide-data" id="title" name="title">
</div>
<div class="form-group">
<label for="content">內容</label>
<include file="Post/editor" form_name="content" form_value="" height="286" />
</div>
<div class="form-group">
<input type="hidden" name="type" value="{$type}" class="hide-data">
<input type="hidden" name="member_id" value="{:is_login()}" class="hide-data">
<a class="btn btn-default pull-left" href="{:U('/')}">取消</a>
<button type="button" class="btn btn-primary pull-right ajax-post no-refresh" hide-data="true" target-form="post" href="/api.php/post">保存</button>
</div>
</div><!-- /.blog-post -->
<script type="text/javascript">
$(function(){
})
</script>
</block>
<block name="script">
</block>
~~~
這時候,整個文字類發布文章頁面的效果就出來了:

怎么樣和點點很像吧。
### 等等,還有ajax
在頁面的父模板里,我們只看到一個form標簽,沒有action,也沒有type,卻你那提交數據,表單怎么提交的呢?
別急,我們看看Public里的common.js里有這么一段:
~~~
//ajax post submit請求
$('.ajax-post').on('click', function(){
var target,query,form;
var target_form = $(this).attr('target-form');
var that = this;
var nead_confirm = false;
if( ($(this).attr('type')=='submit') || (target = $(this).attr('href')) || (target = $(this).attr('url')) ){
form = $('.'+target_form);
if(form.lenght < 1 )
alert('表單選擇不正確');
if ($(this).attr('hide-data') === 'true'){//無數據時也可以使用的功能
form = $('.hide-data');
query = form.serialize();
}else if (form.get(0)==undefined){
return false;
}else if ( form.get(0).nodeName=='FORM' ){
if ( $(this).hasClass('confirm') ) {
if(!confirm('確認要執行該操作嗎?')){
return false;
}
}
if($(this).attr('url') !== undefined){
target = $(this).attr('url');
}else{
target = form.get(0).action;
}
query = form.serialize();
}else if( form.get(0).nodeName=='INPUT' || form.get(0).nodeName=='SELECT' || form.get(0).nodeName=='TEXTAREA') {
form.each(function(k,v){
if(v.type=='checkbox' && v.checked==true){
nead_confirm = true;
}
})
if ( nead_confirm && $(this).hasClass('confirm') ) {
if(!confirm('確認要執行該操作嗎?')){
return false;
}
}
query = form.serialize();
}else{
if ( $(this).hasClass('confirm') ) {
if(!confirm('確認要執行該操作嗎?')){
return false;
}
}
query = form.find('input,select,textarea').serialize();
}
$(that).addClass('disabled').attr('autocomplete','off').prop('disabled',true);
$.post(target,query).success(function(data){
if (data.code >= 200 && data.code < 400) {
console.log(data);
console.log('success');
if (data.url) {
notify(data.info + ' 頁面即將自動跳轉~','success');
}else{
notify(data.info ,'success');
}
setTimeout(function(){
$(that).removeClass('disabled').prop('disabled',false);
if(data.url)
location.href = data.url;
},1500);
}else{
notify(data.info, 'error');
setTimeout(function(){
$(that).removeClass('disabled').prop('disabled',false);
if (data.url) {
location.href = data.url;
}
}, 1500);
}
}).error(function(jqXHR, textStatus, errorThrown) {
$(that).removeClass('disabled').prop('disabled',false);
var code = jqXHR.status;
if(404 == code ){
notify(data.info ,'error');
}else if(412 == code){
notify('記錄已經被刪除了' ,'error');
}
});
}
return false;
});
~~~
再看 提交按鈕的代碼:
`<button type="button" class="btn btn-primary pull-right ajax-post no-refresh" hide-data="true" target-form="post" href="/api.php/post">保存</button>`
有這ajax-post類名和 target-form屬性。而父模板里有一個post類名的form標簽。
原來用了ajax提交序列化form了。地址是href屬性。
后面代碼了也些了ajax-put和ajax-delete。
這段代碼是我從OneThink里借過來的,修改了部分,添加了自己的提示信息代碼,以及響應碼判斷,以前的都是200。
成功以后的顯示是這樣的:

## 模型
api部分只是提供了新增數據,具體文章數據的添加有什么處理,還得看模型代碼。
我們看Common下的PostModel:
~~~
<?php
namespace Common\Model;
Use Think\Model;
class PostModel extends Model{
protected $_auto = array (
array('status','1'), // 新增的時候把status字段設置為1
array('create_at','datetime', self::MODEL_INSERT, 'function'), // 對create_at字段在更新的時候寫入當前時間戳
array('deadline','getDeadline', self::MODEL_INSERT, 'callback'), // 對name字段在新增的時候回調getName方法
array('update_at','datetime', self::MODEL_BOTH, 'function'), // 對update_at字段在更新的時候寫入當前時間戳
array('content', 'encode', self::MODEL_BOTH, 'callback'), //對內容字段 根據type選擇進行json_encode
array('view', 0, self::MODEL_INSERT),
array('comment', 0, self::MODEL_INSERT),
);
protected $_validate = array(
);
//截止日期處理
public function getDeadline($deadline){
if('now' == I('request.create_time_choose')){
return datetime('now');
}else{
return datetime($deadline);
}
}
//內容字段加密
public function encode($content){
if(in_array(I('post.type'), array('picture', 'music', 'video')))
return json_encode($content);
else
return $content;
}
protected function _after_find(&$result, $options) {
if(in_array($result['type'], array('picture', 'music', 'video'))){
$result['content'] = json_decode($result['content'], 1);
}
}
protected function _after_select(&$result, $options){
foreach($result as &$record){
$this->_after_find($record, $options);
}
if($result){
$member_ids = array_column($result, 'member_id');
$authors = M('Member')->where(array('id'=>array('in', $member_ids)))->getField('id,nickname');
foreach ($result as &$record) {
if(isset($authors[$record['member_id']]))
$record['author'] = $authors[$record['member_id']];
else
$record['author'] = '系統發布';
}
}
}
// 新增數據前的回調方法
protected function _after_insert($data, $options) {
$this->_after_update($data, $options);
}
// 更新成功后的回調方法
protected function _after_update($data, $options) {
//處理標簽
$tags = $data['tags'];
$tags = explode(',', $tags);
if($tags === FALSE){
$tags = array();
}
$tagsModel = M('Tags');
if(!empty($_POST['id'])){
//更新時候先將原有標簽統計減去1
$orignal_tags = $this->getFieldById('tags', $data['id']);
if($orignal_tags)
$tagsModel->where(array('title'=>array('in', $orignal_tags)))->setDec('count');
}
foreach ($tags as $value) {
if($tagsModel->where("title = '{$value}'")->find())
$tagsModel->where("title = '{$value}'")->setInc('count');
else
if($value)
$tagsModel->add(array('title'=>$value, 'count'=>1));
}
}
}
~~~
模型做了幾件事:
1.完成字段
status 插入時為1;create_at插入寫入日期時間文本;deadline在為選擇時,是當前時間,選擇了定時時是傳遞過來的文本;更新時間新增和更新時都更新為當前時間文本;content內容,在某些類型里json序列化,那時文本編輯器存的值到description里,content是數組表單;view和comment 查看數和評論數默認為0。
2.后置查詢時反序列化和字段值顯示文本處理
反序列化一看就知道,顯示文本的是用戶名處理。存的member_id 顯示時候顯示昵稱。
~~~
if($result){
$member_ids = array_column($result, 'member_id');
$authors = M('Member')->where(array('id'=>array('in', $member_ids)))->getField('id,nickname');
foreach ($result as &$record) {
if(isset($authors[$record['member_id']]))
$record['author'] = $authors[$record['member_id']];
else
$record['author'] = '系統發布';
}
}
~~~
這里用了array_column獲取查出來的`member_id` 數組集合,然后查詢該集合贏的用戶數據,并以`member_id`為鍵名。然后拼接作者字段顯示發布者昵稱。
3.后置更新時處理標簽表的統計
~~~
// 更新成功后的回調方法
protected function _after_update($data, $options) {
//處理標簽
$tags = $data['tags'];
$tags = explode(',', $tags);
if($tags === FALSE){
$tags = array();
}
$tagsModel = M('Tags');
if(!empty($_POST['id'])){
//更新時候先將原有標簽統計減去1
$orignal_tags = $this->getFieldById('tags', $data['id']);
if($orignal_tags)
$tagsModel->where(array('title'=>array('in', $orignal_tags)))->setDec('count');
}
foreach ($tags as $value) {
if($tagsModel->where("title = '{$value}'")->find())
$tagsModel->where("title = '{$value}'")->setInc('count');
else
if($value)
$tagsModel->add(array('title'=>$value, 'count'=>1));
}
}
~~~
標簽的處理思路是,先將原有文章的所有標簽數量-1,然后新增或者更新時的標簽遍歷時再去更新統計數,或者新增標簽。這樣統計不會亂。
## 其他格式
其他格式,之前說了,`'picture', 'music', 'video'`這三個類型 存description。content內容字段是數組序列化json的。
圖片和音樂都可能多數據,視頻只能一種。
### 圖片


圖片的content是個數組,每個元素里存圖片的path和alt。
圖片上傳,不說了就是pupload。后js動態添加圖片數組。
### 音樂



音樂比圖片多了一個id,alt換為title了。
音樂支持本地上傳mp3和百度搜索。
百度音樂搜索,其實就是參考了ueditor里的上傳,只不過自己用bootstrap-table拼接了搜索結果,然后選擇后,js處理了返回一個多維數組的格式。其他沒啥。
### 視頻
視頻支持本地上傳mp4和在線視頻解析。
在線解析用的是云邊輕博客商業版2.0里的類。
然后經過優化了,支持優酷/土豆/酷六/56/搜狐/新浪視頻網站的播放頁面地址解析插入(注意不是FLASH地址)。


mp4本地視頻上傳

#### 在線視頻的地址獲取
我們都知道傳統視頻播放要一個flash播放器,然后播放的一般都是flv文件。
后來html5標準出來后 mp4一般成為了標準格式,因為ios平臺這個格式支持的最好。
我們看一下要分析的視頻
<http://v.youku.com/v_show/id_XMTI2NzQ3NTY2MA==_ev_10.html?from=y1.3-idx-uhome-1519-20887.205985-205986-205830-205987.5-3>

用chrome看下播放器代碼:

然而這里的地址不是我們想要的是個參數給那個播放器object讀取的。
我們要的地址可以通過分享獲取到,視頻網站為例視頻播放量,一般會提供分享功能。

我們復制flash地址 粘貼在地址欄后訪問

怎么樣?出來獨立的播放器了吧。

而我們在線播放的原理就是在當前網頁里嵌入這么一個embed的標簽,地址是在線視頻的真實地址。
`<embed type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" src="http://player.youku.com/player.php/sid/XOTE1NDg4OTE2/v.swf" width="617" height="480" wmode="transparent" play="true" loop="false" menu="false" allowscriptaccess="never" allowfullscreen="true">`
一般來說php程序就是curl請求平播放頁面,然后想辦法解析出這個地址。
具體程序大家可以看我的那個視頻解析類 urlParse.php。
我只展示部分的代碼,以解析優酷視頻地址為例:
~~~
/**
* 優酷網
* http://v.youku.com/v_show/id_XMjI4MDM4NDc2.html
* http://player.youku.com/player.php/sid/XMjU0NjI2Njg4/v.swf
*/
private function _parseYouku($url) {
preg_match("#id\_(\w+)#", $url, $matches);
if (empty($matches)) {
preg_match("#v_playlist\/#", $url, $mat);
if (!$mat)
return false;
$html = self::_fget($url);
preg_match("#videoId2\s*=\s*\'(\w+)\'#", $html, $matches);
if (!$matches)
return false;
}
$link = "http://v.youku.com/player/getPlayList/VideoIDS/{$matches[1]}/timezone/+08/version/5/source/out?password=&ran=2513&n=3";
$retval = self::_cget($link);
if ($retval) {
$json = json_decode($retval, true);
$data['img'] = $json['data'][0]['logo'];
$data['title'] = $json['data'][0]['title'];
$data['url'] = $url;
$data['id'] = "http://player.youku.com/player.php/sid/{$matches[1]}/v.swf";
$data['type'] = 'youku';
return $data;
} else {
return false;
}
}
~~~
前面的部分是獲取vid,然后通過后門地址 拼接getPlayList那個去獲取視頻元數據,也就是標題和封面等信息,每個視頻網站后門地址不一樣,元數據也不一樣,我們只獲得通用的就好。
具體優酷視頻地址原理參見:[優酷真實視頻地址解析](http://my.oschina.net/u/727843/blog/390872)
- 序
- 前言
- 內容簡介
- 目錄
- 基礎知識
- 起步
- 控制器
- 模型
- 模板
- 命名空間
- 進階知識
- 路由
- 配置
- 緩存
- 權限
- 擴展
- 國際化
- 安全
- 單元測試
- 拿來主義
- 調試方法
- 調試的步驟
- 調試工具
- 顯示trace信息
- 開啟調試和關閉調試的區別
- netbeans+xdebug
- Socketlog
- PHP常見錯誤
- 小黃鴨調試法,每個程序員都要知道的
- 應用場景
- 第三方登錄
- 圖片處理
- 博客
- SAE
- REST實踐
- Cli
- ajax分頁
- barcode條形碼
- excel
- 發郵件
- 漢字轉全拼和首字母,支持帶聲調
- 中文分詞
- 瀏覽器useragent解析
- freelog項目實戰
- 需求分析
- 數據庫設計
- 編碼實踐
- 前端實現
- rest接口
- 文章發布
- 文件上傳
- 視頻播放
- 音樂播放
- 圖片幻燈片展示
- 注冊和登錄
- 個人資料更新
- 第三方登錄的使用
- 后臺
- 微信的開發
- 首頁及個人主頁
- 列表
- 歸檔
- 搜索
- 分頁
- 總結經驗
- 自我提升
- 進行小項目的鍛煉
- 對現有輪子的重構和移植
- 寫技術博客
- 制作視頻教程
- 學習PHP的知識和新特性
- 和同行直接溝通、交流
- 學好英語,走向國際
- 如何參與
- 瀏覽官網和極思維還有看云
- 回答ThinkPHP新手的問題
- 嘗試發現ThinkPHP的bug,告訴官方人員或者push request
- 開發能提高效率的ThinkPHP工具
- 嘗試翻譯官方文檔
- 幫新手入門
- 創造基于ThinkPHP的產品,進行連帶推廣
- 展望未來
- OneThink
- ThinkPHP4
- 附錄