精华内容
下载资源
问答
  • Webpack Tapable原理详解

    2019-02-16 14:19:00
    Webpack 就像一条生产线, 要经过一系列的处理流程才能将源文件转换成输出结果。这条生产线上的每个流程都是单一的, 多个流程之间存在依赖关系。只能完成当前处理后才会转交到下一个流程。 插...

    directory

        - src
            - sim    ---- 简单的模拟实现
            - /.js$/ ---- 使用

    代码已上传github, 地址

    Detailed

    Webpack 就像一条生产线, 要经过一系列的处理流程才能将源文件转换成输出结果。这条生产线上的每个流程都是单一的, 多个流程之间存在依赖关系。只能完成当前处理后才会转交到下一个流程。

    插件就像一个插入到生产线中的一个功能, 它会在特定的时机对生产线上的资源进行处理。

    这条生产线很复杂, Webpack则是通过 tapable 核心库来组织这条生产线。

    Webpack 在运行中会通过 tapable 提供的钩子进行广播事件, 插件只需要监听它关心的事件,就可以加入到这条生产线中,去改变生产线的运作。使得 Webpack整体扩展性很好。

    Tapable Hook

    Tapable 提供同步(Sync)和异步(Async)钩子类。而异步又分为 异步串行异步并行钩子类。

    右键图片,在新标签中查看完整图片

    Tapable Hook Class

    逐个分析每个钩子类的使用及其原理

    同步钩子类

    • SyncHook
    • SyncBailHook
    • SyncWaterfallHook
    • SyncLoopHook

    同步钩子类通过实例的 tap 方法监听函数, 通过 call发布事件

    SyncHook

    同步串行不关心订阅函数执行后的返回值是什么。其原理是将监听(订阅)的函数存放到一个数组中, 发布时遍历数组中的监听函数并且将发布时的 arguments传递给监听函数

    class SyncHook {
        constructor(options) {
            this.options = options
            this.hooks = []  //存放监听函数的数组
        }
        tap(name, callback) {
            this.hooks.push(callback)
        }
        call(...args) {
            for (let i = 0; i < this.hooks.length; i++) {
                this.hooks[i](...args)
            }
        }
    }
    
    const synchook = new SyncHook('name')
    // 注册监听函数
    synchook.tap('name', (data) => {
        console.log('name', data)
    })
    synchook.tap('age', (data) => {
        console.log('age', data)
    })
    
    // 发布事件
    synchook.call('qiqingfu')

    打印结果:

    name qiqingfu
    age qiqingfu

    SyncBailHook

    同步串行, 但是如果监听函数的返回值不为 null, 就终止后续的监听函数执行

    class SyncBailHook {
            constructor(options) {
              this.options = options
              this.hooks = []
        }
        tap(name, callback) {
            this.hooks.push(callback)
        }
        call(...args) {
            let ret, i = 0
            do {
                // 将第一个函数的返回结果赋值给ret, 在while中如果结果为 true就继续执行do代码块
                ret = this.hooks[i++](...args)
            } while(!ret)
        }
    }
    
    const syncbailhook = new SyncBailHook('name')
    
    syncbailhook.tap('name', (data) => {
        console.log('name', data)
        return '我的返回值不为null'
    })
    syncbailhook.tap('age', (data) => {
        console.log('age', data)
    })
    
    syncbailhook.call('qiqingfu')

    执行结果

    name qiqingfu

    SyncWaterfallHook

    同步串行瀑布流, 瀑布流指的是第一个监听函数的返回值,做为第二个监听函数的参数。第二个函数的返回值作为第三个监听函数的参数,依次类推...

    class SyncWaterfallHook {
        constructor(options) {
              this.options = options
              this.hooks = []
        }
        tap(name, callback) {
            this.hooks.push(callback)
        }
        call(...args) {
            let [firstHook, ...otherHooks] = this.hooks
            /**
             * 通过解构赋值先取出第一个监听函数执行
             * 并且将第一个函数的执行结果传递给第二个, 第二个传递给第三个,迭代的过程 
             */
            let ret = firstHook(...args)
            otherHooks.reduce((f,n) => {
                return n(f)
            }, ret)
        }
    }
    
    const syncWaterfallHook = new SyncWaterfallHook('name')
    
    syncWaterfallHook.tap('name', data => {
        console.log('name', data)
        return 23
    })
    syncWaterfallHook.tap('age', data => {
        console.log('age', data)
    })
    
    syncWaterfallHook.call('qiqingfu')

    打印结果

    name qiqingfu
    age 23

    SyncLoopHook

    同步串行, 如果监听函数的返回值为 true, 则反复执行当前的监听函数,直到返回指为 undefind则继续执行下面的监听函数

    class SyncLoopHook {
      constructor(options) {
        this.options = options
        this.hooks = []
        }
        tap(name, callback) {
        this.hooks.push(callback)
        }
        call(...args) {
            for (let i = 0; i < this.hooks.length; i++) {
                let hook = this.hooks[i], ret
                do{
                    ret = hook(...args)
                }while(ret === true && ret !== undefined)
            }
        }
    }
    
    const syncLoopHook = new SyncLoopHook('name')
    
    let n1 = 0
    syncLoopHook.tap('name', data => {
        console.log('name', data)
        return n1 < 2 ? true : undefined
    })
    syncLoopHook.tap('end', data => {
        console.log('end', data)
    })
    
    syncLoopHook.call('qiqingfu')

    执行结果

    name qiqingfu
    name qiqingfu
    name qiqingfu  第三次打印的时候, n1的指为2, 返回值为 undefined则执行后面的监听函数
    end qiqingfu

    异步钩子

    • 异步并行 (Parallel)
      • AsyncParallelHook
      • AsyncParalleBailHook
    • 异步串行 (Series)
      • AsyncSeriesHook
      • AsyncSeriesBailHook
      • AsyncSeriesWaterfallHook

    凡有异步,必有回调

    同步钩子是通过 tap来监听函数的, call来发布的。

    异步钩子是通过 tapAsynctapPromise 来监听函数,通过 callAsyncpromise来发布订阅的。

    AsyncParallelHook

    异步并行, 监听的函数会一块执行, 哪个函数先执行完就先触发。不需要关心监听函数的返回值。

    class AsyncParallelHook {
        constructor(options) {
            this.options = options
            this.asyncHooks = []
        }
        // 订阅
        tapAsync(name, callback) {
            this.asyncHooks.push(callback)
        }
        // 发布
        callAsync(...args) {
            /**
             * callAsync(arg1, arg2,..., cb)
             * 发布的时候最后一个参数可以是回调函数
             * 订阅的每一个函数的最后一个参数也是一个回调函数,所有的订阅函数执行完
             * 且都调用了最后一个函数,才会执行cb 
             */
        const finalCallback = args.pop()
            let i = 0
            // 将这个作为最后一个参数传过去,使用的时候选择性调用
            const done = () => {
                ++i === this.asyncHooks.length && finalCallback()
            }
            this.asyncHooks.forEach(hook => {
                hook(...args, done)
            })
        }
    }
    
    const asyncParallelHook = new AsyncParallelHook('name')
    
    asyncParallelHook.tapAsync('name', (data, done) => {
        setTimeout(() => {
        console.log('name', data)
        done()
      }, 2000)
    })
    asyncParallelHook.tapAsync('age', (data, done) => {
        setTimeout(() => {
        console.log('age', data)
        done()
      }, 3000)
    })
    
    console.time('time')
    asyncParallelHook.callAsync('qiqingfu', () => {
      console.log('监听函数都调用了 done')
      console.timeEnd('time')
    })

    打印结果

    name qiqingfu
    age qiqingfu
    监听函数都调用了 done
    time: 3002.691ms

    AsyncParalleBailHook

    暂时不理解

    AsyncSeriesHook

    异步串行钩子类, 不关心 callback的参数。异步函数一个一个的执行,但是必须调用 done函数。

    class AsyncSeriesHook {
        constructor(options) {
            this.options = options
            this.asyncHooks = []
        }
        tapAsync(name, callback) {
            this.asyncHooks.push(callback)
        }
        callAsync(...args) {
            const finalCallback = args.pop()
            
            let i = 0
            const done = () => {
                let task = this.asyncHooks[i++]
                task ? task(...args, done) : finalCallback()
            }
            done()
        }
    }
    
    const asyncSeriesHook = new AsyncSeriesHook('name')
    
    asyncSeriesHook.tapAsync('name', (data, done) => {
        setTimeout(() => {
            console.log('name', data)
            done()
        }, 1000)
    })
    
    asyncSeriesHook.tapAsync('age', (data, done) => {
        setTimeout(() => {
            console.log('age', data)
            done()
        }, 2000)
    })
    
    console.time('time')
    asyncSeriesHook.callAsync('qiqingfu', () => {
        console.log('end')
        console.timeEnd('time')
    })

    执行结果

    name qiqingfu
    age qiqingfu
    end
    time: 3010.915ms

    AsyncSeriesBailHook

    同步串行钩子类, callback的参数如果不是 null, 后面所有的异步函数都不会执行,直接执行 callAsync方法的回调函数

    class AsyncSeriesBailHook {
        constructor(options) {
            this.options = options
            this.asyncHooks = []
        }
        tapAsync(name, callback) {
            this.asyncHooks.push(callback)
        }
        callAsync(...args) {
            const finalCallback = args.pop()
    
            let i = 0
            const done = data => {
          if (data) return finalCallback()
          let task = this.asyncHooks[i++]
          task ? task(...args, done) : finalCallback()
            }
            done()
        }
    }
    
    const asyncSeriesBailHook = new AsyncSeriesBailHook('name')
    
    asyncSeriesBailHook.tapAsync('1', (data, done) => {
        setTimeout(() => {
            console.log('1', data)
            done(null)
        }, 1000)
    })
    
    asyncSeriesBailHook.tapAsync('2', (data, done) => {
        setTimeout(() => {
            console.log('2', data)
            done(null)
        }, 2000)
    })
    
    console.time('times')
    asyncSeriesBailHook.callAsync('qiqingfu', () => {
        console.log('end')
        console.timeEnd('times')
    })

    打印结果

    1 qiqingfu
    2 qiqingfu
    end
    times: 3012.060ms

    AsyncSeriesWaterfallHook

    同步串行钩子类, 上一个监听函数 callback(err, data)的第二个参数, 可以作为下一个监听函数的参数

    class AsyncSeriesWaterfallHook {
        constructor(options) {
            this.options = options
            this.asyncHooks = []
        }
        tapAsync(name, callback) {
            this.asyncHooks.push(callback)
        }
        callAsync(...args) {
            const finalCallback = args.pop()
    
            let i = 0, once
            const done = (err, data) => {
                let task = this.asyncHooks[i++]
                if (!task) return finalCallback()
                if (!once) {
                    // 只执行一次
                    task(...args, done)
                    once = true
                } else {
                    task(data, done)
                }
            }
            done()
        }
    }
    
    const asyncSeriesWaterfallHook = new AsyncSeriesWaterfallHook('name')
    
    asyncSeriesWaterfallHook.tapAsync('1', (data, done) => {
        setTimeout(() => {
            console.log('1', data)
            done(null, '第一个callback传递的参数')
        }, 1000)
    })
    
    asyncSeriesWaterfallHook.tapAsync('2', (data, done) => {
        setTimeout(() => {
            console.log('2', data)
            done(null)
        }, 1000)
    })
    
    console.time('timer')
    asyncSeriesWaterfallHook.callAsync('qiqingfu', () => {
        console.log('end')
        console.timeEnd('timer')
    })

    打印结果

    1 qiqingfu
    2 第一个callback传递的参数
    end
    timer: 2015.445ms

    END

    如果理解有误, 麻烦纠正!

    参考文章

    webpack4.0源码分析之Tapable

    webpack 4.0 Tapable 类中的常用钩子函数源码分析

    转载于:https://www.cnblogs.com/qiqingfu/p/10387634.html

    展开全文
  • webpack基础详解

    2019-09-24 13:52:26
    webpack基础+webpack配置文件常用配置项介绍+webpack-dev-server 一.webpack基础  1.在项目中生成package.json:在项目根目录中输入npm init,根据提示输入相应信息。(也可以不生成package.json文件,但是...

    webpack基础+webpack配置文件常用配置项介绍+webpack-dev-server

     

    一.webpack基础

      1.在项目中生成package.json:在项目根目录中输入npm init,根据提示输入相应信息。(也可以不生成package.json文件,但是package.json是很有用的,所有建议生成)

      2.安装webpaack

        a.在全局中安装webpack:npm install webpack -g

        b.将webpack安装到项目并将webpack写入package.json的devDependencies中:进入项目根目录,然后在命令行中输入npm install webpack --save-dev。

      3.打包模块

      webpack <entry> <output>。<entry>用于指定打包的文件,<output>用于指定打包后的文件。如webpack app/index.js       build/build.js表示将app文件夹中的index.js打包到build文件夹中的build.js中。

    二.webpack配置文件常用配置项介绍

      1.webpack有一个默认的配置文件webpack.config.js,这个文件需要手动的创建,位于项目根目录中。可以为一个项目设置多个配置文件,已达到不同的配置文件完成不同的功能。怎么设置后面介绍。

      2.webpack的配置文件会暴露出一个对象,格式如下:

        module.exports = {
          //配置项
        }

      3.常用配置项将要说明

        entry:打包的入口文件,一个字符串或者一个对象
        output:配置打包的结果,一个对象
          fileName:定义输出文件名,一个字符串
          path:定义输出文件路径,一个字符串
        module:定义对模块的处理逻辑,一个对象
          loaders:定义一系列的加载器,一个数组
            [
              {
                test:正则表达式,用于匹配到的文件
                loader/loaders:字符串或者数组,处理匹配到的文件。如果只需要用到一个模块加载器则使用                 loader:string,如果要使用多个模块加载器,则使用loaders:array

                include:字符串或者数组,指包含的文件夹
                exclude:字符串或者数组,指排除的文件夹
              }
            ]
        resolve:影响对模块的解析,一个对象
          extensions:自动补全识别后缀,是一个数组

        plugins:定义插件,一个数组

      4.entry详细说明

        (1)当entry是一个字符串时,这个字符串表示需要打包的模块的路径,如果只有一个要打包的模块,可以使用这种
        形式

        (2)当entry是一个对象
          a.是数组时,当需要将多个模块打包成一个模块,可以使用这个方式。如果这些模块之间不存在依赖,数组中
          值的顺序没有要求,如果存在依赖,则要将依赖性最高的模块放在最后面。
          例如:entry:["./app/one.js",".app/two.js"]
          b.是键值对形式的对象是,当需要分别打包成多个模块时,可以使用这种方式,例如;
          entry:{
            module1:"./app/one.js",
            module2:["./app/two.js","./app/three.js"]
          }
        注:当entry是一个键值对形式的对象时,包名就是键名,output的filename不能是一个固定的值,因为每个包的
        名字不能一样

      5.output详细说明

        (1)output是一个对象

        (2)output.filename:指定输出文件名,一个字符串。当输出一个文件,output.filename为一个确定的字符串
          如:output:{
              filename:"build.js"
                }
          当输出多个文件,output.filename不能为一个确定的字符串。为了让每个文件有一个唯一的名字,需要用到下面
          的变量
          [name] is replaced by the name of the chunk.对应entry的键名

          [hash] is replaced by the hash of the compilation.

          [chunkhash] is replaced by the hash of the chunk.

          如:output:{

              path:'./build/',

              fialname:'[name].js'

            }
          (3)output.path:指定输出文件的路径,相对路径,一个字符串
          (4)output中还有其他的一些值,不在这里说明,可以在webpack的官方网站中获得更多的详细信息

      6.module.loaders详细说明

        (1)module是一个对象,定义对模块的处理逻辑
        (2)module.loaders是一个数组,定义一系列加载器,这个数组中的每一项都是一个对象
        (3)module.loaders:[
            {
              test:正则,用于匹配要处理的文件
              loader/loaders: 字符串或者数组, 如果只需要用到一个模块加载器 ,则使用loader:string,
              如果要使用多个模块加载器,则使用loaders:array
              include:字符串或者数组,指包含的文件夹
              exclude:字符串或者数组,指排除的文件夹
            }
          ]
        (4)module除了可以配置loaders以外还能配置其他的值,在webpack的官网中获得更多的信息

      7.resolve.extensions详细说明

        (1)resolve.extensions并不是必须配置的,当不配置时,会使用默认值
        ["", ".webpack.js", ".web.js", ".js"],当手动为resolve.extensions设置值,
        它的默认值会被覆盖
        (2)如果你想要每个模块都能够按照它们自己扩展名正确的被解析,要在数组中添加一个空字符串。
        (3)如果你想请求一个js文件但是在请求时不带扩展(如:require('somecode')),那么就需要
        将'.js'添加到数组中。其他文件一样
        (4)resolve还有其他的配置项,在webpack的官网获得更多信息

      8.补充

        (1)当设置了配置文件后,在命令行中输入webpack就可按照默认配置文件中的配置项打包模块了。

        (2)设置多个webpack配置文件。webpack默认的配置文件是webpack.config.js,当在命令行中输入webpack时默认找的是          webpack.config.js。通过在package.json的scripts中添加例如
        "start-html":"webpack --config webpack.html.config.js"
        在命令行中输入npm run start-html查找的就是webpack.html.config.js,通过这种方式可以实现不同的
        配置文件有不同的用处,这样就不用反复修改同一个配置文件

      9.下面是一个简单的配置文件

      

    复制代码
    module.exports = {
        entry:{
            one:"./app/one.js",
            two:"./app/two.js"
        },
        output:{
            path:"./build/",
            filename:"[name].js"
        },
        module:{
            loaders:[
                {
                    test:/.*\.css$/,
                    loaders:["style","css"],
                    exclude:'./node_modules/'
                }
            ]
        },
        resolve:{
            extensions:['','.css','.js','jsx']
        }
    };
    复制代码

     

     

     

    三.webpack-dev-server

      1.webpack-dev-server是一个轻量级的服务器,修改文件源码后,自动刷新页面将修改同步到页面上

      2.安装webpack-dev-server:
        全局安装:npm install webpack-dev-server -g
        在项目中安装并将依赖写在package.json文件中:npm install webpack-dev-server --save-dev

      3.使用命令webpack-dev-server --hot --inline完成自动刷新
      4.默认的端口号是8080,如果需要8080端口被占用,就需要改端口,webpack-dev-server --port 3000(将端口号改为3000)

      5.安装webpack-dev-server后就可以在命令行中输入webpack-dev-server开启服务,然后在浏览器地址栏中
      输入localhost:端口号,就可以在浏览器中打开项目根目录的index.html文件,如果项目根目录中没有index.html
      文件,就会在浏览器中列出项目根目录中的所有的文件夹。
      6.第五条只是启动服务并不能自动刷新,要自动刷新需要用到webpack-dev-server --hot --inline

      7.当使用webpack-dev-server --hot --inline命令时,在每次修改文件,是将文件打包
      保存在内存中并没有写在磁盘里(默认是根据webpack.config.js打包文件,通过--config xxxx.js修改),这种打包得到的文件
      和项目根目录中的index.html位于同一级(但是你看不到,因为
      它在内存中并没有在磁盘里)。使用webpack命令将打包后的文件保存在磁盘中
      例如在index.html文件中引入通过webpack-dev-server --hot --inline打包的build.js
        <script src="build.js"></script>
      在index.html文件中引入通过webpack命令打包的build.js
        <script src="./build/build.js"></script>

      8.每一次都敲一长串命令太麻烦,在项目根目录的package.json文件的scripts配置中添加配置,如
      "build":"webpack-dev-server --hot --inline",然后在命令行中输入 npm run build就能
      代替输入一长串命令(webpack-dev-server --hot --inline),运行webpack-dev-server --hot --inline默认是找        webpack.config.js,通过--config命令可以修改为另一个配置文件。例如:webpack-dev-server --hot --inline --config      'webpack.es6.config.js'

      9.配置根目录

        (1)当在命令行中输入webpack-dev-server --hot --inline,再在浏览器中输入localhost:端口号,浏览器会在项目的

         根目录中去查找内容,通过--content-base可以配置根目录。

        如webpack-dev-server --hot --inline --content-base './build/',在build文件夹中去加载index.html,如果没有

        index.html文件,将会在浏览器中显示所有build目录下的文件和文件夹

    四.例子

      我一个设置了两个webpack的配置文件分别是webpack.config.js和webpack.react.config.js。package.json文件中scripts对象的内容如下:

      "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1",
      "react": "webpack --config webpack.react.config.js",
      "build": "webpack-dev-server --hot --inline --content-base ./build/",
      "build-react": "webpack-dev-server --hot --inline --content-base ./react-build/ --config webpack.react.config.js"
        }

    转载于:https://www.cnblogs.com/Mickey697/p/10831557.html

    展开全文
  • 最近要做一个js解析markdown的项目,所以当然想到了ant design,不过ant design内部又使用了atool-build脚手架,所以决定好好研究一下。...1.atool-build的简单说明该脚手架只是对webpack进行了简单的封装。首先,w

    最近要做一个js解析markdown的项目,所以当然想到了ant design,不过ant design内部又使用了atool-build脚手架,所以决定好好研究一下。如果有不对的地方还烦请指正。不过个人建议阅读github版本,因为最近也在不断的学习这部分内容,可能会随时修改,所以可能存在没有及时同步的问题。

    1.说在前面的话

    atool-build本身是基于webpack1的,如果你使用的是webpack2,可以试试wcf。这是在atool-build基础上开发的,集成了webpack-dev-server(启动了webpack-dev-server打包), webpack watch(webpack自身的watch模式,监听文件变化重新打包), webpack(打包一次然后退出)三种打包方式。因为webpack2更新后我们下面描述的很多plugin都已经移除而内置了,同时很多配置项都已经失效了,以前出现的问题都已经解决了。所以我还是强烈建议更新到webpack2的。

    2.atool-build的简单说明

    废话不多说,请看下面内容:

    atool-build脚手架只是对webpack进行了简单的封装。

    首先,webpack/babel/TypeScript那些基本配置信息都有了默认信息,并内置了很多默认的loader来处理文件;然后,他是自己调用compiler.run方法开始编译的,并通过compiler.watch来监听文件的变化,生产build-bundle.json表示编译的信息;

    然后,里面通过一个hack来解决extract-webpack-text-plugin打印log的问题;Babel的缓存目录会使用操作系统默认的缓存目录来完成,使用os模块的tmpdir方法;其中devtool采用的是如下的方式加载:

    js
    webpack --devtool source-map

    (1)如果在 package.json 中 browser 没有设置,则设置 child_process, cluster, dgram, dns, fs, module, net, readline, repl, tls 为 empty!

    (2)对于自定义添加的 loader,plugin等依赖,需要在项目文件夹中npm install 这些依赖。但不需要再安装 webpack 依赖,因为可以通过 require(‘atool-build/lib/webpack’) 得到;

    3.atool-build官方配置项与内部处理

    下面是atool-build给出的那些可以允许配置信息:

    (1): –verbose:是否在shell中传入verbose参数(表示是否输出过程日志)

    //如果没有指定verbose
    if (!args.verbose) {
        compiler.plugin('done', (stats) => {
          stats.stats.forEach((stat) => {
            stat.compilation.children = stat.compilation.children.filter((child) => {
              return child.name !== 'extract-text-webpack-plugin';
            });
          });
        });
      }

    如果没有传入verbose,那么表示不允许输出日志。至于为什么是移除’extract-text-webpack-plugin’可以参见这个hack

    (2):–json 是否生成bundle.json文件

    if (args.json) {
          const filename = typeof args.json === 'boolean' ? 'build-bundle.json' : args.json;
          const jsonPath = join(fileOutputPath, filename);
          writeFileSync(jsonPath, JSON.stringify(stats.toJson()), 'utf-8');
          console.log(`Generate Json File: ${jsonPath}`);
        }

    表示是否在shell中配置了json参数,在doneHandle,也就是说每次修改都会调用这个方法,然后写一个默认为build-bundle.json文件:

    (3)-o, –output-path 指定构建后的输出路径。
    处理如下:

    //对应于webpack的output.path选项
      if (args.outputPath) {
        webpackConfig.output.path = args.outputPath;
      }

    (4)-w, –watch [delpay] 是否监控文件变化,默认为不监控。内部处理如下:

    if (args.watch) {
        compiler.watch(args.watch || 200, doneHandler);
        //启动compiler.watch监听文件变化
      } else {
        compiler.run(doneHandler);
      }

    也用于监控编译的过程

     if (args.watch) {
        webpackConfig.forEach(config => {
          config.plugins.push(
            new ProgressPlugin((percentage, msg) => {
              const stream = process.stderr;
              if (stream.isTTY && percentage < 0.71) {
                stream.cursorTo(0);
                stream.write(`��  ${chalk.magenta(msg)}`);
                stream.clearLine(1);
              } else if (percentage === 1) {
                console.log(chalk.green('\nwebpack: bundle build is now finished.'));
              }
            })
          );
        });
      }

    (5)–public-path

    具体可以查看该文档,内部处理如下:

    //对应于webpack的虚拟路径
      if (args.publicPath) {
        webpackConfig.output.publicPath = args.publicPath;
      }

    (6)–no-compress 不压缩代码。

     if (args.compress) {//配置为--no-compress表示不压缩
        webpackConfig.UglifyJsPluginConfig = {
          output: {
            ascii_only: true,
          },
          compress: {
            warnings: false,
          },
        };
        webpackConfig.plugins = [...webpackConfig.plugins,
          new webpack.optimize.UglifyJsPlugin(webpackConfig.UglifyJsPluginConfig),
          new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'),
          }),
        ];
      } else {
        //https://cnodejs.org/topic/5785b3ef3b501f7054982f69
        if (process.env.NODE_ENV) {
          webpackConfig.plugins = [...webpackConfig.plugins,
            new webpack.DefinePlugin({
              'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
            }),
          ];
        }
      }

    如果压缩代码,那么我们添加UglifyJsPlugin。

    (7)–config [userConfigFile] 指定用户配置文件。默认为根目录下的 webpack.config.js文件。这个配置文件不是必须的。处理方式如下:

    if (typeof args.config === 'function') {
        webpackConfig = args.config(webpackConfig) || webpackConfig;
      } else {
        webpackConfig = mergeCustomConfig(webpackConfig, resolve(args.cwd, args.config || 'webpack.config.js'));
      }

    也就说,如果config参数是一个函数,那么直接调用这个函数,否则获取路径并调用这个路径引入的文件的默认导出函数,传入参数为webpackConfig,下面是内置的mergeCustomConfig内部逻辑:

    export default function mergeCustomConfig(webpackConfig, customConfigPath) {
      if (!existsSync(customConfigPath)) {
        return webpackConfig;
      }
      const customConfig = require(customConfigPath);
      if (typeof customConfig === 'function') {
        return customConfig(webpackConfig, ...[...arguments].slice(2));
      }
      throw new Error(`Return of ${customConfigPath} must be a function.`);
    }

    注意,也就说如果我们传入的是config为file,那么这个config必须导出的是一个函数!但是在wcf中我们采用了webpack-merge来合并配置项,更加灵活多变

    (8)–devtool 生成 sourcemap 的方法,默认为空,这个参数和 webpack 的配置一致。表示sourceMap的等级。

    (9)–hash 使用hash模式的构建, 并生成映射表map.json。内部的处理如下:

    //如果指定了hash,那么我们的生成的文件名称为[name]-[chunkhash]这种类型
      if (args.hash) {
        const pkg = require(join(args.cwd, 'package.json'));
        webpackConfig.output.filename = webpackConfig.output.chunkFilename = '[name]-[chunkhash].js';
        webpackConfig.plugins = [...webpackConfig.plugins,
          require('map-json-webpack-plugin')({
            assetsPath: pkg.name,//项目名称,会放置在项目根路径
            cache,
          }),
        ];
      }

    也就是说如果指定了hash,那么我们必须修改输出的文件名,即webpackConfig.output.filename 和webpackConfig.output.chunkFilename并添加hash。而且这里使用的是chunkhash,同时这里使用了map-json-webpack-plugin这个插件生成map.json映射文件。

    4.atool-build中内置的那些插件

    (1)ProgressPlugin学习

     config.plugins.push(new ProgressPlugin(percentage,msg)=>{
        const stream=process.stderr;
        if(stream.isTTY&&percentage<0.71){
             stream.cursorTo(0);
            stream.write(`��  ${chalk.magenta(msg)}`);
            stream.clearLine(1);
        }else if(percentate==1){
         console.log(chalk.green('webpack: bundle build is now finished.'));
        }
      })
    

    该插件表示编译的进度。插件详见官方网站阅读

    (2)NoErrorsPlugin

    表示如果编译的时候有错误,那么我们跳过emit阶段,因此包含错误信息的资源都不会经过emit阶段也就是没有文件产生。这时候所有资源的emitted都是false。如果你使用CLI,那么当你使用这个插件的时候不会退出并产生一个error code,如果你想要CLI退出,那么使用bail选项。

    (3)DefinePlugin
    表示允许你定义全局变量,可以用于在编译阶段和开发阶段进行不同的处理。用法如下:

     new webpack.DefinePlugin({
              'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
            }),

    (4)DedupePlugin
    查找相等或者相似的文件并从输出去剔除。在watch模式下不要使用,要在production下使用。

    export default function mergeCustomConfig(webpackConfig, customConfigPath) {
      if (!existsSync(customConfigPath)) {
        return webpackConfig;
      }
      const customConfig = require(customConfigPath);
      //必须返回函数,也就是我们通过shell配置的args.config必须是返回一个函数!
      if (typeof customConfig === 'function') {
        return customConfig(webpackConfig, ...[...arguments].slice(2));
      }
      throw new Error(`Return of ${customConfigPath} must be a function.`);
    }

    5.atool-build源码分析

    export default function build(args, callback) {
      // Get config.
      let webpackConfig = getWebpackConfig(args, {});
      //这里是把shell传入的options和默认的option进行配置后得到的最终options
      webpackConfig = Array.isArray(webpackConfig) ? webpackConfig : [webpackConfig];
      let fileOutputPath;
      webpackConfig.forEach(config => {
        fileOutputPath = config.output.path;
      });
      //获取最终的config.output.path属性表示最终的输出路径
      if (args.watch) {
        webpackConfig.forEach(config => {
          config.plugins.push(
            new ProgressPlugin((percentage, msg) => {
              const stream = process.stderr;
              if (stream.isTTY && percentage < 0.71) {
                stream.cursorTo(0);
                stream.write(`��  ${chalk.magenta(msg)}`);
                stream.clearLine(1);
              } else if (percentage === 1) {
                console.log(chalk.green('\nwebpack: bundle build is now finished.'));
              }
            })
          );
        });
      }
     //如果配置了watch,表示要监听,我们加入ProgressPlugin
      function doneHandler(err, stats) {
        //shell中配置了json参数,那么在fileOutputPath = config.output.path;也就是config.output.path
        //中输出我们的json文件
        if (args.json) {
          const filename = typeof args.json === 'boolean' ? 'build-bundle.json' : args.json;
          const jsonPath = join(fileOutputPath, filename);
          writeFileSync(jsonPath, JSON.stringify(stats.toJson()), 'utf-8');
          console.log(`Generate Json File: ${jsonPath}`);
        }
        //如果出错,那么退出码是1
        const { errors } = stats.toJson();
        if (errors && errors.length) {
          process.on('exit', () => {
            process.exit(1);
          });
        }
        // if watch enabled only stats.hasErrors would log info
        // otherwise  would always log info
        if (!args.watch || stats.hasErrors()) {
          const buildInfo = stats.toString({
            colors: true,
            children: true,//添加子模块的信息,https://github.com/webpack/extract-text-webpack-plugin/issues/35
            chunks: !!args.verbose,
            modules: !!args.verbose,
            chunkModules: !!args.verbose,
            hash: !!args.verbose,//如果verbose为true表示有日志,那么我们会输出这部分内容
            version: !!args.verbose,
          });
          if (stats.hasErrors()) {
            console.error(buildInfo);
          } else {
            console.log(buildInfo);
          }
        }
        if (err) {
          process.on('exit', () => {
            process.exit(1);
          });
          console.error(err);
        }
    
        if (callback) {
          callback(err);
        }
      }
      // Run compiler.
      //webpack返回的是一个Compiler实例对象
      const compiler = webpack(webpackConfig);
      // Hack: remove extract-text-webpack-plugin log
      //verbose: 是否输出过程日志,这里是取消'extract-text-webpack-plugin'所有的日志信息
      if (!args.verbose) {
        compiler.plugin('done', (stats) => {
          stats.stats.forEach((stat) => {
            //compilation.children是他所有依赖的plugin信息
            stat.compilation.children = stat.compilation.children.filter((child) => {
              return child.name !== 'extract-text-webpack-plugin';
            });
          });
        });
      }
      //调用compiler对象的核心方法watch和run方法
      if (args.watch) {
        compiler.watch(args.watch || 200, doneHandler);
      } else {
        compiler.run(doneHandler);
      }
    }

    上面的代码是很容易看懂的,其实我们最重要的代码就是如下的内容:

    function doneHandler(err, stats) {
        if (args.json) {
          const filename = typeof args.json === 'boolean' ? 'build-bundle.json' : args.json;
          const jsonPath = join(fileOutputPath, filename);
          writeFileSync(jsonPath, JSON.stringify(stats.toJson()), 'utf-8');
          console.log(`Generate Json File: ${jsonPath}`);
        }
        //如果出错,那么退出码是1
        const { errors } = stats.toJson();
        if (errors && errors.length) {
          process.on('exit', () => {
            process.exit(1);
          });
        }
        // if watch enabled only stats.hasErrors would log info
        // otherwise  would always log info
        if (!args.watch || stats.hasErrors()) {
          const buildInfo = stats.toString({
            colors: true,
            children: true,//添加子模块的信息,https://github.com/webpack/extract-text-webpack-plugin/issues/35
            chunks: !!args.verbose,
            modules: !!args.verbose,
            chunkModules: !!args.verbose,
            hash: !!args.verbose,//如果verbose为true表示有日志,那么我们会输出这部分内容
            version: !!args.verbose,
          });
          if (stats.hasErrors()) {
            console.error(buildInfo);
          } else {
            console.log(buildInfo);
          }
        }
        if (err) {
          process.on('exit', () => {
            process.exit(1);
          });
          console.error(err);
        }
        if (callback) {
          callback(err);
        }
      }

    因为我们调用compiler.watch方法,在webpack中,其会调用Watching对象的watch方法监听文件的变化,每次变化的时候我们只是重新生成我们的’build-bundle.json’文件表示本次编译的信息!而且在webpack的watch的回调函数,也就是doneHandler中每次都会传入Stats对象,如果你还不知道可以查看下面这个文章

    6.TypeScript默认配置项

    export default function ts() {
      return {
        target: 'es6',
        jsx: 'preserve',
        moduleResolution: 'node',
        declaration: false,
        sourceMap: true,
      };
    }

    7.Babel配置项

    export default function babel() {
      return {
        cacheDirectory: tmpdir(),//临时文件存放位置
        presets: [//presets字段设定转码规则
          require.resolve('babel-preset-es2015-ie'),
          require.resolve('babel-preset-react'),
          require.resolve('babel-preset-stage-0'),
        ],
        plugins: [
          require.resolve('babel-plugin-add-module-exports'),
          require.resolve('babel-plugin-transform-decorators-legacy'),
        ],
      };
    }

    上面tmpdir的作用如下:

    The os.tmpdir() method returns a string specifying the operating system’s default directory for temporary files.

    8.Webpack默认配置项

    直接上源码部分,再分开分析下:

    export default function getWebpackCommonConfig(args) {
      const pkgPath = join(args.cwd, 'package.json');
      const pkg = existsSync(pkgPath) ? require(pkgPath) : {};
      const jsFileName = args.hash ? '[name]-[chunkhash].js' : '[name].js';
      const cssFileName = args.hash ? '[name]-[chunkhash].css' : '[name].css';
      const commonName = args.hash ? 'common-[chunkhash].js' : 'common.js';
      //如果传入hash,那么输出文件名要修改
      const babelQuery = getBabelCommonConfig();
      const tsQuery = getTSCommonConfig();
      //获取TypeScript配置
      tsQuery.declaration = false;
      let theme = {};
      if (pkg.theme && typeof(pkg.theme) === 'string') {
        let cfgPath = pkg.theme;
        // relative path
        if (cfgPath.charAt(0) === '.') {
          cfgPath = resolve(args.cwd, cfgPath);
        }
        const getThemeConfig = require(cfgPath);
        theme = getThemeConfig();
      } else if (pkg.theme && typeof(pkg.theme) === 'object') {
        theme = pkg.theme;
      }
      const emptyBuildins = [
        'child_process',
        'cluster',
        'dgram',
        'dns',
        'fs',
        'module',
        'net',
        'readline',
        'repl',
        'tls',
      ];
      const browser = pkg.browser || {};
      const node = emptyBuildins.reduce((obj, name) => {
        //如果browser里面没有这个模块,那么我们会把obj对象上这个模块的信息设置为'empty'字符串
        if (!(name in browser)) {
          return { ...obj, ...{ [name]: 'empty' } };
        }
        return obj;
      }, {});
      return {
        babel: babelQuery,
        ts: {
          transpileOnly: true,
          compilerOptions: tsQuery,
        },
        output: {
          path: join(process.cwd(), './dist/'),
          filename: jsFileName,
          chunkFilename: jsFileName,
        },
        devtool: args.devtool,//source-map
        resolve: {
          modulesDirectories: ['node_modules', join(__dirname, '../node_modules')],
          //本层级的node_modules和上一级node_modules
          extensions: ['', '.web.tsx', '.web.ts', '.web.jsx', '.web.js', '.ts', '.tsx', '.js', '.jsx', '.json'],
          //扩展名
        },
        resolveLoader: {
          modulesDirectories: ['node_modules', join(__dirname, '../node_modules')],
        },
        entry: pkg.entry,
        //package.json中配置的entry对象
        node,
        module: {
          noParse: [/moment.js/],//不解析moment.js
          loaders: [
            {
              test: /\.js$/,
              exclude: /node_modules/,
              loader: require.resolve('babel-loader'),
              query: babelQuery,
            },
            {
              test: /\.jsx$/,
              loader: require.resolve('babel-loader'),
              query: babelQuery,
            },
            {
              test: /\.tsx?$/,
              loaders: [require.resolve('babel-loader'), require.resolve('ts-loader')],
            },
            {
              test(filePath) {
                return /\.css$/.test(filePath) && !/\.module\.css$/.test(filePath);
              },
              loader: ExtractTextPlugin.extract(
                `${require.resolve('css-loader')}` +
                `?sourceMap&-restructuring&-autoprefixer!${require.resolve('postcss-loader')}`
              ),
            },
            {
              test: /\.module\.css$/,
              loader: ExtractTextPlugin.extract(
                `${require.resolve('css-loader')}` +
                `?sourceMap&-restructuring&modules&localIdentName=[local]___[hash:base64:5]&-autoprefixer!` +
                `${require.resolve('postcss-loader')}`
              ),
            },
            {
              test(filePath) {
                return /\.less$/.test(filePath) && !/\.module\.less$/.test(filePath);
              },
              loader: ExtractTextPlugin.extract(
                `${require.resolve('css-loader')}?sourceMap&-autoprefixer!` +
                `${require.resolve('postcss-loader')}!` +
                `${require.resolve('less-loader')}?{"sourceMap":true,"modifyVars":${JSON.stringify(theme)}}`
              ),
            },
            {
              test: /\.module\.less$/,
              loader: ExtractTextPlugin.extract(
                `${require.resolve('css-loader')}?` +
                `sourceMap&modules&localIdentName=[local]___[hash:base64:5]&-autoprefixer!` +
                `${require.resolve('postcss-loader')}!` +
                `${require.resolve('less-loader')}?` +
                `{"sourceMap":true,"modifyVars":${JSON.stringify(theme)}}`
              ),
            },
            {
              test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
              loader: `${require.resolve('url-loader')}?` +
              `limit=10000&minetype=application/font-woff`,
            },
            {
              test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
              loader: `${require.resolve('url-loader')}?` +
              `limit=10000&minetype=application/font-woff`,
            },
            {
              test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
              loader: `${require.resolve('url-loader')}?` +
              `limit=10000&minetype=application/octet-stream`,
            },
            { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: `${require.resolve('file-loader')}` },
            {
              test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
              loader: `${require.resolve('url-loader')}?` +
              `limit=10000&minetype=image/svg+xml`,
            },
            {
              test: /\.(png|jpg|jpeg|gif)(\?v=\d+\.\d+\.\d+)?$/i,
              loader: `${require.resolve('url-loader')}?limit=10000`,
            },
            { test: /\.json$/, loader: `${require.resolve('json-loader')}` },
            { test: /\.html?$/, loader: `${require.resolve('file-loader')}?name=[name].[ext]` },
          ],
        },
        postcss: [
          rucksack(),
          autoprefixer({
            browsers: ['last 2 versions', 'Firefox ESR', '> 1%', 'ie >= 8', 'iOS >= 8', 'Android >= 4'],
          }),
        ],
        plugins: [
          new webpack.optimize.CommonsChunkPlugin('common', commonName),
          //公共模块名字
          new ExtractTextPlugin(cssFileName, {
            disable: false,
            allChunks: true,
          }),
          //css文件名字
          new webpack.optimize.OccurenceOrderPlugin(),
          //顺序触发的插件
        ],
      };
    }

    我们是如下调用的:

      let webpackConfig = getWebpackCommonConfig(args);

    而我们的args表示从shell控制台传入的参数,这些参数会被原样传入到上面的getWebpackCommonConfig方法中。但是,我们依然要弄清楚下面的内容:

     let theme = {};
      if (pkg.theme && typeof(pkg.theme) === 'string') {
        let cfgPath = pkg.theme;
        // relative path
        if (cfgPath.charAt(0) === '.') {
          cfgPath = resolve(args.cwd, cfgPath);
        }
        const getThemeConfig = require(cfgPath);
        theme = getThemeConfig();
      } else if (pkg.theme && typeof(pkg.theme) === 'object') {
        theme = pkg.theme;
      }

    我们可以在package.json中配置theme选项,如果配置为对象,那么就是theme内容,否则如果是文件那么我们require进来,然后调用默认的方法!这也就是告诉我们,我们配置的这个文件名导出的内容必须是一个函数!那么这个theme有什么用呢?其实这是less为我们提供的覆盖less文件默认配置的变量的方法!我们在package.json中配置的theme会被传入到以下的插件中:
    ExtractTextPlugin

       {
              test(filePath) {
                return /\.less$/.test(filePath) && !/\.module\.less$/.test(filePath);
              },
              loader: ExtractTextPlugin.extract(
                `${require.resolve('css-loader')}?sourceMap&-autoprefixer!` +
                `${require.resolve('postcss-loader')}!` +
                `${require.resolve('less-loader')}?{"sourceMap":true,"modifyVars":${JSON.stringify(theme)}}`
              ),
            }

    首先:一种文件可以使用多个loader来完成;然后:我们可以使用?为不同的loader添加参数并且注意哪些参数是变量哪些参数是字符串!比如对于less-loader来说,我们使用了modifyVars来覆盖原来的样式,因为在loader里面会通过query读取查询字符串,然后做相应的覆盖(因为less里面使用了变量)。

    less.modifyVars({
      '@buttonFace': '#5B83AD',
      '@buttonText': '#D9EEF2'
    });

    详见链接:modifyVars

    9.webpack/TypeScript/Babel基本配置的含义

    为什么说getWebpackCommonConfig返回的是一个webpack的common配置信息,这些信息都是什么意思?为何说getBabelCommonConfig.js得到的是babel的基本配置,配置是什么意思?getTSCommonConfig得到的又是什么配置?这些内容不再一一赘述,读者可自行google.

    10.wcf vs atool-build

    最后打一个小广告说一下wcf与atool-build的区别,如果你有兴趣,也欢迎star,issue,贡献代码:

    (1)wcf集成了三种打包模式

    上面已经说过了,我们的wcf集成了三种打包模式,而且功能是逐渐增强的。webpack模式只是打包一次,然后退出,和webpack自己的打包方式是一样的。webpack watch模式会自动监听文件是否发生变化,然后重新打包。webpack-dev-server模式天然支持了HMR,支持无刷新更新数据。具体你可以阅读文档

    (2)很好的扩展性

    atool-build提供一个mergeCustomConfig函数来合并用户自定义的配置与默认的配置,并将用户配置作为参数传入函数进行修改,但是当要修改的配置项很多的时候就比较麻烦。wcf自己也集成了很多loader对文件进行处理,但是很容易进行拓展,只要你配置自己的扩展文件就可以了,内部操作都会自动完成。你可以通过两种方式来配置:

    cli模式:

    wcf --dev --devServer --config "Your custom webpack config file path"
    //此时会自动合并用户自定义的配置与默认配置,通过webpack-merge完成,而不用逐项修改

    Nodejs模式:

    const build = require("webpackcc/lib/build");
    const program = {
        onlyCf : true,
        //不启动打包,只是获取最终配置信息
        cwd : process.cwd(),
        dev : true,
        //开发模式,不启动如UglifyJs等
        config :"Your custom webpack config file path"
      };
    const finalConfig = build(program);
    //得到最终的配置,想干嘛干嘛

    通过nodejs模式,你可以获取webpack配置项用于其他地方。

    下面给出一个完整的例子(假如下面给出的是我们自定义的配置文件):

    module.exports = {
      entry:{
          'main': [
            'webpack-hot-middleware/client?path=http://' + host + ':' + port + '/__webpack_hmr',
            "bootstrap-webpack!./src/theme/bootstrap.config.js",
            './src/client.js'
          ]
      },
       output: {
          path: assetsPath,
          filename: '[name]-[hash].js',
          chunkFilename: '[name]-[chunkhash].js',
          publicPath: 'http://' + host + ':' + port + '/dist/'
        },
      plugins:[
        new webpack.DefinePlugin({
            __CLIENT__: true,
            __SERVER__: false,
            __DEVELOPMENT__: true,
            __DEVTOOLS__: true 
             // <-------- DISABLE redux-devtools HERE
          }),
         new webpack.IgnorePlugin(/webpack-stats\.json$/),
         webpackIsomorphicToolsPlugin.development()
      ],
       module:{
          rules:[
            { test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: "url-loader?limit=10000&mimetype=application/font-woff" },
            {
               test: webpackIsomorphicToolsPlugin.regular_expression('images'), 
               use: {
                 loader: require.resolve("url-loader"),
                 options:{}
               }
             },
               {
                test: /\.jsx$/,
                exclude :path.resolve("node_modules"),
                use: [{
                  loader:require.resolve('babel-loader'),
                  options:updateCombinedBabelConfig()
                }]
              },
                {
                test: /\.js$/,
                exclude :path.resolve("node_modules"),
                use: [{
                  loader:require.resolve('babel-loader'),
                  options:updateCombinedBabelConfig()
                }]
              }]
       }
    }

    注意:我们的wcf没有内置的entry,所以你这里配置的entry将会作为合并后的最终webpack配置的entry项。对于output来说,用户自定义的配置将会覆盖默认的配置(其他的也一样,除了module,plugins等)。对于plugin来说,我们会进行合并,此时不仅包含用户自定义的plugin,同时也包含内置的plugin。对于loader来说,如果有两个相同的loader,那么用户自定义的loader也会原样覆盖默认的loader。这样就很容易进行扩展。只要用户配置一个自定义配置文件的路径即可

    (3)dedupe

    atool-build并没有对我们的plugin和loader进行去重,这样可能导致同一个plugin被添加了两次,这就要求用户必须了解内置那些plugin,从而不去添加它。同时也会导致某一个js文件的loader也添加了两次,得到如下的内容:

     [ { test: { /\.jsx$/ [lastIndex]: 0 },
        exclude:
         { [Function: exclude]
           [length]: 1,
           [name]: 'exclude',
           [arguments]: null,
           [caller]: null,
           [prototype]: exclude { [constructor]: [Circular] } },
        use: [ { loader: 'babel-loader', options: {} }, [length]: 1 ] },
      { test: { /\.jsx$/ [lastIndex]: 0 },
      //对于jsx的loader又添加了一次
        exclude:
         { [Function: exclude]
           [length]: 1,
           [name]: 'exclude',
           [arguments]: null,
           [caller]: null,
           [prototype]: exclude { [constructor]: [Circular] } },
        use: [ { loader: 'after', options: {} }, [length]: 1 ] },
      [length]: 2 ]

    这个问题你可以查看我给webpack-merge提出的issue。但是这些工作wcf已经做了,所以当你有两个相同的插件,或者两个相同的loader的情况下,都只会留下一个,并且用户自定义的优先级要高于默认配置的优先级。

    (4)打包前进行钩子设置

    如果在打包前,或者获取到最终配置之前,你要对最终配置做一个处理,比如删除某个plugin/loader,那么我们提供了一个钩子函数:

    const program = {
        onlyCf : true,
        //此时不打包,只是为了获取最终配置用于nodejs
        cwd : process.cwd(),
        dev : true,
        //不启动压缩
        //下面这个hook用于去掉commonchunkplugin
        hook:function(webpackConfig){
             const commonchunkpluginIndex = webpackConfig.plugins.findIndex(plugin => {
               return plugin.constructor.name == "CommonsChunkPlugin"
             });
             webpackConfig.plugins.splice(commonchunkpluginIndex, 1);
             return webpackConfig;
        }
      };

    (5)其他功能

    参考这里

    11.关于webpack+babel打包的更多文章

    11.1 webpack相关

    webpack-dev-server原理分析

    webpack热加载HMR深入学习

    集成webpack,webpack-dev-server的打包工具

    prepack与webpack对比

    webpack插件书写你需要了解的知识点

    CommonsChunkPlugin深入分析

    CommonsChunkPlugin配置项深入分析

    webpack.DllPlugin提升打包性能

    webpack实现code splitting方式分析

    webpack中的externals vs libraryTarget vs library

    webpack的compiler与compilation对象

    webpack-dev-middleware原理分析

    11.2 Babel相关

    Babel编译class继承与源码打包结果分析

    使用babel操作AST来完成某种特效

    babylon你了解多少

    更加深入的问题,您可以继续阅读react+webpack+babel全家桶完整实例

    参考资料:

    atoolo-build官方文档

    webpack配置文档

    Babel入门教程

    展开全文
  • webpack使用详解

    2018-08-28 11:41:58
    Webpack 是当下最热门的前端资源模块化管理和打包工具。它可以将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源。还可以将按需加载的模块进行代码分隔,等到实际需要的时候再异步加载。通过loader的...

    Webpack 是当下最热门的前端资源模块化管理和打包工具。它可以将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源。还可以将按需加载的模块进行代码分隔,等到实际需要的时候再异步加载。通过loader的转换,任何形式的资源都可以视作模块,比如 CommonJs 模块、AMD 模块、ES6 模块、CSS、图片、JSON、Coffeescript、LESS 等。

    1、webpack简介

    前端是基于多语言、多层次的编码和组织工作,其次前端产品的交付是基于浏览器,这些资源是通过增量加载的方式运行到浏览器端,如何在开发环境组织好这些碎片化的代码和资源,并且保证他们在浏览器端快速、优雅的加载和更新,就需要一个模块化系统。

    webpack 的优势

    • 1、支持CommonJs和AMD模块,意思也就是我们基本可以无痛迁移旧项目。
    • 2、支持模块加载器和插件机制,可对模块灵活定制。babel-loader更是有效支持ES6。
    • 3、可以通过配置,打包成多个文件。有效利用浏览器的缓存功能提升性能。
    • 4、将样式文件和图片等静态资源也可视为模块进行打包。配合loader加载器,可以支持sass,less等CSS预处理器。
    • 5、内置有source map,即使打包在一起依旧方便调试。

    webpack 核心概念

    Webpack 具有四个核心的概念,想要入门 Webpack 就得先好好了解这四个核心概念。它们分别是Entry(入口)、Output(输出)、loader 和 Plugins(插件)。接下来详细介绍这四个核心概念。

    1. Entry

    Entry 是 Webpack 的入口起点指示,它指示 webpack 应该从哪个模块开始着手,来作为其构建内部依赖图的开始。可以在配置文件(webpack.config.js)中配置 entry 属性来指定一个或多个入口点,默认为./src( webpack 4开始引入默认值)。 具体配置方法:

    entry: string | Array<string>
    复制代码

    前者一个单独的 string 是配置单独的入口文件,配置为后者(一个数组)时,是多文件入口。

    //webpack.config.js
    module.exports = {
        entry: {
            app: './app.js',
            vendors: './vendors.js'
        }
    };
    复制代码

    以上配置表示从 app 和 vendors 属性开始打包构建依赖树,这样做的好处在于分离自己开发的业务逻辑代码和第三方库的源码,因为第三方库安装后,源码基本就不再变化,这样分开打包有利于提升打包速度,减少了打包文件的个数。

    2. Output

    Output 属性告诉webpack在哪里输出它所创建的 bundles,也可指定 bundles 的名称,默认位置为 ./dist。整个应用结构都会被编译到指定的输出文件夹中去,最基本的属性包括 filename(文件名)和 path(输出路径)。

    值得注意的是,即是你配置了多个入口文件,你也只能有一个输出点。

    具体配置方法:

    output: {
        filename: 'bundle.js',
        path: '/home/proj/public/dist'
    }
    复制代码

    值得注意的是,output.filename 必须是绝对路径,如果是一个相对路径,打包时 webpack 会抛出异常。

    多个入口时,使用下面的语法输出多个 bundle :

    // webpack.config.js
    module.exports = {
        entry: {
            app: './src/app.js',
            vendors: './src/vendors.js'
        },
        output: {
            filename: '[name].js',
            path: __dirname + '/dist'
        }
    }
    复制代码

    3. Loaders

    loader 可以理解为webpack的编译器,它使得webpack可以处理一些非 JavaScript 文件,比如 png、csv、xml、css、json 等各种类型的文件,使用合适的 loader 可以让 JavaScript 的 import 导入非 JavaScript 模块。JavaScript 只认为 JavaScript 文件是模块,而 webpack 的设计思想即万物皆模块,为了使得 webpack 能够认识其他“模块”,所以需要 loader 这个“编译器”。

    webpack 中配置 loader 有两个目标:

    (1)test 属性:标志有哪些后缀的文件应该被处理,是一个正则表达式。

    (2)use 属性:指定 test 类型的文件应该使用哪个 loader 进行预处理。

    比如webpack.config.js:

    module.exports = {
        entry: '...',
        output: '...',
        module: {
            rules: [
                {
                    test: /\.css$/,
                    use: 'css-loader'
                }
            ]
        }
    };
    复制代码

    该配置文件指示了所有的 css 文件在 import 时都应该经过 css-loader 处理,经过 css-loader 处理后,可以在 JavaScript 模块中直接使用 import 语句导入 css 模块。但是使用 css-loader 的前提是先使用 npm 安装 css-loader。

    此处需要注意的是定义 loaders 规则时,不是定义在对象的 rules 属性上,而是定义在 module 属性的 rules 属性中。

    配置多个 loader:

    有时候,导入一个模块可能要先使用多个 loader 进行预处理,这时就要对指定类型的文件配置多个 loader 进行预处理,配置多个 loader,把 use 属性赋值为数组即可,webpack 会按照数组中 loader 的先后顺序,使用对应的 loader 依次对模块文件进行预处理。

    {
        module: {
            rules: [
                {
                    test: /\.css$/,
                    use: [
                        {
                            loader: 'style-loader'
                        },
                        {
                            loader: 'css-loader'
                        }
                    ]
                }
            ]
        }
    }
    复制代码

    4. Plugins

    loader 用于转换非 JavaScript 类型的文件,而插件可以用于执行范围更广的任务,包括打包、优化、压缩、搭建服务器等等,功能十分强大。要是用一个插件,一般是先使用npm包管理器进行安装,然后在配置文件中引入,最后将其实例化后传递给 plugins 数组属性。

    插件是 webpack 的支柱功能,目前主要是解决 loader 无法实现的其他许多复杂功能,通过 plugins 属性使用插件:

    // webpack.config.js
    const webpack = require('webpack');
    module.exports = {
        plugins: [
            new webpack.optimize.UglifyJsPlugin()
        ]
    }
    复制代码

    5. Mode

    模式( Mode )可以通过配置对象的 mode 属性进行配置,主要值为 production 或者 development。两种模式的区别在于一个是为生产环境编译打包,一个是为了开发环境编译打包。生产环境模式下,webpack 会自动对代码进行压缩等优化,省去了配置的麻烦。

    学习完以上基本概念之后,基本也就入门 webpack 了,因为 webpack 的强大就是建立在这些基本概念之上,利用 webpack 多样的 loaders 和 plugins,可以实现强大的打包功能。

    2、js模块化

    2.1 命名空间

    命名空间是通过为项目或库创建一个全局对象,然后将所有功能添加到该全局变量中。通过减少程序中全局变量的数量,实现单全局变量,从而在具有大量函数、对象和其他变量的情况下不会造成全局污染,同时也避免了命名冲突等问题。

    然而,在不同的文件中给一个命名空间添加属性的时候,首先要保证这个命名空间是已经存在的,同时不对已有的命名空间造成任何破坏。可以通过非破坏性的命名空间函数实现:

    var KUI = KUI || {};
    KUI.utils = KUI.utils || {};
    
    KUI.utils.namespace = function(ns){
        var parts = ns.split("."),
            object = KUI,
            i, len;
    
        if(parts[0] === "KUI"){
            parts = parts.slice(1);
        }
    
        for(i = 0, len = parts.length; i < len; i+=1){
    
            if(!object[parts[i]]){
                object[parts[i]] = {};
            }
    
            object = object[parts[i]];
        }
    
        return object;
    };
    复制代码

    用法:

    KUI.utils.namespace("KUI.common");
    KUI.utils.namespace("KUI.common.testing");
    KUI.utils.namespace("KUI.modules.function.plugins");
    KUI.utils.namespace("format");
    复制代码

    看一下经过上述后 KUI 都有什么:

    {
        "utils": {},
        "common": {
            "testing": {}
        },
        "modules": {
            "function": {
                "plugins": {}
            }
        },
        "format": {}
    }
    复制代码

    命名空间模式的缺点

    1.需要输入更长的字符,并且需要更长的解析时间; 2.对单全局变量的依赖性,即任何代码都可以修改该全局实例,其他代码将获得修改后的实例。

    2.2 CommonJs

    CommonJS 是 nodejs 也就是服务器端广泛使用的模块化机制。 该规范的主要内容是,模块必须通过 module.exports 导出对外的变量或接口,通过 require() 来导入其他模块的输出到当前模块作用域中。

    根据这个规范,每个文件就是一个模块,有自己的作用域,文件中的变量、函数、类等都是对其他文件不可见的。

    如果想在多个文件分享变量,必须定义为 global 对象的属性。

    定义模块

    在每个模块内部,module 变量代表当前模块。它的 exports 属性是对外的接口,将模块的接口暴露出去。其他文件加载该模块,实际上就是读取 module.exports 变量。

    var x = 5;
    var addX = function (value) {
      return value + x;
    };
    module.exports.x = x;
    module.exports.addX = addX;
    复制代码

    加载模块

    require 方法用于加载模块,后缀名默认为.js

    var app = require('./app.js');
    复制代码

    模块加载的顺序,按照其在代码中出现的顺序

    根据参数的不同格式,require 命令去不同路径寻找模块文件。

    • 如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。
    • 如果参数字符串以“./”开头,则表示加载的是一个位于相对路径的模块文件
    • 如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块( node 核心模块,或者通过全局安装或局部安装在 node_modules 目录中的模块)

    入口文件

    一般都会有一个主文件(入口文件),在 index.html 中加载这个入口文件,然后在这个入口文件中加载其他文件。

    可以通过在 package.json 中配置 main 字段来指定入口文件。

    模块缓存

    第一次加载某个模块时,Node 会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的 module.exports 属性。

    加载机制

    CommonJS 模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。

    由于 CommonJS 是同步加载模块,这对于服务器端不是一个问题,因为所有的模块都放在本地硬盘。等待模块时间就是硬盘读取文件时间很小。但是,对于浏览器而言,它需要从服务器加载模块,涉及到网速,代理等原因,一旦等待时间过长,浏览器处于”假死”状态。

    2.3 AMD

    AMD 是 "Asynchronous Module Definition" 的缩写,即 “异步模块定义”。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。

    这里异步指的是不堵塞浏览器其他任务( dom 构建,css 渲染等),而加载内部是同步的(加载完模块后立即执行回调)。

    requirejs 即为遵循AMD规范的模块化工具。

    RequireJS 的基本思想是,通过 define 方法,将代码定义为模块;通过 require 方法,实现代码的模块加载。

    RequireJS 主要解决两个问题:

    • 多个 js 文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器。
    • js 加载的时候浏览器会停止页面渲染,加载文件越多,页面失去响应时间越长。

    定义模块

    RequireJS 定义了一个函数 define,它是全局变量,用来定义模块:

    define(id?, dependencies?, factory);
    复制代码

    参数说明:

    • id:指定义中模块的名字,可选;如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字。如果提供了该参数,模块名必须是“顶级”的和绝对的(不允许相对名字)。

    • 依赖 dependencies:是一个当前模块依赖的,已被模块定义的模块标识的数组字面量。 依赖参数是可选的,如果忽略此参数,它应该默认为["require", "exports", "module"]。然而,如果工厂方法的长度属性小于 3 ,加载器会选择以函数的长度属性指定的参数个数调用工厂方法。

    • 工厂方法 factory,模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值。

    define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {
          exports.verb = function() {
              return beta.verb();
              //Or:
              return require("beta").verb();
          }
      });
    复制代码

    加载模块

    AMD 也采用 require 命令加载模块,但是不同于 CommonJS ,它要求两个参数:

    require(['math'], function(math) {
      math.add(2, 3);
    })
    复制代码

    第一个参数是一个数组,里面的成员是要加载的模块,第二个参数是加载完成后的回调函数。

    配置

    require 方法本身也是一个对象,它带有一个 config 方法,用来配置 require.js 运行参数。

    require.config({
        paths: {
            "backbone": "vendor/backbone",
            "underscore": "vendor/underscore"
        },
        shim: {
            "backbone": {
                deps: [ "underscore" ],
                exports: "Backbone"
            },
            "underscore": {
                exports: "_"
            }
        }
    });
    复制代码

    paths:paths 参数指定各个模块的位置。这个位置可以是同一个服务器上的相对位置,也可以是外部网址。可以为每个模块定义多个位置,如果第一个位置加载失败,则加载第二个位置。上面就是指定了 jquery 的位置,那么就可以直接在文件中

    require(['jquery'],function($){})
    复制代码

    shim:有些库不是 AMD 兼容的,这时就需要指定 shim 属性的值。shim 可以理解成“垫片”,用来帮助require.js 加载非 AMD 规范的库。

    2.4 CMD

    CMD 即Common Module Definition 通用模块定义,CMD 规范是国内发展出来的,就像 AMD 有个requireJS,CMD 有个浏览器的实现 SeaJS,SeaJS 要解决的问题和 requireJS 一样,只不过在模块定义方式和模块加载(可以说运行、解析)时机上有所不同。

    在 CMD 规范中,一个模块就是一个文件。代码的书写格式如下:

    define(function(require, exports, module) {
    
      // 模块代码
    
    });
    复制代码

    require 是可以把其他模块导入进来的一个参数; 而 exports 是可以把模块内的一些属性和方法导出的; module 是一个对象,上面存储了与当前模块相关联的一些属性和方法。

    • AMD 是依赖关系前置,在定义模块的时候就要声明其依赖的模块;
    • CMD 是按需加载依赖就近,只有在用到某个模块的时候再去 require ;
    // CMD
    define(function(require, exports, module) {
      var a = require('./a')
      a.doSomething()
      // 此处略去 100 行
      var b = require('./b') // 依赖可以就近书写
      b.doSomething()
      // ... 
    })
    
    // AMD 默认推荐的是
    define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
      a.doSomething()
      // 此处略去 100 行
      b.doSomething()
      ...
    })
    
    复制代码

    2.5 ES6 Module

    ES6 正式提出了内置的模块化语法,我们在浏览器端无需额外引入 requirejs 来进行模块化。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

    ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过 import 命令输入。

    ES6 中的模块有以下特点:

    • 模块自动运行在严格模式下
    • 在模块的顶级作用域创建的变量,不会被自动添加到共享的全局作用域,它们只会在模块顶级作用域的内部存在;
    • 模块顶级作用域的 this 值为 undefined
    • 对于需要让模块外部代码访问的内容,模块必须导出它们

    定义模块

    使用 export 关键字将任意变量、函数或者类公开给其他模块。

    //导出变量
    export var color = "red";
    export let name = "cz";
    export const age = 25;
    
    //导出函数
    export function add(num1,num2){
        return num1+num2;
    }
    
    //导出类
    export class Rectangle {
        constructor(length, width) {
            this.length = length;
            this.width = width;
        }
    }
    
    function multiply(num1, num2) {
        return num1 * num2;
    }
    
    //导出对象,即导出引用
    export {multiply}
    复制代码

    重命名模块

    重命名想导出的变量、函数或类的名称

    function sum(num1, num2) {
        return num1 + num2;
    }
    
    export {sum as add}
    复制代码

    这里将本地的 sum 函数重命名为 add 导出,因此在使用此模块的时候必须使用 add 这个名称。

    导出默认值

    模块的默认值是使用 default 关键字所指定的单个变量、函数或类,而你在每个模块中只能设置一个默认导出。

    export default function(num1, num2) {
        return num1 + num2;
    }
    复制代码

    此模块将一个函数作为默认值进行了导出, default 关键字标明了这是一个默认导出。此函数并不需要有名称,因为它就代表这个模块自身。对比最前面使用 export 导出的函数,并不是匿名函数而是必须有一个名称用于加载模块的时候使用,但是默认导出则无需一个名字,因为模块名就代表了这个导出值。

    也可以使用重命名语法来导出默认值。

    function sum(num1, num2) {
        return num1 + num2;
    }
    
    export { sum as default };
    复制代码

    加载模块

    在模块中使用 import 关键字来导入其他模块。 import 语句有两个部分,一是需要导入的标识符,二是需导入的标识符的来源模块。此处是导入语句的基本形式:

    import { identifier1,identifier2 } from "./example.js"
    复制代码
    • 大括号中指定了从给定模块导入的标识符
    • from 指明了需要导入的模块。模块由一个表示模块路径的字符串来指定。

    当从模块导入了一个绑定时,你不能在当前文件中再定义另一个同名变量(包括导入另一个同名绑定),也不能在对应的 import 语句之前使用此标识符,更不能修改它的值。

    //导入单个绑定
    import {sum} from './example.js'
    
    //导入多个绑定
    import {sum,multiply} from './example.js'
    
    //完全导入一个模块
    import * as example from './example.js'
    example.sum(1,2);
    example.multiply(2,3);
    
    //重命名导入
    import { sum as add} from './example.js'
    
    //导入默认值
    import sum from "./example.js";
    复制代码

    然而要记住,无论你对同一个模块使用了多少次 import 语句,该模块都只会被执行一次。

    在导出模块的代码执行之后,已被实例化的模块就被保留在内存中,并随时都能被其他 import 所引用.

    import { sum } from "./example.js";
    import { multiply } from "./example.js";
    import { magicNumber } from "./example.js";
    复制代码

    尽管此处的模块使用了三个 import 语句,但 example.js 只会被执行一次。若同一个应用中的其他模块打算从 example.js 导入绑定,则那些模块都会使用这段代码中所用的同一个模块实例。

    限制

    export 与 import 都有一个重要的限制,那就是它们必须被用在其他语句或表达式的外部,而不能使用在if等代码块内部。原因之一是模块语法需要让 JS 能静态判断需要导出什么,正因为此,你只能在模块的顶级作用域使用 export 与 import。

    3、webpack使用

    3.1 打包js

    webpack 对各种模块化的支持

    // app.js
    // es module
    import sum from './sum'
    
    // commonjs
    var minus = require('./minux')
    
    //amd
    require(['muti'], function () {
        console.log(muti(2, 3))
    })
    
    console.log(sum(2, 3))
    console.log(minus(3, 2))
    复制代码
    // sum.js
    export default function () {
        return a + b
    }
    复制代码
    // minus.js
    module.exports = function (a, b) {
        a - b
    }
    复制代码
    // muti.js
    define(function() {
        'use strict';
        return function (a, b) {
            return a * b;
        }
    });
    复制代码

    压缩JS代码:

    现在你写的 JS 代码,在上线之前,都是需要进行压缩的,在没有 webpack 和 gulp 这些工具前,你可能需要找一个压缩软件或者在线进行压缩,在Webpack中可以很轻松的实现JS代码的压缩,它是通过插件的方式实现的,这里我们就先来引入一个 uglifyjs-webpack-plugin ( JS 压缩插件,简称 uglify)。

    注意:虽然 uglifyjs 是插件,但是webpack版本里默认已经集成,不需要再次安装。

    引入:

    我们需要在 webpack.config.js 中引入 uglifyjs-webpack-glugin 插件

    const uglify = require('uglifyjs-webpack-plugin');
    复制代码

    引入后在 plugins 配置里new一个 uglify 对象就可以了,代码如下。

    plugins:[
            new uglify()
        ],
    复制代码

    这时候在终端中使用 webpack 进行打包,你会发现 JS 代码已经被压缩了。

    3.2 编译ES6

    在前端开发中都开始使用ES6的语法了,虽然说 webpack3 增加了一些 ES6 的转换支持,但是实际效果不是很好。所以我在开发中还是喜欢添加 Babel-loader 的,我也查看了一些别人的 webpack 配置也都增加了 babel-loader,所以这节课我们学习一下如何增加 Babel 支持。

    Babel 是什么? Babel 其实是一个编译 JavaScript 的平台,它的强大之处表现在可以通过便宜帮你达到以下目的:

    • 使用下一代的 javaScript 代码( ES6, ES7….),即使这些标准目前并未被当前的浏览器完全支持。
    • 使用基于 JavaScript 进行了扩展的语言,比如 React 的 JSX。

    Babel的安装与配置

    Babel 其实是几个模块化的包,其核心功能位于称为 babel-core 的 npm 包中,webpack 可以把其不同的包整合在一起使用,对于每一个你需要的功能或拓展,你都需要安装单独的包(用得最多的是解析 ES6 的 babel-preset-es2015 包和解析 JSX 的 babel-preset-react 包)。

    安装依赖包

    npm install --save-dev babel-loader babel-core babel-preset-env
    复制代码

    在 webpack 中配置 Babel 的方法如下:

    {
        test:/\.(jsx|js)$/,
        use:{
            loader:'babel-loader',
            options:{
                presets:[
                    "es2015","react"
                ]
            }
        },
        exclude:/node_modules/
    }
    复制代码

    .babelrc配置

    虽然 Babel 可以直接在 webpack.config.js 中进行配置,但是考虑到 babel 具有非常多的配置选项,如果卸载 webapck.config.js 中会非常的雍长不可阅读,所以我们经常把配置卸载 .babelrc 文件里。

    在项目根目录新建 .babelrc 文件,并把配置写到文件里。

    . babelrc

    {
        "presets":["react","es2015"]
    }
    复制代码

    .webpack.config.js 里的 loader 配置

    {
        test:/\.(jsx|js)$/,
        use:{
            loader:'babel-loader',
        },
        exclude:/node_modules/
    }
    复制代码

    ENV:

    babel-preset-env 代替 babel-preset-ES2015 , babel 官方推出了 babel-preset-env ,并建议在使用的时候选择 env 代替之前的 ES20** 。env 为我们提供了更智能的编译选择。

    npm install --save-dev babel-preset-env
    复制代码

    然后修改 .babelrc 里的配置文件。其实只要把之前的 es2015 换成 env 就可以了。

    {
        "presets":["react","env"]
    }
    复制代码

    3.3 打包公共代码

    CommonsChunkPlugin 插件,是一个可选的用于建立一个独立文件 (又称作 chunk ) 的功能,这个文件包括多个入口 chunk 的公共模块。

    通过将公共模块拆出来,最终合成的文件能够在最开始的时候加载一次,便存到缓存中供后续使用。这个带来速度上的提升,因为浏览器会迅速将公共的代码从缓存中取出来,而不是每次访问一个新页面时,再去加载一个更大的文件。

    公共chunk 用于 入口chunk (entry chunk)

    生成一个额外的 chunk 包含入口 chunk 的公共模块。

    new webpack.optimize.CommonsChunkPlugin({
      name: "commons",
      // ( 公共chunk(commnons chunk) 的名称)
    
      filename: "commons.js",
      // ( 公共chunk 的文件名)
    
      // minChunks: 3,
      // (模块必须被3个 入口 chunk 共享)
    
      // chunks: ["pageA", "pageB"],
      // (只使用这些 入口chunk)
    })
    复制代码

    你必须在 入口 chunk 之前加载生成的这个公共 chunk:

    <script src="commons.js" charset="utf-8"></script>
    <script src="entry.bundle.js" charset="utf-8"></script>
    复制代码

    明确第三方库 chunk

    将你的代码拆分成公共代码和应用代码。

    entry: {
      vendor: ["jquery", "other-lib"],
      app: "./entry"
    },
    plugins: [
      new webpack.optimize.CommonsChunkPlugin({
        name: "vendor",
        // filename: "vendor.js"
        // (给 chunk 一个不同的名字)
    
        minChunks: Infinity,
        // (随着 entry chunk 越来越多,
        // 这个配置保证没其它的模块会打包进 vendor chunk)
      })
    ]
    复制代码

    将公共模块打包进父 chunk

    使用代码拆分功能,一个 chunk 的多个子 chunk 会有公共的依赖。为了防止重复,可以将这些公共模块移入父 chunk。这会减少总体的大小,但会对首次加载时间产生不良影响。如果预期到用户需要下载许多兄弟 chunks(例如,入口 trunk 的子 chunk),那这对改善加载时间将非常有用。

    new webpack.optimize.CommonsChunkPlugin({
      // names: ["app", "subPageA"]
      // (选择 chunks,或者忽略该项设置以选择全部 chunks)
    
      children: true,
      // (选择所有被选 chunks 的子 chunks)
    
      // minChunks: 3,
      // (在提取之前需要至少三个子 chunk 共享这个模块)
    })
    复制代码

    额外的异步公共 chunk

    与上面的类似,但是并非将公共模块移动到父 chunk(增加初始加载时间),而是使用新的异步加载的额外公共chunk。当下载额外的 chunk 时,它将自动并行下载。

    new webpack.optimize.CommonsChunkPlugin({
      name: "app",
      // or
      names: ["app", "subPageA"]
      // the name or list of names must match the name or names
      // of the entry points that create the async chunks
    
      children: true,
      // (选择所有被选 chunks 的子 chunks)
    
      async: true,
      // (创建一个异步 公共chunk)
    
      minChunks: 3,
      // (在提取之前需要至少三个子 chunk 共享这个模块)
    })
    复制代码

    3.4 代码分割和懒加载

    webpack 可以帮助我们将代码分成不同的逻辑块,在需要的时候加载这些代码。

    使用 require.ensure() 来拆分代码

    require.ensure() 是一种使用 CommonJS 的形式来异步加载模块的策略。在代码中通过 require.ensure([]) 引用模块,其使用方法如下:

    require.ensure(dependencies: String[], callback: function(require), chunkName: String);
    复制代码

    第一个参数指定依赖的模块,第二个参数是一个函数,在这个函数里面你可以使用 require 来加载其他的模块,webpack 会收集 ensure 中的依赖,将其打包在一个单独的文件中,在后续用到的时候使用 jsonp 异步地加载进去。

    //进行代码分割
    require.ensure(['lodash'],function(){
        var _ = require('lodash');//上边的require.ensure只会引入进来,但是并不会执行,再次require才会执行。
    },'vendor')
    复制代码

    或者

    if(page=='subPageA'){
        require.ensure(['./subPageA'],function(){
            var subPageA=require('subPageA');
        },'subPageA')
    }else if(page=='subPageB'){
        require.ensure(['./subPageB'],function(){
            var subPageA=require('subPageB');
        },subPageB)
    }
    复制代码

    或者

    require.ensure(['./subPageA','./subPageB'],function(){
            var subPageA=require('subPageA');
            var subPageB=require('subPageB');
        },common)
        //common表示这个模块的名字
    复制代码

    但是仅仅这样配置并不能把公共 js 抽离出来,在多页面应用中可以通过 new webpack.optimize.CommonsChunkPlugin 这个 plugin 来实现,但是对于单页面来说,就需要借助 require.include 了

    require.include('./moduleA')
    
    if(page=='subPageA'){
        require.ensure(['./subPageA'],function(){
            var subPageA=require('subPageA');
        },'subPageA')
    }else if(page=='subPageB'){
        require.ensure(['./subPageB'],function(){
            var subPageA=require('subPageB');
        },subPageB)
    }
    复制代码

    这样就会把公共模块 moduleA 给抽离出来。

    import

    import 与 require.ensure 最大的区别就是,他在引入的时候会直接执行,而不需要在此 require 了

    import('./subPageA').then(function(){
    
    })
    复制代码

    但是这样打包出来的是没有 chunkname 的,怎么添加 chunkname 呢?需要 webpack3+ 的魔法注释

    import(/*webpackChunkName:'subPageA'*/'./subPageA').then(function(){
    
    })
    复制代码

    3.5 处理css

    打包CSS

    首先,在 src 目录下建立 css 文件夹,和 index.css 文件,并编写如下代码:

    body{
        background: burlywood;
        color:white;
        font-size:30px;
    }
    复制代码

    建立好后,需要引入到入口文件中,才可以打包。在 entery.js 的首行加入代码:

    import css from './css/index.css';
    复制代码

    CSS 和引入做好后,我们就需要使用 loader 来解析 CSS 文件了,这里我们需要两个解析用的 loader,分别是 style-loader 和 css-loader。

    style-loader

    它是用来处理 css 文件中的 url() 等。 用 npm install 进行项目安装:

    npm install --save-dev style-loader
    复制代码

    CSS-loader

    它是用来将 css 插入到页面的 style 标签。 用 npm install 进行项目安装:

    npm install --save-dev css-loader
    复制代码

    loaders配置:

    修改 webpack.config.js 中 module 属性中的配置代码如下:

    webpack.config.js

    module:{
            rules: [
                {
                  test: /\.css$/,
                  use: [ 'style-loader', 'css-loader' ]
                }
              ]
        },
    复制代码

    提取 CSS

    目前,打包后的文件中,css 是打包在 js 代码里面的,这样不便于以后的维护,所以需要把 CSS 从 js 中分离出来,我们需要使用插件 Extract Text Plugin。

    安装:

    npm install --save-dev extract-text-webpack-plugin
    复制代码

    在 webpack.config.js 中引入

    const ExtractTextPlugin = require('extract-text-webpack-plugin');
    复制代码

    在 Plugins中配置:

    new ExtractTextPlugin('css/index.css');
    //css/index.css是分离后的路径位置
    复制代码

    修改 Loader 配置:

    module:{
        rules:[
            {
                test:/\.css$/,
                use:ExtractTextPlugin.extract({
                    fallback:"style-loader",
                    use:"css-loader"
                })
            }
        ]
    }
    复制代码

    配置Less

    Less 作为目前很火的 CSS 预处理语言,它扩展了 CSS 语言,增加了变量、Mixin 、函数等特性,使 CSS 更易维护和扩展;

    安装:

    npm install --save-dev less less-loader
    复制代码

    在 webpack.config.js 中配置 Loader:

    module:{
        rules:[
            {
                test:/\.less$/,
                use:ExtractTextPlugin.extract({
                    fallback:"style-loader",
                    use:[{
                        loader:"css-loader"
                    },{
                        loader:"less-loader"
                    }]
                })
            }
        ]
    }
    复制代码

    配置sass

    Sass 的打包和分离和 less 的类似,首先下载安装 Sass 所支持的服务与 loader。 安装:

    npm install --save-dev node-sass sass-loader
    复制代码

    在 webpack.config.js 中配置 Loader:

    module:{
        rules:[
            {
                test:/\.less$/,
                use:ExtractTextPlugin.extract({
                    fallback:"style-loader",
                    use:[{
                        loader:"css-loader"
                    },{
                        loader:"sass-loader"
                    }]
                })
            }
        ]
    }
    复制代码

    PostCSS-in-webpack

    CSS3 是目前作为一个前端必须要掌握的技能,但是由于现在好多浏览器还是不兼容 CSS3,所以前端需要多写很丑很难看的前缀代码;以前都是边查 Can I Use ,边添加,这样很麻烦,现在配置一个插件 postcss就可以搞定;

    PostCSS 是一个 CSS 的处理平台,它可以帮助你的 CSS 实现更多的功能,但是今天我们就通过其中的一个加前缀的功能,初步了解一下 PostCSS。

    安装:

    npm install --save-dev postcss-loader autoprefixer
    复制代码

    在根目录下,建立一个 postcss.config.js 文件:

    module.exports = {
        plugins:[
            require('autoprefixer')
        ]
    }
    复制代码

    这就是对 postCSS 一个简单的配置,引入了 autoprefixer 插件。让 postCSS 拥有添加前缀的能力,它会根据 can i use 来增加相应的css3属性前缀。

    在 webpack.config.js 中配置 Loader:

    {
        test: /\.css$/,
        use: extractTextPlugin.extract({
            fallback: 'style-loader',
            use: [
                { loader: 'css-loader', 
                    options: { importLoaders: 1 } 
                },
                'postcss-loader'
            ]
        })
    
    }
    复制代码

    3.6 Tree-shaking

    Tree-shaking 字面意思就是摇晃树, 其实就是去除那些引用的但却没有使用的代码。 Tree-shaking 概念最早由 Rollup.js 提出,后来在 webpack2 中被引入进来,但是这个这一特性能够被支持得益于 ES6 modules 的静态特性。ES6的模块声明相比于传统 CommonJS 的同步 require 有着本质区别。这种 modules 设计保证了依赖关系是提前确定的,使得静态分析成为了可能,与运行时无关。 并且 webpack 中并没有直接对 tree-shaking 的配置,需要借助 uglifyjs-webpack-plugin。

    webpack 中 tree-shaking主要分为两个方面:

    • JS tree shaking: JS 文件中定义的多个方法或者变量没有全部使用。
    • CSS tree shaking: 样式通过 css 选择器没有匹配到相应的 DOM 节点。

    JS Tree-shaking

    将文件标记为无副作用( side-effect-free ) 在一个纯粹的 ESM 模块世界中,识别出哪些文件有副作用很简单。然而,我们的项目无法达到这种纯度,所以,此时有必要向 webpack 的 compiler 提供提示哪些代码是“纯粹部分”。

    这种方式是通过 package.json 的 "sideEffects" 属性来实现的。

    {
      "name": "your-project",
      "sideEffects": false
    }
    复制代码

    如同上面提到的,如果所有代码都不包含副作用,我们就可以简单地将该属性标记为 false,来告知 webpack,它可以安全地删除未用到的 export 导出。

    「副作用」的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export 。举例说明,例如 polyfill ,它影响全局作用域,并且通常不提供 export 。

    如果你的代码确实有一些副作用,那么可以改为提供一个数组:

    {
      "name": "your-project",
      "sideEffects": [
        "./src/some-side-effectful-file.js"
      ]
    }
    复制代码

    压缩输出 通过如上方式,我们已经可以通过 import 和 export 语法,找出那些需要删除的“未使用代码(dead code)”,然而,我们不只是要找出,还需要在 bundle 中删除它们。为此,我们将使用 -p(production) 这个 webpack 编译标记,来启用 uglifyjs 压缩插件。

    注意,--optimize-minimize 标记也会在 webpack 内部调用 UglifyJsPlugin。 从 webpack 4 开始,也可以通过 "mode" 配置选项轻松切换到压缩输出,只需设置为 "production"。

    webpack.config.js

    const path = require('path');
    
    module.exports = {
      entry: './src/index.js',
      output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
      },
      mode: "production"
    };
    复制代码

    为了学会使用 tree shaking,你必须……

    • 使用 ES2015 模块语法(即 import 和 export)。
    • 在项目 package.json 文件中,添加一个 "sideEffects" 入口。
    • 引入一个能够删除未引用代码( dead code )的压缩工具( minifier )(例如 UglifyJSPlugin )。

    CSS Tree-shaking

    像 Bootstrap 这样的框架往往会带有很多 CSS。在项目中通常我们只使用它的一小部分。就算我们自己写CSS,随着项目的进展,CSS 也会越来越多,有时候需求更改,带来了 DOM 结构的更改,这时候我们可能无暇关注 CSS 样式,造成很多 CSS 的冗余。

    PurifyCSS 使用 PurifyCSS 可以大大减少 CSS 冗余,比如我们经常使用的 BootStrap (140KB)就可以减少到只有 35KB 大小。这在实际开发当中是非常有用的。

    安装 PurifyCSS-webpack 从名字你就可以看出这是一个插件,而不是 loader。所以这个需要安装还需要引入。 PurifyCSS-webpack 要以来于 purify-css 这个包,所以这两个都需要安装。

    npm i –save-dev purifycss-webpack purify-css
    复制代码

    引入 glob 因为我们需要同步检查html模板,所以我们需要引入 node 的 glob 对象使用。在 webpack.config.js 文件头部引入 glob。

    const glob = require('glob');
    复制代码

    引入 purifycss-webpack 同样在 webpack.config.js 文件头部引入 purifycss-webpack

    const PurifyCSSPlugin = require("purifycss-webpack");
    复制代码

    配置 plugins 引入完成后我们需要在 webpack.config.js 里配置 plugins 。代码如下,重点看标黄部分。

    plugins:[
        //new uglify() 
        new htmlPlugin({
            minify:{
                removeAttrubuteQuotes:true
            },
            hash:true,
            template:'./src/index.html'
            
        }),
        new extractTextPlugin("css/index.css"),
        new PurifyCSSPlugin({
            // Give paths to parse for rules. These should be absolute!
            paths: glob.sync(path.join(__dirname, 'src/*.html')),
            })
     
    ]
    复制代码

    这里配置了一个 paths ,主要是需找 html 模板,purifycss 根据这个配置会遍历你的文件,查找哪些css 被使用了。

    配置好上边的代码,我们可以故意在 css 文件里写一些用不到的属性,然后用 webpack 打包,你会发现没用的 CSS 已经自动给你删除掉了。在工作中记得一定要配置这个 plugins ,因为这决定你代码的质量,非常有用。

    3.7 文件处理

    图片处理

    在 index.html 文件中增加一个放置 div 的标签

    <div id="tupian"></div>
    复制代码

    编写 css 文件,把图片作为背景显示。

    #tupian{
       background-image: url(../images/manhua.png);
       width:466px;
       height:453px;
    }
    复制代码

    安装 file-loader 和 url-loader

    npm install --save-dev file-loader url-loader
    复制代码

    file-loader :解决引用路径的问题,拿 background 样式用 url 引入背景图来说,我们都知道, webpack 最终会将各个模块打包成一个文件,因此我们样式中的 url 路径是相对入口 html 页面的,而不是相对于原始 css 文件所在的路径的。这就会导致图片引入失败。这个问题是用 file-loader 解决的,file-loader 可以解析项目中的 url 引入(不仅限于 css),根据我们的配置,将图片拷贝到相应的路径,再根据我们的配置,修改打包后文件引用路径,使之指向正确的文件。 url-loader:如果图片较多,会发很多 http 请求,会降低页面性能。这个问题可以通过 url-loader 解决。url-loader 会将引入的图片编码,生成 dataURl 。相当于把图片数据翻译成一串字符。再把这串字符打包到文件中,最终只需要引入这个文件就能访问图片了。当然,如果图片较大,编码会消耗性能。因此url-loader 提供了一个 limit 参数,小于 limit 字节的文件会被转为 DataURl ,大于 limit 的还会使用 file-loader 进行 copy。

    配置 url-loader 我们安装好后,就可以使用这个 loader 了,记得在 loader 使用时不需要用 require 引入,在plugins 才需要使用 require 引入。

    webpack.config.js文件

    //模块:例如解读 CSS,图片如何转换,压缩
        module:{
            rules: [
                {
                  test: /\.css$/,
                  use: [ 'style-loader', 'css-loader' ]
                },{
                   test:/\.(png|jpg|gif)/ ,
                   use:[{
                       loader:'url-loader',
                       options:{
                           limit:500000
                       }
                   }]
                }
              ]
        },
    复制代码
    • test:/.(png|jpg|gif)/ 是匹配图片文件后缀名称。
    • use:是指定使用的 loader 和 loader 的配置参数。
    • limit:是把小于 500000B 的文件打成 Base64 的格式,写入JS 。
    • 写好后就可以使用 webpack 进行打包了,这回你会发现打包很顺利的完成了。具体的 Base64 的格式,你可以查看视频中的样子。

    为什么只使用了url-loader

    有的小伙伴会发现我们并没有在 webpack.config.js 中使用 file-loader ,但是依然打包成功了。我们需要了解 file-loader 和 url-loader 的关系。url-loader 和 file-loader 是什么关系呢?简答地说,url-loader 封装了 file-loader 。 url-loader 不依赖于 file-loader ,即使用 url-loader 时,只需要安装 url-loader 即可,不需要安装 file-loader ,因为 url-loader内置了 file-loader 。通过上面的介绍,我们可以看到,url-loader 工作分两种情况:

    • 1.文件大小小于 limit 参数, url-loader 将会把文件转为 DataURL( Base64格式 );

    • 2.文件大小大于 limit , url-loader 会调用 file-loader 进行处理,参数也会直接传给 file-loader。

    也就是说,其实我们只安装一个 url-loader 就可以了。但是为了以后的操作方便,我们这里就顺便安装上 file-loader。

    如何把图片放到指定的文件夹下

    前边两节课程,打包后的图片并没有放到images文件夹下,要放到 images 文件夹下,其实只需要配置我们的 url-loader 选项就可以了。

       module:{
            rules: [
                {
                  test: /\.css$/,
                  use: extractTextPlugin.extract({
                    fallback: "style-loader",
                    use: "css-loader"
                  })
                },{
                   test:/\.(png|jpg|gif)/ ,
                   use:[{
                       loader:'url-loader',
                       options:{
                           limit:5000,
                           outputPath:'images/',
                       }
                   }]
                }
              ]
        },
    复制代码

    CSS分离时图片路径处理

    在处理 css 时我们已经学会如何使用 extract-text-webpack-plugin 插件提取 css,利用 extract-text-webpack-plugin 插件很轻松的就把 CSS 文件分离了出来,但是 CSS 路径并不正确,很多小伙伴就在这里搞个几天还是没有头绪,网上也给出了很多的解决方案,我觉的最好的解决方案是使用publicPath 解决,我也一直在用。

    publicPath:是在 webpack.config.js 文件的 output 选项中,主要作用就是处理静态文件路径的。

    在处理前,我们在 webpack.config.js 上方声明一个对象,叫 website。

    var website ={
        publicPath:"http://192.168.1.108:1717/"
    }
    复制代码

    注意,这里的 IP 和端口,是你本机的 ip 或者是你 devServer 配置的 IP 和端口。 然后在 output 选项中引用这个对象的 publicPath 属性。

    //出口文件的配置项
        output:{
            //输出的路径,用了Node语法
            path:path.resolve(__dirname,'dist'),
            //输出的文件名称
            filename:'[name].js',
            publicPath:website.publicPath
        },
    复制代码

    配置完成后,你再使用 webpack 命令进行打包,你会发现原来的相对路径改为了绝对路径,这样来讲速度更快。

    处理字体文件

    将字体图标和 css 打包到同一个文件中

    {
       test:/\.(png|woff|woff2|svg|ttf|eot)$/,
       use:{
            loader:'url-loader',
            options: {
                limit: 100000,  //这里要足够大这样所有的字体图标都会打包到css中
            }
    }
    复制代码

    上文中的 limit 一定要保证大于最大字体文件的大小,因为这个参数是告诉 url-loader,如果文件小于这个参数,那么就以 Data Url 的方式直接构建到文件中。使用这种方式最方便,不用打包后路径的问题,但是缺点就是构建出来的文件特别大,如果线上不要使用这种方式打包。

    将字体图标独放打包到一个文件夹中

    {
       test: /\.(woff|woff2|svg|ttf|eot)$/,
       use:[
            {
    	        loader:'file-loader',
    	        options:{name:'fonts/[name].[hash:8].[ext]'}}
    	        //项目设置打包到dist下的fonts文件夹下
         ]
     }
    复制代码

    打包中会遇到的问题就是路径不对,可以通过配置 publicPath 解决。

    Json配置文件使用

    在实际工作中,我们的项目都会配置一个 Json 的文件或者说 API 文件,作为项目的配置文件。有时候你也会从后台读取到一个 json 的文件,这节课就学习如何在 webpack 环境中使用 Json。如果你会 webpack1 或者 webpack2 版本中,你是需要加载一个 json-loader 的 loader 进来的,但是在webpack3.x 版本中,你不再需要另外引入了。

    读出 Json 内容 第一步:现在我们的 index.html 模板中加入一个层,并给层一个 Id,为了是在 javascript 代码中可以方便引用。

    <div id="json"></div>
    复制代码

    第二步:到 src 文件夹下,找到入口文件,我这里是 entry.js 文件。修改里边的代码,如下:

    var json =require('../config.json');
    document.getElementById("json").innerHTML= json.name;
    复制代码

    这两行代码非常简单,第一行是引入我们的 json 文件,第二行驶写入到到 DOM 中。

    3.8 html in webpack

    生成html

    html-webpack-plugin 可以根据你设置的模板,在每次运行后生成对应的模板文件,同时所依赖的 CSS/JS 也都会被引入,如果 CSS/JS 中含有 hash 值,则 html-webpack-plugin 生成的模板文件也会引入正确版本的 CSS/JS 文件。

    安装

    npm install html-webpack-plugin --save-dev
    复制代码

    引入

    在webpack.config.js中引入:

    const HtmlWebpackPlugin = require('html-webpack-plugin');
    复制代码

    配置

    module.exports = {
        entry: './app/index.js',
        output: {
            ...
        },
        module: {
            ...
        },
        plugins: [
            new HtmlWebpackPlugin({
                title: "This is the result",
                filename: "./index.html",
                template: "./app/index.html",
                inject: "body",
                favicon: "",
                minify: {
                    caseSensitive: false,
                    collapseBooleanAttributes: true,
                    collapseWhitespace: true
                },
                hash: true,
                cache: true,
                chunks: ""
            })
        ]
    };
    复制代码

    然后看一下这些参数的意义:

    • title: 生成的HTML模板的 title,如果模板中有设置 title 的名字,则会忽略这里的设置
    • filename: 生成的模板文件的名字
    • template: 模板来源文件
    • inject: 引入模块的注入位置;取值有 true/false/body/head
    • favicon: 指定页面图标;
    • minify: 是 html-webpack-plugin 中集成的 html-minifier ,生成模板文件压缩配置
    • caseSensitive: false, //是否大小写敏感
    • collapseBooleanAttributes: true, //是否简写 boolean 格式的属性如:disabled="disabled" 简写为disabled
    • collapseWhitespace: true //是否去除空格
    • hash: 是否生成hash添加在引入文件地址的末尾,类似于我们常用的时间戳
    • cache: 是否需要缓存,如果填写 true,则文件只有在改变时才会重新生成
    • chunks: 引入的模块,这里指定的是 entry 中设置多个 js 时,在这里指定引入的 js,如果不设置则默认全部引入

    html中引入图片

    html-withimg-loader html-withimg-loader 就是我们今天的重点了,这个插件并不是很火,也是我个人喜欢的一个小loader 。解决的问题就是在hmtl文件中引入 标签的问题。

    安装:

    npm install html-withimg-loader --save
    复制代码

    配置 loader webpack.config.js

    {
        test: /\.(htm|html)$/i,
         use:[ 'html-withimg-loader'] 
    }
    复制代码

    然后在终端中可以进行打包了。你会发现 images 被很好的打包了。并且路径也完全正确。

    webpack环境配置

    搭建开发环境

    在使用 webpack-cli 进行打包时,通过命令 webpack --watch即可开启 watch 模式,进入 watch 模式之后,一旦依赖树中的某一个模块发生了变化,webpack 就会重新进行编译。

    clean-webpack-plugin

    在 webpack 中打包生成的文件会覆盖之前的文件,不过生成文件的时候文件名加了 hash 之后会每次都生成不一样的文件,这就会很麻烦,不但会生成很多冗余的文件,还很难搞清楚到底是哪个文件,这就需要引入该插件

    npm install –save-dev clean-webpack-plugin
    复制代码
    //webpack.config.js
    //引入clean-webpack-plugin
    const CleanWebpackPlugin = require('clean-webpack-plugin');
    
    //plugin 插入你想删除的路径,注意在生成出来文件之前,他会删除 public 的文件夹,而不是根据生成的文件来删除对应的文件。
    new CleanWebpackPlugin(['public']);
    复制代码

    webpack dev server

    webpack-dev-server 简介:

    • 是一个小型 node.js express 服务器
    • 新建一个开发服务器,可以 serve 我们 pack 以后的代码,并且当代码更新的时候自动刷新浏览器
    • 启动 webpack-dev-server 后,你在目标文件夹中是看不到编译后的文件的,实时编译后的文件都保存到了内存当中。 两种自动刷新方式:
    • iframe mode 在网页中嵌入了一个 iframe ,将我们自己的应用注入到这个 iframe 当中去,因此每次你修改的文件后,都是这个 iframe 进行了 reload 命令行:webpack-dev-server,无需 --inline 浏览器访问:http://localhost:8080/webpack-dev-server/index.html
    • inline mode 命令行:webpack-dev-server --inline 浏览器访问:http://localhost:8080

    安装webpack-dev-server

    npm install webpack-dev-server --save-dev
    复制代码

    在 webpack.config.js 中添加配置

    var webpack=require('webpack');
    module.exports = {
    ……
    devServer: {
        historyApiFallback: true,
        inline: true,//注意:不写hot: true,否则浏览器无法自动更新;也不要写  colors:true,progress:true等,webpack2.x已不支持这些
    },
    plugins:[
        ……
        new webpack.HotModuleReplacementPlugin()
     ]
        ……
    };
    复制代码

    在 package.json 里配置运行的命令

    "scripts": 
    { 
    &emsp;&emsp;"start": "webpack-dev-server --inline"
    },
    复制代码

    代理远程接口

    如果你有单独的后端开发服务器 API,并且希望在同域名下发送 API 请求 ,那么代理某些 URL 会很有用。 webpack-dev-server 使用了非常强大的 http-proxy-middleware 包。

    配置如下:

    proxy: {
        '/apis': {
            target: '', //要代理到的地址
            secure: false, //若地址为https,需要设置为false
            onProxyReq: function(proxyReq, req, res) { //提前设置一些代理的头部,如token信息等
    
            },
            //...其他配置请自行查阅文档http-proxy-middleware文档
        }
    }
    复制代码

    模块热更新

    DevServer 还支持一 种叫做模块热替换( Hot Module Replacement )的技术可在不刷新整个网页的情况下 做到超 灵敏实时预览。原理是在一个源码发生变化时,只需重新编译发生变化的模块,再用新输 出 的模块替换掉浏览器中对应的老模块 。

    模块热替换技术的优势如下:

    • 实时预览反应更快,等待时间更短。
    • 不刷新浏览器时能保留当前网页的运行状态,例如在使用 Redux 管理数据的应用中搭配模块热替换能做到在代码更新时 Redux 中的数据保持不变。

    总的来说,模块热替换技术在很大程度上提升了开发效率和体验 。

    DevServer 默认不会开启模块热替换模式,要开启该模式,则只 需在启动时带上参数 --hot ,完整的命令是 webpack-dev-server --hot。

    除了通过在启动时带上 --hot 参数,还可以通过接入 Plugin 实现,相关代码如下 :

    canst HotModuleReplacementPlugin = require (’ webpack/lib/HotModuleReplacementPlugin ’);
    module.exports = { 
    	entry:{
    		//为每个入口都注入代理客户端
    		main: [’ webpack-dev-server/client?http://localhost:8080 /’, ’webpack/hot/dev-server ’,’. / src/main.j s ’],
    	},
    	 plugIns : [
    		//该插件的作用就是实现模块热替换,实际上若启动时带上 、 --hot 、参数,就会注入该插件,生 成 .hot-update.json 文件。
    		new HotModuleReplacementPlugin() ,
    	],
    	devServer : {
    		//告诉 DevServer 要开启 模块热替换模式 
    		hot: true ,
    	},
    };	
    
    复制代码

    借助于 style-loader 的帮助,CSS 的模块热替换实际上是相当简单的。当更新 CSS 依赖模块时,此 loader 在后台使用 module.hot.accept 来修补(patch) <style>标签。

    但当修改 js 文件时,我们会发现模块热替换没有生效,而是整个页面被刷新了,为了让使用者在使用模块热替换功能时能灵活地控制老模块被替换时的逻辑,webpack 允许在源码中定义一些代码去做相应的处理。

    // 只有当开启了模块热替换时 module.hot 才存在 
    if (module.hot) {
        module.hot.accept(['.IAppComponent'],()=>{
    		//在新的 AppComponent 加载成功后重新执行组建渲染逻辑 		render(<AppComponentl>, window.document.getElementByid ('app'));
    	}) ;
    }
    复制代码

    其中的 module.hot 是当开启模块热替换后注入全局的 API,用于控制模块热替换的逻辑 。 当子模块发生更新时,更新事件会一层层地向上传递,也就是从 AppComponent.js 文件传递到 main.js 文件,直到有某层的文件接收了当前变化的模块,即 main.js 文 件中定义的 module.hot.accept(['.IAppComponent'], callback),这时就会调用 callback 函数去执行自定义逻辑。 如果事件一直往上抛,到最外层都没有文件接收它,则会直接刷新网页。

    开启调试SourceMap

    作为一个程序员每天的大部分工作就是调试自己写的程序,那我们使用了webpack后,所以代码都打包到了一起,给调试带来了麻烦,但是webpack已经为我们充分考虑好了这点,它支持生产 Source Maps 来方便我们的调试。 在使用 webpack 时只要通过简单的 devtool 配置,webapck 就会自动给我们生产 source maps 文件,map 文件是一种对应编译文件和源文件的方法,让我们调试起来更简单。

    在配置 devtool 时,webpack 给我们提供了四种选项:

    • source-map: 在一个单独文件中产生一个完整且功能完全的文件。这个文件具有最好的 source map,但是它会减慢打包速度;
    • cheap-module-source-map: 在一个单独的文件中产生一个不带列映射的map,不带列映射提高了打包速度,但是也使得浏览器开发者工具只能对应到具体的行,不能对应到具体的列(符号),会对调试造成不便。
    • eval-source-map: 使用 eval 打包源文件模块,在同一个文件中生产干净的完整版的 sourcemap ,但是对打包后输出的JS文件的执行具有性能和安全的隐患。在开发阶段这是一个非常好的选项,在生产阶段则一定要不开启这个选项。
    • cheap-module-eval-source-map: 这是在打包文件时最快的生产 source map 的方法,生产的 Source map 会和打包后的 JavaScript 文件同行显示,没有影射列,和 eval-source-map 选项具有相似的缺点。 四种打包模式,有上到下打包速度越来越快,不过同时也具有越来越多的负面作用,较快的打包速度的后果就是对执行和调试有一定的影响。

    个人意见是,如果大型项目可以使用 source-map,如果是中小型项目使用 eval-source-map 就完全可以应对,需要强调说明的是,source map 只适用于开发阶段,上线前记得修改这些调试设置。

    简单的配置:

    module.exports = {
      devtool: 'eval-source-map',
      entry:  __dirname + "/app/main.js",
      output: {
        path: __dirname + "/public",
        filename: "bundle.js"
      }
    }
    复制代码

    设置 ESLint 检查代码格式

    首先,要使 webpack 支持 eslint,就要要安装 eslint-loader ,命令如下:

    npm install --save-dev eslint-loader
    复制代码

    在 webpack.config.js 中添加如下代码:

    {
        test: /\.js$/,
        loader: 'eslint-loader',
        enforce: "pre",
        include: [path.resolve(__dirname, 'src')], // 指定检查的目录
        options: { // 这里的配置项参数将会被传递到 eslint 的 CLIEngine 
            formatter: require('eslint-friendly-formatter') // 指定错误报告的格式规范
        }
    }
    复制代码

    注:formatter 默认是 stylish ,如果想用第三方的可以安装该插件,如上方的示例中的 eslint-friendly-formatter 。

    其次,要想 webpack 具有 eslint 的能力,就要安装 eslint,命令如下:

    npm install --save-dev eslint
    复制代码

    最后,项目想要使用那些 eslin 规则,可以创建一个配置项文件 '.eslintrc.js',代码如下:

    module.exports = {
        root: true, 
        parserOptions: {
            sourceType: 'module'
        },
        env: {
            browser: true,
        },
        rules: {
            "indent": ["error", 2],
            "quotes": ["error", "double"],
            "semi": ["error", "always"],
            "no-console": "error",
            "arrow-parens": 0
        }
    }
    复制代码

    这样,一个简单的 webpack 引入 eslint 已经完成了。

    总结

    webpack 确实是一个功能强大的模块打包工具,丰富的 loader 和 plugin 使得其功能多而强。学习 webpack 使得我们可以自定义自己的开发环境,无需依赖 create-react-app 和 Vue-Cli 这类脚手架,也可以针对不同的需求对代码进行不同方案的处理。

    展开全文
  • webpack使用详解(二)

    2020-12-17 22:18:26
    webpack历史 前端工具的极速发展 grunt打包工具,速度太慢,快死了 gulp打包工具,速度可以,但没webpack繁荣 require.js 快死了 sea.js 死了 Browserify 已经挂了 与webpack竞争的工具 Rollup 比webpack的打包...
  • Gulp & webpack 配置详解

    2019-10-31 23:38:15
    1. Gulp VS webpack 比较 Gulp 是一个任务管理工具,让简单的任务更清晰,让复杂的任务易于掌控;而 webpack 的理念是,一切皆为模块,每个模块在打包的时候都会经过一个叫做 loader 的东西,它具备非常强大的精细...
  • (二)webpack babel详解

    2021-07-14 11:37:51
    也可以在webpack.json.js中设置 module: { rules: [ { test: /\.js$/, use: { loader: "babel-loader", options: { presets:[ ["@babel/preset-env",{ targets:["chrome 88"] }] ] } } } ] } 5.babel底层原理 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,742
精华内容 1,096
关键字:

webpack原理详解