file-based-routing-for-static-sites
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFile Based Routing for Static Sites
静态站点的基于文件的路由方案
In this article I will go over how to use file based routing to output as a static site multi page application.
Step 1
步骤1
Create a new folder called “static-site-file-based-routing” and open it up in VSCode.
mkdir static-site-file-based-routing
cd static-site-file-based-routing
code .创建一个名为“static-site-file-based-routing”的新文件夹,并在VSCode中打开它。
mkdir static-site-file-based-routing
cd static-site-file-based-routing
code .Step 2
步骤2
Create a and replace it with the following:
tsconfig.json{
"compilerOptions": {
"incremental": true,
"target": "es5",
"module": "es2020",
"outDir": "dist",
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"typeRoots": [
"node_modules/@types",
"src/@types"
]
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}创建文件,并替换为以下内容:
tsconfig.json{
"compilerOptions": {
"incremental": true,
"target": "es5",
"module": "es2020",
"outDir": "dist",
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"typeRoots": [
"node_modules/@types",
"src/@types"
]
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}Step 3
步骤3
Create a and update it with the following:
package.json{
"name": "static-site-file-based-routing",
"version": "1.0.0",
"description": "File based routing for HTML MPA",
"type": "module",
"scripts": {
"start": "wds --node-resolve --root-dir build --base-path /static-site-file-based-routing --open --watch",
"postinstall": "npm run tsc",
"build": "node dist/main.js --inputDir ./example --outputDir ./build",
"dev": "node dist/main.js --inputDir ./example --outputDir ./build -w",
"tsc": "tsc",
"tsc:watch": "tsc -w"
},
"devDependencies": {
"@pkgjs/parseargs": "^0.10.0",
"@types/markdown-it": "^12.2.3",
"@types/node": "^18.7.23",
"@web/dev-server": "^0.1.34",
"chokidar": "^3.5.3",
"highlight.js": "^11.6.0",
"html-format": "^1.0.2",
"markdown-it": "^13.0.1",
"parse5": "^7.1.1",
"typescript": "^4.8.4"
}
}Then run to install all the dependencies.
npm installI am usingto serve the site locally. You can use any server you want.@web/dev-server
These dependencies are used for various file transformations such as markdown to HTML, HTML formatting, and file watching.
创建文件,并更新为以下内容:
package.json{
"name": "static-site-file-based-routing",
"version": "1.0.0",
"description": "File based routing for HTML MPA",
"type": "module",
"scripts": {
"start": "wds --node-resolve --root-dir build --base-path /static-site-file-based-routing --open --watch",
"postinstall": "npm run tsc",
"build": "node dist/main.js --inputDir ./example --outputDir ./build",
"dev": "node dist/main.js --inputDir ./example --outputDir ./build -w",
"tsc": "tsc",
"tsc:watch": "tsc -w"
},
"devDependencies": {
"@pkgjs/parseargs": "^0.10.0",
"@types/markdown-it": "^12.2.3",
"@types/node": "^18.7.23",
"@web/dev-server": "^0.1.34",
"chokidar": "^3.5.3",
"highlight.js": "^11.6.0",
"html-format": "^1.0.2",
"markdown-it": "^13.0.1",
"parse5": "^7.1.1",
"typescript": "^4.8.4"
}
}然后运行安装所有依赖。
npm install我使用在本地运行站点,你也可以使用任何其他服务器。@web/dev-server
这些依赖用于各种文件转换,例如Markdown转HTML、HTML格式化以及文件监听等功能。
Step 4
步骤4
Create a folder and add 4 files:
src创建文件夹,并添加4个文件:
srcsrc/build.ts
src/build.tssrc/build.ts
src/build.tsimport * as fs from "fs";
import { compileDir, compileTarget } from "./compile.js";
import * as path from "path";
import chokidar from "chokidar";
import { publicDirectory } from "./static.js";
interface Options {
inputDir?: string;
outputDir?: string;
publicDir?: string;
watch?: boolean;
clean?: boolean;
}
export default async function build(options: Options) {
const inputDir = options.inputDir || "www";
const outputDir = options.outputDir || "build";
const publicDir = options.publicDir || "public";
const watch = options.watch || false;
const clean = options.clean || false;
if (!fs.existsSync(inputDir)) {
throw new Error(`Input directory ${inputDir} does not exist`);
}
if (clean) {
if (fs.existsSync(outputDir)) {
fs.rmdirSync(outputDir, { recursive: true });
}
}
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
if (watch) {
console.log("Watching for changes...");
chokidar.watch(inputDir).on("all", async (event, inputFile) => {
console.log(event, inputFile);
if (fs.existsSync(inputFile)) {
const relativePath = path.relative(inputDir, inputFile);
const outputFile = `${outputDir}/${relativePath}`;
const stat = fs.statSync(inputFile);
if (stat.isDirectory()) {
await compileDir(inputFile, outputFile);
} else if (stat.isFile()) {
const filename = path.basename(inputFile);
if (filename === "layout.html") {
// Rebuild all related directories
const dir = path.dirname(inputFile);
const inDir = path.relative(inputDir, dir);
const outDir = `${outputDir}/${inDir}`;
await compileDir(dir, outDir);
} else {
await compileTarget(inputFile, outputFile);
}
}
}
});
} else {
await compileDir(inputDir, outputDir);
}
if (publicDir.split(',').length > 1) {
for (const dir of publicDir.split(',')) {
publicDirectory(dir, outputDir, watch);
}
} else {
publicDirectory(publicDir, outputDir, watch);
}
}import * as fs from "fs";
import { compileDir, compileTarget } from "./compile.js";
import * as path from "path";
import chokidar from "chokidar";
import { publicDirectory } from "./static.js";
interface Options {
inputDir?: string;
outputDir?: string;
publicDir?: string;
watch?: boolean;
clean?: boolean;
}
export default async function build(options: Options) {
const inputDir = options.inputDir || "www";
const outputDir = options.outputDir || "build";
const publicDir = options.publicDir || "public";
const watch = options.watch || false;
const clean = options.clean || false;
if (!fs.existsSync(inputDir)) {
throw new Error(`Input directory ${inputDir} does not exist`);
}
if (clean) {
if (fs.existsSync(outputDir)) {
fs.rmdirSync(outputDir, { recursive: true });
}
}
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
if (watch) {
console.log("Watching for changes...");
chokidar.watch(inputDir).on("all", async (event, inputFile) => {
console.log(event, inputFile);
if (fs.existsSync(inputFile)) {
const relativePath = path.relative(inputDir, inputFile);
const outputFile = `${outputDir}/${relativePath}`;
const stat = fs.statSync(inputFile);
if (stat.isDirectory()) {
await compileDir(inputFile, outputFile);
} else if (stat.isFile()) {
const filename = path.basename(inputFile);
if (filename === "layout.html") {
// Rebuild all related directories
const dir = path.dirname(inputFile);
const inDir = path.relative(inputDir, dir);
const outDir = `${outputDir}/${inDir}`;
await compileDir(dir, outDir);
} else {
await compileTarget(inputFile, outputFile);
}
}
}
});
} else {
await compileDir(inputDir, outputDir);
}
if (publicDir.split(',').length > 1) {
for (const dir of publicDir.split(',')) {
publicDirectory(dir, outputDir, watch);
}
} else {
publicDirectory(publicDir, outputDir, watch);
}
}src/compile.ts
src/compile.tssrc/compile.ts
src/compile.tsimport * as fs from "fs";
import * as path from "path";
import MarkdownIt from "markdown-it";
import hljs from "highlight.js";
import * as parse5 from "parse5";
import type { Document } from "parse5/dist/tree-adapters/default.js";
import format from 'html-format';
function compile(file: string) {
const raw = fs.readFileSync(file, "utf-8");
const ext = path.extname(file);
switch (ext) {
case ".md":
case ".markdown":
const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true,
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return (
'<pre class="hljs"><code>' +
hljs.highlight(str, { language: lang, ignoreIllegals: true })
.value +
"</code></pre>"
);
} catch (__) {
console.error(__);
}
}
return "";
},
});
return parse5.parse(md.render(raw));
case ".html":
return parse5.parse(raw);
default:
break;
}
return raw;
}
function createHtml(options?: { head?: string; body?: string; }) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
${options?.head ?? ""}
</head>
<body>
${options?.body ?? "<slot></slot>"}
</body>
</html>
`;
}
export async function compileFile(file: string, target: string) {
// Use regex to check if ends with index.*
const isIndex = /index\.([a-z]+)$/i.test(file);
if (!isIndex && !fs.statSync(file).isDirectory()) {
// Skipping for nested layouts
return;
}
const parent = path.dirname(target);
// Render up the directory tree until we hit the root directory
const files: string[] = [file];
let filePath = file;
while (filePath !== parent) {
filePath = path.dirname(filePath);
// Check for root level layout
const layout = path.join(filePath, "layout.html");
if (fs.existsSync(layout)) {
files.unshift(layout);
}
if (filePath === ".") break;
}
// Check for root level index markdown or html
const layout = path.join(parent, "layout.html");
if (fs.existsSync(layout)) {
if (
fs.existsSync(path.join(parent, "index.html")) ||
fs.existsSync(path.join(parent, "index.md")) ||
fs.existsSync(path.join(parent, "index.markdown"))
) {
files.unshift(layout);
}
}
let output = createHtml();
for (const item of files) {
const doc = compile(item);
if (typeof doc !== 'string') {
const content = parse5.serialize(doc);
output = mergeDocuments(output, content);
}
}
// Replace extension
const ext = path.extname(file);
const newFile = target.replace(ext, '.html');
// Check if parent directory exists
const parentDir = path.dirname(newFile);
if (!fs.existsSync(parentDir)) {
fs.mkdirSync(parentDir, { recursive: true });
}
fs.writeFileSync(newFile, output);
console.log(`--> ${newFile}`);
}
function mergeDocuments(current: string, source: string) {
let raw = current;
// Merge body
const html = extractDoc(parse5.parse(source));
// Check for <slot></slot>
const hasSlot = raw.includes("<slot></slot>");
if (hasSlot) {
raw = raw.replace("<slot></slot>", parse5.serialize(html.body));
} else {
// Append to body
const endBodyIdx = raw.lastIndexOf("</body>");
const start = raw.slice(0, endBodyIdx);
const end = raw.slice(endBodyIdx);
const body = parse5.serialize(html.body);
raw = start + body + end;
}
// Merge head
const endHeadIdx = raw.lastIndexOf("</head>");
const start = raw.slice(0, endHeadIdx);
const end = raw.slice(endHeadIdx);
const head = parse5.serialize(html.head);
raw = start + head + end;
// Format
raw = format(raw);
// Remove duplicate title tags
const lastTitle = raw.lastIndexOf("<title>");
const lastTitleEnd = raw.lastIndexOf("</title>");
const title = raw.slice(lastTitle, lastTitleEnd + 8);
raw = raw.replace(/<title>.*<\/title>/, "");
raw = raw.replace("</head>", title + "</head>");
return raw;
}
function extractDoc(doc: Document) {
const html = (doc.childNodes[1] ?? doc.childNodes[0]) as unknown as Document;
const head = html.childNodes.find(
(node) => node.nodeName === "head"
) as unknown as Document;
const body = html.childNodes.find(
(node) => node.nodeName === "body"
) as unknown as Document;
return { head, body };
}
export async function compileDir(inputDir: string, outputDir: string) {
const files = fs.readdirSync(inputDir);
for (const file of files) {
const inputFile = `${inputDir}/${file}`;
const outputFile = `${outputDir}/${file}`;
await compileTarget(inputFile, outputFile);
}
}
export async function compileTarget(input: string, output: string) {
const stat = fs.statSync(input);
if (stat.isDirectory()) {
if (!fs.existsSync(output)) {
fs.mkdirSync(output, { recursive: true });
}
await compileDir(input, output);
} else if (stat.isFile()) {
const ext = path.extname(input);
if (['.html', '.md', '.markdown'].includes(ext)) {
await compileFile(input, output);
} else {
const current = fs.readFileSync(input);
const parentDir = path.dirname(output);
if (!fs.existsSync(parentDir)) {
fs.mkdirSync(parentDir, { recursive: true });
}
if (fs.existsSync(output)) {
// Check if content is the same
const previous = fs.readFileSync(output);
if (Buffer.compare(current, previous) !== 0) {
fs.writeFileSync(output, current);
}
} else {
// Copy the file
fs.copyFileSync(input, output);
}
}
}
}import * as fs from "fs";
import * as path from "path";
import MarkdownIt from "markdown-it";
import hljs from "highlight.js";
import * as parse5 from "parse5";
import type { Document } from "parse5/dist/tree-adapters/default.js";
import format from 'html-format';
function compile(file: string) {
const raw = fs.readFileSync(file, "utf-8");
const ext = path.extname(file);
switch (ext) {
case ".md":
case ".markdown":
const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true,
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return (
'<pre class="hljs"><code>' +
hljs.highlight(str, { language: lang, ignoreIllegals: true })
.value +
"</code></pre>"
);
} catch (__) {
console.error(__);
}
}
return "";
},
});
return parse5.parse(md.render(raw));
case ".html":
return parse5.parse(raw);
default:
break;
}
return raw;
}
function createHtml(options?: { head?: string; body?: string; }) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
${options?.head ?? ""}
</head>
<body>
${options?.body ?? "<slot></slot>"}
</body>
</html>
`;
}
export async function compileFile(file: string, target: string) {
// Use regex to check if ends with index.*
const isIndex = /index\.([a-z]+)$/i.test(file);
if (!isIndex && !fs.statSync(file).isDirectory()) {
// Skipping for nested layouts
return;
}
const parent = path.dirname(target);
// Render up the directory tree until we hit the root directory
const files: string[] = [file];
let filePath = file;
while (filePath !== parent) {
filePath = path.dirname(filePath);
// Check for root level layout
const layout = path.join(filePath, "layout.html");
if (fs.existsSync(layout)) {
files.unshift(layout);
}
if (filePath === ".") break;
}
// Check for root level index markdown or html
const layout = path.join(parent, "layout.html");
if (fs.existsSync(layout)) {
if (
fs.existsSync(path.join(parent, "index.html")) ||
fs.existsSync(path.join(parent, "index.md")) ||
fs.existsSync(path.join(parent, "index.markdown"))
) {
files.unshift(layout);
}
}
let output = createHtml();
for (const item of files) {
const doc = compile(item);
if (typeof doc !== 'string') {
const content = parse5.serialize(doc);
output = mergeDocuments(output, content);
}
}
// Replace extension
const ext = path.extname(file);
const newFile = target.replace(ext, '.html');
// Check if parent directory exists
const parentDir = path.dirname(newFile);
if (!fs.existsSync(parentDir)) {
fs.mkdirSync(parentDir, { recursive: true });
}
fs.writeFileSync(newFile, output);
console.log(`--> ${newFile}`);
}
function mergeDocuments(current: string, source: string) {
let raw = current;
// Merge body
const html = extractDoc(parse5.parse(source));
// Check for <slot></slot>
const hasSlot = raw.includes("<slot></slot>");
if (hasSlot) {
raw = raw.replace("<slot></slot>", parse5.serialize(html.body));
} else {
// Append to body
const endBodyIdx = raw.lastIndexOf("</body>");
const start = raw.slice(0, endBodyIdx);
const end = raw.slice(endBodyIdx);
const body = parse5.serialize(html.body);
raw = start + body + end;
}
// Merge head
const endHeadIdx = raw.lastIndexOf("</head>");
const start = raw.slice(0, endHeadIdx);
const end = raw.slice(endHeadIdx);
const head = parse5.serialize(html.head);
raw = start + head + end;
// Format
raw = format(raw);
// Remove duplicate title tags
const lastTitle = raw.lastIndexOf("<title>");
const lastTitleEnd = raw.lastIndexOf("</title>");
const title = raw.slice(lastTitle, lastTitleEnd + 8);
raw = raw.replace(/<title>.*<\/title>/, "");
raw = raw.replace("</head>", title + "</head>");
return raw;
}
function extractDoc(doc: Document) {
const html = (doc.childNodes[1] ?? doc.childNodes[0]) as unknown as Document;
const head = html.childNodes.find(
(node) => node.nodeName === "head"
) as unknown as Document;
const body = html.childNodes.find(
(node) => node.nodeName === "body"
) as unknown as Document;
return { head, body };
}
export async function compileDir(inputDir: string, outputDir: string) {
const files = fs.readdirSync(inputDir);
for (const file of files) {
const inputFile = `${inputDir}/${file}`;
const outputFile = `${outputDir}/${file}`;
await compileTarget(inputFile, outputFile);
}
}
export async function compileTarget(input: string, output: string) {
const stat = fs.statSync(input);
if (stat.isDirectory()) {
if (!fs.existsSync(output)) {
fs.mkdirSync(output, { recursive: true });
}
await compileDir(input, output);
} else if (stat.isFile()) {
const ext = path.extname(input);
if (['.html', '.md', '.markdown'].includes(ext)) {
await compileFile(input, output);
} else {
const current = fs.readFileSync(input);
const parentDir = path.dirname(output);
if (!fs.existsSync(parentDir)) {
fs.mkdirSync(parentDir, { recursive: true });
}
if (fs.existsSync(output)) {
// Check if content is the same
const previous = fs.readFileSync(output);
if (Buffer.compare(current, previous) !== 0) {
fs.writeFileSync(output, current);
}
} else {
// Copy the file
fs.copyFileSync(input, output);
}
}
}
}src/static.ts
src/static.tssrc/static.ts
src/static.tsimport * as fs from "fs";
import * as path from "path";
import chokidar from "chokidar";
export function publicDirectory(publicDir: string, outputDir: string, watch: boolean) {
if (watch) {
if (fs.existsSync(publicDir)) {
chokidar.watch(publicDir).on("all", (event, inputFile) => {
console.log(event, inputFile);
if (fs.existsSync(inputFile)) {
const relativePath = path.relative(publicDir, inputFile);
const outputFile = `${outputDir}/${relativePath}`;
const stat = fs.statSync(inputFile);
if (stat.isDirectory()) {
copyStaticFiles(inputFile, outputFile);
} else if (stat.isFile()) {
fs.copyFileSync(inputFile, outputFile);
}
}
});
}
} else {
// Copy static files
if (fs.existsSync(publicDir)) {
copyStaticFiles(publicDir, outputDir);
}
}
}
function copyStaticFiles(inDir: string, outDir: string) {
const files = fs.readdirSync(inDir);
for (const file of files) {
const inputFile = `${inDir}/${file}`;
const outputFile = `${outDir}/${file}`;
const stat = fs.statSync(inputFile);
if (stat.isDirectory()) {
if (!fs.existsSync(outputFile)) {
fs.mkdirSync(outputFile, { recursive: true });
}
copyStaticFiles(inputFile, outputFile);
} else if (stat.isFile()) {
fs.copyFileSync(inputFile, outputFile);
}
}
}import * as fs from "fs";
import * as path from "path";
import chokidar from "chokidar";
export function publicDirectory(publicDir: string, outputDir: string, watch: boolean) {
if (watch) {
if (fs.existsSync(publicDir)) {
chokidar.watch(publicDir).on("all", (event, inputFile) => {
console.log(event, inputFile);
if (fs.existsSync(inputFile)) {
const relativePath = path.relative(publicDir, inputFile);
const outputFile = `${outputDir}/${relativePath}`;
const stat = fs.statSync(inputFile);
if (stat.isDirectory()) {
copyStaticFiles(inputFile, outputFile);
} else if (stat.isFile()) {
fs.copyFileSync(inputFile, outputFile);
}
}
});
}
} else {
// Copy static files
if (fs.existsSync(publicDir)) {
copyStaticFiles(publicDir, outputDir);
}
}
}
function copyStaticFiles(inDir: string, outDir: string) {
const files = fs.readdirSync(inDir);
for (const file of files) {
const inputFile = `${inDir}/${file}`;
const outputFile = `${outDir}/${file}`;
const stat = fs.statSync(inputFile);
if (stat.isDirectory()) {
if (!fs.existsSync(outputFile)) {
fs.mkdirSync(outputFile, { recursive: true });
}
copyStaticFiles(inputFile, outputFile);
} else if (stat.isFile()) {
fs.copyFileSync(inputFile, outputFile);
}
}
}src/main.ts
src/main.tssrc/main.ts
src/main.ts#!/usr/bin/env node
// @ts-ignore
import { parseArgs } from "@pkgjs/parseargs";
import build from "./build.js";
export async function main() {
const {
values: { inputDir, outputDir, watch },
} = parseArgs({
options: {
inputDir: {
type: "string",
short: "i",
},
outputDir: {
type: "string",
short: "o",
},
watch: {
type: "boolean",
short: "w",
},
},
allowPositional: true,
});
if (inputDir === undefined || outputDir === undefined) {
console.log("Usage: build -i <inputDir> -o <outputDir> [-w]");
return;
}
await build({
inputDir,
outputDir,
watch,
});
}
main();#!/usr/bin/env node
// @ts-ignore
import { parseArgs } from "@pkgjs/parseargs";
import build from "./build.js";
export async function main() {
const {
values: { inputDir, outputDir, watch },
} = parseArgs({
options: {
inputDir: {
type: "string",
short: "i",
},
outputDir: {
type: "string",
short: "o",
},
watch: {
type: "boolean",
short: "w",
},
},
allowPositional: true,
});
if (inputDir === undefined || outputDir === undefined) {
console.log("Usage: build -i <inputDir> -o <outputDir> [-w]");
return;
}
await build({
inputDir,
outputDir,
watch,
});
}
main();Step 5
步骤5
Now that the project is setup we can start the typescript compiler in watch mode.
npm run ts:watch现在项目已设置完成,我们可以启动TypeScript编译器的监听模式。
npm run ts:watchStep 6
步骤6
Now create a folder that will contain the source files for the website.
现在创建一个包含网站源文件的文件夹。
example/index.md
example/index.mdexample/index.md
example/index.mdundefinedundefinedHello World
Hello World
This is a test
undefinedThis is a test
undefinedexample/style.css
example/style.cssexample/style.css
example/style.cssbody {
background-color: #000;
color: #fff;
}body {
background-color: #000;
color: #fff;
}example/layout.html
example/layout.htmlexample/layout.html
example/layout.html<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Example</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<slot></slot>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Example</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<slot></slot>
</body>
</html>Step 7
步骤7
With the source files created we can now run the build script.
For a one time build run:
npm run buildFor a watch mode run:
npm run devNow start the http server:
npm run start源文件创建完成后,我们可以运行构建脚本。
如需一次性构建,请运行:
npm run build如需启用监听模式,请运行:
npm run dev现在启动HTTP服务器:
npm run startConclusion
总结
As you make changes it will only update affected files and be very fast to update.
Note that this does not bundle the javascript and will be up to you if you are usingin any files (for the example in the repo I show how to use UNPKG).node_modules
If you want to find the source code you can check it out here otherwise thanks for reading and let me know if you have any questions!
当你修改文件时,只会更新受影响的文件,更新速度非常快。
注意:本方案不会打包JavaScript代码,如果你在文件中使用,需要自行处理(在仓库的示例中,我展示了如何使用UNPKG)。node_modules
如果你想获取源码,请查看这里。感谢阅读,如有疑问欢迎提出!