Let's write a React+Typescript+Zent backend together

Let's write a React+Typescript+Zent backend together

Preface

Because the company H5 uses vue, the small program uses native, and the backend uses the form of react+typescript+zent. For personal use of react combined with libraries, antd is often used, so let's use zent to simply build a backend.

Preview address
Source address
personal blog address

on

Zent is a set of react business component libraries based on the WebUI specification developed by Youzan , and Youzan also specially wrote babel-plugin-zent for zent to combine with the zent library for on-demand loading

Use technology stack

begin

create

To create a react+typescript project, we still use the official command npx create-react-app name --template typescript to create the directory structure of the project

  • src project directory
    • App.tsx official use case
    • index.tsx main entry file (create a react instance)
  • tsconfig (ts configuration file)

Next we will transform the src

  • src-project directory
    • assets-resource storage directory
    • components- global component storage directory
    • interface-interface storage directory
    • layout-layout component
    • modules-redux module storage directory
    • router-route storage directory
    • utils-tool storage directory
    • views-page storage directory
  • craco.config.js-configure webpack file
  • The inheritance file of paths.json-tsconfig mainly stores alias

Configure alias

(The exception to npm run eject) Since React does not have an external webpack configuration file, you need to use a third-party library to reconfigure webpack. Here are two. One is craco and the other is react-app-rewired. Craco is used in this project (this is also the first time I used react-app-rewired). npm install @craco/craco --save-dev After the installation is complete, create a craco.config.js file in the project root directory according to the introduction of the document, and then write the following content in the file

