寫給前端新人:從 0到1 搭建一個(gè)前端項(xiàng)目,都需要做什么?
點(diǎn)擊上方關(guān)注 前端技術(shù)江湖,一起學(xué)習(xí),天天進(jìn)步
不使用腳手架搭建 React 項(xiàng)目:https://github.com/zhuyuanmin/react-0-1-build。讀者可根據(jù)提交的分支順序一步步搭建,所以庫都使用了最新版本,讓我們?cè)诓瓤又谐砷L!【master 分支:完整版,不包含 typescript ;typescript-react 分支: 包含 typescript 的完整版本】
一、項(xiàng)目啟動(dòng)
了解需求背景
了解業(yè)務(wù)流程
二、項(xiàng)目搭建初始化
本案例使用腳手架 create-react-app 初始化了項(xiàng)目。此腳手架有利有弊吧,項(xiàng)目目錄結(jié)構(gòu)簡潔,不需要太關(guān)心 webpack 令人頭疼的配置;弊端在于,腳手架確實(shí)有些龐大,構(gòu)建時(shí)間在 4mins 左右。各位看官擇優(yōu)選擇吧,也可以完全自己搭建一個(gè)項(xiàng)目。
設(shè)置淘寶鏡像倉庫
$ yarn config set registry registry.npm.taobao.org/ -g
$ yarn config set sass_binary_site cdn.npm.taobao.org/dist/node-sass -g
工程目錄 init
$ create-react-app qpj-web-pc --typescript
$ tree -I "node_modules"
.
|-- README.md
|-- package.json
|-- public
| |-- favicon.ico
| |-- index.html
| |-- logo192.png
| |-- logo512.png
| |-- manifest.json
| `-- robots.txt
|-- src
| |-- App.css
| |-- App.test.tsx
| |-- App.tsx
| |-- index.css
| |-- index.tsx
| |-- logo.svg
| |-- react-app-env.d.ts
| |-- reportWebVitals.ts
| `-- setupTests.ts
`-- tsconfig.json
yarn build試試
$ yarn build & tree -I "node_modules"
.
|-- README.md
|-- build/ # 改造點(diǎn)(由于 `Jenkins` 構(gòu)建打包腳本有可能已經(jīng)寫死了 `dist` 包名)
|-- package.json
|-- public
| |-- favicon.ico
| |-- index.html
| |-- logo192.png
| |-- logo512.png
| |-- manifest.json
| `-- robots.txt
|-- src
| |-- App.css
| |-- App.test.tsx
| |-- App.tsx
| |-- index.css
| |-- index.tsx
| |-- logo.svg
| |-- react-app-env.d.ts
| |-- reportWebVitals.ts
| `-- setupTests.ts
`-- tsconfig.json
連接 git遠(yuǎn)程倉庫
$ git remote add origin yuanmin.zhu%40wetax.com.cn:[email protected]/front/qpj-web-pc.git
添加 .gitignore
$ echo -e " yarn.lock \n package-lock.json \n /dist \n .idea" >> .gitignore
添加 eslint代碼及提交評(píng)論校驗(yàn)
$ yarn add husky lint-staged @commitlint/cli @commitlint/config-conventional -D
$ npx husky install
$ npx husky add .husky/pre-commit "npx lint-staged"
$ npx husky add .husky/prepare-commit-msg "npx commitlint -e"
項(xiàng)目根目錄新建 commitlint.config.js
// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'revert'],
],
'subject-full-stop': [0, 'never'],
'subject-case': [0, 'never'],
},
}
vscode擴(kuò)展中搜索ESLint并安裝,項(xiàng)目根目錄新建.eslintrc.js,內(nèi)容可參考文章配置:zhuanlan.zhihu.com/p/84329603 看第五點(diǎn)Commit message 格式說明
<type>: <subject>
type值枚舉如下:feat: 添加新特性 fix: 修復(fù) bug docs: 僅僅修改了文檔 style: 僅僅修改了空格、格式縮進(jìn)、都好等等,不改變代碼邏輯 refactor: 代碼重構(gòu),沒有加新功能或者修復(fù) bug perf: 增加代碼進(jìn)行性能測(cè)試 test: 增加測(cè)試用例 chore: 改變構(gòu)建流程、或者增加依賴庫、工具等 revert: 當(dāng)前 commit 用于撤銷以前的 commit subject是 commit 目的的簡短描述,不超過 50 個(gè)字符,且結(jié)尾不加句號(hào)(.)package.json新加入如下配置:
{
...,
"lint-staged": {
"src/**/*.{jsx,txs,ts,js,json,css,md}": [
"eslint --quiet"
]
},
}
可執(zhí)行 npx eslint [filePath] \--fix進(jìn)行格式修復(fù),無法修復(fù)的需手動(dòng)解決
三、項(xiàng)目配置一(功能配置)
安裝項(xiàng)目常用依賴庫
$ yarn add antd axios dayjs qs -S # UI 庫 及工具庫
$ yarn add react-router-dom redux react-redux redux-logger redux-thunk -S # 路由及狀態(tài)管理
webpack配置拓展很有必要
根目錄新建 config-overrides.js,詳細(xì)使用可訪問:簡書:React 之 config-overrides文件配置安裝 $ yarn add react-app-rewired customize-cra -D
修改 package.json中啟動(dòng)項(xiàng)
// package.json
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
}
使用
// config-overrides.js
const {
override, // 主函數(shù)
fixBabelImports, // 配置按需加載
addWebpackExternals, // 不做打包處理配置
addWebpackAlias, // 配置別名
addLessLoader // lessLoader 配置,可更改主題色等
} = require('customize-cra')
module.exports = override(/* ... */, config => config)
配置按需加載
// config-overrides.js
...
module.exports = override(
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es', // library 目錄
style: true, // 自動(dòng)打包相關(guān)的樣式
}),
)
更改主題色
// config-overrides.js
...
module.exports = override(
addLessLoader({
lessOptions: {
javascriptEnabled: true,
modifyVars: {
'@primary-color': '#1890ff',
},
}
}),
)
別名配置( typescript項(xiàng)目這里有坑)
// config-overrides.js
const path = require('path')
...
module.exports = override(
addWebpackAlias({
'@': path.resolve(__dirname, 'src'),
}),
)
去除注釋、多進(jìn)程打包壓縮
// config-overrides.js
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
...
module.exports = override(/* ... */, config => {
config.plugins = [...config.plugins, {
new UglifyJsPlugin({
uglifyOptions: {
warnings: false,
compress: {
drop_debugger: true,
drop_console: true,
},
},
}),
new HardSourceWebpackPlugin()
}]
return config
})
解決埋下的兩個(gè)坑
修改打包出的文件夾名為 dist
// 修改打包路徑除了output,這里也要修改
const paths = require('react-scripts/config/paths')
paths.appBuild = path.join(path.dirname(paths.appBuild), 'dist')
module.exports = override(/* ... */, config => {
config.output.path = path.resolve(__dirname, 'dist')
return config
})
解決 typescript別名配置查閱相關(guān)資料,需要在 tsconfig.json中添加一項(xiàng)配置
{
...
"extends": "./paths.json"
}
新建文件 paths.json
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@/*": ["*"]
}
}
}
配置裝飾器寫法
{
"compilerOptions": {
"experimentalDecorators": true,
...
}
}
配置開發(fā)代理
在 src目錄新建setupProxy.js
// src/setupProxy.js
const proxy = require('http-proxy-middleware').createProxyMiddleware
module.exports = function(app) {
// app 為 Express 實(shí)例,此處可以寫 Mock 數(shù)據(jù)
app.use(
proxy('/api',
{
"target": "https://qpj-test.fapiaoer.cn",
"changeOrigin": true,
"secure": false,
// "pathRewrite": {
// "^/api": ""
// }
})
)
}
加入 polyfill和antd組件國際化處理
// src/index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
// 注入 store
import { Provider } from 'react-redux'
import store from '@/store/store'
import { ConfigProvider, Empty } from 'antd'
import App from './App'
import zhCN from 'antd/es/locale/zh_CN'
import 'moment/locale/zh-cn'
// polyfill
import 'core-js/stable'
import 'regenerator-runtime/runtime'
ReactDOM.render(
<Provider store={store}>
<ConfigProvider locale={zhCN} renderEmpty={Empty}>
<App />
</ConfigProvider>
</Provider>,
document.getElementById('root')
)
CSS Modules
create-react-app自帶支持以xxx.module.(c|le|sa)ss的樣式表文件,使用上typescript項(xiàng)目中要注意:
const styles = require('./index.module.less')
retrun (
<div className={`${styles.container}`}>
<Table
columns={columns}
className={`${styles['border-setting']}`}
dataSource={props.store.check.items}
rowKey={record => record.id}
pagination={false}
/>
<div className="type-check-box"></div>
</div>
)
// index.module.less
.container {
padding: 24px;
background-color: #fff;
height: 100%;
overflow: auto;
.border-setting {
tr {
td:nth-child(3) {
border-left: 1px solid #F0F0F0;
border-right: 1px solid #F0F0F0;
}
}
td {
text-align: left !important;
}
}
:global { // 這個(gè)標(biāo)識(shí)之后,其子代元素可以不需要使用 `styles['type-check-box']` 的方式,直接寫 `className`
.type-check-box {
.ant-checkbox-wrapper + .ant-checkbox-wrapper{
margin-left: 0;
}
}
}
}
【新】配置 React jsx指令式屬性r-if、r-for、r-model、r-show,提升開發(fā)效率:
安裝依賴
$ yarn add babel-react-rif babel-react-rfor babel-react-rmodel babel-react-rshow -D
配置 .babelrc:
// .babelrc
{
...,
"plugins": [
"babel-react-rif",
"babel-react-rfor",
"babel-react-rmodel",
"babel-react-rshow",
]
}
使用示例: r-if
<div>
<h1 r-if={height < 170}>good</h1>
<h1 r-else-if={height > 180}>best</h1>
<h1 r-else>other</h1>
</div>
r-for
{/* eslint-disable-next-line no-undef */}
<div r-for={(item, index) in [1, 2, 3, 4]} key={index}>
內(nèi)容 {item + '-' + index}
</div>
r-model
<input onChange={this.callback} type="text" r-model={inputVale} />
r-show
<div r-show={true}>內(nèi)容</div> # 注意:這是 `r-if` 的效果,不會(huì)渲染節(jié)點(diǎn)
四、項(xiàng)目配置二(優(yōu)化配置)
實(shí)現(xiàn)組件懶加載 react-loadable
import Loadable from 'react-loadable'
const Loading = (props: any) => {
if (props.error) {
console.error(props.error)
return <div>Error! <Button type="link" onClick={props.retry}>Retry</Button></div>
} else if (props.timedOut) {
return <div>Timeout! <Button onClick={props.retry}>Retry</Button></div>
} else if (props.pastDelay) {
return <div>Loading...</div>
} else {
return null
}
}
const loadable = (path: any) => {
return Loadable({
loader: () => import(`@/pages${path}`),
loading: Loading,
delay: 200,
timeout: 10000,
})
}
const Home = loadable('/homePage/Home')
處理 axios攔截響應(yīng)
const service = axios.create({
baseURL: '/',
timeout: 15000,
})
service.interceptors.request.use(function (config) {
return config
})
service.interceptors.response.use(function (config) {
return config
})
處理 React router的嵌套配置
我們知道
React中不支持類似Vue Router路由配置方式,React中一切皆組件,路由也是組件,需要用到路由要臨時(shí)加上路由組件,寫起來就很繁瑣,但我們可以自己實(shí)現(xiàn)路由配置表方式。
// router/router.config.ts
const routes = [
{
path: '/home',
component: loadable('components/Index'),
exact: true,
},
{
path: '/new',
component: loadable('components/New'),
redirect: '/new/list',
// exact: true,
routes: [
{
path: '/new/list',
component: loadable('components/NewList'),
exact: true,
},
{
path: '/new/content',
component: loadable('components/NewContent'),
exact: true,
},
],
},
]
export default routes
// router/router.ts
import React from 'react'
import { Switch, BrowserRouter as Router, Route } from 'react-router-dom'
import routes from './index'
function mapRoutes(routes: any[], store: object): any {
return routes.map((item: any, index: number) => {
return (
<Route exact={item.exact || false} path={item.path} key={index} render={props => {
const NewComp = item.component
Object.assign(props, {
redirect: item.redirect || null,
permission: item.permission || [],
...store
})
if (item.routes) {
return <NewComp {...props}>{ mapRoutes(item.routes, store) }</NewComp>
} else {
return <NewComp {...props} />
}
}} />
)
})
}
const Routes = (props: any) => {
return (
<Router>
<Switch>
{ mapRoutes(routes, props.store) }
<Route component={() => (<div>404 Page not Found!</div>)} />
</Switch>
</Router>
)
}
export default Routes
子路由承載頁面需要加上如下代碼:
import { Redirect, Route, Switch } from 'react-router-dom'
<Switch>
{props.children}
<Route component={() => (<div>404 Page not Found!</div>)} />
{props.redirect && <Redirect to={props.redirect} />}
</Switch>
處理 React store
// store/store.ts
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import logger from 'redux-logger'
import reducers from './reducer'
const store = process.env.NODE_ENV === 'development'
? createStore(reducers, applyMiddleware(thunk, logger))
: createStore(reducers, applyMiddleware(thunk))
export default store
為了方便使用,避免每個(gè)組件都需要
connect,這邊實(shí)現(xiàn)了redux store的全局注入,但是如果項(xiàng)目龐大的話就會(huì)損耗些性能。
// App.tsx
import { dispatchActions } from '@/store/reducer'
export default connect((state: any) => ({ store: state }), dispatchActions)(App)
五、總結(jié)
自此項(xiàng)目搭建就全部完成了,剩下的就是業(yè)務(wù)代碼了。相信你可以得到如下收獲:
① 項(xiàng)目構(gòu)建在宏觀上有個(gè)極大的能力提升;
② 項(xiàng)目整體功能了解清晰;
③ 排查問題不慌亂;
④ 封裝能力有加強(qiáng);
⑤ 業(yè)務(wù)功能很清楚。
六、題外話
基于
create-react-app創(chuàng)建的React項(xiàng)目,本人實(shí)現(xiàn)了一個(gè)腳手架,以上配置默認(rèn)已經(jīng)全部加入實(shí)現(xiàn),歡迎Github試用并star。鏈接:https://github.com/zhuyuanmin/zym-cli
關(guān)于本文
作者:前端小猿_zym
https://juejin.cn/post/6953807616082460702
The End
歡迎自薦投稿到《前端技術(shù)江湖》,如果你覺得這篇內(nèi)容對(duì)你挺有啟發(fā),記得點(diǎn)個(gè) 「在看」哦
點(diǎn)個(gè)『在看』支持下 
