引入antd组件样式_扩大团队技术影响力,搭建团队自己的 UI 组件库
一、技術棧
我們先簡單了解一下要搭建一個團隊的 UI 組件庫,會涉及到哪些技術棧:
- Create React App:官方支持的 CLI 腳手架,提供一個零配置的現代構建設置;
- React: 用于構建用戶界面的 JavaScript 庫;
- Ant Design:一套企業級 UI 設計語言和 React 組件庫;
- Storybook: 輔助 UI 控件開發的工具,通過story創建獨立的控件,讓每個控件開發都有一個獨立的開發調試環境;
- TypeScript:2020 最火的前端語言,是JavaScript類型的超集;
- ESLint && husky:統一團隊代碼風格;
- Jest:JavaScript 測試框架,用于組件庫的單元測試;
- Travis CI: 提供持續集成服務,用于進行項目的持續集成以及持續部署;
二、項目準備
2.1 創建組件庫的工程環境
使用 Create React App 創建 UI 組件庫的前端工程環境。
npx create-react-app ii-admin-base --typescript
2.2 安裝 Storybook
采用自動方式安裝 Storybook,命令如下:
npx -p @storybook/cli sb init --type react_scripts
- 參數 react_scripts 用來告訴 Storybook 當前項目使用 Create React App 創建的,Storybook會根據該參數來自動安裝合適的包。
2.3 安裝 Storybook 插件
2.3.1 addon-info 插件
addon-info 插件會自動識別組件傳遞的 props 生成表格。
yarn add @storybook/addon-info -D yarn add @types/storybook__addon-info -D
三、配置 Storybook
在配置 Storybook 之前,先簡單了解下 stories 的加載流程。
stories 的加載是在 .storybook/main.js 或 .storybook/preview.js 這兩個文件中進行。加載 stories 的最簡便方式是按文件名進行加載。假設你的 stories 文件位于 src/components 目錄,則可以通過如下方式進行加載:
// .storybook/main.jsmodule.exports = { stories: ['../src/components/**/*.stories.js'],};復制代碼或者可以在 .storybook/preview.js 中加載所有的 stories :
import { configure } from '@storybook/react';configure(require.context('../src/components', true, /.stories.js$/), module);復制代碼注意:在 .storybook/preview.js 文件中,只能調用一次 configure 函數。
configure 函數接收參數為:
- 單個 require.context “req”
- 從多個地方加載文件的 “req”s 數組;
- 返回值是 void 或 an array of module exports 的加載函數;
如果想從多個地方進行加載,可采用數組方式,如下所示:
import { configure } from '@storybook/react';configure( [ require.context('../src/components', true, /.stories.js$/), require.context('../lib', true, /.stories.js$/), ], module);復制代碼注:
如果想引入一個文件夾下面的所有文件,或者引入能匹配一個正則表達式的所有文件,可以使用函數require.context()。require.context() 函數有 3 個參數:
- 要搜索的文件夾目錄;
- 是否還應該搜索它的子目錄;
- 以及一個匹配文件的正則表達式;
3.1 配置 stories 顯示順序
若想改變 stories 的顯示順序,該如何操作?示例如下,將 welcome.stories.tsx 先添加至數組中,從而改變 stories 的顯示順序:
import { configure } from '@storybook/react';// 將 welcome 文檔說明置于頂部const loaderFn = () => { const allExports = [require('../src/welcome.stories.tsx')]; const req = require.context('../src/components', true, /.stories.tsx$/); req.keys().forEach((fname) => allExports.push(req(fname))); return allExports;};// automatically import all files ending in *.stories.tsxconfigure(loaderFn, module);復制代碼3.2 支持 Typescript
要搭建的基礎組件庫是基于 Typescript 進行編寫的,因此還需添加 Typescript 支持。配置 ./storybook/main.js 文件,內容如下:
webpackFinal: async (config) => { config.module.rules.push({ test: /.(ts|tsx)$/, use: [ { loader: require.resolve('babel-loader'), options: { presets: [require.resolve('babel-preset-react-app')], }, }, ], }); return config; }復制代碼3.3 配置 less
要搭建的基礎組件庫是基于 Ant Design 做的二次封裝,因此就不得不支持 less。針對 less,配置 ./storybook/main.js 文件,內容如下:
// .storybook/main.js webpackFinal: async (config) => { config.module.rules.push({ test: /.(ts|tsx)$/, use: [ { loader: require.resolve('babel-loader'), options: { presets: [require.resolve('babel-preset-react-app')], }, }, ], }); config.module.rules.push({ test: /.less$/, loaders: [ 'style-loader', 'css-loader', { loader: 'less-loader', options: { lessOptions: { javascriptEnabled: true, }, }, }, ], include: [path.resolve(__dirname, '../src'), /[/]node_modules[/].*antd/], }); return config; },復制代碼完成上述內容配置,發現導入的 less 文件不生效。針對這一問題,進行了以下幾點排查。
問題1: 如果 less-loader 版本是 6.0 以上,則如下配置會報錯:
{ loader: "less-loader", options: { javascriptEnabled: true }}復制代碼需修改成:
{ loader: 'less-loader', options: { lessOptions: { javascriptEnabled: true, }, }, }復制代碼問題2: storybook 5.3.0 與 storybook 5.2.x 存在一些差異,見參考鏈接。cra(create-react-app)的 file-loader 會攔截所有其他文件,導致less 文件不能進入less-loader中。針對這一問題,需配置 @storybook/preset-create-react-app,配置內容如下:
{ name: '@storybook/preset-create-react-app', options: { craOverrides: { fileLoaderExcludes: ['less'], }, },}復制代碼問題3: 此次搭建的基礎組件庫是基于 Ant Design 做的二次封裝,再對 Ant Design 組件進行引用時,發現樣式不生效。針對這一問題,可以在 preview.tsx 進行如下配置:
import { configure } from '@storybook/react';import 'antd/dist/antd.less' // 引入 antd 樣式復制代碼3.4 添加全局裝飾器
啟動 Storybook,會發現右側 stories 內容緊靠著左側菜單欄,整體感覺非常緊湊、不美觀。針對這種情況通??梢酝ㄟ^添加 padding 來解決。那么該如何讓 padding 對 Storybook 中的所有 stories 進行生效呢?這個時候就需使用到全局裝飾器。
在 .storybook 目錄下,創建全局裝飾器,如下所示:
// .storybook/decorators/WrapperDecorator/index.tsximport React from 'react';const wrapperStyle: React.CSSProperties = { padding: '20px 40px',};// 創建一個樣式包裹的裝飾器const WrapperDecorator = (storyFn) => {storyFn()};export default WrapperDecorator;復制代碼然后在 preview.tsx 添加該裝飾器即可。
// .storybook/preview.tsx import { addDecorator, configure } from '@storybook/react';import WrapperDecorator from './decorators/WrapperDecorator';import 'antd/dist/antd.less';// 通過addDecorator添加插件addDecorator(WrapperDecorator);復制代碼最后效果如下所示。
四、組件開發
4.1 驗證碼輸入組件
此次要示例的驗證碼輸入組件是一個帶驗證碼發送功能的 Input 組件,如下圖所示。
整個組件是在 Ant Design 的 Input 組件上進行的二次開發,詳細代碼如下圖示所示:
import React, { useState, FC } from 'react';import { Input } from 'antd';import { InputProps } from 'antd/lib/input';import classNames from 'classnames';export interface InputVerifyProps extends InputProps { /** 發送驗證碼接口函數 */ sendCode: () => void; /** 倒計時時間 */ countDown?: number; /** 初始驗證碼文本內容 */ initCodeText?: string; /** 重新發送驗證碼文本內容 */ reCodeText?: string; /** 驗證碼類名 */ codeClassname?: string;}export const InputVerify: FC = (props) => { const { sendCode, countDown, initCodeText, reCodeText, codeClassname, ...restProps } = props; const [codeText, setCodeText] = useState(initCodeText); const [codeStatus, setCodeStatus] = useState(false); const handleCountDown = (timer: ReturnType | null, count: number) => { if (timer) { clearTimeout(timer); } if (count <= 0) { setCodeText(reCodeText); setCodeStatus(false); } else { setCodeText(`${count} s`); const newTimer: ReturnType = setTimeout(() => { handleCountDown(newTimer, count - 1); }, 1000); } }; const handleCodeClick = () => { if (codeStatus) return; sendCode && sendCode(); setCodeStatus(true); handleCountDown(null, countDown as number); }; const codeCls = classNames('ii-verify-button', codeClassname, { 'ii-verify-button-disabled': codeStatus, }); return ( {codeText} } /> );};InputVerify.defaultProps = { countDown: 60, initCodeText: '發送驗證碼', reCodeText: '重新發送',};export default InputVerify;復制代碼4.2 添加單元測試
完成組件開發任務后,接下來就需添加單元測試。針對驗證碼輸入組件,單元測試主要分兩個方面,一方面測試 antd 原生 Input 組件是否正常工作,另一方面則是測試驗證碼輸入組件是否正常工作。
import React from 'react';import { render, fireEvent, wait, RenderResult } from '@testing-library/react';import '@testing-library/jest-dom';import '@testing-library/jest-dom/extend-expect';import InputVerify, { InputVerifyProps } from './InputVerify';const antdProps: InputVerifyProps = { placeholder: 'antd input placeholder', size: 'large', sendCode: jest.fn(), onPressEnter: jest.fn(), onChange: jest.fn(),};const selfProps: InputVerifyProps = { countDown: 3, initCodeText: '發送驗證碼', reCodeText: '再次發送', sendCode: jest.fn(),};let wrapper: RenderResult, inputElement: HTMLInputElement;describe("Test InputVerify component on the props of antd's input component", () => { beforeEach(() => { wrapper = render(); inputElement = wrapper.getByTestId('test-input-verify') as HTMLInputElement; }); it("should have the input's class of antd", () => { expect(inputElement).toBeInTheDocument(); expect(inputElement).toHaveClass('ant-input'); }); it('should support size', () => { expect(inputElement).toHaveClass('ant-input-lg'); }); it('should trigger onChange event correctly', () => { fireEvent.change(inputElement, { target: { value: 'input test' } }); expect(antdProps.onChange).toHaveBeenCalled(); expect(inputElement.value).toEqual('input test'); });});describe("Test InputVerify component on the self's props", () => { beforeEach(() => { wrapper = render(); }); it('should render the correct InputVerify component', () => { const suffixElement = wrapper.getByText('發送驗證碼'); expect(suffixElement).toBeInTheDocument(); expect(suffixElement).toHaveClass('ii-verify-button'); }); it('click verify button should call the right callback ', async () => { const suffixElement = wrapper.getByText('發送驗證碼'); fireEvent.click(suffixElement); expect(selfProps.sendCode).toHaveBeenCalled(); await wait( () => { expect(wrapper.getByText('再次發送')).toBeInTheDocument(); }, { timeout: 4000 } ); });});復制代碼4.3 組件說明文檔
當開發完單個組件,還需添加相應的文檔說明,告訴其他人該如何使用這個組件。
4.3.1 自動生成說明文檔
如果想通過注釋方式來自動生成組件的說明文檔,這個時候就需借助 react-docgen 插件。由于 @storybook/addon-info依賴包對 react-docgen 插件已進行了集成,所以編寫注釋的時候只需按照 JSDoc 標準來編寫就會生成相應的說明文檔。
/** * 帶驗證碼功能的輸入組件,適用于要發送驗證碼的場景。 * * ## 引用方法 * * ~~~javascript * import { InputVerfiy } from 'ii-admin-base' * ~~~ */export const InputVerify: FC = (props) => {復制代碼注意: react-docgen 插件要求組件還需通過 export 方式進行導出。
4.3.2 過濾 Prop Types
@storybook/addon-info 插件在自動生成 Prop Types 的說明文檔時,會連組件繼承的 Props 也自動生成,這里面不僅包括了第三方依賴包攜帶的 props,還可能包括 HTML 元素的原生 Props。若要過濾這些 Props,就需借助依賴包 react-docgen-typescript-loader。
先安裝該依賴:
yarn add react-docgen-typescript-loader -D
然后配置 main.js文件,配置內容如下:
// .storybook/main.jsconfig.module.rules.push({ test: /.(ts|tsx)$/, use: [ { loader: require.resolve('babel-loader'), options: { presets: [require.resolve('babel-preset-react-app')], }, }, // 過濾 node_modules 中的 props { loader: require.resolve('react-docgen-typescript-loader'), options: { // 將枚舉或者聯合類型轉換成字符串形式,避免字符串字面量顯示別名。 shouldExtractLiteralValuesFromEnum: true, // 避免顯示原生內置屬性 propFilter: (prop) => { if (prop.parent) { return !prop.parent.fileName.includes('node_modules'); } return true; }, }, }, ],});復制代碼注意:
在使用最新的 Storybook v5.3.19 版本時,發現上述配置并不生效。針對這一問題,可以將 Storybook 版本降至 5.3.18 來進行規避。
五、構建及測試
5.1 打包構建
5.1.1 創建組件庫的模塊入口文件
在 src/index.tsx 文件中將所有組件都導入,再導出。這樣就可以從入口文件直接導入所有組件。
export { default as InputVerfiy } from './components/InputVerify';復制代碼5.1.2 編譯 TS 文件
使用 CRA(Create-React-App) 默認會創建一個 tsconfig.json 文件,該配置文件是與開發環境相關的。要針對組件庫進行打包編譯,并生成標準的 ES modules,還需單獨創建一個 tsconfig.build.json 文件。
/** * 用于最后打包編譯 */{ "compilerOptions": { // 文件輸出目錄 "outDir": "dist", // ESNext: 是標準的ES Modules形式 "module": "esnext", // 指定編譯以后符合什么樣的ES標準 "target": "es5", // 為每一個js文件生成一個對應的.d.ts類型文件,方便使用組件庫的用戶可以獲得類型檢查和ts提示 "declaration": true, // jsx 是一種語法糖,是React.createElement的縮寫。此處置為react,編譯出來的文件就可以用React.createElement來代替JSX語法的過程 "jsx": "react", // tsc 處理模塊的方式和node不一樣,默認處理方式是"classic",針對絕對路徑有的時候會找不到文件(一直向上找文件),所以需設置成'node'。 "moduleResolution": "node", // 默認不支持 import React from 'react',只支持 import * as React from 'react' "allowSyntheticDefaultImports": true }, // 要編譯哪些文件 "include": ["src"], "exclude": ["src/**/*.test.tsx", "src/**/*.stories.tsx"]}復制代碼然后在 package.json 文件中添加 build:ts 腳本,用于將 TS 文件編譯成 ES modules 文件。
"build:ts": "tsc -p tsconfig.build.json",
5.1.3 編譯 less 文件
在 package.json 文件中添加 build:css 腳本,用于將 less 文件編譯成 css 。
"build:css": "lessc ./src/styles/index.less ./dist/index.css"
5.1.4 配置最終構建腳本
在 package.json 中配置最終的構建腳本 build,如下所示:
"clean": "rimraf ./dist", "build:ts": "tsc -p tsconfig.build.json", "build:css": "lessc ./src/styles/index.less ./dist/index.css", "build": "npm run clean && npm run build:ts && npm run build:css",復制代碼- 使用 rimraf 來完成跨平臺文件的刪除;
5.2 本地測試組件庫
5.2.1 添加入口文件
在進行本地組件庫測試之前,還需添加組件庫的入口文件。配置 package.json 文件,添加如下字段:
"main": "dist/index.js","module": "dist/index.js","types": "dist/index.d.ts",復制代碼其中:
- main 字段:定義了 npm 包的入口文件;
- module 字段:定義了 npm 包的 ES6 模塊規范的入口文件;
注: 此處使用 main 字段和 module 字段,相當于在一個包內同時發布了兩種模塊規范的版本。當打包工具遇到我們的模塊時:
5.2.2 使用 npm link 測試本地組件庫
在組件庫目錄下,運行 npm link 命令,即創建軟鏈接到全局的 node_modules 下。
/Users/xxx/.nvm/versions/node/v12.14.0/lib/node_modules/ii-admin-base -> /Users/xxx/Job/ii-admin-base
在項目外層,創建一個測試目錄 test-ii-admin-base,然后在該目錄下運行 npm link ii-admin-base 命令,將測試目錄的組件庫 ii-admin-base 鏈接到全局:
? test-ii-admin-base npm link ii-admin-base /Users/xxx/Job/test-ii-admin-base/node_modules/ii-admin-base -> /Users/xxx/.nvm/versions/node/v12.14.0/lib/node_modules/ii-admin-base -> /Users/xxx/Job/ii-admin-base
然后修改測試目錄 test-ii-admin-base 的 package.json文件,手動添加依賴:
"dependencies": { ..., "ii-admin-base": "0.1.0"復制代碼這樣就可以在測試目錄中引用組件庫 ii-admin-base。
import { InputVerfiy } from 'ii-admin-base'import 'ii-admin-base/dist/index.css'import 'antd/dist/antd.css'復制代碼注: 如果在測試的過程中,報如下錯誤:
這是因為我們開發組件庫時使用了一個React版本,測試目錄又使用了一個React版本,當一個項目中如果出現多個 React 版本就會報上述錯誤。針對這種情況,只需要在組件庫目錄下運行如下命令: npm link ../test-ii-admin-base/node_modules/react , 即將組件庫的 react 版本鏈接到測試組件目錄下,然后重新運行項目即可。
六、發布至 NPM
6.1 登錄 NPM 賬號
先切換官方鏡像源。
npm config set registry registry.npmjs.org/
檢測當前賬號是否登錄:
npm whoami
如果未登錄,則使用 npm adduser 進行賬號登錄。
6.2 發布至 NPM
6.2.1 添加描述信息
在發布到 NPM 之前,還需配置 package.json 文件,添加一些必要的描述信息:
{ "name": "ii-admin-base", "version": "0.1.0", "private": false, "description": "A library of react components, which mainly stores components that can be reused by all business lines of AI-Indeeded Company.", "author": "ShiMu", "license": "MIT", "keywords": [ "React", "Component" ], "homepage": "https://lagrangelabs.github.io/ii-admin-base", "repository": { "type": "git", "url": "https://github.com/LagrangeLabs/ii-admin-base.git" }, "files": [ "build" ], "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", "storybook": "start-storybook -p 9009 -s public", "build-storybook": "build-storybook -s public", "prepublishOnly": "npm run build" }, ...}復制代碼其中:
- 將 private 字段置為 false , 表示非私有包;
- 添加 description 、 author 、 license 、 keywords 等相關字段;
- 添加 homepage 字段,即項目主頁URL;
- 添加 repository 字段,即項目倉庫地址URL;
- 添加 files 字段,表示要將哪些文件上傳到 npm 上去。如果什么都不寫,則默認會使用 .gitignore 里面的信息。但要注意,不管 .gitignore 怎么配置,一些文件會始終發布到 package 上,這些文件包括 package.json 、 README.md 、 LICENSE 等等;
- 添加 prepublishOnly 鉤子函數,在該鉤子函數中運行 npm run build ,用來確保 NPM 包發布之前采用的是最新編譯的代碼;
6.2.2 配置 peerDependencies 字段
此次搭建的組件庫是在 React 基礎上對 Ant Design 進行的二次封裝。為了減少組件庫體積,通常不會將React、Ant Design 等第三方依賴打包進去,其次若打包進去,可能還會造成各個版本之間的沖突。
針對這種情況,可以提示用戶如果要想使用當前組件庫,還需安裝以下核心依賴,如 react 、 Ant Design 等,這個時候就需利用 package.json 中的 peerDependencies 字段。當使用 npm install 安裝依賴時, peerDependencies 聲明的依賴不會被自動安裝,而是通過輸出 warining 日志告訴用戶需安裝以下依賴。
"peerDependencies": { "react": ">= 16.8.0", "react-dom": ">= 16.8.0", "antd": ">= 4.3.5" },復制代碼- react 版本需大于等于 v16.8.0,因為在 v16.8.0 以上版本才引入了 React Hooks;
6.2.3 代碼規范檢查和單元測試檢查
對于一個組件庫來說,代碼質量是非常重要的。為了防止不符合團隊規范的代碼或未通過單元測試的代碼被commit 亦或者被 publish,需要使用一些鉤子函數來驗證開發者是否通過代碼規范檢查和單元測試檢查。
6.2.3.1 添加代碼規范檢查
在 package.json文件中,添加 lint 腳本,針對 src 目錄下的文件進行 eslint 檢查。
"lint": "eslint --ext js,ts,tsx, src --max-warnings 5",復制代碼- --max-warnings 5 : 表示最大允許的 warnings 警告是 5;
6.2.3.2 添加單元測試檢查
在使用 CRA 創建項目時,默認會創建 test 腳本,但該腳本是用于開發環境,執行完后不會返回執行結果(即不會返回執行通過還是未通過),而是一直處于 watch 模式下。針對這一情況,可以設置環境變量 CI=true ,即可返回測試運行結果。
"test:nowatch": "cross-env CI=true npm run test"復制代碼- 在不同的操作系統環境下,設置環境變量方式不一樣。故需借助 cross-env 依賴包完成跨平臺的環境變量設置。
6.2.3.3 commit 或 publish 前的流程檢查
針對 commit 代碼,安裝 husky 依賴,在代碼提交前先進行單元測試檢查和代碼規范檢測,如下所示。在發布NPM 包之前也進行同樣配置。
"scripts": { ..., "prepublishOnly": "npm run test:nowatch && npm run lint && npm run build" }, "husky": { "hooks": { "pre-commit": "npm run test:nowatch && npm run lint" } },復制代碼完成上述配置后,運行命令 npm publish 即可完成 NPM 包的發布。
七、配置持續集成環境
通常從各個業務線上抽離的基礎組件庫,會是各個業務線的團隊成員一起來維護,這個時候就可以利用 Github 提供的 Github Organization 來共同維護這個組件庫。
在這種情況下,使用 Travis CI 進行持續集成就沒有那么簡單了。之前寫過一篇文章如何使用Travis CI對Github Organization下的代碼進行持續集成,可以參照該文章完成基礎組件庫的持續集成環境配置。
八、持續發布
Travis CI 還可以自動將組件庫發布到 NPM 上,具體設置如下:
NPM api key: ************************************
改寫后的 travis.yml 文件:
language: node_jsnode_js: - stablecache: directories: - node_modulesenv: matrix: - CI=truescript: - npm run build-storybookdeploy: # 發布到 gh-pages 上 - provider: script skip_cleanup: true script: bash scripts/deploy.sh on: branch: master # 發布到 npm 上 - provider: npm skip_cleanup: true email: xxxx@qq.com api_key: secure: Lsb1/coESXgnDgcotaObyV7QKDVeZJWpAcduyZt/bxAqspN/EdOR2duraPpBHKzme7tOHT4ybIAQusJqSl36K/WX2WFXqhKHw+FoFOobK1aa/azQDkpwllgdxrlx0fCbLpxBPDdKxbJspXwphSgCi2rjY8F/PBdy4+g8IEh/FJQckuFHAEhpTuk+SZPJT5eAqhctxXSaNKB712x4vX9AJLHRT791nB388dsjKOz2NWGNJ14arxukvnb/Yt02hHWKpGQPgQQY9QjfENYnspFYBXYssKV2nhC+0EFoXNn6UK3C4gXo96hV2yqFbP0AhZdHiYxOJ/v1KN7xt+I3popw+puETFyno4TgepGqU/EvkB5r3DnB9CrYsOpeN4+wZtfVtwxMxxxJ8q/EbC7RH45b39056B0i7PnJViIHLWps3XxFQ/bi1CgWdiFyzNofwCYVV6uT0UNR0XZDqUzre10GBrvDogMNWPKMaTmJCWVA8c6AkB4XjfU/jY1xaWxbNuD+Z+p3uLSTKm+c2xrUJFl5KW4/ocyS8No/J+e/9uNkXYcTEdkwnBioWfT7OaBrIpzrkKL9RftkDzjkeUo8h9/XpXNHEUGMK6ZDO0n3zlQ8/qcMHJvS5dXbKmvwZ9GNnOS1EvR1X32MlTfcW0EzDgCXufyAK6UdUGm7jm+dfJJkD60g= on: branch: master repo: LagrangeLabs/ii-admin-base復制代碼注: Travis CI 在對組件庫進行持續發布的時候,如果報如下問題:
sh: 1: cross-env: not foundnpm ERR! code ELIFECYCLEnpm ERR! syscall spawnnpm ERR! file sh復制代碼針對這一問題,將 package.json 文件中的 cross-env 換成 ./node_mdoules/.bin/cross-env 即可。
"test:nowatch": "./node_modules/.bin/cross-env CI=true npm run test"
作者:辻目
原文鏈接:https://juejin.im/post/5ef7328cf265da22a8513da2
總結
以上是生活随笔為你收集整理的引入antd组件样式_扩大团队技术影响力,搭建团队自己的 UI 组件库的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python核心编程电子版_python
- 下一篇: keras cnn注意力机制_Tenso