手把手教你搭建一個React TS 項目模板


手把手教你搭建一個React TS 項目模板


前言

在小公司待了三年多,前端團隊很小很小,沒有前端大佬坐鎮,完全處於自我摸索的狀態。兩年前開始獨立負責前端項目,熱衷於自己手搭項目。對於那個時候的我來說,一切都處於朦朧的狀態,雖然有心想要把項目設計的更好,但是沒有什麼好的方向/思路(就比如剛開始寫項目,調用後端接口都是分散在每個模塊中的,沒有統一放在一個目錄下去維護,如果後端接口變了,就需要全局搜索一個個的去修改接口...)。後來閱讀了大量的書籍、文章、別人開源的項目以及慘痛的項目重構血淚史,漸漸地積累了一些項目經驗,有了自己的積累(配置項目模板、寫腳手架、搭建組件庫...),漸漸的往前端工程化這個方向靠。

寫這篇文章的目的:給那些和我相同處境、喜歡自己手搭項目的小夥伴們一個參考,讓初學者少走點彎路。如果有更好的建議還請告知,不勝感激。


項目文件樹結構


手把手教你搭建一個React TS 項目模板


項目特點


Normalize.css

CSS reset 相對“暴力”,不管你有沒有用,統統重置成一樣的效果,且影響的範圍很大,講求跨瀏覽器的一致性。Normalize.css 不講求樣式一致,而講求通用性和可維護性,是一種 CSS reset 的替代方案。它在默認的 HTML 元素樣式上提供了跨瀏覽器的高度一致性。相比於傳統的 CSS reset,Normalize.css 是一種現代的、為 HTML5 準備的優質替代方案。


默認支持 CSS 模塊化

  • 使用 css-loader 的參數配置實現 CSS 模塊化
<code>






