Better

Ethan的博客,欢迎访问交流

webpack入门学习

如今的前端不再是以前的“所见即所得了”,最终被浏览器解释执行的代码,可能已经被加工过很多次了,比如转译,压缩,丑化,分割,打包等,这其中webpack扮演的非常重要的作用,他不但能帮助我们提高开发效率,还能帮助我们对线上代码进行调优,这么重要的东西,不学可不行,但我在学习的过程中碰到很多的难题,累觉webpack学习曲度有点高

累,但依旧爱你

其实学习曲度有点高,很大一部分原因也是因为自己,为什么这么说呢?因为我切入学习的点还是有点晚,不能从webpack 1.x开始逐步实践学习。

前端的更新速度相比大家都知道,由于如今最近的webpack都已经到4.6了,网络上的资料鱼龙混杂,良莠不齐,版本也不一致,又将1.x的,有3.x的,也有4.x的,而且语法还有不兼容的地方,我指向说OMG,你这是搞事情啊。

但是想到你这么牛逼,而且在前端扮演的角色越来与重要,还是决定好好学习,天天向上,相信掌握了你,会对我的成长有极大的提高。

聊聊前端的价值

  1. 搭建前端工程
    • 已经不是所见即所得了(ES6,sass)
  2. 网络优化
    • HTTP知识
  3. API定制
  4. nodejs层npm

前端模块化发展

以前我们前端开发的限制:

  • 浏览器对资源加载有同源策略限制,也不支持编程化加载资源。
  • 大部分加载器选择通过<script>标签加载,然后通过各种hack判断是否加载完成。

发展

  • require.js 将 AMD 发扬光大,成为 AMD 事实标准。
  • Common.js / CMD

webpack时代

  • 与 require.js / sea.js 不同,webpack 是一个在开发阶段进行打包的模块化工具,也就是说它无法不经过打包直接在线上使用。
  • webpack同时兼容 AMD、Common.js 以及非模块化的写法。注意这里的同时兼容可不是指你可以任选一种,你可以同时用三种!

webpack 基本概念

入口文件

  • 入口文件就是在 HTML 直接引用的,由浏览器触发执行的 JS 文件。其它的非入口文件则是由入口文件来直接或间接依赖,由 JS 互相调用执行。
  • 一般而言,我们会将一些逻辑提取封装后放到独立的文件中,最后由入口文件引入来调用它们提供的方法完成整个程序逻辑。也就是一般入口文件关注整体流程,而非入口文件关注公共某一部分的实现。

打包文件分析

假设我们将一个仅有1行代码的js用webpack打包,查看打包后的文件,你会发现文件变大很多。其实主要分为两部分

  1. 函数定义,也就是官方文档中提到的 Runtime,作用是保证模块的顺利加载和运行。
  2. 自定义的 JS 文件,不过被包裹在一个函数中,也就是模块

值得注意的是第20行,使用了.call,第一个参数是 module.exports,这就导致模块的包裹函数的作用域中的 this 指向了module.exports。这会带来两个后果:

  1. 模块中无法使用this指代全局变量(浏览器中就是window)
  2. 模块中可以使用this.xxx来指定模块包含的成员,类似 Common.js 中 exports.xxx 的方式(感觉我们找到了除AMD/Common.js之外的另一种模块化规范,不过因为 webpack 官方并没有强调这个,我们也只是代过。)
  3. 影响
    • 模块不是暴露在全局作用域下了。也即通过var xxx的方式定义的xxx变量不再挂在全局对象下。这可能是在非模块化的代码迁移到webpack中碰到的最大的问题,需要手工将var xxx的定义方式改为window.xxx。
    • 由于模块源码是采用非模块化的方案编写的,因此没有通过AMD的return或者CommonJS的exports或者this导出模块本身,这会导致模块被引入的时候只能执行代码而无法将模块引入后赋值给其它模块使用。

为何webpack能同时使用多种模块化(及非模块化)方案的模块,而传统的require.js / sea.js之类的方案则不行?这是因为webpack对模块的处理是在编译阶段,针对每一个模块都可以针对性地分析,然后对不同方案加以不同的包裹方案,最后生成可供浏览器运行的代码。而require.js之类的方案必须保证在运行时阶段能正确地引入模块并运行。

