Here’s the video commentary for part two

Let’s pick up where we left off

Componentization and modularization

The idea of componentization and modularization

First of all, we need to clarify what the problem is that leads us to modularity and componentization.

  • The code is written many, many times together
  • There is no good functional division
  • Component reuse cannot be implemented
  • Page UI and data are not separated
  • Page logic is not clear
  • Readability is poor
  • Poor maintainability

Therefore, the idea of componentization and modularization appeared in front of the above problems. From Jquery in the past to Angular, React and Vue based on MVC/MVVM, the idea of componentization and modularization became more and more standardized.

Let’s see what React does.

The main idea of React is to separate data from DOM operations, and use JS to implement all functions (UI rendering, data processing). After separating, we only need to focus on adding, deleting, modifying and viewing data, and don’t need to worry about rendering data to DOM elements. React does all of these things for DOM mounting.

But in fact, in the actual development, if there is no certain specification in the construction of the project, in fact, the code will still be very messy, and can not effectively solve the above problems. Therefore, as a multi-person long-term development and maintenance project, it is necessary to consider how to solve these problems from the root when building the project.

In the following, we will realize the separation of data and DOM operation in React thought, use JS to realize all functions, and only focus on the increase, deletion, change and check of data level.

Functional programming

Function programming is a design idea, just as object oriented programming is a design idea. Functional programming in general is to use the combination of functions as far as possible to program, first declare the function, and then call the function of each step has a return value, the concrete each step of the logical operation abstract, encapsulated in the function. Then combine the functions to write the program.

The React neutron component is passed by the parent componentpropsControls, but child components cannot be modified directlypropsThat is, monomial data flow, which is somewhat similar to the properties of pure functions, without changing the external state. So when we develop components, we should try to leave the side effects of operations to external control, so that components are independent and highly adaptable.

Why use functional programming?

  1. Development speed, high use function can continue to reuse logic, function for external black box can be directly used without side effects.
  2. Function name can be understood directly, do not need to understand the internal implementation, close to natural language, we use a hair dryer, do not need to step by step to make a hair dryer.
  3. The code is cleaner, the actual calling code is simple, and all the detailed logic is wrapped up in functions.
  4. Convenient concurrent processing, because the method is pure function does not affect external variables, can be arbitrary discharge processing order.

Write the routing

Before we start talking about componentization and modularization we need a default route to our home page container.

import React from 'react';

import GlobalStyle from '.. /.. /global-styles';
import { Switch, Route } from 'react-router-dom';
import Home from 'app/containers/Home';

export default function App() {
  return (
    <div>
      <Switch>
        <Route exact path="/" component={Home} />
      </Switch>
      <GlobalStyle />
    </div>
  );
}
Copy the code

Write the container

A container is essentially a component that can operate asynchronously, so containers need to use Redux +Saga to handle asynchronous operations. However, component is a piece of function extracted from the container, which does not need the support of Redux +Saga. Its logic should be realized internally, and it can only receive some required parameters to complete rendering. Therefore, sometimes components do not need to be highly reusable, or I think a certain piece is a whole. Then you can pull it out as a component.

Imagine that each page container is composed of components, so the layout of the page can be clearly seen, as the previous functional programming said, I do not need to care about the internal implementation of each component, because I only need to see the structure of the container.

So let’s take a look at the files needed for a page container modularization:

  • constants.jsThe action constants
  • actions.jsThe action definition
  • reducer.jsReducer function
  • selectors.jsThe state method for resELECT packaging
  • Loadable.jsReact uses lazy loading to lazily load components
  • saga.jsSaga module
  • mseeages.jsInternationalization information
  • node.jsstyle
  • index.jscomponent

At first glance, there are a lot of documents, which seem to be in a mess. Let’s take care of them one by one:

constants.js

Provide constants for other files. The constants must correspond to the container name, because our store is global and it needs to know which container the action belongs to.

export const DEFAULT_ACTION = 'app/Home/DEFAULT_ACTION';
Copy the code

actions.js

The files defined by the action, components, and saga perform calls to them to modify the values in state.

import { DEFAULT_ACTION } from './constants';

export function defaultAction() {
  return {
    type: DEFAULT_ACTION,
  };
}
Copy the code

reducer.js

Simply put, a REdux is a set of states, and to change its values you need to initiate an action, which is a normal JavaScript object. The advantage of forcing action to describe all changes is that it is clear what is happening in the application. If something changes, you can see why. Actions are like indicators of what’s going on. Finally, to string actions and states together, we developed some functions, known as reducer. The last place to put state is the Store, and the one that triggers the action method is the Dispatch.

So we used the component in the entry file to achieve global store sharing, but looking back at the /app/reducers.js code, we reserved an injectedReducers entry parameter, The redux for each container can be merged into the Store when the component is initialized. So we now need a method that automatically merges the redux of the current container into the store every time the container is initialized.

So let’s create some files in /app/utils to implement the merge redux method.

injectReducer.js

import React from 'react';
import { ReactReduxContext } from 'react-redux';

import getInjectors from './reducerInjectors';

/** * Dynamically inject a reducer **@param {string} Key Reducer module name *@param {function} Reducer A reducer function that will be injected * */
const useInjectReducer = ({ key, reducer }) = > {
  // Get the context of the top-level parent
  const context = React.useContext(ReactReduxContext);
  // Perform a Reducer injection when the current component is initialized
  React.useEffect(() = >{ getInjectors(context.store).injectReducer(key, reducer); } []); };export { useInjectReducer };
Copy the code

reducerInjectors.js

import invariant from 'invariant';
import { isEmpty, isFunction, isString } from 'lodash';

import checkStore from './checkStore';
import createReducer from '.. /reducers';

