【開源】一個 React + TS 項目模板

前言

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

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

項目文件樹結構

tree.png

項目特色

Normalize.css

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

默認支持 CSS 模塊化

  • 使用 css-loader 的參數配置實現 CSS 模塊化
module: {
   rules: [
     {
       test: /\.css$/,
       use: ['style-loader','css-loader']
     },
     {
       test: /\.less$/,
       use: [
         'style-loader',
         {
           loader: 'css-loader',
           options: {
             importLoaders: 2,
             localsConvention: 'camelCase',
             modules: {
               localIdentName: '[name]__[local]--[hash:base64:5]'
             },
           }
         },
         'less-loader'
       ]
     }
   ],
}
複製代碼

postcss-loader + autoprefixer

  • 自動兼容處理不一樣瀏覽器的樣式問題
module: {
  rules: [
    {
      test: /\.css$/,
      use: ['style-loader','css-loader', 'postcss-loader']
    },
    {
      test: /\.less$/,
      use: [
        'style-loader',
        {
          loader: 'css-loader',
          options: {
            importLoaders: 2,
            localsConvention: 'camelCase',
            modules: {
              localIdentName: '[name]__[local]--[hash:base64:5]'
            },
          }
        },
        'postcss-loader',
        'less-loader'
      ]
    }
  ],
}
複製代碼

postcss.config.js前端

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

自定義配置 html

  • 配置更加靈活,尤爲是多頁面應用
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1"/>
    <meta name="renderer" content="webkit"/>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>首頁</title>
    <%htmlWebpackPlugin.options.dependencies.css.forEach(css=>{%>
    <link rel="stylesheet" href="<%= css %>">
    <%})%>
</head>
<body>
<div id="root"></div>
<%htmlWebpackPlugin.options.dependencies.js.forEach(js=>{%>
<script src="<%=js%>"></script>
<%})%>
</body>
</html>
複製代碼

webpack.html.config.jsjava

const HtmlWebpackPlugin = require('html-webpack-plugin');

/** * 生產環境資源包路徑配置 */
const libs = {
    // iconfont: {
    // js: ['./fonts/iconfont.js'],
    // css: ['./fonts/iconfont.css']
    // },
};

/** * 開發環境資源包路徑配置 */
if (process.env.NODE_ENV === 'development') {
    Object.assign(libs, {
        // iconfont:{
        // js: ['//at.alicdn.com/t/font_xxx.js'],
        // css: ['//at.alicdn.com/t/font_xxx.css']
        // },
        dll: {
            js: ['/public/dll/dllLibs.dll.js']
        }
    });
}

function createHtmlWebpackPluginConfig(chunkName, path, modules, chunks) {
    const config = {
        favicon: './src/entry/favicon.ico',
        filename: `${chunkName}.html`,
        template: `${path || './src/entry/index.html'}`,
        inject: true,
        chunks: ['vendors', chunkName].concat(chunks),
        chunksSortMode: 'dependency',
        minify: {
            removeComments: true,
            collapseWhitespace: false
        },
        dependencies: {
            css: [],
            js: []
        },
    };
    modules && modules.forEach(m => {
        if (m && m.css && m.css.length > 0) {
            config.dependencies.css = config.dependencies.css.concat(m.css);
        }
        if (m && m.js && m.js.length > 0) {
            config.dependencies.js = config.dependencies.js.concat(m.js);
        }
    });
    return new HtmlWebpackPlugin(config);
}

module.exports = [
    createHtmlWebpackPluginConfig('index', '', [libs.dll]),
];
複製代碼

DllPlugin

  • 由於是將之前配置的模板進行了一次大升級,因此繼續沿用了這個依賴緩存插件,可是我習慣用在開發環境中,生產環境是不配置的,因此在新版本的 Webpack 開發環境中測試時,提高的速度不是很明顯,對於將來的 Webpack 5 來講,這個插件就更沒有使用的意義了。
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'),
        }),
    ]
};
複製代碼

@babel/preset-typescript

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

package.jsonreact

"scripts": {
     "type-check": "tsc --watch",
}
複製代碼

tsconfig.jsonwebpack

{
    "compilerOptions": {
       // 不生成文件,只作類型檢查
        "noEmit": true,
    }
}
複製代碼

tsconfig-paths-webpack-plugin

tsconfig.jsonios

{
    "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/*"]
        }
    }
}
複製代碼

webpack.base.config.jsgit

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'),
      // },
}
複製代碼

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

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'),
                ),
            },
        ],
    },
];
複製代碼

ESLint+Prettier

husky + lint-staged

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

rematch

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

image.png

models/register/index.tsgithub

// 添加狀態
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();
    // },
    // },
};
複製代碼

events

  • 使用 events 建立一個全局的事件中心(發佈訂閱),雖然項目中已經用 redux 做爲全局通訊的工具,但在某些狀況下,仍是得依賴事件訂閱和通知。

utils

  • 內置了一些好用的工具函數,以下:
/** * 檢測變量類型 * @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'),
};
複製代碼

項目地址

react-ts-project-template

參考

使用 ESLint + Prettier 來統一前端代碼風格

用 husky 和 lint-staged 構建超溜的代碼檢查工做流

推薦閱讀

TS 常見問題整理(60多個,持續更新ing)

你真的瞭解 React 生命週期嗎

React Hooks 詳解 【近 1W 字】+ 項目實戰

React SSR 詳解【近 1W 字】+ 2個項目實戰

從 0 到 1 實現一款簡易版 Webpack

Webpack 轉譯 Typescript 現有方案

傻傻分不清之 Cookie、Session、Token、JWT

相關文章
相關標籤/搜索