自动化构建

什么是项目构建

  • 编译项目中的js, sass, less

  • 合并js/css等资源文件

  • 压缩js/css/html等资源文件

  • JS语法的检查

  • ……

构建工具

  • Grunt

  • Gulp

    • http://www.gulpjs.com.cn/

    • http://www.jianshu.com/p/cc1cb9a5650c(教程)

    • http://www.ydcss.com/archives/category/%E6%9E%84%E5%BB%BA%E5%B7%A5%E5%85%B7 (详细教程)

  • Webpack

    • http://webpack.github.io/ (英文官网)

    • http://guowenfh.github.io/2016/03/24/vue-webpack-01-base/(系列教程)

    • https://github.com/nimojs/webpack-book(webpack入门指南)

    • http://www.jianshu.com/p/bb48898eded5(教程)

2. Grunt

Grunt介绍

  • 中文主页 : http://www.gruntjs.net/

  • 是一套前端自动化构建工具,一个基于nodeJs的命令行工具

  • 它是一个任务运行器, 配合其丰富强大的插件

  • 常用功能:

    • 合并文件

    • 压缩文件

    • 语法检查

    • less/sass预编译处理等等

    • grunt本身不适配es6语法,需要先用Babel处理,不然会报错

Grunt常用插件

  • grunt官网的插件列表页面 http://www.gruntjs.net/plugins

  • 插件分类:

    • grunt团队贡献的插件 : 插件名大都以contrib-开头

    • 第三方提供的插件 : 大都不以contrib-开头

  • 常用的插件:

    • grunt-contrib-clean——清除文件(打包处理生成的)

    • grunt-contrib-concat——合并多个文件的代码到一个文件中

    • grunt-contrib-uglify——压缩js文件
    • grunt-contrib-jshint——javascript语法错误检查
    • grunt-contrib-cssmin——压缩合并css文件
    • grunt-contrib-htmlmin——压缩html文件
    • grunt-contrib-imagemin——压缩图片文件(无损)
    • grunt-contrib-copy——复制文件、文件夹
    • grunt-contrib-watch——实时监控文件变化、调用相应的任务重新执行

创建一个简单的grunt应用

  • 全局安装 grunt-cli
  • npm install -g grunt-cli
  • 安装grunt
    • npm install grunt –save-dev
  • 构建项目目录
  • 构建项目命令,把配置信息插入相应位置
  • 合并js:使用concat插件,
  • 压缩js: 使用uglify插件
    • 压缩针对合并的文件,所以files路径是合并后生成的built.js
    • 生成的文件添加.min.作为标示
    • 压缩操作:一行;形参简化;定义不调用的函数直接筛选;
  • js语法检查: 使用jshint插件
    • jshint插件有自己的配置文件.jshintrc 需要先配置好。类似的.babelrc,是一个JSON文件
    • predef:有些变量是不用自己定义,一旦引入了库就可以,所以需要指示一下
  • 合并压缩css文件,使用cssmin插件
  • 使用watch插件(真正实现自动化)
    • 监视原文件,一旦原文件发生了变化,自动执行task
    • 注册默认任务方便统一执行
  • grunt执行任务是同步的,按任务顺序执行,先压缩后合并会生成min。文件路径的问题,要先合并在压缩,因为压缩的路径指定的是合并后的文件

3. Gulp

Gulp介绍

  • 中文主页: http://www.gulpjs.com.cn/

  • gulp是与grunt功能类似的前端项目构建工具, 也是基于Nodejs的自动任务运行器

  • 能自动化地完成 javascript/coffee/sass/less/html/image/css 等文件的合并、压缩、检查、监听文件变化、浏览器自动刷新、测试等任务

  • gulp更高效(异步多任务), 更易于使用, 插件高质量

  • gulp的特点

    • 任务化
    • 基于流
    • 有自己的内存,在I/O流入流出,处理文件
    • 异步/同步任务

gulp.src(globs[, options])

  • 输出(Emits)符合所提供的匹配模式(glob)或者匹配模式的数组(array of globs)的文件。 将返回一个 Vinyl filesstream 它可以被 piped 到别的插件中。

    gulp.src('client/templates/*.jade')
     .pipe(jade())
     .pipe(minify()) 
     .pipe(gulp.dest('build/minified_templates'));
    

gulp.dest(path[, options])

  • 能被 pipe 进来,并且将会写文件。并且重新输出(emits)所有数据,因此你可以将它 pipe 到多个文件夹。如果某文件夹不存在,将会自动创建它。

    gulp.src('./client/templates/*.jade') 
    .pipe(jade()) 
    .pipe(gulp.dest('./build/templates')) 
    .pipe(minify()) .pipe(gulp.dest('./build/minified_templates'));
    

gulp.task(name[, deps], fn)

  • 定义一个使用 Orchestrator 实现的任务(task)

    gulp.task('somename', function() { 
    // 做一些事
     });
    

gulp.watch(glob [, opts],tasks) 或 gulp.watch(glob[, opts, cb])

  • 监视文件,并且可以在文件发生改动时候做一些事情。它总会返回一个 EventEmitter 来发射(emit) change 事件。

常用的Gulp插件

  • gulp-concat : 合并文件(js/css)

  • gulp-uglify : 压缩js文件

  • gulp-rename : 文件重命名(在名字中加min)

  • gulp-less : 编译less //转换为css代码

  • gulp-clean-css : 压缩css

  • gulp-htmlmin:压缩html

  • gulp-livereload : 实时自动编译刷新

  • gulp-connecti:热加载,实时加载(不用手动刷新浏览器)

  • gulp-load-plugins:下载打包插件

  • 下载插件:
    • npm install gulp-concat gulp-uglify gulp-rename –save-dev
    • 同时下载多个包,用空格隔开即可
  • 引入插件
    • var concat = require(‘gulp-concat’);
    • 引入的插件都是方法
    • concat(“文件名.js”)
  • 不支持es6语法
  • 定义未使用的变量,压缩都会筛选掉
  • glup的链式调用与jQuery不同,继续执行的是上一个返回的文件而不是最初调用的,压缩的是合并后的,而不是初始,就是管道咯

创建一个简单的Gulp应用

  • 文档结构

  • 安装gulp:

    • 全局安装gulp
    • npm install gulp -g
    • 局部安装gulp
    • npm install gulp –save-dev
  • 配置编码: gulpfile.js

    //引入gulp模块
    var gulp = require('gulp');
    //定义任务
    gulp.task('任务名', function() {
      // 将你的任务的任务代码放在这
    });
    //注册默认任务
    gulp.task('default', ['任务名'])//异步执行
    
  • 编写js文件

  • 配置文件:任务编码

    var concat = require('gulp-concat'); //引入插件
    var uglify = require('gulp-uglify');
    var rename = require('gulp-rename');
    gulp.task('minifyjs', function() {
        return gulp.src('src/js/*.js') //操作的源文件 **/*.js深度遍历
            .pipe(concat('built.js')) //合并到临时文件     
            .pipe(gulp.dest('dist/js')) //生成到目标文件夹
            .pipe(rename({suffix: '.min'})) //重命名  
            .pipe(uglify())    //压缩
            .pipe(gulp.dest('dist/js')); //把压缩后的文件
    });
      
    gulp.task('default', ['minifyjs']);
    
  • 页面引入js浏览测试 : index.html

  • 处理CSS

    var less = require('gulp-less');
    var cleanCSS = require('gulp-clean-css');
      
    //less处理任务
    gulp.task('lessTask', function () {
      return gulp.src('src/less/*.less')
          .pipe(less()) 
            
          .pipe(gulp.dest('src/css')); //输出到css文件,方便一起合并压缩
    })
    //css处理任务, 指定依赖的任务,依赖任务优先处理
    gulp.task('cssTask',['lessTask'], function () {
      
      return gulp.src('src/css/*.css')
          .pipe(concat('built.css'))
          .pipe(gulp.dest('dist/css'))
          .pipe(rename({suffix: '.min'}))
          .pipe(cleanCSS({compatibility: 'ie8'})) //兼容至ie8
          .pipe(gulp.dest('dist/css'));
    });
      
    gulp.task('default', ['minifyjs', 'cssTask']);
    
  • 处理html

    var htmlmin = require('gulp-htmlmin');
      //压缩html任务
      gulp.task('htmlMinify', function() {
          return gulp.src('index.html')
    //消除空格
              .pipe(htmlmin({collapseWhitespace: true}))
              .pipe(gulp.dest('dist'));//文件位置变化,引入代码变化
      });
      gulp.task('default', ['minifyjs', 'cssTask', 'htmlMinify']);
    

    修改页面引入

      <link rel="stylesheet" href="css/built.min.css">
      <script type="text/javascript" src="js/built.min.js"></script>
    

    打包测试: gulp

      
    
  • 自动编译(需要手动刷新浏览器)

    var livereload = require('gulp-livereload');
    //所有的pipe
    .pipe(livereload());
    //依赖任务default,先启动,用于监视
    gulp.task('watch', ['default'], function () {    
      //开启监视
      livereload.listen();
      //监视指定的文件, 并指定对应的处理任务
      gulp.watch('src/js/*.js', ['minifyjs'])
    //cssTask依赖于lessTask,所以一个就够
      gulp.watch(['src/css/*.css','src/less/*.less'], ['cssTask']);
    });
    
  • 热加载(自动刷新浏览器)

    1npm install gulp-connect --save-dev
    2、注册 热加载的任务 server,注意依赖build任务
    3、注册 热加载的任务
        //配置加载的选项,插件内置的微型服务器,自动读取gulpfile.js配置文件
        //把所有的操作都在自己的微型服务器执行,提供一个可访问的服务器地址
        connect.server({
              root : 'dist/',//提供服务的根路径
              livereload : true,//是否实时刷新
              port : 5000//开启端口号,localhost:5000用于访问
         });
         //每个任务下添加.pipe(connect.reload())
         // open插件可以自动开启链接
         open('http://localhost:5000');//npm install open --save-dev
         // 监视目标文件
        gulp.watch('src/js/*.js', ['js']);
        gulp.watch(['src/css/*.css', 'src/css/*.less'], ['cssMin', 'less']);
    
  • 扩展

  • 打包加载gulp插件

  • 前提:将插件下载好。

  • 载打包插件: gulp-load-plugins

    *引入: var $ = require('gulp-load-plugins')();
    //引入的插件是个方法,必须记住调用。
    //神来之笔:其他的插件不用再引入了
    使用方法:
    //所有的插件用 $ 引出,其他插件的方法名统一为插件的功能名字(即插件名字的最后一部分) 如:concat,connect,cssmin...
    //默认的方法名就是插件名:
    //gulp-htmlmin → $.htmlmin 不需要驼峰命名
    //gulp-clean-css → $.cleanCss
    gulp.task('lib', function() {
      gulp.src('bower_components/**/*.js')
        .pipe(gulp.dest(app.devPath + 'vendor'))
        .pipe(gulp.dest(app.prdPath + 'vendor'))
        .pipe($.connect.reload());
    });
    
  • gulp任务默认异步执行,如果在声明任务时不用return,则会变成同步执行。使用return异步执行的同时,还会在任务执行完毕之后将内存释放

webpack

  • https://github.com/gwuhaolin/dive-into-webpack/

基本概念

  • webpack 本质上是一个打包工具,它会根据代码的内容解析模块依赖,帮助我们把多个模块的代码打包

    而grunt和gulp仅仅是项目构建工具

  • webpack 会把我们项目中使用到的多个代码模块(可以是不同文件类型),打包构建成项目运行仅需要的几个静态文件

  • 在Webpack看来, 前端的所有资源文件(js/json/css/img/less/…)都会作为模块处理,除了html

  • 它将根据模块的依赖关系进行静态分析,生成对应的静态资源

  • 内置支持commonJS、ES6、AMD三种模块化规范

配置文件(默认)

  • webpack.config.js : 是一个node模块,返回一个 json 格式的配置信息对象

    类似于gruntfile.js和gulpfile.js

  • 入口 输出 loader plugin 等相关配置全都是在webpack.config.js内设置

入口

  • 入口可以使用 entry字段来进行配置,webpack 支持配置多个入口来进行构建

    module.exports = {
      entry: './src/index.js' 
    }
      
    // 上述配置等同于
    module.exports = {
      entry: {
        main: './src/index.js'
      }
    }
      
    // 或者配置多个入口
    module.exports = {
      entry: {
        foo: './src/page-foo.js',
        bar: './src/page-bar.js', 
        // ...
      }
    }
      
    // 使用数组来对多个文件进行打包
    module.exports = {
      entry: {
        main: [
          './src/foo.js',
          './src/bar.js'
        ]
      }
    }...
    

loader

  • Webpack 本身只能加载JS/JSON模块,如果要加载其他类型的文件(模块),就需要使用对应的loader 进行转换/加载

  • Loader 本身也是运行在 node.js 环境中的 JavaScript 模块

  • 它本身是一个函数,接受源文件作为参数,返回转换的结果

  • loader 一般以 xxx-loader 的方式命名,xxx 代表了这个 loader 要做的转换功能,比如 json-loader

  • 可以把 loader理解为是一个转换器,负责把某种文件格式的内容转换成 webpack 可以支持打包的模块

  • 当我们需要使用不同的 loader 来解析处理不同类型的文件时,我们可以在 module.rules 字段下来配置相关的规则,例如使用 Babel 来处理 .js 文件

    module: {
      // ...
      rules: [
        {
          test: /.jsx?/, // 匹配文件路径的正则表达式,通常我们都是匹配文件类型后缀
          include: [
            path.resolve(__dirname, 'src') // 指定哪些路径下的文件需要经过 loader 处理
          ],
          use: 'babel-loader', // 指定使用的 loader
        },
      ],
    }...
    

plugin

  • 模块代码转换的工作由 loader 来处理,除此之外的其他任何工作都可以交由 plugin 来完成。

  • 通过添加我们需要的 plugin,可以满足更多构建中特殊的需求。例如,要使用压缩 JS代码的 uglifyjs-webpack-plugin插件,只需在配置中通过 plugins字段添加新的 plugin即可

    const UglifyPlugin = require('uglifyjs-webpack-plugin')
      
    module.exports = {
      plugins: [
        new UglifyPlugin()
      ],
    }
    
  • plugin 理论上可以干涉 webpack 整个构建流程,可以在流程的每一个步骤中定制自己的构建需求

输出

  • 构建结果的文件名、路径等都是可以配置的,使用 output字段

    module.exports = {
      // ...
      output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js',
      },
    }
      
    // 或者多个入口生成不同文件
    module.exports = {
      entry: {
        foo: './src/foo.js',
        bar: './src/bar.js',
      },
      output: {
        filename: '[name].js',
        path: __dirname + '/dist',
      },
    }
      
    // 路径中使用 hash,每次构建时会有一个不同 hash 值,避免发布新版本时线上使用浏览器缓存
    module.exports = {
      // ...
      output: {
        filename: '[name].js',
        path: __dirname + '/dist/[hash]',
      },
    }...
    
  • 我们一开始直接使用 webpack 构建时,默认创建的输出内容就是 ./dist/main.js

一个简单的webpack应用

  • webpack 运行时默认读取项目下的 webpack.config.js 文件作为配置。所以我们在项目中创建一个 webpack.config.js 文件

    const path = require('path')
    const UglifyPlugin = require('uglifyjs-webpack-plugin')
      
    module.exports = {
      entry: './src/index.js',
      
      output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js',
      },
      
      module: {
        rules: [
          {
            test: /.jsx?/,
            include: [
              path.resolve(__dirname, 'src')
            ],
            use: 'babel-loader',
          },
        ],
      },
      
      // 代码模块路径解析的配置
      resolve: {
        modules: [
          "node_modules",
          path.resolve(__dirname, 'src')
        ],
    	// 自动添加模块后缀名
        extensions: [".wasm", ".mjs", ".js", ".json", ".jsx"],
      },
      
      plugins: [
        new UglifyPlugin(), 
        // 使用 uglifyjs-webpack-plugin 来压缩 JS 代码
        // 如果你留意了我们一开始直接使用 webpack 构建的结果,你会发现默认已经使用了 JS 代码压缩的插件
        // 这其实也是我们命令中的 --mode production 的效果,后续的小节会介绍 webpack 的 mode 参数
      ],
    }...
    

搭建基础的前端开发环境

关联 HTML

  • webpack 默认从作为入口的 .js 文件进行构建(更多是基于 SPA 去考虑),但通常一个前端项目都是从一个页面(即 HTML)出发的,最简单的方法是,创建一个 HTML 文件,使用 script 标签直接引用构建好的 JS 文件,如…

    <script src="./dist/bundle.js"></script>
    
  • 但是,如果我们的文件名或者路径会变化,例如使用 [hash] 来进行命名,那么最好是将 HTML 引用路径和我们的构建结果关联起来,这个时候我们可以使用 html-webpack-plugin

  • html-webpack-plugin 是一个独立的 node package,所以在使用之前我们需要先安装它,把它安装到项目的开发依赖中

    npm install html-webpack-plugin -D
    
  • 然后在 webpack配置中,将 html-webpack-plugin 添加到 plugins 列表中

    const HtmlWebpackPlugin = require('html-webpack-plugin')
      
    module.exports = {
      // ...
      plugins: [
        new HtmlWebpackPlugin(),
      ],
    }...
    
  • 这样配置好之后,构建时 html-webpack-plugin 会为我们创建一个 HTML 文件,其中会引用构建出来的 JS 文件。实际项目中,默认创建的 HTML 文件并没有什么用,我们需要自己来写 HTML 文件,可以通过 html-webpack-plugin 的配置,传递一个写好的 HTML 模板…

    module.exports = {
      // ...
      plugins: [
        new HtmlWebpackPlugin({
          filename: 'index.html', // 配置输出文件名和路径
          template: 'assets/index.html', // 配置文件模板,也就是入口
          inject: 'body',
        }),
      ],
    }...
    
  • 这样,通过 html-webpack-plugin 就可以将我们的页面和构建 JS 关联起来,回归日常,从页面开始开发。如果需要添加多个页面关联,那么实例化多个 html-webpack-plugin, 并将它们都放到 plugins 字段数组中就可以了…

构建 CSS

  • 我们编写 CSS,并且希望使用 webpack 来进行构建,为此,需要在配置中引入 loader 来解析和处理 CSS 文件

    module.exports = {
      module: {
        rules: [
          // ...
          {
            test: /.css/,
            include: [
              path.resolve(__dirname, 'src'),
            ],
            use: [
              'style-loader',
              'css-loader',
            ],
          },
        ],
      }
    }...
    
  • css-loader 负责解析 CSS 代码,主要是为了处理 CSS 中的依赖,例如 @importurl() 等引用外部文件的声明;

  • style-loader 会将 css-loader 解析的结果转变成 JS代码,运行时动态插入 style 标签来让 CSS 代码生效…

  • 经由上述两个 loader 的处理后,CSS 代码会转变为 JS,和 index.js一起打包了。如果需要单独把 CSS 文件分离出来,我们需要使用 extract-text-webpack-plugin 插件

    const ExtractTextPlugin = require('extract-text-webpack-plugin')
      
    module.exports = {
      // ...
      module: {
        rules: [
          {
            test: /.css$/,
            // 因为这个插件需要干涉模块转换的内容,所以需要使用它对应的 loader
            use: ExtractTextPlugin.extract({ 
              fallback: 'style-loader',
              use: 'css-loader',
            }), 
          },
        ],
      },
      plugins: [
        // 引入插件,配置文件名,这里同样可以使用 [hash]
        new ExtractTextPlugin('index.css'),
      ],
    }...
    

CSS 预处理器

  • 在上述使用 CSS 的基础上,通常我们会使用 Less/Sass 等 CSS 预处理器,webpack 可以通过添加对应的 loader 来支持,以使用 Less 为例,我们可以在官方文档中找到对应的 loader

    module.exports = {
      // ...
      module: {
        rules: [
          {
            test: /.less$/,
            // 因为这个插件需要干涉模块转换的内容,所以需要使用它对应的 loader
            use: ExtractTextPlugin.extract({ 
              fallback: 'style-loader',
              use: [
                'css-loader', 
                'less-loader',
              ],
            }), 
          },
        ],
      },
      // ...
    }...
    

处理图片文件

  • 在前端项目的样式中总会使用到图片,虽然我们已经提到 css-loader 会解析样式中用 url() 引用的文件路径,但是图片对应的 jpg/png/gif 等文件格式,webpack 处理不了。是的,我们只要添加一个处理图片的 loader 配置就可以了

file-loader

  • 现有的 file-loader 就是个不错的选择…

    module.exports = {
      // ...
      module: {
        rules: [
          {
            test: /.(png|jpg|gif)$/,
            use: [
              {
                loader: 'file-loader',
                options: {},
              },
            ],
          },
        ],
      },
    }...
    
  • 在HTML和CSS内使用时,我们可以像平常一样使用相对路径和绝对路径

    // html
    <img src="./images/bg_img.png">
      
    // css
    { background: url("./images/bg_img.png"); }
    
  • 在JS内使用的时候,我们需要通过引入图片,作为模块使用

    import imgURL from '../src/img/aboutme-background.jpg';
    <img src={imgURL } />  
    
     <img src={require('../../../src/img/aboutme-background.jpg')} /> 
    

url-loader

  • url-loader功能基本和file-loader一致,所以也可用url-loader替代。

  • url-loader还可对小于某个大小尺寸的图片进行base64格式的转化处理。

    module.exports={
        module:{
            rules:[
                {
                    test: /.(png|jpg|gif|svg)$/,
                    use: ['url-loader'],
                    options: {
                        name: './images/[name].[ext]',
                        limit: 1024
                    }        
                }
            ]
        }
    }
    
  • limit属性的作用就是,将文件小于1024B大小的图片转成base64格式,而大于的则以file-loader方式打包处理。

对比

  • 使用file-loader方式打包:

  • 使用url-loader方式打包:

    img

  • 如果不写limit属性,则不会以url-loader方式打包

使用 Babel

  • Babel 是一个让我们能够使用 ES 新特性的 JS 编译工具,我们可以在 webpack 中配置 Babel,以便使用 ES6ES7 标准来编写 JS代码

    module.exports = {
      // ...
      module: {
        rules: [
          {
            test: /.jsx?/, // 支持 js 和 jsx
            include: [
              path.resolve(__dirname, 'src'), // src 目录下的才需要经过 babel-loader 处理
            ],
            loader: 'babel-loader',
          },
        ],
      },
    }...
    

启动静态服务

  • 至此,我们完成了处理多种文件类型的 webpack 配置。我们可以使用 webpack-dev-server 在本地开启一个简单的静态服务来进行开发

    npm i --save-dev webpack-dev-server
    
  • package.json

    "scripts": {
      "build": "webpack --mode production",
      "start": "webpack-dev-server --mode development"
    }
    
  • 尝试着运行 npm start 或者 yarn start,然后就可以访问http://localhost:8080/ 来查看你的页面了。默认是访问 index.html,如果是其他页面要注意访问的 URL 是否正确

webpack如何解析代码模块路径

  • webpack 中有一个很关键的模块 enhanced-resolve 就是处理依赖模块路径的解析的,这个模块可以说是 Node.js 那一套模块路径解析的增强版本,有很多可以自定义的解析配置

  • 在 webpack 配置中,和模块路径解析相关的配置都在 resolve字段下

    module.exports = {
      resolve: {
        // ...
      }
    }
    

一般步骤

  • 解析路径后,解析器将路径指向文件或者文件夹(目录)
    • 如果是文件,直接加载,根据resolve.extensions配置补充后缀名
    • 如果是文件夹,查找里面是否有package.json文件
      • 如果有,默认按照里面的main字段的文件名查找文件 (可以通过resolve.mainFields 配置更改)
      • 如果没有,默认查找index.js文件(可以通过resolve.mainFiles配置更改)

resolve.alias

  • alias,顾名思义,是指路径的别名。简单点说,就是用一个简单的别名来替换一个常用的或者复杂的文件路径。

  • 原理:先替换,后解析。在引入模块时,先将模块路径中匹配alias中的key替换成对应的value,再做查找

    1. 替换掉的路径可以是相对路径,也可以是绝对路径。
  • 假设我们有个 utils 模块极其常用,经常编写相对路径很麻烦,希望可以直接 import 'utils' 来引用,那么我们可以配置某个模块的别名,如

    alias: {
      utils: path.resolve(__dirname, 'src/utils') // 这里使用 path.resolve 和 __dirname 来获取绝对路径
    }
    
  • 上述的配置是模糊匹配,意味着只要模块路径中携带了 utils 就可以被替换掉,如:

    import 'utils/query.js' // 等同于 import '[项目绝对路径]/src/utils/query.js'
    
  • 如果需要进行精确匹配可以使用$

    alias: {
      utils$: path.resolve(__dirname, 'src/utils') // 只会匹配 import 'utils'
    }
    

resolve.extensions

  • extensions: ['.wasm', '.mjs', '.js', '.json', '.jsx'],
    // 这里的顺序代表匹配后缀的优先级,例如对于 index.js 和 index.jsx,会优先选择 index.js
    
  • 这个配置的作用是和文件后缀名有关的,这个配置可以定义在进行模块路径解析时,webpack 会尝试帮你补全那些后缀名来进行查找

resolve.modules

  • 查找声明依赖名的模块,默认搜索node_modules目录。一般我们不修改这个配置。

    resolve: {
      modules: ['node_modules']
    },
    

resolve.mainFields

  • 在引用模块时,指明使用package.json中哪个字段指定的文件,默认是“main”

    resolve: {
      // 配置 target === "web" 或者 target === "webworker" 时 mainFields 默认值是:
      mainFields: ['browser', 'module', 'main'],
      
      // target 的值为其他时,mainFields 默认值为:
      mainFields: ["module", "main"],
    }
    
  • 因为通常情况下,模块的 package 都不会声明 browser 或 module 字段,所以便是使用 main 了。

resolve.mainFiles

  • 在目录中没有package.json时,指明使用该目录中哪个文件,默认是index.js

    resolve: {
      mainFiles: ['index'], // 可以添加其他默认使用的文件名
    }
    
  • ./design/Cascader/ 解析为文件夹,有没有办法不加最后一个斜杠呢?
  • ./design/Cascader 解析为文件

配置loader

loader 匹配规则

  • 当我们需要配置 loader 时,都是在 module.rules 中添加新的配置项,在该字段中,每一项被视为一条匹配使用 loader的规则

    module.exports = {
      // ...
      module: {
        rules: [ 
          {
            test: /.jsx?/, // 条件
            include: [ 
              path.resolve(__dirname, 'src'),
            ], // 条件
            use: 'babel-loader', // 规则应用结果
          }, // 一个 object 即一条规则
          // ...
        ],
      },
    }...
    
  • loader 的匹配规则中有两个最关键的因素:一个是匹配条件,一个是匹配规则后的应用

规则条件配置

  • 大多数情况下,配置 loader 的匹配条件时,只要使用 test 字段就好了,很多时候都只需要匹配文件后缀名来决定使用什么 loader,但也不排除在某些特殊场景下,我们需要配置比较复杂的匹配条件。webpack 的规则提供了多种配置形式…

    { test: ... } 匹配特定条件
    { include: ... } 匹配特定路径
    { exclude: ... }排除特定路径
    { and: [...] }必须匹配数组中所有条件
    { or: [...] }匹配数组中任意一个条件
    { not: [...] } 排除匹配数组中所有条件…
    
  • 上述的所谓条件的值可以是:

    • 字符串:必须以提供的字符串开始,所以是字符串的话,这里我们需要提供绝对路径
    • 正则表达式:调用正则的 test 方法来判断匹配
    • 函数:(path) => boolean,返回 true 表示匹配
    • 数组:至少包含一个条件的数组
    • 对象:匹配所有属性值的条件…
    rules: [
      {
        test: /.jsx?/, // 正则
        include: [
          path.resolve(__dirname, 'src'), // 字符串,注意是绝对路径
        ], // 数组
        // ...
      },
      {
        test: {
          js: /.js/,
          jsx: /.jsx/,
        }, // 对象,不建议使用
        not: [
          (value) => { /* ... */ return true; }, // 函数,通常需要高度自定义时才会使用
        ],
      },
    ],...
    

使用 loader 配置

  • module.rules 的匹配规则最重要的还是用于配置 loader,我们可以使用 use 字段

    rules: [
      {
        test: /.less/,
        use: [
          'style-loader', // 直接使用字符串表示 loader
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1
            },
          }, // 用对象表示 loader,可以传递 loader 配置等
          {
            loader: 'less-loader',
            options: {
              noIeCompat: true
            }, // 传递 loader 配置
          },
        ],
      },
    ],...
    
  • use字段可以是一个数组,也可以是一个字符串或者表示 loader 的对象。如果只需要一个 loader,也可以这样:use: { loader: 'babel-loader', options: { ... } }

loader 应用顺序

  • 对于上面的 less 规则配置,一个 style.less 文件会途径 less-loadercss-loaderstyle-loader 处理,成为一个可以打包的模块。

  • loader 的应用顺序在配置多个 loader 一起工作时很重要,通常会使用在 CSS 配置上,除了 style-loadercss-loader,你可能还要配置 less-loader然后再加个 postcssautoprefixer 等。

  • 上述从后到前的顺序是在同一个 rule 中进行的,那如果多个 rule 匹配了同一个模块文件,loader 的应用顺序又是怎样的呢?看一份这样的配置…

    rules: [
      {
        test: /.js$/,
        exclude: /node_modules/,
        loader: "eslint-loader",
      },
      {
        test: /.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
      },
    ],...
    
  • 这样无法法保证 eslint-loaderbabel-loader 应用前执行。webpack在 rules 中提供了一个 enforce 的字段来配置当前 ruleloader 类型,没配置的话是普通类型,我们可以配置 prepost,分别对应前置类型或后置类型的 loader

  • 所有的 loader 按照前置 -> 内 -> 普通 -> 后置的顺序执行。所以当我们要确保 eslint-loaderbabel-loader 之前执行时,可以如下添加 enforce 配置

    rules: [
      {
        enforce: 'pre', // 指定为前置类型
        test: /.js$/,
        exclude: /node_modules/,
        loader: "eslint-loader",
      },
    ]...
    
  • 当项目文件类型和应用的 loader 不是特别复杂的时候,通常建议把要应用的同一类型 loader 都写在同一个匹配规则中,这样更好维护和控制

使用plugin

DefinePlugin

  • DefinePluginwebpack 内置的插件,可以使用 webpack.DefinePlugin 直接获取

  • 这个插件用于创建一些在编译时可以配置的全局常量,这些常量的值我们可以在 webpack 的配置中去指定,例如

    module.exports = {
      // ...
      plugins: [
        new webpack.DefinePlugin({
          PRODUCTION: JSON.stringify(true), // const PRODUCTION = true
          VERSION: JSON.stringify('5fa3b9'), // const VERSION = '5fa3b9'
          BROWSER_SUPPORTS_HTML5: true, // const BROWSER_SUPPORTS_HTML5 = 'true'
          TWO: '1+1', // const TWO = 1 + 1,
          CONSTANTS: {
            APP_VERSION: JSON.stringify('1.1.2') // const CONSTANTS = { APP_VERSION: '1.1.2' }
          }
        }),
      ],
    }...
    
  • 有了上面的配置,就可以在应用代码文件中,访问配置好的变量了,如:

    console.log("Running App version " + VERSION);
      
    if(!BROWSER_SUPPORTS_HTML5) require("html5shiv");
    
  • 上面配置的注释已经简单说明了这些配置的效果,这里再简述一下整个配置规则:

    • 如果配置的值是字符串,那么整个字符串会被当成代码片段来执行,其结果作为最终变量的值,如上面的 "1+1",最后的结果是 2
    • 如果配置的值不是字符串,也不是一个对象字面量,那么该值会被转为一个字符串,如 true,最后的结果是 'true'
    • 如果配置的是一个对象字面量,那么该对象的所有 key会以同样的方式去定义
    • 这样我们就可以理解为什么要使用 JSON.stringify() 了,因为 JSON.stringify(true) 的结果是 'true'JSON.stringify("5fa3b9") 的结果是 "5fa3b9"
  • 社区中关于 DefinePlugin 使用得最多的方式是定义环境变量,例如 PRODUCTION = true 或者 __DEV__ = true等。部分类库在开发环境时依赖这样的环境变量来给予开发者更多的开发调试反馈,例如 react 等。

  • 建议使用 process.env.NODE_ENV: … 的方式来定义 process.env.NODE_ENV,而不是使用 process: { env: { NODE_ENV: ... } } 的方式,因为这样会覆盖掉 process 这个对象,可能会对其他代码造成影响…

copy-webpack-plugin

  • 我们一般会把开发的所有源码和资源文件放在 src/ 目录下,构建的时候产出一个 build/ 目录,通常会直接拿 build 中的所有文件来发布。有些文件没经过 webpack 处理,但是我们希望它们也能出现在 build目录下,这时就可以使用 CopyWebpackPlugin 来处理了…

    const CopyWebpackPlugin = require('copy-webpack-plugin')
      
    module.exports = {
      // ...
      plugins: [
        new CopyWebpackPlugin([
          { from: 'src/file.txt', to: 'build/file.txt', }, // 顾名思义,from 配置来源,to 配置目标路径
          { from: 'src/*.ico', to: 'build/*.ico' }, // 配置项可以使用 glob
          // 可以配置很多项复制规则
        ]),
      ],
    }...
    
  • https://juejin.im/post/59a3ec53f265da2490350edb

extract-text-webpack-plugin

  • 我们用它来把依赖的 CSS 分离出来成为单独的文件。这里再看一下使用 extract-text-webpack-plugin 的配置

    const ExtractTextPlugin = require('extract-text-webpack-plugin')
      
    module.exports = {
      // ...
      module: {
        rules: [
          {
            test: /.css$/,
            // 因为这个插件需要干涉模块转换的内容,所以需要使用它对应的 loader
            use: ExtractTextPlugin.extract({ 
              fallback: 'style-loader',
              use: 'css-loader',
            }), 
          },
        ],
      },
      plugins: [
        // 引入插件,配置文件名,这里同样可以使用 [hash]
        new ExtractTextPlugin('index.css'),
      ],
    }...
    
  • 在上述的配置中,我们使用了 index.css 作为单独分离出来的文件名,但有的时候构建入口不止一个,extract-text-webpack-plugin 会为每一个入口创建单独分离的文件,因此最好这样配置

    // 这样确保在使用多个构建入口时,生成不同名称的文件
    plugins: [
      new ExtractTextPlugin('[name].css'),
    ],
    

更好地使用webpack-dev-server

  • webpack-dev-serverwebpack 官方提供的一个工具,可以基于当前的 webpack 构建配置快速启动一个静态服务。当 modedevelopment 时,会具备 hot reload 的功能,即当源码文件变化时,会即时更新当前页面,以便你看到最新的效果…

基础使用

  • webpack-dev-server 是一个 npm package,安装后在已经有 webpack 配置文件的项目目录下直接启动就可以

    npm install webpack-dev-server -g
    webpack-dev-server --mode development
    
  • webpack-dev-server 默认使用 8080 端口

  • package.json

    {
      // ...
      "scripts": {
        "start": "webpack-dev-server --mode development"
      }
    }
    

配置

  • 在 webpack 的配置中,可以通过 devServer 字段来配置 webpack-dev-server,如端口设置、启动 gzip 压缩等,这里简单讲解几个常用的配置

  • public字段用于指定静态服务的域名,默认是 http://localhost:8080/ ,当你使用 Nginx 来做反向代理时,应该就需要使用该配置来指定 Nginx 配置使用的服务域名

  • port 字段用于指定静态服务的端口,如上,默认是 8080,通常情况下都不需要改动

  • publicPath 字段用于指定构建好的静态文件在浏览器中用什么路径去访问,默认是 /

    • 例如,对于一个构建好的文件 bundle.js,完整的访问路径是 http://localhost:8080/bundle.js
    • 如果你配置了 publicPath: 'assets/',那么上述 bundle.js 的完整访问路径就是 http://localhost:8080/assets/bundle.js
    • 可以使用整个 URL 来作为 publicPath的值,如 publicPath: 'http://localhost:8080/assets/'
    • 如果你使用了 HMR,那么要设置 publicPath 就必须使用完整的 URL
    • 建议将 devServer.publicPathoutput.publicPath 的值保持一致
  • beforeafter 配置用于在 webpack-dev-server 定义额外的中间件,如

    before(app){
      app.get('/some/path', function(req, res) { // 当访问 /some/path 路径时,返回自定义的 json 数据
        res.json({ custom: 'response' })
      })
    }...
      
    
  • beforewebpack-dev-server 静态资源中间件处理之前,可以用于拦截部分请求返回特定内容,或者实现简单的数据 mock

  • afterwebpack-dev-server 静态资源中间件处理之后,比较少用到,可以用于打印日志或者做一些额外处理…

proxy字段

  • proxy用于配置 webpack-dev-server将特定 URL 的请求代理到另外一台服务器上。当你有单独的后端开发服务器用于请求 API 时,这个配置相当有用

  • dev-server 使用了非常强大的 http-proxy-middleware 包。更多高级用法,请查阅其 文档

基本使用

  • localhost:3000 上有后端服务的话,你可以这样启用代理:

  • webpack.config.js

    module.exports = {
      //...
      devServer: {
        proxy: {
          '/api': 'http://localhost:3000'
        }
      }
    };
    
  • 请求到 /api/users 现在会被代理到请求 http://localhost:3000/api/users

重写路径

  • 如果你不想始终传递 /api ,则需要重写路径:

  • webpack.config.js

    module.exports = {
      //...
      devServer: {
        proxy: {
          '/api': {
            target: 'http://localhost:3000',
            pathRewrite: {'^/api' : ''}
          }
        }
      }
    };
    
  • 后端不需要再加上 ‘api’ ,但是前端请求还是要的,用作转发的标志

代理多个路径

  • 如果你想要代理多个路径特定到同一个 target 下,你可以使用由一个或多个「具有 context 属性的对象」构成的数组:

    webpack.config.js

    module.exports = {
      //...
      devServer: {
        proxy: [{
          context: ['/auth', '/api'],
          target: 'http://localhost:3000',
        }]
      }
    };
    

配合HTTPs

  • https://webpack.docschina.org/configuration/dev-server/#devserver-proxy

代理根路径

完整代码

  • const path = require('path')
    const webpack = require('webpack')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    const ExtractTextPlugin = require('extract-text-webpack-plugin')
    const CopyWebpackPlugin = require('copy-webpack-plugin')
      
    module.exports = {
      entry: './src/index',
      
      output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js',
      },
      
      module: {
        rules: [
          {
            enforce: 'pre', // 指定为前置类型
            test: /.jsx?$/,
            exclude: /node_modules/,
            loader: "eslint-loader",
          },
          {
            test: /.jsx?$/,
            include: [
              path.resolve(__dirname, 'src'),
            ],
            use: 'babel-loader',
          },
          {
            test: /.css$/,
            use: ExtractTextPlugin.extract({
              fallback: 'style-loader',
              use: [
                'css-loader',
              ],
            }),
          },
          {
            test: /.less$/,
            use: ExtractTextPlugin.extract({
              fallback: 'style-loader',
              use: [
                'css-loader',
                'less-loader',
              ],
            }),
          },
          {
            test: /.(png|jpg|gif)$/,
            use: [
              {
                loader: 'file-loader'
              },
            ],
          },
        ],
      },
      
      resolve: {
        alias: {
          utils: path.resolve(__dirname, 'src/utils'), // 这里使用 path.resolve 和 __dirname 来获取绝对路径
          log$: path.resolve(__dirname, 'src/utils/log.js') // 只匹配 log
        },
        extensions: ['.js', '.json', '.jsx', '.css', '.less'],
        modules: [
          path.resolve(__dirname, 'node_modules'), // 指定当前目录下的 node_modules 优先查找
        ],
      },
      
      plugins: [
        new HtmlWebpackPlugin({
          filename: 'index.html', // 配置输出文件名和路径
          template: 'src/index.html', // 配置文件模板
        }),
        new ExtractTextPlugin('[name].css'),
        new webpack.DefinePlugin({
          TWO: '1+1',
          CONSTANTS: {
            APP_VERSION: JSON.stringify('1.1.2'), // const CONSTANTS = { APP_VERSION: '1.1.2' }
          },
        }),
        new CopyWebpackPlugin([
          { from: 'src/assets/favicon.ico', to: 'favicon.ico', }, // 顾名思义,from 配置来源,to 配置目标路径
        ]),
        new webpack.ProvidePlugin({
          _: 'lodash',
        }),
      ],
      
      devServer: {
        port: '1234',
        before(app){
          app.get('/api/test.json', function(req, res) { // 当访问 /some/path 路径时,返回自定义的 json 数据
            res.json({ code: 200, message: 'hello world' })
          })
        }
      },
    }
    

开发和生产环境的构建配置差异

  • 我们在日常的前端开发工作中,一般都会有两套构建环境:一套开发时使用,构建结果用于本地开发调试,不进行代码压缩,打印 debug 信息,包含sourcemap 文件
  • 另外一套构建后的结果是直接应用于线上的,即代码都是压缩后,运行时不打印 debug 信息,静态文件不包括 sourcemap 的。有的时候可能还需要多一套测试环境,在运行时直接进行请求 mock 等工作
  • webpack 4.x 版本引入了 mode 的概念,在运行 webpack 时需要指定使用 productiondevelopment 两个 mode 其中一个,这个功能也就是我们所需要的运行两套构建环境的能力。

在配置文件中区分 mode

  • 之前我们的配置文件都是直接对外暴露一个 JS 对象,这种方式暂时没有办法获取到 webpackmode参数,我们需要更换一种方式来处理配置。根据官方的文档多种配置类型,配置文件可以对外暴露一个函数,因此我们可以这样做

    module.exports = (env, argv) => ({
      // ... 其他配置
      optimization: {
        minimize: false,
        // 使用 argv 来获取 mode 参数的值
        minimizer: argv.mode === 'production' ? [
          new UglifyJsPlugin({ /* 你自己的配置 */ }), 
          // 仅在我们要自定义压缩配置时才需要这么做
          // mode 为 production 时 webpack 会默认使用压缩 JS 的 plugin
        ] : [],
      },
    })...
    
  • 这样获取 mode 之后,我们就能够区分不同的构建环境,然后根据不同环境再对特殊的 loaderplugin做额外的配置就可以了
  • 以上是 webpack 4.x 的做法,由于有了 mode 参数,区分环境变得简单了。不过在当前业界,估计还是使用 webpack 3.x 版本的居多,所以这里也简单介绍一下 3.x 如何区分环境

常见的环境差异配置

  • 生产环境可能需要分离 CSS成单独的文件,以便多个页面共享同一个 CSS 文件
  • 生产环境需要压缩 HTML/CSS/JS 代码
  • 生产环境需要压缩图片
  • 开发环境需要生成 sourcemap 文件
  • 开发环境需要打印 debug 信息
  • 开发环境需要 live reload或者 hot reload 的功能…
  • webpack 4.xmode 已经提供了上述差异配置的大部分功能,modeproduction 时默认使用 JS 代码压缩,而modedevelopment 时默认启用 hot reload,等等。这样让我们的配置更为简洁,我们只需要针对特别使用的 loaderplugin 做区分配置就可以了…

拆分配置

  • 前面我们列出了几个环境差异配置,可能这些构建需求就已经有点多了,会让整个 webpack 的配置变得复杂,尤其是有着大量环境变量判断的配置。我们可以把 webpack 的配置按照不同的环境拆分成多个文件,运行时直接根据环境变量加载对应的配置即可。基本的划分如下…
    • webpack.base.js:基础部分,即多个文件中共享的配置
    • webpack.development.js:开发环境使用的配置
    • webpack.production.js:生产环境使用的配置
    • webpack.test.js:测试环境使用的配置…
  • 首先我们要明白,对于 webpack 的配置,其实是对外暴露一个 JS 对象,所以对于这个对象,我们都可以用 JS 代码来修改它,例如

    const config = {
      // ... webpack 配置
    }
      
    // 我们可以修改这个 config 来调整配置,例如添加一个新的插件
    config.plugins.push(new YourPlugin());
      
    module.exports = config;...
    
  • 因此,只要有一个工具能比较智能地合并多个配置对象,我们就可以很轻松地拆分 webpack 配置,然后通过判断环境变量,使用工具将对应环境的多个配置对象整合后提供给 webpack 使用。这个工具就是 webpack-merge

  • 我们的 webpack 配置基础部分,即 webpack.base.js 应该大致是这样的

    module.exports = {
      entry: '...',
      output: {
        // ...
      },
      resolve: {
        // ...
      },
      module: {
        // 这里是一个简单的例子,后面介绍 API 时会用到
        rules: [
          {
            test: /.js$/, 
            use: ['babel'],
          },
        ],
        // ...
      },
      plugins: [
        // ...
      ],
    }...
    
  • 然后 webpack.development.js 需要添加 loaderplugin,就可以使用 webpack-mergeAPI,例如

    const { smart } = require('webpack-merge')
    const webpack = require('webpack')
    const base = require('./webpack.base.js')
      
    module.exports = smart(base, {
      module: {
        rules: [
          // 用 smart API,当这里的匹配规则相同且 use 值都是数组时,smart 会识别后处理
          // 和上述 base 配置合并后,这里会是 { test: /.js$/, use: ['babel', 'coffee'] }
          // 如果这里 use 的值用的是字符串或者对象的话,那么会替换掉原本的规则 use 的值
          {
            test: /.js$/,
            use: ['coffee'],
          },
          // ...
        ],
      },
      plugins: [
        // plugins 这里的数组会和 base 中的 plugins 数组进行合并
        new webpack.DefinePlugin({
          'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
        }),
      ],
    })...
    
  • 可见 webpack-merge 提供的 smart 方法,可以帮助我们更加轻松地处理 loader 配置的合并。webpack-merge 还有其他 API 可以用于自定义合并行为 https://github.com/survivejs/webpack-merge

完整代码

  • webpack.config.js

    module.exports = function(env, argv) {
      return argv.mode === 'production' ?
        require('./configs/webpack.production') :
        require('./configs/webpack.development')
    }
    
  • configs/webpack.base.js

    const path = require('path')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
      
    module.exports = {
      entry: './src/index.js',
      
      output: {
        path: path.resolve(__dirname, '../dist'),
        filename: '[name].js',
      },
      
      module: {
        rules: [
          {
            test: /.jsx?/,
            include: [
              path.resolve(__dirname, '../src'),
            ],
            use: 'babel-loader',
          },
          {
            test: /.(png|jpg|gif)$/,
            use: [
              {
                loader: 'file-loader'
              },
            ],
          },
        ],
      },
      
      plugins: [
        new HtmlWebpackPlugin({
          filename: 'index.html', // 配置输出文件名和路径
          template: 'src/index.html', // 配置文件模板
        }),
      ],
    }
    
  • configs/webpack.development.js

    const webpack = require('webpack')
    const merge = require('webpack-merge')
    const baseConfig = require('./webpack.base')
      
    const config = merge.smart(baseConfig, {
      module: {
        rules: [
          {
            enforce: 'pre',
            test: /.jsx?$/,
            exclude: /node_modules/,
            loader: "eslint-loader",
          },
          {
            test: /.less$/,
            use: [
              'style-loader',
              'css-loader',
              'less-loader'
            ],
          },
        ],
      },
      
      devServer: {
        port: '1234',
        before(app){
          app.get('/api/test.json', function(req, res) {
            res.json({ code: 200, message: 'hello world' })
          })
        },
      },
    })
      
    config.plugins.push(
      new webpack.DefinePlugin({
        __DEV__: JSON.stringify(true),
      })
    )
      
    module.exports = config
    
  • configs/webpack.production.js

    const merge = require('webpack-merge')
    const ExtractTextPlugin = require('extract-text-webpack-plugin')
    const baseConfig = require('./webpack.base')
      
    const config = merge.smart(baseConfig, {
      module: {
        rules: [
          {
            test: /.less$/,
            use: ExtractTextPlugin.extract({
              fallback: 'style-loader',
              use: [
                {
                  loader: 'css-loader',
                  options: {
                    minimize: true
                  }
                },
                'less-loader',
              ],
            }),
          },
        ],
      }
    })
      
    config.plugins.push(new ExtractTextPlugin('[name].css'))
      
    module.exports = config
    

分别引用

  • package.json

      "scripts": {
        "test": "echo "Error: no test specified" && exit 1",
        "dev": "webpack-dev-server --hot --inline --progress --colors --config config/webpack.dev.conf.js",
        "start": "npm run dev",
        "build": "webpack --progress --colors --config config/webpack.prod.conf.js"
      }
    
  • 不同的命令执行不同的webpack.config

模块热替换提高开发效率

  • HMR 全称是 Hot Module Replacement,即模块热替换。在这个概念出来之前,我们使用过 Hot Reloading,当代码变更时通知浏览器刷新页面,以避免频繁手动刷新浏览器页面。HMR 可以理解为增强版的 Hot Reloading,但不用整个页面刷新,而是局部替换掉部分模块代码并且使其生效,可以看到代码变更后的效果。所以,HMR 既避免了频繁手动刷新页面,也减少了页面刷新时的等待,可以极大地提高前端页面开发效率…

配置使用 HMR

  • HMRwebpack 提供的非常有用的一个功能,跟我们之前提到的一样,安装好 webpack-dev-server, 添加一些简单的配置,即在webpack 的配置文件中添加启用HMR需要的两个插件

    const webpack = require('webpack')
      
    module.exports = {
      // ...
      devServer: {
        hot: true // dev server 的配置要启动 hot,或者在命令行中带参数开启
      },
      plugins: [
        // ...
        new webpack.NamedModulesPlugin(), // 用于启动 HMR 时可以显示模块的相对路径
        new webpack.HotModuleReplacementPlugin(), // Hot Module Replacement 的插件
      ],
    }...
    

module.hot 常见的 API

  • 前面 HMR实现部分已经讲解了实现 HMR 接口的重要性,下面来看看常见的 module.hot API 有哪些,以及如何使用

  • module.hot.accept 方法指定在应用特定代码模块更新时执行相应的 callback,第一个参数可以是字符串或者数组,如

    if (module.hot) {
      module.hot.accept(['./bar.js', './index.css'], () => {
        // ... 这样当 bar.js 或者 index.css 更新时都会执行该函数
      })
    }...
    
  • module.hot.decline 对于指定的代码模块,拒绝进行模块代码的更新,进入更新失败状态,如 module.hot.decline('./bar.js')。这个方法比较少用到

  • module.hot.dispose 用于添加一个处理函数,在当前模块代码被替换时运行该函数,例如

    if (module.hot) {
      module.hot.dispose((data) => {
        // data 用于传递数据,如果有需要传递的数据可以挂在 data 对象上,然后在模块代码更新后可以通过 module.hot.data 来获取
      })
    }...
    
  • module.hot.accept 通常用于指定当前依赖的某个模块更新时需要做的处理,如果是当前模块更新时需要处理的动作,使用 module.hot.dispose 会更加容易方便

  • module.hot.removeDisposeHandler用于移除 dispose 方法添加的 callback

图片加载优化

CSS Sprites

  • 如果你使用的 webpack 3.x 版本,需要 CSS Sprites 的话,可以使用 webpack-spritesmith 或者 sprite-webpack-plugin

  • 我们以 webpack-spritesmith 为例,先安装依赖…

    module: {
      loaders: [
        // ... 这里需要有处理图片的 loader,如 file-loader
      ]
    },
    resolve: {
      modules: [
        'node_modules', 
        'spritesmith-generated', // webpack-spritesmith 生成所需文件的目录
      ],
    },
    plugins: [
      new SpritesmithPlugin({
        src: {
          cwd: path.resolve(__dirname, 'src/ico'), // 多个图片所在的目录
          glob: '*.png' // 匹配图片的路径
        },
        target: {
          // 生成最终图片的路径
          image: path.resolve(__dirname, 'src/spritesmith-generated/sprite.png'), 
          // 生成所需 SASS/LESS/Stylus mixins 代码,我们使用 Stylus 预处理器做例子
          css: path.resolve(__dirname, 'src/spritesmith-generated/sprite.styl'), 
        },
        apiOptions: {
          cssImageRef: "~sprite.png"
        },
      }),
    ],...
    
  • 在你需要的样式代码中引入 sprite.styl 后调用需要的mixins 即可

    @import '~sprite.styl'
      
    .close-button
        sprite($close)
    .open-button
        sprite($open)
    
  • 如果你使用的是 webpack 4.x,你需要配合使用 postcsspostcss-sprites,才能实现 CSS Sprites 的相关构建

图片压缩

  • 在一般的项目中,图片资源会占前端资源的很大一部分,既然代码都进行压缩了,占大头的图片就更不用说了

  • 我们之前提及使用file-loader 来处理图片文件,在此基础上,我们再添加一个 image-webpack-loader来压缩图片文件。简单的配置如下…

    module.exports = {
      // ...
      module: {
        rules: [
          {
            test: /.*.(gif|png|jpe?g|svg|webp)$/i,
            use: [
              {
                loader: 'file-loader',
                options: {}
              },
              {
                loader: 'image-webpack-loader',
                options: {
                  mozjpeg: { // 压缩 jpeg 的配置
                    progressive: true,
                    quality: 65
                  },
                  optipng: { // 使用 imagemin-optipng 压缩 png,enable: false 为关闭
                    enabled: false,
                  },
                  pngquant: { // 使用 imagemin-pngquant 压缩 png
                    quality: '65-90',
                    speed: 4
                  },
                  gifsicle: { // 压缩 gif 的配置
                    interlaced: false,
                  },
                  webp: { // 开启 webp,会把 jpg 和 png 图片压缩为 webp 格式
                    quality: 75
                  },
              },
            ],
          },
        ],
      },
    }...
    

使用 DataURL

  • 有的时候我们的项目中会有一些很小的图片,因为某些缘故并不想使用 CSS Sprites 的方式来处理(譬如小图片不多,因此引入 CSS Sprites 感觉麻烦),那么我们可以在 webpack 中使用 url-loader 来处理这些很小的图片…
  • url-loaderfile-loader 的功能类似,但是在处理文件的时候,可以通过配置指定一个大小,当文件小于这个配置值时,url-loader 会将其转换为一个 base64 编码的 DataURL

代码压缩

  • webpack 4.x 版本运行时,modeproduction 即会启动压缩 JS 代码的插件,而对于 webpack 3.x,使用压缩 JS 代码插件的方式也已经介绍过了。在生产环境中,压缩 JS 代码基本是一个必不可少的步骤,这样可以大大减小 JavaScript 的体积,相关内容这里不再赘述。

  • 除了 JS 代码之外,我们一般还需要 HTML 和 CSS 文件,这两种文件也都是可以压缩的,虽然不像 JS 的压缩那么彻底(替换掉长变量等),只能移除空格换行等无用字符,但也能在一定程度上减小文件大小。在 webpack 中的配置使用也不是特别麻烦,所以我们通常也会使用。

  • 对于 HTML 文件,之前介绍的 html-webpack-plugin 插件可以帮助我们生成需要的 HTML 并对其进行压缩…

    module.exports = {
      // ...
      plugins: [
        new HtmlWebpackPlugin({
          filename: 'index.html', // 配置输出文件名和路径
          template: 'assets/index.html', // 配置文件模板
          minify: { // 压缩 HTML 的配置
            minifyCSS: true, // 压缩 HTML 中出现的 CSS 代码
            minifyJS: true // 压缩 HTML 中出现的 JS 代码
          }
        }),
      ],
    }...
    
  • 如上,使用 minify 字段配置就可以使用 HTML 压缩,这个插件是使用 html-minifier 来实现HTML 代码压缩的,minify下的配置项直接透传给 html-minifier,配置项参考 html-minifier 文档即可。

  • 对于 CSS 文件,我们之前介绍过用来处理 CSS 文件的 css-loader,也提供了压缩 CSS 代码的功能:…

    module.exports = {
      module: {
        rules: [
          // ...
          {
            test: /.css/,
            include: [
              path.resolve(__dirname, 'src'),
            ],
            use: [
              'style-loader',
              {
                loader: 'css-loader',
                options: {
                  minimize: true, // 使用 css 的压缩功能
                },
              },
            ],
          },
        ],
      }
    }...
    
  • css-loader 的选项中配置 minimize 字段为 true来使用CSS 压缩代码的功能。css-loader 是使用 cssnano来压缩代码的,minimize 字段也可以配置为一个对象,来将相关配置传递给 cssnano

分离代码文件

  • 关于分离 CSS 文件这个主题,之前在介绍如何搭建基本的前端开发环境时有提及,在 webpack 中使用 extract-text-webpack-plugin 插件即可。

  • 先简单解释一下为何要把 CSS 文件分离出来,而不是直接一起打包在 JS 中。最主要的原因是我们希望更好地利用缓存。

  • 假设我们原本页面的静态资源都打包成一个 JS 文件,加载页面时虽然只需要加载一个 JS 文件,但是我们的代码一旦改变了,用户访问新的页面时就需要重新加载一个新的 JS 文件。有些情况下,我们只是单独修改了样式,这样也要重新加载整个应用的 JS 文件,相当不划算。

  • 还有一种情况是我们有多个页面,它们都可以共用一部分样式(这是很常见的,CSS Reset、基础组件样式等基本都是跨页面通用),如果每个页面都单独打包一个 JS 文件,那么每次访问页面都会重复加载原本可以共享的那些 CSS 代码。如果分离开来,第二个页面就有了 CSS 文件的缓存,访问速度自然会加快。虽然对第一个页面来说多了一个请求,但是对随后的页面来说,缓存带来的速度提升相对更加可观…

  • 3.x 以前的版本是使用 CommonsChunkPlugin 来做代码分离的,而 webpack 4.x 则是把相关的功能包到了optimize.splitChunks 中,直接使用该配置就可以实现代码分离。

    module.exports = {
      // ... webpack 配置
      
      optimization: {
        splitChunks: {
          chunks: "all", // 所有的 chunks 代码公共的部分分离出来成为一个单独的文件
        },
      },
    }...
    
  • 我们需要在 HTML 中引用两个构建出来的 JS 文件,并且 commons.js 需要在入口代码之前。下面是个简单的例子

    <script src="commons.js" charset="utf-8"></script>
    <script src="entry.bundle.js" charset="utf-8"></script>
    
  • 如果你使用了 html-webpack-plugin,那么对应需要的 JS 文件都会在 HTML 文件中正确引用,不用担心。如果没有使用,那么你需要从 statsentrypoints 属性来获取入口应该引用哪些 JS 文件,可以参考 Node API 了解如何从 stats 中获取信息…

显式配置共享类库可以这么操作

  • module.exports = {
      entry: {
        vendor: ["react", "lodash", "angular", ...], // 指定公共使用的第三方类库
      },
      optimization: {
        splitChunks: {
          cacheGroups: {
            vendor: {
              chunks: "initial",
              test: "vendor",
              name: "vendor", // 使用 vendor 入口作为公共部分
              enforce: true,
            },
          },
        },
      },
      // ... 其他配置
    }
      
    // 或者
    module.exports = {
      optimization: {
        splitChunks: {
          cacheGroups: {
            vendor: {
              test: /react|angluar|lodash/, // 直接使用 test 来做路径匹配
              chunks: "initial",
              name: "vendor",
              enforce: true,
            },
          },
        },
      },
    }
      
    // 或者
    module.exports = {
      optimization: {
        splitChunks: {
          cacheGroups: {
            vendor: {
              chunks: "initial",
              test: path.resolve(__dirname, "node_modules") // 路径在 node_modules 目录下的都作为公共部分
              name: "vendor", // 使用 vendor 入口作为公共部分
              enforce: true,
            },
          },
        },
      },
    }...
    
  • 上述第一种做法是显示指定哪些类库作为公共部分,第二种做法实现的功能差不多,只是利用了 test 来做模块路径的匹配,第三种做法是把所有在 node_modules 下的模块,即作为依赖安装的,都作为公共部分。你可以针对项目情况,选择最合适的做法..

进一步控制JS大小

按需加载模块

  • 在 webpack 的构建环境中,要按需加载代码模块很简单,遵循 ES 标准的动态加载语法 dynamic-import 来编写代码即可,webpack 会自动处理使用该语法编写的模块

    // import 作为一个方法使用,传入模块名即可,返回一个 promise 来获取模块暴露的对象
    // 注释 webpackChunkName: "lodash" 可以用于指定 chunk 的名称,在输出文件时有用
    import(/* webpackChunkName: "lodash" */ 'lodash').then((_) => { 
      console.log(_.lash([1, 2, 3])) // 打印 3
    })...
    
  • 注意一下,如果你使用了 Babel 的话,还需要 Syntax Dynamic Import 这个 Babel 插件来处理 import() 这种语法。

  • 由于动态加载代码模块的语法依赖于 promise,对于低版本的浏览器,需要添加 promisepolyfill 后才能使用。

  • 如上的代码,webpack 构建时会自动把 lodash 模块分离出来,并且在代码内部实现动态加载 lodash 的功能。动态加载代码时依赖于网络,其模块内容会异步返回,所以 import 方法是返回一个 promise 来获取动态加载的模块内容。

  • import 后面的注释 webpackChunkName: "lodash" 用于告知 webpack所要动态加载模块的名称。我们在 webpack 配置中添加一个 output.chunkFilename 的配置…

    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: '[name].[hash:8].js',
      chunkFilename: '[name].[hash:8].js' // 指定分离出来的代码文件的名称
    },...
    
  • 这样就可以把分离出来的文件名称用 lodash 标识了,如下图:

  • 如果没有添加注释 webpackChunkName: "lodash" 以及 output.chunkFilename 配置,那么分离出来的文件名称会以简单数字的方式标识,不便于识别

完整代码

  • const path = require('path')
    const webpack = require('webpack')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    const ExtractTextPlugin = require('extract-text-webpack-plugin')
      
    module.exports = {
      entry: './src/index.js',
      
      output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js',
      },
      
      module: {
        rules: [
          {
            test: /.jsx?/,
            include: [
              path.resolve(__dirname, 'src'),
            ],
            use: 'babel-loader',
          },
          {
            test: /.less$/,
            use: ExtractTextPlugin.extract({
              fallback: 'style-loader',
              use: [
                'css-loader',
                'postcss-loader',
                'less-loader',
              ],
            }),
          },
          {
            test: /.(png|jpg|gif)$/,
            use: [
              {
                loader: 'url-loader',
                options: {
                  limit: 8192
                },
              },
              {
                loader: 'image-webpack-loader',
                options: {
                  mozjpeg: { // 压缩 jpeg 的配置
                    progressive: true,
                    quality: 65
                  },
                  optipng: { // 使用 imagemin-optipng 压缩 png,enable: false 为关闭
                    enabled: false,
                  },
                  pngquant: { // 使用 imagemin-pngquant 压缩 png
                    quality: '65-90',
                    speed: 4
                  },
                  gifsicle: { // 压缩 gif 的配置
                    interlaced: false,
                  },
                  webp: { // 开启 webp,会把 jpg 和 png 图片压缩为 webp 格式
                    quality: 75
                  },
                },
              },
            ],
          },
        ],
      },
      
      optimization: {
        splitChunks: {
          cacheGroups: {
            vendor: {
              chunks: "initial",
              test: path.resolve(__dirname, "node_modules"), // 路径在 node_modules 目录下的都作为公共部分
              name: "vendor", // 使用 vendor 入口作为公共部分
              enforce: true,
            },
          },
        },
      },
      
      plugins: [
        new HtmlWebpackPlugin({
          filename: 'index.html', // 配置输出文件名和路径
          template: 'src/index.html', // 配置文件模板
          minify: { // 压缩 HTML 的配置
            minifyCSS: true, // 压缩 HTML 中出现的 CSS 代码
            minifyJS: true, // 压缩 HTML 中出现的 JS 代码
            removeComments: true,
          },
        }),
        new ExtractTextPlugin('[name].css'),
        new webpack.NamedModulesPlugin(),
        new webpack.HotModuleReplacementPlugin(),
      ],
      
      devServer: {
        hot: true
      }
    }
    

创建一个简单webpack应用

  • 生成package.json文件

    {
      "name": "webpack_test",
      "version": "1.0.0"
    }
    
  • 安装webpack

    • npm install webpack -g //全局安装
    • npm install webpack –save-dev //局部安装
    • 根据package.json文件下载相应版本的webpack
    • npm install [email protected] –save-dev
  • 编译打包应用

    • 创建入口src/js/ : entry.js

      document.write("entry.js is working");
      
    • 创建主页面: dist/index.html

      <script
      type="text/javascript" src="bundle.js"></script>
      
    • 编译js

      webpack src/js/entry.js dist/bundle.js 
      
    • 打包js,指定输出路径

    • 与grunt和gulp不同,打包功能(合并功能),通过配置文件简化流程

      // 创建第二个js: src/js/math.js
      export function square(x) {
      return x * x;
      }
          
      export function cube(x) {
      return x * x * x;
      }
      // 创建json文件: src/json/data.json
      {
      "name": "Tom",
      "age": 12
      }
      // 更新入口js : entry.js
      import {cube} from './math'
      import data from '../json/data.json'
      // 注意data会自动被转换为原生的js对象或者数组
      document.write("entry.js is work <br/>");
      document.write(cube(2) + '<br/>');
      document.write(JSON.stringify(data) + '<br/>')
      // 编译js:
      webpack src/js/entry.js dist/bundle.js
      // 查看页面效果
      
  • 使用webpack配置文件

    • 创建webpack.config.js

      const path = require('path'); //path内置的模块,用来设置路径。
          
      module.exports = {
        entry: './src/js/entry.js',   // 入口文件(主文件)
        output: {                     // 输出配置
          filename: 'bundle.js',      // 输出文件名
      pubulicPath:js/  //设置为index.html提供资源服务的地址(强制性)会影响到热加载的配置,不推荐使用
          path: path.resolve(__dirname, 'dist')   //输出文件路径配置
        }
      };
      
    • webpack执行时会首先读取配置文件,配置文件设置好后,直接输入webpack即可执行打包

  • 配置npm命令: package.json

    scripts": {
      "build": "webpack"
    },
    
    npm run build
    

图片

为什么在JavaScript中加载图片不能直接识别图片路径

因为在webpack中,除了js类型的文件是能够直接被识别并打包,其他类型文件(css、图片等)则需要通过特定的loader来进行加载打包,而图片则需要用到file-loader或url-loader。

在JavaScript中引入图片路径时,webpack并不知道它是一张图片,所以需要先用require将图片资源加载进来,然后再作为图片路径添加到对象上。

url-loader和file-loader功能基本一致,只不过url-loader能将小于某个大小(可以自定义配置)的图片进行base64格式的转化处理。

参考:https://juejin.im/post/5c6515c8e51d456d9574d868

  • 打包css和图片文件

    • 安装样式的loader

    • npm install css-loader style-loader --save-dev //css
      npm install file-loader url-loader --save-dev   //图片
      // 补充:url-loader是对象file-loader的上层封装,使用时需配合file-loader使用。
      
    • 直接引入css文件(一起打包会报错,需要loader),

    • css in js

    • import '../css/test.css';
      
    • 配置loader

    • module: {
        rules: [
          {
            test: /.css$/,  //以.css结尾 . 转义等于.
            use: [
              'style-loader', //把加载到的css添加到style标签中
              'css-loader'   //只能加载css,不能添加到html中
            ]
          },
          {
            test: /.(png|jpg|gif)$/,
            use: [
              {
                loader: 'url-loader',
                options: {
                  limit: 8192     //小于8kb的图片以base64形式打包到JS内,可以减少请求次数
                }
              }
            ]
          }
        ]
      }
      
    • 打包图片出现的问题:

      • 大图无法打包到entry.js文件中,index.html不在生成资源目录下。
      • 页面加载图片会在所在目录位置查找,导致页面加载图片时候大图路径无法找到
      • 解决办法:
      • 使用publicPath : ‘dist/js/’ //设置为index.html提供资源的路径,设置完后找所有的资源都会去当前目录下找。
      • 将index.html放在dist/js/也可以解决。
  • 自动编译打包(指南→开发)

    • 利用webpack开发服务器工具: webpack-dev-server

    • 下载

      • npm install –save-dev webpack-dev-server
      • 下载完之后多出一个webpack-dev-server命令,也是内置一个微型服务器,自动读取webpack配置文件
      • 访问localhost:8080,可以看到当前的项目
      • webpack-dev-server默认服务于根目录下的index.html
      • 根目录以配置文件为基准,所以index.html直接放在配置文件旁边,可以不设置contenBase属性,且文件名固定的
      • 热加载会自动寻找打包后的大图片,热加载生成的文件都在微型服务器的内存里,与本地文件无关
      • webpack-dev-server—open
    • webpack.config.js配置

      devServer: {
        contentBase: './dist'
      },
      
    • package.json配置

      "start": "webpack-dev-server --open"
      
  • 使用webpack插件

    • 常用的插件

    • 使用html-webpack-plugin根据模板html生成引入script的页面

    • 使用clean-webpack-plugin清除dist文件夹

    • 下载

      npm install --save-dev  html-webpack-plugin clean-webpack-plugin
      
    • webpack配置

      const HtmlWebpackPlugin = require('html-webpack-plugin'); //自动生成html文件的插件
       const CleanWebpackPlugin = require('clean-webpack-plugin'); //清除之前打包的文件   
      plugins: [
        new HtmlWebpackPlugin({template: './index.html'}),
        new CleanWebpackPlugin(['dist']),
      ]
      
    • 创建页面: index.html

      <!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="UTF-8">
        <title>webpack test</title>
      </head>
      <body>
      <div id="app"></div>
      <!--打包文件将自动通过script标签注入到此处-->
      </body>
      </html>
      
  • 热加载和html的插件

    • 热加载找到index.html之后在自己的微型服务器内存里面操作,不会生产静态的文件
    • html插件则是以index.html为模板在指定目录生产所有的静态文件,css在js 里面打包,大的图片同一路径,小的图片也在js里面,然后生产的index.html直接引用同一路径的js
    • 热加载用webpack-dev-server开启
    • html插件和普通的打包编译压缩一样用webpack同一执行
    • 调试在webpack-dev-server进行,结束后用webpack生产所有静态文件

项目的打包与发布

  • 打包:npm run build

  • 发布 1: 使用静态服务器工具包

    npm install -g serve
    serve dist
    
  • 访问: http://localhost:5000

  • 用动态 web 服务器(tomcat)

  • 使用静态服务器 https://www.cnblogs.com/greenteaone/p/10083129.html