参考文章:我是这样搭建Typescript+React项目环境的
基本配置
基于 webpack 4+ 搭建
安装 webpack
:
npm install webpack@4 webpack-cli@3 -D
新建文件夹build
,用于保存配置:
mkdir build
接着在build
文件夹下新建这几个文件:
config.js
环境变量proxy.js
代理配置webpack.common.js
通用配置webpack.dev.js
开发环境配置webpack.prod.js
生产环境配置
$ cd build
$ touch config.js proxy.js webpack.common.js webpack.dev.js webpack.prod.js
接下来安装两个依赖包:
webpack-merge
可以将通用配置 webpack.common.js 和 开发环境 dev 或 生产环境 prod 的配置合并起来cross-env
可以跨平台设置和使用环境变量,解决mac
和window
配置不同的问题
npm install webpack-merge cross-env -D
修改 package.json
文件:
"scripts": {
"start": "cross-env NODE_ENV=development webpack-dev-server --config ./build/webpack.dev.js",
"build": "cross-env NODE_ENV=production webpack --config ./build/webpack.prod.js"
}
准备好构建需要的环境变量,修改config.js
:
const SERVER_PORT = 9527
const SERVER_HOST = '127.0.0.1'
const PROJECT_NAME = "cli"
const isDev = process.env.NODE_ENV !== 'production'
module.exports = {
isDev,
PROJECT_NAME,
SERVER_PORT,
SERVER_HOST
}
接下来准备好webpack
配置文件:
// webpack.common.js
const { resolve } = require('path')
module.exports = {
entry: resolve(__dirname,"../src/index.js"),
output: {
filename: 'js/bundle.[hash:8].js',
path: resolve(__dirname, '../dist'),
},
}
//webpack.dev.js
const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
module.exports = merge(common, {
mode: 'development',
})
//webpack.prod.js
const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
module.exports = merge(common, {
mode: 'production',
})
新建工程入口文件:
src/
- index.js
启动项目
要启动项目,有几个配置依赖包是必备的:
html-webpack-plugin
模板文件,将我们打包后的资源引入到 html 中webpack-dev-server
开启一个本地 http 服务,可以配置热更新、代理等。clean-webpack-plugin
清理文件夹,每次打包后先自动清理旧的文件copy-webpack-plugin
将资源文件复制到打包目录下
npm install html-webpack-plugin webpack-dev-server clean-webpack-plugin copy-webpack-plugin -D
模板文件配置
新建 public
文件夹,里面放我们的 html 模板文件:
mkdir public
touch index.html
//index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%=htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="root"></div>
</body>
</html>
通过 htmlWebpackPlugin
可以拿到配置的变量信息,接着修改webpack.common.js
:
const { resolve } = require('path')
const config = require("./config")
const CopyPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
entry: "../src/index.tsx",
output: {
filename: 'js/bundle.[hash:8].js',
path: resolve(__dirname, '../dist'),
},
plugins:[
new HtmlWebpackPlugin({
template: resolve(__dirname, '../public/index.html'),
filename: 'index.html',
title: config.PROJECT_NAME,
cache: false,
}),
new CopyPlugin({
patterns: [
{ from: resolve(__dirname, "../public"), to: resolve(__dirname, "../dist") }
],
}),
new CleanWebpackPlugin()
]
}
devServer 配置
代理
修改proxy.js
,配置代理:
const proxySetting = {
'/api/': {
target: 'http://localhost:3001',
changeOrigin: true,
},
//接口代理2
'/api-2/': {
target: 'http://localhost:3002',
changeOrigin: true,
pathRewrite: {
'^/api-2': '',
},
},
}
module.exports = proxySetting
devServer
修改 webpack.dev.js
:
const { merge } = require('webpack-merge');
const webpack = require('webpack');
const {resolve} = require("path");
const common = require('./webpack.common.js');
const proxySetting = require('./proxy');
const config = require('./config');
module.exports = merge(common, {
mode: 'development',
devServer: {
host: config.SERVER_HOST,
port: config.SERVER_PORT,
stats: 'errors-only',
clientLogLevel: 'silent',
compress: true,
open: false,
hot: true, // 热更新
proxy: { ...proxySetting }, // 代理配置
contentBase: resolve(__dirname, '../public')
},
plugins: [new webpack.HotModuleReplacementPlugin()],
});
devtool
devtool
可以将编译后的代码映射回原始源代码,方便我们调试错误代码,对我来说eval-source-map
是能够接受的调试模式,生产环境下直接禁用,修改文件如下:
//webpack.dev.js
module.exports = merge(common, {
mode: 'development',
+ devtool: 'eval-source-map',
})
//webpack.prod.js
module.exports = merge(common, {
mode: 'production',
+ devtool: 'none',
})
样式处理
style-loader
和css-loader
是必备的了,接下来如果是处理less
文件,需要安装less
和less-loader
。处理sass
需要安装node-sass
和sass-loader
,这里我用的是less
,所以安装:
npm install css-loader style-loader less less-loader -D
正常情况下我们配置两条rule
,针对css
文件和less
文件就好了:
// webpack.common.js
module.exports = {
// other...
module: {
rules: [
{test: /\.css$/,use: ['style-loader','css-loader']},
{test: /\.less$/,use: [
'style-loader',
{
loader:'css-loader',
options:{importLoaders:1}
},
'less-loader'
]
},
]
},
}
不过我们还是要处理样式兼容性问题和不同环境下的sourceMap
postcss 样式兼容
postcss
和babel类似,我们也要安装一些preset才能生效:
- postcss-flexbugs-fixes:用于修复一些和 flex 布局相关的 bug。
- postcss-preset-env:将最新的 CSS 语法转换为目标环境的浏览器能够理解的 CSS 语法,目的是使开发者不用考虑浏览器兼容问题。我们使用 autoprefixer 来自动添加浏览器头。
- postcss-normalize:从 browserslist 中自动导入所需要的 normalize.css 内容。
npm install postcss-loader postcss-flexbugs-fixes postcss-preset-env autoprefixer postcss-normalize -D
postcss
的配置如下:
{
loader: 'postcss-loader',
options: {
sourceMap: config.isDev, // 是否生成 sourceMap
postcssOptions: {
plugins: [
// 修复一些和 flex 布局相关的 bug
require('postcss-flexbugs-fixes'),
require('postcss-preset-env')({
autoprefixer: {grid: true,flexbox: 'no-2009'},
stage: 3,
}),
require('postcss-normalize')]}
}
}
这里可以发现css
和less
的编译配置差不多,所以这里封装成一个通用方法来配置,
build文件夹下新建utils.js
文件,用来存放封装的通用方法:
//utils.js
const {isDev} = require('./config')
exports.getCssLoaders = (importLoaders) => [
'style-loader',
{
loader: 'css-loader',
options: {
modules: false,
sourceMap: isDev,
importLoaders,
},
},
{
loader: 'postcss-loader',
options: {
sourceMap: isDev,
postcssOptions: {
plugins: [
// 修复一些和 flex 布局相关的 bug
require('postcss-flexbugs-fixes'),
require('postcss-preset-env')({
autoprefixer: {
grid: true,
flexbox: 'no-2009',
},
stage: 3,
}),
require('postcss-normalize'),
],
},
},
},
]
接着修改webpack.common.js
文件:
const {getCssLoaders} = require("./utils");
...
module:{
rules:[
{ test: /.(css)$/, use: getCssLoaders(1) },
{
test: /\.less$/,
use: [
...getCssLoaders(2),
{
loader: 'less-loader',
options: {sourceMap: config.isDev},
}
]
}
]
}
...
最后,还需要在 package.json
中添加 browserslist
:
{
"browserslist": [
">0.2%",
"not dead",
"ie >= 9",
"not op_mini all"
],
}
图片和字体文件处理
图片和其它资源文件处理比较简单,图片可以使用url-loader
处理,如果是小图或图标可以转成 base64,如果其它资源文件,通过file-loader
转成流的方式输出,先安装依赖包:
npm install file-loader url-loader -D
// webpack.common.js
module.exports = {
// other...
module: {
rules: [
// other...
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
use: [
{
loader: 'url-loader',
options: {
limit: 10 * 1024,
name: '[name].[contenthash:8].[ext]',
outputPath: 'assets/images',
},
},
],
},
{
test: /\.(ttf|woff|woff2|eot|otf|svg)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[contenthash:8].[ext]',
outputPath: 'assets/fonts',
},
},
],
},
]
},
plugins: [//...],
}
typescript
环境下还要先声明类型,这里再src/typings
下新建file.d.ts
文件,输入下面内容:
declare module '*.svg' {
const path: string
export default path
}
declare module '*.bmp' {
const path: string
export default path
}
declare module '*.gif' {
const path: string
export default path
}
declare module '*.jpg' {
const path: string
export default path
}
declare module '*.jpeg' {
const path: string
export default path
}
declare module '*.png' {
const path: string
export default path
}
react 和 typescript
先安装react
:
npm install react react-dom -S
安装babel
相关依赖:
npm install babel-loader @babel/core @babel/preset-env @babel/plugin-transform-runtime @babel/preset-react -D
npm install @babel/runtime-corejs3 -S
注意: @babel/runtime-corejs3 的安装为生产依赖。
- babel-loader 使用
babel
解析文件 - @babel/core
babel
核心模块 - @babel/preset-env 转换成最新的 javascript 规则
- @babel/preset-react 转译 jsx 语法
- @babel/plugin-transform-runtime 开发库/工具、移除冗余工具函数(helper function)
- @babel/runtime-corejs3 辅助函数
新建.babelrc
,输入以下代码:
{
"presets": [
[
"@babel/preset-env",
{
// 防止babel将任何模块类型都转译成CommonJS类型,导致tree-shaking失效问题
"modules": false
}
],
"@babel/preset-react"
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": {
"version": 3,
"proposals": true
},
"useESModules": true
}
]
]
}
修改 webpack.common.js
文件,增加以下代码:
module.exports = {
// other...
module: {
rules: [
{
test: /\.(tsx?|js)$/,
loader: 'babel-loader',
options: { cacheDirectory: true },
exclude: /node_modules/,
},
// other...
]
},
plugins: [ //... ],
}
babel-loader 在执行的时候,可能会产生一些运行期间重复的公共文件,造成代码体积大冗余,同时也会减慢编译效率,所以我们开启 cacheDirectory 将这些公共文件缓存起来,下次编译就会加快很多。
resolve.extensions 和 resolve.alias
extensions
扩展名识别alias
别名
给 webpack.common.js
新增 resolve
:
resolve: {
alias:{"@":resolve(__dirname, '../src')},
extensions: ['.tsx', '.ts', '.js', '.json'],
},
支持 typescript
修改src/index.js
文件为src/index.tsx
:
entry: {
app: resolve(__dirname, '../src/index.tsx'),
},
每个 Typescript 都会有一个 tsconfig.json
文件,其作用简单来说就是:
- 编译指定的文件
- 定义了编译选项
在控制台输入下面代码可以生成tsconfig.json
文件:
npx tsc --init
默认的tsconfig.json
的配置有点乱不好管理,这里推荐深入理解 TypeScript-tsconfig-json查看更多,打开tsconfig.json
,输入新的配置:
{
"compilerOptions": {
// 基本配置
"target": "ES5", // 编译成哪个版本的 es
"module": "ESNext", // 指定生成哪个模块系统代码
"lib": ["dom", "dom.iterable", "esnext"], // 编译过程中需要引入的库文件的列表
"allowJs": true, // 允许编译 js 文件
"jsx": "react", // 在 .tsx 文件里支持 JSX
"isolatedModules": true,
"strict": true, // 启用所有严格类型检查选项
"noImplicitAny": false, // 允许any类型
// 模块解析选项
"moduleResolution": "node", // 指定模块解析策略
"esModuleInterop": true, // 支持 CommonJS 和 ES 模块之间的互操作性
"resolveJsonModule": true, // 支持导入 json 模块
"baseUrl": "./", // 根路径
"paths": { // 路径映射,与 baseUrl 关联
"@/*": ["src/*"],
},
// 实验性选项
"experimentalDecorators": true, // 启用实验性的ES装饰器
"emitDecoratorMetadata": true, // 给源码里的装饰器声明加上设计类型元数据
// 其他选项
"forceConsistentCasingInFileNames": true, // 禁止对同一个文件的不一致的引用
"skipLibCheck": true, // 忽略所有的声明文件( *.d.ts)的类型检查
"allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入
"noEmit": true // 只想使用tsc的类型检查作为函数时(当其他工具(例如Babel实际编译)时)使用它
},
"exclude": ["node_modules"]
}
因为eslint
的原因,这里配置的baseUrl
和paths
别名还是会报错,解决这个问题还需要安装依赖包:
npm install eslint-import-resolver-typescript -D
修改 eslintrc.js 文件的 setting 字段:
settings: {
'import/resolver': {
node: {
extensions: ['.tsx', '.ts', '.js', '.json'],
},
typescript: {},
},
},
这里编译typescript
用到的是@babel/preset-typescript
和fork-ts-checker-webpack-plugin
@babel/preset-typescript
编译 ts 代码很粗暴,直接去掉 ts 的类型声明,再用其他 babel 插件进行编译fork-ts-checker-webpack-plugin
虽然用preset-typescript
编译简单粗暴速度快,但是启动和编译过程中控制台还是会缺少类型检查的错误提醒
安装插件:
npm install @babel/preset-typescript fork-ts-checker-webpack-plugin -D
给webpack.common.js
增加下面代码:
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
module.exports = {
plugins: [
// 其它 plugin...
new ForkTsCheckerWebpackPlugin({
typescript: {
configFile: resolve(__dirname, '../tsconfig.json'),
},
}),
]
}
给.babelrc
添加 preset-typescript
:
"presets": [
[
//...
"@babel/preset-typescript"
]
最后装上React
类型声明文件:
npm install @types/react @types/react-dom -D
测试
src
文件夹下新建index.tsx
和App.tsx
文件,输入下面内容测试:
- index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App age={12} name="test" />, document.querySelector("#root"));
- App.tsx
import React from "react";
interface IProps {
name: string;
age: number;
}
function App(props: IProps) {
const { name, age } = props;
return (
<div className="app">
<span>{`Hello! I'm ${name}, ${age} years old.`}</span>
</div>
);
}
export default App;
优化
显示编译进度
webpackbar
可以在启动或编译的时候显示打包进度
npm install webpackbar -D
在 webpack.common.js 增加以下代码:
const WebpackBar = require("webpackbar");
class Reporter {
done(context) {
if (config.isDev) {
console.clear();
console.log(`启动成功:${config.SERVER_HOST}:${config.SERVER_PORT}`);
}
}
}
module.exports = {
plugins: [
// 其它 plugin...
new WebpackBar({
name: config.isDev ? "正在启动" : "正在打包",
color: "#fa8c16",
reporter: new Reporter()
})
]
}
加快二次编译速度
hard-source-webpack-plugin
为程序中的模块(如 lodash)提供了一个中间缓存,放到本项目 node_modules/.cache/hard-source 下,首次编译时会耗费稍微比原来多一点的时间,因为它要进行一个缓存工作,但是再之后的每一次构建都会变得快很多
npm install hard-source-webpack-plugin -D
在 webpack.common.js
中增加以下代码:
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
module.exports = {
plugins: [
// 其它 plugin...
new HardSourceWebpackPlugin(),
]
}
external 减少打包体积
我们其实并不想把react
、react-dom
打包进最终生成的代码中,这种第三方包一般会剥离出去或者采用 CDN 链接形式
修改 webpack.common.js ,增加以下代码:
module.exports = {
externals: {
react: 'React',
'react-dom': 'ReactDOM',
},
}
可以通过两种方式引入
- CDN 方式引入:
<!DOCTYPE html>
<html lang="en">
<body>
<div id="root"></div>
+ <script crossorigin src="https://unpkg.com/react@16.13.1/umd/react.production.min.js"></script>
+ <script crossorigin src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
</body>
</html>
- 本地引入:
在public
文件夹下新建lib
文件夹,存放我们的公共文件:
public/
index.html
lib/
react.production.min.js
react-dom.production.min.js
DllPlugin
另外一种通过 dll 动态链接库的方式也可以达到减少打包体积的作用,这里不做示例了,推荐一步到位的autodll-webpack-plugin;
splitChunks
React 组件可以借助 React.lazy
和 React.Suspense
进行懒加载,具体可以看下面的示例:
import React, { Suspense, useState } from 'react'
const ComputedOne = React.lazy(() => import('Components/ComputedOne'))
const ComputedTwo = React.lazy(() => import('Components/ComputedTwo'))
function App() {
const [showTwo, setShowTwo] = useState<boolean>(false)
return (
<div className='app'>
<Suspense fallback={<div>Loading...</div>}>
<ComputedOne a={1} b={2} />
{showTwo && <ComputedTwo a={3} b={4} />}
<button type='button' onClick={() => setShowTwo(true)}>
显示Two
</button>
</Suspense>
</div>
)
}
export default App
通过懒加载的加载的组件会打出独立的 chunk
文件,为了让第三方依赖也打出来独立 chunk,需要在 webpack.common.js 中增加以下代码:
module.exports = {
// other...
externals: {//...},
optimization: {
splitChunks: {
chunks: 'all',
name: true,
},
},
}
热更新
前面 devServer 其实已经做了热更新的配置,但是修改 js 代码还是不能达到局部刷新的目的,这里还要在入口文件index.jsx
添加判断:
if (module && module.hot) {
module.hot.accept()
}
因为 ts 的缘故,会导致未声明的文件报错,这里还要安装@types/webpack-env
:
npm install @types/webpack-env -D
生产环境优化
样式处理
抽离样式
安装mini-css-extract-plugin
:
npm install mini-css-extract-plugin -D
build/utils.js
新增下面代码:
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const getCssLoaders = (importLoaders) => [
isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
// ....
]
webpack.prop.js
新增下面代码:
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
plugins: [
// 其它 plugin...
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].css',
ignoreOrder: false,
}),
]
}
去除无用样式
npm install purgecss-webpack-plugin glob -D
webpack.prop.js
新增下面代码:
const glob = require("glob");
const PurgeCSSPlugin = require('purgecss-webpack-plugin')
const { resolve } = require("path");
module.exports = merge(common, {
plugins: [
// ...
new PurgeCSSPlugin({
paths: glob.sync(`${resolve(__dirname, "../src")}/**/*.{tsx,scss,less,css}`, {
nodir: true
}),
whitelist: ["html", "body"]
})
],
})
代码压缩
npm install optimize-css-assets-webpack-plugin terser-webpack-plugin@4 -D
webpack.prop.js
新增下面代码:
const TerserPlugin = require("terser-webpack-plugin");
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = merge(common, {
//...
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
extractComments: false,
terserOptions: {
compress: { pure_funcs: ["console.log"] }
}
}),
new OptimizeCssAssetsPlugin()
]
},
plugins:[...]
})
添加包注释
webpack.prop.js
新增下面代码:
const webpack = require('webpack')
module.exports = merge(common, {
plugins: [
// ...
new webpack.BannerPlugin({
raw: true,
banner: '/** @preserve Powered by chenwl */',
}),
]
})
打包分析
npm install webpack-bundle-analyzer -D
webpack.prop.js
新增下面代码:
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
module.exports = merge(common, {
plugins: [
// ...
new BundleAnalyzerPlugin({
analyzerMode: 'server', // 开一个本地服务查看报告
analyzerHost: '127.0.0.1', // host 设置
analyzerPort: 8081, // 端口号设置
}),
],
})
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。