手撸 webpack+vue 打包环境

手撸 webpack+vue 打包环境_第1张图片

概要

如果我们手动去配置一个webpack+vue的开发环境,这中间不免要使用到各种依赖。下图所示我在本项目中所用到的依赖包在本文章中以代码形式有展示。

如果我们还要使用别的更多依赖,那么我们就要使用更多的依赖。但是这些依赖的开发者不是同一个人,功能各不相同,带来的结果就是依赖的使用方法各有各的风格,每一个都要详尽去阅读他们的文档。于是vue-cli应运而生,简单几步操作就可以搭建出基于vuejs框架的开发环境,特别是vue-cli3还推出可视化操作,让我们的搭建更加快捷。然而在使用vue-cli之后,我们会发现一些瑕疵:打包出错或者不符合预期很难排查出错出、无法升级webpack等一些依赖包。webpack在官方文档中已经声明:他们会不停的优化打包性能,从性能上讲,低版本肯定不如高版本。

所以我就手撸了基于配置文件打包的项目,本项目的代码托管在github上,css预处理使用的是scss(可自行修改),vue、vuex、vue-router、axios、babel等均有配置,有兴趣的朋友可下载下来直接使用。本项目长期维护更新,如有不对的地方请大家issue。

webpack 的配置

大家在阅读webpack文档的时候,推荐大家查看英文文档,而不是中文。因为中文文档并没有及时翻译出来导致文档滞后版本。打包配置文件我们分为三个:webpack.base.js(基本配置)、webpack.dev.js(开发配置)、webpack.prod.js(生产配置),置于项目根部录下build文件夹下面。

webpack.base.js:

const path = require('path');
const pathResolve = filename => path.resolve(__dirname,'../',filename);
const htmlWebpackPlugin = require('html-webpack-plugin'); //文档地址:https://www.npmjs.com/package/html-webpack-plugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); //提取css为外部样式,文档地址:https://www.npmjs.com/package/mini-css-extract-plugin
const VueLoaderPlugin = require('vue-loader/lib/plugin'); //vue-loader文档:https://vue-loader.vuejs.org/zh/
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin'); // script标签添加async defer等属性;文档:https://github.com/numical/script-ext-html-webpack-plugin
const WebpackSpritesmithPlugin = require('webpack-spritesmith'); //生成雪碧图 文档:https://www.npmjs.com/package/webpack-spritesmith

const resourcesLoader = {//引入全局scss变量,文档:https://www.npmjs.com/package/sass-resources-loader
    loader: 'sass-resources-loader',
    options: {
        resources: [
            pathResolve('assets/css/mixins.scss')
        ]
    }
};