export function injectReducerFactory(store, isValid) {
  return function injectReducer(key, reducer) {
    if(! isValid) checkStore(store);// Verify that the parameters are validinvariant( isString(key) && ! isEmpty(key) && isFunction(reducer),'(app/utils...) InjectReducer: Module name key should be a non-empty string and reducer should be a reducer function',);// We may modify Reducer during development
    InjectedReducers [key] === Reducer 'so that hot load is performed at the same time when the module keys are the same but the reducer is not
    if (Reflect.has(store.injectedReducers, key) && store.injectedReducers[key] === reducer) return;

    Replace the new Reducer
    store.injectedReducers[key] = reducer; // eslint-disable-line no-param-reassign
    store.replaceReducer(createReducer(store.injectedReducers));
  };
}

export default function getInjectors(store) {
  // Check the validity of store
  checkStore(store);

  return {
    injectReducer: injectReducerFactory(store, true),}; }Copy the code

checkStore.js

import { conformsTo, isFunction, isObject } from 'lodash';
import invariant from 'invariant';

/** * Verify the validity of redux store */
export default function checkStore(store) {
  const shape = {
    dispatch: isFunction,
    subscribe: isFunction,
    getState: isFunction,
    replaceReducer: isFunction,
    runSaga: isFunction,
    injectedReducers: isObject,
    injectedSagas: isObject,
  };
  invariant(conformsTo(store, shape), '(app/utils...) Injectors: Requires valid redux store');
}
Copy the code

Now that we have the general method to merge redux, we only need to call it once in index.js.

Return to the reducer.js of our current container

When we introduced the React plugin, we mentioned immer. If the previous explanation was a little hard to understand, this article explains it well. Here’s my summary:

Using ES6 proxy, js immutable data structure is realized with minimal cost. When the data needs to be changed, the entire structure tree is not re-deep-copied, but only the data needs to be changed. Here’s an example:

const currentState = {
  a: [].p: {
    x: 1}}let nextState = produce(currentState, (draft) = > {
  draft.a.push(2);
})

currentState.a === nextState.a; // false
currentState.p === nextState.p; // true
Copy the code

reducer.js

import produce from 'immer';
import { DEFAULT_ACTION } from './constants';

export const initialState = {
  title: 'Home'
  msg: 'rainbow in paper'};/* eslint-disable default-case, no-param-reassign */
const homeReducer = (state = initialState, action) = >
  // eslint-disable-next-line no-unused-vars
  produce(state, draft= > {
    switch (action.type) {
      case DEFAULT_ACTION:
        break; }});export default homeReducer;
Copy the code

selectors.js

Reselect is used to evaluate state. The values we use in saga and index for state are retrieved from these selectors to improve performance.

import { createSelector } from 'reselect';
import { initialState } from './reducer';

// Home is a direct selector for the state field
// Return state of the current module
const selectHomeDomain = state= > state.home || initialState;

/** * ResELECT can cache incoming dependencies. If the result of the function passed in is unchanged, the result returned will not change

const makeSelectHome = () = > createSelector(selectHomeDomain, subState= > subState);
const makeSelectHomeMsg = () = > createSelector(selectHomeDomain, subState= > subState.msg);

export default makeSelectHome;
export { makeSelectHome, makeSelectHomeMsg };
Copy the code

Loadable.js

We all use lazy loading to load containers when we write routes to improve performance, so this file is the entry to the current container or component and returns a wrapped lazy loading component. If you want to see the implementation of lazy loading, take a look at this article.

Since all lazy loading wrappers work the same way, we’ll start with a wrapper inside /app/utils/loadable.js. The packaging method is fixed and I won’t explain it too much here.

import React, { lazy, Suspense } from 'react';

const loadable = (importFunc, { fallback = null } = { fallback: null }) = > {
  const LazyComponent = lazy(importFunc);

  return props= > (
    <Suspense fallback={fallback}>
      <LazyComponent {. props} / >
    </Suspense>
  );
};

export default loadable;
Copy the code

Loadable.js

Because there is already a wrapper, the implementation is simple.

import loadable from '.. /.. /utils/loadable';

export default loadable(() = > import('./index'));
Copy the code

saga.js

Saga is used to handle side effects such as exceptions and asynchronism, and the running of saga depends on Redux. Therefore, Saga also needs to be registered dynamically, so we need to write a public method to register saga just like redux, which is called when each container is initialized and automatically logged out when components are uninstalled.

So let’s create our registration method under /app/utils:

injectSaga.js

import React from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics';
import { ReactReduxContext } from 'react-redux';

import getInjectors from './sagaInjectors';

/** * Dynamically inject Saga, pass the props of the component as the Saga parameter *@param {string} Modules for Saga *@param {function} Saga * will be injected@param {string} [mode] Register mode */

