前言
本篇文章主要讲解如何从一个空目录开始,建立起一个基于webpack + react + typescript的标准化前端应用。
技术栈: webpack5 + React18 + TS
工程化: eslint + prettier + husky + git hooks
支持图片、less、sass、fonts、数据资源(JSON、csv、tsv等)、Antd按需加载以及主题
支持热更新、资源压缩、代码分离(动态导入、懒加载等)、缓存、devServer
当我们使用优秀的脚手架工具开发项目时,当然会提升很多便利,他们的功能更全面、性能更强大,但是在这些值得学习的榜样面前,我们需要从零开始,动手去实现每一个细节和功能,看的再多都不如自己动手实现一个demo更有效果。并且动手实践也可以帮助我们理解项目打包和编译的原理,进而提升自己的技术熟练度,扩展我们的知识面。Webpack 实现工程化方方面面的功能,自然不是 all in one code实现的。从 Webpack 的设计理念和实现原理中,我们能接触到工程化方面的知识:架构扩展、插件化、缓存机制。学习Webpack也代表着学习前端的发展趋势:例如在webpack的竟对Vite上,我们可以学到bundleless的理念,跳过了传统的打包这个概念,并且其他先进理念都是我们需要去学习的地方。
目录
├── dist // 默认的 build 输出目录├── .husky // pre-commit hook├── webpack.config.js // 全局配置文件及webpack配置文件├── test // 测试目录└── src // 源码目录 ├── assets // 公共的文件(如image、css、font等) ├── components // 项目组件 ├── constants // 常量/接口地址等 ├── routes // 路由 ├── utils // 工具库 ├── pages // 页面模块 ├── Home // Home模块,建议组件统一大写开头 ├── ... ├── App.tsx // react顶层文件 ├── typing // ts类型文件├── .editorconfig // IDE格式规范├── .eslintignore // eslint忽略├── .eslintrc // eslint配置文件├── .gitignore // git忽略├── .prettierrc // prettierc配置文件├── .babelrc // babel配置文件├── LICENSE.md // LICENSE├── package.json // package├── README.md // README├── tsconfig.json // typescript配置文件
依赖
"dependencies": { "antd": "^4.22.4", // 懂得都懂 "react": "^18.2.0", // 懂得都懂 "react-dom": "^18.2.0" // 懂得都懂 }, "devDependencies": { // babel全家桶 "@babel/core": "^7.18.10", "@babel/plugin-proposal-class-properties": "^7.18.6", // React class支持 "@babel/plugin-transform-runtime": "^7.18.10", // 抽离提取 Babel的注入代码,防止重复加载,减小体积 "@babel/preset-env": "^7.18.10", // 提供的预设,允许我们使用最新的JavaScript "@babel/preset-react": "^7.18.6", // react支持 // ts类型检查 "@types/node": "^18.6.4", "@types/react": "^18.0.15", "@types/react-dom": "^18.0.6", // @types 开头的是对应包的 TypeScript 类型声明 "@typescript-eslint/eslint-plugin": "^5.33.0", "@typescript-eslint/parser": "^5.33.0", // webpack loader:解析对应文件 "csv-loader": "^3.0.5", "sass-loader": "^13.0.2", "xml-loader": "^1.2.1", "ts-loader": "^9.3.1", "less-loader": "^11.0.0", // eslint全家桶 "eslint": "^8.21.0", "eslint-config-ali": "^14.0.1", // ali前端规约 "eslint-config-prettier": "^8.5.0", // 关闭所有不必要或可能与[Prettier]冲突的规则 "eslint-import-resolver-typescript": "^3.4.0", // 添加 ts 语法支持 eslint-plugin-import "eslint-plugin-import": "^2.26.0", // ES6+ import/export 语法支持 "eslint-plugin-prettier": "^4.2.1", // prettier语法支持 "eslint-plugin-react": "^7.30.1", // react语法支持 "eslint-plugin-react-hooks": "^4.6.0", // hooks语法支持 "eslint-webpack-plugin": "^3.2.0", // webpack plugin "fork-ts-checker-webpack-plugin": "^7.2.13", // 避免webpack中检测ts类型 "html-webpack-plugin": "^5.5.0", // 简化HTML文件的创建 ,配合webpack包含hash的bundle使用 "mini-css-extract-plugin": "^2.6.1", // css拆分 "optimize-css-assets-webpack-plugin": "^6.0.1", // css压缩 "terser-webpack-plugin": "^5.3.3", // 使用 terser 压缩 js (terser 是一个管理和压缩 ES6+ 的工具) "webpack-bundle-analyzer": "^4.5.0", // webpack打包体积可视化分析 "webpack-cli": "^4.10.0", // 提供脚手架命令 "webpack": "^5.74.0", // webpack引擎 "webpack-dev-server": "^4.9.3", // 开发环境的live server // 工具 "husky": "^8.0.1", // 自动配置 Git hooks 钩子 "less": "^4.1.3", // css类型 "sass": "^1.54.3", // css类型 "typescript": "^4.7.4", // ts "lint-staged": "^13.0.3", // 对暂存的git文件运行linter // prettier 格式化 "prettier": "^2.7.1", "pretty-quick": "^3.1.3", // 在更改的文件上运行 prettier }
项目初始化
首先从一个空目录开始,对项目初始化:
mkdir democd demogit initnpm init
React和Babel引入
tnpm i -S react react-dom
tnpm i -D @babel/core babel-preset-env babel-preset-react @babel/plugin-proposal-class-properties
@babel/core: babel转码的核心引擎
babel-preset-env: 添加对ES5、ES6的支持
babel-preset-react: 添加对JSX的支持
@babel/plugin-proposal-class-properties: 对React中class的支持
Webpack引入
tnpm i -D webpack webpack-cli webpack-dev-server html-webpack-plugin
webpack: weback插件的核心依赖
webpack-cli: 为插件提供命令行工具
webpack-dev-server: 帮助启动live server
html-webpack-plugin: 帮助创建HTML模版
Babel配置
{ "presets": ["@babel/react", "@babel/env"], "plugins": ["@babel/plugin-proposal-class-properties"]}
@babel/react: 支持了React所有的转码需求
@babel/env: 不夸张滴讲,仅需要它自己内部的配置项,就可以完成现代JS工程几乎所有的转码需求
Webpack基本配置
//webpack.config.jsconst path = require('path');const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = { entry: './src/index.js', output: { path: path.join(__dirname, '/dist'), filename: 'bundle.js' }, devServer: { port: 8080 }, module: { rules: [ { test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader', }, { test: /\.css$/, use: [ 'style-loader', 'css-loader' ] } ] }, plugins:[ new HtmlWebpackPlugin({ template: path.join(__dirname,'/src/index.html') }) ]}
entry: 入口,开始打包的起点
output: 打包文件的地址
devServer: live server配置
test: 使用loader的文件类型
Package.json基本配置
"start": "webpack serve --mode development --open --hot","build": "webpack --mode production"
mode: process.env.NODE_ENV --> development, 为modules和chunks启用有意义的名称
open: 告诉server在服务启动后打开默认浏览器
hot: 开启热更新
写一个React Demo
/_index.js_import React from "react";import ReactDOM from "react-dom";const App = () => {return (<div><h1>Hello!!</h1><h2>Welcome to your First React App..!</h2></div>);};ReactDOM.render(<App />, document.getElementById("root"));
//_index.html_<html lang = "en"><head><meta charset = "UTF-8"><title>React Web</title></head><body><div id = "root"></div><script src = 'bundle.js'></script></body></html>
TypeScript配置
tnpm install -D typescript ts-loader @types/node @types/react @types/react-dom
typescript: TypeScript的主要引擎
ts-loader: 转义.ts --> .js 并打包
//_tsconfig.json_
{ "compilerOptions": { "outDir": "./dist/", "noImplicitAny": true, "module": "es6", "target": "es5", "jsx": "react", "allowJs": true, "allowSyntheticDefaultImports": true, "moduleResolution": "Node" }}
//_webpack.config.js_...{ test: /\.tsx?$/, exclude: /node_modules/, loader: 'ts-loader'}...
//_webpack.config.js_...resolve: { extensions: [ '.tsx', '.ts', '.js' ],}...
rename入口:
//_webpack.config.js_...entry: "./src/index.tsx",...
npx create-react-app my-app --template typescript
module: { rules: [ { test: /\.tsx?$/, exclude: /node_modules/, loader: ['babel-loader'] } ] },
ts-loader 在内部是调用了 TypeScript 的官方编译器 -- tsc。所以,ts-loader 和 tsc 是共享 tsconfig.json,所以会提供完整的报错信息,ts-loader也与 vscode 提供的语法校验表现一致
而@babel/preset-typescript有的时候会无法提供完整的报错信息和类型提示
管理资源
tnpm i -D less less-loader style-loader css-loader sass sass-loader
//_webpack.config.js_...rules: [ { test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader', }, { test: /\.css$/, use: ['style-loader', 'css-loader'], }, { test: /\.tsx?$/, exclude: /node_modules/, loader: 'ts-loader', }, { test: /\.(less|css)$/, exclude: /\.module\.less$/, use: [ { loader: 'css-loader', options: { importLoaders: 2, sourceMap: !!DEV, }, }, { loader: 'less-loader', options: { sourceMap: !!DEV, }, }, ], }, { test: /\.(sass|scss)$/, use: [ { loader: 'css-loader', options: { importLoaders: 2, sourceMap: !!DEV, }, }, { loader: 'sass-loader', options: { sourceMap: !!DEV, }, }, ], }, ...
asset/resource 发送一个单独的文件并导出 URL。
asset/inline 导出一个资源的 data URI。
asset/source 导出资源的源代码。
//_webpack.config.js_...module: { rules: [{ test: /\.png/, type: 'asset/resource' }]},...
//_webpack.config.js_...{ test: /\.(csv|tsv)$/i, use: ['csv-loader'],},{ test: /\.xml$/i, use: ['xml-loader'],},...
搭建开发环境
//_webpack.config.js_...const { DEV, DEBUG } = process.env;process.env.BABEL_ENV = DEV ? 'development' : 'production';process.env.NODE_ENV = DEV ? 'development' : 'production';...mode: DEV ? 'development' : 'production',devtool: DEV && 'source-map',...
完善打包配置与缓存
//_webpack.config.js_...const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = { plugins: [ new CleanWebpackPlugin(), ]}...
webpack.prod.js 生产环境打包配置
output: { filename: 'js/[name].[contenthash:8].js', // contenthash:只有模块的内容改变,才会改变hash值 },
output: { filename: 'js/[name].[hash:8].js',}
性能优化
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');plugins: [ DEBUG && new BundleAnalyzerPlugin(),]
//_webpack.config.js_...const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');const TerserPlugin = require('terser-webpack-plugin');...optimization: { minimizer: [ new TerserPlugin({ parallel: false, terserOptions: { output: { comments: false, }, }, }), new OptimizeCSSAssetsPlugin({}), ], minimize: !DEV, splitChunks: { minSize: 500000, cacheGroups: { vendors: false, }, },},...
//_webpack.config.js_...entry: { index: './src/index.js', another: './src/another-module.js',},output: { filename: '[name].bundle.js'...
// _webpack.config.js_module.exports = { // ... mode: 'production',};
// _webpack.config.js_module.exports = { // ... mode: 'development', optimization: { usedExports: true, }};
// _test.js_// 这会被看作“活”代码,不会做 tree-shakingimport { add } from './math'console.log(add(5, 6))
// 导入但没有赋值给 JavaScript 对象,也没有在代码里用到// 这会被当做“死”代码,会被 tree-shakingimport { add, minus } from './math'console.log('hello webpack')
// 导入整个库,但是没有赋值给 JavaScript 对象,也没有在代码里用到// 非常奇怪,这竟然被当做“活”代码,因为 Webpack 对库的导入和本地代码导入的处理方式不同。import { add, minus } from './math' // 死的import 'lodash' // 活的console.log('hello webpack')
{ "name": "your-project", "sideEffects": ["./src/some-side-effectful-file.js", "*.css"]}
// _src/index.tsxconsole.log(join(['hello', 'webpack'], ' '))
// _webpack.config.js_plugins: [ new webpack.ProvidePlugin({ //_: 'lodash' // 如果没注释的话,需要这样引用console.log(_.join(['hello', 'webpack'], ' ')) join: ['lodash', 'join'], })]
// _webpack.config.js_module: { rules: [ { test: require.resolve('./src/index.js'), use: 'imports-loader?wrapper=window', }, ]},
//_webpack.config.js_...minimize: !DEV, splitChunks: { minSize: 500000, cacheGroups: { vendors: false, }, },...
minSize:形成一个新代码块最小的体积
//_index.tsx_...const WdAndDxEntry = lazy(() => import(/* webpackChunkName: "wd-and-dx" */ '../../old-code/component/wd-and-dx/entry'));const WdAndDxFallback = () => ()const SSRCompatibleSuspense = (props: Parameters<typeof Suspense>['0']) => { const isMounted = useMounted(); if (isMounted) { return <Suspense {...props} />; } return <>{props.fallback}</>;} ...return ( <SSRCompatibleSuspense fallback={<WdAndDxFallback />}> <WdAndDxEntry className="" data={data} style={{ height: 150, }} /> </SSRCompatibleSuspense>);
//_webpack.config.js_...module.exports = { entry: { index: { import: './src/index.js', dependOn: 'shared', }, another: { import: './src/another-module.js', dependOn: 'shared', }, shared: 'lodash', }}...
//_webpack.config.js_...const MiniCssExtractPlugin = require('mini-css-extract-plugin');...{ test: /\.(sass|scss)$/, use: [ { loader: MiniCssExtractPlugin.loader, }, { loader: 'css-loader', options: { importLoaders: 2, sourceMap: !!DEV, }, }, { loader: 'sass-loader', options: { sourceMap: !!DEV, }, }, ],},...DEBUG && new BundleAnalyzerPlugin(), new MiniCssExtractPlugin({ filename: '[name].css', chunkFilename: '[name].css',}),...
//_webpack.config.js_...module: { rules: [ { test: /\.tsx?$/, use: [ { loader: 'ts-loader', options: { transpileOnly: true } } ] } ]}...
webpack --mode=production --config ./build/webpack.config.jsHash: 36308e3786425ccd2e9dVersion: webpack 4.41.0Time: 2482msBuilt at: 12/20/2019 4:52:43 PM Asset Size Chunks Chunk Names 932 bytes 0 [emitted] main 338 bytes [emitted]Entrypoint main = app.js ./src/index.ts 14 bytes {0} [built]Child html-webpack-plugin for "index.html": 1 asset Entrypoint undefined = index.html ./node_modules/html-webpack-plugin/lib/loader.js!./index.html 489 bytes {0} [built] (webpack)/buildin/global.js 472 bytes {0} [built] (webpack)/buildin/module.js 497 bytes {0} [built] 1 hidden module Done in 4.88s.
webpack --mode=production --config ./build/webpack.config.jsHash: e5a133a9510259e1f027Version: webpack 4.41.0Time: 726msBuilt at: 12/20/2019 4:54:20 PMAsset Size Chunks Chunk Names 932 bytes 0 [emitted] main 338 bytes [emitted]Entrypoint main = app.js ./src/index.ts 14 bytes {0} [built]Child html-webpack-plugin for "index.html":1 assetEntrypoint undefined = index.html ./node_modules/html-webpack-plugin/lib/loader.js!./index.html 489 bytes {0} [built] (webpack)/buildin/global.js 472 bytes {0} [built] (webpack)/buildin/module.js 497 bytes {0} [built] 1 hidden module Done in 2.40s.
//_webpack.config.js_...module: { rules: [ { test: /\.tsx?$/, use: [ { loader: 'ts-loader', options: { transpileOnly: true } } ] } ] }, plugins: [ new ForkTsCheckerWebpackPlugin() ]...
用editorconfig统一编辑器规范
__.editorconfig__# http://editorconfig.orgroot = true
[*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.md]trim_trailing_whitespace = false
[makefile]indent_style = tabindent_size = 4
Antd配置
{ "presets": ["@babel/react", "@babel/env"], "plugins": [ "@babel/plugin-proposal-class-properties", [ "import", { "libraryName": "antd", "libraryDirectory": "es", "style": true // or 'css' }, "antd" ] ]}
module: { rules: [ // 处理 .css { test: /\.css$/, use: ['style-loader', 'css-loader'], }, // 处理 .less { test: /\.less$/, use: [ 'style-loader', 'css-loader', // less-loader { loader: 'less-loader', options: { lessOptions: { // 替换antd的变量,去掉 @ 符号即可 // https://ant.design/docs/react/customize-theme-cn modifyVars: { 'primary-color': '#1DA57A', }, javascriptEnabled: true, // 支持js }, }, }, ], }, ] }
ESlint配置
tnpm install -D eslint eslint-webpack-plugin @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react
eslint: eslint主要引擎
eslint-webpack-plugin: webpack loader
@typescript-eslint/parser: 帮助ESlint lint ts代码
@typescript-eslint/eslint-plugin: 包含TS扩展规则的插件
// _eslintrc_module.exports = { parser: '@typescript-eslint/parser', // ESlint Parser extends: [ 'plugin:react/recommended', // 从@eslint-plugin-react中选择推荐的规则 'plugin:@typescript-eslint/recommended', // 从@typescript-eslint/eslint-plugin选择推荐的规则 ], parserOptions: { ecmaVersion: 2018, // 帮助转化最先进的ECMAScript功能 sourceType: 'module', // 允许imports的用法 ecmaFeatures: { jsx: true, // JSX兼容 }, }, rules: { }, settings: { react: { version: 'detect', // 告诉eslint-plugin-react自动检测最新版本的react }, },};
Prettier配置
{ "arrowParens": "avoid", "bracketSpacing": true, "embeddedLanguageFormatting": "auto", "htmlWhitespaceSensitivity": "css", "insertPragma": false, "jsxBracketSameLine": true, "jsxSingleQuote": false, "printWidth": 100, "proseWrap": "preserve", "quoteProps": "as-needed", "requirePragma": false, "semi": true, "singleQuote": true, "tabWidth": 2, "trailingComma": "es5", "useTabs": true, "vueIndentScriptAndStyle": false}
代码提交规范
tnpm i -D pretty-quick prettier husky
pretty-quick: 配合git-hooks进行代码检测,并且fix
"pretty": "./node_modules/.bin/pretty-quick --staged"..."husky": { "hooks": { "pre-commit": "tnpm run pretty" } },
最后贴一下完整的配置,因为Aone发布自动更新版本号,所以不用拆分config文件来根据环境设置缓存,并且配置已经尽可能简化,拆分反而会增加维护成本。
//_webpack.config.js_//webpack.config.jsconst path = require('path');const HtmlWebpackPlugin = require('html-webpack-plugin');const MiniCssExtractPlugin = require('mini-css-extract-plugin');const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');const TerserPlugin = require('terser-webpack-plugin');const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');const ESLintPlugin = require('eslint-webpack-plugin');const { DEV, DEBUG } = process.env;process.env.BABEL_ENV = DEV ? 'development' : 'production';process.env.NODE_ENV = DEV ? 'development' : 'production';module.exports = {entry: './src/index.tsx',output: {path: path.join(__dirname, '/dist'),filename: 'bundle.js',clean: true,},devServer: {port: 8080,},mode: DEV ? 'development' : 'production',devtool: DEV && 'source-map',module: {rules: [{test: /\.jsx?$/,exclude: /node_modules/,loader: 'babel-loader',},{test: /\.tsx?$/,exclude: /node_modules/,loader: 'ts-loader',},{test: /\.css$/,use: ['style-loader', 'css-loader'],},// 处理 .less{test: /\.less$/,use: ['style-loader','css-loader',// less-loader{loader: 'less-loader',options: {lessOptions: {// 替换antd的变量,去掉 @ 符号即可// https://ant.design/docs/react/customize-theme-cnmodifyVars: {'primary-color': '#1DA57A','border-color-base': '#d9d9d9', // 边框色'text-color': '#d9d9d9'},javascriptEnabled: true, // 支持js},},},],},{test: /\.(sass|scss)$/,use: [{loader: MiniCssExtractPlugin.loader,},{loader: 'css-loader',options: {importLoaders: 2,sourceMap: !!DEV,},},{loader: 'sass-loader',options: {sourceMap: !!DEV,},},],},{test: /\.png/,type: 'asset/resource',},{test: /\.(woff|woff2|eot|ttf|otf)$/i,type: 'asset/resource',},{test: /\.(csv|tsv)$/i,use: ['csv-loader'],},{test: /\.xml$/i,use: ['xml-loader'],},],},optimization: {minimizer: [new TerserPlugin({parallel: false,terserOptions: {output: {comments: false,},},}),new OptimizeCSSAssetsPlugin({}),],minimize: !DEV,splitChunks: {minSize: 500000,cacheGroups: {vendors: false,},},},resolve: {modules: ['node_modules'],extensions: ['.json', '.js', '.jsx', '.ts', '.tsx', '.less', 'scss'],},plugins: [new HtmlWebpackPlugin({template: path.join(__dirname, '/src/index.html'),filename: 'app.html',inject: 'body',}),DEBUG && new BundleAnalyzerPlugin(),new MiniCssExtractPlugin({filename: '[name].css',chunkFilename: '[name].css',}),new ESLintPlugin(),new ForkTsCheckerWebpackPlugin(),].filter(Boolean),};
这篇文章主要记录了开发过程中从项目初始化开始,再到一个标准化前端项目的搭建路程。涉及相关代码规范、开发环境搭建、生产环境优化等,旨在打造出一个可快速使用的现代Webpack5.x+React18.x+Typescript+Antd4.x模板,以供在以后的实际业务场景需求中零成本使用。
推荐阅读
重磅来袭!2022上半年阿里云社区最热电子书榜单!
千万阅读量、百万下载量、上百本电子书,近200位阿里专家参与编写。多元化选择、全领域覆盖,汇聚阿里巴巴技术实践精华,读、学、练一键三连。开发者藏经阁,开发者的工作伴侣~
点击阅读原文查看详情。