如果有人读这篇文章并跟着做的话,希望你能使用支持函数跳转的编辑器,还要善用var_dump和exit,对着源码去调试着看。跟着入口文件读,执行到哪里你看到哪里,对于那些不能一眼看出来的配置,则要记录下来,可能一个比较简单的功能会写出很长的代码,这个时候难免会看到后面忘了前面。
那么进入正题,从index.php文件可以看到入口文件只定义了几项常量作为配置,紧接着就引入了require './ThinkPHP/ThinkPHP.php';
// 检测PHP环境
if(version_compare(PHP_VERSION,'5.3.0','<')) die('require PHP > 5.3.0 !'); // 开启调试模式 建议开发阶段开启 部署阶段注释或者设为false
define('APP_DEBUG',True); // 定义应用目录
define('APP_PATH','./Application/'); // 引入ThinkPHP入口文件
require './ThinkPHP/ThinkPHP.php';
在ThinkpPHP文件依然比较简单,tp定义了一些常量配置项(defined函数的写法让之前在index入口文件里定义的配置项不会被重置)记录了运行时间和内存使用信息,进行了php版本的判断,以及cli命令行模式的判断。并在文件末尾再次引入了Think核心类,并进行了初始化。
require CORE_PATH.'Think'.EXT;路径为ThinkPHP\Library\Think\Think.class.php
这个think类就比较长了,一开始就定义了$_map , $_instance两个数组,其中$_map作为映射数组使用,think类在会把核心模块的路劲存在这个数组里。$_instance则存储了系统运行时所实例化的对象
// 类映射
private static $_map = array(); // 实例化对象
private static $_instance = array();
刚刚的入口文件执行了start方法来运行系统。这个start方法则一开始就通过spl_autoload_register方法注册了自动加载函数。(php本身有一些魔术方法,在执行某些方法,或变量时,如果它找不到这个方法或变量,就会执行相应的魔术方法,tp便是用自己的自动引入方法替换了该方法,达到不需要引入,直接new对象,系统便会自动引入该类文件的目的)
static public function start() {
// 注册AUTOLOAD方法
spl_autoload_register('Think\Think::autoload');
// 设定错误和异常处理
register_shutdown_function('Think\Think::fatalError');
set_error_handler('Think\Think::appError');
set_exception_handler('Think\Think::appException'); // 初始化文件存储方式
Storage::connect(STORAGE_TYPE);
················
我们往下翻看,autoload方法传入了一个$class类名,然后便在类的$_map里检测是否存在该类的映射,如果有,则表明是系统核心模块直接通过存储的地址include引入;反之则会判断$class是否为带有命名空间的路径字符串,然后通过strstr函数分割字符串获得命名空间前缀,判断是否为tp系统定义的命名空间,然后便通过之前定义的常量来确定文件路径。同时判断是否为win环境,进行大小写的匹配,然后include引入;
public static function autoload($class) {
// 检查是否存在映射
if(isset(self::$_map[$class])) {
include self::$_map[$class];
}elseif(false !== strpos($class,'\\')){
$name = strstr($class, '\\', true);
if(in_array($name,array('Think','Org','Behavior','Com','Vendor')) || is_dir(LIB_PATH.$name)){
// Library目录下面的命名空间自动定位
$path = LIB_PATH;
}else{
// 检测自定义命名空间 否则就以模块为命名空间
$namespace = C('AUTOLOAD_NAMESPACE');
$path = isset($namespace[$name])? dirname($namespace[$name]).'/' : APP_PATH;
}
$filename = $path . str_replace('\\', '/', $class) . EXT;
if(is_file($filename)) {
// Win环境下面严格区分大小写
if (IS_WIN && false === strpos(str_replace('/', '\\', realpath($filename)), $class . EXT)){
return ;
}
include $filename;
}
}elseif (!C('APP_USE_NAMESPACE')) {
// 自动加载的类库层
foreach(explode(',',C('APP_AUTOLOAD_LAYER')) as $layer){
if(substr($class,-strlen($layer))==$layer){
if(require_cache(MODULE_PATH.$layer.'/'.$class.EXT)) {
return ;
}
}
}
// 根据自动加载路径设置进行尝试搜索
foreach (explode(',',C('APP_AUTOLOAD_PATH')) as $path){
if(import($path.'.'.$class))
// 如果加载类成功则返回
return ;
}
}
}
autoload
通过APP_USE_NAMESPACE判断如果配置项里未使用命名空间,那么通过APP_AUTOLOAD_LAYER配置项循环判断为控制器或模型,调用公共方法判断路径并require。若引入失败则再按照配置文件中的APP_AUTOLOAD_PATH路径寻找引入;
注册了autoload方法后,tp系统又注册了错误及异常处理方法,接管了报错时的信息提示功能(这些方法与autoload差不多,有兴趣的朋友自己研究,在这里就不一一赘述了)。
然后它通过Storage::connect(STORAGE_TYPE);类初始化了自己的文件存储类。ThinkPHP\Library\Think\Storage.class.php该类简单的定义了一个操作句柄,一个初始化方法,将各种不同方式的操作对象赋予本类,(通过传入不同参数,可以确定为SAE环境不同类型的存储操作,默认为file文件操作类)
namespace Think;
// 分布式文件存储类
class Storage { /**
* 操作句柄
* @var string
* @access protected
*/
static protected $handler ; /**
* 连接分布式文件系统
* @access public
* @param string $type 文件类型
* @param array $options 配置数组
* @return void
*/
static public function connect($type='File',$options=array()) {
$class = 'Think\\Storage\\Driver\\'.ucwords($type);
self::$handler = new $class($options);
} static public function __callstatic($method,$args){
//调用缓存驱动的方法
if(method_exists(self::$handler, $method)){
return call_user_func_array(array(self::$handler,$method), $args);
}
}
}
file文件位于ThinkPHP\Library\Think\Storage\Driver\File.class.php(tp通过不同的driver驱动层,来适应不同环境,不同类型的动态操作)这个文件也是写的相当简单,类方法里定义了写入,删除,加载,读取,等基本操作。
回到start方法,Tp通过APP_DEBUG配置来判断是否有runtime缓存文件,通过Storage::has方法来读取,或unlink方法删除该缓存文件。
$runtimefile = RUNTIME_PATH.APP_MODE.'~runtime.php';
if(!APP_DEBUG && Storage::has($runtimefile)){
Storage::load($runtimefile);
}else{
if(Storage::has($runtimefile))
Storage::unlink($runtimefile);
然后它读取了应用模式:
$content = '';
// 读取应用模式
$mode = include is_file(CONF_PATH.'core.php')?CONF_PATH.'core.php':MODE_PATH.APP_MODE.'.php';
// 加载核心文件
foreach ($mode['core'] as $file){
if(is_file($file)) {
include $file;
if(!APP_DEBUG) $content .= compile($file);
}
}
is_file(CONF_PATH.'core.php')? //判断是否有隐含应用模式文件
CONF_PATH.'core.php':MODE_PATH.APP_MODE.'.php';
//yes,读取Application/Common/Conf/core.php ; 否,读取/ThinkPHP/Mode/common.php
大家打开ThinkPHP/Common/functions.php文件,这个文件里通过数组定义了配置文件和行文扩展,其中core里面存储了tp的核心文件。(大家在查看源码的时候,可以多用var_dump和exit这两个函数来查看一些变量常量的值)这里放一下这个配置文件的路径
//载入配置列表
Array
(
[config] => Array
(
[0] => D:\wamp\www\tp\ThinkPHP/Conf/convention.php
[1] => ./Application/Common/Conf/config.php
)
//增加为映射
[alias] => Array
(
[Think\Log] => D:\wamp\www\tp\ThinkPHP\Library/Think/Log.class.php
[Think\Log\Driver\File] => D:\wamp\www\tp\ThinkPHP\Library/Think/Log/Driver/File.class.php
[Think\Exception] => D:\wamp\www\tp\ThinkPHP\Library/Think/Exception.class.php
[Think\Model] => D:\wamp\www\tp\ThinkPHP\Library/Think/Model.class.php
[Think\Db] => D:\wamp\www\tp\ThinkPHP\Library/Think/Db.class.php
[Think\Template] => D:\wamp\www\tp\ThinkPHP\Library/Think/Template.class.php
[Think\Cache] => D:\wamp\www\tp\ThinkPHP\Library/Think/Cache.class.php
[Think\Cache\Driver\File] => D:\wamp\www\tp\ThinkPHP\Library/Think/Cache/Driver/File.class.php
[Think\Storage] => D:\wamp\www\tp\ThinkPHP\Library/Think/Storage.class.php
) //加载核心类
[core] => Array
(
[0] => D:\wamp\www\tp\ThinkPHP/Common/functions.php
[1] => ./Application/Common/Common/function.php
[2] => D:\wamp\www\tp\ThinkPHP\Library/Think/Hook.class.php
[3] => D:\wamp\www\tp\ThinkPHP\Library/Think/App.class.php
[4] => D:\wamp\www\tp\ThinkPHP\Library/Think/Dispatcher.class.php
[5] => D:\wamp\www\tp\ThinkPHP\Library/Think/Route.class.php
[6] => D:\wamp\www\tp\ThinkPHP\Library/Think/Controller.class.php
[7] => D:\wamp\www\tp\ThinkPHP\Library/Think/View.class.php
[8] => D:\wamp\www\tp\ThinkPHP\Library/Behavior/BuildLiteBehavior.class.php
[9] => D:\wamp\www\tp\ThinkPHP\Library/Behavior/ParseTemplateBehavior.class.php
[10] => D:\wamp\www\tp\ThinkPHP\Library/Behavior/ContentReplaceBehavior.class.php
)
//加载到Think/Hook->tags里
[tags] => Array
(
[app_init] => Array
(
[0] => Behavior\BuildLiteBehavior
) [app_begin] => Array
(
[0] => Behavior\ReadHtmlCacheBehavior
) [app_end] => Array
(
[0] => Behavior\ShowPageTraceBehavior
) [view_parse] => Array
(
[0] => Behavior\ParseTemplateBehavior
) [template_filter] => Array
(
[0] => Behavior\ContentReplaceBehavior
) [view_filter] => Array
(
[0] => Behavior\WriteHtmlCacheBehavior
) ) )
在include核心文件后,通过C方法加载了应用模式的配置。(C方法位于ThinkPHP\Common\functions.php,tp的单字母函数都是在这里定义的,也是比较简单,通过静态变量来存储配置)
function C($name=null, $value=null,$default=null)这里解释下auto自动变量会随着函数被调用和退出而存在和消失,而static类局部变量不会,它不管其所在的函数是否被调用,都将一直存在;不过,尽管该变量还继续存在,但不能使用它。倘若再次调用定义它的函数时,它又可继续使用,而且保存了前次被调用后留下的值。
加载了配置项,又通过map定义了模式的别名。
然后加载了应用行为定义,这里的行为比较关键,解释一下这个概念。
行为就是钩子函数,有些了解钩子函数的同学可能已经知道这是干嘛的了,这里解释一下,钩子函数就是系统在运行过程中,挂在某一段代码中的方法,在代码执行到钩子方法那里的时候就会执行这个钩子上所绑的函数了,不了解的同学可以理解为方法间的调用,比如我有一个a方法,一个b方法,a方法里显示的写了b();这样来调用b方法,这样虽然能起到调用的目的,但是一旦程序需要改动,要把调用b方法换成调用c方法则需要改动所有写了b();的地方,十分繁琐,还可能出错,于是,如果我们在需要调用b方法的地方,不显示的调用b方法,而是读取一个配置变量,或配置文件,调用配置里定义的方法,那现在这样写就把a方法和b方法的耦合给解开了,以后要改变b方法为c方法d方法的时候我都可以只改动配置文件,是不是很方便呢?
Function a(){
````
B();
`````
} Function b(){
Echo ‘我是钩子函数b’;
}
如果你已经理解了钩子函数,那么ThinkPHP\Library\Think\Hook.class.php这就是tp里的钩子类,用来挂载行为(tp中把钩子函数叫做行为)。打开这个文件来看一下(这里我就不整篇贴代码了):
- 一开始定义了一个$tags变量,用来存储需要执行的方法。
- Add方法添插件行为(就是钩子函数),import方法批量导入,
- get获取插件数组,
- exec方法执行插件,
- listen方法则是线判断了是否为debug模式,如果是debug模式,则通过G方法记录了插件的执行,再调用exec方法,最后通过trace记录了日志。
看完了hook类,再回到think类来,加载应用行为就很好理解了,通过Hook::import将tags.php里配置的钩子数组导入了hook类里(tp里定义钩子函数通过config里新建tags.php,不清楚的朋友自行翻阅手册)。Tp的行为在上面放出的tags配置数组里可以看到。
加载完行为,又加载了语言包、引入debug文件、读取应用状态的配置文件、创建基本目录结构、记录加载文件时间。
// 读取当前应用模式对应的配置文件
if('common' != APP_MODE && is_file(CONF_PATH.'config_'.APP_MODE.CONF_EXT))
C(load_config(CONF_PATH.'config_'.APP_MODE.CONF_EXT)); // 加载模式别名定义
if(isset($mode['alias'])){
self::addMap(is_array($mode['alias'])?$mode['alias']:include $mode['alias']);
} // 加载应用别名定义文件
if(is_file(CONF_PATH.'alias.php'))
self::addMap(include CONF_PATH.'alias.php'); // 加载模式行为定义
if(isset($mode['tags'])) {
Hook::import(is_array($mode['tags'])?$mode['tags']:include $mode['tags']);
} // 加载应用行为定义
if(is_file(CONF_PATH.'tags.php'))
// 允许应用增加开发模式配置定义
Hook::import(include CONF_PATH.'tags.php'); // 加载框架底层语言包
L(include THINK_PATH.'Lang/'.strtolower(C('DEFAULT_LANG')).'.php'); if(!APP_DEBUG){
$content .= "\nnamespace { Think\\Think::addMap(".var_export(self::$_map,true).");";
$content .= "\nL(".var_export(L(),true).");\nC(".var_export(C(),true).');Think\Hook::import('.var_export(Hook::get(),true).');}';
Storage::put($runtimefile,strip_whitespace('<?php '.$content));
}else{
// 调试模式加载系统默认的配置文件
C(include THINK_PATH.'Conf/debug.php');
// 读取应用调试配置文件
if(is_file(CONF_PATH.'debug'.CONF_EXT))
C(include CONF_PATH.'debug'.CONF_EXT);
}
} // 读取当前应用状态对应的配置文件
if(APP_STATUS && is_file(CONF_PATH.APP_STATUS.CONF_EXT))
C(include CONF_PATH.APP_STATUS.CONF_EXT); // 设置系统时区
date_default_timezone_set(C('DEFAULT_TIMEZONE')); // 检查应用目录结构 如果不存在则自动创建
if(C('CHECK_APP_DIR')) {
$module = defined('BIND_MODULE') ? BIND_MODULE : C('DEFAULT_MODULE');
if(!is_dir(APP_PATH.$module) || !is_dir(LOG_PATH)){
// 检测应用目录结构
Build::checkDir($module);
}
} // 记录加载文件时间
G('loadTime');
// 运行应用
App::run();
}
最后app:run()又调用了另一个类,整个配置完成,开始运行了。ThinkPHP\Library\Think\App.class.php
Run方法一开始就通过刚刚说的Hook::listen方法调用了一个行为,然后执行了init方法来初始化系统。
static public function run() {
// 应用初始化标签
Hook::listen('app_init');
App::init();
// 应用开始标签
Hook::listen('app_begin');
// Session初始化
if(!IS_CLI){
session(C('SESSION_OPTIONS'));
}
// 记录应用初始化时间
G('initTime');
App::exec();
// 应用结束标签
Hook::listen('app_end');
return ;
}
Init方法也是一开始就加载了公共配置,配置了日志路径,以及定义了不少常量,都是与服务器接到的请求有关。
static public function init() {
// 加载动态应用公共文件和配置
load_ext_file(COMMON_PATH); // 日志目录转换为绝对路径 默认情况下存储到公共模块下面
C('LOG_PATH', realpath(LOG_PATH).'/Common/'); // 定义当前请求的系统常量
define('NOW_TIME', $_SERVER['REQUEST_TIME']);
define('REQUEST_METHOD',$_SERVER['REQUEST_METHOD']);
define('IS_GET', REQUEST_METHOD =='GET' ? true : false);
define('IS_POST', REQUEST_METHOD =='POST' ? true : false);
define('IS_PUT', REQUEST_METHOD =='PUT' ? true : false);
define('IS_DELETE', REQUEST_METHOD =='DELETE' ? true : false); // URL调度
Dispatcher::dispatch();
``````````````
Dispatcher::dispatch(); 这个方法就是用来解析路由并进行控制器调用的。文件位于ThinkPHP\Library\Think\ Dispatcher.class.php
这个方法一上来就获取了不少配置,接着对get到的参数进行判断是否为兼容模式或cli命令行模式。又对子域名部署进行了判断,分析了pathinfo信息。
static public function dispatch() {
$varPath = C('VAR_PATHINFO');
$varAddon = C('VAR_ADDON');
$varModule = C('VAR_MODULE');
$varController = C('VAR_CONTROLLER');
$varAction = C('VAR_ACTION');
$urlCase = C('URL_CASE_INSENSITIVE');
if(isset($_GET[$varPath])) { // 判断URL里面是否有兼容模式参数
$_SERVER['PATH_INFO'] = $_GET[$varPath];
unset($_GET[$varPath]);
}elseif(IS_CLI){ // CLI模式下 index.php module/controller/action/params/...
$_SERVER['PATH_INFO'] = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : '';
}
// 开启子域名部署
if(C('APP_SUB_DOMAIN_DEPLOY')) {
````````
}
// 分析PATHINFO信息
if(!isset($_SERVER['PATH_INFO'])) {
````````
}
然后开始进入正题了,先通过URL_PATHINFO_DEPR获取url的分隔符,然后是从$_SERVER里获取PATH_INFO用作路由截取,存入__INFO__常量,这里有个判断未绑定模,未开启路由配置,以及路由检测的判断,Route::check()
这个动态路由处理部分我也不是很清楚,不过最后它返回了false,满足了外面的if条件。接下来就通过配置变量对url进行了拆解,建议大家把这些变量配置都打印出来,看一眼便知道了。
$depr = C('URL_PATHINFO_DEPR');
define('MODULE_PATHINFO_DEPR', $depr); if(empty($_SERVER['PATH_INFO'])) {
$_SERVER['PATH_INFO'] = '';
define('__INFO__','');
define('__EXT__','');
}else{
define('__INFO__',trim($_SERVER['PATH_INFO'],'/'));
// URL后缀
define('__EXT__', strtolower(pathinfo($_SERVER['PATH_INFO'],PATHINFO_EXTENSION)));
$_SERVER['PATH_INFO'] = __INFO__;
if(!defined('BIND_MODULE') && (!C('URL_ROUTER_ON') || !Route::check())){
if (__INFO__ && C('MULTI_MODULE')){ // 获取模块名
$paths = explode($depr,__INFO__,2);
$allowList = C('MODULE_ALLOW_LIST'); // 允许的模块列表
$module = preg_replace('/\.' . __EXT__ . '$/i', '',$paths[0]);
if( empty($allowList) || (is_array($allowList) && in_array_case($module, $allowList))){
$_GET[$varModule] = $module;
$_SERVER['PATH_INFO'] = isset($paths[1])?$paths[1]:'';
}
}
}
}
`````````````
真正获取到路由的是define('__SELF__',strip_tags($_SERVER[C('URL_REQUEST_URI')]));这一句,通过系统$_SERVER全局变量中的REQUEST_URI项获取到完整的url路由。
再剩下的都是根据配置常量来对url进行处理,加载相应模块的配置了,费点时间依次打印记录就能很清晰的看出来只是对字符串进行拆分、过滤、拼接。
至于路由传参则是由这一段正则进行匹配的。preg_replace_callback('/(\w+)\/([^\/]+)/', function($match) use(&$var){$var[$match[1]]=strip_tags($match[2]);}, implode('/',$paths));
获取到mca及自定义参数后,url解析完成,调回init方法,开启url钩子行为
回到APP类,init方法初始化完成,开启app_begin钩子行为,记录session配置,调用exec执行方法。
Exec方法也是一开始就又对控制器名进行了过滤,然后正常情况会直接跳到$module = controller(CONTROLLER_NAME,CONTROLLER_PATH);这里创建控制器对象。我们跳到controller函数这里,也是简单的字符拼接后就直接new出了对象,得到对象以后又是跳转到了invokeAction方法这里。
static public function exec() { if(!preg_match('/^[A-Za-z](\/|\w)*$/',CONTROLLER_NAME)){ // 安全检测
$module = false;
}elseif(C('ACTION_BIND_CLASS')){
// 操作绑定到类:模块\Controller\控制器\操作
$layer = C('DEFAULT_C_LAYER');
if(is_dir(MODULE_PATH.$layer.'/'.CONTROLLER_NAME)){
$namespace = MODULE_NAME.'\\'.$layer.'\\'.CONTROLLER_NAME.'\\';
}else{
// 空控制器
$namespace = MODULE_NAME.'\\'.$layer.'\\_empty\\';
}
$actionName = strtolower(ACTION_NAME);
if(class_exists($namespace.$actionName)){
$class = $namespace.$actionName;
}elseif(class_exists($namespace.'_empty')){
// 空操作
$class = $namespace.'_empty';
}else{
E(L('_ERROR_ACTION_').':'.ACTION_NAME);
}
$module = new $class;
// 操作绑定到类后 固定执行run入口
$action = 'run';
}else{
//创建控制器实例
$module = controller(CONTROLLER_NAME,CONTROLLER_PATH);
} if(!$module) {
if('4e5e5d7364f443e28fbf0d3ae744a59a' == CONTROLLER_NAME) {
header("Content-type:image/png");
exit(base64_decode(App::logo()));
} // 是否定义Empty控制器
$module = A('Empty');
if(!$module){
E(L('_CONTROLLER_NOT_EXIST_').':'.CONTROLLER_NAME);
}
} // 获取当前操作名 支持动态路由
if(!isset($action)){
$action = ACTION_NAME.C('ACTION_SUFFIX');
}
try{
self::invokeAction($module,$action);
} catch (\ReflectionException $e) {
// 方法调用发生异常后 引导到__call方法处理
$method = new \ReflectionMethod($module,'__call');
$method->invokeArgs($module,array($action,''));
}
return ;
}
invokeAction方法在一开始进行安全过滤之后,就利用了ReflectionMethod这个反射类,对我们刚刚的控制器对象进行了检测,有无_before前置操作啊,通过反射类获取方法参数列表,并将获取到的参数赋值并执行,判断后置方法执行之类的。到这里,我们的控制器方法就已经开始执行了。
public static function invokeAction($module,$action){
if(!preg_match('/^[A-Za-z](\w)*$/',$action)){
// 非法操作
throw new \ReflectionException();
}
//执行当前操作
$method = new \ReflectionMethod($module, $action);
if($method->isPublic() && !$method->isStatic()) {
$class = new \ReflectionClass($module);
// 前置操作
if($class->hasMethod('_before_'.$action)) {
$before = $class->getMethod('_before_'.$action);
if($before->isPublic()) {
$before->invoke($module);
}
}
// URL参数绑定检测
if($method->getNumberOfParameters()>0 && C('URL_PARAMS_BIND')){
switch($_SERVER['REQUEST_METHOD']) {
case 'POST':
$vars = array_merge($_GET,$_POST);
break;
case 'PUT':
parse_str(file_get_contents('php://input'), $vars);
break;
default:
$vars = $_GET;
}
$params = $method->getParameters();
$paramsBindType = C('URL_PARAMS_BIND_TYPE');
foreach ($params as $param){
$name = $param->getName();
if( 1 == $paramsBindType && !empty($vars) ){
$args[] = array_shift($vars);
}elseif( 0 == $paramsBindType && isset($vars[$name])){
$args[] = $vars[$name];
}elseif($param->isDefaultValueAvailable()){
$args[] = $param->getDefaultValue();
}else{
E(L('_PARAM_ERROR_').':'.$name);
}
}
// 开启绑定参数过滤机制
if(C('URL_PARAMS_SAFE')){
$filters = C('URL_PARAMS_FILTER')?:C('DEFAULT_FILTER');
if($filters) {
$filters = explode(',',$filters);
foreach($filters as $filter){
$args = array_map_recursive($filter,$args); // 参数过滤
}
}
}
array_walk_recursive($args,'think_filter');
$method->invokeArgs($module,$args);
}else{
$method->invoke($module);
}
// 后置操作
if($class->hasMethod('_after_'.$action)) {
$after = $class->getMethod('_after_'.$action);
if($after->isPublic()) {
$after->invoke($module);
}
}
}else{
// 操作方法不是Public 抛出异常
throw new \ReflectionException();
}
}
好的,到这里。tp框架最复杂最麻烦的前置准备工作已经全部完成了。