使用 Vite 搭建 React 項目
背景
爲支持公司業務發展,方便業務在非工作時間段在手機端處理部分工作,需要新開發一個 移動飛書 H5 工作臺
系統。
很高興能負責此次的項目搭建,讓我有機會從頭到尾地經歷一次項目的搭建過程。
在此,我將記錄並分享從項目設計到最終上線的完整步驟,總結搭建過程中遇見過的坑。希望下次有這樣的機會時,能在本次搭建的基礎上,更快地搭建出質量更高的項目。
需求概覽
-
在移動端使用。需要兼容
手機
及pad
。 -
支持
中文
、英文
兩種語言方式。 -
PC
或者Pad
中訪問時,需要SSO
登錄。 -
在飛書中訪問時,免登錄。
-
需要在國內和國外都能使用。
-
需要收集線上
bug
使用信息。 -
需要收集
PV
、UV
等用戶訪問信息。
技術選型
除部分需要 Node V16+
版本及以上的包外,基本使用的都是對應庫的最新版本。這樣有什麼優缺點呢?
-
優點:能充分地使用對應庫最新的一些功能,快速完成業務開發。
-
缺點:凡是程序都可能會有
bug
,使用最新庫遇見問題時,定位以及修復方法不好找,需要花更多的時間。
爲什麼 Node
沒使用 V16+
的呢?
- 公司系統很多,許多項目的
gitlab runner
都使用的同一個。有一些項目搭建時間較久,能使用最高的Node
版本爲V14+
,超過V14+
安裝的包有兼容性問題。爲了不影響其它系統的正常使用,我們這次的Node
最高版本也只支持在V14+
。
爲什麼使用 Vite
來構建初始化項目?
Vite
、 Webpack
等都是很好的構建工具。
Vite
提出,它利用瀏覽器開始支持 ES
模塊的特性,基於原生 ES
模塊進行構建,可以極大地縮短項目啓動以及 HMR
的時間。
閱文千遍,不如親身實踐一遍。所以正好藉此機會,從實際使用上來感受一下 Vite
與 Webpack
在開發體驗上的區別。
爲什麼使用 pnpm?
-
快速:
pnpm
是同類工具速度的將近2
倍。 -
高效:
node_modules
中的所有文件均克隆或硬鏈接自單一存儲位置。 -
支持單體倉庫:
pnpm
內置了對單個源碼倉庫中包含多個軟件包的支持。 -
權限嚴格:
pnpm
創建的node_modules
默認並非扁平結構,因此代碼無法對任意軟件包進行訪問。
操作步驟
初始化項目
> pnpm create vite $projectName --template react-ts
> cd $projectName
> pnpm i
> pnpm dev
初始化項目後,tsconfig.json
報錯:
解決辦法
修改 moduleResolution
的值爲 node
:
//tsconfig.json
"moduleResolution": "node"
tsconfig.node.json
處也做相同修改。
tsconfig.json
報 Unknown compiler option 'allowImportingTsExtensions'
錯誤:
解決辦法
將 allowImportingTsExtensions
移動到和 compilerOptions
同級的地方。
// tsconfig.node.json
{
"compilerOptions": {
// ...
},
"allowImportingTsExtensions": true,
}
參考鏈接:https://blog.csdn.net/qq_46266305/article/details/131140524
main.tsx
報 This module is declared with using 'export =', and can only be used with a default import when using the 'allowSyntheticDefaultImports' flag
錯誤
解決辦法
tsconfig.json
中配置
// tsconfig.json
"noFallthroughCasesInSwitch": true
main.tsx
報 An import path cannot end with a '.tsx' extension. Consider importing './App.js' instead.
錯
解決辦法
去掉 .tsx
結尾。
安裝 less
> pnpm i less -D
如果需要以 styles.
的方式使用 less
,需要將 less
的文件命名爲 *.module.less
。
import styles from '*.module.less';
部分變量需要在很多 less
文件中使用,在 vite.config.ts
中配置全局 less
// vite.config.ts
css: {
modules: {
generateScopedName: "[local]__[hash:base64:5]",
hashPrefix: "prefix",
},
preprocessorOptions: {
less: {
javascriptEnabled: true,
// 配置 less 全局變量
additionalData: `@import "${path.resolve(
__dirname,
"src/assets/styles/variable.less"
)}";`,
},
},
// 開發環境生成 less 的 sourceMap
devSourcemap: !isOnline,
},
參考鏈接:https://cn.vitejs.dev/guide/features.html#css-modules
安裝 eslint
> pnpm i eslint -D
因爲有使用 TypeScript
。所以還需要安裝
-
@typescript-eslint/parser
-
@typescript-eslint/eslint-plugin
-
eslint-plugin-react-hooks
-
eslint-plugin-react-refresh
-
...
在 package.json
的 scripts
中配置
// package.json
"lint-staged:js": "eslint --cache --ext .js,.jsx,.ts,.tsx ./src"
執行命令
> pnpm run lint-staged:js
如果還有依賴包沒安裝好,在 terminal
中會有提示,根據提示信息完善依賴包的 install
就行。
配置好 eslint
後,將初始化項目中,不符合 eslint
規範的代碼先修改一下,以免後面不符合 eslint
的越來越多。
husky
和 Eslint
較爲相關的當然是 husky
了。
它的作用是什麼呢?
能在 git
操作過程中,觸發對應的鉤子,執行對應的命令。在這裏與 Eslint
結合,能在提前到 git
倉庫前,對 Eslint
進行一次校驗,以防止不符合 Eslint
規範的代碼提交到無端倉庫。
具體的安裝步驟,可以參考 github
官方文檔來操作:https://github.com/typicode/husky
然後在 husky
的 pre-commit
鉤子方法裏面寫
// .husky/pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
echo 'precommit'
npm run lint-staged:js
// package.json
"scripts": {
"lint-staged:js": "eslint --cache --ext .js,.jsx,.ts,.tsx ./src"
}
安裝 commitlint
它的作用是什麼?
規範 commit
信息。如果是功能提交,commit message
需要以 feat:
開頭,如果是修復 bug
,需要以 fix:
開頭,如果只是樣式修改,需要以 style
開頭等等。
這樣可以從 commit
處,直觀看到本次 commit
的具體作用是什麼。
-
build
-
chore
-
ci
-
docs
-
feat
-
fix
-
perf
-
refactor
-
revert
-
style
-
test
具體操作步驟參考官方文檔:https://github.com/conventional-changelog/commitlint
在安裝 commitlint
過程中,遇見一個報錯:
SyntaxError: Failed to load plugin '@typescript-eslint' declared in '.eslintrc.js » @vue/eslint-config-typescript/recommended » /home/viktord/Projects/renthome/dashboard/node_modules/@vue/eslint-config-typescript/index.js': Unexpected token '??='
Referenced from: /home/viktord/Projects/renthome/dashboard/node_modules/@vue/eslint-config-typescript/index.js
/home/viktord/Projects/renthome/dashboard/node_modules/@typescript-eslint/typescript-estree/dist/convert.js:176
result.range ??= (0, node_utils_1.getRange)(node, this.ast);
^^^
SyntaxError: Unexpected token '??='
at wrapSafe (internal/modules/cjs/loader.js:1001:16)
at Module._compile (internal/modules/cjs/loader.js:1049:27)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)
at Module.load (internal/modules/cjs/loader.js:950:32)
at Function.Module._load (internal/modules/cjs/loader.js:790:12)
at Module.require (internal/modules/cjs/loader.js:974:19)
at require (internal/modules/cjs/helpers.js:101:18)
at Object.<anonymous> (/home/viktord/Projects/renthome/dashboard/node_modules/@typescript-eslint/typescript-estree/dist/ast-converter.js:4:19)
at Module._compile (internal/modules/cjs/loader.js:1085:14)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)
Process finished with exit code -1
原因:最新的 @typescript-eslint/eslint-plugin v6.2.0
有問題,先降級爲 5.62.0
。 等 bug
修復後,可以嘗試再次升級爲 v6+
。
安裝 antd-mobile
有許多移動端的組件,引入一個完整的組件庫,可以減少很多重複造輪子的時間。
> pnpm i antd-mobile
安裝 iconfont
在 https://www.iconfont.cn/ 中上傳自己的圖標。
以 Symbol
的方式生成圖標
將資源下載到項目的 public
文件夾下。
然後在 index.html
處引入對應的 js
// index.html
<body>
<div id="root"></div>
<script src="/iconfont.js"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
如果使用的是 @ant-design/icons
它提供了一個方法 createFromIconfontCN
可以擴展字體圖標。但我們不需要它的圖標,所以不需要安裝它的包。
這樣,就需要自己實現一個 Iconfont
組件。
// components/Iconfont.tsx
import { FC } from "react";
interface IProps extends IObject {
/** 圖標類型 icon- */
type: string;
/** 圖標大小 */
size: number;
/** 圖標顏色 */
color?: string;
}
const Iconfont: FC<IProps> = (props) => {
const { type, color, size, style, ...rest } = props;
return (
<svg
class
aria-hidden="true"
fill={color}
style={{ width: size, height: size, ...style }}
{...rest}
>
<use xlinkHref={`#${type}`}></use>
</svg>
);
};
export default Iconfont;
這樣就能使用 IconFont
了。
<Iconfont type="icon-image" size={24} class />
安裝 react-intl
國際化語言支持。
> pnpm i react-intl
修改 main.tsx
// main.tsx
const localeInfo = locales();
ReactDOM.createRoot(document.getElementById("root")!).render(
<IntlProvider locale={localeInfo.locale} messages={localeInfo.localeMessages}>
<React.StrictMode>
<App />
</React.StrictMode>
</IntlProvider>
);
添加 src/locales/index.ts
。優先讀取 localStorage
中是否有保存語言環境,如果沒有,默認讀取瀏覽器語言環境。切換語言後,將語言信息存放在 localStorage
中。
// src/locales/index.ts
function locales() {
const language = localStorage.getItem('language-locale') || navigator.language;
switch(language) {
case 'zh-CN':
return { locale: navigator.language, localeMessages: zhCN };
default:
return { locale: navigator.language, localeMessages: enUS };
}
}
export default locales;
切換語言組件
// components/SwitchLanguage.tsx
import { Button, Popover } from "antd-mobile";
import { Action } from "antd-mobile/es/components/popover";
import styles from "./index.module.less";
enum languageEnum {
"zh-CN" = "zh-CN",
"en-US" = "en-US",
}
const actions: Action[] = [
{ key: languageEnum["zh-CN"], text: "簡體中文(CN)" },
{ key: languageEnum["en-US"], text: "English(EN)" },
];
/** 切換語言 */
const SelectLanguage = () => {
const currentLanguage =
localStorage.getItem("language-locale") || navigator.language;
// 如果找不到,就展示 English。因爲 zh-CN 只有一個,其它的語言很多
const languageText =
actions.find((item) => item.key === currentLanguage)?.text ||
actions[1].text;
const handleLanguageChange = (node: Action) => {
// 如果選擇的是和目前一樣的語言,不做任何處理
if (node.key === currentLanguage) return;
localStorage.setItem("language-locale", node.key as string);
window.location.reload();
};
return (
<section className={styles.selectLanguage}>
<Popover.Menu
actions={actions}
onAction={handleLanguageChange}
placement="bottom-start"
trigger="click"
>
<Button>{languageText}</Button>
</Popover.Menu>
</section>
);
};
export default SelectLanguage;
然後在對應的組件中,就可以使用了
import { useIntl } from "react-intl";
const intl = useIntl();
intl.formatMessage({ id: "global.copySuccess" })
添加 axios
> pnpm i axios
添加了 axios
後,需要封裝 request
請求。請求數據時,給接口添加請求頭,返回數據時,對接口進行統一錯誤處理。
添加 alias
正常情況下,在一個文件中,使用相對路徑加載其他模塊,如果文件層級較深,經常會出現 ../../../../*.tsx
這樣的寫法。在文件目錄發生變化時,很容易出現路徑修改不完整而導致運行錯誤。
配置 alias
,可以以絕對路徑的方式引入其他模塊。例如:import Loading from '@/components/Loading'
。
配置 alias
的方法如下:
在 vite.config.ts
中配置
// vite.config.ts
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
extensions: [".ts", ".tsx", ".js", ".jsx"],
},
另外,還需要在 tsconfig.ts
中配置
// tsconfig.ts
"baseUrl": "./",
"paths": {
"@/*": ["src/*"],
}
接入 pont
pont
是什麼?
pont
在法語中是 “橋” 的意思,寓意着前後端之間的橋樑。
Pont
把swagger
、rap
、dip
等多種接口文檔平臺,轉換成Pont
元數據。Pont
利用接口元數據,可以高度定製化生成前端接口層代碼,接口mock
平臺和接口測試平臺。其中
swagger
數據源,Pont
已經完美支持。並在一些大型項目中使用了近兩年,各種高度定製化需求都可以滿足。https://github.com/alibaba/pont
通俗來說就是
-
它可以根據
Swagger
定義,自動生成對應的TS
的interface
。 -
結合着我們封裝的
request
方法,自動生成對應的api
。 -
在接口定義發生變化時,可以通過
diff
、updateMod
、updateInterface
、updateBo
等方法進行更新。 -
...
首先,安裝包
> pnpm i pont-engine -D
在項目的根目錄下添加 pont-config.json
// pont-config.json
{
"outDir": "./src/services/auto-gen-api/src",
"templatePath": "./generate-api-template",
"originType": "SwaggerV2 | SwaggerV3",
"prettierConfig": {
"singleQuote": true,
"trailingComma": "all",
"tabWidth": 2,
"endOfLine": "lf",
"printWidth": 100,
"proseWrap": "never"
},
"origins": [
{
"originType": "SwaggerV2 | SwaggerV3",
"originUrl": "https://**/v2/api-docs?group=api",
"name": "projectNameApi",
"usingMultipleOrigins": true,
"usingOperationId": true,
}
]
}
同級目錄下添加 generate-api-template.ts
,裏面的具體內容,根據接口定義以及封裝的 request
進行自定義開發。
// generate-api-template.ts
export default class MyGenerator extends CodeGenerator {
// ...
}
做完這些,項目運行時,我們可以對生成的文件進行檢驗,看其語法是否正確,執行 node ./config/swagger-to-api/compile.js
。
// package.json
"scripts": {
"compile": "cross-env NODE_ENV=development pnpm compileApi",
"diff": "npx pont diff",
"compileApi": "node ./config/swagger-to-api/compile.js",
"updateMod": "npx pont updateMod",
"updateBo": "npx pont updateBo",
"updateInterface": "npx pont updateInterface",
},
在 compile.js
中,對生成的文件使用 ttsc
進行校驗。
在執行 pnpm diff
時,有些會報 ES Module
錯誤。
解決辦法:
去掉 package.json
中的
// package.json
type: 'module'
接入 react-router-dom
> pnpm install react-router-dom
修改 main.tsx
// main.tsx
ReactDOM.createRoot(document.getElementById("root")!).render(
<IntlProvider locale={locale} messages={localeMessages}>
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
</IntlProvider>
);
後面的所有的頁面都會經過 Authority.tsx
組件。
爲避免 router.ts
文件過大,不好維護。將路由分散在各個主功能下,使用 import
的方式進行引入。
// router.ts
import { createBrowserRouter } from "react-router-dom";
import Authority from "@/layout/Authority";
import ErrorBoundary from "@/layout/ErrorBoundary";
import Home from "@/pages/home";
import Search from "@/pages/search";
import clueRouters from "@/pages/clue/router";
const router = createBrowserRouter([
{
path: "/",
element: <Authority />,
errorElement: <ErrorBoundary />,
children: [
{
path: "/home",
element: <Home />,
},
{
path: "/search",
element: <Search />,
},
...clueRouters,
],
},
]);
export default router;
如果頁面中有由 JS
執行出錯的地方,會進入 errorElement
。在這裏可以做一些頁面錯誤的統一處理。
通過 react-router-dom
的 useRouteError
能獲取到錯誤的具體信息。
// ErrorBoundary
import { useRouteError } from "react-router-dom";
import PageError from "./PageError";
// 錯誤處理
const ErrorBoundary = () => {
const error = useRouteError() as IObject;
// ...
return <PageError message={error.message} />;
};
export default ErrorBoundary;
接入 SSO
在入口文件處引入封裝好的 SSO SDK
。由於不同環境的 SDK
對應的 CDN
地址不一致。所有的頁面都會經過 layout/Authority.tsx
組件。所以我們根據當前運行環境,在 Authority.tsx
的 useLayoutEffect
中進行動態加載,並且進行授權校驗。
// Authority.tsx
useLayoutEffect(() => {
// 動態加載 SSO 文件
const script = document.createElement("script");
script.src = `${process.env.SSO_SERVER}/t.js`;
document.head.appendChild(script);
script.onload = async () => {
// 封裝的 SSO 資源加載完成後,實例化 SSO 方法
window.authentication = new window.$logo(config);
setLoading(false);
};
// ...
}, []);
在後面的接口請求時,在 Network
中發現,第一次請求的接口,都執行了兩次。原因是 React
使用了 StrictMode
模式,這是官方的預期行爲。
https://react.dev/reference/react/StrictMode
https://juejin.cn/post/7231842222782054461
運維資源申請
-
聯繫產品及運維,在不同環境的後臺管理系統增加新系統配置,以方便權限控制。
-
找運維進行不同環境的域名申請。
-
CDN
資源申請。
由於系統需要國內國外都能訪問,所以服務器資源需要全球加速。
申請 CDN
資源後,修改 vite.config.ts
中 base
字段的值。
// vite.config.ts
base: process.env.PKM_CDN_PATH || "/",
由於項目最後是由 PKM
發佈的,所以需要在 PKM
系統中申請簽名。將簽名信息在 CI
中配置。
添加 vconsole
項目部署後,在移動設備上使用,遇見問題時,不能像在 瀏覽器
中一樣按 F12
打開控制檯進行調試代碼。所以添加 vconsole
插件,打開時,可以看到 console
、network
等信息,方便 debug
。
> pnpm i vconsole -D
然後在 Authority.tsx
的頂部添加
// 非線上環境,開啓 VConsole,方便在飛書中查看日誌
if (process.env.ENV !== "online") {
new VConsole();
}
在 qa
及 sim
環境,可以看到 vconsole
信息,線上環境不會出現。
添加 Sentry
> pnpm i @sentry/react @sentry/tracing
在不同的環境中分別配置 Sentry
需要的信息。
// env.ts
SENTRY_DSN: '',
SENTRY_AUTH_TOKEN: '',
SENTRY_ORG: '',
SENTRY_PROJECT: '',
SENTRY_URL: '',
初始化 Sentry
// initSentry.ts
import * as Sentry from "@sentry/react";
import { BrowserTracing } from "@sentry/tracing";
// 初始化 Sentry
function initSentry() {
if (!process.env.SENTRY_DSN) return;
Sentry.init({
dsn: process.env.SENTRY_DSN as string,
tracesSampleRate: 1.0,
release: process.env.PKM_VERSION,
integrations: [new BrowserTracing()],
ignoreErrors: [
"Request failed with status code 403",
});
}
export default initSentry;
在剛纔寫的 ErroryBoundry.tsx
組件中添加錯誤數據上報。
// ErrorBoundary
// 錯誤處理
const ErrorBoundary = () => {
const error = useRouteError();
// ...
Sentry.captureException(error, {});
// ...
};
將 sourcemap
資源上傳到 Sentry
中,以便能更好地定位錯誤位置。
> pnpm i @sentry/vite-plugin
修改 vite.config.ts
,只在線上環境才上傳 sourcemap
到 Sentry
。
// vite.config.ts
import { sentryVitePlugin } from "@sentry/vite-plugin";
export default defineConfig({
build: {
sourcemap: isOnline,
},
plugins: [
// ...
isOnline
? sentryVitePlugin({
url: definedEnv.SENTRY_URL,
org: definedEnv.SENTRY_ORG,
project: definedEnv.SENTRY_PROJECT,
authToken: definedEnv.SENTRY_AUTH_TOKEN,
sourcemaps: {
ignore: ["node_modules"],
},
release: {
name: process.env.PKM_VERSION,
uploadLegacySourcemaps: {
paths: [path.join(process.cwd(), "/dist")],
urlPrefix: process.env.PKM_CDN_PATH || "~/",
},
},
})
: null,
]
注意如果公司有自己配置 Sentry
,一定要配置 url
,不然我們寫了 authToken
,會報 token invalid
錯誤。
添加 spark trace
使用的是字節的用戶信息收集。
添加 .gitlab-ci.yml
cache:
policy: pull
key: ${CI_COMMIT_REF_NAME}
paths:
- .pnpm-store
// ...
配置好對應的 CI
後,等運維將服務資源申請好,域名申請好後,部署到對應的環境中,進行一次測試,看流程是否跑通。
飛書工作臺申請
在飛書工作臺 https://open.feishu.cn/app 申請應用。根據要求填寫好申請信息,給需要看到的人配置權限,提交申請,審覈通過後,有權限的人就可以在飛書的工作臺中看到對應的應用,點擊添加就可以添加到工作臺中。
然後聯繫運維,配置對應環境與飛書的關聯,實現在飛書工作臺的免登錄操作。
其他優化
-
頁面跳轉時,自動滾動到最頂部
-
壓縮靜態圖片資源
-
包體積優化,按需引入
-
動態拆包
-
增加兼容性
頁面跳轉時,自動滾動到最頂部
Authority.tsx
中添加
// Authority.tsx
const location = useLocation();
useEffect(() => {
// 路由變化時,頁面滾動到最頂部
if (location.pathname) {
window.scrollTo({ top: 0 });
}
}, [location.pathname]);
縮靜態圖片資源
將圖片資源進行無損壓縮,減少空間佔用。
組件按需引入
使用 vite-plugin-imp
對部分組件進行按需引入。
> pnpm i vite-plugin-imp -D
修改 vite.config.ts
// vite.config.ts
export default defineConfig({
plugins: [
vitePluginImp({
libList: [
{
libName: "lodash",
libDirectory: "",
camel2DashComponentName: false,
},
{
libName: "antd-mobile",
libDirectory: "es/components",
style() {
return `antd-mobile/es/global/index.js`;
},
},
],
}),
]
})
拆包
默認情況下,打包後的 JS
只有一個,達到了 2M+
,在請求時,如果網絡較慢,這個資源可能等待的時間會比較久。
瀏覽器有一個特性,就是併發請求,同一時間可以同時請求多個資源,所以,使用 npx vite-bundle-visualizer -c vite.config.ts
命令對包體積進行分析,對包進行合理拆分。
// vite.config.ts
build: {
// ...
chunkSizeWarningLimit: 1024,
rollupOptions: {
output: {
manualChunks: (id) => {
const bigNodeModules = [
"html2canvas",
"cos-js-sdk",
"crypto-js",
"jsencrypt",
].some((item) => id.includes(item));
if (bigNodeModules) {
return "vendorLarge";
}
if (id.includes("node_modules")) {
return "vendor";
}
},
},
},
},
增加兼容性
現在較新的瀏覽器都支持較新的語法。但在監控平臺發現,還有好些用在使用類似 iOS 9
等版本較低的系統及瀏覽器。所以,我們還需要對較低版本的系統及瀏覽器增加兼容。
> pnpm i @vitejs/plugin-legacy terser -D
// vite.config.ts
plugins: [
// ...
legacy({
// 需要兼容的目標列表,可以設置多個
targets: ["defaults", "ie >= 11", "chrome >= 70"],
additionalLegacyPolyfills: ["regenerator-runtime/runtime"],
renderLegacyChunks: true,
// 下面的數組可以自定義添加低版本轉換的方法
polyfills: [
"es.symbol",
"es.array.filter",
"es.promise",
"es.promise.finally",
"es/map",
"es/set",
"es.array.for-each",
"es.object.define-properties",
"es.object.define-property",
"es.object.get-own-property-descriptor",
"es.object.get-own-property-descriptors",
"es.object.keys",
"es.object.to-string",
"web.dom-collections.for-each",
"esnext.global-this",
"esnext.string.match-all",
],
}),
注意
這個是在 build 時才生效,本地因爲 vite 使用的是 module 的方式打包構建的,所以本地加了這個,在本地啓動項目,用手機連接本地時,也是無法訪問的。
兼容的版本越低,生成的包的文件會越大。需要根據項目使用的具體情況,合理調整最小兼容版本。
https://github.com/vitejs/vite/tree/main/packages/plugin-legacy
最後
通過這次搭建,我更加清晰地瞭解了從 0
到 1
這樣一個完整的開發流程。除了正常業務開發外,對項目結構、公共組件設計、性能優化、兼容性、信息收集以及分析等都有了更進一步的瞭解及思考。以前不太明白的一些點,這次親身實踐後,都有了更清晰的認識。
雖然花了更多的時間,但收穫很多,非常充實。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/QPJltvJ_T-aIaYSE5h6uzA