如果是多模块依赖呢

  • 发现模块列表数组有了多个模块
  • 模块紧挨着的数字注释是,表示模块的索引
  • 值得注意的是第40行,return __webpack_require__(0),如果留心的话会发现我们前面的例子中编译出来的这一行全部都是引用写死模块ID 0,也就是说,模块ID为0的永远是入口。

Node模块

Node使用的模块规范也是CommonJS,所以理想情况下,是可以做到代码在Node和浏览器中复用的。但这里面有几处关键的差异可能导致无法复用:

  • Node的模块在运行时会有一个包裹函数,下面详述
  • 浏览器并不支持所有的Node模块,因此部分使用了Node API的模块无法在浏览器中运行

不知道大家在写Node模块的时候是否有好奇过,这里的module(以及require / exports / filename / dirname)是从哪里来的?因为按照对JS的认知,如果它不是一个局部变量(含函数形参)的话,那么只能是全局变量了。难道这些变量是全局变量?然而当我们打开Node的命令行去访问的时候又明明白白地告诉我们是undefined。

按照Node的文档,global.require确实是存在的,还有.cache / .resolve()等成员,但每个模块中使用的require仍然是局部变量,并非全局require。

如果在运行的时候去查看它的话,它会变成这样:

(function (exports, require, module, __filename, __dirname) {
    var me = {};
    module.exports = me;
});

可以看到,我们的模块代码在运行的时候是被包裹了一层的,而上面列的这些变量正是在这个包裹函数中作为形参传入的。

其中module指向模块本身,module.exports和exports是等价的,表示模块要导出供调用的内容,__filename表示当前模块的文件名,__dirname表示当前模块所在路径。

使用模式

在进入配置项讲解之前,我们首先看一看webpack为我们提供的使用方式:CLI和API。

CLI

CLI即Command Line Interface,顾名思义,也就是命令行用户界面。

大部分生产环境下使用时都会需要加上非常多的参数,导致整个命令非常长,既不利于记忆编写,也不利于传播交接等。因此,一般会将配置项写在同目录的webpack.config.js中,然后执行webpack即可,webpack会从该配置文件中读取参数,此时不需要在命令行中传入任何参数。

执行时webpack会去寻找当前目录下的webpack.config.js当作配置文件使用,也可以使用 -c 指定配置文件

配置文件webpack.config.js的写法则是:

module.exports = {
    // 配置项
};

值得注意的是,配置文件是一个真正的JS文件,因此配置项只要是一个对象即可,并不要求是JSON。也就意味着你可以使用表达式,也可以进行动态计算,或者甚至使用继承的方式生成配置项。

API

API则是指将webpack作为Node.js模块使用,例如:

webpack({
    // 配置项
    entry:'main.js',
    ...
},callback);

相比CLI模式而言,使用API模式会更加灵活,因为可以与你的各种工具进行集成,甚至共享一些环境变量然后动态生成打包配置等等,在复杂项目中会很有用。

webpack 1.x 使用

最终我决定从头开始学期,主要原因有两点:

  • 因为我相信虽然语法不一样,但是思想是不会过时的
  • 很多旧项目还是基于1.x,学习它不但加深对项目的理解,也为以后项目的迁移做准备

简介

webpack是什么

  • JS模块打包工具,将很多模块打包成很小的静态文件
  • 代码分隔允许在应用需要是加载文件
  • 模块加载器,模块可以是CommonJs,AMD,ES6,CSS,Image,JSON,CoffeeScript,less,甚至自定义的文件等

目标

  • 切分依赖树,按需加载
  • 保证初始加载时间耗时少
  • 每个静态资源都是一个文件
  • 可以集成第三方库作为一个模块
  • 可以定制几乎模块的每个部分
  • 适合大型项目

为什么webpack不一样

  • 代码分隔
  • 加载器
  • 插件系统
  • 模块热更新

配置文件

通常我们都是通过配置文件的参数,来集中管理webpack相关的代码。下面是基本知识。

  • 一般取名 webpack.config.js
    • 默认从根目录读取此文件,否则通过--config参数进行自定义配置
  • entry参数:入口文件,三种方式
    • 简单的字符串,默认chunk name为main
    • 数组:多个入口文件,默认chunk name均为main,此时webpack会将它们的代码合并打包!
    • 对象:k-v,k表示chunk name,名字和模块文件名一一对应,每个模块都有独立的名字,因此这里的[name]可以理解成模块名字。适合多页应用的情况
    • 事实上,在webpack的文档中,这个name是指“chunk name”,即分片的名字,所谓分片就是指一个入口模块的代码有可能会被分成多个文件,还有一些文件可能是来自模块的公共代码,而不是入口模块。因此这里的[name]并非严格与入口模块一一对应。
  • output参数:打包后文件
    • path:执行输出目录
    • filename:多页应用,不能写死,或者会被后者覆盖,支持写目录,来进一步划分子目录
      • 使用占位符 [name],[hash],[chunkhash]
      • 分别表示模块名称、模块编译后的(整体)Hash值、分片的Hash值
    • publicPath:设置上线地址,可以理解成一个占位符,设置就会使用他进行替换绝对地址

让webpack不打包指定的lib

  • 在开发中有些时候我们需要webpack不打包某些lib,这在我们开发lib的时候特别常见
  • 使用配置的external选项可以做到

loader

loader可以在加载模块时预处理文件,loader 相比 plugin 而言,用法没那么多样,最多也就是配置参数的不同。

loader是webpack中一个重要的概念,它是指用来将一段代码转换成另一段代码的webpack插件。为什么需要将一段代码转换成另一段代码呢?这是因为webpack实际上只能处理JS文件,如果需要使用一些非JS文件(比如Coffee Script),就需要将它转换成JS再require进来。

webpack的每一个loader命名都是xxx-loader,在安装的时候需要将对应的loader装到项目目录下

除了npm安装模块的时候以外,在任何场景下,loader名字都是可以简写的,coffee-loader和coffee是等价的

loader是可以串联使用的,也就是说,一个文件可以先经过A-loader再经过B-loader最后再经过C-loader处理。

loader还可以接受参数,不同的参数可以让loader有不同的行为(前提是loader确实支持不同的行为),具体每个loader支持什么样的参数可以参考loader的文档。参数的指定方式和url很像,要通过?来指定。

loader的使用方式

  • 直接引用的时候处理,比如require的时候,import的时候,eg:css-loader!./style.css,注意感叹号很重要
  • 每次引用写的时候太麻烦了,可以使用命令行工具,也顺便了解其他命令参数,参数可以写进npm脚本中
    • --module-loader 'css=style-loader!css-loader'
    • --watch 检测改动,自动编译
    • --progress 显示打包过程
    • --display-modules 显示引入的模块
    • --display-reasons 显示模块打包原因
    • -- colors 彩色
  • 配置文件

loader 常见使用场景

  • 处理es6
  • 处理css,less、sass
  • 图片压缩,转base64(减少请求)
  • loader配置文件参数有test、loader、loaders、include、exclude,query

新的webpack版本已经发生改变,配置loader需要在module选项下指定对应后缀名相应的rules,使用test正则指定后缀名,使用use指定相应的loader

const config = {
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist')
  },
  module: {
    rules: [{
      test: /\.css$/,
      use: [
        'style-loader',
        'css-loader'
      ]
    }]
  }
};

loader本质上做的是一个anything to JS的转换,因此想象空间极大,大致有这样一些用途:

  • 其它语言编译到JS,包括JSON、纯文本、coffee、CSS等,也包括比较复杂的整体方案处理,比如vue-loader、polymer-loader等
  • “微处理”类,比如为非模块化文件添加一些模块化导入导出代码,对模块细节(代码)微调等,例如exports-loader、expose-loader等
  • 校验和压缩类,这一类其实最终并不生成代码,检验类如果报错就阻止构建进行,压缩类只是替换一下图片资源等
  • 纯打包类,比如file-loader,将资源文件一并纳入路径管理

处理es6

  1. 安装 babel-loader
    npm install -save-dev babel-loader babel-core
    
  2. 配置babel,有多种方式
    • 添加.babelrc,指定babel的一些配置,比如
      {
         presets:['latest'] // 指定支持哪些语法,比如es2015 es2016 es2017,lastest囊括三者
      }
      
    • webpack配置文件中,使用query指定参数
      {
         test:/\.js$/,
         loader:'babel-loader',
         query:{
             presets:['latest']
         }
      }
      
    • 还可以在package.json中增加babel参数指定
      "babel":{
         presets:['latest']
      }
      
  3. 这个预设置配置,也是需要安装的,比如latest
    npm install --save-dev babel-preset-latest
    
  4. 示例
    module:{
     loaders:[
         {
             test:/\.js$/,
             loader:'babel-loader'
         }
     ]
    }
    

优化babel的速度

  • babel转换语法是非常耗时的
  • 通过exclude排除node_modules下的转换
  • 注意路径比如是正则表达式,绝对路径,函数或绝对路径数组,绝对路径可以使用path
    var path = require('path');
    // ...
    {
      exclude:path.resolve(__dirname,'node_modules/'),
      include:path.resolve(__dirname,'src/'),
    }
    

loader处理css

  • 允许将css当作模块使用
  • 安装style-loader,css-loader
    • css-loader:支持将css作为模块
    • style-loader:将css内容插入到对应页面,使之生效
  • 需要安装postcss-loader
    • 在css-loader处理之前,sass-loader后面,帮助我们后处理css的
    • 非常多的postcss-plugins
      • autoprefixer:添加兼容性前缀
    • module同级,添加postcss配置,配置下的插件依旧需要单独安装,比如autoprefixer
    • 这时直接@import其他css模板,语法支持,但postcss并没有进行处理,给css-loader添加importLoaders参数解决
  • less-loader sass-loader
    • 不需要加importLoaders,自身处理了
  • 实例
    module:{
      loaders:[
          {
              test:/\.css$/,
              loader:'style-loader!css-loader?importLoaders=1!postcss-loader'
          },
          {
              test:/\.less$/,
              loader:'style-loader!css-loader!postcss-loader!less-loader'
          },
          {
              test:/\.scss$/,
              loader:'style-loader!css-loader!postcss-loader!sass-loader'
          }
      ]
    },
    // module同级,添加postcss配置
    postcss:function(){
      return [
          require('autoprefixer')({
              browsers:['last 5 versions']
          })
      ]
    }
    

loader处理项目模板文件

  • 由于现在有很多的模板处理器,关于处理模板的插件,webpack对应也收录了很多,比如ejs,handlebars,jsx,html等
  • 对于一些热门的模板,比如jsx,已经集成在babel,因此不需要在额外添加loader
  • 安装html-loader
  • 实例
    {
      test:/\.html$/,
      loader:'html-loader'
    }
    

loader处理图片和其他文件

  • css引用图片,模板引用图片,最顶层html引用图片
  • 安装并file-loader
  • css引用图片,顶层html引用图片都会被替换,但是模板中会出现问题,怎么解决
    • 使用绝对路径,比如cdn
    • 使用webpack提供的require函数
  • 可以指定文件名称,需要提供参数
  • url-loader(依赖file-loader)
    • 处理图片或文件
    • 通过设置limit参数,如果文件大小大于limit,丢给file-loader处理,如果小于limit,图片和文件转成base64的编码
  • image-loader
    • 安装 npm install --save-dev image-webpack-loader
    • 和file或url loader搭配使用
    • 压缩图片
  • 实例
    {
      test:/\.(png|jpg|gif|svg))$/i,
      loaders:[
          'url-loader?limit=20000&name=assets/[name]-[hash:5].[ext]',
          'image-webpack'
      ]
    }
    

plugins

插件的目的在于解决loader解决不了的事情,使用插件指定plugins选项即可,需要注意的使用插件需要引入插件。

plugin 是 webpack 中非常重要的一环,每个 plugin 的使用方式可能五花八门,因此我们需要对插件本身有一定的了解,通过在 npm 上查看 plugin 的具体用法。

在这里演示一下最常用的 htmlWebpackPlugin 的用法

  • 功能:自动按照模版生成 html
  • 使用:加入config.plugins即可
  • 为什么是指向当前的目录的index.html呢,上下文的概念,context字段,默认是当前配置文件脚本运行的目录
  • 其他非必要字段
    • filename:也可以使用占位符,默认和模板名称保持一致
    • inject:指定插入的位置,比如head或body里
    • 其他字段,比如 title,date,favicon 等。
    • minify:实现压缩html,值是对象,可以配置压缩哪些内容,比如删除空格,删除注释等
  • 在模板文件中使用,通过htmlWebpackPlugin引用
    • 比如<%= htmlWebpackPlugin.options.title %>读取
    • 主要有两个属性:options和files
    • options 配置信息
    • files 文件信息,可以实现类似于,一部分js在head引入,一部分在body引入的功能,但是需要关闭自动inject,设置inject为false即可
  • 实现多页
    • 我们直接plugins数组中创建多个htmlWebpackPlugin即可
    • 如果我们不想创建多个模板页面,如何支持一个模板生成多个页面呢
    • 使用chunks:指定页面加载哪些chunk
    • excludeChunks:指定排除哪些chunk
  • 初始化代码不想通过加载js脚本的方式实现,而是直接inline到页面,达到减少http请求,这时候就需要关闭inject,然后在模板中使用模板写法过滤掉已经加载完成的初始化脚本啦
    <script>
      <%= compilation.assets[htmlWebpackPlugin.files.chunks.main.entry.substr(htmlWebpackPlugin.files.publicPath.length)].source()%>
    </script>
    
  • 使用示例
    new htmlWebpackPlugin({
      template:'index.html',
    })
    

Other Useful Plugin List

  • uglifyjs-webpack-plugin:精简输出,混淆,丑化
  • clean-webpack-plugin:清除重复的文件

分片

随着项目开发过程中越来越大,我们的代码体积也会越来越大,而将所有的脚本都打包到同一个JS文件中显然会带来性能方面的问题(无法并发,首次加载时间过长等)。

值得注意的是,webpack对代码拆分的定位仅仅是为了解决文件过大,无法并发加载,加载时间过长等问题,并不包括公共代码提取和复用的功能。对公共代码的提取将由CommonChunks插件来完成。

要使用webpack的分片功能,首先需要定义“分割点”,即代码从哪里分割成两个文件。

分割点表示代码在此处被分割成两个独立的文件。具体的方式有两种。

  • 使用 require.ensure
  • 使用 AMD 的动态 require

分片只是分片,并没有自动提取公共模块的作用。

development or production

声明:这里的演示代码。可能有版本语法问题

生产和开发中的构建肯定是不同,生产中侧重于一个更好的开发体验,而生产环境中则需要更多的性能优化,更小的chunk。

在具体使用webpack时候,我们除了上面基础的配置外,通常还会有生产和开发环境下不同的配置,我们可以项目比较大时,我们可以额外创建如下三个文件,然后在webpack.config.js中集中配置,可以通过webpack-merge库合并参数

  • webpack.base.config.js
  • webpack.dev.config.js
  • webpack.prod.config.js

在这里介绍一个node包cross-env来帮助我们兼容平台

  • mac上设置环境变量,直接NODE_ENV=production
  • windows上设置环境变量,需要set NODE_ENV=production
  • 帮助我们忽略平台的不同,统一如下:cross-env NODE_ENV=production

webpack.DefinePlugin很重要

  • 方便自己代码进行环境判断
  • 许多lib通过与process.env.NODE_ENV环境关联来决定lib中使用哪些内容,使用webpack内置的DefinePlugin可以为所有依赖指定这个变量。比如 vue 和 react,会根据不同环境进行区分打包。
  • 示例
    new webpack.DefinePlugin({
      'process.env': {
          NODE_ENV: isDev ? '"development"' : '"production"' // 注意这个的引号
      }
    })
    

开发环境

webpack 给我们开发环境提供了webpack-dev-server,webpack-dev-server提供了一个简单的web服务器,并能够实时重新加载,帮我们提高开发效率。

