hexo-theme-stellar 1.34.0 → 1.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/_config.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  ######## Stellar info ########
2
2
  stellar:
3
- version: '1.34.0'
3
+ version: '1.35.0'
4
4
  homepage: 'https://xaoxuu.com/wiki/stellar/'
5
5
  repo: 'https://github.com/xaoxuu/hexo-theme-stellar'
6
6
  main_css: /css/main.css
@@ -537,15 +537,6 @@ plugins:
537
537
  service: flying_pages # flying_pages
538
538
  flying_pages: https://gcore.jsdelivr.net/npm/flying-pages@2/flying-pages.min.js
539
539
 
540
- # pjax - 无刷新加载
541
- # 启用后点击链接将使用 AJAX 加载内容,无需整页刷新
542
- pjax:
543
- enable: false # 还在测试中,可能存在兼容问题
544
- selectors: ['title', '#l_cover', '.l_body'] # 需要替换的元素选择器
545
- timeout: 10000 # 请求超时时间(毫秒)
546
- cacheBust: false # 是否添加时间戳防止缓存
547
- translate-distance: 8px # 动画
548
-
549
540
  # https://fancyapps.com/docs/ui/fancybox/
550
541
  # available for {% image xxx %}
551
542
  fancybox:
@@ -139,9 +139,6 @@ function generate_canonical() {
139
139
  if (path.startsWith('/404') || path.startsWith('404')) {
140
140
  return '';
141
141
  }
142
- if (path.endsWith('.html') && path.length > 5) {
143
- path = path.split('.html')[0];
144
- }
145
142
  return `<link rel="canonical" href="https://${originalHost}${path}">`;
146
143
  }
147
144
  return '';
@@ -146,29 +146,12 @@
146
146
  document.head.appendChild(script)
147
147
  }),
148
148
 
149
- // PJAX 事件监听器管理
150
- _pjaxListeners: new Set(),
151
-
152
- jq: (fn, pjax = true) => {
149
+ jq: (fn) => {
153
150
  if (typeof jQuery === 'undefined') {
154
151
  utils.js(deps.jquery).then(fn)
155
152
  } else {
156
153
  fn()
157
154
  }
158
- if (pjax && !fn._pjax_registered) {
159
- document.addEventListener('pjax:complete', fn);
160
- fn._pjax_registered = true;
161
- utils._pjaxListeners.add(fn); // 追踪监听器以便清理
162
- }
163
- },
164
-
165
- // 清理所有 PJAX 事件监听器
166
- cleanupPjaxListeners: () => {
167
- utils._pjaxListeners.forEach(fn => {
168
- document.removeEventListener('pjax:complete', fn);
169
- fn._pjax_registered = false;
170
- });
171
- utils._pjaxListeners.clear();
172
155
  },
173
156
 
174
157
  onLoading: (el) => {
@@ -334,20 +317,6 @@
334
317
  }
335
318
  },
336
319
 
337
- // 重新初始化所有插件(用于 PJAX 等场景)
338
- reinitPlugins: () => {
339
- utils._pluginInitializers.forEach(plugin => {
340
- // 跳过标记为不需要重新初始化的插件
341
- if (plugin.options.skipReinit) return;
342
-
343
- try {
344
- plugin.fn();
345
- } catch (error) {
346
- console.error(`[Plugin ${plugin.name}] 重新初始化失败:`, error);
347
- }
348
- });
349
- },
350
-
351
320
  // 清理所有插件资源