<title>首頁/<title>
{%>
<link>




{%>




複製代碼/<code>


postcss-loader + autoprefixer

  • 自動兼容處理不同瀏覽器的樣式問題
<code>const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');

module.exports = {
mode: 'development',
context: path.resolve(__dirname, "../"),
entry: {
dllLibs: ['react', 'react-dom', 'lodash', 'antd', 'react-redux', 'redux','history', 'react-router-dom', 'connected-react-router','axios','events','moment','react-beautiful-dnd']
},
output: {
path: path.resolve('public'),
// 輸出的動態鏈接庫的文件名稱,[name] 代表當前動態鏈接庫的名稱
filename: 'dll/[name].dll.js',
// 默認是 var 這個全局變量,如果以這種方式導出的話,只能用腳本的方式進行全局訪問
libraryTarget: 'var',
// 存放動態鏈接庫的全局變量名稱,例如對應 libs 來說就是 _dll_libs
library: '_dll_[name]',
},
plugins: [
new DllPlugin({
// 動態鏈接庫的全局變量名稱,需要和 output.library 中保持一致

// 該字段的值也就是輸出的 manifest.json 文件 中 name 字段的值
// 例如 libs.manifest.json 中就有 "name": "_dll_libs"
name: '_dll_[name]',
// 描述動態鏈接庫的 manifest.json 文件輸出時的文件名稱
path: path.join('public', 'dll/[name].manifest.json'),
}),
]
};
複製代碼/<code>

postcss.config.js

<code>
// postcss-loader 會自動查找並調用這個文件
const autoprefixer = require('autoprefixer');
module.exports = {
plugins: [autoprefixer()],
};
複製代碼/<code>


自定義配置 html

  • 配置更加靈活,尤其是多頁面應用
<code>






<title>首頁/<title>
{%>
<link>




{%>




複製代碼/<code>

webpack.html.config.js

<code>export interface RouteConfigDeclaration {
/**
* 當前路由路徑
*/
path: string;
/**
* 當前路由名稱
*/
name?: string;
/**
* 是否嚴格匹配路由
*/
exact?: boolean;
/**
* 是否需要路由鑑權
*/
isProtected?: boolean;
/**
* 是否需要路由重定向
*/
isRedirect?: boolean;
/**
* 是否需要動態加載路由
*/
isDynamic?: boolean;
/**
* 動態加載路由時的提示文案
*/
loadingFallback?: string;
/**
* 路由組件
*/
component: any;

/**
* 子路由
*/
routes?: RouteConfigDeclaration[];
}
export const routesConfig: RouteConfigDeclaration[] = [
{
path: '/',
name: 'root-route',
component: App,
routes: [
{
path: '/home',
// exact: true,
isDynamic: true,
// loadingFallback: '不一樣的 loading 內容...',
// component: Home,
// component: React.lazy(
// () =>
// new Promise(resolve =>
// setTimeout(
// () =>
// resolve(
// import(/* webpackChunkName: "home"*/ '@src/views/home/Home'),
// ),
// 2000,
// ),
// ),
// ),
component: React.lazy(() =>
import(/* webpackChunkName: "home"*/ '@src/views/home/Home'),
),
routes: [
{
path: '/home/child-one',
isDynamic: true,
component: React.lazy(() =>
import(/* webpackChunkName: "child-one" */ '@src/views/home/ChildOne'),
),
},
{
path: '/home/child-two',
isRedirect: true,
isDynamic: true,
component: React.lazy(() =>
import(/* webpackChunkName: "child-two" */ '@src/views/home/ChildTwo'),
),
},
],
},

{
path: '/login',
isDynamic: true,
isRedirect: true,
component: React.lazy(() =>
import(
/* webpackChunkName: "login" */
'@src/views/login/Login'
),
),
},
{
path: '/register',
isDynamic: true,
component: React.lazy(() =>
import(/* webpackChunkName: "register"*/ '@src/views/register/Register'),
),
},
],
},
];
複製代碼/<code>


DllPlugin

  • 因為是將以前配置的模板進行了一次大升級,所以繼續沿用了這個依賴緩存插件,但是我習慣用在開發環境中,生產環境是不配置的,所以在新版本的 Webpack 開發環境中測試時,提升的速度不是很明顯,對於未來的 Webpack 5 來說,這個插件就更沒有使用的意義了。
<code>const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');

module.exports = {
mode: 'development',
context: path.resolve(__dirname, "../"),
entry: {

dllLibs: ['react', 'react-dom', 'lodash', 'antd', 'react-redux', 'redux','history', 'react-router-dom', 'connected-react-router','axios','events','moment','react-beautiful-dnd']
},
output: {
path: path.resolve('public'),
// 輸出的動態鏈接庫的文件名稱,[name] 代表當前動態鏈接庫的名稱
filename: 'dll/[name].dll.js',
// 默認是 var 這個全局變量,如果以這種方式導出的話,只能用腳本的方式進行全局訪問
libraryTarget: 'var',
// 存放動態鏈接庫的全局變量名稱,例如對應 libs 來說就是 _dll_libs
library: '_dll_[name]',
},
plugins: [
new DllPlugin({
// 動態鏈接庫的全局變量名稱,需要和 output.library 中保持一致
// 該字段的值也就是輸出的 manifest.json 文件 中 name 字段的值
// 例如 libs.manifest.json 中就有 "name": "_dll_libs"
name: '_dll_[name]',
// 描述動態鏈接庫的 manifest.json 文件輸出時的文件名稱
path: path.join('public', 'dll/[name].manifest.json'),
}),
]
};
複製代碼/<code>


@babel/preset-typescript

  • 使用 @babel/preset-typescript 轉譯 TS,如果想要校驗 TS 文件,只需執行 npm run type-check

package.json

<code>export interface RouteConfigDeclaration {
/**
* 當前路由路徑
*/
path: string;
/**
* 當前路由名稱
*/
name?: string;
/**
* 是否嚴格匹配路由
*/
exact?: boolean;
/**
* 是否需要路由鑑權
*/
isProtected?: boolean;
/**
* 是否需要路由重定向
*/
isRedirect?: boolean;
/**
* 是否需要動態加載路由
*/
isDynamic?: boolean;
/**
* 動態加載路由時的提示文案
*/
loadingFallback?: string;
/**
* 路由組件
*/
component: any;
/**
* 子路由
*/
routes?: RouteConfigDeclaration[];
}
export const routesConfig: RouteConfigDeclaration[] = [
{
path: '/',
name: 'root-route',
component: App,

routes: [
{
path: '/home',
// exact: true,
isDynamic: true,
// loadingFallback: '不一樣的 loading 內容...',
// component: Home,
// component: React.lazy(
// () =>
// new Promise(resolve =>
// setTimeout(
// () =>
// resolve(
// import(/* webpackChunkName: "home"*/ '@src/views/home/Home'),
// ),
// 2000,
// ),
// ),
// ),
component: React.lazy(() =>
import(/* webpackChunkName: "home"*/ '@src/views/home/Home'),
),
routes: [
{
path: '/home/child-one',
isDynamic: true,
component: React.lazy(() =>
import(/* webpackChunkName: "child-one" */ '@src/views/home/ChildOne'),
),
},
{
path: '/home/child-two',
isRedirect: true,
isDynamic: true,
component: React.lazy(() =>
import(/* webpackChunkName: "child-two" */ '@src/views/home/ChildTwo'),
),
},
],
},
{
path: '/login',
isDynamic: true,
isRedirect: true,
component: React.lazy(() =>
import(
/* webpackChunkName: "login" */
'@src/views/login/Login'
),
),

},
{
path: '/register',
isDynamic: true,
component: React.lazy(() =>
import(/* webpackChunkName: "register"*/ '@src/views/register/Register'),
),
},
],
},
];
複製代碼/<code>

tsconfig.json

<code>{
"compilerOptions": {
// 不生成文件,只做類型檢查
"noEmit": true,
}
}
複製代碼/<code>
  • 更多 TS 轉譯方案,請看 Webpack 轉譯 Typescript 現有方案


tsconfig-paths-webpack-plugin

tsconfig.json

<code>{
"compilerOptions": {
// 在解析非絕對路徑模塊名的時候的基準路徑
"baseUrl": "./",
"paths": {
/*路徑映射的集合*/
"@public/*": ["public/*"],
"@src/*": ["src/*"],

"@assets/*": ["src/assets/*"],
"@styles/*": ["src/assets/styles/*"],
"@common/*": ["src/common/*"],
"@components/*": ["src/components/*"],
"@library/*": ["src/library/*"],
"@routes/*": ["src/routes/*"],
"@store/*": ["src/store/*"],
"@server/*": ["src/server/*"],
"@api/*": ["src/server/api/*"],
"@utils/*": ["src/utils/*"]
}
}
}
複製代碼/<code>

webpack.base.config.js

<code>resolve: {
plugins: [
// 將 tsconfig.json 中的路徑配置映射到 webpack 中
new TsconfigPathsPlugin({
configFile: './tsconfig.json'
})
],
// 因為使用了 TsconfigPathsPlugin 插件,所以這裡就不需要再映射路徑了
// alias: {
// "@src": path.resolve('src'),
// "@public": path.resolve('public'),
// "@assets": path.resolve('src/assets'),
// },
}
複製代碼/<code>


支持配置式路由+路由懶加載

<code>export interface RouteConfigDeclaration {
/**
* 當前路由路徑
*/
path: string;
/**
* 當前路由名稱

*/
name?: string;
/**
* 是否嚴格匹配路由
*/
exact?: boolean;
/**
* 是否需要路由鑑權
*/
isProtected?: boolean;
/**
* 是否需要路由重定向
*/
isRedirect?: boolean;
/**
* 是否需要動態加載路由
*/
isDynamic?: boolean;
/**
* 動態加載路由時的提示文案
*/
loadingFallback?: string;
/**
* 路由組件
*/
component: any;
/**
* 子路由
*/
routes?: RouteConfigDeclaration[];
}
export const routesConfig: RouteConfigDeclaration[] = [
{
path: '/',
name: 'root-route',
component: App,
routes: [
{
path: '/home',
// exact: true,
isDynamic: true,
// loadingFallback: '不一樣的 loading 內容...',
// component: Home,
// component: React.lazy(

// () =>
// new Promise(resolve =>
// setTimeout(
// () =>
// resolve(
// import(/* webpackChunkName: "home"*/ '@src/views/home/Home'),
// ),
// 2000,
// ),
// ),
// ),
component: React.lazy(() =>
import(/* webpackChunkName: "home"*/ '@src/views/home/Home'),
),
routes: [
{
path: '/home/child-one',
isDynamic: true,
component: React.lazy(() =>
import(/* webpackChunkName: "child-one" */ '@src/views/home/ChildOne'),
),
},
{
path: '/home/child-two',
isRedirect: true,
isDynamic: true,
component: React.lazy(() =>
import(/* webpackChunkName: "child-two" */ '@src/views/home/ChildTwo'),
),
},
],
},
{
path: '/login',
isDynamic: true,
isRedirect: true,
component: React.lazy(() =>
import(
/* webpackChunkName: "login" */
'@src/views/login/Login'
),
),
},
{
path: '/register',
isDynamic: true,
component: React.lazy(() =>
import(/* webpackChunkName: "register"*/ '@src/views/register/Register'),
),
},

],
},
];
複製代碼/<code>


ESLint+Prettier

  • 使用 ESLint +Prettier 來統一前端代碼風格
  • 可以在編輯器中進行配置,當文件保存時自動格式化代碼。在 WebStorm 中使用 Prettier 自動格式化代碼。


husky + lint-staged

  • 在提交代碼前,進行代碼風格校驗並修復:每次提交時,只檢查本次提交所修改的文件(相比 git 暫存區),節省了大量的時間。
<code>"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
]
},
複製代碼/<code>


rematch

  • rematch 參考了 Dva和Mirror,在 redux 的基礎上進行了二次封裝,在 rematch 沒有多餘的 action types、action creators、switch 語句、thunks、saga 以及繁瑣的 store 配置,極大的簡化了 redux 的使用成本。


手把手教你搭建一個React TS 項目模板


models/register/index.ts

<code>// 添加狀態
const INCREMENT = 'INCREMENT';

import { RootDispatch, RootState } from '@src/store';

export interface RegisterStateDeclaration {
pageName?: string;
count: number;
}

const state: RegisterStateDeclaration = {
pageName: 'register',
count: 0,
};

export default {
name: 'register',
state,
reducers: {
[INCREMENT]: (state: RegisterStateDeclaration, payload): RegisterStateDeclaration => {
// 打印輸出的是一個 proxy 代理實例對象
// console.log(state);
state.count += 1;
// 最終要返回整棵 state 樹(當前 model 的 state 樹——login)
return state;
},
},
// 兩種寫法:一種用常量作為 key ,一種直接定義方法
effects: (dispatch: RootDispatch) => ({
// async incrementAsync(payload, rootState: RootState) {
async incrementAsync() {
await new Promise(resolve =>
setTimeout(() => {
resolve();
}, 1000),
);
// 派發 login 裡面的 action
// dispatch.login.INCREMENT();
this.INCREMENT();
},
}),
// effects: {
// async incrementAsync(payload, rootState: RootState) {
// await new Promise(resolve =>

// setTimeout(() => {
// resolve();
// }, 1000),
// );
// this.INCREMENT();
// },
// },
};
複製代碼/<code>


events

  • 使用 events 創建一個全局的事件中心(發佈訂閱),雖然項目中已經用 redux 作為全局通信的工具,但在某些情況下,還是得依賴事件訂閱和通知。


utils

  • 內置了一些好用的工具函數,如下:
<code>/**
* 檢測變量類型
* @param type
*/
function isType(type) {
return function(value): boolean {
return Object.prototype.toString.call(value) === `[object ${type}]`;
};
}

export const variableTypeDetection = {
isNumber: isType('Number'),
isString: isType('String'),
isBoolean: isType('Boolean'),

isNull: isType('Null'),
isUndefined: isType('Undefined'),
isSymbol: isType('Symbol'),
isFunction: isType('Function'),
isObject: isType('Object'),
isArray: isType('Array'),
};
複製代碼/<code>


項目地址 react-ts-project-template

源代碼:https://github.com/yjd-cli/react-ts-project-template


分享到:


相關文章: