react+TypeScript+mobx+sass+antd项目实践

论坛 期权论坛 编程之家     
选择匿名的用户   2021-5-26 09:13   479   0

react中使用typescript创建项目

create-react-app react-ts --scripts-version=react-scripts-ts

输入y,不要点回车,成功后的项目目录如下:

node_modules直接装好了,cd react-ts npm run start项目跑起来。

  • tsconfig.json包含了工程里TypeScript特定的选项,是TypeScript的配置文件,项目要想使用TypeScript需要增加这个文件。
  • tslint.json保存了要使用的代码检查器的设置,TSLint
  • package.json包含了依赖,还有一些命令的快捷方式,如测试命令,预览命令和发布应用的命令。
  • public包含了静态资源如HTML页面或图片。除了index.html文件外,其它的文件都可以删除。
  • src包含了TypeScript和CSS源码。index.tsx是强制使用的入口文件。
{
  "compilerOptions": {
    // import的相对起使路径
    "baseUrl": ".",
    "outDir": "build/dist",
    "module": "esnext",
    "target": "es5",
    "lib": ["es6", "dom"],
    "sourceMap": true,
    "allowJs": true,
    "jsx": "react",
    "moduleResolution": "node",
    "rootDir": "src",
    "forceConsistentCasingInFileNames": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noImplicitAny": true,
    "importHelpers": true,
    "strictNullChecks": true,
    // 开启装饰器的使用
    "experimentalDecorators": true,
    "suppressImplicitAnyIndexErrors": true,
    "noUnusedLocals": true
  },
  "exclude": [
    "node_modules",
    "build",
    "scripts",
    "acceptance-tests",
    "webpack",
    "jest",
    "src/setupTests.ts"
  ]
}
"include": [
        "./src/**/*"
    ]

  • 读取所有可识别的src目录下的文件(通过include)。
  • 接受JavaScript做为输入(通过allowJs)。
  • 生成的所有文件放在built目录下(通过outDir)。
  • 将JavaScript代码降级到低版本比如ECMAScript 5(通过target

Webpack

Webpack集成非常简单。 你可以使用awesome-typescript-loader,它是一个TypeScript的加载器,结合source-map-loader方便调试。 运行:

npm install awesome-typescript-loader source-map-loader

并将下面的选项合并到你的webpack.config.js文件里:

module.exports = {
    entry: "./src/index.ts",
    output: {
        filename: "./dist/bundle.js",
    },

    // Enable sourcemaps for debugging webpack's output.
    devtool: "source-map",

    resolve: {
        // Add '.ts' and '.tsx' as resolvable extensions.
        extensions: ["", ".webpack.js", ".web.js", ".ts", ".tsx", ".js"]
    },

    module: {
        loaders: [
            // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
            { test: /\.tsx?$/, loader: "awesome-typescript-loader" }
        ],

        preLoaders: [
            // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
            { test: /\.js$/, loader: "source-map-loader" }
        ]
    },

    // Other options...
};

要注意的是,awesome-typescript-loader必须在其它处理.js文件的加载器之前运行。

这与另一个TypeScript的Webpack加载器ts-loader是一样的。 你可以到这里了解两者之间的差别。

注:如果要在项目中使用ts 就需要把js文件 改成 ts文件 jsx文件改为tsx文件,增加tsconfig.json文件配置,增加webpack配置。

index.tsx文件

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from './App';
import './index.css';
import registerServiceWorker from './registerServiceWorker';

ReactDOM.render(
  <App />,
  document.getElementById('root') as HTMLElement
);
registerServiceWorker();

来写一个button组件

import * as React from 'react'

class Button extends React.Component {
 public render() {
    return  (
      <div>
        <button>点击事件</button>
      </div>
    )
  }
}

export default Button;

如果直接写render函数,会有下面的提示信息

需要在render函数前面加public,App.tsx ts中的类有严格的格式要求,每一个属性都需要加上private, public,或者 protected

import * as React from 'react';
import './App.css';
import Button from './Button'
import logo from './logo.svg';

class App extends React.Component {
  public render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.tsx</code> and save to reload.
        </p>
        <Button name={'麦乐'} />
      </div>
    );
  }
}

export default App;

button组件需要定义两个接口,接口的名称必须大写,这样组件内部才可以访问props和state

interface IPros {
  name: string
}
interface IState {
  age: number
}
import * as React from 'react'


interface IPros {
  name: string
}
interface IState {
  age: number
}
class Button extends React.Component<IPros, IState> {
  public constructor(props: IPros) {
    super(props)
    this.state = {
      age: 18
    }
  }
 public render() {
    return  (
      <div>
        <button>点击事件{this.props.name}{this.state.age}</button>
      </div>
    )
  }
}

export default Button;

增加点击事件 会有下面提示信息

tslint规则,由于渲染性能的影响,表达式是被禁止使用的。所以我们需要改一下代码。

import * as React from 'react'


interface IPros {
  name: string
}
interface IState {
  age: number
}
class Button extends React.Component<IPros, IState> {
  public constructor(props: IPros) {
    super(props)
    this.handlerClick = this.handlerClick.bind(this)
    this.state = {
      age: 18
    }
  }
  public handlerClick(e: React.MouseEvent<HTMLButtonElement>) {
    console.log('我被点击了')
  }
  public render() {
    return  (
      <div>
        <button onClick={this.handlerClick}>点击事件{this.props.name}{this.state.age}</button>
      </div>
    )
  }
}

export default Button;

这是页面中会看到console.log这里画这红色的波浪线,这是因为tslint的原因

在tslint.json文件中增加配置 "no-console":false

{
  "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
  "rules": {
    "no-console":false
  },
  "linterOptions": {
    "exclude": [
      "config/**/*.js",
      "node_modules/**/*.ts",
      "coverage/lcov-report/*.js"
    ]
  }
}

增加生命周期函数,使用方法没有区别,需要加上共有还是私有。

 public componentDidMount() {
    console.log('组件加载完成')
  }

我们来写一个todoList

TodoList组件

import * as React from 'react'
import  { FriendItem } from './types'
 
type onDel = (index: number) => void

interface IProps {
  list: FriendItem[];
  onDel: onDel;
}

interface IState {
  age: number;
}
class TodoList extends React.Component<IProps, IState> {
  public constructor(props: IProps) {
    super(props)
    this.state = {
      age: 18,
    }
  }
 
  public render() {
    const { list, onDel } = this.props
    return  (
      <div>
        {
          list.map((item: FriendItem, index: number) => 
            <div key={index}>
              <span>{item.name}</span> <button onClick={() => {
                onDel(index)
              }
              }>删除</button>
            </div>
          )
        }
      </div>
    )
  }
  public componentDidMount() {
    console.log('组件加载完成')
  }
}

export default TodoList;

action 组件

import * as React from 'react'


type onAdd = (value: string) => void
type onValueChange = (value: string) => void
interface IProps {
  onAdd: onAdd;
  onValueChange: onValueChange;
  value: string;

}

class Action extends React.Component<IProps> {
  
  public render() {
    const { value, onAdd, onValueChange } = this.props
    return  (
      <div>
        <input type="text" value={value} onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
          onValueChange(e.target.value)
        }}/>
        <button onClick={() => {
          onAdd(value)
        }}>add</button>
      </div>
    )
  }
  public componentDidMount() {
    console.log('组件加载完成')
  }
}

export default Action;

暴露一个接口

export interface FriendItem {
    name: string;
    age: number;
  }

App.jsx

import * as React from 'react';
import './App.css';
import Actions from './action'
import TodoList from './todoList'
import logo from './logo.svg';
import  { FriendItem } from './types'


interface IState {
  value: string;
  list: FriendItem[];
}

class App extends React.Component<Object, IState> {
  public constructor(props: Object) {
    super(props)
    this.state = {
      list: [{
        age: 1,
        name: ''
      }],
      value: ''
    }
  }
  public render() {
    const { list, value } = this.state
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <Actions onValueChange={(value: string) => {
          this.setState({
            value
          })
          
        }} onAdd={(value: string) => {
          list.push({name: value, age: 3})
          this.setState({
            list: [...list],
            value: ''
          })
          
        }} value={value}/>
        <TodoList list={list} onDel={(index: number) => {
          list.splice(index,1)
          this.setState({
            list: [...list]
          })
        }}/>
      </div>
    );
  }
}

export default App;

tslint.json文件做了一点修改,可选择配置。

"rules": {
    "no-console":false,
    "jsx-no-lambda": false,
    "ban-types": false, // 是否禁止使用特定的类型 例如Object,
    "interface-name": false, //收否要求接口名称大写
    "no-shadowed-variable":false,
    "ordered-imports": false // 收否按照字母循序引入
  },
  "linterOptions": {
    "exclude": [
      "config/**/*.js",
      "node_modules/**/*.ts",
      "coverage/lcov-report/*.js"
    ]
  }
}

react中配置mobx

npm install mobx --save 
 
npm install mobx-react --save 

创建store文件夹

下面创建friend.ts文件

import { observable, computed } from 'mobx'
import post from './post'
import { PostListItem } from './type'

class Friend {
    @observable public list: string[] = ['nhao', '今天好开心']
    @observable public id: number = 0
    @computed
    get friendPost() {
        return post.list.filter((item: PostListItem) => item.friendId === this.id)
    }
}

const friend = new Friend()
export default friend

定义了响应式变量list和id, 还有一个计算属性 friendPost,它的值是又post里面的list和friend里面的id共同计算得来的。只要二者发生变化,就会重新计算。

post.ts

import { observable } from 'mobx'
import { PostListItem } from './type'

class Post {
    @observable public list: PostListItem[] = [
        {
            title: '你好',
            content: '今天是周二',
            id: 1,
            friendId: 1
        },
        {
            title: '你好',
            content: '今天是周三',
            id: 2,
            friendId: 2
        }
    ]
}

export default new Post()

这个文件中只放了一个列表

把接口定义在type.ts中


export interface PostListItem {
    title: string;
    content: string;
    id: number;
    friendId: number;
}
export interface FriendListItem {
    name: string;
    id: number;
}
export interface Friend {
    list: FriendListItem[];
    activeId: number;
    friendPost: PostListItem;
  }

export interface PostList {
    list: PostListItem[];
}

export interface Store {
    friend: Friend;
    post: PostList;
}

export interface IMobxStore {
    name: string;
    greeting: string;
    setName(name:string): void;
}

下面是index.ts

import friend from './friend'
import post from './post'

export default {
    friend,
    post
}

如果直接这样定义App组件,会发现一直提示下面的错信息,

//编译时错误代码:

“Property 'friend' is missing in type '{}' but required in type 'Readonly<IPropsFromParent>'”

index.tsx中并没有对App组件传递属性。这是因为,App组件会一直去passedProps中找这个值,而没有去store中找。类似下面的

都是同一个问题。有两种解决方法:

一:换一种写法

举例

// 创建store
import {action, computed, observable} from 'mobx'
import { IMobxStore } from './type'
 
class MobxStore implements IMobxStore {
    @observable  public name: string = "world"
 
    @computed
    public get greeting(): string {
        return `hello ${this.name}`
    }
 
    @action.bound
    public setName(name: string): void{
        this.name = name
    }
 
}
 
export default new MobxStore()

implements是一个类实现一个接口用的关键字, 接口如下

export interface IMobxStore {
    name: string;
    greeting: string;
    setName(name:string): void;
}

实现一个App组件 主要改变的就是props接口的定义方法。

// 使用Store 
// App.tsx  在src/App.tsx中 使用store, 代码如下:
import * as React from 'react'
import './App.css'
import { IMobxStore } from './store/type'
import { inject, observer } from 'mobx-react'
 
interface IAppProps {
    person?: IMobxStore //  这里比较关键 ?表示可或缺,如果没有就会报错。
}
 
@inject('person')
@observer
class App extends React.Component<IAppProps, {}> {
    constructor(props: IAppProps) {
        super(props)
        this.clickHandler = this.clickHandler.bind(this);
    }
  public render() {
    const {greeting} = this.props.person!; // 这里要加!
    
    return (
      <div className="App">
        <header className="App-header">
            {greeting}
          <button onClick={this.clickHandler}>Change Greeting</button>
        </header>
        
      </div>
    );
  }
 
  private clickHandler = (): void =>{
    console.log(this.props)
    const { setName } = this.props.person!; // 这里也是
    setName("Bob");
  }
}
 
export default App

index.tsx 不用修改

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from './App';
import './index.css';
import registerServiceWorker from './registerServiceWorker';
import { Provider } from "mobx-react"
import  store from "./store"

ReactDOM.render(
  <Provider {...store}>
    <App />
  </Provider>,
  document.getElementById('root') as HTMLElement
);
registerServiceWorker();

这样可以解决上面的问题。

二:换一个插件

npm install ts-mobx-react --save 

修改App.tsx,

import * as React from 'react';
import './App.css';
import Friends from './components/friend'
import { observer } from 'ts-mobx-react'
import Post from './components/post'

interface IPropsFromParent {
  age: string
}
@observer
class App extends React.Component<{} & IPropsFromParent, {}> {

  public render() {
    return (
      <div className="App">
        <Friends />
        <Post  />
      </div>
    );
  }
}

export default App;

friend.tsx内部

import * as React from 'react'
import { observer, inject} from 'ts-mobx-react'
import { FriendListItem, Friend } from '../store/type'

@observer
class Friends extends React.Component<{}> {
   @inject('friend') public friend: Friend;
   public render() {
        return (
            <div>
                {
                    this.friend.list.map((item: FriendListItem, index: number) => {
                       return <span key={ index }  onClick={() => {
                           this.friend!.changeActiveId(item.id)
                           console.log(this.friend.activeId)
                       }} >{item.name}|</span>
                    })
                }
            </div>
        )
    }
}

export default Friends

index.tsx 把mobx-react都换成 ts-mobx-react

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from './App';
import './index.css';
import registerServiceWorker from './registerServiceWorker';
import { Provider } from "ts-mobx-react"
import  store from "./store"

ReactDOM.render(
  <Provider {...store}>
    <App age="8"/>
  </Provider>,
  document.getElementById('root') as HTMLElement
);
registerServiceWorker();

这样就可以拿到store中的数据,但是,这个插件有一个问题,那就是数据不再是响应式的,this.friend的改变并不能改变store中数据,看了源码发现

export function inject(dataIndex?: string) {
  return function(prototype: any, propertyName: string) {
    const constructor = prototype.constructor;
    const stores: string[] =
      Reflect.getOwnMetadata(StoreNamesSymbol, constructor) || [];
    if (!dataIndex) {
      dataIndex = propertyName;
    }
    Reflect.defineMetadata(
      StoreNamesSymbol,
      stores.concat(dataIndex),
      constructor
    );
    Object.defineProperty(prototype, propertyName, {
      enumerable: true,
      configurable: true,
      get() {
        return this.props[addPrefix(dataIndex!)];
      }
    });
  };
}

函数内部是设置了监听,但是只监听了属性的获取,属性值改变的时候并没有做任何操作。所以这个这个插件还是不要使用了。

修改friend.tsx组件

import * as React from 'react'
import { observer, inject} from 'mobx-react'
import { FriendListItem, Friend } from '../store/type'

interface StoreProps {
    friend?: Friend
}
@inject('friend')
@observer
class Friends extends React.Component<StoreProps> {
   public render() {
       const { list, changeActiveId } = this.props.friend!
        return (
            <div>
                {
                    list.map((item: FriendListItem, index: number) => {
                       return <span key={ index }  onClick={() => {
                       changeActiveId(item.id)
                       }} >{item.name}|</span>
                    })
                }
            </div>
        )
    }
}

export default Friends

修改post组件

import * as React from 'react'
import { observer, inject } from 'mobx-react'
import { Friend } from '../store/type'

interface StoreProps {
    friend?: Friend
}
@inject('friend')
@observer
class Post extends React.Component<StoreProps> {
   public render() {
       const { friendPost } = this.props.friend!;
        return (
            <div>
                <p>{friendPost[0].title}</p>
                <p>{friendPost[0].content}</p>
            </div>
        )
    }
}

export default Post

App.tsx中和indx.tsx中的ts-mobx-react换成mobx-react

如果运行过程中遇到这样的报错。

Cannot find name 'it'. Do you need to install type definitions for a test runner? Try `npm i @types/jest` or `npm i @types/mocha`.