const useInjectSaga = ({ key, saga, mode }) = > {
  // Get the context of the top-level parent
  const context = React.useContext(ReactReduxContext);
  // A Saga registration is performed when the current component is initialized
  React.useEffect(() = > {
    const injectors = getInjectors(context.store);
    / / registered saga
    injectors.injectSaga(key, { saga, mode });

    return () = > {
      // Execute logout saga when the component is uninstalledinjectors.ejectSaga(key); }; } []); };export { useInjectSaga };
Copy the code

sagaInjectors.js

import invariant from 'invariant';
import { isEmpty, isFunction, isString, conformsTo } from 'lodash';

import checkStore from './checkStore';
import { DAEMON, ONCE_TILL_UNMOUNT, RESTART_ON_REMOUNT } from './constants';

const allowedModes = [RESTART_ON_REMOUNT, DAEMON, ONCE_TILL_UNMOUNT];

const checkKey = key= >invariant(isString(key) && ! isEmpty(key),'(app/utils...) InjectSaga: Module name key should be a non-empty string ');

const checkDescriptor = descriptor= > {
  const shape = {
    saga: isFunction,
    mode: mode= > isString(mode) && allowedModes.includes(mode),
  };
  invariant(
    conformsTo(descriptor, shape),
    '(app/utils...) InjectSaga: Should pass in a valid saga Descriptor ',); };export function injectSagaFactory(store, isValid) {
  return function injectSaga(key, descriptor = {}, args) {
    if(! isValid) checkStore(store);constnewDescriptor = { ... descriptor,mode: descriptor.mode || DAEMON,
    };
    const { saga, mode } = newDescriptor;
    // Verify that the parameters are valid
    checkKey(key);
    checkDescriptor(newDescriptor);
    // Is there any saga registered
    let hasSaga = Reflect.has(store.injectedSagas, key);
    // If not in production, perform hot update judgment, as saga does not change in development
    if(process.env.NODE_ENV ! = ='production') {
      const oldDescriptor = store.injectedSagas[key];
      // If the new saga is different from the old saga, cancel the old saga
      if(hasSaga && oldDescriptor.saga ! == saga) { oldDescriptor.task.cancel(); hasSaga =false; }}// Register the current saga if no saga has been registered or if the registration mode is restart with reload
    if(! hasSaga || (hasSaga && mode ! == DAEMON && mode ! == ONCE_TILL_UNMOUNT)) {/* eslint-disable no-param-reassign */store.injectedSagas[key] = { ... newDescriptor,task: store.runSaga(saga, args),
      };
      /* eslint-enable no-param-reassign */}}; }export function ejectSagaFactory(store, isValid) {
  return function ejectSaga(key) {
    if(! isValid) checkStore(store);// Verify that the parameters are valid
    checkKey(key);
    // Determine whether the saga of the current module exists
    if (Reflect.has(store.injectedSagas, key)) {
      const descriptor = store.injectedSagas[key];
      // If the registration mode is not the default, the current saga will be unregistered each time
      if(descriptor.mode && descriptor.mode ! == DAEMON) { descriptor.task.cancel();// Clean up during production; During development, we need "Descriptor. Saga" for hot reloading
        if (process.env.NODE_ENV === 'production') {
          // Some values are required to test 'injectSaga' in 'ONCE_TILL_UNMOUNT' saga
          store.injectedSagas[key] = 'done'; // eslint-disable-line no-param-reassign}}}}; }export default function getInjectors(store) {
  // Check the validity of store
  checkStore(store);

  return {
    / / register sage
    injectSaga: injectSagaFactory(store, true),
    / / cancellation of saga
    ejectSaga: ejectSagaFactory(store, true),}; }Copy the code

constants.js

// RESTART_ON_REMOUNT saga will be started when components are installed and cancelled with 'task.cancel()' when components are uninstalled to improve performance.
export const RESTART_ON_REMOUNT = '@@saga-injector/restart-on-remount';
// By default (DAEMON) saga is started when a component is loaded, never cancelled or restarted.
export const DAEMON = '@@saga-injector/daemon';
// ONCE_TILL_UNMOUNT behaves like "restart on reload" and will not run again until reload.
export const ONCE_TILL_UNMOUNT = '@@saga-injector/once-till-unmount';
Copy the code

Now that we have completed the general method for registering and unregistering saga, we only need to call it once in index.js.

Go back to saga.js for our current container

Saga is a type of Middleware that works between actions and reducer. If you follow the original Redux workflow, when a component generates a

After action, reducer modification state will be directly triggered. In practice, the actions that occur in the components need to be completed before entering the reducer

Asynchronous tasks, which is obviously not supported in native Redux.

However, the overall steps for implementing asynchronous operations are clear: when an action is triggered, it executes the asynchronous task first, and when it is complete, it is handed the action

Reducer.

Saga needs a global listener that listens for actions issued by components and forwards them to the appropriate receiver

(Worker saga), and then the receiver performs specific tasks. After the task is executed, another action is sent to reducer to modify state, so

It must be noted that the action monitored by Watcher Saga and the action issued by the corresponding worker saga cannot be the same; otherwise, an infinite loop will occur

In Saga, both global listeners and receivers use Generator functions and some of saga’s own auxiliary functions to control the entire process

The whole process can be simply described as

Component - > Action1 - > Watcher Saga - > Worker Saga - > Action2 - > Reducer - > Component

saga.js

import { takeLatest, delay } from 'redux-saga/effects';

import { DEFAULT_ACTION } from './constants';

// Page initialization
export function* defaultSaga() {
  try {
    yield delay(3000);
    console.log('delay 3s log');
  } catch (error) {
    console.log(error); }}export default function* homeSaga() {
  // takeLatest does not allow multiple Saga tasks to run in parallel.
  // Once the newly initiated action is received, it will cancel all previously forked tasks (if they are still being executed).
  yield takeLatest(DEFAULT_ACTION, defaultSaga);
}
Copy the code

mseeages.js

In order to avoid frequent conflicts caused by multiple people co-developing a zh.json file, message.js files are placed under each module first and automatically generated to zh.json and en.json files by Node tool after development.

import { defineMessages } from 'react-intl';

export const scope = 'app.containers.Home';

export default defineMessages({
  changeLang: {
    id: `${scope}.changeLang`.defaultMessage: 'Switch languages'.// Default Chinese
    // Language description, here we write English information
		// NPM run extract-intl assigns description to the corresponding English message
    description: 'Change Language',},webTitle: {
    id: `${scope}.webTitle`.defaultMessage: 'Rainbow on paper'.description: 'Rainbow In Paper',}});Copy the code

nodes.js

Nodes are styled-components for all nodes of the current page, and are referenced by the Index page, so the page layout is structured as a series of semantic tags, which can make the structure of the page clear.

import styled, { css } from 'styled-components';

const container = css`
  text-align: center;
  margin: 50px;
`;

/* eslint-disable prettier/prettier */
const Container = styled.div`${container}`;

export default { Container };
Copy the code

index.js

/** React.memo(...) React v16.6 is a new property. It acts like the React.PureComponent, controlling the re-rendering of function components. React.memo(...) This is the react. PureComponent of the function component. Packing the memo component is to optimize the performance of the component as much as possible and avoid unnecessary, useless or repetitive rendering */
import React, { memo, useEffect } from 'react';
import { connect } from 'react-redux';
import { Helmet } from 'react-helmet';
import PropTypes from 'prop-types';
import { createStructuredSelector } from 'reselect';
import { compose } from 'redux';
import { FormattedMessage } from 'react-intl';
import { useInjectReducer } from '.. /.. /utils/injectReducer';
import { useInjectSaga } from '.. /.. /utils/injectSaga';
import reducer from './reducer';
import saga from './saga';
import { makeSelectHome } from './selectors';
import { makeSelectLocale } from '.. /LanguageProvider/selectors';
import { defaultAction } from './actions';
import messages from './messages';
import Nodes from './nodes';
import { Button } from 'antd';
import { changeLocale } from '.. /LanguageProvider/actions';

export function Home(props) {
  / / register reducer
  useInjectReducer({ key: 'home', reducer });
  / / registered saga
  useInjectSaga({ key: 'home', saga });
  const { msg } = props.home;
  UseEffect is equivalent to the life cycle of a function component
  useEffect(() = > {
    props.defaultAction();
    console.log('Component load');
    return () = > {
      console.log('Component unload'); }} []);return (
    <div>
      <FormattedMessage {. messages.webTitle} >
        {title => (
          <Helmet>
            <title>{title}</title>
            <meta name="description" content="Description of Home" />
          </Helmet>
        )}
      </FormattedMessage>
      <Nodes.Container>
        <Nodes.Title>
          <FormattedMessage {. messages.webTitle} / >
        </Nodes.Title>
        <Button
          type="primary"
          onClick={()= > {
            props.changeLang(props.locale === 'zh' ? 'en' : 'zh');
          }}
        >
          <FormattedMessage {. messages.changeLang} / >
        </Button>
      </Nodes.Container>
    </div>
  );
}

/** Use the property type to record the expected type of the property passed to the component. The runtime type checks the props. * /
// eslint-disable-next-line react/no-typos
Home.PropTypes = {
  dispatch: PropTypes.func.isRequired,
};

// Inject the required state for the props of the current component
const mapStateToProps = createStructuredSelector({
  locale: makeSelectLocale(),
  home: makeSelectHome(),
});
// Import the required actions for the props of the current component
function mapDispatchToProps(dispatch) {
  return {
    dispatch,
    defaultAction: () = > {
      dispatch(defaultAction());
    },
    changeLang: lang= >{ dispatch(changeLocale(lang)); }}; }/** Connect the React component to Redux store. The wire operation does not change the original component class. Instead, it returns a new component class that is connected to the Redux Store. MapStateToProps: If defined, the component will listen for changes to the Redux store. Any time the Redux Store changes, the mapStateToProps function is called. The callback function must return a pure object that is merged with the component's props. If you omit this parameter, your component will not listen to the Redux store. MapDispatchToProps: If you were passing an object, then each function defined on that object would be treated as Redux Action Creator, with the method name defined by the object as the property name; Each method returns a new function in which the Dispatch method takes the return value of Action Creator as an argument. These properties are incorporated into the props of the component. Based on the configuration information, a React component is returned with state and Action Creator injected. * /
const withConnect = connect(mapStateToProps, mapDispatchToProps);

/** Combine multiple functions from right to left. This is the method in functional programming, put in Redux for convenience. It is needed when multiple Store enhancers need to be executed sequentially. Return value: The final function synthesized from the received function from right to left. * /
export default compose(withConnect, memo)(Home);
Copy the code

Write a component

The component doesn’t need to deal with async, and it doesn’t need state either. It just needs to receive incoming data from the outside and digest it internally, so the component needs fewer modular files than the container.

  • Loadable.jsReact lazily loads components. If the components are too large, they need to be lazily loaded. Otherwise, index can be used
  • mseeages.jsInternationalization information
  • node.jsstyle
  • index.jscomponent

Here I write a component that dynamically changes the theme color:

Here we use a plugin for color selection:

$ npm install react-color
Copy the code

Loadable.js

import loadable from '.. /.. /utils/loadable';

export default loadable(() = > import('./index'));
Copy the code

mseeages.js

import { defineMessages } from 'react-intl';

export const scope = 'app.components.ThemeSelect';

export default defineMessages({
  changeTheme: {
    id: `${scope}.changeTheme`.defaultMessage: 'Switch themes'.description: 'Change Theme',}});Copy the code

node.js

import { createGlobalStyle } from 'styled-components';

const ContainerStyle = createGlobalStyle` .ant-popover-inner{ background-color: transparent ! important; box-shadow: none ! important; } `;

export default {
  ContainerStyle,
};
Copy the code

index.js

import React, { memo, useState } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import messages from './messages';
import { Popover, Button, message } from 'antd';
import { BlockPicker, CirclePicker } from 'react-color';
import Nodes from './nodes';

function ThemeSelect(props) {
  const [visible, setVisible] = useState(false);
  const type = props.type === 'circle' ? 'circle' : 'default';

  const handleColorChange = color= > {
    window.less
      .modifyVars({ '@primary-color': color.hex, '@btn-primary-bg': color.hex })
      .then(() = > {
        setVisible(false);
        message.success('Theme change successful');
      })
      .catch(error= > {
        message.error(error);
      });
  };

  const handleVisibleChange = v= > {
    setVisible(v);
  };

  return (
    <>
      <Popover
        content={
          type= = ='circle'? (<CirclePicker onChange={handleColorChange} />
          ) : (
            <BlockPicker onChange={handleColorChange} />
          )
        }
        trigger="click"
        visible={visible}
        onVisibleChange={handleVisibleChange}
      >
        <Button type="primary">
          <FormattedMessage {. messages.changeTheme} / >
        </Button>
      </Popover>
      <Nodes.ContainerStyle />
    </>
  );
}
// Default parameters
ThemeSelect.defaultProps = {
  type: 'default'};// Pass in parameters
ThemeSelect.propTypes = {
  type: PropTypes.string,
};

export default memo(ThemeSelect);
Copy the code

Then we’ll reference it in the previous Home:

.import ThemeSelect from '.. /.. /components/ThemeSelect'; .export function Home(props) {...return(... <ThemeSelect type="circle" />
    ...
  )
}
Copy the code

We’re done. We’ve gone through the whole engineering process so far. One problem was that although we did a good job modularizing each container, we created too many files, and a lot of the code was the same, so we needed a template building tool that allowed us to automatically initialize containers and components, and we just wrote the business. And since there is internationalization so we now default to Chinese and English, how to dynamically add another language? So let’s move on to the final step: template building.

Templates to build

Template construction here to use the scaffolding tool ploP, it is a miniature scaffolding tool, its characteristics can be based on a template file batch generated text or code, no need to manually copy and paste, save trouble and effort.

First let’s install:

$ npm install plop -D
Copy the code

Then we will create a new folder generators under /internals and create index.js.

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Component generator
const componentGenerator = require('./component/index.js');
// Container generator
const containerGenerator = require('./container/index.js');
// Language generator
const languageGenerator = require('./language/index.js');

// Backup file name extension
const BACKUPFILE_EXTENSION = 'rbgen';

module.exports = plop= > {
  // Create a generator
  plop.setGenerator('component', componentGenerator);
  plop.setGenerator('container', containerGenerator);
  plop.setGenerator('language', languageGenerator);
	// Define the plop Action Type
  // Format the code
  plop.setActionType('prettify'.(answers, config) = > {
    const folderPath = `${path.join(
      __dirname,
      '/.. /.. /app/',
      config.path,
      plop.getHelper('properCase')(answers.name),
      '* *'.'**.js')},`;

    // eslint-disable-next-line no-useless-catch
    try {
      execSync(`npm run prettify -- "${folderPath}"`);
      return folderPath;
    } catch (err) {
      throwerr; }});// Back up the file
  plop.setActionType('backup'.(answers, config) = > {
    // eslint-disable-next-line no-useless-catch
    try {
      fs.copyFileSync(
        path.join(__dirname, config.path, config.file),
        path.join(
          __dirname,
          config.path,
          `${config.file}.${BACKUPFILE_EXTENSION}`,),'utf8',);return path.join(
        __dirname,
        config.path,
        `${config.file}.${BACKUPFILE_EXTENSION}`,); }catch (err) {
      throwerr; }}); };module.exports.BACKUPFILE_EXTENSION = BACKUPFILE_EXTENSION;
Copy the code

We also need a way to check each time we create a new component to see if we have already created a component or container with the same name. Here we will create an utils folder for generators and then create a Componentsyt.js.

const fs = require('fs');
const path = require('path');
const pageComponents = fs.readdirSync(path.join(__dirname, '.. /.. /.. /app/components'));
const pageContainers = fs.readdirSync(path.join(__dirname, '.. /.. /.. /app/containers'));
const components = pageComponents.concat(pageContainers);

function componentExists(comp) {
  return components.indexOf(comp) >= 0;
}

module.exports = componentExists;

Copy the code

Container to build

Create the folder container under generators and create index.js.

const componentExists = require('.. /utils/componentExists');

module.exports = {
  description: 'Add a container'.prompts: [{type: 'input'.name: 'name'.message: 'What is the container name? '.default: 'Form'.validate: value= > {
        if (+ /. /.test(value)) {
          return componentExists(value) ? 'Same container name or component name already exists' : true;
        }

        return 'Container name required'; }, {},type: 'confirm'.name: 'memo'.default: true.message: 'Do YOU want to wrap the container in React. Memo? '}, {type: 'confirm'.name: 'wantHeaders'.default: true.message: 'Do I need page header information? '}, {type: 'confirm'.name: 'wantActionsAndReducer'.default: true.message:
        'if containers need actions/constants/selectors/reducer? '}, {type: 'confirm'.name: 'wantSaga'.default: true.message: 'Does container need Saga? '}, {type: 'confirm'.name: 'wantMessages'.default: true.message: 'Does the container need to internationalize components? '}, {type: 'confirm'.name: 'wantLoadable'.default: true.message: 'Does the container load asynchronously? '],},actions: data= > {
    // Generate the page
    const actions = [
      {
        type: 'add'.path: '.. /.. /app/containers/{{properCase name}}/index.js'.templateFile: './container/index.js.hbs'.abortOnFail: true.// If this operation fails for any reason, abort all subsequent operations
      },
      {
        type: 'add'.path: '.. /.. /app/containers/{{properCase name}}/nodes.js'.templateFile: './container/nodes.js.hbs'.abortOnFail: true,},];if (data.wantMessages) {
      actions.push({
        type: 'add'.path: '.. /.. /app/containers/{{properCase name}}/messages.js'.templateFile: './container/messages.js.hbs'.abortOnFail: true}); }if (data.wantActionsAndReducer) {
      // Actions
      actions.push({
        type: 'add'.path: '.. /.. /app/containers/{{properCase name}}/actions.js'.templateFile: './container/actions.js.hbs'.abortOnFail: true});// Constants
      actions.push({
        type: 'add'.path: '.. /.. /app/containers/{{properCase name}}/constants.js'.templateFile: './container/constants.js.hbs'.abortOnFail: true});// Selectors
      actions.push({
        type: 'add'.path: '.. /.. /app/containers/{{properCase name}}/selectors.js'.templateFile: './container/selectors.js.hbs'.abortOnFail: true});// Reducer
      actions.push({
        type: 'add'.path: '.. /.. /app/containers/{{properCase name}}/reducer.js'.templateFile: './container/reducer.js.hbs'.abortOnFail: true}); }// Sagas
    if (data.wantSaga) {
      actions.push({
        type: 'add'.path: '.. /.. /app/containers/{{properCase name}}/saga.js'.templateFile: './container/saga.js.hbs'.abortOnFail: true}); }if (data.wantLoadable) {
      actions.push({
        type: 'add'.path: '.. /.. /app/containers/{{properCase name}}/Loadable.js'.templateFile: './container/loadable.js.hbs'.abortOnFail: true}); }/ / format
    actions.push({
      type: 'prettify'.path: '/containers/'});returnactions; }};Copy the code

As shown in the code above, there are nine files to create for each container, which also corresponds to nine templates:

  • constants.js.hbs
  • actions.js.hbs
  • reducer.js.hbs
  • selectors.js.hbs
  • loadable.js.hbs
  • saga.js.hbs
  • mseeages.js.hbs
  • node.js.hbs
  • index.js.hbs

Template files are. HBS, which stands for Handlebars, a pure template engine. HBS template engine syntax: {{… }}, two curly braces. The template file content is easy to understand, but not much to explain.

Handlebars is a JavaScript semantic template library that allows you to quickly build Web templates by separating views from data. It uses the idea of “logic-less templates” and is precompiled at load time, rather than when the client executes the code, to ensure that the template loads and runs quickly.

constants.js.hbs

// Export const COMPONENT_DID_MOUNT = 'app/{{ properCase name }}/COMPONENT_DID_MOUNT';
Copy the code

actions.js.hbs

import { COMPONENT_DID_MOUNT } from './constants'; export function componentDidMountAction() { return { type: COMPONENT_DID_MOUNT, }; }Copy the code

reducer.js.hbs

import produce from 'immer'; import { COMPONENT_DID_MOUNT } from './constants'; export const initialState = {}; /* eslint-disable default-case, no-param-reassign */ const{{ camelCase name }}Reducer = (state = initialState, action) => // eslint-disable-next-line no-unused-vars produce(state, draft => { switch (action.type) { case COMPONENT_DID_MOUNT: break; }}); export default{{ camelCase name }}Reducer;
Copy the code

selectors.js.hbs

import { createSelector } from 'reselect'; import { initialState } from './reducer'; const select{{ properCase name }}Domain = state => state.{{ camelCase name }}|| initialState; const makeSelect{{ properCase name }} = () => createSelector(select{{ properCase name }}Domain, subState => subState); export default makeSelect{{ properCase name }};
export { select{{ properCase name }}Domain, makeSelect{{ properCase name }} };
Copy the code

loadable.js.hbs

import loadable from '.. /.. /utils/loadable'; export default loadable(() => import('./index'));Copy the code

saga.js.hbs

import { take, takeLatest, put, select } from 'redux-saga/effects'; import { COMPONENT_DID_MOUNT } from './constants'; Export function* componentDidMountSaga() {try {console.log('componentDidMountSaga'); } catch (error) { console.log(error); } } export default function*{{ camelCase name }}Saga() { yield takeLatest(COMPONENT_DID_MOUNT, componentDidMountSaga) }Copy the code

mseeages.js.hbs

import { defineMessages } from 'react-intl'; export const scope = 'app.containers.{{ properCase name }}'; export default defineMessages({ header: { id: `${scope}.header`, defaultMessage: 'This is the{{ properCase name }}container! '}});Copy the code

node.js.hbs

/* import styled, { css } from 'styled-components' const containerStyle = css` width: 100%; ` const container = styled.div`${containerStyle}` export default { container } */Copy the code

index.js.hbs

{{#if memo}}
import React, { memo, useEffect } from 'react';
{{else}}
import React from 'react';
{{/if}}
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
{{#if wantHeaders}}
import { Helmet } from 'react-helmet';
{{/if}}
{{#if wantMessages}}
import { FormattedMessage } from 'react-intl';
{{/if}}
{{#if wantActionsAndReducer}}
import { createStructuredSelector } from 'reselect';
{{/if}}
import { compose } from 'redux';

{{#if wantSaga}}
import { useInjectSaga } from 'utils/injectSaga';
{{/if}}
{{#if wantActionsAndReducer}}
import { useInjectReducer } from 'utils/injectReducer';
import makeSelect{{properCase name}} from './selectors';
import reducer from './reducer';
import { componentDidMountAction } from './actions';
{{/if}}
{{#if wantSaga}}
import saga from './saga';
{{/if}}
{{#if wantMessages}}
import messages from './messages';
{{/if}}// import Nodes from './nodes'; export function{{ properCase name }}(props) {
  {{#if wantActionsAndReducer}}
  useInjectReducer({ key: '{{ camelCase name }}', reducer });
  {{/if}}
  {{#if wantSaga}}
  useInjectSaga({ key: '{{ camelCase name }}', saga }); useEffect(() => { props.componentDidMountAction(); } []);{{/if}}

  return (
    <div>
    {{#if wantHeaders}}
      <Helmet>
        <title>{{properCase name}}</title>
        <meta name="description" content="Description of {{properCase name}}" />
      </Helmet>
    {{/if}}
    {{#if wantMessages}}
      <FormattedMessage {. messages.header} / >
    {{/if}}
    </div>
  );
}

{{ properCase name }}.propTypes = { dispatch: PropTypes.func.isRequired, };{{#if wantActionsAndReducer}}
const mapStateToProps = createStructuredSelector({
  {{ camelCase name }}: makeSelect{{properCase name}}()});{{/if}}function mapDispatchToProps(dispatch) { return { dispatch, componentDidMountAction: () => dispatch(componentDidMountAction()), }; }{{#if wantActionsAndReducer}}
const withConnect = connect(mapStateToProps, mapDispatchToProps);
{{else}}
const withConnect = connect(null, mapDispatchToProps);
{{/if}}export default compose( withConnect,{{#if memo}}
  memo,
{{/if}}) ({{ properCase name }});
Copy the code

Components to build

/ / Create index.js in the generators folder component. The build method is the same as the container, which is not explained here.

const componentExists = require('.. /utils/componentExists');

module.exports = {
  description: 'Add a component'.prompts: [{type: 'input'.name: 'name'.message: 'What is the component name? '.default: 'Button'.validate: value= > {
        if (+ /. /.test(value)) {
          return componentExists(value) ? 'Same container name or component name already exists' : true;
        }

        return 'Component name Required'; }, {},type: 'confirm'.name: 'memo'.default: true.message: 'Do YOU want to wrap components in React. Memo? '}, {type: 'confirm'.name: 'wantMessages'.default: true.message: 'Does the component require an internationalized component? '}, {type: 'confirm'.name: 'wantLoadable'.default: true.message: 'Does the component load asynchronously? '],},actions: data= > {
    const actions = [
      {
        type: 'add'.path: '.. /.. /app/components/{{properCase name}}/index.js'.templateFile: './component/index.js.hbs'.abortOnFail: true}, {type: 'add'.path: '.. /.. /app/components/{{properCase name}}/nodes.js'.templateFile: './container/nodes.js.hbs'.abortOnFail: true,},];if (data.wantMessages) {
      actions.push({
        type: 'add'.path: '.. /.. /app/components/{{properCase name}}/messages.js'.templateFile: './component/messages.js.hbs'.abortOnFail: true}); }if (data.wantLoadable) {
      actions.push({
        type: 'add'.path: '.. /.. /app/components/{{properCase name}}/Loadable.js'.templateFile: './container/loadable.js.hbs'.abortOnFail: true}); } actions.push({type: 'prettify'.path: '/components/'});returnactions; }};Copy the code

As shown in the code above, there are four files to create for each component, which also correspond to four templates:

  • loadable.js.hbsThis file is the same as the container so there is no need to write a template.
  • mseeages.js.hbs
  • node.js.hbsThis file is the same as the container so there is no need to write a template.
  • index.js.hbs

mseeages.js.hbs

import { defineMessages } from 'react-intl'; export const scope = 'app.components.{{ properCase name }}'; export default defineMessages({ header: { id: `${scope}.header`, defaultMessage: 'This is the{{ properCase name }}component! '}});Copy the code

index.js.hbs

{{#if memo}}
import React, { memo } from 'react';
{{else}}
import React from 'react';
{{/if}}
// import PropTypes from 'prop-types';

{{#if wantMessages}}
import { FormattedMessage } from 'react-intl';
import messages from './messages';
{{/if}}
import Nodes from './nodes';

function {{ properCase name }}() {
  return (
    <div>
    {{#if wantMessages}}
      <FormattedMessage {. messages.header} / >
    {{/if}}
    </div>
  );
}

{{ properCase name }}.defaultProps = {};

{{ properCase name }}.propTypes = {};

{{#if memo}}
export default memo({{ properCase name }});
{{else}}
export default {{ properCase name }};
{{/if}}
Copy the code

Add language

In the previous /app/i18n.js file we configured the internationalization of the project, so adding a language also needs to be done in this file. Since we are modifying an existing file, we need to use the re to match the location in the file and then add statements from the template. So according to the previous i18n.js file, determine that there are several places to add, and then determine that the following file template is needed:

  • intl-locale-data.hbs

    $&const {{language}}LocaleData = require('react-intl/locale-data/{{language}}');
    Copy the code
  • translation-messages.hbs

    $1const {{language}}TranslationMessages = require('./translations/{{language}}.json');
    Copy the code
  • add-locale-data.hbs

    $1addLocaleData({{language}}LocaleData);
    Copy the code
  • format-translation-messages.hbs

    The $1{{language}}: formatTranslationMessages('{{language}}', {{language}}TranslationMessages),
    Copy the code
  • translations-json.hbs

    {}
    Copy the code
  • polyfill-intl-locale.hbs

    $1 import('intl/locale-data/jsonp/{{language}}.js'),
    Copy the code

Generators file to create folder language, then index.js.

const fs = require('fs');
const { exec } = require('child_process');
// Check whether the language has been added
function languageIsSupported(language) {
  try {
    fs.accessSync(`app/translations/${language}.json`, fs.F_OK);
    return true;
  } catch (e) {
    return false; }}module.exports = {
  description: 'Add a language'.prompts: [{type: 'input'.name: 'language'.message: 'What languages would you like to add i18N support for (e.g. "FR ", "de")? '.default: 'en'.validate: value= > {
        if (+ /. /.test(value) && value.length === 2) {
          return languageIsSupported(value) ? 'Language already supported'${value}"The ` : true;
        }

        return '2-character language specifier required'; }},].actions: ({ test }) = > {
    const actions = [];

    if (test) {
      // Back up the files to be modified so that we can restore them
      actions.push({
        type: 'backup'.path: '.. /.. /app'.file: 'i18n.js'}); actions.push({type: 'backup'.path: '.. /.. /app'.file: 'app.js'}); } actions.push({type: 'modify'.// Perform the modification in the specified file
      path: '.. /.. /app/i18n.js'.// The regular expression used to match the text to be replaced writes the template content on the line after the last match
      pattern: /(const .. LocaleData = require\('react-intl\/locale-data\/.. '\); \n)+/g,
      templateFile: './language/intl-locale-data.hbs'}); actions.push({type: 'modify'.path: '.. /.. /app/i18n.js'.pattern: /(\s+'[a-z]+',\n)(? ! .*\s+'[a-z]+',)/g,
      templateFile: './language/app-locale.hbs'}); actions.push({type: 'modify'.path: '.. /.. /app/i18n.js'.pattern:
        /(const .. TranslationMessages = require\('\.\/translations\/.. \.json'\); \n)(? ! const .. TranslationMessages = require\('\.\/translations\/.. \.json'\); \n)/g,
      templateFile: './language/translation-messages.hbs'}); actions.push({type: 'modify'.path: '.. /.. /app/i18n.js'.pattern: /(addLocaleData\([a-z]+LocaleData\); \n)(? ! .*addLocaleData\([a-z]+LocaleData\);) /g,
      templateFile: './language/add-locale-data.hbs'}); actions.push({type: 'modify'.path: '.. /.. /app/i18n.js'.pattern:
        /([a-z]+:\sformatTranslationMessages\('[a-z]+',\s[a-z]+TranslationMessages\),\n)(? ! .*[a-z]+:\sformatTranslationMessages\('[a-z]+',\s[a-z]+TranslationMessages\),)/g,
      templateFile: './language/format-translation-messages.hbs'}); actions.push({type: 'add'.path: '.. /.. /app/translations/{{language}}.json'.templateFile: './language/translations-json.hbs'.abortOnFail: true}); actions.push({type: 'modify'.path: '.. /.. /app/app.js'.pattern:
        /(import\('intl\/locale-data\/jsonp\/[a-z]+\.js'\),\n)(? ! .*import\('intl\/locale-data\/jsonp\/[a-z]+\.js'\),)/g,
      templateFile: './language/polyfill-intl-locale.hbs'});if(! test) {// Perform an integration of internationalization information once the code has been added
      actions.push(() = > {
        const cmd = 'npm run extract-intl';
        exec(cmd, (err, result) = > {
          if (err) throw err;
          process.stdout.write(result);
        });
        return 'modify translation messages';
      });
    }

    returnactions; }};Copy the code

Add template build directives

"scripts": {
  "generate": "plop --plopfile internals/generators/index.js"."prettify": "prettier --write"
}
Copy the code

Now that we’ve finished building the template, try NPM Run generate.

Engineering summary

So far, let’s review what engineering has done:

  • Initialization package. Json

  • Build the Webpack scaffolding

    • html-webpack-plugin

      Generate an HTML5 file and import all of your WebPack-generated bundles using the Script tag in the body.

    • loader

      Converts the matched files

    • circular-dependency-plugin

      Detect loop dependent modules when Webpack is packaged.

    • react-app-polyfill

      Includes compatibility with various browsers. It includes the minimum requirements and common language features used by the Create React App project.

    • webpack-dev-middleware webpack-hot-middleware

      Writing your own back-end service and then using it makes development more flexible.

    • offline-plugin

      Offline-plugin applies PWA technology to help us generate service-worker.js, and the resource list of SW will record our project resource files. Each time the code is updated, notify the client to update the cached resource by updating the SW file version number, or use the cached file otherwise.

    • terser-webpack-plugin

      Use Terser to compress JavaScript.

    • compression-webpack-plugin

      File Gzip compression improves network transmission rate and optimizes web page loading time.

  • The React buckets

    • react-helmet

      React Helmet is an HTML document head management tool that manages all changes to document headers.

    • react-intl

      Internationalization.

    • redux-saga

      Use Saga to modularize asynchronous business.

    • connected-react-router

      Implement routing operations in action.

    • history

      Use the history of the React route when jumping outside the component.

    • prop-types

      Use the property type to record the expected type of the property passed to the component. The runtime type checks the props.

    • resclect

      Redux’s selector library can improve the efficiency of using Redux data.

    • immer

      Wrap the Reducer so that the data in the state cannot be changed and can only be replaced by a specific method.

  • Antd component library

    A plugin called antD-theme -webpack-plugin is used to dynamically set theme colors.

  • Other plug-ins

    The main styled- Components are used to separate styles from the page, allowing the style to be JS. Other plug-ins are mostly used in auxiliary Node.js.

  • ESLint configuration

    Where prettier and ESLint work together to introduce common configuration plugins and then configure esLint rules We then customize the output when ESLint executes via the Node API of ESLint.

  • Stylelint configuration

    Style verification, introducing styled- Components verification rules, in conjunction with the current project.

  • Babel configuration

    A number of conversion plug-ins have been introduced for compatibility with JS syntax.

  • Project startup Configuration

    Here we have configured two ways of environment hot boot, customized command line parameter, boot output beautification and so on.

  • Writing entry files

    • Reducer

      Reducer initialization module of the project.

    • I18n internationalization

      From configuration to internationalizing the component package project, and finally the integration approach to internationalizing information.

    • Global style

    • The App component is the project root component

    • Entrance to the file

  • Write NPM instructions

    Several startup options for production and development environments, internationalization and ESLint validation, etc.

  • Componentization and modularization of projects

    • Componentization and modularization
    • Functional programming
    • routing
    • The container
    • component
  • Templates to build

    • Container to build
    • Components to build
    • Add language
    • Template build instructions

Said in the back

Here, I want to say engineering has all said, I do not know whether you can see here to engineering have their own understanding. In fact, it is not difficult to see that engineering is to put a lot of front-end ideas and concepts into practice in the project, there are many aspects of optimization, so that we can quickly and efficiently develop in both development and production environments. Of course, there are a lot of engineering things, such as: Jest testing, git operation pre-check and so on.

Skyscraper is not built into a day, I also mentioned in the article are all derived in more projects, may bring up inside of each point can be very simple things, you may find this thing take a look at the document can be used freely, but we need is not the ability to put them all together. So this article is to give you an idea that we can consider componentization, modularity, etc., as well as the specifications for multiplayer development when we build our own projects in the future.

In fact, there is one important thing missing from this project, which is the specification of API requests, so leave a hint.

Finally, see you in the next article!