可可爱爱没有脑袋00

导航

从零搭建 React 开发环境

  1 前言
  2 大概在 2019 年,自己搭建 React 开发环境的想法萌芽,到目前为止,公司的很多项目上,也在使用中,比较稳定。为什么要自己造轮子?起初是因为自己并不满意市面上的脚手架。另外,造轮子对于自己也有一些技术上的帮助,学别人二次封装的东西,不如直接使用底层的库,这样也有助于自己系统的学习一遍知识,废话不多说,直接进入正文,如何搭建自己的开发环境。
  3 
  4 初始化
  5 创建文件夹并进入:
  6 
  7 $ mkdir tristana && cd tristana
  8 初始化 package.json
  9 
 10 $ npm init
 11 安装 Webpack
 12 
 13 $ npm install webpack webpack-cli --save-dev
 14 创建以下目录结构、文件和内容:
 15 
 16 project
 17 tristana
 18 |- package.json
 19 |- /dist
 20    |- index.html
 21 |- /script
 22    |- webpack.config.js
 23 |- index.html
 24 |- /src
 25    |- index.js
 26 src/index.js
 27 document.getElementById("root").append("React");
 28 index.html && dist/index.html
 29 <!DOCTYPE html>
 30 <html>
 31     <head>
 32         <meta charset="utf-8" />
 33         <title>tristana</title>
 34     </head>
 35     <body>
 36         <script src="../src/index.js"></script>
 37         <div id="root"></div>
 38     </body>
 39 </html>
 40 script/webpack.config.js
 41 module.exports = {
 42     mode: "development",
 43     entry: "./src/index.js",
 44 };
 45 package.json
 46 {
 47     // ...
 48     "scripts": {
 49         "build": "webpack --mode=development --config script/webpack.config.js"
 50     },
 51 }
 52 然后根目录终端输入:npm run build
 53 
 54 在浏览器中打开 dist 目录下的 index.html,如果一切正常,你应该能看到以下文本:'React'
 55 
 56 index.html 目前放在 dist 目录下,但它是手动创建的,下面会教你如何生成 index.html 而非手动编辑它。
 57 
 58 Webpack 核心功能
 59 Babel
 60 $ npm install @babel/cli @babel/core babel-loader @babel/preset-env --save-dev
 61 script/webpack.config.js
 62 module.exports = {
 63     // ...
 64     module: {
 65         rules: [
 66             {
 67                 test: /\.(js|jsx)$/,
 68                 loader: "babel-loader",
 69                 exclude: /node_modules/,
 70             },
 71         ],
 72     },
 73 };
 74 .babelrc
 75 在根目录下添加 .babelrc 文件:
 76 
 77 {
 78     "presets": ["@babel/preset-env", "@babel/preset-react"]
 79 }
 80 样式
 81 $ npm install style-loader css-loader less less-loader --save-dev
 82 script/webpack.config.js
 83 module.exports = {
 84     // ...
 85     module: {
 86         rules: [
 87             {
 88                 test: /\.(css|less)$/,
 89                 use: [
 90                     {
 91                         loader: "style-loader",
 92                     },
 93                     {
 94                         loader: "css-loader",
 95                         options: {
 96                             importLoaders: 1,
 97                         },
 98                     },
 99                     {
100                         loader: "less-loader",
101                         lessOptions: {
102                             javascriptEnabled: true,
103                         },
104                     },
105                 ],
106             },
107         ],
108     },
109 };
110 图片字体
111 $ npm install file-loader --save-dev
112 script/webpack.config.js
113 module.exports = {
114     // ...
115     module: {
116         rules: [
117             {
118                 test: /\.(png|svg|jpg|gif|jpeg)$/,
119                 loader: 'file-loader'
120             },
121             {
122                 test: /\.(woff|woff2|eot|ttf|otf)$/,
123                 loader: 'file-loader'
124             }
125         ],
126     },
127 };
128 HTML
129 $ npm install html-webpack-plugin --save-dev
130 script/webpack.config.js
131 const HtmlWebpackPlugin = require('html-webpack-plugin');
132 module.exports = {
133     // ...
134     plugins: {
135         html: new HtmlWebpackPlugin({
136             title: 'tristana',
137             template: 'public/index.html'
138         }),
139     }
140 };
141 index.html
142 <!DOCTYPE html>
143 <html>
144     <head>
145         <meta charset="utf-8" />
146         <title>tristana</title>
147     </head>
148     <body>
149         <div id="root"></div>
150     </body>
151 </html>
152 开发服务
153 $ npm install webpack-dev-server --save-dev
154 script/webpack.config.js
155 const path = require("path");
156 const HtmlWebpackPlugin = require('html-webpack-plugin');
157 module.exports = {
158     // ...
159     devServer: {
160         contentBase: path.resolve(__dirname, "dist"),
161         hot: true,
162         historyApiFallback: true,
163         compress: true,
164     },
165 };
166 package.json
167 {
168     // ...
169     "scripts": {
170         "start": "webpack serve --mode=development --config script/webpack.config.js"
171     },
172     // ...
173 }
174 清理 dist
175 $ npm install clean-webpack-plugin --save-dev
176 script/webpack.config.js
177 const { CleanWebpackPlugin } = require('clean-webpack-plugin');
178 module.exports = {
179     // ...
180     plugins: {
181         new CleanWebpackPlugin()
182     }
183 };
184 Tips
185 由于 webpack 使用的是^5.21.2 版本,在使用该插件时,会提示clean-webpack-plugin: options.output.path not defined. Plugin disabled...,暂时还未解决。
186 
187 环境变量
188 $ npm install cross-env --save-dev
189 package.json
190 {
191     // ...
192     "scripts": {
193         "start": "cross-env ENV_LWD=development webpack serve  --mode=development --config script/webpack.config.js",
194         "build": "cross-env ENV_LWD=production webpack --mode=production --config script/webpack.config.js"
195     },
196     // ...
197 }
198 .jsx 文件
199 安装依赖
200 $ npm install @babel/preset-react react react-dom --save-dev
201 .babelrc
202 {
203   "presets": ["@babel/preset-env", "@babel/preset-react"]
204 }
205 src/App.jsx
206 在 src 目录下,新增 App.jsx 文件:
207 
208 import React, { Component } from "react";
209 
210 class App extends Component {
211     render() {
212         return (
213             <div>
214                 <h1> Hello, World! </h1>
215             </div>
216         );
217     }
218 }
219 
220 export default App;
221 src/index.js
222 import React from "react";
223 import ReactDOM from "react-dom";
224 import App from "./App.jsx";
225 ReactDOM.render(<App />, document.getElementById("root"));
226 React Router
227 安装依赖
228 $ npm install react-router history --save
229 src/index.js
230 import React from "react";
231 import ReactDOM from "react-dom";
232 import { Router, Route, Link } from "react-router";
233 import { createBrowserHistory } from "history";
234 import App from "./App.jsx";
235 
236 const About = () => {
237     return <>About</>;
238 };
239 
240 ReactDOM.render(
241     <Router history={createBrowserHistory()}>
242         <Route path="/" component={App} />
243         <Route path="/about" component={About} />
244     </Router>,
245     document.getElementById("root")
246 );
247 MobX
248 安装依赖
249 $ npm install mobx mobx-react babel-preset-mobx --save
250 .babelrc
251 {
252   "presets": ["@babel/preset-env", "@babel/preset-react", "mobx"]
253 }
254 src/store.js
255 在 src 目录下新建 store.js
256 
257 import { observable, action, makeObservable } from "mobx";
258 
259 class Store {
260 
261     constructor() {
262         makeObservable(this);
263     }
264 
265     @observable
266     count = 0;
267 
268     @action("add")
269     add = () => {
270         this.count = this.count + 1;
271     };
272 
273     @action("reduce")
274     reduce = () => {
275         this.count = this.count - 1;
276     };
277 }
278 export default new Store();
279 index.js
280 import { Provider } from "mobx-react";
281 import Store from "./store";
282 // ...
283 ReactDOM.render(
284     <Provider store={Store}>
285         <Router history={createBrowserHistory()}>
286         <Route path="/" component={App} />
287         <Route path="/about" component={About} />
288         </Router>
289     </Provider>,
290     document.getElementById("root")
291 );
292 src/App.jsx
293 import React, { Component } from "react";
294 import { observer, inject } from "mobx-react";
295 
296 @inject("store")
297 @observer
298 class App extends Component {
299     render() {
300         return (
301             <div>
302                 <div>{this.props.store.count}</div>
303                 <button onClick={this.props.store.add}>add</button>
304                 <button onClick={this.props.store.reduce}>reduce</button>
305             </div>
306         );
307     }
308 }
309 
310 export default App;
311 Ant Design
312 安装依赖
313 $ npm install antd babel-plugin-import --save
314 .babelrc
315 {
316     // ...
317     "plugins": [
318         [
319             "import",
320             {
321                 "libraryName": "antd",
322                 "libraryDirectory": "es",
323                 "style": true
324             }
325         ]
326     ]
327 }
328 src/App.jsx
329 // ...
330 import { DatePicker } from "antd";
331 import "antd/dist/antd.css";
332 
333 @inject("store")
334 @observer
335 class App extends Component {
336     render() {
337         return (
338             <div>
339                 <DatePicker />
340             </div>
341         );
342     }
343 }
344 
345 export default App;
346 TypeScript
347 安装依赖
348 $ npm install typescript @babel/preset-typescript --save-dev
349 .babelrc
350 {
351     "presets": [
352         // ...
353         "@babel/preset-typescript"
354     ]
355 }
356 tsconfig.json
357 在根目录下,新增 tsconfig.json 文件:
358 
359 {
360     "compilerOptions": {
361         "emitDecoratorMetadata": true,
362         "experimentalDecorators": true,
363         "target": "ES5",
364         "allowSyntheticDefaultImports": true,
365         "strict": true,
366         "forceConsistentCasingInFileNames": true,
367         "allowJs": true,
368         "outDir": "./dist/",
369         "esModuleInterop": true,
370         "noImplicitAny": false,
371         "sourceMap": true,
372         "module": "esnext",
373         "moduleResolution": "node",
374         "isolatedModules": true,
375         "importHelpers": true,
376         "lib": ["esnext", "dom", "dom.iterable"],
377         "skipLibCheck": true,
378         "jsx": "react",
379         "typeRoots": ["node", "node_modules/@types"],
380         "rootDirs": ["./src"],
381         "baseUrl": "./src"
382     },
383     "include": ["./src/**/*"],
384     "exclude": ["node_modules"]
385 }
386 src/App.jsx
387 更换文件后缀 App.jsx -> App.tsx
388 
389 import React, { Component } from "react";
390 import { observer, inject } from "mobx-react";
391 import { DatePicker } from "antd";
392 import "antd/dist/antd.css";
393 
394 @inject("store")
395 @observer
396 class App extends Component {
397     props: any;
398     render() {
399         return (
400             <div>
401                 <DatePicker />
402                 <div>{this.props.store.count}</div>
403                 <button onClick={this.props.store.add}>add</button>
404                 <button onClick={this.props.store.reduce}>reduce</button>
405             </div>
406         );
407     }
408 }
409 
410 export default App;
411 代码规范
412 代码校验、代码格式化、Git 提交前校验、Vscode配置、编译校验
413 
414 ESLint
415 安装依赖
416 $ npm install @typescript-eslint/parser eslint eslint-plugin-standard @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-promise --save-dev
417 .eslintrc.js
418 在根目录下,新增 .eslintrc.js 文件:
419 
420 module.exports = {
421     extends: ["eslint:recommended", "plugin:react/recommended"],
422     env: {
423         browser: true,
424         commonjs: true,
425         es6: true,
426     },
427     globals: {
428         $: true,
429         process: true,
430         __dirname: true,
431     },
432     parser: "@typescript-eslint/parser",
433     parserOptions: {
434         ecmaFeatures: {
435             jsx: true,
436             modules: true,
437         },
438         sourceType: "module",
439         ecmaVersion: 6,
440     },
441     plugins: ["react", "standard", "promise", "@typescript-eslint"],
442     settings: {
443         "import/ignore": ["node_modules"],
444         react: {
445             version: "latest",
446         },
447     },
448     rules: {
449         quotes: [2, "single"],
450         "no-console": 0,
451         "no-debugger": 1,
452         "no-var": 1,
453         semi: ["error", "always"],
454         "no-irregular-whitespace": 0,
455         "no-trailing-spaces": 1,
456         "eol-last": 0,
457         "no-unused-vars": [
458         1,
459         {
460             vars: "all",
461             args: "after-used",
462         },
463         ],
464         "no-case-declarations": 0,
465         "no-underscore-dangle": 0,
466         "no-alert": 2,
467         "no-lone-blocks": 0,
468         "no-class-assign": 2,
469         "no-cond-assign": 2,
470         "no-const-assign": 2,
471         "no-delete-var": 2,
472         "no-dupe-keys": 2,
473         "use-isnan": 2,
474         "no-duplicate-case": 2,
475         "no-dupe-args": 2,
476         "no-empty": 2,
477         "no-func-assign": 2,
478         "no-invalid-this": 0,
479         "no-redeclare": 2,
480         "no-spaced-func": 2,
481         "no-this-before-super": 0,
482         "no-undef": 2,
483         "no-return-assign": 0,
484         "no-script-url": 2,
485         "no-use-before-define": 2,
486         "no-extra-boolean-cast": 0,
487         "no-unreachable": 1,
488         "comma-dangle": 2,
489         "no-mixed-spaces-and-tabs": 2,
490         "prefer-arrow-callback": 0,
491         "arrow-parens": 0,
492         "arrow-spacing": 0,
493         camelcase: 0,
494         "jsx-quotes": [1, "prefer-double"],
495         "react/display-name": 0,
496         "react/forbid-prop-types": [
497         2,
498         {
499             forbid: ["any"],
500         },
501         ],
502         "react/jsx-boolean-value": 0,
503         "react/jsx-closing-bracket-location": 1,
504         "react/jsx-curly-spacing": [
505         2,
506         {
507             when: "never",
508             children: true,
509         },
510         ],
511         "react/jsx-indent": ["error", 4],
512         "react/jsx-key": 2,
513         "react/jsx-no-bind": 0,
514         "react/jsx-no-duplicate-props": 2,
515         "react/jsx-no-literals": 0,
516         "react/jsx-no-undef": 1,
517         "react/jsx-pascal-case": 0,
518         "react/jsx-sort-props": 0,
519         "react/jsx-uses-react": 1,
520         "react/jsx-uses-vars": 2,
521         "react/no-danger": 0,
522         "react/no-did-mount-set-state": 0,
523         "react/no-did-update-set-state": 0,
524         "react/no-direct-mutation-state": 2,
525         "react/no-multi-comp": 0,
526         "react/no-set-state": 0,
527         "react/no-unknown-property": 2,
528         "react/prefer-es6-class": 2,
529         "react/prop-types": 0,
530         "react/react-in-jsx-scope": 2,
531         "react/self-closing-comp": 0,
532         "react/sort-comp": 0,
533         "react/no-array-index-key": 0,
534         "react/no-deprecated": 1,
535         "react/jsx-equals-spacing": 2,
536     },
537 };
538 
539 .eslintignore
540 在根目录下,新增 .eslintignore 文件:
541 
542 src/assets
543 .vscode
544 在根目录下新增 .vscode 文件夹,然后新增 .vscode/settings.json
545 
546 {
547     "eslint.validate": [
548         "javascript",
549         "javascriptreact",
550         "typescript",
551         "typescriptreact"
552     ]
553 }
554 Perttier
555 安装依赖
556 $ npm install prettier --save-dev
557 prettier.config.js
558 在根目录下,新增 prettier.config.js 文件:
559 
560 module.exports = {
561     // 一行最多 100 字符
562     printWidth: 100,
563     // 使用 4 个空格缩进
564     tabWidth: 4,
565     // 不使用缩进符,而使用空格
566     useTabs: false,
567     // 行尾需要有分号
568     semi: true,
569     // 使用单引号
570     singleQuote: true,
571     // 对象的 key 仅在必要时用引号
572     quoteProps: 'as-needed',
573     // jsx 不使用单引号,而使用双引号
574     jsxSingleQuote: false,
575     // 末尾不需要逗号
576     trailingComma: 'none',
577     // 大括号内的首尾需要空格
578     bracketSpacing: true,
579     // jsx 标签的反尖括号需要换行
580     jsxBracketSameLine: false,
581     // 箭头函数,只有一个参数的时候,也需要括号
582     arrowParens: 'avoid',
583     // 每个文件格式化的范围是文件的全部内容
584     rangeStart: 0,
585     rangeEnd: Infinity,
586     // 不需要写文件开头的 @prettier
587     requirePragma: false,
588     // 不需要自动在文件开头插入 @prettier
589     insertPragma: false,
590     // 使用默认的折行标准
591     proseWrap: 'preserve',
592     // 根据显示样式决定 html 要不要折行
593     htmlWhitespaceSensitivity: 'css',
594     // 换行符使用 lf
595     endOfLine: 'lf'
596 };
597 stylelint
598 安装依赖
599 $ npm install stylelint stylelint-config-standard stylelint-config-prettier --save-dev
600 stylelint.config.js
601 在根目录下,新增 stylelint.config.js 文件:
602 
603 module.exports = {
604     extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
605     ignoreFiles: [
606         '**/*.ts',
607         '**/*.tsx',
608         '**/*.png',
609         '**/*.jpg',
610         '**/*.jpeg',
611         '**/*.gif',
612         '**/*.mp3',
613         '**/*.json'
614     ],
615     rules: {
616         'at-rule-no-unknown': [
617             true,
618             {
619                 ignoreAtRules: ['extends', 'ignores']
620             }
621         ],
622         indentation: 4,
623         'number-leading-zero': null,
624         'unit-allowed-list': ['em', 'rem', 's', 'px', 'deg', 'all', 'vh', '%'],
625         'no-eol-whitespace': [
626             true,
627             {
628                 ignore: 'empty-lines'
629             }
630         ],
631         'declaration-block-trailing-semicolon': 'always',
632         'selector-pseudo-class-no-unknown': [
633             true,
634             {
635                 ignorePseudoClasses: ['global']
636             }
637         ],
638         'block-closing-brace-newline-after': 'always',
639         'declaration-block-semicolon-newline-after': 'always',
640         'no-descending-specificity': null,
641         'selector-list-comma-newline-after': 'always',
642         'selector-pseudo-element-colon-notation': 'single'
643     }
644 };
645 lint-staged、pre-commit
646 安装依赖
647 $ npm install lint-staged prettier eslint pre-commit --save-dev
648 package.json
649 {
650     // ...
651     "scripts": {
652         "lint:tsx": "eslint --ext .tsx src && eslint --ext .ts src",
653         "lint:css": "stylelint --aei .less .css src",
654         "precommit": "lint-staged",
655         "precommit-msg": "echo 'Pre-commit checks...' && exit 0"
656     },
657     "pre-commit": [
658         "precommit",
659         "precommit-msg"
660     ],
661     "lint-staged": {
662         "*.{js,jsx,ts,tsx}": [
663             "eslint --fix",
664             "prettier --write",
665             "git add"
666         ],
667         "*.{css,less}": [
668             "stylelint --fix",
669             "prettier --write",
670             "git add"
671         ]
672     }
673 }
674 eslint-webpack-plugin
675 安装依赖
676 $ npm install eslint-webpack-plugin --save-dev
677 script/webpack.config.js
678 const ESLintPlugin = require('eslint-webpack-plugin');
679 module.exports = {
680     // ...
681     plugins: [new ESLintPlugin()],
682 };
683 总结
684 搭建这个的过程,也是遇到了不少坑,收获也是蛮多的,希望这个教程能够帮助更多的同学,少采点坑,完整的 React 开发环境可以看这个tristana,

 

posted on 2021-02-21 18:19  可可爱爱没有脑袋00  阅读(49)  评论(0编辑  收藏  举报