The cause of

I have been working in the company for a long time, and FOUND that many students below do not understand how to set up the development framework we use. In order to strengthen the basic skills of our friends, so I wrote this article.

This time I will lead you step by step to build a development framework, a total of four articles to write.

  • Build the basic React development framework
  • Basic Node side extraction
  • Cli integration
  • The development of specification

What will this article achieve

Build webPack-based React from 0 to 1

Set up

The preparatory work

Let’s start with my development environment

To speed up our download, please execute the following command before proceeding to the next step

npm install -g yarn
npm install -g nrm 
npm install -g n

nrm use taobao

#Set the dependency on internal modules to download the Taobao image of Node during the installation
npm config set disturl https://npm.taobao.org/mirrors/node/
#Set taobao mirror of common modulesnpm config set sass_binary_site https://npm.taobao.org/mirrors/node-sass/ npm config set sharp_dist_base_url https://npm.taobao.org/mirrors/sharp-libvips/ npm config set electron_mirror https://npm.taobao.org/mirrors/electron/ npm config set puppeteer_download_host https://npm.taobao.org/mirrors/ npm config set phantomjs_cdnurl https://npm.taobao.org/mirrors/phantomjs/ npm config set sentrycli_cdnurl https://npm.taobao.org/mirrors/sentry-cli/ npm  config set sqlite3_binary_site https://npm.taobao.org/mirrors/sqlite3/ npm config set python_mirror https://npm.taobao.org/mirrors/python/Copy the code

The global Node module will be used in this article

npm install -g http-server
Copy the code

Code article

Let’s start by creating a simple development project

cd ~/Documents
mkdir react-base && cd react-base
yarn init -y
mkdir src config types public
Copy the code

Our current directory structure looks like this

Webpack ├ ─ ─ the config stored configuration file ├ ─ ─ package. The json ├ ─ ─ public deposit has nothing to do with the business of static resource file ├ ─ ─ the SRC deposit entry documents and business related code └ ─ ─ declaration file types to store tsCopy the code

Let’s start by installing webPack, our modular packaging tool

yarn add webpack webpack-cli -D
Copy the code

Create the main.js file in the SRC directory as the entry file and run the following command

echo "console.log('Hello World');" > src/index.js
Copy the code

At this time to runyarn webpack, you’ll find one generated in the dist directorymain.jsThe file can be seen belowwebpackThe working mechanism of.

becausewebpackOnly for modular syntaxexportandimportProvides support fores6The other syntax is not supported, so we compile it herees6andjsxSyntax, need to usebabel.webpackThe working mechanism is to pass firstloaderConvert static resources and output them to the target filebabel-loaderAnd create it in the root directory.babelrcThe file provides the configuration, as followsbabelThe conversion work done for us.

yarn add babel-loader @babel/core @babel/preset-env @babel/preset-react -D
yarn add react react-dom
touch .babelrc
Copy the code

The configuration in the. Babelrc file is as follows

{
    "presets": [
        "@babel/preset-env"."@babel/preset-react"]}Copy the code
  • @babel/preset-reactIs a way to convert JSX code into a function API
  • @babel/preset-envIs to transform the new feature of ES, and note that the transformation is carried out from back to front

Create SRC/app.js as follows:

import React from 'react';

const App = () = > {
  return <div>Hello World</div>
}

export default App;
Copy the code

Modify index.js as follows

import React from 'react';
import ReactDom from 'react-dom';
import App from './App';

ReactDom.render(<App />.document.getElementById('root'))
Copy the code

Create the webpack.config. MJS file in the project root directory, using the MJS suffix here to avoid using both CommonJs and ES Module in the same project, as follows

import path from 'path';

const config = {
    entry: './src/index.js'.output: {
        path: path.resolve(path.resolve(), 'dist'),
        filename: 'bundle.js'
    },
    module: {
        rules: [{
            test: /\.m? jsx? $/,
            exclude: /node_modules/,
            use: 'babel-loader'}]}};export default config;
Copy the code

To make the code work, we also need an HTML file. In the dist directory, create an index.html file that looks like this

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Document</title>
</head>
<body>
    <div id="root"></div>
    <script src="bundle.js"></script>
</body>
</html>
Copy the code

At this point we executeyarn webpack --mode none, you can see there are filesdist/bundle.jsFile generation, the file is folded, you can see is an immediate execution of the function, interested partners can take a closer look at the contents of the inside, at this time we through the above installationhttp-serverTo launch ahttpServer, execute the commandhttp-server distAnd you can see it’s started up for us8080Port.

Open it in a browserhttp://127.0.0.1:8080We can see the screen is white. Turn it onconsoleYou can see the following error

If we look at the packaged code, we can see that the import is introduced into the development version based on the configuration of the environment variablesreactIt’s a production versionreactAt this time,processThe variable is not defined.

We can use webPack’s plug-in mechanism to solve this problem by adding the following code under the module node of webpack.config.mjs

+ import webpack from 'webpack';

constconfig = { ... .plugins: [
      new webpack.DefinePlugin({
        'process.env.NODE_ENV': JSON.stringify('development')]}})Copy the code

repackyarn webpack --mode noneAs you can seeprocess.env.NODE_ENV === 'production'It’s been replaced byfalse. Run clear cache again to refresh, and you can see that the page has been displayedHello World, means there is no problem with our program.

The problem here is that the HTML files and the bundle files are written dead. We can use another webpack plugin, html-webpack-plugin, to generate the index.html file from the template and dynamically inject the packed JS file.

Create a file SRC /index.html with the following contents

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Document</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>
Copy the code

Install dependencies

yarn add html-webpack-plugin -D
Copy the code

Fortunately, the 5.0.0 version was released at the same time as this package

Change webpack.config.mjs and add the following

. +import HtmlWebpackPlugin from 'html-webpack-plugin';

const config = {
	...
    plugins: [... +new HtmlWebpackPlugin({
+            template: './src/index.html',
+            inject: 'body'+})]};Copy the code

runyarn webpack --mode none, you can see the generated indistGenerated in the directoryindex.htmlIt has been automatically injected into the filebundle.js.

The introduction oftypescript

In the same way that our framework supports typescript, we change SRC /app.js to SRC /app.tsx without changing the content. Here we add ts dependencies.

yarn add typescript ts-loader -D
yarn add @types/react @types/react-dom -D
yarn tsc --init
Copy the code

Json file will be generated in the root directory. We do not need to do too much configuration. Release the JSX node and configure react as follows

"jsx": "react".Copy the code

Add the following under the rules node of the webpack.config. MJS file

{
    test: /\.tsx? $/,
    exclude: /node_modules/,
    use: 'ts-loader'
},
Copy the code

Add the following under the Output node to avoid suffixes.

resolve: {
    extensions: ['.mjs'.'.js'.'.json'.".ts".".tsx"],},Copy the code

The directory structure is now as follows

├ ─ ─ the config ├ ─ ─ dist │ ├ ─ ─ bundle. Js │ └ ─ ─ index. The HTML ├ ─ ─ package. The json ├ ─ ─ public ├ ─ ─ the SRC │ ├ ─ ─ App. The TSX │ ├ ─ ─ index. The HTML │ └ ─ ─ index. Js ├ ─ ─ tsconfig. Json ├ ─ ─ types ├ ─ ─ webpack. Config. MJS └ ─ ─ yarn. The lockCopy the code

Yarn webpack –mode None also works.

Added CSS/LESS/SCSS support

Add loader

yarn add style-loader css-loader less-loader sass-loader less sass -D 
Copy the code

Add test file SRC/index. The CSS, SRC/index. The less, the SRC/index SCSS. src/index.css

.css {
    background-color: #f00;
}
Copy the code
.less {
    background-color: #0f0;
}
Copy the code
.scss {
    background-color: #00f;
}
Copy the code

The SRC/app.tsx code is as follows

import React from 'react';
import './index.css';
import './index.less';
import './index.scss';

const App = () = > {
  return <div>
    <h1 className="css">Hello CSS</h1>
    <h1 className="less">Hello Less</h1>
    <h1 className="scss">Hello SCSS</h1>
  </div>
}

export default App;
Copy the code

Add the following information under the rules node of webpack.config. MJS

{
  test: /\.css$/i,
  use: ['style-loader'.'css-loader'],}, {test: /\.less$/,
  use: ['style-loader'.'css-loader'.'less-loader'],}, {test: /\.s[ac]ss$/i,
  use: [
    "style-loader"."css-loader"."sass-loader",]},Copy the code

Package it with YARN webpack –mode None, and start it with HTTP-server dist. Visit http://127.0.0.1:8080/ and it will display normally.

A little bit about eachloaderThe role of,sass-loaderandless-loaderIs to.scssand.lessCode conversion to.cssCode, andcss-loaderThe role of thecssConvert the code toJs script.style-loaderThe role of is to be generatedcssFile embedded intostyleTAB, a quick look at the packaged code can be seenwebpackwillcssThe code is placed as a moduleIIFEIn the.

Static resource

