⚠️ this article for nuggets community first contract article, not authorized to forbid reprint
preface
In the last article on enterprise CLI development, we have done a preliminary CLI for the process of building this piece, but we have only taken the first step towards the construction of the engineering system.
Developers are mostly developing business code, and it’s not enough to rely on the CLI to constrain devOps from the end, so small teams often start with scaffolding.
This article will use React as an example to customize a set of custom scaffolding and upgrade the previous CLI.
Custom React scaffolding
Scaffolding design is generally divided into two parts, one is the infrastructure and the other is the business architecture.
Infrastructure determines the technical selection of scaffolding, the selection of construction tools, and development optimization, construction optimization, environmental configuration, code constraints, submission specifications, etc.
Business architecture is for business module division, request encapsulation, permission design and so on more coupled with the business module design.
Build the infrastructure
As with the CLI, the scaffolding is built from 0, so the first step is to initialize the project and ts configuration.
npm init
tsx --init
Copy the code
Package. josn and tsconfig.json are generated as above. The configuration items of tsconfig.json can be directly used in the following configuration or redefined according to your needs.
{
"include": [
"src"]."compilerOptions": {
"module": "CommonJS"."target": "es2018"."outDir": "dist"."noEmit": true."jsx": "react-jsx"."esModuleInterop": true."moduleResolution": "node"."strict": true."noUnusedLocals": false."noFallthroughCasesInSwitch": true."baseUrl": ". /"."keyofStringsOnly": true."skipLibCheck": true."paths": {
"@ / *": [
"./src/*"]}}}Copy the code
The following are package.josn’s dependencies, along with some other configurations, attached as well. There are no separate descriptions for each dependency package here. If you have any questions about which module, please comment in the comments section.
{
"name": "react-tpl"."version": "1.0.0"."description": "a react tpl"."main": "index.js"."scripts": {
"test": "echo \"Error: no test specified\" && exit 1"."start": "cross-env NODE_ENV=development webpack-dev-server --config ./script/webpack.config.js",},"author": "cookieboty"."license": "ISC"."dependencies": {
"@babel/cli": "^ 7.14.5"."@babel/core": "^ 7.14.6"."@babel/preset-env": "^ 7.14.7"."@babel/preset-react": "^ 7.14.5"."@babel/preset-typescript": "^ 7.14.5"."babel-loader": "^ 8.2.2"."clean-webpack-plugin": "^ 4.0.0 - alpha."."cross-env": "^ 7.0.3." "."css-loader": "^ 6.1.0"."file-loader": "^ 6.2.0"."html-webpack-plugin": "^ 5.3.2." "."less": "^ 4.4.1"."less-loader": "^ 10.0.1." "."react": "^ 17.0.2"."react-dom": "^ 17.0.2"."style-loader": "^ 3.1.0"."typescript": "^ 4.3.5." "."webpack": "^ 5.45.1"."webpack-cli": "3.3.12"."webpack-dev-server": "^ 3.11.2"
},
"devDependencies": {
"@types/react": "^ 17.0.14"."@types/react-dom": "^ 17.0.9"}}Copy the code
Configuration webpack
Create script/webpack.config.js and copy the following configuration.
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
mode: "development".entry: "./src/index.tsx".devServer: {
contentBase: path.resolve(__dirname, "dist"),
hot: true.historyApiFallback: true.compress: true,},resolve: {
alias: {
The '@': path.resolve('src')},extensions: ['.ts'.'.tsx'.'.js'.'.json']},module: {
rules: [{test: /\.(js|jsx|ts|tsx)$/,
use: {
loader: require.resolve('babel-loader')},exclude: [/node_modules/],}, {test: /\.(css|less)$/,
use: [
{
loader: "style-loader"}, {loader: "css-loader".options: {
importLoaders: 1,},},],}, {test: /\.(png|svg|jpg|gif|jpeg)$/,
loader: 'file-loader'
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
loader: 'file-loader'}],},plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: 'tpl/index.html'})]};Copy the code
Note that webpack-CLI and Webpack-dev-server versions need to be consistent. Use version 3.0 for both webpack-CLI and Webpack-dev-server. If the version is not consistent, an error will be reported.
Configure React related
Create a TPL /index.html file (HTML template) and copy the following code
<! DOCTYPEhtml>
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
<div id="root"></div>
</body>
</html>
Copy the code
Create a SRC /index. TSX file (entry file) and copy the following code
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(
<App />.document.getElementById("root"));Copy the code
Create a.babelrc file (Babel parsing configuration) and copy the following code
{
"presets": [
"@babel/preset-env"."@babel/preset-react"["@babel/preset-typescript",
{
"isTSX": true."allExtensions": true}}]]Copy the code
After the preceding configurations and dependencies are installed, run Yarn Start. The following figure shows that the items can run properly
Open the browser to http://localhost:8081/ and you can see the written display page
So far, a preliminary scaffolding has been completed, but for the business, there are still many details to be completed. Next, we work together to further configure the project for the modules we would normally use for development.
Due to the length, this paper will not explain too much about configuration items of Webpack, Babel and React, but only provide a complete example, which can build a basic framework according to the steps. If students want to know more details, it is recommended to read the document directly after the construction is completed. Then configure the functions you want according to the documentation, thinking and doing more.
Optimized Webpck Dev configuration
Simplify server information output
As you can see from the previous picture, the output of Webpack-dev-server is very messy. You can use the Stats configuration field to filter the output.
Generally, we only need to see the error message. We can add the following parameters:
devServer: {
stats: 'errors-only'.// Filter information output
contentBase: path.resolve(__dirname, "dist"),
hot: true.historyApiFallback: true.compress: true,},Copy the code
Add build information output
ProgressPlugin can monitor the execution progress of each hook percentage and output the name and description of each hook.
It is also very simple to use, following the following reference, you can output the normal build progress as shown in the red icon.
const { ProgressPlugin } = require('webpack')
plugins: [...new ProgressPlugin(),
]
Copy the code
Optimize the business module
First, the project directory is divided, and the role and function of the files in each directory are agreed.
The specification here is not certain, it depends on each team’s own development specifications to customize, for example, some teams like to put public resources in the public directory, etc.
├ ─ ─ dist /// The default build output directory└ ─ ─ SRC /// The source directory├ ─ ─ assets /// Static resource directory├ ─ ─ the config ├ ─ ─ config. Js// Configure basic services related to the project├ ─ ─ the components /// Public component directory├ ─ ─ service /// Business request management├ ─ ─ store /// Shared store Management directory├ ─ ─ util /// The directory of utility functions├ ─ ─ pages /// Page directory├ ─ ─ the router /// Route configuration directory├ ─ ─. Index. The TSX// Rely on the main entry└ ─ ─ package. JsonCopy the code
Configure the routing
Convergent routing allows you to view an overview of the current project in a routing configuration file, facilitating maintenance and management. Of course, you can also use the reductic routing, that is, read the file name under pages, and automatically generate routes according to the file naming rules. However, I feel this constraint is not very convenient, and individuals are used to configuring their own routing rules.
First change the index. TSX entry file, code as follows:
import React from 'react'
import ReactDOM from 'react-dom'
import { HashRouter, Route, Switch } from 'react-router-dom'
import routerConfig from './router/index'
import './base.less'
ReactDOM.render(
<React.StrictMode>
<HashRouter>
<Switch>
{
routerConfig.routes.map((route) => {
return (
<Route key={route.path} {. route} / >)})}</Switch>
</HashRouter>
</React.StrictMode>.document.getElementById('root'))Copy the code
The router/index.ts file is configured with the following code:
import BlogsList from '@/pages/blogs/index'
import BlogsDetail from '@/pages/blogs/detail'
export default {
routes: [{exact: true.path: '/'.component: BlogsList },
{ exact: true.path: '/blogs/detail/:article_id'.component: BlogsDetail },
],
}
Copy the code
The Service management
Similar to convergent routing, convergent interfaces can also modify and manage these requests uniformly, and if any interface changes are reused, they can be handled from the source.
All project requests are put into the Service directory, and it is recommended that each module has a corresponding file management, as shown below:
import * as information from './information'
import * as base from './base'
export {
information,
base
}
Copy the code
This makes it easier to manage requests, and base-.ts, as a business request class, can handle some business-specific processing here.
import { request } from '.. /until/request'
const prefix = '/api'
export const getAllInfoGzip = () = > {
return request({
url: `${prefix}/apis/random`.method: 'GET'})}Copy the code
As a unified request method, util/request can be replaced by fetch, AXIos, and other request libraries. At the same time, common interception logic can be encapsulated in this method.
import qs from 'qs'
import axios from "axios";
interface IRequest {
url: stringparams? : SVGForeignObjectElement query? :objectheader? :objectmethod? :"POST" | "OPTIONS" | "GET" | "HEAD" | "PUT" | "DELETE" | undefined
}
interface IResponse {
count: number
errorMsg: string
classify: string
data: anydetail? :anyimg? :object
}
export const request = ({ url, params, query, header, method = 'POST' }: IRequest): Promise<IResponse> => {
return new Promise((resolve, reject) = > {
axios(query ? `${url}/?${qs.stringify(query)}` : url, {
data: params,
headers: header,
method: method,
})
.then(res= > {
resolve(res.data)
})
.catch(error= > {
reject(error)
})
})
}
Copy the code
For generic interception, refer to the AXIOS configuration or rewrite it yourself to suit your business needs.
In specific service development, you can import the interface module by the module name to easily find the corresponding interface module.
import { information } from "@/service/index";
const { data } = await information.getAllInfoGzip({ id });
Copy the code
This set of rules can also be applied to store, router, utils and other places where modules can be disassembled, which is conducive to project maintenance.
The above is some configuration and agreement of business development for the project, which can be modified according to the rules and preferences of your team.
CLI upgrade
After the custom React scaffolding is built, we will not be able to build the project successfully if we directly use the CLI built in the previous article. If you remember the CLI entry file is SRC /index.js. The HTML template uses public/index.html.
Obviously, the CLI is far from being up to par. We can’t update the CLI every time we develop it, which is against the principle of universality of the CLI.
So how do you solve this problem?
Custom configuration files
Create a cli.config.json file in the root directory. This file will be the one that needs to read the configuration.
Write the self-defined configuration of this project to a file for CLI to read.
{
"entry": {
"app": "./src/index.tsx"
},
"output": {
"filename": "build.js"."path": "./dist"
},
"template": "tpl/index.html"
}
Copy the code
CLI synchronous transformation, the code is as follows:
require('module-alias/register')
import webpack from 'webpack';
import { getCwdPath, loggerTiming, loggerError } from '@/util'
import { loadFile } from '@/util/file'
import { getProConfig } from './webpack.pro.config'
import ora from "ora";
export const buildWebpack = () = > {
const spinner = ora('Webpack building... ')
const rewriteConfig = loadFile(getCwdPath('./cli.config.json')) // Read the scaffold configuration file
const compiler = webpack(getProConfig(rewriteConfig));
return new Promise((resolve, reject) = > {
loggerTiming('WEBPACK BUILD');
spinner.start();
compiler.run((err: any, stats: any) = > {
console.log(err)
if (err) {
if(! err.message) { spinner.fail('WEBPACK BUILD FAILED! ');
loggerError(err);
returnreject(err); }}}); spinner.succeed('WEBPACK BUILD Successful! ');
loggerTiming('WEBPACK BUILD'.false); })}Copy the code
Webpack.pro.config. ts code:
import getBaseConfig from './webpack.base.config'
import { getCwdPath, } from '@/util'
interface IWebpackConfig {
entry: {
app: string
}
output: {
filename: string.path: string
}
template: string
}
export const getProConfig = (config: IWebpackConfig) = > {
const { entry: { app }, template, output: { filename, path }, ... rest } = configreturn {
...getBaseConfig({
mode: 'production'.entry: {
app: getCwdPath(app || './src/index.js')},output: {
filename: filename || 'build.js'.path: getCwdPath(path || './dist'), // The output path after packaging
},
template: getCwdPath(template || 'public/index.html')}),... rest } }Copy the code
Through the loadFile function, read the customized configuration items of scaffolding, replace the initial values, and then carry out project construction. The construction results are as follows:
This custom configuration is preliminary and can be customized later to add more, such as custom Babel plug-ins, Webpack plug-ins, public paths, reverse proxy requests, and so on.
Taking over the DEV process
Similar to the take over build process, we can take over the dev process of the project after we have built our custom scaffolding, avoiding build failure due to different dependencies between development and build, and managing the specification and quality of the project from the source.
The Webpack-dev-server configured in the previous scaffolding is used based on the Webpack-CLI.
Since you are using CLI to take over the dev environment, you do not need to use webpack-dev-server as a plug-in for WebPack. Instead, you directly call webpack-dev-server’s Node Api.
Remove the webpack-dev-server configuration from the scaffold and put it into the CLI.
const WebpackDevServer = require('webpack-dev-server/lib/Server')
export const devWebpack = () = > {
const spinner = ora('Webpack running dev ... ')
const rewriteConfig = loadFile(getCwdPath('./cli.config.json'))
const webpackConfig = getDevConfig(rewriteConfig)
const compiler = webpack(webpackConfig);
const devServerOptions = {
contentBase: 'dist'.hot: true.historyApiFallback: true.compress: true.open: true
};
const server = new WebpackDevServer(compiler, devServerOptions);
server.listen(8000.'127.0.0.1'.() = > {
console.log('Starting server on http://localhost:8000');
});
}
Copy the code
Then add the corresponding command to the scaffold package.json scripts to complete the takeover of the dev environment, as follows:
"scripts": {
"dev": "cross-env NODE_ENV=development fe-cli webpack"."build": "cross-env NODE_ENV=production fe-cli webpack"
}
Copy the code
Run the corresponding command to run or package the current scaffold content.
Optimize webPack build configuration
As described in the last article, the current build product results are clearly not what we wanted and do not conform to the normal project specifications, so the configuration of the build needs to be optimized.
mini-css-extract-plugin
Mini-css-extract-plugin is a style extract-plugin that separates CSS and packages it into a separate file. It creates a CSS file for each JS file that contains CSS. On-demand loading of CSS and sourceMaps is also supported. The configuration code is as follows:
{
rules: [
test: /\.(css|less)$/,
use: [MiniCssExtractPlugin.loader],
}
]
}
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css'.chunkFilename: '[id].[contenthash].css'.ignoreOrder: true,})]Copy the code
Extracting common modules
Using the splitChunks function provided by WebPack, we can extract the common module of node_modules and add the following configuration to the WebPack configuration item.
optimization: {
splitChunks: {
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors'.chunks: 'all',},},},},Copy the code
As shown in the figure, now the product is built is not instantly clearer.
Optimize the build product path
The above build artifacts have been optimized, but the catalog is still not clear enough. We can compare the crA build artifacts in the following figure and then optimize the reference path.
Simply add static/js to the front of all the build artifacts, and you’ll get something like the figure below.
Configuring incremental builds (persistent caching)
This is a new feature of WebPack 5. In WebPack 4, we used the hard-source-webpack-plugin to cache module dependencies and read the cache directly during the second build to speed up the build.
In WebPack 5, this process is replaced by cache. The official built-in persistent cache function is very easy to configure. Add the following code:
import { getCwdPath } from '@/util'
export default {
cache: {
type: 'filesystem'.// 'memory' | 'filesystem'
cacheDirectory: getCwdPath('./temp_cache'), // By default, the cache is stored in the current running path /.cache/webpack
// Cache dependencies. When a cache dependency changes, the cache becomes invalid
buildDependencies: {
// Add your configuration to a dependency and change the configuration to invalidate the cache
config: [__filename]
},
allowCollectingMemory: true.profile: true,}}Copy the code
Then, when a build or development is run, the following cache files are produced in the current run directory:
Now let’s take a look at the build speed improvement:
As you can see, the first build was about 2 seconds slower than the previous build, but the second build was significantly faster, since the scaffold was so small at the moment, the first build using increments required more caching than a normal build.
One thing to note here is that since we are calling WebPack’s Node Api to build, we need to display the compiler off to properly produce the cache file.
const compiler = webpack(webpackConfig);
try {
compiler.run((err: any, stats: any) = > {
if (err) {
loggerError(err);
} else {
loggerSuccess('WEBPACK SUCCESS! ');
}
compiler.close(() = > {
loggerInfo('WEBPACK GENERATE CACHE'); // The display call compiler is closed and the cache is generated
});
loggerTiming('WEBPACK BUILD'.false);
});
} catch (error) {
loggerError(error)
}
Copy the code
Interested students can try the dev environment, the boot speed will be shortened to second on level.
Special thanks to
This is the reader comment of the last post, here @saitu, thank you for your suggestion. In the following series of blog posts, besides introducing the idea, coding and steps will be more detailed, and we will provide project demo for reference in time. Other students’ better suggestions can also be feedback in the comment area. I hope that in addition to finishing this series, I can write better, so that I can learn from each other and grow up together with more classmates.
Write in the last
So far, the CLI tool has a working prototype, but as an enterprise-level CLI goal, we still have a long way to go. There are a lot of optimizations just to build, including but not limited to build configuration constraints, extension constraints, commit constraints, and so on.
All the project codes have been uploaded to the project address. Students who are interested can pull them for reference. The related codes of all subsequent columns will be put in BOTY DESIGN.