react移动端多页面的webpack搭建

682 阅读4分钟

有个需求是写移动端618多页面活动,但公司现在要从vue转向react,所以需要建新库,要求以后的所有的活动都在这个库中。

目录结构

|-- react_cli // 项目
    |-- build // webpack 配置
        |-- webpack.base.config.js // webpack 基本配置
        |-- webpack.dev.config.js // webpack 开大环境配置
        |-- webpack.prod.config.js // webpack 生产环境配置
    |-- dist // 打包目录
    |-- mock // mock数据
        |-- mockData // 所有的json数据
            |-- initData.json // 某个json数据
        |-- index.js // 导出mock数据
    |-- src // 源代码
        |-- assets // 所有活动的公共图片、样式
        |-- comments // 所有活动的公共代码,比如utils、fatch、native等
        |-- components // 所有活动的公共组件
        |-- pages // 活动
            |-- test_2020_618 // 618活动
                |-- test22 // test22页面
                |-- test33 // test33页面
            |-- test_2020_625 // 625活动
    |-- .babelrc // babel配置
    |-- .env // 环境,用来配置哪个活动
    |-- index.ejs // html模版
    |-- package.json // package
    |-- postcss.config.js // css打包配置

1、package.json

{
  "name": "react_st_cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "cross-env NODE_ENV=development webpack-dev-server --config build/webpack.dev.config.js",
    "mock": "cross-env NODE_ENV=mock webpack-dev-server --config build/webpack.dev.config.js",
    "build": "cross-env NODE_ENV=production webpack --config build/webpack.prod.config.js"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.10.2",
    "@babel/plugin-transform-runtime": "^7.10.1",
    "@babel/preset-env": "^7.10.2",
    "@babel/preset-react": "^7.10.1",
    "autoprefixer": "^9.8.0",
    "babel-loader": "^8.1.0",
    "clean-webpack-plugin": "^3.0.0",
    "cross-env": "^7.0.2",
    "css-loader": "^3.5.3",
    "eslint": "^7.2.0",
    "eslint-loader": "^4.0.2",
    "file-loader": "^6.0.0",
    "glob": "^7.1.6",
    "happypack": "^5.0.1",
    "hard-source-webpack-plugin": "^0.13.1",
    "html-webpack-plugin": "^4.3.0",
    "less": "^3.11.2",
    "less-loader": "^6.1.0",
    "mini-css-extract-plugin": "^0.9.0",
    "mocker-api": "^2.1.0",
    "optimize-css-assets-webpack-plugin": "^5.0.3",
    "postcss-loader": "^3.0.0",
    "postcss-px-to-viewport": "^1.1.1",
    "purgecss-webpack-plugin": "^2.2.0",
    "speed-measure-webpack-plugin": "^1.3.3",
    "style-loader": "^1.2.1",
    "terser-webpack-plugin": "^3.0.3",
    "url-loader": "^4.1.0",
    "webpack": "^4.43.0",
    "webpack-bundle-analyzer": "^3.8.0",
    "webpack-cli": "^3.3.11",
    "webpack-dev-server": "^3.11.0",
    "webpack-merge": "^4.2.2"
  },
  "dependencies": {
    "@babel/runtime": "^7.10.2",
    "dotenv": "^8.2.0",
    "lodash": "^4.17.15",
    "lodash-es": "^4.17.15",
    "react": "^16.13.1",
    "react-dom": "^16.13.1"
  },
  "browserslist": [
    "defaults",
    "ie >= 9",
    "last 2 versions",
    "> 1%",
    "iOS 7",
    "last 3 iOS versions"
  ]
}

package.json分为development、mock、production这三种环境,其他没什么好说的。

2、webpack.base.config.js

const path = require("path")
// 打包优化插件,加快打包速度
const HappyPack = require('happypack')
// 抽离 css 样式,防止将样式打包在 js 中文件过大和因为文件大网络请求超时的情况。
// style-loader和 mini-css-extract-plugin 冲突。如果使用了 mini-css-extract-plugin 插件,就可以不用style-loader
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const HtmlWebpackPlugin = require("html-webpack-plugin")
// 引入dotenv包,默认读取项目根目录下的.env文件
require('dotenv').config()
// 处理文件路径
const glob = require("glob")

const isProduction = process.env.NODE_ENV === 'production'

// 读取src目录所有page入口
const entryToObj = () => {
    let entry = {}
    glob
        .sync(`./src/pages/${process.env.ACTIVITY_NAME}/**/index.js`)
        .map(item => {
            let name = item.split('/')
            entry[name[4]] =  item
        })
    return entry
}

// 获取html-webpack-plugin参数的方法
const getHtmlConfig = function (name) {
    return {
        template: `./index.ejs`, 
        filename: isProduction ? `html/${name}.html` : `${name}.html`,
        inject: true, // true:默认值,script标签位于html文件的 body 底部
        hash: false, // 在打包的资源插入html会加上hash
        chunks: ['vendor', 'common', name],
        minify: {
            removeComments: true, // 移除HTML中的注释
            collapseWhitespace: true, // 折叠空白区域 也就是压缩代码
            removeAttributeQuotes: true, // 去除属性引用
        },
    };
};

