从零开始搭建一个 react+typescript 脚手架

参考文章:我是这样搭建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 可以跨平台设置和使用环境变量,解决macwindow配置不同的问题
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


  
    
    
    <%=htmlWebpackPlugin.options.title %>
  
  
    

通过 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-loadercss-loader是必备的了,接下来如果是处理less文件,需要安装lessless-loader。处理sass需要安装node-sasssass-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 样式兼容

postcssbabel类似,我们也要安装一些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')]}
        }
}

这里可以发现cssless的编译配置差不多,所以这里封装成一个通用方法来配置,
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的原因,这里配置的baseUrlpaths别名还是会报错,解决这个问题还需要安装依赖包:

npm install eslint-import-resolver-typescript -D

修改 eslintrc.js 文件的 setting 字段:

settings: {
  'import/resolver': {
    node: {
      extensions: ['.tsx', '.ts', '.js', '.json'],
    },
    typescript: {},
  },
},

这里编译typescript用到的是@babel/preset-typescriptfork-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.tsxApp.tsx文件,输入下面内容测试:

  • index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(, document.querySelector("#root"));
  • App.tsx
import React from "react";

interface IProps {
    name: string;
    age: number;
}

function App(props: IProps) {
    const { name, age } = props;
    return (
        
{`Hello! I'm ${name}, ${age} years old.`}
); } 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 减少打包体积

我们其实并不想把 reactreact-dom打包进最终生成的代码中,这种第三方包一般会剥离出去或者采用 CDN 链接形式

修改 webpack.common.js ,增加以下代码:

module.exports = {
  externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
  },
}

可以通过两种方式引入

  • CDN 方式引入:


  
    
+ +
  • 本地引入:

public文件夹下新建lib文件夹,存放我们的公共文件:

public/
    index.html
    lib/
        react.production.min.js
        react-dom.production.min.js

DllPlugin

另外一种通过 dll 动态链接库的方式也可以达到减少打包体积的作用,这里不做示例了,推荐一步到位的autodll-webpack-plugin;

splitChunks

React 组件可以借助 React.lazyReact.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(false)

 return (
   
Loading...
}> {showTwo && }
) } 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,                           // 端口号设置
    }),
  ],
})

你可能感兴趣的