这半周作了一件事,将手上的前端项目从使用过去dva脚手架自带的roadhog2.x打包工具迁移至使用webpack4.x打包,成功让本人掉了很多头发。javascript
先说背景,目前主要作的项目其实都是兄弟姐妹系统(是的没错,就是前端圈位于鄙视链底部的TO B系统),基于早期的JSP多页应用使用React进行拆分重构;技术选型采用的是react
+ antd
+ dva
。我从学校回来接入的时候,项目已经开始一段时间了。当时dva
脚手架仍是带的roadhog2.x
构建包工具,它是在webpack
之上的封装,大致上就是提供一个开箱即用的傻瓜式构建方案,技术自己是没有问题的,可是难受就难受在相关文档不是那么全,并且扩展性不足(固然若是你是随便改底层的带哥,当我没说...);好比roadhog2.x
移除了过去支持的dll
配置项,同时sorrycc老哥重心也转移到umi
的开发维护上了...这边随着公司项目版本不断迭代,代码量的日渐增加以及一些工具、第三方库的引入致使项目构建愈来愈慢,拖了一万年的我终于忍不住,开始了将roadhog2.x
对应构建方式迁移至webpack4.x
的工做。css
一语蔽之,它们三个就是同一份代码在不一样阶段的产物或者说别名,源文件是咱们本地coding的代码,chunk则是源代码在webpack编译过程当中的中间产物,最终源代码打包出来的就是bundle文件。html
webpack 4.x
要再装一个webpack-cli
依赖配合,能够经过npm i webpack webpack-cli -D
一块儿安装。前端
撸过webpack 4.x
的兄弟姐妹确定有见过一个WARNING
:The 'mode' option has not been set, webpack will fallback to 'production' for this value.
。如今咱们再进行webpack
命令行操做的时候须要指定模式--mode production/development
,若是没有指定会使用默认的production
。两个模式下webpack
会自动地进行相应的优化操做,好比指定production
会自动进行代码压缩等等。java
过去咱们还须要指定入口文件好比下面这样的:node
entry: {
index: ['babel-polyfill', path.resolve(__dirname, './src/index.js')],
}
复制代码
如今则根本不须要配置了,由于默认使用的就是这个模块。react
emm,这个通常就不能不设置了,若是每次打包后的资源文件(html,js,css)名相同,因为强缓存的缘由,咱们部署在服务器(好比Nginx)上的项目并不会更新,虽然这也能够经过Nginx配置,但其实没啥必要,咱们只要使每次打出来的文件名不一样(设置hash),浏览器访问的时候就会从新去请求最新的资源。好比:jquery
output: {
filename: '[name].[hash:8].js',
path: path.resolve(__dirname, './dist'),
publicPath: '/'
}
复制代码
做为开发者,咱们在开发环境下debug每每须要根据控制台的报错信息定位具体文件,若是没有source-map
,咱们获得的将是一段处理过的压缩代码,没法定位到具体文件具体代码行,这样很是不利于调试,在webpack4.x前,咱们须要手动配置:webpack
module.exports = {
devtool: 'source-map'
}
复制代码
而如今在webpack4.x中经过指定模式--mode development
将会自动开启该功能。ios
在开始讲迁移的踩坑记录前,我先简要讲讲通常webpack的配置文件由哪些部分组成:
1. entry
,即咱们的总入口文件,咱们要打包总得把从哪里开始告诉webpack吧?一般这个文件都在src/index.js
。举个例子,你配置完全部的组件之后,确定有一个顶层爹,中间嵌套的用来提供Provider的也好,配置路由的也好,最终都是将这个爹经过选择器挂载到你的根节点上,相似下面这样:
ReactDOM.render(<Father />, document.getElementById('root')); 复制代码
固然我这边项目看了下以前貌似直接拿的ant-design-pro
v1版本的改的(裂开,如今都到v4了)...入口文件dva有本身的封装,v1版本的大概长下面这样:
const app = dva({
history: createHistory(),
});
app.use(createLoading());
app.model(require('./models/global').default);
app.router(require('./router').default);
app.start('#root');
export default app._store;
复制代码
2. webpack如今有文件解析了,可是咋解析,这个方案须要你告诉webpack。咱们须要在module
配置项下的rules
内经过正则断定文件类型而后根据该类型选择不一样的loader
来进行不一样编译,下面以解析js
和jsx
文件为例子:
{
test: /\.(js|jsx)$/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true, // 默认false,开启后,转换结果会被缓存,再次编译优先读取缓存内容
}
},
exclude: /node_modules/, // include指定包含文件,exclude除去包含文件
}
复制代码
3. 指定了不一样类型文件的处理方式之后,咱们可能还想要作一些额外的扩展,好比代码压缩、生成link
、script
标签、图片拷贝到存放静态资源的目录、编译过程根据库依赖关系自动引入依赖等等。这时候就须要配置plugins
配置项了,拿生成script
标签引入咱们的bundle
为例:
new HtmlWebpackPlugin({
template: path.join(__dirname, '/src/index.ejs'), // 参照模板,bundle会在这个模板中经过插入script的方式引入
filename: 'index.html',
hash: true, // 防止缓存
})
复制代码
4. 最终咱们获得的编译结果须要一个输出,能够经过配置项中的output
来控制:
output: {
filename: '[name].[hash:8].js',
path: path.resolve( __dirname, './dist' ),
chunkFilename: '[name].[hash:8].async.js', // 按需加载的异步模块输出名
publicPath: '/'
}
复制代码
webpack4.x
中推荐使用的CSS压缩提取插件,最终会在咱们提供的模板HTML中插入一个link标签引入编译后的样式文件;过去版本中的webpack
使用的是extract-text-webpack-plugin
,可是本人最初尝试使用的时候,报了Tapable.plugin is deprecated. Use new API on .hooks instead
问题,去github对应项目下能够发现以下提示:
loader
支持不少种写法,具体看实际场景,简单配置的能够直接写在一个字符串内好比loader: 'style-loader!css-loader'
,匹配顺序从右向左。复杂配置的推荐仍是用数组,虽然字符串也能够经过相似GET请求那种拼接方案来设置配置项,可是可阅读性太差了。在数组中,具体loader
咱们能够经过对象写法来配置,看上去就清晰明了,例子以下:
module.exports = {
module: {
rules: [
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
hmr: true,
}
},
{
loader: 'css-loader',
},
]
]
}
}
复制代码
emm...这实际上是我当时睿智了,想一想都知道没有装less
咋处理呢,经过npm i -D less
解决。
在我本身鼓捣小DEMO的时候,用style-loader
都是没啥问题的,不过在迁移的项目里,加上就会报错。这里就要理清一个问题,style-loader
到底负责的内容是什么,根据webpack
官方的文档说明,它最终会将处理后的CSS以<style></style>
的DOM结构写入HTML。而后思考一下前面的mini-css-extract-plugin
功能,它俩最终想要的效果是一致的,会有冲突,因此咱们移除style-loader
便可。关联issue能够看下这个issue。
最开始的时候,我对样式的处理都是经过正则test: /\.(css|less)$/
写在一块的,可是一直编译报错,估计是具体配置项不能共享或者有冲突,分开单独作处理问题解决。
以前roadhog
中在webpackrc.js
中的处理是:
["import", { "libraryName": "antd", "libraryDirectory": "es", "style": true }],
复制代码
改用webpack4.x
后,在.babelrc
文件中一样写入以上配置,可是要把style
的值设置为css
,修改后,antd
样式成功载入。
HOC的装饰器写法,须要配置babel支持。如今webpack通常都不直接在自身配置文件里面设置babel了,而是将babel的配置信息抽出来放到.babelrc
内以JSON格式维护,在plugins
内加入下面这段便可:
["@babel/plugin-proposal-decorators", { "legacy": true }],
复制代码
在转webpack4.x
的过程当中发现有babel报错的问题,后查发现是兼容性的坑,因此将有问题的怼到了babel7.x
版本配合webpack,7.x版本的babel都带上了@
前缀。
由于项目内的样式是按照css-modules的规范来写的,因此编译的时候也须要开启支持,在css-loader
的options
内设置modules: true
便可。
如今生成class
名能够方便咱们定位调试一些样式,好比你想在控制台Element
的DOM树结构里ctrl + F
检索对应样式类,而后直接进行调试。这里就须要接着上面的css-modules配置调整了:
{
loader: 'css-loader',
options: {
importLoaders: 1, // 设置css-loader处理样式文件前 容许别的loader处理的数量 默认为0
modules: {
localIdentName: '[name]_[local]_[hash:base64:5]', // 修改生成的class的名称 name就是文件名 local对应类名 [path]支持路径
}
}
},
{
loader: 'less-loader',
options: {
javascriptEnabled: true,
}
}
复制代码
当时改的时候有一个坑,即不能像下面这样设置class:
改进后先后对比:
这是我迁移得差很少的时候忽然发现的,即部分场景出现了React is not defined
的报错,而后定位了代码发现的确会缺乏依赖,好比我在一个组件中引入了antd
的UI组件,即使只是对引入的UI组件进行纯函数的操做,但antd
自己也有对React
的依赖,那为何以前roadhog
处理就没有问题呢?确定是有额外的插件作了骚操做!最后在stackoverflow上看到一个老哥的回答,又去webpack官方文档对比了下,靠谱!加入对应插件后解决该问题。
new webpack.ProvidePlugin({ // 根据上下文,在须要依赖React处,自动引入
"React": "react",
})
复制代码
不吹不黑,这东西是我迁移过程当中遇到最坑的问题...最先的时候我曾经在webpack输出的内容里看到Router
的warning,可是后面就消失了,形成当时走了弯路,其实罪魁祸首是这个项目在.webpackrc.js
内禁用了import()
这种按需动态引入的方式,就直接致使了我编译出来的文件其实除了根路由的内容,别的内容缺失。找到根源,再定位解决,就容易了,看下roadhog
内对应配置项是用什么处理的便可,最后引入babel-plugin-dynamic-import-node-sync
解决:
webpack4.x中,该用于抽离不一样入口文件公共部分的插件已被移除,改用optimization
配置项下的splitChunks
选项使用。
用来在命令行可视化webpack编译进度的插件:
new ProgressBar({
format: ' build [:bar] ' + chalk.green.bold(':percent') + ' (:elapsed seconds)',
clear: false
})
复制代码
用来设置输出颜色的“粉笔”,经过const chalk = require('chalk');
引入。
自定义输出提示工具:
new FriendlyErrorsWebpackPlugin({
compilationSuccessInfo: {
messages: [`You application is running here http://localhost:3000`],
},
})
复制代码
这个库主要是用来进行webpack分包的,针对不一样环境和功能,咱们彻底能够将webpack配置文件拆成多个,好比base
文件里就是分包的webpack会共用的配置信息,dev
里就是webpack-dev-server
和development
模式下的配置信息,prod
放生产部署的压缩优化配置,dll
进行代码预编译,提高首次编译后的代码编译效率,通常结构以下:
webpack携带的dll预编译插件,它会将几乎不改动的库进行编译(由你指定),而后生成一个编译后的js
以及负责告知webpack以后编译过程哪些内容不须要再处理的json
。
查找可用端口。
开发环境编译时长从以前的半分到一分钟不等到如今的10s左右:
进行生产打包部署的替换。毕竟迁移后的打包结果还须要评估依赖缺失的风险,这中间须要通过大量测试及灰度验证...
{
"presets": [
"@babel/preset-env",
"@babel/preset-react",
],
"plugins": [
"dva-hmr",
[
"babel-plugin-module-resolver",
{
"alias": {
"components": "./src/components",
},
},
],
"@babel/plugin-proposal-function-bind",
"dynamic-import-node-sync",
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose" : false }],
["@babel/plugin-transform-runtime"],
["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }],
],
}
复制代码
{
"name": "your-app-name",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"dll": "cross-env ESLINT=none webpack --progress --colors --config webpack.config.dll.js --mode production",
"dev": "cross-env ESLINT=none webpack-dev-server --open --config webpack.config.dev.js --mode development",
},
"dependencies": {
"@babel/polyfill": "^7.0.0-beta.36",
"antd": "3.7.2",
"aphrodite": "^1.2.1",
"axios": "^0.18.0",
"classnames": "^2.2.5",
"dva": "^2.1.0",
"dva-loading": "^1.0.4",
"enquire-js": "^0.1.1",
"jquery": "^3.2.1",
"lodash": "^4.17.4",
"lodash-decorators": "^4.4.1",
"moment": "^2.19.1",
"omit.js": "^1.0.0",
"path-to-regexp": "^2.1.0",
"prop-types": "^15.5.10",
"qs": "^6.5.0",
"rc-drawer-menu": "^0.5.0",
"react": "^16.7.0-alpha.0",
"react-addons-css-transition-group": "^15.6.2",
"react-container-query": "^0.9.1",
"react-document-title": "^2.0.3",
"react-dom": "^16.7.0-alpha.0",
"react-fittext": "^1.0.0",
"react-image-lightbox-rotate": "^1.2.0",
"react-lazyload": "^2.3.0",
"react-pdf-js": "^4.2.3",
"react-swf": "^1.0.7",
"rollbar": "^2.3.4",
"url-polyfill": "^1.0.10"
},
"devDependencies": {
"@babel/core": "^7.5.5",
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-proposal-decorators": "^7.3.0",
"@babel/plugin-proposal-function-bind": "^7.2.0",
"@babel/plugin-transform-runtime": "^7.5.5",
"@babel/preset-env": "^7.5.5",
"@babel/preset-react": "^7.0.0",
"babel-eslint": "^8.1.2",
"babel-loader": "^8.0.6",
"babel-plugin-dva-hmr": "^0.4.2",
"babel-plugin-dynamic-import-node-sync": "^2.0.1",
"babel-plugin-import": "^1.6.7",
"babel-plugin-module-resolver": "^3.1.1",
"babel-plugin-transform-decorators-legacy": "^1.3.5",
"babel-polyfill": "^6.26.0",
"chalk": "^2.4.2",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^5.0.4",
"cross-env": "^5.2.0",
"cross-port-killer": "^1.0.1",
"css-loader": "^3.1.0",
"eslint": "^4.14.0",
"eslint-config-airbnb": "^16.0.0",
"eslint-config-prettier": "^2.9.0",
"eslint-plugin-babel": "^4.0.0",
"eslint-plugin-compat": "^2.1.0",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-markdown": "^1.0.0-beta.6",
"eslint-plugin-react": "^7.7.0",
"file-loader": "^4.1.0",
"friendly-errors-webpack-plugin": "^1.7.0",
"glob": "^7.1.4",
"html-webpack-plugin": "^3.2.0",
"less": "^2.7.3",
"less-loader": "^5.0.0",
"mini-css-extract-plugin": "^0.8.0",
"mockjs": "^1.0.1-beta3",
"portfinder": "^1.0.13",
"postcss-loader": "^3.0.0",
"progress-bar-webpack-plugin": "^1.12.1",
"purify-css": "^1.2.5",
"purifycss-webpack": "^0.7.0",
"redbox-react": "^1.6.0",
"regenerator-runtime": "^0.11.1",
"style-loader": "^0.23.1",
"url-loader": "^2.1.0",
"webpack": "^4.39.1",
"webpack-bundle-analyzer": "^2.11.2",
"webpack-cli": "^3.3.6",
"webpack-dev-server": "^3.7.2",
"webpack-merge": "^4.2.1"
},
"engines": {
"node": ">=8.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 10"
]
}
复制代码
const path = require('path');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const theme = require('./src/theme');
const Mode = process.env.NODE_ENV !== 'production';
module.exports = {
output: {
filename: '[name].[hash:8].js',
path: path.resolve( __dirname, './dist' ),
chunkFilename: '[name].[hash:8].async.js',
publicPath: '/'
},
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
hmr: Mode,
}
},
{
loader: 'css-loader',
},
]
},
{
test: /\.less$/,
exclude: /node_modules/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
hmr: Mode,
}
},
{
loader: 'css-loader',
options: {
importLoaders: 1,
modules: {
localIdentName: '[name]_[local]_[hash:base64:5]',
}
}
},
{
loader: 'less-loader',
options: {
javascriptEnabled: true,
modifyVars: theme,
}
}
]
},
{
test: /(\.js|\.jsx)$/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
}
},
exclude: /node_modules/,
},
{
test: /\.(jpg|jpeg|png|svg|git|swf)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 1024,
outputPath: 'images'
}
}
]
}
],
},
plugins: [
new MiniCssExtractPlugin({
filename: Mode ? '[name].css' : '[name].[hash:8].css',
chunkFilename: Mode ? '[id].css' : '[id].[hash:8].css',
ignoreOrder: false,
}),
new CopyWebpackPlugin(
[
{
from: path.resolve(__dirname, './public'),
}
]
),
new webpack.IgnorePlugin(/\.\/locale/, /moment/),
new webpack.ProvidePlugin({
"React": "react",
}),
],
optimization: {
splitChunks: {
chunks: 'all'
}
}
}
复制代码
const merge = require('webpack-merge');
const webpack = require('webpack');
const baseConfig = require('./webpack.config.base');
const ProgressBar = require('progress-bar-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
const portfinder = require('portfinder');
const chalk = require('chalk');
const path = require('path');
let DEFAULT_PORT = 8000;
let checkAndGetPort = () => {
portfinder.basePort = DEFAULT_PORT;
portfinder.getPort((err, port) => {
if (!err) {
DEFAULT_PORT = port;
}
})
}
let mergeConfig = async () => {
await checkAndGetPort();
return merge(
baseConfig, {
devServer: {
contentBase: './dist',
port: DEFAULT_PORT,
inline: true,
historyApiFallback: true,
hot: true,
quiet: true,
proxy: {}
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
template: path.join(__dirname, '/src/index.ejs'),
filename: 'index.html',
hash: true,
isDev: true,
}),
new ProgressBar({
format: ' build [:bar] ' + chalk.green.bold(':percent') + ' (:elapsed seconds)',
clear: false
}),
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./dist/vendor-manifest.json')
}),
new FriendlyErrorsWebpackPlugin({
compilationSuccessInfo: {
messages: [`You application is running here http://localhost:${DEFAULT_PORT}`],
},
})
]
}
)
}
module.exports = mergeConfig();
复制代码
const path = require('path');
const webpack = require('webpack');
module.exports = {
resolve: {
extensions: [ '.js', '.jsx' ]
},
entry: {
vendor: [
'antd', 'aphrodite', 'axios', 'classnames',
'dva', 'dva-loading', 'enquire-js',
'react', 'react-dom', 'react-image-lightbox-rotate', 'moment',
'qs', 'prop-types', 'path-to-regexp', 'react-pdf-js', 'react-swf',
'lodash', 'jquery', 'rc-drawer-menu'
]
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, './dist'),
library: 'vendor_lib_[hash:8]',
},
plugins: [
new webpack.DllPlugin({
context: __dirname,
path: path.resolve(__dirname, './dist/vendor-manifest.json'),
name: 'vendor_lib_[hash:8]',
})
],
};
复制代码
<%= htmlWebpackPlugin.options.isDev ? '<script src='./vendor.dll.js'></script>' : '' %>
复制代码