开启热更新

  • 设置 hot 为 true
  • 加入 HotModuleReplacementPlugin 和 NoEmitOnErrorsPlugin 插件

config.devtool

  • 帮助我们在浏览器中调试
  • 原理使用sourceMap映射
  • config.devtool = '#cheap-module-eval-source-map'

开发环境下webpack-dev-server基本配置

if (isDev) {
    config.devtool = '#cheap-module-eval-source-map';
    config.devServer = {
        port: 8000,
        // 可以通过localhost,127.0.0.1,主要还包括内网地址可以访问
        host: '0.0.0.0',
        // 将错误显示到网页上
        overlay: {
            errors: true
        },
        // 编译完成打开浏览器
        open: true,
        // 前端路由的问题
        // historyFallback: {},
        // 热更新
        hot: true,
        // 告诉服务器在哪里寻找文件?是否必须
        contentBase: './dist',
    };
    config.plugins.push(
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NoEmitOnErrorsPlugin()
    );
}

webpack-dev-middleware:对更改的文件进行监控,编译, 一般和 webpack-hot-middleware 配合使用,实现热加载功能。待实践?我如上配置,也可以啊?

http-proxy-middleware:设置代理。待实践?

生产环境

开发环境下, 我们只关注方便开发。但生产环境下,我们需要关注性能问题。

比如我们希望将css单独提取出来做持久化,head头部加载style资源。我们希望代码分割,比如vue这些类库从业务代码中提取出来,因为他们比较稳定,可以做持久化

提取css,我们需要用到extract-text-webpack-plugin插件,作用将非javascipt资源打包成单独的文件,配置具体查阅文档。

代码分割是 webpack 一个比较重要的点,而且在 webpack 4.x 中还做了优化,在这里简单演示一下,后面会深入学习。下面是生产环境的基本配置

config.entry = {
    app: path.join(__dirname, 'src/index.js'),
    // 指定类库
    vendor: ['vue']
}
config.output.filename = '[name].[chunkhash:8].js'
config.module.rules.push({
    test: /\.styl$/,
    use: extractPlugin.extract({
        fallback: 'style-loader',
        use: [
            'css-loader',
            {
                loader: 'postcss-loader',
                options: {
                    sourceMap: true
                }
            },
            'stylus-loader'
        ]
    })
})
config.plugins.push(
    new extractPlugin('styles.[chunkhash:8].css')
    // // 将类库文件单独打包出来
    // new webpack.optimize.CommonsChunkPlugin({
    //     name: 'vendor'
    // })

    // webpack相关的代码单独打包
    // new webpack.optimize.CommonsChunkPlugin({
    //     name: 'runtime'
    // })
)
config.optimization = {
    splitChunks: {
        cacheGroups: {                  // 这里开始设置缓存的 chunks
            commons: {
                chunks: 'initial',      // 必须三选一: "initial" | "all" | "async"(默认就是异步)
                minSize: 0,             // 最小尺寸,默认0,
                minChunks: 2,           // 最小 chunk ,默认1
                maxInitialRequests: 5   // 最大初始化请求数量,默认1
            },
            vendor: {
                test: /node_modules/,   // 正则规则验证,如果符合就提取 chunk
                chunks: 'initial',      // 必须三选一: "initial" | "all" | "async"(默认就是异步)
                name: 'vendor',         // 要缓存的 分隔出来的 chunk 名称
                priority: 10,           // 缓存组优先级
                enforce: true
            }
        }
    },
    runtimeChunk: true
}

Common Chunks 插件的作用就是提取代码中的公共模块,然后将公共模块打包到一个独立的文件中去,以便在其它的入口和模块中使用。

var webpack = require('webpack');

module.exports = {
    entry:{
        main1:'./main',
        main2:'./main.2'
    },
    output:{
        filename:'bundle.[name].js'
    },
    plugins: [
        new  webpack.optimize.CommonsChunkPlugin('common.js', ['main1', 'main2'])
    ]
};

参数common.js表示公共模块的文件名,后面的数组元素与entry一一对应,表示要提取这些模块中的公共模块。

实践源码

资料

官网

入门级别

更进一步 TODO



留言