废话不多说,webpack系列生态的更新速度真是让人感觉每天赶飞机一样,
入坑已经很久,使用过程中避免不了将生态相关的插件更新到新版,这个过程中可能会遇到很多坑,
举个最简单的例子,当webpack刚刚出4的时候看起来很好的样子,实际上相关的loader和plugin没有及时更新的时候就会遇见更新到新版本有些api更改
导致整体不兼容,开头满心欢喜结果却不得不回退版本
目前到webpack4.7之后,很多周边的插件已经趋于稳定,本文将介绍一些新的需要更改的配置
如下所示,package.json的依赖版本
{ "name": "webpacknew", "version": "1.0.0", "description": "new webpack4", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack --color --config webpack.build.config.js --progress", "unit": "jest --config test/unit/jest.conf.js --coverage", "start": "webpack-dev-server --open --config webpack.dev.config.js", "lint": "eslint --ext .js,.vue src", "dll": "webpack --color --config webpack.dll.config.js --progress" }, "author": "LeoZhang", "license": "ISC", "dependencies": { "autoprefixer": "^8.4.1", "babel-core": "^6.26.3", "babel-eslint": "^8.2.3", "babel-jest": "^22.4.3", "babel-loader": "^7.1.4", "babel-plugin-component": "^1.1.0", "babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-polyfill": "^6.26.0", "babel-preset-env": "^1.6.1", "chokidar": "^2.0.3", "clean-webpack-plugin": "^0.1.19", "css-hot-loader": "^1.3.9", "css-loader": "^0.28.11", "es6-promise": "^4.2.4", "eslint": "^4.19.1", "eslint-config-standard": "^11.0.0", "eslint-friendly-formatter": "^4.0.1", "eslint-loader": "^2.0.0", "eslint-plugin-html": "^4.0.3", "eslint-plugin-import": "^2.11.0", "eslint-plugin-node": "^6.0.1", "eslint-plugin-promise": "^3.7.0", "eslint-plugin-standard": "^3.1.0", "extract-text-webpack-plugin": "^4.0.0-beta.0", "file-loader": "^1.1.11", "html-loader": "^0.5.5", "html-webpack-plugin": "^3.2.0", "jest": "^22.4.3", "jest-serializer-vue": "^1.0.0", "jquery": "^3.3.1", "js-loader": "^0.1.1", "less": "^3.0.2", "less-loader": "^4.1.0", "mini-css-extract-plugin": "^0.4.0", "node-less": "^1.0.0", "postcss-cssnext": "^3.1.0", "postcss-loader": "^2.1.5", "preload-webpack-plugin": "^3.0.0-alpha.3", "prerender-spa-plugin": "^3.1.0", "style-loader": "^0.20.3", "tapable": "^1.0.0", "uglifyjs-webpack-plugin": "^1.2.5", "uninstall": "0.0.0", "url-loader": "^1.0.1", "vue": "^2.5.16", "vue-jest": "^2.5.0", "vue-loader": "^15.0.9", "vue-template-compiler": "^2.5.16", "vuex": "^3.0.1", "webpack": "^4.7.0", "webpack-cli": "^2.1.3", "webpack-dev-middleware": "^3.1.3", "webpack-dev-server": "^3.1.4" } }由于vue脚手架的项目使用的版本已经很低了,所以,作者整理了一套新版本的专门针对vue单页应用的配置方式
这里关于jest以及eslint的配置方式就不单独介绍了,因为他们的配置方式与前序版本几乎没有异同可以直接上手,
首先介绍一下文件结构
这个结构类似于nuxt.js的文件结构,
layout是html模版的存放目录,default是app.vue的目录,pages是所有vue单页的目录,js是app.js也就是主入口文件的地方
routers是配置路由的地方,http-interceptor是放置vue-resource全局拦截器的地方
其他地方就见名知意了,不多做解释
这里需要注意的是postcss的配置文件
由于我们使用postcss不光要把普通的css文件less文件中的兼容性前缀补全,还需要将vue文件中的css文件补全
并且后续在构建静态文件的时候我们还需要从每个vue文件中将less代码直接提取到css文件中
所以配置如图所示
接下来我们要做的工作就是看一下development和production两个场景的配置文件应该怎么编写
上代码啦:
const path = require('path'); const webpack = require('webpack'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const PreloadWebpackPlugin = require('preload-webpack-plugin'); const { VueLoaderPlugin } = require('vue-loader'); const fs = require('fs'); var config={ entry:{}, devtool: 'inline-source-map', devServer: { contentBase: '../dist', host:"localhost", //填写你自己的IP地址 port: 8080, hot: true, inline: true, openPage:'html/main.html', progress:true, proxy:{ "/": { target: "http://www.baidu.com", } } }, externals: { 'vue': 'Vue', 'vue-router':'VueRouter', 'vue-resource':'VueResource', 'mint-ui':'MINT' }, mode:'development', module:{ rules:[ { test:/\.html$/, use:'html-loader' }, { test:/\.js$/, use:[ {loader:'babel-loader'} ] }, { test:/\.css$/, use:[{loader:'style-loader'},{loader:'css-loader',options:{sourceMap:true}},'postcss-loader'] }, { test:/\.less$/, exclude:/node_modules/, use:[{loader:'style-loader'},{loader:'css-loader',options:{sourceMap:true}},'postcss-loader','less-loader'] }, { test:/\.vue$/, use:[{ loader:'vue-loader', options: { postcss: [] } }] }, { test:/\.(gif|jpeg|jpg|png)\??.*$/, use:{ loader:'file-loader', options: { name:'[name].[ext]', //outputPath为生成物理文件的路径 outputPath:'img', //publicPath为自定义html界面中的显示路径 publicPath:'../img' } } } ] }, plugins:[ new VueLoaderPlugin(), new webpack.NamedModulesPlugin(), new webpack.HotModuleReplacementPlugin(), new webpack.optimize.SplitChunksPlugin({ cacheGroups:{ default:{ minChunks: 2, priority: -20, reuseExistingChunk: true, }, //打包重复出现的代码 vendor: { chunks: 'initial', minChunks: 2, maxInitialRequests: 5, minSize: 0, name: 'vendor' }, commons: { name: "commons", chunks: 'initial', minChunks: 'Infinity' } } }), new webpack.optimize.RuntimeChunkPlugin({ name: "mainfest" }), new CleanWebpackPlugin(['dist']), ], output:{ filename:'js/[name].bundle.js', path:path.join(__dirname,'dist'), publicPath:'/' }, } var files = fs.readdirSync('./src/layout'); for(var item of files){ var chunk = item.substring(0,item.indexOf('.')); config.entry[chunk] = `./src/js/${chunk}.js`; config.plugins.push(new HtmlWebpackPlugin({ title:`${chunk}`, favicon:'./src/img/favicon.ico', template:`./src/layout/${chunk}.html`, filename:`html/${chunk}.html`, chunks:[chunk,'vendor','mainfest'], })) } config.entry['vendor']=['vuex','babel-polyfill']; config.plugins.push(new PreloadWebpackPlugin({ rel: 'preload', include: 'allChunks', // or 'initial' })) module.exports = config;
观察上述代码,简单做个解释webpack4以后在配置中
需要说明当前的代码环境,参数为mode,默认值是production,dev环境时需要手动改成development
这个参数相对webpack3以前的版本提供了很大的能力,
webapck会根据两个参数自动区分两种环境执行相应的编译方式
还有就是CommonsChunkPlugin已经在webpack4中移除,新的配置方式可以参照我的配置文件来使用
由于我的配置文件入口不是完全针对单页应用写的,所以entry部分使用了fs模块进行了动态处理,
默认使用单页无区别,只有vendor部分在最下层进行配置
然后看一下打包发布环境的配置文件有什么不同的地方
const path = require('path'); const webpack = require('webpack'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin') const PreloadWebpackPlugin = require('preload-webpack-plugin'); const { VueLoaderPlugin } = require('vue-loader') const fs = require('fs'); var config={ entry:{}, externals: { 'vue': 'Vue', 'vue-router':'VueRouter', 'vue-resource':'VueResource', 'mint-ui':'MINT' }, mode:'production', module:{ rules:[ { test:/\.html$/, use:'html-loader' }, { test:/\.vue$/, use:[{ loader:'vue-loader', options:{ extractCSS: true, postcss: [], } }] }, { test:/\.js$/, //exclude:/node_modules/, use:[ {loader:'babel-loader'} ] }, { test:/\.css$/, //exclude:/node_modules/, use:[{loader:'style-loader'},{loader:'css-loader',options:{sourceMap:true}},'postcss-loader'] }, // { // test:/\.less$/, // exclude:/node_modules/, // use:ExtractWebpackPluginLess.extract({ // fallback:'style-loader', // use:[{loader:'css-loader',options:{minimize:true}},'postcss-loader','less-loader'] // }) // }, { test:/\.less$/, use:[ MiniCssExtractPlugin.loader, {loader:'css-loader',options:{minimize:true}},'postcss-loader','less-loader' ] }, { test:/\.(gif|jpeg|jpg|png)\??.*$/, use:{ loader:'file-loader', options: { name:'[name].[ext]', //outputPath为生成物理文件的路径 outputPath:'img', //publicPath为自定义html界面中的显示路径 publicPath:'../img' } } } ] }, plugins:[ new VueLoaderPlugin(), new webpack.optimize.SplitChunksPlugin({ cacheGroups:{ default:{ minChunks: 2, priority: -20, reuseExistingChunk: true, }, //打包重复出现的代码 vendor: { chunks: 'initial', minChunks: 2, maxInitialRequests: 5, minSize: 0, name: 'vendor' }, commons: { name: "commons", chunks: 'initial', minChunks: 'Infinity' } } }), new webpack.optimize.RuntimeChunkPlugin({ name: "mainfest" }), new MiniCssExtractPlugin({ filename: 'css/[name]_[chunkhash:6].css' }) ], output:{ filename:'js/[name].[chunkhash:6].js', path:path.join(__dirname,'dist'), publicPath:'../' }, } var files = fs.readdirSync('./src/layout'); for(var item of files){ var chunk = item.substring(0,item.indexOf('.')); config.entry[chunk] = `./src/js/${chunk}.js`; config.plugins.push(new HtmlWebpackPlugin({ title:`${chunk}`, favicon:'./src/img/favicon.ico', template:`./src/layout/${chunk}.html`, filename:`html/${chunk}.html`, chunks:[chunk,'vendor','mainfest'], })) } config.entry['vendor']=['vuex','babel-polyfill']; config.plugins.push(new PreloadWebpackPlugin({ rel: 'preload', include: 'allChunks', // or 'initial' })) module.exports = config;
当mode为production的时候webpack4是不需要引入压缩插件的,也不需要使用旧的api进行压缩,js会自动被混淆压缩,
样式文件可以使用css-loader进行压缩,
这里需要注意的是,
我已经把使用了extract-text-webpack-plugin的部分都进行了替换,这个是因为
webpack4之后extract-text-webpack-plugin默认的版本已经不兼容,他最新的4.0.0beta版本在正常情况下是可以使用的
不过当我们在vue中使用
const router = new VueRouter({ routes:[ {path:'/',component:resolve => import('../pages/index.vue'),name:'index'}, {path:'/test',component:resolve => import('../pages/test.vue'),name:'test'}, {path:'/user',component:resolve => import('../pages/user/user.vue'),name:'user'} ] })
如上代码这种lazyload形式加载单页的时候
在提取css之后就会报错,
并且在配置文件中我还多加了一个
VueLoaderPlugin
这个插件就是vue-loader出15版本之后进行的一个大的变化,这个变化导致了配置方式也与之前不同,
在提取vue中的样式文件时,不在从option中进行配置,并且vue-loader15必须引入
VueLoaderPlugin才能保证他的正常运转
如果使用最新版本的话,需要将extract-text-webpack-plugin替换为官方文档提供的
mini-css-extract-plugin详细配置按照我的配置使用即可
这里需要说一下的地方是路由懒加载机制
这个在写纯单页应用的时候应该是必备的配置,
我们知道可以使用splitPlugin来将第三方模块拆分出去,
不过这里再强调一点
关于打包优化的地方,个人认为的就是像vue系列生态圈中本身有一些可以使用cdn资源的
如图,这种可以直接在界面cdn加载的一部分插件最好直接在界面引入
webpack中可以使用
如图的配置方式,这样可以把界面引入的包整合到开发代码中以import的形式进行编写,
其他的公共插件可以放在vendor中进行拆分,
回到正题,这样的拆分实际已经满足了大部分的中小型webapp
不过当vue单页应用中vue模块的数量及其庞大的时候这一部分vue文件如果不做处理仍然会打包到一个js文件
这样就算js代码中做多少异步处理,当首页加载的时候都必须读取完vue部分的js代码才能进行界面渲染
所以当vue界面足够多的时候如果能把首页的代码单独加载,并且每个大模块的代码进行一个拆分这样就能实现与普通网页一样的加载速度,加上单页应用的体验感是一件完美的事儿
在vue-router中给我们提供了一个解决方案
就是可以使用import()动态倒入的形式来进行路由的懒加载
import语法默认是可以通过babel使用的,不过import()是需要
babel-plugin-syntax-dynamic-impor
这个组件来进行实现的,安装组建后还需要在babelrc中加入如下配置"plugins": ['syntax-dynamic-import']
这样可以才能在js中使用import()语法
如果你还想使用async/await语法还需要使用
babel-polyfill
插件,前序的这些工作都是动态路由的准备工作按照配置完成后我们可以启动项目进行检查
const router = new VueRouter({ routes:[ {path:'/',component:resolve => import('../pages/index.vue'),name:'index'}, {path:'/test',component:resolve => import('../pages/test.vue'),name:'test'}, {path:'/user',component:resolve => import('../pages/user/user.vue'),name:'user'} ] })按照如下配置的界面编译之后
生成了012三个文件,这三个其实就是三个vue的js代码
并且我们看首页运行的样子
默认只在界面引入了一个js文件,
当我们点击跳转之后
2.bundle.js动态的加载到了代码中
这样就实现了将vue界面分块进行加载,可以快速的渲染界面而不用等待所有的vue内容读取完毕再去进行渲染
当然,实例中的内容有个不好的地方就是动态路由的首页还应该使用import进行普通引用,这样首页直接跟随main.js加载不需要单独异步加载
其他模块如果不想单独形成文件还可以参照官方文档的介绍进行分组拆分
当然这种方式的拆分不是极致的拆分,当我们进行代码build的时候
可以看一下使用新的minicss插件实现的效果
这个组件帮助我们把每个vue中的less等样式代码进行了抽离,实现了extract-text的功能,
当然这个地方正常使用extract-text插件也是可以的不过在最新版的配置文件中使用此插件抽离文件后动态路由就会报错
所以我们要使用
mini-css-extract-plugin
来进行代码的抽离这个配置对于单页应用的大部分需求已经足够了,当然不同项目有不同的需求
需要我们针对不同的场景进行不同的插件搭配,必要的时候可能还需要自己进行编写本地的插件
尾声:现在大多数人过分的依赖于现成的demo以及脚手架,这个对于开发来说是提升效率的事情,但是脚手架等资源实际上都有能力有限的时候,比如vue-cli的单页应用demo使用的依赖版本已经有点低的过头了,虽然不影响开发,但是为了不被前端如此快的更新速度淘汰,我们需要了解webpack这个工具的核心原理,以及他的使用方式,我们可以围绕webpack搭配不同的配置来实现不同的项目开发,这样才能深刻理解目前的前端架构,并且自身形成架构思想,并且,其实webpack的手动配置过程并不复杂,也不耗时,很多人看到密集的配置文件会产生恐惧而根本没有一行一句的去查阅webpack的官方文档,所以觉得他难,实际上如果踏踏实实的一步一步去做,对于前端工程化开发,成为前端架构师这些事离每个前端工程师实际上都是很近的
最后由于github一直传不上去,就只能放在百度云盘了,附上云盘链接,项目的源码拿去自己玩吧
链接:https://pan.baidu.com/s/1_M4OLJeCRxVLBG2UdW-18g 密码:3u0o