您好,登錄后才能下訂單哦!
今天就跟大家聊聊有關(guān)如何自定義配置Angular CLI下的Webpack和loader處理,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。
使用Angular CLI新建工程后,一鍵式的配置已經(jīng)能滿足大部分需求,但針對(duì)個(gè)體述求,可能會(huì)希望給webpack配置一些額外的loader或者plugins?!鞠嚓P(guān)教程推薦:《angular教程》】
angular.json 暴露了多種Builder可以替換的接口,如果需要使用自定義webpack配置可以替換一下builder。 @angular-builders/custom-webpack
和 ngx-build-plus
都提供了對(duì)應(yīng)的builder,查看npm的趨勢(shì)custom-webpack用戶比較多,這里以custom-webpack為例,介紹如何修改angular.json以用上自定義的webpack配置。
由于@angular-builders/custom-webpack
并不是ng官方的包,所以使用前都需要先安裝一下:
npm install @angular-builders/custom-webpack
不同的ng版本需要安裝對(duì)應(yīng)不同的版本的包, ng的大部分庫(kù)目前有一個(gè)約定俗成的好習(xí)慣,就是主版本號(hào)和ng的主版本號(hào)是能夠?qū)ι系?。比如使用的是ng12,那就用custom-webpack@12的版本。那么為什么需要這么多版本,原因是ng在自己的不同版本下的默認(rèn)使用的@angular-devkit/build-angular
包的內(nèi)容和結(jié)構(gòu)甚至schema結(jié)構(gòu)和位置可能會(huì)發(fā)生變化。對(duì)于custom-webpack來(lái)說(shuō)更多是是繼承build-angular的schema和代碼,并暴露webpack的修改入口,讓用戶不需要了解整個(gè)webpack配置的情況下局部配置自己想要的功能。
在angular.json文件中,替換@angular-devkit/build-angular
為@angular-builders/custom-webpack
, 主要包括browser、dev-server、karma等幾個(gè)不同環(huán)節(jié)的builder,并增加配置參數(shù)
"build": { "builder": "@angular-builders/custom-webpack:browser", "options": { // 以下為新增的配置 customWebpackConfig "customWebpackConfig": { "path": "scripts/extra-webpack.config.js" }, .... }, "configurations": ... },
path可以按自己的工程來(lái)指定。 該文件可以導(dǎo)出一個(gè)函數(shù)(將會(huì)被調(diào)用)或者一段webpack配置(將會(huì)被Merge Options)。
從使用情況來(lái)說(shuō)函數(shù)靈活性更好,可以直接操作整個(gè)webpack配置。示例文件內(nèi)容
// extra-webpack.config.js module.exports = (config) => { // do something.. return config; };
至此,webpack的擴(kuò)展配置所需要的基礎(chǔ)步驟就完成了。
組件庫(kù)主題化采用了css-var方案進(jìn)行主題化定制,通過(guò)運(yùn)行時(shí)替換樣式:root里的css自定義屬性的值來(lái)達(dá)到變更主題色的功能。對(duì)于IE來(lái)說(shuō)它不認(rèn)識(shí)也無(wú)法解析帶var的值,那么它會(huì)表現(xiàn)為無(wú)顏色。為了盡量滿足漸進(jìn)增強(qiáng)和優(yōu)雅退化。我們需要做一些兼容,以便IE無(wú)法使用主題化的情況下也能正常顯示顏色。
目標(biāo):
color: var(--devui-brand, #5e7ce0); -> color: #5e7ce0; color: var(--devui-brand, #5e7ce0);
上下文:
為了規(guī)范顏色的使用,庫(kù)里使用的是scss變量來(lái)約束。如$devui-brand: var(--devui-brand, #5e7ce0)
, 本身這種寫(xiě)法是能滿足現(xiàn)代瀏覽器的降級(jí)的,當(dāng)找不到--devui-brand的css自定義屬性,會(huì)回落到后面的色值,但是IE不認(rèn)識(shí)var所以無(wú)法讀出色值。組件的樣式文件引用是定義文件然后直接使用$devui-brand
作為值,如下
@import '~ng-devui/styles-var/devui-var.scss'; .custom-class { color: $devui-brand; }
默認(rèn)編譯完為:
.custom-class { color: var(--devui-brand, #5e7ce0); }
既然已經(jīng)知道目標(biāo)了,那么這件事情就變得簡(jiǎn)單多了,通過(guò)插樁(console.log)查看默認(rèn)NG工程啟動(dòng)的webpack配置,可以看到module里有兩個(gè)rule是負(fù)責(zé)處理SCSS和SASS文件的,
它們都擁有test: /\.scss$|\.sass$/
字段,一個(gè)負(fù)責(zé)全局的scss的編譯(通過(guò)include字段指定了配置在angular.json的style的路徑集合),一個(gè)負(fù)責(zé)全局以外的組件內(nèi)引用的scss的處理(通過(guò)exclude字段排除了前面全局已經(jīng)處理過(guò)的scss)。
通常第一個(gè)想法可能是處理sass,遇到$devui-brand
的地方前面插入一句它的原始值。但是由于sass變量本身可能被二次賦值,如$my-brand: $devui-brand; color: $my-brand;
,這時(shí)候遇到$devui-brand
的就插值的顯然不合適,重復(fù)的定義$my-brand只是會(huì)最后一個(gè)值生效。
換個(gè)思路,當(dāng)scss展開(kāi)為css之后,每個(gè)取值的位置就是確定的了,哪怕二次賦值的地方也是同一個(gè)終值了。這時(shí)候就可以采用腳本來(lái)寫(xiě)IE的降級(jí),也就是目標(biāo)所寫(xiě)的內(nèi)容。
那么,我們可以再sass-loader處理完之后增加一個(gè)loader來(lái)處理這段css。對(duì)css的處理使用PostCSS能對(duì)語(yǔ)法結(jié)構(gòu)進(jìn)行走查更嚴(yán)謹(jǐn)。
最后修改代碼如下:
// webpack-config-add-theme.js function webpackConfigAddThemeSupportForIE(config) { [{ ruleTest: /\.scss$|\.sass$/, loaderName: 'sass-loader' }, { ruleTest: /\.less$/, loaderName: 'less-loader' }].forEach(({ruleTest, loaderName}) => { config.module.rules.filter(rule => rule.test + '' === ruleTest + '').forEach((styleRule) => { if (styleRule) { var insertPosition = styleRule.use.findIndex(loaderUse => loaderUse.loader === loaderName || loaderUse.loader === require.resolve(loaderName)); if (insertPosition > -1) { styleRule.use.splice(insertPosition, 0, { loader: 'postcss-loader', options: { sourceMap: styleRule.use[insertPosition].options.sourceMap, plugins: () => { return [ require('./add-origin-varvalue'), ]; } } }); } } }); }); return config; }; module.exports = webpackConfigAddThemeSupportForIE;
代碼大致邏輯為尋找test為less/sass正則的rule,在對(duì)應(yīng)的use里的loader里找到less-loader/sass-loader的位置,然后在其數(shù)組位置前面增加一個(gè)postcss-loader,loader里使用了自定義的add-origin-varvalue的PostCSS插件。(備注:這里有一塊邏輯是找到sass-loader的位置, 這里有兩個(gè)等式是因?yàn)閚g7,8和ng9用戶的loader寫(xiě)法不一樣了,之前ng7用字符串,后面ng9用的是文件路徑)
PostCSS插件如下:
var postcss = require('postcss'); var varStringJoinSeparator = 'devui-(?:.*?)'; var cssVarReg = new RegExp('var\\(\\-\\-(?:' + varStringJoinSeparator + '),(.*?)\\)', 'g'); module.exports = postcss.plugin('postcss-plugin-add-origin-varvalue', () => { return (root) => { root.walkDecls(decl => { if (decl.type !== 'comment' && decl.value && decl.value.match(cssVarReg)) { decl.cloneBefore({value: decl.value.replace(cssVarReg, (match, item) => item) }); } }); } });
代碼的大致邏輯如下,通過(guò)postcss.plugin定義了一個(gè)插件,該插件遍歷css每一條declarion(聲明),如果不是注釋,且它的值(對(duì)于每一條css聲明來(lái)說(shuō)冒號(hào)左邊稱為property,postcss里為decl.prop;右邊稱為value,postcss里為decl.value)剛好匹配了正則規(guī)則(這里的正則規(guī)則為--devui-開(kāi)頭),則在這條規(guī)則的前面插入該規(guī)則且把值替換為原規(guī)則逗號(hào)后面的值。
最后掛載到extra-webpack-config里
// extra-webpack.config.js const webpackConfigAddTheme = require('./webpack-config-add-theme'); module.exports = (config) => { return webpackConfigAddTheme(config); };
至此我們達(dá)成了我們的目標(biāo),而且對(duì)插值的范圍做了限定,限定為--devui
開(kāi)頭的才需要插值,避免其他不想被處理的var被處理了。
要點(diǎn):
找準(zhǔn)CSS處理的位置, sass存在變量依賴問(wèn)題,更適合在編譯后的css文件里處理
掌握PostCss插件的簡(jiǎn)單寫(xiě)法, sourceMap選項(xiàng)維持不變
注意loader的處理順序,是從use里的最后一個(gè)loader接收原始數(shù)據(jù)不斷往前面的loader傳遞,最前面的loader負(fù)責(zé)了最后內(nèi)容的呈現(xiàn)。
組件庫(kù)的demo對(duì)組件的引用,我們通過(guò)tsconfig里的alias實(shí)現(xiàn)了ts的別名引用,并在網(wǎng)站生產(chǎn)構(gòu)建階段采用了分開(kāi)構(gòu)建,先構(gòu)建庫(kù),然后配置另外的tsconfig指向了構(gòu)建完的庫(kù)(不再直接指向源碼)。
一方面使得demo看起來(lái)用法和業(yè)務(wù)一致,另一方面分開(kāi)構(gòu)建實(shí)現(xiàn)生產(chǎn)端組件庫(kù)的demo的使用方法和業(yè)務(wù)使用方法完全一致,減少因?yàn)閣ebpack構(gòu)建和ng-packagr構(gòu)建出來(lái)后一些細(xì)微差別導(dǎo)致問(wèn)題沒(méi)有提前暴露出來(lái)。
這些通過(guò)tsconfig和配置build的不同的configuration已經(jīng)可以實(shí)現(xiàn)了,但是僅僅只適用于ts文件,導(dǎo)出的scss文件/less文件就不生效了(由于支持外部主題化變量使用,scss文件和less文件會(huì)導(dǎo)出)。
目標(biāo): sass、less文件實(shí)現(xiàn)ts別名一樣的引用路徑。
上下文:
現(xiàn)有angular.json里配置了兩個(gè)configuration,一個(gè)是使用默認(rèn)的tsconfig.app.json,一個(gè)是分開(kāi)構(gòu)建的tsconfig.app.separate.json。
angular.json如下:
tsconfig.app.json 繼承了tsconfig.json有如下別名配置
{ .... "compilerOptions":{ "paths": { "ng-devui": ["devui/index.ts"], "ng-devui/*": ["devui/*"] } } ... }
tsconfig.app.separate.json又繼承了tsconfig.app.json并且覆寫(xiě)了path字段,
{ .... "compilerOptions":{ "paths": { "ng-devui": ["./publish"], "ng-devui/*": ["./publish/*"] } } ... }
所以當(dāng)npm run start
(ng serve
)的時(shí)候,會(huì)直接從ts目錄讀取文件,直接走webpack構(gòu)建,編譯速度快;
當(dāng)npm run build:prod
(ng build --prod --configuration separate
)的時(shí)候,會(huì)從組件構(gòu)建的目錄./publish/
下找尋npm包同目錄結(jié)構(gòu)的組件。
以上就是整個(gè)不同環(huán)境采用不同ts配置達(dá)到不同的構(gòu)建,可以看出來(lái)ts別名在這里起到非常大的作用。
然而我們的npm二方庫(kù)的包里面還導(dǎo)出了.scss 和 .less 文件。在demo里我們可以非常簡(jiǎn)單的用ts別名‘ng-devui’引用 ./devui
目錄的文件,在生產(chǎn)打包又會(huì)自動(dòng)引用 ./publish
目錄下的組件非常方便。
和業(yè)務(wù)側(cè)在代碼里引用node_modules目錄下的文件是一樣的寫(xiě)法,最后構(gòu)建也是一樣的編譯路徑,屏蔽了這一層差異。但是sass和less文件卻不支持再引用包里的變量,原因是,當(dāng)ts文件請(qǐng)求了sass文件,這一層的路徑處理是webpack處理的,但是sass-loader接手之后(less-loader也是同理,這里僅直接說(shuō)sass-loader),sass內(nèi)部對(duì)sass文件引用的處理是sass-loader去啟動(dòng)一個(gè)sass編譯器實(shí)例編譯拿到的sass文件內(nèi)容,該sass編譯器實(shí)例也直接處理了sass文件之間的引用。
好在sass-loader其實(shí)提供了一個(gè)importer的配置option,這里可以弄點(diǎn)文章。
importer提供了同步和異步的api,考慮不阻塞我們采用異步的api function(url, prev, done)
,它可以直接返回內(nèi)容{content: string}
或者返回文件的實(shí)際路徑{file: string}
,而且它規(guī)定了如果返回null則代表這個(gè)importer里找不到,它會(huì)繼續(xù)鏈?zhǔn)秸{(diào)用查找其他importer。
這個(gè)是一個(gè)很關(guān)鍵的點(diǎn)。
通過(guò)走讀sass-loader本身的代碼,我們可以看到傳給sass-loader的importer會(huì)和它內(nèi)置的波浪線(~
)的importer合并,見(jiàn)代碼1,代碼2。
也就是說(shuō),我們可以實(shí)現(xiàn)自己的importer的同時(shí),仍然保留sass-loader內(nèi)置的波浪線解析到node_module的語(yǔ)法糖。
和案例1的思路一樣,我們可以通過(guò)webpack配置的module的rules里找到sass-loader,并給它的options的sassOptions傳入一個(gè)importer的數(shù)組,這樣就可以完成importer的插入。
那么下一個(gè)問(wèn)題就是,我們?cè)趺磸倪\(yùn)行時(shí)拿出對(duì)應(yīng)的別名路徑映射過(guò)去?
Angular在編譯的時(shí)候,有一個(gè)AngularCompilerPlugin
(require('@ngtools/webpack').AngularCompilerPlugin
)會(huì)用于處理angular.json的build不同configuration下對(duì)應(yīng)的tsconfig文件路徑,Angular Compiler CLI內(nèi)又導(dǎo)出了一個(gè)readConfiguration
函數(shù)(require('@angular/compiler-cli').readConfiguration
)用于解析路徑下的tsconfig下的最后真實(shí)的配置。
tsconfig的描述文件是可以具有擴(kuò)展功能的,可以拓展另一個(gè)tsconfig文件,readConfiguration幫我們解決了擴(kuò)展過(guò)的tsconfig的合并問(wèn)題。這樣就能拿到當(dāng)前運(yùn)行環(huán)境對(duì)應(yīng)的tsconfig里面的path別名配置了。
下一步就是簡(jiǎn)單的取出path數(shù)據(jù)進(jìn)行一個(gè)簡(jiǎn)單的映射,保留波浪線的規(guī)則,我們把~ng-devui
在本地開(kāi)發(fā)時(shí)候映射到./devui
的,在生成打包時(shí)映射到./publish
目錄,在用戶側(cè)時(shí)候的時(shí)候會(huì)引用來(lái)自node_modules的。
最后代碼如下:
// tsconfig-alias-importer.js const path = require('path'); const readConfiguration = require('@angular/compiler-cli').readConfiguration; function pathAlias(tsconfigPath) { const {baseUrl, paths} = readConfiguration(path.resolve(tsconfigPath)).options; if (!paths) { return []; } return Object.keys(paths) .filter(alias => alias.endsWith('/*')) .map(alias => ( {alias: alias, paths:paths[alias]} )) .map(rule => ({ aliasReg: new RegExp('^~' + rule.alias.replace(/\/\*$/,'/(.*?)')), pathPrefixes: rule.paths.map(pathname => path.resolve(baseUrl || '' , pathname.replace(/\*$/,''))) })); } module.exports = function getTsconfigPathAlias(tsconfigPath = 'tsconfig.json') { try { const rules = pathAlias(tsconfigPath); // 匹配的情況下給出文件 return function importer(url, prev, done) { if (!rules || rules.length === 0) { return null; } for (let rule of rules) { if (rule.aliasReg.test(url)) { // 暫時(shí)只支持第一個(gè)alias地址,其他的忽略 const prefix = rule.pathPrefixes[0]; const filename = path.resolve(prefix, url.replace(rule.aliasReg, (item, match) => match)); return { file: filename}; } } return null; // 沒(méi)有匹配的返回null,以繼續(xù)使用下一個(gè)importer }; } catch (error) { console.warn('Sass alias importer might not effected', error); return function importer(url, prev, done) { return null; } } }
代碼的大體邏輯是pathAlias函數(shù)通過(guò)讀tsconfig里的baseUrl和path,過(guò)濾出/*
結(jié)尾的(因?yàn)榉?code>/*結(jié)尾的主要是指向index.ts的,不會(huì)代理到樣式),然后通過(guò)整合組裝成一條條正則和正則要替換的內(nèi)容,比如這條規(guī)則
"ng-devui/*": ["./devui/*"]
通過(guò)map轉(zhuǎn)換為
{ alias:"ng-devui/*", paths: ["./devui/"] }
進(jìn)一步轉(zhuǎn)換為
{ aliasReg: /^~ng-devui\/(.*?)/, pathPrefixes: "D:\\code\\ng-devui\devui" // 真實(shí)路徑,筆者此處用的是windows系統(tǒng) }
這里baseUrl最外層tsconfig指向了./
, 也就是工程的根目錄,最后pathResolve會(huì)解析為真實(shí)的路徑。
導(dǎo)出的getTsconfigPathAlias
這個(gè)sass的importer,假定我們有一個(gè)文件在./devui/styles-var/devui-var.scss
這個(gè)路徑,那么demo引用的時(shí)候可以使用~ng-devui/styles-var/devui-var.scss
, 函數(shù)將多個(gè)別名進(jìn)行挨個(gè)檢測(cè)匹配到了, 如果有url匹配到正則,比如目前demo這個(gè)引用地址匹配到了 /^~ng-devui\/(.*?)/
, 那么importer會(huì)返回{filename: "D:\\code\\ng-devui\\devui\\styles-var\\devui-var.scss"}
。這樣就能找到tsconfig別名里面配置的路徑別名,實(shí)現(xiàn)了sass文件引用的別名。
Tsconfig的path別名本身是可以回落到多個(gè)地址的,這里簡(jiǎn)化成只回落到第一個(gè)地址, 如果需要實(shí)現(xiàn)多個(gè)地址, 可能需要塞進(jìn)去多個(gè)importer或者在一個(gè)importer里面檢測(cè)文件是否存在,回落到第二個(gè)地址,第三個(gè)地址。 這時(shí)候再把這個(gè)importer塞到webpack配置的每個(gè)sass-loader里。
// webpack-config-sass-loader-importer.js const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin; const getTsConfigAlias = require('./get-tsconfig-alias'); function getAngularCompilerTsConfigPath(config) { const angularCompilerPlugin = config.plugins.filter(plugin => plugin instanceof AngularCompilerPlugin).pop(); if (angularCompilerPlugin) { return angularCompilerPlugin.options.tsConfigPath; } return undefined; } function webpackConfigSassImporterAlias(config) { const tsconfigPath = getAngularCompilerTsConfigPath(config) || 'tsconfig.json'; [{ ruleTest: /\.scss$|\.sass$/, loaderName: 'sass-loader' }].forEach(({ruleTest, loaderName}) => { config.module.rules.filter(rule => rule.test + '' === ruleTest + '').forEach((styleRule) => { if (styleRule) { var insertPosition = styleRule.use.findIndex(loaderUse => loaderUse.loader === loaderName || loaderUse.loader === require.resolve(loaderName)); if (insertPosition > -1) { styleRule.use[insertPosition].options.sassOptions.importer = [ getTsConfigAlias(tsconfigPath) ]; } } }); }); return config; } module.exports = webpackConfigSassImporterAlias;
這段代碼先從webpack的配置里找到AngualrCompilerPlugin插件,然后讀取它此時(shí)的tsconfig路徑。
以上是sass路徑別名的解決,得益于sass本身有一個(gè)importer,但是less上這個(gè)問(wèn)題就沒(méi)有那么好解決了,less只提供includePaths的選項(xiàng), 它會(huì)挨個(gè)遍歷去回落,并且是一視同仁的,即所有文件都會(huì)按這個(gè)includePaths去挨個(gè)嘗試。實(shí)際情況less是不支持波浪線的,但是less-loader卻又是支持波浪線語(yǔ)法的,走讀一下less-loader的代碼看看有沒(méi)有線索。可以看到less-loader用的寫(xiě)了一個(gè)WebpackFIleManagerment的plugin來(lái)做后綴名補(bǔ)充和利用webpack的resolve來(lái)做回落判斷。這個(gè)邏輯相對(duì)來(lái)說(shuō)就比較復(fù)雜了。
我們只能換個(gè)思路來(lái)解決這個(gè)問(wèn)題, less本身是有語(yǔ)法的,也就是我們能從語(yǔ)法中判定哪些是引用外部文件的,在引用之前我們可以處理一下路徑,比如把~ng-devui/styles-var/devui-var.less
處理成相對(duì)于less文件的../../styles-var/devui-var.less
那么less編譯器就能理解。
這是我們可以借用前面幾個(gè)案例的思路,在less-loader加載前增加一個(gè)loader,提前處理less語(yǔ)法里面的import引用語(yǔ)句,直接把波浪線地址替換成我們真實(shí)開(kāi)發(fā)環(huán)境或者生產(chǎn)打包環(huán)境的地址。
const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin; const pathAlias = require('./get-path-alias-from-tsconfig'); function getAngularCompilerTsConfigPath(config) { const angularCompilerPlugin = config.plugins.filter(plugin => plugin instanceof AngularCompilerPlugin).pop(); if (angularCompilerPlugin) { return angularCompilerPlugin.options.tsConfigPath; } return undefined; } function webpackConfigSassImporterAlias(config) { const tsconfigPath = getAngularCompilerTsConfigPath(config) || 'tsconfig.json'; [{ ruleTest: /\.less$/, loaderName: 'less-loader' }].forEach(({ruleTest, loaderName}) => { config.module.rules.filter(rule => rule.test + '' === ruleTest + '').forEach((styleRule) => { if (styleRule) { var insertPosition = styleRule.use.findIndex(loaderUse => loaderUse.loader === loaderName || loaderUse.loader === require.resolve(loaderName)); if (insertPosition > -1) { styleRule.use.splice(insertPosition + 1, 0, { loader: require.resolve('./less-alias-replacer-loader'), options: { aliasMap: pathAlias(tsconfigPath) } }); } } }); }); return config; } module.exports = webpackConfigSassImporterAlias;
這是webpack的修改,代碼大致意思是找到less-loader并在后面位置增加一個(gè)自定義的loader,并且把路徑別名從tsconfig的數(shù)據(jù)取出作為options傳給該loader。
pathAlias的寫(xiě)法就和前面sass-loader的是一樣的
// get-path-alias-from-tsconfig.js const path = require('path'); const readConfiguration = require('@angular/compiler-cli').readConfiguration; module.exports = function pathAlias(tsconfigPath) { const { baseUrl, paths } = readConfiguration(path.resolve(tsconfigPath)).options; if (!paths) { return []; } return Object.keys(paths) .filter(alias => alias.endsWith('/*')) .map(alias => ( { alias: alias, paths: paths[alias] } )) .map(rule => ({ aliasReg: new RegExp('^~' + rule.alias.replace(/\/\*$/, '/(.*?)')), pathPrefixes: rule.paths.map(pathname => path.resolve(baseUrl || '', pathname.replace(/\*$/, ''))) })); }
const path = require('path'); const { getOptions } = require('loader-utils'); const validateOptions = require('schema-utils'); const postcss = require('postcss'); const postcssLessSyntax = require('postcss-less'); const loaderName = 'less-path-alias-replacer-loader'; const trailingSlashAndContent = /[/\\][^/\\]*?$/; const optionsSchema = { type: 'object', properties: { aliasMap: { anyOf: [{ instanceof: 'Array' }, { enum: [ null ] }] }, }, additionalProperties: false } const defaultOptions = { } function getOptionsFromConfig(config) { const rawOptions = getOptions(config) if (rawOptions) { validateOptions(optionsSchema, rawOptions, loaderName); } return Object.assign({}, defaultOptions, rawOptions); } /** * * @param {*} css less文本內(nèi)容 * @param {*} aliasMap 別名規(guī)則集合 */ function lessReplacePathAlias(css, aliasMap, sourcePath) { const replacePathAlias = postcss.plugin('postcss-plugin-replace-path-alias', () => { return (root) => { root.walkAtRules(atRule => { if (atRule.import && atRule.filename) { const oFilename = atRule.filename.substring(1, atRule.filename.length - 1); // 去掉頭尾單引號(hào)雙引號(hào) const rule = aliasMap.filter(rule => rule.aliasReg.test(oFilename)).pop(); if (rule) { const prefix = rule.pathPrefixes[0]; // 取第一個(gè)路徑忽略剩余的 const filename = path.resolve(prefix, oFilename.replace(rule.aliasReg, (item, match) => match)); const relativePath = path.relative(sourcePath.replace(trailingSlashAndContent, ""), filename).split(path.sep).join('/'); var realPathAtRule = atRule.clone({ params: (atRule.options || '' ) + " '" + relativePath + "'", filename: "'" + relativePath + "'"}); atRule.replaceWith(realPathAtRule); } } }); } }); return postcss([replacePathAlias]).process(css, { syntax: postcssLessSyntax }).css; } function process(source, map) { this.cacheable && this.cacheable(); // 獲取配置文件里的主題數(shù)據(jù) const aliasMap = getOptionsFromConfig(this).aliasMap; let newSource = source; if (aliasMap.length > 0) { newSource = lessReplacePathAlias(source, aliasMap, this.resourcePath); } // 返回結(jié)果 this.callback(null, newSource, map); return newSource; } exports.default = process;
這里自定義一個(gè)wepack-loader的寫(xiě)法,實(shí)際上也是可以用postcss-loader搭配自定義replacePathAlias 的plugin 和 postcss-less的syntax進(jìn)行使用。這里演示了wepack-loader的寫(xiě)法。
代碼大意是定義了一個(gè)loader的optionSchema用于校驗(yàn)選項(xiàng),process函數(shù)獲取option里的路徑別名數(shù)據(jù)之后,如果路徑別名有數(shù)據(jù)則用postcss對(duì)代碼進(jìn)行處理。注意在process里this指向webpack的上下文,所以可以從resourcePath里獲取當(dāng)前文件路徑。
代碼核心為中間的postcss插件, 通過(guò)遍歷@開(kāi)頭的規(guī)則,如果是一個(gè)import聲明則讀取文件名去掉頭尾的單雙引號(hào);測(cè)試是否文件名命中了規(guī)則中的任意一條,命中則取第一條,和sass-loader處理的一樣得到了文件的絕對(duì)路徑,然后通過(guò)path.relative重新計(jì)算出和當(dāng)前文件的相對(duì)路徑。然后將這條@import規(guī)則替換成新的文件地址。
實(shí)際上這個(gè)思路同樣適用于sass規(guī)則的處理,只需要把語(yǔ)法syntax換成postcss-sass。
我們可以看到前兩個(gè)大案例都是在處理css類問(wèn)題的,大部分時(shí)候處理都可以用上postcss利器。直接去操作css內(nèi)容容易誤修改內(nèi)容,而經(jīng)過(guò)AST語(yǔ)法樹(shù)拆解后的遍歷會(huì)更穩(wěn)當(dāng)一些。
說(shuō)一下為什么會(huì)同時(shí)使用sass和less。一般工程是不會(huì)同時(shí)使用兩種的。實(shí)際上我們的工程主要也是使用sass。但是對(duì)于一個(gè)打包后的組件庫(kù)來(lái)說(shuō),業(yè)務(wù)使用的時(shí)候是不會(huì)感知它是sass還是less的,甚至也不會(huì)提供sass或者less文件給業(yè)務(wù)引用。這里是為了主題化的能力能夠?qū)ν廨椛?,組件庫(kù)同時(shí)提供了sass和less的版本變量供使用。
這兩個(gè)樣式編譯器loader支持路徑別名的腳本最早寫(xiě)于2020年8月。
Webpack官方在sass-loader/less-loader的使用文檔里面都說(shuō)明了,~語(yǔ)法已經(jīng)廢除,建議刪除。
sass-loader@11.0.0(2021-2-5),less-loader@8.0.0(2021-2-1)分別發(fā)布了對(duì)應(yīng)版本聲明~已經(jīng)標(biāo)記為Deprecated。
作為歷史解決方案,這個(gè)案例依然會(huì)放在這里,提供一些解決思路。
筆者認(rèn)為能~和現(xiàn)在的回落解決方案還是不一樣的,尤其當(dāng)存在同名文件的時(shí)候(目前這種情況會(huì)比較少,少有人使用模塊同名路徑作為相對(duì)目錄路徑),波浪線方案仍然能明顯強(qiáng)調(diào)出文件的第一指向。
Webpack官方在2020年8月底開(kāi)始給less-loader也加上了webpackImporter的選項(xiàng),進(jìn)一步屏蔽less-loader和sass-loader之間的差異。兼容歷史原因,這兩個(gè)loader目前還會(huì)保留波浪線語(yǔ)法。由于項(xiàng)目ng版本滯后于NG官方版本,NG官方版本使用的loader又滯后于webpack官方版本。目前NG9版本仍在用less-loader@5.0.0,sass-loader@8.0.2。
要點(diǎn):
從運(yùn)行時(shí)獲取tsconfig配置項(xiàng)
文件路徑處理(scss、less)
掌握l(shuí)oader的寫(xiě)法,接收參數(shù)。
Webpack配置中 config.resolve.alias 也是一個(gè)配置別名的地方, 而且sass-loader/less-loader也支持了webpackImporter的配置,其實(shí)可以直接通過(guò)修改config中的alias就能達(dá)成目的。 比如:
config.resolve.alias = { ... config.resolve.alias, 'ng-devui': path.resolve('./devui/'), })
那么如果webpack的resolve alias已經(jīng)支持了,是不是tsconfig就可以不用配置,或者怎么配置成一份?
不幸的是 Angular工程的 webpack 和 tsconfig不是同步的,兩邊需要同時(shí)配置,否則會(huì)出現(xiàn)找不到模塊的構(gòu)建錯(cuò)誤, 如果兩者配置不同,隱患會(huì)更大,因?yàn)閠sconfig的配置是直接影響tsc編譯器的拿不到文件,webpack也會(huì)解讀出一份等。
配置成一份可以通過(guò)寫(xiě)webpack-plugin在運(yùn)行時(shí)拿到當(dāng)前的tsconfig再處理數(shù)據(jù)塞到alias里。因?yàn)槿绻苯有薷腸onfig,那么它是靜態(tài)的,實(shí)際情況還是需要獲取到ng工程當(dāng)前的ts配置是什么文件會(huì)比較好。
Awesome-typescript-loader有個(gè)TsConfigPathsPlugin,使用也非常簡(jiǎn)單。
const { TsConfigPathsPlugin } = require('awesome-typescript-loader'); module.exports = (config) => { config.resolve.plugins = [ ... (config.resolve.plugins || []), new TsConfigPathsPlugin() ] };
給resolve.plugins塞一個(gè)TsConfigPathsPlugin就可以把tsconfig里的,不過(guò)這個(gè)插件已經(jīng)存檔了,最后一個(gè)版本是3年前的了。這里我們做了一個(gè)測(cè)試,仍然是不支持動(dòng)態(tài)的tsconfig,它的代碼可以參考用來(lái)寫(xiě)一個(gè)resolve的plugin, 動(dòng)態(tài)的tsconfig的path獲取可以參考我們之前的操作。
Webpack config的resolve.alias和 tsconfig的alias不是同步的,需要配置兩份或者找到一個(gè)方法同步。
Sass-loader 和less-loader的內(nèi)部引用都能走webpack的resolve.alias, 可以說(shuō)resolve.alias的解法會(huì)比 在改sass-loader的importer或者在less-loader前先處理 通用性更好,基本上所有blob都可以嘗試去這樣解決路徑別名問(wèn)題。更多需要注意的仍然是動(dòng)態(tài)的tsconfig的配置獲取問(wèn)題。
組件庫(kù)的業(yè)務(wù)使用方剛升級(jí)ng7的時(shí)候,打包經(jīng)常出問(wèn)題,經(jīng)常出現(xiàn)文件搖樹(shù)之后,很多自執(zhí)行命令被搖樹(shù)認(rèn)為無(wú)副作用搖樹(shù)掉了。
目標(biāo):不關(guān)掉全局搖樹(shù)的情況下,針對(duì)個(gè)別目錄進(jìn)行搖樹(shù)的問(wèn)題排除。
上下文:
Angular.json里有個(gè)配置,默認(rèn)為打開(kāi),打開(kāi)之后可以對(duì)js類型的代碼進(jìn)行搖樹(shù)優(yōu)化,從而減小打包體積。
Terser是從Uglifyjs這個(gè)庫(kù)fork 出來(lái)的項(xiàng)目用來(lái)支持ES6語(yǔ)法,Angular 編譯階段用它來(lái)壓縮js代碼。
Angular使用TerserPlugin來(lái)進(jìn)行搖樹(shù),由于裝飾器等問(wèn)題導(dǎo)致?lián)u樹(shù)效果不理想的問(wèn)題(相關(guān)討論內(nèi)容見(jiàn)Angular搖樹(shù)如何工作),angular提供了一個(gè)專門(mén)用于標(biāo)記純函數(shù)的注釋的優(yōu)化器,所以對(duì)于默認(rèn)的Angular項(xiàng)目來(lái)說(shuō),搖樹(shù)是一個(gè)兩階段模型。
第一個(gè)階段是Angular在生產(chǎn)構(gòu)建階段(ng build --prod
)在解析資源的時(shí)候加入了一個(gè)@angular_devkit/build_optimizer/webpack-loader
,用于標(biāo)記js文件里的無(wú)用代碼;angular_devkit/build_optimizer 介紹它的主要功能為標(biāo)記/* PURE */功能。
第二個(gè)階段是Angular在生成打包的時(shí)候給Webpack配置了 optimization.minimizer數(shù)組塞入了TerserPlugin然后把無(wú)副作用的無(wú)引用代碼搖掉。(準(zhǔn)確說(shuō)是塞入了兩個(gè)Plugin,一個(gè)針對(duì)globalScript,一個(gè)排除globalScript。GlobalScript是指在angular.json的build的script字段配置的路徑。)
TerserPlugin本身是有一個(gè)include和exclude字段(見(jiàn)API)可以用正則和字符串來(lái)排除。
function terserOptionsWebpackConfig(config) { let excludeList = [ // 此處可以填自己要排除的目錄 ] let minimizerArr = config.optimization.minimizer; let terserPlugins = minimizerArr .filter(plugin => plugin.options && plugin.options.terserOptions) terserPlugins.forEach(terserPlugin => { if (terserPlugin.plugin.exclude) { const isArray = Array.isArray(terserPlugin.plugin.exclude) if (isArray) { terserPlugin.plugin.exclude = [ ...terserPlugin.plugin.exclude, ...excludeList ]; } else { terserPlugin.plugin.exclude = [ terserPlugin.plugin.exclude, ...excludeList ]; } } else { terserPlugin.plugin.exclude = excludeList; } }); return config; }; module.exports = terserOptionsWebpackConfig;
之前在ng7工程會(huì)遇到比較多的搖樹(shù)問(wèn)題,有些升級(jí)到ng9之后默認(rèn)配置有點(diǎn)變化之后就沒(méi)有搖樹(shù)問(wèn)題了。包括IVY打包模式下編譯引擎實(shí)際上搖樹(shù)的方式不太一樣,有些文件不會(huì)被搖掉了,這個(gè)問(wèn)題還是要遇到具體問(wèn)題具體分析來(lái)解決。必要時(shí)可以重新new一個(gè)TerserPlugin但是要保持它的原來(lái)的options不變。
要點(diǎn):
搖樹(shù)具體的階段,攔截問(wèn)題
維持原有的Options,不對(duì)其進(jìn)行破壞。
highlight.js升級(jí)到10.0.0之后,官方就開(kāi)始不再默認(rèn)支持ie11了,導(dǎo)出的包也只有es2015的包。之前的業(yè)務(wù)需要為了兼容IE11,我們需要對(duì)highlight.js進(jìn)行一次babel。更新到ng9之后,其實(shí)ng默認(rèn)會(huì)僅打包es2015然后進(jìn)行差分打包到es5,所以實(shí)際情況在生產(chǎn)打包是沒(méi)有問(wèn)題的,但是在本地開(kāi)發(fā)的ie11調(diào)試環(huán)節(jié)會(huì)出現(xiàn)問(wèn)題,比如class的語(yǔ)法ie不認(rèn)識(shí)等等,導(dǎo)致整個(gè)js無(wú)法加載。
目標(biāo):讓不支持es5的包在開(kāi)發(fā)態(tài)支持es5。
上下文:由于之前組件庫(kù)9的版在一段時(shí)間內(nèi)仍然需要支持ie11,也就經(jīng)常需要開(kāi)發(fā)態(tài)下到ie11下debug,所以開(kāi)發(fā)態(tài)下的highlight.js導(dǎo)致ie無(wú)法訪問(wèn)問(wèn)題需要解決。
首先es2015轉(zhuǎn)es5經(jīng)典的做法就是讓babel幫忙處理。然后大部分的ES語(yǔ)法新增的api可以由core-js來(lái)解決,剩下的IE11還有一些瀏覽器端DOM的API的實(shí)現(xiàn)還需要添加一些polyfill才行。Polyfill可以用到了什么api就加什么api,具體可以從這里參考,本文不再累述。
// babel-loader-wepack-config.js const path = require('path'); const ts = require('typescript'); const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin; const readConfiguration = require('@angular/compiler-cli').readConfiguration; const ES6_ONLY_THIRD_PARTY_LIST = require('./es6-only-third-party-list'); function getAngularCompilerTsConfigPath(config) { const angularCompilerPlugin = config.plugins.filter(plugin => plugin instanceof AngularCompilerPlugin).pop(); if (angularCompilerPlugin) { return angularCompilerPlugin.options.tsConfigPath; } return undefined; } function getTsconfigCompileTarget(tsconfigPath) { const {target} = readConfiguration(path.resolve(tsconfigPath)).options; return target; } function webpackConfigAddBabel2ES5(config, list = []) { const tsconfigPath = getAngularCompilerTsConfigPath(config) || 'tsconfig.json'; const target = getTsconfigCompileTarget(tsconfigPath); if (target === ts.ScriptTarget.ES5) { config.module.rules.push({ test: /\.js$/, use: [{ loader: 'babel-loader' }], include: [ ...ES6_ONLY_THIRD_PARTY_LIST, ...list ] }); } return config; }; module.exports = webpackConfigAddBabel2ES5; /** * 備注:如果三方庫(kù)只提供es6版本, 則添加到ES6_ONLY_THIRD_PARTY_LIST, 通過(guò)babel轉(zhuǎn)換語(yǔ)法到es5 * 僅對(duì)target為es5的時(shí)候啟用(比如npm start狀態(tài)) * 差分打包會(huì)自動(dòng)解決,不需要解決 */
// es6-only-third-party-list.js /** * 如果三方庫(kù)只提供es6版本, 則添加到ES6_ONLY_THIRD_PARTY_LIST, 通過(guò)babel轉(zhuǎn)換語(yǔ)法到es5 */ const path = require('path'); const ES6_ONLY_THIRD_PARTY_LIST = [ path.resolve('./node_modules/highlight.js') // ^10.0.0 no longer support ie 11 ]; module.exports = ES6_ONLY_THIRD_PARTY_LIST;
兩段代碼大概思路就是從tsconfig里面讀,如果目標(biāo)為es5,則塞進(jìn)去babel-loader,然后把對(duì)應(yīng)的三方庫(kù)的路徑放到include里邊。
ES6_ONLY_THIRD_PARTY_LIST 列表示意了highlight.js的路徑應(yīng)該怎么寫(xiě)。如果編譯目標(biāo)為es2015,則這段處理就不需要了不會(huì)被插入,哪怕是差分打包也不會(huì)調(diào)用它,ng-cli會(huì)自行調(diào)用內(nèi)部邏輯。
IE11已經(jīng)慢慢退出了歷史舞臺(tái),各大網(wǎng)站也開(kāi)始聲明不再支持IE11,這些冗余的插件已經(jīng)可以慢慢移除。包括ng12起也不再承諾支持ie,升級(jí)到ng12之后這些插件也沒(méi)有必要了。
要點(diǎn):
分清語(yǔ)法API的降級(jí)和瀏覽器BOM/DOM墊片
提供一個(gè)可維護(hù)的列表
針對(duì)tsconfig的target上下文進(jìn)行編譯。
在可視化拖拽生產(chǎn)力平臺(tái)項(xiàng)目,組件定義目錄會(huì)有一系列重復(fù)雷同的目錄結(jié)構(gòu),最后需要匯總到一個(gè)ts里作為全局信息入口。
目標(biāo):自動(dòng)掃描組件定義目錄,匯總信息到ts里,避免手動(dòng)增加信息維護(hù)。
上下文:
生成的信息匯總內(nèi)容結(jié)構(gòu)為:
src/app/connect/connet.ts
目錄的結(jié)構(gòu)為:
src/component-lib
要求不要包含 _目錄的內(nèi)容
const fs = require('fs').promises; const path = require('path'); async function listDir() { return fs.readdir(path.resolve('./src/component-lib')).then(dirs => dirs.filter(item => !item.startsWith('_'))); // 過(guò)濾_開(kāi)頭的 } function genConnectInfo(dirArr) { return `export const ConnectInfo = { ${dirArr.map(item =>"'"+ item +"': import( /* webpackChunkName: \"component-lib-" + item + "-connect\" */ 'src/component-lib/" + item + "/connect')").join(`, `)} };`; } async function process() { var list = await listDir(); return genConnectInfo(list); } module.exports = function(content, map, meta) { var callback = this.async(); this.addContextDependency(path.resolve('./src/component-lib')); // 自動(dòng)掃描目錄,但是刪除目錄可能會(huì)引起報(bào)錯(cuò) process().then((result)=> { callback(null, result, map, meta); }, err => { if (err) return callback(err); }); };
const path = require('path'); function webpackConfigAddScanAndGenerateConnectInfo(config) { config.module.rules.push({ test: /connect\.ts$/, use: [{ loader: require.resolve('./scan-n-gen-connect-webpack-loader') }], include: [ path.resolve('./src/app/connect') ], enforce: 'post', }); return config; }; module.exports = webpackConfigAddScanAndGenerateConnectInfo;
通過(guò)簡(jiǎn)單的一個(gè)loader 將目錄掃描內(nèi)容組裝返回給src/app/connect/connet.ts。
這里有幾個(gè)要點(diǎn):
這里需要使用loader的enforce: 'post'
屬性,因?yàn)閠s的編譯最后loader是直接去文件系統(tǒng)讀取內(nèi)容的不是從上一個(gè)loader拿到結(jié)果繼續(xù)往下處理的(不符合loader的規(guī)范,但是性能會(huì)更好),所以這里需要把階段屬性配置為'post',確保最后編譯的內(nèi)容是我們生成的內(nèi)容。
對(duì)loader添加一些依賴可以在目錄結(jié)構(gòu)變化的時(shí)候刷新內(nèi)容,否則就只能等下一次啟動(dòng)的時(shí)候獲取內(nèi)容,即這一行 this.addContextDependency(path.resolve('./src/component-lib'));
由于webpack的tsc編譯是文件分析依賴型的,我們動(dòng)態(tài)生成的文件內(nèi)容,webpack就無(wú)法從中分析依賴了另外一些ts內(nèi)容,導(dǎo)致其他ts內(nèi)容不會(huì)走ts編譯。這時(shí)候可以參考下面,在tsconfing加一下include字段,解決編譯問(wèn)題。
/* tsconfig.app.json*/ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": [ "src/main.ts", "src/polyfills.ts" ], "include": [ "src/**/*.d.ts", "src/component-lib/**/*.ts" ] }
要點(diǎn):
自動(dòng)掃描邏輯
添加依賴
添加非直接依賴的編譯入口
圖標(biāo)庫(kù)導(dǎo)出了一系列可用字體圖標(biāo),圖標(biāo)通過(guò)不同的css類名來(lái)引用,現(xiàn)在要做一個(gè)圖標(biāo)選擇器,需要把所有圖標(biāo)列出來(lái)。
目標(biāo):分析icon.css,提取所有有效的icon名字。
上下文:
需要把信息自動(dòng)生成到一個(gè)文件叫 src/app/properties-panel/properties-control/icon-picker/icon-library.data.ts
格式為一個(gè)數(shù)組
圖標(biāo)庫(kù)的文件為./node_modules/@devui-design/icons/icomoon/devui-icon.css
格式為:
圖片里紅色標(biāo)記的就是要提取出來(lái)的圖標(biāo)名。
loader的定義
// auto-gen-icon-data-webpack-loader.js const fs = require('fs').promises; const path = require('path'); function genIconData(fileContent) { const iconNames = [...fileContent.matchAll(/\.icon-(.*?):before/g)].map(item => item[1]); return `export const ICON_DATA = [ ${iconNames.map(item => "'" + item + "'").join(",")} ]; `; } async function process(file) { const content = await fs.readFile(`${path.resolve(file)}`, 'utf8'); return genIconData(content); } module.exports = function(content, map, meta) { const file = './node_modules/@devui-design/icons/icomoon/devui-icon.css'; var callback = this.async(); this.addDependency(path.resolve(file)); process(file).then((result)=> { callback(null, result, map, meta); }, err => { if (err) return callback(err); }); };
webpack config里塞入loader
const path = require('path'); function webpackConfigAddGenIconData(config) { config.module.rules.push({ test: /icon-library.data\.ts$/, use: [{ loader: require.resolve('./auto-gen-icon-data-webpack-loader') }], include: [ path.resolve('./src/app/properties-panel/properties-control/icon-picker') ], enforce: 'post', }); return config; }; module.exports = webpackConfigAddGenIconData;
代碼相對(duì)就比較簡(jiǎn)單了。
要點(diǎn):
1、分析圖標(biāo)庫(kù)文件結(jié)構(gòu),排除一些對(duì)齊的干擾項(xiàng)
配置項(xiàng)調(diào)試:在方法中進(jìn)行打印調(diào)試, 通常在執(zhí)行前就已經(jīng)可以打印出配置項(xiàng)相關(guān)的內(nèi)容。
內(nèi)容調(diào)試:使用簡(jiǎn)單的loader打印前后經(jīng)過(guò)loader的內(nèi)容, 自定義一個(gè)打印內(nèi)容的loader,在執(zhí)行自定義loader前后都打印一下,可以獲得對(duì)比內(nèi)容。
一些經(jīng)驗(yàn): 修改loader的時(shí)候要注意本地開(kāi)發(fā)階段和生產(chǎn)打包階段,不要顧此失彼。
文章介紹了如何在AngularCLI生成的工程里使用自定義的webpack設(shè)置,并且舉了幾個(gè)實(shí)際情況下為了解決問(wèn)題修改的webpack配置,覆蓋css的修改、js的修改以及攔截內(nèi)容自動(dòng)掃描生成。
本文主要為解決問(wèn)題或者功能特性去修改webpack配置,沒(méi)有涉及復(fù)雜的構(gòu)建速度優(yōu)化、性能優(yōu)化等。
有些內(nèi)容是舊版本處理IE11問(wèn)題做兼容的,具體的實(shí)踐不再具備復(fù)制方案就能解決現(xiàn)實(shí)問(wèn)題的意義,更多的是提供一些思路和參考。
Webpack目前幾個(gè)版本始終是從入口文件開(kāi)始,通過(guò)規(guī)則配置對(duì)文件進(jìn)行加載解析,通過(guò)插件攔截整個(gè)構(gòu)建生命周期做一些抽取變換。
大部分時(shí)候Angular的默認(rèn)配置已經(jīng)能滿足需求,當(dāng)和三方庫(kù)集成、自動(dòng)化處理結(jié)合的時(shí)候,采用自定義的配置可以解決問(wèn)題,節(jié)省工作甚至進(jìn)一步優(yōu)化性能等等。
看完上述內(nèi)容,你們對(duì)如何自定義配置Angular CLI下的Webpack和loader處理有進(jìn)一步的了解嗎?如果還想了解更多知識(shí)或者相關(guān)內(nèi)容,請(qǐng)關(guān)注億速云行業(yè)資訊頻道,感謝大家的支持。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。