在找一份相对完整的Webpack项目配置指南么?这里有_.NET_编程开发_程序员俱乐部

中国优秀的程序员网站程序员频道CXYCLUB技术地图
热搜:
更多>>
 
您所在的位置: 程序员俱乐部 > 编程开发 > .NET > 在找一份相对完整的Webpack项目配置指南么?这里有

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

 2017/11/5 16:21:42  -渔人码头-  程序员俱乐部  我要评论(0)
  • 摘要:Webpack已经出来很久了,相关的文章也有很多,然而比较完整的例子却不是很多,让很多新手不知如何下脚,下脚了又遍地坑说实话,官方文档是蛮乱的,而且有些还是错的错的。。很多配置问题只有爬过坑才知道本文首先介绍Webpack的一些基础知识,然后以一个已经完成的小Demo,逐一介绍如何在项目中进行配置该Demo主要包含编译Sass/ES6,提取(多个)CSS文件,提取公共文件,模块热更新替换,开发与线上环境区分,使用jQuery插件的方式、页面资源引入路径自动生成
  • 标签:Web 配置 项目

 

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

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

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

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

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

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

 

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

 

  • 1. 为什么要用Webpack
  • 2. 什么是Webpack
  • 3. Webpack的基础配置
    • 1. webpack的配置方式主要有三种
      • 1. 通过cli命令行传入参数
      • 2. 通过在一个配置文件设置相应配置,导出使用
      • 3. 通过使用NodeJS的API配置
    • 2. 常见的几个配置属性
      • 1. context  绝对路径
      • 2. entry  模块入口文件设置
      • 3. resolve 处理资源的查找引用方式
      • 4. output 设置文件的输出
      • 5. devtool指定sourceMap的配置
      • 6. module指定模块如何被加载
      • 7.  plugins设置webpack配置过程中所用到的插件
  • 4. Webpack3配置在Demo中的应用
    • 1. 搭建个服务器
    • 2. 设置基础项目目录
    • 3. 开发和生产环境的Webpack配置文件区分
    • 4. 设置公共模块
    • 5. 编译ES6成ES5
    • 6. 编译Sass成CSS,嵌入到页面<style>标签中,或将其提取出(多个)CSS文件来用<link>引入
    • 7. jQuery插件的引入方式 
    • 8. HtmlWebpackPlugin将页面模板编译成最终的页面文件,包含JS及CSS资源的引用
    • 9. 使用url-loader和file-loader和html-loader来处理图片、字体等文件的资源引入路径问题
    • 10. 模块热更新替换的正确姿势
    • 11. 压缩模块代码
    • 12. 其他配置

 

一 、为什么要用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. 其他配置

再来稍微配一下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支持等就不在这说了,用到了就用类似的方式添加进去吧 

 

 

 

 转载请注明

发表评论
用户名: 匿名