module.exports = env => {
    const devMode = !!env.development; //是否为开发模式
    const outputFileName = devMode 
                            ? 'js/[name].js' 
                            : 'js/[name].[chunkhash:7].js';

    return {
        entry: pathResolve('src/index.js'),
        output: {
            filename: outputFileName,
            path: pathResolve('dist'),
            publicPath: devMode ? '' : '/dist/'
        },
        module: {
            rules: [{
                test: /\.js$/,
                exclude: /node_modules/,
                loader: 'babel-loader'
            },{
                test: /\.(sa|sc|c)ss$/,
                exclude: /\.module\.(sa|sc|c)ss$/i,
                use: [
                    devMode ? 'style-loader' : MiniCssExtractPlugin.loader,//开发模式使用内联样式,生产环境提取css为外部样式
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 1
                        }
                    },
                    'postcss-loader',
                    'sass-loader',
                    resourcesLoader
                ]
            },{
                test: /\.module\.(sa|sc|c)ss$/,
                use: [
                    devMode ? 'style-loader' : MiniCssExtractPlugin.loader,//开发模式使用内联样式,生产环境提取css为外部样式
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 1,
                            modules: {//xxx.module.scss文件开启css modules
                                mode: 'local',
                                localIdentName: '[local]-[hash:5]'
                            }
                        }
                    },
                    'postcss-loader',
                    'sass-loader',
                    resourcesLoader
                ]
            },{
                test: /\.(png|jpg|gif|svg)$/,
                use: [{
                    loader: 'url-loader',
                    options: {
                        limit: 8192,
                        name: devMode ? '[name].[ext]' : '[name].[contenthash:7].[ext]',
                        outputPath: 'images'
                    }
                }]
            },{
                test: /\.(woff|woff2|eot|ttf|otf)$/,
                use: [{
                    loader: 'url-loader',
                    options: {
                        limit: 100000,
                        outputPath: 'fonts'
                    }
                }]
            },{
                test: /\.vue$/,
                loader: 'vue-loader'
            }]
        },
        resolve: {
            alias: {//路径别名
                '@': pathResolve('src/'),
                '@r': pathResolve('')
            },
            extensions: ['.js','.vue'], //自动解析的扩展
            modules: ['node_modules',pathResolve('dist/sprites')]
        },
        optimization: {
            moduleIds: 'hashed', //使用hash生成的值作为模块id来进行长缓存
            runtimeChunk: 'single', //分离webpack运行文件到一个单独文件
            splitChunks: { //splitChunksPlugin代码分割文档:https://webpack.js.org/plugins/split-chunks-plugin/
                chunks: 'all', //无论同步还是异步代码都进行分割
                minSize: 30000, //形成一个新代码块最小的体积
                minChunks: 1, //在分割之前,改代码块至少被复用1次
                maxAsyncRequests: 5,//按需加载的最大并行数
                maxInitialRequests: 3,//入口最大并行请求数
                cacheGroups: { //缓存组:如果满足vendor的条件,就按vender打包,否则按default打包
                    elementVendor: { // element-ui是按需引入,所以这个包会不断变化,单独打包
                        test: /[\\/]node_modules[\\/](element-ui)[\\/]/,
                        name: 'elementVendor',
                        priority: -20
                    },
                    vendors: { //其他依赖包分割打包(一次引入,长缓存)
                        test: /[\\/]node_modules[\\/]/,
                        name: 'vendor',
                        priority: -30, //打包优先级,值越大,优先级越高
                    }
                }
            }
        },
        plugins: [
            new htmlWebpackPlugin({ // 创建html入口,自动引入资源
                title: 'Vue config',
                filename: pathResolve('dist/index.html'),
                template: pathResolve('index.html'),
                favicon: pathResolve('favicon.ico'),
                minify: !devMode
            }),
            new ScriptExtHtmlWebpackPlugin({ // 为所有script添加defer属性
                defaultAttribute: 'defer'
            }),
            new WebpackSpritesmithPlugin({
                src: {
                    cwd: pathResolve('assets/icons'), // 小图标路径
                    glob: '*.png' // 小图标后缀
                },
                target: {
                    image: pathResolve('dist/sprites/sprites.png'), // 生成的雪碧图
                    css: pathResolve('dist/sprites/sprites.scss') // 生成的scss文件,调用时引入
                },
                apiOptions: {
                    cssImageRef: '~sprites.png' // css根据该设置指引sprite图
                }
            }),
            new VueLoaderPlugin()
        ]
    };
};

webpack.dev.js:

const webpackMerge = require('webpack-merge');
const path = require('path');
const pathResolve = filename => path.resolve(__dirname,'../',filename);
const base = require('./webpack.base.js');

//webpack使用环境变量文档: https://webpack.js.org/guides/environment-variables/
module.exports = env => {
    return webpackMerge.smart(base(env),{
        devtool: 'inline-source-map',
        devServer: {
            open: true,
            compress: true,
            hot: true,
            historyApiFallback: true,
            contentBase: pathResolve('dist'),
            proxy: {
                '/api': {
                    target: 'http://XXX/api',
                    pathRewrite: {
                        '^/api': '/backend'
                    },
                    changeOrigin: true
                }
            }
        },
        mode: 'development'
    });
};

webpack.prod.js:

const webpack = require('webpack');
const webpackMerge = require('webpack-merge'); //合并配置文件
const base = require('./webpack.base');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // 提取css文件到外部样式表中,文档:https://webpack.js.org/plugins/mini-css-extract-plugin/
const {
    CleanWebpackPlugin
} = require('clean-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin'); //压缩js文档:https://github.com/terser-js/terser#minify-options
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); //压缩css文档:https://webpack.js.org/plugins/mini-css-extract-plugin/#minimizing-for-production
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; // 打包分析工具:https://www.npmjs.com/package/webpack-bundle-analyzer

