Проект React+Typescript шагает в яму

React.js
Проект React+Typescript шагает в яму

предисловие

  • В проекте используется react 16.8.x, typescript 3.5.3.
  • тогда также используйтеkoa2+typescriptвзял одинПростойФоновая служба API используется только для проверки использования инкапсулированного Axios Api, а также для личных игровых потребностей node.js🙃, не требует операций с базой данных и т. д. . . код можно поставить штампздесь
  • Кстати, я обновил webpack4
  • Тогда это просто пустой шаблон для проверки чего-то, всего несколько простых демонстрационных страниц, остальные удалены. . .
  • Исходный код этого проекта можно увидетьздесь
  • Обновление: [2019-09-05]: электрон, вы можете видетьздесь
  • Обновление: [2019-09-09]: сторонние ресурсы используют CDN (см. 13, сборка)
  • Обновление: [2019-11-08]: Управление состояниемredux+rematchзаменитьmobx, предварительная загрузка ресурсов, предварительная выборка и т. д.

1. Создайте проект

Официальная демонстрация antd здесь не используется., но добавьте antd в обычный проект react+typescript, а затем преобразуйте его

Почему бы не использовать официальную демо-версию antd?Потому что я могу использовать его после того, как попробовал, но веб-пакет не может установить псевдоним, всегда есть проблема, поэтому мне это не нужно. . .

create-react-app project --typescript

исходная структура:

.
├── api
├── assets
├── components
├── lang
├── routes
├── store
├── utils
├── views
├── App.scss
├── App.test.tsx
├── App.tsx
├── index.scss
├── index.tsx
├── router.tsx
└── setupProxy.js

2. Машинопись

tsconfig.json:

{
  "compilerOptions": {
    "baseUrl": "src",
    "outDir": "build/dist",
    "module": "esnext",
    "target": "es5",
    "lib": ["es6", "dom"],
    "sourceMap": true,
    "allowJs": true,
    "jsx": "preserve",
    "moduleResolution": "node",
    "rootDir": ".",
    "forceConsistentCasingInFileNames": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noImplicitAny": true,
    "importHelpers": true,
    "strictNullChecks": true,
    "suppressImplicitAnyIndexErrors": true,
    "noUnusedLocals": true,
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "paths": {
      "@/*": ["./*"]
    }
  },
  "awesomeTypescriptLoaderOptions": {
    "useBabel": true,
    "useCache": false,
    "emitRequireType": false
  },
  "includes": [
    "src"
  ],
  "exclude": [
    "node_modules",
    "build",
    "scripts",
    "acceptance-tests",
    "webpack",
    "jest",
    "src/setupTests.ts",
    "public/"
  ]
}

.babelrc

{
  "presets": [
    "react-app"
  ],
  "plugins": [
    "transform-decorators-legacy", 
    [
      "import",
      {
        "libraryName": "antd-mobile",
        "style": "css"
      }
    ]
  ]
}

3. Обновите webpack4.x

добавить в webpack.config.dev.jsmodeПоле:mode: 'development'
добавить в webpack.config.prod.jsmodeПоле:mode: 'production'

Связанные модули, которые необходимо обновить:

yarn upgrade **обновить или напрямуюyarn add ** -Dтакже может

  • file-loader
  • fork-ts-checker-webpack-plugin
  • html-webpack-plugin@next
  • react-dev-utils
  • url-loader
  • webpack
  • webpack-cli
  • webpack-dev-server
  • webpack-manifest-plugin

Частичный контроль качества

  1. Ошибка компиляции: webpack не является функцией

    Обновите соответствующий плагин выше, затем script/start.js:
    const compiler = createCompiler(webpack, config, appName, urls, useYarn);Измените его на:
    const compiler = createCompiler({webpack, config, appName, urls, useYarn});

  2. Ошибка компиляции: this.htmlWebpackPlugin.getHooks не является функцией.

    Уведомлениеhtml-webpack-plugin@nextЭтот плагин должен добавить @next
    config/webpack.comfig.dev.js, config/webpack.config.prod.js:
    new InterpolateHtmlPlugin(env.raw)Измените его на:
    new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw)

  3. После упаковки сообщается об ошибке загрузки чанка.

    config/paths.js:

    function getServedPath(appPackageJson) {
      const publicUrl = getPublicUrl(appPackageJson);
      const servedUrl = envPublicUrl ||
        (publicUrl ? url.parse(publicUrl).pathname : '/');
      return ensureSlash(servedUrl, true);
    }
    

    положить'/'изменить на'./'Только что

  4. Сообщить @types/tapable @types/html-minifier @types/webpack не существует

    yarn add @types/tapable @types/html-minifier @types/webpack
    

