詳解vue-cli 2.0配置文件(小結(jié))
上次給大家分享的是用vue-cli快速搭建vue項目,雖然很省時間和精力,但想要真正搞明白,我們還需要對其原理一探究竟。
大家拿到一個項目,要快速上手,正確的思路是這樣的:
首先,如果在項目有readme.md的情況下,大家要先讀readme,項目的一些基本介紹,包括項目信息、運行的腳本、采用何種框架,以及項目維護者等信息通常都會有。一般在git上維護的項目都會有readme.md,不熟悉markdown語法的同學可以先了解下markdown入門。
第二步,要看package.json?,F(xiàn)代的前端項目中通常都會有package.json文件。在package.json里,會介紹項目名稱、版本、描述、作者、腳本、依賴包,對環(huán)境的要求,以及對瀏覽器要求。
{
"name": "uccn",
"version": "1.0.0",
"description": "uccn3.0",
"author": "v_yangtianjiao <v_yangtianjiao@baidu.com>",
"private": true,
// 這里的腳本是分析項目的主要入口
"scripts": {
"dev": "node build/dev-server.js",
"start": "node build/dev-server.js",
"build": "node build/build.js",
"jsonp": "node build/jsonp-server.js"
},
// 項目依賴
"dependencies": {
"fetch-jsonp": "^1.1.3",
"less": "^2.7.2",
"less-loader": "^4.0.4",
"stylus": "^0.54.5",
"stylus-loader": "^3.0.1",
"vue": "^2.4.2"
},
"devDependencies": {
"autoprefixer": "^7.1.2",
"babel-core": "^6.22.1",
"babel-loader": "^7.1.1",
"babel-plugin-component": "^0.10.1",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-preset-env": "^1.3.2",
"babel-preset-es2015": "^6.24.1",
"babel-preset-stage-2": "^6.22.0",
"babel-register": "^6.22.0",
"chalk": "^2.0.1",
"connect-history-api-fallback": "^1.3.0",
"copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.28.0",
"cssnano": "^3.10.0",
"eventsource-polyfill": "^0.9.6",
"express": "^4.14.1",
"extract-text-webpack-plugin": "^2.0.0",
"file-loader": "^0.11.1",
"friendly-errors-webpack-plugin": "^1.1.3",
"html-webpack-plugin": "^2.28.0",
"http-proxy-middleware": "^0.17.3",
"opn": "^5.1.0",
"optimize-css-assets-webpack-plugin": "^2.0.0",
"ora": "^1.2.0",
"rimraf": "^2.6.0",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"url-loader": "^0.5.8",
"vue-loader": "^13.0.4",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.4.2",
"webpack": "^2.6.1",
"webpack-bundle-analyzer": "^2.2.1",
"webpack-dev-middleware": "^1.10.0",
"webpack-hot-middleware": "^2.18.0",
"webpack-merge": "^4.1.0"
},
// 對node版本的以及npm版本的要求
"engines": {
"node": ">= 4.0.0",
"npm": ">= 3.0.0"
},
// 瀏覽器要求,vue項目不支持ie8,因為ie8是es3,尚沒有Object.defineProperty屬性
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}
上面的package.json是從實際vue項目中摘出來的,大家從package.json中就會對項目有一個大概的了解,最主要的是腳本部分。通過npm的自動化任務,可以很方便的執(zhí)行配置文件中的腳本。通過配置 "jsonp": "node build/jsonp-server.js",可以方便的使用npm run jsonp命令,代替node build/jsonp-server.js或者更復雜的一系列命令。詳細的npm自動化命令可以移步npm 自動化。