//webpack使用环境变量文档: https://webpack.js.org/guides/environment-variables/
module.exports = env => {
    return webpackMerge.smart(base(env),{
        mode: 'production',
        devtool: 'source-map', // 调试生产代码,不需要请删除
        performance: {
            maxEntrypointSize: 2000000, // 打包后入口网页css+js超过2M时,警告提示
            maxAssetSize: 800000, //引入资源超过800K,警告提示
        },
        optimization: {
            minimizer: [
                new TerserPlugin({// 压缩js代码
                    parallel: true,//使用多进程并行执行任务来提高构建效率
                    sourceMap: true,// 将错误消息位置映射到模块
                    cache: true,// 启用文件缓存
                    terserOptions: {
                        compress: {
                            drop_console: false //设置为true清除console.log打印
                        }
                    }
                }),
                new OptimizeCssAssetsPlugin() //压缩css文件
            ]
        },
        plugins: [
            new CleanWebpackPlugin(), // 打包前清除dist目录
            new MiniCssExtractPlugin({ // 提取css文件到外部样式表中
                filename: 'css/[name].[contenthash:7].css'
            }),
            new BundleAnalyzerPlugin() // 打包大小分析
        ]
    });
}

使用element-ui框架按需加载,配置babel presets(babel.config.js):

//babel 配置文件

module.exports = api => {
    api.cache(true); // 缓存这个配置文件,不用每次编译都导入

    const presets = [
        [
            '@babel/preset-env',
            {
                'useBuiltIns': 'usage', // 按需引入babel-polyfill
                'modules': false,
                'corejs': 3 // 安装corejs3一定要指定版本,不然会找不到目录报错
            }
        ]
    ];

    const plugins = [
        [
            'component',//按需加载element-ui
            {
                libraryName: 'element-ui',
                styleLibraryName: 'theme-chalk'
            }
        ]
    ];

    return {
        presets,
        plugins
    };
}

css自动补全浏览器前缀(postcss.config.js):

module.exports = {
    plugins: [
        require('autoprefixer')
    ]
};

然后我们的项目执行脚本和browserlist配置都放在package.sjon配置中:

{
  "name": "vue_config",
  "version": "1.0.0",
  "description": "build vue project config by webpack",
  "private": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack-dev-server --env.development --config ./build/webpack.dev.js",
    "build": "webpack --env.production --config ./build/webpack.prod.js"
  },
  "author": "Zoro Yang",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.5.5",
    "@babel/polyfill": "^7.4.4",
    "@babel/preset-env": "^7.5.5",
    "autoprefixer": "^9.6.1",
    "axios": "^0.19.0",
    "babel-loader": "^8.0.6",
    "babel-plugin-component": "^1.1.1",
    "clean-webpack-plugin": "^3.0.0",
    "css-loader": "^3.1.0",
    "element-ui": "^2.11.1",
    "html-webpack-plugin": "^3.2.0",
    "optimize-css-assets-webpack-plugin": "^5.0.3",
    "sass-loader": "^7.1.0",
    "style-loader": "^0.23.1",
    "terser-webpack-plugin": "^1.4.1",
    "vue": "^2.6.10",
    "vue-loader": "^15.7.1",
    "vue-template-compiler": "^2.6.10",
    "webpack": "^4.39.1",
    "webpack-cli": "^3.3.6",
    "webpack-dev-server": "^3.7.2"
  },
  "dependencies": {
    "core-js": "^3.2.1",
    "file-loader": "^4.2.0",
    "mini-css-extract-plugin": "^0.8.0",
    "node-sass": "^4.12.0",
    "postcss-loader": "^3.0.0",
    "sass-resources-loader": "^2.0.1",
    "url-loader": "^2.1.0",
    "vue-router": "^3.1.0",
    "vuex": "^3.1.1",
    "webpack-merge": "^4.2.1"
  },
  "browserslist": [
    "last 10 versions",
    "> 95%",
    "not ie <= 8",
    "Firefox >= 20",
    "Android >= 4.0",
    "iOS >= 8"
  ],
  "sideEffects": [ // 配置会去除无用的代码,但是不应去除无用css样式
    "*.css",
    "*.scss"
  ]
}

css背景图中我们生成了雪碧图,配置中生成的是scss文件,使用demo如下:

/* src/views/Home/index.module.scss */

@import '~@r/dist/sprites/sprites.scss';

.friends{
    @include sprite($friends);
}
.qq{
    @include sprite($qq);
}
.wechat{
    @include sprite($wechat);
}

sprite混入参数就是引入图片名称(建议不要生成插入过多图片,以免最终生成图片过大,图标的话尽量使用图标库来生成,例如:阿里巴巴矢量图库)。

然后就是在入口文件的开始我们的项目了。本文参考文档:webpack官方文档

你可能感兴趣的:(前端,js)