4. и тд

yarn add antd

нагрузка по требованию

  • ts/tsxиспользоватьawesome-typescript-loaderАнализ этого загрузчика
  • antdCSS компонента загружается по требованию и используетсяbabel-plugin-importэтот плагин
yarn add awesome-typescript-loader babel-plugin-import
// webpack.config.dev.js, webpack.config.prod.js
{
    test: /\.(ts|tsx)$/,
    include: paths.appSrc,
    loader: 'awesome-typescript-loader',
    exclude: /node_modules/,
    options: {
      babelOptions: {
        "presets": ["react"],
        "plugins": [
          [
            "import", 
            { 
              "libraryName": "antd", 
              "style": "css" 
            }
          ]
        ]
      }
    }
  },

5. Контроль маршрутизации/авторизации

Маршруты загружаются и используются по запросу@loadable/component
Если вы сообщаете об ошибке @types/xxx, просто следуйте инструкциям по установке, если нет, установите вручную.common.d.tsдобавить одинdeclare module '@loadable/component';

yarn add @loadable/component

маршрутизация

  • Маршрутизация в приложении

Благодаря следующему мы можем реализовать метод написания вложенных маршрутов внутри приложения в Vue.props.childrenЭквивалент Vuerouter-view,ПотомHeaderПодождите, пока глобальный компонент будет смонтирован только один раз

// src/router.tsx
...
<AuthRoute 
  path='/' 
  render={() => (
    <App>
      <Switch>
        {routes.map(route => route)}
      </Switch>
    </App>
  )}
/>
...
  • Маршрутизация не зависит от приложения

aloneComp

// src/router.tsx
<Switch>
  {
    aloneComp.map(route => route)
  }
  <AuthRoute 
    path='/' 
    render={() => (
      <App>
        <Switch>
          {routes.map(route => route)}
        </Switch>
      </App>
    )}
  />
</Switch>
// src/App.tsx
...
  public render() {
    return (
      <div className={style.app}>
        <Header />
        { this.props.children }
      </div>
    );
  }

управление маршрутом

  • Маршрут унифицированное управление
// src/routes/index.tsx
import login from './login-register';
import home from './home';

/**
 * 使用这个组件 '@/routes/auth-route',代替官方 Route,控制需要登录权限的路由
 */
export default [
  ...login,
  ...home
]
  • модуль маршрутизации
// src/routes/home.tsx
import AuthRoute from '@/routes/auth-route';
import * as React from 'react';
import Loadable from '@loadable/component';

// home
export default [
  <AuthRoute 
    key="home" 
    exact={true} 
    path="/" 
    component={Loadable(() => import('@/views/home'))} 
  />,
  <AuthRoute 
    key="home" 
    exact={true} 
    path="/home" 
    component={Loadable(() => import('@/views/home'))} 
  />
]
  • Запись маршрутизации router.tsx

Он разделен на маршруты под приложением и маршруты, независимые от приложения; в некоторых случаях, если все страницы имеют одну и ту же оболочку приложения, нет необходимости их разделять.

// src/router.tsx
import * as React from 'react';
import { HashRouter, Switch } from 'react-router-dom';
import AuthRoute from '@/routes/auth-route';
import Loadable from '@loadable/component';
import PageRoutes from './routes';
import login from '@/routes/login-register';

// 使用 import { lazy } from '@loadable/component';
// lazy()会有警告,跟React.lazy()一样的警告
const App = Loadable(() => import('./App'));
const ErrComp = Loadable(() => import(/* webpackPrefetch: true */ './views/err-comp'));

const AppComp = () => {
  // 独立在 app 之外的路由
  const aloneComp = [
    ...login
  ];
  const ErrRoute = 
    <AuthRoute 
      key='err404' 
      exact={true} 
      path='/err404' 
      component={ErrComp} 
    />;
  const NoMatchRoute = 
    <AuthRoute 
      key='no-match' 
      component={ErrComp} 
    />;

  const routes = [...PageRoutes, ErrRoute, NoMatchRoute];

  return (
    <Switch>
      {
        aloneComp.map(route => route)
      }
      <AuthRoute 
        path='/' 
        render={() => (
          <App>
            <Switch>
              {routes.map(route => route)}
            </Switch>
          </App>
        )}
      />
    </Switch>
  );
}

