目錄
1、简介
1.1 测试环境
1.2 定义&运行测试
2、应用测试
2.1 与应用交互
2.2 测试JSON API
2.3 Session/认证
2.4 禁止中间件
2.5 自定义HTTP请求
2.6 PHPUnit断言方法
3、处理数据库
3.1 每次测试后重置数据库
3.2 模型工厂
4、 模拟
4.1 模拟事件
4.2 模拟队列任务
4.3 模拟门面
首頁 後端開發 php教程 [ Laravel 5.2 文档 ] 服务 -- 测试

[ Laravel 5.2 文档 ] 服务 -- 测试

Jun 20, 2016 pm 12:37 PM

1、简介

Laravel 植根于测试,实际上,内置使用 PHPUnit对测试提供支持是即开即用的,并且 phpunit.xml文件已经为应用设置好了。框架还提供了方便的辅助方法允许你对应用进行富有表现力的测试。

tests目录中提供了一个 ExampleTest.php文件,安装完新的 Laravel 应用后,只需简单在命令行运行 phpunit来运行测试。

1.1 测试环境

运行测试的时候,Laravel 自动设置配置环境为 testing。Laravel在测试时自动配置 session 和 cache 驱动为数组驱动,这意味着测试时不会持久化存储 session 和 cache。

如果需要的话你也可以创建其它测试环境配置。 testing环境变量可以在 phpunit.xml文件中配置。

1.2 定义&运行测试

要创建一个新的测试用例,可以使用如下Artisan命令:

php artisan make:test UserTest
登入後複製

该命令将会在 tests目录下生成一个新的 UserTest类。然后你可以使用PHPUnit 定义测试方法。要运行测试,简单从终端执行 phpunit命令即可:

<?phpuse Illuminate\Foundation\Testing\WithoutMiddleware;use Illuminate\Foundation\Testing\DatabaseMigrations;use Illuminate\Foundation\Testing\DatabaseTransactions;class UserTest extends TestCase{    /**     * A basic test example.     *     * @return void     */    public function testExample()    {        $this->assertTrue(true);    }}
登入後複製

注意:如果你在测试类中定义自己的 setUp方法,确保在其中调用 parent::setUp。

2、应用测试

Laravel 为生成 HTTP请求、测试输出、以及填充表单提供了平滑的API。举个例子,我们看下 tests目录下包含的 ExampleTest.php文件:

<?phpuse Illuminate\Foundation\Testing\WithoutMiddleware;use Illuminate\Foundation\Testing\DatabaseTransactions;class ExampleTest extends TestCase{    /**     * 基本功能测试示例     *     * @return void     */    public function testBasicExample()    {        $this->visit('/')             ->see('Laravel 5')             ->dontSee('Rails');    }}
登入後複製

visit方法生成了一个GET请求, see方法对我们从应用返回响应中应该看到的给定文本进行断言。 dontSee方法对给定文本没有从应用响应中返回进行断言。在Laravel中这是最基本的有效应用测试。

2.1 与应用交互

当然,除了对响应文本进行断言之外还有做更多测试,让我们看一些点击链接和填充表单的例子:

点击链接

在本测试中,我们将为应用生成请求,在返回的响应中“点击”链接,然后对访问URI进行断言。例如,假定响应中有一个“关于我们”的链接:

<a href="/about-us">About Us</a>
登入後複製

现在,让我们编写一个测试点击链接并断言用户访问页面是否正确:

public function testBasicExample(){    $this->visit('/')         ->click('About Us')         ->seePageIs('/about-us');}
登入後複製

处理表单

Laravel 还为处理表单提供了多个方法。 type, select, check, attach, 和 press方法允许你与所有表单输入进行交互。例如,我们假设这个表单存在于应用注册页面:

<form action="/register" method="POST">    {!! csrf_field() !!}    <div>        Name: <input type="text" name="name">    </div>    <div>        <input type="checkbox" value="yes" name="terms"> Accept Terms    </div>    <div>        <input type="submit" value="Register">    </div></form>
登入後複製

我们可以编写测试完成表单并检查结果:

public function testNewUserRegistration(){    $this->visit('/register')         ->type('Taylor', 'name')         ->check('terms')         ->press('Register')         ->seePageIs('/dashboard');}
登入後複製

当然,如果你的表单包含其他输入比如单选按钮或下拉列表,也可以轻松填写这些字段类型。这里是所有表单操作方法列表:

方法 描述
$this->type($text, $elementName) “Type” 文本到给定字段
$this->select($value, $elementName) “Select” 单选框或下拉列表
$this->check($elementName) “Check” 复选框
$this->attach($pathToFile, $elementName) “Attach” 文件到表单
$this->press($buttonTextOrElementName) “Press” 给定文本或name的按钮
$this->uncheck($elementName) “Uncheck”复选框

处理附件

如果表单包含 file输入类型,可以使用 attach方法添加文件到表单:

public function testPhotoCanBeUploaded(){    $this->visit('/upload')         ->name('File Name', 'name')         ->attach($absolutePathToFile, 'photo')         ->press('Upload')         ->see('Upload Successful!');}
登入後複製

2.2 测试JSON API

Laravel 还提供多个帮助函数用于测试 JSON API 及其响应。例如, get, post, put, patch, 和 delete方法用于通过多种 HTTP 请求方式发出请求。你还可以轻松传递数据和头到这些方法。作为开始,我们编写测试来生成 POST 请求到 /user并断言返回的数据是否是 JSON 格式:

<?phpclass ExampleTest extends TestCase{    /**     * 基本功能测试示例     *     * @return void     */    public function testBasicExample()    {        $this->post('/user', ['name' => 'Sally'])             ->seeJson([                 'created' => true,             ]);    }}
登入後複製

seeJson方法将给定数组转化为 JSON,然后验证应用返回的整个 JSON 响应中的 JSON 片段。因此,如果在 JSON 响应中有其他属性,只要给定片段存在的话测试依然会通过。

验证JSON值匹配

如果你想要验证给定数组和应用返回的JSON能够精确匹配,使用 seeJsonEquals方法:

<?phpclass ExampleTest extends TestCase{    /**     * 基本功能测试示例     *     * @return void     */    public function testBasicExample()    {        $this->post('/user', ['name' => 'Sally'])             ->seeJsonEquals([                 'created' => true,             ]);    }}
登入後複製

验证JSON数据结构匹配

还可以验证JSON响应是否与指定数据结构匹配,我们使用 seeJsonStructure方法来实现这一功能:

<?phpclass ExampleTest extends TestCase{    /**     * A basic functional test example.     *     * @return void     */    public function testBasicExample()    {        $this->get('/user/1')             ->seeJsonStructure([                 'name',                 'pet' => [                     'name', 'age'                 ]             ]);    }}
登入後複製

上面的例子演示了期望获取一个包含 name和嵌套 pet对象(该对象包含 name和 age属性)的 JSON 数据。如果 JSON 响应中包含其它额外键 seeJsonStructure也不会失败,例如,如果 pet对象包含 weight属性测试仍将通过。

你可以使用*来断言返回JSON结构包含一个列表,该列表中的每个数据项都包含至少如下示例中列出的属性:

<?phpclass ExampleTest extends TestCase{    /**     * A basic functional test example.     *     * @return void     */    public function testBasicExample()    {        // Assert that each user in the list has at least an id, name and email attribute.        $this->get('/users')             ->seeJsonStructure([                 '*' => [                     'id', 'name', 'email'                 ]             ]);    }}
登入後複製

你还可以使用嵌套的*,在这种场景中,我们可以断言JSON响应中的每个用户都包含一个给定属性集合,而且每个用户的每个 pet都包含给定属性集合:

$this->get('/users')     ->seeJsonStructure([         '*' => [             'id', 'name', 'email', `pets` => [                 '*' => [                     'name', 'age'                 ]             ]         ]     ]);
登入後複製

2.3 Session/认证

Laravel提供了多个辅助函数用于在测试期间处理Session,首先,可以使用 withSession方法设置 session 值到给定数组。这在测试请求前获取 session 数据时很有用:

<?phpclass ExampleTest extends TestCase{    public function testApplication()    {        $this->withSession(['foo' => 'bar'])             ->visit('/');    }}
登入後複製

当然,session的通常用于操作用户状态,例如认证用户。辅助函数 actingAs提供了认证给定用户为当前用户的简单方法,例如,我们使用模型工厂生成和认证用户:

<?phpclass ExampleTest extends TestCase{    public function testApplication()    {        $user = factory('App\User')->create();        $this->actingAs($user)             ->withSession(['foo' => 'bar'])             ->visit('/')             ->see('Hello, '.$user->name);    }}
登入後複製

还可以通过传递 guard名称作为 actingAs函数的第二个参数的方式来指定使用哪个 guard来认证给定用户:

$this->actingAs($user, 'backend')
登入後複製

2.4 禁止中间件

测试应用时,为某些测试禁止中间件很方便。这种机制允许你将路由和控制器与中间件孤立开来做测试,Laravel包含了一个简单的 WithoutMiddlewaretrait,可以使用该 trait 自动在测试类中禁止所有中间件:

<?phpuse Illuminate\Foundation\Testing\WithoutMiddleware;use Illuminate\Foundation\Testing\DatabaseTransactions;class ExampleTest extends TestCase{    use WithoutMiddleware;    //}
登入後複製

如果你只想在某些方法中禁止中间件,可以在测试方法中调用 withoutMiddleware方法:

<?phpclass ExampleTest extends TestCase{    /**     * 基本功能测试示例     *     * @return void     */    public function testBasicExample()    {        $this->withoutMiddleware();        $this->visit('/')             ->see('Laravel 5');    }}
登入後複製

2.5 自定义HTTP请求

如果你想要在应用中生成自定义HTTP请求并获取完整的 Illuminate\Http\Response对象,可以使用 call方法:

public function testApplication(){    $response = $this->call('GET', '/');    $this->assertEquals(200, $response->status());}
登入後複製

如果你要生成 POST, PUT, 或者 PATCH请求可以在请求中传入输入数据数组,在路由或控制器中可以通过Request实例访问请求数据:

$response = $this->call('POST', '/user', ['name' => 'Taylor']);
登入後複製

2.6 PHPUnit断言方法

Laravel 为 PHPUnit 测试提供了额外的断言方法:

方法 描述
->assertResponseOk(); 断言客户端响应状态码是否为200
->assertResponseStatus($code); 断言客户端响应状态码是否是给定$code
->assertViewHas($key, $value = null); 断言响应视图是否包含给定的绑定数据片段
->assertViewHasAll(array $bindings); 断言视图是否包含给定绑定数据列表
->assertViewMissing($key); 断言响应视图缺失绑定数据片段
->assertRedirectedTo($uri, $with = []); 断言客户端是否重定向到给定URI
->assertRedirectedToRoute($name, $parameters = [], $with = []); 断言客户端是否重定向到给定路由
->assertRedirectedToAction($name, $parameters = [], $with = []); 断言客户端是否重定向到给定action
->assertSessionHas($key, $value = null); 断言session是否包含给定值
->assertSessionHasAll(array $bindings); 断言session是否保护眼给定值列表
->assertSessionHasErrors($bindings = [], $format = null); 断言session是否包含错误绑定
->assertHasOldInput(); 断言sessio包含上次输入数据

3、处理数据库

Laravel还提供了多种有用的工具让测试数据库驱动的应用更加简单。首先,你可以使用帮助函数 seeInDatabase来断言数据库中的数据是否和给定数据集合匹配。例如,如果你想要通过 email 值为 sally@example.com的条件去数据表 users查询是否存在该记录 ,我们可以这样做:

public function testDatabase(){    // 调用应用...    $this->seeInDatabase('users', ['email' => 'sally@foo.com']);}
登入後複製

当然, seeInDatabase方法和其它类似辅助方法都是为了方便起见进行的封装,你也可以使用其它PHPUnit内置的断言方法来进行测试。

3.1 每次测试后重置数据库

每次测试后重置数据库通常很有用,这样的话上次测试的数据不会影响下一次测试。

使用迁移

一种方式是每次测试后回滚数据库并在下次测试前重新迁移。Laravel提供了一个简单的 DatabaseMigrationstrait来自动为你处理。在测试类上简单使用该trait如下:

<?phpuse Illuminate\Foundation\Testing\WithoutMiddleware;use Illuminate\Foundation\Testing\DatabaseMigrations;use Illuminate\Foundation\Testing\DatabaseTransactions;class ExampleTest extends TestCase{    use DatabaseMigrations;    /**     * 基本功能测试示例     *     * @return void     */    public function testBasicExample()    {        $this->visit('/')             ->see('Laravel 5');    }}
登入後複製

使用事务

另一种方式是将每一个测试用例包裹到一个数据库事务中,Laravel提供了方便的 DatabaseTransactionstrait自动为你处理:

<?phpuse Illuminate\Foundation\Testing\WithoutMiddleware;use Illuminate\Foundation\Testing\DatabaseMigrations;use Illuminate\Foundation\Testing\DatabaseTransactions;class ExampleTest extends TestCase{    use DatabaseTransactions;    /**     * 基本功能测试示例     *     * @return void     */    public function testBasicExample()    {        $this->visit('/')             ->see('Laravel 5');    }}
登入後複製

注意:该trait只在事务中封装默认数据库连接。

3.2 模型工厂

测试时,通常需要在执行测试前插入新数据到数据库。在创建测试数据时,Laravel允许你使用“factories”为每个Eloquent模型定义默认的属性值集合,而不用手动为每一列指定值。作为开始,我们看一下 database/factories/ModelFactory.php文件,该文件包含了一个工厂定义:

$factory->define(App\User::class, function (Faker\Generator $faker) {    return [        'name' => $faker->name,        'email' => $faker->email,        'password' => bcrypt(str_random(10)),        'remember_token' => str_random(10),    ];});
登入後複製

在闭包中,作为工厂定义,我们返回该模型上所有属性默认测试值。该闭包接收PHP库 Faker实例,从而允许你方便地为测试生成多种类型的随机数据。

当然,你可以添加更多工厂到 ModelFactory.php文件。

多个工厂类型

有时候你可能想要为同一个Eloquent模型类生成多个工厂,例如,除了正常用户外可能你想要为“管理员”用户生成一个工厂,你可以使用 defineAs方法定义这些工厂:

$factory->defineAs(App\User::class, 'admin', function ($faker) {    return [        'name' => $faker->name,        'email' => $faker->email,        'password' => str_random(10),        'remember_token' => str_random(10),        'admin' => true,    ];});
登入後複製

你可以使用 raw方法获取基本属性而不用重复基本用户工厂中的所有属性,获取这些属性后,只需将你要求的额外值增补进去即可:

$factory->defineAs(App\User::class, 'admin', function ($faker) use ($factory) {    $user = $factory->raw(App\User::class);    return array_merge($user, ['admin' => true]);});
登入後複製

在测试中使用工厂

定义好工厂后,可以在测试或数据库填充文件中通过全局的 factory方法使用它们来生成模型实例,所以,让我们看一些生成模型的例子,首先,我们使用 make方法,该方法创建模型但不将其保存到数据库:

public function testDatabase(){    $user = factory(App\User::class)->make();    // 用户模型测试...}
登入後複製

如果你想要覆盖模型的一些默认值,可以传递数组值到 make方法。只有指定值被替换,其他数据保持不变:

$user = factory(App\User::class)->make([    'name' => 'Abigail',]);
登入後複製

还可以创建多个模型集合或者创建给定类型的集合:

// 创建3个 App\User 实例...$users = factory(App\User::class, 3)->make();// 创建1个 App\User "admin" 实例...$user = factory(App\User::class, 'admin')->make();// 创建3个 App\User "admin" 实例...$users = factory(App\User::class, 'admin', 3)->make();
登入後複製

持久化工厂模型

create方法不仅能创建模型实例,还可以使用Eloquent的 save方法将它们保存到数据库:

public function testDatabase(){    $user = factory(App\User::class)->create();    //用户模型测试...}
登入後複製

你仍然可以通过传递数组到 create方法覆盖模型上的属性:

$user = factory(App\User::class)->create([    'name' => 'Abigail',]);
登入後複製

添加关联关系到模型

你甚至可以持久化多个模型到数据库。在本例中,我们添加一个关联到创建的模型,使用 create方法创建多个模型的时候,会返回一个Eloquent集合实例,从而允许你使用集合提供的所有便利方法,例如 each:

$users = factory(App\User::class, 3)           ->create()           ->each(function($u) {                $u->posts()->save(factory(App\Post::class)->make());            });
登入後複製

4、 模拟

4.1 模拟事件

如果你在重度使用Laravel的时间系统,可能想要在测试时模拟特定事件。例如,如果你在测试用户注册,你可能不想所有 UserRegistered的时间处理器都被触发,因为这可能会发送欢迎邮件,等等。

Laravel提供可一个方便的 expectsEvents方法来验证期望的事件被触发,但同时阻止该事件的其它处理器运行:

<?phpclass ExampleTest extends TestCase{    public function testUserRegistration()    {        $this->expectsEvents(App\Events\UserRegistered::class);        // 测试用户注册代码...    }}
登入後複製

可以使用 doesntExpectEvents方法来验证给定事件没有被触发:

<?phpclass ExampleTest extends TestCase{    public function testPodcastPurchase()    {        $this->expectsEvents(App\Events\PodcastWasPurchased::class);        $this->doesntExpectEvents(App\Events\PaymentWasDeclined::class);        // Test purchasing podcast...    }}
登入後複製

如果你想要阻止所有事件运行,可以使用 withoutEvents方法:

<?phpclass ExampleTest extends TestCase{    public function testUserRegistration()    {        $this->withoutEvents();        // 测试用户注册代码...    }}
登入後複製

4.2 模拟队列任务

有时候,你可能想要在请求时简单测试控制器分发的指定任务,这允许你孤立的测试路由/控制器——将其从任务逻辑中分离出去,当然,接下来你可以在一个独立测试类中测试任务本身。

Laravel提供了一个方便的 expectsJobs方法来验证期望的任务被分发,但该任务本身不会被测试:

<?phpclass ExampleTest extends TestCase{    public function testPurchasePodcast()    {        $this->expectsJobs(App\Jobs\PurchasePodcast::class);        // 测试购买播客代码...    }}
登入後複製

注意:这个方法只检查通过 DispatchesJobstrait分发方法分发的任务,并不检查直接通过 Queue::push分发的任务。

4.3 模拟门面

测试的时候,你可能经常想要模拟Laravel门面的调用,例如,看看下面的控制器动作:

<?phpnamespace App\Http\Controllers;use Cache;use Illuminate\Routing\Controller;class UserController extends Controller{    /**     * 显示应用用户列表     *     * @return Response     */    public function index()    {        $value = Cache::get('key');        //    }}
登入後複製

我们可以通过使用 shouldReceive方法模拟 Cache门面的调用,该方法返回一个 Mockery模拟的实例,由于门面通过Laravel服务容器解析和管理,它们比通常的静态类更具有可测试性。例如,我们来模拟 Cache门面的调用:

<?phpclass FooTest extends TestCase{    public function testGetIndex()    {        Cache::shouldReceive('get')                    ->once()                    ->with('key')                    ->andReturn('value');        $this->visit('/users')->see('value');    }}
登入後複製

注意:不要模拟 Request门面,取而代之地,在测试时传递输入到HTTP帮助函数如 call和 post。

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

<🎜>:泡泡膠模擬器無窮大 - 如何獲取和使用皇家鑰匙
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
北端:融合系統,解釋
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
Mandragora:巫婆樹的耳語 - 如何解鎖抓鉤
3 週前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1665
14
CakePHP 教程
1424
52
Laravel 教程
1322
25
PHP教程
1269
29
C# 教程
1249
24
說明PHP中的安全密碼散列(例如,password_hash,password_verify)。為什麼不使用MD5或SHA1? 說明PHP中的安全密碼散列(例如,password_hash,password_verify)。為什麼不使用MD5或SHA1? Apr 17, 2025 am 12:06 AM

在PHP中,應使用password_hash和password_verify函數實現安全的密碼哈希處理,不應使用MD5或SHA1。1)password_hash生成包含鹽值的哈希,增強安全性。 2)password_verify驗證密碼,通過比較哈希值確保安全。 3)MD5和SHA1易受攻擊且缺乏鹽值,不適合現代密碼安全。

PHP和Python:比較兩種流行的編程語言 PHP和Python:比較兩種流行的編程語言 Apr 14, 2025 am 12:13 AM

PHP和Python各有優勢,選擇依據項目需求。 1.PHP適合web開發,尤其快速開發和維護網站。 2.Python適用於數據科學、機器學習和人工智能,語法簡潔,適合初學者。

PHP行動:現實世界中的示例和應用程序 PHP行動:現實世界中的示例和應用程序 Apr 14, 2025 am 12:19 AM

PHP在電子商務、內容管理系統和API開發中廣泛應用。 1)電子商務:用於購物車功能和支付處理。 2)內容管理系統:用於動態內容生成和用戶管理。 3)API開發:用於RESTfulAPI開發和API安全性。通過性能優化和最佳實踐,PHP應用的效率和可維護性得以提升。

PHP:網絡開發的關鍵語言 PHP:網絡開發的關鍵語言 Apr 13, 2025 am 12:08 AM

PHP是一種廣泛應用於服務器端的腳本語言,特別適合web開發。 1.PHP可以嵌入HTML,處理HTTP請求和響應,支持多種數據庫。 2.PHP用於生成動態網頁內容,處理表單數據,訪問數據庫等,具有強大的社區支持和開源資源。 3.PHP是解釋型語言,執行過程包括詞法分析、語法分析、編譯和執行。 4.PHP可以與MySQL結合用於用戶註冊系統等高級應用。 5.調試PHP時,可使用error_reporting()和var_dump()等函數。 6.優化PHP代碼可通過緩存機制、優化數據庫查詢和使用內置函數。 7

PHP的持久相關性:它還活著嗎? PHP的持久相關性:它還活著嗎? Apr 14, 2025 am 12:12 AM

PHP仍然具有活力,其在現代編程領域中依然佔據重要地位。 1)PHP的簡單易學和強大社區支持使其在Web開發中廣泛應用;2)其靈活性和穩定性使其在處理Web表單、數據庫操作和文件處理等方面表現出色;3)PHP不斷進化和優化,適用於初學者和經驗豐富的開發者。

PHP類型提示如何起作用,包括標量類型,返回類型,聯合類型和無效類型? PHP類型提示如何起作用,包括標量類型,返回類型,聯合類型和無效類型? Apr 17, 2025 am 12:25 AM

PHP類型提示提升代碼質量和可讀性。 1)標量類型提示:自PHP7.0起,允許在函數參數中指定基本數據類型,如int、float等。 2)返回類型提示:確保函數返回值類型的一致性。 3)聯合類型提示:自PHP8.0起,允許在函數參數或返回值中指定多個類型。 4)可空類型提示:允許包含null值,處理可能返回空值的函數。

PHP和Python:代碼示例和比較 PHP和Python:代碼示例和比較 Apr 15, 2025 am 12:07 AM

PHP和Python各有優劣,選擇取決於項目需求和個人偏好。 1.PHP適合快速開發和維護大型Web應用。 2.Python在數據科學和機器學習領域佔據主導地位。

PHP與其他語言:比較 PHP與其他語言:比較 Apr 13, 2025 am 12:19 AM

PHP適合web開發,特別是在快速開發和處理動態內容方面表現出色,但不擅長數據科學和企業級應用。與Python相比,PHP在web開發中更具優勢,但在數據科學領域不如Python;與Java相比,PHP在企業級應用中表現較差,但在web開發中更靈活;與JavaScript相比,PHP在後端開發中更簡潔,但在前端開發中不如JavaScript。

See all articles