手动搭建一个支持TypeScript的React脚手架

1,698 阅读6分钟

用了很多次creat-react-app,对这个很好奇,于是就去摸了一下webpack,自己实现了一个React的脚手架。

我在windows操作系统下,搭建了一个简易的React脚手架,支持TypeScript,能处理CSS、图片、iconfont等静态资源,实现了对css的Tree-Shaking和压缩,拆分了开发环境和生产环境并安装了一些额外的插件来优化开发体验。

如果有大佬能指出一些可以改进的地方,我将感激不尽~

一、环境准备

1、环境

  • windows
  • node
  • vscode
  • git

2、项目初始化

首先新建一个项目文件夹,在命令行依次输入如下命令

npm init //然后一路回车
git init //没有安装git可以不要输入此命令,对整个过程没有影响
npm install webpack webpack-cli -D  //webpack版本为5.14.0
New-Item webpack.config.js -type file //创建'webpack.config.js'这个文件
// 创建src文件夹,用来存放打包的入口ts文件 start
mkdir src
cd src
New-Item index.ts -type file 
cd.. 
// 创建src文件夹,用来存放打包的入口ts文件 end

现在.项目结构如下:

.
├── node_modules
├── src
│   ├── index.ts
├── package.json
├── package-lock.json
└── webpack.config.js

3、配置npm run build命令

在package.json中加入如下代码

"scripts": {
  "build": "webpack"
}

这样,在运行npm run build的时候就会打包了。

4、配置webpack打包的入口和出口

复制下面代码到webpack.config.js

// webpack.config.js
const path = require('path');

module.exports = {
  entry: {
    app: './src/index.ts'
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: '[name].bundle.js',
  },
}

!!!注意webpack只负责将文件打包到dist目录下,当dist目录存在文件,重名的文件将覆盖,不重名的文件不删除。

5、配置webpack打包的dist要注入的html

① 安装插件

// webpack5是这样安装的,比webpack4多了一个@next
npm i --save-dev html-webpack-plugin@next

② 使用插件

// webpack.config.js
// 添加的第一行
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
   ----这里是其他代码----
  //添加的第二行
  plugins: [new HtmlWebpackPlugin({
      title: 'webpack is funny',
      template: path.resolve(__dirname, './public/index.html'),
      filename: 'index.html'
    })],
};

这个HtmlWebpackPlugin会自动以外链的方式注入dist下面的index.ts到public下面的index.html,title就是一个自定义的变量,可以传值。

③ 自定义html模板

在根目录下新建一个public文件夹,在里面新建一个index.html。

现在,假设我创建的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>

这样就算配置好了webpack打包的dist要注入的html

6、测试

在index.ts中输入下面的内容

var root = document.getElementById("root");
root.innerHTML = "hello world";

运行npm run build,目录的第一行出现一个dist文件夹

在浏览器中打开dist下面的index.html,出现hello world,代表初始化成功

二、引入TypeScript

为了使用TypeScript,要安装TypeScript

为了让webpack能在打包的时候把TypeScript转义成ES5,需要安装并配置ts-loader

1、安装TypeScript和ts-loader

npm install typescript  ts-loader --save-dev

2、配置TypeScript和ts-loader

①TypeScript配置

在根目录下新建一个tsconfig文件夹,什么也不写,自动采用了默认配置。

// 什么都不写

② ts-loader使用

module.exports = {
  //添加的第一段代码 start
  resolve: {
    extensions: ["", ".webpack.js", ".web.js", ".ts", ".tsx", ".js"],
  },
  //添加的第一段 end
  module: {
      rules: [
          { test: /\.tsx?$/, loader: "ts-loader", exclude: /node_modules/ },//添加的第二段代码
      ]
  }
}    

3、测试

改变index.ts的内容为下面这样:

let a: string = "hello world";
var root = document.getElementById("root");
root.innerHTML = a;

运行npm run build,还是输出hello world

三、引入React

1、安装

npm install react react-dom @types/react @types/react-dom

2、配置tsconfig.json

在tsconfig.json中添加如下内容

{
  "compilerOptions": {
    "esModuleInterop": true,  //表明支持import语法
    "jsx": "react",  //表明要使用react
    "sourceMap": true  
  }
}

3、测试

首先修改index.ts为index.tsx,表明这是使用ts的react代码。

然后修改index.tsx为下面的代码

import React from "react";
import ReactDom from "react-dom";