export default function Router() {
  return (
    <HashRouter>
      <AppComp />
    </HashRouter>
  );
}
  • Запись проекта src/index.tsx
// src/index.tsx
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Provider } from 'mobx-react';
import store from '@/store';
import AxiosConfig from './api';
import Router from './router';
import './index.scss';
// import registerServiceWorker from './registerServiceWorker'; 

const Loading = () => (<div>loading...</div>);

AxiosConfig(); // 初始化 axios

ReactDOM.render(
  <React.Suspense fallback={<Loading />}>
    <Provider {...store}>
      <Router />
    </Provider>
  </React.Suspense>,
  document.getElementById('root') as HTMLElement
);

// registerServiceWorker();

Контроль разрешений на вход

использоватьjs-cookieпакет, сохраните токен (sessionId?), возвращенный внутренним интерфейсом после входа в систему, в поле «auth» в файле cookie.

// src/routes/auth-route.tsx:
import * as React from 'react';
import { ComponentProps } from 'react';
import { Route, Redirect, RouteProps } from 'react-router';
import * as Cookies from 'js-cookie';

export interface AuthRouteProps extends RouteProps {
  key?: string|number,
  path?: string,
  auth?: boolean, // 是否需要权限
  redirectPath?: string, // 重定向后的路由
  render?: any,
  component?: ComponentProps<any>
}

const initialProps = {
  key: 1,
  path: '/login',
  auth: true,
  component: () => <div />
};

/**
 * 权限控制处理路由
 */
const AuthRoute = (props: AuthRouteProps = initialProps) => {
  const { auth, path, component, render, key, redirectPath } = props;
  if (auth && !Cookies.get('auth')) {
    // console.log('path: ', path);
    return (
      <Route 
        key={key}
        path={path}
        render={() => 
          <Redirect to={{
            pathname: redirectPath || '/login',
            search: '?fromUrl='+path
          }} />
        } 
      />
    )
  }
  return (
    <Route 
      key={key}
      path={path}
      component={component}
      render={render}
    />
  )
}

export default AuthRoute;

6. Управление API

axios

yarn add axios

конфигурация axios, перехват запроса/ответа

// src/api/index.ts
import axios, { AxiosRequestConfig, AxiosError, AxiosResponse } from 'axios';
import { message } from 'antd';
import * as Cookies from 'js-cookie';
import * as NProgress from 'nprogress';

axios.defaults.timeout = 10000;
axios.defaults.baseURL = process.env.NODE_ENV === 'production'
  ? 'http://192.168.0.5:2333' // 这里设置实际项目的生产环境地址
  : '';

let startFlag = false; // loadingStart的标志

// 拦截器
export default function AxiosConfig() {
  // 请求拦截
  axios.interceptors.request.use((config: AxiosRequestConfig) => {
    if (config.data && config.data.showLoading) {
      // 需要显示loading的请求
      startFlag = true;
      NProgress.start();
    }
    // 请求 access_token,登录后每个请求都带上
    if (Cookies.get('auth')) {
      config.headers.Authorization = Cookies.get('auth');
    }
    if (config.params) config.params._t = Date.now();

    return config;

  }, (err: AxiosError) => {
    if (startFlag) {
      startFlag = false;
      NProgress.done();
    }
    return Promise.reject(err);
  });

  // 响应拦截
  axios.interceptors.response.use((res: AxiosResponse) => {
    if (startFlag) {
      startFlag = false;
      NProgress.done();
    }
    return res.data;
    
  }, (err: AxiosError) => {
    // 服务器错误
    if (err.response && (err.response.status+'').startsWith('5')) {
      message.error('请求出错!')
    }
    if (startFlag) {
      startFlag = false;
      NProgress.done();
    }
    return Promise.reject(err);
  })
}

модуль API

// src/api/test-api.ts
import axios from 'axios';

// 获取文件
const api = {
  // 示例:
  // get只有params才会作为请求参数
  // 其他请求方式如:POST,PUT,PATCH,data作为请求参数
  testApi: (params: any = {}) => {
    // post
    // return axios.post('/api/file/uploadFile', params);

    // get
    return axios.get('/api/file/getFile', { 
      params, 
      data: { showLoading: true }
    });
  }
};

export default api;

использование API

import Api from '@/api/test-api';
...

Api.testApi(params).then((res: any) => {...});

-7.Использовать рематч для управления состоянием (рематч заброшен и заменен на mobx)