We will also use some static resources in the development process, such as images, here to add dependencies for images, inwebpack@5In the version, we usually usefile-loaderandurl-loader.file-loaderTo process some of the larger file contents, andurl-loaderIt handles some small file content. inwebpack@5Features have been optimized in the version, see the following description.

Let’s create a new page SRC /pages/ home.tsx with the following content

import React from 'react';
import git from '@static/img/git.png';

const Home = () = > {
    return <div>
        <h1>Home</h1>
        <img src={git} />
    </div>
}

export default Home;
Copy the code

Add image SRC /static/img/git.png.

At this point, we can see that the compilation error has been reported, with the following error message:

The module @static/img/git.png or its corresponding type declaration could not be found.Copy the code

This is because TS does not have a way to parse the.png type declaration, we are creating the types/static.d.ts file as follows

declare module '*.svg'
declare module '*.png'
declare module '*.jpg'
declare module '*.jpeg'
declare module '*.gif'
declare module '*.bmp'
declare module '*.tiff'
Copy the code

Here, because we don’t want to keep looking for the file relative to the path, we alias the file path, change webpack.config. MJS, and add the following configuration under the Extensions node

alias: {
   '@pages': path.resolve(path.resolve(), './src/pages/'),
   '@components': path.resolve(path.resolve(), './src/components/'),
   '@static': path.resolve(path.resolve(), './src/static/'),},Copy the code

Change tsconfig.json and add the following configuration

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

Then we add loader to process the image file and add the following content under the rules node of webpack.config. MJS.

 {
   test: /\.png/,
   type: 'asset/resource'
 }
Copy the code

Run package again, we can see that an image file has been packageddistDirectory.

After a brief analysis of the packaged files, we foundloaderThe file name is processed and put into the module.

In this case, Webpack will convert the file into data: URL and put it into the bundle.js file. This method is only recommended for small files, too large will affect the size of the bundle.js file. You can see it’s a very long string.

{
  test: /\.svg/,
  type: 'asset/inline',}Copy the code

Accelerate our development process

This is done manually to improve your understanding of the packaging process. Instead of writing the makefiles directly to disk, we put them in memory to speed up compilation.

Webpack-dev-server provides a better experience for our development process, and its benefits are as follows

  • providehttp-server
  • Provide hot updates
  • Provide the agent

Let’s use webpack-dev-server step by step

providehttp-server

Install dependencies

yarn add webpack-dev-server -D
Copy the code

Change the package.json script and add the following script under the following node

"scripts": {
  "start": "webpack serve --mode development"
},
Copy the code

At this point, the browser has port 8080 enabled for us. Some static resources like Favicon. ico have been put in the public directory, and if you want your application to access this file, To add static resource path-forwarding to webpack-dev-server, add the following properties to the config object of webpack.config.mjs

devServer: {
    contentBase: path.join(path.resolve(), './public'),},Copy the code

At this point, run YARN Start and you can see that WebPack has been listening for file changes for a long time and compiled automatically. Open http://localhost:8080/ and you can see that Favicon. ico has been loaded normally.

Provide hot updates

We don’t want to manually refresh the browser every time we change code during development, so webpack-dev-server provides the configuration to automatically refresh the browser. We added hotOnly: True to the devServer property of webpack.config. MJS, and when we went to modify the CSS file, we found that the browser had refreshed automatically.

Tips: WebPack provides two properties for hot reloading: hotOnly and HOT. The difference between hot and WebPack is that HOT synchronously refreshes the browser regardless of whether it has compiled successfully, while hotOnly refreshes the browser only after it has compiled successfully.

Some students may wonder why js files do not provide such functionality, this is because CSS processing is relatively common, so style-loader already handles.

However, js needs to be customized according to the use of different frameworks. Fortunately, the community already has a ready-made solution, we choose react-hot-loader, let’s install the following

yarn add react-hot-loader @hot-loader/react-dom
Copy the code

Add plugins to.babelrc as follows

{
  "plugins": ["react-hot-loader/babel"]}Copy the code

Make adjustments to SRC/app.tsx and the code after adjustment is as follows

+ import { hot } from 'react-hot-loader/root';
import React from 'react';
import './index.css';
import './index.less';
import './index.scss';
import Home from '@pages/Home';

const App = () = > {
  return <div>
    <h1 className="css">Hello CSS</h1>
    <h1 className="less">Hello Less</h1>
    <h1 className="scss">Hello SCSS</h1>
    <Home />
  </div>
}
- export default App;
+ export default hot(App);
Copy the code

