React16 + Webpack4 模拟Create-React-App 打包结构从零开始

751 阅读8分钟

说人话就是我这个是用来回馈社会的,一直用的create-react-app但是每次有啥特殊需求就可能需要eject,让我觉得很不爽,因为我就是不想eject。

写这个主要是两个原因, 第一个是因为网上关于react + webpack的配置真的很杂,而且有的是老版本的,几篇文章看下来都不知道应该用哪个,越配越杂,我这个写出来就是为了让新手少走弯路,因为我就是一直在走弯路,我这个就是2020年最新版的。

第二个原因呢就是一直用creat-react-app然后里面打包出来的分包啊,都很好用,但是网上的react+ webpack 都是最基本的, 我就想做一个能满足基本需求并且打包出来长得像create-react-app的打包效果的东西。

  • 首先是创建一个空文件夹,然后通过cmd输入 :
npm init
  • 然后一直按回车,这个时候文件夹就会出现一个package.json的文件,里面一些都是关于你个人和项目基本信息, 有了这个package.json 文件,你下载的那些依赖包就会添加到package.json里面。然后根据个人爱好选择npm或者yarn来作为你的包安装器,我这里就用yarn为例子作为包下载器,在cmd中输入:
yarn add -D path webpack webpack-cli webpack-merge webpack-dev-server html-webpack-plugin mini-css-extract-plugin rimraf
  • path是文件路径工具; webpack和webpack-cli是你打包react需要的工具; webpack-merge 是你整合webpack基础配置和dev/production的单独配置所需要的依赖包,这个是可选项,你可以吧自己的所有config写在一个文件中,然后通过环境判断当前属于development还是production; webpack-dev-server是本地测试的服务器,里面有自动刷新等功能; html-webpack-plugin 是用来 生成一个index.html模板,以后所有打包的js,css等文件都会attach到这个模板上; mini-css-extract-plugin 是用来分离打包css,然后css和js在客户发出request的时候是可以并行的,就是打包成两个文件,你可以加快效率返回文件,当然前提是多一个http request. 你不用他也行,但是我为了能够和create-react-app一样所有的css放在一个包内,我就用了他; rimraf 存在的意义就是当我打包的时候我想吧原来在dist目录下的dev或者debug的文件全部删了然后保持一个最新的 继续cmd输入:
yarn add -D @babel/core @babel/preset-env @babel/preset-react babel-loader
  • 这个是通过babel这个编译器把react代码转移成ES5常规代码,然后在项目根目录下创建.babelrc文件,文件内容配置,这些配置就是为了能够让babel去编译react代码
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react"
  ]
}

babelrc.png

  • 然后是加入各种loader, 我这里只是放了几个基本的,你要需要其他的,可以自己去webpack上找loader然后自己加就行了
yarn add -D url-loader file-loader style-loader css-loader
  • 现在我们要开始配置webpack里,在根目录下创建一个config目录,里面存放webpack配置文件们。 进入config目录,创建一个webpack.base.conf.js 文件,作为production和development的公用配置
const path = require('path');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 生成html模板
const resolve = (dir) => path.join(__dirname, '..', dir); //写一个路径解析的功能,给后面的路径调用

module.exports = {
    entry: {
        app: resolve("/src/index.js") //webpack 的文件入口,获取寻找这个文件然后开始需要所有的dependencies
    },
    output: {
        path: resolve("dist/static"), //打包后输出的目录,意思就是在根目录下有一个dist目录,下面有一个static目录
        // path: path.join(__dirname,"../../src/main/resources/static/static"),  //直接写入springboot resource下面的static 目录下
        filename: "js/bundle.[hash].js", //打包后的输出文件名, output中的path和filename联合起来就是文件打包地址 : react-app-root-folder/dist/static/js/bundle.ha23j3kdf3kso3.js
        publicPath: "/static" // 这个是用来路径映射,dist 目录作为打包后的资源总目录,我们希望需要他下面的/static/js/bundle.ha23j3kdf3kso3.js, 如果不放这个publicPath,那么在打包后的index.html script路径就会变成 /js/bundle.ha23j3kdf3kso3.js
    },
    module: {
        rules: [
            // 使用Eslint才需要配置这个
            // {
            //     test: /\.(jsx|js)$/,
            //     enforce: 'pre',
            //     include: resolve('src'),
            //     exclude:[
            //         resolve('node_modules')
            //     ],
            //     loader: 'eslint-loader',
            // },
            {
                test: /\.(jsx|js)$/, //所有以jsx或者js结尾的文件
                include: resolve('src'), //包括当前项目下的src,其他的目录不管,主要目的是为了避开node_modules,因为nod_modules下面全是js文件,我们不想编译那些文件
                loader: 'babel-loader', // 采用何种编译器去编译上面限制的这些文件, 总而言之就是 使用babel-loader去编译src目录下带有jsx或者js结尾的文件
            },
            {
                test: /\.css$/, //所有以css结尾的文件,
                use: [
                    "style-loader",
                    MiniCssExtractPlugin.loader,
                    "css-loader",
                ],
                include: resolve('src')
            },
            {
                test: /\.(eot|woff2?|ttf|svg)$/,
                use: [
                    {
                        loader: "url-loader",
                        options: {
                            name: "[name]-[hash:5].min.[ext]",
                            limit: 5000, // 当文件小于5000的时候会吧文件转成dataUrl,如果大于就是直接使用file-loader的那一套了
                            outputPath: 'media/'
                            // publicPath: "fonts/",
                            // outputPath: "assets/fonts"
                        }
                    }
                ]
            },
            {
                test: /\.(jpg|svg|png|gif|pdf)$/,
                use: {
                    loader: "file-loader",
                    options: {
                        name: '[name].[ext]',
                        outputPath: 'media/'
                    }
                }
            }
        ]
    },
    resolve:{
        extensions:['.js','.jsx','.json','.css'] //这个就是当你引用其他的组件或者文件的时候不需要输入后缀webpack也能识别的功能,你不用这个,然后每次在code里面引入外部组件的时候记得加后缀也可以的
    },
    plugins: [
        new HtmlWebpackPlugin({
            filename: resolve("/dist/index.html"), // index。html 模板生成的地址
            // filename: path.join(__dirname,"../../src/main/resources/static/index.html"), //如果直接build到spring下
            template: resolve("/src/index.html"), // 模板 的源文件 地址
        }),
        new MiniCssExtractPlugin({
            filename: "css/[name].[hash].css", //css文件分离打包的地址和文件名
            chunkFilename: "css/[id].[hash].css" //当css文件特别大的时候可以把一个包打成多个小包
        }),
    ]
};
  • 再接着我们在config这个目录下再创建一个webpack.dev.conf.js