module.exports = {
    // 多入口
    entry: entryToObj(),
    // 解析模块
    resolve: {
        extensions: ['.js', '.json', '.jsx'],
        alias: {
            '@': path.resolve('src')
        }
    },
    // 模块
    module:{
        rules: [
            {
                test: /\.(js|jsx)$/,
                use: ['happypack/loader?id=babel'],
                exclude: path.resolve(__dirname, 'node_modules'),
            }, {
                test: /\.(le|c)ss$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    "css-loader",
                    "postcss-loader",
                    "less-loader"
                ]
            }, {
                test: /\.(png|jpe?g|gif)$/,
                use: [{
                    loader: "url-loader",
                    options: {
                        limit: 5 * 1024, // 小于这个时将会以base64位图片打包处理
                        name: 'imgs/[name].[contenthash].[ext]',
                        esModule: false, // 解决react中img的src为[object Module]问题
                        outputPath:'static' // 打包后的图片放在 dist/static/下边
                    }
                }]
            }, {
                test: /\.(woff|woff2|eot|ttf|otf)$/,
                use: [
                    'file-loader'
                ]
            }

        ]
    },
    // 提取、生成公共代码
    optimization: {
        splitChunks: {
            chunks: 'all',
            cacheGroups: {  // 设置缓存组用来抽取满足不同规则的chunk
                vendors: { // 抽离第三方插件
                    // 指定是node_modules下的第三方包.如果首页不需要用到lodash,可以把lodash单独打包
                    // 去除 lodash,剩余的第三方库打成一个包: /node_modules\/(?!(lodash)\/)/
                    // lodash库单独打包: /node_modules\/lodash\//
                    test: /node_modules/, 
                    chunks: 'all',
                    name: 'vendors', // 打包后的文件名,任意命名    
                    // 设置优先级,防止和自定义的公共代码提取时被覆盖,不进行打包
                    priority: 10
                },
                common: { // 抽离自己写的公共代码,common这个名字可以随意起
                    chunks: 'initial', // initial表示提取入口文件的公共部分
                    name: 'common', // 任意命名
                    minSize: 0, // 只要超出0字节就生成一个新包
                    minChunks: 2
                }
            }
        }
    },
    plugins: [
        new HappyPack({
            id: 'babel',
            loaders: ['babel-loader?cacheDirectory=true'] // 开启缓存
        }),
        new MiniCssExtractPlugin({
            filename: isProduction ? 'css/[name].[contenthash].css' : '[name].[contenthash].css',
        })
    ],
    performance: {
        hints: "warning", // 包大于250kb时,会出现警告
        maxEntrypointSize: 40000000, // 根据入口起点的最大体积,控制 webpack 何时生成性能提示
        maxAssetSize: 30000000, // 此选项根据单个资源体积,控制 webpack 何时生成性能提示
        assetFilter: function(assetFilename) {
            return assetFilename.endsWith('.js')
        }
    }
}

const entryObj = entryToObj()

for (item in entryObj) {
    module.exports.plugins.push(new HtmlWebpackPlugin(getHtmlConfig(item)))
}

在 webpack.base.config.js 中,

require('dotenv').config()

是用来配置是哪个活动,比如我现在要做618的活动,就在.env中配置

ACTIVITY_NAME = test_2020_618

那么我webpack的入口文件在遍历的时候就可以用process.env.ACTIVITY_NAME来指定目录。entryToObj方法是生成入口对象的,返回如下

{ 
    test22: './src/pages/test_2020_618/test22/index.js',
    test33: './src/pages/test_2020_618/test33/index.js'
}

splitChunks是用来分割代码的,其中我把第三方插件提取成vendors,但如果我的项目比较大,需要引入好多插件,如antd、lodash、momnetjs等,vendors的体积就会很大,这时候就需要考虑怎样缩小vendors,有几种思路:

  • 1、对vendors进行拆分,比如我进入首页不需要用到lodash,那我去除lodash,剩余的第三方库打成一个包,lodash库单独打包。
  • 2、按需引入。
  • 3、利用tree-shaking的特性,引用具有 ES6 模块化的版本
    import { fill } from "lodash-es" 
    

最后遍历,有多少个页面就push多少个HtmlWebpackPlugin

3、webpack.dev.config.js

const webpackMerge = require("webpack-merge")
// mock数据
const apiMocker = require('mocker-api')
const baseWebpackConfig = require("./webpack.base.config")
const path = require("path")

