react + redux + webpack+typescript 环境搭建

1,241 阅读3分钟

package.json

{
  "name": "react-webpack-typescript-init",
  "version": "1.0.0",
  "description": "react webapck typescript",
  "main": "src/index.tsx",
  "repository": "git@git.threatbook-inc.cn:zoushijun/react-webpack-typescript-init.git",
  "author": "zsj <zoushijun@threatbook.cn>",
  "license": "MIT",
  "scripts": {
    "dll": "./node_modules/.bin/webpack --config ./config/webpack/webpack.dll.js",
    "build": "./node_modules/.bin/webpack --env production --config ./config/webpack/webpack.config.js && node express.js",
    "start": "webpack-dev-server --env development --config ./config/webpack/webpack.config.js"
  },
  "dependencies": {
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "typescript": "^3.7.4"
  },
  "devDependencies": {
    "@babel/core": "^7.7.7",
    "@babel/preset-env": "^7.7.7",
    "@babel/preset-react": "^7.7.4",
    "@types/react": "^16.9.17",
    "@types/react-css-modules": "^4.6.2",
    "@types/react-dom": "^16.9.4",
    "add": "^2.0.6",
    "add-asset-html-webpack-plugin": "^3.1.3",
    "babel-loader": "^8.0.6",
    "babel-plugin-react-css-modules": "^5.2.6",
    "css-loader": "^3.4.0",
    "express": "^4.17.1",
    "fork-ts-checker-webpack-plugin": "^4.0.0-beta.4",
    "generic-names": "^2.0.1",
    "happypack": "^5.0.1",
    "html-webpack-plugin": "^3.2.0",
    "os": "^0.1.1",
    "postcss-styl": "^0.5.1",
    "shelljs": "^0.8.3",
    "style-loader": "^1.1.2",
    "stylus": "^0.54.7",
    "stylus-loader": "^3.0.2",
    "sugarss": "^2.0.0",
    "ts-loader": "^6.2.1",
    "webpack": "^4.41.4",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.10.1",
    "webpack-merge": "^4.2.2",
    "yarn": "^1.21.1"
  }
}

基本配置

初始化项目

1, 新建一个项目prod。
2, cd prod, mkdir src 和 mkdir dist
3, npm init
这里遇到了一个小坑,在目录里边添加了一个src目录,git一直检测不到,我还以为我建的git仓库有问题呢,后来想到git是通过文件的变化来检测的,如果只是建立了目录,里边没有,git是不会检测到变化的。

安装项目依赖

1, yarn add react react-dom
2, yarn add webpack webpack-cli webpack-dev-server -D
3, yarn add ts-loader source-map-loader -D (source-map-loader 为了调试方便,可以深入的到源码内部)
4, yarn add @types/react @types/react-dom -D (TS 不知道 React、 React-dom 的类型,以及该模块导出了什么,此时需要引入 .d.ts 的声明文件)
5,yarn add html-webpack-plugin -D

建立项目基本文件结构

1,新建src/index.html

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


2, 新建./src/index.tsx

import * as React from "react";
import * as ReactDOM from "react-dom";

const ROOT = document.getElementById("container");

import { HelloWorld } from "./components/HelloWorld";

ReactDOM.render(<HelloWorld firstName="Chris" lastName="Parker" />, ROOT);

3,在src/components 里边新建Helloworld.tsx

import * as React from "react";

export interface HelloWorldProps {
  firstName: string;
  lastName: string;
}

export const HelloWorld = (props: HelloWorldProps) => (
  <h1>
    Hi there from React! Welcome {props.firstName} and {props.lastName}!
  </h1>
);

4,新建tsconfig.json,告诉webapack去寻找typescript 文件

{
  "compilerOptions": {
    "jsx": "react",
    "module": "commonjs",
    "noImplicitAny": true,
    "outDir": "./dist/",
    "preserveConstEnums": true,
    "removeComments": true,
    "sourceMap": true,
    "target": "es5"
  },
  "include": ["./src/**/**/*"]
}

5,新建 webpack.config.js

const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  entry: "./src/index.tsx",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bundle.js"
  },
  devtool: "source-map",
  mode: "development",
  resolve: {
    extensions: [".js", ".ts", ".tsx"]
  },

  module: {
    rules: [
      {
        test: /\.tsx?$/,
        loader: "ts-loader"
      },
      { enforce: "pre", test: /\.js$/, loader: "source-map-loader" }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "src", "index.html")
    })
  ]
};

6, 修改package.json

"build": "./node_modules/.bin/webpack"

7, 执行yarn run build

webpack 热更新

安装依赖

1, yarn add webpack-dev-server -D

修改package.json

"start:dev": "webpack-dev-server"

