• Zend_Framework_1 框架是如何被启动的?


    Zend Framework 1 是一个十年前的老框架了,我接触它也有两年了,现在来写这篇文章,主要原因是最近要写入职培训教程。公司项目基本上都是基于Zend1框架,即使现在要转 Laravel 也肯定要有好长一段时间的过渡过程,而且基本上是新项目用 Laravel,老项目基本不会再重构了。因此,新人入职的话,还是需要培训一下 Zend Framework 1 的,之前把Zend官方文档的提供的一个入门教程翻译整理了一遍,作为入门教程,但这次又看了一遍之后发现,那篇教程只是教你如何做,是什么,却没有讲关于整个框架的整体的逻辑,所以,这篇文章就是为了解决这个问题的。阅读完本文之后,你将加深理解Zend1框架的启动、运行的完整流程。只有理解了这个完整的流程,才能在使用时或遇到问题时迅速解决问题,找到解决方案。

    PS:我在梳理的时候,才发现其实我本身对于它也是不够了解的,业务上基本上熟悉了常用的东西之后,就不怎么关注框架本身的东西了,所以说这次整理也算是温故而知新,帮助别人的同时,也帮助了自己。

    First of all

    首先,我们从官网上下载Zend1的最新版本的 ZendFramework-1.12.20 源码包,然后解压,其目录结构简化如下:

    ZendFramework-1.12.20
    |-- bin
    |   |-- zf.sh
    |   `-- ...
    |-- library
    |   |-- Zend
    `-- ...
    

    现在我们只需关注两个:zf.sh 文件和 Zend 目录。zf.sh 是 Zend1 提供的一个命令行工具,用于创建 Project、Controller、Model 等类。接下来我将使用它来创建一个示例项目,为了更方便地全局使用该命令,把它链接到系统的环境变量PATH里面,执行命令如下:

    $ ln -s /home/<user>/Downloads/ZendFramework-1.12.20/bin/zh.sh /usr/bin/zf
    

    然后,可以用 zf 命令创建项目了:

    $ zf create-project training
    

    默认创建的项目目录结构如下:

    training
    |-- application
    |   |-- Bootstrap.php
    |   |-- configs
    |   |   `-- application.ini
    |   |-- controllers
    |   |   |-- ErrorController.php
    |   |   `-- IndexController.php
    |   |-- models
    |   `-- views
    |       |-- helpers
    |       `-- scripts
    |           |-- error
    |           |   `-- error.phtml
    |           `-- index
    |               `-- index.phtml
    |-- library
    |-- public
    |   |-- .htaccess
    |   `-- index.php
    `-- tests
        |-- application
        |   `-- bootstrap.php
        |-- library
        |   `-- bootstrap.php
        `-- phpunit.xml
    

    先浏览一下这个项目的目录结构,后面将会逐一分析每个文件和目录的作用。

    index.php

    因为所有的 Web 请求都将被重定位到 index.php上,所以先来看 index.php 的内容,:

    <?php
    
    // Define path to application directory
    defined('APPLICATION_PATH')
        || define('APPLICATION_PATH', realpath(dirname(__FILE__) . '/../application'));
    
    // Define application environment
    defined('APPLICATION_ENV')
        || define('APPLICATION_ENV', (getenv('APPLICATION_ENV') ? getenv('APPLICATION_ENV') : 'production'));
    
    // Ensure library/ is on include_path
    set_include_path(implode(PATH_SEPARATOR, array(
        realpath(APPLICATION_PATH . '/../library'),
        get_include_path(),
    )));
    
    /** Zend_Application */
    require_once 'Zend/Application.php';
    
    // Create application, bootstrap, and run
    $application = new Zend_Application(
        APPLICATION_ENV,
        APPLICATION_PATH . '/configs/application.ini'
    );
    $application->bootstrap()
                ->run();
    

    第4行和第8行定义了两个常量,APPLICATION_PATH - 应用的根路径,APPLICATION_ENV - 应用的运行环境,这两个常量是默认生成的,用于在配置文件(configs/application.ini)中指定相应路径。

    第12行增加了PHP的include_path,默认PHP的include_path是在php.ini中指定的,这里把我们自己的library目录包括了进去,这样PHP在解析类的时候会到这个目录中去找,后面将会详述这个寻找类的过程。

    第18行 require_once 'Zend/Application.php'; 包括了一个Application.php文件。PHP执行到这一步的时候,会去include_path列表里面寻找有没有Zend目录,然后再去Zend目录寻找有没有Application.php。我们继续执行,然后报了一个错误:

    PHP Fatal error:  require_once(): Failed opening required 'Zend/Application.php' (include_path='/home/feiffy/Repo/feiffy/Training/library:.:/usr/share/php')
    

    显然,PHP没有找到这个文件,所以报错了,它去了这三个目录(/home/feiffy/Repo/feiffy/Training/library,.,/usr/share/php)中去找了,都没有找到。这个文件是框架提供的,用于初始化Zend Application,之前我们只是用zf命令创建了基于Zend1的项目,但是没有把Zend1框架本身引入进去,所以报了这个错误。现在可以看到,PHP确实去我们设置的 /home/feiffy/Repo/feiffy/Training/library 去找了,所以可以把 Zend 框架放到这里,这里的 Zend 框架就是之前下载的 ZendFramework1.12.20/library/Zend 目录,将其整体的复制到 /home/feiffy/Repo/feiffy/Training/library 目录中即可。还有一种方式是直接建立软链接(相当于 Windows 中的快捷方式),我偏向于这种,这样减少了文件的复制:

    $ ln -s /home/<user>/Downloads/ZendFramework-1.12.20/library/Zend /home/feiffy/Repo/feiffy/Training/library/Zend
    

    这次PHP就能找到文件了,再说一遍其过程:PHP搜索include_path中的所有路径,发现在 /home/feiffy/Repo/feiffy/Training/library 中是存在 Zend/Application.php 文件的,所以就加载了它。

    再看 Application.php 的内容:

    <?php
    
    class Zend_Application {
     ....
    }
    

    它定义了一个 Zend_Application 类,这个类就是整个 Zend 应用。

    然后看第21行,实例化了一个 Zend_Application 应用,现在主要看传入的第二个参数:application.ini 的内容,现在全部是默认生成的:

    [production]
    ...
    includePaths.library = APPLICATION_PATH "/../library"
    bootstrap.path = APPLICATION_PATH "/Bootstrap.php"
    bootstrap.class = "Bootstrap"
    resources.frontController.controllerDirectory = APPLICATION_PATH "/controllers"
    ...
    

    这里只列出了一些重要的配置,这里的配置将会在后面的 Zend_Application 的初始化中会用到。

    Application.php

    现在我们来看 Zend_Application 的实例化过程。

    <?php
    class Zend_Application
    {
        ...
        
        public function __construct($environment, $options = null, $suppressNotFoundWarnings = null)
        {
            require_once 'Zend/Loader/Autoloader.php';
            $this->_autoloader = Zend_Loader_Autoloader::getInstance();
            ...
            $options = $this->_loadConfig($options);
            $this->setOptions($options);
        }
    }
    

    其实就做了两件事:初始化 _autoloader 属性和 options 属性。

    _autoloader 是 Zend1 框架自己实现的一个类加载器,其类名为 Zend_Loader_Autoloader,稍后,用到它的时候再讲它的加载类的过程,此处就把它当做应用的一个属性就好了。

    然后,从配置文件 application.ini 转换为配置为一个 $options 数组,其内容如下:

    <?php
    $options = array(
        "includePaths" => "/home/feiffy/Repo/feiffy/Training/../library",
        "bootstrap" => array(
            "path" => "/home/feiffy/Repo/feiffy/Training/Bootstrap.php",
            "class" => "Bootstrap",
        ),
        "resources" => array(
            "frontController" => array(
                "controllerDirectory" => "/home/feiffy/Repo/feiffy/Training/controllers",
            ),
            ...
        ),
    );
    

    setOptions()

    最后是 setOptions() 方法。

    setOptions() 方法不仅设置了 _options 属性,还做了其他的初始化操作,主要的就是实例化了 _bootstrap 属性:

    <?php
    ...
    public function setOptions()
    {
        $this->_options = $options; # 设置 _options 属性
    
        $this->setIncludePaths($options['includepaths']); # 设置include_paths,把ini里面的路径加到了原先的include_paths列表里面去
    
        # 初始化 _bootstrap 属性,后面会详述这个 _bootstrap 属性
        $bootstrap = $options['bootstrap'];
        $path  = $bootstrap['path'];
        $class = $bootstrap['class'];
        $this->setBootstrap($path, $class);
    }
    

    Bootstrap.php

    setBootstrap()

    setBootstrap() 设置应用的 _bootstrap 属性。

    <?php
    public function setBootstrap($path, $class)
    {
        ...
        if (null == $class) {
            $class = 'Bootstrap';
        }
        require_once $path;
        $this->_bootstrap = new $class($this);
        ...
    }
    

    这段代码初始化了应用的 _bootstrap 属性,此时 $path 的值为:"/home/feiffy/Repo/feiffy/Training/application/Bootstrap.php",文件内容如下:

    <?php
    
    class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
    {
    
    
    }
    

    然后 $bootstrap = new $class(); 就是让PHP去找 Bootstrap 类,而在上一步中 require 了 Bootstrap.php 文件,所以最终在 /home/feiffy/Repo/feiffy/Training/application/Bootstrap.php 文件中找到了,于是实例化该类。

    默认这个文件的内容是空的,但是它非常重要,应用所有的资源的初始化都要写在这个类里面。实例化 Bootstrap 时此处没有定义 __construct() 方法,所以PHP会去执行父类 Zend_Application_Bootstrap_Bootstrap 的 __construct() 方法。父类定义如下,可见它又继承了一个抽象父类。

    class Zend_Application_Bootstrap_Bootstrap
        extends Zend_Application_Bootstrap_BootstrapAbstract
    {
        parent::__construct($application);
    }
    

    bootstrap的主要功能都是由这个抽象父类提供的:

    abstract class Zend_Application_Bootstrap_BootstrapAbstract
        implements Zend_Application_Bootstrap_Bootstrapper,
                   Zend_Application_Bootstrap_ResourceBootstrapper
    {
        public function __construct($application)
        {
            $this->setApplication($application);
            $options = $application->getOptions();
            $this->setOptions($options);
        }
    }
    

    Loader.php

    但是这里有一个问题:Bootstrap 类所继承的 Zend_Application_Bootstrap_Bootstrap 类是如何找到它所在的类定义的文件的呢?这里并不像实例化Bootstrap类之前require了一个Bootstrap.php文件,到目前为止,所有require的文件中都没有包含Zend_Application_Bootstrap_Bootstrap 类的定义。上文在介绍 Bootstrap 的实例化时直接就跳转到了 Zend_Application_Bootstrap_Bootstrap->__construct() 方法,这中间必定经过了一个很重要的过程。这个过程就是PHP自动加载的过程,还记得之前提到的 Zend_Loader 类吗?在实例化 Zend_Application 类的时候添加了一个 _autoloader 属性。我们再回到上面详细看一下,它是如何被初始化的:

    #1 Zend_Application
        require_once 'Zend/Loader/Autoloader.php';
        $this->_autoloader = Zend_Loader_Autoloader::getInstance();
    
    #2 Zend_Loader_Autoloader
        public static function getInstance()
        {
            if (null === self::$_instance) {
                self::$_instance = new self();
            }
            return self::$_instance;
        }
    
        protected function __construct()
        {
            spl_autoload_register(array(__CLASS__, 'autoload'));
            $this->_internalAutoloader = array($this, '_autoload');
        }
    

    第2行,Autoloader.php 中定义了 Zend_Loader_Autoloader 类,被 require 了,所以第3行能够加载该类并调用一个静态方法 getInstance(),其实就是实例化本身,实例化会自动调用__construct(),所以再去看它的 __construct() 方法。看到一个:spl_autoload_register(),这是什么?这是PHP用来注册自动加载函数的一个方法。这里就把 Zend_Loader_Autoloader->autoload() 方法注册为一个类自动加载器。当遇到需要解析类名的时候,就会自动找到这个类加载器,把类名交给它,然后它通过自己定义的规则,解析出类所在的文件名,加载该文件,然后就能实例化所需要的类了。

    有了类加载器之后,上面在 require Bootstrap.php 文件时,发现 Bootstrap 类继承自 Zend_Application_Bootstrap_Bootstrap 类,然后 PHP 会去解析该类,结果发现现在所有的 require 的文件里面都没有该类的定义,默认的解析规则找不到类文件,所以就交给 Zend_Loader_Autoloader->autoload(),在 Zend_Loader_Autoloader 里面经过一番规则转换:

        public static function autoload($class)
        {
            call_user_func($autoloader, $class) // $autoloader->autoload()
        }
    
        protected function _autoload($class)
        {
            $callback = $this->getDefaultAutoloader();
            call_user_func($callback, $class); // $this->loadClass()
        }
    
        public static function loadClass($class, $dirs = null)
        {
            $file = self::standardiseFile($class);
            ...
            self::loadFile($file, $dirs, true);
        }
    
        public static function standardiseFile($file)
        {
            $fileName = ltrim($file, '\');
            $file      = '';
            $namespace = '';
            if ($lastNsPos = strripos($fileName, '\')) {
                $namespace = substr($fileName, 0, $lastNsPos);
                $fileName = substr($fileName, $lastNsPos + 1);
                $file      = str_replace('\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
            }
            $file .= str_replace('_', DIRECTORY_SEPARATOR, $fileName) . '.php';
            return $file;    
        }
    
        public static function loadFile($filename, $dirs = null, $once = false)
        {
            if ($once) {
                include_once $filename;
            } else {
                include $filename;
            }
        }
    

    经过层层调用,最终在 standardiseFile() 方法中,把 Zend_Application_Bootstrap_Bootstrap 转换为 "Zend/Application/Bootstrap/Bootstrap.php" 路径,然后在 loadFile() 方法中加载了该文件。加载文件是按照路径层级一级一级往下找时,PHP首先去 include_paths 目录列表中去寻找有没有 Zend 目录,结果发现在 /home/feiffy/Repo/feiffy/training/library 中找到了,并且后面的子目录也正确找到了,于是加载了 Bootstrap.php 文件,这个文件中定义了 Zend_Application_Bootstrap_Bootstrap 类。所以 PHP 现在知道了这个类在这个文件里,直接实例化它,并调用了 __construct()。

    Zend1框架这种风格的加载文件的方式是老版PHP代码流行的风格,通过下划线来匹配目录层级,现在已经过时,这里只要了解一下就好了,新版的PHP自动加载风格请参考官方文档:PSR-4。

    index.php

    _bootstrap 属性实例化完成之后,就回到 index.php 中的 $application->bootstrap()->run();。应用的启动是通过 Bootstrap 类的 bootstrap() 方法启动的,调用顺序如下:

    # index.php
    $application->bootstrap()
    
    # Zend_Application->bootstrap()
        public function bootstrap($resource = null)
        {
            $this->getBootstrap()->bootstrap($resource);
            return $this;
        }
    
    # Zend_Application_Bootstrap_BootstrapAbstract->bootstrap()
        final public function bootstrap($resource = null)
        {
            $this->_bootstrap($resource);
            return $this;
        }
    
        protected function _bootstrap($resource = null)
        {
            if (null === $resource) {
                foreach ($this->getClassResourceNames() as $resource) {
                    $this->_executeResource($resource);
                }
    
                foreach ($this->getPluginResourceNames() as $resource) {
                    $this->_executeResource($resource);
                }
            } elseif (is_string($resource)) {
                $this->_executeResource($resource);
            } elseif (is_array($resource)) {
                foreach ($resource as $r) {
                    $this->_executeResource($r);
                }
            } else {
                throw new Zend_Application_Bootstrap_Exception('Invalid argument passed to ' . __METHOD__);
            }
        }
    

    最终在 _bootstrap() 中加载了所有的资源,至此应用的初始化、启动结束。接下来执行 run() 方法获取前端控制器资源,通过前端控制器处理路由、分发请求和输出响应。

    # Zend_Application->run()
        public function run()
        {
            $this->getBootstrap()->run();
        }
    
    # Zend_Application_Bootstrap_Bootstrap->run()
        public function run()
        {
            $front   = $this->getResource('FrontController');
            $default = $front->getDefaultModule();
            if (null === $front->getControllerDirectory($default)) {
                throw new Zend_Application_Bootstrap_Exception(
                    'No default controller directory registered with front controller'
                );
            }
    
            $front->setParam('bootstrap', $this);
            $response = $front->dispatch();
            if ($front->returnResponse()) {
                return $response;
            }
        }
    

    在这一步里面,初始化front前端控制器,由前端控制器负责把请求分发给相应的具体的Controller,返回Controller所产生的响应数据。到这一步之后就是后面,核心的类就是 Controller 类了。

    Front.php

    Front.php 中定义了 Zend_Controller_Front 即前端控制器,用于把请求分发给相应的具体的控制器,并且接收响应,路由功能就由它控制的。它有一个核心方法 dispatch():

    class Zend_Controller_Front
    {
        public function dispatch(Zend_Controller_Request_Abstract $request = null, Zend_Controller_Response_Abstract $response = null)
        {
            require_once 'Zend/Controller/Request/Http.php';
            $request = new Zend_Controller_Request_Http();
            $this->setRequest($request);
            ...
            require_once 'Zend/Controller/Response/Http.php';
            $response = new Zend_Controller_Response_Http();
            $this->setResponse($response);
            ...
            $router = $this->getRouter();
    
            $dispatcher = $this->getDispatcher();
    
            ...
            $dispatcher->dispatch($this->_request, $this->_response);
          
            ...
            $this->_response->sendResponse();
        }
    
        public function getRouter()
        {
            if (null == $this->_router) {
                require_once 'Zend/Controller/Router/Rewrite.php';
                $this->setRouter(new Zend_Controller_Router_Rewrite());
            }
    
            return $this->_router;
        }
    }
    

    在初始化空的 Request 和 Response 对象,以及设置了 router 对象之后,然后获取 FrontController 的 dispatcher 对象,调用该对象的 dispatch() 方法。FrontController 相当于分发流程的容器,真正实现路由分发的是 dispatcher 对象,即 Zend_Controller_Dispatcher_Standard 类:

    class Zend_Controller_Dispatcher_Standard extends Zend_Controller_Dispatcher_Abstract
    {
        public function dispatch(Zend_Controller_Request_Abstract $request, Zend_Controller_Response_Abstract $response)
        {
            ...
            $className = $this->getControllerClass($request);
            if (!$className) {
                $className = $this->getDefaultControllerClass($request);
            }
            $moduleClassName = $className;
            ...
            $className = $this->loadClass($className);
    
    
            ...
            $controller = new $moduleClassName($request, $this->getResponse(), $this->getParams());
    
            ...
            $action = $this->getActionMethod($request);
    
    
            ...
            $controller->dispatch($action);
    
    
            ...
            $content = ob_get_clean();
            $response->appendBody($content);
        }
    }
    

    从 request 中获取 indexController 类名,加载类文件。这个方法里面解析到的 indexController 文件名为:"/home/feiffy/Repo/feiffy/Training/application/controllers/IndexController.php" 然后加载之。

    第16行,最终实例化了具体的 IndexController 类,这就从框架层到了我们的业务层。

    第23行,执行到业务action。

    第27行,获取输出缓存中的数据,并清理输出缓存,将其内容添加到 response 对象中。执行完这一切之后,返回上层调用,继续执行到 FrontController,调用 reponse 对象的 sendResponse() 方法,输出内容到浏览器。

    下面简要讲一讲 Reponse 对象:

    abstract class Zend_Controller_Response_Abstract
    {
        public function sendResponse()
        {
            $this->sendHeaders();
    
            ...
    
            $this->outputBody();
        }
    
        public function outputBody()
        {
            $body = implode('', $this->_body);
            echo $body;
        }
    }
    
    

    sendHeaders() 输出HTTP报文的头部,这是HTTP协议规定的内容就不必多说,outputBody() 方法输出内容部分,其实很简单,就是把 Response 对象中的 _body 数组里面存储的字符串值全部连接起来输出就OK了。

    echo 函数默认是输出到标准输出(对于纯PHP脚本的话,就是屏幕或者控制台),但这里是 Web 项目,浏览器发出请求首先到 Apache 服务器,然后根据 Apache 的配置调用了 PHP 来接收请求,处理请求,所以这里的PHP输出会返回给 Apache,然后 Apache 再原样返回给浏览器。

    Action.php

    所有的 Controller 都继承自 Zend_Controller_Action 类:

    abstract class Zend_Controller_Action implements Zend_Controller_Action_Interface
    {
        public function __construct(Zend_Controller_Request_Abstract $request, Zend_Controller_Response_Abstract $response, array $invokeArgs = array())
        {
            $this->setRequest($request)
                 ->setResponse($response)
                 ->_setInvokeArgs($invokeArgs);
            $this->_helper = new Zend_Controller_Action_HelperBroker($this);
            $this->init();
        }
        
        public function init()
        {
        }
    
    
        public function dispatch($action)
        {
            $this->preDispatch();
            ...
            $this->$action();
            ...
            $this->postDispatch();
    
    
            ...
            $this->_helper->notifyPostDispatch();
        }
    }
    

    该类实例化时会执行 init() 方法,默认是空的,所以我们可以在自己写的 Controller 里面重写 init() 方法来做些初始化的工作。

    最终使用 dispatch() 来调用 action(),preDispatch() 是在调用 action() 前的准备工作,postDispatch() 是在调用 action() 的收尾工作,我们可以在子类(自己写的 Controller )中的这两个方法里面可以加上对请求参数、返回响应做一些处理,或者单纯记录日志等工作。 而 action() 则是执行具体业务操作的方法。

    现在我们再仔细看一下最后一个 notifyPostDispatch() 方法,运行到这里时,Controller 其实已经执行完了,这个方法主要通知相关的 helper 类更新它们的状态,其中有一个重要的 helper:Zend_Controller_Action_Helper_ViewRenderer,用来渲染视图层的类:

    #1 Zend_Controller_Action_HelperBroker
        public function notifyPostDispatch()
        {
            foreach (self::getStack() as $helper) {
                $helper->postDispatch();
            }
        }
    
    #2 Zend_Controller_Action_Helper_ViewRenderer
        public function postDispatch()
        {
            if ($this->_shouldRender()) {
                $this->render();
            }
        }
    
        public function render($action = null, $name = null, $noController = null)
        {
            $this->setRender($action, $name, $noController);
            $path = $this->getViewScript();
            $this->renderScript($path, $name);
        }
    
        public function renderScript($script, $name = null)
        {
            ...
            $this->getResponse()->appendBody(
                $this->view->render($script),
                $name
            );
            ...
        }
    

    第19行初始化需要的东西,第20行获取需要渲染的phtml脚本文件,第21行渲染该文件。

    getViewScript() 默认把 application/views/scripts/ 当做视图脚本文件的根目录,然后按照 controller/action 的命名规则去寻找相应的.phtml视图脚本文件(.phtml文件其实就是php和html代码混合的文件,php可以直接读取。),比如 index/index 的请求将会去找 index/index.phtml 文件。当然,这个是默认的配置,你也可以在 action() 方法中使用方法指定某个视图文件,这就不提了。

    rederScript() 方法调用视图对象view的render()方法渲染脚本文件,那么渲染是什么意思呢?看View对象的定义就知道了.

    View.php

    abstract class Zend_View_Abstract implements Zend_View_Interface
    {
        public function render($name)
        {
            // find the script file name using the parent private method
            $this->_file = $this->_script($name);
            unset($name); // remove $name from local scope
    
            ob_start();
            $this->_run($this->_file);
    
            return $this->_filter(ob_get_clean()); // filter output
        }
    
        protected function _run()
        {
            ...
            include func_get_arg(0);
        }
    
        private function _filter($buffer)
        {
            ...
            return $buffer;
        }
    }
    

    其实View对象渲染的原理很简单,就是先开启输出缓冲区ob_start(),然后include了一个视图文件(.phtml),这个文件里面非PHP的代码会直接输出,PHP的部分用echo或printf这种输出函数输出内容,输出缓冲开启之后,所有输出的内容会全部存在缓冲区里面,然后调用ob_get_clean() 获取缓冲区内容字符串并清理缓冲区,然后返回所有的字符串给上层调用。最后所有字符串内容通过 Response 对象的 appendBody() 方法添加到其 _body 属性里面,最后通过 Response 对象的输出方法,返回给 Apache,然后PHP结束运行。

    PS - 个人博客原文:Zend_Framework_1框架是如何被启动的?

  • 相关阅读:
    puppet运维配置实列
    puppet yum仓库
    autoSvn
    centos svn快速搭建
    无交互 直接传入 -yes
    puppet案例
    if
    CentosX64使用yum快速搭建xen虚拟化环境
    groupinfo
    Ubuntu编码问题
  • 原文地址:https://www.cnblogs.com/feiffy/p/9173548.html
Copyright © 2020-2023  润新知