const {resolve} = require ( 'path' ); module .exports = { webpack : { alias : { //You can add it according to your needs. For example, I added the alias @ to the src directory and I am in the project You can use'@/' instead of'./src' '@' : resolve(__dirname, './src' ) }, }, }; Copy code

Then change the package.json

"scripts" : { "start" : "react-scripts start" , "build" : "react-scripts build" , "test" : "react-scripts test" , "eject" : "react-scripts eject" }, Change to "scripts" : { "start" : "craco start" , "build" : "craco build" , "test" : "craco test" , "eject" : "craco eject" }, Copy code

In fact, according to the normal logic, this sub-alias is already in effect, but it is different in the ts project. We need to synchronize our alias settings in tsconfig, so we need to add it in the compilerOptions of tsconfig

"baseUrl" : "." , "paths" : { "@/*" : [ "src/*" ], } Copy code

These four lines of code, and then we start through the start command. But when you start it, you will find that your alias still does not take effect (I don t know why this bug is actually), that is, when you start the project through craco or react-app-rewired, the paths code just added in tsconfig will be changed. disappear. Anyway, I can't solve it, so I can solve it in another way. Create a new file through the extends attribute in tsconfig, which is inherited from the paths.json mentioned above. He never said that I deleted all of my files .

//paths.json { "compilerOptions" : { "baseUrl" : "." , "paths" : { "@/*" : [ "src/*" ] } } } Copy code

Then add extends in tsconfig: Your paths file path is recommended to be placed at the same level, and then we will find that our alias is already in effect at startup

dva registration

Friends who are not familiar with dva can go to the official website of dva to look at the document. There may be some differences between the registration of dva and the registration of ReactDOM.

ReactDOM

ReactDOM.render( < div > 123 </div > , document .querySelector( '#root' ), ) Copy code

dva

const app = dva(); app.start( '#root' ); Copy code

vue

new Vue({ }).$mount(HTMLElement) Copy code

Did you find that the registration of dva is actually similar to vue (other redux and vuex encapsulated by him are more like ); we need to change the ReactDOM of the index to the form of dva above, please make sure that your project can be started normally before making sure to proceed Content

Layout container writing

What is the layout container, the background system (refer to antd-pro), it is divided into three parts, the sildbar on the left, the user feedback part on the upper right, and the routing container below it. In fact, clicking the slider on the left just switches the routing container. The corresponding components inside

  • layout
    • UserLayout-what the user can see when he is not logged in
    • BaseLayout-what the user actually sees after logging in

We first write UserLayout

//Since I only have one login page, I wrote the login components directly into this. If there are multiple pages that users can see without logging in, you can refer to the configuration changes of BaseLayout and router to import { Form, FormStrategy, FormInputField, Validators, Button } From 'Zent' ; 1. we introduce the corresponding components according to the official example of zent form const UserLayout = () => { //form instance const form = Form.useForm(FormStrategy.View); //button loading state const [lazy, setLazy] = useState<boolean>( false ); //trigger for form submission Event const onSubmit = useCallback( form => { setLazy( true ); //Ensure that the form verification has been performed when the successful return event is performed form.getValue(); return new Promise ( ( resolve ) => { setTimeout (resolve, 1000 ); }) }, []); //Auto-fill username and password const onSubLaySubmit = useCallback( () => { setLazy( true ); setTimeout ( () => { form.initialize({ username : 'Hyouka' , password : '123456' }); onSubmitSuccess(); }, 1000 ); }, []); //The success callback function will call after resolve const onSubmitSuccess = useCallback( () => { setLazy( false ); }, []) return ( < div className = 'user-login-container' > < div className = 'user-login-container-form' > < header > Hyouka </header > < Form layout = 'horizontal' form = {form} onSubmit = {onSubmit} onSubmitSuccess = {onSubmitSuccess} > < FormInputField name = 'username' helpDesc = "Username:Hyouka" required = "Please fill in your username" /> < FormInputField name = 'password' props = {{ type: ' password ' }} helpDesc = 'Fill in the password casually, only numbers, letter form' required = 'Please fill in the password' validators = {[ Validators.pattern (/^[ a-zA-Z0-9 ]+$/,'Only English letters are allowed And number'), ]} /> < div className = 'user-login-container-form-action' > < Button loading = {lazy} htmlType = 'submit' > Login </Button > < Button onClick = {onSubLaySubmit} loading = {lazy} > Too lazy Fill in me </Button > </div > </Form > </div > </div > ) }; Copy code

After writing the above content, a simple username and password login page will come out

Secondly, we continue to write BaseLayout, just said that in the background, what the user sees is actually the switching of this piece of routing component, so we can design routing and layout around this point

//BaseLayout const BaseLayout = () => { return ( < div className = 'layout' > < div className = 'layout-slide' > This is the navigation bar </div > < div className = 'layout-content' > < Header/> < div className = 'layout-content-body' > < Switch > This is where we want to sub-route </Switch > </div > </div > </div > ) }; Copy code

Through the above code, our UserLayout and BaseLayout have been designed, and then we will design our routing

routing

First of all, if you are not familiar with routing knowledge points, you can take a look at the link I gave at the beginning

Because I am relatively lazy, I like things that can be written once, so we write a method to automatically configure routing. 1. create two files in the router folder.

  • router.txs (used to manage the routing information table, similar to the one in vue)
    • const routes = [ { name:'', path:'', component:'', redirect:'', and many more } ] Copy code
  • react-router-render (used to render the component through the route's render function)

router.tsx

import createRouter from "@/router/react-router-config" ; Through require method .context (path, whether to take subdirectories, matching rules) will be taken out of your uniform components For example, you have under your views * views * home * Home.tsx Then he will output a string array in the form of [ '../views/home/Home.tsx' ]; const views = require .context( '../views' , true , /\.tsx$/ ); //Get the word in front of .tsx, used for the name value of our routes const capital = /.*\/(.+?)\.tsx$/ ; // Get each path in the array in the form of traversal const paths = views.keys().map( ( view ) => { if (capital.exec(view) && capital.exec(view)[ 1 ] && !view.includes( 'components' )) { //take To name const name = capital.exec(view)[ 1 ]; //Here is to load the component in the form of module import, so the correct way to obtain it is to add default const component = views(view).default; //Because our component name is uppercase, we change it to lowercase const path = `/${name.toLowerCase()} ` ; return { name, component, path }; } }).filter( item => item); //emmm, after I finished writing, I actually considered the above. Actually there is a bug, that is, when you are at one level, the path is wrong. This will be considered later. Bar RouterConfig = { name : string; path: string; component: React.ReactNode | Function ; meta?: { icon?: string | React.ReactNode; title?: string; }; redirect?: string, //The first consideration here is to use children, but children is a keyword in react-props, so I changed a route?: Array <T>; }; const routes: Array <RouterConfig> = [ //Since UserLayout and BaseLayout are both the top-level containers, I wrote it dead { path : '/login' , component : () => import ( '@/layout/UserLayout' ), name : 'UserLayout' , }, { path : '/' , name : 'BaseLayout' , component : () => import ( '@/layout/BaseLayout' ), redirect : '/basis' , //The main reason is that the lazy operation routes : paths are used here } ]; Export default () => createRouter (routes); duplicated code

reactRouterConfig

//Note that AsyncRoute is separate from the following here for convenience, I wrote it together import Loading from "@/components/loading/Loading" ; export default class AsyncRoute extends React . Component { constructor ( props ) { super (props); //Define an initial value to load a loading effect this .state = { Com : null , } } componentDidMount () { //Receive the passed () => import(''); const {render} = this .props; //If it is not configured by lazy, first judge whether it is a promise, if yes, take out default Module if ( Object .prototype.toString.call(render()) === '[object Promise]' ) { render().then( res => { this .setState({ Com : res.default? res.default: Loading }); }); } else { //The lazy person came in or directly defined the component: React.ReactNode gave this .setState({ Com : the render }); } } render () { const {Com} = this .state; const {location, self} = this .props; //Then just render directly. Note that self may contain routes return Com? < Com { ...self } { . ..location }/> : < Loading/> } } //react-router-config Import {the Redirect, the Route, Switch} from 'REACT-Router-DOM' ; Import {the isArray, Random} from 'lodash' ; Import AsyncRoute from "@/Components/asyncRoute/AsyncRoute" ; const createRouter = ( routes: Array <RouterConfig> ) => { //Render each component through the route's render const createRoute = ( route: RouterConfig ) => { const {path, redirect, component : Com, ... arg} = route; //Whether to include routes, if included, recursive sub- if (arg.routes the isArray && (arg.routes) && arg.routes.length) { arg.routes = arg.routes.map( childrenRoute => { return createRoute(childrenRoute); }); //If there is a subset, the first layer of routing is definitely not itself, so you need to add a redirect component in front of routes to point it to the path corresponding to the redirect redirect && arg.routes.unshift( < Redirect from = {path} to = {redirect} key = { `${ redirect } _ ${ path }`} exact/> ) } const render = { key : path || random(), render : ( {...routeConfig} ) => { { /*<Com componentConfig={arg} {...routeConfig}/>*/ } //Return by wrapping a layer of loading effect component return ( < AsyncRoute render = {Com} self = {arg} location = { routeConfig}/ > ) } }; return < Route path = {path} { ...render }/> }; return ( < Switch > {routes.map(route => createRoute(route))} </Switch > ) }; export default createRouter; copy code

The above routing has also been completed. The next step is to register the routing. Because of the dva we use, we have to register the routing information through the dva method.

Register route

We slightly modify the code in the index

const createHashHistory = require ( 'history' ).createHashHistory; history = createHashHistory({ basename : '/' }); const app = dva({history}); app.router( () => ( < HashRouter > < Router history = {history} > {renderRoute()} </Router > </HashRouter > )) app.start( '#root' ); Copy code

After completing the above operations, we can see our page by starting the service with yarn run start, but the user can still perform operations between logging in and logging in, so next we will make a login judgment

Login permission

1. we create a login.ts module under modules

//The following are all redux packaged by dva. If you don t understand, please import {Effect} from "@/interface/model" through the top dva link ; Import {EffectsCommandMap} from 'DVA' ; Import {AnyAction} from 'Redux' ; type Effect = ( action: AnyAction, effects: EffectsCommandMap, ) => void ; Import {} the Reducer from 'Redux' ; Import {clearLocal, setlocal} from "@/utils/localStorage" ; Import {} History from "@/utils/History" ; export interface LoginState { user : string | unknown } interface LoginType { namespace : string; state: LoginState; effects: { whetherLogin : Effect, logout : Effect, }, reducers : { changeWhetherLogin : Reducer<string | unknown>; } } const loginModule: LoginType = { //Module name, if you call the module, you must pass this name namespace : 'loginModule' , //State state : { user : undefined }, //Used to process asynchronous operations and business logic effects : { //payload is the value passed in when calling effects, put is to call reducers, call is to call your asynchronous operation * whetherLogin ( {payload}, {put, call } ) { const {user} = payload; //Here should be through the interface form //const res = call(loginServer, data); yield put({ type : 'changeWhetherLogin' , payload : user }); //I came here directly through the username entered setLocal( 'username' , user); }, }, //Used to handle synchronization operations, the only place that can modify the state reducers : { changeWhetherLogin ( state, {payload} ) { return { user : payload } } } }; export default loginModule; copy code

Then we need to let dva know that we have written a redux module, or slightly modify the following index.tsx

const app = dva({history}); This loginModule is the module we exported, write a registration, write a registration, and a Of course , there are lazy way is through The require the .context (); find the following modules like module unified registration, Too lazy to bother to write app.model(loginModule); app.router( () => ( < HashRouter > < Router history = {history} > {renderRoute()} </Router > </HashRouter > )) app.start( '#root' ); Then we restart the service and our module is registered in redux Copy code

Then we are modifying the UserLayout and BaseLayout we wrote above

UserLayout

type Dispatch = <T = any, callback = (payload: T) => void>( action: { type: string; payload?: T; callback?: callback; } ) => void; GlobalDispatchComponentType = { dispatch: Dispatch } const UserLayout: React.FC<GlobalDispatchComponentType> = ({dispatch}) => { const form = Form.useForm(FormStrategy.View); const history = useHistory(); const [lazy, setLazy] = useState<boolean>(false); const onSubmit = useCallback(form => { setLazy(true); form.getValue(); return new Promise((resolve) => { setTimeout(resolve, 1000); }) }, []); const onSubLaySubmit = useCallback(() => { setLazy(true); setTimeout(() => { form.initialize({ username:'Hyouka', password: '123456' }); onSubmitSuccess(); }, 1000); }, []); const onSubmitSuccess = useCallback(() => { const {username} = form.getValue(); setLazy(false); //If the form is validated, dispatch is triggered dispatch({ type:'loginModule/whetherLogin', payload: { user: username } }); history.push('/'); }, []) return ( <div className='user-login-container'> <div className='user-login-container-form'> <header>Hyouka</header> <Form layout='horizontal' form={form} onSubmit={onSubmit} onSubmitSuccess={onSubmitSuccess} > <FormInputField name='username' helpDesc="Username: Hyouka" required="Please fill in the user name" /> <FormInputField name='password' props={{ type:'password' }} helpDesc='Fill in the password casually, only numbers and letters form' required='Please fill in the password' validators={[ Validators.pattern(/^[a-zA-Z0-9]+$/,'Only English letters and numbers are allowed'), ]} /> <div className='user-login-container-form-action'> <Button loading={lazy} htmlType='submit'>Login</Button> <Button onClick={onSubLaySubmit} loading={lazy}>Too lazy to fill in me</Button> </div> </Form> </div> </div> ) }; export default connect()(UserLayout); Copy code

BaseLayout

const BaseLayout: React.FC<GlobalDispatchComponentType & LoginState & { routes : Array <React.ReactNode> }> = ( {user, routes} ) => { const history = useHistory(); //If there is no username, return to the landing page via useEffect( () => { if (!getLocal( 'username' )) { history.push( '/login' ); } }, [user]); return ( < div className = 'layout' > < div className = 'layout-slide' > This is the navigation bar </div > < div className = 'layout-content' > < Header/> < div className = 'layout-content-body' > < Switch > {routes} </Switch > </div > </div > </div > ) }; Export default Connect ( ( {} LoginModule: ConnectType ) => (LoginModule {...})) (baselayout); duplicated code

Navigation Bar

Import {Menu, Icon} from 'Zent' ; const {MenuItem, SubMenu} = Menu; const SlideBar = () => { const { location : {pathname}} = history; const [defaultSelectedKey, setDefaultSelectedKey] = useState<string>( '/basis' ); /** * Here is the {name, path, component} component we don t need * It is necessary to modify this function to change promise.default to {} export * The meta information is also returned consistently. Of course, the real environment must be obtained through api * */ const renderMenu = ( menu = paths ) => { if (!menu) return ; /** * The sub here should correspond to the routing interface Array<RouterConfig> set at the beginning, I just wrote it casually for convenience, and I only have one layer here * **/ const renderItemOrSub = ( sub: any ) => { if (sub.children && sub.children.length) { //Other parameters can be added in SubMenu return ( < SubMenu title = {sub.name} > {renderMenu(sub.children)} </SubMenu > ) } else { return ( < MenuItem key = {sub.path} > {sub.name} </MenuItem > ) } } return menu.map( ( item: any ) => { return renderItemOrSub(item); }) }; const slideMenuClick = async (e: React.MouseEvent, key : string) => { await setDefaultSelectedKey(pathname); await history.push(key); }; return ( < div className = 'slide-bar' > < div className = 'slide-bar-header' > < Icon type = 'youzan'/> < span className = 'slide-bar-header-title' > Zent </span > </div > < div className = 'slide-bar-menu' > < Menu mode = "inline" defaultSelectedKey = {defaultSelectedKey} onClick = {slideMenuClick} > {renderMenu()} </Menu > </div > </div > ) }; Copy code

After writing the above code, we finally introduce it in BaseLayout

return ( < div className = 'layout' > < div className = 'layout-slide' > < SlideBar/> </div > < div className = 'layout-content' > < Header/> < div className = 'layout-content -body' > < Switch > {routes} </Switch > </div > </div > </div > ) Copy code

Then we can write our page happily

Personal thoughts

If I simply compare the antd document and the zent document, I think the antd document is relatively clear, each method and attribute are listed at the end of each component document, and the parameters returned by the callback are notified. Value, like zent, although there is a document of all the methods, it is all in English (the English is poor, and the English is supplemented recently), and the enumeration of the entire method looks messy. . . . So I still want the big guys who like it to improve it. Manual @ here, keep your life.

At last