转自IMWeb社区,做者:IMWeb团队,原文连接javascript
webpack的核心是一切皆模块,因此它其实本质上就是个静态模块打包器。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图,其中包含应用程序须要的每一个模块,而后将全部这些模块打包成一个或多个 bundle。官网显示的这幅图很形象地描述了这个过程:css
那么接下来咱们来咱们从零开始一步步完成一个完整项目的配置,每部分配置除了会列出基础配置,还会给出一些额外须要注意的事项,也是我在项目中的踩坑总结。html
先贴一下项目目录结构:java
- src
- common 公用代码库
- pages
- [活动名称]\_[h5|pc]
- index.js
- index.html
复制代码
首先咱们看看项目的打包入口如何配置: webpack打包入口支持但入口和多入口,但入口文件只限于js文件(听说webpack5在考虑增长HTML文件和CSS文件做为入口)。 多入口时,给entry传入对象便可,以下所示, 其中对象的key值则是入口的name:node
const config = {
entry: {
pageOne: './src/pageOne/index.js',
pageTwo: './src/pageTwo/index.js',
pageThree: './src/pageThree/index.js'
}
};
复制代码
显然,咱们的项目页面数量是未知的,将全部页面都枚举在配置里显然是不合理的,因此能够定义getEntry()
方法来遍历指定文件夹获取入口。webpack
const webpack = require("webpack");
const glob = require("glob");
function getEntry() {
const entry = {};
//读取src目录全部page入口
glob.sync('./src/pages/*/*/index.js')
.forEach(function (filePath) {
var name = filePath.match(/\/pages\/(.+)\/index.js/);
name = name[1];
entry[name] = filePath;
});
return entry;
};
module.exports = {
mode: 'development',
// 多入口
entry: getEntry(),
}
复制代码
不管是单入口仍是多入口,都只能指定一个输出配置。咱们看看项目的output
配置web
output: {
publicPath: CDN.js,
filename: '[name].[chunkhash].js',
chunkFilename: '[name]_[chunkhash].min.js',
path: distDir,
},
复制代码
一般,dev环境时,不用配置publicPath,此时静态资源的引用路径相对于HTML页面。而生产环境时,把publicPath的值设为CDN的目录路径就能够了。 这里配置有几点须要注意的:json
这里说了是多端多页面项目,多端只的就是PC和H5两端,那么这就意味着各端的CDN资源路径是不同的,因此publicPath值也应该不同。如何动态设置publicPath呢? webpack 提供了__webpack_public_path__
来动态设置publicPath,咱们在入口文件的最顶部进行定义便可,以下所示index.js
。segmentfault
__webpack_public_path__ = myRuntimePublicPath; // 必定要写在最顶部
复制代码
hash:以项目为维度生成的hash值,项目所有文件都共用一个hash值 chunkhash: 以chunk为维度生成的hash值,不一样入口生成不一样的chunkhash值 contenthash: 根据资源内容生成的hash值 通常是用chunkhash,contenthash也有使用场合,好比在mini-css-extract-plugin插件配置使用,后面会详细讲到。windows
配置好了输入输出后,咱们就须要来配置对模块内容如何进行处理。webpack 只能理解 JavaScript 和 JSON 文件。loader 让 webpack 可以去处理其余类型的文件,并将它们转换为有效模块。
须要引入babel的话,咱们就须要使用babel-loader
babel-loader
{
test: /\.js$/,
loader: 'babel-loader',
include: [path.resolve(rootDir, 'src')],
},
复制代码
使用babel时须要注意,Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,好比Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(好比Object.assign)都不会转码,若是要使用须要引入polyfill。 引入polyfill 的方式有不少种,这里推荐babel transformtime
+runtime
,transform-time
的做用是将遇到须要转化的语法时引入polyfill,而run-time
则是提供polyfill, 这样就能够作到按需引入,而不是全部的都打包进去。因此babel的配置以下:
{
"presets": [
[
"env",
{
"browsers": ["last 5 versions", "> 5%", "Android > 4.3"]
}
],
"stage-2"
],
"plugins": [
"transform-runtime"
]
}
复制代码
对于css模块,经常使用的loader有style-loader和css-loader。 css loader
用来处理js文件中引入的css模块(处理@import和url()),style-loader
是将css-loader
打包好的css代码以<style>
标签的形式插入到html文件中。 这个项目用到了sass和post-css,因此这里还引入了sass-loader和postcss-loader。由于webpack对于loader的调用是从右往左的,因此配置以下:
{
// 增长对 SCSS 文件的支持
test: /\.scss|\.css/,
// SCSS 文件的处理顺序为先 sass-loader 再 css-loader 再 style-loader
use: [
'style-loader',
{
loader: 'css-loader',
// 给 css-loader 传入配置项
options: {
importLoaders: 2,
},
},
'postcss-loader',
{
loader: 'sass-loader',
},
],
},
复制代码
若是你也使用了sass-loader,有个问题可能须要注意。当你的index.scss里@import了其余scss文件好比a.scss时,若是a.scss里使用了url(),且里面的路径是相对路径,那么在sass-loader 处理事后给css-loader处理时就会报错,找不到url()里指定的资源。这是为何呢? 实际上,当sass-loader处理时,会将index.scss里@import的A.scss合并进来,最后只输出index.scss。但A.scss里的url()原本是以A.scss写的相对路径,这样合并又不对url()作处理的话,就致使了合并后没法定位到url()里的资源。对于这个问题,有两种解决办法:
resolve-url-loader
,将 resolve-url-loader
设置于 loader 链中的 sass-loader 以前,就能够重写 url。可是这个办法有个问题,那就是 resolve-url-loader
不识别scss文件的行内注释语法,即// 注释
,这个问题使得接入一些已存在的公共样式库时会存在问题,目前还在研究是否有其余loader能够解决,你们有较好的解决办法也能够一块儿讨论。对于图片等其余资源,咱们通常使用file-loader进行处理,它实现的功能很简单:
{
test: /\.(gif|png|jpe?g|eot|woff|ttf|pdf)$/,
loader: 'file-loader',
},
复制代码
尽管webpack既支持commonjs规范也支持AMD规范。可是咱们如何经过import 的方式引入AMD 模块或者其余不支持模块化的库呢? 咱们项目里使用到了zepto,这里就以zepto为例,在import zepto时会报错
Uncaught TypeError: Cannot read property 'createElement' of undefined
复制代码
这就是由于zepto只使用了AMD 规范导出模块。解决全部这类问题其实很简单,只须要使用script-loader
和exports-loader
便可:
{
test: require.resolve('zepto'),
use: ['exports-loader?window.Zepto','script-loader']
}
复制代码
script-loader
用 eval 的方法将 zepto 在引入的时候执行了一遍,此时 zepto 库已存在于 window.Zeptoexports-loader
将传入的 window.Zepto 以 module.exports = window.Zepto 的形式向外暴露接口,使这个模块符合 CommonJS 规范,支持 import 这样咱们就能够直接import $ from 'zepto'
了,其余AMD 模块或者其余不支持模块化的库也相似。插件机制是webpack的核心之一,插件(Plugins)是用来拓展webpack功能的,它们会在整个构建过程当中生效,执行相关的任务。咱们通常使用插件来完善咱们的构建流程,webpack有许多插件可用,这里只挑两个必备插件来详细说明
前面有说过,目前webpack的打包入口只支持JS文件,因此它打包输出的也是JS文件,那么如何把这个JS文件引入咱们的html中去呢,手动引入没法监测到hash值的变化,确定是不OK的。所以咱们就用到了html-webpack-plugin
这个插件,它会将打包好的文件自动引入到指定的html中去,并将html文件输出在指定位置。 html-webpack-plugin
使用时,一个实例操做只能一个html,因此对于多页面项目,咱们须要创造多个实例,结合前面的getEntry方法,咱们能够在遍历获得entry的时候进行实例化,获得htmlPluginArray
const htmlPluginArray= [];
function getEntry() {
const entry = {};
glob.sync('./src/pages/*/*/index.js')
.forEach(function (filePath) {
var name = filePath.match(/\/pages\/(.+)\/index.js/);
name = name[1];
entry[name] = filePath;
// 实例化插件
+ htmlPluginArray.push(newHtmlWebpackPlugin({
+ filename: './' + name + '/index.html',
+ template: './src/pages/' + name + '/index.html',
+ }))
});
return entry;
};
// 配置plugin,此处省略其余配置代码
plugins: [
htmlPluginArray
],
复制代码
前面使用css loader 和 style-loader对css文件进行处理后,css文件被做为模块也打包在了js文件中。实际生产环境,咱们固然是但愿js文件和css文件分离的,因此这里就可使用mini-css-extract-plugin
。 具体配置以下:
module: {
rules: [
{
// 增长对 SCSS 文件的支持
test: /\.scss|\.css/,
// SCSS 文件的处理顺序为先 sass-loader 再 css-loader 再 style-loader
use: [
{
+ loader: MiniCssExtractPlugin.loader,
+ options: {
+ publicPath: CDN.css,
},
},
{
loader: 'css-loader',
// 给 css-loader 传入配置项
options: {
importLoaders: 2,
},
},
'postcss-loader',
{
loader: 'sass-loader',
},
],
}
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
chunkFilename: '[name].[contenthash].css',
}),
],
复制代码
这里之因此设置为contenthash
,是用来解决抽离css文件后,js文件变化致使的css文件hash值变化的问题
resolve配置规定了webpack如何寻找各个依赖模块。 前面讲到的alias就是在这里配置。在资源引用时,若是资源引用路径太深,又比较经常使用,咱们则能够定义路径别名,例如:
alias: {
h5: path.resolve(__dirname, 'src/common/h5/'),
pc: path.resolve(__dirname, 'src/common/pc/'),
}
复制代码
咱们就能够直接在代码中这样引用了:
import Utility from 'h5/util';
复制代码
webpack-dev-server
是开发时的必备利器,它能够在本地起一个简单的 web 服务器,当文件发生变化时,可以实时从新加载。webpack-dev-server
的配置也很简单:
devServer: {
publicPath: '/act/',
port: 8888,
hot: true,
},
复制代码
启动webpack-dev-server后,在目标文件夹中是看不到编译后的文件的,实时编译后的文件都保存到了内存当中
hot
设置为true是启用 webpack 的 模块热替换(HMR)功能,但这里注意必需要添加插件webpack.HotModuleReplacementPlugin
才能彻底启用 HMR
publicPath路径下的打包文件能够在浏览器中访问,能够这么理解,webpack-dev-server打包的内容是放在内存中的,这些打包后的资源对外的的根目录就是publicPath。 默认 devServer.publicPath 是 '/',因此你的包(bundle)能够经过 http://localhost:8888/bundle.js
访问。当咱们要设置具体路径时记得要以/
开头,如上面配置所示,设置了publicPath: '/act/'
后bundle的访问路径则变成了: http://localhost:8888/act/bundle.js
注意:当这里的publicPath和output的publicPath同时设置时,这里的优先级更高
一般,咱们本地开发环境和生产环境会采用不一样的配置文件,发布上线时,咱们会对资源进行压缩、合并等优化,但在本地开发时,为了提升构建速度,方便调试代码,咱们则会省去这些优化配置,与此同时,咱们更加关注模块热更新、localhost server等等。因此通常会为每一个环境编写彼此独立的 webpack 配置,这里项目的webpack配置文件以下,其中webpack.common.js是用来放dev和dist里的公共配置:
这里会用到webpack-merge
工具进行配置的合并。 好比webpack.common.js
内容以下:
module.exports = {
module: {
rules: []
}
};
复制代码
webpack.dev.js
的则可使用webpack-merge合并配置:
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
devtool: 'inline-source-map',
devServer: {
// dev 配置
}
});
复制代码
因此咱们能够在package.json添加咱们的webpack启动命令以下:
"scripts": {
"dist": "cross-env NODE_ENV=production webpack --config webpack.dist.js",
"dev": "webpack-dev-server --config webpack.dev.js",
},
复制代码
其中,
cross-env NODE_ENV=production
是用来设置node环境变量,设置环境变量的目的是由于许多库自身会判断当前环境,并在生产环境下作一些优化处理,而用cross-env来设置是为了兼容windows系统。
到这里,咱们项目已经能起来了,可是做为一名合格的程序猿,咱们固然要探索更优实践。webpack有哪些经常使用的优化措施呢?
webpack 提供了两种动态加载的语法。第一种,也是推荐选择的方式是,使用符合 ECMAScript 提案 的 import() 语法 来实现动态导入。第二种,则是 webpack 的遗留功能,使用 webpack 特定的 require.ensure。 import() 会返回一个 promise,在代码中全部被import()的模块,都将打成一个单独的包,在浏览器运行到这一行代码时,就会自动请求这个资源,实现动态加载。 ** 使用import()时应该注意如下几点: **
为了合理利用浏览器缓存,通常会将不常变更的第三方库以及公共代码和业务代码分开打包 因此通常项目的打包策略为:
对于分包方式,webpack 4 移除 CommonsChunkPlugin,取而代之的是optimization.splitChunks 让咱们看看这里怎么配置:
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/
name: 'vendor',
chunks: 'initial',
priority: 2,
minChunks: 2
},
common: {
test: /.js$/,
name: 'common',
chunks: 'initial',
priority: 1,
minChunks: 2
}
}}
复制代码
注意抽离出来的代码要在HTML文件里引入
因为项目包含两端代码,H5\PC部分依赖是独立的,单纯的从项目层面进行公共模块的抽离是不行的。 因此这里得详细设置公共库和代码的匹配规则。好比咱们项目PC用的JQ,H5用的zepto,就能够配置
optimization: {
splitChunks: {
cacheGroups: {
h5common: {
test: /zepto/,
name: 'h5common',
chunks: 'initial',
priority: 1,
minChunks: 1,
},
},
},
},
复制代码
配置loader时,咱们能够经过exclude设置哪些目录下的文件不进行处理,经过include精确指定只处理哪些目录下的文件,以此来缩小处理范围,加快构建速度。
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/,
include: path.resolve(__dirname, 'src')
}
]
}
复制代码
当咱们引用模块时,若是出现import ‘zepto’这样的依赖引入方式,webpack会默认从当前目录往上逐层查找是否有node_modules
,而后在node_modules
下查找是否存在指定依赖。 为了减小搜索范围,咱们能够经过设置resolve.modules
来告诉 webpack 解析这类依赖时应该搜索的目录
resolve: {
modules: [path.resolve(rootDir, 'node_modules')],
},
复制代码
这篇文章以多端多页面项目为例,深刻讲解了如何初始化项目webpack配置,这些实践不只适用于这个项目,对于多页面项目和普通项目也一样适用。