из-заredux v7.1.0недавно добавленныйuseSelector, useDispatchДождитесь хуков, обновитеreact-reduxВерсия может быть использована, следующее увеличит использованиеuseSelector, useDispatchверсия

yarn add @rematch/core react-redux

управление магазином

// src/store-rematch/index.ts
import { init, RematchRootState } from '@rematch/core';
import * as models from './models/index';

// 缓存列表
const cacheList = ['common'];
const stateCache = sessionStorage.getItem('store-rematch');
// 初始化 state
const initialState = (stateCache && JSON.parse(stateCache)) || {};

const store = init({
  models,
  redux: {
    initialState
  }
});

// 监听每次 state 的变化
store.subscribe(() => {
  const state = store.getState();
  let stateData = {};
  
  Object.keys(state).forEach(item => {
    if (cacheList.includes(item)) {
      stateData[item] = state[item];
    }
  });

  sessionStorage.setItem('store-rematch', JSON.stringify(stateData));
});

export type Store = typeof store;
export type Dispatch = typeof store.dispatch;
export type iRootState = RematchRootState<typeof models>;
export default store;

models

// src/store-rematch/models/indes.ts
import { createModel } from '@rematch/core';
// import detail from './detial';

export interface ICommonState {
  appName: string,
  isMobile: boolean,
  count: number,
  countAsync: number
}
const initialState: ICommonState = {
  appName: 'react-ts-mdnote',
  isMobile: false,
  count: 0,
  countAsync: 0
};
const common = createModel({
  state: initialState,
  reducers: {
    setIsMobile(state: ICommonState, payload: boolean) {
      return {
        ...state,
        isMobile: payload
      }
    },
    addCount(state: ICommonState) {
      return {
        ...state,
        count: state.count + 1
      }
    },
    setCount(state: ICommonState, payload: number) {
      return {
        ...state,
        countAsync: payload
      }
    }
  },
  effects: (dispatch) => ({
    async setCountAsync(payload, rootState) {
      await new Promise(resolve => setTimeout(resolve, 1000))
      dispatch.common.setCount(payload)
    }
  })
});

export {
  common,
  // detail
}

используемые компоненты

  • обычныйconnect + mapState + mapDispatchнаписание
// src/views/home/index.tsx
import * as React from 'react';
import { connect } from 'react-redux';
import { iRootState, Dispatch } from '@/store-rematch';
import { Button } from 'antd';
import styles from './home.scss';

interface IProps {
  [prop: string]: any
}

function Home(props: IProps) {
  return (
    <div className={styles.home}>
      <div className={styles.content}>
        <p>react-ts-antd-template</p>
        <p className={styles.count}>
          count: { props.count } &emsp;
          <Button onClick={props.addCount}>count++</Button>
        </p>
        <p className={styles.count}>
          countAsync: { props.countAsync } &emsp;
          <Button onClick={props.setCountAsync}>countAsync</Button>
        </p>
      </div>
    </div>
  )
}

const mapState = (state: iRootState) => {
  return {
    count: state.common.count,
    countAsync: state.common.countAsync
  }
}
const mapDispatch = (dispatch: Dispatch) => {
  return {
    addCount: () => dispatch({ type: 'common/addCount' }),
    setCountAsync: () => dispatch({ type: 'common/setCountAsync', payload: new Date().getSeconds() }),
  }
}

export default connect(mapState, mapDispatch)(Home);
  • react-reduxДобавлены хуки:useSelector, useDispatchнаписание
import * as React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { iRootState, Dispatch } from '@/store-rematch';
import { Button } from 'antd';
import styles from './home.scss';

interface IProps {
  [prop: string]: any
}

function Home(props: IProps) {
  const dispatch: Dispatch = useDispatch();
  const { count, countAsync } = useSelector((state: iRootState) => state.common);
  
  return (
    <div className={styles.home}>
      <div className={styles.content}>
        <p>react-ts-antd-template</p>
        <p className={styles.count}>
          count: { count } &emsp;
          <Button onClick={() => dispatch({ type: 'common/addCount' })}>count++</Button>
        </p>
        <p className={styles.count}>
          countAsync: { countAsync } &emsp;
          <Button 
            onClick={() => dispatch({ type: 'common/setCountAsync', payload: new Date().getSeconds() })}
          >countAsync</Button>
        </p>
      </div>
    </div>
  )
}

export default Home;

+7, управление состоянием мобх

yarn add mobx mobx-react

