在找一份相对完整的Webpack项目配置指南么?这里有

 

Webpack已经出来很久了,相关的文章也有很多,然而比较完整的例子却不是很多,让很多新手不知如何下脚,下脚了又遍地坑

说实话,官方文档是蛮乱的,而且有些还是错的错的。。很多配置问题只有爬过坑才知道

本文首先介绍Webpack(3)的一些基础知识,然后以一个已经完成的小Demo,逐一介绍如何在项目中进行配置

该Demo主要包含编译Sass/ES6,提取(多个)CSS文件,提取公共文件,模块热更新替换,开发与线上环境区分,使用jQuery插件的方式、页面资源引入路径自动生成(可指定生成位置),热更新编译模版文件自动生成webpack服务器中的资源路径,编写一个简单的插件,异步加载模块 等基础功能

应该能帮助大家更好地在项目中使用Webpack3来管理前端资源

本文比较啰嗦,可以直接看第四部分Webpack3配置在Demo中的应用,或者直接去Fork这个Demo边看边玩

 

Webpack已升级为v4版本,优化之后性能提升好几倍,请移步这个 webpack4项目配置Demo,以及 这篇升级优化点 

 

首先,学习Webpack,还是推荐去看官方文档,还是挺全面的,包括中文的和英文的,以及GitHub上关于webpack的项目issues,还有就是一些完整了例子,最后就是得自己练手配置,才能在过程中掌握好这枯燥的配置。

 

 

一 、为什么要用Webpack

首先,得知道为什么要用webpack

前端本可以直接HTML、CSS、Javascript就上了,不过如果要处理文件依赖、文件合并压缩、资源管理、使用新技术改善生活的时候,就得利用工具来辅助了。

以往有常见的模块化工具RequireJS,SeaJS等,构建工具Grunt、Gulp等,新的技术Sass、React、ES6、Vue等,要在项目中使用这些东西,不用工具的话就略麻烦了。

其实简单地说要聚焦两点:模块化以及自动构建。

模块化可以使用RequireJS来处理依赖,使用Gulp来进行构建;也可以使用ES6新特性来处理模块化依赖,使用webpack来构建

两种方式都狠不错,但潮流所驱,后者变得愈来愈强大,当然也不是说后者就替代了前者,只是大部分情况下,后者更好

 

二、什么是Webpack

如其名,Web+Pack 即web的打包,主要用于web项目中打包资源进行自动构建。

Webpack将所有资源视为JS的模块来进行构建,所以对于CSS,Image等非JS类型的文件,Webpack会使用相应的加载器来加载成其可识别的JS模块资源

通过配置一些信息,就能将资源进行打包构建,更好地实现前端的工程化

 

三、Webpack的基础配置

可以认为Webpack的配置是4+n模式,四个基本的 entry(入口设置)output(输出设置)loader(加载器设置)、plugin(插件设置),然后加上一些特殊功能的配置。

使用Webpack首先需要安装好NodeJS

node -v
npm -v

确保已经可以使用node,使用NPM包管理工具来安装相应依赖包(网络环境差可以使用淘宝镜像CNPM来安装)

npm install -g cnpm --registry=https://registry.npm.taobao.org
cnpm -v

全局安装好webpack包

npm i -g webpack
webpack -v

 

1. webpack的配置方式主要有三种

1. 通过cli命令行传入参数 

webpack ./src.js -o ./dest.js --watch --color 

2. 通过在一个配置文件设置相应配置,导出使用

// ./webpack.config.js文件
module.exports = {
   context: ... entry: { }, output: { } };
// 命令行调用(不指定文件时默认查找webpack.config.js) webpack [--config webpack.config.js]

3. 通过使用NodeJS的API配置

这个和第二点有点类似,区别主要是第二种基本都是使用{key: value}的形式配置的,API则主要是一些调用

另外,某些插件的在这两种方式的配置上也有一些区别

 

最常用的是第二种,其次第三种,第一种不太建议单独使用(因为相对麻烦,功能相对简单)

 

2. 常见的几个配置属性

1. context  绝对路径

一般当做入口文件(包括但不限于JS、HTML模板等文件)的上下文位置,

默认使用当前目录,不过建议还是填上一个

// 上下文位置
context: path.resolve(__dirname, 'static')

2. entry  模块入口文件设置

可以接受字符串表示一个入口文件,不过一般来说是多页应用多,就设置成每页一个入口文件得了

比如home对应于一个./src/js/home模块,这里的key会被设置成webpack的一个chunk,即最终webpack会又三个chunkname:home | detail | common

也可以对应于多个模块,用数组形式指定,比如这里把jquery设置在common的chunk中

也可以设置成匿名函数,用于动态添加的模块

// 文件入口配置
    entry: {
        home: './src/js/home',
        detail: './src/js/detail',
        // 提取jquery入公共文件
        common: ['jquery']
    },

3. resolve 处理资源的查找引用方式

如上方其实是省略了后JS缀,又比如想在项目中引入util.js 可以省略后缀

import {showMsg} from './components/util';
// 处理相关文件的检索及引用方式
    resolve: {
        extensions: ['.js', '.jsx', '.json'],
        modules: ['node_modules'],
        alias: {

        }
    },

4. output 设置文件的输出

最基础的就是这三个了

path指定输出目录,要注意的是这个目录影响范围是比较大,与该chunk相关的资源生成路径是会基于这个路径的

filename指定生成的文件名,可以使用[name] [id]来指定相应chunk的名称,如上的home和detail,用[hash]来指定本次webpack编译的标记来防缓存,不过建议是使用[chunkhash]来依据每个chunk单独来设置,这样不改变的chunk就不会变了

hash放在?号之后的好处是,不会生成新的文件(只是文件内容被更改了),同时hash会附在引用该资源的URL后(如script标签中的引用)

publicPath指定所引用资源的目录,如在html中的引用方式,建议设置一个

 

// 文件输出配置
    output: {
        // 输出所在目录
        path: path.resolve(__dirname, 'static/dist/js'),
        filename: '[name].js?[chunkhash:8]'// 设置文件引用主路径
        publicPath: '/public/static/dist/js/'
    }

5.devtool指定sourceMap的配置

如果开启了,就可以在浏览器开发者工具查看源文件

// 启用sourceMap
    devtool: 'cheap-module-source-map',

比如这里就是对应的一个source Map,建议在开发环境下开启,帮助调试每个模块的代码

这个配置的选项是满多的,而且还可以各种组合,按照自己的选择来吧

6. module指定模块如何被加载

通过设置一些规则,使用相应的loader来加载

主要就是配置modulerules规则组,通过use字段指定loader,如果只有一个loader,可以直接用字符串,loader要设置options的就换成数组的方式吧

或者使用多个loader的时候,也用数组的形式,规则不要用{ }留空,在windows下虽然正常,但在Mac下会报错提示找不到loader

多个loader遵循从右到左的pipe 的方式,如下 eslint-loader是先于babel-loader执行的

通过excludeinclude等属性再确定规则的匹配位置

// 模块的处理配置,匹配规则对应文件,使用相应loader配置成可识别的模块
    module: {
        rules: [{
           test: /\.css$/,
           use: 'css-loader'
        }, {
            test: /\.jsx?$/,
            // 编译js或jsx文件,使用babel-loader转换es6为es5
            exclude: /node_modules/,
            use: [{
                loader: 'babel-loader',
                options: {

                }
            }, {
                loader: 'eslint-loader'
            }]
        }

7.  plugins设置webpack配置过程中所用到的插件

比如下方为使用webpack自带的提取公共JS模块的插件

// 插件配置
    plugins: [
        // 提取公共模块文件
        new webpack.optimize.CommonsChunkPlugin({
            chunks: ['home', 'detail'],
            filename: '[name].js',
            name: 'common'
        }),
       new ...

]

这就是webpack最基础的东西了,看起来内容很少,当然还有其他很多,但复杂的地方在于如何真正去使用这些配置

 

四、Webpack配置在Demo中的应用

下面以一个相对完整的基础Demo着手,介绍一下几个基本功能该如何配置

Demo项目地址   建议拿来练练

 

1. 搭建个服务器

既然是Demo,至少就得有一个服务器,用node来搭建一个简单的服务器,处理各种资源的请求返回

新建一个服务器文件server.js,以及页面文件目录views,其他资源文件目录public

服务器文件很简单,请求什么就返回什么,外加了一个gzip的功能

let http = require('http'),
    fs = require('fs'),
    path = require('path'),
    url = require('url'),
    zlib = require('zlib');

http.createServer((req, res) => {
    let {pathname} = url.parse(req.url),
        acceptEncoding = req.headers['accept-encoding'] || '',
        referer = req.headers['Referer'] || '',
        raw;

    console.log('Request: ', req.url);

    try {
        raw = fs.createReadStream(path.resolve(__dirname, pathname.replace(/^\//, '')));

        raw.on('error', (err) => {
            console.log(err);

            if (err.code === 'ENOENT') {
                res.writeHeader(404, {'content-type': 'text/html;charset="utf-8"'});
                res.write('<h1>404错误</h1><p>你要找的页面不存在</p>');
                res.end();
            }
        });

        if (acceptEncoding.match(/\bgzip\b/)) {
            res.writeHead(200, { 'Content-Encoding': 'gzip' });
            raw.pipe(zlib.createGzip()).pipe(res);
        } else if (acceptEncoding.match(/\bdeflate\b/)) {
            res.writeHead(200, { 'Content-Encoding': 'deflate' });
            raw.pipe(zlib.createDeflate()).pipe(res);
        } else {
            res.writeHead(200, {});
            raw.pipe(res);
        }
    } catch (e) {
        console.log(e);
    }
}).listen(8088);

console.log('服务器开启成功', 'localhost:8088/');

2. 设置基础项目目录

页面文件假设采用每一类一个目录,目录下的tpl为源文件,另外一个为生成的目标页面文件

/public目录下,基本配置文件就放在根目录下,JS,CSS,Image等资源文件就放在/public/static目录下

我们要利用package.json文件来管理编译构建的包依赖,以及设置快捷的脚本启动方式,所以,先在/public目录下执行 npm init

public/static/dist目录用来放置编译后的文件目录,最终页面引用的将是这里的资源

public/static/imgs目录用来放置图片源文件,有些图片会生成到dist中

public/static/libs目录主要用来放置第三方文件,也包括那些很少改动的文件

public/static/src 用来放置js和css的源文件,相应根目录下暴露一个文件出来,公共文件放到相应子目录下(如js/componentsscss/util

 

最后文件结构看起来是这样的,那就可以开干了

 

 

3. 开发和生产环境的Webpack配置文件区分

首先在项目目录下安装webpack吧

npm i webpack --save-dev

用Webpack来构建,在开发环境和生产环境的配置还是有一些区别的,构建是耗时的,比如在开发环境下就不需要压缩文件、计算文件hash、提取css文件、清理文件目录这些辅助功能了,而可以引入热更新替换来加快开发时的模块更新效率。

所以建议区分一下两个环境,同时将两者的共同部分提取出来便于维护

NODE_ENV是nodejs在执行时的环境变量,webpack在运行构建期间也可以访问这个变量,所以我们可以在devprod下配置相应的环境变量

这个配置写在package.json里的scripts字段就好了,比如

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build:dev": "export  NODE_ENV=development && webpack-dev-server --config webpack.config.dev.js",
    "build:prod": "export NODE_ENV=production && webpack --config webpack.config.prod.js --watch "
  },

这样一来,我们就可以直接用 npm run build:prod来执行生产环境的配置命令(设置了production的环境变量,使用prod.js)

直接用npm run build:dev来执行开发环境的配置命令(设置了development的环境变量,使用dev.js,这里还使用了devServer,后面说)

注意这里是Unix系统配置环境变量的写法,在windows下,记得改成 SET NODE_ENV=development&& webpack-dev-server.......(&&前不要空格)

 

然后就可以在common.js配置文件中获取环境变量

// 是否生产环境
    isProduction = process.env.NODE_ENV === 'production',

然后可以在plugins中定义一个变量提供个编译中的模块文件使用

// 插件配置
    plugins: [
        // 定义变量,此处定义NODE_ENV环境变量,提供给生成的模块内部使用
        new webpack.DefinePlugin({
            'process.env': {
                NODE_ENV: JSON.stringify(process.env.NODE_ENV)
            }
        }),

这样一来,我们可以在home.js中判断是否为开发环境来引入一些文件

// 开发环境时,引入页面文件,方便改变页面文件后及时模块热更新
if (process.env.NODE_ENV === 'development') {
    require('../../../../views/home/home.html');
}

 

然后我们使用webpack-merge工具来合并公共配置文件和开发|生产配置文件

npm i webpack-merge --save-dev


merge = require('webpack-merge')

commonConfig = require('./webpack.config.common.js')


/**
 * 生产环境Webpack打包配置,整合公共部分
 * @type {[type]}
 */
module.exports = merge(commonConfig, {
    // 生产环境不开启sourceMap
    devtool: false,

    // 文件输出配置
    output: {
        // 设置文件引用主路径
        publicPath: '/public/static/dist/js/'
    },

    // 模块的处理配置

 

4. 设置公共模块

公共模块其实可以分为JS和CSS两部分(如果有提取CSS文件的话)

在公共文件的plugin中加入

// 提取公共模块文件
        new webpack.optimize.CommonsChunkPlugin({
            chunks: ['home', 'detail'],
            // 开发环境下需要使用热更新替换,而此时common用chunkhash会出错,可以直接不用hash
            filename: '[name].js' + (isProduction ? '?[chunkhash:8]' : ''),
            name: 'common'
        }),

设置公共文件的提取源模块chunks,以及最终的公共文件模块名

公共模块的文件的提取规则是chunks中的模块公共部分,如果没有公共的就不会提取,所以最好是在entry中就指定common模块初始包含的第三方模块,如jquery,react等

 // 文件入口配置
    entry: {
        home: './src/js/home',
        detail: './src/js/detail',
        // 提取jquery入公共文件
        common: ['jquery']
    },

5. 编译ES6成ES5

要讲ES6转换为ES5,当然首用babel了,先安装loader及相关的包

npm i babel-core babel-loader babel-preset-env babel-polyfill babel-plugin-transform-runtime --save-dev

-env包主要用来配置语法支持度

-polyfill用来支持一些ES6拓展的但babel转换不了的方法(Array.from Generator等)

-runtime用来防止重复的ES6编译文件所需生成(可以减小文件大小)

然后在/public根目录下新建 .babelrc文件,写入配置

{
    "presets": [
        "env"
    ],
    "plugins": ["transform-runtime"]
}

然后在common.js的配置文件中新增一条loader配置就行了,注意使用exclude排除掉不需要转换的目录,否则可能会出错哦

{
            test: /\.jsx?$/,
            // 编译js或jsx文件,使用babel-loader转换es6为es5
            exclude: /node_modules/,
            use: [{
                loader: 'babel-loader',
                options: {

                }
            }]
        }

 

 6. 编译Sass成CSS,嵌入到页面<style>标签中,或将其提取出(多个)CSS文件来用<link>引入

sass的编译node-sass需要python2.7的环境,先确定已经安装并设置了环境变量

npm i sass-loader node-sass style-loader css-loader --save-dev

类似的,设置一下loader规则

不过这里要设置成使用提取CSS文件的插件设置了,因为它的disable属性可以快速切换是否提取CSS(这里设置成生产环境才提取)

好好看这个栗子,其实分三步:设置(new)两个实例,loader匹配css和sass两种文件规则,在插件中引入这两个实例

提取多个CSS文件其实是比较麻烦的,但也不是不可以,方法就是设置多个实例和对应的几个loader规则

这里把引入的sass当做是自己写的文件,提取成一个文件[name].css,把引入的css当做是第三方的文件,提取成一个[name]_vendor.css,既做到了合并,也做到了拆分,目前还没想到更好的方案

上面提到过,output的path设置成了/public/static/dist/js ,所以这里的filename 生成是基于上面的路径,可以用../来更换生成的css目录

[contenthash]是css文件内容的hash,在引用它的地方有体现

fallback表示不可提取时的代替方案,即上述所说的使用style-loader嵌入到<style>标签

npm i extract-text-webpack-plugin --save-dev


ExtractTextWebpackPlugin = require('extract-text-webpack-plugin')

/ 对import 引入css(如第三方css)的提取
    cssExtractor = new ExtractTextWebpackPlugin({
        // 开发环境下不需要提取,禁用
        disable: !isProduction,
        filename: '../css/[name]_vendor.css?[contenthash:8]',
        allChunks: true
    })

    // 对import 引入sass(如自己写的sass)的提取
    sassExtractor = new ExtractTextWebpackPlugin({
        // 开发环境下不需要提取,禁用
        disable: !isProduction,
        filename: '../css/[name].css?[contenthash:8]',
        allChunks: true
    });




// 插件配置
    plugins: [
        // 从模块中提取CSS文件的配置
        cssExtractor,
        sassExtractor
    ]


    



module: {
        rules: [{
            test: /\.css$/,
            // 提取CSS文件
            use: cssExtractor.extract({
                // 如果配置成不提取,则此类文件使用style-loader插入到<head>标签中
                fallback: 'style-loader',
                use: [{
                        loader: 'css-loader',
                        options: {
                            // url: false,
                            minimize: true
                        }
                    },
                    // 'postcss-loader'
                ]
            })
        }, {
            test: /\.scss$/,
            // 编译Sass文件 提取CSS文件
            use: sassExtractor.extract({
                // 如果配置成不提取,则此类文件使用style-loader插入到<head>标签中
                fallback: 'style-loader',
                use: [
                    'css-loader',
                    // 'postcss-loader',
                    {
                        loader: 'sass-loader',
                        options: {
                            sourceMap: true,
                            outputStyle: 'compressed'
                        }
                    }
                ]
            })
        }

这样一来,如果在不同文件中引入不同的文件,生成的css可能长这样

// ./home.js
import '../../libs/bootstrap-datepicker/datepicker3.css';

import '../../libs/chosen/chosen.1.0.0.css';

import '../../libs/layer/skin/layer.css';

import '../../libs/font-awesome/css/font-awesome.min.css';


import '../scss/detail.scss';




// ./detail.js
import '../../libs/bootstrap-datepicker/datepicker3.css';

import '../../libs/chosen/chosen.1.0.0.css';

import '../../libs/layer/skin/layer.css';

import '../scss/detail.scss';

// ./home.html
<link href="/public/static/dist/js/../css/common_vendor.css?66cb1f48" rel="stylesheet">
<link href="/public/static/dist/js/../css/common.css?618d2a04" rel="stylesheet">
<link href="/public/static/dist/js/../css/home_vendor.css?12a314c8" rel="stylesheet">
<link href="/public/static/dist/js/../css/home.css?c196fc33" rel="stylesheet">




// ./detail.html
<link href="/public/static/dist/js/../css/common_vendor.css?66cb1f48" rel="stylesheet">
<link href="/public/static/dist/js/../css/common.css?618d2a04" rel="stylesheet">

可以看到,公共文件也被提取出来了,利用HtmlWebpackPlugin就能将其置入了

另外,可以看到这里的绝对路径,其实就是因为在output中设置了publicPath为/public/static/dist/js/

 

当然了,也不是说一定得在js中引入这些css资源文件,你可以直接在页面中手动<link>引入第三方CSS

我这里主要是基于模块化文件依赖,以及多CSS文件的合并压缩的考虑才用这种引入方式的

 

7. jQuery插件的引入方式 

 目前来说,jQuery及其插件在项目中还是很常用到的,那么就要考虑如何在Webpack中使用它

第一种方法,就是直接页面中<script>标签引入了,但这种方式不受模块化的管理,好像有些不妥

第二种方法,就是直接在模块中引入所需要的jQuery插件,而jQuery本身由Webpack插件提供,通过ProvidePlugin提供模块可使用的变量$|jQuery|window.jQuery

不过这种方法好像也有不妥,把所有第三方JS都引入了,可能会降低编译效率,生成的文件也可能比较臃肿

npm i jquery --save



// 
plugins: [
    new webpack.ProvidePlugin({
            $: 'jquery',
            jQuery: 'jquery',
            'window.jQuery': 'jquery'
        }),

]



// ./home.js


import '../../libs/bootstrap-datepicker/bootstrap-datepicker.js';
console.log('.header__img length', jQuery('.header__img').length);

第三种办法,可以在模块内部直接引入jQuery插件,也可以直接在页面通过<script>标签引入jQuery插件,而jQuery本身由Webpack的loader导出为全局可用

上述ProvidePlugin定义的变量只能在模块内部使用,我们可以使用expose-loader将jQuery设置为全局可见

npm i expose-loader --save



// 添加一条规则
{
            test: require.resolve('jquery'),
            // 将jQuery插件变量导出至全局,提供外部引用jQuery插件使用
            use: [{
                loader: 'expose-loader',
                options: '$'
            }, {
                loader: 'expose-loader',
                options: 'jQuery'
            }]
        }

要注意在Webpack3中不能使用webpack.NamedModulesPlugin()来获取模块名字,它会导致expose 出错失效(bug)

 

不过现在问题又来了,这个应该是属于HtmlWebpackPlugin的不够机智的问题,先说说它怎么用吧

 

8. HtmlWebpackPlugin将页面模板编译成最终的页面文件,包含JS及CSS资源的引用

第一个重要的功能就是生成对资源的引入了,第二个就是帮助我们填入资源的chunkhash值防止浏览器缓存

这个在生产环境使用就行了,开发环境是不需要的

npm i html-webpack-plugin --save-dev


HtmlWebpackPlugin = require('html-webpack-plugin')


plugins: [

 // 设置编译文件页面文件资源模块的引入
        new HtmlWebpackPlugin({
            // 模版源文件
            template: '../../views/home/home_tpl.html',
            // 编译后的目标文件
            filename: '../../../../views/home/home.html',
            // 要处理的模块文件
            chunks: ['common', 'home'],
            // 插入到<body>标签底部
            inject: true
        }),
        new HtmlWebpackPlugin({
            template: '../../views/detail/detail_tpl.html',
            filename: '../../../../views/detail/detail.html',
            chunks: ['common', 'detail'],
            inject: true
        }),



]

使用方式是配置成插件的形式,想对多少个模板进行操作就设置多少个实例

注意template是基于context配置中的上下文的,filename是基于output中的path路径的

// ./home_tpl.html

    <script src="/public/static/libs/magicsearch/jquery.magicsearch2.js"></script>
</body>



// ./home.html

<script src=/public/static/libs/magicsearch/jquery.magicsearch2.js></script>
<script type="text/javascript" src="/public/static/dist/js/common.js?cc867232"></script>
<script type="text/javascript" src="/public/static/dist/js/home.js?5d4a7836"></script>
</body>

它会编译成这样,然而,然而,要注意到这里是有问题的

这里有个jQuery插件,而Webpack使用expose是将jQuery导出到了全局中,我们通过entry设置把jQuery提取到了公共文件common中

所以正确的做法是common.js文件先于jQuery插件加载

而这个插件只能做到在<head> 或<body>标签尾部插入,我们只好手动挪动一下<script>的位置

 

不过,我们还可以基于这个插件,再写一个插件来实现自动提升公共文件 <script>标签到最开始

HtmlWebpackPlugin运行时有一些事件

    html-webpack-plugin-before-html-generation
    html-webpack-plugin-before-html-processing
    html-webpack-plugin-alter-asset-tags
    html-webpack-plugin-after-html-processing
    html-webpack-plugin-after-emit
    html-webpack-plugin-alter-chunks

在编译完成时,正则匹配到<script>标签,找到所设置的公共模块(可能设置了多个公共模块),按实际顺序提升这些公共模块即可

完整代码如下:

 1 // ./webpack.myPlugin.js
 2 
 3 
 4 let extend = require('util')._extend;
 5 
 6 
 7 // HtmlWebpackPlugin 运行后调整公共script文件在html中的位置,主要用于jQuery插件的引入
 8 function HtmlOrderCommonScriptPlugin(options) {
 9     this.options = extend({
10         commonName: 'common'
11     }, options);
12 }
13 
14 HtmlOrderCommonScriptPlugin.prototype.apply = function(compiler) {
15     compiler.plugin('compilation', compilation => {
16         compilation.plugin('html-webpack-plugin-after-html-processing', (htmlPluginData, callback) => {
17             // console.log(htmlPluginData.assets);
18 
19             // 组装数组,反转保证顺序
20             this.options.commonName = [].concat(this.options.commonName).reverse();
21 
22             let str = htmlPluginData.html,
23                 scripts = [],
24                 commonScript,
25                 commonIndex,
26                 commonJS;
27 
28             //获取编译后html的脚本标签,同时在原html中清除
29             str = str.replace(/(<script[^>]*>(\s|\S)*?<\/script>)/gi, ($, $1) => {
30                 scripts.push($1);
31                 return '';
32             });
33 
34             this.options.commonName.forEach(common => {
35                 if (htmlPluginData.assets.chunks[common]) {
36                     // 找到公共JS标签位置
37                     commonIndex = scripts.findIndex(item => {
38                         return item.includes(htmlPluginData.assets.chunks[common].entry);
39                     });
40 
41                     // 提升该公共JS标签至顶部
42                     if (commonIndex !== -1) {
43                         commonScript = scripts[commonIndex];
44                         scripts.splice(commonIndex, 1);
45                         scripts.unshift(commonScript);
46                     }
47                 }
48             });
49 
50             // 重新插入html中
51             htmlPluginData.html = str.replace('</body>', scripts.join('\r\n') + '\r\n</body>');
52 
53             callback(null, htmlPluginData);
54         });
55     });
56 };
57 
58 
59 module.exports = {
60     HtmlOrderCommonScriptPlugin,
61 };

 

然后,就可以在配置中通过插件引入了

{HtmlOrderCommonScriptPlugin} = require('./webpack.myPlugin.js');


// HtmlWebpackPlugin 运行后调整公共script文件在html中的位置,主要用于jQuery插件的引入
        new HtmlOrderCommonScriptPlugin({
            // commonName: 'vendor'
        })

亲测还是蛮好用的,可以应对简单的需求了

 

9. 使用url-loader和file-loader和html-loader来处理图片、字体等文件的资源引入路径问题

这个配置开发环境和生产环境是不同的,先看看生产环境的,主要的特点是有目录结构的设置,设置了一些生成的路径以及名字信息

开发环境因为是使用了devServer,不需要控制目录结构

npm i url-loader file-loader@0.10.0 html-loader --save-dev

这里要注意的是file-loader就不要用0.10版本以上的了,会出现奇怪的bug,主要是下面设置的outputPath和publicPath和[path]会不按套路出牌

导致生成的页面引用资源变成奇怪的相对路径

rules: [{
            test: /\.(png|gif|jpg)$/,
            use: {
                loader: 'url-loader',
                // 处理图片,当大小在范围之内时,图片转换成Base64编码,否则将使用file-loader引入
                options: {
                    limit: 8192,
                    // 设置生成图片的路径名字信息 [path]相对context,outputPath输出的路径,publicPath相应引用的路径
                    name: '[path][name].[ext]?[hash:8]',
                    outputPath: '../',
                    publicPath: '/public/static/dist/',
                }
            }
        }, {
            test: /\.(eot|svg|ttf|otf|woff|woff2)\w*/,
            use: [{
                loader: 'file-loader',
                options: {
                    // 设置生成字体文件的路径名字信息 [path]相对context,outputPath输出的路径,publicPath相应引用的主路径
                    name: '[path][name].[ext]?[hash:8]',
                    outputPath: '../',
                    publicPath: '/public/static/dist/',
                    // 使用文件的相对路径,这里先不用这种方式
                    // useRelativePath: isProduction
                }
            }],
        }, {
            test: /\.html$/,
            // 处理html源文件,包括html中图片路径加载、监听html文件改变重新编译等
            use: [{
                loader: 'html-loader',
                options: {
                    minimize: true,
                    removeComments: false,
                    collapseWhitespace: false
                }
            }]
        }]
    

比较生涩难懂,看个栗子吧

scrat.png是大于8192的,最终页面引入会被替换成绝对路径,并且带有hash防止缓存,而输出的图片所在位置也是用着相应的目录,便于管理

// ./home_tpl.html

        <img class="header__img" src="../../public/static/imgs/kl/scrat.png" width="200" height="200">


// ./home.html

        <img class=header__img src=/public/static/dist/imgs/kl/scrat.png?8ad54ef5 width=200 height=200>

如果换个小图,就会替换成base64编码了,在css中的引入也一样

<img class=header__img src=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARgAAAA6CAYAAABrnUYFAAAaVElEQVR4Xu1df3wdVZX/npnkNUlbWn6sFhAELSgibSYtCIhagXXB0vdSsOwu4CKiXbAglk8zAVwloELzwoqIUsFFRXQVaiEvRcAfLD9EfqxtXtqKS239AQoFRUlb+tK+5N2znzNvJp1M34+ZeZOElLn/QPPuPffcc2e+c+75dQlxiyUQSyCWwChJgEaJbkw2lkAsgVg

 

再来看看开发环境的 

rules: [{
            test: /\.(png|gif|jpg)$/,
            // 处理图片,当大小在范围之内时,图片转换成Base64编码,否则将使用file-loader引入
            use: [{
                loader: 'url-loader',
                options: {
                    limit: 8192
                }
            }]
        }, {
            test: /\.(eot|svg|ttf|otf|woff|woff2)\w*/,
            // 引入文件
            use: 'file-loader'
        }]

 

 10. 模块热更新替换的正确姿势

在开发环境下,如果做到模块的热更新替换,效果肯定是棒棒的。生成环境就先不用了

在最初的时候,只是做到了热更新,并没有做到热替换,其实都是坑在作祟

热更新,需要一个配置服务器,Webpack集成了devServer的nodejs服务器,配置一下它

// 开发环境设置本地服务器,实现热更新
    devServer: {
        contentBase: path.resolve(__dirname, 'static'),
        // 提供给外部访问
        host: '0.0.0.0',
        port: 8188,
        // 设置页面引入
        inline: true
    },

正常的话,启动服务应该就可以了吧

webpack-dev-server --config webpack.config.dev.js

要记住,devServer编译的模块是输出在服务器上的(默认根目录),不会影响到本地文件,所以要在页面上手动设置一下引用的资源

<script src="http://localhost:8188/common.js"></script>
<script src="http://localhost:8188/home.js"></script>

浏览器访问,改动一下home.js文件,这时应该可以看到页面自动刷新,这就是热更新了😁

当然了,热更新还不够,得做到热替换,即页面不刷新替换模块

可以呀,多配置一下

// 开发环境设置本地服务器,实现热更新
    devServer: {
        ...
        // 设置热替换
        hot: true,
        ...
    },



 // 插件配置
    plugins: [
        // 热更新替换
        new webpack.HotModuleReplacementPlugin(),
]

再去浏览器试试,改个文件,正常的话应该也能看到

但就是一直停留在App hot update...不动了,惊不惊喜,意不意外

原因是还没在当前项目中安装webpack-dev-server,HMR的消息接收不到,命令没报错只是因为在全局安装了webpack有那命令

npm i webpack-dev-server --save-dev

再试试,然而你发现,才刚开始编译,就不停地重复编译了

你得设置一下publicPath 比如

output: {
        publicPath: '/dist/js/',
    },

再试试,更改模块,你又会发现页面还是重新刷新了

要善于用Preserve log来看看刷新之前发生了什么

已经有进展了,这时HMR在获取JSON文件时404了,而且访问的域名端口是localhost:8088是我们自己node服务器的端口

devServer的端口是8188的,看起来这JSON文件时devServer生成的,可能是路径被识别成相对路径了

那就设置成绝对路径吧

output: {
        // 设置路径,防止访问本地服务器相关资源时,被开发服务器认为时相对其的路径
        publicPath: 'http://localhost:8188/dist/js/',
    },

再来,恭喜 又错了,跨域访问

那就在devServer再配置一下header让8088可以访问,可以暴力一点设置*

devServer: {
       ...
        // 允许开发服务器访问本地服务器的包JSON文件,防止跨域
        headers: {
            'Access-Control-Allow-Origin': '*'
        },
        ...
    },

再来,额😆呵呵,又重新刷新了

指明了模块没有被设置成accepted,那它就不知道要热替换哪个模块了,只好整个刷新。

需要在模块中设置一下,机智是冒泡型的,所以在主入口设置就行了,比如这里的模块入口home.js

// 设置允许模块热替换
if (module.hot) {
    module.hot.accept();
}

这就成功了,这里建议的NamedModulesPlugin是用不了了,因为和espose-loader冲突了

 

是不是很啰嗦呢,总结一下

1. 在本项目总安装webpack-dev-server

2. devServer配置中设置hot: true

3. plugins配置中设置new webpack.HotModuleReplacementPlugin() 

4. output配置中设置publicPath: 'http://localhost:8188/dist/js/'

5. devServer配置中设置header允许跨域访问

6. 模块中设置接受热替换module.hot.accept()

7. 不要在命令行加参数 --hot 和 new webpack.HotModuleReplacementPlugin() 同时使用,会栈溢出错误,只用配置文件的就行了

 

另外,默认是只能模块热替换,如果也想监听页面文件改变来实现HTML页面的热替换,该怎么做呢

把HTML也当做模块引入就行了(开发环境下),在之前已经使用了html-loader能处理html后缀资源的情况下

// ./home.js

// 开发环境时,引入页面文件,方便改变页面文件后及时模块热更新
if (process.env.NODE_ENV === 'development') {
    require('../../../../views/home/home_tpl.html');
}

记得import不能放在if语句块里面,所以这里用require来代替

有点奇怪,在最开始的时候,这样是能实现热替换的,但这段时间却一直不行了,显示已更新,但内容却没更新

只好暂时用第二步热更新来替换,接收到改变时页面自动刷新

//  ./home.js

// 开发环境时,引入页面文件,方便改变页面文件后及时模块热更新
if (process.env.NODE_ENV === 'development') {
    require('../../../../views/home/home_tpl.html');
}

// 设置允许模块热替换
if (module.hot) {
    module.hot.accept();

    // 页面文件更新 自动刷新页面
    module.hot.accept('../../../../views/home/home_tpl.html', () => {
        location.reload();
    });
}

 

11. 压缩模块代码

压缩JS代码就用自带的插件就行了

压缩CSS代码用相应的loader options

// 压缩代码
        new webpack.optimize.UglifyJsPlugin({
            sourceMap: true,
            compress: {
                warnings: false
            }
        }),

 

12. 异步加载模块 require.ensure

异步加载模块,在很多时候是需要的。比如在首页的时候,不应该要求用户就下载了其他不需要的资源。

而webpack中异步加载模块是比较方便的,主要是require.ensure这个方法

require.ensure(dependencies: String[], callback: function(require), chunkName: String)

比如,在home.html页面中,我想点击某个元素之后,再异步加载某个模块来执行

// 添加一个模块  ./async.js

// 这个模块用于检测异步加载
function log() {
    console.log('log from async.js');
}

export {
    log
};




// 在 ./home.js模块中设置点击之后异步引入

$('.bg-input').click(() => {
    console.log('clicked, loading async.js');

    require.ensure([], require => {
        require('./components/async').log();
        console.log('loading async.js done');
    });
});

可以看到,点击之后,异步请求了这个模块

webpack 在编译的时候分析在require.ensure中定义的依赖模块,将其生成到一个新的chunk中(不在home.js里),之后按需拉取下来

另外,要注意的是,如果模块已经被引入了,那它是不会单独被打包出去的

// require('./components/async2').log();

$('.bg-input').click(() => {
    console.log('clicked, loading async.js')

    require.ensure([], require => {

        require('./components/async2').log();
        require('./components/async1').log();
        console.log('loading async.js done');
    });
});

两个依赖都会放到一起,如果把注释去掉的话,那异步的模块就只有async-1.js了

require.ensure的第一个参数是依赖,这里的依赖加载完成后,才会执行回调函数(在里头我们可以再次设置依赖)

所以,如果只是想加载一个模块,我们可以直接这么写。但是,这只是下载了,它是执行不了的

$('.bg-input').click(() => {
    console.log('clicked, loading async.js')

    require(['./components/async1']);
});

所以一般来说,第一个参数更多是用做回调里模块的依赖,一般执行的操作都是放到回调里

第三个参数是定义这个chunk的名字,要同时在output中设置chunkFilename

// 文件输出配置
    output: {
        // 异步加载模块名
        chunkFilename: '[name].js'
    },



  require.ensure([], require => {
          ...

    }, 'async_chunk');        

 

13. 其他配置

再来稍微配一下react的环境

npm i react react-dom babel-preset-react --save-dev

在home.js文件中加入

let React = require('react');
let ReactDOM = require('react-dom')

class Info extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            name: this.props.name || 'myName'
        };
    }

    showYear(e) {
        console.log(this);

        let elem = ReactDOM.findDOMNode(e.target);
        console.log('year ' + elem.getAttribute('data-year'));
    }

    render() {
        return <p onClick={this.showYear} data-year={this.props.year}>{this.state.name}</p>
    }
}

Info.defaultProps = {
    year: new Date().getFullYear()
};

ReactDOM.render(<Info />, document.querySelector('#box'));

修改.bablerc文件

{
    "presets": [
        "env",
        "react"
    ],
    "plugins": ["transform-runtime"]
}

 

 

其他配置,比如eslint代码检查、postcss支持等就不在这说了,用到了就用类似的方式添加进去吧 

 

14. 自定义HtmlWebpackPlugin插件编译模版文件生成的JS/CSS插入位置

HtmlWebpackPlugin主要用来编译模版文件,生成新的页面文件

new HtmlWebpackPlugin({
            template: '../../parent/parent_index_src.html',
            filename: '../../../../parent/parent_index.tpl',
            chunks: ['common', 'parent'],
            inject: true
        }),

一般来说会这样用,可以同时将JS资源与CSS资源插入到页面中(可自动配hash值),非常方便

但是修改inject属性只会不插入或插入到</head>或</body>标签之前,自定义不了插入位置

上述第八点提到了利用插件来调整生成<script>标签,其实还有更便捷的方法可以实现:使用其支持的模版引擎

假设现在是smarty页面,有个公共父模版文件,很多子页面套用这个文件,那么它可以长成这个样子

<!-- 父页面  -->
<!DOCTYPE html>
<html>
    <head>
        <title>某个系统</title>
        <meta charset="utf-8">
        <meta lang="zh-CN">

        <% for(var key in htmlWebpackPlugin.files.css) { %>
        <link rel="stylesheet" href="<%= htmlWebpackPlugin.files.css[key] %>">
        <% } %>

        <{block name="page_css"}><{/block}>
    </head>
    <body>
        <section class="container">
            父页面
        </section>

        <{block name="page_content"}><{/block}>

        <script src="/public/static/js/jquery.min.js"></script>

        <% for(var key in htmlWebpackPlugin.files.js) { %>
        <script src="<%= htmlWebpackPlugin.files.js[key] %>"></script>
        <% } %>

        <{block name="page_js"}><{/block}>
        <script src="http://localhost:8188/dist/js/common.js"></script>
        <script src="http://localhost:8188/dist/js/parent.js"></script>
    </body>
</html>
<!-- 子页面 -->

<{extends file="../parent/parent_index.tpl"}>


<{block name="page_css"}>
<% for(var key in htmlWebpackPlugin.files.css) { %>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.files.css[key] %>">
<% } %>
<{/block}>


<{block name="page_content"}>
<h1>子页面</h1>
<div>

</div>
<{/block}>



<{block name="page_js"}>
<% for(var key in htmlWebpackPlugin.files.js) { %>
<script src="<%= htmlWebpackPlugin.files.js[key] %>"></script>
<% } %>
<{/block}>

这里,为了实现子页面插入到父页面之后,还能保持CSS与JS资源放在正确的位置,需要指定一个编译后的生成位置

使用到了Webpack内置支持的ejs模版,并使用到了其htmlWebpackPlugin变量,里面携带了本次编译的一些信息,我们可以直接输出来插入资源,然后再设置 inject: false就行了

下面是一个例子的输出,更多的就去看文档吧

"htmlWebpackPlugin": {
  "files": {
    publicPath : "", 
    "css": [],
    "js": [ "js/main.ae8647e767cd76e54693.bundle.js"],
    "chunks": {
      "main": {
        "size":23,
        "entry": "js/main.ae8647e767cd76e54693.bundle.js", 
        "css": [],
        "hash":"ae8647e767cd76e54693",
      }
    },
    manifest : ""
  },
  "options":{
        template : "C:\\dev\\webpack-demo\\node_modules\\.2.28.0@html-webpack-plugin\\lib\\loader.js!c:\\dev\\webpack-demo\\index.html",    
    filename : "index.html",    
    hash : false,    
    inject : false,    
    compile : true,    
    favicon : false,    
    minify : false,        
    cache : true,    
    showErrors : true,    
    chunks : ["main"],    
    excludeChunks : [],    
    title : "I am title",    
    xhtml : false    
    }
}

 

 15. 热更新编译模版文件自动生成webpack服务器中的资源路径

热更新时,webpack的devServer默认只会将模块编译到内存中,编译到我们设置的服务器里,不会编译生成到本地开发目录中

这并不算什么问题,问题是我们需要在页面中手动引入服务器的模块,比如

<script src="http://localhost:8188/dist/js/common.js"></script>
        
        <script src="http://localhost:8188/dist/js/parent.js"></script>

使用热更新时手动添加,不使用时手动删掉才上传代码,这还好,但是,我们有模版文件

假设模版文件为a_src.html ,需要编译成a.html,我们实际项目中要访问的文件是编译后的a.html文件,而我们只能在源文件a_src.html中做改动

使用热更新的时候,并不能将源文件编译写到新文件上,我们只能换着法子访问源文件或者直接改动新文件并复制一份到源文件中,而且还得手动添加热更新的服务器模块路径

太麻烦了,那就在热更新的时候也编译模版文件吧,使用HtmlWebpackHarddiskPlugin 插件自动生成资源引用路径,同时在源文件的更改可以自动编译写到新文件中

// 安装
 npm install --save-dev html-webpack-harddisk-plugin



var HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin');


// 配合htmlWebpackPlugin使用,加上参数alwaysWriteToDisk
new HtmlWebpackPlugin({
            template: '../../main/protected/views/flow/a_src.html',
            filename: '../../../../main/protected/views/flow/a.htmll',
            chunks: ['a'],
            inject: false,
            alwaysWriteToDisk: true
        }),
        new HtmlWebpackPlugin({
            template: '../../main/protected/views/parent/parent_src.html',
            filename: '../../../../main/protected/views/parent/parent.html',
            chunks: ['common', 'parent'],
            inject: false,
            alwaysWriteToDisk: true
        }),
    // 调用
        new HtmlWebpackHarddiskPlugin()

然后在源模版文件里,配合上一点的ejs模版生成出来就行了,可以自动检测是生成环境的路径还是开发环境的热更新路径 解放了劳动力

源模版文件:

<!-- 编译后脚本 -->
        <% for(var key in htmlWebpackPlugin.files.js) { %>
        <script src="<%= htmlWebpackPlugin.files.js[key] %>"></script>
        <% } %>

development:

// 文件输出配置
    output: {
        publicPath: 'http://localhost:8188/dist/js/',
    },
 <!-- 编译后脚本 -->
        
        <script src="http://localhost:8188/dist/js/common.js"></script>
        
        <script src="http://localhost:8188/dist/js/parent.js"></script>

production:

// 文件输出配置
    output: {
        publicPath: '/public/assets/dist/js/'
    },
 <!-- 编译后脚本 -->
        
        <script src="/public/assets/dist/js/common.js?784109bb"></script>
        
        <script src="/public/assets/dist/js/parent.js?997487cf"></script>

 

 

16. 其他常见问题整理

一个项目有多个webpack冲突的解决

如果一个项目中用多个webpack来编译,并引入了多个文件,就会产生冲突了,这会导致webpack只会识别第一个引入的变量

这时候,需要配置output的jsonpFunction参数

// 文件输出配置
    output: {
        // 输出所在目录
        path: path.resolve(__dirname, 'assets/dist/js'),
        // 开发环境使用热更新,方便编译,可以直接不用hash
        filename: '[name].js',
        jsonpFunction: 'abcJSONP'
    },

 

Only one instance of babel-polyfill is allowed

引入多个polyfill导致冲突,不能重复引入

import 'babel-polyfill';

解决办法是:引入的时候判断一下(没办法,它自己没判断)

if (!global._babelPolyfill) {
   require('babel-polyfill');
}

 

 转载请注明

posted @ 2017-11-05 03:36 -渔人码头- 阅读(...) 评论(...) 编辑 收藏