你需要将tsconfig.json中的typeRoots这个属性路径配置对就可以了。

"typeRoots": [
      "../node_modules/@types/",
      "../@types/"
    ]

需要进一步了解,可以看官网:https://www.tslang.cn/docs/handbook/tsconfig-json.html#types-typeroots-and-types

大致意思就是说

@typestypeRootstypes

默认所有可见的"@types"包会在编译过程中被包含进来。 node_modules/@types文件夹下以及它们子文件夹下的所有包都是可见的; 也就是说, ./node_modules/@types/../node_modules/@types/../../node_modules/@types/等等。

如果指定了typeRoots只有typeRoots下面的包才会被包含进来。 比如:

{
   "compilerOptions": {
       "typeRoots" : ["./typings"]
   }
}

这个配置文件会包含所有./typings下面的包,而不包含./node_modules/@types里面的包。

项目中使用sass, tnpm run eject 展开webpack配置。tnpm i sass-loader node-sass 安装相应的插件

webpack.config.dev.js文件中添加配置,加在rules属性配置里面。webpack.config.prod.js做同样的操作。

          {   
            //这里是新加的
            test: /\.scss$/,
            loaders: ['style-loader', 'css-loader', 'sass-loader'],
          },
          // "file" loader makes sure those assets get served by WebpackDevServer.
          // When you `import` an asset, you get its (virtual) filename.
          // In production, they would get copied to the `build` folder.
          // This loader doesn't use a "test" so it will catch all modules
          // that fall through the other loaders.
          {
            // Exclude `js` files to keep "css" loader working as it injects
            // its runtime that would otherwise processed through "file" loader.
            // Also exclude `html` and `json` extensions so they get processed
            // by webpacks internal loaders.
            exclude: [/\.(js|jsx|mjs)$/, /\.html$/, /\.json$/, /\.scss$/],
            loader: require.resolve('file-loader'),
            options: {
              name: 'static/media/[name].[hash:8].[ext]',
            },
          },

运行的时候遇到下面的报错。

this.getResolve is not a function
将    "sass-loader": "^8.0.0",更换成了 "sass-loader": "^7.3.1",

这样直接在tsx文件中引入scss文件,就生效了。

引入antd design

如果按照管网上给出的方法,会发现如论是增加.bebelrc还是在webpack中加配置都不生效。下面直接给出一种生效的方法。

npm install antd  babel-plugin-import --save
npm install react-app-rewired react-app-rewire-less --save
npm install ts-import-plugin  react-scripts-ts --save

react-app-rewired@1.6.2 这个插件要指定版本,不然会报错。

项目根目录下增加 config-overrides.js文件

const tsImportPluginFactory = require('ts-import-plugin');
const { getLoader } = require('react-app-rewired');
const rewireLess = require('react-app-rewire-less');

module.exports = function override(config, env) {
  const tsLoader = getLoader(
    config.module.rules,
    rule => rule.loader && typeof rule.loader === 'string' && rule.loader.includes('ts-loader')
  );

  tsLoader.options = {
    getCustomTransformers: () => ({
      before: [
        tsImportPluginFactory({
          libraryDirectory: 'es',
          libraryName: 'antd',
          style: "css",
        }),
      ],
    }),
  };

  config = rewireLess.withLoaderOptions({
    javascriptEnabled: true,
    modifyVars: {
      '@primary-color': '#1DA57A', // 主题色
    },
  })(config, env);

  return config;
};

package.json文件中的scripts属性换一下。

"scripts": {
    "start": "react-app-rewired start --scripts-version react-scripts-ts",
    "build": "react-app-rewired build --scripts-version react-scripts-ts",
    "test": "react-app-rewired test --env=jsdom --scripts-version react-scripts-ts"
  },

tsx文件中引入你需要使用的组件import { DatePicker } from 'antd';

重启项目,就能看到进入成功了。

如果遇到下面错误

可以在tsconfig.json文件中增加"skipLibCheck": true配置,这只是一个应对方法。

项目重启后发现,样式失效了,是因为我原来使用sass,这里配置了less,覆盖掉了,直接把scss文件改成less文件就可以了。

分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:3875789
帖子:775174
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP