从零搭建React+ts+webpack项目

425 阅读7分钟

初始化项目

yarn init -y

安装 webpack

yarn add -D webpack webpack-cli webpack-dev-server

webpack 最好是安装在本项目中,避免与其它项目冲突

创建scripts文件夹,并新建base.config.js

const path = require('path');
module.exports = {
  entry: path.resolve(__dirname, '../src/index.js'),
  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: 'static/js/[name].[contenthash:8].js'
  }
};

创建src文件夹,并新建index.js

console.log('hello');

修改package.json文件中的打包命令

"scripts": {
  "dev": "webpack --config scripts/base.config.js --mode development"
}

添加 react

yarn add react react-dom

修改src/index.js

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(<div>Hello React</div>, document.getElementById('app'));

创建public文件夹, 并新建index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

添加 babel

yarn add -D @babel/core @babel/preset-env @babel/preset-react babel-loader

在根目录下创建babel.config.js

module.exports = {
  presets: [['@babel/preset-env', { targets: { ie: 11 } }], '@babel/preset-react'],
  plugins: []
};

配置 loader

module: {
  rules: [
    {
      test: /\.jsx?$/,
      use: 'babel-loader',
      exclude: /node_modules/
    }
  ];
}

添加html-webpack-plugin

yarn add -D html-webpack-plugin

添加打包配置

plugins: [
  new HtmlWebpackPlugin({
    template: path.resolve(__dirname, '../public/index.html'),
    filename: 'index.html',
    inject: 'body'
  })
];

环境配置

开发环境配置

引入webpack-merge

yarn add -D webpack-merge

创建dev.config.js文件

const { merge } = require('webpack-merge');
const baseConfig = require('./base.config');

const devConfig = {
  mode: 'development',
  devServer: {
    port: '9090',
    contentBase: path.resolve(__dirname, '../dist'),
    hot: true,
    open: true
  }
};

module.exports = merge(baseConfig, devConfig);

修改打包命令

"dev": "webpack serve --config scripts/dev.config.js"

线上环境配置

创建pro.config.js

const { merge } = require('webpack-merge');
const baseConfig = require('./base.config');
const preConfig = {
  mode: 'production'
};

module.exports = merge(baseConfig, preConfig);

修改打包命令

"build": "webpack --config scripts/pro.config.js"

使用 typescript

yarn add -D typescript ts-node @types/node @types/webpack @types/webpack-dev-server @types/react @types/react-dom @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/plugin-syntax-dynamic-import
  1. 添加tsconfig.json
tsc --init
  1. 修改tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "jsx": "react",
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src"]
}
  1. 修改base.config.js为 ts 文件
import * as path from 'path';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import { Configuration } from 'webpack';
const BaseConfig: Configuration = {
  entry: path.resolve(__dirname, '../src/index.tsx'),
  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: 'static/js/[name].[contenthash:8].js'
  },
  module: {
    rules: [
      {
        test: /\.(j|t)sx?$/,
        use: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../public/index.html'),
      filename: 'index.html',
      inject: 'body'
    })
  ]
};

export default BaseConfig;
  1. 修改dev.config.js为 ts 文件
import { merge } from 'webpack-merge';
import * as path from 'path';
import BaseConfig from './base.config';
import { Configuration } from 'webpack';

const devConfig: Configuration = {
  mode: 'development',
  devServer: {
    port: 9000,
    contentBase: path.resolve(__dirname, '../dist'),
    hot: true,
    open: true
  }
};

module.exports = merge(BaseConfig, devConfig);
  1. 修改pro.config.js为 ts 文件
import { merge } from 'webpack-merge';
import BaseConfig from './base.config';
import { Configuration } from 'webpack';

const config: Configuration = {
  mode: 'production'
};

const preConfig = merge(BaseConfig, config);

export default preConfig;
  1. 修改src/index.js为 tsx 文件
  2. 修改打包命令
"dev": "webpack serve --config scripts/dev.config.ts",
"build": "webpack --config scripts/pro.config.ts"
  1. 修改babel.config.js
module.exports = {
  //...
  plugins: [
    ['@babel/plugin-proposal-decorators', { legacy: true }], // 支持装饰器模式
    ['@babel/plugin-proposal-class-properties', { loose: true }],
    '@babel/plugin-syntax-dynamic-import' // 动态导入
  ]
};

css 打包配置

基础配置

使用 style-loader 和 css-loader

  1. css-loader 使你能够使用类似@import 和 url()的方法引入 css 文件
  2. style-loader 将所有的计算后的样式加入页面中
  3. 二者结合能够把样式表嵌入 webpack 打包后的 js 文件中
yarn add -D style-loader css-loader

修改base.config.ts中打包规则

{
  test: /\.css$/,
  use: ['style-loader', 'css-loader]
}

引入 sass

使用sass,需要引入sass-loadernode-sass

yarn add -D node-sass sass-loader
  1. 修改base.config.ts中的打包规则
{
  test: /\.(sc|c)ss$/,
  include: path.resolve(__dirname, '../src'),
  use: ['style-loader', 'css-loader', 'sass-loader']
}
  1. 配置公共sass属性 在 src 目录下新建 style 文件夹,并新建var.sass文件,写入样式变量
$pink: pink;
@mixin ellipsis {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

index.sass中引入变量

@import './style/var.scss';
div {
  color: $pink;
  width: 200px;
  @include ellipsis;
}

优化引入路径层级

{
  test: /\.(sc|c)ss$/,
  include: path.resolve(__dirname, '../src'),
  use: [
    'style-loader',
    'css-loader',
    {
      loader: 'sass-loader',
      options: {
        sassOptions: {
          includePaths: [path.resolve(__dirname, '../src/style')]
        }
      }
    }
  ]
}
// @import var.scss

引入 postcss

postcss 是一个 js 工具和插件转换 css 代码的工具

autoprefixer 自动获取浏览器的流行度和能够支持的属性,并根据这些属性自动添加 css 规则添加前缀 postcss preset env 将最新的 css 语法转换成大多数浏览器都能理解的语法

yarn add post-loader postcss-preset-env -D

修改打包规则

...
'css-loader',
'postcss-loader',
...

添加postcss.config.js配置

const presetenv = require('postcss-preset-env');
module.exports = {
  plugins: [
    presetenv({
      autoprefixer: {
        flexbox: 'no-2009'
      },
      stage: 3
    })
  ]
};

加载图片字体媒体文件

使用 url-loader 和 file-loader

yarn add -D url-loader file-loader

修改打包配置

{
  test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
  use: [
    {
      loader: 'url-loader',
      options: {
        limit: 10240,
        name: 'static/img/[name].[hash:8].[ext]'
      }
    }
  ]
},
{
  test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
  use: [
    {
      loader: 'file-loader',
      options: {
        limit: 10240,
        name: 'static/font/[name].[hash:8].[ext]'
      }
    }
  ]
},
{
  test: /\.mp3(\?.*)?$/,
  use: [
    {
      loader: 'file-loader',
      options: { name: 'static/media/[name].[hash:8].[ext]' }
    }
  ]
}

代码校验

使用 eslint prettier husky 和 lint-staged

yarn add -D eslint eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-prettier prettier eslint-config-prettier

配置编辑器设置

  1. 创建*.editorConfig*文件
# top-most EditorConfig file
# 表示最顶层的配置文件,发现设为true时,才会停止查找.editorconfig文件
root = true
# 匹配除路径分隔符(/)之外的任意字符
[*]
# 设置编码
charset = utf-8
# 设置换行符,值为lf(常用)、cr和crlf
end_of_line = lf
# 设置缩进风格(tab是硬缩进,space为软缩进)
indent_style = space
# 用一个整数定义的列数来设置缩进的宽度,如果indent_style为tab,则此属性默认为tab_width
indent_size = 2
# 设置为true以删除换行符前面的任何空白字符
trim_trailing_whitespace = true
# 设为true表示使文件以一个空白行结尾
insert_final_newline = true
  1. 创建*.eslintrc.js*
module.exports = {
  extends: ['eslint:recommended', 'plugin:prettier/recommended'],
  parserOptions: {
    ecmaVersion: 2019,
    sourceType: 'module'
  },
  env: {
    node: true,
    browser: true,
    commonjs: true,
    es6: true
  },
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint', 'react-hooks', 'prettier'],
  globals: {
    // 这里填入你的项目需要的全局变量
    // 这里值为 false 表示这个全局变量不允许被重新赋值,比如:
    // React: false,
    // ReactDOM: false
  },
  settings: {
    react: {
      pragma: 'React',
      version: 'detect'
    }
  },
  rules: {
    'prettier/prettier': 'error',
    // 这里填入你的项目需要的个性化配置,比如:
    'no-console': 'off',
    'no-unused-vars': [
      'warn',
      {
        vars: 'all',
        args: 'none',
        caughtErrors: 'none'
      }
    ],
    'max-nested-callbacks': 'off',
    'react/no-children-prop': 'off',
    'typescript/member-ordering': 'off',
    'typescript/member-delimiter-style': 'off',
    'react/jsx-indent-props': 'off',
    'react/no-did-update-set-state': 'off',
    'react-hooks/rules-of-hooks': 'error',
    'react-hooks/exhaustive-deps': 'warn',
    indent: [
      'off',
      2,
      {
        SwitchCase: 1,
        flatTernaryExpressions: true
      }
    ]
  }
};

  1. 创建 .prettierrc.js
module.exports = {
  // 一行最多 100 字符
  printWidth: 100,
  // 使用 2 个空格缩进
  tabWidth: 2,
  // 使用缩进符
  useTabs: false,
  // 行尾需要有分号
  semi: true,
  // 使用单引号
  singleQuote: true,
  // 对象的 key 仅在必要时用引号
  quoteProps: 'as-needed',
  // jsx 不使用单引号,而使用双引号
  jsxSingleQuote: false,
  // 末尾不需要逗号
  trailingComma: 'none',
  // 大括号内的首尾需要空格
  bracketSpacing: true,
  // jsx 标签的反尖括号需要换行
  jsxBracketSameLine: false,
  // 箭头函数,只有一个参数的时候,也需要括号
  arrowParens: 'always',
  // 每个文件格式化的范围是文件的全部内容
  rangeStart: 0,
  rangeEnd: Infinity,
  // 不需要写文件开头的 @prettier
  requirePragma: false,
  // 不需要自动在文件开头插入 @prettier
  insertPragma: false,
  // 使用默认的折行标准
  proseWrap: 'preserve',
  // 根据显示样式决定 html 要不要折行
  htmlWhitespaceSensitivity: 'css',
  // 换行符使用 lf
  endOfLine: 'lf'
};
  1. package.json中添加一下内容
"husky": {
  "hooks": {
    "pre-commit": "lint-staged"
  }
},
"lint-staged": {
"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [
    "prettier --write",
    "eslint --fix"
   ]
 }

打包优化

用到的依赖

  • webpack-merge: 合并webpack配置
  • html-webpack-plugin: 生成html文件
  • add-asset-html-webpack-plugin: 和html-webpack-plugin配合使用,把资源文件引用到生成的html中
  • mini-css-extract-plugin: 把css提取到单独的文件中
  • css-minimizer-webpack-plugin: 使用cssnano优化和压缩css
  • clean-webpack-plugin: 清理打包文件夹
  • webpack.DefinePlugin: 编译时创建一些全局变量
  • compression-webpack-plugin: gzip压缩
  • webpack.DllPlugin: 将模块预先编译,在第一次编译时将配置好的需要预先编译的模块编译在缓存中。第二次编译的时候,解析到这些模块就直接使用缓存
  • webpack.DllReferencePlugin: 将预先编译好的模块关联到当前编译中,当webpack解析到这些模块时,会直接使用预先编译好的模块
  • webpack-bundle-analyzer: 依赖分析

公共配置

  1. clean-webpack-plugin
yarn add -D clean-webpack-plugin

修改base.config.ts文件

import { CleanWebpackPlugin } from 'clean-webpack-plugin';
...
  new CleanWebpackPlugin()
...
  1. 公共变量 打开scripts目录,并新建env.ts文件
const envConfig = {
  dev: {
    BASEURL: 'devurl'
  },
  pro: {
    BASEURL: 'preurl'
  }
};

export default envConfig;

修改开发环境和线上环境打包配置

dev.config.ts添加plugins配置

plugins: [new DefinePlugin(envConfig.dev)]

pro.config.ts添加plugins配置

plugins: [new DefinePlugin(envConfig.pro)]
  1. css打包配置

使用mini-css-extract-plugin将css从js中分离出来,并支持chunk 使用css-minimizer-webpack-plugin优化和压缩css

yarn add -D mini-css-extract-plugin css-minimizer-webpack-plugin @types/mini-css-extract-plugin @types/css-minimizer-webpack-plugin

修改公共配置base.config.ts, 将style-loader改为MiniCssExtractPlugin.loader

import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import Cssminimizer from 'css-minimizer-webpack-plugin';
{
  module: {
    rules: [
      {
        // style-loader -> MiniCssExtractPlugin.loader
      }
    ]
  },
  // ...
  plugins: [
    // ...
    new MiniCssExtractPlugin({
      filename: 'static/css/[name].[contenthash:8].css',
      chunkFilename: 'static/css/[name].[id].[contenthash:8].css'
    })
  ],
  optimization: {
    minimize: true,
    minimizer: [new Cssminimizer()]
  }
}

分包

webpack.splitChunksPlugin 默认情况下,只会影响到按需加载的chunks。 webpack将根据一下条件自动拆分chunks:

  1. 新的chunk可以被共享,或者模块来自于node_modules文件夹
  2. 新的chunk体积大于20kb
  3. 当按需加载chunks时,并行请求的最大数量小于或等于30
  4. 当加载初始化页面时,并发请求的最大数量小于或等于30

默认配置

export default const config = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async', // 提取的 chunk 类型,all: 所有,async: 异步,initial: 初始
      // minSize: 30000, // 默认值,新 chunk 产生的最小限制 整数类型(以字节为单位)
      // maxSize: 0, // 默认值,新 chunk 产生的最大限制,0为无限 整数类型(以字节为单位)
      // minChunks: 1, // 默认值,新 chunk 被引用的最少次数
      // maxAsyncRequests: 5, // 默认值,按需加载的 chunk,最大数量
      // maxInitialRequests: 3, // 默认值,初始加载的 chunk,最大数量
      // name: true, // 默认值,控制 chunk 的命名
      cacheGroups: {
        // 配置缓存组
        vendor: {
          name: 'vendor',
          chunks: 'initial',
          priority: 10, // 优先级
          reuseExistingChunk: false, // 允许复用已经存在的代码块
          test: /node_modules/ // 只打包初始时依赖的第三方
        },
        common: {
          name: 'common',
          chunks: 'initial',
          // test: resolve("src/components"), // 可自定义拓展你的规则
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true
        }
      }
    }
  }
} 

gzip

yarn add -D compression-webpack-plugin @types/compression-webpack-plugin

修改pro.config.ts

plugins: [
  ...
  new CompressionPlugin()
  ...
]

DllPluginh和DllRefrencePlugin

将引入的基础包如reactreact-dom打到一个包,只要这些包的版本没升级,以后每次打包就不需要再编译这些模块,提高打包速率。

  1. scripts目录下创建一个webpack.dll.ts文件
import * as path from 'path';
import {} from 'webpack';
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
import webpack from 'webpack';
const DllConfig = {
  mode: 'production',
  entry: {
    vendor: ['react', 'react-dom']
  },
  output: {
    filename: '[name].dll.[hash:8].js',
    path: path.resolve(__dirname, '../dll'),
    // 链接库输出方式 默认'var'形式赋给变量
    libraryTarget: 'var',
    // 全局变量名称 导出库将被以var的形式赋给这个全局变量 通过这个变量获取到里面模块
    library: '_dll_[name]_[hash:8]'
  },
  plugins: [
    new CleanWebpackPlugin({
      cleanOnceBeforeBuildPatterns: [path.resolve(__dirname, '../dll')]
    }),
    new webpack.DllPlugin({
      // path 指定manifest文件的输出路径
      path: path.resolve(__dirname, '../dll/[name].manifest.json'),
      // 和library 一致,输出的manifest.json中的name值
      name: '_dll_[name]_[hash:8]'
    })
  ]
};
export default DllConfig;

  1. 添加命令,并执行
"dll": "webpack --config scripts/dll.config.ts"
  1. 使用DllReferencePlugin告诉webpack使用哪些动态链接,然后使用add-asset-html-webpack-plugin将DllPlugin生成的文件引入到生成的html中 修改pro.config.ts
import 
  plugins: [
    ...
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, '../dll/vendor.manifest.json')
    }),
    new AddAssetHtmlPlugin({
      filepath: path.resolve(__dirname, '../dll/**/*.js')
    })
  ]

依赖分析

如果要分析打包之后依赖的占比,可以使用webpack-bundle-analyzer

  1. 引入webpack-bundle-analyzer
yarn add -D webpack-bundle-analyzer @types/webpack-bundle-analyzer
  1. scripts目录下,新建analyzer.config.ts
import { merge } from 'webpack-merge';
import { Configuration } from 'webpack';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import proConfig from './pro.config';
const config: Configuration = {
  plugins: [new BundleAnalyzerPlugin()]
};

const AnalyzerConfig = merge(proConfig, config);

export default AnalyzerConfig;

  1. package.json中添加命令
"analyzer": "webpack --config scripts/analyzer.config.ts"