interface HelloProps {
  compiler: string;
  framework: string;
}

class App extends React.Component<HelloProps> {
  render() {
    return (
      <>
        <div className="app-text">
          {this.props.compiler} and {this.props.framework}!
        </div>
      </>
    );
  }
}

ReactDom.render(
  <App compiler="TypeScript" framework="React" />,
  document.getElementById("root")
);

运行npm run build,打开dist下的index.html,出现如下字样代表react注入成功。

四、引入CSS

想要打包css文件,需要用到style-loader/MiniCssExtractPlugin.loadercss-loader预处理CSS文件。

css-loader解析CSS导入,style-loader/MiniCssExtractPlugin.loader二选一,如果使用MiniCssExtractPlugin.loader还要安装MiniCssExtractPlugin

style-loaderMiniCssExtractPlugin.loader的区别是style-loader是将CSS注入到DOM,而MiniCssExtractPlugin.loader是将CSS单独打包成一个CSS文件。

1、安装

npm install --save--dev style-loader css-loader mini-css-extract-plugin

2、使用

两种方式二选一

  // 若想CSS注入到DOM
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ["style-loader", "css-loader"],
      },
    ],
  }
  // 若要要提取出CSS
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [MiniCssExtractPlugin.loader, 'css-loader'],
      },
    ],
  },
  plugins: [new MiniCssExtractPlugin({
      filename: "css/[name].css",//这里自定义输出的文件名
    })],
};

3、测试

首先,在src目录下创建一个index.css,输入下面内容

.app-text {
  color: red;
  background: skyblue;
  height: 100px;
}

index.tsx中,已经有一个app-text的div,我们只要导入

// index.css
import './index.css'

现在运行npm run build,打开dist目录下的index.html,浏览器出现如下字样和样式,代表引入css成功

五、引入图片

想要打包图片,就要使用file-loader或者url-loader。

file-loader会把图片单独打出来一个文件,url-loader会在图片打包时,把它打包成html内嵌的图片。

1、安装

npm install url-loader file-loader --save-dev

2、使用

打包图片的配置如下:

看样子它会在打包图片的时候使用url-loader,没有用到file-loader。其实当图片大于8192的时候就会使用默认的file-loader的配置,把图片单独打包出来。outputPath表明当使用file-loader的时候会把图片一起放到imgs目录下。

    rules: [
      {
        test: /\.(png|jpg|gif|svg|jpeg|bmp|tiff)$/i,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192,
              outputPath: "imgs",
            },
          },
        ],
      },
    ],

为了能在index.tsx下引入图片(TypeScript不认识图片),我们还需要在根目录下定义一个images.d.ts文件夹,输入下面内容

declare module '*.svg'
declare module '*.png'
declare module '*.jpg'
declare module '*.jpeg'
declare module '*.gif'
declare module '*.bmp'
declare module '*.tiff'

3、测试

index.tsx下加入两行代码

import Bear from "../public/imgs/bear.jpg"; //要加的
<div className="app-text">
          <img src={Bear} /> //要加的
          {this.props.compiler} and {this.props.framework}!
</div>

运行npm run build,dist目录下出现一个imgs文件夹,打开dist下面的index.html出现下面情况表明引入图片成功

六、使用iconfont

1、下载iconfont

www.iconfont.cn/

2、创建fonts文件夹

在public下创建一个fonts文件夹,放入下载好的.css|.eto|.svg|.ttf|woff五个文件(当然也可以全部放入,其他无用文件webpack不会理睬)

3、修改iconfont.css

看网上有些人在所有iconfont开头的url前加上一个"./",一共5个要加,其中两个是iconfont.eot开头,其实我试了一下不加,也行。

如果不用伪类的办法使用iconfont,只要在iconfont.css删除没有用的伪类代码就好了,这样可以减文件体积。如下面的放大镜,我就删掉了。

// 删除所有:before的元素,不用伪类的方法(也可以不用删除,然后使用的时候用伪类也可)
.iconfangdajing:before {
  content: "\e614";
}

4、配置webpack

   //webpack.config.js
    rules: [
      {
        test: /\.(woff|woff2|eot|ttf|otf|)$/,
        loader: "file-loader",
        options: {
          name: "[name].[ext]",
          outputPath: "fonts",
        },
      },
    ],