module.exports = webpackMerge(baseWebpackConfig, {
    // 指定构建环境  
    mode:"development",
    devtool: "source-map",  // 开启调试模式
    // 出口
    output: {
        path: path.resolve(__dirname, '../dist'),
        filename: "js/[name].[contenthash].js",
        // publicPath: "../" // 打包后的资源的访问路径前缀
    },
    // 开发环境本地启动的服务配置
    devServer: {
        contentBase: path.join(__dirname, "../src/pages/index"), // 告诉服务器从哪里提供内容。只有在你想要提供静态文件时才需要
		publicPath:'/',  // 访问资源加前缀
		host: "0.0.0.0",
		port: "9527",
        overlay: true, // 浏览器页面上显示错误
        // 服务器代理配置项
        proxy: {
            '/api': {
                target:'http://1.1.1.1:0000',
                changeOrigin: true,
                pathRewrite: {
                    '^/api': '/'
                }
            }
        },
        before (app) {
            if(process.env.NODE_ENV === 'mock') {
                apiMocker(app, path.resolve(__dirname, '../mock/index.js'))
            }
        }
    }
})

这里说说mock,当在mock环境下,就会调用mock文件夹里的数据。下面是index.js里的代码

const fs = require('fs')

function fromJSONFile(filename) {
    return (req, res) => {
        const data = fs.readFileSync(`mock/mockData/${filename}.json`).toString()
        const json = JSON.parse(data)
        return res.json(json)
    }
}

const proxy = {
    'POST /api/test_618/initData': fromJSONFile('initData')
};

module.exports = proxy

在js中就可以使用了

const fatchMock = () => {
    fetch('/api/test_618/initData', { method: "POST" })
        .then(res => res.json().then(data => console.log(data)))
}

4、webpack.prod.config.js

// 合并
const webpackMerge = require("webpack-merge")
const baseWebpackConfig = require("./webpack.base.config")
const path = require("path")
const glob = require("glob")
// 清除dist目录等
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
// css压缩
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
// 去除无用css代码
const PurgecssPlugin = require('purgecss-webpack-plugin')
// 压缩js
const TerserPlugin = require('terser-webpack-plugin')
// 打包缓存,优化二次打包时间
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
// webpack体积分析工具
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
// webpack速度分析工具
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin")
const smp = new SpeedMeasurePlugin()

module.exports = smp.wrap(webpackMerge(baseWebpackConfig, {
    // 指定构建环境  
    mode: "production",
    // cheap-module-eval-source-map 可以看到源码,方便断点调试,建议测试环境使用。
    // cheap-module-source-map 生产环境推荐
    devtool: 'cheap-module-eval-source-map',  
    // 出口
    output: {
        path: path.resolve(__dirname, '../dist'),
        filename: "js/[name].[contenthash].js", // 建议名中用hash的改成contenthash
        publicPath: "../" // 打包后的资源的访问路径前缀
    },
    // 插件
    plugins:[
        // 删除dist目录
		new CleanWebpackPlugin({
			verbose: true, // 开启在控制台输出信息
			dry: false,
        }),
        // 压缩css
        new OptimizeCssAssetsPlugin({}),
        new BundleAnalyzerPlugin(),
        new PurgecssPlugin({
            paths: glob.sync(`${path.join(__dirname, '../src/pages/**')}`, { nodir: true }) // 注意路径,否则css文件空白
        }),
        new HardSourceWebpackPlugin()
    ],
    optimization: {
        usedExports: true, // Tree Shaking
        minimize: true, // 执行默认压缩如果想用第三方插件就需要在minimizer设置
        minimizer: [new TerserPlugin({
            cache: true, // 是否缓存
            parallel: true, // 是否并行打包
            terserOptions: {
                compress: {
                  drop_console: true, // 去除console
                }
            },
      
        })],
    }
}))

webpack.prod.config.js里的代码注解已经比较详细了。

5、.babelrc

{
    "presets": [
        ["@babel/preset-env", {
            "modules": false // 使tree-shaking生效
        }],
        "@babel/preset-react"
    ],
    "plugins": ["@babel/plugin-transform-runtime"]
}

5、index.ejs

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="format-detection" content="telephone=no,email=no">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
    <meta name="theme-color" content="#000000" />
    <title>ST</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
    <% if (process.env.NODE_ENV !== 'production') {%>
      <script type="text/javascript" src="https://cdn.bootcss.com/eruda/1.2.6/eruda.min.js"></script>
      <script>eruda.init();</script>
    <%}%>
    
  </body>
</html>

这里使用ejs,比较方便

6、postcss.config.js

module.exports = {
    plugins: {
    	//自动添加css前缀
        autoprefixer: {},
        "postcss-px-to-viewport": {
            viewportWidth: 375,      // 视窗的宽度,对应的是我们设计稿的宽度,一般是750。
            unitPrecision: 3,        // 指定`px`转换为视窗单位值的小数位数(很多时候无法整除)
            viewportUnit: 'vw',      // 指定需要转换成的视窗单位,建议使用vw
            selectorBlackList: ['.ignore'],  // 指定不转换为视窗单位的类,可以自定义,可以无限添加,建议定义一至两个通用的类名
            minPixelValue: 1,       // 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值
            mediaQuery: false       // 允许在媒体查询中转换`px`
          }
    }
}

对css的一些设置,autoprefixer需要配合package.json下的browserslist才能生效。