随着前端功能不断丰富,前端代码也越来越复杂难以管理。为了简化开发的复杂度,出现了众多新的处理技术:模块化、组件化、css预处理器(less,scss)等,它们提高了我们开发效率,但众多模块文件的处理打包还是会非常繁琐的。
Webpack是一个nodejs工具,它的工作方式是:把你的项目当做一个整体,通过一个给定的主文件(如:index.js),Webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,***打包为一个浏览器可识别的JavaScript文件。
- webpack {entry file/入口文件}
- {destination for bundled file/存放bundle.js的地方}
编写它的默认配置文件webpack.config.js,加入简单配置就可以简化上面的命令为webpack(非全局安装需使用node_modules/.bin/webpack):
- module.exports = {
- entry: __dirname + "/development/main.js",//已多次提及的唯一入口文件
- output: {
- path: __dirname + "/build",//打包后的文件存放的地方
- filename: "bundle.js"//打包后输出文件的文件名
- }
接上一步,继续修改该node的配置文件package.json,加入如下配置就可以使用更简单的命令npm start:
- "scripts": {
- "start": "webpack"
- ......
- },
这就是webpack的简单用法了,下面开始详细介绍webpack的配置文件。
一、.webpack的配置文件其实就是一个node的module,用commonJS风格编写。默认名为webpack.config.js,要自定义配置文件名可以在webpack命令中指定(显然不能写到配置文件中):
webpack --config ./webpackConfig/dev.config.js
默认会以当前目录为基本目录(可以通过--content-base来更换基本目录),webpack-dev-server生成的包并没有放在你的真实目录中,而是放在了内存中。可以另外开一个cmd,使用监控命令来输出到磁盘:
webpack --watch
二、entry段
可以是字符串(单入口),可以是数组(多入口),但为了后续发展,建议【使用object】。entry.value是入口文件或者内置模块名,entry.key就是output.filename中的[name]变量的值。
- entry值是字符串或者数组时时,output中没有变量[name],entry值是对象时,output中才有变量[name]。
- entry值是数组,表示把数组中的所有js文件内容都打包到一个js文件内作为入口js文件。
- entry的值是对象,表示这是一个多页面应用,对象里每一个属性代表一个入口js文件配置。属性的值还可以是数组,表示数组中的文件都会被打包到同一个文件中,一般用来打包合并第三方插件代码到同一个js文件,减少网络请求次数。
三、output段
相当于【一套规则】,所有的入口都必须使用这一套规则,不能针对某一个特定的入口来制定output规则。
- output中的常用参数包括:path/publicPath/filename/chunkFilename。
- path参数(只是给webpack用的)表示生成文件的根目录,需要传入一个【文件系统绝对路径】。path参数和后面的filename参数共同组成入口文件的完整路径。
- publicPath(是给webpack-dev-server用的)参数表示的是一个【URL路径】(指向生成文件的根目录),用于生成css/js/图片/字体文件等资源的起始路径,会自动加到资源文件的url前面。
- path参数其实是针对本地文件系统的,而publicPath则针对的是浏览器;因此,publicPath既可以是一个相对路径(相对当前代码所在文件路径),如示例中的'../../../../build/',也可以是一个绝对路径如http://www.xxxxx.com/。一般来说,我还是更推荐相对路径的写法,这样的话整体迁移起来非常方便。那什么时候用绝对路径呢?其实也很简单,当你的html文件跟其它资源放在不同的域名下的时候,就应该用绝对路径了,这种情况非常多见于后端渲染模板的场景。
- filename参数是生成出来的入口文件的【命名规则】:[name]指代entry配置的key,[hash]与版本有关,每次编译都不一样,但在同一次编译过程中生成的文件它都是一样的,[chunkhash]对每个文件生成hash,与文件内容有关,而与版本无关。
- chunkFilename参数也是用来定义生成文件的命名方式,针对除入口文件外的chunk命名。
- library参数中的变量[name]就是webpack.DllPlugin中name参数的变量[name]
四、用CommonsChunkPlugin【智能】判断提取并打包、出现在入口js文件中的公共代码
- var commonsChunkPlugin = new webpack.optimize.CommonsChunkPlugin({
- name: 'commons', // 放共有代码的chunck的唯一标识符(可以在后面filename中用[name]引用)
- filename: '[name].bundle.js', // 放共有代码的文件名的模板([name]=CommonsChunkPlugin.name),它的省略值就是[name].js
- minChunks: 4, // 设定要有4个chunk(即4个入口)都有的代码才会被纳入公共代码,默认在所有入口文件中都出现的代码才会提取。
- chunks:[],//表示需要在哪些chunk里寻找公共代码进行打包。则默认提取范围为所有的chunk。
- });//最终生成文件的url是ouput.path + CommonsChunkPlugin.filename
它的作用在于:在以入口文件为单位打包js的前提下,再进一步去提取入口文件间的共有代码:
1.如果name的值是不存在的chunk,则filename的值不能与现有的入口文件打包后的名字重复:
- 如果只给了name、filename,就是在<所有入口js>中提取<全部都有>的代码单独打包
- 如果给minChunks:2,针对<全部都有>,表示限定了在<所有入口js>中找,只有同时在任意2个入口文件中都出现的代码才会被提取单独打包
- 如果给chunks:['a','b'],针对<所有入口js>,表示只从a、b这两个入口文件中提取<全部都有>的代码单独打包
2.如果name的值是现有chunk(entry.key),就会提取共有代码放到同名的入口文件中(还可以通过filename改变对它打包后的文件名,通过minChunks规定在多少个入口文件中都出现了的代码才算共有代码,通过chunks需要提取共有代码的入口文件范围)。
五、兼容老式的jQuery插件的方式
解决使用老的jquery插件时报$/jQuery未定义错误。原因是我们在require jquery的时候,实际上并不会把jQuery对象设置为全局变量。jquery插件们找不到jQuery对象了,因为在它们各自的上下文环境里,既没有局部变量jQuery(因为没有适配AMD/CMD,所以代码内部就没有写相应的require语句引入依赖),也没有全局变量jQuery。(切记【三种方法不要混用】):
方法一、ProvidePlugin + expose-loader【用ProvidePlugin向插件中引入局部变量jQuery、用expose-loader引入全局变量jQuery】。
- var providePlugin = new webpack.ProvidePlugin({
- $: 'jquery',
- jQuery: 'jquery',
- 'window.jQuery': 'jquery',
- 'window.$': 'jquery',
- });
ProvidePlugin的机制是:【当webpack加载到某个js模块里,出现了未定义且名称符合(字符串完全匹配)配置中key的变量时】,会自动require配置中value所指定的js模块。使用ProvidePlugin还有个好处,就是,你自己写的代码里,再!也!不!用require jquery啦!。
expose-loader【将指定js模块export的变量声明为全局变量】。如果你所有的jQuery插件都是用webpack来加载的话,的确用ProvidePlugin就足够了;但总有那么些需求是只能用<script>来加载的:
- {
- test: require.resolve('jquery'), // 此loader配置项的目标是NPM中的jquery的资源路径
- loader: 'expose?$!expose?jQuery', // 先把jQuery对象声明成为全局变量`jQuery`,再通过管道进一步又声明成为全局变量`$`
- },
方法二、使用externals配置项,用来【将某个全局变量“伪装”成某个js模块的exports】,当某个js模块显式地调用var $ = require('jquery')的时候,就会把window.jQuery返回给它,但是要先在页面中写<script>标签手动加载 jquery.min.js(意味着如果某个库没有提供生产环境的文件XXX.min.js就不能用这个方法):
- externals: {
- 'jquery': 'window.jQuery',
- }
方法三、imports-loader,相当于【手动版的ProvidePlugin】,不建议使用
- {
- test: require.resolve("some-module"),
- loader: "imports?$=jquery&jQuery=jquery", // 相当于`var $ = require("jquery");var jQuery = require("jquery");`
- }
六、其它资源的打包处理方式
loader【扩展了require()方法的能力】,使它可以以模块的形式导入非js文件,处理***导出的都是javascrit。loader有两种使用方式,一种是写在require的参数里: require("!style!css!./style.css");还有一种是些到配置文件中,通过扩展名自动绑定loader。
1.对css的处理方式
less-loader模块对less文件进行编译,但并不会针对url()语法做特别的转换。如果想把url()语句里涉及到的文件(比如图片、字体文件等)也一并用webpack打包的话,就必须利用管道交给css-loader做进一步的处理。在css-loader会把它转成require()语句,从而触发在webpack配置文件里定义好可以处理这类资源的其它loader(比如url-loader,file-loader)。一般我在url()语句里都会以相对路径的方式(相对于此语句所在的less/css文件)来指定资源路径。
方法一、可以使用style-loader直接把css代码段跟js打包在一起,并自动用<style>标签插入到页面。
方法二、也可以用ExtractTextPlugin生成并加载CSS文件(结果是每个入口js都只对应一个css文件)的形式。extractTextPlugin会对每一个entry执行操作:查找entry中所有通过require()语句导入的css代码,把它们打包到特定路径(由插件的唯一参数指定)的一个css文件中:
- var precss = require('precss');
- var autoprefixer = require('autoprefixer');
- var ExtractTextPlugin = require('extract-text-webpack-plugin');
- ......
- module.exports = {
- ......
- module: {
- loaders: [
- {
- test: /\.css$/,
- exclude: /node_modules|bootstrap/,
- //关闭autoprefixer以避免你的废弃CSS代码被css-loader删除了
- //***步:loader里加入ExtractTextPlugin.extract(),这样才会提取css到独立的文件
- loader: ExtractTextPlugin.extract('css-loader?minimize&-autoprefixer!postcss-loader'),
- }
- ]
- },
- //PostCSS 是ExtractTextPlugin的插件,主要功能只有两个:***个就是前面提到的把 CSS 解析成 JavaScript 可以操作的抽象语法树结构(AST),第二个就是调用插件来处理 AST 并得到结果。
- postcss: function () {
- return [precss, autoprefixer({
- remove: false,
- browsers: ['ie >= 8', '> 1% in CN'],
- })];
- },
- ......
- plugins: [
- //第二步:指定打包css文件的文件名和路径(相对于output.path)
- new ExtractTextPlugin('[name]/styles.css'), // [name]=entry.key,表示每一个入口js文件可以对应打包一个css文件,不用这个[name]变量就会把所有入口文件中的require("XXX.css")语句引入的css文件内容都打包到同一个文件中。
- ]
- }
2.对图片和字体文件的处理方式
图片可以直接在js文件中通过require导入,也可以在css中通过url()引入,所以对应有两种打包方式:
方法一、require('!url-loader?limit=8192&name=static/images/[hash].[ext]!./imgs/login-bg.jpg')//只对单个图片文件,用于在js文件中导入图片
方法二、loader: 'url?limit=8192&name=./static/img/[hash].[ext]'//可以匹配文件后缀,从而批量处理,用于在css文件中的url()语句处理
表示图片小于8k就转成url(dataURL)直接替换原url()语句,否则打包到output.publicPath + /static/img/[hash].[ext](只是简单的字符串拼接,没有路径解析操作),然后用这个路径替换掉原url()中的路径。
file-loader的主要功能是:把源文件迁移到指定的目录(可以简单理解为从源文件目录迁移到build目录),并返回新文件的路径(只是简单的字符串拼接,没有路径解析操作),与url-loader的使用方式相同(没有limit参数)
七、用DllPlugin&DllReferencePlugin把第三方库预打包成dll
但与CommonsChunkPlugin不一样的是,它不仅仅是把公用代码提取出来,还进一步把公用代码和它的使用者(业务代码)从编译这一步就分离出来,以做到分别编译公用代码和业务代码。因为业务代码常改,而公用代码不常改,开发过程中就可以做到只编译一次公用代码,在以后的修改重编译过程中就会把公用代码排除在外。
1.编写配置文件ddl.config.js:
- const webpack = require('webpack');
- module.exports = {
- output: {
- path: 'build',
- filename: '[name].js',
- library: '[name]',//与webpack.DllPlugin.name保持一样
- },
- entry: {
- "lib": ['react','react-dom','react-router',]//需要打包成dll的公共模块
- },
- plugins: [
- new webpack.DllPlugin({
- path: 'manifest.json',
- name: '[name]',//与output.library保持一样
- context: __dirname,//与DllReferencePlugin.context保持一样,建议统一设置为项目根目录
- }),
- ],
- };
2.编译一次公共代码:
webpack --progress --colors --config ./ddl.config.js
,结果会输出两个文件一个是打包好的 lib.js,一个就是 manifest.json
3.在打包业务代码的配置文件webpack.config.js中引用:
- plugins: [
- new webpack.DllReferencePlugin({
- context: __dirname,//DllPlugin.context保持一样,建议统一设置为项目根目录
- manifest: require('./manifest.json'),
- }),
- ]
4.以后业务代码修改只需要运行:
webpack --progress --colors --config ./webpack.config.js
,而不用再执行编译公共代码的命令了。
八、自动生成html页面
每一个html-webpack-plugin的对象实例都只针对/生成一个页面(会自动向模板文件里插入依赖的js、css标签,***生成html文件),因此,我们做多页应用的话,就要配置多个html-webpack-plugin的对象实例:
- new HtmlWebpackPlugin({
- filename: `${page}/page.html`,//生成html页面的命名,最终位置:output.path + HtmlWebpackPlugin.filename
- template: path.resolve(dirVars.pagesDir, `./${page}/html.js`),//生成html需要引入的模板路径
- chunks: [page, 'commons/commons'],//指定生成的html文件中需要引入的、打包好的入口js文件,
- //如果使用了CommonsChunkPlugin,则这里必须填写存放公共js代码的chunk的name
- hash: true, // 为静态资源生成hash值
- minify: true,
- xhtml: true,
- });
另外,webpack默认支持的ejs模板,还需要在webpack配置文件中配置好相应的loader。
九、用resolve为require做请求重定向
可以提高打包效率节省时间。这里的请求是对模块的依赖,也就是一个 require 语句,而不是一个 HTTP 请求:
- resolve: {
- alias: {
- myModule: "app/my.min.js"//require('myModule')就相当于require('app/my.min.js')
- }
- }
参考资料:https://segmentfault.com/a/1190000006843916