現(xiàn)在的項目目錄結(jié)構如上,我們從剛才的腳本入手。首先是啟服務的腳本npm run dev,實際上是執(zhí)行node build/dev-server.js,我們在build文件夾中找到dev-server.js,一步步分析。
/* eslint-disable */
// 首先檢查node和npm的版本
require('./check-versions')()
// 獲取配置文件中默認的配置
var config = require('../config')
// 如果node無法判斷當前是開發(fā)環(huán)境還是生產(chǎn)環(huán)境,則使用config.dev.env.NODE_ENV作為當前的環(huán)境
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
}
var opn = require('opn')// 用來在起來服務之后,打開瀏覽器并跳轉(zhuǎn)指定URL
var path = require('path')// node自帶文件路徑工具
var express = require('express')// node框架express(本地開發(fā)的核心,起服務)
var webpack = require('webpack')// webpack,壓縮打包
var proxyMiddleware = require('http-proxy-middleware')// 中間件
var webpackConfig = require('./webpack.dev.conf')// 開發(fā)環(huán)境的webpack配置
var mockMiddleware = require('../config/dev.mock')// 開發(fā)環(huán)境本地mock數(shù)據(jù)中間件
var port = process.env.PORT || config.dev.port
var autoOpenBrowser = !!config.dev.autoOpenBrowser
var proxyTable = config.dev.proxyTable
var app = express()// 起服務
var compiler = webpack(webpackConfig)// webpack進行編譯
// webpack-dev-middleware將編譯的文件放在內(nèi)存中,后續(xù)注入
var devMiddleware = require('webpack-dev-middleware')(compiler, {
publicPath: webpackConfig.output.publicPath,
quiet: true
})
// 熱加載
var hotMiddleware = require('webpack-hot-middleware')(compiler, {
log: false,
heartbeat: 2000
})
compiler.plugin('compilation', function (compilation) {
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
hotMiddleware.publish({ action: 'reload' })
cb()
})
})
// proxy api requests
// proxyTable中的配置掛載到express中
Object.keys(proxyTable).forEach(function (context) {
var options = proxyTable[context]
if (typeof options === 'string') {
options = { target: options }
}
app.use(proxyMiddleware(options.filter || context, options))
})
// 處理后退的時候匹配資源
app.use(require('connect-history-api-fallback')())
// 暫存在內(nèi)存的webpack編譯后的文件掛載到express上
app.use(devMiddleware)
// 將本地mock中間件掛載到express上
app.use(mockMiddleware);
// 熱加載掛載到express上
app.use(hotMiddleware)
// 拼static靜態(tài)資源文件路徑
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
// express為靜態(tài)資源提供服務
app.use(staticPath, express.static('./static'))
var uri = 'http://localhost:' + port
var _resolve
var readyPromise = new Promise(resolve => {
_resolve = resolve
})
console.log('> Starting dev server...')
devMiddleware.waitUntilValid(() => {
console.log('> Listening at ' + uri + '\n')
if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
opn(uri)
}
_resolve()
})
// 通過配置的端口,自動打開瀏覽器,并跳轉(zhuǎn)拼好的URL,至此,發(fā)開環(huán)境已經(jīng)跑起來了
var server = app.listen(port)
module.exports = {
ready: readyPromise,
close: () => {
server.close()
}
}
在上面的dev-server中,有很多變量來自于./config/index.js和webpack.dev.conf.js,我們一個個看上述配置文件。
首先看./config/index.js,這里是整個項目主要的配置入口,我們在代碼中一步步分析:
// node自帶路徑工具.
var path = require('path')
// 分為兩種環(huán)境,dev和production
module.exports = {
build: {
env: require('./prod.env'),// 使用config/prod.env.js中定義的編譯環(huán)境
index: path.resolve(__dirname, '../dist/index.html'),// 編譯輸入的index.html文件。node.js中,在任何模塊文件內(nèi)部,可以使用__filename變量獲取當前模塊文件的帶有完整絕對路徑的文件名,
assetsRoot: path.resolve(__dirname, '../dist'),// 編譯輸出的靜態(tài)資源路徑
assetsSubDirectory: 'static',// 編譯輸出的二級目錄
assetsPublicPath: './', // 編譯發(fā)布的根目錄,可配置為資源服務器或者cdn域名
productionSourceMap: false,//是否開啟cssSourceMap
productionGzip: false,// 是否開啟gzip
productionGzipExtensions: ['js', 'css'],// 需要用gzip壓縮的文件擴展名
bundleAnalyzerReport: process.env.npm_config_report
},
dev: {
env: require('./dev.env'),
port: 8989,// 起服務的端口
autoOpenBrowser: true,
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {},// 需要代理的接口,可以跨域
cssSourceMap: false
}
}
接著我們分析webpack.dev.conf.js:
var utils = require('./utils')// 工具類
var webpack = require('webpack')
var config = require('../config')
var merge = require('webpack-merge')// 使用webpack配置合并插件
var baseWebpackConfig = require('./webpack.base.conf')
var HtmlWebpackPlugin = require('html-webpack-plugin')// 這個插件自動生成HTML,并注入到.html文件中
var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
// 將hot-reload相對路徑添加到webpack.base.conf的對應的entry前面
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
})
// webpack.dev.conf.js與webpack.base.conf.js中的配置合并
module.exports = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
},
// webpack-devtool有7種模式,cheap-module-eval-source-map模式是比較快的開發(fā)模式
devtool: '#cheap-module-eval-source-map',
plugins: [
// 你可以理解為,通過配置了DefinePlugin,那么這里面的標識就相當于全局變量,你的業(yè)務代碼可以直接使用配置的標識。
new webpack.DefinePlugin({
'process.env': config.dev.env
}),
// hotModule插件讓頁面變動時,只重繪對應的模塊,不會重繪整個HTML文件
new webpack.HotModuleReplacementPlugin(),
// 在編譯出現(xiàn)錯誤時,使用 NoEmitOnErrorsPlugin 來跳過輸出階段。這樣可以確保輸出資源不會包含錯誤
new webpack.NoEmitOnErrorsPlugin(),
// 將生成的HTML代碼注入index.html文件
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
}),
// friendly-errors-webpack-plugin用于更友好地輸出webpack的警告、錯誤等信息
new FriendlyErrorsPlugin()
]
})
剛才的webpack.dev.conf.js中有引到webpack.base.conf.js,我們就把他們一網(wǎng)打盡,繼續(xù)看webpack.base.conf.js!
/* eslint-disable */
var path = require('path')// node自帶的文件路徑插件
var utils = require('./utils')// 工具類
var config = require('../config')// 上面說過的config/index
var vueLoaderConfig = require('./vue-loader.conf')// vue-loader.conf配置文件是用來解決各種css文件的,定義了諸如css,less,sass之類的和樣式有關的loader
// 此函數(shù)是用來返回當前目錄的平行目錄的路徑,
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
entry: {
uccn: './src/main.js'// 入口
},
output: {
// 路徑是config目錄下的index.js中的build配置中的assetsRoot,也就是dist目錄
path: config.build.assetsRoot,
filename: '[name].js',
// 上線地址,也就是真正的文件引用路徑,如果是production生產(chǎn)環(huán)境,其實這里都是 '/'
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
// resolve是webpack的內(nèi)置選項,顧名思義,決定要做的事情,也就是說當使用 import "jquery",該如何去執(zhí)行這件事情,就是resolve配置項要做的,import jQuery from "./additional/dist/js/jquery" 這樣會很麻煩,可以起個別名簡化操作
resolve: {
// 省略擴展名,比方說import index form '../js/index', 會默認去找index文件,然后找index.js,.vue,.josn.
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
// 使用上面的resolve函數(shù),意思是用@代替src的絕對路徑
'@': resolve('src'),
}
},
// 不同的模塊使用不同的loader
module: {
rules: [
{
// 對vue文件,使用vue-loader解析
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
// babel-loader把es6解析成es5
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test')]
},
{
// url-loader將文件大小低于下面option中l(wèi)imit的圖片,轉(zhuǎn)化為一個64位的DataURL,這樣會省去很多請求,大于limit的,按[name].[hash:7].[ext]的命名方式放到了static/img下面,方便做cache
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 20000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
// 音頻和視頻文件處理,同上
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
}
},
{
// 字體處理,同上
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
}
}
至此,npm run dev起本地開發(fā)環(huán)境相關的配置文件基本說完了,接著說一下上面都用到的util工具類:
var path = require('path')
var config = require('../config')
// extract-text-webpack-plugin該插件的主要是為了抽離css樣式,防止將樣式打包在js中引起頁面樣式加載錯亂的現(xiàn)象
var ExtractTextPlugin = require('extract-text-webpack-plugin')
// 返回資源文件路徑,path.posix以posix兼容的方式交互,是跨平臺的,如果是path.win32的話,只能在win上
exports.assetsPath = function (_path) {
var assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}
// 通過判斷是否是生產(chǎn)環(huán)境,配置不同的樣式語言的loader配置
exports.cssLoaders = function (options) {
options = options || {}
var cssLoader = {
loader: 'css-loader',
options: {
minimize: process.env.NODE_ENV === 'production',
sourceMap: options.sourceMap
}
}
// 生成各種loader配置,通過傳入不同的loader和option,將不同樣式文件語言的loader拼好,push到loader配置中。
function generateLoaders (loader, loaderOptions) {
var loaders = [cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// extract-text-webpack-plugin有三個參數(shù),use指需要用什么loader去編譯文件;fallback指編譯后用什么loader去提取文件;還有一個publicfile用來覆蓋項目路徑
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader'
})
} else {
return ['vue-style-loader'].concat(loaders)
}
}
// 對不同的樣式語言,返回相應的loader
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders('less'),
sass: generateLoaders('sass', { indentedSyntax: true }),
scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus')
}
}
// 生成處理不同的樣式文件處理規(guī)則
exports.styleLoaders = function (options) {
var output = []
var loaders = exports.cssLoaders(options)
for (var extension in loaders) {
var loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
return output
}
———————————————— 華麗的分隔符 —————————————————
下面我們繼續(xù)說npm run build,打包編譯的一系列操作~
從package.json 中可以看出,npm run build,其實是執(zhí)行了 node build/build.js,我們在build文件夾中找到build.js,build主要的工作是:檢測node和npm版本,刪除dist包,webpack構建打包,在終端輸出構建信息并結(jié)束,如果報錯,則輸出報錯信息。
require('./check-versions')()
process.env.NODE_ENV = 'production'
// 在終端顯示的旋轉(zhuǎn)器插件
var ora = require('ora')
// 用于刪除文件夾
var rm = require('rimraf')
var path = require('path')
// 終端文字顏色插件
var chalk = require('chalk')
var webpack = require('webpack')
var config = require('../config')
var webpackConfig = require('./webpack.prod.conf')
var spinner = ora('building for production...')
spinner.start()
// 刪除dist文件夾,之后webpack打包
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
webpack(webpackConfig, function (err, stats) {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n\n')
if (stats.hasErrors()) {
console.log(chalk.red(' Build failed with errors.\n'))
process.exit(1)
}
console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
})
})
build.js用到了webpack.prod.conf.js,他與webpack.base.conf.js merge之后,作為webpack配置文件,我們再看看webpack.prod.conf.js,主要做的工作是:
1.提取webpack生成的bundle中的文本,到特定的文件,使得css,js文件與webpack輸出的bundle分離。
2.合并基本的webpack配置
3.配置webpack的輸出,包括輸出路徑,文件名格式。
4.配置webpack插件,包括丑化代碼。
5.gzip下引入compression插件進行壓縮。
/* eslint-disable */
var path = require('path')
var utils = require('./utils')
var webpack = require('webpack')
var config = require('../config')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var CopyWebpackPlugin = require('copy-webpack-plugin')
var HtmlWebpackPlugin = require('html-webpack-plugin')
// 用于從webpack生成的bundle中提取文本到特定文件中的插件
// 可以抽取出css,js文件將其與webpack輸出的bundle分離
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
var env = config.build.env
// 合并基礎的webpack配置
var webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true
})
},
// 7中sourceMap上面有講過
devtool: config.build.productionSourceMap ? '#source-map' : false,
// 配置webpack輸出的目錄,及文件命名規(guī)則
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].min.js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
// webpack插件配置
plugins: [
// 同webpack.dev.conf.js
new webpack.DefinePlugin({
'process.env': env
}),
// 丑化代碼
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},
sourceMap: true
}),
// 抽離css文件到單獨的文件
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].min.css')
}),
new OptimizeCSSPlugin({
cssProcessorOptions: {
safe: true
}
}),
// 生成并注入index.html
new HtmlWebpackPlugin({
filename: config.build.index,
template: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: false,
removeAttributeQuotes: true
},
chunksSortMode: 'dependency'
}),
// keep module.id stable when vender modules does not change
new webpack.HashedModuleIdsPlugin(),
split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function (module, count) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
extract webpack runtime and module manifest to its own file in order to
prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor']
}),
copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
// gzip模式下需要引入compression插件進行壓縮
if (config.build.productionGzip) {
var CompressionWebpackPlugin = require('compression-webpack-plugin')
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'\\.(' +
config.build.productionGzipExtensions.join('|') +
')$'
),
threshold: 10240,
minRatio: 0.8
})
)
}
if (config.build.bundleAnalyzerReport) {
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
module.exports = webpackConfig
到此為止,vue官方腳手架工具vue-cli 2.0的所有配置文件都已介紹完畢,從頭到尾再梳理一遍:
執(zhí)行npm run dev或者npm run start,實際是在node環(huán)境執(zhí)行build/dev-server.js, dev-server.js會去拿到config中的端口等配置,通過express起一個服務,通過插件自動打開瀏覽器,加載webpack編譯后放在內(nèi)存的bundle。
執(zhí)行npm run build,實際上執(zhí)行了build/build.js,通過webpack的一系列配置及插件,將文件打包合并丑化,并創(chuàng)建dist目錄,放置編譯打包后的文件,這將是未來用在生產(chǎn)環(huán)境的包。
寫這篇文章我自身的收獲也挺多,第一是對vue-cli整體的認知更加清晰條理,第二是對webpack的一些插件有了新的認識。以前對一些插件模棱兩可,直接越過,這是不對的,要一步一個腳印兒,遇坑填坑,這樣才會有收獲。雖然過程可能是艱辛的,但收獲將會是巨大的~希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
vue3+ts如何通過lodash實現(xiàn)防抖節(jié)流詳解
loadsh是一個工具庫,我們通常使用loadsh的debounce函數(shù)處理防抖,下面這篇文章主要給大家介紹了關于vue3+ts如何通過lodash實現(xiàn)防抖節(jié)流的相關資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2022-08-08
vue3子組件上綁定(v-model="xx")父組件傳過來的值后報錯解決
這篇文章主要給大家介紹了關于vue3子組件上綁定(v-model="xx")父組件傳過來的值后報錯解決辦法,文中通過示例代碼介紹的非常詳細,對大家學習或者使用vue3具有一定的參考學習價值,需要的朋友可以參考下2023-07-07
vue封裝自定義指令之動態(tài)顯示title操作(溢出顯示,不溢出不顯示)
這篇文章主要介紹了vue封裝自定義指令之動態(tài)顯示title操作(溢出顯示,不溢出不顯示),具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-11-11
Vite打包優(yōu)化之縮小打包體積實現(xiàn)詳解
這篇文章主要為大家介紹了使用Vite縮小打包體積如何實現(xiàn)的示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-01-01
使用vue.js實現(xiàn)checkbox的全選和多個的刪除功能
這篇文章主要介紹了使用vue.js實現(xiàn)checkbox的全選和多個的刪除功能,需要的朋友可以參考下2017-02-02
vue?hash模式改成history,同時實現(xiàn)spa預渲染問題
這篇文章主要介紹了vue?hash模式改成history,同時實現(xiàn)spa預渲染問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-03-03

