实现一个loader

WEEBPACK LOADER

简单loader

配置webpack.config,webpace5提供默认的entryoutput所以无需配置

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
module:{
rules:[{
test:/\.js$/,
use:['my-loader','my-loader1','my-loader2']
}]
},
mode:'production',
// webpack5 提供resolveLoader可以配置loader的查找目录
resolveLoader: {
modules: ['node_modules',path.resolve(__dirname,'loader')],
},
}

实现loader loader/my-loader loader/my-loader1 loader/my-loader2

1
2
3
4
5
6
7
8
9
10
11
// 同步方法在loader解析时执行
// 也就是use数组中的loader从右向左执行
module.exports = function(content){
console.log(111);
return content;
}
// pitch 方法在loader加载时执行
// 也就是use数组中的loader从左向右执行
module.exports.pitch = function(){
console.log('p111')
}

同步loader和异步loader

同步写法

1
2
3
module.exports = function(content){
this.callback(null,content);
}

异步写法,仍然按顺序依次执行,不会改变loader执行顺序

1
2
3
4
5
6
module.exports = function(content){
var callback = this.async();
setTimeout(()=>{
callback(null,content)
},3000)
}

获取loader的options

为loader添加options

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
module.exports = {
module:{
rules:[{
test:/\.js$/,
use:[{
loader:'my-loader',
options:{
test:'1231'
}
},'my-loader1','my-loader2']
}]
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const  { getOptions }  = require('loader-utils');
const { validate } = require('schema-utils');

const schema = {
type: 'object',
properties: {
test: {
type: 'string',
}
}
};

module.exports = function(content){
const options = getOptions(this);

console.log(options);
validate(schema, options, {
// 需要校验的loader名称
name: 'my-loader',
baseDataPath: 'options',
});

this.callback(null,content);
}

实现一个loader

添加webpack.config.json配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const path = require('path');

module.exports = {
module:{
rules:[{
test:/\.js$/,
loader:'babelLoader',
options:{
"presets": ["@babel/preset-env"]
}
}]
},
mode:'production',
resolveLoader: {
modules: ['node_modules',path.resolve(__dirname,'loader')],
},
}

loader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const  { getOptions }  = require('loader-utils');
const { validate } = require('schema-utils');
const {promisify} = require('util')
const babel = require('@babel/core');

const schema = {
type: 'object',
properties: {
persets:{
type:'array'
}
}
};

module.exports = function(content){
const options = getOptions(this);
const callback = this.async();

validate(schema, options, {
// 需要校验的loader名称
name: 'babelLoader',
baseDataPath: 'options',
});

console.log(options);


const transfrom = promisify(babel.transform);
transfrom(content,options)
.then(({code}) =>callback(null,code))
}

create-react-app 脚手架分析

package.json

1
2
3
4
5
6
7
8
{
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject" // 弹出配置文件,操作不可逆
}
}

弹出配置之后自动修改package.json 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
"scripts": {
"start": "node scripts/start.js",
"build": "node scripts/build.js",
"test": "node scripts/test.js"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"babel": {
"presets": [
"react-app"
]
}dd
}

paths 解析路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61


const path = require('path');
const fs = require('fs');
const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath');

// Make sure any symlinks in the project folder are resolved:
// https://github.com/facebook/create-react-app/issues/637

// process.cwd() 当前Node.js进程执行时的工作目录 /home/supreme/Dropbox/Workspace/cra-learn/config
// 项目根路径
const appDirectory = fs.realpathSync(process.cwd());
//path.resolve() 方法将路径或路径片段的序列解析为绝对路径。
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);

// We use `PUBLIC_URL` environment variable or "homepage" field to infer
// "public path" at which the app is served.
// webpack needs to know it to put the right <script> hrefs into HTML even in
// single-page apps that may serve index.html for nested URLs like /todos/42.
// We can't use a relative path in HTML because we don't want to load something
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.

// 分析publicUrl路径 默认是"/"
const publicUrlOrPath = getPublicUrlOrPath(
process.env.NODE_ENV === 'development',
require(resolveApp('package.json')).homepage,
process.env.PUBLIC_URL
);

// 打包路径
const buildPath = process.env.BUILD_PATH || 'build';

// 文件扩展名
const moduleFileExtensions = [
'web.mjs',
'mjs',
'web.js',
'js',
'web.ts',
'ts',
'web.tsx',
'tsx',
'json',
'web.jsx',
'jsx',
];

// Resolve file paths in the same order as webpack
// appIndexJs: resolveModule(resolveApp, 'src/index')
const resolveModule = (resolveFn, filePath) => {
// 判断存不存在有这样扩展名的文件
const extension = moduleFileExtensions.find(extension =>
fs.existsSync(resolveFn(`${filePath}.${extension}`))
);
// 返回文件的绝对路径
if (extension) {
return resolveFn(`${filePath}.${extension}`);
}
// 默认返回js文件
return resolveFn(`${filePath}.js`);
};

start.js启动文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
// Do this as the first thing so that any code reading it knows the right env.
// 声明环境变量
process.env.BABEL_ENV = 'development';
process.env.NODE_ENV = 'development';

// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
throw err;
});

// Ensure environment variables are read.
// 引入env配置文件
require('../config/env');


const fs = require('fs');
const chalk = require('react-dev-utils/chalk');
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const clearConsole = require('react-dev-utils/clearConsole');
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
const {
choosePort,
createCompiler,
prepareProxy,
prepareUrls,
} = require('react-dev-utils/WebpackDevServerUtils');
const openBrowser = require('react-dev-utils/openBrowser');
const semver = require('semver');
const paths = require('../config/paths');
const configFactory = require('../config/webpack.config');
const createDevServerConfig = require('../config/webpackDevServer.config');
const getClientEnvironment = require('../config/env');
const react = require(require.resolve('react', { paths: [paths.appPath] }));

const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
const useYarn = fs.existsSync(paths.yarnLockFile);
const isInteractive = process.stdout.isTTY;

// Warn and crash if required files are missing
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
process.exit(1);
}

// Tools like Cloud9 rely on this.
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
const HOST = process.env.HOST || '0.0.0.0';

if (process.env.HOST) {
console.log(
chalk.cyan(
`Attempting to bind to HOST environment variable: ${chalk.yellow(
chalk.bold(process.env.HOST)
)}`
)
);
console.log(
`If this was unintentional, check that you haven't mistakenly set it in your shell.`
);
console.log(
`Learn more here: ${chalk.yellow('https://cra.link/advanced-config')}`
);
console.log();
}

// We require that you explicitly set browsers and do not fall back to
// browserslist defaults.
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
// 选择端口号,当端口冲突的时候回自动加一
checkBrowsers(paths.appPath, isInteractive)
.then(() => {
// We attempt to use the default port but if it is busy, we offer the user to
// run on a different port. `choosePort()` Promise resolves to the next free port.
return choosePort(HOST, DEFAULT_PORT);
})
.then(port => {
if (port == null) {
// We have not found a port.
return;
}

// 核心文件通过configFactory创建
const config = configFactory('development');
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
const appName = require(paths.appPackageJson).name;
// 是否存在ts配置文件,判断是否启用TS
const useTypeScript = fs.existsSync(paths.appTsConfig);
const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
const urls = prepareUrls(
protocol,
HOST,
port,
paths.publicUrlOrPath.slice(0, -1)
);

const devSocket = {
warnings: warnings =>
devServer.sockWrite(devServer.sockets, 'warnings', warnings),
errors: errors =>
devServer.sockWrite(devServer.sockets, 'errors', errors),
};
// Create a webpack compiler that is configured with custom messages.
// 创建webpack编译器
const compiler = createCompiler({
appName,
config,
devSocket,
urls,
useYarn,
useTypeScript,
tscCompileOnError,
webpack,
});
// Load proxy config
// 创建代理配置信息
const proxySetting = require(paths.appPackageJson).proxy;
const proxyConfig = prepareProxy(
proxySetting,
paths.appPublic,
paths.publicUrlOrPath
);
// Serve webpack assets generated by the compiler over a web server.
// 创建server服务器配置
const serverConfig = createDevServerConfig(
proxyConfig,
urls.lanUrlForConfig
);
// 创建服务器并监听端口
const devServer = new WebpackDevServer(compiler, serverConfig);
// Launch WebpackDevServer.
devServer.listen(port, HOST, err => {
if (err) {
return console.log(err);
}
if (isInteractive) {
clearConsole();
}

if (env.raw.FAST_REFRESH && semver.lt(react.version, '16.10.0')) {
console.log(
chalk.yellow(
`Fast Refresh requires React 16.10 or higher. You are using React ${react.version}.`
)
);
}

console.log(chalk.cyan('Starting the development server...\n'));
openBrowser(urls.localUrlForBrowser);
});

['SIGINT', 'SIGTERM'].forEach(function (sig) {
process.on(sig, function () {
devServer.close();
process.exit();
});
});

if (process.env.CI !== 'true') {
// Gracefully exit when stdin ends
process.stdin.on('end', function () {
devServer.close();
process.exit();
});
}
})

// 如果操作过程中有报错退出进程
.catch(err => {
if (err && err.message) {
console.log(err.message);
}
process.exit(1);
});

config.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772

// Source maps are resource heavy and can cause out of memory issue for large source files.
// 是否会生成sourceMap文件
// 安装 cross-env 在package.json scripts指定命令中添加 GENERATE_SOURCEMAP=false
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';

const webpackDevClientEntry = require.resolve(
'react-dev-utils/webpackHotDevClient'
);
const reactRefreshOverlayEntry = require.resolve(
'react-dev-utils/refreshOverlayInterop'
);

// Some apps do not need the benefits of saving a web request, so not inlining the chunk
// makes for a smoother build process.
// 是否需要吧runtime文件内联在打包的js中
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';

const emitErrorsAsWarnings = process.env.ESLINT_NO_DEV_ERRORS === 'true';
const disableESLintPlugin = process.env.DISABLE_ESLINT_PLUGIN === 'true';

// 最小转换成base64图片的大小
const imageInlineSizeLimit = parseInt(
process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'
);

// Check if TypeScript is setup
const useTypeScript = fs.existsSync(paths.appTsConfig);

// Get the path to the uncompiled service worker (if it exists).
const swSrc = paths.swSrc;

// style files regexes
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;

const hasJsxRuntime = (() => {
if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') {
return false;
}

try {
require.resolve('react/jsx-runtime');
return true;
} catch (e) {
return false;
}
})();

// This is the production and development configuration.
// It is focused on developer experience, fast rebuilds, and a minimal bundle.
// 最终返回的开发或生产环境的函数
module.exports = function (webpackEnv) {
const isEnvDevelopment = webpackEnv === 'development';
const isEnvProduction = webpackEnv === 'production';

// Variable used for enabling profiling in Production
// passed into alias object. Uses a flag if passed into the build command
const isEnvProductionProfile =
isEnvProduction && process.argv.includes('--profile');

// We will provide `paths.publicUrlOrPath` to our app
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
// Get environment variables to inject into our app.

// 获取环境变量的方法 必须以 REACT_APP开头
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));

const shouldUseReactRefresh = env.raw.FAST_REFRESH;

// common function to get style loaders
// 获取处理样式文件的loader
const getStyleLoaders = (cssOptions, preProcessor) => {
const loaders = [
// 开发环境用style-loader 生产环境压缩css
isEnvDevelopment && require.resolve('style-loader'),
isEnvProduction && {
loader: MiniCssExtractPlugin.loader,
// css is located in `static/css`, use '../../' to locate index.html folder
// in production `paths.publicUrlOrPath` can be a relative path
options: paths.publicUrlOrPath.startsWith('.')
? { publicPath: '../../' }
: {},
},
// 将样式整合到js中
{
loader: require.resolve('css-loader'),
options: cssOptions,
},
{
// Options for PostCSS as we reference these options twice
// Adds vendor prefixing based on your specified browser support in
// package.json
// 做css兼容性处理
loader: require.resolve('postcss-loader'),
options: {
// Necessary for external CSS imports to work
// https://github.com/facebook/create-react-app/issues/2677
ident: 'postcss',
plugins: () => [
require('postcss-flexbugs-fixes'),
require('postcss-preset-env')({
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
}),
// Adds PostCSS Normalize as the reset css with default options,
// so that it honors browserslist config in package.json
// which in turn let's users customize the target behavior as per their needs.
postcssNormalize(),
],
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
},
},
].filter(Boolean);
if (preProcessor) {
loaders.push(
{
loader: require.resolve('resolve-url-loader'),
options: {
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
root: paths.appSrc,
},
},
{
loader: require.resolve(preProcessor),
options: {
sourceMap: true,
},
}
);
}
return loaders;
};

return {
mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
// Stop compilation early in production
// 开发环境出错会继续打包,因为接下来代码可能被修改
// 生产环境则立即停止打包
bail: isEnvProduction,
devtool: isEnvProduction
? shouldUseSourceMap
? 'source-map' // 生产环境
: false
: isEnvDevelopment && 'cheap-module-source-map', // 开发环境
// These are the "entry points" to our application.
// This means they will be the "root" imports that are included in JS bundle.
entry:
isEnvDevelopment && !shouldUseReactRefresh
? [
// Include an alternative client for WebpackDevServer. A client's job is to
// connect to WebpackDevServer by a socket and get notified about changes.
// When you save a file, the client will either apply hot updates (in case
// of CSS changes), or refresh the page (in case of JS changes). When you
// make a syntax error, this client will display a syntax error overlay.
// Note: instead of the default WebpackDevServer client, we use a custom one
// to bring better experience for Create React App users. You can replace
// the line below with these two lines if you prefer the stock client:
//
// require.resolve('webpack-dev-server/client') + '?/',
// require.resolve('webpack/hot/dev-server'),
//
// When using the experimental react-refresh integration,
// the webpack plugin takes care of injecting the dev client for us.
webpackDevClientEntry,
// Finally, this is your app's code:
paths.appIndexJs,
// We include the app code last so that if there is a runtime error during
// initialization, it doesn't blow up the WebpackDevServer client, and
// changing JS code would still trigger a refresh.
]
: paths.appIndexJs,
output: {
// The build folder.
// 生产环境输出到目录,开发环境不输出
path: isEnvProduction ? paths.appBuild : undefined,
// Add /* filename */ comments to generated require()s in the output.
pathinfo: isEnvDevelopment,
// There will be one main bundle, and one file per asynchronous chunk.
// In development, it does not produce real files.
filename: isEnvProduction
? 'static/js/[name].[contenthash:8].js'
: isEnvDevelopment && 'static/js/bundle.js',
// TODO: remove this when upgrading to webpack 5
futureEmitAssets: true,
// There are also additional JS chunk files if you use code splitting.
// 经过代码分割的文件会带上chunk的后缀,用于区分入口文件
chunkFilename: isEnvProduction
? 'static/js/[name].[contenthash:8].chunk.js'
: isEnvDevelopment && 'static/js/[name].chunk.js',
// webpack uses `publicPath` to determine where the app is being served from.
// It requires a trailing slash, or the file assets will get an incorrect path.
// We inferred the "public path" (such as / or /my-project) from homepage.
publicPath: paths.publicUrlOrPath,
// Point sourcemap entries to original disk location (format as URL on Windows)
devtoolModuleFilenameTemplate: isEnvProduction
? info =>
path
.relative(paths.appSrc, info.absoluteResourcePath)
.replace(/\\/g, '/')
: isEnvDevelopment &&
(info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
// Prevents conflicts when multiple webpack runtimes (from different apps)
// are used on the same page.
jsonpFunction: `webpackJsonp${appPackageJson.name}`,
// this defaults to 'window', but by setting it to 'this' then
// module chunks which are built will work in web workers as well.
// 用this来统一顶级变量
globalObject: 'this',
},
optimization: {
minimize: isEnvProduction,
minimizer: [
// This is only used in production mode
// 压缩 JS
new TerserPlugin({
terserOptions: {
parse: {
// We want terser to parse ecma 8 code. However, we don't want it
// to apply any minification steps that turns valid ecma 5 code
// into invalid ecma 5 code. This is why the 'compress' and 'output'
// sections only apply transformations that are ecma 5 safe
// https://github.com/facebook/create-react-app/pull/4234
ecma: 8,
},
compress: {
ecma: 5,
warnings: false,
// Disabled because of an issue with Uglify breaking seemingly valid code:
// https://github.com/facebook/create-react-app/issues/2376
// Pending further investigation:
// https://github.com/mishoo/UglifyJS2/issues/2011
comparisons: false,
// Disabled because of an issue with Terser breaking valid code:
// https://github.com/facebook/create-react-app/issues/5250
// Pending further investigation:
// https://github.com/terser-js/terser/issues/120
inline: 2,
},
mangle: {
safari10: true,
},
// Added for profiling in devtools
keep_classnames: isEnvProductionProfile,
keep_fnames: isEnvProductionProfile,
output: {
ecma: 5,
comments: false,
// Turned on because emoji and regex is not minified properly using default
// https://github.com/facebook/create-react-app/issues/2488
ascii_only: true,
},
},
sourceMap: shouldUseSourceMap,
}),
// This is only used in production mode
// 压缩CSS
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
parser: safePostCssParser,
map: shouldUseSourceMap
? {
// `inline: false` forces the sourcemap to be output into a
// separate file
inline: false,
// `annotation: true` appends the sourceMappingURL to the end of
// the css file, helping the browser find the sourcemap
annotation: true,
}
: false,
},
cssProcessorPluginOptions: {
preset: ['default', { minifyFontValues: { removeQuotes: false } }],
},
}),
],
// Automatically split vendor and commons
// https://twitter.com/wSokra/status/969633336732905474
// https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
// 使用的默认配置
splitChunks: {
chunks: 'all',
name: isEnvDevelopment,
},
// Keep the runtime chunk separated to enable long term caching
// https://twitter.com/wSokra/status/969679223278505985
// https://github.com/facebook/create-react-app/issues/5358
// 分别打包runtime文件
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}`,
},
},
resolve: {
// This allows you to set a fallback for where webpack should look for modules.
// We placed these paths second because we want `node_modules` to "win"
// if there are any conflicts. This matches Node resolution mechanism.
// https://github.com/facebook/create-react-app/issues/253
// 依赖模块查找路径
modules: ['node_modules', paths.appNodeModules].concat(
modules.additionalModulePaths || []
),
// These are the reasonable defaults supported by the Node ecosystem.
// We also include JSX as a common component filename extension to support
// some tools, although we do not recommend using it, see:
// https://github.com/facebook/create-react-app/issues/290
// `web` extension prefixes have been added for better support
// for React Native Web.
extensions: paths.moduleFileExtensions
.map(ext => `.${ext}`)
.filter(ext => useTypeScript || !ext.includes('ts')),
// 定义一些别名
alias: {
// Support React Native Web
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
'react-native': 'react-native-web',
// Allows for better profiling with ReactDevTools
...(isEnvProductionProfile && {
'react-dom$': 'react-dom/profiling',
'scheduler/tracing': 'scheduler/tracing-profiling',
}),
...(modules.webpackAliases || {}),
},
plugins: [
// Adds support for installing with Plug'n'Play, leading to faster installs and adding
// guards against forgotten dependencies and such.
PnpWebpackPlugin,
// Prevents users from importing files from outside of src/ (or node_modules/).
// This often causes confusion because we only process files within src/ with babel.
// To fix this, we prevent you from importing files out of src/ -- if you'd like to,
// please link the files into your node_modules/ and let module-resolution kick in.
// Make sure your source files are compiled, as they will not be processed in any way.
// 用于检测查找文件的范围,必须在src下面或者package.json文件
new ModuleScopePlugin(paths.appSrc, [
paths.appPackageJson,
reactRefreshOverlayEntry,
]),
],
},
resolveLoader: {
plugins: [
// Also related to Plug'n'Play, but this time it tells webpack to load its loaders
// from the current package.
PnpWebpackPlugin.moduleLoader(module),
],
},
module: {
strictExportPresence: true,
rules: [
// Disable require.ensure as it's not a standard language feature.
// requireEnsure不在被支持
{ parser: { requireEnsure: false } },
{
// "oneOf" will traverse all following loaders until one will
// match the requirements. When no loader matches it will fall
// back to the "file" loader at the end of the loader list.
// 用于优化解析一个匹配将不在检查
oneOf: [
// TODO: Merge this config once `image/avif` is in the mime-db
// https://github.com/jshttp/mime-db
{
test: [/\.avif$/],
loader: require.resolve('url-loader'),
options: {
limit: imageInlineSizeLimit,
mimetype: 'image/avif',
name: 'static/media/[name].[hash:8].[ext]',
},
},
// "url" loader works like "file" loader except that it embeds assets
// smaller than specified limit in bytes as data URLs to avoid requests.
// A missing `test` is equivalent to a match.
//
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
loader: require.resolve('url-loader'),
options: {
limit: imageInlineSizeLimit,
name: 'static/media/[name].[hash:8].[ext]',
},
},
// Process application JS with Babel.
// The preset includes JSX, Flow, TypeScript, and some ESnext features.
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
// 只处理src文件夹目录下面的文件
include: paths.appSrc,
// babel-loader的配置在package.json中添加
loader: require.resolve('babel-loader'),
options: {
customize: require.resolve(
'babel-preset-react-app/webpack-overrides'
),
presets: [
[
require.resolve('babel-preset-react-app'),
{
runtime: hasJsxRuntime ? 'automatic' : 'classic',
},
],
],

plugins: [
[
require.resolve('babel-plugin-named-asset-import'),
{
loaderMap: {
svg: {
ReactComponent:
'@svgr/webpack?-svgo,+titleProp,+ref![path]',
},
},
},
],
isEnvDevelopment &&
shouldUseReactRefresh &&
require.resolve('react-refresh/babel'),
].filter(Boolean),
// This is a feature of `babel-loader` for webpack (not Babel itself).
// It enables caching results in ./node_modules/.cache/babel-loader/
// directory for faster rebuilds.
cacheDirectory: true,
// See #6846 for context on why cacheCompression is disabled
cacheCompression: false,
compact: isEnvProduction,
},
},
// Process any JS outside of the app with Babel.
// Unlike the application JS, we only compile the standard ES features.
// 除了src文件下面以外的js文件
{
test: /\.(js|mjs)$/,
exclude: /@babel(?:\/|\\{1,2})runtime/,
loader: require.resolve('babel-loader'),
options: {
babelrc: false,
configFile: false,
compact: false,
presets: [
[
require.resolve('babel-preset-react-app/dependencies'),
{ helpers: true },
],
],
cacheDirectory: true,
// See #6846 for context on why cacheCompression is disabled
cacheCompression: false,

// Babel sourcemaps are needed for debugging into node_modules
// code. Without the options below, debuggers like VSCode
// show incorrect code and set breakpoints on the wrong lines.
sourceMaps: shouldUseSourceMap,
inputSourceMap: shouldUseSourceMap,
},
},
// "postcss" loader applies autoprefixer to our CSS.
// "css" loader resolves paths in CSS and adds assets as dependencies.
// "style" loader turns CSS into JS modules that inject <style> tags.
// In production, we use MiniCSSExtractPlugin to extract that CSS
// to a file, but in development "style" loader enables hot editing
// of CSS.
// By default we support CSS Modules with the extension .module.css
{
test: cssRegex,
exclude: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
}),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
// 样式资源是有副作用的,不要进行treeShaking
sideEffects: true,
},
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
// using the extension .module.css
// 处理模块化样式, 文件以module.css结尾
{
test: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
}),
},
// Opt-in support for SASS (using .scss or .sass extensions).
// By default we support SASS Modules with the
// extensions .module.scss or .module.sass
// 处理sassloader
{
test: sassRegex,
exclude: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
},
'sass-loader'
),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
// Adds support for CSS Modules, but using SASS
// using the extension .module.scss or .module.sass
{
test: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
},
'sass-loader'
),
},
// "file" loader makes sure those assets get served by WebpackDevServer.
// When you `import` an asset, you get its (virtual) filename.
// In production, they would get copied to the `build` folder.
// This loader doesn't use a "test" so it will catch all modules
// that fall through the other loaders.
// 一些特殊文件,视频文件等,直接输出
{
loader: require.resolve('file-loader'),
// Exclude `js` files to keep "css" loader working as it injects
// its runtime that would otherwise be processed through "file" loader.
// Also exclude `html` and `json` extensions so they get processed
// by webpacks internal loaders.
exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
options: {
name: 'static/media/[name].[hash:8].[ext]',
},
},
// ** STOP ** Are you adding a new loader?
// Make sure to add the new loader(s) before the "file" loader.
],
},
],
},
plugins: [
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appHtml,
},
// 生产环境加了压缩的配置
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),
// Inlines the webpack runtime script. This script is too small to warrant
// a network request.
// https://github.com/facebook/create-react-app/issues/5358
// 是否要内联runtime文件
isEnvProduction &&
shouldInlineRuntimeChunk &&
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]),
// Makes some environment variables available in index.html.
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
// <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
// It will be an empty string unless you specify "homepage"
// in `package.json`, in which case it will be the pathname of that URL.
// 解析html中的 %PUBLIC_URL%
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
// This gives some necessary context to module not found errors, such as
// the requesting resource.
// 给没有找到的模块提供更好的提示
new ModuleNotFoundPlugin(paths.appPath),
// Makes some environment variables available to the JS code, for example:
// if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
// It is absolutely essential that NODE_ENV is set to production
// during a production build.
// Otherwise React will be compiled in the very slow development mode.
// 定义环境变量
new webpack.DefinePlugin(env.stringified),
// This is necessary to emit hot updates (CSS and Fast Refresh):
// 开发环境提供热模块替换
isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
// Experimental hot reloading for React .
// https://github.com/facebook/react/tree/master/packages/react-refresh

isEnvDevelopment &&
shouldUseReactRefresh &&
new ReactRefreshWebpackPlugin({
overlay: {
entry: webpackDevClientEntry,
// The expected exports are slightly different from what the overlay exports,
// so an interop is included here to enable feedback on module-level errors.
module: reactRefreshOverlayEntry,
// Since we ship a custom dev client and overlay integration,
// the bundled socket handling logic can be eliminated.
sockIntegration: false,
},
}),
// Watcher doesn't work well if you mistype casing in a path so we use
// a plugin that prints an error when you attempt to do this.
// See https://github.com/facebook/create-react-app/issues/240
// 文件路径严格区分大小写
isEnvDevelopment && new CaseSensitivePathsPlugin(),
// If you require a missing module and then `npm install` it, you still have
// to restart the development server for webpack to discover it. This plugin
// makes the discovery automatic so you don't have to restart.
// See https://github.com/facebook/create-react-app/issues/186
// 监视nodemodules一旦变化会重启server
isEnvDevelopment &&
new WatchMissingNodeModulesPlugin(paths.appNodeModules),
// 提取css为单独的文件
isEnvProduction &&
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: 'static/css/[name].[contenthash:8].css',
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
}),
// Generate an asset manifest file with the following content:
// - "files" key: Mapping of all asset filenames to their corresponding
// output file so that tools can pick it up without having to parse
// `index.html`
// - "entrypoints" key: Array of files which are included in `index.html`,
// can be used to reconstruct the HTML if necessary
new ManifestPlugin({
fileName: 'asset-manifest.json',
publicPath: paths.publicUrlOrPath,
generate: (seed, files, entrypoints) => {
const manifestFiles = files.reduce((manifest, file) => {
manifest[file.name] = file.path;
return manifest;
}, seed);
const entrypointFiles = entrypoints.main.filter(
fileName => !fileName.endsWith('.map')
);

return {
files: manifestFiles,
entrypoints: entrypointFiles,
};
},
}),
// Moment.js is an extremely popular library that bundles large locale files
// by default due to how webpack interprets its code. This is a practical
// solution that requires the user to opt into importing specific locales.
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
// You can remove this if you don't use Moment.js:
// 优化momentjs
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
// Generate a service worker script that will precache, and keep up to date,
// the HTML & assets that are part of the webpack build.
isEnvProduction &&
fs.existsSync(swSrc) &&
new WorkboxWebpackPlugin.InjectManifest({
swSrc,
dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./,
exclude: [/\.map$/, /asset-manifest\.json$/, /LICENSE/],
// Bump up the default maximum size (2mb) that's precached,
// to make lazy-loading failure scenarios less likely.
// See https://github.com/cra-template/pwa/issues/13#issuecomment-722667270
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
}),
// TypeScript type checking
useTypeScript &&
new ForkTsCheckerWebpackPlugin({
typescript: resolve.sync('typescript', {
basedir: paths.appNodeModules,
}),
async: isEnvDevelopment,
checkSyntacticErrors: true,
resolveModuleNameModule: process.versions.pnp
? `${__dirname}/pnpTs.js`
: undefined,
resolveTypeReferenceDirectiveModule: process.versions.pnp
? `${__dirname}/pnpTs.js`
: undefined,
tsconfig: paths.appTsConfig,
reportFiles: [
// This one is specifically to match during CI tests,
// as micromatch doesn't match
// '../cra-template-typescript/template/src/App.tsx'
// otherwise.
'../**/src/**/*.{ts,tsx}',
'**/src/**/*.{ts,tsx}',
'!**/src/**/__tests__/**',
'!**/src/**/?(*.)(spec|test).*',
'!**/src/setupProxy.*',
'!**/src/setupTests.*',
],
silent: true,
// The formatter is invoked directly in WebpackDevServerUtils during development
formatter: isEnvProduction ? typescriptFormatter : undefined,
}),
!disableESLintPlugin &&
new ESLintPlugin({
// Plugin options
extensions: ['js', 'mjs', 'jsx', 'ts', 'tsx'],
formatter: require.resolve('react-dev-utils/eslintFormatter'),
eslintPath: require.resolve('eslint'),
failOnError: !(isEnvDevelopment && emitErrorsAsWarnings),
context: paths.appSrc,
cache: true,
cacheLocation: path.resolve(
paths.appNodeModules,
'.cache/.eslintcache'
),
// ESLint class options
cwd: paths.appPath,
resolvePluginsRelativeTo: __dirname,
baseConfig: {
extends: [require.resolve('eslint-config-react-app/base')],
rules: {
...(!hasJsxRuntime && {
'react/react-in-jsx-scope': 'error',
}),
},
},
}),
].filter(Boolean),
// Some libraries import Node modules but don't use them in the browser.
// Tell webpack to provide empty mocks for them so importing them works.
// 避免打包一些node模块文件
node: {
module: 'empty',
dgram: 'empty',
dns: 'mock',
fs: 'empty',
http2: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty',
},
// Turn off performance processing because we utilize
// our own hints via the FileSizeReporter
// 关闭性能分析
performance: false,
};
};

build.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184

const measureFileSizesBeforeBuild =
FileSizeReporter.measureFileSizesBeforeBuild;
const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
const useYarn = fs.existsSync(paths.yarnLockFile);

// These sizes are pretty large. We'll warn for bundles exceeding them.
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;

const isInteractive = process.stdout.isTTY;

// Warn and crash if required files are missing
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
process.exit(1);
}

const argv = process.argv.slice(2);
const writeStatsJson = argv.indexOf('--stats') !== -1;

// Generate configuration
const config = configFactory('production');

// We require that you explicitly set browsers and do not fall back to
// browserslist defaults.
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
checkBrowsers(paths.appPath, isInteractive)
.then(() => {
// First, read the current file sizes in build directory.
// This lets us display how much they changed later.
return measureFileSizesBeforeBuild(paths.appBuild);
})
.then(previousFileSizes => {
// Remove all content but keep the directory so that
// if you're in it, you don't end up in Trash
fs.emptyDirSync(paths.appBuild);
// Merge with the public folder
copyPublicFolder();
// Start the webpack build
return build(previousFileSizes);
})
.then(
({ stats, previousFileSizes, warnings }) => {
if (warnings.length) {
console.log(chalk.yellow('Compiled with warnings.\n'));
console.log(warnings.join('\n\n'));
console.log(
'\nSearch for the ' +
chalk.underline(chalk.yellow('keywords')) +
' to learn more about each warning.'
);
console.log(
'To ignore, add ' +
chalk.cyan('// eslint-disable-next-line') +
' to the line before.\n'
);
} else {
console.log(chalk.green('Compiled successfully.\n'));
}

console.log('File sizes after gzip:\n');
printFileSizesAfterBuild(
stats,
previousFileSizes,
paths.appBuild,
WARN_AFTER_BUNDLE_GZIP_SIZE,
WARN_AFTER_CHUNK_GZIP_SIZE
);
console.log();

const appPackage = require(paths.appPackageJson);
const publicUrl = paths.publicUrlOrPath;
const publicPath = config.output.publicPath;
const buildFolder = path.relative(process.cwd(), paths.appBuild);
printHostingInstructions(
appPackage,
publicUrl,
publicPath,
buildFolder,
useYarn
);
},
err => {
const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
if (tscCompileOnError) {
console.log(
chalk.yellow(
'Compiled with the following type errors (you may want to check these before deploying your app):\n'
)
);
printBuildError(err);
} else {
console.log(chalk.red('Failed to compile.\n'));
printBuildError(err);
process.exit(1);
}
}
)
.catch(err => {
if (err && err.message) {
console.log(err.message);
}
process.exit(1);
});

// Create the production build and print the deployment instructions.
function build(previousFileSizes) {
console.log('Creating an optimized production build...');

const compiler = webpack(config);
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
let messages;
if (err) {
if (!err.message) {
return reject(err);
}

let errMessage = err.message;

// Add additional information for postcss errors
if (Object.prototype.hasOwnProperty.call(err, 'postcssNode')) {
errMessage +=
'\nCompileError: Begins at CSS selector ' +
err['postcssNode'].selector;
}

messages = formatWebpackMessages({
errors: [errMessage],
warnings: [],
});
} else {
messages = formatWebpackMessages(
stats.toJson({ all: false, warnings: true, errors: true })
);
}
if (messages.errors.length) {
// Only keep the first error. Others are often indicative
// of the same problem, but confuse the reader with noise.
if (messages.errors.length > 1) {
messages.errors.length = 1;
}
return reject(new Error(messages.errors.join('\n\n')));
}
if (
process.env.CI &&
(typeof process.env.CI !== 'string' ||
process.env.CI.toLowerCase() !== 'false') &&
messages.warnings.length
) {
console.log(
chalk.yellow(
'\nTreating warnings as errors because process.env.CI = true.\n' +
'Most CI servers set it automatically.\n'
)
);
return reject(new Error(messages.warnings.join('\n\n')));
}

const resolveArgs = {
stats,
previousFileSizes,
warnings: messages.warnings,
};

if (writeStatsJson) {
return bfj
.write(paths.appBuild + '/bundle-stats.json', stats.toJson())
.then(() => resolve(resolveArgs))
.catch(error => reject(new Error(error)));
}

return resolve(resolveArgs);
});
});
}

function copyPublicFolder() {
fs.copySync(paths.appPublic, paths.appBuild, {
dereference: true,
filter: file => file !== paths.appHtml,
});
}

kmp算法

字符串匹配是计算机的基本任务之一。Knuth-Morris-Pratt算法(简称KMP)是最常用的之一。它以三个发明者命名,起头的那个K就是著名科学家Donald Knuth。

匹配逻辑

kmp 算法的关键点在于,当匹配串(pattern)下一个字符不再匹配的时候,不会盲目的移动到原串的下一位继续从头判断,而是利用已知信息跳到一个合理的位置继续匹配。

已知信息,前缀/后缀

首先,要了解两个概念:”前缀”和”后缀”。 “前缀”指除了最后一个字符以外,一个字符串的全部头部组合;”后缀”指除了第一个字符以外,一个字符串的全部尾部组合。

以”ABCDABD”为例:

“A”的前缀和后缀都为空集,共有元素的长度为0;

“AB”的前缀为[A],后缀为[B],共有元素的长度为0;

“ABC”的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;

“ABCD”的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;

“ABCDA”的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为”A”,长度为1;

“ABCDAB”的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为”AB”,长度为2;

“ABCDABD”的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。

另一个重要的信息是,对于匹配串的任意一个位置而言,由该位置发起的下一个匹配点位置其实与原串无关。

ABCDABD 匹配串中的两个AB分别为匹配串的前缀和后缀,当最后一个D没有匹配的时候,他会跳转到第三个字符C的位置尝试匹配,因为他们有相同的前缀,因此合理跳转匹配额位置的本质就是找到上一个前缀和后缀的位置

因为匹配点与源串无关,所以可以预先准备出一个部分匹配表

部分匹配表

首先需要两个指针来记录匹配位置

用一个next数组来保存部分匹配值

由于第一个匹配字符一定从索引0开始,所以吧比配置初始化为0,那么i为前一个字符,j为后一字符

1
2
3
4
5
6
7
8
let i = 0, j = 1;

const next = [0]

for(;j < pattern.length;j++){

}

一但,下一个字符pattern[j]与上一个字符pattern[i]不在匹配,需要在之前遍历过的字符中查找与当前pattern[j]相同的字符

可以看做是,匹配串中一对相同的前缀和后缀,如果没有则继续向前查找,直到i指针回到初始位置

1
2
3
while(i>0 && pattern[i]!==pattern[j]) {
i = next[i-1]
}

有疑问的点可能在于,为什么不是依次判断之前每一个值,而是直接调到了匹配值的位置

i0的意义是从0索引到i索引的这个字符串,找不到一对相同的前缀和后缀,因此下个位置必然没有可复用的子串,只需要跳到开头位置重新匹配。

如果pattern[j]pattern[i]匹配,那么两个指针同时向后面移动,并记录新的位置

所以完整的代码为

1
2
3
4
5
6
7
8
9
let i=0;j=1;
const next = [0];
for(;j < pattern.length;j++){
while(i>0 && pattern[i]!==pattern[j]) {
i = next[i-1]
}
if(pattern[i]===pattern[j]) i++
next[j]=i;
}

匹配实现

在拿到了部分匹配表之后,可以通过一个循环依次向后匹配

m为原字符串指针,n为匹配串指针

如果两个字符匹配,两个指针都向后移动一位

1
2
3
4
5
6
7
let m=0;
let n=0;

for(;m<sourceString.length;m++){
if (sourceString[m] === pattern[n]) n++
}

如果不同则需要利用匹配表移动到最近的可复用的匹配位置,如图一中的事例

abeabf中的f与原串不在匹配,通过匹配表next[n-1]查找之前的子串中是否有可复用的位置,如果复用位置不匹配,则继续回溯之前的位置

1
while(n>0 && sourceString[m] === pattern[n]) n = next[n-1]

如果n 指针与匹配串长度相同,表示已经匹配成功则返回

完整的代码为

1
2
3
4
5
6
7
8
9
10
let m = 0;
let n = 0;
for(;m<sourceString.length;m++) {
while (n > 0 && sourceString[m] !== pattern[n]) {
n = next[n - 1];
}
if (sourceString[m] === pattern[n]) n++
if (n === pattern.length) return m - n + 1;
}
return -1;

kmp

leetcode 28.实现 strStr()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const kmp = (haystack, needle) => {
if (needle.length == 0) return 0;
const next = [0];
for (let i = 0, j = 1; j < needle.length; j++) {
while (i && needle[j] !== needle[i]) {
i = next[i - 1];
}
if (needle[j] === needle[i]) i++;
next[j] = i;
}
let m = 0;
let n = 0;
for(;m<sourceString.length;m++) {
while (n > 0 && haystack[m] !== needle[n]) {
n = next[n - 1];
}
if (haystack[m] === needle[n]) n++
if (n === needle.length) return m - n + 1;
}
return -1;
}

Recoil状态管理库

Recoil 产生背景

前端的应用越来越复杂,诸如常见的 Web 监控面板,包含各类的性能数据、节点信息、分类聚合用来进行应用分析。可以想象得到面板中包含各类的交互行为,编辑、删除、添加、将一个数据源绑定多个面板等等。除此之外,还需要对数据持久化,这样就能把 url 分享给其他人,并要确保被分享的人看到的是一致的。

因此开发过程中要尽量做到页面最小化更新达到高性能的目的,需要对数据流的操作更加精细。

面对这样的挑战,一般会想到用一些状态管理的函数或者库,如 React 内置的 state 管理,或者 Redux。

Recoil 想通过一个不一样的方式来解决这些问题,主要分为 3 个方面:

Flexible shared state: 在 react tree 任意的地方都能灵活共享 state,并保持高性能
Derived data and queries: 高效可靠地根据变化的 state 进行计算
App-wide state observation: time travel debugging, 支持 undo, 日志持久化

Recoil 主要设计

有一个应用基于这样一个场景,将 List 中更新一个节点,然后对应 Canvas 中的节点也更新

第 1 种方式

把 state 传到公共的父节点转发给 canvas 子节点,这样显然会全量 re-render

第 2 种方式

给父节点加 Provider 在子节点加 Consumer,不过每多加一个 item 就要多一层 Provider

第 3 种方式

在 react tree 上创建另一个正交的 tree,把每片 item 的 state 抽出来。每个 component 都有对应单独的一片 state,当数据更新的时候对应的组件也会更新。Recoil 把 这每一片的数据称为 Atom,Atom 是可订阅可变的 state 单元。

配合 useRecoilState 可以使用这些 Atom,实践上对多个 item 的 Atom 可以用 memorize 进行优化,具体可以在官方文档查看

Derived Data

有这么一个场景需要根据多个 Item Box 计算 Bounding Box

如果你是 Vue 的爱好者,你可能想到了计算属性。Derived Data 确实有 computed props 的味道,具体思路是选取多个 Atom 进行计算,然后返回一个新的 state。因此在 Recoil 中设计了 select 这样的 API 来选取多个 Atom 进行计算。

select 的设计和 Proxy 挺像的,属性上有 get 进行读取,有 set 进行设置,函数内部又有 get, set 操作 state

1
2
3
4
5
6
7
8
9
10
11
12
13
import {atom, selector, useRecoilState} from 'recoil';

const tempFahrenheit = atom({
key: 'tempFahrenheit',
default: 32,
});

const tempCelcius = selector({
key: 'tempCelcius',
get: ({get}) => ((get(tempFahrenheit) - 32) * 5) / 9,
set: ({set}, newValue) => set(tempFahrenheit, (newValue * 9) / 5 + 32),
});

App-wide observation

这个场景下需要把 url 分享给其他人,别人打开相同的链接也能看到一样的页面。

那么就需要 observe Atom 的变更,Recoil 使用 useTransactionObservation 进行订阅

1
useTransactionObservation(({atomValues,modifiedAtoms,...} => {}))

另一方面,打开链接的时候也需要对输入的数据进行校验

1
2
3
4
5
6
const counter = atom({
key: 'myCounter',
default: 0,
validator: (untrustedInput),
metadata: ...
})

Ubuntu20.04安装utorrent

原文

安装

虽然官网显示的最后版本是ubuntu13.04,但是仍然可以安装

也可以使用下面的命令下载

64 bits

1
wget http://download-hr.utorrent.com/track/beta/endpoint/utserver/os/linux-x64-ubuntu-13-04 -O utserver.tar.gz

32 bits

1
wget http://download-hr.utorrent.com/track/beta/endpoint/utserver/os/linux-x64-ubuntu-13-04 -O utserver.tar.gz

下载完成后把文件解压到 /opt目录

1
sudo tar xvf utserver.tar.gz -C /opt/

安装所需要的依赖

文件列表

1
2
3
4
5
sudo apt install libssl-dev

wget http://archive.ubuntu.com/ubuntu/pool/main/o/openssl1.0/libssl1.0.0_1.0.2n-1ubuntu5.6_amd64.deb

sudo apt install ./libssl1.0.0_1.0.2n-1ubuntu5.3_amd64.deb

创建一个链接

1
sudo ln -s /opt/utorrent-server-alpha-v3_3/utserver /usr/bin/utserver

通过一下命令可以启动 utorrent server,默认utorrent server监听在 0.0.0.0:8080 端口,如果被占用,请先停止占用的服务,utorrent server 同样会使用10000 和 6881 端口,添加 -daemon 参数后服务会在后台运行

1
utserver -settingspath /opt/utorrent-server-alpha-v3_3/ -daemon

现在可以通过图形界面来访问

1
localhost:8080/gui

请注意url中必须有gui,默认的用户名为admin,密码不用填写,登录之后可以在设置中修改,账户信息和端口

再改配置后需要通过一下命令重启服务

1
2
3
sudo pkill utserver

utserver -settingspath /opt/utorrent-server-alpha-v3_3/ &

配置自动启动服务

使用下面的命令创建一个系统服务

1
sudo nano /etc/systemd/system/utserver.service

把下面的配置添加到文件中,因为我们使用的是系统服务,所以不需要加-daemon

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Unit]
Description=uTorrent Server
After=network.target

[Service]
Type=simple
User=utorrent
Group=utorrent
ExecStart=/usr/bin/utserver -settingspath /opt/utorrent-server-alpha-v3_3/
ExecStop=/usr/bin/pkill utserver
Restart=always
SyslogIdentifier=uTorrent Server

[Install]
WantedBy=multi-user.target

按下 Ctrl+O , 按下 Enter键保存文件,按 Ctrl+X 退出,然后重新加载

1
sudo systemctl daemon-reload

注意:以root权限运行 utorrent server是不被允许的,我们已经在server的文件中明确规定了utorrent server 必须以utorrent用户/组来运行,所以用下面的命令来添加这个用户

1
sudo adduser --system --group utorrent

启动服务

1
sudo systemctl start utserver

添加开机启动项

1
sudo systemctl enable utserver

检查服务

1
systemctl status utserver

133. 克隆图

LeetCode

注意: 执行测试用例的时候,不会每次都在一个新的全局作用域中执行,需要注意变量的声明位置,防止污染代码执行

第一步 实现图的深度遍历 DFS

1
2
3
4
5
6
7
8
9
10
11
const cloneGraph = (node)=>{
if(!node) return node;
const dfs = (node) => {
console.log(node.val);
node.isVisit = true;
node.neighbors.forEach(n => {
if(!n.isVisit) dfs(n);
});
}
dfs(node);
}

第二步 克隆节点并保存

1
2
3
4
5
6
7
8
9
10
11
const cloneGraph = (node)=>{
if(!node) return node;
const map = new Map();
const dfs = (node) => {
map.set(node,new Node(node.val))
node.neighbors.forEach(n => {
if(!map.get(n)) dfs(n);
});
}
dfs(node);
}

第三步 添加节点间关系

1
2
3
4
5
6
7
8
9
10
11
12
13
const cloneGraph = (node)=>{
if(!node) return node;
const map = new Map();
const dfs = (node) => {
map.set(node,new Node(node.val))
node.neighbors.forEach(n => {
if(!map.get(n)) dfs(n);
map.get(node).neighbors.push(map.get(n));
});
}
dfs(node);
return map.get(node)
}

变形写法

在传参的过程中添加更多的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var cloneGraph = function (node) {
const map = new Map();
const cloneNode = new Node(node.val);
map.set(node.val, cloneNode)
const dfs = (cloneNode, node, neighbors) => {
for (let n of node.neighbors) {
if (map.has(n.val)) {
neighbors.push(map.get(n.val))
} else {
const cloneNode = new Node(n.val)
neighbors.push(cloneNode)
map.set(n.val, cloneNode)
dfs(cloneNode, n, cloneNode.neighbors)
}
}
}
dfs(cloneNode, node, cloneNode.neighbors)
return cloneNode;
};

广度优先遍历BFS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const cloneGraph = (node) => {
if (!node) return node;
const map = new Map();
const stack = [node];
let clone = null;
while (stack.length) {
const last = stack.pop();
if (!map.get(last)) {
clone = new Node(last.val);
map.set(last, clone);
} else {
clone = map.get(last)
}
last.neighbors.forEach(n => {
if (!map.get(n)) {
const temp = new Node(n.val);
clone.neighbors.push(temp);
map.set(n, temp);
stack.unshift(n);
} else {
clone.neighbors.push(map.get(n));
}
})
}
return map.get(node);
}

广度优先遍历优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const cloneGraph = (node) => {
if (!node) return node;
const map = new Map();
const stack = [node];
// 默认克隆第一个节点
map.set(node, new Node(node.val));

while (stack.length) {
const next = stack.pop();
next.neighbors.forEach(n => {
if (!map.get(n)) {
map.set(n, new Node(n.val));
stack.unshift(n);
}
//上一步已经在map中保存了节点,可以直接使用
map.get(next).neighbors.push(map.get(n));
})
}
return map.get(node);
}

65.有效数字

LeetCode

条件判断

维护了三个状态

  • 有 e|E 的时候,!hasE用于判断这是第一次遇到 e|E 这个字符,因为 e|E 只能有一个,hasNum表示遇到 e|E 的时候前面必须有一个数字

    后面一句 hasNum = false 排除了 +2e 以 e|E 结尾的情况

  • . 的时候,.一定在 e|E 的前面,所以判断!hasE.只可能有一个,所以isFloatfalse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var isNumber = function (s) {
// 是否存在e,
var hasE = false;
var hasNum = false;
var isFloat = false;
for (let i = 0; i < s.length; i++) {
var c = s[i];
// 符号只能在首位,或者E|e 的后一位
if (
(c == "+" || c == "-") &&
(i == 0 || s[i - 1] === "e" || s[i - 1] === "E")
) {
//e只能有一个,e的前面必须是数字
} else if ((c === "e" || c === "E") && !hasE && hasNum) {
hasE = true;
hasNum = false;
} else if (c === "." && !isFloat && !hasE) {
isFloat = true;
} else if (/[0-9]/.test(c)) {
hasNum = true;
} else {
return false;
}
}
return hasNum;
};

有限状态机

确定有限状态自动机

一个有效数字可以分为一下几个部分 :

  • 符号位,即 + - 两种符号
  • 整数部分,即由若干字符 0-9 组成的字符串
  • 小数点
  • 小数部分,其构成与整数部分相同
  • 指数部分,其中包含开头的字符 e(大写小写均可)、可选的符号位,和整数部分

每个部分都不是必需的,但也受一些额外规则的制约:

  • 如果符号位存在,其后面必须跟着数字或小数点。
  • 小数点的前后两侧,至少有一侧是数字。

根据上面的分析可以把一个有效数字分为下面几个状态:

  • 符号位
  • 整数部分
  • 左侧有整数的小数点
  • 左侧无整数的小数点(根据前面的第二条额外规则,需要对左侧有无整数的两种小数点做区分)
  • 小数部分
  • 字符 e
  • 指数部分的符号位
  • 指数部分的整数部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
var isNumber = function (s) {
const State = {
STATE_INITIAL: "STATE_INITIAL", // 初始状态
STATE_INT_SIGN: "STATE_INT_SIGN", // 符号位
STATE_INTEGER: "STATE_INTEGER", //整数部分
STATE_POINT: "STATE_POINT", // 左侧有整数的小数点
STATE_POINT_WITHOUT_INT: "STATE_POINT_WITHOUT_INT", //左侧无整数的小数点
STATE_FRACTION: "STATE_FRACTION", //小数部分
STATE_EXP: "STATE_EXP", //字符e
STATE_EXP_SIGN: "STATE_EXP_SIGN", //指数部分的符号位
STATE_EXP_NUMBER: "STATE_EXP_NUMBER", //指数部分的整数部分
STATE_END: "STATE_END", //结束状态
};

const CharType = {
CHAR_NUMBER: "CHAR_NUMBER",
CHAR_EXP: "CHAR_EXP",
CHAR_POINT: "CHAR_POINT",
CHAR_SIGN: "CHAR_SIGN",
CHAR_ILLEGAL: "CHAR_ILLEGAL",
};

const toCharType = (ch) => {
if (!isNaN(ch)) {
return CharType.CHAR_NUMBER;
} else if (ch.toLowerCase() === "e") {
return CharType.CHAR_EXP;
} else if (ch === ".") {
return CharType.CHAR_POINT;
} else if (ch === "+" || ch === "-") {
return CharType.CHAR_SIGN;
} else {
return CharType.CHAR_ILLEGAL;
}
};

const transfer = new Map();
const initialMap = new Map();
initialMap.set(CharType.CHAR_NUMBER, State.STATE_INTEGER);
initialMap.set(CharType.CHAR_POINT, State.STATE_POINT_WITHOUT_INT);
initialMap.set(CharType.CHAR_SIGN, State.STATE_INT_SIGN);
transfer.set(State.STATE_INITIAL, initialMap);
const intSignMap = new Map();
intSignMap.set(CharType.CHAR_NUMBER, State.STATE_INTEGER);
intSignMap.set(CharType.CHAR_POINT, State.STATE_POINT_WITHOUT_INT);
transfer.set(State.STATE_INT_SIGN, intSignMap);
const integerMap = new Map();
integerMap.set(CharType.CHAR_NUMBER, State.STATE_INTEGER);
integerMap.set(CharType.CHAR_EXP, State.STATE_EXP);
integerMap.set(CharType.CHAR_POINT, State.STATE_POINT);
transfer.set(State.STATE_INTEGER, integerMap);
const pointMap = new Map();
pointMap.set(CharType.CHAR_NUMBER, State.STATE_FRACTION);
pointMap.set(CharType.CHAR_EXP, State.STATE_EXP);
transfer.set(State.STATE_POINT, pointMap);
const pointWithoutIntMap = new Map();
pointWithoutIntMap.set(CharType.CHAR_NUMBER, State.STATE_FRACTION);
transfer.set(State.STATE_POINT_WITHOUT_INT, pointWithoutIntMap);
const fractionMap = new Map();
fractionMap.set(CharType.CHAR_NUMBER, State.STATE_FRACTION);
fractionMap.set(CharType.CHAR_EXP, State.STATE_EXP);
transfer.set(State.STATE_FRACTION, fractionMap);
const expMap = new Map();
expMap.set(CharType.CHAR_NUMBER, State.STATE_EXP_NUMBER);
expMap.set(CharType.CHAR_SIGN, State.STATE_EXP_SIGN);
transfer.set(State.STATE_EXP, expMap);
const expSignMap = new Map();
expSignMap.set(CharType.CHAR_NUMBER, State.STATE_EXP_NUMBER);
transfer.set(State.STATE_EXP_SIGN, expSignMap);
const expNumberMap = new Map();
expNumberMap.set(CharType.CHAR_NUMBER, State.STATE_EXP_NUMBER);
transfer.set(State.STATE_EXP_NUMBER, expNumberMap);

const length = s.length;
let state = State.STATE_INITIAL;

for (let i = 0; i < length; i++) {
const type = toCharType(s[i]);
if (!transfer.get(state).has(type)) {
return false;
} else {
state = transfer.get(state).get(type);
}
}
return (
state === State.STATE_INTEGER ||
state === State.STATE_POINT ||
state === State.STATE_FRACTION ||
state === State.STATE_EXP_NUMBER ||
state === State.STATE_END
);
};

函数式有限状态机

1
2
3
4
5
6
7
8
9
10
11
const start = (c) => {
const type = toCharType(c);
if (type === CharType.CHAR_NUMBER) return integer;
if (type === CharType.CHAR_POINT) return pointWithoutInt;
if (type === CharType.CHAR_SIGN) return intSign;
return end;
};
const integer = (c) => {};
const pointWithoutInt = () => {};
const intSign = () => {};
const end = () => end;

FSM 有限状态机

有限状态机

有限状态机(英语:finite-state machine,缩写:FSM)又称有限状态自动机(英语:finite-state automation,缩写:FSA),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型。

特点

  • 状态总数(state)是有限的。

  • 任一时刻,只处在一种状态之中。

  • 某种条件下,会从一种状态转变(transition)到另一种状态。

  • 每个状态都是一个机器,所有机器接受的输入是一致的

  • 状态机的本身是没有状态的,如果用函数来表示的话,应该是纯函数。

  • 每一个状态机都知道自己的下一个状态

    每个机器都有确定的下一个状态 (Moore)

    每个机器根据输入决定下一个状态 (Mealy)

案例

网页上有一个菜单元素。鼠标悬停的时候,菜单显示;鼠标移开的时候,菜单隐藏。如果使用有限状态机描述,就是这个菜单只有两种状态(显示和隐藏),鼠标会引发状态转变。

本案例中的状态机其实就是摩尔状态机,每个状态都有确定的下一个状态,而存在的问题就是状态和行为是耦合的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<body>
<div id = 'box1'>1</div>
<div id = 'box2' style="display:none">2</div>
<script>

var menu = {
// 当前状态
currentState: 'hide',
// 绑定事件
initialize: function (dom) {
dom.addEventListener('mouseover',this.transition.bind(this))
dom.addEventListener('mouseout',this.transition.bind(this))
},
// 状态转换
transition: function (event) {
switch (this.currentState) {
case "hide":
this.currentState = 'show';
document.getElementById('box2').style.display ='block'
break;
case "show":
this.currentState = 'hide';
document.getElementById('box2').style.display ='none'
break;
default:
console.log('Invalid State!');
break;
}
}
};
menu.initialize(document.getElementById('box1'));
</script>
</body>

查找字符串

尝试在一个字符串中找到 abcd

需要注意的是在每次状态迁移之后,一定要把上一个的状态置为false,因为状态转移之后与上一个状态再没有任何关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const machine = (str) => {
let foundA = false;
let foundB = false;
let foundC = false;
for (let s of str) {
if (s === 'a') {
foundA = true;
} else if (s === 'b' && foundA) {
foundA = false;
foundB = true;
} else if (s === 'c' && foundB) {
foundB = false;
foundC = true;
} else if (s === 'd' && foundC) {
foundC = false;
return true;
} else {
foundA = false;
foundB = false;
foundC = false;
}
}
return false;
}
console.log(machine('abbccd'))

查找字符串 函数式状态机

显而易见的好处是,状态机本身没有状态,所以无需在维护状态

需要注意的一点是,每一个函数返回的是start(s),因为遇到ababcd这种情况时,第二个a可以当做字符串的开头,如果直接返回start会跳过这一步的判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const start = (s) => {
if (s === 'a') return foundB;
return start;
}
const foundB = (s) => {
if (s === 'b') return foundC;
return start(s);
}
const foundC = (s) => {
if (s === 'c') return foundD;
return start(s);
}
const foundD = (s) => {
if (s === 'd') return end;
return start(s)
}
const end = () => end;
let state = start;
for (let s of 'ababcd') {
state = state(s);
}
console.log(state === end)

有效数字问题

Cookie的domain属性

一些概念

调试第三方模块

存在的问题

有时候需要在react项目中打断点调试,或者调试react源码

如果直接在node_modules中的文件打断点,添加注释或修改,在某些vsCode的版本中会提示nobonud breakPoint,不能进入断点

但最主要的问题,当项目重新初始化,所有的修改会被删除

安装插件

在vsCode市场中安装 Debugger for Chrome 插件

添加配置文件

选择Chrome

修改配置文件端口为项目端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:3000", //把端口修改为项目端口
"webRoot": "${workspaceFolder}"
}
]
}
  • 将需要调试的第三方包复制到本地,并在node_modules中删除

  • 进入第三方包的文件夹根目录执行,yarn link 创建一个链接

  • 进入项目文件夹根目录执行,yarn link "package name" 将依赖添加到node_modules中

  • 这时查看依赖包的路径为本地第三方包的路径

  • 进入项目文件夹根目录执行,yarn unlink "package name" 将依赖添从node_modules中删除

  • 进入第三方包的文件夹根目录执行,yarn unlink, 不在链接到全局

调试

如果react项目打断点不能被捕获,可以尝试在入口index.js文件中添加一行 debugger;

这样在调试器中点击下一步,会跳到下一个断点

  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信