По сравнению с redux, mobx имеет несколько концепций и прост в написании и использовании; компоненты класса используют декораторы, а компоненты функций используют функции с тем же именем.

  • @observable: объявить состояние данных
  • @computed: вычисляемые свойства, вы можете получить необходимые данные из объекта или массива
  • @action: функция действия, вы можете напрямую писать асинхронные функции
  • Работание: Обратите внимание, что нет@, Не декоратор; в@actionМодификация внутри декорированной функцииstate, как показано нижеsetTimeoutИзменить данные в
  • поток: возвращает функцию генератора генератора сfunction */yieldзаменятьasync/await(эти два на самом деле их синтаксический сахар), нет необходимости использовать@action/runInAction
  • @inject('homeStore'): будетhomeStoreвнедрить в компонент
  • @observer: функцию/декоратор можно использовать для превращения компонентов React в реактивные компоненты. Он обертывает функцию рендеринга компонента с помощью mobx.autorun, чтобы гарантировать, что любые изменения данных, используемых в рендеринге компонента, могут привести к обновлению компонента. наблюдатель предоставляется отдельным пакетом mobx-react.

Другая конфигурация:

  • Скачать плагин
    yarn add babel-plugin-transform-decorators-legacy -D
    
  • Затем в .babelrc: используйте декоратор
    "plugins": ["transform-decorators-legacy"]
    
  • tsconfig.json: использовать декораторы
    "compilerOptions": {
      "experimentalDecorators": true,
    }
    

7.1 Вход в проект

использоватьProviderвключить элементы

import { Provider } from 'mobx-react';
import * as React from 'react';
import * as ReactDOM from 'react-dom';

import { Provider } from 'mobx-react';
import store from './store';
import AxiosConfig from './api';
import Router from './router';
import './index.scss';
import registerServiceWorker from './registerServiceWorker'; 

const Loading = () => (<div>loading...</div>);

AxiosConfig(); // 初始化 axios

ReactDOM.render(
  <React.Suspense fallback={<Loading />}>
    <Provider {...store}>
      <Router />
    </Provider>
  </React.Suspense>,
  document.getElementById('root') as HTMLElement
);

registerServiceWorker();

7.2 Модули

// src/store/home.ts
import * as mobx from 'mobx';

// 禁止在 action 外直接修改 state 
mobx.configure({ enforceActions: "observed"});
const { observable, action, computed, runInAction, autorun } = mobx;

let cache = sessionStorage.getItem('homeStore');

// 初始化数据
let initialState = {
  count: 0,
  data: {
    time: '2019-11-08'
  },
};

// 缓存数据
if (cache) {
  initialState = {
    ...initialState,
    ...JSON.parse(cache)
  }
}

class Home {
  @observable
  public count = initialState.count;

  @observable
  public data = initialState.data;

  @computed
  public get getTime() {
    return this.data.time;
  }

  @action
  public setCount = (_count: number) => {
    this.count = _count;
  }

  @action
  public setCountAsync = (_count: number) => {
    setTimeout(() => {
      runInAction(() => {
        this.count = _count;
      })
    }, 1000);
  }

  // public setCountFlow = flow(function *(_count: number) {
  //   yield setTimeout(() => {}, 1000);
  //   this.count = _count;
  // })
}

const homeStore = new Home();

autorun(() => {
  // 数据变化后触发,数据缓存
  const obj = mobx.toJS(homeStore);
  sessionStorage.setItem('homeStore', JSON.stringify(obj));
})

export type homeStoreType = typeof homeStore;
export default homeStore;

7.3 Кэш

Используйте sessionStorage здесь, измените на другие необязательные

При кэшировании данных вы можете сопоставлять определенные ключи для кэширования вместо всех данных по мере необходимости;

  • Данные инициализации

    Когда данные инициализируются, если в кеше есть данные, данные по умолчанию перезаписываются кешированными данными.

    let cache = sessionStorage.getItem('homeStore');
    
    // 初始化数据
    let initialState = {
      count: 0,
      data: {
        time: '2019-11-08'
      },
    };
    
    // 缓存数据
    if (cache) {
      initialState = {
        ...initialState,
        ...JSON.parse(cache)
      }
    }
    
  • Мониторинг изменений данных

    автозапуск будет выполняться каждый раз при изменении данных, а затемhomeStoreПреобразуйте его в объект js (только состояние) и сохраните его в кеше.

    const homeStore = new Home();
    
    autorun(() => {
      // 数据变化后触发,数据缓存
      const obj = mobx.toJS(homeStore);
      sessionStorage.setItem('homeStore', JSON.stringify(obj));
    })
    

7.4 Выход управления модулем

