前言
文末有送書福利哦!css
若是近期你有關注 Vue 的動態,就能發現 Vue 做者最近一直在搗鼓的新工具 vite。vite 1.0 目前已經進入了 rc 版本,立刻就要正式發佈 1.0 的版本了。幾個月前,尤雨溪就已經在微博介紹過了 vite ,是一個基於瀏覽器原生 ESM 的開發服務器。html
![](http://static.javashuo.com/static/loading.gif)
早期 Webpack 剛出來的時候,是爲了解決低版本瀏覽器不支持 ESM 模塊化的問題,將各個分散的 JavaScript 模塊合併成一個文件,同時將多個 JavaScript 腳本文件合併成一個文件,減小 HTTP 請求的數量,有助於提高頁面首次訪問的速度。後期 Webpack 乘勝追擊,引入了 Loader、Plugin 機制,提供了各類構建相關的能力(babel轉義、css合併、代碼壓縮),取代了同期的 Browserify、Gulp。前端
現在,HTTP/2 的盛行,HTTP/3 也即將發行,再加上 5G 網絡的商用,減小 HTTP 請求數量起到的做用已經微乎其微,並且新版的瀏覽器基本已經支持了 ESM(<script module>
)。vue
![](http://static.javashuo.com/static/loading.gif)
上手 vite
vite 帶着它的歷史使命隨之出現。因爲省略了打包的過程,首次啓動 vite 的時候可謂秒開。能夠看下我錄製的 Gif 圖,徹底無需等待就能進入開發。node
![](http://static.javashuo.com/static/loading.gif)
想要嘗試 vite ,能夠直接經過以下命令:react
$ npm init vite-app <project-name>
$ cd <project-name>
$ npm install
$ npm run dev
npm init vite-app
命令會執行 npx create-vite-app
,從 npm 上拉取 create-vite-app 模塊,而後經過對應的模板生成模板文件到指定文件夾。webpack
{
"name": "vite-app",
"version": "0.0.1",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"vue": "^3.0.0-rc.1"
},
"devDependencies": {
"vite": "^1.0.0-rc.1",
"@vue/compiler-sfc": "^3.0.0-rc.1"
}
}
目前 vite 都是和 vue 3 搭配使用,若是要在 vue 2 使用 vite 估計還得等正式版發佈。固然,能上 vue 3 仍是上 vue 3 吧,不管性能、包大小還有 ts 加持方面,vue 3 都遠優於 vue 2 。除了 vue,vite 還提供了 react、preat 相關的模板。git
![](http://static.javashuo.com/static/loading.gif)
生成的 vue 項目的目錄結構以下。github
![](http://static.javashuo.com/static/loading.gif)
項目的入口爲 index.html
,html 文件中直接使用了瀏覽器原生的 ESM(type="module"
) 能力。關於瀏覽器 ESM 能力的介紹,能夠閱讀我以前的文章《前端模塊化的此生》。web
<script type="module" src="/src/main.js"></script>
全部的 js 文件通過 vite 處理後,其 import 的模塊路徑都會被修改,在前面加上 /@modules/
。當瀏覽器請求 import 模塊的時候,vite 會在 node_modules
中找到對應的文件進行返回。
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
createApp(App).mount('#app')
![](http://static.javashuo.com/static/loading.gif)
這樣就省略了打包的過程,大大提高了開發效率。固然 vite 也提供了生產模式,利用 Rollup 進行構建。
談談 snowpack
首次提出利用瀏覽器原生 ESM 能力的工具並不是是 vite,而是一個叫作 snowpack 的工具。snowpack 在發佈 1.0 以前,名字還叫作 @pika/web
。
![](http://static.javashuo.com/static/loading.gif)
pika 團隊之因此要作 snowpack ,是由於 pika 致力於爲 web 應用提速 90%。
![](http://static.javashuo.com/static/loading.gif)
因爲當前許多 web 應用都是在不一樣開源模塊的基礎上進行構建的,而這些開源模塊都被 webpack 之類的打包工具打成了一個包,若是這些開源模塊都來源於同一個 CDN 地址,且支持跨域緩存,那麼這些開源模塊都只須要加載一次,其餘網站用到了一樣的開源模塊,就不須要從新在下載,直接讀取本地緩存。
舉個例子,淘寶和天貓都是基於 react + redux + antd + loadsh 進行開發的,當我打開過淘寶以後,進入天貓這些開源模塊都不用從新下載,只須要下載天貓頁面相關的一些業務代碼便可。爲此,pika 專門創建了一個 CDN(skypack) 用了下載 npm 上的一些 esm 模塊。
後來 snowpack 發佈的時候,pika 團隊順便發表了一篇名爲《A Future Without Webpack》 的文章,告訴你們能夠嘗試拋棄 webpack,革 webpack 的命。
![](http://static.javashuo.com/static/loading.gif)
在 vite 的 README 中也提到了在某些方面參考了 snowpack,而且列舉了 vite 與 snowpack 的異同。
![](http://static.javashuo.com/static/loading.gif)
snowpack 如今已經發布到 v2 了,咱們能夠找到 v1 時期的源碼看看 snowpack 的早期實現。
源碼解析
在 github 上,根據 git tag 能夠找到 snowpack v1.0.0 的版本,下載下來發現好像有點 bug ,建議你們閱讀源碼的時候能夠跳到 v1.2.0(https://github.com/pikapkg/snowpack/tree/v1.2.0)。
在 package.json
中能夠看到,snowpack 經過他們團隊的 @pika/pack
進行打包,這個工具將打包流程進行了管道化,有點相似與 gulp,感興趣能夠了解了解,這裏重點仍是 snowpack 的原理。
{
"scripts": {
"build": "pika build"
},
// snowpack 的構建工具
"@pika/pack": {
"pipeline": [
[
"@pika/plugin-ts-standard-pkg"
],
[
"@pika/plugin-copy-assets"
],
[
"@pika/plugin-build-node"
],
[
"@pika/plugin-simple-bin",
{
// 經過 snowpack 運行命令
"bin": "snowpack"
}
]
]
}
}
這裏咱們以 vue 項目爲例,使用 snowpack 運行一個 vue 2 的項目。目錄結構以下:
![](http://static.javashuo.com/static/loading.gif)
若是要在項目中引入 snowpack,須要在項目的 package.json
中,添加 snowpack 相關的配置,配置中比較重要的就是這個 snowpack.webDependencies
,表示當前項目的依賴項,這兩個文件會被 snowpack 打包到 web_modules
目錄。
{
"scripts": {
"build": "snowpack",
"start": "serve ./"
},
"dependencies": {
"http-vue-loader": "^1.4.2",
"vue": "^2.6.12"
},
"devDependencies": {
"serve": "^11.3.2",
"snowpack": "~1.2.0"
},
"snowpack": {
"webDependencies": [
"http-vue-loader",
"vue/dist/vue.esm.browser.js"
]
}
}
運行 npm run build
以後,會新生成一個 web_modules
目錄,該目錄下的文件就是咱們在 snowpack.webDependencies
中聲明的兩個 js 文件。
![](http://static.javashuo.com/static/loading.gif)
![](http://static.javashuo.com/static/loading.gif)
snowpack 運行的時候,會調用源碼 src/index.ts
中的 cli 方法,該方法的代碼刪減版以下:
// 精簡了部分代碼,若是想看完整版建議去 github
// https://github.com/pikapkg/snowpack/blob/v1.2.0/src/index.ts
const cwd = process.cwd();
export async function cli(args: string[]) {
// 解析命令行參數
const { dest = 'web_modules' } = yargs(args);
// esm 腳本文件的輸出目錄,默認爲 web_modules
const destLoc = path.resolve(cwd, dest);
// 獲取 pkg.json
const pkgManifest: any = require(path.join(cwd, 'package.json'));
// 獲取 pkg.json 中的依賴模塊
const implicitDependencies = [
...Object.keys(pkgManifest.dependencies || {}),
...Object.keys(pkgManifest.peerDependencies || {}),
];
// 獲取 pkg.json 中 snowpack 相關配置
const { webDependencies } = pkgManifest['snowpack'] || {
webDependencies: undefined
};
const installTargets = [];
// 須要被安裝的模塊,若是沒有該配置,會嘗試安裝全部 dependencies 內的模塊
if (webDependencies) {
installTargets.push(...scanDepList(webDependencies, cwd));
} else {
installTargets.push(...scanDepList(implicitDependencies, cwd));
}
// 模塊安裝
const result = await install(installTargets, installOptions);
}
該方法會讀取項目的 package.json
文件,若是有 snowpack.webDependencies
配置,會優先安裝 snowpack.webDependencies
中聲明的模塊,若是沒有該配置,會把 dependencies
和 devDependencies
中的模塊都進行安裝。全部的模塊名都會經過 scanDepList
,轉化爲特定格式,而且會把glob
語法的模塊名,通過 glob
還原成單個的文件。
import path from 'path';
function createInstallTarget(specifier: string): InstallTarget {
return {
specifier,
named: [],
};
}
export function scanDepList(depList: string[], cwd: string): InstallTarget[] {
// 獲取 node_modules 路徑
const nodeModules = path.join(cwd, 'node_modules');
return depList
.map(whitelistItem => {
// 判斷文件名是否爲 glob 語法 (e.g. `vue/*.js`)
if (!glob.hasMagic(whitelistItem)) {
return [createInstallTarget(whitelistItem)];
} else {
// 轉換 glob 路徑
return scanDepList(glob.sync(whitelistItem,{cwd: nodeModules}), cwd);
}
})
// 將全部文件合併成一個數組
.reduce((flat, item) => flat.concat(item), []);
}
最後,全部的模塊會通過 install 進行安裝。
![](http://static.javashuo.com/static/loading.gif)
// 移除 .js、.mjs 後綴
function getWebDependencyName(dep: string): string {
return dep.replace(/\.m?js$/i, '');
}
// 獲取模塊的類型以及絕對路徑
function resolveWebDependency(dep: string): {
type: 'JS' | 'ASSET';
loc: string;
} {
var packagePattern = new RegExp('^(?:@([^/]+?)[/])?([^/]+?)$')
// 若是帶有擴展名,且非 npm 模塊,直接返回
if (path.extname(dep) && !packagePattern.test(dep)) {
const isJSFile = ['.js', '.mjs', '.cjs'].includes(path.extname(dep));
return {
type: isJSFile ? 'JS' : 'ASSET',
// 還原絕對路徑
loc: require.resolve(dep, {paths: [cwd]}),
};
}
// 若是是 npm 模塊,須要查找模塊對應的 package.json 文件
const manifestPath = `${cwd}/node_modules/${dep}/package.json`;
const manifestStr = fs.readFileSync(manifestPath, {encoding: 'utf8'});
const depManifest = JSON.parse(manifestStr);
// 而後讀取 package.json 中的 module屬性、browser屬性
let foundEntrypoint: string =
depManifest['browser:module'] || depManifest.module || depManifest.browser;
if (!foundEntrypoint) {
// 若是都不存在就取 main 屬性
foundEntrypoint = depManifest.main || 'index.js';
}
return {
type: 'JS',
// 還原絕對路徑
loc: path.join(`${cwd}/node_modules/${dep}`, foundEntrypoint),
};
}
// 模塊安裝
function install(installTargets, installOptions) {
const {
destLoc
} = installOptions;
// 使用 set 將待安裝模塊進行一次去重
const allInstallSpecifiers = new Set(installTargets.map(dep => dep.specifier));
// 模塊查找轉化
for (const installSpecifier of allInstallSpecifiers) {
// 移除 .js、.mjs 後綴
const targetName = getWebDependencyName(installSpecifier);
// 獲取文件類型,以及文件絕對路徑
const {type: targetType, loc: targetLoc} = resolveWebDependency(installSpecifier);
if (targetType === 'JS') {
// 腳本文件
const hash = await generateHashFromFile(targetLoc);
// 添加到腳本依賴對象
depObject[targetName] = targetLoc;
importMap[targetName] = `./${targetName}.js?rev=${hash}`;
installResults.push([installSpecifier, true]);
} else if (targetType === 'ASSET') {
// 靜態資源
// 添加到靜態資源對象
assetObject[targetName] = targetLoc;
installResults.push([installSpecifier, true]);
}
}
if (Object.keys(depObject).length > 0) {
// 經過 rollup 打包文件
const packageBundle = await rollup.rollup({
input: depObject,
plugins: [
// rollup 插件
// 這裏能夠進行一些 babel 轉義、代碼壓縮之類的操做
// 還能夠將一些 commonjs 的模塊轉化爲 ESM 模塊
]
});
// 文件輸出到 web_modules 目錄
await packageBundle.write({
dir: destLoc,
});
}
// 拷貝靜態資源
Object.entries(assetObject).forEach(([assetName, assetLoc]) => {
mkdirp.sync(path.dirname(`${destLoc}/${assetName}`));
fs.copyFileSync(assetLoc, `${destLoc}/${assetName}`);
});
return true;
}
基本原理已經分析完畢,下面看一看實際案例。咱們在 html 中經過 type="module"
的 script 標籤引入 index.js
做爲入口文件。
<!DOCTYPE html>
<html lang="en">
<title>snowpack-vue-httpvueloader</title>
<link rel="stylesheet" href="./assets/style.css">
<body>
<h1>snowpack - Vue Example</h1>
<div id="app"></div>
<script type="module" src="./js/index.js"></script>
</body>
</html>
而後在 index.js
中, import 在 webDependenies
中聲明的兩個 js 文件,而且在以前加上 /web_modules
。
import Vue from '/web_modules/vue/dist/vue.esm.browser.js'
import httpVueLoader from '/web_modules/http-vue-loader.js'
Vue.use(httpVueLoader)
new Vue({
el: '#app',
components: {
app: 'url:./components/app.vue',
},
template: '<app></app>',
})
最後經過 npm run start
,使用 serve
起一個 node 服務就能夠正常訪問了。
能夠看到 snowpack v1 的功能總體比較簡陋,只是將須要依賴的模塊從 node_modules 中提取到了 web_modules 中,中間經過 rollup 進行了一次編譯。這裏引入 rollup 主要是爲了對 js 代碼作一些壓縮優化,還有將某些 commonjs 的模塊轉化爲 ESM 的模塊。
可是最後還須要藉助第三方模塊來啓動 node 服務,當時官方還熱心的告訴你能夠選擇哪些第三方模塊來提供服務。
![](http://static.javashuo.com/static/loading.gif)
v2 版本已經支持內部啓用一個 node server 來開發,而不須要藉助,並且能夠進行熱更新。固然 v2 版本除了 js 模塊還提供了 css 模塊的支持。
vite 原理
在瞭解了 snowpack v1 的源碼後,再回過頭看看 vite 的原理。仍是按照以前的方式,追溯到 vite v0.1.1,代碼量較少的時候,看看 vite 的思路。
vite 在啓動時,內部會啓一個 http server,用於攔截頁面的腳本文件。
// 精簡了熱更新相關代碼,若是想看完整版建議去 github
// https://github.com/vitejs/vite/blob/a4f093a0c3/src/server/server.ts
import http, { Server } from 'http'
import serve from 'serve-handler'
import { vueMiddleware } from './vueCompiler'
import { resolveModule } from './moduleResolver'
import { rewrite } from './moduleRewriter'
import { sendJS } from './utils'
export async function createServer({
port = 3000,
cwd = process.cwd()
}: ServerConfig = {}): Promise<Server> {
const server = http.createServer(async (req, res) => {
const pathname = url.parse(req.url!).pathname!
if (pathname.startsWith('/__modules/')) {
// 返回 import 的模塊文件
return resolveModule(pathname.replace('/__modules/', ''), cwd, res)
} else if (pathname.endsWith('.vue')) {
// 解析 vue 文件
return vueMiddleware(cwd, req, res)
} else if (pathname.endsWith('.js')) {
// 讀取 js 文本內容,而後使用 rewrite 處理
const filename = path.join(cwd, pathname.slice(1))
const content = await fs.readFile(filename, 'utf-8')
return sendJS(res, rewrite(content))
}
serve(req, res, {
public: cwd,
// 默認返回 index.html
rewrites: [{ source: '**', destination: '/index.html' }]
})
})
return new Promise((resolve, reject) => {
server.on('listening', () => {
console.log(`Running at http://localhost:${port}`)
resolve(server)
})
server.listen(port)
})
}
訪問 vite 服務的時候,默認會返回 index.html。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
處理 js 文件
html 文件會請求 /src/main.js
, vite 服務在返回 js 文件的時候,會使用 rewrite
方法對 js 文件內容進行一次替換。
if (pathname.endsWith('.js')) {
// 讀取 js 文本內容,而後使用 rewrite 處理
const filename = path.join(cwd, pathname.slice(1))
const content = await fs.readFile(filename, 'utf-8')
return sendJS(res, rewrite(content))
}
// 精簡了部分代碼,若是想看完整版建議去 github
// https://github.com/vitejs/vite/blob/a4f093a0c3/src/server/moduleRewriter.ts
import { parse } from '@babel/parser'
export function rewrite(source: string, asSFCScript = false) {
// 經過 babel 解析,找到 import from、export default 相關代碼
const ast = parse(source, {
sourceType: 'module',
plugins: [
'bigInt',
'optionalChaining',
'nullishCoalescingOperator'
]
}).program.body
let s = source
ast.forEach((node) => {
if (node.type === 'ImportDeclaration') {
if (/^[^\.\/]/.test(node.source.value)) {
// 在 import 模塊名稱前加上 /__modules/
// import { foo } from 'vue' --> import { foo } from '/__modules/vue'
s = s.slice(0, node.source.start)
+ `"/__modules/${node.source.value}"`
+ s.slice(node.source.end)
}
} else if (asSFCScript && node.type === 'ExportDefaultDeclaration') {
// export default { xxx } -->
// let __script; export default (__script = { xxx })
s = s.slice(0, node.source.start)
+ `let __script; export default (__script = ${
s.slice(node.source.start, node.declaration.start)
})`
+ s.slice(node.source.end)
s.overwrite(
node.start!,
node.declaration.start!,
`let __script; export default (__script = `
)
s.appendRight(node.end!, `)`)
}
})
return s.toString()
}
html 文件請求 /src/main.js
, 通過 vite 處理後,結果以下:
- import { createApp } from 'vue'
+ import { createApp } from '/__modules/vue'
import App from './App.vue'
createApp(App).mount('#app')
![](http://static.javashuo.com/static/loading.gif)
處理 npm 模塊
瀏覽器解析完 main.js 以後,會讀取其中的 import 模塊,進行請求。請求的文件若是是 /__modules/
開頭的話,代表是一個 npm 模塊,vite 會使用 resolveModule
方法進行處理。
// fetch /__modules/vue
if (pathname.startsWith('/__modules/')) {
// 返回 import 的模塊文件
return resolveModule(pathname.replace('/__modules/', ''), cwd, res)
}
// 精簡了部分代碼,若是想看完整版建議去 github
// https://github.com/vitejs/vite/blob/a4f093a0c3/src/server/moduleResolver.ts
import path from 'path'
import resolve from 'resolve-from'
import { sendJSStream } from './utils'
import { ServerResponse } from 'http'
export function resolveModule(id: string, cwd: string, res: ServerResponse) {
let modulePath: string
modulePath = resolve(cwd, 'node_modules', `${id}/package.json`)
if (id === 'vue') {
// 若是是 vue 模塊,返回 vue.runtime.esm-browser.js
modulePath = path.join(
path.dirname(modulePath),
'dist/vue.runtime.esm-browser.js'
)
} else {
// 經過 package.json 文件,找到須要返回的 js 文件
const pkg = require(modulePath)
modulePath = path.join(path.dirname(modulePath), pkg.module || pkg.main)
}
sendJSStream(res, modulePath)
}
處理 vue 文件
main.js 除了獲取框架代碼,還 import 了一個 vue 組件。若是是 .vue
結尾的文件,vite 會經過 vueMiddleware
方法進行處理。
if (pathname.endsWith('.vue')) {
// 解析 vue 文件
return vueMiddleware(cwd, req, res)
}
// 精簡了部分代碼,若是想看完整版建議去 github
// https://github.com/vitejs/vite/blob/a4f093a0c3/src/server/vueCompiler.ts
import url from 'url'
import path from 'path'
import { parse, SFCDescriptor } from '@vue/compiler-sfc'
import { rewrite } from './moduleRewriter'
export async function vueMiddleware(
cwd: string, req, res
) {
const { pathname, query } = url.parse(req.url, true)
const filename = path.join(cwd, pathname.slice(1))
const content = await fs.readFile(filename, 'utf-8')
const { descriptor } = parse(content, { filename }) // vue 模板解析
if (!query.type) {
let code = ``
if (descriptor.script) {
code += rewrite(
descriptor.script.content,
true /* rewrite default export to `script` */
)
} else {
code += `const __script = {}; export default __script`
}
if (descriptor.styles) {
descriptor.styles.forEach((s, i) => {
code += `\nimport ${JSON.stringify(
pathname + `?type=style&index=${i}`
)}`
})
}
if (descriptor.template) {
code += `\nimport { render as __render } from ${JSON.stringify(
pathname + `?type=template`
)}`
code += `\n__script.render = __render`
}
sendJS(res, code)
return
}
if (query.type === 'template') {
// 返回模板
}
if (query.type === 'style') {
// 返回樣式
}
}
通過解析,.vue
文件返回的時候會被拆分紅三個部分:script、style、template。
// 解析前
<template>
<div>
<img alt="Vue logo" src="./assets/logo.png" />
<HelloWorld msg="Hello Vue 3.0 + Vite" />
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld.vue";
export default {
name: "App",
components: {
HelloWorld
}
};
</script>
// 解析後
import HelloWorld from "/src/components/HelloWorld.vue";
let __script;
export default (__script = {
name: "App",
components: {
HelloWorld
}
})
import {render as __render} from "/src/App.vue?type=template"
__script.render = __render
template 中的內容,會被 vue 解析成 render 方法。關於 vue 模板是如何編譯成 render 方法的,能夠看個人另外一篇文章:《Vue 模板編譯原理》。
import {
parse,
SFCDescriptor,
compileTemplate
} from '@vue/compiler-sfc'
export async function vueMiddleware(
cwd: string, req, res
) {
// ...
if (query.type === 'template') {
// 返回模板
const { code } = compileTemplate({
filename,
source: template.content,
})
sendJS(res, code)
return
}
if (query.type === 'style') {
// 返回樣式
}
}
![](http://static.javashuo.com/static/loading.gif)
而 template 的樣式
import {
parse,
SFCDescriptor,
compileStyle,
compileTemplate
} from '@vue/compiler-sfc'
export async function vueMiddleware(
cwd: string, req, res
) {
// ...
if (query.type === 'style') {
// 返回樣式
const index = Number(query.index)
const style = descriptor.styles[index]
const { code } = compileStyle({
filename,
source: style.content
})
sendJS(
res,
`
const id = "vue-style-${index}"
let style = document.getElementById(id)
if (!style) {
style = document.createElement('style')
style.id = id
document.head.appendChild(style)
}
style.textContent = ${JSON.stringify(code)}
`.trim()
)
}
}
style 的處理也不復雜,拿到 style 標籤的內容,而後 js 經過建立一個 style 標籤,將樣式添加到 head 標籤中。
小結
這裏只是簡單的解析了 vite 是如何攔截請求,而後返回須要的文件的過程,省略了熱更新的代碼。並且待發布 vite v1 除了啓動服務用來開發,還支持了 rollup 打包,輸出生產環境代碼的能力。
總結
vite 剛剛發佈的時候,還只能作 vue 的配套工具使用,如今已經支持了 JSX、TypeScript、Web Assembly、PostCSS 等等一系列能力。咱們就靜靜的等待 vue3 和 vite 的正式版發佈吧,到底能不能革了 webpack 的命,就看天意了。
對了,vite 和 vue 同樣,來自法語,中文是「快」的意思。
![](http://static.javashuo.com/static/loading.gif)
本公衆號將抽獎送出 5 本 《Web前端性能優化》,感謝機械工業出版社的贊助~
性能就像一條管道,數據流在管道流動的過程當中,哪裏容易堵塞,哪裏就存在瓶頸。
《Web前端性能優化》經過三大優化思惟+八處優化落點+40多個典型案例,教你輕鬆學會「大廠」的優化套路!
點擊下方小程序參與抽獎吧~
本文分享自微信公衆號 - 前端下午茶(qianduanxiawucha)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。