前言
相信很多人對Babel
都瞭解,但是差不多是一知半解,很多地方估計是懵懵懂懂的感覺。配置的時候,在網上搜尋,然後複製貼上,能跑通就好,但偶爾會出現一些稀奇古怪的問題。後面想深入瞭解學習Babel
,又發現官網讀起來晦澀難懂,或者照著官網敲Demo
,又發現實際結果不是官網說的那樣(其實是因為我們安裝的依賴版本有問題,安裝成最新的了,所以輸出的效果跟官網的不一樣)。這樣就造成我們對Babel
更加的困惑。
因為Babel
內容實在太多了,所以這篇文章不講原理,也不講怎麼配置(配置後續會出專門的文章說),但會帶著大家一起理解讓人覺得“晦澀難懂”的官網,然後梳理、瞭解,我們平時接觸Babel
用到的主要幾個包,搞清楚Babel
是什麼、作用又是什麼。
為了讓大家更好的感受Babel
對我們日常專案的作用,有些例子會結合Webpack
,畢竟我們平時的專案,基本都會透過Webpack
等打包工具跟Babel
相結合輸出最後的包,然後在瀏覽器中執行。
章節中的案例,程式碼都放到Github
上了,建議大家邊閱讀,邊跟著案例看。如果大家覺得有幫助到,歡迎Star 跟 Fork學習。
備註:
- 當前
@babel/core
最新版本是:7.20.12
- 當前
@babel/preset-env
最新版本是:7.20.2
Babel
官網解釋:Babel
是一個工具鏈,主要用於將採用ECMAScript 2015+
語法編寫的程式碼轉換為向後相容的JavaScript
語法,以便能夠執行在當前和舊版本的瀏覽器或其他環境中。
我們可以這麼理解,Babel
就是一個工具。它是一個可以將ES6+
等新特性,轉換成低版本瀏覽器或其他環境能支援並正常執行的一個工具。
結構
很多人以為Babel
只有plugins
、presets
等幾個配置。其實不止,我們看看Babel
配置檔案大致架構:
// babel.config.js
module.exports = {
...,
envName: "development",
plugins: [],
presets: [],
passPerPreset: false,
targets: {},
browserslistConfigFile: true,
browserslistEnv: undefined,
inputSourceMap: true
...
}
我們一般主要用到的就是plugins
、presets
這兩個
功能
從大體上看,Babel
提供以下兩個功能組成:
- 編譯
ES6+
最新語法(let
、class
、() => {}
等) - 實現舊版本瀏覽器不支援的
ES6+
的API
(Promise
、Symbol
、Array.prototype.includes
等)
參考文章:
@babel/core
從core
可以看出,它是Babel
實現編譯的核心。所以我們如果要使用Babel
,@babel/core
這個包一定是必不可少的。另外我們平常說的Babel 6
、Babel 7
指的就是@babele/core
的版本
參考文章:@babel/core
@bable/cli
官網解釋:Babel
自帶了一個內建的CLI
命令列工具,可透過命令列編譯檔案
簡單地說就是,讓我們可以在終端裡使用命令來編譯(這樣可以更好的除錯列印資訊):
npx babel index.js
安裝的話,我們最好安裝到我們專案的本地目錄下,儘量不要安裝到全域性(影響全域性的東西,都很可怕)
參考文章:@babel/cli
@bable/preset-env
官網解釋:@babel/preset-env
是一個智慧預設,它允許您使用最新的JavaScript
,而無需微觀管理目標環境需要哪些語法轉換(以及可選的瀏覽器polyfill
)。這既讓你的生活更輕鬆,也讓JavaScript
包更小!
理解
@bable/preset-env
這個名字,我們可以拆開兩部分來看,這樣方便理解:
preset
預設env
環境
preset
Babel
編譯ES6+
語法,是透過一個個外掛plugin
去實現的。每年都會有不同新的提案、新的語法,但我們不可能一個個外掛去配置,所以就有了preset
這個東西。因此我們可以理解成preset
就是一個語法外掛集合包,這樣我們只用安裝這一個包,不需要一個個配外掛,就可以很方便的編譯最新的語法了。
我們透過一個不用預設的案例 no-preset ,感受一下如果不用preset
有多麻煩。
// 入口檔案 index.js
const senses = ['eye', 'nose', 'ear', 'mouth'];
const lMC = {
senses,
like: ['eat', 'drink', 'play', 'fun'],
information: {
sex: 'male',
age: '18+'
},
play: (sport = 'badminton') => {
console.log(`play ${sport}`);
}
};
const { like, information } = lMC;
這段程式碼,我們用了幾個ES6
新語法:
const
宣告- 屬性的簡潔表示法
- 箭頭函式
- 函式預設值
- 模板字串
- 解構
如果不用preset
我們Babel
配置如下:
// Babel配置檔案 babel.config.js
const plugins = [
'@babel/plugin-transform-arrow-functions',
'@babel/plugin-transform-block-scoping',
'@babel/plugin-transform-destructuring',
'@babel/plugin-transform-parameters',
'@babel/plugin-transform-shorthand-properties',
'@babel/plugin-transform-template-literals'
];
module.exports = {plugins};
編譯後的檔案:
// 編譯後的檔案 compile.js
var senses = ['eye', 'nose', 'ear', 'mouth'];
var lMC = {
senses: senses,
like: ['eat', 'drink', 'play', 'fun'],
information: {
sex: 'male',
age: '18+'
},
play: function () {
var sport = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'badminton';
console.log("play ".concat(sport));
}
};
var like = lMC.like,
information = lMC.information;
在不用preset
的情況下,實現上述編譯的過程,我基本是用一個ES6
新語法,我就要去查一個外掛,首先我不記得那麼多外掛,其次一個個外掛找真的很累。
ok,那我們再用一個使用了預設的案例 use-preset ,感受一下預設到底有多方便。
我們npm i @babel/preset-env -D
,修改babel.config.js
使用preset
預設:
// 修改babel.json
const presets = [
'@babel/preset-env'
];
module.exports = {presets};
編譯後的檔案:
// 編譯後的檔案 compile.js
"use strict";
var senses = ['eye', 'nose', 'ear', 'mouth'];
var lMC = {
senses: senses,
like: ['eat', 'drink', 'play', 'fun'],
information: {
sex: 'male',
age: '18+'
},
play: function play() {
var sport = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'badminton';
console.log("play ".concat(sport));
}
};
var like = lMC.like,
information = lMC.information;
我們會發現,用preset
(預設)方式輸出的程式碼,跟plugins
(不用預設)方式輸出的程式碼是幾乎是一模一樣的。但是preset
的babel.config.js
更簡潔,我也不需要一個個外掛去找,也不需要安裝那麼多外掛,只用安裝@babel/preset-env
這一個包,就可以很愉快的寫ES6+
。
env
env
指的是環境。因為@babel/preset-env
還有一個配置功能,我們可以透過配置我們程式碼執行的目標環境,來控制polyfill
(一個提供低版本瀏覽器缺失的ES6+
新特性的方法與實現的集合 ,後面會有更詳細的講解)的匯入跟語法編譯,從而使ES6+
新的特性可以在我們想要的目標環境中順利執行。
備註:@babel/preset-env
還有一個配置功能,本文不講配置,關於配置後續會有文章說明
功能
透過上面對preset
、env
的理解跟案例感受,我們能總結出@babel/preset-env
主要提供以下功能:
- 它只編譯
ES6+
語法(上述案例只使用了ES6+
的語法,並沒有用ES6+
的API
) - 它並不提供
polyfill
,但是可以透過配置我們程式碼執行的目標環境,從而控制polyfill
的匯入跟語法編譯,使ES6+
的新特性可以在我們想要的目標環境中順利執行
注意
我們先看看TC39
提案分為幾個階段:
- 階段0 (
stage-0
)——草根(Strawman):只是一個想法,可能是Babel
外掛。 - 第一階段(
stage-1
)——提案(Proposal):這是值得研究的。 - 第二階段(
stage-2
)——草案(Draft):初步規範。 - 第三階段(
stage-3
)——候選(Candidate):完整的規範和最初的瀏覽器實現。 - 第四階段(
stage-4
)——完成(Finished):將被新增到下一年度的版本中。
再看看官網中這段話:
Note: @babel/preset-env
won't include any JavaScript syntax proposals less than Stage 3 because at that stage in the TC39 process, it wouldn't be implemented by any browsers anyway. Those would need to be included manually.
大致意思是:
- 在
Babel 7
以後,@bable/preset-env
捨棄了Stage presets
(@babel/preset-stage-x
)這種預設 @bable/preset-env
只提供TC39
大於stage-3
的提案(即只包含stage-4
階段),因此如果要用小於stage 4
的提案語法,則必須先安裝再手動引入對應外掛
第一點相信大家都很好理解,我們來理解一下第二點是什麼意思。
意思是,如果我們想用一些小於stage-4
階段的語法的話,光安裝@babel/preset-env
這一個包是沒有用的,因為這個包裡只包含編譯stage-4
的預設,所以我們就得安裝並配置相應的plugin
去編譯。
在寫這篇文章的時候,有一個新的語法 do expressions ,它當前是處於stage-1
階段的語法,用外掛@babel/plugin-proposal-do-expressions
可以編譯這個語法。
官網解釋:do { .. } 表示式執行一個塊(其中有一個或多個語句),塊內的最終語句完成值成為 do 表示式的完成值。
我們藉助官網,整理成這個案例 compile-stage-1 來看看怎麼使用小於stage-4
的語法。
我們先只用@babel/preset-env
,看看能不能編譯do {...}
這個語法。
// do expressions stage-1語法
let x = 100;
let y = 20;
let a = do {
if (x > 10) {
if (y > 20) {
("big x, big y");
} else {
("big x, small y");
}
} else {
if (y > 10) {
("small x, big y");
} else {
("small x, small y");
}
}
};
Babel.config.js
配置:
const presets = [
'@babel/preset-env'
];
// const plugins = [
// '@babel/plugin-proposal-do-expressions'
// ];
// module.exports = {plugins, presets};
module.exports = {presets};
我們會發現,終端會報錯:
大致意思是:@babel/preset-env
當前未啟用對實驗語法doExpressions
的支援(因為doExpressions
當前是stage-1
的語法,@babel/preset-env
只包含必編譯stage-4
的語法外掛),需要我們加入@babel/plugin-proposal-do-expressions
外掛去編譯。
那我們npm i @babel/plugin-proposal-do-expressions -D
,修改一下babel.config.js
:
const presets = [
'@babel/preset-env'
];
const plugins = [
'@babel/plugin-proposal-do-expressions'
];
module.exports = {plugins, presets};
// module.exports = {presets};
我們可以看到,可以正常輸出編譯後的檔案:
"use strict";
var x = 100;
var y = 20;
var a = x > 10 ? y > 20 ? "big x, big y" : "big x, small y" : y > 10 ? "small x, big y" : "small x, small y";
所以,當我們想使用小於stage-4
階段的語法時,我們要先找到其對應的編譯外掛安裝,然後在plugins
裡面配置就好了。
參考文章:
補充
有時我們也可能需要知道我們當前的preset
(預設)包含了哪些外掛,那我們怎麼檢視當前@babel/preset-env
包含了哪些預設呢?
我們可以透過檢視@babel/preset-env
--> package.json
--> dependencies
裡面可以找到。我目前安裝的@babel/preset-env
版本為7.20.2
,它包含了以下預設:
// @babel/preset-env@7.20.2預設
"dependencies": {
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6",
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9",
...,
"@babel/plugin-transform-sticky-regex": "^7.18.6",
"@babel/plugin-transform-template-literals": "^7.18.9",
"@babel/plugin-transform-typeof-symbol": "^7.18.9",
"@babel/plugin-transform-unicode-escapes": "^7.18.10",
"@babel/plugin-transform-unicode-regex": "^7.18.6",
},
polyfill
功能
ES6+
除了提供很多簡潔的語法(let
、class
、() => {}
等)外,還為我們提供了很多便捷的API
(Promise
、Symbol
、Array.prototype.includes
等)。但舊版本瀏覽器是不支援這些API
的,而polyfill
存放了這些API
的方法與實現,所以它可以使得這些不支援的瀏覽器,支援這些API
。
理解
我們可以把所有這種存放了ES6+
API
的方法與實現的集合叫做polyfill
,也就是我們經常說的墊片。
polyfill
也分很多種,像core-js
是會提供舊版本瀏覽器缺失的所有的API
;還有一些只提供缺失API
的某塊,例如 promise-polyfill、proxy-polyfill 等。
Babel
配置polyfill
的過程,就是實現舊版本瀏覽器對這些API
支援的過程。
@babel/polyfill
上面我們解釋了polyfill
是什麼,從包名@babel/polyfill
就知道,它就是一個polyfill
(其核心是依靠core-js@2.x.x
實現)。雖然這個包已經被廢棄了,但我們還是稍微瞭解一下它。
官網解釋:
? 從Babel 7.4.0開始,這個包已經被棄用,轉而直接包含core-js/stable
(用於polyfill ECMAScript
功能)使用:
import "core-js/stable";
初識
我們透過這個例子 know-babel-polyfill,來了解一下@babel/polyfill
的組成。
know-babel-polyfill 什麼都沒安裝,只安裝了@bable/polyfill
這個依賴,我們可以很清楚看到,@bable/polyfill
由以下兩個包組成:
<img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8ff1acb6c6e2463aad5798f805f3efd8~tplv-k3u1fbpfcp-watermark.image?" alt="babel-polyfill.jpeg" width="100%" />
core-js
(版本為2
)regenerator-runtime
我們來大致理解一下這兩包是什麼:
core-js
這個包就是我們上述polyfill
模組所說的,裡面存放了很多ES6+
API
的方法與實現。如果要在舊瀏覽用到Promise
、Symbol
、Array.prototype.includes
等方法時,這個包會為我們提供。它可以使那些不支援API
的瀏覽器,支援這些API
,它就是一種墊片。
特別注意:由上圖可知,@babel/polyfill
是與2
版本的core-js
繫結的,2
版本的core-js
並不包含stable
這個資料夾的。因此官網說的import "core-js/stable"
,實際上是要我們安裝core-js@3.x.x
版本來代替@babel/polyfill
,因為從3
版本開始,才有stable
這個資料夾
regenerator-runtime
我們的原始碼裡面使用了async function() {}
等非同步函式,或者fuction* myGenerator() {}
這種Generator
函式的話,就會需要用到這個包來編譯。
總結
所以對於@babel/polyfill
,我們有以下總結:
- 這個包由
core-js
(版本為2.x.x
)與regenerator-runtime
兩個包組成 - 這個包在
Babel 7.4.0
以後就被廢棄了,所以在Babel 7.4.0
以後,我們想讓一些不支援ES6+
API
的瀏覽器支援這些API
,就不應該安裝這個包,應該安裝core-js@3.x.x
的包(不要安裝2.x.x
的版本,已經不維護了,目前最新版本為3.x.x
)
參考文章:@babel/polyfill
core-js
概述
透過上面polyfill
、@babel/polyfill
兩個模組,我們可以知道@babel/polyfill
已經不再使用,而且@babel/polyfill
實現的核心就是core-js
,所以如果我們想要在舊瀏覽器用到Promise
、Symbol
、Array.prototype.includes
等方法時,我們直接安裝core-js@3.x.x
這個包。
透過 官方的介紹,我們可以知道:
import '@babel/polyfill';
等同於
// core-js必須是3.x.x版本,因為2.x.x版本,不包含stable資料夾
import "core-js/stable";
import "regenerator-runtime/runtime";
Babel >= 7.18.0
等同於
// core-js必須是3.x.x版本,因為2.x.x版本,不包含stable資料夾
// Babel >= 7.18.0後 不需要再 import "regenerator-runtime/runtime";
import "core-js/stable";
注意
我們針對不需要再import "regenerator-runtime/runtime"
這塊,稍微解釋一下,加深一下我們對Babel
跟官網文件的理解。
我們看官方這段話:
If you are compiling generators or async function toES5
, and you are using a version of@babel/core
or@babel/plugin-transform-regenerator
older than7.18.0
, you must also load the regenerator-runtime package
大家看這句話的時候可能有點疑惑,其實它的意思就是:
如果我們要把async function() {}
等非同步函式,或者fuction* myGenerator() {}
這種Generator
函式編譯成ES5
,並且@babel/core
或@babel/plugin-transform-regenerator
小於7.18.0
,我們就需要手動import "regenerator-runtime/runtime"
這個包。
但在Babel 7.18.0
或者@babel/plugin-transform-regenerator 7.18.0
及其以後的版本,regenerator-runtime
包裡面的內容會被內聯編譯到我們的程式碼中,所以我們只用引入import "core-js/stable"
這一個包就可以了。
我們來用兩個例子結合Webpack
打包出來,在瀏覽器執行,這樣更直觀的理解感受一下。
Babel < 7.18.0
我們用這個例子 import-regenerator-runtime 看看在Babel 7.18.0
之前為什麼要手動引入regenerator-runtime
這個包。
特別說明: 我們例子安裝Babel
的版本為7.16.7
,@babel/plugin-transform-regenerator
這個外掛必須手動安裝為小於7.18.0
的版本(因為我們安裝依賴的時候,即使指定了依賴的版本,但依賴的依賴安裝時,可能會是最新的,這樣可能會看不出效果。所以為什麼有時我們對著官網敲Demo
實際出來的結果不一樣,因為版本沒對上)。可以透過package-lock.json
檢視各個依賴版本
ok,來看看我們的入口檔案(index.js
):
// 先不引入regenerator-runtime/runtime
// import 'regenerator-runtime/runtime';
const sleep = async function() {
setTimeout(() => console.log('get up'), 1000);
}
sleep();
接著我們打包(Webpack
打包出來的檔案在dist/dist.js
)在瀏覽器執行。正常情況下,瀏覽器應該會過一秒後輸出get up
。但實際情況如下,我們會發現之前網友們經常出現的一個問題——regeneratorRuntime is not defined
:
說明缺失了regeneratorRuntime
,我們再看看Babel
編譯後的檔案(compile.js
):
我們發現在全域性中,regeneratorRuntime
根本沒有定義,所以才報了regeneratorRuntime is not defined
的錯。
如果我們再手動引入一下import "regenerator-runtime/runtime"
:
import 'regenerator-runtime/runtime';
const sleep = async function() {
setTimeout(() => console.log('get up'), 1000);
}
sleep();
此時瀏覽器輸出:
當我們手動引入以後,瀏覽器可以正常執行了。
這說明,在@babel/core
或@babel/plugin-transform-regenerator
的版本小於7.18.0
的時候,使用了非同步函式(async function() {}
),或者Generator
這種函式(fuction* myGenerator() {}
)的話,是需要我們手動引入regenerator-runtime
這個包的,因為regenerator-runtime
這個包會為我們提供regeneratorRuntime
這個全域性物件。
Babel >= 7.18.0
我們用這個例子 no-import-regenerator-runtime 看看在Babel 7.18.0
之後為什麼不需要手動引入regenerator-runtime
這個包。(@babel/core
版本為7.20.12
)
ok,來看看我們的入口檔案,這時不再手動引入regenerator-runtime
這個包:
const sleep = async function() {
setTimeout(() => console.log('get up'), 1000);
}
sleep();
編譯出包以後在瀏覽器執行,得到跟上述手動引入regenerator-runtime
這個包一模一樣的效果:
我們再看看Babel
編譯後的檔案:
我們會發現,regenerator-runtime
包裡的內容,會以區域性變數的方式內聯注入到我們的程式碼中,這樣我們就不需要全域性提供一個regeneratorRuntime
物件了。
所以,在Babel >= 7.18.0
以後,我們直接import "core-js/stable";
就好
參考文章:
@babel/runtime
官方解釋:@babel/runtime
是一個包含Babel
模組化執行時助手的庫
在Babel
編譯的時候,會有一些輔助函式,這些函式就是ES6+
一些語法糖的實現,我們用這個案例 helper-functions 看看輔助函式是什麼。
我們用Babel
編譯一下class
這個語法糖:
class People {
constructor() {
}
}
const person = new Person();
編譯以後:
我們先看紅色框,它是Babel
編譯後的程式碼。我們會發現,編譯以後生成很多函式,並且會以內聯的方式插入到我們的程式碼中,這些函式就是我們說的輔助函式。
我們再看藍色框,它是@babel/runtime
的內容,它在node_modules/@babel/runtime/helpers
。
我們最後來看看白色框,會發現Babel
編譯後的輔助函式,都可以在@bable/runtime
裡面找到,所以@babel/runtime
是存放了Babel
輔助函式的一個集合包。
參考文章:@babel/runtime
@babel/plugin-transform-runtime
官方解釋:一個外掛,可以重用Babel
注入的幫助程式程式碼以節省程式碼大小
透過上面@babel/runtime
模組的瞭解,我們知道當我們使用了一些ES6+
的語法糖時,Babel
會生成一些輔助函式來編譯這些語法糖,並以內聯的方式插入到程式碼中。
那如果我們有10個檔案都用到了語法糖,那這些輔助函式,是不是會生成10次,並內聯插入10次呢?我們用這個案例 no-use-transform-runtime 來感受一下。
我們定義了三個檔案,每個檔案都用了class
這個語法糖。
// babel.config.js 配置檔案
const presets = [
'@babel/preset-env'
];
module.exports = {presets};
// Animal.js 檔案
export default class Animal {
constructor() {}
};
// Country.js 檔案
export default class Country {
constructor() {}
};
// index.js 檔案
import Animal from "./class/Animal";
import Country from "./class/Country";
class People {
constructor() {
}
};
const lMC = new People();
const cat = new Animal();
const usa = new Country();
最後打包出來檔案:
看看紅色的框框,我們會發現實現的方法都是一樣的,所以在每個使用到class
語法糖的檔案中,輔助函式都被生成並插入了一次,這些基本重複的程式碼,無疑是會大大增加我們的打包體積的。目前打包出來的體積是:6KB
。
為了解決上述的弊端,我們就得使用@babel/plugin-transform-runtime
外掛。從@babel/runtime
模組我們知道,它裡面存放了Babel
輔助函式的集合,@babel/plugin-transform-runtime
會將我們用到的輔助函式,從@babel/runtime
中以require
或者import
的方式,引入到我們的檔案中,實現複用,從而減小我們最終輸出包的體積。
所以@babel/runtime
跟@babel/plugin-transform-runtime
兩者通常是配合一起使用。
備註:@babel/plugin-transform-runtime
還有一個配置功能,本文不講配置,關於配置後續會有文章說明
我們用這個案例 use-transform-runtime 看看使用了@babel/plugin-transform-runtime
外掛以後有什麼變化。
我們的案例程式碼跟上述一樣,只是在babel.config.js
增加了@babel/plugin-transform-runtime
配置
// babel.config.js 配置檔案
// 增加了@babel/plugin-transform-runtime 配置
const plugins = [
'@babel/plugin-transform-runtime'
]
const presets = [
'@babel/preset-env'
];
module.exports = {plugins, presets};
// Animal.js 檔案
export default class Animal {
constructor() {}
};
// Country.js 檔案
export default class Country {
constructor() {}
};
// index.js 檔案
import Animal from "./class/Animal";
import Country from "./class/Country";
class People {
constructor() {
}
};
const lMC = new People();
const cat = new Animal();
const usa = new Country();
編譯跟打包後的檔案:
我們會發現:
- 輔助函式會以
require
引用的方式加到我們的程式碼中 - 打包後,輔助函式只用了一次,而且不是插入三次,很好的實現了複用
- 打包出來的體積也變成了
3KB
,很好的縮小了最後包的體積(不要小看縮小了3KB
,只是因為我用最簡單的方式寫了ES6+
語法,實際中我們專案肯定沒那麼簡單)
最後
因為Babel
的知識體系實在太大了,所以我們應該先把Babel
主要的幾個包弄清楚,再深入配置。關於Babel
的配置,會後續再出文章。
我們平常專案中Babel
用到的包,基本就是這篇文章中講解的幾個包,這篇文章算是十分詳細的介紹了這幾個包了。如果大家能把這幾個包弄得很清楚,Babel
的大部分知識也瞭解的差不多了。
如果之前對Babel
還有點懵懵的你,希望讀完這篇文章後,可以很好的理解Babel
大致是個什麼東西,也能更清楚的看懂官網寫的內容。
中間有用到Webpack
,我自己用Webpack5
搭了個腳手架 webpack5-boilerplate,如果你也想了解Webpack
的知識,也可以看看之前我寫的這篇文章—— webpack5最佳化的地方及搭建的一些體會。
如果覺得真的有幫助到,歡迎點贊收藏;如果有異同點,歡迎在評論區討論。