// src/store/index.ts
import homeStore from './home';

/**
 * 使用 mobx 状态管理
 */
export default {
  homeStore
}

7.5 Использование компонентов

Использование декоратора в классе делает свое дело,injectВнедрить соответствующий модуль, который может быть несколько разinject;

Уведомление@inject('homeStore') @observerПорядок этих двух, иначе будет предупреждение

// src/views/home/index.tsx
import { observer, inject } from 'mobx-react';
import { homeStoreType } from '@/store/home';
...

interface IProps extends RouteComponentProps {
  history: History,
  homeStore: homeStoreType
}

@inject('homeStore')
@observer
class Home extends React.Component<IProps> {
  ...
  
  public componentDidMount() {

    this.props.homeStore.setCount(2);
    console.log(this.props.homeStore.count); // 2
    
  }

  ...
}

8. Междоменный прокси

использоватьhttp-proxy-middlewareплагин

yarn add http-proxy-middleware

Новый scr/setupProxy.js

const proxy = require("http-proxy-middleware");

module.exports = function(app) {
  app.use(
    proxy('/', {
      target: 'http://192.168.0.5:2333',
      changeOrigin: true
    })
  );
};

В script/start.js используйте:

существует

const devServer = new WebpackDevServer(compiler, serverConfig);

После этого добавьте следующий код (если ниже можно проксировать, то добавлять не нужно)

require('../src/setupProxy')(devServer);

9, css-модуль, глобальные переменные scss

конфигурация вывода класса:[local]__[hash:base64:6], вывод выглядит так:content__1f1Aqs, подробности см.здесь

глобальные переменные sass используют этот загрузчикsass-resources-loader,
Настроить загрузчик, а потом в этом файлеsrc/utils/variable.scssНапишите переменную, и тогда вы можете использовать ее с удовольствием

yarn add sass-resources-loader
// webpack.config.dev.js, webpack.config.prod.js
  {
    test: /\.(scss|less)$/,
    exclude: [/node_modules/],
    use: [
      {
        loader: require.resolve('style-loader'),
      },
      {
        loader: require.resolve('css-loader'),
        options: {
          importLoaders: 1,
          modules: true,
          localIdentName: '[local]__[hash:base64:6]'
        }
      },
      {
        loader: require.resolve('postcss-loader'),
        options: {
          // Necessary for external CSS imports to work
          // https://github.com/facebookincubator/create-react-app/issues/2677
          ident: 'postcss',
          plugins: () => [
            require('postcss-flexbugs-fixes'),
            autoprefixer({
              browsers: [
                '>1%',
                'last 4 versions',
                'Firefox ESR',
                'not ie < 9', // React doesn't support IE8 anyway
              ],
              flexbox: 'no-2009',
            }),
          ],
        },
      },
      {
        loader: require.resolve('sass-loader'),
      },
      {
        loader: 'sass-resources-loader',
        options: {
          resources: [
            path.resolve(__dirname, './../src/utils/variable.scss'),
          ],
        }
      }
    ]
  },

10. Список поддержания активности

можно смотретьздесь

11. Компоненты высшего порядка и withRouter

В основном, когда используются несколько компонентов более высокого порядкаprops перевод типатребует внимания

Context.Provider

// src/App.tsx
import * as React from 'react';
import Header from '@/components/header';
import Sidebar from '@/components/sidebar';
import Footer from '@/components/footer';
import styles from './App.scss';
import { RouteComponentProps, withRouter } from 'react-router';

interface IProps extends RouteComponentProps {
  [prop: string]: any
}
export interface IState {
  timer?: any
}
export type State = Readonly<IState>;

export interface IAppContext {
  appname: string
}
const defaultContext: IAppContext = { appname: 'react-antd-ts' };
export const AppContext = React.createContext(defaultContext);

class App extends React.Component<IProps, State> {
  public readonly state: State = {};
  
  constructor(props: IProps) {
    super(props);
  }

  public render() {
    return (
      <div className={styles.app}>
        <AppContext.Provider value={defaultContext}>
          <Header text="tteexxtt" />
          <Sidebar />
          { this.props.children }
          <Footer />
        </AppContext.Provider>
      </div>
    );
  }
}

export default withRouter(App);

Оболочка Context.Consumer

Вместо этого вы также можете использовать useContext, вам не нужна следующая потребительская упаковка.

// src/components/withAppContext/index.tsx
import * as React from 'react';
import { AppContext, IAppContext } from '@/App';