Stop the service and start YARN Start again. At this time, we add an input box to the App component and enter 123 on the page. Then, we add another input box to the App component.

Provide the agent

A React SPA application usually sends requests to the server, but the security policy of the browser often causes cross-domain problems. Here we use github’s/Users interface to simulate a simple application.

Install the required dependencies first

yarn add axios
Copy the code

Create a file SRC/pages/Users/index TSX, file contents are as follows

import React, { useEffect, useState } from 'react';
import axios from 'axios';

interface User {
    login: string;
}

const Users = () = > {
    const [users, setUsers] = useState<Array<User>>([])
    useEffect(() = > {
        axios.get('/api/users').then(res= > {
            setUsers(res.data)
        })
    }, []);
    return <>
        <ul>
            {users.map(item => <li key={item.login}>{item.login}</li>)}
        </ul>
    </>
}

export default Users;
Copy the code

At this point we need to configure the proxy for it in the webpack.config.mjs file, which looks like this:

devServer {
	...
    proxy: {
      '/api': {
        target: 'https://api.github.com/'.pathRewrite: { '^/api': ' ' },
        changeOrigin: true}}}Copy the code

Simple says, is to convert http://localhost:8080/api/users to https://api.github.com/users. ChangeOrigin :true: indicates that the request source is changed. If the request source is not changed, an error will be reported.

After importing this component in the app.tsx file and placing it in the Home component, you can restart and see that the user list is displayed properly. src/App.tsx

.import Users from '@pages/Users';

const App = () = > {
	return(... <Users /> ); }Copy the code

Production Environment Configuration

Now we have a set of framework that can be used in the development environment, but our packaging strategy is different in the development environment and production environment. At this time, we extract the WebPack configuration file, put the common parts into the webpack.base. MJS file, and combine the configuration through webpack-merge. Change our existing file structure as follows

├ ─ ─ the config │ ├ ─ ─ webpack. Base. The MJS │ ├ ─ ─ webpack. Dev. MJS │ └ ─ ─ webpack. The PRD, MJS ├ ─ ─ package. The json ├ ─ ─ public │ └ ─ ─ Favicon. Ico ├─ SRC │ ├─ app.tsX │ ├─ index.css │ ├─ index.html │ ├─ index.js │ ├─ index.less │ ├─ index.scSS │ ├ ─ ─ pages │ │ ├ ─ ─ Home │ │ │ └ ─ ─ index. The TSX │ │ └ ─ ─ the Users │ │ └ ─ ─ index. The TSX │ └ ─ ─ the static │ └ ─ ─ img │ └ ─ ─ git. PNG ├ ─ ─ Json ├─ types │ ├─ static.txt ├─ dam.txtCopy the code

Installation required dependencies

yarn add webpack-merge -D 
Copy the code

The contents of the config/webpack.base. MJS file are as follows

import path from 'path';
import HtmlWebpackPlugin from 'html-webpack-plugin';

const config = {
    entry: './src/index.js'.output: {
        path: path.resolve(path.resolve(), './dist'),
        filename: 'bundle.js'
    },
    resolve: {
        extensions: ['.mjs'.'.js'.'.json'.".ts".".tsx"].alias: {
            '@pages': path.resolve(path.resolve(), './src/pages/'),
            '@components': path.resolve(path.resolve(), './src/components/'),
            '@static': path.resolve(path.resolve(), './src/static/'),}},module: {
        rules: [{test: /\.m? jsx? $/,
                exclude: /node_modules/,
                use: 'babel-loader'
            },
            {
                test: /\.tsx? $/,
                exclude: /node_modules/,
                use: 'ts-loader'
            },
            {
                test: /\.png/,
                type: 'asset/inline'}],},plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html'.inject: 'body'})]};export default config;
Copy the code

After modification, the contents of config/webpack.dev.mjs are as follows

import { merge } from 'webpack-merge';
import base  from './webpack.base.mjs';
import path from 'path';
import webpack from 'webpack';

export default merge(base, {
  mode: 'development'.devtool: 'eval-cheap-module-source-map'.devServer: {
    contentBase: path.join(path.resolve(), './public'),
    hotOnly: true.proxy: {
      '/api': {
        target: 'https://api.github.com/'.pathRewrite: { '^/api': ' ' },
        changeOrigin: true}}},resolve: {
    alias: {
      'react-dom': '@hot-loader/react-dom',}},module: {
    rules: [{test: /\.css$/i,
        use: ['style-loader'.'css-loader'],}, {test: /\.less$/,
        use: ['style-loader'.'css-loader'.'less-loader'],}, {test: /\.s[ac]ss$/i,
        use: [
          "style-loader"."css-loader"."sass-loader",],},]},plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('development')]}}))Copy the code