const path = require('path');
const webpack = require('webpack');
const webpackMerge = require("webpack-merge"); //用来合并在webpack.base.conf.js定义的配置
const baseConfig = require("./webpack.base.conf");
const resolve = (dir) => path.join(__dirname, '..', dir);

module.exports = webpackMerge(baseConfig, {
    mode: "development",
    devtool: 'cheap-module-eval-source-map', //这个是为了debug好定位code出错在哪一行设置的,有其他的选项我不细讲,网上好多
    devServer: {
        contentBase: resolve("dist"), // 指定dev-server去哪个目录下读取编译的文件
        // contentBase: path.join(__dirname,"../../src/main/resources/static"),  //如果base config直接build 到springboot resource下面的static 目录下, 那么dev直接去那里读取
        port: 3000, //开一个port 3000的端口,这个端口可以自定义,我跟随这create-react-app的节奏走,就定义的3000
        // open: true, //开启dev server之后会自动开启浏览器,但是第一次compile之后写入dist目录需要一段时间,所以一般是不自动开启
        hot: true, //打开hot就可以局部更新并且临时状态保存,但是单独的css文件更新会捕捉不到
        writeToDisk: true, //dev server开启自动编译文件然后编译文件存在于内存中,但是我们指定了读取位置contentBase实际存储地址,所以内存中地址和实际存储地址不同,所以需要把内存中文件写入磁盘就可以
        inline: true, //实时刷新
        watchContentBase:true, //一直看着我们指定的contentBase是否有变化,有变化就刷新,主要是要解决css文件更新后没有自动刷新浏览器的问题,如果哪位大神有答案我就可以修改修改
        publicPath: '/static', //要保持和webpack.base.conf.js 里面配置的publicPath一直,因为dev-serve存在的意义就是为了模拟服务器读取打包的文件
        historyApiFallback: {
            index: '/index.html' // 当request路径找不到或者不存在的时候就会去读取index.html
        }
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin()  //启动热更替
    ]
});
  • 再然后创建一个webpack.prod.conf.js
const baseConf = require("./webpack.base.conf");
const webpackMerge = require("webpack-merge");

module.exports = webpackMerge(baseConf, {
    mode: 'production'
});
  • 三个webpack的配置文件就配置好了,dev就是本地测试的时候跑的,prod就是你要打包上传的时候跑的,现在就是进入正题了,我们需要导入我们的react包,我这里多放了个react-router-dom, 你们不想要就不要加,顺便提一句前面所有的add都有加-D 是把它们放到dev环境,打包不会把这些包编译进去
yarn add react react-dom react-router-dom
  • 这个时候基本上已经可以开始玩了, 你接着在根目录下创建src的目录,然后在下面新建一个index.js文件,这个是webpack去打包的入口,必须叫这个名字,因为你webpack里面是这么配的,不然的话你自己可以去改webpack里面的entry, 接着在src目录下写一个index.html 的模板页, 当然名字不能改,要改就是webpack和你的html名字一起改。
  • 写好了组件后,我们一起去package.json文件,替换一下它里面的scripts 代码
  "scripts": {
    "clear": "rimraf dist",
    "build": "npm run clear && webpack --config ./config/webpack.prod.conf.js",
    "start": "webpack-dev-server --config ./config/webpack.dev.conf.js"
  },
  • 剩下的我也没什么好说的里,就在src目录下面各种建立component和你的组件。写好了之后就直接npm run start了。 这个时候你应该会在根目录下产生一个dist目录,然后目录下面的结构跟我的图类似

structure.png

  • 自此也算是基本的react和webpack完成了,当然我没有加入eslint检查,如果这篇文章有人喜欢的话,我下一张就讲讲如何整合eslint和mobx好了