如果你:
- 是前端热爱者 :)
- 有JavaScript/nodejs基础
- 会使用一些常用命令行,
mkdir
,cd
,etc. - 会使用
npm
- 想对webpack有更深的认识,或许此时你恰好遇到关于webpack的问题
那么,请继续阅读:
什么是webpack,为什么使用webpack
webpack官方是这样定义她的:
webpack 是一个用来构建我们应用程序中的 JavaScript 模块的工具。
简单来说就是一个打包器。(打包器: 它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其打包为合适的格式以供浏览器使用。)
打包器茫茫多,那么为什么选择她呢?因为她具备以下特性
- 附加模块按需加载
- AMD
define
- AMD
require
/ 按需加载 - CommonJS
exports
- CommonJS
require
- CommonJS
require.resolve
- require 中拼接
require("./fi" + "le")
- 调试支持
SourceUrl
,SourceMaps
- ES2015
import
/export
require (guided)
中的表达式require("./templates/" + template)
- 生成单独包
- 间接的
require
:var r = require; r("./file")
- 压缩
uglify
- 可配置用
common
bundle
构建多页 - 可多个
bundle
- Node.js 内置 libs
require("path")
- Node.js 的一些变量可用:
process
,__dir/filename
,global
- 丰富的插件
- 预处理:
loaders
,transforms
- 浏览器替换
web_modules
,.web.js
,package.json
field,alias
config option - 拥有文件系统:
require
文件 - 执行时(runtime)开销低:243B + 20B 每个模块 + 4B 每个依赖
- 开发文件监听(
watch
)模式 - etc.
用过的都说好。
如果你曾经挣扎于下面这些情况中的其中之一:
- 不小心将一些不需要的样式表或者 JS 库引入生产环境,导致项目体积变大
- 遇到作用域问题 - 不管是来自 CSS 还是 JavaScript
- 不停寻找一个好的系统好让你可以在 JavaScript 代码里使用 Node 或 Bower 的模块,或者依赖一系列疯狂的后端配置来正确地使用那些模块
- 需要优化资源分发机制却又担心会破坏掉某些东西
那么你就可以受益于 Webpack 了。它通过让 JavaScript 取代开发者的大脑来关心依赖和加载顺序,轻松地解决了上面这些问题。最好的部分是什么?Webpack 甚至可以在服务端无缝运行,这意味着你仍然可以使用 Webpack 来构建渐进式增强的网站。
安装
预准备
在开始前,先要确认你已经安装 Node.js 的最新版本。使用 Node.js 最新的 LTS 版本,是理想的起步。使用旧版本,你可能遇到各种问题,因为它们可能缺少 webpack 功能或缺少相关 package 包。
本地安装
使用npm
:
npm install --save-dev webpack
npm install --save-dev webpack@<version>
- 当你在本地安装 webpack 后,你能够从
node_modules/.bin/webpack
访问它的 bin 版本。
全局安装
npm install --global webpack
但不推荐这样做,原因是:会锁定 webpack 到指定版本,并且在使用不同的 webpack 版本的项目中可能会导致构建失败。
配置
很多情况,你在使用webpack中遇到的问题都是配置问题,好好阅读下下面的配置,90%的问题应该都可以解决了。
在根目录新建一个 webpack.config.js 文件用来声明 Webpack 的配置,你也可以指定特定的config文件:webpack --config mywebpack.config.js
const path = require('path');
module.exports = {
entry: "./app/entry", // string | object | array entry: ["./app/entry1", "./app/entry2"],
entry: {
a: "./app/entry-a",
b: ["./app/entry-b1", "./app/entry-b2"]
},
// 这里应用程序开始执行
// webpack 开始打包
output: {
// webpack 如何输出结果的相关选项
path: path.resolve(__dirname, "dist"), // string
// 所有输出文件的目标路径
// 必须是绝对路径(使用 Node.js 的 path 模块)
// PS:__dirname指的是根目录
filename: "bundle.js", // string filename: "[name].js", // 用于多个入口点(entry point)(出口点?)
filename: "[chunkhash].js", // 用于长效缓存
// 「入口分块(entry chunk)」的文件名模板(出口分块?)
publicPath: "/assets/", // string publicPath: "",
publicPath: "https://cdn.example.com/",
// 输出解析文件的目录,url 相对于 HTML 页面
library: "MyLibrary", // string,
// 导出库(exported library)的名称
libraryTarget: "umd", // 通用模块定义 libraryTarget: "umd2", // 通用模块定义
libraryTarget: "commonjs2", // exported with module.exports
libraryTarget: "commonjs-module", // 使用 module.exports 导出
libraryTarget: "commonjs", // 作为 exports 的属性导出
libraryTarget: "amd", // 使用 AMD 定义方法来定义
libraryTarget: "this", // 在 this 上设置属性
libraryTarget: "var", // 变量定义于根作用域下
libraryTarget: "assign", // 盲分配(blind assignment)
libraryTarget: "window", // 在 window 对象上设置属性
libraryTarget: "global", // property set to global object
libraryTarget: "jsonp", // jsonp wrapper
// 导出库(exported library)的类型
/* 高级输出配置 */
pathinfo: true, // boolean
// 在生成代码时,引入相关的模块、导出、请求等有帮助的路径信息。
chunkFilename: "[id].js",
chunkFilename: "[chunkhash].js", // 长效缓存(/guides/caching)
// 「附加分块(additional chunk)」的文件名模板
jsonpFunction: "myWebpackJsonp", // string
// 用于加载分块的 JSONP 函数名
sourceMapFilename: "[file].map", // string
sourceMapFilename: "sourcemaps/[file].map", // string
// 「source map 位置」的文件名模板
devtoolModuleFilenameTemplate: "webpack:///[resource-path]", // string
// 「devtool 中模块」的文件名模板
devtoolFallbackModuleFilenameTemplate: "webpack:///[resource-path]?[hash]", // string
// 「devtool 中模块」的文件名模板(用于冲突)
umdNamedDefine: true, // boolean
// 在 UMD 库中使用命名的 AMD 模块
crossOriginLoading: "use-credentials", // 枚举
crossOriginLoading: "anonymous",
crossOriginLoading: false,
// 指定运行时如何发出跨域请求问题
/* 专家级输出配置(自行承担风险) */
devtoolLineToLine: {
test: /\.jsx$/
},
// 为这些模块使用 1:1 映射 SourceMaps(快速)
hotUpdateMainFilename: "[hash].hot-update.json", // string
// 「HMR 清单」的文件名模板
hotUpdateChunkFilename: "[id].[hash].hot-update.js", // string
// 「HMR 分块」的文件名模板
sourcePrefix: "\t", // string
// 包内前置式模块资源具有更好可读性
},
module: {
// 关于模块配置
rules: [
// 模块规则(配置 loader、解析器等选项)
{
test: /\.jsx?$/,
include: [
path.resolve(__dirname, "app")
],
exclude: [
path.resolve(__dirname, "app/demo-files")
],
// 这里是匹配条件,每个选项都接收一个正则表达式或字符串
// test 和 include 具有相同的作用,都是必须匹配选项
// exclude 是必不匹配选项(优先于 test 和 include)
// 最佳实践:
// - 只在 test 和 文件名匹配 中使用正则表达式
// - 在 include 和 exclude 中使用绝对路径数组
// - 尽量避免 exclude,更倾向于使用 include
issuer: { test, include, exclude },
// issuer 条件(导入源)
enforce: "pre",
enforce: "post",
// 标识应用这些规则,即使规则覆盖(高级选项)
loader: "babel-loader",
// 应该应用的 loader,它相对上下文解析
// 为了更清晰,`-loader` 后缀在 webpack 2 中不再是可选的
// 查看 webpack 1 升级指南。
options: {
presets: ["es2015"]
},
// loader 的可选项
},
{
test: "\.html$"
use: [
// 应用多个 loader 和选项
"htmllint-loader",
{
loader: "html-loader",
options: {
/* ... */
}
}
]
},
{ oneOf: [ /* rules */ ] },
// 只使用这些嵌套规则之一
{ rules: [ /* rules */ ] },
// 使用所有这些嵌套规则(合并可用条件)
{ resource: { and: [ /* 条件 */ ] } },
// 仅当所有条件都匹配时才匹配
{ resource: { or: [ /* 条件 */ ] } },
{ resource: [ /* 条件 */ ] },
// 任意条件匹配时匹配(默认为数组)
{ resource: { not: /* 条件 */ } }
// 条件不匹配时匹配
],
/* 高级模块配置 */
noParse: [
/special-library\.js$/
],
// 不解析这里的模块
unknownContextRequest: ".",
unknownContextRecursive: true,
unknownContextRegExp: /^\.\/.*$/,
unknownContextCritical: true,
exprContextRequest: ".",
exprContextRegExp: /^\.\/.*$/,
exprContextRecursive: true,
exprContextCritical: true,
wrappedContextRegExp: /.*/,
wrappedContextRecursive: true,
wrappedContextCritical: false,
// specifies default behavior for dynamic requests
},
resolve: {
// 解析模块请求的选项
// (不适用于对 loader 解析)
modules: [
"node_modules",
path.resolve(__dirname, "app")
],
// 用于查找模块的目录
extensions: [".js", ".json", ".jsx", ".css"],
// 使用的扩展名
alias: {
// 模块别名列表
"module": "new-module",
// 起别名:"module" -> "new-module" 和 "module/path/file" -> "new-module/path/file"
"only-module$": "new-module",
// 起别名 "only-module" -> "new-module",但不匹配 "module/path/file" -> "new-module/path/file"
"module": path.resolve(__dirname, "app/third/module.js"),
// 起别名 "module" -> "./app/third/module.js" 和 "module/file" 会导致错误
// 模块别名相对于当前上下文导入
},
/* 可供选择的别名语法 */ alias: [
{
name: "module",
// 旧的请求
alias: "new-module",
// 新的请求
onlyModule: true
// 如果为 true,只有 "module" 是别名
// 如果为 false,"module/inner/path" 也是别名
}
],
/* 高级解析选项 */
symlinks: true,
// 遵循符号链接(symlinks)到新位置
descriptionFiles: ["package.json"],
// 从 package 描述中读取的文件
mainFields: ["main"],
// 从描述文件中读取的属性
// 当请求文件夹时
aliasFields: ["browser"],
// 从描述文件中读取的属性
// 以对此 package 的请求起别名
enforceExtension: false,
// 如果为 true,请求必不包括扩展名
// 如果为 false,请求可以包括扩展名
moduleExtensions: ["-module"],
enforceModuleExtension: false,
// 类似 extensions/enforceExtension,但是用模块名替换文件
unsafeCache: true,
unsafeCache: {},
// 为解析的请求启用缓存
// 这是不安全,因为文件夹结构可能会改动
// 但是性能改善是很大的
cachePredicate: (path, request) => true,
// predicate function which selects requests for caching
plugins: [
// ...
]
// 应用于解析器的附加插件
},
performance: {
hints: "warning", // 枚举 hints: "error", // 性能提示中抛出错误
hints: false, // 关闭性能提示
maxAssetSize: 200000, // 整数类型(以字节为单位)
maxEntrypointSize: 400000, // 整数类型(以字节为单位)
assetFilter: function(assetFilename) {
// 提供资源文件名的断言函数
return assetFilename.endsWith('.css') || assetFilename.endsWith('.js');
}
},
devtool: "source-map", // enum devtool: "inline-source-map", // 嵌入到源文件中
devtool: "eval-source-map", // 将 SourceMap 嵌入到每个模块中
devtool: "hidden-source-map", // SourceMap 不在源文件中引用
devtool: "cheap-source-map", // 没有模块映射(module mappings)的 SourceMap 低级变体(cheap-variant)
devtool: "cheap-module-source-map", // 有模块映射(module mappings)的 SourceMap 低级变体
devtool: "eval", // 没有模块映射,而是命名模块。以牺牲细节达到最快。
// 通过在浏览器调试工具(browser devtools)中添加元信息(meta info)增强调试
// 牺牲了构建速度的 `source-map' 是最详细的。
context: __dirname, // string(绝对路径!)
// webpack 的主目录
// entry 和 module.rules.loader 选项
// 相对于此目录解析
target: "web", // 枚举 target: "webworker", // WebWorker
target: "node", // node.js 通过 require
target: "async-node", // Node.js 通过 fs and vm
target: "node-webkit", // nw.js
target: "electron-main", // electron,主进程(main process)
target: "electron-renderer", // electron,渲染进程(renderer process)
target: (compiler) => { /* ... */ }, // 自定义
// 包(bundle)应该运行的环境
// 更改 块加载行为(chunk loading behavior) 和 可用模块(available module)
externals: ["react", /^@angular\//], externals: "react", // string(精确匹配)
externals: /^[a-z\-]+($|\/)/, // 正则
externals: { // 对象
angular: "this angular", // this["angular"]
react: { // UMD
commonjs: "react",
commonjs2: "react",
amd: "react",
root: "React"
}
},
externals: (request) => { /* ... */ return "commonjs " + request }
// 不要遵循/打包这些模块,而是在运行时从环境中请求他们
stats: "errors-only", stats: { //object
assets: true,
colors: true,
errors: true,
errorDetails: true,
hash: true,
// ...
},
// 精确控制要显示的 bundle 信息
devServer: {
proxy: { // proxy URLs to backend development server
'/api': 'http://localhost:3000'
},
contentBase: path.join(__dirname, 'public'), // boolean | string | array, static file location
compress: true, // enable gzip compression
historyApiFallback: true, // true for index.html upon 404, object for multiple paths
hot: true, // hot module replacement. Depends on HotModuleReplacementPlugin
https: false, // true for self-signed, object for cert authority
noInfo: true, // only errors & warns on hot reload
// ...
},
plugins: [
// ...
],
// 附加插件列表
/* 高级配置 */
resolveLoader: { /* 等同于 resolve */ }
// 独立解析选项的 loader
profile: true, // boolean
// 捕获时机信息
bail: true, //boolean
// 在第一个错误出错时抛出,而不是无视错误。
cache: false, // boolean
// 禁用/启用缓存
watch: true, // boolean
// 启用观察
watchOptions: {
aggregateTimeout: 1000, // in ms
// 将多个更改聚合到单个重构建(rebuild)
poll: true,
poll: 500, // 间隔单位 ms
// 启用轮询观察模式
// 必须用在不通知更改的文件系统中
// 即 nfs shares(译者注:Network FileSystem,最大的功能就是可以透過網路,讓不同的機器、不同的作業系統、可以彼此分享個別的檔案 ( share file ))
},
node: {
/* TODO */
},
recordsPath: path.resolve(__dirname, "build/records.json"),
recordsInputPath: path.resolve(__dirname, "build/records.json"),
recordsOutputPath: path.resolve(__dirname, "build/records.json"),// TODO}
分割代码(代码分离)
代码分离是 webpack 中最引人注目的特性之一。你可以把你的代码分离到不同的 bundle 中,然后你就可以去按需加载这些文件
- 例如,当用户导航到匹配的路由,或用户触发了事件时,加载对应文件。如果使用了正确的使用方式,这可以使我们有更小的 bundle,同时可以控制资源加载优先级,从而对你的应用程序加载时间产生重要影响。
总的来说,使用 webpack 可以完成两类代码分离工作:
按资源进行分离
一、分离第三方库
现代流行开发网站,多多少少都用到了不少三方库,而这些三方库一般不会频繁的去修改,将这些代码和我们的业务逻辑一同打包,这无疑是低效的。如果我们将这些库(library)中的代码,保留在与应用程序代码相独立的 bundle 中,我们就可以利用浏览器缓存机制,把这些文件长时间地缓存在用户机器上,增加了访问速度。
为了完成这个目标,不管应用程序代码如何变化,vendor
文件名中的 hash
部分必须保持不变。学习如何使用 CommonsChunkPlugin
分离 vendor/library
代码。
你可能会想到搞多个入口啊,类似这样:
var path = require('path');
module.exports = function(env) {
return {
entry: {
main: './index.js',
vendor: 'moment'
},
output: {
filename: '[name].[chunkhash].js',
path: path.resolve(__dirname, 'dist')
}
}
}
再次运行 webpack,可以发现生成了两个 bundle。然而如果查看他们的代码,会发现 moment 的代码在两个文件中都出现了!其原因是 moment 是主应用程序(例如 index.js
)的依赖模块,每个入口起点都会打包自己的依赖模块。
为此我们需要使用插件CommonsChunkPlugin,这是一个非常复杂的插件。它从根本上允许我们从不同的 bundle 中提取所有的公共模块,并且将他们加入公共 bundle 中。如果公共 bundle 不存在,那么它将会创建一个出来。
var webpack = require('webpack');
var path = require('path');
module.exports = function(env) {
return {
entry: {
main: './index.js',
vendor: 'moment'
},
output: {
filename: '[name].[chunkhash].js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor' // 指定公共 bundle 的名字。
})
]
}
}
但是这样vendor的文件的hashcode还是会每个编译都不一样,为此,可以使用manifest,如下:
var webpack = require('webpack');
var path = require('path');
module.exports = function(env) {
return {
entry: {
main: './index.js',
vendor: 'moment'
},
output: {
filename: '[name].[chunkhash].js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
names: ['vendor', 'manifest'] // 指定公共 bundle 的名字。
})
]
}
};
总结使用CommonsChunkPlugin:
var webpack = require('webpack');
var path = require('path');
module.exports = function() {
return {
entry: {
main: './index.js' //Notice that we do not have an explicit vendor entry here
},
output: {
filename: '[name].[chunkhash].js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function (module) {
// this assumes your vendor imports exist in the node_modules directory
return module.context && module.context.indexOf('node_modules') !== -1;
}
}),
//CommonChunksPlugin will now extract all the common modules from vendor and main bundles
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest' //But since there are no more common modules between them we end up with just the runtime code included in the manifest file
})
]
};
}
你还可以使用DllPlugin(提供分离打包的方式,可以极大提高构建时间性能)
new webpack.DllPlugin({
path: `${__dirname}/manifest.json`,
name: '[name]',
context: __dirname,
})
二、分离CSS
为了用 webpack 对 CSS 文件进行打包,你可以像其它模块一样将 CSS 引入到你的 JavaScript 代码中,同时用 css-loader (像 JS 模块一样输出 CSS),也可以选择使用 ExtractTextWebpackPlugin (将打好包的 CSS 提出出来并输出成 CSS 文件)。
为什么会使用这个插件,假如不使用,那么你可能如下:
安装CSS加载器:
npm install --save-dev css-loader style-loader
webpack配置:
module.exports = {
module: {
rules: [{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
}]
}
}
但是这样,CSS 会跟你的 JavaScript 打包在一起,并且在初始加载后,通过一个 <style>
标签注入样式,然后作用于页面。
这里有一个缺点就是,你无法使用浏览器的能力,去异步且并行去加载 CSS。取而代之的是,你的页面需要等待整个 JavaScript 文件加载完,才能进行样式渲染。
webpack 能够用 ExtractTextWebpackPlugin
帮助你将 CSS 单独打包,以解决以上问题。
安装:
npm install --save-dev extract-text-webpack-plugin
添加到配置中使用她:
+var ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
module: {
rules: [{
test: /\.css$/,
- use: [ 'style-loader', 'css-loader' ]
+ use: ExtractTextPlugin.extract({
+ use: 'css-loader'
+ })
}]
},
+ plugins: [
+ new ExtractTextPlugin('styles.css'),
+ ]
}
OK,CSS分离完成。
按工程需求进行分离
工程一旦达到一定的程度,就会有按需加载的需求。接下来是说如何将您的 bundle 拆分成可以在之后异步下载的 chunk。例如,这允许首先提供最低限度的引导 bundle,并在稍后再异步地加载其他功能。如果你们不需要按需加载,那么可以跳过。
webpack 支持两种相似的技术实现此目的:使用 import() (推荐,ECMAScript 提案) 和 require.ensure() (遗留,webpack 特定,这里不讨论这个,舍弃)。
import()
ES2015 loader 规范定义了 import() 作为一种在运行时(runtime)动态载入 ES2015 模块的方法。
webpack 把 import() 作为一个分离点(split-point),并把引入的模块作为一个单独的 chunk。 import() 将模块名字作为参数并返回一个 Promoise 对象,即 import(name) -> Promise.
如:
function determineDate() {
import('moment').then(function(moment) {
console.log(moment().format());
}).catch(function(err) {
console.log('Failed to load moment', err);
});
}
determineDate();
- 注意,由于 webpack 至少需要感知到文件的位置信息,因此类似
import(foo)
的完全动态语句会导致失败。这是因为 foo 可以是系统或项目中的任意路径下任意文件。import()
至少应感知的信息是模块所处的位置,所以打包将限制在特定目录或一组文件中。
例如,import(``./locale/${language}.json``)
将会使 ./locale
目录下的每个 .json
都打包到分离点(split-point)中。在运行时(runtime),当计算出变量 language
时,任何像 english.json
或 german.json
这样的文件都可以供使用。所以请牢记,在使用 import()
时,该路径必须包含路径信息或完整的静态路径(就像上面例子中的 'moment'
一样)。
使用Promise polyfill
使用它是因为:import()
在内部依赖于 Promise
。 如果你想在老版本浏览器使用 import(),请记得使用 polyfill(例如 es6-promise 或 promise-polyfill)来 shim Promise。
入口处配置:
import Es6Promise from 'es6-promise';
Es6Promise.polyfill();
// 或
import 'es6-promise/auto';
// 或
import Promise from 'promise-polyfill';
if (!window.Promise) {
window.Promise = Promise;
}
自定义Chunk 名称
这个是webpack 2.4.0新加的"魔力注释"
import(/* webpackChunkName: "my-chunk-name" */ 'module');
配合 Babel 使用
import() 是属于 Stage 3 的特性,需要安装/添加 syntax-dynamic-import 插件来避免 parser 报错。
npm install --save-dev babel-core babel-loader babel-plugin-syntax-dynamic-import babel-preset-es2015
然后你就可以使用了:
function determineDate() {
import('moment')
.then(moment => moment().format('LLLL'))
.then(str => console.log(str))
.catch(err => console.log('Failed to load moment', err));
}
determineDate();
但是你的webpack的配置文件得像下面这样去修改它:
module.exports = {
entry: './index-es2015.js',
output: {
filename: 'dist.js',
},
module: {
rules: [{
test: /\.js$/,
exclude: /(node_modules)/,
use: [{
loader: 'babel-loader',
options: {
presets: [['es2015', {modules: false}]],
plugins: ['syntax-dynamic-import']
}
}]
}]
}
};
没有使用 syntax-dynamic-import
插件会导致构建失败,并提示:
- Module build failed(模块构建失败):
SyntaxError: 'import' and 'export' may only appear at the top level
('import' 和 'export' 只能出现在顶层),或提示 - Module build failed(模块构建失败):
SyntaxError: Unexpected token, expected {
使用async/await
说了,import(name) -> Promise
返回的是promise,so,用async/await
大法好;那么如何使用呢:
安装插件:
npm install --save-dev babel-plugin-transform-async-to-generator babel-plugin-transform-regenerator babel-plugin-transform-runtime
然后你就可以这样用了:
async function determineDate() {
const moment = await import('moment');
return moment().format('LLLL');
}
determineDate().then(str => console.log(str));
当然在插件是这样使用的:
module.exports = {
entry: './index-es2017.js',
output: {
filename: 'dist.js',
},
module: {
rules: [{
test: /\.js$/,
exclude: /(node_modules)/,
use: [{
loader: 'babel-loader',
options: {
presets: [['es2015', {modules: false}]],
plugins: [
'syntax-dynamic-import',
'transform-async-to-generator',
'transform-regenerator',
'transform-runtime'
]
}
}]
}]
}
};
import() 导入整个模块命名空间
需要注意的是import() 导入整个模块命名空间。举个例子:
// 示例 1: 最顶层 import
import * as Component from './component';
// 示例 2: 使用 import() 进行代码分离
import('./component').then(Component => /* ... */);
但是,在使用带有 ES2015 模块的 import() 时,您必须显式地访问默认导出和命名导出:
async function main() {
// 解构赋值用法示例
const { default: Component } = await import('./component');
// 行内用法示例
render((await import('./component')).default);
}
可去我博客主页查看更多详情:http://www.fangyongle.com/xiang-xi-webpack-2jiao-cheng/