// 高阶组件:AppContext Consumer包装
// 使用时包在最外层,如 withAppContext<IProps>(withRouter(Header));
function withAppContext<T>(Component: React.ElementType) {
  // T: 泛型,传递 Component 的 props 类型,被包装的组件在父组件使用时智能提示
  // 但是需要和 withRouter 的类型分开, 
  // 因为 withRouter 不会传递除 history/location/match 之外的 props
  return (props: T) => {
    return (
      <AppContext.Consumer>
        {
          (appcontext: IAppContext) =>  <Component {...props} {...appcontext} />
        }
      </AppContext.Consumer>
    );
  }
}

export default withAppContext;

Использование компонентов

Уведомление:

1,withRouterне проходит кромеhistory/location/matchза пределамиprops, Так вот с самим компонентомpropsтип раздельный;

2. ИспользуйтеwithAppContextПередаваемые дженерики являются реквизитами самого компонента: т.е. IProps

// src/components/header/index.tsx
import * as React from 'react';
import withAppContext from '@/components/withAppContext';
import { withRouter, RouteComponentProps } from 'react-router';
import styles from './header.scss';

const { useEffect } = React;

interface IProps {
  text: string,
  [prop: string]: any
}
// withRouter不会传递除 history/location/match 之外的 props,
// 所以这里与组件本身的 props 类型分开
type IPropsWithRoute = IProps & RouteComponentProps;

function Header(props: IPropsWithRoute) {
  useEffect(() => {
    console.log(props);
  }, []);
  
  return (
    <section className={styles.header}>
      <div className="center-content">
        <div>LOGO</div>
        <div>HEADER, { props.appname }, {props.text}</div>
      </div>
    </section>
  );
}

// withRouter不会传递除 history/location/match 之外的 props,
// 所以这里使用组件本身的 props:即 IProps
export default withAppContext<IProps>(withRouter(Header));

12. Интернационализация

использовать реакцию-интл

yarn add react-intl @types/react-intl

Использовать IntlProvider в приложении

// src/App.tsx
import { IntlProvider } from 'react-intl';
import messages from '@/lang';

...

class App extends React.Component<Props, State> {
  public readonly state: State = {
    lang: Cookies.get('lang') || 'zh'
  };
  
  constructor(props: Props) {
    super(props);
  }
  
  public onLangChange(locale: string) {
    Cookies.set('lang', locale);
    this.setState({ lang: locale });
  }

  public render() {
    // console.log(this.props);
    const { lang } = this.state;

    return (
      <div className={styles.app}>
        <IntlProvider key="intl" locale={lang} messages={messages[lang]}>
          <AppContext.Provider value={defaultContext}>
            <Header text="tteexxtt" onLangChange={(locale: string) => this.onLangChange(locale)} />
            <Sidebar />
            { this.props.children }
            <Footer />
          </AppContext.Provider>
        </IntlProvider>
      </div>
    );
  }
}
...

языковой файл

ланг вход

// src/lang/index.ts
import en from './en_US';
import zh from './zh_CN';

export default {
  en,
  zh
};

Сообщения на конкретном языке

первоначально воображаемыйVueиспользуется внутриi18nТаким образом, языковой модуль имеет еще один слой, но структура плагина, похоже, не разрешена (может быть, нужно установить), поэтому вы можете только сплющить модуль расширения, а затем нижемодуль сообщенийИмя ключа внутри обрабатывается

// src/lang/zh_CN/index.ts
import home from './home';
// import detail from './detail';

export default {
  ...home,
  // ...detail
};

модуль сообщений

Обратите внимание на имя ключа и временно используйте этот метод для реализации многоязычности по модулю.

// src/lang/zh_CN/home.ts
const home = {
  'home.home': '首页',
  'home.list': '列表',
  'home.login': '登录'
};

export default home;

Использование компонентов

react-intlВ дополнение к этому многоязычному пакетуFormattedMessageКроме того, есть другие компоненты, используемые для реализации отображения разницы между суммой, валютой, датой и т. д., поэтому я не буду писать об этом здесь, просто следуйте документу, если вам нужно.

// src/components/sidebar/index.tsx
import { FormattedMessage } from 'react-intl';
...
<FormattedMessage id="home.home" />

переключить язык

// src/components/header/index.tsx
...
import Cookies from 'js-cookie';

const { useEffect, useMemo } = React;

interface IProps {
  text: string,
  onLangChange: (locale: string) => void,
  [prop: string]: any
}
// withRouter不会传递除 history/location/match 之外的 props,
// 所以这里与组件本身的 props 类型分开
type IPropsWithRoute = IProps & RouteComponentProps;

