有个需求是写移动端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才能生效。
- 参考阅读:webpack4多页面,多环境配置