React+TypeScript+webpack4多入口项目搭建

资源

  • React-16.8.*
  • react-router-dom-4.3.*
  • TypeScript-3.5.*
  • webpack-4.*
  • eslint-5.16.*

项目目录

├── dist # 打包结果目录
│   ├── demo1 //类别demo1的打包结果
│   │   ├── demo1.himl
│   │   ├── demo1.js
│   │   └── demo1.css
│   └── demo2 ... //类别demo2的打包结果
├── src # 业务资源文件目录
│   ├── category //项目分类
│   │   ├── demo1
│   │   ├── demo2
│   │   └── ...
│   ├── components //公共组件
│   ├── util //公共资源
│   └── custom.d.ts //项目全局变量声明文件
├── index.html //项目启动入口
├── .gitignore //git忽略文件
├── .eslintrc.js //eslint校验配置
├── package.json //依赖包
├── tsconfig.json //ts配置
├── webpack.config.build.js //webpack打包
├── webpack.config.base.js //webpack基础配置
└── webpack.config.js //项目启动配置

前言

对于复杂或多人开发的 React 项目来说,管理和使用每个组件的 propsstate 或许会成为一件让人头痛的事情,而为每一个组件写文档,成本也会比较大,对项目的开发效率也不是最理想的。

TypescriptReact 带来很多好处:

  • 在组件头部定义 interface,让每个人在拿到组件的第一时间就可以很明确知道该组件需要使用的 propsstate
  • 在编译中发现问题,减少运行时的报错;
  • 可以在编辑器中实现实时类型校验、引用查询;
  • 约束类型,在混合多语言环境中降低风险,等。

需求

要搭建一个React+TypeScript+webpack的项目的话,一般都是团队开发多人多文件项目,在搭建之前需要优先考虑以下几个方面:

  • 开发体验
  • 项目打包
  • 团队规范

安装

  • 前置安装
    首先需要全局安装typescript,这里默认大家都已经安装了node以及npm

    npm install -g typescript
    
  • 首先新建文件夹并进入

    mkdir tsDemo && cd tsDemo
    
  • 然后进行初始化,生成package.jsontsconfig.json

    npm init -y && tsc --init
    
  • 安装开发工具

    npm install-D webpack webpack-cli webpack-dev-server
    
  • 安装react相关
    因为需要整合ts,而react原本的包是不包含验证包的,所以这里也需要安装相关ts验证包

    npm install -S react react-dom
    npm install -D @types/react @types/react-dom
    
  • 安装ts-loader

    npm install -D ts-loader
    
  • 以上是基本的 后续会贴出项目demo里面包含所有依赖包

webpack配置

添加webpack文件

根目录下新建webpack.config.base.js、webpack.config.build.js、webpack.config.js文件

touch webpack.config.base.js webpack.config.build.js webpack.config.js
  1. entry:入口文件(你要打包,就告诉我打包哪些)
  2. output:出口文件(我打包完了,给你放到哪里)
  3. resolve: 寻找模块所对应的文件
  4. module:模块(放lorder,编译浏览器不认识的东西)
  5. plugins:插件(辅助开发,提高开发效率)
  6. externals:打包忽略
  7. devServer:服务器(webpack提供的本地服务器)
  8. mode:模式,分为开发模式、生产模式。此为4.X里新增的

配置entry入口文件

因为大部分项目是多入口,多类别的,所有入口配置时不要配置单一入口

const fs = require("fs");
const path = require("path");
const optimist = require("optimist");

const cateName = optimist.argv.cate;
let entryObj = {};
const srcPath = `${__dirname}/src`;
//获取当前项目要启动或者打包的基础路径
const entryPath = `${srcPath}/category/`;
//未指定类别 启动或者打包所有类别
//如:npm run dev 或者npm run build
if (cateName == true) {
    fs.readdirSync(entryPath).forEach((cateName, index) => {
        // cateName/cateName指定输出路径为entryname
        if (cateName != "index.html" && cateName != ".DS_Store") entryObj[`${cateName}/${cateName}`] = `${entryPath + cateName}/${cateName}.tsx`;
    });
} else if (cateName.indexOf(",")) {
    // 一次指定多个类别 类别之间以","分割
    //如:npm run dev erhsouche,huoche 
    let cateNameArray = cateName.split(",");
    for (let i = 0; i < cateNameArray.length; i++) {
        entryObj[`${cateNameArray[i]}/${cateNameArray[i]}`] = `${entryPath + cateNameArray[i]}/${
            cateNameArray[i]
        }.tsx`;
    }
} else {
    // 打包单个入口文件
    //如:npm run dev ershouche
    entryObj[`${cateName}/${cateName}`] = `${entryPath + cateName}/${cateName}.tsx`;
}
const webpackConfig = {
    entry: entryObj,
}
module.exports = {
    webpackConfig,
    entryObj
};

配置output出口文件

const webpackConfig = {
    output: {
        //输出文件名称以当前传入的cate类别名称命名
        filename: "[name].js", 
        //输出到根目录下的dist目录中
        path: path.resolve(__dirname, "dist"),
        publicPath: "/",
    },
}

配置resolve

需要import xxx from 'xxx'这样的文件的话需要在webpack中的resolve项中配置extensions,这样以后引入文件就不需要带扩展名

const webpackConfig = {
    resolve: {
        extensions: [".tsx", ".ts", ".js", ".jsx", ".json"],
        //配置项通过别名来把原导入路径映射成一个新的导入路径。
        alias: {
            images: path.join(__dirname, "src/util/img")
        },
        // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
        modules: [path.resolve(__dirname, "node_modules")] 
    },
}

配置module

概念

webpack中任何一个东西都称为模块,js就不用说了。一个css文件,一张图片、一个less文件都是一个模块,都能用导入模块的语法(commonjsrequireES6import)导入进来。webpack自身只能读懂js类型的文件,其它的都不认识。但是webpack却能编译打包其它类型的文件,像ES6JSXlesstypeScript等,甚至cssimages也是Ok的,而想要编译打包这些文件就需要借助loader

loader就像是一个翻译员,浏览器不是不认识这些东西么?那好交给loader来办,它能把这些东西都翻译成浏览器认识的语言。loader描述了webpack如何处理非js模块,而这些模块想要打包loader必不可少,所以它在webpack里显得异常重要。loader跟插件一样都是模块,想要用它需要先安装它,使用的时候把它放在module.rules参数里,rules翻译过来的意思就是规则,所以也可以认为loader就是一个用来处理不同文件的规则

所需loader

  • ts-loader

    编译TypeScript文件

    npm install ts-loader -D
    
  • url-loader

    处理css中的图片资源时,我们常用的两种loader是file-loader或者url-loader,两者的主要差异在于。url-loader可以设置图片大小限制,当图片超过限制时,其表现行为等同于file-loader,而当图片不超过限制时,则会将图片以base64的形式打包进css文件,以减少请求次数。

    npm install url-loader -D
    
  • css处理所需loader
    css-loader 处理css

    sass-loader 编译处理scss

    sass-resources-loader 全局注册变量

  • html-loader
    处理.html文件

module完整配置

const webpackConfig = {
    module: {
        rules: [
            //处理tsx文件
            { test: /\.(tsx|ts)?$/, use: ["ts-loader"], include: path.resolve(__dirname, "src") },
            //处理图片资源
            {
                test: /\.(png|jpe?g|jpg|gif|woff|eot|ttf|svg)/,
                use: [
                    // 对非文本文件采用file-loader加载
                    {
                        loader: "url-loader",
                        options: {
                            limit: 1024 * 30, // 30KB以下的文件
                            name: "images/[name].[hash:8].[ext]",
                        }
                    }
                ],
            },
            //处理css和scss
            {
                test: /\.(css|scss)$/,
                use: [
                    //css单独打包
                    MiniCssExtractPlugin.loader,
                    {
                        loader: "css-loader"
                    },
                    {
                        loader: "postcss-loader",
                        options: {
                            plugins: () => [require("autoprefixer")],
                            sourceMap: true
                        }
                    },
                    {
                        loader: "sass-loader",
                        options: {
                            sourceMap: true
                        }
                    },
                    {
                        loader: "sass-resources-loader",
                        options: {
                            resources: ["./skin/mixin.scss", "./skin/base.scss"]
                        }
                    }
                ],
                exclude: path.resolve(__dirname, "node_modules")
            },
            {
                test: /\.html$/,
                use: {
                    loader: "html-loader",
                }
            },
            { test: /src\/containers(\/.*).(tsx|ts)/, loader: "bundle-loader?lazy!ts-loader" },
            { enforce: "pre", test: /\.js$/, loader: "source-map-loader" }
        ]
    },
}

配置plugins

plugins里面放的是插件,插件的作用在于提高开发效率,能够解放双手,让我们去做更多有意义的事情。一些很low的事就统统交给插件去完成。

const webpackConfig = {
    plugins: [
        //清除文件
        new CleanWebpackPlugin(),
        //css单独打包
        new MiniCssExtractPlugin({
            filename: "[name].css",
            chunkFilename: "[name].css"
        }),
        // 引入热更新插件
        new webpack.HotModuleReplacementPlugin() 
    ]
}

配置externals

如果需要引用一个库,但是又不想让webpack打包(减少打包的时间),并且又不影响我们在程序中以CMD、AMD或者window/global全局等方式进行使用(一般都以import方式引用使用),那就可以通过配置externals。

const webpackConfig = {
    //项目编译打包是忽略这些依赖包
    externals: {
        react: "React",
        "react-dom": "ReactDOM",
        "react-redux": "ReactRedux",
    }
}

配置mode

modewebpack4新增的一条属性,它的意思为当前开发的环境。mode的到来减少了很多的配置,它内置了很多的功能。相较以前的版本提升了很多,减少了很多专门的配置

  1. 提升了构建速度
  2. 默认为开发环境,不需要专门配置
  3. 提供压缩功能,不需要借助插件
  4. 提供SouceMap,不需要专门配置

mode分为两种环境,一种是开发环境(development),一种是生产环境(production)。开发环境就是我们写代码的环境,生产环境就是代码放到线上的环境。这两种环境的最直观区别就是,开发环境的代码不提供压缩,生产环境的代码提供压缩。

配置devServer

const webpackConfig = {
    devServer: {
        // 本地服务器所加载的页面所在的目录
        contentBase: srcPath, 
        //热更新
        hot: true,
        //服务端口
        port: "7788",
        // 是否向Chunk中注入代理客户端,默认注入
        inline: true, 
        // publicPath: '/dist/',
        historyApiFallback: {
            index: "template.html",
        },
        //默认检查hostname
        disableHostCheck: true,
        compress: true,
        open: true // 自动打开首页
    }
}

webpack.config.base.js完整文件

const fs = require("fs");
const path = require("path");
const webpack = require("webpack");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlguin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); // webpack4支持的单独打包css
// const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); //webpack4支持自动优化代码 mode:'development'即可
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");

const isProduction = process.env.NODE_ENV === "production";

const optimist = require("optimist");

const cateName = optimist.argv.cate;
let entryObj = {};
const srcPath = `${__dirname}/src`;
const entryPath = `${srcPath}/category/`;
if (cateName == true) {
    fs.readdirSync(entryPath).forEach((cateName, index) => {
        // cateName/cateName指定输出路径为entryname
        if (cateName != "index.html" && cateName != ".DS_Store") entryObj[`${cateName}/${cateName}`] = `${entryPath + cateName}/${cateName}.tsx`;
    });
} else if (cateName.indexOf(",")) {
    // 一次打包多个入口文件以逗号分隔
    let cateNameArray = cateName.split(",");
    for (let i = 0; i < cateNameArray.length; i++) {
        entryObj[`${cateNameArray[i]}/${cateNameArray[i]}`] = `${entryPath + cateNameArray[i]}/${
            cateNameArray[i]
        }.tsx`;
    }
} else {
    // 打包单个入口文件
    entryObj[`${cateName}/${cateName}`] = `${entryPath + cateName}/${cateName}.tsx`;
}

const webpackConfig = {
    entry: entryObj,
    output: {
        filename: "[name].js",
        path: path.resolve(__dirname, "dist"),
        publicPath: "/",
    },
    resolve: {
        extensions: [".tsx", ".ts", ".js", ".jsx", ".json"],
        alias: {
            images: path.join(__dirname, "src/util/img"),
            components: path.join(__dirname, "src/components")
        },
        modules: [path.resolve(__dirname, "node_modules")] // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
    },
    module: {
        rules: [
            { test: /\.(tsx|ts)?$/, use: ["ts-loader"], include: path.resolve(__dirname, "src") },
            {
                test: /\.(png|jpe?g|jpg|gif|woff|eot|ttf|svg)/,
                use: [
                    // 对非文本文件采用file-loader加载
                    {
                        loader: "url-loader",
                        options: {
                            limit: 1024 * 30, // 30KB以下的文件
                            name: "images/[name].[hash:8].[ext]",
                        }
                    }
                ],
            },
            {
                test: /\.(css|scss)$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    {
                        loader: "css-loader"
                    },
                    {
                        loader: "postcss-loader",
                        options: {
                            plugins: () => [require("autoprefixer")],
                            sourceMap: true
                        }
                    },
                    {
                        loader: "sass-loader",
                        options: {
                            sourceMap: true
                        }
                    },
                    {
                        loader: "sass-resources-loader",
                        options: {
                            resources: ["./skin/mixin.scss", "./skin/base.scss"]
                        }
                    }
                ],
                exclude: path.resolve(__dirname, "node_modules")
            },
            {
                test: /\.html$/,
                use: {
                    loader: "html-loader",
                }
            },
            { test: /src\/containers(\/.*).(tsx|ts)/, loader: "bundle-loader?lazy!ts-loader" },
            { enforce: "pre", test: /\.js$/, loader: "source-map-loader" }
        ]
    },
    mode: isProduction ? "development" : "production",
    // 安全起见,生产环境使用hidden-source-map,会生成详细的Source Map,但不会将Source Map暴露出去
    devtool: isProduction ? "hidden-source-map" : "cheap-module-eval-source-map",
    externals: {
        react: "React",
        "react-dom": "ReactDOM",
        "react-redux": "ReactRedux",
    },
    plugins: [
        new CleanWebpackPlugin(),
        new MiniCssExtractPlugin({
            filename: "[name].css",
            chunkFilename: "[name].css"
        }),
        new webpack.HotModuleReplacementPlugin() // 引入热更新插件
    ]
};

const pages = Object.keys(entryObj);

pages.forEach(pathname => {
    const htmlName = entryObj[pathname];
    const template_local = htmlName.replace(".tsx", ".html");
    const entryName = pathname.split("/")[0];
    let conf = {
        filename: `category/${entryName}/${entryName}.html`, // 生成的html存放路径,相对于path
        title: entryName,
        template: template_local, // html模板路径
        inject: true, // js插入的位置,true/'head'/'body'/false
        hash: true, // 为静态资源生成hash值
        // favicon: 'src/favicon.ico', //favicon路径,通过webpack引入同时可以生成hash值
        chunks: [pathname],
        minify: {
            // 压缩HTML文件
            removeComments: true, // 移除HTML中的注释
            collapseWhitespace: false, // 删除空白符与换行符
        },
    };
    const defineConf = Object.assign({}, conf, { template: "src/template.html" });
    const exists = fs.existsSync(template_local);
    if (exists) {
        webpackConfig.plugins.push(new HtmlWebpackPlguin(conf));
    } else {
        webpackConfig.plugins.push(new HtmlWebpackPlguin(defineConf));
    }
});

module.exports = {
    webpackConfig,
    entryObj
};

webpack.config.js完整文件

const webpack = require("webpack");
const { webpackConfig } = require("./webpack.config.base");
let optimist = require("optimist");

let cateName = optimist.argv.cate;
const srcPath = `${__dirname}/src`;
webpackConfig.devServer = {
    contentBase: srcPath, // 本地服务器所加载的页面所在的目录
    hot: true,
    port: "7788",
    inline: true, // 是否向Chunk中注入代理客户端,默认注入
    // publicPath: '/dist/',
    historyApiFallback: {
        index: "template.html",
    },
    disableHostCheck: true,
    compress: true,
    open: true // 自动打开首页
};
// 通过插件模式开启模块热替换 也可在执行命令时加上 --hot
webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
module.exports = webpackConfig;

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容