增加webpack.config.js配置

devServer: {
    port: 3001,
    hot: true
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        loader: "ts-loader"
      }
      // include: path.resolve(__dirname, "./src"),
      // exclude: "./node_modules/",
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "src", "index.html")
    }),
    new webpack.HotModuleReplacementPlugin()
  ]

error

1, Property 'hot' does not exist on type 'Module'

解决办法: 使用以下方法:

if ((module as any).hot) {}

使用css,并且使用局域化css

1, 安装依赖

yarn add style-loader css-loader stylus-loader stylus babel-plugin-react-css-modules -D(简单来说就是翻译文件,把style文件翻译为css文件)
2, yarn add happypack -D (并行化的tsx文件)
3, yarn add babel-loader -D (为了使用babel-plugin-react-css-modules)
4, yarn add generic-names -D (生成name)
5, yarn add os -D
6, yarn add @babel/core -D
7, yarn add fork-ts-checker-webpack-plugin -D (配合happypack使用typescript)

修改webpack.config.js

const path = require("path");
const webpack = require("webpack");
const genericNames = require("generic-names");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const os = require("os");
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
const HappyPack = require("happypack");
const happyThreadPool = HappyPack.ThreadPool({
  size: os.cpus().length
});

const stylModuleRegex = /\.cssmodule\.styl$/;
const localIdentName = "[name]__[local]-[hash:base64:5]";
const generateScope = genericNames(localIdentName, {
  context: process.cwd()
});

const getStyleLoaders = (cssOptions, preProcessor) => {
  const loaders = [
    require.resolve("style-loader"),
    // MiniCssExtractPlugin.loader,
    {
      loader: require.resolve("css-loader"),
      options: cssOptions
    }
  ];
  if (preProcessor) {
    loaders.push(require.resolve(preProcessor));
  }
  return loaders;
};
module.exports = {
  entry: "./src/index.tsx",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bundle.js"
  },
  devtool: "inline-source-map",
  mode: "development",
  resolve: {
    extensions: [".tsx", ".ts", ".js", ".styl"]
  },
  devServer: {
    port: 3001,
    hot: true
  },
  module: {
    rules: [
      {
        oneOf: [
          {
            test: stylModuleRegex,
            use: getStyleLoaders(
              {
                importLoaders: 1,
                modules: true,
                camelCase: "dashes",
                getLocalIdent({ resourcePath }, localIdentName, localName) {
                  return generateScope(localName, resourcePath);
                }
              },
              "stylus-loader"
            )
          },
          {
            test: /\.css$/,
            include: path.resolve(__dirname, "./src"),
            use: ["style-loader", "css-loader"]
          }
        ]
      },
      {
        test: /\.tsx?$/,
        include: path.resolve(__dirname, "./src"),
        exclude: [path.resolve("./node_modules/")],
        use: ["happypack/loader?id=happybabel"]
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "src", "index.html")
    }),
    new webpack.HotModuleReplacementPlugin(),
    new ForkTsCheckerWebpackPlugin(),
    new HappyPack({
      id: "happybabel",
      loaders: [
        {
          loader: "babel-loader",
          options: {
            // babelrc: false,
            // configFile: false,
            // 设置缓存
            cacheDirectory: true,
            exclude: [path.resolve("./node_modules/")],
            plugins: [
              [
                "react-css-modules",
                {
                  // 生成css名字样式
                  generateScopedName: "[name]__[local]-[hash:base64:5]",
                  filetypes: { ".styl": { syntax: "sugarss" } },
                  webpackHotModuleReloading: true,
                  exclude: ".css$"
                }
              ]
            ]
          }
        },
        {
          loader: "ts-loader",
          options: {
            happyPackMode: true,
            // disable type checker - we will use it in fork plugin
            transpileOnly: true
          }
        }
      ],
      threadPool: happyThreadPool
    })
  ]
};

error

1,提示css-loader options报错

查了下css-loader的文档,修改webpack.config.js 的css-loader的配置如下

{
            test: stylModuleRegex,
            use: getStyleLoaders(
              {
                importLoaders: 1,
                modules: {
                  getLocalIdent({ resourcePath }, localIdentName, localName) {
                    return generateScope(localName, resourcePath);
                  }
                }
              },
              "stylus-loader"
            )
          },

2 DetailHTMLProps 错误

解决办法: 1, 图中第一个错误的解决办法
yarn add @types/react-css-modules -D 2, 图中第二个错误的解决办法
原因是指styleName 被转成了stylename,导致不认识该属性,也就是说styleName配置有问题

3 without importing at least one styleSheet

解决:
1
引入styl文件的时候,不省略.styl

import "./index.cssmodule.styl";

