前端自动化打包工具--webpack

前端自动化打包工具–webpack

背景

记得2004年的时候,互联网开发就是做网页,那时也没有前端和后端的区分,有时一个网站就是一些纯静态的html,通过链接组织在一起。用过Dreamweaver的都知道,做网页就像用word编辑文档一样。一个html页面,夹杂着css,javascript是再常见不过的事了。
在这里插入图片描述
随着前端的不断发展,特别是单页应用的兴起,这种所见即所得的IDE工具,就渐渐地退出了前端的主流。一个应用,通常只有一个静态页面(index.html),甚至这个页面的body只有一个或少数几个div组成。这时有大量的css和javascript代码需要编写。如何组织他们,就是现在前端所面临和要解决的问题。
在这里插入图片描述
一些很好的前端框架(像angularjs,React,Vue)可以帮我们如何合理的组织代码,保持代码的可维护性和扩展性。这在开发阶段是很有用的,可是要把应用发布到线上的时候,需要把代码进行合并压缩,以减小代码体积,和文件数量,人为的对代码进行丑化。于是就有了grunt,gulp,webpack等前端工程化打包工具。

如何使用webpack

ebpack是近期最火的一款模块加载器兼打包工具,它能把各种资源,例如JS(含JSX)、coffee、样式(含less/sass)、图片等都作为模块来使用和处理。它将一堆文件中的每个文件都作为一个模块,找出它们的依赖关系,将它们打包为可部署的静态资源。

我们可以直接使用 require(XXX) 的形式来引入各模块,即使它们可能需要经过编译(比如JSX和sass),但我们无须在上面花费太多心思,因为 webpack 有着各种健全的加载器(loader)在默默处理这些事情。

一个基本的例子,想像我们有一些 CommonJS 模块,它们不能直接在浏览器中运行,所以我们需要打包成一个文件,这样就可以通过

但是 webpack 可以做的不止这些。通过“loader”,我们可以配置 webpack 以任何方式去转换所有类型的文件。包括以下例子:

转换 ES2015,CoffeeScript 或者 TypeScript 模块为普通的 ES5 CommonJS 模块;
可以选择在编译之前检验你的源代码;
将 Jade 模版转换为纯 HTML 并且嵌入 Javascript 字符串中;
将 Sass 文件转换为纯 CSS,然后将其转换成 JavaScript 片段,将生成的 CSS 作为

webpack模块引入

一. HTML

直接在页面引入 webpack 最终生成的页面脚本即可,不用再写什么 data-main 或 seajs.use 了:

<!DOCTYPE html>
 
<html>
 
<head
lang="en">
 
  <meta
charset="UTF-8">
 
  <title>demo</title>
 
</head>
 
<body>
 
  <script
src="dist/js/page/common.js"></script>
 
  <script
src="dist/js/page/index.js"></script>
 
</body>
 
</html>

可以看到我们连样式都不用引入,毕竟脚本执行时会动态生成

二. JS

各脚本模块可以直接使用 commonJS 来书写,并可以直接引入未经编译的模块,比如 JSX、sass、coffee等(只要你在 webpack.config.js 里配置好了对应的加载器)。

我们再看看编译前的页面入口文件(index.js):

require('../../css/reset.scss'); //加载初始化样式
 
require('../../css/allComponent.scss'); //加载组件样式
 
var
React = require('react');
 
var
AppWrap = require('../component/AppWrap'); //加载组件
 
var
createRedux = require('redux').createRedux;
 
var
Provider = require('redux/react').Provider;
 
var
stores = require('AppStore');
 
var
redux = createRedux(stores);
 
var
App = React.createClass({
 
    render: function() {
 
        return
(
 
            <Provider redux={redux}>
 
                {function() { return
<AppWrap />; }}
 
            </Provider>
 
        );
 
    }
 
});
 
React.render(
 
    <App />, document.body
 
);

那好,我们来看一下具体使用。
使用webpack之前,需要安装node.js,然后通过npm 安装webpack.具体的安装过程移步官网本着从入门到精通的顺序,先来看一个最简单的应用。

场景一:

在demo1目录下,有两个文件,app.js,cats.js,需要把它们合并成一个bundle.js文件.
demo
cats.js:

var cats = ['dave', 'henry', 'martha'];
module.exports = cats;

app.js:

cats = require('./cats.js');
console.log(cats);

如果是全局安装的webpack,那么直接在命令行窗口中输入webpack app.js bundle.js就可以了:
在这里插入图片描述
要得到压缩版的也很容易,在后面追加一个-p参数:
在这里插入图片描述
bundle.js由原来的1.58kb缩小到304b.

如果每改一次代码就要输一次命令,就太没意思了,这时就需要追加一个" -w " 参数 (watch) 监视代码的改动。

webpack app.js bundle.js -p -w

注意:如果是clone的代码,试验时,请移除目录下的webpack.config.js文件。

虽然简单,但是这里有一个重要的概念要说一下:官方文档中把app.js这个文件称为“entry point”,即“入口”。代表着webpack从哪开始。webpack会顺着这个入口文件自动寻找里边所依赖的文件,比如demo01中的cats.js会自动被载入。而bundle.js 是我们指定打包之后输出的文件名,默认的输出目录就是命令运行时所在的目录,也可以在指定输出目录,如./dist/bundle.js ,这样webpack就会自动创建dist目录,然后把bundle.js写在dist目录下。由于app.js这个入口文件是纯js,webpack直接就可以支持,如果是其它类型的文件,比如css,就需要用到"loader",即“加载器”,后面会有详细介绍。

除了直接用webpack命令指定入口文件打包之外,还可以通过配置webpack.config.js文件实现同样的功能:

webpack.config.js :

//最简单的webpack配置
 
module.exports = {
    entry: './app.js', //入口文件地址
    output: {
        filename: 'bundle.js',  //打包后的文件名
    }
};

通过配置webpack.config.js之后,在命令行下只需要简单的输入webpack就可以了。如果是这么简单的应用,显然体现不出webpack.config.js存在的价值。通常我们的网站都会有多个页面,比如index,home,about等等,每个页面都是一个独立的入口,于是就产生了多入口的情况,下面就看看多入口的情况下,webpack怎么输出不同的打包文件。
demo2

//webpack.config.js
//多入口示例
module.exports = {
  entry: {
    bundle1: './main1.js', //入口1
    bundle2: './main2.js'//入口2
  },
  output: {
    filename: '[name].js' // [name]是一个变量,会自动替换成entry的key
  }
};

和demo01相比,这次的入口(entry)是一个对象, 用键值对的形式指定了多个入口文件,输出的文件名用了变量表示。事实上,入口文件的值还可以是数组。如:

//webpack.config.js
//多入口示例
module.exports = {
  entry: {
    bundle1: ['./main1.js'], //入口1
    bundle2: ['./main2.js']//入口2
  },
  output: {
    filename: '[name].js' // [name]是一个变量,会自动替换成entry的key
  }
};

这种用法,对于入口文件需要指定多种类型的文件时比较有用。比如[’./main1.js’,’./main1.css’],后面用到再细讲。小结一下:对于entry一共展示了三种形式:

  1. entry:'app.js'  直接写入口文件

  2. entry:{bundle:'./main1.js'} 对象形式

  3. entry:{bundle:['./main1.js']} 对象中的值用数组表示

接下来的demo03将展示webpack在jsx, es6 中的用法。这一节内容会稍稍有点多。首先是package.json文件,它不是webpack的组成部分,但是常和webpack项目出双入对,先看一下它的大概模样:

{
  "name": "demo01",
  "version": "1.0.0",
  "description": "sample to use webpack",
  "main": "index.html",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack"
  },
  "keywords": [
    "webpack"
  ],
  "author": "frog",
  "license": "MIT",<br>  ”dependencies":{},
  "devDependencies": {
    "babel-core": "^6.20.0",
    "babel-loader": "^6.2.10",
    "babel-preset-es2015": "^6.18.0",
    "babel-preset-react": "^6.16.0",
    "react": "^15.4.1",
    "react-dom": "^15.4.1",
    "webpack": "^1.13.0"
  }
}

关于这个文件的更多介绍,请移步官方内容 这里我只重点介绍一下以下内个内容:

1. scripts 命令行脚本通过key:value的方式描述。key是脚本名,value是脚本执行的内容,通过在命令行中输入npm run 脚本名 就可以执行。这一块的内容是实际开发中很实用的,这里不详情展开,

参考地址

常见的脚本名有:npm run start , npm run test 。 内置的脚本名(比如start),可以省略run。


2. devDependencies 开发依赖,
3. 相应的还有一个dependencies(可以理解为生产环境依赖)

通过npm install 包名 --save-dev (保存到devDependencies),或 --save 保存到(dependencies)

package.json是用来配合包的管理和发布用的,如果你不想发布这个项目,似乎以上内容对项目开发并没有什么好处,但是作为团队协作,它可以方便自己和同事快速搭建项目,管理项目中用到的第三方包。

下面回到webpack.config.js这个文件来。由于jsx是react专用的语法,超出了js的语法范围,要想加载jsx文件,需要借助一个loader(加载器), 不同类型的文件有不同的加载器,比如jsx,es6要用到babel-loader

加载css要用到css-loader,加载html要用到html-loader等等. 下面是具体的用法:

module.exports = {
  entry: './main.jsx',
  output: {
    filename: 'bundle.js'
  },
  module: {
    loaders:[
      {
        test: /\.js[x]?$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        query:{
           presets:['react','es2015']
        }
      },
    ]
  }
};

所有的loader都放在module下面的loaders里边.通常有以下内容:

 1.  test:是对该类文件的正则表达式,用来判断采用这个loader的条件。

   2.  exclude是排除的目录,比如node_modules中的文件,
   3. 通常都是编译好的js,可以直接加载,因此为了优化打包速度,
   4. 可以排除。作为优化手段它不是必须的。

   5. loader: 加载器的名称,每一个加载器都有属于它自己的用法,
   6. 具体要参考官方说明。

   7. query: 传递给加载器的附加参数或配置信息,
   8. 有些也可以通过在根目录下生成特殊的文件来单独配置,比如.babelrc 

这里配置好,还不能用,需要安装对应的加载器到项目中来,安装方式很简单,通过命令行,输入npm install 加载器的名称 --save-dev 或 --save

加–save或–save-dev的目的是为了把该插件记录到package.json中去,方便通过npm install的时候自动安装。
通过npm3.0+版本安装的时候,它不会自动安装依赖,需要手动去安装,比如安装这个babel-loader 的时候,它提示要安装babel-core和webpack,依次安装即可。demo03比较激进,直接用了jsx和es6的语法,所以要安装的插件比较多,但这也是实际开发中经常用到的。

"devDependencies": {
    "babel-core": "^6.20.0",
    "babel-loader": "^6.2.10",
    "babel-preset-es2015": "^6.18.0",//es6转普通js用
    "babel-preset-react": "^6.16.0", //解析jsx用
    "react": "^15.4.1",
    "react-dom": "^15.4.1",
    "webpack": "^1.13.0"
  }

由于我们在package.json的script中加了一个start脚本,所以这次,我不打算老套的用法,这次来点新鲜的尝试。直接运行npm start,看看是否大力出奇迹。
在这里插入图片描述
这和直接运行webpack是一样的结果,但是显得更高大上一些。如果你一半会不觉用不到react或es6这么新潮的东西,那就请忽略前面的内容,下面看一点更加简单更加常用的加载器.demo04
css-loader样式加载器

module.exports = {
entry: './main.js',
output: {
 filename: 'bundle.js'
},
module: {
 loaders:[
   { test: /\.css$/, loader: 'style-loader!css-loader' },
 ]
}
};

这里有两个要注意的地方:

  1。 对于有多个加载器串联的情况,webpack,
  它是从右向左依赖加载的,也就是说先用css-loader,再用style-loader.
 2.  为什么会有一个style-loader, 因为webpack
 3. 默认是把css文件插在html文件内,通过style标签加载样式的。
 4. 所以需要用style-loader这个加载器。

如果想要把css用文件的形式link到html中,也是可以的,后面会讲到。
由于我们用了css加载器,所以入口文件其实也可以直接写成:entry:’./app.css’; 效果是一样的。这就体现了,入口文件,不一定要是js格式,只要有对应的加载器,就可以直接在入口中使用,甚至多种类型混合使用,比如[’./app.js’,‘app.css’],都是可以的。
在这里插入图片描述
样式中,常常会用到图片,比如background:url(’…/images/logo.jpg’); 如果没有指定加载器,就会报错(you may need an appropriate loader to handle this file type),这时,就需要用到图片加载器了,不要以为,只有在入口中用到的文件才要加载器,只要是在webpack工作期间加载到的文件,只要不是js文件,就需要指定加载器,并在webpack.config.js中正确配置。

module.exports = {
  entry: './main.js',
  output: {
    filename: 'bundle.js'
  },
  module: {
    loaders:[
        { test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192&name=[name][ext]'}
    ]
  }
};

图片加载- demo05 示例中用的是url-loader,并不是期望的image-loader, 原因是url-loader url-loader 就可以加载图片字体这些文件了,因此不需要重复造轮子,事实上,url-loader还不是最终的加载器,它只不过是对file-loader的进一步封装。通过在url-loader后面加?来挂载更多的配置参数,可以实现定制化的需求,比如对于图片小于8192字节的图片,采用base64的方式,直接输出在css中,可以减少http请求。对于大这个限制的图片,通过name指定输出的文件名,在前面指定路径也是可以的。比如/images/[name][ext] ,这里的[name]和[ext]都是变量的表示,前面有讲过,用在这里,表示用原来输入时的文件名和扩展名。需要注意的是,这个路径是参考默认的输出路径的来的。如果要指定输出路径怎么处理呢?

请参考以下方法:

  1. 通过在webpack.config.js 中指定,output:{path:’./dist’,…}
module.exports = {
     entry: './src/app.js',
     output: {
         path: './dist',//新的输出路径
         filename: 'app.bundle.js'
     }
 };

‘./‘代表项目的当前目录,通常指根目录,这是一种相对路径的表示,也可以用绝对路径,通过path.resolve(__dirname,’./’)来指定,这时,webpack所生成的js,css文件都会变成./dist目录下,而对于本例中的图片,则还是在./目录下,
在这里插入图片描述

<br>并没有把图片生成在dist目录下,试试 loader: 'url?publicPath=./dist/'
module.exports = {
  entry: ['./main.js','./icon.css'],
  output: {
    path:'./dist',
    filename: 'bundle.js'
  },
  module: {
    loaders:[
        { test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192&publicPath=./dist/'},
        { test: /\.css$/, loader: 'style-loader!css-loader' },
    ]
  }
};

在这里插入图片描述
通过指定这个publicPath实现了图片生成到指定的目录。同样的,通过在output中指定这个值也是同样的作用。

output: {
  path:'./dist',
  publicPath:'./dist/', //在这里指定同样生效
  filename: 'bundle.js'
},

这个publicPath原本是用来配置虚拟目录用的,也就是通过http方式访问时的路径,或者通过webpack HMR方式加载时的输出目录。在这里只能算是一种hack用法。说到output,就要提一下文件缓存[hash]的用法:
output: {
path:’./dist’,
publicPath:’./dist/’,
filename: ‘bundle_[hash:8].js’ //通过:8截取has值的前8位
},

 output: {
  path:'./dist',
  publicPath:'./dist/',
  filename: 'bundle_[hash:8].js' //通过:8截取has值的前8位
},

这个[hash]作用很少被提到,在实际开发中,是很常见的功能,原样输出的hash我觉得太长,可以通过[hash:加数字]的方式进行截取,很方便。

对于webpack.config.js ,前面已经介绍了entry,output,module,下面以代码丑化为例,说说plugins,webpack的插件的用法:
demo07

var webpack = require('webpack');
var uglifyJsPlugin = webpack.optimize.UglifyJsPlugin;
module.exports = {
  entry: './main.js',
  output: {
    filename: 'bundle.js'
  },
  plugins: [
    new uglifyJsPlugin({
      compress: {
        warnings: false
      }
    })
  ]
}

plugins:的值是一个数组,所有插件都通过npm install进行安装,然后在plugins数组中添加对应的插件配置。有三个插件需要提一下:

  1. HtmlwebpackPlugin 这个插件可以把生成的css ,js 插入到html页中的head,body中,这对于加了hash值的输出很有用。这个功能也可以用gulp的insject插件做。不过既然用webpack了,就暂时忘了gulp吧。在具有相似功能的不同的工具之间切换,并不是一个好主意。不过html这款插件有一个小小的问题,它对html中的img不会像css中那样解析。造成dist目录下的html文件,img下的src报错。解决办法是添加html-withimg-loader这个插件。
{
    test:/.html$/,
    loader:'html-withimg-loader?min=false'
},
  1. CommonsChunkPlugin 提取公共代码,这个不需要安装,webpack集成有。
new webpack.optimize.CommonsChunkPlugin({
            name:'vendor',
            filename:'js/vendor.js',
            chunks:['chunk1','chunk2','chunk3']//不写为所有的chunk,
        }),

chunk (块), webpack中另一个非常重要的概念,和entry对应。有三种情况:

2.1 如果entry通过字符串的方式指定的入口文件,那么chunk就是指入口文件,比如entry: ‘./app.js’; 那么可以肯定chunk和’./app.js’一一对应。

2.2 如果是entry:[’./app1.js’,‘app2.js’]那么chunk就是这两个文件之和。

 *  以上chunk的[name]就是默认的"main".

2.3 如果是下面这种形式:

entry:{
   index:'./index.js',
   error:'./error.js',
   vendor:['react','redux']
}

那么就会产生多个chunk,[name]分别和index,error,vendor对应。

  1. ExtractTextPlugin 这个插件就是开头提到的,从html中分离css的插件。npm install extract-text-webpack-plugin --save-dev
plugins: [
       new ExtractTextPlugin("[name].css"),
]

在这里插入图片描述
需要注意的是,如果在[name].css前面加了子路径,如css/[name].css 那么就要小心样式中的图片路径出错,特别是在没有指定publicPah的情况下。background:url(这个地方的图片默认是和chunk的输出路径同级的,如果指定了publicPath,则以publicPath代替,不存在这个问题),但是由于我们人为的指定了打包后的样式放在css/目录下,而图片默认还在原来的目录,这就导致css中引用的图片路径失效。看下面的例子:

var ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
  entry: ['./main.js','./icon.css'],
  output: {
    path:'./dist',
    //publicPath:'./dist/',
    filename: 'bundle_[hash:8].js'
  },
  module: {
    loaders:[
        { test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192'},
        { test: /\.css$/, loader: ExtractTextPlugin.extract('style', 'css') },
    ]
  },
  plugins: [
    new ExtractTextPlugin("css/[name].css"),
  ]
};

在这里插入图片描述
从图上看到,添了css子目录之后,样式中的图片,还是原来的路径,而图片并不存在css目录下。

解决办法,要么不加子目录,保持样式文件和图片文件始终在同一层级,要么添加publicPath,相当于使用绝对路径。由于这个是在webpack.config.js中配置的,要更换也很容易。

看起来一切都很美好,可是当我们的html中也用了img标签的时候,问题就来了,还记得html-withimg-loader这个插件吗?它际实上也是调用的url-loader,所以,它最终的图片输出路径也样受publicPath的影响。考虑一下这样的目录结构:
在这里插入图片描述
样式文件是位于css子目录,而html则是和图片保持同级的。样式中的图片需要指定为"…/",而html中的图片需要指定成"./",这在同一个publicPath中,显示是冲突的。这时就需要权衡一下,要么所有的文件都堆在根目录下,要么html中的图片用别的插件进行处理。总之,不能让这种相冲突的情况发生。

最后再简单说一下webpack.config.js 中的 resolve;

resolve: {
    extensions: ['', '.js', '.vue', '.json']
  },

通常我们都知道通过配置这个属性下的extensions,可以省略扩展名,似乎没有什么可以介绍的,直到有一次我在项目中通过npm install vue --save 安装了vue ,然后我在代码中用import Vue from ‘vue’ 导入了vue。到这一步都是正常的,可是当我打算进一步使用vue的时候,代码就报错了,然后去查官方文档(出错的时候不要惊慌,大多数情况都会在官方找到解决的办法,比如github中的issue等)。官方介绍如下

resolve: {
    alias: {
      'vue$': 'vue/dist/vue.common.js'
    }
  },

大意是说:vue有两种构建方式,独立构建和运行构建。它们的区别在于前者包含模板编译器而后者不包含。而默认 NPM 包导出的是 运行时 构建。为了使用独立构建,要在 webpack 配置中添加下面的别名:添加alias:{‘vue$’:‘vue/dist/vue.common.js’}. 这个别名,同样适用于jquery,zepto这些库。

对于vue来说,如果不用别名,也可以从node_modules/vue/dist/复制vue.common.js到开发目录下,比如./src/vue下面,然后像普通的js文件一样引用, import vue from ‘./src/js/vue.common.js’ 这也是可以的。只是和使用别名相比,显的很lower

总结:

使用webpack,分为两种方式,一种是CLI(命令行方式),一种是API方式(new webpack(config)),两种方式都可以通过webpack.config.js 来配置。所以学习webpack,就是掌握webpack.config.js配置的过程。我相继介绍了entry,output,module,plugins,resolve,本来还想写写webpack的热加载HMR, 以及webpack-dev-server,browser-sync结合webpack的用法,感觉要写的内容有点多,这些内容也是实际开发中非常有用的技术。越写到后面,越觉得难于下笔。想起一句话,要想给别人一滴水,自己至少要有一桶水。前端工程自动化方案更新很快,webpack还没有来的极普及,webpack2,rollup等又出来了. 学习这些工具,是为了减轻重复劳动,提高效率。选择适合自己的方案,而不是在追寻技术的路上迷失了方向。

每天一句中文式外语

马来语

汉语	马来语
一点点	Sedikit
很多	Banyak
海滩	Pantai
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Eugene.Tom.Lee

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值