react + webpack + typescript 搭建自己的脚手架


前言

市场上基于 react 的脚手架很多,用起来也很方便,比如:create-react-app、ant design pro、next.js等,每个脚手架都有自己突出的地方,但大体用到的技术都差不多,要想真正理解脚手架的运行原理,就是亲自打造一个。本项目git地址

初始化

这里要用到 node.js ,没有的同学可以自行去下载安装。
创建目录,并初始化node工程。

$ mkdir empty-scaffold
$ cd empty-scaffold

$ npm init

然后一路enter,最后输入yes即可。新建的工程里只有一个 package.json 文件。

webpack 安装配置

第一步、安装webpack的配置:

$ npm i --save-dev webpack webpack-cli html-webpack-plugin terser-webpack-plugin webpack-dev-server webpack-merge

简单解释各个模块的作用:

  • webpack-cli: webpack 的命令行接口
  • html-webpack-plugin: 生成 HTML 模板,简易模板配置步骤
  • webpack-dev-server: webpack 本地服务
  • webpack-merge: 合并多个 wenpack 配置文件,由于要配置多个环境,所以需要合并多个配置文件
    安装好 webpack 模块后,下面就是配置环境,我们先配置一个开发环境。

第二步、创建 webpack 存放配置文件的目录

在跟目录新建 webpack 文件夹,用来存放其配置文件,然后新建 webpack.base.config.jswebpack.dev.config.jswebpack.prod.config.js 文件,此时目录结构是:

|-- empty-scaffold
    |-- package-lock.json
    |-- package.json
    |-- webpack
        |-- webpack.base.config.js
        |-- webpack.dev.config.js
        |-- webpack.prod.config.js

第三步、简单配置 webpack.base.config.js

先简单配置 webpack.base.config.js 文件,使其可以运行即可:

const path = require('path');

module.exports = {
  entry: path.join(__dirname, '../src/index.js'), // 入口文件,/src/index.js
}

第四步、配置 webpack.dev.config.js

const WebpackMerge = require("webpack-merge");
const baseWebpackConfig = require("./webpack.base.config");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = WebpackMerge.merge(baseWebpackConfig, {
  // 指定构建环境
  mode: "development",
  plugins: [
    // 配置输出的HTML
    new HtmlWebpackPlugin({
      filename: "index.html",
      template: "public/index.html",
      inject: true
    }),
  ],
  // 开发环境本地启动的服务配置
  devtool: 'eval-source-map',
  devServer: {
    host: 'localhost',
    port: 8089,
    // 要求每次都返回HTML,不配置会出现can not GET/
    historyApiFallback: true,
    hot: true
  }
});

上面的配置为本地开发环境的配置,主要功能是:

  1. 开启本地服务,指定端口为8089
  2. 配置输出的HTML

第五步、创建src文件夹,作为存放业务代码的地方

在跟目录创建src文件夹,然后创建 index.js 文件,作为打包的入口文件。
此时的目录结构:

|-- empty-scaffold
    |-- package-lock.json
    |-- package.json
    |-- public
    |   |-- index.html
    |-- src
    |   |-- index.js
    |-- webpack
        |-- webpack.base.config.js
        |-- webpack.dev.config.js
        |-- webpack.prod.config.js

第六步、创建启动命令

package.jsonscripts 中,创建命令:

  • start 为本地开发启动的命令
  • build 是打包项目的命令
    {
        // ...
        "scripts": {
          "start": "webpack-dev-server --config webpack/webpack.dev.config.js --open",
          "build": "webpack --config webpack/webpack.prod.config.js",
          "test": "echo "Error: no test specified" && exit 1"
        }
        // ...
    }

第七步、启动本地开发环境

运行:

$ npm start