2
在tsconfig.json 添加

// 这三项解决 import * as React from react to import React from react 和 babel-plugin-react-css-modules 提示without importing at least one stylesheet.
    "allowSyntheticDefaultImports": true,
    "moduleResolution": "node",
    "module": "esNext"

4 not find module sugarss

解决: yarn add sugarss -D

5 Type '{ first: true; }' is not assignable to type 'IntrinsicAttributes & HelloWorldProps

原因:

if ((module as any).hot) {
 (module as any).hot.accept("./components/HelloWorld.tsx", function() {
   console.log("Accepting the updated printMe module!");
   console.log(111);
   ReactDOM.render(<HelloWorld first
 });
}

解决:

if ((module as any).hot) {
  (module as any).hot.accept("./components/HelloWorld.tsx", function() {
    console.log("Accepting the updated printMe module!");
    console.log(111);
    ReactDOM.render(<HelloWorld firstName="Chris" lastName="Parker" />, ROOT);
  });
}

配置babel-plugin-react-css-modules 总结

babel-plugin-react-css-modules这个包因为配置需要babel-loader的配合使用,所以在配置的时候首先使用ts-loader转然后再使用babel-loader转,流程没有什么问题,可是在配置过程中就出现了很多问题。

1 tsconfig.json

在配置tsconfig.json的时候,把jsx 配置成了react,这就出现了一个问题,实际上是全部都通过ts-loader给转化了,并没有走到babel-loader这里,所以一直出现写的styleName,经过转化之后,就转成了stylename,一直报错。

拆分webpack.config.js

原理

其实就是利用webpack-merge,根据传入的参数的不同,来区分是开发环境还是生产环境,然后把webpack.config.js 拆成一个公共的webpack.common.config.js,一个开发环境用的webpack.dev.config.js,一个生产环境用的 webpack.prod.config.js,还有一个根目录下的webpack.config.js

webpack.config.js 代码

const commConfig = require("./config/webpack.common.config.js");
const devConfig = require("./config/webpack.dev.config.js");
const prodConfig = require("./config/webpack.prod.config.js");
const merge = require("webpack-merge");

module.exports = mode => {
  if (mode === "development") {
    return merge(commConfig, devConfig, { mode });
  }
  return merge(commConfig, prodConfig, { mode });
};

package.json

"start": "webpack-dev-server --env development",
"build": "./node_modules/.bin/webpack --env production"

增加express

1, 安装express yarn add express -D
2, 新建express.js

const express = require("express");
const app = express();
const portNumber = 3888;
const sourceDir = "dist";

app.use(express.static(sourceDir));

app.listen(portNumber, () => {
  console.log(`Express web server started: http://localhost:${portNumber}`);
  console.log(`Serving content from /${sourceDir}/`);
});

3, 修改package.json

"build": "./node_modules/.bin/webpack --env production --config ./config/webpack/webpack.config.js && node express.js",

把react,reat-dom 等静态文件打包成dll文件

1, 新建lib.dep.js

// 资源依赖包,提前编译

const lib = ["react", "react-dom"];

module.exports = lib;

新建webpack.dll.js

/* webpack --config  webpack.dll.config.js --progress */

const path = require("path");
const webpack = require("webpack");
const lib = require("./lib.dep");
const outputPath = path.join(__dirname, "../dll");
const shelljs = require("shelljs");
shelljs.rm("-r", outputPath);

module.exports = {
  devtool: "eval",
  entry: {
    dllSelf: lib
  },
  mode: "production",
  optimization: {
    minimize: true
  },
  output: {
    path: outputPath,
    filename: "[name].[hash].js",
    /**
     * output.library
     * 将会定义为 window.${output.library}
     * 在这次的例子中,将会定义为`window.vendor_library`
     */
    library: "[name]"
  },
  plugins: [
    new webpack.DllPlugin({
      /**
       * path
       * 定义 manifest 文件生成的位置
       * [name]的部分由entry的名字替换
       */
      path: path.join(outputPath, "manifest.json"),
      /**
       * name
       * dll bundle 输出到那个全局变量上
       * 和 output.library 一样即可。
       */
      name: "[name]",
      context: __dirname
    }),
    new webpack.DefinePlugin({
      "process.env.NODE_ENV": JSON.stringify("production")
    })
  ]
};

3, 在webpack 的pulgin中引用dll文件

module.exports = {
  plugins: [
    new webpack.DllReferencePlugin({
      context: __dirname,
      manifest: require("../dll/manifest.json")
    }),
    new AddAssetHtmlPlugin([
      {
        filepath: utils.getDllPath("../dll"),
        includeSourcemap: false
      }
    ])
  ]
};