如果导入CSS使用的MiniCssExtractPlugin.loader,那么还要配置一下它的options.publicPath;如果是使用的style-loader,就不用做修改。

    rules: [
      {         
        test: /\.css$/i,
        use: [
          //"style-loader",
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              publicPath: "../",
            },
          },
          "css-loader",
        ],
      },
    ],

5、测试

在index.css中引入iconfont.css

@import "../public/fonts/iconfont.css";

在index.tsx中使用iconfont

  render() {
    return (
      <>
        <div className="app-text">
          <img src={Bear} />
          {this.props.compiler} and {this.props.framework}!
        </div>
        <span className="iconfont">&#xe614;</span> //这一行是添加的
      </>
    );
  }

运行npm run build,打开dist目录下的index.html,出现下面的情况表明iconfont使用成功

七、打包前删除dist

使用一个clean-webpack-plugin插件解决

1、安装

npm install --save-dev clean-webpack-plugin

2、使用

const { CleanWebpackPlugin } = require('clean-webpack-plugin');

    plugins: [
        new CleanWebpackPlugin(),
    ],

不测试了

八、优化命令行提示

1、安装

npm install friendly-errors-webpack-plugin --save-dev

2、使用

const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
  plugins: [
    new FriendlyErrorsWebpackPlugin(),
  ],

不测试了

九、支持CSS的最新语法

比方说动画、3D之类的

1、安装

npm install --save-dev postcss-loader postcss postcss-preset-env

2、使用

在webpack.config.js中

    rules: [
      {
        test: /\.css$/i,
        use: [
          // "style-loader",
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              publicPath: "../",
            },
          },
          "css-loader",
          "postcss-loader",//添加的那一行代码
        ],
      },
    ],

在根目录新建一个文件postcss.config.js

module.exports = {
  plugins: [
    [
      "postcss-preset-env",
      {
        // Options
      },
    ],
  ],
};

3、用CSS动画测试一下postcss-preset-env

在index.css中修改了一下图片大小,定义了一个动画给他

// index.css
@keyframes move {
  0% {
    transform: translateX(0px);
  }
  100% {
    transform: translateX(1000px);
  }
}

img {
  width: 100px;
  height: 100px;
  animation-name: move;
  animation-duration: 2s;
}

运行npm run build之后,dist下的css文件变成下面这样了,应该是使用默认配置实现了让浏览器支持某些高级特性

@-webkit-keyframes move {
  0% {
    transform: translateX(0px);
  }
  100% {
    transform: translateX(1000px);
  }
}

@keyframes move {
  0% {
    transform: translateX(0px);
  }
  100% {
    transform: translateX(1000px);
  }
}

img {
  width: 100px;
  height: 100px;
  -webkit-animation-name: move;
          animation-name: move;
  -webkit-animation-duration: 2s;
          animation-duration: 2s;
}

打开index.html之后,发现小熊图片从左边慢慢移到了靠右边的区域,然后回到原处

十、消除未使用的CSS

1、安装purgecss-webpack-plugin

npm install purgecss-webpack-plugin --save-dev

2、使用

purgecss-webpack-plugin的使用要依赖mini-css-extract-pluigin(已安装)

const glob = require('glob')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const PurgeCSSPlugin = require('purgecss-webpack-plugin')
const PATHS = {
  src: path.join(__dirname, 'src')
}
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader"
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "css/[name].css",
    }),
    new PurgeCSSPlugin({
      paths: glob.sync(`${srcPath}/**/*`, { nodir: true }),
    }),
  ]
}

3、测试

向src的index.css加了两个代码

结果是dist下的css文件剔除掉了这个代码

十一、压缩CSS代码

1、安装

npm install --save-dev optimize-css-assets-webpack-plugin

2、使用

var OptimizeCssAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin");
  plugins: [
    new OptimizeCssAssetsWebpackPlugin(),
  ],

然后打包后的CSS就被压缩成了一行

十二、开发环境和生产环境的拆分

1、首先拆分文件

在命令行输入下面内容

mkdir webpack-config
cd webpack-config
New-Item webpack.common.js -type file 
New-Item webpack.dev.js -type file 
New-Item webpack.prod.js -type file 
New-Item paths.js -type file 
cd.. 

在package.json里面配置命令

  "scripts": {
    "build": "webpack --config webpack-config/webpack.prod.js",
    "dev": "webpack serve  --config webpack-config/webpack.dev.js"
  },

2、安装webpack-dev-server实现HMR

实现HMR可以用监听模式、webpack-dev-server、webpack-dev-middleware,我选择了webpack-dev-server

 npm install webpack-dev-server --save--dev

