webpack学习笔记(二)环境分离+多页面开发配置

本章描述的主要功能
  • postcss-loader
  • nodemon
  • 使用less/scss/sass
  • 封装资源处理,样式处理,并加入全局config(仿照vue)
  • 生产开发分离
  • 开发环境配置压缩打包
  • chunk接受
  • 多页面开发原理和配置介绍

代码接着上一章webpack学习笔记(一)从零开始构建基础webpack项目

postcss-loader

postcss-loader介绍 使用postcss-loader来为css样式自动加入浏览器前缀,postcss-loader的使用需要配合相对应的postcss.config.js文件,以及其插件,这里我只使用了autoprefixer插件

npm i -D postcss-loader autoprefixer

加入postcss-loader,创建postcss.config.js文件并写代码

/* webpack.js */
...
{
    // 对css文件使用loader
    test: /\.css$/,
    // 使用插件提取样式
    use: ExtractTextPlugin.extract({
        // 样式loader运行顺序 加入postcss-loader
        use: ['css-loader', 'postcss-loader'],
        // 若上述处理进行顺利,执行style-loader并导出文件
        fallback: 'style-loader',
        // 样式覆盖路径 处理背景图之类
        publicPath: '../'
    })
}
...

/* postcss.config.js */
module.exports = {
    plugins: [
        require('autoprefixer')({
            browsers: ['last 5 versions']
        })
    ]
};

写一下样式并看效果,这里直接打包生成文件看效果npm run build

这里写图片描述
已经加入相关了浏览器前缀

nodemon

先附上nodemon的GitHub介绍,我们用它来监控进程,这样我们就不需要每次修改之后还需要重启node服务,下载安装包,并修改package.json

npm i -D nodemon

"scripts": {
    "start": "nodemon --watch build/ --exec \"webpack-dev-server  --config build/webpack.js\"",
    "build": "webpack --config build/webpack.js"
  }

我们使用nodemon监控build这个文件夹,并且开辟一个子进程运行webpack-dev-server,然后我们可以先运行npm start,之后在运行本地服务的时候修改build文件夹下面的文件就不再需要重启服务

使用less/scss/sass

首先下载loader,并且修改.cssloader,添加对.less .scss .sass文件的处理,.scss.sass公用一个loader,并且执行sass-loader需要node-sass来配合使用

下载安装包

npm i -D less-loader sass-loader node-sass

增加对应的处理loader,这里就先不分离css了,直接在浏览器上看

/* webpack.js */
...
{
    // 对less文件使用loader
    test: /\.less$/,
    use: ['style-loader', 'css-loader', 'postcss-loader', 'less-loader']
},
{
    // 对sass文件使用loader
    test: /\.sass$/,
    use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
},
{
    // 对scss文件使用loader
    test: /\.scss$/,
    use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
}
...

创建.less .scss .sass文件,随便写点什么,然后由main.js引入

源码如下
这里写图片描述

运行结果如下
这里写图片描述

封装资源处理,样式处理,并加入全局config(仿照vue)

考虑到之后要的分离,不可能每一个环境配置里面都写一遍loader,而且样式的loader基本相似,顶多预处理的loader不一样而已,用代码来生成这些吧

封装处理资源类(img, media)

build目录下创建utils.jswebpack.js引入utils.js,并修改图片和媒体文件的loader处理

/* utils.js */
const path = require('path');

// 静态资源文件+第三方资源存放文件夹名称
const staticDir = 'static';

// 资源路径 背景图片 bgm等,传入路径以及文件名 ex img/xxx.png
exports.assetsPath = _path => path.posix.join(staticDir, _path);

/* webpack.js */
const utils = require('./utils');
...
{
    // 对下列资源文件使用loader
    test: /\.(gif|jpg|png|woff|svg|eot|ttf)\??.*$/,
    loader: 'url-loader',
    options: {
        // 小于10kb将会转换成base64
        limit: 10240,
        // 大于10kb的资源输出地[name]是名字[ext]后缀
        name: utils.assetsPath('img/[name].[hash:6].[ext]')
    }
},
{
    test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
    loader: 'url-loader',
    options: {
        limit: 10240,
        name: utils.assetsPath('media/[name].[hash:6].[ext]')
    }
}
...

这里也就不运行看效果了,只是改了一下执行方法而已~

封装处理样式类

无论怎样封装,最终输出的都必须是对象或类数组对象,类似下面的形式

// 分离样式
{
    // xxx.ext
    test: /\.xxx$/,
    // 使用插件提取样式
    use: ExtractTextPlugin.extract({
        // 样式loader运行顺序
        use: [xx, xx, xx],
        fallback: 'style-loader',
        // 样式覆盖路径 处理背景图之类
        publicPath: '../../'
    })
}
// 不分离样式
{
    // xxx.ext
    test: /\.xxx$/,
    use: [xx, xx, xx]
}

第一步,在build下创建一个全局配置的config.js文件,之后多页面开发也会用到

/* config.js */
module.exports = {
    // 开发
    dev: {
         // .map文件是否生成
        sourceMap: true,
        // 是否分离样式
        extract: false
    },
    // 生产
    prod: {
        sourceMap: false,
        extract: true
    }
};

重头戏来了,utils.js加入样式处理函数,具体代码和功能附上

const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
// 静态资源文件+第三方资源存放文件夹名称
const staticDir = 'static';

// 资源路径 背景图片 bgm等,传入路径以及文件名 ex img/xxx.png
exports.assetsPath = _path => path.posix.join(staticDir, _path);

/**
 * 样式处理 生成样式loader对象,并放入数组里面
 * @param { Object } options 对应config.js里面的对象属性
 * @return { Array }
 */
exports.styleLoader = (options) => {
    options = options || {};
    // 两个固定的loader
    const cssLoader = {
        loader: 'css-loader',
        options: {
            sourceMap: options.sourceMap
        }
    };
    const postcssLoader = {
        loader: 'postcss-loader',
        options: {
            sourceMap: options.sourceMap
        }
    };
    /**
     * 生成{test: xxx, use: xxx}中use的值
     * @param { String } loader loader名 less, scss, sass, 可为空,空默认css
     */
    function generateLoaders(loader) {
        // 加入两个固定loader
        const loaders = [cssLoader, postcssLoader];
        if (loader) {
            // 通过名字加入loader,然后从末尾压入数组,感谢loader的执行顺序是从后往前
            loaders.push({
                loader: `${loader}-loader`,
                options: {
                    // 是否生成.map
                    sourceMap: options.sourceMap
                }
            });
        }
        // 是否需要提取样式
        if (options.extract) {
            return ExtractTextPlugin.extract({
                use: loaders,
                fallback: 'style-loader',
                publicPath: '../../'
            });
        }
        return ['style-loader'].concat(loaders);
    }
    // 通过上述函数生成对应的值
    const loaders = {
        css: generateLoaders(),
        less: generateLoaders('less'),
        scss: generateLoaders('sass'),
        sass: generateLoaders('sass')
    };
    // console.log(loaders);
    // 打印结果如下
    // {
    //     css:
    //     ['style-loader',
    //         { loader: 'css-loader', options: [Object] },
    //         { loader: 'postcss-loader', options: [Object] }],
    //     less:
    //     ['style-loader',
    //         { loader: 'css-loader', options: [Object] },
    //         { loader: 'postcss-loader', options: [Object] },
    //         { loader: 'less-loader', options: [Object] }],
    //     scss:
    //     ['style-loader',
    //         { loader: 'css-loader', options: [Object] },
    //         { loader: 'postcss-loader', options: [Object] },
    //         { loader: 'sass-loader', options: [Object] }],
    //     sass:
    //     ['style-loader',
    //         { loader: 'css-loader', options: [Object] },
    //         { loader: 'postcss-loader', options: [Object] },
    //         { loader: 'sass-loader', options: [Object] }]
    // }
    const output = [];
    // 遍历并加入正则,得到最终的loader
    for (const key in loaders) {
        const loader = loaders[key];
        // console.log(new RegExp(`\\.${key}$`));
        output.push({
            test: new RegExp(`\\.${key}$`),
            use: loader
        });
    }
    return output;
    // console.log(JSON.stringify(output));
    // 运行结果如下 test其实是有值的,上述打印可见
    // [
    //     {
    //         "test": {},
    //         "use": ["style-loader", { "loader": "css-loader", "options": {} }, { "loader": "postcss-loader", "options": {} }]
    //     },
    //     {
    //         "test": {},
    //         "use": ["style-loader", { "loader": "css-loader", "options": {} }, { "loader": "postcss-loader", "options": {} }, { "loader": "less-loader", "options": {} }]
    //     },
    //     {
    //         "test": {},
    //         "use": ["style-loader", { "loader": "css-loader", "options": {} }, { "loader": "postcss-loader", "options": {} }, { "loader": "sass-loader", "options": {} }]
    //     },
    //     {
    //         "test": {},
    //         "use": ["style-loader", { "loader": "css-loader", "options": {} }, { "loader": "postcss-loader", "options": {} }, { "loader": "sass-loader", "options": {} }]
    //     }
    // ]
};

webpack.js中使用这个方法,删除原有的样式loader处理方法,并在loaders数组中通过...utils.styleLoader(),附上目前webpack.js代码

/* webpack.js */
// 路径解析
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const rm = require('rimraf');
const utils = require('./utils');

// 删除
rm(path.join(__dirname, '../dist'), (err) => {
    if (err) throw err;
});

module.exports = {
    // js文件入口
    entry: path.join(__dirname, '../src/script/main.js'),
    // 输出到dist目录
    output: {
        path: path.join(__dirname, '../dist'),
        filename: 'static/js/[name].[hash].js'
    },
    // devServer: {
    //     host: '192.168.0.101',
    //     port: '2018'
    // },
    module: {
        loaders: [
            ...utils.styleLoader(),
            {
                // 对js文件使用loader
                test: /\.js$/,
                use: 'babel-loader'
            },
            {
                test: /\.html$/,
                loader: 'html-loader',
                options: {
                    // 标签+属性
                    attrs: ['img:src', 'audio:src', 'video:src']
                }
            },
            {
                // 对下列资源文件使用loader
                test: /\.(gif|jpg|png|woff|svg|eot|ttf)\??.*$/,
                loader: 'url-loader',
                options: {
                    // 小于10kb将会转换成base64
                    limit: 10240,
                    // 大于10kb的资源输出地[name]是名字[ext]后缀
                    name: utils.assetsPath('img/[name].[hash:6].[ext]')
                }
            },
            {
                test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
                loader: 'url-loader',
                options: {
                    limit: 10240,
                    name: utils.assetsPath('media/[name].[hash:6].[ext]')
                }
            }
        ]
    },
    plugins: [
        new ExtractTextPlugin('static/css/[name].[hash].css'),
        new HtmlWebpackPlugin({
            // 生成的html文件名,该文件将被放置在输出目录
            filename: 'index.html',
            // 源html文件路径
            template: path.join(__dirname, '../src/page/index.html')
        }),
        new CopyWebpackPlugin([{
            // 源文件目录
            from: path.join(__dirname, '../static'),
            // 目标目录 dist目录下
            to: 'static',
            // 筛选过滤,这里复制所有文件,连同文件夹
            ignore: ['.*']
        }])
    ]
};

环境分离

一般工作中都会建立两个环境,开发环境,生成环境,话不多说,先在build下面创建两个环境文件再说,开发webpack.dev.js,生产webpack.prod.js,然后修改package.json中的运行命令

/* package.json */
"scripts": {
    "start": "nodemon --watch build/ --exec \"webpack-dev-server  --config build/webpack.dev.js\"",
    "build": "webpack --config build/webpack.prod.js"
}
基础配置

在之前的webpack.js中有一些配置不需要分离出去,如.js .html,图片和媒体资源的处理,html-webpack-plugin的配置,copy-webpack-plugin等这些基础的配置,开发和生产环境可以共用,其它配置需要分离出去,删除配置之后的代码如下

/* webpack.js */
// 路径解析
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const utils = require('./utils');

module.exports = {
    // js文件入口
    entry: path.join(__dirname, '../src/script/main.js'),
    // 输出到dist目录
    output: {
        path: path.join(__dirname, '../dist'),
        filename: 'static/js/[name].[hash].js'
    },
    // devServer: {
    //     host: '192.168.0.101',
    //     port: '2018'
    // },
    module: {
        loaders: [
            {
                // 对js文件使用loader
                test: /\.js$/,
                use: 'babel-loader'
            },
            {
                test: /\.html$/,
                loader: 'html-loader',
                options: {
                    // 标签+属性
                    attrs: ['img:src', 'audio:src', 'video:src']
                }
            },
            {
                // 对下列资源文件使用loader
                test: /\.(gif|jpg|png|woff|svg|eot|ttf)\??.*$/,
                loader: 'url-loader',
                options: {
                    // 小于10kb将会转换成base64
                    limit: 10240,
                    // 大于10kb的资源输出地[name]是名字[ext]后缀
                    name: utils.assetsPath('img/[name].[hash:6].[ext]')
                }
            },
            {
                test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
                loader: 'url-loader',
                options: {
                    limit: 10240,
                    name: utils.assetsPath('media/[name].[hash:6].[ext]')
                }
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            // 生成的html文件名,该文件将被放置在输出目录
            filename: 'index.html',
            // 源html文件路径
            template: path.join(__dirname, '../src/page/index.html')
        }),
        new CopyWebpackPlugin([{
            // 源文件目录
            from: path.join(__dirname, '../static'),
            // 目标目录 dist目录下
            to: 'static',
            // 筛选过滤,这里复制所有文件,连同文件夹
            ignore: ['.*']
        }])
    ]
};
webpack-merge

先附上介绍webpack-merge 使用这个包,我们可以合并对象或数组,以及函数,这样我们就可以在开发和生产环境的代码中引入基础配置,用这个包来合并两者,达到环境的构建

npm i -D webpack-merge

暂停一下

写这篇博客的时候,发现自己漏了sourceMap的生成,按照上一篇的配置,即便在config.js里面设置sourceMap: true也不会生成.map文件,原因是我忘记在webpack配置里面加入devtool: 'inline-source-map'(开发),devtool: 'source-map'(生产)

开发环境

在开发环境中,我们并需要压缩代码,也不需要分离样式(不过部分移动端手机浏览器不分离样式就没法加载样式),只需开启本地服务,进行项目调试,引入webpack.js通过webpack-merge进行添砖加瓦

const baseWebpack = require('./webpack');
const merge = require('webpack-merge');
const config = require('./config');
const utils = require('./utils');
// 部分移动端浏览器不提取样式无法被加载
// const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = merge(baseWebpack, {
    devtool: 'inline-source-map',
    module: {
        loaders: utils.styleLoader(config.dev)
    }
    // 先保留一下
    // plugins: [
    //     new ExtractTextPlugin('static/css/style.css')
    // ]
});
生产环境

生产环境中,我们需要删除dist文件夹,需要压缩css和js代码,先下载需要两个包uglifyjs-webpack-pluginjs压缩,压缩css我们只需要改动utils.js(ps:写这片博客前我用的是optimize-css-assets-webpack-plugin来压缩的,按照loader的执行顺序,只需要在css-loader加入minimize属性即可,然后整理这篇文章的时候发现这样也可以压缩)

npm install -D uglifyjs-webpack-plugin

/* webpack.prod.js */
const path = require('path');
const baseWebpack = require('./webpack');
const merge = require('webpack-merge');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const config = require('./config');
const rm = require('rimraf');
const utils = require('./utils');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

// 删除
rm(path.join(__dirname, '../dist'), (err) => {
    if (err) throw err;
});

module.exports = merge(baseWebpack, {
    devtool: 'source-map',
    module: {
        loaders: utils.styleLoader(config.prod)
    },
    plugins: [
        new ExtractTextPlugin('static/css/[name].[hash].css'),
        new UglifyJsPlugin({
            // 是否生成.map
            sourceMap: config.prod.sourceMap
        })
    ]
});

打包然后浏览器打开dist下面的index.html。js和css都已被压缩,另外我添加了一个name.js

这里写图片描述

浏览器打开结果(sourceMap生效了)
这里写图片描述

多页面开发

前提条件chunk

如果不了解chunk,即便是我们能生成多个html文件,但其所引用的js文件始终是同一个,这样就达不到多页面开发的要求,毕竟a页面不需要b页面的某些东西。。。

这里简单说一下chunk,我们现在在创建entry的时候只提供了一个入口文件main.js,如果我们添加多个入口文件,在src/script/创建demo1.js demo2.js 然后我们修改entry的写法,这些入口文件就是chunk,其chuankNameentry的键名,也就是之前一直使用[name]的值

/* webpack.js */
entry: {
    index: path.join(__dirname, '../src/script/main.js'),
    demo1: path.join(__dirname, '../src/script/demo1.js'),
    demo2: path.join(__dirname, '../src/script/demo2.js')
}

直接打包生成文件看一下npm run build

这里写图片描述

可以看到生成了index, demo1, demo2这些js文件,不再是之前的main名字的形式,说明我们的设置的chuankName生效了,但是我们的index.html只想要index.js这个文件,怎么弄呢,别着急,html-webpack-plugin插件早已提供的方法,其提供了一个chunks属性,值为一个数组,里面放入需要提取的入口文件的chuankName即可