function Header(props: IPropsWithRoute) {
  const lang = useMemo(() => {
    return Cookies.get('lang') || 'zh';
  }, [Cookies.get('lang')]);

  return (
    <section className={styles.header}>
      ...
          <div className={styles.langsection}>
            <span 
              className={`${styles.lang} ${lang === 'zh' ? styles.active : ''}`} 
              onClick={() => props.onLangChange('zh')}
            >中文</span>
            <span 
              className={`${styles.lang} ${lang === 'en' ? styles.active : ''}`} 
              onClick={() => props.onLangChange('en')}
            >English</span>
          </div>
      ...
    </section>
  );
}
...

13. Построить

выход

использоватьchunkhashЕсли каждая сборка будет генерировать хэш, что приведет к тому же содержимому, но с другим именем файла, поэтому измените его наcontenthashГенерировать хэш по содержимому, тогда значение хэша связано с содержимым, лучшее кеширование, но это неизбежно приведет к увеличению времени построения, но оно того стоит

  • Имя файла: измените имя файла в выводеchunkhash -> contenthash,Такие как:
filename: 'static/js/[name].[contenthash:8].js',
chunkFilename: 'static/js/[name].[contenthash:8].chunk.js',
  • разделение кода
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  },

tree-shaking

В документации по веб-пакету есть инструкции по настройке.mode: 'production', но файл после того, как я построю здесь, откройтеwebpack moduleБудет жаловаться; но наборmode: 'development'После этого к нему можно нормально обращаться, но файл сравнивается сproductionДелать больше, , тогда смысла нет, так что эта частьпока не занят. . .

TypeError: Cannot read property 'call' of undefined

в пакете.json

Добавить к"sideEffects": false,

в webpack.prod.js

  optimization: {
    ...
    // tree shaking,与 package.json 中 "sideEffects": false 配合使用
    usedExports: true
  }

CDN стороннего ресурса

В настоящее время только строительство использует ресурс CDN для внедрения, и нет никакой разницы в стадии разработки.

react-router-dom сообщит об ошибке, если есть проблема, временно недоступен

Временно обработайте это вручную, или вы можете использовать HtmlWebpackPlugin для автоматической обработки.

Формат:包名: 导出变量名

  • webpack использует внешние:
  externals: {
    'axios': 'axios',
    'lodash' : {
      commonjs: 'lodash',
      amd: 'lodash',
      root: '_' // 指向全局变量
    },
    'react': 'React',
    'react-dom': 'ReactDOM',
    'react-router': 'ReactRouter',
    // 'react-router-dom': 'ReactRouterDOM',
    'mobx': 'mobx',
    'react-mobx': 'ReactMobx',
  },
  • Добавьте ссылки CDN на сторонние ресурсы в public/index.html.
<script src="https://cdn.bootcss.com/axios/0.19.0/axios.min.js"></script>
<script src="https://cdn.bootcss.com/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdn.bootcss.com/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<script src="https://cdn.bootcss.com/react-router/5.0.1/react-router.min.js"></script>
<!-- <script src="https://cdn.bootcss.com/react-router-dom/5.0.1/react-router-dom.min.js"></script> -->
<script src="https://cdn.bootcss.com/mobx/4.14.0/mobx.umd.min.js"></script>
<script src="https://cdn.bootcss.com/mobx-react/5.2.0/custom.min.js"></script>
<script src="https://cdn.bootcss.com/lodash.js/4.17.15/lodash.core.min.js"></script>

наконец

  • Вещи, используемые в проекте, в основном указаны выше, а другие вещи будут обновлены и добавлены позже;
  • Некоторая часть предыдущего кода была написана в первые дни, а новые вещи были добавлены позже, поэтому он отличается от некоторых из следующих функций, но, как правило, в соответствии с предыдущим методом написания проблем не возникает; то есть новые функции нуждаются переписать исходную часть кода
  • Кроме того, вы можете использовать только одну конфигурацию разработки/производства веб-пакетов, а затем использовать слияние веб-пакетов для входа в нее.Все веб-пакеты в этой статье изменены на основе старых файлов, и некоторые вещи могут быть избыточными. . .
  • React Hooks уже очень полезны, почти не нужно писать компоненты класса
  • Обратите внимание на передачу реквизита нескольких компонентов более высокого порядка.
  • React используется уже несколько месяцев, и это все, о чем я могу думать, другие продвинутые продукты временно недоступны. . .