352
321
  cleanupPlugins: () => {
353
322
  utils._pluginCleanups.forEach((cleanup, name) => {
@@ -363,21 +332,9 @@
363
332
  // 清理所有资源(用于页面卸载或重置)
364
333
  cleanupAll: () => {
365
334
  utils.cleanupPlugins();
366
- utils.cleanupPjaxListeners();
367
335
  },
368
336
  };
369
337
 
370
- // 自动处理 PJAX 事件
371
- document.addEventListener('pjax:before', () => {
372
- // 在导航前清理旧的 PJAX 监听器(如果需要重新注册)
373
- // 注意:这里不清理,因为监听器应该是持久的
374
- // utils.cleanupPjaxListeners();
375
- });
376
-
377
- document.addEventListener('pjax:complete', () => {
378
- utils.reinitPlugins();
379
- });
380
-
381
338
  // utils.dark.mode 当前模式 dark or light
382
339
  // utils.dark.toggle() 暗黑模式触发器
383
340
  // utils.dark.push(callBack[,"callBackName"]) 传入触发器回调函数
package/layout/layout.ejs CHANGED
@@ -69,7 +69,6 @@ if (theme.style.prefers_theme === 'auto') {
69
69
  html += `<div class="scripts">`
70
70
  html += partial('_partial/scripts')
71
71
  html += `</div>`
72
- html += `<div class="page-loading-bar"><img src="${theme.default.loading}"></div>`
73
72
  html += `</body>`
74
73
  html += `</html>`
75
74
  %>
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "hexo-theme-stellar",
3
- "version": "1.34.0",
3
+ "version": "1.35.0",
4
4
  "description": "Elegant and powerful theme for Hexo.",
5
5
  "main": "package.json",
6
6
  "scripts": {
7
- "test": "echo test"
7
+ "test": "echo test",
8
+ "release": "node release.js",
9
+ "release:dry": "node release.js --dry-run"
8
10
  },
9
11
  "repository": {
10
12
  "type": "git",
@@ -3,6 +3,8 @@
3
3
  'use strict';
4
4
 
5
5
  hexo.on('generateBefore', () => {
6
+ // 页面路径归一化:xxx.html → xxx/,必须先于所有读取 page.path 的逻辑
7
+ require('./lib/path_normalize')(hexo);
6
8
  // Merge config.
7
9
  require('./lib/config')(hexo);
8
10
  require('./lib/links')(hexo);
@@ -65,4 +67,3 @@ hexo.extend.filter.register('before_generate', async () => {
65
67
  }
66
68
  });
67
69
 
68
-
@@ -4,13 +4,15 @@
4
4
 
5
5
  'use strict';
6
6
 
7
+ const { normalize_path } = require('../../lib/path_utils');
8
+
7
9
  class WikiPage {
8
10
  constructor(page) {
9
11
  this.id = page._id
10
12
  this.wiki = page.wiki
11
13
  this.title = page.title
12
14
  this.path = page.path
13
- this.path_key = page.path.replace('.html', '')
15
+ this.path_key = normalize_path(page.path)
14
16
  this.layout = page.layout
15
17
  this.updated = page.updated
16
18
  }
@@ -107,7 +109,7 @@ module.exports = ctx => {
107
109
  for (let id of Object.keys(item.tree)) {
108
110
  const sec = item.tree[id]
109
111
  for (let key of sec) {
110
- let hs = sub_pages.filter(p => p.path_key == item.base_dir + key)
112
+ let hs = sub_pages.filter(p => p.path_key == normalize_path(item.base_dir + key))
111
113
  if (hs.length > 0) {
112
114
  homepage = hs[0]
113
115
  break
@@ -134,8 +136,9 @@ module.exports = ctx => {
134
136
  for (let title of Object.keys(item.tree)) {
135
137
  var sec = { title: title, pages: []}
136
138
  for (let key of item.tree[title]) {
137
- sec.pages = sec.pages.concat(sub_pages.filter(p => p.path_key == item.base_dir + key))
138
- others = others.filter(p => p.path_key != item.base_dir + key)
139
+ const pagePathKey = normalize_path(item.base_dir + key)
140
+ sec.pages = sec.pages.concat(sub_pages.filter(p => p.path_key == pagePathKey))
141
+ others = others.filter(p => p.path_key != pagePathKey)
139
142
  }
140
143
  sections.push(sec)
141
144
  }
@@ -5,6 +5,8 @@
5
5
 
6
6
  'use strict';
7
7
 
8
+ const { normalize_path } = require('../../lib/path_utils');
9
+
8
10
  class RelatedPage {
9
11
  constructor(page) {
10
12
  this.id = page._id
@@ -12,7 +14,7 @@ class RelatedPage {
12
14
  this.topic = page.topic
13
15
  this.title = page.title
14
16
  this.path = page.path
15
- this.path_key = page.path.replace('.html', '')
17
+ this.path_key = normalize_path(page.path)
16
18
  this.layout = page.layout
17
19
  this.date = page.date
18
20
  this.updated = page.updated
@@ -4,6 +4,8 @@
4
4
 
5
5
  'use strict'
6
6
 
7
+ const { normalize_path } = require('../../lib/path_utils');
8
+
7
9
  class NotePage {
8
10
  constructor(page) {
9
11
  this.id = page._id
@@ -11,7 +13,7 @@ class NotePage {
11
13
  this.title = page.title
12
14
  this.tags = page.tags
13
15
  this.path = page.path
14
- this.path_key = page.path.replace('.html', '')
16
+ this.path_key = normalize_path(page.path)
15
17
  this.layout = page.layout
16
18
  this.date = page.date
17
19
  this.updated = page.updated || page.date
@@ -39,8 +41,8 @@ function prepareNotebook(id, info, ctx) {
39
41
  if (notebook.base_dir.startsWith('/')) {
40
42
  notebook.base_dir = notebook.base_dir.substring(1)
41
43
  }
42
- if (notebook.base_dir.endsWith('/')) {
43
- notebook.base_dir = notebook.base_dir.substring(0, notebook.base_dir.length - 1)
44
+ if (notebook.base_dir.length > 1 && !notebook.base_dir.endsWith('/')) {
45
+ notebook.base_dir = notebook.base_dir + '/'
44
46
  }
45
47
  } else {
46
48
  const notebooksBaseDir = ctx.theme.config.site_tree.notebooks.base_dir
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 页面路径归一化:将 xxx.html 归一为目录形式 xxx/
5
+ * 必须在 generateBefore 最早阶段执行(先于 doc_tree / notebooks 等读取 page.path),
6
+ * 直接改写 Page model 存储,保证 page.permalink、JSON-LD、sitemap、search.json
7
+ * 与 canonical(尾斜杠格式)一致。
8
+ */
9
+ module.exports = ctx => {
10
+ const data = ctx.model('Page').data || {}
11
+ Object.values(data).forEach(page => {
12
+ if (!page || typeof page.path !== 'string') {
13
+ return
14
+ }
15
+ if (page.layout === false || page.layout === 'false') {
16
+ return
17
+ }
18
+ if (page.path.endsWith('.html') && !page.path.endsWith('/index.html')) {
19
+ page.path = page.path.replace(/\.html$/, '/')
20
+ }
21
+ })
22
+ }
@@ -2,6 +2,7 @@
2
2
  * https://github.com/wzpan/hexo-generator-search
3
3
  */
4
4
  const { stripHTML } = require('hexo-util')
5
+ const { normalize_path } = require('../lib/path_utils')
5
6
 
6
7
  hexo.extend.generator.register('search_json_generator', function (locals) {
7
8
  if (this.theme.config.search.service != 'local_search') { return {} }
@@ -28,7 +29,8 @@ hexo.extend.generator.register('search_json_generator', function (locals) {
28
29
  temp_post.title = post.title.trim()
29
30
  }
30
31
  if (post.path) {
31
- temp_post.path = root + post.path
32
+ const path = normalize_path(root + post.path)
33
+ temp_post.path = path === '/' ? '/' : path + '/'
32
34
  }
33
35
  if (cfg.content != false && post.content) {
34
36
  var content = stripHTML(post.content.replace(/<span class="line">\d+<\/span>/g, '')).trim()
@@ -75,7 +75,8 @@ hexo.extend.helper.register('json_ld', function(args) {
75
75
 
76
76
  } else if (isPage || this.is_home()) {
77
77
 
78
- const url = this.is_home() ? config.url : this.pretty_url(page.permalink);
78
+ // 首页 URL 归一为带尾斜杠形式,与 canonical 保持一致
79
+ const url = this.is_home() ? config.url.replace(/\/?$/, '/') : this.pretty_url(page.permalink);
79
80
  schema = {
80
81
  '@context': 'https://schema.org',
81
82
  '@type': 'Website',
@@ -119,4 +120,4 @@ hexo.extend.helper.register('json_ld', function(args) {
119
120
  }
120
121
 
121
122
  return `<script type="application/ld+json">${JSON.stringify(schema)}</script>`;
122
- });
123
+ });
@@ -1,22 +1,21 @@
1
1
  'use strict';
2
2
 
3
+ const { normalize_path } = require('../lib/path_utils');
4
+
5
+ hexo.extend.helper.register('normalize_path', function (path = '') {
6
+ return normalize_path(path);
7
+ });
8
+
3
9
  hexo.extend.helper.register('pretty_url', function (path = '') {
4
10
  if (path.startsWith('http://') || path.startsWith('https://')) {
5
- // 如果是绝对 URL,直接返回
6
11
  return path;
7
12
  }
8
-
9
13
  let url = this.url_for(path);
10
14
 
11
- // 替换 /index.html → /
12
- url = url.replace(/\/index\.html$/, '/');
13
-
14
- // 替换 /about.html → /about/
15
- url = url.replace(/\.html$/, '/');
15
+ url = normalize_path(url);
16
16
 
17
- // 如果没有扩展名,并且不以 / 结尾,补一个 /
18
- const hasExtension = /\.[a-zA-Z0-9]+$/.test(url);
19
- if (!hasExtension && !url.endsWith('/')) {
17
+ // 添加尾 /(根路径 / 除外)
18
+ if (url !== '/') {
20
19
  url += '/';
21
20
  }
22
21
 
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 统一路径规范化:去除 .html 后缀,输出无尾斜杠的干净路径(path_key 格式)
5
+ * 用于路径比较和匹配
6
+ */
7
+ function normalize_path(path = '') {
8
+ if (path.startsWith('http://') || path.startsWith('https://')) {
9
+ return path;
10
+ }
11
+ // /index.html → /
12
+ path = path.replace(/\/index\.html$/, '/');
13
+ // 目录首页(无 .html 后缀形式,如 wiki/stellar/index)→ /
14
+ path = path.replace(/\/index$/, '/');
15
+ // /xxx.html → /xxx
16
+ path = path.replace(/\.html$/, '');
17
+ // 去除尾 /
18
+ if (path.length > 1 && path.endsWith('/')) {
19
+ path = path.slice(0, -1);
20
+ }
21
+ return path;
22
+ }
23
+
24
+ module.exports = { normalize_path };
@@ -14,9 +14,6 @@ if hexo-config('plugins.copycode.enable')
14
14
  @import 'copycode'
15
15
  if hexo-config('plugins.tianli_gpt.enable')
16
16
  @import 'tianli_gpt'
17
- if hexo-config('plugins.pjax.enable')
18
- @import 'pjax'
19
-
20
17
  if hexo-config('plugins.katex.enable')
21
18
  @import 'katex'
22
19
 
package/source/js/main.js CHANGED
@@ -315,7 +315,3 @@ stellar.initPage = function () {
315
315
  stellar.initPage();
316
316
  init.canonicalCheck();
317
317
 
318
- // Listen for PJAX navigation complete
319
- document.addEventListener('pjax:complete', function () {
320
- stellar.initPage();
321
- });
package/CLAUDE.md DELETED
@@ -1,217 +0,0 @@
1
- # Stellar Theme Development
2
-
3
- 这是 Hexo 主题 **Stellar** 的仓库。主题是一个独立的 npm 包,用于 [hexo-theme-stellar](https://github.com/xaoxuu/hexo-theme-stellar)。
4
-
5
- ## 技术栈
6
-
7
- | 层级 | 技术 | 目录 |
8
- |------|------|------|
9
- | 模板引擎 | EJS | `layout/` |
10
- | CSS 预处理 | Stylus | `source/css/` |
11
- | 服务端 JS | CommonJS (ES5) | `scripts/` |
12
- | 浏览器 JS | ES5 (Babel 转译) | `source/js/` |
13
- | 国际化 | YAML | `languages/` |
14
- | 文档 | Markdown | `docs/` |
15
-
16
- ## 目录结构
17
-
18
- ```
19
- layout/ # EJS 模板
20
- ├── _partial/ # 可复用组件
21
- │ ├── main/ # 文章列表、导航、页脚
22
- │ ├── sidebar/ # 侧栏组件
23
- │ ├── cover/ # 封面
24
- │ ├── comments/ # 评论系统
25
- │ ├── widgets/ # 小部件
26
- │ └── scripts/ # 脚本注入
27
- ├── _plugins/ # 可选插件片段
28
- ├── layout.ejs # 根布局
29
- ├── index.ejs # 首页
30
- ├── page.ejs # 通用页面
31
- └── archive.ejs # 归档页
32
- scripts/ # Hexo 服务端脚本
33
- ├── tags/ # 自定义标签 `{% tag %}`
34
- │ └── lib/ # 标签实现
35
- ├── helpers/ # EJS 辅助函数
36
- ├── filters/ # Hexo 过滤器
37
- ├── generators/ # 页面生成器
38
- ├── events/ # 事件处理
39
- └── commands/ # CLI 命令
40
- source/ # 浏览器端资源
41
- ├── css/ # Stylus 样式
42
- │ ├── _defines/ # 变量和函数
43
- │ ├── _common/ # 通用基础样式
44
- │ ├── _components/ # 组件样式
45
- │ └── _plugins/ # 插件样式
46
- └── js/ # 浏览器 JavaScript
47
- ├── plugins/ # 交互插件
48
- ├── services/ # 数据服务
49
- └── search/ # 搜索
50
- ```
51
-
52
- ## EJS 模板规范
53
-
54
- - `<% %>` 逻辑控制,`<%- %>` 输出非转义 HTML
55
- - 变量声明用 `var`(IE8 兼容)
56
- - 2 空格缩进,HTML 属性双引号
57
- - 可复用片段提取到 `_partial/`
58
- - 复杂逻辑提取到 `helpers/` 辅助函数
59
-
60
- ```ejs
61
- <%
62
- var items = site.posts.sort('date', -1).limit(10)
63
- items.forEach(function(post) {
64
- %>
65
- <article>
66
- <%- partial('_partial/main/post_list/post_card', {post: post}) %>
67
- </article>
68
- <%
69
- })
70
- %>
71
- ```
72
-
73
- ## Node.js 脚本规范
74
-
75
- - CommonJS: `require()` / `module.exports`
76
- - 文件头: `/* global hexo */` + `'use strict';`
77
- - 2 空格缩进,双引号,分号结尾
78
- - 标签注册: `hexo.extend.tag.register(name, handler, options)`
79
- - 辅助函数注册: `hexo.extend.helper.register(name, handler)`
80
-
81
- ```js
82
- /* global hexo */
83
- 'use strict';
84
-
85
- module.exports = function(hexo) {
86
- return function(args, content) {
87
- var result = '';
88
- // ...
89
- return result;
90
- };
91
- };
92
- ```
93
-
94
- ## Stylus 样式规范
95
-
96
- - 文件引入顺序: `const` → `custom` → `theme_base` → `theme_colorful` → `func`
97
- - 类名和文件名: `kebab-case`
98
- - 变量在 `_defines/`,通用样式在 `_common/`,组件在 `_components/`
99
- - 2 空格缩进,属性后空格
100
- - CSS 兼容 IE8
101
-
102
- ## 浏览器 JS 规范
103
-
104
- - ES5 语法(Gulp Babel 转译为 ES2015+)
105
- - 避免直接操作 DOM,使用主题工具函数
106
- - 注释: `//` 单行,`/* */` 多行
107
-
108
- ## 工作流程
109
-
110
- 每次修改必须遵循以下流程,产物保留在仓库中:
111
-
112
- ### 1. 方案
113
-
114
- 在 `docs/` 目录下创建或更新方案文档,描述:
115
- - 要解决的问题或新增的能力
116
- - 技术方案和实现思路
117
- - 影响范围(涉及哪些文件/模块)
118
-
119
- ### 2. 执行计划
120
-
121
- 方案通过后,列出具体执行步骤,记录在对应文档中:
122
- - 改动文件清单
123
- - 分步实施顺序
124
- - 依赖关系
125
-
126
- ### 3. 测试
127
-
128
- 变更完成后,在自己的 Hexo 项目中集成验证:
129
- - **`npm run g && npx gulp minify` 全量验证**(`scripts/` 变更必须执行:`npm run g` 发现模板渲染错误,`npx gulp minify` 发现 HTML 结构错误如多余引号等)
130
- - `npm run s` 启动本地服务
131
- - 检查涉及的所有页面类型(首页、文章页、Wiki 页等)
132
- - 验证浏览器兼容性
133
- - 测试结果记录在 `docs/` 中
134
-
135
- ### 4. 文档归档
136
-
137
- - 方案、执行计划、测试记录保存在 `docs/` 目录
138
- - 文件命名: `docs/{YYYY-MM-DD}-{功能简称}.md`
139
- - 涉及逻辑变更(API、配置项、行为变化)必须同步更新仓库 Wiki
140
-
141
- ### 新增功能 Checklist
142
-
143
- 新增功能必须覆盖以下维度:
144
-
145
- 1. `layout/` — EJS 模板
146
- 2. `scripts/` — Hexo 标签 / 辅助函数 / 过滤器
147
- 3. `source/css/` — Stylus 样式
148
- 4. `source/js/` — 浏览器脚本(如需)
149
- 5. `docs/` — 方案 + 执行计划 + 测试记录
150
- 6. `languages/` — 国际化文案(如需新增文本)
151
-
152
- ## 组件架构
153
-
154
- ### 自定义标签开发
155
-
156
- 1. 在 `scripts/tags/lib/` 下创建标签实现文件
157
- 2. 在 `scripts/tags/index.js` 中注册
158
- 3. 在 `source/css/_components/tag-plugins/` 添加对应样式
159
- 4. 推荐使用 `hexo` 参数传入,避免 `require('hexo')`
160
-
161
- ### 评论系统
162
-
163
- - 接口目录: `layout/_partial/comments/`
164
- - 每个评论系统提供 `layout.ejs` 和 `script.ejs`
165
- - 遵循现有的配置驱动模式
166
-
167
- ## Git 规范
168
-
169
- ```
170
- <type>(<scope>): <description>
171
- ```
172
-
173
- | Type | 说明 |
174
- |------|------|
175
- | `feat` | 新功能 |
176
- | `fix` | Bug 修复 |
177
- | `refactor` | 重构 |
178
- | `perf` | 性能优化 |
179
- | `style` | CSS/样式修改 |
180
- | `docs` | 文档更新 |
181
-
182
- ## 发版规范
183
-
184
- 发版分两步:脚本完成版本号更新和推送 → CI 自动完成 npm 发布和 tag 创建。
185
-
186
- ```
187
- npm-publish.sh → push main + npm → CI → npm publish + git tag
188
- ```
189
-
190
- ### 使用方式
191
-
192
- ```bash
193
- # 正常发版(CI 自动处理 npm publish 和 tag)
194
- bash npm-publish.sh 1.33.2
195
-
196
- # 预览模式(仅显示改动,不提交/推送,执行后自动回滚)
197
- bash npm-publish.sh 1.33.2 --dry-run
198
- ```
199
-
200
- ### AI 调用指南
201
-
202
- 1. **分析变更确定版本号**: 查看上一版本以来的 commit,按以下规则确定版本号:
203
- - `x.y.z` → `x.y.(z+1)`: 仅含 fix / perf / style(修复和优化,安全升级)
204
- - `x.y.z` → `x.(y+1).0`: 含 feat / refactor / breaking change(功能增减、一般重构)
205
- - `x.y.z` → `(x+1).0.0`: 大型重构,用户可感知的设计调整
206
- - `x.y.z` → `x.y.z-rc.N`: 测试版本
207
- 2. **向用户确认**: 列出版本号和变更摘要,等待用户确认后再继续
208
- 3. **dry-run 预览**: `bash npm-publish.sh <version> --dry-run` 检查变更是否正确
209
- 4. **正式执行**: `bash npm-publish.sh <version>`
210
- 5. **CI 自动**: 检测 `release:` commit → npm publish + tag
211
-
212
- ## 约束
213
-
214
- - **修改 `scripts/` 目录下任何文件后,必须运行 `npm run g && npx gulp minify` 验证构建成功。** `npm run g` 做全量渲染发现模板错误,`npx gulp minify` 做 HTML 压缩发现结构错误(如多余引号)。`npm run s` 是按需渲染,不能替代全量验证。
215
- - 不引入新构建系统,保持 Hexo 原生 + Gulp 后处理
216
- - 不混用 EJS 和前端框架语法
217
- - CSS 兼容 IE8,JS 兼容 ES2015+