/* webpack.js */
new HtmlWebpackPlugin({
    // 生成的html文件名,该文件将被放置在输出目录
    filename: 'index.html',
    // 只提取chuankName为index的入口文件打包生成的js
    chunks: ['index'],
    // 源html文件路径
    template: path.join(__dirname, '../src/page/index.html')
})

继续打包一下npm run build,只引入了一个js

这里写图片描述

加入一个新的html文件,我们创建demo1.html,并且使用html-webpack-plugin来处理它,让它所对应的入口文件为demo1.js

/* webpack.js */
new HtmlWebpackPlugin({
    // 生成的html文件名,该文件将被放置在输出目录
    filename: 'demo1.html',
    chunks: ['demo1'],
    // 源html文件路径
    template: path.join(__dirname, '../src/page/demo1.html')
})

继续打包
这里写图片描述

了解了上述原理,多页面开发的配置就一目了然,我们需要创建入口文件entry对象,还需要配置html文件的HtmlWebpackPlugin插件的数组,并且为了统一配置,我们需要保持entry对象的键名和html文件的文件名相同,直白一点就是我们创建一个html的时候,需要同时创建一个同名的js文件,并且在entry对像中注册登记,键名就是其文件名,举个栗子如下

/* 栗子 */
module.exports = {
    /* 四个入口文件 */
    entry: {
        /* 键名 文件名保持一致 */
        index: path.join(__dirname, '../src/script/index.js'),
        demo1: path.join(__dirname, '../src/script/demo1.js'),
        demo2: path.join(__dirname, '../src/script/demo2.js'),
        demo3: path.join(__dirname, '../src/script/demo3.js')
    },
    ....
    plugins: [
         /* 所对应的html文件 */
        new HtmlWebpackPlugin({
            filename: 'index.html',
            chunks: ['index'],
            template: path.join(__dirname, '../src/page/index.html')
        }),
        new HtmlWebpackPlugin({
            filename: 'demo1.html',
            chunks: ['demo1'],
            template: path.join(__dirname, '../src/page/demo1.html')
        }),
        new HtmlWebpackPlugin({
            filename: 'demo2.html',
            chunks: ['demo2'],
            template: path.join(__dirname, '../src/page/demo2.html')
        }),
        new HtmlWebpackPlugin({
            filename: 'demo3.html',
            chunks: ['demo3'],
            template: path.join(__dirname, '../src/page/demo3.html')
        })
    ]
};

看完上面的栗子,所谓的多页面开发无非就是通过简单的代码生成上述栗子中的entry对象 HtmlWebpackPlugin数组,而且我们要求名称一致,这就便利了我们来通过代码生成这些配置

撸起袖子加油干

通过代码生成对象和数组

/* webpack.js */
// 所有多页面的文件名
const PageName = ['index', 'demo1', 'demo2'];
// entry对象
const Entries = {};
// 插件数组
const HtmlPlugins = [];
// 生成
PageName.forEach((page) => {
    const htmlPlugin = new HtmlWebpackPlugin({
        filename: `${page}.html`,
        template: path.join(__dirname, `../src/page/${page}.html`),
        chunks: [page]
    });
    HtmlPlugins.push(htmlPlugin);
    Entries[page] = path.join(__dirname, `../src/script/${page}.js`);
});
/* 配置代码 省略了很多0.0 */
module.exports = {
    // js文件入口
    entry: Entries,
    plugins: [
        ...HtmlPlugins,
    ]
};

到这里时候我们需要改一下入口文件的名字,之前我们一直使用的是main.js,现在我们需要将其改为index.js,运行一下

这里写图片描述

html,js一一对应的被生成了,我们只需要手动输入html文件名就能进行多页面,不过为了规范,我们之前创建了一个全局对象config.js,我们可以将这PageName的值写到config.js里面,然后在webpack.js里面调用

/* config.js */
module.exports = {
    // 多页面配置
    pageNames: [
        'index',
        'demo1',
        'demo2'
    ],
    // 开发
    dev: {
        sourceMap: true,
        extract: false
    },
    // 生产
    prod: {
        sourceMap: true,
        extract: true
    }
};

/* webpack.js */
const config = require('./config');
config.pageNames.forEach((page) => {
    ....
});

如果进行多页面开发,就自行创建对应文件并在config.js内注册,如果不使用nodemon就需要重启node

基础的多页面开发的原理以及配置就说到这里了,下一章的笔记将会描述如何创建模版,提取公共模块~

依旧附上本章GitHub源码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值