本质上,webpack是一个用于现代JavaScript应用程序的静态模块打包工具。当webpack处理应用程序时,它会在内部从一个或多个入口点构建一个依赖图,然后将你项目中所需的每一个模块组合成一个或多个bundles,它们均为静态资源,用于展示你的内容。
从v4.0.0开始,webpack 可以不用再引入一个配置文件来打包项目,然而,它仍然有着高度可配置性,可以很好满足你的需求。
在开始前需要先了解一些核心概念:
入口起点(entry point)指示webpack应该使用哪个模块,来作为构建其内部依赖图(dependency graph)的开始。进入入口起点后,webpack会找到有哪些模块和库是入口起点(直接和间接)的依赖。
默认值是./src/index.js,但你可以通过在webpack configuration中配置entry属性,来指定一个(或多个)不同的入口起点。例如:
webpack.config.js
module.exports = {entry: './path/to/my/entry/file.js',
};
1.2、输出(output)output属性告诉webpack在哪里输出它所创建的bundle。以及如何命名这些文件。主要输出文件的默认值是"./dist/main.js",其他生成文件默认放置在"./dist.js"文件夹中。
你可以通过在配置中指定一个output字段,来配置这些处理过程:
webpack.config.js
const path = require('path')
module.exports = {entry: './path/to/my/entry/file.js',
output: {path: path.resolve(__dirname, 'dist'), // bundle生成(emit)到哪里
filename: 'my-first-webpack.bundle.js', // 告诉webpack bundle的名称
……
}
}
1.3、loaderwebpack只能理解JavaScript和JSON文件,这是webpack开箱可用的自带能力。loader让webpack能够去处理其他类型的文件,并将它们转化为有效的模板,以供应用程序使用,以及被添加到依赖图中。
Warnig
webpack的其中一个强大的特性就是能通过import导入任何类型的模板(例如.css文件),其他打包程序或执行器的可能不支持。我们认为这种语言扩展是很有必要的,因为这可以使开发人员创建出更准确地依赖关系图。
在更高层面,在webpack的配置中,loader有两个属性:
webpack.config.js
moudle.exports = {output: {filename: 'my-first-webpack.bundle.js'
},
module: {rules: [{test:/\.txt$/, use: 'raw-loader'}]
}
}
在以上配置中,对一个单独的module对象定义了rules属性,里面包含两个必须属性:test和use。告诉webpack编译器(compiler)如下信息:
在require()/import语句中被解析为.txt的路径时,在你对它打包之前,先用use(使用)raw-loader转换一下。
Warning
重要的是:在webpack配置中定义rules时,要定义在module.rules而不是rules中。为了使你便于理解,如果没有按照正确的方式去做,webpack会给出警告。
1.4、插件(plugin)Warning
在使用正则表达式匹配文件时,不要为它添加引号。也就是说,/.txt / 与 ′ / t ˙ x t /与 '/\.txt /与′/t˙xt/’ 或者 “/.txt$/” 不一样。前者指示webpack匹配任何以.txt结尾的文件,后者指示webpack匹配具有绝对路径‘.txt’的单个文件;这可能不符合你的意图。
loader用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量等。
想要使用一个插件,你只需要require()它,然后把它添加到plugins数组中。多数插件可以通过选项(option)自定义。你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用new操作符来创建一个插件实例。
webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack') // 用于访问内直插件
module.exports = {module: [{test: /\.txt$/, use: 'raw-loader'}],
plugins: [new HtmlWebpackPlugin({template: './src/index.html'})]
}
在上面的示例中,html-webpack-plugin为应用程序生成一个HTML文件,并自动将生成的所有bundle注入到此文件中。
1.5、模式(mode)通过选择developmemt,production或none之中的一个,来设置mode参数,你可以启用webpack内置在相应环境下的优化。其默认值为production。
module.exports = {mode: 'production',
}
1.6、浏览器兼容性(browser compatibility)Webpack支持所有符合ES5标准的浏览器(不支持IE8及以下版本)。
webpack的import()和require.ensure()需要Promise。如果你想要支持旧版本浏览器,在使用这些表达式之前,还要提前加载polyfill。
Webpack5运行于Node.js v10.13.0+的版本
2、入口起点(entry points) 2.1、单个入口(简写)语法用法:entry: string | [string]
webpack.config.js
module.exports = {entry: './path/to/my/entry/file.js'
}
entry属性的单个入口语法,是以下形式的简写:
module.exports = {entry: {main: './path/to/my/entry/file.js'
}
}
也可以将一个文件路径数组传递给entry属性,这将创建一个所谓的‘multi-main entry’。在你想要一次注入多个依赖文件,并将它们的依赖关系绘制在一个‘chunk’中时,这种方式就很有用。
module.exports = {entry: ['./src/file_1.js', './src/file_2.js'],
output: {filename: 'bundle.js',
},
};
2.2、对象语法用法:
entry: {string | [string] } | {}
module.exports = {entry: {app: './src/app.js',
adminApp: './src/adminApp.js'
}
}
对象语法会比较繁琐。然而,这是应用程序中定义入口的最可扩展的方式。
Tip
“webpack配置的可扩展”是指,这些配置可以重复使用,并且可以与其他配置组合使用。这是一种流行的技术,用于将关注点从环境(environment)、构建目标(build target)、运行时(runtime)中分离。然后使用专门的工具(如webpack-merge)将它们合并起来。
2.2.1、描述入口的对象“Tip”
当你通过插件生成入口时,你可以传递空对象{}给entry。
用于描述入口的对象。你可以使用如下属性:
module.exports = {entry: {a2: 'dependingfile.js',
b2: { dependOn: 'a2',
import: './src/app.js'
}
}
}
runtime和dependOn不应该在同一个入口上同时使用,所以如下配置无效,并会抛出错误:module.exports = {entry: {a2: './a',
b2: { runtime: 'x2',
dependOn: 'a2',
import: './b',
},
},
};
确保runtime不能指向已存在的入口名称,例如下面配置会抛出一个错误:module.exports = {entry: {a1: './a',
b1: { runtime: 'a1',
import: './b'
}
}
}
另外dependOn不能是循环引用的,下面的例子也会出现错误:module.exports = {entry: {a3: { import: './a',
dependOn: 'b3',
},
b3: { import: './b',
dependOn: 'a3'
}
}
}
以下列出一些入口配置和他们的实际用例:
2.3.1、分离app和vendor(第三方库)入口// webpack.config.js
module.exports = {module.exports = {entry: { main: './src/app.js',
vendor: './src/vendor.js',
}
}
}
// webpack.prod.js
module.exports = {output: {filename: '[name].[contenthash].bundle.js,
}
}
// webpack.dev.js
module.exports = {module.exports = {filename: '[name].bundle.js'
}
}
这是告诉webpack我们想要配置2个单独的入口点(例如上面的示例)。
这样就可以在vendor.js中存入未做修改的必要library或文件(例如Bootstrap,JQuery,图片等),然后将它们打包在一起成为单独的chunk。内容哈希保持不变,这使浏览器可以独立地缓存它们,从而减少了加载时间。
2.3.2、多页面应用程序Tip
在webpack< 4中不鼓励这样做,通常将vendor作为一个单独的入口起点添加到entry选项中,以将其编译为一个单独的文件(与CommonsChunkPlugin结合使用)。
而在webpack4中不鼓励这样做,而是使用optimization.splitChunks选项,将vendor和app模块分开,并为其创建一个单独的文件。不要为vendor或其他不是执行起点创建entry。
module.exports = {entry: {pageOne: './src/pageone/index.js',
pageTwo: './src/pageTwo/index.js',
pageThree: './src/pageThree/index.js'
}
}
告诉webpack需要三个独立分离的依赖图(如上面的示例)。
原因:在多页面应用程序中,server会拉取一个新的HTML文档给你的客户端。页面重新加载此新文档,并且资源被重新下载。然而,这给了我们特殊的机会去做很多事情,例如使用optimization.splitChunks为页面间共享的应用程序代码创建bundle。由于入口起点数量的增多,多页应用能够复用多个入口起点之间的大量代码/模块,从而可以极大地从这些技术中受益。
3、输出(output)根据经验:每个HTML文档只使用一个入口起点。
可以通过配置output选项,告知webpack如何向硬盘写入编译文件。注意,即使可以存在多个entry起点,但只能指定一个output配置。
3.1、用法在webpack配置中,output属性的最低要求是,将它的值设置为一个对象,然后为将输出文件名配置为一个output.filename:
module.exports = {output: {filename: 'bundle.js'
}
}
此配置将一个单独的bundle.js文件输出到dist目录中。
3.2、多个入口起点如果配置中创建出多余一个“chunk”(例如,使用多个入口起点或使用像CommonsChunkPlugin这样的插件),则应该使用占位符(substitutions)来确保每个文件具有唯一的名称。
module.exports = {entry: {app: './src/app.js',
search: './src/search.js'
},
output: {filename: '[name].js',
path: __dirname + '/dist'
}
}
// 写入到硬盘:./dist/app.js, ./dist/search.js
3.3、高级进阶以下是对资源使用CDN和hash的复杂示例:
config.js
module.exports = {output: {path: '/home/proj/cdn/assets/[fullhash]',
publicPath: 'https://cdn.example.com/assets/[fullhash]'
}
}
如果在编译时,不知道最终输出文件的publicPath是什么地址,则可以将其留空,并且在运行时通过入口起点文件中的__webpack_public_path__ 动态设置
__webpack_public_path__ = myRuntimePublicPath
4、loaderloader用于对模块的源码进行转换。loader可以使你在import或“load(加载)”模块时预处理文件。因此,loader类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的得力方式。loader可以将文件从不同的语言(如TypeScript)转换为JavaScript或将内联图像转换为data URL。loader甚至允许你直接在JavaScript模板中import CSS文件!
4.1、示例例如,你可以使用loader告诉webpack加载css文件,或者将TypeScript转换为JavaScript。为此,首先安装相对应的loader:
npm install --save-dev css-loader ts-loader
然后指示webpack对每个.css使用css-loader,以及对所有.ts文件使用ts-loader:
webpack.config.js
module.exports = {module: {rules: [
{test: /\.css$/, use: 'css-loader'},
{test: /\.ts$/, use: 'ts-loader'}
]
}
}
4.2、使用loader在你的应用程序中,有两种使用loader的方式:
module.rules允许你在webpack配置中指定多个loader。这种方式是展示loader的一种简明的方式,并且有助于使代码变得简洁和易于维护。同时让你对各个loader有个全局概览:
loader从右到左,从sass-loader开始执行,然后继续执行css-loader,最后以style-loader为结束。
module.exports = {module: {rules: [{ test: /\.css$/,
use: [
{loader: 'style-loader'},
{ loader: 'css-loader',
options: {modules: true,
}
},
{loader: 'sass-loader' }
]
}]
}
}
4.4、内联方式可以在import语句或任何与import方法同等的引用方式中指定loader。使用!将资源中的loader分开。每部分都会相当于当前的目录解析。
import Styles from 'style-loader!css-loader?modules!./styles.css';
通过为内联import语句添加前缀,可以覆盖配置中的所有loader,preLoader和postLoader:
import Styles from '!style-loader!css-loader?modules!./styles.css'
import Styles from '!!style-loader!css-loader?modules!./styles.css'
import Styles from '-!style-loader!css-loader?modules!./styles.css'
选项可以传递查询参数,例如?key=value&foo=bar,或者一个JSON对象,例如?{‘key’:‘value’,‘foo’:‘bar’}
4.5、loader特性Tip
尽可能使用module.rules,因为这样可以减少源码中样板文件的代码量,并且可以在出错时,更快地调试和定位loader中的问题。
可以通过loader的预处理函数,为JavaScript生态系统提供更多能力。用户现在可以更加灵活地引入细粒度逻辑,例如:压缩、打包、语言转译(或编译)和更多其他特性。
4.6、解析loaderloader遵循标准模块解析规则。多数情况下,loader将从模块路径加载(通常是从npm install,node_modules进行加载)。
我们预期loader模块导出为一个函数,并且编写为Node.js兼容的Javascript。通常使用npm进行管理loader,但是也可以将应用程序中的文件作为自定义loader。按照规定,loader通常被命名为xxx-loader(例如json-loader)。
插件是webpack的支柱功能。Webpack自身也是构建于你在webpack配置中用到的相同的插件系统之上。
插件目的在于解决loader无法实现的其他事。webpack提供很多开箱即用的插件。
5.1、剖析Tip
如果在插件中使用了webpack-sources的package,请使用require(‘webpack’).source替代require(‘webpack-source’),以避免持久缓存的版本冲突。
webpack 插件是一个具有apply方法的Javascript对象。apply方法会被webpack compiler调用,并且在整个编译生命周期都可以访问compiler对象。
ConsoleLogOnBuildWebpackPlugin.js
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';
class ConsoleLogOnBuildWebpackPlugin { apply(compiler) {compiler.hooks.run.tap(pluginName, (compilation) =>{ console.log('webpack 构建正在启动');
})
}
}
module.exports = ConsoleLogOnBuildwebpackPlugin;
compiler hook的tap方法的第一个参数,应该是驼峰式命名的插件名称。建议为此使用一个常量,以便它可以在所有的hook中重复使用。
5.2 用法由于插件可以携带参数/选项,你必须在webpack配置中,向plugins属性传入一个new实例。
取决于你的webpack用法,对应有多种使用插件的方式。
5.3 配置方式webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack'); // 访问内置的插件
const path = require('path');
module.exports = {entry: './path/to/my/entry/file.js',
output: {filename: 'my-first-webpack.bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {rule: [
{test: /\.(js|jsx)$/,
use: 'bable-loader',
}
]
},
plugins: [
new webpack.ProgressPlugin(),
new HtmlWebpackPlugin({tempalte: './src/index.html'})
]
}
ProgressPlugin用于自定义编译过程中的进度报告,HtmlWebpackPlugin将生成一个HTML文件,并在其中使用script引入一个名为my-first-webpack.bundle.js的JS文件。
5.4、Node API方式在使用Node API 时,还可以通过配置中的plugins属性传入插件。
some-node-script.js
const webpack = require('webpack'); // 访问webpack运行时(runtime)
const configuration = require('./webpack.config.js')
let compiler = webpack('configuration');
new webpack.ProgressPlugin().apply(compiler)
compiler.run(function(err, stats) {// ...
})
6、配置(Configuration)在线配置示例:https://createapp.dev/webpack/no-library–html-webpack-plugin
你可能已经注意到,很少有webpack配置看起来完全相同。这是因为webpack的配置文件是Javascript文件,文件内导出了一个webpack配置对象。webpack会根据该配置定义的属性进行处理。
由于webpack遵循Commonjs模板规范,因此,你可以在配置中使用:
请在合适的场景,使用这些功能。
虽然技术上可行,但还是应避免如下操作:
Tip
此文档中得出最重要的结论是,webpack的配置可以有许多不同的样式和风格。关键在于,为了易于维护和理解这些配置,需要在团队内部保证一致。
接下来的示例中,展示了webpack配置如何实现即可表达,又可灵活配置,这主要得益于 配置即为代码:
6.1、基本配置webpack.config.js
const path = require('path');
module.exports = {mode: 'development',
entry: './foo.js',
output: {path: path.resolve(__dirname, 'dist'),
filename: 'foo.bundle.js',
}
}
6.2、多个target除了可以将单个配置导出为object、function或Promise以外,还可以将其导出为多个配置。
6.3、使用其他配置语言Webpack支持有多种编程和数据语言编写的配置文件。
7、模块(Modules)在模块化编程中,开发者将程序分解为功能离散的chunk,并称之为模块。
每个模块都拥有小于完整程序的体积,使得验证、调试及测试变得轻而易举。精心编写的模块提供了可靠的抽象和封装界限,使得应用程序中每个模块都具备了条理清晰的设计和明确的目的。
Node.js从一开始就支持模块化编程。然而,web的模块化正在缓慢支持中。在web界存在多种支持Javascript模块化工具,这些工具各有优势和限制。Webpack从这些系统中汲取经验和教训,并将模块的概念应用到项目的任何文件中。
7.1、何为webpack模块与Node.js模块相比,webpack模块能以各种方式表达它们的依赖关系。下面是一些示例:
Webpack天生支持如下模块类型:
通过loader可以使webpack支持多种语言和预处理器语法编写的模块。loader向webpack描述了如何处理非原生模块,并将相关依赖引入到你的bundles中。webpack社区已经为各种流行的语言和预处理器创建了loader,其中包括:
总的来说,webpack提供了可定制,强大且丰富的API,允许在任何技术栈中使用,同时支持在开发、测试和生产环境的工作流中做到无入侵性。
8、模块解析(Module Resolution)resolver是一个帮助寻找模块绝对路径的库。一个模块可以作为另一个模块的依赖模块,然后被后者引用,如下:
import foo from 'path/to/module';
// 或者
require('path/to/module')
所依赖的模块可以是来自应用程序的代码或第三方库。resolver帮助webpack从每个require/import语句中,找到需要引入到bundle中的模块代码。当打包模块时,webpack使用enhanced-resolve来解析文件路径。
8.1、webpack中的解析规则使用enhanced-resolve,webpack能解析三种文件路径:
绝对路径
import '/home/me/file';
import 'c:\\Users\\me\\file';
由于已经获得文件的绝对路径,因此不需要再做进一步解析。
相对路径
import '../src/file1';
import './file2';
在这种情况下,使用import或require的源文件所在的目录,被认为是上下文目录,在import/require中给定的相对路径,会拼接此上下文路径,来生成模块的绝对路径。
模块路径
import 'module';
import 'module/lib/file';
在resolve.modules中指定的所有目录中检索模块。你可以通过配置别名的方式来替换初始模块路径,具体请参照resolve.alias配置选项。
一旦根据上述规则解析路径后,resolver将会检查路径是指向文件还是文件夹。如果路径指向文件:
如果路径指向一个文件夹,则进行如下步骤寻找具有正确扩展名的文件:
Webpack会根据构建目标,为这些选项提供合理的默认配置。
8.2、解析loaderloader的解析规则也遵循特定的规范。但是resolveLoader配置项可以为loader设置独立的解析规则。
8.3、缓存每次文件系统访问文件都会被缓存,以便更快触发对同一文件的多个并行或串行请求。在watch模式下,只有修改过的文件会被从缓存中移出。如果关闭watch模式,则会在每次编译前清理缓存。
9、Module Fedration 9.1、动机多个独立的构建可以组成一个应用程序,这些独立的构建之间不应该存在依赖关系,因此可以单独开发和部署他它们。
9.2、底层概念我们区分本地模块和远程模块。本地模块即为普通模块,是当前构建的一部分。远程模块不属于当前构建,并在运行时从所谓的容器加载。
加载远程模块被认为是异步操作。当使用远程模块时,这些异步操作将被放置在远程模块和入口之间的下一个chunk的加载操作中。如果没有chunk加载操作,就不能使用远程模块。
chunk的加载操作通常是通过调用import()实现的,但也支持像require.ensure或require([…])子类的旧语法。
容器是由容器入口创建的,该入口暴露了对特定模块的异步访问。暴露的访问分为两个步骤:
步骤1将在chunk加载期间完成。步骤2将在与其他(本地和远程)的模块交错执行期间完成。这样一来,执行顺序不受模块从本地转换为远程或远程转换为本地的影响。
容器可以嵌套使用,容器可以使用来自其他容器的模块。容器之间也可以循环依赖。
9.3、高级概念每个构建都充当一个容器,也可以将其他构建作为容器。通过这种方式,每个构建都能够通过从对应容器中加载模块来访问其他容器暴露出来的模块。
共享模块是指即可重写的又可作为向嵌套容器提供重写的模块。它们通常指向每个构建中的相同模块,例如相同的库。
packageName选项允许通过设置包名来查找所需要的版本。默认情况下,它会自动推断模块请求,当想禁用自动推断时,请将requiredVersion设置为false。
9.4、构建块(Building blocks)ContainerPlugin(low level)
该插件使用指定的公开模块来创建一个额外的容器入口。
ContainerReferencePlugin(low level)
该插件将特定的引用添加到作为外部资源(externals)的容器中,并允许从这些容器中导入远程模块。它还会调用这些容器的override API来为它们提供重载(当构建也是一个容器时,通过__webpack__override__或override API)和指定的重载被提供给所有引用的容器。
ModuleFederationPlugin(high level)
ModuleFederation组合了ContainerPlugin和ContainerReferencePlugin。
每个页面单独构建
单页应用的每个页面都是在单独的构建中从容器暴露出来的。主体应用程序(application shell)也是独立构建,会将所有页面作为远程模块来引用。通过这种方式,可以单独部署每个页面。在更新路由或添加新路由时部署主体应用程序。主体应用程序将常用库定义为共享模块,以避免在页面构建中出现重复。
将组件库作为容器
许多应用程序共享一个通用的组件库,可以将其构建成暴露所有组件的容器。每个应用程序使用来自组件库容器的组件。可以单独部署对组件库的更改,而不需要重新部署所有应用程序。应用程序自动使用组件库的最新版本。
动态远程容器
该容器接口支持 get 和 init 方法。 init 是一个兼容 async 的方法,调用时,只含有一个参数:共享作用域对象(shared scope object)。此对象在远程容器中用作共享作用域,并由 host 提供的模块填充。 可以利用它在运行时动态地将远程容器连接到 host 容器。
(async () =>{// 初始化共享作用域(shared scope)用提供的已知此构建和所有远程的模块填充它
await __webpack_init_sharing__('default');
const container = window.someContainer; // 或从其他地方获取容器
// 初始化容器 它可能提供共享模块
await container.init(__webpack_share_scopes__.default);
const module = await container.get('./module');
})();
容器尝试提供共享模块,但是如果共享模块已经被使用,则会发出警告,并忽略所提供的共享模块。容器仍能将其作为降级模块。
你可以通过动态加载的方式,提供一个共享模块的不同版本,从而实现 A/B 测试。
Tip
在尝试动态连接远程容器之前,确保已加载容器。
例子:
init.js
function loadComponent(scope, module) {return async () =>{// 初始化共享作用域(shared scope)用提供的已知此构建和所有远程的模块填充它
await __webpack_init_sharing__('default');
const container = window[scope]; // 或从其他地方获取容器
// 初始化容器 它可能提供共享模块
await container.init(__webpack_share_scopes__.default);
const factory = await window[scope].get(module);
const Module = factory();
return Module;
};
}
loadComponent('abtests', 'test123');
9.7、基于Promise的动态Remote一般来说,remote是使用URL配置的,示例如下:
module.exports = {plugins: [
new ModuleFederationPlugin({ name: 'host',
remotes: {app1: ''
}
})
]
}
但是你也可以向remote传递一个promise,其会在运行时被调用。你应该用符合上面描述的get/init接口的模板来调用这个promise。例如,如果你想传递你应该使用哪个版本的联邦模块,你可以通过一个查询参数做一些事情。
9.8、动态Public Path提供一个host api以设置publicPath
可以允许host在运行时通过公开远程模块的方法来设置远程模块的publicPath。
当你在host域的子路径上挂载独立部署的子应用程序时,这种方法特别有用。
场景:
你在 https://my-host.com/app/* 上有一个 host 应用,并且在 https://foo-app.com 上有一个子应用。子应用程序也挂载在 host 域上, 因此, https://foo-app.com 可以通过 https://my-host.com/app/foo-app 访问,并且 https://my-host.com/app/foo-app/* 可以通过代理重定向到 https://foo-app.com/*。
示例:
webpack.config.js (remote)
module.exports = {entry: {remote: './public-path',
},
plugins: [
new ModuleFederationPlugin({ name: 'remote', // 该名称必须与入口名称相匹配
exposes: ['./public-path'],
// ...
}),
],
};
public-path.js (remote)
export function set(value) {__webpack_public_path__ = value;
}
src/index.js (host)
const publicPath = await import('remote/public-path');
publicPath.set('/your-public-path');
//bootstrap app e.g. import('./bootstrap.js')
10、更多https://webpack.docschina.org/
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
售后响应及时
7×24小时客服热线数据备份
更安全、更高效、更稳定价格公道精准
项目经理精准报价不弄虚作假合作无风险
重合同讲信誉,无效全额退款