在 index.js 中,输入 console.log('hello),在浏览器中可以看到答应的字符串。

安装配置 React

安装 React

运行:

$ npm install --save react react-dom

此时还无法运行,react需要经过 babel 编译才能执行。下面安装 babel。

安装配置 babel

运行:

$ npm install --save-dev @babel/core babel-loader @babel/preset-env @babel/preset-react @babel/plugin-transform-runtime @babel/runtime-corejs3

模块简单说明:

  • babel-loader: 告诉 webpack 使用 babel 解析 jsx 或 tsx 文件
  • @babel/core: 将 js 代码分析成 ast,方便各个插件分析语法进行相应处理,官方文档
  • @babel/preset-env: preset 本身是预设的意思,它是一系列预设的合集,作用很多,简单的作用是允许我们使用很多js语法,比如es6的const、let、箭头函数等,详细用法可以看官网
  • @babel/preset-react: 对 react 的 JSX 语法进行转化
  • @babel/plugin-transform-runtime: 由于 babel 是以 polyfill(补丁)方式来处理一些特定的代码,不使用此包时通常直接引入全部补丁,导致代码体积变大;使用 @babel/plugin-transform-runtime 可以使将补丁改为统一引入,缩减代码体积
    下面将配置 webpack:
  1. 在跟目录创建 babel.config.js 文件,用来配置 babel
    module.exports = {
      presets: [ // 预设
        '@babel/preset-env',
        '@babel/preset-react'
      ],
      plugins: [ // 插件
        [
          '@babel/plugin-transform-runtime',
          {
            'corejs': 3
          }
        ],
      ]
    }
  2. 修改 webpack 配置,使其使用 babel-loader 处理 JSX 语法
    修改 webpack.base.config.js :
    const path = require('path');
    
    const resolve = (link) => path.resolve(__dirname, link);
    module.exports = {
      entry: path.join(__dirname, '../src/index.jsx'),
      module: { // 模块
        rules: [
          {
            test: /.jsx$/,
            use: [
              {
                loader: 'babel-loader', // 使用 babel-loader
              }
            ],
            include: [resolve('../src')],
            exclude: /node_modules/
          }
        ]
      }
    }
    修改 index.js 为 index.jsx,修改里面代码:
    import React, { useEffect } from 'react';
    import ReactDom from 'react-dom';
    
    const Index = () => {
      useEffect(() => {
        console.log('hello')
      }, [])
      return (
        <div>HELLO</div>
      )
    }
    
    ReactDom.render(
      <Index />,
      document.getElementById('root')
    )
    运行代码:
    $ npm start
    可以看到正常运行:

image.png

到此,简单的 react 脚手架就搭好了,但是里面还缺失很多常用功能,比如:css文件配置、less文件配置、图片等静态资源、lint、typescript等,下面将逐个安装配置。

loader 配置

安装加载各种 loader

运行:

$ npm install --save-dev css-loader style-loader url-loader post-loader less less-loader autoprefixer

重新配置 webpack.base.config.js

const path = require('path');

const lessRegex = /.less$/;
const lessModuleRegex = /.module.less$/;
const resolve = (link) => path.resolve(__dirname, link);
module.exports = {
  entry: path.join(__dirname, '../src/index.jsx'),
  module: {
    rules: [
      {
        test: /.jsx$/,
        use: [
          {
            loader: 'babel-loader',
          }
        ],
        include: [resolve('../src')],
        exclude: /node_modules/
      },
      {
        test: /.css$/,
        exclude: /node_modules/,
        use: [
          // 注意loader生效是从下往上的
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: lessRegex,
        exclude: lessModuleRegex,
        use: [
          'style-loader',
          'css-loader',
          'postcss-loader',
          {
            loader: 'less-loader',
          },
        ]
      },
      {
        test: lessModuleRegex,
        use: [
          'style-loader',
          'css-loader',
          'postcss-loader',
          {
            loader: 'less-loader',
          },
        ]
      },
      {
        test: /.(png|jpe?g|gif|svg)(?.*)?$/,
        use: [{
          loader: 'url-loader',
          options: {
            //1024 == 1kb
            //小于10kb时打包成base64编码的图片否则单独打包成图片
            limit: 10240,
            name: path.join('img/[name].[hash:7].[ext]')
          }
        }]
      },
      {
        test: /.(woff2?|eot|ttf|otf)(?.*)?$/,
        use: [{
          loader: 'url-loader',
          options: {
            limit: 10240,
            name: path.join('font/[name].[hash:7].[ext]')
          }
        }]
      }
    ]
  }
}

注意:有安装 postcss-loader,其作用是自动补全 css 前缀,比如:ranslate,谷歌中会补全为 -webkit-transition,但需要配置 postcss-loader 使其生效,方法类似babel,在跟目录新建 postcss.config.js,将下面代码复制进去即可

module.exports = {
  plugins: [
    require('autoprefixer')
  ]
};

在 src 下新建 global.less 进行测试随便写入 css 并在 index.jsx 中引入

目录结构

|-- empty-scaffold
    |-- babel.config.js
    |-- package-lock.json
    |-- package.json
    |-- postcss.config.js
    |-- public
    |   |-- index.html
    |-- src
    |   |-- global.less
    |   |-- index.jsx
    |-- webpack
        |-- webpack.base.config.js
        |-- webpack.dev.config.js
        |-- webpack.prod.config.js

测试现有代码

运行:

$ npm start

image.png
可以看到正常加载 .less 文件,并且 css 自动添加前缀。

安装配置 typescript

安装 typescript

$ npm install --save typescript
$ npm install --save-dev @babel/preset-typescript @types/react

配置 ts

  1. 修改 babel.config.js
    module.exports = {
      presets: [
        '@babel/preset-env',
        '@babel/preset-react',
        '@babel/preset-typescript'
      ],
      plugins: [
        [
          '@babel/plugin-transform-runtime',
          {
            'corejs': 3
          }
        ],
      ]
    }
  2. 修改 webpack.base.config.js,两处地方
    // ...
    module.exports = {
        // ...
        module: {
            rules: [
                {
                  test: /.(j|t)sx?$/,  // 1. 修改此处
                  use: [
                    {
                      loader: 'babel-loader',
                    }
                  ],
                  include: [resolve('../src')],
                  exclude: /node_modules/
                }
                // ...
            ]
        },
        resolve: { // 2. 添加这里代码
          extensions: ['.ts', '.tsx', '.js', '.jsx'],
        }
    }
  3. 添加 typescript 配置文件
    新建 tsconfig.json
    {
      "compilerOptions": {
        /* Basic Options */
        "target": "es5",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
        "module": "esnext",                       /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
        "lib": [
          "dom",
          "dom.iterable",
          "esnext"
        ],                                        /* Specify library files to be included in the compilation. */
        "allowJs": true,                          /* Allow javascript files to be compiled. */
        "jsx": "react",                           /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
        "sourceMap": true,                        /* Generates corresponding '.map' file. */
        "outDir": "./dist",                       /* Redirect output structure to the directory. */
        "isolatedModules": false,                  /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
        "resolveJsonModule": true,
        "forceConsistentCasingInFileNames": true,
        "skipLibCheck": true,
        "strict": true,                           /* Enable all strict type-checking options. */
        "noImplicitThis": true,                   /* Raise error on 'this' expressions with an implied 'any' type. */
        "noImplicitReturns": true,                /* Report error when not all code paths in function return a value. */
        "moduleResolution": "node",               /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
        "baseUrl": ".",                       /* Base directory to resolve non-absolute module names. */
        "allowSyntheticDefaultImports": true,     /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
        "esModuleInterop": true,                  /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
        "experimentalDecorators": true,           /* Enables experimental support for ES7 decorators. */
        "paths": {
    
        }
      },
      "include": [
        "src"
      ],
      "exclude": [
        "node_modules"
      ]
    }
    OK,到此 typescript 配置完成,可修改 src 下的文件为 tsx 进行测试

此时目录结构

|-- empty-scaffold',
      |-- babel.config.js',
      |-- directoryList.md',
      |-- package-lock.json',
      |-- package.json',
      |-- postcss.config.js',
      |-- tsconfig.json',
      |-- public',
      |   |-- index.html',
      |-- src',
      |   |-- global.less',
      |   |-- index.jsx',
      |   |-- pages',
      |       |-- Home',
      |           |-- index.tsx',
      |-- webpack',
          |-- webpack.base.config.js',
          |-- webpack.dev.config.js',
          |-- webpack.prod.config.js',

配置生产环境

修改 webpack/webpack.pro.config.js

const path = require('path');
const WebpackMerge = require("webpack-merge");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const baseWebpackConfig = require("./webpack.base.config");
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');

module.exports = WebpackMerge.merge(baseWebpackConfig, {
  // 指定构建环境
  mode: "production",
  // 输出位置
  output: {
    filename: "[name].bundle.js",
    path: path.join(__dirname, './../dist'),
    chunkFilename: "[chunkhash:8].js"
  },
  // 插件
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      filename: "index.html",
      template: "public/index.html",
      inject: true, // true:默认值,script标签位于html文件的 body 底部
      hash: true, // 在打包的资源插入html会加上hash
      minify: {
        removeComments: true,               //去注释
        collapseWhitespace: true,           //压缩空格
        removeAttributeQuotes: true         //去除属性引用
      }
    })
  ],
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        extractComments: false, // 不将注释提取到单独文件中
      })
    ]
  }
})

运行:

$ npm run build

会在跟目录下生产 dist 文件夹,即打包后的文件,可运行 index.html

最终目录结构

|-- empty-scaffold
    |-- babel.config.js
    |-- directoryList.md
    |-- package-lock.json
    |-- package.json
    |-- postcss.config.js
    |-- tsconfig.json
    |-- dist
    |   |-- index.html
    |   |-- main.bundle.js
    |-- public
    |   |-- index.html
    |-- src
    |   |-- global.less
    |   |-- index.jsx
    |   |-- pages
    |       |-- Home
    |           |-- index.tsx
    |-- webpack
        |-- webpack.base.config.js
        |-- webpack.dev.config.js
        |-- webpack.prod.config.js

下节将引入React路由、ant design


文章作者: Icon
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Icon !
评论
  目录