You can see here that we added devtool: ‘eval-cheap-module-source-map’, which helps us locate the source code if the application produces errors. I also included the CSS /less/sass loader in the development configuration above, because when the style file is too large, we can use another plugin, mini-css-extract-plugin, to extract the bundle.js file we are using.

The optimized config/webpack.prd. MJS content is as follows

import { CleanWebpackPlugin } from 'clean-webpack-plugin';
import { merge } from 'webpack-merge';
import CopyPlugin from "copy-webpack-plugin";
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
import base from './webpack.base.mjs';
import webpack from 'webpack';

export default merge(base, {
    mode: 'production'.devtool: false.module: {
        rules: [{test: /\.css$/i,
                use: [MiniCssExtractPlugin.loader, 'css-loader'],}, {test: /\.less$/,
                use: [MiniCssExtractPlugin.loader, 'css-loader'.'less-loader'],}, {test: /\.s[ac]ss$/i,
                use: [
                    MiniCssExtractPlugin.loader,
                    "css-loader"."sass-loader",],},],},optimization: {
        minimize: true.minimizer: [
            // For webpack@5 you can use the `... ` syntax to extend existing minimizers (i.e. `terser-webpack-plugin`), uncomment the next line
            `... `.new CssMinimizerPlugin(),
        ],
    },
    plugins: [
        new CleanWebpackPlugin(),
        new CopyPlugin({
            patterns: [{from: "public".to: "."}},]),new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify('production')}),new MiniCssExtractPlugin(),
    ],
})
Copy the code

The addition of several plug-ins, a brief introduction to the role.

  • clean-webpack-pluginEmpty it before each packingdistContents in the directory
  • copy-webpack-pluginIt’s time to packpublicCopy the contents of the directory todistdirectory
  • mini-css-extract-pluginbundle.jsTo extract the style file fromcssIn the file
  • css-minimizer-webpack-pluginTo extractcssThe file is compressed.

Change scripts in package.json

"start": "webpack serve --config config/webpack.dev.mjs"."build:prd": "webpack --config config/webpack.prd.mjs"
Copy the code

Install the dependencies and perform the build

yarn add clean-webpack-plugin -D
yarn add copy-webpack-plugin -D
yarn add mini-css-extract-plugin -D
yarn add css-minimizer-webpack-plugin -D
yarn build:prd
Copy the code

The image below shows the packaged content, and you can see that the bundle.js unzipped is 143K.

According to the need to load

As our project gets bigger and bigger, the bundle.js file size will also increase, so the loading speed of the page will be slow. In this case, we can introduce lazy load, which is to load the content of the page when we need a component. Let’s improve our project by using @loadable/ Component and introducing the React-router for effect

yarn add react-router react-router-dom @loadable/component 
yarn add @types/react-router @types/react-router-dom @types/loadable__component -D
Copy the code

Js from SRC/app.js to SRC/app.tsx. Currently, files with. TSX suffix are loaded on demand. This problem will be solved later. The SRC/app.js content is as follows

import { hot } from 'react-hot-loader/root';
import React from 'react';
import './index.css';
import './index.less';
import './index.scss';
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom';
import loadable from "@loadable/component";

const Loading = () = > {
  return <div>Loading...</div>
}

const Home = loadable(() = > import("@pages/Home"), {
  fallback: <Loading />
});
const Users = loadable(() = > import("@pages/Users"), {
  fallback: <Loading />
});

const App = () = > {
  return <div>
    <h1 className="css">Hello CSS</h1>
    <h1 className="less">Hello Less</h1>
    <h1 className="scss">Hello SCSS</h1>
    return <>
      <Router>
        <div>
          <nav>
            <ul>
              <li><Link to="/">Home</Link></li>
              <li><Link to="/users">Users</Link></li>
            </ul>
          </nav>
          <main>
            <input />
            <Switch>
              <Route path="/" exact>
                <Home />
              </Route>
              <Route path="/users">
                <Users />
              </Route>
            </Switch>
          </main>
        </div>
      </Router>
    </>
  </div>
}

export default hot(App);
Copy the code

Babelrc adding plugin

{
    "presets": [
        "@babel/preset-env"."@babel/preset-react"]."plugins": [
        "react-hot-loader/babel"."@babel/plugin-syntax-dynamic-import"]}Copy the code

performyarn build:prd, you can see that each route is shelled

At this point we can deploy the contents of the dist directory together with the server code.