webpack学习笔记
参考文章:峰华前端工程师的 Webpack 入门教程 (opens new window)
听了 B 站 UP(峰华前端工程师 (opens new window))讲解
webpack,决定重新整理一下笔记,webpack教程很多,对于新手来说,阅读webpack的文档时常会感到头晕,因为webpack功能强大,插件丰富,在学习的时候,不容易抓到主线。导致之前做笔记的时候,大多是进行文档摘抄,没有一个全局的理解,这次重新编辑一下,梳理一下逻辑
webpack是一个打包工具,它可以将一个入口js文件,梳理 js 文件的依赖情况,将依赖的所有 js 代码打包到一个或多个文件中,这是它的基本功能。
通过webpack的丰富的loader,我们可以增加webpack的打包能力,可以解析除了.js后缀以外的其他文件,如.vue、.css、.png、.svg文件等。
webpack不仅是有打包文件的能力,还有解决文件名缓存,设置路径别名,代码分析等功能,还可以借助插件,可以增加 ES6 转义 ES5、热更新、代码压缩、DevServer 的能力
下面将记录一下webpack的作用、loader 使用、插件使用以及配置文件名缓存、路径别名、ES6 转义 ES5,热更新、代码压缩、DevServer 等功能
# 为什么要使用 webpack
我们在传统的网页开发项目中,需要通过在 html 中引入大量的 JavaScript、CSS 等文件,不仅会导致命名冲突,还会使页面体积变大,因为如果是第三方库,需要加载所有代码。
<script src="./jquery.js"></script>
<script src="./index.js"></script>
<script src="./home.js"></script>
2
3
// index.js
var a = 1
// home.js
var a = 2
2
3
4
5
而在 node.js 出现之后,JavaScript 项目支持通过 require 进行模块化开发了,并且支持 npm 方便的管理依赖:
npm install someLib
require("someLib");
2
3
借着 Node.js 和浏览器 JS 语法的一致性,前端项目开始在 node.js 下开发,完成之后,把代码构建成浏览器支持的形式。对于 react 或 vue 这种组件化开发方式,因为有很多分散的文件,那么就特别需要这样的构建操作。 Webpack 就是进行这一步构建操作的,把 Node.js 模块化的代码转换为浏览器可执行的代码。它提供了 import 和 export ES 6 模块化的语法支持,然后通过分析代码中的 import 导入的依赖,把需要的代码加载进来。 在 webpack 中,任何文件都可以通过 import 导入,只要有对应的 loader 就可以:
import 'style.css'
import 'image/avatar.png'
2
在打包过程中还可以通过插件干预打包过程,例如剔除不必要的代码,形成体积更小的项目。 好,我们具体的来看一个使用 webpack 打包项目的例子。
# webpack 配置文件
Webpack 最核心的一个部分是它的配置文件,在里边我们可以修改入口的 JS 文件,也就是说从哪个文件里边开始寻找 import 依赖,还可以配置出口,也就是最后生成的 JS 文件的一些信息,还能够通过 loader 加载不同类型的文件,再可以通过 plugins 在打包的过程中,对代码进行一些优化或者其他的操作。 我们来测试一下看看,把打包后的文件名修改一下,我们在这里先新建一个 webpack.config.js 文件,这里边使用的是 Node.js 的模块化语法,使用 module.exports,导出一个空的对象,在里边我们配置 webpack 的配置项:
我们添加一个
mode属性,因为我们之前打包的时候,命令行提示了这个mode选项没有设置,webpack默认取的是production,但是我们也可以设置为 development 开发环境,这样的话在开发环境下,webpack打包的代码会不太一样,方便我们开发者进行调试,这里我们把mode先设置成development。看一下
entry,这里我们还是取的默认值,可以把它拿出来,看一下是怎么设置的,这里可以直接把咱们的入口文件路径给它写上,./src/index js。配置打包后的文件名,我们使用
output配置项,传递一个对象属性,配置filename,这个就是打包后的文件名,例如我们改成dist.js,然后存放目录也可以改一下,现在是dist,如果要改成别的话,可以给它配置一个path属性,这里我们可以利用Node.js提供的path库,来获取webpack.config.js所在的目录,基于它我们再去找到一个新的目录,来存放我们打包后的文件,例如说我们还是把它放到dist里边。我们可以调用path.resolve(),resolve接收多个参数可以指定多级目录,例如第一级我们设置为__dirname,然后第二级设置为dist,这样它就会放到dist目录下边了,你也可以把它改成别的名字。
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
filename: 'dist.js',
path: path.resolve(__dirname, 'dist')
}
}
2
3
4
5
6
7
8
9
# 使用 Webpack Loader
# 加载 CSS
要加载 CSS 文件,我们需要安装两个 loader,一个叫 style loader,另一个是 CSS loader。
yarn add style-loader css-loader --dev
安装完成之后,我们需要在 webpack 的配置文件里边去配置它,对于 loader,在 webpack 里边,我们需要先去匹配,以什么样的扩展名结尾的文件,去应用什么样的 loader。 在这里我们:
- 添加一个
module配置项。 - 在里边配置
rules配置项,它的值是一个数组,数组里边的每一个元素,都对应一个loader的配置,每个loader的配置都包含匹配扩展名,以及使用哪些loader和它相关的选项,例如说我们这里配置CSS loader,先给rules传递一个对象。 - 在对象中配置
test属性,它的值是一个正则表达式,用于匹配文件名,这里我们匹配所有以.css结尾的文件,那么这里我们先传递一个正在表达式,然后使用\.来匹配文件名里边的点号,因为点号在正则表达式中有特殊的含义,所以需要反/进行转义,然后写上CSS这个扩展名,再接着使用一个$符号,来匹配文件名的结尾,后边我们可以加上一个i来忽略大小写。 - 给它传递一个
use属性,就是说使用哪些loader,这里我们使用刚才安装好的style loader和css loader。
// webpack.config.js
module: {
rules: [
{
test: /\.css$/i, // 匹配文件结尾,正则 - 忽略大小写
use: ['style-loader', 'css-loader'], // 使用哪些 Loader
},
],
},
2
3
4
5
6
7
8
9
10
# 加载图片
对于图片等静态资源的文件,webpack 原生的支持,所以就不再需要额外的安装 loader 了,我们这里可以:
- 直接在
rules里边,再添加一个配置对象。 - 设置
test属性,这里匹配所有的图片文件,那么我们使用\.,然后使用一个小括号,里边我们要匹配多种图片的扩展名,例如png,然后使用|表示或,svg或jpg或jpeg或gif,然后加上$,匹配扩展名这个结尾,再加上一个i表示忽略大小写。 - 这里因为它是使用内置的
loader,所以使用type属性,设置asset/resource这样的值就可以了。
rules: [
// ...
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource', // 资源类型
}
],
2
3
4
5
6
7
接下来我们测试一下,先添加一张测试图片,在 src 下新建一个 assets 目录,再在它里边新建一个 images 目录,把图片 copy 过来,然后在 index.js 中创建一个 image 元素,把图片展示出来,这里我们先使用 import 把图片导入进来,给他一个名字,比如叫做 HeroImage,然后 from ./assets/images/hero.jpeg,这样导入之后,HeroImage 表示的就是打包后的图片的真实的路径,可以直接用于 img 元素的 src 属性上面。 接下来我们创建 image 元素,然后设置它的 src 属性值,为我们导入进来的 HeroImage,最后把它添加到 body 里边,这里我要把它放到 body 的开头。
import HeroImage from './assets/images/hero.jpeg' // 打包时替换成真实的路径
const image = document.createElement('img')
image.src = HeroImage
document.body.prepend(image)
2
3
4
5
我们打包一下 npx webpack。回到浏览器看一下,这个图片就正常的加载出来了,我们还可以看到在打包之后,这个图片的名字,也变成了随机的字符串。
# 使用插件
# 自动生成 index.html
根目录下的 index.html 是手写的,非常容易出错,需要同步 src 下边的 JS 文件路径,而 webpack 有一个插件,可以自动生成 html 文件,这样就不需要我们再手动去编写代码了,这个插件叫做 HtmlWebpackPlugin。 我们先安装它,并添加到开发者依赖中:
yarn add html-webpack-plugin --dev
打开 webpack.config.js,里边我们使用这个插件: 首先先导入进来,之后在配置对象里边,添加一个 plugins 配置项,它的值是一个数组,在里边我们可以加载多种插件,加上 HtmlWebpackPlugin,它导出的是一个构造函数,我们使用 new 调用它,这样就加载好了这个插件:
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: {},
plugins: [new HtmlWebpackPlugin()]
}
2
3
4
5
运行一下 npx webpack,dsit 目录下会多一个 index.html,这个就是它自动帮我们生成的 html 代码
不过看一下这个 html 的标题是默认生成的,HtmlWebpackPlugin 插件支持我们传递参数,来自定义 html 代码的生成。例如,这里可以传递一个 title 属性,设置一下网页的标题,例如叫做博客列表:
new HtmlWebpackPlugin({title: "博客列表"}),
# 转译 ES6 为 ES5 插件
我们在开发前端项目的时候,有的时候会使用新的 ECMAScript 特性,但是要兼容低版本的浏览器,那么我们可以利用 Babel 这个工具,来转译咱们的 JS 代码。webpack 也支持相应的 loader,我们看一下来怎么实现。 首先安装 babel-loader,@babel/core,@babel/preset-env 这 3 个依赖:
yarn add --dev babel-loader @babel/core @babel/preset-env
安装完成之后:
- 在
webpack配置项里边,我们再新添加一个loader配置,这里我们要匹配JS结尾的文件,那么使用test,匹配./js这个扩展名。 - 使用
exclude,把nodule_modules这个目录给去掉,这样它就不会转译它下边的代码。 - 使用
use来配置使用哪些loader,这里我们使用对象形式,因为我们需要给loader,传递一些自定义的配置。 - 使用
loader配置项,指定要使用哪个loader,我们这里使用babel loader。 - 使用
options来给这个loader传递一些配置,配置presets,使用@babel/preset-env,这样就能够自动转译代码了。 - 为了方便我们看打包后的代码,我们可以在
webpack的配置项里,添加一个devtool配置项,把它设置为inline-source map,这样方便我们查看打包后的源代码。
devtool: 'inline-source-map', // 方便查看源码
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
}
}
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 压缩 JS 代码
webpack 的另一个常见的场景,就是压缩打包后的 JS 代码,这样的话可以减少打包后的文件体积,他需要一个叫做 terser-webpack-plugin 的插件,我们这里先安装一下:
yarn add --dev terser-webpack-plugin
好安装完成之后,我们在 webpack.config.js 里边配置它。
- 导入进来,把它保存到一个叫做
terser-webpack-plugin的常量里边。 - 在导出的配置对象里边,添加一个
optimization配置项,配置minimize是否要压缩,把它的值设置为true。 - 配置
minimizer,用什么工具来压缩,我们这里使用安装的terser-webpack-plugin,这里直接调用new TerserPlugin()这个构造函数就可以了。
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
optimization: {
minimize: true,
minimizer: [new TerserPlugin()]
}
}
2
3
4
5
6
7
8
# Dev Server 开发服务器
现在我们在开发的时候,需要每次在改动 JS 之后都要重新打包,webpack 提供了一个 dev server 开发服务器,它可以在启动之后,如果我们修改的 JS 代码,它就会自动重新打包并刷新页面,我们来看一看怎么来使用这个工具。首先先安装它 :
yarn add --dev webpack-dev-server
接下来我们需要指定 dev server 要从哪里去加载代码,我们打开 webpack.config.js,在配置项里边再添加一个 devServer 配置项,给他的值设置为一个对象,然后设置 static,给他指定咱们的 dist 目录:
devServer: {
static: './dist', // 从哪里找打包好的文件
},
2
3
接下来,为了方便我们运行开发服务器,我们在 package.json 里边再新添加一个 script,例如,我们可以使用 yarn start 来启动开发服务器,那么这里我们添加一个 scripts 配置项,然后在里边配置 start 命令,值为 webpack serve --open,这样就能够启动 webpack dev server,并自动打开浏览器:
"scripts": {
"start": "webpack serve --open", // 自动打开浏览器
},
2
3
# 文件名缓存
现在我们打包后的文件,这个 dist.js 每次都是一样的,但是浏览器会根据这个文件名,进行缓存,一般我们为了避免浏览器进行缓存,我们会给文件名加上一串随机的字符,每次更新之后都改为新的字符,那么 webpack 也支持自动在打包后,生成新的一串字符,我们看一下怎么来实现。 打开 webpack.config.js,这里我们需要
- 配置
output,这里filename我们配置了dist.js,但是我们也可以把它改成一个带有 ·,也就是不重复的字符串的文件名。 - 把它的值改成
[name][contenthash].js,这里name也可以写死,不过写 [name] 的话,webpack会自动把它替换成main,就是默认的文件名,contenthash会每次根据文件的内容进行hash计算,得出一串不重复的字符:
output: {
filename: 'dist.js',
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
},
2
3
4
5
# 路径别名
接下来我们看一下,如何给导入的路径设置别名,有的时候,JS 文件所在的目录可能会嵌套的比较深,要引入其他的目录下边的 JS 文件,需要使用很多 ../ 来访问这个相对路径。但是 webpack 可以让我们指定一个路径别名,来把这一串相对路径给替换掉,这样就少写一些字符串。
配置路径别名,打开 webpack.config.js,再添加一个 resolve 配置项,它的值也是一个对象,然后添加 alias 配置项,它里面的属性名就是路径别名,值就是真实的路径:
resolve: {
alias: {
utils: path.resolve(__dirname, 'src/utils/'),
},
},
2
3
4
5
# 管理输出
# HtmlWebpackPlugin
HtmlWebpackPlugin插件可以自动输出 HTML,并嵌入多个出口 js
# 清理 dist 文件夹(出口文件夹)
webpack 每次打包不会在出口文件夹检测里面的文件是否都用到了,仅仅是将生成的文件放到了输出目录(dist)里面。可以采用output.clean配置项来实现这个构建后没有旧文件。
# manifest
webpack 和 webpack 插件知道哪些文件应该生成,它是通过 manifest 来实现的。可以通过WebpackManifestPlugin插件可以提取一个 json 文件。
# 开发环境
将mode设置为development
# source map
多个依赖最终会打包为一个输出文件,使用 source map 功能可以追踪到错误和警告对应于源代码中的位置
module.exports = {
devtool: 'inline-source-map'
}
2
3
# 源代码改变后自动编译(webpack-dev-server)
webpack 可以做到在源代码发生改变时,提供了几种可选的方式帮助我们自动编译代码,可以根据项目需要选择,浏览器应用一般需要 webpack-dev-server。以下是几种可选方式:
- watch mode
- webpack-dev-server
- webpack-dev-middleware
# watch mode
在 package.json 的 scripts 中配置--watch参数:
{
"scripts": {
"watch": "webpack --watch"
}
}
2
3
4
5
这种方式会自动重新编译修改后的模块,唯一的缺点是浏览器需要刷新。
# webpack-dev-server
webpack-dev-server 提供了一个基本的 web server,具有实时重新加载的功能。配置方法是在 webpack.config.json 中添加 devServer 字段:
module.exports = {
devServer: {
static: './dist'
},
optimizatino: {
runtimeChunk: 'single'
}
}
2
3
4
5
6
7
8
因为在这个示例中单个 HTML 页面有多个入口,所以添加了 optimization.runtimeChunk: 'single' 配置。没有这个配置的话,我们可能会遇到 这个问题 (opens new window)。 查看 代码分割 (opens new window) 章节获取更多细节。
webpack-dev-server 会从 output.path 中定义的目录中的 bundle 文件提供服务,即文件将可以通过 http://[devServer.host]:[devServer.port]/[output.publicPath]/[output.filename] 进行访问。
添加 scripts 到 package.json
{
"scripts": {
"start": "webpack serve --open"
}
}
2
3
4
5
# webpack-dev-middleware
webpack-dev-server 在内部使用了 webpack-dev-middleware,然而 webpack-dev-middleware 也可以单独来使用。比如可以配合 express server 来使用,参考连接 (opens new window)
# 代码分离
一种不够灵活的方式:入口起点包含多个文件时,如果这些文件内部包含相同的依赖,最后的输出将会包含重复的依赖。
# 防止重复(prevent duplication)
# 入口依赖
在入口配置 dependOn 选项,可以在多个 chunk 之间共享模块
const path = require('path')
module.exports = {
mode: 'development',
entry: {
// index: './src/index.js',
// another: './src/another-module.js',
index: {
import: './src/index.js',
dependOn: 'shared'
},
another: {
import: './src/another-module.js',
dependOn: 'shared'
},
shared: 'lodash'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
如果我们要在一个 HTML 页面上使用多个入口时,还需设置 optimization.runtimeChunk: 'single',否则还会遇到这里 (opens new window)所述的麻烦。
module.exports = {
optimization: {
runtimeChunk: 'single'
}
}
2
3
4
5
# SplitChunksPlugin
SplitChunksPlugin 插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。
module.exports = {
optimization: {
splitChunks: {
chunks: 'all'
}
}
}
2
3
4
5
6
7
使用 optimization.splitChunks 配置选项之后,现在应该可以看出,index.bundle.js 和 another.bundle.js 中已经移除了重复的依赖模块。需要注意的是,插件将 lodash 分离到单独的 chunk,并且将其从 main bundle 中移除,减轻了大小。
# 动态导入
进行动态代码拆分有两种方式,第一种是采用 EMCAScript 提案的 import()语法,第二种是使用 webpack 遗留功能 require.ensure
import()内部会用到 promise,在旧版本的浏览器中使用需要使用一个 polyfill 库(例如 es6-promise 或 promise-polifill)来 shim Promise
# 预获取/预加载模块
在动态导入的时候,对 import()使用内置指令,可以使得最后生成的 link 标签上有"prefetch"/"preload"
import(/* webpackPrefetch: true */ './path/to/LoginModal.js')
会生成,并在浏览器闲置时间预取 login-modal-chunk.js
感觉这一块用来做性能优化的
# 缓存
浏览器缓存机制是为了节省网络流量,提高加载速度。然而在开发时,这不利于调试,因为文件名没有改变时,浏览器会利用缓存,不会更新
# 修改输出文件名
output.filename 可以采用可替换模板字符串的方式来使得每次更改完文件,输出的文件名都不同。
模板规则可以查看:https://webpack.docschina.org/configuration/output/#template-strings (opens new window)
# 提取引导模板(boilerplate)
可以将第三方库(例如 lodash 或 react)提取到单独的 vendor chunk 文件中,他们很少会频繁修改,可以利用浏览器长缓存机制,命中缓存来消除请求。
可以使用 SplitChunksPlugin 插件的 cacheGroups 选项来实现,我们在 optimization.splitChunks 添加如下 cacheGroups 参数并构建:
module.exports = {
optimization: {
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
这里的single表示输出为一个 runtime chunk 文件
# 模板标识符
每个模块的 id 会默认的基于解析顺序(resolve order)进行增量,即当解析顺序发生改变,id 也会发生改变
我们只期望内容发生改变的模块的输出文件名发生变化,而内容没有修改的模块,它们的输出文件名不要发生改变,尤其是提取引导模块的 vendor chunk 的输出文件名不需要改变。我们将 optimization.moduleIds 设置为 'deterministic':
module.exports = {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
title: 'Caching'
})
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true
},
optimization: {
moduleIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 创建库(library)
编写一些工具库的时候,可以选择 webpack 来进行打包。
需要安装 webpack、webpack-cli、lodash
需要将他们都安装到 devDependency 中,而不是 dependency,因为我们不需要将他们打包到我们的库中,否则我们的库体积很容易变得很大。
# 暴露库(expose library)
通过 output.library 配置项暴露从入口导出的内容
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'webpack-numbers.js',
library: 'webpackNumbers'
}
}
2
3
4
5
6
7
8
9
10
将入口七起点公开为 webpackNumbers,这样用户就可以通过 script 标签使用它:
<script src="https://example.org/webpack-numbers.js"></script>
<script>
window.webpackNumbers.wordToNum('Five');
</script>
2
3
4
可以设置 type 来打包一个库
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'webpack-numbers.js',
library: 'webpackNumbers',
library: {
name: 'webpackNumbers',
type: 'umd'
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
类型默认包括 'var'、 'module'、 'assign'、 'assign-properties'、 'this'、 'window'、 'self'、 'global'、 'commonjs'、 'commonjs2'、 'commonjs-module'、 'commonjs-static'、 'amd'、 'amd-require'、 'umd'、 'umd2'、 'jsonp' 以及 'system',除此之外也可以通过插件添加。
# 外部化 lodash
lodash 会被打包到代码中,更倾向于将 lodash 放到 peerDependency 中,可以通过 externals 配置来完成
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'webpack-numbers.js',
library: {
name: 'webpackNumbers',
type: 'umd'
}
},
externals: {
lodash: {
commonjs: 'lodash',
commonjs2: 'lodash',
amd: 'lodash',
root: '_'
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这意味着你的 library 需要一个名为 lodash 的依赖,这个依赖在 consumer 环境中必须存在且可用。
# 外部化的限制
对于想要实现从一个依赖中调用多个文件的那些 library,
import A from 'library/one'
import B from 'library/two'
// ...
2
3
4
无法通过在 externals 中指定整个 library 的方式,将它们从 bundle 中排除。而是需要逐个或者使用一个正则表达式,来排除它们。
module.exports = {
//...
externals: [
'library/one',
'library/two',
// 匹配以 "library/" 开始的所有依赖
/^library\/.+$/
]
}
2
3
4
5
6
7
8
9
# 生产环境
在 package.json 中添加 main 字段来输出结果
# webpack 环境变量
webpack 命令行参数--env 参数可以传入任意数量的环境变量,在 webpack.config.js 中可以访问到这些环境变量。例如--env production 或--env goal=local
webpack 配置之前默认导出的是一个对象,限制要修改为一个函数以接受 env 环境变量
const path = require('path')
module.exports = (env) => {
// Use env.<YOUR VARIABLE> here:
console.log('Goal: ', env.goal) // 'local'
console.log('Production: ', env.production) // true
return {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 构建性能
最新版本的 webpack 和 nodejs
# loader
减少 loader 数量
通过 include 字段,仅将 loader 应用在实际需要转换的模块
const path = require('path')
module.exports = {
//...
module: {
rules: [
{
test: /\.js$/,
include: path.resolve(__dirname, 'src'),
loader: 'babel-loader'
}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 引导(bootstrap)
。。。略