demo
// main.js
import { name, age } from "./msg.js";
let msg = 123;
console.log(name);
// msg.js
export const name = "name";
export const age = "age";
// 通过 https://astexplorer.net/ 查看 ast语法树
// rollup.config.js
import babel from "@rollup/plugin-babel";
export default {
input: "./main.js",
output: {
file: "dist/bundle.js",
format: "es",
},
plugins: [
babel({
babelHelpers: "bundled",
exclude: "node_modules/**",
presets: ["@babel/preset-env"],
}),
],
};
// npm script
"build": "rollup -c"
复制代码
1. 从入口出发
- rollup -c 命令
// package.json中bin和main字段
"bin": {
"rollup": "dist/bin/rollup"
}
"main": "dist/rollup.js",
复制代码
- bin/rollup
const rollup = require('../shared/rollup.js')
// 参数
const command = yargsParser(process.argv.slice(2))
// 执行rollup
runRollup(command);
async function runRollup(command) {
const { options } = await getConfigs(command);
for (const inputOptions of options) {
await build(inputOptions, warnings, command.silent);
}
}
// build执行打包
async function build(inputOptions) {
// rollup函数返回的是bundle 里面几个方法
const bundle = await rollup.rollup(inputOptions);
// 调用rollup函数返回的 write方法
await Promise.all(outputOptions.map(bundle.write));
}
复制代码
2.rollup函数
// shared/rollup.js
function rollup(rawInputOptions) {
return rollupInternal(rawInputOptions, null);
}
// 只关系主流程 不 care watcher
function rollupInternal(rawInputOptions, watcher) {
// 1. 处理参数 标准化参数 会有一些默认的参数
const { options: inputOptions, unsetOptions: unsetInputOptions } = await getInputOptions(rawInputOptions, watcher !== null);
// 2. 初始化 Graph(rollup的核心) 类比 webpack 的 compiler 编译对象?
const graph = new Graph(inputOptions, watcher);
// 3. 执行 build 方法 生成 module
// rollup也是基础插件的 有很多钩子的
await graph.pluginDriver.hookParallel('buildStart', [inputOptions]);
await graph.build();
await graph.pluginDriver.hookParallel('buildEnd', []);
// 4. 定义write函数 返回结果
const result = {
async close() {},
async generate() {},
watchFiles: Object.keys(graph.watchFiles),
// 执行rollup生成bundle之后 会调用这个write方法
async write(rawOutputOptions) {
return handleGenerateWrite(true, inputOptions, unsetInputOptions, rawOutputOptions, graph);
}
}
}
复制代码
2.1.getInputOptions
function getInputOptions(rawInputOptions, watchMode) {
// 确保plugins是数组
const rawPlugins = ensureArray(rawInputOptions.plugins);
// 格式化配置 rollup.config.js
const { options, unsetOptions } = normalizeInputOptions(
await rawPlugins.reduce(
// 处理插件的参数 options 包装成promise数组
applyOptionHook(watchMode),
Promise.resolve(rawInputOptions)
)
);
// 格式化插件 设置默认插件名 index
normalizePlugins(options.plugins);
return { options, unsetOptions };
}
复制代码
2.2 Graph
// 初始化 这里面很多概念
class Graph {
constructor(options, watcher) {
this.modulesById = {}
this.modules =[]
this.options = options
// 1. 路径追踪系统 先跳过
this.deoptimizationTracker = new PathTracker();
// 缓存
this.cachedModules = new Map();
if(options.cache !== false) {}
// 监听模式
if(watcher) {}
// 2.插件驱动器 注入文件操作方法 插件环境上下文等操作 basePluginDriver
this.pluginDriver = new PluginDriver()
// 3. 全局作用域 作用域 分很多种 全局 模块 块级 等等
this.scope = new GlobalScope();
// 4. js parser acorn
this.acornParser = acorn.Parser.extend(...options.acornInjectPlugins);
// 5. 模块加载器 用于模块的解析和加载
this.moduleLoader = new ModuleLoader();
}
}
复制代码
2.3 PluginDriver
// 插件驱动器 使用 pluginDriver 来执行插件中的hook
class PluginDriver {
constructor(graph, options, userPlugins, pluginCache, basePluginDriver) {
this.basePluginDriver = basePluginDriver; // 根插件驱动器
// 为插件设置上下文
this.pluginContexts.set()
}
hookFirst(){}
hookParallel(){}
// 执行插件 hook.apply()
runHook(){}
}
复制代码
2.4 GlobalScope
// 全局作用域 对应的 GlobalVariable
class GlobalScope extends Scope {}
class Scope{
// 添加到作用域中
addDeclaration() {}
}
复制代码
2.5 ModuleLoader
class ModuleLoader {
constructor(graph, modulesById, options, pluginDriver) {
this.modulesById = modulesById;
}
// 类比 webpack 中 的 compilation addEntry ?
async addEntryModules(unresolvedEntryModules, isUserDefined) {
}
}
复制代码
3. build
// 执行 graph的build方法 主要用来生成 module
function build() {
// 1.构建模块图
await this.generateModuleGraph();
// 2.排序
// this.sortModules();
// 3. 标记为包含
this.includeStatements();
}
复制代码
3.1 generateModuleGraph
async generateModuleGraph() {
await this.moduleLoader.addEntryModules(normalizeEntryModules(this.options.input))
}
function normalizeEntryModules(entryModules) {
return Object.entries(entryModules).map(([name, id]) => ({
fileName: null,
id,
implicitlyLoadedAfter: [],
importer: undefined,
name,
}));
}
复制代码
3.2 addEntryModules
// name id filename
function addEntryModules(unresolvedEntryModules) {
unresolvedEntryModules.map(({ id, importer }) =>
// 对文件进行解析分析依赖 id是文件的路径 从入口开始 这里就是 main.js
this.loadEntryModule(id, true, importer, null)
)
}
复制代码
3.3 loadEntryModule
function loadEntryModule() {
// 找到绝对路径
const resolveIdResult = await resolveId();
return this.fetchModule(this.addDefaultsToResolvedId(resolveIdResult), undefined);
}
function addDefaultsToResolvedId(resolvedId) {
return {
id: resolvedId.id
}
}
复制代码
3.4 fetchModule
function fetchModule({ id }, importer, isEntry) {
// 1. 生成模块
const module = new Module()
// 2. 建立对应关系
this.modulesById.set(id, module);
// 3. 为每个模块添加监听
this.graph.watchFiles[id] = true;
// 4. 获取文件的内容
await this.addModuleSource(id, importer, module);
// 5. 执行钩子
await this.pluginDriver.hookParallel("moduleParsed", [module.info]);
// 6. 处理依赖 递归处理
await this.fetchStaticDependencies(module),
await this.fetchDynamicDependencies(module),
// 7. linkImports
module.linkImports();
// 8. 返回模块
return module
}
复制代码
3.4.1 Module
// 简单理解为每个文件都是一个module
class Module {
constructor(graph, id, options, isEntry) {
this.context = options.moduleContext(id);
this.info = {}
}
}
复制代码
3.4.2 addModuleSource
function addModuleSource(id, importer, module) {
// 1. load modules 执行load方法或者直接读取文件内容
let source =
(await this.pluginDriver.hookFirst("load", [id])) ?? (await readFile(id))
// 2. 添加到模块内容上
const sourceDescription = { code: source };
module.setSource(
await transform(
sourceDescription,
module,
this.pluginDriver,
this.options.onwarn
)
)
}
复制代码
3.4.3 transform
function transform() {
return pluginDriver.hookReduceArg0('transform').then(code => {
return {ast, code}
})
}
复制代码
3.4.4 setSource
import { nodeConstructors } from "./ast/nodes/index";
function setSource(ast, code) {
// 1. acorn.Parser.parse(code) 生成ast语法树
if (!ast) ast = this.graph.contextParse(this.info.code);
// 2. magicString
this.magicString = new MagicString(code, {});
// 3.ast上下文
this.astContext = {
addExport: this.addExport.bind(this),
addImport: this.addImport.bind(this),
nodeConstructors,
};
// 4. 模块作用域
this.scope = new ModuleScope(this.graph.scope, this.astContext);
// 5. 分析ast
this.ast = new Program(ast, { context: this.astContext, type: 'Module' }, this.scope)
this.info.ast = ast
}
function addImport() {
const source = node.source.value;
this.sources.add(source);
}
复制代码
3.4.5 nodeConstructors
// 不同ast节点的构造函数 在 Program中会使用
import ImportDeclaration from "./ImportDeclaration";
import Program from "./Program";
import Identifier from "./Identifier";
export const nodeConstructors = {
ImportDeclaration,
Program,
Identifier,
};
复制代码
3.4.6 Program
class Program extends NodeBase {
render(code, options) {
if (this.boy.length) {
renderStatementList(this.body, code, this.start, this.end, options);
} else {
super.render(code, options);
}
}
}
复制代码
3.4.7 NodeBase
class NodeBase extends ExpressionEntity {
constructor(esTreeNode, parent, parentScope) {
// 创建作用域
this.createScope(parentScope);
// 解析节点
this.parseNode(esTreeNode);
this.initialise();
}
// 我们的 esTreeNode 结果大概是 {start: end: body: []} 这样的
parseNode(esTreeNode) {
// 分析body中的调用对应的构造器
for (const [key, value] of Object.entries(esTreeNode)) {
if (Array.isArray(value)) {
// body属性 [statement]
for (const child of value) {
this[key].push(
child === null
? null
: // ImportDeclaration class ImportDeclaration
new this.context.nodeConstructors[child.type]()
);
}
}
}
}
}
// ImportDeclaration
class ImportDeclaration extends NodeBase {
// 调用上下文中的 addImport
initialise() {
// this.sources.add(source); './msg'在这里被添加到 sources中了
this.context.addImport(this);
}
}
复制代码
3.4.8 fetchStaticDependencies
// 处理依赖 处理source属性 我们import中添加了一个msg 递归生成 msg对应的module
function fetchStaticDependencies() {
Array.from(module.sources, async (source) =>
// this.fetchModule(resolvedId, importer, false); 会区分是否为外部依赖
this.fetchResolvedDependency(source, module.id, module.resolvedIds)
)
}
复制代码
3.5 result
// build函数完成 我们得到 main和msg对应的两个module
// graph 的结构大致如下
{
modules: [mainModule, msgModule],
modulesById: {main: mainModule, msg: msgModule},
moduleLoader:ModuleLoader,
pluginDriver:PluginDriver,
scope:GlobalScope: {children: [moduleScope, moduleScope]}
}
// module 中的属性
{
graph,
id,
name,
filename,
imports: [],
exports: [],
scope,
ast,
info
}
复制代码
4. bundle.write
// 经过rollup的过程 我们得到了modules 然后我们 实例话bundle 然后根据module生成chunk
// 将 outputBundle 写入到文件系统中
// rollup 函数返回 write
async write(rawOutputOptions) {
return handleGenerateWrite(true, inputOptions, unsetInputOptions, rawOutputOptions, graph);
}
async function handleGenerateWrite() {
// 1. 标准化配置 创建插件驱动器 createOutputPluginDriver
// outputOptions: {file:'dist/bundle.js'}
const { options: outputOptions, outputPluginDriver } =
getOutputOptionsAndPluginDriver();
// 2. 创建 bundle
const bundle = new Bundle(outputOptions, outputPluginDriver, graph);
// 3.调用bundle的generate方法
const generated = await bundle.generate();
// 4. 写入到文件系统中
await Promise.all(
Object.values(generated).map((chunk) =>
// fs.writeFile 判断sourcemap
writeOutputFile(chunk, outputOptions)
)
);
// 5. 返回
return createOutput(generated);
}
复制代码
4.1 getOutputOptionsAndPluginDriver
function getOutputOptionsAndPluginDriver() {
// new PluginDriver(); fileEmitter 为什么要 new 一个
const outputPluginDriver = inputPluginDriver.createOutputPluginDriver(rawPlugins);
return {
...getOutputOptions(),
outputPluginDriver
}
}
复制代码
4.2
class Bundle {
constructor(outputOptions, unsetOptions, inputOptions, pluginDriver, graph) {
this.outputOptions = outputOptions;
this.unsetOptions = unsetOptions;
this.inputOptions = inputOptions;
this.pluginDriver = pluginDriver;
this.graph = graph;
}
}
复制代码
4.3 bundle.generate
function generate() {
const outputBundle = Object.create(null);
// 1. renderStart 插件
await this.pluginDriver.hookParallel('renderStart', [this.outputOptions, this.inputOptions]);
// 2. 生成chunks new Chunk()
const chunks = await this.generateChunks()
// 3. 获取公共的路径
const inputBase = commondir(getAbsoluteEntryModulePaths(chunks));
// 4. 预渲染 chunks
// 我们需要在预渲染之前创建插件 预渲染和渲染之间没有异步代码
// {banner: '', footer: '', intro: '', outro: ''}
const addons = await createAddons(this.outputOptions, this.pluginDriver);
// 调用 chunk 的 preRender 方法
this.prerenderChunks(chunks, inputBase);
// 5. 将chunk加入到bundle中 调用chunk 的 render方法
// Object.assign(outputChunk, await chunk.render())
await this.addFinalizedChunksToBundle(chunks, inputBase, addons, outputBundle);
this.finaliseAssets(outputBundle);
// code在哪里处理的 tree-shaking如何处理的
// bundle.js: { code:'const name = "name";\n\nconsole.log(name);\n', fileName:'bundle.js'}
return outputBundle
}
复制代码
4.3.1 generateChunks
function generateChunks() {
const chunks = []
const chunkByModule = new Map()
// {alias: null, modules: [main.js, msg.js]} 我们上一步生成的两个 module
const chunkDefinitions = getChunkAssignments(this.graph.entryModules)
for(const { alias, modules } of chunkDefinitions) {
// 生成chunk 和webpack有点类似 一般是一个入口对应一个chunk (动态导入的怎么处理的?)
const chunk = new Chunk(modules, this.inputOptions)
chunks.push(chunk)
}
return [...chunks]
}
function addManualChunks(manualChunks) {
addModuleToManualChunk(alias, entry, manualChunkAliasByEntry);
}
function getChunkAssignments() {
for(const entry of entryModules) {
assignEntryToStaticDependencies(entry, null);
}
// dynamicEntryModules
// assignEntryToStaticDependencies()
// {alias: null, modules}
chunkDefinitions.push(...createChunks([...entryModules, ...dynamicEntryModules]));
return chunkDefinitions;
}
复制代码
4.3.2 prerenderChunks
function prerenderChunks() {
for (const chunk of chunks) {
chunk.generateExports();
}
for (const chunk of chunks) {
chunk.preRender(this.outputOptions, inputBase);
}
}
复制代码
4.3.3 addFinalizedChunksToBundle
function addFinalizedChunksToBundle() {
for(const chunk of chunks) {
// 为 outputBundle 添加chunk
outputBundle[chunk.id] = chunk.getChunkInfoWithFileNames();
}
await Promise.all(chunks.map(async (chunk) => {
const outputChunk = outputBundle[chunk.id];
// 调用 chunk的render方法 得到code
Object.assign(outputChunk, await chunk.render(this.outputOptions, addons, outputChunk));
}))
}
复制代码
4.3.4 finaliseAssets
function finaliseAssets(outputBundle) {
for(const file of Object.values(outputBundle)) {
if (this.outputOptions.validate && typeof file.code == 'string') {
this.graph.contextParse(file.code)
}
}
this.pluginDriver.finaliseAssets();
}
复制代码
5. chunk
class Chunk {
constructor(orderedModules, modulesById) {
this.orderedModules = orderedModules
this.modulesById = modulesById;
this.imports = new Set();
const chunkModules = new Set(orderedModules);
// 我们生成的 main msg两个module
for(const module of orderedModules) {
}
}
}
复制代码
5.1 preRender
function preRender(options, inputBase) {
const magicString = new MagicStringBundle()
const renderOptions = {}
for(const module of this.orderedModules) {
// 调用module的render方法
const source = module.render(renderOptions)
// 我们最终得到的资源还是module render生成的内容
this.renderedModuleSources.set(module, source);
magicString.addSource(source);
// [name] [age] 没有用到的就需要 removed
const { renderedExports, removedExports } = module.getRenderedExports();
const { renderedModuleSources } = this;
// 渲染的模块 最终生成的 code
renderedModules[module.id] = {
get code() {
var _a, _b;
return (_b =
(_a = renderedModuleSources.get(module)) === null || _a === void 0
? void 0
: _a.toString()) !== null && _b !== void 0
? _b
: null;
},
originalLength: module.originalCode.length,
removedExports,
renderedExports,
};
}
}
复制代码
5.2 render
function render(options, addons, outputChunk) {
// 1. this.dependencies
// 2. finaliseDynamicImports
// 3. finaliseImportMetas
// 4. scope
const format = options.format;
// 不同格式的 我们这里是 es
const finalise = finalisers[format];
// renderedSource 在 preRender中处理
const magicString = finalize(this.renderedSource, {})
const prevCode = magicString.toString();
let code = await renderChunk({code: prevCode})
if (options.sourcemap) {}
return {code, map}
}
复制代码
5.3 renderChunk
function renderChunk({code, options, renderChunk}) {
return outputPluginDriver.hookReduceArg0('renderChunk', [code, renderChunk, options], renderChunkReducer);
}
复制代码
5.4 es
// source: {}
function es(magicString) {
const importBlock = getImportBlock(dependencies, _)
const exportBlock = getExportBlock(exports, _, varOrConst)
return magicString.trim();
}
复制代码
6. module
// 我们在chunk中得到的source是module生成的
for (const module of this.orderedModules) {
// 遍历我们得到的两个module [main, msg]
// 先处理msg 得到 const name = 'name'
// 在处理 main.js 得到 console.log(name)
// 我们得到source 然后根据资源生成对应的 code 写入到文件系统中
const source = module.render(renderOptions).trim();
}
// module的render 我们最终得到的内容是 module生成的
function render() {
const magicString = this.magicString.clone();
this.ast.render(magicString, options)
return magicString;
}
// Program 从这里开始处理 ast节点
function render(code, options) {
renderStatementsList(this.body,code)
}
复制代码
6.1 Program
function render(code, options) {
// 开始处理ast树
renderStatementList(this.body, code, this.start, this.end, options);
}
复制代码
6.2 renderStatementList
function renderStatementList() {
for (let nextIndex = 1; nextIndex <= statements.length; nextIndex++) {
// 不同语句调用自己的render方法
// 我们先处理msg的时候会调用 ExportNamedDeclaration 的render方法
if(currentNode.included) {
currentNode.render(code, options, {})
} else {
// export const age 没有用到需要 treeShaking掉的 included属性何时添加的?
treeshakeNode(currentNode, code)
}
}
}
复制代码
6.2.1 ExportNamedDeclaration
function render(code, options) {
// 将export关键字去掉
code.remove(this.start, this.declaration.start);
// VariableDeclaration
this.declaration.render(code, options, { end, start });
}
复制代码
6.2.2 VariableDeclaration
function render() {
// VariableDeclarator const name = 'name'
for (const declarator of this.declarations) {
declarator.render(code, options);
}
}
复制代码
6.2.3 VariableDeclarator
function render(code, options) {
this.id.render(code, options); // Identifier name
this.init.render(code, options) // Literal 'name'
}
复制代码
6.2.4 ExpressionStatement
// main.js 中 第一个ImportDeclaration 直接 treeshakeNode
// 第二个 let msg VariableDeclaration 也直接treeshake掉
// 第三个是 ExpressionStatement 会被多包装一层 ExpressionStatement
function render(code, options) {
super.render(code, options)
this.insertSemicolon(code);
}
复制代码
6.2.5 NodeBase
function render() {
// ['expression']
for (const key of this.keys) {
value.render(code, options)
}
}
复制代码
6.2.6 CallExpression
function render() {
// 1. callee MemberExpression
this.callee.render(code, options, {})
// 2. arguments name Identifier
for (const arg of this.arguments) {
arg.render(code, options);
}
}
复制代码
6.2.7 MemberExpression
function render() {
// object, property Identifier
this.object.render();
this.property.render(code, options);
}
复制代码
6.2.8 Identifier
function render() {
const name = this.variable.getName();
// 不想等就replace
}
复制代码
6.3 treeshakeNode
function treeshakeNode(node, code, start, end) {
code.remove(start, end)
}
复制代码
6.4 include
// 我们处理ast节点的时候是根据 included 属性来判断是否需要 tree-shaking的
// 我们是什么时间添加的这个属性? 我们之前通过rollup得到我们的module 在module上有ast属性
// 回到rollup函数
// graph.build()
// => generateModuleGraph()
// => addEntryModules()
// => addEntryModules
// => loadEntryModule
// => fetchModule
// => new Module() addModuleSource()
// ==> module.setSource(await transform())
// code: main.js msg.js源代码
function setSource({ast, code}) {
this.astContext = {}
this.scope = new ModuleScope()
this.ast = new Program(ast, { context: this.astContext, type: 'Module' }, this.scope)
}
// 经过处理 ast中的 included:false 都为false 那么什么时候发生变化的?
// 我们会递归的处理 依赖 main中的就是msg
await this.fetchStaticDependencies()
// build完成之后 我们会执行 在这里修改的 included属性
this.includeStatements()
复制代码
6.4.1 includeStatements
function includeStatements() {
module.includeAllExports(false);
for (const module of this.modules) {
// 先处理msg 执行include shouldBeIncluded 为 false
// 在处理main 执行include this.ast.include(content,false)
// 当处理到main的console时候会访问到name变量 然后会递归的去处理msg模块
module.include()
}
}
// 不同的类型不同的只 简单理解为我们访问到的变量就会被包含进行
function include() {
const context = createInclusionContext();
// this.ast = new Program
// NodeBase中 return this.included || (!content.brokenFlow)
// 调用 include 方法就会将 this.included = true;
if(this.ast.shouldBeIncluded(context)) {
this.ast.include(context, false)
}
}
// Program
function include(context, includeChildrenRecursively) {
this.included = true;
// 执行node的include
for (const node of this.body) {
// console.log() 才会执行到
// msg中 name的会执行 但是age不会执行
// 这个属性又是如何确定的?
if(includeChildrenRecursively || node.shouldBeIncluded(context)) {
node.include(context)
}
}
}
// brokenFlow 属性如何确定的
function shouldBeIncluded(context) {
return this.included || (!context.brokenFlow && this.hasEffects(createHasEffectsContext()));
}
// CallExpression
function include() {
this.included = true;
this.callee.include(context, false);
// 处理arguments
this.callee.includeCallArguments(context, this.arguments);
}
// callee MemberExpression
function include() {
this.included = true;
this.object.include(context, includeChildrenRecursively);
this.property.include(context, includeChildrenRecursively);
}
// Identifier
function include() {
this.included = true;
// 调用module的方法 当变量name的时候
this.context.includeVariableInModule(this.variable);
}
function includeVariableInModule() {
// this.graph.needsTreeshakingPass = true; 下次遍历 msg
// 递归处理msg的
this.includeVariable(variable);
this.imports.add(variable); // main模块的 imports中有一个 name变量
}
function includeVariable() {
variable.include(); // LocalVariable
}
// GlobalVariable console log
// VariableDeclaration
function include() {
this.include = true
// name就会被include
for (const declarator of this.declarations) {
if (includeChildrenRecursively || declarator.shouldBeIncluded(context))
declarator.include(context, includeChildrenRecursively);
}
}
// VariableDeclarator id init
复制代码
6.4.2 shouldBeIncluded
// context.brokenFlow
function shouldBeIncluded(context) {
return this.included || (!context.brokenFlow && this.hasEffects())
}
function include() {
const context = createInclusionContext();
}
function createInclusionContext() {
return {
brokenFlow: BROKEN_FLOW_NONE, // 0
}
}
function hasEffects() {
for (const node of this.body) {
if (node.hasEffects(context)) {
return (this.hasCachedEffect = true);
}
}
return false;
}
// ExportNamedDeclaration
function hasEffects(context) {
return this.declaration !== null && this.declaration.hasEffects(context);
}
// NodeBase
function hasEffects(context) {
child.hasEffects(context)
}
// VariableDeclarator
function hasEffects(context) {
// id是变量名 init是值
// class LocalVariable
return this.id.hasEffects(context) || (this.init !== null && this.init.hasEffects(context));
}
// Identifier
function hasEffects() {
// console就会到这里 然后include
return this.variable instanceof GlobalVariable && this.variable.hasEffectsWhenAccessedAtPath(EMPTY_PATH)
}
// ImportDeclaration
function hasEffects() return false
// ExpressionStatement
function hasEffects() {
child.hasEffects()
}
// CallExpression
function hasEffects() {
argument.hasEffects(context)
this.callee.hasEffects(context)
}
// MemberExpression object property
// LocalVariable
function include() {
this.included = true;
// 这里将 export 的name变成 include
for(const declaration of this.declarations) {
declaration.include(createInclusionContext(), false);
}
}
复制代码
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END