[toc]
Why configure WebPack yourself
At present, there are many front-end packaging solutions based on Webpack provided by the community, such as UMI create-React-app. However, after checking the corresponding documents, the MPA(multi-page application) they can configure is not what I want.
Since many integrated packaging solutions are based on WebPack, and we know that after Webpack 4, the configuration is much simplified, and the construction speed is faster, why not configure a set of MPA packaging solutions for WebPack according to your own needs? Once you know webpack, you’ll be more comfortable using a secondary wrapper like UMI.
This article is for documentation purposes only, and the code that appears in this article is hosted at frmachao/webpack-examples
Site directory structure
Back-end engineering and front-end engineering in the same project, the front-end compiles TS/JSX files and presents them to the server
This may not be a good way
|--
|--build
|--build.js Nodebuild /build.js all --build
|--rules.js #
|--utils.js
|--webpack.common.js
|--webpack.dev.js
|--webpack.prod.js
|--dist # Front-end packaged resources have no 'HTML' file, 'HTML' to the server to process
|--mpa
|--page1
|--index.js
|--page2
|--index.js
|--verdor.js
|--spa1
|--xxx
|--server # Website backend
|--dist # Back-end build
|--src
|--middlewares
|--controllers
|--models
|--app.ts
|--public
|--view
|--site # Website front-end
|--mpa # Multipage app resource directory
|--page1
|--index.tsx
|--page2
|--spa1 # Single page apps
|--spa2
|--xxxx
Copy the code
Problems encountered
CleanWebpackPlugin is not a constructor
- Change the reference mode to
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
- Change the reference mode to
- The new version of the CleanWebpackPlugin can’t clear the dist directory for some reason, so for now use the old version
- The CSS Module conflicts with antD
- To solve
- Eslint conflicts with path aliases
- To solve
- ** ESLint does not take effect when there are two folders in the repository root **
Some plug-ins added by myself
When I want to build all my multi-page applications using NPM run watch-mpa to perform development, I need to easily get the parameters on the command line
"build": "node build/build.js all --build",
"build-mpa": "node build/build.js site/mpa --build",
"watch-mpa": "node build/build.js site/mpa --watch",
Copy the code
Dotenv is used to load environment variables from files. Environment variables are used to dynamically load parameters while the program is running. In addition to environment variables, we can also specify command line parameters directly when starting node.js:
node index.js --beep=boop -t -z 12 -n5 foo bar
console.log(process.argv);
// ['/bin/node', '/tmp/index.js', '--beep=boop', '-t', '-z', '12', '-n5', 'foo', 'bar']
Copy the code
The process.argv variable is an array. The first two elements of the array are the node program location and the JS script location
Although the list of startup parameters is available from process.argv, we still need to parse the parameters further.
Minimist Minimist is a library dedicated to handling node.js startup parameters that converts the parameter list in process.argv into an easier to use format:
node index.js --beep=boop -t -z 12 -n5 foo bar
const argv = require('minimist')(process.argv.slice(2));
console.dir(argv);
// { _: [ 'foo', 'bar' ], beep: 'boop', t: true, z: 12, n: 5 }
Copy the code
Configure typescript support
Babel 7 @babel/preset-typescript + babel-loader
Advantages:
- Compile speed blocks using plugins from the Babel ecosystem such as ANTD’s babel-plugin-import(the original fuse-box configuration was abandoned by it).
Disadvantages:
- There are four syntax types that cannot be compiled in Babel
- The Namespace syntax is not recommended. Use the standard ES6 module (import/export) instead.
- X syntax conversion type is not supported. Use x as newtype instead.
- Const enumeration
- Legacy import/export syntax. For example: import foo = require(…) And export = foo.
Rules 1 and 4 are not used and are outdated.
This syntax will directly indicate that the syntax error, very easy to change, no problem.
The third defect is gone.
- There is no type checking
VSCode comes with TS detection, so this is not a problem for individual projects
Problems encountered in use:
- Ts cannot use JSX unless the “– JSX “flag is provided
- Define the tsConfig file in which you declare support for JSX. Note that we are not using TSC for compilation, just for the IDE to hint at development time
- When Webpack compiles TS packages using @babel/preset-typescript, the path to the alias is not found
- Automatically resolve the extension to determine this crater
Reference:
- TypeScript and Babel: A beautiful marriage
- Babel-js decorator syntax support
- Replace awl-typescript-loader and TS-loader with @babel/preset-typescript
Babel configuration
Current version of Babel: “@babel/core”: “^7.9.0”,
Be sure to see the documentation for the version you are using!! babel-preset-env
- Use Babel for:
- Use the latest TS/JS syntax, even some features in the proposal stage
- More versions of Babel are available after translation
For the Babel configuration, I was mostly stuck in the useBuiltIns option of @babel/preset-env and the @babel/ plugin-transform-Runtime plugin. Since Babel only interprets the syntax and does not handle the new API when translating higher versions of JS code, see the following example:
// src/index.js
const add = (a, b) = > a + b
const arr = [1.2]
const hasThreee = arr.includes(3)
new Promise()[object Object]
Copy the code
After Babel to perform
// dist/index.js
"use strict";
var add = function add(a, b) {
return a + b;
};
var arr = [1.2];
var hasThreee = arr.includes(3);
new Promise(a);Copy the code
Babel divides ES6 standards into syntax and built-in types. Syntax is syntax, and types such as const and => that Babel translates by default are syntax types. Those that can be overridden by rewriting are considered built-in, such as includes and promises. By default, Babel only interprets syntax, and @babel/polyfill for built-in types. The @Babel/Polyfill implementation is also simple enough to override any built-in additions to ES6. The schematic diagram is as follows:
Object.defineProperty(Array.prototype, 'includes'.function(){... })Copy the code
Babel scrapped @babel/polyfill in 7.4 and replaced it with core-js; Built-in injection is controlled by the useBuiltIns parameter in @babel/preset-env; UseBuiltIns This option configures how @babel/preset-env will handle polyfills. It can be set to ‘entry’, ‘Usage’, and false. The default value is false and no spacer is injected.
- For ‘Entry’, you just need to import core-JS at the entry point of the entire project.
- With ‘usage’, instead of importing core-js at the entry point of the project, Babel will optionally inject the appropriate implementation based on the built-in usage during source compilation
// src/index.js
const add = (a, b) = > a + b
const arr = [1.2]
const hasThreee = arr.includes(3)
new Promise(a)// dist/index.js
"use strict";
require("core-js/modules/es6.promise");
require("core-js/modules/es6.object.to-string");
require("core-js/modules/es7.array.includes");
var add = function add(a, b) {
return a + b;
};
var arr = [1.2];
var hasThreee = arr.includes(3);
new Promise(a);Copy the code
Before introducing the @babel/ plugin-transform-Runtime plugin, here’s an example:
// src/index.js
const add = (a, b) = > a + b
const arr = [1.2]
const hasThreee = arr.includes(3)
new Promise(resolve= >resolve(10))
class Person {
static a = 1;
static b;
name = 'morrain';
age = 18
}
// dist/index.js
"use strict";
require("core-js/modules/es.array.includes");
require("core-js/modules/es.object.define-property");
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
function _classCallCheck(instance, Constructor) { if(! (instanceinstanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); }}function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true.configurable: true.writable: true }); } else { obj[key] = value; } return obj; }
var add = function add(a, b) {
return a + b;
};
var arr = [1.2];
var hasThreee = arr.includes(3);
new Promise(function (resolve) {
return resolve(10);
});
var Person = function Person() {
_classCallCheck(this, Person);
_defineProperty(this."name".'morrain');
_defineProperty(this."age".18);
};
_defineProperty(Person, "a".1);
_defineProperty(Person, "b".void 0);
Copy the code
During compilation, the built-in syntax is polyfill compatible with require(“core-js/modules/ XXXX “), For syntax types, compatibility is implemented by injecting helper functions like _classCallCheck and _defineProperty into the current module during translation. This may be fine for one module, but for many modules in a project, injecting these helper functions into each module will inevitably result in a large amount of code.
The @babel/ plugin-transform-Runtime is designed to reuse these helper functions and reduce the size of the code. Of course, it also provides a sandbox environment for compiled code, avoiding global contamination.
- @ babel/runtime + @babel/plugin-transform-runtime
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime
Copy the code
@babel/ plugin-transform-Runtime is a compile-time dependency that is installed as a development dependency, while @babel/ Runtime is a collection of helper functions that need to be imported into compiled code, so it is installed as a production dependency
// Modify the Babel configuration
const presets = [
[
'@babel/env',
{
debug: true.useBuiltIns: 'usage'.corejs: 3.targets: {}}]]const plugins = [
'@babel/plugin-proposal-class-properties'['@babel/plugin-transform-runtime']]Copy the code
- The previous example, compiled once again, you can see, before the helper function, all become similar to the require (” @ Babel/runtime/helpers/classCallCheck “) implementation.
// dist/index.js
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
require("core-js/modules/es.array.includes");
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var add = function add(a, b) {
return a + b;
};
var arr = [1.2];
var hasThreee = arr.includes(3);
new Promise(function (resolve) {
return resolve(10);
});
var Person = function Person() {(0, _classCallCheck2["default"]) (this, Person);
(0, _defineProperty2["default"]) (this."name".'morrain');
(0, _defineProperty2["default"]) (this."age".18);
};
(0, _defineProperty2["default"])(Person, "a".1);
(0, _defineProperty2["default"])(Person, "b".void 0);
Copy the code
Babel configuration in the final project:
{
test: /\.(js|jsx|tsx)$/.// include: [path.join(__dirname, '../site')],
exclude: /(node_modules|bower_components)/,
use: {
loader: "babel-loader".options: {
cacheDirectory: true.// Presets are executed in reverse order
presets: [["@babel/preset-env",
process.env.NODE_ENV === "production"
? {
targets: {
// Browser share >5% The latest two versions of Firefox below the latest version of Internet Explorer 9
browsers: [
"5%" >."last 2 versions"."Firefox ESR"."not ie <= 9",]},useBuiltIns: "usage".corejs: { version: 3.proposals: true}} : {targets: { browsers: ["last 2 Chrome versions"]},// Only build for the latest two versions of Chrome to make the development environment build faster},]."@babel/preset-react"["@babel/preset-typescript"]],plugins: [
// Enable support for experimental ES proposals, forget why you configured this in the first place
["@babel/plugin-proposal-decorators", { "legacy": true}].// Decorator support
["@babel/plugin-proposal-class-properties", { "loose": true}].// Class internal static attribute support
// Configure ANTD Desgin to be loaded on demand
[
"import",
{
libraryName: "antd".libraryDirectory: "es".style: "css".// 'style: true' will load less files},].// Avoid duplicate code in compiled output and avoid global variable contamination
"@babel/plugin-transform-runtime".// Lazy loading front-end routes are loaded on demand
"@babel/plugin-syntax-dynamic-import"],}}},Copy the code
Reference:
- Front science series (4) : Babel — The Babel that sends ES6 to the sky
Use ESLint+Prettier specifications for React+Typescript projects
Reference:
- How to gracefully use ESLint and Prettier in Typescript projects
What is the use of include/exclude in Webpack loaders?
In fact, it is written in the official documentation, even at the beginning of the configuration, alas to look at things to be patient ah, the complete reading of the document is really a basic requirement
rules: [
// Module rules (configures loader, parser, etc.)
{
test: /\.jsx? $/,
include: [
path.resolve(__dirname, "app")].exclude: [
path.resolve(__dirname, "app/demo-files")].// Here is the matching condition, each option accepts a regular expression or string
// Test and include have the same function in that they must match options
// exclude is a mandatory mismatch option (prior to test and include)
// Best practices:
// - Use regular expressions only for test and file name matches
// - Use an absolute path array in include and exclude
// - Avoid exclude in favor of include
issuer: { test, include, exclude },
// Issuer condition (import source)
enforce: "pre".enforce: "post".// flag that these rules apply, even if rules override (advanced option)
loader: "babel-loader".// The loader that should be applied, which resolves relative to context
For clarity, the '-loader' suffix is no longer optional in WebPack 2
// View the WebPack 1 upgrade guide.
options: {
presets: ["es2015"]},// Loader is optional},]Copy the code
Webpack document
Webpack package optimization
RuntimeChunk? I don’t know if it really needs it
The build script./build/build.js
const cp = require('child_process');
const path = require('path');
const fs = require('fs');
const { getEntry } = require('./utils')
const buildRoot = path.join(__dirname)
const projectRoot = path.join(buildRoot, '.. ')
const distRoot = path.join(projectRoot, '/dist')
/ * * * developing output dist directory to the server using the build. The js/site/map | spa1 | all] - watch * build production node build. Js all - build * /
const argv = require('minimist')(process.argv.slice(2));
let hasWatch = argv.watch ? '--watch' : ' '
isBuild = argv.build ? `${buildRoot}/webpack.prod.js` : `${buildRoot}/webpack.dev.js`
fileName = argv._[0];
isHashName = argv.build ? '[name][hash]' : '[name]'
if (argv.build) {
process.env.NODE_ENV = 'production'
}
if (fileName === 'all') {
fs.readdirSync(`${projectRoot}/site`).forEach(file= > {
const blockFiles = ['.DS_Store'.'common'.'mpa']
if (blockFiles.indexOf(file) === -1) {
buildSPA(file)
}
});
buildMPA()
return false;
}
if (fileName === 'site/mpa') {
buildMPA()
return false;
}
buildSPA(fileName)
function buildMPA() {
let ENTRY_FILESNAME = {};
OUT_PUT_STR = `${distRoot}/mpa/${isHashName}.js --output-public-path=/fe-static/mpa/`;
if(! fs.readdirSync(`${projectRoot}/site/mpa`)) {
console.log('Err: There is no such project. ');
return false;
}
// Go through the folder directory to get the multi-page folder name
// getEntry(): handle the key-value pair
=
to dynamically generate
// End up like this: datum/index=/Users/ma/DEV/webSite/site/mpa/datum/index.tsx game/index=/Users/ma/DEV/webSite/site/mpa/game/index.tsx
fs.readdirSync(`${projectRoot}/site/mpa`).forEach(file= > {
const blockFiles = ['.DS_Store']
if (blockFiles.indexOf(file) === -1) {
ENTRY_FILESNAME[`${file}/index`] = (`${projectRoot}/site/mpa/${file}/index.tsx`); }}); buildOne(getEntry(ENTRY_FILESNAME), OUT_PUT_STR); }function buildSPA(name) {
console.log('buildSPA===', name)
let ENTRY_FILESNAME = {};
let OUT_PUT_STR = `${distRoot}/${name}/${isHashName}.js --output-public-path=/fe-static/${name}/ `;
if(! fs.readdirSync(`${projectRoot}/site`).find(file= > file === name)) {
console.log(`err!!!!!!!! :${projectRoot}/ Site is no such project. `);
return false;
}
ENTRY_FILESNAME[`index`] = (`${projectRoot}/site/${name}/index.tsx`);
buildOne(getEntry(ENTRY_FILESNAME), OUT_PUT_STR);
}
function buildOne(ENTRY_FILESNAME_STR, OUT_PUT_STR) {
console.log('ENTRY_FILESNAME_STR===========>', ENTRY_FILESNAME_STR)
// -o
const cpBuild = cp.exec(
`npx webpack ${hasWatch} ${ENTRY_FILESNAME_STR} --config ${isBuild} -o ${OUT_PUT_STR}`.(error, stdout, stderr) = > {
if (error) {
console.error('error:', error);
}
}
)
cpBuild.stdout.on('data'.(data) = > {
console.log('on stdout: ' + data);
});
cpBuild.stderr.on('on data'.(data) = > {
if (data) {
console.log('on stderr: '+ data); }}); }Copy the code
How to build front-end engineering
Developing output dist directory to the server using the node build. Js [` site/map ` | ` spa1 ` | ` all `] - watch build production node build. Js all - build 'site/mpa' Is the convention multi-page project directoryCopy the code