3、拆分后的代码

① paths.js

const path = require("path");

const srcPath = path.join(__dirname, "..", "src");
const distPath = path.join(__dirname, "..", "dist");
const publicPath = path.join(__dirname, "..","public");

module.exports = {
  srcPath,
  distPath,
  publicPath,
};

② webpack.common.js

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
var OptimizeCssAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin");
const { srcPath, publicPath } = require("./paths");
const glob = require("glob");
const PurgeCSSPlugin = require("purgecss-webpack-plugin");

module.exports = {
  entry: path.join(srcPath, "index.tsx"),
  module: {
    rules: [
      { test: /\.tsx?$/, loader: "ts-loader", exclude: /node_modules/ },
      {
        test: /\.css$/i,
        use: [
          // "style-loader",
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              publicPath: "../",
            },
          },
          "css-loader",
          "postcss-loader",
        ],
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf|)$/,
        loader: "file-loader",
        options: {
          name: "[name].[ext]",
          outputPath: "fonts",
        },
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: "webpack is funny",
      template: path.join(publicPath, "index.html"),
      filename: "index.html",
    }),
    new MiniCssExtractPlugin({
      filename: "css/[name].css",
    }),
    new PurgeCSSPlugin({
      paths: glob.sync(`${srcPath}/**/*`, { nodir: true }),
    }),
    new OptimizeCssAssetsWebpackPlugin(),
  ],
};

③ webpack.dev.js

const webpackCommonConf = require("./webpack.common.js");
const merge = require("webpack-merge");
const { distPath } = require("./paths");
var FriendlyErrorsWebpackPlugin = require("friendly-errors-webpack-plugin");

module.exports = merge(webpackCommonConf, {
  devtool: "inline-source-map",
  module: "development",
  module: {
    rules: [
      // 直接引入图片 url
      {
        test: /\.(png|jpg|jpeg|gif|svg)$/,
        use: "file-loader",
      },
    ],
  },
  devServer: {
    historyApiFallback: true,
    contentBase: distPath,
    open: true,
    hot: true,
    quiet: true,
    port: 8082,
  },
  plugins: [new FriendlyErrorsWebpackPlugin()],
});

④ webpack.prod.js

const path = require("path");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const webpackCommonConf = require("./webpack.common.js");
const merge = require("webpack-merge");
const { distPath } = require("./paths");

module.exports = merge(webpackCommonConf, {
  mode: "production",
  output: {
    filename: "bundle.[contentHash:8].js", // 打包代码时,加上 hash 戳
    path: distPath,
  },
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif|svg|)$/i,
        use: [
          {
            loader: "url-loader",
            options: {
              limit: 8192,
              outputPath: "imgs",
            },
          },
        ],
      },
    ],
  },
  plugins: [new CleanWebpackPlugin()],
});


然后根目录下的那个webpack.config.js可以删除了

然后npm run dev或者npm run build可以分别使用开发环境和生产环境

十三、styled-components

webpack打包时默认样式都是全局的,社区上有很多方案来引入局部样式。个人比较喜欢styled-components。

1、安装

npm install --save styled-components

2、配置

在package.json中添加如下代码

{
  "resolutions": {
    "styled-components": "^5"
  }
}

3、使用

① 添加局部样式

import styled from "styled-components";
const Container = styled.div`
  width: 100vw;
  height: auto;
`;

<Container>这是一个带样式的div<Container>

styled-components怎么使用:

www.jianshu.com/p/20215e035…

zhuanlan.zhihu.com/p/29344146

② 添加全局样式

// 全局CSS
import { createGlobalStyle } from "styled-components";
export const Globalstyle = createGlobalStyle`
  你的CSS代码
`
// 在index.tsx中注入全局样式
const App = () => {
  return (
    <>
      <Globalstyle />
    </>
  );
};

十四、总结

基本功能

  • 引入TypeScript
  • 引入React
  • 引入CSS
  • 引入图片
  • 引入iconfont
  • 支持CSS最新特性

上述功能都是用loader实现的

css in js方案是styled-components

开发体验

  • 打包前删除dist——一个插件
  • 优化命令行提示——一个插件
  • 区分生产环境和开发环境

优化生产

  • 小图片内联、大图片打包出来——两个loader和一个插件
  • 消除未使用的CSS——Tree-Shaking
  • 压缩CSS代码——一个插件
  • 使用contentHash——长缓存